From d69b61d560b7991c1349c4bdd1d4e19298eeee5e Mon Sep 17 00:00:00 2001 From: Justin Davis Date: Mon, 10 Aug 2026 23:08:58 +0000 Subject: [PATCH 1/5] feat: add code for v0.17.0 release --- .agents/guidance/BUGFIX_GUIDELINES.md | 368 + .agents/guidance/MAKE_OP_GUIDELINES.md | 260 + .agents/guidance/OPTIMIZATION_GUIDELINES.md | 646 + .agents/guidance/REFACTOR_OP_GUIDELINES.md | 178 + .agents/guidance/REVIEW_OP_GUIDELINES.md | 199 + .agents/skills/make-op-scaffold/SKILL.md | 18 + .agents/skills/make-op-verify/SKILL.md | 23 + .agents/skills/make-op/SKILL.md | 46 + .agents/skills/optimize-op-preflight/SKILL.md | 16 + .agents/skills/optimize-op-verify/SKILL.md | 31 + .agents/skills/optimize-op/SKILL.md | 64 + .agents/skills/refactor-op/SKILL.md | 42 + .../skills/review-op-bench-coverage/SKILL.md | 17 + .agents/skills/review-op-docs-api/SKILL.md | 17 + .agents/skills/review-op-support/SKILL.md | 16 + .../skills/review-op-test-coverage/SKILL.md | 17 + .agents/skills/review-op/SKILL.md | 32 + .agents/tools/binding_api.py | 263 + .agents/tools/claude_review.sh | 53 + .agents/tools/codex_review.sh | 57 + .agents/tools/cursor_review.sh | 92 + .agents/tools/mr_code_review_prompt.md | 52 + .agents/tools/operator_source_map.py | 64 + .agents/tools/optimization_summary.py | 1961 +++ .agents/tools/review_context.sh | 71 + .agents/tools/review_op_data.json | 46 + .agents/tools/review_worktree.sh | 57 + .../tools/tests/test_optimization_summary.py | 1057 ++ .github/workflows/codeql.yml | 10 +- .gitignore | 32 +- .pre-commit-config.yaml | 9 +- 3rdparty/CMakeLists.txt | 60 - 3rdparty/cuOSD/aarch64/include/cuosd.h | 178 - 3rdparty/cuOSD/aarch64/lib/libcuosd.a | 3 - 3rdparty/cuOSD/x86_64/include/cuosd.h | 178 - 3rdparty/cuOSD/x86_64/lib/libcuosd.a | 3 - 3rdparty/dlpack | 1 - 3rdparty/googletest | 1 - 3rdparty/nvbench | 1 - 3rdparty/pybind11 | 1 - AGENTS.md | 140 + CLAUDE.md | 1 + CMakeLists.txt | 82 +- CMakePresets.json | 75 + DEVELOPER_GUIDE.md | 210 - README.md | 34 +- THIRD_PARTY_LICENSES.md | 879 -- bench/BenchAdaptiveThreshold.cpp | 96 - bench/BenchAdvCvtColor.cpp | 74 - bench/BenchAverageBlur.cpp | 93 - bench/BenchBilateralFilter.cpp | 96 - bench/BenchBndBox.cpp | 97 - bench/BenchBoxBlur.cpp | 97 - bench/BenchBrightnessContrast.cpp | 91 - bench/BenchCenterCrop.cpp | 85 - bench/BenchChannelReorder.cpp | 73 - bench/BenchColorTwist.cpp | 85 - bench/BenchComposite.cpp | 91 - bench/BenchConv2D.cpp | 83 - bench/BenchConvertTo.cpp | 70 - bench/BenchCopyMakeBorder.cpp | 96 - bench/BenchCropFlipNormalizeReformat.cpp | 116 - bench/BenchCustomCrop.cpp | 70 - bench/BenchCvtColor.cpp | 183 - bench/BenchErase.cpp | 95 - bench/BenchFindHomography.cpp | 139 - bench/BenchFlip.cpp | 99 - bench/BenchGammaContrast.cpp | 75 - bench/BenchGaussian.cpp | 96 - bench/BenchGaussianNoise.cpp | 87 - bench/BenchHQResize.cpp | 129 - bench/BenchHistogram.cpp | 71 - bench/BenchHistogramEq.cpp | 77 - bench/BenchInpaint.cpp | 85 - bench/BenchJointBilateralFilter.cpp | 100 - bench/BenchLabel.cpp | 113 - bench/BenchLaplacian.cpp | 91 - bench/BenchMedianBlur.cpp | 86 - bench/BenchMinAreaRect.cpp | 70 - bench/BenchMinMaxLoc.cpp | 92 - bench/BenchMinMaxLoc.hpp | 121 - bench/BenchMorphology.cpp | 135 - bench/BenchNMS.cpp | 76 - bench/BenchNormalize.cpp | 99 - bench/BenchOSD.cpp | 94 - bench/BenchPadAndStack.cpp | 82 - bench/BenchPillowResize.cpp | 105 - bench/BenchRandomResizedCrop.cpp | 103 - bench/BenchReformat.cpp | 67 - bench/BenchRemap.cpp | 120 - bench/BenchResize.cpp | 97 - bench/BenchResizeCropConvertReformat.cpp | 124 - bench/BenchRotate.cpp | 91 - bench/BenchStack.cpp | 68 - bench/BenchThreshold.cpp | 85 - bench/BenchUtils.hpp | 324 - bench/BenchWarpAffine.cpp | 95 - bench/BenchWarpPerspective.cpp | 95 - bench/CMakeLists.txt | 152 +- bench/README.md | 414 + bench/_internal/__init__.py | 4 + bench/_internal/axes.py | 50 + bench/_internal/baselines.py | 1397 ++ bench/_internal/check_bench_sku.py | 201 + bench/_internal/quality.py | 72 + bench/_internal/update_baseline.py | 535 + bench/_internal/validate_baselines.py | 367 + bench/_internal/warmup.py | 38 + bench/compare_to_baseline.py | 598 + bench/compare_wheels.py | 1207 ++ bench/config/axis_order.py | 52 + bench/config/bench_params.json | 309 + bench/config/load_config.py | 752 + bench/config/operator_categories.json | 203 + bench/config/operators/adaptivethreshold.json | 938 ++ bench/config/operators/adjustcontrast.json | 570 + bench/config/operators/adjusthue.json | 390 + bench/config/operators/adjustsaturation.json | 390 + bench/config/operators/adjustsharpness.json | 773 + bench/config/operators/advcvtcolor.json | 2882 ++++ bench/config/operators/autocontrast.json | 538 + bench/config/operators/averageblur.json | 1524 ++ bench/config/operators/bilateralfilter.json | 1340 ++ bench/config/operators/bndbox.json | 505 + bench/config/operators/boxblur.json | 1071 ++ .../config/operators/brightnesscontrast.json | 1073 ++ bench/config/operators/centercrop.json | 1226 ++ bench/config/operators/channelreorder.json | 934 ++ bench/config/operators/clahe.json | 771 + bench/config/operators/colortwist.json | 1190 ++ bench/config/operators/composite.json | 674 + bench/config/operators/conv2d.json | 558 + bench/config/operators/convertto.json | 1943 +++ bench/config/operators/copymakeborder.json | 1432 ++ .../operators/cropflipnormalizereformat.json | 1256 ++ bench/config/operators/customcrop.json | 1274 ++ bench/config/operators/cvtcolor.json | 1375 ++ bench/config/operators/erase.json | 2341 +++ bench/config/operators/findhomography.json | 285 + bench/config/operators/flip.json | 1262 ++ bench/config/operators/gammacontrast.json | 953 ++ bench/config/operators/gaussian.json | 2472 +++ bench/config/operators/gaussiannoise.json | 1315 ++ bench/config/operators/histogram.json | 314 + bench/config/operators/histogrameq.json | 770 + bench/config/operators/hqresize.json | 5458 +++++++ bench/config/operators/inpaint.json | 1185 ++ bench/config/operators/invert.json | 773 + .../operators/jointbilateralfilter.json | 1116 ++ .../operators/jpegcompressiondistortion.json | 254 + bench/config/operators/label.json | 486 + bench/config/operators/laplacian.json | 1834 +++ bench/config/operators/medianblur.json | 1202 ++ bench/config/operators/minarearect.json | 192 + bench/config/operators/minmaxloc.json | 643 + bench/config/operators/morphology.json | 3193 ++++ .../operators/nonmaximumsuppression.json | 255 + bench/config/operators/normalize.json | 1764 +++ bench/config/operators/osd.json | 506 + bench/config/operators/padandstack.json | 717 + bench/config/operators/pairwisematcher.json | 439 + bench/config/operators/pillowresize.json | 13162 ++++++++++++++++ bench/config/operators/posterize.json | 344 + bench/config/operators/randomresizedcrop.json | 1191 ++ bench/config/operators/reformat.json | 381 + bench/config/operators/remap.json | 721 + bench/config/operators/resize.json | 11420 ++++++++++++++ .../operators/resizecropconvertreformat.json | 263 + bench/config/operators/rotate.json | 1236 ++ bench/config/operators/sift.json | 365 + bench/config/operators/solarize.json | 683 + bench/config/operators/stack.json | 820 + bench/config/operators/threshold.json | 322 + bench/config/operators/warpaffine.json | 1414 ++ bench/config/operators/warpperspective.json | 1383 ++ bench/config/sku_map.json | 16 + bench/cpp/.gitignore | 5 + bench/cpp/BenchFillKernels.cu | 180 + bench/cpp/BenchFillKernels.hpp | 94 + bench/cpp/CMakeLists.txt | 219 + bench/cpp/CppBenchUtils.hpp | 916 ++ bench/cpp/GenerateBenchConfig.cmake | 337 + bench/cpp/WarmupPolicy.hpp | 61 + bench/cpp/ops/BenchAdaptiveThreshold.cpp | 134 + bench/cpp/ops/BenchAdjustContrast.cpp | 135 + bench/cpp/ops/BenchAdjustHue.cpp | 135 + bench/cpp/ops/BenchAdjustSaturation.cpp | 135 + bench/cpp/ops/BenchAdjustSharpness.cpp | 135 + bench/cpp/ops/BenchAdvCvtColor.cpp | 250 + bench/cpp/ops/BenchAutoContrast.cpp | 136 + bench/cpp/ops/BenchAverageBlur.cpp | 135 + bench/cpp/ops/BenchBilateralFilter.cpp | 136 + bench/cpp/ops/BenchBndBox.cpp | 144 + bench/cpp/ops/BenchBoxBlur.cpp | 172 + bench/cpp/ops/BenchBrightnessContrast.cpp | 152 + bench/cpp/ops/BenchCLAHE.cpp | 113 + bench/cpp/ops/BenchCenterCrop.cpp | 131 + bench/cpp/ops/BenchChannelReorder.cpp | 259 + bench/cpp/ops/BenchColorTwist.cpp | 150 + bench/cpp/ops/BenchComposite.cpp | 326 + bench/cpp/ops/BenchConv2D.cpp | 108 + bench/cpp/ops/BenchConvertTo.cpp | 185 + bench/cpp/ops/BenchCopyMakeBorder.cpp | 152 + .../ops/BenchCropFlipNormalizeReformat.cpp | 167 + bench/cpp/ops/BenchCustomCrop.cpp | 125 + bench/cpp/ops/BenchCvtColor.cpp | 399 + bench/cpp/ops/BenchErase.cpp | 198 + bench/cpp/ops/BenchFindHomography.cpp | 164 + bench/cpp/ops/BenchFlip.cpp | 156 + bench/cpp/ops/BenchGammaContrast.cpp | 159 + bench/cpp/ops/BenchGaussian.cpp | 233 + bench/cpp/ops/BenchGaussianNoise.cpp | 132 + bench/cpp/ops/BenchHQResize.cpp | 302 + bench/cpp/ops/BenchHistogram.cpp | 260 + bench/cpp/ops/BenchHistogramEq.cpp | 124 + bench/cpp/ops/BenchInpaint.cpp | 237 + bench/cpp/ops/BenchInvert.cpp | 132 + bench/cpp/ops/BenchJointBilateralFilter.cpp | 142 + .../ops/BenchJpegCompressionDistortion.cpp | 146 + bench/cpp/ops/BenchLabel.cpp | 269 + bench/cpp/ops/BenchLaplacian.cpp | 133 + bench/cpp/ops/BenchMedianBlur.cpp | 210 + bench/cpp/ops/BenchMinAreaRect.cpp | 73 + bench/cpp/ops/BenchMinMaxLoc.cpp | 188 + bench/cpp/ops/BenchMorphology.cpp | 281 + bench/cpp/ops/BenchNonMaximumSuppression.cpp | 71 + bench/cpp/ops/BenchNormalize.cpp | 211 + bench/cpp/ops/BenchOSD.cpp | 194 + bench/cpp/ops/BenchPadAndStack.cpp | 125 + bench/{ => cpp/ops}/BenchPairwiseMatcher.cpp | 52 +- bench/cpp/ops/BenchPillowResize.cpp | 150 + bench/cpp/ops/BenchPosterize.cpp | 137 + bench/cpp/ops/BenchRandomResizedCrop.cpp | 157 + bench/cpp/ops/BenchReformat.cpp | 73 + bench/cpp/ops/BenchRemap.cpp | 162 + bench/cpp/ops/BenchResize.cpp | 145 + .../ops/BenchResizeCropConvertReformat.cpp | 169 + bench/cpp/ops/BenchRotate.cpp | 232 + bench/{ => cpp/ops}/BenchSIFT.cpp | 71 +- bench/cpp/ops/BenchSolarize.cpp | 139 + bench/cpp/ops/BenchStack.cpp | 166 + bench/cpp/ops/BenchThreshold.cpp | 135 + bench/cpp/ops/BenchWarpAffine.cpp | 149 + bench/cpp/ops/BenchWarpPerspective.cpp | 144 + bench/python/CMakeLists.txt | 116 + bench/python/README.md | 280 - bench/python/all_ops/op_adaptivethreshold.py | 42 - bench/python/all_ops/op_as_image.py | 39 - bench/python/all_ops/op_as_images.py | 43 - bench/python/all_ops/op_averageblur.py | 33 - bench/python/all_ops/op_blurbox.py | 81 - bench/python/all_ops/op_boundingbox.py | 85 - bench/python/all_ops/op_brightnesscontrast.py | 47 - bench/python/all_ops/op_cache_limit.py | 114 - bench/python/all_ops/op_centercrop.py | 34 - bench/python/all_ops/op_composite.py | 53 - bench/python/all_ops/op_convertto.py | 35 - bench/python/all_ops/op_copymakeborder.py | 43 - bench/python/all_ops/op_customcrop.py | 30 - bench/python/all_ops/op_cvtcolor.py | 37 - bench/python/all_ops/op_flip.py | 48 - bench/python/all_ops/op_gaussianblur.py | 31 - bench/python/all_ops/op_hqresize.py | 55 - bench/python/all_ops/op_inpaint.py | 57 - bench/python/all_ops/op_jointbilateral.py | 56 - bench/python/all_ops/op_laplacian.py | 31 - bench/python/all_ops/op_morphology.py | 106 - bench/python/all_ops/op_nms.py | 53 - bench/python/all_ops/op_normalize.py | 46 - bench/python/all_ops/op_randomresizedcrop.py | 45 - bench/python/all_ops/op_reformat.py | 44 - bench/python/all_ops/op_remap.py | 54 - bench/python/all_ops/op_reshape.py | 33 - bench/python/all_ops/op_resize.py | 59 - .../op_resize_crop_convert_reformat.py | 44 - bench/python/all_ops/op_rotate.py | 32 - bench/python/all_ops/op_sift.py | 48 - bench/python/all_ops/op_threshold.py | 43 - bench/python/all_ops/op_warpaffine.py | 62 - bench/python/all_ops/op_warpperspective.py | 72 - bench/python/assets/NOTICE.md | 19 - bench/python/assets/brooklyn.jpg | 3 - bench/python/assets/brooklyn_bboxes.pt | 3 - bench/python/assets/brooklyn_mask.jpg | 3 - bench/python/assets/brooklyn_nms_masks.pt | 3 - bench/python/assets/brooklyn_scores.pt | 3 - bench/python/assets/countour_lines.jpg | 3 - bench/python/batch.py | 45 - bench/python/bench_utils.py | 417 - bench/python/benchmark.py | 1576 -- bench/python/install_bench_dependencies.sh | 180 + bench/python/install_dependencies.sh | 160 - bench/python/nvcodec_utils.py | 635 - bench/python/ops/bench_adaptivethreshold.py | 168 + bench/python/ops/bench_adjustcontrast.py | 137 + bench/python/ops/bench_adjusthue.py | 134 + bench/python/ops/bench_adjustsaturation.py | 136 + bench/python/ops/bench_adjustsharpness.py | 138 + bench/python/ops/bench_advcvtcolor.py | 191 + bench/python/ops/bench_autocontrast.py | 141 + bench/python/ops/bench_averageblur.py | 188 + bench/python/ops/bench_bilateralfilter.py | 195 + bench/python/ops/bench_bndbox.py | 123 + bench/python/ops/bench_boxblur.py | 137 + bench/python/ops/bench_brightnesscontrast.py | 173 + bench/python/ops/bench_centercrop.py | 144 + bench/python/ops/bench_channelreorder.py | 158 + bench/python/ops/bench_clahe.py | 174 + bench/python/ops/bench_colortwist.py | 155 + bench/python/ops/bench_composite.py | 215 + bench/python/ops/bench_conv2d.py | 113 + bench/python/ops/bench_convertto.py | 139 + bench/python/ops/bench_copymakeborder.py | 197 + .../ops/bench_cropflipnormalizereformat.py | 144 + bench/python/ops/bench_customcrop.py | 140 + bench/python/ops/bench_cvtcolor.py | 308 + bench/python/ops/bench_erase.py | 255 + bench/python/ops/bench_findhomography.py | 153 + bench/python/ops/bench_flip.py | 146 + bench/python/ops/bench_gammacontrast.py | 166 + bench/python/ops/bench_gaussian.py | 176 + bench/python/ops/bench_gaussiannoise.py | 159 + bench/python/ops/bench_histogram.py | 156 + bench/python/ops/bench_histogrameq.py | 146 + bench/python/ops/bench_hqresize.py | 230 + bench/python/ops/bench_inpaint.py | 179 + bench/python/ops/bench_invert.py | 129 + .../python/ops/bench_jointbilateralfilter.py | 215 + .../ops/bench_jpegcompressiondistortion.py | 142 + bench/python/ops/bench_label.py | 179 + bench/python/ops/bench_laplacian.py | 179 + bench/python/ops/bench_medianblur.py | 170 + bench/python/ops/bench_minarearect.py | 102 + bench/python/ops/bench_minmaxloc.py | 182 + bench/python/ops/bench_morphology.py | 251 + .../python/ops/bench_nonmaximumsuppression.py | 87 + bench/python/ops/bench_normalize.py | 243 + bench/python/ops/bench_osd.py | 161 + bench/python/ops/bench_padandstack.py | 136 + bench/python/ops/bench_pairwisematcher.py | 95 + bench/python/ops/bench_pillowresize.py | 178 + bench/python/ops/bench_posterize.py | 131 + bench/python/ops/bench_randomresizedcrop.py | 173 + bench/python/ops/bench_reformat.py | 86 + bench/python/ops/bench_remap.py | 167 + bench/python/ops/bench_resize.py | 146 + .../ops/bench_resizecropconvertreformat.py | 188 + bench/python/ops/bench_rotate.py | 222 + bench/python/ops/bench_sift.py | 106 + bench/python/ops/bench_solarize.py | 132 + bench/python/ops/bench_stack.py | 133 + bench/python/ops/bench_threshold.py | 149 + bench/python/ops/bench_warpaffine.py | 178 + bench/python/ops/bench_warpperspective.py | 196 + bench/python/python_bench_utils.py | 1123 ++ .../python/requirements.bench.common.template | 43 + bench/python/requirements.bench.cu12.template | 32 + bench/python/requirements.bench.cu13.template | 32 + bench/python/requirements_common.txt | 31 - bench/python/run_bench.py | 234 - bench/run_bench.py | 2630 ++- .../tests/conftest.py | 18 +- bench/tests/test_axis_display.py | 43 + bench/tests/test_bench_cli_surface.py | 48 + bench/tests/test_bench_rgb_guidelines.py | 297 + bench/tests/test_benchmark_state_order.py | 208 + bench/tests/test_channelreorder_benchmark.py | 71 + bench/tests/test_check_bench_sku.py | 116 + bench/tests/test_compare_to_baseline.py | 348 + bench/tests/test_compare_wheels.py | 590 + .../test_histogrameq_benchmark_isolation.py | 56 + bench/tests/test_normalize_benchmark.py | 77 + bench/tests/test_python_bench_fill.py | 136 + .../tests/test_python_bench_state_lifetime.py | 112 + bench/tests/test_run_bench_config_key.py | 1227 ++ bench/tests/test_update_baseline.py | 656 + bench/tests/test_validate_baselines.py | 519 + bench/tests/test_warmup_policy.py | 126 + ci/build.sh => build.sh | 25 +- cmake/BuildPython.cmake | 129 +- cmake/CUDAArchitecturePolicy.cmake | 232 + cmake/ConfigBuildTree.cmake | 19 +- cmake/ConfigCPack.cmake | 4 + cmake/ConfigCUDA.cmake | 78 +- cmake/ConfigCompiler.cmake | 4 +- cmake/ConfigVersion.cmake | 24 +- cmake/InstallBench.cmake | 30 + cmake/InstallSamples.cmake | 4 +- cmake/InstallTests.cmake | 4 +- cmake/PrintConfig.cmake | 4 +- cmake/VersionDef.h.in | 4 +- docker/Dockerfile.builder.deps | 196 +- docker/Dockerfile.cuda12.5.0.deps | 39 - docker/Dockerfile.cuda12.9.0.deps | 39 - docker/Dockerfile.cuda13.0.1.deps | 39 - docker/Dockerfile.devel.deps | 370 +- docker/README.md | 66 +- docker/build_dockers.sh | 232 +- .../requirements.build.all_pythons.template | 23 +- docker/requirements.build.sys_python.template | 28 + docker/requirements.no_torch_no_numpy.txt | 25 - docker/requirements.sys_python.txt | 51 - docs/CMakeLists.txt | 33 +- docs/Doxyfile.in | 8 +- {ci => docs}/build_docs.sh | 12 +- docs/requirements.docs.template | 35 + docs/sphinx/advanced/make_operator.rst | 371 +- docs/sphinx/advanced/object_cache.rst | 4 +- docs/sphinx/advanced/operator_variants.rst | 107 + docs/sphinx/conf.py | 33 +- docs/sphinx/content/cat_adaptivethreshold.jpg | 3 + docs/sphinx/content/cat_advcvtcolor.jpg | 3 + docs/sphinx/content/cat_averageblur.jpg | 3 + docs/sphinx/content/cat_bilateral_filter.jpg | 3 + docs/sphinx/content/cat_bndbox.jpg | 3 + docs/sphinx/content/cat_boxblur.jpg | 3 + .../content/cat_brightness_contrast.jpg | 3 + docs/sphinx/content/cat_center_crop.jpg | 3 + docs/sphinx/content/cat_channelreorder.jpg | 3 + docs/sphinx/content/cat_clahe.jpg | 3 + docs/sphinx/content/cat_color_twist.jpg | 3 + docs/sphinx/content/cat_composite.jpg | 3 + docs/sphinx/content/cat_conv2d.jpg | 3 + docs/sphinx/content/cat_convertto.jpg | 3 + docs/sphinx/content/cat_copymakeborder.jpg | 3 + .../cat_crop_flip_normalize_reformat.jpg | 3 + docs/sphinx/content/cat_customcrop.jpg | 3 + docs/sphinx/content/cat_cvtcolor.jpg | 3 + docs/sphinx/content/cat_erase.jpg | 3 + docs/sphinx/content/cat_flip.jpg | 3 + docs/sphinx/content/cat_gamma_contrast.jpg | 3 + docs/sphinx/content/cat_gaussiannoise.jpg | 3 + docs/sphinx/content/cat_histogrameq.jpg | 3 + docs/sphinx/content/cat_hq_resize.jpg | 3 + docs/sphinx/content/cat_inpaint.jpg | 3 + docs/sphinx/content/cat_inpaint_damaged.jpg | 3 + .../content/cat_joint_bilateral_filter.jpg | 3 + docs/sphinx/content/cat_labeled.jpg | 4 +- docs/sphinx/content/cat_laplacian.jpg | 3 + docs/sphinx/content/cat_median_blur.jpg | 3 + docs/sphinx/content/cat_morphology.jpg | 3 + docs/sphinx/content/cat_normalize.jpg | 3 + docs/sphinx/content/cat_osd.jpg | 3 + docs/sphinx/content/cat_pillowresize.jpg | 3 + .../content/cat_random_resized_crop.jpg | 3 + docs/sphinx/content/cat_remap.jpg | 3 + .../cat_resize_crop_convert_reformat.jpg | 3 + docs/sphinx/content/cat_rotate.jpg | 3 + docs/sphinx/content/cat_threshold.jpg | 3 + docs/sphinx/content/cat_warp_affine.jpg | 3 + docs/sphinx/content/cat_warp_perspective.jpg | 3 + docs/sphinx/cvcuda_limitations_ext.py | 413 + docs/sphinx/docker_images.rst | 149 +- docs/sphinx/generate_groups.py | 86 +- docs/sphinx/getting_started.rst | 21 +- docs/sphinx/index.rst | 10 +- docs/sphinx/installation.rst | 124 +- docs/sphinx/interop/cupy.rst | 58 - docs/sphinx/interop/pytorch.rst | 60 - docs/sphinx/modules/c_api.rst | 52 - .../modules/{cpp_api.rst => c_cpp_api.rst} | 38 +- docs/sphinx/modules/c_status.rst | 25 + .../sphinx/modules/python/auxiliary_types.rst | 10 +- docs/sphinx/modules/python/data_types.rst | 10 +- docs/sphinx/modules/python/index.rst | 10 - docs/sphinx/modules/python/operators.rst | 262 +- .../cvcuda_op.rst => operator_list.rst} | 30 +- docs/sphinx/perf_benchmark.rst | 4 +- docs/sphinx/relnotes/relnotes-template.md | 44 + docs/sphinx/relnotes/v0.11.0-beta.rst | 130 +- docs/sphinx/relnotes/v0.13.0-beta.rst | 130 +- docs/sphinx/relnotes/v0.16.0-beta.rst | 4 +- docs/sphinx/relnotes/v0.17.0-beta.rst | 151 + docs/sphinx/samples.rst | 88 +- .../samples/applications/classification.rst | 2 +- .../samples/applications/hello_world.rst | 4 +- .../samples/applications/object_detection.rst | 4 +- .../samples/applications/segmentation.rst | 2 +- docs/sphinx/samples/common.rst | 1 + .../sphinx/{ => samples}/interoperability.rst | 34 +- .../interoperability}/cuda_python.rst | 26 +- docs/sphinx/samples/interoperability/cupy.rst | 106 + .../interoperability}/numpy.rst | 12 +- .../interoperability}/nvimgcodec.rst | 12 +- .../interoperability}/pycuda.rst | 8 +- .../interoperability}/pynvvideocodec.rst | 16 +- .../samples/interoperability/pytorch.rst | 102 + docs/sphinx/samples/operators.rst | 99 +- .../samples/operators/adaptivethreshold.rst | 138 + docs/sphinx/samples/operators/advcvtcolor.rst | 141 + docs/sphinx/samples/operators/averageblur.rst | 131 + .../samples/operators/bilateral_filter.rst | 139 + docs/sphinx/samples/operators/bndbox.rst | 133 + docs/sphinx/samples/operators/boxblur.rst | 141 + .../samples/operators/brightness_contrast.rst | 150 + docs/sphinx/samples/operators/center_crop.rst | 146 + .../samples/operators/channelreorder.rst | 147 + docs/sphinx/samples/operators/clahe.rst | 149 + docs/sphinx/samples/operators/color_twist.rst | 136 + docs/sphinx/samples/operators/composite.rst | 137 + docs/sphinx/samples/operators/conv2d.rst | 144 + docs/sphinx/samples/operators/convertto.rst | 137 + .../samples/operators/copymakeborder.rst | 138 + .../crop_flip_normalize_reformat.rst | 147 + docs/sphinx/samples/operators/customcrop.rst | 142 + docs/sphinx/samples/operators/cvtcolor.rst | 140 + docs/sphinx/samples/operators/erase.rst | 152 + docs/sphinx/samples/operators/flip.rst | 129 + .../samples/operators/gamma_contrast.rst | 143 + .../samples/operators/gaussiannoise.rst | 133 + docs/sphinx/samples/operators/histogrameq.rst | 138 + docs/sphinx/samples/operators/hq_resize.rst | 147 + docs/sphinx/samples/operators/inpaint.rst | 144 + .../operators/joint_bilateral_filter.rst | 143 + docs/sphinx/samples/operators/laplacian.rst | 141 + docs/sphinx/samples/operators/median_blur.rst | 131 + docs/sphinx/samples/operators/morphology.rst | 133 + docs/sphinx/samples/operators/normalize.rst | 150 + docs/sphinx/samples/operators/osd.rst | 149 + .../sphinx/samples/operators/pillowresize.rst | 142 + .../samples/operators/random_resized_crop.rst | 150 + docs/sphinx/samples/operators/reformat.rst | 2 +- docs/sphinx/samples/operators/remap.rst | 147 + docs/sphinx/samples/operators/resize.rst | 4 +- .../resize_crop_convert_reformat.rst | 159 + docs/sphinx/samples/operators/rotate.rst | 135 + docs/sphinx/samples/operators/stack.rst | 2 +- docs/sphinx/samples/operators/threshold.rst | 140 + docs/sphinx/samples/operators/warp_affine.rst | 145 + .../samples/operators/warp_perspective.rst | 145 + docs/sphinx/wsl2.rst | 6 +- generate_requirements.sh | 128 + init_repo.sh | 17 +- print_env.sh | 336 +- python/CMakeLists.txt | 47 +- python/MANIFEST.in | 6 +- python/README.md.in | 6 +- python/__init__.py.in | 2 +- python/build_wheels.sh | 79 +- python/common/Assert.hpp | 18 +- python/common/CheckError.cpp | 15 +- python/common/Hash.hpp | 4 +- python/common/PyUtil.cpp | 29 +- python/common/PyUtil.hpp | 4 +- python/common/String.cpp | 23 +- python/common/String.hpp | 17 +- python/cpack/debian_python_postinst.in | 2 +- python/mod_cvcuda/AdaptiveThresholdType.cpp | 10 +- python/mod_cvcuda/BorderType.cpp | 4 +- python/mod_cvcuda/CMakeLists.txt | 32 +- python/mod_cvcuda/ChannelManipType.cpp | 4 +- python/mod_cvcuda/ColorConversionCode.cpp | 9 +- python/mod_cvcuda/ConnectivityType.cpp | 14 +- python/mod_cvcuda/CvtColorUtil.cpp | 150 +- python/mod_cvcuda/CvtColorUtil.hpp | 2 +- python/mod_cvcuda/InterpolationType.cpp | 4 +- python/mod_cvcuda/LabelType.cpp | 9 +- python/mod_cvcuda/Main.cpp | 44 +- python/mod_cvcuda/MorphologyType.cpp | 12 +- python/mod_cvcuda/NormType.cpp | 4 +- python/mod_cvcuda/NvtxRange.hpp | 88 + python/mod_cvcuda/OsdElement.cpp | 188 +- python/mod_cvcuda/PairwiseMatcherType.cpp | 5 +- python/mod_cvcuda/RemapMapValueType.cpp | 12 +- python/mod_cvcuda/RoundMode.cpp | 31 + python/mod_cvcuda/RoundMode.hpp | 30 + python/mod_cvcuda/SIFTFlagType.cpp | 9 +- python/mod_cvcuda/ThresholdType.cpp | 5 +- python/mod_cvcuda/WorkspaceCache.cpp | 83 +- python/mod_cvcuda/WorkspaceCache.hpp | 72 +- python/mod_cvcuda/gen_dtype_list.sh | 4 +- python/mod_cvcuda/gen_imgformat_list.sh | 4 +- .../mod_cvcuda/include/nvcv/python/Array.hpp | 9 +- .../mod_cvcuda/include/nvcv/python/CAPI.hpp | 34 +- .../mod_cvcuda/include/nvcv/python/Cache.hpp | 30 +- .../include/nvcv/python/Container.hpp | 4 +- .../include/nvcv/python/DataType.hpp | 2 +- .../mod_cvcuda/include/nvcv/python/Image.hpp | 8 +- .../nvcv/python/ImageBatchVarShape.hpp | 29 +- .../include/nvcv/python/ImageFormat.hpp | 2 +- .../include/nvcv/python/Resource.hpp | 10 +- .../include/nvcv/python/ResourceGuard.hpp | 213 +- .../mod_cvcuda/include/nvcv/python/Shape.hpp | 15 +- .../mod_cvcuda/include/nvcv/python/Size.hpp | 2 +- .../mod_cvcuda/include/nvcv/python/Stream.hpp | 7 +- .../mod_cvcuda/include/nvcv/python/Tensor.hpp | 23 +- .../include/nvcv/python/TensorBatch.hpp | 19 +- python/mod_cvcuda/nvcv/Array.cpp | 76 +- python/mod_cvcuda/nvcv/Array.hpp | 17 +- python/mod_cvcuda/nvcv/CAPI.cpp | 431 +- python/mod_cvcuda/nvcv/CAPI.hpp | 7 +- python/mod_cvcuda/nvcv/Cache.cpp | 242 +- python/mod_cvcuda/nvcv/Cache.hpp | 44 +- python/mod_cvcuda/nvcv/Container.cpp | 12 +- python/mod_cvcuda/nvcv/Container.hpp | 29 +- python/mod_cvcuda/nvcv/DLPackUtils.cpp | 133 +- python/mod_cvcuda/nvcv/DataType.cpp | 238 +- python/mod_cvcuda/nvcv/DataType.hpp | 2 +- python/mod_cvcuda/nvcv/ExternalBuffer.cpp | 744 +- python/mod_cvcuda/nvcv/ExternalBuffer.hpp | 122 +- python/mod_cvcuda/nvcv/Image.cpp | 800 +- python/mod_cvcuda/nvcv/Image.hpp | 18 +- python/mod_cvcuda/nvcv/ImageBatch.cpp | 63 +- python/mod_cvcuda/nvcv/ImageBatch.hpp | 14 +- python/mod_cvcuda/nvcv/ImageFormat.cpp | 27 +- python/mod_cvcuda/nvcv/Object.cpp | 7 +- python/mod_cvcuda/nvcv/Resource.cpp | 221 +- python/mod_cvcuda/nvcv/Resource.hpp | 89 +- python/mod_cvcuda/nvcv/Stream.cpp | 545 +- python/mod_cvcuda/nvcv/Stream.hpp | 66 +- python/mod_cvcuda/nvcv/StreamStack.cpp | 4 +- python/mod_cvcuda/nvcv/Tensor.cpp | 218 +- python/mod_cvcuda/nvcv/Tensor.hpp | 24 +- python/mod_cvcuda/nvcv/TensorBatch.cpp | 53 +- python/mod_cvcuda/nvcv/TensorBatch.hpp | 17 +- .../operators/OpAdaptiveThreshold.cpp | 95 +- .../mod_cvcuda/operators/OpAdjustContrast.cpp | 148 + python/mod_cvcuda/operators/OpAdjustHue.cpp | 143 + .../operators/OpAdjustSaturation.cpp | 144 + .../operators/OpAdjustSharpness.cpp | 145 + python/mod_cvcuda/operators/OpAdvCvtColor.cpp | 29 +- .../mod_cvcuda/operators/OpAutoContrast.cpp | 184 + python/mod_cvcuda/operators/OpAverageBlur.cpp | 78 +- .../operators/OpBilateralFilter.cpp | 90 +- python/mod_cvcuda/operators/OpBndBox.cpp | 39 +- python/mod_cvcuda/operators/OpBoxBlur.cpp | 39 +- .../operators/OpBrightnessContrast.cpp | 210 +- python/mod_cvcuda/operators/OpCLAHE.cpp | 181 + python/mod_cvcuda/operators/OpCenterCrop.cpp | 28 +- .../mod_cvcuda/operators/OpChannelReorder.cpp | 97 +- python/mod_cvcuda/operators/OpColorTwist.cpp | 53 +- python/mod_cvcuda/operators/OpComposite.cpp | 122 +- python/mod_cvcuda/operators/OpConv2D.cpp | 40 +- python/mod_cvcuda/operators/OpConvertTo.cpp | 45 +- .../mod_cvcuda/operators/OpCopyMakeBorder.cpp | 218 +- .../operators/OpCropFlipNormalizeReformat.cpp | 56 +- python/mod_cvcuda/operators/OpCustomCrop.cpp | 28 +- python/mod_cvcuda/operators/OpCvtColor.cpp | 160 +- python/mod_cvcuda/operators/OpErase.cpp | 167 +- .../mod_cvcuda/operators/OpFindHomography.cpp | 229 +- python/mod_cvcuda/operators/OpFlip.cpp | 78 +- .../mod_cvcuda/operators/OpGammaContrast.cpp | 179 +- python/mod_cvcuda/operators/OpGaussian.cpp | 77 +- .../mod_cvcuda/operators/OpGaussianNoise.cpp | 163 +- python/mod_cvcuda/operators/OpHQResize.cpp | 547 +- python/mod_cvcuda/operators/OpHistogram.cpp | 53 +- python/mod_cvcuda/operators/OpHistogramEq.cpp | 89 +- python/mod_cvcuda/operators/OpInpaint.cpp | 129 +- python/mod_cvcuda/operators/OpInvert.cpp | 136 + .../operators/OpJointBilateralFilter.cpp | 98 +- .../operators/OpJpegCompressionDistortion.cpp | 320 + python/mod_cvcuda/operators/OpLabel.cpp | 58 +- python/mod_cvcuda/operators/OpLaplacian.cpp | 76 +- python/mod_cvcuda/operators/OpMedianBlur.cpp | 77 +- python/mod_cvcuda/operators/OpMinAreaRect.cpp | 43 +- python/mod_cvcuda/operators/OpMinMaxLoc.cpp | 222 +- python/mod_cvcuda/operators/OpMorphology.cpp | 128 +- .../operators/OpNonMaximumSuppression.cpp | 26 +- python/mod_cvcuda/operators/OpNormalize.cpp | 220 +- python/mod_cvcuda/operators/OpOSD.cpp | 38 +- python/mod_cvcuda/operators/OpPadAndStack.cpp | 41 +- .../operators/OpPairwiseMatcher.cpp | 56 +- .../mod_cvcuda/operators/OpPillowResize.cpp | 142 +- python/mod_cvcuda/operators/OpPosterize.cpp | 145 + .../operators/OpRandomResizedCrop.cpp | 103 +- python/mod_cvcuda/operators/OpReformat.cpp | 27 +- python/mod_cvcuda/operators/OpRemap.cpp | 112 +- python/mod_cvcuda/operators/OpResize.cpp | 90 +- .../operators/OpResizeCropConvertReformat.cpp | 172 +- python/mod_cvcuda/operators/OpRotate.cpp | 91 +- python/mod_cvcuda/operators/OpSIFT.cpp | 71 +- python/mod_cvcuda/operators/OpSolarize.cpp | 143 + python/mod_cvcuda/operators/OpStack.cpp | 233 +- python/mod_cvcuda/operators/OpThreshold.cpp | 93 +- python/mod_cvcuda/operators/OpWarpAffine.cpp | 115 +- .../operators/OpWarpPerspective.cpp | 114 +- python/mod_cvcuda/operators/Operators.hpp | 35 +- .../operators/UnaryElementwiseOp.hpp | 85 + python/mod_cvcuda/operators/VarShapeUtils.hpp | 87 + python/py.typed | 2 + python/pyproject.toml.in | 10 +- python/setup.py.in | 14 +- samples/CMakeLists.txt | 80 + samples/README.md | 23 +- samples/applications/hello_world.py | 4 +- samples/common.py | 162 +- samples/install_interop_dependencies.sh | 71 - samples/install_samples_dependencies.sh | 50 +- .../pynvvideocodec_interop.py | 52 +- samples/object_cache/threads.py | 27 +- samples/operators/adaptivethreshold.py | 81 + samples/operators/advcvtcolor.py | 64 + samples/operators/averageblur.py | 55 + samples/operators/bilateral_filter.py | 59 + samples/operators/bndbox.py | 90 + samples/operators/boxblur.py | 77 + samples/operators/brightness_contrast.py | 86 + samples/operators/center_crop.py | 58 + samples/operators/channelreorder.py | 81 + samples/operators/clahe.py | 79 + samples/operators/color_twist.py | 83 + samples/operators/composite.py | 91 + samples/operators/conv2d.py | 109 + samples/operators/convertto.py | 65 + samples/operators/copymakeborder.py | 60 + .../operators/crop_flip_normalize_reformat.py | 136 + samples/operators/customcrop.py | 65 + samples/operators/cvtcolor.py | 59 + samples/operators/erase.py | 146 + samples/operators/flip.py | 49 + samples/operators/gamma_contrast.py | 75 + samples/operators/gaussiannoise.py | 81 + samples/operators/histogrameq.py | 73 + samples/operators/hq_resize.py | 58 + samples/operators/inpaint.py | 108 + samples/operators/joint_bilateral_filter.py | 77 + samples/operators/label.py | 21 +- samples/operators/laplacian.py | 94 + samples/operators/median_blur.py | 51 + samples/operators/morphology.py | 86 + samples/operators/normalize.py | 112 + samples/operators/osd.py | 137 + samples/operators/pillowresize.py | 55 + samples/operators/random_resized_crop.py | 82 + samples/operators/reformat.py | 2 +- samples/operators/remap.py | 96 + .../operators/resize_crop_convert_reformat.py | 118 + samples/operators/rotate.py | 73 + samples/operators/stack.py | 2 +- samples/operators/threshold.py | 79 + samples/operators/warp_affine.py | 79 + samples/operators/warp_perspective.py | 97 + ...t => requirements.samples.common.template} | 24 +- ...txt => requirements.samples.cu12.template} | 25 +- ...txt => requirements.samples.cu13.template} | 24 +- ...rements.samples.hello_world_cu12.template} | 13 +- ...rements.samples.hello_world_cu13.template} | 13 +- samples/requirements_interop_cu12.txt | 32 - samples/requirements_interop_cu13.txt | 32 - samples/run_samples.sh | 53 +- src/cvcuda/CMakeLists.txt | 15 +- src/cvcuda/OpAdaptiveThreshold.cpp | 33 +- src/cvcuda/OpAdjustContrast.cpp | 74 + src/cvcuda/OpAdjustHue.cpp | 72 + src/cvcuda/OpAdjustSaturation.cpp | 72 + src/cvcuda/OpAdjustSharpness.cpp | 74 + src/cvcuda/OpAdvCvtColor.cpp | 16 +- src/cvcuda/OpAutoContrast.cpp | 70 + src/cvcuda/OpAverageBlur.cpp | 29 +- src/cvcuda/OpBilateralFilter.cpp | 32 +- src/cvcuda/OpBndBox.cpp | 15 +- src/cvcuda/OpBoxBlur.cpp | 15 +- src/cvcuda/OpBrightnessContrast.cpp | 80 +- src/cvcuda/OpCLAHE.cpp | 73 + src/cvcuda/OpCenterCrop.cpp | 16 +- src/cvcuda/OpChannelReorder.cpp | 35 +- src/cvcuda/OpColorTwist.cpp | 24 +- src/cvcuda/OpComposite.cpp | 29 +- src/cvcuda/OpConv2D.cpp | 17 +- src/cvcuda/OpConvertTo.cpp | 20 +- src/cvcuda/OpCopyMakeBorder.cpp | 38 +- src/cvcuda/OpCropFlipNormalizeReformat.cpp | 25 +- src/cvcuda/OpCustomCrop.cpp | 15 +- src/cvcuda/OpCvtColor.cpp | 23 +- src/cvcuda/OpErase.cpp | 57 +- src/cvcuda/OpFindHomography.cpp | 26 +- src/cvcuda/OpFlip.cpp | 23 +- src/cvcuda/OpGammaContrast.cpp | 60 +- src/cvcuda/OpGaussian.cpp | 32 +- src/cvcuda/OpGaussianNoise.cpp | 46 +- src/cvcuda/OpHQResize.cpp | 153 +- src/cvcuda/OpHistogram.cpp | 16 +- src/cvcuda/OpHistogramEq.cpp | 23 +- src/cvcuda/OpInpaint.cpp | 36 +- src/cvcuda/OpInvert.cpp | 70 + src/cvcuda/OpJointBilateralFilter.cpp | 35 +- src/cvcuda/OpJpegCompressionDistortion.cpp | 106 + src/cvcuda/OpLabel.cpp | 20 +- src/cvcuda/OpLaplacian.cpp | 28 +- src/cvcuda/OpMedianBlur.cpp | 24 +- src/cvcuda/OpMinAreaRect.cpp | 17 +- src/cvcuda/OpMinMaxLoc.cpp | 31 +- src/cvcuda/OpMorphology.cpp | 31 +- src/cvcuda/OpNonMaximumSuppression.cpp | 18 +- src/cvcuda/OpNormalize.cpp | 52 +- src/cvcuda/OpOSD.cpp | 15 +- src/cvcuda/OpPadAndStack.cpp | 17 +- src/cvcuda/OpPairwiseMatcher.cpp | 18 +- src/cvcuda/OpPillowResize.cpp | 66 +- src/cvcuda/OpPosterize.cpp | 72 + src/cvcuda/OpRandomResizedCrop.cpp | 27 +- src/cvcuda/OpReformat.cpp | 15 +- src/cvcuda/OpRemap.cpp | 30 +- src/cvcuda/OpResize.cpp | 23 +- src/cvcuda/OpResizeCropConvertReformat.cpp | 26 +- src/cvcuda/OpRotate.cpp | 28 +- src/cvcuda/OpSIFT.cpp | 24 +- src/cvcuda/OpSolarize.cpp | 72 + src/cvcuda/OpStack.cpp | 26 +- src/cvcuda/OpThreshold.cpp | 30 +- src/cvcuda/OpWarpAffine.cpp | 26 +- src/cvcuda/OpWarpPerspective.cpp | 26 +- src/cvcuda/Operator.cpp | 4 +- src/cvcuda/Types.cpp | 264 + src/cvcuda/include/cvcuda/IOperator.hpp | 66 +- .../include/cvcuda/OpAdaptiveThreshold.h | 10 +- .../include/cvcuda/OpAdaptiveThreshold.hpp | 38 +- src/cvcuda/include/cvcuda/OpAdjustContrast.h | 157 + .../include/cvcuda/OpAdjustContrast.hpp | 87 + src/cvcuda/include/cvcuda/OpAdjustHue.h | 152 + src/cvcuda/include/cvcuda/OpAdjustHue.hpp | 82 + .../include/cvcuda/OpAdjustSaturation.h | 156 + .../include/cvcuda/OpAdjustSaturation.hpp | 85 + src/cvcuda/include/cvcuda/OpAdjustSharpness.h | 161 + .../include/cvcuda/OpAdjustSharpness.hpp | 85 + src/cvcuda/include/cvcuda/OpAdvCvtColor.h | 29 +- src/cvcuda/include/cvcuda/OpAdvCvtColor.hpp | 33 +- src/cvcuda/include/cvcuda/OpAutoContrast.h | 166 + src/cvcuda/include/cvcuda/OpAutoContrast.hpp | 81 + src/cvcuda/include/cvcuda/OpAverageBlur.h | 18 +- src/cvcuda/include/cvcuda/OpAverageBlur.hpp | 39 +- src/cvcuda/include/cvcuda/OpBilateralFilter.h | 26 +- .../include/cvcuda/OpBilateralFilter.hpp | 39 +- src/cvcuda/include/cvcuda/OpBndBox.h | 10 +- src/cvcuda/include/cvcuda/OpBndBox.hpp | 33 +- src/cvcuda/include/cvcuda/OpBoxBlur.h | 28 +- src/cvcuda/include/cvcuda/OpBoxBlur.hpp | 33 +- .../include/cvcuda/OpBrightnessContrast.h | 43 +- .../include/cvcuda/OpBrightnessContrast.hpp | 63 +- src/cvcuda/include/cvcuda/OpCLAHE.h | 143 + src/cvcuda/include/cvcuda/OpCLAHE.hpp | 85 + src/cvcuda/include/cvcuda/OpCenterCrop.h | 18 +- src/cvcuda/include/cvcuda/OpCenterCrop.hpp | 33 +- src/cvcuda/include/cvcuda/OpChannelReorder.h | 106 +- .../include/cvcuda/OpChannelReorder.hpp | 34 +- src/cvcuda/include/cvcuda/OpColorTwist.h | 10 +- src/cvcuda/include/cvcuda/OpColorTwist.hpp | 38 +- src/cvcuda/include/cvcuda/OpComposite.h | 9 +- src/cvcuda/include/cvcuda/OpComposite.hpp | 40 +- src/cvcuda/include/cvcuda/OpConv2D.h | 14 +- src/cvcuda/include/cvcuda/OpConv2D.hpp | 34 +- src/cvcuda/include/cvcuda/OpConvertTo.h | 22 +- src/cvcuda/include/cvcuda/OpConvertTo.hpp | 33 +- src/cvcuda/include/cvcuda/OpCopyMakeBorder.h | 16 +- .../include/cvcuda/OpCopyMakeBorder.hpp | 48 +- .../cvcuda/OpCropFlipNormalizeReformat.h | 6 +- .../cvcuda/OpCropFlipNormalizeReformat.hpp | 34 +- src/cvcuda/include/cvcuda/OpCustomCrop.h | 18 +- src/cvcuda/include/cvcuda/OpCustomCrop.hpp | 33 +- src/cvcuda/include/cvcuda/OpCvtColor.h | 36 +- src/cvcuda/include/cvcuda/OpCvtColor.hpp | 39 +- src/cvcuda/include/cvcuda/OpErase.h | 81 +- src/cvcuda/include/cvcuda/OpErase.hpp | 56 +- src/cvcuda/include/cvcuda/OpFindHomography.h | 45 +- .../include/cvcuda/OpFindHomography.hpp | 37 +- src/cvcuda/include/cvcuda/OpFlip.h | 16 +- src/cvcuda/include/cvcuda/OpFlip.hpp | 39 +- src/cvcuda/include/cvcuda/OpGammaContrast.h | 80 +- src/cvcuda/include/cvcuda/OpGammaContrast.hpp | 45 +- src/cvcuda/include/cvcuda/OpGaussian.h | 10 +- src/cvcuda/include/cvcuda/OpGaussian.hpp | 39 +- src/cvcuda/include/cvcuda/OpGaussianNoise.h | 55 +- src/cvcuda/include/cvcuda/OpGaussianNoise.hpp | 53 +- src/cvcuda/include/cvcuda/OpHQResize.h | 27 +- src/cvcuda/include/cvcuda/OpHQResize.hpp | 70 +- src/cvcuda/include/cvcuda/OpHistogram.h | 15 +- src/cvcuda/include/cvcuda/OpHistogram.hpp | 32 +- src/cvcuda/include/cvcuda/OpHistogramEq.h | 16 +- src/cvcuda/include/cvcuda/OpHistogramEq.hpp | 38 +- src/cvcuda/include/cvcuda/OpInpaint.h | 13 +- src/cvcuda/include/cvcuda/OpInpaint.hpp | 38 +- src/cvcuda/include/cvcuda/OpInvert.h | 142 + src/cvcuda/include/cvcuda/OpInvert.hpp | 80 + .../include/cvcuda/OpJointBilateralFilter.h | 37 +- .../include/cvcuda/OpJointBilateralFilter.hpp | 43 +- .../cvcuda/OpJpegCompressionDistortion.h | 224 + .../cvcuda/OpJpegCompressionDistortion.hpp | 106 + src/cvcuda/include/cvcuda/OpLabel.h | 18 +- src/cvcuda/include/cvcuda/OpLabel.hpp | 28 +- src/cvcuda/include/cvcuda/OpLaplacian.h | 16 +- src/cvcuda/include/cvcuda/OpLaplacian.hpp | 40 +- src/cvcuda/include/cvcuda/OpMedianBlur.h | 12 +- src/cvcuda/include/cvcuda/OpMedianBlur.hpp | 40 +- src/cvcuda/include/cvcuda/OpMinAreaRect.h | 9 +- src/cvcuda/include/cvcuda/OpMinAreaRect.hpp | 32 +- src/cvcuda/include/cvcuda/OpMinMaxLoc.h | 12 +- src/cvcuda/include/cvcuda/OpMinMaxLoc.hpp | 33 +- src/cvcuda/include/cvcuda/OpMorphology.h | 24 +- src/cvcuda/include/cvcuda/OpMorphology.hpp | 38 +- .../include/cvcuda/OpNonMaximumSuppression.h | 9 +- .../cvcuda/OpNonMaximumSuppression.hpp | 30 +- src/cvcuda/include/cvcuda/OpNormalize.h | 142 +- src/cvcuda/include/cvcuda/OpNormalize.hpp | 54 +- src/cvcuda/include/cvcuda/OpOSD.h | 14 +- src/cvcuda/include/cvcuda/OpOSD.hpp | 33 +- src/cvcuda/include/cvcuda/OpPadAndStack.h | 14 +- src/cvcuda/include/cvcuda/OpPadAndStack.hpp | 33 +- src/cvcuda/include/cvcuda/OpPairwiseMatcher.h | 33 +- .../include/cvcuda/OpPairwiseMatcher.hpp | 32 +- src/cvcuda/include/cvcuda/OpPillowResize.h | 31 +- src/cvcuda/include/cvcuda/OpPillowResize.hpp | 53 +- src/cvcuda/include/cvcuda/OpPosterize.h | 147 + src/cvcuda/include/cvcuda/OpPosterize.hpp | 83 + .../include/cvcuda/OpRandomResizedCrop.h | 8 +- .../include/cvcuda/OpRandomResizedCrop.hpp | 39 +- src/cvcuda/include/cvcuda/OpReformat.h | 14 +- src/cvcuda/include/cvcuda/OpReformat.hpp | 32 +- src/cvcuda/include/cvcuda/OpRemap.h | 11 +- src/cvcuda/include/cvcuda/OpRemap.hpp | 42 +- src/cvcuda/include/cvcuda/OpResize.h | 8 +- src/cvcuda/include/cvcuda/OpResize.hpp | 39 +- .../cvcuda/OpResizeCropConvertReformat.h | 9 +- .../cvcuda/OpResizeCropConvertReformat.hpp | 44 +- src/cvcuda/include/cvcuda/OpRotate.h | 8 +- src/cvcuda/include/cvcuda/OpRotate.hpp | 42 +- src/cvcuda/include/cvcuda/OpSIFT.h | 13 +- src/cvcuda/include/cvcuda/OpSIFT.hpp | 36 +- src/cvcuda/include/cvcuda/OpSolarize.h | 150 + src/cvcuda/include/cvcuda/OpSolarize.hpp | 84 + src/cvcuda/include/cvcuda/OpStack.h | 35 +- src/cvcuda/include/cvcuda/OpStack.hpp | 33 +- src/cvcuda/include/cvcuda/OpThreshold.h | 12 +- src/cvcuda/include/cvcuda/OpThreshold.hpp | 40 +- src/cvcuda/include/cvcuda/OpWarpAffine.h | 12 +- src/cvcuda/include/cvcuda/OpWarpAffine.hpp | 40 +- src/cvcuda/include/cvcuda/OpWarpPerspective.h | 12 +- .../include/cvcuda/OpWarpPerspective.hpp | 39 +- src/cvcuda/include/cvcuda/Types.h | 62 +- src/cvcuda/include/cvcuda/Workspace.hpp | 108 +- .../include/cvcuda/cuda_tools/ArrayWrap.hpp | 8 +- .../include/cvcuda/cuda_tools/Atomics.hpp | 11 +- .../cvcuda/cuda_tools/BorderVarShapeWrap.hpp | 4 +- .../include/cvcuda/cuda_tools/BorderWrap.hpp | 108 +- .../include/cvcuda/cuda_tools/DropCast.hpp | 8 +- .../cvcuda/cuda_tools/FullTensorWrap.hpp | 37 +- .../cuda_tools/ImageBatchVarShapeWrap.hpp | 12 +- .../cuda_tools/InterpolationVarShapeWrap.hpp | 262 +- .../cvcuda/cuda_tools/InterpolationWrap.hpp | 290 +- .../include/cvcuda/cuda_tools/MathOps.hpp | 10 +- .../include/cvcuda/cuda_tools/RangeCast.hpp | 8 +- .../cvcuda/cuda_tools/SaturateCast.hpp | 10 +- .../include/cvcuda/cuda_tools/StaticCast.hpp | 8 +- .../cvcuda/cuda_tools/TensorBatchWrap.hpp | 25 +- .../include/cvcuda/cuda_tools/TensorWrap.hpp | 52 +- .../include/cvcuda/cuda_tools/TypeTraits.hpp | 88 +- .../cuda_tools/detail/MathWrappersImpl.hpp | 36 +- .../cuda_tools/detail/RangeCastImpl.hpp | 98 +- .../cuda_tools/detail/SaturateCastImpl.hpp | 104 +- .../include/cvcuda/cuda_tools/math/LinAlg.hpp | 33 +- src/cvcuda/priv/AdjustColorCommon.cuh | 108 + src/cvcuda/priv/BrightnessContrastPolicy.hpp | 48 + src/cvcuda/priv/CMakeLists.txt | 50 +- src/cvcuda/priv/CudaDeviceUtils.hpp | 61 + src/cvcuda/priv/IOperator.cpp | 6 +- src/cvcuda/priv/IOperator.hpp | 26 +- src/cvcuda/priv/InvertPolicy.hpp | 37 + src/cvcuda/priv/JpegDistortionMath.hpp | 295 + src/cvcuda/priv/Nvtx.hpp | 51 + src/cvcuda/priv/OpAdaptiveThreshold.cpp | 76 +- src/cvcuda/priv/OpAdaptiveThreshold.hpp | 9 +- src/cvcuda/priv/OpAdjustContrast.cu | 1358 ++ src/cvcuda/priv/OpAdjustContrast.hpp | 80 + src/cvcuda/priv/OpAdjustHue.cu | 656 + src/cvcuda/priv/OpAdjustHue.hpp | 48 + src/cvcuda/priv/OpAdjustSaturation.cu | 583 + src/cvcuda/priv/OpAdjustSaturation.hpp | 48 + src/cvcuda/priv/OpAdjustSharpness.cu | 520 + src/cvcuda/priv/OpAdjustSharpness.hpp | 48 + src/cvcuda/priv/OpAdvCvtColor.cu | 455 +- src/cvcuda/priv/OpAutoContrast.cu | 1505 ++ src/cvcuda/priv/OpAutoContrast.hpp | 96 + src/cvcuda/priv/OpAverageBlur.cpp | 28 +- src/cvcuda/priv/OpAverageBlur.hpp | 9 +- src/cvcuda/priv/OpBilateralFilter.cpp | 8 +- src/cvcuda/priv/OpBndBox.cpp | 272 +- src/cvcuda/priv/OpBndBox.hpp | 28 +- src/cvcuda/priv/OpBoxBlur.cpp | 234 +- src/cvcuda/priv/OpBrightnessContrast.cu | 362 +- src/cvcuda/priv/OpBrightnessContrast.hpp | 9 +- src/cvcuda/priv/OpCLAHE.cu | 668 + src/cvcuda/priv/OpCLAHE.hpp | 83 + src/cvcuda/priv/OpCenterCrop.cpp | 108 +- src/cvcuda/priv/OpChannelReorder.cpp | 63 - src/cvcuda/priv/OpChannelReorder.cu | 279 + src/cvcuda/priv/OpChannelReorder.hpp | 4 + src/cvcuda/priv/OpColorTwist.cu | 418 +- src/cvcuda/priv/OpComposite.cpp | 33 +- src/cvcuda/priv/OpConv2D.cpp | 7 +- src/cvcuda/priv/OpConvertTo.cpp | 11 +- src/cvcuda/priv/OpConvertTo.hpp | 5 +- src/cvcuda/priv/OpCopyMakeBorder.cpp | 9 +- .../priv/OpCropFlipNormalizeReformat.cu | 134 +- .../priv/OpCropFlipNormalizeReformat.hpp | 4 +- src/cvcuda/priv/OpCustomCrop.cpp | 18 +- src/cvcuda/priv/OpCvtColor.cpp | 8 +- src/cvcuda/priv/OpErase.cpp | 52 +- src/cvcuda/priv/OpErase.hpp | 12 +- src/cvcuda/priv/OpEraseRegion.cu | 534 + src/cvcuda/priv/OpFindHomography.cu | 204 +- src/cvcuda/priv/OpFindHomography.hpp | 30 +- src/cvcuda/priv/OpFlip.cpp | 10 +- src/cvcuda/priv/OpGammaContrast.cpp | 78 +- src/cvcuda/priv/OpGammaContrast.hpp | 14 +- src/cvcuda/priv/OpGaussian.cpp | 29 +- src/cvcuda/priv/OpGaussian.hpp | 9 +- src/cvcuda/priv/OpGaussianNoise.cpp | 54 +- src/cvcuda/priv/OpGaussianNoise.hpp | 12 +- src/cvcuda/priv/OpHQResize.cu | 2796 +--- src/cvcuda/priv/OpHQResize2D.cu | 95 + src/cvcuda/priv/OpHQResize3D.cu | 97 + src/cvcuda/priv/OpHQResizeBatchWrap.cuh | 27 +- src/cvcuda/priv/OpHQResizeDispatch.hpp | 32 + src/cvcuda/priv/OpHQResizeFilter.cuh | 147 +- src/cvcuda/priv/OpHQResizeKernel.cuh | 4604 ++++++ src/cvcuda/priv/OpHQResizePlanar.cuh | 72 + src/cvcuda/priv/OpHQResizePolicy.hpp | 117 + src/cvcuda/priv/OpHistogram.cpp | 244 +- src/cvcuda/priv/OpHistogram.hpp | 30 +- src/cvcuda/priv/OpHistogramEq.cpp | 34 +- src/cvcuda/priv/OpHistogramEq.hpp | 10 +- src/cvcuda/priv/OpInpaint.cpp | 61 +- src/cvcuda/priv/OpInpaint.hpp | 11 +- src/cvcuda/priv/OpInvert.cu | 951 ++ src/cvcuda/priv/OpInvert.hpp | 47 + src/cvcuda/priv/OpJointBilateralFilter.cpp | 8 +- .../priv/OpJpegCompressionDistortion.cu | 874 + .../priv/OpJpegCompressionDistortion.hpp | 54 + src/cvcuda/priv/OpLabel.cu | 116 +- src/cvcuda/priv/OpLaplacian.cpp | 39 +- src/cvcuda/priv/OpLaplacian.hpp | 10 +- src/cvcuda/priv/OpMedianBlur.cpp | 24 +- src/cvcuda/priv/OpMedianBlur.hpp | 9 +- src/cvcuda/priv/OpMinAreaRect.cpp | 29 +- src/cvcuda/priv/OpMinAreaRect.hpp | 7 +- src/cvcuda/priv/OpMinMaxLoc.cu | 231 +- src/cvcuda/priv/OpMorphology.cpp | 282 +- src/cvcuda/priv/OpMorphology.hpp | 4 +- src/cvcuda/priv/OpNonMaximumSuppression.cu | 25 +- src/cvcuda/priv/OpNormalize.cpp | 31 +- src/cvcuda/priv/OpNormalize.hpp | 6 +- src/cvcuda/priv/OpOSD.cpp | 198 +- src/cvcuda/priv/OpOSD.hpp | 53 +- src/cvcuda/priv/OpPadAndStack.cpp | 7 +- src/cvcuda/priv/OpPairwiseMatcher.cu | 151 +- src/cvcuda/priv/OpPillowResize.cpp | 51 +- src/cvcuda/priv/OpPosterize.cu | 683 + src/cvcuda/priv/OpPosterize.hpp | 48 + src/cvcuda/priv/OpRandomResizedCrop.cpp | 37 +- src/cvcuda/priv/OpRandomResizedCrop.hpp | 9 +- src/cvcuda/priv/OpReformat.cpp | 7 +- src/cvcuda/priv/OpRemap.cu | 340 +- src/cvcuda/priv/OpResize.cpp | 60 +- src/cvcuda/priv/OpResize.cu | 1355 +- .../priv/OpResizeCropConvertReformat.cu | 355 +- src/cvcuda/priv/OpRotate.cpp | 25 +- src/cvcuda/priv/OpRotate.hpp | 9 +- src/cvcuda/priv/OpSIFT.cu | 103 +- src/cvcuda/priv/OpSIFT.hpp | 11 +- src/cvcuda/priv/OpSolarize.cu | 764 + src/cvcuda/priv/OpSolarize.hpp | 48 + src/cvcuda/priv/OpStack.cpp | 225 +- src/cvcuda/priv/OpStack.hpp | 8 +- src/cvcuda/priv/OpStackKernels.cu | 165 + src/cvcuda/priv/OpStackKernels.hpp | 36 + src/cvcuda/priv/OpThreshold.cpp | 33 +- src/cvcuda/priv/OpThreshold.hpp | 9 +- src/cvcuda/priv/OpWarpAffine.cpp | 24 +- src/cvcuda/priv/OpWarpAffine.hpp | 9 +- src/cvcuda/priv/OpWarpPerspective.cpp | 24 +- src/cvcuda/priv/OpWarpPerspective.hpp | 9 +- src/cvcuda/priv/PerDeviceResource.hpp | 109 + src/cvcuda/priv/PlanarTensorView.hpp | 123 + src/cvcuda/priv/SafeSize.hpp | 56 + src/cvcuda/priv/Types.hpp | 495 +- src/cvcuda/priv/WorkspaceAllocator.hpp | 54 +- .../priv/legacy/AdaptiveThresholdPolicy.hpp | 39 + src/cvcuda/priv/legacy/CMakeLists.txt | 41 +- .../priv/legacy/CopyMakeBorderPolicy.hpp | 33 + src/cvcuda/priv/legacy/CvCudaLegacy.h | 298 +- .../priv/legacy/CvCudaLegacyHelpers.cpp | 13 +- src/cvcuda/priv/legacy/CvCudaOSD.hpp | 92 +- src/cvcuda/priv/legacy/CvCudaUtils.cuh | 25 +- src/cvcuda/priv/legacy/EraseCopyPolicy.hpp | 36 + src/cvcuda/priv/legacy/ReformatCopyPolicy.hpp | 50 + src/cvcuda/priv/legacy/adaptive_threshold.cu | 190 +- .../legacy/adaptive_threshold_var_shape.cu | 216 +- src/cvcuda/priv/legacy/bilateral_filter.cu | 358 +- .../priv/legacy/bilateral_filter_var_shape.cu | 367 +- src/cvcuda/priv/legacy/box_blur.cu | 309 +- src/cvcuda/priv/legacy/calc_hist.cu | 192 +- src/cvcuda/priv/legacy/center_crop.cu | 11 +- .../priv/legacy/channel_reorder_var_shape.cu | 338 +- src/cvcuda/priv/legacy/composite.cu | 161 +- src/cvcuda/priv/legacy/composite_var_shape.cu | 123 +- src/cvcuda/priv/legacy/convert_to.cu | 264 +- src/cvcuda/priv/legacy/copy_make_border.cu | 149 +- .../priv/legacy/copy_make_border_var_shape.cu | 434 +- src/cvcuda/priv/legacy/custom_crop.cu | 105 +- src/cvcuda/priv/legacy/cvt_color.cu | 592 +- src/cvcuda/priv/legacy/cvt_color_var_shape.cu | 876 +- src/cvcuda/priv/legacy/erase.cu | 273 +- src/cvcuda/priv/legacy/erase_var_shape.cu | 359 +- src/cvcuda/priv/legacy/filter.cu | 1373 +- src/cvcuda/priv/legacy/filter_utils.cu | 2 +- src/cvcuda/priv/legacy/filter_utils.cuh | 32 +- src/cvcuda/priv/legacy/filter_var_shape.cu | 2554 ++- src/cvcuda/priv/legacy/flip.cu | 354 +- .../priv/legacy/flip_or_copy_var_shape.cu | 345 +- src/cvcuda/priv/legacy/gamma_contrast.cu | 795 + .../priv/legacy/gamma_contrast_common.cuh | 199 + .../priv/legacy/gamma_contrast_var_shape.cu | 556 +- src/cvcuda/priv/legacy/gaussian_noise.cu | 922 +- .../priv/legacy/gaussian_noise_util.cuh | 194 +- .../priv/legacy/gaussian_noise_var_shape.cu | 622 +- src/cvcuda/priv/legacy/histogram_eq.cu | 114 +- .../priv/legacy/histogram_eq_var_shape.cu | 115 +- src/cvcuda/priv/legacy/inpaint.cu | 204 +- src/cvcuda/priv/legacy/inpaint_utils.cuh | 67 +- src/cvcuda/priv/legacy/inpaint_var_shape.cu | 170 +- .../priv/legacy/joint_bilateral_filter.cu | 343 +- .../joint_bilateral_filter_var_shape.cu | 357 +- src/cvcuda/priv/legacy/median_blur.cu | 149 +- .../priv/legacy/median_blur_var_shape.cu | 247 +- src/cvcuda/priv/legacy/min_area_rect.cu | 58 +- src/cvcuda/priv/legacy/morphology.cu | 237 +- .../priv/legacy/morphology_var_shape.cu | 770 +- src/cvcuda/priv/legacy/normalize.cu | 1394 +- src/cvcuda/priv/legacy/normalize_planar.cuh | 159 + src/cvcuda/priv/legacy/normalize_var_shape.cu | 577 +- src/cvcuda/priv/legacy/osd.cu | 118 +- src/cvcuda/priv/legacy/pad_and_stack.cu | 150 +- src/cvcuda/priv/legacy/pillow_resize.cu | 1121 +- src/cvcuda/priv/legacy/pillow_resize.h | 134 + .../priv/legacy/pillow_resize_var_shape.cu | 1024 +- src/cvcuda/priv/legacy/random_resized_crop.cu | 568 +- .../legacy/random_resized_crop_common.cuh | 59 + .../legacy/random_resized_crop_var_shape.cu | 522 +- src/cvcuda/priv/legacy/reformat.cu | 121 +- src/cvcuda/priv/legacy/resize_var_shape.cu | 1614 +- src/cvcuda/priv/legacy/rotate.cu | 281 +- src/cvcuda/priv/legacy/rotate_var_shape.cu | 309 +- .../priv/legacy/textbackend/backend.cpp | 20 +- .../priv/legacy/textbackend/backend.hpp | 14 +- src/cvcuda/priv/legacy/textbackend/memory.hpp | 9 +- src/cvcuda/priv/legacy/textbackend/stb.cpp | 899 +- .../priv/legacy/textbackend/stb_truetype.h | 40 +- src/cvcuda/priv/legacy/threshold.cu | 266 +- src/cvcuda/priv/legacy/threshold_util.cu | 3 +- src/cvcuda/priv/legacy/threshold_util.cuh | 36 +- src/cvcuda/priv/legacy/threshold_var_shape.cu | 389 +- src/cvcuda/priv/legacy/warp.cu | 381 +- src/cvcuda/priv/legacy/warp_cubic.cuh | 420 + src/cvcuda/priv/legacy/warp_var_shape.cu | 356 +- src/cvcuda/util/Event.hpp | 6 +- src/cvcuda/util/PerStreamCache.hpp | 64 +- src/cvcuda/util/PerStreamCacheImpl.hpp | 24 +- src/cvcuda/util/SimpleCache.hpp | 5 +- src/cvcuda/util/StreamId.cpp | 7 +- src/cvcuda/util/UniqueHandle.hpp | 15 +- src/nvcv/CMakeLists.txt | 7 +- src/nvcv/cmake/VersionDef.h.in | 4 +- src/nvcv/src/Allocator.cpp | 49 +- src/nvcv/src/Array.cpp | 43 +- src/nvcv/src/ColorSpec.cpp | 52 +- src/nvcv/src/Config.cpp | 12 +- src/nvcv/src/DataLayout.cpp | 20 +- src/nvcv/src/DataType.cpp | 24 +- src/nvcv/src/Image.cpp | 44 +- src/nvcv/src/ImageBatch.cpp | 47 +- src/nvcv/src/ImageFormat.cpp | 101 +- src/nvcv/src/Requirements.cpp | 10 +- src/nvcv/src/Status.cpp | 6 +- src/nvcv/src/Tensor.cpp | 65 +- src/nvcv/src/TensorBatch.cpp | 74 +- src/nvcv/src/TensorLayout.cpp | 14 +- src/nvcv/src/TensorShape.cpp | 5 +- src/nvcv/src/include/nvcv/Array.h | 6 +- src/nvcv/src/include/nvcv/Array.hpp | 42 +- src/nvcv/src/include/nvcv/ArrayData.hpp | 12 +- src/nvcv/src/include/nvcv/ArrayDataAccess.hpp | 31 +- src/nvcv/src/include/nvcv/Casts.hpp | 2 +- src/nvcv/src/include/nvcv/ColorSpec.hpp | 54 +- src/nvcv/src/include/nvcv/Config.hpp | 4 +- src/nvcv/src/include/nvcv/CoreResource.hpp | 59 +- src/nvcv/src/include/nvcv/DataLayout.hpp | 16 +- src/nvcv/src/include/nvcv/DataType.hpp | 48 +- src/nvcv/src/include/nvcv/Exception.hpp | 110 +- src/nvcv/src/include/nvcv/HandleWrapper.hpp | 24 +- src/nvcv/src/include/nvcv/Image.h | 11 +- src/nvcv/src/include/nvcv/Image.hpp | 42 +- src/nvcv/src/include/nvcv/ImageBatch.h | 6 +- src/nvcv/src/include/nvcv/ImageBatch.hpp | 88 +- src/nvcv/src/include/nvcv/ImageBatchData.h | 20 +- src/nvcv/src/include/nvcv/ImageBatchData.hpp | 6 +- src/nvcv/src/include/nvcv/ImageData.h | 18 +- src/nvcv/src/include/nvcv/ImageData.hpp | 6 +- src/nvcv/src/include/nvcv/ImageFormat.hpp | 147 +- src/nvcv/src/include/nvcv/Optional.hpp | 245 +- src/nvcv/src/include/nvcv/RoundMode.h | 40 + src/nvcv/src/include/nvcv/Shape.hpp | 29 +- src/nvcv/src/include/nvcv/Size.h | 5 +- src/nvcv/src/include/nvcv/Size.hpp | 4 +- src/nvcv/src/include/nvcv/Tensor.h | 12 +- src/nvcv/src/include/nvcv/Tensor.hpp | 54 +- src/nvcv/src/include/nvcv/TensorBatch.h | 6 +- src/nvcv/src/include/nvcv/TensorBatch.hpp | 77 +- src/nvcv/src/include/nvcv/TensorBatchData.hpp | 10 +- src/nvcv/src/include/nvcv/TensorData.hpp | 18 +- .../src/include/nvcv/TensorDataAccess.hpp | 82 +- src/nvcv/src/include/nvcv/TensorLayout.h | 41 +- src/nvcv/src/include/nvcv/TensorLayout.hpp | 42 +- src/nvcv/src/include/nvcv/TensorLayoutDef.inc | 8 +- .../src/include/nvcv/TensorLayoutInfo.hpp | 39 +- src/nvcv/src/include/nvcv/TensorShape.hpp | 14 +- src/nvcv/src/include/nvcv/TensorShapeInfo.hpp | 57 +- src/nvcv/src/include/nvcv/alloc/Allocator.h | 34 +- src/nvcv/src/include/nvcv/alloc/Allocator.hpp | 129 +- .../src/include/nvcv/alloc/AllocatorImpl.hpp | 83 +- src/nvcv/src/include/nvcv/alloc/Fwd.h | 15 +- .../src/include/nvcv/alloc/Requirements.hpp | 13 +- src/nvcv/src/include/nvcv/detail/Align.hpp | 9 +- .../src/include/nvcv/detail/ArrayDataImpl.hpp | 12 +- .../src/include/nvcv/detail/ArrayImpl.hpp | 15 +- src/nvcv/src/include/nvcv/detail/Callback.hpp | 101 +- .../src/include/nvcv/detail/CastsImpl.hpp | 7 +- .../src/include/nvcv/detail/CheckError.hpp | 11 +- src/nvcv/src/include/nvcv/detail/Concepts.hpp | 2 +- src/nvcv/src/include/nvcv/detail/CudaFwd.h | 6 +- src/nvcv/src/include/nvcv/detail/Format.hpp | 782 + .../nvcv/detail/ImageBatchDataImpl.hpp | 6 +- .../include/nvcv/detail/ImageBatchImpl.hpp | 34 +- .../src/include/nvcv/detail/ImageDataImpl.hpp | 12 +- .../src/include/nvcv/detail/ImageImpl.hpp | 13 +- src/nvcv/src/include/nvcv/detail/InPlace.hpp | 2 +- .../src/include/nvcv/detail/IndexSequence.hpp | 2 +- .../include/nvcv/detail/TensorBatchImpl.hpp | 24 +- .../include/nvcv/detail/TensorDataImpl.hpp | 18 +- .../src/include/nvcv/detail/TensorImpl.hpp | 25 +- .../src/include/nvcv/detail/TypeTraits.hpp | 12 +- .../src/include/nvcv/detail/UniqueObj.hpp | 64 + src/nvcv/src/priv/Array.cpp | 24 +- src/nvcv/src/priv/Array.hpp | 8 +- src/nvcv/src/priv/ArrayWrapData.cpp | 9 +- src/nvcv/src/priv/ArrayWrapData.hpp | 8 +- src/nvcv/src/priv/ColorSpec.cpp | 94 +- src/nvcv/src/priv/ColorSpec.hpp | 9 +- src/nvcv/src/priv/Context.cpp | 19 +- src/nvcv/src/priv/Context.hpp | 21 +- src/nvcv/src/priv/CustomAllocator.cpp | 14 +- src/nvcv/src/priv/CustomAllocator.hpp | 22 +- src/nvcv/src/priv/DataLayout.cpp | 280 +- src/nvcv/src/priv/DataType.cpp | 134 +- src/nvcv/src/priv/DefaultAllocator.cpp | 57 +- src/nvcv/src/priv/DefaultAllocator.hpp | 14 +- src/nvcv/src/priv/Exception.cpp | 75 +- src/nvcv/src/priv/Exception.hpp | 63 +- src/nvcv/src/priv/HandleManager.hpp | 21 +- src/nvcv/src/priv/HandleManagerImpl.hpp | 34 +- src/nvcv/src/priv/IAllocator.cpp | 14 +- src/nvcv/src/priv/IAllocator.hpp | 30 +- src/nvcv/src/priv/IContext.hpp | 4 +- src/nvcv/src/priv/ICoreObject.hpp | 21 +- src/nvcv/src/priv/IImageBatch.hpp | 6 +- src/nvcv/src/priv/Image.cpp | 48 +- src/nvcv/src/priv/Image.hpp | 14 +- src/nvcv/src/priv/ImageBatchVarShape.cpp | 91 +- src/nvcv/src/priv/ImageBatchVarShape.hpp | 27 +- src/nvcv/src/priv/ImageFormat.cpp | 693 +- src/nvcv/src/priv/ImageFormat.hpp | 31 +- src/nvcv/src/priv/LockFreeStack.hpp | 20 +- src/nvcv/src/priv/Requirements.cpp | 13 +- src/nvcv/src/priv/SharedCoreObj.hpp | 60 +- src/nvcv/src/priv/Status.cpp | 51 +- src/nvcv/src/priv/Status.hpp | 4 +- src/nvcv/src/priv/TLS.cpp | 10 +- src/nvcv/src/priv/TLS.hpp | 59 +- src/nvcv/src/priv/Tensor.cpp | 29 +- src/nvcv/src/priv/Tensor.hpp | 8 +- src/nvcv/src/priv/TensorBatch.cpp | 51 +- src/nvcv/src/priv/TensorBatch.hpp | 28 +- src/nvcv/src/priv/TensorData.cpp | 188 +- src/nvcv/src/priv/TensorData.hpp | 4 +- src/nvcv/src/priv/TensorLayout.cpp | 18 +- src/nvcv/src/priv/TensorWrapDataStrided.cpp | 17 +- src/nvcv/src/priv/TensorWrapDataStrided.hpp | 9 +- src/nvcv/util/Assert.h | 18 +- src/nvcv/util/CheckError.cpp | 24 +- src/nvcv/util/CheckError.hpp | 59 +- src/nvcv/util/Compat.cpp | 118 +- src/nvcv/util/Compiler.hpp | 10 +- src/nvcv/util/Math.hpp | 37 +- src/nvcv/util/Ranges.hpp | 14 +- src/nvcv/util/Size.hpp | 7 +- src/nvcv/util/StaticVector.hpp | 151 +- src/nvcv/util/String.cpp | 128 +- src/nvcv/util/String.hpp | 44 +- src/nvcv/util/Version.hpp | 4 +- tests/CMakeLists.txt | 9 +- tests/Main.cpp | 6 +- tests/README.md | 58 + tests/cmake/CMakeLists.txt | 30 + tests/cmake/test_cuda_architecture_policy.py | 420 + tests/common/CMakeLists.txt | 4 +- tests/common/CheckStatus.hpp | 12 +- tests/common/HashMD5.cpp | 52 +- tests/common/HashMD5.hpp | 91 +- tests/common/InterpUtils.hpp | 343 +- tests/common/MixTypedTests.hpp | 23 +- tests/common/ObjectBag.cpp | 4 +- tests/common/ObjectBag.hpp | 7 +- tests/common/TensorDataUtils.cpp | 67 +- tests/common/TensorDataUtils.hpp | 353 +- tests/common/TypeList.hpp | 6 +- tests/common/ValueList.hpp | 97 +- tests/common/ValueTests.hpp | 100 +- tests/cvcuda/CMakeLists.txt | 5 +- tests/cvcuda/nvtx_probe/CMakeLists.txt | 27 + tests/cvcuda/nvtx_probe/NvtxProbe.cpp | 148 + tests/cvcuda/python/CMakeLists.txt | 6 +- tests/cvcuda/python/cvcuda_test_python.in | 33 +- tests/cvcuda/python/cvcuda_tools.py | 1204 ++ tests/cvcuda/python/cvcuda_types.py | 659 + tests/cvcuda/python/cvcuda_util.py | 140 +- .../python/test_adaptivethresholdtype.py | 1 - tests/cvcuda/python/test_bordertype.py | 1 - tests/cvcuda/python/test_cache.py | 154 +- .../python/test_cai_input_stream_race.py | 325 + tests/cvcuda/python/test_datatype.py | 84 +- tests/cvcuda/python/test_image.py | 217 +- tests/cvcuda/python/test_imgbatchvarshape.py | 24 +- tests/cvcuda/python/test_imgformat.py | 5 +- tests/cvcuda/python/test_import_order.py | 6 +- .../cvcuda/python/test_interop_cai_stream.py | 179 + tests/cvcuda/python/test_interptype.py | 1 - tests/cvcuda/python/test_multi_gpu.py | 402 + tests/cvcuda/python/test_multi_stream.py | 107 +- tests/cvcuda/python/test_multi_threading.py | 4 +- tests/cvcuda/python/test_nvtx_markers.py | 255 + .../cvcuda/python/test_opadaptivethreshold.py | 81 +- tests/cvcuda/python/test_opadjust_hue.py | 120 + .../cvcuda/python/test_opadjust_saturation.py | 124 + tests/cvcuda/python/test_opadjustcontrast.py | 101 + tests/cvcuda/python/test_opadjustsharpness.py | 117 + tests/cvcuda/python/test_opadvcvtcolor.py | 188 +- tests/cvcuda/python/test_opautocontrast.py | 117 + tests/cvcuda/python/test_opaverageblur.py | 52 +- tests/cvcuda/python/test_opbilateralfilter.py | 52 +- tests/cvcuda/python/test_opbndbox.py | 35 +- tests/cvcuda/python/test_opboxblur.py | 28 +- .../python/test_opbrightnesscontrast.py | 194 +- tests/cvcuda/python/test_opcentercrop.py | 33 +- tests/cvcuda/python/test_opchannelreorder.py | 112 +- tests/cvcuda/python/test_opclahe.py | 163 + tests/cvcuda/python/test_opcolortwist.py | 46 +- tests/cvcuda/python/test_opcomposite.py | 166 +- tests/cvcuda/python/test_opconv2d.py | 58 +- tests/cvcuda/python/test_opconvertto.py | 97 +- tests/cvcuda/python/test_opcopymakeborder.py | 215 +- .../python/test_opcopymakeborder_varshape.py | 148 - .../test_opcropflipnormalizereformat.py | 120 +- tests/cvcuda/python/test_opcustomcrop.py | 33 +- tests/cvcuda/python/test_opcvtcolor.py | 238 +- tests/cvcuda/python/test_operase.py | 257 +- tests/cvcuda/python/test_operase_varshape.py | 89 - tests/cvcuda/python/test_opfindhomography.py | 41 +- .../test_opfindhomography_cache_regression.py | 84 + tests/cvcuda/python/test_opflip.py | 58 +- tests/cvcuda/python/test_opgammacontrast.py | 201 +- tests/cvcuda/python/test_opgaussian.py | 51 +- tests/cvcuda/python/test_opgaussiannoise.py | 108 +- tests/cvcuda/python/test_ophistogram.py | 59 +- tests/cvcuda/python/test_ophistogrameq.py | 29 +- tests/cvcuda/python/test_ophqresize.py | 94 +- tests/cvcuda/python/test_ophqresize_cache.py | 406 + tests/cvcuda/python/test_opinpaint.py | 47 +- tests/cvcuda/python/test_opinvert.py | 134 + .../python/test_opjointbilateralfilter.py | 63 +- .../test_opjpegcompressiondistortion.py | 180 + tests/cvcuda/python/test_oplabel.py | 88 +- tests/cvcuda/python/test_oplaplacian.py | 45 +- tests/cvcuda/python/test_opmatch.py | 35 +- tests/cvcuda/python/test_opmedianblur.py | 36 +- tests/cvcuda/python/test_opminarearect.py | 137 +- tests/cvcuda/python/test_opminmaxloc.py | 44 +- tests/cvcuda/python/test_opmorphology.py | 81 +- tests/cvcuda/python/test_opnms.py | 67 +- tests/cvcuda/python/test_opnormalize.py | 262 +- tests/cvcuda/python/test_oposd.py | 33 +- tests/cvcuda/python/test_oppadandstack.py | 59 +- tests/cvcuda/python/test_oppillowresize.py | 111 +- tests/cvcuda/python/test_opposterize.py | 118 + .../cvcuda/python/test_oprandomresizedcrop.py | 59 +- tests/cvcuda/python/test_opreformat.py | 58 +- tests/cvcuda/python/test_opremap.py | 70 +- tests/cvcuda/python/test_opresize.py | 96 +- .../test_opresizecropconvertreformat.py | 103 +- tests/cvcuda/python/test_oprotate.py | 45 +- tests/cvcuda/python/test_opsift.py | 21 +- tests/cvcuda/python/test_opsolarize.py | 85 + tests/cvcuda/python/test_opstack.py | 122 +- tests/cvcuda/python/test_opthreshold.py | 152 +- tests/cvcuda/python/test_opwarpaffine.py | 55 +- tests/cvcuda/python/test_opwarpperspective.py | 83 +- tests/cvcuda/python/test_re_export.py | 18 +- tests/cvcuda/python/test_rect.py | 3 +- .../cvcuda/python/test_resource_submitsync.py | 204 + tests/cvcuda/python/test_resourceguard.py | 259 + tests/cvcuda/python/test_samples_common.py | 129 + tests/cvcuda/python/test_stream.py | 129 +- tests/cvcuda/python/test_tensor.py | 229 +- tests/cvcuda/python/test_tensor_batch.py | 33 +- tests/cvcuda/python/test_tensorlayout.py | 200 + tests/cvcuda/python/test_util.py | 11 +- tests/cvcuda/python/test_version.py | 1 + tests/cvcuda/stressTest/cvcuda_cache_repro.py | 49 +- .../stressTest/stress_test_inference.py | 33 +- .../cvcuda/stressTest/stress_test_mt_prep.py | 47 +- .../stressTest/stress_test_preprocess.py | 42 +- tests/cvcuda/system/CMakeLists.txt | 73 +- tests/cvcuda/system/ConvUtils.cpp | 227 +- tests/cvcuda/system/ConvUtils.hpp | 64 +- tests/cvcuda/system/CvtColorUtils.cpp | 384 +- tests/cvcuda/system/ElementwiseOpHarness.hpp | 372 + tests/cvcuda/system/FlipUtils.cpp | 38 +- tests/cvcuda/system/GaussianNoiseUtils.cu | 28 +- tests/cvcuda/system/GaussianNoiseUtils.cuh | 4 +- tests/cvcuda/system/OsdUtils.cu | 248 - tests/cvcuda/system/OsdUtils.cuh | 96 - tests/cvcuda/system/PlanarParityUtils.hpp | 479 + tests/cvcuda/system/ResizeUtils.cpp | 577 +- .../cvcuda/system/TestOpAdaptiveThreshold.cpp | 530 +- tests/cvcuda/system/TestOpAdjustContrast.cpp | 509 + tests/cvcuda/system/TestOpAdjustHue.cpp | 387 + .../cvcuda/system/TestOpAdjustSaturation.cpp | 305 + tests/cvcuda/system/TestOpAdjustSharpness.cpp | 514 + tests/cvcuda/system/TestOpAdvCvtColor.cpp | 989 +- tests/cvcuda/system/TestOpAutoContrast.cpp | 1480 ++ tests/cvcuda/system/TestOpAverageBlur.cpp | 253 +- tests/cvcuda/system/TestOpBilateralFilter.cpp | 858 +- tests/cvcuda/system/TestOpBndBox.cpp | 409 +- tests/cvcuda/system/TestOpBndBox_Smoke.cpp | 19 +- tests/cvcuda/system/TestOpBoxBlur.cpp | 635 +- tests/cvcuda/system/TestOpBoxBlur_Smoke.cpp | 34 +- .../system/TestOpBrightnessContrast.cpp | 489 +- tests/cvcuda/system/TestOpCLAHE.cpp | 423 + tests/cvcuda/system/TestOpCenterCrop.cpp | 242 +- tests/cvcuda/system/TestOpChannelReorder.cpp | 597 +- tests/cvcuda/system/TestOpColorTwist.cpp | 461 +- tests/cvcuda/system/TestOpComposite.cpp | 566 +- tests/cvcuda/system/TestOpConv2D.cpp | 168 +- tests/cvcuda/system/TestOpConvertTo.cpp | 192 +- tests/cvcuda/system/TestOpCopyMakeBorder.cpp | 583 +- .../TestOpCropFlipNormalizeReformat.cpp | 402 +- tests/cvcuda/system/TestOpCustomCrop.cpp | 303 +- tests/cvcuda/system/TestOpCvtColor.cpp | 1035 +- tests/cvcuda/system/TestOpErase.cpp | 1301 +- tests/cvcuda/system/TestOpFindHomography.cpp | 264 +- tests/cvcuda/system/TestOpFlip.cpp | 150 +- tests/cvcuda/system/TestOpGammaContrast.cpp | 843 +- tests/cvcuda/system/TestOpGaussian.cpp | 268 +- tests/cvcuda/system/TestOpGaussianNoise.cpp | 823 +- tests/cvcuda/system/TestOpHQResize.cpp | 1279 +- tests/cvcuda/system/TestOpHistogram.cpp | 258 +- tests/cvcuda/system/TestOpHistogramEq.cpp | 240 +- tests/cvcuda/system/TestOpInpaint.cpp | 1990 ++- tests/cvcuda/system/TestOpInvert.cpp | 293 + .../system/TestOpJointBilateralFilter.cpp | 452 +- .../TestOpJpegCompressionDistortion.cpp | 756 + tests/cvcuda/system/TestOpLabel.cpp | 838 +- tests/cvcuda/system/TestOpLaplacian.cpp | 251 +- tests/cvcuda/system/TestOpMedianBlur.cpp | 215 +- tests/cvcuda/system/TestOpMinAreaRect.cpp | 168 +- tests/cvcuda/system/TestOpMinMaxLoc.cpp | 594 +- tests/cvcuda/system/TestOpMorphology.cpp | 481 +- .../system/TestOpNonMaximumSuppression.cpp | 135 +- tests/cvcuda/system/TestOpNormalize.cpp | 2629 ++- tests/cvcuda/system/TestOpOSD.cpp | 1453 +- tests/cvcuda/system/TestOpOSD_Smoke.cpp | 639 - tests/cvcuda/system/TestOpPadAndStack.cpp | 339 +- tests/cvcuda/system/TestOpPairwiseMatcher.cpp | 199 +- tests/cvcuda/system/TestOpPillowResize.cpp | 604 +- tests/cvcuda/system/TestOpPosterize.cpp | 165 + .../cvcuda/system/TestOpRandomResizedCrop.cpp | 283 +- tests/cvcuda/system/TestOpReformat.cpp | 400 +- tests/cvcuda/system/TestOpRemap.cpp | 425 +- tests/cvcuda/system/TestOpResize.cpp | 501 +- .../TestOpResizeCropConvertReformat.cpp | 801 +- tests/cvcuda/system/TestOpRotate.cpp | 353 +- tests/cvcuda/system/TestOpSIFT.cpp | 995 +- tests/cvcuda/system/TestOpSolarize.cpp | 161 + tests/cvcuda/system/TestOpStack.cpp | 177 +- tests/cvcuda/system/TestOpThreshold.cpp | 1078 +- tests/cvcuda/system/TestOpWarpAffine.cpp | 530 +- tests/cvcuda/system/TestOpWarpPerspective.cpp | 678 +- .../cvcuda/system/TestOperatorWrapperCopy.cpp | 162 + tests/cvcuda/system/TestUtils.cpp | 112 +- tests/cvcuda/system/TestUtils.hpp | 21 +- tests/cvcuda/unit/CMakeLists.txt | 17 +- tests/cvcuda/unit/TestCudaDeviceUtils.cpp | 76 + tests/cvcuda/unit/TestLegacyHelpers.cpp | 57 + tests/cvcuda/unit/TestOpBoxBlur.cpp | 114 + tests/cvcuda/unit/TestOpHQResize.cpp | 48 + tests/cvcuda/unit/TestOpHQResizePolicy.cpp | 89 + tests/cvcuda/unit/TestOpInvertPolicy.cpp | 36 + tests/cvcuda/unit/TestPerStreamCache.cpp | 84 +- tests/cvcuda/unit/TestSimpleCache.cpp | 6 +- tests/cvcuda/unit/TestStreamId.cpp | 29 +- tests/cvcuda/unit/TestTextBackend.cpp | 209 + tests/cvcuda/unit/TestWorkspaceAllocator.cpp | 88 +- tests/install_test_dependencies.sh | 143 + .../cudatools_system/DeviceTensorWrap.hpp | 69 +- .../TestBorderVarShapeWrap.cpp | 73 +- .../cudatools_system/TestBorderWrap.cpp | 20 +- .../cudatools_system/TestFullTensorWrap.cpp | 219 +- .../TestImageBatchVarShapeWrap.cpp | 60 +- .../TestInterpolationVarShapeWrap.cpp | 18 +- .../TestInterpolationWrap.cpp | 557 +- .../cudatools_system/TestLinAlg.cpp | 162 +- .../cudatools_system/TestTensorBatchWrap.cpp | 10 +- .../cudatools_system/TestTensorWrap.cpp | 156 +- .../cudatools_system/TestTypeTraits.cpp | 16 +- .../cudatools_unit/TestLegacyHelpers.cpp | 10 +- .../standalone/TestNVCVStandalone.cpp | 6 +- tests/nvcv_types/system/CMakeLists.txt | 5 +- tests/nvcv_types/system/TestAllocatorC.cpp | 255 +- tests/nvcv_types/system/TestAllocatorCpp.cpp | 224 +- tests/nvcv_types/system/TestArray.cpp | 112 +- tests/nvcv_types/system/TestColorSpec.cpp | 91 +- tests/nvcv_types/system/TestConfig.cpp | 6 +- tests/nvcv_types/system/TestDataLayout.cpp | 201 +- tests/nvcv_types/system/TestDataType.cpp | 37 +- tests/nvcv_types/system/TestExceptions.cpp | 57 +- tests/nvcv_types/system/TestImage.cpp | 159 +- tests/nvcv_types/system/TestImageBatch.cpp | 106 +- tests/nvcv_types/system/TestImageFormat.cpp | 600 +- tests/nvcv_types/system/TestRequirements.cpp | 21 +- tests/nvcv_types/system/TestSize.cpp | 20 +- tests/nvcv_types/system/TestStatus.cpp | 102 +- tests/nvcv_types/system/TestTensor.cpp | 242 +- tests/nvcv_types/system/TestTensorBatch.cpp | 82 +- .../system/TestTensorDataAccess.cpp | 104 +- .../nvcv_types/system/TestTensorDataUtils.cpp | 115 +- tests/nvcv_types/system/TestTensorLayout.cpp | 74 +- .../system/TestTensorLayoutInfo.cpp | 46 +- .../nvcv_types/system/TestTensorShapeInfo.cpp | 30 +- tests/nvcv_types/system/TestVersion.cpp | 50 +- .../system/TestVersionApiOverride.cpp | 60 + tests/nvcv_types/unit/TestArray.cpp | 8 +- tests/nvcv_types/unit/TestCallback.cpp | 115 +- tests/nvcv_types/unit/TestCheckError.cpp | 5 +- tests/nvcv_types/unit/TestColorSpec.cpp | 35 +- tests/nvcv_types/unit/TestCompat.cpp | 73 +- tests/nvcv_types/unit/TestDataLayout.cpp | 15 +- tests/nvcv_types/unit/TestExceptions.cpp | 64 +- tests/nvcv_types/unit/TestHandleManager.cpp | 59 +- tests/nvcv_types/unit/TestHandleWrapper.cpp | 39 +- tests/nvcv_types/unit/TestHashMD5.cpp | 11 +- tests/nvcv_types/unit/TestImageFormat.cpp | 52 +- tests/nvcv_types/unit/TestLockFreeStack.cpp | 48 +- tests/nvcv_types/unit/TestMath.cpp | 42 +- tests/nvcv_types/unit/TestOptional.cpp | 45 +- tests/nvcv_types/unit/TestRange.cpp | 38 +- tests/nvcv_types/unit/TestSharedCoreObj.cpp | 13 +- tests/nvcv_types/unit/TestStaticVector.cpp | 285 +- tests/nvcv_types/unit/TestString.cpp | 86 +- tests/nvcv_types/unit/TestTypeList.cpp | 30 +- tests/nvcv_types/unit/TestTypeTraits.cpp | 4 +- tests/nvcv_types/unit/TestTypedTests.cpp | 4 +- tests/nvcv_types/unit/TestValueList.cpp | 100 +- tests/nvcv_types/unit/TestValueTests.cpp | 31 +- tests/nvcv_types/unit/TestVersion.cpp | 169 +- .../requirements.tests.common.template | 20 +- .../requirements.tests.cu12.numpy1.template | 22 +- .../requirements.tests.cu12.template | 22 +- tests/requirements.tests.cu13.template | 21 + .../requirements.tests.numpy1.template | 12 +- .../requirements.tests.numpy2.template | 18 +- tests/run_tests.sh.in | 16 +- tools/make_op.py | 1425 ++ tools/mkop/Bench.cpp | 83 + tools/mkop/BenchConfig.json | 167 + tools/mkop/BenchPy.py | 86 + tools/mkop/CImpl.cpp | 18 +- tools/mkop/CppTest.cpp | 8 +- tools/mkop/PrivateImpl.cpp | 4 +- tools/mkop/Public.hpp | 28 +- tools/mkop/PythonWrap.cpp | 18 +- tools/mkop/mkop.sh | 253 +- tools/mkstub/mkstub.sh | 28 +- tools/mkstub/test.sh | 10 +- tools/optimize_op.py | 1832 +++ tools/refactor_op.py | 1258 ++ tools/review_op.py | 2255 +++ tools/symcompat/symcompat.sh | 10 +- tools/tests/test_make_op.py | 170 + tools/tests/test_optimize_op.py | 1482 ++ tools/tests/test_refactor_op.py | 371 + tools/tests/test_review_op.py | 705 + versions.env | 97 + 1599 files changed, 270853 insertions(+), 41143 deletions(-) create mode 100644 .agents/guidance/BUGFIX_GUIDELINES.md create mode 100644 .agents/guidance/MAKE_OP_GUIDELINES.md create mode 100644 .agents/guidance/OPTIMIZATION_GUIDELINES.md create mode 100644 .agents/guidance/REFACTOR_OP_GUIDELINES.md create mode 100644 .agents/guidance/REVIEW_OP_GUIDELINES.md create mode 100644 .agents/skills/make-op-scaffold/SKILL.md create mode 100644 .agents/skills/make-op-verify/SKILL.md create mode 100644 .agents/skills/make-op/SKILL.md create mode 100644 .agents/skills/optimize-op-preflight/SKILL.md create mode 100644 .agents/skills/optimize-op-verify/SKILL.md create mode 100644 .agents/skills/optimize-op/SKILL.md create mode 100644 .agents/skills/refactor-op/SKILL.md create mode 100644 .agents/skills/review-op-bench-coverage/SKILL.md create mode 100644 .agents/skills/review-op-docs-api/SKILL.md create mode 100644 .agents/skills/review-op-support/SKILL.md create mode 100644 .agents/skills/review-op-test-coverage/SKILL.md create mode 100644 .agents/skills/review-op/SKILL.md create mode 100644 .agents/tools/binding_api.py create mode 100755 .agents/tools/claude_review.sh create mode 100755 .agents/tools/codex_review.sh create mode 100755 .agents/tools/cursor_review.sh create mode 100644 .agents/tools/mr_code_review_prompt.md create mode 100644 .agents/tools/operator_source_map.py create mode 100644 .agents/tools/optimization_summary.py create mode 100755 .agents/tools/review_context.sh create mode 100644 .agents/tools/review_op_data.json create mode 100755 .agents/tools/review_worktree.sh create mode 100644 .agents/tools/tests/test_optimization_summary.py delete mode 100644 3rdparty/CMakeLists.txt delete mode 100644 3rdparty/cuOSD/aarch64/include/cuosd.h delete mode 100644 3rdparty/cuOSD/aarch64/lib/libcuosd.a delete mode 100644 3rdparty/cuOSD/x86_64/include/cuosd.h delete mode 100644 3rdparty/cuOSD/x86_64/lib/libcuosd.a delete mode 160000 3rdparty/dlpack delete mode 160000 3rdparty/googletest delete mode 160000 3rdparty/nvbench delete mode 160000 3rdparty/pybind11 create mode 100644 AGENTS.md create mode 120000 CLAUDE.md create mode 100644 CMakePresets.json delete mode 100644 DEVELOPER_GUIDE.md delete mode 100644 THIRD_PARTY_LICENSES.md delete mode 100644 bench/BenchAdaptiveThreshold.cpp delete mode 100644 bench/BenchAdvCvtColor.cpp delete mode 100644 bench/BenchAverageBlur.cpp delete mode 100644 bench/BenchBilateralFilter.cpp delete mode 100644 bench/BenchBndBox.cpp delete mode 100644 bench/BenchBoxBlur.cpp delete mode 100644 bench/BenchBrightnessContrast.cpp delete mode 100644 bench/BenchCenterCrop.cpp delete mode 100644 bench/BenchChannelReorder.cpp delete mode 100644 bench/BenchColorTwist.cpp delete mode 100644 bench/BenchComposite.cpp delete mode 100644 bench/BenchConv2D.cpp delete mode 100644 bench/BenchConvertTo.cpp delete mode 100644 bench/BenchCopyMakeBorder.cpp delete mode 100644 bench/BenchCropFlipNormalizeReformat.cpp delete mode 100644 bench/BenchCustomCrop.cpp delete mode 100644 bench/BenchCvtColor.cpp delete mode 100644 bench/BenchErase.cpp delete mode 100644 bench/BenchFindHomography.cpp delete mode 100644 bench/BenchFlip.cpp delete mode 100644 bench/BenchGammaContrast.cpp delete mode 100644 bench/BenchGaussian.cpp delete mode 100644 bench/BenchGaussianNoise.cpp delete mode 100644 bench/BenchHQResize.cpp delete mode 100644 bench/BenchHistogram.cpp delete mode 100644 bench/BenchHistogramEq.cpp delete mode 100644 bench/BenchInpaint.cpp delete mode 100644 bench/BenchJointBilateralFilter.cpp delete mode 100644 bench/BenchLabel.cpp delete mode 100644 bench/BenchLaplacian.cpp delete mode 100644 bench/BenchMedianBlur.cpp delete mode 100644 bench/BenchMinAreaRect.cpp delete mode 100644 bench/BenchMinMaxLoc.cpp delete mode 100644 bench/BenchMinMaxLoc.hpp delete mode 100644 bench/BenchMorphology.cpp delete mode 100644 bench/BenchNMS.cpp delete mode 100644 bench/BenchNormalize.cpp delete mode 100644 bench/BenchOSD.cpp delete mode 100644 bench/BenchPadAndStack.cpp delete mode 100644 bench/BenchPillowResize.cpp delete mode 100644 bench/BenchRandomResizedCrop.cpp delete mode 100644 bench/BenchReformat.cpp delete mode 100644 bench/BenchRemap.cpp delete mode 100644 bench/BenchResize.cpp delete mode 100644 bench/BenchResizeCropConvertReformat.cpp delete mode 100644 bench/BenchRotate.cpp delete mode 100644 bench/BenchStack.cpp delete mode 100644 bench/BenchThreshold.cpp delete mode 100644 bench/BenchUtils.hpp delete mode 100644 bench/BenchWarpAffine.cpp delete mode 100644 bench/BenchWarpPerspective.cpp create mode 100644 bench/README.md create mode 100644 bench/_internal/__init__.py create mode 100644 bench/_internal/axes.py create mode 100644 bench/_internal/baselines.py create mode 100755 bench/_internal/check_bench_sku.py create mode 100644 bench/_internal/quality.py create mode 100755 bench/_internal/update_baseline.py create mode 100755 bench/_internal/validate_baselines.py create mode 100644 bench/_internal/warmup.py create mode 100755 bench/compare_to_baseline.py create mode 100755 bench/compare_wheels.py create mode 100755 bench/config/axis_order.py create mode 100644 bench/config/bench_params.json create mode 100755 bench/config/load_config.py create mode 100644 bench/config/operator_categories.json create mode 100644 bench/config/operators/adaptivethreshold.json create mode 100644 bench/config/operators/adjustcontrast.json create mode 100644 bench/config/operators/adjusthue.json create mode 100644 bench/config/operators/adjustsaturation.json create mode 100644 bench/config/operators/adjustsharpness.json create mode 100644 bench/config/operators/advcvtcolor.json create mode 100644 bench/config/operators/autocontrast.json create mode 100644 bench/config/operators/averageblur.json create mode 100644 bench/config/operators/bilateralfilter.json create mode 100644 bench/config/operators/bndbox.json create mode 100644 bench/config/operators/boxblur.json create mode 100644 bench/config/operators/brightnesscontrast.json create mode 100644 bench/config/operators/centercrop.json create mode 100644 bench/config/operators/channelreorder.json create mode 100644 bench/config/operators/clahe.json create mode 100644 bench/config/operators/colortwist.json create mode 100644 bench/config/operators/composite.json create mode 100644 bench/config/operators/conv2d.json create mode 100644 bench/config/operators/convertto.json create mode 100644 bench/config/operators/copymakeborder.json create mode 100644 bench/config/operators/cropflipnormalizereformat.json create mode 100644 bench/config/operators/customcrop.json create mode 100644 bench/config/operators/cvtcolor.json create mode 100644 bench/config/operators/erase.json create mode 100644 bench/config/operators/findhomography.json create mode 100644 bench/config/operators/flip.json create mode 100644 bench/config/operators/gammacontrast.json create mode 100644 bench/config/operators/gaussian.json create mode 100644 bench/config/operators/gaussiannoise.json create mode 100644 bench/config/operators/histogram.json create mode 100644 bench/config/operators/histogrameq.json create mode 100644 bench/config/operators/hqresize.json create mode 100644 bench/config/operators/inpaint.json create mode 100644 bench/config/operators/invert.json create mode 100644 bench/config/operators/jointbilateralfilter.json create mode 100644 bench/config/operators/jpegcompressiondistortion.json create mode 100644 bench/config/operators/label.json create mode 100644 bench/config/operators/laplacian.json create mode 100644 bench/config/operators/medianblur.json create mode 100644 bench/config/operators/minarearect.json create mode 100644 bench/config/operators/minmaxloc.json create mode 100644 bench/config/operators/morphology.json create mode 100644 bench/config/operators/nonmaximumsuppression.json create mode 100644 bench/config/operators/normalize.json create mode 100644 bench/config/operators/osd.json create mode 100644 bench/config/operators/padandstack.json create mode 100644 bench/config/operators/pairwisematcher.json create mode 100644 bench/config/operators/pillowresize.json create mode 100644 bench/config/operators/posterize.json create mode 100644 bench/config/operators/randomresizedcrop.json create mode 100644 bench/config/operators/reformat.json create mode 100644 bench/config/operators/remap.json create mode 100644 bench/config/operators/resize.json create mode 100644 bench/config/operators/resizecropconvertreformat.json create mode 100644 bench/config/operators/rotate.json create mode 100644 bench/config/operators/sift.json create mode 100644 bench/config/operators/solarize.json create mode 100644 bench/config/operators/stack.json create mode 100644 bench/config/operators/threshold.json create mode 100644 bench/config/operators/warpaffine.json create mode 100644 bench/config/operators/warpperspective.json create mode 100644 bench/config/sku_map.json create mode 100644 bench/cpp/.gitignore create mode 100644 bench/cpp/BenchFillKernels.cu create mode 100644 bench/cpp/BenchFillKernels.hpp create mode 100644 bench/cpp/CMakeLists.txt create mode 100644 bench/cpp/CppBenchUtils.hpp create mode 100644 bench/cpp/GenerateBenchConfig.cmake create mode 100644 bench/cpp/WarmupPolicy.hpp create mode 100644 bench/cpp/ops/BenchAdaptiveThreshold.cpp create mode 100644 bench/cpp/ops/BenchAdjustContrast.cpp create mode 100644 bench/cpp/ops/BenchAdjustHue.cpp create mode 100644 bench/cpp/ops/BenchAdjustSaturation.cpp create mode 100644 bench/cpp/ops/BenchAdjustSharpness.cpp create mode 100644 bench/cpp/ops/BenchAdvCvtColor.cpp create mode 100644 bench/cpp/ops/BenchAutoContrast.cpp create mode 100644 bench/cpp/ops/BenchAverageBlur.cpp create mode 100644 bench/cpp/ops/BenchBilateralFilter.cpp create mode 100644 bench/cpp/ops/BenchBndBox.cpp create mode 100644 bench/cpp/ops/BenchBoxBlur.cpp create mode 100644 bench/cpp/ops/BenchBrightnessContrast.cpp create mode 100644 bench/cpp/ops/BenchCLAHE.cpp create mode 100644 bench/cpp/ops/BenchCenterCrop.cpp create mode 100644 bench/cpp/ops/BenchChannelReorder.cpp create mode 100644 bench/cpp/ops/BenchColorTwist.cpp create mode 100644 bench/cpp/ops/BenchComposite.cpp create mode 100644 bench/cpp/ops/BenchConv2D.cpp create mode 100644 bench/cpp/ops/BenchConvertTo.cpp create mode 100644 bench/cpp/ops/BenchCopyMakeBorder.cpp create mode 100644 bench/cpp/ops/BenchCropFlipNormalizeReformat.cpp create mode 100644 bench/cpp/ops/BenchCustomCrop.cpp create mode 100644 bench/cpp/ops/BenchCvtColor.cpp create mode 100644 bench/cpp/ops/BenchErase.cpp create mode 100644 bench/cpp/ops/BenchFindHomography.cpp create mode 100644 bench/cpp/ops/BenchFlip.cpp create mode 100644 bench/cpp/ops/BenchGammaContrast.cpp create mode 100644 bench/cpp/ops/BenchGaussian.cpp create mode 100644 bench/cpp/ops/BenchGaussianNoise.cpp create mode 100644 bench/cpp/ops/BenchHQResize.cpp create mode 100644 bench/cpp/ops/BenchHistogram.cpp create mode 100644 bench/cpp/ops/BenchHistogramEq.cpp create mode 100644 bench/cpp/ops/BenchInpaint.cpp create mode 100644 bench/cpp/ops/BenchInvert.cpp create mode 100644 bench/cpp/ops/BenchJointBilateralFilter.cpp create mode 100644 bench/cpp/ops/BenchJpegCompressionDistortion.cpp create mode 100644 bench/cpp/ops/BenchLabel.cpp create mode 100644 bench/cpp/ops/BenchLaplacian.cpp create mode 100644 bench/cpp/ops/BenchMedianBlur.cpp create mode 100644 bench/cpp/ops/BenchMinAreaRect.cpp create mode 100644 bench/cpp/ops/BenchMinMaxLoc.cpp create mode 100644 bench/cpp/ops/BenchMorphology.cpp create mode 100644 bench/cpp/ops/BenchNonMaximumSuppression.cpp create mode 100644 bench/cpp/ops/BenchNormalize.cpp create mode 100644 bench/cpp/ops/BenchOSD.cpp create mode 100644 bench/cpp/ops/BenchPadAndStack.cpp rename bench/{ => cpp/ops}/BenchPairwiseMatcher.cpp (62%) create mode 100644 bench/cpp/ops/BenchPillowResize.cpp create mode 100644 bench/cpp/ops/BenchPosterize.cpp create mode 100644 bench/cpp/ops/BenchRandomResizedCrop.cpp create mode 100644 bench/cpp/ops/BenchReformat.cpp create mode 100644 bench/cpp/ops/BenchRemap.cpp create mode 100644 bench/cpp/ops/BenchResize.cpp create mode 100644 bench/cpp/ops/BenchResizeCropConvertReformat.cpp create mode 100644 bench/cpp/ops/BenchRotate.cpp rename bench/{ => cpp/ops}/BenchSIFT.cpp (52%) create mode 100644 bench/cpp/ops/BenchSolarize.cpp create mode 100644 bench/cpp/ops/BenchStack.cpp create mode 100644 bench/cpp/ops/BenchThreshold.cpp create mode 100644 bench/cpp/ops/BenchWarpAffine.cpp create mode 100644 bench/cpp/ops/BenchWarpPerspective.cpp create mode 100644 bench/python/CMakeLists.txt delete mode 100644 bench/python/README.md delete mode 100644 bench/python/all_ops/op_adaptivethreshold.py delete mode 100644 bench/python/all_ops/op_as_image.py delete mode 100644 bench/python/all_ops/op_as_images.py delete mode 100644 bench/python/all_ops/op_averageblur.py delete mode 100644 bench/python/all_ops/op_blurbox.py delete mode 100644 bench/python/all_ops/op_boundingbox.py delete mode 100644 bench/python/all_ops/op_brightnesscontrast.py delete mode 100644 bench/python/all_ops/op_cache_limit.py delete mode 100644 bench/python/all_ops/op_centercrop.py delete mode 100644 bench/python/all_ops/op_composite.py delete mode 100644 bench/python/all_ops/op_convertto.py delete mode 100644 bench/python/all_ops/op_copymakeborder.py delete mode 100644 bench/python/all_ops/op_customcrop.py delete mode 100644 bench/python/all_ops/op_cvtcolor.py delete mode 100644 bench/python/all_ops/op_flip.py delete mode 100644 bench/python/all_ops/op_gaussianblur.py delete mode 100644 bench/python/all_ops/op_hqresize.py delete mode 100644 bench/python/all_ops/op_inpaint.py delete mode 100644 bench/python/all_ops/op_jointbilateral.py delete mode 100644 bench/python/all_ops/op_laplacian.py delete mode 100644 bench/python/all_ops/op_morphology.py delete mode 100644 bench/python/all_ops/op_nms.py delete mode 100644 bench/python/all_ops/op_normalize.py delete mode 100644 bench/python/all_ops/op_randomresizedcrop.py delete mode 100644 bench/python/all_ops/op_reformat.py delete mode 100644 bench/python/all_ops/op_remap.py delete mode 100644 bench/python/all_ops/op_reshape.py delete mode 100644 bench/python/all_ops/op_resize.py delete mode 100644 bench/python/all_ops/op_resize_crop_convert_reformat.py delete mode 100644 bench/python/all_ops/op_rotate.py delete mode 100644 bench/python/all_ops/op_sift.py delete mode 100644 bench/python/all_ops/op_threshold.py delete mode 100644 bench/python/all_ops/op_warpaffine.py delete mode 100644 bench/python/all_ops/op_warpperspective.py delete mode 100644 bench/python/assets/NOTICE.md delete mode 100644 bench/python/assets/brooklyn.jpg delete mode 100644 bench/python/assets/brooklyn_bboxes.pt delete mode 100644 bench/python/assets/brooklyn_mask.jpg delete mode 100644 bench/python/assets/brooklyn_nms_masks.pt delete mode 100644 bench/python/assets/brooklyn_scores.pt delete mode 100644 bench/python/assets/countour_lines.jpg delete mode 100644 bench/python/batch.py delete mode 100644 bench/python/bench_utils.py delete mode 100644 bench/python/benchmark.py create mode 100755 bench/python/install_bench_dependencies.sh delete mode 100755 bench/python/install_dependencies.sh delete mode 100644 bench/python/nvcodec_utils.py create mode 100755 bench/python/ops/bench_adaptivethreshold.py create mode 100755 bench/python/ops/bench_adjustcontrast.py create mode 100755 bench/python/ops/bench_adjusthue.py create mode 100755 bench/python/ops/bench_adjustsaturation.py create mode 100755 bench/python/ops/bench_adjustsharpness.py create mode 100755 bench/python/ops/bench_advcvtcolor.py create mode 100755 bench/python/ops/bench_autocontrast.py create mode 100755 bench/python/ops/bench_averageblur.py create mode 100755 bench/python/ops/bench_bilateralfilter.py create mode 100644 bench/python/ops/bench_bndbox.py create mode 100644 bench/python/ops/bench_boxblur.py create mode 100755 bench/python/ops/bench_brightnesscontrast.py create mode 100755 bench/python/ops/bench_centercrop.py create mode 100644 bench/python/ops/bench_channelreorder.py create mode 100755 bench/python/ops/bench_clahe.py create mode 100755 bench/python/ops/bench_colortwist.py create mode 100755 bench/python/ops/bench_composite.py create mode 100644 bench/python/ops/bench_conv2d.py create mode 100755 bench/python/ops/bench_convertto.py create mode 100755 bench/python/ops/bench_copymakeborder.py create mode 100644 bench/python/ops/bench_cropflipnormalizereformat.py create mode 100755 bench/python/ops/bench_customcrop.py create mode 100755 bench/python/ops/bench_cvtcolor.py create mode 100755 bench/python/ops/bench_erase.py create mode 100755 bench/python/ops/bench_findhomography.py create mode 100755 bench/python/ops/bench_flip.py create mode 100755 bench/python/ops/bench_gammacontrast.py create mode 100755 bench/python/ops/bench_gaussian.py create mode 100755 bench/python/ops/bench_gaussiannoise.py create mode 100755 bench/python/ops/bench_histogram.py create mode 100755 bench/python/ops/bench_histogrameq.py create mode 100755 bench/python/ops/bench_hqresize.py create mode 100755 bench/python/ops/bench_inpaint.py create mode 100755 bench/python/ops/bench_invert.py create mode 100755 bench/python/ops/bench_jointbilateralfilter.py create mode 100755 bench/python/ops/bench_jpegcompressiondistortion.py create mode 100755 bench/python/ops/bench_label.py create mode 100755 bench/python/ops/bench_laplacian.py create mode 100755 bench/python/ops/bench_medianblur.py create mode 100755 bench/python/ops/bench_minarearect.py create mode 100755 bench/python/ops/bench_minmaxloc.py create mode 100755 bench/python/ops/bench_morphology.py create mode 100755 bench/python/ops/bench_nonmaximumsuppression.py create mode 100755 bench/python/ops/bench_normalize.py create mode 100644 bench/python/ops/bench_osd.py create mode 100755 bench/python/ops/bench_padandstack.py create mode 100644 bench/python/ops/bench_pairwisematcher.py create mode 100755 bench/python/ops/bench_pillowresize.py create mode 100755 bench/python/ops/bench_posterize.py create mode 100755 bench/python/ops/bench_randomresizedcrop.py create mode 100755 bench/python/ops/bench_reformat.py create mode 100755 bench/python/ops/bench_remap.py create mode 100755 bench/python/ops/bench_resize.py create mode 100755 bench/python/ops/bench_resizecropconvertreformat.py create mode 100755 bench/python/ops/bench_rotate.py create mode 100755 bench/python/ops/bench_sift.py create mode 100755 bench/python/ops/bench_solarize.py create mode 100755 bench/python/ops/bench_stack.py create mode 100755 bench/python/ops/bench_threshold.py create mode 100755 bench/python/ops/bench_warpaffine.py create mode 100755 bench/python/ops/bench_warpperspective.py create mode 100755 bench/python/python_bench_utils.py create mode 100644 bench/python/requirements.bench.common.template create mode 100644 bench/python/requirements.bench.cu12.template create mode 100644 bench/python/requirements.bench.cu13.template delete mode 100644 bench/python/requirements_common.txt delete mode 100644 bench/python/run_bench.py mode change 100644 => 100755 bench/run_bench.py rename samples/requirements_samples_common.txt => bench/tests/conftest.py (60%) create mode 100644 bench/tests/test_axis_display.py create mode 100644 bench/tests/test_bench_cli_surface.py create mode 100644 bench/tests/test_bench_rgb_guidelines.py create mode 100644 bench/tests/test_benchmark_state_order.py create mode 100644 bench/tests/test_channelreorder_benchmark.py create mode 100644 bench/tests/test_check_bench_sku.py create mode 100644 bench/tests/test_compare_to_baseline.py create mode 100644 bench/tests/test_compare_wheels.py create mode 100644 bench/tests/test_histogrameq_benchmark_isolation.py create mode 100644 bench/tests/test_normalize_benchmark.py create mode 100644 bench/tests/test_python_bench_fill.py create mode 100644 bench/tests/test_python_bench_state_lifetime.py create mode 100644 bench/tests/test_run_bench_config_key.py create mode 100644 bench/tests/test_update_baseline.py create mode 100644 bench/tests/test_validate_baselines.py create mode 100644 bench/tests/test_warmup_policy.py rename ci/build.sh => build.sh (76%) create mode 100644 cmake/CUDAArchitecturePolicy.cmake create mode 100644 cmake/InstallBench.cmake delete mode 100644 docker/Dockerfile.cuda12.5.0.deps delete mode 100644 docker/Dockerfile.cuda12.9.0.deps delete mode 100644 docker/Dockerfile.cuda13.0.1.deps rename bench/python/requirements_cu13.txt => docker/requirements.build.all_pythons.template (57%) create mode 100644 docker/requirements.build.sys_python.template delete mode 100644 docker/requirements.no_torch_no_numpy.txt delete mode 100644 docker/requirements.sys_python.txt rename {ci => docs}/build_docs.sh (75%) create mode 100644 docs/requirements.docs.template create mode 100644 docs/sphinx/advanced/operator_variants.rst create mode 100644 docs/sphinx/content/cat_adaptivethreshold.jpg create mode 100644 docs/sphinx/content/cat_advcvtcolor.jpg create mode 100644 docs/sphinx/content/cat_averageblur.jpg create mode 100644 docs/sphinx/content/cat_bilateral_filter.jpg create mode 100644 docs/sphinx/content/cat_bndbox.jpg create mode 100644 docs/sphinx/content/cat_boxblur.jpg create mode 100644 docs/sphinx/content/cat_brightness_contrast.jpg create mode 100644 docs/sphinx/content/cat_center_crop.jpg create mode 100644 docs/sphinx/content/cat_channelreorder.jpg create mode 100644 docs/sphinx/content/cat_clahe.jpg create mode 100644 docs/sphinx/content/cat_color_twist.jpg create mode 100644 docs/sphinx/content/cat_composite.jpg create mode 100644 docs/sphinx/content/cat_conv2d.jpg create mode 100644 docs/sphinx/content/cat_convertto.jpg create mode 100644 docs/sphinx/content/cat_copymakeborder.jpg create mode 100644 docs/sphinx/content/cat_crop_flip_normalize_reformat.jpg create mode 100644 docs/sphinx/content/cat_customcrop.jpg create mode 100644 docs/sphinx/content/cat_cvtcolor.jpg create mode 100644 docs/sphinx/content/cat_erase.jpg create mode 100644 docs/sphinx/content/cat_flip.jpg create mode 100644 docs/sphinx/content/cat_gamma_contrast.jpg create mode 100644 docs/sphinx/content/cat_gaussiannoise.jpg create mode 100644 docs/sphinx/content/cat_histogrameq.jpg create mode 100644 docs/sphinx/content/cat_hq_resize.jpg create mode 100644 docs/sphinx/content/cat_inpaint.jpg create mode 100644 docs/sphinx/content/cat_inpaint_damaged.jpg create mode 100644 docs/sphinx/content/cat_joint_bilateral_filter.jpg create mode 100644 docs/sphinx/content/cat_laplacian.jpg create mode 100644 docs/sphinx/content/cat_median_blur.jpg create mode 100644 docs/sphinx/content/cat_morphology.jpg create mode 100644 docs/sphinx/content/cat_normalize.jpg create mode 100644 docs/sphinx/content/cat_osd.jpg create mode 100644 docs/sphinx/content/cat_pillowresize.jpg create mode 100644 docs/sphinx/content/cat_random_resized_crop.jpg create mode 100644 docs/sphinx/content/cat_remap.jpg create mode 100644 docs/sphinx/content/cat_resize_crop_convert_reformat.jpg create mode 100644 docs/sphinx/content/cat_rotate.jpg create mode 100644 docs/sphinx/content/cat_threshold.jpg create mode 100644 docs/sphinx/content/cat_warp_affine.jpg create mode 100644 docs/sphinx/content/cat_warp_perspective.jpg create mode 100644 docs/sphinx/cvcuda_limitations_ext.py delete mode 100644 docs/sphinx/interop/cupy.rst delete mode 100644 docs/sphinx/interop/pytorch.rst delete mode 100644 docs/sphinx/modules/c_api.rst rename docs/sphinx/modules/{cpp_api.rst => c_cpp_api.rst} (59%) create mode 100644 docs/sphinx/modules/c_status.rst rename docs/sphinx/{content/cvcuda_op.rst => operator_list.rst} (78%) create mode 100644 docs/sphinx/relnotes/relnotes-template.md create mode 100644 docs/sphinx/relnotes/v0.17.0-beta.rst rename docs/sphinx/{ => samples}/interoperability.rst (80%) rename docs/sphinx/{interop => samples/interoperability}/cuda_python.rst (82%) create mode 100644 docs/sphinx/samples/interoperability/cupy.rst rename docs/sphinx/{interop => samples/interoperability}/numpy.rst (84%) rename docs/sphinx/{interop => samples/interoperability}/nvimgcodec.rst (83%) rename docs/sphinx/{interop => samples/interoperability}/pycuda.rst (84%) rename docs/sphinx/{interop => samples/interoperability}/pynvvideocodec.rst (80%) create mode 100644 docs/sphinx/samples/interoperability/pytorch.rst create mode 100644 docs/sphinx/samples/operators/adaptivethreshold.rst create mode 100644 docs/sphinx/samples/operators/advcvtcolor.rst create mode 100644 docs/sphinx/samples/operators/averageblur.rst create mode 100644 docs/sphinx/samples/operators/bilateral_filter.rst create mode 100644 docs/sphinx/samples/operators/bndbox.rst create mode 100644 docs/sphinx/samples/operators/boxblur.rst create mode 100644 docs/sphinx/samples/operators/brightness_contrast.rst create mode 100644 docs/sphinx/samples/operators/center_crop.rst create mode 100644 docs/sphinx/samples/operators/channelreorder.rst create mode 100644 docs/sphinx/samples/operators/clahe.rst create mode 100644 docs/sphinx/samples/operators/color_twist.rst create mode 100644 docs/sphinx/samples/operators/composite.rst create mode 100644 docs/sphinx/samples/operators/conv2d.rst create mode 100644 docs/sphinx/samples/operators/convertto.rst create mode 100644 docs/sphinx/samples/operators/copymakeborder.rst create mode 100644 docs/sphinx/samples/operators/crop_flip_normalize_reformat.rst create mode 100644 docs/sphinx/samples/operators/customcrop.rst create mode 100644 docs/sphinx/samples/operators/cvtcolor.rst create mode 100644 docs/sphinx/samples/operators/erase.rst create mode 100644 docs/sphinx/samples/operators/flip.rst create mode 100644 docs/sphinx/samples/operators/gamma_contrast.rst create mode 100644 docs/sphinx/samples/operators/gaussiannoise.rst create mode 100644 docs/sphinx/samples/operators/histogrameq.rst create mode 100644 docs/sphinx/samples/operators/hq_resize.rst create mode 100644 docs/sphinx/samples/operators/inpaint.rst create mode 100644 docs/sphinx/samples/operators/joint_bilateral_filter.rst create mode 100644 docs/sphinx/samples/operators/laplacian.rst create mode 100644 docs/sphinx/samples/operators/median_blur.rst create mode 100644 docs/sphinx/samples/operators/morphology.rst create mode 100644 docs/sphinx/samples/operators/normalize.rst create mode 100644 docs/sphinx/samples/operators/osd.rst create mode 100644 docs/sphinx/samples/operators/pillowresize.rst create mode 100644 docs/sphinx/samples/operators/random_resized_crop.rst create mode 100644 docs/sphinx/samples/operators/remap.rst create mode 100644 docs/sphinx/samples/operators/resize_crop_convert_reformat.rst create mode 100644 docs/sphinx/samples/operators/rotate.rst create mode 100644 docs/sphinx/samples/operators/threshold.rst create mode 100644 docs/sphinx/samples/operators/warp_affine.rst create mode 100644 docs/sphinx/samples/operators/warp_perspective.rst create mode 100755 generate_requirements.sh create mode 100644 python/mod_cvcuda/NvtxRange.hpp create mode 100644 python/mod_cvcuda/RoundMode.cpp create mode 100644 python/mod_cvcuda/RoundMode.hpp create mode 100644 python/mod_cvcuda/operators/OpAdjustContrast.cpp create mode 100644 python/mod_cvcuda/operators/OpAdjustHue.cpp create mode 100644 python/mod_cvcuda/operators/OpAdjustSaturation.cpp create mode 100644 python/mod_cvcuda/operators/OpAdjustSharpness.cpp create mode 100644 python/mod_cvcuda/operators/OpAutoContrast.cpp create mode 100644 python/mod_cvcuda/operators/OpCLAHE.cpp create mode 100644 python/mod_cvcuda/operators/OpInvert.cpp create mode 100644 python/mod_cvcuda/operators/OpJpegCompressionDistortion.cpp create mode 100644 python/mod_cvcuda/operators/OpPosterize.cpp create mode 100644 python/mod_cvcuda/operators/OpSolarize.cpp create mode 100644 python/mod_cvcuda/operators/UnaryElementwiseOp.hpp create mode 100644 python/mod_cvcuda/operators/VarShapeUtils.hpp create mode 100644 python/py.typed create mode 100644 samples/CMakeLists.txt delete mode 100755 samples/install_interop_dependencies.sh create mode 100644 samples/operators/adaptivethreshold.py create mode 100644 samples/operators/advcvtcolor.py create mode 100644 samples/operators/averageblur.py create mode 100644 samples/operators/bilateral_filter.py create mode 100644 samples/operators/bndbox.py create mode 100644 samples/operators/boxblur.py create mode 100644 samples/operators/brightness_contrast.py create mode 100644 samples/operators/center_crop.py create mode 100644 samples/operators/channelreorder.py create mode 100644 samples/operators/clahe.py create mode 100644 samples/operators/color_twist.py create mode 100644 samples/operators/composite.py create mode 100644 samples/operators/conv2d.py create mode 100644 samples/operators/convertto.py create mode 100644 samples/operators/copymakeborder.py create mode 100644 samples/operators/crop_flip_normalize_reformat.py create mode 100644 samples/operators/customcrop.py create mode 100644 samples/operators/cvtcolor.py create mode 100644 samples/operators/erase.py create mode 100644 samples/operators/flip.py create mode 100644 samples/operators/gamma_contrast.py create mode 100644 samples/operators/gaussiannoise.py create mode 100644 samples/operators/histogrameq.py create mode 100644 samples/operators/hq_resize.py create mode 100644 samples/operators/inpaint.py create mode 100644 samples/operators/joint_bilateral_filter.py create mode 100644 samples/operators/laplacian.py create mode 100644 samples/operators/median_blur.py create mode 100644 samples/operators/morphology.py create mode 100644 samples/operators/normalize.py create mode 100644 samples/operators/osd.py create mode 100644 samples/operators/pillowresize.py create mode 100644 samples/operators/random_resized_crop.py create mode 100644 samples/operators/remap.py create mode 100644 samples/operators/resize_crop_convert_reformat.py create mode 100644 samples/operators/rotate.py create mode 100644 samples/operators/threshold.py create mode 100644 samples/operators/warp_affine.py create mode 100644 samples/operators/warp_perspective.py rename samples/{requirements_interop_common.txt => requirements.samples.common.template} (52%) rename samples/{requirements_samples_cu12.txt => requirements.samples.cu12.template} (53%) rename samples/{requirements_samples_cu13.txt => requirements.samples.cu13.template} (55%) rename samples/{requirements_hello_world_cu12.txt => requirements.samples.hello_world_cu12.template} (71%) rename samples/{requirements_hello_world_cu13.txt => requirements.samples.hello_world_cu13.template} (71%) delete mode 100644 samples/requirements_interop_cu12.txt delete mode 100644 samples/requirements_interop_cu13.txt create mode 100644 src/cvcuda/OpAdjustContrast.cpp create mode 100644 src/cvcuda/OpAdjustHue.cpp create mode 100644 src/cvcuda/OpAdjustSaturation.cpp create mode 100644 src/cvcuda/OpAdjustSharpness.cpp create mode 100644 src/cvcuda/OpAutoContrast.cpp create mode 100644 src/cvcuda/OpCLAHE.cpp create mode 100644 src/cvcuda/OpInvert.cpp create mode 100644 src/cvcuda/OpJpegCompressionDistortion.cpp create mode 100644 src/cvcuda/OpPosterize.cpp create mode 100644 src/cvcuda/OpSolarize.cpp create mode 100644 src/cvcuda/Types.cpp create mode 100644 src/cvcuda/include/cvcuda/OpAdjustContrast.h create mode 100644 src/cvcuda/include/cvcuda/OpAdjustContrast.hpp create mode 100644 src/cvcuda/include/cvcuda/OpAdjustHue.h create mode 100644 src/cvcuda/include/cvcuda/OpAdjustHue.hpp create mode 100644 src/cvcuda/include/cvcuda/OpAdjustSaturation.h create mode 100644 src/cvcuda/include/cvcuda/OpAdjustSaturation.hpp create mode 100644 src/cvcuda/include/cvcuda/OpAdjustSharpness.h create mode 100644 src/cvcuda/include/cvcuda/OpAdjustSharpness.hpp create mode 100644 src/cvcuda/include/cvcuda/OpAutoContrast.h create mode 100644 src/cvcuda/include/cvcuda/OpAutoContrast.hpp create mode 100644 src/cvcuda/include/cvcuda/OpCLAHE.h create mode 100644 src/cvcuda/include/cvcuda/OpCLAHE.hpp create mode 100644 src/cvcuda/include/cvcuda/OpInvert.h create mode 100644 src/cvcuda/include/cvcuda/OpInvert.hpp create mode 100644 src/cvcuda/include/cvcuda/OpJpegCompressionDistortion.h create mode 100644 src/cvcuda/include/cvcuda/OpJpegCompressionDistortion.hpp create mode 100644 src/cvcuda/include/cvcuda/OpPosterize.h create mode 100644 src/cvcuda/include/cvcuda/OpPosterize.hpp create mode 100644 src/cvcuda/include/cvcuda/OpSolarize.h create mode 100644 src/cvcuda/include/cvcuda/OpSolarize.hpp create mode 100644 src/cvcuda/priv/AdjustColorCommon.cuh create mode 100644 src/cvcuda/priv/BrightnessContrastPolicy.hpp create mode 100644 src/cvcuda/priv/CudaDeviceUtils.hpp create mode 100644 src/cvcuda/priv/InvertPolicy.hpp create mode 100644 src/cvcuda/priv/JpegDistortionMath.hpp create mode 100644 src/cvcuda/priv/Nvtx.hpp create mode 100644 src/cvcuda/priv/OpAdjustContrast.cu create mode 100644 src/cvcuda/priv/OpAdjustContrast.hpp create mode 100644 src/cvcuda/priv/OpAdjustHue.cu create mode 100644 src/cvcuda/priv/OpAdjustHue.hpp create mode 100644 src/cvcuda/priv/OpAdjustSaturation.cu create mode 100644 src/cvcuda/priv/OpAdjustSaturation.hpp create mode 100644 src/cvcuda/priv/OpAdjustSharpness.cu create mode 100644 src/cvcuda/priv/OpAdjustSharpness.hpp create mode 100644 src/cvcuda/priv/OpAutoContrast.cu create mode 100644 src/cvcuda/priv/OpAutoContrast.hpp create mode 100644 src/cvcuda/priv/OpCLAHE.cu create mode 100644 src/cvcuda/priv/OpCLAHE.hpp delete mode 100644 src/cvcuda/priv/OpChannelReorder.cpp create mode 100644 src/cvcuda/priv/OpChannelReorder.cu create mode 100644 src/cvcuda/priv/OpEraseRegion.cu create mode 100644 src/cvcuda/priv/OpHQResize2D.cu create mode 100644 src/cvcuda/priv/OpHQResize3D.cu create mode 100644 src/cvcuda/priv/OpHQResizeDispatch.hpp create mode 100644 src/cvcuda/priv/OpHQResizeKernel.cuh create mode 100644 src/cvcuda/priv/OpHQResizePlanar.cuh create mode 100644 src/cvcuda/priv/OpHQResizePolicy.hpp create mode 100644 src/cvcuda/priv/OpInvert.cu create mode 100644 src/cvcuda/priv/OpInvert.hpp create mode 100644 src/cvcuda/priv/OpJpegCompressionDistortion.cu create mode 100644 src/cvcuda/priv/OpJpegCompressionDistortion.hpp create mode 100644 src/cvcuda/priv/OpPosterize.cu create mode 100644 src/cvcuda/priv/OpPosterize.hpp create mode 100644 src/cvcuda/priv/OpSolarize.cu create mode 100644 src/cvcuda/priv/OpSolarize.hpp create mode 100644 src/cvcuda/priv/OpStackKernels.cu create mode 100644 src/cvcuda/priv/OpStackKernels.hpp create mode 100644 src/cvcuda/priv/PerDeviceResource.hpp create mode 100644 src/cvcuda/priv/PlanarTensorView.hpp create mode 100644 src/cvcuda/priv/SafeSize.hpp create mode 100644 src/cvcuda/priv/legacy/AdaptiveThresholdPolicy.hpp create mode 100644 src/cvcuda/priv/legacy/CopyMakeBorderPolicy.hpp create mode 100644 src/cvcuda/priv/legacy/EraseCopyPolicy.hpp create mode 100644 src/cvcuda/priv/legacy/ReformatCopyPolicy.hpp create mode 100644 src/cvcuda/priv/legacy/gamma_contrast.cu create mode 100644 src/cvcuda/priv/legacy/gamma_contrast_common.cuh create mode 100644 src/cvcuda/priv/legacy/normalize_planar.cuh create mode 100644 src/cvcuda/priv/legacy/random_resized_crop_common.cuh create mode 100644 src/cvcuda/priv/legacy/warp_cubic.cuh create mode 100644 src/nvcv/src/include/nvcv/RoundMode.h create mode 100644 src/nvcv/src/include/nvcv/detail/Format.hpp create mode 100644 src/nvcv/src/include/nvcv/detail/UniqueObj.hpp create mode 100644 tests/README.md create mode 100644 tests/cmake/CMakeLists.txt create mode 100755 tests/cmake/test_cuda_architecture_policy.py create mode 100644 tests/cvcuda/nvtx_probe/CMakeLists.txt create mode 100644 tests/cvcuda/nvtx_probe/NvtxProbe.cpp create mode 100644 tests/cvcuda/python/cvcuda_tools.py create mode 100644 tests/cvcuda/python/cvcuda_types.py create mode 100644 tests/cvcuda/python/test_cai_input_stream_race.py create mode 100644 tests/cvcuda/python/test_interop_cai_stream.py create mode 100644 tests/cvcuda/python/test_multi_gpu.py create mode 100644 tests/cvcuda/python/test_nvtx_markers.py create mode 100644 tests/cvcuda/python/test_opadjust_hue.py create mode 100644 tests/cvcuda/python/test_opadjust_saturation.py create mode 100644 tests/cvcuda/python/test_opadjustcontrast.py create mode 100644 tests/cvcuda/python/test_opadjustsharpness.py create mode 100644 tests/cvcuda/python/test_opautocontrast.py create mode 100644 tests/cvcuda/python/test_opclahe.py delete mode 100644 tests/cvcuda/python/test_opcopymakeborder_varshape.py delete mode 100644 tests/cvcuda/python/test_operase_varshape.py create mode 100644 tests/cvcuda/python/test_opfindhomography_cache_regression.py create mode 100644 tests/cvcuda/python/test_ophqresize_cache.py create mode 100644 tests/cvcuda/python/test_opinvert.py create mode 100644 tests/cvcuda/python/test_opjpegcompressiondistortion.py create mode 100644 tests/cvcuda/python/test_opposterize.py create mode 100644 tests/cvcuda/python/test_opsolarize.py create mode 100644 tests/cvcuda/python/test_resource_submitsync.py create mode 100644 tests/cvcuda/python/test_resourceguard.py create mode 100644 tests/cvcuda/python/test_samples_common.py create mode 100644 tests/cvcuda/python/test_tensorlayout.py create mode 100644 tests/cvcuda/system/ElementwiseOpHarness.hpp delete mode 100644 tests/cvcuda/system/OsdUtils.cu delete mode 100644 tests/cvcuda/system/OsdUtils.cuh create mode 100644 tests/cvcuda/system/PlanarParityUtils.hpp create mode 100644 tests/cvcuda/system/TestOpAdjustContrast.cpp create mode 100644 tests/cvcuda/system/TestOpAdjustHue.cpp create mode 100644 tests/cvcuda/system/TestOpAdjustSaturation.cpp create mode 100644 tests/cvcuda/system/TestOpAdjustSharpness.cpp create mode 100644 tests/cvcuda/system/TestOpAutoContrast.cpp create mode 100644 tests/cvcuda/system/TestOpCLAHE.cpp create mode 100644 tests/cvcuda/system/TestOpInvert.cpp create mode 100644 tests/cvcuda/system/TestOpJpegCompressionDistortion.cpp delete mode 100644 tests/cvcuda/system/TestOpOSD_Smoke.cpp create mode 100644 tests/cvcuda/system/TestOpPosterize.cpp create mode 100644 tests/cvcuda/system/TestOpSolarize.cpp create mode 100644 tests/cvcuda/system/TestOperatorWrapperCopy.cpp create mode 100644 tests/cvcuda/unit/TestCudaDeviceUtils.cpp create mode 100644 tests/cvcuda/unit/TestLegacyHelpers.cpp create mode 100644 tests/cvcuda/unit/TestOpBoxBlur.cpp create mode 100644 tests/cvcuda/unit/TestOpHQResize.cpp create mode 100644 tests/cvcuda/unit/TestOpHQResizePolicy.cpp create mode 100644 tests/cvcuda/unit/TestOpInvertPolicy.cpp create mode 100644 tests/cvcuda/unit/TestTextBackend.cpp create mode 100755 tests/install_test_dependencies.sh create mode 100644 tests/nvcv_types/system/TestVersionApiOverride.cpp rename samples/run_interop.sh => tests/requirements.tests.common.template (61%) mode change 100755 => 100644 rename bench/python/requirements_cu12.txt => tests/requirements.tests.cu12.numpy1.template (59%) rename .gitmodules => tests/requirements.tests.cu12.template (56%) create mode 100644 tests/requirements.tests.cu13.template rename docker/requirements.numpy1.txt => tests/requirements.tests.numpy1.template (66%) rename docker/requirements.numpy2.txt => tests/requirements.tests.numpy2.template (61%) create mode 100755 tools/make_op.py create mode 100644 tools/mkop/Bench.cpp create mode 100644 tools/mkop/BenchConfig.json create mode 100755 tools/mkop/BenchPy.py create mode 100755 tools/optimize_op.py create mode 100755 tools/refactor_op.py create mode 100755 tools/review_op.py create mode 100644 tools/tests/test_make_op.py create mode 100644 tools/tests/test_optimize_op.py create mode 100644 tools/tests/test_refactor_op.py create mode 100644 tools/tests/test_review_op.py create mode 100644 versions.env diff --git a/.agents/guidance/BUGFIX_GUIDELINES.md b/.agents/guidance/BUGFIX_GUIDELINES.md new file mode 100644 index 000000000..d8b5eba4d --- /dev/null +++ b/.agents/guidance/BUGFIX_GUIDELINES.md @@ -0,0 +1,368 @@ +[//]: # "SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved." +[//]: # "SPDX-License-Identifier: Apache-2.0" + +# CV-CUDA Bug Fix Guidelines + +How to reproduce and patch a bug in CV-CUDA. These guidelines apply to both +humans and AI agents and are derived from the project's recent bug-fix PRs. +For *reviewing* a bug-fix PR, see [REVIEW_PR_GUIDELINES.md](REVIEW_PR_GUIDELINES.md). + +The single most important rule: + +> **Land a deterministic failing test first, then land the fix on top of it, +> in the same PR.** CI should record a real failure against pre-fix `main`, +> and the same CI should turn green when the fix commit is added. + +If you cannot produce a deterministic test that fails before the fix and +passes after, you don't yet understand the bug well enough to fix it. + +## 1. Frame the bug + +Before touching code, answer in writing: + +- **Is there an existing public issue?** Search public GitHub issues for the + symptom *before* you start. If an issue exists, link it in the PR description + and reference it in relevant commits when it adds useful traceability. If the + bug should be public but has no issue, open one first; do not fix silently. +- **What is the observed failure?** Crash, OOB, garbled output, hang, + flaky test, sanitizer report, user issue. Quote the exact error or symptom. +- **What input triggers it?** Specific shapes, dtypes, formats, parameters, + matrices, multi-GPU topology, stream configuration. Reduce to the smallest + reproducer you can. +- **Where is the root cause?** Read the relevant code path top-to-bottom. + Don't fix the symptom in the wrong layer. +- **What is the blast radius?** Which other operators / call sites share the + same code path or the same class of mistake? See *Sibling search* below. + +For non-trivial bugs, capture this in the PR description or commit body so +reviewers can verify your understanding without re-deriving it. + +### Failure locale is not root-cause locale + +The test that fails is often *not* the test with the bug. A producer +test corrupts shared GPU state — context, a cached buffer, a stream +that swallows its own error in a `noexcept` destructor — and the next +test in pytest order is the one that reports `cudaErrorIllegalAddress`, +an OOB, or a wrong result. Investigating the reporter directly burns +days. Before forming a hypothesis: + +- **Identify the test that ran immediately before the failure.** Pull + the prior test name and parametrization from the CI log. When the + symptom is `cudaErrorIllegalAddress` on a generic `Stream.sync()`, + `cuda.synchronize()`, or teardown call, the producer is almost + certainly upstream. +- **Check whether the symptom moves across runs.** If the "failing + test" name changes between CI attempts but the failure mode is the + same, the failing test is downstream of a context-poisoning bug — + pytest ordering is the only thing varying. Stop investigating the + reporter; find the producer. +- **Look for unifying hypotheses across seemingly unrelated symptoms.** + Multiple distinct-looking failures (e.g. a sanitizer OOB on operator + X and a `cudaErrorIllegalAddress` on operator Y) often share one + root cause in shared infrastructure (`ImageBatchVarShape::exportData`, + `Resource::submitSync`, a Python-binding cache). Build the unifying + hypothesis before patching either symptom; otherwise you will land + two partial fixes for one bug and miss the third sibling. +- **Beware errors swallowed in `noexcept` destructors.** A logged-and- + consumed CUDA error in a `Stream` or `Resource` destructor leaves + the CUDA context poisoned, so the failure surfaces on the next CUDA + call from the next test. When tracing a confusing failure, audit + destructors and `CheckLog`/`try-catch` paths upstream of the reporter + for swallowed errors. + +## 2. Reproduce as a deterministic test + +The repro test is the first deliverable, not an afterthought. It must: + +- **Live in the existing test tree.** C++ tests under `tests/cvcuda/system/` + or `tests/nvcv_types/system/`; Python tests under `tests/cvcuda/python/`. + Match the existing naming and parametrization style of sibling tests. +- **Fail deterministically against pre-fix `main`.** Not "fails sometimes", + not "fails under load". If the bug is a race, force ordering with a + host-side sleep, a synthetic kernel delay, or a known-stalled producer + stream so the failure window is wide and reproducible. +- **Assert the actual symptom, not a proxy.** OOB → run under + `compute-sanitizer` and assert clean output. Garbled output → assert exact + expected values. Crash → assert no exception / no `cudaErrorIllegalAddress`. +- **Be small.** One or two parametrized cases that pin the contract. Don't + bundle a fuzz suite into the regression test. +- **Be self-contained.** No external data, no network, no flaky timing + assumptions beyond what you explicitly engineered. + +If the bug only surfaces on specific hardware (multi-GPU, specific SKU), +note that in the test docstring and gate the test appropriately, but still +make it deterministic on that hardware. + +## 3. Get failure evidence from CI on the test-only commit + +**Reproduce the bug locally before pushing.** Each CI run on this repo +costs roughly 50–70 minutes of GPU/cluster time; treating CI as an +interactive debugger is expensive and antisocial. Produce the failure +on a developer machine first, or document the exact hardware constraint +that prevents this (multi-GPU topology, specific SKU, sanitizer-only) +and capture local evidence in the commit body. + +Once the repro is deterministic locally, push the test commit by itself +and let CI run **once**. The goal is to produce a durable, link-able CI +failure that: + +- proves the bug exists on `main` at this SHA, and +- becomes the green CI run that proves the fix works once the fix commit + is added on top. + +Do not loop CI to chase a bug. Pushing instrumentation tweaks, sanitizer +configuration experiments, or speculative fixes hoping the matrix will +catch the bug for you wastes cluster time and obscures the test/fix +pair the PR is meant to demonstrate. If a CI run surfaces evidence local +runs missed, extract that evidence and reproduce locally before the +next push. + +**Keep repro experiments on a dedicated branch.** Diagnostic and +instrumentation commits made while reproducing a bug belong on a +separate `fix/` branch off the parent, not on the user's +existing MR or optimization branch — even when the failure fired on +that branch. The user's branch is the artifact under review; +debugging detritus on it muddies the diff and the history. + +Conventions used in recent PRs: + +- Branch name: `fix/` (e.g. `fix/minarearect-numpoints-oob`, + `fix/hqresize-degenerate-roi-fpe`). +- Test commit message: `test: deterministic repro for ` or + `test: add regression for `. Body should explain *why* the test fails + pre-fix, not just what it asserts. +- Explicitly state in the commit body: the test is committed first so CI + records the failure; the fix lands on top. + +If the failure does not surface in CI (e.g. only fires under +`compute-sanitizer`, or only on a specific GPU not in the default matrix), +say so in the commit body and capture local evidence (sanitizer output, +`nvidia-smi`, repro logs) in the PR description. + +## 4. Patch the bug + +The fix commit goes on top of the test commit on the same branch / same PR. + +Guidance gathered from recent fixes: + +- **Validate user input at the C API boundary**, not inside a CUDA kernel. + The codebase has two distinct validation idioms; use whichever matches + the layer you are editing: + - **C API / operator layer** (`src/cvcuda/Op*.cpp`, + `src/cvcuda/priv/Op*.cpp`): `throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "...")`. + Exceptions are caught and translated to status codes by `ProtectCall`. + - **Legacy CUDA kernel layer** (`src/cvcuda/priv/legacy/*.cu`): + `LOG_ERROR("...")` followed by `return ErrorCode::INVALID_PARAMETER`. + Do **not** use `NVCV_ASSERT` for input validation — `NVCV_ASSERT` aborts + the process and bypasses `ProtectCall` exception safety. Asserts are for + internal invariants only. +- **Clamp at the source, not at every consumer.** When a bug originates from + a single arithmetic primitive (e.g. `__float2int_rd` saturating to + `INT32_MAX`), clamp inside that primitive so every downstream caller is + fixed in one place. +- **Beware compiler-level UB.** Out-of-range enum casts, signed integer + overflow, NaN→int conversions, dereferencing past-end pointers — the + compiler is licensed to elide bounds checks on UB grounds. Fixing these at + the *source of UB* is mandatory; defence-in-depth at the kernel is a + bonus, not a substitute. +- **Multi-GPU safety.** Per-GPU state must use `PerDeviceResource<...>`, + matching the pattern in `HistogramEq`. Save and restore the current CUDA + device around teardown loops, including the primary handle, not just the + aux handles. +- **Stream / event lifecycle.** Wrap CUDA events in RAII guards on exception + paths. Do not call `cudaStreamWaitEvent` if the matching `cudaEventRecord` + failed. Honor and populate the CAI v3 / DLPack stream contract on both + input (`load`) and output (`cuda()` / `__dlpack__`) sides. +- **Python-binding caches.** A custom `Key` must implement `doGetHash`, + `doIsCompatible`, and `payloadSize` consistently — they jointly define + cache identity. Strict equality is the safe default unless an op + explicitly tolerates oversized reuse. +- **Don't add comments that paraphrase the diff.** The fix's *why* belongs + in the commit message; the code itself should be self-explanatory after + the change. + +### Sibling search + +Almost every fix in the recent history had at least one sibling site that +needed the same patch. Before declaring a fix complete, grep for: + +- The same operator's tensor and `_var_shape` paths. +- The same operator's `legacy/` and non-legacy implementations. +- Other operators that include the same template / utility (e.g. + `InterpolationWrap`, `BorderWrap`, `filter_utils`, `ProtectCall`). +- The matching half of any contract: input *and* output, wrap *and* + unwrap, alloc *and* free, record *and* wait. +- C++ and Python wrappers of the same operator. + +If you only fix one half, file a follow-up; do not silently leave the other +half broken. + +## 5. Validate before pushing the fix + +Required for any non-trivial fix: + +- **C++ test suites green:** `cvcuda_test_system`, + `nvcv_test_cudatools_system`. Note any *pre-existing* unrelated failures + in the commit body so reviewers don't conflate them with your change. +- **Python suite green:** the relevant `tests/cvcuda/python/` subset, and + ideally the full pytest suite. +- **Memory bugs:** clean run under `compute-sanitizer`. Quote + `ERROR SUMMARY: 0 errors` in the commit body. +- **Sample / reporter repro:** if the bug came from an external report or + GitHub issue, re-run the original reporter's script and confirm it now + passes. +- **Multi-GPU bugs:** validate on multi-GPU hardware (or document that you + could not, with a CI link that does). + +For bug fixes that touch operators with benchmarks, sanity-check that the +fix doesn't regress benchmark numbers materially (per-operator regression +gating is wired into CI; respect its verdict). + +### Tooling caveats that produce false signals + +Local validation is only useful if the tooling is honest. The following +caveats have masked real fixes or faked regressions on this repo: + +- **Reinstall the Python wheel after every C++ rebuild.** Python tests + and benchmarks resolve `libcvcuda.so` via the installed wheel's + RPATH, not the build tree. A freshly-built C++ artifact paired with + a stale wheel runs the *old* C++ under the new Python — a silent ABI + mismatch that can hide a fix or fake a regression. Either reinstall + the wheel or set `LD_LIBRARY_PATH` to the build tree explicitly + before running pytest or bench. +- **`ncu` does not see bank-conflict or memory-stall latency directly.** + Concluding "the kernel is unchanged" from `ncu` SOL deltas alone + misses real perf shifts on memory-bound kernels. Cross-check against + wall-clock bench runs. +- **DRAM placement perturbs memory-bound kernel timing.** A different + `cudaMalloc` history (dummy allocations, fragmentation, even alloc + order across tests) can produce several percent of spread on + memory-bound operators within a single process. If a bench delta is + in that range, investigate placement and pinning before claiming the + fix caused the change — and never relax a parity threshold to make + the gate pass; the trip is the signal. + +## 6. Commit message conventions + +If a public GitHub issue describes this bug, link it in the PR description and +add commit trailers when the reference is useful for downstream tooling, +release notes, or `git log` searches. Do not add placeholder issue references. + +For the fix commit: + +``` +fix: + + + + + + + +Fixes https://github.com/CVCUDA/CV-CUDA/issues/ # use when closing a public GitHub issue +``` + +For the test-only commit, use the same trailers as the fix commit: + +``` +test: for + + + + +Fixes https://github.com/CVCUDA/CV-CUDA/issues/ # use when closing a public GitHub issue +``` + +Conventional-commit prefixes used in this repo: `fix:`, `test:`, `feat:`, `perf:`, +`refactor:`, `docs:`, `chore:`, `ci:`, `build:`, `revert:`. Use `fix:` +for behavior-changing patches and `test:` for the matching repro. + +## 7. Anti-patterns + +- **"While I'm here" cleanups in a bug-fix PR.** Bug fixes should be + reviewable as bug fixes. Refactors, renames, and unrelated tweaks belong + in a separate PR. +- **Hiding flakes with retries or tolerance bumps.** If a test is flaky, + diagnose the race; do not add `@retry` or widen tolerances. +- **Skipping the failing CI run.** If a fix lands without a corresponding + pre-fix red CI on the same branch, future maintainers cannot tell whether + the fix is necessary. +- **Bypassing pre-commit / signing hooks.** Never use `--no-verify` or + `--no-gpg-sign` to push a fix. Fix the underlying hook failure. +- **`git commit --amend` / force-push to overwrite review history** once + the PR is under review. Land follow-ups as new commits; let the merge + flow squash if needed. +- **Treating a CI flake on rare hardware as a regression in the most recent + commit.** Investigate the current code path on the failing + configuration first; the bug is often older than the commit that + triggered the failing run. +- **Pushing diagnostic commits onto someone else's MR or optimization + branch.** Repro experiments belong on a fresh `fix/` branch off + the parent (see §3). +- **Anecdotal session-specific comments in code.** Phrases like + "around bench #146", "on a 21 GB box", or "free drops 20→5 GB" + describe a single debugging session, not the code's invariants — + they rot the moment the symptom shifts. Explain the *mechanism* + (what the code guarantees, what would otherwise break) and leave + session-specific evidence in the commit message or PR description + where it belongs. + +## 8. Debugging tooling + +Tools that have repeatedly pinned amorphous failures to specific bugs on +this codebase. Pick by symptom. + +- **`compute-sanitizer --tool memcheck `** — Catches GPU OOB, + uninitialized memory, and misaligned access. For context-poisoning + bugs (see §1) run it twice: against the producer test to surface the + real OOB, then against the reporter to confirm silence after the fix. +- **`compute-sanitizer --tool racecheck` / `--tool synccheck`** — + Detect shared-memory races and missing or divergent `__syncthreads` + inside a kernel. Add `--print-limit 0` when an OOB fires thousands of + times and you need every offending address. +- **ASAN / UBSAN host build** — Catches host-side OOB, signed overflow, + NaN→int conversions, and OOB enum casts. Configure with + `-DCMAKE_CXX_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer"`. +- **`CUDA_LAUNCH_BLOCKING=1`** — Serializes kernel launches so + `cudaErrorIllegalAddress` fires on the offending launch instead of + the downstream sync. Use this whenever §1's "find the producer" + workflow is in play. +- **Nsight Systems (`nsys profile --trace=cuda,osrt,nvtx`)** — Timeline + view of stream interactions; the right tool for the legacy-default + vs. `cudaStreamNonBlocking` race class. Look for user-stream kernel + launches overlapping an unfinished default-stream H2D. +- **`cuda-gdb` on a `-G`-built operator (`-DCMAKE_CUDA_FLAGS="-G"`)** — + Step into kernel code, inspect registers and shared memory. Expensive + to build; reserve for kernels you cannot diagnose any other way. +- **`gdb` on the host process** — For crashes in operator dispatch or + the Python binding layer. Pybind11 frames are noisy but the trace + reaches the actual C++ source. +- **`pytest --collect-only -q`** — Print test execution order; use it + to identify the test that runs immediately before a failure (the + producer candidate per §1). +- **`pytest -x `** — Run a suspected failing test + in a fresh process. If it passes in isolation but fails in the full + suite, the bug is upstream. +- **`ldd` on the cvcuda Python `.so`** — Verify which `libcvcuda.so` + Python actually resolves. Catches the wheel-vs-build-tree ABI + mismatch from §5's tooling caveats. +- **`nvidia-smi`** — Confirm the GPU is visible, no other process is + hogging memory, and no ECC errors are pending. +- **`ncu --set full `** — Full kernel profile for perf work. Read + with §5's caveats: bank-conflict and memory-stall latency are not + exposed by SOL metrics, so cross-check against wall-clock bench. +- **Wall-clock bench (`bench/run_bench.py --lang python` and the C++ bench + binaries)** — Authoritative source when `ncu` and reality disagree. +- **`git bisect run `** — Pin a regression to the + introducing commit when the bug predates the most recent change. + Write the script to exit 0/non-zero deterministically. + +## 9. After merge + +- If the fix exposed a class of bug (UB at a primitive, missing per-device + resource, missing stream contract), open follow-ups for the siblings you + did not fix. +- If the bug originated from a public GitHub issue, post the merged commit + / release and close the issue. diff --git a/.agents/guidance/MAKE_OP_GUIDELINES.md b/.agents/guidance/MAKE_OP_GUIDELINES.md new file mode 100644 index 000000000..1e1eb4c19 --- /dev/null +++ b/.agents/guidance/MAKE_OP_GUIDELINES.md @@ -0,0 +1,260 @@ +[//]: # "SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved." +[//]: # "SPDX-License-Identifier: Apache-2.0" + +# CV-CUDA New-Operator Guidelines (`/make-op`) + +The single source of truth for adding a **new** operator to CV-CUDA end-to-end. It is +**implemented by** `tools/make_op.py` (the deterministic checker) and **cited by** the thin +per-tool wrappers (`.claude/commands/make-op*.md`, `.codex/skills/make-op*/SKILL.md`). The +checklist substance lives here once; both Claude and Codex delegate to it — no duplication. + +This document also **documents the skill** (purpose, modes, workflow, invocation), exactly as +`OPTIMIZATION_GUIDELINES.md` documents `/optimize-op`. It does not restate house policy; it +*cites* the authoritative docs and turns their requirements into machine-checkable items: +`AGENTS.md`, `docs/sphinx/advanced/make_operator.rst` (the narrative tutorial), +`REVIEW_OP_GUIDELINES.md`, and `OPTIMIZATION_GUIDELINES.md`. + +`make_op.py` **composes, does not duplicate**: it imports `review_op.py` (all four review +domains) and `optimize_op.py --phase preflight`, then adds the new-operator-specific spec / +scaffold / coverage / execution gates on top. + +``` +review_op.py coverage primitive (support / test / bench / docs) + └─ optimize_op.py --phase preflight = review_op(test+bench) + baseline + profiling readiness + └─ make_op.py = review_op(all 4) + preflight(RDY) + SPEC + SCF + IMP + COV + DOC-REL + EXEC +``` + +## The skill + +Two modes: + +- **(A) Full end-to-end** — `/make-op `: propose a spec + reference oracle → **get user + approval** → scaffold (`tools/mkop/mkop.sh`) → record the approved contract in the C-API header + → `/make-op-scaffold` (structural gate) → implement (guided by `make_operator.rst` + this doc) + → `/make-op-verify` (the deterministic done-gate) → hand off to `/optimize-op` for the + performance campaign. +- **(B) Scaffold-only, implementation delegated** — `/make-op-scaffold [--bare]`: produce + a complete, wired, *building* skeleton and stop, leaving implementation to a human or another + AI. *Spec'd* (default) records the approved contract in the header; `--bare` skips the spec + (definition + implementation fully delegated). The done-gate stays red until someone implements + it and runs `/make-op-verify`. + +### Part 0 — Spec & approval (the operator's definition) + +The checker gates completeness *against a declared contract*; this interactive step (in the +skill, not the checker) authors that contract so it is deliberate, not a guess: + +1. The agent **proposes a spec**: name; one-line semantics; a **cited reference oracle** (e.g. + "mimic `torchvision.transforms.v2.functional.invert`: `out = dtype_max − in`", or OpenCV + `cv::bitwise_not`, or an explicit formula); API parameters; the target **support matrix** + (dtype × channel × layout × container). +2. The agent **queries the user for approval** before scaffolding. The user may amend the + semantics/oracle/matrix, or **explicitly waive the oracle** for a genuinely-novel custom op + (waiver + rationale recorded). +3. On approval, the contract is recorded in the C-API header (`Op.h`): `@brief` = semantics; + a `Reference:` line = the cited oracle (or `Reference: custom — oracle waived ()`); + the **Limitations table** = the approved matrix. This header **is** the single source of truth + that COV, the gold reference, and DOC all mirror. Image operators include interleaved and + planar layouts by default. If the inputs are not images, record `Planar image layouts: Not + applicable` and a non-empty `Reason` beside the Limitations table. + +## How to use + +```bash +python3 tools/make_op.py --phase scaffold|done \ + [--bare] [--format md|json] [--out report.json] [--run] +``` +- Each checklist item is reported with a **status**, literal **evidence**, and its **id**. +- **Completion** of the `done` phase = a re-run shows **zero `GAP` and zero unresolved `MANUAL`**. + Final green requires `--run` (tests compile + execute + pass) and CI-seeded baselines. + +### Status vocabulary + +| status | meaning | affects exit code | +|--------|---------|-------------------| +| `PASS` | check satisfied | no | +| `GAP` | check failed; an actionable deficiency | **yes (non-zero)** | +| `N-A` | not applicable to this operator | no | +| `MANUAL` | needs human reading; checker points at the location | no (but surfaced) | +| `RECOMMENDATION` | advisory follow-up | no | + +The static checks are **deterministic & idempotent** (no network, no clocks, no randomness); +only `--run` executes (opt-in, like `review_op --run`). + +### Inviolable rules (shared with the sibling skills) +- **Never fabricate baselines** — a missing baseline is `GAP "requires CI"` → regen fan-out. +- **Bit-exact is the default** for correctness — never silently introduce or widen a + tolerance; an unexplained `EXPECT_NEAR` is a `GAP` here (stronger than `review_op`). +- **Layout-support gaps are author work** — the checker reports missing operator capability + and does not implement it. Planar image layouts are required by default; only an operator-local + C-header declaration with a reason makes them not applicable. + +### Operator name resolution +Same as `REVIEW_OP_GUIDELINES.md`: `` is PascalCase; the checker derives `op` +(lowercase stem), `Op` (C++/test stem), and `pyname` (Python function), reusing +`review_op.resolve_op`. + +--- + +## Phase `scaffold` — the wired skeleton (run right after `mkop.sh`) + +### Domain: spec (the approved contract) + +| id | check | probe | PASS condition | +|----|-------|-------|----------------| +| SPEC-BRIEF | Header `@brief` authored | `Op.h` Doxygen brief | not the mkop stub ("Defines types and functions to handle…" / `TBD`); `--bare` → `MANUAL` "spec delegated" | +| SPEC-ORACLE | Reference oracle cited | header brief | a `Reference:`/`matches`/`mimics` line naming an external API (OpenCV/PIL/TorchVision/formula) **or** an explicit `oracle waived`/`custom — no external reference` token; `--bare` → `MANUAL` | +| SPEC-MATRIX | Limitations matrix declared | header Limitations table | real layout/channel/dtype rows, no `TODO`/`[TODO]`; `--bare` → `MANUAL` | +| SPEC-CORRECT | Semantics match the oracle; gold implements it independently | — | **MANUAL** (design/review judgment) | + +### Domain: scaffold (files + wiring) + +| id | check | probe | PASS condition | +|----|-------|-------|----------------| +| SCF-1 | Public C API header | `src/cvcuda/include/cvcuda/Op.h` | present | +| SCF-2 | Public C++ header | `src/cvcuda/include/cvcuda/Op.hpp` | present | +| SCF-3 | C API impl | `src/cvcuda/Op.cpp` | present | +| SCF-4 | Private impl (`.cpp` or `.cu`) | `src/cvcuda/priv/Op.{cpp,cu}` | present (`.cpp`↔`.cu` is the author's choice → N-A either way) | +| SCF-5 | Private header | `src/cvcuda/priv/Op.hpp` | present | +| SCF-6 | C++ system test | `tests/cvcuda/system/TestOp.cpp` | present | +| SCF-7 | Python binding under `operators/` | `python/mod_cvcuda/operators/Op.cpp` | present (not in `python/mod_cvcuda/`) | +| SCF-8 | Python test | `tests/cvcuda/python/test_op.py` | present | +| SCF-9 | Bench C++ + Python + config | `bench/cpp/ops/Bench.cpp`, `bench/python/ops/bench_.py`, `bench/config/operators/.json` | all present | +| SCF-10 | Lib + priv + test CMake wiring | `Op.cpp` in `src/cvcuda{,/priv}/CMakeLists.txt`; `TestOp.cpp` in `tests/cvcuda/system/CMakeLists.txt` | all present | +| SCF-11 | Python module wiring | `ExportOp` in `Main.cpp`; decl in `operators/Operators.hpp`; `operators/Op.cpp` in `python/mod_cvcuda/CMakeLists.txt` | all present | +| SCF-12 | Bench wiring | `ops/Bench.cpp` in `bench/cpp/CMakeLists.txt`; `ops/bench_.py` in `bench/python/CMakeLists.txt`; `` in `bench_params.json` | all present | +| SCF-13 | Docs rows | `operator_list.rst` row + `operators.rst` autofunction (fn + `_into`) + latest-relnote bullet | all present | +| SCF-14 | SPDX headers | the op's new source/test/bench/doc files | present (`.json` exempt) | + +A green scaffold phase = the skeleton is complete and wired (and, unless `--bare`, the contract +is authored). The done-gate (below) stays red until it is implemented. + +--- + +## Phase `done` — the deterministic final regression checklist + +Runs the scaffold checks **plus** the groups below. Green ⇒ correct, complete, documented, in +the relnotes, and optimization-ready. + +### Domain: implementation + +| id | check | probe | PASS condition | +|----|-------|-------|----------------| +| IMP-1 | No stub markers | `TODO`, `t.fail`, `noop`, the template's `std::generate(goldVec…)` / placeholder `ASSERT_EQ(goldVec, testVec)` | none remain in the op's src/test | +| IMP-2 | Limitations table filled | header | no `TODO`/`[TODO]` rows (also SPEC-MATRIX) | +| IMP-3 | Multi-GPU safety | if priv has `cudaMalloc`, it uses `PerDeviceResource<>` | satisfied / N-A if no device alloc | +| NVTX-1 | NVTX marker present **and** registered | `CVCUDA_NVTX_RANGE("cvcuda…Submit")` in `Op.cpp` (the scaffold templates emit the submit / priv `operator()` / Python `NvtxTrace` markers — this guards they survived) **and** a `"": ("cvcudaSubmit", _)` row in `tests/cvcuda/python/test_nvtx_markers.py` `OPERATORS` | both present → else GAP. Every public operator (`fn` + `_into`) must be in the registry or `test_all_operators_registered` fails once always-on NVTX is merged; the invocation helper must issue a minimal real call so the submit range actually fires | + +### Domain: coverage + test-rigor (the teeth — stricter than `review_op`) + +Declared-matrix mirroring with **hard GAPs**; the header Limitations table is the source of +truth (an integer-only op is never forced to add float). Uses a fixed `FMT_`/`TYPE_`/bench-name +→ canonical-dtype table. COV iterates **only declared dtypes/channels**. + +| id | check | probe | PASS condition | +|----|-------|-------|----------------| +| COV-GOLD | Independent CPU gold reference | `Gold`/`Reference`/`Ref`/`naive`/`CPU` fn in `TestOp.cpp` | present (hard; independence stays MANUAL via SPEC-CORRECT) | +| COV-BITEXACT | Results compared bit-exact | `EXPECT_EQ`/`ASSERT_EQ` of gold vs result; any unexplained `EXPECT_NEAR`/`ASSERT_NEAR` | bit-exact; a NEAR site → **GAP** | +| COV-1 | Every declared **dtype** tested | `FMT_*`/`nvcv::TYPE_*`/type-list tokens in positive cases | each declared dtype detected → else GAP (strict, no MANUAL) | +| COV-CHAN | Every declared **channel count** tested | channel of the `FMT_*` tokens | each declared channel (1/3/4) detected → else GAP | +| COV-2 | Every declared dtype **benched** | `dtypes` in `bench/config/operators/.json` (canonical map) | each declared dtype present → else GAP | +| BEN-DRV | Bench **drivers** implemented (not the NHWC-only stub) | `bench/cpp/ops/Bench.cpp` + `bench/python/ops/bench_.py` carry no `TODO(make-op)` markers | both real → else GAP (the structural BEN-* checks pass on a skip-everything stub, so this guards the drivers actually exercise the declared layouts/containers; see `BenchFlip.cpp`) | +| BEN-GUIDE | RGB-benchmark-guideline tests pass | runs `bench/tests/test_bench_rgb_guidelines.py` + `test_run_bench_config_key.py` (static, no GPU) | green → else GAP. These gate the **CI baseline burn-in** *before* any benchmark runs, so a config that is unclassified in `bench/config/operator_categories.json`, mis-tiered (Cat-A scalar in basic), or that drifts the tripwire key/row counts fails the regen silently. `mkop.sh` auto-adds a category-`A` entry; **verify** A (general per-pixel op) vs B (inherently single-channel) vs C (intrinsic channel semantics), keep Cat-A basic RGB-only (single-channel + RGBA in advanced, R2/R3/R4), and bump the counts in `test_run_bench_config_key.py` for the op's added profiles | +| COV-3 | Each declared **container** tested + benched | TST-2/3 + BEN-14 promoted | Tensor (+ VarShape unless tensor-only) covered | +| COV-PARITY | Equivalent image-layout parity | `PlanarParityUtils`/`matches_interleaved`, or `Reformat`+planar+`EQ` in the test | native planar == reformat→interleaved-op→reformat, bit-exact | +| COV-5 | Image-layout completeness | header declares planar layouts + COV-PARITY + bench `NCHW` & `NCHW_FAKE` | satisfied; **only** escape = the public C header declares planar image layouts not applicable and gives a reason | +| COV-NEG | Complement rejected | `Op_Negative` asserting `NVCV_ERROR_INVALID_ARGUMENT` for an unsupported dtype + layout + channel + in/out mismatch | each represented | +| COV-MATRIX | Per-dtype × per-container exhaustiveness | — | **MANUAL** (tokens can't fully prove) | + +### Domain: docs + +Runs `review_op`'s **docs domain directly** (DOC-1 operator_list row, DOC-2 autofunction +fn+`_into`, DOC-5 pybind docstrings, DOC-7 SPDX; DOC-3/4/6 MANUAL) — these would otherwise be +missed, since `optimize_op` preflight only pulls test+bench. Plus: + +| id | check | probe | PASS condition | +|----|-------|-------|----------------| +| DOC-REL | Operator in the latest release notes | bullet in `docs/sphinx/relnotes/vX.Y.Z-*.rst` (X.Y.Z = `CMakeLists.txt` `VERSION`) | present → else GAP | + +### Domain: execution (`--run`, mandatory for final green) + +| id | check | PASS condition | +|----|-------|----------------| +| EXEC-1 | C++ tests build + run + pass | `--run` builds `cvcuda_test_system` and runs `Op.*` / `TestOp` → all pass; no GPU → `GAP`/`MANUAL`, defer to CI | +| EXEC-2 | Python tests run + pass | `--run` runs `tests/cvcuda/python/test_op.py` → pass; no GPU → `GAP`/`MANUAL` | + +### Domain: readiness (reuses `optimize_op.py --phase preflight`) + +| id | check | PASS condition | +|----|-------|----------------| +| RDY-1 | Optimization-readiness | `optimize_op.py --phase preflight` green — correctness + bench coverage (re-runs `review_op`) + baseline captured (`GAP [requires CI]` if absent) + profiling available | + +### Domain: review (reuses `review_op.py`, all four domains) + +`make_op.py --phase done` runs `review_op.run(P, all 4 domains)` so the full support / test / +bench / docs checklist (`SUP-*`/`TST-*`/`BEN-*`/`DOC-*`) is enforced. The COV/IMP/EXEC groups +above are make-op-specific *additions on top* (they tighten `review_op`'s MANUAL/REC items into +GAPs for a brand-new operator). + +--- + +## Notes + +- **Python scope (repo split):** C++ owns numerical bit-exact regression; Python tests stay + API-surface (`make_op_tests`: layouts / `_into` / negative). COV-BITEXACT + the gold-reference + mandate apply to the **C++** suite only. +- **Benchmark coverage (representative-sampled, deliberate):** tests are exhaustive over the + declared matrix; image-operator benchmarks mandate the `layout` axis on every config plus native + `NCHW` and the `NCHW_FAKE` reference + + the basic-tier floor (BEN-14) + every declared dtype benched once (COV-2) + baselines + (`GAP [requires CI]`). No full cross-product (it explodes the suite and breaks row parity). +- **Benchmark calibration (1–2 ms interleaved, mandatory):** because runtime scales with per-pixel + bytes (1–16 across u8…float4), **a single shape cannot calibrate all dtypes** — use **per-dtype + configs** with per-dtype batch sizes, as `flip.json` does. Within a dtype, **calibrate so the + INTERLEAVED (`NHWC`) config runs 1–2 ms of nvbench GPU (kernel) time** on the target SKU — long + enough that timing noise is low, short enough that the suite stays fast. + **Apples-to-apples (mandatory): the interleaved, native-planar (`NCHW`) and fake-planar + (`NCHW_FAKE`) configs of the same dtype must share the SAME input size** (`shape`), so the three + layouts are directly comparable. Calibrate the *interleaved* config to 1–2 ms; the planar and + fake-planar configs reuse that same shape and **may legitimately run longer** (planar moves + 1 element/thread; fake-planar adds reformat traffic) — that is expected and must not be "fixed" by + shrinking their batch. (This is checked statically by `review_op`'s `BEN-SIZE` and is why the + `BenchConfig.json` template shares one `shape` per dtype across the three layouts.) + Calibrate by measuring: `./build-rel/bin/bench_ -a "shape=NxHxW" -a "InOutDataType=…" …` + reports `Cold: ms GPU`; adjust the batch `N` until the **NHWC** `t ∈ [1,2] ms`. Memory-bound + element-wise ops can reuse `flip.json`'s batches directly (same traffic profile). +- **Benchmark drivers + run (`BEN-DRV`, run-validated):** the C++ and Python drivers must be real + (no `TODO(make-op)` stub; `BEN-DRV` GAPs otherwise) and must be **exercised**: build + `bench_` + run it (all configs Pass, none unintentionally Skip), and run + `python3 bench/run_bench.py --operator ` to confirm **noise < 5%** and **C++/Python parity** + (BEN-11; run-dependent, so verified with `--run`/CI, not statically). +- **Baseline regen (CI, closes `BEN-7`/`RDY-1`):** once the configs are calibrated and run-green + **locally**, trigger the named CI `baseline-regen` workflow to seed baselines on the **reference + SKUs** and import its artifacts with + `python3 bench/_internal/update_baseline.py --from --operator ` (never fabricate + baselines). + Local absolute timings are SKU-specific, so the gating baselines come from CI, not the dev box; + after import, `make_op.py --phase done` shows `BEN-7`/`RDY-1` green. + Current trigger, artifact, and import mechanics live in `bench/README.md` under "Regenerating + baselines via CI"; CI selection semantics live in `ci/README.md`. +- **SonarQube "Sonar way" gate (hard CI gate, `allow_failure=false`):** the MR pipeline runs a + SonarQube quality gate that fails on **any** new issue (`new_violations > 0`) and on unreviewed + new security hotspots. Existing operators are grandfathered, so a brand-new op's code is judged + against a zero-tolerance bar and the idiomatic-but-noncompliant boilerplate trips it. Write the + op's `.cpp`/`.hpp`/test/bench **Sonar-clean from the start** (CUDA `.cu` files are excluded from + analysis). The recurring rules and their fixes: + - **S3608** default lambda capture — use explicit captures (`[handle, stream, in, out]`), never `[&]`. + - **S5025** manual `new` — the operator's `Create` transfers ownership to the C handle; keep the + `// NOSONAR` on that one line (NOSONAR is honored on NVIDIA's server) rather than fighting it. + - **S5817** non-mutating method should be `const` — the public `operator()` is `const`. + - **S3471/S3576** redundant `virtual` on an override — write `… handle() const noexcept override;`. + - **S1659** multiple declarations per line — one identifier per statement (e.g. split + `TensorWrapHandle input(in), output(out);`). + - **S6012** redundant class-template args — rely on CTAD (`std::uniform_int_distribution dist(0, m);`). + - **S5827** repeated type — `auto`. **S1301** two-case `switch` — use `if`. **S1481** unnecessary + lambda capture of a `constexpr`/const-integral — drop it. + - **S924** >1 `break`/`goto` per loop — GTest `ASSERT_NO_THROW`/`EXPECT_NO_THROW` expand to `goto` + labels, so **don't wrap calls in `*_NO_THROW` inside a loop**; call the helper directly (a throw + still fails the test). The `mkop.sh` C-API + C++ templates are already Sonar-clean; this is on + the agent-written test/bench code. Verify locally by reading the `sonarqube` job log on the MR. +- **Samples:** not gated (consistent with `review_op`); optional/manual. diff --git a/.agents/guidance/OPTIMIZATION_GUIDELINES.md b/.agents/guidance/OPTIMIZATION_GUIDELINES.md new file mode 100644 index 000000000..043a4cc68 --- /dev/null +++ b/.agents/guidance/OPTIMIZATION_GUIDELINES.md @@ -0,0 +1,646 @@ +[//]: # "SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved." +[//]: # "SPDX-License-Identifier: Apache-2.0" + +# CV-CUDA Operator Optimization Guidelines + +How to land a performance change in CV-CUDA -- kernels, host-side overhead, +caches, allocators, and Python bindings -- without regressing correctness or +neighbouring code paths. These guidelines apply to humans and AI agents. + +For *reviewing* an optimization MR, see [REVIEW_PR_GUIDELINES.md](REVIEW_PR_GUIDELINES.md). +For bug fixes that may incidentally improve performance, see +[BUGFIX_GUIDELINES.md](BUGFIX_GUIDELINES.md). + +## Golden Rules + +These rules override every workflow shortcut, technique, or convenience. +An optimization MR is not ready unless all four hold. + +1. **Correctness is mandatory.** Do not ignore, disable, weaken, or silently + retune tests. Any deliberate precision or correctness trade requires explicit + human-maintainer review. Before touching a kernel, confirm pixel-level + regression coverage against an independent reference: integer outputs use + bit-exact checks such as `EXPECT_EQ`; floating-point outputs use a stated + tight tolerance such as `EXPECT_NEAR(tol)` when device FMA contraction makes + bit-exact host comparison invalid. If coverage is missing, add it before + optimizing or leave that kernel untouched and record the gap as a required + follow-up. +2. **Benchmark evidence is mandatory.** Every claimed win needs a named-hardware + before/after, and no benchmark in the regression surface may slow down beyond + its noise band. The regression surface is the existing benchmark suite, + including sibling configurations and other operators that share the modified + kernel or code path. Any committed baseline update that replaces a previous + comparable baseline row with a slower same-key value is blocking by default. +3. **CI evidence is mandatory.** Local benchmarks guide iteration. The + review-ready MR needs a passing CI run on established reference hardware, + with the benchmark baseline updated when CI flags real improvements. +4. **Limit memory growth.** Operational memory should remain unchanged by + default. Up to **10 MB** of peak attributable growth is + automatically allowed if no new runtime CUDA allocation/free path is added. + Anything larger, or any new allocation/free path, requires explicit human + review before submission. + +## Definitions + +- **Full benchmark surface**: the union of every exact case expanded by + `expected_case_keys_for_entry` from every declared operator config entry. + Each entry's authoritative `tier` must be `basic` or `advanced`; an operator + need not declare both. The nested baseline keys must exactly equal that union, + covering all benchmarked layouts, dtypes, sizes, and variants. Use + `bench_ --list` and `run_bench.py ... --operator --tier basic,advanced`. +- **Optimization-target surface**: rows in the full benchmark surface that are + real operator execution paths. Layout-conversion comparison rows such as + `NCHW_FAKE` are reference-only: include them in regression evidence and the + full-operator Impact statistics, but do not pick them as optimization leads + or spend strike/triage budget on them. Every FakePlanar row is advanced-tier + and Tensor-only, and must have exactly one same-tier native `NCHW`/`CHW` row + whose other expanded axes are identical; unmatched or ambiguous rows are not + a comparison surface and block preflight, final evidence, and summary output. +- **Regression surface**: the full existing benchmark suite, not a selected + subset. Include the target operator's other configurations and any other + operator that shares changed code. +- **Profile/lead coverage**: every row in the optimization-target surface is + either directly profiled or mapped to a representative profiled kernel group, + and every group has an optimization-lead disposition: improved, at ridge, + triaged after struck leads, or unchanged/no-regression with a measured reason. + Reference-only rows still need harness-verified no-regression evidence, but + they are not optimization leads. +- **Noise band**: the per-config noise/parity gate reported by the benchmark + harness; see [bench/README.md](../../bench/README.md). A benchmark change is real + only when it clears this band. A regression is a slowdown beyond this band. +- **At ridge**: profiled near the hardware limit, typically about 90% or higher + `BWUtil` / SOL for memory-bound kernels, with no meaningful headroom left. +- **Triaged**: left unoptimized for a measurement-backed reason, never for an + unprofiled "low ROI" estimate. + +## Workflow + +Follow these phases for every optimization campaign. + +### 1. Survey And Scope + +Baseline the full benchmark surface before narrowing the work. Rank +optimization-target configurations by measured headroom, weighted by absolute +cost: a 36% `BWUtil` case taking 1.5 ms outranks a 39% case taking 0.7 ms. +Do not optimize layout-conversion reference rows (`NCHW_FAKE`/`CHW_FAKE`); +they compare a native layout path against the equivalent reformat-and-run path. + +Build a profile/lead coverage map before coding. For each benchmark row, record +the kernel group or host path it exercises, the representative profile that +covers it, and the initial candidate leads. Grouping rows is allowed only when +they share the same implementation path and bottleneck; otherwise profile them +separately. + +Treat layouts and variants as separate until proven otherwise. In CV-CUDA, +NHWC vs. NCHW and Tensor vs. VarShape often use different kernels; optimizing +one does not prove the other is done. + +Each attempt stays narrow: one operator, one variant, one input type, and one +optimization idea. The campaign stays exhaustive: every surveyed configuration +with material headroom is either optimized or measurement-backed-triaged. +Out-of-scope ideas start a separate branch and MR. + +Public C, C++, and Python API/ABI must remain unchanged. Signature or behavior +changes are feature work, not optimization work. + +### 2. Establish Coverage And Baseline + +If benchmarks do not cover the scoped work, add benchmark coverage first and +push it separately from optimization changes. Do not put the benchmark and the +optimization in the same commit; doing so erases the pre-optimization baseline. +New benchmarks must pass the noise and parity gates. + +For each kernel to be optimized, verify pixel-level regression tests against an +independent reference across the kernel branches being changed: vector body, +scalar tail, and each broadcast or parameter mode. Coverage of one layout or +variant is not coverage of another kernel. + +Capture the baseline before coding. Local numbers are acceptable for iteration; +CI on the benchmark-only SHA is the reference for new benchmarks. Existing +benchmarks may already have a usable CI baseline. + +### 3. Profile Before Coding + +Profile the baseline and name the bottleneck before writing code. Classify it as +memory-bound, compute-bound, issue-bound, launch-overhead-bound, or sync-bound, +then derive the hypothesis target from the SOL ceiling or host-side profile. +See [Profiling](#profiling). + +When available, run `compute-sanitizer --tool memcheck ./bench_` on the +baseline before optimizing. If the tool or required GPU environment is +unavailable, record that fact in the MR. + +### 4. Run The Optimization Loop + +Run this loop until no new promising leads remain. Failed attempts are dropped; +the MR is the sequence of kept attempts. + +1. **Profile** the current hot configuration and name the bottleneck. If a kept + win changed the profile, re-profile before choosing the next lead. +2. **Pick the most promising lead** from [Technique Leads](#technique-leads) or + from deeper profiling. Do not choose by ease alone; choose by measured + headroom and likely impact on the full regression surface. +3. **State the hypothesis** in the work log: evidence, named metric, and target, + for example `Memory SOL 38% -> >=70%, about 1.8x`. +4. **Implement and test one change** at the level the bottleneck dictates: + kernel, launch config, host-side path, or shared utility. Reuse local + primitives before adding generic abstractions. +5. **Evaluate** correctness, profile movement, benchmark delta, and operational + memory footprint against the baseline. Re-profile with the same flags and use + `ncu --diff` when `ncu` drove the hypothesis. An unchanged-memory claim may + use code inspection. Any positive peak attributable increase requires a + before/after measurement on a representative workload. Record whether the + change adds a runtime CUDA allocation or free path, and apply Golden Rule 4. +6. **Review/refactor gate** before lock-in. Inspect the attempted diff like a + reviewer, then run `python3 tools/refactor_op.py --phase assess` + to catch operator-scoped redundancy such as duplicated Tensor/VarShape + kernels, local helpers that should use shared utilities, and dead code. Fix + every relevant recommendation introduced or exposed by the attempt before + committing, or document why intentional duplication is required for + performance, translation-unit locality, ABI, or another concrete technical + reason. If a refactor is applied, run `refactor_op.py --phase verify`, the + frozen operator tests, and the targeted benchmark again before committing. +7. **Lock in or strike** by the criteria below. Keep a successful attempt as a + `perf:` commit. Drop or revert a struck lead and record the evidence. Never + keep a change whose only evidence is an unprofiled wall-time improvement. +8. **Rinse and repeat**: after a kept win, re-profile and pick the next most + promising lead. Update the profile/lead coverage map after each kept win or + strike. Stop only after the new-lead strike budget is exhausted. + +A numerical-test failure is either a logic regression or an explicit precision +trade. Drop logic regressions. Document precision trades in the commit body and +request human-maintainer review. Do not silently bump tolerances. + +### 5. Finish The MR + +After a kept win, push for CI evidence. If CI flags an expected improvement, +update the benchmark baseline in the same MR using the baseline tooling in +[bench/README.md](../../bench/README.md). Import collected JSON run artifacts +with `bench/_internal/update_baseline.py`; do not hand-edit baseline blocks. Before review, +`tools/optimize_op.py --phase evidence --base ` must pass ODO-7, +which runs `bench/_internal/validate_baselines.py --reject-regressions-from ` across the +committed baseline set and rejects same-key committed baseline slowdowns. New +benchmark rows with no previous comparable baseline are seeded through the +normal CI baseline workflow; existing rows must not be reset slower unless +there is an explicit maintainer-reviewed exception outside the optimization +done-gate. + +Use `perf:` as the subject prefix for kept optimization commits. Keep profiler +artifacts such as `.ncu-rep` and `.nsys-rep` files local; summarize their +evidence in the commit body or MR description. + +If a correctness bug in the operator kernel is fixed during the campaign, +preserve the bug-fix workflow as an immediate `test():` then +`fix():` pair. The test commit must change only `tests/` and include +an operator-specific regression; unscoped, unpaired, or implementation-bearing +test commits do not bypass the `perf:` hygiene gate. + +Maintain the MR results summary throughout development. Local numbers must be +clearly labeled provisional. Before review, add complete CI results for every +reference SKU and set the block to `final`. Local rows may remain only with the +prominent non-reference warning. See +[Results Summary Format](#results-summary-format). +The MR description is the authoritative home for the Results Summary. Keep +optimization evidence as summary text in the MR, and keep any +`tools/optimize_op.py --phase evidence` results file as a temporary local +artifact generated from that MR description. + +Close the campaign only when every optimization-target configuration is +optimized or measurement-backed-triaged, and the remaining headroom has +exhausted the new-lead strike budget below. Every row in the full benchmark +surface must be present in the result data validated by the harness; +layout-conversion reference rows need no-regression evidence but no +optimization-lead disposition. Do not expand the concise MR summary with a +per-configuration detail table. + +## Accept, Drop, And Stop Criteria + +Use these definitions for every attempt, lead, and campaign decision. +An attempt is one atomic optimization change, committed separately if kept. +A lead is one optimization idea matched to a profiled bottleneck. A campaign is +one MR's optimization effort for one operator. + +**Success**: keep the attempt as its own commit only when all are true: + +- functional and unit tests stay green; +- the targeted metric moved in the hypothesized direction; +- the benchmark gain clears the config's noise band; and +- the regression surface has no slowdown beyond noise; and +- operational memory grows by no more than 10 MB with no new runtime CUDA + allocation/free path; any other result has explicit human review before + submission; and +- the review/refactor gate is clean: relevant `refactor-op` recommendations are + fixed, or intentional duplication is documented with a concrete technical or + measured reason. + +A gain that clears the noise band is a success even if it misses the hypothesis +target. The hypothesis sizes the opportunity; it is not the pass/fail line. + +**Strike**: drop or revert the lead when the result is within noise, the +targeted metric does not move, tests fail, any regression surface benchmark +slows down beyond noise, or an increase above 10 MB or new runtime CUDA +allocation/free path does not receive explicit human review. Do not submit such +a memory result while review is unresolved. Log one line: what was tried, what +metric failed to move, which config regressed, or the measured memory increase. +A mechanical correction to the same idea, such as fixing a launch-bound bug or +instrumentation mistake, may be retested, but do not turn one lead into an +open-ended search. + +**New-lead strike budget**: after each kept win, re-profile and start a fresh +search for the next most promising lead. A successful lead resets the strike +count. Three struck new leads after the last kept win exhaust the campaign for +the currently scoped operator/configuration surface. A dropped lead does not +finish a configuration that still has measured headroom; it must be covered by +another successful lead, triaged at ridge, or counted toward this exhaustion +rule. + +**Triage**: leave a configuration unoptimized only with measurement-backed +evidence: it is already at ridge, or the applicable leads struck out. Estimated +effort, implementation risk, or "low ROI" is not sufficient until the config has +been profiled. + +**Stop**: the campaign is exhausted only when every surveyed configuration is +optimized or triaged, and the new-lead strike budget for remaining headroom is +spent. + +## Profiling + +Classify the bottleneck before choosing a technique. Every number in the work +log, commit body, or MR description must come from benchmark or profiler output. +Do not fabricate or extrapolate metrics. + +Start with the benchmark's CPU-time and GPU-time columns. If GPU time dominates, +use Nsight Compute (`ncu`) on the hot kernel. If CPU time is high, launches are +fragmented, or CPU/GPU times diverge, use Nsight Systems (`nsys`) to diagnose +launch overhead, synchronization, allocator overhead, or Python binding cost. + +```bash +ncu --launch-skip 10 --launch-count 5 --kernel-name regex:"" --set full -o pre.ncu-rep ./bench_ +nsys profile -t cuda,nvtx,osrt -o sys.nsys-rep ./bench_ +nsys stats sys.nsys-rep +``` + +Use `--kernel-name` with `ncu`. Without it, `--launch-skip` can land on warmup +or setup work. If the profiled `Duration`, grid, or kernel name does not match +the benchmark configuration, the SOL numbers are not evidence. + +| Profile signal | Bottleneck | Primary evidence | Typical next step | +|---|---|---|---| +| Memory SOL higher than Compute SOL, low `BWUtil`, memory stalls | Memory-bound | `SpeedOfLight`, `MemoryWorkloadAnalysis`, `WarpStateStatistics` | Improve access pattern, coalescing, vector width, or memory-level parallelism | +| Compute SOL higher than Memory SOL, one math pipe saturated | Compute-bound | `SpeedOfLight`, `ComputeWorkloadAnalysis`, roofline | Reduce or change the dominant math | +| Compute SOL high, no single pipe saturated, high issue-slot pressure | Issue-bound | `Issue Slots Busy`, `Executed IPC`, pipe utilization | Hoist invariants or reduce instruction count | +| Many small launches or CPU time dominates GPU time | Launch-overhead-bound | nvbench CPU/GPU columns, `nsys` timeline | Batch work, collapse per-channel/per-plane launches | +| Host gaps, stream waits, CPU/GPU synchronization | Sync-bound | `nsys` timeline | Remove unnecessary synchronization or reuse stream-ordered resources | + +Read `SpeedOfLight` first for kernel work. The higher of Compute SOL% and +Memory SOL% names the bound; `100 / max(SOL%)` is a rough speedup ceiling for +the hypothesis. A memory-bound kernel already near ridge is normally triaged +instead of chasing noise. + +After a change, capture `post.ncu-rep` with the same command and diff it: + +```bash +ncu --import pre.ncu-rep --import post.ncu-rep --page diff +``` + +Report only the summary evidence, not raw profiler files. A useful profile +summary includes the bottleneck, the technique, the moved metric from the diff, +and the benchmark delta. For memory-bound wins, also include `BWUtil` before and +after. + +Further reading: NVIDIA TensorRT-LLM's performance-analysis skill has a broader +profiler-agnostic methodology: +. + +## Results Summary Format + +The MR description is the human-readable report and the input to the +deterministic done-gate. Keep exactly one bounded, versioned summary block. The +visible portion is deliberately concise; full per-case data remains in the +benchmark artifacts and is checked by `tools/optimize_op.py` rather than copied +into the description. + +This contract describes one operator-optimization campaign. Use the exact MR +scope `perf(bench)` for aggregate benchmark-suite, harness, configuration, or CI +maintenance that has no runtime implementation changes; CI verifies that scope +against the complete diff. A live v1 marker always opts into this contract. + +### Metadata Contract + +Begin the block with a standalone `cvcuda-optimize-summary:v1` HTML-comment +line containing compact JSON. End it with the matching standalone end marker. +Markers inside fenced code examples do not count; duplicate or nested live +blocks are invalid. The object has the following required fields and may add +the optional `impact_metric` and `secondary_operators` fields described below: + +```markdown + +... + +``` + +`state` is exactly `provisional` or `final`. `optimized_cases` contains the +unique exact case keys intentionally covered by the optimization; it is not a +list of only the cases that got faster. The harness rejects duplicate, missing, +or unknown keys, a candidate SHA that is not the revision being validated, and +a baseline SHA that is not its ancestor. Do not put calculated statistics or +human-authored evidence in the hidden object. The visible Markdown remains the +authoritative report for those claims. + +`impact_metric` is optional and defaults to `cpp_time`, preserving summaries +that predate the field. Its only other allowed value is `python_overhead`. The +generator omits the default from canonical metadata and emits +`"impact_metric":"python_overhead"` only when the campaign targets Python +binding overhead. + +`secondary_operators` is an optional array of operator names whose generated +baseline config must change because the primary implementation shares the +optimized runtime path. A declaration does not authorize broader scope by +itself: the internal code-reviewed policy must allow the exact MR IID, primary +operator, and complete secondary set, and CI requires the changed operator +configs to match that declaration exactly. Missing policy, an undeclared or +unchanged secondary, or any additional operator fails closed. The normal rule +remains one MR and one summary per operator. When exercising an approved +exception locally, pass the exact MR IID to the evidence gate with `--mr-iid`. + +`baseline_commit` identifies the revision that supplies the before benchmark +measurements. It is distinct from `--base`, which identifies the changed-set +reference for the MR evidence gate; they may resolve to the same commit but are +not interchangeable. The baseline must be an ancestor that precedes the +candidate, never the candidate itself. Both metadata revisions are full +lowercase 40-character commit SHAs; the generator resolves symbolic refs before +writing them. + +A summary configuration is one unique expanded benchmark case key, independent +of SKU and benchmark driver. The full set is the union expanded from every +declared `basic` or `advanced` candidate entry, including reference-only +comparison cases; it is never only the optimized, touched, or result-present +rows. Candidate nested baseline keys must equal that union exactly. The +optimized set is `optimized_cases`. A fake-planar comparison case is +reference-only and cannot be in that set. Summary generation and validation +reject a partial FakePlanar surface instead of silently omitting unmatched +cases from the layout-comparison statistics. + +### Canonical Visible Format + +Use the following headings, tables, labels, and order. Generator-owned values +must be regenerated rather than hand-edited. + +```markdown +## ExampleOp optimization summary + +> Final reference-hardware evidence · base `` · candidate `` + +**Primary bottleneck: Memory-bound** + +Representative profiles showed 88–94% Memory SOL versus 23–38% Compute SOL, +with `long-scoreboard` as the dominant stall. + +### Scope + +**Configurations optimized:** 18 / 42 total + +**Optimized categories:** + +- `Tensor Interleaved RGBF32` +- `Tensor Planar RGBF32` +- `VarShape Interleaved RGB8` +- `VarShape Planar RGB8` + +### Impact + +Speedup is `before / after`; `2.00x` means twice as fast. + +| SKU | Configuration scope | Configurations | Min | Median | Max | +|---|---|---:|---:|---:|---:| +| A100 | Optimized | 18 | 1.18x | 1.74x | 3.02x | +| A100 | Full operator | 42 | 0.99x | 1.11x | 3.02x | +| H100 | Optimized | 18 | 1.12x | 1.58x | 2.71x | +| H100 | Full operator | 42 | 0.99x | 1.08x | 2.71x | + +### Layout comparison + +Timing ratio is `numerator / Planar`; values above `1.00x` mean Planar is faster. + +| SKU | Comparison | Before min / median / max | After min / median / max | +|---|---|---:|---:| +| A100 | Interleaved / Planar | 0.81x / 0.94x / 1.08x | 1.14x / 1.39x / 1.67x | +| A100 | FakePlanar / Planar | 0.96x / 1.10x / 1.27x | 1.32x / 1.57x / 1.93x | +| H100 | Interleaved / Planar | 0.84x / 0.97x / 1.11x | 1.09x / 1.31x / 1.54x | +| H100 | FakePlanar / Planar | 0.98x / 1.13x / 1.30x | 1.27x / 1.48x / 1.82x | + +### Evidence checklist + +- [x] **Pixelwise equality to reference** — `` +- [x] **Memory-footprint checks** — `` +- [x] **Baselines updated** — `` +- [x] **Baseline validation** — `` +- [x] **Lead exhaustion** — `` +- [x] **Review/refactor gate** — `` + +### Top learnings + +- Vectorized RGB loads made Tensor Interleaved RGBF32 configurations up to 3.02x faster. +- Wider launch blocks increased register spilling and degraded VarShape performance, so that attempt was dropped. +``` + +The primary bottleneck value is exactly `Memory-bound` or `Compute-bound` and +must appear before Scope. Follow it with measured profiler evidence supporting +the classification. The binary top-level assessment does not replace the more +specific issue, launch, or synchronization diagnosis used in the campaign. + +`Configurations optimized: X / Y total` reports intentional scope, not how many +cases improved. `X` is the number of unique optimized case keys; `Y` is the +candidate's complete union of expanded keys from all declared `basic` and +`advanced` entries. List each distinct optimized category once in +`Container Layout Type` order, for example +`Tensor Interleaved RGBF32`. The harness derives these categories from the exact +keys, sorts the complete category strings lexicographically, and rejects +missing, extra, or differently ordered categories. + +Derive Container from `inputKind` (`Tensor`, `TensorBatch`, or `VarShape`), +defaulting to `Tensor` only for benchmarks without a container axis. Map +`NHWC`/`HWC` to +`Interleaved`, `NCHW`/`CHW` to `Planar`, and an absent layout axis to +`NoLayout`. Render the type as a friendly image type such as `RGB8`, `RGBA8`, +or `RGBF32` when channel semantics are known; otherwise use the canonical +scalar/vector type such as `U8`, `F32`, or `2S16`. Use `A→B` for conversions. +Do not list shape, tier, mode, or other narrow benchmark axes as categories. + +The Impact table always contains two rows for every reported SKU: `Optimized` +and `Full operator`. With the default `cpp_time` metric, calculate each case's +speedup as the baseline C++ GPU time divided by candidate +`gpu_time_us_cpp`. Report the minimum, standard median, and maximum across the +named case set as factors rounded to two decimals. Do not substitute geomean, +Python time, percentage improvement, or only improved cases. This default uses +the canonical explanation and table shown above, so existing v1 summaries +remain valid without an `impact_metric` field. + +With `impact_metric` set to `python_overhead`, derive each case's overhead as +`gpu_time_us_python - gpu_time_us_cpp` separately for the baseline and +candidate. Derive reduction as `before - after`, so positive values mean less +Python overhead. The canonical Impact section instead uses this deterministic +microsecond table: + +```markdown +Python overhead is `gpu_time_us_python - gpu_time_us_cpp`; reduction is `before - after`, so positive values mean less overhead. + +| SKU | Configuration scope | Configurations | Before min / median / max | After min / median / max | Reduction min / median / max | +|---|---|---:|---:|---:|---:| +| A100 | Optimized | 18 | 12.00 µs / 18.00 µs / 24.00 µs | 2.00 µs / 4.00 µs / 8.00 µs | 8.00 µs / 14.00 µs / 22.00 µs | +| A100 | Full operator | 42 | 10.00 µs / 17.00 µs / 25.00 µs | 1.00 µs / 5.00 µs / 9.00 µs | 7.00 µs / 12.00 µs / 23.00 µs | +``` + +Report before, after, and reduction minimum, standard median, and maximum, +rounded to two decimal places. Every Full operator row requires before/after +`gpu_time_us_cpp` for `cpp_time`, or both `gpu_time_us_cpp` and +`gpu_time_us_python` for `python_overhead`, for every key in the full union on +that SKU. A missing tier, entry, case, or required timing is blocking; the +harness recomputes the values and counts from the benchmark data. Every SKU +present in either artifact must have that full coverage and appear in the +tables; a partially measured local SKU is blocking, not silently omitted. + +For a binding-only campaign, the evidence gate additionally requires every +optimized case on every configured reference SKU to show a positive overhead +reduction larger than its combined standard error. Within each independently +collected before/after wave, pair C++ and Python from the same artifact and use +the unbiased sample standard deviation of those Python-minus-C++ gaps +(`gpu_gap_stddev_us`). The combined error is the root sum of the before and +after gap variances divided by their respective artifact counts. Do not treat +nvbench's within-process `gpu_noise_us_*` as between-artifact uncertainty, and +do not pair before/after artifacts by burn-in index. The candidate +Python-minus-C++ gap must also remain within the shared absolute parity limit. +A reduction inside that error band is not a proven optimization even when the +summary's aggregate median is positive. + +The binding-only evidence gate derives this result from the committed raw +artifact fields; it does not trust the summary's selected Impact-table metric. +An MR that introduces a new optional summary metric may therefore retain the +previous parser's canonical `cpp_time` block while that MR is under review, +provided the exact Python-overhead table remains visible next to the block and +the raw-artifact significance/parity gate passes. After the schema extension is +merged, use `impact_metric=python_overhead` for subsequent binding campaigns. + +The Layout comparison table also has two rows per reported SKU. Form pairs from +the same full union, matching by tier and every case axis except layout, and +require equal data volumes. For both baseline and candidate data, calculate +`Interleaved / Planar` and +`FakePlanar / Planar` timing ratios, then report min / median / max. A factor +above `1.00x` means native Planar is faster. If no valid pairs exist for a +comparison, put `n/a — no matched equivalent configurations` in both statistic +cells; the harness must verify that claim. A signature with no counterpart +layout produces no comparison, but its complete workload identity must still +be unique. When both sides exist, never silently omit the comparison or choose +arbitrarily between multiple same-layout cases; make the benchmark axes +unambiguous first. + +Layout comparison remains based on `gpu_time_us_cpp` for both impact metrics. +Expanded workload identities must be unique by `(tier, complete raw axes)`, +independent of their config-entry names and whether a matching layout exists. +The summary tool carries a closed transitional inventory of exact aliases that +predate this rule. It canonicalizes each listed pair to one workload so legacy +configs are not double-weighted, rejects any new alias pair, and the inventory +entry must be removed when that operator config is migrated. + +Every checklist item is mandatory. A box may be checked only when its line +contains concrete hard evidence and its corresponding done-gate check passes. +A bare `PASS`, a checked label without a named command/test/artifact and result, +or placeholder text is not evidence. Pixelwise evidence names the independent +reference, assertion, branch coverage, and passing test. Baseline evidence names +the imported artifacts/updated generated files and the passing +`bench/_internal/validate_baselines.py --reject-regressions-from ` result. Lead exhaustion +names at-ridge measurements or the three struck leads since the last kept win. +Review/refactor evidence includes the assessment, disposition, and, when a +refactor was applied, verification, frozen tests, and benchmark result. + +Memory-footprint acceptance is owned by ODO-9 and its canonical companion +`## Memory footprint` section below. Cite that gate's hard evidence and passing +result in the checklist; do not duplicate or weaken its policy. The memory item +remains unchecked until ODO-9 passes. + +Top learnings contains one to five bullets. Each bullet is one concise sentence +describing a noteworthy technique and its measured success or failure. Do not +recreate per-configuration results, split successes and failures into separate +sections, or add a sixth bullet. + +### Provisional And Final States + +A provisional summary uses the same visible format. Put +`Provisional local evidence` in the status line, leave any unsupported checklist +items unchecked, and show this warning immediately below the title for every +non-reference GPU represented in its statistics: + +```markdown +> ⚠️ **Non-reference local GPU:** Results from NVIDIA RTX 6000 Ada are provisional and cannot satisfy final readiness; final A100 and H100 statistics are still required. +``` + +Reference SKUs come from `bench/config/sku_map.json`; do not hard-code them in +the validator. They are currently A100 and H100. A final summary has complete +baseline and candidate coverage for every key in the full union, plus separate +Impact and Layout comparison rows for every configured reference SKU. It also +has all six evidence items checked with hard evidence, one to five learnings, +the current candidate SHA, and no placeholders or unresolved `MANUAL` findings. +A local-SKU row may remain for context only if the warning remains visible; it +never substitutes for a reference SKU. + +Use `tools/optimize_op.py` to initialize or refresh this block from benchmark +artifacts and to validate it through the evidence phase. The generator preserves +surrounding MR prose and the human-authored assessment, checklist evidence, and +learnings while regenerating scope, categories, Impact, and Layout comparison. + +**Memory footprint (ODO-9)**: include this canonical section with exactly these +fields (replace the placeholders). Place it before or after, never inside, the +bounded v1 summary block; summary refresh preserves it as surrounding prose. + +```markdown +## Memory footprint +Peak attributable increase: B +New runtime CUDA allocations/frees: +Evidence: +``` + +Use `0 B` when there is no increase. An unchanged claim may cite code +inspection; a positive increase requires a before/after measurement on a +representative workload, summarized on the `Evidence` line. Report the +non-negative aggregate peak-live increase; do not net growth in one path +against an unrelated decrease. Do not use scaled units. The checker returns +`MANUAL` when the increase is above 10 MB or the allocation/free field is +`yes`. Such a result requires explicit human review and must not be submitted +while that review is unresolved. + +## Technique Leads + +These are advisory leads for loop step 2. Pick only after profiling names the +bottleneck. `NIX` means elements processed per thread; `DPT` is the local +data-packing helper for vectorized writes. + +| Profile evidence | Candidate lead | Validation signal | Watch out for | +|---|---|---|---| +| Memory or latency bound; small dtype moves too few bytes per thread | Vectorize loads/stores per thread with NIX and DPT | `BWUtil` rises, memory stalls fall, benchmark clears noise | Wide elements may already be at ridge; contraction kernels can regress, so keep a measured scalar fallback if needed | +| Compute or issue bound; repeated invariant work per element | Hoist loop-invariant or redundant compute, then amortize with NIX | Issue pressure or dominant math pipe falls; output remains covered by pixel tests | Any arithmetic change that affects rounding is a precision trade, not a silent test update | +| Fixed per-thread cost dominates, such as repeated var-shape pointer lookup | Resolve invariant state once per thread and process more elements | Same body improves on expensive wrappers and remains stable on cheap wrappers | Raising bytes/thread with too many registers can reduce occupancy | +| Memory-bound kernel has similar absolute GB/s across low- and high-bandwidth SKUs | Increase memory-level parallelism with grid-stride loops, wide accesses, and enough occupancy | Reference SKU `BWUtil` improves and dev GPU does not regress | Prefer one SKU-generic kernel; only fork per architecture after proving a Pareto conflict | +| Cross-thread reductions cause shared-memory traffic or barriers | Use warp shuffle reductions, then one shared write per warp and a final first-warp reduce | Barrier/shared-memory stalls fall; tests cover edge counts and masks | Incorrect masks or tail handling create silent data errors | +| Writes are poorly coalesced | Make block X dimension at least 32 when compatible with the algorithm | Sectors/request and write throughput improve | Recheck occupancy and shared-memory layout | +| Per-channel, per-plane, or per-image dispatch dominates | Collapse launches and batch work inside one kernel | `nsys` shows fewer launches; benchmark clears noise | Preserve stream ordering, error handling, and per-image parameter semantics | +| Transient allocation, event creation, or synchronization dominates | Reuse cached stream-ordered resources such as `cudaMallocAsync`, cached events, or `PerDeviceResource` | CPU time, sync gaps, or allocator cost falls | Check multi-GPU behavior and resource lifetime | +| A local helper already covers the operation | Reuse local primitives before adding generic abstractions | Existing tests and edge cases carry over | Grep first for `InterpolationWrap`, `BorderWrap`, `filter_utils`, NIX/DPT helpers, `PerDeviceResource`, and cached-event helpers | + +## Final Checklist + +Each item is mandatory. An MR with any unchecked item is not ready for review. + +- [ ] [Golden Rules](#golden-rules) are satisfied: pixel-wise equality evidence + for all modified configurations/kernels, benchmark evidence, CI evidence, and + the canonical memory-footprint evidence. Any increase above 10 MB or new + runtime CUDA allocation/free path has explicit human review before submission. +- [ ] [Workflow](#workflow) is complete, including survey, coverage, baseline, + full-surface profile/lead coverage, scoped attempts, review/refactor gates, + API/ABI stability, baseline updates, and a passing comparable-baseline + regression gate across the committed baseline set + (`bench/_internal/validate_baselines.py --reject-regressions-from ` via ODO-7). +- [ ] [Accept, Drop, And Stop Criteria](#accept-drop-and-stop-criteria) are + satisfied for every attempt, lead, and campaign decision. +- [ ] The versioned [Results Summary Format](#results-summary-format) is final, + complete for every configured reference SKU, and has all six hard-evidence + items checked. diff --git a/.agents/guidance/REFACTOR_OP_GUIDELINES.md b/.agents/guidance/REFACTOR_OP_GUIDELINES.md new file mode 100644 index 000000000..d027ea1fc --- /dev/null +++ b/.agents/guidance/REFACTOR_OP_GUIDELINES.md @@ -0,0 +1,178 @@ +[//]: # "SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved." +[//]: # "SPDX-License-Identifier: Apache-2.0" + +# CV-CUDA Operator Refactoring Guidelines (`/refactor-op`) + +The single source of truth for the per-operator refactoring / redundancy-reduction harness. It is +**implemented by** `tools/refactor_op.py` (the deterministic checker) and **cited by** the thin +skill wrapper (`.agents/skills/refactor-op/SKILL.md`). The catalog substance lives here once; tools +delegate to it instead of duplicating the workflow. + +This document does not restate house policy; it *cites* the authoritative docs and turns their +requirements into concrete, machine-checkable items: `AGENTS.md` ("Follow existing local patterns", +"do not mix unrelated refactors into functional changes", SPDX), +`.agents/guidance/REVIEW_OP_GUIDELINES.md` (the support/test matrices reused for parity), and +`.agents/guidance/OPTIMIZATION_GUIDELINES.md` (bit-exact default). + +## Scope, and the relationship to SonarQube + +`refactor-op` finds and safely applies **operator-scoped, semantic** redundancy that generic +copy-paste detection cannot express: near-duplicate Tensor/VarShape kernels that should share a +template, re-implemented validation that should call a shared helper, a local helper that shadows a +canonical `cuda_tools` utility, dead code. It is **complementary to SonarQube CPD** +(`sonar-project.properties` + the `sonarqube` CI job), which already owns generic, whole-corpus +duplication *amount and location*. For raw duplication metrics, read Sonar; `refactor-op` does not +re-implement clone detection across the tree. It also honors Sonar's `sonar.cpd.exclusions` +philosophy: per-op benchmark and sibling-test scaffolds are near-identical *by construction* and are +out of scope here (they are part of the frozen surface — see below). + +## How to use + +```bash +python3 tools/refactor_op.py [--phase assess|verify] [--domain impl|api|xcut|all] \ + [--base ] [--format md|json] [--out PATH] [--apply] +``` +- **assess** (default) — a read-only candidate report. Every finding carries a **status**, literal + **evidence** (`file:line`), its **id**, and a named **fix**. +- **verify** — the strict parity gate for an applied refactor (see "The parity gate"). +- **Completion** of an assess-then-apply cycle is defined as: `--phase verify` shows **zero `GAP`** + and every `MANUAL` has been resolved green (the VER-6/VER-7 proofs, plus any VER-3 header/binding + inspection). + +The checker is **deterministic & idempotent**: no network, no clocks, no randomness; the changed-set +comes from `git diff `. The same tree yields a byte-identical report. + +### Status vocabulary + +| status | meaning | affects exit code | +|--------|---------|-------------------| +| `PASS` | check satisfied | no | +| `GAP` | a parity violation (verify only); an actionable hard failure | **yes (non-zero)** | +| `N-A` | not applicable (e.g. no priv files, no binding) | no | +| `MANUAL` | needs a step the checker can't take (build+run, human read); it points at the spot | no (but surfaced) | +| `RECOMMENDATION` | advisory refactoring opportunity (assess); human judges worth | no | + +Assess emits only `RECOMMENDATION`/`MANUAL`/`PASS`/`N-A` — **refactoring is improvement, not a +gate**, so an assess run is always exit 0. `GAP` arises **only** in `--phase verify`, where a +parity violation must block the refactor. + +### Two modes: assess → apply → verify + +The agent that *performs* a refactor must also produce local proof that it changed nothing +observable. Verification is this deterministic checker, run locally when the MR scope includes a +semantic refactor; the agent applies, then the checker + the frozen tests prove parity. `--apply` is +a wrapper-level flag that emphasizes the `fix` actions — the Python checker **never writes files**. + +1. **assess** — run the checker, choose a `RECOMMENDATION`/`MANUAL` to act on. +2. **apply** — the agent applies that finding's named fix on the **implementation / binding only** + (never tests, never benchmarks), reusing existing shared utilities (`AGENTS.md`). +3. **verify** — when the MR scope includes a refactor, run `--phase verify` locally to prove + parity; then build + run the frozen tests (bit-exact) and re-run assess (redundancy gone). If the + same MR also changes performance-sensitive implementation, use the operator optimization workflow + to provide benchmark evidence. + +### Inviolable rules + +- **Frozen test surface** — a refactor must not change the operator's *tested feature set*. Do not + edit `tests/cvcuda/system/TestOp.cpp` or `tests/cvcuda/python/test_op.py`; move shared + helpers outside the operator test files or split the test change into a separate MR. +- **Frozen bench surface** — do not touch anything under `bench/` (sources, configs, **baselines**). + The benchmarks are the unbiased measurement instrument; touching them could mask a regression. +- **API/ABI unchanged** — public C, C++, and Python signatures and ABI stay identical (VER-3). A + signature change is feature work, not a refactor — split it into its own MR. +- **Bit-exact** — output must be unchanged; the frozen `EXPECT_EQ` tests are the oracle (VER-6). + Never weaken a test, widen a tolerance, or fabricate a baseline to make a refactor "pass". +- **Scoped commits** — kept refactors are `refactor:` commits with nothing unrelated folded in. + +--- + +## Domain: impl (the priv implementation — kernels, validation, indexing) + +| id | check | probe | status | fix | +|----|-------|-------|--------|-----| +| RED-1 | Near-duplicate Tensor/VarShape kernels or functions in priv | shingle-hash each `__global__`/function body in `P.priv`; report pairs with `jaccard ≥ threshold` | `RECOMMENDATION` (near-dup); `PASS` if none | Unify behind a templated kernel; put the addressing difference in the accessor (cf. `OpBrightnessContrast.cu` → `DoBrightnessContrast`). | +| RED-4 | Re-implemented layout/dtype/channel validation | regex for manual `TENSOR_NCHW \|\| … TENSOR_NHWC` chains | `MANUAL` if present; else `PASS` | Replace hand-rolled layout checks with `nvcv` `TensorDataAccess` helpers (cf. `OpStack.cpp`). | +| RED-5 | Manual index/stride arithmetic duplicating accessors | stride math in a priv file that uses no `TensorWrap`/`ImageBatchVarShapeWrap`/`TensorDataAccess` | `MANUAL` if present; else `PASS` | Use `TensorWrap` / `TensorDataAccessStridedImagePlanar` accessors instead of hand-rolled stride math. | + +## Domain: api (the Python binding) + +| id | check | probe | status | fix | +|----|-------|-------|--------|-----| +| RED-6 | Duplicated binding bodies (Tensor↔VarShape, allocating↔`_into`) | shingle-hash the function/lambda bodies in `Op.cpp`; report `jaccard ≥ threshold` | `RECOMMENDATION`; `PASS` if none | Extract a shared submit helper for the Tensor/VarShape & allocating/`_into` paths; reuse `VarShapeUtils.hpp` (`CreateSameShapeImageBatch`). | + +## Domain: xcut (cross-cutting) + +| id | check | probe | status | fix | +|----|-------|-------|--------|-----| +| RED-10 | Local helper shadowing a canonical shared util | a local definition whose name ∈ the shared-util reference set | `MANUAL` if present; else `PASS` | Replace the reinvented helper with `cuda_tools/{SaturateCast,StaticCast,TypeTraits}.hpp`. | +| RED-11 | Dead code | a `static` function whose name occurs exactly once (definition only) across the op surface | `RECOMMENDATION` if present; else `PASS` | Remove the unreferenced function. | + +Source: `AGENTS.md` (reuse shared utilities; don't mix refactors into functional changes), +the `cuda_tools` accessor/cast headers, and `python/mod_cvcuda/operators/VarShapeUtils.hpp`. + +--- + +## The parity gate (`--phase verify`) + +Run locally when an MR's scope includes an operator refactor. This is not a standing CI job: +optimization, feature, baseline, and bug-fix MRs should run it only when they deliberately include a +refactoring change that must preserve observable behavior. All deterministic legs are +artifact-derived (`git diff ` and parsed matrices); the bit-exact leg is delegated to the +frozen tests. + +| id | check | PASS condition | +|----|-------|----------------| +| VER-1 | Frozen test surface | no diff under the op's test files | +| VER-2 | Frozen bench surface | no diff under the op's `bench/` sources, config, or baselines | +| VER-3 | API/ABI unchanged | no public-header signature diff; binding registration, callable-signature, and reachable-type-alias snapshots are identical | +| VER-4 | Feature-matrix parity | declared layouts/channels/dtypes identical base-vs-working | +| VER-5 | Coverage-matrix parity | test macros + parametrized value-row count identical base-vs-working | +| VER-6 | Bit-exact (`MANUAL`) | `build-rel/bin/cvcuda_test_system --gtest_filter='Op*'` green (frozen `EXPECT_EQ`) | +| VER-7 | Redundancy resolved (`MANUAL`) | re-running assess no longer reports the applied finding | + +A `GAP` on any of VER-1..5 is a hard failure: revert the offending change (it is feature/measurement +drift, not a refactor) or split it into its own MR. The gate passes when VER-1..5 are `PASS` and the +VER-6/VER-7 proofs have been run green. + +### Refactor summary (deterministic, appended to `--phase verify`) + +Every verify run ends with a quantified impact summary — the headline deliverable of the refactor, +computed from artifacts (not narrated by the agent): + +- **LOC delta** on the implementation/binding files vs `` (`git diff --numstat`): + `+insertions / -deletions (net)`. A unification should net-reduce lines. +- **Redundancy resolved** — the `RED-*` finding ids that assess reported at `` and no longer + reports on the working tree (the before→after of VER-7). `introduced` should be empty. +- **Redundancy still open** — `RED-*` ids assess still reports (remaining opportunities). + +Example: `RED-6 resolved; impl/binding net -38 LOC; parity OK`. + +--- + +## Curated data (reviewed material) + +These values tune the checker without code edits. + +similarity-threshold = 0.80 + +### Shared-util reference set +Canonical helpers a local definition must not shadow (RED-10). Header homes: +`src/cvcuda/include/cvcuda/cuda_tools/{TensorWrap,TypeTraits,SaturateCast,StaticCast,ImageBatchVarShapeWrap,BorderWrap,InterpolationWrap}.hpp`; +`src/nvcv/src/include/nvcv/TensorDataAccess.hpp` (`TensorDataAccessStridedImagePlanar`); +`python/mod_cvcuda/operators/VarShapeUtils.hpp` (`CreateSameShapeImageBatch`). +```text +SaturateCast +StaticCast +ConvertBaseTypeTo +TensorWrap +CreateSameShapeImageBatch +``` + +### Duplicate allowlist +Block names that are legitimately duplicated for a given operator and should be suppressed from +RED-1/RED-6 (e.g. two kernels that must stay separate for a measured reason). Format: +`op: , ` (one line per operator). Extend deliberately, with justification. +```text +# resize: someIntentionallyDuplicatedKernel +# (populate per operator as confirmed-intentional duplications are reviewed) +``` diff --git a/.agents/guidance/REVIEW_OP_GUIDELINES.md b/.agents/guidance/REVIEW_OP_GUIDELINES.md new file mode 100644 index 000000000..674d25b5c --- /dev/null +++ b/.agents/guidance/REVIEW_OP_GUIDELINES.md @@ -0,0 +1,199 @@ +[//]: # "SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved." +[//]: # "SPDX-License-Identifier: Apache-2.0" + +# CV-CUDA Operator Review Guidelines (`/review-op`) + +The single source of truth for the per-operator review harness. It is **implemented by** +`tools/review_op.py` (the deterministic checker) and **cited by** the thin skill +wrappers (`.agents/skills/review-op*/SKILL.md`, symlinked into `.claude/skills` for +Claude Code). The checklist substance lives here once; every tool's skill delegates +to it — no duplication. + +This document does not restate house policy; it *cites* the authoritative docs and turns +their requirements into concrete, machine-checkable items: +`AGENTS.md`, `REVIEW_PR_GUIDELINES.md`, `docs/sphinx/advanced/make_operator.rst`, +and `OPTIMIZATION_GUIDELINES.md`. + +## How to use + +```bash +python3 tools/review_op.py [--domain support|test|bench|docs|all] \ + [--format md|json] [--out report.json] [--run] [--fix] +``` +- Each checklist item is reported with a **status**, literal **evidence**, and its **id**. +- **Completion** is defined as: a re-run of the checker shows **zero `GAP` and zero + unresolved `MANUAL`** for the selected domains. That re-run is the deterministic + evidence of completion. + +### Status vocabulary + +| status | meaning | affects exit code | +|--------|---------|-------------------| +| `PASS` | check satisfied | no | +| `GAP` | check failed; an actionable deficiency | **yes (non-zero)** | +| `N-A` | not applicable to this operator (e.g. no VarShape surface) | no | +| `MANUAL` | needs human reading; checker points at the location | no (but surfaced) | +| `RECOMMENDATION` | advisory follow-up (bench coverage analysis); human may override | no | + +The checker is **deterministic & idempotent**: no network, no clocks, no randomness — the +same tree yields a byte-identical report. + +### `--fix` +The checker itself stays read-only; `--fix` directs the wrapper/agent to apply the +per-item corrective action below where mechanical, then **re-run the checker to prove +the item is now `PASS`**. Inviolable fix rules: +- **Never fabricate baselines** (a missing baseline is `GAP "requires CI"`; use the baseline + workflow in `bench/README.md`). +- **Never silently introduce or widen a test tolerance** — bit-exact is the default; a + needed `EXPECT_NEAR` emits rationale + a `needs-human` flag (see `OPTIMIZATION_GUIDELINES.md`). +- **A missing layout capability is author work** — `--fix` reports it and does not + reimplement operator functionality. + +### Operator name resolution +`` is PascalCase (e.g. `CenterCrop`). The checker derives: +- `op` — lowercase bench/config stem (e.g. `centercrop`), the key in `bench/config/bench_params.json`. +- `Op` — C++ class / test-suite stem (e.g. `CenterCrop`), the `cvcudaSubmit` infix. +- `pyname` — the Python function name (e.g. `cvcuda.hq_resize` for `HqResize`). It can + differ from `op`; resolve via the `m.def("", …)` in the Python binding / + `operator_list.rst` link, MANUAL fallback if ambiguous. + +--- + +## Domain: support (matrix + enforcement + consistency) + +Owns the support matrix `container × layout × dtype × channels` and its enforcement and +cross-surface consistency. Does **not** verify tests (that's the test domain). + +| id | check | probe | PASS condition | fix | +|----|-------|-------|----------------|-----| +| SUP-1 | Tensor container declared | primary data input of `cvcudaSubmit` is `NVCVTensorHandle` or `NVCVTensorBatchHandle` in `src/cvcuda/include/cvcuda/Op.h` | present | report (rare) | +| SUP-2 | VarShape container declared | primary data input of a Submit entry point is `NVCVImageBatchHandle` (including legacy generic `cvcudaSubmit` APIs), or an established `cvcudaVarShapeSubmit` is present | present → PASS; absent → **N-A iff deterministic tool data or operator-local evidence marks the op tensor-only, else GAP** | report | +| SUP-3 | C++ overloads match C-API containers | `Op.hpp` operator() overloads | same container set as SUP-1/2 | report | +| SUP-4 | Python binds allocating + `_into` per container | `m.def(""…)` / `"_into"` in `python/mod_cvcuda/operators/Op.cpp` | both variants per supported container | add missing binding (mechanical) | +| SUP-5 | Limitations tables present | header Doxygen `Data Layout:`/`Channels:`/`Data Type` (Input + Output) | all three present, in & out | add table row (mechanical) | +| SUP-6 | Declared layouts parsed | parse `Data Layout: [..]` | layout set extracted | — | +| SUP-7 | Declared dtypes parsed | parse `Data Type \| Allowed` rows | dtype set extracted | — | +| SUP-8 | Declared channels parsed | parse `Channels: [..]` | channel set extracted | — | +| SUP-9 | Enforcement present | guards in `src/cvcuda/priv/**` reject unsupported layout/dtype/channel with `ERROR_INVALID_ARGUMENT`; **declared == enforced** | guards found & consistent | report (often MANUAL — needs a code guard) | +| SUP-10 | Default image-layout policy | header layout set plus optional `Planar image layouts: Not applicable` and required `Reason` beside the Limitations table | `NCHW`/`CHW` declared; or a well-formed operator-local inapplicability declaration; never both | author | +| SUP-11 | Cross-surface consistency | diff parsed sets across header ↔ `.hpp` ↔ python | agree | report | + +SUP-6/7/8 use a **full structured parse with MANUAL fallback** (emit MANUAL + the table +location if a Doxygen table doesn't parse cleanly). + +SUP-1/2 classify the **primary data input**, meaning the first Tensor/TensorBatch/ImageBatch +handle after the operator and stream parameters. Tensor outputs and auxiliary tensors do not +declare Tensor-input support for mixed-container legacy APIs. + +Source: `make_operator.rst` (Limitations table, runtime validation), `REVIEW_PR_GUIDELINES.md` +(input-type support documented & tested), and `OPTIMIZATION_GUIDELINES.md`. + +--- + +## Domain: test (correctness coverage + rigor) + +Consumes the support matrix. **C++ owns numerical correctness; Python owns the API +surface** (`make_operator.rst`). Every declared layout needs correctness coverage; equivalent +interleaved and planar image layouts additionally require bit-exact parity. + +| id | check | probe | PASS condition | fix | +|----|-------|-------|----------------|-----| +| TST-1 | Independent CPU reference | reference fn in `tests/cvcuda/system/TestOp.cpp` | present (independence = MANUAL) | report | +| TST-2 | `tensor_correct_output` | grep | present | author (mirror existing) | +| TST-3 | `varshape_correct_output` | grep | present / N-A if tensor-only | author | +| TST-4 | `NVCV_TEST_SUITE_P` + parse cases | grep/parse | present (parse → MANUAL fallback) | — | +| TST-5 | Matrix-mirror (axis-coverage) | each supported dtype/channel/layout/container/**mode** value appears in ≥1 positive case | all axis values covered; GAP lists uncovered | author missing case | +| TST-6 | Negative suite | `Op_Negative` asserting `NVCV_ERROR_INVALID_ARGUMENT` for unsupported matrix combinations | present | author | +| TST-7 | Equivalent-layout parity | `PlanarParityUtils` + `OpPlanar.*_matches_interleaved` | present for image-layout operators; N-A only with SUP-10 declaration | author | +| TST-8 | Tolerance discipline | `EXPECT_EQ` vs `EXPECT_NEAR(tol)` | **bit-exact `EXPECT_EQ` default**; every `EXPECT_NEAR` w/o adjacent rationale → MANUAL; `EXPECT_NEAR`-on-int → stronger flag | never auto-loosen; emit `needs-human` | +| TST-9 | Reference independence + edge adequacy | — | MANUAL with pointers | report | +| TST-10 | Deterministic inputs | grep for seeded/fixed fill vs unseeded RNG | seeded; unseeded RNG → flag | report | +| TST-11 | Python Tensor NHWC + HWC | `test_op.py` | exercised | author | +| TST-12 | Python VarShape | grep | exercised / N-A if tensor-only | author | +| TST-13 | Python allocating + `_into` | grep | both called | author | +| TST-14 | Python negative | `pytest.raises` | present | author | + +**Mode axes** for TST-5 are parsed from the op's C-API enums (e.g. `NVCVInterpolationType`, +`NVCVBorderType`), MANUAL fallback if unparseable. + +Source: `make_operator.rst` (test structure, C++/Python split), `OPTIMIZATION_GUIDELINES.md` +(bit-exact default, no silent tolerance bumps). + +--- + +## Domain: bench (structural + baselines + the basic-tier floor + advisory coverage) + +### Hard layer (PASS/GAP) + +| id | check | probe | PASS condition | fix | +|----|-------|-------|----------------|-----| +| BEN-1 | C++ bench present + registered | `bench/cpp/ops/Bench.cpp` + `bench/cpp/CMakeLists.txt` | both | scaffold | +| BEN-2 | Python bench present + registered | `bench/python/ops/bench_.py` + `bench/python/CMakeLists.txt` | both | scaffold | +| BEN-3 | Manifest entry | `` in `bench/config/bench_params.json` (config+cpp+python) | present | add | +| BEN-4 | Config + tiers | `bench/config/operators/.json`; every config has a valid `tier` | present | — | +| BEN-5 | truthful `layout` axis on every layout-bearing config | every config `string_axes.layout` present, unless deterministic review data marks the benchmark layout axis N-A | all applicable configs / N-A | add the real layout axis; never add a dummy axis | +| BEN-6 | Exact planar comparison coverage (if planar) | native `NCHW`/`CHW` configs; every `NCHW_FAKE`/`CHW_FAKE` expanded case is advanced-tier, Tensor-only, and has exactly one same-tier native case with every non-layout axis identical | exact per-signature pairs / N-A when no Tensor-image FakePlanar surface applies | add the advanced native/FakePlanar pair together | +| BEN-7 | Baseline completeness | every case-key × every `sku_map.json` SKU has metrics | complete; missing → **GAP "requires CI"** | baseline workflow in `bench/README.md` (never fabricate) | +| BEN-8 | Config expansion consistency | recompute `max(len(dtypes),1)·Π len(axis)` for the operator config and compare against generated benchmark rows/baselines where available | match | fix the operator config or baselines | +| BEN-9 | Internal baseline validation | `python3 bench/_internal/validate_baselines.py --operator ` | **MANUAL** — run the probe command; PASS = exit 0 | fix case keys | +| BEN-10 | C++/Python config parity | both drivers handle the configured axes | structural PASS / full = `--run` | — | +| BEN-11 | Noise/parity quality + currency | `run_bench.py` | **"requires GPU run"** (MANUAL; `--run` checks) | — | + +### Basic-tier minimum floor (HARD → GAP) + +| id | check | PASS condition | +|----|-------|----------------| +| BEN-14 | `basic` tier covers **≥ RGB × Tensor(if supported) × VarShape(if supported) × interleaved `NHWC` × planar `NCHW`(if planar)** | each *applicable* element is present in a `basic` config; applicability comes from the support matrix, operator-local layout policy, and deterministic review data. Any applicable element missing → GAP | + +BEN-14 is an axis-coverage floor, not a demand for the full Cartesian product. +An unsupported container/layout combination must remain absent rather than being +added as a skipped or misleading row (for example, a VarShape API that rejects +planar image formats). + +(Bench expresses container via the `inputKind` axis = `Tensor`/`VarShape` and, for an +operator with a distinct batch-of-tensors API, `TensorBatch`. The universal floor applies +to Tensor and VarShape; reviewed TensorBatch profiles may remain advanced. Layout uses +`NHWC`/`NCHW`/`NCHW_FAKE`. "RGB" = the primary 3-channel dtype, e.g. `uchar3`/`RGB8`.) + +### Advisory coverage analysis (`RECOMMENDATION`, report-only) + +| id | check | +|----|-------| +| BEN-13 | Coverage statistics vs the support matrix, built from the per-tier (`basic`/`advanced`) benched matrix of `.json`: per-axis (layout/inputKind/dtypes(benched)/channels → basic/advanced/none) | +| BEN-15 | Tiering check vs curated operator-local or deterministic tool expectations: listed combos in `basic`; remainder in `advanced` or flagged | +| BEN-16 | Emit advisory follow-ups (`RECOMMENDATION`); report-only, no persistence (human override wins) | +| BEN-SIZE | Apples-to-apples sizes: same-dtype `NHWC`/`NCHW`/`NCHW_FAKE` configs share one input `shape` (calibrate the interleaved config to 1-2 ms); mismatch → `RECOMMENDATION` | + +Source: `bench/README.md` and `OPTIMIZATION_GUIDELINES.md` (layout axis, NCHW_FAKE, +baseline regen). + +--- + +## Domain: docs (published artifacts + semantic consistency + SPDX) + +Owns docs artifacts + Limitations-vs-code semantics + docstrings + SPDX. Does **not** re-do +container/overload parity (support SUP-3/4). + +| id | check | probe | PASS condition | fix | +|----|-------|-------|----------------|-----| +| DOC-1 | `operator_list.rst` row | `:py:func:``` row in `docs/sphinx/operator_list.rst` | present | add row | +| DOC-2 | Python autofunction (fn + `_into`) | `.. cvcuda-autofunction:: cvcuda.` and `…_into` in `docs/sphinx/modules/python/operators.rst` | both | add directive | +| DOC-3 | C++ API-reference entry | operator listed in the C++ API docs | present (MANUAL fallback) | report | +| DOC-4 | Limitations table consistent with code | **diff** declared (SUP-6/7/8) vs enforced (SUP-9) vs tested (TST-5) | three agree; divergence → GAP naming the mismatch; nuance → MANUAL | correct table iff diff unambiguous | +| DOC-5 | Python docstrings (op + `_into`) | grep binding | present (quality MANUAL) | report | +| DOC-6 | Doxygen param docs explain "why" | — | MANUAL | report | +| DOC-7 | SPDX 2026 headers | the op's new/changed src/test/bench/doc files | present | add header (mechanical) | + +Source: `make_operator.rst` (docs steps), `AGENTS.md` (SPDX), `REVIEW_PR_GUIDELINES.md` +(docs consistency). + +--- + +## Operator-specific review data + +Keep this guidance generic. Do not add operator names, reviewed exception lists, +campaign status, or operator-specific benchmark expectations here. + +Operator-specific facts belong in deterministic tool data, the active campaign +backlog/status, or operator-local source/test/bench artifacts. This keeps +unrelated operator work from conflicting in the shared guidance file. diff --git a/.agents/skills/make-op-scaffold/SKILL.md b/.agents/skills/make-op-scaffold/SKILL.md new file mode 100644 index 000000000..848bfa095 --- /dev/null +++ b/.agents/skills/make-op-scaffold/SKILL.md @@ -0,0 +1,18 @@ +--- +name: make-op-scaffold +description: Scaffold a new CV-CUDA operator — a complete, wired, building skeleton — and delegate the implementation to a human or another AI. Use when asked to set up / stub out a new operator without implementing it, or to bootstrap one for someone else to finish. Supports --bare (skip the spec when the definition itself is delegated). +--- + +[//]: # "SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved." +[//]: # "SPDX-License-Identifier: Apache-2.0" + +# Make Op — scaffold + +Thin entry point. Produce a wired, building skeleton and stop. By default, propose the operator's +semantics + a cited reference oracle + the support matrix, **get user approval**, and record the +contract in `Op.h`; pass `--bare` to delegate the definition too (`SPEC-*` then report +`MANUAL` "spec delegated", not GAP). Run `tools/mkop/mkop.sh `, then gate with +`python3 tools/make_op.py --phase scaffold [--bare]` and resolve every `SCF-*`/`SPEC-*` +GAP per `.agents/guidance/MAKE_OP_GUIDELINES.md`. Report the outstanding work (IMP/COV/EXEC) and that +`make-op-verify` is the bar for "done" — it stays red until the operator is implemented. +Findings-first. diff --git a/.agents/skills/make-op-verify/SKILL.md b/.agents/skills/make-op-verify/SKILL.md new file mode 100644 index 000000000..79f320619 --- /dev/null +++ b/.agents/skills/make-op-verify/SKILL.md @@ -0,0 +1,23 @@ +--- +name: make-op-verify +description: Verify a new CV-CUDA operator against the deterministic final regression checklist (the /make-op done-gate). Use when asked whether a new operator is complete/done, or to gate it before merge - gold reference, bit-exact coverage across the declared support matrix, required layout parity, complement negatives, docs + relnote, benched dtypes, tests that run and pass, and optimization-readiness. +--- + +[//]: # "SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved." +[//]: # "SPDX-License-Identifier: Apache-2.0" + +# Make Op — verify + +Thin entry point. Run `python3 tools/make_op.py --phase done --run` and interpret per +`.agents/guidance/MAKE_OP_GUIDELINES.md`. This is the strict-regression done-gate: it reuses `review-op` (all four +domains) and `optimize-op` preflight, then adds the make-op teeth — independent CPU gold reference +(`COV-GOLD`), **bit-exact** coverage across the declared support matrix +(`COV-1`/`COV-CHAN`/`COV-3`/`COV-BITEXACT`), required equivalent-layout parity +(`COV-PARITY`/`COV-5`), +complement negatives (`COV-NEG`), every declared dtype benched (`COV-2`), the always-on NVTX +marker present and registered in the runtime marker test (`NVTX-1`), the operator in the +latest relnote (`DOC-REL`), and tests that compile/run/pass (`EXEC-*`). Resolve every `GAP` with +its named fix and re-run; resolve each `MANUAL` at the cited location. Inviolable: never fabricate +baselines (`GAP [requires CI]` → CI regen); never loosen tolerances (bit-exact default; any +`EXPECT_NEAR` in the C++ test is a GAP); layout-support gaps are author work. +Findings-first; completion = a re-run shows zero `GAP` and zero unresolved `MANUAL`. diff --git a/.agents/skills/make-op/SKILL.md b/.agents/skills/make-op/SKILL.md new file mode 100644 index 000000000..747610440 --- /dev/null +++ b/.agents/skills/make-op/SKILL.md @@ -0,0 +1,46 @@ +--- +name: make-op +description: Add a new CV-CUDA operator end-to-end per .agents/guidance/MAKE_OP_GUIDELINES.md, with a deterministically-enforced definition-of-done. Use when asked to create/add a new operator, scaffold one, or verify that a new operator is complete (approved spec, wired scaffold, bit-exact regression tests across the declared support matrix, required layout parity, complement negatives, docs + relnote, benched dtypes, tests that run and pass, optimization-ready). +--- + +[//]: # "SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved." +[//]: # "SPDX-License-Identifier: Apache-2.0" + +# Make Op + +Thin entry point. The workflow + rules live in `.agents/guidance/MAKE_OP_GUIDELINES.md` and the +narrative how-to in `docs/sphinx/advanced/make_operator.rst`; the definition-of-done is gated by the +deterministic checker `tools/make_op.py`. Keep this skill thin; do not duplicate the guidelines. + +## Workflow + +1. **Spec & approval (Part 0)** — propose the operator's semantics + a **cited reference oracle** + (e.g. mimic TorchVision/OpenCV, or an explicit formula) + API + the support matrix + (dtype × channel × layout × container); **get user approval** (or an explicit oracle waiver for + a novel custom op), then record the contract in `Op.h` (`@brief` + `Reference:` + + Limitations matrix). +2. **Scaffold** — `tools/mkop/mkop.sh ` → the wired skeleton; gate with + `python3 tools/make_op.py --phase scaffold` (SCF-green). +3. **Implement** — kernel + an independent CPU gold reference + tests/bench per the `COV-*` rules: + bit-exact for every declared variant, required equivalent-layout parity, complement negatives, + and every declared dtype benched. Image operators support interleaved and planar layouts by + default; record an operator-local reason when image layouts do not apply. The scaffold emits the + always-on NVTX markers (C-API submit / priv `operator()` / Python `NvtxTrace`); keep them and add + the op to the `OPERATORS` registry in `tests/cvcuda/python/test_nvtx_markers.py` (`NVTX-1`). +4. **Done gate** — `python3 tools/make_op.py --phase done --run`: composes `/review-op` + (all domains) + `/optimize-op` preflight and adds COV/EXEC/DOC-REL. Must be green; loop on the + verdict — for each `GAP` apply its named fix and re-run. + **Inviolable:** never fabricate baselines (missing → `GAP [requires CI]` → CI regen); bit-exact + is the default (an `EXPECT_NEAR` on an interleaved path is a GAP); layout-support gaps are + author work. +5. **Calibrate + seed baselines** — calibrate each bench config to **1–2 ms nvbench GPU time** + (per-dtype; measure with `bench_` and adjust the batch), exercise with + `bench/run_bench.py --operator --lang both` (noise < 5% + C++/Python parity), then trigger + the named CI `baseline-regen` workflow to seed baselines on the reference SKUs and import its + artifacts with `bench/_internal/update_baseline.py --from --operator `. This closes + `BEN-7`/`RDY-1` (never fabricate baselines; absolute timings are SKU-specific, so baselines come + from CI). Follow `bench/README.md` for the workflow and `ci/README.md` for CI selection. +6. **Hand off** to `/optimize-op ` for the performance campaign. + +For a wired skeleton with the implementation delegated to a human/other AI, use `make-op-scaffold` +(optionally `--bare`); `make-op-verify` is the done-gate. Findings-first. diff --git a/.agents/skills/optimize-op-preflight/SKILL.md b/.agents/skills/optimize-op-preflight/SKILL.md new file mode 100644 index 000000000..996663221 --- /dev/null +++ b/.agents/skills/optimize-op-preflight/SKILL.md @@ -0,0 +1,16 @@ +--- +name: optimize-op-preflight +description: Check whether a CV-CUDA operator is READY to optimize (correctness + bench coverage + captured baseline + profiling) per .agents/guidance/OPTIMIZATION_GUIDELINES.md. Use before starting an optimization campaign to confirm the readiness gate is clean. +--- + +[//]: # "SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved." +[//]: # "SPDX-License-Identifier: Apache-2.0" + +# Optimize Op — preflight + +Thin entry point. Run `python3 tools/optimize_op.py --phase preflight` and interpret per +the tool's readiness (PRE-*) findings against `.agents/guidance/OPTIMIZATION_GUIDELINES.md`: correctness/regression coverage and +benchmark coverage (reused from `review_op.py` test+bench), a captured baseline, and profiling +tools. Any `GAP` means the operator is not ready — add the missing coverage **first**, in a +separate non-`perf:` commit, before starting the optimization. Resolve `MANUAL` items at the +cited locations. Findings-first. diff --git a/.agents/skills/optimize-op-verify/SKILL.md b/.agents/skills/optimize-op-verify/SKILL.md new file mode 100644 index 000000000..7a97d6aa6 --- /dev/null +++ b/.agents/skills/optimize-op-verify/SKILL.md @@ -0,0 +1,31 @@ +--- +name: optimize-op-verify +description: Verify a CV-CUDA optimization campaign's deterministic definition-of-done and concise versioned MR summary per .agents/guidance/OPTIMIZATION_GUIDELINES.md. Use to gate whether a perf campaign or performance MR is ready, including reference-SKU statistics, hard checklist evidence, baseline validation, lead exhaustion, memory checks, and API/ABI stability. +--- + +[//]: # "SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved." +[//]: # "SPDX-License-Identifier: Apache-2.0" + +# Optimize Op — verify + +Read `.agents/guidance/OPTIMIZATION_GUIDELINES.md`, then refresh the v1 summary +and run the done-gate: + +```bash +python3 tools/optimize_op.py --phase summary \ + --results --state final --out +python3 tools/optimize_op.py --phase evidence \ + --base --results +``` + +Report findings first. Require the exact full-union key set expanded from every +declared `basic` and `advanced` entry, before/after C++ timing for every key on +every configured reference SKU, exact optimized/full counts, verified +min/median/max statistics, verified layout ratios or verified n/a rows, and all +six checklist items checked with hard evidence. Require the canonical companion +`## Memory footprint` section outside the bounded v1 block, and treat ODO-9 as +the memory-footprint authority; its `MANUAL` result requires explicit human +review before submission. Declare the campaign done only after a rerun shows +zero `GAP` and zero unresolved `MANUAL`. Never weaken tests, fabricate +baselines, check unsupported evidence, weaken the ODO-9 memory policy, or +change public API/ABI. diff --git a/.agents/skills/optimize-op/SKILL.md b/.agents/skills/optimize-op/SKILL.md new file mode 100644 index 000000000..3924f5daf --- /dev/null +++ b/.agents/skills/optimize-op/SKILL.md @@ -0,0 +1,64 @@ +--- +name: optimize-op +description: Drive a single-operator optimization campaign per .agents/guidance/OPTIMIZATION_GUIDELINES.md, with a deterministically enforced definition-of-done and versioned MR summary. Use when asked to optimize an operator, run or finish a performance campaign, generate or refresh its MR performance summary, or verify that a perf MR satisfies correctness, benchmark, baseline, lead-exhaustion, memory, and API/ABI gates. +--- + +[//]: # "SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved." +[//]: # "SPDX-License-Identifier: Apache-2.0" + +# Optimize Op + +Read `.agents/guidance/OPTIMIZATION_GUIDELINES.md` and follow it as the source of +truth. Use `tools/optimize_op.py` for the deterministic gates and the versioned +MR summary. Do not duplicate or weaken the guidance here. + +## Workflow + +1. Run `python3 tools/optimize_op.py --phase preflight`. Resolve every + readiness `GAP` in a separate non-`perf:` coverage commit before optimizing. +2. Survey and baseline the full union expanded from all declared `basic` and + `advanced` configurations. Iterate profile → lead → one change → + correctness/performance evaluation → review/refactor. Run + `python3 tools/refactor_op.py --phase assess` before every kept `perf:` + commit. Re-profile after each win and stop only at the guidance's measured + ridge or three-post-win-strike condition. +3. Initialize the concise v1 MR summary from exact optimized case keys: + + ```bash + python3 tools/optimize_op.py --phase summary \ + --benchmark-base --optimized-cases-file \ + --state provisional --bottleneck Memory-bound \ + --profile-evidence '' --out + ``` + + Use `Compute-bound` when supported by the profile. Keep the human-authored + assessment, six hard-evidence checklist lines, and one to five learnings + current. Regenerate scope, categories, per-SKU Impact, and Layout comparison; + do not hand-edit those derived fields. Maintain the canonical companion + `## Memory footprint` section outside the bounded v1 block; check its summary + item only after ODO-9 passes. +4. Refresh the description with reference-SKU evidence and finalize it: + + ```bash + python3 tools/optimize_op.py --phase summary \ + --results --state final --out + ``` + + Require every configured reference SKU. Keep the visible warning for any + non-reference local SKU retained for context. +5. Run the final gate: + + ```bash + python3 tools/optimize_op.py --phase evidence \ + --base --results + ``` + + Require pixelwise-equality evidence for every changed configuration, updated + and non-regressing comparable baselines, full-surface profile/lead coverage + (agent-verified per the guidance; the gate does not check it), + hard evidence for all six checklist items, exhausted leads, stable API/ABI, + and `perf:` commit hygiene. Treat ODO-9 as the memory-footprint authority; + its `MANUAL` result requires explicit human review before submission. + Resolve every `GAP` and unresolved `MANUAL`, then rerun. Never weaken or + silently retune tests, fabricate baselines, check an evidence item without + its hard proof, weaken the ODO-9 memory policy, or change public API/ABI. diff --git a/.agents/skills/refactor-op/SKILL.md b/.agents/skills/refactor-op/SKILL.md new file mode 100644 index 000000000..f265a5cf0 --- /dev/null +++ b/.agents/skills/refactor-op/SKILL.md @@ -0,0 +1,42 @@ +--- +name: refactor-op +description: Find and safely apply per-operator refactoring / redundancy-reduction opportunities in a CV-CUDA operator (near-duplicate Tensor/VarShape kernels, reinvented shared utilities, dead code). Use when asked to reduce code duplication, de-duplicate or unify an operator's kernels/bindings, remove dead code, or verify a refactor changed nothing observable. Produces a deterministic findings-first report; the apply path is gated by a strict bit-exact / feature-set / test-coverage parity check. +--- + +[//]: # "SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved." +[//]: # "SPDX-License-Identifier: Apache-2.0" + +# Refactor Op + +Thin entry point. The catalog substance and the checks are shared repo artifacts: the deterministic +checker `tools/refactor_op.py` and the spec `.agents/guidance/REFACTOR_OP_GUIDELINES.md`. Keep this +skill thin; do not duplicate the catalog here. `refactor-op` is **complementary to SonarQube CPD** +(which owns generic, whole-corpus duplication) — it surfaces operator-scoped *semantic* redundancy +Sonar cannot express. + +## Workflow + +1. **assess** — `python3 tools/refactor_op.py ` (scope with `--domain impl|api|xcut`; + `--format json` for machine output). Read-only and deterministic. Interpret each finding by its + `RED-*` id in `.agents/guidance/REFACTOR_OP_GUIDELINES.md`. +2. **apply** (only if asked to fix) — apply the finding's named corrective action on the + **implementation / binding only**, reusing existing shared utilities. **Never** edit the + operator's tests or anything under `bench/`, and **never** change public API/ABI. +3. **verify** — when the MR scope includes a refactor, run + `python3 tools/refactor_op.py --phase verify --base ` locally and require zero + `GAP` (frozen test surface, frozen bench surface, API/ABI, feature-matrix and coverage-matrix + parity), then build + run the frozen `Op` tests (bit-exact, VER-6) and re-run assess + (redundancy gone, VER-7). The verify report ends with a deterministic **refactor summary** (LOC + delta + `RED-*` resolved before→after) — report it as the outcome. If the MR also changes + performance-sensitive implementation, provide benchmark evidence through the optimization + workflow. + +Inviolable: refactoring changes nothing observable — bit-exact output, identical feature set, frozen +tests and benchmarks, unchanged API/ABI; never weaken a test or fabricate a baseline to pass. There +is no standing `refactor-parity` CI job; invoke the local verify flow when the MR scope calls for it. + +## Prompt Handling + +Requests like "reduce duplication in Flip", "/refactor-op BrightnessContrast", "unify the Tensor and +VarShape kernels for Resize", "is there dead code in OpStack?", or "verify my refactor is bit-exact" +trigger this skill. Use `--domain` to scope; use `--phase verify` to gate an applied refactor. diff --git a/.agents/skills/review-op-bench-coverage/SKILL.md b/.agents/skills/review-op-bench-coverage/SKILL.md new file mode 100644 index 000000000..cf3ff4de7 --- /dev/null +++ b/.agents/skills/review-op-bench-coverage/SKILL.md @@ -0,0 +1,17 @@ +--- +name: review-op-bench-coverage +description: Review a CV-CUDA operator's BENCHMARK coverage — drivers, layout axis, baselines, the basic-tier floor, row counts, and coverage statistics. Use when asked whether an operator's benchmarks/baselines are complete or to find/fill bench gaps. +--- + +[//]: # "SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved." +[//]: # "SPDX-License-Identifier: Apache-2.0" + +# Review Op — bench coverage + +Thin entry point. Run `python3 tools/review_op.py --domain bench` (add `--run` +for the GPU-run-dependent checks) and interpret per the **bench** section (BEN-*) of +`.agents/guidance/REVIEW_OP_GUIDELINES.md`: structural rules + the hard basic-tier minimum floor +(BEN-14) + advisory coverage statistics and `RECOMMENDATION`s. Resolve `MANUAL` items at the cited locations. When fixing +(`--fix`), add the layout axis and required native/reference layout configs, then recompute row counts - a missing-SKU +**baseline gap is reported "requires CI" and never fabricated** — and re-run to confirm. +Findings-first. diff --git a/.agents/skills/review-op-docs-api/SKILL.md b/.agents/skills/review-op-docs-api/SKILL.md new file mode 100644 index 000000000..b71dc71ea --- /dev/null +++ b/.agents/skills/review-op-docs-api/SKILL.md @@ -0,0 +1,17 @@ +--- +name: review-op-docs-api +description: Review a CV-CUDA operator's DOCS & API artifacts — operator_list row, Python autofunction (fn + _into), Limitations-table-vs-code consistency, docstrings, and SPDX headers. Use when asked whether an operator's docs/API surface is complete and consistent. +--- + +[//]: # "SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved." +[//]: # "SPDX-License-Identifier: Apache-2.0" + +# Review Op — docs & API + +Thin entry point. Run `python3 tools/review_op.py --domain docs` and interpret +per the **docs** section (DOC-*) of `.agents/guidance/REVIEW_OP_GUIDELINES.md`: the `operator_list.rst` row, +Python `cvcuda-autofunction` directives (fn + `_into`), the Limitations-table-vs-code +cross-domain diff, docstrings, and SPDX headers. Resolve `MANUAL` items at the cited +locations. When fixing (`--fix`), add the missing docs row / autofunction / SPDX header; +correct a stale Limitations entry only where the declared-vs-enforced-vs-tested diff is +unambiguous; re-run to confirm. Findings-first. diff --git a/.agents/skills/review-op-support/SKILL.md b/.agents/skills/review-op-support/SKILL.md new file mode 100644 index 000000000..333429367 --- /dev/null +++ b/.agents/skills/review-op-support/SKILL.md @@ -0,0 +1,16 @@ +--- +name: review-op-support +description: Review a CV-CUDA operator's input-type, layout, dtype, and channel support matrix. Use when asked which inputs an operator supports, whether required layouts are complete, or whether support is enforced consistently across C, C++, and Python APIs. +--- + +[//]: # "SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved." +[//]: # "SPDX-License-Identifier: Apache-2.0" + +# Review Op — support + +Thin entry point. Run `python3 tools/review_op.py --domain support` and interpret +per the **support** section (SUP-*) of `.agents/guidance/REVIEW_OP_GUIDELINES.md`: the container × layout × +dtype × channel matrix, runtime enforcement, cross-surface (C-API/C++/Python) consistency, +and layout completeness. Resolve `MANUAL` items at the cited locations. When fixing (`--fix`), +apply the per-item corrective action and keep missing layout capability as explicit author work. +Re-run to confirm. Findings-first. diff --git a/.agents/skills/review-op-test-coverage/SKILL.md b/.agents/skills/review-op-test-coverage/SKILL.md new file mode 100644 index 000000000..8080a254a --- /dev/null +++ b/.agents/skills/review-op-test-coverage/SKILL.md @@ -0,0 +1,17 @@ +--- +name: review-op-test-coverage +description: Review a CV-CUDA operator's test coverage, including C++ correctness, required cross-layout parity, correctness rigor, and the Python API surface. Use when asked whether an operator is adequately tested or to find and fill test gaps. +--- + +[//]: # "SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved." +[//]: # "SPDX-License-Identifier: Apache-2.0" + +# Review Op — test coverage + +Thin entry point. Run `python3 tools/review_op.py --domain test` and interpret +per the **test** section (TST-*) of `.agents/guidance/REVIEW_OP_GUIDELINES.md`: independent gold, +required cross-layout parity, axis-coverage mirror, negative tests, tolerance discipline (bit-exact by +default), and the Python API surface. Resolve `MANUAL` items at the cited locations. When +fixing (`--fix`), author the missing positive, negative, or layout-parity cases - **never introduce +or widen a tolerance silently** (emit rationale + needs-human) — and re-run to confirm. +Findings-first. diff --git a/.agents/skills/review-op/SKILL.md b/.agents/skills/review-op/SKILL.md new file mode 100644 index 000000000..6725c9077 --- /dev/null +++ b/.agents/skills/review-op/SKILL.md @@ -0,0 +1,32 @@ +--- +name: review-op +description: Review a CV-CUDA operator end-to-end (support / test / bench / docs coverage). Use when the user asks to review an operator, audit its input-type/layout/dtype support, test coverage, benchmark coverage, or docs/API, or to find & fix per-operator coverage gaps. Produces a deterministic findings-first report. +--- + +[//]: # "SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved." +[//]: # "SPDX-License-Identifier: Apache-2.0" + +# Review Op + +Thin entry point. The checklist substance and the checks are shared repo artifacts: +the deterministic checker `tools/review_op.py` and the spec `.agents/guidance/REVIEW_OP_GUIDELINES.md`. +Keep this skill thin; do not duplicate the checklist here. + +## Workflow + +1. Run `python3 tools/review_op.py ` (scope with + `--domain support|test|bench|docs`; `--format json` for machine output; `--run` for the + GPU-run-dependent bench checks). The checker is read-only and deterministic. +2. Interpret each finding by its cited item id in `.agents/guidance/REVIEW_OP_GUIDELINES.md`. Resolve every + `MANUAL` item by inspecting the location it points to. +3. If the user asked to fix (`--fix`), apply the corrective action named per `GAP` in + `.agents/guidance/REVIEW_OP_GUIDELINES.md`, then **re-run the checker to prove the item is `PASS`**. + Follow the inviolable `--fix` rules in `.agents/guidance/REVIEW_OP_GUIDELINES.md` ("### `--fix`"). +4. Return findings-first: `GAP`s, then unresolved `MANUAL`s, then `RECOMMENDATION`s, then the + per-domain + overall verdict. Completion = a re-run shows zero GAP and zero unresolved MANUAL. + +## Prompt Handling + +Requests like "review the Resize operator", "/review-op CenterCrop", "audit Flip's bench +coverage", or "does Remap cover every declared layout?" trigger this skill. Use `--domain` to +scope to a single domain when the request is domain-specific. diff --git a/.agents/tools/binding_api.py b/.agents/tools/binding_api.py new file mode 100644 index 000000000..0f7fff569 --- /dev/null +++ b/.agents/tools/binding_api.py @@ -0,0 +1,263 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""Conservative Python-binding API snapshots shared by optimization gates.""" + +from __future__ import annotations + +import re + + +def _normalize_cpp_surface(text): + return re.sub(r"\s+", " ", text or "").strip() + + +def _without_cpp_comments(text): + """Blank comments while preserving line breaks for declaration parsing.""" + + def blank(match): + return re.sub(r"[^\n]", " ", match.group(0)) + + return re.sub(r"//[^\n]*|/\*.*?\*/", blank, text or "", flags=re.S) + + +def _top_level_commas(text): + """Return comma offsets outside nested template/declarator delimiters.""" + opening = {"<": ">", "(": ")", "[": "]", "{": "}"} + closing = set(opening.values()) + stack = [] + commas = [] + for index, char in enumerate(text): + if char in opening: + stack.append(opening[char]) + elif char in closing: + if not stack or stack.pop() != char: + return None + elif char == "," and not stack: + commas.append(index) + return commas if not stack else None + + +def _unsupported_typedef_names(declarator): + """Extract names without pretending to resolve an unsupported declarator.""" + opening = {"<": ">", "(": ")", "[": "]", "{": "}"} + closing = set(opening.values()) + stack = [] + names = set() + non_names = {"alignas", "decltype", "noexcept", "sizeof", "typeid"} + + for index, char in enumerate(declarator): + if char in opening: + if not stack and char in "([": + suffix = declarator[index:] + grouped = None + if char == "(": + grouped = re.match( + r"\(\s*(?:(?:[A-Za-z_]\w*\s*::\s*)+)?[*&]+\s*" + r"([A-Za-z_]\w*)", + suffix, + ) + if grouped: + names.add(grouped.group(1)) + else: + direct = re.search(r"([A-Za-z_]\w*)\s*$", declarator[:index]) + if direct and direct.group(1) not in non_names: + names.add(direct.group(1)) + stack.append(opening[char]) + elif char in closing: + if not stack or stack.pop() != char: + return () + + trailing_pointer = re.search(r"[*&]\s*([A-Za-z_]\w*)\s*$", declarator) + if trailing_pointer: + names.add(trailing_pointer.group(1)) + return tuple(sorted(names)) + + +def _local_type_aliases(source): + """Collect conservative local ``using``/``typedef`` alias definitions. + + Each name maps to all definitions found for it. ``None`` marks syntax this + lightweight checker cannot resolve safely; a referenced duplicate or + unresolved definition therefore makes the whole API snapshot unavailable. + A typedef whose names cannot be identified makes this collection unavailable. + """ + text = _without_cpp_comments(source) + aliases = {} + + using_attempts = list(re.finditer(r"(?m)^\s*using\s+([A-Za-z_]\w*)\s*=", text)) + using_defs = list( + re.finditer(r"(?ms)^\s*using\s+([A-Za-z_]\w*)\s*=\s*([^;{}]+);", text) + ) + parsed_using_starts = {match.start() for match in using_defs} + for match in using_defs: + name, target = match.group(1), _normalize_cpp_surface(match.group(2)) + aliases.setdefault(name, []).append(target or None) + for match in using_attempts: + if match.start() not in parsed_using_starts: + aliases.setdefault(match.group(1), []).append(None) + + typedef_attempts = list(re.finditer(r"(?m)^\s*typedef\b", text)) + typedef_defs = list(re.finditer(r"(?ms)^\s*typedef\s+([^;{}]+);", text)) + parsed_typedef_starts = {match.start() for match in typedef_defs} + if any(match.start() not in parsed_typedef_starts for match in typedef_attempts): + return None + + for match in typedef_defs: + body = _normalize_cpp_surface(match.group(1)) + commas = _top_level_commas(body) + if commas is None: + return None + if commas: + # Multiple declarators share one base type. Resolving all of them + # correctly requires a C++ declarator parser, so expose each likely + # name as unresolved and fail closed only if it reaches a binding. + starts = [0, *(offset + 1 for offset in commas)] + ends = [*commas, len(body)] + pieces = [body[start:end] for start, end in zip(starts, ends, strict=True)] + for piece in pieces: + name_match = re.search(r"([A-Za-z_]\w*)\s*$", piece.strip()) + names = set(_unsupported_typedef_names(piece)) + if name_match: + names.add(name_match.group(1)) + if not names: + return None + for name in sorted(names): + aliases.setdefault(name, []).append(None) + continue + + simple = re.fullmatch(r"(.+?)\s+([A-Za-z_]\w*)", body) + if simple: + target, name = _normalize_cpp_surface(simple.group(1)), simple.group(2) + aliases.setdefault(name, []).append(target or None) + continue + + pointer = re.search(r"\(\s*[*&]\s*([A-Za-z_]\w*)\s*\)", body) + if pointer: + aliases.setdefault(pointer.group(1), []).append(None) + continue + names = _unsupported_typedef_names(body) + if not names: + return None + for name in names: + aliases.setdefault(name, []).append(None) + + return aliases + + +def _referenced_type_aliases(source, signatures): + """Resolve aliases reachable from bound callable signatures. + + Unused private aliases are intentionally omitted when they can be parsed. + Duplicate definitions, unsupported declarations, or cycles on a reachable + path return ``None``; an unclassifiable typedef also returns ``None`` because + its reachability cannot be established safely. + """ + aliases = _local_type_aliases(source) + if aliases is None: + return None + signature_text = " ".join(signatures) + roots = sorted( + name for name in aliases if re.search(rf"\b{re.escape(name)}\b", signature_text) + ) + state = {} + resolved = {} + + def visit(name): + if state.get(name) == "done": + return True + if state.get(name) == "visiting": + return False + definitions = aliases.get(name, []) + if len(definitions) != 1 or definitions[0] is None: + return False + + state[name] = "visiting" + target = definitions[0] + dependencies = sorted( + candidate + for candidate in aliases + if re.search(rf"\b{re.escape(candidate)}\b", target) + ) + if any(not visit(dependency) for dependency in dependencies): + return False + state[name] = "done" + resolved[name] = target + return True + + if any(not visit(root) for root in roots): + return None + return tuple(sorted(resolved.items())) + + +def binding_api_snapshot(source, op): + """Return registration, callable signatures, and reachable type aliases. + + Function bodies and private helpers are deliberately excluded. Unsupported + or ambiguous syntax returns ``None`` so callers can require manual review. + """ + if not source: + return None + + export = re.search( + rf"(?ms)^void\s+ExportOp{re.escape(op)}\s*\(\s*py::module\s*&\s*m\s*\)\s*" + rf"\{{(?P.*?)^\}}\s*// namespace cvcudapy\s*$", + source, + ) + if not export: + return None + + registration = re.sub( + r'R"pbdoc\(.*?\)pbdoc"', + 'R"pbdoc()pbdoc"', + export.group("body"), + flags=re.S, + ) + registration_count = len(re.findall(r"\bm\.def\s*\(", registration)) + symbols = re.findall( + r'\bm\.def\s*\(\s*"[^"]+"\s*,\s*' + r'(?:&|NvtxTrace\s*\(\s*"[^"]+"\s*,\s*&)' + r"([A-Za-z_]\w*)", + registration, + ) + if registration_count == 0 or len(symbols) != registration_count: + return None + + callable_signatures = [] + for symbol in symbols: + matches = list( + re.finditer( + rf"(?ms)^(?P[^\n{{;]*\b{re.escape(symbol)}\s*" + rf"\([^;{{]*?\))\s*\{{", + source, + ) + ) + if len(matches) != 1: + return None + callable_signatures.append( + (symbol, _normalize_cpp_surface(matches[0].group("signature"))) + ) + + referenced_aliases = _referenced_type_aliases( + source, [signature for _, signature in callable_signatures] + ) + if referenced_aliases is None: + return None + + return ( + _normalize_cpp_surface(registration), + tuple(callable_signatures), + referenced_aliases, + ) diff --git a/.agents/tools/claude_review.sh b/.agents/tools/claude_review.sh new file mode 100755 index 000000000..80b56b505 --- /dev/null +++ b/.agents/tools/claude_review.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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 -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +readonly SCRIPT_DIR +REPO_ROOT="$(git -C "${SCRIPT_DIR}/../.." rev-parse --show-toplevel)" +readonly REPO_ROOT + +# Missing CLI: write a sentinel the orchestrator skips, don't fail the review. +if ! command -v claude >/dev/null 2>&1; then + echo "MISSING HARNESS: 'claude' CLI not found on PATH -- skipping this review agent." + exit 0 +fi + +# Prompt = injected git context, then the shared review contract, then any focus. +BASE="${REVIEW_BASE:-main}" +review_prompt="$(bash "${SCRIPT_DIR}/review_context.sh" "${REPO_ROOT}" "${BASE}")" +review_prompt+=$'\n\n---\n\n' +review_prompt+="$(cat "${SCRIPT_DIR}/mr_code_review_prompt.md")" +if (($# > 0)); then + review_prompt+=$'\n\nSpecial instructions:\n' + review_prompt+="$*" +fi + +# Run in a throwaway worktree so a full-access agent stays isolated from the +# shared tree; this shell owns the cleanup trap (see review_worktree.sh). +source "${SCRIPT_DIR}/review_worktree.sh" +if ! agent_cwd="$(review_worktree_create "${REPO_ROOT}")"; then + echo "MISSING HARNESS: could not create an isolated worktree -- skipping this review agent." + exit 0 +fi +trap 'review_worktree_remove "${REPO_ROOT}" "${agent_cwd}"' EXIT +cd "${agent_cwd}" + +# Full access relying on the sandboxed environment; read-only by contract. +# --print reads the prompt on stdin, so it never collides with flag parsing. +claude --print --dangerously-skip-permissions <<<"${review_prompt}" diff --git a/.agents/tools/codex_review.sh b/.agents/tools/codex_review.sh new file mode 100755 index 000000000..8c436ff2e --- /dev/null +++ b/.agents/tools/codex_review.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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 -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +readonly SCRIPT_DIR +REPO_ROOT="$(git -C "${SCRIPT_DIR}/../.." rev-parse --show-toplevel)" +readonly REPO_ROOT + +# Missing CLI: write a sentinel the orchestrator skips, don't fail the review. +if ! command -v codex >/dev/null 2>&1; then + echo "MISSING HARNESS: 'codex' CLI not found on PATH -- skipping this review agent." + exit 0 +fi + +# Prompt = injected git context, then the shared review contract, then any focus. +# `codex review` rejects `--base` with a custom PROMPT, so the diff is passed here. +BASE="${REVIEW_BASE:-main}" +review_prompt="$(bash "${SCRIPT_DIR}/review_context.sh" "${REPO_ROOT}" "${BASE}")" +review_prompt+=$'\n\n---\n\n' +review_prompt+="$(cat "${SCRIPT_DIR}/mr_code_review_prompt.md")" +if (($# > 0)); then + review_prompt+=$'\n\nSpecial instructions:\n' + review_prompt+="$*" +fi + +# Run in a throwaway worktree so a full-access agent stays isolated from the +# shared tree; this shell owns the cleanup trap (see review_worktree.sh). +source "${SCRIPT_DIR}/review_worktree.sh" +if ! agent_cwd="$(review_worktree_create "${REPO_ROOT}")"; then + echo "MISSING HARNESS: could not create an isolated worktree -- skipping this review agent." + exit 0 +fi +trap 'review_worktree_remove "${REPO_ROOT}" "${agent_cwd}"' EXIT + +# Full access relying on the sandboxed environment; read-only by contract. +codex \ + --sandbox danger-full-access \ + --ask-for-approval never \ + --cd "${agent_cwd}" \ + review \ + - <<<"${review_prompt}" diff --git a/.agents/tools/cursor_review.sh b/.agents/tools/cursor_review.sh new file mode 100755 index 000000000..89fe3e9a6 --- /dev/null +++ b/.agents/tools/cursor_review.sh @@ -0,0 +1,92 @@ +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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 -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +readonly SCRIPT_DIR +REPO_ROOT="$(git -C "${SCRIPT_DIR}/../.." rev-parse --show-toplevel)" +readonly REPO_ROOT + +# Missing CLI: write a sentinel the orchestrator skips, don't fail the review. +if ! command -v cursor-agent >/dev/null 2>&1; then + echo "MISSING HARNESS: 'cursor-agent' CLI not found on PATH -- skipping this review agent." + exit 0 +fi + +# Model-agnostic: forward an optional --model; everything else is the review focus. +model="" +focus_args=() +while (($# > 0)); do + case "$1" in + --model) + if (($# < 2)) || [[ -z "$2" ]]; then + echo "cursor_review: --model requires a non-empty value" >&2 + exit 2 + fi + model="$2" + shift 2 + ;; + --model=*) + model="${1#*=}" + if [[ -z "${model}" ]]; then + echo "cursor_review: --model requires a non-empty value" >&2 + exit 2 + fi + shift + ;; + *) + focus_args+=("$1") + shift + ;; + esac +done + +# Prompt = injected git context, then the shared review contract, then any focus. +BASE="${REVIEW_BASE:-main}" +review_prompt="$(bash "${SCRIPT_DIR}/review_context.sh" "${REPO_ROOT}" "${BASE}")" +review_prompt+=$'\n\n---\n\n' +review_prompt+="$(cat "${SCRIPT_DIR}/mr_code_review_prompt.md")" +if ((${#focus_args[@]} > 0)); then + review_prompt+=$'\n\nSpecial instructions:\n' + review_prompt+="${focus_args[*]}" +fi + +model_args=() +if [[ -n "${model}" ]]; then + model_args+=(--model "${model}") +fi + +# Run in a throwaway worktree so a full-access agent stays isolated from the +# shared tree; this shell owns the cleanup trap (see review_worktree.sh). +source "${SCRIPT_DIR}/review_worktree.sh" +if ! agent_cwd="$(review_worktree_create "${REPO_ROOT}")"; then + echo "MISSING HARNESS: could not create an isolated worktree -- skipping this review agent." + exit 0 +fi +trap 'review_worktree_remove "${REPO_ROOT}" "${agent_cwd}"' EXIT +cd "${agent_cwd}" + +# Full access relying on the sandboxed environment; read-only by contract. +# --force auto-allows commands (no --mode ask/plan: ask can't run git); --trust +# skips the workspace-trust prompt a headless run cannot answer. +cursor-agent \ + --print \ + --force \ + --trust \ + "${model_args[@]}" \ + <<<"${review_prompt}" diff --git a/.agents/tools/mr_code_review_prompt.md b/.agents/tools/mr_code_review_prompt.md new file mode 100644 index 000000000..742b13bcf --- /dev/null +++ b/.agents/tools/mr_code_review_prompt.md @@ -0,0 +1,52 @@ +[//]: # "SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved." +[//]: # "SPDX-License-Identifier: Apache-2.0" + +# Independent CV-CUDA code review + +You are an independent, read-only code reviewer for the CV-CUDA repository. +Follow the repository's AGENTS.md and review guidance. Review the complete +change against the base branch (default `main` if none is configured) and +produce findings-first output. This is a static code review; you may read any +repository file for context, but do not build or run anything. + +## Scope + +Do: + +- Gather git context read-only and inspect the actual diff. +- Apply the code-level review areas from `.agents/guidance/REVIEW_PR_GUIDELINES.md` + (every area except "Review-Ready Criteria" and build/run/CI gating). +- Reason statically about correctness, safety, and the repository invariants in + AGENTS.md. + +Do NOT: + +- Build the project, run tests, benchmarks, or `compute-sanitizer`, or execute + project binaries or scripts (read-only git inspection is expected and allowed). +- Edit files, apply patches, format code, run destructive git, commit, or push. +- Gate on CI status, merge-readiness, or benchmark-baseline artifacts. Name + these under residual validation gaps instead. + +## Workflow + +1. Read and follow AGENTS.md. +2. Read the code-level review areas in `.agents/guidance/REVIEW_PR_GUIDELINES.md`; + skip its "Review-Ready Criteria" and any build/run/CI gating. +3. Use the injected "Git context" block (status, log, and the merge-base diff + `git diff ...HEAD`) as the review scope; only if it is missing or + empty, gather the same commands read-only yourself. If the working tree has + staged, unstaged, or untracked changes, inspect them and state that they are + uncommitted. +4. Generate an independent summary of the change and flag any mismatch between + the summary, commits, and code. + +## Output format + +1. Findings — first, grouped by severity, each with an exact `file:line` + reference and a concrete recommendation. If there are none, say so. +2. Open questions or assumptions, if any. +3. A short, independent summary of what the change does. +4. A checklist verdict, one line per applicable review area. +5. Residual validation gaps — the build/run/CI/benchmark checks this static + review did not perform. +6. Final recommendation: `Approve`, `Request Changes`, or `Needs Discussion`. diff --git a/.agents/tools/operator_source_map.py b/.agents/tools/operator_source_map.py new file mode 100644 index 000000000..d736f3f5f --- /dev/null +++ b/.agents/tools/operator_source_map.py @@ -0,0 +1,64 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. +"""Operator kernel-source attribution shared by tools/review_op.py and tools/refactor_op.py: +name-based legacy-kernel ownership plus explicit per-op kernel sources the resolvers' exact +Op.cu/.cpp candidates and legacy globs cannot find (unrelated names like filter.cu, or +multi-file kernels like OpHQResize2D.cu). Private Op.hpp class headers stay excluded +for every operator.""" + +from pathlib import Path + +_REPO = Path(__file__).resolve().parents[2] + + +def all_op_names(): + """Lowercased operator names derived from the public Op*.h headers.""" + hdr_dir = _REPO / "src/cvcuda/include/cvcuda" + if not hdr_dir.is_dir(): + return set() + return {h.stem[2:].lower() for h in hdr_dir.glob("Op*.h") if h.stem != "Operator"} + + +def legacy_belongs(file_stem, op, all_ops): + """True if a legacy kernel file belongs to `op`, tolerating the underscores the op name + drops (op `pillowresize` owns `pillow_resize.cu`, `convertto` owns `convert_to.cu`). The + longest matching op name wins, so a file is never mis-attributed to an op whose name is + merely a prefix of the real owner's (e.g. `gaussian` must not claim `gaussian_noise.cu`).""" + key = file_stem.replace("_", "").lower() + if not key.startswith(op): + return False + return not any(o != op and len(o) > len(op) and key.startswith(o) for o in all_ops) + + +SHARED_KERNEL_SOURCES = { + "conv2d": ["legacy/filter_var_shape.cu"], + "gaussian": ["legacy/filter.cu", "legacy/filter_var_shape.cu"], + "histogram": ["legacy/calc_hist.cu"], + "warpaffine": ["legacy/warp.cu", "legacy/warp_var_shape.cu"], + "warpperspective": ["legacy/warp.cu", "legacy/warp_var_shape.cu"], + "hqresize": [ + "OpHQResize2D.cu", + "OpHQResize3D.cu", + "OpHQResizeBatchWrap.cuh", + "OpHQResizeDispatch.hpp", + "OpHQResizeFilter.cuh", + "OpHQResizeKernel.cuh", + "OpHQResizePlanar.cuh", + ], + "stack": ["OpStackKernels.cu", "OpStackKernels.hpp"], + "averageblur": ["legacy/filter.cu", "legacy/filter_var_shape.cu"], + "laplacian": ["legacy/filter.cu", "legacy/filter_var_shape.cu"], + "reformat": ["legacy/reformat.cu"], +} diff --git a/.agents/tools/optimization_summary.py b/.agents/tools/optimization_summary.py new file mode 100644 index 000000000..66f1a12cd --- /dev/null +++ b/.agents/tools/optimization_summary.py @@ -0,0 +1,1961 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. +"""Canonical parser, generator, and validator for optimization MR summaries. + +This module owns the public, versioned Markdown contract. It intentionally has +no deployment-specific workflow policy: lifecycle consumers pass repository +evidence to ``validate_summary`` when they need numerical validation. +""" + +from __future__ import annotations + +import json +import re +import sys +from dataclasses import dataclass +from decimal import Decimal, InvalidOperation, ROUND_HALF_UP +from pathlib import Path +from statistics import median +from typing import Any, Iterable, Mapping, Sequence + + +REPO = Path(__file__).resolve().parents[2] +BENCH = REPO / "bench" +if str(BENCH) not in sys.path: + sys.path.insert(0, str(BENCH)) + +from _internal.baselines import ( # noqa: E402 + BaselineError, + expected_case_keys_for_entry, + fake_planar_pairing_issues, + parse_case_key, +) + + +CHECKLIST_LABELS = ( + "Pixelwise equality to reference", + "Memory-footprint checks", + "Baselines updated", + "Baseline validation", + "Lead exhaustion", + "Review/refactor gate", +) +SECTION_HEADINGS = ( + "Scope", + "Impact", + "Layout comparison", + "Evidence checklist", + "Top learnings", +) +START_TOKEN = "cvcuda-optimize-summary:v1" +END_MARKER = "" +MAX_DESCRIPTION_BYTES = 2_000_000 +MAX_BLOCK_BYTES = 400_000 +MAX_OPTIMIZED_CASES = 10_000 +MAX_SECONDARY_OPERATORS = 64 +NA_STATS = "n/a — no matched equivalent configurations" +DEFAULT_LEARNING = "No optimization attempt has been accepted or rejected yet." +_SHA_RE = re.compile(r"^[0-9a-f]{40}$") +_OPERATOR_RE = re.compile(r"^[A-Za-z][A-Za-z0-9]{0,127}$") +_START_RE = re.compile(r"^\s*$") +_FACTOR_RE = re.compile(r"^(\d{1,9}\.\d{2})x$") +_STATS_RE = re.compile(r"^(\d{1,9}\.\d{2})x / (\d{1,9}\.\d{2})x / (\d{1,9}\.\d{2})x$") +_DURATION_STATS_RE = re.compile( + r"^(-?\d{1,9}\.\d{2}) µs / (-?\d{1,9}\.\d{2}) µs / " r"(-?\d{1,9}\.\d{2}) µs$" +) +_PLACEHOLDER_RE = re.compile( + r"<[^>]+>|\b(?:todo|tbd|placeholder|add hard evidence|add one concise)\b", + re.IGNORECASE, +) + +# These exact checked-in config aliases predate unique-workload enforcement. +# Keep them readable without double-weighting summaries, but reject every new +# alias pair. Remove entries as the owning operator configs are migrated. +_LEGACY_DUPLICATE_CONFIG_ALIASES = { + "bilateralfilter": { + ( + "bilateralfilter_rgb_auto_fakeplanar_nchw_advanced", + "bilateralfilter_uchar3_fakeplanar_nchw_advanced", + ), + }, + "brightnesscontrast": { + ( + "brightnesscontrast_fakeplanar_nchw_float3_advanced", + "brightnesscontrast_fakeplanar_rgb_f32_nchw_1080p_advanced", + ), + ( + "brightnesscontrast_fakeplanar_nchw_uchar3_advanced", + "brightnesscontrast_fakeplanar_rgb_u8_nchw_1080p_advanced", + ), + ( + "brightnesscontrast_float3_varshape_advanced", + "brightnesscontrast_rgb_f32_1080p_advanced", + ), + ( + "brightnesscontrast_planar_nchw_float3_varshape_advanced", + "brightnesscontrast_planar_rgb_f32_nchw_1080p_advanced", + ), + }, + "centercrop": { + ( + "centercrop_fakeplanar_nchw_float3_advanced", + "centercrop_fakeplanar_nchw_rgb_f32_1080p_advanced", + ), + }, + "clahe": { + ("clahe_clip20_advanced", "clahe_tiles8_varshape_1080p_advanced"), + }, + "colortwist": { + ( + "colortwist_fakeplanar_nchw_advanced", + "colortwist_fakeplanar_nchw_rgb_u8_1080p_advanced", + ), + ( + "colortwist_fakeplanar_nchw_advanced", + "colortwist_fakeplanar_nchw_rgba_u8_1080p_advanced", + ), + }, + "copymakeborder": { + ( + "copymakeborder_fake_planar_rgb_u8_reflect101_1080p_advanced", + "copymakeborder_fake_planar_uchar3_advanced", + ), + }, + "customcrop": { + ( + "customcrop_fakeplanar_nchw_float3_advanced", + "customcrop_fakeplanar_nchw_rgb_f32_full_1080p_advanced", + ), + ( + "customcrop_fakeplanar_nchw_rgb_u8_full_1080p_advanced", + "customcrop_fakeplanar_nchw_uchar3_advanced", + ), + }, + "normalize": { + ( + "normalize_fakeplanar_nchw_1080p_float3_advanced", + "normalize_fakeplanar_rgb_f32_nchw_1080p_advanced", + ), + ("normalize_float3_advanced", "normalize_rgb_f32_1080p_advanced"), + ( + "normalize_planar_nchw_1080p_float3_advanced", + "normalize_planar_rgb_f32_nchw_1080p_advanced", + ), + }, + "pillowresize": { + ( + "pillowresize_fakeplanar_nchw_contract_cubic_1080p_float3_advanced", + "pillowresize_fakeplanar_nchw_contract_cubic_1080p_varshape_float3_advanced", + ), + ( + "pillowresize_fakeplanar_nchw_contract_cubic_1080p_float4_advanced", + "pillowresize_fakeplanar_nchw_contract_cubic_1080p_varshape_float4_advanced", + ), + ( + "pillowresize_fakeplanar_nchw_contract_cubic_1080p_uchar3_advanced", + "pillowresize_fakeplanar_nchw_contract_cubic_1080p_varshape_uchar3_advanced", + ), + ( + "pillowresize_fakeplanar_nchw_contract_cubic_1080p_uchar4_advanced", + "pillowresize_fakeplanar_nchw_contract_cubic_1080p_varshape_uchar4_advanced", + ), + ( + "pillowresize_planar_nchw_contract_cubic_1080p_float4_advanced", + "pillowresize_planar_nchw_contract_cubic_1080p_varshape_float4_advanced", + ), + ( + "pillowresize_planar_nchw_contract_cubic_1080p_uchar4_advanced", + "pillowresize_planar_nchw_contract_cubic_1080p_varshape_uchar4_advanced", + ), + }, +} + + +class SummaryError(ValueError): + """The bounded optimization-summary contract is malformed.""" + + +@dataclass(frozen=True) +class SummaryMetadata: + operator: str + state: str + baseline_commit: str + candidate_commit: str + optimized_cases: tuple[str, ...] + impact_metric: str = "cpp_time" + secondary_operators: tuple[str, ...] = () + + def __post_init__(self) -> None: + object.__setattr__(self, "optimized_cases", tuple(self.optimized_cases)) + if isinstance(self.secondary_operators, (str, bytes)): + raise SummaryError("metadata secondary_operators must be an array") + object.__setattr__(self, "secondary_operators", tuple(self.secondary_operators)) + + +@dataclass(frozen=True) +class ChecklistItem: + label: str + checked: bool + evidence: str + + +@dataclass(frozen=True) +class ImpactRow: + sku: str + scope: str + configurations: int + minimum: Decimal + median: Decimal + maximum: Decimal + + +@dataclass(frozen=True) +class PythonOverheadImpactRow: + sku: str + scope: str + configurations: int + before: tuple[Decimal, Decimal, Decimal] + after: tuple[Decimal, Decimal, Decimal] + reduction: tuple[Decimal, Decimal, Decimal] + + +@dataclass(frozen=True) +class LayoutRow: + sku: str + comparison: str + before: tuple[Decimal, Decimal, Decimal] | None + after: tuple[Decimal, Decimal, Decimal] | None + + +@dataclass(frozen=True) +class OptimizationSummary: + metadata: SummaryMetadata + text: str + block: str + prefix: str + suffix: str + title_operator: str + status_line: str + warnings: tuple[str, ...] + bottleneck: str + profile_evidence: str + optimized_count: int + total_count: int + categories: tuple[str, ...] + impact_rows: tuple[ImpactRow | PythonOverheadImpactRow, ...] + layout_rows: tuple[LayoutRow, ...] + checklist: tuple[ChecklistItem, ...] + learnings: tuple[str, ...] + + +@dataclass(frozen=True) +class ValidationIssue: + code: str + message: str + + +@dataclass(frozen=True) +class ValidationResult: + errors: tuple[ValidationIssue, ...] = () + warnings: tuple[ValidationIssue, ...] = () + + @property + def ok(self) -> bool: + return not self.errors + + +@dataclass(frozen=True) +class _Case: + key: str + tier: str + axes: tuple[tuple[str, str], ...] + baselines: Mapping[str, Any] + + @property + def axis_map(self) -> dict[str, str]: + return dict(self.axes) + + +@dataclass(frozen=True) +class _ConfigSurface: + benchmark: str + cases: Mapping[str, _Case] + skus: tuple[str, ...] + + +def parse_summary(description: str) -> OptimizationSummary: + """Parse exactly one live v1 block from an untrusted MR description.""" + + if not isinstance(description, str): + raise SummaryError("MR description must be text") + if len(description.encode("utf-8")) > MAX_DESCRIPTION_BYTES: + raise SummaryError("MR description exceeds the parser size limit") + + lines = description.splitlines(keepends=True) + live = _live_marker_lines(lines) + starts = [] + for index in live: + match = _START_RE.fullmatch(lines[index].rstrip("\r\n")) + if match: + starts.append((index, match)) + ends = [index for index in live if lines[index].rstrip("\r\n") == END_MARKER] + if len(starts) != 1 or len(ends) != 1: + raise SummaryError( + "expected exactly one live start marker and one matching end marker" + ) + start_index, start_match = starts[0] + end_index = ends[0] + if end_index <= start_index: + raise SummaryError("optimization summary end marker precedes its start") + if any( + START_TOKEN in lines[index] and index not in {start_index, end_index} + for index in live + ): + raise SummaryError("nested or duplicate optimization summary marker") + + prefix = "".join(lines[slice(None, start_index)]) + end_line = lines[end_index] + end_text = end_line.rstrip("\r\n") + end_eol = end_line[slice(len(end_text), None)] + block = "".join(lines[slice(start_index, end_index)]) + end_text + suffix = end_eol + "".join(lines[slice(end_index + 1, None)]) + if len(block.encode("utf-8")) > MAX_BLOCK_BYTES: + raise SummaryError("optimization summary block exceeds the parser size limit") + metadata = _parse_metadata(start_match.group(1)) + body = "".join(lines[slice(start_index + 1, end_index)]).strip("\r\n") + parsed = _parse_body(body, metadata) + return OptimizationSummary( + metadata=metadata, + text=description, + block=block, + prefix=prefix, + suffix=suffix, + **parsed, + ) + + +def validate_summary( + summary_or_description: OptimizationSummary | str, + *, + baseline_config: Mapping[str, Any] | None = None, + candidate_config: Mapping[str, Any] | None = None, + sku_map: Mapping[str, Any] | None = None, + expected_operator: str | None = None, + expected_candidate_commit: str | None = None, + evidence_results: Mapping[str, str] | None = None, +) -> ValidationResult: + """Validate structure and, when supplied, all artifact-derived claims.""" + + errors: list[ValidationIssue] = [] + warnings: list[ValidationIssue] = [] + try: + summary = ( + summary_or_description + if isinstance(summary_or_description, OptimizationSummary) + else parse_summary(summary_or_description) + ) + except SummaryError as exc: + return ValidationResult((ValidationIssue("parse", str(exc)),), ()) + + meta = summary.metadata + if expected_operator and meta.operator.casefold() != expected_operator.casefold(): + errors.append( + ValidationIssue( + "operator-mismatch", + f"metadata operator {meta.operator!r} does not match {expected_operator!r}", + ) + ) + if summary.title_operator != meta.operator: + errors.append( + ValidationIssue( + "title-operator", "visible title does not match metadata operator" + ) + ) + if expected_candidate_commit and meta.candidate_commit != expected_candidate_commit: + errors.append( + ValidationIssue( + "candidate-commit", + "metadata candidate_commit does not match the revision being validated", + ) + ) + expected_status = ( + "Final reference-hardware evidence" + if meta.state == "final" + else "Provisional local evidence" + ) + if expected_status not in summary.status_line: + errors.append( + ValidationIssue("state-status", f"status line must say {expected_status!r}") + ) + for label, commit in ( + ("base", meta.baseline_commit), + ("candidate", meta.candidate_commit), + ): + if commit[:12] not in summary.status_line: + errors.append( + ValidationIssue( + "status-commit", f"status line does not show the {label} commit" + ) + ) + + if not _profile_is_measured(summary.profile_evidence): + issue = ValidationIssue( + "profile-evidence", + "primary bottleneck requires a measured profiler metric and named signal", + ) + (errors if meta.state == "final" else warnings).append(issue) + + if summary.optimized_count != len(meta.optimized_cases): + errors.append( + ValidationIssue( + "optimized-count", + "visible optimized count does not match hidden exact case keys", + ) + ) + _validate_visible_tables(summary, errors) + _validate_checklist(summary, evidence_results, errors, warnings) + _validate_learnings(summary, errors, warnings) + + supplied = [ + baseline_config is not None, + candidate_config is not None, + sku_map is not None, + ] + if any(supplied) and not all(supplied): + errors.append( + ValidationIssue( + "artifact-inputs", + "baseline config, candidate config, and SKU map must be supplied together", + ) + ) + elif all(supplied): + try: + _validate_artifact_claims( + summary, + baseline_config or {}, + candidate_config or {}, + sku_map or {}, + errors, + warnings, + ) + except SummaryError as exc: + errors.append(ValidationIssue("benchmark-surface", str(exc))) + + return ValidationResult(tuple(errors), tuple(warnings)) + + +def generate_summary( + metadata: SummaryMetadata, + *, + baseline_config: Mapping[str, Any], + candidate_config: Mapping[str, Any], + sku_map: Mapping[str, Any], + bottleneck: str, + profile_evidence: str, + checklist: Mapping[str, Any] | Sequence[ChecklistItem] | None = None, + learnings: Sequence[str] = (), + prefix: str = "", + suffix: str = "", +) -> str: + """Render a summary, deriving every numerical field from benchmark data.""" + + _validate_metadata(metadata) + if bottleneck not in {"Memory-bound", "Compute-bound"}: + raise SummaryError("bottleneck must be Memory-bound or Compute-bound") + baseline = _config_surface(baseline_config, "baseline") + candidate = _config_surface(candidate_config, "candidate") + if candidate.benchmark.casefold() != metadata.operator.casefold(): + raise SummaryError( + f"candidate benchmark {candidate.benchmark!r} does not match operator {metadata.operator!r}" + ) + if baseline.benchmark.casefold() != candidate.benchmark.casefold(): + raise SummaryError("baseline and candidate benchmark identities differ") + _assert_comparable_surfaces(baseline, candidate) + optimized = _optimized_cases(metadata, candidate) + references = _reference_skus(sku_map) + skus = _complete_skus(baseline, candidate, metadata.impact_metric) + if metadata.state == "final": + missing = [stem for stem in references if stem not in skus] + if missing: + coverage = ( + "C++ timing" + if metadata.impact_metric == "cpp_time" + else "Python/C++ timing" + ) + raise SummaryError( + f"final summary lacks full before/after {coverage} coverage for reference SKU(s): " + + ", ".join(missing) + ) + if not skus: + raise SummaryError("no SKU has full before/after C++ timing coverage") + + categories = _categories(optimized) + impact_rows = _expected_impact_rows( + baseline, + candidate, + optimized, + skus, + references, + metadata.impact_metric, + ) + layout_rows = _expected_layout_rows(baseline, candidate, skus, references) + checklist_items = _normalize_checklist(checklist) + learning_items = tuple(learnings) or (DEFAULT_LEARNING,) + local_skus = [stem for stem in skus if stem not in references] + + metadata_payload = { + "operator": metadata.operator, + **( + {"secondary_operators": list(metadata.secondary_operators)} + if metadata.secondary_operators + else {} + ), + "state": metadata.state, + **( + {"impact_metric": metadata.impact_metric} + if metadata.impact_metric != "cpp_time" + else {} + ), + "baseline_commit": metadata.baseline_commit, + "candidate_commit": metadata.candidate_commit, + "optimized_cases": list(metadata.optimized_cases), + } + metadata_json = json.dumps( + metadata_payload, + ensure_ascii=False, + separators=(",", ":"), + ) + impact_explanation, impact_header, impact_separator = _impact_table_schema( + metadata.impact_metric + ) + status = ( + "Final reference-hardware evidence" + if metadata.state == "final" + else "Provisional local evidence" + ) + lines = [ + f"", + f"## {metadata.operator} optimization summary", + "", + f"> {status} · base `{metadata.baseline_commit[:12]}` · candidate `{metadata.candidate_commit[:12]}`", + ] + for stem in local_skus: + required = ( + " and ".join(_sku_label(item) for item in references) or "reference-SKU" + ) + lines.extend( + [ + "", + "> ⚠️ **Non-reference local GPU:** Results from " + f"{_sku_label(stem)} are provisional and cannot satisfy final readiness; " + f"final {required} statistics are still required.", + ] + ) + lines.extend( + [ + "", + f"**Primary bottleneck: {bottleneck}**", + "", + profile_evidence.strip(), + "", + "### Scope", + "", + f"**Configurations optimized:** {len(optimized)} / {len(candidate.cases)} total", + "", + "**Optimized categories:**", + "", + *[f"- `{category}`" for category in categories], + "", + "### Impact", + "", + impact_explanation, + "", + impact_header, + impact_separator, + *[_render_impact_row(row) for row in impact_rows], + "", + "### Layout comparison", + "", + "Timing ratio is `numerator / Planar`; values above `1.00x` mean Planar is faster.", + "", + "| SKU | Comparison | Before min / median / max | After min / median / max |", + "|---|---|---:|---:|", + *[_render_layout_row(row) for row in layout_rows], + "", + "### Evidence checklist", + "", + *[ + f"- [{'x' if item.checked else ' '}] **{item.label}** — {item.evidence}" + for item in checklist_items + ], + "", + "### Top learnings", + "", + *[f"- {item.strip()}" for item in learning_items], + END_MARKER, + ] + ) + block = "\n".join(lines) + rendered = _join_description(prefix, block, suffix) + if metadata.state == "final": + validation = validate_summary( + rendered, + baseline_config=baseline_config, + candidate_config=candidate_config, + sku_map=sku_map, + ) + if not validation.ok: + detail = "; ".join( + f"{item.code}: {item.message}" for item in validation.errors[:6] + ) + raise SummaryError("cannot render an invalid final summary: " + detail) + return rendered + + +def refresh_summary( + description: str, + *, + baseline_config: Mapping[str, Any], + candidate_config: Mapping[str, Any], + sku_map: Mapping[str, Any], + state: str | None = None, + candidate_commit: str | None = None, + impact_metric: str | None = None, + secondary_operators: Sequence[str] | None = None, +) -> str: + """Regenerate derived fields while preserving bounded human-authored fields.""" + + old = parse_summary(description) + metadata = SummaryMetadata( + operator=old.metadata.operator, + state=state or old.metadata.state, + baseline_commit=old.metadata.baseline_commit, + candidate_commit=candidate_commit or old.metadata.candidate_commit, + optimized_cases=old.metadata.optimized_cases, + impact_metric=impact_metric or old.metadata.impact_metric, + secondary_operators=( + tuple(secondary_operators) + if secondary_operators is not None + else old.metadata.secondary_operators + ), + ) + return generate_summary( + metadata, + baseline_config=baseline_config, + candidate_config=candidate_config, + sku_map=sku_map, + bottleneck=old.bottleneck, + profile_evidence=old.profile_evidence, + checklist=old.checklist, + learnings=old.learnings, + prefix=old.prefix, + suffix=old.suffix, + ) + + +def _live_marker_lines(lines: Sequence[str]) -> list[int]: + live: list[int] = [] + fence: str | None = None + for index, line in enumerate(lines): + stripped = line.lstrip() + marker = re.match(r"^(`{3,}|~{3,})", stripped) + if marker: + token = marker.group(1) + if fence is None: + fence = token + elif token[0] == fence[0] and len(token) >= len(fence): + fence = None + continue + if fence is None and START_TOKEN in line: + live.append(index) + return live + + +def _parse_metadata(blob: str) -> SummaryMetadata: + if len(blob.encode("utf-8")) > 300_000: + raise SummaryError("metadata exceeds the parser size limit") + try: + data = json.loads(blob, object_pairs_hook=_unique_json_object) + except (json.JSONDecodeError, RecursionError) as exc: + raise SummaryError(f"metadata is not valid JSON: {exc}") from exc + required = { + "operator", + "state", + "baseline_commit", + "candidate_commit", + "optimized_cases", + } + allowed = required | {"impact_metric", "secondary_operators"} + if ( + not isinstance(data, dict) + or not required.issubset(data) + or not set(data).issubset(allowed) + ): + raise SummaryError( + "metadata must contain exactly operator, state, baseline_commit, " + "candidate_commit, and optimized_cases, with optional impact_metric " + "and secondary_operators" + ) + if not isinstance(data["optimized_cases"], list): + raise SummaryError("metadata optimized_cases must be an array") + if not isinstance(data.get("secondary_operators", []), list): + raise SummaryError("metadata secondary_operators must be an array") + metadata = SummaryMetadata( + operator=data["operator"], + state=data["state"], + baseline_commit=data["baseline_commit"], + candidate_commit=data["candidate_commit"], + optimized_cases=tuple(data["optimized_cases"]), + impact_metric=data.get("impact_metric", "cpp_time"), + secondary_operators=tuple(data.get("secondary_operators", [])), + ) + _validate_metadata(metadata) + return metadata + + +def _unique_json_object(pairs: Sequence[tuple[str, Any]]) -> dict[str, Any]: + result = {} + for key, value in pairs: + if key in result: + raise SummaryError(f"metadata JSON repeats field {key!r}") + result[key] = value + return result + + +def _validate_metadata(metadata: SummaryMetadata) -> None: + if not isinstance(metadata.operator, str) or not _OPERATOR_RE.fullmatch( + metadata.operator + ): + raise SummaryError("metadata operator is invalid") + if not isinstance(metadata.state, str) or metadata.state not in { + "provisional", + "final", + }: + raise SummaryError("metadata state must be provisional or final") + if not isinstance(metadata.impact_metric, str) or metadata.impact_metric not in { + "cpp_time", + "python_overhead", + }: + raise SummaryError("metadata impact_metric must be cpp_time or python_overhead") + secondary = metadata.secondary_operators + if len(secondary) > MAX_SECONDARY_OPERATORS: + raise SummaryError( + "metadata secondary_operators may contain at most " + f"{MAX_SECONDARY_OPERATORS} names" + ) + if any( + not isinstance(operator, str) or not _OPERATOR_RE.fullmatch(operator) + for operator in secondary + ): + raise SummaryError("metadata secondary_operators contains an invalid operator") + normalized_secondary = [operator.casefold() for operator in secondary] + if len(set(normalized_secondary)) != len(normalized_secondary): + raise SummaryError("metadata secondary_operators contains duplicate operators") + if metadata.operator.casefold() in normalized_secondary: + raise SummaryError( + "metadata secondary_operators must not repeat the primary operator" + ) + for field, value in ( + ("baseline_commit", metadata.baseline_commit), + ("candidate_commit", metadata.candidate_commit), + ): + if not isinstance(value, str) or not _SHA_RE.fullmatch(value): + raise SummaryError( + f"metadata {field} must be a full lowercase 40-character commit SHA" + ) + cases = metadata.optimized_cases + if not cases or len(cases) > MAX_OPTIMIZED_CASES: + raise SummaryError( + f"metadata optimized_cases must contain 1..{MAX_OPTIMIZED_CASES} keys" + ) + if any(not isinstance(case, str) or not case or len(case) > 4096 for case in cases): + raise SummaryError("metadata contains an invalid optimized case key") + if len(set(cases)) != len(cases): + raise SummaryError("metadata optimized_cases contains duplicate keys") + + +def _parse_body(body: str, metadata: SummaryMetadata) -> dict[str, Any]: + headings = [ + (match.group(1), match.group(2), match.start(), match.end()) + for match in re.finditer(r"^(#{2,3})\s+(.+?)\s*$", body, re.MULTILINE) + ] + expected = [f"{metadata.operator} optimization summary", *SECTION_HEADINGS] + actual = [text for _, text, _, _ in headings] + if actual != expected: + raise SummaryError( + "visible headings must appear exactly once in canonical order: " + + " -> ".join(expected) + ) + if headings[0][0] != "##" or any(item[0] != "###" for item in headings[1:]): + raise SummaryError("summary title must be ## and section headings must be ###") + + preamble = body[slice(headings[0][3], headings[1][2])].strip() + scope = body[slice(headings[1][3], headings[2][2])].strip() + impact = body[slice(headings[2][3], headings[3][2])].strip() + layout = body[slice(headings[3][3], headings[4][2])].strip() + checklist_text = body[slice(headings[4][3], headings[5][2])].strip() + learnings_text = body[slice(headings[5][3], None)].strip() + + status_lines = [ + line.strip() for line in preamble.splitlines() if line.startswith("> ") + ] + ordinary_status = [ + line for line in status_lines if "Non-reference local GPU" not in line + ] + if len(ordinary_status) != 1: + raise SummaryError("summary preamble must contain exactly one status line") + bottleneck_matches = list( + re.finditer( + r"^\*\*Primary bottleneck: (Memory-bound|Compute-bound)\*\*$", + preamble, + re.MULTILINE, + ) + ) + if len(bottleneck_matches) != 1: + raise SummaryError("summary requires one exact Primary bottleneck line") + bottleneck_match = bottleneck_matches[0] + profile = preamble[slice(bottleneck_match.end(), None)].strip() + if not profile: + raise SummaryError("Primary bottleneck must be followed by profiler evidence") + warnings = tuple(line for line in status_lines if "Non-reference local GPU" in line) + + count_match = re.search( + r"^\*\*Configurations optimized:\*\* (\d{1,9}) / (\d{1,9}) total$", + scope, + re.MULTILINE, + ) + if not count_match: + raise SummaryError("Scope has no canonical Configurations optimized count") + categories = tuple(re.findall(r"^- `([^`\r\n]+)`\s*$", scope, re.MULTILINE)) + if not categories or len(categories) != len(set(categories)): + raise SummaryError("Scope requires unique Container Layout Type categories") + scope_lines = [line.strip() for line in scope.splitlines() if line.strip()] + expected_scope_lines = [ + count_match.group(0), + "**Optimized categories:**", + *[f"- `{category}`" for category in categories], + ] + if scope_lines != expected_scope_lines: + raise SummaryError( + "Scope may contain only the canonical count and category list" + ) + + impact_rows = _parse_impact_table(impact, metadata.impact_metric) + layout_rows = _parse_layout_table(layout) + checklist = _parse_checklist(checklist_text) + learnings = tuple( + match.group(1).strip() + for match in re.finditer(r"^- (.+?)\s*$", learnings_text, re.MULTILINE) + ) + if not 1 <= len(learnings) <= 5: + raise SummaryError("Top learnings must contain one to five bullets") + nonblank = [line for line in learnings_text.splitlines() if line.strip()] + if len(nonblank) != len(learnings): + raise SummaryError("Top learnings may contain only bullet lines") + + return { + "title_operator": metadata.operator, + "status_line": ordinary_status[0], + "warnings": warnings, + "bottleneck": bottleneck_match.group(1), + "profile_evidence": profile, + "optimized_count": int(count_match.group(1)), + "total_count": int(count_match.group(2)), + "categories": categories, + "impact_rows": impact_rows, + "layout_rows": layout_rows, + "checklist": checklist, + "learnings": learnings, + } + + +def _impact_table_schema(impact_metric: str) -> tuple[str, str, str]: + if impact_metric == "cpp_time": + return ( + "Speedup is `before / after`; `2.00x` means twice as fast.", + "| SKU | Configuration scope | Configurations | Min | Median | Max |", + "|---|---|---:|---:|---:|---:|", + ) + if impact_metric == "python_overhead": + return ( + "Python overhead is `gpu_time_us_python - gpu_time_us_cpp`; " + "reduction is `before - after`, so positive values mean less overhead.", + "| SKU | Configuration scope | Configurations | Before min / median / max | " + "After min / median / max | Reduction min / median / max |", + "|---|---|---:|---:|---:|---:|", + ) + raise SummaryError(f"unsupported impact metric {impact_metric!r}") + + +def _parse_impact_table( + section: str, impact_metric: str +) -> tuple[ImpactRow | PythonOverheadImpactRow, ...]: + explanation, header, _ = _impact_table_schema(impact_metric) + _require_table_only_section(section, explanation) + rows = _table_rows(section) + expected_header = tuple( + cell.strip() for cell in header.strip().strip("|").split("|") + ) + if not rows or rows[0] != expected_header: + raise SummaryError("Impact table header does not match the v1 schema") + parsed = [] + for cells in rows[1:]: + if len(cells) != 6 or cells[1] not in {"Optimized", "Full operator"}: + raise SummaryError("Impact table contains a malformed row") + try: + if impact_metric == "cpp_time": + parsed.append( + ImpactRow( + cells[0], + cells[1], + int(cells[2]), + _parse_factor(cells[3]), + _parse_factor(cells[4]), + _parse_factor(cells[5]), + ) + ) + else: + parsed.append( + PythonOverheadImpactRow( + cells[0], + cells[1], + int(cells[2]), + _parse_duration_stats(cells[3]), + _parse_duration_stats(cells[4]), + _parse_duration_stats(cells[5]), + ) + ) + except ValueError as exc: + raise SummaryError( + f"Impact table contains a malformed value: {exc}" + ) from exc + if not parsed: + raise SummaryError("Impact table must contain at least one SKU") + return tuple(parsed) + + +def _parse_layout_table(section: str) -> tuple[LayoutRow, ...]: + _require_table_only_section( + section, + "Timing ratio is `numerator / Planar`; values above `1.00x` mean Planar is faster.", + ) + rows = _table_rows(section) + expected_header = ( + "SKU", + "Comparison", + "Before min / median / max", + "After min / median / max", + ) + if not rows or rows[0] != expected_header: + raise SummaryError( + "Layout comparison table header does not match the v1 schema" + ) + parsed = [] + for cells in rows[1:]: + if len(cells) != 4 or cells[1] not in { + "Interleaved / Planar", + "FakePlanar / Planar", + }: + raise SummaryError("Layout comparison table contains a malformed row") + before = _parse_stats(cells[2]) + after = _parse_stats(cells[3]) + if (before is None) != (after is None): + raise SummaryError("layout n/a must appear in both before and after cells") + parsed.append(LayoutRow(cells[0], cells[1], before, after)) + if not parsed: + raise SummaryError("Layout comparison table must contain at least one SKU") + return tuple(parsed) + + +def _table_rows(section: str) -> list[tuple[str, ...]]: + table_lines = [ + line for line in section.splitlines() if line.strip().startswith("|") + ] + if len(table_lines) < 3: + return [] + separator = tuple( + cell.strip() for cell in table_lines[1].strip().strip("|").split("|") + ) + if not all(re.fullmatch(r":?-{3,}:?", cell) for cell in separator): + raise SummaryError("Markdown table separator is malformed") + rows = [] + for line in [table_lines[0], *table_lines[2:]]: + rows.append(tuple(cell.strip() for cell in line.strip().strip("|").split("|"))) + return rows + + +def _require_table_only_section(section: str, explanation: str) -> None: + lines = [line.strip() for line in section.splitlines() if line.strip()] + if ( + not lines + or lines[0] != explanation + or any(not line.startswith("|") for line in lines[1:]) + ): + raise SummaryError( + "table section may contain only its canonical explanation and table" + ) + + +def _parse_checklist(section: str) -> tuple[ChecklistItem, ...]: + pattern = re.compile( + r"^- \[([ xX])\] \*\*([^*\r\n]+)\*\* — (.+?)\s*$", re.MULTILINE + ) + matches = list(pattern.finditer(section)) + if [match.group(2) for match in matches] != list(CHECKLIST_LABELS): + raise SummaryError( + "Evidence checklist labels must appear exactly once in canonical order" + ) + nonblank = [line for line in section.splitlines() if line.strip()] + if len(nonblank) != len(matches): + raise SummaryError( + "Evidence checklist may contain only the six canonical item lines" + ) + return tuple( + ChecklistItem( + match.group(2), match.group(1).lower() == "x", match.group(3).strip() + ) + for match in matches + ) + + +def _parse_factor(value: str) -> Decimal: + match = _FACTOR_RE.fullmatch(value) + if not match: + raise ValueError(f"expected a factor such as 1.23x, got {value!r}") + return Decimal(match.group(1)) + + +def _parse_stats(value: str) -> tuple[Decimal, Decimal, Decimal] | None: + if value == NA_STATS: + return None + match = _STATS_RE.fullmatch(value) + if not match: + raise SummaryError(f"invalid min / median / max cell {value!r}") + return tuple(Decimal(item) for item in match.groups()) # type: ignore[return-value] + + +def _parse_duration_stats(value: str) -> tuple[Decimal, Decimal, Decimal]: + match = _DURATION_STATS_RE.fullmatch(value) + if not match: + raise ValueError( + f"expected microsecond min / median / max values, got {value!r}" + ) + return tuple(Decimal(item) for item in match.groups()) # type: ignore[return-value] + + +def _config_surface(config: Mapping[str, Any], label: str) -> _ConfigSurface: + if not isinstance(config, Mapping): + raise SummaryError(f"{label} benchmark config must be an object") + benchmark = config.get("benchmark") + entries = config.get("configs") + if not isinstance(benchmark, str) or not benchmark: + raise SummaryError(f"{label} benchmark config has no benchmark identity") + if not isinstance(entries, Mapping) or not entries: + raise SummaryError(f"{label} benchmark config has no configs") + cases: dict[str, _Case] = {} + workload_identities: dict[ + tuple[str, tuple[tuple[str, str], ...]], tuple[str, str] + ] = {} + for config_key, raw_entry in entries.items(): + if not isinstance(config_key, str) or not isinstance(raw_entry, Mapping): + raise SummaryError(f"{label} config entries must be named objects") + entry = dict(raw_entry) + tier = entry.get("tier") + if tier not in {"basic", "advanced"}: + raise SummaryError( + f"{label} config {config_key!r} has invalid or missing tier {tier!r}" + ) + try: + expected = expected_case_keys_for_entry(config_key, entry) + except BaselineError as exc: + raise SummaryError(f"{label} config {config_key!r}: {exc}") from exc + baselines = entry.get("baselines") + if not isinstance(baselines, Mapping): + raise SummaryError( + f"{label} config {config_key!r} baselines must be an object" + ) + expected_set = set(expected) + actual_set = set(baselines) + if expected_set != actual_set: + missing = sorted(expected_set - actual_set) + extra = sorted(actual_set - expected_set) + detail = [] + if missing: + detail.append( + f"missing {len(missing)} expanded case(s), e.g. {missing[0]}" + ) + if extra: + detail.append(f"has {len(extra)} undeclared case(s), e.g. {extra[0]}") + raise SummaryError(f"{label} config {config_key!r} " + "; ".join(detail)) + for key in expected: + if key in cases: + raise SummaryError(f"{label} expanded case key is duplicated: {key}") + payload = baselines[key] + if not isinstance(payload, Mapping): + raise SummaryError( + f"{label} case {key!r} SKU payload must be an object" + ) + try: + _, axes = parse_case_key(key) + except BaselineError as exc: + raise SummaryError(f"{label} case {key!r}: {exc}") from exc + workload_identity = (tier, tuple(sorted(axes))) + previous = workload_identities.get(workload_identity) + if previous is not None: + previous_config, previous_key = previous + alias_pair = tuple(sorted((previous_config, config_key))) + if alias_pair not in _LEGACY_DUPLICATE_CONFIG_ALIASES.get( + benchmark, set() + ): + raise SummaryError( + f"{label} duplicate expanded workload identity " + f"(tier, complete raw axes): {previous_key!r} and {key!r}" + ) + # This is a transitional compatibility path only. Keeping one + # canonical case prevents the legacy alias from double-weighting + # Full operator statistics while its config cleanup is pending. + if previous_config == alias_pair[0]: + continue + cases.pop(previous_key) + workload_identities[workload_identity] = (config_key, key) + cases[key] = _Case(key, tier, axes, payload) + skus = {str(stem) for case in cases.values() for stem in case.baselines} + for axis in ("layout", "inputKind"): + presence = {axis in case.axis_map for case in cases.values()} + if len(presence) > 1: + raise SummaryError( + f"{label} benchmark mixes cases with and without the {axis} axis" + ) + try: + pairing_issues = fake_planar_pairing_issues(entries) + except BaselineError as exc: + raise SummaryError( + f"{label} FakePlanar pairing cannot be evaluated: {exc}" + ) from exc + if pairing_issues: + raise SummaryError( + f"{label} benchmark has {len(pairing_issues)} unmatched FakePlanar case " + "signature(s); every FakePlanar case must be advanced Tensor and have " + "exactly one same-tier otherwise-identical native NCHW/CHW case; " + + pairing_issues[0] + ) + return _ConfigSurface(benchmark, cases, tuple(sorted(skus))) + + +def _optimized_cases( + metadata: SummaryMetadata, candidate: _ConfigSurface +) -> tuple[_Case, ...]: + unknown = [key for key in metadata.optimized_cases if key not in candidate.cases] + if unknown: + raise SummaryError( + f"optimized_cases contains {len(unknown)} unknown key(s), e.g. {unknown[0]}" + ) + optimized = tuple(candidate.cases[key] for key in metadata.optimized_cases) + fake = [ + case.key for case in optimized if case.axis_map.get("layout") == "NCHW_FAKE" + ] + if fake: + raise SummaryError( + "layout-conversion reference cases cannot be optimization targets, e.g. " + + fake[0] + ) + return optimized + + +def _reference_skus(sku_map: Mapping[str, Any]) -> tuple[str, ...]: + entries = sku_map.get("entries") if isinstance(sku_map, Mapping) else None + if not isinstance(entries, list): + raise SummaryError("SKU map must contain an entries array") + stems = [] + for entry in entries: + if not isinstance(entry, Mapping) or not isinstance(entry.get("stem"), str): + raise SummaryError("SKU map entries must contain string stems") + stems.append(entry["stem"]) + if len(stems) != len(set(stems)): + raise SummaryError("SKU map contains duplicate stems") + return tuple(sorted(stems, key=_sku_label)) + + +def _complete_skus( + baseline: _ConfigSurface, + candidate: _ConfigSurface, + impact_metric: str = "cpp_time", +) -> tuple[str, ...]: + timing_fields = ( + ("gpu_time_us_cpp",) + if impact_metric == "cpp_time" + else ("gpu_time_us_cpp", "gpu_time_us_python") + ) + complete = [] + for stem in sorted(set(baseline.skus) | set(candidate.skus)): + missing = [ + key + for key, case in candidate.cases.items() + if key not in baseline.cases + or any( + _timing(baseline.cases[key], stem, field) is None + or _timing(case, stem, field) is None + for field in timing_fields + ) + ] + if missing: + fields = " and ".join(timing_fields) + raise SummaryError( + f"SKU {stem!r} lacks full before/after {fields} coverage " + f"for {len(missing)} expanded case(s), e.g. {missing[0]}" + ) + complete.append(stem) + return tuple(complete) + + +def _assert_comparable_surfaces( + baseline: _ConfigSurface, candidate: _ConfigSurface +) -> None: + baseline_keys = set(baseline.cases) + candidate_keys = set(candidate.cases) + if baseline_keys != candidate_keys: + missing = sorted(baseline_keys - candidate_keys) + added = sorted(candidate_keys - baseline_keys) + detail = [] + if missing: + detail.append( + f"candidate omits {len(missing)} baseline case(s), e.g. {missing[0]}" + ) + if added: + detail.append( + f"candidate adds {len(added)} case(s) without before data, e.g. {added[0]}" + ) + raise SummaryError( + "baseline and candidate expanded benchmark surfaces differ: " + + "; ".join(detail) + ) + tier_changes = [ + key + for key in candidate_keys + if baseline.cases[key].tier != candidate.cases[key].tier + ] + if tier_changes: + raise SummaryError( + "baseline and candidate tier assignments differ, e.g. " + tier_changes[0] + ) + + +def _timing(case: _Case, stem: str, field: str = "gpu_time_us_cpp") -> Decimal | None: + payload = case.baselines.get(stem) + if not isinstance(payload, Mapping): + return None + value = payload.get(field) + try: + result = Decimal(str(value)) + except (InvalidOperation, ValueError): + return None + if not result.is_finite() or result <= 0: + return None + return result + + +def _categories(cases: Sequence[_Case]) -> tuple[str, ...]: + return tuple(sorted({_category(case) for case in cases})) + + +def _category(case: _Case) -> str: + axes = case.axis_map + container = axes.get("inputKind", "Tensor") + if container not in {"Tensor", "TensorBatch", "VarShape"}: + raise SummaryError(f"unsupported inputKind {container!r} in {case.key}") + layout = { + "NHWC": "Interleaved", + "HWC": "Interleaved", + "NCHW": "Planar", + "CHW": "Planar", + "NCHW_FAKE": "FakePlanar", + "NW": "NoLayout", + "NWC": "NoLayout", + None: "NoLayout", + }.get(axes.get("layout")) + if layout is None: + raise SummaryError(f"unsupported layout {axes.get('layout')!r} in {case.key}") + if not axes.get("InOutDataType"): + raise SummaryError(f"case has no InOutDataType axis: {case.key}") + return f"{container} {layout} {_logical_type(axes)}" + + +def _logical_type(axes: Mapping[str, str]) -> str: + dtype = axes["InOutDataType"] + code = axes.get("code") + if code: + return _color_conversion(code, dtype) + + channels = _vector_width(dtype) + num_channels = axes.get("numChannels") + if num_channels: + channels = int(num_channels) + source = _friendly_type_with_width(dtype, channels) + + output_dtype = axes.get("outDataType") + if output_dtype: + output = _friendly_type_with_width(output_dtype, channels) + return f"{source}→{output}" + + output_channels = axes.get("outChannels") + if output_channels: + output = _friendly_type_with_width(dtype, int(output_channels)) + return source if output == source else f"{source}→{output}" + return source + + +def _friendly_type(dtype: str) -> str: + names = { + "uchar3": "RGB8", + "uchar4": "RGBA8", + "float3": "RGBF32", + "float4": "RGBAF32", + "uint8": "U8", + "uint16": "U16", + "uint32": "U32", + "int16": "S16", + "int32": "S32", + "float32": "F32", + "short2": "2S16", + "short4": "4S16", + } + if dtype in names: + return names[dtype] + if "->" in dtype: + return "→".join(_friendly_type(item.strip()) for item in dtype.split("->")) + return dtype.upper() + + +def _vector_width(dtype: str) -> int: + match = re.fullmatch(r"(?:uchar|float|short)([234])", dtype) + return int(match.group(1)) if match else 1 + + +def _friendly_type_with_width(dtype: str, width: int) -> str: + scalar = { + "uint8": "U8", + "uchar3": "U8", + "uchar4": "U8", + "uint16": "U16", + "uint32": "U32", + "int16": "S16", + "int32": "S32", + "short2": "S16", + "short4": "S16", + "float32": "F32", + "float3": "F32", + "float4": "F32", + }.get(dtype, _friendly_type(dtype)) + if width == 3 and scalar == "U8": + return "RGB8" + if width == 4 and scalar == "U8": + return "RGBA8" + if width == 3 and scalar == "F32": + return "RGBF32" + if width == 4 and scalar == "F32": + return "RGBAF32" + return scalar if width == 1 else f"{width}{scalar}" + + +def _color_conversion(code: str, dtype: str) -> str: + if "2" not in code: + raise SummaryError(f"unsupported color conversion code {code!r}") + source, target = code.split("2", 1) + nv_format = None + for suffix in ("_NV12", "_NV21"): + if target.endswith(suffix): + target = target[: -len(suffix)] + nv_format = suffix[1:] + break + width = _vector_width(dtype) + if nv_format and source == "YUV": + source = nv_format + elif nv_format and target == "YUV": + target = nv_format + + def endpoint(name: str, *, vector_endpoint: bool) -> str: + if name in {"NV12", "NV21"}: + return name + if name in {"RGB", "BGR"} and vector_endpoint and width == 4: + name += "A" + return name + "8" + + return ( + f"{endpoint(source, vector_endpoint=not source.startswith('NV'))}→" + f"{endpoint(target, vector_endpoint=not target.startswith('NV'))}" + ) + + +def _expected_impact_rows( + baseline: _ConfigSurface, + candidate: _ConfigSurface, + optimized: Sequence[_Case], + skus: Sequence[str], + references: Sequence[str], + impact_metric: str = "cpp_time", +) -> tuple[ImpactRow | PythonOverheadImpactRow, ...]: + rows = [] + ordered_skus = _ordered_skus(skus, references) + full = tuple(candidate.cases.values()) + for stem in ordered_skus: + for scope, cases in (("Optimized", optimized), ("Full operator", full)): + if impact_metric == "cpp_time": + stats = _stats(_speedups(baseline, cases, stem)) + rows.append( + ImpactRow( + _sku_label(stem), + scope, + len(cases), + stats[0], + stats[1], + stats[2], + ) + ) + else: + before, after, reduction = _python_overhead_values( + baseline, cases, stem + ) + rows.append( + PythonOverheadImpactRow( + _sku_label(stem), + scope, + len(cases), + _stats(before), + _stats(after), + _stats(reduction), + ) + ) + return tuple(rows) + + +def _speedups( + baseline: _ConfigSurface, cases: Sequence[_Case], stem: str +) -> tuple[Decimal, ...]: + values = [] + for candidate_case in cases: + baseline_case = baseline.cases.get(candidate_case.key) + before = _timing(baseline_case, stem) if baseline_case else None + after = _timing(candidate_case, stem) + if before is None or after is None: + raise SummaryError( + f"missing positive gpu_time_us_cpp for {candidate_case.key} on {stem}" + ) + values.append(before / after) + return tuple(values) + + +def _python_overhead_values( + baseline: _ConfigSurface, cases: Sequence[_Case], stem: str +) -> tuple[tuple[Decimal, ...], tuple[Decimal, ...], tuple[Decimal, ...]]: + before_values = [] + after_values = [] + reductions = [] + for candidate_case in cases: + baseline_case = baseline.cases.get(candidate_case.key) + timings = ( + _timing(baseline_case, stem, "gpu_time_us_cpp") if baseline_case else None, + _timing(baseline_case, stem, "gpu_time_us_python") + if baseline_case + else None, + _timing(candidate_case, stem, "gpu_time_us_cpp"), + _timing(candidate_case, stem, "gpu_time_us_python"), + ) + if any(value is None for value in timings): + raise SummaryError( + "missing positive gpu_time_us_cpp or gpu_time_us_python for " + f"{candidate_case.key} on {stem}" + ) + before_cpp, before_python, after_cpp, after_python = timings + before_gap = before_python - before_cpp + after_gap = after_python - after_cpp + before_values.append(before_gap) + after_values.append(after_gap) + reductions.append(before_gap - after_gap) + return tuple(before_values), tuple(after_values), tuple(reductions) + + +def _expected_layout_rows( + baseline: _ConfigSurface, + candidate: _ConfigSurface, + skus: Sequence[str], + references: Sequence[str], +) -> tuple[LayoutRow, ...]: + rows = [] + pairs = { + "Interleaved / Planar": _layout_pairs(candidate, "Interleaved"), + "FakePlanar / Planar": _layout_pairs(candidate, "FakePlanar"), + } + for stem in _ordered_skus(skus, references): + for comparison in ("Interleaved / Planar", "FakePlanar / Planar"): + matched = pairs[comparison] + if not matched: + before = after = None + else: + before_values = [] + after_values = [] + for numerator_key, planar_key in matched: + before_values.append( + _ratio_for_pair(baseline, numerator_key, planar_key, stem) + ) + after_values.append( + _ratio_for_pair(candidate, numerator_key, planar_key, stem) + ) + before = _stats(before_values) + after = _stats(after_values) + rows.append(LayoutRow(_sku_label(stem), comparison, before, after)) + return tuple(rows) + + +def _layout_pairs( + surface: _ConfigSurface, numerator: str +) -> tuple[tuple[str, str], ...]: + groups: dict[tuple[Any, ...], dict[str, list[str]]] = {} + layout_names = { + "NHWC": "Interleaved", + "HWC": "Interleaved", + "NCHW": "Planar", + "CHW": "Planar", + "NCHW_FAKE": "FakePlanar", + } + for case in surface.cases.values(): + axes = case.axis_map + kind = layout_names.get(axes.get("layout")) + if kind is None: + continue + identity = ( + case.tier, + tuple( + sorted((name, value) for name, value in case.axes if name != "layout") + ), + ) + groups.setdefault(identity, {}).setdefault(kind, []).append(case.key) + pairs = [] + for identity, layouts in groups.items(): + numerator_keys = layouts.get(numerator, []) + planar_keys = layouts.get("Planar", []) + if not numerator_keys or not planar_keys: + continue + duplicates = { + kind: layouts[kind] + for kind in (numerator, "Planar") + if len(layouts.get(kind, [])) > 1 + } + if duplicates: + sample = next(iter(duplicates.values())) + raise SummaryError( + "ambiguous layout comparison signature " + f"{identity!r}; multiple same-layout cases include {sample[:2]}" + ) + if len(numerator_keys) == 1 and len(planar_keys) == 1: + pairs.append((numerator_keys[0], planar_keys[0])) + return tuple(sorted(pairs)) + + +def _ratio_for_pair( + surface: _ConfigSurface, numerator_key: str, planar_key: str, stem: str +) -> Decimal: + numerator = surface.cases.get(numerator_key) + planar = surface.cases.get(planar_key) + if numerator is None or planar is None: + raise SummaryError( + f"baseline/candidate layout surfaces differ for pair {numerator_key!r} / {planar_key!r}" + ) + numerator_time = _timing(numerator, stem) + planar_time = _timing(planar, stem) + if numerator_time is None or planar_time is None: + raise SummaryError(f"layout pair lacks gpu_time_us_cpp on {stem}") + return numerator_time / planar_time + + +def _stats(values: Iterable[Decimal]) -> tuple[Decimal, Decimal, Decimal]: + ordered = sorted(values) + if not ordered: + raise SummaryError("cannot calculate statistics for an empty case set") + return (_round(ordered[0]), _round(median(ordered)), _round(ordered[-1])) + + +def _round(value: Decimal) -> Decimal: + rounded = value.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) + return Decimal("0.00") if rounded == 0 else rounded + + +def _sku_label(stem: str) -> str: + match = re.match(r"^((?:A|H)\d+)(?:_|$)", stem, re.IGNORECASE) + return match.group(1).upper() if match else stem.replace("_", " ") + + +def _ordered_skus(skus: Sequence[str], references: Sequence[str]) -> tuple[str, ...]: + present = set(skus) + ordered = tuple( + [stem for stem in references if stem in present] + + sorted(present - set(references), key=_sku_label) + ) + labels = [_sku_label(stem) for stem in ordered] + if len(labels) != len(set(labels)): + raise SummaryError( + "reported SKU stems do not have unique human-readable labels" + ) + return ordered + + +def _render_impact_row(row: ImpactRow | PythonOverheadImpactRow) -> str: + if isinstance(row, PythonOverheadImpactRow): + return ( + f"| {row.sku} | {row.scope} | {row.configurations} | " + f"{_duration_stats_cell(row.before)} | {_duration_stats_cell(row.after)} | " + f"{_duration_stats_cell(row.reduction)} |" + ) + return ( + f"| {row.sku} | {row.scope} | {row.configurations} | " + f"{_factor(row.minimum)} | {_factor(row.median)} | {_factor(row.maximum)} |" + ) + + +def _render_layout_row(row: LayoutRow) -> str: + return ( + f"| {row.sku} | {row.comparison} | {_stats_cell(row.before)} | " + f"{_stats_cell(row.after)} |" + ) + + +def _factor(value: Decimal) -> str: + return f"{value:.2f}x" + + +def _stats_cell(values: tuple[Decimal, Decimal, Decimal] | None) -> str: + if values is None: + return NA_STATS + return " / ".join(_factor(value) for value in values) + + +def _duration_stats_cell(values: tuple[Decimal, Decimal, Decimal]) -> str: + return " / ".join(f"{value:.2f} µs" for value in values) + + +def _normalize_checklist( + checklist: Mapping[str, Any] | Sequence[ChecklistItem] | None, +) -> tuple[ChecklistItem, ...]: + if checklist is None: + return tuple( + ChecklistItem(label, False, "") + for label in CHECKLIST_LABELS + ) + if isinstance(checklist, Mapping): + items = [] + for label in CHECKLIST_LABELS: + value = checklist.get(label) + if isinstance(value, ChecklistItem): + items.append(value) + elif isinstance(value, (tuple, list)) and len(value) == 2: + items.append(ChecklistItem(label, bool(value[0]), str(value[1]))) + elif isinstance(value, str): + items.append(ChecklistItem(label, False, value)) + else: + raise SummaryError(f"checklist is missing {label!r}") + return tuple(items) + items = tuple(checklist) + if [item.label for item in items] != list(CHECKLIST_LABELS): + raise SummaryError("checklist labels are incomplete or out of order") + return items + + +def _join_description(prefix: str, block: str, suffix: str) -> str: + result = prefix + if result and not result.endswith("\n"): + result += "\n" + result += block + if suffix: + if not result.endswith("\n") and not suffix.startswith("\n"): + result += "\n" + result += suffix + return result + + +def _profile_is_measured(evidence: str) -> bool: + return bool( + re.search(r"\d+(?:\.\d+)?\s*%", evidence) + and re.search( + r"\b(?:SOL|BWUtil|DRAM|compute|memory|stall|scoreboard|throughput|occupancy)\b", + evidence, + re.IGNORECASE, + ) + ) + + +def _validate_visible_tables( + summary: OptimizationSummary, errors: list[ValidationIssue] +) -> None: + impact_by_sku: dict[str, list[ImpactRow | PythonOverheadImpactRow]] = {} + for row in summary.impact_rows: + impact_by_sku.setdefault(row.sku, []).append(row) + expected_count = ( + summary.optimized_count if row.scope == "Optimized" else summary.total_count + ) + if row.configurations != expected_count: + errors.append( + ValidationIssue( + "impact-count", + f"{row.sku} {row.scope} count does not match Scope", + ) + ) + statistics = ( + (("speedup", (row.minimum, row.median, row.maximum)),) + if isinstance(row, ImpactRow) + else ( + ("before", row.before), + ("after", row.after), + ("reduction", row.reduction), + ) + ) + for label, values in statistics: + if not values[0] <= values[1] <= values[2]: + errors.append( + ValidationIssue( + "impact-order", + f"{row.sku} {row.scope} {label} statistics are not ordered", + ) + ) + for sku, rows in impact_by_sku.items(): + if [row.scope for row in rows] != ["Optimized", "Full operator"]: + errors.append( + ValidationIssue( + "impact-rows", + f"{sku} must have exactly Optimized then Full operator rows", + ) + ) + + layout_by_sku: dict[str, list[LayoutRow]] = {} + for row in summary.layout_rows: + layout_by_sku.setdefault(row.sku, []).append(row) + for label, values in (("before", row.before), ("after", row.after)): + if values is not None and not values[0] <= values[1] <= values[2]: + errors.append( + ValidationIssue( + "layout-order", + f"{row.sku} {row.comparison} {label} statistics are not ordered", + ) + ) + for sku, rows in layout_by_sku.items(): + if [row.comparison for row in rows] != [ + "Interleaved / Planar", + "FakePlanar / Planar", + ]: + errors.append( + ValidationIssue( + "layout-rows", + f"{sku} must have exactly both canonical layout-comparison rows", + ) + ) + if list(impact_by_sku) != list(layout_by_sku): + errors.append( + ValidationIssue( + "sku-rows", "Impact and Layout comparison must report the same SKUs" + ) + ) + + +def _validate_checklist( + summary: OptimizationSummary, + evidence_results: Mapping[str, str] | None, + errors: list[ValidationIssue], + warnings: list[ValidationIssue], +) -> None: + for item in summary.checklist: + if summary.metadata.state == "final" and not item.checked: + errors.append( + ValidationIssue("checklist-unchecked", f"{item.label} is unchecked") + ) + if item.checked and not _hard_evidence(item.evidence): + errors.append( + ValidationIssue( + "checklist-evidence", + f"{item.label} is checked without concrete hard evidence", + ) + ) + if evidence_results is not None and item.checked: + status = evidence_results.get(item.label) + if status != "PASS": + errors.append( + ValidationIssue( + "checklist-gate", + f"{item.label} is checked but its hard gate is {status or 'missing'}", + ) + ) + if not item.checked and _PLACEHOLDER_RE.search(item.evidence): + target = errors if summary.metadata.state == "final" else warnings + target.append( + ValidationIssue( + "checklist-placeholder", + f"{item.label} still contains a placeholder", + ) + ) + + +def _hard_evidence(evidence: str) -> bool: + text = evidence.strip() + if len(text) < 20 or _PLACEHOLDER_RE.search(text): + return False + if text.casefold() in {"pass", "passed", "yes", "done", "n/a"}: + return False + named = bool( + re.search( + r"(?:\b\w+\.py\b|\b\w+_test\w*\b|\bODO-\d+\b|`[^`]+`|" + r"\b(?:baseline|artifact|benchmark|run_bench|EXPECT_(?:EQ|NEAR)|" + r"ncu|nsys|commit|SHA)\b)", + text, + re.IGNORECASE, + ) + ) + result = bool( + re.search( + r"\b(?:pass(?:ed)?|green|zero regressions?|no regressions?|\d+\s*/\s*\d+|" + r"at[- ]?ridge|strikes?|recommendations?|updated|imported|committed)\b", + text, + re.IGNORECASE, + ) + ) + return named and result + + +def _validate_learnings( + summary: OptimizationSummary, + errors: list[ValidationIssue], + warnings: list[ValidationIssue], +) -> None: + for learning in summary.learnings: + if learning == DEFAULT_LEARNING: + target = errors if summary.metadata.state == "final" else warnings + target.append( + ValidationIssue( + "learning-placeholder", + "Top learnings still contains the provisional default", + ) + ) + continue + if len(learning) > 500 or _PLACEHOLDER_RE.search(learning): + errors.append( + ValidationIssue( + "learning-placeholder", + "Top learnings contains a placeholder or oversized item", + ) + ) + continue + if not re.search(r"[.!?]$", learning): + errors.append( + ValidationIssue( + "learning-sentence", "Each top learning must be one sentence" + ) + ) + if re.search(r"[!?]\s+\S|\.\s+[A-Z]", learning[:-1]): + errors.append( + ValidationIssue( + "learning-sentence", "Each top learning must be one sentence" + ) + ) + + +def _validate_artifact_claims( + summary: OptimizationSummary, + baseline_config: Mapping[str, Any], + candidate_config: Mapping[str, Any], + sku_map: Mapping[str, Any], + errors: list[ValidationIssue], + warnings: list[ValidationIssue], +) -> None: + baseline = _config_surface(baseline_config, "baseline") + candidate = _config_surface(candidate_config, "candidate") + if baseline.benchmark.casefold() != candidate.benchmark.casefold(): + raise SummaryError("baseline and candidate benchmark identities differ") + _assert_comparable_surfaces(baseline, candidate) + if candidate.benchmark.casefold() != summary.metadata.operator.casefold(): + raise SummaryError( + "metadata operator does not match candidate benchmark identity" + ) + optimized = _optimized_cases(summary.metadata, candidate) + if summary.total_count != len(candidate.cases): + errors.append( + ValidationIssue( + "full-count", + "visible total is not the full expanded basic+advanced operator case set", + ) + ) + expected_categories = _categories(optimized) + if summary.categories != expected_categories: + errors.append( + ValidationIssue( + "categories", + "optimized categories do not match exact Container Layout Type derivation", + ) + ) + + references = _reference_skus(sku_map) + complete_skus = _complete_skus(baseline, candidate, summary.metadata.impact_metric) + missing_references = [stem for stem in references if stem not in complete_skus] + if summary.metadata.state == "final" and missing_references: + errors.append( + ValidationIssue( + "reference-coverage", + "final summary lacks full basic+advanced timing coverage for reference SKU(s): " + + ", ".join(missing_references), + ) + ) + if not complete_skus: + raise SummaryError("no SKU has timings for every expanded candidate case") + + expected_impact = _expected_impact_rows( + baseline, + candidate, + optimized, + complete_skus, + references, + summary.metadata.impact_metric, + ) + if summary.impact_rows != expected_impact: + errors.append( + ValidationIssue( + "impact-statistics", + "Impact rows/counts/min/median/max do not match the selected impact metric", + ) + ) + expected_layout = _expected_layout_rows( + baseline, candidate, complete_skus, references + ) + if summary.layout_rows != expected_layout: + errors.append( + ValidationIssue( + "layout-statistics", + "Layout rows or before/after min/median/max do not match full matched pairs", + ) + ) + + local_labels = { + _sku_label(stem) for stem in complete_skus if stem not in set(references) + } + warning_text = "\n".join(summary.warnings) + for label in sorted(local_labels): + if label not in warning_text: + errors.append( + ValidationIssue( + "local-sku-warning", + f"non-reference SKU {label} is reported without the required warning", + ) + ) + if not local_labels and summary.warnings: + errors.append( + ValidationIssue( + "local-sku-warning", + "non-reference warning is present without a local SKU row", + ) + ) diff --git a/.agents/tools/review_context.sh b/.agents/tools/review_context.sh new file mode 100755 index 000000000..025e085d0 --- /dev/null +++ b/.agents/tools/review_context.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +# Emit the shared "git context" block every review wrapper prepends to the shared +# review prompt. Injecting the committed diff as leading context makes all the +# independent agents (claude/codex/cursor) reason over the identical, correct +# scope, instead of each re-deriving it from whatever its own tool access happens +# to gather (which previously drifted -- e.g. an agent with no shell reviewing the +# staged working tree rather than the branch's committed diff). +# +# The diff is the merge-base ("three-dot") diff against the base, so it is scoped +# to this branch's own work and is not polluted by base-side changes made since +# the fork point (a two-dot diff would be). Uncommitted changes are surfaced under +# git status and left for the agent to treat as uncommitted. +# +# Args: [base_ref] (base defaults to main). + +set -euo pipefail + +repo_root="$1" +base="${2:-main}" + +# Never let a git hiccup abort the wrapper: the agents retain full read access and +# can gather context themselves if a command here comes back empty. +run() { git -C "${repo_root}" "$@" 2>&1 || true; return 0; } + +cat </dev/null; then + printf '%s\n' "${wt}" + else + rm -rf "${parent}" 2>/dev/null || true + return 1 + fi +} + +# Remove a worktree created by review_worktree_create; a no-op on an empty path or +# the repo root, so the real working tree is never removed. +review_worktree_remove() { + local repo_root="$1" wt="$2" + [[ -n "${wt}" && "${wt}" != "${repo_root}" ]] || return 0 + git -C "${repo_root}" worktree remove --force "${wt}" 2>/dev/null || true + rm -rf "$(dirname "${wt}")" 2>/dev/null || true +} diff --git a/.agents/tools/tests/test_optimization_summary.py b/.agents/tools/tests/test_optimization_summary.py new file mode 100644 index 000000000..b2f8a66e7 --- /dev/null +++ b/.agents/tools/tests/test_optimization_summary.py @@ -0,0 +1,1057 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +from __future__ import annotations + +import copy +import json +import sys +from pathlib import Path + +import pytest + + +REPO = Path(__file__).resolve().parents[3] +sys.path.insert(0, str(REPO / ".agents" / "tools")) +sys.path.insert(0, str(REPO / "bench")) + +from _internal.baselines import expected_case_keys_for_entry # noqa: E402 +from optimization_summary import ( # noqa: E402 + CHECKLIST_LABELS, + SummaryError, + SummaryMetadata, + generate_summary, + parse_summary, + refresh_summary, + validate_summary, +) +from optimization_summary import _config_surface, _logical_type # noqa: E402 + + +A100 = "A100_PCIE_40GB_250W_1095MHz" +H100 = "H100_PCIe_350W_1095MHz" +BASE = "1" * 40 +CANDIDATE = "2" * 40 + + +def _entry(tier, dtype, layout, before, after, *, extra_axes=None): + axes = { + "shape": ["4x1080x1920"], + "layout": [layout], + "inputKind": ["Tensor"], + **(extra_axes or {}), + } + entry = { + "tier": tier, + "dtypes": [dtype], + "string_axes": axes, + "baselines": {}, + } + return entry, before, after + + +def _surface(): + specs = { + "example_rgb_interleaved_basic": _entry("basic", "uchar3", "NHWC", 20, 10), + "example_rgb_planar_basic": _entry("basic", "uchar3", "NCHW", 10, 5), + "example_rgb_fake_advanced": _entry("advanced", "uchar3", "NCHW_FAKE", 30, 20), + "example_rgb_planar_pair_advanced": _entry("advanced", "uchar3", "NCHW", 10, 5), + "example_float_interleaved_advanced": _entry( + "advanced", "float3", "NHWC", 40, 20 + ), + "example_float_planar_advanced": _entry("advanced", "float3", "NCHW", 20, 10), + } + before = {"benchmark": "example", "configs": {}} + after = {"benchmark": "example", "configs": {}} + for key, (entry, old, new) in specs.items(): + before_entry = copy.deepcopy(entry) + after_entry = copy.deepcopy(entry) + case_key = expected_case_keys_for_entry(key, entry)[0] + before_entry["baselines"] = { + case_key: { + A100: {"gpu_time_us_cpp": old}, + H100: {"gpu_time_us_cpp": old * 2}, + } + } + after_entry["baselines"] = { + case_key: { + A100: {"gpu_time_us_cpp": new}, + H100: {"gpu_time_us_cpp": new * 2}, + } + } + before["configs"][key] = before_entry + after["configs"][key] = after_entry + return before, after + + +def _python_overhead_surface(): + before, after = _surface() + before_gaps = (100, 80, 60, 40, 20, 10) + after_gaps = (20, 30, 40, 30, 10, 15) + for index, key in enumerate(before["configs"]): + before_payload = next(iter(before["configs"][key]["baselines"].values())) + after_payload = next(iter(after["configs"][key]["baselines"].values())) + for stem, scale in ((A100, 1), (H100, 2)): + before_payload[stem]["gpu_time_us_python"] = ( + before_payload[stem]["gpu_time_us_cpp"] + before_gaps[index] * scale + ) + after_payload[stem]["gpu_time_us_python"] = ( + after_payload[stem]["gpu_time_us_cpp"] + after_gaps[index] * scale + ) + return before, after + + +def _surface_with_valid_advanced_fake_pair(): + return _surface() + + +def _metadata(candidate): + keys = [ + next(iter(candidate["configs"]["example_rgb_interleaved_basic"]["baselines"])), + next(iter(candidate["configs"]["example_float_planar_advanced"]["baselines"])), + ] + return SummaryMetadata("Example", "final", BASE, CANDIDATE, tuple(keys)) + + +def _sku_map(): + return {"entries": [{"stem": H100}, {"stem": A100}]} + + +def _checklist(): + evidence = { + "Pixelwise equality to reference": ( + "cvcuda_test_system used EXPECT_EQ against the CPU reference; " + "12/12 passed." + ), + "Memory-footprint checks": "ODO-9 `compute-sanitizer` memory checks passed 12/12.", + "Baselines updated": ( + "bench/_internal/update_baseline.py imported A100/H100 artifacts " + "and committed generated files." + ), + "Baseline validation": ( + "bench/_internal/validate_baselines.py passed against the baseline " + "with zero regressions." + ), + "Lead exhaustion": "ncu measured 93% Memory SOL at-ridge; PASS.", + "Review/refactor gate": "refactor_op.py --phase assess PASS with 0 recommendations.", + } + return {label: (True, evidence[label]) for label in CHECKLIST_LABELS} + + +def _render(*, state="final", prefix="", suffix="", secondary_operators=()): + before, after = _surface() + source = _metadata(after) + metadata = SummaryMetadata( + source.operator, + source.state, + source.baseline_commit, + source.candidate_commit, + source.optimized_cases, + impact_metric=source.impact_metric, + secondary_operators=secondary_operators, + ) + if state != "final": + metadata = SummaryMetadata( + metadata.operator, + state, + metadata.baseline_commit, + metadata.candidate_commit, + metadata.optimized_cases, + impact_metric=metadata.impact_metric, + secondary_operators=metadata.secondary_operators, + ) + return generate_summary( + metadata, + baseline_config=before, + candidate_config=after, + sku_map=_sku_map(), + bottleneck="Memory-bound", + profile_evidence="ncu measured 93% Memory SOL and 21% Compute SOL.", + checklist=_checklist(), + learnings=( + "Vectorized RGB loads made Tensor Interleaved RGB8 cases 2.00x faster.", + ), + prefix=prefix, + suffix=suffix, + ) + + +def test_generate_parse_and_validate_full_basic_advanced_surface(): + before, after = _surface() + text = _render() + parsed = parse_summary(text) + + assert parsed.optimized_count == 2 + assert parsed.total_count == 6 + assert parsed.categories == ( + "Tensor Interleaved RGB8", + "Tensor Planar RGBF32", + ) + assert [row.sku for row in parsed.impact_rows[:4]] == [ + "A100", + "A100", + "H100", + "H100", + ] + assert "| A100 | Full operator | 6 | 1.50x | 2.00x | 2.00x |" in text + assert "| A100 | Interleaved / Planar | 2.00x / 2.00x / 2.00x" in text + assert "| A100 | FakePlanar / Planar | 3.00x / 3.00x / 3.00x | 4.00x" in text + + result = validate_summary( + parsed, + baseline_config=before, + candidate_config=after, + sku_map=_sku_map(), + expected_operator="Example", + expected_candidate_commit=CANDIDATE, + evidence_results={label: "PASS" for label in CHECKLIST_LABELS}, + ) + assert result.ok, result.errors + + +def test_default_cpp_impact_metadata_remains_backward_compatible(): + text = _render() + + assert '"impact_metric"' not in text.splitlines()[0] + assert parse_summary(text).metadata.impact_metric == "cpp_time" + + explicit = text.replace( + '"state":"final"', + '"state":"final","impact_metric":"cpp_time"', + 1, + ) + assert parse_summary(explicit).metadata.impact_metric == "cpp_time" + + +def test_secondary_operator_metadata_round_trips_and_refreshes(): + before, after = _surface() + text = _render(secondary_operators=("BndBox",)) + + assert '"secondary_operators":["BndBox"]' in text.splitlines()[0] + assert parse_summary(text).metadata.secondary_operators == ("BndBox",) + + refreshed = refresh_summary( + text, + baseline_config=before, + candidate_config=after, + sku_map=_sku_map(), + state="provisional", + ) + assert parse_summary(refreshed).metadata.secondary_operators == ("BndBox",) + + +def test_secondary_operator_metadata_supports_bounded_shared_surfaces(): + secondary = tuple(f"Shared{index}" for index in range(34)) + text = _render(secondary_operators=secondary) + + assert parse_summary(text).metadata.secondary_operators == secondary + + with pytest.raises(SummaryError, match="at most 64 names"): + _render(secondary_operators=tuple(f"Shared{index}" for index in range(65))) + + +@pytest.mark.parametrize( + ("payload", "message"), + [ + ('"BndBox"', "must be an array"), + ('["BndBox","bndbox"]', "duplicate operators"), + ('["Example"]', "must not repeat the primary"), + ], +) +def test_secondary_operator_metadata_rejects_invalid_declarations(payload, message): + text = _render().replace( + '"operator":"Example"', + f'"operator":"Example","secondary_operators":{payload}', + 1, + ) + with pytest.raises(SummaryError, match=message): + parse_summary(text) + + +def test_python_overhead_impact_is_derived_and_validated(): + before, after = _python_overhead_surface() + metadata = SummaryMetadata( + "Example", + "final", + BASE, + CANDIDATE, + _metadata(after).optimized_cases, + impact_metric="python_overhead", + ) + text = generate_summary( + metadata, + baseline_config=before, + candidate_config=after, + sku_map=_sku_map(), + bottleneck="Memory-bound", + profile_evidence="nsys measured 93% memory throughput and Python launch gaps.", + checklist=_checklist(), + learnings=("Caching metadata reduced measured Python overhead by 80.00 µs.",), + ) + parsed = parse_summary(text) + + assert parsed.metadata.impact_metric == "python_overhead" + assert '"impact_metric":"python_overhead"' in text.splitlines()[0] + assert ( + "Python overhead is `gpu_time_us_python - gpu_time_us_cpp`; " + "reduction is `before - after`" in text + ) + assert ( + "| A100 | Optimized | 2 | 10.00 µs / 55.00 µs / 100.00 µs | " + "15.00 µs / 17.50 µs / 20.00 µs | " + "-5.00 µs / 37.50 µs / 80.00 µs |" in text + ) + assert ( + "| H100 | Full operator | 6 | 20.00 µs / 100.00 µs / 200.00 µs | " + "20.00 µs / 50.00 µs / 80.00 µs | " + "-10.00 µs / 30.00 µs / 160.00 µs |" in text + ) + + result = validate_summary( + parsed, + baseline_config=before, + candidate_config=after, + sku_map=_sku_map(), + evidence_results={label: "PASS" for label in CHECKLIST_LABELS}, + ) + assert result.ok, result.errors + + refreshed = refresh_summary( + text, + baseline_config=before, + candidate_config=after, + sku_map=_sku_map(), + state="provisional", + ) + assert parse_summary(refreshed).metadata.impact_metric == "python_overhead" + + edited = text.replace( + "-5.00 µs / 37.50 µs / 80.00 µs", + "-5.00 µs / 37.50 µs / 81.00 µs", + 1, + ) + result = validate_summary( + edited, + baseline_config=before, + candidate_config=after, + sku_map=_sku_map(), + ) + assert any(item.code == "impact-statistics" for item in result.errors) + + +def test_python_overhead_impact_requires_python_and_cpp_timings(): + before, after = _python_overhead_surface() + first = next(iter(after["configs"].values())) + del next(iter(first["baselines"].values()))[A100]["gpu_time_us_python"] + + with pytest.raises(SummaryError, match="gpu_time_us_python"): + generate_summary( + SummaryMetadata( + "Example", + "provisional", + BASE, + CANDIDATE, + _metadata(after).optimized_cases, + impact_metric="python_overhead", + ), + baseline_config=before, + candidate_config=after, + sku_map=_sku_map(), + bottleneck="Memory-bound", + profile_evidence="nsys measured 93% memory throughput and Python launch gaps.", + ) + + +def test_metadata_rejects_unknown_impact_metric(): + text = _render().replace( + '"state":"final"', + '"state":"final","impact_metric":"wall_clock"', + 1, + ) + with pytest.raises(SummaryError, match="impact_metric"): + parse_summary(text) + + +def test_tensor_batch_category_generates_and_validates(): + key = "example_tensor_batch_advanced" + entry, old, new = _entry( + "advanced", + "uchar3", + "NCHW", + 10, + 5, + extra_axes={"inputKind": ["TensorBatch"]}, + ) + case_key = expected_case_keys_for_entry(key, entry)[0] + before_entry = copy.deepcopy(entry) + after_entry = copy.deepcopy(entry) + before_entry["baselines"] = { + case_key: { + A100: {"gpu_time_us_cpp": old}, + H100: {"gpu_time_us_cpp": old * 2}, + } + } + after_entry["baselines"] = { + case_key: { + A100: {"gpu_time_us_cpp": new}, + H100: {"gpu_time_us_cpp": new * 2}, + } + } + before = {"benchmark": "example", "configs": {key: before_entry}} + after = {"benchmark": "example", "configs": {key: after_entry}} + metadata = SummaryMetadata("Example", "final", BASE, CANDIDATE, (case_key,)) + + text = generate_summary( + metadata, + baseline_config=before, + candidate_config=after, + sku_map=_sku_map(), + bottleneck="Memory-bound", + profile_evidence="nsys measured 88% memory throughput in the TensorBatch path.", + checklist=_checklist(), + learnings=( + "Caching TensorBatch metadata improved the measured path by 2.00x.", + ), + ) + parsed = parse_summary(text) + + assert parsed.categories == ("TensorBatch Planar RGB8",) + result = validate_summary( + parsed, + baseline_config=before, + candidate_config=after, + sku_map=_sku_map(), + expected_operator="Example", + expected_candidate_commit=CANDIDATE, + evidence_results={label: "PASS" for label in CHECKLIST_LABELS}, + ) + assert result.ok, result.errors + + +def test_generator_refuses_to_label_incomplete_evidence_final(): + before, after = _surface() + with pytest.raises(SummaryError, match="invalid final summary"): + generate_summary( + _metadata(after), + baseline_config=before, + candidate_config=after, + sku_map=_sku_map(), + bottleneck="Memory-bound", + profile_evidence="ncu measured 93% Memory SOL and 21% Compute SOL.", + ) + + +def test_full_surface_rejects_omitted_advanced_case(): + before, after = _surface() + text = _render() + del after["configs"]["example_float_planar_advanced"] + + result = validate_summary( + text, + baseline_config=before, + candidate_config=after, + sku_map=_sku_map(), + ) + + assert not result.ok + assert "candidate omits" in " ".join(item.message for item in result.errors) + + +def test_refresh_can_migrate_cpp_summary_to_python_overhead(): + before, after = _python_overhead_surface() + text = generate_summary( + _metadata(after), + baseline_config=before, + candidate_config=after, + sku_map=_sku_map(), + bottleneck="Memory-bound", + profile_evidence="nsys measured 93% memory throughput and Python launch gaps.", + checklist=_checklist(), + learnings=("Caching metadata reduced measured Python overhead by 80.00 µs.",), + ) + + migrated = refresh_summary( + text, + baseline_config=before, + candidate_config=after, + sku_map=_sku_map(), + impact_metric="python_overhead", + ) + + assert parse_summary(migrated).metadata.impact_metric == "python_overhead" + assert "Before min / median / max" in migrated + + +def test_generator_rejects_fake_planar_whose_native_match_is_cross_tier(): + before, after = _surface_with_valid_advanced_fake_pair() + for config in (before, after): + del config["configs"]["example_rgb_planar_pair_advanced"] + + with pytest.raises(SummaryError, match=r"unmatched FakePlanar.*same-tier"): + generate_summary( + _metadata(after), + baseline_config=before, + candidate_config=after, + sku_map=_sku_map(), + bottleneck="Memory-bound", + profile_evidence="ncu measured 93% Memory SOL and 21% Compute SOL.", + checklist=_checklist(), + learnings=("Vector loads improved the measured path by 2.00x.",), + ) + + +def test_generator_rejects_partial_fake_planar_pairing(): + before, after = _surface_with_valid_advanced_fake_pair() + key = "example_rgba_fake_advanced" + entry, old, new = _entry("advanced", "uchar4", "NCHW_FAKE", 50, 40) + case_key = expected_case_keys_for_entry(key, entry)[0] + for config, value in ((before, old), (after, new)): + item = copy.deepcopy(entry) + item["baselines"] = { + case_key: { + A100: {"gpu_time_us_cpp": value}, + H100: {"gpu_time_us_cpp": value * 2}, + } + } + config["configs"][key] = item + + with pytest.raises(SummaryError, match="1 unmatched FakePlanar"): + generate_summary( + _metadata(after), + baseline_config=before, + candidate_config=after, + sku_map=_sku_map(), + bottleneck="Memory-bound", + profile_evidence="ncu measured 93% Memory SOL and 21% Compute SOL.", + checklist=_checklist(), + learnings=("Vector loads improved the measured path by 2.00x.",), + ) + + +def test_validator_rejects_unmatched_fake_planar_in_supplied_artifacts(): + before, after = _surface_with_valid_advanced_fake_pair() + text = generate_summary( + _metadata(after), + baseline_config=before, + candidate_config=after, + sku_map=_sku_map(), + bottleneck="Memory-bound", + profile_evidence="ncu measured 93% Memory SOL and 21% Compute SOL.", + checklist=_checklist(), + learnings=("Vector loads improved the measured path by 2.00x.",), + ) + for config in (before, after): + del config["configs"]["example_rgb_planar_pair_advanced"] + + result = validate_summary( + text, + baseline_config=before, + candidate_config=after, + sku_map=_sku_map(), + ) + + assert not result.ok + assert "unmatched FakePlanar" in " ".join(item.message for item in result.errors) + + +def test_operator_that_declares_only_basic_tier_is_valid(): + before, after = _surface() + before["configs"] = { + key: value + for key, value in before["configs"].items() + if value["tier"] == "basic" + } + after["configs"] = { + key: value + for key, value in after["configs"].items() + if value["tier"] == "basic" + } + key = next(iter(after["configs"]["example_rgb_interleaved_basic"]["baselines"])) + text = generate_summary( + SummaryMetadata("Example", "provisional", BASE, CANDIDATE, (key,)), + baseline_config=before, + candidate_config=after, + sku_map=_sku_map(), + bottleneck="Memory-bound", + profile_evidence="ncu measured 93% Memory SOL and 21% Compute SOL.", + ) + assert parse_summary(text).total_count == 2 + + +def test_no_layout_operator_gets_verified_na_rows(): + entry = { + "tier": "basic", + "dtypes": ["uint8"], + "string_axes": {"shape": ["4x16x16"]}, + "baselines": {}, + } + key = "example_basic" + case_key = expected_case_keys_for_entry(key, entry)[0] + before_entry = copy.deepcopy(entry) + after_entry = copy.deepcopy(entry) + before_entry["baselines"] = { + case_key: { + A100: {"gpu_time_us_cpp": 10}, + H100: {"gpu_time_us_cpp": 8}, + } + } + after_entry["baselines"] = { + case_key: { + A100: {"gpu_time_us_cpp": 5}, + H100: {"gpu_time_us_cpp": 4}, + } + } + before = {"benchmark": "example", "configs": {key: before_entry}} + after = {"benchmark": "example", "configs": {key: after_entry}} + text = generate_summary( + SummaryMetadata("Example", "provisional", BASE, CANDIDATE, (case_key,)), + baseline_config=before, + candidate_config=after, + sku_map=_sku_map(), + bottleneck="Compute-bound", + profile_evidence="ncu measured 91% Compute SOL and 20% Memory SOL.", + ) + parsed = parse_summary(text) + assert parsed.categories == ("Tensor NoLayout U8",) + assert all(row.before is None and row.after is None for row in parsed.layout_rows) + + +def test_non_image_point_layouts_are_categorized_as_no_layout(): + entry = { + "tier": "basic", + "dtypes": ["float32"], + "string_axes": {"shape": ["512x1024"], "layout": ["NW", "NWC"]}, + "baselines": {}, + } + key = "example_basic" + case_keys = expected_case_keys_for_entry(key, entry) + before_entry = copy.deepcopy(entry) + after_entry = copy.deepcopy(entry) + before_entry["baselines"] = { + case_key: { + A100: {"gpu_time_us_cpp": 10}, + H100: {"gpu_time_us_cpp": 8}, + } + for case_key in case_keys + } + after_entry["baselines"] = { + case_key: { + A100: {"gpu_time_us_cpp": 5}, + H100: {"gpu_time_us_cpp": 4}, + } + for case_key in case_keys + } + before = {"benchmark": "example", "configs": {key: before_entry}} + after = {"benchmark": "example", "configs": {key: after_entry}} + + text = generate_summary( + SummaryMetadata("Example", "provisional", BASE, CANDIDATE, case_keys), + baseline_config=before, + candidate_config=after, + sku_map=_sku_map(), + bottleneck="Compute-bound", + profile_evidence="ncu measured 91% Compute SOL and 20% Memory SOL.", + ) + + parsed = parse_summary(text) + assert parsed.categories == ("Tensor NoLayout F32",) + assert all(row.before is None and row.after is None for row in parsed.layout_rows) + + +def test_full_surface_rejects_missing_expanded_baseline_key(): + before, after = _surface() + text = _render() + entry = after["configs"]["example_float_planar_advanced"] + entry["baselines"].clear() + + result = validate_summary( + text, + baseline_config=before, + candidate_config=after, + sku_map=_sku_map(), + ) + + assert not result.ok + assert "missing 1 expanded case" in " ".join(item.message for item in result.errors) + + +def test_full_surface_rejects_missing_reference_sku_timing(): + before, after = _surface() + text = _render() + case = next( + iter(after["configs"]["example_float_planar_advanced"]["baselines"].values()) + ) + del case[H100] + + result = validate_summary( + text, + baseline_config=before, + candidate_config=after, + sku_map=_sku_map(), + ) + + assert not result.ok + assert "lacks full before/after" in " ".join(item.message for item in result.errors) + + +def test_refresh_preserves_human_fields_and_surrounding_description(): + before, after = _surface() + memory_section = ( + "\n\n## Memory footprint\n" + "Peak attributable increase: 0 B\n" + "New runtime CUDA allocations/frees: no\n" + "Evidence: Source inspection found no new runtime allocations.\n" + ) + text = _render(prefix="Intro\n\n", suffix=memory_section) + refreshed = refresh_summary( + text, + baseline_config=before, + candidate_config=after, + sku_map=_sku_map(), + state="provisional", + candidate_commit="3" * 40, + ) + parsed = parse_summary(refreshed) + + assert refreshed.startswith("Intro\n\n") + assert refreshed.endswith(memory_section) + assert parsed.bottleneck == "Memory-bound" + assert parsed.profile_evidence == "ncu measured 93% Memory SOL and 21% Compute SOL." + assert parsed.checklist[0].evidence.endswith("12/12 passed.") + assert parsed.learnings[0].startswith("Vectorized RGB loads") + assert parsed.metadata.state == "provisional" + assert parsed.metadata.candidate_commit == "3" * 40 + + +def test_parser_ignores_fenced_example_but_rejects_duplicate_live_blocks(): + live = _render() + fenced = "```markdown\n" + live + "\n```\n\n" + assert parse_summary(fenced + live).metadata.operator == "Example" + four_tick_fence = "````markdown\n```\n" + live + "\n```\n````\n\n" + assert parse_summary(four_tick_fence + live).metadata.operator == "Example" + with pytest.raises(SummaryError, match="exactly one"): + parse_summary(live + "\n" + live) + + +def test_visible_schema_rejects_extra_detail_content(): + scope_detail = _render().replace( + "\n### Impact\n", + "\nExtra per-case detail must stay in artifacts.\n\n### Impact\n", + ) + with pytest.raises(SummaryError, match="Scope may contain only"): + parse_summary(scope_detail) + + impact_detail = _render().replace( + "\n### Layout comparison\n", + "\n| Extra | Per-case | Detail |\n\n### Layout comparison\n", + ) + with pytest.raises(SummaryError, match="Impact table"): + parse_summary(impact_detail) + + +@pytest.mark.parametrize("bad_state", [{}, [], 7, None]) +def test_untrusted_metadata_type_errors_are_bounded(bad_state): + text = _render() + text = text.replace('"state":"final"', f'"state":{json.dumps(bad_state)}', 1) + with pytest.raises(SummaryError, match="state"): + parse_summary(text) + + +def test_checked_item_requires_hard_evidence_and_matching_gate(): + before, after = _surface() + text = _render().replace( + "cvcuda_test_system used EXPECT_EQ against the CPU reference; 12/12 passed.", + "PASS", + ) + result = validate_summary( + text, + baseline_config=before, + candidate_config=after, + sku_map=_sku_map(), + evidence_results={label: "PASS" for label in CHECKLIST_LABELS}, + ) + assert any(item.code == "checklist-evidence" for item in result.errors) + + result = validate_summary( + _render(), evidence_results={label: "MANUAL" for label in CHECKLIST_LABELS} + ) + assert len([item for item in result.errors if item.code == "checklist-gate"]) == 6 + + +def test_hand_edited_impact_and_layout_statistics_are_rejected(): + before, after = _surface() + impact = _render().replace( + "| A100 | Optimized | 2 | 2.00x | 2.00x | 2.00x |", + "| A100 | Optimized | 2 | 1.00x | 1.00x | 1.00x |", + 1, + ) + result = validate_summary( + impact, + baseline_config=before, + candidate_config=after, + sku_map=_sku_map(), + ) + assert any(item.code == "impact-statistics" for item in result.errors) + + layout = _render().replace( + "| A100 | Interleaved / Planar | 2.00x / 2.00x / 2.00x | 2.00x / 2.00x / 2.00x |", + "| A100 | Interleaved / Planar | 1.00x / 1.00x / 1.00x | 1.00x / 1.00x / 1.00x |", + 1, + ) + result = validate_summary( + layout, + baseline_config=before, + candidate_config=after, + sku_map=_sku_map(), + ) + assert any(item.code == "layout-statistics" for item in result.errors) + + +def test_non_reference_sku_requires_prominent_warning(): + before, after = _surface() + for config in (before, after): + for entry in config["configs"].values(): + payload = next(iter(entry["baselines"].values())) + payload["RTX_6000_Ada"] = {"gpu_time_us_cpp": 100} + text = generate_summary( + _metadata(after), + baseline_config=before, + candidate_config=after, + sku_map=_sku_map(), + bottleneck="Memory-bound", + profile_evidence="ncu measured 93% Memory SOL and 21% Compute SOL.", + checklist=_checklist(), + learnings=("Vector loads improved the measured path by 2.00x.",), + ) + assert "⚠️ **Non-reference local GPU:** Results from RTX 6000 Ada" in text + + +def test_partial_non_reference_sku_is_not_silently_omitted(): + before, after = _surface() + first_before = next(iter(before["configs"].values())) + first_after = next(iter(after["configs"].values())) + next(iter(first_before["baselines"].values()))["RTX_6000_Ada"] = { + "gpu_time_us_cpp": 10 + } + next(iter(first_after["baselines"].values()))["RTX_6000_Ada"] = { + "gpu_time_us_cpp": 5 + } + with pytest.raises(SummaryError, match="lacks full before/after"): + generate_summary( + SummaryMetadata( + "Example", + "provisional", + BASE, + CANDIDATE, + _metadata(after).optimized_cases, + ), + baseline_config=before, + candidate_config=after, + sku_map=_sku_map(), + bottleneck="Memory-bound", + profile_evidence="ncu measured 93% Memory SOL and 21% Compute SOL.", + ) + + +def test_conversion_categories_use_logical_input_and_output_types(): + before, after = _surface() + key = "example_convert_advanced" + entry, old, new = _entry( + "advanced", + "uchar4", + "NHWC", + 10, + 5, + extra_axes={"code": ["YUV2RGB_NV12"]}, + ) + case_key = expected_case_keys_for_entry(key, entry)[0] + for config, value in ((before, old), (after, new)): + item = copy.deepcopy(entry) + item["baselines"] = { + case_key: { + A100: {"gpu_time_us_cpp": value}, + H100: {"gpu_time_us_cpp": value}, + } + } + config["configs"][key] = item + metadata = SummaryMetadata("Example", "final", BASE, CANDIDATE, (case_key,)) + text = generate_summary( + metadata, + baseline_config=before, + candidate_config=after, + sku_map=_sku_map(), + bottleneck="Compute-bound", + profile_evidence="ncu measured 91% Compute SOL and 20% Memory SOL.", + checklist=_checklist(), + learnings=("Conversion fusion improved the measured path by 2.00x.",), + ) + assert "`Tensor Interleaved NV12→RGBA8`" in text + + +@pytest.mark.parametrize( + ("axes", "expected"), + [ + ({"InOutDataType": "uint8", "numChannels": "3"}, "RGB8"), + ( + {"InOutDataType": "uchar3", "outDataType": "float32"}, + "RGB8→RGBF32", + ), + ({"InOutDataType": "uchar3", "outChannels": "4"}, "RGB8→RGBA8"), + ( + {"InOutDataType": "short2", "outDataType": "float32"}, + "2S16→2F32", + ), + ], +) +def test_logical_type_uses_channel_and_output_axes(axes, expected): + assert _logical_type(axes) == expected + + +def test_duplicate_workload_identity_is_rejected_before_layout_pairing(): + before, after = _surface() + source = "example_rgb_interleaved_basic" + duplicate = "example_rgb_interleaved_duplicate_basic" + for config in (before, after): + entry = copy.deepcopy(config["configs"][source]) + old_key = next(iter(entry["baselines"])) + new_key = duplicate + old_key[slice(old_key.index("["), None)] + entry["baselines"] = {new_key: entry["baselines"][old_key]} + config["configs"][duplicate] = entry + metadata = _metadata(after) + with pytest.raises(SummaryError, match="duplicate expanded workload identity"): + generate_summary( + metadata, + baseline_config=before, + candidate_config=after, + sku_map=_sku_map(), + bottleneck="Memory-bound", + profile_evidence="ncu measured 93% Memory SOL and 21% Compute SOL.", + ) + + +def test_duplicate_workload_without_layout_counterpart_is_rejected(): + before, after = _surface() + source = "example_rgb_interleaved_basic" + duplicate = "example_rgb_interleaved_duplicate_basic" + for config in (before, after): + config["configs"].pop("example_rgb_planar_basic") + entry = copy.deepcopy(config["configs"][source]) + old_key = next(iter(entry["baselines"])) + new_key = duplicate + old_key[slice(old_key.index("["), None)] + entry["baselines"] = {new_key: entry["baselines"][old_key]} + config["configs"][duplicate] = entry + + with pytest.raises(SummaryError, match="duplicate expanded workload identity"): + generate_summary( + _metadata(after), + baseline_config=before, + candidate_config=after, + sku_map=_sku_map(), + bottleneck="Memory-bound", + profile_evidence="ncu measured 93% Memory SOL and 21% Compute SOL.", + checklist=_checklist(), + learnings=("Vectorized RGB loads improved the measured path by 2.00x.",), + ) + + +def test_duplicate_workload_identity_ignores_axis_declaration_order(): + before, after = _surface() + source = "example_float_interleaved_advanced" + duplicate = "example_float_interleaved_reordered_advanced" + for config in (before, after): + entry = copy.deepcopy(config["configs"][source]) + entry["string_axes"] = dict(reversed(entry["string_axes"].items())) + old_key = next(iter(entry["baselines"])) + new_key = expected_case_keys_for_entry(duplicate, entry)[0] + entry["baselines"] = {new_key: entry["baselines"][old_key]} + config["configs"][duplicate] = entry + + with pytest.raises(SummaryError, match="complete raw axes"): + generate_summary( + _metadata(after), + baseline_config=before, + candidate_config=after, + sku_map=_sku_map(), + bottleneck="Memory-bound", + profile_evidence="ncu measured 93% Memory SOL and 21% Compute SOL.", + ) + + +def test_legacy_duplicate_alias_canonicalization_ignores_config_order(): + canonical = "brightnesscontrast_float3_varshape_advanced" + legacy_alias = "brightnesscontrast_rgb_f32_1080p_advanced" + + def config(order): + entries = {} + for config_key in order: + entry, _, _ = _entry("advanced", "float3", "NHWC", 20, 10) + case_key = expected_case_keys_for_entry(config_key, entry)[0] + entry["baselines"] = { + case_key: { + A100: {"gpu_time_us_cpp": 10}, + H100: {"gpu_time_us_cpp": 20}, + } + } + entries[config_key] = entry + return {"benchmark": "brightnesscontrast", "configs": entries} + + canonical_key = expected_case_keys_for_entry( + canonical, + config((canonical,))["configs"][canonical], + )[0] + baseline = _config_surface(config((canonical, legacy_alias)), "baseline") + candidate = _config_surface(config((legacy_alias, canonical)), "candidate") + + assert tuple(baseline.cases) == (canonical_key,) + assert tuple(candidate.cases) == (canonical_key,) + + +def test_identical_raw_axes_in_different_tiers_are_distinct_workloads(): + before, after = _surface() + source = "example_float_interleaved_advanced" + duplicate = "example_float_interleaved_basic_copy" + for config in (before, after): + entry = copy.deepcopy(config["configs"][source]) + entry["tier"] = "basic" + old_key = next(iter(entry["baselines"])) + new_key = duplicate + old_key[slice(old_key.index("["), None)] + entry["baselines"] = {new_key: entry["baselines"][old_key]} + config["configs"][duplicate] = entry + + text = generate_summary( + _metadata(after), + baseline_config=before, + candidate_config=after, + sku_map=_sku_map(), + bottleneck="Memory-bound", + profile_evidence="ncu measured 93% Memory SOL and 21% Compute SOL.", + checklist=_checklist(), + learnings=("Vectorized RGB loads improved the measured path by 2.00x.",), + ) + + assert parse_summary(text).metadata.operator == "Example" + + +def test_checked_in_operator_configs_have_supported_workload_identities(): + failures = [] + for path in sorted((REPO / "bench" / "config" / "operators").glob("*.json")): + config = json.loads(path.read_text(encoding="utf-8")) + try: + _config_surface(config, path.name) + except SummaryError as exc: + if "duplicate expanded workload identity" in str(exc): + failures.append(f"{path.name}: {exc}") + + assert not failures, "\n".join(failures) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 817341989..afa6f124e 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -53,7 +53,9 @@ jobs: git git-lfs gcc-11 g++-11 ninja-build ccache libgtest-dev libgmock-dev \ shellcheck curl doxygen python3 python3-pip python3-dev python3-distutils \ texlive-latex-extra ghostscript graphviz \ - && curl -L https://cmake.org/files/v3.20/cmake-3.20.1-linux-x86_64.tar.gz --output /tmp/cmake-3.20.1.tar.gz \ + && curl --fail --location --proto '=https' --proto-redir '=https' --tlsv1.2 \ + https://cmake.org/files/v3.20/cmake-3.20.1-linux-x86_64.tar.gz \ + --output /tmp/cmake-3.20.1.tar.gz \ && tar -xzf /tmp/cmake-3.20.1.tar.gz -C /tmp/ && sudo cp -r /tmp/cmake-3.20.1-linux-x86_64/bin/ /usr/local/ \ && sudo cp -r /tmp/cmake-3.20.1-linux-x86_64/share/ /usr/local/ && sudo cp -r /tmp/cmake-3.20.1-linux-x86_64/doc/ /usr/local/ \ && rm -rf /tmp/cmake-3.20.1* @@ -68,7 +70,7 @@ jobs: - if: matrix.language == 'c-cpp' name: Install CUDA Toolkit - uses: Jimver/cuda-toolkit@v0.2.16 + uses: Jimver/cuda-toolkit@9b295696791d75d658d8de64c4a85097ad8abeaf id: cuda-toolkit with: cuda: '12.2.0' @@ -97,7 +99,7 @@ jobs: name: Build CMake project run: | echo "Running CMake project build script" - ./ci/build.sh debug build "-DBUILD_SAMPLES=OFF -DBUILD_TESTS=OFF -DBUILD_PYTHON=ON" $* + ./build.sh debug build "-DBUILD_SAMPLES=OFF -DBUILD_TESTS=OFF -DBUILD_PYTHON=ON" $* - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 diff --git a/.gitignore b/.gitignore index 06e4bd4f3..4610e4ec6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,6 +22,12 @@ /cvcuda-installer*/ /.cache/ +# CMake user presets (personal overrides) +CMakeUserPresets.json + +# Build time benchmark reports +/bench-reports/ + # Visual Studio Code # ------------------ .vscode/ @@ -40,6 +46,10 @@ ipynb_checkpoints profile_default/ ipython_config.py +# Build artifacts +# --------------- +compile_commands.json + # Miscellaneous # ------------- *.bin @@ -49,12 +59,26 @@ ipython_config.py # Documentation # ------------- -_c_api -_cpp_api _c_cpp_api -_python_api _cvcuda_api +# Optimization summaries are MR/artifact state, not repository documentation. +/docs/optimize/ + # Samples # ------------- *.engine + +# Generated requirements files (produced by generate_requirements.sh from *.template sources) +docker/requirements.build.*.txt +tests/requirements.tests.*.txt +bench/python/requirements.bench.*.txt +samples/requirements.samples.*.txt +docs/requirements.docs.txt + +# Benchmark per-config outputs +# ---------------------------- +# run_bench.py writes one out__.csv per config (see make output +# filename in bench/run_bench.py); these are run artifacts, never source. +out_*_cpp.csv +out_*_python.csv diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 57be30ab8..91f1a0ef1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -37,6 +37,7 @@ repos: - id: check-case-conflict - id: destroyed-symlinks - id: check-yaml + args: [--unsafe] exclude: '.clang-format' - repo: https://github.com/detailyang/pre-commit-shell @@ -49,6 +50,7 @@ repos: rev: v14.0.6 hooks: - id: clang-format + exclude_types: ['json'] - repo: https://github.com/pryorda/dockerfilelint-precommit-hooks rev: v0.1.0 @@ -56,13 +58,14 @@ repos: - id: dockerfilelint - repo: https://github.com/psf/black - rev: 22.10.0 + rev: 25.11.0 hooks: - id: black - repo: https://github.com/pycqa/flake8 - rev: 5.0.4 + rev: 7.1.1 hooks: - id: flake8 args: - "--max-line-length=110" + diff --git a/3rdparty/CMakeLists.txt b/3rdparty/CMakeLists.txt deleted file mode 100644 index 0868ee168..000000000 --- a/3rdparty/CMakeLists.txt +++ /dev/null @@ -1,60 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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(CMAKE_FOLDER 3rdparty) - -# disable all warnings when compiling objects of 3rdparty -# libraries included here. It *doesn't* affect warnings in public -# header files that are included by cvcuda code. For that, see -# solution employed with nvbench. -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -w") -set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -w") -set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} -w") - -set(CMAKE_POSITION_INDEPENDENT_CODE on) - -# GoogleTest ----------------------------- -set(INSTALL_GTEST off) -set(BUILD_GMOCK on) -set(CMAKE_POLICY_DEFAULT_CMP0077 NEW) -if(BUILD_TESTS) - add_subdirectory(googletest) -endif() - -# pybind11 ----------------------------- -set(PYBIND11_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/pybind11" PARENT_SCOPE) - -# dlpack ----------------------------- -set(DLPACK_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/dlpack" PARENT_SCOPE) - -# cuOSD ----------------------------- -set(CUOSD_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/cuOSD" PARENT_SCOPE) - -# NVBench -------------------------------- -if(BUILD_BENCH) - set(NVBench_ENABLE_CUPTI off) - set(BUILD_SHARED_LIBS off) - add_subdirectory(nvbench) - # Because nvbench::main is an object library, cmake<=3.20 doesn't treat it - # like regular libraries, and just creating an cvcuda_nvbench interface - # library that depends on it doesn't work. We need to create an static - # library and pull in the objects created by nvbench::main, as per cmake - # docs. - add_library(cvcuda_nvbench_main STATIC $) - target_link_libraries(cvcuda_nvbench_main PUBLIC nvbench::nvbench) - target_include_directories(cvcuda_nvbench_main SYSTEM INTERFACE - ${CMAKE_CURRENT_SOURCE_DIR}/nvbench) - add_library(cvcuda::nvbench::main ALIAS cvcuda_nvbench_main) -endif() diff --git a/3rdparty/cuOSD/aarch64/include/cuosd.h b/3rdparty/cuOSD/aarch64/include/cuosd.h deleted file mode 100644 index 08dfdae7f..000000000 --- a/3rdparty/cuOSD/aarch64/include/cuosd.h +++ /dev/null @@ -1,178 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the "Software"), - * to deal in the Software without restriction, including without limitation - * the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL - * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - * DEALINGS IN THE SOFTWARE. - */ - -#ifndef CUOSD_H -#define CUOSD_H - -typedef struct -{ -} cuOSDContext; - -typedef cuOSDContext *cuOSDContext_t; - -enum class cuOSDClockFormat : int -{ - None = 0, - YYMMDD_HHMMSS = 1, - YYMMDD = 2, - HHMMSS = 3 -}; - -enum class cuOSDImageFormat : int -{ - None = 0, - RGB = 1, - RGBA = 2, - BlockLinearNV12 = 3, - PitchLinearNV12 = 4 -}; - -enum class cuOSDTextBackend : int -{ - None = 0, - PangoCairo = 1, - StbTrueType = 2 -}; - -typedef struct _cuOSDColor -{ - unsigned char r; - unsigned char g; - unsigned char b; - unsigned char a; -} cuOSDColor; - -// cuosd_context_create: support online generate text bitmap with required font. -cuOSDContext_t cuosd_context_create(); - -// set context text rendering backend -void cuosd_set_text_backend(cuOSDContext_t context, cuOSDTextBackend text_backend); - -// cuosd_context_destroy: deallocate all resource related to allocated cuOSD context -void cuosd_context_destroy(cuOSDContext_t context); - -// cuosd_measure_text: API to get tight width, height and upper offset from the given text's tight bounding box -void cuosd_measure_text(cuOSDContext_t context, const char *utf8_text, int font_size, const char *font, int *width, - int *height, int *yoffset); - -// cuosd_draw_text: draw utf8 text on given cuOSD context. -// x, y stands for left upper corner of the text's bounding box. -// bg_color stands for textbox background color in case alpha != 0 -// Draw nothing if font_size <=0, font_size is scaled by 3 and clamped to 10 - 500 pixels by default -void cuosd_draw_text(cuOSDContext_t context, const char *utf8_text, int font_size, const char *font, int x, int y, - cuOSDColor border_color, cuOSDColor bg_color = {0, 0, 0, 0}); - -// cuosd_draw_clock: draw clock element on given cuOSD context. -// x, y stands for left upper corner of the text's bounding box. 3 clock formats are supported: -// YYMMDD_HHMMSS, YYMMDD, HHMMSS -// Draw nothing if font_size <=0, font_size is scaled by 3 and clamped to 10 - 500 pixels by default -void cuosd_draw_clock(cuOSDContext_t context, cuOSDClockFormat format, long time, int font_size, const char *font, - int x, int y, cuOSDColor border_color, cuOSDColor bg_color = {0, 0, 0, 0}); - -// cuosd_draw_line: draw line element on given cuOSD context. -// x0, y0 stands for start point coordinate of the line, and x1, y1 stands for end point coordinate of the line. -void cuosd_draw_line(cuOSDContext_t context, int x0, int y0, int x1, int y1, int thickness, cuOSDColor color, - bool interpolation = true); - -// cuosd_draw_arrow: draw arrow element on given cuOSD context. -// x0, y0 stands for start point coordinate of the arrow, and x1, y1 stands for end point coordinate of the arrow. -void cuosd_draw_arrow(cuOSDContext_t context, int x0, int y0, int x1, int y1, int arrow_size, int thickness, - cuOSDColor color, bool interpolation = false); - -// cuosd_draw_point: draw point element on given cuOSD context. -// cx, cy stands for center point coordinate of the point. -void cuosd_draw_point(cuOSDContext_t context, int cx, int cy, int radius, cuOSDColor color); - -// cuosd_draw_circle: draw circle element on given cuOSD context. -// cx, cy stands for center point coordinate of the circle. -// thickness stands for border width when thickness > 0; stands for filled mode when thickness = -1. -// bg_color stands for inner color inside hollow circle in case alpha != 0 -void cuosd_draw_circle(cuOSDContext_t context, int cx, int cy, int radius, int thickness, cuOSDColor border_color, - cuOSDColor bg_color = {0, 0, 0, 0}); - -// cuosd_draw_rectangle: draw rectangle element on given cuOSD context. -// thickness stands for border width when thickness > 0; stands for filled mode when thickness = -1. -// bg_color stands for inner color inside hollow rectangle in case alpha != 0 -void cuosd_draw_rectangle(cuOSDContext_t context, int left, int top, int right, int bottom, int thickness, - cuOSDColor border_color, cuOSDColor bg_color = {0, 0, 0, 0}); - -// cuosd_draw_boxblur: Mean filtering in the region of interest -// The region of interest is first scaled to 32x32, filtered, and then scaled to the region of interest by nearest neighbor interpolation -// It is executed by a separate kernel function that is independent from the other drawing functions -void cuosd_draw_boxblur(cuOSDContext_t context, int left, int top, int right, int bottom, int kernel_size = 7); - -// cuosd_draw_rotationbox: draw rotated rectangle element on given cuOSD context. -// yaw: rotation angle from y-axis, clockwise +, unit in rad. -void cuosd_draw_rotationbox(cuOSDContext_t _context, int cx, int cy, int width, int height, float yaw, int thickness, - cuOSDColor border_color, bool interpolation = false, cuOSDColor bg_color = {0, 0, 0, 0}); - -// cuosd_draw_segmentmask: draw segmentation mask on given cuOSD context. -// d_seg: device pointer of segmentation mask, alpha in seg_color is ignored. -// thickness should > 0 for drawing border, threshold: Threshold for binarization -// 1. resize mask rect to object rect of given left, top, right, bottom. -// 2. set the alpha to 127 if mask value > threshold, else alpha = 0. -void cuosd_draw_segmentmask(cuOSDContext_t context, int left, int top, int right, int bottom, int thickness, - float *d_seg, int seg_width, int seg_height, float seg_threshold, cuOSDColor border_color, - cuOSDColor seg_color = {0, 0, 0, 0}); - -// cuosd_draw_polyline: draw polyline element on given cuOSD context. -// h_pts: host point of polyline points in { int x, int y } data format. -// d_pts: device point of polyline points, shall not be nullptr if fill_color.a != 0. -// n_pts: number of polyline points, thickness: polyline thickness. -// is_closed: if the end point shall be connected to start point. -// border_color: polyline color, fill_color: polyfill color. -void cuosd_draw_polyline(cuOSDContext_t context, int *h_pts, int *d_pts, int n_pts, int thickness, bool is_closed, - cuOSDColor border_color, bool interpolation = true, cuOSDColor fill_color = {0, 0, 0, 0}); - -// cuosd_draw_rgba_source: draw color from rgba source image on given cuOSD context. -// 1. resize incoming rgba source rect to target rect of given left, top, right, bottom. -// 2. blend incoming rgba src on target image rect in bilinear interpolation manner. -void cuosd_draw_rgba_source(cuOSDContext_t _context, int left, int top, int right, int bottom, void *d_src, - int src_width, int src_stride, int src_height); - -// cuosd_draw_nv12_source: draw color from nv12 source image on given cuOSD context. -// 1. resize incoming nv12 source rect to target rect of given left, top, right, bottom. -// 2. blend incoming nv12 src on target image rect in bilinear interpolation manner. -// note: use unified alpha and can support both PL and BL nv12 format. -void cuosd_draw_nv12_source(cuOSDContext_t context, int left, int top, int right, int bottom, void *d_src0, - void *d_src1, int src_width, int src_stride, int src_height, unsigned char alpha = 127, - bool block_linear = false); - -// cuosd_apply: calculate bounding box of all elements and transfer drawing commands to GPU. -// If format is RGBA, data0 is RGBA buffer, and data1 must be nullptr. -// If format is BlockLinearNV12, data0 and data1 is cudaSurfaceObject_t for Luma(Y) plane and Chroma(UV) plane -// If format is PitchLinearNV12, data0 is Luma(Y) plane buffer, and data1 is Chroma(UV) plane buffer -void cuosd_apply(cuOSDContext_t context, void *data0, void *data1, int width, int stride, int height, - cuOSDImageFormat format, void *stream = nullptr, bool launch_and_clear = true); - -// clear all pushed commands -void cuosd_clear(cuOSDContext_t context); - -// cuosd_launch: launch drawing kernel in async manner. -// If format is RGBA, data0 is RGBA buffer, and data1 must be nullptr. -// If format is BlockLinearNV12, data0 and data1 is cudaSurfaceObject_t for Luma(Y) plane and Chroma(UV) plane -// If format is PitchLinearNV12, data0 is Luma(Y) plane buffer, and data1 is Chroma(UV) plane buffer -void cuosd_launch(cuOSDContext_t context, void *data0, void *data1, int width, int stride, int height, - cuOSDImageFormat format, void *stream = nullptr); - -#endif // CUOSD_H diff --git a/3rdparty/cuOSD/aarch64/lib/libcuosd.a b/3rdparty/cuOSD/aarch64/lib/libcuosd.a deleted file mode 100644 index 8f9398ccd..000000000 --- a/3rdparty/cuOSD/aarch64/lib/libcuosd.a +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:67f5c92d4b2de6f276fdec8995e4cd346a28d613219424077303b68f4d23731a -size 8852890 diff --git a/3rdparty/cuOSD/x86_64/include/cuosd.h b/3rdparty/cuOSD/x86_64/include/cuosd.h deleted file mode 100644 index 08dfdae7f..000000000 --- a/3rdparty/cuOSD/x86_64/include/cuosd.h +++ /dev/null @@ -1,178 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the "Software"), - * to deal in the Software without restriction, including without limitation - * the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL - * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - * DEALINGS IN THE SOFTWARE. - */ - -#ifndef CUOSD_H -#define CUOSD_H - -typedef struct -{ -} cuOSDContext; - -typedef cuOSDContext *cuOSDContext_t; - -enum class cuOSDClockFormat : int -{ - None = 0, - YYMMDD_HHMMSS = 1, - YYMMDD = 2, - HHMMSS = 3 -}; - -enum class cuOSDImageFormat : int -{ - None = 0, - RGB = 1, - RGBA = 2, - BlockLinearNV12 = 3, - PitchLinearNV12 = 4 -}; - -enum class cuOSDTextBackend : int -{ - None = 0, - PangoCairo = 1, - StbTrueType = 2 -}; - -typedef struct _cuOSDColor -{ - unsigned char r; - unsigned char g; - unsigned char b; - unsigned char a; -} cuOSDColor; - -// cuosd_context_create: support online generate text bitmap with required font. -cuOSDContext_t cuosd_context_create(); - -// set context text rendering backend -void cuosd_set_text_backend(cuOSDContext_t context, cuOSDTextBackend text_backend); - -// cuosd_context_destroy: deallocate all resource related to allocated cuOSD context -void cuosd_context_destroy(cuOSDContext_t context); - -// cuosd_measure_text: API to get tight width, height and upper offset from the given text's tight bounding box -void cuosd_measure_text(cuOSDContext_t context, const char *utf8_text, int font_size, const char *font, int *width, - int *height, int *yoffset); - -// cuosd_draw_text: draw utf8 text on given cuOSD context. -// x, y stands for left upper corner of the text's bounding box. -// bg_color stands for textbox background color in case alpha != 0 -// Draw nothing if font_size <=0, font_size is scaled by 3 and clamped to 10 - 500 pixels by default -void cuosd_draw_text(cuOSDContext_t context, const char *utf8_text, int font_size, const char *font, int x, int y, - cuOSDColor border_color, cuOSDColor bg_color = {0, 0, 0, 0}); - -// cuosd_draw_clock: draw clock element on given cuOSD context. -// x, y stands for left upper corner of the text's bounding box. 3 clock formats are supported: -// YYMMDD_HHMMSS, YYMMDD, HHMMSS -// Draw nothing if font_size <=0, font_size is scaled by 3 and clamped to 10 - 500 pixels by default -void cuosd_draw_clock(cuOSDContext_t context, cuOSDClockFormat format, long time, int font_size, const char *font, - int x, int y, cuOSDColor border_color, cuOSDColor bg_color = {0, 0, 0, 0}); - -// cuosd_draw_line: draw line element on given cuOSD context. -// x0, y0 stands for start point coordinate of the line, and x1, y1 stands for end point coordinate of the line. -void cuosd_draw_line(cuOSDContext_t context, int x0, int y0, int x1, int y1, int thickness, cuOSDColor color, - bool interpolation = true); - -// cuosd_draw_arrow: draw arrow element on given cuOSD context. -// x0, y0 stands for start point coordinate of the arrow, and x1, y1 stands for end point coordinate of the arrow. -void cuosd_draw_arrow(cuOSDContext_t context, int x0, int y0, int x1, int y1, int arrow_size, int thickness, - cuOSDColor color, bool interpolation = false); - -// cuosd_draw_point: draw point element on given cuOSD context. -// cx, cy stands for center point coordinate of the point. -void cuosd_draw_point(cuOSDContext_t context, int cx, int cy, int radius, cuOSDColor color); - -// cuosd_draw_circle: draw circle element on given cuOSD context. -// cx, cy stands for center point coordinate of the circle. -// thickness stands for border width when thickness > 0; stands for filled mode when thickness = -1. -// bg_color stands for inner color inside hollow circle in case alpha != 0 -void cuosd_draw_circle(cuOSDContext_t context, int cx, int cy, int radius, int thickness, cuOSDColor border_color, - cuOSDColor bg_color = {0, 0, 0, 0}); - -// cuosd_draw_rectangle: draw rectangle element on given cuOSD context. -// thickness stands for border width when thickness > 0; stands for filled mode when thickness = -1. -// bg_color stands for inner color inside hollow rectangle in case alpha != 0 -void cuosd_draw_rectangle(cuOSDContext_t context, int left, int top, int right, int bottom, int thickness, - cuOSDColor border_color, cuOSDColor bg_color = {0, 0, 0, 0}); - -// cuosd_draw_boxblur: Mean filtering in the region of interest -// The region of interest is first scaled to 32x32, filtered, and then scaled to the region of interest by nearest neighbor interpolation -// It is executed by a separate kernel function that is independent from the other drawing functions -void cuosd_draw_boxblur(cuOSDContext_t context, int left, int top, int right, int bottom, int kernel_size = 7); - -// cuosd_draw_rotationbox: draw rotated rectangle element on given cuOSD context. -// yaw: rotation angle from y-axis, clockwise +, unit in rad. -void cuosd_draw_rotationbox(cuOSDContext_t _context, int cx, int cy, int width, int height, float yaw, int thickness, - cuOSDColor border_color, bool interpolation = false, cuOSDColor bg_color = {0, 0, 0, 0}); - -// cuosd_draw_segmentmask: draw segmentation mask on given cuOSD context. -// d_seg: device pointer of segmentation mask, alpha in seg_color is ignored. -// thickness should > 0 for drawing border, threshold: Threshold for binarization -// 1. resize mask rect to object rect of given left, top, right, bottom. -// 2. set the alpha to 127 if mask value > threshold, else alpha = 0. -void cuosd_draw_segmentmask(cuOSDContext_t context, int left, int top, int right, int bottom, int thickness, - float *d_seg, int seg_width, int seg_height, float seg_threshold, cuOSDColor border_color, - cuOSDColor seg_color = {0, 0, 0, 0}); - -// cuosd_draw_polyline: draw polyline element on given cuOSD context. -// h_pts: host point of polyline points in { int x, int y } data format. -// d_pts: device point of polyline points, shall not be nullptr if fill_color.a != 0. -// n_pts: number of polyline points, thickness: polyline thickness. -// is_closed: if the end point shall be connected to start point. -// border_color: polyline color, fill_color: polyfill color. -void cuosd_draw_polyline(cuOSDContext_t context, int *h_pts, int *d_pts, int n_pts, int thickness, bool is_closed, - cuOSDColor border_color, bool interpolation = true, cuOSDColor fill_color = {0, 0, 0, 0}); - -// cuosd_draw_rgba_source: draw color from rgba source image on given cuOSD context. -// 1. resize incoming rgba source rect to target rect of given left, top, right, bottom. -// 2. blend incoming rgba src on target image rect in bilinear interpolation manner. -void cuosd_draw_rgba_source(cuOSDContext_t _context, int left, int top, int right, int bottom, void *d_src, - int src_width, int src_stride, int src_height); - -// cuosd_draw_nv12_source: draw color from nv12 source image on given cuOSD context. -// 1. resize incoming nv12 source rect to target rect of given left, top, right, bottom. -// 2. blend incoming nv12 src on target image rect in bilinear interpolation manner. -// note: use unified alpha and can support both PL and BL nv12 format. -void cuosd_draw_nv12_source(cuOSDContext_t context, int left, int top, int right, int bottom, void *d_src0, - void *d_src1, int src_width, int src_stride, int src_height, unsigned char alpha = 127, - bool block_linear = false); - -// cuosd_apply: calculate bounding box of all elements and transfer drawing commands to GPU. -// If format is RGBA, data0 is RGBA buffer, and data1 must be nullptr. -// If format is BlockLinearNV12, data0 and data1 is cudaSurfaceObject_t for Luma(Y) plane and Chroma(UV) plane -// If format is PitchLinearNV12, data0 is Luma(Y) plane buffer, and data1 is Chroma(UV) plane buffer -void cuosd_apply(cuOSDContext_t context, void *data0, void *data1, int width, int stride, int height, - cuOSDImageFormat format, void *stream = nullptr, bool launch_and_clear = true); - -// clear all pushed commands -void cuosd_clear(cuOSDContext_t context); - -// cuosd_launch: launch drawing kernel in async manner. -// If format is RGBA, data0 is RGBA buffer, and data1 must be nullptr. -// If format is BlockLinearNV12, data0 and data1 is cudaSurfaceObject_t for Luma(Y) plane and Chroma(UV) plane -// If format is PitchLinearNV12, data0 is Luma(Y) plane buffer, and data1 is Chroma(UV) plane buffer -void cuosd_launch(cuOSDContext_t context, void *data0, void *data1, int width, int stride, int height, - cuOSDImageFormat format, void *stream = nullptr); - -#endif // CUOSD_H diff --git a/3rdparty/cuOSD/x86_64/lib/libcuosd.a b/3rdparty/cuOSD/x86_64/lib/libcuosd.a deleted file mode 100644 index 48df12797..000000000 --- a/3rdparty/cuOSD/x86_64/lib/libcuosd.a +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:123f4253184c2e03051d4838bd047d74e15e2c4a9bad0c85ee8aa6d4ab9a8473 -size 8714458 diff --git a/3rdparty/dlpack b/3rdparty/dlpack deleted file mode 160000 index ca4d00ad3..000000000 --- a/3rdparty/dlpack +++ /dev/null @@ -1 +0,0 @@ -Subproject commit ca4d00ad3e2e0f410eeab3264d21b8a39397f362 diff --git a/3rdparty/googletest b/3rdparty/googletest deleted file mode 160000 index 5ab508a01..000000000 --- a/3rdparty/googletest +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 5ab508a01f9eb089207ee87fd547d290da39d015 diff --git a/3rdparty/nvbench b/3rdparty/nvbench deleted file mode 160000 index 92d2e01cd..000000000 --- a/3rdparty/nvbench +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 92d2e01cd1ec32ecb914888a8b3374923d818165 diff --git a/3rdparty/pybind11 b/3rdparty/pybind11 deleted file mode 160000 index 941f45bcb..000000000 --- a/3rdparty/pybind11 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 941f45bcb51457884fa1afd6e24a67377d70f75c diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..b1b7101d8 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,140 @@ +[//]: # "SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved." +[//]: # "SPDX-License-Identifier: Apache-2.0" + +# CV-CUDA Agent Guidelines + +These instructions are for AI coding agents working in the CV-CUDA repository. +They define shared agent behavior and point to the project documents that own +the detailed workflows. This file (`AGENTS.md`) is the single canonical entry +point, read natively by Codex and Cursor; `CLAUDE.md` is a symlink to it for +Claude Code. + +Skills are defined once under `.agents/skills//SKILL.md` (the tool-agnostic +standard, read directly by Codex and Cursor); `.claude/skills` is a symlink to +`.agents/skills` for Claude Code. Edit the canonical files under `.agents/skills` +only — never the symlinked copies. + +Topic guidance documents live under `.agents/guidance/.md`; see the +"Authoritative docs" section below. This file and `README.md` remain at the +repository root as the canonical entry points. + +Shared helper modules imported by skill scripts live under `.agents/tools/`. + +## Repository map + +| Path | Purpose | +|------|---------| +| `src/` | C++ core library, C API, private operator implementations, and nvcv types | +| `python/` | pybind11-based Python bindings and wheel packaging | +| `tests/` | C++ googletest and Python pytest test suites | +| `bench/` | C++ and Python nvbench benchmarks; shared config in `bench/config/` | +| `samples/` | Example applications and interoperability samples | +| `docs/` | Sphinx and Doxygen documentation sources | +| `docker/` | Multi-arch builder and development Docker images | +| `ci/` | CI tooling and pipeline configuration | +| `lint/` | Pre-commit hooks and repository checks | + +## Authoritative docs + +- Project overview and compatibility: `README.md` +- Installation and source builds: `docs/sphinx/installation.rst` +- Tests: `tests/README.md` +- Benchmarks: `bench/README.md` +- Samples: `samples/README.md` +- New operators: `.agents/guidance/MAKE_OP_GUIDELINES.md` (narrative how-to: + `docs/sphinx/advanced/make_operator.rst`); `/make-op`, `/make-op-scaffold`, + `/make-op-verify` skills +- Per-operator coverage review (support/test/bench/docs): `.agents/guidance/REVIEW_OP_GUIDELINES.md` + (`/review-op` skill) +- Bug fixes: `.agents/guidance/BUGFIX_GUIDELINES.md` +- Operator optimization: `.agents/guidance/OPTIMIZATION_GUIDELINES.md` + (`/optimize-op` skill) +- Per-operator refactoring / redundancy reduction: `.agents/guidance/REFACTOR_OP_GUIDELINES.md` + (`/refactor-op` skill) + +Use those documents as the source of truth. Do not duplicate their checklists +in tool-specific prompts unless a command needs a short output template. + +## Working tree rules + +- Check `git status --short --branch` before editing and again before + summarizing work. +- Treat untracked or modified files you did not create as user-owned. Do not + remove, overwrite, reset, or check them out unless explicitly asked. +- Keep edits scoped to the requested task. Do not mix cleanup, formatting, or + unrelated refactors into functional changes. +- Prefer `rg` and `rg --files` for repository searches. +- Follow existing local patterns before adding helpers or abstractions. +- Use structured parsers or existing generators when the repository provides + them. + +## Repository invariants + +- New source, script, and documentation files need NVIDIA Apache 2.0 SPDX + headers. Files created in 2026 should use `Copyright (c) 2026`, not a range. +- Requirements `.txt` files under `tests/`, `bench/`, `samples/`, `docker/`, + and `docs/` are generally generated from `.template` files and `versions.env`. + Do not hand-edit generated requirements. Update the matching source template + or `versions.env`, then run `bash generate_requirements.sh`. +- Changes touching CUDA 12 paths usually need matching CUDA 13 coverage, and + vice versa. Check requirements, Docker, CI, and docs for paired updates. +- Public C, C++, and Python API changes need matching docs, tests, and review + of ABI/API compatibility expectations. +- Operators that consume images support both interleaved (`NHWC`/`HWC`) and planar + (`NCHW`/`CHW`) layouts by default. If image layouts do not apply, declare + `Planar image layouts: Not applicable` with a `Reason` in the operator's public + C-header Limitations contract. +- Comments should explain why something non-obvious is necessary. Do not add + comments that restate the code. + +## Validation ladder + +Use the narrowest validation that proves the change, then state exactly what +was and was not run. + +Common checks: + +```bash +bash generate_requirements.sh --check +pre-commit run --files +cmake --preset dev +cmake --build --preset dev +bash build.sh release build-rel -DBUILD_TESTS=1 +build-rel/bin/run_tests.sh +build-rel/bin/run_tests.sh cvcuda,cpp +build-rel/bin/run_tests.sh cvcuda,python +``` + +For targeted C++ work, prefer building and running the relevant executable +under `build-rel/bin/`, such as `cvcuda_test_system` or +`nvcv_test_cudatools_system`. For Python work, run the relevant pytest file or +the generated `cvcuda_test_python` wrapper when available. + +GPU, CUDA toolkit, Docker, profiler, and `compute-sanitizer` checks may not be +available in every agent environment. If a required check cannot be run, say so +plainly and identify the missing prerequisite. + +## Task policy + +- Reviews: act as a reviewer, lead with findings ordered by severity, and + include file and line references. +- Per-operator coverage review: follow `.agents/guidance/REVIEW_OP_GUIDELINES.md`; run the + deterministic checker `tools/review_op.py [--domain support|test|bench|docs]` + (or the `/review-op` skill), resolve every `MANUAL`, and close `GAP`s. +- Bug fixes: follow `.agents/guidance/BUGFIX_GUIDELINES.md`; add the deterministic failing + regression test before the fix and search sibling code paths. +- Optimization: follow `.agents/guidance/OPTIMIZATION_GUIDELINES.md`; establish benchmark + coverage, profile before coding, and preserve correctness. Gate readiness and the + definition-of-done with `tools/optimize_op.py --phase preflight|evidence` + (or the `/optimize-op` skill); close every `GAP` before the campaign is done. +- Per-operator refactoring: follow `.agents/guidance/REFACTOR_OP_GUIDELINES.md`; + use `tools/refactor_op.py ` to assess redundancy opportunities and + `tools/refactor_op.py --phase verify` to gate parity before review. +- New operators: follow `.agents/guidance/MAKE_OP_GUIDELINES.md` (narrative how-to in + `docs/sphinx/advanced/make_operator.rst`). Propose + get user approval of the spec (semantics + + cited reference oracle + support matrix), scaffold with `tools/mkop/mkop.sh`, then gate with + `tools/make_op.py --phase scaffold|done` (or the `/make-op` family). The done-gate is the + deterministic regression checklist — independent CPU gold reference, bit-exact coverage of every + declared variant, required equivalent-layout parity, complement negatives, docs + relnote, + benched dtypes, tests that run & pass; it reuses `/review-op` and `/optimize-op` preflight and + hands off to `/optimize-op`. Use `/make-op-scaffold [--bare]` to delegate the implementation. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 000000000..47dc3e3d8 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 9ad41245f..c46156af5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,18 +15,19 @@ cmake_minimum_required(VERSION 3.20.1) -# We need to check this variable before starting a CUDA project - otherwise it will appear -# as set, with the default value pointing to the oldest supported architecture (52 as of CUDA 11.8) -if(CMAKE_CUDA_ARCHITECTURES) - set(USE_CMAKE_CUDA_ARCHITECTURES TRUE) -endif() - project(cvcuda LANGUAGES C CXX - VERSION 0.16.0 + VERSION 0.17.0 DESCRIPTION "CUDA-accelerated Computer Vision algorithms" ) +# CUDA initializes CMAKE_CUDA_ARCHITECTURES while enabling the language. Record +# its provenance after project() loads any toolchain file, but before enabling +# CUDA, so toolchain/cache values (including false values such as OFF) and +# CUDAARCHS are distinguishable from CV-CUDA's generated defaults. +include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/CUDAArchitecturePolicy.cmake") +cvcuda_detect_cuda_architecture_source() + # Make sure the cuda host compiler agrees with what we're using, # unless user overwrites it (at their own risk). if(NOT CMAKE_CUDA_HOST_COMPILER) @@ -35,9 +36,6 @@ endif() enable_language(CUDA) -# Used when creating special builds -set(PROJECT_VERSION_SUFFIX "") - # if user didn't set install prefix, if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) # Allow cv-cuda libraries with different major versions to be @@ -51,12 +49,15 @@ set(_BUILD_TESTS_WHEELS_CMDLINE ${BUILD_TESTS_WHEELS}) set(_BUILD_TESTS_PYTHON_CMDLINE ${BUILD_TESTS_PYTHON}) # Options to configure the build tree ======= +option(BUILD_LIB "Build cvcuda + nvcv_types libraries (and their consumers: Python bindings, tests, docs). When OFF, downstream sub-projects (bench, samples) build against an installed cvcuda-dev via find_package." ON) option(BUILD_TESTS "Enable testsuite" ON) option(BUILD_TESTS_CPP "Build C++ tests" OFF) option(BUILD_TESTS_WHEELS "Generate test_wheels.sh script" OFF) option(BUILD_TESTS_PYTHON "Build Python tests" OFF) option(BUILD_PYTHON "Build python bindings" ON) +option(BUILD_PYTHON_WHEEL "Package Python bindings into a wheel" ON) option(BUILD_BENCH "Build benchmark" OFF) +option(BUILD_SAMPLES "Build samples" OFF) option(BUILD_DOCS "Build documentation" OFF) option(ENABLE_SANITIZER "Enabled sanitized build" OFF) @@ -109,34 +110,67 @@ include(ConfigBuildTree) include(ConfigCompiler) include(ConfigCUDA) include(ConfigCCache) -if(BUILD_PYTHON) + +add_library(cvcuda_nvtx_config INTERFACE) +find_path(CVCUDA_NVTX_INCLUDE_DIR + NAMES nvtx3/nvToolsExt.h + HINTS + ${CUDAToolkit_INCLUDE_DIRS} + ${CUDAToolkit_TARGET_DIR}/include + NO_DEFAULT_PATH +) +if(NOT CVCUDA_NVTX_INCLUDE_DIR) + message(FATAL_ERROR "nvtx3/nvToolsExt.h not found in the CUDA Toolkit include path") +endif() +target_include_directories(cvcuda_nvtx_config INTERFACE ${CVCUDA_NVTX_INCLUDE_DIR}) + +if(BUILD_LIB AND BUILD_PYTHON) + # ConfigPython generates Find*.cmake files that reference the in-tree + # nvcv_types + cvcuda targets via $. Only meaningful when + # we're actually building the lib in this tree; with BUILD_LIB=OFF, those + # targets don't exist and the generator expressions error out. include(ConfigPython) endif() # Define the build tree ==================== -add_subdirectory(3rdparty EXCLUDE_FROM_ALL) - -add_subdirectory(src) +# In-tree library + everything that links against it directly. Skipping this +# block (BUILD_LIB=OFF) lets a tree configure for downstream-only builds — +# e.g. a CI test pod that compiles bench against an installed cvcuda-dev +# without rebuilding the lib itself. +if(BUILD_LIB) + add_subdirectory(src) -if(BUILD_PYTHON) - include(BuildPython) -endif() + if(BUILD_PYTHON) + include(BuildPython) + endif() -if(BUILD_TESTS_CPP OR BUILD_TESTS_WHEELS OR BUILD_TESTS_PYTHON) - add_subdirectory(tests) -endif() + if(BUILD_TESTS_CPP OR BUILD_TESTS_WHEELS OR BUILD_TESTS_PYTHON) + add_subdirectory(tests) + endif() -if(BUILD_DOCS) - add_subdirectory(docs) + if(BUILD_DOCS) + add_subdirectory(docs) + endif() endif() +# Bench + samples are downstream consumers — they work whether the lib was +# built in this tree (BUILD_LIB=ON) or imported from an installed cvcuda-dev +# (BUILD_LIB=OFF, see bench/cpp/CMakeLists.txt for the find_package fallback). if(BUILD_BENCH) + find_package(nvbench REQUIRED) + add_library(cvcuda::nvbench::main ALIAS nvbench::main) add_subdirectory(bench) endif() +if(BUILD_SAMPLES) + add_subdirectory(samples) +endif() + # Must be done after build tree is defined -include(ConfigCPack) +if(BUILD_LIB) + include(ConfigCPack) +endif() # Print build tree configuration =========== diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 000000000..43c3ac882 --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,75 @@ +{ + "version": 6, + "cmakeMinimumRequired": { + "major": 3, + "minor": 24, + "patch": 0 + }, + "configurePresets": [ + { + "name": "dev", + "displayName": "Developer (native GPU, Release)", + "description": + "Fast build for local development: detects the installed GPU and compiles only for that architecture.", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build-dev", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "CMAKE_CUDA_ARCHITECTURES": "native", + "BUILD_TESTS": "ON", + "BUILD_PYTHON": "OFF", + "BUILD_BENCH": "OFF", + "BUILD_SAMPLES": "OFF", + "BUILD_DOCS": "OFF" + } + }, + { + "name": "dev-debug", + "inherits": "dev", + "displayName": "Developer (native GPU, Debug)", + "binaryDir": "${sourceDir}/build-dev-dbg", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } + }, + { + "name": "dev-py", + "inherits": "dev", + "displayName": "Developer (native GPU, Release, Python)", + "description": "Like 'dev' but also builds Python bindings.", + "binaryDir": "${sourceDir}/build-dev-py", + "cacheVariables": { + "BUILD_PYTHON": "ON" + } + }, + { + "name": "dev-bench", + "inherits": "dev", + "displayName": "Developer (native GPU, Release, Benchmarks)", + "description": + "Like 'dev' but also builds C++ and Python benchmarks. Requires nvbench installed system-wide.", + "binaryDir": "${sourceDir}/build-dev-bench", + "cacheVariables": { + "BUILD_BENCH": "ON" + } + } + ], + "buildPresets": [ + { + "name": "dev", + "configurePreset": "dev" + }, + { + "name": "dev-debug", + "configurePreset": "dev-debug" + }, + { + "name": "dev-py", + "configurePreset": "dev-py" + }, + { + "name": "dev-bench", + "configurePreset": "dev-bench" + } + ] +} diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md deleted file mode 100644 index 1b63db508..000000000 --- a/DEVELOPER_GUIDE.md +++ /dev/null @@ -1,210 +0,0 @@ - -[//]: # "SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved." -[//]: # "SPDX-License-Identifier: Apache-2.0" -[//]: # "" -[//]: # "Licensed 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." - - -# CV-CUDA Developer Guide - -## What is CV-CUDA? - -CV-CUDA™ is an open-source, graphics processing unit (GPU)-accelerated library -for cloud-scale image processing and computer vision developed jointly by NVIDIA -and the ByteDance Applied Machine Learning teams. CV-CUDA helps developers build -highly efficient pre- and post-processing pipelines that can improve throughput -by more than 10x while lowering cloud computing costs. - -CV-CUDA includes: - -- A unified, specialized set of high-performance CV and image processing kernels -- C, C++, and Python APIs -- Batching support, with variable shape images -- Zero-copy interfaces to PyTorch -- Sample applications - -## What Pre- and Post-Processing Operators Are Included? - -| Pre/Post-Processing Operators | Definition | -|-------------------------------|------------| -| Adaptive Thresholding | Chooses threshold based on smaller regions in the neighborhood of each pixel. | -| Advanced Color Format Conversions | Performs color conversion from interleaved RGB/BGR <-> YUV/YVU and semi planar. Supported standards: BT.601. BT.709. BT.2020 | -| AverageBlur | Reduces image noise using an average filter | -| BilateralFilter | Reduces image noise while preserving strong edges | -| Bounding Box | Draws an rectangular border using the X-Y coordinates and dimensions typically to define the location and size of an object in an image | -| Box Blurring | Overlays a blurred rectangle using the X-Y coordinates and dimensions that define the location and size of an object in an image | -| Brightness_Contrast | Adjusts brightness and contrast of an image | -| CenterCrop | Crops an image at its center | -| ChannelReorder | Shuffles the order of image channels | -| Color_Twist | Adjusts the hue saturation brightness and contrast of an image | -| Composite | Composites two images together | -| Conv2D | Convolves an image with a provided kernel | -| CopyMakeBorder | Creates a border around an image | -| CustomCrop | Crops an image with a given region-of-interest | -| CvtColor | Converts an image from one color space to another | -| DataTypeConvert | Converts an image’s data type, with optional scaling | -| Erase | Erases image regions | -| Flip | Flips a 2D image around its axis | -| GammaContrast | Adjusts image contrast | -| Gaussian | Applies a gaussian blur filter to the image | -| Gaussian Noise | Generates a statistical noise with a normal (Gaussian) distribution | -| Histogram | Provides a grayscale value distribution showing the frequency of occurrence of each gray value. | -| Histogram Equalizer | Allows effective spreading out the intensity range of the image typically used to improve contrast | -| HqResize | Performs advanced resizing supporting 2D and 3D data, tensors, tensor batches, and varshape image batches (2D only). Supports nearest neighbor, linear, cubic, Gaussian and Lanczos interpolation, with optional antialiasing when down-sampling | -| Inpainting | Performs inpainting by replacing a pixel by normalized weighted sum of all the known pixels in the neighborhood | -| Joint Bilateral Filter | Reduces image noise while preserving strong edges based on a guidance image | -| Label | Labels connected regions in an image using 4-way connectivity for foreground and 8-way for background pixels | -| Laplacian | Applies a Laplace transform to an image | -| MedianBlur | Reduces an image’s salt-and-pepper noise | -| MinArea Rect | Finds the minimum area rotated rectangle typically used to draw bounding rectangle with minimum area | -| MinMaxLoc | Finds the maximum and minimum values in a given array | -| Morphology | Performs morphological erode and dilate transformations | -| Morphology (close) | Performs morphological operation that involves dilation followed by erosion on an image | -| Morphology (open) | Performs morphological operation that involves erosion followed by dilation on an image | -| Non-Maximum Suppression | Enables selecting a single entity out of many overlapping ones typically used for selecting from multiple bounding boxes during object detection | -| Normalize | Normalizes an image pixel’s range | -| OSD (Polyline Line Text Rotated Rect Segmented Mask) | Displays an overlay on the image of different forms including polyline line text rotated rectangle segmented mask | -| PadStack | Stacks several images into a tensor with border extension | -| PairwiseMatcher | Matches features computed separately (e.g. via the SIFT operator) in two images, e.g. using the brute force method | -| PillowResize | Changes the size and scale of an image using python-pillow algorithm | -| RandomResizedCrop | Crops a random portion of an image and resizes it to a specified size. | -| Reformat | Converts a planar image into non-planar and vice versa | -| Remap | Maps pixels in an image with one projection to another projection in a new image. | -| Resize | Changes the size and scale of an image | -| ResizeCropConvertReformat | Performs fused Resize-Crop-Convert-Reformat sequence with optional channel reordering | -| Rotate | Rotates a 2D array in multiples of 90 degrees | -| SIFT | Identifies and describes features in images that are invariant to scale rotation and affine distortion. | -| Thresholding | Chooses a global threshold value that is the same for all pixels across the image. | -| WarpAffine | Applies an affine transformation to an image | -| WarpPerspective | Applies a perspective transformation to an image | - -## Where Are the Release Notes? - -CV-CUDA release notes can be -found [here](https://github.com/CVCUDA/CV-CUDA/releases) - -## Where Can I Get Help? - -An awesome product requires excellent support. File requests for enhancements and bug reports -[here](https://github.com/CVCUDA/CV-CUDA/issues/new/choose). - -We are providing limited, direct, support to select enterprises using CV-CUDA. -To apply for direct enterprise developer engagement from NVIDIA , please fill -out the early access developer application -[here](http://developer.nvidia.com/cv-cuda/early-access). - -## What Other Computer Vision Products Does NVIDIA Offer? - -NVIDIA offers a number of computer vision products - -In addition to cloud-scale computer vision and image processing, NVIDIA offers: - -- [DALI](https://developer.nvidia.com/dali) (Data Loading Library), a portable, - holistic framework for accelerated data loading and augmentation in deep - learning workflows involving images, videos, and audio data. -- [VPI](https://developer.nvidia.com/embedded/vpi) (Vision Programming - Interface), an accelerated computer vision and image processing software - library primarily for embedded/edge applications. -- [cuCIM](https://developer.nvidia.com/multidimensional-image-processing) - (Compute Unified Device Architecture Clara Image), an open source, - accelerated computer vision and image processing library for multidimensional - images in biomedical, geospatial, material life science, and remote sensing - use cases. -- [NPP](https://developer.nvidia.com/npp) (NVIDIA Performance Primitives), an - image, signal, and video processing library that accelerates and performs - domain-specific functions. - -If you want to learn more about what computer vision solutions are available, -review the computer vision solutions landing page. - ---- - - -Notice - -The information provided in this specification is believed to be accurate and -reliable as of the date provided. However, NVIDIA Corporation (“NVIDIA”) does -not give any representations or warranties, expressed or implied, as to the -accuracy or completeness of such information. NVIDIA shall have no liability for -the consequences or use of such information or for any infringement of patents -or other rights of third parties that may result from its use. This publication -supersedes and replaces all other specifications for the product that may have -been previously supplied. - -NVIDIA reserves the right to make corrections, modifications, enhancements, -improvements, and other changes to this specification, at any time and/or to -discontinue any product or service without notice. Customer should obtain the -latest relevant specification before placing orders and should verify that such -information is current and complete. - -NVIDIA products are sold subject to the NVIDIA standard terms and conditions of -sale supplied at the time of order acknowledgement, unless otherwise agreed in -an individual sales agreement signed by authorized representatives of NVIDIA and -customer. NVIDIA hereby expressly objects to applying any customer general terms -and conditions with regards to the purchase of the NVIDIA product referenced in -this specification. - -NVIDIA products are not designed, authorized or warranted to be suitable for use -in medical, military, aircraft, space or life support equipment, nor in -applications where failure or malfunction of the NVIDIA product can reasonably -be expected to result in personal injury, death or property or environmental -damage. NVIDIA accepts no liability for inclusion and/or use of NVIDIA products -in such equipment or applications and therefore such inclusion and/or use is at -customer’s own risk. - -NVIDIA makes no representation or warranty that products based on these -specifications will be suitable for any specified use without further testing or -modification. Testing of all parameters of each product is not necessarily -performed by NVIDIA. It is customer’s sole responsibility to ensure the product -is suitable and fit for the application planned by customer and to do the -necessary testing for the application in order to avoid a default of the -application or the product. Weaknesses in customer’s product designs may affect -the quality and reliability of the NVIDIA product and may result in additional -or different conditions and/or requirements beyond those contained in this -specification. NVIDIA does not accept any liability related to any default, -damage, costs or problem which may be based on or attributable to: (i) the use -of the NVIDIA product in any manner that is contrary to this specification, or -(ii) customer product designs. - -No license, either expressed or implied, is granted under any NVIDIA patent -right, copyright, or other NVIDIA intellectual property right under this -specification. Information published by NVIDIA regarding third-party products or -services does not constitute a license from NVIDIA to use such products or -services or a warranty or endorsement thereof. Use of such information may -require a license from a third party under the patents or other intellectual -property rights of the third party, or a license from NVIDIA under the patents -or other intellectual property rights of NVIDIA. Reproduction of information in -this specification is permissible only if reproduction is approved by NVIDIA in -writing, is reproduced without alteration, and is accompanied by all associated -conditions, limitations, and notices. - -ALL NVIDIA DESIGN SPECIFICATIONS, REFERENCE BOARDS, FILES, DRAWINGS, DIAGNOSTICS, -LISTS, AND OTHER DOCUMENTS (TOGETHER AND SEPARATELY, “MATERIALS”) ARE BEING -PROVIDED “AS IS.” NVIDIA MAKES NO WARRANTIES, EXPRESSED, IMPLIED, STATUTORY, OR -OTHERWISE WITH RESPECT TO THE MATERIALS, AND EXPRESSLY DISCLAIMS ALL IMPLIED -WARRANTIES OF NONINFRINGEMENT, MERCHANTABILITY, AND FITNESS FOR A PARTICULAR -PURPOSE. Notwithstanding any damages that customer might incur for any reason -whatsoever, NVIDIA’s aggregate and cumulative liability towards customer for the -products described herein shall be limited in accordance with the NVIDIA terms -and conditions of sale for the product. - -Trademarks - -NVIDIA, the NVIDIA logo, NVIDIA CV-CUDA, and NVIDIA TensorRT are trademarks -and/or registered trademarks of NVIDIA Corporation in the U.S. and other -countries. Other company and product names may be trademarks of the respective -companies with which they are associated. - -Copyright - -© 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - diff --git a/README.md b/README.md index 92a90d107..99d3e2ada 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -[//]: # "SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved." +[//]: # "SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved." [//]: # "SPDX-License-Identifier: Apache-2.0" [//]: # "" [//]: # "Licensed under the Apache License, Version 2.0 (the 'License');" @@ -15,8 +15,7 @@ # CV-CUDA - -![Version](https://img.shields.io/badge/Version-v0.16.0-blue) +![Version](https://img.shields.io/badge/Version-v0.17.0-blue) [![PyPI](https://img.shields.io/badge/PyPI-available-blue?logo=pypi&logoColor=white)](https://pypi.org/search/?q=cvcuda) [![License](https://img.shields.io/badge/License-Apache_2.0-yellogreen.svg)](https://opensource.org/licenses/Apache-2.0) @@ -24,10 +23,9 @@ [![CUDA](https://img.shields.io/badge/CUDA-v12.2+_%7c_v13.x-%2376B900?logo=nvidia)](https://developer.nvidia.com/cuda-toolkit-archive) [![GCC](https://img.shields.io/badge/GCC-v11.0-yellow)](https://gcc.gnu.org/gcc-11/changes.html) -[![Python](https://img.shields.io/badge/python-3.9--3.14-blue?logo=python)](https://www.python.org/) +[![Python](https://img.shields.io/badge/python-3.10--3.14-blue?logo=python)](https://www.python.org/) [![CMake](https://img.shields.io/badge/CMake-v3.24.3+-%23008FBA?logo=cmake)](https://cmake.org/) - CV-CUDA is an open-source library of GPU-accelerated computer vision algorithms designed for speed and scalability. It delivers high-throughput, low-latency image/video processing for AI pipelines across NVIDIA cloud, desktop, and edge platforms. CV-CUDA is built for performance and works seamlessly with C/C++ and Python Image and AI frameworks. For more information on available operators, API documentation, and getting started guides, refer to our [online documentation][CV-CUDA Documentation]. @@ -36,6 +34,7 @@ For more information on available operators, API documentation, and getting star CV-CUDA Architecture + ## CV-CUDA in Action Fully GPU-accelerated image resizing with [nvImageCodec](https://docs.nvidia.com/cuda/nvimagecodec/index.html) and CV-CUDA. @@ -56,7 +55,7 @@ resized = cvcuda.resize(cvcuda_tensor, (224, 224, 3), cvcuda.Interp.LINEAR) ## Installation CV-CUDA can be installed from pre-built packages (Python wheels, Debian packages, or tar archives) or built from source. -We provide pre-built Python wheels on pypi.org for a variety of Python versions (3.9 to 3.14) and Linux-based platforms (x86_64 and aarch64). See [cvcuda-cu12] and [cvcuda-cu13] for CUDA 12 and CUDA 13, respectively. +We provide pre-built Python wheels on pypi.org for a variety of Python versions (3.10 to 3.14) and Linux-based platforms (x86_64 and aarch64). See [cvcuda-cu12] and [cvcuda-cu13] for CUDA 12 and CUDA 13, respectively. | CUDA Version | Installation Command | |--------------|---------------------| @@ -65,31 +64,38 @@ We provide pre-built Python wheels on pypi.org for a variety of Python versions See [Installation](https://cvcuda.github.io/CV-CUDA/installation.html) for complete installation instructions including building from source, installing Debian packages, and tar archives. +> **Building from source:** CV-CUDA no longer uses git submodules. Build dependencies +> (googletest, nvbench, dlpack, pybind11) are pre-installed in the Docker devel images +> (see [`docker/README.md`](docker/README.md)) and resolved via CMake's `find_package` — running +> `git submodule update --init` is not required and will find nothing. +> To build outside Docker, install these packages through your system package manager or +> CMake's `FetchContent` before running CMake. + ### Compatibility |CV-CUDA Build|Platform|CUDA Version|CUDA Compute Capability|Hardware Architectures|Nvidia Driver|Python Versions|Supported Compilers (build from source and API compatiblity)|API compatibility with prebuilt binaries|OS/Linux distributions tested with prebuilt packages| |-|-|-|-|-|-|-|-|-|-| -|x86_64_cu12|x86_64|≥12.2|≥SM7.5|Turing, Ampere, Ada Lovelace, Hopper, Blackwell|≥r525**|3.9 - 3.14|gcc≥10*|gcc≥10
clang≥11|ManyLinux2014-compliant, Ubuntu≥22.04
WSL2/Ubuntu≥22.04| -|x86_64_cu13|x86_64|≥13.0|≥SM7.5|Turing, Ampere, Ada Lovelace, Hopper, Blackwell|≥r580**|3.9 - 3.14|gcc≥10*|gcc≥10
clang≥11|ManyLinux2014-compliant, Ubuntu≥22.04
WSL2/Ubuntu≥22.04| -|aarch64_cu12|aarch64 SBSA***|≥12.2|≥SM7.5|ARM SBSA (incl. Grace): Volta, Turing, Ampere, Ada Lovelace, Hopper, Blackwell|≥r525**|3.9 - 3.14|gcc≥10*|gcc≥10
clang≥11|ManyLinux2014-compliant, Ubuntu≥22.04| +|x86_64_cu12|x86_64|≥12.2|≥SM7.5|Turing, Ampere, Ada Lovelace, Hopper, Blackwell|≥r525**|3.10 - 3.14|gcc≥10*|gcc≥10
clang≥11|ManyLinux2014-compliant, Ubuntu≥22.04
WSL2/Ubuntu≥22.04| +|x86_64_cu13|x86_64|≥13.0|≥SM7.5|Turing, Ampere, Ada Lovelace, Hopper, Blackwell|≥r580**|3.10 - 3.14|gcc≥10*|gcc≥10
clang≥11|ManyLinux2014-compliant, Ubuntu≥22.04
WSL2/Ubuntu≥22.04| +|aarch64_cu12|aarch64 SBSA***|≥12.2|≥SM7.5|ARM SBSA (incl. Grace): Turing, Ampere, Ada Lovelace, Hopper, Blackwell|≥r525**|3.10 - 3.14|gcc≥10*|gcc≥10
clang≥11|ManyLinux2014-compliant, Ubuntu≥22.04| |aarch64_cu12|aarch64 Jetson***|12.2|≥SM7.5|Jetson AGX Orin, IGX Orin + Ampere RTX6000, IGX Orin + ADA RTX6000|JetPack 6.0 DP, r535 (IGX OS v0.6)|3.10|gcc≥10*|gcc≥10
clang≥11|Jetson Linux 36.2
IGX OS v0.6| -|aarch64_cu13|aarch64 SBSA and Jetson Thor***|≥13.0|≥SM7.5|ARM SBSA (incl. Grace): Volta, Turing, Ampere, Ada Lovelace, Hopper, Blackwell, Jetson Thor|≥r580**|3.9 - 3.14|gcc≥10*|gcc≥10
clang≥11|ManyLinux2014-compliant, Ubuntu≥22.04| +|aarch64_cu13|aarch64 SBSA and Jetson Thor***|≥13.0|≥SM7.5|ARM SBSA (incl. Grace): Turing, Ampere, Ada Lovelace, Hopper, Blackwell, Jetson Thor|≥r580**|3.10 - 3.14|gcc≥10*|gcc≥10
clang≥11|ManyLinux2014-compliant, Ubuntu≥22.04| \* test module with partial coverage, need gcc≥11 for full coverage (see Known Limitations)
-\** [samples][CV-CUDA Samples] require driver ≥r535 to run. CUDA 13 requires ≥r580.
-\*** starting with v0.14, aarch64 packages (deb, tar.xz or wheels) distributed on Github (release "assets") or Pypi are SBSA-compatible unless noted otherwise. Jetson 6 builds (deb, tar.xz, whl) can be found in explicitly named "Jetson" archives in Github release assets. Packages marked 'aarch64_cu13' are built with the unified CUDA toolkit, compatible with both server-class and embedded platforms (Jetson Thor). +\** CUDA 12 x86_64 and aarch64 SBSA packages require driver ≥r525. [Samples][CV-CUDA Samples] require driver ≥r535, Jetson Orin packages follow JetPack 6/r535, and CUDA 13 requires ≥r580.
+\*** starting with v0.14, aarch64 packages (deb, tar.xz or wheels) distributed on Github (release "assets") or Pypi are SBSA-compatible unless noted otherwise. Jetson 6 builds (deb, tar.xz, whl) can be found in explicitly named "Jetson" archives in Github release assets. Packages marked 'aarch64_cu13' are built with the unified CUDA toolkit, compatible with both server-class and embedded platforms (Jetson Thor).
### Known limitations - CV-CUDA does not currently support native Windows, only [WSL2](https://cvcuda.github.io/CV-CUDA/wsl2.html) - Starting with v0.16, CV-CUDA is dropping official support for CUDA 11, SM7 (Volta), Ubuntu 20.04, Python 3.8. +- When building from source for Jetson Orin, use `-DCVCUDA_AARCH64_JETSON=ON` to target only Orin-relevant GPU architectures and reduce build time. - Starting with v0.14, aarch64 packages (deb, tar.xz or wheels) distributed on Github (release "assets") and Pypi are the SBSA-compatible ones. Jetson builds (deb, tar.xz, whl) can be found in explicitly named "Jetson" archives in Github release assets. - The C++ test module builds with gcc≥10 with partial coverage. Full coverage requires gcc≥11 with full C++20 support (NTTP). -- [CV-CUDA Samples] require driver ≥r535 to run and are only officially supported with CUDA 12. +- [CV-CUDA Samples] are only officially supported with CUDA 12. - Only one CUDA version (CUDA 12.x or CUDA 13.x) of CV-CUDA packages (Debian packages, tarballs, Python Wheels) can be installed at a time. Please uninstall all packages from a given CUDA version before installing packages from a different version. -- The Resize and RandomResizedCrop operators incorrectly interpolate pixel values near the boundary of an image or tensor when using cubic interpolation. This will be fixed in an upcoming release. - The OSD operator's text rendering functionality has known issues on Jetson/aarch64 platforms, to be fixed in an upcoming release. ## Contributing diff --git a/THIRD_PARTY_LICENSES.md b/THIRD_PARTY_LICENSES.md deleted file mode 100644 index 2360a7f82..000000000 --- a/THIRD_PARTY_LICENSES.md +++ /dev/null @@ -1,879 +0,0 @@ - -[//]: # "SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved." -[//]: # "SPDX-License-Identifier: Apache-2.0" -[//]: # "" -[//]: # "Licensed 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." - -Third party redistributed software and dependencies -=================================================== - -CV-CUDA redistributes, in its code or binary packages, the following open source software, with licenses included below: -- Pybind11 -- Dlpack -- GoogleTest and GoogleMock -- cuOSD -- nvBench -- OpenSSL - -Additionally, CV-CUDA tests and sample applications depend on third-party open source software that need to be downloaded and installed before use: -- numpy -- cupy-cuda -- matplotlib -- nvimagecodec -- pytest -- TensorRT -- nvJpeg -- torchNvJpeg -- Pytorch -- Torchvision -- onnx -- pyAV -- pyCuda -- nvtx -- tensorflow -- pandas -- pyNvVideoCodec -- VPF -- pyIndex -- PILLOW -- TritonClient -- Tao Converter -- Nsight-Systems - -Review the license terms of these open source projects before use. - -------------------------------------------------------------------------------- -[Pybind11](https://github.com/pybind/pybind11/blob/master/LICENSE) - -Copyright (c) 2016 Wenzel Jakob , All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -3. Neither the name of the copyright holder nor the names of its contributors - may be used to endorse or promote products derived from this software - without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -Please also refer to the file .github/CONTRIBUTING.md, which clarifies licensing of -external contributions to this project including patches, pull requests, etc. - -------------------------------------------------------------------------------- -[Dlpack](https://github.com/dmlc/dlpack/blob/ca4d00ad3e2e0f410eeab3264d21b8a39397f362/LICENSE) - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2017 by Contributors - - Licensed 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. - -------------------------------------------------------------------------------- -[GoogleTest and GoogleMock](https://github.com/google/googletest/blob/5ab508a01f9eb089207ee87fd547d290da39d015/LICENSE) - -Copyright 2008, Google Inc. -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -------------------------------------------------------------------------------- -[cuOSD](https://github.com/NVIDIA-AI-IOT/Lidar_AI_Solution/blob/master/LICENSE.md) - -SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -SPDX-License-Identifier: MIT - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. - - -------------------------------------------------------------------------------- -[nvBench](https://github.com/NVIDIA/nvbench/blob/main/LICENSE) - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed 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. - - ---- LLVM Exceptions to the Apache 2.0 License ---- - -As an exception, if, as a result of your compiling your source code, portions -of this Software are embedded into an Object form of such source code, you -may redistribute such embedded portions in such Object form without complying -with the conditions of Sections 4(a), 4(b) and 4(d) of the License. - -In addition, if you combine or link compiled forms of this Software with -software that is licensed under the GPLv2 ("Combined Software") and if a -court of competent jurisdiction determines that the patent provision (Section -3), the indemnity provision (Section 9) or other Section of the License -conflicts with the conditions of the GPLv2, you may retroactively and -prospectively choose to deem waived or otherwise exclude such Section(s) of -the License, but only in their entirety and only with respect to the Combined -Software. - -------------------------------------------------------------------------------- -[OpenSSL Legacy (pre 3.0.0)](https://github.com/openssl/openssl/blob/OpenSSL_1_0_0-beta1/LICENSE) - -LICENSE ISSUES - ============== - - The OpenSSL toolkit stays under a dual license, i.e. both the conditions of - the OpenSSL License and the original SSLeay license apply to the toolkit. - See below for the actual license texts. Actually both licenses are BSD-style - Open Source licenses. In case of any license issues related to OpenSSL - please contact openssl-core@openssl.org. - - OpenSSL License - --------------- - -/* ==================================================================== - * Copyright (c) 1998-2008 The OpenSSL Project. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in - * the documentation and/or other materials provided with the - * distribution. - * - * 3. All advertising materials mentioning features or use of this - * software must display the following acknowledgment: - * "This product includes software developed by the OpenSSL Project - * for use in the OpenSSL Toolkit. (http://www.openssl.org/)" - * - * 4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to - * endorse or promote products derived from this software without - * prior written permission. For written permission, please contact - * openssl-core@openssl.org. - * - * 5. Products derived from this software may not be called "OpenSSL" - * nor may "OpenSSL" appear in their names without prior written - * permission of the OpenSSL Project. - * - * 6. Redistributions of any form whatsoever must retain the following - * acknowledgment: - * "This product includes software developed by the OpenSSL Project - * for use in the OpenSSL Toolkit (http://www.openssl.org/)" - * - * THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY - * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OpenSSL PROJECT OR - * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) - * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, - * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED - * OF THE POSSIBILITY OF SUCH DAMAGE. - * ==================================================================== - * - * This product includes cryptographic software written by Eric Young - * (eay@cryptsoft.com). This product includes software written by Tim - * Hudson (tjh@cryptsoft.com). - * - */ - - Original SSLeay License - ----------------------- - -/* Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com) - * All rights reserved. - * - * This package is an SSL implementation written - * by Eric Young (eay@cryptsoft.com). - * The implementation was written so as to conform with Netscapes SSL. - * - * This library is free for commercial and non-commercial use as long as - * the following conditions are aheared to. The following conditions - * apply to all code found in this distribution, be it the RC4, RSA, - * lhash, DES, etc., code; not just the SSL code. The SSL documentation - * included with this distribution is covered by the same copyright terms - * except that the holder is Tim Hudson (tjh@cryptsoft.com). - * - * Copyright remains Eric Young's, and as such any Copyright notices in - * the code are not to be removed. - * If this package is used in a product, Eric Young should be given attribution - * as the author of the parts of the library used. - * This can be in the form of a textual message at program startup or - * in documentation (online or textual) provided with the package. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * 3. All advertising materials mentioning features or use of this software - * must display the following acknowledgement: - * "This product includes cryptographic software written by - * Eric Young (eay@cryptsoft.com)" - * The word 'cryptographic' can be left out if the rouines from the library - * being used are not cryptographic related :-). - * 4. If you include any Windows specific code (or a derivative thereof) from - * the apps directory (application code) you must include an acknowledgement: - * "This product includes software written by Tim Hudson (tjh@cryptsoft.com)" - * - * THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE - * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS - * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) - * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT - * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY - * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF - * SUCH DAMAGE. - * - * The licence and distribution terms for any publically available version or - * derivative of this code cannot be changed. i.e. this code cannot simply be - * copied and put under another distribution licence - * [including the GNU Public Licence.] - */ - -------------------------------------------------------------------------------- -[OpenSSL New (>= 3.0.0)](https://github.com/openssl/openssl/blob/master/LICENSE.txt) - - - Apache License - Version 2.0, January 2004 - https://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS diff --git a/bench/BenchAdaptiveThreshold.cpp b/bench/BenchAdaptiveThreshold.cpp deleted file mode 100644 index 41e3a48b6..000000000 --- a/bench/BenchAdaptiveThreshold.cpp +++ /dev/null @@ -1,96 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed 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. - */ - -#include "BenchUtils.hpp" - -#include - -#include - -template -inline void AdaptiveThreshold(nvbench::state &state, nvbench::type_list) -try -{ - long3 shape = benchutils::GetShape<3>(state.get_string("shape")); - long varShape = state.get_int64("varShape"); - int blockSize = static_cast(state.get_int64("blockSize")); - - NVCVThresholdType threshType = NVCV_THRESH_BINARY; - NVCVAdaptiveThresholdType adaptType = NVCV_ADAPTIVE_THRESH_GAUSSIAN_C; - - double maxValue = 123.; - double c = -2.3; - - state.add_global_memory_reads(shape.x * shape.y * shape.z * sizeof(T)); - state.add_global_memory_writes(shape.x * shape.y * shape.z * sizeof(T)); - - cvcuda::AdaptiveThreshold op(blockSize, shape.x); - - // clang-format off - - if (varShape < 0) // negative var shape means use Tensor - { - nvcv::Tensor src({{shape.x, shape.y, shape.z, 1}, "NHWC"}, benchutils::GetDataType()); - nvcv::Tensor dst({{shape.x, shape.y, shape.z, 1}, "NHWC"}, benchutils::GetDataType()); - - benchutils::FillTensor(src, benchutils::RandomValues()); - - state.exec(nvbench::exec_tag::sync, - [&op, &src, &dst, &maxValue, &adaptType, &threshType, &blockSize, &c](nvbench::launch &launch) - { - op(launch.get_stream(), src, dst, maxValue, adaptType, threshType, blockSize, c); - }); - } - else // zero and positive var shape means use ImageBatchVarShape - { - nvcv::ImageBatchVarShape src(shape.x); - nvcv::ImageBatchVarShape dst(shape.x); - - benchutils::FillImageBatch(src, long2{shape.z, shape.y}, long2{varShape, varShape}, - benchutils::RandomValues()); - dst.pushBack(src.begin(), src.end()); - - nvcv::Tensor maxValueTensor({{shape.x}, "N"}, nvcv::TYPE_F64); - nvcv::Tensor blockSizeTensor({{shape.x}, "N"}, nvcv::TYPE_S32); - nvcv::Tensor cTensor({{shape.x}, "N"}, nvcv::TYPE_F64); - - benchutils::FillTensor(maxValueTensor, [&maxValue](const long4_16a &){ return maxValue; }); - benchutils::FillTensor(blockSizeTensor, [&blockSize](const long4_16a &){ return blockSize; }); - benchutils::FillTensor(cTensor, [&c](const long4_16a &){ return c; }); - - state.exec(nvbench::exec_tag::sync, - [&op, &src, &dst, &maxValueTensor, &adaptType, &threshType, &blockSizeTensor, &cTensor] - (nvbench::launch &launch) - { - op(launch.get_stream(), src, dst, maxValueTensor, adaptType, threshType, blockSizeTensor, cTensor); - }); - } -} -catch (const std::exception &err) -{ - state.skip(err.what()); -} - -// clang-format on - -using AdaptiveThresholdTypes = nvbench::type_list; - -NVBENCH_BENCH_TYPES(AdaptiveThreshold, NVBENCH_TYPE_AXES(AdaptiveThresholdTypes)) - .set_type_axes_names({"InOutDataType"}) - .add_string_axis("shape", {"1x1080x1920"}) - .add_int64_axis("varShape", {-1, 0}) - .add_int64_axis("blockSize", {7}); diff --git a/bench/BenchAdvCvtColor.cpp b/bench/BenchAdvCvtColor.cpp deleted file mode 100644 index 04459bdb4..000000000 --- a/bench/BenchAdvCvtColor.cpp +++ /dev/null @@ -1,74 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed 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. - */ - -#include "BenchUtils.hpp" - -#include - -#include - -template -inline void AdvCvtColor(nvbench::state &state, nvbench::type_list) -try -{ - long3 shape = benchutils::GetShape<3>(state.get_string("shape")); - long varShape = state.get_int64("varShape"); - - using BT = typename nvcv::cuda::BaseType; - - int ch = nvcv::cuda::NumElements; - - NVCVColorConversionCode code = NVCV_COLOR_BGR2YUV; - nvcv::ColorSpec colorSpec = NVCV_COLOR_SPEC_BT2020; - - state.add_global_memory_reads(shape.x * shape.y * shape.z * sizeof(T)); - state.add_global_memory_writes(shape.x * shape.y * shape.z * sizeof(T)); - - cvcuda::AdvCvtColor op; - - // clang-format off - - if (varShape < 0) // negative var shape means use Tensor - { - nvcv::Tensor src({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); - nvcv::Tensor dst({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); - - benchutils::FillTensor(src, benchutils::RandomValues()); - - state.exec(nvbench::exec_tag::sync, [&op, &src, &dst, &code, &colorSpec](nvbench::launch &launch) - { - op(launch.get_stream(), src, dst, code, colorSpec); - }); - } - else // zero and positive var shape means use ImageBatchVarShape - { - throw std::invalid_argument("ImageBatchVarShape not implemented for this operator"); - } -} -catch (const std::exception &err) -{ - state.skip(err.what()); -} - -// clang-format on - -using AdvCvtColorTypes = nvbench::type_list; - -NVBENCH_BENCH_TYPES(AdvCvtColor, NVBENCH_TYPE_AXES(AdvCvtColorTypes)) - .set_type_axes_names({"InOutDataType"}) - .add_string_axis("shape", {"1x1080x1920"}) - .add_int64_axis("varShape", {-1}); diff --git a/bench/BenchAverageBlur.cpp b/bench/BenchAverageBlur.cpp deleted file mode 100644 index 60bb5d6c5..000000000 --- a/bench/BenchAverageBlur.cpp +++ /dev/null @@ -1,93 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed 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. - */ - -#include "BenchUtils.hpp" - -#include - -#include - -template -inline void AverageBlur(nvbench::state &state, nvbench::type_list) -try -{ - long3 shape = benchutils::GetShape<3>(state.get_string("shape")); - long varShape = state.get_int64("varShape"); - int2 kernelSize = nvcv::cuda::StaticCast(benchutils::GetShape<2>(state.get_string("kernelSize"))); - - NVCVBorderType borderType = benchutils::GetBorderType(state.get_string("border")); - - nvcv::Size2D kernelSize2d{kernelSize.x, kernelSize.y}; - int2 kernelAnchor{-1, -1}; - - state.add_global_memory_reads(shape.x * shape.y * shape.z * sizeof(T)); - state.add_global_memory_writes(shape.x * shape.y * shape.z * sizeof(T)); - - cvcuda::AverageBlur op(kernelSize2d, shape.x); - - // clang-format off - - if (varShape < 0) // negative var shape means use Tensor - { - nvcv::Tensor src({{shape.x, shape.y, shape.z, 1}, "NHWC"}, benchutils::GetDataType()); - nvcv::Tensor dst({{shape.x, shape.y, shape.z, 1}, "NHWC"}, benchutils::GetDataType()); - - benchutils::FillTensor(src, benchutils::RandomValues()); - - state.exec(nvbench::exec_tag::sync, - [&op, &src, &dst, &kernelSize2d, &kernelAnchor, &borderType](nvbench::launch &launch) - { - op(launch.get_stream(), src, dst, kernelSize2d, kernelAnchor, borderType); - }); - } - else // zero and positive var shape means use ImageBatchVarShape - { - nvcv::ImageBatchVarShape src(shape.x); - nvcv::ImageBatchVarShape dst(shape.x); - - benchutils::FillImageBatch(src, long2{shape.z, shape.y}, long2{varShape, varShape}, - benchutils::RandomValues()); - dst.pushBack(src.begin(), src.end()); - - nvcv::Tensor kernelSizeTensor({{shape.x}, "N"}, nvcv::TYPE_2S32); - nvcv::Tensor kernelAnchorTensor({{shape.x}, "N"}, nvcv::TYPE_2S32); - - benchutils::FillTensor(kernelSizeTensor, [&kernelSize](const long4_16a &){ return kernelSize; }); - benchutils::FillTensor(kernelAnchorTensor, [&kernelAnchor](const long4_16a &){ return kernelAnchor; }); - - state.exec(nvbench::exec_tag::sync, - [&op, &src, &dst, &kernelSizeTensor, &kernelAnchorTensor, &borderType](nvbench::launch &launch) - { - op(launch.get_stream(), src, dst, kernelSizeTensor, kernelAnchorTensor, borderType); - }); - } -} -catch (const std::exception &err) -{ - state.skip(err.what()); -} - -// clang-format on - -using AverageBlurTypes = nvbench::type_list; - -NVBENCH_BENCH_TYPES(AverageBlur, NVBENCH_TYPE_AXES(AverageBlurTypes)) - .set_type_axes_names({"InOutDataType"}) - .add_string_axis("shape", {"1x1080x1920"}) - .add_int64_axis("varShape", {-1, 0}) - .add_string_axis("kernelSize", {"7x7"}) - .add_string_axis("border", {"REPLICATE"}); diff --git a/bench/BenchBilateralFilter.cpp b/bench/BenchBilateralFilter.cpp deleted file mode 100644 index eab57243f..000000000 --- a/bench/BenchBilateralFilter.cpp +++ /dev/null @@ -1,96 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed 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. - */ - -#include "BenchUtils.hpp" - -#include - -#include - -template -inline void BilateralFilter(nvbench::state &state, nvbench::type_list) -try -{ - long3 shape = benchutils::GetShape<3>(state.get_string("shape")); - long varShape = state.get_int64("varShape"); - int diameter = static_cast(state.get_int64("diameter")); - float sigmaSpace = static_cast(state.get_float64("sigmaSpace")); - float sigmaColor = -1.f; - - NVCVBorderType borderType = benchutils::GetBorderType(state.get_string("border")); - - state.add_global_memory_reads(shape.x * shape.y * shape.z * sizeof(T)); - state.add_global_memory_writes(shape.x * shape.y * shape.z * sizeof(T)); - - cvcuda::BilateralFilter op; - - // clang-format off - - if (varShape < 0) // negative var shape means use Tensor - { - nvcv::Tensor src({{shape.x, shape.y, shape.z, 1}, "NHWC"}, benchutils::GetDataType()); - nvcv::Tensor dst({{shape.x, shape.y, shape.z, 1}, "NHWC"}, benchutils::GetDataType()); - - benchutils::FillTensor(src, benchutils::RandomValues()); - - state.exec(nvbench::exec_tag::sync, - [&op, &src, &dst, &diameter, &sigmaColor, &sigmaSpace, &borderType](nvbench::launch &launch) - { - op(launch.get_stream(), src, dst, diameter, sigmaColor, sigmaSpace, borderType); - }); - } - else // zero and positive var shape means use ImageBatchVarShape - { - nvcv::ImageBatchVarShape src(shape.x); - nvcv::ImageBatchVarShape dst(shape.x); - - benchutils::FillImageBatch(src, long2{shape.z, shape.y}, long2{varShape, varShape}, - benchutils::RandomValues()); - dst.pushBack(src.begin(), src.end()); - - nvcv::Tensor diameterTensor({{shape.x}, "N"}, nvcv::TYPE_S32); - nvcv::Tensor sigmaSpaceTensor({{shape.x}, "N"}, nvcv::TYPE_F32); - nvcv::Tensor sigmaColorTensor({{shape.x}, "N"}, nvcv::TYPE_F32); - - benchutils::FillTensor(diameterTensor, [&diameter](auto &){ return diameter; }); - benchutils::FillTensor(sigmaSpaceTensor, [&sigmaSpace](auto &){ return sigmaSpace; }); - benchutils::FillTensor(sigmaColorTensor, [&sigmaColor](auto &){ return sigmaColor; }); - - state.exec(nvbench::exec_tag::sync, - [&op, &src, &dst, &diameterTensor, &sigmaColorTensor, &sigmaSpaceTensor, &borderType] - (nvbench::launch &launch) - { - op(launch.get_stream(), src, dst, diameterTensor, sigmaColorTensor, sigmaSpaceTensor, borderType); - }); - } -} -catch (const std::exception &err) -{ - state.skip(err.what()); -} - -// clang-format on - -using BilateralFilterTypes = nvbench::type_list; - -NVBENCH_BENCH_TYPES(BilateralFilter, NVBENCH_TYPE_AXES(BilateralFilterTypes)) - .set_type_axes_names({"InOutDataType"}) - .add_string_axis("shape", {"1x1080x1920"}) - .add_int64_axis("varShape", {-1, 0}) - .add_int64_axis("diameter", {-1}) - .add_float64_axis("sigmaSpace", {1.2}) - .add_string_axis("border", {"REFLECT"}); diff --git a/bench/BenchBndBox.cpp b/bench/BenchBndBox.cpp deleted file mode 100644 index 9f714e0a1..000000000 --- a/bench/BenchBndBox.cpp +++ /dev/null @@ -1,97 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed 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. - */ - -#include "BenchUtils.hpp" - -#include <../priv/Types.hpp> -#include - -#include - -template -inline void BndBox(nvbench::state &state, nvbench::type_list) -try -{ - long3 shape = benchutils::GetShape<3>(state.get_string("shape")); - long varShape = state.get_int64("varShape"); - int numBoxes = static_cast(state.get_int64("numBoxes")); - - using BT = typename nvcv::cuda::BaseType; - - int ch = nvcv::cuda::NumElements; - - NVCVBndBoxI bndBox{ - {43, 21, 12, 34}, // box x, y position w, h size - 2, // box thickness - { 0, 0, 0, 255}, // box border color - { 0, 0, 0, 0} // box fill color - }; - - std::vector> bndBoxesVec; - - for (int i = 0; i < shape.x; i++) - { - std::vector curVec; - for (int j = 0; j < numBoxes; j++) - { - curVec.push_back(bndBox); - } - bndBoxesVec.push_back(curVec); - } - - std::shared_ptr bndBoxesImpl - = std::make_shared(bndBoxesVec); - NVCVBndBoxesI bndBoxes = (NVCVBndBoxesI)bndBoxesImpl.get(); - - state.add_global_memory_reads(shape.x * shape.y * shape.z * sizeof(T) + shape.x * numBoxes * sizeof(NVCVBndBoxI)); - state.add_global_memory_writes(shape.x * shape.y * shape.z * sizeof(T)); - - cvcuda::BndBox op; - - // clang-format off - - if (varShape < 0) // negative var shape means use Tensor - { - nvcv::Tensor src({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); - nvcv::Tensor dst({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); - - benchutils::FillTensor(src, benchutils::RandomValues()); - - state.exec(nvbench::exec_tag::sync, [&op, &src, &dst, &bndBoxes](nvbench::launch &launch) - { - op(launch.get_stream(), src, dst, bndBoxes); - }); - } - else // zero and positive var shape means use ImageBatchVarShape - { - throw std::invalid_argument("ImageBatchVarShape not implemented for this operator"); - } -} -catch (const std::exception &err) -{ - state.skip(err.what()); -} - -// clang-format on - -using BndBoxTypes = nvbench::type_list; - -NVBENCH_BENCH_TYPES(BndBox, NVBENCH_TYPE_AXES(BndBoxTypes)) - .set_type_axes_names({"InOutDataType"}) - .add_string_axis("shape", {"1x1080x1920"}) - .add_int64_axis("varShape", {-1}) - .add_int64_axis("numBoxes", {10, 100}); diff --git a/bench/BenchBoxBlur.cpp b/bench/BenchBoxBlur.cpp deleted file mode 100644 index 031d74f99..000000000 --- a/bench/BenchBoxBlur.cpp +++ /dev/null @@ -1,97 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed 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. - */ - -#include "BenchUtils.hpp" - -#include <../priv/Types.hpp> -#include - -#include - -template -inline void BoxBlur(nvbench::state &state, nvbench::type_list) -try -{ - long3 shape = benchutils::GetShape<3>(state.get_string("shape")); - long varShape = state.get_int64("varShape"); - int numBoxes = static_cast(state.get_int64("numBoxes")); - int kernelSize = static_cast(state.get_int64("kernelSize")); - - using BT = typename nvcv::cuda::BaseType; - - int ch = nvcv::cuda::NumElements; - - NVCVBlurBoxI blurBox{ - {43, 21, 12, 34}, // box x, y position w, h size - kernelSize // median filter kernel size - }; - - std::vector> blurBoxesVec; - - for (int i = 0; i < shape.x; i++) - { - std::vector curVec; - for (int j = 0; j < numBoxes; j++) - { - curVec.push_back(blurBox); - } - blurBoxesVec.push_back(curVec); - } - - std::shared_ptr blurBoxesImpl - = std::make_shared(blurBoxesVec); - NVCVBlurBoxesI blurBoxes = (NVCVBlurBoxesI)blurBoxesImpl.get(); - - state.add_global_memory_reads(shape.x * shape.y * shape.z * sizeof(T) + shape.x * numBoxes * sizeof(NVCVBlurBoxI)); - state.add_global_memory_writes(shape.x * shape.y * shape.z * sizeof(T)); - - cvcuda::BoxBlur op; - - // clang-format off - - if (varShape < 0) // negative var shape means use Tensor - { - nvcv::Tensor src({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); - nvcv::Tensor dst({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); - - benchutils::FillTensor(src, benchutils::RandomValues()); - - state.exec(nvbench::exec_tag::sync, [&op, &src, &dst, &blurBoxes](nvbench::launch &launch) - { - op(launch.get_stream(), src, dst, blurBoxes); - }); - } - else // zero and positive var shape means use ImageBatchVarShape - { - throw std::invalid_argument("ImageBatchVarShape not implemented for this operator"); - } -} -catch (const std::exception &err) -{ - state.skip(err.what()); -} - -// clang-format on - -using BoxBlurTypes = nvbench::type_list; - -NVBENCH_BENCH_TYPES(BoxBlur, NVBENCH_TYPE_AXES(BoxBlurTypes)) - .set_type_axes_names({"InOutDataType"}) - .add_string_axis("shape", {"1x1080x1920"}) - .add_int64_axis("varShape", {-1}) - .add_int64_axis("numBoxes", {4}) - .add_int64_axis("kernelSize", {5}); diff --git a/bench/BenchBrightnessContrast.cpp b/bench/BenchBrightnessContrast.cpp deleted file mode 100644 index 69367649b..000000000 --- a/bench/BenchBrightnessContrast.cpp +++ /dev/null @@ -1,91 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed 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. - */ - -#include "BenchUtils.hpp" - -#include - -#include - -template -inline void BrightnessContrast(nvbench::state &state, nvbench::type_list) -try -{ - long3 shape = benchutils::GetShape<3>(state.get_string("shape")); - long varShape = state.get_int64("varShape"); - - state.add_global_memory_reads(shape.x * shape.y * shape.z * sizeof(T) + shape.x * sizeof(float) * 4); - state.add_global_memory_writes(shape.x * shape.y * shape.z * sizeof(T)); - - cvcuda::BrightnessContrast op; - - // clang-format off - - nvcv::Tensor brightness({{shape.x}, "N"}, nvcv::TYPE_F32); - nvcv::Tensor contrast({{shape.x}, "N"}, nvcv::TYPE_F32); - nvcv::Tensor brightnessShift({{shape.x}, "N"}, nvcv::TYPE_F32); - nvcv::Tensor contrastCenter({{shape.x}, "N"}, nvcv::TYPE_F32); - - benchutils::FillTensor(brightness, benchutils::RandomValues(0.f, 1.f)); - benchutils::FillTensor(contrast, benchutils::RandomValues()); - benchutils::FillTensor(brightnessShift, benchutils::RandomValues()); - benchutils::FillTensor(contrastCenter, benchutils::RandomValues()); - - if (varShape < 0) // negative var shape means use Tensor - { - nvcv::Tensor src({{shape.x, shape.y, shape.z, 1}, "NHWC"}, benchutils::GetDataType()); - nvcv::Tensor dst({{shape.x, shape.y, shape.z, 1}, "NHWC"}, benchutils::GetDataType()); - - benchutils::FillTensor(src, benchutils::RandomValues()); - - state.exec(nvbench::exec_tag::sync, - [&op, &src, &dst, &brightness, &contrast, &brightnessShift, &contrastCenter] - (nvbench::launch &launch) - { - op(launch.get_stream(), src, dst, brightness, contrast, brightnessShift, contrastCenter); - }); - } - else // zero and positive var shape means use ImageBatchVarShape - { - nvcv::ImageBatchVarShape src(shape.x); - nvcv::ImageBatchVarShape dst(shape.x); - - benchutils::FillImageBatch(src, long2{shape.z, shape.y}, long2{varShape, varShape}, - benchutils::RandomValues()); - dst.pushBack(src.begin(), src.end()); - - state.exec(nvbench::exec_tag::sync, - [&op, &src, &dst, &brightness, &contrast, &brightnessShift, &contrastCenter] - (nvbench::launch &launch) - { - op(launch.get_stream(), src, dst, brightness, contrast, brightnessShift, contrastCenter); - }); - } -} -catch (const std::exception &err) -{ - state.skip(err.what()); -} - -// clang-format on - -using BrightnessContrastTypes = nvbench::type_list; - -NVBENCH_BENCH_TYPES(BrightnessContrast, NVBENCH_TYPE_AXES(BrightnessContrastTypes)) - .set_type_axes_names({"InOutDataType"}) - .add_string_axis("shape", {"1x1080x1920"}) - .add_int64_axis("varShape", {-1, 0}); diff --git a/bench/BenchCenterCrop.cpp b/bench/BenchCenterCrop.cpp deleted file mode 100644 index 3ebe7d137..000000000 --- a/bench/BenchCenterCrop.cpp +++ /dev/null @@ -1,85 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed 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. - */ - -#include "BenchUtils.hpp" - -#include - -#include - -template -inline void CenterCrop(nvbench::state &state, nvbench::type_list) -try -{ - long3 srcShape = benchutils::GetShape<3>(state.get_string("shape")); - long varShape = state.get_int64("varShape"); - - nvcv::Size2D cropSize; - - if (state.get_string("cropType") == "SAME") - { - cropSize = nvcv::Size2D{(int)srcShape.z, (int)srcShape.y}; - } - else if (state.get_string("cropType") == "QUARTER") - { - cropSize = nvcv::Size2D{(int)srcShape.z / 2, (int)srcShape.y / 2}; - } - else - { - throw std::invalid_argument("Invalid resizeType = " + state.get_string("resizeType")); - } - - long3 dstShape{srcShape.x, cropSize.h, cropSize.w}; - - state.add_global_memory_reads(dstShape.x * dstShape.y * dstShape.z * sizeof(T)); - state.add_global_memory_writes(dstShape.x * dstShape.y * dstShape.z * sizeof(T)); - - cvcuda::CenterCrop op; - - // clang-format off - - if (varShape < 0) // negative var shape means use Tensor - { - nvcv::Tensor src({{srcShape.x, srcShape.y, srcShape.z, 1}, "NHWC"}, benchutils::GetDataType()); - nvcv::Tensor dst({{dstShape.x, dstShape.y, dstShape.z, 1}, "NHWC"}, benchutils::GetDataType()); - - benchutils::FillTensor(src, benchutils::RandomValues()); - - state.exec(nvbench::exec_tag::sync, [&op, &src, &dst, &cropSize](nvbench::launch &launch) - { - op(launch.get_stream(), src, dst, cropSize); - }); - } - else // zero and positive var shape means use ImageBatchVarShape - { - throw std::invalid_argument("ImageBatchVarShape not implemented for this operator"); - } -} -catch (const std::exception &err) -{ - state.skip(err.what()); -} - -// clang-format on - -using CenterCropTypes = nvbench::type_list; - -NVBENCH_BENCH_TYPES(CenterCrop, NVBENCH_TYPE_AXES(CenterCropTypes)) - .set_type_axes_names({"InOutDataType"}) - .add_string_axis("shape", {"1x1080x1920"}) - .add_int64_axis("varShape", {-1}) - .add_string_axis("cropType", {"QUARTER"}); diff --git a/bench/BenchChannelReorder.cpp b/bench/BenchChannelReorder.cpp deleted file mode 100644 index fed1a6007..000000000 --- a/bench/BenchChannelReorder.cpp +++ /dev/null @@ -1,73 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed 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. - */ - -#include "BenchUtils.hpp" - -#include - -#include - -template -inline void ChannelReorder(nvbench::state &state, nvbench::type_list) -try -{ - long3 shape = benchutils::GetShape<3>(state.get_string("shape")); - long varShape = state.get_int64("varShape"); - - state.add_global_memory_reads(shape.x * shape.y * shape.z * sizeof(T)); - state.add_global_memory_writes(shape.x * shape.y * shape.z * sizeof(T)); - - cvcuda::ChannelReorder op; - - // clang-format off - - nvcv::Tensor orders({{shape.x, 4}, "NC"}, nvcv::TYPE_S32); - - benchutils::FillTensor(orders, benchutils::RandomValues(0, nvcv::cuda::NumElements)); - - if (varShape < 0) // negative var shape means use Tensor - { - throw std::invalid_argument("Tensor not implemented for this operator"); - } - else // zero and positive var shape means use ImageBatchVarShape - { - nvcv::ImageBatchVarShape src(shape.x); - nvcv::ImageBatchVarShape dst(shape.x); - - benchutils::FillImageBatch(src, long2{shape.z, shape.y}, long2{varShape, varShape}, - benchutils::RandomValues()); - dst.pushBack(src.begin(), src.end()); - - state.exec(nvbench::exec_tag::sync, [&op, &src, &dst, &orders](nvbench::launch &launch) - { - op(launch.get_stream(), src, dst, orders); - }); - } -} -catch (const std::exception &err) -{ - state.skip(err.what()); -} - -// clang-format on - -using ChannelReorderTypes = nvbench::type_list; - -NVBENCH_BENCH_TYPES(ChannelReorder, NVBENCH_TYPE_AXES(ChannelReorderTypes)) - .set_type_axes_names({"InOutDataType"}) - .add_string_axis("shape", {"1x1080x1920"}) - .add_int64_axis("varShape", {0}); diff --git a/bench/BenchColorTwist.cpp b/bench/BenchColorTwist.cpp deleted file mode 100644 index 8ce4d139a..000000000 --- a/bench/BenchColorTwist.cpp +++ /dev/null @@ -1,85 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed 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. - */ - -#include "BenchUtils.hpp" - -#include - -#include - -template -inline void ColorTwist(nvbench::state &state, nvbench::type_list) -try -{ - long3 shape = benchutils::GetShape<3>(state.get_string("shape")); - long varShape = state.get_int64("varShape"); - - using BT = typename nvcv::cuda::BaseType; - - int ch = nvcv::cuda::NumElements; - - state.add_global_memory_reads(shape.x * shape.y * shape.z * sizeof(T)); - state.add_global_memory_writes(shape.x * shape.y * shape.z * sizeof(T)); - - cvcuda::ColorTwist op; - - // clang-format off - - nvcv::Tensor twist({{shape.x, 3}, "NH"}, nvcv::TYPE_4F32); - - benchutils::FillTensor(twist, benchutils::RandomValues()); - - if (varShape < 0) // negative var shape means use Tensor - { - nvcv::Tensor src({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); - nvcv::Tensor dst({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); - - benchutils::FillTensor(src, benchutils::RandomValues()); - - state.exec(nvbench::exec_tag::sync, [&op, &src, &dst, &twist](nvbench::launch &launch) - { - op(launch.get_stream(), src, dst, twist); - }); - } - else // zero and positive var shape means use ImageBatchVarShape - { - nvcv::ImageBatchVarShape src(shape.x); - nvcv::ImageBatchVarShape dst(shape.x); - - benchutils::FillImageBatch(src, long2{shape.z, shape.y}, long2{varShape, varShape}, - benchutils::RandomValues()); - dst.pushBack(src.begin(), src.end()); - - state.exec(nvbench::exec_tag::sync, [&op, &src, &dst, &twist](nvbench::launch &launch) - { - op(launch.get_stream(), src, dst, twist); - }); - } -} -catch (const std::exception &err) -{ - state.skip(err.what()); -} - -// clang-format on - -using ColorTwistTypes = nvbench::type_list; - -NVBENCH_BENCH_TYPES(ColorTwist, NVBENCH_TYPE_AXES(ColorTwistTypes)) - .set_type_axes_names({"InOutDataType"}) - .add_string_axis("shape", {"1x1080x1920"}) - .add_int64_axis("varShape", {-1, 0}); diff --git a/bench/BenchComposite.cpp b/bench/BenchComposite.cpp deleted file mode 100644 index f83f08a59..000000000 --- a/bench/BenchComposite.cpp +++ /dev/null @@ -1,91 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed 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. - */ - -#include "BenchUtils.hpp" - -#include - -#include - -template -inline void Composite(nvbench::state &state, nvbench::type_list) -try -{ - long3 shape = benchutils::GetShape<3>(state.get_string("shape")); - long varShape = state.get_int64("varShape"); - - using BT = typename nvcv::cuda::BaseType; - - int ch = nvcv::cuda::NumElements; - - state.add_global_memory_reads(shape.x * shape.y * shape.z * (sizeof(T) * 2 + sizeof(M))); - state.add_global_memory_writes(shape.x * shape.y * shape.z * sizeof(T)); - - cvcuda::Composite op; - - // clang-format off - - if (varShape < 0) // negative var shape means use Tensor - { - nvcv::Tensor fg({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); - nvcv::Tensor bg({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); - nvcv::Tensor mask({{shape.x, shape.y, shape.z, 1}, "NHWC"}, benchutils::GetDataType()); - nvcv::Tensor dst({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); - - benchutils::FillTensor(fg, benchutils::RandomValues()); - benchutils::FillTensor(bg, benchutils::RandomValues()); - benchutils::FillTensor(mask, [](const long4_16a &){ return 1; }); - - state.exec(nvbench::exec_tag::sync, [&op, &fg, &bg, &mask, &dst](nvbench::launch &launch) - { - op(launch.get_stream(), fg, bg, mask, dst); - }); - } - else // zero and positive var shape means use ImageBatchVarShape - { - nvcv::ImageBatchVarShape fg(shape.x); - nvcv::ImageBatchVarShape bg(shape.x); - nvcv::ImageBatchVarShape mask(shape.x); - nvcv::ImageBatchVarShape dst(shape.x); - - benchutils::FillImageBatch(fg, long2{shape.z, shape.y}, long2{varShape, varShape}, - benchutils::RandomValues()); - bg.pushBack(fg.begin(), fg.end()); - dst.pushBack(fg.begin(), fg.end()); - - benchutils::FillImageBatch(mask, long2{shape.z, shape.y}, long2{varShape, varShape}, - [](const long4_16a &){ return 1; }); - - state.exec(nvbench::exec_tag::sync, [&op, &fg, &bg, &mask, &dst](nvbench::launch &launch) - { - op(launch.get_stream(), fg, bg, mask, dst); - }); - } -} -catch (const std::exception &err) -{ - state.skip(err.what()); -} - -// clang-format on - -using CompositeTypes = nvbench::type_list; - -NVBENCH_BENCH_TYPES(Composite, NVBENCH_TYPE_AXES(CompositeTypes)) - .set_type_axes_names({"InOutDataType"}) - .add_string_axis("shape", {"1x1080x1920"}) - .add_int64_axis("varShape", {-1, 0}); diff --git a/bench/BenchConv2D.cpp b/bench/BenchConv2D.cpp deleted file mode 100644 index 40ec611ce..000000000 --- a/bench/BenchConv2D.cpp +++ /dev/null @@ -1,83 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed 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. - */ - -#include "BenchUtils.hpp" - -#include - -#include - -template -inline void Conv2D(nvbench::state &state, nvbench::type_list) -try -{ - long3 shape = benchutils::GetShape<3>(state.get_string("shape")); - long varShape = state.get_int64("varShape"); - int2 kernelSize = nvcv::cuda::StaticCast(benchutils::GetShape<2>(state.get_string("kernelSize"))); - - NVCVBorderType borderType = benchutils::GetBorderType(state.get_string("border")); - - state.add_global_memory_reads(shape.x * shape.y * shape.z * sizeof(T)); - state.add_global_memory_writes(shape.x * shape.y * shape.z * sizeof(T)); - - cvcuda::Conv2D op; - - // clang-format off - - nvcv::Tensor kernelAnchor({{shape.x}, "N"}, nvcv::TYPE_2S32); - - benchutils::FillTensor(kernelAnchor, [](auto &){ return int2{-1, -1}; }); - - if (varShape < 0) // negative var shape means use Tensor - { - throw std::invalid_argument("Tensor not implemented for this operator"); - } - else // zero and positive var shape means use ImageBatchVarShape - { - nvcv::ImageBatchVarShape src(shape.x); - nvcv::ImageBatchVarShape dst(shape.x); - nvcv::ImageBatchVarShape kernel(shape.x); - - benchutils::FillImageBatch(src, long2{shape.z, shape.y}, long2{varShape, varShape}, - benchutils::RandomValues()); - dst.pushBack(src.begin(), src.end()); - - benchutils::FillImageBatch(kernel, long2{kernelSize.x, kernelSize.y}, long2{0, 0}, - benchutils::RandomValues(0.f, 1.f)); - - state.exec(nvbench::exec_tag::sync, - [&op, &src, &dst, &kernel, &kernelAnchor, &borderType](nvbench::launch &launch) - { - op(launch.get_stream(), src, dst, kernel, kernelAnchor, borderType); - }); - } -} -catch (const std::exception &err) -{ - state.skip(err.what()); -} - -// clang-format on - -using Conv2DTypes = nvbench::type_list; - -NVBENCH_BENCH_TYPES(Conv2D, NVBENCH_TYPE_AXES(Conv2DTypes)) - .set_type_axes_names({"InOutDataType"}) - .add_string_axis("shape", {"1x1080x1920"}) - .add_int64_axis("varShape", {0}) - .add_string_axis("kernelSize", {"7x7"}) - .add_string_axis("border", {"REPLICATE"}); diff --git a/bench/BenchConvertTo.cpp b/bench/BenchConvertTo.cpp deleted file mode 100644 index 2bbd74a72..000000000 --- a/bench/BenchConvertTo.cpp +++ /dev/null @@ -1,70 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed 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. - */ - -#include "BenchUtils.hpp" - -#include - -#include - -template -inline void ConvertTo(nvbench::state &state, nvbench::type_list) -try -{ - long3 shape = benchutils::GetShape<3>(state.get_string("shape")); - long varShape = state.get_int64("varShape"); - - double alpha = 0.123; - double beta = 0.456; - - state.add_global_memory_reads(shape.x * shape.y * shape.z * sizeof(T)); - state.add_global_memory_writes(shape.x * shape.y * shape.z * sizeof(T)); - - cvcuda::ConvertTo op; - - // clang-format off - - if (varShape < 0) // negative var shape means use Tensor - { - nvcv::Tensor src({{shape.x, shape.y, shape.z, 1}, "NHWC"}, benchutils::GetDataType()); - nvcv::Tensor dst({{shape.x, shape.y, shape.z, 1}, "NHWC"}, benchutils::GetDataType()); - - benchutils::FillTensor(src, benchutils::RandomValues()); - - state.exec(nvbench::exec_tag::sync, [&op, &src, &dst, &alpha, &beta](nvbench::launch &launch) - { - op(launch.get_stream(), src, dst, alpha, beta); - }); - } - else // zero and positive var shape means use ImageBatchVarShape - { - throw std::invalid_argument("ImageBatchVarShape not implemented for this operator"); - } -} -catch (const std::exception &err) -{ - state.skip(err.what()); -} - -// clang-format on - -using ConvertToTypes = nvbench::type_list; - -NVBENCH_BENCH_TYPES(ConvertTo, NVBENCH_TYPE_AXES(ConvertToTypes)) - .set_type_axes_names({"InOutDataType"}) - .add_string_axis("shape", {"1x1080x1920"}) - .add_int64_axis("varShape", {-1}); diff --git a/bench/BenchCopyMakeBorder.cpp b/bench/BenchCopyMakeBorder.cpp deleted file mode 100644 index 88138d95c..000000000 --- a/bench/BenchCopyMakeBorder.cpp +++ /dev/null @@ -1,96 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed 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. - */ - -#include "BenchUtils.hpp" - -#include - -#include - -template -inline void CopyMakeBorder(nvbench::state &state, nvbench::type_list) -try -{ - long3 srcShape = benchutils::GetShape<3>(state.get_string("shape")); - long varShape = state.get_int64("varShape"); - - NVCVBorderType borderType = benchutils::GetBorderType(state.get_string("border")); - - float4 borderValue{0.f, 0.f, 0.f, 0.f}; - - int top = srcShape.y / 2; - int left = srcShape.z / 2; - - long3 dstShape{srcShape.x, top + srcShape.y, left + srcShape.z}; - - state.add_global_memory_reads(srcShape.x * srcShape.y * srcShape.z * sizeof(T)); - state.add_global_memory_writes(dstShape.x * dstShape.y * dstShape.z * sizeof(T)); - - cvcuda::CopyMakeBorder op; - - // clang-format off - - if (varShape < 0) // negative var shape means use Tensor - { - nvcv::Tensor src({{srcShape.x, srcShape.y, srcShape.z, 1}, "NHWC"}, benchutils::GetDataType()); - nvcv::Tensor dst({{dstShape.x, dstShape.y, dstShape.z, 1}, "NHWC"}, benchutils::GetDataType()); - - benchutils::FillTensor(src, benchutils::RandomValues()); - - state.exec(nvbench::exec_tag::sync, - [&op, &src, &dst, &top, &left, &borderType, &borderValue](nvbench::launch &launch) - { - op(launch.get_stream(), src, dst, top, left, borderType, borderValue); - }); - } - else // zero and positive var shape means use ImageBatchVarShape - { - nvcv::ImageBatchVarShape src(srcShape.x); - nvcv::ImageBatchVarShape dst(dstShape.x); - - benchutils::FillImageBatch(src, long2{srcShape.z, srcShape.y}, long2{varShape, varShape}, - benchutils::RandomValues()); - benchutils::FillImageBatch(dst, long2{dstShape.z, dstShape.y}, long2{varShape, varShape}, - benchutils::RandomValues()); - - nvcv::Tensor topTensor({{srcShape.x, 1, 1, 1}, "NHWC"}, nvcv::TYPE_S32); - nvcv::Tensor leftTensor({{srcShape.x, 1, 1, 1}, "NHWC"}, nvcv::TYPE_S32); - - benchutils::FillTensor(topTensor, [&top](const long4_16a &){ return top; }); - benchutils::FillTensor(leftTensor, [&left](const long4_16a &){ return left; }); - - state.exec(nvbench::exec_tag::sync, - [&op, &src, &dst, &topTensor, &leftTensor, &borderType, &borderValue](nvbench::launch &launch) - { - op(launch.get_stream(), src, dst, topTensor, leftTensor, borderType, borderValue); - }); - } -} -catch (const std::exception &err) -{ - state.skip(err.what()); -} - -// clang-format on - -using CopyMakeBorderTypes = nvbench::type_list; - -NVBENCH_BENCH_TYPES(CopyMakeBorder, NVBENCH_TYPE_AXES(CopyMakeBorderTypes)) - .set_type_axes_names({"InOutDataType"}) - .add_string_axis("shape", {"1x1080x1920"}) - .add_int64_axis("varShape", {-1, 0}) - .add_string_axis("border", {"REFLECT101"}); diff --git a/bench/BenchCropFlipNormalizeReformat.cpp b/bench/BenchCropFlipNormalizeReformat.cpp deleted file mode 100644 index b5ca6c83d..000000000 --- a/bench/BenchCropFlipNormalizeReformat.cpp +++ /dev/null @@ -1,116 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed 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. - */ - -#include "BenchUtils.hpp" - -#include - -#include - -template -inline void CropFlipNormalizeReformat(nvbench::state &state, nvbench::type_list) -try -{ - long3 srcShape = benchutils::GetShape<3>(state.get_string("shape")); - long varShape = state.get_int64("varShape"); - long3 dstShape = srcShape; - - NVCVBorderType borderType = benchutils::GetBorderType(state.get_string("border")); - - float borderValue{0.f}; - - float globalScale = 1.234f; - float globalShift = 2.345f; - float epsilon = 12.34f; - uint32_t flags = 0; - - long3 baseShape{srcShape.x, 1, 1}; - long3 scaleShape{srcShape.x, 1, 1}; - long3 cropShape{srcShape.x, 1, 1}; - - state.add_global_memory_reads(srcShape.x * srcShape.y * srcShape.z * sizeof(T) - + baseShape.x * baseShape.y * baseShape.z * sizeof(float) - + scaleShape.x * scaleShape.y * scaleShape.z * sizeof(float) - + cropShape.x * cropShape.y * cropShape.z * sizeof(int) * 4); - state.add_global_memory_writes(dstShape.x * dstShape.y * dstShape.z * sizeof(T)); - - cvcuda::CropFlipNormalizeReformat op; - - // clang-format off - - nvcv::Tensor dst({{dstShape.x, dstShape.y, dstShape.z, 1}, "NHWC"}, benchutils::GetDataType()); - - nvcv::Tensor flipCode({{srcShape.x}, "N"}, nvcv::TYPE_S32); - - nvcv::Tensor base({{baseShape.x, baseShape.y, baseShape.z, 1}, "NHWC"}, nvcv::TYPE_F32); - nvcv::Tensor scale({{scaleShape.x, scaleShape.y, scaleShape.z, 1}, "NHWC"}, nvcv::TYPE_F32); - - nvcv::Tensor crop({{cropShape.x, cropShape.y, cropShape.z, 4}, "NHWC"}, nvcv::TYPE_S32); - - benchutils::FillTensor(flipCode, [](auto &){ return -1; }); - - benchutils::FillTensor(base, benchutils::RandomValues()); - benchutils::FillTensor(scale, benchutils::RandomValues(0.f, 1.f)); - - // Always crop entire source image for easy bandwidth calculations - benchutils::FillTensor(crop, [&srcShape](const long4_16a &c) - { - if (c.w == 2) - { - return (int)srcShape.z; - } - else if (c.w == 3) - { - return (int)srcShape.y; - } - return 0; - }); - - if (varShape < 0) // negative var shape means use Tensor - { - throw std::invalid_argument("Tensor not implemented for this operator"); - } - else // zero and positive var shape means use ImageBatchVarShape - { - nvcv::ImageBatchVarShape src(srcShape.x); - - benchutils::FillImageBatch(src, long2{srcShape.z, srcShape.y}, long2{varShape, varShape}, - benchutils::RandomValues()); - - state.exec(nvbench::exec_tag::sync, - [&op, &src, &dst, &crop, &borderType, &borderValue, &flipCode, &base, &scale, &globalScale, - &globalShift, &epsilon, &flags](nvbench::launch &launch) - { - op(launch.get_stream(), src, dst, crop, borderType, borderValue, flipCode, base, scale, globalScale, - globalShift, epsilon, flags); - }); - } -} -catch (const std::exception &err) -{ - state.skip(err.what()); -} - -// clang-format on - -using CropFlipNormalizeReformatTypes = nvbench::type_list; - -NVBENCH_BENCH_TYPES(CropFlipNormalizeReformat, NVBENCH_TYPE_AXES(CropFlipNormalizeReformatTypes)) - .set_type_axes_names({"InOutDataType"}) - .add_string_axis("shape", {"1x1080x1920"}) - .add_int64_axis("varShape", {0}) - .add_string_axis("border", {"REFLECT101"}); diff --git a/bench/BenchCustomCrop.cpp b/bench/BenchCustomCrop.cpp deleted file mode 100644 index 07478a393..000000000 --- a/bench/BenchCustomCrop.cpp +++ /dev/null @@ -1,70 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed 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. - */ - -#include "BenchUtils.hpp" - -#include - -#include - -template -inline void CustomCrop(nvbench::state &state, nvbench::type_list) -try -{ - long3 shape = benchutils::GetShape<3>(state.get_string("shape")); - long varShape = state.get_int64("varShape"); - - // Always crop entire source image for easy bandwidth calculations - NVCVRectI cropRect{0, 0, (int)shape.z, (int)shape.y}; - - state.add_global_memory_reads(shape.x * shape.y * shape.z * sizeof(T)); - state.add_global_memory_writes(shape.x * shape.y * shape.z * sizeof(T)); - - cvcuda::CustomCrop op; - - // clang-format off - - if (varShape < 0) // negative var shape means use Tensor - { - nvcv::Tensor src({{shape.x, shape.y, shape.z, 1}, "NHWC"}, benchutils::GetDataType()); - nvcv::Tensor dst({{shape.x, shape.y, shape.z, 1}, "NHWC"}, benchutils::GetDataType()); - - benchutils::FillTensor(src, benchutils::RandomValues()); - - state.exec(nvbench::exec_tag::sync, [&op, &src, &dst, &cropRect](nvbench::launch &launch) - { - op(launch.get_stream(), src, dst, cropRect); - }); - } - else // zero and positive var shape means use ImageBatchVarShape - { - throw std::invalid_argument("ImageBatchVarShape not implemented for this operator"); - } -} -catch (const std::exception &err) -{ - state.skip(err.what()); -} - -// clang-format on - -using CustomCropTypes = nvbench::type_list; - -NVBENCH_BENCH_TYPES(CustomCrop, NVBENCH_TYPE_AXES(CustomCropTypes)) - .set_type_axes_names({"InOutDataType"}) - .add_string_axis("shape", {"1x1080x1920"}) - .add_int64_axis("varShape", {-1}); diff --git a/bench/BenchCvtColor.cpp b/bench/BenchCvtColor.cpp deleted file mode 100644 index de110a618..000000000 --- a/bench/BenchCvtColor.cpp +++ /dev/null @@ -1,183 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed 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. - */ - -#include "BenchUtils.hpp" - -#include - -#include - -#include -#include -#include - -using ConvCodeToFormat = std::tuple; -using CodeMap = std::map; - -inline static ConvCodeToFormat str2Frmt(const std::string &str) -{ - // clang-format off - static const CodeMap codeMap { - { "RGB2BGR", {NVCV_COLOR_RGB2BGR, NVCV_IMAGE_FORMAT_RGB8, NVCV_IMAGE_FORMAT_BGR8 }}, - { "RGB2RGBA", {NVCV_COLOR_RGB2RGBA, NVCV_IMAGE_FORMAT_RGB8, NVCV_IMAGE_FORMAT_RGBA8}}, - { "RGBA2RGB", {NVCV_COLOR_RGBA2RGB, NVCV_IMAGE_FORMAT_RGBA8, NVCV_IMAGE_FORMAT_RGB8 }}, - { "RGB2GRAY", {NVCV_COLOR_RGB2GRAY, NVCV_IMAGE_FORMAT_RGB8, NVCV_IMAGE_FORMAT_Y8 }}, - { "GRAY2RGB", {NVCV_COLOR_GRAY2RGB, NVCV_IMAGE_FORMAT_Y8, NVCV_IMAGE_FORMAT_RGB8 }}, - { "RGB2HSV", {NVCV_COLOR_RGB2HSV, NVCV_IMAGE_FORMAT_RGB8, NVCV_IMAGE_FORMAT_HSV8 }}, - { "HSV2RGB", {NVCV_COLOR_HSV2RGB, NVCV_IMAGE_FORMAT_HSV8, NVCV_IMAGE_FORMAT_RGB8 }}, - { "RGB2YUV", {NVCV_COLOR_RGB2YUV, NVCV_IMAGE_FORMAT_RGB8, NVCV_IMAGE_FORMAT_YUV8 }}, - { "YUV2RGB", {NVCV_COLOR_YUV2RGB, NVCV_IMAGE_FORMAT_YUV8, NVCV_IMAGE_FORMAT_RGB8 }}, - {"RGB2YUV_NV12", {NVCV_COLOR_RGB2YUV_NV12, NVCV_IMAGE_FORMAT_RGB8, NVCV_IMAGE_FORMAT_NV12 }}, - {"YUV2RGB_NV12", {NVCV_COLOR_YUV2RGB_NV12, NVCV_IMAGE_FORMAT_NV12, NVCV_IMAGE_FORMAT_RGB8 }}, - }; - // clang-format on - - if (auto it = codeMap.find(str); it != codeMap.end()) - { - return it->second; - } - else - { - throw std::invalid_argument("Unrecognized color code"); - } -} - -template -inline float bytesPerPixel(NVCVImageFormat imgFormat) -{ -#define BPP_CASE(frmt, bytes) \ - case frmt: \ - return bytes * sizeof(BT) - - switch (imgFormat) - { - BPP_CASE(NVCV_IMAGE_FORMAT_RGB8, 3); - BPP_CASE(NVCV_IMAGE_FORMAT_BGR8, 3); - BPP_CASE(NVCV_IMAGE_FORMAT_HSV8, 3); - BPP_CASE(NVCV_IMAGE_FORMAT_RGBA8, 4); - BPP_CASE(NVCV_IMAGE_FORMAT_YUV8, 3); - BPP_CASE(NVCV_IMAGE_FORMAT_NV12, 1.5f); - BPP_CASE(NVCV_IMAGE_FORMAT_Y8, 1); - default: - throw std::invalid_argument("Unrecognized format"); - } -#undef BPP_CASE -} - -// Adapted from src/util/TensorDataUtils.hpp -inline static nvcv::Tensor CreateTensor(int numImages, int imgWidth, int imgHeight, const nvcv::ImageFormat &imgFormat) -{ - if (imgFormat == NVCV_IMAGE_FORMAT_NV12 || imgFormat == NVCV_IMAGE_FORMAT_NV12_ER - || imgFormat == NVCV_IMAGE_FORMAT_NV21 || imgFormat == NVCV_IMAGE_FORMAT_NV21_ER) - { - if (imgHeight % 2 != 0 || imgWidth % 2 != 0) - { - throw std::invalid_argument("Invalid height"); - } - - int height420 = (imgHeight * 3) / 2; - - return nvcv::Tensor(numImages, {imgWidth, height420}, nvcv::ImageFormat(NVCV_IMAGE_FORMAT_Y8)); - } - else - { - return nvcv::Tensor(numImages, {imgWidth, imgHeight}, imgFormat); - } -} - -template -inline void CvtColor(nvbench::state &state, nvbench::type_list) -try -{ - long3 shape = benchutils::GetShape<3>(state.get_string("shape")); - long varShape = state.get_int64("varShape"); - - ConvCodeToFormat formats = str2Frmt(state.get_string("code")); - - NVCVColorConversionCode code = std::get<0>(formats); - nvcv::ImageFormat inFormat{std::get<1>(formats)}; - nvcv::ImageFormat outFormat{std::get<2>(formats)}; - - state.add_global_memory_reads(shape.x * shape.y * shape.z * bytesPerPixel(inFormat)); - state.add_global_memory_writes(shape.x * shape.y * shape.z * bytesPerPixel(outFormat)); - - cvcuda::CvtColor op; - - if (varShape < 0) // negative var shape means use Tensor - { - nvcv::Tensor src = CreateTensor(shape.x, shape.z, shape.y, inFormat); - nvcv::Tensor dst = CreateTensor(shape.x, shape.z, shape.y, outFormat); - - benchutils::FillTensor(src, benchutils::RandomValues()); - - state.exec(nvbench::exec_tag::sync, - [&op, &src, &dst, &code](nvbench::launch &launch) { op(launch.get_stream(), src, dst, code); }); - } - else // zero and positive var shape means use ImageBatchVarShape - { - if (inFormat.chromaSubsampling() != nvcv::ChromaSubsampling::CSS_444 - || outFormat.chromaSubsampling() != nvcv::ChromaSubsampling::CSS_444) - { - state.skip("Skipping formats that have subsampled planes for the varshape benchmark"); - } - - std::vector imgSrc; - std::vector imgDst; - nvcv::ImageBatchVarShape src(shape.x); - nvcv::ImageBatchVarShape dst(shape.x); - std::vector> srcVec(shape.x); - - auto randomValuesU8 = benchutils::RandomValues(); - - for (int i = 0; i < shape.x; i++) - { - imgSrc.emplace_back(nvcv::Size2D{(int)shape.z, (int)shape.y}, inFormat); - imgDst.emplace_back(nvcv::Size2D{(int)shape.z, (int)shape.y}, outFormat); - - int srcRowStride = imgSrc[i].size().w * inFormat.planePixelStrideBytes(0); - int srcBufSize = imgSrc[i].size().h * srcRowStride; - srcVec[i].resize(srcBufSize); - for (int idx = 0; idx < srcBufSize; idx++) - { - srcVec[i][idx] = randomValuesU8(); - } - - auto imgData = imgSrc[i].exportData(); - CUDA_CHECK_ERROR(cudaMemcpy2D(imgData->plane(0).basePtr, imgData->plane(0).rowStride, srcVec[i].data(), - srcRowStride, srcRowStride, imgSrc[i].size().h, cudaMemcpyHostToDevice)); - } - src.pushBack(imgSrc.begin(), imgSrc.end()); - dst.pushBack(imgDst.begin(), imgDst.end()); - - state.exec(nvbench::exec_tag::sync, - [&op, &src, &dst, &code](nvbench::launch &launch) { op(launch.get_stream(), src, dst, code); }); - } -} - -catch (const std::exception &err) -{ - state.skip(err.what()); -} - -using BaseTypes = nvbench::type_list; - -NVBENCH_BENCH_TYPES(CvtColor, NVBENCH_TYPE_AXES(BaseTypes)) - .set_type_axes_names({"BaseType"}) - .add_string_axis("shape", {"1x1080x1920", "64x720x1280"}) - .add_string_axis("code", {"RGB2BGR", "RGB2RGBA", "RGBA2RGB", "RGB2GRAY", "GRAY2RGB", "RGB2HSV", "HSV2RGB", - "RGB2YUV", "YUV2RGB", "RGB2YUV_NV12", "YUV2RGB_NV12"}) - .add_int64_axis("varShape", {-1, 0}); diff --git a/bench/BenchErase.cpp b/bench/BenchErase.cpp deleted file mode 100644 index f7d0910a5..000000000 --- a/bench/BenchErase.cpp +++ /dev/null @@ -1,95 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed 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. - */ - -#include "BenchUtils.hpp" - -#include - -#include - -template -inline void Erase(nvbench::state &state, nvbench::type_list) -try -{ - long3 shape = benchutils::GetShape<3>(state.get_string("shape")); - long varShape = state.get_int64("varShape"); - int numErase = static_cast(state.get_int64("numErase")); - - bool random = true; - int seed = 0; - - state.add_global_memory_reads(shape.x * shape.y * shape.z * sizeof(T) - + shape.x * (sizeof(int2) + sizeof(int3) + sizeof(float) + sizeof(int))); - state.add_global_memory_writes(shape.x * shape.y * shape.z * sizeof(T)); - - cvcuda::Erase op(numErase); - - // clang-format off - - nvcv::Tensor anchor({{shape.x}, "N"}, nvcv::TYPE_2S32); - nvcv::Tensor erasing({{shape.x}, "N"}, nvcv::TYPE_3S32); - nvcv::Tensor values({{shape.x}, "N"}, nvcv::TYPE_F32); - nvcv::Tensor imgIdx({{shape.x}, "N"}, nvcv::TYPE_S32); - - benchutils::FillTensor(anchor, [](const long4_16a &){ return int2{0, 0}; }); - benchutils::FillTensor(erasing, [](const long4_16a &){ return int3{10, 10, 1}; }); - benchutils::FillTensor(values, [](const long4_16a &){ return 1.f; }); - benchutils::FillTensor(imgIdx, [](const long4_16a &){ return 0; }); - - if (varShape < 0) // negative var shape means use Tensor - { - nvcv::Tensor src({{shape.x, shape.y, shape.z, 1}, "NHWC"}, benchutils::GetDataType()); - nvcv::Tensor dst({{shape.x, shape.y, shape.z, 1}, "NHWC"}, benchutils::GetDataType()); - - benchutils::FillTensor(src, benchutils::RandomValues()); - - state.exec(nvbench::exec_tag::sync, - [&op, &src, &dst, &anchor, &erasing, &values, &imgIdx, &random, &seed](nvbench::launch &launch) - { - op(launch.get_stream(), src, dst, anchor, erasing, values, imgIdx, random, seed); - }); - } - else // zero and positive var shape means use ImageBatchVarShape - { - nvcv::ImageBatchVarShape src(shape.x); - nvcv::ImageBatchVarShape dst(shape.x); - - benchutils::FillImageBatch(src, long2{shape.z, shape.y}, long2{varShape, varShape}, - benchutils::RandomValues()); - dst.pushBack(src.begin(), src.end()); - - state.exec(nvbench::exec_tag::sync, - [&op, &src, &dst, &anchor, &erasing, &values, &imgIdx, &random, &seed](nvbench::launch &launch) - { - op(launch.get_stream(), src, dst, anchor, erasing, values, imgIdx, random, seed); - }); - } -} -catch (const std::exception &err) -{ - state.skip(err.what()); -} - -// clang-format on - -using EraseTypes = nvbench::type_list; - -NVBENCH_BENCH_TYPES(Erase, NVBENCH_TYPE_AXES(EraseTypes)) - .set_type_axes_names({"InOutDataType"}) - .add_string_axis("shape", {"1x1080x1920"}) - .add_int64_axis("varShape", {-1, 0}) - .add_int64_axis("numErase", {3}); diff --git a/bench/BenchFindHomography.cpp b/bench/BenchFindHomography.cpp deleted file mode 100644 index 3fa1a0a7f..000000000 --- a/bench/BenchFindHomography.cpp +++ /dev/null @@ -1,139 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed 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. - */ - -#include "BenchUtils.hpp" - -#include - -#include - -template -void fill_vector(std::vector &vec) -{ - auto random_val = benchutils::RandomValues(); - for (std::size_t i = 0; i < vec.size(); i++) - { - vec[i] = random_val(); - } -} - -template -void fill_dst(const std::vector &srcVec, const std::vector &modelsVec, std::vector &dstVec, std::size_t numSamples) -{ - for (std::size_t i = 0; i < numSamples; i++) - { - const T* model = &modelsVec[i * 9]; - std::size_t numPoints = srcVec.size() / (2 * numSamples); - for (std::size_t j = 0; j < numPoints; j++) - { - T x = srcVec[i * numPoints * 2 + j * 2]; - T y = srcVec[i * numPoints * 2 + j * 2 + 1]; - - // Apply homography transformation: - T w = model[6] * x + model[7] * y + model[8]; - - if (std::abs(w) > 1e-10) { - w = 1.0 / w; - T x_transformed = (model[0] * x + model[1] * y + model[2]) * w; - T y_transformed = (model[3] * x + model[4] * y + model[5]) * w; - - dstVec[i * numPoints * 2 + j * 2] = x_transformed; - dstVec[i * numPoints * 2 + j * 2 + 1] = y_transformed; - } else { - dstVec[i * numPoints * 2 + j * 2] = 0; - dstVec[i * numPoints * 2 + j * 2 + 1] = 0; - } - } - } -} - -template -void fill_tensor(nvcv::Tensor &tensor, const std::vector &vec) -{ - auto tensorData = tensor.exportData(); - CVCUDA_CHECK_DATA(tensorData); - - long3 strides{tensorData->stride(0), tensorData->stride(1)}; - long3 shape{tensorData->shape(0), tensorData->shape(1)}; - long bufSize{nvcv::cuda::GetElement(strides, 0) * nvcv::cuda::GetElement(shape, 0)}; - CVCUDA_CHECK_DATA(bufSize == vec.size()); - - CUDA_CHECK_ERROR(cudaMemcpy(tensorData->basePtr(), vec.data(), bufSize, cudaMemcpyHostToDevice)); -} - -template -inline void FindHomography(nvbench::state &state, nvbench::type_list) -try -{ - long2 shape = benchutils::GetShape<2>(state.get_string("shape")); - bool batch = state.get_int64("batch"); - auto numSamples = batch ? 1 : shape.x; - - nvcv::Tensor src({{numSamples, shape.y}, "NW"}, nvcv::TYPE_2F32); - nvcv::Tensor dst({{numSamples, shape.y}, "NW"}, nvcv::TYPE_2F32); - nvcv::Tensor models({{numSamples, 3, 3}, "NHW"}, benchutils::GetDataType()); - - std::vector srcVec(2 * numSamples * shape.y); - std::vector dstVec(2 * numSamples * shape.y); - std::vector modelsVec(9); - - fill_vector(srcVec); - fill_vector(modelsVec); - fill_dst(srcVec, modelsVec, dstVec, numSamples); - fill_tensor(src, srcVec); - fill_tensor(dst, dstVec); - - state.add_global_memory_reads(shape.x * shape.y * 4 * sizeof(T)); - state.add_global_memory_writes(shape.x * 3 * 3 * sizeof(T)); - - cvcuda::FindHomography op(shape.x, shape.y); - - if (!batch) // negative var shape means use Tensor - { - state.exec(nvbench::exec_tag::sync, [&op, &src, &dst, &models](nvbench::launch &launch) - { - op(launch.get_stream(), src, dst, models); - }); - } - else - { - nvcv::TensorBatch srcTensors(shape.x); - nvcv::TensorBatch dstTensors(shape.x); - nvcv::TensorBatch modelsTensors(shape.x); - srcTensors.pushBack(src); - dstTensors.pushBack(dst); - modelsTensors.pushBack(models); - - state.exec(nvbench::exec_tag::sync, [&op, &srcTensors, &dstTensors, &modelsTensors](nvbench::launch &launch) - { - op(launch.get_stream(), srcTensors, dstTensors, modelsTensors); - }); - } -} -catch (const std::exception &err) -{ - state.skip(err.what()); -} - -// clang-format on - -using FindHomographyTypes = nvbench::type_list; - -NVBENCH_BENCH_TYPES(FindHomography, NVBENCH_TYPE_AXES(FindHomographyTypes)) - .set_type_axes_names({"InOutDataType"}) - .add_string_axis("shape", {"1x1024"}) - .add_int64_axis("batch", {false, true}); diff --git a/bench/BenchFlip.cpp b/bench/BenchFlip.cpp deleted file mode 100644 index 6dc203333..000000000 --- a/bench/BenchFlip.cpp +++ /dev/null @@ -1,99 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed 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. - */ - -#include "BenchUtils.hpp" - -#include - -#include - -template -inline void Flip(nvbench::state &state, nvbench::type_list) -try -{ - long3 shape = benchutils::GetShape<3>(state.get_string("shape")); - long varShape = state.get_int64("varShape"); - int flipCode; - - if (state.get_string("flipType") == "HORIZONTAL") - { - flipCode = 0; - } - else if (state.get_string("flipType") == "VERTICAL") - { - flipCode = 1; - } - else if (state.get_string("flipType") == "BOTH") - { - flipCode = -1; - } - else - { - throw std::invalid_argument("Invalid flipType = " + state.get_string("flipType")); - } - - state.add_global_memory_reads(shape.x * shape.y * shape.z * sizeof(T)); - state.add_global_memory_writes(shape.x * shape.y * shape.z * sizeof(T)); - - cvcuda::Flip op; - - // clang-format off - if (varShape < 0) // negative var shape means use Tensor - { - nvcv::Tensor src({{shape.x, shape.y, shape.z, 1}, "NHWC"}, benchutils::GetDataType()); - nvcv::Tensor dst({{shape.x, shape.y, shape.z, 1}, "NHWC"}, benchutils::GetDataType()); - - benchutils::FillTensor(src, benchutils::RandomValues()); - - state.exec(nvbench::exec_tag::sync, [&op, &src, &dst, &flipCode](nvbench::launch &launch) - { - op(launch.get_stream(), src, dst, flipCode); - }); - } - else // zero and positive var shape means use ImageBatchVarShape - { - nvcv::ImageBatchVarShape src(shape.x); - nvcv::ImageBatchVarShape dst(shape.x); - - benchutils::FillImageBatch(src, long2{shape.z, shape.y}, long2{varShape, varShape}, - benchutils::RandomValues()); - dst.pushBack(src.begin(), src.end()); - - nvcv::Tensor flipCodeTensor({{shape.x}, "N"}, nvcv::TYPE_S32); - - benchutils::FillTensor(flipCodeTensor, [&flipCode](const long4_16a &){ return flipCode; }); - - state.exec(nvbench::exec_tag::sync, [&op, &src, &dst, &flipCodeTensor](nvbench::launch &launch) - { - op(launch.get_stream(), src, dst, flipCodeTensor); - }); - } -} -catch (const std::exception &err) -{ - state.skip(err.what()); -} - -// clang-format on - -using FlipTypes = nvbench::type_list; - -NVBENCH_BENCH_TYPES(Flip, NVBENCH_TYPE_AXES(FlipTypes)) - .set_type_axes_names({"InOutDataType"}) - .add_string_axis("shape", {"1x1080x1920"}) - .add_int64_axis("varShape", {-1, 0}) - .add_string_axis("flipType", {"BOTH"}); diff --git a/bench/BenchGammaContrast.cpp b/bench/BenchGammaContrast.cpp deleted file mode 100644 index e1e16958f..000000000 --- a/bench/BenchGammaContrast.cpp +++ /dev/null @@ -1,75 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed 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. - */ - -#include "BenchUtils.hpp" - -#include - -#include - -template -inline void GammaContrast(nvbench::state &state, nvbench::type_list) -try -{ - long3 shape = benchutils::GetShape<3>(state.get_string("shape")); - long varShape = state.get_int64("varShape"); - - int ch = nvcv::cuda::NumElements; - - state.add_global_memory_reads(shape.x * shape.y * shape.z * sizeof(T)); - state.add_global_memory_writes(shape.x * shape.y * shape.z * sizeof(T)); - - cvcuda::GammaContrast op(shape.x, ch); - - // clang-format off - - nvcv::Tensor gamma({{shape.x * ch}, "N"}, nvcv::TYPE_F32); - - benchutils::FillTensor(gamma, benchutils::RandomValues(.5f, 1.f)); - - if (varShape < 0) // negative var shape means use Tensor - { - throw std::invalid_argument("Tensor not implemented for this operator"); - } - else // zero and positive var shape means use ImageBatchVarShape - { - nvcv::ImageBatchVarShape src(shape.x); - nvcv::ImageBatchVarShape dst(shape.x); - - benchutils::FillImageBatch(src, long2{shape.z, shape.y}, long2{varShape, varShape}, - benchutils::RandomValues()); - dst.pushBack(src.begin(), src.end()); - - state.exec(nvbench::exec_tag::sync, [&op, &src, &dst, &gamma](nvbench::launch &launch) - { - op(launch.get_stream(), src, dst, gamma); - }); - } -} -catch (const std::exception &err) -{ - state.skip(err.what()); -} - -// clang-format on - -using GammaContrastTypes = nvbench::type_list; - -NVBENCH_BENCH_TYPES(GammaContrast, NVBENCH_TYPE_AXES(GammaContrastTypes)) - .set_type_axes_names({"InOutDataType"}) - .add_string_axis("shape", {"1x1080x1920"}) - .add_int64_axis("varShape", {0}); diff --git a/bench/BenchGaussian.cpp b/bench/BenchGaussian.cpp deleted file mode 100644 index 0acf2945c..000000000 --- a/bench/BenchGaussian.cpp +++ /dev/null @@ -1,96 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed 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. - */ - -#include "BenchUtils.hpp" - -#include - -#include - -template -inline void Gaussian(nvbench::state &state, nvbench::type_list) -try -{ - long3 shape = benchutils::GetShape<3>(state.get_string("shape")); - long varShape = state.get_int64("varShape"); - double sigma = state.get_float64("sigma"); - - NVCVBorderType borderType = benchutils::GetBorderType(state.get_string("border")); - - int kernelSize = (int)std::round(sigma * (std::is_same_v, uint8_t> ? 3 : 4) * 2 + 1) | 1; - int2 ksize2{kernelSize, kernelSize}; - - nvcv::Size2D kernelSize2{kernelSize, kernelSize}; - double2 sigma2{sigma, sigma}; - - state.add_global_memory_reads(shape.x * shape.y * shape.z * sizeof(T)); - state.add_global_memory_writes(shape.x * shape.y * shape.z * sizeof(T)); - - cvcuda::Gaussian op(kernelSize2, shape.x); - - // clang-format off - - if (varShape < 0) // negative var shape means use Tensor - { - nvcv::Tensor src({{shape.x, shape.y, shape.z, 1}, "NHWC"}, benchutils::GetDataType()); - nvcv::Tensor dst({{shape.x, shape.y, shape.z, 1}, "NHWC"}, benchutils::GetDataType()); - - benchutils::FillTensor(src, benchutils::RandomValues()); - - state.exec(nvbench::exec_tag::sync, - [&op, &src, &dst, &kernelSize2, &sigma2, &borderType](nvbench::launch &launch) - { - op(launch.get_stream(), src, dst, kernelSize2, sigma2, borderType); - }); - } - else // zero and positive var shape means use ImageBatchVarShape - { - nvcv::ImageBatchVarShape src(shape.x); - nvcv::ImageBatchVarShape dst(shape.x); - - benchutils::FillImageBatch(src, long2{shape.z, shape.y}, long2{varShape, varShape}, - benchutils::RandomValues()); - dst.pushBack(src.begin(), src.end()); - - nvcv::Tensor kernelSizeTensor({{shape.x}, "N"}, nvcv::TYPE_2S32); - nvcv::Tensor sigmaTensor({{shape.x}, "N"}, nvcv::TYPE_2F64); - - benchutils::FillTensor(kernelSizeTensor, [&ksize2](const long4_16a &){ return ksize2; }); - benchutils::FillTensor(sigmaTensor, [&sigma2](const long4_16a &){ return sigma2; }); - - state.exec(nvbench::exec_tag::sync, - [&op, &src, &dst, &kernelSizeTensor, &sigmaTensor, &borderType](nvbench::launch &launch) - { - op(launch.get_stream(), src, dst, kernelSizeTensor, sigmaTensor, borderType); - }); - } -} -catch (const std::exception &err) -{ - state.skip(err.what()); -} - -// clang-format on - -using GaussianTypes = nvbench::type_list; - -NVBENCH_BENCH_TYPES(Gaussian, NVBENCH_TYPE_AXES(GaussianTypes)) - .set_type_axes_names({"InOutDataType"}) - .add_string_axis("shape", {"1x1080x1920"}) - .add_int64_axis("varShape", {-1, 0}) - .add_float64_axis("sigma", {1.2}) - .add_string_axis("border", {"REFLECT"}); diff --git a/bench/BenchGaussianNoise.cpp b/bench/BenchGaussianNoise.cpp deleted file mode 100644 index 3806e7524..000000000 --- a/bench/BenchGaussianNoise.cpp +++ /dev/null @@ -1,87 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed 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. - */ - -#include "BenchUtils.hpp" - -#include - -#include - -template -inline void GaussianNoise(nvbench::state &state, nvbench::type_list) -try -{ - long3 shape = benchutils::GetShape<3>(state.get_string("shape")); - long varShape = state.get_int64("varShape"); - - bool perCh = nvcv::cuda::NumElements > 1; - - unsigned long long int seed = 12345; - - state.add_global_memory_reads(shape.x * shape.y * shape.z * sizeof(T)); - state.add_global_memory_writes(shape.x * shape.y * shape.z * sizeof(T)); - - cvcuda::GaussianNoise op(shape.x); - - // clang-format off - - nvcv::Tensor mu({{shape.x}, "N"}, nvcv::TYPE_F32); - nvcv::Tensor sigma({{shape.x}, "N"}, nvcv::TYPE_F32); - - benchutils::FillTensor(mu, benchutils::RandomValues(.0f, 1.f)); - benchutils::FillTensor(sigma, benchutils::RandomValues(.05f, .1f)); - - if (varShape < 0) // negative var shape means use Tensor - { - nvcv::Tensor src({{shape.x, shape.y, shape.z, 1}, "NHWC"}, benchutils::GetDataType()); - nvcv::Tensor dst({{shape.x, shape.y, shape.z, 1}, "NHWC"}, benchutils::GetDataType()); - - benchutils::FillTensor(src, benchutils::RandomValues()); - - state.exec(nvbench::exec_tag::sync, [&op, &src, &dst, &mu, &sigma, &perCh, &seed](nvbench::launch &launch) - { - op(launch.get_stream(), src, dst, mu, sigma, perCh, seed); - }); - } - else // zero and positive var shape means use ImageBatchVarShape - { - nvcv::ImageBatchVarShape src(shape.x); - nvcv::ImageBatchVarShape dst(shape.x); - - benchutils::FillImageBatch(src, long2{shape.z, shape.y}, long2{varShape, varShape}, - benchutils::RandomValues()); - dst.pushBack(src.begin(), src.end()); - - state.exec(nvbench::exec_tag::sync, [&op, &src, &dst, &mu, &sigma, &perCh, &seed](nvbench::launch &launch) - { - op(launch.get_stream(), src, dst, mu, sigma, perCh, seed); - }); - } -} -catch (const std::exception &err) -{ - state.skip(err.what()); -} - -// clang-format on - -using GaussianNoiseTypes = nvbench::type_list; - -NVBENCH_BENCH_TYPES(GaussianNoise, NVBENCH_TYPE_AXES(GaussianNoiseTypes)) - .set_type_axes_names({"InOutDataType"}) - .add_string_axis("shape", {"1x1080x1920"}) - .add_int64_axis("varShape", {-1, 0}); diff --git a/bench/BenchHQResize.cpp b/bench/BenchHQResize.cpp deleted file mode 100644 index 49ff41412..000000000 --- a/bench/BenchHQResize.cpp +++ /dev/null @@ -1,129 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed 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. - */ - -#include "BenchUtils.hpp" - -#include - -#include - -template -inline void HQResize(nvbench::state &state, nvbench::type_list) -try -{ - long3 srcShape = benchutils::GetShape<3>(state.get_string("shape")); - bool antialias = state.get_int64("antialias"); - NVCVInterpolationType interpolation = benchutils::GetInterpolationType(state.get_string("interpolation")); - bool batch = state.get_int64("batch"); - - long3 dstShape; - if (state.get_string("resizeType") == "EXPAND") - { - if (antialias) - { - state.skip("Antialias is no-op for expanding"); - return; - } - dstShape = long3{srcShape.x, srcShape.y * 2, srcShape.z * 2}; - } - else if (state.get_string("resizeType") == "CONTRACT") - { - // resize from shape to shape/2 - dstShape = long3{srcShape.x, srcShape.y / 2, srcShape.z / 2}; - } - else - { - throw std::invalid_argument("Invalid resizeType = " + state.get_string("resizeType")); - } - - nvcv::Size2D srcSize{(int)srcShape.z, (int)srcShape.y}; - nvcv::Size2D dstSize{(int)dstShape.z, (int)dstShape.y}; - - nvcv::DataType dtype{benchutils::GetDataType()}; - nvcv::ImageFormat fmt(nvcv::MemLayout::PITCH_LINEAR, dtype.dataKind(), nvcv::Swizzle::S_X000, dtype.packing()); - - state.add_global_memory_reads(srcShape.x * srcShape.y * srcShape.z * sizeof(T)); - state.add_global_memory_writes(dstShape.x * dstShape.y * dstShape.z * sizeof(T)); - - cvcuda::HQResize op; - - if (!batch) - { - HQResizeTensorShapeI inShapeDesc{ - {srcSize.h, srcSize.w}, - 2, - 1 - }; - HQResizeTensorShapeI outShapeDesc{ - {dstSize.h, dstSize.w}, - 2, - 1 - }; - cvcuda::UniqueWorkspace ws = cvcuda::AllocateWorkspace( - op.getWorkspaceRequirements(1, inShapeDesc, outShapeDesc, interpolation, interpolation, antialias)); - - // clang-format off - nvcv::Tensor src({{srcShape.x, srcShape.y, srcShape.z, 1}, "NHWC"}, dtype); - nvcv::Tensor dst({{dstShape.x, dstShape.y, dstShape.z, 1}, "NHWC"}, dtype); - // clang-format on - - benchutils::FillTensor(src, benchutils::RandomValues()); - - state.exec(nvbench::exec_tag::sync, [&op, &ws, &src, &dst, interpolation, antialias](nvbench::launch &launch) - { op(launch.get_stream(), ws.get(), src, dst, interpolation, interpolation, antialias); }); - } - else - { - HQResizeTensorShapeI maxShape{ - {std::max(srcSize.h, dstSize.h), std::max(srcSize.w, dstSize.w)}, - 2, - 1 - }; - cvcuda::UniqueWorkspace ws = cvcuda::AllocateWorkspace(op.getWorkspaceRequirements(1, maxShape)); - - // clang-format off - nvcv::Tensor src({{srcShape.y, srcShape.z, 1}, "HWC"}, dtype); - nvcv::Tensor dst({{dstShape.y, dstShape.z, 1}, "HWC"}, dtype); - // clang-format on - - benchutils::FillTensor(src, benchutils::RandomValues()); - nvcv::TensorBatch srcTensors(1); - nvcv::TensorBatch dstTensors(1); - srcTensors.pushBack(src); - dstTensors.pushBack(dst); - - state.exec( - nvbench::exec_tag::sync, - [&op, &ws, &srcTensors, &dstTensors, interpolation, antialias](nvbench::launch &launch) - { op(launch.get_stream(), ws.get(), srcTensors, dstTensors, interpolation, interpolation, antialias); }); - } -} - -catch (const std::exception &err) -{ - state.skip(err.what()); -} - -using HQResizeTypes = nvbench::type_list; - -NVBENCH_BENCH_TYPES(HQResize, NVBENCH_TYPE_AXES(HQResizeTypes)) - .set_type_axes_names({"InOutDataType"}) - .add_int64_axis("batch", {false, true}) - .add_string_axis("shape", {"1x1080x1920"}) - .add_string_axis("interpolation", {"CUBIC"}) - .add_int64_axis("antialias", {false, true}) - .add_string_axis("resizeType", {"CONTRACT"}); diff --git a/bench/BenchHistogram.cpp b/bench/BenchHistogram.cpp deleted file mode 100644 index 7734250d2..000000000 --- a/bench/BenchHistogram.cpp +++ /dev/null @@ -1,71 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed 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. - */ - -#include "BenchUtils.hpp" - -#include - -#include - -template -inline void Histogram(nvbench::state &state, nvbench::type_list) -try -{ - long3 shape = benchutils::GetShape<3>(state.get_string("shape")); - long varShape = state.get_int64("varShape"); - - int numBins = 256; - nvcv::Tensor mask{nullptr}; - - state.add_global_memory_reads(shape.x * shape.y * shape.z * sizeof(T)); - state.add_global_memory_writes(numBins * sizeof(int)); - - cvcuda::Histogram op; - - // clang-format off - - if (varShape < 0) // negative var shape means use Tensor - { - nvcv::Tensor src({{shape.x, shape.y, shape.z, 1}, "NHWC"}, benchutils::GetDataType()); - nvcv::Tensor hist({{shape.x, numBins, 1}, "HWC"}, nvcv::TYPE_S32); - - benchutils::FillTensor(src, benchutils::RandomValues()); - benchutils::FillTensor(hist, [](auto &){ return 0; }); - - state.exec(nvbench::exec_tag::sync, [&op, &src, &mask, &hist](nvbench::launch &launch) - { - op(launch.get_stream(), src, mask, hist); - }); - } - else // zero and positive var shape means use ImageBatchVarShape - { - throw std::invalid_argument("ImageBatchVarShape not implemented for this operator"); - } -} -catch (const std::exception &err) -{ - state.skip(err.what()); -} - -// clang-format on - -using HistogramTypes = nvbench::type_list; - -NVBENCH_BENCH_TYPES(Histogram, NVBENCH_TYPE_AXES(HistogramTypes)) - .set_type_axes_names({"InOutDataType"}) - .add_string_axis("shape", {"1x1080x1920"}) - .add_int64_axis("varShape", {-1}); diff --git a/bench/BenchHistogramEq.cpp b/bench/BenchHistogramEq.cpp deleted file mode 100644 index 0af7ea793..000000000 --- a/bench/BenchHistogramEq.cpp +++ /dev/null @@ -1,77 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed 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. - */ - -#include "BenchUtils.hpp" - -#include - -#include - -template -inline void HistogramEq(nvbench::state &state, nvbench::type_list) -try -{ - long3 shape = benchutils::GetShape<3>(state.get_string("shape")); - long varShape = state.get_int64("varShape"); - - state.add_global_memory_reads(shape.x * shape.y * shape.z * sizeof(T)); - state.add_global_memory_writes(shape.x * shape.y * shape.z * sizeof(T)); - - cvcuda::HistogramEq op(shape.x); - - // clang-format off - - if (varShape < 0) // negative var shape means use Tensor - { - nvcv::Tensor src({{shape.x, shape.y, shape.z, 1}, "NHWC"}, benchutils::GetDataType()); - nvcv::Tensor dst({{shape.x, shape.y, shape.z, 1}, "NHWC"}, benchutils::GetDataType()); - - benchutils::FillTensor(src, benchutils::RandomValues()); - - state.exec(nvbench::exec_tag::sync, [&op, &src, &dst](nvbench::launch &launch) - { - op(launch.get_stream(), src, dst); - }); - } - else // zero and positive var shape means use ImageBatchVarShape - { - nvcv::ImageBatchVarShape src(shape.x); - nvcv::ImageBatchVarShape dst(shape.x); - - benchutils::FillImageBatch(src, long2{shape.z, shape.y}, long2{varShape, varShape}, - benchutils::RandomValues()); - dst.pushBack(src.begin(), src.end()); - - state.exec(nvbench::exec_tag::sync, [&op, &src, &dst](nvbench::launch &launch) - { - op(launch.get_stream(), src, dst); - }); - } -} -catch (const std::exception &err) -{ - state.skip(err.what()); -} - -// clang-format on - -using HistogramEqTypes = nvbench::type_list; - -NVBENCH_BENCH_TYPES(HistogramEq, NVBENCH_TYPE_AXES(HistogramEqTypes)) - .set_type_axes_names({"InOutDataType"}) - .add_string_axis("shape", {"1x1080x1920"}) - .add_int64_axis("varShape", {-1, 0}); diff --git a/bench/BenchInpaint.cpp b/bench/BenchInpaint.cpp deleted file mode 100644 index 19ae66060..000000000 --- a/bench/BenchInpaint.cpp +++ /dev/null @@ -1,85 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed 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. - */ - -#include "BenchUtils.hpp" - -#include - -#include - -template -inline void Inpaint(nvbench::state &state, nvbench::type_list) -try -{ - long3 shape = benchutils::GetShape<3>(state.get_string("shape")); - long varShape = state.get_int64("varShape"); - - double inpaintRadius = 5.0; - - state.add_global_memory_reads(shape.x * shape.y * shape.z * (sizeof(T) + sizeof(uint8_t))); - state.add_global_memory_writes(shape.x * shape.y * shape.z * sizeof(T)); - - cvcuda::Inpaint op(shape.x, nvcv::Size2D{(int)shape.z, (int)shape.y}); - - // clang-format off - - if (varShape < 0) // negative var shape means use Tensor - { - nvcv::Tensor src({{shape.x, shape.y, shape.z, 1}, "NHWC"}, benchutils::GetDataType()); - nvcv::Tensor dst({{shape.x, shape.y, shape.z, 1}, "NHWC"}, benchutils::GetDataType()); - nvcv::Tensor mask({{shape.x, shape.y, shape.z, 1}, "NHWC"}, nvcv::TYPE_U8); - - benchutils::FillTensor(src, benchutils::RandomValues()); - benchutils::FillTensor(mask, benchutils::RandomValues(0, 1)); - - state.exec(nvbench::exec_tag::sync, [&op, &src, &mask, &dst, &inpaintRadius](nvbench::launch &launch) - { - op(launch.get_stream(), src, mask, dst, inpaintRadius); - }); - } - else // zero and positive var shape means use ImageBatchVarShape - { - nvcv::ImageBatchVarShape src(shape.x); - nvcv::ImageBatchVarShape dst(shape.x); - nvcv::ImageBatchVarShape mask(shape.x); - - benchutils::FillImageBatch(src, long2{shape.z, shape.y}, long2{varShape, varShape}, - benchutils::RandomValues()); - dst.pushBack(src.begin(), src.end()); - - benchutils::FillImageBatch(mask, long2{shape.z, shape.y}, long2{varShape, varShape}, - benchutils::RandomValues(0, 1)); - - state.exec(nvbench::exec_tag::sync, [&op, &src, &mask, &dst, &inpaintRadius](nvbench::launch &launch) - { - op(launch.get_stream(), src, mask, dst, inpaintRadius); - }); - } -} -catch (const std::exception &err) -{ - state.skip(err.what()); -} - -// clang-format on - -using InpaintTypes = nvbench::type_list; - -NVBENCH_BENCH_TYPES(Inpaint, NVBENCH_TYPE_AXES(InpaintTypes)) - .set_type_axes_names({"InOutDataType"}) - .add_string_axis("shape", {"1x1080x1920"}) - .add_int64_axis("varShape", {-1, 0}); diff --git a/bench/BenchJointBilateralFilter.cpp b/bench/BenchJointBilateralFilter.cpp deleted file mode 100644 index c3b10c9d2..000000000 --- a/bench/BenchJointBilateralFilter.cpp +++ /dev/null @@ -1,100 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed 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. - */ - -#include "BenchUtils.hpp" - -#include - -#include - -template -inline void JointBilateralFilter(nvbench::state &state, nvbench::type_list) -try -{ - long3 shape = benchutils::GetShape<3>(state.get_string("shape")); - long varShape = state.get_int64("varShape"); - int diameter = static_cast(state.get_int64("diameter")); - float sigmaSpace = static_cast(state.get_float64("sigmaSpace")); - float sigmaColor = -1.f; - - NVCVBorderType borderType = benchutils::GetBorderType(state.get_string("border")); - - state.add_global_memory_reads(shape.x * shape.y * shape.z * sizeof(T)); - state.add_global_memory_writes(shape.x * shape.y * shape.z * sizeof(T)); - - cvcuda::JointBilateralFilter op; - - // clang-format off - - if (varShape < 0) // negative var shape means use Tensor - { - nvcv::Tensor src({{shape.x, shape.y, shape.z, 1}, "NHWC"}, benchutils::GetDataType()); - nvcv::Tensor color({{shape.x, shape.y, shape.z, 1}, "NHWC"}, benchutils::GetDataType()); - nvcv::Tensor dst({{shape.x, shape.y, shape.z, 1}, "NHWC"}, benchutils::GetDataType()); - - benchutils::FillTensor(src, benchutils::RandomValues()); - benchutils::FillTensor(color, benchutils::RandomValues()); - - state.exec(nvbench::exec_tag::sync, - [&op, &src, &color, &dst, &diameter, &sigmaColor, &sigmaSpace, &borderType](nvbench::launch &launch) - { - op(launch.get_stream(), src, color, dst, diameter, sigmaColor, sigmaSpace, borderType); - }); - } - else // zero and positive var shape means use ImageBatchVarShape - { - nvcv::ImageBatchVarShape src(shape.x); - nvcv::ImageBatchVarShape color(shape.x); - nvcv::ImageBatchVarShape dst(shape.x); - - benchutils::FillImageBatch(src, long2{shape.z, shape.y}, long2{varShape, varShape}, - benchutils::RandomValues()); - color.pushBack(src.begin(), src.end()); - dst.pushBack(src.begin(), src.end()); - - nvcv::Tensor diameterTensor({{shape.x}, "N"}, nvcv::TYPE_S32); - nvcv::Tensor sigmaSpaceTensor({{shape.x}, "N"}, nvcv::TYPE_F32); - nvcv::Tensor sigmaColorTensor({{shape.x}, "N"}, nvcv::TYPE_F32); - - benchutils::FillTensor(diameterTensor, [&diameter](const long4_16a &){ return diameter; }); - benchutils::FillTensor(sigmaSpaceTensor, [&sigmaSpace](const long4_16a &){ return sigmaSpace; }); - benchutils::FillTensor(sigmaColorTensor, [&sigmaColor](const long4_16a &){ return sigmaColor; }); - - state.exec(nvbench::exec_tag::sync, - [&op, &src, &color, &dst, &diameterTensor, &sigmaColorTensor, &sigmaSpaceTensor, &borderType] - (nvbench::launch &launch) - { - op(launch.get_stream(), src, color, dst, diameterTensor, sigmaColorTensor, sigmaSpaceTensor, borderType); - }); - } -} -catch (const std::exception &err) -{ - state.skip(err.what()); -} - -// clang-format on - -using JointBilateralFilterTypes = nvbench::type_list; - -NVBENCH_BENCH_TYPES(JointBilateralFilter, NVBENCH_TYPE_AXES(JointBilateralFilterTypes)) - .set_type_axes_names({"InOutDataType"}) - .add_string_axis("shape", {"1x1080x1920"}) - .add_int64_axis("varShape", {-1, 0}) - .add_int64_axis("diameter", {-1}) - .add_float64_axis("sigmaSpace", {1.2}) - .add_string_axis("border", {"REFLECT"}); diff --git a/bench/BenchLabel.cpp b/bench/BenchLabel.cpp deleted file mode 100644 index c5cd7b218..000000000 --- a/bench/BenchLabel.cpp +++ /dev/null @@ -1,113 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed 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. - */ - -#include "BenchUtils.hpp" - -#include - -#include - -template -inline void Label(nvbench::state &state, nvbench::type_list) -try -{ - using DT = uint32_t; - - long3 srcShape = benchutils::GetShape<3>(state.get_string("shape")); - long3 dstShape = srcShape; - - std::string runChoice = state.get_string("runChoice"); - - // Use [BG][MIN][MAX][ISLAND][COUNT][STAT][MASK] in runChoice to run Label with: - // background; minThreshold; maxThreshold; island removal; count; statistics; mask - - long3 staShape{srcShape.x, 10000, 7}; // using fixed 10K max. cap. and 2D problem - - NVCVConnectivityType conn = NVCV_CONNECTIVITY_4_2D; - NVCVLabelType alab = NVCV_LABEL_FAST; - NVCVLabelMaskType mType = NVCV_REMOVE_ISLANDS_OUTSIDE_MASK_ONLY; - - nvcv::Tensor bgT, minT, maxT, countT, statsT, mszT, maskT; - - cvcuda::Label op; - - state.add_global_memory_reads(srcShape.x * srcShape.y * srcShape.z * sizeof(ST)); - state.add_global_memory_writes(dstShape.x * dstShape.y * dstShape.z * sizeof(DT)); - - // clang-format off - - if (runChoice.find("BG") != std::string::npos) - { - bgT = nvcv::Tensor({{srcShape.x}, "N"}, benchutils::GetDataType()); - - benchutils::FillTensor(bgT, benchutils::RandomValues()); - } - if (runChoice.find("MIN") != std::string::npos) - { - minT = nvcv::Tensor({{srcShape.x}, "N"}, benchutils::GetDataType()); - - benchutils::FillTensor(minT, benchutils::RandomValues()); - } - if (runChoice.find("MAX") != std::string::npos) - { - maxT = nvcv::Tensor({{srcShape.x}, "N"}, benchutils::GetDataType()); - - benchutils::FillTensor(maxT, benchutils::RandomValues()); - } - if (runChoice.find("ISLAND") != std::string::npos) - { - mszT = nvcv::Tensor({{srcShape.x}, "N"}, benchutils::GetDataType
()); - - benchutils::FillTensor
(mszT, benchutils::RandomValues
()); - } - if (runChoice.find("COUNT") != std::string::npos) - { - countT = nvcv::Tensor({{srcShape.x}, "N"}, benchutils::GetDataType
()); - } - if (runChoice.find("STAT") != std::string::npos) - { - statsT = nvcv::Tensor({{staShape.x, staShape.y, staShape.z}, "NMA"}, benchutils::GetDataType
()); - } - if (runChoice.find("MASK") != std::string::npos) - { - maskT = nvcv::Tensor({{srcShape.x, srcShape.y, srcShape.z, 1}, "NHWC"}, nvcv::TYPE_U8); - } - - nvcv::Tensor src({{srcShape.x, srcShape.y, srcShape.z, 1}, "NHWC"}, benchutils::GetDataType()); - nvcv::Tensor dst({{dstShape.x, dstShape.y, dstShape.z, 1}, "NHWC"}, benchutils::GetDataType
()); - - benchutils::FillTensor(src, benchutils::RandomValues()); - - state.exec(nvbench::exec_tag::sync, [&op, &src, &dst, &bgT, &minT, &maxT, &mszT, &countT, &statsT, &maskT, &conn, - &alab, &mType](nvbench::launch &launch) - { - op(launch.get_stream(), src, dst, bgT, minT, maxT, mszT, countT, statsT, maskT, conn, alab, mType); - }); -} -catch (const std::exception &err) -{ - state.skip(err.what()); -} - -// clang-format on - -using LabelTypes = nvbench::type_list; - -NVBENCH_BENCH_TYPES(Label, NVBENCH_TYPE_AXES(LabelTypes)) - .set_type_axes_names({"InOutDataType"}) - .add_string_axis("shape", {"1x1080x1920"}) - .add_string_axis("runChoice", {""}); diff --git a/bench/BenchLaplacian.cpp b/bench/BenchLaplacian.cpp deleted file mode 100644 index 340db402b..000000000 --- a/bench/BenchLaplacian.cpp +++ /dev/null @@ -1,91 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed 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. - */ - -#include "BenchUtils.hpp" - -#include - -#include - -template -inline void Laplacian(nvbench::state &state, nvbench::type_list) -try -{ - long3 shape = benchutils::GetShape<3>(state.get_string("shape")); - long varShape = state.get_int64("varShape"); - int ksize = static_cast(state.get_int64("ksize")); - float scale = static_cast(state.get_float64("scale")); - - NVCVBorderType borderType = benchutils::GetBorderType(state.get_string("border")); - - state.add_global_memory_reads(shape.x * shape.y * shape.z * sizeof(T)); - state.add_global_memory_writes(shape.x * shape.y * shape.z * sizeof(T)); - - cvcuda::Laplacian op; - - // clang-format off - - if (varShape < 0) // negative var shape means use Tensor - { - nvcv::Tensor src({{shape.x, shape.y, shape.z, 1}, "NHWC"}, benchutils::GetDataType()); - nvcv::Tensor dst({{shape.x, shape.y, shape.z, 1}, "NHWC"}, benchutils::GetDataType()); - - benchutils::FillTensor(src, benchutils::RandomValues()); - - state.exec(nvbench::exec_tag::sync, [&op, &src, &dst, &ksize, &scale, &borderType](nvbench::launch &launch) - { - op(launch.get_stream(), src, dst, ksize, scale, borderType); - }); - } - else // zero and positive var shape means use ImageBatchVarShape - { - nvcv::ImageBatchVarShape src(shape.x); - nvcv::ImageBatchVarShape dst(shape.x); - - benchutils::FillImageBatch(src, long2{shape.z, shape.y}, long2{varShape, varShape}, - benchutils::RandomValues()); - dst.pushBack(src.begin(), src.end()); - - nvcv::Tensor ksizeTensor({{shape.x}, "N"}, nvcv::TYPE_S32); - nvcv::Tensor scaleTensor({{shape.x}, "N"}, nvcv::TYPE_F32); - - benchutils::FillTensor(ksizeTensor, [&ksize](const long4_16a &){ return ksize; }); - benchutils::FillTensor(scaleTensor, [&scale](const long4_16a &){ return scale; }); - - state.exec(nvbench::exec_tag::sync, - [&op, &src, &dst, &ksizeTensor, &scaleTensor, &borderType](nvbench::launch &launch) - { - op(launch.get_stream(), src, dst, ksizeTensor, scaleTensor, borderType); - }); - } -} -catch (const std::exception &err) -{ - state.skip(err.what()); -} - -// clang-format on - -using LaplacianTypes = nvbench::type_list; - -NVBENCH_BENCH_TYPES(Laplacian, NVBENCH_TYPE_AXES(LaplacianTypes)) - .set_type_axes_names({"InOutDataType"}) - .add_string_axis("shape", {"1x1080x1920"}) - .add_int64_axis("varShape", {-1, 0}) - .add_int64_axis("ksize", {1}) - .add_float64_axis("scale", {1.0}) - .add_string_axis("border", {"REFLECT101"}); diff --git a/bench/BenchMedianBlur.cpp b/bench/BenchMedianBlur.cpp deleted file mode 100644 index f21adf203..000000000 --- a/bench/BenchMedianBlur.cpp +++ /dev/null @@ -1,86 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed 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. - */ - -#include "BenchUtils.hpp" - -#include - -#include - -template -inline void MedianBlur(nvbench::state &state, nvbench::type_list) -try -{ - long3 shape = benchutils::GetShape<3>(state.get_string("shape")); - long varShape = state.get_int64("varShape"); - int2 kernelSize = nvcv::cuda::StaticCast(benchutils::GetShape<2>(state.get_string("kernelSize"))); - - nvcv::Size2D kernelSize2d{kernelSize.x, kernelSize.y}; - - state.add_global_memory_reads(shape.x * shape.y * shape.z * sizeof(T)); - state.add_global_memory_writes(shape.x * shape.y * shape.z * sizeof(T)); - - cvcuda::MedianBlur op(shape.x); - - // clang-format off - - if (varShape < 0) // negative var shape means use Tensor - { - nvcv::Tensor src({{shape.x, shape.y, shape.z, 1}, "NHWC"}, benchutils::GetDataType()); - nvcv::Tensor dst({{shape.x, shape.y, shape.z, 1}, "NHWC"}, benchutils::GetDataType()); - - benchutils::FillTensor(src, benchutils::RandomValues()); - - state.exec(nvbench::exec_tag::sync, [&op, &src, &dst, &kernelSize2d](nvbench::launch &launch) - { - op(launch.get_stream(), src, dst, kernelSize2d); - }); - } - else // zero and positive var shape means use ImageBatchVarShape - { - nvcv::ImageBatchVarShape src(shape.x); - nvcv::ImageBatchVarShape dst(shape.x); - - benchutils::FillImageBatch(src, long2{shape.z, shape.y}, long2{varShape, varShape}, - benchutils::RandomValues()); - dst.pushBack(src.begin(), src.end()); - - nvcv::Tensor kernelSizeTensor({{shape.x, 2}, "NW"}, nvcv::TYPE_S32); - - benchutils::FillTensor(kernelSizeTensor, - [&kernelSize](const long4_16a &c){ return nvcv::cuda::GetElement(kernelSize, c.y); }); - - state.exec(nvbench::exec_tag::sync, [&op, &src, &dst, &kernelSizeTensor](nvbench::launch &launch) - { - op(launch.get_stream(), src, dst, kernelSizeTensor); - }); - } -} -catch (const std::exception &err) -{ - state.skip(err.what()); -} - -// clang-format on - -using MedianBlurTypes = nvbench::type_list; - -NVBENCH_BENCH_TYPES(MedianBlur, NVBENCH_TYPE_AXES(MedianBlurTypes)) - .set_type_axes_names({"InOutDataType"}) - .add_string_axis("shape", {"1x1080x1920"}) - .add_int64_axis("varShape", {-1, 0}) - .add_string_axis("kernelSize", {"5x5"}); diff --git a/bench/BenchMinAreaRect.cpp b/bench/BenchMinAreaRect.cpp deleted file mode 100644 index 1eae7a3e5..000000000 --- a/bench/BenchMinAreaRect.cpp +++ /dev/null @@ -1,70 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed 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. - */ - -#include "BenchUtils.hpp" - -#include - -#include - -template -inline void MinAreaRect(nvbench::state &state, nvbench::type_list) -try -{ - long2 shape = benchutils::GetShape<2>(state.get_string("shape")); - long varShape = state.get_int64("varShape"); - - state.add_global_memory_reads(shape.x * shape.y * sizeof(T)); - state.add_global_memory_writes(shape.x * 8 * sizeof(float) + shape.x * sizeof(int)); - - cvcuda::MinAreaRect op(shape.x); - - // clang-format off - - if (varShape < 0) // negative var shape means use Tensor - { - nvcv::Tensor src({{shape.x, shape.y, 2}, "NWC"}, benchutils::GetDataType()); - nvcv::Tensor dst({{shape.x, 8}, "NW"}, nvcv::TYPE_F32); - nvcv::Tensor points({{1, shape.x}, "NW"}, nvcv::TYPE_S32); - - benchutils::FillTensor(src, benchutils::RandomValues()); - benchutils::FillTensor(dst, benchutils::RandomValues(0.f, 1.f)); - benchutils::FillTensor(points, benchutils::RandomValues(10, 100)); - - state.exec(nvbench::exec_tag::sync, [&op, &src, &dst, &points, &shape](nvbench::launch &launch) - { - op(launch.get_stream(), src, dst, points, shape.x); - }); - } - else // zero and positive var shape means use ImageBatchVarShape - { - throw std::invalid_argument("ImageBatchVarShape not implemented for this operator"); - } -} -catch (const std::exception &err) -{ - state.skip(err.what()); -} - -// clang-format on - -using MinAreaRectTypes = nvbench::type_list; - -NVBENCH_BENCH_TYPES(MinAreaRect, NVBENCH_TYPE_AXES(MinAreaRectTypes)) - .set_type_axes_names({"InOutDataType"}) - .add_string_axis("shape", {"1x1024"}) - .add_int64_axis("varShape", {-1}); diff --git a/bench/BenchMinMaxLoc.cpp b/bench/BenchMinMaxLoc.cpp deleted file mode 100644 index 7f1b28980..000000000 --- a/bench/BenchMinMaxLoc.cpp +++ /dev/null @@ -1,92 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed 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. - */ - -#include "BenchMinMaxLoc.hpp" - -#include - -#include - -template -inline void MinMaxLoc(nvbench::state &state, nvbench::type_list) -try -{ - long3 shape = benchutils::GetShape<3>(state.get_string("shape")); - long varShape = state.get_int64("varShape"); - long maxLocs = state.get_int64("maxLocations"); - - // clang-format off - - nvcv::Tensor minVal({{shape.x}, "N"}, nvcv::TYPE_U32); - nvcv::Tensor minLoc({{shape.x, maxLocs}, "NM"}, nvcv::TYPE_2S32); - nvcv::Tensor numMin({{shape.x}, "N"}, nvcv::TYPE_S32); - - nvcv::Tensor maxVal({{shape.x}, "N"}, nvcv::TYPE_U32); - nvcv::Tensor maxLoc({{shape.x, maxLocs}, "NM"}, nvcv::TYPE_2S32); - nvcv::Tensor numMax({{shape.x}, "N"}, nvcv::TYPE_S32); - - // clang-format on - - // R/W bandwidth rationale: - // 1 read to find min/max + 1 read to collect their locations - // 2 writes of min/max values (U32), locations (2S32) and quantity (S32) - state.add_global_memory_reads(shape.x * shape.y * shape.z * sizeof(T) * 2); - state.add_global_memory_writes(shape.x * (sizeof(uint32_t) + maxLocs * sizeof(int2) + sizeof(int)) * 2); - - cvcuda::MinMaxLoc op; - - // clang-format off - - if (varShape < 0) // negative var shape means use Tensor - { - nvcv::Tensor src({{shape.x, shape.y, shape.z, 1}, "NHWC"}, benchutils::GetDataType()); - - benchutils::FillTensorWithMinMax(src, maxLocs); - - state.exec(nvbench::exec_tag::sync, - [&op, &src, &minVal, &minLoc, &numMin, &maxVal, &maxLoc, &numMax](nvbench::launch &launch) - { - op(launch.get_stream(), src, minVal, minLoc, numMin, maxVal, maxLoc, numMax); - }); - } - else // zero and positive var shape means use ImageBatchVarShape - { - nvcv::ImageBatchVarShape src(shape.x); - - benchutils::FillImageBatchWithMinMax(src, long2{shape.z, shape.y}, long2{varShape, varShape}, maxLocs); - - state.exec(nvbench::exec_tag::sync, - [&op, &src, &minVal, &minLoc, &numMin, &maxVal, &maxLoc, &numMax](nvbench::launch &launch) - { - op(launch.get_stream(), src, minVal, minLoc, numMin, maxVal, maxLoc, numMax); - }); - } -} -catch (const std::exception &err) -{ - state.skip(err.what()); -} - -// clang-format on - -using MinMaxLocTypes = nvbench::type_list; - -NVBENCH_BENCH_TYPES(MinMaxLoc, NVBENCH_TYPE_AXES(MinMaxLocTypes)) - .set_type_axes_names({"InOutDataType"}) - .add_string_axis("shape", {"1x1080x1920"}) - .add_int64_axis("varShape", {-1, 0}) - .add_int64_axis("maxLocations", {100000}); diff --git a/bench/BenchMinMaxLoc.hpp b/bench/BenchMinMaxLoc.hpp deleted file mode 100644 index cd7f6a278..000000000 --- a/bench/BenchMinMaxLoc.hpp +++ /dev/null @@ -1,121 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed 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. - */ - -#ifndef CVCUDA_BENCH_MINMAXLOC_HPP -#define CVCUDA_BENCH_MINMAXLOC_HPP - -#include "BenchUtils.hpp" - -namespace benchutils { - -template, typename RE = typename R::RE, typename UD = typename R::UD, - class = nvcv::cuda::Require == 0 && std::is_integral_v>> -inline auto RandomValuesWithoutMinMax(VT min = nvcv::cuda::TypeTraits::min, - VT max = nvcv::cuda::TypeTraits::max, RE rng = DefaultGenerator()) -{ - return R{UD(min + 1, max - 1), rng}; -} - -template, typename RE = typename R::RE> -inline void RandomMinMax(std::vector &srcVec, const long3 &shape, const long3 &strides, long locs, - RE randomGenerator = DefaultGenerator()) -{ - long locsPerHeight = static_cast(std::ceil(locs / shape.y)); - if (locsPerHeight * 2 >= shape.z) - { - throw std::runtime_error("Locations is bigger than available pixels"); - } - - for (long x = 0; x < shape.x; ++x) - { - long countLocs = 0; - for (long y = 0; y < shape.y; ++y) - { - long numLocs = (countLocs + locsPerHeight > locs) ? (locs - countLocs) : locsPerHeight; - for (long z = 0; z < numLocs; ++z) - { - ValueAt(srcVec, strides, long3{x, y, z}) = nvcv::cuda::TypeTraits::min; - } - for (long z = numLocs; z < numLocs * 2; ++z) - { - ValueAt(srcVec, strides, long3{x, y, z}) = nvcv::cuda::TypeTraits::max; - } - std::shuffle(&ValueAt(srcVec, strides, long3{x, y, 0}), - &ValueAt(srcVec, strides, long3{x, y, shape.z}), randomGenerator); - - countLocs += locsPerHeight; - } - } -} - -template -inline void FillTensorWithMinMax(const nvcv::Tensor &tensor, long locations) -{ - auto tensorData = tensor.exportData(); - CVCUDA_CHECK_DATA(tensorData); - - if (tensor.rank() != 3 && tensor.rank() != 4) - { - throw std::invalid_argument("Tensor rank is not 3 or 4"); - } - - long3 strides{tensorData->stride(0), tensorData->stride(1), tensorData->stride(2)}; - long3 shape{tensorData->shape(0), tensorData->shape(1), tensorData->shape(2)}; - long bufSize{nvcv::cuda::GetElement(strides, 0) * nvcv::cuda::GetElement(shape, 0)}; - - std::vector tensorVec(bufSize); - - FillBuffer(tensorVec, shape, strides, RandomValuesWithoutMinMax()); - - RandomMinMax(tensorVec, shape, strides, locations); - - CUDA_CHECK_ERROR(cudaMemcpy(tensorData->basePtr(), tensorVec.data(), bufSize, cudaMemcpyHostToDevice)); -} - -template -inline void FillImageBatchWithMinMax(nvcv::ImageBatchVarShape &imageBatch, long2 size, long2 varSize, long locations) -{ - auto randomWidth = RandomValues(static_cast(size.x - varSize.x), static_cast(size.x)); - auto randomHeight = RandomValues(static_cast(size.y - varSize.y), static_cast(size.y)); - - for (int i = 0; i < imageBatch.capacity(); ++i) - { - nvcv::Image image(nvcv::Size2D{randomWidth(), randomHeight()}, GetFormat()); - - auto data = image.exportData(); - CVCUDA_CHECK_DATA(data); - - long2 strides{data->plane(0).rowStride, sizeof(VT)}; - long2 shape{data->plane(0).height, data->plane(0).width}; - long bufSize{strides.x * shape.x}; - - std::vector imageBuffer(bufSize); - - FillBuffer(imageBuffer, shape, strides, RandomValuesWithoutMinMax()); - - RandomMinMax(imageBuffer, long3{1, shape.x, shape.y}, long3{bufSize, strides.x, strides.y}, locations); - - CUDA_CHECK_ERROR(cudaMemcpy2D(data->plane(0).basePtr, strides.x, imageBuffer.data(), strides.x, strides.x, - data->plane(0).height, cudaMemcpyHostToDevice)); - - imageBatch.pushBack(image); - } -} - -} // namespace benchutils - -#endif // CVCUDA_BENCH_MINMAXLOC_HPP diff --git a/bench/BenchMorphology.cpp b/bench/BenchMorphology.cpp deleted file mode 100644 index 69a413045..000000000 --- a/bench/BenchMorphology.cpp +++ /dev/null @@ -1,135 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed 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. - */ - -#include "BenchUtils.hpp" - -#include - -#include - -template -inline void Morphology(nvbench::state &state, nvbench::type_list) -try -{ - long3 shape = benchutils::GetShape<3>(state.get_string("shape")); - long varShape = state.get_int64("varShape"); - int iteration = static_cast(state.get_int64("iteration")); - int2 kernelSize = nvcv::cuda::StaticCast(benchutils::GetShape<2>(state.get_string("kernelSize"))); - - NVCVBorderType borderType = benchutils::GetBorderType(state.get_string("border")); - - NVCVMorphologyType morphType; - - if (state.get_string("morphType") == "ERODE") - { - morphType = NVCV_ERODE; - } - else if (state.get_string("morphType") == "DILATE") - { - morphType = NVCV_DILATE; - } - else if (state.get_string("morphType") == "OPEN") - { - morphType = NVCV_OPEN; - } - else if (state.get_string("morphType") == "CLOSE") - { - morphType = NVCV_CLOSE; - } - - nvcv::Size2D mask{kernelSize.x, kernelSize.y}; - int2 anchor{-1, -1}; - - int bwIteration = (morphType == NVCV_OPEN || morphType == NVCV_CLOSE || iteration > 1) ? 2 * iteration : iteration; - - state.add_global_memory_reads(shape.x * shape.y * shape.z * sizeof(T) * bwIteration); - state.add_global_memory_writes(shape.x * shape.y * shape.z * sizeof(T) * bwIteration); - - cvcuda::Morphology op; - - // clang-format off - - if (varShape < 0) // negative var shape means use Tensor - { - nvcv::Tensor src({{shape.x, shape.y, shape.z, 1}, "NHWC"}, benchutils::GetDataType()); - nvcv::Tensor dst({{shape.x, shape.y, shape.z, 1}, "NHWC"}, benchutils::GetDataType()); - - benchutils::FillTensor(src, benchutils::RandomValues()); - - nvcv::Tensor workspace{nullptr}; - - if (morphType == NVCV_OPEN || morphType == NVCV_CLOSE || iteration > 1) - { - workspace = nvcv::Tensor({{shape.x, shape.y, shape.z, 1}, "NHWC"}, benchutils::GetDataType()); - } - - state.exec(nvbench::exec_tag::sync, - [&op, &src, &dst, &workspace, &morphType, &mask, &anchor, &iteration, &borderType] - (nvbench::launch &launch) - { - op(launch.get_stream(), src, dst, workspace, morphType, mask, anchor, iteration, borderType); - }); - } - else // zero and positive var shape means use ImageBatchVarShape - { - nvcv::ImageBatchVarShape src(shape.x); - nvcv::ImageBatchVarShape dst(shape.x); - - benchutils::FillImageBatch(src, long2{shape.z, shape.y}, long2{varShape, varShape}, - benchutils::RandomValues()); - dst.pushBack(src.begin(), src.end()); - - nvcv::Tensor maskTensor({{shape.x}, "N"}, nvcv::TYPE_2S32); - nvcv::Tensor anchorTensor({{shape.x}, "N"}, nvcv::TYPE_2S32); - - benchutils::FillTensor(maskTensor, [&mask](const long4_16a &){ return int2{mask.w, mask.h}; }); - benchutils::FillTensor(anchorTensor, [&anchor](const long4_16a &){ return anchor; }); - - nvcv::ImageBatchVarShape workspace{nullptr}; - - if (morphType == NVCV_OPEN || morphType == NVCV_CLOSE || iteration > 1) - { - workspace = nvcv::ImageBatchVarShape(shape.x); - - workspace.pushBack(dst.begin(), dst.end()); - } - - state.exec(nvbench::exec_tag::sync, - [&op, &src, &dst, &workspace, &morphType, &maskTensor, &anchorTensor, &iteration, &borderType] - (nvbench::launch &launch) - { - op(launch.get_stream(), src, dst, workspace, morphType, maskTensor, anchorTensor, iteration, borderType); - }); - } -} -catch (const std::exception &err) -{ - state.skip(err.what()); -} - -// clang-format on - -using MorphologyTypes = nvbench::type_list; - -NVBENCH_BENCH_TYPES(Morphology, NVBENCH_TYPE_AXES(MorphologyTypes)) - .set_type_axes_names({"InOutDataType"}) - .add_string_axis("shape", {"1x1080x1920"}) - .add_int64_axis("varShape", {-1, 0}) - .add_int64_axis("iteration", {1}) - .add_string_axis("kernelSize", {"3x3"}) - .add_string_axis("morphType", {"ERODE", "DILATE", "OPEN", "CLOSE"}) - .add_string_axis("border", {"REPLICATE"}); diff --git a/bench/BenchNMS.cpp b/bench/BenchNMS.cpp deleted file mode 100644 index bad16a31e..000000000 --- a/bench/BenchNMS.cpp +++ /dev/null @@ -1,76 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed 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. - */ - -#include "BenchUtils.hpp" - -#include - -#include - -template -inline void NMS(nvbench::state &state, nvbench::type_list) -try -{ - long2 shape = benchutils::GetShape<2>(state.get_string("shape")); - long varShape = state.get_int64("varShape"); - float scThr = static_cast(state.get_float64("scoreThreshold")); - float iouThr = static_cast(state.get_float64("iouThreshold")); - - // R/W bandwidth rationale: - // 1 read of scores (F32) to mask out lower scores boxes + 1 read of boxes (4S16) for IoU threshold - // 2 writes of masks (U8) by score and IoU thresholds - state.add_global_memory_reads(shape.x * shape.y * (sizeof(T) + sizeof(S))); - state.add_global_memory_writes(shape.x * shape.y * sizeof(M) * 2); - - cvcuda::NonMaximumSuppression op; - - // clang-format off - - if (varShape < 0) // negative var shape means use Tensor - { - nvcv::Tensor srcBB({{shape.x, shape.y}, "NB"}, benchutils::GetDataType()); - nvcv::Tensor srcSc({{shape.x, shape.y}, "NB"}, benchutils::GetDataType()); - nvcv::Tensor dstMk({{shape.x, shape.y}, "NB"}, benchutils::GetDataType()); - - benchutils::FillTensor(srcBB, benchutils::RandomValues(10, 50)); - benchutils::FillTensor(srcSc, benchutils::RandomValues()); - - state.exec(nvbench::exec_tag::sync, [&op, &srcBB, &dstMk, &srcSc, &scThr, &iouThr](nvbench::launch &launch) - { - op(launch.get_stream(), srcBB, dstMk, srcSc, scThr, iouThr); - }); - } - else // zero and positive var shape means use ImageBatchVarShape - { - throw std::invalid_argument("ImageBatchVarShape not implemented for this operator"); - } -} -catch (const std::exception &err) -{ - state.skip(err.what()); -} - -// clang-format on - -using NMSTypes = nvbench::type_list; - -NVBENCH_BENCH_TYPES(NMS, NVBENCH_TYPE_AXES(NMSTypes)) - .set_type_axes_names({"InOutDataType"}) - .add_string_axis("shape", {"1x1024", "32x1024"}) - .add_int64_axis("varShape", {-1}) - .add_float64_axis("scoreThreshold", {0.5}) - .add_float64_axis("iouThreshold", {0.75}); diff --git a/bench/BenchNormalize.cpp b/bench/BenchNormalize.cpp deleted file mode 100644 index ae7003c4d..000000000 --- a/bench/BenchNormalize.cpp +++ /dev/null @@ -1,99 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed 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. - */ - -#include "BenchUtils.hpp" - -#include - -#include - -template -inline void Normalize(nvbench::state &state, nvbench::type_list) -try -{ - long3 srcShape = benchutils::GetShape<3>(state.get_string("shape")); - long varShape = state.get_int64("varShape"); - long3 dstShape = srcShape; - - float globalScale = 1.234f; - float globalShift = 2.345f; - float epsilon = 12.34f; - uint32_t flags = CVCUDA_NORMALIZE_SCALE_IS_STDDEV; - - long3 baseShape{srcShape.x, 1, 1}; - long3 scaleShape{srcShape.x, 1, 1}; - - state.add_global_memory_reads(srcShape.x * srcShape.y * srcShape.z * sizeof(T) - + baseShape.x * baseShape.y * baseShape.z * sizeof(float) - + scaleShape.x * scaleShape.y * scaleShape.z * sizeof(float)); - state.add_global_memory_writes(dstShape.x * dstShape.y * dstShape.z * sizeof(T)); - - cvcuda::Normalize op; - - // clang-format off - - nvcv::Tensor base({{baseShape.x, baseShape.y, baseShape.z, 1}, "NHWC"}, nvcv::TYPE_F32); - nvcv::Tensor scale({{scaleShape.x, scaleShape.y, scaleShape.z, 1}, "NHWC"}, nvcv::TYPE_F32); - - benchutils::FillTensor(base, benchutils::RandomValues()); - benchutils::FillTensor(scale, benchutils::RandomValues(0.f, 1.f)); - - if (varShape < 0) // negative var shape means use Tensor - { - nvcv::Tensor src({{srcShape.x, srcShape.y, srcShape.z, 1}, "NHWC"}, benchutils::GetDataType()); - nvcv::Tensor dst({{dstShape.x, dstShape.y, dstShape.z, 1}, "NHWC"}, benchutils::GetDataType()); - - benchutils::FillTensor(src, benchutils::RandomValues()); - - state.exec(nvbench::exec_tag::sync, - [&op, &src, &base, &scale, &dst, &globalScale, &globalShift, &epsilon, &flags] - (nvbench::launch &launch) - { - op(launch.get_stream(), src, base, scale, dst, globalScale, globalShift, epsilon, flags); - }); - } - else // zero and positive var shape means use ImageBatchVarShape - { - nvcv::ImageBatchVarShape src(srcShape.x); - nvcv::ImageBatchVarShape dst(dstShape.x); - - benchutils::FillImageBatch(src, long2{srcShape.z, srcShape.y}, long2{varShape, varShape}, - benchutils::RandomValues()); - benchutils::FillImageBatch(dst, long2{dstShape.z, dstShape.y}, long2{varShape, varShape}, - benchutils::RandomValues()); - - state.exec(nvbench::exec_tag::sync, - [&op, &src, &base, &scale, &dst, &globalScale, &globalShift, &epsilon, &flags] - (nvbench::launch &launch) - { - op(launch.get_stream(), src, base, scale, dst, globalScale, globalShift, epsilon, flags); - }); - } -} -catch (const std::exception &err) -{ - state.skip(err.what()); -} - -// clang-format on - -using NormalizeTypes = nvbench::type_list; - -NVBENCH_BENCH_TYPES(Normalize, NVBENCH_TYPE_AXES(NormalizeTypes)) - .set_type_axes_names({"InOutDataType"}) - .add_string_axis("shape", {"1x1080x1920"}) - .add_int64_axis("varShape", {-1, 0}); diff --git a/bench/BenchOSD.cpp b/bench/BenchOSD.cpp deleted file mode 100644 index b5e9b0a65..000000000 --- a/bench/BenchOSD.cpp +++ /dev/null @@ -1,94 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed 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. - */ - -#include "BenchUtils.hpp" - -#include <../priv/Types.hpp> -#include - -#include - -template -inline void OSD(nvbench::state &state, nvbench::type_list) -try -{ - long3 shape = benchutils::GetShape<3>(state.get_string("shape")); - long varShape = state.get_int64("varShape"); - int numElem = static_cast(state.get_int64("numElem")); - - int ch = nvcv::cuda::NumElements; - - using BT = nvcv::cuda::BaseType; - - std::vector>> elementVec; - - for (int n = 0; n < (int)shape.x; n++) - { - std::vector> curVec; - for (int i = 0; i < numElem; i++) - { - NVCVPoint point; - point.centerPos.x = shape.z / 2; - point.centerPos.y = shape.y / 2; - point.radius = std::min(shape.z, shape.y) / 2; - point.color = {0, 0, 0, 255}; - auto element = std::make_shared(NVCVOSDType::NVCV_OSD_POINT, &point); - curVec.push_back(element); - } - elementVec.push_back(curVec); - } - - std::shared_ptr ctx = std::make_shared(elementVec); - - state.add_global_memory_reads(shape.x * shape.y * shape.z * sizeof(T) + numElem * sizeof(int) * 16); - state.add_global_memory_writes(shape.x * shape.y * shape.z * sizeof(T)); - - cvcuda::OSD op; - - // clang-format off - - if (varShape < 0) // negative var shape means use Tensor - { - nvcv::Tensor src({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); - nvcv::Tensor dst({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); - - benchutils::FillTensor(src, benchutils::RandomValues()); - - state.exec(nvbench::exec_tag::sync, [&op, &src, &dst, &ctx](nvbench::launch &launch) - { - op(launch.get_stream(), src, dst, (NVCVElements)ctx.get()); - }); - } - else // zero and positive var shape means use ImageBatchVarShape - { - throw std::invalid_argument("ImageBatchVarShape not implemented for this operator"); - } -} -catch (const std::exception &err) -{ - state.skip(err.what()); -} - -// clang-format on - -using OSDTypes = nvbench::type_list; - -NVBENCH_BENCH_TYPES(OSD, NVBENCH_TYPE_AXES(OSDTypes)) - .set_type_axes_names({"InOutDataType"}) - .add_string_axis("shape", {"1x1080x1920"}) - .add_int64_axis("varShape", {-1}) - .add_int64_axis("numElem", {100}); diff --git a/bench/BenchPadAndStack.cpp b/bench/BenchPadAndStack.cpp deleted file mode 100644 index ba0f900af..000000000 --- a/bench/BenchPadAndStack.cpp +++ /dev/null @@ -1,82 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed 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. - */ - -#include "BenchUtils.hpp" - -#include - -#include - -template -inline void PadAndStack(nvbench::state &state, nvbench::type_list) -try -{ - long3 srcShape = benchutils::GetShape<3>(state.get_string("shape")); - long varShape = state.get_int64("varShape"); - long3 dstShape = srcShape; - - NVCVBorderType borderType = benchutils::GetBorderType(state.get_string("border")); - - float borderValue{0.f}; - - state.add_global_memory_reads(srcShape.x * srcShape.y * srcShape.z * sizeof(T) + srcShape.x * sizeof(int) * 2); - state.add_global_memory_writes(dstShape.x * dstShape.y * dstShape.z * sizeof(T)); - - cvcuda::PadAndStack op; - - // clang-format off - - nvcv::Tensor dst({{dstShape.x, dstShape.y, dstShape.z, 1}, "NHWC"}, benchutils::GetDataType()); - - nvcv::Tensor top({{srcShape.x, 1, 1, 1}, "NHWC"}, nvcv::TYPE_S32); - nvcv::Tensor left({{srcShape.x, 1, 1, 1}, "NHWC"}, nvcv::TYPE_S32); - - benchutils::FillTensor(top, [&srcShape](const long4_16a &){ return srcShape.y / 2; }); - benchutils::FillTensor(left, [&srcShape](const long4_16a &){ return srcShape.z / 2; }); - - if (varShape < 0) // negative var shape means use Tensor - { - throw std::invalid_argument("Tensor not implemented for this operator"); - } - else // zero and positive var shape means use ImageBatchVarShape - { - nvcv::ImageBatchVarShape src(srcShape.x); - - benchutils::FillImageBatch(src, long2{srcShape.z, srcShape.y}, long2{varShape, varShape}, - benchutils::RandomValues()); - - state.exec(nvbench::exec_tag::sync, - [&op, &src, &dst, &top, &left, &borderType, &borderValue](nvbench::launch &launch) - { - op(launch.get_stream(), src, dst, top, left, borderType, borderValue); - }); - } -} -catch (const std::exception &err) -{ - state.skip(err.what()); -} - -// clang-format on - -using PadAndStackTypes = nvbench::type_list; - -NVBENCH_BENCH_TYPES(PadAndStack, NVBENCH_TYPE_AXES(PadAndStackTypes)) - .set_type_axes_names({"InOutDataType"}) - .add_string_axis("shape", {"1x1080x1920"}) - .add_int64_axis("varShape", {0}) - .add_string_axis("border", {"REFLECT101"}); diff --git a/bench/BenchPillowResize.cpp b/bench/BenchPillowResize.cpp deleted file mode 100644 index 6dd5ae67d..000000000 --- a/bench/BenchPillowResize.cpp +++ /dev/null @@ -1,105 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed 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. - */ - -#include "BenchUtils.hpp" - -#include - -#include - -template -inline void PillowResize(nvbench::state &state, nvbench::type_list) -try -{ - long3 srcShape = benchutils::GetShape<3>(state.get_string("shape")); - long varShape = state.get_int64("varShape"); - - NVCVInterpolationType interpType = benchutils::GetInterpolationType(state.get_string("interpolation")); - - long3 dstShape; - - if (state.get_string("resizeType") == "EXPAND") - { - dstShape = long3{srcShape.x, srcShape.y * 2, srcShape.z * 2}; - } - else if (state.get_string("resizeType") == "CONTRACT") - { - dstShape = long3{srcShape.x, srcShape.y / 2, srcShape.z / 2}; - } - else - { - throw std::invalid_argument("Invalid resizeType = " + state.get_string("resizeType")); - } - - nvcv::Size2D srcSize{(int)srcShape.z, (int)srcShape.y}; - nvcv::Size2D dstSize{(int)dstShape.z, (int)dstShape.y}; - - nvcv::DataType dtype{benchutils::GetDataType()}; - nvcv::ImageFormat fmt(nvcv::MemLayout::PITCH_LINEAR, dtype.dataKind(), nvcv::Swizzle::S_X000, dtype.packing()); - - state.add_global_memory_reads(srcShape.x * srcShape.y * srcShape.z * sizeof(T)); - state.add_global_memory_writes(dstShape.x * dstShape.y * dstShape.z * sizeof(T)); - - cvcuda::PillowResize op; - cvcuda::UniqueWorkspace ws - = cvcuda::AllocateWorkspace(op.getWorkspaceRequirements(srcShape.x, srcSize, dstSize, fmt)); - - // clang-format off - - if (varShape < 0) // negative var shape means use Tensor - { - nvcv::Tensor src({{srcShape.x, srcShape.y, srcShape.z, 1}, "NHWC"}, dtype); - nvcv::Tensor dst({{dstShape.x, dstShape.y, dstShape.z, 1}, "NHWC"}, dtype); - - benchutils::FillTensor(src, benchutils::RandomValues()); - - state.exec(nvbench::exec_tag::sync, [&op, &ws, &src, &dst, &interpType](nvbench::launch &launch) - { - op(launch.get_stream(), ws.get(), src, dst, interpType); - }); - } - else // zero and positive var shape means use ImageBatchVarShape - { - nvcv::ImageBatchVarShape src(srcShape.x); - nvcv::ImageBatchVarShape dst(dstShape.x); - - benchutils::FillImageBatch(src, long2{srcShape.z, srcShape.y}, long2{varShape, varShape}, - benchutils::RandomValues()); - benchutils::FillImageBatch(dst, long2{dstShape.z, dstShape.y}, long2{varShape, varShape}, - benchutils::RandomValues()); - - state.exec(nvbench::exec_tag::sync, [&op, &ws, &src, &dst, &interpType](nvbench::launch &launch) - { - op(launch.get_stream(), ws.get(), src, dst, interpType); - }); - } -} -catch (const std::exception &err) -{ - state.skip(err.what()); -} - -// clang-format on - -using PillowResizeTypes = nvbench::type_list; - -NVBENCH_BENCH_TYPES(PillowResize, NVBENCH_TYPE_AXES(PillowResizeTypes)) - .set_type_axes_names({"InOutDataType"}) - .add_string_axis("shape", {"1x1080x1920"}) - .add_int64_axis("varShape", {-1, 0}) - .add_string_axis("resizeType", {"CONTRACT"}) - .add_string_axis("interpolation", {"CUBIC"}); diff --git a/bench/BenchRandomResizedCrop.cpp b/bench/BenchRandomResizedCrop.cpp deleted file mode 100644 index 55161a982..000000000 --- a/bench/BenchRandomResizedCrop.cpp +++ /dev/null @@ -1,103 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed 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. - */ - -#include "BenchUtils.hpp" - -#include - -#include - -template -inline void RandomResizedCrop(nvbench::state &state, nvbench::type_list) -try -{ - long3 srcShape = benchutils::GetShape<3>(state.get_string("shape")); - long varShape = state.get_int64("varShape"); - - NVCVInterpolationType interpType = benchutils::GetInterpolationType(state.get_string("interpolation")); - - long3 dstShape; - - if (state.get_string("resizeType") == "EXPAND") - { - dstShape = long3{srcShape.x, srcShape.y * 2, srcShape.z * 2}; - } - else if (state.get_string("resizeType") == "CONTRACT") - { - dstShape = long3{srcShape.x, srcShape.y / 2, srcShape.z / 2}; - } - else - { - throw std::invalid_argument("Invalid resizeType = " + state.get_string("resizeType")); - } - - double minScale = 0.08; - double maxScale = 1.0; - double minRatio = 0.5; - double maxRatio = 2.0; - uint32_t seed = 1234; - - state.add_global_memory_reads(srcShape.x * srcShape.y * srcShape.z * sizeof(T)); - state.add_global_memory_writes(dstShape.x * dstShape.y * dstShape.z * sizeof(T)); - - cvcuda::RandomResizedCrop op(minScale, maxScale, minRatio, maxRatio, srcShape.x, seed); - - // clang-format off - - if (varShape < 0) // negative var shape means use Tensor - { - nvcv::Tensor src({{srcShape.x, srcShape.y, srcShape.z, 1}, "NHWC"}, benchutils::GetDataType()); - nvcv::Tensor dst({{dstShape.x, dstShape.y, dstShape.z, 1}, "NHWC"}, benchutils::GetDataType()); - - benchutils::FillTensor(src, benchutils::RandomValues()); - - state.exec(nvbench::exec_tag::sync, [&op, &src, &dst, &interpType](nvbench::launch &launch) - { - op(launch.get_stream(), src, dst, interpType); - }); - } - else // zero and positive var shape means use ImageBatchVarShape - { - nvcv::ImageBatchVarShape src(srcShape.x); - nvcv::ImageBatchVarShape dst(dstShape.x); - - benchutils::FillImageBatch(src, long2{srcShape.z, srcShape.y}, long2{varShape, varShape}, - benchutils::RandomValues()); - benchutils::FillImageBatch(dst, long2{dstShape.z, dstShape.y}, long2{varShape, varShape}, - benchutils::RandomValues()); - - state.exec(nvbench::exec_tag::sync, [&op, &src, &dst, &interpType](nvbench::launch &launch) - { - op(launch.get_stream(), src, dst, interpType); - }); - } -} -catch (const std::exception &err) -{ - state.skip(err.what()); -} - -// clang-format on - -using RandomResizedCropTypes = nvbench::type_list; - -NVBENCH_BENCH_TYPES(RandomResizedCrop, NVBENCH_TYPE_AXES(RandomResizedCropTypes)) - .set_type_axes_names({"InOutDataType"}) - .add_string_axis("shape", {"1x1080x1920"}) - .add_int64_axis("varShape", {-1, 0}) - .add_string_axis("resizeType", {"EXPAND"}) - .add_string_axis("interpolation", {"LINEAR"}); diff --git a/bench/BenchReformat.cpp b/bench/BenchReformat.cpp deleted file mode 100644 index 26e8f28c2..000000000 --- a/bench/BenchReformat.cpp +++ /dev/null @@ -1,67 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed 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. - */ - -#include "BenchUtils.hpp" - -#include - -#include - -template -inline void Reformat(nvbench::state &state, nvbench::type_list) -try -{ - long3 shape = benchutils::GetShape<3>(state.get_string("shape")); - long varShape = state.get_int64("varShape"); - - state.add_global_memory_reads(shape.x * shape.y * shape.z * sizeof(T)); - state.add_global_memory_writes(shape.x * shape.y * shape.z * sizeof(T)); - - cvcuda::Reformat op; - - // clang-format off - - if (varShape < 0) // negative var shape means use Tensor - { - nvcv::Tensor src({{shape.x, 1, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); - nvcv::Tensor dst({{shape.x, shape.y, shape.z, 1}, "NHWC"}, benchutils::GetDataType()); - - benchutils::FillTensor(src, benchutils::RandomValues()); - - state.exec(nvbench::exec_tag::sync, [&op, &src, &dst](nvbench::launch &launch) - { - op(launch.get_stream(), src, dst); - }); - } - else // zero and positive var shape means use ImageBatchVarShape - { - throw std::invalid_argument("ImageBatchVarShape not implemented for this operator"); - } -} -catch (const std::exception &err) -{ - state.skip(err.what()); -} - -// clang-format on - -using ReformatTypes = nvbench::type_list; - -NVBENCH_BENCH_TYPES(Reformat, NVBENCH_TYPE_AXES(ReformatTypes)) - .set_type_axes_names({"InOutDataType"}) - .add_string_axis("shape", {"1x1080x1920"}) - .add_int64_axis("varShape", {-1}); diff --git a/bench/BenchRemap.cpp b/bench/BenchRemap.cpp deleted file mode 100644 index 4f35e016d..000000000 --- a/bench/BenchRemap.cpp +++ /dev/null @@ -1,120 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed 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. - */ - -#include "BenchUtils.hpp" - -#include - -#include - -template -inline void Remap(nvbench::state &state, nvbench::type_list) -try -{ - long3 srcShape = benchutils::GetShape<3>(state.get_string("shape")); - long varShape = state.get_int64("varShape"); - long3 dstShape = srcShape; - long3 mapShape; - - NVCVInterpolationType srcInterp, mapInterp; - NVCVBorderType borderType; - NVCVRemapMapValueType mapValueType; - - bool alignCorners{true}; - float4 borderValue{0, 0, 0, 0}; - - if (state.get_string("mapType") == "DENSE") - { - srcInterp = NVCV_INTERP_NEAREST; - mapInterp = NVCV_INTERP_NEAREST; - borderType = NVCV_BORDER_CONSTANT; - mapValueType = NVCV_REMAP_ABSOLUTE_NORMALIZED; - mapShape = srcShape; - } - else if (state.get_string("mapType") == "RELATIVE") - { - srcInterp = NVCV_INTERP_CUBIC; - mapInterp = NVCV_INTERP_CUBIC; - borderType = NVCV_BORDER_REFLECT101; - mapValueType = NVCV_REMAP_RELATIVE_NORMALIZED; - mapShape = long3{srcShape.x, 4, 4}; - } - else - { - throw std::invalid_argument("Invalid mapType = " + state.get_string("mapType")); - } - - state.add_global_memory_reads(srcShape.x * srcShape.y * srcShape.z * sizeof(T) - + mapShape.x * mapShape.y * mapShape.z * sizeof(float2)); - state.add_global_memory_writes(dstShape.x * dstShape.y * dstShape.z * sizeof(T)); - - cvcuda::Remap op; - - // clang-format off - - nvcv::Tensor map({{mapShape.x, mapShape.y, mapShape.z, 1}, "NHWC"}, nvcv::TYPE_2F32); - - benchutils::FillTensor(map, benchutils::RandomValues()); - - if (varShape < 0) // negative var shape means use Tensor - { - nvcv::Tensor src({{srcShape.x, srcShape.y, srcShape.z, 1}, "NHWC"}, benchutils::GetDataType()); - nvcv::Tensor dst({{dstShape.x, dstShape.y, dstShape.z, 1}, "NHWC"}, benchutils::GetDataType()); - - benchutils::FillTensor(src, benchutils::RandomValues()); - - state.exec(nvbench::exec_tag::sync, - [&op, &src, &dst, &map, &srcInterp, &mapInterp, &mapValueType, &alignCorners, &borderType, - &borderValue](nvbench::launch &launch) - { - op(launch.get_stream(), src, dst, map, srcInterp, mapInterp, mapValueType, alignCorners, borderType, - borderValue); - }); - } - else // zero and positive var shape means use ImageBatchVarShape - { - nvcv::ImageBatchVarShape src(srcShape.x); - nvcv::ImageBatchVarShape dst(dstShape.x); - - benchutils::FillImageBatch(src, long2{srcShape.z, srcShape.y}, long2{varShape, varShape}, - benchutils::RandomValues()); - benchutils::FillImageBatch(dst, long2{dstShape.z, dstShape.y}, long2{varShape, varShape}, - benchutils::RandomValues()); - - state.exec(nvbench::exec_tag::sync, - [&op, &src, &dst, &map, &srcInterp, &mapInterp, &mapValueType, &alignCorners, &borderType, - &borderValue](nvbench::launch &launch) - { - op(launch.get_stream(), src, dst, map, srcInterp, mapInterp, mapValueType, alignCorners, borderType, - borderValue); - }); - } -} -catch (const std::exception &err) -{ - state.skip(err.what()); -} - -// clang-format on - -using RemapTypes = nvbench::type_list; - -NVBENCH_BENCH_TYPES(Remap, NVBENCH_TYPE_AXES(RemapTypes)) - .set_type_axes_names({"InOutDataType"}) - .add_string_axis("shape", {"1x1080x1920"}) - .add_int64_axis("varShape", {-1, 0}) - .add_string_axis("mapType", {"DENSE"}); diff --git a/bench/BenchResize.cpp b/bench/BenchResize.cpp deleted file mode 100644 index 9cb608bc9..000000000 --- a/bench/BenchResize.cpp +++ /dev/null @@ -1,97 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed 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. - */ - -#include "BenchUtils.hpp" - -#include - -#include - -template -inline void Resize(nvbench::state &state, nvbench::type_list) -try -{ - long3 srcShape = benchutils::GetShape<3>(state.get_string("shape")); - long varShape = state.get_int64("varShape"); - - NVCVInterpolationType interpType = benchutils::GetInterpolationType(state.get_string("interpolation")); - - long3 dstShape; - - if (state.get_string("resizeType") == "EXPAND") - { - dstShape = long3{srcShape.x, srcShape.y * 2, srcShape.z * 2}; - } - else if (state.get_string("resizeType") == "CONTRACT") - { - dstShape = long3{srcShape.x, srcShape.y / 2, srcShape.z / 2}; - } - else - { - throw std::invalid_argument("Invalid resizeType = " + state.get_string("resizeType")); - } - - state.add_global_memory_reads(srcShape.x * srcShape.y * srcShape.z * sizeof(T)); - state.add_global_memory_writes(dstShape.x * dstShape.y * dstShape.z * sizeof(T)); - - cvcuda::Resize op; - - // clang-format off - - if (varShape < 0) // negative var shape means use Tensor - { - nvcv::Tensor src({{srcShape.x, srcShape.y, srcShape.z, 1}, "NHWC"}, benchutils::GetDataType()); - nvcv::Tensor dst({{dstShape.x, dstShape.y, dstShape.z, 1}, "NHWC"}, benchutils::GetDataType()); - - benchutils::FillTensor(src, benchutils::RandomValues()); - - state.exec(nvbench::exec_tag::sync, [&op, &src, &dst, &interpType](nvbench::launch &launch) - { - op(launch.get_stream(), src, dst, interpType); - }); - } - else // zero and positive var shape means use ImageBatchVarShape - { - nvcv::ImageBatchVarShape src(srcShape.x); - nvcv::ImageBatchVarShape dst(dstShape.x); - - benchutils::FillImageBatch(src, long2{srcShape.z, srcShape.y}, long2{varShape, varShape}, - benchutils::RandomValues()); - benchutils::FillImageBatch(dst, long2{dstShape.z, dstShape.y}, long2{varShape, varShape}, - benchutils::RandomValues()); - - state.exec(nvbench::exec_tag::sync, [&op, &src, &dst, &interpType](nvbench::launch &launch) - { - op(launch.get_stream(), src, dst, interpType); - }); - } -} -catch (const std::exception &err) -{ - state.skip(err.what()); -} - -// clang-format on - -using ResizeTypes = nvbench::type_list; - -NVBENCH_BENCH_TYPES(Resize, NVBENCH_TYPE_AXES(ResizeTypes)) - .set_type_axes_names({"InOutDataType"}) - .add_string_axis("shape", {"1x1080x1920"}) - .add_int64_axis("varShape", {-1, 0}) - .add_string_axis("resizeType", {"EXPAND"}) - .add_string_axis("interpolation", {"LINEAR"}); diff --git a/bench/BenchResizeCropConvertReformat.cpp b/bench/BenchResizeCropConvertReformat.cpp deleted file mode 100644 index b9e9e3125..000000000 --- a/bench/BenchResizeCropConvertReformat.cpp +++ /dev/null @@ -1,124 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed 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. - */ - -#include "BenchUtils.hpp" - -#include -#include - -#include - -template -inline void ResizeCropConvertReformat(nvbench::state &state, nvbench::type_list) -try -{ - long3 srcShape = benchutils::GetShape<3>(state.get_string("shape")); - long varShape = state.get_int64("varShape"); - - NVCVInterpolationType interpType = benchutils::GetInterpolationType(state.get_string("interpolation")); - - int2 cropPos{1, 1}; - - NVCVSize2D resize; - - if (state.get_string("resizeType") == "EXPAND") - { - resize = NVCVSize2D{(int)(srcShape.y * 2), (int)(srcShape.z * 2)}; - } - else if (state.get_string("resizeType") == "CONTRACT") - { - resize = NVCVSize2D{(int)(srcShape.y / 2), (int)(srcShape.z / 2)}; - } - else - { - throw std::invalid_argument("Invalid resizeType = " + state.get_string("resizeType")); - } - - NVCVChannelManip manip; - - if (state.get_string("manip") == "NO_OP") - { - manip = NVCV_CHANNEL_NO_OP; - } - else if (state.get_string("manip") == "REVERSE") - { - manip = NVCV_CHANNEL_REVERSE; - } - else - { - throw std::invalid_argument("Invalid channel manipulation = " + state.get_string("manip")); - } - - using BT = nvcv::cuda::BaseType; - long nc = nvcv::cuda::NumElements; - - long3 dstShape{srcShape.x, resize.h - cropPos.y, resize.w - cropPos.x}; - - if (dstShape.y <= 0 || dstShape.z <= 0) - { - throw std::invalid_argument("Invalid shape and resizeType"); - } - - state.add_global_memory_reads(srcShape.x * srcShape.y * srcShape.z * sizeof(T)); - state.add_global_memory_writes(dstShape.x * dstShape.y * dstShape.z * sizeof(T)); - - cvcuda::ResizeCropConvertReformat op; - - // clang-format off - - if (varShape < 0) // negative var shape means use Tensor - { - nvcv::Tensor src({{srcShape.x, srcShape.y, srcShape.z, nc}, "NHWC"}, benchutils::GetDataType()); - nvcv::Tensor dst({{dstShape.x, dstShape.y, dstShape.z, nc}, "NHWC"}, benchutils::GetDataType()); - - benchutils::FillTensor(src, benchutils::RandomValues()); - - state.exec(nvbench::exec_tag::sync, [&op, &src, &dst, &resize, &interpType, &cropPos, &manip](nvbench::launch &launch) - { - op(launch.get_stream(), src, dst, resize, interpType, cropPos, manip); - }); - } - else // zero and positive var shape means use ImageBatchVarShape - { - nvcv::ImageBatchVarShape src(srcShape.x); - nvcv::Tensor dst({{dstShape.x, dstShape.y, dstShape.z, nc}, "NHWC"}, benchutils::GetDataType()); - - benchutils::FillImageBatch(src, long2{srcShape.z, srcShape.y}, long2{varShape, varShape}, - benchutils::RandomValues()); - - state.exec(nvbench::exec_tag::sync, [&op, &src, &dst, &resize, &interpType, &cropPos, &manip](nvbench::launch &launch) - { - op(launch.get_stream(), src, dst, resize, interpType, cropPos, manip); - }); - } -} -catch (const std::exception &err) -{ - state.skip(err.what()); -} - -// clang-format on - -using ResizeCropConvertReformatTypes = nvbench::type_list; - -NVBENCH_BENCH_TYPES(ResizeCropConvertReformat, NVBENCH_TYPE_AXES(ResizeCropConvertReformatTypes)) - .set_type_axes_names({"InOutDataType"}) - .add_string_axis("shape", {"1x1080x1920"}) - .add_int64_axis("varShape", {-1, 0}) - .add_string_axis("resizeType", {"EXPAND"}) - .add_string_axis("manip", {"NO_OP"}) - .add_string_axis("interpolation", {"LINEAR"}); diff --git a/bench/BenchRotate.cpp b/bench/BenchRotate.cpp deleted file mode 100644 index a567869f8..000000000 --- a/bench/BenchRotate.cpp +++ /dev/null @@ -1,91 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed 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. - */ - -#include "BenchUtils.hpp" - -#include - -#include - -template -inline void Rotate(nvbench::state &state, nvbench::type_list) -try -{ - long3 shape = benchutils::GetShape<3>(state.get_string("shape")); - long varShape = state.get_int64("varShape"); - - NVCVInterpolationType interpType = benchutils::GetInterpolationType(state.get_string("interpolation")); - - double angleDeg = 123.456; - double2 shift{12.34, 12.34}; - - state.add_global_memory_reads(shape.x * shape.y * shape.z * sizeof(T)); - state.add_global_memory_writes(shape.x * shape.y * shape.z * sizeof(T)); - - cvcuda::Rotate op(shape.x); - - // clang-format off - - if (varShape < 0) // negative var shape means use Tensor - { - nvcv::Tensor src({{shape.x, shape.y, shape.z, 1}, "NHWC"}, benchutils::GetDataType()); - nvcv::Tensor dst({{shape.x, shape.y, shape.z, 1}, "NHWC"}, benchutils::GetDataType()); - - benchutils::FillTensor(src, benchutils::RandomValues()); - - state.exec(nvbench::exec_tag::sync, [&op, &src, &dst, &angleDeg, &shift, &interpType](nvbench::launch &launch) - { - op(launch.get_stream(), src, dst, angleDeg, shift, interpType); - }); - } - else // zero and positive var shape means use ImageBatchVarShape - { - nvcv::ImageBatchVarShape src(shape.x); - nvcv::ImageBatchVarShape dst(shape.x); - - benchutils::FillImageBatch(src, long2{shape.z, shape.y}, long2{varShape, varShape}, - benchutils::RandomValues()); - dst.pushBack(src.begin(), src.end()); - - nvcv::Tensor angleDegTensor({{shape.x}, "N"}, nvcv::TYPE_F64); - nvcv::Tensor shiftTensor({{shape.x, 2}, "NW"}, nvcv::TYPE_F64); - - benchutils::FillTensor(angleDegTensor, [&angleDeg](const long4_16a &){ return angleDeg; }); - benchutils::FillTensor(shiftTensor, - [&shift](const long4_16a &c){ return nvcv::cuda::GetElement(shift, c.y); }); - - state.exec(nvbench::exec_tag::sync, - [&op, &src, &dst, &angleDegTensor, &shiftTensor, &interpType](nvbench::launch &launch) - { - op(launch.get_stream(), src, dst, angleDegTensor, shiftTensor, interpType); - }); - } -} -catch (const std::exception &err) -{ - state.skip(err.what()); -} - -// clang-format on - -using RotateTypes = nvbench::type_list; - -NVBENCH_BENCH_TYPES(Rotate, NVBENCH_TYPE_AXES(RotateTypes)) - .set_type_axes_names({"InOutDataType"}) - .add_string_axis("shape", {"1x1080x1920"}) - .add_int64_axis("varShape", {-1, 0}) - .add_string_axis("interpolation", {"CUBIC"}); diff --git a/bench/BenchStack.cpp b/bench/BenchStack.cpp deleted file mode 100644 index ebd3c0797..000000000 --- a/bench/BenchStack.cpp +++ /dev/null @@ -1,68 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed 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. - */ - -#include "BenchUtils.hpp" - -#include - -#include - -template -inline void Stack(nvbench::state &state, nvbench::type_list) -try -{ - long3 shape = benchutils::GetShape<3>(state.get_string("shape")); - - using BT = typename nvcv::cuda::BaseType; - - int ch = nvcv::cuda::NumElements; - - state.add_global_memory_reads(shape.x * shape.y * shape.z * sizeof(T)); - state.add_global_memory_writes(shape.x * shape.y * shape.z * sizeof(T)); - - cvcuda::Stack op; - - // clang-format off - - nvcv::TensorBatch src(nvcv::TensorBatch::CalcRequirements(shape.x)); - nvcv::Tensor dst({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); - - for (int i = 0 ; i < shape.x; i++) - { - nvcv::Tensor srcIn({{shape.y, shape.z, ch}, "HWC"}, benchutils::GetDataType()); - benchutils::FillTensor(srcIn, benchutils::RandomValues()); - src.pushBack(srcIn); - } - - state.exec(nvbench::exec_tag::sync, [&op, &src, &dst](nvbench::launch &launch) - { - op(launch.get_stream(), src, dst); - }); - -} -catch (const std::exception &err) -{ - state.skip(err.what()); -} - -// clang-format on - -using StackTypes = nvbench::type_list; - -NVBENCH_BENCH_TYPES(Stack, NVBENCH_TYPE_AXES(StackTypes)) - .set_type_axes_names({"InOutDataType"}) - .add_string_axis("shape", {"10x1080x1920"}); diff --git a/bench/BenchThreshold.cpp b/bench/BenchThreshold.cpp deleted file mode 100644 index bfc438623..000000000 --- a/bench/BenchThreshold.cpp +++ /dev/null @@ -1,85 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed 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. - */ - -#include "BenchUtils.hpp" - -#include - -#include - -template -inline void Threshold(nvbench::state &state, nvbench::type_list) -try -{ - long3 shape = benchutils::GetShape<3>(state.get_string("shape")); - long varShape = state.get_int64("varShape"); - - uint32_t threshType = NVCV_THRESH_BINARY | (std::is_same_v ? NVCV_THRESH_OTSU : 0); - - state.add_global_memory_reads(shape.x * shape.y * shape.z * sizeof(T)); - state.add_global_memory_writes(shape.x * shape.y * shape.z * sizeof(T)); - - cvcuda::Threshold op(threshType, shape.x); - - // clang-format off - - nvcv::Tensor thresh({{shape.x}, "N"}, nvcv::TYPE_F64); - nvcv::Tensor maxval({{shape.x}, "N"}, nvcv::TYPE_F64); - - benchutils::FillTensor(thresh, benchutils::RandomValues()); - benchutils::FillTensor(maxval, benchutils::RandomValues()); - - if (varShape < 0) // negative var shape means use Tensor - { - nvcv::Tensor src({{shape.x, shape.y, shape.z, 1}, "NHWC"}, benchutils::GetDataType()); - nvcv::Tensor dst({{shape.x, shape.y, shape.z, 1}, "NHWC"}, benchutils::GetDataType()); - - benchutils::FillTensor(src, benchutils::RandomValues()); - - state.exec(nvbench::exec_tag::sync, [&op, &src, &dst, &thresh, &maxval](nvbench::launch &launch) - { - op(launch.get_stream(), src, dst, thresh, maxval); - }); - } - else // zero and positive var shape means use ImageBatchVarShape - { - nvcv::ImageBatchVarShape src(shape.x); - nvcv::ImageBatchVarShape dst(shape.x); - - benchutils::FillImageBatch(src, long2{shape.z, shape.y}, long2{varShape, varShape}, - benchutils::RandomValues()); - dst.pushBack(src.begin(), src.end()); - - state.exec(nvbench::exec_tag::sync, [&op, &src, &dst, &thresh, &maxval](nvbench::launch &launch) - { - op(launch.get_stream(), src, dst, thresh, maxval); - }); - } -} -catch (const std::exception &err) -{ - state.skip(err.what()); -} - -// clang-format on - -using ThresholdTypes = nvbench::type_list; - -NVBENCH_BENCH_TYPES(Threshold, NVBENCH_TYPE_AXES(ThresholdTypes)) - .set_type_axes_names({"InOutDataType"}) - .add_string_axis("shape", {"1x1080x1920"}) - .add_int64_axis("varShape", {-1, 0}); diff --git a/bench/BenchUtils.hpp b/bench/BenchUtils.hpp deleted file mode 100644 index be88ac928..000000000 --- a/bench/BenchUtils.hpp +++ /dev/null @@ -1,324 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed 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. - */ - -#ifndef CVCUDA_BENCH_UTILS_HPP -#define CVCUDA_BENCH_UTILS_HPP - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -#define CVCUDA_CHECK_DATA(data) \ - if (!data) \ - { \ - throw std::runtime_error("Invalid data"); \ - } - -#define CUDA_CHECK_ERROR(RC) \ - { \ - benchutils::cudaCheckError((RC), __FILE__, __LINE__); \ - } - -namespace benchutils { - -inline void cudaCheckError(cudaError_t code, const char *file, int line) -{ - if (code != cudaSuccess) - { - fprintf(stderr, "\nE In CUDA: %s %s %d\n", cudaGetErrorString(code), file, line); - exit(code); - } -} - -template> -inline RT GetShape(const std::string &shapeStr, const std::string &delimiter = "x") -{ - std::string str = shapeStr; - RT shape; - for (int i = 0; i < N; ++i) - { - size_t pos = str.find(delimiter); - - if ((pos == std::string::npos && i != (N - 1)) || (pos != std::string::npos && i == (N - 1))) - { - throw std::invalid_argument("Expecting " + std::to_string(N) + "-rank shape in " + shapeStr - + " (pass shape separated by " + delimiter + ")"); - } - - nvcv::cuda::GetElement(shape, i) = std::stoi(str.substr(0, pos)); - - str.erase(0, pos + delimiter.length()); - } - - return shape; -} - -template -inline nvcv::DataType GetDataType() -{ -#define CVCUDA_BENCH_GET_DATA_TYPE(TYPE, DATA_TYPE) \ - if constexpr (std::is_same_v) \ - { \ - return DATA_TYPE; \ - } - - CVCUDA_BENCH_GET_DATA_TYPE(uint8_t, nvcv::TYPE_U8); - CVCUDA_BENCH_GET_DATA_TYPE(uint16_t, nvcv::TYPE_U16); - CVCUDA_BENCH_GET_DATA_TYPE(uint32_t, nvcv::TYPE_U32); - - CVCUDA_BENCH_GET_DATA_TYPE(uchar3, nvcv::TYPE_3U8); - CVCUDA_BENCH_GET_DATA_TYPE(uchar4, nvcv::TYPE_4U8); - CVCUDA_BENCH_GET_DATA_TYPE(float, nvcv::TYPE_F32); - - CVCUDA_BENCH_GET_DATA_TYPE(float3, nvcv::TYPE_3F32); - CVCUDA_BENCH_GET_DATA_TYPE(float4, nvcv::TYPE_4F32); - - CVCUDA_BENCH_GET_DATA_TYPE(int, nvcv::TYPE_S32); - - CVCUDA_BENCH_GET_DATA_TYPE(short, nvcv::TYPE_S16); - - CVCUDA_BENCH_GET_DATA_TYPE(ushort3, nvcv::TYPE_3U16); - CVCUDA_BENCH_GET_DATA_TYPE(ushort4, nvcv::TYPE_4U16); - CVCUDA_BENCH_GET_DATA_TYPE(short4, nvcv::TYPE_4S16); - -#undef CVCUDA_BENCH_GET_DATA_TYPE - - throw std::invalid_argument("Unexpected data type"); -} - -template -inline nvcv::ImageFormat GetFormat() -{ - return nvcv::ImageFormat{GetDataType()}; -} - -inline NVCVBorderType GetBorderType(const std::string &border) -{ -#define CVCUDA_BENCH_GET_BORDER_TYPE(BORDER) \ - if (border == #BORDER) \ - { \ - return NVCV_BORDER_##BORDER; \ - } - - CVCUDA_BENCH_GET_BORDER_TYPE(CONSTANT); - CVCUDA_BENCH_GET_BORDER_TYPE(REPLICATE); - CVCUDA_BENCH_GET_BORDER_TYPE(REFLECT); - CVCUDA_BENCH_GET_BORDER_TYPE(WRAP); - CVCUDA_BENCH_GET_BORDER_TYPE(REFLECT101); - -#undef CVCUDA_BENCH_GET_BORDER_TYPE - - throw std::invalid_argument("Unexpected border type = " + border); -} - -inline NVCVNormType GetNormType(const std::string &normType) -{ -#define CVCUDA_BENCH_GET_NORM_TYPE(NORM) \ - if (normType == #NORM) \ - { \ - return NVCV_NORM_##NORM; \ - } - - CVCUDA_BENCH_GET_NORM_TYPE(HAMMING); - CVCUDA_BENCH_GET_NORM_TYPE(L1); - CVCUDA_BENCH_GET_NORM_TYPE(L2); - -#undef CVCUDA_BENCH_GET_NORM_TYPE - - throw std::invalid_argument("Unexpected norm type = " + normType); -} - -inline NVCVInterpolationType GetInterpolationType(const std::string &interpolation) -{ -#define CVCUDA_BENCH_GET_INTERPOLATION_TYPE(INTERP) \ - if (interpolation == #INTERP) \ - { \ - return NVCV_INTERP_##INTERP; \ - } - - CVCUDA_BENCH_GET_INTERPOLATION_TYPE(NEAREST); - CVCUDA_BENCH_GET_INTERPOLATION_TYPE(LINEAR); - CVCUDA_BENCH_GET_INTERPOLATION_TYPE(CUBIC); - CVCUDA_BENCH_GET_INTERPOLATION_TYPE(AREA); - -#undef CVCUDA_BENCH_GET_INTERPOLATION_TYPE - - throw std::invalid_argument("Unexpected interpolation type = " + interpolation); -} - -template, const T, T>> -inline RT &ValueAt(VecType &vec, const ST &strides, const ST &coord) -{ - return *reinterpret_cast(&vec[nvcv::cuda::dot(coord, strides)]); -} - -static std::default_random_engine DefaultGenerator(unsigned long int seed = 0) -{ - static std::default_random_engine defaultRandomGenerator{std::random_device{}()}; - - defaultRandomGenerator.seed(seed); - - return defaultRandomGenerator; -} - -template -struct Randomizer -{ - using BT = nvcv::cuda::BaseType; - using RE = std::default_random_engine; - using UD = std::conditional_t, std::uniform_real_distribution, - std::uniform_int_distribution>; - - VT operator()() - { - VT ret; - for (int i = 0; i < nvcv::cuda::NumElements; ++i) - { - nvcv::cuda::GetElement(ret, i) = uniformDistribution(randomGenerator); - } - return ret; - } - - VT operator()(const long4_16a &) - { - return operator()(); - } - - UD uniformDistribution; - RE randomGenerator; -}; - -template, typename BT = typename R::BT, typename RE = typename R::RE, - typename UD = typename R::UD> -inline auto RandomValues(BT min = std::is_integral_v ? nvcv::cuda::TypeTraits::min : -1, - BT max = std::is_integral_v ? nvcv::cuda::TypeTraits::max : +1, - RE rng = DefaultGenerator()) -{ - return R{UD(min, max), rng}; -} - -template -inline void FillBuffer(std::vector &vec, const ST &shape, const ST &strides, VG valuesGenerator) -{ - for (long x = 0; x < (nvcv::cuda::NumElements >= 1 ? nvcv::cuda::GetElement(shape, 0) : 1); ++x) - { - for (long y = 0; y < (nvcv::cuda::NumElements >= 2 ? nvcv::cuda::GetElement(shape, 1) : 1); ++y) - { - for (long z = 0; z < (nvcv::cuda::NumElements >= 3 ? nvcv::cuda::GetElement(shape, 2) : 1); ++z) - { - for (long w = 0; w < (nvcv::cuda::NumElements == 4 ? nvcv::cuda::GetElement(shape, 3) : 1); ++w) - { - long4_16a coord16a{x, y, z, w}; - ST stCoord = nvcv::cuda::DropCast>(coord16a); - - ValueAt(vec, strides, stCoord) = valuesGenerator(coord16a); - } - } - } - } -} - -template -inline void FillTensor(const nvcv::Tensor &tensor, VG valuesGenerator) -{ - using longR = nvcv::cuda::MakeType; - - auto tensorData = tensor.exportData(); - CVCUDA_CHECK_DATA(tensorData); - - longR strides, shape; - - for (int i = 0; i < RANK; ++i) - { - nvcv::cuda::GetElement(strides, i) = tensorData->stride(i); - nvcv::cuda::GetElement(shape, i) = tensorData->shape(i); - } - - long bufSize{nvcv::cuda::GetElement(strides, 0) * nvcv::cuda::GetElement(shape, 0)}; - - std::vector tensorVec(bufSize); - - FillBuffer(tensorVec, shape, strides, valuesGenerator); - - CUDA_CHECK_ERROR(cudaMemcpy(tensorData->basePtr(), tensorVec.data(), bufSize, cudaMemcpyHostToDevice)); -} - -template -inline void FillTensor(const nvcv::Tensor &tensor, VG valuesGenerator) -{ - switch (tensor.rank()) - { -#define CVCUDA_BENCH_FILL_TENSOR_CASE(RANK) \ -case RANK: \ - FillTensor(tensor, valuesGenerator); \ - break - - CVCUDA_BENCH_FILL_TENSOR_CASE(1); - CVCUDA_BENCH_FILL_TENSOR_CASE(2); - CVCUDA_BENCH_FILL_TENSOR_CASE(3); - CVCUDA_BENCH_FILL_TENSOR_CASE(4); - -#undef CVCUDA_BENCH_FILL_TENSOR_CASE - default: - throw std::invalid_argument("Tensor has rank not in [1, 4]"); - } -} - -template -inline void FillImageBatch(nvcv::ImageBatchVarShape &imageBatch, long2 size, long2 varSize, VG valuesGenerator) -{ - auto randomWidth = RandomValues(static_cast(size.x - varSize.x), static_cast(size.x)); - auto randomHeight = RandomValues(static_cast(size.y - varSize.y), static_cast(size.y)); - - for (int i = 0; i < imageBatch.capacity(); ++i) - { - nvcv::Image image(nvcv::Size2D{randomWidth(), randomHeight()}, GetFormat()); - - auto data = image.exportData(); - CVCUDA_CHECK_DATA(data); - - long2 strides{data->plane(0).rowStride, sizeof(VT)}; - long2 shape{data->plane(0).height, data->plane(0).width}; - - std::vector imageBuffer(strides.x * shape.x); - - FillBuffer(imageBuffer, shape, strides, valuesGenerator); - - CUDA_CHECK_ERROR(cudaMemcpy2D(data->plane(0).basePtr, strides.x, imageBuffer.data(), strides.x, strides.x, - data->plane(0).height, cudaMemcpyHostToDevice)); - - imageBatch.pushBack(image); - } -} - -} // namespace benchutils - -#endif // CVCUDA_BENCH_UTILS_HPP diff --git a/bench/BenchWarpAffine.cpp b/bench/BenchWarpAffine.cpp deleted file mode 100644 index 29661fb9b..000000000 --- a/bench/BenchWarpAffine.cpp +++ /dev/null @@ -1,95 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed 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. - */ - -#include "BenchUtils.hpp" - -#include - -#include - -template -inline void WarpAffine(nvbench::state &state, nvbench::type_list) -try -{ - long3 shape = benchutils::GetShape<3>(state.get_string("shape")); - long varShape = state.get_int64("varShape"); - - NVCVBorderType borderType = benchutils::GetBorderType(state.get_string("border")); - NVCVInterpolationType interpType = benchutils::GetInterpolationType(state.get_string("interpolation")); - - int flags = interpType | ((state.get_string("inverseMap") == "Y") ? NVCV_WARP_INVERSE_MAP : 0); - - float4 borderValue{0, 0, 0, 0}; - - NVCVAffineTransform transMatrix{2.f, 2.f, 0.f, 3.f, 1.f, 0.f}; - - state.add_global_memory_reads(shape.x * shape.y * shape.z * sizeof(T) + 6 * sizeof(float)); - state.add_global_memory_writes(shape.x * shape.y * shape.z * sizeof(T)); - - cvcuda::WarpAffine op(shape.x); - - // clang-format off - - if (varShape < 0) // negative var shape means use Tensor - { - nvcv::Tensor src({{shape.x, shape.y, shape.z, 1}, "NHWC"}, benchutils::GetDataType()); - nvcv::Tensor dst({{shape.x, shape.y, shape.z, 1}, "NHWC"}, benchutils::GetDataType()); - - benchutils::FillTensor(src, benchutils::RandomValues()); - - state.exec(nvbench::exec_tag::sync, [&op, &src, &dst, &transMatrix, &flags, &borderType, &borderValue] - (nvbench::launch &launch) - { - op(launch.get_stream(), src, dst, transMatrix, flags, borderType, borderValue); - }); - } - else // zero and positive var shape means use ImageBatchVarShape - { - nvcv::ImageBatchVarShape src(shape.x); - nvcv::ImageBatchVarShape dst(shape.x); - - benchutils::FillImageBatch(src, long2{shape.z, shape.y}, long2{varShape, varShape}, - benchutils::RandomValues()); - dst.pushBack(src.begin(), src.end()); - - nvcv::Tensor transMatrixTensor({{shape.x, 6}, "NW"}, nvcv::TYPE_F32); - - benchutils::FillTensor(transMatrixTensor, [&transMatrix](const long4_16a &c){ return transMatrix[c.y]; }); - - state.exec(nvbench::exec_tag::sync, - [&op, &src, &dst, &transMatrixTensor, &flags, &borderType, &borderValue](nvbench::launch &launch) - { - op(launch.get_stream(), src, dst, transMatrixTensor, flags, borderType, borderValue); - }); - } -} -catch (const std::exception &err) -{ - state.skip(err.what()); -} - -// clang-format on - -using WarpAffineTypes = nvbench::type_list; - -NVBENCH_BENCH_TYPES(WarpAffine, NVBENCH_TYPE_AXES(WarpAffineTypes)) - .set_type_axes_names({"InOutDataType"}) - .add_string_axis("shape", {"1x1080x1920"}) - .add_int64_axis("varShape", {-1, 0}) - .add_string_axis("border", {"REFLECT"}) - .add_string_axis("interpolation", {"CUBIC"}) - .add_string_axis("inverseMap", {"Y"}); diff --git a/bench/BenchWarpPerspective.cpp b/bench/BenchWarpPerspective.cpp deleted file mode 100644 index 433909885..000000000 --- a/bench/BenchWarpPerspective.cpp +++ /dev/null @@ -1,95 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed 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. - */ - -#include "BenchUtils.hpp" - -#include - -#include - -template -inline void WarpPerspective(nvbench::state &state, nvbench::type_list) -try -{ - long3 shape = benchutils::GetShape<3>(state.get_string("shape")); - long varShape = state.get_int64("varShape"); - - NVCVBorderType borderType = benchutils::GetBorderType(state.get_string("border")); - NVCVInterpolationType interpType = benchutils::GetInterpolationType(state.get_string("interpolation")); - - int flags = interpType | ((state.get_string("inverseMap") == "Y") ? NVCV_WARP_INVERSE_MAP : 0); - - float4 borderValue{0, 0, 0, 0}; - - NVCVPerspectiveTransform transMatrix{0.27, 0.16, 0.00, -0.11, 0.61, 0.65, -0.09, 0.06, 1.00}; - - state.add_global_memory_reads(shape.x * shape.y * shape.z * sizeof(T) + 9 * sizeof(float)); - state.add_global_memory_writes(shape.x * shape.y * shape.z * sizeof(T)); - - cvcuda::WarpPerspective op(shape.x); - - // clang-format off - - if (varShape < 0) // negative var shape means use Tensor - { - nvcv::Tensor src({{shape.x, shape.y, shape.z, 1}, "NHWC"}, benchutils::GetDataType()); - nvcv::Tensor dst({{shape.x, shape.y, shape.z, 1}, "NHWC"}, benchutils::GetDataType()); - - benchutils::FillTensor(src, benchutils::RandomValues()); - - state.exec(nvbench::exec_tag::sync, - [&op, &src, &dst, &transMatrix, &flags, &borderType, &borderValue](nvbench::launch &launch) - { - op(launch.get_stream(), src, dst, transMatrix, flags, borderType, borderValue); - }); - } - else // zero and positive var shape means use ImageBatchVarShape - { - nvcv::ImageBatchVarShape src(shape.x); - nvcv::ImageBatchVarShape dst(shape.x); - - benchutils::FillImageBatch(src, long2{shape.z, shape.y}, long2{varShape, varShape}, - benchutils::RandomValues()); - dst.pushBack(src.begin(), src.end()); - - nvcv::Tensor transMatrixTensor({{shape.x, 9}, "NW"}, nvcv::TYPE_F32); - - benchutils::FillTensor(transMatrixTensor, [&transMatrix](auto &c){ return transMatrix[c.y]; }); - - state.exec(nvbench::exec_tag::sync, - [&op, &src, &dst, &transMatrixTensor, &flags, &borderType, &borderValue](nvbench::launch &launch) - { - op(launch.get_stream(), src, dst, transMatrixTensor, flags, borderType, borderValue); - }); - } -} -catch (const std::exception &err) -{ - state.skip(err.what()); -} - -// clang-format on - -using WarpPerspectiveTypes = nvbench::type_list; - -NVBENCH_BENCH_TYPES(WarpPerspective, NVBENCH_TYPE_AXES(WarpPerspectiveTypes)) - .set_type_axes_names({"InOutDataType"}) - .add_string_axis("shape", {"1x1080x1920"}) - .add_int64_axis("varShape", {-1, 0}) - .add_string_axis("border", {"REFLECT"}) - .add_string_axis("interpolation", {"CUBIC"}) - .add_string_axis("inverseMap", {"Y"}); diff --git a/bench/CMakeLists.txt b/bench/CMakeLists.txt index adee42ce8..cca014fd0 100644 --- a/bench/CMakeLists.txt +++ b/bench/CMakeLists.txt @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,71 +13,95 @@ # See the License for the specific language governing permissions and # limitations under the License. -project(cvcuda_bench) +project(cvcuda_benchmarks) -set(bench_sources - BenchMinMaxLoc.cpp - BenchNMS.cpp - BenchRemap.cpp - BenchGaussian.cpp - BenchLaplacian.cpp - BenchAverageBlur.cpp - BenchMedianBlur.cpp - BenchBilateralFilter.cpp - BenchSIFT.cpp - BenchReformat.cpp - BenchResize.cpp - BenchFlip.cpp - BenchRotate.cpp - BenchPillowResize.cpp - BenchHQResize.cpp - BenchCenterCrop.cpp - BenchWarpPerspective.cpp - BenchWarpAffine.cpp - BenchThreshold.cpp - BenchRandomResizedCrop.cpp - BenchNormalize.cpp - BenchMorphology.cpp - BenchAdaptiveThreshold.cpp - BenchCvtColor.cpp - BenchAdvCvtColor.cpp - BenchBndBox.cpp - BenchBoxBlur.cpp - BenchBrightnessContrast.cpp - BenchChannelReorder.cpp - BenchColorTwist.cpp - BenchComposite.cpp - BenchConv2D.cpp - BenchConvertTo.cpp - BenchCopyMakeBorder.cpp - BenchCropFlipNormalizeReformat.cpp - BenchResizeCropConvertReformat.cpp - BenchCustomCrop.cpp - BenchErase.cpp - BenchGammaContrast.cpp - BenchGaussianNoise.cpp - BenchHistogramEq.cpp - BenchHistogram.cpp - BenchInpaint.cpp - BenchJointBilateralFilter.cpp - BenchMinAreaRect.cpp - BenchPadAndStack.cpp - BenchOSD.cpp - BenchLabel.cpp - BenchPairwiseMatcher.cpp - BenchStack.cpp - BenchFindHomography.cpp +# Build C++ benchmarks +add_subdirectory(cpp) + +# Install Python benchmarks +add_subdirectory(python) + +# Copy benchmark config files to build directory +# Use custom command to ensure config files are updated when source changes +# Note: load_config.py is imported by python_bench_utils.py and run_bench.py +set(CONFIG_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/config") +set(CONFIG_DEST_DIR "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/config") +set(CONFIG_JSON_SOURCE "${CONFIG_SOURCE_DIR}/bench_params.json") +set(CONFIG_JSON_DEST "${CONFIG_DEST_DIR}/bench_params.json") +set(CONFIG_SKU_MAP_SOURCE "${CONFIG_SOURCE_DIR}/sku_map.json") +set(CONFIG_SKU_MAP_DEST "${CONFIG_DEST_DIR}/sku_map.json") +set(CONFIG_LOADER_SOURCE "${CONFIG_SOURCE_DIR}/load_config.py") +set(CONFIG_LOADER_DEST "${CONFIG_DEST_DIR}/load_config.py") +set(CONFIG_AXIS_ORDER_SOURCE "${CONFIG_SOURCE_DIR}/axis_order.py") +set(CONFIG_AXIS_ORDER_DEST "${CONFIG_DEST_DIR}/axis_order.py") +file(GLOB CONFIG_OPERATOR_SOURCES CONFIGURE_DEPENDS "${CONFIG_SOURCE_DIR}/operators/*.json") + +file(MAKE_DIRECTORY "${CONFIG_DEST_DIR}") +file(MAKE_DIRECTORY "${CONFIG_DEST_DIR}/operators") + +# Add custom commands to copy config files when they change +add_custom_command( + OUTPUT "${CONFIG_JSON_DEST}" + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CONFIG_JSON_SOURCE}" "${CONFIG_JSON_DEST}" + DEPENDS "${CONFIG_JSON_SOURCE}" + COMMENT "Copying bench_params.json to build directory" + VERBATIM ) -# Metatarget for all benchmarks -add_custom_target(bench_all) +add_custom_command( + OUTPUT "${CONFIG_LOADER_DEST}" + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CONFIG_LOADER_SOURCE}" "${CONFIG_LOADER_DEST}" + DEPENDS "${CONFIG_LOADER_SOURCE}" + COMMENT "Copying load_config.py to build directory" + VERBATIM +) + +add_custom_command( + OUTPUT "${CONFIG_AXIS_ORDER_DEST}" + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CONFIG_AXIS_ORDER_SOURCE}" "${CONFIG_AXIS_ORDER_DEST}" + DEPENDS "${CONFIG_AXIS_ORDER_SOURCE}" + COMMENT "Copying canonical benchmark axis order to build directory" + VERBATIM +) -foreach(bench_source IN LISTS bench_sources) - get_filename_component(bench_file_name "${bench_source}" NAME_WLE) - string(REPLACE "Bench" "cvcuda_bench_" algo_name ${bench_file_name}) - string(TOLOWER ${algo_name} bench_name) - add_executable(${bench_name} "${bench_source}") - target_link_libraries(${bench_name} PRIVATE cvcuda::nvbench::main cvcuda) - set_target_properties(${bench_name} PROPERTIES COMPILE_FEATURES cuda_std_17) - add_dependencies(bench_all ${bench_name}) +add_custom_command( + OUTPUT "${CONFIG_SKU_MAP_DEST}" + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CONFIG_SKU_MAP_SOURCE}" "${CONFIG_SKU_MAP_DEST}" + DEPENDS "${CONFIG_SKU_MAP_SOURCE}" + COMMENT "Copying bench sku_map.json to build directory" + VERBATIM +) + +set(CONFIG_OPERATOR_DESTS "") +foreach(CONFIG_OPERATOR_SOURCE IN LISTS CONFIG_OPERATOR_SOURCES) + get_filename_component(CONFIG_OPERATOR_FILE "${CONFIG_OPERATOR_SOURCE}" NAME) + set(CONFIG_OPERATOR_DEST "${CONFIG_DEST_DIR}/operators/${CONFIG_OPERATOR_FILE}") + add_custom_command( + OUTPUT "${CONFIG_OPERATOR_DEST}" + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CONFIG_OPERATOR_SOURCE}" "${CONFIG_OPERATOR_DEST}" + DEPENDS "${CONFIG_OPERATOR_SOURCE}" + COMMENT "Copying ${CONFIG_OPERATOR_FILE} benchmark config to build directory" + VERBATIM + ) + list(APPEND CONFIG_OPERATOR_DESTS "${CONFIG_OPERATOR_DEST}") endforeach() + +# Create a custom target that depends on the copied files +add_custom_target(copy_bench_config ALL + DEPENDS "${CONFIG_JSON_DEST}" "${CONFIG_LOADER_DEST}" "${CONFIG_AXIS_ORDER_DEST}" + "${CONFIG_SKU_MAP_DEST}" ${CONFIG_OPERATOR_DESTS} +) + +# Install benchmark config files +install(FILES + config/bench_params.json + config/sku_map.json + config/load_config.py + config/axis_order.py + DESTINATION ${CMAKE_INSTALL_BINDIR}/config + COMPONENT bench) + +install(DIRECTORY config/operators + DESTINATION ${CMAKE_INSTALL_BINDIR}/config + COMPONENT bench + FILES_MATCHING PATTERN "*.json") diff --git a/bench/README.md b/bench/README.md new file mode 100644 index 000000000..2b1f33694 --- /dev/null +++ b/bench/README.md @@ -0,0 +1,414 @@ + + +# CV-CUDA Benchmarks + +CV-CUDA provides C++ and Python operator benchmarks built on +[nvbench](https://github.com/NVIDIA/nvbench). They report GPU time, measurement +noise, and bandwidth utilization; paired C++ and Python runs also check +performance parity. Committed performance baselines are available for NVIDIA +A100 and H100 GPUs. + +## Choose a Workflow + +| Goal | Command | Main output | +|---|---|---| +| Run C++ and/or Python benchmarks | `run_bench.py` | `bench_output.csv` or JSON | +| Compare two Python wheels | `compare_wheels.py` | `summary.md` and `comparison.csv` | +| Compare a run with committed baselines | `compare_to_baseline.py` | Console, Markdown, or JUnit report | + +Use `--help` on any command for its complete option list. + +## Setup + +The benchmarks require Python 3.10+, CUDA Toolkit 12 or 13, and nvbench. The +development container provides the expected environment. Otherwise, install the +Python benchmark dependencies from the repository root: + +```bash +bench/python/install_bench_dependencies.sh +``` + +Build the benchmark component by following the +[installation guide](../docs/sphinx/installation.rst). The examples below assume +the build output is `build-rel/bin`. + +`run_bench.py` and `compare_wheels.py` are copied into build and install +directories. `compare_to_baseline.py` and the baseline-maintenance helpers are +source-tree tools. + +## Run Benchmarks + +From the benchmark build directory, a command with no selection options runs the +`basic` tier for both C++ and Python: + +```bash +cd build-rel/bin + +# Default basic suite, both languages +python3 run_bench.py + +# Select exact operators or one language +python3 run_bench.py --operator resize,gaussian +python3 run_bench.py --lang python --operator resize + +# Select deeper profiles or one exact configuration +python3 run_bench.py --tier basic,advanced +python3 run_bench.py \ + --config-key resize_expand_linear_1080p_uchar3_basic + +# Discover valid operator names +python3 run_bench.py --list-operators +``` + +Operator names come from `config/bench_params.json`. `--config-key` bypasses +operator and tier selection. + +The default result is `bench_output.csv`. Use JSON when the result will be +compared with or imported into committed baselines: + +```bash +python3 run_bench.py --operator resize --output bench_output.json +``` + +The runner fails noisy results. When both languages run, it also checks that C++ +and Python timings agree within the configured relative and absolute thresholds. +`--skip-validation` retains row statuses but prevents those checks from failing +the final run. + +Output from `python3 run_bench.py --lang python --config-key +resize_expand_linear_1080p_uchar3_basic` is shown below. It is abridged, and +timings vary by GPU: + +```text +=== Running Benchmarks === +→ GPU: NVIDIA Graphics Device +→ Found 1 benchmarks + +[1/1] resize_expand_linear_1080p_uchar3_basic +| Benchmark | config_key | ... | Py (µs) | Py Noise | Status | +| resize | resize_expand_linear_1080p_uchar3_basic | ... | 1,972.7 | ±0.07% (±1µs) | PASS | + +✓ All validations passed (noise <10.0%, perf diff <10.0% AND <100us) +✓ Results written to bench_output.csv + +--- Summary --- + Operators: 1, Configurations: 1 (1 passed, 0 failed) + Py noise: ±0.07% (±1.4µs) +``` + +Exit status is `0` for a successful validated run, `1` for an execution or +validation failure, and `2` for invalid arguments or configuration. + +## Compare Two Python Wheels + +`compare_wheels.py` installs the reference and candidate wheels into separate +temporary environments, runs the same current Python benchmark harness against +each, and compares the results sequentially on the same GPU. + +Both wheels must use the same distribution and CUDA channel, such as +`cvcuda-cu12`. The selected Python interpreter must support both wheel tags and +already provide the benchmark dependencies. The output directory must be new or +empty. + +From the repository root: + +```bash +python3 bench/compare_wheels.py \ + /path/to/reference/cvcuda_cu12-*.whl \ + /path/to/candidate/cvcuda_cu12-*.whl \ + --operator resize,gaussian \ + --output-dir wheel-comparison +``` + +From a build or installed benchmark directory, omit the `bench/` prefix. + +The default selection is the complete `basic` tier. Use `--tier +basic,advanced` for both tiers, `--operator` for exact operator names, or +`--config-key` for one or more exact configurations. + +The output directory contains: + +```text +wheel-comparison/ +├── baseline/bench_output.csv +├── candidate/bench_output.csv +├── comparison.csv +└── summary.md +``` + +The baseline and candidate directories also contain logs, wheel metadata, and +GPU diagnostics. + +This abridged example is from the tested v0.16.0 versus v0.17.0-pre comparison: + +```text +- baseline: `cvcuda-cu12 0.16.0 [...]` +- candidate: `cvcuda-cu12 0.17.0rc0 [...]` +- comparison status: **FAIL** — reference `run_bench.py` exited 1; 213 configurations only in candidate; 1 configuration non-PASS +- artifacts: `wheel-comparison` + - reference benchmark CSV: `baseline/bench_output.csv` + - candidate benchmark CSV: `candidate/bench_output.csv` + - comparison CSV: `comparison.csv` + +## Summary + +- overall candidate speedup vs baseline: **1.8258x** (mean of per-operator geomeans across matched benchmarks) +- valid compatible operators: 49/60 +- valid compatible configurations: 146/360 +- configuration regressions over threshold: 0 +- reference `run_bench.py`: **FAIL** (exit code 1) +- candidate `run_bench.py`: **PASS** (exit code 0) + +## Compatibility + +- operators partially compatible: 43/60 +- operators fully compatible: 7/60 +- expanded benchmark configurations incompatible or not present: 213/360 +- present in both but non-PASS: 1 + +## Top 5 operator improvements + +| Operator | Speedup | Candidate time delta | Matched configurations | +|---|---:|---:|---:| +| gaussiannoise | 13.0905x | -92.36% | 3 | +... + +## Top 5 operator regressions + +_none_ +``` + +This fails because the older reference is incomplete relative to the current +harness, not because a configuration regressed. The overall result is the +arithmetic mean of per-operator geometric speedups, so every operator has equal +weight. Compatibility counts include present non-PASS rows; speedups exclude +them. + +Exit status is `0` when both runs and the comparison pass, `1` when a run or +comparison check fails after producing the report, and `2` for setup or artifact +errors. + +## Compare with Committed Baselines + +Committed baselines are embedded in `bench/config/operators/*.json` for these +reference GPU identities: + +- `A100_PCIE_40GB_250W_1095MHz` +- `H100_PCIe_350W_1095MHz` + +First create a JSON result, then compare it from the repository root: + +```bash +cd build-rel/bin +python3 run_bench.py --output bench_output.json +cd ../.. + +python3 bench/compare_to_baseline.py \ + --current build-rel/bin/bench_output.json +``` + +For a scoped comparison, add `--operator resize` to both commands. The JSON +input must contain only the selected operators. + +The tool resolves the SKU from the JSON result. It fails on regressions, +unexpected improvements that may indicate stale baselines, missing or new rows, +and missing SKU data. The default regression and improvement thresholds are +both 10%. + +Abridged passing output: + +```text +Resolved SKU: A100_PCIE_40GB_250W_1095MHz (current JSON SKU key) +matched=720 regressions=0 improvements=0 missing=0 new=0 missing_sku=0 + +- thresholds: regression=0.1, improvement=0.1 +- all-rows |Delta|: median 0.30%, max 5.61% + +## Regressions (0) +_none_ + +## Unexpected improvements (0) +_none_ + +``` + +The report prints to the console by default. It can also be written as Markdown +or JUnit: + +```bash +python3 bench/compare_to_baseline.py \ + --current build-rel/bin/bench_output.json \ + --markdown comparison.md --junit comparison.xml +``` + +Exit status is `0` when all expected rows match within the thresholds, `1` when +the comparison finds an incompatibility or timing failure, and `2` for invalid +input or configuration. + +## Advanced Benchmark Usage and Configuration + +### Directory Structure + +```text +bench/ +├── config/ +│ ├── bench_params.json # Operator manifest +│ ├── operators/ # Per-operator cases and embedded baselines +│ └── sku_map.json # Supported benchmark GPU identities +├── cpp/ops/ # C++ operator benchmarks +├── python/ops/ # Python operator benchmarks +├── _internal/ # Shared helpers and baseline maintenance +├── run_bench.py # Run C++ and/or Python benchmarks +├── compare_wheels.py # Compare two Python wheels +└── compare_to_baseline.py # Compare a run with committed baselines +``` + +The three top-level scripts are the supported benchmark commands. Files under +`_internal/` are implementation and maintenance helpers. + +### Individual Benchmark Drivers + +Individual drivers expose their nvbench axes directly. Python drivers also +require a configuration key: + +```bash +cd build-rel/bin +./bench_resize --list +python3 bench_resize.py \ + --config-key resize_expand_linear_1080p_uchar3_basic --list +``` + +Remove `--list` and use nvbench `--axis` filters to run a direct selection. +Prefer `run_bench.py` for combined output and validation. + +### Configuration Files + +`config/bench_params.json` is the operator manifest. Each operator points to a +file under `config/operators/` and its matching C++ and Python benchmarks: + +```json +{ + "operators": { + "resize": { + "config": "operators/resize.json", + "cpp": "bench_resize", + "python": "bench_resize.py" + } + } +} +``` + +Each operator file defines stable configuration keys, tiers, data types, axes, +warmup behavior, and machine-owned baselines. This schema example is abridged: + +```json +{ + "benchmark": "resize", + "configs": { + "resize_basic": { + "tier": "basic", + "dtypes": ["uint8", "float32"], + "string_axes": { + "shape": ["1x1080x1920"], + "interpolation": ["LINEAR", "CUBIC"], + "inputKind": ["Tensor", "VarShape"] + }, + "int64_axes": {"kernelSize": [3, 5]}, + "float64_axes": {"sigma": [1.2]}, + "warmup_iterations": 100, + "baselines": { + "resize_basic[InOutDataType=uint8][shape=1x1080x1920][interpolation=LINEAR][inputKind=Tensor][kernelSize=3][sigma=1.2]": { + "H100_PCIe_350W_1095MHz": { + "n_runs": 20, + "gpu_time_us_cpp": 100.0, + "gpu_time_us_python": 103.0, + "gpu_noise_us_cpp": 1.0, + "gpu_noise_us_python": 2.0, + "gpu_bwutil_cpp": 0.42, + "gpu_bwutil_python": 0.41, + "gpu_gap_stddev_us": 3.0 + } + } + } + } + } +} +``` + +### Field Reference + +| Field | Description | +|---|---| +| Manifest `config` | Per-operator JSON file under `config/operators/`. | +| Manifest `cpp` / `python` | Matching C++ binary and Python benchmark script. | +| `benchmark` | The `bench_` target for this operator file. | +| `configs` | Configuration entries keyed by stable `config_key`. | +| `tier` | `basic` runs by default; `advanced` is explicitly selected. | +| `dtypes` | Input/output data types used by the benchmark. | +| `string_axes` | String-valued axes such as shape, mode, layout, and `inputKind`. | +| `int64_axes` / `float64_axes` | Integer and floating-point benchmark axes. | +| `warmup_iterations` | Warmup count before measurement; default is zero. | +| `baselines` | Machine-owned case/SKU measurements keyed by the fully expanded case identity. | + +A baseline metric payload records the number of imported runs, C++ and Python +GPU time, measurement noise, bandwidth utilization, and optionally the +cross-run C++/Python gap standard deviation. + +`inputKind` selects the input container: `Tensor` uses a dense tensor and +`VarShape` uses `ImageBatchVarShape`. Operators with a distinct batch-of-tensors +API may also implement `TensorBatch`. + +Python benchmarks load the manifest and operator files at registration. +`run_bench.py` passes selected axis values to C++ benchmarks at runtime. Value +changes do not require a rebuild; rebuild the affected `bench_` target when +adding a new axis name, data type, or operator: + +```bash +cmake --build build-rel --target bench_resize +``` + +`--warmup-cap N` limits configured warmups for one run without modifying the +configuration. Passing zero disables warmup. + +## Advanced Baseline Maintenance + +Baseline blocks are generated data. Do not hand-edit them except for surgical +recovery; keep the source run or CI link with every reviewed update. + + +### Import, Validate, and Recheck + +Dry-run the import before writing operator files: + +```bash +python3 bench/_internal/update_baseline.py --from /path/to/artifacts \ + --operator resize --dry-run + +python3 bench/_internal/update_baseline.py --from /path/to/artifacts \ + --operator resize \ + --write-summary baseline-update.md +``` + +Validate the updated baselines, including same-key timing changes against the +branch base: + +```bash +python3 bench/_internal/validate_baselines.py \ + --operator resize --reject-regressions-from origin/main +``` + +Finally, create a fresh JSON run and compare it with the updated baselines, then +run the full MR matrix. + +`--allow-regressions` waives only the same-key slowdown check and is reserved +for intentional, reviewed baseline resets. Schema, SKU, noise, C++/Python +parity, and fresh-run comparison checks remain enforced. + +## Resources + +- [CV-CUDA documentation](https://docs.nvidia.com/cvcuda/) +- [nvbench documentation](https://github.com/NVIDIA/nvbench) +- [CV-CUDA installation guide](../docs/sphinx/installation.rst) diff --git a/bench/_internal/__init__.py b/bench/_internal/__init__.py new file mode 100644 index 000000000..4a9734fc7 --- /dev/null +++ b/bench/_internal/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Internal support modules for the CV-CUDA benchmark command-line tools.""" diff --git a/bench/_internal/axes.py b/bench/_internal/axes.py new file mode 100644 index 000000000..2636fb302 --- /dev/null +++ b/bench/_internal/axes.py @@ -0,0 +1,50 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""Human-readable formatting for benchmark config axes.""" + +from __future__ import annotations + +import math +from typing import Iterable, Tuple + + +def format_axis_name(name: str) -> str: + """Return the display label for a config axis.""" + return name + + +def format_axis_value(name: str, value) -> str: + """Return the display value for a config axis (empty string when missing).""" + if _is_missing(value): + return "" + return str(value) + + +def format_axes(axes: Iterable[Tuple[str, str]]) -> str: + """Format row-key axes for reports.""" + return ( + ", ".join(f"{format_axis_name(k)}={format_axis_value(k, v)}" for k, v in axes) + or "-" + ) + + +def _is_missing(value) -> bool: + if value is None: + return True + if isinstance(value, float) and math.isnan(value): + return True + text = str(value).strip() + return text in {"", "nan", "NaN", "", "None"} diff --git a/bench/_internal/baselines.py b/bench/_internal/baselines.py new file mode 100644 index 000000000..f38f4f4cf --- /dev/null +++ b/bench/_internal/baselines.py @@ -0,0 +1,1397 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""Shared helpers for JSON-backed benchmark baselines. + +The benchmark config files are human-authored; the nested ``baselines`` blocks +are machine-owned. This module centralizes the rules that turn benchmark result +rows into stable case keys and validate the JSON baseline payloads so compare, +update, and validation all agree on row identity. +""" + +from __future__ import annotations + +import json +import math +import re +from dataclasses import dataclass +from itertools import product +from pathlib import Path +from typing import ( + TYPE_CHECKING, + Any, + Dict, + Iterable, + List, + Mapping, + Optional, + Sequence, + Set, + Tuple, +) + +if TYPE_CHECKING: + import pandas as pd + +from .quality import BenchmarkQualityCriteria, DEFAULT_BENCHMARK_QUALITY + +BENCH_DIR = Path(__file__).resolve().parent.parent +DEFAULT_CONFIG_DIR = BENCH_DIR / "config" +DEFAULT_OPERATORS_DIR = DEFAULT_CONFIG_DIR / "operators" +DEFAULT_SKU_MAP_PATH = DEFAULT_CONFIG_DIR / "sku_map.json" +DEFAULT_MAX_BASELINE_REGRESSION_PCT = 10.0 + +CONFIG_KEY_COLUMN = "config_key" +DEVICE_NAME_COLUMN = "Device Name" +POWER_CAP_COLUMN = "Power Cap (W)" +LOCKED_CLOCK_COLUMN = "Locked SM Clock (MHz)" +VBIOS_COLUMN = "vBIOS Version" + +LANGUAGES = frozenset({"cpp", "python"}) + +GPU_TIME_COLUMN = "GPU Time (µs)" +GPU_NOISE_US_COLUMN = "GPU Noise (µs)" +BWUTIL_COLUMN = "BWUtil" + +RUN_REQUIRED_COLUMNS = frozenset( + { + CONFIG_KEY_COLUMN, + "Benchmark", + "Language", + GPU_TIME_COLUMN, + GPU_NOISE_US_COLUMN, + BWUTIL_COLUMN, + DEVICE_NAME_COLUMN, + POWER_CAP_COLUMN, + LOCKED_CLOCK_COLUMN, + } +) + +RUN_METADATA_COLUMNS = frozenset( + { + CONFIG_KEY_COLUMN, + "Benchmark", + "Language", + "tier", + GPU_TIME_COLUMN, + "GPU Noise (%)", + GPU_NOISE_US_COLUMN, + "CPU Time (µs)", + "CPU Noise (%)", + "CPU Noise (µs)", + "GlobalMem BW (bytes/sec)", + BWUTIL_COLUMN, + "Py overhead (%)", + "Py overhead (µs)", + "Status", + DEVICE_NAME_COLUMN, + POWER_CAP_COLUMN, + LOCKED_CLOCK_COLUMN, + VBIOS_COLUMN, + "Device", + "Skipped", + "Samples", + "Samples.1", + "Batch GPU (sec)", + } +) + +METRIC_FIELDS_BY_LANGUAGE = { + "cpp": ("gpu_time_us_cpp", "gpu_noise_us_cpp", "gpu_bwutil_cpp"), + "python": ("gpu_time_us_python", "gpu_noise_us_python", "gpu_bwutil_python"), +} + +BASELINE_METRIC_FIELDS = ( + "n_runs", + "gpu_time_us_cpp", + "gpu_time_us_python", + "gpu_noise_us_cpp", + "gpu_noise_us_python", + "gpu_bwutil_cpp", + "gpu_bwutil_python", +) +GPU_GAP_STDDEV_FIELD = "gpu_gap_stddev_us" + +DTYPE_MAP = { + "U8": "uint8", + "U16": "uint16", + "U32": "uint32", + "U64": "uint64", + "I8": "int8", + "I16": "int16", + "I32": "int32", + "I64": "int64", + "F32": "float32", + "F64": "float64", +} + +_CASE_KEY_RE = re.compile(r"^([^\[\]]+)((?:\[[^\[\]=]+=[^\[\]]+\])*)$") +_CASE_AXIS_RE = re.compile(r"\[([^\[\]=]+)=([^\[\]]+)\]") + + +class BaselineError(RuntimeError): + """Raised when baseline/config/run data violates the hard schema.""" + + +@dataclass(frozen=True) +class AxisSpec: + name: str + kind: str + values: Tuple[Any, ...] + + +@dataclass +class OperatorConfigFile: + path: Path + benchmark: str + configs: Dict[str, Dict[str, Any]] + raw: Dict[str, Any] + new_shape: bool + + +@dataclass(frozen=True) +class ConfigRef: + key: str + benchmark: str + path: Path + entry: Dict[str, Any] + + @property + def tier(self) -> Optional[str]: + tier = self.entry.get("tier") + return str(tier) if tier is not None else None + + @property + def operator(self) -> str: + return self.benchmark + + @property + def case_axis_specs(self) -> Tuple[AxisSpec, ...]: + return case_axis_specs(self.entry) + + +@dataclass +class ConfigIndex: + refs_by_key: Dict[str, ConfigRef] + refs_by_operator: Dict[str, List[ConfigRef]] + docs_by_path: Dict[Path, OperatorConfigFile] + + def require(self, config_key: str) -> ConfigRef: + ref = self.refs_by_key.get(config_key) + if ref is None: + raise BaselineError( + f"config_key {config_key!r} does not exist in exactly one operator JSON" + ) + return ref + + +@dataclass(frozen=True) +class RunMeasurement: + config_key: str + benchmark: str + language: str + case_key: str + sku: str + gpu_time_us: float + gpu_noise_us: float + gpu_bwutil: float + source: Path + row_number: int + tier: Optional[str] = None + + +@dataclass(frozen=True) +class LanguageMetric: + gpu_time_us: float + gpu_noise_us: float + gpu_bwutil: float + + +@dataclass(frozen=True) +class BaselineUpdate: + config_key: str + benchmark: str + case_key: str + sku: str + n_runs: int + language_metrics: Dict[str, LanguageMetric] + # Unbiased sample standard deviation across paired artifact-level + # (Python GPU time - C++ GPU time) gaps. It is absent when fewer than two + # paired artifacts are available or the input aggregation lost pairing. + gpu_gap_stddev_us: Optional[float] = None + + +def _gpu_gap_stats( + update: BaselineUpdate, +) -> Optional[Tuple[int, float, float]]: + """Return (count, mean, M2) when artifact-level gap dispersion is known.""" + cpp = update.language_metrics.get("cpp") + python = update.language_metrics.get("python") + if cpp is None or python is None: + return None + + gap_mean = python.gpu_time_us - cpp.gpu_time_us + if update.n_runs == 1: + # A raw benchmark artifact is one valid paired gap observation, but one + # observation cannot independently expose a sample standard deviation. + return 1, gap_mean, 0.0 + if update.gpu_gap_stddev_us is None: + return None + return ( + update.n_runs, + gap_mean, + update.gpu_gap_stddev_us**2 * (update.n_runs - 1), + ) + + +def _pooled_gpu_gap_stddev(updates: Sequence[BaselineUpdate]) -> Optional[float]: + """Pool paired-gap sample variances without assuming language independence.""" + count = 0 + mean = 0.0 + m2 = 0.0 + for update in updates: + stats = _gpu_gap_stats(update) + if stats is None: + return None + item_count, item_mean, item_m2 = stats + combined_count = count + item_count + if count: + delta = item_mean - mean + m2 += item_m2 + delta * delta * count * item_count / combined_count + mean += delta * item_count / combined_count + else: + mean = item_mean + m2 = item_m2 + count = combined_count + + if count < 2: + return None + return math.sqrt(max(m2, 0.0) / (count - 1)) + + +def _is_missing(value: Any) -> bool: + if value is None: + return True + try: + # Keep case-key expansion and JSON validation stdlib-only for lightweight + # lifecycle checks. Dataframe import/update paths load pandas on demand. + import pandas as pd + + missing = pd.isna(value) + except (ImportError, TypeError, ValueError): + missing = False + if isinstance(missing, bool): + if missing: + return True + if isinstance(value, str): + return value.strip() == "" + return False + + +def _finite_float(value: Any, *, label: str) -> float: + try: + out = float(value) + except (TypeError, ValueError) as exc: + raise BaselineError(f"{label} must be numeric, got {value!r}") from exc + if not math.isfinite(out): + raise BaselineError(f"{label} must be finite, got {value!r}") + return out + + +def _canonical_int(value: Any) -> str: + raw = str(value).strip() + try: + number = float(raw) + except ValueError as exc: + raise BaselineError(f"expected integer axis value, got {value!r}") from exc + if not math.isfinite(number) or not number.is_integer(): + raise BaselineError(f"expected integer axis value, got {value!r}") + return str(int(number)) + + +def _canonical_float(value: Any) -> str: + raw = str(value).strip() + try: + number = float(raw) + except ValueError as exc: + raise BaselineError( + f"expected floating-point axis value, got {value!r}" + ) from exc + if not math.isfinite(number): + raise BaselineError(f"expected finite floating-point axis value, got {value!r}") + return str(number) + + +def _declared_value_strings(spec: AxisSpec) -> List[str]: + out: List[str] = [] + for value in spec.values: + if spec.kind == "int64": + out.append(_canonical_int(value)) + elif spec.kind == "float64": + out.append(_canonical_float(value)) + elif spec.kind == "dtype": + out.append(DTYPE_MAP.get(str(value).strip(), str(value).strip())) + else: + out.append(str(value).strip()) + return out + + +def canonical_axis_value(spec: AxisSpec, value: Any) -> str: + if _is_missing(value): + raise BaselineError(f"missing value for axis {spec.name!r}") + + if spec.kind == "int64": + candidate = _canonical_int(value) + elif spec.kind == "float64": + candidate = _canonical_float(value) + elif spec.kind == "dtype": + candidate = DTYPE_MAP.get(str(value).strip(), str(value).strip()) + else: + candidate = str(value).strip() + + declared = _declared_value_strings(spec) + if candidate not in declared: + raise BaselineError( + f"axis {spec.name!r} value {candidate!r} is not declared " + f"(allowed: {declared})" + ) + return candidate + + +def case_axis_specs(entry: Dict[str, Any]) -> Tuple[AxisSpec, ...]: + specs: List[AxisSpec] = [ + AxisSpec("InOutDataType", "dtype", tuple(entry.get("dtypes", []))) + ] + for group_name, kind in ( + ("string_axes", "string"), + ("int64_axes", "int64"), + ("float64_axes", "float64"), + ): + axes = entry.get(group_name, {}) + if not isinstance(axes, dict): + raise BaselineError(f"{group_name} must be a dictionary") + for axis_name, values in axes.items(): + if not isinstance(values, list): + raise BaselineError(f"axis {axis_name!r} values must be a list") + specs.append(AxisSpec(axis_name, kind, tuple(values))) + return tuple(specs) + + +def case_key_from_axis_values( + config_key: str, axis_values: Sequence[Tuple[str, str]] +) -> str: + return str(config_key) + "".join( + f"[{axis_name}={axis_value}]" for axis_name, axis_value in axis_values + ) + + +def expected_case_keys_for_entry(config_key: str, entry: Dict[str, Any]) -> List[str]: + specs = case_axis_specs(entry) + return [ + case_key_from_axis_values( + config_key, + [(spec.name, value) for spec, value in zip(specs, values)], + ) + for values in product(*(_declared_value_strings(spec) for spec in specs)) + ] + + +def fake_planar_pairing_issues( + configs: Mapping[str, Mapping[str, Any]], +) -> Tuple[str, ...]: + """Return deterministic violations of the FakePlanar comparison contract. + + Every FakePlanar case is an advanced-tier, Tensor-only reference row. Its + signature must have exactly one same-tier native-planar case after removing + only the layout axis. Native-only signatures are allowed because a benchmark + may select a representative subset for layout-conversion comparisons. + """ + + native_layouts = {"NCHW", "CHW"} + fake_layouts = {"NCHW_FAKE", "CHW_FAKE"} + groups: Dict[Tuple[str, Tuple[Tuple[str, str], ...]], Dict[str, List[str]]] = {} + + for config_key in sorted(configs): + entry = configs[config_key] + if not isinstance(entry, Mapping): + raise BaselineError(f"config {config_key!r} must be an object") + tier = str(entry.get("tier", "")) + for case_key in expected_case_keys_for_entry(config_key, dict(entry)): + _, axes = parse_case_key(case_key) + axis_map = dict(axes) + layout = axis_map.get("layout") + if layout not in native_layouts | fake_layouts: + continue + signature = ( + tier, + tuple( + sorted((name, value) for name, value in axes if name != "layout") + ), + ) + kind = "fake" if layout in fake_layouts else "native" + groups.setdefault(signature, {"fake": [], "native": []})[kind].append( + case_key + ) + + issues = [] + for (tier, identity), cases in sorted(groups.items(), key=lambda item: item[0]): + fake_cases = sorted(cases["fake"]) + if not fake_cases: + continue + native_cases = sorted(cases["native"]) + reasons = [] + if tier != "advanced": + reasons.append(f"tier is {tier or ''}, expected advanced") + if dict(identity).get("inputKind") != "Tensor": + reasons.append("inputKind is not Tensor") + if len(fake_cases) != 1: + reasons.append( + f"signature expands to {len(fake_cases)} FakePlanar cases, expected exactly 1" + ) + if len(native_cases) != 1: + reasons.append( + f"found {len(native_cases)} same-tier native NCHW/CHW matches, expected exactly 1" + ) + if reasons: + issues.append(f"{', '.join(fake_cases)}: {'; '.join(reasons)}") + return tuple(issues) + + +def parse_case_key(case_key: str) -> Tuple[str, Tuple[Tuple[str, str], ...]]: + match = _CASE_KEY_RE.fullmatch(case_key) + if not match: + raise BaselineError( + f"invalid case key syntax {case_key!r}; expected " + "'[axis=value]...'" + ) + config_key, axis_blob = match.groups() + axes = tuple(_CASE_AXIS_RE.findall(axis_blob)) + if not axes or "".join(f"[{name}={value}]" for name, value in axes) != axis_blob: + raise BaselineError(f"invalid axis syntax in case key {case_key!r}") + return config_key, axes + + +def validate_case_key_for_config( + case_key: str, ref: ConfigRef +) -> Tuple[Tuple[str, str], ...]: + parsed_config_key, axes = parse_case_key(case_key) + if parsed_config_key != ref.key: + raise BaselineError( + f"case key config {parsed_config_key!r} does not match nested " + f"config {ref.key!r}" + ) + + specs = ref.case_axis_specs + expected_names = tuple(spec.name for spec in specs) + actual_names = tuple(name for name, _ in axes) + if actual_names != expected_names: + raise BaselineError( + f"case key {case_key!r} has axes {list(actual_names)}, expected " + f"{list(expected_names)}" + ) + + canonical_axes: List[Tuple[str, str]] = [] + for spec, (_, raw_value) in zip(specs, axes): + canonical = canonical_axis_value(spec, raw_value) + if canonical != raw_value: + raise BaselineError( + f"case key {case_key!r} uses non-canonical value {raw_value!r} " + f"for axis {spec.name!r}; expected {canonical!r}" + ) + canonical_axes.append((spec.name, canonical)) + return tuple(canonical_axes) + + +def _read_json(path: Path) -> Dict[str, Any]: + with open(path, "r") as f: + raw = json.load(f) + if not isinstance(raw, dict): + raise BaselineError(f"{path}: JSON root must be an object") + return raw + + +def _entry_without_benchmark(entry: Dict[str, Any]) -> Dict[str, Any]: + return {k: v for k, v in entry.items() if k != "benchmark"} + + +def _entry_without_baselines(entry: Dict[str, Any]) -> Dict[str, Any]: + return {k: v for k, v in entry.items() if k != "baselines"} + + +def split_operator_payload(path: Path, raw: Dict[str, Any]) -> OperatorConfigFile: + if "configs" in raw or "benchmark" in raw: + benchmark = raw.get("benchmark") + configs = raw.get("configs") + if not isinstance(benchmark, str) or not benchmark: + raise BaselineError(f"{path}: top-level 'benchmark' must be a string") + if not isinstance(configs, dict): + raise BaselineError(f"{path}: top-level 'configs' must be an object") + for key, entry in configs.items(): + if not isinstance(key, str) or not key: + raise BaselineError(f"{path}: config keys must be non-empty strings") + if not isinstance(entry, dict): + raise BaselineError(f"{path}: config {key!r} must be an object") + return OperatorConfigFile(path, benchmark, configs, raw, True) + + benchmark_values = { + entry.get("benchmark") + for entry in raw.values() + if isinstance(entry, dict) and isinstance(entry.get("benchmark"), str) + } + if len(benchmark_values) != 1: + raise BaselineError( + f"{path}: old-shape operator config must contain exactly one " + f"benchmark value, found {sorted(benchmark_values)!r}" + ) + configs = {} + for key, entry in raw.items(): + if not isinstance(key, str) or not key: + raise BaselineError(f"{path}: config keys must be non-empty strings") + if not isinstance(entry, dict): + raise BaselineError(f"{path}: config {key!r} must be an object") + configs[key] = _entry_without_benchmark(entry) + return OperatorConfigFile(path, next(iter(benchmark_values)), configs, raw, False) + + +def split_operator_document(path: Path) -> OperatorConfigFile: + return split_operator_payload(path, _read_json(path)) + + +def operator_config_paths(operators_dir: Path = DEFAULT_OPERATORS_DIR) -> List[Path]: + return sorted(Path(operators_dir).glob("*.json")) + + +def load_config_index( + operators_dir: Path = DEFAULT_OPERATORS_DIR, + paths: Optional[Sequence[Path]] = None, +) -> ConfigIndex: + docs: Dict[Path, OperatorConfigFile] = {} + refs_by_key: Dict[str, ConfigRef] = {} + refs_by_operator: Dict[str, List[ConfigRef]] = {} + duplicates: Dict[str, List[Path]] = {} + + selected_paths = [ + Path(p) + for p in (paths if paths is not None else operator_config_paths(operators_dir)) + ] + for path in selected_paths: + doc = split_operator_document(path) + docs[path] = doc + for config_key, entry in doc.configs.items(): + if config_key in refs_by_key: + duplicates.setdefault( + config_key, [refs_by_key[config_key].path] + ).append(path) + continue + ref = ConfigRef(config_key, doc.benchmark, path, entry) + refs_by_key[config_key] = ref + refs_by_operator.setdefault(doc.benchmark, []).append(ref) + + if duplicates: + details = ", ".join( + f"{key}: {[str(p) for p in paths]}" for key, paths in duplicates.items() + ) + raise BaselineError(f"duplicate config key(s): {details}") + + return ConfigIndex(refs_by_key, refs_by_operator, docs) + + +def operator_document_to_new_shape(doc: OperatorConfigFile) -> Dict[str, Any]: + if doc.new_shape: + return doc.raw + + configs: Dict[str, Dict[str, Any]] = {} + for key, entry in doc.configs.items(): + configs[key] = _entry_without_benchmark(entry) + return {"benchmark": doc.benchmark, "configs": configs} + + +def write_json(path: Path, payload: Dict[str, Any]) -> None: + path.write_text(json.dumps(payload, indent=4) + "\n") + + +def row_axis_values(row: pd.Series, ref: ConfigRef) -> Tuple[Tuple[str, str], ...]: + expected_specs = ref.case_axis_specs + expected_names = {spec.name for spec in expected_specs} + + extra_axes: List[str] = [] + for column in row.index: + if column in RUN_METADATA_COLUMNS: + continue + if column.startswith("Unnamed:"): + continue + if column in expected_names: + continue + if not _is_missing(row[column]): + extra_axes.append(column) + if extra_axes: + raise BaselineError( + f"unknown non-metadata axis column(s) for config {ref.key!r}: " + f"{extra_axes}" + ) + + axis_values: List[Tuple[str, str]] = [] + for spec in expected_specs: + if spec.name not in row.index or _is_missing(row[spec.name]): + if len(spec.values) != 1: + raise BaselineError( + f"row for config {ref.key!r} is missing axis column " + f"{spec.name!r}" + ) + raw_value = spec.values[0] + else: + raw_value = row[spec.name] + axis_values.append((spec.name, canonical_axis_value(spec, raw_value))) + return tuple(axis_values) + + +def case_key_from_row(row: pd.Series, ref: ConfigRef) -> str: + return case_key_from_axis_values(ref.key, row_axis_values(row, ref)) + + +def load_sku_map( + sku_map_path: Optional[Path] = DEFAULT_SKU_MAP_PATH, *, strict: bool = False +) -> Dict[Tuple[str, int, int], str]: + if not sku_map_path or not Path(sku_map_path).is_file(): + if strict: + raise BaselineError(f"sku_map.json not found at {sku_map_path}") + return {} + try: + raw = json.loads(Path(sku_map_path).read_text()) + except (OSError, json.JSONDecodeError) as exc: + if strict: + raise BaselineError( + f"could not parse sku_map.json at {sku_map_path}" + ) from exc + return {} + + out: Dict[Tuple[str, int, int], str] = {} + for idx, entry in enumerate(raw.get("entries", [])): + try: + name = str(entry["gpu_name"]) + cap = int(round(float(entry["power_cap_w"]))) + clock = int(round(float(entry["locked_sm_clock_mhz"]))) + stem = str(entry["stem"]) + except (KeyError, TypeError, ValueError) as exc: + if strict: + raise BaselineError( + f"invalid sku_map.json entry #{idx}: {entry!r}" + ) from exc + continue + out[(name, cap, clock)] = stem + return out + + +def sku_stems( + sku_map_path: Optional[Path] = DEFAULT_SKU_MAP_PATH, *, strict: bool = False +) -> Set[str]: + return set(load_sku_map(sku_map_path, strict=strict).values()) + + +def resolve_sku_for_row( + row: pd.Series, sku_map: Dict[Tuple[str, int, int], str] +) -> str: + missing = [ + column + for column in (DEVICE_NAME_COLUMN, POWER_CAP_COLUMN, LOCKED_CLOCK_COLUMN) + if column not in row.index or _is_missing(row[column]) + ] + if missing: + raise BaselineError(f"missing SKU routing column(s): {missing}") + name = str(row[DEVICE_NAME_COLUMN]).strip() + cap = int(round(_finite_float(row[POWER_CAP_COLUMN], label=POWER_CAP_COLUMN))) + clock = int( + round(_finite_float(row[LOCKED_CLOCK_COLUMN], label=LOCKED_CLOCK_COLUMN)) + ) + sku = sku_map.get((name, cap, clock)) + if not sku: + raise BaselineError(f"({name!r}, {cap!r}, {clock!r}) is not in sku_map.json") + return sku + + +def _validate_required_run_columns(df: pd.DataFrame, source: Path) -> None: + missing = sorted(RUN_REQUIRED_COLUMNS - set(df.columns)) + if missing: + raise BaselineError(f"{source}: missing required column(s): {missing}") + + +def _row_label(path: Path, row_number: int) -> str: + return f"{path}:{row_number}" + + +def measurement_from_row( + row: pd.Series, + *, + source: Path, + row_number: int, + index: ConfigIndex, + sku_map: Dict[Tuple[str, int, int], str], +) -> RunMeasurement: + config_key = str(row[CONFIG_KEY_COLUMN]).strip() + ref = index.require(config_key) + + benchmark = str(row["Benchmark"]).strip() + if benchmark != ref.benchmark: + raise BaselineError( + f"Benchmark {benchmark!r} does not match config {config_key!r}'s " + f"operator {ref.benchmark!r}" + ) + + language = str(row["Language"]).strip() + if language not in LANGUAGES: + raise BaselineError( + f"Language must be one of {sorted(LANGUAGES)}, got {language!r}" + ) + + sku = resolve_sku_for_row(row, sku_map) + gpu_time_us = _finite_float(row[GPU_TIME_COLUMN], label=GPU_TIME_COLUMN) + gpu_noise_us = _finite_float(row[GPU_NOISE_US_COLUMN], label=GPU_NOISE_US_COLUMN) + gpu_bwutil = _finite_float(row[BWUTIL_COLUMN], label=BWUTIL_COLUMN) + if gpu_time_us <= 0: + raise BaselineError(f"{GPU_TIME_COLUMN} must be > 0, got {gpu_time_us}") + if gpu_noise_us < 0: + raise BaselineError(f"{GPU_NOISE_US_COLUMN} must be >= 0, got {gpu_noise_us}") + if gpu_bwutil < 0: + raise BaselineError(f"{BWUTIL_COLUMN} must be >= 0, got {gpu_bwutil}") + + case_key = case_key_from_row(row, ref) + return RunMeasurement( + config_key=config_key, + benchmark=benchmark, + language=language, + case_key=case_key, + sku=sku, + gpu_time_us=gpu_time_us, + gpu_noise_us=gpu_noise_us, + gpu_bwutil=gpu_bwutil, + source=source, + row_number=row_number, + tier=ref.tier, + ) + + +def aggregate_measurements( + measurements: Sequence[RunMeasurement], +) -> Dict[Tuple[str, str, str], BaselineUpdate]: + grouped: Dict[Tuple[str, str, str, str], List[RunMeasurement]] = {} + for measurement in measurements: + grouped.setdefault( + ( + measurement.config_key, + measurement.case_key, + measurement.sku, + measurement.language, + ), + [], + ).append(measurement) + + by_case: Dict[ + Tuple[str, str, str], Dict[str, Tuple[int, float, float, float, str]] + ] = {} + benchmark_by_case: Dict[Tuple[str, str, str], str] = {} + errors: List[str] = [] + + for (config_key, case_key, sku, language), rows in grouped.items(): + n = len(rows) + avg_time = sum(row.gpu_time_us for row in rows) / n + avg_noise = sum(row.gpu_noise_us for row in rows) / n + avg_bwutil = sum(row.gpu_bwutil for row in rows) / n + case = (config_key, case_key, sku) + benchmark_by_case[case] = rows[0].benchmark + by_case.setdefault(case, {})[language] = ( + n, + avg_time, + avg_noise, + avg_bwutil, + language, + ) + + updates: Dict[Tuple[str, str, str], BaselineUpdate] = {} + for case, per_language in by_case.items(): + counts = {payload[0] for payload in per_language.values()} + if len(counts) != 1: + counts_by_language = {lang: data[0] for lang, data in per_language.items()} + errors.append( + f"{case}: repeated-run counts differ by language: " + f"{counts_by_language}" + ) + continue + n_runs = counts.pop() + updates[case] = BaselineUpdate( + config_key=case[0], + benchmark=benchmark_by_case[case], + case_key=case[1], + sku=case[2], + n_runs=n_runs, + language_metrics={ + lang: LanguageMetric( + gpu_time_us=payload[1], + gpu_noise_us=payload[2], + gpu_bwutil=payload[3], + ) + for lang, payload in per_language.items() + }, + ) + + if errors: + raise BaselineError("invalid repeated-run groups:\n " + "\n ".join(errors)) + return updates + + +def merge_baseline_updates( + updates: Iterable[BaselineUpdate], +) -> Dict[Tuple[str, str, str], BaselineUpdate]: + grouped: Dict[Tuple[str, str, str], List[BaselineUpdate]] = {} + for update in updates: + grouped.setdefault((update.config_key, update.case_key, update.sku), []).append( + update + ) + + merged: Dict[Tuple[str, str, str], BaselineUpdate] = {} + errors: List[str] = [] + for key, items in grouped.items(): + benchmarks = {item.benchmark for item in items} + if len(benchmarks) != 1: + errors.append(f"{key}: conflicting benchmark names {sorted(benchmarks)}") + continue + + per_language: Dict[str, Tuple[int, float, float, float]] = {} + for item in items: + for language, metric in item.language_metrics.items(): + n_prev, time_prev, noise_prev, bwutil_prev = per_language.get( + language, (0, 0.0, 0.0, 0.0) + ) + per_language[language] = ( + n_prev + item.n_runs, + time_prev + metric.gpu_time_us * item.n_runs, + noise_prev + metric.gpu_noise_us * item.n_runs, + bwutil_prev + metric.gpu_bwutil * item.n_runs, + ) + + counts = {payload[0] for payload in per_language.values()} + if len(counts) != 1: + counts_by_language = {lang: data[0] for lang, data in per_language.items()} + errors.append( + f"{key}: repeated-run counts differ by language: " + f"{counts_by_language}" + ) + continue + n_runs = counts.pop() + merged[key] = BaselineUpdate( + config_key=key[0], + benchmark=next(iter(benchmarks)), + case_key=key[1], + sku=key[2], + n_runs=n_runs, + language_metrics={ + language: LanguageMetric( + gpu_time_us=time_sum / n_runs, + gpu_noise_us=noise_sum / n_runs, + gpu_bwutil=bwutil_sum / n_runs, + ) + for language, ( + _count, + time_sum, + noise_sum, + bwutil_sum, + ) in per_language.items() + }, + gpu_gap_stddev_us=_pooled_gpu_gap_stddev(items), + ) + + if errors: + raise BaselineError("invalid baseline update groups:\n " + "\n ".join(errors)) + return merged + + +def measurements_from_dataframe( + df: pd.DataFrame, + *, + index: ConfigIndex, + sku_map_path: Path = DEFAULT_SKU_MAP_PATH, + source: Path = Path(""), +) -> List[RunMeasurement]: + sku_map = load_sku_map(sku_map_path, strict=True) + errors: List[str] = [] + try: + _validate_required_run_columns(df, source) + except Exception as exc: + errors.append(f"{source}: {exc}") + + measurements: List[RunMeasurement] = [] + if not errors: + for row_idx, row in df.iterrows(): + row_number = int(row_idx) + 2 + try: + measurements.append( + measurement_from_row( + row, + source=source, + row_number=row_number, + index=index, + sku_map=sku_map, + ) + ) + except Exception as exc: + errors.append(f"{_row_label(source, row_number)}: {exc}") + + if errors: + raise BaselineError("invalid benchmark row(s):\n " + "\n ".join(errors)) + return measurements + + +def baseline_payload_from_updates( + updates: Iterable[BaselineUpdate], +) -> Dict[str, Dict[str, Dict[str, Any]]]: + payload: Dict[str, Dict[str, Dict[str, Any]]] = {} + for update in sorted( + updates, + key=lambda item: (item.config_key, item.case_key, item.sku), + ): + case_payload = payload.setdefault(update.case_key, {}) + metrics: Dict[str, Any] = {"n_runs": update.n_runs} + for language in sorted(update.language_metrics): + time_field, noise_field, bwutil_field = METRIC_FIELDS_BY_LANGUAGE[language] + metric = update.language_metrics[language] + metrics[time_field] = metric.gpu_time_us + metrics[noise_field] = metric.gpu_noise_us + metrics[bwutil_field] = metric.gpu_bwutil + if update.gpu_gap_stddev_us is not None: + metrics[GPU_GAP_STDDEV_FIELD] = update.gpu_gap_stddev_us + case_payload[update.sku] = _ordered_metric_object(metrics) + return payload + + +def baseline_payload_from_dataframe( + df: pd.DataFrame, + *, + index: ConfigIndex, + sku_map_path: Path = DEFAULT_SKU_MAP_PATH, + source: Path = Path(""), +) -> Dict[str, Dict[str, Dict[str, Any]]]: + measurements = measurements_from_dataframe( + df, index=index, sku_map_path=sku_map_path, source=source + ) + return baseline_payload_from_updates(aggregate_measurements(measurements).values()) + + +def _positive_int(value: Any, *, label: str) -> int: + if isinstance(value, bool) or not isinstance(value, int) or value <= 0: + raise BaselineError(f"{label} must be a positive integer, got {value!r}") + return value + + +def baseline_updates_from_payload( + payload: Dict[str, Any], + *, + index: ConfigIndex, + sku_stem_set: Set[str], + source: Path = Path(""), +) -> List[BaselineUpdate]: + if not isinstance(payload, dict): + raise BaselineError(f"{source}: baseline JSON root must be an object") + + updates: List[BaselineUpdate] = [] + errors: List[str] = [] + for case_key, case_payload in payload.items(): + try: + config_key, _ = parse_case_key(case_key) + ref = index.require(config_key) + validate_case_key_for_config(case_key, ref) + if not isinstance(case_payload, dict): + raise BaselineError("SKU map must be an object") + + for sku, metric_payload in case_payload.items(): + if sku not in sku_stem_set: + raise BaselineError(f"unknown SKU {sku!r}") + if not isinstance(metric_payload, dict): + raise BaselineError(f"{sku}: metrics must be an object") + + n_runs = _positive_int( + metric_payload.get("n_runs"), label=f"{sku}: n_runs" + ) + language_metrics: Dict[str, LanguageMetric] = {} + for ( + language, + (time_field, noise_field, bwutil_field), + ) in METRIC_FIELDS_BY_LANGUAGE.items(): + has_time = time_field in metric_payload + has_noise = noise_field in metric_payload + has_bwutil = bwutil_field in metric_payload + if len({has_time, has_noise, has_bwutil}) != 1: + raise BaselineError( + f"{sku}: fields {time_field!r}, {noise_field!r}, " + f"and {bwutil_field!r} " + "must be provided together" + ) + if not has_time: + continue + time_us = _finite_float( + metric_payload[time_field], label=f"{sku}: {time_field}" + ) + noise_us = _finite_float( + metric_payload[noise_field], label=f"{sku}: {noise_field}" + ) + bwutil = _finite_float( + metric_payload[bwutil_field], label=f"{sku}: {bwutil_field}" + ) + if time_us <= 0: + raise BaselineError( + f"{sku}: {time_field} must be > 0, got {time_us}" + ) + if noise_us < 0: + raise BaselineError( + f"{sku}: {noise_field} must be >= 0, got {noise_us}" + ) + if bwutil < 0: + raise BaselineError( + f"{sku}: {bwutil_field} must be >= 0, got {bwutil}" + ) + language_metrics[language] = LanguageMetric( + gpu_time_us=time_us, + gpu_noise_us=noise_us, + gpu_bwutil=bwutil, + ) + + if not language_metrics: + raise BaselineError( + f"{sku}: expected at least one complete language metric pair" + ) + + gpu_gap_stddev_us = None + if GPU_GAP_STDDEV_FIELD in metric_payload: + gpu_gap_stddev_us = _finite_float( + metric_payload[GPU_GAP_STDDEV_FIELD], + label=f"{sku}: {GPU_GAP_STDDEV_FIELD}", + ) + if gpu_gap_stddev_us < 0: + raise BaselineError( + f"{sku}: {GPU_GAP_STDDEV_FIELD} must be >= 0, " + f"got {gpu_gap_stddev_us}" + ) + if n_runs < 2: + raise BaselineError( + f"{sku}: {GPU_GAP_STDDEV_FIELD} requires n_runs >= 2" + ) + missing_gap_languages = sorted(LANGUAGES - set(language_metrics)) + if missing_gap_languages: + raise BaselineError( + f"{sku}: {GPU_GAP_STDDEV_FIELD} requires paired " + f"C++/Python metrics; missing {missing_gap_languages}" + ) + + updates.append( + BaselineUpdate( + config_key=config_key, + benchmark=ref.benchmark, + case_key=case_key, + sku=sku, + n_runs=n_runs, + language_metrics=language_metrics, + gpu_gap_stddev_us=gpu_gap_stddev_us, + ) + ) + except Exception as exc: + errors.append(f"{source}: {case_key}: {exc}") + + if errors: + raise BaselineError("invalid baseline JSON:\n " + "\n ".join(errors)) + return updates + + +def baseline_updates_from_jsons( + paths: Sequence[Path], + *, + index: ConfigIndex, + sku_map_path: Path = DEFAULT_SKU_MAP_PATH, +) -> Dict[Tuple[str, str, str], BaselineUpdate]: + stems = sku_stems(sku_map_path, strict=True) + updates: List[BaselineUpdate] = [] + errors: List[str] = [] + for path in paths: + try: + raw = json.loads(Path(path).read_text()) + updates.extend( + baseline_updates_from_payload( + raw, + index=index, + sku_stem_set=stems, + source=Path(path), + ) + ) + except Exception as exc: + errors.append(f"{path}: {exc}") + + if errors: + raise BaselineError("invalid baseline JSON input(s):\n " + "\n ".join(errors)) + return merge_baseline_updates(updates) + + +def _ordered_metric_object(metrics: Dict[str, Any]) -> Dict[str, Any]: + ordered = { + field: metrics[field] for field in BASELINE_METRIC_FIELDS if field in metrics + } + if GPU_GAP_STDDEV_FIELD in metrics: + ordered[GPU_GAP_STDDEV_FIELD] = metrics[GPU_GAP_STDDEV_FIELD] + for key, value in metrics.items(): + if key not in ordered: + ordered[key] = value + return ordered + + +def apply_updates_to_document( + doc: OperatorConfigFile, + updates: Iterable[BaselineUpdate], +) -> Dict[str, Any]: + raw = operator_document_to_new_shape(doc) + configs = raw["configs"] + + for update in updates: + entry = configs[update.config_key] + baselines = entry.setdefault("baselines", {}) + if not isinstance(baselines, dict): + raise BaselineError( + f"{doc.path}: config {update.config_key!r} has non-object baselines" + ) + case_payload = baselines.setdefault(update.case_key, {}) + if not isinstance(case_payload, dict): + raise BaselineError( + f"{doc.path}: case {update.case_key!r} has non-object SKU map" + ) + metric_payload = dict(case_payload.get(update.sku, {})) + metric_payload["n_runs"] = update.n_runs + for language, metric in update.language_metrics.items(): + time_field, noise_field, bwutil_field = METRIC_FIELDS_BY_LANGUAGE[language] + metric_payload[time_field] = metric.gpu_time_us + metric_payload[noise_field] = metric.gpu_noise_us + metric_payload[bwutil_field] = metric.gpu_bwutil + if update.gpu_gap_stddev_us is None: + # Never retain dispersion from an older baseline when the new + # source did not preserve artifact-level C++/Python pairing. + metric_payload.pop(GPU_GAP_STDDEV_FIELD, None) + else: + metric_payload[GPU_GAP_STDDEV_FIELD] = update.gpu_gap_stddev_us + case_payload[update.sku] = _ordered_metric_object(metric_payload) + + return raw + + +def validate_baseline_update_quality( + update: BaselineUpdate, + *, + criteria: BenchmarkQualityCriteria = DEFAULT_BENCHMARK_QUALITY, +) -> List[str]: + errors: List[str] = [] + missing_languages = sorted(LANGUAGES - set(update.language_metrics)) + if missing_languages: + errors.append( + f"{update.case_key}: {update.sku}: missing language metric(s) " + f"{missing_languages}; baseline quality requires C++/Python parity" + ) + return errors + + for language in sorted(update.language_metrics): + metric = update.language_metrics[language] + if metric.gpu_time_us <= 0: + errors.append( + f"{update.case_key}: {update.sku}: {language} GPU time must be > 0, " + f"got {metric.gpu_time_us}" + ) + continue + noise_pct = criteria.noise_pct(metric.gpu_time_us, metric.gpu_noise_us) + if criteria.noise_exceeds_limit(noise_pct): + errors.append( + f"{update.case_key}: {update.sku}: {language} noise " + f"{noise_pct:.2f}% exceeds {criteria.max_noise_pct:.2f}% " + f"({metric.gpu_noise_us:.6g}us / {metric.gpu_time_us:.6g}us)" + ) + + cpp = update.language_metrics.get("cpp") + python = update.language_metrics.get("python") + if cpp is None or python is None or cpp.gpu_time_us <= 0: + return errors + + diff_pct, diff_us = criteria.parity_deltas(cpp.gpu_time_us, python.gpu_time_us) + if criteria.relative_parity_exceeds_limit(diff_pct): + errors.append( + f"{update.case_key}: {update.sku}: C++/Python parity {diff_pct:+.2f}% " + f"exceeds {criteria.max_perf_diff_pct:.2f}% " + f"(cpp={cpp.gpu_time_us:.6g}us, python={python.gpu_time_us:.6g}us)" + ) + if criteria.absolute_parity_exceeds_limit(diff_us): + errors.append( + f"{update.case_key}: {update.sku}: C++/Python parity {diff_us:+.2f}us " + f"exceeds {criteria.max_perf_diff_us:.2f}us " + f"(cpp={cpp.gpu_time_us:.6g}us, python={python.gpu_time_us:.6g}us)" + ) + return errors + + +def validate_baseline_updates_quality( + updates: Iterable[BaselineUpdate], + *, + criteria: BenchmarkQualityCriteria = DEFAULT_BENCHMARK_QUALITY, +) -> List[str]: + errors: List[str] = [] + for update in updates: + errors.extend(validate_baseline_update_quality(update, criteria=criteria)) + return errors + + +def validate_baselines_in_document( + doc: OperatorConfigFile, + index: ConfigIndex, + sku_stem_set: Set[str], + *, + config_key_filter: Optional[Set[str]] = None, + criteria: BenchmarkQualityCriteria = DEFAULT_BENCHMARK_QUALITY, +) -> List[str]: + errors: List[str] = [] + for config_key, entry in doc.configs.items(): + if config_key_filter is not None and config_key not in config_key_filter: + continue + ref = index.refs_by_key.get(config_key) + if ref is None: + errors.append(f"{doc.path}: config {config_key!r} is not indexed") + continue + baselines = entry.get("baselines", {}) + if baselines is None: + baselines = {} + if not isinstance(baselines, dict): + errors.append(f"{doc.path}: {config_key}: baselines must be an object") + continue + try: + expected_case_keys = expected_case_keys_for_entry(config_key, entry) + except BaselineError as exc: + errors.append(f"{doc.path}: {config_key}: {exc}") + continue + for case_key in expected_case_keys: + case_payload = baselines.get(case_key) + if case_payload is None: + errors.append( + f"{doc.path}: {config_key}: missing baseline case {case_key}" + ) + continue + if isinstance(case_payload, dict): + missing_skus = sorted(sku_stem_set - set(case_payload)) + if missing_skus: + errors.append( + f"{doc.path}: {config_key}: {case_key}: " + f"missing SKU baseline(s) {missing_skus}" + ) + for case_key, case_payload in baselines.items(): + try: + validate_case_key_for_config(case_key, ref) + except BaselineError as exc: + errors.append(f"{doc.path}: {config_key}: {case_key}: {exc}") + continue + if not isinstance(case_payload, dict): + errors.append( + f"{doc.path}: {config_key}: {case_key}: SKU map must be an object" + ) + continue + for sku, metric_payload in case_payload.items(): + if sku not in sku_stem_set: + errors.append( + f"{doc.path}: {config_key}: {case_key}: unknown SKU {sku!r}" + ) + if not isinstance(metric_payload, dict): + errors.append( + f"{doc.path}: {config_key}: {case_key}: {sku}: " + "metrics must be an object" + ) + continue + missing = [ + field + for field in BASELINE_METRIC_FIELDS + if field not in metric_payload + ] + if missing: + errors.append( + f"{doc.path}: {config_key}: {case_key}: {sku}: " + f"missing metric field(s) {missing}" + ) + continue + metric_errors: List[str] = [] + n_runs = metric_payload["n_runs"] + if ( + isinstance(n_runs, bool) + or not isinstance(n_runs, int) + or n_runs <= 0 + ): + metric_errors.append( + f"{doc.path}: {config_key}: {case_key}: {sku}: " + f"n_runs must be a positive integer, got {n_runs!r}" + ) + language_metrics: Dict[str, LanguageMetric] = {} + for field_name in BASELINE_METRIC_FIELDS[1:]: + value = metric_payload[field_name] + if isinstance(value, bool) or not isinstance(value, (int, float)): + metric_errors.append( + f"{doc.path}: {config_key}: {case_key}: {sku}: " + f"{field_name} must be numeric, got {value!r}" + ) + continue + if not math.isfinite(float(value)) or float(value) < 0: + metric_errors.append( + f"{doc.path}: {config_key}: {case_key}: {sku}: " + f"{field_name} must be finite and >= 0, got {value!r}" + ) + gpu_gap_stddev_us = None + if GPU_GAP_STDDEV_FIELD in metric_payload: + value = metric_payload[GPU_GAP_STDDEV_FIELD] + if isinstance(value, bool) or not isinstance(value, (int, float)): + metric_errors.append( + f"{doc.path}: {config_key}: {case_key}: {sku}: " + f"{GPU_GAP_STDDEV_FIELD} must be numeric, got {value!r}" + ) + elif not math.isfinite(float(value)) or float(value) < 0: + metric_errors.append( + f"{doc.path}: {config_key}: {case_key}: {sku}: " + f"{GPU_GAP_STDDEV_FIELD} must be finite and >= 0, " + f"got {value!r}" + ) + elif isinstance(n_runs, int) and not isinstance(n_runs, bool): + if n_runs < 2: + metric_errors.append( + f"{doc.path}: {config_key}: {case_key}: {sku}: " + f"{GPU_GAP_STDDEV_FIELD} requires n_runs >= 2" + ) + else: + gpu_gap_stddev_us = float(value) + if metric_errors: + errors.extend(metric_errors) + continue + for language, fields in METRIC_FIELDS_BY_LANGUAGE.items(): + time_field, noise_field, bwutil_field = fields + language_metrics[language] = LanguageMetric( + gpu_time_us=float(metric_payload[time_field]), + gpu_noise_us=float(metric_payload[noise_field]), + gpu_bwutil=float(metric_payload[bwutil_field]), + ) + update = BaselineUpdate( + config_key=config_key, + benchmark=ref.benchmark, + case_key=case_key, + sku=sku, + n_runs=n_runs, + language_metrics=language_metrics, + gpu_gap_stddev_us=gpu_gap_stddev_us, + ) + for quality_error in validate_baseline_update_quality( + update, criteria=criteria + ): + errors.append(f"{doc.path}: {config_key}: {quality_error}") + return errors diff --git a/bench/_internal/check_bench_sku.py b/bench/_internal/check_bench_sku.py new file mode 100755 index 000000000..7765f06fe --- /dev/null +++ b/bench/_internal/check_bench_sku.py @@ -0,0 +1,201 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""Pre-flight benchmark-SKU eligibility check for CI. + +The benchmark regression check (``compare_to_baseline.py``) routes a run to a +per-SKU JSON baseline by the ``(Device Name, Power Cap (W), Locked SM Clock (MHz))`` +triple via ``bench/config/sku_map.json``. Only SKUs present in that map have +a baseline, so a benchmark leg that lands on a GPU outside the map (e.g. an H100 +PCIe at a non-canonical 310 W TDP, or silicon that clamps ``-lgc 1095`` to +1005 MHz) cannot be compared and should be retried on a different node. + +This script predicts the routing key *without* locking the clock — which is why +it is safe to run in the lightweight pre-bench environment stage: + +* Power cap comes straight from ``nvidia-smi --query-gpu=power.max_limit`` + (the same value ``run_bench.py`` stamps as ``Power Cap (W)``). +* The locked SM clock ``run_bench.py`` will commit to is the highest entry of + its preferred list (``BENCH_LOCK_SM_CLOCK_MHZ``, default 1095,1005,900,750) + that the device actually exposes in ``clocks.gr.supported`` — so a card whose + supported list lacks 1095 is exactly the one that would clamp to 1005. + +Exit codes: + 0 eligible (triple is in sku_map.json), or fail-open: the GPU model is not + covered by the map, or nvidia-smi is *absent* (FileNotFoundError). These + are genuinely "cannot/should not gate" cases — do not block scheduling. + 3 ineligible: the GPU model IS in the map but its predicted (power, clock) + has no baseline. Callers should exclude this node and retry elsewhere. + 1 error: nvidia-smi is present but a query failed or timed out. Callers + should fail fast rather than mislabel the node as ineligible — a tooling + failure is not a bad node, and retrying elsewhere would not fix it. +""" + +import json +import os +import subprocess +import sys +from pathlib import Path + +EXIT_ELIGIBLE = 0 +EXIT_ERROR = 1 +EXIT_INELIGIBLE = 3 + + +class NvidiaSmiError(RuntimeError): + """nvidia-smi is present but a query failed — distinct from the binary being + absent, so the caller can fail fast instead of silently passing eligibility.""" + + +# Mirrors run_bench.py's _CLOCK_LOCK_PREFERRED_MHZ so the predicted lock target +# matches what the bench run will actually commit to. +_PREFERRED_MHZ = [ + int(x) + for x in os.environ.get("BENCH_LOCK_SM_CLOCK_MHZ", "1095,1005,900,750").split(",") + if x.strip() +] + + +def _smi(*args): + """Run nvidia-smi. Returns stdout on success, or None only when the binary + is absent (FileNotFoundError) — the sole "can't check, skip" case. A + present-but-failing nvidia-smi (non-zero exit or timeout) raises + NvidiaSmiError so the caller fails fast instead of silently passing.""" + try: + r = subprocess.run( + ["nvidia-smi", *args], capture_output=True, text=True, timeout=10 + ) + except FileNotFoundError: + return None + except subprocess.TimeoutExpired as exc: + raise NvidiaSmiError(f"nvidia-smi timed out for args {args}") from exc + if r.returncode != 0: + raise NvidiaSmiError( + f"nvidia-smi failed for args {args}: rc={r.returncode}, " + f"stderr={r.stderr.strip()}" + ) + return r.stdout + + +def _gpu_name(): + out = _smi("--query-gpu=gpu_name", "--format=csv,noheader", "-i", "0") + return out.strip().splitlines()[0].strip() if out else None + + +def _power_cap_w(): + out = _smi( + "--query-gpu=power.max_limit", "--format=csv,noheader,nounits", "-i", "0" + ) + if not out: + return None + try: + return int(round(float(out.strip().splitlines()[0]))) + except (IndexError, ValueError): + return None + + +def _supported_sm_clocks(): + """Graphics (SM) clocks the driver will accept for -lgc, via + ``nvidia-smi -q -d SUPPORTED_CLOCKS``. Returns a set, or None on failure.""" + out = _smi("-q", "-d", "SUPPORTED_CLOCKS", "-i", "0") + if not out: + return None + clocks = set() + in_graphics = False + for line in out.splitlines(): + s = line.strip() + if s.startswith("Graphics"): + in_graphics = True + head = s.split(":", 1) + if len(head) == 2 and head[1].strip(): + try: + clocks.add(int(head[1].split()[0])) + except (IndexError, ValueError): + pass + elif s.startswith("Memory") or s.startswith("SM"): + in_graphics = False + elif in_graphics and s.endswith("MHz"): + try: + clocks.add(int(s.split()[0])) + except (IndexError, ValueError): + pass + return clocks or None + + +def predict_locked_clock(supported, preferred=None): + """Highest preferred clock present in `supported` (what -lgc lands on). + `supported` is a set of MHz ints, or None if it couldn't be queried — in + which case fall back to preferred[0] (what run_bench.py requests blindly).""" + pref = preferred if preferred is not None else _PREFERRED_MHZ + if supported is None: + return pref[0] if pref else None + for mhz in pref: + if mhz in supported: + return mhz + return None + + +def decide(name, power, clock, entries): + """Pure eligibility decision. Returns (exit_code, message).""" + if name is None: + return EXIT_ELIGIBLE, "nvidia-smi unavailable; skipping SKU eligibility check." + + known_names = {e["gpu_name"] for e in entries} + if name not in known_names: + # Not a baseline-backed GPU model at all — not this gate's concern. + return EXIT_ELIGIBLE, f"'{name}' is not in sku_map.json; skipping (fail-open)." + + allowed = [ + (e["power_cap_w"], e["locked_sm_clock_mhz"]) + for e in entries + if e["gpu_name"] == name + ] + if (power, clock) in allowed: + return ( + EXIT_ELIGIBLE, + f"OK: {name} @ {power}W / {clock}MHz is a baseline-backed SKU.", + ) + + return EXIT_INELIGIBLE, ( + f"INELIGIBLE: {name} @ {power}W / predicted {clock}MHz has no baseline " + f"(allowed for this model: {allowed}). Node should be excluded and the " + f"leg retried elsewhere." + ) + + +def main(): + sku_map_path = Path(__file__).resolve().parent.parent / "config" / "sku_map.json" + entries = json.loads(sku_map_path.read_text()).get("entries", []) + + try: + name = _gpu_name() + power = _power_cap_w() if name is not None else None + clock = ( + predict_locked_clock(_supported_sm_clocks()) if name is not None else None + ) + except NvidiaSmiError as exc: + print(f"[bench-sku] ERROR: {exc}", file=sys.stderr) + return EXIT_ERROR + + code, msg = decide(name, power, clock, entries) + print( + f"[bench-sku] {msg}", file=sys.stderr if code != EXIT_ELIGIBLE else sys.stdout + ) + return code + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/bench/_internal/quality.py b/bench/_internal/quality.py new file mode 100644 index 000000000..1c7bd9a53 --- /dev/null +++ b/bench/_internal/quality.py @@ -0,0 +1,72 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""Shared benchmark quality thresholds. + +The default values match the CI benchmark gate. Runtime validation, committed +baseline verification, and baseline imports must all read from this module so +the quality policy cannot drift between collection and persistence. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Tuple + + +_FLOAT_TOLERANCE = 1e-9 + + +def exceeds_limit(value: float, limit: float) -> bool: + return value > limit + _FLOAT_TOLERANCE + + +@dataclass(frozen=True) +class BenchmarkQualityCriteria: + max_noise_pct: float + max_perf_diff_pct: float + max_perf_diff_us: float + + def noise_pct(self, gpu_time_us: float, gpu_noise_us: float) -> float: + return gpu_noise_us / gpu_time_us * 100.0 + + def parity_deltas( + self, cpp_time_us: float, python_time_us: float + ) -> Tuple[float, float]: + return ( + (python_time_us / cpp_time_us - 1.0) * 100.0, + python_time_us - cpp_time_us, + ) + + def noise_exceeds_limit(self, noise_pct: float) -> bool: + return exceeds_limit(noise_pct, self.max_noise_pct) + + def relative_parity_exceeds_limit(self, diff_pct: float) -> bool: + return exceeds_limit(abs(diff_pct), self.max_perf_diff_pct) + + def absolute_parity_exceeds_limit(self, diff_us: float) -> bool: + return exceeds_limit(abs(diff_us), self.max_perf_diff_us) + + def parity_exceeds_limit(self, diff_pct: float, diff_us: float) -> bool: + return self.relative_parity_exceeds_limit( + diff_pct + ) or self.absolute_parity_exceeds_limit(diff_us) + + +DEFAULT_BENCHMARK_QUALITY = BenchmarkQualityCriteria( + max_noise_pct=10.0, + max_perf_diff_pct=10.0, + max_perf_diff_us=100.0, +) diff --git a/bench/_internal/update_baseline.py b/bench/_internal/update_baseline.py new file mode 100755 index 000000000..33f2629cc --- /dev/null +++ b/bench/_internal/update_baseline.py @@ -0,0 +1,535 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""Validate and import run_bench.py JSON artifacts into committed baselines.""" + +from __future__ import annotations + +import argparse +import re +import subprocess +import sys +from pathlib import Path +from typing import Dict, List, Optional, Sequence, Set, Tuple + +# Support direct source-tree invocation from any working directory. +if __package__ in (None, ""): + sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from _internal.axes import format_axes +from _internal.baselines import ( + DEFAULT_CONFIG_DIR, + DEFAULT_MAX_BASELINE_REGRESSION_PCT, + BaselineError, + BaselineUpdate, + OperatorConfigFile, + apply_updates_to_document, + baseline_updates_from_jsons, + load_config_index, + sku_stems, + validate_baselines_in_document, + validate_baseline_updates_quality, + write_json, +) + + +def _collect_jsons(paths: Sequence[Path]) -> List[Path]: + jsons: List[Path] = [] + for path in paths: + if not path.exists(): + raise BaselineError(f"--from path does not exist: {path}") + if path.is_file(): + if path.suffix.lower() == ".json": + jsons.append(path) + else: + raise BaselineError(f"--from file is not a .json file: {path}") + else: + found_jsons = sorted(path.glob("*.json")) + if not found_jsons: + raise BaselineError(f"--from directory has no *.json files: {path}") + jsons.extend(found_jsons) + if not jsons: + raise BaselineError("no input files collected from --from arguments") + + seen: Set[Path] = set() + deduped_jsons: List[Path] = [] + for path in jsons: + resolved = path.resolve() + if resolved in seen: + continue + seen.add(resolved) + deduped_jsons.append(path) + return deduped_jsons + + +def _normalize_operators(raw: Optional[Sequence[str]]) -> Optional[Set[str]]: + if not raw: + return None + out: Set[str] = set() + for item in raw: + for token in str(item).split(","): + token = token.strip() + if token: + out.add(token) + return out or None + + +_OP_FILE_RE = re.compile(r"^src/cvcuda/priv/Op([A-Za-z0-9_]+)\.(?:cu|cpp|hpp|h)$") + + +def _repo_root() -> Path: + return Path(__file__).resolve().parents[2] + + +def _resolve_diff_ref(requested: Optional[str]) -> str: + if requested: + return requested + for candidate in ("origin/main", "main"): + rc = subprocess.run( + ["git", "rev-parse", "--verify", "--quiet", candidate], + cwd=_repo_root(), + capture_output=True, + ).returncode + if rc == 0: + return candidate + raise BaselineError( + "--from-diff could not find origin/main or main. Pass an explicit ref." + ) + + +def _ops_from_git_diff(ref: str, bench_names: Set[str]) -> Set[str]: + proc = subprocess.run( + ["git", "diff", "--name-only", f"{ref}...HEAD"], + cwd=_repo_root(), + capture_output=True, + text=True, + ) + if proc.returncode != 0: + raise BaselineError(f"`git diff` failed:\n{proc.stderr.strip()}") + + matched: Set[str] = set() + seen_unmatched: List[str] = [] + for filename in [line.strip() for line in proc.stdout.splitlines() if line.strip()]: + match = _OP_FILE_RE.match(filename) + if not match: + continue + stem = match.group(1).lower() + if stem in bench_names: + matched.add(stem) + else: + seen_unmatched.append(f"{filename} -> {stem!r}") + + if not matched: + message = ( + f"--from-diff (ref={ref}) found no operator files matching " + "src/cvcuda/priv/Op*.{cu,cpp,hpp,h} whose stem is also present in " + "the input benchmark JSON." + ) + if seen_unmatched: + message += "\n Files inspected:\n " + "\n ".join(seen_unmatched) + raise BaselineError(message) + return matched + + +def _updates_for_operators( + updates: Dict[Tuple[str, str, str], BaselineUpdate], + operators: Optional[Set[str]], +) -> Dict[Tuple[str, str, str], BaselineUpdate]: + if operators is None: + return dict(updates) + return { + key: update for key, update in updates.items() if update.benchmark in operators + } + + +def _existing_metrics( + doc: OperatorConfigFile, update: BaselineUpdate, language: str +) -> Optional[float]: + entry = doc.configs.get(update.config_key, {}) + baselines = entry.get("baselines", {}) + case_payload = ( + baselines.get(update.case_key, {}) if isinstance(baselines, dict) else {} + ) + metric_payload = ( + case_payload.get(update.sku, {}) if isinstance(case_payload, dict) else {} + ) + field_name = "gpu_time_us_cpp" if language == "cpp" else "gpu_time_us_python" + value = metric_payload.get(field_name) if isinstance(metric_payload, dict) else None + return float(value) if isinstance(value, (int, float)) else None + + +def _diff_summary( + docs_by_path: Dict[Path, OperatorConfigFile], + updates: Sequence[BaselineUpdate], +) -> List[dict]: + rows: List[dict] = [] + for update in updates: + doc = docs_by_path[ + next( + path + for path, doc in docs_by_path.items() + if update.config_key in doc.configs + ) + ] + _, axes = update.case_key.split("[", 1) + axes_tuple = tuple( + tuple(part.rstrip("]").split("=", 1)) + for part in ("[" + axes).split("[") + if part + ) + for language, metric in update.language_metrics.items(): + prev_us = _existing_metrics(doc, update, language) + pct = ( + (metric.gpu_time_us / prev_us - 1.0) * 100.0 + if prev_us is not None and prev_us > 0 + else None + ) + rows.append( + { + "benchmark": update.benchmark, + "config_key": update.config_key, + "language": language, + "sku": update.sku, + "axes": axes_tuple, + "prev_us": prev_us, + "new_us": metric.gpu_time_us, + "pct": pct, + "n_runs": update.n_runs, + } + ) + return rows + + +def _baseline_regression_errors( + diffs: Sequence[dict], + max_regression_pct: float, +) -> List[str]: + errors: List[str] = [] + for item in diffs: + pct = item["pct"] + if pct is None or pct <= max_regression_pct: + continue + label = ( + f"{item['sku']} {item['config_key']} [{item['language']}] " + f"({format_axes(item['axes'])})" + ) + errors.append( + f"{label}: {pct:+.2f}% " + f"({item['prev_us']:.2f} -> {item['new_us']:.2f} us)" + ) + return errors + + +def _format_regression_error( + regressions: Sequence[str], + max_regression_pct: float, +) -> str: + shown = list(regressions[:80]) + omitted = len(regressions) - len(shown) + message = ( + f"refusing to import same-key baseline regressions > " + f"{max_regression_pct:.2f}%:\n " + "\n ".join(shown) + ) + if omitted > 0: + message += f"\n ... {omitted} more row(s) omitted." + message += ( + "\nUse --allow-regressions only for an intentional, reviewed baseline reset." + ) + return message + + +def _print_plan( + updates_by_path: Dict[Path, List[BaselineUpdate]], + operators: Optional[Set[str]], +) -> None: + print("--- update plan (dry run) ---") + print(f"Operators: {sorted(operators) if operators is not None else '(all)'}") + for path, updates in sorted(updates_by_path.items()): + skus = sorted({update.sku for update in updates}) + print(f" {path}: {len(updates)} case/SKU update(s), SKU(s) {skus}") + print("--- end plan; re-run without --dry-run to write. ---") + + +def _print_summary(diffs: List[dict], operators: Optional[Set[str]]) -> None: + if not diffs: + return + ops_label = sorted(operators) if operators is not None else "(all)" + print() + print(f"=== summary (operators: {ops_label}) ===") + diffs_sorted = sorted( + diffs, + key=lambda item: ( + item["sku"], + item["benchmark"], + item["config_key"], + item["language"], + ), + ) + for item in diffs_sorted[:80]: + label = ( + f"{item['sku']} {item['config_key']} [{item['language']}] " + f"({format_axes(item['axes'])})" + ) + if item["prev_us"] is None: + print(f" NEW {item['new_us']:>9.2f} us {label}") + else: + print( + f" {item['pct']:+7.2f}% {item['prev_us']:>9.2f} -> " + f"{item['new_us']:>9.2f} us {label}" + ) + if len(diffs_sorted) > 80: + print(f" ... {len(diffs_sorted) - 80} more row(s) omitted.") + + +def _format_markdown_summary(diffs: List[dict], operators: Optional[Set[str]]) -> str: + lines: List[str] = [] + ops_label = ", ".join(sorted(operators)) if operators is not None else "(all)" + lines.append(f"# Baseline update -- operators: {ops_label}") + lines.append("") + lines.append("| SKU | Delta | Before (us) | After (us) | n_runs | Benchmark |") + lines.append("|---|---:|---:|---:|---:|---|") + for item in diffs: + axes_str = format_axes(item["axes"]) + label = f"`{item['config_key']}` [{item['language']}] ({axes_str})" + if item["prev_us"] is None: + delta = "**NEW**" + before = "-" + else: + delta = f"{item['pct']:+.2f}%" + before = f"{item['prev_us']:.2f}" + lines.append( + f"| {item['sku']} | {delta} | {before} | {item['new_us']:.2f} | " + f"{item['n_runs']} | {label} |" + ) + lines.append("") + return "\n".join(lines) + + +def cmd_update(args: argparse.Namespace) -> int: + config_dir = args.config_dir + operators_dir = config_dir / "operators" + sku_map_path = args.sku_map or (config_dir / "sku_map.json") + if args.max_regression_pct < 0: + raise BaselineError("--max-regression-pct must be >= 0") + + jsons = _collect_jsons(args.from_paths) + index = load_config_index(operators_dir) + all_updates = baseline_updates_from_jsons( + jsons, index=index, sku_map_path=sku_map_path + ) + + bench_names = {update.benchmark for update in all_updates.values()} + if args.from_diff is not None: + ref = _resolve_diff_ref(args.from_diff) + operators = _ops_from_git_diff(ref, bench_names) + print(f"--from-diff: resolved {ref}...HEAD -> {sorted(operators)}") + else: + operators = _normalize_operators(args.operators) + if operators is not None: + missing = sorted(operators - bench_names) + if missing: + raise BaselineError( + f"--operator names not found in any input JSON: {missing}. " + f"Known benchmark names across inputs: {sorted(bench_names)}" + ) + + updates = _updates_for_operators(all_updates, operators) + if not updates: + print("WARN: no matching rows to update.") + return 0 + + quality_errors = validate_baseline_updates_quality(updates.values()) + if quality_errors: + raise BaselineError( + "refusing to import quality-violating JSON baselines:\n " + + "\n ".join(quality_errors) + ) + + updates_by_path: Dict[Path, List[BaselineUpdate]] = {} + for update in updates.values(): + path = index.require(update.config_key).path + updates_by_path.setdefault(path, []).append(update) + + diffs = _diff_summary(index.docs_by_path, list(updates.values())) + if not args.allow_regressions: + regressions = _baseline_regression_errors(diffs, args.max_regression_pct) + if regressions: + raise BaselineError( + _format_regression_error(regressions, args.max_regression_pct) + ) + + new_payloads: Dict[Path, dict] = {} + for path, path_updates in updates_by_path.items(): + doc = index.docs_by_path[path] + new_payloads[path] = apply_updates_to_document(doc, path_updates) + + stems = sku_stems(sku_map_path, strict=True) + validation_errors: List[str] = [] + for path, payload in new_payloads.items(): + doc = OperatorConfigFile( + path=path, + benchmark=payload["benchmark"], + configs=payload["configs"], + raw=payload, + new_shape=True, + ) + validation_errors.extend(validate_baselines_in_document(doc, index, stems)) + if validation_errors: + raise BaselineError( + "refusing to write invalid JSON baselines:\n " + + "\n ".join(validation_errors) + ) + + if args.dry_run: + _print_plan(updates_by_path, operators) + return 0 + + for path, payload in sorted(new_payloads.items()): + before = path.read_text() if path.exists() else "" + after = __import__("json").dumps(payload, indent=4) + "\n" + if before != after: + write_json(path, payload) + print(f"updated {path}") + else: + print(f"unchanged {path}") + + _print_summary(diffs, operators) + if args.write_summary: + args.write_summary.write_text(_format_markdown_summary(diffs, operators)) + print(f"wrote markdown summary to {args.write_summary}") + + return 0 + + +def _parse_args(argv: Optional[Sequence[str]] = None) -> argparse.Namespace: + p = argparse.ArgumentParser( + description="Update committed operator baselines from benchmark JSON artifacts.", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog="""\ +Examples: + bench/_internal/update_baseline.py --from bench_output.json --operator resize --dry-run + bench/_internal/update_baseline.py --from bench_output.json --operator resize \\ + --write-summary baseline-update.md + bench/_internal/update_baseline.py --from bench_diagnostics/ --from-diff origin/main + +Output: + Updates matching files under /operators/. Use --dry-run first. + +Exit status: + 0 Validation passed and the requested update or dry run completed. + 1 Input, quality, parity, or regression validation failed. + 2 Command syntax is invalid. +""", + ) + p.add_argument( + "--from", + dest="from_paths", + action="append", + metavar="JSON_OR_DIR", + type=Path, + required=True, + help=( + "Read a JSON artifact or all *.json files directly under a " + "directory. Repeatable." + ), + ) + p.add_argument( + "--config-dir", + metavar="DIR", + type=Path, + default=DEFAULT_CONFIG_DIR, + help=( + "Benchmark config directory containing operators/ and sku_map.json " + "(default: bench/config)." + ), + ) + p.add_argument( + "--sku-map", + metavar="JSON", + type=Path, + default=None, + help="SKU map to use (default: /sku_map.json).", + ) + + ops_group = p.add_mutually_exclusive_group() + ops_group.add_argument( + "--operator", + action="append", + default=None, + dest="operators", + metavar="NAME", + help="Update only these operator names; repeatable or comma-separated.", + ) + ops_group.add_argument( + "--from-diff", + nargs="?", + const="", + default=None, + metavar="REF", + help=( + "Update only operators changed between REF and HEAD. With no REF, " + "use origin/main, then main." + ), + ) + + p.add_argument( + "--dry-run", + action="store_true", + help="Validate and show what would change without writing files.", + ) + p.add_argument( + "--write-summary", + metavar="MD", + type=Path, + default=None, + dest="write_summary", + help=( + "After updating, write a Markdown before/after table to MD " + "(not written with --dry-run)." + ), + ) + p.add_argument( + "--max-regression-pct", + type=float, + default=DEFAULT_MAX_BASELINE_REGRESSION_PCT, + help=( + "Maximum allowed same-key slowdown when replacing an existing " + f"baseline metric. Default: {DEFAULT_MAX_BASELINE_REGRESSION_PCT}." + ), + ) + p.add_argument( + "--allow-regressions", + action="store_true", + help=( + "Permit slower replacement values after review. Schema, quality, " + "and parity checks still apply." + ), + ) + p.add_argument("--allow-missing", action="store_true", help=argparse.SUPPRESS) + return p.parse_args(argv) + + +def main(argv: Optional[Sequence[str]] = None) -> int: + args = _parse_args(argv) + try: + return cmd_update(args) + except BaselineError as exc: + print(f"ERROR: {exc}", file=sys.stderr) + raise SystemExit(1) from exc + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/bench/_internal/validate_baselines.py b/bench/_internal/validate_baselines.py new file mode 100755 index 000000000..734ad3d6a --- /dev/null +++ b/bench/_internal/validate_baselines.py @@ -0,0 +1,367 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""Validate committed benchmark baseline schema, quality, and parity.""" + +from __future__ import annotations + +import argparse +import json +import subprocess +import sys +from pathlib import Path +from typing import Dict, List, Optional, Sequence, Set, Tuple + +# Support direct source-tree invocation from any working directory. +if __package__ in (None, ""): + sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from _internal.axes import format_axes +from _internal.baselines import ( + DEFAULT_CONFIG_DIR, + DEFAULT_MAX_BASELINE_REGRESSION_PCT, + BaselineError, + OperatorConfigFile, + load_config_index, + operator_config_paths, + parse_case_key, + sku_stems, + split_operator_payload, + validate_baselines_in_document, +) + +BaselineMetricKey = Tuple[str, str, str, str] +BaselineMetricValue = Tuple[float, Tuple[Tuple[str, str], ...]] +GIT_COMMAND_TIMEOUT_SECONDS = 30 + + +def _parse_list(raw: Optional[Sequence[str]]) -> Optional[Set[str]]: + if not raw: + return None + out: Set[str] = set() + for item in raw: + for token in str(item).split(","): + token = token.strip() + if token: + out.add(token) + return out or None + + +def _selected_paths(args: argparse.Namespace) -> List[Path]: + operators_dir = args.config_dir / "operators" + if args.paths: + return [Path(path) for path in args.paths] + operators = _parse_list(args.operator) + if operators is None: + return operator_config_paths(operators_dir) + return [operators_dir / f"{operator}.json" for operator in sorted(operators)] + + +def _repo_root() -> Path: + return Path(__file__).resolve().parents[2] + + +def _git_command(*args: str) -> List[str]: + repo_root = _repo_root() + # CI may check out files under a UID different from the benchmark container's UID. + return ["git", "-c", f"safe.directory={repo_root}", *args] + + +def _path_at_git_ref(path: Path, repo_root: Path) -> str: + try: + relpath = path.resolve().relative_to(repo_root.resolve()) + except ValueError as exc: + raise BaselineError( + f"{path}: path is outside git repository {repo_root}" + ) from exc + return relpath.as_posix() + + +def _verify_git_ref(ref: str) -> None: + # The ^{commit} suffix forces annotated tags and other refs to resolve to a commit object. + try: + proc = subprocess.run( + _git_command("rev-parse", "--verify", "--quiet", f"{ref}^{{commit}}"), + cwd=_repo_root(), + capture_output=True, + timeout=GIT_COMMAND_TIMEOUT_SECONDS, + ) + except subprocess.TimeoutExpired as exc: + raise BaselineError( + f"timed out verifying baseline regression ref: {ref}" + ) from exc + if proc.returncode != 0: + raise BaselineError(f"baseline regression ref does not exist: {ref}") + + +def _operator_doc_at_ref(ref: str, path: Path) -> Optional[OperatorConfigFile]: + git_path = _path_at_git_ref(path, _repo_root()) + try: + proc = subprocess.run( + _git_command("show", f"{ref}:{git_path}"), + cwd=_repo_root(), + capture_output=True, + text=True, + timeout=GIT_COMMAND_TIMEOUT_SECONDS, + ) + except subprocess.TimeoutExpired as exc: + raise BaselineError(f"timed out reading {ref}:{git_path}") from exc + if proc.returncode != 0: + return None + try: + raw = json.loads(proc.stdout) + except json.JSONDecodeError as exc: + raise BaselineError(f"{ref}:{git_path}: invalid JSON: {exc}") from exc + if not isinstance(raw, dict): + raise BaselineError(f"{ref}:{git_path}: JSON root must be an object") + return split_operator_payload(path, raw) + + +def _baseline_metrics( + doc: OperatorConfigFile, + config_key_filter: Optional[Set[str]] = None, +) -> Dict[BaselineMetricKey, BaselineMetricValue]: + rows: Dict[BaselineMetricKey, BaselineMetricValue] = {} + for config_key, entry in doc.configs.items(): + if config_key_filter is not None and config_key not in config_key_filter: + continue + baselines = entry.get("baselines", {}) + if not isinstance(baselines, dict): + continue + for case_key, case_payload in baselines.items(): + if not isinstance(case_payload, dict): + continue + try: + _, axes = parse_case_key(case_key) + except BaselineError: + axes = () + for sku, metric_payload in case_payload.items(): + if not isinstance(metric_payload, dict): + continue + for language, field_name in ( + ("cpp", "gpu_time_us_cpp"), + ("python", "gpu_time_us_python"), + ): + value = metric_payload.get(field_name) + if isinstance(value, (int, float)): + rows[(config_key, case_key, str(sku), language)] = ( + float(value), + axes, + ) + return rows + + +def _baseline_regression_errors_against_ref( + docs_by_path: Dict[Path, OperatorConfigFile], + paths: Sequence[Path], + ref: str, + max_regression_pct: float, + config_key_filter: Optional[Set[str]], +) -> List[str]: + _verify_git_ref(ref) + errors: List[str] = [] + for path in paths: + current_doc = docs_by_path[path] + base_doc = _operator_doc_at_ref(ref, path) + if base_doc is None: + continue + base_rows = _baseline_metrics(base_doc, config_key_filter) + for key, (current_us, axes) in _baseline_metrics( + current_doc, config_key_filter + ).items(): + base_row = base_rows.get(key) + if base_row is None: + continue + base_us, _ = base_row + if base_us <= 0: + continue + pct = (current_us / base_us - 1.0) * 100.0 + if pct <= max_regression_pct: + continue + config_key, case_key, sku, language = key + errors.append( + f"{path}: {sku} {config_key} [{language}] " + f"({format_axes(axes)}): same-key baseline regressed {pct:+.2f}% " + f"vs {ref} ({base_us:.2f} -> {current_us:.2f} us, " + f"case={case_key})" + ) + return errors + + +def _parse_args(argv: Optional[Sequence[str]] = None) -> argparse.Namespace: + p = argparse.ArgumentParser( + description="Validate committed baselines and optionally compare them with a git ref.", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog="""\ +Examples: + bench/_internal/validate_baselines.py + bench/_internal/validate_baselines.py --operator resize,gaussian + bench/_internal/validate_baselines.py --reject-regressions-from origin/main + +Exit status: + 0 All selected baselines are valid. + 1 Baseline validation or regression checks failed. + 2 Inputs, configuration, or git reference are invalid. +""", + ) + p.add_argument( + "paths", + nargs="*", + metavar="OPERATOR_JSON", + type=Path, + help=( + "Operator JSON file(s) to validate (default: every file under " + "/operators). Takes precedence over --operator." + ), + ) + p.add_argument( + "--config-dir", + metavar="DIR", + type=Path, + default=DEFAULT_CONFIG_DIR, + help=( + "Benchmark config directory containing operators/ and sku_map.json " + "(default: bench/config)." + ), + ) + p.add_argument( + "--sku-map", + metavar="JSON", + type=Path, + default=None, + help="SKU map to use (default: /sku_map.json).", + ) + p.add_argument( + "--operator", + action="append", + default=None, + metavar="NAME", + help="Validate named operator(s); repeatable or comma-separated.", + ) + p.add_argument( + "--config-key", + action="append", + default=None, + metavar="KEY", + help="Validate named config key(s); repeatable or comma-separated.", + ) + p.add_argument( + "--reject-regressions-from", + metavar="REF", + default=None, + help=( + "Compare same-key timings with git REF and fail on slowdowns above " + "the threshold." + ), + ) + p.add_argument( + "--max-regression-pct", + type=float, + default=DEFAULT_MAX_BASELINE_REGRESSION_PCT, + help=( + "Maximum allowed same-key committed-baseline slowdown when " + f"--reject-regressions-from is set. Default: " + f"{DEFAULT_MAX_BASELINE_REGRESSION_PCT}." + ), + ) + p.add_argument( + "--allow-regressions", + action="store_true", + help=( + "Permit reviewed timing slowdowns; requires " + "--reject-regressions-from. Schema, quality, and parity checks " + "still apply." + ), + ) + return p.parse_args(argv) + + +def main(argv: Optional[Sequence[str]] = None) -> int: + args = _parse_args(argv) + sku_map_path = args.sku_map or (args.config_dir / "sku_map.json") + paths = _selected_paths(args) + + try: + if args.max_regression_pct < 0: + raise BaselineError("--max-regression-pct must be >= 0") + if args.allow_regressions and not args.reject_regressions_from: + raise BaselineError( + "--allow-regressions requires --reject-regressions-from" + ) + missing = [path for path in paths if not path.is_file()] + if missing: + raise BaselineError(f"operator JSON file(s) not found: {missing}") + index = load_config_index(args.config_dir / "operators") + stems = sku_stems(sku_map_path, strict=True) + config_key_filter = _parse_list(args.config_key) + if config_key_filter is not None: + unknown = sorted(config_key_filter - set(index.refs_by_key)) + if unknown: + raise BaselineError(f"unknown config key(s): {unknown}") + + errors: List[str] = [] + docs_by_path: Dict[Path, OperatorConfigFile] = {} + for path in paths: + doc = index.docs_by_path.get(path) + if doc is None: + selected = load_config_index(paths=[path]) + doc = selected.docs_by_path[path] + docs_by_path[path] = doc + errors.extend( + validate_baselines_in_document( + doc, + index, + stems, + config_key_filter=config_key_filter, + ) + ) + if not errors and args.reject_regressions_from: + if args.allow_regressions: + _verify_git_ref(args.reject_regressions_from) + else: + errors.extend( + _baseline_regression_errors_against_ref( + docs_by_path, + paths, + args.reject_regressions_from, + args.max_regression_pct, + config_key_filter, + ) + ) + except Exception as exc: + print(f"ERROR: {exc}", file=sys.stderr) + return 2 + + if errors: + print("JSON baseline validation failed:", file=sys.stderr) + for error in errors: + print(f" {error}", file=sys.stderr) + return 1 + + if args.allow_regressions: + print( + "WARNING: reviewed same-key baseline regressions are allowed; " + "schema, quality, and parity checks remain enforced.", + file=sys.stderr, + ) + + scope = ", ".join(str(path) for path in paths) + print(f"JSON baseline validation passed ({len(paths)} file(s)): {scope}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/bench/_internal/warmup.py b/bench/_internal/warmup.py new file mode 100644 index 000000000..2435466c5 --- /dev/null +++ b/bench/_internal/warmup.py @@ -0,0 +1,38 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Shared runtime policy for capping benchmark warmup iterations.""" + +from __future__ import annotations + +import os +from collections.abc import Mapping + + +WARMUP_CAP_ENV = "CVCUDA_BENCH_WARMUP_CAP" +_MAX_CXX_INT = 2_147_483_647 + + +def parse_warmup_cap(raw_value: str) -> int: + """Parse the nonnegative decimal cap accepted by both harnesses.""" + if not raw_value or any(ch < "0" or ch > "9" for ch in raw_value): + raise ValueError(f"{WARMUP_CAP_ENV} must be a nonnegative decimal integer") + + cap = int(raw_value) + if cap > _MAX_CXX_INT: + raise ValueError(f"{WARMUP_CAP_ENV} must not exceed {_MAX_CXX_INT}") + return cap + + +def resolve_warmup_iterations( + configured_iterations: int, environ: Mapping[str, str] | None = None +) -> int: + """Apply the optional process-level cap to a configured warmup count.""" + if configured_iterations <= 0: + return configured_iterations + + env = os.environ if environ is None else environ + raw_cap = env.get(WARMUP_CAP_ENV) + if raw_cap is None: + return configured_iterations + return min(configured_iterations, parse_warmup_cap(raw_cap)) diff --git a/bench/compare_to_baseline.py b/bench/compare_to_baseline.py new file mode 100755 index 000000000..ce4780773 --- /dev/null +++ b/bench/compare_to_baseline.py @@ -0,0 +1,598 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""Compare a run_bench.py JSON artifact with committed per-GPU baselines.""" + +from __future__ import annotations + +import argparse +import sys +import xml.etree.ElementTree as ET +from dataclasses import dataclass, field +from pathlib import Path +from typing import Dict, Iterable, List, Optional, Sequence, Tuple + +from _internal.axes import format_axes +from _internal.baselines import ( + DEFAULT_CONFIG_DIR, + BaselineError, + BaselineUpdate, + ConfigIndex, + ConfigRef, + baseline_updates_from_jsons, + load_config_index, + parse_case_key, + validate_baselines_in_document, + validate_case_key_for_config, + sku_stems, +) + + +@dataclass +class Thresholds: + regression: float = 0.10 + improvement: float = 0.10 + + +@dataclass +class RowResult: + benchmark: str + config_key: str + language: str + axes: Tuple[Tuple[str, str], ...] + base_mean_us: float + cur_gpu_us: float + delta: float + + +@dataclass +class MissingRow: + benchmark: str + config_key: str + language: str + axes: Tuple[Tuple[str, str], ...] + reason: str = "" + + +@dataclass +class CompareResult: + regressions: List[RowResult] = field(default_factory=list) + improvements: List[RowResult] = field(default_factory=list) + missing_in_current: List[MissingRow] = field(default_factory=list) + new_in_current: List[MissingRow] = field(default_factory=list) + missing_sku: List[MissingRow] = field(default_factory=list) + matched: int = 0 + matched_abs_deltas: List[float] = field(default_factory=list) + + @property + def any_fail(self) -> bool: + return bool( + self.regressions + or self.improvements + or self.missing_in_current + or self.new_in_current + or self.missing_sku + ) + + +def _axes_from_case_key(case_key: str) -> Tuple[Tuple[str, str], ...]: + _, axes = parse_case_key(case_key) + return axes + + +def _baseline_rows_for_sku( + index: ConfigIndex, + sku: str, + tiers: Optional[set], + operators: Optional[set[str]] = None, +) -> Dict[Tuple[str, str, str], RowResult]: + rows: Dict[Tuple[str, str, str], RowResult] = {} + for ref in index.refs_by_key.values(): + if operators is not None and ref.benchmark not in operators: + continue + if tiers is not None and ref.tier not in tiers: + continue + baselines = ref.entry.get("baselines", {}) + if not isinstance(baselines, dict): + continue + for case_key, case_payload in baselines.items(): + if not isinstance(case_payload, dict) or sku not in case_payload: + continue + metrics = case_payload[sku] + axes = validate_case_key_for_config(case_key, ref) + for language, time_field in { + "cpp": "gpu_time_us_cpp", + "python": "gpu_time_us_python", + }.items(): + if time_field not in metrics: + continue + key = (ref.key, case_key, language) + rows[key] = RowResult( + benchmark=ref.benchmark, + config_key=ref.key, + language=language, + axes=axes, + base_mean_us=float(metrics[time_field]), + cur_gpu_us=0.0, + delta=0.0, + ) + return rows + + +def _current_rows( + updates: Sequence[BaselineUpdate], +) -> Dict[Tuple[str, str, str], RowResult]: + rows: Dict[Tuple[str, str, str], RowResult] = {} + duplicates: List[str] = [] + for update in updates: + axes = _axes_from_case_key(update.case_key) + for language, metric in update.language_metrics.items(): + key = (update.config_key, update.case_key, language) + if key in rows: + duplicates.append(f"duplicate current JSON metric for {key}") + rows[key] = RowResult( + benchmark=update.benchmark, + config_key=update.config_key, + language=language, + axes=axes, + base_mean_us=0.0, + cur_gpu_us=metric.gpu_time_us, + delta=0.0, + ) + if duplicates: + raise BaselineError("duplicate current metrics:\n " + "\n ".join(duplicates)) + return rows + + +def _case_has_other_sku(ref: ConfigRef, case_key: str, sku: str) -> bool: + baselines = ref.entry.get("baselines", {}) + case_payload = baselines.get(case_key) if isinstance(baselines, dict) else None + return isinstance(case_payload, dict) and case_payload and sku not in case_payload + + +def compare_updates( + updates: Sequence[BaselineUpdate], + *, + index: ConfigIndex, + sku: str, + thresholds: Thresholds = Thresholds(), + operators: Optional[set[str]] = None, +) -> CompareResult: + result = CompareResult() + if not updates: + return result + + if operators is not None: + unknown = operators - set(index.refs_by_operator) + if unknown: + raise BaselineError(f"unknown benchmark operator(s): {sorted(unknown)}") + unexpected = {update.benchmark for update in updates} - operators + if unexpected: + raise BaselineError( + f"current JSON contains unselected operator(s): {sorted(unexpected)}" + ) + + observed_skus = {update.sku for update in updates} + if observed_skus != {sku}: + raise BaselineError( + f"current JSON contains SKU(s) {sorted(observed_skus)}, " + f"but compare resolved {sku!r}" + ) + + current_tiers = set() + for update in updates: + tier = index.require(update.config_key).tier + if tier: + current_tiers.add(tier) + tiers = current_tiers or None + baseline = _baseline_rows_for_sku(index, sku, tiers, operators) + current = _current_rows(updates) + + baseline_keys = set(baseline) + current_keys = set(current) + + for key in sorted(baseline_keys - current_keys): + row = baseline[key] + result.missing_in_current.append( + MissingRow(row.benchmark, row.config_key, row.language, row.axes) + ) + + for key in sorted(current_keys - baseline_keys): + config_key, case_key, language = key + ref = index.require(config_key) + axes = _axes_from_case_key(case_key) + row = MissingRow(ref.benchmark, config_key, language, axes) + if _case_has_other_sku(ref, case_key, sku): + row.reason = f"case exists but SKU {sku!r} is missing" + result.missing_sku.append(row) + else: + row.reason = "case key is not present in JSON baselines" + result.new_in_current.append(row) + + for key in sorted(baseline_keys & current_keys): + base_row = baseline[key] + cur_row = current[key] + base_mean = base_row.base_mean_us + cur_us = cur_row.cur_gpu_us + result.matched += 1 + if base_mean <= 0: + continue + + delta = cur_us / base_mean - 1.0 + result.matched_abs_deltas.append(abs(delta)) + row = RowResult( + benchmark=base_row.benchmark, + config_key=base_row.config_key, + language=base_row.language, + axes=base_row.axes, + base_mean_us=base_mean, + cur_gpu_us=cur_us, + delta=delta, + ) + if delta > thresholds.regression: + result.regressions.append(row) + elif -delta > thresholds.improvement: + result.improvements.append(row) + + return result + + +def _axes_str(axes: Iterable[Tuple[str, str]]) -> str: + return format_axes(axes) + + +def _fmt_row(row: RowResult) -> str: + label = f"{row.config_key} [{row.language}] ({_axes_str(row.axes)})" + return ( + f"{label} -- {row.delta * 100:+.2f}% " + f"({row.base_mean_us:.2f} -> {row.cur_gpu_us:.2f} us)" + ) + + +def _fmt_missing(row: MissingRow) -> str: + suffix = f" -- {row.reason}" if row.reason else "" + return f"{row.config_key} [{row.language}] ({_axes_str(row.axes)}){suffix}" + + +def _median(values: Sequence[float]) -> float: + if not values: + return 0.0 + sorted_vals = sorted(values) + mid = len(sorted_vals) // 2 + if len(sorted_vals) % 2: + return sorted_vals[mid] + return 0.5 * (sorted_vals[mid - 1] + sorted_vals[mid]) + + +def _summary_stats_lines(result: CompareResult) -> List[str]: + if not result.matched_abs_deltas: + return [] + lines = [ + f"- all-rows |Delta|: median {_median(result.matched_abs_deltas) * 100:.2f}%, " + f"max {max(result.matched_abs_deltas) * 100:.2f}%" + ] + flagged_abs = [abs(r.delta) for r in result.regressions + result.improvements] + if flagged_abs: + lines.append( + f"- flagged |Delta|: min {min(flagged_abs) * 100:.2f}%, " + f"median {_median(flagged_abs) * 100:.2f}%, " + f"max {max(flagged_abs) * 100:.2f}%" + ) + return lines + + +def format_markdown( + result: CompareResult, + sku: str, + baseline_label: Path | str, + current_path: Path, + thresholds: Thresholds, +) -> str: + lines: List[str] = [] + lines.append(f"# Performance regression report -- {sku}") + lines.append("") + lines.append(f"- baseline source: `{baseline_label}`") + lines.append(f"- current: `{current_path}`") + lines.append( + f"- thresholds: regression={thresholds.regression:.3g}, " + f"improvement={thresholds.improvement:.3g}" + ) + lines.append( + f"- matched: {result.matched}, " + f"regressions: {len(result.regressions)}, " + f"improvements: {len(result.improvements)}, " + f"missing: {len(result.missing_in_current)}, " + f"new: {len(result.new_in_current)}, " + f"missing SKU: {len(result.missing_sku)}" + ) + lines.extend(_summary_stats_lines(result)) + lines.append("") + + def _row_section(title: str, rows: List[RowResult]) -> None: + lines.append(f"## {title} ({len(rows)})") + if not rows: + lines.append("_none_") + else: + for row in rows: + lines.append(f"- {_fmt_row(row)}") + lines.append("") + + def _missing_section(title: str, rows: List[MissingRow]) -> None: + lines.append(f"## {title} ({len(rows)})") + if not rows: + lines.append("_none_") + else: + for row in rows: + lines.append(f"- {_fmt_missing(row)}") + lines.append("") + + _row_section("Regressions", result.regressions) + _row_section("Unexpected improvements", result.improvements) + _missing_section("Missing in current", result.missing_in_current) + _missing_section("New in current", result.new_in_current) + _missing_section("Missing SKU", result.missing_sku) + return "\n".join(lines) + + +def format_junit(result: CompareResult, sku: str) -> bytes: + failures = ( + len(result.regressions) + + len(result.improvements) + + len(result.missing_in_current) + + len(result.new_in_current) + + len(result.missing_sku) + ) + total = ( + result.matched + + len(result.missing_in_current) + + len(result.new_in_current) + + len(result.missing_sku) + ) + suite = ET.Element( + "testsuite", + { + "name": f"cvcuda.perf_regression.{sku}", + "tests": str(total), + "failures": str(failures), + "errors": "0", + }, + ) + + def _case(name: str, classname: str, message: str, text: str) -> None: + case = ET.SubElement( + suite, + "testcase", + { + "classname": classname, + "name": name, + }, + ) + failure = ET.SubElement( + case, "failure", {"type": classname, "message": message} + ) + failure.text = text + + for row in result.regressions: + _case( + f"{row.config_key}[{row.language}]({_axes_str(row.axes)})", + "cvcuda.perf.regression", + f"GPU time increased by {row.delta * 100:+.2f}% vs baseline", + _fmt_row(row), + ) + for row in result.improvements: + _case( + f"{row.config_key}[{row.language}]({_axes_str(row.axes)})", + "cvcuda.perf.unexpected_improvement", + f"GPU time decreased by {row.delta * 100:+.2f}% vs baseline", + _fmt_row(row), + ) + for category, rows in ( + ("missing_in_current", result.missing_in_current), + ("new_in_current", result.new_in_current), + ("missing_sku", result.missing_sku), + ): + for row in rows: + _case( + f"{row.config_key}[{row.language}]({_axes_str(row.axes)})", + f"cvcuda.perf.{category}", + row.reason or category, + _fmt_missing(row), + ) + + return ET.tostring(suite, encoding="utf-8", xml_declaration=True) + + +def _validate_json_baselines(index: ConfigIndex, sku_map_path: Path) -> None: + stems = sku_stems(sku_map_path, strict=True) + errors: List[str] = [] + for doc in index.docs_by_path.values(): + errors.extend(validate_baselines_in_document(doc, index, stems)) + if errors: + raise BaselineError("invalid JSON baseline entries:\n " + "\n ".join(errors)) + + +def _resolve_current_sku( + updates: Sequence[BaselineUpdate], cli_sku: Optional[str] +) -> Tuple[str, str]: + observed = sorted({update.sku for update in updates}) + if not observed: + raise BaselineError("current JSON contains no baseline metrics") + if cli_sku: + if cli_sku not in observed: + raise BaselineError( + f"--sku {cli_sku!r} is not present in current JSON SKU(s) {observed}" + ) + return cli_sku, "--sku" + if len(observed) != 1: + raise BaselineError( + f"current JSON contains multiple SKU(s) {observed}; pass --sku" + ) + return observed[0], "current JSON SKU key" + + +def _parse_args(argv: Optional[Sequence[str]] = None) -> argparse.Namespace: + p = argparse.ArgumentParser( + description=( + "Compare one benchmark JSON artifact with the committed operator " + "baselines." + ), + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog="""\ +Examples: + compare_to_baseline.py --current bench_output.json + compare_to_baseline.py --current bench_output.json --operator resize \\ + --markdown comparison.md --junit comparison.xml + +Exit status: + 0 All expected rows matched within both thresholds. + 1 A regression, unexpected improvement, or incompatible row was found. + 2 The input artifact or benchmark configuration is invalid. +""", + ) + p.add_argument( + "--current", + metavar="JSON", + type=Path, + required=True, + help="JSON artifact produced by run_bench.py --output.", + ) + p.add_argument( + "--config-dir", + metavar="DIR", + type=Path, + default=DEFAULT_CONFIG_DIR, + help=( + "Benchmark config directory containing operators/ and sku_map.json " + "(default: bench/config)." + ), + ) + p.add_argument( + "--sku-map", + metavar="JSON", + type=Path, + default=None, + help="SKU map to use (default: /sku_map.json).", + ) + p.add_argument( + "--sku", + metavar="SKU", + type=str, + default=None, + help="Compare this SKU key. Omit when the artifact contains one SKU.", + ) + p.add_argument( + "--operator", + type=str, + default=None, + help=( + "Compare only these operator names, comma-separated (default: all). " + "The input JSON must contain only the selected operators." + ), + ) + p.add_argument( + "--regression", + metavar="FRACTION", + type=float, + default=0.10, + help=( + "Fail when a matched result is slower by more than this fraction " + "(default: 0.10 = 10%%)." + ), + ) + p.add_argument( + "--improvement", + metavar="FRACTION", + type=float, + default=0.10, + help=( + "Fail when a matched result is faster by more than this fraction, " + "which can indicate a stale baseline (default: 0.10 = 10%%)." + ), + ) + p.add_argument( + "--junit", + metavar="XML", + type=Path, + default=None, + help="Write a JUnit XML report to XML.", + ) + p.add_argument( + "--markdown", + metavar="MD", + type=Path, + default=None, + help="Write the Markdown report to MD instead of printing it.", + ) + return p.parse_args(argv) + + +def main(argv: Optional[Sequence[str]] = None) -> int: + args = _parse_args(argv) + sku_map_path = args.sku_map or (args.config_dir / "sku_map.json") + operators_dir = args.config_dir / "operators" + + try: + index = load_config_index(operators_dir) + _validate_json_baselines(index, sku_map_path) + operators = None + if args.operator is not None: + operators = { + item.strip() for item in args.operator.split(",") if item.strip() + } + if not operators: + raise BaselineError("--operator must select at least one benchmark") + updates = list( + baseline_updates_from_jsons( + [args.current], index=index, sku_map_path=sku_map_path + ).values() + ) + sku, source = _resolve_current_sku(updates, args.sku) + updates = [update for update in updates if update.sku == sku] + thresholds = Thresholds(args.regression, args.improvement) + result = compare_updates( + updates, + index=index, + sku=sku, + thresholds=thresholds, + operators=operators, + ) + except Exception as exc: + print(f"ERROR: {exc}", file=sys.stderr) + return 2 + + print(f"Resolved SKU: {sku} ({source})") + print( + f"matched={result.matched} regressions={len(result.regressions)} " + f"improvements={len(result.improvements)} " + f"missing={len(result.missing_in_current)} " + f"new={len(result.new_in_current)} missing_sku={len(result.missing_sku)}" + ) + + report = format_markdown(result, sku, operators_dir, args.current, thresholds) + if args.markdown: + args.markdown.write_text(report) + else: + print() + print(report) + + if args.junit: + args.junit.write_bytes(format_junit(result, sku)) + + return 1 if result.any_fail else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/bench/compare_wheels.py b/bench/compare_wheels.py new file mode 100755 index 000000000..3008ebb99 --- /dev/null +++ b/bench/compare_wheels.py @@ -0,0 +1,1207 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Benchmark two CV-CUDA Python wheels with the same current harness. + +The command preserves each raw benchmark run and writes a compatibility and +performance comparison. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import math +import os +import re +import shutil +import subprocess +import sys +import tempfile +import zipfile +from dataclasses import dataclass +from datetime import datetime +from email.parser import Parser +from pathlib import Path +from typing import Any, Optional, Sequence + +import pandas as pd + +from _internal.axes import format_axes +from _internal.baselines import ( + CONFIG_KEY_COLUMN, + DEFAULT_MAX_BASELINE_REGRESSION_PCT, + DEVICE_NAME_COLUMN, + GPU_TIME_COLUMN, + LOCKED_CLOCK_COLUMN, + POWER_CAP_COLUMN, + VBIOS_COLUMN, + BaselineError, + ConfigIndex, + case_key_from_row, + expected_case_keys_for_entry, + load_config_index, + parse_case_key, +) +from config.load_config import ( + load_bench_manifest, + parse_config_key_arg, + parse_operator_arg, + parse_tier_arg, +) +from _internal.quality import DEFAULT_BENCHMARK_QUALITY, exceeds_limit +from _internal.warmup import parse_warmup_cap + +BENCH_DIR = Path(__file__).resolve().parent +RUNNER_FILES = ("run_bench.py",) +INTERNAL_FILES = ( + "__init__.py", + "axes.py", + "baselines.py", + "quality.py", + "warmup.py", +) +CONFIG_FILES = ("bench_params.json", "sku_map.json", "load_config.py", "axis_order.py") +GPU_NOISE_COLUMN = "GPU Noise (%)" +STATUS_COLUMN = "Status" +STABLE_FINGERPRINT_COLUMNS = (DEVICE_NAME_COLUMN, POWER_CAP_COLUMN, VBIOS_COLUMN) +REQUIRED_RUN_COLUMNS = frozenset( + { + "Benchmark", + CONFIG_KEY_COLUMN, + "Language", + GPU_TIME_COLUMN, + GPU_NOISE_COLUMN, + STATUS_COLUMN, + DEVICE_NAME_COLUMN, + POWER_CAP_COLUMN, + LOCKED_CLOCK_COLUMN, + } +) +NORMALIZED_COLUMNS = ( + "benchmark", + "config_key", + "case_key", + "axes", + "gpu_us", + "noise_pct", + "status", + "device_name", + "power_cap", + "vbios", + "clock_mhz", +) +COMPARISON_COLUMNS = ( + "Benchmark", + "config_key", + "case_key", + "axes", + "baseline_gpu_us", + "candidate_gpu_us", + "candidate_delta_pct", + "candidate_speedup", + "baseline_noise_pct", + "candidate_noise_pct", + "baseline_status", + "candidate_status", + "comparison_status", +) +OPERATOR_RESULT_LIMIT = 5 + + +PROBE_SCRIPT = r""" +import importlib.metadata +import json +import sys +from pathlib import Path + +expected_name, expected_version, output_path = sys.argv[1:] + +# python_bench_utils applies the cuda-pathfinder patch used by the benchmarks. +import pandas +import python_bench_utils # noqa: F401 +import cuda.bench +import cvcuda +import cupy + +dist = importlib.metadata.distribution(expected_name) +environment = Path(sys.prefix).resolve() +package = Path(dist.locate_file("cvcuda")).resolve() +module = Path(cvcuda.__file__).resolve() +extension = Path(cvcuda._cvcuda.__file__).resolve() +errors = [] +if dist.version != expected_version: + errors.append(f"installed version {dist.version!r}, expected {expected_version!r}") +for label, path in (("package", package), ("module", module), ("extension", extension)): + try: + path.relative_to(environment) + except ValueError: + errors.append(f"cvcuda {label} is outside the wheel environment: {path}") +try: + module.relative_to(package) + extension.relative_to(package) +except ValueError: + errors.append("cvcuda module/extension do not belong to the installed distribution") + +# Exercise device-backed creation before either long run starts. +stream = cvcuda.Stream() +tensor = cvcuda.Tensor((1, 1, 1, 1), cvcuda.Type.U8, "NHWC") +stream.sync() + +payload = { + "passed": not errors, + "errors": errors, + "python": sys.executable, + "distribution": dist.metadata["Name"], + "distribution_version": dist.version, + "cvcuda_version": getattr(cvcuda, "__version__", None), + "cvcuda_module": str(module), + "cvcuda_extension": str(extension), + "dependencies": { + "pandas": pandas.__version__, + "cupy": cupy.__version__, + "cuda_bench": cuda.bench.__file__, + }, +} +Path(output_path).write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n") +if errors: + raise SystemExit("; ".join(errors)) +""" + + +class WheelComparisonError(RuntimeError): + """Raised when wheel setup or benchmark artifacts are invalid.""" + + +@dataclass(frozen=True) +class WheelMetadata: + path: Path + name: str + canonical_name: str + version: str + sha256: str + + @property + def label(self) -> str: + return f"{self.name} {self.version} [{self.sha256[:12]}] ({self.path})" + + +@dataclass +class Comparison: + table: pd.DataFrame + expected_case_operators: dict[str, str] + missing_baseline_cases: tuple[str, ...] + missing_candidate_cases: tuple[str, ...] + baseline_fingerprint: Optional[tuple[Optional[str], ...]] + candidate_fingerprint: Optional[tuple[Optional[str], ...]] + baseline_clocks: tuple[str, ...] + candidate_clocks: tuple[str, ...] + baseline_row_count: int + candidate_row_count: int + threshold: float + + def rows(self, status: str) -> pd.DataFrame: + return self.table[self.table["comparison_status"] == status] + + @property + def regressions(self) -> pd.DataFrame: + return self.rows("regression").sort_values( + ["candidate_delta_pct", "case_key"], ascending=[False, True] + ) + + @property + def improvements(self) -> pd.DataFrame: + return self.rows("improvement").sort_values(["candidate_delta_pct", "case_key"]) + + @property + def comparable(self) -> pd.DataFrame: + return self.table[ + self.table["comparison_status"].isin( + ("regression", "improvement", "neutral") + ) + ] + + @property + def operator_geomean_speedups(self) -> pd.Series: + if self.comparable.empty: + return pd.Series(dtype=float) + return self.comparable.groupby("Benchmark", sort=True)["candidate_speedup"].agg( + lambda values: math.exp( + sum(math.log(value) for value in values) / len(values) + ) + ) + + @property + def mean_operator_speedup(self) -> Optional[float]: + operator_speedups = self.operator_geomean_speedups + if operator_speedups.empty: + return None + return float(operator_speedups.mean()) + + @property + def shared_expected_cases(self) -> set[str]: + both_present = self.table[ + self.table["comparison_status"].isin( + ("regression", "improvement", "neutral", "excluded") + ) + ] + return set(both_present["case_key"]) & set(self.expected_case_operators) + + @property + def operator_compatibility(self) -> dict[str, str]: + shared_cases = self.shared_expected_cases + cases_by_operator: dict[str, set[str]] = {} + for case_key, operator in self.expected_case_operators.items(): + cases_by_operator.setdefault(operator, set()).add(case_key) + + compatibility = {} + for operator, case_keys in sorted(cases_by_operator.items()): + shared_count = len(case_keys & shared_cases) + if shared_count == len(case_keys): + status = "fully_compatible" + elif shared_count: + status = "partially_compatible" + else: + status = "incompatible_or_not_present" + compatibility[operator] = status + return compatibility + + @property + def failed(self) -> bool: + failures = { + "regression", + "excluded", + "missing_in_candidate", + "only_in_candidate", + "missing_from_both", + } + return bool( + not self.baseline_row_count + or not self.candidate_row_count + or self.table["comparison_status"].isin(failures).any() + or self.missing_baseline_cases + or self.missing_candidate_cases + ) + + +def _canonical_distribution_name(name: str) -> str: + return re.sub(r"[-_.]+", "-", name).lower() + + +def _sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as wheel_file: + for block in iter(lambda: wheel_file.read(1024 * 1024), b""): + digest.update(block) + return digest.hexdigest() + + +def read_wheel_metadata(path: Path) -> WheelMetadata: + path = path.expanduser().resolve() + if not path.is_file() or path.suffix != ".whl": + raise WheelComparisonError( + f"wheel does not exist or is not a .whl file: {path}" + ) + try: + with zipfile.ZipFile(path) as wheel: + metadata_paths = [ + name + for name in wheel.namelist() + if name.endswith(".dist-info/METADATA") and name.count("/") == 1 + ] + if len(metadata_paths) != 1: + raise WheelComparisonError( + f"{path}: expected one top-level .dist-info/METADATA, " + f"found {len(metadata_paths)}" + ) + metadata = Parser().parsestr(wheel.read(metadata_paths[0]).decode()) + except (OSError, UnicodeDecodeError, zipfile.BadZipFile) as exc: + raise WheelComparisonError( + f"could not read wheel metadata from {path}: {exc}" + ) from exc + + name, version = metadata.get("Name"), metadata.get("Version") + if not name or not version: + raise WheelComparisonError( + f"{path}: wheel metadata must contain Name and Version" + ) + canonical_name = _canonical_distribution_name(name) + if canonical_name != "cvcuda" and not canonical_name.startswith("cvcuda-cu"): + raise WheelComparisonError( + f"{path}: {name!r} is not a CV-CUDA wheel distribution" + ) + return WheelMetadata(path, name, canonical_name, version, _sha256(path)) + + +def _resolve_asset(source_dir: Path, *relative_paths: str) -> Path: + for relative_path in relative_paths: + candidate = source_dir / relative_path + if candidate.is_file(): + return candidate + choices = ", ".join(str(source_dir / path) for path in relative_paths) + raise WheelComparisonError(f"benchmark asset not found; checked {choices}") + + +def stage_benchmark_harness(source_dir: Path, destination: Path) -> None: + """Stage the flattened Python benchmark layout installed by CMake.""" + source_dir = source_dir.resolve() + destination.mkdir(parents=True) + for filename in RUNNER_FILES: + shutil.copy2(_resolve_asset(source_dir, filename), destination / filename) + staged_internal = destination / "_internal" + staged_internal.mkdir() + for filename in INTERNAL_FILES: + shutil.copy2( + _resolve_asset(source_dir / "_internal", filename), + staged_internal / filename, + ) + + source_config, staged_config = source_dir / "config", destination / "config" + staged_config.mkdir() + for filename in CONFIG_FILES: + shutil.copy2(_resolve_asset(source_config, filename), staged_config / filename) + shutil.copytree(source_config / "operators", staged_config / "operators") + shutil.copy2( + _resolve_asset( + source_dir, "python_bench_utils.py", "python/python_bench_utils.py" + ), + destination / "python_bench_utils.py", + ) + + manifest_path = source_config / "bench_params.json" + try: + manifest = load_bench_manifest(str(manifest_path)) + except (OSError, ValueError, json.JSONDecodeError) as exc: + raise WheelComparisonError( + f"invalid benchmark manifest {manifest_path}: {exc}" + ) from exc + for operator, spec in manifest.items(): + filename = spec.get("python") + if not filename: + raise WheelComparisonError( + f"benchmark manifest operator {operator!r} has no Python script" + ) + shutil.copy2( + _resolve_asset(source_dir, filename, f"python/ops/{filename}"), + destination / filename, + ) + + +def _clean_subprocess_env() -> dict[str, str]: + env = os.environ.copy() + env.pop("PYTHONHOME", None) + env.pop("PYTHONPATH", None) + env["PYTHONUNBUFFERED"] = "1" + return env + + +def _run_logged( + command: Sequence[str], + log_path: Path, + *, + cwd: Optional[Path] = None, + append: bool = False, +) -> int: + log_path.parent.mkdir(parents=True, exist_ok=True) + with log_path.open("a" if append else "w", encoding="utf-8") as log: + log.write("$ " + " ".join(command) + "\n\n") + log.flush() + try: + return subprocess.run( + list(command), + cwd=cwd, + env=_clean_subprocess_env(), + stdout=log, + stderr=subprocess.STDOUT, + text=True, + check=False, + ).returncode + except OSError as exc: + log.write(f"\n{exc}\n") + return 127 + + +def create_wheel_environment( + base_python: Path, environment_dir: Path, wheel: WheelMetadata, log_path: Path +) -> Path: + create = [ + str(base_python), + "-m", + "venv", + "--system-site-packages", + str(environment_dir), + ] + if _run_logged(create, log_path) != 0: + raise WheelComparisonError( + f"could not create wheel environment; see {log_path}" + ) + environment_python = environment_dir / "bin" / "python" + install = [ + str(environment_python), + "-m", + "pip", + "install", + "--disable-pip-version-check", + "--no-input", + "--force-reinstall", + "--no-deps", + str(wheel.path), + ] + if _run_logged(install, log_path, append=True) != 0: + raise WheelComparisonError(f"could not install {wheel.path}; see {log_path}") + return environment_python + + +def probe_wheel_environment( + environment_python: Path, + harness_dir: Path, + wheel: WheelMetadata, + output_path: Path, + log_path: Path, +) -> None: + command = [ + str(environment_python), + "-c", + PROBE_SCRIPT, + wheel.name, + wheel.version, + str(output_path), + ] + returncode = _run_logged(command, log_path, cwd=harness_dir) + if returncode != 0 or not output_path.is_file(): + raise WheelComparisonError( + f"wheel environment did not pass import/dependency verification; see {log_path}" + ) + payload = json.loads(output_path.read_text()) + if not payload.get("passed"): + raise WheelComparisonError( + f"wheel environment verification failed: {payload.get('errors', [])}" + ) + + +def build_runner_command( + args: argparse.Namespace, + environment_python: Path, + harness_dir: Path, + output_csv: Path, +) -> list[str]: + command = [ + str(environment_python), + str(harness_dir / "run_bench.py"), + "--lang", + "python", + "--no-color", + "--output", + str(output_csv), + "--tier", + args.tier, + "--max-noise-pct", + str(args.max_noise_pct), + ] + for flag, value in ( + ("--operator", args.operator), + ("--config-key", args.config_key), + ("--warmup-cap", args.warmup_cap), + ("--bench-min-time", args.bench_min_time), + ("--bench-max-noise", args.bench_max_noise), + ): + if value is not None: + command.extend([flag, str(value)]) + return command + [str(harness_dir)] + + +def run_benchmark( + args: argparse.Namespace, + environment_python: Path, + harness_dir: Path, + output_csv: Path, + log_path: Path, +) -> int: + return _run_logged( + build_runner_command(args, environment_python, harness_dir, output_csv), + log_path, + cwd=harness_dir, + ) + + +def _optional_text(value: Any) -> Optional[str]: + if value is None or pd.isna(value): + return None + if isinstance(value, float) and value.is_integer(): + return str(int(value)) + return str(value).strip() or None + + +def load_measurements(path: Path, index: ConfigIndex) -> pd.DataFrame: + try: + dataframe = pd.read_csv(path) + except Exception as exc: + raise WheelComparisonError( + f"could not read benchmark CSV {path}: {exc}" + ) from exc + missing_columns = sorted(REQUIRED_RUN_COLUMNS - set(dataframe.columns)) + if missing_columns: + raise WheelComparisonError( + f"{path}: missing required columns {missing_columns}" + ) + + records, errors = [], [] + for position, (_, row) in enumerate(dataframe.iterrows(), start=2): + try: + config_key = str(row[CONFIG_KEY_COLUMN]).strip() + ref = index.require(config_key) + benchmark = str(row["Benchmark"]).strip() + if benchmark != ref.benchmark: + raise BaselineError( + f"benchmark {benchmark!r} does not match config operator {ref.benchmark!r}" + ) + if str(row["Language"]).strip() != "python": + raise BaselineError( + f"expected Language='python', got {row['Language']!r}" + ) + case_key = case_key_from_row(row, ref) + _, axes = parse_case_key(case_key) + gpu_us, noise_pct = float(row[GPU_TIME_COLUMN]), float( + row[GPU_NOISE_COLUMN] + ) + if not math.isfinite(gpu_us) or gpu_us <= 0: + raise BaselineError( + f"GPU time must be finite and positive, got {gpu_us}" + ) + if not math.isfinite(noise_pct) or noise_pct < 0: + raise BaselineError( + f"GPU noise must be finite and nonnegative, got {noise_pct}" + ) + status = _optional_text(row[STATUS_COLUMN]) + if not status: + raise BaselineError("Status must not be empty") + fingerprint = tuple( + _optional_text(row[column]) if column in dataframe.columns else None + for column in STABLE_FINGERPRINT_COLUMNS + ) + records.append( + { + "benchmark": benchmark, + "config_key": config_key, + "case_key": case_key, + "axes": format_axes(axes), + "gpu_us": gpu_us, + "noise_pct": noise_pct, + "status": status, + "device_name": fingerprint[0], + "power_cap": fingerprint[1], + "vbios": fingerprint[2], + "clock_mhz": _optional_text(row[LOCKED_CLOCK_COLUMN]), + } + ) + except Exception as exc: + errors.append(f"{path}:{position}: {exc}") + if errors: + raise WheelComparisonError("invalid benchmark rows:\n " + "\n ".join(errors)) + + normalized = pd.DataFrame.from_records(records, columns=NORMALIZED_COLUMNS) + duplicates = normalized[normalized["case_key"].duplicated()]["case_key"].tolist() + if duplicates: + raise WheelComparisonError(f"{path}: duplicate benchmark row(s): {duplicates}") + fingerprints = normalized[["device_name", "power_cap", "vbios"]].drop_duplicates() + if len(fingerprints) > 1: + raise WheelComparisonError( + f"{path}: benchmark rows contain inconsistent stable GPU fingerprints" + ) + return normalized + + +def selected_config_keys(index: ConfigIndex, args: argparse.Namespace) -> set[str]: + if args.config_key: + selected = set(parse_config_key_arg(args.config_key)) + unknown = selected - set(index.refs_by_key) + if unknown: + raise WheelComparisonError(f"unknown config key(s): {sorted(unknown)}") + return selected + + tiers = parse_tier_arg(args.tier) + operators = ( + set(parse_operator_arg(args.operator)) + if args.operator + else set(index.refs_by_operator) + ) + unknown = operators - set(index.refs_by_operator) + if unknown: + raise WheelComparisonError(f"unknown operator(s): {sorted(unknown)}") + selected = { + ref.key + for operator in operators + for ref in index.refs_by_operator[operator] + if ref.tier in tiers + } + if not selected: + raise WheelComparisonError( + f"no configs selected for operators {sorted(operators)} and tiers {sorted(tiers)}" + ) + return selected + + +def expected_case_keys(index: ConfigIndex, config_keys: set[str]) -> set[str]: + return set(expected_case_operators(index, config_keys)) + + +def expected_case_operators( + index: ConfigIndex, config_keys: set[str] +) -> dict[str, str]: + cases = {} + for config_key in config_keys: + ref = index.require(config_key) + for case_key in expected_case_keys_for_entry(config_key, ref.entry): + cases[case_key] = ref.benchmark + return cases + + +def _metadata( + frame: pd.DataFrame, +) -> tuple[Optional[tuple[Optional[str], ...]], tuple[str, ...]]: + if frame.empty: + return None, () + first = frame.iloc[0] + fingerprint = (first["device_name"], first["power_cap"], first["vbios"]) + clocks = tuple(sorted(value for value in frame["clock_mhz"].dropna().unique())) + return fingerprint, clocks + + +def compare_measurements( + baseline: pd.DataFrame, + candidate: pd.DataFrame, + *, + expected_case_operators: dict[str, str], + threshold: float, +) -> Comparison: + baseline_fingerprint, baseline_clocks = _metadata(baseline) + candidate_fingerprint, candidate_clocks = _metadata(candidate) + if ( + baseline_fingerprint is not None + and candidate_fingerprint is not None + and baseline_fingerprint != candidate_fingerprint + ): + raise WheelComparisonError( + "benchmark GPU fingerprints differ: " + f"baseline={baseline_fingerprint!r}, candidate={candidate_fingerprint!r}" + ) + + baseline_rows = {row.case_key: row for row in baseline.itertuples(index=False)} + candidate_rows = {row.case_key: row for row in candidate.itertuples(index=False)} + expected_cases = set(expected_case_operators) + records = [] + for case_key in sorted(set(baseline_rows) | set(candidate_rows) | expected_cases): + base, current = baseline_rows.get(case_key), candidate_rows.get(case_key) + source = base or current + if source is None: + config_key, axes = parse_case_key(case_key) + records.append( + { + "Benchmark": expected_case_operators[case_key], + "config_key": config_key, + "case_key": case_key, + "axes": format_axes(axes), + "comparison_status": "missing_from_both", + } + ) + continue + record = { + "Benchmark": source.benchmark, + "config_key": source.config_key, + "case_key": case_key, + "axes": source.axes, + "baseline_gpu_us": base.gpu_us if base else None, + "candidate_gpu_us": current.gpu_us if current else None, + "baseline_noise_pct": base.noise_pct if base else None, + "candidate_noise_pct": current.noise_pct if current else None, + "baseline_status": base.status if base else None, + "candidate_status": current.status if current else None, + } + if base is None: + record["comparison_status"] = "only_in_candidate" + elif current is None: + record["comparison_status"] = "missing_in_candidate" + else: + fraction = current.gpu_us / base.gpu_us - 1.0 + record["candidate_delta_pct"] = fraction * 100.0 + record["candidate_speedup"] = base.gpu_us / current.gpu_us + if base.status != "PASS" or current.status != "PASS": + record["comparison_status"] = "excluded" + elif exceeds_limit(fraction, threshold): + record["comparison_status"] = "regression" + elif exceeds_limit(-fraction, threshold): + record["comparison_status"] = "improvement" + else: + record["comparison_status"] = "neutral" + records.append(record) + + baseline_keys, candidate_keys = set(baseline_rows), set(candidate_rows) + table = pd.DataFrame.from_records(records, columns=COMPARISON_COLUMNS) + return Comparison( + table=table, + expected_case_operators=dict(expected_case_operators), + missing_baseline_cases=tuple(sorted(expected_cases - baseline_keys)), + missing_candidate_cases=tuple(sorted(expected_cases - candidate_keys)), + baseline_fingerprint=baseline_fingerprint, + candidate_fingerprint=candidate_fingerprint, + baseline_clocks=baseline_clocks, + candidate_clocks=candidate_clocks, + baseline_row_count=len(baseline), + candidate_row_count=len(candidate), + threshold=threshold, + ) + + +def _result_failed(comparison: Comparison, baseline_rc: int, candidate_rc: int) -> bool: + return bool(comparison.failed or baseline_rc or candidate_rc) + + +def _summary_items( + comparison: Comparison, baseline_rc: int, candidate_rc: int +) -> list[tuple[str, str]]: + count = lambda status: str(len(comparison.rows(status))) # noqa: E731 + speedup = comparison.mean_operator_speedup + operator_total = len(set(comparison.expected_case_operators.values())) + configuration_total = len(comparison.expected_case_operators) + run_status = lambda returncode: ( # noqa: E731 + f"**{'PASS' if returncode == 0 else 'FAIL'}** (exit code {returncode})" + ) + return [ + ( + "overall candidate speedup vs baseline", + ( + f"**{speedup:.4f}x** (mean of per-operator geomeans across " + "matched benchmarks)" + if speedup is not None + else "N/A" + ), + ), + ( + "rows", + f"baseline={comparison.baseline_row_count}, candidate={comparison.candidate_row_count}", + ), + ( + "valid compatible operators", + f"{len(comparison.operator_geomean_speedups)}/{operator_total}", + ), + ( + "valid compatible configurations", + f"{len(comparison.comparable)}/{configuration_total}", + ), + ("configuration regressions over threshold", count("regression")), + ("configuration improvements over threshold", count("improvement")), + ("reference `run_bench.py`", run_status(baseline_rc)), + ("candidate `run_bench.py`", run_status(candidate_rc)), + ] + + +def _compatibility_items(comparison: Comparison) -> list[tuple[str, str]]: + operator_compatibility = comparison.operator_compatibility + operator_total = len(operator_compatibility) + operators = { + status: tuple( + operator + for operator, actual_status in operator_compatibility.items() + if actual_status == status + ) + for status in ( + "incompatible_or_not_present", + "partially_compatible", + "fully_compatible", + ) + } + configuration_total = len(comparison.expected_case_operators) + shared_configurations = len(comparison.shared_expected_cases) + expected_rows = comparison.table[ + comparison.table["case_key"].isin(comparison.expected_case_operators) + ] + count = lambda status: int( # noqa: E731 + (expected_rows["comparison_status"] == status).sum() + ) + names = lambda status: ( # noqa: E731 + ", ".join(f"`{operator}`" for operator in operators[status]) or "_none_" + ) + operators_with_any_incompatibility = len( + operators["incompatible_or_not_present"] + ) + len(operators["partially_compatible"]) + return [ + ( + "compatibility denominator", + f"selected current-harness surface ({operator_total} operators, " + f"{configuration_total} expanded configurations)", + ), + ( + "operators present in both", + f"{operator_total - len(operators['incompatible_or_not_present'])}/{operator_total}", + ), + ( + "operators with any incompatible or missing configurations", + f"{operators_with_any_incompatibility}/{operator_total}", + ), + ( + "operators with no shared configurations", + f"{len(operators['incompatible_or_not_present'])}/{operator_total}", + ), + ( + "operators partially compatible", + f"{len(operators['partially_compatible'])}/{operator_total}", + ), + ( + "operators fully compatible", + f"{len(operators['fully_compatible'])}/{operator_total}", + ), + ( + "operator names with no shared configurations", + names("incompatible_or_not_present"), + ), + ("partially compatible operator names", names("partially_compatible")), + ( + "expanded benchmark configurations incompatible or not present", + f"{configuration_total - shared_configurations}/{configuration_total}", + ), + ( + "expanded benchmark configurations present in both", + f"{shared_configurations}/{configuration_total}", + ), + ("only in baseline", str(count("missing_in_candidate"))), + ("only in candidate", str(count("only_in_candidate"))), + ("missing from both", str(count("missing_from_both"))), + ("present in both but non-PASS", str(count("excluded"))), + ] + + +def _operator_result_lines(comparison: Comparison, *, improvements: bool) -> list[str]: + speedups = comparison.operator_geomean_speedups + speedups = speedups[speedups > 1.0] if improvements else speedups[speedups < 1.0] + speedups = speedups.sort_values(ascending=not improvements).head( + OPERATOR_RESULT_LIMIT + ) + if speedups.empty: + return ["_none_"] + + case_counts = comparison.comparable.groupby("Benchmark").size() + lines = [ + "| Operator | Speedup | Candidate time delta | Matched configurations |", + "|---|---:|---:|---:|", + ] + for operator, speedup in speedups.items(): + delta = (1.0 / speedup - 1.0) * 100.0 + lines.append( + f"| {operator} | {speedup:.4f}x | {delta:+.2f}% | " + f"{case_counts[operator]} |" + ) + return lines + + +def _comparison_status( + comparison: Comparison, baseline_rc: int, candidate_rc: int +) -> str: + if not _result_failed(comparison, baseline_rc, candidate_rc): + return "**PASS**" + + reasons = [] + if baseline_rc: + reasons.append(f"reference `run_bench.py` exited {baseline_rc}") + if candidate_rc: + reasons.append(f"candidate `run_bench.py` exited {candidate_rc}") + if not comparison.baseline_row_count: + reasons.append("reference produced no benchmark rows") + if not comparison.candidate_row_count: + reasons.append("candidate produced no benchmark rows") + + for status, description in ( + ("regression", "regression over threshold"), + ("missing_in_candidate", "only in reference"), + ("only_in_candidate", "only in candidate"), + ("missing_from_both", "missing from both"), + ("excluded", "non-PASS"), + ): + count = len(comparison.rows(status)) + if count: + noun = "configuration" if count == 1 else "configurations" + reasons.append(f"{count} {noun} {description}") + + if not reasons: + reasons.append("comparison completeness checks failed") + return "**FAIL** — " + "; ".join(reasons) + + +def format_report( + baseline_wheel: WheelMetadata, + candidate_wheel: WheelMetadata, + comparison: Comparison, + *, + baseline_rc: int, + candidate_rc: int, + output_dir: Path, +) -> str: + lines = [ + "# CV-CUDA Python wheel benchmark comparison", + "", + f"- baseline: `{baseline_wheel.label}`", + f"- candidate: `{candidate_wheel.label}`", + f"- GPU fingerprint: `{comparison.baseline_fingerprint or comparison.candidate_fingerprint}`", + f"- sampled SM clocks (MHz): baseline={list(comparison.baseline_clocks)}, " + f"candidate={list(comparison.candidate_clocks)}", + f"- regression threshold: `{comparison.threshold * 100:.2f}%`", + f"- comparison status: {_comparison_status(comparison, baseline_rc, candidate_rc)}", + f"- artifacts: `{output_dir}`", + " - reference benchmark CSV: `baseline/bench_output.csv`", + " - candidate benchmark CSV: `candidate/bench_output.csv`", + " - comparison CSV: `comparison.csv`", + "", + "## Summary", + "", + ] + lines.extend( + f"- {label}: {value}" + for label, value in _summary_items(comparison, baseline_rc, candidate_rc) + ) + lines.extend(["", "## Compatibility", ""]) + lines.extend( + f"- {label}: {value}" for label, value in _compatibility_items(comparison) + ) + lines.extend(["", "## Top 5 operator improvements", ""]) + lines.extend(_operator_result_lines(comparison, improvements=True)) + lines.extend(["", "## Top 5 operator regressions", ""]) + lines.extend(_operator_result_lines(comparison, improvements=False)) + return "\n".join(lines) + "\n" + + +def _nonnegative_float(value: str) -> float: + parsed = float(value) + if not math.isfinite(parsed) or parsed < 0: + raise argparse.ArgumentTypeError("must be finite and nonnegative") + return parsed + + +def _warmup_cap(value: str) -> int: + try: + return parse_warmup_cap(value) + except ValueError as exc: + raise argparse.ArgumentTypeError(str(exc)) from exc + + +def parse_args(argv: Optional[Sequence[str]] = None) -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Compare two CV-CUDA Python wheels with the current benchmark suite.", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog="""\ +Examples: + compare_wheels.py reference.whl candidate.whl + compare_wheels.py reference.whl candidate.whl \\ + --operator resize,gaussian --output-dir wheel-comparison + +Outputs under DIR: + baseline/bench_output.csv, candidate/bench_output.csv, comparison.csv, + summary.md, and per-run logs and wheel metadata. + +Exit status: + 0 Both runs and the comparison passed. + 1 A benchmark run or comparison check failed; the report was still written. + 2 Inputs, wheel setup, or artifacts are invalid. +""", + ) + parser.add_argument( + "baseline_wheel", + metavar="REFERENCE_WHEEL", + type=Path, + help="Reference CV-CUDA wheel (.whl).", + ) + parser.add_argument( + "candidate_wheel", + metavar="CANDIDATE_WHEEL", + type=Path, + help="Candidate CV-CUDA wheel (.whl) to compare with the reference.", + ) + selectors = parser.add_mutually_exclusive_group() + selectors.add_argument( + "--operator", help="Run exact operator name(s), comma-separated." + ) + selectors.add_argument( + "--config-key", + help="Run exact config key(s), comma-separated; bypasses --tier.", + ) + parser.add_argument( + "--tier", + default="basic", + help="Tiers: basic, advanced, or basic,advanced (default: basic).", + ) + parser.add_argument( + "--output-dir", + metavar="DIR", + type=Path, + help=( + "Write all artifacts to DIR. It must be new or empty " + "(default: wheel-benchmark-)." + ), + ) + parser.add_argument( + "--python", + metavar="PYTHON", + type=Path, + default=Path(sys.executable), + help=( + "Interpreter used for both wheel runs; it must provide the benchmark " + "dependencies (default: current interpreter)." + ), + ) + parser.add_argument( + "--regression-threshold-pct", + metavar="PCT", + type=_nonnegative_float, + default=DEFAULT_MAX_BASELINE_REGRESSION_PCT, + help=( + "Fail when any matched candidate configuration is slower by more " + "than PCT percent (default: %(default)s)." + ), + ) + parser.add_argument( + "--max-noise-pct", + metavar="PCT", + type=_nonnegative_float, + default=DEFAULT_BENCHMARK_QUALITY.max_noise_pct, + help="Maximum allowed measurement noise in percent (default: %(default)s).", + ) + parser.add_argument( + "--warmup-cap", + metavar="N", + type=_warmup_cap, + help="Cap configured warmup iterations; 0 disables warmup.", + ) + parser.add_argument( + "--bench-min-time", + metavar="SECONDS", + type=_nonnegative_float, + help="Override the minimum measurement time for each configuration.", + ) + parser.add_argument( + "--bench-max-noise", + metavar="PCT", + type=_nonnegative_float, + help="Override the benchmark stopping-noise target in percent.", + ) + return parser.parse_args(argv) + + +def _prepare_output_dir(requested: Optional[Path]) -> Path: + output_dir = requested or Path( + "wheel-benchmark-" + datetime.now().strftime("%Y%m%d-%H%M%S") + ) + output_dir = output_dir.expanduser().resolve() + if output_dir.exists(): + if not output_dir.is_dir(): + raise WheelComparisonError(f"output path is not a directory: {output_dir}") + if any(output_dir.iterdir()): + raise WheelComparisonError(f"output directory is not empty: {output_dir}") + output_dir.mkdir(parents=True, exist_ok=True) + return output_dir + + +def run(args: argparse.Namespace) -> int: + baseline_wheel = read_wheel_metadata(args.baseline_wheel) + candidate_wheel = read_wheel_metadata(args.candidate_wheel) + if baseline_wheel.canonical_name != candidate_wheel.canonical_name: + raise WheelComparisonError( + f"wheel distributions differ: {baseline_wheel.name!r} vs {candidate_wheel.name!r}" + ) + base_python = args.python.expanduser().resolve() + if not base_python.is_file(): + raise WheelComparisonError(f"Python executable does not exist: {base_python}") + + output_dir = _prepare_output_dir(args.output_dir) + artifact_dirs = {side: output_dir / side for side in ("baseline", "candidate")} + for artifact_dir in artifact_dirs.values(): + artifact_dir.mkdir() + workdir = Path(tempfile.mkdtemp(prefix="work-", dir=output_dir)) + try: + harness_dir = workdir / "harness-bin" + stage_benchmark_harness(BENCH_DIR, harness_dir) + index = load_config_index(harness_dir / "config" / "operators") + declared_cases = expected_case_operators( + index, selected_config_keys(index, args) + ) + + wheels = {"baseline": baseline_wheel, "candidate": candidate_wheel} + environments = {} + for side in ("baseline", "candidate"): + print(f"Preparing {side} environment for {wheels[side].label}") + environments[side] = create_wheel_environment( + base_python, + workdir / f"{side}-env", + wheels[side], + artifact_dirs[side] / "install.log", + ) + probe_wheel_environment( + environments[side], + harness_dir, + wheels[side], + artifact_dirs[side] / "wheel.json", + artifact_dirs[side] / "probe.log", + ) + + results = {} + for side in ("baseline", "candidate"): + print(f"Running {side} benchmarks...") + output_csv = artifact_dirs[side] / "bench_output.csv" + returncode = run_benchmark( + args, + environments[side], + harness_dir, + output_csv, + artifact_dirs[side] / "run.log", + ) + measurements = ( + load_measurements(output_csv, index) + if output_csv.is_file() + else pd.DataFrame(columns=NORMALIZED_COLUMNS) + ) + results[side] = (returncode, measurements) + + baseline_rc, baseline = results["baseline"] + candidate_rc, candidate = results["candidate"] + comparison = compare_measurements( + baseline, + candidate, + expected_case_operators=declared_cases, + threshold=args.regression_threshold_pct / 100.0, + ) + comparison.table.to_csv(output_dir / "comparison.csv", index=False) + report = format_report( + baseline_wheel, + candidate_wheel, + comparison, + baseline_rc=baseline_rc, + candidate_rc=candidate_rc, + output_dir=output_dir, + ) + (output_dir / "summary.md").write_text(report) + print("\n" + report) + if comparison.baseline_clocks != comparison.candidate_clocks: + print( + "WARNING: sampled SM clocks differ; clocks are diagnostic, " + "not part of the stable GPU fingerprint" + ) + return 1 if _result_failed(comparison, baseline_rc, candidate_rc) else 0 + finally: + shutil.rmtree(workdir, ignore_errors=True) + + +def main(argv: Optional[Sequence[str]] = None) -> int: + try: + return run(parse_args(argv)) + except ( + WheelComparisonError, + BaselineError, + ValueError, + json.JSONDecodeError, + OSError, + ) as exc: + print(f"ERROR: {exc}", file=sys.stderr) + return 2 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/bench/config/axis_order.py b/bench/config/axis_order.py new file mode 100755 index 000000000..f67351cee --- /dev/null +++ b/bench/config/axis_order.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Generic axis ordering shared by C++ and Python benchmarks.""" + +import argparse +import json +from pathlib import Path + + +AXIS_CATEGORIES = ("string_axes", "int64_axes", "float64_axes") + + +def order_axis_names(axis_mappings): + """Return unique axis names in case-sensitive UTF-8 byte order.""" + names = {name for mapping in axis_mappings for name in mapping} + return sorted(names, key=lambda name: name.encode("utf-8")) + + +def operator_axis_order(config_path, operator): + """Return the union axis order for one generated C++ benchmark.""" + document = json.loads(Path(config_path).read_text()) + if "configs" in document: + entries = ( + document["configs"].values() + if document.get("benchmark") == operator + else () + ) + else: + entries = ( + entry + for entry in document.values() + if isinstance(entry, dict) and entry.get("benchmark") == operator + ) + + mappings = [] + for entry in entries: + mappings.extend(entry.get(category, {}) for category in AXIS_CATEGORIES) + return order_axis_names(mappings) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--config", required=True) + parser.add_argument("--operator", required=True) + args = parser.parse_args() + print("\n".join(operator_axis_order(args.config, args.operator))) + + +if __name__ == "__main__": + main() diff --git a/bench/config/bench_params.json b/bench/config/bench_params.json new file mode 100644 index 000000000..351c20121 --- /dev/null +++ b/bench/config/bench_params.json @@ -0,0 +1,309 @@ +{ + "operators": { + "adaptivethreshold": { + "config": "operators/adaptivethreshold.json", + "cpp": "bench_adaptivethreshold", + "python": "bench_adaptivethreshold.py" + }, + "adjustcontrast": { + "config": "operators/adjustcontrast.json", + "cpp": "bench_adjustcontrast", + "python": "bench_adjustcontrast.py" + }, + "adjusthue": { + "config": "operators/adjusthue.json", + "cpp": "bench_adjusthue", + "python": "bench_adjusthue.py" + }, + "adjustsaturation": { + "config": "operators/adjustsaturation.json", + "cpp": "bench_adjustsaturation", + "python": "bench_adjustsaturation.py" + }, + "adjustsharpness": { + "config": "operators/adjustsharpness.json", + "cpp": "bench_adjustsharpness", + "python": "bench_adjustsharpness.py" + }, + "advcvtcolor": { + "config": "operators/advcvtcolor.json", + "cpp": "bench_advcvtcolor", + "python": "bench_advcvtcolor.py" + }, + "autocontrast": { + "config": "operators/autocontrast.json", + "cpp": "bench_autocontrast", + "python": "bench_autocontrast.py" + }, + "averageblur": { + "config": "operators/averageblur.json", + "cpp": "bench_averageblur", + "python": "bench_averageblur.py" + }, + "bilateralfilter": { + "config": "operators/bilateralfilter.json", + "cpp": "bench_bilateralfilter", + "python": "bench_bilateralfilter.py" + }, + "bndbox": { + "config": "operators/bndbox.json", + "cpp": "bench_bndbox", + "python": "bench_bndbox.py" + }, + "boxblur": { + "config": "operators/boxblur.json", + "cpp": "bench_boxblur", + "python": "bench_boxblur.py" + }, + "brightnesscontrast": { + "config": "operators/brightnesscontrast.json", + "cpp": "bench_brightnesscontrast", + "python": "bench_brightnesscontrast.py" + }, + "centercrop": { + "config": "operators/centercrop.json", + "cpp": "bench_centercrop", + "python": "bench_centercrop.py" + }, + "channelreorder": { + "config": "operators/channelreorder.json", + "cpp": "bench_channelreorder", + "python": "bench_channelreorder.py" + }, + "clahe": { + "config": "operators/clahe.json", + "cpp": "bench_clahe", + "python": "bench_clahe.py" + }, + "colortwist": { + "config": "operators/colortwist.json", + "cpp": "bench_colortwist", + "python": "bench_colortwist.py" + }, + "composite": { + "config": "operators/composite.json", + "cpp": "bench_composite", + "python": "bench_composite.py" + }, + "conv2d": { + "config": "operators/conv2d.json", + "cpp": "bench_conv2d", + "python": "bench_conv2d.py" + }, + "convertto": { + "config": "operators/convertto.json", + "cpp": "bench_convertto", + "python": "bench_convertto.py" + }, + "copymakeborder": { + "config": "operators/copymakeborder.json", + "cpp": "bench_copymakeborder", + "python": "bench_copymakeborder.py" + }, + "cropflipnormalizereformat": { + "config": "operators/cropflipnormalizereformat.json", + "cpp": "bench_cropflipnormalizereformat", + "python": "bench_cropflipnormalizereformat.py" + }, + "customcrop": { + "config": "operators/customcrop.json", + "cpp": "bench_customcrop", + "python": "bench_customcrop.py" + }, + "cvtcolor": { + "config": "operators/cvtcolor.json", + "cpp": "bench_cvtcolor", + "python": "bench_cvtcolor.py" + }, + "erase": { + "config": "operators/erase.json", + "cpp": "bench_erase", + "python": "bench_erase.py" + }, + "findhomography": { + "config": "operators/findhomography.json", + "cpp": "bench_findhomography", + "python": "bench_findhomography.py" + }, + "flip": { + "config": "operators/flip.json", + "cpp": "bench_flip", + "python": "bench_flip.py" + }, + "gammacontrast": { + "config": "operators/gammacontrast.json", + "cpp": "bench_gammacontrast", + "python": "bench_gammacontrast.py" + }, + "gaussian": { + "config": "operators/gaussian.json", + "cpp": "bench_gaussian", + "python": "bench_gaussian.py" + }, + "gaussiannoise": { + "config": "operators/gaussiannoise.json", + "cpp": "bench_gaussiannoise", + "python": "bench_gaussiannoise.py" + }, + "histogram": { + "config": "operators/histogram.json", + "cpp": "bench_histogram", + "python": "bench_histogram.py" + }, + "histogrameq": { + "config": "operators/histogrameq.json", + "cpp": "bench_histogrameq", + "python": "bench_histogrameq.py" + }, + "hqresize": { + "config": "operators/hqresize.json", + "cpp": "bench_hqresize", + "python": "bench_hqresize.py" + }, + "inpaint": { + "config": "operators/inpaint.json", + "cpp": "bench_inpaint", + "python": "bench_inpaint.py" + }, + "invert": { + "config": "operators/invert.json", + "cpp": "bench_invert", + "python": "bench_invert.py" + }, + "jointbilateralfilter": { + "config": "operators/jointbilateralfilter.json", + "cpp": "bench_jointbilateralfilter", + "python": "bench_jointbilateralfilter.py" + }, + "jpegcompressiondistortion": { + "config": "operators/jpegcompressiondistortion.json", + "cpp": "bench_jpegcompressiondistortion", + "python": "bench_jpegcompressiondistortion.py" + }, + "label": { + "config": "operators/label.json", + "cpp": "bench_label", + "python": "bench_label.py" + }, + "laplacian": { + "config": "operators/laplacian.json", + "cpp": "bench_laplacian", + "python": "bench_laplacian.py" + }, + "medianblur": { + "config": "operators/medianblur.json", + "cpp": "bench_medianblur", + "python": "bench_medianblur.py" + }, + "minarearect": { + "config": "operators/minarearect.json", + "cpp": "bench_minarearect", + "python": "bench_minarearect.py" + }, + "minmaxloc": { + "config": "operators/minmaxloc.json", + "cpp": "bench_minmaxloc", + "python": "bench_minmaxloc.py" + }, + "morphology": { + "config": "operators/morphology.json", + "cpp": "bench_morphology", + "python": "bench_morphology.py" + }, + "nonmaximumsuppression": { + "config": "operators/nonmaximumsuppression.json", + "cpp": "bench_nonmaximumsuppression", + "python": "bench_nonmaximumsuppression.py" + }, + "normalize": { + "config": "operators/normalize.json", + "cpp": "bench_normalize", + "python": "bench_normalize.py" + }, + "osd": { + "config": "operators/osd.json", + "cpp": "bench_osd", + "python": "bench_osd.py" + }, + "padandstack": { + "config": "operators/padandstack.json", + "cpp": "bench_padandstack", + "python": "bench_padandstack.py" + }, + "pairwisematcher": { + "config": "operators/pairwisematcher.json", + "cpp": "bench_pairwisematcher", + "python": "bench_pairwisematcher.py" + }, + "pillowresize": { + "config": "operators/pillowresize.json", + "cpp": "bench_pillowresize", + "python": "bench_pillowresize.py" + }, + "posterize": { + "config": "operators/posterize.json", + "cpp": "bench_posterize", + "python": "bench_posterize.py" + }, + "randomresizedcrop": { + "config": "operators/randomresizedcrop.json", + "cpp": "bench_randomresizedcrop", + "python": "bench_randomresizedcrop.py" + }, + "reformat": { + "config": "operators/reformat.json", + "cpp": "bench_reformat", + "python": "bench_reformat.py" + }, + "remap": { + "config": "operators/remap.json", + "cpp": "bench_remap", + "python": "bench_remap.py" + }, + "resize": { + "config": "operators/resize.json", + "cpp": "bench_resize", + "python": "bench_resize.py" + }, + "resizecropconvertreformat": { + "config": "operators/resizecropconvertreformat.json", + "cpp": "bench_resizecropconvertreformat", + "python": "bench_resizecropconvertreformat.py" + }, + "rotate": { + "config": "operators/rotate.json", + "cpp": "bench_rotate", + "python": "bench_rotate.py" + }, + "sift": { + "config": "operators/sift.json", + "cpp": "bench_sift", + "python": "bench_sift.py" + }, + "solarize": { + "config": "operators/solarize.json", + "cpp": "bench_solarize", + "python": "bench_solarize.py" + }, + "stack": { + "config": "operators/stack.json", + "cpp": "bench_stack", + "python": "bench_stack.py" + }, + "threshold": { + "config": "operators/threshold.json", + "cpp": "bench_threshold", + "python": "bench_threshold.py" + }, + "warpaffine": { + "config": "operators/warpaffine.json", + "cpp": "bench_warpaffine", + "python": "bench_warpaffine.py" + }, + "warpperspective": { + "config": "operators/warpperspective.json", + "cpp": "bench_warpperspective", + "python": "bench_warpperspective.py" + } + } +} diff --git a/bench/config/load_config.py b/bench/config/load_config.py new file mode 100755 index 000000000..5b464235c --- /dev/null +++ b/bench/config/load_config.py @@ -0,0 +1,752 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +""" +Unified benchmark configuration loader for CV-CUDA benchmarks. + +Provides configuration loading for both: +- Python benchmarks (structured config objects) +- C++ benchmark runners (CLI argument generation) +""" + +import json +import warnings +from pathlib import Path +from typing import Dict, List, Any, Optional, Set + +try: + from .axis_order import order_axis_names +except ImportError: # Standalone scripts add bench/config directly to sys.path. + from axis_order import order_axis_names + + +# Allow-list for the per-entry `tier` field. The default `basic` suite is the +# broad CI-gated set. The `advanced` tier holds deeper operator profiles and is +# only run when explicitly requested. +VALID_TIERS = frozenset({"basic", "advanced"}) + + +def _default_config_path() -> Path: + script_dir = Path(__file__).parent + config_path = script_dir / "bench_params.json" + if config_path.exists(): + return config_path + + alt_path = script_dir.parent / "bench_params.json" + if alt_path.exists(): + return alt_path + + raise FileNotFoundError( + f"Cannot find bench_params.json. Searched:\n" + f" 1. {config_path}\n" + f" 2. {alt_path}\n" + f" load_config.py location: {Path(__file__).resolve()}" + ) + + +def _merge_config_entries( + merged: Dict[str, Any], incoming: Dict[str, Any], source_path: Path +) -> None: + duplicates = [key for key in incoming if key in merged] + if duplicates: + raise ValueError(f"Duplicate config key(s) in {source_path}: {duplicates}") + merged.update(incoming) + + +def _expand_include_patterns(manifest_path: Path, patterns: List[str]) -> List[Path]: + included_paths = [] + for pattern in patterns: + matches = sorted(manifest_path.parent.glob(pattern)) + if not matches: + raise FileNotFoundError( + f"Config include {pattern!r} from {manifest_path} matched no files" + ) + included_paths.extend(path for path in matches if path.is_file()) + return included_paths + + +def _default_operator_spec(operator_name: str) -> Dict[str, str]: + return { + "config": f"operators/{operator_name}.json", + "cpp": f"bench_{operator_name}", + "python": f"bench_{operator_name}.py", + } + + +def _normalize_operator_manifest( + manifest_path: Path, operators: Dict[str, Any] +) -> Dict[str, Dict[str, str]]: + if not isinstance(operators, dict): + raise ValueError(f"{manifest_path}: 'operators' must be a dictionary") + + normalized: Dict[str, Dict[str, str]] = {} + for operator_name, spec in operators.items(): + if not isinstance(operator_name, str) or not operator_name: + raise ValueError( + f"{manifest_path}: operator names must be non-empty strings" + ) + if not isinstance(spec, dict): + raise ValueError( + f"{manifest_path}: operator {operator_name!r} must map to a dictionary" + ) + + defaults = _default_operator_spec(operator_name) + normalized_spec: Dict[str, str] = {} + for field in ("config", "cpp", "python"): + value = spec.get(field, defaults[field] if field != "config" else None) + if not isinstance(value, str) or not value: + raise ValueError( + f"{manifest_path}: operator {operator_name!r} missing " + f"non-empty string field {field!r}" + ) + normalized_spec[field] = value + + normalized[operator_name] = normalized_spec + + return normalized + + +def _config_path_from_manifest_spec(manifest_path: Path, spec: Dict[str, str]) -> Path: + config_path = Path(spec["config"]) + if not config_path.is_absolute(): + config_path = manifest_path.parent / config_path + return config_path + + +def _infer_manifest_from_config( + config: Dict[str, Any], config_path: Optional[Path] = None +) -> Dict[str, Dict[str, str]]: + if _is_operator_config_document(config): + benchmark = config["benchmark"] + spec = _default_operator_spec(benchmark) + if config_path is not None: + spec["config"] = str(config_path) + return {benchmark: spec} + + operators: Dict[str, Dict[str, str]] = {} + for entry in config.values(): + if not isinstance(entry, dict): + continue + benchmark = entry.get("benchmark") + if not isinstance(benchmark, str) or not benchmark: + continue + if benchmark in operators: + continue + spec = _default_operator_spec(benchmark) + if config_path is not None: + spec["config"] = str(config_path) + operators[benchmark] = spec + return operators + + +def _is_operator_config_document(config: Dict[str, Any]) -> bool: + return "benchmark" in config and "configs" in config + + +def _strip_machine_owned_fields(entry: Dict[str, Any]) -> Dict[str, Any]: + return {key: value for key, value in entry.items() if key != "baselines"} + + +def _flatten_operator_config_document( + config: Dict[str, Any], config_path: Path +) -> Dict[str, Any]: + benchmark = config.get("benchmark") + configs = config.get("configs") + if not isinstance(benchmark, str) or not benchmark: + raise ValueError(f"{config_path}: top-level 'benchmark' must be a string") + if not isinstance(configs, dict): + raise ValueError(f"{config_path}: top-level 'configs' must be a dictionary") + + flattened: Dict[str, Any] = {} + for config_key, entry in configs.items(): + if not isinstance(config_key, str) or not config_key: + raise ValueError(f"{config_path}: config keys must be non-empty strings") + if not isinstance(entry, dict): + raise ValueError( + f"{config_path}: config entry {config_key!r} must be a dictionary" + ) + cleaned = {"benchmark": benchmark} + cleaned.update(_strip_machine_owned_fields(entry)) + flattened[config_key] = cleaned + return flattened + + +def _flatten_legacy_config_document( + config: Dict[str, Any], config_path: Path +) -> Dict[str, Any]: + flattened: Dict[str, Any] = {} + for config_key, entry in config.items(): + if not isinstance(entry, dict): + flattened[config_key] = entry + continue + flattened[config_key] = _strip_machine_owned_fields(entry) + return flattened + + +def _load_manifest_path(config_path: Path) -> Dict[str, Dict[str, str]]: + config_path = Path(config_path) + + if config_path.is_dir(): + manifest = config_path / "bench_params.json" + if manifest.exists(): + return _load_manifest_path(manifest) + + inferred: Dict[str, Dict[str, str]] = {} + for path in sorted(config_path.glob("*.json")): + inferred.update(_load_manifest_path(path)) + return inferred + + with open(config_path, "r") as f: + cfg = json.load(f) + + if not isinstance(cfg, dict): + raise ValueError(f"{config_path}: benchmark config must be a dictionary") + + has_operators = "operators" in cfg + has_include = "include" in cfg + if has_operators and has_include: + raise ValueError( + f"{config_path}: use either 'operators' or 'include', not both" + ) + + if has_operators: + return _normalize_operator_manifest(config_path, cfg["operators"]) + + if has_include: + include_patterns = cfg["include"] + if not isinstance(include_patterns, list) or not all( + isinstance(pattern, str) for pattern in include_patterns + ): + raise ValueError(f"{config_path}: 'include' must be a list of strings") + + inferred: Dict[str, Dict[str, str]] = {} + for path in _expand_include_patterns(config_path, include_patterns): + inferred.update(_load_manifest_path(path)) + return inferred + + return _infer_manifest_from_config(cfg, config_path) + + +def _load_config_path(config_path: Path) -> Dict[str, Any]: + config_path = Path(config_path) + + if config_path.is_dir(): + manifest = config_path / "bench_params.json" + if manifest.exists(): + return _load_config_path(manifest) + + merged: Dict[str, Any] = {} + for path in sorted(config_path.glob("*.json")): + _merge_config_entries(merged, _load_config_path(path), path) + return merged + + with open(config_path, "r") as f: + cfg = json.load(f) + + if not isinstance(cfg, dict): + raise ValueError(f"{config_path}: benchmark config must be a dictionary") + + has_operators = "operators" in cfg + has_include = "include" in cfg + if has_operators and has_include: + raise ValueError( + f"{config_path}: use either 'operators' or 'include', not both" + ) + + if has_operators: + merged: Dict[str, Any] = {} + for spec in _normalize_operator_manifest( + config_path, cfg["operators"] + ).values(): + path = _config_path_from_manifest_spec(config_path, spec) + _merge_config_entries(merged, _load_config_path(path), path) + return merged + + include_patterns = cfg.get("include") + if include_patterns is None: + if _is_operator_config_document(cfg): + return _flatten_operator_config_document(cfg, config_path) + return _flatten_legacy_config_document(cfg, config_path) + if not isinstance(include_patterns, list) or not all( + isinstance(pattern, str) for pattern in include_patterns + ): + raise ValueError(f"{config_path}: 'include' must be a list of strings") + + merged: Dict[str, Any] = {} + for path in _expand_include_patterns(config_path, include_patterns): + _merge_config_entries(merged, _load_config_path(path), path) + return merged + + +def _validate_tiers(config: Dict[str, Any]) -> None: + """Hard-fail if any entry is missing `tier` or has an unknown value. + + Run at config-load time so a typo (or a forgotten `tier` on a freshly + added entry) surfaces before any benchmark runs, with the offending key + in the error. + """ + bad = [] + for key, entry in config.items(): + if not isinstance(entry, dict): + continue + tier = entry.get("tier") + if tier is None: + bad.append(f"{key!r}: missing required 'tier' field") + elif tier not in VALID_TIERS: + bad.append(f"{key!r}: tier={tier!r} not in {sorted(VALID_TIERS)}") + if bad: + raise ValueError("Invalid bench config values:\n " + "\n ".join(bad)) + + +def parse_tier_arg(value: str) -> Set[str]: + """Convert a --tier CLI value into a set of tier names. + + Accepts: + "basic" → {"basic"} + "advanced" → {"advanced"} + "basic,advanced" → {"basic", "advanced"} + + Raises ValueError on unknown tier names. + """ + parts = [p.strip() for p in value.split(",") if p.strip()] + if not parts: + raise ValueError(f"--tier: empty value {value!r}") + unknown = [p for p in parts if p not in VALID_TIERS] + if unknown: + raise ValueError( + f"--tier: unknown tier(s) {unknown}; " + f"valid options: {sorted(VALID_TIERS)}" + ) + return set(parts) + + +def parse_config_key_arg(value: str) -> List[str]: + """Convert a --config-key CLI value into an ordered list of config keys. + + Accepts: + "resize_advanced" -> ["resize_advanced"] + "resize_advanced,gaussian_advanced" -> ["resize_advanced", "gaussian_advanced"] + + Raises ValueError on empty values or duplicate keys. + """ + parts = [p.strip() for p in value.split(",") if p.strip()] + if not parts: + raise ValueError(f"--config-key: empty value {value!r}") + + seen = set() + duplicates = [] + for key in parts: + if key in seen and key not in duplicates: + duplicates.append(key) + seen.add(key) + if duplicates: + raise ValueError(f"--config-key: duplicate key(s) {duplicates}") + + return parts + + +def parse_operator_arg(value: str) -> List[str]: + """Convert a --operator CLI value into an ordered list of operator names. + + Accepts comma-separated and/or space-separated values. Matching is exact + against the operator names advertised by bench_params.json. + """ + parts = [p.strip() for p in value.replace(",", " ").split() if p.strip()] + if not parts: + raise ValueError(f"--operator: empty value {value!r}") + + seen = set() + duplicates = [] + for operator_name in parts: + if operator_name in seen and operator_name not in duplicates: + duplicates.append(operator_name) + seen.add(operator_name) + if duplicates: + raise ValueError(f"--operator: duplicate operator(s) {duplicates}") + + return parts + + +class BenchmarkConfig: + """Generic benchmark configuration container.""" + + def __init__(self, config_dict: Dict[str, Any]): + self.dtypes = config_dict.get("dtypes", []) + self.string_axes = config_dict.get("string_axes", {}) + self.int64_axes = config_dict.get("int64_axes", {}) + self.float64_axes = config_dict.get("float64_axes", {}) + self.metadata = config_dict.get("metadata", {}) + self.warmup_iterations = config_dict.get("warmup_iterations", 100) + + def get_string_axis(self, name: str) -> List[str]: + """Get values for a string axis.""" + return self.string_axes.get(name, []) + + def get_int64_axis(self, name: str) -> List[int]: + """Get values for an int64 axis.""" + return self.int64_axes.get(name, []) + + def get_float64_axis(self, name: str) -> List[float]: + """Get values for a float64 axis.""" + return self.float64_axes.get(name, []) + + def has_axis(self, name: str) -> bool: + """Check if an axis exists in any category.""" + return ( + name in self.string_axes + or name in self.int64_axes + or name in self.float64_axes + ) + + def get_ordered_axes(self): + """ + Get all axes in standardized order matching C++ benchmarks. + + Returns a list of tuples: (axis_name, axis_type, values) + where axis_type is 'string', 'int64', or 'float64'. + """ + ordered = [] + + for axis_name in order_axis_names( + (self.string_axes, self.int64_axes, self.float64_axes) + ): + if axis_name in self.string_axes: + ordered.append((axis_name, "string", self.string_axes[axis_name])) + elif axis_name in self.int64_axes: + ordered.append((axis_name, "int64", self.int64_axes[axis_name])) + else: + ordered.append((axis_name, "float64", self.float64_axes[axis_name])) + + return ordered + + +class ConfigLoader: + """Loads benchmark configurations from JSON file or manifest.""" + + def __init__(self, config_path: Optional[str] = None): + import os + + if config_path is None: + config_path = _default_config_path() + + # Debug output for path issues + if os.environ.get("BENCH_DEBUG"): + print(f"[DEBUG] ConfigLoader: loading from {config_path}") + + self.config = _load_config_path(Path(config_path)) + + _validate_tiers(self.config) + + if os.environ.get("BENCH_DEBUG"): + print(f"[DEBUG] ConfigLoader: loaded {len(self.config)} config entries") + + def get_operator_config(self, operator_name: str) -> BenchmarkConfig: + """Get configuration for a specific operator.""" + # Direct hit (a fully-qualified key like "resize_expand_basic"). + if operator_name in self.config: + return BenchmarkConfig(self.config[operator_name]) + + # Standalone-run convenience: a developer running + # `python3 bench_resize.py` with no --config-key gets + # operator_name="resize" by default. Since every entry now carries + # a tier suffix, fall back to "_basic" so the basic suite + # stays directly invokable; if that's still not unique enough + # (operator has no entry that's exactly "_basic"), point + # them at --config-key. + fallback = f"{operator_name}_basic" + if fallback in self.config: + return BenchmarkConfig(self.config[fallback]) + + raise KeyError( + f"Operator not found in config: {operator_name!r}. " + f"Tried {operator_name!r} and {fallback!r}. " + f"Pass --config-key to select a specific entry." + ) + + +def load_bench_config(config_file: str = None) -> Dict[str, Any]: + """ + Load benchmark configuration JSON. + + Args: + config_file: Path to a flat config file, split-config manifest, or + directory. If None, looks for bench_params.json next to this script. + + Returns: + Dictionary of operator configurations + """ + if config_file is None: + config_file = _default_config_path() + + cfg = _load_config_path(Path(config_file)) + _validate_tiers(cfg) + return cfg + + +def load_bench_manifest(config_file: Optional[str] = None) -> Dict[str, Dict[str, str]]: + """ + Load the benchmark operator manifest. + + Args: + config_file: Path to bench_params.json, a flat config file, split-config + manifest, or directory. If None, uses the default bench_params.json. + + Returns: + Dictionary keyed by exact operator name. Each entry provides the + operator config file and the C++/Python benchmark file names. + """ + if config_file is None: + config_file = _default_config_path() + + return _load_manifest_path(Path(config_file)) + + +def load_operator_config(operator_name: str, args: list = None) -> tuple: + """ + Load configuration for a specific operator with --config-key aliasing support. + + This is the single entry point for Python benchmarks to load their config. + It handles: + 1. Parsing --config-key from command line args (uses sys.argv[1:] by default) + 2. Loading the config by key (falling back to operator_name if no --config-key) + 3. Returning cleaned args for nvbench + + Args: + operator_name: Default operator name (e.g., "resize") + args: Command line args (default: sys.argv[1:], excluding script path) + + Returns: + Tuple of (BenchmarkConfig, remaining_args) where remaining_args + has --config-key removed and can be passed to nvbench. + + Example usage in benchmark: + ```python + from load_config import load_operator_config, register_axes_from_config + + config, bench_args = load_operator_config("resize") + + b = bench.register(my_benchmark) + b.add_string_axis("InOutDataType", config.dtypes) + register_axes_from_config(b, config) + bench.run_all_benchmarks(bench_args) + ``` + """ + import sys + + if args is None: + # Skip argv[0] (script path) - nvbench expects arguments only + args = sys.argv[1:] + + # Parse --config-key and --config-file from args + remaining_args = [] + config_key = operator_name + config_file = None + i = 0 + while i < len(args): + if args[i] == "--config-key" and i + 1 < len(args): + config_key = args[i + 1] + i += 2 # Skip both --config-key and its value + elif args[i] == "--config-file" and i + 1 < len(args): + config_file = args[i + 1] + i += 2 # Skip both --config-file and its value + else: + remaining_args.append(args[i]) + i += 1 + + # Load config by key (from --config-file if provided, else default) + loader = ConfigLoader(config_file) if config_file else ConfigLoader() + config = loader.get_operator_config(config_key) + + # Debug output (only if BENCH_DEBUG env var is set) + import os + + if os.environ.get("BENCH_DEBUG"): + print(f"[DEBUG] load_operator_config: key={config_key}") + print(f"[DEBUG] dtypes={config.dtypes}") + print(f"[DEBUG] string_axes={config.string_axes}") + print(f"[DEBUG] int64_axes={config.int64_axes}") + + return config, remaining_args + + +def register_axes_from_config(benchmark, config: BenchmarkConfig): + """ + Register benchmark axes from config in standardized order matching C++. + + Args: + benchmark: nvbench benchmark object + config: BenchmarkConfig object + + Usage: + config = load_operator_config("resize") + b = bench.register(resize_benchmark) + b.add_string_axis("dtype", config.dtypes) # Type axis first + register_axes_from_config(b, config) # Then ordered axes + """ + ordered = config.get_ordered_axes() + + # Register axes in standardized order (interleaving types as needed) + for axis_name, axis_type, values in ordered: + if axis_type == "string": + benchmark.add_string_axis(axis_name, values) + elif axis_type == "int64": + benchmark.add_int64_axis(axis_name, values) + elif axis_type == "float64": + benchmark.add_float64_axis(axis_name, values) + + +# Map Python dtype names to nvbench type axis names +# Used by generate_axis_args() to filter C++ benchmarks by dtype at runtime +DTYPE_TO_NVBENCH = { + "uint8": "U8", + "uint16": "U16", + "uint32": "U32", + "uint64": "U64", + "int8": "I8", + "int16": "I16", + "int32": "I32", + "int64": "I64", + "float32": "F32", + "float64": "F64", +} + + +def generate_axis_args(operator_name: str, config: Dict[str, Any]) -> List[str]: + """ + Generate --axis CLI arguments from operator config. + + Used by C++ benchmark runner to pass config values as command-line arguments. + + Args: + operator_name: Name of operator (e.g., "resize") + config: Full config dictionary + + Returns: + List of CLI arguments like ["--axis", "shape=[1x1080x1920]", ...] + """ + if operator_name not in config: + return [] + + op_config = config[operator_name] + args = [] + + # Add dtype filter if specified in config + # This ensures C++ runs only the dtypes specified in this config entry + if "dtypes" in op_config and op_config["dtypes"]: + dtypes = op_config["dtypes"] + # Map Python dtype names to nvbench type names + nvbench_types = [DTYPE_TO_NVBENCH.get(dt, dt) for dt in dtypes] + if len(nvbench_types) == 1: + args.extend(["--axis", f"InOutDataType={nvbench_types[0]}"]) + else: + types_str = ",".join(nvbench_types) + args.extend(["--axis", f"InOutDataType=[{types_str}]"]) + + # Add string axes + if "string_axes" in op_config: + for axis_name, values in op_config["string_axes"].items(): + if len(values) == 1: + args.extend(["--axis", f"{axis_name}={values[0]}"]) + else: + values_str = ",".join(values) + args.extend(["--axis", f"{axis_name}=[{values_str}]"]) + + # Add int64 axes + if "int64_axes" in op_config: + for axis_name, values in op_config["int64_axes"].items(): + if len(values) == 1: + args.extend(["--axis", f"{axis_name}={values[0]}"]) + else: + values_str = ",".join(str(v) for v in values) + args.extend(["--axis", f"{axis_name}=[{values_str}]"]) + + # Add float64 axes + if "float64_axes" in op_config: + for axis_name, values in op_config["float64_axes"].items(): + if len(values) == 1: + args.extend(["--axis", f"{axis_name}={values[0]}"]) + else: + values_str = ",".join(str(v) for v in values) + args.extend(["--axis", f"{axis_name}=[{values_str}]"]) + + return args + + +def get_operator_from_benchmark_name(bench_name: str) -> str: + """ + Extract operator name from benchmark executable name. + + Examples: + bench_resize -> resize + bench_resize.py -> resize + bench_gaussian -> gaussian + """ + name = bench_name + # Remove .py extension if present + if name.endswith(".py"): + name = name[:-3] + # Remove bench_ prefix + if name.startswith("bench_"): + name = name[len("bench_") :] # noqa: E203 + return name + + +def get_configs_for_benchmark( + benchmark_name: str, + config: Dict[str, Any], + tiers: Optional[Set[str]] = None, +) -> List[str]: + """ + Get all config keys that target a specific benchmark. + + Returns a list of config keys where the "benchmark" field matches + the given benchmark_name. Optionally filtered to a subset of tiers. + + Args: + benchmark_name: The benchmark name (e.g., "resize") + config: Full config dictionary + tiers: Optional set of tier names (e.g. {"basic"}). None → no tier + filter. + + Returns: + List of config keys that target this benchmark + """ + matching_configs = [] + + for config_key, op_config in config.items(): + # Skip non-dict entries (shouldn't happen but be safe) + if not isinstance(op_config, dict): + continue + + # Skip entries without benchmark field (invalid config) + if "benchmark" not in op_config: + warnings.warn( + f"Config entry '{config_key}' missing required 'benchmark' field, skipping" + ) + continue + + # Match on the benchmark field + if op_config["benchmark"] != benchmark_name: + continue + + # Tier filter: tier was already validated at load time, so we can + # trust it's present and one of VALID_TIERS. + if tiers is not None and op_config.get("tier") not in tiers: + continue + + matching_configs.append(config_key) + + return matching_configs diff --git a/bench/config/operator_categories.json b/bench/config/operator_categories.json new file mode 100644 index 000000000..b071b45e2 --- /dev/null +++ b/bench/config/operator_categories.json @@ -0,0 +1,203 @@ +{ + "_comment": "Source-of-truth operator categorization for the RGB benchmark guidelines. A = general image op (basic tier = RGB uchar3/float3; 1-ch moved to advanced; RGBA mirror in advanced). B = inherently single-channel (left 1-channel). C = intrinsic channel semantics (unchanged). See bench/tests/test_bench_rgb_guidelines.py.", + "category_reasons": { + "A": "General image op: per-pixel/per-channel on color images; RGB is the realistic input.", + "B": "Inherently single-channel/luma/binary; RGB would mislead or be rejected by the op.", + "C": "Intrinsic channel semantics (color conversion, channel ops, drawing, non-image data); leave as-is." + }, + "operators": { + "adaptivethreshold": { + "category": "B" + }, + "adjustcontrast": { + "category": "A", + "rgba_unsupported": true + }, + "adjusthue": { + "category": "A", + "rgba_unsupported": true + }, + "adjustsaturation": { + "category": "A", + "rgba_unsupported": true + }, + "adjustsharpness": { + "category": "A" + }, + "advcvtcolor": { + "category": "C" + }, + "autocontrast": { + "category": "A" + }, + "averageblur": { + "category": "A" + }, + "bilateralfilter": { + "category": "A" + }, + "bndbox": { + "category": "C" + }, + "boxblur": { + "category": "C" + }, + "brightnesscontrast": { + "category": "A" + }, + "centercrop": { + "category": "A" + }, + "channelreorder": { + "category": "C" + }, + "clahe": { + "category": "B" + }, + "colortwist": { + "category": "C" + }, + "composite": { + "category": "C" + }, + "conv2d": { + "category": "A" + }, + "convertto": { + "category": "A" + }, + "copymakeborder": { + "category": "A" + }, + "cropflipnormalizereformat": { + "category": "A" + }, + "customcrop": { + "category": "A" + }, + "cvtcolor": { + "category": "C" + }, + "erase": { + "category": "A" + }, + "findhomography": { + "category": "C" + }, + "flip": { + "category": "A" + }, + "gammacontrast": { + "category": "A" + }, + "gaussian": { + "category": "A" + }, + "gaussiannoise": { + "category": "A" + }, + "histogram": { + "category": "B" + }, + "histogrameq": { + "category": "A" + }, + "hqresize": { + "category": "A", + "rgb_unsupported": true + }, + "inpaint": { + "category": "A" + }, + "invert": { + "category": "A" + }, + "jointbilateralfilter": { + "category": "A" + }, + "jpegcompressiondistortion": { + "category": "A", + "rgba_unsupported": true + }, + "label": { + "category": "B" + }, + "laplacian": { + "category": "A" + }, + "medianblur": { + "category": "A" + }, + "minarearect": { + "category": "C" + }, + "minmaxloc": { + "category": "B" + }, + "morphology": { + "category": "A" + }, + "nonmaximumsuppression": { + "category": "C" + }, + "normalize": { + "category": "A" + }, + "osd": { + "category": "C" + }, + "padandstack": { + "category": "A" + }, + "pairwisematcher": { + "category": "C" + }, + "pillowresize": { + "category": "A" + }, + "posterize": { + "category": "A" + }, + "randomresizedcrop": { + "category": "A" + }, + "reformat": { + "category": "A" + }, + "remap": { + "category": "A" + }, + "resize": { + "category": "A" + }, + "resizecropconvertreformat": { + "category": "A", + "rgba_unsupported": true + }, + "rotate": { + "category": "A" + }, + "sift": { + "category": "B" + }, + "solarize": { + "category": "A" + }, + "stack": { + "category": "A" + }, + "threshold": { + "category": "B" + }, + "warpaffine": { + "category": "A" + }, + "warpperspective": { + "category": "A" + } + }, + "_notes": { + "rgba_unsupported": "Cat-A op whose kernel has no 4-channel instantiation; exempt from the RGBA-mirror rule (R4).", + "rgb_unsupported": "Cat-A op whose bench has no multi-channel instantiation (U8/F32 only); left single-channel (would need a bench code change to RGB-convert)." + } +} diff --git a/bench/config/operators/adaptivethreshold.json b/bench/config/operators/adaptivethreshold.json new file mode 100644 index 000000000..15756ff2c --- /dev/null +++ b/bench/config/operators/adaptivethreshold.json @@ -0,0 +1,938 @@ +{ + "benchmark": "adaptivethreshold", + "configs": { + "adaptivethreshold_basic": { + "tier": "basic", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "int64_axes": { + "blockSize": [ + 7 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AdaptiveThreshold single-channel (basic)." + }, + "baselines": { + "adaptivethreshold_basic[InOutDataType=uint8][shape=64x1080x1920][layout=NHWC][inputKind=Tensor][blockSize=7]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3956.4685208693, + "gpu_time_us_python": 3967.4611791139796, + "gpu_noise_us_cpp": 3.2619999999999996, + "gpu_noise_us_python": 5.556, + "gpu_bwutil_cpp": 0.0431365250794684, + "gpu_bwutil_python": 0.043016913474434745, + "gpu_gap_stddev_us": 2.5683690790530465 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 3710.0676316191502, + "gpu_time_us_python": 3723.36156094145, + "gpu_noise_us_cpp": 1.115, + "gpu_noise_us_python": 4.5649999999999995, + "gpu_bwutil_cpp": 0.03509018638752845, + "gpu_bwutil_python": 0.03496513549295657, + "gpu_gap_stddev_us": 2.4087260871319045 + } + }, + "adaptivethreshold_basic[InOutDataType=uint8][shape=64x1080x1920][layout=NHWC][inputKind=VarShape][blockSize=7]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4735.64451145672, + "gpu_time_us_python": 4752.34920753618, + "gpu_noise_us_cpp": 7.0920000000000005, + "gpu_noise_us_python": 7.861999999999999, + "gpu_bwutil_cpp": 0.03603897246540016, + "gpu_bwutil_python": 0.03591235771214676, + "gpu_gap_stddev_us": 4.252374827623534 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 4452.9313541250995, + "gpu_time_us_python": 4470.9726652781255, + "gpu_noise_us_cpp": 3.4250000000000003, + "gpu_noise_us_python": 5.0649999999999995, + "gpu_bwutil_cpp": 0.029236194915296474, + "gpu_bwutil_python": 0.029118386062366174, + "gpu_gap_stddev_us": 1.7395417070022228 + } + } + } + }, + "adaptivethreshold_planar_nchw_basic": { + "tier": "basic", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "blockSize": [ + 7 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AdaptiveThreshold single-channel (basic) (native planar NCHW tensor)." + }, + "baselines": { + "adaptivethreshold_planar_nchw_basic[InOutDataType=uint8][shape=64x1080x1920][layout=NCHW][inputKind=Tensor][blockSize=7]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3955.5002429182196, + "gpu_time_us_python": 3967.8178458341, + "gpu_noise_us_cpp": 4.048, + "gpu_noise_us_python": 5.144, + "gpu_bwutil_cpp": 0.0431470493897683, + "gpu_bwutil_python": 0.0430130375566251, + "gpu_gap_stddev_us": 2.0616814682912343 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 3710.0675379252248, + "gpu_time_us_python": 3721.850276238275, + "gpu_noise_us_cpp": 1.6549999999999998, + "gpu_noise_us_python": 3.205, + "gpu_bwutil_cpp": 0.03509019251957115, + "gpu_bwutil_python": 0.03497877768893555, + "gpu_gap_stddev_us": 1.79324448078274 + } + } + } + }, + "adaptivethreshold_planar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "blockSize": [ + 7 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AdaptiveThreshold single-channel (advanced) (native planar NCHW reference tensor)." + }, + "baselines": { + "adaptivethreshold_planar_nchw_advanced[InOutDataType=uint8][shape=64x1080x1920][layout=NCHW][inputKind=Tensor][blockSize=7]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3954.855063013559, + "gpu_time_us_python": 3965.32822699534, + "gpu_noise_us_cpp": 3.7939999999999996, + "gpu_noise_us_python": 4.806, + "gpu_bwutil_cpp": 0.043154052633499904, + "gpu_bwutil_python": 0.04304005103463642, + "gpu_gap_stddev_us": 1.4790187764410279 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 3710.712542568375, + "gpu_time_us_python": 3722.563936677825, + "gpu_noise_us_cpp": 1.6775, + "gpu_noise_us_python": 3.2399999999999998, + "gpu_bwutil_cpp": 0.03508393002470285, + "gpu_bwutil_python": 0.03497200639047025, + "gpu_gap_stddev_us": 1.0918621823395613 + } + } + } + }, + "adaptivethreshold_fakeplanar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "blockSize": [ + 7 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AdaptiveThreshold single-channel (advanced) (fake planar NCHW_FAKE tensor)." + }, + "baselines": { + "adaptivethreshold_fakeplanar_nchw_advanced[InOutDataType=uint8][shape=64x1080x1920][layout=NCHW_FAKE][inputKind=Tensor][blockSize=7]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4339.214078311221, + "gpu_time_us_python": 4356.8199420797, + "gpu_noise_us_cpp": 4.484, + "gpu_noise_us_python": 5.104, + "gpu_bwutil_cpp": 0.11799442100740516, + "gpu_bwutil_python": 0.11751753991873555, + "gpu_gap_stddev_us": 0.8832412498205284 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 3998.9866543530998, + "gpu_time_us_python": 4014.607048142775, + "gpu_noise_us_cpp": 1.54, + "gpu_noise_us_python": 5.465000000000001, + "gpu_bwutil_cpp": 0.09766325091663572, + "gpu_bwutil_python": 0.09728286007752582, + "gpu_gap_stddev_us": 2.089699889079874 + } + } + } + }, + "adaptivethreshold_block3_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "128x720x1280", + "64x1080x1920", + "16x2160x3840" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "int64_axes": { + "blockSize": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AdaptiveThreshold single-channel block3 (advanced)." + }, + "baselines": { + "adaptivethreshold_block3_advanced[InOutDataType=uint8][shape=128x720x1280][layout=NHWC][inputKind=Tensor][blockSize=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 910.2740038083, + "gpu_time_us_python": 920.8474986366, + "gpu_noise_us_cpp": 1.504, + "gpu_noise_us_python": 2.2659999999999996, + "gpu_bwutil_cpp": 0.1666584288818464, + "gpu_bwutil_python": 0.16474473890555522, + "gpu_gap_stddev_us": 0.9465205767232141 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 860.3711560256, + "gpu_time_us_python": 870.7621246226, + "gpu_noise_us_cpp": 1.4150000000000003, + "gpu_noise_us_python": 4.3100000000000005, + "gpu_bwutil_cpp": 0.1345003006380995, + "gpu_bwutil_python": 0.13289173453353575, + "gpu_gap_stddev_us": 1.3114974419585856 + } + }, + "adaptivethreshold_block3_advanced[InOutDataType=uint8][shape=128x720x1280][layout=NHWC][inputKind=VarShape][blockSize=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1567.7545062157, + "gpu_time_us_python": 1582.08228442494, + "gpu_noise_us_cpp": 4.160000000000001, + "gpu_noise_us_python": 4.996, + "gpu_bwutil_cpp": 0.09676554582081882, + "gpu_bwutil_python": 0.09588911504431148, + "gpu_gap_stddev_us": 2.4296852659081805 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1439.999092410975, + "gpu_time_us_python": 1456.05266069305, + "gpu_noise_us_cpp": 4.2125, + "gpu_noise_us_python": 4.915, + "gpu_bwutil_cpp": 0.0803611272211926, + "gpu_bwutil_python": 0.07947305830369812, + "gpu_gap_stddev_us": 2.283301755554672 + } + }, + "adaptivethreshold_block3_advanced[InOutDataType=uint8][shape=16x2160x3840][layout=NHWC][inputKind=Tensor][blockSize=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1024.8276309469602, + "gpu_time_us_python": 1034.19285152954, + "gpu_noise_us_cpp": 0.8, + "gpu_noise_us_python": 2.904, + "gpu_bwutil_cpp": 0.1665337109529807, + "gpu_bwutil_python": 0.1650249085685494, + "gpu_gap_stddev_us": 2.206205741789934 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 967.13550415225, + "gpu_time_us_python": 978.6117336065499, + "gpu_noise_us_cpp": 1.02, + "gpu_noise_us_python": 4.985, + "gpu_bwutil_cpp": 0.13460909036331728, + "gpu_bwutil_python": 0.13302699726028275, + "gpu_gap_stddev_us": 1.464026567841511 + } + }, + "adaptivethreshold_block3_advanced[InOutDataType=uint8][shape=16x2160x3840][layout=NHWC][inputKind=VarShape][blockSize=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1741.2903789416, + "gpu_time_us_python": 1754.25483556902, + "gpu_noise_us_cpp": 2.3999999999999995, + "gpu_noise_us_python": 3.63, + "gpu_bwutil_cpp": 0.09801239358726, + "gpu_bwutil_python": 0.09728790547956784, + "gpu_gap_stddev_us": 3.000263683640708 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1603.5736309253002, + "gpu_time_us_python": 1619.6862091246248, + "gpu_noise_us_cpp": 2.3725, + "gpu_noise_us_python": 5.9350000000000005, + "gpu_bwutil_cpp": 0.08118390841782258, + "gpu_bwutil_python": 0.08037535753241118, + "gpu_gap_stddev_us": 1.665154801483366 + } + }, + "adaptivethreshold_block3_advanced[InOutDataType=uint8][shape=64x1080x1920][layout=NHWC][inputKind=Tensor][blockSize=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1029.99122386582, + "gpu_time_us_python": 1040.9196787430997, + "gpu_noise_us_cpp": 1.9760000000000002, + "gpu_noise_us_python": 3.69, + "gpu_bwutil_cpp": 0.1656989171658667, + "gpu_bwutil_python": 0.1639583663057951, + "gpu_gap_stddev_us": 2.096901530436515 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 972.2250359134, + "gpu_time_us_python": 982.325594060075, + "gpu_noise_us_cpp": 0.9, + "gpu_noise_us_python": 4.0875, + "gpu_bwutil_cpp": 0.13390483528643, + "gpu_bwutil_python": 0.13252730107247992, + "gpu_gap_stddev_us": 0.5448819238671344 + } + }, + "adaptivethreshold_block3_advanced[InOutDataType=uint8][shape=64x1080x1920][layout=NHWC][inputKind=VarShape][blockSize=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1755.36010675822, + "gpu_time_us_python": 1769.2946877194202, + "gpu_noise_us_cpp": 4.726, + "gpu_noise_us_python": 3.9919999999999995, + "gpu_bwutil_cpp": 0.09722673250454758, + "gpu_bwutil_python": 0.09646079884719325, + "gpu_gap_stddev_us": 2.1250145446907074 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1614.8349225155002, + "gpu_time_us_python": 1631.4801993142999, + "gpu_noise_us_cpp": 1.9449999999999998, + "gpu_noise_us_python": 5.375, + "gpu_bwutil_cpp": 0.08061743836935108, + "gpu_bwutil_python": 0.0797943737058315, + "gpu_gap_stddev_us": 0.5538558493469165 + } + } + } + }, + "adaptivethreshold_planar_nchw_block3_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "128x720x1280", + "64x1080x1920", + "16x2160x3840" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "blockSize": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AdaptiveThreshold single-channel block3 (advanced) (native planar NCHW tensor)." + }, + "baselines": { + "adaptivethreshold_planar_nchw_block3_advanced[InOutDataType=uint8][shape=128x720x1280][layout=NCHW][inputKind=Tensor][blockSize=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 910.11306456816, + "gpu_time_us_python": 921.07725008466, + "gpu_noise_us_cpp": 2.3199999999999994, + "gpu_noise_us_python": 2.334, + "gpu_bwutil_cpp": 0.1666873977250392, + "gpu_bwutil_python": 0.1647039384702583, + "gpu_gap_stddev_us": 1.1591669766632928 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 860.4022878761999, + "gpu_time_us_python": 871.210520005625, + "gpu_noise_us_cpp": 2.055, + "gpu_noise_us_python": 3.25, + "gpu_bwutil_cpp": 0.13449577270485574, + "gpu_bwutil_python": 0.13282758197704764, + "gpu_gap_stddev_us": 2.4556555993358997 + } + }, + "adaptivethreshold_planar_nchw_block3_advanced[InOutDataType=uint8][shape=16x2160x3840][layout=NCHW][inputKind=Tensor][blockSize=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1023.77807065804, + "gpu_time_us_python": 1034.5472671615, + "gpu_noise_us_cpp": 1.362, + "gpu_noise_us_python": 2.614, + "gpu_bwutil_cpp": 0.16670394550974788, + "gpu_bwutil_python": 0.16496866107005442, + "gpu_gap_stddev_us": 1.1200290277445912 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 967.0525030219499, + "gpu_time_us_python": 978.703311285525, + "gpu_noise_us_cpp": 1.3925, + "gpu_noise_us_python": 3.3575, + "gpu_bwutil_cpp": 0.13462092230036993, + "gpu_bwutil_python": 0.13301162828509616, + "gpu_gap_stddev_us": 2.8170784899194596 + } + }, + "adaptivethreshold_planar_nchw_block3_advanced[InOutDataType=uint8][shape=64x1080x1920][layout=NCHW][inputKind=Tensor][blockSize=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1030.15110878624, + "gpu_time_us_python": 1040.64740245866, + "gpu_noise_us_cpp": 1.846, + "gpu_noise_us_python": 1.8059999999999998, + "gpu_bwutil_cpp": 0.1656729824808563, + "gpu_bwutil_python": 0.16400158787370606, + "gpu_gap_stddev_us": 0.8050220383559554 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 972.1901613176749, + "gpu_time_us_python": 982.4878074131251, + "gpu_noise_us_cpp": 1.165, + "gpu_noise_us_python": 3.515, + "gpu_bwutil_cpp": 0.13390933182989112, + "gpu_bwutil_python": 0.13250684232782878, + "gpu_gap_stddev_us": 0.8718801349459745 + } + } + } + }, + "adaptivethreshold_fakeplanar_nchw_block3_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "128x720x1280", + "64x1080x1920", + "16x2160x3840" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "blockSize": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AdaptiveThreshold single-channel block3 (advanced) (fake planar NCHW_FAKE tensor)." + }, + "baselines": { + "adaptivethreshold_fakeplanar_nchw_block3_advanced[InOutDataType=uint8][shape=128x720x1280][layout=NCHW_FAKE][inputKind=Tensor][blockSize=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1253.61504020338, + "gpu_time_us_python": 1270.5046626981002, + "gpu_noise_us_cpp": 1.794, + "gpu_noise_us_python": 3.19, + "gpu_bwutil_cpp": 0.3630404858480327, + "gpu_bwutil_python": 0.3582142016423568, + "gpu_gap_stddev_us": 1.411562713081079 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1115.86526776705, + "gpu_time_us_python": 1130.1310521973749, + "gpu_noise_us_cpp": 2.53, + "gpu_noise_us_python": 3.4299999999999997, + "gpu_bwutil_cpp": 0.31109791659715713, + "gpu_bwutil_python": 0.3071672312900222, + "gpu_gap_stddev_us": 1.3300150837921798 + } + }, + "adaptivethreshold_fakeplanar_nchw_block3_advanced[InOutDataType=uint8][shape=16x2160x3840][layout=NCHW_FAKE][inputKind=Tensor][blockSize=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1410.31670655802, + "gpu_time_us_python": 1427.40689046968, + "gpu_noise_us_cpp": 1.29, + "gpu_noise_us_python": 4.640000000000001, + "gpu_bwutil_cpp": 0.36304117990805673, + "gpu_bwutil_python": 0.3586941508755301, + "gpu_gap_stddev_us": 1.9151617810656194 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1255.0967712166998, + "gpu_time_us_python": 1269.4141430449001, + "gpu_noise_us_cpp": 1.42, + "gpu_noise_us_python": 3.585, + "gpu_bwutil_cpp": 0.3111600916906277, + "gpu_bwutil_python": 0.3076471666769576, + "gpu_gap_stddev_us": 0.8727453864524247 + } + }, + "adaptivethreshold_fakeplanar_nchw_block3_advanced[InOutDataType=uint8][shape=64x1080x1920][layout=NCHW_FAKE][inputKind=Tensor][blockSize=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1415.49614468032, + "gpu_time_us_python": 1430.80989934558, + "gpu_noise_us_cpp": 1.6139999999999997, + "gpu_noise_us_python": 2.8440000000000003, + "gpu_bwutil_cpp": 0.36171259774499986, + "gpu_bwutil_python": 0.35784024810827575, + "gpu_gap_stddev_us": 2.3785923427155806 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1260.54430298235, + "gpu_time_us_python": 1274.5461713507998, + "gpu_noise_us_cpp": 2.5575, + "gpu_noise_us_python": 2.3699999999999997, + "gpu_bwutil_cpp": 0.3098159872531966, + "gpu_bwutil_python": 0.3064056171712196, + "gpu_gap_stddev_us": 2.3218048508204885 + } + } + } + }, + "adaptivethreshold_block7_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "64x720x1280", + "32x1080x1920", + "8x2160x3840" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "int64_axes": { + "blockSize": [ + 7 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AdaptiveThreshold single-channel block7 (advanced)." + }, + "baselines": { + "adaptivethreshold_block7_advanced[InOutDataType=uint8][shape=32x1080x1920][layout=NHWC][inputKind=Tensor][blockSize=7]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1985.48894133686, + "gpu_time_us_python": 1996.70511061646, + "gpu_noise_us_cpp": 3.825999999999999, + "gpu_noise_us_python": 5.116, + "gpu_bwutil_cpp": 0.04297880445301193, + "gpu_bwutil_python": 0.042737446580388576, + "gpu_gap_stddev_us": 1.9814507104830263 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1861.004274379525, + "gpu_time_us_python": 1872.910379553825, + "gpu_noise_us_cpp": 1.075, + "gpu_noise_us_python": 3.09, + "gpu_bwutil_cpp": 0.03497753712345868, + "gpu_bwutil_python": 0.034755733934049425, + "gpu_gap_stddev_us": 3.8719421925187483 + } + }, + "adaptivethreshold_block7_advanced[InOutDataType=uint8][shape=32x1080x1920][layout=NHWC][inputKind=VarShape][blockSize=7]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2396.08770534662, + "gpu_time_us_python": 2409.33414235652, + "gpu_noise_us_cpp": 5.1, + "gpu_noise_us_python": 5.692, + "gpu_bwutil_cpp": 0.035613826763197805, + "gpu_bwutil_python": 0.035418027414360995, + "gpu_gap_stddev_us": 2.70954708354198 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2251.507557232575, + "gpu_time_us_python": 2267.494586702275, + "gpu_noise_us_cpp": 3.2474999999999996, + "gpu_noise_us_python": 8.1125, + "gpu_bwutil_cpp": 0.0289106950669181, + "gpu_bwutil_python": 0.028706870533326254, + "gpu_gap_stddev_us": 2.0801346644159553 + } + }, + "adaptivethreshold_block7_advanced[InOutDataType=uint8][shape=64x720x1280][layout=NHWC][inputKind=Tensor][blockSize=7]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1762.4006741212802, + "gpu_time_us_python": 1772.4626791654002, + "gpu_noise_us_cpp": 2.39, + "gpu_noise_us_python": 3.5620000000000003, + "gpu_bwutil_cpp": 0.04303920965086992, + "gpu_bwutil_python": 0.0427949225589161, + "gpu_gap_stddev_us": 2.2910700183721993 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1654.142467331375, + "gpu_time_us_python": 1666.1258190577, + "gpu_noise_us_cpp": 2.2225, + "gpu_noise_us_python": 5.42, + "gpu_bwutil_cpp": 0.03497982469387327, + "gpu_bwutil_python": 0.034728510151978774, + "gpu_gap_stddev_us": 3.2029126863853334 + } + }, + "adaptivethreshold_block7_advanced[InOutDataType=uint8][shape=64x720x1280][layout=NHWC][inputKind=VarShape][blockSize=7]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2135.3228967188797, + "gpu_time_us_python": 2151.32866338958, + "gpu_noise_us_cpp": 5.101999999999999, + "gpu_noise_us_python": 5.41, + "gpu_bwutil_cpp": 0.03552266870125182, + "gpu_bwutil_python": 0.03525833652835582, + "gpu_gap_stddev_us": 2.2084755383759216 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2003.210693227325, + "gpu_time_us_python": 2021.55243799445, + "gpu_noise_us_cpp": 7.1274999999999995, + "gpu_noise_us_python": 5.26, + "gpu_bwutil_cpp": 0.02888330414168295, + "gpu_bwutil_python": 0.02862173908008125, + "gpu_gap_stddev_us": 4.5798148015974345 + } + }, + "adaptivethreshold_block7_advanced[InOutDataType=uint8][shape=8x2160x3840][layout=NHWC][inputKind=Tensor][blockSize=7]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1981.1742431892599, + "gpu_time_us_python": 1992.3171167978599, + "gpu_noise_us_cpp": 1.8380000000000003, + "gpu_noise_us_python": 2.5260000000000002, + "gpu_bwutil_cpp": 0.04307252367432775, + "gpu_bwutil_python": 0.042831589746003824, + "gpu_gap_stddev_us": 2.188442065983459 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1861.0394466156251, + "gpu_time_us_python": 1872.38741436395, + "gpu_noise_us_cpp": 1.175, + "gpu_noise_us_python": 3.45, + "gpu_bwutil_cpp": 0.0349769502691359, + "gpu_bwutil_python": 0.03476496115278663, + "gpu_gap_stddev_us": 1.9237439188369425 + } + }, + "adaptivethreshold_block7_advanced[InOutDataType=uint8][shape=8x2160x3840][layout=NHWC][inputKind=VarShape][blockSize=7]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2392.7135235922005, + "gpu_time_us_python": 2404.6134727516, + "gpu_noise_us_cpp": 6.516, + "gpu_noise_us_python": 6.446, + "gpu_bwutil_cpp": 0.03566407977590674, + "gpu_bwutil_python": 0.03548743809114172, + "gpu_gap_stddev_us": 4.204838399963295 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2246.59249050855, + "gpu_time_us_python": 2261.3685864549752, + "gpu_noise_us_cpp": 3.2975000000000003, + "gpu_noise_us_python": 6.5475, + "gpu_bwutil_cpp": 0.028974136206686874, + "gpu_bwutil_python": 0.028784606983485474, + "gpu_gap_stddev_us": 1.4153846792377238 + } + } + } + }, + "adaptivethreshold_planar_nchw_block7_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "64x720x1280", + "32x1080x1920", + "8x2160x3840" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "blockSize": [ + 7 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AdaptiveThreshold single-channel block7 (advanced) (native planar NCHW tensor)." + }, + "baselines": { + "adaptivethreshold_planar_nchw_block7_advanced[InOutDataType=uint8][shape=32x1080x1920][layout=NCHW][inputKind=Tensor][blockSize=7]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1985.07463703326, + "gpu_time_us_python": 1996.1703280191, + "gpu_noise_us_cpp": 2.574, + "gpu_noise_us_python": 3.69, + "gpu_bwutil_cpp": 0.04298774563903444, + "gpu_bwutil_python": 0.04274880073876478, + "gpu_gap_stddev_us": 1.0790913993495295 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1861.6805527308, + "gpu_time_us_python": 1875.0213553526248, + "gpu_noise_us_cpp": 1.9575, + "gpu_noise_us_python": 3.53, + "gpu_bwutil_cpp": 0.034964930163842, + "gpu_bwutil_python": 0.03471555757550833, + "gpu_gap_stddev_us": 2.3827842624986566 + } + }, + "adaptivethreshold_planar_nchw_block7_advanced[InOutDataType=uint8][shape=64x720x1280][layout=NCHW][inputKind=Tensor][blockSize=7]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1762.48421190424, + "gpu_time_us_python": 1773.7361610822197, + "gpu_noise_us_cpp": 2.256, + "gpu_noise_us_python": 3.6740000000000004, + "gpu_bwutil_cpp": 0.0430371608563319, + "gpu_bwutil_python": 0.04276416304796311, + "gpu_gap_stddev_us": 1.9812181426467628 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1654.2470382097, + "gpu_time_us_python": 1665.8905039152999, + "gpu_noise_us_cpp": 2.1625, + "gpu_noise_us_python": 4.9625, + "gpu_bwutil_cpp": 0.0349776036515725, + "gpu_bwutil_python": 0.034732485860552824, + "gpu_gap_stddev_us": 2.4034959029756275 + } + }, + "adaptivethreshold_planar_nchw_block7_advanced[InOutDataType=uint8][shape=8x2160x3840][layout=NCHW][inputKind=Tensor][blockSize=7]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1981.8305672964998, + "gpu_time_us_python": 1993.4166132759801, + "gpu_noise_us_cpp": 3.3940000000000006, + "gpu_noise_us_python": 4.064, + "gpu_bwutil_cpp": 0.043058156493118685, + "gpu_bwutil_python": 0.042807923525573346, + "gpu_gap_stddev_us": 0.9030373903896501 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1862.3419185228252, + "gpu_time_us_python": 1874.8805935989749, + "gpu_noise_us_cpp": 2.2425, + "gpu_noise_us_python": 5.44, + "gpu_bwutil_cpp": 0.03495255836863832, + "gpu_bwutil_python": 0.034718916515743777, + "gpu_gap_stddev_us": 4.09281369181637 + } + } + } + }, + "adaptivethreshold_fakeplanar_nchw_block7_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "64x720x1280", + "32x1080x1920", + "8x2160x3840" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "blockSize": [ + 7 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AdaptiveThreshold single-channel block7 (advanced) (fake planar NCHW_FAKE tensor)." + }, + "baselines": { + "adaptivethreshold_fakeplanar_nchw_block7_advanced[InOutDataType=uint8][shape=32x1080x1920][layout=NCHW_FAKE][inputKind=Tensor][blockSize=7]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2180.23315139434, + "gpu_time_us_python": 2197.04282290326, + "gpu_noise_us_cpp": 2.798, + "gpu_noise_us_python": 5.568, + "gpu_bwutil_cpp": 0.11741939584344832, + "gpu_bwutil_python": 0.1165208033334955, + "gpu_gap_stddev_us": 2.3520393682553933 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2007.1231921032752, + "gpu_time_us_python": 2023.6021364629999, + "gpu_noise_us_cpp": 5.147500000000001, + "gpu_noise_us_python": 5.42, + "gpu_bwutil_cpp": 0.09729076806643472, + "gpu_bwutil_python": 0.09650004138836601, + "gpu_gap_stddev_us": 2.7516034135963547 + } + }, + "adaptivethreshold_fakeplanar_nchw_block7_advanced[InOutDataType=uint8][shape=64x720x1280][layout=NCHW_FAKE][inputKind=Tensor][blockSize=7]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1936.3005884359998, + "gpu_time_us_python": 1952.8996001221003, + "gpu_noise_us_cpp": 3.3939999999999997, + "gpu_noise_us_python": 3.054, + "gpu_bwutil_cpp": 0.11752143232247994, + "gpu_bwutil_python": 0.11652236690053444, + "gpu_gap_stddev_us": 1.144385851584945 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1784.399020984425, + "gpu_time_us_python": 1799.093624516675, + "gpu_noise_us_cpp": 2.2325, + "gpu_noise_us_python": 4.4175, + "gpu_bwutil_cpp": 0.09727659192212898, + "gpu_bwutil_python": 0.09648180383785465, + "gpu_gap_stddev_us": 2.236561248113829 + } + }, + "adaptivethreshold_fakeplanar_nchw_block7_advanced[InOutDataType=uint8][shape=8x2160x3840][layout=NCHW_FAKE][inputKind=Tensor][blockSize=7]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2176.9911241852797, + "gpu_time_us_python": 2193.9677058495004, + "gpu_noise_us_cpp": 3.0220000000000002, + "gpu_noise_us_python": 3.752, + "gpu_bwutil_cpp": 0.11759437554315902, + "gpu_bwutil_python": 0.1166842055685644, + "gpu_gap_stddev_us": 4.209608179020383 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2008.07679748285, + "gpu_time_us_python": 2022.615038668975, + "gpu_noise_us_cpp": 1.6774999999999998, + "gpu_noise_us_python": 3.8474999999999997, + "gpu_bwutil_cpp": 0.09724637679207307, + "gpu_bwutil_python": 0.0965465170285177, + "gpu_gap_stddev_us": 1.4318391065558234 + } + } + } + } + } +} diff --git a/bench/config/operators/adjustcontrast.json b/bench/config/operators/adjustcontrast.json new file mode 100644 index 000000000..2c6a3ed09 --- /dev/null +++ b/bench/config/operators/adjustcontrast.json @@ -0,0 +1,570 @@ +{ + "benchmark": "adjustcontrast", + "configs": { + "adjustcontrast_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AdjustContrast RGB8 interleaved + planar, Tensor + VarShape (~1-2ms)." + }, + "baselines": { + "adjustcontrast_uchar3_basic[InOutDataType=uchar3][shape=32x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1105.59967404416, + "gpu_time_us_python": 1115.0067606194402, + "gpu_noise_us_cpp": 1.1600000000000001, + "gpu_noise_us_python": 2.6719999999999997, + "gpu_bwutil_cpp": 0.34732289800388605, + "gpu_bwutil_python": 0.34439290256662203, + "gpu_gap_stddev_us": 1.714448583948295 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 744.5338396801999, + "gpu_time_us_python": 758.4946458514601, + "gpu_noise_us_cpp": 0.9640000000000001, + "gpu_noise_us_python": 3.558, + "gpu_bwutil_cpp": 0.3933912007040953, + "gpu_bwutil_python": 0.3861518210085613, + "gpu_gap_stddev_us": 2.9318707569266755 + } + }, + "adjustcontrast_uchar3_basic[InOutDataType=uchar3][shape=32x1080x1920][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1210.96857239078, + "gpu_time_us_python": 1224.6501985934399, + "gpu_noise_us_cpp": 3.566, + "gpu_noise_us_python": 4.256, + "gpu_bwutil_cpp": 0.31710176004059637, + "gpu_bwutil_python": 0.31356047541579, + "gpu_gap_stddev_us": 3.5450270526422614 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 846.8981957814598, + "gpu_time_us_python": 861.8928999071799, + "gpu_noise_us_cpp": 1.3599999999999999, + "gpu_noise_us_python": 3.038, + "gpu_bwutil_cpp": 0.34584829599375727, + "gpu_bwutil_python": 0.339829799701632, + "gpu_gap_stddev_us": 2.4885067901476674 + } + }, + "adjustcontrast_uchar3_basic[InOutDataType=uchar3][shape=32x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 814.16003860802, + "gpu_time_us_python": 823.6712491825399, + "gpu_noise_us_cpp": 1.536, + "gpu_noise_us_python": 2.588, + "gpu_bwutil_cpp": 0.47165195039374364, + "gpu_bwutil_python": 0.4662056805935374, + "gpu_gap_stddev_us": 0.9145757025070158 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 616.8917617765198, + "gpu_time_us_python": 628.84600466984, + "gpu_noise_us_cpp": 1.326, + "gpu_noise_us_python": 3.0, + "gpu_bwutil_cpp": 0.47478704135805155, + "gpu_bwutil_python": 0.4657849453677666, + "gpu_gap_stddev_us": 2.4518140456020756 + } + }, + "adjustcontrast_uchar3_basic[InOutDataType=uchar3][shape=32x1080x1920][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 872.86189171492, + "gpu_time_us_python": 885.43969633282, + "gpu_noise_us_cpp": 2.5759999999999996, + "gpu_noise_us_python": 3.086, + "gpu_bwutil_cpp": 0.4399322808108754, + "gpu_bwutil_python": 0.4336848768045713, + "gpu_gap_stddev_us": 1.8032349927257254 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 674.51342656758, + "gpu_time_us_python": 690.74636335016, + "gpu_noise_us_cpp": 1.886, + "gpu_noise_us_python": 3.088, + "gpu_bwutil_cpp": 0.4342405994778574, + "gpu_bwutil_python": 0.4240640135393267, + "gpu_gap_stddev_us": 3.4720623974525235 + } + } + } + }, + "adjustcontrast_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "20x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AdjustContrast RGBf32 interleaved + planar, Tensor (~1-2ms)." + }, + "baselines": { + "adjustcontrast_float3_basic[InOutDataType=float3][shape=20x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3496.3740011055597, + "gpu_time_us_python": 3508.0192049343605, + "gpu_noise_us_cpp": 5.032, + "gpu_noise_us_python": 5.176, + "gpu_bwutil_cpp": 0.2745701760359053, + "gpu_bwutil_python": 0.2736587389695079, + "gpu_gap_stddev_us": 1.5124182560684742 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3426.7789438714, + "gpu_time_us_python": 3440.67349303254, + "gpu_noise_us_cpp": 5.4, + "gpu_noise_us_python": 6.882000000000001, + "gpu_bwutil_cpp": 0.21367349724318876, + "gpu_bwutil_python": 0.21281199228804054, + "gpu_gap_stddev_us": 3.6306533851524123 + } + }, + "adjustcontrast_float3_basic[InOutDataType=float3][shape=20x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3530.08567447388, + "gpu_time_us_python": 3543.69767215884, + "gpu_noise_us_cpp": 21.264, + "gpu_noise_us_python": 19.375999999999998, + "gpu_bwutil_cpp": 0.2719482069341902, + "gpu_bwutil_python": 0.2709035688403279, + "gpu_gap_stddev_us": 4.5585414764150896 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3337.87910059036, + "gpu_time_us_python": 3350.6215305328005, + "gpu_noise_us_cpp": 6.368, + "gpu_noise_us_python": 7.869999999999999, + "gpu_bwutil_cpp": 0.21936451412050378, + "gpu_bwutil_python": 0.21853005571540512, + "gpu_gap_stddev_us": 2.3697447027468477 + } + } + } + }, + "adjustcontrast_float3_varshape_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "20x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AdjustContrast RGBf32 interleaved + planar, VarShape (~1-2ms)." + }, + "baselines": { + "adjustcontrast_float3_varshape_advanced[InOutDataType=float3][shape=20x1080x1920][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2332.1789825403803, + "gpu_time_us_python": 2347.56305819354, + "gpu_noise_us_cpp": 7.254, + "gpu_noise_us_python": 8.956, + "gpu_bwutil_cpp": 0.41163250153711395, + "gpu_bwutil_python": 0.4089364352092546, + "gpu_gap_stddev_us": 4.251472613995515 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3359.7680632272995, + "gpu_time_us_python": 3371.6680856309004, + "gpu_noise_us_cpp": 4.272, + "gpu_noise_us_python": 11.219999999999999, + "gpu_bwutil_cpp": 0.21793468821446565, + "gpu_bwutil_python": 0.21716566525352832, + "gpu_gap_stddev_us": 2.9611024615494217 + } + }, + "adjustcontrast_float3_varshape_advanced[InOutDataType=float3][shape=20x1080x1920][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3570.29266057114, + "gpu_time_us_python": 3584.47984354832, + "gpu_noise_us_cpp": 6.032, + "gpu_noise_us_python": 7.894, + "gpu_bwutil_cpp": 0.2688855836557279, + "gpu_bwutil_python": 0.2678213274299357, + "gpu_gap_stddev_us": 3.2703468103941455 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3289.150580721, + "gpu_time_us_python": 3303.2472422248, + "gpu_noise_us_cpp": 7.773999999999999, + "gpu_noise_us_python": 8.791999999999998, + "gpu_bwutil_cpp": 0.22261405153412306, + "gpu_bwutil_python": 0.22166439140089383, + "gpu_gap_stddev_us": 3.9884520026264485 + } + } + } + }, + "adjustcontrast_planar_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AdjustContrast RGB8 native-planar peer for the fake-planar comparison." + }, + "baselines": { + "adjustcontrast_planar_uchar3_advanced[InOutDataType=uchar3][shape=32x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1104.9916021623999, + "gpu_time_us_python": 1115.20048253874, + "gpu_noise_us_cpp": 1.568, + "gpu_noise_us_python": 2.334, + "gpu_bwutil_cpp": 0.3475140056891029, + "gpu_bwutil_python": 0.34433295021971655, + "gpu_gap_stddev_us": 0.7405259450326266 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 744.8496484939199, + "gpu_time_us_python": 756.6733766063201, + "gpu_noise_us_cpp": 1.1300000000000001, + "gpu_noise_us_python": 3.3, + "gpu_bwutil_cpp": 0.3932243369656342, + "gpu_bwutil_python": 0.38708338766431305, + "gpu_gap_stddev_us": 2.550069846806808 + } + } + } + }, + "adjustcontrast_fake_planar_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AdjustContrast RGB8 fake-planar comparison (tensor-only)." + }, + "baselines": { + "adjustcontrast_fake_planar_uchar3_advanced[InOutDataType=uchar3][shape=32x1080x1920][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2006.84341354366, + "gpu_time_us_python": 2024.26789576002, + "gpu_noise_us_cpp": 2.3720000000000003, + "gpu_noise_us_python": 3.7959999999999994, + "gpu_bwutil_cpp": 0.4464724856913488, + "gpu_bwutil_python": 0.4426292789163959, + "gpu_gap_stddev_us": 1.6449697472746678 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1546.54401591582, + "gpu_time_us_python": 1561.9656972346, + "gpu_noise_us_cpp": 1.098, + "gpu_noise_us_python": 2.996, + "gpu_bwutil_cpp": 0.4418918250134146, + "gpu_bwutil_python": 0.43752797556762363, + "gpu_gap_stddev_us": 0.6320118811610096 + } + } + } + }, + "adjustcontrast_planar_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "20x1080x1920" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AdjustContrast RGBf32 native-planar peer for the fake-planar comparison." + }, + "baselines": { + "adjustcontrast_planar_float3_advanced[InOutDataType=float3][shape=20x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3497.13347090608, + "gpu_time_us_python": 3508.46009784272, + "gpu_noise_us_cpp": 5.306, + "gpu_noise_us_python": 5.638, + "gpu_bwutil_cpp": 0.27451063745050613, + "gpu_bwutil_python": 0.2736243581841717, + "gpu_gap_stddev_us": 1.6444171251027673 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3427.7065669196804, + "gpu_time_us_python": 3440.9719238541998, + "gpu_noise_us_cpp": 6.410000000000001, + "gpu_noise_us_python": 8.126, + "gpu_bwutil_cpp": 0.21361547525209584, + "gpu_bwutil_python": 0.21279251561817025, + "gpu_gap_stddev_us": 2.603862462384922 + } + } + } + }, + "adjustcontrast_fake_planar_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "20x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AdjustContrast RGBf32 fake-planar comparison (tensor-only)." + }, + "baselines": { + "adjustcontrast_fake_planar_float3_advanced[InOutDataType=float3][shape=20x1080x1920][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5208.72008889704, + "gpu_time_us_python": 5225.5701204140605, + "gpu_noise_us_cpp": 19.662, + "gpu_noise_us_python": 19.032000000000004, + "gpu_bwutil_cpp": 0.4300482688833941, + "gpu_bwutil_python": 0.4286615578179312, + "gpu_gap_stddev_us": 5.9578035025807505 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4713.450450417319, + "gpu_time_us_python": 4733.10696583868, + "gpu_noise_us_cpp": 7.434, + "gpu_noise_us_python": 9.054, + "gpu_bwutil_cpp": 0.3624704817660126, + "gpu_bwutil_python": 0.36096666343870487, + "gpu_gap_stddev_us": 4.744348384012614 + } + } + } + }, + "adjustcontrast_uint8_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AdjustContrast U8 1-channel interleaved, Tensor + VarShape (single-channel lives in advanced per RGB guideline)." + }, + "baselines": { + "adjustcontrast_uint8_advanced[InOutDataType=uint8][shape=128x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1503.33624043146, + "gpu_time_us_python": 1514.53908272402, + "gpu_noise_us_cpp": 3.2640000000000002, + "gpu_noise_us_python": 4.488, + "gpu_bwutil_cpp": 0.3405759362037024, + "gpu_bwutil_python": 0.33805701541943156, + "gpu_gap_stddev_us": 1.135972686124994 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1138.9085489714403, + "gpu_time_us_python": 1151.1427961801198, + "gpu_noise_us_cpp": 1.37, + "gpu_noise_us_python": 3.444, + "gpu_bwutil_cpp": 0.3429081700404506, + "gpu_bwutil_python": 0.339268538903151, + "gpu_gap_stddev_us": 2.149463038572432 + } + }, + "adjustcontrast_uint8_advanced[InOutDataType=uint8][shape=128x1080x1920][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1760.1812920311, + "gpu_time_us_python": 1778.32889113016, + "gpu_noise_us_cpp": 2.902, + "gpu_noise_us_python": 4.244, + "gpu_bwutil_cpp": 0.2908797528767214, + "gpu_bwutil_python": 0.28791312141691583, + "gpu_gap_stddev_us": 4.32462802867699 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1346.2490653936002, + "gpu_time_us_python": 1368.2898325499398, + "gpu_noise_us_cpp": 2.364, + "gpu_noise_us_python": 7.031999999999999, + "gpu_bwutil_cpp": 0.2900864742873842, + "gpu_bwutil_python": 0.2854188177455967, + "gpu_gap_stddev_us": 2.2861248777661314 + } + } + } + }, + "adjustcontrast_float32_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "40x1080x1920" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AdjustContrast F32 1-channel interleaved, Tensor + VarShape (single-channel lives in advanced per RGB guideline)." + }, + "baselines": { + "adjustcontrast_float32_advanced[InOutDataType=float32][shape=40x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1930.45584293506, + "gpu_time_us_python": 1941.1896557770601, + "gpu_noise_us_cpp": 4.236, + "gpu_noise_us_python": 5.146, + "gpu_bwutil_cpp": 0.33152794859626095, + "gpu_bwutil_python": 0.32969475597607933, + "gpu_gap_stddev_us": 0.7834926326902542 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1749.9458986642999, + "gpu_time_us_python": 1764.1091740981399, + "gpu_noise_us_cpp": 1.9419999999999997, + "gpu_noise_us_python": 11.206000000000001, + "gpu_bwutil_cpp": 0.27894588896675193, + "gpu_bwutil_python": 0.2767058508942328, + "gpu_gap_stddev_us": 2.134463258185147 + } + }, + "adjustcontrast_float32_advanced[InOutDataType=float32][shape=40x1080x1920][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1952.8934461547801, + "gpu_time_us_python": 1950.6018396841803, + "gpu_noise_us_cpp": 6.118, + "gpu_noise_us_python": 6.574, + "gpu_bwutil_cpp": 0.3277193316610484, + "gpu_bwutil_python": 0.328104043084802, + "gpu_gap_stddev_us": 1.8787101440078324 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1776.9378356303798, + "gpu_time_us_python": 1792.3203734841402, + "gpu_noise_us_cpp": 2.7600000000000002, + "gpu_noise_us_python": 8.65, + "gpu_bwutil_cpp": 0.27470853960100816, + "gpu_bwutil_python": 0.27235086357650073, + "gpu_gap_stddev_us": 3.830126109761594 + } + } + } + } + } +} diff --git a/bench/config/operators/adjusthue.json b/bench/config/operators/adjusthue.json new file mode 100644 index 000000000..73b44b5ae --- /dev/null +++ b/bench/config/operators/adjusthue.json @@ -0,0 +1,390 @@ +{ + "benchmark": "adjusthue", + "configs": { + "adjusthue_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AdjustHue RGB8 interleaved + planar, Tensor + VarShape (~1-2ms)." + }, + "baselines": { + "adjusthue_uchar3_basic[InOutDataType=uchar3][shape=32x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1176.11885860614, + "gpu_time_us_python": 1185.77065921026, + "gpu_noise_us_cpp": 0.966, + "gpu_noise_us_python": 2.3080000000000003, + "gpu_bwutil_cpp": 0.21766633762178095, + "gpu_bwutil_python": 0.21589496371335026, + "gpu_gap_stddev_us": 1.0498349385279033 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1103.69576799634, + "gpu_time_us_python": 1113.5009855104202, + "gpu_noise_us_cpp": 1.1260000000000001, + "gpu_noise_us_python": 2.676, + "gpu_bwutil_cpp": 0.17693000340235518, + "gpu_bwutil_python": 0.17537000598918467, + "gpu_gap_stddev_us": 1.8477040530462001 + } + }, + "adjusthue_uchar3_basic[InOutDataType=uchar3][shape=32x1080x1920][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1735.1618048707999, + "gpu_time_us_python": 1747.1812478416002, + "gpu_noise_us_cpp": 2.3779999999999997, + "gpu_noise_us_python": 3.5460000000000003, + "gpu_bwutil_cpp": 0.1475376711075525, + "gpu_bwutil_python": 0.14652268601062007, + "gpu_gap_stddev_us": 1.4785435054348917 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1621.1483734898, + "gpu_time_us_python": 1634.98627367884, + "gpu_noise_us_cpp": 1.438, + "gpu_noise_us_python": 3.3880000000000003, + "gpu_bwutil_cpp": 0.12045577468172113, + "gpu_bwutil_python": 0.1194349677414451, + "gpu_gap_stddev_us": 3.964668481478549 + } + }, + "adjusthue_uchar3_basic[InOutDataType=uchar3][shape=32x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1274.48594346136, + "gpu_time_us_python": 1284.7557808322001, + "gpu_noise_us_cpp": 1.6199999999999999, + "gpu_noise_us_python": 2.866, + "gpu_bwutil_cpp": 0.20086606134653762, + "gpu_bwutil_python": 0.1992617610509539, + "gpu_gap_stddev_us": 2.592738260106866 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1210.5473331477, + "gpu_time_us_python": 1219.9564829205199, + "gpu_noise_us_cpp": 0.6719999999999999, + "gpu_noise_us_python": 2.322, + "gpu_bwutil_cpp": 0.16131268529293674, + "gpu_bwutil_python": 0.16006844891779368, + "gpu_gap_stddev_us": 0.45622748060964996 + } + }, + "adjusthue_uchar3_basic[InOutDataType=uchar3][shape=32x1080x1920][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1668.18946579956, + "gpu_time_us_python": 1681.03136296386, + "gpu_noise_us_cpp": 2.876, + "gpu_noise_us_python": 3.8059999999999996, + "gpu_bwutil_cpp": 0.15346058614136954, + "gpu_bwutil_python": 0.15228966771187083, + "gpu_gap_stddev_us": 3.2995425068101154 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1586.60538230516, + "gpu_time_us_python": 1601.05863352126, + "gpu_noise_us_cpp": 1.576, + "gpu_noise_us_python": 4.858, + "gpu_bwutil_cpp": 0.12307866681783382, + "gpu_bwutil_python": 0.12196570621513039, + "gpu_gap_stddev_us": 3.052759739553311 + } + } + } + }, + "adjusthue_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "20x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AdjustHue RGBf32 interleaved + planar, Tensor (~1-2ms)." + }, + "baselines": { + "adjusthue_float3_basic[InOutDataType=float3][shape=20x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 794.3604272447799, + "gpu_time_us_python": 800.99241731116, + "gpu_noise_us_cpp": 2.89, + "gpu_noise_us_python": 3.558, + "gpu_bwutil_cpp": 0.8056806654152282, + "gpu_bwutil_python": 0.7990101258950064, + "gpu_gap_stddev_us": 1.1642671983281017 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 687.5765224599202, + "gpu_time_us_python": 697.1263287353601, + "gpu_noise_us_cpp": 0.7619999999999999, + "gpu_noise_us_python": 2.542, + "gpu_bwutil_cpp": 0.7099796982287218, + "gpu_bwutil_python": 0.7002513055878827, + "gpu_gap_stddev_us": 2.0835171074829897 + } + }, + "adjusthue_float3_basic[InOutDataType=float3][shape=20x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 803.38804095852, + "gpu_time_us_python": 813.64193819384, + "gpu_noise_us_cpp": 1.128, + "gpu_noise_us_python": 3.6759999999999997, + "gpu_bwutil_cpp": 0.7966275487764649, + "gpu_bwutil_python": 0.7865909655111734, + "gpu_gap_stddev_us": 1.822212593599091 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 745.68635445282, + "gpu_time_us_python": 755.72180114454, + "gpu_noise_us_cpp": 0.858, + "gpu_noise_us_python": 2.998, + "gpu_bwutil_cpp": 0.6546726239628257, + "gpu_bwutil_python": 0.6460069002252785, + "gpu_gap_stddev_us": 2.0566918344013314 + } + } + } + }, + "adjusthue_float3_varshape_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "20x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AdjustHue RGBf32 interleaved + planar, VarShape (~1-2ms)." + }, + "baselines": { + "adjusthue_float3_varshape_advanced[InOutDataType=float3][shape=20x1080x1920][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1072.3372662772201, + "gpu_time_us_python": 1082.81634142114, + "gpu_noise_us_cpp": 2.4699999999999998, + "gpu_noise_us_python": 3.16, + "gpu_bwutil_cpp": 0.5968299715336707, + "gpu_bwutil_python": 0.5910577472995829, + "gpu_gap_stddev_us": 2.8743193078656444 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 986.03552282204, + "gpu_time_us_python": 999.3843823356401, + "gpu_noise_us_cpp": 1.734, + "gpu_noise_us_python": 3.9840000000000004, + "gpu_bwutil_cpp": 0.49509857566747184, + "gpu_bwutil_python": 0.4885005627628624, + "gpu_gap_stddev_us": 2.713105211338284 + } + }, + "adjusthue_float3_varshape_advanced[InOutDataType=float3][shape=20x1080x1920][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1024.76260304364, + "gpu_time_us_python": 1036.56388927748, + "gpu_noise_us_cpp": 2.2, + "gpu_noise_us_python": 2.3600000000000003, + "gpu_bwutil_cpp": 0.6245370121798278, + "gpu_bwutil_python": 0.6174298675516756, + "gpu_gap_stddev_us": 1.7954412891113114 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 948.66960110728, + "gpu_time_us_python": 962.2190264686, + "gpu_noise_us_cpp": 1.594, + "gpu_noise_us_python": 4.836, + "gpu_bwutil_cpp": 0.5146021448857793, + "gpu_bwutil_python": 0.5073481737464404, + "gpu_gap_stddev_us": 2.3818347291784625 + } + } + } + }, + "adjusthue_layout_compare_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NCHW", + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AdjustHue RGB8 native-planar vs fake-planar comparison (tensor-only, same shape as the uchar3 config)." + }, + "baselines": { + "adjusthue_layout_compare_uchar3_advanced[InOutDataType=uchar3][shape=32x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1175.82065443896, + "gpu_time_us_python": 1186.8111296735801, + "gpu_noise_us_cpp": 1.5859999999999999, + "gpu_noise_us_python": 4.630000000000001, + "gpu_bwutil_cpp": 0.21772145511999827, + "gpu_bwutil_python": 0.2157063502636288, + "gpu_gap_stddev_us": 2.6754035884748357 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1103.89563217156, + "gpu_time_us_python": 1113.3224950465399, + "gpu_noise_us_cpp": 1.124, + "gpu_noise_us_python": 3.9739999999999993, + "gpu_bwutil_cpp": 0.1768972381166251, + "gpu_bwutil_python": 0.17539884771386977, + "gpu_gap_stddev_us": 0.5465228937342621 + } + }, + "adjusthue_layout_compare_uchar3_advanced[InOutDataType=uchar3][shape=32x1080x1920][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2617.5466602047004, + "gpu_time_us_python": 2636.03890397564, + "gpu_noise_us_cpp": 1.298, + "gpu_noise_us_python": 3.0780000000000003, + "gpu_bwutil_cpp": 0.2934054250697405, + "gpu_bwutil_python": 0.29134732713493233, + "gpu_gap_stddev_us": 2.440315864951194 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2370.96094028002, + "gpu_time_us_python": 2385.0713048254, + "gpu_noise_us_cpp": 1.42, + "gpu_noise_us_python": 2.678, + "gpu_bwutil_cpp": 0.24707048251431746, + "gpu_bwutil_python": 0.24560827539179342, + "gpu_gap_stddev_us": 1.8300060534914722 + } + } + } + }, + "adjusthue_layout_compare_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "20x1080x1920" + ], + "layout": [ + "NCHW", + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AdjustHue RGBf32 native-planar vs fake-planar comparison (tensor-only, same shape as the float3 config)." + }, + "baselines": { + "adjusthue_layout_compare_float3_advanced[InOutDataType=float3][shape=20x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 791.47923972086, + "gpu_time_us_python": 801.6901907829999, + "gpu_noise_us_cpp": 2.866, + "gpu_noise_us_python": 4.54, + "gpu_bwutil_cpp": 0.8086133022877455, + "gpu_bwutil_python": 0.7983162693558642, + "gpu_gap_stddev_us": 1.5882475285099402 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 687.79676340266, + "gpu_time_us_python": 698.72729443882, + "gpu_noise_us_cpp": 1.0859999999999999, + "gpu_noise_us_python": 2.902, + "gpu_bwutil_cpp": 0.7097509636092787, + "gpu_bwutil_python": 0.6986467628098051, + "gpu_gap_stddev_us": 1.9554454599984197 + } + }, + "adjusthue_layout_compare_float3_advanced[InOutDataType=float3][shape=20x1080x1920][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2482.1415910626, + "gpu_time_us_python": 2499.55352227992, + "gpu_noise_us_cpp": 1.7399999999999998, + "gpu_noise_us_python": 4.180000000000001, + "gpu_bwutil_cpp": 0.773525773815995, + "gpu_bwutil_python": 0.7681373354466918, + "gpu_gap_stddev_us": 1.6681216114258743 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2114.80887136196, + "gpu_time_us_python": 2130.1742380086203, + "gpu_noise_us_cpp": 1.64, + "gpu_noise_us_python": 3.4879999999999995, + "gpu_bwutil_cpp": 0.6924621040481175, + "gpu_bwutil_python": 0.6874699004257177, + "gpu_gap_stddev_us": 1.3551047250558976 + } + } + } + } + } +} diff --git a/bench/config/operators/adjustsaturation.json b/bench/config/operators/adjustsaturation.json new file mode 100644 index 000000000..e04b72ff2 --- /dev/null +++ b/bench/config/operators/adjustsaturation.json @@ -0,0 +1,390 @@ +{ + "benchmark": "adjustsaturation", + "configs": { + "adjustsaturation_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AdjustSaturation RGB8 interleaved + planar, Tensor + VarShape (~1-2ms)." + }, + "baselines": { + "adjustsaturation_uchar3_basic[InOutDataType=uchar3][shape=32x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 509.03291153078, + "gpu_time_us_python": 520.9842254947, + "gpu_noise_us_cpp": 0.724, + "gpu_noise_us_python": 2.7960000000000003, + "gpu_bwutil_cpp": 0.5029154855570261, + "gpu_bwutil_python": 0.49138747729283816, + "gpu_gap_stddev_us": 2.351019516917237 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 487.95741249590003, + "gpu_time_us_python": 498.4792208499601, + "gpu_noise_us_cpp": 0.8320000000000001, + "gpu_noise_us_python": 1.9959999999999998, + "gpu_bwutil_cpp": 0.40016135981655954, + "gpu_bwutil_python": 0.3917358156103675, + "gpu_gap_stddev_us": 2.9049663734893043 + } + }, + "adjustsaturation_uchar3_basic[InOutDataType=uchar3][shape=32x1080x1920][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 669.6134006785, + "gpu_time_us_python": 681.87915520404, + "gpu_noise_us_cpp": 3.6020000000000003, + "gpu_noise_us_python": 2.9299999999999997, + "gpu_bwutil_cpp": 0.3823144954655781, + "gpu_bwutil_python": 0.3754452254989112, + "gpu_gap_stddev_us": 1.9947108851003954 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 579.8508188879201, + "gpu_time_us_python": 592.94607118568, + "gpu_noise_us_cpp": 2.284, + "gpu_noise_us_python": 4.57, + "gpu_bwutil_cpp": 0.3367529998861206, + "gpu_bwutil_python": 0.32931358437000186, + "gpu_gap_stddev_us": 1.4822376332167355 + } + }, + "adjustsaturation_uchar3_basic[InOutDataType=uchar3][shape=32x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 504.5987084194, + "gpu_time_us_python": 514.6411516539799, + "gpu_noise_us_cpp": 0.8200000000000001, + "gpu_noise_us_python": 2.2640000000000002, + "gpu_bwutil_cpp": 0.5073346362670721, + "gpu_bwutil_python": 0.4974408104973892, + "gpu_gap_stddev_us": 1.8821699798850664 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 487.55315030516005, + "gpu_time_us_python": 497.08872770996004, + "gpu_noise_us_cpp": 0.674, + "gpu_noise_us_python": 2.598, + "gpu_bwutil_cpp": 0.4004898306199175, + "gpu_bwutil_python": 0.3928146871656084, + "gpu_gap_stddev_us": 1.038645745624255 + } + }, + "adjustsaturation_uchar3_basic[InOutDataType=uchar3][shape=32x1080x1920][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 563.01878587918, + "gpu_time_us_python": 575.45277618364, + "gpu_noise_us_cpp": 3.668, + "gpu_noise_us_python": 3.31, + "gpu_bwutil_cpp": 0.45470344692041087, + "gpu_bwutil_python": 0.4448953691085856, + "gpu_gap_stddev_us": 2.8148452986965937 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 521.92366925916, + "gpu_time_us_python": 533.74360711622, + "gpu_noise_us_cpp": 2.918, + "gpu_noise_us_python": 2.896, + "gpu_bwutil_cpp": 0.37411388230628945, + "gpu_bwutil_python": 0.3658370971932086, + "gpu_gap_stddev_us": 1.4095331022468784 + } + } + } + }, + "adjustsaturation_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "20x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AdjustSaturation RGBf32 interleaved + planar, Tensor (~1-2ms)." + }, + "baselines": { + "adjustsaturation_float3_basic[InOutDataType=float3][shape=20x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 731.1684681745999, + "gpu_time_us_python": 740.27992798376, + "gpu_noise_us_cpp": 1.228, + "gpu_noise_us_python": 2.118, + "gpu_bwutil_cpp": 0.8753121829519713, + "gpu_bwutil_python": 0.8645406514564329, + "gpu_gap_stddev_us": 0.6709301063623521 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 568.17332028766, + "gpu_time_us_python": 578.0440984610201, + "gpu_noise_us_cpp": 1.346, + "gpu_noise_us_python": 2.092, + "gpu_bwutil_cpp": 0.8591319673992039, + "gpu_bwutil_python": 0.84446577840252, + "gpu_gap_stddev_us": 1.4466812937627298 + } + }, + "adjustsaturation_float3_basic[InOutDataType=float3][shape=20x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 721.4440124304999, + "gpu_time_us_python": 730.69536927274, + "gpu_noise_us_cpp": 1.1059999999999999, + "gpu_noise_us_python": 2.2039999999999997, + "gpu_bwutil_cpp": 0.8871103370445652, + "gpu_bwutil_python": 0.8758817657366995, + "gpu_gap_stddev_us": 1.2507066438762238 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 557.38129727726, + "gpu_time_us_python": 568.1365131918601, + "gpu_noise_us_cpp": 1.182, + "gpu_noise_us_python": 2.2920000000000003, + "gpu_bwutil_cpp": 0.8757666494196398, + "gpu_bwutil_python": 0.8591979545347772, + "gpu_gap_stddev_us": 2.3605468821128417 + } + } + } + }, + "adjustsaturation_float3_varshape_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "20x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AdjustSaturation RGBf32 interleaved + planar, VarShape (~1-2ms)." + }, + "baselines": { + "adjustsaturation_float3_varshape_advanced[InOutDataType=float3][shape=20x1080x1920][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 756.16454359384, + "gpu_time_us_python": 767.53380887542, + "gpu_noise_us_cpp": 2.242, + "gpu_noise_us_python": 3.272, + "gpu_bwutil_cpp": 0.8463800609694891, + "gpu_bwutil_python": 0.8338532318712917, + "gpu_gap_stddev_us": 2.2054720213930628 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 591.8377048458, + "gpu_time_us_python": 602.0373469089, + "gpu_noise_us_cpp": 2.9059999999999997, + "gpu_noise_us_python": 3.872, + "gpu_bwutil_cpp": 0.8247802683772049, + "gpu_bwutil_python": 0.8108105563682753, + "gpu_gap_stddev_us": 1.3623507446788763 + } + }, + "adjustsaturation_float3_varshape_advanced[InOutDataType=float3][shape=20x1080x1920][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 739.53627518538, + "gpu_time_us_python": 750.1552766614, + "gpu_noise_us_cpp": 1.932, + "gpu_noise_us_python": 2.5100000000000002, + "gpu_bwutil_cpp": 0.8654127643447046, + "gpu_bwutil_python": 0.8531631236488912, + "gpu_gap_stddev_us": 0.5275790722536648 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 570.7040669311599, + "gpu_time_us_python": 581.1475203104999, + "gpu_noise_us_cpp": 2.8, + "gpu_noise_us_python": 3.5159999999999996, + "gpu_bwutil_cpp": 0.8553220356524083, + "gpu_bwutil_python": 0.8399534944725044, + "gpu_gap_stddev_us": 1.4168917687989682 + } + } + } + }, + "adjustsaturation_layout_compare_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NCHW", + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AdjustSaturation RGB8 native-planar vs fake-planar comparison (tensor-only, same shape as the uchar3 config)." + }, + "baselines": { + "adjustsaturation_layout_compare_uchar3_advanced[InOutDataType=uchar3][shape=32x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 510.57529105938, + "gpu_time_us_python": 519.80419131834, + "gpu_noise_us_cpp": 2.144, + "gpu_noise_us_python": 2.4300000000000006, + "gpu_bwutil_cpp": 0.5013956392574503, + "gpu_bwutil_python": 0.49249466179681106, + "gpu_gap_stddev_us": 0.8608566024515717 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 487.92604105734, + "gpu_time_us_python": 496.89329556865994, + "gpu_noise_us_cpp": 0.9299999999999999, + "gpu_noise_us_python": 3.278, + "gpu_bwutil_cpp": 0.4001833784560206, + "gpu_bwutil_python": 0.39296049347784096, + "gpu_gap_stddev_us": 0.3351129051672054 + } + }, + "adjustsaturation_layout_compare_uchar3_advanced[InOutDataType=uchar3][shape=32x1080x1920][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1846.4210780227397, + "gpu_time_us_python": 1863.29670794036, + "gpu_noise_us_cpp": 2.262, + "gpu_noise_us_python": 2.754, + "gpu_bwutil_cpp": 0.41594042824815425, + "gpu_bwutil_python": 0.41217345686662715, + "gpu_gap_stddev_us": 2.110721815863949 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1647.3666790816999, + "gpu_time_us_python": 1662.04859845996, + "gpu_noise_us_cpp": 0.9400000000000001, + "gpu_noise_us_python": 2.9019999999999997, + "gpu_bwutil_cpp": 0.35557682923895706, + "gpu_bwutil_python": 0.3524368047484333, + "gpu_gap_stddev_us": 1.902369088195654 + } + } + } + }, + "adjustsaturation_layout_compare_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "20x1080x1920" + ], + "layout": [ + "NCHW", + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AdjustSaturation RGBf32 native-planar vs fake-planar comparison (tensor-only, same shape as the float3 config)." + }, + "baselines": { + "adjustsaturation_layout_compare_float3_advanced[InOutDataType=float3][shape=20x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 731.3819588916, + "gpu_time_us_python": 740.206110031, + "gpu_noise_us_cpp": 1.544, + "gpu_noise_us_python": 1.716, + "gpu_bwutil_cpp": 0.8750561829270393, + "gpu_bwutil_python": 0.8646261593911794, + "gpu_gap_stddev_us": 0.8789569433589394 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 567.8850316136001, + "gpu_time_us_python": 576.80596520524, + "gpu_noise_us_cpp": 1.086, + "gpu_noise_us_python": 2.206, + "gpu_bwutil_cpp": 0.8595678253833692, + "gpu_bwutil_python": 0.8462779262161556, + "gpu_gap_stddev_us": 1.3856998513133134 + } + }, + "adjustsaturation_layout_compare_float3_advanced[InOutDataType=float3][shape=20x1080x1920][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2398.88667606168, + "gpu_time_us_python": 2416.06304874782, + "gpu_noise_us_cpp": 1.47, + "gpu_noise_us_python": 3.65, + "gpu_bwutil_cpp": 0.8003713164675762, + "gpu_bwutil_python": 0.7946813948960398, + "gpu_gap_stddev_us": 0.9206274464126979 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1928.0787682532598, + "gpu_time_us_python": 1943.7457851660802, + "gpu_noise_us_cpp": 1.984, + "gpu_noise_us_python": 3.9, + "gpu_bwutil_cpp": 0.759516116315643, + "gpu_bwutil_python": 0.7533948417519141, + "gpu_gap_stddev_us": 1.475543134314326 + } + } + } + } + } +} diff --git a/bench/config/operators/adjustsharpness.json b/bench/config/operators/adjustsharpness.json new file mode 100644 index 000000000..d503b6d6b --- /dev/null +++ b/bench/config/operators/adjustsharpness.json @@ -0,0 +1,773 @@ +{ + "benchmark": "adjustsharpness", + "configs": { + "adjustsharpness_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AdjustSharpness RGB8 interleaved + planar, Tensor + VarShape (~1-2ms)." + }, + "baselines": { + "adjustsharpness_uchar3_basic[InOutDataType=uchar3][shape=32x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1337.0336590766997, + "gpu_time_us_python": 1344.07526805468, + "gpu_noise_us_cpp": 1.6199999999999999, + "gpu_noise_us_python": 3.85, + "gpu_bwutil_cpp": 0.1914703296388828, + "gpu_bwutil_python": 0.190467924100389, + "gpu_gap_stddev_us": 1.769007006190916 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1005.72364343498, + "gpu_time_us_python": 1012.3963926791399, + "gpu_noise_us_cpp": 0.6779999999999999, + "gpu_noise_us_python": 1.746, + "gpu_bwutil_cpp": 0.19415660019863434, + "gpu_bwutil_python": 0.19287725034054262, + "gpu_gap_stddev_us": 3.373016858903996 + } + }, + "adjustsharpness_uchar3_basic[InOutDataType=uchar3][shape=32x1080x1920][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1437.18476675532, + "gpu_time_us_python": 1449.7802013205599, + "gpu_noise_us_cpp": 6.066000000000001, + "gpu_noise_us_python": 2.808, + "gpu_bwutil_cpp": 0.17813002031530362, + "gpu_bwutil_python": 0.1765812095806168, + "gpu_gap_stddev_us": 3.077075612783932 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1179.8533963894, + "gpu_time_us_python": 1190.69666189162, + "gpu_noise_us_cpp": 1.8079999999999998, + "gpu_noise_us_python": 2.782, + "gpu_bwutil_cpp": 0.16551869621745888, + "gpu_bwutil_python": 0.16401196150730718, + "gpu_gap_stddev_us": 1.2573613280587765 + } + }, + "adjustsharpness_uchar3_basic[InOutDataType=uchar3][shape=32x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1213.02631724156, + "gpu_time_us_python": 1221.98615629064, + "gpu_noise_us_cpp": 1.508, + "gpu_noise_us_python": 3.095999999999999, + "gpu_bwutil_cpp": 0.21104700793826323, + "gpu_bwutil_python": 0.2094994487720912, + "gpu_gap_stddev_us": 1.1187352670657178 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 832.7817072314399, + "gpu_time_us_python": 839.6182967552801, + "gpu_noise_us_cpp": 0.7700000000000001, + "gpu_noise_us_python": 2.366, + "gpu_bwutil_cpp": 0.23450842739731778, + "gpu_bwutil_python": 0.23260304944096663, + "gpu_gap_stddev_us": 3.553560954479585 + } + }, + "adjustsharpness_uchar3_basic[InOutDataType=uchar3][shape=32x1080x1920][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1220.9871486468999, + "gpu_time_us_python": 1232.77646977016, + "gpu_noise_us_cpp": 1.802, + "gpu_noise_us_python": 2.9539999999999997, + "gpu_bwutil_cpp": 0.2096721566081718, + "gpu_bwutil_python": 0.2076663920538518, + "gpu_gap_stddev_us": 0.7127997244029309 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 972.6087373895, + "gpu_time_us_python": 981.06503366926, + "gpu_noise_us_cpp": 1.508, + "gpu_noise_us_python": 3.0320000000000005, + "gpu_bwutil_cpp": 0.20079642043472257, + "gpu_bwutil_python": 0.19906894167857409, + "gpu_gap_stddev_us": 5.3897919400136685 + } + } + } + }, + "adjustsharpness_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "20x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AdjustSharpness RGBf32 interleaved + planar, Tensor (~1-2ms)." + }, + "baselines": { + "adjustsharpness_float3_basic[InOutDataType=float3][shape=20x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1018.7852708339999, + "gpu_time_us_python": 1027.94687706334, + "gpu_noise_us_cpp": 1.072, + "gpu_noise_us_python": 2.536, + "gpu_bwutil_cpp": 0.628200018059967, + "gpu_bwutil_python": 0.6226017644471022, + "gpu_gap_stddev_us": 1.0925965628793473 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 734.1043590819199, + "gpu_time_us_python": 742.4423900085, + "gpu_noise_us_cpp": 1.2440000000000002, + "gpu_noise_us_python": 1.456, + "gpu_bwutil_cpp": 0.664940747293062, + "gpu_bwutil_python": 0.6574733733905426, + "gpu_gap_stddev_us": 0.41998614219978425 + } + }, + "adjustsharpness_float3_basic[InOutDataType=float3][shape=20x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 829.97485304582, + "gpu_time_us_python": 839.92245072412, + "gpu_noise_us_cpp": 1.3599999999999999, + "gpu_noise_us_python": 3.3519999999999994, + "gpu_bwutil_cpp": 0.77110844279601, + "gpu_bwutil_python": 0.7619768925655901, + "gpu_gap_stddev_us": 1.101656910755969 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 702.3403176675399, + "gpu_time_us_python": 709.7863428999001, + "gpu_noise_us_cpp": 2.6399999999999997, + "gpu_noise_us_python": 2.694, + "gpu_bwutil_cpp": 0.6950189451875801, + "gpu_bwutil_python": 0.6877297068549614, + "gpu_gap_stddev_us": 1.6075258422095768 + } + } + } + }, + "adjustsharpness_float3_varshape_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "20x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AdjustSharpness RGBf32 interleaved + planar, VarShape (~1-2ms)." + }, + "baselines": { + "adjustsharpness_float3_varshape_advanced[InOutDataType=float3][shape=20x1080x1920][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1071.4194959110798, + "gpu_time_us_python": 1082.5196288012598, + "gpu_noise_us_cpp": 1.49, + "gpu_noise_us_python": 3.374, + "gpu_bwutil_cpp": 0.5973399347869565, + "gpu_bwutil_python": 0.5912182901829303, + "gpu_gap_stddev_us": 2.7932328251728817 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 767.23107508122, + "gpu_time_us_python": 777.6070256568, + "gpu_noise_us_cpp": 1.158, + "gpu_noise_us_python": 3.5599999999999996, + "gpu_bwutil_cpp": 0.6362367198307587, + "gpu_bwutil_python": 0.6277461094207405, + "gpu_gap_stddev_us": 1.143082054404134 + } + }, + "adjustsharpness_float3_varshape_advanced[InOutDataType=float3][shape=20x1080x1920][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 848.0062796103, + "gpu_time_us_python": 860.67987236594, + "gpu_noise_us_cpp": 1.754, + "gpu_noise_us_python": 3.5980000000000003, + "gpu_bwutil_cpp": 0.7547149997617538, + "gpu_bwutil_python": 0.7436034254945991, + "gpu_gap_stddev_us": 2.032119795060681 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 711.8621045058801, + "gpu_time_us_python": 722.3846247715799, + "gpu_noise_us_cpp": 3.158, + "gpu_noise_us_python": 4.138, + "gpu_bwutil_cpp": 0.6857174160309418, + "gpu_bwutil_python": 0.6757295800561296, + "gpu_gap_stddev_us": 0.7913520866973313 + } + } + } + }, + "adjustsharpness_fake_planar_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AdjustSharpness RGB8 fake-planar comparison (tensor-only, ~1-2ms)." + }, + "baselines": { + "adjustsharpness_fake_planar_uchar3_advanced[InOutDataType=uchar3][shape=32x1080x1920][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2554.36672983986, + "gpu_time_us_python": 2572.21743058782, + "gpu_noise_us_cpp": 1.3119999999999998, + "gpu_noise_us_python": 3.476, + "gpu_bwutil_cpp": 0.3006640765242933, + "gpu_bwutil_python": 0.29857794615576727, + "gpu_gap_stddev_us": 2.97478188009405 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1993.7437265449603, + "gpu_time_us_python": 2008.58065512768, + "gpu_noise_us_cpp": 0.8720000000000001, + "gpu_noise_us_python": 3.6160000000000005, + "gpu_bwutil_cpp": 0.2938307865609236, + "gpu_bwutil_python": 0.29165650892721956, + "gpu_gap_stddev_us": 2.5802064972447756 + } + } + } + }, + "adjustsharpness_uchar3_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AdjustSharpness uchar3 native planar NCHW Tensor (advanced; same-tier match for the NCHW_FAKE comparison)." + }, + "baselines": { + "adjustsharpness_uchar3_nchw_advanced[InOutDataType=uchar3][shape=32x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1335.53313181412, + "gpu_time_us_python": 1345.6812266109202, + "gpu_noise_us_cpp": 1.5, + "gpu_noise_us_python": 2.69, + "gpu_bwutil_cpp": 0.19168532872091415, + "gpu_bwutil_python": 0.1902394225116939, + "gpu_gap_stddev_us": 0.6365683436029094 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1004.3689484884399, + "gpu_time_us_python": 1012.9316387973998, + "gpu_noise_us_cpp": 1.14, + "gpu_noise_us_python": 2.228, + "gpu_bwutil_cpp": 0.1944213813527409, + "gpu_bwutil_python": 0.19277781079787232, + "gpu_gap_stddev_us": 0.6215468176501671 + } + } + } + }, + "adjustsharpness_fake_planar_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "20x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AdjustSharpness RGBf32 fake-planar comparison (tensor-only, ~1-2ms)." + }, + "baselines": { + "adjustsharpness_fake_planar_float3_advanced[InOutDataType=float3][shape=20x1080x1920][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2503.7455272673997, + "gpu_time_us_python": 2522.2307648658198, + "gpu_noise_us_cpp": 1.8239999999999998, + "gpu_noise_us_python": 2.9800000000000004, + "gpu_bwutil_cpp": 0.7668511565787693, + "gpu_bwutil_python": 0.761231323692318, + "gpu_gap_stddev_us": 1.8201923261749622 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2071.1621812552003, + "gpu_time_us_python": 2084.8440639702994, + "gpu_noise_us_cpp": 2.346, + "gpu_noise_us_python": 3.37, + "gpu_bwutil_cpp": 0.7070467780165577, + "gpu_bwutil_python": 0.7024063528488325, + "gpu_gap_stddev_us": 0.9264420633966678 + } + } + } + }, + "adjustsharpness_float3_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "20x1080x1920" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AdjustSharpness float3 native planar NCHW Tensor (advanced; same-tier match for the NCHW_FAKE comparison)." + }, + "baselines": { + "adjustsharpness_float3_nchw_advanced[InOutDataType=float3][shape=20x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1017.63205159966, + "gpu_time_us_python": 1027.43245917376, + "gpu_noise_us_cpp": 1.186, + "gpu_noise_us_python": 3.0, + "gpu_bwutil_cpp": 0.6289119197046666, + "gpu_bwutil_python": 0.6229134668468587, + "gpu_gap_stddev_us": 1.421956656684426 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 734.45063654624, + "gpu_time_us_python": 743.9039650150801, + "gpu_noise_us_cpp": 0.8539999999999999, + "gpu_noise_us_python": 2.886, + "gpu_bwutil_cpp": 0.6646272710676941, + "gpu_bwutil_python": 0.6561844884102785, + "gpu_gap_stddev_us": 1.3974457122404362 + } + } + } + }, + "adjustsharpness_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "24x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AdjustSharpness RGBA8 interleaved + planar, Tensor (uchar4 planar var-shape unsupported)." + }, + "baselines": { + "adjustsharpness_uchar4_advanced[InOutDataType=uchar4][shape=24x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1307.21268241352, + "gpu_time_us_python": 1315.3276660002202, + "gpu_noise_us_cpp": 2.538, + "gpu_noise_us_python": 2.7700000000000005, + "gpu_bwutil_cpp": 0.19583715018451026, + "gpu_bwutil_python": 0.19462894826093902, + "gpu_gap_stddev_us": 1.851199092232555 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 974.3106025346202, + "gpu_time_us_python": 983.91242962858, + "gpu_noise_us_cpp": 0.508, + "gpu_noise_us_python": 2.176, + "gpu_bwutil_cpp": 0.20041885547692226, + "gpu_bwutil_python": 0.19846301018924645, + "gpu_gap_stddev_us": 1.4957909053563836 + } + }, + "adjustsharpness_uchar4_advanced[InOutDataType=uchar4][shape=24x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1189.1785511502399, + "gpu_time_us_python": 1198.8818447632, + "gpu_noise_us_cpp": 1.862, + "gpu_noise_us_python": 3.3, + "gpu_bwutil_cpp": 0.21527681464668377, + "gpu_bwutil_python": 0.21353330118708258, + "gpu_gap_stddev_us": 2.0669957877060194 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 768.7257171343201, + "gpu_time_us_python": 777.5758779792601, + "gpu_noise_us_cpp": 1.4760000000000002, + "gpu_noise_us_python": 2.4259999999999997, + "gpu_bwutil_cpp": 0.2540659863923073, + "gpu_bwutil_python": 0.2511673474600644, + "gpu_gap_stddev_us": 0.6524559943163338 + } + } + } + }, + "adjustsharpness_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AdjustSharpness RGBAf32 interleaved + planar, Tensor + VarShape (~1-2ms)." + }, + "baselines": { + "adjustsharpness_float4_advanced[InOutDataType=float4][shape=16x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1078.17929837892, + "gpu_time_us_python": 1089.3837320286, + "gpu_noise_us_cpp": 0.8379999999999999, + "gpu_noise_us_python": 3.276, + "gpu_bwutil_cpp": 0.6331664240998788, + "gpu_bwutil_python": 0.6266547739390479, + "gpu_gap_stddev_us": 1.5217237489055784 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 779.72270698186, + "gpu_time_us_python": 787.6903104372, + "gpu_noise_us_cpp": 0.78, + "gpu_noise_us_python": 1.816, + "gpu_bwutil_cpp": 0.6677738191476362, + "gpu_bwutil_python": 0.6610192381096308, + "gpu_gap_stddev_us": 0.40426168189230954 + } + }, + "adjustsharpness_float4_advanced[InOutDataType=float4][shape=16x1080x1920][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1134.53548709726, + "gpu_time_us_python": 1138.8428981197799, + "gpu_noise_us_cpp": 1.5239999999999998, + "gpu_noise_us_python": 3.062, + "gpu_bwutil_cpp": 0.6017150415888681, + "gpu_bwutil_python": 0.5994400678968244, + "gpu_gap_stddev_us": 1.5247056916144468 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 806.641478798, + "gpu_time_us_python": 817.7374306998599, + "gpu_noise_us_cpp": 1.384, + "gpu_noise_us_python": 2.2380000000000004, + "gpu_bwutil_cpp": 0.6454911788985459, + "gpu_bwutil_python": 0.6367325090013829, + "gpu_gap_stddev_us": 1.1037961712725923 + } + }, + "adjustsharpness_float4_advanced[InOutDataType=float4][shape=16x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 787.7118041904599, + "gpu_time_us_python": 796.36616701142, + "gpu_noise_us_cpp": 1.384, + "gpu_noise_us_python": 2.934, + "gpu_bwutil_cpp": 0.8666462978494909, + "gpu_bwutil_python": 0.8572301042372936, + "gpu_gap_stddev_us": 0.8395481246085734 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 650.45284936054, + "gpu_time_us_python": 658.28904287696, + "gpu_noise_us_cpp": 1.9460000000000002, + "gpu_noise_us_python": 2.04, + "gpu_bwutil_cpp": 0.8004858065285367, + "gpu_bwutil_python": 0.7909583419902789, + "gpu_gap_stddev_us": 0.6089307848971767 + } + }, + "adjustsharpness_float4_advanced[InOutDataType=float4][shape=16x1080x1920][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 879.79644254348, + "gpu_time_us_python": 892.7800757961801, + "gpu_noise_us_cpp": 3.7059999999999995, + "gpu_noise_us_python": 6.093999999999999, + "gpu_bwutil_cpp": 0.7759394484259275, + "gpu_bwutil_python": 0.7646625531767659, + "gpu_gap_stddev_us": 2.2178503559139195 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 636.1614287406, + "gpu_time_us_python": 647.0193437228, + "gpu_noise_us_cpp": 2.716, + "gpu_noise_us_python": 3.438, + "gpu_bwutil_cpp": 0.8185217242157726, + "gpu_bwutil_python": 0.8047743953922681, + "gpu_gap_stddev_us": 0.8355348919595712 + } + } + } + }, + "adjustsharpness_fake_planar_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "24x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AdjustSharpness RGBA8 fake-planar comparison (tensor-only, ~1-2ms)." + }, + "baselines": { + "adjustsharpness_fake_planar_uchar4_advanced[InOutDataType=uchar4][shape=24x1080x1920][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2443.7432034501194, + "gpu_time_us_python": 2461.20205580014, + "gpu_noise_us_cpp": 1.108, + "gpu_noise_us_python": 3.62, + "gpu_bwutil_cpp": 0.3142727384894143, + "gpu_bwutil_python": 0.3120431683434747, + "gpu_gap_stddev_us": 2.0363474519142093 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1828.20093906618, + "gpu_time_us_python": 1842.5108534371798, + "gpu_noise_us_cpp": 4.122, + "gpu_noise_us_python": 3.6079999999999997, + "gpu_bwutil_cpp": 0.32043478332541914, + "gpu_bwutil_python": 0.31794992846004816, + "gpu_gap_stddev_us": 4.513766708127416 + } + } + } + }, + "adjustsharpness_fake_planar_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AdjustSharpness RGBAf32 fake-planar comparison (tensor-only, ~1-2ms)." + }, + "baselines": { + "adjustsharpness_fake_planar_float4_advanced[InOutDataType=float4][shape=16x1080x1920][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2898.5767293249996, + "gpu_time_us_python": 2916.40442083044, + "gpu_noise_us_cpp": 3.242, + "gpu_noise_us_python": 3.806, + "gpu_bwutil_cpp": 0.706553674745648, + "gpu_bwutil_python": 0.7022346918489991, + "gpu_gap_stddev_us": 1.1808583665098267 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2402.9538482448997, + "gpu_time_us_python": 2416.41328839152, + "gpu_noise_us_cpp": 4.002000000000001, + "gpu_noise_us_python": 5.997999999999999, + "gpu_bwutil_cpp": 0.6500482449672484, + "gpu_bwutil_python": 0.6464271762614098, + "gpu_gap_stddev_us": 1.3681799524260068 + } + } + } + }, + "adjustsharpness_uint16_advanced": { + "tier": "advanced", + "dtypes": [ + "uint16" + ], + "string_axes": { + "shape": [ + "96x1080x1920" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AdjustSharpness U16 1-channel interleaved, Tensor + VarShape (single-channel lives in advanced per RGB guideline R3)." + }, + "baselines": { + "adjustsharpness_uint16_advanced[InOutDataType=uint16][shape=96x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1578.04967007544, + "gpu_time_us_python": 1588.0633950184797, + "gpu_noise_us_cpp": 1.6620000000000001, + "gpu_noise_us_python": 3.7379999999999995, + "gpu_bwutil_cpp": 0.3244516940274769, + "gpu_bwutil_python": 0.32240640035665175, + "gpu_gap_stddev_us": 1.213703375616625 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1459.5529270168997, + "gpu_time_us_python": 1468.395755966, + "gpu_noise_us_cpp": 2.654, + "gpu_noise_us_python": 2.11, + "gpu_bwutil_cpp": 0.26758261078018597, + "gpu_bwutil_python": 0.26597246953855436, + "gpu_gap_stddev_us": 0.6929577968803543 + } + }, + "adjustsharpness_uint16_advanced[InOutDataType=uint16][shape=96x1080x1920][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1784.7605793610996, + "gpu_time_us_python": 1800.17395138366, + "gpu_noise_us_cpp": 2.3120000000000003, + "gpu_noise_us_python": 3.7659999999999996, + "gpu_bwutil_cpp": 0.2868742961443567, + "gpu_bwutil_python": 0.28441918404863153, + "gpu_gap_stddev_us": 4.499521174249873 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1547.7596167514798, + "gpu_time_us_python": 1559.42731007656, + "gpu_noise_us_cpp": 1.8960000000000001, + "gpu_noise_us_python": 4.058, + "gpu_bwutil_cpp": 0.2523254278670495, + "gpu_bwutil_python": 0.25043157328904836, + "gpu_gap_stddev_us": 3.403830188839944 + } + } + } + } + } +} diff --git a/bench/config/operators/advcvtcolor.json b/bench/config/operators/advcvtcolor.json new file mode 100644 index 000000000..d5ed7b990 --- /dev/null +++ b/bench/config/operators/advcvtcolor.json @@ -0,0 +1,2882 @@ +{ + "benchmark": "advcvtcolor", + "configs": { + "advcvtcolor_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "96x1080x1920" + ], + "code": [ + "BGR2YUV" + ], + "inputKind": [ + "Tensor" + ], + "layout": [ + "NHWC" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AdvCvtColor RGB8 (basic)." + }, + "baselines": { + "advcvtcolor_basic[InOutDataType=uchar3][shape=96x1080x1920][code=BGR2YUV][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1320.1462216125801, + "gpu_time_us_python": 1329.6856051466, + "gpu_noise_us_cpp": 0.8039999999999999, + "gpu_noise_us_python": 2.304, + "gpu_bwutil_cpp": 0.5817542309300484, + "gpu_bwutil_python": 0.5775806218670514, + "gpu_gap_stddev_us": 0.9254405596930119 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1171.9486589007001, + "gpu_time_us_python": 1182.75699272665, + "gpu_noise_us_cpp": 2.1125, + "gpu_noise_us_python": 2.755, + "gpu_bwutil_cpp": 0.4998259124586252, + "gpu_bwutil_python": 0.4952558688668045, + "gpu_gap_stddev_us": 1.5561550219507627 + } + } + } + }, + "advcvtcolor_planar_nchw_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "96x1080x1920" + ], + "code": [ + "BGR2YUV" + ], + "inputKind": [ + "Tensor" + ], + "layout": [ + "NCHW" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AdvCvtColor RGB8 (basic) (NCHW)." + }, + "baselines": { + "advcvtcolor_planar_nchw_basic[InOutDataType=uchar3][shape=96x1080x1920][code=BGR2YUV][inputKind=Tensor][layout=NCHW]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 958.3939744027799, + "gpu_time_us_python": 967.1715667348399, + "gpu_noise_us_cpp": 1.1360000000000001, + "gpu_noise_us_python": 2.318, + "gpu_bwutil_cpp": 0.8013407718912744, + "gpu_bwutil_python": 0.7940683653927557, + "gpu_gap_stddev_us": 1.1813329701138386 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 796.09866236125, + "gpu_time_us_python": 805.484656927225, + "gpu_noise_us_cpp": 1.5174999999999998, + "gpu_noise_us_python": 2.7575000000000003, + "gpu_bwutil_cpp": 0.7357922147638086, + "gpu_bwutil_python": 0.7272182423928181, + "gpu_gap_stddev_us": 0.6921956560696527 + } + } + } + }, + "advcvtcolor_fakeplanar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "96x1080x1920" + ], + "code": [ + "BGR2YUV" + ], + "inputKind": [ + "Tensor" + ], + "layout": [ + "NCHW_FAKE" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AdvCvtColor RGB8 (advanced) (NCHW_FAKE)." + }, + "baselines": { + "advcvtcolor_fakeplanar_nchw_advanced[InOutDataType=uchar3][shape=96x1080x1920][code=BGR2YUV][inputKind=Tensor][layout=NCHW_FAKE]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4863.80303823024, + "gpu_time_us_python": 4880.703084285399, + "gpu_noise_us_cpp": 2.2979999999999996, + "gpu_noise_us_python": 3.114, + "gpu_bwutil_cpp": 0.4737036248061236, + "gpu_bwutil_python": 0.47206323518923343, + "gpu_gap_stddev_us": 2.0712498208294123 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 3935.043002478725, + "gpu_time_us_python": 3949.9886306002004, + "gpu_noise_us_cpp": 1.3875, + "gpu_noise_us_python": 5.9624999999999995, + "gpu_bwutil_cpp": 0.44657895013364435, + "gpu_bwutil_python": 0.44488883747447583, + "gpu_gap_stddev_us": 1.92865068525741 + } + } + } + }, + "advcvtcolor_444_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "256x720x1280", + "128x1080x1920", + "32x2160x3840" + ], + "code": [ + "BGR2YUV", + "RGB2YUV", + "YUV2BGR", + "YUV2RGB" + ], + "inputKind": [ + "Tensor" + ], + "layout": [ + "NHWC" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AdvCvtColor RGB8 444 (advanced)." + }, + "baselines": { + "advcvtcolor_444_advanced[InOutDataType=uchar3][shape=128x1080x1920][code=BGR2YUV][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1757.2914401967598, + "gpu_time_us_python": 1767.5783085151002, + "gpu_noise_us_cpp": 0.8740000000000002, + "gpu_noise_us_python": 2.286, + "gpu_bwutil_cpp": 0.5827155260532804, + "gpu_bwutil_python": 0.5793240042390944, + "gpu_gap_stddev_us": 1.8266650831633815 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1560.0815943730502, + "gpu_time_us_python": 1572.85487444, + "gpu_noise_us_cpp": 1.6375, + "gpu_noise_us_python": 3.5575, + "gpu_bwutil_cpp": 0.5006330597811562, + "gpu_bwutil_python": 0.49656208371958765, + "gpu_gap_stddev_us": 3.416023244135903 + } + }, + "advcvtcolor_444_advanced[InOutDataType=uchar3][shape=256x720x1280][code=BGR2YUV][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1565.3452783822602, + "gpu_time_us_python": 1576.27988656358, + "gpu_noise_us_cpp": 1.036, + "gpu_noise_us_python": 3.028, + "gpu_bwutil_cpp": 0.581483957641965, + "gpu_bwutil_python": 0.5774499688608581, + "gpu_gap_stddev_us": 1.9196899324667456 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1391.640719228875, + "gpu_time_us_python": 1399.5793183437, + "gpu_noise_us_cpp": 1.425, + "gpu_noise_us_python": 4.96, + "gpu_bwutil_cpp": 0.4988694187048732, + "gpu_bwutil_python": 0.49604184957831604, + "gpu_gap_stddev_us": 4.103018833002451 + } + }, + "advcvtcolor_444_advanced[InOutDataType=uchar3][shape=32x2160x3840][code=BGR2YUV][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1764.03466714934, + "gpu_time_us_python": 1772.2602939643202, + "gpu_noise_us_cpp": 0.93, + "gpu_noise_us_python": 2.148, + "gpu_bwutil_cpp": 0.5804879666325334, + "gpu_bwutil_python": 0.57779377393918, + "gpu_gap_stddev_us": 1.1531700853559326 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1560.0739570034998, + "gpu_time_us_python": 1572.97746352265, + "gpu_noise_us_cpp": 3.625, + "gpu_noise_us_python": 4.4425, + "gpu_bwutil_cpp": 0.5006357096775066, + "gpu_bwutil_python": 0.4965284822318854, + "gpu_gap_stddev_us": 6.641402214070155 + } + }, + "advcvtcolor_444_advanced[InOutDataType=uchar3][shape=128x1080x1920][code=RGB2YUV][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1757.4776225989997, + "gpu_time_us_python": 1769.0047072692, + "gpu_noise_us_cpp": 0.8880000000000001, + "gpu_noise_us_python": 2.6100000000000003, + "gpu_bwutil_cpp": 0.5826537866106046, + "gpu_bwutil_python": 0.5788569925190528, + "gpu_gap_stddev_us": 2.457561697914947 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1558.8256496835002, + "gpu_time_us_python": 1572.0861292262248, + "gpu_noise_us_cpp": 1.2575, + "gpu_noise_us_python": 2.9225000000000003, + "gpu_bwutil_cpp": 0.5010362058659147, + "gpu_bwutil_python": 0.49680617659656656, + "gpu_gap_stddev_us": 2.3695054682460737 + } + }, + "advcvtcolor_444_advanced[InOutDataType=uchar3][shape=256x720x1280][code=RGB2YUV][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1567.85537928338, + "gpu_time_us_python": 1578.6009556092, + "gpu_noise_us_cpp": 1.7479999999999998, + "gpu_noise_us_python": 1.984, + "gpu_bwutil_cpp": 0.5805530736523267, + "gpu_bwutil_python": 0.576600964718682, + "gpu_gap_stddev_us": 1.9673704172147988 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1392.3697187611751, + "gpu_time_us_python": 1404.950253941775, + "gpu_noise_us_cpp": 0.9849999999999999, + "gpu_noise_us_python": 4.9624999999999995, + "gpu_bwutil_cpp": 0.49860817836372096, + "gpu_bwutil_python": 0.4941426185446318, + "gpu_gap_stddev_us": 2.8172490991307857 + } + }, + "advcvtcolor_444_advanced[InOutDataType=uchar3][shape=32x2160x3840][code=RGB2YUV][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1763.5342114407601, + "gpu_time_us_python": 1773.3910525669803, + "gpu_noise_us_cpp": 1.286, + "gpu_noise_us_python": 3.09, + "gpu_bwutil_cpp": 0.5806527546714234, + "gpu_bwutil_python": 0.5774253766623118, + "gpu_gap_stddev_us": 2.0696602041149137 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1561.6398437799, + "gpu_time_us_python": 1573.5333987259, + "gpu_noise_us_cpp": 1.3650000000000002, + "gpu_noise_us_python": 3.2725, + "gpu_bwutil_cpp": 0.5001339470243689, + "gpu_bwutil_python": 0.49635327410852775, + "gpu_gap_stddev_us": 3.434443889566999 + } + }, + "advcvtcolor_444_advanced[InOutDataType=uchar3][shape=128x1080x1920][code=YUV2BGR][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1794.0596968786601, + "gpu_time_us_python": 1800.5017356171, + "gpu_noise_us_cpp": 2.5740000000000003, + "gpu_noise_us_python": 2.464, + "gpu_bwutil_cpp": 0.5707735935950922, + "gpu_bwutil_python": 0.568730762771632, + "gpu_gap_stddev_us": 2.664883172040593 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1558.9608450514002, + "gpu_time_us_python": 1574.17983150445, + "gpu_noise_us_cpp": 1.1175, + "gpu_noise_us_python": 4.2475000000000005, + "gpu_bwutil_cpp": 0.5009935506527132, + "gpu_bwutil_python": 0.4961456561790492, + "gpu_gap_stddev_us": 3.9899241135282697 + } + }, + "advcvtcolor_444_advanced[InOutDataType=uchar3][shape=256x720x1280][code=YUV2BGR][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1600.09586135872, + "gpu_time_us_python": 1607.5815295561401, + "gpu_noise_us_cpp": 2.05, + "gpu_noise_us_python": 3.5199999999999996, + "gpu_bwutil_cpp": 0.5688560807566972, + "gpu_bwutil_python": 0.5662069666284862, + "gpu_gap_stddev_us": 1.2314610215410595 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1391.83016898915, + "gpu_time_us_python": 1404.607166292475, + "gpu_noise_us_cpp": 1.1975, + "gpu_noise_us_python": 4.27, + "gpu_bwutil_cpp": 0.4988019689353566, + "gpu_bwutil_python": 0.4942614254869324, + "gpu_gap_stddev_us": 2.961186498644649 + } + }, + "advcvtcolor_444_advanced[InOutDataType=uchar3][shape=32x2160x3840][code=YUV2BGR][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1790.27530619072, + "gpu_time_us_python": 1798.05682323814, + "gpu_noise_us_cpp": 1.9060000000000001, + "gpu_noise_us_python": 2.21, + "gpu_bwutil_cpp": 0.5719800211649045, + "gpu_bwutil_python": 0.5695043304253742, + "gpu_gap_stddev_us": 1.4901298219668764 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1562.02059762985, + "gpu_time_us_python": 1573.91445796065, + "gpu_noise_us_cpp": 1.4475, + "gpu_noise_us_python": 2.9799999999999995, + "gpu_bwutil_cpp": 0.5000115871941496, + "gpu_bwutil_python": 0.4962302305126186, + "gpu_gap_stddev_us": 2.6551830852060947 + } + }, + "advcvtcolor_444_advanced[InOutDataType=uchar3][shape=128x1080x1920][code=YUV2RGB][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1791.90491574148, + "gpu_time_us_python": 1802.05732930962, + "gpu_noise_us_cpp": 2.0879999999999996, + "gpu_noise_us_python": 3.572, + "gpu_bwutil_cpp": 0.5714599791166941, + "gpu_bwutil_python": 0.5682402927420376, + "gpu_gap_stddev_us": 2.623855517003036 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1560.421932604725, + "gpu_time_us_python": 1574.1286396557998, + "gpu_noise_us_cpp": 1.21, + "gpu_noise_us_python": 5.612499999999999, + "gpu_bwutil_cpp": 0.5005239732059583, + "gpu_bwutil_python": 0.4961618706428631, + "gpu_gap_stddev_us": 2.9661808020101383 + } + }, + "advcvtcolor_444_advanced[InOutDataType=uchar3][shape=256x720x1280][code=YUV2RGB][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1600.8292045241, + "gpu_time_us_python": 1607.79420397216, + "gpu_noise_us_cpp": 2.122, + "gpu_noise_us_python": 2.8440000000000003, + "gpu_bwutil_cpp": 0.5685953194015673, + "gpu_bwutil_python": 0.5661316758564163, + "gpu_gap_stddev_us": 1.7034385113582422 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1392.476443851625, + "gpu_time_us_python": 1403.8638647140751, + "gpu_noise_us_cpp": 1.6125, + "gpu_noise_us_python": 4.329999999999999, + "gpu_bwutil_cpp": 0.4985702219807441, + "gpu_bwutil_python": 0.49452123757875793, + "gpu_gap_stddev_us": 3.5030266124291374 + } + }, + "advcvtcolor_444_advanced[InOutDataType=uchar3][shape=32x2160x3840][code=YUV2RGB][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1789.39028297148, + "gpu_time_us_python": 1799.1453201159998, + "gpu_noise_us_cpp": 1.688, + "gpu_noise_us_python": 2.976, + "gpu_bwutil_cpp": 0.5722627942953097, + "gpu_bwutil_python": 0.5691600848138623, + "gpu_gap_stddev_us": 1.75758219617745 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1561.4345919846498, + "gpu_time_us_python": 1573.4517392415, + "gpu_noise_us_cpp": 0.885, + "gpu_noise_us_python": 2.7925, + "gpu_bwutil_cpp": 0.5001993959549917, + "gpu_bwutil_python": 0.496379677365392, + "gpu_gap_stddev_us": 2.7680857717688676 + } + } + } + }, + "advcvtcolor_planar_nchw_444_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "256x720x1280", + "128x1080x1920", + "32x2160x3840" + ], + "code": [ + "BGR2YUV", + "RGB2YUV", + "YUV2BGR", + "YUV2RGB" + ], + "inputKind": [ + "Tensor" + ], + "layout": [ + "NCHW" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AdvCvtColor RGB8 444 (advanced) (NCHW)." + }, + "baselines": { + "advcvtcolor_planar_nchw_444_advanced[InOutDataType=uchar3][shape=128x1080x1920][code=BGR2YUV][inputKind=Tensor][layout=NCHW]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1271.49406786494, + "gpu_time_us_python": 1283.85830741722, + "gpu_noise_us_cpp": 1.184, + "gpu_noise_us_python": 2.106, + "gpu_bwutil_cpp": 0.8053518681733511, + "gpu_bwutil_python": 0.7975968778744746, + "gpu_gap_stddev_us": 1.6222712965475639 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1057.651409494175, + "gpu_time_us_python": 1065.7880717152, + "gpu_noise_us_cpp": 2.16, + "gpu_noise_us_python": 3.2399999999999998, + "gpu_bwutil_cpp": 0.7384451705340734, + "gpu_bwutil_python": 0.732810818807661, + "gpu_gap_stddev_us": 2.455458906103595 + } + }, + "advcvtcolor_planar_nchw_444_advanced[InOutDataType=uchar3][shape=128x1080x1920][code=RGB2YUV][inputKind=Tensor][layout=NCHW]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1271.6718390508, + "gpu_time_us_python": 1284.29134967994, + "gpu_noise_us_cpp": 2.074, + "gpu_noise_us_python": 2.452, + "gpu_bwutil_cpp": 0.8052393858496913, + "gpu_bwutil_python": 0.7973272231547093, + "gpu_gap_stddev_us": 1.1734143075457493 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1057.9758302823002, + "gpu_time_us_python": 1066.1957459246248, + "gpu_noise_us_cpp": 2.5, + "gpu_noise_us_python": 4.2975, + "gpu_bwutil_cpp": 0.7382186015744296, + "gpu_bwutil_python": 0.7325275115850505, + "gpu_gap_stddev_us": 1.1858686859357237 + } + }, + "advcvtcolor_planar_nchw_444_advanced[InOutDataType=uchar3][shape=128x1080x1920][code=YUV2BGR][inputKind=Tensor][layout=NCHW]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1280.7324498283201, + "gpu_time_us_python": 1290.6549062925, + "gpu_noise_us_cpp": 1.8739999999999999, + "gpu_noise_us_python": 5.26, + "gpu_bwutil_cpp": 0.7995425296785712, + "gpu_bwutil_python": 0.7933959904040052, + "gpu_gap_stddev_us": 1.1137523958520308 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1068.4557256870999, + "gpu_time_us_python": 1078.0581360914, + "gpu_noise_us_cpp": 1.6824999999999999, + "gpu_noise_us_python": 3.0925000000000002, + "gpu_bwutil_cpp": 0.7309806647711242, + "gpu_bwutil_python": 0.7244684732127067, + "gpu_gap_stddev_us": 0.7824707434795357 + } + }, + "advcvtcolor_planar_nchw_444_advanced[InOutDataType=uchar3][shape=128x1080x1920][code=YUV2RGB][inputKind=Tensor][layout=NCHW]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1280.86619815042, + "gpu_time_us_python": 1289.98244423222, + "gpu_noise_us_cpp": 1.536, + "gpu_noise_us_python": 2.216, + "gpu_bwutil_cpp": 0.7994591364307555, + "gpu_bwutil_python": 0.7938096388595268, + "gpu_gap_stddev_us": 0.872881927000783 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1066.960477828925, + "gpu_time_us_python": 1078.60997970735, + "gpu_noise_us_cpp": 1.42, + "gpu_noise_us_python": 2.5700000000000003, + "gpu_bwutil_cpp": 0.7320036567262282, + "gpu_bwutil_python": 0.7241004833110991, + "gpu_gap_stddev_us": 1.9287142201769247 + } + }, + "advcvtcolor_planar_nchw_444_advanced[InOutDataType=uchar3][shape=256x720x1280][code=BGR2YUV][inputKind=Tensor][layout=NCHW]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1139.4941410151, + "gpu_time_us_python": 1148.7138303047602, + "gpu_noise_us_cpp": 2.146, + "gpu_noise_us_python": 3.432, + "gpu_bwutil_cpp": 0.7987950469591081, + "gpu_bwutil_python": 0.7923843181394985, + "gpu_gap_stddev_us": 0.9314752303168522 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 932.1254684797749, + "gpu_time_us_python": 942.95555051415, + "gpu_noise_us_cpp": 1.5224999999999997, + "gpu_noise_us_python": 3.9225000000000003, + "gpu_bwutil_cpp": 0.7447902819228427, + "gpu_bwutil_python": 0.7362381140934204, + "gpu_gap_stddev_us": 1.298551922149159 + } + }, + "advcvtcolor_planar_nchw_444_advanced[InOutDataType=uchar3][shape=256x720x1280][code=RGB2YUV][inputKind=Tensor][layout=NCHW]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1139.7250088777798, + "gpu_time_us_python": 1148.4366922203, + "gpu_noise_us_cpp": 1.0859999999999999, + "gpu_noise_us_python": 1.908, + "gpu_bwutil_cpp": 0.7986333287266181, + "gpu_bwutil_python": 0.7925757109295601, + "gpu_gap_stddev_us": 1.1316262902558618 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 932.9374610878501, + "gpu_time_us_python": 942.2164052385999, + "gpu_noise_us_cpp": 1.3325, + "gpu_noise_us_python": 3.155, + "gpu_bwutil_cpp": 0.7441417188319526, + "gpu_bwutil_python": 0.7368142648750712, + "gpu_gap_stddev_us": 0.8109362786287482 + } + }, + "advcvtcolor_planar_nchw_444_advanced[InOutDataType=uchar3][shape=256x720x1280][code=YUV2BGR][inputKind=Tensor][layout=NCHW]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1144.9058927901601, + "gpu_time_us_python": 1154.5453414389399, + "gpu_noise_us_cpp": 1.158, + "gpu_noise_us_python": 1.7399999999999998, + "gpu_bwutil_cpp": 0.7950193525523866, + "gpu_bwutil_python": 0.7883820811889498, + "gpu_gap_stddev_us": 1.0256241427090802 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 945.6913162778749, + "gpu_time_us_python": 957.074495143125, + "gpu_noise_us_cpp": 1.1099999999999999, + "gpu_noise_us_python": 3.225, + "gpu_bwutil_cpp": 0.7341093053720056, + "gpu_bwutil_python": 0.725376231188057, + "gpu_gap_stddev_us": 0.9796749518589535 + } + }, + "advcvtcolor_planar_nchw_444_advanced[InOutDataType=uchar3][shape=256x720x1280][code=YUV2RGB][inputKind=Tensor][layout=NCHW]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1145.4408078432602, + "gpu_time_us_python": 1152.90565543334, + "gpu_noise_us_cpp": 1.342, + "gpu_noise_us_python": 2.382, + "gpu_bwutil_cpp": 0.7946481951940955, + "gpu_bwutil_python": 0.7895034638925237, + "gpu_gap_stddev_us": 1.080172955084645 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 947.413549779225, + "gpu_time_us_python": 958.7671614609, + "gpu_noise_us_cpp": 0.8300000000000001, + "gpu_noise_us_python": 2.5775, + "gpu_bwutil_cpp": 0.7327750170496171, + "gpu_bwutil_python": 0.7241013717609285, + "gpu_gap_stddev_us": 2.3487773120321243 + } + }, + "advcvtcolor_planar_nchw_444_advanced[InOutDataType=uchar3][shape=32x2160x3840][code=BGR2YUV][inputKind=Tensor][layout=NCHW]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1275.00583435674, + "gpu_time_us_python": 1281.1922081878602, + "gpu_noise_us_cpp": 1.682, + "gpu_noise_us_python": 2.382, + "gpu_bwutil_cpp": 0.8031336199204036, + "gpu_bwutil_python": 0.7992558208420956, + "gpu_gap_stddev_us": 0.872524081479873 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1057.4653827691, + "gpu_time_us_python": 1068.04925320055, + "gpu_noise_us_cpp": 1.405, + "gpu_noise_us_python": 3.315, + "gpu_bwutil_cpp": 0.7385759177637182, + "gpu_bwutil_python": 0.7312568971502311, + "gpu_gap_stddev_us": 1.4854759700639244 + } + }, + "advcvtcolor_planar_nchw_444_advanced[InOutDataType=uchar3][shape=32x2160x3840][code=RGB2YUV][inputKind=Tensor][layout=NCHW]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1273.7739127298998, + "gpu_time_us_python": 1282.1773723954, + "gpu_noise_us_cpp": 2.154, + "gpu_noise_us_python": 2.834, + "gpu_bwutil_cpp": 0.8039103905036272, + "gpu_bwutil_python": 0.7986416674403215, + "gpu_gap_stddev_us": 0.9816123290611665 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1054.1911272942, + "gpu_time_us_python": 1068.7794716390501, + "gpu_noise_us_cpp": 1.4775, + "gpu_noise_us_python": 2.9675, + "gpu_bwutil_cpp": 0.7408688471592201, + "gpu_bwutil_python": 0.7307595380550025, + "gpu_gap_stddev_us": 2.184288904110743 + } + }, + "advcvtcolor_planar_nchw_444_advanced[InOutDataType=uchar3][shape=32x2160x3840][code=YUV2BGR][inputKind=Tensor][layout=NCHW]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1279.57678461558, + "gpu_time_us_python": 1284.6851996886198, + "gpu_noise_us_cpp": 1.098, + "gpu_noise_us_python": 1.92, + "gpu_bwutil_cpp": 0.8002646875647207, + "gpu_bwutil_python": 0.7970826070338622, + "gpu_gap_stddev_us": 0.8356048335891968 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1070.73491506355, + "gpu_time_us_python": 1083.7475695702249, + "gpu_noise_us_cpp": 1.5349999999999997, + "gpu_noise_us_python": 2.8175, + "gpu_bwutil_cpp": 0.7294251920398821, + "gpu_bwutil_python": 0.7206694291183101, + "gpu_gap_stddev_us": 2.462498200844301 + } + }, + "advcvtcolor_planar_nchw_444_advanced[InOutDataType=uchar3][shape=32x2160x3840][code=YUV2RGB][inputKind=Tensor][layout=NCHW]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1278.3490240573399, + "gpu_time_us_python": 1285.58842732352, + "gpu_noise_us_cpp": 1.3699999999999999, + "gpu_noise_us_python": 1.9620000000000002, + "gpu_bwutil_cpp": 0.8010332960764804, + "gpu_bwutil_python": 0.7965225721428728, + "gpu_gap_stddev_us": 0.8046388089570339 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1068.5235568801, + "gpu_time_us_python": 1082.34364587485, + "gpu_noise_us_cpp": 0.9075, + "gpu_noise_us_python": 3.1475, + "gpu_bwutil_cpp": 0.7309343327363551, + "gpu_bwutil_python": 0.7216008428028328, + "gpu_gap_stddev_us": 0.38433376243534806 + } + } + } + }, + "advcvtcolor_fakeplanar_nchw_444_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "256x720x1280", + "128x1080x1920", + "32x2160x3840" + ], + "code": [ + "BGR2YUV", + "RGB2YUV", + "YUV2BGR", + "YUV2RGB" + ], + "inputKind": [ + "Tensor" + ], + "layout": [ + "NCHW_FAKE" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AdvCvtColor RGB8 444 (advanced) (NCHW_FAKE)." + }, + "baselines": { + "advcvtcolor_fakeplanar_nchw_444_advanced[InOutDataType=uchar3][shape=128x1080x1920][code=BGR2YUV][inputKind=Tensor][layout=NCHW_FAKE]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6473.92233824112, + "gpu_time_us_python": 6491.97091078144, + "gpu_noise_us_cpp": 2.66, + "gpu_noise_us_python": 2.6060000000000003, + "gpu_bwutil_cpp": 0.47451927596033877, + "gpu_bwutil_python": 0.473200030328096, + "gpu_gap_stddev_us": 3.411936386024307 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 5237.467993633625, + "gpu_time_us_python": 5251.9968251387, + "gpu_noise_us_cpp": 2.7299999999999995, + "gpu_noise_us_python": 7.955, + "gpu_bwutil_cpp": 0.4473686216662595, + "gpu_bwutil_python": 0.4461290645978014, + "gpu_gap_stddev_us": 4.280193611779178 + } + }, + "advcvtcolor_fakeplanar_nchw_444_advanced[InOutDataType=uchar3][shape=128x1080x1920][code=RGB2YUV][inputKind=Tensor][layout=NCHW_FAKE]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6473.69654239746, + "gpu_time_us_python": 6490.5425634139, + "gpu_noise_us_cpp": 2.992, + "gpu_noise_us_python": 4.95, + "gpu_bwutil_cpp": 0.47453582438936015, + "gpu_bwutil_python": 0.473304172536829, + "gpu_gap_stddev_us": 4.3936236337879775 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 5235.399683874025, + "gpu_time_us_python": 5250.207992891401, + "gpu_noise_us_cpp": 2.1625, + "gpu_noise_us_python": 4.2924999999999995, + "gpu_bwutil_cpp": 0.4475451158633282, + "gpu_bwutil_python": 0.446282676845715, + "gpu_gap_stddev_us": 0.6258753920167032 + } + }, + "advcvtcolor_fakeplanar_nchw_444_advanced[InOutDataType=uchar3][shape=128x1080x1920][code=YUV2BGR][inputKind=Tensor][layout=NCHW_FAKE]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6506.643065428079, + "gpu_time_us_python": 6523.8934443546805, + "gpu_noise_us_cpp": 4.936, + "gpu_noise_us_python": 5.151999999999999, + "gpu_bwutil_cpp": 0.47213301804415975, + "gpu_bwutil_python": 0.4708845824752661, + "gpu_gap_stddev_us": 1.3041011665913693 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 5237.40522046475, + "gpu_time_us_python": 5253.229004641325, + "gpu_noise_us_cpp": 1.8199999999999998, + "gpu_noise_us_python": 3.685, + "gpu_bwutil_cpp": 0.4473736670599229, + "gpu_bwutil_python": 0.4460253073811003, + "gpu_gap_stddev_us": 2.5196039088138007 + } + }, + "advcvtcolor_fakeplanar_nchw_444_advanced[InOutDataType=uchar3][shape=128x1080x1920][code=YUV2RGB][inputKind=Tensor][layout=NCHW_FAKE]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6506.454025170699, + "gpu_time_us_python": 6525.27456283564, + "gpu_noise_us_cpp": 4.032, + "gpu_noise_us_python": 4.232, + "gpu_bwutil_cpp": 0.47214672943832864, + "gpu_bwutil_python": 0.47078494852108915, + "gpu_gap_stddev_us": 1.4908198483775446 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 5235.720155089026, + "gpu_time_us_python": 5252.66599406795, + "gpu_noise_us_cpp": 3.9625000000000004, + "gpu_noise_us_python": 5.205, + "gpu_bwutil_cpp": 0.4475178883149185, + "gpu_bwutil_python": 0.44607208626570116, + "gpu_gap_stddev_us": 4.713716212963284 + } + }, + "advcvtcolor_fakeplanar_nchw_444_advanced[InOutDataType=uchar3][shape=256x720x1280][code=BGR2YUV][inputKind=Tensor][layout=NCHW_FAKE]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5757.53312327642, + "gpu_time_us_python": 5777.18923308628, + "gpu_noise_us_cpp": 4.0600000000000005, + "gpu_noise_us_python": 4.558, + "gpu_bwutil_cpp": 0.47427742176241594, + "gpu_bwutil_python": 0.4726636565401372, + "gpu_gap_stddev_us": 1.7385970469032261 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 4653.806952996651, + "gpu_time_us_python": 4668.404144269376, + "gpu_noise_us_cpp": 1.5575, + "gpu_noise_us_python": 4.015, + "gpu_bwutil_cpp": 0.4475337329429667, + "gpu_bwutil_python": 0.4461350206364024, + "gpu_gap_stddev_us": 2.1291985259138646 + } + }, + "advcvtcolor_fakeplanar_nchw_444_advanced[InOutDataType=uchar3][shape=256x720x1280][code=RGB2YUV][inputKind=Tensor][layout=NCHW_FAKE]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5757.4028080159405, + "gpu_time_us_python": 5777.51971591598, + "gpu_noise_us_cpp": 2.8840000000000003, + "gpu_noise_us_python": 5.006, + "gpu_bwutil_cpp": 0.47428803934181796, + "gpu_bwutil_python": 0.47263675030916347, + "gpu_gap_stddev_us": 5.0098906271867305 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 4653.408739181425, + "gpu_time_us_python": 4672.414814984324, + "gpu_noise_us_cpp": 2.4, + "gpu_noise_us_python": 9.3625, + "gpu_bwutil_cpp": 0.4475717778444535, + "gpu_bwutil_python": 0.4457512605016812, + "gpu_gap_stddev_us": 2.63773097466337 + } + }, + "advcvtcolor_fakeplanar_nchw_444_advanced[InOutDataType=uchar3][shape=256x720x1280][code=YUV2BGR][inputKind=Tensor][layout=NCHW_FAKE]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5789.06765417614, + "gpu_time_us_python": 5813.738472668321, + "gpu_noise_us_cpp": 3.904000000000001, + "gpu_noise_us_python": 9.610000000000001, + "gpu_bwutil_cpp": 0.4716939030445981, + "gpu_bwutil_python": 0.4696923438074586, + "gpu_gap_stddev_us": 5.660510886145657 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 4652.7443675882, + "gpu_time_us_python": 4670.2969030097, + "gpu_noise_us_cpp": 2.1825, + "gpu_noise_us_python": 11.922500000000001, + "gpu_bwutil_cpp": 0.44763548940878717, + "gpu_bwutil_python": 0.44595320174581754, + "gpu_gap_stddev_us": 4.130261405416488 + } + }, + "advcvtcolor_fakeplanar_nchw_444_advanced[InOutDataType=uchar3][shape=256x720x1280][code=YUV2RGB][inputKind=Tensor][layout=NCHW_FAKE]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5789.402812177421, + "gpu_time_us_python": 5805.74484955174, + "gpu_noise_us_cpp": 3.2599999999999993, + "gpu_noise_us_python": 4.578, + "gpu_bwutil_cpp": 0.47166654699841476, + "gpu_bwutil_python": 0.4703388426121922, + "gpu_gap_stddev_us": 4.092446740073975 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 4653.281331503775, + "gpu_time_us_python": 4666.662657702375, + "gpu_noise_us_cpp": 1.76, + "gpu_noise_us_python": 4.355, + "gpu_bwutil_cpp": 0.4475841623347896, + "gpu_bwutil_python": 0.4463006496444599, + "gpu_gap_stddev_us": 0.9750687012497591 + } + }, + "advcvtcolor_fakeplanar_nchw_444_advanced[InOutDataType=uchar3][shape=32x2160x3840][code=BGR2YUV][inputKind=Tensor][layout=NCHW_FAKE]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6464.59601231104, + "gpu_time_us_python": 6479.6042124429805, + "gpu_noise_us_cpp": 3.008, + "gpu_noise_us_python": 2.666, + "gpu_bwutil_cpp": 0.4752039191163509, + "gpu_bwutil_python": 0.47410334508077356, + "gpu_gap_stddev_us": 5.556934405978769 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 5236.0314065907005, + "gpu_time_us_python": 5249.924177924725, + "gpu_noise_us_cpp": 2.38, + "gpu_noise_us_python": 3.6950000000000003, + "gpu_bwutil_cpp": 0.4474914821006427, + "gpu_bwutil_python": 0.4463063028448502, + "gpu_gap_stddev_us": 2.272187202828961 + } + }, + "advcvtcolor_fakeplanar_nchw_444_advanced[InOutDataType=uchar3][shape=32x2160x3840][code=RGB2YUV][inputKind=Tensor][layout=NCHW_FAKE]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6463.14664987412, + "gpu_time_us_python": 6479.56222631984, + "gpu_noise_us_cpp": 2.7279999999999998, + "gpu_noise_us_python": 4.264, + "gpu_bwutil_cpp": 0.4753104322206754, + "gpu_bwutil_python": 0.47410628796219345, + "gpu_gap_stddev_us": 3.4458419467007033 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 5233.969714011, + "gpu_time_us_python": 5248.74632308875, + "gpu_noise_us_cpp": 3.0749999999999997, + "gpu_noise_us_python": 2.9124999999999996, + "gpu_bwutil_cpp": 0.44766743590658137, + "gpu_bwutil_python": 0.44640664786618195, + "gpu_gap_stddev_us": 1.3255982028904334 + } + }, + "advcvtcolor_fakeplanar_nchw_444_advanced[InOutDataType=uchar3][shape=32x2160x3840][code=YUV2BGR][inputKind=Tensor][layout=NCHW_FAKE]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6487.2027250436195, + "gpu_time_us_python": 6503.46079850804, + "gpu_noise_us_cpp": 4.702, + "gpu_noise_us_python": 2.586, + "gpu_bwutil_cpp": 0.4735479296708761, + "gpu_bwutil_python": 0.47236409225040044, + "gpu_gap_stddev_us": 2.1474463008591007 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 5235.8802914821745, + "gpu_time_us_python": 5248.960661391375, + "gpu_noise_us_cpp": 2.2425, + "gpu_noise_us_python": 4.2225, + "gpu_bwutil_cpp": 0.4475040107538602, + "gpu_bwutil_python": 0.4463891051310278, + "gpu_gap_stddev_us": 1.6452018944043771 + } + }, + "advcvtcolor_fakeplanar_nchw_444_advanced[InOutDataType=uchar3][shape=32x2160x3840][code=YUV2RGB][inputKind=Tensor][layout=NCHW_FAKE]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6488.65207280862, + "gpu_time_us_python": 6507.1629573137, + "gpu_noise_us_cpp": 3.284, + "gpu_noise_us_python": 4.8580000000000005, + "gpu_bwutil_cpp": 0.4734421176686932, + "gpu_bwutil_python": 0.47209528349711805, + "gpu_gap_stddev_us": 2.197643760009811 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 5234.9365015013245, + "gpu_time_us_python": 5251.1991610129, + "gpu_noise_us_cpp": 3.1125, + "gpu_noise_us_python": 5.194999999999999, + "gpu_bwutil_cpp": 0.44758507940573766, + "gpu_bwutil_python": 0.4461978822066186, + "gpu_gap_stddev_us": 2.305301323156714 + } + } + } + }, + "advcvtcolor_rgb_to_nv_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3", + "uchar4" + ], + "string_axes": { + "shape": [ + "128x720x1280", + "64x1080x1920", + "16x2160x3840" + ], + "code": [ + "RGB2YUV_NV12", + "BGR2YUV_NV21" + ], + "inputKind": [ + "Tensor" + ], + "layout": [ + "NHWC" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AdvCvtColor RGB/RGBA nv (advanced)." + }, + "baselines": { + "advcvtcolor_rgb_to_nv_advanced[InOutDataType=uchar3][shape=128x720x1280][code=BGR2YUV_NV21][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 437.0122290835199, + "gpu_time_us_python": 445.82375603414005, + "gpu_noise_us_cpp": 1.496, + "gpu_noise_us_python": 1.9220000000000002, + "gpu_bwutil_cpp": 0.7810618015366345, + "gpu_bwutil_python": 0.7656290938154872, + "gpu_gap_stddev_us": 1.1835948585856755 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 370.223592236225, + "gpu_time_us_python": 380.71130734782497, + "gpu_noise_us_cpp": 1.5899999999999999, + "gpu_noise_us_python": 3.12, + "gpu_bwutil_cpp": 0.7032405385101168, + "gpu_bwutil_python": 0.683826975294449, + "gpu_gap_stddev_us": 2.8811022629648964 + } + }, + "advcvtcolor_rgb_to_nv_advanced[InOutDataType=uchar3][shape=16x2160x3840][code=BGR2YUV_NV21][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 492.5367383506, + "gpu_time_us_python": 500.98848700197993, + "gpu_noise_us_cpp": 0.7180000000000001, + "gpu_noise_us_python": 1.714, + "gpu_bwutil_cpp": 0.7796374450052829, + "gpu_bwutil_python": 0.7664852961081012, + "gpu_gap_stddev_us": 0.49345926551611774 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 415.048853260225, + "gpu_time_us_python": 426.98504688462504, + "gpu_noise_us_cpp": 1.01, + "gpu_noise_us_python": 3.0524999999999998, + "gpu_bwutil_cpp": 0.7056939551094232, + "gpu_bwutil_python": 0.68597576020377, + "gpu_gap_stddev_us": 3.36310625366006 + } + }, + "advcvtcolor_rgb_to_nv_advanced[InOutDataType=uchar3][shape=64x1080x1920][code=BGR2YUV_NV21][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 489.86467609647997, + "gpu_time_us_python": 498.32749803992, + "gpu_noise_us_cpp": 2.1020000000000003, + "gpu_noise_us_python": 2.1700000000000004, + "gpu_bwutil_cpp": 0.7838902287058115, + "gpu_bwutil_python": 0.7705785679451341, + "gpu_gap_stddev_us": 0.6830047494778734 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 414.352990657875, + "gpu_time_us_python": 424.040698292975, + "gpu_noise_us_cpp": 1.0550000000000002, + "gpu_noise_us_python": 2.5875, + "gpu_bwutil_cpp": 0.7068812503250169, + "gpu_bwutil_python": 0.6906973286156612, + "gpu_gap_stddev_us": 2.4879908701850515 + } + }, + "advcvtcolor_rgb_to_nv_advanced[InOutDataType=uchar3][shape=128x720x1280][code=RGB2YUV_NV12][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 437.08106892946, + "gpu_time_us_python": 445.75214602172, + "gpu_noise_us_cpp": 1.2120000000000002, + "gpu_noise_us_python": 2.178, + "gpu_bwutil_cpp": 0.7809384343313214, + "gpu_bwutil_python": 0.7657497734464558, + "gpu_gap_stddev_us": 1.0554433954360523 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 369.95094985992506, + "gpu_time_us_python": 379.45283230235, + "gpu_noise_us_cpp": 0.9249999999999999, + "gpu_noise_us_python": 2.94, + "gpu_bwutil_cpp": 0.703753433027362, + "gpu_bwutil_python": 0.6861175738398839, + "gpu_gap_stddev_us": 1.0775703522720412 + } + }, + "advcvtcolor_rgb_to_nv_advanced[InOutDataType=uchar3][shape=16x2160x3840][code=RGB2YUV_NV12][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 492.9882067483601, + "gpu_time_us_python": 501.49444745767994, + "gpu_noise_us_cpp": 1.426, + "gpu_noise_us_python": 3.22, + "gpu_bwutil_cpp": 0.7789235211876646, + "gpu_bwutil_python": 0.7657121970546934, + "gpu_gap_stddev_us": 0.5970761405232965 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 414.91839804175, + "gpu_time_us_python": 426.33973659385, + "gpu_noise_us_cpp": 1.8875, + "gpu_noise_us_python": 4.5649999999999995, + "gpu_bwutil_cpp": 0.7059169572810342, + "gpu_bwutil_python": 0.6869756416187089, + "gpu_gap_stddev_us": 2.7651210777415938 + } + }, + "advcvtcolor_rgb_to_nv_advanced[InOutDataType=uchar3][shape=64x1080x1920][code=RGB2YUV_NV12][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 489.734115001, + "gpu_time_us_python": 498.54235902246, + "gpu_noise_us_cpp": 0.656, + "gpu_noise_us_python": 3.0380000000000003, + "gpu_bwutil_cpp": 0.784099084879566, + "gpu_bwutil_python": 0.7702466849202503, + "gpu_gap_stddev_us": 0.6849329840436869 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 414.2305656594, + "gpu_time_us_python": 423.70170957955, + "gpu_noise_us_cpp": 1.0350000000000001, + "gpu_noise_us_python": 2.5175, + "gpu_bwutil_cpp": 0.7070880606294165, + "gpu_bwutil_python": 0.6912756231434181, + "gpu_gap_stddev_us": 0.7331202398435676 + } + }, + "advcvtcolor_rgb_to_nv_advanced[InOutDataType=uchar4][shape=128x720x1280][code=BGR2YUV_NV21][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 507.48853211664, + "gpu_time_us_python": 514.99144526432, + "gpu_noise_us_cpp": 2.68, + "gpu_noise_us_python": 3.428, + "gpu_bwutil_cpp": 0.822058563016155, + "gpu_bwutil_python": 0.8100832509500705, + "gpu_gap_stddev_us": 0.7540912464362742 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 436.87510032367504, + "gpu_time_us_python": 444.0059500867, + "gpu_noise_us_cpp": 2.5575, + "gpu_noise_us_python": 3.665, + "gpu_bwutil_cpp": 0.7283382505417882, + "gpu_bwutil_python": 0.7166474192012324, + "gpu_gap_stddev_us": 0.961393568626186 + } + }, + "advcvtcolor_rgb_to_nv_advanced[InOutDataType=uchar4][shape=16x2160x3840][code=BGR2YUV_NV21][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 570.8243182823801, + "gpu_time_us_python": 578.4777803906001, + "gpu_noise_us_cpp": 2.4219999999999997, + "gpu_noise_us_python": 2.7840000000000003, + "gpu_bwutil_cpp": 0.8222028586774244, + "gpu_bwutil_python": 0.8113248563143841, + "gpu_gap_stddev_us": 0.42805727846998004 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 491.995468489025, + "gpu_time_us_python": 502.7323727178, + "gpu_noise_us_cpp": 2.6225, + "gpu_noise_us_python": 3.7575000000000003, + "gpu_bwutil_cpp": 0.7275810780434236, + "gpu_bwutil_python": 0.7120887197930905, + "gpu_gap_stddev_us": 4.090523040813219 + } + }, + "advcvtcolor_rgb_to_nv_advanced[InOutDataType=uchar4][shape=64x1080x1920][code=BGR2YUV_NV21][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 570.64518851758, + "gpu_time_us_python": 577.8478697033399, + "gpu_noise_us_cpp": 2.794, + "gpu_noise_us_python": 3.532, + "gpu_bwutil_cpp": 0.8224615900142977, + "gpu_bwutil_python": 0.8122097771842745, + "gpu_gap_stddev_us": 1.0206656255409605 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 490.9627661905, + "gpu_time_us_python": 498.43768614215, + "gpu_noise_us_cpp": 2.72, + "gpu_noise_us_python": 3.46, + "gpu_bwutil_cpp": 0.7291117430579999, + "gpu_bwutil_python": 0.7181858250042378, + "gpu_gap_stddev_us": 1.4236763751989068 + } + }, + "advcvtcolor_rgb_to_nv_advanced[InOutDataType=uchar4][shape=128x720x1280][code=RGB2YUV_NV12][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 507.51966748025995, + "gpu_time_us_python": 514.88896600032, + "gpu_noise_us_cpp": 2.368, + "gpu_noise_us_python": 2.8440000000000003, + "gpu_bwutil_cpp": 0.8220080900777681, + "gpu_bwutil_python": 0.8102440189658584, + "gpu_gap_stddev_us": 0.6714121369985785 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 436.6596689058, + "gpu_time_us_python": 443.779491014575, + "gpu_noise_us_cpp": 2.5425, + "gpu_noise_us_python": 3.1449999999999996, + "gpu_bwutil_cpp": 0.7286971900574244, + "gpu_bwutil_python": 0.717010942433992, + "gpu_gap_stddev_us": 1.184783026036122 + } + }, + "advcvtcolor_rgb_to_nv_advanced[InOutDataType=uchar4][shape=16x2160x3840][code=RGB2YUV_NV12][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 571.16599868874, + "gpu_time_us_python": 578.42717388708, + "gpu_noise_us_cpp": 2.43, + "gpu_noise_us_python": 2.592, + "gpu_bwutil_cpp": 0.8217111002737804, + "gpu_bwutil_python": 0.8113958572727038, + "gpu_gap_stddev_us": 0.5052217805845418 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 492.121670670325, + "gpu_time_us_python": 501.034322738475, + "gpu_noise_us_cpp": 2.6149999999999998, + "gpu_noise_us_python": 4.07, + "gpu_bwutil_cpp": 0.727394419787714, + "gpu_bwutil_python": 0.7144670542686802, + "gpu_gap_stddev_us": 2.238688438154579 + } + }, + "advcvtcolor_rgb_to_nv_advanced[InOutDataType=uchar4][shape=64x1080x1920][code=RGB2YUV_NV12][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 570.4347108382201, + "gpu_time_us_python": 577.8718899457401, + "gpu_noise_us_cpp": 2.55, + "gpu_noise_us_python": 3.372, + "gpu_bwutil_cpp": 0.8227645225667285, + "gpu_bwutil_python": 0.812175885952444, + "gpu_gap_stddev_us": 0.5785737423332011 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 490.54742810352496, + "gpu_time_us_python": 495.3111200105, + "gpu_noise_us_cpp": 2.7074999999999996, + "gpu_noise_us_python": 3.1950000000000003, + "gpu_bwutil_cpp": 0.7297290771800506, + "gpu_bwutil_python": 0.7227130793268579, + "gpu_gap_stddev_us": 1.9959467051193924 + } + } + } + }, + "advcvtcolor_planar_nchw_rgb_to_nv_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3", + "uchar4" + ], + "string_axes": { + "shape": [ + "128x720x1280", + "64x1080x1920", + "16x2160x3840" + ], + "code": [ + "RGB2YUV_NV12", + "BGR2YUV_NV21" + ], + "inputKind": [ + "Tensor" + ], + "layout": [ + "NCHW" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AdvCvtColor RGB/RGBA nv (advanced) (NCHW)." + }, + "baselines": { + "advcvtcolor_planar_nchw_rgb_to_nv_advanced[InOutDataType=uchar3][shape=128x720x1280][code=BGR2YUV_NV21][inputKind=Tensor][layout=NCHW]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 459.16611723937996, + "gpu_time_us_python": 467.9245915570799, + "gpu_noise_us_cpp": 1.04, + "gpu_noise_us_python": 2.052, + "gpu_bwutil_cpp": 0.7433766831469938, + "gpu_bwutil_python": 0.7294632325193345, + "gpu_gap_stddev_us": 0.5406157678783017 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 365.39317266492503, + "gpu_time_us_python": 375.831658057975, + "gpu_noise_us_cpp": 1.2925, + "gpu_noise_us_python": 2.7525, + "gpu_bwutil_cpp": 0.7125051191886573, + "gpu_bwutil_python": 0.6927093785674974, + "gpu_gap_stddev_us": 1.9556600000624431 + } + }, + "advcvtcolor_planar_nchw_rgb_to_nv_advanced[InOutDataType=uchar3][shape=128x720x1280][code=RGB2YUV_NV12][inputKind=Tensor][layout=NCHW]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 459.59103395328003, + "gpu_time_us_python": 467.87689118736, + "gpu_noise_us_cpp": 1.1300000000000001, + "gpu_noise_us_python": 2.05, + "gpu_bwutil_cpp": 0.7426893342925548, + "gpu_bwutil_python": 0.7295367625769515, + "gpu_gap_stddev_us": 0.13593128999991153 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 365.64148452925, + "gpu_time_us_python": 374.21484531745, + "gpu_noise_us_cpp": 0.815, + "gpu_noise_us_python": 2.395, + "gpu_bwutil_cpp": 0.7120205700821103, + "gpu_bwutil_python": 0.6957070674177341, + "gpu_gap_stddev_us": 0.5199394259699494 + } + }, + "advcvtcolor_planar_nchw_rgb_to_nv_advanced[InOutDataType=uchar3][shape=16x2160x3840][code=BGR2YUV_NV21][inputKind=Tensor][layout=NCHW]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 510.91938105352, + "gpu_time_us_python": 519.2400825756399, + "gpu_noise_us_cpp": 1.7480000000000004, + "gpu_noise_us_python": 2.9339999999999997, + "gpu_bwutil_cpp": 0.7515865101896007, + "gpu_bwutil_python": 0.7395426425752356, + "gpu_gap_stddev_us": 0.4168753462150707 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 412.003747142625, + "gpu_time_us_python": 421.106878464475, + "gpu_noise_us_cpp": 1.82, + "gpu_noise_us_python": 3.355, + "gpu_bwutil_cpp": 0.7108829151925213, + "gpu_bwutil_python": 0.6955058072013756, + "gpu_gap_stddev_us": 1.3623429770683517 + } + }, + "advcvtcolor_planar_nchw_rgb_to_nv_advanced[InOutDataType=uchar3][shape=16x2160x3840][code=RGB2YUV_NV12][inputKind=Tensor][layout=NCHW]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 510.66672298366, + "gpu_time_us_python": 519.1663137114999, + "gpu_noise_us_cpp": 0.8620000000000001, + "gpu_noise_us_python": 1.706, + "gpu_bwutil_cpp": 0.7519583313458872, + "gpu_bwutil_python": 0.7396475390081901, + "gpu_gap_stddev_us": 0.24716523753089895 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 411.475434627, + "gpu_time_us_python": 420.84217616359996, + "gpu_noise_us_cpp": 1.3824999999999998, + "gpu_noise_us_python": 3.49, + "gpu_bwutil_cpp": 0.7118001544795559, + "gpu_bwutil_python": 0.6959433456380876, + "gpu_gap_stddev_us": 1.4716255830633458 + } + }, + "advcvtcolor_planar_nchw_rgb_to_nv_advanced[InOutDataType=uchar3][shape=64x1080x1920][code=BGR2YUV_NV21][inputKind=Tensor][layout=NCHW]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 511.78181471263997, + "gpu_time_us_python": 520.07309615094, + "gpu_noise_us_cpp": 0.9360000000000002, + "gpu_noise_us_python": 1.782, + "gpu_bwutil_cpp": 0.7503198300631915, + "gpu_bwutil_python": 0.7383580856061464, + "gpu_gap_stddev_us": 0.39097352173027944 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 410.647365204825, + "gpu_time_us_python": 420.17333539002504, + "gpu_noise_us_cpp": 2.4924999999999997, + "gpu_noise_us_python": 3.3499999999999996, + "gpu_bwutil_cpp": 0.7132333375573545, + "gpu_bwutil_python": 0.6970500842404481, + "gpu_gap_stddev_us": 1.8622167946401733 + } + }, + "advcvtcolor_planar_nchw_rgb_to_nv_advanced[InOutDataType=uchar3][shape=64x1080x1920][code=RGB2YUV_NV12][inputKind=Tensor][layout=NCHW]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 511.82218578196, + "gpu_time_us_python": 520.13848846322, + "gpu_noise_us_cpp": 1.296, + "gpu_noise_us_python": 1.746, + "gpu_bwutil_cpp": 0.750260930178342, + "gpu_bwutil_python": 0.7382655294370759, + "gpu_gap_stddev_us": 0.7535682525734496 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 409.99039873032496, + "gpu_time_us_python": 421.023386502, + "gpu_noise_us_cpp": 0.92, + "gpu_noise_us_python": 2.315, + "gpu_bwutil_cpp": 0.7143779586817032, + "gpu_bwutil_python": 0.6956590049678376, + "gpu_gap_stddev_us": 2.198064470241411 + } + }, + "advcvtcolor_planar_nchw_rgb_to_nv_advanced[InOutDataType=uchar4][shape=128x720x1280][code=BGR2YUV_NV21][inputKind=Tensor][layout=NCHW]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 461.76505298845996, + "gpu_time_us_python": 470.13599291166, + "gpu_noise_us_cpp": 0.708, + "gpu_noise_us_python": 3.282, + "gpu_bwutil_cpp": 0.90345795697445, + "gpu_bwutil_python": 0.887372434105701, + "gpu_gap_stddev_us": 0.45991854447393565 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 367.233866419125, + "gpu_time_us_python": 377.57234353237504, + "gpu_noise_us_cpp": 1.0925, + "gpu_noise_us_python": 2.4675, + "gpu_bwutil_cpp": 0.8664700950046349, + "gpu_bwutil_python": 0.8427455937979906, + "gpu_gap_stddev_us": 2.282494542852446 + } + }, + "advcvtcolor_planar_nchw_rgb_to_nv_advanced[InOutDataType=uchar4][shape=128x720x1280][code=RGB2YUV_NV12][inputKind=Tensor][layout=NCHW]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 461.01422600757996, + "gpu_time_us_python": 470.2251597468, + "gpu_noise_us_cpp": 1.952, + "gpu_noise_us_python": 1.7580000000000002, + "gpu_bwutil_cpp": 0.9049294126673697, + "gpu_bwutil_python": 0.8872033289637891, + "gpu_gap_stddev_us": 0.07686825343661255 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 367.6629604796, + "gpu_time_us_python": 376.872026031075, + "gpu_noise_us_cpp": 1.305, + "gpu_noise_us_python": 2.6875, + "gpu_bwutil_cpp": 0.8654578003958631, + "gpu_bwutil_python": 0.8442979210706285, + "gpu_gap_stddev_us": 1.3259020514432245 + } + }, + "advcvtcolor_planar_nchw_rgb_to_nv_advanced[InOutDataType=uchar4][shape=16x2160x3840][code=BGR2YUV_NV21][inputKind=Tensor][layout=NCHW]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 512.23364032678, + "gpu_time_us_python": 521.8775186750402, + "gpu_noise_us_cpp": 1.334, + "gpu_noise_us_python": 2.69, + "gpu_bwutil_cpp": 0.9162486891066134, + "gpu_bwutil_python": 0.8993198605354626, + "gpu_gap_stddev_us": 0.8792212105975225 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 412.4234903483, + "gpu_time_us_python": 421.640827339675, + "gpu_noise_us_cpp": 0.9550000000000001, + "gpu_noise_us_python": 3.0850000000000004, + "gpu_bwutil_cpp": 0.8679704369004884, + "gpu_bwutil_python": 0.8489855787220795, + "gpu_gap_stddev_us": 1.3927225379815515 + } + }, + "advcvtcolor_planar_nchw_rgb_to_nv_advanced[InOutDataType=uchar4][shape=16x2160x3840][code=RGB2YUV_NV12][inputKind=Tensor][layout=NCHW]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 511.76003634558003, + "gpu_time_us_python": 521.0643191474601, + "gpu_noise_us_cpp": 0.742, + "gpu_noise_us_python": 2.1740000000000004, + "gpu_bwutil_cpp": 0.9170966990978584, + "gpu_bwutil_python": 0.9007207426659422, + "gpu_gap_stddev_us": 0.3481118708727091 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 412.48701613575, + "gpu_time_us_python": 420.9048949245, + "gpu_noise_us_cpp": 1.4124999999999999, + "gpu_noise_us_python": 2.4850000000000003, + "gpu_bwutil_cpp": 0.8678375849560391, + "gpu_bwutil_python": 0.8504827685844474, + "gpu_gap_stddev_us": 0.4744584230105031 + } + }, + "advcvtcolor_planar_nchw_rgb_to_nv_advanced[InOutDataType=uchar4][shape=64x1080x1920][code=BGR2YUV_NV21][inputKind=Tensor][layout=NCHW]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 512.6530044391, + "gpu_time_us_python": 521.15478778022, + "gpu_noise_us_cpp": 0.6499999999999999, + "gpu_noise_us_python": 1.404, + "gpu_bwutil_cpp": 0.9154991613719563, + "gpu_bwutil_python": 0.9005644647544427, + "gpu_gap_stddev_us": 0.293005906879306 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 411.500950194075, + "gpu_time_us_python": 421.468798977275, + "gpu_noise_us_cpp": 1.695, + "gpu_noise_us_python": 3.2024999999999997, + "gpu_bwutil_cpp": 0.8699193678591128, + "gpu_bwutil_python": 0.8493308274263208, + "gpu_gap_stddev_us": 1.6368343686371989 + } + }, + "advcvtcolor_planar_nchw_rgb_to_nv_advanced[InOutDataType=uchar4][shape=64x1080x1920][code=RGB2YUV_NV12][inputKind=Tensor][layout=NCHW]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 511.6787872430999, + "gpu_time_us_python": 521.13344771878, + "gpu_noise_us_cpp": 1.8960000000000001, + "gpu_noise_us_python": 2.37, + "gpu_bwutil_cpp": 0.9172422620649131, + "gpu_bwutil_python": 0.9006015571523317, + "gpu_gap_stddev_us": 0.34787106401704787 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 411.18443896022495, + "gpu_time_us_python": 421.30480671695, + "gpu_noise_us_cpp": 1.2774999999999999, + "gpu_noise_us_python": 3.2724999999999995, + "gpu_bwutil_cpp": 0.8705891005162634, + "gpu_bwutil_python": 0.8496622284805773, + "gpu_gap_stddev_us": 1.7253502760476365 + } + } + } + }, + "advcvtcolor_fakeplanar_nchw_rgb_to_nv_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3", + "uchar4" + ], + "string_axes": { + "shape": [ + "128x720x1280", + "64x1080x1920", + "16x2160x3840" + ], + "code": [ + "RGB2YUV_NV12", + "BGR2YUV_NV21" + ], + "inputKind": [ + "Tensor" + ], + "layout": [ + "NCHW_FAKE" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AdvCvtColor RGB/RGBA nv (advanced) (NCHW_FAKE)." + }, + "baselines": { + "advcvtcolor_fakeplanar_nchw_rgb_to_nv_advanced[InOutDataType=uchar3][shape=128x720x1280][code=BGR2YUV_NV21][inputKind=Tensor][layout=NCHW_FAKE]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2103.0826344209, + "gpu_time_us_python": 2120.97642583358, + "gpu_noise_us_cpp": 1.702, + "gpu_noise_us_python": 2.338, + "gpu_bwutil_cpp": 0.4869044412890357, + "gpu_bwutil_python": 0.48279652858474975, + "gpu_gap_stddev_us": 0.6895883088535123 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1635.902879399925, + "gpu_time_us_python": 1649.229792055925, + "gpu_noise_us_cpp": 1.5, + "gpu_noise_us_python": 3.8975, + "gpu_bwutil_cpp": 0.47742903262700964, + "gpu_bwutil_python": 0.47356957812100375, + "gpu_gap_stddev_us": 1.379160873401585 + } + }, + "advcvtcolor_fakeplanar_nchw_rgb_to_nv_advanced[InOutDataType=uchar3][shape=128x720x1280][code=RGB2YUV_NV12][inputKind=Tensor][layout=NCHW_FAKE]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2103.33562498328, + "gpu_time_us_python": 2121.2246175539003, + "gpu_noise_us_cpp": 1.274, + "gpu_noise_us_python": 2.396, + "gpu_bwutil_cpp": 0.4868458751485754, + "gpu_bwutil_python": 0.4827400571886229, + "gpu_gap_stddev_us": 1.4936420227768354 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1635.9424951582998, + "gpu_time_us_python": 1650.3713111351, + "gpu_noise_us_cpp": 3.4850000000000003, + "gpu_noise_us_python": 4.545, + "gpu_bwutil_cpp": 0.4774189622629299, + "gpu_bwutil_python": 0.47324309643721146, + "gpu_gap_stddev_us": 3.649238188431343 + } + }, + "advcvtcolor_fakeplanar_nchw_rgb_to_nv_advanced[InOutDataType=uchar3][shape=16x2160x3840][code=BGR2YUV_NV21][inputKind=Tensor][layout=NCHW_FAKE]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2369.93586342284, + "gpu_time_us_python": 2387.80811854768, + "gpu_noise_us_cpp": 4.209999999999999, + "gpu_noise_us_python": 2.852, + "gpu_bwutil_cpp": 0.4860892794242053, + "gpu_bwutil_python": 0.48245094389995186, + "gpu_gap_stddev_us": 2.7017396300696253 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1842.317986524775, + "gpu_time_us_python": 1855.4336233189752, + "gpu_noise_us_cpp": 1.69, + "gpu_noise_us_python": 3.6325000000000003, + "gpu_bwutil_cpp": 0.4769293461813471, + "gpu_bwutil_python": 0.47355767588784825, + "gpu_gap_stddev_us": 2.064552898465076 + } + }, + "advcvtcolor_fakeplanar_nchw_rgb_to_nv_advanced[InOutDataType=uchar3][shape=16x2160x3840][code=RGB2YUV_NV12][inputKind=Tensor][layout=NCHW_FAKE]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2369.39681206106, + "gpu_time_us_python": 2387.45898473826, + "gpu_noise_us_cpp": 1.924, + "gpu_noise_us_python": 1.9899999999999998, + "gpu_bwutil_cpp": 0.48619981352802055, + "gpu_bwutil_python": 0.48252176339501496, + "gpu_gap_stddev_us": 1.9531498100827982 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1842.3493780169251, + "gpu_time_us_python": 1856.25790190395, + "gpu_noise_us_cpp": 1.7000000000000002, + "gpu_noise_us_python": 5.452500000000001, + "gpu_bwutil_cpp": 0.4769219484892152, + "gpu_bwutil_python": 0.4733468030276574, + "gpu_gap_stddev_us": 2.335139161267794 + } + }, + "advcvtcolor_fakeplanar_nchw_rgb_to_nv_advanced[InOutDataType=uchar3][shape=64x1080x1920][code=BGR2YUV_NV21][inputKind=Tensor][layout=NCHW_FAKE]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2365.4283532556, + "gpu_time_us_python": 2381.8183109423403, + "gpu_noise_us_cpp": 2.122, + "gpu_noise_us_python": 3.0740000000000003, + "gpu_bwutil_cpp": 0.4870155026007715, + "gpu_bwutil_python": 0.48366413738254127, + "gpu_gap_stddev_us": 0.9719797241938044 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1841.36779819555, + "gpu_time_us_python": 1854.969216870125, + "gpu_noise_us_cpp": 1.9049999999999998, + "gpu_noise_us_python": 3.495, + "gpu_bwutil_cpp": 0.4771767507897898, + "gpu_bwutil_python": 0.4736770915873629, + "gpu_gap_stddev_us": 2.935821398165138 + } + }, + "advcvtcolor_fakeplanar_nchw_rgb_to_nv_advanced[InOutDataType=uchar3][shape=64x1080x1920][code=RGB2YUV_NV12][inputKind=Tensor][layout=NCHW_FAKE]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2364.3618961549596, + "gpu_time_us_python": 2384.69123794916, + "gpu_noise_us_cpp": 2.518, + "gpu_noise_us_python": 2.5580000000000003, + "gpu_bwutil_cpp": 0.4872352013032817, + "gpu_bwutil_python": 0.48308197984154777, + "gpu_gap_stddev_us": 3.12786947381899 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1841.2836911552, + "gpu_time_us_python": 1857.040709372325, + "gpu_noise_us_cpp": 1.57, + "gpu_noise_us_python": 6.0475, + "gpu_bwutil_cpp": 0.4771980802921255, + "gpu_bwutil_python": 0.4731489405591429, + "gpu_gap_stddev_us": 5.028227365716797 + } + }, + "advcvtcolor_fakeplanar_nchw_rgb_to_nv_advanced[InOutDataType=uchar4][shape=128x720x1280][code=BGR2YUV_NV21][inputKind=Tensor][layout=NCHW_FAKE]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2621.80900176362, + "gpu_time_us_python": 2639.8418787905402, + "gpu_noise_us_cpp": 3.4319999999999995, + "gpu_noise_us_python": 2.878, + "gpu_bwutil_cpp": 0.47736346959630566, + "gpu_bwutil_python": 0.4741026004049381, + "gpu_gap_stddev_us": 2.077052787419382 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2041.1509013756997, + "gpu_time_us_python": 2057.0065808601003, + "gpu_noise_us_cpp": 2.86, + "gpu_noise_us_python": 3.775, + "gpu_bwutil_cpp": 0.46766924639420776, + "gpu_bwutil_python": 0.4640637186569886, + "gpu_gap_stddev_us": 3.249917065070228 + } + }, + "advcvtcolor_fakeplanar_nchw_rgb_to_nv_advanced[InOutDataType=uchar4][shape=128x720x1280][code=RGB2YUV_NV12][inputKind=Tensor][layout=NCHW_FAKE]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2622.6324756939803, + "gpu_time_us_python": 2639.03992000376, + "gpu_noise_us_cpp": 2.018, + "gpu_noise_us_python": 3.124, + "gpu_bwutil_cpp": 0.4772135746165298, + "gpu_bwutil_python": 0.4742468846181177, + "gpu_gap_stddev_us": 1.4952792671806836 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2041.094046298025, + "gpu_time_us_python": 2055.58375212465, + "gpu_noise_us_cpp": 2.0725, + "gpu_noise_us_python": 3.505, + "gpu_bwutil_cpp": 0.4676822239181867, + "gpu_bwutil_python": 0.4643845759824019, + "gpu_gap_stddev_us": 2.399858348020195 + } + }, + "advcvtcolor_fakeplanar_nchw_rgb_to_nv_advanced[InOutDataType=uchar4][shape=16x2160x3840][code=BGR2YUV_NV21][inputKind=Tensor][layout=NCHW_FAKE]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3000.58327345616, + "gpu_time_us_python": 3020.31640604314, + "gpu_noise_us_cpp": 2.9939999999999998, + "gpu_noise_us_python": 6.040000000000001, + "gpu_bwutil_cpp": 0.4692421305220657, + "gpu_bwutil_python": 0.4661766961717947, + "gpu_gap_stddev_us": 3.356690197874266 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2303.737175027125, + "gpu_time_us_python": 2317.449021062925, + "gpu_noise_us_cpp": 2.6675, + "gpu_noise_us_python": 3.085, + "gpu_bwutil_cpp": 0.46615784373259017, + "gpu_bwutil_python": 0.46339946184715897, + "gpu_gap_stddev_us": 1.0765357163252554 + } + }, + "advcvtcolor_fakeplanar_nchw_rgb_to_nv_advanced[InOutDataType=uchar4][shape=16x2160x3840][code=RGB2YUV_NV12][inputKind=Tensor][layout=NCHW_FAKE]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2998.89122361224, + "gpu_time_us_python": 3019.84264247384, + "gpu_noise_us_cpp": 2.388, + "gpu_noise_us_python": 3.836, + "gpu_bwutil_cpp": 0.46950700876147317, + "gpu_bwutil_python": 0.46624975282780035, + "gpu_gap_stddev_us": 2.616414935743374 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2304.0049185446, + "gpu_time_us_python": 2318.6481169371, + "gpu_noise_us_cpp": 2.1575, + "gpu_noise_us_python": 4.7425, + "gpu_bwutil_cpp": 0.4661035057628933, + "gpu_bwutil_python": 0.4631598804526896, + "gpu_gap_stddev_us": 3.0894424579515327 + } + }, + "advcvtcolor_fakeplanar_nchw_rgb_to_nv_advanced[InOutDataType=uchar4][shape=64x1080x1920][code=BGR2YUV_NV21][inputKind=Tensor][layout=NCHW_FAKE]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2951.50296996614, + "gpu_time_us_python": 2969.44816589352, + "gpu_noise_us_cpp": 2.272, + "gpu_noise_us_python": 4.542, + "gpu_bwutil_cpp": 0.4770451789731146, + "gpu_bwutil_python": 0.47416228364778534, + "gpu_gap_stddev_us": 1.3940808108061629 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2303.4016101731504, + "gpu_time_us_python": 2317.2482696775496, + "gpu_noise_us_cpp": 3.9274999999999998, + "gpu_noise_us_python": 4.3125, + "gpu_bwutil_cpp": 0.4662257568221376, + "gpu_bwutil_python": 0.4634386089982866, + "gpu_gap_stddev_us": 2.3160576176197436 + } + }, + "advcvtcolor_fakeplanar_nchw_rgb_to_nv_advanced[InOutDataType=uchar4][shape=64x1080x1920][code=RGB2YUV_NV12][inputKind=Tensor][layout=NCHW_FAKE]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2951.4065927617203, + "gpu_time_us_python": 2968.23863422164, + "gpu_noise_us_cpp": 2.814, + "gpu_noise_us_python": 2.984, + "gpu_bwutil_cpp": 0.477060769079597, + "gpu_bwutil_python": 0.4743554906070727, + "gpu_gap_stddev_us": 2.361645167466854 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2302.423625911025, + "gpu_time_us_python": 2318.1976622113752, + "gpu_noise_us_cpp": 2.5825, + "gpu_noise_us_python": 5.6775, + "gpu_bwutil_cpp": 0.46642381044593434, + "gpu_bwutil_python": 0.46325019156027486, + "gpu_gap_stddev_us": 4.2351124457293094 + } + } + } + }, + "advcvtcolor_nv_to_rgb3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "256x720x1280", + "128x1080x1920", + "32x2160x3840" + ], + "code": [ + "YUV2RGB_NV12", + "YUV2BGR_NV21" + ], + "inputKind": [ + "Tensor" + ], + "layout": [ + "NHWC" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AdvCvtColor RGB8 nv rgb3 (advanced)." + }, + "baselines": { + "advcvtcolor_nv_to_rgb3_advanced[InOutDataType=uchar3][shape=128x1080x1920][code=YUV2BGR_NV21][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1766.23123430864, + "gpu_time_us_python": 1776.59353567346, + "gpu_noise_us_cpp": 1.874, + "gpu_noise_us_python": 3.508, + "gpu_bwutil_cpp": 0.4348242141754778, + "gpu_bwutil_python": 0.4322882695904896, + "gpu_gap_stddev_us": 1.4972367405869267 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1192.789465044375, + "gpu_time_us_python": 1202.7223146307251, + "gpu_noise_us_cpp": 1.53, + "gpu_noise_us_python": 5.165, + "gpu_bwutil_cpp": 0.491267165403683, + "gpu_bwutil_python": 0.4872102827310317, + "gpu_gap_stddev_us": 1.3146096486531171 + } + }, + "advcvtcolor_nv_to_rgb3_advanced[InOutDataType=uchar3][shape=256x720x1280][code=YUV2BGR_NV21][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1575.6345577959198, + "gpu_time_us_python": 1585.42552175398, + "gpu_noise_us_cpp": 1.254, + "gpu_noise_us_python": 2.028, + "gpu_bwutil_cpp": 0.43326466211666403, + "gpu_bwutil_python": 0.43058909873994555, + "gpu_gap_stddev_us": 0.9998803466877441 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1077.8451883121, + "gpu_time_us_python": 1089.3613037147747, + "gpu_noise_us_cpp": 2.3775000000000004, + "gpu_noise_us_python": 4.295, + "gpu_bwutil_cpp": 0.48325687821112184, + "gpu_bwutil_python": 0.47811624149270443, + "gpu_gap_stddev_us": 3.907419935771994 + } + }, + "advcvtcolor_nv_to_rgb3_advanced[InOutDataType=uchar3][shape=32x2160x3840][code=YUV2BGR_NV21][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1773.82780138918, + "gpu_time_us_python": 1784.34850466282, + "gpu_noise_us_cpp": 1.3960000000000001, + "gpu_noise_us_python": 3.336, + "gpu_bwutil_cpp": 0.43296208213181187, + "gpu_bwutil_python": 0.4304093165993524, + "gpu_gap_stddev_us": 0.6337041346233284 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1215.3680357395, + "gpu_time_us_python": 1226.297685325125, + "gpu_noise_us_cpp": 1.44, + "gpu_noise_us_python": 3.7, + "gpu_bwutil_cpp": 0.4821455336322876, + "gpu_bwutil_python": 0.47784737407251415, + "gpu_gap_stddev_us": 1.5239221690906308 + } + }, + "advcvtcolor_nv_to_rgb3_advanced[InOutDataType=uchar3][shape=128x1080x1920][code=YUV2RGB_NV12][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1766.0567198000801, + "gpu_time_us_python": 1776.3291861147998, + "gpu_noise_us_cpp": 2.678, + "gpu_noise_us_python": 2.784, + "gpu_bwutil_cpp": 0.4348672273660272, + "gpu_bwutil_python": 0.4323524714177597, + "gpu_gap_stddev_us": 1.2147021094236492 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1192.6446706821, + "gpu_time_us_python": 1202.518946352525, + "gpu_noise_us_cpp": 2.125, + "gpu_noise_us_python": 2.95, + "gpu_bwutil_cpp": 0.4913266001630383, + "gpu_bwutil_python": 0.4872889820539763, + "gpu_gap_stddev_us": 0.29046011184055254 + } + }, + "advcvtcolor_nv_to_rgb3_advanced[InOutDataType=uchar3][shape=256x720x1280][code=YUV2RGB_NV12][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1575.82647770452, + "gpu_time_us_python": 1585.96603236618, + "gpu_noise_us_cpp": 2.004, + "gpu_noise_us_python": 2.7800000000000002, + "gpu_bwutil_cpp": 0.43321196564140835, + "gpu_bwutil_python": 0.4304425019856655, + "gpu_gap_stddev_us": 1.4446115097941543 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1077.894733442875, + "gpu_time_us_python": 1089.589719408275, + "gpu_noise_us_cpp": 2.385, + "gpu_noise_us_python": 4.785, + "gpu_bwutil_cpp": 0.4832406345384371, + "gpu_bwutil_python": 0.4780167142746395, + "gpu_gap_stddev_us": 3.017736733833429 + } + }, + "advcvtcolor_nv_to_rgb3_advanced[InOutDataType=uchar3][shape=32x2160x3840][code=YUV2RGB_NV12][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1773.77295441918, + "gpu_time_us_python": 1784.0563150379203, + "gpu_noise_us_cpp": 1.5260000000000002, + "gpu_noise_us_python": 3.6420000000000003, + "gpu_bwutil_cpp": 0.4329754636225517, + "gpu_bwutil_python": 0.430480019101639, + "gpu_gap_stddev_us": 1.9484503283678463 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1215.203990668425, + "gpu_time_us_python": 1226.840736400625, + "gpu_noise_us_cpp": 1.1524999999999999, + "gpu_noise_us_python": 3.735, + "gpu_bwutil_cpp": 0.48221432845813017, + "gpu_bwutil_python": 0.47762156292697167, + "gpu_gap_stddev_us": 1.8434820227110658 + } + } + } + }, + "advcvtcolor_planar_nchw_nv_to_rgb3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "256x720x1280", + "128x1080x1920", + "32x2160x3840" + ], + "code": [ + "YUV2RGB_NV12", + "YUV2BGR_NV21" + ], + "inputKind": [ + "Tensor" + ], + "layout": [ + "NCHW" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AdvCvtColor RGB8 nv rgb3 (advanced) (NCHW)." + }, + "baselines": { + "advcvtcolor_planar_nchw_nv_to_rgb3_advanced[InOutDataType=uchar3][shape=128x1080x1920][code=YUV2BGR_NV21][inputKind=Tensor][layout=NCHW]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1260.1961479713798, + "gpu_time_us_python": 1272.13237854424, + "gpu_noise_us_cpp": 2.2079999999999997, + "gpu_noise_us_python": 2.012, + "gpu_bwutil_cpp": 0.6094289867390391, + "gpu_bwutil_python": 0.6037114364044958, + "gpu_gap_stddev_us": 1.5996675840591832 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1006.9441625961999, + "gpu_time_us_python": 1014.8038678992999, + "gpu_noise_us_cpp": 1.9475000000000002, + "gpu_noise_us_python": 2.6574999999999998, + "gpu_bwutil_cpp": 0.5818315595208717, + "gpu_bwutil_python": 0.5773189951118234, + "gpu_gap_stddev_us": 1.6190020309183026 + } + }, + "advcvtcolor_planar_nchw_nv_to_rgb3_advanced[InOutDataType=uchar3][shape=128x1080x1920][code=YUV2RGB_NV12][inputKind=Tensor][layout=NCHW]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1266.1852942572, + "gpu_time_us_python": 1275.9270828337599, + "gpu_noise_us_cpp": 2.056, + "gpu_noise_us_python": 2.5519999999999996, + "gpu_bwutil_cpp": 0.6065463783148519, + "gpu_bwutil_python": 0.6019158245838341, + "gpu_gap_stddev_us": 1.182585983741536 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1006.723025997775, + "gpu_time_us_python": 1014.78067708465, + "gpu_noise_us_cpp": 2.6325000000000003, + "gpu_noise_us_python": 2.4275, + "gpu_bwutil_cpp": 0.5819707338133714, + "gpu_bwutil_python": 0.5773374669097058, + "gpu_gap_stddev_us": 0.9393293085252086 + } + }, + "advcvtcolor_planar_nchw_nv_to_rgb3_advanced[InOutDataType=uchar3][shape=256x720x1280][code=YUV2BGR_NV21][inputKind=Tensor][layout=NCHW]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1098.38822850006, + "gpu_time_us_python": 1107.6743650225003, + "gpu_noise_us_cpp": 1.7, + "gpu_noise_us_python": 2.316, + "gpu_bwutil_cpp": 0.6215168661821755, + "gpu_bwutil_python": 0.6163075597678737, + "gpu_gap_stddev_us": 1.7674079320438907 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 894.8485321367, + "gpu_time_us_python": 903.0210789339, + "gpu_noise_us_cpp": 1.8125, + "gpu_noise_us_python": 2.27, + "gpu_bwutil_cpp": 0.5819693749097496, + "gpu_bwutil_python": 0.5766990249974915, + "gpu_gap_stddev_us": 0.6574876281913561 + } + }, + "advcvtcolor_planar_nchw_nv_to_rgb3_advanced[InOutDataType=uchar3][shape=256x720x1280][code=YUV2RGB_NV12][inputKind=Tensor][layout=NCHW]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1098.7241750223197, + "gpu_time_us_python": 1108.97686426474, + "gpu_noise_us_cpp": 2.5780000000000003, + "gpu_noise_us_python": 3.086, + "gpu_bwutil_cpp": 0.6213267904029838, + "gpu_bwutil_python": 0.6155840107602646, + "gpu_gap_stddev_us": 1.8917834858278926 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 894.6856756935749, + "gpu_time_us_python": 904.50362788205, + "gpu_noise_us_cpp": 0.9225000000000001, + "gpu_noise_us_python": 3.8175, + "gpu_bwutil_cpp": 0.5820798568981899, + "gpu_bwutil_python": 0.5757617060436095, + "gpu_gap_stddev_us": 1.2721961264886639 + } + }, + "advcvtcolor_planar_nchw_nv_to_rgb3_advanced[InOutDataType=uchar3][shape=32x2160x3840][code=YUV2BGR_NV21][inputKind=Tensor][layout=NCHW]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1316.18201049566, + "gpu_time_us_python": 1322.7856885659198, + "gpu_noise_us_cpp": 2.224, + "gpu_noise_us_python": 2.894, + "gpu_bwutil_cpp": 0.5835060179387901, + "gpu_bwutil_python": 0.5805932817231276, + "gpu_gap_stddev_us": 1.6285601129317044 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1014.7739796892249, + "gpu_time_us_python": 1024.1204862512498, + "gpu_noise_us_cpp": 1.1375, + "gpu_noise_us_python": 2.5775, + "gpu_bwutil_cpp": 0.5773353846604989, + "gpu_bwutil_python": 0.5720662457228942, + "gpu_gap_stddev_us": 0.5248658889929749 + } + }, + "advcvtcolor_planar_nchw_nv_to_rgb3_advanced[InOutDataType=uchar3][shape=32x2160x3840][code=YUV2RGB_NV12][inputKind=Tensor][layout=NCHW]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1312.40448714548, + "gpu_time_us_python": 1325.1166590937398, + "gpu_noise_us_cpp": 2.1679999999999997, + "gpu_noise_us_python": 2.628, + "gpu_bwutil_cpp": 0.5851855111590576, + "gpu_bwutil_python": 0.579571844103042, + "gpu_gap_stddev_us": 1.106172594051916 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1015.036095466725, + "gpu_time_us_python": 1024.576037940775, + "gpu_noise_us_cpp": 1.4675, + "gpu_noise_us_python": 4.6175, + "gpu_bwutil_cpp": 0.5771807616922053, + "gpu_bwutil_python": 0.5717998179827999, + "gpu_gap_stddev_us": 1.441922902304913 + } + } + } + }, + "advcvtcolor_fakeplanar_nchw_nv_to_rgb3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "256x720x1280", + "128x1080x1920", + "32x2160x3840" + ], + "code": [ + "YUV2RGB_NV12", + "YUV2BGR_NV21" + ], + "inputKind": [ + "Tensor" + ], + "layout": [ + "NCHW_FAKE" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AdvCvtColor RGB8 nv rgb3 (advanced) (NCHW_FAKE)." + }, + "baselines": { + "advcvtcolor_fakeplanar_nchw_nv_to_rgb3_advanced[InOutDataType=uchar3][shape=128x1080x1920][code=YUV2BGR_NV21][inputKind=Tensor][layout=NCHW_FAKE]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3879.15458312396, + "gpu_time_us_python": 3897.3408677027205, + "gpu_noise_us_cpp": 2.304, + "gpu_noise_us_python": 2.952, + "gpu_bwutil_cpp": 0.5939439126404086, + "gpu_bwutil_python": 0.5911723469332291, + "gpu_gap_stddev_us": 1.3229841830405045 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2879.83274828515, + "gpu_time_us_python": 2898.0383319809, + "gpu_noise_us_cpp": 3.2575, + "gpu_noise_us_python": 3.3075, + "gpu_bwutil_cpp": 0.610244503365521, + "gpu_bwutil_python": 0.606406670835093, + "gpu_gap_stddev_us": 3.491729181477727 + } + }, + "advcvtcolor_fakeplanar_nchw_nv_to_rgb3_advanced[InOutDataType=uchar3][shape=128x1080x1920][code=YUV2RGB_NV12][inputKind=Tensor][layout=NCHW_FAKE]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3880.7929955995596, + "gpu_time_us_python": 3897.19278262206, + "gpu_noise_us_cpp": 2.334, + "gpu_noise_us_python": 2.388, + "gpu_bwutil_cpp": 0.5936931772526279, + "gpu_bwutil_python": 0.5911949216143331, + "gpu_gap_stddev_us": 3.3099706023318616 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2879.9578494705, + "gpu_time_us_python": 2898.290077906025, + "gpu_noise_us_cpp": 1.6925000000000001, + "gpu_noise_us_python": 4.967499999999999, + "gpu_bwutil_cpp": 0.6102181948573175, + "gpu_bwutil_python": 0.606349432041513, + "gpu_gap_stddev_us": 4.145380241171651 + } + }, + "advcvtcolor_fakeplanar_nchw_nv_to_rgb3_advanced[InOutDataType=uchar3][shape=256x720x1280][code=YUV2BGR_NV21][inputKind=Tensor][layout=NCHW_FAKE]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3450.5574174123003, + "gpu_time_us_python": 3467.4350699332995, + "gpu_noise_us_cpp": 2.4400000000000004, + "gpu_noise_us_python": 4.470000000000001, + "gpu_bwutil_cpp": 0.5935273296807693, + "gpu_bwutil_python": 0.5906383497352283, + "gpu_gap_stddev_us": 1.184862019450962 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2578.6578441616502, + "gpu_time_us_python": 2593.89072893575, + "gpu_noise_us_cpp": 2.2575, + "gpu_noise_us_python": 5.535, + "gpu_bwutil_cpp": 0.6057929643175061, + "gpu_bwutil_python": 0.6022281285359936, + "gpu_gap_stddev_us": 2.500037933683595 + } + }, + "advcvtcolor_fakeplanar_nchw_nv_to_rgb3_advanced[InOutDataType=uchar3][shape=256x720x1280][code=YUV2RGB_NV12][inputKind=Tensor][layout=NCHW_FAKE]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3450.1955065007996, + "gpu_time_us_python": 3469.3568406039, + "gpu_noise_us_cpp": 3.3099999999999996, + "gpu_noise_us_python": 2.6380000000000003, + "gpu_bwutil_cpp": 0.5935896127251901, + "gpu_bwutil_python": 0.5903111899522494, + "gpu_gap_stddev_us": 1.970051299094415 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2578.427002953075, + "gpu_time_us_python": 2594.680235098075, + "gpu_noise_us_cpp": 1.8424999999999998, + "gpu_noise_us_python": 4.475, + "gpu_bwutil_cpp": 0.6058470501927953, + "gpu_bwutil_python": 0.6020491998439952, + "gpu_gap_stddev_us": 2.727193290243626 + } + }, + "advcvtcolor_fakeplanar_nchw_nv_to_rgb3_advanced[InOutDataType=uchar3][shape=32x2160x3840][code=YUV2BGR_NV21][inputKind=Tensor][layout=NCHW_FAKE]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3865.7891009403597, + "gpu_time_us_python": 3883.93118198098, + "gpu_noise_us_cpp": 3.9840000000000004, + "gpu_noise_us_python": 4.25, + "gpu_bwutil_cpp": 0.5959974278991104, + "gpu_bwutil_python": 0.5932134554553476, + "gpu_gap_stddev_us": 2.308717622339692 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2898.5300446075753, + "gpu_time_us_python": 2916.9317983127753, + "gpu_noise_us_cpp": 2.33, + "gpu_noise_us_python": 4.137499999999999, + "gpu_bwutil_cpp": 0.6063108985126677, + "gpu_bwutil_python": 0.6024802801912894, + "gpu_gap_stddev_us": 2.3813592708689857 + } + }, + "advcvtcolor_fakeplanar_nchw_nv_to_rgb3_advanced[InOutDataType=uchar3][shape=32x2160x3840][code=YUV2RGB_NV12][inputKind=Tensor][layout=NCHW_FAKE]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3866.46964586694, + "gpu_time_us_python": 3883.4585637312002, + "gpu_noise_us_cpp": 4.476, + "gpu_noise_us_python": 3.286, + "gpu_bwutil_cpp": 0.5958924612710428, + "gpu_bwutil_python": 0.5932856258597401, + "gpu_gap_stddev_us": 0.5281212908532237 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2898.8953657613247, + "gpu_time_us_python": 2914.827580737075, + "gpu_noise_us_cpp": 2.5525, + "gpu_noise_us_python": 3.32, + "gpu_bwutil_cpp": 0.6062327599203932, + "gpu_bwutil_python": 0.6029232938631087, + "gpu_gap_stddev_us": 1.7430120958583777 + } + } + } + }, + "advcvtcolor_nv_to_rgb4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "256x720x1280", + "128x1080x1920", + "32x2160x3840" + ], + "code": [ + "YUV2RGB_NV12", + "YUV2BGR_NV21" + ], + "inputKind": [ + "Tensor" + ], + "layout": [ + "NHWC" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AdvCvtColor RGBA8 nv rgb4 (advanced)." + }, + "baselines": { + "advcvtcolor_nv_to_rgb4_advanced[InOutDataType=uchar4][shape=128x1080x1920][code=YUV2BGR_NV21][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1892.43052185024, + "gpu_time_us_python": 1903.6330605997998, + "gpu_noise_us_cpp": 2.65, + "gpu_noise_us_python": 4.662, + "gpu_bwutil_cpp": 0.4960112996899242, + "gpu_bwutil_python": 0.49309283290079814, + "gpu_gap_stddev_us": 2.018167298381171 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1645.3319831856747, + "gpu_time_us_python": 1655.221602974525, + "gpu_noise_us_cpp": 1.5199999999999998, + "gpu_noise_us_python": 5.1475, + "gpu_bwutil_cpp": 0.43513559217447845, + "gpu_bwutil_python": 0.43253872397676985, + "gpu_gap_stddev_us": 1.923558681483909 + } + }, + "advcvtcolor_nv_to_rgb4_advanced[InOutDataType=uchar4][shape=256x720x1280][code=YUV2BGR_NV21][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1682.11037648598, + "gpu_time_us_python": 1692.4269484323002, + "gpu_noise_us_cpp": 1.8039999999999998, + "gpu_noise_us_python": 2.7779999999999996, + "gpu_bwutil_cpp": 0.49602625618838747, + "gpu_bwutil_python": 0.49300300809894493, + "gpu_gap_stddev_us": 1.21461725521029 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1462.249725049825, + "gpu_time_us_python": 1473.6294025292, + "gpu_noise_us_cpp": 1.9649999999999999, + "gpu_noise_us_python": 2.8024999999999998, + "gpu_bwutil_cpp": 0.4352155123138632, + "gpu_bwutil_python": 0.43185681191838743, + "gpu_gap_stddev_us": 1.8762482813541421 + } + }, + "advcvtcolor_nv_to_rgb4_advanced[InOutDataType=uchar4][shape=32x2160x3840][code=YUV2BGR_NV21][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1896.0011744137398, + "gpu_time_us_python": 1907.39807239728, + "gpu_noise_us_cpp": 2.676, + "gpu_noise_us_python": 3.382, + "gpu_bwutil_cpp": 0.4950772033891293, + "gpu_bwutil_python": 0.4921195224396112, + "gpu_gap_stddev_us": 1.2924962437538996 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1646.152727131525, + "gpu_time_us_python": 1656.0422261977, + "gpu_noise_us_cpp": 1.31, + "gpu_noise_us_python": 3.1625, + "gpu_bwutil_cpp": 0.434919228406681, + "gpu_bwutil_python": 0.4323211566229733, + "gpu_gap_stddev_us": 0.8493046701787125 + } + }, + "advcvtcolor_nv_to_rgb4_advanced[InOutDataType=uchar4][shape=128x1080x1920][code=YUV2RGB_NV12][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1893.73765056808, + "gpu_time_us_python": 1903.6113384997602, + "gpu_noise_us_cpp": 2.2939999999999996, + "gpu_noise_us_python": 3.066, + "gpu_bwutil_cpp": 0.4956691399603379, + "gpu_bwutil_python": 0.4930982220203389, + "gpu_gap_stddev_us": 0.2184107343521327 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1645.23009635635, + "gpu_time_us_python": 1656.7442142079, + "gpu_noise_us_cpp": 1.1, + "gpu_noise_us_python": 4.585, + "gpu_bwutil_cpp": 0.43516272101519166, + "gpu_bwutil_python": 0.4321414154881139, + "gpu_gap_stddev_us": 2.177455329415998 + } + }, + "advcvtcolor_nv_to_rgb4_advanced[InOutDataType=uchar4][shape=256x720x1280][code=YUV2RGB_NV12][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1681.7887475826199, + "gpu_time_us_python": 1692.0908173999, + "gpu_noise_us_cpp": 3.002, + "gpu_noise_us_python": 2.9539999999999997, + "gpu_bwutil_cpp": 0.49612106734401384, + "gpu_bwutil_python": 0.4931003429230967, + "gpu_gap_stddev_us": 0.6189042974410057 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1461.979373865925, + "gpu_time_us_python": 1473.1400938349498, + "gpu_noise_us_cpp": 1.055, + "gpu_noise_us_python": 2.6825, + "gpu_bwutil_cpp": 0.4352965629759749, + "gpu_bwutil_python": 0.4319953231144955, + "gpu_gap_stddev_us": 2.4440213932863593 + } + }, + "advcvtcolor_nv_to_rgb4_advanced[InOutDataType=uchar4][shape=32x2160x3840][code=YUV2RGB_NV12][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1896.9134688376998, + "gpu_time_us_python": 1907.4507758638, + "gpu_noise_us_cpp": 2.58, + "gpu_noise_us_python": 3.55, + "gpu_bwutil_cpp": 0.49483923530458734, + "gpu_bwutil_python": 0.4921057053437366, + "gpu_gap_stddev_us": 0.874900050616768 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1646.3082627045999, + "gpu_time_us_python": 1657.681703834975, + "gpu_noise_us_cpp": 1.1424999999999998, + "gpu_noise_us_python": 5.9475, + "gpu_bwutil_cpp": 0.43487725617764345, + "gpu_bwutil_python": 0.4318910583215574, + "gpu_gap_stddev_us": 2.55234482795529 + } + } + } + }, + "advcvtcolor_planar_nchw_nv_to_rgb4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "256x720x1280", + "128x1080x1920", + "32x2160x3840" + ], + "code": [ + "YUV2RGB_NV12", + "YUV2BGR_NV21" + ], + "inputKind": [ + "Tensor" + ], + "layout": [ + "NCHW" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AdvCvtColor RGBA8 nv rgb4 (advanced) (NCHW)." + }, + "baselines": { + "advcvtcolor_planar_nchw_nv_to_rgb4_advanced[InOutDataType=uchar4][shape=128x1080x1920][code=YUV2BGR_NV21][inputKind=Tensor][layout=NCHW]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1632.2826207457802, + "gpu_time_us_python": 1641.7637909159598, + "gpu_noise_us_cpp": 2.7640000000000002, + "gpu_noise_us_python": 2.5740000000000003, + "gpu_bwutil_cpp": 0.5750639053266967, + "gpu_bwutil_python": 0.5717429536859338, + "gpu_gap_stddev_us": 0.7427490346747654 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1278.997905874325, + "gpu_time_us_python": 1289.0344361056, + "gpu_noise_us_cpp": 1.1375, + "gpu_noise_us_python": 3.1325, + "gpu_bwutil_cpp": 0.5598445503416851, + "gpu_bwutil_python": 0.5554840590266681, + "gpu_gap_stddev_us": 1.2704733358167302 + } + }, + "advcvtcolor_planar_nchw_nv_to_rgb4_advanced[InOutDataType=uchar4][shape=128x1080x1920][code=YUV2RGB_NV12][inputKind=Tensor][layout=NCHW]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1635.91428878256, + "gpu_time_us_python": 1644.6026354064002, + "gpu_noise_us_cpp": 2.254, + "gpu_noise_us_python": 2.8419999999999996, + "gpu_bwutil_cpp": 0.5737872915451332, + "gpu_bwutil_python": 0.5707560568846104, + "gpu_gap_stddev_us": 0.4161097304186837 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1278.2926962382749, + "gpu_time_us_python": 1287.9726570634748, + "gpu_noise_us_cpp": 1.9, + "gpu_noise_us_python": 2.6550000000000002, + "gpu_bwutil_cpp": 0.5601540460383836, + "gpu_bwutil_python": 0.5559468470382829, + "gpu_gap_stddev_us": 1.110328034903262 + } + }, + "advcvtcolor_planar_nchw_nv_to_rgb4_advanced[InOutDataType=uchar4][shape=256x720x1280][code=YUV2BGR_NV21][inputKind=Tensor][layout=NCHW]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1446.55569950274, + "gpu_time_us_python": 1456.60666171888, + "gpu_noise_us_cpp": 1.94, + "gpu_noise_us_python": 3.6519999999999997, + "gpu_bwutil_cpp": 0.5767979737270775, + "gpu_bwutil_python": 0.5728179540972966, + "gpu_gap_stddev_us": 0.6202520233513819 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1133.8282699888998, + "gpu_time_us_python": 1143.740525371575, + "gpu_noise_us_cpp": 1.3125, + "gpu_noise_us_python": 2.6075, + "gpu_bwutil_cpp": 0.5613601706568178, + "gpu_bwutil_python": 0.5565032077848208, + "gpu_gap_stddev_us": 1.7265317808915084 + } + }, + "advcvtcolor_planar_nchw_nv_to_rgb4_advanced[InOutDataType=uchar4][shape=256x720x1280][code=YUV2RGB_NV12][inputKind=Tensor][layout=NCHW]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1445.49856356434, + "gpu_time_us_python": 1455.61123171512, + "gpu_noise_us_cpp": 2.618, + "gpu_noise_us_python": 5.064, + "gpu_bwutil_cpp": 0.5772198515135734, + "gpu_bwutil_python": 0.5732097698307638, + "gpu_gap_stddev_us": 1.0081695555934933 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1133.565194703875, + "gpu_time_us_python": 1143.3504258747, + "gpu_noise_us_cpp": 1.195, + "gpu_noise_us_python": 2.7075, + "gpu_bwutil_cpp": 0.5614925246927657, + "gpu_bwutil_python": 0.5566928942289271, + "gpu_gap_stddev_us": 1.360654096916967 + } + }, + "advcvtcolor_planar_nchw_nv_to_rgb4_advanced[InOutDataType=uchar4][shape=32x2160x3840][code=YUV2BGR_NV21][inputKind=Tensor][layout=NCHW]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1811.38523632978, + "gpu_time_us_python": 1824.59274772026, + "gpu_noise_us_cpp": 2.7039999999999997, + "gpu_noise_us_python": 5.6339999999999995, + "gpu_bwutil_cpp": 0.5182039837792922, + "gpu_bwutil_python": 0.5144533966481366, + "gpu_gap_stddev_us": 1.9685841891740976 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1291.588465643775, + "gpu_time_us_python": 1305.974198388825, + "gpu_noise_us_cpp": 1.2650000000000001, + "gpu_noise_us_python": 4.13, + "gpu_bwutil_cpp": 0.5544265309802203, + "gpu_bwutil_python": 0.5483267613722794, + "gpu_gap_stddev_us": 2.54278856655742 + } + }, + "advcvtcolor_planar_nchw_nv_to_rgb4_advanced[InOutDataType=uchar4][shape=32x2160x3840][code=YUV2RGB_NV12][inputKind=Tensor][layout=NCHW]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1821.7999052310001, + "gpu_time_us_python": 1826.7189214900598, + "gpu_noise_us_cpp": 2.832, + "gpu_noise_us_python": 3.4219999999999997, + "gpu_bwutil_cpp": 0.515241480497691, + "gpu_bwutil_python": 0.513854334199061, + "gpu_gap_stddev_us": 2.656560681734491 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1285.2417931852, + "gpu_time_us_python": 1305.412335600975, + "gpu_noise_us_cpp": 4.205, + "gpu_noise_us_python": 4.805, + "gpu_bwutil_cpp": 0.5571535308068831, + "gpu_bwutil_python": 0.5485642902671157, + "gpu_gap_stddev_us": 3.192620002353962 + } + } + } + }, + "advcvtcolor_fakeplanar_nchw_nv_to_rgb4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "256x720x1280", + "128x1080x1920", + "32x2160x3840" + ], + "code": [ + "YUV2RGB_NV12", + "YUV2BGR_NV21" + ], + "inputKind": [ + "Tensor" + ], + "layout": [ + "NCHW_FAKE" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AdvCvtColor RGBA8 nv rgb4 (advanced) (NCHW_FAKE)." + }, + "baselines": { + "advcvtcolor_fakeplanar_nchw_nv_to_rgb4_advanced[InOutDataType=uchar4][shape=128x1080x1920][code=YUV2BGR_NV21][inputKind=Tensor][layout=NCHW_FAKE]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4355.5451310913795, + "gpu_time_us_python": 4370.11421795544, + "gpu_noise_us_cpp": 2.978, + "gpu_noise_us_python": 3.1, + "gpu_bwutil_cpp": 0.6465322447643811, + "gpu_bwutil_python": 0.6443769664696103, + "gpu_gap_stddev_us": 1.2889585277884439 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 3643.1768044181003, + "gpu_time_us_python": 3659.9594207777, + "gpu_noise_us_cpp": 1.865, + "gpu_noise_us_python": 7.195, + "gpu_bwutil_cpp": 0.5895477290393735, + "gpu_bwutil_python": 0.5868429125104004, + "gpu_gap_stddev_us": 2.610891014931179 + } + }, + "advcvtcolor_fakeplanar_nchw_nv_to_rgb4_advanced[InOutDataType=uchar4][shape=128x1080x1920][code=YUV2RGB_NV12][inputKind=Tensor][layout=NCHW_FAKE]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4352.4484585070795, + "gpu_time_us_python": 4373.306299078, + "gpu_noise_us_cpp": 2.662, + "gpu_noise_us_python": 3.55, + "gpu_bwutil_cpp": 0.6469922346370932, + "gpu_bwutil_python": 0.643906688329189, + "gpu_gap_stddev_us": 2.2568249611697 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 3642.1866520961, + "gpu_time_us_python": 3660.17773445095, + "gpu_noise_us_cpp": 1.6475, + "gpu_noise_us_python": 3.12, + "gpu_bwutil_cpp": 0.5897081851752497, + "gpu_bwutil_python": 0.5868068686366579, + "gpu_gap_stddev_us": 4.052259874127372 + } + }, + "advcvtcolor_fakeplanar_nchw_nv_to_rgb4_advanced[InOutDataType=uchar4][shape=256x720x1280][code=YUV2BGR_NV21][inputKind=Tensor][layout=NCHW_FAKE]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3851.12536356992, + "gpu_time_us_python": 3868.9586910834005, + "gpu_noise_us_cpp": 3.5599999999999996, + "gpu_noise_us_python": 3.2640000000000002, + "gpu_bwutil_cpp": 0.6499690067971239, + "gpu_bwutil_python": 0.6469730496958354, + "gpu_gap_stddev_us": 1.045104523949515 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 3224.8544608935, + "gpu_time_us_python": 3240.8717367654754, + "gpu_noise_us_cpp": 3.7824999999999998, + "gpu_noise_us_python": 5.59, + "gpu_bwutil_cpp": 0.5920203577306395, + "gpu_bwutil_python": 0.5890944541583877, + "gpu_gap_stddev_us": 1.391785863016066 + } + }, + "advcvtcolor_fakeplanar_nchw_nv_to_rgb4_advanced[InOutDataType=uchar4][shape=256x720x1280][code=YUV2RGB_NV12][inputKind=Tensor][layout=NCHW_FAKE]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3851.7996347866997, + "gpu_time_us_python": 3868.4325137504798, + "gpu_noise_us_cpp": 2.238, + "gpu_noise_us_python": 3.63, + "gpu_bwutil_cpp": 0.6498551219571557, + "gpu_bwutil_python": 0.6470610272196986, + "gpu_gap_stddev_us": 1.3201231305373593 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 3224.7978387733997, + "gpu_time_us_python": 3239.701082358675, + "gpu_noise_us_cpp": 1.585, + "gpu_noise_us_python": 3.4525000000000006, + "gpu_bwutil_cpp": 0.5920310559649713, + "gpu_bwutil_python": 0.5893046577744191, + "gpu_gap_stddev_us": 3.147115195153357 + } + }, + "advcvtcolor_fakeplanar_nchw_nv_to_rgb4_advanced[InOutDataType=uchar4][shape=32x2160x3840][code=YUV2BGR_NV21][inputKind=Tensor][layout=NCHW_FAKE]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4496.219440868879, + "gpu_time_us_python": 4515.057325363099, + "gpu_noise_us_cpp": 3.396, + "gpu_noise_us_python": 3.5, + "gpu_bwutil_cpp": 0.6263040215618344, + "gpu_bwutil_python": 0.623691046313119, + "gpu_gap_stddev_us": 1.1204451307644945 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 3632.61241786965, + "gpu_time_us_python": 3650.2998315769, + "gpu_noise_us_cpp": 1.51, + "gpu_noise_us_python": 4.4275, + "gpu_bwutil_cpp": 0.591262399718863, + "gpu_bwutil_python": 0.5883983415699664, + "gpu_gap_stddev_us": 1.462385091984388 + } + }, + "advcvtcolor_fakeplanar_nchw_nv_to_rgb4_advanced[InOutDataType=uchar4][shape=32x2160x3840][code=YUV2RGB_NV12][inputKind=Tensor][layout=NCHW_FAKE]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4498.05901391162, + "gpu_time_us_python": 4514.9695685931, + "gpu_noise_us_cpp": 3.3040000000000007, + "gpu_noise_us_python": 5.604000000000001, + "gpu_bwutil_cpp": 0.6260479048145888, + "gpu_bwutil_python": 0.6237032087295049, + "gpu_gap_stddev_us": 1.5871211199448227 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 3632.3046449918497, + "gpu_time_us_python": 3649.377739083875, + "gpu_noise_us_cpp": 1.7425000000000002, + "gpu_noise_us_python": 4.404999999999999, + "gpu_bwutil_cpp": 0.591312290261127, + "gpu_bwutil_python": 0.5885453220969257, + "gpu_gap_stddev_us": 1.1202531845369312 + } + } + } + } + } +} diff --git a/bench/config/operators/autocontrast.json b/bench/config/operators/autocontrast.json new file mode 100644 index 000000000..49411fd16 --- /dev/null +++ b/bench/config/operators/autocontrast.json @@ -0,0 +1,538 @@ +{ + "benchmark": "autocontrast", + "configs": { + "autocontrast_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "28x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 1000, + "metadata": { + "description": "Auto Contrast RGB8 interleaved + native planar. N=28 is the nearest round batch that keeps interleaved A100/H100 timings in the 1-2ms window." + }, + "baselines": { + "autocontrast_uchar3_basic[InOutDataType=uchar3][shape=28x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1404.17845656764, + "gpu_time_us_python": 1415.2112130459, + "gpu_noise_us_cpp": 1.526, + "gpu_noise_us_python": 3.6739999999999995, + "gpu_bwutil_cpp": 0.15952423399984264, + "gpu_bwutil_python": 0.15828099294846898 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1241.6292531637398, + "gpu_time_us_python": 1251.59094137712, + "gpu_noise_us_cpp": 0.916, + "gpu_noise_us_python": 2.4059999999999997, + "gpu_bwutil_cpp": 0.13761121629527878, + "gpu_bwutil_python": 0.13651486468673535 + } + }, + "autocontrast_uchar3_basic[InOutDataType=uchar3][shape=28x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1072.1814488022399, + "gpu_time_us_python": 1080.8478930900399, + "gpu_noise_us_cpp": 1.578, + "gpu_noise_us_python": 1.9259999999999997, + "gpu_bwutil_cpp": 0.20892037502605426, + "gpu_bwutil_python": 0.20724494154949719 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1035.1131421159803, + "gpu_time_us_python": 1045.5516790944198, + "gpu_noise_us_cpp": 0.676, + "gpu_noise_us_python": 3.2380000000000004, + "gpu_bwutil_cpp": 0.16506778262101957, + "gpu_bwutil_python": 0.16341552114075708 + } + } + } + }, + "autocontrast_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "24x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Auto Contrast RGBf32 interleaved + planar. N=24 is a round batch calibrated to the 1-2ms interleaved A100/H100 window." + }, + "baselines": { + "autocontrast_float3_basic[InOutDataType=float3][shape=24x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1891.9932536259398, + "gpu_time_us_python": 1904.3037471632001, + "gpu_noise_us_cpp": 1.6320000000000001, + "gpu_noise_us_python": 2.636, + "gpu_bwutil_cpp": 0.4059216704855137, + "gpu_bwutil_python": 0.403297777299841 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1617.80825096816, + "gpu_time_us_python": 1628.5120455458, + "gpu_noise_us_cpp": 0.866, + "gpu_noise_us_python": 2.8040000000000003, + "gpu_bwutil_cpp": 0.3620775635758083, + "gpu_bwutil_python": 0.3596964260299494 + } + }, + "autocontrast_float3_basic[InOutDataType=float3][shape=24x1080x1920][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2064.12718315104, + "gpu_time_us_python": 2077.0783208519397, + "gpu_noise_us_cpp": 2.556, + "gpu_noise_us_python": 5.674, + "gpu_bwutil_cpp": 0.37207077044350534, + "gpu_bwutil_python": 0.36975308859834705 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1764.2876652293598, + "gpu_time_us_python": 1778.0021229484398, + "gpu_noise_us_cpp": 1.8760000000000001, + "gpu_noise_us_python": 4.518, + "gpu_bwutil_cpp": 0.33201723991679427, + "gpu_bwutil_python": 0.3294555237678763 + } + }, + "autocontrast_float3_basic[InOutDataType=float3][shape=24x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1349.12332206638, + "gpu_time_us_python": 1358.2875949802399, + "gpu_noise_us_cpp": 1.5779999999999998, + "gpu_noise_us_python": 2.564, + "gpu_bwutil_cpp": 0.5692589033865552, + "gpu_bwutil_python": 0.5654184831938065 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1099.2848356454, + "gpu_time_us_python": 1108.86503934638, + "gpu_noise_us_cpp": 1.122, + "gpu_noise_us_python": 3.2019999999999995, + "gpu_bwutil_cpp": 0.5328630195996908, + "gpu_bwutil_python": 0.5282601406844484 + } + }, + "autocontrast_float3_basic[InOutDataType=float3][shape=24x1080x1920][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1402.13749572292, + "gpu_time_us_python": 1414.6928839007, + "gpu_noise_us_cpp": 2.06, + "gpu_noise_us_python": 3.4239999999999995, + "gpu_bwutil_cpp": 0.5477366512774248, + "gpu_bwutil_python": 0.5428768004129094 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1168.5827891027, + "gpu_time_us_python": 1181.37808301056, + "gpu_noise_us_cpp": 2.0660000000000003, + "gpu_noise_us_python": 3.874, + "gpu_bwutil_cpp": 0.5012642477838612, + "gpu_bwutil_python": 0.495834911826363 + } + } + } + }, + "autocontrast_uchar3_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "28x1080x1920" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 1000, + "metadata": { + "description": "Auto Contrast RGB8 native-planar Tensor comparison for the advanced fake-planar row." + }, + "baselines": { + "autocontrast_uchar3_nchw_advanced[InOutDataType=uchar3][shape=28x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1404.8519798998, + "gpu_time_us_python": 1417.40632589248, + "gpu_noise_us_cpp": 1.03, + "gpu_noise_us_python": 2.858, + "gpu_bwutil_cpp": 0.1594474658506537, + "gpu_bwutil_python": 0.15803543606075368 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1241.3568339088802, + "gpu_time_us_python": 1250.50974758568, + "gpu_noise_us_cpp": 0.826, + "gpu_noise_us_python": 2.118, + "gpu_bwutil_cpp": 0.13764103842121272, + "gpu_bwutil_python": 0.13663413136702512 + } + } + } + }, + "autocontrast_uint16_advanced": { + "tier": "advanced", + "dtypes": [ + "uint16" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Auto Contrast U16 1-channel interleaved (single-channel -> advanced per RGB R3). N=64 is a round batch calibrated to the 1-2ms A100/H100 window." + }, + "baselines": { + "autocontrast_uint16_advanced[InOutDataType=uint16][shape=64x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1232.2890015036398, + "gpu_time_us_python": 1242.6432824135397, + "gpu_noise_us_cpp": 1.4120000000000001, + "gpu_noise_us_python": 3.8659999999999997, + "gpu_bwutil_cpp": 0.27699159238221527, + "gpu_bwutil_python": 0.27468496043898255 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1013.8850233139601, + "gpu_time_us_python": 1021.62270642116, + "gpu_noise_us_cpp": 1.6640000000000001, + "gpu_noise_us_python": 6.2780000000000005, + "gpu_bwutil_cpp": 0.25679180846298677, + "gpu_bwutil_python": 0.2548341878706567 + } + }, + "autocontrast_uint16_advanced[InOutDataType=uint16][shape=64x1080x1920][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1394.0667551922002, + "gpu_time_us_python": 1411.65052208502, + "gpu_noise_us_cpp": 3.5780000000000003, + "gpu_noise_us_python": 7.025999999999999, + "gpu_bwutil_cpp": 0.24485048625362663, + "gpu_bwutil_python": 0.2418038031533411 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1122.6557897436, + "gpu_time_us_python": 1135.67295644358, + "gpu_noise_us_cpp": 2.19, + "gpu_noise_us_python": 4.854000000000001, + "gpu_bwutil_cpp": 0.2319040561964099, + "gpu_bwutil_python": 0.22924273849105012 + } + } + } + }, + "autocontrast_fake_planar_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "28x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 1000, + "metadata": { + "description": "Auto Contrast RGB8 fake-planar (tensor-only; ~3x traffic). Shares uchar3 N=28 (BEN-SIZE); fake-planar may run longer than 1-2ms." + }, + "baselines": { + "autocontrast_fake_planar_uchar3_advanced[InOutDataType=uchar3][shape=28x1080x1920][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2241.08611174986, + "gpu_time_us_python": 2258.5620991817996, + "gpu_noise_us_cpp": 2.3619999999999997, + "gpu_noise_us_python": 5.276000000000001, + "gpu_bwutil_cpp": 0.2998547608290148, + "gpu_bwutil_python": 0.2975351173924471 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2050.13923888828, + "gpu_time_us_python": 2065.56057956646, + "gpu_noise_us_cpp": 0.908, + "gpu_noise_us_python": 3.838, + "gpu_bwutil_cpp": 0.25001560066163614, + "gpu_bwutil_python": 0.24814583478962157 + } + } + } + }, + "autocontrast_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "20x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Auto Contrast RGBA8 interleaved + planar (uchar4 planar var-shape unsupported). N=20 targets the 1-2ms reference-GPU window across A100/H100 after optimization." + }, + "baselines": { + "autocontrast_uchar4_advanced[InOutDataType=uchar4][shape=20x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1324.98769293382, + "gpu_time_us_python": 1335.60119385954, + "gpu_noise_us_cpp": 1.114, + "gpu_noise_us_python": 2.94, + "gpu_bwutil_cpp": 0.16100787867871014, + "gpu_bwutil_python": 0.1597285298609866 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1162.7810877677798, + "gpu_time_us_python": 1174.0307420520398, + "gpu_noise_us_cpp": 4.04, + "gpu_noise_us_python": 1.846, + "gpu_bwutil_cpp": 0.1399408654701288, + "gpu_bwutil_python": 0.13860204851773808 + } + }, + "autocontrast_uchar4_advanced[InOutDataType=uchar4][shape=20x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1001.5969071171199, + "gpu_time_us_python": 1011.89592436322, + "gpu_noise_us_cpp": 1.374, + "gpu_noise_us_python": 3.41, + "gpu_bwutil_cpp": 0.21299339682866147, + "gpu_bwutil_python": 0.21082639727277092 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1021.5311864151001, + "gpu_time_us_python": 1030.1709959836, + "gpu_noise_us_cpp": 0.9960000000000001, + "gpu_noise_us_python": 3.4019999999999997, + "gpu_bwutil_cpp": 0.15929695489529228, + "gpu_bwutil_python": 0.15795835378258 + } + } + } + }, + "autocontrast_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "20x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Auto Contrast RGBAf32 interleaved + planar. N=20 is a round batch calibrated to the 1-2ms interleaved A100/H100 window." + }, + "baselines": { + "autocontrast_float4_advanced[InOutDataType=float4][shape=20x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2086.8368228276004, + "gpu_time_us_python": 2096.81065877274, + "gpu_noise_us_cpp": 1.142, + "gpu_noise_us_python": 1.9460000000000002, + "gpu_bwutil_cpp": 0.4089124144225016, + "gpu_bwutil_python": 0.4069676489796247 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1781.72524251834, + "gpu_time_us_python": 1792.2516594673002, + "gpu_noise_us_cpp": 0.8799999999999999, + "gpu_noise_us_python": 2.772, + "gpu_bwutil_cpp": 0.3652956864055932, + "gpu_bwutil_python": 0.3631494886447391 + } + }, + "autocontrast_float4_advanced[InOutDataType=float4][shape=20x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1475.2129741275398, + "gpu_time_us_python": 1485.49480478534, + "gpu_noise_us_cpp": 2.116, + "gpu_noise_us_python": 3.144, + "gpu_bwutil_cpp": 0.5784479395202289, + "gpu_bwutil_python": 0.5744472029774097 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1160.26646601836, + "gpu_time_us_python": 1169.83232191884, + "gpu_noise_us_cpp": 1.436, + "gpu_noise_us_python": 2.8460000000000005, + "gpu_bwutil_cpp": 0.5609584049146276, + "gpu_bwutil_python": 0.5563723500102131 + } + } + } + }, + "autocontrast_float4_nhwc_varshape_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "20x1080x1920" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Auto Contrast RGBAf32 interleaved VarShape. Shares float4 N=20 (BEN-SIZE); split from the Tensor profile to avoid an unintended layout/input-kind cross-product." + }, + "baselines": { + "autocontrast_float4_nhwc_varshape_advanced[InOutDataType=float4][shape=20x1080x1920][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1494.5905777406401, + "gpu_time_us_python": 1504.8159749087001, + "gpu_noise_us_cpp": 3.6800000000000006, + "gpu_noise_us_python": 3.1, + "gpu_bwutil_cpp": 0.5709491205862232, + "gpu_bwutil_python": 0.5670707056215945 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1222.5391718087599, + "gpu_time_us_python": 1235.33604422196, + "gpu_noise_us_cpp": 1.8519999999999999, + "gpu_noise_us_python": 3.3579999999999997, + "gpu_bwutil_cpp": 0.5323770780753401, + "gpu_bwutil_python": 0.5268683665539091 + } + } + } + }, + "autocontrast_fake_planar_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "20x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Auto Contrast RGBAf32 fake-planar (tensor-only). Shares float4 N=20 (BEN-SIZE) and may run longer than 1-2ms." + }, + "baselines": { + "autocontrast_fake_planar_float4_advanced[InOutDataType=float4][shape=20x1080x1920][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4114.6602740052795, + "gpu_time_us_python": 4132.44434888242, + "gpu_noise_us_cpp": 3.861999999999999, + "gpu_noise_us_python": 6.098000000000001, + "gpu_bwutil_cpp": 0.6221656172880706, + "gpu_bwutil_python": 0.6194882881807541 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3349.62543360388, + "gpu_time_us_python": 3364.69290606176, + "gpu_noise_us_cpp": 5.1, + "gpu_noise_us_python": 6.434, + "gpu_bwutil_cpp": 0.5829168010874378, + "gpu_bwutil_python": 0.5803054828646366 + } + } + } + } + } +} diff --git a/bench/config/operators/averageblur.json b/bench/config/operators/averageblur.json new file mode 100644 index 000000000..a70acdcdf --- /dev/null +++ b/bench/config/operators/averageblur.json @@ -0,0 +1,1524 @@ +{ + "benchmark": "averageblur", + "configs": { + "averageblur_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x720x1280" + ], + "kernelSize": [ + "5x5" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AverageBlur RGB8 (basic)." + }, + "baselines": { + "averageblur_uchar3_basic[InOutDataType=uchar3][shape=32x720x1280][kernelSize=5x5][border=REPLICATE][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1443.82161323302, + "gpu_time_us_python": 1454.2682953980602, + "gpu_noise_us_cpp": 1.114, + "gpu_noise_us_python": 2.266, + "gpu_bwutil_cpp": 0.07880341620955963, + "gpu_bwutil_python": 0.07823742879077303 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1008.2277382368, + "gpu_time_us_python": 1018.7118121105599, + "gpu_noise_us_cpp": 2.946, + "gpu_noise_us_python": 4.247999999999999, + "gpu_bwutil_cpp": 0.08608550666191017, + "gpu_bwutil_python": 0.08520008539484687 + } + }, + "averageblur_uchar3_basic[InOutDataType=uchar3][shape=32x720x1280][kernelSize=5x5][border=REPLICATE][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1333.4074847254801, + "gpu_time_us_python": 1345.98301625372, + "gpu_noise_us_cpp": 1.4, + "gpu_noise_us_python": 2.9080000000000004, + "gpu_bwutil_cpp": 0.0853289595567992, + "gpu_bwutil_python": 0.084531809139799 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 952.4370202443799, + "gpu_time_us_python": 964.6596920560199, + "gpu_noise_us_cpp": 0.8320000000000001, + "gpu_noise_us_python": 3.0759999999999996, + "gpu_bwutil_cpp": 0.09112949113403632, + "gpu_bwutil_python": 0.08997536470737301 + } + } + } + }, + "averageblur_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "32x720x1280" + ], + "kernelSize": [ + "5x5" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AverageBlur RGBA8 (advanced)." + }, + "baselines": { + "averageblur_uchar4_advanced[InOutDataType=uchar4][shape=32x720x1280][kernelSize=5x5][border=REPLICATE][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1853.25827517406, + "gpu_time_us_python": 1864.60525109656, + "gpu_noise_us_cpp": 1.246, + "gpu_noise_us_python": 4.434, + "gpu_bwutil_cpp": 0.08185800756483505, + "gpu_bwutil_python": 0.08135997807082233 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1362.9599527064997, + "gpu_time_us_python": 1373.7059055722798, + "gpu_noise_us_cpp": 1.7420000000000002, + "gpu_noise_us_python": 1.83, + "gpu_bwutil_cpp": 0.08490905919088751, + "gpu_bwutil_python": 0.08424730582054421 + } + }, + "averageblur_uchar4_advanced[InOutDataType=uchar4][shape=32x720x1280][kernelSize=5x5][border=REPLICATE][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1739.11466913086, + "gpu_time_us_python": 1752.5377944308202, + "gpu_noise_us_cpp": 1.556, + "gpu_noise_us_python": 3.654, + "gpu_bwutil_cpp": 0.0872306796398519, + "gpu_bwutil_python": 0.08656270137309266 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1115.56304890704, + "gpu_time_us_python": 1128.74057935066, + "gpu_noise_us_cpp": 1.02, + "gpu_noise_us_python": 4.1899999999999995, + "gpu_bwutil_cpp": 0.10373842350103739, + "gpu_bwutil_python": 0.10252737987544511 + } + } + } + }, + "averageblur_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x720x1280" + ], + "kernelSize": [ + "5x5" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AverageBlur RGBf32 (basic)." + }, + "baselines": { + "averageblur_float3_basic[InOutDataType=float3][shape=32x720x1280][kernelSize=5x5][border=REPLICATE][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1077.99802961884, + "gpu_time_us_python": 1089.29726335018, + "gpu_noise_us_cpp": 1.076, + "gpu_noise_us_python": 2.662, + "gpu_bwutil_cpp": 0.4221852424351731, + "gpu_bwutil_python": 0.4178075399832659 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 996.0029576129, + "gpu_time_us_python": 1006.19887162462, + "gpu_noise_us_cpp": 1.3199999999999998, + "gpu_noise_us_python": 2.516, + "gpu_bwutil_cpp": 0.3485733203076025, + "gpu_bwutil_python": 0.3450454579423682 + } + } + } + }, + "averageblur_float3_varshape_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x720x1280" + ], + "kernelSize": [ + "5x5" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AverageBlur RGBf32 VarShape (advanced)." + }, + "baselines": { + "averageblur_float3_varshape_advanced[InOutDataType=float3][shape=32x720x1280][kernelSize=5x5][border=REPLICATE][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1221.04018227578, + "gpu_time_us_python": 1233.40245948108, + "gpu_noise_us_cpp": 1.738, + "gpu_noise_us_python": 4.062, + "gpu_bwutil_cpp": 0.3727262048022615, + "gpu_bwutil_python": 0.3689907658189388 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1078.84088237848, + "gpu_time_us_python": 1092.0413649794598, + "gpu_noise_us_cpp": 0.984, + "gpu_noise_us_python": 2.916, + "gpu_bwutil_cpp": 0.3218094033755571, + "gpu_bwutil_python": 0.31791778588320485 + } + } + } + }, + "averageblur_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "32x720x1280" + ], + "kernelSize": [ + "5x5" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AverageBlur RGBAf32 (advanced)." + }, + "baselines": { + "averageblur_float4_advanced[InOutDataType=float4][shape=32x720x1280][kernelSize=5x5][border=REPLICATE][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1301.0655177611602, + "gpu_time_us_python": 1312.4622919057201, + "gpu_noise_us_cpp": 1.546, + "gpu_noise_us_python": 3.2700000000000005, + "gpu_bwutil_cpp": 0.4664003453738399, + "gpu_bwutil_python": 0.46235099660502765 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1049.6970382867598, + "gpu_time_us_python": 1060.81470869356, + "gpu_noise_us_cpp": 0.954, + "gpu_noise_us_python": 2.718, + "gpu_bwutil_cpp": 0.4409737517647782, + "gpu_bwutil_python": 0.4363562574957477 + } + }, + "averageblur_float4_advanced[InOutDataType=float4][shape=32x720x1280][kernelSize=5x5][border=REPLICATE][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1243.6104433089, + "gpu_time_us_python": 1256.7120277401798, + "gpu_noise_us_cpp": 1.496, + "gpu_noise_us_python": 2.768, + "gpu_bwutil_cpp": 0.4879482888826054, + "gpu_bwutil_python": 0.4828598614390522 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1089.1035863204402, + "gpu_time_us_python": 1101.9384418108798, + "gpu_noise_us_cpp": 0.866, + "gpu_noise_us_python": 3.092, + "gpu_bwutil_cpp": 0.4250246725162842, + "gpu_bwutil_python": 0.42006453635453245 + } + } + } + }, + "averageblur_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "64x720x1280", + "32x1080x1920", + "8x2160x3840" + ], + "kernelSize": [ + "5x5" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AverageBlur single-channel (advanced)." + }, + "baselines": { + "averageblur_scalar_advanced[InOutDataType=float32][shape=32x1080x1920][kernelSize=5x5][border=REPLICATE][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 960.00588090936, + "gpu_time_us_python": 968.7660691185399, + "gpu_noise_us_cpp": 2.234, + "gpu_noise_us_python": 3.224, + "gpu_bwutil_cpp": 0.3555536549385714, + "gpu_bwutil_python": 0.35233963399177637 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 650.05558794692, + "gpu_time_us_python": 661.2460639430599, + "gpu_noise_us_cpp": 0.704, + "gpu_noise_us_python": 2.496, + "gpu_bwutil_cpp": 0.4005057303671425, + "gpu_bwutil_python": 0.3937326840345574 + } + }, + "averageblur_scalar_advanced[InOutDataType=float32][shape=32x1080x1920][kernelSize=5x5][border=REPLICATE][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1031.27333391834, + "gpu_time_us_python": 1040.64848341546, + "gpu_noise_us_cpp": 1.778, + "gpu_noise_us_python": 3.6019999999999994, + "gpu_bwutil_cpp": 0.33098263617274837, + "gpu_bwutil_python": 0.3280013745192152 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 848.18315181498, + "gpu_time_us_python": 859.9985665527199, + "gpu_noise_us_cpp": 0.8379999999999999, + "gpu_noise_us_python": 2.1160000000000005, + "gpu_bwutil_cpp": 0.3069862218343248, + "gpu_bwutil_python": 0.3027625801079562 + } + }, + "averageblur_scalar_advanced[InOutDataType=float32][shape=64x720x1280][kernelSize=5x5][border=REPLICATE][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 861.2884369268, + "gpu_time_us_python": 871.71713880354, + "gpu_noise_us_cpp": 1.7899999999999998, + "gpu_noise_us_python": 2.3839999999999995, + "gpu_bwutil_cpp": 0.35227221992473645, + "gpu_bwutil_python": 0.34806049546798057 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 584.4030299769199, + "gpu_time_us_python": 595.11992630696, + "gpu_noise_us_cpp": 1.384, + "gpu_noise_us_python": 3.6300000000000003, + "gpu_bwutil_cpp": 0.3960028124063066, + "gpu_bwutil_python": 0.388874114247236 + } + }, + "averageblur_scalar_advanced[InOutDataType=float32][shape=64x720x1280][kernelSize=5x5][border=REPLICATE][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 942.35663156318, + "gpu_time_us_python": 953.4556811546599, + "gpu_noise_us_cpp": 1.3760000000000001, + "gpu_noise_us_python": 3.184, + "gpu_bwutil_cpp": 0.3219672003779468, + "gpu_bwutil_python": 0.3182208400935146 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 771.8799427827599, + "gpu_time_us_python": 782.77931819984, + "gpu_noise_us_cpp": 1.426, + "gpu_noise_us_python": 3.242, + "gpu_bwutil_cpp": 0.29984819927241757, + "gpu_bwutil_python": 0.29565969727138147 + } + }, + "averageblur_scalar_advanced[InOutDataType=float32][shape=8x2160x3840][kernelSize=5x5][border=REPLICATE][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 949.81893664168, + "gpu_time_us_python": 960.6421803630001, + "gpu_noise_us_cpp": 1.564, + "gpu_noise_us_python": 2.272, + "gpu_bwutil_cpp": 0.3593672045840794, + "gpu_bwutil_python": 0.35531895008109027 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 645.55338087904, + "gpu_time_us_python": 657.41456928968, + "gpu_noise_us_cpp": 0.842, + "gpu_noise_us_python": 3.4599999999999995, + "gpu_bwutil_cpp": 0.4032949713646592, + "gpu_bwutil_python": 0.3960155114132303 + } + }, + "averageblur_scalar_advanced[InOutDataType=float32][shape=8x2160x3840][kernelSize=5x5][border=REPLICATE][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1006.5005004865, + "gpu_time_us_python": 1018.8606423552001, + "gpu_noise_us_cpp": 1.292, + "gpu_noise_us_python": 4.4399999999999995, + "gpu_bwutil_cpp": 0.3391293292904143, + "gpu_bwutil_python": 0.33501803300057215 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 831.3303232762601, + "gpu_time_us_python": 843.57103983876, + "gpu_noise_us_cpp": 1.686, + "gpu_noise_us_python": 2.51, + "gpu_bwutil_cpp": 0.3132116572017132, + "gpu_bwutil_python": 0.3086660459963995 + } + }, + "averageblur_scalar_advanced[InOutDataType=uint8][shape=32x1080x1920][kernelSize=5x5][border=REPLICATE][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 986.80845312082, + "gpu_time_us_python": 996.2032056171599, + "gpu_noise_us_cpp": 1.968, + "gpu_noise_us_python": 3.3600000000000003, + "gpu_bwutil_cpp": 0.086474197320722, + "gpu_bwutil_python": 0.08565887173389586 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 715.46725069356, + "gpu_time_us_python": 725.78473826482, + "gpu_noise_us_cpp": 0.924, + "gpu_noise_us_python": 2.992, + "gpu_bwutil_cpp": 0.09098273794626985, + "gpu_bwutil_python": 0.08968949106293345 + } + }, + "averageblur_scalar_advanced[InOutDataType=uint8][shape=32x1080x1920][kernelSize=5x5][border=REPLICATE][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1121.19735097084, + "gpu_time_us_python": 1132.82778603152, + "gpu_noise_us_cpp": 1.9700000000000002, + "gpu_noise_us_python": 3.0580000000000003, + "gpu_bwutil_cpp": 0.07610935922041377, + "gpu_bwutil_python": 0.07532804573224919 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 916.8461240725999, + "gpu_time_us_python": 929.4607056827401, + "gpu_noise_us_cpp": 0.924, + "gpu_noise_us_python": 2.906, + "gpu_bwutil_cpp": 0.07099853105037551, + "gpu_bwutil_python": 0.07003350875857595 + } + }, + "averageblur_scalar_advanced[InOutDataType=uint8][shape=64x720x1280][kernelSize=5x5][border=REPLICATE][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 894.9193756549599, + "gpu_time_us_python": 905.6162370008, + "gpu_noise_us_cpp": 2.196, + "gpu_noise_us_python": 2.6439999999999997, + "gpu_bwutil_cpp": 0.08475857072962104, + "gpu_bwutil_python": 0.0837582391326335 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 648.53765514726, + "gpu_time_us_python": 659.2371665479201, + "gpu_noise_us_cpp": 0.994, + "gpu_noise_us_python": 3.3480000000000003, + "gpu_bwutil_cpp": 0.08922048756516296, + "gpu_bwutil_python": 0.08777582528102365 + } + }, + "averageblur_scalar_advanced[InOutDataType=uint8][shape=64x720x1280][kernelSize=5x5][border=REPLICATE][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1015.2446317851202, + "gpu_time_us_python": 1029.84626346304, + "gpu_noise_us_cpp": 1.8800000000000001, + "gpu_noise_us_python": 3.386, + "gpu_bwutil_cpp": 0.07471294697968725, + "gpu_bwutil_python": 0.07365497148866607 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 829.6938922885001, + "gpu_time_us_python": 841.8313684202001, + "gpu_noise_us_cpp": 1.048, + "gpu_noise_us_python": 2.4219999999999997, + "gpu_bwutil_cpp": 0.06973889487237013, + "gpu_bwutil_python": 0.06873087389198818 + } + }, + "averageblur_scalar_advanced[InOutDataType=uint8][shape=8x2160x3840][kernelSize=5x5][border=REPLICATE][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 966.7501150410601, + "gpu_time_us_python": 979.5520299600199, + "gpu_noise_us_cpp": 1.34, + "gpu_noise_us_python": 4.078, + "gpu_bwutil_cpp": 0.08826837650242701, + "gpu_bwutil_python": 0.0871154065875963 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 701.1523467026601, + "gpu_time_us_python": 711.62112863572, + "gpu_noise_us_cpp": 0.8619999999999999, + "gpu_noise_us_python": 1.766, + "gpu_bwutil_cpp": 0.09284115914178895, + "gpu_bwutil_python": 0.0914747245684473 + } + }, + "averageblur_scalar_advanced[InOutDataType=uint8][shape=8x2160x3840][kernelSize=5x5][border=REPLICATE][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1100.17223984876, + "gpu_time_us_python": 1113.8511997001401, + "gpu_noise_us_cpp": 1.6960000000000002, + "gpu_noise_us_python": 4.5920000000000005, + "gpu_bwutil_cpp": 0.0775637720987942, + "gpu_bwutil_python": 0.07661196755641408 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 905.7046436288999, + "gpu_time_us_python": 918.7825893483601, + "gpu_noise_us_cpp": 0.8620000000000001, + "gpu_noise_us_python": 3.2800000000000002, + "gpu_bwutil_cpp": 0.07187269781094605, + "gpu_bwutil_python": 0.07084870862978496 + } + } + } + }, + "averageblur_rgb_tensor_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "64x720x1280", + "32x1080x1920", + "4x2160x3840" + ], + "kernelSize": [ + "5x5" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AverageBlur RGB8 tensor (advanced)." + }, + "baselines": { + "averageblur_rgb_tensor_advanced[InOutDataType=uchar3][shape=32x1080x1920][kernelSize=5x5][border=REPLICATE][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3175.81553036648, + "gpu_time_us_python": 3186.88237214386, + "gpu_noise_us_cpp": 3.626, + "gpu_noise_us_python": 5.418000000000001, + "gpu_bwutil_cpp": 0.08060932960182052, + "gpu_bwutil_python": 0.08032931888347337 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2200.4364201592603, + "gpu_time_us_python": 2211.80915396402, + "gpu_noise_us_cpp": 2.122, + "gpu_noise_us_python": 4.076, + "gpu_bwutil_cpp": 0.08875054899487336, + "gpu_bwutil_python": 0.08829342138423106 + } + }, + "averageblur_rgb_tensor_advanced[InOutDataType=uchar3][shape=4x2160x3840][kernelSize=5x5][border=REPLICATE][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1569.04705718154, + "gpu_time_us_python": 1581.6135181314198, + "gpu_noise_us_cpp": 2.286, + "gpu_noise_us_python": 4.1579999999999995, + "gpu_bwutil_cpp": 0.08157825042907262, + "gpu_bwutil_python": 0.08093032065445466 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1080.5526226717798, + "gpu_time_us_python": 1090.24803214526, + "gpu_noise_us_cpp": 0.7979999999999999, + "gpu_noise_us_python": 1.7740000000000002, + "gpu_bwutil_cpp": 0.0903652823072433, + "gpu_bwutil_python": 0.08956297608941326 + } + }, + "averageblur_rgb_tensor_advanced[InOutDataType=uchar3][shape=64x720x1280][kernelSize=5x5][border=REPLICATE][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2869.92980946188, + "gpu_time_us_python": 2881.7078404042195, + "gpu_noise_us_cpp": 2.966, + "gpu_noise_us_python": 4.656000000000001, + "gpu_bwutil_cpp": 0.07928962068073395, + "gpu_bwutil_python": 0.07896555657653123 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2003.1047916619602, + "gpu_time_us_python": 2014.47814951596, + "gpu_noise_us_cpp": 1.208, + "gpu_noise_us_python": 6.854000000000001, + "gpu_bwutil_cpp": 0.08666126532887206, + "gpu_bwutil_python": 0.08617232771135523 + } + } + } + }, + "averageblur_rgb_varshape_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x720x1280", + "16x1080x1920", + "4x2160x3840" + ], + "kernelSize": [ + "5x5" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AverageBlur RGB8 varshape (advanced)." + }, + "baselines": { + "averageblur_rgb_varshape_advanced[InOutDataType=uchar3][shape=16x1080x1920][kernelSize=5x5][border=REPLICATE][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1472.0928554359, + "gpu_time_us_python": 1486.9873386221402, + "gpu_noise_us_cpp": 2.002, + "gpu_noise_us_python": 3.81, + "gpu_bwutil_cpp": 0.08695115674547832, + "gpu_bwutil_python": 0.08608029211475952 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1056.88591412076, + "gpu_time_us_python": 1069.81618540348, + "gpu_noise_us_cpp": 1.104, + "gpu_noise_us_python": 2.9799999999999995, + "gpu_bwutil_cpp": 0.09238892829449287, + "gpu_bwutil_python": 0.0912754172729585 + } + }, + "averageblur_rgb_varshape_advanced[InOutDataType=uchar3][shape=32x720x1280][kernelSize=5x5][border=REPLICATE][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1332.64885544772, + "gpu_time_us_python": 1344.24699876666, + "gpu_noise_us_cpp": 2.508, + "gpu_noise_us_python": 3.2, + "gpu_bwutil_cpp": 0.08537734148900532, + "gpu_bwutil_python": 0.08464065025235966 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 952.3734163593201, + "gpu_time_us_python": 964.16960217898, + "gpu_noise_us_cpp": 0.748, + "gpu_noise_us_python": 4.41, + "gpu_bwutil_cpp": 0.09113581113320383, + "gpu_bwutil_python": 0.09002014615116354 + } + }, + "averageblur_rgb_varshape_advanced[InOutDataType=uchar3][shape=4x2160x3840][kernelSize=5x5][border=REPLICATE][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1446.72618243042, + "gpu_time_us_python": 1463.2138501117001, + "gpu_noise_us_cpp": 1.848, + "gpu_noise_us_python": 6.781999999999999, + "gpu_bwutil_cpp": 0.08847568415902987, + "gpu_bwutil_python": 0.087479285987041 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1044.0194979244, + "gpu_time_us_python": 1057.1787600835, + "gpu_noise_us_cpp": 0.916, + "gpu_noise_us_python": 5.146, + "gpu_bwutil_cpp": 0.09352790584120356, + "gpu_bwutil_python": 0.09236403262280235 + } + } + } + }, + "averageblur_uchar3_planar_nchw_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x720x1280" + ], + "kernelSize": [ + "5x5" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AverageBlur RGB8 (basic) (planar NCHW)." + }, + "baselines": { + "averageblur_uchar3_planar_nchw_basic[InOutDataType=uchar3][shape=32x720x1280][kernelSize=5x5][border=REPLICATE][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1334.91385229368, + "gpu_time_us_python": 1346.1848659255797, + "gpu_noise_us_cpp": 1.8700000000000003, + "gpu_noise_us_python": 3.322, + "gpu_bwutil_cpp": 0.08523242405470302, + "gpu_bwutil_python": 0.08451898004113786 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 966.80737229806, + "gpu_time_us_python": 978.19659387298, + "gpu_noise_us_cpp": 1.062, + "gpu_noise_us_python": 2.9240000000000004, + "gpu_bwutil_cpp": 0.08977340605735647, + "gpu_bwutil_python": 0.08872823583440487 + } + }, + "averageblur_uchar3_planar_nchw_basic[InOutDataType=uchar3][shape=32x720x1280][kernelSize=5x5][border=REPLICATE][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1399.00027347004, + "gpu_time_us_python": 1411.8746538158198, + "gpu_noise_us_cpp": 2.6120000000000005, + "gpu_noise_us_python": 3.5020000000000002, + "gpu_bwutil_cpp": 0.08132803314885054, + "gpu_bwutil_python": 0.08058637142389043 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1060.2440436268398, + "gpu_time_us_python": 1073.0594806405802, + "gpu_noise_us_cpp": 0.9700000000000001, + "gpu_noise_us_python": 3.104, + "gpu_bwutil_cpp": 0.0818618101670219, + "gpu_bwutil_python": 0.0808828110432478 + } + } + } + }, + "averageblur_uchar3_fakeplanar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x720x1280" + ], + "kernelSize": [ + "5x5" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AverageBlur RGB8 (advanced) (fake planar NCHW_FAKE)." + }, + "baselines": { + "averageblur_uchar3_fakeplanar_nchw_advanced[InOutDataType=uchar3][shape=32x720x1280][kernelSize=5x5][border=REPLICATE][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2042.39216897542, + "gpu_time_us_python": 2058.55217527164, + "gpu_noise_us_cpp": 2.41, + "gpu_noise_us_python": 3.8980000000000006, + "gpu_bwutil_cpp": 0.05570823785594522, + "gpu_bwutil_python": 0.05527092314004702 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1532.3161339465, + "gpu_time_us_python": 1546.1318249382, + "gpu_noise_us_cpp": 1.08, + "gpu_noise_us_python": 2.052, + "gpu_bwutil_cpp": 0.05663920890189078, + "gpu_bwutil_python": 0.056132949068560145 + } + } + } + }, + "averageblur_uchar4_planar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "32x720x1280" + ], + "kernelSize": [ + "5x5" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AverageBlur RGBA8 (advanced) (planar NCHW)." + }, + "baselines": { + "averageblur_uchar4_planar_nchw_advanced[InOutDataType=uchar4][shape=32x720x1280][kernelSize=5x5][border=REPLICATE][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1775.3336267184397, + "gpu_time_us_python": 1785.5057377075602, + "gpu_noise_us_cpp": 2.346, + "gpu_noise_us_python": 3.218, + "gpu_bwutil_cpp": 0.08545108199899124, + "gpu_bwutil_python": 0.08496413331852055 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1284.19825877554, + "gpu_time_us_python": 1293.74643870418, + "gpu_noise_us_cpp": 1.19, + "gpu_noise_us_python": 2.508, + "gpu_bwutil_cpp": 0.09011410450571807, + "gpu_bwutil_python": 0.08944832635036112 + } + } + } + }, + "averageblur_uchar4_fakeplanar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "32x720x1280" + ], + "kernelSize": [ + "5x5" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AverageBlur RGBA8 (advanced) (fake planar NCHW_FAKE)." + }, + "baselines": { + "averageblur_uchar4_fakeplanar_nchw_advanced[InOutDataType=uchar4][shape=32x720x1280][kernelSize=5x5][border=REPLICATE][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2598.7638758168405, + "gpu_time_us_python": 2616.6933516661, + "gpu_noise_us_cpp": 1.3379999999999999, + "gpu_noise_us_python": 2.686, + "gpu_bwutil_cpp": 0.058375616073572875, + "gpu_bwutil_python": 0.05797566169435242 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1995.9557702918003, + "gpu_time_us_python": 2010.07009772792, + "gpu_noise_us_cpp": 1.554, + "gpu_noise_us_python": 4.912, + "gpu_bwutil_cpp": 0.05797722518907884, + "gpu_bwutil_python": 0.057570801190485 + } + } + } + }, + "averageblur_float3_planar_nchw_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x720x1280" + ], + "kernelSize": [ + "5x5" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AverageBlur RGBf32 (basic) (planar NCHW)." + }, + "baselines": { + "averageblur_float3_planar_nchw_basic[InOutDataType=float3][shape=32x720x1280][kernelSize=5x5][border=REPLICATE][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1283.2980792925, + "gpu_time_us_python": 1294.8945293861002, + "gpu_noise_us_cpp": 1.6779999999999997, + "gpu_noise_us_python": 3.786, + "gpu_bwutil_cpp": 0.3546423238074766, + "gpu_bwutil_python": 0.3514671912402235 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 871.0499598353799, + "gpu_time_us_python": 880.36041662768, + "gpu_noise_us_cpp": 1.28, + "gpu_noise_us_python": 1.6599999999999997, + "gpu_bwutil_cpp": 0.39852863861848803, + "gpu_bwutil_python": 0.3943118805051292 + } + } + } + }, + "averageblur_float3_planar_nchw_varshape_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x720x1280" + ], + "kernelSize": [ + "5x5" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AverageBlur RGBf32 VarShape (advanced) (planar NCHW)." + }, + "baselines": { + "averageblur_float3_planar_nchw_varshape_advanced[InOutDataType=float3][shape=32x720x1280][kernelSize=5x5][border=REPLICATE][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1256.15741054972, + "gpu_time_us_python": 1269.98260972962, + "gpu_noise_us_cpp": 2.248, + "gpu_noise_us_python": 3.284, + "gpu_bwutil_cpp": 0.3623046946925765, + "gpu_bwutil_python": 0.3583612464879689 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 989.70174484106, + "gpu_time_us_python": 1000.90477587024, + "gpu_noise_us_cpp": 1.672, + "gpu_noise_us_python": 2.082, + "gpu_bwutil_cpp": 0.35079316511015685, + "gpu_bwutil_python": 0.3468739039469891 + } + } + } + }, + "averageblur_float3_fakeplanar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x720x1280" + ], + "kernelSize": [ + "5x5" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AverageBlur RGBf32 (advanced) (fake planar NCHW_FAKE)." + }, + "baselines": { + "averageblur_float3_fakeplanar_nchw_advanced[InOutDataType=float3][shape=32x720x1280][kernelSize=5x5][border=REPLICATE][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2268.8267032207395, + "gpu_time_us_python": 2285.99435762922, + "gpu_noise_us_cpp": 4.514, + "gpu_noise_us_python": 2.732, + "gpu_bwutil_cpp": 0.20059357047072038, + "gpu_bwutil_python": 0.19908740801103003 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1970.5115084875797, + "gpu_time_us_python": 1984.82295440274, + "gpu_noise_us_cpp": 2.522, + "gpu_noise_us_python": 5.65, + "gpu_bwutil_cpp": 0.17616580913179275, + "gpu_bwutil_python": 0.17489368378798492 + } + } + } + }, + "averageblur_float4_planar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "32x720x1280" + ], + "kernelSize": [ + "5x5" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AverageBlur RGBAf32 (advanced) (planar NCHW)." + }, + "baselines": { + "averageblur_float4_planar_nchw_advanced[InOutDataType=float4][shape=32x720x1280][kernelSize=5x5][border=REPLICATE][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1707.4956269491, + "gpu_time_us_python": 1718.6128557544403, + "gpu_noise_us_cpp": 1.444, + "gpu_noise_us_python": 2.4579999999999997, + "gpu_bwutil_cpp": 0.3553831602179363, + "gpu_bwutil_python": 0.3530850035367399 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1156.3911908044, + "gpu_time_us_python": 1167.6073355659, + "gpu_noise_us_cpp": 0.9560000000000001, + "gpu_noise_us_python": 3.2399999999999998, + "gpu_bwutil_cpp": 0.40025449720567813, + "gpu_bwutil_python": 0.3964151354322064 + } + }, + "averageblur_float4_planar_nchw_advanced[InOutDataType=float4][shape=32x720x1280][kernelSize=5x5][border=REPLICATE][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1669.05583715436, + "gpu_time_us_python": 1682.6807900562599, + "gpu_noise_us_cpp": 4.606, + "gpu_noise_us_python": 4.244, + "gpu_bwutil_cpp": 0.36356859329797475, + "gpu_bwutil_python": 0.3606247508755261 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1292.33556475464, + "gpu_time_us_python": 1306.35909428158, + "gpu_noise_us_cpp": 1.3679999999999999, + "gpu_noise_us_python": 4.762, + "gpu_bwutil_cpp": 0.3581948996264973, + "gpu_bwutil_python": 0.35435057378442847 + } + } + } + }, + "averageblur_float4_fakeplanar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "32x720x1280" + ], + "kernelSize": [ + "5x5" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AverageBlur RGBAf32 (advanced) (fake planar NCHW_FAKE)." + }, + "baselines": { + "averageblur_float4_fakeplanar_nchw_advanced[InOutDataType=float4][shape=32x720x1280][kernelSize=5x5][border=REPLICATE][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3168.7330487407603, + "gpu_time_us_python": 3184.9173352687803, + "gpu_noise_us_cpp": 2.668, + "gpu_noise_us_python": 3.2039999999999997, + "gpu_bwutil_cpp": 0.19150096446143378, + "gpu_bwutil_python": 0.19052788382010802 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2603.28332573888, + "gpu_time_us_python": 2616.26606652084, + "gpu_noise_us_cpp": 2.682, + "gpu_noise_us_python": 3.3840000000000003, + "gpu_bwutil_cpp": 0.17778930566161757, + "gpu_bwutil_python": 0.1769067819277686 + } + } + } + }, + "averageblur_rgb_tensor_planar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "64x720x1280", + "32x1080x1920", + "4x2160x3840" + ], + "kernelSize": [ + "5x5" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AverageBlur RGB8 tensor (advanced) (planar NCHW)." + }, + "baselines": { + "averageblur_rgb_tensor_planar_nchw_advanced[InOutDataType=uchar3][shape=32x1080x1920][kernelSize=5x5][border=REPLICATE][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2933.5074979205, + "gpu_time_us_python": 2945.7725679564, + "gpu_noise_us_cpp": 3.3979999999999997, + "gpu_noise_us_python": 4.768, + "gpu_bwutil_cpp": 0.08726765855262417, + "gpu_bwutil_python": 0.08690436558547829 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2118.3226491752403, + "gpu_time_us_python": 2128.9221087699602, + "gpu_noise_us_cpp": 2.522, + "gpu_noise_us_python": 2.404, + "gpu_bwutil_cpp": 0.09218854352272297, + "gpu_bwutil_python": 0.09173055940810053 + } + }, + "averageblur_rgb_tensor_planar_nchw_advanced[InOutDataType=uchar3][shape=4x2160x3840][kernelSize=5x5][border=REPLICATE][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1445.40628376574, + "gpu_time_us_python": 1457.63546976506, + "gpu_noise_us_cpp": 1.996, + "gpu_noise_us_python": 3.3200000000000003, + "gpu_bwutil_cpp": 0.08855653385846597, + "gpu_bwutil_python": 0.08781434174793783 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1044.4374763895798, + "gpu_time_us_python": 1054.22886497028, + "gpu_noise_us_cpp": 1.012, + "gpu_noise_us_python": 2.1559999999999997, + "gpu_bwutil_cpp": 0.09348880766224106, + "gpu_bwutil_python": 0.09262034670732282 + } + }, + "averageblur_rgb_tensor_planar_nchw_advanced[InOutDataType=uchar3][shape=64x720x1280][kernelSize=5x5][border=REPLICATE][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2655.66086630266, + "gpu_time_us_python": 2668.15621193412, + "gpu_noise_us_cpp": 3.346, + "gpu_noise_us_python": 5.156000000000001, + "gpu_bwutil_cpp": 0.0856870869742463, + "gpu_bwutil_python": 0.0852858659738976 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1919.34227730452, + "gpu_time_us_python": 1929.8260871562402, + "gpu_noise_us_cpp": 1.5, + "gpu_noise_us_python": 2.9040000000000004, + "gpu_bwutil_cpp": 0.09044041957255387, + "gpu_bwutil_python": 0.0899492189187441 + } + } + } + }, + "averageblur_rgb_tensor_fakeplanar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "64x720x1280", + "32x1080x1920", + "4x2160x3840" + ], + "kernelSize": [ + "5x5" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AverageBlur RGB8 tensor (advanced) (fake planar NCHW_FAKE)." + }, + "baselines": { + "averageblur_rgb_tensor_fakeplanar_nchw_advanced[InOutDataType=uchar3][shape=32x1080x1920][kernelSize=5x5][border=REPLICATE][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4516.849310057461, + "gpu_time_us_python": 4535.54392186074, + "gpu_noise_us_cpp": 4.492, + "gpu_noise_us_python": 6.692, + "gpu_bwutil_cpp": 0.05667676686110497, + "gpu_bwutil_python": 0.05644318806627648 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3361.5911883075, + "gpu_time_us_python": 3375.1496731182, + "gpu_noise_us_cpp": 1.338, + "gpu_noise_us_python": 3.9019999999999997, + "gpu_bwutil_cpp": 0.05809025103842471, + "gpu_bwutil_python": 0.05785707765629529 + } + }, + "averageblur_rgb_tensor_fakeplanar_nchw_advanced[InOutDataType=uchar3][shape=4x2160x3840][kernelSize=5x5][border=REPLICATE][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2243.5583182743203, + "gpu_time_us_python": 2260.6949754663, + "gpu_noise_us_cpp": 2.562, + "gpu_noise_us_python": 5.526, + "gpu_bwutil_cpp": 0.057052334546241826, + "gpu_bwutil_python": 0.05661987155388898 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1669.3481493785598, + "gpu_time_us_python": 1683.7259097386, + "gpu_noise_us_cpp": 1.462, + "gpu_noise_us_python": 2.884, + "gpu_bwutil_cpp": 0.0584885196157913, + "gpu_bwutil_python": 0.0579899572777355 + } + }, + "averageblur_rgb_tensor_fakeplanar_nchw_advanced[InOutDataType=uchar3][shape=64x720x1280][kernelSize=5x5][border=REPLICATE][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4058.12852459564, + "gpu_time_us_python": 4077.92472377894, + "gpu_noise_us_cpp": 4.287999999999999, + "gpu_noise_us_python": 5.986, + "gpu_bwutil_cpp": 0.05607406540355734, + "gpu_bwutil_python": 0.05580192984461525 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3039.36739226416, + "gpu_time_us_python": 3053.84659926732, + "gpu_noise_us_cpp": 1.492, + "gpu_noise_us_python": 3.936, + "gpu_bwutil_cpp": 0.05711002991666728, + "gpu_bwutil_python": 0.0568396318175976 + } + } + } + }, + "averageblur_rgb_varshape_planar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x720x1280", + "16x1080x1920", + "4x2160x3840" + ], + "kernelSize": [ + "5x5" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "AverageBlur RGB8 varshape (advanced) (planar NCHW)." + }, + "baselines": { + "averageblur_rgb_varshape_planar_nchw_advanced[InOutDataType=uchar3][shape=16x1080x1920][kernelSize=5x5][border=REPLICATE][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1545.7241747113799, + "gpu_time_us_python": 1561.54081297952, + "gpu_noise_us_cpp": 2.024, + "gpu_noise_us_python": 6.066000000000001, + "gpu_bwutil_cpp": 0.08280912016028283, + "gpu_bwutil_python": 0.08197075196450528 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1177.95798886052, + "gpu_time_us_python": 1189.09321877592, + "gpu_noise_us_cpp": 1.652, + "gpu_noise_us_python": 3.1399999999999997, + "gpu_bwutil_cpp": 0.08289493713747656, + "gpu_bwutil_python": 0.0821173759772825 + } + }, + "averageblur_rgb_varshape_planar_nchw_advanced[InOutDataType=uchar3][shape=32x720x1280][kernelSize=5x5][border=REPLICATE][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1398.5723689947401, + "gpu_time_us_python": 1413.4894928182198, + "gpu_noise_us_cpp": 1.688, + "gpu_noise_us_python": 5.311999999999999, + "gpu_bwutil_cpp": 0.08135284671653742, + "gpu_bwutil_python": 0.08049466232551444 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1060.0802574691, + "gpu_time_us_python": 1073.9979297883601, + "gpu_noise_us_cpp": 1.342, + "gpu_noise_us_python": 2.6900000000000004, + "gpu_bwutil_cpp": 0.08187418762146818, + "gpu_bwutil_python": 0.080817661438074 + } + }, + "averageblur_rgb_varshape_planar_nchw_advanced[InOutDataType=uchar3][shape=4x2160x3840][kernelSize=5x5][border=REPLICATE][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1520.2664022734402, + "gpu_time_us_python": 1534.6736797668, + "gpu_noise_us_cpp": 1.762, + "gpu_noise_us_python": 3.918, + "gpu_bwutil_cpp": 0.08419582172862661, + "gpu_bwutil_python": 0.0834057647876196 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1175.00872746762, + "gpu_time_us_python": 1188.0239159704802, + "gpu_noise_us_cpp": 1.294, + "gpu_noise_us_python": 3.3639999999999994, + "gpu_bwutil_cpp": 0.08310288474025915, + "gpu_bwutil_python": 0.08219087212224988 + } + } + } + } + } +} diff --git a/bench/config/operators/bilateralfilter.json b/bench/config/operators/bilateralfilter.json new file mode 100644 index 000000000..4ab3777f9 --- /dev/null +++ b/bench/config/operators/bilateralfilter.json @@ -0,0 +1,1340 @@ +{ + "benchmark": "bilateralfilter", + "configs": { + "bilateralfilter_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "int64_axes": { + "diameter": [ + -1 + ] + }, + "float64_axes": { + "sigmaSpace": [ + 1.2 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "BilateralFilter RGB8 (basic)." + }, + "baselines": { + "bilateralfilter_uchar3_basic[InOutDataType=uchar3][shape=16x1080x1920][border=REFLECT][layout=NHWC][inputKind=Tensor][diameter=-1][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1912.18988891826, + "gpu_time_us_python": 1926.4561931595003, + "gpu_noise_us_cpp": 1.518, + "gpu_noise_us_python": 5.25, + "gpu_bwutil_cpp": 0.0669394524880563, + "gpu_bwutil_python": 0.06644394585300525 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1730.0562718115802, + "gpu_time_us_python": 1742.3431555502, + "gpu_noise_us_cpp": 1.6060000000000003, + "gpu_noise_us_python": 3.7, + "gpu_bwutil_cpp": 0.056439305988704856, + "gpu_bwutil_python": 0.05604015397510352 + } + }, + "bilateralfilter_uchar3_basic[InOutDataType=uchar3][shape=16x1080x1920][border=REFLECT][layout=NHWC][inputKind=VarShape][diameter=-1][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2053.04815010194, + "gpu_time_us_python": 2068.5351072037397, + "gpu_noise_us_cpp": 14.129999999999999, + "gpu_noise_us_python": 3.604, + "gpu_bwutil_cpp": 0.06234690300676402, + "gpu_bwutil_python": 0.0618801511095121 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1809.0585688277401, + "gpu_time_us_python": 1825.12767542952, + "gpu_noise_us_cpp": 1.1320000000000001, + "gpu_noise_us_python": 7.114, + "gpu_bwutil_cpp": 0.053974204690170134, + "gpu_bwutil_python": 0.05349787509369399 + } + } + } + }, + "bilateralfilter_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "diameter": [ + -1 + ] + }, + "float64_axes": { + "sigmaSpace": [ + 1.2 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "BilateralFilter RGBf32 (basic)." + }, + "baselines": { + "bilateralfilter_float3_basic[InOutDataType=float3][shape=16x1080x1920][border=REFLECT][layout=NHWC][inputKind=Tensor][diameter=-1][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2040.15682117482, + "gpu_time_us_python": 2054.755034603, + "gpu_noise_us_cpp": 8.554, + "gpu_noise_us_python": 5.718, + "gpu_bwutil_cpp": 0.25096372417610013, + "gpu_bwutil_python": 0.24917928833521633 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1836.4457010009198, + "gpu_time_us_python": 1849.73406089528, + "gpu_noise_us_cpp": 0.982, + "gpu_noise_us_python": 3.16, + "gpu_bwutil_cpp": 0.21267931983590302, + "gpu_bwutil_python": 0.21115084760166747 + } + } + } + }, + "bilateralfilter_float3_varshape_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "int64_axes": { + "diameter": [ + -1 + ] + }, + "float64_axes": { + "sigmaSpace": [ + 1.2 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "BilateralFilter RGBf32 VarShape (advanced)." + }, + "baselines": { + "bilateralfilter_float3_varshape_advanced[InOutDataType=float3][shape=16x1080x1920][border=REFLECT][layout=NHWC][inputKind=VarShape][diameter=-1][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1960.1455956696998, + "gpu_time_us_python": 1978.3553074723197, + "gpu_noise_us_cpp": 2.33, + "gpu_noise_us_python": 7.49, + "gpu_bwutil_cpp": 0.26120656282082433, + "gpu_bwutil_python": 0.25880248057337385 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1688.0669491287397, + "gpu_time_us_python": 1704.4058883219, + "gpu_noise_us_cpp": 1.55, + "gpu_noise_us_python": 7.0040000000000004, + "gpu_bwutil_cpp": 0.2313690908691383, + "gpu_bwutil_python": 0.2291549924552601 + } + } + } + }, + "bilateralfilter_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "8x1080x1920" + ], + "border": [ + "REFLECT" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "int64_axes": { + "diameter": [ + -1 + ] + }, + "float64_axes": { + "sigmaSpace": [ + 1.2 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "BilateralFilter RGBA8 (advanced)." + }, + "baselines": { + "bilateralfilter_uchar4_advanced[InOutDataType=uchar4][shape=8x1080x1920][border=REFLECT][layout=NHWC][inputKind=Tensor][diameter=-1][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1133.91814947418, + "gpu_time_us_python": 1146.4994960484, + "gpu_noise_us_cpp": 1.8800000000000001, + "gpu_noise_us_python": 4.19, + "gpu_bwutil_cpp": 0.07525565013462668, + "gpu_bwutil_python": 0.0744302720887284 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1008.49604443364, + "gpu_time_us_python": 1020.2574397503998, + "gpu_noise_us_cpp": 1.3199999999999998, + "gpu_noise_us_python": 3.414, + "gpu_bwutil_cpp": 0.06454641053133842, + "gpu_bwutil_python": 0.06380306653841424 + } + }, + "bilateralfilter_uchar4_advanced[InOutDataType=uchar4][shape=8x1080x1920][border=REFLECT][layout=NHWC][inputKind=VarShape][diameter=-1][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1284.58552484558, + "gpu_time_us_python": 1299.5676486372, + "gpu_noise_us_cpp": 1.684, + "gpu_noise_us_python": 2.88, + "gpu_bwutil_cpp": 0.06642907759297383, + "gpu_bwutil_python": 0.06566345020993414 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1134.75719476034, + "gpu_time_us_python": 1148.95363172248, + "gpu_noise_us_cpp": 1.172, + "gpu_noise_us_python": 4.088, + "gpu_bwutil_cpp": 0.05736414658176027, + "gpu_bwutil_python": 0.056654536108698604 + } + } + } + }, + "bilateralfilter_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "8x1080x1920" + ], + "border": [ + "REFLECT" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "int64_axes": { + "diameter": [ + -1 + ] + }, + "float64_axes": { + "sigmaSpace": [ + 1.2 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "BilateralFilter RGBAf32 (advanced)." + }, + "baselines": { + "bilateralfilter_float4_advanced[InOutDataType=float4][shape=8x1080x1920][border=REFLECT][layout=NHWC][inputKind=Tensor][diameter=-1][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1081.18668854954, + "gpu_time_us_python": 1094.05789272418, + "gpu_noise_us_cpp": 1.5559999999999998, + "gpu_noise_us_python": 3.4379999999999997, + "gpu_bwutil_cpp": 0.31570384701543264, + "gpu_bwutil_python": 0.31199277442718754 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 905.4254755311, + "gpu_time_us_python": 916.4513325595601, + "gpu_noise_us_cpp": 1.2160000000000002, + "gpu_noise_us_python": 2.3560000000000003, + "gpu_bwutil_cpp": 0.2875746848834334, + "gpu_bwutil_python": 0.28411163821277136 + } + }, + "bilateralfilter_float4_advanced[InOutDataType=float4][shape=8x1080x1920][border=REFLECT][layout=NHWC][inputKind=VarShape][diameter=-1][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1156.7838174070603, + "gpu_time_us_python": 1172.9433197854, + "gpu_noise_us_cpp": 1.5699999999999998, + "gpu_noise_us_python": 3.69, + "gpu_bwutil_cpp": 0.29507242174163617, + "gpu_bwutil_python": 0.2910112074805996 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 981.5066987746401, + "gpu_time_us_python": 993.96571283054, + "gpu_noise_us_cpp": 2.602, + "gpu_noise_us_python": 2.574, + "gpu_bwutil_cpp": 0.26527998971442657, + "gpu_bwutil_python": 0.2619626092520431 + } + } + } + }, + "bilateralfilter_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "int64_axes": { + "diameter": [ + -1 + ] + }, + "float64_axes": { + "sigmaSpace": [ + 1.2 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "BilateralFilter single-channel (advanced)." + }, + "baselines": { + "bilateralfilter_scalar_advanced[InOutDataType=float32][shape=16x1080x1920][border=REFLECT][layout=NHWC][inputKind=Tensor][diameter=-1][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1425.3999102626199, + "gpu_time_us_python": 1438.3819828440003, + "gpu_noise_us_cpp": 1.232, + "gpu_noise_us_python": 3.5620000000000003, + "gpu_bwutil_cpp": 0.11973301188788407, + "gpu_bwutil_python": 0.11865248257739461 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1299.1563159992802, + "gpu_time_us_python": 1309.82591502856, + "gpu_noise_us_cpp": 1.022, + "gpu_noise_us_python": 2.966, + "gpu_bwutil_cpp": 0.10021224923882847, + "gpu_bwutil_python": 0.09939646957699175 + } + }, + "bilateralfilter_scalar_advanced[InOutDataType=float32][shape=16x1080x1920][border=REFLECT][layout=NHWC][inputKind=VarShape][diameter=-1][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1461.91436202688, + "gpu_time_us_python": 1479.3077609856398, + "gpu_noise_us_cpp": 1.404, + "gpu_noise_us_python": 4.037999999999999, + "gpu_bwutil_cpp": 0.11674257119017721, + "gpu_bwutil_python": 0.11536989523124346 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1317.10994765738, + "gpu_time_us_python": 1330.1027915626998, + "gpu_noise_us_cpp": 1.198, + "gpu_noise_us_python": 2.684, + "gpu_bwutil_cpp": 0.098845831834208, + "gpu_bwutil_python": 0.09788033213909192 + } + }, + "bilateralfilter_scalar_advanced[InOutDataType=uint8][shape=16x1080x1920][border=REFLECT][layout=NHWC][inputKind=Tensor][diameter=-1][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1444.1163008462, + "gpu_time_us_python": 1456.74325361486, + "gpu_noise_us_cpp": 1.5419999999999998, + "gpu_noise_us_python": 4.216, + "gpu_bwutil_cpp": 0.02954537024349378, + "gpu_bwutil_python": 0.02928964051124442 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1321.0948660346598, + "gpu_time_us_python": 1332.18001466216, + "gpu_noise_us_cpp": 1.184, + "gpu_noise_us_python": 3.836, + "gpu_bwutil_cpp": 0.02463705172409254, + "gpu_bwutil_python": 0.02443218454347594 + } + }, + "bilateralfilter_scalar_advanced[InOutDataType=uint8][shape=16x1080x1920][border=REFLECT][layout=NHWC][inputKind=VarShape][diameter=-1][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1506.03226561724, + "gpu_time_us_python": 1521.4081507599199, + "gpu_noise_us_cpp": 1.6460000000000001, + "gpu_noise_us_python": 4.982, + "gpu_bwutil_cpp": 0.028330710960054416, + "gpu_bwutil_python": 0.0280443819542755 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1353.90593976888, + "gpu_time_us_python": 1367.2683631880402, + "gpu_noise_us_cpp": 1.2280000000000002, + "gpu_noise_us_python": 3.428, + "gpu_bwutil_cpp": 0.02403986963563302, + "gpu_bwutil_python": 0.023805164734059442 + } + } + } + }, + "bilateralfilter_d9_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "8x1080x1920" + ], + "border": [ + "REFLECT" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "int64_axes": { + "diameter": [ + 9 + ] + }, + "float64_axes": { + "sigmaSpace": [ + 1.2 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "BilateralFilter single-channel d9 (advanced)." + }, + "baselines": { + "bilateralfilter_d9_1080p_advanced[InOutDataType=float32][shape=8x1080x1920][border=REFLECT][layout=NHWC][inputKind=Tensor][diameter=9][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2091.47131901824, + "gpu_time_us_python": 2104.0513533536796, + "gpu_noise_us_cpp": 2.452, + "gpu_noise_us_python": 4.038, + "gpu_bwutil_cpp": 0.0408009355049828, + "gpu_bwutil_python": 0.040557100788390685 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1858.30080441516, + "gpu_time_us_python": 1871.09362000066, + "gpu_noise_us_cpp": 2.102, + "gpu_noise_us_python": 5.024, + "gpu_bwutil_cpp": 0.035029770487082876, + "gpu_bwutil_python": 0.03479034311271133 + } + }, + "bilateralfilter_d9_1080p_advanced[InOutDataType=float32][shape=8x1080x1920][border=REFLECT][layout=NHWC][inputKind=VarShape][diameter=9][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2109.07000373386, + "gpu_time_us_python": 2125.9857165606, + "gpu_noise_us_cpp": 2.47, + "gpu_noise_us_python": 6.162, + "gpu_bwutil_cpp": 0.04046046347934726, + "gpu_bwutil_python": 0.040138655503577106 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1849.8553229027598, + "gpu_time_us_python": 1865.63104327174, + "gpu_noise_us_cpp": 1.9880000000000002, + "gpu_noise_us_python": 4.792, + "gpu_bwutil_cpp": 0.035189623292307175, + "gpu_bwutil_python": 0.034892551809072934 + } + }, + "bilateralfilter_d9_1080p_advanced[InOutDataType=uint8][shape=8x1080x1920][border=REFLECT][layout=NHWC][inputKind=Tensor][diameter=9][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2102.11793288122, + "gpu_time_us_python": 2117.48372415342, + "gpu_noise_us_cpp": 2.5100000000000002, + "gpu_noise_us_python": 5.287999999999999, + "gpu_bwutil_cpp": 0.01014856256607958, + "gpu_bwutil_python": 0.010074897145454838 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1870.6492796032203, + "gpu_time_us_python": 1882.4125644742003, + "gpu_noise_us_cpp": 2.144, + "gpu_noise_us_python": 3.5059999999999993, + "gpu_bwutil_cpp": 0.00869964307983482, + "gpu_bwutil_python": 0.0086452691086975 + } + }, + "bilateralfilter_d9_1080p_advanced[InOutDataType=uint8][shape=8x1080x1920][border=REFLECT][layout=NHWC][inputKind=VarShape][diameter=9][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2165.74215428578, + "gpu_time_us_python": 2183.8126659392797, + "gpu_noise_us_cpp": 2.214, + "gpu_noise_us_python": 6.106, + "gpu_bwutil_cpp": 0.009850425897385101, + "gpu_bwutil_python": 0.00976894020433426 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1913.2314292676801, + "gpu_time_us_python": 1927.62167281962, + "gpu_noise_us_cpp": 2.318, + "gpu_noise_us_python": 5.656000000000001, + "gpu_bwutil_cpp": 0.008505953103498099, + "gpu_bwutil_python": 0.008442421368895999 + } + } + } + }, + "bilateralfilter_rgb_auto_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "int64_axes": { + "diameter": [ + -1 + ] + }, + "float64_axes": { + "sigmaSpace": [ + 1.2 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "BilateralFilter RGB8 auto (advanced)." + }, + "baselines": { + "bilateralfilter_rgb_auto_1080p_advanced[InOutDataType=uchar3][shape=16x1080x1920][border=REFLECT][layout=NHWC][inputKind=Tensor][diameter=-1][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1912.1822974594597, + "gpu_time_us_python": 1925.44282795744, + "gpu_noise_us_cpp": 1.7719999999999998, + "gpu_noise_us_python": 3.936, + "gpu_bwutil_cpp": 0.06693966214057794, + "gpu_bwutil_python": 0.06647893798743097 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1730.29870653356, + "gpu_time_us_python": 1743.09193083136, + "gpu_noise_us_cpp": 1.828, + "gpu_noise_us_python": 5.194000000000001, + "gpu_bwutil_cpp": 0.05643135892977972, + "gpu_bwutil_python": 0.0560159626771088 + } + }, + "bilateralfilter_rgb_auto_1080p_advanced[InOutDataType=uchar3][shape=16x1080x1920][border=REFLECT][layout=NHWC][inputKind=VarShape][diameter=-1][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2053.42215827248, + "gpu_time_us_python": 2069.23965719422, + "gpu_noise_us_cpp": 3.2020000000000004, + "gpu_noise_us_python": 5.389999999999999, + "gpu_bwutil_cpp": 0.06233528491876757, + "gpu_bwutil_python": 0.061859219358249694 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1809.1661224479399, + "gpu_time_us_python": 1823.5705858518602, + "gpu_noise_us_cpp": 1.3539999999999999, + "gpu_noise_us_python": 4.148, + "gpu_bwutil_cpp": 0.05397110786915175, + "gpu_bwutil_python": 0.053545032140976646 + } + } + } + }, + "bilateralfilter_uchar3_planar_nchw_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "int64_axes": { + "diameter": [ + -1 + ] + }, + "float64_axes": { + "sigmaSpace": [ + 1.2 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "BilateralFilter RGB8 native planar (basic)." + }, + "baselines": { + "bilateralfilter_uchar3_planar_nchw_basic[InOutDataType=uchar3][shape=16x1080x1920][border=REFLECT][layout=NCHW][inputKind=Tensor][diameter=-1][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2246.7457064291602, + "gpu_time_us_python": 2259.8111938785396, + "gpu_noise_us_cpp": 1.3099999999999998, + "gpu_noise_us_python": 3.656, + "gpu_bwutil_cpp": 0.05697165551544472, + "gpu_bwutil_python": 0.05664233369085934 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1928.31186583714, + "gpu_time_us_python": 1939.8293811021201, + "gpu_noise_us_cpp": 1.2739999999999998, + "gpu_noise_us_python": 5.098, + "gpu_bwutil_cpp": 0.05063626553525817, + "gpu_bwutil_python": 0.05033555485813783 + } + }, + "bilateralfilter_uchar3_planar_nchw_basic[InOutDataType=uchar3][shape=16x1080x1920][border=REFLECT][layout=NCHW][inputKind=VarShape][diameter=-1][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2120.8138764065397, + "gpu_time_us_python": 2136.9013581813397, + "gpu_noise_us_cpp": 1.6239999999999999, + "gpu_noise_us_python": 4.108, + "gpu_bwutil_cpp": 0.06035456621791694, + "gpu_bwutil_python": 0.059900084262460664 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1881.6159906130001, + "gpu_time_us_python": 1895.07264612716, + "gpu_noise_us_cpp": 1.3880000000000001, + "gpu_noise_us_python": 3.8379999999999996, + "gpu_bwutil_cpp": 0.051893078968616925, + "gpu_bwutil_python": 0.051524517494061936 + } + } + } + }, + "bilateralfilter_uchar3_fakeplanar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "diameter": [ + -1 + ] + }, + "float64_axes": { + "sigmaSpace": [ + 1.2 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "BilateralFilter RGB8 fake-planar tensor comparison (advanced)." + }, + "baselines": { + "bilateralfilter_uchar3_fakeplanar_nchw_advanced[InOutDataType=uchar3][shape=16x1080x1920][border=REFLECT][layout=NCHW_FAKE][inputKind=Tensor][diameter=-1][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2588.28555824824, + "gpu_time_us_python": 2608.3031498227997, + "gpu_noise_us_cpp": 1.58, + "gpu_noise_us_python": 4.036, + "gpu_bwutil_cpp": 0.04945378385464845, + "gpu_bwutil_python": 0.04907431899150244 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2319.01026125768, + "gpu_time_us_python": 2332.9844334762197, + "gpu_noise_us_cpp": 1.5539999999999998, + "gpu_noise_us_python": 3.492, + "gpu_bwutil_cpp": 0.04210355063460246, + "gpu_bwutil_python": 0.04185146304822904 + } + } + } + }, + "bilateralfilter_float3_planar_nchw_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "diameter": [ + -1 + ] + }, + "float64_axes": { + "sigmaSpace": [ + 1.2 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "BilateralFilter RGBf32 native planar (basic)." + }, + "baselines": { + "bilateralfilter_float3_planar_nchw_basic[InOutDataType=float3][shape=16x1080x1920][border=REFLECT][layout=NCHW][inputKind=Tensor][diameter=-1][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2172.5539480405996, + "gpu_time_us_python": 2186.3903947316203, + "gpu_noise_us_cpp": 1.8720000000000003, + "gpu_noise_us_python": 2.7660000000000005, + "gpu_bwutil_cpp": 0.23566838408332433, + "gpu_bwutil_python": 0.23417777993543712 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1801.1613869160599, + "gpu_time_us_python": 1812.8184817768401, + "gpu_noise_us_cpp": 1.6660000000000004, + "gpu_noise_us_python": 5.8, + "gpu_bwutil_cpp": 0.2168455150035919, + "gpu_bwutil_python": 0.21544965115116127 + } + } + } + }, + "bilateralfilter_float3_planar_nchw_varshape_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "VarShape" + ] + }, + "int64_axes": { + "diameter": [ + -1 + ] + }, + "float64_axes": { + "sigmaSpace": [ + 1.2 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "BilateralFilter RGBf32 native planar VarShape (advanced)." + }, + "baselines": { + "bilateralfilter_float3_planar_nchw_varshape_advanced[InOutDataType=float3][shape=16x1080x1920][border=REFLECT][layout=NCHW][inputKind=VarShape][diameter=-1][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2199.64363156696, + "gpu_time_us_python": 2214.93160051688, + "gpu_noise_us_cpp": 2.1420000000000003, + "gpu_noise_us_python": 3.2399999999999998, + "gpu_bwutil_cpp": 0.23276747994811667, + "gpu_bwutil_python": 0.23116008571952545 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1879.5776723486797, + "gpu_time_us_python": 1894.6434400686, + "gpu_noise_us_cpp": 0.9540000000000001, + "gpu_noise_us_python": 4.244, + "gpu_bwutil_cpp": 0.20779700277704433, + "gpu_bwutil_python": 0.20614811474992223 + } + } + } + }, + "bilateralfilter_float3_fakeplanar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "diameter": [ + -1 + ] + }, + "float64_axes": { + "sigmaSpace": [ + 1.2 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "BilateralFilter RGBf32 fake-planar tensor comparison (advanced)." + }, + "baselines": { + "bilateralfilter_float3_fakeplanar_nchw_advanced[InOutDataType=float3][shape=16x1080x1920][border=REFLECT][layout=NCHW_FAKE][inputKind=Tensor][diameter=-1][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3386.6835233327, + "gpu_time_us_python": 3405.3645720352397, + "gpu_noise_us_cpp": 1.9760000000000002, + "gpu_noise_us_python": 3.1279999999999997, + "gpu_bwutil_cpp": 0.15118090850292104, + "gpu_bwutil_python": 0.1503519874925939 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2933.42154504152, + "gpu_time_us_python": 2949.2820479279203, + "gpu_noise_us_cpp": 1.65, + "gpu_noise_us_python": 5.046, + "gpu_bwutil_cpp": 0.1331328779090528, + "gpu_bwutil_python": 0.13241587111318015 + } + } + } + }, + "bilateralfilter_uchar4_planar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "8x1080x1920" + ], + "border": [ + "REFLECT" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "diameter": [ + -1 + ] + }, + "float64_axes": { + "sigmaSpace": [ + 1.2 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "BilateralFilter RGBA8 native planar tensor (advanced)." + }, + "baselines": { + "bilateralfilter_uchar4_planar_nchw_advanced[InOutDataType=uchar4][shape=8x1080x1920][border=REFLECT][layout=NCHW][inputKind=Tensor][diameter=-1][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1304.79228685348, + "gpu_time_us_python": 1317.73286275734, + "gpu_noise_us_cpp": 2.202, + "gpu_noise_us_python": 3.5580000000000007, + "gpu_bwutil_cpp": 0.06540030010982234, + "gpu_bwutil_python": 0.06475811671569312 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1109.9891198683001, + "gpu_time_us_python": 1121.4554852466802, + "gpu_noise_us_cpp": 0.9340000000000002, + "gpu_noise_us_python": 2.592, + "gpu_bwutil_cpp": 0.058644630513556915, + "gpu_bwutil_python": 0.058043866150996916 + } + } + } + }, + "bilateralfilter_uchar4_fakeplanar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "8x1080x1920" + ], + "border": [ + "REFLECT" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "diameter": [ + -1 + ] + }, + "float64_axes": { + "sigmaSpace": [ + 1.2 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "BilateralFilter RGBA8 fake-planar tensor comparison (advanced)." + }, + "baselines": { + "bilateralfilter_uchar4_fakeplanar_nchw_advanced[InOutDataType=uchar4][shape=8x1080x1920][border=REFLECT][layout=NCHW_FAKE][inputKind=Tensor][diameter=-1][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1561.34232863316, + "gpu_time_us_python": 1579.0247644201602, + "gpu_noise_us_cpp": 2.286, + "gpu_noise_us_python": 3.6079999999999997, + "gpu_bwutil_cpp": 0.05465437677924386, + "gpu_bwutil_python": 0.05404217719801112 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1372.05315100672, + "gpu_time_us_python": 1386.91772638948, + "gpu_noise_us_cpp": 0.924, + "gpu_noise_us_python": 2.7459999999999996, + "gpu_bwutil_cpp": 0.047441169589989715, + "gpu_bwutil_python": 0.04693268104632072 + } + } + } + }, + "bilateralfilter_float4_planar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "8x1080x1920" + ], + "border": [ + "REFLECT" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "int64_axes": { + "diameter": [ + -1 + ] + }, + "float64_axes": { + "sigmaSpace": [ + 1.2 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "BilateralFilter RGBAf32 native planar (advanced)." + }, + "baselines": { + "bilateralfilter_float4_planar_nchw_advanced[InOutDataType=float4][shape=8x1080x1920][border=REFLECT][layout=NCHW][inputKind=Tensor][diameter=-1][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1674.49087651886, + "gpu_time_us_python": 1687.38426064458, + "gpu_noise_us_cpp": 1.6460000000000001, + "gpu_noise_us_python": 5.886, + "gpu_bwutil_cpp": 0.20384538923423623, + "gpu_bwutil_python": 0.2022880026521953 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1476.626411794, + "gpu_time_us_python": 1487.46757867324, + "gpu_noise_us_cpp": 0.958, + "gpu_noise_us_python": 3.3579999999999997, + "gpu_bwutil_cpp": 0.1763358628060489, + "gpu_bwutil_python": 0.17504878944410165 + } + }, + "bilateralfilter_float4_planar_nchw_advanced[InOutDataType=float4][shape=8x1080x1920][border=REFLECT][layout=NCHW][inputKind=VarShape][diameter=-1][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1350.88292465134, + "gpu_time_us_python": 1368.12953077254, + "gpu_noise_us_cpp": 1.0639999999999998, + "gpu_noise_us_python": 4.048, + "gpu_bwutil_cpp": 0.2526765998777135, + "gpu_bwutil_python": 0.24949358790801712 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1141.76398724622, + "gpu_time_us_python": 1157.0650420239601, + "gpu_noise_us_cpp": 0.9620000000000001, + "gpu_noise_us_python": 3.7320000000000007, + "gpu_bwutil_cpp": 0.22804862282336608, + "gpu_bwutil_python": 0.22503890574306676 + } + } + } + }, + "bilateralfilter_float4_fakeplanar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "8x1080x1920" + ], + "border": [ + "REFLECT" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "diameter": [ + -1 + ] + }, + "float64_axes": { + "sigmaSpace": [ + 1.2 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "BilateralFilter RGBAf32 fake-planar tensor comparison (advanced)." + }, + "baselines": { + "bilateralfilter_float4_fakeplanar_nchw_advanced[InOutDataType=float4][shape=8x1080x1920][border=REFLECT][layout=NCHW_FAKE][inputKind=Tensor][diameter=-1][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2141.1070138979603, + "gpu_time_us_python": 2160.1262294012404, + "gpu_noise_us_cpp": 1.89, + "gpu_noise_us_python": 4.24, + "gpu_bwutil_cpp": 0.1594195840150773, + "gpu_bwutil_python": 0.1580162173990721 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1781.9689959803, + "gpu_time_us_python": 1796.6093900027201, + "gpu_noise_us_cpp": 2.222, + "gpu_noise_us_python": 4.2219999999999995, + "gpu_bwutil_cpp": 0.14610184941500282, + "gpu_bwutil_python": 0.14491228676058449 + } + } + } + }, + "bilateralfilter_rgb_auto_planar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "int64_axes": { + "diameter": [ + -1 + ] + }, + "float64_axes": { + "sigmaSpace": [ + 1.2 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "BilateralFilter RGB8 auto native planar (advanced)." + }, + "baselines": { + "bilateralfilter_rgb_auto_planar_nchw_advanced[InOutDataType=uchar3][shape=16x1080x1920][border=REFLECT][layout=NCHW][inputKind=Tensor][diameter=-1][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2246.6730073803596, + "gpu_time_us_python": 2261.25401462516, + "gpu_noise_us_cpp": 2.942, + "gpu_noise_us_python": 5.0920000000000005, + "gpu_bwutil_cpp": 0.05697347750303644, + "gpu_bwutil_python": 0.05660626748305162 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1928.16045051978, + "gpu_time_us_python": 1940.3518635579999, + "gpu_noise_us_cpp": 1.198, + "gpu_noise_us_python": 5.529999999999999, + "gpu_bwutil_cpp": 0.05064055684855216, + "gpu_bwutil_python": 0.05032164782828752 + } + }, + "bilateralfilter_rgb_auto_planar_nchw_advanced[InOutDataType=uchar3][shape=16x1080x1920][border=REFLECT][layout=NCHW][inputKind=VarShape][diameter=-1][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2120.90385123166, + "gpu_time_us_python": 2137.64790411046, + "gpu_noise_us_cpp": 2.88, + "gpu_noise_us_python": 10.03, + "gpu_bwutil_cpp": 0.06035203431919346, + "gpu_bwutil_python": 0.05987925947095718 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1881.5156133045998, + "gpu_time_us_python": 1897.48744217582, + "gpu_noise_us_cpp": 1.0239999999999998, + "gpu_noise_us_python": 5.264, + "gpu_bwutil_cpp": 0.051895627210173254, + "gpu_bwutil_python": 0.0514592759397172 + } + } + } + }, + "bilateralfilter_rgb_auto_fakeplanar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "diameter": [ + -1 + ] + }, + "float64_axes": { + "sigmaSpace": [ + 1.2 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "BilateralFilter RGB8 auto fake-planar tensor comparison (advanced)." + }, + "baselines": { + "bilateralfilter_rgb_auto_fakeplanar_nchw_advanced[InOutDataType=uchar3][shape=16x1080x1920][border=REFLECT][layout=NCHW_FAKE][inputKind=Tensor][diameter=-1][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2590.93319706076, + "gpu_time_us_python": 2610.7659380767195, + "gpu_noise_us_cpp": 1.5499999999999998, + "gpu_noise_us_python": 3.724, + "gpu_bwutil_cpp": 0.04940352002713444, + "gpu_bwutil_python": 0.049028212228734734 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2318.8821320626603, + "gpu_time_us_python": 2335.4697947591003, + "gpu_noise_us_cpp": 1.006, + "gpu_noise_us_python": 5.572, + "gpu_bwutil_cpp": 0.04210590663459839, + "gpu_bwutil_python": 0.041807225231232106 + } + } + } + } + } +} diff --git a/bench/config/operators/bndbox.json b/bench/config/operators/bndbox.json new file mode 100644 index 000000000..a59f35d27 --- /dev/null +++ b/bench/config/operators/bndbox.json @@ -0,0 +1,505 @@ +{ + "benchmark": "bndbox", + "configs": { + "bndbox_basic": { + "tier": "basic", + "dtypes": [ + "uchar3", + "uchar4" + ], + "string_axes": { + "shape": [ + "16x720x1280" + ], + "inputKind": [ + "Tensor" + ], + "layout": [ + "NHWC", + "NCHW" + ] + }, + "int64_axes": { + "numBoxes": [ + 10 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "BndBox RGB/RGBA (basic)." + }, + "baselines": { + "bndbox_basic[InOutDataType=uchar3][shape=16x720x1280][inputKind=Tensor][layout=NHWC][numBoxes=10]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 157.95263054907997, + "gpu_time_us_python": 168.60401475874, + "gpu_noise_us_cpp": 3.174, + "gpu_noise_us_python": 3.95, + "gpu_bwutil_cpp": 0.36019447018795714, + "gpu_bwutil_python": 0.33745428398188343, + "gpu_gap_stddev_us": 0.7209520514808225 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 154.01157054124, + "gpu_time_us_python": 165.35667500824, + "gpu_noise_us_cpp": 2.572, + "gpu_noise_us_python": 4.364, + "gpu_bwutil_cpp": 0.281745724746156, + "gpu_bwutil_python": 0.26242126992097753, + "gpu_gap_stddev_us": 0.8497488085296298 + } + }, + "bndbox_basic[InOutDataType=uchar4][shape=16x720x1280][inputKind=Tensor][layout=NHWC][numBoxes=10]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 181.00142233214, + "gpu_time_us_python": 190.54802902972, + "gpu_noise_us_cpp": 3.586, + "gpu_noise_us_python": 4.074, + "gpu_bwutil_cpp": 0.4190960977026421, + "gpu_bwutil_python": 0.3981619823215746, + "gpu_gap_stddev_us": 3.1897598454482767 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 175.40076887138, + "gpu_time_us_python": 187.32713914348, + "gpu_noise_us_cpp": 3.7079999999999997, + "gpu_noise_us_python": 4.534, + "gpu_bwutil_cpp": 0.3298850727386432, + "gpu_bwutil_python": 0.30884796300947726, + "gpu_gap_stddev_us": 2.5971506049996593 + } + }, + "bndbox_basic[InOutDataType=uchar3][shape=16x720x1280][inputKind=Tensor][layout=NCHW][numBoxes=10]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 350.3054526728, + "gpu_time_us_python": 362.29666914986, + "gpu_noise_us_cpp": 3.072, + "gpu_noise_us_python": 4.664, + "gpu_bwutil_cpp": 0.48720531780066684, + "gpu_bwutil_python": 0.47109707278585555, + "gpu_gap_stddev_us": 1.7443438669642404 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 288.57571327226003, + "gpu_time_us_python": 296.84984377456, + "gpu_noise_us_cpp": 3.1800000000000006, + "gpu_noise_us_python": 4.712000000000001, + "gpu_bwutil_cpp": 0.4510835742806898, + "gpu_bwutil_python": 0.4385111003238321, + "gpu_gap_stddev_us": 0.5248072568687054 + } + }, + "bndbox_basic[InOutDataType=uchar4][shape=16x720x1280][inputKind=Tensor][layout=NCHW][numBoxes=10]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 431.6860903316, + "gpu_time_us_python": 443.12737528726, + "gpu_noise_us_cpp": 3.7039999999999997, + "gpu_noise_us_python": 4.202, + "gpu_bwutil_cpp": 0.5271497386958768, + "gpu_bwutil_python": 0.513562958542324, + "gpu_gap_stddev_us": 2.175728172994232 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 352.31712414218, + "gpu_time_us_python": 360.44132126646, + "gpu_noise_us_cpp": 4.136, + "gpu_noise_us_python": 6.018, + "gpu_bwutil_cpp": 0.4926298666992303, + "gpu_bwutil_python": 0.4815278565490882, + "gpu_gap_stddev_us": 0.846146504771457 + } + } + } + }, + "bndbox_10boxes_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3", + "uchar4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "inputKind": [ + "Tensor" + ], + "layout": [ + "NHWC", + "NCHW" + ] + }, + "int64_axes": { + "numBoxes": [ + 10 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "BndBox RGB/RGBA 10boxes (advanced)." + }, + "baselines": { + "bndbox_10boxes_1080p_advanced[InOutDataType=uchar3][shape=16x1080x1920][inputKind=Tensor][layout=NHWC][numBoxes=10]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 268.5001992254, + "gpu_time_us_python": 279.79744837594, + "gpu_noise_us_cpp": 3.4539999999999997, + "gpu_noise_us_python": 3.9939999999999998, + "gpu_bwutil_cpp": 0.4767412872313326, + "gpu_bwutil_python": 0.4575417579082438, + "gpu_gap_stddev_us": 2.3795459272458643 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 264.9191462146, + "gpu_time_us_python": 272.99378855514, + "gpu_noise_us_cpp": 3.5020000000000002, + "gpu_noise_us_python": 5.246, + "gpu_bwutil_cpp": 0.3685256724716829, + "gpu_bwutil_python": 0.3576279873471172, + "gpu_gap_stddev_us": 0.8253098841542188 + } + }, + "bndbox_10boxes_1080p_advanced[InOutDataType=uchar4][shape=16x1080x1920][inputKind=Tensor][layout=NHWC][numBoxes=10]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 317.70947356453996, + "gpu_time_us_python": 328.63615055135995, + "gpu_noise_us_cpp": 3.636, + "gpu_noise_us_python": 4.198, + "gpu_bwutil_cpp": 0.5371922992218476, + "gpu_bwutil_python": 0.5193796433124547, + "gpu_gap_stddev_us": 3.124353011800666 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 313.7183011583, + "gpu_time_us_python": 323.01036906589997, + "gpu_noise_us_cpp": 3.476, + "gpu_noise_us_python": 4.536, + "gpu_bwutil_cpp": 0.41493264978986544, + "gpu_bwutil_python": 0.40300415458115857, + "gpu_gap_stddev_us": 1.312398180578562 + } + }, + "bndbox_10boxes_1080p_advanced[InOutDataType=uchar3][shape=16x1080x1920][inputKind=Tensor][layout=NCHW][numBoxes=10]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 675.92557879328, + "gpu_time_us_python": 688.4592810919, + "gpu_noise_us_cpp": 3.7340000000000004, + "gpu_noise_us_python": 5.354, + "gpu_bwutil_cpp": 0.5681162570205694, + "gpu_bwutil_python": 0.5577809997964456, + "gpu_gap_stddev_us": 2.273050286054552 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 543.07101750692, + "gpu_time_us_python": 555.7109160903799, + "gpu_noise_us_cpp": 4.1, + "gpu_noise_us_python": 4.94, + "gpu_bwutil_cpp": 0.53931036992342, + "gpu_bwutil_python": 0.5270437373872586, + "gpu_gap_stddev_us": 0.8865705429913091 + } + }, + "bndbox_10boxes_1080p_advanced[InOutDataType=uchar4][shape=16x1080x1920][inputKind=Tensor][layout=NCHW][numBoxes=10]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 852.2812505328599, + "gpu_time_us_python": 864.10434276744, + "gpu_noise_us_cpp": 5.0520000000000005, + "gpu_noise_us_python": 6.176, + "gpu_bwutil_cpp": 0.600745459673949, + "gpu_bwutil_python": 0.5925318319569207, + "gpu_gap_stddev_us": 1.97892660676107 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 687.2062723344201, + "gpu_time_us_python": 699.10597513062, + "gpu_noise_us_cpp": 4.096, + "gpu_noise_us_python": 5.846000000000001, + "gpu_bwutil_cpp": 0.5682585181524467, + "gpu_bwutil_python": 0.5585860478842603, + "gpu_gap_stddev_us": 0.621105917567566 + } + } + } + }, + "bndbox_200boxes_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3", + "uchar4" + ], + "string_axes": { + "shape": [ + "3x1080x1920" + ], + "inputKind": [ + "Tensor" + ], + "layout": [ + "NHWC", + "NCHW" + ] + }, + "int64_axes": { + "numBoxes": [ + 200 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "BndBox RGB/RGBA 200boxes (advanced)." + }, + "baselines": { + "bndbox_200boxes_1080p_advanced[InOutDataType=uchar3][shape=3x1080x1920][inputKind=Tensor][layout=NHWC][numBoxes=200]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 849.6802997225801, + "gpu_time_us_python": 862.6248459737001, + "gpu_noise_us_cpp": 2.416, + "gpu_noise_us_python": 5.464, + "gpu_bwutil_cpp": 0.02825935501186354, + "gpu_bwutil_python": 0.02783547561394556, + "gpu_gap_stddev_us": 0.951297448293884 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 866.9585136550601, + "gpu_time_us_python": 886.48122907494, + "gpu_noise_us_cpp": 9.698, + "gpu_noise_us_python": 15.495999999999999, + "gpu_bwutil_cpp": 0.021123670247274823, + "gpu_bwutil_python": 0.020659136559251577, + "gpu_gap_stddev_us": 5.612470879796421 + } + }, + "bndbox_200boxes_1080p_advanced[InOutDataType=uchar4][shape=3x1080x1920][inputKind=Tensor][layout=NHWC][numBoxes=200]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 867.14748004836, + "gpu_time_us_python": 878.0560256333602, + "gpu_noise_us_cpp": 3.146, + "gpu_noise_us_python": 4.92, + "gpu_bwutil_cpp": 0.03691686034813714, + "gpu_bwutil_python": 0.036459072578014264, + "gpu_gap_stddev_us": 2.1093571006149436 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 875.8903272084801, + "gpu_time_us_python": 886.09765670512, + "gpu_noise_us_cpp": 6.214, + "gpu_noise_us_python": 7.967999999999999, + "gpu_bwutil_cpp": 0.02787451740615402, + "gpu_bwutil_python": 0.02755364339496392, + "gpu_gap_stddev_us": 3.056417555568685 + } + }, + "bndbox_200boxes_1080p_advanced[InOutDataType=uchar3][shape=3x1080x1920][inputKind=Tensor][layout=NCHW][numBoxes=200]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 947.36887316226, + "gpu_time_us_python": 959.4324003415401, + "gpu_noise_us_cpp": 3.1020000000000003, + "gpu_noise_us_python": 4.494, + "gpu_bwutil_cpp": 0.07601481061645225, + "gpu_bwutil_python": 0.07505917731367544, + "gpu_gap_stddev_us": 7.208858592960481 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 929.5603820563599, + "gpu_time_us_python": 942.8068545899599, + "gpu_noise_us_cpp": 9.292, + "gpu_noise_us_python": 10.693999999999999, + "gpu_bwutil_cpp": 0.05908546510264786, + "gpu_bwutil_python": 0.05825536046930609, + "gpu_gap_stddev_us": 0.8837008302646465 + } + }, + "bndbox_200boxes_1080p_advanced[InOutDataType=uchar4][shape=3x1080x1920][inputKind=Tensor][layout=NCHW][numBoxes=200]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 987.6448941033199, + "gpu_time_us_python": 996.56798778336, + "gpu_noise_us_cpp": 3.374, + "gpu_noise_us_python": 3.7479999999999998, + "gpu_bwutil_cpp": 0.09721506569488442, + "gpu_bwutil_python": 0.09634787315166005, + "gpu_gap_stddev_us": 3.4997512339613857 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 968.51817824176, + "gpu_time_us_python": 975.2418038635599, + "gpu_noise_us_cpp": 7.016, + "gpu_noise_us_python": 9.226, + "gpu_bwutil_cpp": 0.07560890263957988, + "gpu_bwutil_python": 0.07508769027572186, + "gpu_gap_stddev_us": 1.6164355140794242 + } + } + } + }, + "bndbox_planar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3", + "uchar4" + ], + "string_axes": { + "shape": [ + "16x720x1280" + ], + "inputKind": [ + "Tensor" + ], + "layout": [ + "NCHW" + ] + }, + "int64_axes": { + "numBoxes": [ + 10 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "BndBox RGB/RGBA native-planar Tensor comparison (advanced)." + }, + "baselines": { + "bndbox_planar_nchw_advanced[InOutDataType=uchar3][shape=16x720x1280][inputKind=Tensor][layout=NCHW][numBoxes=10]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 351.2441339701, + "gpu_time_us_python": 363.49827118461997, + "gpu_noise_us_cpp": 3.3, + "gpu_noise_us_python": 4.218000000000001, + "gpu_bwutil_cpp": 0.4859086917801484, + "gpu_bwutil_python": 0.4695650585486077, + "gpu_gap_stddev_us": 2.482070355612337 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 288.82100435884, + "gpu_time_us_python": 297.03319751594, + "gpu_noise_us_cpp": 3.936, + "gpu_noise_us_python": 5.359999999999999, + "gpu_bwutil_cpp": 0.4507003082629525, + "gpu_bwutil_python": 0.43824011947261965, + "gpu_gap_stddev_us": 0.3675774735173519 + } + }, + "bndbox_planar_nchw_advanced[InOutDataType=uchar4][shape=16x720x1280][inputKind=Tensor][layout=NCHW][numBoxes=10]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 429.79843274719997, + "gpu_time_us_python": 441.61397179214, + "gpu_noise_us_cpp": 4.56, + "gpu_noise_us_python": 4.231999999999999, + "gpu_bwutil_cpp": 0.5294593809336102, + "gpu_bwutil_python": 0.5153322563891749, + "gpu_gap_stddev_us": 3.5740887859573256 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 351.53547747596, + "gpu_time_us_python": 360.76300466812, + "gpu_noise_us_cpp": 4.087999999999999, + "gpu_noise_us_python": 5.4799999999999995, + "gpu_bwutil_cpp": 0.4937247745215284, + "gpu_bwutil_python": 0.4810971270624423, + "gpu_gap_stddev_us": 1.1106084016623943 + } + } + } + }, + "bndbox_fakeplanar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3", + "uchar4" + ], + "string_axes": { + "shape": [ + "16x720x1280" + ], + "inputKind": [ + "Tensor" + ], + "layout": [ + "NCHW_FAKE" + ] + }, + "int64_axes": { + "numBoxes": [ + 10 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "BndBox RGB/RGBA (advanced) (fake planar NCHW_FAKE tensor-only)." + }, + "baselines": { + "bndbox_fakeplanar_nchw_advanced[InOutDataType=uchar3][shape=16x720x1280][inputKind=Tensor][layout=NCHW_FAKE][numBoxes=10]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 442.04939945434, + "gpu_time_us_python": 457.8321920894999, + "gpu_noise_us_cpp": 4.166, + "gpu_noise_us_python": 4.736, + "gpu_bwutil_cpp": 0.3860904282005445, + "gpu_bwutil_python": 0.37278788355620884, + "gpu_gap_stddev_us": 1.476423983566551 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 377.1691036097, + "gpu_time_us_python": 389.03264406484004, + "gpu_noise_us_cpp": 3.382, + "gpu_noise_us_python": 4.422, + "gpu_bwutil_cpp": 0.34512834057119657, + "gpu_bwutil_python": 0.3346039522898422, + "gpu_gap_stddev_us": 0.6257107831512612 + } + }, + "bndbox_fakeplanar_nchw_advanced[InOutDataType=uchar4][shape=16x720x1280][inputKind=Tensor][layout=NCHW_FAKE][numBoxes=10]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 543.5797977842001, + "gpu_time_us_python": 559.3654037835, + "gpu_noise_us_cpp": 3.5200000000000005, + "gpu_noise_us_python": 4.9159999999999995, + "gpu_bwutil_cpp": 0.41863124248135175, + "gpu_bwutil_python": 0.4068207046055342, + "gpu_gap_stddev_us": 1.2583493263459777 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 461.10654204292007, + "gpu_time_us_python": 476.38978086654, + "gpu_noise_us_cpp": 3.914, + "gpu_noise_us_python": 5.73, + "gpu_bwutil_cpp": 0.37640263158186127, + "gpu_bwutil_python": 0.36432708409134185, + "gpu_gap_stddev_us": 0.911931464104236 + } + } + } + } + } +} diff --git a/bench/config/operators/boxblur.json b/bench/config/operators/boxblur.json new file mode 100644 index 000000000..6bd9147c1 --- /dev/null +++ b/bench/config/operators/boxblur.json @@ -0,0 +1,1071 @@ +{ + "benchmark": "boxblur", + "configs": { + "boxblur_basic": { + "tier": "basic", + "dtypes": [ + "uchar3", + "uchar4" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "boxSize": [ + "12x34" + ], + "boxPattern": [ + "fixed" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "numBoxes": [ + 4 + ], + "kernelSize": [ + 5 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "BoxBlur RGB/RGBA (basic)." + }, + "baselines": { + "boxblur_basic[InOutDataType=uchar3][shape=64x1080x1920][boxSize=12x34][boxPattern=fixed][layout=NHWC][inputKind=Tensor][numBoxes=4][kernelSize=5]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 615.77948234036, + "gpu_time_us_python": 625.46037243082, + "gpu_noise_us_cpp": 3.628, + "gpu_noise_us_python": 3.876, + "gpu_bwutil_cpp": 0.8314740157500967, + "gpu_bwutil_python": 0.818607027700385, + "gpu_gap_stddev_us": 1.42561399080696 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 474.52848322827504, + "gpu_time_us_python": 483.974549583225, + "gpu_noise_us_cpp": 3.435, + "gpu_noise_us_python": 5.5175, + "gpu_bwutil_cpp": 0.822945400140943, + "gpu_bwutil_python": 0.8068842573571935, + "gpu_gap_stddev_us": 0.46883033157169324 + } + }, + "boxblur_basic[InOutDataType=uchar4][shape=64x1080x1920][boxSize=12x34][boxPattern=fixed][layout=NHWC][inputKind=Tensor][numBoxes=4][kernelSize=5]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 806.2948146640999, + "gpu_time_us_python": 816.47783334646, + "gpu_noise_us_cpp": 3.6479999999999997, + "gpu_noise_us_python": 5.17, + "gpu_bwutil_cpp": 0.8466775076662593, + "gpu_bwutil_python": 0.8361195643995638, + "gpu_gap_stddev_us": 0.6615613689138873 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 617.8799802506, + "gpu_time_us_python": 628.289304394625, + "gpu_noise_us_cpp": 3.6774999999999998, + "gpu_noise_us_python": 4.4075, + "gpu_bwutil_cpp": 0.8426887752037927, + "gpu_bwutil_python": 0.828734739736078, + "gpu_gap_stddev_us": 2.09849826441401 + } + } + } + }, + "boxblur_k5_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3", + "uchar4" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "boxSize": [ + "64x64" + ], + "boxPattern": [ + "grid" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "numBoxes": [ + 4 + ], + "kernelSize": [ + 5 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "BoxBlur RGB/RGBA k5 (advanced)." + }, + "baselines": { + "boxblur_k5_1080p_advanced[InOutDataType=uchar3][shape=64x1080x1920][boxSize=64x64][boxPattern=grid][layout=NHWC][inputKind=Tensor][numBoxes=4][kernelSize=5]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 620.0180973182399, + "gpu_time_us_python": 630.27454156998, + "gpu_noise_us_cpp": 3.97, + "gpu_noise_us_python": 4.7, + "gpu_bwutil_cpp": 0.8257901540200072, + "gpu_bwutil_python": 0.812355734474289, + "gpu_gap_stddev_us": 1.0425359524185112 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 478.494610916125, + "gpu_time_us_python": 488.65277981730003, + "gpu_noise_us_cpp": 3.74, + "gpu_noise_us_python": 5.154999999999999, + "gpu_bwutil_cpp": 0.8161241859523148, + "gpu_bwutil_python": 0.7991613126800176, + "gpu_gap_stddev_us": 0.9309791045482311 + } + }, + "boxblur_k5_1080p_advanced[InOutDataType=uchar4][shape=64x1080x1920][boxSize=64x64][boxPattern=grid][layout=NHWC][inputKind=Tensor][numBoxes=4][kernelSize=5]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 809.6798395354601, + "gpu_time_us_python": 820.1582589972, + "gpu_noise_us_cpp": 4.1419999999999995, + "gpu_noise_us_python": 4.6080000000000005, + "gpu_bwutil_cpp": 0.8431367541500171, + "gpu_bwutil_python": 0.8323650785385877, + "gpu_gap_stddev_us": 0.566678251863799 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 620.6164561193, + "gpu_time_us_python": 631.2929488265249, + "gpu_noise_us_cpp": 4.02, + "gpu_noise_us_python": 4.609999999999999, + "gpu_bwutil_cpp": 0.8389730558973328, + "gpu_bwutil_python": 0.8247865965595613, + "gpu_gap_stddev_us": 1.2518009245252095 + } + } + } + }, + "boxblur_k17_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3", + "uchar4" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "boxSize": [ + "64x64" + ], + "boxPattern": [ + "grid" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "numBoxes": [ + 4 + ], + "kernelSize": [ + 17 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "BoxBlur RGB/RGBA k17 (advanced)." + }, + "baselines": { + "boxblur_k17_1080p_advanced[InOutDataType=uchar3][shape=64x1080x1920][boxSize=64x64][boxPattern=grid][layout=NHWC][inputKind=Tensor][numBoxes=4][kernelSize=17]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 693.2682603707401, + "gpu_time_us_python": 703.41607050396, + "gpu_noise_us_cpp": 3.822, + "gpu_noise_us_python": 4.456, + "gpu_bwutil_cpp": 0.7385370772170324, + "gpu_bwutil_python": 0.7278858091520094, + "gpu_gap_stddev_us": 1.3117046726456318 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 550.33522027155, + "gpu_time_us_python": 560.34494823195, + "gpu_noise_us_cpp": 3.4925, + "gpu_noise_us_python": 4.047499999999999, + "gpu_bwutil_cpp": 0.709589003603774, + "gpu_bwutil_python": 0.6969161939987887, + "gpu_gap_stddev_us": 1.1241713157838338 + } + }, + "boxblur_k17_1080p_advanced[InOutDataType=uchar4][shape=64x1080x1920][boxSize=64x64][boxPattern=grid][layout=NHWC][inputKind=Tensor][numBoxes=4][kernelSize=17]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 882.25169748542, + "gpu_time_us_python": 892.6035386371799, + "gpu_noise_us_cpp": 4.096000000000001, + "gpu_noise_us_python": 3.992, + "gpu_bwutil_cpp": 0.773783077284475, + "gpu_bwutil_python": 0.7648099965633143, + "gpu_gap_stddev_us": 0.7599409393497519 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 690.9427570400501, + "gpu_time_us_python": 700.7914394751751, + "gpu_noise_us_cpp": 3.5525, + "gpu_noise_us_python": 4.1525, + "gpu_bwutil_cpp": 0.7535800108818175, + "gpu_bwutil_python": 0.742991374717642, + "gpu_gap_stddev_us": 0.9927491910577864 + } + } + } + }, + "boxblur_manyboxes_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3", + "uchar4" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "boxSize": [ + "64x64" + ], + "boxPattern": [ + "grid" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "numBoxes": [ + 64 + ], + "kernelSize": [ + 5 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "BoxBlur RGB/RGBA manyboxes (advanced)." + }, + "baselines": { + "boxblur_manyboxes_1080p_advanced[InOutDataType=uchar3][shape=64x1080x1920][boxSize=64x64][boxPattern=grid][layout=NHWC][inputKind=Tensor][numBoxes=64][kernelSize=5]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 901.73802611466, + "gpu_time_us_python": 912.28058696214, + "gpu_noise_us_cpp": 3.6339999999999995, + "gpu_noise_us_python": 4.704000000000001, + "gpu_bwutil_cpp": 0.5679424563623034, + "gpu_bwutil_python": 0.5613848500776291, + "gpu_gap_stddev_us": 1.034701782976245 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 733.4741770967751, + "gpu_time_us_python": 746.081933435275, + "gpu_noise_us_cpp": 3.89, + "gpu_noise_us_python": 5.0375000000000005, + "gpu_bwutil_cpp": 0.5324726012176574, + "gpu_bwutil_python": 0.5234751325032775, + "gpu_gap_stddev_us": 2.98518478096814 + } + }, + "boxblur_manyboxes_1080p_advanced[InOutDataType=uchar4][shape=64x1080x1920][boxSize=64x64][boxPattern=grid][layout=NHWC][inputKind=Tensor][numBoxes=64][kernelSize=5]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1074.8941732015403, + "gpu_time_us_python": 1086.6928720626402, + "gpu_noise_us_cpp": 4.432, + "gpu_noise_us_python": 3.982, + "gpu_bwutil_cpp": 0.6352222714640584, + "gpu_bwutil_python": 0.628323041148999, + "gpu_gap_stddev_us": 1.6480403989050434 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 860.8786268882, + "gpu_time_us_python": 873.560799793725, + "gpu_noise_us_cpp": 3.97, + "gpu_noise_us_python": 4.7025, + "gpu_bwutil_cpp": 0.6048753497266791, + "gpu_bwutil_python": 0.5960885874980277, + "gpu_gap_stddev_us": 2.888780253990511 + } + } + } + }, + "boxblur_planar_basic": { + "tier": "basic", + "dtypes": [ + "uchar3", + "uchar4" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "boxSize": [ + "12x34" + ], + "boxPattern": [ + "fixed" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "numBoxes": [ + 4 + ], + "kernelSize": [ + 5 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "BoxBlur RGB/RGBA (basic) (planar NCHW basic)." + }, + "baselines": { + "boxblur_planar_basic[InOutDataType=uchar3][shape=64x1080x1920][boxSize=12x34][boxPattern=fixed][layout=NCHW][inputKind=Tensor][numBoxes=4][kernelSize=5]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 710.8453473952201, + "gpu_time_us_python": 721.66613522456, + "gpu_noise_us_cpp": 3.2020000000000004, + "gpu_noise_us_python": 3.9579999999999997, + "gpu_bwutil_cpp": 0.720275630679514, + "gpu_bwutil_python": 0.7094762650301297, + "gpu_gap_stddev_us": 0.3348973209285836 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 671.381297821725, + "gpu_time_us_python": 682.9304693172251, + "gpu_noise_us_cpp": 3.895, + "gpu_noise_us_python": 5.1125, + "gpu_bwutil_cpp": 0.5816596604949952, + "gpu_bwutil_python": 0.571829878140708, + "gpu_gap_stddev_us": 1.477365101831456 + } + }, + "boxblur_planar_basic[InOutDataType=uchar4][shape=64x1080x1920][boxSize=12x34][boxPattern=fixed][layout=NCHW][inputKind=Tensor][numBoxes=4][kernelSize=5]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 927.9243137766, + "gpu_time_us_python": 938.78386782786, + "gpu_noise_us_cpp": 3.6119999999999997, + "gpu_noise_us_python": 4.478000000000001, + "gpu_bwutil_cpp": 0.7356966314921328, + "gpu_bwutil_python": 0.7271867076559996, + "gpu_gap_stddev_us": 0.6985785036291589 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 877.4076851632251, + "gpu_time_us_python": 887.9530095010999, + "gpu_noise_us_cpp": 3.1725000000000003, + "gpu_noise_us_python": 4.8100000000000005, + "gpu_bwutil_cpp": 0.5934348814006499, + "gpu_bwutil_python": 0.5863940203238776, + "gpu_gap_stddev_us": 1.7308102569430384 + } + } + } + }, + "boxblur_planar_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3", + "uchar4" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "boxSize": [ + "12x34" + ], + "boxPattern": [ + "fixed" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "numBoxes": [ + 4 + ], + "kernelSize": [ + 5 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "BoxBlur RGB/RGBA native-planar Tensor comparison (advanced)." + }, + "baselines": { + "boxblur_planar_advanced[InOutDataType=uchar3][shape=64x1080x1920][boxSize=12x34][boxPattern=fixed][layout=NCHW][inputKind=Tensor][numBoxes=4][kernelSize=5]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 711.16118520976, + "gpu_time_us_python": 721.6024679445, + "gpu_noise_us_cpp": 3.7659999999999996, + "gpu_noise_us_python": 3.682, + "gpu_bwutil_cpp": 0.7199549026430639, + "gpu_bwutil_python": 0.7095388178526552, + "gpu_gap_stddev_us": 0.8205386663671527 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 671.3445659269499, + "gpu_time_us_python": 683.4595839024, + "gpu_noise_us_cpp": 3.2074999999999996, + "gpu_noise_us_python": 4.8075, + "gpu_bwutil_cpp": 0.5816922320365774, + "gpu_bwutil_python": 0.5713766335457293, + "gpu_gap_stddev_us": 1.1665933015560888 + } + }, + "boxblur_planar_advanced[InOutDataType=uchar4][shape=64x1080x1920][boxSize=12x34][boxPattern=fixed][layout=NCHW][inputKind=Tensor][numBoxes=4][kernelSize=5]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 928.0152790593002, + "gpu_time_us_python": 938.24479646892, + "gpu_noise_us_cpp": 5.144, + "gpu_noise_us_python": 4.218, + "gpu_bwutil_cpp": 0.7356242923148487, + "gpu_bwutil_python": 0.727603972923818, + "gpu_gap_stddev_us": 1.0380223785312719 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 877.349042813825, + "gpu_time_us_python": 888.6048122131999, + "gpu_noise_us_cpp": 3.4175, + "gpu_noise_us_python": 4.55, + "gpu_bwutil_cpp": 0.5934740279972557, + "gpu_bwutil_python": 0.5859651217007029, + "gpu_gap_stddev_us": 2.2414858565742994 + } + } + } + }, + "boxblur_fakeplanar_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3", + "uchar4" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "boxSize": [ + "12x34" + ], + "boxPattern": [ + "fixed" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "numBoxes": [ + 4 + ], + "kernelSize": [ + 5 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "BoxBlur RGB/RGBA (advanced) (fake-planar NCHW)." + }, + "baselines": { + "boxblur_fakeplanar_advanced[InOutDataType=uchar3][shape=64x1080x1920][boxSize=12x34][boxPattern=fixed][layout=NCHW_FAKE][inputKind=Tensor][numBoxes=4][kernelSize=5]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2982.26351965038, + "gpu_time_us_python": 2999.81283176508, + "gpu_noise_us_cpp": 5.932, + "gpu_noise_us_python": 5.779999999999999, + "gpu_bwutil_cpp": 0.17168284515043533, + "gpu_bwutil_python": 0.1706784682871366, + "gpu_gap_stddev_us": 0.5547460178776217 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2320.5405683631, + "gpu_time_us_python": 2335.674295838225, + "gpu_noise_us_cpp": 4.005000000000001, + "gpu_noise_us_python": 4.9624999999999995, + "gpu_bwutil_cpp": 0.16828533314332195, + "gpu_bwutil_python": 0.167195327797535, + "gpu_gap_stddev_us": 1.5853281161317616 + } + }, + "boxblur_fakeplanar_advanced[InOutDataType=uchar4][shape=64x1080x1920][boxSize=12x34][boxPattern=fixed][layout=NCHW_FAKE][inputKind=Tensor][numBoxes=4][kernelSize=5]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3862.9029934221994, + "gpu_time_us_python": 3885.5317864050603, + "gpu_noise_us_cpp": 4.25, + "gpu_noise_us_python": 4.7700000000000005, + "gpu_bwutil_cpp": 0.17672462005862016, + "gpu_bwutil_python": 0.17569538864370343, + "gpu_gap_stddev_us": 1.1552299635205197 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 3019.6370985818, + "gpu_time_us_python": 3034.8288187061, + "gpu_noise_us_cpp": 3.705, + "gpu_noise_us_python": 8.03, + "gpu_bwutil_cpp": 0.17243297987977635, + "gpu_bwutil_python": 0.1715700284823826, + "gpu_gap_stddev_us": 5.267915245361322 + } + } + } + }, + "boxblur_k5_1080p_planar_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3", + "uchar4" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "boxSize": [ + "64x64" + ], + "boxPattern": [ + "grid" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "numBoxes": [ + 4 + ], + "kernelSize": [ + 5 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "BoxBlur RGB/RGBA k5 (advanced) (planar NCHW advanced)." + }, + "baselines": { + "boxblur_k5_1080p_planar_advanced[InOutDataType=uchar3][shape=64x1080x1920][boxSize=64x64][boxPattern=grid][layout=NCHW][inputKind=Tensor][numBoxes=4][kernelSize=5]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 716.3476995523599, + "gpu_time_us_python": 727.16797895768, + "gpu_noise_us_cpp": 4.032, + "gpu_noise_us_python": 4.396, + "gpu_bwutil_cpp": 0.7147434073879415, + "gpu_bwutil_python": 0.7041108424066319, + "gpu_gap_stddev_us": 1.109162395306854 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 676.775105344675, + "gpu_time_us_python": 688.958153146525, + "gpu_noise_us_cpp": 2.955, + "gpu_noise_us_python": 5.615, + "gpu_bwutil_cpp": 0.5770244505428656, + "gpu_bwutil_python": 0.5668211714389624, + "gpu_gap_stddev_us": 1.3367382880901526 + } + }, + "boxblur_k5_1080p_planar_advanced[InOutDataType=uchar4][shape=64x1080x1920][boxSize=64x64][boxPattern=grid][layout=NCHW][inputKind=Tensor][numBoxes=4][kernelSize=5]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 934.9295746923801, + "gpu_time_us_python": 945.7461931099799, + "gpu_noise_us_cpp": 3.6800000000000006, + "gpu_noise_us_python": 3.7840000000000003, + "gpu_bwutil_cpp": 0.7301838700932928, + "gpu_bwutil_python": 0.7218333199813407, + "gpu_gap_stddev_us": 1.2713217331286848 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 884.42750315695, + "gpu_time_us_python": 894.39270458065, + "gpu_noise_us_cpp": 2.8275, + "gpu_noise_us_python": 4.36, + "gpu_bwutil_cpp": 0.5887248152902806, + "gpu_bwutil_python": 0.58216869034215, + "gpu_gap_stddev_us": 1.4566765461241127 + } + } + } + }, + "boxblur_k5_1080p_fakeplanar_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3", + "uchar4" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "boxSize": [ + "64x64" + ], + "boxPattern": [ + "grid" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "numBoxes": [ + 4 + ], + "kernelSize": [ + 5 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "BoxBlur RGB/RGBA k5 (advanced) (fake-planar NCHW advanced)." + }, + "baselines": { + "boxblur_k5_1080p_fakeplanar_advanced[InOutDataType=uchar3][shape=64x1080x1920][boxSize=64x64][boxPattern=grid][layout=NCHW_FAKE][inputKind=Tensor][numBoxes=4][kernelSize=5]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2986.33026792884, + "gpu_time_us_python": 3003.913735208, + "gpu_noise_us_cpp": 3.6399999999999997, + "gpu_noise_us_python": 4.997999999999999, + "gpu_bwutil_cpp": 0.1714490110687067, + "gpu_bwutil_python": 0.17044543087451808, + "gpu_gap_stddev_us": 2.341565338061424 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2323.872739518075, + "gpu_time_us_python": 2340.6345368865, + "gpu_noise_us_cpp": 5.7775, + "gpu_noise_us_python": 4.3875, + "gpu_bwutil_cpp": 0.16804383514321697, + "gpu_bwutil_python": 0.16684061866196395, + "gpu_gap_stddev_us": 3.173944441175529 + } + }, + "boxblur_k5_1080p_fakeplanar_advanced[InOutDataType=uchar4][shape=64x1080x1920][boxSize=64x64][boxPattern=grid][layout=NCHW_FAKE][inputKind=Tensor][numBoxes=4][kernelSize=5]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3867.7173086313, + "gpu_time_us_python": 3890.0909753945803, + "gpu_noise_us_cpp": 4.107999999999999, + "gpu_noise_us_python": 4.36, + "gpu_bwutil_cpp": 0.1765046201961677, + "gpu_bwutil_python": 0.17548949372257966, + "gpu_gap_stddev_us": 2.202659166217684 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 3024.0372456203995, + "gpu_time_us_python": 3037.205592695425, + "gpu_noise_us_cpp": 3.8825, + "gpu_noise_us_python": 8.4875, + "gpu_bwutil_cpp": 0.17218241755742986, + "gpu_bwutil_python": 0.1714346062253725, + "gpu_gap_stddev_us": 6.376361046680335 + } + } + } + }, + "boxblur_k17_1080p_planar_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3", + "uchar4" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "boxSize": [ + "64x64" + ], + "boxPattern": [ + "grid" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "numBoxes": [ + 4 + ], + "kernelSize": [ + 17 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "BoxBlur RGB/RGBA k17 (advanced) (planar NCHW advanced)." + }, + "baselines": { + "boxblur_k17_1080p_planar_advanced[InOutDataType=uchar3][shape=64x1080x1920][boxSize=64x64][boxPattern=grid][layout=NCHW][inputKind=Tensor][numBoxes=4][kernelSize=17]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 839.47980713246, + "gpu_time_us_python": 850.3380890860001, + "gpu_noise_us_cpp": 4.109999999999999, + "gpu_noise_us_python": 4.5040000000000004, + "gpu_bwutil_cpp": 0.6099067315192548, + "gpu_bwutil_python": 0.6021187011384954, + "gpu_gap_stddev_us": 0.7676852175993369 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 772.8514899618251, + "gpu_time_us_python": 784.3840795272, + "gpu_noise_us_cpp": 3.2325, + "gpu_noise_us_python": 5.74, + "gpu_bwutil_cpp": 0.5052950792962674, + "gpu_bwutil_python": 0.49787029173976793, + "gpu_gap_stddev_us": 0.9740133683365082 + } + }, + "boxblur_k17_1080p_planar_advanced[InOutDataType=uchar4][shape=64x1080x1920][boxSize=64x64][boxPattern=grid][layout=NCHW][inputKind=Tensor][numBoxes=4][kernelSize=17]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1057.23808103466, + "gpu_time_us_python": 1067.6998669313002, + "gpu_noise_us_cpp": 4.214, + "gpu_noise_us_python": 4.417999999999999, + "gpu_bwutil_cpp": 0.6457113320641914, + "gpu_bwutil_python": 0.6393844898200044, + "gpu_gap_stddev_us": 0.8186586881213305 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 977.469457565325, + "gpu_time_us_python": 988.965213892025, + "gpu_noise_us_cpp": 3.04, + "gpu_noise_us_python": 5.305000000000001, + "gpu_bwutil_cpp": 0.5326873362715538, + "gpu_bwutil_python": 0.5265048293073744, + "gpu_gap_stddev_us": 2.679330460169359 + } + } + } + }, + "boxblur_k17_1080p_fakeplanar_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3", + "uchar4" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "boxSize": [ + "64x64" + ], + "boxPattern": [ + "grid" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "numBoxes": [ + 4 + ], + "kernelSize": [ + 17 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "BoxBlur RGB/RGBA k17 (advanced) (fake-planar NCHW advanced)." + }, + "baselines": { + "boxblur_k17_1080p_fakeplanar_advanced[InOutDataType=uchar3][shape=64x1080x1920][boxSize=64x64][boxPattern=grid][layout=NCHW_FAKE][inputKind=Tensor][numBoxes=4][kernelSize=17]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3060.7734121926997, + "gpu_time_us_python": 3076.76539653682, + "gpu_noise_us_cpp": 4.569999999999999, + "gpu_noise_us_python": 7.563999999999998, + "gpu_bwutil_cpp": 0.16727909482023023, + "gpu_bwutil_python": 0.1664096636592861, + "gpu_gap_stddev_us": 1.133667771995771 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2398.4483838081, + "gpu_time_us_python": 2413.123918840475, + "gpu_noise_us_cpp": 3.3425000000000002, + "gpu_noise_us_python": 8.0275, + "gpu_bwutil_cpp": 0.16281932769290772, + "gpu_bwutil_python": 0.16182814116622252, + "gpu_gap_stddev_us": 6.090805440200883 + } + }, + "boxblur_k17_1080p_fakeplanar_advanced[InOutDataType=uchar4][shape=64x1080x1920][boxSize=64x64][boxPattern=grid][layout=NCHW_FAKE][inputKind=Tensor][numBoxes=4][kernelSize=17]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3940.1759617030207, + "gpu_time_us_python": 3961.85279339546, + "gpu_noise_us_cpp": 4.01, + "gpu_noise_us_python": 4.122, + "gpu_bwutil_cpp": 0.17325878847518356, + "gpu_bwutil_python": 0.1723108406147934, + "gpu_gap_stddev_us": 1.6689952309151423 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 3095.766238466575, + "gpu_time_us_python": 3112.576497925625, + "gpu_noise_us_cpp": 4.015, + "gpu_noise_us_python": 5.38, + "gpu_bwutil_cpp": 0.16819293975587762, + "gpu_bwutil_python": 0.16728417181033967, + "gpu_gap_stddev_us": 1.4970175495137592 + } + } + } + }, + "boxblur_manyboxes_1080p_planar_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3", + "uchar4" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "boxSize": [ + "64x64" + ], + "boxPattern": [ + "grid" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "numBoxes": [ + 64 + ], + "kernelSize": [ + 5 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "BoxBlur RGB/RGBA manyboxes (advanced) (planar NCHW advanced)." + }, + "baselines": { + "boxblur_manyboxes_1080p_planar_advanced[InOutDataType=uchar3][shape=64x1080x1920][boxSize=64x64][boxPattern=grid][layout=NCHW][inputKind=Tensor][numBoxes=64][kernelSize=5]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1299.03753163098, + "gpu_time_us_python": 1309.49882028562, + "gpu_noise_us_cpp": 4.718, + "gpu_noise_us_python": 3.836, + "gpu_bwutil_cpp": 0.39420982703933616, + "gpu_bwutil_python": 0.3910582742584869, + "gpu_gap_stddev_us": 1.0329684981710625 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1242.309000472775, + "gpu_time_us_python": 1255.0448197757, + "gpu_noise_us_cpp": 3.4225, + "gpu_noise_us_python": 6.494999999999999, + "gpu_bwutil_cpp": 0.3143855277898393, + "gpu_bwutil_python": 0.3111933612031435, + "gpu_gap_stddev_us": 1.1831397067978477 + } + }, + "boxblur_manyboxes_1080p_planar_advanced[InOutDataType=uchar4][shape=64x1080x1920][boxSize=64x64][boxPattern=grid][layout=NCHW][inputKind=Tensor][numBoxes=64][kernelSize=5]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1616.70911504898, + "gpu_time_us_python": 1626.7531730845199, + "gpu_noise_us_cpp": 3.446, + "gpu_noise_us_python": 4.364, + "gpu_bwutil_cpp": 0.4223103773663793, + "gpu_bwutil_python": 0.4197013775053978, + "gpu_gap_stddev_us": 1.1981235838171311 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1540.8420649700752, + "gpu_time_us_python": 1553.607170892575, + "gpu_noise_us_cpp": 3.6825, + "gpu_noise_us_python": 5.4575000000000005, + "gpu_bwutil_cpp": 0.3379548229892706, + "gpu_bwutil_python": 0.33518115991451936, + "gpu_gap_stddev_us": 2.7747186869279425 + } + } + } + }, + "boxblur_manyboxes_1080p_fakeplanar_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3", + "uchar4" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "boxSize": [ + "64x64" + ], + "boxPattern": [ + "grid" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "numBoxes": [ + 64 + ], + "kernelSize": [ + 5 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "BoxBlur RGB/RGBA manyboxes (advanced) (fake-planar NCHW advanced)." + }, + "baselines": { + "boxblur_manyboxes_1080p_fakeplanar_advanced[InOutDataType=uchar3][shape=64x1080x1920][boxSize=64x64][boxPattern=grid][layout=NCHW_FAKE][inputKind=Tensor][numBoxes=64][kernelSize=5]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3269.56191287814, + "gpu_time_us_python": 3295.71947096534, + "gpu_noise_us_cpp": 3.29, + "gpu_noise_us_python": 23.458000000000002, + "gpu_bwutil_cpp": 0.15661374210095855, + "gpu_bwutil_python": 0.15537027975241519, + "gpu_gap_stddev_us": 6.566291383874529 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2583.1286718713, + "gpu_time_us_python": 2599.55672317425, + "gpu_noise_us_cpp": 5.327500000000001, + "gpu_noise_us_python": 4.945, + "gpu_bwutil_cpp": 0.15119358718026965, + "gpu_bwutil_python": 0.15023775594829158, + "gpu_gap_stddev_us": 4.003479605488673 + } + }, + "boxblur_manyboxes_1080p_fakeplanar_advanced[InOutDataType=uchar4][shape=64x1080x1920][boxSize=64x64][boxPattern=grid][layout=NCHW_FAKE][inputKind=Tensor][numBoxes=64][kernelSize=5]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4133.40458166398, + "gpu_time_us_python": 4155.52652460626, + "gpu_noise_us_cpp": 3.9239999999999995, + "gpu_noise_us_python": 4.628, + "gpu_bwutil_cpp": 0.1651723037133088, + "gpu_bwutil_python": 0.1642931900107788, + "gpu_gap_stddev_us": 1.4002323545373578 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 3271.306801925975, + "gpu_time_us_python": 3288.9408452699254, + "gpu_noise_us_cpp": 5.5175, + "gpu_noise_us_python": 7.742500000000001, + "gpu_bwutil_cpp": 0.15917981201215425, + "gpu_bwutil_python": 0.15832567953445226, + "gpu_gap_stddev_us": 3.263940317664089 + } + } + } + } + } +} diff --git a/bench/config/operators/brightnesscontrast.json b/bench/config/operators/brightnesscontrast.json new file mode 100644 index 000000000..1b0014d66 --- /dev/null +++ b/bench/config/operators/brightnesscontrast.json @@ -0,0 +1,1073 @@ +{ + "benchmark": "brightnesscontrast", + "configs": { + "brightnesscontrast_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "cropMode": [ + "full" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "BrightnessContrast RGB8 (basic)." + }, + "baselines": { + "brightnesscontrast_uchar3_basic[InOutDataType=uchar3][shape=128x1080x1920][cropMode=full][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1170.24439628988, + "gpu_time_us_python": 1180.68555379808, + "gpu_noise_us_cpp": 1.52, + "gpu_noise_us_python": 2.3040000000000003, + "gpu_bwutil_cpp": 0.8750320469855986, + "gpu_bwutil_python": 0.8672939476497774, + "gpu_gap_stddev_us": 0.3508156303495335 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 999.44118659455, + "gpu_time_us_python": 1010.68252867015, + "gpu_noise_us_cpp": 1.8525, + "gpu_noise_us_python": 4.1499999999999995, + "gpu_bwutil_cpp": 0.7814674500048331, + "gpu_bwutil_python": 0.7727741740118493, + "gpu_gap_stddev_us": 1.3085359713841511 + } + }, + "brightnesscontrast_uchar3_basic[InOutDataType=uchar3][shape=128x1080x1920][cropMode=full][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1243.9466521978, + "gpu_time_us_python": 1258.17800634464, + "gpu_noise_us_cpp": 3.176, + "gpu_noise_us_python": 3.564, + "gpu_bwutil_cpp": 0.8231880854386182, + "gpu_bwutil_python": 0.8138765951490982, + "gpu_gap_stddev_us": 1.3323157417350844 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1052.8233710271, + "gpu_time_us_python": 1068.6066875891001, + "gpu_noise_us_cpp": 4.6525, + "gpu_noise_us_python": 6.334999999999999, + "gpu_bwutil_cpp": 0.7418358183330714, + "gpu_bwutil_python": 0.730879792555149, + "gpu_gap_stddev_us": 0.5625268284691997 + } + } + } + }, + "brightnesscontrast_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "cropMode": [ + "full" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "BrightnessContrast RGBf32 (basic)." + }, + "baselines": { + "brightnesscontrast_float3_basic[InOutDataType=float3][shape=32x1080x1920][cropMode=full][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1149.2681214568202, + "gpu_time_us_python": 1160.4555975507199, + "gpu_noise_us_cpp": 0.974, + "gpu_noise_us_python": 2.772, + "gpu_bwutil_cpp": 0.891002122158668, + "gpu_bwutil_python": 0.8824124119330069, + "gpu_gap_stddev_us": 0.39326774984833635 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 875.8277761247999, + "gpu_time_us_python": 886.236035100475, + "gpu_noise_us_cpp": 1.7174999999999998, + "gpu_noise_us_python": 3.0900000000000003, + "gpu_bwutil_cpp": 0.891747880007752, + "gpu_bwutil_python": 0.8812759796158061, + "gpu_gap_stddev_us": 1.349523305627223 + } + } + } + }, + "brightnesscontrast_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "cropMode": [ + "full" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "BrightnessContrast RGBA8 (advanced)." + }, + "baselines": { + "brightnesscontrast_uchar4_advanced[InOutDataType=uchar4][shape=64x1080x1920][cropMode=full][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 766.59899914564, + "gpu_time_us_python": 777.54538292556, + "gpu_noise_us_cpp": 1.4580000000000002, + "gpu_noise_us_python": 2.812, + "gpu_bwutil_cpp": 0.8905143557717038, + "gpu_bwutil_python": 0.8779793619371707, + "gpu_gap_stddev_us": 1.3068519645780257 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 609.0429942149251, + "gpu_time_us_python": 619.113524055025, + "gpu_noise_us_cpp": 2.28, + "gpu_noise_us_python": 3.7424999999999997, + "gpu_bwutil_cpp": 0.8549367441595007, + "gpu_bwutil_python": 0.841027139798976, + "gpu_gap_stddev_us": 0.9093882873326613 + } + }, + "brightnesscontrast_uchar4_advanced[InOutDataType=uchar4][shape=64x1080x1920][cropMode=full][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1218.61667499924, + "gpu_time_us_python": 1233.74518850544, + "gpu_noise_us_cpp": 2.0100000000000002, + "gpu_noise_us_python": 3.7920000000000003, + "gpu_bwutil_cpp": 0.5602002641704991, + "gpu_bwutil_python": 0.5533312029878985, + "gpu_gap_stddev_us": 1.146224986123973 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1272.0921090032, + "gpu_time_us_python": 1288.465842411925, + "gpu_noise_us_cpp": 2.9375, + "gpu_noise_us_python": 4.21, + "gpu_bwutil_cpp": 0.40934038204752315, + "gpu_bwutil_python": 0.40414201312130205, + "gpu_gap_stddev_us": 1.5299783712742905 + } + } + } + }, + "brightnesscontrast_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "cropMode": [ + "full" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "BrightnessContrast RGBAf32 (advanced)." + }, + "baselines": { + "brightnesscontrast_float4_advanced[InOutDataType=float4][shape=16x1080x1920][cropMode=full][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 765.6745383134, + "gpu_time_us_python": 776.2563126795001, + "gpu_noise_us_cpp": 1.1680000000000001, + "gpu_noise_us_python": 2.018, + "gpu_bwutil_cpp": 0.8915888007328082, + "gpu_bwutil_python": 0.8794353313896822, + "gpu_gap_stddev_us": 0.600275831829565 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 587.5982991421, + "gpu_time_us_python": 598.6709899045751, + "gpu_noise_us_cpp": 1.6724999999999999, + "gpu_noise_us_python": 2.7175000000000002, + "gpu_bwutil_cpp": 0.8861135092351178, + "gpu_bwutil_python": 0.8697261936739703, + "gpu_gap_stddev_us": 1.2236055764006093 + } + }, + "brightnesscontrast_float4_advanced[InOutDataType=float4][shape=16x1080x1920][cropMode=full][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 773.16989908632, + "gpu_time_us_python": 785.18842695934, + "gpu_noise_us_cpp": 1.796, + "gpu_noise_us_python": 2.8939999999999997, + "gpu_bwutil_cpp": 0.8829457590840615, + "gpu_bwutil_python": 0.869431168249404, + "gpu_gap_stddev_us": 0.9817582230050159 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 594.4038695016, + "gpu_time_us_python": 609.28014162715, + "gpu_noise_us_cpp": 2.8449999999999998, + "gpu_noise_us_python": 4.8625, + "gpu_bwutil_cpp": 0.8759708481861009, + "gpu_bwutil_python": 0.8545992300373468, + "gpu_gap_stddev_us": 4.357202830157062 + } + } + } + }, + "brightnesscontrast_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "cropMode": [ + "full" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "BrightnessContrast single-channel (advanced)." + }, + "baselines": { + "brightnesscontrast_scalar_advanced[InOutDataType=float32][shape=128x1080x1920][cropMode=full][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1520.0045097235, + "gpu_time_us_python": 1530.9211813821598, + "gpu_noise_us_cpp": 1.356, + "gpu_noise_us_python": 3.942, + "gpu_bwutil_cpp": 0.8982438250362748, + "gpu_bwutil_python": 0.8918390586393837, + "gpu_gap_stddev_us": 1.219977033646733 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1166.0961848358502, + "gpu_time_us_python": 1177.062948544775, + "gpu_noise_us_cpp": 1.1524999999999999, + "gpu_noise_us_python": 2.8525, + "gpu_bwutil_cpp": 0.8930285685267354, + "gpu_bwutil_python": 0.8847087812417211, + "gpu_gap_stddev_us": 1.1232768187746232 + } + }, + "brightnesscontrast_scalar_advanced[InOutDataType=float32][shape=128x1080x1920][cropMode=full][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2228.06239281452, + "gpu_time_us_python": 2242.5105874027004, + "gpu_noise_us_cpp": 3.7920000000000003, + "gpu_noise_us_python": 4.3839999999999995, + "gpu_bwutil_cpp": 0.6127932382309333, + "gpu_bwutil_python": 0.6088425871288434, + "gpu_gap_stddev_us": 3.852370782306089 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2424.28613143055, + "gpu_time_us_python": 2446.005141000075, + "gpu_noise_us_cpp": 3.7975000000000003, + "gpu_noise_us_python": 7.0, + "gpu_bwutil_cpp": 0.42960037668280904, + "gpu_bwutil_python": 0.42578004192533675, + "gpu_gap_stddev_us": 2.4821512451343595 + } + }, + "brightnesscontrast_scalar_advanced[InOutDataType=uint8][shape=128x1080x1920][cropMode=full][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 391.719168334, + "gpu_time_us_python": 401.54797288756, + "gpu_noise_us_cpp": 1.6560000000000001, + "gpu_noise_us_python": 1.9060000000000001, + "gpu_bwutil_cpp": 0.8713759625284851, + "gpu_bwutil_python": 0.8500476739333811, + "gpu_gap_stddev_us": 0.4314718983495421 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 327.32910202834995, + "gpu_time_us_python": 338.0773557166, + "gpu_noise_us_cpp": 2.01, + "gpu_noise_us_python": 2.2625, + "gpu_bwutil_cpp": 0.7953560781200772, + "gpu_bwutil_python": 0.7700725547766801, + "gpu_gap_stddev_us": 0.9587304890121887 + } + }, + "brightnesscontrast_scalar_advanced[InOutDataType=uint8][shape=128x1080x1920][cropMode=full][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 530.50437099188, + "gpu_time_us_python": 546.5082355595199, + "gpu_noise_us_cpp": 3.45, + "gpu_noise_us_python": 3.654, + "gpu_bwutil_cpp": 0.6434295407826831, + "gpu_bwutil_python": 0.6245747565092883, + "gpu_gap_stddev_us": 2.0672145194357814 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 501.22742952860006, + "gpu_time_us_python": 519.797244391575, + "gpu_noise_us_cpp": 2.4475, + "gpu_noise_us_python": 4.5024999999999995, + "gpu_bwutil_cpp": 0.5194319821362743, + "gpu_bwutil_python": 0.500901934012767, + "gpu_gap_stddev_us": 2.9381531249417114 + } + } + } + }, + "brightnesscontrast_rgb_u8_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "cropMode": [ + "full" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "BrightnessContrast RGB8 (advanced)." + }, + "baselines": { + "brightnesscontrast_rgb_u8_1080p_advanced[InOutDataType=uchar3][shape=128x1080x1920][cropMode=full][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1170.2195131889798, + "gpu_time_us_python": 1180.6459413384, + "gpu_noise_us_cpp": 2.3660000000000005, + "gpu_noise_us_python": 1.89, + "gpu_bwutil_cpp": 0.8750506766348038, + "gpu_bwutil_python": 0.8673235416025082, + "gpu_gap_stddev_us": 0.786162057092684 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 999.572536609875, + "gpu_time_us_python": 1011.1219446703999, + "gpu_noise_us_cpp": 1.94, + "gpu_noise_us_python": 4.0675, + "gpu_bwutil_cpp": 0.7813637318292539, + "gpu_bwutil_python": 0.7724376318643388, + "gpu_gap_stddev_us": 1.3148146870885111 + } + }, + "brightnesscontrast_rgb_u8_1080p_advanced[InOutDataType=uchar3][shape=128x1080x1920][cropMode=full][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1243.5168502512602, + "gpu_time_us_python": 1258.6493516087799, + "gpu_noise_us_cpp": 2.886, + "gpu_noise_us_python": 4.522, + "gpu_bwutil_cpp": 0.8234720345435858, + "gpu_bwutil_python": 0.8135729975899126, + "gpu_gap_stddev_us": 1.8028031252629821 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1051.927828096625, + "gpu_time_us_python": 1070.1572064421748, + "gpu_noise_us_cpp": 3.4525, + "gpu_noise_us_python": 5.3125, + "gpu_bwutil_cpp": 0.7424683833332119, + "gpu_bwutil_python": 0.7298261646567699, + "gpu_gap_stddev_us": 3.5732685072662327 + } + } + } + }, + "brightnesscontrast_rgb_f32_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "cropMode": [ + "full" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "BrightnessContrast RGBf32 (advanced)." + }, + "baselines": { + "brightnesscontrast_rgb_f32_1080p_advanced[InOutDataType=float3][shape=32x1080x1920][cropMode=full][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1172.26274381168, + "gpu_time_us_python": 1185.7560665121, + "gpu_noise_us_cpp": 2.0740000000000003, + "gpu_noise_us_python": 2.706, + "gpu_bwutil_cpp": 0.8735247316572512, + "gpu_bwutil_python": 0.8635850823461968, + "gpu_gap_stddev_us": 1.6004753668882226 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 900.3263015648, + "gpu_time_us_python": 915.4432424664999, + "gpu_noise_us_cpp": 2.6825, + "gpu_noise_us_python": 4.992500000000001, + "gpu_bwutil_cpp": 0.8674827483939357, + "gpu_bwutil_python": 0.8531612423753463, + "gpu_gap_stddev_us": 2.1484681256707514 + } + } + } + }, + "brightnesscontrast_planar_nchw_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "cropMode": [ + "full" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "BrightnessContrast RGB8 (basic) (planar NCHW)." + }, + "baselines": { + "brightnesscontrast_planar_nchw_uchar3_basic[InOutDataType=uchar3][shape=128x1080x1920][cropMode=full][layout=NCHW][inputKind=Tensor]": { + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 995.1638864359999, + "gpu_time_us_python": 1008.78103052055, + "gpu_noise_us_cpp": 2.035, + "gpu_noise_us_python": 3.36, + "gpu_bwutil_cpp": 0.7848186355869579, + "gpu_bwutil_python": 0.7742340495866209, + "gpu_gap_stddev_us": 2.2109759739693544 + }, + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1223.929775866, + "gpu_time_us_python": 1233.7726846704, + "gpu_noise_us_cpp": 3.4899999999999998, + "gpu_noise_us_python": 3.8940000000000006, + "gpu_bwutil_cpp": 0.8366504497627829, + "gpu_bwutil_python": 0.8299759304361091, + "gpu_gap_stddev_us": 0.7986797243894307 + } + }, + "brightnesscontrast_planar_nchw_uchar3_basic[InOutDataType=uchar3][shape=128x1080x1920][cropMode=full][layout=NCHW][inputKind=VarShape]": { + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1040.131828680375, + "gpu_time_us_python": 1056.870383525, + "gpu_noise_us_cpp": 3.1149999999999998, + "gpu_noise_us_python": 7.445, + "gpu_bwutil_cpp": 0.7508870319212259, + "gpu_bwutil_python": 0.7390174085771782, + "gpu_gap_stddev_us": 5.389481383359777 + }, + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1261.9497444410003, + "gpu_time_us_python": 1275.71916912952, + "gpu_noise_us_cpp": 4.144, + "gpu_noise_us_python": 3.1220000000000003, + "gpu_bwutil_cpp": 0.8114443711421213, + "gpu_bwutil_python": 0.8026858289739043, + "gpu_gap_stddev_us": 1.8856804659419653 + } + } + } + }, + "brightnesscontrast_planar_nchw_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "cropMode": [ + "full" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "BrightnessContrast RGBf32 (basic) (planar NCHW)." + }, + "baselines": { + "brightnesscontrast_planar_nchw_float3_basic[InOutDataType=float3][shape=32x1080x1920][cropMode=full][layout=NCHW][inputKind=Tensor]": { + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1071.345651247225, + "gpu_time_us_python": 1083.7976469459, + "gpu_noise_us_cpp": 1.4925, + "gpu_noise_us_python": 3.5, + "gpu_bwutil_cpp": 0.7290080919817379, + "gpu_bwutil_python": 0.720639300074371, + "gpu_gap_stddev_us": 3.5512832559221787 + }, + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1266.80590456176, + "gpu_time_us_python": 1277.81197611162, + "gpu_noise_us_cpp": 1.5379999999999998, + "gpu_noise_us_python": 2.094, + "gpu_bwutil_cpp": 0.8083325083531205, + "gpu_bwutil_python": 0.80137085045063, + "gpu_gap_stddev_us": 1.3381893391821231 + } + } + } + }, + "brightnesscontrast_planar_nchw_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "cropMode": [ + "full" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "BrightnessContrast RGBA8 (advanced) (planar NCHW tensor)." + }, + "baselines": { + "brightnesscontrast_planar_nchw_uchar4_advanced[InOutDataType=uchar4][shape=64x1080x1920][cropMode=full][layout=NCHW][inputKind=Tensor]": { + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 655.7859766041249, + "gpu_time_us_python": 667.9168529146, + "gpu_noise_us_cpp": 1.53, + "gpu_noise_us_python": 2.935, + "gpu_bwutil_cpp": 0.7939826397512509, + "gpu_bwutil_python": 0.7795618179392714, + "gpu_gap_stddev_us": 3.993843814569547 + }, + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 815.8584961673199, + "gpu_time_us_python": 824.8634929728199, + "gpu_noise_us_cpp": 2.7139999999999995, + "gpu_noise_us_python": 2.9319999999999995, + "gpu_bwutil_cpp": 0.8367473441699189, + "gpu_bwutil_python": 0.8276125861833068, + "gpu_gap_stddev_us": 0.37533556368861715 + } + } + } + }, + "brightnesscontrast_planar_nchw_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "cropMode": [ + "full" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "BrightnessContrast RGBAf32 (advanced) (planar NCHW)." + }, + "baselines": { + "brightnesscontrast_planar_nchw_float4_advanced[InOutDataType=float4][shape=16x1080x1920][cropMode=full][layout=NCHW][inputKind=Tensor]": { + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 699.691091616725, + "gpu_time_us_python": 713.7362069148751, + "gpu_noise_us_cpp": 1.105, + "gpu_noise_us_python": 3.0825, + "gpu_bwutil_cpp": 0.7441550019984065, + "gpu_bwutil_python": 0.7295254122490092, + "gpu_gap_stddev_us": 4.111538888152721 + }, + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 843.9925113911999, + "gpu_time_us_python": 854.84639096988, + "gpu_noise_us_cpp": 1.5500000000000003, + "gpu_noise_us_python": 4.013999999999999, + "gpu_bwutil_cpp": 0.8088541472585185, + "gpu_bwutil_python": 0.7985844282835827, + "gpu_gap_stddev_us": 0.5527208887052919 + } + }, + "brightnesscontrast_planar_nchw_float4_advanced[InOutDataType=float4][shape=16x1080x1920][cropMode=full][layout=NCHW][inputKind=VarShape]": { + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 713.4625198408748, + "gpu_time_us_python": 729.381816330025, + "gpu_noise_us_cpp": 2.7975, + "gpu_noise_us_python": 3.5725, + "gpu_bwutil_cpp": 0.7297939255519675, + "gpu_bwutil_python": 0.7138826344812403, + "gpu_gap_stddev_us": 5.1972627135686364 + }, + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 870.44978671598, + "gpu_time_us_python": 884.64306213356, + "gpu_noise_us_cpp": 1.308, + "gpu_noise_us_python": 3.008, + "gpu_bwutil_cpp": 0.7842691088944369, + "gpu_bwutil_python": 0.771687749933282, + "gpu_gap_stddev_us": 1.4294936048144993 + } + } + } + }, + "brightnesscontrast_planar_rgb_u8_nchw_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "cropMode": [ + "full" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "BrightnessContrast RGB8 (advanced) (planar NCHW)." + }, + "baselines": { + "brightnesscontrast_planar_rgb_u8_nchw_1080p_advanced[InOutDataType=uchar3][shape=128x1080x1920][cropMode=full][layout=NCHW][inputKind=Tensor]": { + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 995.838452263625, + "gpu_time_us_python": 1007.3837847825749, + "gpu_noise_us_cpp": 1.5825, + "gpu_noise_us_python": 3.21, + "gpu_bwutil_cpp": 0.7842870383849181, + "gpu_bwutil_python": 0.7752971596669229, + "gpu_gap_stddev_us": 0.9662683225788393 + }, + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1223.7619396534799, + "gpu_time_us_python": 1233.0412544052601, + "gpu_noise_us_cpp": 2.12, + "gpu_noise_us_python": 2.55, + "gpu_bwutil_cpp": 0.8367651781111205, + "gpu_bwutil_python": 0.8304680389201369, + "gpu_gap_stddev_us": 0.3630035351417832 + } + }, + "brightnesscontrast_planar_rgb_u8_nchw_1080p_advanced[InOutDataType=uchar3][shape=128x1080x1920][cropMode=full][layout=NCHW][inputKind=VarShape]": { + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1039.3034035234, + "gpu_time_us_python": 1057.0749205618501, + "gpu_noise_us_cpp": 2.54, + "gpu_noise_us_python": 4.4875, + "gpu_bwutil_cpp": 0.7514859316509953, + "gpu_bwutil_python": 0.7388522392104302, + "gpu_gap_stddev_us": 2.255444202934881 + }, + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1261.45580437908, + "gpu_time_us_python": 1274.85195138122, + "gpu_noise_us_cpp": 2.908, + "gpu_noise_us_python": 2.898, + "gpu_bwutil_cpp": 0.8117616871463762, + "gpu_bwutil_python": 0.8032317498745633, + "gpu_gap_stddev_us": 0.9357930870963636 + } + } + } + }, + "brightnesscontrast_planar_rgb_f32_nchw_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "cropMode": [ + "full" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "BrightnessContrast RGBf32 (advanced) (planar NCHW)." + }, + "baselines": { + "brightnesscontrast_planar_rgb_f32_nchw_1080p_advanced[InOutDataType=float3][shape=32x1080x1920][cropMode=full][layout=NCHW][inputKind=VarShape]": { + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1112.67235121605, + "gpu_time_us_python": 1126.448336758625, + "gpu_noise_us_cpp": 2.5675, + "gpu_noise_us_python": 5.1125, + "gpu_bwutil_cpp": 0.7019331151368973, + "gpu_bwutil_python": 0.6933540537927627, + "gpu_gap_stddev_us": 1.8548557943033805 + }, + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1319.81099768684, + "gpu_time_us_python": 1333.9321346992801, + "gpu_noise_us_cpp": 1.286, + "gpu_noise_us_python": 3.6119999999999997, + "gpu_bwutil_cpp": 0.7758690913460343, + "gpu_bwutil_python": 0.7676562438223008, + "gpu_gap_stddev_us": 1.1670537681997344 + } + }, + "brightnesscontrast_planar_rgb_f32_nchw_1080p_advanced[InOutDataType=float3][shape=32x1080x1920][cropMode=full][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1266.64764953376, + "gpu_time_us_python": 1277.3678941385601, + "gpu_noise_us_cpp": 0.7620000000000001, + "gpu_noise_us_python": 1.8219999999999998, + "gpu_bwutil_cpp": 0.8084334756022807, + "gpu_bwutil_python": 0.8016490511153661, + "gpu_gap_stddev_us": 1.0143261830839563 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1071.70854288205, + "gpu_time_us_python": 1083.034281613975, + "gpu_noise_us_cpp": 1.6675, + "gpu_noise_us_python": 3.1525, + "gpu_bwutil_cpp": 0.7287608995860562, + "gpu_bwutil_python": 0.7211402358620447, + "gpu_gap_stddev_us": 0.33571439031059935 + } + } + } + }, + "brightnesscontrast_fakeplanar_nchw_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "cropMode": [ + "full" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "BrightnessContrast RGBA8 (advanced) (fake planar NCHW_FAKE)." + }, + "baselines": { + "brightnesscontrast_fakeplanar_nchw_uchar4_advanced[InOutDataType=uchar4][shape=64x1080x1920][cropMode=full][layout=NCHW_FAKE][inputKind=Tensor]": { + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 3014.4282792979, + "gpu_time_us_python": 3025.2408499493, + "gpu_noise_us_cpp": 1.54, + "gpu_noise_us_python": 7.1675, + "gpu_bwutil_cpp": 0.5181949772194062, + "gpu_bwutil_python": 0.5163415659955468, + "gpu_gap_stddev_us": 5.658895456560527 + }, + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3824.72381519544, + "gpu_time_us_python": 3842.58165503988, + "gpu_noise_us_cpp": 3.402, + "gpu_noise_us_python": 3.656, + "gpu_bwutil_cpp": 0.5354637651579865, + "gpu_bwutil_python": 0.5329753271937927, + "gpu_gap_stddev_us": 2.6054041398220464 + } + } + } + }, + "brightnesscontrast_fakeplanar_nchw_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "cropMode": [ + "full" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "BrightnessContrast RGBAf32 (advanced) (fake planar NCHW_FAKE)." + }, + "baselines": { + "brightnesscontrast_fakeplanar_nchw_float4_advanced[InOutDataType=float4][shape=16x1080x1920][cropMode=full][layout=NCHW_FAKE][inputKind=Tensor]": { + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2342.4014226298, + "gpu_time_us_python": 2358.39730364535, + "gpu_noise_us_cpp": 5.095, + "gpu_noise_us_python": 6.7175, + "gpu_bwutil_cpp": 0.6668526917794617, + "gpu_bwutil_python": 0.662328837710292, + "gpu_gap_stddev_us": 2.2955335616093793 + }, + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2879.7940150074, + "gpu_time_us_python": 2895.7496100458, + "gpu_noise_us_cpp": 4.432, + "gpu_noise_us_python": 4.022, + "gpu_bwutil_cpp": 0.7111620573324069, + "gpu_bwutil_python": 0.7072436050583033, + "gpu_gap_stddev_us": 1.5957236416597431 + } + } + } + }, + "brightnesscontrast_fakeplanar_rgb_u8_nchw_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "cropMode": [ + "full" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "BrightnessContrast RGB8 (advanced) (fake planar NCHW_FAKE)." + }, + "baselines": { + "brightnesscontrast_fakeplanar_rgb_u8_nchw_1080p_advanced[InOutDataType=uchar3][shape=128x1080x1920][cropMode=full][layout=NCHW_FAKE][inputKind=Tensor]": { + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 4674.893195982325, + "gpu_time_us_python": 4690.5003101737, + "gpu_noise_us_cpp": 2.2225, + "gpu_noise_us_python": 6.237500000000001, + "gpu_bwutil_cpp": 0.5012046382834012, + "gpu_bwutil_python": 0.4995369178036446, + "gpu_gap_stddev_us": 1.8675523366874833 + }, + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5885.60907674384, + "gpu_time_us_python": 5903.75055490534, + "gpu_noise_us_cpp": 2.19, + "gpu_noise_us_python": 5.8, + "gpu_bwutil_cpp": 0.5219513882651874, + "gpu_bwutil_python": 0.520347583402982, + "gpu_gap_stddev_us": 3.678537179679094 + } + } + } + }, + "brightnesscontrast_fakeplanar_rgb_f32_nchw_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "cropMode": [ + "full" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "BrightnessContrast RGBf32 (advanced) (fake planar NCHW_FAKE)." + }, + "baselines": { + "brightnesscontrast_fakeplanar_rgb_f32_nchw_1080p_advanced[InOutDataType=float3][shape=32x1080x1920][cropMode=full][layout=NCHW_FAKE][inputKind=Tensor]": { + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 3074.379902787275, + "gpu_time_us_python": 3091.980637591525, + "gpu_noise_us_cpp": 2.235, + "gpu_noise_us_python": 3.8725, + "gpu_bwutil_cpp": 0.7621230939381761, + "gpu_bwutil_python": 0.757784203737378, + "gpu_gap_stddev_us": 2.724110796699324 + }, + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3828.5840150081594, + "gpu_time_us_python": 3847.37534749752, + "gpu_noise_us_cpp": 3.2020000000000004, + "gpu_noise_us_python": 3.464, + "gpu_bwutil_cpp": 0.8023855336217738, + "gpu_bwutil_python": 0.7984666010512746, + "gpu_gap_stddev_us": 1.7062902426428819 + } + } + } + } + } +} diff --git a/bench/config/operators/centercrop.json b/bench/config/operators/centercrop.json new file mode 100644 index 000000000..e89cfd524 --- /dev/null +++ b/bench/config/operators/centercrop.json @@ -0,0 +1,1226 @@ +{ + "benchmark": "centercrop", + "configs": { + "centercrop_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "256x1080x1920" + ], + "cropType": [ + "QUARTER" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CenterCrop RGB8 (basic)." + }, + "baselines": { + "centercrop_uchar3_basic[InOutDataType=uchar3][shape=256x1080x1920][cropType=QUARTER][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 880.68175450454, + "gpu_time_us_python": 890.13269189272, + "gpu_noise_us_cpp": 1.9100000000000001, + "gpu_noise_us_python": 2.908, + "gpu_bwutil_cpp": 0.581368529139297, + "gpu_bwutil_python": 0.5751961841172022, + "gpu_gap_stddev_us": 1.0617594720820824 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 752.6211701368001, + "gpu_time_us_python": 763.47526543015, + "gpu_noise_us_cpp": 2.4625000000000004, + "gpu_noise_us_python": 2.75, + "gpu_bwutil_cpp": 0.5188679177808205, + "gpu_bwutil_python": 0.5115040064551386, + "gpu_gap_stddev_us": 3.125992490463569 + } + } + } + }, + "centercrop_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "cropType": [ + "QUARTER" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CenterCrop RGBf32 (basic)." + }, + "baselines": { + "centercrop_float3_basic[InOutDataType=float3][shape=64x1080x1920][cropType=QUARTER][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 591.1315976462399, + "gpu_time_us_python": 599.90914311648, + "gpu_noise_us_cpp": 1.222, + "gpu_noise_us_python": 2.132, + "gpu_bwutil_cpp": 0.8661354462999169, + "gpu_bwutil_python": 0.8534637572775379, + "gpu_gap_stddev_us": 0.832674022732822 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 450.4248131186, + "gpu_time_us_python": 459.8869079781, + "gpu_noise_us_cpp": 1.38, + "gpu_noise_us_python": 3.4875, + "gpu_bwutil_cpp": 0.8669800522887352, + "gpu_bwutil_python": 0.849150536948303, + "gpu_gap_stddev_us": 1.9702857337814752 + } + } + } + }, + "centercrop_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "256x1080x1920" + ], + "cropType": [ + "QUARTER" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CenterCrop RGBA8 (advanced)." + }, + "baselines": { + "centercrop_uchar4_advanced[InOutDataType=uchar4][shape=256x1080x1920][cropType=QUARTER][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 891.38579699488, + "gpu_time_us_python": 901.6214892439, + "gpu_noise_us_cpp": 1.202, + "gpu_noise_us_python": 3.054, + "gpu_bwutil_cpp": 0.7658489317508039, + "gpu_bwutil_python": 0.7571570460916716, + "gpu_gap_stddev_us": 1.975949374999925 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 788.962462559125, + "gpu_time_us_python": 799.8087058486501, + "gpu_noise_us_cpp": 2.115, + "gpu_noise_us_python": 3.095, + "gpu_bwutil_cpp": 0.6599548920678631, + "gpu_bwutil_python": 0.6510066550113344, + "gpu_gap_stddev_us": 2.930114431411938 + } + } + } + }, + "centercrop_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "cropType": [ + "QUARTER" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CenterCrop RGBAf32 (advanced)." + }, + "baselines": { + "centercrop_float4_advanced[InOutDataType=float4][shape=64x1080x1920][cropType=QUARTER][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 767.65762157964, + "gpu_time_us_python": 776.28382224858, + "gpu_noise_us_cpp": 2.592, + "gpu_noise_us_python": 2.9899999999999998, + "gpu_bwutil_cpp": 0.8892854517947724, + "gpu_bwutil_python": 0.8794036721185556, + "gpu_gap_stddev_us": 0.5825360774910519 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 592.004437445725, + "gpu_time_us_python": 602.607503946825, + "gpu_noise_us_cpp": 2.395, + "gpu_noise_us_python": 3.7075, + "gpu_bwutil_cpp": 0.8795178329747599, + "gpu_bwutil_python": 0.8640522545472514, + "gpu_gap_stddev_us": 2.7539555016129524 + } + } + } + }, + "centercrop_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "256x1080x1920" + ], + "cropType": [ + "QUARTER" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CenterCrop single-channel (advanced)." + }, + "baselines": { + "centercrop_scalar_advanced[InOutDataType=float32][shape=256x1080x1920][cropType=QUARTER][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 891.4696117736601, + "gpu_time_us_python": 900.3081639892798, + "gpu_noise_us_cpp": 1.762, + "gpu_noise_us_python": 2.4139999999999997, + "gpu_bwutil_cpp": 0.7657769863089987, + "gpu_bwutil_python": 0.7582592123898407, + "gpu_gap_stddev_us": 0.5542481718119543 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 788.5956589852001, + "gpu_time_us_python": 798.3572418167249, + "gpu_noise_us_cpp": 1.2950000000000002, + "gpu_noise_us_python": 3.335, + "gpu_bwutil_cpp": 0.6602614754955876, + "gpu_bwutil_python": 0.6521933830592674, + "gpu_gap_stddev_us": 1.5715187388589333 + } + }, + "centercrop_scalar_advanced[InOutDataType=uint8][shape=256x1080x1920][cropType=QUARTER][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 231.0101246541, + "gpu_time_us_python": 239.67942455398003, + "gpu_noise_us_cpp": 1.1520000000000001, + "gpu_noise_us_python": 2.718, + "gpu_bwutil_cpp": 0.7387868035385934, + "gpu_bwutil_python": 0.7120647967570429, + "gpu_gap_stddev_us": 0.29444678133165414 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 212.140594111525, + "gpu_time_us_python": 221.04446655135, + "gpu_noise_us_cpp": 1.4025, + "gpu_noise_us_python": 2.5700000000000003, + "gpu_bwutil_cpp": 0.6136049487017157, + "gpu_bwutil_python": 0.5888914497235225, + "gpu_gap_stddev_us": 0.3587024650334175 + } + } + } + }, + "centercrop_scalar_u8_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "1024x1080x1920" + ], + "cropType": [ + "QUARTER" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CenterCrop single-channel (advanced)." + }, + "baselines": { + "centercrop_scalar_u8_1080p_advanced[InOutDataType=uint8][shape=1024x1080x1920][cropType=QUARTER][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 894.0968335525598, + "gpu_time_us_python": 903.56503626064, + "gpu_noise_us_cpp": 2.2039999999999997, + "gpu_noise_us_python": 3.364, + "gpu_bwutil_cpp": 0.7635266978045516, + "gpu_bwutil_python": 0.7555259833609786, + "gpu_gap_stddev_us": 0.803030410984441 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 813.443815150325, + "gpu_time_us_python": 823.9315429845, + "gpu_noise_us_cpp": 1.405, + "gpu_noise_us_python": 3.8024999999999998, + "gpu_bwutil_cpp": 0.6400940254118008, + "gpu_bwutil_python": 0.6319459705508551, + "gpu_gap_stddev_us": 1.5505982102432738 + } + } + } + }, + "centercrop_rgb_u8_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "336x1080x1920" + ], + "cropType": [ + "QUARTER" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CenterCrop RGB8 (advanced)." + }, + "baselines": { + "centercrop_rgb_u8_1080p_advanced[InOutDataType=uchar3][shape=336x1080x1920][cropType=QUARTER][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1152.8132272512998, + "gpu_time_us_python": 1162.42547330104, + "gpu_noise_us_cpp": 2.366, + "gpu_noise_us_python": 2.8779999999999997, + "gpu_bwutil_cpp": 0.5829226976070355, + "gpu_bwutil_python": 0.5781019222936006, + "gpu_gap_stddev_us": 1.3421744374649143 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 983.63361524215, + "gpu_time_us_python": 994.846664585925, + "gpu_noise_us_cpp": 1.8925, + "gpu_noise_us_python": 2.6925, + "gpu_bwutil_cpp": 0.5210741033300962, + "gpu_bwutil_python": 0.5152007332839665, + "gpu_gap_stddev_us": 0.5240712637252601 + } + } + } + }, + "centercrop_rgb_f32_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "cropType": [ + "QUARTER" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CenterCrop RGBf32 (advanced)." + }, + "baselines": { + "centercrop_rgb_f32_1080p_advanced[InOutDataType=float3][shape=64x1080x1920][cropType=QUARTER][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 591.2285575347801, + "gpu_time_us_python": 599.9778134361401, + "gpu_noise_us_cpp": 1.964, + "gpu_noise_us_python": 2.354, + "gpu_bwutil_cpp": 0.8659934474501538, + "gpu_bwutil_python": 0.8533651571299373, + "gpu_gap_stddev_us": 0.5173175403476086 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 450.266871785825, + "gpu_time_us_python": 459.50349080115, + "gpu_noise_us_cpp": 1.1775, + "gpu_noise_us_python": 3.1525, + "gpu_bwutil_cpp": 0.8672838406848569, + "gpu_bwutil_python": 0.8498633241731205, + "gpu_gap_stddev_us": 1.7413015807367946 + } + } + } + }, + "centercrop_planar_nchw_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "256x1080x1920" + ], + "cropType": [ + "QUARTER" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CenterCrop RGB8 (basic). [native planar NCHW]" + }, + "baselines": { + "centercrop_planar_nchw_uchar3_basic[InOutDataType=uchar3][shape=256x1080x1920][cropType=QUARTER][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 673.79939867486, + "gpu_time_us_python": 683.19515119, + "gpu_noise_us_cpp": 1.286, + "gpu_noise_us_python": 3.1420000000000003, + "gpu_bwutil_cpp": 0.7598701604739583, + "gpu_bwutil_python": 0.7494202201595458, + "gpu_gap_stddev_us": 0.39144911665744536 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 614.3005381176499, + "gpu_time_us_python": 625.78576562945, + "gpu_noise_us_cpp": 1.2449999999999999, + "gpu_noise_us_python": 3.1725, + "gpu_bwutil_cpp": 0.6356981947070485, + "gpu_bwutil_python": 0.6240411164278883, + "gpu_gap_stddev_us": 2.5357367537361486 + } + } + } + }, + "centercrop_planar_nchw_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "cropType": [ + "QUARTER" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CenterCrop RGBf32 (basic). [native planar NCHW]" + }, + "baselines": { + "centercrop_planar_nchw_float3_basic[InOutDataType=float3][shape=64x1080x1920][cropType=QUARTER][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 672.1089151183002, + "gpu_time_us_python": 680.89893252568, + "gpu_noise_us_cpp": 1.01, + "gpu_noise_us_python": 1.7120000000000002, + "gpu_bwutil_cpp": 0.7617814008443696, + "gpu_bwutil_python": 0.7519477998827944, + "gpu_gap_stddev_us": 0.5023303794710924 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 595.3290668522251, + "gpu_time_us_python": 604.930994591075, + "gpu_noise_us_cpp": 1.1375, + "gpu_noise_us_python": 3.57, + "gpu_bwutil_cpp": 0.6559565543441186, + "gpu_bwutil_python": 0.6455502646632769, + "gpu_gap_stddev_us": 1.2056147472263452 + } + } + } + }, + "centercrop_planar_nchw_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "256x1080x1920" + ], + "cropType": [ + "QUARTER" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CenterCrop RGBA8 (advanced). [native planar NCHW]" + }, + "baselines": { + "centercrop_planar_nchw_uchar4_advanced[InOutDataType=uchar4][shape=256x1080x1920][cropType=QUARTER][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 894.8428939921199, + "gpu_time_us_python": 905.03226557422, + "gpu_noise_us_cpp": 1.0699999999999998, + "gpu_noise_us_python": 2.066, + "gpu_bwutil_cpp": 0.7628901380908915, + "gpu_bwutil_python": 0.7543016384092334, + "gpu_gap_stddev_us": 1.1078627201898397 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 815.1394690976, + "gpu_time_us_python": 826.631908256825, + "gpu_noise_us_cpp": 1.6925, + "gpu_noise_us_python": 4.072500000000001, + "gpu_bwutil_cpp": 0.6387615434140035, + "gpu_bwutil_python": 0.6298794919412587, + "gpu_gap_stddev_us": 1.7719883816107718 + } + } + } + }, + "centercrop_planar_nchw_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "cropType": [ + "QUARTER" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CenterCrop RGBAf32 (advanced). [native planar NCHW]" + }, + "baselines": { + "centercrop_planar_nchw_float4_advanced[InOutDataType=float4][shape=64x1080x1920][cropType=QUARTER][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 892.28808968938, + "gpu_time_us_python": 901.2570275247799, + "gpu_noise_us_cpp": 1.1300000000000001, + "gpu_noise_us_python": 2.0420000000000003, + "gpu_bwutil_cpp": 0.7650743755186185, + "gpu_bwutil_python": 0.757460853967978, + "gpu_gap_stddev_us": 0.5239076385418604 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 789.95229622775, + "gpu_time_us_python": 799.152301278175, + "gpu_noise_us_cpp": 1.2774999999999999, + "gpu_noise_us_python": 3.4299999999999997, + "gpu_bwutil_cpp": 0.6591276146988209, + "gpu_bwutil_python": 0.6515400985978336, + "gpu_gap_stddev_us": 0.7350416379795277 + } + } + } + }, + "centercrop_planar_nchw_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "256x1080x1920" + ], + "cropType": [ + "QUARTER" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CenterCrop single-channel (advanced). [native planar NCHW]" + }, + "baselines": { + "centercrop_planar_nchw_scalar_advanced[InOutDataType=float32][shape=256x1080x1920][cropType=QUARTER][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 891.9688621449199, + "gpu_time_us_python": 901.1258955481401, + "gpu_noise_us_cpp": 1.11, + "gpu_noise_us_python": 1.704, + "gpu_bwutil_cpp": 0.7653481471935015, + "gpu_bwutil_python": 0.7575711778246665, + "gpu_gap_stddev_us": 0.562921377179168 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 789.6210379816, + "gpu_time_us_python": 801.197725102725, + "gpu_noise_us_cpp": 1.4275, + "gpu_noise_us_python": 4.27, + "gpu_bwutil_cpp": 0.6594050196150956, + "gpu_bwutil_python": 0.6498867216275106, + "gpu_gap_stddev_us": 4.616778311058188 + } + }, + "centercrop_planar_nchw_scalar_advanced[InOutDataType=uint8][shape=256x1080x1920][cropType=QUARTER][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 231.77103663309998, + "gpu_time_us_python": 240.95020324306, + "gpu_noise_us_cpp": 1.6779999999999997, + "gpu_noise_us_python": 2.6540000000000004, + "gpu_bwutil_cpp": 0.736359390680164, + "gpu_bwutil_python": 0.708309617286097, + "gpu_gap_stddev_us": 0.4589815909487002 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 213.1934399677, + "gpu_time_us_python": 224.384413032575, + "gpu_noise_us_cpp": 1.165, + "gpu_noise_us_python": 5.6925, + "gpu_bwutil_cpp": 0.610577313449644, + "gpu_bwutil_python": 0.5801383608116396, + "gpu_gap_stddev_us": 1.084333853479603 + } + } + } + }, + "centercrop_planar_nchw_scalar_u8_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "1024x1080x1920" + ], + "cropType": [ + "QUARTER" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CenterCrop single-channel (advanced). [native planar NCHW]" + }, + "baselines": { + "centercrop_planar_nchw_scalar_u8_1080p_advanced[InOutDataType=uint8][shape=1024x1080x1920][cropType=QUARTER][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 894.78145561044, + "gpu_time_us_python": 904.67405000742, + "gpu_noise_us_cpp": 0.966, + "gpu_noise_us_python": 2.354, + "gpu_bwutil_cpp": 0.7629425057930501, + "gpu_bwutil_python": 0.7545999838528561, + "gpu_gap_stddev_us": 0.4493087592035211 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 814.7381256401, + "gpu_time_us_python": 824.245408651475, + "gpu_noise_us_cpp": 1.4525000000000001, + "gpu_noise_us_python": 3.9475, + "gpu_bwutil_cpp": 0.6390764016658288, + "gpu_bwutil_python": 0.6317045008200484, + "gpu_gap_stddev_us": 1.214776034333152 + } + } + } + }, + "centercrop_planar_nchw_rgb_u8_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "336x1080x1920" + ], + "cropType": [ + "QUARTER" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CenterCrop RGB8 (advanced). [native planar NCHW]" + }, + "baselines": { + "centercrop_planar_nchw_rgb_u8_1080p_advanced[InOutDataType=uchar3][shape=336x1080x1920][cropType=QUARTER][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 881.02722969688, + "gpu_time_us_python": 891.40622496948, + "gpu_noise_us_cpp": 1.8540000000000003, + "gpu_noise_us_python": 3.012, + "gpu_bwutil_cpp": 0.762746332467196, + "gpu_bwutil_python": 0.7538671222082444, + "gpu_gap_stddev_us": 2.0675921846383796 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 802.3523510896, + "gpu_time_us_python": 813.3947300285251, + "gpu_noise_us_cpp": 1.7000000000000002, + "gpu_noise_us_python": 3.495, + "gpu_bwutil_cpp": 0.6388020842111978, + "gpu_bwutil_python": 0.6301347607858258, + "gpu_gap_stddev_us": 2.425323377260178 + } + } + } + }, + "centercrop_planar_nchw_rgb_f32_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "cropType": [ + "QUARTER" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CenterCrop RGBf32 (advanced). [native planar NCHW]" + }, + "baselines": { + "centercrop_planar_nchw_rgb_f32_1080p_advanced[InOutDataType=float3][shape=64x1080x1920][cropType=QUARTER][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 672.2801220426401, + "gpu_time_us_python": 681.85699874884, + "gpu_noise_us_cpp": 1.1520000000000001, + "gpu_noise_us_python": 2.2439999999999998, + "gpu_bwutil_cpp": 0.7615874432478047, + "gpu_bwutil_python": 0.7508921993849577, + "gpu_gap_stddev_us": 0.8822215659090279 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 595.1574185961501, + "gpu_time_us_python": 604.651182442125, + "gpu_noise_us_cpp": 0.8200000000000001, + "gpu_noise_us_python": 2.73, + "gpu_bwutil_cpp": 0.6561459095788506, + "gpu_bwutil_python": 0.6458544041760108, + "gpu_gap_stddev_us": 1.830895679351597 + } + } + } + }, + "centercrop_planar_nchw_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "256x1080x1920" + ], + "cropType": [ + "QUARTER" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CenterCrop RGB8 native-planar Tensor comparison (advanced)." + }, + "baselines": { + "centercrop_planar_nchw_uchar3_advanced[InOutDataType=uchar3][shape=256x1080x1920][cropType=QUARTER][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 673.78116489136, + "gpu_time_us_python": 683.8914488333601, + "gpu_noise_us_cpp": 1.2580000000000002, + "gpu_noise_us_python": 2.532, + "gpu_bwutil_cpp": 0.7598907043872727, + "gpu_bwutil_python": 0.7486582379264723, + "gpu_gap_stddev_us": 1.2330844600656736 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 614.02183382575, + "gpu_time_us_python": 625.15981992565, + "gpu_noise_us_cpp": 0.8775000000000001, + "gpu_noise_us_python": 4.425000000000001, + "gpu_bwutil_cpp": 0.6359871406446237, + "gpu_bwutil_python": 0.6246638755408119, + "gpu_gap_stddev_us": 1.5751675680419872 + } + } + } + }, + "centercrop_fakeplanar_nchw_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "256x1080x1920" + ], + "cropType": [ + "QUARTER" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CenterCrop RGB8 (advanced). [fake-planar NCHW_FAKE]" + }, + "baselines": { + "centercrop_fakeplanar_nchw_uchar3_advanced[InOutDataType=uchar3][shape=256x1080x1920][cropType=QUARTER][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 8024.5567440986, + "gpu_time_us_python": 8041.7664349078605, + "gpu_noise_us_cpp": 4.180000000000001, + "gpu_noise_us_python": 3.322, + "gpu_bwutil_cpp": 0.38282500671106046, + "gpu_bwutil_python": 0.3820057558854218, + "gpu_gap_stddev_us": 5.12515342872257 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 6242.695141129325, + "gpu_time_us_python": 6258.508847617501, + "gpu_noise_us_cpp": 2.7624999999999997, + "gpu_noise_us_python": 4.8725000000000005, + "gpu_bwutil_cpp": 0.3753299777161253, + "gpu_bwutil_python": 0.37438201690381245, + "gpu_gap_stddev_us": 1.6812485527744407 + } + } + } + }, + "centercrop_fakeplanar_nchw_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "256x1080x1920" + ], + "cropType": [ + "QUARTER" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CenterCrop RGBA8 (advanced). [fake-planar NCHW_FAKE]" + }, + "baselines": { + "centercrop_fakeplanar_nchw_uchar4_advanced[InOutDataType=uchar4][shape=256x1080x1920][cropType=QUARTER][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 10271.842300414979, + "gpu_time_us_python": 10294.886375427199, + "gpu_noise_us_cpp": 2.37, + "gpu_noise_us_python": 9.112, + "gpu_bwutil_cpp": 0.3987601363622228, + "gpu_bwutil_python": 0.3978676919570665, + "gpu_gap_stddev_us": 4.616052653731618 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 8027.5885090231495, + "gpu_time_us_python": 8041.5341911777, + "gpu_noise_us_cpp": 3.7475, + "gpu_noise_us_python": 3.88, + "gpu_bwutil_cpp": 0.38917204808984895, + "gpu_bwutil_python": 0.3884964591791947, + "gpu_gap_stddev_us": 2.8897441981766354 + } + } + } + }, + "centercrop_fakeplanar_nchw_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "cropType": [ + "QUARTER" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CenterCrop RGBAf32 (advanced). [fake-planar NCHW_FAKE]" + }, + "baselines": { + "centercrop_fakeplanar_nchw_float4_advanced[InOutDataType=float4][shape=64x1080x1920][cropType=QUARTER][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6861.19293779936, + "gpu_time_us_python": 6879.503275896981, + "gpu_noise_us_cpp": 3.618, + "gpu_noise_us_python": 3.818, + "gpu_bwutil_cpp": 0.5969807577039669, + "gpu_bwutil_python": 0.5953918431021713, + "gpu_gap_stddev_us": 2.2646902473771946 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 5806.98108458865, + "gpu_time_us_python": 5824.7258232839, + "gpu_noise_us_cpp": 6.4025, + "gpu_noise_us_python": 7.67, + "gpu_bwutil_cpp": 0.5379868189047046, + "gpu_bwutil_python": 0.5363476524851359, + "gpu_gap_stddev_us": 1.6103242947213414 + } + } + } + }, + "centercrop_fakeplanar_nchw_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "256x1080x1920" + ], + "cropType": [ + "QUARTER" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CenterCrop single-channel (advanced). [fake-planar NCHW_FAKE]" + }, + "baselines": { + "centercrop_fakeplanar_nchw_scalar_advanced[InOutDataType=float32][shape=256x1080x1920][cropType=QUARTER][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5190.53486415314, + "gpu_time_us_python": 5208.254307467881, + "gpu_noise_us_cpp": 1.07, + "gpu_noise_us_python": 2.7339999999999995, + "gpu_bwutil_cpp": 0.7891288897514587, + "gpu_bwutil_python": 0.7864440337490565, + "gpu_gap_stddev_us": 1.0586931932385422 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 4807.459799575475, + "gpu_time_us_python": 4821.936439057675, + "gpu_noise_us_cpp": 2.615, + "gpu_noise_us_python": 4.2, + "gpu_bwutil_cpp": 0.6498425175281863, + "gpu_bwutil_python": 0.6478914297853884, + "gpu_gap_stddev_us": 0.8297652436467704 + } + }, + "centercrop_fakeplanar_nchw_scalar_advanced[InOutDataType=uint8][shape=256x1080x1920][cropType=QUARTER][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1189.60166644142, + "gpu_time_us_python": 1206.06927818154, + "gpu_noise_us_cpp": 1.652, + "gpu_noise_us_python": 3.0119999999999996, + "gpu_bwutil_cpp": 0.8607923472280955, + "gpu_bwutil_python": 0.8490406895211023, + "gpu_gap_stddev_us": 1.9245517022642398 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 939.031848635075, + "gpu_time_us_python": 952.4075137344499, + "gpu_noise_us_cpp": 1.9075, + "gpu_noise_us_python": 4.135, + "gpu_bwutil_cpp": 0.8317257393951856, + "gpu_bwutil_python": 0.8200450804884446, + "gpu_gap_stddev_us": 0.46204797460061675 + } + } + } + }, + "centercrop_fakeplanar_nchw_scalar_u8_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "1024x1080x1920" + ], + "cropType": [ + "QUARTER" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CenterCrop single-channel (advanced). [fake-planar NCHW_FAKE]" + }, + "baselines": { + "centercrop_fakeplanar_nchw_scalar_u8_1080p_advanced[InOutDataType=uint8][shape=1024x1080x1920][cropType=QUARTER][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4705.81469712428, + "gpu_time_us_python": 4722.77307330432, + "gpu_noise_us_cpp": 3.4200000000000004, + "gpu_noise_us_python": 5.082, + "gpu_bwutil_cpp": 0.8704125144578951, + "gpu_bwutil_python": 0.8672871545580361, + "gpu_gap_stddev_us": 1.6050307369019652 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 3691.517762401475, + "gpu_time_us_python": 3707.0920029106746, + "gpu_noise_us_cpp": 2.1725000000000003, + "gpu_noise_us_python": 5.180000000000001, + "gpu_bwutil_cpp": 0.8462829727647552, + "gpu_bwutil_python": 0.8427275466438844, + "gpu_gap_stddev_us": 0.25306935629817723 + } + } + } + }, + "centercrop_fakeplanar_nchw_rgb_u8_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "336x1080x1920" + ], + "cropType": [ + "QUARTER" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CenterCrop RGB8 (advanced). [fake-planar NCHW_FAKE]" + }, + "baselines": { + "centercrop_fakeplanar_nchw_rgb_u8_1080p_advanced[InOutDataType=uchar3][shape=336x1080x1920][cropType=QUARTER][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 10520.1407670974, + "gpu_time_us_python": 10541.081587473502, + "gpu_noise_us_cpp": 4.464, + "gpu_noise_us_python": 5.775999999999999, + "gpu_bwutil_cpp": 0.3832649454757742, + "gpu_bwutil_python": 0.3825035643833356, + "gpu_gap_stddev_us": 2.7451585401125658 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 8183.6516164963505, + "gpu_time_us_python": 8200.695960752375, + "gpu_noise_us_cpp": 2.0900000000000003, + "gpu_noise_us_python": 8.600000000000001, + "gpu_bwutil_cpp": 0.37578363566266704, + "gpu_bwutil_python": 0.37500248057284513, + "gpu_gap_stddev_us": 2.166882366234863 + } + } + } + }, + "centercrop_fakeplanar_nchw_rgb_f32_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "cropType": [ + "QUARTER" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CenterCrop RGBf32 (advanced). [fake-planar NCHW_FAKE]" + }, + "baselines": { + "centercrop_fakeplanar_nchw_rgb_f32_1080p_advanced[InOutDataType=float3][shape=64x1080x1920][cropType=QUARTER][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4222.3548460006405, + "gpu_time_us_python": 4240.2284112487805, + "gpu_noise_us_cpp": 1.85, + "gpu_noise_us_python": 4.67, + "gpu_bwutil_cpp": 0.7275561192819466, + "gpu_bwutil_python": 0.7244893980602766, + "gpu_gap_stddev_us": 2.4112087428224624 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 3513.999645302325, + "gpu_time_us_python": 3529.4807877898247, + "gpu_noise_us_cpp": 4.175, + "gpu_noise_us_python": 10.4175, + "gpu_bwutil_cpp": 0.666779462578198, + "gpu_bwutil_python": 0.6638529904534853, + "gpu_gap_stddev_us": 3.3139743945729583 + } + } + } + } + } +} diff --git a/bench/config/operators/channelreorder.json b/bench/config/operators/channelreorder.json new file mode 100644 index 000000000..3f5181354 --- /dev/null +++ b/bench/config/operators/channelreorder.json @@ -0,0 +1,934 @@ +{ + "benchmark": "channelreorder", + "configs": { + "channelreorder_tensor_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "orderPattern": [ + "rotate" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ChannelReorder RGB8 Tensor native-layout comparison." + }, + "baselines": { + "channelreorder_tensor_basic[InOutDataType=uchar3][shape=64x1080x1920][layout=NCHW][orderPattern=rotate][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1916.8873450227602, + "gpu_time_us_python": 1928.37318273686, + "gpu_noise_us_cpp": 1.192, + "gpu_noise_us_python": 3.8320000000000007, + "gpu_bwutil_cpp": 0.2671000995296616, + "gpu_bwutil_python": 0.2655089143238003, + "gpu_gap_stddev_us": 2.9098664928221294 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1660.5143807560803, + "gpu_time_us_python": 1671.55854656324, + "gpu_noise_us_cpp": 1.094, + "gpu_noise_us_python": 2.5380000000000003, + "gpu_bwutil_cpp": 0.23520115345884252, + "gpu_bwutil_python": 0.23364886866773568, + "gpu_gap_stddev_us": 2.1355520217860726 + } + }, + "channelreorder_tensor_basic[InOutDataType=uchar3][shape=64x1080x1920][layout=NHWC][orderPattern=rotate][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1599.2245121366, + "gpu_time_us_python": 1610.34372528586, + "gpu_noise_us_cpp": 1.746, + "gpu_noise_us_python": 3.19, + "gpu_bwutil_cpp": 0.3201556115980627, + "gpu_bwutil_python": 0.3179456167425501, + "gpu_gap_stddev_us": 1.9542810829406785 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1516.85053999334, + "gpu_time_us_python": 1528.4608525128601, + "gpu_noise_us_cpp": 0.892, + "gpu_noise_us_python": 2.684, + "gpu_bwutil_cpp": 0.25751228651278324, + "gpu_bwutil_python": 0.25555493249123146, + "gpu_gap_stddev_us": 2.077098790353357 + } + } + } + }, + "channelreorder_tensor_u8_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW", + "NCHW_FAKE" + ], + "orderPattern": [ + "rotate" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ChannelReorder RGB8 Tensor native/fake-planar comparison." + }, + "baselines": { + "channelreorder_tensor_u8_advanced[InOutDataType=uchar3][shape=64x1080x1920][layout=NCHW][orderPattern=rotate][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1916.8185574407203, + "gpu_time_us_python": 1928.3511290182803, + "gpu_noise_us_cpp": 1.0379999999999998, + "gpu_noise_us_python": 3.138, + "gpu_bwutil_cpp": 0.26710963076260225, + "gpu_bwutil_python": 0.26551188645558543, + "gpu_gap_stddev_us": 2.2946037863067392 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1660.3673042632, + "gpu_time_us_python": 1671.4401665649598, + "gpu_noise_us_cpp": 1.098, + "gpu_noise_us_python": 2.8800000000000003, + "gpu_bwutil_cpp": 0.23522225913501354, + "gpu_bwutil_python": 0.2336685344915367, + "gpu_gap_stddev_us": 2.299795205611309 + } + }, + "channelreorder_tensor_u8_advanced[InOutDataType=uchar3][shape=64x1080x1920][layout=NCHW_FAKE][orderPattern=rotate][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3968.1224928370198, + "gpu_time_us_python": 3984.8358918749605, + "gpu_noise_us_cpp": 1.346, + "gpu_noise_us_python": 3.7279999999999993, + "gpu_bwutil_cpp": 0.3870854030222858, + "gpu_bwutil_python": 0.3854617574637532, + "gpu_gap_stddev_us": 3.254525283980284 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3361.1213874032, + "gpu_time_us_python": 3378.80096042902, + "gpu_noise_us_cpp": 1.916, + "gpu_noise_us_python": 6.338, + "gpu_bwutil_cpp": 0.3485832844039945, + "gpu_bwutil_python": 0.3467614445418075, + "gpu_gap_stddev_us": 2.3180938921400256 + } + }, + "channelreorder_tensor_u8_advanced[InOutDataType=uchar3][shape=64x1080x1920][layout=NHWC][orderPattern=rotate][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1598.94927079504, + "gpu_time_us_python": 1609.59979188346, + "gpu_noise_us_cpp": 3.5, + "gpu_noise_us_python": 2.448, + "gpu_bwutil_cpp": 0.32021071333394274, + "gpu_bwutil_python": 0.31809296552188593, + "gpu_gap_stddev_us": 3.0016502869607593 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1516.68538569654, + "gpu_time_us_python": 1527.24502591278, + "gpu_noise_us_cpp": 0.7419999999999999, + "gpu_noise_us_python": 3.636, + "gpu_bwutil_cpp": 0.25754013721610347, + "gpu_bwutil_python": 0.25576201085071304, + "gpu_gap_stddev_us": 1.0848166518620646 + } + } + } + }, + "channelreorder_tensor_u16_advanced": { + "tier": "advanced", + "dtypes": [ + "ushort3" + ], + "string_axes": { + "shape": [ + "48x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW", + "NCHW_FAKE" + ], + "orderPattern": [ + "rotate" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ChannelReorder RGB16 Tensor native/fake-planar comparison." + }, + "baselines": { + "channelreorder_tensor_u16_advanced[InOutDataType=ushort3][shape=48x1080x1920][layout=NCHW][orderPattern=rotate][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1584.9708277711402, + "gpu_time_us_python": 1594.56353835456, + "gpu_noise_us_cpp": 0.968, + "gpu_noise_us_python": 2.118, + "gpu_bwutil_cpp": 0.48455240534695754, + "gpu_bwutil_python": 0.48163894300924737, + "gpu_gap_stddev_us": 2.647773526189101 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1309.84384004092, + "gpu_time_us_python": 1320.95136299476, + "gpu_noise_us_cpp": 0.694, + "gpu_noise_us_python": 2.29, + "gpu_bwutil_cpp": 0.4472364746821483, + "gpu_bwutil_python": 0.44348237679446195, + "gpu_gap_stddev_us": 2.334716087267991 + } + }, + "channelreorder_tensor_u16_advanced[InOutDataType=ushort3][shape=48x1080x1920][layout=NCHW_FAKE][orderPattern=rotate][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3845.62431724472, + "gpu_time_us_python": 3871.55491755554, + "gpu_noise_us_cpp": 2.96, + "gpu_noise_us_python": 6.156, + "gpu_bwutil_cpp": 0.5991266230468192, + "gpu_bwutil_python": 0.5951127101029708, + "gpu_gap_stddev_us": 4.312792885986892 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3171.3375214307603, + "gpu_time_us_python": 3185.6303178648204, + "gpu_noise_us_cpp": 1.2280000000000002, + "gpu_noise_us_python": 3.782, + "gpu_bwutil_cpp": 0.5541412106612829, + "gpu_bwutil_python": 0.5516561669345319, + "gpu_gap_stddev_us": 1.0730233063279757 + } + }, + "channelreorder_tensor_u16_advanced[InOutDataType=ushort3][shape=48x1080x1920][layout=NHWC][orderPattern=rotate][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1363.3010215213799, + "gpu_time_us_python": 1374.1426418813, + "gpu_noise_us_cpp": 1.668, + "gpu_noise_us_python": 3.4539999999999997, + "gpu_bwutil_cpp": 0.5633460901997235, + "gpu_bwutil_python": 0.5589020112869281, + "gpu_gap_stddev_us": 1.2908189374704053 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1160.06840885608, + "gpu_time_us_python": 1169.7314809066, + "gpu_noise_us_cpp": 0.908, + "gpu_noise_us_python": 2.498, + "gpu_bwutil_cpp": 0.505046618623069, + "gpu_bwutil_python": 0.5008760924331218, + "gpu_gap_stddev_us": 0.6151840721365203 + } + } + } + }, + "channelreorder_tensor_f32_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW", + "NCHW_FAKE" + ], + "orderPattern": [ + "rotate" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ChannelReorder RGBf32 Tensor native/fake-planar comparison." + }, + "baselines": { + "channelreorder_tensor_f32_advanced[InOutDataType=float3][shape=32x1080x1920][layout=NCHW][orderPattern=rotate][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1330.32068555768, + "gpu_time_us_python": 1343.4308397729199, + "gpu_noise_us_cpp": 1.494, + "gpu_noise_us_python": 3.332, + "gpu_bwutil_cpp": 0.7697393705026615, + "gpu_bwutil_python": 0.7622292539927485, + "gpu_gap_stddev_us": 1.709117051543666 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1071.71503136918, + "gpu_time_us_python": 1081.6688693424398, + "gpu_noise_us_cpp": 0.7300000000000001, + "gpu_noise_us_python": 2.216, + "gpu_bwutil_cpp": 0.7287666634142853, + "gpu_bwutil_python": 0.7220560412524442, + "gpu_gap_stddev_us": 1.124640441075723 + } + }, + "channelreorder_tensor_f32_advanced[InOutDataType=float3][shape=32x1080x1920][layout=NCHW_FAKE][orderPattern=rotate][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4345.280486139701, + "gpu_time_us_python": 4374.1714082915005, + "gpu_noise_us_cpp": 2.482, + "gpu_noise_us_python": 4.568, + "gpu_bwutil_cpp": 0.7069822971915756, + "gpu_bwutil_python": 0.7023114795014058, + "gpu_gap_stddev_us": 1.809720681021203 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3509.1417782835997, + "gpu_time_us_python": 3521.41644265352, + "gpu_noise_us_cpp": 2.838, + "gpu_noise_us_python": 4.598, + "gpu_bwutil_cpp": 0.667699338164775, + "gpu_bwutil_python": 0.6653719387949989, + "gpu_gap_stddev_us": 0.7236398878575812 + } + }, + "channelreorder_tensor_f32_advanced[InOutDataType=float3][shape=32x1080x1920][layout=NHWC][orderPattern=rotate][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1668.9186107212597, + "gpu_time_us_python": 1679.4588487208002, + "gpu_noise_us_cpp": 2.3619999999999997, + "gpu_noise_us_python": 3.2439999999999998, + "gpu_bwutil_cpp": 0.6135817869085616, + "gpu_bwutil_python": 0.6097326267049613, + "gpu_gap_stddev_us": 2.1457230286595137 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1318.2460955569, + "gpu_time_us_python": 1328.22729445016, + "gpu_noise_us_cpp": 2.16, + "gpu_noise_us_python": 3.182, + "gpu_bwutil_cpp": 0.592467740906524, + "gpu_bwutil_python": 0.5880155740011317, + "gpu_gap_stddev_us": 1.1435946779709303 + } + } + } + }, + "channelreorder_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "layout": [ + "NHWC" + ], + "orderPattern": [ + "rotate" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ChannelReorder RGB8 (basic)." + }, + "baselines": { + "channelreorder_basic[InOutDataType=uchar3][shape=64x1080x1920][layout=NHWC][orderPattern=rotate][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 750.3529241383401, + "gpu_time_us_python": 754.66834125614, + "gpu_noise_us_cpp": 12.557999999999998, + "gpu_noise_us_python": 11.562, + "gpu_bwutil_cpp": 0.6839544137091745, + "gpu_bwutil_python": 0.6793811348148737, + "gpu_gap_stddev_us": 10.386735347169582 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 648.8495909368199, + "gpu_time_us_python": 645.92437833674, + "gpu_noise_us_cpp": 11.002, + "gpu_noise_us_python": 12.974, + "gpu_bwutil_cpp": 0.601861156146179, + "gpu_bwutil_python": 0.6046103512815688, + "gpu_gap_stddev_us": 6.7923359966347565 + } + } + } + }, + "channelreorder_planar_nchw_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "layout": [ + "NCHW" + ], + "orderPattern": [ + "rotate" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ChannelReorder RGB8 (basic) (native planar NCHW)." + }, + "baselines": { + "channelreorder_planar_nchw_basic[InOutDataType=uchar3][shape=64x1080x1920][layout=NCHW][orderPattern=rotate][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 877.06228544108, + "gpu_time_us_python": 890.2607364233002, + "gpu_noise_us_cpp": 13.762, + "gpu_noise_us_python": 18.134, + "gpu_bwutil_cpp": 0.5851946066345348, + "gpu_bwutil_python": 0.576739975534998, + "gpu_gap_stddev_us": 7.681054715899055 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 787.8739324993401, + "gpu_time_us_python": 797.02209968888, + "gpu_noise_us_cpp": 15.12, + "gpu_noise_us_python": 21.308, + "gpu_bwutil_cpp": 0.4956561368769779, + "gpu_bwutil_python": 0.4899867169501576, + "gpu_gap_stddev_us": 5.7885743314968074 + } + } + } + }, + "channelreorder_rgb_u8_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "layout": [ + "NHWC" + ], + "orderPattern": [ + "rotate" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ChannelReorder RGB8 (advanced)." + }, + "baselines": { + "channelreorder_rgb_u8_1080p_advanced[InOutDataType=uchar3][shape=64x1080x1920][layout=NHWC][orderPattern=rotate][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 747.4313219584001, + "gpu_time_us_python": 756.08740472514, + "gpu_noise_us_cpp": 8.55, + "gpu_noise_us_python": 11.92, + "gpu_bwutil_cpp": 0.6864474814675505, + "gpu_bwutil_python": 0.678127262759108, + "gpu_gap_stddev_us": 9.193214386026538 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 652.9392343314, + "gpu_time_us_python": 648.9915533026399, + "gpu_noise_us_cpp": 16.189999999999998, + "gpu_noise_us_python": 17.716, + "gpu_bwutil_cpp": 0.5981844350437097, + "gpu_bwutil_python": 0.6017732644518744, + "gpu_gap_stddev_us": 6.0770264814420125 + } + } + } + }, + "channelreorder_rgb_u8_planar_nchw_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "layout": [ + "NCHW" + ], + "orderPattern": [ + "rotate" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ChannelReorder RGB8 (advanced) (native planar NCHW)." + }, + "baselines": { + "channelreorder_rgb_u8_planar_nchw_1080p_advanced[InOutDataType=uchar3][shape=64x1080x1920][layout=NCHW][orderPattern=rotate][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 875.93834046498, + "gpu_time_us_python": 885.68779055092, + "gpu_noise_us_cpp": 10.504, + "gpu_noise_us_python": 11.476, + "gpu_bwutil_cpp": 0.5858807590153833, + "gpu_bwutil_python": 0.579537867625756, + "gpu_gap_stddev_us": 3.0567415930717403 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 795.46457830504, + "gpu_time_us_python": 798.43235394154, + "gpu_noise_us_cpp": 25.809999999999995, + "gpu_noise_us_python": 20.338, + "gpu_bwutil_cpp": 0.49094389357345963, + "gpu_bwutil_python": 0.4891216141807016, + "gpu_gap_stddev_us": 7.542590619196061 + } + } + } + }, + "channelreorder_rgba_u8_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "layout": [ + "NHWC" + ], + "orderPattern": [ + "rotate" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ChannelReorder RGBA8 (advanced)." + }, + "baselines": { + "channelreorder_rgba_u8_1080p_advanced[InOutDataType=uchar4][shape=64x1080x1920][layout=NHWC][orderPattern=rotate][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 923.6312271332199, + "gpu_time_us_python": 933.7849124663201, + "gpu_noise_us_cpp": 13.948000000000002, + "gpu_noise_us_python": 8.82, + "gpu_bwutil_cpp": 0.7402797643474751, + "gpu_bwutil_python": 0.7321665293165658, + "gpu_gap_stddev_us": 2.4180268075119886 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 780.48723009442, + "gpu_time_us_python": 785.00037146412, + "gpu_noise_us_cpp": 24.804000000000002, + "gpu_noise_us_python": 12.376, + "gpu_bwutil_cpp": 0.6671829372573145, + "gpu_bwutil_python": 0.6633106660614898, + "gpu_gap_stddev_us": 9.564206085441189 + } + } + } + }, + "channelreorder_rgba_u8_planar_nchw_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "layout": [ + "NCHW" + ], + "orderPattern": [ + "rotate" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ChannelReorder RGBA8 (advanced) (native planar NCHW)." + }, + "baselines": { + "channelreorder_rgba_u8_planar_nchw_1080p_advanced[InOutDataType=uchar4][shape=64x1080x1920][layout=NCHW][orderPattern=rotate][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1100.66747012224, + "gpu_time_us_python": 1097.0895714666199, + "gpu_noise_us_cpp": 30.483999999999998, + "gpu_noise_us_python": 7.678, + "gpu_bwutil_cpp": 0.6221173347034248, + "gpu_bwutil_python": 0.623352015034556, + "gpu_gap_stddev_us": 19.27306168026861 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 955.0650456422602, + "gpu_time_us_python": 964.8773285786799, + "gpu_noise_us_cpp": 15.644, + "gpu_noise_us_python": 21.198, + "gpu_bwutil_cpp": 0.5451781664901387, + "gpu_bwutil_python": 0.5396786053404721, + "gpu_gap_stddev_us": 10.653654844718055 + } + } + } + }, + "channelreorder_rgb_f32_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NHWC" + ], + "orderPattern": [ + "rotate" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ChannelReorder RGBf32 (advanced)." + }, + "baselines": { + "channelreorder_rgb_f32_1080p_advanced[InOutDataType=float3][shape=32x1080x1920][layout=NHWC][orderPattern=rotate][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1595.4670572107202, + "gpu_time_us_python": 1603.31938950154, + "gpu_noise_us_cpp": 5.776000000000001, + "gpu_noise_us_python": 6.867999999999999, + "gpu_bwutil_cpp": 0.6418595720342418, + "gpu_bwutil_python": 0.638699430829646, + "gpu_gap_stddev_us": 3.7638330298638305 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1403.99653870058, + "gpu_time_us_python": 1410.21647200144, + "gpu_noise_us_cpp": 5.866, + "gpu_noise_us_python": 8.016, + "gpu_bwutil_cpp": 0.5562818233056628, + "gpu_bwutil_python": 0.5538308260325622, + "gpu_gap_stddev_us": 4.000083491559786 + } + } + } + }, + "channelreorder_rgb_f32_planar_nchw_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NCHW" + ], + "orderPattern": [ + "rotate" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ChannelReorder RGBf32 (advanced) (native planar NCHW)." + }, + "baselines": { + "channelreorder_rgb_f32_planar_nchw_1080p_advanced[InOutDataType=float3][shape=32x1080x1920][layout=NCHW][orderPattern=rotate][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1437.0667869230201, + "gpu_time_us_python": 1443.7729935562602, + "gpu_noise_us_cpp": 11.774, + "gpu_noise_us_python": 7.272, + "gpu_bwutil_cpp": 0.7127424609827258, + "gpu_bwutil_python": 0.7094601441692886, + "gpu_gap_stddev_us": 5.772151690321994 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1225.80212427756, + "gpu_time_us_python": 1226.6274237204802, + "gpu_noise_us_cpp": 11.93, + "gpu_noise_us_python": 6.404000000000001, + "gpu_bwutil_cpp": 0.6371664131983235, + "gpu_bwutil_python": 0.6367236731000827, + "gpu_gap_stddev_us": 5.152556918882306 + } + } + } + }, + "channelreorder_rgba_f32_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NHWC" + ], + "orderPattern": [ + "rotate" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ChannelReorder RGBAf32 (advanced)." + }, + "baselines": { + "channelreorder_rgba_f32_1080p_advanced[InOutDataType=float4][shape=32x1080x1920][layout=NHWC][orderPattern=rotate][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2724.77704433702, + "gpu_time_us_python": 2736.8668829887397, + "gpu_noise_us_cpp": 8.544, + "gpu_noise_us_python": 4.934, + "gpu_bwutil_cpp": 0.5010903030432715, + "gpu_bwutil_python": 0.4988773209949545, + "gpu_gap_stddev_us": 3.6787164278510485 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2413.46634442988, + "gpu_time_us_python": 2423.28990835406, + "gpu_noise_us_cpp": 9.678, + "gpu_noise_us_python": 9.065999999999999, + "gpu_bwutil_cpp": 0.43147868046649274, + "gpu_bwutil_python": 0.42972877052885144, + "gpu_gap_stddev_us": 4.920814232178313 + } + } + } + }, + "channelreorder_rgba_f32_planar_nchw_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NCHW" + ], + "orderPattern": [ + "rotate" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ChannelReorder RGBAf32 (advanced) (native planar NCHW)." + }, + "baselines": { + "channelreorder_rgba_f32_planar_nchw_1080p_advanced[InOutDataType=float4][shape=32x1080x1920][layout=NCHW][orderPattern=rotate][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1848.85627922606, + "gpu_time_us_python": 1854.3743533805398, + "gpu_noise_us_cpp": 12.746, + "gpu_noise_us_python": 7.902000000000001, + "gpu_bwutil_cpp": 0.7386665336538539, + "gpu_bwutil_python": 0.7363865168550715, + "gpu_gap_stddev_us": 11.137958785367285 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1533.30971680584, + "gpu_time_us_python": 1537.31440176964, + "gpu_noise_us_cpp": 4.798, + "gpu_noise_us_python": 9.39, + "gpu_bwutil_cpp": 0.6791572266617993, + "gpu_bwutil_python": 0.6773909331631119, + "gpu_gap_stddev_us": 3.0049223683287716 + } + } + } + }, + "channelreorder_zero_fill_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "layout": [ + "NHWC" + ], + "orderPattern": [ + "zero_fill" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ChannelReorder RGBA8 zero fill (advanced)." + }, + "baselines": { + "channelreorder_zero_fill_1080p_advanced[InOutDataType=uchar4][shape=64x1080x1920][layout=NHWC][orderPattern=zero_fill][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 922.91303842582, + "gpu_time_us_python": 934.90550631568, + "gpu_noise_us_cpp": 11.906, + "gpu_noise_us_python": 5.3759999999999994, + "gpu_bwutil_cpp": 0.7407607257452751, + "gpu_bwutil_python": 0.7313678325282421, + "gpu_gap_stddev_us": 3.2100727842549723 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 772.690086717, + "gpu_time_us_python": 785.4382168968401, + "gpu_noise_us_cpp": 15.878000000000004, + "gpu_noise_us_python": 13.298000000000002, + "gpu_bwutil_cpp": 0.6738611344276317, + "gpu_bwutil_python": 0.6629236428057835, + "gpu_gap_stddev_us": 4.767786773665544 + } + } + } + }, + "channelreorder_zero_fill_planar_nchw_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "layout": [ + "NCHW" + ], + "orderPattern": [ + "zero_fill" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ChannelReorder RGBA8 zero fill (advanced) (native planar NCHW)." + }, + "baselines": { + "channelreorder_zero_fill_planar_nchw_1080p_advanced[InOutDataType=uchar4][shape=64x1080x1920][layout=NCHW][orderPattern=zero_fill][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 965.9598546651399, + "gpu_time_us_python": 978.2413121284801, + "gpu_noise_us_cpp": 12.526, + "gpu_noise_us_python": 23.372000000000003, + "gpu_bwutil_cpp": 0.7079873479645601, + "gpu_bwutil_python": 0.6994375328726462, + "gpu_gap_stddev_us": 13.483801388942519 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 879.42865929608, + "gpu_time_us_python": 887.2106328226798, + "gpu_noise_us_cpp": 11.082, + "gpu_noise_us_python": 18.242, + "gpu_bwutil_cpp": 0.5920709906277517, + "gpu_bwutil_python": 0.5868815317150325, + "gpu_gap_stddev_us": 7.14811851688717 + } + } + } + } + } +} diff --git a/bench/config/operators/clahe.json b/bench/config/operators/clahe.json new file mode 100644 index 000000000..1f03dc517 --- /dev/null +++ b/bench/config/operators/clahe.json @@ -0,0 +1,771 @@ +{ + "benchmark": "clahe", + "configs": { + "clahe_basic": { + "tier": "basic", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "int64_axes": { + "tilesX": [ + 8 + ], + "tilesY": [ + 8 + ], + "clip10": [ + 400 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CLAHE single-channel clip10=400 (basic)." + }, + "baselines": { + "clahe_basic[InOutDataType=uint8][shape=64x1080x1920][layout=NHWC][inputKind=Tensor][tilesX=8][tilesY=8][clip10=400]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1384.8786994440602, + "gpu_time_us_python": 1394.7483193637202, + "gpu_noise_us_cpp": 1.136, + "gpu_noise_us_python": 3.2399999999999998, + "gpu_bwutil_cpp": 0.12323637712883828, + "gpu_bwutil_python": 0.12236433016178283, + "gpu_gap_stddev_us": 1.194097182917312 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1254.2733621289499, + "gpu_time_us_python": 1263.2199524222249, + "gpu_noise_us_cpp": 1.845, + "gpu_noise_us_python": 3.0174999999999996, + "gpu_bwutil_cpp": 0.10379319054962134, + "gpu_bwutil_python": 0.10305776379697251, + "gpu_gap_stddev_us": 1.618614323971966 + } + }, + "clahe_basic[InOutDataType=uint8][shape=64x1080x1920][layout=NHWC][inputKind=VarShape][tilesX=8][tilesY=8][clip10=400]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3609.01745678718, + "gpu_time_us_python": 3622.8441565672997, + "gpu_noise_us_cpp": 5.474, + "gpu_noise_us_python": 5.99, + "gpu_bwutil_cpp": 0.04728949333524732, + "gpu_bwutil_python": 0.04710891569172768, + "gpu_gap_stddev_us": 4.01993513357974 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 3299.1910661078755, + "gpu_time_us_python": 3314.5423532151, + "gpu_noise_us_cpp": 5.0575, + "gpu_noise_us_python": 7.4925, + "gpu_bwutil_cpp": 0.03946021142145757, + "gpu_bwutil_python": 0.03927733831010205, + "gpu_gap_stddev_us": 1.3726509020908018 + } + }, + "clahe_basic[InOutDataType=uint8][shape=64x1080x1920][layout=NCHW][inputKind=Tensor][tilesX=8][tilesY=8][clip10=400]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1382.37529930522, + "gpu_time_us_python": 1394.35230334966, + "gpu_noise_us_cpp": 1.126, + "gpu_noise_us_python": 2.128, + "gpu_bwutil_cpp": 0.12345960078514548, + "gpu_bwutil_python": 0.12239908659033187, + "gpu_gap_stddev_us": 2.162112898758884 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1253.53819300695, + "gpu_time_us_python": 1264.20818887475, + "gpu_noise_us_cpp": 1.9775000000000003, + "gpu_noise_us_python": 2.9499999999999997, + "gpu_bwutil_cpp": 0.1038539839781658, + "gpu_bwutil_python": 0.10297588834081739, + "gpu_gap_stddev_us": 1.0319763919210032 + } + }, + "clahe_basic[InOutDataType=uint8][shape=64x1080x1920][layout=NCHW][inputKind=VarShape][tilesX=8][tilesY=8][clip10=400]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3608.11690226838, + "gpu_time_us_python": 3622.5935685115996, + "gpu_noise_us_cpp": 5.018000000000001, + "gpu_noise_us_python": 5.782, + "gpu_bwutil_cpp": 0.047301229001017374, + "gpu_bwutil_python": 0.04711211447203838, + "gpu_gap_stddev_us": 1.3656456470932865 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 3299.40003759945, + "gpu_time_us_python": 3313.693113004325, + "gpu_noise_us_cpp": 6.54, + "gpu_noise_us_python": 9.2275, + "gpu_bwutil_cpp": 0.0394578441876906, + "gpu_bwutil_python": 0.03928707770715345, + "gpu_gap_stddev_us": 5.618298616066475 + } + } + } + }, + "clahe_clip20_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "tilesX": [ + 8 + ], + "tilesY": [ + 8 + ], + "clip10": [ + 20 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CLAHE single-channel clip10=20 (advanced)." + }, + "baselines": { + "clahe_clip20_advanced[InOutDataType=uint8][shape=64x1080x1920][layout=NHWC][inputKind=Tensor][tilesX=8][tilesY=8][clip10=20]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1384.52029423766, + "gpu_time_us_python": 1394.6753773579198, + "gpu_noise_us_cpp": 1.6059999999999999, + "gpu_noise_us_python": 3.5159999999999996, + "gpu_bwutil_cpp": 0.12326819455385651, + "gpu_bwutil_python": 0.12237083271622713, + "gpu_gap_stddev_us": 1.4484166546893238 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1253.8672775634, + "gpu_time_us_python": 1263.7137669444, + "gpu_noise_us_cpp": 1.7325, + "gpu_noise_us_python": 2.99, + "gpu_bwutil_cpp": 0.10382691162642929, + "gpu_bwutil_python": 0.10301614914629836, + "gpu_gap_stddev_us": 1.1395616005067961 + } + }, + "clahe_clip20_advanced[InOutDataType=uint8][shape=64x1080x1920][layout=NCHW][inputKind=Tensor][tilesX=8][tilesY=8][clip10=20]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1382.48201760638, + "gpu_time_us_python": 1389.02957163386, + "gpu_noise_us_cpp": 1.986, + "gpu_noise_us_python": 2.838, + "gpu_bwutil_cpp": 0.1234500498644461, + "gpu_bwutil_python": 0.12286808154984716, + "gpu_gap_stddev_us": 2.053134185381245 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1253.661389681175, + "gpu_time_us_python": 1265.205419490725, + "gpu_noise_us_cpp": 1.7225000000000001, + "gpu_noise_us_python": 4.755, + "gpu_bwutil_cpp": 0.10384374364182095, + "gpu_bwutil_python": 0.10289258225425169, + "gpu_gap_stddev_us": 3.290806329565428 + } + } + } + }, + "clahe_tiles8_tensor_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "tilesX": [ + 8 + ], + "tilesY": [ + 8 + ], + "clip10": [ + 20 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CLAHE single-channel tiles8 tensor (advanced)." + }, + "baselines": { + "clahe_tiles8_tensor_1080p_advanced[InOutDataType=uint8][shape=32x1080x1920][layout=NHWC][inputKind=Tensor][tilesX=8][tilesY=8][clip10=20]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 709.7917419857001, + "gpu_time_us_python": 719.5273235713801, + "gpu_noise_us_cpp": 1.27, + "gpu_noise_us_python": 3.532, + "gpu_bwutil_cpp": 0.1202236979184835, + "gpu_bwutil_python": 0.1185969242744204, + "gpu_gap_stddev_us": 0.5745165086855771 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 644.0901378128749, + "gpu_time_us_python": 653.994843955125, + "gpu_noise_us_cpp": 2.4699999999999998, + "gpu_noise_us_python": 2.4425, + "gpu_bwutil_cpp": 0.10105864516578437, + "gpu_bwutil_python": 0.09952896159775301, + "gpu_gap_stddev_us": 1.0343716983393199 + } + }, + "clahe_tiles8_tensor_1080p_advanced[InOutDataType=uint8][shape=32x1080x1920][layout=NCHW][inputKind=Tensor][tilesX=8][tilesY=8][clip10=20]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 709.13783647106, + "gpu_time_us_python": 719.4950430764, + "gpu_noise_us_cpp": 1.8359999999999999, + "gpu_noise_us_python": 2.566, + "gpu_bwutil_cpp": 0.12033462403426567, + "gpu_bwutil_python": 0.11860251068101788, + "gpu_gap_stddev_us": 1.1728815984318768 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 644.972605387025, + "gpu_time_us_python": 655.7309302484, + "gpu_noise_us_cpp": 1.5099999999999998, + "gpu_noise_us_python": 2.6675000000000004, + "gpu_bwutil_cpp": 0.10092137420189908, + "gpu_bwutil_python": 0.09926890634887955, + "gpu_gap_stddev_us": 2.779666418497355 + } + } + } + }, + "clahe_tiles8_varshape_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "VarShape" + ] + }, + "int64_axes": { + "tilesX": [ + 8 + ], + "tilesY": [ + 8 + ], + "clip10": [ + 20 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CLAHE single-channel tiles8 varshape (advanced)." + }, + "baselines": { + "clahe_tiles8_varshape_1080p_advanced[InOutDataType=uint8][shape=64x1080x1920][layout=NHWC][inputKind=VarShape][tilesX=8][tilesY=8][clip10=20]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3609.88097513678, + "gpu_time_us_python": 3623.30803972836, + "gpu_noise_us_cpp": 7.037999999999999, + "gpu_noise_us_python": 9.362, + "gpu_bwutil_cpp": 0.04727805530817757, + "gpu_bwutil_python": 0.047102874163524884, + "gpu_gap_stddev_us": 5.554585709758348 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 3299.2148068952997, + "gpu_time_us_python": 3315.6335036216, + "gpu_noise_us_cpp": 6.71, + "gpu_noise_us_python": 8.9225, + "gpu_bwutil_cpp": 0.03945982750619052, + "gpu_bwutil_python": 0.039264286299357576, + "gpu_gap_stddev_us": 4.2417601839632875 + } + }, + "clahe_tiles8_varshape_1080p_advanced[InOutDataType=uint8][shape=64x1080x1920][layout=NCHW][inputKind=VarShape][tilesX=8][tilesY=8][clip10=20]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3608.2008978644, + "gpu_time_us_python": 3621.31011484076, + "gpu_noise_us_cpp": 8.145999999999999, + "gpu_noise_us_python": 6.5760000000000005, + "gpu_bwutil_cpp": 0.04730006885405934, + "gpu_bwutil_python": 0.04712892974577654, + "gpu_gap_stddev_us": 2.1494138407249603 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 3298.292112193575, + "gpu_time_us_python": 3316.2816880997752, + "gpu_noise_us_cpp": 5.25, + "gpu_noise_us_python": 8.1575, + "gpu_bwutil_cpp": 0.03947063491588075, + "gpu_bwutil_python": 0.03925644591298175, + "gpu_gap_stddev_us": 1.2492473947173341 + } + } + } + }, + "clahe_tiles16_tensor_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "tilesX": [ + 16 + ], + "tilesY": [ + 16 + ], + "clip10": [ + 20 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CLAHE single-channel tiles16 tensor (advanced)." + }, + "baselines": { + "clahe_tiles16_tensor_1080p_advanced[InOutDataType=uint8][shape=32x1080x1920][layout=NHWC][inputKind=Tensor][tilesX=16][tilesY=16][clip10=20]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1501.2040405963598, + "gpu_time_us_python": 1512.09472635484, + "gpu_noise_us_cpp": 2.534, + "gpu_noise_us_python": 3.064, + "gpu_bwutil_cpp": 0.0568437623540559, + "gpu_bwutil_python": 0.05643424351497396, + "gpu_gap_stddev_us": 1.8840280688170072 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1384.5858402437998, + "gpu_time_us_python": 1396.032741368675, + "gpu_noise_us_cpp": 1.7625000000000002, + "gpu_noise_us_python": 3.64, + "gpu_bwutil_cpp": 0.04701272275612412, + "gpu_bwutil_python": 0.046626825076194894, + "gpu_gap_stddev_us": 1.7623159106211665 + } + }, + "clahe_tiles16_tensor_1080p_advanced[InOutDataType=uint8][shape=32x1080x1920][layout=NCHW][inputKind=Tensor][tilesX=16][tilesY=16][clip10=20]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1500.2603694354598, + "gpu_time_us_python": 1511.3399588525801, + "gpu_noise_us_cpp": 1.95, + "gpu_noise_us_python": 3.562, + "gpu_bwutil_cpp": 0.05687950882075086, + "gpu_bwutil_python": 0.05646240705547476, + "gpu_gap_stddev_us": 1.2093471975174184 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1384.6518048339249, + "gpu_time_us_python": 1396.681147282275, + "gpu_noise_us_cpp": 1.8774999999999997, + "gpu_noise_us_python": 4.4399999999999995, + "gpu_bwutil_cpp": 0.04701043601974227, + "gpu_bwutil_python": 0.046605137977801726, + "gpu_gap_stddev_us": 1.505683209146138 + } + } + } + }, + "clahe_tiles16_varshape_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "VarShape" + ] + }, + "int64_axes": { + "tilesX": [ + 16 + ], + "tilesY": [ + 16 + ], + "clip10": [ + 20 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CLAHE single-channel tiles16 varshape (advanced)." + }, + "baselines": { + "clahe_tiles16_varshape_1080p_advanced[InOutDataType=uint8][shape=64x1080x1920][layout=NHWC][inputKind=VarShape][tilesX=16][tilesY=16][clip10=20]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3625.36926146122, + "gpu_time_us_python": 3640.9759362538002, + "gpu_noise_us_cpp": 4.804, + "gpu_noise_us_python": 4.672, + "gpu_bwutil_cpp": 0.0470760104032469, + "gpu_bwutil_python": 0.04687411231385412, + "gpu_gap_stddev_us": 2.9012295891795046 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 3318.06045577635, + "gpu_time_us_python": 3334.2932301578753, + "gpu_noise_us_cpp": 4.3575, + "gpu_noise_us_python": 9.7475, + "gpu_bwutil_cpp": 0.03923577910967695, + "gpu_bwutil_python": 0.03904430524851292, + "gpu_gap_stddev_us": 4.276213251551783 + } + }, + "clahe_tiles16_varshape_1080p_advanced[InOutDataType=uint8][shape=64x1080x1920][layout=NCHW][inputKind=VarShape][tilesX=16][tilesY=16][clip10=20]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3622.36756699916, + "gpu_time_us_python": 3638.80919995508, + "gpu_noise_us_cpp": 4.1579999999999995, + "gpu_noise_us_python": 7.275999999999999, + "gpu_bwutil_cpp": 0.0471149305122725, + "gpu_bwutil_python": 0.046902050313770836, + "gpu_gap_stddev_us": 2.165142593554458 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 3317.035981648875, + "gpu_time_us_python": 3332.6784521160253, + "gpu_noise_us_cpp": 3.8499999999999996, + "gpu_noise_us_python": 7.484999999999999, + "gpu_bwutil_cpp": 0.0392475098361715, + "gpu_bwutil_python": 0.03906380743011033, + "gpu_gap_stddev_us": 3.1431441457842544 + } + } + } + }, + "clahe_clip400_planar_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "tilesX": [ + 8 + ], + "tilesY": [ + 8 + ], + "clip10": [ + 400 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CLAHE clip10=400 native-planar Tensor comparison (advanced)." + }, + "baselines": { + "clahe_clip400_planar_advanced[InOutDataType=uint8][shape=64x1080x1920][layout=NCHW][inputKind=Tensor][tilesX=8][tilesY=8][clip10=400]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1384.66277534528, + "gpu_time_us_python": 1394.77190420312, + "gpu_noise_us_cpp": 1.864, + "gpu_noise_us_python": 2.45, + "gpu_bwutil_cpp": 0.12325558423902247, + "gpu_bwutil_python": 0.12236227443013288, + "gpu_gap_stddev_us": 3.401456914570606 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1252.52043947235, + "gpu_time_us_python": 1264.04385026785, + "gpu_noise_us_cpp": 4.5575, + "gpu_noise_us_python": 5.387499999999999, + "gpu_bwutil_cpp": 0.10393777083059388, + "gpu_bwutil_python": 0.10299053802099548, + "gpu_gap_stddev_us": 3.611032781242686 + } + } + } + }, + "clahe_fake_planar_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "tilesX": [ + 8 + ], + "tilesY": [ + 8 + ], + "clip10": [ + 20, + 400 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CLAHE single-channel fake-planar tensor (advanced)." + }, + "baselines": { + "clahe_fake_planar_advanced[InOutDataType=uint8][shape=64x1080x1920][layout=NCHW_FAKE][inputKind=Tensor][tilesX=8][tilesY=8][clip10=20]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1764.82071540722, + "gpu_time_us_python": 1781.84568459256, + "gpu_noise_us_cpp": 1.5539999999999998, + "gpu_noise_us_python": 5.571999999999999, + "gpu_bwutil_cpp": 0.2901153061236976, + "gpu_bwutil_python": 0.28734302117641025, + "gpu_gap_stddev_us": 1.3502943108988206 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1539.3380900283, + "gpu_time_us_python": 1558.952790611825, + "gpu_noise_us_cpp": 5.142500000000001, + "gpu_noise_us_python": 3.6725, + "gpu_bwutil_cpp": 0.2537032378654809, + "gpu_bwutil_python": 0.2505081007275189, + "gpu_gap_stddev_us": 3.2184606773784554 + } + }, + "clahe_fake_planar_advanced[InOutDataType=uint8][shape=64x1080x1920][layout=NCHW_FAKE][inputKind=Tensor][tilesX=8][tilesY=8][clip10=400]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1761.63041151578, + "gpu_time_us_python": 1782.6037102351197, + "gpu_noise_us_cpp": 1.4160000000000001, + "gpu_noise_us_python": 4.668, + "gpu_bwutil_cpp": 0.2906407790241786, + "gpu_bwutil_python": 0.28722109670708973, + "gpu_gap_stddev_us": 2.0394933893829794 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1542.2194166382249, + "gpu_time_us_python": 1557.477795455075, + "gpu_noise_us_cpp": 2.315, + "gpu_noise_us_python": 4.515000000000001, + "gpu_bwutil_cpp": 0.2532313941768692, + "gpu_bwutil_python": 0.2507450071940618, + "gpu_gap_stddev_us": 2.4457644254787088 + } + } + } + }, + "clahe_tiles8_tensor_1080p_fake_planar_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "tilesX": [ + 8 + ], + "tilesY": [ + 8 + ], + "clip10": [ + 20 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CLAHE single-channel tiles8 fake-planar tensor (advanced)." + }, + "baselines": { + "clahe_tiles8_tensor_1080p_fake_planar_advanced[InOutDataType=uint8][shape=32x1080x1920][layout=NCHW_FAKE][inputKind=Tensor][tilesX=8][tilesY=8][clip10=20]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 900.3980537216801, + "gpu_time_us_python": 917.2847809195, + "gpu_noise_us_cpp": 2.474, + "gpu_noise_us_python": 3.268, + "gpu_bwutil_cpp": 0.2843194998035051, + "gpu_bwutil_python": 0.27908520327853675, + "gpu_gap_stddev_us": 1.497576021494888 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 790.23519901905, + "gpu_time_us_python": 805.830010971425, + "gpu_noise_us_cpp": 3.39, + "gpu_noise_us_python": 3.3425000000000002, + "gpu_bwutil_cpp": 0.24709829583734255, + "gpu_bwutil_python": 0.24232059332329453, + "gpu_gap_stddev_us": 2.529747951582386 + } + } + } + }, + "clahe_tiles16_tensor_1080p_fake_planar_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "tilesX": [ + 16 + ], + "tilesY": [ + 16 + ], + "clip10": [ + 20 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CLAHE single-channel tiles16 fake-planar tensor (advanced)." + }, + "baselines": { + "clahe_tiles16_tensor_1080p_fake_planar_advanced[InOutDataType=uint8][shape=32x1080x1920][layout=NCHW_FAKE][inputKind=Tensor][tilesX=16][tilesY=16][clip10=20]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1692.2446267468003, + "gpu_time_us_python": 1711.11707156148, + "gpu_noise_us_cpp": 3.218, + "gpu_noise_us_python": 2.572, + "gpu_bwutil_cpp": 0.1512795707793257, + "gpu_bwutil_python": 0.14961113155570058, + "gpu_gap_stddev_us": 1.0811805175995588 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1530.551508814, + "gpu_time_us_python": 1546.192216521975, + "gpu_noise_us_cpp": 1.8875, + "gpu_noise_us_python": 2.6425, + "gpu_bwutil_cpp": 0.12758371022705017, + "gpu_bwutil_python": 0.12629519210298717, + "gpu_gap_stddev_us": 2.0622538571284377 + } + } + } + } + } +} diff --git a/bench/config/operators/colortwist.json b/bench/config/operators/colortwist.json new file mode 100644 index 000000000..335e0c46a --- /dev/null +++ b/bench/config/operators/colortwist.json @@ -0,0 +1,1190 @@ +{ + "benchmark": "colortwist", + "configs": { + "colortwist_basic": { + "tier": "basic", + "dtypes": [ + "uchar3", + "uchar4" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "layout": [ + "NHWC" + ], + "twistMode": [ + "per_sample" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ColorTwist RGB/RGBA (basic)." + }, + "baselines": { + "colortwist_basic[InOutDataType=uchar3][shape=128x1080x1920][layout=NHWC][twistMode=per_sample][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2015.01294735934, + "gpu_time_us_python": 2012.3296467165396, + "gpu_noise_us_cpp": 0.9639999999999999, + "gpu_noise_us_python": 2.186, + "gpu_bwutil_cpp": 0.5081855281530342, + "gpu_bwutil_python": 0.5088638203253435, + "gpu_gap_stddev_us": 2.6209854310212526 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1947.1695107622502, + "gpu_time_us_python": 1958.060630239275, + "gpu_noise_us_cpp": 1.145, + "gpu_noise_us_python": 3.5, + "gpu_bwutil_cpp": 0.4011287075459986, + "gpu_bwutil_python": 0.39889540169972765, + "gpu_gap_stddev_us": 1.4030020500705433 + } + }, + "colortwist_basic[InOutDataType=uchar3][shape=128x1080x1920][layout=NHWC][twistMode=per_sample][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2491.7001946137198, + "gpu_time_us_python": 2493.07497609956, + "gpu_noise_us_cpp": 1.9400000000000002, + "gpu_noise_us_python": 2.7720000000000002, + "gpu_bwutil_cpp": 0.410967187387008, + "gpu_bwutil_python": 0.4107403555520054, + "gpu_gap_stddev_us": 2.3214719560271333 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2236.05292708745, + "gpu_time_us_python": 2249.52622647525, + "gpu_noise_us_cpp": 7.177499999999999, + "gpu_noise_us_python": 7.635, + "gpu_bwutil_cpp": 0.3493151125419805, + "gpu_bwutil_python": 0.347226009465639, + "gpu_gap_stddev_us": 3.027914330985519 + } + }, + "colortwist_basic[InOutDataType=uchar4][shape=128x1080x1920][layout=NHWC][twistMode=per_sample][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1999.34979657214, + "gpu_time_us_python": 1999.5669458101202, + "gpu_noise_us_cpp": 1.262, + "gpu_noise_us_python": 2.134, + "gpu_bwutil_cpp": 0.682888774379182, + "gpu_bwutil_python": 0.682815196038679, + "gpu_gap_stddev_us": 2.409947914283832 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1946.18958574715, + "gpu_time_us_python": 1958.0177932382248, + "gpu_noise_us_cpp": 1.3724999999999998, + "gpu_noise_us_python": 3.9725, + "gpu_bwutil_cpp": 0.5351073253944534, + "gpu_bwutil_python": 0.5318699118436229, + "gpu_gap_stddev_us": 1.6591597644830853 + } + }, + "colortwist_basic[InOutDataType=uchar4][shape=128x1080x1920][layout=NHWC][twistMode=per_sample][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2302.35406499384, + "gpu_time_us_python": 2306.1043599329596, + "gpu_noise_us_cpp": 2.024, + "gpu_noise_us_python": 3.782, + "gpu_bwutil_cpp": 0.5930192445623744, + "gpu_bwutil_python": 0.5920552972453003, + "gpu_gap_stddev_us": 2.1865094246721366 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2058.96954482125, + "gpu_time_us_python": 2072.5159012855747, + "gpu_noise_us_cpp": 3.0100000000000002, + "gpu_noise_us_python": 6.3625, + "gpu_bwutil_cpp": 0.505785687313764, + "gpu_bwutil_python": 0.5024697284048415, + "gpu_gap_stddev_us": 4.744873047638581 + } + } + } + }, + "colortwist_planar_nchw_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "layout": [ + "NCHW" + ], + "twistMode": [ + "per_sample" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ColorTwist RGB/RGBA (basic). Native planar NCHW." + }, + "baselines": { + "colortwist_planar_nchw_basic[InOutDataType=uchar3][shape=128x1080x1920][layout=NCHW][twistMode=per_sample][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1217.21386053024, + "gpu_time_us_python": 1220.15003184356, + "gpu_noise_us_cpp": 2.432, + "gpu_noise_us_python": 2.484, + "gpu_bwutil_cpp": 0.8412654931570108, + "gpu_bwutil_python": 0.839241394050952, + "gpu_gap_stddev_us": 0.9586098618165172 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1018.8917276438251, + "gpu_time_us_python": 1027.3341616772, + "gpu_noise_us_cpp": 3.9725, + "gpu_noise_us_python": 2.89, + "gpu_bwutil_cpp": 0.7665416276376384, + "gpu_bwutil_python": 0.7602430441157804, + "gpu_gap_stddev_us": 1.610126365833259 + } + }, + "colortwist_planar_nchw_basic[InOutDataType=uchar3][shape=128x1080x1920][layout=NCHW][twistMode=per_sample][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1328.10089272794, + "gpu_time_us_python": 1341.32608882878, + "gpu_noise_us_cpp": 3.436, + "gpu_noise_us_python": 4.774, + "gpu_bwutil_cpp": 0.7710266520138391, + "gpu_bwutil_python": 0.7634239284764377, + "gpu_gap_stddev_us": 1.5450510363055086 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1099.83968499455, + "gpu_time_us_python": 1112.334245060175, + "gpu_noise_us_cpp": 2.6750000000000003, + "gpu_noise_us_python": 4.04, + "gpu_bwutil_cpp": 0.7101192220592587, + "gpu_bwutil_python": 0.7021488660464016, + "gpu_gap_stddev_us": 4.755566152743841 + } + } + } + }, + "colortwist_fakeplanar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3", + "uchar4" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "twistMode": [ + "per_sample" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ColorTwist RGB/RGBA (advanced). Fake planar NCHW via Reformat." + }, + "baselines": { + "colortwist_fakeplanar_nchw_advanced[InOutDataType=uchar3][shape=128x1080x1920][layout=NCHW_FAKE][twistMode=per_sample][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6734.4060621763, + "gpu_time_us_python": 6741.36923488812, + "gpu_noise_us_cpp": 1.5099999999999998, + "gpu_noise_us_python": 4.34, + "gpu_bwutil_cpp": 0.4561650774059801, + "gpu_bwutil_python": 0.4556939198875304, + "gpu_gap_stddev_us": 1.4013678386069386 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 5625.315192010625, + "gpu_time_us_python": 5639.996796184075, + "gpu_noise_us_cpp": 1.9675, + "gpu_noise_us_python": 4.0649999999999995, + "gpu_bwutil_cpp": 0.41652691854533264, + "gpu_bwutil_python": 0.4154432463155665, + "gpu_gap_stddev_us": 3.0929612289211974 + } + }, + "colortwist_fakeplanar_nchw_advanced[InOutDataType=uchar4][shape=128x1080x1920][layout=NCHW_FAKE][twistMode=per_sample][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 8101.41896893894, + "gpu_time_us_python": 8109.597722945618, + "gpu_noise_us_cpp": 1.656, + "gpu_noise_us_python": 3.2079999999999997, + "gpu_bwutil_cpp": 0.5055905634930717, + "gpu_bwutil_python": 0.5050806421760723, + "gpu_gap_stddev_us": 1.5054774539717812 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 6735.38230382865, + "gpu_time_us_python": 6751.6278020008995, + "gpu_noise_us_cpp": 1.7349999999999999, + "gpu_noise_us_python": 3.385, + "gpu_bwutil_cpp": 0.46383994147310925, + "gpu_bwutil_python": 0.4627244577899791, + "gpu_gap_stddev_us": 2.0203731594918275 + } + } + } + }, + "colortwist_rgb_u8_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "layout": [ + "NHWC" + ], + "twistMode": [ + "per_sample" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ColorTwist RGB8 (advanced)." + }, + "baselines": { + "colortwist_rgb_u8_1080p_advanced[InOutDataType=uchar3][shape=128x1080x1920][layout=NHWC][twistMode=per_sample][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2015.27502614908, + "gpu_time_us_python": 2011.9585342406801, + "gpu_noise_us_cpp": 1.056, + "gpu_noise_us_python": 3.934, + "gpu_bwutil_cpp": 0.5081194014966517, + "gpu_bwutil_python": 0.5089571682003273, + "gpu_gap_stddev_us": 1.0516192799911885 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1947.37302064175, + "gpu_time_us_python": 1956.218394449125, + "gpu_noise_us_cpp": 2.6750000000000003, + "gpu_noise_us_python": 2.8149999999999995, + "gpu_bwutil_cpp": 0.40108629350761754, + "gpu_bwutil_python": 0.3992730516125176, + "gpu_gap_stddev_us": 1.005362730251027 + } + }, + "colortwist_rgb_u8_1080p_advanced[InOutDataType=uchar3][shape=128x1080x1920][layout=NHWC][twistMode=per_sample][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2491.7306135196, + "gpu_time_us_python": 2491.83198579464, + "gpu_noise_us_cpp": 1.786, + "gpu_noise_us_python": 3.374, + "gpu_bwutil_cpp": 0.41096218171556825, + "gpu_bwutil_python": 0.4109451297535829, + "gpu_gap_stddev_us": 0.8067833561143859 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2239.1005087372496, + "gpu_time_us_python": 2246.889181224225, + "gpu_noise_us_cpp": 1.5, + "gpu_noise_us_python": 7.975, + "gpu_bwutil_cpp": 0.34884259697625186, + "gpu_bwutil_python": 0.34763007861158984, + "gpu_gap_stddev_us": 6.036175160766161 + } + } + } + }, + "colortwist_planar_nchw_rgb_u8_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "layout": [ + "NCHW" + ], + "twistMode": [ + "per_sample" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ColorTwist RGB8 (advanced). Native planar NCHW." + }, + "baselines": { + "colortwist_planar_nchw_rgb_u8_1080p_advanced[InOutDataType=uchar3][shape=128x1080x1920][layout=NCHW][twistMode=per_sample][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1217.14029057506, + "gpu_time_us_python": 1220.19939120218, + "gpu_noise_us_cpp": 3.8519999999999994, + "gpu_noise_us_python": 2.842, + "gpu_bwutil_cpp": 0.8413163435782058, + "gpu_bwutil_python": 0.8392071584961307, + "gpu_gap_stddev_us": 0.48067863881379475 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1020.1370826931501, + "gpu_time_us_python": 1028.1594265816, + "gpu_noise_us_cpp": 1.855, + "gpu_noise_us_python": 5.9174999999999995, + "gpu_bwutil_cpp": 0.7656079434158702, + "gpu_bwutil_python": 0.7596279636662907, + "gpu_gap_stddev_us": 2.5441081773282908 + } + }, + "colortwist_planar_nchw_rgb_u8_1080p_advanced[InOutDataType=uchar3][shape=128x1080x1920][layout=NCHW][twistMode=per_sample][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1328.41622085162, + "gpu_time_us_python": 1340.9557587198801, + "gpu_noise_us_cpp": 2.804, + "gpu_noise_us_python": 3.7299999999999995, + "gpu_bwutil_cpp": 0.7708428062475482, + "gpu_bwutil_python": 0.7636356741579813, + "gpu_gap_stddev_us": 2.2712246199214077 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1100.056560677325, + "gpu_time_us_python": 1112.700131047375, + "gpu_noise_us_cpp": 2.785, + "gpu_noise_us_python": 7.975, + "gpu_bwutil_cpp": 0.7099790062299549, + "gpu_bwutil_python": 0.7019127244164503, + "gpu_gap_stddev_us": 1.9325839711401096 + } + } + } + }, + "colortwist_fakeplanar_nchw_rgb_u8_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "twistMode": [ + "per_sample" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ColorTwist RGB8 (advanced). Fake planar NCHW via Reformat." + }, + "baselines": { + "colortwist_fakeplanar_nchw_rgb_u8_1080p_advanced[InOutDataType=uchar3][shape=128x1080x1920][layout=NCHW_FAKE][twistMode=per_sample][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6735.564791528739, + "gpu_time_us_python": 6740.194328207661, + "gpu_noise_us_cpp": 3.3919999999999995, + "gpu_noise_us_python": 4.514, + "gpu_bwutil_cpp": 0.45608657837319866, + "gpu_bwutil_python": 0.4557733616384752, + "gpu_gap_stddev_us": 5.596904698779336 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 5625.488871998199, + "gpu_time_us_python": 5641.440531942525, + "gpu_noise_us_cpp": 1.8274999999999997, + "gpu_noise_us_python": 3.5075000000000003, + "gpu_bwutil_cpp": 0.4165138081470583, + "gpu_bwutil_python": 0.41533616779686633, + "gpu_gap_stddev_us": 0.3080764662462076 + } + } + } + }, + "colortwist_rgba_u8_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "layout": [ + "NHWC" + ], + "twistMode": [ + "per_sample" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ColorTwist RGBA8 (advanced)." + }, + "baselines": { + "colortwist_rgba_u8_1080p_advanced[InOutDataType=uchar4][shape=128x1080x1920][layout=NHWC][twistMode=per_sample][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1998.91838942628, + "gpu_time_us_python": 1999.88804238782, + "gpu_noise_us_cpp": 1.296, + "gpu_noise_us_python": 3.8819999999999992, + "gpu_bwutil_cpp": 0.6830361509385383, + "gpu_bwutil_python": 0.6827051939218178, + "gpu_gap_stddev_us": 0.9358931812401522 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1946.669478599775, + "gpu_time_us_python": 1956.161575476525, + "gpu_noise_us_cpp": 1.7374999999999998, + "gpu_noise_us_python": 3.5825, + "gpu_bwutil_cpp": 0.5349742842759335, + "gpu_bwutil_python": 0.5323782455935306, + "gpu_gap_stddev_us": 1.2552492911484319 + } + }, + "colortwist_rgba_u8_1080p_advanced[InOutDataType=uchar4][shape=128x1080x1920][layout=NHWC][twistMode=per_sample][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2302.9553137787398, + "gpu_time_us_python": 2306.6886783739396, + "gpu_noise_us_cpp": 2.306, + "gpu_noise_us_python": 3.982, + "gpu_bwutil_cpp": 0.5928641249672477, + "gpu_bwutil_python": 0.5919050953095837, + "gpu_gap_stddev_us": 0.9503457636554135 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2059.562775349575, + "gpu_time_us_python": 2071.59840555435, + "gpu_noise_us_cpp": 3.1075, + "gpu_noise_us_python": 4.08, + "gpu_bwutil_cpp": 0.5056371741514728, + "gpu_bwutil_python": 0.5026958886002678, + "gpu_gap_stddev_us": 1.7695891393627832 + } + } + } + }, + "colortwist_planar_nchw_rgba_u8_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "layout": [ + "NCHW" + ], + "twistMode": [ + "per_sample" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ColorTwist RGBA8 (advanced). Native planar NCHW." + }, + "baselines": { + "colortwist_planar_nchw_rgba_u8_1080p_advanced[InOutDataType=uchar4][shape=128x1080x1920][layout=NCHW][twistMode=per_sample][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1631.0551457590398, + "gpu_time_us_python": 1637.7427539014602, + "gpu_noise_us_cpp": 3.5900000000000007, + "gpu_noise_us_python": 4.252, + "gpu_bwutil_cpp": 0.8370859528613289, + "gpu_bwutil_python": 0.8336681249927189, + "gpu_gap_stddev_us": 1.3788778985246741 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1317.2198329304, + "gpu_time_us_python": 1321.37632928855, + "gpu_noise_us_cpp": 3.1575, + "gpu_noise_us_python": 4.67, + "gpu_bwutil_cpp": 0.7905736856649701, + "gpu_bwutil_python": 0.7880853492824784, + "gpu_gap_stddev_us": 1.6597653640848269 + } + } + } + }, + "colortwist_fakeplanar_nchw_rgba_u8_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "twistMode": [ + "per_sample" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ColorTwist RGBA8 (advanced). Fake planar NCHW via Reformat." + }, + "baselines": { + "colortwist_fakeplanar_nchw_rgba_u8_1080p_advanced[InOutDataType=uchar4][shape=128x1080x1920][layout=NCHW_FAKE][twistMode=per_sample][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 8100.83103795198, + "gpu_time_us_python": 8111.11723376854, + "gpu_noise_us_cpp": 3.35, + "gpu_noise_us_python": 2.854, + "gpu_bwutil_cpp": 0.5056272518596753, + "gpu_bwutil_python": 0.504986079933197, + "gpu_gap_stddev_us": 2.876167707325984 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 6737.155864537125, + "gpu_time_us_python": 6754.54776571955, + "gpu_noise_us_cpp": 2.9125, + "gpu_noise_us_python": 5.585, + "gpu_bwutil_cpp": 0.4637169068621146, + "gpu_bwutil_python": 0.46252206078764607, + "gpu_gap_stddev_us": 2.38612649905338 + } + } + } + }, + "colortwist_rgb_f32_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NHWC" + ], + "twistMode": [ + "per_sample" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ColorTwist RGBf32 (advanced)." + }, + "baselines": { + "colortwist_rgb_f32_1080p_advanced[InOutDataType=float3][shape=32x1080x1920][layout=NHWC][twistMode=per_sample][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1146.9995118007002, + "gpu_time_us_python": 1156.2303738735802, + "gpu_noise_us_cpp": 1.89, + "gpu_noise_us_python": 2.366, + "gpu_bwutil_cpp": 0.8927641366937074, + "gpu_bwutil_python": 0.8856369694518715, + "gpu_gap_stddev_us": 0.6774644201545436 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 877.7713481641999, + "gpu_time_us_python": 886.73277291465, + "gpu_noise_us_cpp": 2.5649999999999995, + "gpu_noise_us_python": 3.0100000000000002, + "gpu_bwutil_cpp": 0.8897728469761416, + "gpu_bwutil_python": 0.8807806070087947, + "gpu_gap_stddev_us": 0.2772224023316688 + } + }, + "colortwist_rgb_f32_1080p_advanced[InOutDataType=float3][shape=32x1080x1920][layout=NHWC][twistMode=per_sample][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1166.7484654936202, + "gpu_time_us_python": 1178.8348427052001, + "gpu_noise_us_cpp": 1.6939999999999997, + "gpu_noise_us_python": 3.6340000000000003, + "gpu_bwutil_cpp": 0.8776529750363167, + "gpu_bwutil_python": 0.8686548252469037, + "gpu_gap_stddev_us": 1.3910443595512925 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 898.42661208275, + "gpu_time_us_python": 903.9073217431, + "gpu_noise_us_cpp": 3.0175, + "gpu_noise_us_python": 3.8375, + "gpu_bwutil_cpp": 0.8693178403890236, + "gpu_bwutil_python": 0.8640458372262183, + "gpu_gap_stddev_us": 2.1052299336038893 + } + } + } + }, + "colortwist_planar_nchw_rgb_f32_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NCHW" + ], + "twistMode": [ + "per_sample" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ColorTwist RGBf32 (advanced). Native planar NCHW." + }, + "baselines": { + "colortwist_planar_nchw_rgb_f32_1080p_advanced[InOutDataType=float3][shape=32x1080x1920][layout=NCHW][twistMode=per_sample][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1166.8132484790399, + "gpu_time_us_python": 1175.5558554555, + "gpu_noise_us_cpp": 2.968, + "gpu_noise_us_python": 1.7959999999999998, + "gpu_bwutil_cpp": 0.8776040469942987, + "gpu_bwutil_python": 0.8710775792259273, + "gpu_gap_stddev_us": 0.9544814757704769 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 899.2838091779499, + "gpu_time_us_python": 909.15935140545, + "gpu_noise_us_cpp": 1.1975, + "gpu_noise_us_python": 3.3549999999999995, + "gpu_bwutil_cpp": 0.8684898295769722, + "gpu_bwutil_python": 0.8590586700527164, + "gpu_gap_stddev_us": 1.43202393555411 + } + }, + "colortwist_planar_nchw_rgb_f32_1080p_advanced[InOutDataType=float3][shape=32x1080x1920][layout=NCHW][twistMode=per_sample][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1191.4064574241202, + "gpu_time_us_python": 1209.1121164358199, + "gpu_noise_us_cpp": 2.028, + "gpu_noise_us_python": 3.854, + "gpu_bwutil_cpp": 0.8594884136595022, + "gpu_bwutil_python": 0.8469039431685221, + "gpu_gap_stddev_us": 1.904865439977889 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 935.7655787290751, + "gpu_time_us_python": 950.949857666875, + "gpu_noise_us_cpp": 2.5675000000000003, + "gpu_noise_us_python": 3.8974999999999995, + "gpu_bwutil_cpp": 0.8346321021749, + "gpu_bwutil_python": 0.8213117835165489, + "gpu_gap_stddev_us": 3.4329020254971105 + } + } + } + }, + "colortwist_fakeplanar_nchw_rgb_f32_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "twistMode": [ + "per_sample" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ColorTwist RGBf32 (advanced). Fake planar NCHW via Reformat." + }, + "baselines": { + "colortwist_fakeplanar_nchw_rgb_f32_1080p_advanced[InOutDataType=float3][shape=32x1080x1920][layout=NCHW_FAKE][twistMode=per_sample][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3827.1100665583203, + "gpu_time_us_python": 3844.97797650328, + "gpu_noise_us_cpp": 1.5459999999999998, + "gpu_noise_us_python": 2.54, + "gpu_bwutil_cpp": 0.8026944767710115, + "gpu_bwutil_python": 0.7989643468212885, + "gpu_gap_stddev_us": 1.8883952615333315 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 3077.033947880625, + "gpu_time_us_python": 3092.7783677607003, + "gpu_noise_us_cpp": 2.465, + "gpu_noise_us_python": 4.015000000000001, + "gpu_bwutil_cpp": 0.7614651276479216, + "gpu_bwutil_python": 0.7575884939623794, + "gpu_gap_stddev_us": 2.6259132560678227 + } + } + } + }, + "colortwist_rgba_f32_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NHWC" + ], + "twistMode": [ + "per_sample" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ColorTwist RGBAf32 (advanced)." + }, + "baselines": { + "colortwist_rgba_f32_1080p_advanced[InOutDataType=float4][shape=32x1080x1920][layout=NHWC][twistMode=per_sample][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1518.8923753391198, + "gpu_time_us_python": 1527.84918037852, + "gpu_noise_us_cpp": 1.3439999999999999, + "gpu_noise_us_python": 1.752, + "gpu_bwutil_cpp": 0.8989006511207307, + "gpu_bwutil_python": 0.8936309759418049, + "gpu_gap_stddev_us": 0.07903606757018249 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1163.881452139, + "gpu_time_us_python": 1173.66303697455, + "gpu_noise_us_cpp": 1.2249999999999999, + "gpu_noise_us_python": 5.1674999999999995, + "gpu_bwutil_cpp": 0.8947269470336183, + "gpu_bwutil_python": 0.8872713284341786, + "gpu_gap_stddev_us": 1.8168744288287846 + } + }, + "colortwist_rgba_f32_1080p_advanced[InOutDataType=float4][shape=32x1080x1920][layout=NHWC][twistMode=per_sample][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1532.43221960412, + "gpu_time_us_python": 1542.5133855796, + "gpu_noise_us_cpp": 2.512, + "gpu_noise_us_python": 2.484, + "gpu_bwutil_cpp": 0.8909584355246544, + "gpu_bwutil_python": 0.8851355234601526, + "gpu_gap_stddev_us": 0.3115638066603586 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1171.6843000916997, + "gpu_time_us_python": 1184.090432706675, + "gpu_noise_us_cpp": 3.15, + "gpu_noise_us_python": 5.4175, + "gpu_bwutil_cpp": 0.8887684211144138, + "gpu_bwutil_python": 0.8794603258458281, + "gpu_gap_stddev_us": 2.697204486218304 + } + } + } + }, + "colortwist_planar_nchw_rgba_f32_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NCHW" + ], + "twistMode": [ + "per_sample" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ColorTwist RGBAf32 (advanced). Native planar NCHW." + }, + "baselines": { + "colortwist_planar_nchw_rgba_f32_1080p_advanced[InOutDataType=float4][shape=32x1080x1920][layout=NCHW][twistMode=per_sample][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1580.59100864814, + "gpu_time_us_python": 1589.18188961242, + "gpu_noise_us_cpp": 2.022, + "gpu_noise_us_python": 2.136, + "gpu_bwutil_cpp": 0.863811913411096, + "gpu_bwutil_python": 0.859142382922205, + "gpu_gap_stddev_us": 0.5292650143243421 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1223.858654789775, + "gpu_time_us_python": 1232.7056156990247, + "gpu_noise_us_cpp": 2.5625, + "gpu_noise_us_python": 2.7275, + "gpu_bwutil_cpp": 0.850880241612215, + "gpu_bwutil_python": 0.8447753084358727, + "gpu_gap_stddev_us": 1.4210189259680974 + } + }, + "colortwist_planar_nchw_rgba_f32_1080p_advanced[InOutDataType=float4][shape=32x1080x1920][layout=NCHW][twistMode=per_sample][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1616.6106268666801, + "gpu_time_us_python": 1630.2399237433997, + "gpu_noise_us_cpp": 2.2920000000000003, + "gpu_noise_us_python": 2.9780000000000006, + "gpu_bwutil_cpp": 0.8445653880302608, + "gpu_bwutil_python": 0.8375055665932795, + "gpu_gap_stddev_us": 2.179307838721667 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1267.536979639825, + "gpu_time_us_python": 1282.6568063791249, + "gpu_noise_us_cpp": 2.6950000000000003, + "gpu_noise_us_python": 18.54, + "gpu_bwutil_cpp": 0.8215608260852056, + "gpu_bwutil_python": 0.8118833071600979, + "gpu_gap_stddev_us": 3.8908088864030836 + } + } + } + }, + "colortwist_fakeplanar_nchw_rgba_f32_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "twistMode": [ + "per_sample" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ColorTwist RGBAf32 (advanced). Fake planar NCHW via Reformat." + }, + "baselines": { + "colortwist_fakeplanar_nchw_rgba_f32_1080p_advanced[InOutDataType=float4][shape=32x1080x1920][layout=NCHW_FAKE][twistMode=per_sample][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5741.688988425461, + "gpu_time_us_python": 5760.51200303162, + "gpu_noise_us_cpp": 5.04, + "gpu_noise_us_python": 5.0520000000000005, + "gpu_bwutil_cpp": 0.7133789520468042, + "gpu_bwutil_python": 0.7110480622777333, + "gpu_gap_stddev_us": 3.6154220046213617 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 4672.7496275195, + "gpu_time_us_python": 4687.43334434645, + "gpu_noise_us_cpp": 8.8125, + "gpu_noise_us_python": 10.5725, + "gpu_bwutil_cpp": 0.6685730572665574, + "gpu_bwutil_python": 0.6664778008995578, + "gpu_gap_stddev_us": 3.5161315587400996 + } + } + } + }, + "colortwist_global_twist_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "layout": [ + "NHWC" + ], + "twistMode": [ + "global" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ColorTwist RGB8 global twist (advanced)." + }, + "baselines": { + "colortwist_global_twist_1080p_advanced[InOutDataType=uchar3][shape=128x1080x1920][layout=NHWC][twistMode=global][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1985.3100472026, + "gpu_time_us_python": 1995.7353820876, + "gpu_noise_us_cpp": 1.02, + "gpu_noise_us_python": 1.964, + "gpu_bwutil_cpp": 0.5157884849835458, + "gpu_bwutil_python": 0.5130942821298106, + "gpu_gap_stddev_us": 1.0181504587048664 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1945.53197414895, + "gpu_time_us_python": 1956.1917023774, + "gpu_noise_us_cpp": 2.7575000000000003, + "gpu_noise_us_python": 3.4925, + "gpu_bwutil_cpp": 0.4014638884721278, + "gpu_bwutil_python": 0.3992762320831118, + "gpu_gap_stddev_us": 0.7363704136290117 + } + }, + "colortwist_global_twist_1080p_advanced[InOutDataType=uchar3][shape=128x1080x1920][layout=NHWC][twistMode=global][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2449.1996422372404, + "gpu_time_us_python": 2463.2038742887794, + "gpu_noise_us_cpp": 1.256, + "gpu_noise_us_python": 2.78, + "gpu_bwutil_cpp": 0.4180984241975235, + "gpu_bwutil_python": 0.41572177651541836, + "gpu_gap_stddev_us": 1.3997608935690327 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2226.46172128565, + "gpu_time_us_python": 2239.31583474355, + "gpu_noise_us_cpp": 2.0825, + "gpu_noise_us_python": 8.3725, + "gpu_bwutil_cpp": 0.35082442323800395, + "gpu_bwutil_python": 0.34880755508504463, + "gpu_gap_stddev_us": 5.995718506718343 + } + } + } + }, + "colortwist_planar_nchw_global_twist_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "layout": [ + "NCHW" + ], + "twistMode": [ + "global" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ColorTwist RGB8 global twist (advanced). Native planar NCHW." + }, + "baselines": { + "colortwist_planar_nchw_global_twist_1080p_advanced[InOutDataType=uchar3][shape=128x1080x1920][layout=NCHW][twistMode=global][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1188.01857939258, + "gpu_time_us_python": 1196.88541275836, + "gpu_noise_us_cpp": 1.908, + "gpu_noise_us_python": 2.466, + "gpu_bwutil_cpp": 0.8619394326274887, + "gpu_bwutil_python": 0.8555542771617258, + "gpu_gap_stddev_us": 0.9959945160223406 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 989.635626092675, + "gpu_time_us_python": 1000.092483512375, + "gpu_noise_us_cpp": 2.385, + "gpu_noise_us_python": 4.2675, + "gpu_bwutil_cpp": 0.789204276857765, + "gpu_bwutil_python": 0.7809482108335718, + "gpu_gap_stddev_us": 3.290877459044966 + } + }, + "colortwist_planar_nchw_global_twist_1080p_advanced[InOutDataType=uchar3][shape=128x1080x1920][layout=NCHW][twistMode=global][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1313.8595428765998, + "gpu_time_us_python": 1325.74297864594, + "gpu_noise_us_cpp": 2.546, + "gpu_noise_us_python": 3.4259999999999997, + "gpu_bwutil_cpp": 0.7793832206047362, + "gpu_bwutil_python": 0.7723972757751796, + "gpu_gap_stddev_us": 0.6352329693517 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1092.678243091525, + "gpu_time_us_python": 1106.794167858, + "gpu_noise_us_cpp": 3.1300000000000003, + "gpu_noise_us_python": 6.105, + "gpu_bwutil_cpp": 0.714774360619459, + "gpu_bwutil_python": 0.705657986624068, + "gpu_gap_stddev_us": 0.8548035558302618 + } + } + } + }, + "colortwist_fakeplanar_nchw_global_twist_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "twistMode": [ + "global" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ColorTwist RGB8 global twist (advanced). Fake planar NCHW via Reformat." + }, + "baselines": { + "colortwist_fakeplanar_nchw_global_twist_1080p_advanced[InOutDataType=uchar3][shape=128x1080x1920][layout=NCHW_FAKE][twistMode=global][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6705.16814181674, + "gpu_time_us_python": 6723.066658722681, + "gpu_noise_us_cpp": 2.964, + "gpu_noise_us_python": 2.416, + "gpu_bwutil_cpp": 0.4581541287753875, + "gpu_bwutil_python": 0.45693441906405374, + "gpu_gap_stddev_us": 2.0168190526373024 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 5624.2657793892, + "gpu_time_us_python": 5639.243727260125, + "gpu_noise_us_cpp": 2.485, + "gpu_noise_us_python": 5.095000000000001, + "gpu_bwutil_cpp": 0.4166043144934758, + "gpu_bwutil_python": 0.4154986017285063, + "gpu_gap_stddev_us": 1.5049823167674532 + } + } + } + } + } +} diff --git a/bench/config/operators/composite.json b/bench/config/operators/composite.json new file mode 100644 index 000000000..571a8f08f --- /dev/null +++ b/bench/config/operators/composite.json @@ -0,0 +1,674 @@ +{ + "benchmark": "composite", + "configs": { + "composite_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "128x720x1280" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "int64_axes": { + "outChannels": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Composite RGB8 (basic)." + }, + "baselines": { + "composite_basic[InOutDataType=uchar3][shape=128x720x1280][layout=NHWC][inputKind=Tensor][outChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1034.5590571726002, + "gpu_time_us_python": 1042.9021568099201, + "gpu_noise_us_cpp": 1.554, + "gpu_noise_us_python": 2.028, + "gpu_bwutil_cpp": 0.7331806696622074, + "gpu_bwutil_python": 0.7273158627918989, + "gpu_gap_stddev_us": 0.9866860019344453 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 836.9026023419251, + "gpu_time_us_python": 848.093099701975, + "gpu_noise_us_cpp": 1.4025, + "gpu_noise_us_python": 3.495, + "gpu_bwutil_cpp": 0.6912800110856497, + "gpu_bwutil_python": 0.6821567964637754, + "gpu_gap_stddev_us": 1.3188278767668247 + } + }, + "composite_basic[InOutDataType=uchar3][shape=128x720x1280][layout=NHWC][inputKind=VarShape][outChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1248.46670849104, + "gpu_time_us_python": 1267.8011640651598, + "gpu_noise_us_cpp": 5.511999999999999, + "gpu_noise_us_python": 5.67, + "gpu_bwutil_cpp": 0.6075626729411974, + "gpu_bwutil_python": 0.5982976636450495, + "gpu_gap_stddev_us": 1.527718233378603 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1116.038240667675, + "gpu_time_us_python": 1134.8637794342, + "gpu_noise_us_cpp": 6.454999999999999, + "gpu_noise_us_python": 5.865, + "gpu_bwutil_cpp": 0.5184019327839877, + "gpu_bwutil_python": 0.5097913036784051, + "gpu_gap_stddev_us": 3.7722669295897973 + } + } + } + }, + "composite_rgb_tensor_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "outChannels": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Composite RGB8 tensor (advanced)." + }, + "baselines": { + "composite_rgb_tensor_1080p_advanced[InOutDataType=uchar3][shape=64x1080x1920][layout=NHWC][inputKind=Tensor][outChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1164.23755313068, + "gpu_time_us_python": 1172.6260191251, + "gpu_noise_us_cpp": 1.734, + "gpu_noise_us_python": 1.988, + "gpu_bwutil_cpp": 0.7329550247454725, + "gpu_bwutil_python": 0.7277118752626276, + "gpu_gap_stddev_us": 0.3753321441611434 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 940.642012084825, + "gpu_time_us_python": 949.588151150675, + "gpu_noise_us_cpp": 1.15, + "gpu_noise_us_python": 2.775, + "gpu_bwutil_cpp": 0.6919211567437932, + "gpu_bwutil_python": 0.6854041544811508, + "gpu_gap_stddev_us": 0.7565157467411654 + } + } + } + }, + "composite_rgb_varshape_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "int64_axes": { + "outChannels": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Composite RGB8 varshape (advanced)." + }, + "baselines": { + "composite_rgb_varshape_1080p_advanced[InOutDataType=uchar3][shape=64x1080x1920][layout=NHWC][inputKind=VarShape][outChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1320.1052503836002, + "gpu_time_us_python": 1336.04815844066, + "gpu_noise_us_cpp": 2.056, + "gpu_noise_us_python": 3.4880000000000004, + "gpu_bwutil_cpp": 0.6464138899533693, + "gpu_bwutil_python": 0.6387017942075752, + "gpu_gap_stddev_us": 2.1961204864634625 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1188.182131690775, + "gpu_time_us_python": 1203.4095978832252, + "gpu_noise_us_cpp": 6.415, + "gpu_noise_us_python": 4.3325, + "gpu_bwutil_cpp": 0.5477922914677599, + "gpu_bwutil_python": 0.540859151101867, + "gpu_gap_stddev_us": 4.846068179346253 + } + } + } + }, + "composite_rgba_tensor_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "outChannels": [ + 4 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Composite RGBA8 output tensor (advanced)." + }, + "baselines": { + "composite_rgba_tensor_1080p_advanced[InOutDataType=uchar3][shape=64x1080x1920][layout=NHWC][inputKind=Tensor][outChannels=4]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1212.19540137596, + "gpu_time_us_python": 1221.92571628378, + "gpu_noise_us_cpp": 1.738, + "gpu_noise_us_python": 3.65, + "gpu_bwutil_cpp": 0.7743526371911236, + "gpu_bwutil_python": 0.7681866940389794, + "gpu_gap_stddev_us": 0.6688807508455491 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 983.2135788268499, + "gpu_time_us_python": 992.132723547825, + "gpu_noise_us_cpp": 1.3325, + "gpu_noise_us_python": 3.0549999999999997, + "gpu_bwutil_cpp": 0.728158104559926, + "gpu_bwutil_python": 0.7216120261077603, + "gpu_gap_stddev_us": 0.885075507688095 + } + } + } + }, + "composite_rgba_varshape_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "int64_axes": { + "outChannels": [ + 4 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Composite RGBA8 output varshape (advanced)." + }, + "baselines": { + "composite_rgba_varshape_1080p_advanced[InOutDataType=uchar3][shape=64x1080x1920][layout=NHWC][inputKind=VarShape][outChannels=4]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1533.1729976668, + "gpu_time_us_python": 1550.26212164924, + "gpu_noise_us_cpp": 1.7420000000000002, + "gpu_noise_us_python": 5.712, + "gpu_bwutil_cpp": 0.6122394790082455, + "gpu_bwutil_python": 0.6054917198983877, + "gpu_gap_stddev_us": 1.3830805712667877 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1335.7947788938, + "gpu_time_us_python": 1353.51241615055, + "gpu_noise_us_cpp": 5.515000000000001, + "gpu_noise_us_python": 5.9225, + "gpu_bwutil_cpp": 0.5359927856099498, + "gpu_bwutil_python": 0.5289768569052979, + "gpu_gap_stddev_us": 2.1183360035823466 + } + } + } + }, + "composite_basic_planar_tensor": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "128x720x1280" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "outChannels": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Composite RGB8 planar tensor (basic)." + }, + "baselines": { + "composite_basic_planar_tensor[InOutDataType=uchar3][shape=128x720x1280][layout=NCHW][inputKind=Tensor][outChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1046.26752644814, + "gpu_time_us_python": 1055.74672131508, + "gpu_noise_us_cpp": 2.4899999999999998, + "gpu_noise_us_python": 3.022, + "gpu_bwutil_cpp": 0.7249760115525015, + "gpu_bwutil_python": 0.7184671188070968, + "gpu_gap_stddev_us": 0.6470306201230458 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 902.2379600947249, + "gpu_time_us_python": 913.644322634775, + "gpu_noise_us_cpp": 4.909999999999999, + "gpu_noise_us_python": 4.5225, + "gpu_bwutil_cpp": 0.6412646856359565, + "gpu_bwutil_python": 0.6332621816638896, + "gpu_gap_stddev_us": 1.2557869610000343 + } + } + } + }, + "composite_fakeplanar_tensor_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "128x720x1280" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "outChannels": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Composite RGB8 fake-planar tensor (advanced)." + }, + "baselines": { + "composite_fakeplanar_tensor_advanced[InOutDataType=uchar3][shape=128x720x1280][layout=NCHW_FAKE][inputKind=Tensor][outChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4726.64489926028, + "gpu_time_us_python": 4750.56403358024, + "gpu_noise_us_cpp": 3.104, + "gpu_noise_us_python": 4.572, + "gpu_bwutil_cpp": 0.16047721259655418, + "gpu_bwutil_python": 0.15966920884192065, + "gpu_gap_stddev_us": 2.6014198220797256 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 3684.35146300735, + "gpu_time_us_python": 3703.13082109475, + "gpu_noise_us_cpp": 1.7275, + "gpu_noise_us_python": 4.2125, + "gpu_bwutil_cpp": 0.15702514626807632, + "gpu_bwutil_python": 0.156228547506215, + "gpu_gap_stddev_us": 2.18229769270872 + } + } + } + }, + "composite_basic_planar_varshape": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "128x720x1280" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "VarShape" + ] + }, + "int64_axes": { + "outChannels": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Composite RGB8 planar varshape (basic)." + }, + "baselines": { + "composite_basic_planar_varshape[InOutDataType=uchar3][shape=128x720x1280][layout=NCHW][inputKind=VarShape][outChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2066.5695196372803, + "gpu_time_us_python": 2087.05086695912, + "gpu_noise_us_cpp": 3.682000000000001, + "gpu_noise_us_python": 5.712000000000001, + "gpu_bwutil_cpp": 0.3670426709060835, + "gpu_bwutil_python": 0.3634412904304513, + "gpu_gap_stddev_us": 2.2623656319881262 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1729.826945234675, + "gpu_time_us_python": 1749.6595106118998, + "gpu_noise_us_cpp": 7.0675, + "gpu_noise_us_python": 12.0075, + "gpu_bwutil_cpp": 0.33445689900168535, + "gpu_bwutil_python": 0.33066652571449034, + "gpu_gap_stddev_us": 8.315919794779083 + } + } + } + }, + "composite_rgb_tensor_1080p_planar_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "layout": [ + "NCHW", + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "outChannels": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Composite RGB8 planar tensor (advanced)." + }, + "baselines": { + "composite_rgb_tensor_1080p_planar_advanced[InOutDataType=uchar3][shape=64x1080x1920][layout=NCHW][inputKind=Tensor][outChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1173.5859515159998, + "gpu_time_us_python": 1185.46927083124, + "gpu_noise_us_cpp": 2.316, + "gpu_noise_us_python": 2.002, + "gpu_bwutil_cpp": 0.7271163878976727, + "gpu_bwutil_python": 0.719827873708087, + "gpu_gap_stddev_us": 0.6074918106414172 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1016.41956494225, + "gpu_time_us_python": 1029.815755193525, + "gpu_noise_us_cpp": 4.305000000000001, + "gpu_noise_us_python": 3.5075, + "gpu_bwutil_cpp": 0.6403924445006488, + "gpu_bwutil_python": 0.6320389199848331, + "gpu_gap_stddev_us": 4.531807257408059 + } + }, + "composite_rgb_tensor_1080p_planar_advanced[InOutDataType=uchar3][shape=64x1080x1920][layout=NCHW_FAKE][inputKind=Tensor][outChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5320.24026149548, + "gpu_time_us_python": 5347.43264583826, + "gpu_noise_us_cpp": 2.5160000000000005, + "gpu_noise_us_python": 5.228, + "gpu_bwutil_cpp": 0.16039379110535645, + "gpu_bwutil_python": 0.15957814444789525, + "gpu_gap_stddev_us": 2.523999557395783 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 4146.063724502125, + "gpu_time_us_python": 4165.702440927549, + "gpu_noise_us_cpp": 2.315, + "gpu_noise_us_python": 8.735, + "gpu_bwutil_cpp": 0.15698088700196172, + "gpu_bwutil_python": 0.1562400379157664, + "gpu_gap_stddev_us": 6.086387714390076 + } + } + } + }, + "composite_rgb_varshape_1080p_planar_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "VarShape" + ] + }, + "int64_axes": { + "outChannels": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Composite RGB8 planar varshape (advanced)." + }, + "baselines": { + "composite_rgb_varshape_1080p_planar_advanced[InOutDataType=uchar3][shape=64x1080x1920][layout=NCHW][inputKind=VarShape][outChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2256.75799853548, + "gpu_time_us_python": 2276.1516051834797, + "gpu_noise_us_cpp": 2.6580000000000004, + "gpu_noise_us_python": 3.718000000000001, + "gpu_bwutil_cpp": 0.37812431090103693, + "gpu_bwutil_python": 0.3749032559148263, + "gpu_gap_stddev_us": 2.7809197886988386 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1895.36575298685, + "gpu_time_us_python": 1908.494044039875, + "gpu_noise_us_cpp": 6.3975, + "gpu_noise_us_python": 3.5975, + "gpu_bwutil_cpp": 0.3434000650262978, + "gpu_bwutil_python": 0.3410354888678164, + "gpu_gap_stddev_us": 3.2233270863892978 + } + } + } + }, + "composite_rgba_tensor_1080p_planar_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "layout": [ + "NCHW", + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "outChannels": [ + 4 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Composite RGBA8 planar output tensor (advanced)." + }, + "baselines": { + "composite_rgba_tensor_1080p_planar_advanced[InOutDataType=uchar3][shape=64x1080x1920][layout=NCHW][inputKind=Tensor][outChannels=4]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1250.0886355430398, + "gpu_time_us_python": 1258.9900107838798, + "gpu_noise_us_cpp": 1.886, + "gpu_noise_us_python": 1.9460000000000002, + "gpu_bwutil_cpp": 0.7508802468218141, + "gpu_bwutil_python": 0.7455712569246729, + "gpu_gap_stddev_us": 0.853953510249139 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1062.4679529273249, + "gpu_time_us_python": 1073.755691925725, + "gpu_noise_us_cpp": 4.08, + "gpu_noise_us_python": 4.9475, + "gpu_bwutil_cpp": 0.6738901693227219, + "gpu_bwutil_python": 0.6668011327254924, + "gpu_gap_stddev_us": 1.6951932123145388 + } + }, + "composite_rgba_tensor_1080p_planar_advanced[InOutDataType=uchar3][shape=64x1080x1920][layout=NCHW_FAKE][inputKind=Tensor][outChannels=4]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5542.70720274546, + "gpu_time_us_python": 5567.30600992832, + "gpu_noise_us_cpp": 4.816, + "gpu_noise_us_python": 7.87, + "gpu_bwutil_cpp": 0.16935169757776353, + "gpu_bwutil_python": 0.16860346042824279, + "gpu_gap_stddev_us": 5.355731741954236 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 4342.66000369495, + "gpu_time_us_python": 4364.45929264195, + "gpu_noise_us_cpp": 4.005, + "gpu_noise_us_python": 9.0725, + "gpu_bwutil_cpp": 0.1648616544968056, + "gpu_bwutil_python": 0.1640373719314308, + "gpu_gap_stddev_us": 5.916673791395283 + } + } + } + }, + "composite_rgba_varshape_1080p_planar_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "VarShape" + ] + }, + "int64_axes": { + "outChannels": [ + 4 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Composite RGBA8 planar output varshape (advanced)." + }, + "baselines": { + "composite_rgba_varshape_1080p_planar_advanced[InOutDataType=uchar3][shape=64x1080x1920][layout=NCHW][inputKind=VarShape][outChannels=4]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2368.03859719686, + "gpu_time_us_python": 2386.18935455632, + "gpu_noise_us_cpp": 2.0460000000000003, + "gpu_noise_us_python": 3.9619999999999997, + "gpu_bwutil_cpp": 0.39639061902397177, + "gpu_bwutil_python": 0.393375570735395, + "gpu_gap_stddev_us": 1.1184504187037803 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1981.2442306297003, + "gpu_time_us_python": 2000.856003028975, + "gpu_noise_us_cpp": 8.115, + "gpu_noise_us_python": 8.3625, + "gpu_bwutil_cpp": 0.3613692207655494, + "gpu_bwutil_python": 0.35781961208614943, + "gpu_gap_stddev_us": 6.821813218025668 + } + } + } + } + } +} diff --git a/bench/config/operators/conv2d.json b/bench/config/operators/conv2d.json new file mode 100644 index 000000000..a3d07848e --- /dev/null +++ b/bench/config/operators/conv2d.json @@ -0,0 +1,558 @@ +{ + "benchmark": "conv2d", + "configs": { + "conv2d_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "8x1080x1920" + ], + "kernelSize": [ + "7x7" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Conv2D RGB8 (basic)." + }, + "baselines": { + "conv2d_uchar3_basic[InOutDataType=uchar3][shape=8x1080x1920][kernelSize=7x7][border=REPLICATE][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 631.16850403516, + "gpu_time_us_python": 643.07170517224, + "gpu_noise_us_cpp": 1.34, + "gpu_noise_us_python": 1.922, + "gpu_bwutil_cpp": 0.10140017265304353, + "gpu_bwutil_python": 0.09952346722165875 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 596.0484895914, + "gpu_time_us_python": 606.99363747728, + "gpu_noise_us_cpp": 3.428, + "gpu_noise_us_python": 4.082, + "gpu_bwutil_cpp": 0.08190319023651599, + "gpu_bwutil_python": 0.08042538559466167 + } + } + } + }, + "conv2d_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "8x1080x1920" + ], + "kernelSize": [ + "7x7" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Conv2D RGBf32 (basic)." + }, + "baselines": { + "conv2d_float3_basic[InOutDataType=float3][shape=8x1080x1920][kernelSize=7x7][border=REPLICATE][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 741.93471625144, + "gpu_time_us_python": 753.44030291954, + "gpu_noise_us_cpp": 1.246, + "gpu_noise_us_python": 3.7880000000000003, + "gpu_bwutil_cpp": 0.34504540074692996, + "gpu_bwutil_python": 0.3397774132044099 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 641.4025200051001, + "gpu_time_us_python": 651.7766564556802, + "gpu_noise_us_cpp": 1.0399999999999998, + "gpu_noise_us_python": 2.658, + "gpu_bwutil_cpp": 0.3044518944692405, + "gpu_bwutil_python": 0.299597763893809 + } + } + } + }, + "conv2d_planar_nchw_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "8x1080x1920" + ], + "kernelSize": [ + "7x7" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Conv2D planar RGB8 (basic)." + }, + "baselines": { + "conv2d_planar_nchw_uchar3_basic[InOutDataType=uchar3][shape=8x1080x1920][kernelSize=7x7][border=REPLICATE][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 765.05934630944, + "gpu_time_us_python": 776.66583213392, + "gpu_noise_us_cpp": 1.28, + "gpu_noise_us_python": 3.55, + "gpu_bwutil_cpp": 0.08365435399237833, + "gpu_bwutil_python": 0.08240488817969298 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 730.60253856876, + "gpu_time_us_python": 740.6293763644201, + "gpu_noise_us_cpp": 0.6499999999999999, + "gpu_noise_us_python": 1.8199999999999998, + "gpu_bwutil_cpp": 0.06682156090674597, + "gpu_bwutil_python": 0.06591468003817208 + } + } + } + }, + "conv2d_planar_nchw_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "8x1080x1920" + ], + "kernelSize": [ + "7x7" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Conv2D planar RGBf32 (basic)." + }, + "baselines": { + "conv2d_planar_nchw_float3_basic[InOutDataType=float3][shape=8x1080x1920][kernelSize=7x7][border=REPLICATE][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 973.8106149942599, + "gpu_time_us_python": 988.8940837917601, + "gpu_noise_us_cpp": 1.258, + "gpu_noise_us_python": 4.717999999999999, + "gpu_bwutil_cpp": 0.2628869099329224, + "gpu_bwutil_python": 0.2588762259370863 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 899.1715709735199, + "gpu_time_us_python": 910.87754044878, + "gpu_noise_us_cpp": 2.0300000000000002, + "gpu_noise_us_python": 4.01, + "gpu_bwutil_cpp": 0.21717786333970804, + "gpu_bwutil_python": 0.2143846844058645 + } + } + } + }, + "conv2d_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "8x1080x1920" + ], + "kernelSize": [ + "7x7" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Conv2D RGBA8 (advanced)." + }, + "baselines": { + "conv2d_uchar4_advanced[InOutDataType=uchar4][shape=8x1080x1920][kernelSize=7x7][border=REPLICATE][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 677.85514101532, + "gpu_time_us_python": 689.43710040572, + "gpu_noise_us_cpp": 1.084, + "gpu_noise_us_python": 3.0900000000000003, + "gpu_bwutil_cpp": 0.12588903881493796, + "gpu_bwutil_python": 0.12377490724044068 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 746.55237976474, + "gpu_time_us_python": 756.2415738037599, + "gpu_noise_us_cpp": 1.4540000000000002, + "gpu_noise_us_python": 2.522, + "gpu_bwutil_cpp": 0.08718813436138434, + "gpu_bwutil_python": 0.0860703155890034 + } + } + } + }, + "conv2d_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "8x1080x1920" + ], + "kernelSize": [ + "7x7" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Conv2D RGBAf32 (advanced)." + }, + "baselines": { + "conv2d_float4_advanced[InOutDataType=float4][shape=8x1080x1920][kernelSize=7x7][border=REPLICATE][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1374.1945259199802, + "gpu_time_us_python": 1386.25386683318, + "gpu_noise_us_cpp": 2.3819999999999997, + "gpu_noise_us_python": 3.8660000000000005, + "gpu_bwutil_cpp": 0.24838832927990406, + "gpu_bwutil_python": 0.24622734497354473 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1176.9512092897398, + "gpu_time_us_python": 1187.31945204246, + "gpu_noise_us_cpp": 1.348, + "gpu_noise_us_python": 2.536, + "gpu_bwutil_cpp": 0.221232397608813, + "gpu_bwutil_python": 0.21929327393765133 + } + } + } + }, + "conv2d_scalar_k7_replicate_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "kernelSize": [ + "7x7" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Conv2D single-channel k7 replicate (advanced)." + }, + "baselines": { + "conv2d_scalar_k7_replicate_1080p_advanced[InOutDataType=float32][shape=16x1080x1920][kernelSize=7x7][border=REPLICATE][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 651.3009571449601, + "gpu_time_us_python": 663.6549778385399, + "gpu_noise_us_cpp": 1.314, + "gpu_noise_us_python": 3.096, + "gpu_bwutil_cpp": 0.2620406809564053, + "gpu_bwutil_python": 0.2571639024430242 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 604.29584013112, + "gpu_time_us_python": 614.48163399156, + "gpu_noise_us_cpp": 2.248, + "gpu_noise_us_python": 4.208, + "gpu_bwutil_cpp": 0.21543246898516738, + "gpu_bwutil_python": 0.2118541726376258 + } + }, + "conv2d_scalar_k7_replicate_1080p_advanced[InOutDataType=uint8][shape=16x1080x1920][kernelSize=7x7][border=REPLICATE][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 507.059607718, + "gpu_time_us_python": 518.80347956774, + "gpu_noise_us_cpp": 1.0459999999999998, + "gpu_noise_us_python": 3.024, + "gpu_bwutil_cpp": 0.08414602612285463, + "gpu_bwutil_python": 0.08224122273983787 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 492.22052621768, + "gpu_time_us_python": 502.08996847272, + "gpu_noise_us_cpp": 0.704, + "gpu_noise_us_python": 2.5639999999999996, + "gpu_bwutil_cpp": 0.06612129137428277, + "gpu_bwutil_python": 0.06481840258414028 + } + } + } + }, + "conv2d_rgb_k7_replicate_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "kernelSize": [ + "7x7" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Conv2D RGB8 k7 replicate (advanced)." + }, + "baselines": { + "conv2d_rgb_k7_replicate_1080p_advanced[InOutDataType=uchar3][shape=16x1080x1920][kernelSize=7x7][border=REPLICATE][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1243.7166110546, + "gpu_time_us_python": 1256.52372349078, + "gpu_noise_us_cpp": 0.9799999999999999, + "gpu_noise_us_python": 3.54, + "gpu_bwutil_cpp": 0.10291830277556994, + "gpu_bwutil_python": 0.1018700705903521 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1179.94399349206, + "gpu_time_us_python": 1192.08330196638, + "gpu_noise_us_cpp": 1.6159999999999997, + "gpu_noise_us_python": 3.5520000000000005, + "gpu_bwutil_cpp": 0.08275256900112951, + "gpu_bwutil_python": 0.08190677338219966 + } + } + } + }, + "conv2d_scalar_k3_replicate_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "kernelSize": [ + "3x3" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Conv2D single-channel k3 replicate (advanced)." + }, + "baselines": { + "conv2d_scalar_k3_replicate_1080p_advanced[InOutDataType=float32][shape=64x1080x1920][kernelSize=3x3][border=REPLICATE][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1724.1977092281402, + "gpu_time_us_python": 1737.63412766984, + "gpu_noise_us_cpp": 2.154, + "gpu_noise_us_python": 3.6160000000000005, + "gpu_bwutil_cpp": 0.39593308875355837, + "gpu_bwutil_python": 0.39287151619341093 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1405.05428138646, + "gpu_time_us_python": 1416.9195599313202, + "gpu_noise_us_cpp": 2.048, + "gpu_noise_us_python": 5.714, + "gpu_bwutil_cpp": 0.37059816221420144, + "gpu_bwutil_python": 0.36748364371018294 + } + }, + "conv2d_scalar_k3_replicate_1080p_advanced[InOutDataType=uint8][shape=64x1080x1920][kernelSize=3x3][border=REPLICATE][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1183.757375091, + "gpu_time_us_python": 1195.52573958512, + "gpu_noise_us_cpp": 1.264, + "gpu_noise_us_python": 2.63, + "gpu_bwutil_cpp": 0.14417408137775797, + "gpu_bwutil_python": 0.14275486043972335 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 969.8243551823, + "gpu_time_us_python": 980.56336575722, + "gpu_noise_us_cpp": 0.9879999999999999, + "gpu_noise_us_python": 3.3519999999999994, + "gpu_bwutil_cpp": 0.13423759162460103, + "gpu_bwutil_python": 0.132763534634989 + } + } + } + }, + "conv2d_scalar_k7_reflect101_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "8x1080x1920" + ], + "kernelSize": [ + "7x7" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Conv2D single-channel k7 reflect101 (advanced)." + }, + "baselines": { + "conv2d_scalar_k7_reflect101_1080p_advanced[InOutDataType=float32][shape=8x1080x1920][kernelSize=7x7][border=REFLECT101][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 486.77722104578, + "gpu_time_us_python": 498.20319081046, + "gpu_noise_us_cpp": 0.8320000000000001, + "gpu_noise_us_python": 2.1279999999999997, + "gpu_bwutil_cpp": 0.17530351334922795, + "gpu_bwutil_python": 0.17128313230536496 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 455.20003943162, + "gpu_time_us_python": 465.7729692536001, + "gpu_noise_us_cpp": 2.088, + "gpu_noise_us_python": 1.9100000000000001, + "gpu_bwutil_cpp": 0.1429955078808518, + "gpu_bwutil_python": 0.13974454755983384 + } + }, + "conv2d_scalar_k7_reflect101_1080p_advanced[InOutDataType=uint8][shape=8x1080x1920][kernelSize=7x7][border=REFLECT101][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 481.17828926716004, + "gpu_time_us_python": 493.36971074368, + "gpu_noise_us_cpp": 1.498, + "gpu_noise_us_python": 2.896, + "gpu_bwutil_cpp": 0.044335984169899326, + "gpu_bwutil_python": 0.04324044808397237 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 453.87441081956, + "gpu_time_us_python": 464.4579928390999, + "gpu_noise_us_cpp": 2.354, + "gpu_noise_us_python": 4.522, + "gpu_bwutil_cpp": 0.03585334012718472, + "gpu_bwutil_python": 0.03503438084088798 + } + } + } + } + } +} diff --git a/bench/config/operators/convertto.json b/bench/config/operators/convertto.json new file mode 100644 index 000000000..fd54527df --- /dev/null +++ b/bench/config/operators/convertto.json @@ -0,0 +1,1943 @@ +{ + "benchmark": "convertto", + "configs": { + "convertto_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "outDataType": [ + "uint8" + ], + "scaleMode": [ + "affine" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ConvertTo RGB8 (basic)." + }, + "baselines": { + "convertto_uchar3_basic[InOutDataType=uchar3][shape=128x1080x1920][outDataType=uint8][scaleMode=affine][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1242.75987738424, + "gpu_time_us_python": 1250.3261504973198, + "gpu_noise_us_cpp": 3.878, + "gpu_noise_us_python": 3.6580000000000004, + "gpu_bwutil_cpp": 0.8239726642315229, + "gpu_bwutil_python": 0.8189866236507377, + "gpu_gap_stddev_us": 0.95901427327255 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1088.2614581216, + "gpu_time_us_python": 1097.561965403575, + "gpu_noise_us_cpp": 3.645, + "gpu_noise_us_python": 4.705, + "gpu_bwutil_cpp": 0.7177023713128057, + "gpu_bwutil_python": 0.7116429767297594, + "gpu_gap_stddev_us": 3.679635944357487 + } + } + } + }, + "convertto_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "outDataType": [ + "uint8" + ], + "scaleMode": [ + "affine" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ConvertTo RGBA8 (advanced)." + }, + "baselines": { + "convertto_uchar4_advanced[InOutDataType=uchar4][shape=64x1080x1920][outDataType=uint8][scaleMode=affine][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 769.14274517008, + "gpu_time_us_python": 778.3176899607399, + "gpu_noise_us_cpp": 1.064, + "gpu_noise_us_python": 1.6780000000000002, + "gpu_bwutil_cpp": 0.8875682449034816, + "gpu_bwutil_python": 0.877105953656511, + "gpu_gap_stddev_us": 0.676880427936316 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 594.67220528265, + "gpu_time_us_python": 603.26836326385, + "gpu_noise_us_cpp": 1.5899999999999999, + "gpu_noise_us_python": 2.79, + "gpu_bwutil_cpp": 0.8755731806599742, + "gpu_bwutil_python": 0.8630969472058674, + "gpu_gap_stddev_us": 1.3294271468092957 + } + } + } + }, + "convertto_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "outDataType": [ + "float32" + ], + "scaleMode": [ + "affine" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ConvertTo RGBf32 (basic)." + }, + "baselines": { + "convertto_float3_basic[InOutDataType=float3][shape=32x1080x1920][outDataType=float32][scaleMode=affine][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1147.66259652754, + "gpu_time_us_python": 1156.99367915058, + "gpu_noise_us_cpp": 0.9880000000000001, + "gpu_noise_us_python": 1.716, + "gpu_bwutil_cpp": 0.892248354882455, + "gpu_bwutil_python": 0.8850527497222929, + "gpu_gap_stddev_us": 1.0530885803790786 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 876.88262758735, + "gpu_time_us_python": 886.3208308795499, + "gpu_noise_us_cpp": 1.4525, + "gpu_noise_us_python": 2.7175, + "gpu_bwutil_cpp": 0.8906746942780438, + "gpu_bwutil_python": 0.8811901715036657, + "gpu_gap_stddev_us": 1.0114738692812697 + } + } + } + }, + "convertto_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "outDataType": [ + "float32" + ], + "scaleMode": [ + "affine" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ConvertTo RGBAf32 (advanced)." + }, + "baselines": { + "convertto_float4_advanced[InOutDataType=float4][shape=16x1080x1920][outDataType=float32][scaleMode=affine][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 765.17161677732, + "gpu_time_us_python": 774.1284237422799, + "gpu_noise_us_cpp": 1.934, + "gpu_noise_us_python": 2.09, + "gpu_bwutil_cpp": 0.8921746216319597, + "gpu_bwutil_python": 0.8818526944827465, + "gpu_gap_stddev_us": 0.7873079223037602 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 586.736297361875, + "gpu_time_us_python": 596.65232832425, + "gpu_noise_us_cpp": 1.4575, + "gpu_noise_us_python": 3.5075, + "gpu_bwutil_cpp": 0.8874144732140674, + "gpu_bwutil_python": 0.8726730608758188, + "gpu_gap_stddev_us": 1.7092857999728337 + } + } + } + }, + "convertto_scalar_u8_to_u8_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "256x1080x1920" + ], + "outDataType": [ + "uint8" + ], + "scaleMode": [ + "affine" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ConvertTo single-channel (advanced)." + }, + "baselines": { + "convertto_scalar_u8_to_u8_1080p_advanced[InOutDataType=uint8][shape=256x1080x1920][outDataType=uint8][scaleMode=affine][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 769.9503356493201, + "gpu_time_us_python": 779.33911721144, + "gpu_noise_us_cpp": 0.932, + "gpu_noise_us_python": 2.772, + "gpu_bwutil_cpp": 0.886637291869647, + "gpu_bwutil_python": 0.8759563208416493, + "gpu_gap_stddev_us": 0.6728958259825393 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 592.5438665558249, + "gpu_time_us_python": 601.3419981304, + "gpu_noise_us_cpp": 1.4625, + "gpu_noise_us_python": 2.4299999999999997, + "gpu_bwutil_cpp": 0.8787164987154152, + "gpu_bwutil_python": 0.8658615335805503, + "gpu_gap_stddev_us": 0.9125682111896739 + } + } + } + }, + "convertto_scalar_f32_to_f32_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "outDataType": [ + "float32" + ], + "scaleMode": [ + "affine" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ConvertTo single-channel (advanced)." + }, + "baselines": { + "convertto_scalar_f32_to_f32_1080p_advanced[InOutDataType=float32][shape=128x1080x1920][outDataType=float32][scaleMode=affine][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1525.2742547814, + "gpu_time_us_python": 1535.4057098458402, + "gpu_noise_us_cpp": 1.27, + "gpu_noise_us_python": 2.182, + "gpu_bwutil_cpp": 0.8951395772362452, + "gpu_bwutil_python": 0.889233159144989, + "gpu_gap_stddev_us": 0.935254128573038 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1174.3553596290249, + "gpu_time_us_python": 1183.6063159341252, + "gpu_noise_us_cpp": 1.7525, + "gpu_noise_us_python": 3.21, + "gpu_bwutil_cpp": 0.8867475737917073, + "gpu_bwutil_python": 0.8798178471602074, + "gpu_gap_stddev_us": 0.715953259977053 + } + } + } + }, + "convertto_scalar_u8_to_f32_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "outDataType": [ + "float32" + ], + "scaleMode": [ + "affine" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ConvertTo single-channel (advanced)." + }, + "baselines": { + "convertto_scalar_u8_to_f32_1080p_advanced[InOutDataType=uint8][shape=128x1080x1920][outDataType=float32][scaleMode=affine][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1134.02396068308, + "gpu_time_us_python": 1143.99223240538, + "gpu_noise_us_cpp": 1.6039999999999999, + "gpu_noise_us_python": 2.4579999999999997, + "gpu_bwutil_cpp": 0.7524826788104113, + "gpu_bwutil_python": 0.7459261820378741, + "gpu_gap_stddev_us": 1.1073879752524907 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 795.47492109, + "gpu_time_us_python": 805.00060295725, + "gpu_noise_us_cpp": 2.2350000000000003, + "gpu_noise_us_python": 3.3874999999999997, + "gpu_bwutil_cpp": 0.8181876563221466, + "gpu_bwutil_python": 0.8085089855818929, + "gpu_gap_stddev_us": 2.0651639035461273 + } + } + } + }, + "convertto_scalar_f32_to_u8_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "256x1080x1920" + ], + "outDataType": [ + "uint8" + ], + "scaleMode": [ + "affine" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ConvertTo single-channel (advanced)." + }, + "baselines": { + "convertto_scalar_f32_to_u8_1080p_advanced[InOutDataType=float32][shape=256x1080x1920][outDataType=uint8][scaleMode=affine][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1875.68217409188, + "gpu_time_us_python": 1885.852314655, + "gpu_noise_us_cpp": 1.3519999999999999, + "gpu_noise_us_python": 2.518, + "gpu_bwutil_cpp": 0.9098912032794161, + "gpu_bwutil_python": 0.9049845287446866, + "gpu_gap_stddev_us": 1.4040036347498932 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1410.8591461449, + "gpu_time_us_python": 1421.527154219375, + "gpu_noise_us_cpp": 2.1675000000000004, + "gpu_noise_us_python": 4.02, + "gpu_bwutil_cpp": 0.9226258028964621, + "gpu_bwutil_python": 0.9157035468563568, + "gpu_gap_stddev_us": 2.148034092276914 + } + } + } + }, + "convertto_rgb_u8_to_f32_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "outDataType": [ + "float32" + ], + "scaleMode": [ + "affine" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ConvertTo RGB8 (advanced)." + }, + "baselines": { + "convertto_rgb_u8_to_f32_1080p_advanced[InOutDataType=uchar3][shape=64x1080x1920][outDataType=float32][scaleMode=affine][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1657.5101359790199, + "gpu_time_us_python": 1668.3171844482, + "gpu_noise_us_cpp": 3.806, + "gpu_noise_us_python": 4.53, + "gpu_bwutil_cpp": 0.7722427482953974, + "gpu_bwutil_python": 0.76724091025751, + "gpu_gap_stddev_us": 2.0728361322470414 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1215.6988771505248, + "gpu_time_us_python": 1223.85441637715, + "gpu_noise_us_cpp": 2.5875, + "gpu_noise_us_python": 3.2475, + "gpu_bwutil_cpp": 0.8030544862350297, + "gpu_bwutil_python": 0.7977036151229164, + "gpu_gap_stddev_us": 0.5742011347656393 + } + } + } + }, + "convertto_rgb_f32_to_u8_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "outDataType": [ + "uint8" + ], + "scaleMode": [ + "affine" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ConvertTo RGBf32 (advanced)." + }, + "baselines": { + "convertto_rgb_f32_to_u8_1080p_advanced[InOutDataType=float3][shape=64x1080x1920][outDataType=uint8][scaleMode=affine][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1433.53910500658, + "gpu_time_us_python": 1443.31978038806, + "gpu_noise_us_cpp": 2.456, + "gpu_noise_us_python": 2.7380000000000004, + "gpu_bwutil_cpp": 0.8928950853680089, + "gpu_bwutil_python": 0.8868446647872996, + "gpu_gap_stddev_us": 0.8600078191717511 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1089.87123434955, + "gpu_time_us_python": 1100.765511990375, + "gpu_noise_us_cpp": 1.6824999999999999, + "gpu_noise_us_python": 2.8049999999999997, + "gpu_bwutil_cpp": 0.8957690916606503, + "gpu_bwutil_python": 0.8869165165248625, + "gpu_gap_stddev_us": 3.7993393708795122 + } + } + } + }, + "convertto_scalar_u8_to_u16_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "256x1080x1920" + ], + "outDataType": [ + "uint16" + ], + "scaleMode": [ + "affine" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ConvertTo single-channel (advanced)." + }, + "baselines": { + "convertto_scalar_u8_to_u16_1080p_advanced[InOutDataType=uint8][shape=256x1080x1920][outDataType=uint16][scaleMode=affine][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1249.9468851516, + "gpu_time_us_python": 1260.35579588198, + "gpu_noise_us_cpp": 5.878, + "gpu_noise_us_python": 5.656000000000001, + "gpu_bwutil_cpp": 0.819234839715298, + "gpu_bwutil_python": 0.8124707483225297, + "gpu_gap_stddev_us": 2.0142116920212234 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 932.906706793275, + "gpu_time_us_python": 942.045637324625, + "gpu_noise_us_cpp": 2.0075, + "gpu_noise_us_python": 3.2525, + "gpu_bwutil_cpp": 0.8371873120283014, + "gpu_bwutil_python": 0.8290666286173587, + "gpu_gap_stddev_us": 1.3678106698457322 + } + } + } + }, + "convertto_2ch_s16_to_f32_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "short2" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "outDataType": [ + "float32" + ], + "scaleMode": [ + "affine" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ConvertTo 2-channel (advanced)." + }, + "baselines": { + "convertto_2ch_s16_to_f32_1080p_advanced[InOutDataType=short2][shape=64x1080x1920][outDataType=float32][scaleMode=affine][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1219.2539195412, + "gpu_time_us_python": 1229.22366179666, + "gpu_noise_us_cpp": 2.7079999999999997, + "gpu_noise_us_python": 3.024, + "gpu_bwutil_cpp": 0.8398578767896845, + "gpu_bwutil_python": 0.8330463786396873, + "gpu_gap_stddev_us": 0.8084793921724858 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 932.563855256625, + "gpu_time_us_python": 941.5497450451501, + "gpu_noise_us_cpp": 2.3425, + "gpu_noise_us_python": 3.01, + "gpu_bwutil_cpp": 0.8374958329709774, + "gpu_bwutil_python": 0.8295033075083282, + "gpu_gap_stddev_us": 1.7466111756386218 + } + } + } + }, + "convertto_planar_nchw_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "outDataType": [ + "uint8" + ], + "scaleMode": [ + "affine" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ConvertTo RGB8 (basic) (planar NCHW)." + }, + "baselines": { + "convertto_planar_nchw_uchar3_basic[InOutDataType=uchar3][shape=128x1080x1920][outDataType=uint8][scaleMode=affine][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1148.2798158575, + "gpu_time_us_python": 1157.97985913472, + "gpu_noise_us_cpp": 1.23, + "gpu_noise_us_python": 3.816, + "gpu_bwutil_cpp": 0.8917687569695769, + "gpu_bwutil_python": 0.8842999776757219, + "gpu_gap_stddev_us": 1.8419552385689502 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 879.593521257275, + "gpu_time_us_python": 890.30998504915, + "gpu_noise_us_cpp": 1.555, + "gpu_noise_us_python": 3.55, + "gpu_bwutil_cpp": 0.8879306284746449, + "gpu_bwutil_python": 0.877245399002423, + "gpu_gap_stddev_us": 2.5994088837181093 + } + } + } + }, + "convertto_planar_nchw_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "outDataType": [ + "uint8" + ], + "scaleMode": [ + "affine" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ConvertTo RGB8 native-planar Tensor comparison (advanced)." + }, + "baselines": { + "convertto_planar_nchw_uchar3_advanced[InOutDataType=uchar3][shape=128x1080x1920][outDataType=uint8][scaleMode=affine][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1148.1313851994798, + "gpu_time_us_python": 1157.7554367852401, + "gpu_noise_us_cpp": 0.874, + "gpu_noise_us_python": 1.7259999999999998, + "gpu_bwutil_cpp": 0.8918840041692897, + "gpu_bwutil_python": 0.8844704480656869, + "gpu_gap_stddev_us": 0.9102117222414218 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 879.41493297875, + "gpu_time_us_python": 888.1756312986, + "gpu_noise_us_cpp": 1.2449999999999999, + "gpu_noise_us_python": 2.8075, + "gpu_bwutil_cpp": 0.8881104080316478, + "gpu_bwutil_python": 0.8793501058785504, + "gpu_gap_stddev_us": 0.2125277830065748 + } + } + } + }, + "convertto_planar_nchw_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "outDataType": [ + "uint8" + ], + "scaleMode": [ + "affine" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ConvertTo RGBA8 (advanced) (planar NCHW)." + }, + "baselines": { + "convertto_planar_nchw_uchar4_advanced[InOutDataType=uchar4][shape=64x1080x1920][outDataType=uint8][scaleMode=affine][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 770.2244492677199, + "gpu_time_us_python": 779.3421982912199, + "gpu_noise_us_cpp": 1.34, + "gpu_noise_us_python": 1.8279999999999998, + "gpu_bwutil_cpp": 0.8863217409588848, + "gpu_bwutil_python": 0.8759531963629609, + "gpu_gap_stddev_us": 0.8949300495096615 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 592.5965054160749, + "gpu_time_us_python": 601.622594345725, + "gpu_noise_us_cpp": 1.5275, + "gpu_noise_us_python": 3.1374999999999997, + "gpu_bwutil_cpp": 0.8786387714671987, + "gpu_bwutil_python": 0.8654565053859331, + "gpu_gap_stddev_us": 0.1752912451911296 + } + } + } + }, + "convertto_planar_nchw_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "outDataType": [ + "float32" + ], + "scaleMode": [ + "affine" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ConvertTo RGBf32 (basic) (planar NCHW)." + }, + "baselines": { + "convertto_planar_nchw_float3_basic[InOutDataType=float3][shape=32x1080x1920][outDataType=float32][scaleMode=affine][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1148.27417813304, + "gpu_time_us_python": 1158.4749022015199, + "gpu_noise_us_cpp": 1.55, + "gpu_noise_us_python": 2.286, + "gpu_bwutil_cpp": 0.891773088272647, + "gpu_bwutil_python": 0.8839213592005969, + "gpu_gap_stddev_us": 0.9989621403851179 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 884.504564553975, + "gpu_time_us_python": 894.953260026, + "gpu_noise_us_cpp": 2.2525, + "gpu_noise_us_python": 3.0825, + "gpu_bwutil_cpp": 0.8830000317167727, + "gpu_bwutil_python": 0.8726967708200088, + "gpu_gap_stddev_us": 2.477042110060139 + } + } + } + }, + "convertto_planar_nchw_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "outDataType": [ + "float32" + ], + "scaleMode": [ + "affine" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ConvertTo RGBf32 native-planar Tensor comparison (advanced)." + }, + "baselines": { + "convertto_planar_nchw_float3_advanced[InOutDataType=float3][shape=32x1080x1920][outDataType=float32][scaleMode=affine][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1148.1511107277802, + "gpu_time_us_python": 1158.3383582072602, + "gpu_noise_us_cpp": 1.352, + "gpu_noise_us_python": 2.5140000000000002, + "gpu_bwutil_cpp": 0.891868676554844, + "gpu_bwutil_python": 0.8840265414605961, + "gpu_gap_stddev_us": 1.8243764471624202 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 884.4980629614751, + "gpu_time_us_python": 894.75959031925, + "gpu_noise_us_cpp": 1.6175, + "gpu_noise_us_python": 2.58, + "gpu_bwutil_cpp": 0.8830065084355837, + "gpu_bwutil_python": 0.8728862519971294, + "gpu_gap_stddev_us": 2.2721580595214372 + } + } + } + }, + "convertto_planar_nchw_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "outDataType": [ + "float32" + ], + "scaleMode": [ + "affine" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ConvertTo RGBAf32 (advanced) (planar NCHW)." + }, + "baselines": { + "convertto_planar_nchw_float4_advanced[InOutDataType=float4][shape=16x1080x1920][outDataType=float32][scaleMode=affine][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 770.66302475558, + "gpu_time_us_python": 780.00025229286, + "gpu_noise_us_cpp": 1.5739999999999998, + "gpu_noise_us_python": 2.334, + "gpu_bwutil_cpp": 0.8858174063852479, + "gpu_bwutil_python": 0.8752139684062143, + "gpu_gap_stddev_us": 0.7526442136227686 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 593.2110330151249, + "gpu_time_us_python": 602.440320079075, + "gpu_noise_us_cpp": 1.5925, + "gpu_noise_us_python": 3.0225, + "gpu_bwutil_cpp": 0.8777286151716845, + "gpu_bwutil_python": 0.8642885173173831, + "gpu_gap_stddev_us": 1.5651781793938544 + } + } + } + }, + "convertto_planar_nchw_scalar_u8_to_u8_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "256x1080x1920" + ], + "outDataType": [ + "uint8" + ], + "scaleMode": [ + "affine" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ConvertTo single-channel (advanced) (planar NCHW)." + }, + "baselines": { + "convertto_planar_nchw_scalar_u8_to_u8_1080p_advanced[InOutDataType=uint8][shape=256x1080x1920][outDataType=uint8][scaleMode=affine][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 770.26667162084, + "gpu_time_us_python": 779.66913155672, + "gpu_noise_us_cpp": 1.7079999999999997, + "gpu_noise_us_python": 1.9260000000000002, + "gpu_bwutil_cpp": 0.8862731936860623, + "gpu_bwutil_python": 0.8755856233739528, + "gpu_gap_stddev_us": 0.5700925441455302 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 592.64032371405, + "gpu_time_us_python": 602.08703549865, + "gpu_noise_us_cpp": 1.24, + "gpu_noise_us_python": 2.365, + "gpu_bwutil_cpp": 0.8785740697722135, + "gpu_bwutil_python": 0.8648025560626569, + "gpu_gap_stddev_us": 2.1909618755314835 + } + } + } + }, + "convertto_planar_nchw_scalar_f32_to_f32_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "outDataType": [ + "float32" + ], + "scaleMode": [ + "affine" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ConvertTo single-channel (advanced) (planar NCHW)." + }, + "baselines": { + "convertto_planar_nchw_scalar_f32_to_f32_1080p_advanced[InOutDataType=float32][shape=128x1080x1920][outDataType=float32][scaleMode=affine][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1525.38414466666, + "gpu_time_us_python": 1535.4383775792598, + "gpu_noise_us_cpp": 1.274, + "gpu_noise_us_python": 2.2060000000000004, + "gpu_bwutil_cpp": 0.895075091692718, + "gpu_bwutil_python": 0.8892144652819856, + "gpu_gap_stddev_us": 1.262548243600135 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1174.8661414278001, + "gpu_time_us_python": 1183.869482944825, + "gpu_noise_us_cpp": 2.085, + "gpu_noise_us_python": 2.7725, + "gpu_bwutil_cpp": 0.8863618792460864, + "gpu_bwutil_python": 0.8796210339049326, + "gpu_gap_stddev_us": 0.440014125915189 + } + } + } + }, + "convertto_planar_nchw_scalar_u8_to_f32_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "outDataType": [ + "float32" + ], + "scaleMode": [ + "affine" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ConvertTo single-channel (advanced) (planar NCHW)." + }, + "baselines": { + "convertto_planar_nchw_scalar_u8_to_f32_1080p_advanced[InOutDataType=uint8][shape=128x1080x1920][outDataType=float32][scaleMode=affine][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1134.1722309319, + "gpu_time_us_python": 1143.6901752262397, + "gpu_noise_us_cpp": 2.8259999999999996, + "gpu_noise_us_python": 2.268, + "gpu_bwutil_cpp": 0.7523842945767047, + "gpu_bwutil_python": 0.7461231167467, + "gpu_gap_stddev_us": 0.9079901612315914 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 795.6953270967999, + "gpu_time_us_python": 804.165126032525, + "gpu_noise_us_cpp": 2.0749999999999997, + "gpu_noise_us_python": 5.115, + "gpu_bwutil_cpp": 0.8179611733482749, + "gpu_bwutil_python": 0.8093465785782931, + "gpu_gap_stddev_us": 0.4363746818452633 + } + } + } + }, + "convertto_planar_nchw_scalar_f32_to_u8_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "256x1080x1920" + ], + "outDataType": [ + "uint8" + ], + "scaleMode": [ + "affine" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ConvertTo single-channel (advanced) (planar NCHW)." + }, + "baselines": { + "convertto_planar_nchw_scalar_f32_to_u8_1080p_advanced[InOutDataType=float32][shape=256x1080x1920][outDataType=uint8][scaleMode=affine][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1875.8334827066803, + "gpu_time_us_python": 1886.9717829209003, + "gpu_noise_us_cpp": 1.326, + "gpu_noise_us_python": 3.5739999999999994, + "gpu_bwutil_cpp": 0.9098178165527658, + "gpu_bwutil_python": 0.9044495480401056, + "gpu_gap_stddev_us": 3.0168705153817816 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1411.0905608434, + "gpu_time_us_python": 1420.8445126122251, + "gpu_noise_us_cpp": 3.74, + "gpu_noise_us_python": 2.7925, + "gpu_bwutil_cpp": 0.9224745034484166, + "gpu_bwutil_python": 0.9161418411269973, + "gpu_gap_stddev_us": 0.5668810140009212 + } + } + } + }, + "convertto_planar_nchw_rgb_u8_to_f32_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "outDataType": [ + "float32" + ], + "scaleMode": [ + "affine" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ConvertTo RGB8 (advanced) (planar NCHW)." + }, + "baselines": { + "convertto_planar_nchw_rgb_u8_to_f32_1080p_advanced[InOutDataType=uchar3][shape=64x1080x1920][outDataType=float32][scaleMode=affine][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1697.04062745374, + "gpu_time_us_python": 1707.39670626968, + "gpu_noise_us_cpp": 1.754, + "gpu_noise_us_python": 2.644, + "gpu_bwutil_cpp": 0.7542542191974515, + "gpu_bwutil_python": 0.7496797395077119, + "gpu_gap_stddev_us": 1.7148401298575366 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1189.272036625825, + "gpu_time_us_python": 1198.928497339475, + "gpu_noise_us_cpp": 2.5125, + "gpu_noise_us_python": 3.755, + "gpu_bwutil_cpp": 0.8208984391690797, + "gpu_bwutil_python": 0.8142880176078423, + "gpu_gap_stddev_us": 1.466692745664964 + } + } + } + }, + "convertto_planar_nchw_rgb_f32_to_u8_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "outDataType": [ + "uint8" + ], + "scaleMode": [ + "affine" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ConvertTo RGBf32 (advanced) (planar NCHW)." + }, + "baselines": { + "convertto_planar_nchw_rgb_f32_to_u8_1080p_advanced[InOutDataType=float3][shape=64x1080x1920][outDataType=uint8][scaleMode=affine][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1412.27411457688, + "gpu_time_us_python": 1422.21147147086, + "gpu_noise_us_cpp": 1.1260000000000001, + "gpu_noise_us_python": 2.174, + "gpu_bwutil_cpp": 0.9063396468469627, + "gpu_bwutil_python": 0.9000069788591716, + "gpu_gap_stddev_us": 0.810582497185521 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1063.240152612975, + "gpu_time_us_python": 1074.3356781159, + "gpu_noise_us_cpp": 1.5350000000000001, + "gpu_noise_us_python": 3.5075, + "gpu_bwutil_cpp": 0.918204568563182, + "gpu_bwutil_python": 0.9087294230243704, + "gpu_gap_stddev_us": 2.899208484900267 + } + } + } + }, + "convertto_planar_nchw_scalar_u8_to_u16_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "256x1080x1920" + ], + "outDataType": [ + "uint16" + ], + "scaleMode": [ + "affine" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ConvertTo single-channel (advanced) (planar NCHW)." + }, + "baselines": { + "convertto_planar_nchw_scalar_u8_to_u16_1080p_advanced[InOutDataType=uint8][shape=256x1080x1920][outDataType=uint16][scaleMode=affine][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1250.1983716452, + "gpu_time_us_python": 1259.8400530503, + "gpu_noise_us_cpp": 5.6259999999999994, + "gpu_noise_us_python": 5.793999999999999, + "gpu_bwutil_cpp": 0.8190700960033892, + "gpu_bwutil_python": 0.8128017472871363, + "gpu_gap_stddev_us": 0.8626348334125606 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 932.71904962415, + "gpu_time_us_python": 942.7278436124749, + "gpu_noise_us_cpp": 1.9224999999999999, + "gpu_noise_us_python": 3.38, + "gpu_bwutil_cpp": 0.8373561195268981, + "gpu_bwutil_python": 0.8284701131201931, + "gpu_gap_stddev_us": 1.5378461392638407 + } + } + } + }, + "convertto_planar_nchw_2ch_s16_to_f32_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "short2" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "outDataType": [ + "float32" + ], + "scaleMode": [ + "affine" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ConvertTo 2-channel (advanced) (planar NCHW)." + }, + "baselines": { + "convertto_planar_nchw_2ch_s16_to_f32_1080p_advanced[InOutDataType=short2][shape=64x1080x1920][outDataType=float32][scaleMode=affine][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1251.8430671691399, + "gpu_time_us_python": 1261.8432576411, + "gpu_noise_us_cpp": 2.214, + "gpu_noise_us_python": 2.872, + "gpu_bwutil_cpp": 0.8179939437986874, + "gpu_bwutil_python": 0.8115114589192134, + "gpu_gap_stddev_us": 0.5453131364719275 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 922.8695093260999, + "gpu_time_us_python": 932.3646372792, + "gpu_noise_us_cpp": 2.6725, + "gpu_noise_us_python": 3.3499999999999996, + "gpu_bwutil_cpp": 0.8462927035063298, + "gpu_bwutil_python": 0.8376758442242809, + "gpu_gap_stddev_us": 1.1458870161470949 + } + } + } + }, + "convertto_fakeplanar_nchw_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "outDataType": [ + "uint8" + ], + "scaleMode": [ + "affine" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ConvertTo RGB8 (basic) (fake planar NCHW_FAKE)." + }, + "baselines": { + "convertto_fakeplanar_nchw_uchar3_advanced[InOutDataType=uchar3][shape=128x1080x1920][outDataType=uint8][scaleMode=affine][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5950.30319904564, + "gpu_time_us_python": 5968.603449776021, + "gpu_noise_us_cpp": 2.464, + "gpu_noise_us_python": 3.7060000000000004, + "gpu_bwutil_cpp": 0.5162763587724435, + "gpu_bwutil_python": 0.5146933674989066, + "gpu_gap_stddev_us": 1.111488039251845 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 4753.5184846733755, + "gpu_time_us_python": 4768.338421605626, + "gpu_noise_us_cpp": 4.205, + "gpu_noise_us_python": 4.4399999999999995, + "gpu_bwutil_cpp": 0.4929141024095832, + "gpu_bwutil_python": 0.4913832675437949, + "gpu_gap_stddev_us": 5.223685619559693 + } + } + } + }, + "convertto_fakeplanar_nchw_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "outDataType": [ + "uint8" + ], + "scaleMode": [ + "affine" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ConvertTo RGBA8 (advanced) (fake planar NCHW_FAKE)." + }, + "baselines": { + "convertto_fakeplanar_nchw_uchar4_advanced[InOutDataType=uchar4][shape=64x1080x1920][outDataType=uint8][scaleMode=affine][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3825.0496387481203, + "gpu_time_us_python": 3843.4753345720005, + "gpu_noise_us_cpp": 1.968, + "gpu_noise_us_python": 2.274, + "gpu_bwutil_cpp": 0.5354180060086833, + "gpu_bwutil_python": 0.5328511213030918, + "gpu_gap_stddev_us": 1.615399628419675 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2993.7741891258747, + "gpu_time_us_python": 3008.5572487772, + "gpu_noise_us_cpp": 1.4775, + "gpu_noise_us_python": 3.9325, + "gpu_bwutil_cpp": 0.5217671384193149, + "gpu_bwutil_python": 0.5192041516330177, + "gpu_gap_stddev_us": 2.8888098391622337 + } + } + } + }, + "convertto_fakeplanar_nchw_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "outDataType": [ + "float32" + ], + "scaleMode": [ + "affine" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ConvertTo RGBf32 (basic) (fake planar NCHW_FAKE)." + }, + "baselines": { + "convertto_fakeplanar_nchw_float3_advanced[InOutDataType=float3][shape=32x1080x1920][outDataType=float32][scaleMode=affine][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3826.0767495993, + "gpu_time_us_python": 3843.04090702168, + "gpu_noise_us_cpp": 1.484, + "gpu_noise_us_python": 2.13, + "gpu_bwutil_cpp": 0.8029112438123709, + "gpu_bwutil_python": 0.7993670523451433, + "gpu_gap_stddev_us": 1.3900071115933996 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 3074.50760864625, + "gpu_time_us_python": 3095.1757026306996, + "gpu_noise_us_cpp": 2.9299999999999997, + "gpu_noise_us_python": 4.49, + "gpu_bwutil_cpp": 0.762091208665547, + "gpu_bwutil_python": 0.7570015473204944, + "gpu_gap_stddev_us": 1.7565873711003235 + } + } + } + }, + "convertto_fakeplanar_nchw_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "outDataType": [ + "float32" + ], + "scaleMode": [ + "affine" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ConvertTo RGBAf32 (advanced) (fake planar NCHW_FAKE)." + }, + "baselines": { + "convertto_fakeplanar_nchw_float4_advanced[InOutDataType=float4][shape=16x1080x1920][outDataType=float32][scaleMode=affine][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2879.1866740960804, + "gpu_time_us_python": 2896.1945095281, + "gpu_noise_us_cpp": 3.2620000000000005, + "gpu_noise_us_python": 5.202, + "gpu_bwutil_cpp": 0.7113120615970646, + "gpu_bwutil_python": 0.7071349405164448, + "gpu_gap_stddev_us": 2.1344794154767337 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2341.935178943825, + "gpu_time_us_python": 2356.736821548925, + "gpu_noise_us_cpp": 4.077500000000001, + "gpu_noise_us_python": 5.64, + "gpu_bwutil_cpp": 0.6669853854167176, + "gpu_bwutil_python": 0.6627958136286298, + "gpu_gap_stddev_us": 1.8689656150195242 + } + } + } + }, + "convertto_fakeplanar_nchw_scalar_u8_to_u8_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "256x1080x1920" + ], + "outDataType": [ + "uint8" + ], + "scaleMode": [ + "affine" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ConvertTo single-channel (advanced) (fake planar NCHW_FAKE)." + }, + "baselines": { + "convertto_fakeplanar_nchw_scalar_u8_to_u8_1080p_advanced[InOutDataType=uint8][shape=256x1080x1920][outDataType=uint8][scaleMode=affine][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2298.3558676658004, + "gpu_time_us_python": 2315.16438589746, + "gpu_noise_us_cpp": 2.3880000000000003, + "gpu_noise_us_python": 3.348, + "gpu_bwutil_cpp": 0.8910718103105054, + "gpu_bwutil_python": 0.8846026155210721, + "gpu_gap_stddev_us": 1.1484191911693142 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1749.5037785541751, + "gpu_time_us_python": 1762.326868496825, + "gpu_noise_us_cpp": 2.2675, + "gpu_noise_us_python": 15.2525, + "gpu_bwutil_cpp": 0.8928443092835249, + "gpu_bwutil_python": 0.8863477857015515, + "gpu_gap_stddev_us": 2.1327102729196326 + } + } + } + }, + "convertto_fakeplanar_nchw_scalar_f32_to_f32_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "outDataType": [ + "float32" + ], + "scaleMode": [ + "affine" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ConvertTo single-channel (advanced) (fake planar NCHW_FAKE)." + }, + "baselines": { + "convertto_fakeplanar_nchw_scalar_f32_to_f32_1080p_advanced[InOutDataType=float32][shape=128x1080x1920][outDataType=float32][scaleMode=affine][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4967.094756107659, + "gpu_time_us_python": 4985.98443760586, + "gpu_noise_us_cpp": 2.61, + "gpu_noise_us_python": 4.808000000000001, + "gpu_bwutil_cpp": 0.8246270768333451, + "gpu_bwutil_python": 0.8215030330664967, + "gpu_gap_stddev_us": 1.1332021259993519 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 4380.55505113605, + "gpu_time_us_python": 4394.42717726125, + "gpu_noise_us_cpp": 3.51, + "gpu_noise_us_python": 3.6874999999999996, + "gpu_bwutil_cpp": 0.7131698511517843, + "gpu_bwutil_python": 0.7109193484742046, + "gpu_gap_stddev_us": 2.1732097153101546 + } + } + } + }, + "convertto_fakeplanar_nchw_scalar_u8_to_f32_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "outDataType": [ + "float32" + ], + "scaleMode": [ + "affine" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ConvertTo single-channel (advanced) (fake planar NCHW_FAKE)." + }, + "baselines": { + "convertto_fakeplanar_nchw_scalar_u8_to_f32_1080p_advanced[InOutDataType=uint8][shape=128x1080x1920][outDataType=float32][scaleMode=affine][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3239.0118281046007, + "gpu_time_us_python": 3255.2614143916, + "gpu_noise_us_cpp": 3.3939999999999997, + "gpu_noise_us_python": 2.5539999999999994, + "gpu_bwutil_cpp": 0.7903645848639982, + "gpu_bwutil_python": 0.7864192238070301, + "gpu_gap_stddev_us": 1.0613699651148711 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2692.8471469668502, + "gpu_time_us_python": 2709.511660119475, + "gpu_noise_us_cpp": 3.2275, + "gpu_noise_us_python": 4.7225, + "gpu_bwutil_cpp": 0.7250862648461216, + "gpu_bwutil_python": 0.7206261118173483, + "gpu_gap_stddev_us": 1.9688412172951433 + } + } + } + }, + "convertto_fakeplanar_nchw_scalar_f32_to_u8_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "256x1080x1920" + ], + "outDataType": [ + "uint8" + ], + "scaleMode": [ + "affine" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ConvertTo single-channel (advanced) (fake planar NCHW_FAKE)." + }, + "baselines": { + "convertto_fakeplanar_nchw_scalar_f32_to_u8_1080p_advanced[InOutDataType=float32][shape=256x1080x1920][outDataType=uint8][scaleMode=affine][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6069.1797960371605, + "gpu_time_us_python": 6088.957561765341, + "gpu_noise_us_cpp": 2.918, + "gpu_noise_us_python": 4.3759999999999994, + "gpu_bwutil_cpp": 0.8436066674573514, + "gpu_bwutil_python": 0.8408664468879833, + "gpu_gap_stddev_us": 2.1749988898006496 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 5186.648960016199, + "gpu_time_us_python": 5201.739473357025, + "gpu_noise_us_cpp": 1.9324999999999999, + "gpu_noise_us_python": 4.2175, + "gpu_bwutil_cpp": 0.7529135862122615, + "gpu_bwutil_python": 0.7507284402672514, + "gpu_gap_stddev_us": 2.1855261105874275 + } + } + } + }, + "convertto_fakeplanar_nchw_rgb_u8_to_f32_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "outDataType": [ + "float32" + ], + "scaleMode": [ + "affine" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ConvertTo RGB8 (advanced) (fake planar NCHW_FAKE)." + }, + "baselines": { + "convertto_fakeplanar_nchw_rgb_u8_to_f32_1080p_advanced[InOutDataType=uchar3][shape=64x1080x1920][outDataType=float32][scaleMode=affine][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5547.58674994754, + "gpu_time_us_python": 5567.01472388368, + "gpu_noise_us_cpp": 4.218, + "gpu_noise_us_python": 4.938000000000001, + "gpu_bwutil_cpp": 0.6921928899135277, + "gpu_bwutil_python": 0.689777271347876, + "gpu_gap_stddev_us": 0.8624923748840886 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 4189.8520012696, + "gpu_time_us_python": 4208.294806877725, + "gpu_noise_us_cpp": 3.1175, + "gpu_noise_us_python": 7.075, + "gpu_bwutil_cpp": 0.6990260606672513, + "gpu_bwutil_python": 0.6959622838171665, + "gpu_gap_stddev_us": 2.1150203218802788 + } + } + } + }, + "convertto_fakeplanar_nchw_rgb_f32_to_u8_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "outDataType": [ + "uint8" + ], + "scaleMode": [ + "affine" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ConvertTo RGBf32 (advanced) (fake planar NCHW_FAKE)." + }, + "baselines": { + "convertto_fakeplanar_nchw_rgb_f32_to_u8_1080p_advanced[InOutDataType=float3][shape=64x1080x1920][outDataType=uint8][scaleMode=affine][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5256.85765941932, + "gpu_time_us_python": 5275.302392244281, + "gpu_noise_us_cpp": 1.86, + "gpu_noise_us_python": 3.064, + "gpu_bwutil_cpp": 0.7304744517885167, + "gpu_bwutil_python": 0.7279203599225224, + "gpu_gap_stddev_us": 1.4757469371604828 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 4330.576278012324, + "gpu_time_us_python": 4345.811593121451, + "gpu_noise_us_cpp": 2.3875, + "gpu_noise_us_python": 3.91, + "gpu_bwutil_cpp": 0.6763130889538305, + "gpu_bwutil_python": 0.6739419489069707, + "gpu_gap_stddev_us": 1.7212405430938407 + } + } + } + }, + "convertto_fakeplanar_nchw_scalar_u8_to_u16_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "256x1080x1920" + ], + "outDataType": [ + "uint16" + ], + "scaleMode": [ + "affine" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ConvertTo single-channel (advanced) (fake planar NCHW_FAKE)." + }, + "baselines": { + "convertto_fakeplanar_nchw_scalar_u8_to_u16_1080p_advanced[InOutDataType=uint8][shape=256x1080x1920][outDataType=uint16][scaleMode=affine][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5169.89593311228, + "gpu_time_us_python": 5188.17335245556, + "gpu_noise_us_cpp": 4.428, + "gpu_noise_us_python": 4.1, + "gpu_bwutil_cpp": 0.5942095483000716, + "gpu_bwutil_python": 0.5921161047685809, + "gpu_gap_stddev_us": 1.0830154577794402 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 4431.376417477875, + "gpu_time_us_python": 4447.05037694225, + "gpu_noise_us_cpp": 3.7249999999999996, + "gpu_noise_us_python": 3.8625, + "gpu_bwutil_cpp": 0.528744769647679, + "gpu_bwutil_python": 0.5268810143779982, + "gpu_gap_stddev_us": 1.4660255551901749 + } + } + } + }, + "convertto_fakeplanar_nchw_2ch_s16_to_f32_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "short2" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "outDataType": [ + "float32" + ], + "scaleMode": [ + "affine" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ConvertTo 2-channel (advanced) (fake planar NCHW_FAKE)." + }, + "baselines": { + "convertto_fakeplanar_nchw_2ch_s16_to_f32_1080p_advanced[InOutDataType=short2][shape=64x1080x1920][outDataType=float32][scaleMode=affine][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4144.27233524006, + "gpu_time_us_python": 4160.98879673436, + "gpu_noise_us_cpp": 3.9159999999999995, + "gpu_noise_us_python": 3.5100000000000002, + "gpu_bwutil_cpp": 0.7412641503472508, + "gpu_bwutil_python": 0.7382861686694181, + "gpu_gap_stddev_us": 1.3194758712337633 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 3323.980951779725, + "gpu_time_us_python": 3337.708269755, + "gpu_noise_us_cpp": 3.9299999999999997, + "gpu_noise_us_python": 3.9425, + "gpu_bwutil_cpp": 0.7048936828365227, + "gpu_bwutil_python": 0.7019942137570292, + "gpu_gap_stddev_us": 1.8473435286600826 + } + } + } + } + } +} diff --git a/bench/config/operators/copymakeborder.json b/bench/config/operators/copymakeborder.json new file mode 100644 index 000000000..2fd72f89d --- /dev/null +++ b/bench/config/operators/copymakeborder.json @@ -0,0 +1,1432 @@ +{ + "benchmark": "copymakeborder", + "configs": { + "copymakeborder_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CopyMakeBorder RGB8 (basic)." + }, + "baselines": { + "copymakeborder_uchar3_basic[InOutDataType=uchar3][shape=64x1080x1920][border=REFLECT101][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2586.71056019886, + "gpu_time_us_python": 2599.2757477557, + "gpu_noise_us_cpp": 3.44, + "gpu_noise_us_python": 2.24, + "gpu_bwutil_cpp": 0.3216461962484908, + "gpu_bwutil_python": 0.32009077994432406, + "gpu_gap_stddev_us": 4.422677829071358 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2569.8587597829496, + "gpu_time_us_python": 2582.9810536641, + "gpu_noise_us_cpp": 1.8425, + "gpu_noise_us_python": 4.87, + "gpu_bwutil_cpp": 0.24695848298682113, + "gpu_bwutil_python": 0.2457027418731267, + "gpu_gap_stddev_us": 0.8211160566524559 + } + }, + "copymakeborder_uchar3_basic[InOutDataType=uchar3][shape=64x1080x1920][border=REFLECT101][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2166.48214233328, + "gpu_time_us_python": 2208.39431285854, + "gpu_noise_us_cpp": 2.148, + "gpu_noise_us_python": 3.184, + "gpu_bwutil_cpp": 0.3840328056849082, + "gpu_bwutil_python": 0.37674465052803924, + "gpu_gap_stddev_us": 1.7121972761323403 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1669.599425349975, + "gpu_time_us_python": 1710.97134801155, + "gpu_noise_us_cpp": 3.1225, + "gpu_noise_us_python": 8.35, + "gpu_bwutil_cpp": 0.38033052133116874, + "gpu_bwutil_python": 0.3710857717196592, + "gpu_gap_stddev_us": 4.54034166946872 + } + } + } + }, + "copymakeborder_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CopyMakeBorder RGBf32 (basic)." + }, + "baselines": { + "copymakeborder_float3_basic[InOutDataType=float3][shape=16x1080x1920][border=REFLECT101][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1050.88130526176, + "gpu_time_us_python": 1060.6721882092402, + "gpu_noise_us_cpp": 1.998, + "gpu_noise_us_python": 2.6, + "gpu_bwutil_cpp": 0.791716471303205, + "gpu_bwutil_python": 0.7844083024560946, + "gpu_gap_stddev_us": 0.10662955054147136 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 851.1389871829501, + "gpu_time_us_python": 861.5526475609499, + "gpu_noise_us_cpp": 2.1799999999999997, + "gpu_noise_us_python": 3.7, + "gpu_bwutil_cpp": 0.7455635194568823, + "gpu_bwutil_python": 0.7365499230139432, + "gpu_gap_stddev_us": 1.6984993530400296 + } + } + } + }, + "copymakeborder_float3_varshape_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CopyMakeBorder RGBf32 VarShape (advanced)." + }, + "baselines": { + "copymakeborder_float3_varshape_advanced[InOutDataType=float3][shape=16x1080x1920][border=REFLECT101][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1051.7280451389402, + "gpu_time_us_python": 1066.2833660596, + "gpu_noise_us_cpp": 2.2340000000000004, + "gpu_noise_us_python": 4.57, + "gpu_bwutil_cpp": 0.7910792296233092, + "gpu_bwutil_python": 0.7802807807299986, + "gpu_gap_stddev_us": 0.5269571943176994 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 860.494245413075, + "gpu_time_us_python": 878.375356925225, + "gpu_noise_us_cpp": 2.2925, + "gpu_noise_us_python": 3.9000000000000004, + "gpu_bwutil_cpp": 0.7374569404924313, + "gpu_bwutil_python": 0.7224484045702705, + "gpu_gap_stddev_us": 2.9264237767481123 + } + } + } + }, + "copymakeborder_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CopyMakeBorder RGBA8 (advanced)." + }, + "baselines": { + "copymakeborder_uchar4_advanced[InOutDataType=uchar4][shape=32x1080x1920][border=REFLECT101][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1010.03232252208, + "gpu_time_us_python": 1020.0399499705, + "gpu_noise_us_cpp": 1.9420000000000002, + "gpu_noise_us_python": 2.462, + "gpu_bwutil_cpp": 0.5491574004957576, + "gpu_bwutil_python": 0.5437703400387821, + "gpu_gap_stddev_us": 1.3827464996811218 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 863.09716665435, + "gpu_time_us_python": 874.3805246743, + "gpu_noise_us_cpp": 2.4375000000000004, + "gpu_noise_us_python": 4.859999999999999, + "gpu_bwutil_cpp": 0.4901784464829226, + "gpu_bwutil_python": 0.48384512593775897, + "gpu_gap_stddev_us": 3.2713301615080494 + } + }, + "copymakeborder_uchar4_advanced[InOutDataType=uchar4][shape=32x1080x1920][border=REFLECT101][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1041.27060348558, + "gpu_time_us_python": 1044.9277756633, + "gpu_noise_us_cpp": 1.638, + "gpu_noise_us_python": 3.632, + "gpu_bwutil_cpp": 0.532682639098136, + "gpu_bwutil_python": 0.5308203390273608, + "gpu_gap_stddev_us": 1.9315102981470265 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 885.9824261661249, + "gpu_time_us_python": 889.2043883368, + "gpu_noise_us_cpp": 2.755, + "gpu_noise_us_python": 3.7425, + "gpu_bwutil_cpp": 0.4775427774840108, + "gpu_bwutil_python": 0.47579816525366747, + "gpu_gap_stddev_us": 2.316491646803343 + } + } + } + }, + "copymakeborder_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "8x1080x1920" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CopyMakeBorder RGBAf32 (advanced)." + }, + "baselines": { + "copymakeborder_float4_advanced[InOutDataType=float4][shape=8x1080x1920][border=REFLECT101][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 721.10021320468, + "gpu_time_us_python": 732.32832993184, + "gpu_noise_us_cpp": 2.718, + "gpu_noise_us_python": 4.304, + "gpu_bwutil_cpp": 0.7691951046572773, + "gpu_bwutil_python": 0.7574058423177087, + "gpu_gap_stddev_us": 1.7990799119254834 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 539.740162715925, + "gpu_time_us_python": 549.543897907625, + "gpu_noise_us_cpp": 1.34, + "gpu_noise_us_python": 3.0825, + "gpu_bwutil_cpp": 0.7838063865461768, + "gpu_bwutil_python": 0.7698344055054676, + "gpu_gap_stddev_us": 1.9546525731399658 + } + }, + "copymakeborder_float4_advanced[InOutDataType=float4][shape=8x1080x1920][border=REFLECT101][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 719.36941547636, + "gpu_time_us_python": 732.60602139868, + "gpu_noise_us_cpp": 3.194, + "gpu_noise_us_python": 3.4579999999999997, + "gpu_bwutil_cpp": 0.7710457170276387, + "gpu_bwutil_python": 0.7571153861773301, + "gpu_gap_stddev_us": 0.8134661649767627 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 544.116432175675, + "gpu_time_us_python": 557.0367363048, + "gpu_noise_us_cpp": 1.8775, + "gpu_noise_us_python": 3.9675, + "gpu_bwutil_cpp": 0.777501984051538, + "gpu_bwutil_python": 0.7594927712561761, + "gpu_gap_stddev_us": 3.098613201940825 + } + } + } + }, + "copymakeborder_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CopyMakeBorder single-channel (advanced)." + }, + "baselines": { + "copymakeborder_scalar_advanced[InOutDataType=float32][shape=32x1080x1920][border=REFLECT101][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1010.3304290963399, + "gpu_time_us_python": 1020.55067163406, + "gpu_noise_us_cpp": 1.6580000000000001, + "gpu_noise_us_python": 4.053999999999999, + "gpu_bwutil_cpp": 0.5489955485550658, + "gpu_bwutil_python": 0.543497527886867, + "gpu_gap_stddev_us": 0.46365594769584345 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 862.4433968119, + "gpu_time_us_python": 873.59090422605, + "gpu_noise_us_cpp": 1.7875, + "gpu_noise_us_python": 2.8325, + "gpu_bwutil_cpp": 0.49054720799396245, + "gpu_bwutil_python": 0.48428696277707395, + "gpu_gap_stddev_us": 0.8555200547900195 + } + }, + "copymakeborder_scalar_advanced[InOutDataType=float32][shape=32x1080x1920][border=REFLECT101][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 960.0991039329799, + "gpu_time_us_python": 938.40975126872, + "gpu_noise_us_cpp": 1.7100000000000002, + "gpu_noise_us_python": 3.218, + "gpu_bwutil_cpp": 0.5777199148984478, + "gpu_bwutil_python": 0.591073445466411, + "gpu_gap_stddev_us": 0.5433854451651549 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 866.88663158695, + "gpu_time_us_python": 875.5387138411251, + "gpu_noise_us_cpp": 2.0225, + "gpu_noise_us_python": 6.727499999999999, + "gpu_bwutil_cpp": 0.4880537167488339, + "gpu_bwutil_python": 0.4832277957928933, + "gpu_gap_stddev_us": 0.9400800768877736 + } + }, + "copymakeborder_scalar_advanced[InOutDataType=uint8][shape=32x1080x1920][border=REFLECT101][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 907.61794378786, + "gpu_time_us_python": 917.44173869976, + "gpu_noise_us_cpp": 2.3600000000000003, + "gpu_noise_us_python": 2.5660000000000003, + "gpu_bwutil_cpp": 0.15278091411966904, + "gpu_bwutil_python": 0.15114494924909735, + "gpu_gap_stddev_us": 0.2670908585618716 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 742.8183259979751, + "gpu_time_us_python": 753.475197095975, + "gpu_noise_us_cpp": 2.3000000000000003, + "gpu_noise_us_python": 3.4675000000000002, + "gpu_bwutil_cpp": 0.14240298525661219, + "gpu_bwutil_python": 0.14038826759288375, + "gpu_gap_stddev_us": 0.8712511193785153 + } + }, + "copymakeborder_scalar_advanced[InOutDataType=uint8][shape=32x1080x1920][border=REFLECT101][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 757.14232266204, + "gpu_time_us_python": 721.2435303128, + "gpu_noise_us_cpp": 0.788, + "gpu_noise_us_python": 3.6519999999999997, + "gpu_bwutil_cpp": 0.18314649133513797, + "gpu_bwutil_python": 0.19226257216587656, + "gpu_gap_stddev_us": 1.8642851983276223 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 709.20406176765, + "gpu_time_us_python": 676.4241571335, + "gpu_noise_us_cpp": 2.005, + "gpu_noise_us_python": 3.055, + "gpu_bwutil_cpp": 0.1491477502709122, + "gpu_bwutil_python": 0.15637422162445086, + "gpu_gap_stddev_us": 1.1962586374542494 + } + } + } + }, + "copymakeborder_scalar_u8_reflect101_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CopyMakeBorder single-channel reflect101 (advanced)." + }, + "baselines": { + "copymakeborder_scalar_u8_reflect101_1080p_advanced[InOutDataType=uint8][shape=64x1080x1920][border=REFLECT101][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1791.9327136447998, + "gpu_time_us_python": 1803.1093060541, + "gpu_noise_us_cpp": 2.974, + "gpu_noise_us_python": 3.5119999999999996, + "gpu_bwutil_cpp": 0.1547677478691792, + "gpu_bwutil_python": 0.15380844122634532, + "gpu_gap_stddev_us": 1.1602098411591841 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1466.23591937595, + "gpu_time_us_python": 1477.49389417995, + "gpu_noise_us_cpp": 2.5425000000000004, + "gpu_noise_us_python": 4.5725, + "gpu_bwutil_cpp": 0.1442862055247239, + "gpu_bwutil_python": 0.1431855753077452, + "gpu_gap_stddev_us": 1.4379387781860418 + } + }, + "copymakeborder_scalar_u8_reflect101_1080p_advanced[InOutDataType=uint8][shape=64x1080x1920][border=REFLECT101][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1502.69473593958, + "gpu_time_us_python": 1414.75539628582, + "gpu_noise_us_cpp": 0.866, + "gpu_noise_us_python": 3.9479999999999995, + "gpu_bwutil_cpp": 0.18455863983088583, + "gpu_bwutil_python": 0.1960303619790537, + "gpu_gap_stddev_us": 2.781350289461474 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1406.7944119518002, + "gpu_time_us_python": 1328.5873219434252, + "gpu_noise_us_cpp": 1.8599999999999999, + "gpu_noise_us_python": 4.915, + "gpu_bwutil_cpp": 0.1503802494770563, + "gpu_bwutil_python": 0.15923320582272293, + "gpu_gap_stddev_us": 4.002101848339686 + } + } + } + }, + "copymakeborder_scalar_f32_reflect101_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CopyMakeBorder single-channel reflect101 (advanced)." + }, + "baselines": { + "copymakeborder_scalar_f32_reflect101_1080p_advanced[InOutDataType=float32][shape=64x1080x1920][border=REFLECT101][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2007.06296157834, + "gpu_time_us_python": 2018.44608399172, + "gpu_noise_us_cpp": 1.54, + "gpu_noise_us_python": 3.086, + "gpu_bwutil_cpp": 0.552714889165728, + "gpu_bwutil_python": 0.5495982054016024, + "gpu_gap_stddev_us": 2.0477652830088613 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1712.526683723625, + "gpu_time_us_python": 1724.441004050425, + "gpu_noise_us_cpp": 1.9375, + "gpu_noise_us_python": 3.455, + "gpu_bwutil_cpp": 0.4940919257778717, + "gpu_bwutil_python": 0.4906756532483667, + "gpu_gap_stddev_us": 1.0126638654030509 + } + }, + "copymakeborder_scalar_f32_reflect101_1080p_advanced[InOutDataType=float32][shape=64x1080x1920][border=REFLECT101][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1909.10091895812, + "gpu_time_us_python": 1854.68411982964, + "gpu_noise_us_cpp": 1.8059999999999998, + "gpu_noise_us_python": 2.1799999999999997, + "gpu_bwutil_cpp": 0.5810779959971447, + "gpu_bwutil_python": 0.5981293781405833, + "gpu_gap_stddev_us": 2.7146978239543174 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1722.793194329375, + "gpu_time_us_python": 1722.9653818994002, + "gpu_noise_us_cpp": 1.5625, + "gpu_noise_us_python": 10.7575, + "gpu_bwutil_cpp": 0.4911636068466495, + "gpu_bwutil_python": 0.49112009129439005, + "gpu_gap_stddev_us": 7.813483700649654 + } + } + } + }, + "copymakeborder_scalar_u8_constant_tensor_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "CONSTANT" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CopyMakeBorder single-channel constant tensor (advanced)." + }, + "baselines": { + "copymakeborder_scalar_u8_constant_tensor_1080p_advanced[InOutDataType=uint8][shape=16x1080x1920][border=CONSTANT][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 532.4914111395999, + "gpu_time_us_python": 542.41181914866, + "gpu_noise_us_cpp": 2.69, + "gpu_noise_us_python": 3.066, + "gpu_bwutil_cpp": 0.13020558972720092, + "gpu_bwutil_python": 0.12782433140236366, + "gpu_gap_stddev_us": 0.6644183382922425 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 432.91924373695, + "gpu_time_us_python": 442.82514022149996, + "gpu_noise_us_cpp": 1.905, + "gpu_noise_us_python": 3.8225, + "gpu_bwutil_cpp": 0.12215763015424833, + "gpu_bwutil_python": 0.11942542220850845, + "gpu_gap_stddev_us": 0.21821613913098697 + } + } + } + }, + "copymakeborder_scalar_u8_constant_varshape_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "border": [ + "CONSTANT" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CopyMakeBorder single-channel constant varshape (advanced)." + }, + "baselines": { + "copymakeborder_scalar_u8_constant_varshape_1080p_advanced[InOutDataType=uint8][shape=64x1080x1920][border=CONSTANT][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1187.5270464048, + "gpu_time_us_python": 1195.18441773184, + "gpu_noise_us_cpp": 1.1079999999999999, + "gpu_noise_us_python": 3.8979999999999997, + "gpu_bwutil_cpp": 0.23353900681715567, + "gpu_bwutil_python": 0.23204381708058533, + "gpu_gap_stddev_us": 3.0848698679769324 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1125.9688430470749, + "gpu_time_us_python": 1140.202070712375, + "gpu_noise_us_cpp": 2.015, + "gpu_noise_us_python": 3.2075, + "gpu_bwutil_cpp": 0.1878714464850199, + "gpu_bwutil_python": 0.18552657692692318, + "gpu_gap_stddev_us": 0.4057439106364156 + } + } + } + }, + "copymakeborder_scalar_u8_replicate_tensor_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CopyMakeBorder single-channel replicate tensor (advanced)." + }, + "baselines": { + "copymakeborder_scalar_u8_replicate_tensor_1080p_advanced[InOutDataType=uint8][shape=128x1080x1920][border=REPLICATE][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2953.74613425306, + "gpu_time_us_python": 2965.4967251945595, + "gpu_noise_us_cpp": 1.3080000000000003, + "gpu_noise_us_python": 2.252, + "gpu_bwutil_cpp": 0.18778442561139613, + "gpu_bwutil_python": 0.18704028831454264, + "gpu_gap_stddev_us": 1.5898001859891346 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2761.5167439638253, + "gpu_time_us_python": 2778.26808025105, + "gpu_noise_us_cpp": 3.8475, + "gpu_noise_us_python": 3.355, + "gpu_bwutil_cpp": 0.15319661766367482, + "gpu_bwutil_python": 0.15227267103230574, + "gpu_gap_stddev_us": 6.216698357286553 + } + } + } + }, + "copymakeborder_scalar_u8_replicate_varshape_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CopyMakeBorder single-channel replicate varshape (advanced)." + }, + "baselines": { + "copymakeborder_scalar_u8_replicate_varshape_1080p_advanced[InOutDataType=uint8][shape=128x1080x1920][border=REPLICATE][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1667.3356801573398, + "gpu_time_us_python": 1731.11930945822, + "gpu_noise_us_cpp": 1.6780000000000002, + "gpu_noise_us_python": 3.8520000000000003, + "gpu_bwutil_cpp": 0.3326684842264311, + "gpu_bwutil_python": 0.32041118405377395, + "gpu_gap_stddev_us": 1.4584711418403544 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1509.5091648121252, + "gpu_time_us_python": 1570.9219524889, + "gpu_noise_us_cpp": 4.810000000000001, + "gpu_noise_us_python": 8.265, + "gpu_bwutil_cpp": 0.2802783223985167, + "gpu_bwutil_python": 0.26931739585952663, + "gpu_gap_stddev_us": 2.807877501738415 + } + } + } + }, + "copymakeborder_rgb_u8_reflect101_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CopyMakeBorder RGB8 reflect101 (advanced)." + }, + "baselines": { + "copymakeborder_rgb_u8_reflect101_1080p_advanced[InOutDataType=uchar3][shape=64x1080x1920][border=REFLECT101][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2588.79442952338, + "gpu_time_us_python": 2600.48742038786, + "gpu_noise_us_cpp": 0.9960000000000001, + "gpu_noise_us_python": 4.890000000000001, + "gpu_bwutil_cpp": 0.3213869823423641, + "gpu_bwutil_python": 0.31994131316916724, + "gpu_gap_stddev_us": 1.9135819569367236 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2569.668757380875, + "gpu_time_us_python": 2581.5089730486998, + "gpu_noise_us_cpp": 1.8599999999999999, + "gpu_noise_us_python": 3.645, + "gpu_bwutil_cpp": 0.24697619860875702, + "gpu_bwutil_python": 0.2458439880530917, + "gpu_gap_stddev_us": 1.3342577503252018 + } + }, + "copymakeborder_rgb_u8_reflect101_1080p_advanced[InOutDataType=uchar3][shape=64x1080x1920][border=REFLECT101][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2166.4503623698797, + "gpu_time_us_python": 2209.1243013389803, + "gpu_noise_us_cpp": 2.304, + "gpu_noise_us_python": 3.474, + "gpu_bwutil_cpp": 0.3840384277848764, + "gpu_bwutil_python": 0.37662028514581664, + "gpu_gap_stddev_us": 1.8539037312338962 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1670.65700870525, + "gpu_time_us_python": 1710.257416859275, + "gpu_noise_us_cpp": 1.87, + "gpu_noise_us_python": 5.33, + "gpu_bwutil_cpp": 0.38010489405537196, + "gpu_bwutil_python": 0.37127544713156324, + "gpu_gap_stddev_us": 2.256568605220177 + } + } + } + }, + "copymakeborder_planar_nchw_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CopyMakeBorder RGB8 (basic) planar NCHW." + }, + "baselines": { + "copymakeborder_planar_nchw_uchar3_basic[InOutDataType=uchar3][shape=64x1080x1920][border=REFLECT101][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5432.0415233043195, + "gpu_time_us_python": 5445.07996724996, + "gpu_noise_us_cpp": 4.808, + "gpu_noise_us_python": 7.531999999999999, + "gpu_bwutil_cpp": 0.15316527134133123, + "gpu_bwutil_python": 0.15279852195651636, + "gpu_gap_stddev_us": 1.3773923339182037 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 4386.78108935485, + "gpu_time_us_python": 4401.028450878124, + "gpu_noise_us_cpp": 4.6475, + "gpu_noise_us_python": 6.172499999999999, + "gpu_bwutil_cpp": 0.14468055601171498, + "gpu_bwutil_python": 0.14421258044368607, + "gpu_gap_stddev_us": 3.6161035870118856 + } + }, + "copymakeborder_planar_nchw_uchar3_basic[InOutDataType=uchar3][shape=64x1080x1920][border=REFLECT101][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4646.327970646, + "gpu_time_us_python": 4652.369515101081, + "gpu_noise_us_cpp": 6.68, + "gpu_noise_us_python": 5.8740000000000006, + "gpu_bwutil_cpp": 0.17906713745432534, + "gpu_bwutil_python": 0.17883470061625645, + "gpu_gap_stddev_us": 4.9494377935427325 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 4427.180703704525, + "gpu_time_us_python": 4444.95885110835, + "gpu_noise_us_cpp": 4.155, + "gpu_noise_us_python": 4.0375, + "gpu_bwutil_cpp": 0.14335620128648618, + "gpu_bwutil_python": 0.1427832444400377, + "gpu_gap_stddev_us": 5.150839592673027 + } + } + } + }, + "copymakeborder_planar_nchw_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CopyMakeBorder RGBf32 (basic) planar NCHW." + }, + "baselines": { + "copymakeborder_planar_nchw_float3_basic[InOutDataType=float3][shape=16x1080x1920][border=REFLECT101][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1528.6796156952598, + "gpu_time_us_python": 1539.65500688254, + "gpu_noise_us_cpp": 3.72, + "gpu_noise_us_python": 2.716, + "gpu_bwutil_cpp": 0.5442607439622711, + "gpu_bwutil_python": 0.5403812293896204, + "gpu_gap_stddev_us": 1.1162087980458628 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1302.00059986675, + "gpu_time_us_python": 1314.65892028705, + "gpu_noise_us_cpp": 2.6125000000000003, + "gpu_noise_us_python": 5.945, + "gpu_bwutil_cpp": 0.48741356420559306, + "gpu_bwutil_python": 0.4827151472925467, + "gpu_gap_stddev_us": 1.4819747587008234 + } + } + } + }, + "copymakeborder_planar_nchw_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CopyMakeBorder RGBf32 native-planar Tensor comparison (advanced)." + }, + "baselines": { + "copymakeborder_planar_nchw_float3_advanced[InOutDataType=float3][shape=16x1080x1920][border=REFLECT101][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1528.5622317615998, + "gpu_time_us_python": 1540.20155851088, + "gpu_noise_us_cpp": 1.6260000000000001, + "gpu_noise_us_python": 3.782, + "gpu_bwutil_cpp": 0.5443025259566365, + "gpu_bwutil_python": 0.5401893705870189, + "gpu_gap_stddev_us": 1.2199858131601768 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1301.746113834525, + "gpu_time_us_python": 1313.929111925675, + "gpu_noise_us_cpp": 1.8525, + "gpu_noise_us_python": 7.345, + "gpu_bwutil_cpp": 0.4875068144600118, + "gpu_bwutil_python": 0.4829827109518043, + "gpu_gap_stddev_us": 1.9441834855039035 + } + } + } + }, + "copymakeborder_planar_nchw_float3_varshape_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CopyMakeBorder RGBf32 VarShape (advanced) planar NCHW." + }, + "baselines": { + "copymakeborder_planar_nchw_float3_varshape_advanced[InOutDataType=float3][shape=16x1080x1920][border=REFLECT101][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1529.2877922697398, + "gpu_time_us_python": 1486.4638217706001, + "gpu_noise_us_cpp": 1.922, + "gpu_noise_us_python": 2.8899999999999997, + "gpu_bwutil_cpp": 0.5440461082164115, + "gpu_bwutil_python": 0.5597211302789716, + "gpu_gap_stddev_us": 1.9235911029792776 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1407.9275380149752, + "gpu_time_us_python": 1404.3867287458002, + "gpu_noise_us_cpp": 2.505, + "gpu_noise_us_python": 3.685, + "gpu_bwutil_cpp": 0.4507551237805434, + "gpu_bwutil_python": 0.4518943848592115, + "gpu_gap_stddev_us": 1.039139366987849 + } + } + } + }, + "copymakeborder_planar_nchw_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CopyMakeBorder RGBA8 (advanced) planar NCHW tensor-only; RGBA8p varshape is unsupported by the Python image API." + }, + "baselines": { + "copymakeborder_planar_nchw_uchar4_advanced[InOutDataType=uchar4][shape=32x1080x1920][border=REFLECT101][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3647.60971138438, + "gpu_time_us_python": 3659.47322223494, + "gpu_noise_us_cpp": 4.486, + "gpu_noise_us_python": 4.524, + "gpu_bwutil_cpp": 0.15206309550978073, + "gpu_bwutil_python": 0.15157013018531992, + "gpu_gap_stddev_us": 1.60156804498651 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2960.322142323875, + "gpu_time_us_python": 2973.118348309025, + "gpu_noise_us_cpp": 3.815, + "gpu_noise_us_python": 7.0075, + "gpu_bwutil_cpp": 0.14293189558040983, + "gpu_bwutil_python": 0.14231839721157358, + "gpu_gap_stddev_us": 2.5235827000565183 + } + } + } + }, + "copymakeborder_planar_nchw_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "8x1080x1920" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CopyMakeBorder RGBAf32 (advanced) planar NCHW." + }, + "baselines": { + "copymakeborder_planar_nchw_float4_advanced[InOutDataType=float4][shape=8x1080x1920][border=REFLECT101][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1036.7555159153599, + "gpu_time_us_python": 1046.97387422972, + "gpu_noise_us_cpp": 2.0460000000000003, + "gpu_noise_us_python": 3.3579999999999997, + "gpu_bwutil_cpp": 0.535002755025713, + "gpu_bwutil_python": 0.5297815456047599, + "gpu_gap_stddev_us": 0.8547851803882942 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 884.1547047955, + "gpu_time_us_python": 897.5497991952001, + "gpu_noise_us_cpp": 2.1174999999999997, + "gpu_noise_us_python": 6.18, + "gpu_bwutil_cpp": 0.4785097897163328, + "gpu_bwutil_python": 0.4713600052715669, + "gpu_gap_stddev_us": 1.972699769103837 + } + }, + "copymakeborder_planar_nchw_float4_advanced[InOutDataType=float4][shape=8x1080x1920][border=REFLECT101][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1032.41153410432, + "gpu_time_us_python": 1009.18242222422, + "gpu_noise_us_cpp": 2.49, + "gpu_noise_us_python": 2.9539999999999997, + "gpu_bwutil_cpp": 0.5372563317032469, + "gpu_bwutil_python": 0.5496217173654017, + "gpu_gap_stddev_us": 2.256279304056638 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 950.709626103075, + "gpu_time_us_python": 953.994944053175, + "gpu_noise_us_cpp": 1.5975, + "gpu_noise_us_python": 4.305, + "gpu_bwutil_cpp": 0.4450182799574887, + "gpu_bwutil_python": 0.4434911611028896, + "gpu_gap_stddev_us": 2.3142654018537256 + } + } + } + }, + "copymakeborder_planar_nchw_rgb_u8_reflect101_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CopyMakeBorder RGB8 reflect101 (advanced) planar NCHW." + }, + "baselines": { + "copymakeborder_planar_nchw_rgb_u8_reflect101_1080p_advanced[InOutDataType=uchar3][shape=64x1080x1920][border=REFLECT101][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5430.5770082676, + "gpu_time_us_python": 5443.78882698386, + "gpu_noise_us_cpp": 4.788, + "gpu_noise_us_python": 4.884, + "gpu_bwutil_cpp": 0.15320658155551986, + "gpu_bwutil_python": 0.1528347831315172, + "gpu_gap_stddev_us": 2.058442406102444 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 4388.112073981625, + "gpu_time_us_python": 4401.3015995657, + "gpu_noise_us_cpp": 4.904999999999999, + "gpu_noise_us_python": 7.8549999999999995, + "gpu_bwutil_cpp": 0.14463724548558277, + "gpu_bwutil_python": 0.14420306762365287, + "gpu_gap_stddev_us": 4.647442842679555 + } + }, + "copymakeborder_planar_nchw_rgb_u8_reflect101_1080p_advanced[InOutDataType=uchar3][shape=64x1080x1920][border=REFLECT101][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4646.22177371266, + "gpu_time_us_python": 4653.746221683579, + "gpu_noise_us_cpp": 4.752000000000001, + "gpu_noise_us_python": 6.33, + "gpu_bwutil_cpp": 0.17907122726744026, + "gpu_bwutil_python": 0.17878172502626738, + "gpu_gap_stddev_us": 1.7045276287425346 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 4425.796260851, + "gpu_time_us_python": 4444.187563701751, + "gpu_noise_us_cpp": 3.5174999999999996, + "gpu_noise_us_python": 4.432499999999999, + "gpu_bwutil_cpp": 0.1434005288475635, + "gpu_bwutil_python": 0.14280791340635646, + "gpu_gap_stddev_us": 1.9018975814050465 + } + } + } + }, + "copymakeborder_fake_planar_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CopyMakeBorder RGBf32 (advanced) fake-planar reformat-CopyMakeBorder-reformat baseline." + }, + "baselines": { + "copymakeborder_fake_planar_float3_advanced[InOutDataType=float3][shape=16x1080x1920][border=REFLECT101][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3109.11438023596, + "gpu_time_us_python": 3126.88636600964, + "gpu_noise_us_cpp": 2.2439999999999998, + "gpu_noise_us_python": 4.164, + "gpu_bwutil_cpp": 0.8028009696939767, + "gpu_bwutil_python": 0.7982382951311786, + "gpu_gap_stddev_us": 1.6440470170895884 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2510.26695966715, + "gpu_time_us_python": 2526.0336694512, + "gpu_noise_us_cpp": 2.205, + "gpu_noise_us_python": 3.78, + "gpu_bwutil_cpp": 0.7583790512066796, + "gpu_bwutil_python": 0.7536453625541424, + "gpu_gap_stddev_us": 1.3970723569719847 + } + } + } + }, + "copymakeborder_fake_planar_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CopyMakeBorder RGBA8 (advanced) fake-planar reformat-CopyMakeBorder-reformat baseline." + }, + "baselines": { + "copymakeborder_fake_planar_uchar4_advanced[InOutDataType=uchar4][shape=32x1080x1920][border=REFLECT101][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3131.3536012172, + "gpu_time_us_python": 3148.70782554144, + "gpu_noise_us_cpp": 2.38, + "gpu_noise_us_python": 4.614, + "gpu_bwutil_cpp": 0.5313997238297421, + "gpu_bwutil_python": 0.52847084181501, + "gpu_gap_stddev_us": 0.4741948862919384 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2566.9353242312, + "gpu_time_us_python": 2581.538631474325, + "gpu_noise_us_cpp": 1.6775, + "gpu_noise_us_python": 5.585, + "gpu_bwutil_cpp": 0.49443512961934943, + "gpu_bwutil_python": 0.491633441559778, + "gpu_gap_stddev_us": 3.8969495612075433 + } + } + } + }, + "copymakeborder_fake_planar_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "8x1080x1920" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CopyMakeBorder RGBAf32 (advanced) fake-planar reformat-CopyMakeBorder-reformat baseline." + }, + "baselines": { + "copymakeborder_fake_planar_float4_advanced[InOutDataType=float4][shape=8x1080x1920][border=REFLECT101][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2267.61942897826, + "gpu_time_us_python": 2282.8124865618, + "gpu_noise_us_cpp": 3.8880000000000003, + "gpu_noise_us_python": 4.459999999999999, + "gpu_bwutil_cpp": 0.7338092081361978, + "gpu_bwutil_python": 0.728925627390749, + "gpu_gap_stddev_us": 1.6189453189109702 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1788.9591463974248, + "gpu_time_us_python": 1804.5223604003, + "gpu_noise_us_cpp": 4.925, + "gpu_noise_us_python": 6.289999999999999, + "gpu_bwutil_cpp": 0.7094369817141686, + "gpu_bwutil_python": 0.703317833759439, + "gpu_gap_stddev_us": 2.8134274002272583 + } + } + } + }, + "copymakeborder_fake_planar_rgb_u8_reflect101_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CopyMakeBorder RGB8 reflect101 (advanced) fake-planar reformat-CopyMakeBorder-reformat baseline." + }, + "baselines": { + "copymakeborder_fake_planar_rgb_u8_reflect101_1080p_advanced[InOutDataType=uchar3][shape=64x1080x1920][border=REFLECT101][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5921.053766649781, + "gpu_time_us_python": 5937.409201333639, + "gpu_noise_us_cpp": 1.528, + "gpu_noise_us_python": 2.6119999999999997, + "gpu_bwutil_cpp": 0.4215474708974975, + "gpu_bwutil_python": 0.4203861869728713, + "gpu_gap_stddev_us": 1.2005538173317252 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 5236.8381850573, + "gpu_time_us_python": 5254.936662812976, + "gpu_noise_us_cpp": 3.1625, + "gpu_noise_us_python": 5.66, + "gpu_bwutil_cpp": 0.3635443071686293, + "gpu_bwutil_python": 0.36229162716719837, + "gpu_gap_stddev_us": 2.2549104556508723 + } + } + } + } + } +} diff --git a/bench/config/operators/cropflipnormalizereformat.json b/bench/config/operators/cropflipnormalizereformat.json new file mode 100644 index 000000000..52b2a9e83 --- /dev/null +++ b/bench/config/operators/cropflipnormalizereformat.json @@ -0,0 +1,1256 @@ +{ + "benchmark": "cropflipnormalizereformat", + "configs": { + "cropflipnormalizereformat_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "srcLayout": [ + "NHWC", + "NCHW" + ], + "border": [ + "REFLECT101" + ], + "cropMode": [ + "full" + ], + "flagsMode": [ + "normal" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CropFlipNormalizeReformat RGB8 (basic)." + }, + "baselines": { + "cropflipnormalizereformat_uchar3_basic[InOutDataType=uchar3][shape=32x1080x1920][layout=NHWC][srcLayout=NHWC][border=REFLECT101][cropMode=full][flagsMode=normal][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1916.44785054202, + "gpu_time_us_python": 1935.75194833098, + "gpu_noise_us_cpp": 5.7780000000000005, + "gpu_noise_us_python": 6.029999999999999, + "gpu_bwutil_cpp": 0.13358146108979702, + "gpu_bwutil_python": 0.13224972344864291 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1914.0090856920801, + "gpu_time_us_python": 1927.56847754286, + "gpu_noise_us_cpp": 1.44, + "gpu_noise_us_python": 3.28, + "gpu_bwutil_cpp": 0.10202726815412859, + "gpu_bwutil_python": 0.10130923895637405 + } + }, + "cropflipnormalizereformat_uchar3_basic[InOutDataType=uchar3][shape=32x1080x1920][layout=NCHW][srcLayout=NCHW][border=REFLECT101][cropMode=full][flagsMode=normal][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1925.3373880436397, + "gpu_time_us_python": 1938.4882813602599, + "gpu_noise_us_cpp": 1.3279999999999998, + "gpu_noise_us_python": 4.561999999999999, + "gpu_bwutil_cpp": 0.13296435850656194, + "gpu_bwutil_python": 0.132062292362215 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1864.4323554639002, + "gpu_time_us_python": 1877.99574450296, + "gpu_noise_us_cpp": 1.3900000000000001, + "gpu_noise_us_python": 3.6380000000000003, + "gpu_bwutil_cpp": 0.10473980394129109, + "gpu_bwutil_python": 0.10398322587699196 + } + }, + "cropflipnormalizereformat_uchar3_basic[InOutDataType=uchar3][shape=32x1080x1920][layout=NCHW][srcLayout=NHWC][border=REFLECT101][cropMode=full][flagsMode=normal][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1930.4999534899798, + "gpu_time_us_python": 1949.93124167672, + "gpu_noise_us_cpp": 2.724, + "gpu_noise_us_python": 5.686, + "gpu_bwutil_cpp": 0.1326088297735317, + "gpu_bwutil_python": 0.13129072338278078 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1914.60236140836, + "gpu_time_us_python": 1928.93736445686, + "gpu_noise_us_cpp": 0.8739999999999999, + "gpu_noise_us_python": 4.074, + "gpu_bwutil_cpp": 0.10199585458140745, + "gpu_bwutil_python": 0.10123612409050024 + } + }, + "cropflipnormalizereformat_uchar3_basic[InOutDataType=uchar3][shape=32x1080x1920][layout=NHWC][srcLayout=NCHW][border=REFLECT101][cropMode=full][flagsMode=normal][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1904.61650593474, + "gpu_time_us_python": 1921.3447079851198, + "gpu_noise_us_cpp": 1.18, + "gpu_noise_us_python": 4.0760000000000005, + "gpu_bwutil_cpp": 0.13441084736628225, + "gpu_bwutil_python": 0.13324091312190872 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1870.10426636366, + "gpu_time_us_python": 1884.9926451807398, + "gpu_noise_us_cpp": 0.8459999999999999, + "gpu_noise_us_python": 4.544, + "gpu_bwutil_cpp": 0.1044225032625653, + "gpu_bwutil_python": 0.10360001232190368 + } + } + } + }, + "cropflipnormalizereformat_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "srcLayout": [ + "NHWC", + "NCHW" + ], + "border": [ + "REFLECT101" + ], + "cropMode": [ + "full" + ], + "flagsMode": [ + "normal" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CropFlipNormalizeReformat RGBf32 (basic)." + }, + "baselines": { + "cropflipnormalizereformat_float3_basic[InOutDataType=float3][shape=32x1080x1920][layout=NHWC][srcLayout=NHWC][border=REFLECT101][cropMode=full][flagsMode=normal][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1975.6540150154, + "gpu_time_us_python": 1993.57317462262, + "gpu_noise_us_cpp": 1.378, + "gpu_noise_us_python": 6.556, + "gpu_bwutil_cpp": 0.5183103653805543, + "gpu_bwutil_python": 0.5136588124306883 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1977.1549413282598, + "gpu_time_us_python": 1991.8035363262, + "gpu_noise_us_cpp": 0.7859999999999999, + "gpu_noise_us_python": 3.3400000000000007, + "gpu_bwutil_cpp": 0.3950748172563342, + "gpu_bwutil_python": 0.39217409456942215 + } + }, + "cropflipnormalizereformat_float3_basic[InOutDataType=float3][shape=32x1080x1920][layout=NCHW][srcLayout=NCHW][border=REFLECT101][cropMode=full][flagsMode=normal][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2044.43687694822, + "gpu_time_us_python": 2060.06132735574, + "gpu_noise_us_cpp": 2.022, + "gpu_noise_us_python": 3.5080000000000005, + "gpu_bwutil_cpp": 0.500872168136392, + "gpu_bwutil_python": 0.4970748346072976 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1934.5537532860599, + "gpu_time_us_python": 1949.2611033337998, + "gpu_noise_us_cpp": 1.5240000000000002, + "gpu_noise_us_python": 5.082, + "gpu_bwutil_cpp": 0.40377078149110845, + "gpu_bwutil_python": 0.4007195182628819 + } + }, + "cropflipnormalizereformat_float3_basic[InOutDataType=float3][shape=32x1080x1920][layout=NCHW][srcLayout=NHWC][border=REFLECT101][cropMode=full][flagsMode=normal][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1990.4317097058004, + "gpu_time_us_python": 2006.3065343735202, + "gpu_noise_us_cpp": 1.514, + "gpu_noise_us_python": 3.132, + "gpu_bwutil_cpp": 0.514462164919933, + "gpu_bwutil_python": 0.5103935902854485 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2019.8850577433, + "gpu_time_us_python": 2033.5595025376601, + "gpu_noise_us_cpp": 1.2440000000000002, + "gpu_noise_us_python": 2.7699999999999996, + "gpu_bwutil_cpp": 0.3867168551601202, + "gpu_bwutil_python": 0.38411705703062443 + } + }, + "cropflipnormalizereformat_float3_basic[InOutDataType=float3][shape=32x1080x1920][layout=NHWC][srcLayout=NCHW][border=REFLECT101][cropMode=full][flagsMode=normal][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2068.7000217871596, + "gpu_time_us_python": 2081.60464476286, + "gpu_noise_us_cpp": 1.4859999999999998, + "gpu_noise_us_python": 5.58, + "gpu_bwutil_cpp": 0.4949975836319386, + "gpu_bwutil_python": 0.49192984802832224 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1944.4232191388, + "gpu_time_us_python": 1959.34545776004, + "gpu_noise_us_cpp": 1.052, + "gpu_noise_us_python": 2.548, + "gpu_bwutil_cpp": 0.40171946516174567, + "gpu_bwutil_python": 0.39866776351123134 + } + } + } + }, + "cropflipnormalizereformat_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "srcLayout": [ + "NHWC", + "NCHW" + ], + "border": [ + "REFLECT101" + ], + "cropMode": [ + "full" + ], + "flagsMode": [ + "normal" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CropFlipNormalizeReformat RGBA8 (advanced)." + }, + "baselines": { + "cropflipnormalizereformat_uchar4_advanced[InOutDataType=uchar4][shape=16x1080x1920][layout=NHWC][srcLayout=NHWC][border=REFLECT101][cropMode=full][flagsMode=normal][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1350.0209812923201, + "gpu_time_us_python": 1365.27476271286, + "gpu_noise_us_cpp": 2.256, + "gpu_noise_us_python": 5.093999999999999, + "gpu_bwutil_cpp": 0.12641944649180337, + "gpu_bwutil_python": 0.1250061873977156 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1242.9681772299, + "gpu_time_us_python": 1257.45018560502, + "gpu_noise_us_cpp": 1.4140000000000001, + "gpu_noise_us_python": 2.84, + "gpu_bwutil_cpp": 0.10473853398379598, + "gpu_bwutil_python": 0.10352856775769026 + } + }, + "cropflipnormalizereformat_uchar4_advanced[InOutDataType=uchar4][shape=16x1080x1920][layout=NCHW][srcLayout=NCHW][border=REFLECT101][cropMode=full][flagsMode=normal][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1259.2898952122, + "gpu_time_us_python": 1274.00719855318, + "gpu_noise_us_cpp": 1.4140000000000001, + "gpu_noise_us_python": 4.384, + "gpu_bwutil_cpp": 0.13552674987773516, + "gpu_bwutil_python": 0.13396181205458596 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1208.5339790329401, + "gpu_time_us_python": 1223.17753891132, + "gpu_noise_us_cpp": 0.8160000000000001, + "gpu_noise_us_python": 4.382, + "gpu_bwutil_cpp": 0.10772276244859971, + "gpu_bwutil_python": 0.10643538076188663 + } + }, + "cropflipnormalizereformat_uchar4_advanced[InOutDataType=uchar4][shape=16x1080x1920][layout=NCHW][srcLayout=NHWC][border=REFLECT101][cropMode=full][flagsMode=normal][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1267.29374046362, + "gpu_time_us_python": 1281.91975117654, + "gpu_noise_us_cpp": 1.8399999999999999, + "gpu_noise_us_python": 3.75, + "gpu_bwutil_cpp": 0.1346707496844671, + "gpu_bwutil_python": 0.13313504768205572 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1245.55829731932, + "gpu_time_us_python": 1260.34451392124, + "gpu_noise_us_cpp": 2.2340000000000004, + "gpu_noise_us_python": 4.148000000000001, + "gpu_bwutil_cpp": 0.10452092689877504, + "gpu_bwutil_python": 0.10329067102048055 + } + }, + "cropflipnormalizereformat_uchar4_advanced[InOutDataType=uchar4][shape=16x1080x1920][layout=NHWC][srcLayout=NCHW][border=REFLECT101][cropMode=full][flagsMode=normal][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1354.9963312346601, + "gpu_time_us_python": 1369.7177703531802, + "gpu_noise_us_cpp": 2.7619999999999996, + "gpu_noise_us_python": 4.142, + "gpu_bwutil_cpp": 0.12595553759329872, + "gpu_bwutil_python": 0.12460099891748791 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1212.33506645286, + "gpu_time_us_python": 1227.5625539218802, + "gpu_noise_us_cpp": 1.176, + "gpu_noise_us_python": 2.604, + "gpu_bwutil_cpp": 0.10738475931990914, + "gpu_bwutil_python": 0.10604740193700057 + } + } + } + }, + "cropflipnormalizereformat_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "srcLayout": [ + "NHWC", + "NCHW" + ], + "border": [ + "REFLECT101" + ], + "cropMode": [ + "full" + ], + "flagsMode": [ + "normal" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CropFlipNormalizeReformat RGBAf32 (advanced)." + }, + "baselines": { + "cropflipnormalizereformat_float4_advanced[InOutDataType=float4][shape=16x1080x1920][layout=NHWC][srcLayout=NHWC][border=REFLECT101][cropMode=full][flagsMode=normal][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1365.3339311879201, + "gpu_time_us_python": 1377.0457554214597, + "gpu_noise_us_cpp": 1.9080000000000001, + "gpu_noise_us_python": 3.492, + "gpu_bwutil_cpp": 0.5000072788691499, + "gpu_bwutil_python": 0.4957552494251628 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1271.22873659692, + "gpu_time_us_python": 1284.5461578210998, + "gpu_noise_us_cpp": 1.596, + "gpu_noise_us_python": 3.3520000000000003, + "gpu_bwutil_cpp": 0.40963639216008324, + "gpu_bwutil_python": 0.4054021614437341 + } + }, + "cropflipnormalizereformat_float4_advanced[InOutDataType=float4][shape=16x1080x1920][layout=NCHW][srcLayout=NCHW][border=REFLECT101][cropMode=full][flagsMode=normal][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1345.6663626736001, + "gpu_time_us_python": 1359.2369886185002, + "gpu_noise_us_cpp": 1.3059999999999998, + "gpu_noise_us_python": 3.0060000000000002, + "gpu_bwutil_cpp": 0.5073084108810619, + "gpu_bwutil_python": 0.5022444117727322 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1239.3862674692, + "gpu_time_us_python": 1253.1831487560999, + "gpu_noise_us_cpp": 1.3679999999999999, + "gpu_noise_us_python": 4.402, + "gpu_bwutil_cpp": 0.4201575881756344, + "gpu_bwutil_python": 0.41552737582295035 + } + }, + "cropflipnormalizereformat_float4_advanced[InOutDataType=float4][shape=16x1080x1920][layout=NCHW][srcLayout=NHWC][border=REFLECT101][cropMode=full][flagsMode=normal][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1288.61306984206, + "gpu_time_us_python": 1303.5435485478, + "gpu_noise_us_cpp": 1.904, + "gpu_noise_us_python": 3.5480000000000005, + "gpu_bwutil_cpp": 0.5297697217770725, + "gpu_bwutil_python": 0.523704701066845 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1296.29897189538, + "gpu_time_us_python": 1310.0247560091, + "gpu_noise_us_cpp": 1.2200000000000002, + "gpu_noise_us_python": 4.064, + "gpu_bwutil_cpp": 0.40171640000900927, + "gpu_bwutil_python": 0.3975127753211594 + } + }, + "cropflipnormalizereformat_float4_advanced[InOutDataType=float4][shape=16x1080x1920][layout=NHWC][srcLayout=NCHW][border=REFLECT101][cropMode=full][flagsMode=normal][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1491.8360042551399, + "gpu_time_us_python": 1504.4546664066, + "gpu_noise_us_cpp": 2.0300000000000002, + "gpu_noise_us_python": 4.444, + "gpu_bwutil_cpp": 0.457604417187388, + "gpu_bwutil_python": 0.45376630227269166 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1263.64398298456, + "gpu_time_us_python": 1279.3893117309, + "gpu_noise_us_cpp": 1.0740000000000003, + "gpu_noise_us_python": 3.21, + "gpu_bwutil_cpp": 0.41208465241512204, + "gpu_bwutil_python": 0.40702032368698526 + } + } + } + }, + "cropflipnormalizereformat_scalar_reflect101_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "srcLayout": [ + "NHWC", + "NCHW" + ], + "border": [ + "REFLECT101" + ], + "cropMode": [ + "full" + ], + "flagsMode": [ + "normal" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CropFlipNormalizeReformat single-channel reflect101 (advanced)." + }, + "baselines": { + "cropflipnormalizereformat_scalar_reflect101_1080p_advanced[InOutDataType=float32][shape=64x1080x1920][layout=NHWC][srcLayout=NHWC][border=REFLECT101][cropMode=full][flagsMode=normal][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1485.8897770876401, + "gpu_time_us_python": 1501.5779171717, + "gpu_noise_us_cpp": 1.23, + "gpu_noise_us_python": 3.3, + "gpu_bwutil_cpp": 0.45943831214319353, + "gpu_bwutil_python": 0.4546450641345773 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1589.7725783669998, + "gpu_time_us_python": 1603.5001185718997, + "gpu_noise_us_cpp": 0.7699999999999999, + "gpu_noise_us_python": 3.192, + "gpu_bwutil_cpp": 0.3275694793864763, + "gpu_bwutil_python": 0.32475802697447226 + } + }, + "cropflipnormalizereformat_scalar_reflect101_1080p_advanced[InOutDataType=uint8][shape=64x1080x1920][layout=NHWC][srcLayout=NHWC][border=REFLECT101][cropMode=full][flagsMode=normal][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1527.8603788754, + "gpu_time_us_python": 1543.70336776158, + "gpu_noise_us_cpp": 2.152, + "gpu_noise_us_python": 3.496, + "gpu_bwutil_cpp": 0.11170393822289756, + "gpu_bwutil_python": 0.11055918742715698 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1613.83955363454, + "gpu_time_us_python": 1632.3835301415, + "gpu_noise_us_cpp": 1.134, + "gpu_noise_us_python": 3.9480000000000004, + "gpu_bwutil_cpp": 0.08066988183743748, + "gpu_bwutil_python": 0.07975261841028183 + } + }, + "cropflipnormalizereformat_scalar_reflect101_1080p_advanced[InOutDataType=float32][shape=64x1080x1920][layout=NCHW][srcLayout=NCHW][border=REFLECT101][cropMode=full][flagsMode=normal][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1486.60601750072, + "gpu_time_us_python": 1501.6023184320202, + "gpu_noise_us_cpp": 1.324, + "gpu_noise_us_python": 3.25, + "gpu_bwutil_cpp": 0.45921718304307235, + "gpu_bwutil_python": 0.45463006125050365 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1578.1262833789401, + "gpu_time_us_python": 1593.30020708148, + "gpu_noise_us_cpp": 1.476, + "gpu_noise_us_python": 4.032, + "gpu_bwutil_cpp": 0.3299871544324221, + "gpu_bwutil_python": 0.326843617497135 + } + }, + "cropflipnormalizereformat_scalar_reflect101_1080p_advanced[InOutDataType=float32][shape=64x1080x1920][layout=NCHW][srcLayout=NHWC][border=REFLECT101][cropMode=full][flagsMode=normal][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1486.55183714234, + "gpu_time_us_python": 1503.53363716086, + "gpu_noise_us_cpp": 1.318, + "gpu_noise_us_python": 4.942, + "gpu_bwutil_cpp": 0.4592340455355961, + "gpu_bwutil_python": 0.454054507739737 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1577.8302117833402, + "gpu_time_us_python": 1592.5226559422001, + "gpu_noise_us_cpp": 0.646, + "gpu_noise_us_python": 3.6559999999999997, + "gpu_bwutil_cpp": 0.3300486042903145, + "gpu_bwutil_python": 0.3269946272466834 + } + }, + "cropflipnormalizereformat_scalar_reflect101_1080p_advanced[InOutDataType=float32][shape=64x1080x1920][layout=NHWC][srcLayout=NCHW][border=REFLECT101][cropMode=full][flagsMode=normal][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1486.3060091491202, + "gpu_time_us_python": 1502.6417437563398, + "gpu_noise_us_cpp": 1.8020000000000003, + "gpu_noise_us_python": 5.308000000000001, + "gpu_bwutil_cpp": 0.4593096645580125, + "gpu_bwutil_python": 0.45432060248275097 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1589.799677955, + "gpu_time_us_python": 1604.8757022996201, + "gpu_noise_us_cpp": 0.8879999999999999, + "gpu_noise_us_python": 3.842, + "gpu_bwutil_cpp": 0.3275630860034195, + "gpu_bwutil_python": 0.3244873388456567 + } + }, + "cropflipnormalizereformat_scalar_reflect101_1080p_advanced[InOutDataType=uint8][shape=64x1080x1920][layout=NCHW][srcLayout=NCHW][border=REFLECT101][cropMode=full][flagsMode=normal][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1515.7631655938399, + "gpu_time_us_python": 1532.29071621706, + "gpu_noise_us_cpp": 1.544, + "gpu_noise_us_python": 5.5200000000000005, + "gpu_bwutil_cpp": 0.11259541081220048, + "gpu_bwutil_python": 0.11138226696691249 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1595.59149583252, + "gpu_time_us_python": 1615.0228384012003, + "gpu_noise_us_cpp": 1.048, + "gpu_noise_us_python": 5.24, + "gpu_bwutil_cpp": 0.08159202803166751, + "gpu_bwutil_python": 0.08061142875641146 + } + }, + "cropflipnormalizereformat_scalar_reflect101_1080p_advanced[InOutDataType=uint8][shape=64x1080x1920][layout=NCHW][srcLayout=NHWC][border=REFLECT101][cropMode=full][flagsMode=normal][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1515.93995037452, + "gpu_time_us_python": 1530.32053811594, + "gpu_noise_us_cpp": 1.1580000000000001, + "gpu_noise_us_python": 4.682, + "gpu_bwutil_cpp": 0.11258228202590763, + "gpu_bwutil_python": 0.11152459533706507 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1595.5528854712798, + "gpu_time_us_python": 1613.88331221962, + "gpu_noise_us_cpp": 1.42, + "gpu_noise_us_python": 3.092, + "gpu_bwutil_cpp": 0.08159413440571393, + "gpu_bwutil_python": 0.08066938865823439 + } + }, + "cropflipnormalizereformat_scalar_reflect101_1080p_advanced[InOutDataType=uint8][shape=64x1080x1920][layout=NHWC][srcLayout=NCHW][border=REFLECT101][cropMode=full][flagsMode=normal][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1527.8301786759, + "gpu_time_us_python": 1544.7256444843001, + "gpu_noise_us_cpp": 1.526, + "gpu_noise_us_python": 5.0, + "gpu_bwutil_cpp": 0.11170609990822034, + "gpu_bwutil_python": 0.1104858175542109 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1613.8865071686, + "gpu_time_us_python": 1632.57446051764, + "gpu_noise_us_cpp": 1.5619999999999998, + "gpu_noise_us_python": 4.058, + "gpu_bwutil_cpp": 0.08066709459387016, + "gpu_bwutil_python": 0.07974279868394549 + } + } + } + }, + "cropflipnormalizereformat_rgb_u8_reflect101_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "srcLayout": [ + "NHWC", + "NCHW" + ], + "border": [ + "REFLECT101" + ], + "cropMode": [ + "full" + ], + "flagsMode": [ + "normal" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CropFlipNormalizeReformat RGB8 reflect101 (advanced)." + }, + "baselines": { + "cropflipnormalizereformat_rgb_u8_reflect101_1080p_advanced[InOutDataType=uchar3][shape=32x1080x1920][layout=NHWC][srcLayout=NHWC][border=REFLECT101][cropMode=full][flagsMode=normal][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1922.75959827724, + "gpu_time_us_python": 1936.1945782463602, + "gpu_noise_us_cpp": 1.9479999999999997, + "gpu_noise_us_python": 4.776000000000001, + "gpu_bwutil_cpp": 0.13314410686192896, + "gpu_bwutil_python": 0.13222065236979946 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1918.8996838074204, + "gpu_time_us_python": 1934.68279272884, + "gpu_noise_us_cpp": 1.004, + "gpu_noise_us_python": 3.844, + "gpu_bwutil_cpp": 0.10176988202060126, + "gpu_bwutil_python": 0.10093962249805218 + } + }, + "cropflipnormalizereformat_rgb_u8_reflect101_1080p_advanced[InOutDataType=uchar3][shape=32x1080x1920][layout=NCHW][srcLayout=NCHW][border=REFLECT101][cropMode=full][flagsMode=normal][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1925.56337176944, + "gpu_time_us_python": 1941.0441594160402, + "gpu_noise_us_cpp": 2.76, + "gpu_noise_us_python": 5.26, + "gpu_bwutil_cpp": 0.13294872036027988, + "gpu_bwutil_python": 0.13188880617430704 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1869.2094152925201, + "gpu_time_us_python": 1883.0892134555397, + "gpu_noise_us_cpp": 0.8960000000000001, + "gpu_noise_us_python": 3.186, + "gpu_bwutil_cpp": 0.1044750653711773, + "gpu_bwutil_python": 0.10370287365272261 + } + }, + "cropflipnormalizereformat_rgb_u8_reflect101_1080p_advanced[InOutDataType=uchar3][shape=32x1080x1920][layout=NCHW][srcLayout=NHWC][border=REFLECT101][cropMode=full][flagsMode=normal][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1933.3356398076999, + "gpu_time_us_python": 1947.2680144285798, + "gpu_noise_us_cpp": 2.868, + "gpu_noise_us_python": 4.95, + "gpu_bwutil_cpp": 0.1324146911419944, + "gpu_bwutil_python": 0.1314670232245402 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1919.7856520608198, + "gpu_time_us_python": 1935.41113043434, + "gpu_noise_us_cpp": 1.666, + "gpu_noise_us_python": 3.11, + "gpu_bwutil_cpp": 0.10172309026163355, + "gpu_bwutil_python": 0.10090120054343939 + } + }, + "cropflipnormalizereformat_rgb_u8_reflect101_1080p_advanced[InOutDataType=uchar3][shape=32x1080x1920][layout=NHWC][srcLayout=NCHW][border=REFLECT101][cropMode=full][flagsMode=normal][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1907.9043418230801, + "gpu_time_us_python": 1921.94452991918, + "gpu_noise_us_cpp": 1.5799999999999998, + "gpu_noise_us_python": 5.456, + "gpu_bwutil_cpp": 0.13418039292505526, + "gpu_bwutil_python": 0.1332000967446138 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1875.0084929920201, + "gpu_time_us_python": 1890.12429239608, + "gpu_noise_us_cpp": 0.644, + "gpu_noise_us_python": 3.71, + "gpu_bwutil_cpp": 0.10415200765871886, + "gpu_bwutil_python": 0.10331857776105599 + } + } + } + }, + "cropflipnormalizereformat_rgb_f32_reflect101_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "srcLayout": [ + "NHWC", + "NCHW" + ], + "border": [ + "REFLECT101" + ], + "cropMode": [ + "full" + ], + "flagsMode": [ + "normal" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CropFlipNormalizeReformat RGBf32 reflect101 (advanced)." + }, + "baselines": { + "cropflipnormalizereformat_rgb_f32_reflect101_1080p_advanced[InOutDataType=float3][shape=32x1080x1920][layout=NHWC][srcLayout=NHWC][border=REFLECT101][cropMode=full][flagsMode=normal][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1978.1165548330398, + "gpu_time_us_python": 1993.6325190192802, + "gpu_noise_us_cpp": 1.256, + "gpu_noise_us_python": 4.728, + "gpu_bwutil_cpp": 0.5176679059021648, + "gpu_bwutil_python": 0.5136472443929245 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1983.2681113671802, + "gpu_time_us_python": 1998.7701964209202, + "gpu_noise_us_cpp": 1.968, + "gpu_noise_us_python": 4.966, + "gpu_bwutil_cpp": 0.3938658994455661, + "gpu_bwutil_python": 0.3908010052817553 + } + }, + "cropflipnormalizereformat_rgb_f32_reflect101_1080p_advanced[InOutDataType=float3][shape=32x1080x1920][layout=NCHW][srcLayout=NCHW][border=REFLECT101][cropMode=full][flagsMode=normal][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2045.5739577070603, + "gpu_time_us_python": 2060.3032648974795, + "gpu_noise_us_cpp": 1.116, + "gpu_noise_us_python": 3.84, + "gpu_bwutil_cpp": 0.5005966633843917, + "gpu_bwutil_python": 0.4970190118544345 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1939.2676478621202, + "gpu_time_us_python": 1948.83401600252, + "gpu_noise_us_cpp": 0.9340000000000002, + "gpu_noise_us_python": 3.0279999999999996, + "gpu_bwutil_cpp": 0.40279792734926356, + "gpu_bwutil_python": 0.4008041797559735 + } + }, + "cropflipnormalizereformat_rgb_f32_reflect101_1080p_advanced[InOutDataType=float3][shape=32x1080x1920][layout=NCHW][srcLayout=NHWC][border=REFLECT101][cropMode=full][flagsMode=normal][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1993.1076301089602, + "gpu_time_us_python": 2007.8732544959998, + "gpu_noise_us_cpp": 1.6179999999999999, + "gpu_noise_us_python": 3.976, + "gpu_bwutil_cpp": 0.5137747746428797, + "gpu_bwutil_python": 0.509999232326141 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2025.62472630134, + "gpu_time_us_python": 2041.1051092547, + "gpu_noise_us_cpp": 1.202, + "gpu_noise_us_python": 5.606, + "gpu_bwutil_cpp": 0.38562964294475444, + "gpu_bwutil_python": 0.382698367524406 + } + }, + "cropflipnormalizereformat_rgb_f32_reflect101_1080p_advanced[InOutDataType=float3][shape=32x1080x1920][layout=NHWC][srcLayout=NCHW][border=REFLECT101][cropMode=full][flagsMode=normal][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2070.50568372018, + "gpu_time_us_python": 2085.1936718615802, + "gpu_noise_us_cpp": 2.97, + "gpu_noise_us_python": 4.374, + "gpu_bwutil_cpp": 0.4945688218133443, + "gpu_bwutil_python": 0.4910864291341405 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1949.60025487274, + "gpu_time_us_python": 1959.5898536749203, + "gpu_noise_us_cpp": 1.3240000000000003, + "gpu_noise_us_python": 2.7199999999999998, + "gpu_bwutil_cpp": 0.4006617025426465, + "gpu_bwutil_python": 0.39859448921343554 + } + } + } + }, + "cropflipnormalizereformat_padded_constant_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "srcLayout": [ + "NHWC", + "NCHW" + ], + "border": [ + "CONSTANT" + ], + "cropMode": [ + "padded16" + ], + "flagsMode": [ + "normal" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CropFlipNormalizeReformat single-channel padded constant (advanced)." + }, + "baselines": { + "cropflipnormalizereformat_padded_constant_1080p_advanced[InOutDataType=uint8][shape=64x1080x1920][layout=NHWC][srcLayout=NHWC][border=CONSTANT][cropMode=padded16][flagsMode=normal][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1156.00388854944, + "gpu_time_us_python": 1171.8473635459, + "gpu_noise_us_cpp": 1.7120000000000002, + "gpu_noise_us_python": 3.508, + "gpu_bwutil_cpp": 0.14763802420925437, + "gpu_bwutil_python": 0.1456421043433498 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1137.93112919218, + "gpu_time_us_python": 1151.45319822384, + "gpu_noise_us_cpp": 1.41, + "gpu_noise_us_python": 3.62, + "gpu_bwutil_cpp": 0.114404934974175, + "gpu_bwutil_python": 0.11305924472802868 + } + }, + "cropflipnormalizereformat_padded_constant_1080p_advanced[InOutDataType=uint8][shape=64x1080x1920][layout=NCHW][srcLayout=NCHW][border=CONSTANT][cropMode=padded16][flagsMode=normal][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1122.7585854595, + "gpu_time_us_python": 1139.1263208121002, + "gpu_noise_us_cpp": 2.652, + "gpu_noise_us_python": 3.69, + "gpu_bwutil_cpp": 0.15200793618125927, + "gpu_bwutil_python": 0.14982777763265334 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1113.09409909736, + "gpu_time_us_python": 1125.5153888260602, + "gpu_noise_us_cpp": 0.9259999999999999, + "gpu_noise_us_python": 2.88, + "gpu_bwutil_cpp": 0.11695823217135988, + "gpu_bwutil_python": 0.1156650854282955 + } + }, + "cropflipnormalizereformat_padded_constant_1080p_advanced[InOutDataType=uint8][shape=64x1080x1920][layout=NCHW][srcLayout=NHWC][border=CONSTANT][cropMode=padded16][flagsMode=normal][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1123.95338786404, + "gpu_time_us_python": 1138.5874078388201, + "gpu_noise_us_cpp": 1.8559999999999999, + "gpu_noise_us_python": 2.5759999999999996, + "gpu_bwutil_cpp": 0.15184732185025798, + "gpu_bwutil_python": 0.1498973455909278 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1113.33362947156, + "gpu_time_us_python": 1126.6289078156599, + "gpu_noise_us_cpp": 1.4940000000000002, + "gpu_noise_us_python": 3.0759999999999996, + "gpu_bwutil_cpp": 0.11693261606856051, + "gpu_bwutil_python": 0.1155507578302919 + } + }, + "cropflipnormalizereformat_padded_constant_1080p_advanced[InOutDataType=uint8][shape=64x1080x1920][layout=NHWC][srcLayout=NCHW][border=CONSTANT][cropMode=padded16][flagsMode=normal][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1154.46417441562, + "gpu_time_us_python": 1169.93548804854, + "gpu_noise_us_cpp": 2.174, + "gpu_noise_us_python": 3.628, + "gpu_bwutil_cpp": 0.14783296516603767, + "gpu_bwutil_python": 0.14587949313428852 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1138.17436378206, + "gpu_time_us_python": 1150.50102875904, + "gpu_noise_us_cpp": 1.36, + "gpu_noise_us_python": 2.342, + "gpu_bwutil_cpp": 0.11438065850077132, + "gpu_bwutil_python": 0.11315508904860686 + } + } + } + }, + "cropflipnormalizereformat_padded_reflect101_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "srcLayout": [ + "NHWC", + "NCHW" + ], + "border": [ + "REFLECT101" + ], + "cropMode": [ + "padded16" + ], + "flagsMode": [ + "normal" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CropFlipNormalizeReformat single-channel padded reflect101 (advanced)." + }, + "baselines": { + "cropflipnormalizereformat_padded_reflect101_1080p_advanced[InOutDataType=uint8][shape=64x1080x1920][layout=NHWC][srcLayout=NHWC][border=REFLECT101][cropMode=padded16][flagsMode=normal][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1529.2295395556, + "gpu_time_us_python": 1543.2539103871, + "gpu_noise_us_cpp": 1.468, + "gpu_noise_us_python": 5.668, + "gpu_bwutil_cpp": 0.11160539876713405, + "gpu_bwutil_python": 0.11059067312244557 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1613.5344479723603, + "gpu_time_us_python": 1628.08119996926, + "gpu_noise_us_cpp": 0.898, + "gpu_noise_us_python": 3.444, + "gpu_bwutil_cpp": 0.08068395823829791, + "gpu_bwutil_python": 0.07996104472990054 + } + }, + "cropflipnormalizereformat_padded_reflect101_1080p_advanced[InOutDataType=uint8][shape=64x1080x1920][layout=NCHW][srcLayout=NCHW][border=REFLECT101][cropMode=padded16][flagsMode=normal][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1516.1520699759599, + "gpu_time_us_python": 1532.26794566678, + "gpu_noise_us_cpp": 1.842, + "gpu_noise_us_python": 4.824, + "gpu_bwutil_cpp": 0.11256686778576333, + "gpu_bwutil_python": 0.11138403847592818 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1595.4422955679602, + "gpu_time_us_python": 1610.5319032490002, + "gpu_noise_us_cpp": 0.9720000000000001, + "gpu_noise_us_python": 4.162000000000001, + "gpu_bwutil_cpp": 0.08159921786816202, + "gpu_bwutil_python": 0.08083187421514096 + } + }, + "cropflipnormalizereformat_padded_reflect101_1080p_advanced[InOutDataType=uint8][shape=64x1080x1920][layout=NCHW][srcLayout=NHWC][border=REFLECT101][cropMode=padded16][flagsMode=normal][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1516.5122547311, + "gpu_time_us_python": 1531.04983752716, + "gpu_noise_us_cpp": 2.158, + "gpu_noise_us_python": 4.248, + "gpu_bwutil_cpp": 0.11254037463033481, + "gpu_bwutil_python": 0.11147184441660847 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1595.42732832934, + "gpu_time_us_python": 1609.7968669359, + "gpu_noise_us_cpp": 0.7899999999999999, + "gpu_noise_us_python": 3.8259999999999996, + "gpu_bwutil_cpp": 0.08159968668564524, + "gpu_bwutil_python": 0.08086988201346863 + } + }, + "cropflipnormalizereformat_padded_reflect101_1080p_advanced[InOutDataType=uint8][shape=64x1080x1920][layout=NHWC][srcLayout=NCHW][border=REFLECT101][cropMode=padded16][flagsMode=normal][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1527.45827793804, + "gpu_time_us_python": 1542.3830286847601, + "gpu_noise_us_cpp": 1.9200000000000004, + "gpu_noise_us_python": 4.068, + "gpu_bwutil_cpp": 0.11173340180498262, + "gpu_bwutil_python": 0.11065312374736336 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1613.90267293612, + "gpu_time_us_python": 1628.6750151116798, + "gpu_noise_us_cpp": 1.6019999999999999, + "gpu_noise_us_python": 3.554, + "gpu_bwutil_cpp": 0.08066536930963256, + "gpu_bwutil_python": 0.07993332030146172 + } + } + } + }, + "cropflipnormalizereformat_stddev_f32_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "srcLayout": [ + "NHWC", + "NCHW" + ], + "border": [ + "REFLECT101" + ], + "cropMode": [ + "full" + ], + "flagsMode": [ + "stddev" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CropFlipNormalizeReformat single-channel stddev (advanced)." + }, + "baselines": { + "cropflipnormalizereformat_stddev_f32_1080p_advanced[InOutDataType=float32][shape=64x1080x1920][layout=NHWC][srcLayout=NHWC][border=REFLECT101][cropMode=full][flagsMode=stddev][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1645.24088999468, + "gpu_time_us_python": 1661.75185459124, + "gpu_noise_us_cpp": 1.236, + "gpu_noise_us_python": 3.572, + "gpu_bwutil_cpp": 0.41493967334982873, + "gpu_bwutil_python": 0.4108178304253192 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1743.3174787939802, + "gpu_time_us_python": 1761.1138584983796, + "gpu_noise_us_cpp": 1.082, + "gpu_noise_us_python": 6.1899999999999995, + "gpu_bwutil_cpp": 0.29870899503859033, + "gpu_bwutil_python": 0.29569139643909154 + } + }, + "cropflipnormalizereformat_stddev_f32_1080p_advanced[InOutDataType=float32][shape=64x1080x1920][layout=NCHW][srcLayout=NCHW][border=REFLECT101][cropMode=full][flagsMode=stddev][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1643.8491035540799, + "gpu_time_us_python": 1660.4401212979003, + "gpu_noise_us_cpp": 1.3239999999999998, + "gpu_noise_us_python": 3.556, + "gpu_bwutil_cpp": 0.41529044054922404, + "gpu_bwutil_python": 0.4111453996071421 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1728.2682530730199, + "gpu_time_us_python": 1745.82701367034, + "gpu_noise_us_cpp": 0.8859999999999999, + "gpu_noise_us_python": 5.5920000000000005, + "gpu_bwutil_cpp": 0.30131019317846486, + "gpu_bwutil_python": 0.29826421877980197 + } + }, + "cropflipnormalizereformat_stddev_f32_1080p_advanced[InOutDataType=float32][shape=64x1080x1920][layout=NCHW][srcLayout=NHWC][border=REFLECT101][cropMode=full][flagsMode=stddev][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1643.3233481390803, + "gpu_time_us_python": 1658.4128701755399, + "gpu_noise_us_cpp": 1.216, + "gpu_noise_us_python": 3.618, + "gpu_bwutil_cpp": 0.41542274550943314, + "gpu_bwutil_python": 0.4116436825843607 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1728.3634145361, + "gpu_time_us_python": 1745.52677723218, + "gpu_noise_us_cpp": 1.1, + "gpu_noise_us_python": 4.708, + "gpu_bwutil_cpp": 0.3012936708791984, + "gpu_bwutil_python": 0.2983284027891214 + } + }, + "cropflipnormalizereformat_stddev_f32_1080p_advanced[InOutDataType=float32][shape=64x1080x1920][layout=NHWC][srcLayout=NCHW][border=REFLECT101][cropMode=full][flagsMode=stddev][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1645.12698168414, + "gpu_time_us_python": 1661.93548895856, + "gpu_noise_us_cpp": 2.418, + "gpu_noise_us_python": 4.702, + "gpu_bwutil_cpp": 0.41496785897427435, + "gpu_bwutil_python": 0.4107729287737766 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1743.17538399986, + "gpu_time_us_python": 1759.9688039446999, + "gpu_noise_us_cpp": 1.0419999999999998, + "gpu_noise_us_python": 4.6, + "gpu_bwutil_cpp": 0.2987338353865481, + "gpu_bwutil_python": 0.29587144059087855 + } + } + } + } + } +} diff --git a/bench/config/operators/customcrop.json b/bench/config/operators/customcrop.json new file mode 100644 index 000000000..d7322c759 --- /dev/null +++ b/bench/config/operators/customcrop.json @@ -0,0 +1,1274 @@ +{ + "benchmark": "customcrop", + "configs": { + "customcrop_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "cropMode": [ + "full" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CustomCrop RGB8 (basic)." + }, + "baselines": { + "customcrop_uchar3_basic[InOutDataType=uchar3][shape=128x1080x1920][cropMode=full][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1592.9261964722002, + "gpu_time_us_python": 1602.5086276187599, + "gpu_noise_us_cpp": 1.248, + "gpu_noise_us_python": 4.652, + "gpu_bwutil_cpp": 0.6428422256442634, + "gpu_bwutil_python": 0.6389985716122965, + "gpu_gap_stddev_us": 1.3614632278284367 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1284.98256797875, + "gpu_time_us_python": 1294.2479939951, + "gpu_noise_us_cpp": 1.0925, + "gpu_noise_us_python": 3.5875, + "gpu_bwutil_cpp": 0.6078075993528147, + "gpu_bwutil_python": 0.6034573090645378, + "gpu_gap_stddev_us": 0.4640071115658992 + } + } + } + }, + "customcrop_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "cropMode": [ + "full" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CustomCrop RGBf32 (basic)." + }, + "baselines": { + "customcrop_float3_basic[InOutDataType=float3][shape=32x1080x1920][cropMode=full][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1161.69578631714, + "gpu_time_us_python": 1170.7898964391402, + "gpu_noise_us_cpp": 1.416, + "gpu_noise_us_python": 2.164, + "gpu_bwutil_cpp": 0.8814700675157405, + "gpu_bwutil_python": 0.8746236099226572, + "gpu_gap_stddev_us": 1.11128996278274 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 873.4682637199501, + "gpu_time_us_python": 883.603846379425, + "gpu_noise_us_cpp": 3.2800000000000002, + "gpu_noise_us_python": 3.585, + "gpu_bwutil_cpp": 0.8941576264576275, + "gpu_bwutil_python": 0.8839099338734673, + "gpu_gap_stddev_us": 3.014761292483914 + } + } + } + }, + "customcrop_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "cropMode": [ + "full" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CustomCrop RGBA8 (advanced)." + }, + "baselines": { + "customcrop_uchar4_advanced[InOutDataType=uchar4][shape=64x1080x1920][cropMode=full][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 875.1562172299, + "gpu_time_us_python": 883.81612091274, + "gpu_noise_us_cpp": 1.566, + "gpu_noise_us_python": 1.6020000000000003, + "gpu_bwutil_cpp": 0.7800513565092863, + "gpu_bwutil_python": 0.7724084663123406, + "gpu_gap_stddev_us": 0.7592306434047984 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 794.9691014002249, + "gpu_time_us_python": 804.989174975875, + "gpu_noise_us_cpp": 1.7774999999999999, + "gpu_noise_us_python": 3.3575000000000004, + "gpu_bwutil_cpp": 0.6549687819072414, + "gpu_bwutil_python": 0.6468181622269362, + "gpu_gap_stddev_us": 0.8714946574536795 + } + } + } + }, + "customcrop_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "cropMode": [ + "full" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CustomCrop RGBAf32 (advanced)." + }, + "baselines": { + "customcrop_float4_advanced[InOutDataType=float4][shape=16x1080x1920][cropMode=full][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 765.40773294016, + "gpu_time_us_python": 775.10875571884, + "gpu_noise_us_cpp": 0.9700000000000001, + "gpu_noise_us_python": 2.5620000000000003, + "gpu_bwutil_cpp": 0.8918993665810746, + "gpu_bwutil_python": 0.8807440137112417, + "gpu_gap_stddev_us": 2.4864479863250075 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 587.21319947595, + "gpu_time_us_python": 595.7809814407999, + "gpu_noise_us_cpp": 1.6324999999999998, + "gpu_noise_us_python": 2.75, + "gpu_bwutil_cpp": 0.8866939769329278, + "gpu_bwutil_python": 0.8739431301935481, + "gpu_gap_stddev_us": 0.4226047308582465 + } + } + } + }, + "customcrop_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "cropMode": [ + "full" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CustomCrop single-channel (advanced)." + }, + "baselines": { + "customcrop_scalar_advanced[InOutDataType=float32][shape=128x1080x1920][cropMode=full][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1738.50169363946, + "gpu_time_us_python": 1747.9795914163, + "gpu_noise_us_cpp": 2.3020000000000005, + "gpu_noise_us_python": 1.8559999999999999, + "gpu_bwutil_cpp": 0.7853507185412544, + "gpu_bwutil_python": 0.7810923447516376, + "gpu_gap_stddev_us": 0.7860436666005344 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1573.850488854775, + "gpu_time_us_python": 1585.31062947725, + "gpu_noise_us_cpp": 0.8675, + "gpu_noise_us_python": 3.46, + "gpu_bwutil_cpp": 0.6616639752381348, + "gpu_bwutil_python": 0.6568867097718782, + "gpu_gap_stddev_us": 3.6566271218650046 + } + }, + "customcrop_scalar_advanced[InOutDataType=uint8][shape=128x1080x1920][cropMode=full][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 441.16180209628, + "gpu_time_us_python": 450.22537460962, + "gpu_noise_us_cpp": 1.014, + "gpu_noise_us_python": 1.638, + "gpu_bwutil_cpp": 0.7737148582292912, + "gpu_bwutil_python": 0.7581398167794051, + "gpu_gap_stddev_us": 0.4302935396644625 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 419.074194742325, + "gpu_time_us_python": 429.93614695255, + "gpu_noise_us_cpp": 2.035, + "gpu_noise_us_python": 3.7575000000000003, + "gpu_bwutil_cpp": 0.6212277160048881, + "gpu_bwutil_python": 0.6055523945589789, + "gpu_gap_stddev_us": 2.3162859195919774 + } + } + } + }, + "customcrop_scalar_u8_full_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "256x1080x1920" + ], + "cropMode": [ + "full" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CustomCrop single-channel full (advanced)." + }, + "baselines": { + "customcrop_scalar_u8_full_1080p_advanced[InOutDataType=uint8][shape=256x1080x1920][cropMode=full][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 871.56710776614, + "gpu_time_us_python": 882.01169825896, + "gpu_noise_us_cpp": 1.1959999999999997, + "gpu_noise_us_python": 2.21, + "gpu_bwutil_cpp": 0.783263642127985, + "gpu_bwutil_python": 0.7739908960255089, + "gpu_gap_stddev_us": 1.9400661175527707 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 826.692460747925, + "gpu_time_us_python": 838.7780502248249, + "gpu_noise_us_cpp": 1.6925000000000001, + "gpu_noise_us_python": 3.19, + "gpu_bwutil_cpp": 0.629837812652452, + "gpu_bwutil_python": 0.6207645469115055, + "gpu_gap_stddev_us": 1.705956919773072 + } + } + } + }, + "customcrop_rgb_u8_full_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "cropMode": [ + "full" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CustomCrop RGB8 full (advanced)." + }, + "baselines": { + "customcrop_rgb_u8_full_1080p_advanced[InOutDataType=uchar3][shape=128x1080x1920][cropMode=full][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1592.98109912892, + "gpu_time_us_python": 1602.84752305786, + "gpu_noise_us_cpp": 2.25, + "gpu_noise_us_python": 1.9279999999999997, + "gpu_bwutil_cpp": 0.6428201865523225, + "gpu_bwutil_python": 0.6388631619533295, + "gpu_gap_stddev_us": 1.0343599786289737 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1284.3623146921, + "gpu_time_us_python": 1296.89787858985, + "gpu_noise_us_cpp": 2.2800000000000002, + "gpu_noise_us_python": 4.7325, + "gpu_bwutil_cpp": 0.6081003870017693, + "gpu_bwutil_python": 0.6022219247818776, + "gpu_gap_stddev_us": 1.4872146515711604 + } + } + } + }, + "customcrop_rgb_f32_full_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "cropMode": [ + "full" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CustomCrop RGBf32 full (advanced)." + }, + "baselines": { + "customcrop_rgb_f32_full_1080p_advanced[InOutDataType=float3][shape=32x1080x1920][cropMode=full][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1161.5744226508, + "gpu_time_us_python": 1171.0234082747197, + "gpu_noise_us_cpp": 1.3199999999999998, + "gpu_noise_us_python": 2.1159999999999997, + "gpu_bwutil_cpp": 0.8815621302158693, + "gpu_bwutil_python": 0.8744496286510264, + "gpu_gap_stddev_us": 1.3436955297541355 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 873.3714016271499, + "gpu_time_us_python": 882.935428861725, + "gpu_noise_us_cpp": 2.08, + "gpu_noise_us_python": 4.725, + "gpu_bwutil_cpp": 0.8942560784624316, + "gpu_bwutil_python": 0.8845735028241282, + "gpu_gap_stddev_us": 1.9462585368004137 + } + } + } + }, + "customcrop_scalar_u8_center_half_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "1024x1080x1920" + ], + "cropMode": [ + "center_half" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CustomCrop single-channel center half (advanced)." + }, + "baselines": { + "customcrop_scalar_u8_center_half_1080p_advanced[InOutDataType=uint8][shape=1024x1080x1920][cropMode=center_half][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 894.8860505222599, + "gpu_time_us_python": 904.17868867242, + "gpu_noise_us_cpp": 1.764, + "gpu_noise_us_python": 1.7779999999999998, + "gpu_bwutil_cpp": 0.7628532520385206, + "gpu_bwutil_python": 0.7550130630654197, + "gpu_gap_stddev_us": 0.4671362448016733 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 815.119413408775, + "gpu_time_us_python": 825.083666116175, + "gpu_noise_us_cpp": 1.065, + "gpu_noise_us_python": 3.16, + "gpu_bwutil_cpp": 0.638777430706095, + "gpu_bwutil_python": 0.6310651240030907, + "gpu_gap_stddev_us": 0.9223080438267042 + } + } + } + }, + "customcrop_planar_nchw_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "cropMode": [ + "full" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CustomCrop RGB8 (basic). [native planar NCHW]" + }, + "baselines": { + "customcrop_planar_nchw_uchar3_basic[InOutDataType=uchar3][shape=128x1080x1920][cropMode=full][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1302.4175857169198, + "gpu_time_us_python": 1312.32728196686, + "gpu_noise_us_cpp": 1.3119999999999998, + "gpu_noise_us_python": 2.9199999999999995, + "gpu_bwutil_cpp": 0.7862303546580686, + "gpu_bwutil_python": 0.7802932579531673, + "gpu_gap_stddev_us": 0.7118396960848502 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1234.5262699444002, + "gpu_time_us_python": 1247.38337103675, + "gpu_noise_us_cpp": 3.2175000000000002, + "gpu_noise_us_python": 3.6400000000000006, + "gpu_bwutil_cpp": 0.6326497849402063, + "gpu_bwutil_python": 0.6261313482600857, + "gpu_gap_stddev_us": 1.7455693152764966 + } + } + } + }, + "customcrop_planar_nchw_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "cropMode": [ + "full" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CustomCrop RGBf32 (basic). [native planar NCHW]" + }, + "baselines": { + "customcrop_planar_nchw_float3_basic[InOutDataType=float3][shape=32x1080x1920][cropMode=full][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1308.0666649325399, + "gpu_time_us_python": 1318.2307873273603, + "gpu_noise_us_cpp": 1.47, + "gpu_noise_us_python": 3.478, + "gpu_bwutil_cpp": 0.7828349019692216, + "gpu_bwutil_python": 0.7767993510645476, + "gpu_gap_stddev_us": 1.1843904018584026 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1184.166125871875, + "gpu_time_us_python": 1193.279538530475, + "gpu_noise_us_cpp": 1.0550000000000002, + "gpu_noise_us_python": 4.2275, + "gpu_bwutil_cpp": 0.6595525130562709, + "gpu_bwutil_python": 0.6545166249220916, + "gpu_gap_stddev_us": 1.6312027540193355 + } + } + } + }, + "customcrop_planar_nchw_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "cropMode": [ + "full" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CustomCrop RGBA8 (advanced). [native planar NCHW]" + }, + "baselines": { + "customcrop_planar_nchw_uchar4_advanced[InOutDataType=uchar4][shape=64x1080x1920][cropMode=full][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 872.6692242904401, + "gpu_time_us_python": 881.8558053231002, + "gpu_noise_us_cpp": 1.782, + "gpu_noise_us_python": 2.042, + "gpu_bwutil_cpp": 0.7822745393990131, + "gpu_bwutil_python": 0.7741254881224866, + "gpu_gap_stddev_us": 0.8401155885305962 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 828.8412939800751, + "gpu_time_us_python": 838.843183048575, + "gpu_noise_us_cpp": 2.025, + "gpu_noise_us_python": 5.445, + "gpu_bwutil_cpp": 0.6282043319257408, + "gpu_bwutil_python": 0.6207139235384941, + "gpu_gap_stddev_us": 3.379871362237514 + } + } + } + }, + "customcrop_planar_nchw_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "cropMode": [ + "full" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CustomCrop RGBAf32 (advanced). [native planar NCHW]" + }, + "baselines": { + "customcrop_planar_nchw_float4_advanced[InOutDataType=float4][shape=16x1080x1920][cropMode=full][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 876.13224608074, + "gpu_time_us_python": 885.4119449482199, + "gpu_noise_us_cpp": 1.8760000000000001, + "gpu_noise_us_python": 2.934, + "gpu_bwutil_cpp": 0.7791826109898925, + "gpu_bwutil_python": 0.7710167383266846, + "gpu_gap_stddev_us": 1.0397780529148686 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 795.32690789315, + "gpu_time_us_python": 804.674470937875, + "gpu_noise_us_cpp": 1.3575, + "gpu_noise_us_python": 3.0275, + "gpu_bwutil_cpp": 0.6546744136916994, + "gpu_bwutil_python": 0.6470696970231806, + "gpu_gap_stddev_us": 0.712431848798436 + } + } + } + }, + "customcrop_planar_nchw_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "cropMode": [ + "full" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CustomCrop single-channel (advanced). [native planar NCHW]" + }, + "baselines": { + "customcrop_planar_nchw_scalar_advanced[InOutDataType=float32][shape=128x1080x1920][cropMode=full][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1739.07911976174, + "gpu_time_us_python": 1749.16765827276, + "gpu_noise_us_cpp": 1.4260000000000002, + "gpu_noise_us_python": 3.2059999999999995, + "gpu_bwutil_cpp": 0.7850900634889157, + "gpu_bwutil_python": 0.780562140758699, + "gpu_gap_stddev_us": 1.254066192879616 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1574.9162971070748, + "gpu_time_us_python": 1585.005257124125, + "gpu_noise_us_cpp": 1.2025, + "gpu_noise_us_python": 4.24, + "gpu_bwutil_cpp": 0.6612162281255979, + "gpu_bwutil_python": 0.657008212510933, + "gpu_gap_stddev_us": 1.1611987296535824 + } + }, + "customcrop_planar_nchw_scalar_advanced[InOutDataType=uint8][shape=128x1080x1920][cropMode=full][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 441.87838490384, + "gpu_time_us_python": 451.25688674111996, + "gpu_noise_us_cpp": 1.1380000000000001, + "gpu_noise_us_python": 4.433999999999999, + "gpu_bwutil_cpp": 0.7724601496823915, + "gpu_bwutil_python": 0.7564084920810639, + "gpu_gap_stddev_us": 0.9384480191231651 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 419.51518236149997, + "gpu_time_us_python": 429.251426668275, + "gpu_noise_us_cpp": 2.0675000000000003, + "gpu_noise_us_python": 2.7775, + "gpu_bwutil_cpp": 0.6205771322557265, + "gpu_bwutil_python": 0.6065029054263283, + "gpu_gap_stddev_us": 1.2587888606708126 + } + } + } + }, + "customcrop_planar_nchw_scalar_u8_full_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "256x1080x1920" + ], + "cropMode": [ + "full" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CustomCrop single-channel full (advanced). [native planar NCHW]" + }, + "baselines": { + "customcrop_planar_nchw_scalar_u8_full_1080p_advanced[InOutDataType=uint8][shape=256x1080x1920][cropMode=full][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 872.2960195474201, + "gpu_time_us_python": 882.46227318126, + "gpu_noise_us_cpp": 1.074, + "gpu_noise_us_python": 1.9939999999999998, + "gpu_bwutil_cpp": 0.782609122557242, + "gpu_bwutil_python": 0.7735935238110423, + "gpu_gap_stddev_us": 0.9383010638386681 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 828.24891845435, + "gpu_time_us_python": 837.9207019217001, + "gpu_noise_us_cpp": 1.1375, + "gpu_noise_us_python": 3.615, + "gpu_bwutil_cpp": 0.6286527723820704, + "gpu_bwutil_python": 0.6214109844671126, + "gpu_gap_stddev_us": 3.1479318667180274 + } + } + } + }, + "customcrop_planar_nchw_rgb_u8_full_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "cropMode": [ + "full" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CustomCrop RGB8 full (advanced). [native planar NCHW]" + }, + "baselines": { + "customcrop_planar_nchw_rgb_u8_full_1080p_advanced[InOutDataType=uchar3][shape=128x1080x1920][cropMode=full][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1302.3209339358, + "gpu_time_us_python": 1311.5498953464198, + "gpu_noise_us_cpp": 0.884, + "gpu_noise_us_python": 1.864, + "gpu_bwutil_cpp": 0.7862886098722086, + "gpu_bwutil_python": 0.7807557187129026, + "gpu_gap_stddev_us": 0.3010983663638164 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1235.028564179525, + "gpu_time_us_python": 1245.041489077, + "gpu_noise_us_cpp": 1.04, + "gpu_noise_us_python": 2.3925, + "gpu_bwutil_cpp": 0.6323920793358759, + "gpu_bwutil_python": 0.6273044097933423, + "gpu_gap_stddev_us": 0.895567347496837 + } + } + } + }, + "customcrop_planar_nchw_rgb_f32_full_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "cropMode": [ + "full" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CustomCrop RGBf32 full (advanced). [native planar NCHW]" + }, + "baselines": { + "customcrop_planar_nchw_rgb_f32_full_1080p_advanced[InOutDataType=float3][shape=32x1080x1920][cropMode=full][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1307.9573110987199, + "gpu_time_us_python": 1318.23509969204, + "gpu_noise_us_cpp": 1.406, + "gpu_noise_us_python": 3.5460000000000003, + "gpu_bwutil_cpp": 0.7829003286035081, + "gpu_bwutil_python": 0.7767962554715868, + "gpu_gap_stddev_us": 0.8982935908422968 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1182.8786802486752, + "gpu_time_us_python": 1193.8115061354251, + "gpu_noise_us_cpp": 2.5424999999999995, + "gpu_noise_us_python": 4.1575, + "gpu_bwutil_cpp": 0.6602714434761946, + "gpu_bwutil_python": 0.654225047182718, + "gpu_gap_stddev_us": 1.1782081581987744 + } + } + } + }, + "customcrop_planar_nchw_scalar_u8_center_half_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "1024x1080x1920" + ], + "cropMode": [ + "center_half" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CustomCrop single-channel center half (advanced). [native planar NCHW]" + }, + "baselines": { + "customcrop_planar_nchw_scalar_u8_center_half_1080p_advanced[InOutDataType=uint8][shape=1024x1080x1920][cropMode=center_half][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 895.9070317294802, + "gpu_time_us_python": 905.49592993112, + "gpu_noise_us_cpp": 2.65, + "gpu_noise_us_python": 2.284, + "gpu_bwutil_cpp": 0.7619841108169955, + "gpu_bwutil_python": 0.7539150217391338, + "gpu_gap_stddev_us": 0.7620262853677027 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 816.286139633175, + "gpu_time_us_python": 824.9277329149251, + "gpu_noise_us_cpp": 1.445, + "gpu_noise_us_python": 3.39, + "gpu_bwutil_cpp": 0.6378646472223498, + "gpu_bwutil_python": 0.6311822112695289, + "gpu_gap_stddev_us": 1.4091430915785155 + } + } + } + }, + "customcrop_fakeplanar_nchw_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "cropMode": [ + "full" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CustomCrop RGBA8 (advanced). [fake-planar NCHW_FAKE]" + }, + "baselines": { + "customcrop_fakeplanar_nchw_uchar4_advanced[InOutDataType=uchar4][shape=64x1080x1920][cropMode=full][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3932.2751551866, + "gpu_time_us_python": 3952.5728471576394, + "gpu_noise_us_cpp": 3.7900000000000005, + "gpu_noise_us_python": 4.028, + "gpu_bwutil_cpp": 0.5208181926960471, + "gpu_bwutil_python": 0.5181436181860309, + "gpu_gap_stddev_us": 0.9977817054929659 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 3198.43867134825, + "gpu_time_us_python": 3211.973652316075, + "gpu_noise_us_cpp": 1.0925, + "gpu_noise_us_python": 4.615, + "gpu_bwutil_cpp": 0.488380013663474, + "gpu_bwutil_python": 0.4863222503017878, + "gpu_gap_stddev_us": 2.262075984447154 + } + } + } + }, + "customcrop_fakeplanar_nchw_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "cropMode": [ + "full" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CustomCrop RGBAf32 (advanced). [fake-planar NCHW_FAKE]" + }, + "baselines": { + "customcrop_fakeplanar_nchw_float4_advanced[InOutDataType=float4][shape=16x1080x1920][cropMode=full][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2879.70220905606, + "gpu_time_us_python": 2896.5688014852, + "gpu_noise_us_cpp": 3.082, + "gpu_noise_us_python": 3.8739999999999997, + "gpu_bwutil_cpp": 0.7111846640066999, + "gpu_bwutil_python": 0.7070434624105589, + "gpu_gap_stddev_us": 0.863079608225378 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2342.492034502075, + "gpu_time_us_python": 2357.749638813225, + "gpu_noise_us_cpp": 4.465, + "gpu_noise_us_python": 7.119999999999999, + "gpu_bwutil_cpp": 0.6668268329740379, + "gpu_bwutil_python": 0.6625107111182094, + "gpu_gap_stddev_us": 2.458219527619211 + } + } + } + }, + "customcrop_fakeplanar_nchw_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "cropMode": [ + "full" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CustomCrop single-channel (advanced). [fake-planar NCHW_FAKE]" + }, + "baselines": { + "customcrop_fakeplanar_nchw_scalar_advanced[InOutDataType=float32][shape=128x1080x1920][cropMode=full][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5179.20806845834, + "gpu_time_us_python": 5196.239971627941, + "gpu_noise_us_cpp": 1.8500000000000003, + "gpu_noise_us_python": 2.778, + "gpu_bwutil_cpp": 0.7908546968487847, + "gpu_bwutil_python": 0.7882624007010415, + "gpu_gap_stddev_us": 2.1906616662720504 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 4785.149724978275, + "gpu_time_us_python": 4800.9802670973, + "gpu_noise_us_cpp": 1.3975000000000002, + "gpu_noise_us_python": 4.865, + "gpu_bwutil_cpp": 0.6528719009094015, + "gpu_bwutil_python": 0.6507183650604551, + "gpu_gap_stddev_us": 1.68046420613806 + } + }, + "customcrop_fakeplanar_nchw_scalar_advanced[InOutDataType=uint8][shape=128x1080x1920][cropMode=full][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1210.07517605006, + "gpu_time_us_python": 1226.25489257668, + "gpu_noise_us_cpp": 2.112, + "gpu_noise_us_python": 4.359999999999999, + "gpu_bwutil_cpp": 0.8462284265312038, + "gpu_bwutil_python": 0.8350635876038733, + "gpu_gap_stddev_us": 1.3147953305250146 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1003.1245423554749, + "gpu_time_us_python": 1017.66161059655, + "gpu_noise_us_cpp": 1.5150000000000001, + "gpu_noise_us_python": 5.7375, + "gpu_bwutil_cpp": 0.7785847746891205, + "gpu_bwutil_python": 0.7674643880516652, + "gpu_gap_stddev_us": 1.6005736501804202 + } + } + } + }, + "customcrop_fakeplanar_nchw_scalar_u8_full_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "256x1080x1920" + ], + "cropMode": [ + "full" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CustomCrop single-channel full (advanced). [fake-planar NCHW_FAKE]" + }, + "baselines": { + "customcrop_fakeplanar_nchw_scalar_u8_full_1080p_advanced[InOutDataType=uint8][shape=256x1080x1920][cropMode=full][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2401.15509578156, + "gpu_time_us_python": 2418.56792890102, + "gpu_noise_us_cpp": 1.9780000000000002, + "gpu_noise_us_python": 3.1799999999999997, + "gpu_bwutil_cpp": 0.8529228551500841, + "gpu_bwutil_python": 0.8467822043573854, + "gpu_gap_stddev_us": 1.351089386344319 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1981.597544405375, + "gpu_time_us_python": 1996.6743577813, + "gpu_noise_us_cpp": 2.105, + "gpu_noise_us_python": 6.887499999999999, + "gpu_bwutil_cpp": 0.7882707858427619, + "gpu_bwutil_python": 0.7823195864500789, + "gpu_gap_stddev_us": 2.8980301470825522 + } + } + } + }, + "customcrop_fakeplanar_nchw_rgb_u8_full_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "cropMode": [ + "full" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CustomCrop RGB8 full (advanced). [fake-planar NCHW_FAKE]" + }, + "baselines": { + "customcrop_fakeplanar_nchw_rgb_u8_full_1080p_advanced[InOutDataType=uchar3][shape=128x1080x1920][cropMode=full][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6311.10144853588, + "gpu_time_us_python": 6327.56220340722, + "gpu_noise_us_cpp": 3.008, + "gpu_noise_us_python": 3.804, + "gpu_bwutil_cpp": 0.4867614363793928, + "gpu_bwutil_python": 0.4854951250707041, + "gpu_gap_stddev_us": 0.740420068688589 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 4960.07340562105, + "gpu_time_us_python": 4977.9698801975, + "gpu_noise_us_cpp": 1.9675, + "gpu_noise_us_python": 5.9225, + "gpu_bwutil_cpp": 0.4723867603902468, + "gpu_bwutil_python": 0.47068707684555267, + "gpu_gap_stddev_us": 3.3121947872558386 + } + } + } + }, + "customcrop_fakeplanar_nchw_rgb_f32_full_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "cropMode": [ + "full" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CustomCrop RGBf32 full (advanced). [fake-planar NCHW_FAKE]" + }, + "baselines": { + "customcrop_fakeplanar_nchw_rgb_f32_full_1080p_advanced[InOutDataType=float3][shape=32x1080x1920][cropMode=full][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3837.6912694988196, + "gpu_time_us_python": 3856.46595514734, + "gpu_noise_us_cpp": 3.9839999999999995, + "gpu_noise_us_python": 3.594, + "gpu_bwutil_cpp": 0.8004813156813976, + "gpu_bwutil_python": 0.796584256531133, + "gpu_gap_stddev_us": 1.2424499046422912 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 3071.30965953915, + "gpu_time_us_python": 3093.8248869813, + "gpu_noise_us_cpp": 2.3049999999999997, + "gpu_noise_us_python": 5.7125, + "gpu_bwutil_cpp": 0.7628846540827742, + "gpu_bwutil_python": 0.7573317381051773, + "gpu_gap_stddev_us": 3.0510793940071 + } + } + } + }, + "customcrop_fakeplanar_nchw_scalar_u8_center_half_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "1024x1080x1920" + ], + "cropMode": [ + "center_half" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CustomCrop single-channel center half (advanced). [fake-planar NCHW_FAKE]" + }, + "baselines": { + "customcrop_fakeplanar_nchw_scalar_u8_center_half_1080p_advanced[InOutDataType=uint8][shape=1024x1080x1920][cropMode=center_half][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4705.333047442919, + "gpu_time_us_python": 4723.518857416059, + "gpu_noise_us_cpp": 4.1899999999999995, + "gpu_noise_us_python": 4.544, + "gpu_bwutil_cpp": 0.8705016374697735, + "gpu_bwutil_python": 0.8671501586571168, + "gpu_gap_stddev_us": 1.0469490774968337 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 3691.38046573185, + "gpu_time_us_python": 3707.243410979975, + "gpu_noise_us_cpp": 1.6075, + "gpu_noise_us_python": 4.0825, + "gpu_bwutil_cpp": 0.84631446375295, + "gpu_bwutil_python": 0.8426932025327988, + "gpu_gap_stddev_us": 2.275065457722665 + } + } + } + } + } +} diff --git a/bench/config/operators/cvtcolor.json b/bench/config/operators/cvtcolor.json new file mode 100644 index 000000000..f9a3b8b40 --- /dev/null +++ b/bench/config/operators/cvtcolor.json @@ -0,0 +1,1375 @@ +{ + "benchmark": "cvtcolor", + "configs": { + "cvtcolor_grayscale_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "512x1080x1920" + ], + "code": [ + "RGB2GRAY" + ], + "inputKind": [ + "VarShape" + ], + "layout": [ + "NHWC" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CvtColor single-channel grayscale (advanced)." + }, + "baselines": { + "cvtcolor_grayscale_advanced[InOutDataType=uint8][shape=512x1080x1920][code=RGB2GRAY][inputKind=VarShape][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4280.868472083081, + "gpu_time_us_python": 4286.14465907462, + "gpu_noise_us_cpp": 5.79, + "gpu_noise_us_python": 6.7379999999999995, + "gpu_bwutil_cpp": 0.6378781265140836, + "gpu_bwutil_python": 0.6370926404467365, + "gpu_gap_stddev_us": 1.2021524768369818 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 3808.5417842254997, + "gpu_time_us_python": 3835.946813288225, + "gpu_noise_us_cpp": 2.25, + "gpu_noise_us_python": 4.9875, + "gpu_bwutil_cpp": 0.5468887249686463, + "gpu_bwutil_python": 0.5429727997856719, + "gpu_gap_stddev_us": 4.716808809989616 + } + } + } + }, + "cvtcolor_yuv_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "code": [ + "YUV2RGB" + ], + "inputKind": [ + "Tensor" + ], + "layout": [ + "NHWC" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CvtColor single-channel yuv (advanced)." + }, + "baselines": { + "cvtcolor_yuv_advanced[InOutDataType=uint8][shape=128x1080x1920][code=YUV2RGB][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1237.0505163821604, + "gpu_time_us_python": 1244.23138152422, + "gpu_noise_us_cpp": 2.71, + "gpu_noise_us_python": 2.866, + "gpu_bwutil_cpp": 0.8277754925620313, + "gpu_bwutil_python": 0.8229984131980143, + "gpu_gap_stddev_us": 0.8526667665214563 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1077.92321291705, + "gpu_time_us_python": 1084.3792569327252, + "gpu_noise_us_cpp": 3.7425, + "gpu_noise_us_python": 3.9775, + "gpu_bwutil_cpp": 0.7245572182100729, + "gpu_bwutil_python": 0.7202435638402633, + "gpu_gap_stddev_us": 0.5046569646596833 + } + } + } + }, + "cvtcolor_hsv_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "code": [ + "RGB2HSV" + ], + "inputKind": [ + "Tensor" + ], + "layout": [ + "NHWC" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CvtColor single-channel hsv (advanced)." + }, + "baselines": { + "cvtcolor_hsv_advanced[InOutDataType=uint8][shape=128x1080x1920][code=RGB2HSV][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1761.00033167882, + "gpu_time_us_python": 1770.78901543594, + "gpu_noise_us_cpp": 1.318, + "gpu_noise_us_python": 3.4159999999999995, + "gpu_bwutil_cpp": 0.5814914787276226, + "gpu_bwutil_python": 0.5782757380113818, + "gpu_gap_stddev_us": 2.28869010061357 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1650.3740413579, + "gpu_time_us_python": 1661.6536953361, + "gpu_noise_us_cpp": 0.795, + "gpu_noise_us_python": 4.26, + "gpu_bwutil_cpp": 0.4732990102684255, + "gpu_bwutil_python": 0.47008871431934013, + "gpu_gap_stddev_us": 1.0873432145181865 + } + } + } + }, + "cvtcolor_nv12_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "code": [ + "YUV2RGB_NV12" + ], + "inputKind": [ + "Tensor" + ], + "layout": [ + "NHWC" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CvtColor single-channel nv12 (advanced)." + }, + "baselines": { + "cvtcolor_nv12_advanced[InOutDataType=uint8][shape=128x1080x1920][code=YUV2RGB_NV12][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1043.0204512471398, + "gpu_time_us_python": 1049.53700894896, + "gpu_noise_us_cpp": 4.1, + "gpu_noise_us_python": 2.2179999999999995, + "gpu_bwutil_cpp": 0.7363305183065594, + "gpu_bwutil_python": 0.7317603757330677, + "gpu_gap_stddev_us": 4.163597438462316 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 990.7529045215749, + "gpu_time_us_python": 999.7205158062251, + "gpu_noise_us_cpp": 4.29, + "gpu_noise_us_python": 4.7425, + "gpu_bwutil_cpp": 0.5912840164372326, + "gpu_bwutil_python": 0.5859829172604492, + "gpu_gap_stddev_us": 0.7454743462457027 + } + } + } + }, + "cvtcolor_basic": { + "tier": "basic", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "code": [ + "RGB2GRAY", + "RGB2HSV", + "YUV2RGB", + "YUV2RGB_NV12" + ], + "inputKind": [ + "Tensor" + ], + "layout": [ + "NHWC" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CvtColor 1080p branch sweep (basic)." + }, + "baselines": { + "cvtcolor_basic[InOutDataType=uint8][shape=128x1080x1920][code=RGB2GRAY][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 914.63230600316, + "gpu_time_us_python": 923.8089660436401, + "gpu_noise_us_cpp": 0.738, + "gpu_noise_us_python": 1.69, + "gpu_bwutil_cpp": 0.7463838461409457, + "gpu_bwutil_python": 0.7389702047727816, + "gpu_gap_stddev_us": 0.8253286045328102 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 822.6699018293, + "gpu_time_us_python": 831.8303934442749, + "gpu_noise_us_cpp": 1.5799999999999998, + "gpu_noise_us_python": 4.9125, + "gpu_bwutil_cpp": 0.6329821907672759, + "gpu_bwutil_python": 0.6260126205513263, + "gpu_gap_stddev_us": 0.544450526525297 + } + }, + "cvtcolor_basic[InOutDataType=uint8][shape=128x1080x1920][code=RGB2HSV][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1762.09756388488, + "gpu_time_us_python": 1770.08291284534, + "gpu_noise_us_cpp": 2.05, + "gpu_noise_us_python": 5.070000000000001, + "gpu_bwutil_cpp": 0.5811278908592737, + "gpu_bwutil_python": 0.5785072108050506, + "gpu_gap_stddev_us": 2.274549013436695 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1650.3907492962, + "gpu_time_us_python": 1660.558170539425, + "gpu_noise_us_cpp": 0.885, + "gpu_noise_us_python": 2.7875, + "gpu_bwutil_cpp": 0.4732930438033812, + "gpu_bwutil_python": 0.4703953803668303, + "gpu_gap_stddev_us": 0.9722286572525818 + } + }, + "cvtcolor_basic[InOutDataType=uint8][shape=128x1080x1920][code=YUV2RGB][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1237.07977327802, + "gpu_time_us_python": 1243.6935809619602, + "gpu_noise_us_cpp": 2.586, + "gpu_noise_us_python": 3.156, + "gpu_bwutil_cpp": 0.8277559277082052, + "gpu_bwutil_python": 0.8233542687727378, + "gpu_gap_stddev_us": 1.1684618393912531 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1077.8218958076, + "gpu_time_us_python": 1085.386645828075, + "gpu_noise_us_cpp": 4.8375, + "gpu_noise_us_python": 4.65, + "gpu_bwutil_cpp": 0.7246254639694822, + "gpu_bwutil_python": 0.7195767510953872, + "gpu_gap_stddev_us": 1.7434595095723793 + } + }, + "cvtcolor_basic[InOutDataType=uint8][shape=128x1080x1920][code=YUV2RGB_NV12][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1045.7795224290599, + "gpu_time_us_python": 1052.24075591598, + "gpu_noise_us_cpp": 2.83, + "gpu_noise_us_python": 3.586, + "gpu_bwutil_cpp": 0.7344021457358779, + "gpu_bwutil_python": 0.7298875400432777, + "gpu_gap_stddev_us": 2.278385546744444 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 991.845825459975, + "gpu_time_us_python": 1002.7090453836751, + "gpu_noise_us_cpp": 3.045, + "gpu_noise_us_python": 6.9325, + "gpu_bwutil_cpp": 0.5906408565245764, + "gpu_bwutil_python": 0.58426594698687, + "gpu_gap_stddev_us": 3.9116972734913604 + } + } + } + }, + "cvtcolor_planar_nchw_1080p_basic": { + "tier": "basic", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "code": [ + "RGB2GRAY", + "RGB2HSV", + "YUV2RGB" + ], + "inputKind": [ + "Tensor" + ], + "layout": [ + "NCHW" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CvtColor native-planar 1080p branch sweep (basic)." + }, + "baselines": { + "cvtcolor_planar_nchw_1080p_basic[InOutDataType=uint8][shape=128x1080x1920][code=RGB2GRAY][inputKind=Tensor][layout=NCHW]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1072.6438422780798, + "gpu_time_us_python": 1081.8781787725202, + "gpu_noise_us_cpp": 0.9359999999999999, + "gpu_noise_us_python": 2.1220000000000003, + "gpu_bwutil_cpp": 0.6364369192473533, + "gpu_bwutil_python": 0.6310032066272357, + "gpu_gap_stddev_us": 0.9123465583589923 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 955.36252668715, + "gpu_time_us_python": 966.655054663475, + "gpu_noise_us_cpp": 3.485, + "gpu_noise_us_python": 3.5125, + "gpu_bwutil_cpp": 0.545066331453989, + "gpu_bwutil_python": 0.538697264380744, + "gpu_gap_stddev_us": 1.2172986659255114 + } + }, + "cvtcolor_planar_nchw_1080p_basic[InOutDataType=uint8][shape=128x1080x1920][code=RGB2HSV][inputKind=Tensor][layout=NCHW]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2020.9533384784202, + "gpu_time_us_python": 2030.87171364602, + "gpu_noise_us_cpp": 2.5780000000000003, + "gpu_noise_us_python": 3.774, + "gpu_bwutil_cpp": 0.5066952576380497, + "gpu_bwutil_python": 0.5042196414624363, + "gpu_gap_stddev_us": 2.209756294936353 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1882.60253693685, + "gpu_time_us_python": 1893.7558907551, + "gpu_noise_us_cpp": 1.8275, + "gpu_noise_us_python": 3.8874999999999997, + "gpu_bwutil_cpp": 0.4149189547638897, + "gpu_bwutil_python": 0.4124686946607881, + "gpu_gap_stddev_us": 1.6183067700809766 + } + }, + "cvtcolor_planar_nchw_1080p_basic[InOutDataType=uint8][shape=128x1080x1920][code=YUV2RGB][inputKind=Tensor][layout=NCHW]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1376.4787951668, + "gpu_time_us_python": 1387.82721842188, + "gpu_noise_us_cpp": 1.442, + "gpu_noise_us_python": 2.4859999999999998, + "gpu_bwutil_cpp": 0.7439282414913951, + "gpu_bwutil_python": 0.7378463927896458, + "gpu_gap_stddev_us": 2.8698415130136596 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1301.364358084225, + "gpu_time_us_python": 1309.386946596125, + "gpu_noise_us_cpp": 1.2225000000000001, + "gpu_noise_us_python": 5.857500000000001, + "gpu_bwutil_cpp": 0.6002271252452437, + "gpu_bwutil_python": 0.5965403795670079, + "gpu_gap_stddev_us": 2.8715180271154472 + } + } + } + }, + "cvtcolor_planar_tensor_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "code": [ + "RGB2GRAY", + "RGB2HSV", + "YUV2RGB" + ], + "inputKind": [ + "Tensor" + ], + "layout": [ + "NCHW", + "NCHW_FAKE" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CvtColor native/fake-planar 1080p tensor comparison (advanced)." + }, + "baselines": { + "cvtcolor_planar_tensor_1080p_advanced[InOutDataType=uint8][shape=128x1080x1920][code=RGB2GRAY][inputKind=Tensor][layout=NCHW]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1072.59075223358, + "gpu_time_us_python": 1081.450721944, + "gpu_noise_us_cpp": 1.3439999999999999, + "gpu_noise_us_python": 2.198, + "gpu_bwutil_cpp": 0.6364680567198666, + "gpu_bwutil_python": 0.6312529673091283, + "gpu_gap_stddev_us": 0.9487786903343584 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 956.644396368575, + "gpu_time_us_python": 966.9478657129499, + "gpu_noise_us_cpp": 1.7475, + "gpu_noise_us_python": 3.3075, + "gpu_bwutil_cpp": 0.5443345990633877, + "gpu_bwutil_python": 0.5385631421560606, + "gpu_gap_stddev_us": 3.7891588546362165 + } + }, + "cvtcolor_planar_tensor_1080p_advanced[InOutDataType=uint8][shape=128x1080x1920][code=RGB2GRAY][inputKind=Tensor][layout=NCHW_FAKE]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4469.200498717119, + "gpu_time_us_python": 4487.3288716588195, + "gpu_noise_us_cpp": 1.688, + "gpu_noise_us_python": 3.944, + "gpu_bwutil_cpp": 0.4582476489371906, + "gpu_bwutil_python": 0.456396303101186, + "gpu_gap_stddev_us": 1.4818060740972818 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 3525.50975626245, + "gpu_time_us_python": 3542.0462933014005, + "gpu_noise_us_cpp": 1.7200000000000002, + "gpu_noise_us_python": 4.7425, + "gpu_bwutil_cpp": 0.443075245410208, + "gpu_bwutil_python": 0.44100528496721236, + "gpu_gap_stddev_us": 1.9573688123954065 + } + }, + "cvtcolor_planar_tensor_1080p_advanced[InOutDataType=uint8][shape=128x1080x1920][code=RGB2HSV][inputKind=Tensor][layout=NCHW]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2020.96123446182, + "gpu_time_us_python": 2030.5969192103798, + "gpu_noise_us_cpp": 1.028, + "gpu_noise_us_python": 3.068, + "gpu_bwutil_cpp": 0.5066932941202916, + "gpu_bwutil_python": 0.5042880121651627, + "gpu_gap_stddev_us": 1.9009925126233076 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1882.7067028113001, + "gpu_time_us_python": 1894.4983761536, + "gpu_noise_us_cpp": 1.8975, + "gpu_noise_us_python": 3.7299999999999995, + "gpu_bwutil_cpp": 0.4148961498886621, + "gpu_bwutil_python": 0.41231284781195177, + "gpu_gap_stddev_us": 2.8555450374848683 + } + }, + "cvtcolor_planar_tensor_1080p_advanced[InOutDataType=uint8][shape=128x1080x1920][code=RGB2HSV][inputKind=Tensor][layout=NCHW_FAKE]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6479.00553972286, + "gpu_time_us_python": 6500.2469845306605, + "gpu_noise_us_cpp": 2.338, + "gpu_noise_us_python": 4.142, + "gpu_bwutil_cpp": 0.4741474408040898, + "gpu_bwutil_python": 0.4725978613333117, + "gpu_gap_stddev_us": 3.0554075177108313 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 5330.341593540676, + "gpu_time_us_python": 5346.3741344339505, + "gpu_noise_us_cpp": 2.885, + "gpu_noise_us_python": 4.61, + "gpu_bwutil_cpp": 0.4395836421364631, + "gpu_bwutil_python": 0.4382673453389623, + "gpu_gap_stddev_us": 2.5992173228379087 + } + }, + "cvtcolor_planar_tensor_1080p_advanced[InOutDataType=uint8][shape=128x1080x1920][code=YUV2RGB][inputKind=Tensor][layout=NCHW]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1376.82313185466, + "gpu_time_us_python": 1386.68381785816, + "gpu_noise_us_cpp": 1.6019999999999999, + "gpu_noise_us_python": 3.154, + "gpu_bwutil_cpp": 0.7437420541929146, + "gpu_bwutil_python": 0.7384526372215181, + "gpu_gap_stddev_us": 1.5982942280724035 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1301.58436357675, + "gpu_time_us_python": 1313.225789036825, + "gpu_noise_us_cpp": 1.41, + "gpu_noise_us_python": 3.4699999999999998, + "gpu_bwutil_cpp": 0.6001279445391241, + "gpu_bwutil_python": 0.5947972117506722, + "gpu_gap_stddev_us": 2.663433803479447 + } + }, + "cvtcolor_planar_tensor_1080p_advanced[InOutDataType=uint8][shape=128x1080x1920][code=YUV2RGB][inputKind=Tensor][layout=NCHW_FAKE]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5944.496294509479, + "gpu_time_us_python": 5962.7714633941205, + "gpu_noise_us_cpp": 1.53, + "gpu_noise_us_python": 4.26, + "gpu_bwutil_cpp": 0.5167806567830621, + "gpu_bwutil_python": 0.5151967553264326, + "gpu_gap_stddev_us": 2.6317189723555683 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 4722.90676515635, + "gpu_time_us_python": 4739.039859681699, + "gpu_noise_us_cpp": 3.2575000000000003, + "gpu_noise_us_python": 4.7225, + "gpu_bwutil_cpp": 0.49610518441855683, + "gpu_bwutil_python": 0.49441532365043805, + "gpu_gap_stddev_us": 3.992165457740067 + } + } + } + }, + "cvtcolor_4k_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "32x2160x3840" + ], + "code": [ + "RGB2GRAY", + "RGB2HSV", + "YUV2RGB", + "YUV2RGB_NV12" + ], + "inputKind": [ + "Tensor" + ], + "layout": [ + "NHWC" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CvtColor 4K branch sweep (advanced)." + }, + "baselines": { + "cvtcolor_4k_advanced[InOutDataType=uint8][shape=32x2160x3840][code=RGB2GRAY][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 915.61234557068, + "gpu_time_us_python": 924.8898343541001, + "gpu_noise_us_cpp": 0.738, + "gpu_noise_us_python": 2.7619999999999996, + "gpu_bwutil_cpp": 0.7455849858982893, + "gpu_bwutil_python": 0.7381066297802189, + "gpu_gap_stddev_us": 0.7559565470469707 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 821.3889461486749, + "gpu_time_us_python": 832.804498191375, + "gpu_noise_us_cpp": 2.74, + "gpu_noise_us_python": 2.9275, + "gpu_bwutil_cpp": 0.6339655432965366, + "gpu_bwutil_python": 0.6252616339996351, + "gpu_gap_stddev_us": 1.956258308698492 + } + }, + "cvtcolor_4k_advanced[InOutDataType=uint8][shape=32x2160x3840][code=RGB2HSV][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1761.5106892634599, + "gpu_time_us_python": 1771.2191675845002, + "gpu_noise_us_cpp": 0.908, + "gpu_noise_us_python": 2.1719999999999997, + "gpu_bwutil_cpp": 0.5813228036957507, + "gpu_bwutil_python": 0.5781362337937861, + "gpu_gap_stddev_us": 0.5949109975502168 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1651.4038231453, + "gpu_time_us_python": 1661.201426791875, + "gpu_noise_us_cpp": 1.6475, + "gpu_noise_us_python": 3.3674999999999997, + "gpu_bwutil_cpp": 0.4730042559242272, + "gpu_bwutil_python": 0.47021838437833724, + "gpu_gap_stddev_us": 1.5702631786915133 + } + }, + "cvtcolor_4k_advanced[InOutDataType=uint8][shape=32x2160x3840][code=YUV2RGB][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1237.4612075790199, + "gpu_time_us_python": 1244.05196335538, + "gpu_noise_us_cpp": 2.5620000000000003, + "gpu_noise_us_python": 2.772, + "gpu_bwutil_cpp": 0.8275007936946125, + "gpu_bwutil_python": 0.8231169004707232, + "gpu_gap_stddev_us": 0.6114726510125837 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1078.34482693975, + "gpu_time_us_python": 1085.138373917425, + "gpu_noise_us_cpp": 3.73, + "gpu_noise_us_python": 3.8449999999999998, + "gpu_bwutil_cpp": 0.7242739739057611, + "gpu_bwutil_python": 0.7197421187726806, + "gpu_gap_stddev_us": 1.9776561481893873 + } + }, + "cvtcolor_4k_advanced[InOutDataType=uint8][shape=32x2160x3840][code=YUV2RGB_NV12][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1050.8186684718798, + "gpu_time_us_python": 1053.19573878634, + "gpu_noise_us_cpp": 2.43, + "gpu_noise_us_python": 3.306, + "gpu_bwutil_cpp": 0.730875612860377, + "gpu_bwutil_python": 0.7292229407360863, + "gpu_gap_stddev_us": 4.337271395589536 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 984.8856615202249, + "gpu_time_us_python": 995.25966345525, + "gpu_noise_us_cpp": 2.9974999999999996, + "gpu_noise_us_python": 4.09, + "gpu_bwutil_cpp": 0.5948335805019194, + "gpu_bwutil_python": 0.5886464673503136, + "gpu_gap_stddev_us": 1.264069276625486 + } + } + } + }, + "cvtcolor_grayscale_tensor_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "256x1080x1920", + "64x2160x3840" + ], + "code": [ + "RGB2GRAY" + ], + "inputKind": [ + "Tensor" + ], + "layout": [ + "NHWC" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CvtColor single-channel grayscale tensor (advanced)." + }, + "baselines": { + "cvtcolor_grayscale_tensor_advanced[InOutDataType=uint8][shape=256x1080x1920][code=RGB2GRAY][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1812.53476474864, + "gpu_time_us_python": 1821.7702342115801, + "gpu_noise_us_cpp": 2.532, + "gpu_noise_us_python": 1.552, + "gpu_bwutil_cpp": 0.7532730191682956, + "gpu_bwutil_python": 0.7494544345087882, + "gpu_gap_stddev_us": 1.0148466335305477 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1629.465318603, + "gpu_time_us_python": 1641.6154306648, + "gpu_noise_us_cpp": 0.7025, + "gpu_noise_us_python": 3.705, + "gpu_bwutil_cpp": 0.6391562648072889, + "gpu_bwutil_python": 0.634425137145603, + "gpu_gap_stddev_us": 2.074044270587805 + } + }, + "cvtcolor_grayscale_tensor_advanced[InOutDataType=uint8][shape=64x2160x3840][code=RGB2GRAY][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1813.02314219262, + "gpu_time_us_python": 1822.56717163582, + "gpu_noise_us_cpp": 0.9099999999999999, + "gpu_noise_us_python": 1.8, + "gpu_bwutil_cpp": 0.7530700728121555, + "gpu_bwutil_python": 0.7491265761514173, + "gpu_gap_stddev_us": 0.8883651128899324 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1627.52047541665, + "gpu_time_us_python": 1640.1620166232499, + "gpu_noise_us_cpp": 0.9599999999999999, + "gpu_noise_us_python": 3.5625, + "gpu_bwutil_cpp": 0.6399194880679198, + "gpu_bwutil_python": 0.6349767516077196, + "gpu_gap_stddev_us": 2.3110143167700805 + } + } + } + }, + "cvtcolor_rgb2bgr_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "code": [ + "RGB2BGR" + ], + "inputKind": [ + "Tensor", + "VarShape" + ], + "layout": [ + "NHWC" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CvtColor single-channel rgb2bgr (advanced)." + }, + "baselines": { + "cvtcolor_rgb2bgr_1080p_advanced[InOutDataType=uint8][shape=128x1080x1920][code=RGB2BGR][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1238.54322091184, + "gpu_time_us_python": 1246.1794553111, + "gpu_noise_us_cpp": 1.94, + "gpu_noise_us_python": 2.242, + "gpu_bwutil_cpp": 0.8267778531636012, + "gpu_bwutil_python": 0.8217118432370505, + "gpu_gap_stddev_us": 0.6836778970084983 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1080.473291309875, + "gpu_time_us_python": 1089.88329243725, + "gpu_noise_us_cpp": 2.71, + "gpu_noise_us_python": 6.2124999999999995, + "gpu_bwutil_cpp": 0.7229095109485588, + "gpu_bwutil_python": 0.7166960766539656, + "gpu_gap_stddev_us": 4.086246527791532 + } + }, + "cvtcolor_rgb2bgr_1080p_advanced[InOutDataType=uint8][shape=128x1080x1920][code=RGB2BGR][inputKind=VarShape][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2487.95096472934, + "gpu_time_us_python": 2488.3382575345995, + "gpu_noise_us_cpp": 2.422, + "gpu_noise_us_python": 3.636, + "gpu_bwutil_cpp": 0.41158411422210933, + "gpu_bwutil_python": 0.41152018782490307, + "gpu_gap_stddev_us": 0.5819852312347943 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2063.169326516775, + "gpu_time_us_python": 2074.687187686225, + "gpu_noise_us_cpp": 3.225, + "gpu_noise_us_python": 4.96, + "gpu_bwutil_cpp": 0.3785815625968024, + "gpu_bwutil_python": 0.3764694747529147, + "gpu_gap_stddev_us": 4.4683296845855045 + } + } + } + }, + "cvtcolor_rgb2rgba_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "96x1080x1920" + ], + "code": [ + "RGB2RGBA" + ], + "inputKind": [ + "Tensor", + "VarShape" + ], + "layout": [ + "NHWC" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CvtColor single-channel rgb2rgba (advanced)." + }, + "baselines": { + "cvtcolor_rgb2rgba_1080p_advanced[InOutDataType=uint8][shape=96x1080x1920][code=RGB2RGBA][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1053.5437171198198, + "gpu_time_us_python": 1062.03487519486, + "gpu_noise_us_cpp": 1.9440000000000002, + "gpu_noise_us_python": 2.1439999999999997, + "gpu_bwutil_cpp": 0.8504631117287452, + "gpu_bwutil_python": 0.8436656503856936, + "gpu_gap_stddev_us": 1.6612251641817048 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 925.06736560935, + "gpu_time_us_python": 931.675674418425, + "gpu_noise_us_cpp": 2.8400000000000003, + "gpu_noise_us_python": 3.6024999999999996, + "gpu_bwutil_cpp": 0.7387462942863761, + "gpu_bwutil_python": 0.7335079815114953, + "gpu_gap_stddev_us": 2.273599803384843 + } + }, + "cvtcolor_rgb2rgba_1080p_advanced[InOutDataType=uint8][shape=96x1080x1920][code=RGB2RGBA][inputKind=VarShape][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2381.9416716627397, + "gpu_time_us_python": 2390.73578879942, + "gpu_noise_us_cpp": 2.4199999999999995, + "gpu_noise_us_python": 3.1559999999999997, + "gpu_bwutil_cpp": 0.37616421127576893, + "gpu_bwutil_python": 0.3747803240977712, + "gpu_gap_stddev_us": 1.3710185178449659 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1928.1985415193749, + "gpu_time_us_python": 1942.3039856718751, + "gpu_noise_us_cpp": 1.505, + "gpu_noise_us_python": 4.325, + "gpu_bwutil_cpp": 0.3545021208388319, + "gpu_bwutil_python": 0.35191773069451515, + "gpu_gap_stddev_us": 2.3523497205210773 + } + } + } + }, + "cvtcolor_rgba2rgb_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "96x1080x1920" + ], + "code": [ + "RGBA2RGB" + ], + "inputKind": [ + "Tensor", + "VarShape" + ], + "layout": [ + "NHWC" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CvtColor single-channel rgba2rgb (advanced)." + }, + "baselines": { + "cvtcolor_rgba2rgb_1080p_advanced[InOutDataType=uint8][shape=96x1080x1920][code=RGBA2RGB][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1014.45811982094, + "gpu_time_us_python": 1024.2036651276599, + "gpu_noise_us_cpp": 1.25, + "gpu_noise_us_python": 1.6380000000000003, + "gpu_bwutil_cpp": 0.8832301669347162, + "gpu_bwutil_python": 0.8748287644379971, + "gpu_gap_stddev_us": 1.9304830236131958 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 756.74256857065, + "gpu_time_us_python": 765.3829213816, + "gpu_noise_us_cpp": 1.975, + "gpu_noise_us_python": 3.25, + "gpu_bwutil_cpp": 0.9030689724360648, + "gpu_bwutil_python": 0.8928771909963217, + "gpu_gap_stddev_us": 1.2641381787360677 + } + }, + "cvtcolor_rgba2rgb_1080p_advanced[InOutDataType=uint8][shape=96x1080x1920][code=RGBA2RGB][inputKind=VarShape][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1877.69198008432, + "gpu_time_us_python": 1878.24299672192, + "gpu_noise_us_cpp": 3.8579999999999997, + "gpu_noise_us_python": 2.576, + "gpu_bwutil_cpp": 0.47718196735026747, + "gpu_bwutil_python": 0.47704296838926935, + "gpu_gap_stddev_us": 2.6226088817022033 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1554.361477228675, + "gpu_time_us_python": 1565.9986486331502, + "gpu_noise_us_cpp": 3.16, + "gpu_noise_us_python": 4.4675, + "gpu_bwutil_cpp": 0.43968529823478397, + "gpu_bwutil_python": 0.4364097223126949, + "gpu_gap_stddev_us": 2.7096288741989762 + } + } + } + }, + "cvtcolor_hsv2rgb_tensor_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "code": [ + "HSV2RGB" + ], + "inputKind": [ + "Tensor" + ], + "layout": [ + "NHWC" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CvtColor single-channel hsv2rgb tensor (advanced)." + }, + "baselines": { + "cvtcolor_hsv2rgb_tensor_1080p_advanced[InOutDataType=uint8][shape=128x1080x1920][code=HSV2RGB][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1610.60215582814, + "gpu_time_us_python": 1619.88769292528, + "gpu_noise_us_cpp": 1.05, + "gpu_noise_us_python": 3.996, + "gpu_bwutil_cpp": 0.6357911743087872, + "gpu_bwutil_python": 0.6321461947450933, + "gpu_gap_stddev_us": 0.6766332433517972 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1797.63660000495, + "gpu_time_us_python": 1808.0274759171252, + "gpu_noise_us_cpp": 1.9675000000000002, + "gpu_noise_us_python": 4.225, + "gpu_bwutil_cpp": 0.4345287526120402, + "gpu_bwutil_python": 0.4320293521615497, + "gpu_gap_stddev_us": 2.7058325309391815 + } + } + } + }, + "cvtcolor_hsv2rgb_varshape_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "code": [ + "HSV2RGB" + ], + "inputKind": [ + "VarShape" + ], + "layout": [ + "NHWC" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CvtColor single-channel hsv2rgb varshape (advanced)." + }, + "baselines": { + "cvtcolor_hsv2rgb_varshape_1080p_advanced[InOutDataType=uint8][shape=64x1080x1920][code=HSV2RGB][inputKind=VarShape][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1666.3843567734198, + "gpu_time_us_python": 1675.9234582582799, + "gpu_noise_us_cpp": 1.876, + "gpu_noise_us_python": 2.68, + "gpu_bwutil_cpp": 0.3072520840489529, + "gpu_bwutil_python": 0.30550327112513015, + "gpu_gap_stddev_us": 1.149501998285288 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1340.5777899539503, + "gpu_time_us_python": 1346.775592985975, + "gpu_noise_us_cpp": 1.6175, + "gpu_noise_us_python": 3.1625, + "gpu_bwutil_cpp": 0.291352145522202, + "gpu_bwutil_python": 0.29000343106706733, + "gpu_gap_stddev_us": 3.027132942775495 + } + } + } + }, + "cvtcolor_rgb_to_yuv_nv12_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "code": [ + "RGB2YUV", + "RGB2YUV_NV12" + ], + "inputKind": [ + "Tensor" + ], + "layout": [ + "NHWC" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CvtColor single-channel yuv nv12 (advanced)." + }, + "baselines": { + "cvtcolor_rgb_to_yuv_nv12_1080p_advanced[InOutDataType=uint8][shape=128x1080x1920][code=RGB2YUV][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1267.16057263158, + "gpu_time_us_python": 1276.3542986602201, + "gpu_noise_us_cpp": 2.1100000000000003, + "gpu_noise_us_python": 4.029999999999999, + "gpu_bwutil_cpp": 0.8081185075322512, + "gpu_bwutil_python": 0.8022897368769663, + "gpu_gap_stddev_us": 2.688745269420628 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1094.7938924593, + "gpu_time_us_python": 1106.34918808175, + "gpu_noise_us_cpp": 2.0425, + "gpu_noise_us_python": 3.4775, + "gpu_bwutil_cpp": 0.713432657033829, + "gpu_bwutil_python": 0.7060003011507456, + "gpu_gap_stddev_us": 3.2803485838499893 + } + }, + "cvtcolor_rgb_to_yuv_nv12_1080p_advanced[InOutDataType=uint8][shape=128x1080x1920][code=RGB2YUV_NV12][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1069.7289987931601, + "gpu_time_us_python": 1079.0425239961, + "gpu_noise_us_cpp": 1.7879999999999998, + "gpu_noise_us_python": 2.7659999999999996, + "gpu_bwutil_cpp": 0.7179397242172995, + "gpu_bwutil_python": 0.711744029833146, + "gpu_gap_stddev_us": 0.9101018622240522 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 953.13639720525, + "gpu_time_us_python": 965.7589122221, + "gpu_noise_us_cpp": 1.2025000000000001, + "gpu_noise_us_python": 3.3099999999999996, + "gpu_bwutil_cpp": 0.6146099060490273, + "gpu_bwutil_python": 0.6065745845986414, + "gpu_gap_stddev_us": 2.1606342873960482 + } + } + } + }, + "cvtcolor_rgb2rgba_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "code": [ + "RGB2RGBA" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CvtColor RGB to RGBA interleaved RGB basic." + }, + "baselines": { + "cvtcolor_rgb2rgba_uchar3_basic[InOutDataType=uchar3][shape=128x1080x1920][code=RGB2RGBA][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1401.56539059212, + "gpu_time_us_python": 1410.1987298954202, + "gpu_noise_us_cpp": 3.122, + "gpu_noise_us_python": 3.65, + "gpu_bwutil_cpp": 0.8523802797315895, + "gpu_bwutil_python": 0.847162366232115, + "gpu_gap_stddev_us": 1.2871700900352725 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1231.4176357390497, + "gpu_time_us_python": 1238.38407606635, + "gpu_noise_us_cpp": 3.1675000000000004, + "gpu_noise_us_python": 3.6225, + "gpu_bwutil_cpp": 0.7399495135765406, + "gpu_bwutil_python": 0.7357867577411394, + "gpu_gap_stddev_us": 0.6739787562417316 + } + }, + "cvtcolor_rgb2rgba_uchar3_basic[InOutDataType=uchar3][shape=128x1080x1920][code=RGB2RGBA][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3174.2678189579, + "gpu_time_us_python": 3183.0430284330596, + "gpu_noise_us_cpp": 2.4699999999999998, + "gpu_noise_us_python": 6.55, + "gpu_bwutil_cpp": 0.376360352815748, + "gpu_bwutil_python": 0.3753227436090783, + "gpu_gap_stddev_us": 1.5979618512212497 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2570.181756958725, + "gpu_time_us_python": 2584.234713650425, + "gpu_noise_us_cpp": 1.5925, + "gpu_noise_us_python": 4.2675, + "gpu_bwutil_cpp": 0.35461109865952234, + "gpu_bwutil_python": 0.3526812115490351, + "gpu_gap_stddev_us": 0.8156002490491371 + } + } + } + }, + "cvtcolor_rgb2rgba_planar_nchw_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "code": [ + "RGB2RGBA" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CvtColor RGB to RGBA native planar RGB basic." + }, + "baselines": { + "cvtcolor_rgb2rgba_planar_nchw_uchar3_basic[InOutDataType=uchar3][shape=128x1080x1920][code=RGB2RGBA][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1538.38224133094, + "gpu_time_us_python": 1547.68875292786, + "gpu_noise_us_cpp": 1.8359999999999999, + "gpu_noise_us_python": 2.398, + "gpu_bwutil_cpp": 0.7765738642590096, + "gpu_bwutil_python": 0.7719043927830305, + "gpu_gap_stddev_us": 0.8243126610565145 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1378.0385390121, + "gpu_time_us_python": 1389.4199511718502, + "gpu_noise_us_cpp": 1.435, + "gpu_noise_us_python": 5.92, + "gpu_bwutil_cpp": 0.6613046242189643, + "gpu_bwutil_python": 0.6559043410049176, + "gpu_gap_stddev_us": 1.9244519932711024 + } + }, + "cvtcolor_rgb2rgba_planar_nchw_uchar3_basic[InOutDataType=uchar3][shape=128x1080x1920][code=RGB2RGBA][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2824.7672938228598, + "gpu_time_us_python": 2838.3570461216596, + "gpu_noise_us_cpp": 2.028, + "gpu_noise_us_python": 2.66, + "gpu_bwutil_cpp": 0.4229285467914045, + "gpu_bwutil_python": 0.42090373561669897, + "gpu_gap_stddev_us": 1.7231250372789506 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2624.7606097842745, + "gpu_time_us_python": 2642.17953141845, + "gpu_noise_us_cpp": 2.4600000000000004, + "gpu_noise_us_python": 6.815000000000001, + "gpu_bwutil_cpp": 0.34718802573586194, + "gpu_bwutil_python": 0.34489479736332485, + "gpu_gap_stddev_us": 2.1995128814705533 + } + } + } + }, + "cvtcolor_rgb2rgba_planar_nchw_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "code": [ + "RGB2RGBA" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CvtColor RGB to RGBA native-planar tensor RGB comparison (advanced)." + }, + "baselines": { + "cvtcolor_rgb2rgba_planar_nchw_uchar3_advanced[InOutDataType=uchar3][shape=128x1080x1920][code=RGB2RGBA][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1538.7227311455597, + "gpu_time_us_python": 1548.38406598122, + "gpu_noise_us_cpp": 2.338, + "gpu_noise_us_python": 4.016, + "gpu_bwutil_cpp": 0.7764023284196411, + "gpu_bwutil_python": 0.7715577087330259, + "gpu_gap_stddev_us": 1.1276563988042083 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1378.19026263145, + "gpu_time_us_python": 1388.31352130455, + "gpu_noise_us_cpp": 1.4575, + "gpu_noise_us_python": 4.695, + "gpu_bwutil_cpp": 0.6612317736544067, + "gpu_bwutil_python": 0.6564152666332925, + "gpu_gap_stddev_us": 2.1233763005854316 + } + } + } + }, + "cvtcolor_rgb2rgba_fakeplanar_nchw_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "code": [ + "RGB2RGBA" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "CvtColor RGB to RGBA fake-planar tensor RGB advanced." + }, + "baselines": { + "cvtcolor_rgb2rgba_fakeplanar_nchw_uchar3_advanced[InOutDataType=uchar3][shape=128x1080x1920][code=RGB2RGBA][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6452.66507955692, + "gpu_time_us_python": 6469.11214437234, + "gpu_noise_us_cpp": 3.2, + "gpu_noise_us_python": 3.128, + "gpu_bwutil_cpp": 0.5554295245058736, + "gpu_bwutil_python": 0.5540173758269366, + "gpu_gap_stddev_us": 1.8769795190844325 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 5198.8870294726, + "gpu_time_us_python": 5214.821954416125, + "gpu_noise_us_cpp": 3.295, + "gpu_noise_us_python": 5.5575, + "gpu_bwutil_cpp": 0.5258009984578539, + "gpu_bwutil_python": 0.524193822777646, + "gpu_gap_stddev_us": 1.6989840600660793 + } + } + } + } + } +} diff --git a/bench/config/operators/erase.json b/bench/config/operators/erase.json new file mode 100644 index 000000000..8e6271dd4 --- /dev/null +++ b/bench/config/operators/erase.json @@ -0,0 +1,2341 @@ +{ + "benchmark": "erase", + "configs": { + "erase_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "randomMode": [ + "random" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "int64_axes": { + "numErase": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Erase RGB8 (basic)." + }, + "baselines": { + "erase_uchar3_basic[InOutDataType=uchar3][shape=64x1080x1920][randomMode=random][layout=NHWC][inputKind=Tensor][numErase=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 628.07610676226, + "gpu_time_us_python": 646.07076441096, + "gpu_noise_us_cpp": 3.122, + "gpu_noise_us_python": 9.884, + "gpu_bwutil_cpp": 0.8152069423842117, + "gpu_bwutil_python": 0.7925502564678606, + "gpu_gap_stddev_us": 3.536502943502092 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 486.20693456950005, + "gpu_time_us_python": 508.86217211078, + "gpu_noise_us_cpp": 10.458000000000002, + "gpu_noise_us_python": 7.002, + "gpu_bwutil_cpp": 0.8031767302116102, + "gpu_bwutil_python": 0.7674251778560377, + "gpu_gap_stddev_us": 2.5055451069050214 + } + }, + "erase_uchar3_basic[InOutDataType=uchar3][shape=64x1080x1920][randomMode=random][layout=NHWC][inputKind=VarShape][numErase=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 655.7552673029601, + "gpu_time_us_python": 679.50214647826, + "gpu_noise_us_cpp": 4.6979999999999995, + "gpu_noise_us_python": 4.978, + "gpu_bwutil_cpp": 0.7808359809627339, + "gpu_bwutil_python": 0.7536027048472432, + "gpu_gap_stddev_us": 3.264362346617661 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 524.0387320527799, + "gpu_time_us_python": 551.2177350407001, + "gpu_noise_us_cpp": 4.688, + "gpu_noise_us_python": 7.804, + "gpu_bwutil_cpp": 0.7451908976720738, + "gpu_bwutil_python": 0.7084578025545423, + "gpu_gap_stddev_us": 2.042594877032541 + } + } + } + }, + "erase_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "randomMode": [ + "random" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "int64_axes": { + "numErase": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Erase RGBA8 (advanced)." + }, + "baselines": { + "erase_uchar4_advanced[InOutDataType=uchar4][shape=64x1080x1920][randomMode=random][layout=NHWC][inputKind=Tensor][numErase=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 818.12930118348, + "gpu_time_us_python": 838.20370302416, + "gpu_noise_us_cpp": 4.130000000000001, + "gpu_noise_us_python": 6.101999999999999, + "gpu_bwutil_cpp": 0.834438391603836, + "gpu_bwutil_python": 0.8144610747920167, + "gpu_gap_stddev_us": 2.4259970322212108 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 629.4526006522999, + "gpu_time_us_python": 652.3300356945599, + "gpu_noise_us_cpp": 3.4760000000000004, + "gpu_noise_us_python": 6.508000000000001, + "gpu_bwutil_cpp": 0.8271921226113601, + "gpu_bwutil_python": 0.7981885665087913, + "gpu_gap_stddev_us": 2.4418116381537067 + } + }, + "erase_uchar4_advanced[InOutDataType=uchar4][shape=64x1080x1920][randomMode=random][layout=NHWC][inputKind=VarShape][numErase=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 843.3856975639999, + "gpu_time_us_python": 869.0365528387999, + "gpu_noise_us_cpp": 5.17, + "gpu_noise_us_python": 9.729999999999999, + "gpu_bwutil_cpp": 0.8094676878126579, + "gpu_bwutil_python": 0.7856362283480213, + "gpu_gap_stddev_us": 5.6295380648431665 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 659.64005092784, + "gpu_time_us_python": 687.2399244657399, + "gpu_noise_us_cpp": 5.61, + "gpu_noise_us_python": 6.593999999999999, + "gpu_bwutil_cpp": 0.7893378747832963, + "gpu_bwutil_python": 0.7576392419281252, + "gpu_gap_stddev_us": 2.2669924506863675 + } + } + } + }, + "erase_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "randomMode": [ + "random" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "numErase": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Erase RGBf32 (basic)." + }, + "baselines": { + "erase_float3_basic[InOutDataType=float3][shape=32x1080x1920][randomMode=random][layout=NHWC][inputKind=Tensor][numErase=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1199.40343028442, + "gpu_time_us_python": 1219.11268031252, + "gpu_noise_us_cpp": 12.594000000000001, + "gpu_noise_us_python": 5.548, + "gpu_bwutil_cpp": 0.8537645923115094, + "gpu_bwutil_python": 0.8399646548607256, + "gpu_gap_stddev_us": 1.688442524891457 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 915.6242606767198, + "gpu_time_us_python": 939.37031219312, + "gpu_noise_us_cpp": 3.8579999999999997, + "gpu_noise_us_python": 6.478, + "gpu_bwutil_cpp": 0.8529889202310084, + "gpu_bwutil_python": 0.8314273714643001, + "gpu_gap_stddev_us": 1.6187855695356388 + } + } + } + }, + "erase_float3_varshape_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "randomMode": [ + "random" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "int64_axes": { + "numErase": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Erase RGBf32 VarShape (advanced)." + }, + "baselines": { + "erase_float3_varshape_advanced[InOutDataType=float3][shape=32x1080x1920][randomMode=random][layout=NHWC][inputKind=VarShape][numErase=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1215.93817325046, + "gpu_time_us_python": 1238.4355697770602, + "gpu_noise_us_cpp": 6.175999999999999, + "gpu_noise_us_python": 5.922, + "gpu_bwutil_cpp": 0.8421605821365029, + "gpu_bwutil_python": 0.8268698954862419, + "gpu_gap_stddev_us": 2.8381366396592744 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 940.6345086113, + "gpu_time_us_python": 967.0302928820402, + "gpu_noise_us_cpp": 11.246, + "gpu_noise_us_python": 8.794, + "gpu_bwutil_cpp": 0.830309233892421, + "gpu_bwutil_python": 0.8076459946285321, + "gpu_gap_stddev_us": 1.1175403668741148 + } + } + } + }, + "erase_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "randomMode": [ + "random" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "int64_axes": { + "numErase": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Erase RGBAf32 (advanced)." + }, + "baselines": { + "erase_float4_advanced[InOutDataType=float4][shape=16x1080x1920][randomMode=random][layout=NHWC][inputKind=Tensor][numErase=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 817.29813873604, + "gpu_time_us_python": 838.4416146412199, + "gpu_noise_us_cpp": 3.568, + "gpu_noise_us_python": 6.978, + "gpu_bwutil_cpp": 0.8352845841564698, + "gpu_bwutil_python": 0.8142347753461964, + "gpu_gap_stddev_us": 2.914010465096582 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 629.0657293256999, + "gpu_time_us_python": 653.0056678360199, + "gpu_noise_us_cpp": 3.128, + "gpu_noise_us_python": 11.132, + "gpu_bwutil_cpp": 0.8277006894847508, + "gpu_bwutil_python": 0.7973570684699581, + "gpu_gap_stddev_us": 1.0548840081320703 + } + }, + "erase_float4_advanced[InOutDataType=float4][shape=16x1080x1920][randomMode=random][layout=NHWC][inputKind=VarShape][numErase=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 824.1583420475201, + "gpu_time_us_python": 847.8363589224, + "gpu_noise_us_cpp": 3.3920000000000003, + "gpu_noise_us_python": 5.258000000000001, + "gpu_bwutil_cpp": 0.8283378475925852, + "gpu_bwutil_python": 0.8052215795550672, + "gpu_gap_stddev_us": 3.459395779441504 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 642.2699278295601, + "gpu_time_us_python": 670.2393387136201, + "gpu_noise_us_cpp": 4.364, + "gpu_noise_us_python": 6.3420000000000005, + "gpu_bwutil_cpp": 0.8106871529934565, + "gpu_bwutil_python": 0.7768759007539981, + "gpu_gap_stddev_us": 4.407750922134459 + } + } + } + }, + "erase_scalar2_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "randomMode": [ + "random" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "int64_axes": { + "numErase": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Erase single-channel scalar2 (advanced)." + }, + "baselines": { + "erase_scalar2_advanced[InOutDataType=float32][shape=64x1080x1920][randomMode=random][layout=NHWC][inputKind=Tensor][numErase=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 817.9047235682399, + "gpu_time_us_python": 837.4699176246601, + "gpu_noise_us_cpp": 4.606, + "gpu_noise_us_python": 5.470000000000001, + "gpu_bwutil_cpp": 0.8346641942061577, + "gpu_bwutil_python": 0.8151657095766662, + "gpu_gap_stddev_us": 2.7062552797173276 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 629.9923714291399, + "gpu_time_us_python": 651.6802184881801, + "gpu_noise_us_cpp": 5.260000000000001, + "gpu_noise_us_python": 5.95, + "gpu_bwutil_cpp": 0.826483839955255, + "gpu_bwutil_python": 0.7989831507320483, + "gpu_gap_stddev_us": 1.7033080066352901 + } + }, + "erase_scalar2_advanced[InOutDataType=float32][shape=64x1080x1920][randomMode=random][layout=NHWC][inputKind=VarShape][numErase=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 842.66175804882, + "gpu_time_us_python": 867.51994861354, + "gpu_noise_us_cpp": 5.412000000000001, + "gpu_noise_us_python": 6.606, + "gpu_bwutil_cpp": 0.8101616833286425, + "gpu_bwutil_python": 0.7869949528609451, + "gpu_gap_stddev_us": 5.316322981533217 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 658.90531710154, + "gpu_time_us_python": 688.10166833074, + "gpu_noise_us_cpp": 10.186000000000002, + "gpu_noise_us_python": 8.99, + "gpu_bwutil_cpp": 0.7902176024598642, + "gpu_bwutil_python": 0.7566957333154357, + "gpu_gap_stddev_us": 2.744918585045044 + } + } + } + }, + "erase_scalar_u8_random_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "256x1080x1920" + ], + "randomMode": [ + "random" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "int64_axes": { + "numErase": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Erase single-channel random (advanced)." + }, + "baselines": { + "erase_scalar_u8_random_1080p_advanced[InOutDataType=uint8][shape=256x1080x1920][randomMode=random][layout=NHWC][inputKind=Tensor][numErase=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 819.2628430299399, + "gpu_time_us_python": 836.0874454570001, + "gpu_noise_us_cpp": 4.1659999999999995, + "gpu_noise_us_python": 4.172, + "gpu_bwutil_cpp": 0.8332812275976822, + "gpu_bwutil_python": 0.816519477454729, + "gpu_gap_stddev_us": 2.2627096688597974 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 629.24920322592, + "gpu_time_us_python": 660.7776439566401, + "gpu_noise_us_cpp": 11.546, + "gpu_noise_us_python": 11.274000000000001, + "gpu_bwutil_cpp": 0.8274594217580529, + "gpu_bwutil_python": 0.7882345490401219, + "gpu_gap_stddev_us": 13.145308123672585 + } + }, + "erase_scalar_u8_random_1080p_advanced[InOutDataType=uint8][shape=256x1080x1920][randomMode=random][layout=NHWC][inputKind=VarShape][numErase=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 909.4776255834, + "gpu_time_us_python": 939.3599442859601, + "gpu_noise_us_cpp": 10.818000000000001, + "gpu_noise_us_python": 6.356000000000001, + "gpu_bwutil_cpp": 0.7508781489707207, + "gpu_bwutil_python": 0.727140407192796, + "gpu_gap_stddev_us": 10.775155511914248 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 740.58922848446, + "gpu_time_us_python": 791.02804258488, + "gpu_noise_us_cpp": 13.958000000000002, + "gpu_noise_us_python": 19.14, + "gpu_bwutil_cpp": 0.7030609990253651, + "gpu_bwutil_python": 0.6584099288025985, + "gpu_gap_stddev_us": 15.675213358226163 + } + } + } + }, + "erase_scalar_f32_random_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "randomMode": [ + "random" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "int64_axes": { + "numErase": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Erase single-channel random (advanced)." + }, + "baselines": { + "erase_scalar_f32_random_1080p_advanced[InOutDataType=float32][shape=128x1080x1920][randomMode=random][layout=NHWC][inputKind=Tensor][numErase=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1578.96935309998, + "gpu_time_us_python": 1599.75850815132, + "gpu_noise_us_cpp": 3.7619999999999996, + "gpu_noise_us_python": 8.54, + "gpu_bwutil_cpp": 0.8647021180781245, + "gpu_bwutil_python": 0.8534730745446483, + "gpu_gap_stddev_us": 4.755344018942898 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1207.50089754058, + "gpu_time_us_python": 1228.68482073248, + "gpu_noise_us_cpp": 4.0840000000000005, + "gpu_noise_us_python": 8.918, + "gpu_bwutil_cpp": 0.8624061269024065, + "gpu_bwutil_python": 0.8475376817038025, + "gpu_gap_stddev_us": 1.5738601025146794 + } + }, + "erase_scalar_f32_random_1080p_advanced[InOutDataType=float32][shape=128x1080x1920][randomMode=random][layout=NHWC][inputKind=VarShape][numErase=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1626.7286481462402, + "gpu_time_us_python": 1656.58917008908, + "gpu_noise_us_cpp": 6.664, + "gpu_noise_us_python": 7.07, + "gpu_bwutil_cpp": 0.8393413157312908, + "gpu_bwutil_python": 0.8242469965465317, + "gpu_gap_stddev_us": 6.528901620842393 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1262.3560779026798, + "gpu_time_us_python": 1291.8938986635799, + "gpu_noise_us_cpp": 6.630000000000001, + "gpu_noise_us_python": 5.236000000000001, + "gpu_bwutil_cpp": 0.8249327884409539, + "gpu_bwutil_python": 0.8060783738988657, + "gpu_gap_stddev_us": 5.154917639398943 + } + } + } + }, + "erase_scalar_u8_constant_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "256x1080x1920" + ], + "randomMode": [ + "constant" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "int64_axes": { + "numErase": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Erase single-channel constant (advanced)." + }, + "baselines": { + "erase_scalar_u8_constant_1080p_advanced[InOutDataType=uint8][shape=256x1080x1920][randomMode=constant][layout=NHWC][inputKind=Tensor][numErase=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 817.93515232844, + "gpu_time_us_python": 836.76847548564, + "gpu_noise_us_cpp": 3.6879999999999997, + "gpu_noise_us_python": 4.876, + "gpu_bwutil_cpp": 0.8346376877535656, + "gpu_bwutil_python": 0.8158659098099109, + "gpu_gap_stddev_us": 3.1811201546494834 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 629.4689689310401, + "gpu_time_us_python": 658.12595896344, + "gpu_noise_us_cpp": 3.718, + "gpu_noise_us_python": 7.7459999999999996, + "gpu_bwutil_cpp": 0.8271716199106299, + "gpu_bwutil_python": 0.7914633198695725, + "gpu_gap_stddev_us": 13.927888447065184 + } + }, + "erase_scalar_u8_constant_1080p_advanced[InOutDataType=uint8][shape=256x1080x1920][randomMode=constant][layout=NHWC][inputKind=VarShape][numErase=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 907.06671872552, + "gpu_time_us_python": 944.5495193106801, + "gpu_noise_us_cpp": 7.808, + "gpu_noise_us_python": 18.187999999999995, + "gpu_bwutil_cpp": 0.7528060546812776, + "gpu_bwutil_python": 0.7233053829070418, + "gpu_gap_stddev_us": 14.653950548136569 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 741.0722499487799, + "gpu_time_us_python": 784.0939389127, + "gpu_noise_us_cpp": 15.918000000000001, + "gpu_noise_us_python": 9.266, + "gpu_bwutil_cpp": 0.702603990636238, + "gpu_bwutil_python": 0.6642865728003118, + "gpu_gap_stddev_us": 16.9786082831547 + } + } + } + }, + "erase_scalar_f32_constant_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "randomMode": [ + "constant" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "int64_axes": { + "numErase": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Erase single-channel constant (advanced)." + }, + "baselines": { + "erase_scalar_f32_constant_1080p_advanced[InOutDataType=float32][shape=128x1080x1920][randomMode=constant][layout=NHWC][inputKind=Tensor][numErase=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1579.0080371455601, + "gpu_time_us_python": 1599.3339821818001, + "gpu_noise_us_cpp": 4.768, + "gpu_noise_us_python": 5.712000000000001, + "gpu_bwutil_cpp": 0.8646822920087376, + "gpu_bwutil_python": 0.853696749238078, + "gpu_gap_stddev_us": 2.345405182955745 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1207.8927121888003, + "gpu_time_us_python": 1228.95250191872, + "gpu_noise_us_cpp": 21.887999999999998, + "gpu_noise_us_python": 5.236, + "gpu_bwutil_cpp": 0.8621272881934153, + "gpu_bwutil_python": 0.8473544619865502, + "gpu_gap_stddev_us": 2.9378531606965357 + } + }, + "erase_scalar_f32_constant_1080p_advanced[InOutDataType=float32][shape=128x1080x1920][randomMode=constant][layout=NHWC][inputKind=VarShape][numErase=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1625.9384127924798, + "gpu_time_us_python": 1655.6237525450001, + "gpu_noise_us_cpp": 8.828, + "gpu_noise_us_python": 7.144, + "gpu_bwutil_cpp": 0.8397456535326757, + "gpu_bwutil_python": 0.8247242005968204, + "gpu_gap_stddev_us": 7.419308075429377 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1262.5037598479798, + "gpu_time_us_python": 1291.71316095768, + "gpu_noise_us_cpp": 7.2459999999999996, + "gpu_noise_us_python": 15.472, + "gpu_bwutil_cpp": 0.8248370709252034, + "gpu_bwutil_python": 0.8061844192208436, + "gpu_gap_stddev_us": 4.975565978347661 + } + } + } + }, + "erase_rgb_u8_random_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "randomMode": [ + "random" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "int64_axes": { + "numErase": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Erase RGB8 random (advanced)." + }, + "baselines": { + "erase_rgb_u8_random_1080p_advanced[InOutDataType=uchar3][shape=128x1080x1920][randomMode=random][layout=NHWC][inputKind=Tensor][numErase=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1199.2451422766799, + "gpu_time_us_python": 1217.95901061732, + "gpu_noise_us_cpp": 3.903999999999999, + "gpu_noise_us_python": 5.192, + "gpu_bwutil_cpp": 0.8538767616987026, + "gpu_bwutil_python": 0.8407666272056356, + "gpu_gap_stddev_us": 2.971202806114918 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 915.2780377081399, + "gpu_time_us_python": 937.8822074728799, + "gpu_noise_us_cpp": 3.2020000000000004, + "gpu_noise_us_python": 5.5200000000000005, + "gpu_bwutil_cpp": 0.8533113394602146, + "gpu_bwutil_python": 0.8327491251909362, + "gpu_gap_stddev_us": 2.1695003717219166 + } + }, + "erase_rgb_u8_random_1080p_advanced[InOutDataType=uchar3][shape=128x1080x1920][randomMode=random][layout=NHWC][inputKind=VarShape][numErase=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1255.1970066784, + "gpu_time_us_python": 1283.98032369816, + "gpu_noise_us_cpp": 20.273999999999997, + "gpu_noise_us_python": 7.461999999999999, + "gpu_bwutil_cpp": 0.8158555624229855, + "gpu_bwutil_python": 0.7976024871782013, + "gpu_gap_stddev_us": 5.687795730107651 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 990.37931333652, + "gpu_time_us_python": 1020.3246957939198, + "gpu_noise_us_cpp": 8.514, + "gpu_noise_us_python": 9.502, + "gpu_bwutil_cpp": 0.7886068183239725, + "gpu_bwutil_python": 0.7655609601310365, + "gpu_gap_stddev_us": 12.375847527769732 + } + } + } + }, + "erase_planar_nchw_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "randomMode": [ + "random" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "int64_axes": { + "numErase": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Erase RGB8 (basic) (planar NCHW)." + }, + "baselines": { + "erase_planar_nchw_uchar3_basic[InOutDataType=uchar3][shape=64x1080x1920][randomMode=random][layout=NCHW][inputKind=Tensor][numErase=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 628.29862780174, + "gpu_time_us_python": 648.4137633924199, + "gpu_noise_us_cpp": 4.11, + "gpu_noise_us_python": 5.312, + "gpu_bwutil_cpp": 0.8149241705645878, + "gpu_bwutil_python": 0.789664296189868, + "gpu_gap_stddev_us": 2.0641237269967827 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 485.60096536088, + "gpu_time_us_python": 509.0153785687401, + "gpu_noise_us_cpp": 3.4539999999999997, + "gpu_noise_us_python": 16.59, + "gpu_bwutil_cpp": 0.8041759251541002, + "gpu_bwutil_python": 0.7672143941873278, + "gpu_gap_stddev_us": 3.4193595525408154 + } + }, + "erase_planar_nchw_uchar3_basic[InOutDataType=uchar3][shape=64x1080x1920][randomMode=random][layout=NCHW][inputKind=VarShape][numErase=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 656.22998244056, + "gpu_time_us_python": 682.23082479472, + "gpu_noise_us_cpp": 4.762, + "gpu_noise_us_python": 4.794, + "gpu_bwutil_cpp": 0.7802815803400396, + "gpu_bwutil_python": 0.7505729691076469, + "gpu_gap_stddev_us": 3.0299372021576043 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 528.5045792097201, + "gpu_time_us_python": 554.87613379554, + "gpu_noise_us_cpp": 4.119999999999999, + "gpu_noise_us_python": 5.782, + "gpu_bwutil_cpp": 0.7388945465236534, + "gpu_bwutil_python": 0.7037821688218799, + "gpu_gap_stddev_us": 2.420513770513943 + } + } + } + }, + "erase_planar_nchw_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "randomMode": [ + "random" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "numErase": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Erase RGB8 native-planar Tensor comparison (advanced)." + }, + "baselines": { + "erase_planar_nchw_uchar3_advanced[InOutDataType=uchar3][shape=64x1080x1920][randomMode=random][layout=NCHW][inputKind=Tensor][numErase=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 627.7731097315, + "gpu_time_us_python": 646.9351648883601, + "gpu_noise_us_cpp": 3.076, + "gpu_noise_us_python": 5.954, + "gpu_bwutil_cpp": 0.8155995608119921, + "gpu_bwutil_python": 0.7914727055972766, + "gpu_gap_stddev_us": 4.569419375837317 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 486.01983692446004, + "gpu_time_us_python": 507.90992563086, + "gpu_noise_us_cpp": 3.6980000000000004, + "gpu_noise_us_python": 7.214, + "gpu_bwutil_cpp": 0.8034846980246482, + "gpu_bwutil_python": 0.7688584531401086, + "gpu_gap_stddev_us": 1.1414834913847458 + } + } + } + }, + "erase_fakeplanar_nchw_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "randomMode": [ + "random" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "numErase": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Erase RGB8 (advanced) (fake planar NCHW_FAKE tensor-only)." + }, + "baselines": { + "erase_fakeplanar_nchw_uchar3_advanced[InOutDataType=uchar3][shape=64x1080x1920][randomMode=random][layout=NCHW_FAKE][inputKind=Tensor][numErase=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2997.4600860050195, + "gpu_time_us_python": 3032.3674897112796, + "gpu_noise_us_cpp": 6.459999999999999, + "gpu_noise_us_python": 9.92, + "gpu_bwutil_cpp": 0.5124359334513658, + "gpu_bwutil_python": 0.5065376323573837, + "gpu_gap_stddev_us": 4.6808694023887565 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2337.9245953850004, + "gpu_time_us_python": 2368.9193726678604, + "gpu_noise_us_cpp": 5.372, + "gpu_noise_us_python": 6.843999999999999, + "gpu_bwutil_cpp": 0.5010994372379622, + "gpu_bwutil_python": 0.4945436764453367, + "gpu_gap_stddev_us": 2.5955271218732467 + } + } + } + }, + "erase_planar_nchw_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "randomMode": [ + "random" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "numErase": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Erase RGBA8 (advanced) (planar NCHW tensor-only)." + }, + "baselines": { + "erase_planar_nchw_uchar4_advanced[InOutDataType=uchar4][shape=64x1080x1920][randomMode=random][layout=NCHW][inputKind=Tensor][numErase=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 818.68665974076, + "gpu_time_us_python": 837.4993526825599, + "gpu_noise_us_cpp": 4.038, + "gpu_noise_us_python": 5.19, + "gpu_bwutil_cpp": 0.8338697460944168, + "gpu_bwutil_python": 0.8151533340016573, + "gpu_gap_stddev_us": 3.425505719881372 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 629.0269909192, + "gpu_time_us_python": 651.1955747722801, + "gpu_noise_us_cpp": 3.35, + "gpu_noise_us_python": 10.335999999999999, + "gpu_bwutil_cpp": 0.8277520166157695, + "gpu_bwutil_python": 0.7995735303835539, + "gpu_gap_stddev_us": 0.7394702086969049 + } + } + } + }, + "erase_fakeplanar_nchw_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "randomMode": [ + "random" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "numErase": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Erase RGBA8 (advanced) (fake planar NCHW_FAKE tensor-only)." + }, + "baselines": { + "erase_fakeplanar_nchw_uchar4_advanced[InOutDataType=uchar4][shape=64x1080x1920][randomMode=random][layout=NCHW_FAKE][inputKind=Tensor][numErase=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3882.8284094883397, + "gpu_time_us_python": 3917.295734206, + "gpu_noise_us_cpp": 5.093999999999999, + "gpu_noise_us_python": 6.353999999999999, + "gpu_bwutil_cpp": 0.5274517921590312, + "gpu_bwutil_python": 0.5228173693712587, + "gpu_gap_stddev_us": 11.902705171371643 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3033.812277001, + "gpu_time_us_python": 3064.2514734733, + "gpu_noise_us_cpp": 8.1, + "gpu_noise_us_python": 6.812, + "gpu_bwutil_cpp": 0.5148785600709364, + "gpu_bwutil_python": 0.5097643553761861, + "gpu_gap_stddev_us": 4.580667581156248 + } + } + } + }, + "erase_planar_nchw_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "randomMode": [ + "random" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "numErase": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Erase RGBf32 (basic) (planar NCHW)." + }, + "baselines": { + "erase_planar_nchw_float3_basic[InOutDataType=float3][shape=32x1080x1920][randomMode=random][layout=NCHW][inputKind=Tensor][numErase=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1199.45742618682, + "gpu_time_us_python": 1218.81042849018, + "gpu_noise_us_cpp": 4.996, + "gpu_noise_us_python": 6.378, + "gpu_bwutil_cpp": 0.8537256339254533, + "gpu_bwutil_python": 0.8401764453792916, + "gpu_gap_stddev_us": 2.771003779175924 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 915.49430845034, + "gpu_time_us_python": 946.12580287392, + "gpu_noise_us_cpp": 4.392, + "gpu_noise_us_python": 6.484, + "gpu_bwutil_cpp": 0.8531099132341129, + "gpu_bwutil_python": 0.8256964955576584, + "gpu_gap_stddev_us": 17.220682781366843 + } + } + } + }, + "erase_planar_nchw_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "randomMode": [ + "random" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "numErase": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Erase RGBf32 native-planar Tensor comparison (advanced)." + }, + "baselines": { + "erase_planar_nchw_float3_advanced[InOutDataType=float3][shape=32x1080x1920][randomMode=random][layout=NCHW][inputKind=Tensor][numErase=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1199.8748416677001, + "gpu_time_us_python": 1220.40850235142, + "gpu_noise_us_cpp": 4.134, + "gpu_noise_us_python": 6.715999999999999, + "gpu_bwutil_cpp": 0.8534276471576938, + "gpu_bwutil_python": 0.8390744823189197, + "gpu_gap_stddev_us": 4.425285947102962 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 915.6210480564599, + "gpu_time_us_python": 945.724701241, + "gpu_noise_us_cpp": 3.8720000000000008, + "gpu_noise_us_python": 8.484, + "gpu_bwutil_cpp": 0.8529942596689617, + "gpu_bwutil_python": 0.8259752079416897, + "gpu_gap_stddev_us": 14.093300783205423 + } + } + } + }, + "erase_planar_nchw_float3_varshape_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "randomMode": [ + "random" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "VarShape" + ] + }, + "int64_axes": { + "numErase": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Erase RGBf32 VarShape (advanced) (planar NCHW)." + }, + "baselines": { + "erase_planar_nchw_float3_varshape_advanced[InOutDataType=float3][shape=32x1080x1920][randomMode=random][layout=NCHW][inputKind=VarShape][numErase=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1210.6220023393798, + "gpu_time_us_python": 1234.65850030372, + "gpu_noise_us_cpp": 4.686, + "gpu_noise_us_python": 7.635999999999998, + "gpu_bwutil_cpp": 0.8458559885504284, + "gpu_bwutil_python": 0.8294012063040066, + "gpu_gap_stddev_us": 5.557005153009993 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 947.7748845120999, + "gpu_time_us_python": 976.9495349646801, + "gpu_noise_us_cpp": 4.94, + "gpu_noise_us_python": 23.188, + "gpu_bwutil_cpp": 0.8240557671521038, + "gpu_bwutil_python": 0.7994643917712023, + "gpu_gap_stddev_us": 6.518168839944194 + } + } + } + }, + "erase_fakeplanar_nchw_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "randomMode": [ + "random" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "numErase": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Erase RGBf32 (advanced) (fake planar NCHW_FAKE tensor-only)." + }, + "baselines": { + "erase_fakeplanar_nchw_float3_advanced[InOutDataType=float3][shape=32x1080x1920][randomMode=random][layout=NCHW_FAKE][inputKind=Tensor][numErase=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3883.738984328, + "gpu_time_us_python": 3921.22559025878, + "gpu_noise_us_cpp": 5.116, + "gpu_noise_us_python": 10.764, + "gpu_bwutil_cpp": 0.7909914746361478, + "gpu_bwutil_python": 0.7834360477987741, + "gpu_gap_stddev_us": 11.409799515061534 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3116.2896627261, + "gpu_time_us_python": 3144.9571204185004, + "gpu_noise_us_cpp": 4.466, + "gpu_noise_us_python": 7.846000000000001, + "gpu_bwutil_cpp": 0.7518720209498057, + "gpu_bwutil_python": 0.7450186320299459, + "gpu_gap_stddev_us": 1.8569693250945851 + } + } + } + }, + "erase_planar_nchw_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "randomMode": [ + "random" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "int64_axes": { + "numErase": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Erase RGBAf32 (advanced) (planar NCHW)." + }, + "baselines": { + "erase_planar_nchw_float4_advanced[InOutDataType=float4][shape=16x1080x1920][randomMode=random][layout=NCHW][inputKind=Tensor][numErase=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 817.7897870659, + "gpu_time_us_python": 837.13158317202, + "gpu_noise_us_cpp": 4.306, + "gpu_noise_us_python": 5.584, + "gpu_bwutil_cpp": 0.8347851034591223, + "gpu_bwutil_python": 0.8155086713455963, + "gpu_gap_stddev_us": 3.1531568893626525 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 629.39996162646, + "gpu_time_us_python": 650.35192015456, + "gpu_noise_us_cpp": 3.478, + "gpu_noise_us_python": 5.531999999999999, + "gpu_bwutil_cpp": 0.8272618619390606, + "gpu_bwutil_python": 0.8006204029659145, + "gpu_gap_stddev_us": 2.4242298800067252 + } + }, + "erase_planar_nchw_float4_advanced[InOutDataType=float4][shape=16x1080x1920][randomMode=random][layout=NCHW][inputKind=VarShape][numErase=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 826.79258881888, + "gpu_time_us_python": 849.2488305717801, + "gpu_noise_us_cpp": 3.768, + "gpu_noise_us_python": 5.604000000000001, + "gpu_bwutil_cpp": 0.8257013189036666, + "gpu_bwutil_python": 0.8038817609249665, + "gpu_gap_stddev_us": 2.7983894876976634 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 652.87902840618, + "gpu_time_us_python": 683.7312337863399, + "gpu_noise_us_cpp": 16.218, + "gpu_noise_us_python": 8.382, + "gpu_bwutil_cpp": 0.7975127576005747, + "gpu_bwutil_python": 0.7616388745775049, + "gpu_gap_stddev_us": 10.505272211555681 + } + } + } + }, + "erase_fakeplanar_nchw_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "randomMode": [ + "random" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "numErase": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Erase RGBAf32 (advanced) (fake planar NCHW_FAKE tensor-only)." + }, + "baselines": { + "erase_fakeplanar_nchw_float4_advanced[InOutDataType=float4][shape=16x1080x1920][randomMode=random][layout=NCHW_FAKE][inputKind=Tensor][numErase=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2937.0796275693, + "gpu_time_us_python": 2965.73527784904, + "gpu_noise_us_cpp": 5.122, + "gpu_noise_us_python": 8.01, + "gpu_bwutil_cpp": 0.6972916523285724, + "gpu_bwutil_python": 0.6905559212547272, + "gpu_gap_stddev_us": 3.9877501786745135 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2389.1624868483204, + "gpu_time_us_python": 2419.8832640280602, + "gpu_noise_us_cpp": 8.294, + "gpu_noise_us_python": 9.366000000000001, + "gpu_bwutil_cpp": 0.6538001580496113, + "gpu_bwutil_python": 0.6455012871582158, + "gpu_gap_stddev_us": 4.613932101087182 + } + } + } + }, + "erase_planar_nchw_scalar2_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "randomMode": [ + "random" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "numErase": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Erase single-channel scalar2 (advanced) (planar NCHW tensor-only)." + }, + "baselines": { + "erase_planar_nchw_scalar2_advanced[InOutDataType=float32][shape=64x1080x1920][randomMode=random][layout=NCHW][inputKind=Tensor][numErase=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 819.6678148196801, + "gpu_time_us_python": 836.6933125314401, + "gpu_noise_us_cpp": 4.9239999999999995, + "gpu_noise_us_python": 4.402, + "gpu_bwutil_cpp": 0.832878225519598, + "gpu_bwutil_python": 0.8159268145038965, + "gpu_gap_stddev_us": 2.4661441234296935 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 629.78498111182, + "gpu_time_us_python": 651.7141455861799, + "gpu_noise_us_cpp": 3.7760000000000007, + "gpu_noise_us_python": 7.529999999999999, + "gpu_bwutil_cpp": 0.826756224366831, + "gpu_bwutil_python": 0.7989369591074551, + "gpu_gap_stddev_us": 0.5931789444554983 + } + } + } + }, + "erase_fakeplanar_nchw_scalar2_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "randomMode": [ + "random" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "numErase": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Erase single-channel scalar2 (advanced) (fake planar NCHW_FAKE tensor-only)." + }, + "baselines": { + "erase_fakeplanar_nchw_scalar2_advanced[InOutDataType=float32][shape=64x1080x1920][randomMode=random][layout=NCHW_FAKE][inputKind=Tensor][numErase=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2548.39194486908, + "gpu_time_us_python": 2574.47091082428, + "gpu_noise_us_cpp": 6.3660000000000005, + "gpu_noise_us_python": 8.040000000000001, + "gpu_bwutil_cpp": 0.8036465005832396, + "gpu_bwutil_python": 0.7955094002418995, + "gpu_gap_stddev_us": 4.272800272649595 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2246.61243180122, + "gpu_time_us_python": 2277.0846883555205, + "gpu_noise_us_cpp": 5.098, + "gpu_noise_us_python": 10.558, + "gpu_bwutil_cpp": 0.6952887626850341, + "gpu_bwutil_python": 0.6859904279537543, + "gpu_gap_stddev_us": 5.482880933218806 + } + } + } + }, + "erase_planar_nchw_scalar_u8_random_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "256x1080x1920" + ], + "randomMode": [ + "random" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "numErase": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Erase single-channel random (advanced) (planar NCHW tensor-only)." + }, + "baselines": { + "erase_planar_nchw_scalar_u8_random_1080p_advanced[InOutDataType=uint8][shape=256x1080x1920][randomMode=random][layout=NCHW][inputKind=Tensor][numErase=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 817.8040899396799, + "gpu_time_us_python": 836.64379448412, + "gpu_noise_us_cpp": 4.266, + "gpu_noise_us_python": 4.91, + "gpu_bwutil_cpp": 0.8347688788423729, + "gpu_bwutil_python": 0.8159771986927774, + "gpu_gap_stddev_us": 3.3457127804086344 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 629.2265099448, + "gpu_time_us_python": 652.8485175817201, + "gpu_noise_us_cpp": 4.05, + "gpu_noise_us_python": 6.8180000000000005, + "gpu_bwutil_cpp": 0.8274892444781425, + "gpu_bwutil_python": 0.7975528040472412, + "gpu_gap_stddev_us": 1.9085292568420968 + } + } + } + }, + "erase_fakeplanar_nchw_scalar_u8_random_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "256x1080x1920" + ], + "randomMode": [ + "random" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "numErase": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Erase single-channel random (advanced) (fake planar NCHW_FAKE tensor-only)." + }, + "baselines": { + "erase_fakeplanar_nchw_scalar_u8_random_1080p_advanced[InOutDataType=uint8][shape=256x1080x1920][randomMode=random][layout=NCHW_FAKE][inputKind=Tensor][numErase=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2351.1958870932003, + "gpu_time_us_python": 2378.5618709382, + "gpu_noise_us_cpp": 6.496, + "gpu_noise_us_python": 7.639999999999999, + "gpu_bwutil_cpp": 0.8710483056285895, + "gpu_bwutil_python": 0.8610292078704834, + "gpu_gap_stddev_us": 6.567637713089925 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1791.8262878485402, + "gpu_time_us_python": 1825.32124725116, + "gpu_noise_us_cpp": 4.872, + "gpu_noise_us_python": 9.186, + "gpu_bwutil_cpp": 0.8717554709579645, + "gpu_bwutil_python": 0.8557623141479056, + "gpu_gap_stddev_us": 4.01480312859759 + } + } + } + }, + "erase_planar_nchw_scalar_f32_random_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "randomMode": [ + "random" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "numErase": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Erase single-channel random (advanced) (planar NCHW tensor-only)." + }, + "baselines": { + "erase_planar_nchw_scalar_f32_random_1080p_advanced[InOutDataType=float32][shape=128x1080x1920][randomMode=random][layout=NCHW][inputKind=Tensor][numErase=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1580.13246764172, + "gpu_time_us_python": 1601.0530707168803, + "gpu_noise_us_cpp": 5.386, + "gpu_noise_us_python": 7.76, + "gpu_bwutil_cpp": 0.8640653523915841, + "gpu_bwutil_python": 0.852783939203434, + "gpu_gap_stddev_us": 4.852929614166994 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1207.0486714508202, + "gpu_time_us_python": 1230.5306970059598, + "gpu_noise_us_cpp": 3.524, + "gpu_noise_us_python": 5.526, + "gpu_bwutil_cpp": 0.8627293431685802, + "gpu_bwutil_python": 0.8462661616094976, + "gpu_gap_stddev_us": 1.5537636869703269 + } + } + } + }, + "erase_fakeplanar_nchw_scalar_f32_random_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "randomMode": [ + "random" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "numErase": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Erase single-channel random (advanced) (fake planar NCHW_FAKE tensor-only)." + }, + "baselines": { + "erase_fakeplanar_nchw_scalar_f32_random_1080p_advanced[InOutDataType=float32][shape=128x1080x1920][randomMode=random][layout=NCHW_FAKE][inputKind=Tensor][numErase=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5025.2799930572, + "gpu_time_us_python": 5060.28850173946, + "gpu_noise_us_cpp": 8.112, + "gpu_noise_us_python": 10.129999999999999, + "gpu_bwutil_cpp": 0.8150796644446, + "gpu_bwutil_python": 0.8094483232136918, + "gpu_gap_stddev_us": 15.87100470920313 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4423.60645762658, + "gpu_time_us_python": 4455.227320624419, + "gpu_noise_us_cpp": 6.286, + "gpu_noise_us_python": 9.017999999999999, + "gpu_bwutil_cpp": 0.7062307764249384, + "gpu_bwutil_python": 0.7012192060799494, + "gpu_gap_stddev_us": 2.354199153149408 + } + } + } + }, + "erase_planar_nchw_scalar_u8_constant_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "256x1080x1920" + ], + "randomMode": [ + "constant" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "numErase": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Erase single-channel constant (advanced) (planar NCHW tensor-only)." + }, + "baselines": { + "erase_planar_nchw_scalar_u8_constant_1080p_advanced[InOutDataType=uint8][shape=256x1080x1920][randomMode=constant][layout=NCHW][inputKind=Tensor][numErase=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 818.7179609889199, + "gpu_time_us_python": 836.8324901455001, + "gpu_noise_us_cpp": 15.687999999999999, + "gpu_noise_us_python": 4.736000000000001, + "gpu_bwutil_cpp": 0.833836773796014, + "gpu_bwutil_python": 0.8157977347826272, + "gpu_gap_stddev_us": 2.524770335098043 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 629.38343443844, + "gpu_time_us_python": 650.34058857118, + "gpu_noise_us_cpp": 4.17, + "gpu_noise_us_python": 4.582, + "gpu_bwutil_cpp": 0.8272841335941165, + "gpu_bwutil_python": 0.8006276575734317, + "gpu_gap_stddev_us": 2.167264829193609 + } + } + } + }, + "erase_fakeplanar_nchw_scalar_u8_constant_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "256x1080x1920" + ], + "randomMode": [ + "constant" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "numErase": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Erase single-channel constant (advanced) (fake planar NCHW_FAKE tensor-only)." + }, + "baselines": { + "erase_fakeplanar_nchw_scalar_u8_constant_1080p_advanced[InOutDataType=uint8][shape=256x1080x1920][randomMode=constant][layout=NCHW_FAKE][inputKind=Tensor][numErase=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2351.8198632748, + "gpu_time_us_python": 2378.69317889744, + "gpu_noise_us_cpp": 5.006, + "gpu_noise_us_python": 8.128, + "gpu_bwutil_cpp": 0.8708169360978555, + "gpu_bwutil_python": 0.8609895286214652, + "gpu_gap_stddev_us": 7.721563065312551 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1792.35300523888, + "gpu_time_us_python": 1820.0902486192397, + "gpu_noise_us_cpp": 4.215999999999999, + "gpu_noise_us_python": 7.554, + "gpu_bwutil_cpp": 0.8714992927261737, + "gpu_bwutil_python": 0.8582193402357623, + "gpu_gap_stddev_us": 2.347299660816975 + } + } + } + }, + "erase_planar_nchw_scalar_f32_constant_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "randomMode": [ + "constant" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "numErase": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Erase single-channel constant (advanced) (planar NCHW tensor-only)." + }, + "baselines": { + "erase_planar_nchw_scalar_f32_constant_1080p_advanced[InOutDataType=float32][shape=128x1080x1920][randomMode=constant][layout=NCHW][inputKind=Tensor][numErase=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1579.3045033942, + "gpu_time_us_python": 1602.13325345344, + "gpu_noise_us_cpp": 3.7040000000000006, + "gpu_noise_us_python": 8.138, + "gpu_bwutil_cpp": 0.8645207907196439, + "gpu_bwutil_python": 0.852205990725117, + "gpu_gap_stddev_us": 4.990215520320866 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1207.7644809825201, + "gpu_time_us_python": 1230.3436422565399, + "gpu_noise_us_cpp": 3.717999999999999, + "gpu_noise_us_python": 4.619999999999999, + "gpu_bwutil_cpp": 0.8622178316069607, + "gpu_bwutil_python": 0.8464012628888543, + "gpu_gap_stddev_us": 4.267040633055288 + } + } + } + }, + "erase_fakeplanar_nchw_scalar_f32_constant_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "randomMode": [ + "constant" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "numErase": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Erase single-channel constant (advanced) (fake planar NCHW_FAKE tensor-only)." + }, + "baselines": { + "erase_fakeplanar_nchw_scalar_f32_constant_1080p_advanced[InOutDataType=float32][shape=128x1080x1920][randomMode=constant][layout=NCHW_FAKE][inputKind=Tensor][numErase=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5024.70654487606, + "gpu_time_us_python": 5054.574584960899, + "gpu_noise_us_cpp": 7.764, + "gpu_noise_us_python": 9.202, + "gpu_bwutil_cpp": 0.8151723883718702, + "gpu_bwutil_python": 0.8103555452662844, + "gpu_gap_stddev_us": 1.794762757646807 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4424.246008354239, + "gpu_time_us_python": 4454.026518487661, + "gpu_noise_us_cpp": 6.018000000000001, + "gpu_noise_us_python": 18.732, + "gpu_bwutil_cpp": 0.7061287597868272, + "gpu_bwutil_python": 0.7014088210612016, + "gpu_gap_stddev_us": 6.198441550896074 + } + } + } + }, + "erase_planar_nchw_rgb_u8_random_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "randomMode": [ + "random" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "int64_axes": { + "numErase": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Erase RGB8 random (advanced) (planar NCHW)." + }, + "baselines": { + "erase_planar_nchw_rgb_u8_random_1080p_advanced[InOutDataType=uchar3][shape=128x1080x1920][randomMode=random][layout=NCHW][inputKind=Tensor][numErase=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1199.03885907168, + "gpu_time_us_python": 1219.6694608039602, + "gpu_noise_us_cpp": 5.36, + "gpu_noise_us_python": 19.31, + "gpu_bwutil_cpp": 0.8540264601703825, + "gpu_bwutil_python": 0.8395845716159958, + "gpu_gap_stddev_us": 3.487399382151335 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 915.74992647164, + "gpu_time_us_python": 943.7486452515999, + "gpu_noise_us_cpp": 4.872, + "gpu_noise_us_python": 19.708, + "gpu_bwutil_cpp": 0.8528718954934046, + "gpu_bwutil_python": 0.8275940621403514, + "gpu_gap_stddev_us": 6.135648589312421 + } + }, + "erase_planar_nchw_rgb_u8_random_1080p_advanced[InOutDataType=uchar3][shape=128x1080x1920][randomMode=random][layout=NCHW][inputKind=VarShape][numErase=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1249.8731823480998, + "gpu_time_us_python": 1278.8552667542401, + "gpu_noise_us_cpp": 5.272, + "gpu_noise_us_python": 8.712, + "gpu_bwutil_cpp": 0.8193267262489172, + "gpu_bwutil_python": 0.8007921321272752, + "gpu_gap_stddev_us": 4.225557007751637 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 999.5139289007, + "gpu_time_us_python": 1029.5307563747199, + "gpu_noise_us_cpp": 6.6240000000000006, + "gpu_noise_us_python": 8.268, + "gpu_bwutil_cpp": 0.7813980670487959, + "gpu_bwutil_python": 0.7586219488936754, + "gpu_gap_stddev_us": 2.450278351988356 + } + } + } + }, + "erase_fakeplanar_nchw_rgb_u8_random_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "randomMode": [ + "random" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "numErase": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Erase RGB8 random (advanced) (fake planar NCHW_FAKE tensor-only)." + }, + "baselines": { + "erase_fakeplanar_nchw_rgb_u8_random_1080p_advanced[InOutDataType=uchar3][shape=128x1080x1920][randomMode=random][layout=NCHW_FAKE][inputKind=Tensor][numErase=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5922.59770611386, + "gpu_time_us_python": 5948.28253922398, + "gpu_noise_us_cpp": 18.426, + "gpu_noise_us_python": 9.106, + "gpu_bwutil_cpp": 0.5186921467325971, + "gpu_bwutil_python": 0.5164530746382241, + "gpu_gap_stddev_us": 5.679295604046483 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4594.938649264201, + "gpu_time_us_python": 4627.76775861026, + "gpu_noise_us_cpp": 7.014, + "gpu_noise_us_python": 11.772, + "gpu_bwutil_cpp": 0.5099234488405131, + "gpu_bwutil_python": 0.5063071470256059, + "gpu_gap_stddev_us": 3.3784174697462723 + } + } + } + }, + "erase_torchvision_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "randomMode": [ + "torchvision" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "numErase": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Torchvision-compatible Erase region RGB8 Tensor paths." + }, + "baselines": { + "erase_torchvision_uchar3_advanced[InOutDataType=uchar3][shape=64x1080x1920][randomMode=torchvision][layout=NCHW][inputKind=Tensor][numErase=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4742.46469713602, + "gpu_time_us_python": 4757.364928047541, + "gpu_noise_us_cpp": 4.502, + "gpu_noise_us_python": 9.626, + "gpu_bwutil_cpp": 0.10817214152394357, + "gpu_bwutil_python": 0.107833446032042, + "gpu_gap_stddev_us": 4.270215765546677 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4681.87022066122, + "gpu_time_us_python": 4694.271818988801, + "gpu_noise_us_cpp": 1.116, + "gpu_noise_us_python": 5.715999999999999, + "gpu_bwutil_cpp": 0.08358024512114803, + "gpu_bwutil_python": 0.08335984620262028, + "gpu_gap_stddev_us": 1.868812525613217 + } + }, + "erase_torchvision_uchar3_advanced[InOutDataType=uchar3][shape=64x1080x1920][randomMode=torchvision][layout=NHWC][inputKind=Tensor][numErase=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4782.65190664322, + "gpu_time_us_python": 4795.159218100181, + "gpu_noise_us_cpp": 2.158, + "gpu_noise_us_python": 4.644, + "gpu_bwutil_cpp": 0.10726332295763394, + "gpu_bwutil_python": 0.10698361088910144, + "gpu_gap_stddev_us": 0.9898358020033043 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4740.58523288056, + "gpu_time_us_python": 4754.4809605007, + "gpu_noise_us_cpp": 1.426, + "gpu_noise_us_python": 7.159999999999998, + "gpu_bwutil_cpp": 0.08254448293605754, + "gpu_bwutil_python": 0.08230307339752282, + "gpu_gap_stddev_us": 2.4461741574325506 + } + } + } + }, + "erase_torchvision_uchar3_fakeplanar_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "randomMode": [ + "torchvision" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "numErase": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Torchvision-compatible Erase region RGB8 fake-planar comparison." + }, + "baselines": { + "erase_torchvision_uchar3_fakeplanar_advanced[InOutDataType=uchar3][shape=64x1080x1920][randomMode=torchvision][layout=NCHW_FAKE][inputKind=Tensor][numErase=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 7154.17435562792, + "gpu_time_us_python": 7174.243499210859, + "gpu_noise_us_cpp": 1.67, + "gpu_noise_us_python": 6.94, + "gpu_bwutil_cpp": 0.21484057982159177, + "gpu_bwutil_python": 0.2142398780264861, + "gpu_gap_stddev_us": 5.798109826346987 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6589.735967556279, + "gpu_time_us_python": 6608.850429962161, + "gpu_noise_us_cpp": 1.51, + "gpu_noise_us_python": 5.166, + "gpu_bwutil_cpp": 0.17790726272628385, + "gpu_bwutil_python": 0.17739352818155882, + "gpu_gap_stddev_us": 2.8515966838610725 + } + } + } + }, + "erase_torchvision_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "randomMode": [ + "torchvision" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "numErase": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Torchvision-compatible Erase region RGBf32 Tensor paths." + }, + "baselines": { + "erase_torchvision_float3_advanced[InOutDataType=float3][shape=32x1080x1920][randomMode=torchvision][layout=NCHW][inputKind=Tensor][numErase=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2393.6849017369595, + "gpu_time_us_python": 2405.19276091024, + "gpu_noise_us_cpp": 2.394, + "gpu_noise_us_python": 4.324, + "gpu_bwutil_cpp": 0.4282105589991535, + "gpu_bwutil_python": 0.4261616797696062, + "gpu_gap_stddev_us": 1.3263055732526094 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2419.00311500002, + "gpu_time_us_python": 2429.51350975758, + "gpu_noise_us_cpp": 1.0959999999999999, + "gpu_noise_us_python": 3.2399999999999998, + "gpu_bwutil_cpp": 0.32321367439707416, + "gpu_bwutil_python": 0.3218128973644738, + "gpu_gap_stddev_us": 1.555221972893777 + } + }, + "erase_torchvision_float3_advanced[InOutDataType=float3][shape=32x1080x1920][randomMode=torchvision][layout=NHWC][inputKind=Tensor][numErase=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5056.31539154048, + "gpu_time_us_python": 5068.26347732538, + "gpu_noise_us_cpp": 3.7159999999999997, + "gpu_noise_us_python": 5.465999999999999, + "gpu_bwutil_cpp": 0.20271678969261506, + "gpu_bwutil_python": 0.2022389305876358, + "gpu_gap_stddev_us": 2.561048468923847 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3781.30607960824, + "gpu_time_us_python": 3793.9294099807203, + "gpu_noise_us_cpp": 3.9200000000000004, + "gpu_noise_us_python": 8.042, + "gpu_bwutil_cpp": 0.20674858881162378, + "gpu_bwutil_python": 0.2060607144319872, + "gpu_gap_stddev_us": 1.2956779234205416 + } + } + } + }, + "erase_torchvision_float3_fakeplanar_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "randomMode": [ + "torchvision" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "numErase": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Torchvision-compatible Erase region RGBf32 fake-planar comparison." + }, + "baselines": { + "erase_torchvision_float3_fakeplanar_advanced[InOutDataType=float3][shape=32x1080x1920][randomMode=torchvision][layout=NCHW_FAKE][inputKind=Tensor][numErase=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 7733.3038879162605, + "gpu_time_us_python": 7754.04452988588, + "gpu_noise_us_cpp": 4.031999999999999, + "gpu_noise_us_python": 5.42, + "gpu_bwutil_cpp": 0.39737222210267664, + "gpu_bwutil_python": 0.3963093275910765, + "gpu_gap_stddev_us": 2.5600727749559846 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5973.77082733874, + "gpu_time_us_python": 5989.4180593036, + "gpu_noise_us_cpp": 4.122, + "gpu_noise_us_python": 6.564, + "gpu_bwutil_cpp": 0.39235077278069597, + "gpu_bwutil_python": 0.3913260016674071, + "gpu_gap_stddev_us": 5.474363698239557 + } + } + } + } + } +} diff --git a/bench/config/operators/findhomography.json b/bench/config/operators/findhomography.json new file mode 100644 index 000000000..51db772ff --- /dev/null +++ b/bench/config/operators/findhomography.json @@ -0,0 +1,285 @@ +{ + "benchmark": "findhomography", + "configs": { + "findhomography_basic": { + "tier": "basic", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "512x1024" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "FindHomography valid non-collinear projective (basic)." + }, + "baselines": { + "findhomography_basic[InOutDataType=float32][shape=512x1024][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1674.1464100419603, + "gpu_time_us_python": 1686.3512093436598, + "gpu_noise_us_cpp": 23.08, + "gpu_noise_us_python": 23.384, + "gpu_bwutil_cpp": 0.0032289906359946005, + "gpu_bwutil_python": 0.00320562170364726 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1634.63361069714, + "gpu_time_us_python": 1642.6961580674001, + "gpu_noise_us_cpp": 23.596, + "gpu_noise_us_python": 22.686, + "gpu_bwutil_cpp": 0.00252269061857548, + "gpu_bwutil_python": 0.00251030317139974 + } + } + } + }, + "findhomography_varshape_basic": { + "tier": "basic", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "16x16" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "FindHomography valid projective TensorBatch workload." + }, + "baselines": { + "findhomography_varshape_basic[InOutDataType=float32][shape=16x16][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4449.08624821734, + "gpu_time_us_python": 4458.421250961159, + "gpu_noise_us_cpp": 5.832, + "gpu_noise_us_python": 7.5280000000000005, + "gpu_bwutil_cpp": 6.75240467977025e-07, + "gpu_bwutil_python": 6.738313504051135e-07 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4461.8497914652, + "gpu_time_us_python": 4473.702727404279, + "gpu_noise_us_cpp": 3.408, + "gpu_noise_us_python": 4.781999999999999, + "gpu_bwutil_cpp": 5.136142346769413e-07, + "gpu_bwutil_python": 5.122623150138726e-07 + } + } + } + }, + "findhomography_4pts_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "8192x4" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "FindHomography valid non-collinear projective 4pts (advanced)." + }, + "baselines": { + "findhomography_4pts_advanced[InOutDataType=float32][shape=8192x4][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2300.3282288096802, + "gpu_time_us_python": 2311.06650467594, + "gpu_noise_us_cpp": 3.194, + "gpu_noise_us_python": 3.918, + "gpu_bwutil_cpp": 0.00022898995114687998, + "gpu_bwutil_python": 0.00022792614491534002 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2284.7643418354796, + "gpu_time_us_python": 2294.28390790134, + "gpu_noise_us_cpp": 1.248, + "gpu_noise_us_python": 3.1500000000000004, + "gpu_bwutil_cpp": 0.00017586308504328, + "gpu_bwutil_python": 0.00017513222937726001 + } + } + } + }, + "findhomography_16pts_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "4096x16" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "FindHomography valid non-collinear projective 16pts (advanced)." + }, + "baselines": { + "findhomography_16pts_advanced[InOutDataType=float32][shape=4096x16][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1626.7303196892603, + "gpu_time_us_python": 1638.9062411131, + "gpu_noise_us_cpp": 2.312, + "gpu_noise_us_python": 3.214, + "gpu_bwutil_cpp": 0.00047276560108167997, + "gpu_bwutil_python": 0.0004692528131047599 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1764.57778185458, + "gpu_time_us_python": 1773.5568295149, + "gpu_noise_us_cpp": 1.9440000000000002, + "gpu_noise_us_python": 2.674, + "gpu_bwutil_cpp": 0.0003324537801088, + "gpu_bwutil_python": 0.00033076680858446005 + } + } + } + }, + "findhomography_64pts_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "4096x64" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "FindHomography valid non-collinear projective 64pts (advanced)." + }, + "baselines": { + "findhomography_64pts_advanced[InOutDataType=float32][shape=4096x64][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2396.06443632214, + "gpu_time_us_python": 2404.1438749421204, + "gpu_noise_us_cpp": 7.797999999999999, + "gpu_noise_us_python": 9.059999999999999, + "gpu_bwutil_cpp": 0.00116515258154572, + "gpu_bwutil_python": 0.00116123879225096 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2870.54704337648, + "gpu_time_us_python": 2880.0478891631606, + "gpu_noise_us_cpp": 12.496, + "gpu_noise_us_python": 14.376000000000001, + "gpu_bwutil_cpp": 0.00074189230384916, + "gpu_bwutil_python": 0.0007394491040947401 + } + } + } + }, + "findhomography_256pts_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "2048x256" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "FindHomography valid non-collinear projective 256pts (advanced)." + }, + "baselines": { + "findhomography_256pts_advanced[InOutDataType=float32][shape=2048x256][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1667.8467984367599, + "gpu_time_us_python": 1676.7859790330003, + "gpu_noise_us_cpp": 2.992, + "gpu_noise_us_python": 4.69, + "gpu_bwutil_cpp": 0.00326250224835888, + "gpu_bwutil_python": 0.00324512259529516 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2049.94044896282, + "gpu_time_us_python": 2060.61068106412, + "gpu_noise_us_cpp": 3.37, + "gpu_noise_us_python": 3.6559999999999997, + "gpu_bwutil_cpp": 0.00202483699164824, + "gpu_bwutil_python": 0.0020143712500211002 + } + } + } + }, + "findhomography_1024pts_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "1024x1024" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "FindHomography valid non-collinear projective 1024pts (advanced)." + }, + "baselines": { + "findhomography_1024pts_advanced[InOutDataType=float32][shape=1024x1024][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2927.1039923955605, + "gpu_time_us_python": 2942.14702792104, + "gpu_noise_us_cpp": 19.216, + "gpu_noise_us_python": 18.94, + "gpu_bwutil_cpp": 0.00369360031070226, + "gpu_bwutil_python": 0.0036747489139883397 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2956.0247838166597, + "gpu_time_us_python": 2965.6256164030797, + "gpu_noise_us_cpp": 14.828, + "gpu_noise_us_python": 16.796, + "gpu_bwutil_cpp": 0.00278999152596568, + "gpu_bwutil_python": 0.0027809496852598398 + } + } + } + } + } +} diff --git a/bench/config/operators/flip.json b/bench/config/operators/flip.json new file mode 100644 index 000000000..7eeff6786 --- /dev/null +++ b/bench/config/operators/flip.json @@ -0,0 +1,1262 @@ +{ + "benchmark": "flip", + "configs": { + "flip_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "flipType": [ + "BOTH" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Flip RGB8 (basic)." + }, + "baselines": { + "flip_uchar3_basic[InOutDataType=uchar3][shape=128x1080x1920][flipType=BOTH][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1536.11937578466, + "gpu_time_us_python": 1546.00735785044, + "gpu_noise_us_cpp": 1.24, + "gpu_noise_us_python": 2.118, + "gpu_bwutil_cpp": 0.6666149293148032, + "gpu_bwutil_python": 0.6623519056948943, + "gpu_gap_stddev_us": 1.3634262665428252 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1200.3590379699249, + "gpu_time_us_python": 1210.0800704936998, + "gpu_noise_us_cpp": 1.3599999999999999, + "gpu_noise_us_python": 4.045, + "gpu_bwutil_cpp": 0.6506545460828386, + "gpu_bwutil_python": 0.6454301369860367, + "gpu_gap_stddev_us": 2.3880281939111607 + } + }, + "flip_uchar3_basic[InOutDataType=uchar3][shape=128x1080x1920][flipType=BOTH][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1672.65755971268, + "gpu_time_us_python": 1686.3720652878596, + "gpu_noise_us_cpp": 1.3679999999999999, + "gpu_noise_us_python": 2.222, + "gpu_bwutil_cpp": 0.6121995355590719, + "gpu_bwutil_python": 0.6072216866190575, + "gpu_gap_stddev_us": 1.8659696237427175 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1335.18120509025, + "gpu_time_us_python": 1348.385080102775, + "gpu_noise_us_cpp": 1.8525, + "gpu_noise_us_python": 5.205, + "gpu_bwutil_cpp": 0.5849601176270407, + "gpu_bwutil_python": 0.579228988726336, + "gpu_gap_stddev_us": 3.641015183189073 + } + } + } + }, + "flip_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "flipType": [ + "BOTH" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Flip RGBA8 (advanced)." + }, + "baselines": { + "flip_uchar4_advanced[InOutDataType=uchar4][shape=128x1080x1920][flipType=BOTH][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1707.2100193321799, + "gpu_time_us_python": 1717.08104496128, + "gpu_noise_us_cpp": 0.8240000000000001, + "gpu_noise_us_python": 2.054, + "gpu_bwutil_cpp": 0.7997454101540312, + "gpu_bwutil_python": 0.7951483670896066, + "gpu_gap_stddev_us": 1.6227814818978699 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1400.6098329021497, + "gpu_time_us_python": 1410.85796960395, + "gpu_noise_us_cpp": 1.43, + "gpu_noise_us_python": 3.3000000000000003, + "gpu_bwutil_cpp": 0.7435023559614752, + "gpu_bwutil_python": 0.7381019549598105, + "gpu_gap_stddev_us": 2.112331798114571 + } + }, + "flip_uchar4_advanced[InOutDataType=uchar4][shape=128x1080x1920][flipType=BOTH][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1768.81721473072, + "gpu_time_us_python": 1783.4643600558998, + "gpu_noise_us_cpp": 1.194, + "gpu_noise_us_python": 4.064, + "gpu_bwutil_cpp": 0.771890667920624, + "gpu_bwutil_python": 0.7655520623703878, + "gpu_gap_stddev_us": 2.2165876748994546 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1459.6431458916002, + "gpu_time_us_python": 1473.139553449125, + "gpu_noise_us_cpp": 2.93, + "gpu_noise_us_python": 5.305, + "gpu_bwutil_cpp": 0.7134327281700208, + "gpu_bwutil_python": 0.706899739394872, + "gpu_gap_stddev_us": 2.4283400760691096 + } + } + } + }, + "flip_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "flipType": [ + "BOTH" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Flip RGBf32 (basic)." + }, + "baselines": { + "flip_float3_basic[InOutDataType=float3][shape=64x1080x1920][flipType=BOTH][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2290.0159155238002, + "gpu_time_us_python": 2299.5771386207402, + "gpu_noise_us_cpp": 1.582, + "gpu_noise_us_python": 2.304, + "gpu_bwutil_cpp": 0.894316939927623, + "gpu_bwutil_python": 0.8905985887557708, + "gpu_gap_stddev_us": 0.6037958624446541 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1748.35756368115, + "gpu_time_us_python": 1759.90348181075, + "gpu_noise_us_cpp": 1.495, + "gpu_noise_us_python": 3.8550000000000004, + "gpu_bwutil_cpp": 0.893429457368397, + "gpu_bwutil_python": 0.8875696178596972, + "gpu_gap_stddev_us": 3.04881609752925 + } + } + } + }, + "flip_float3_varshape_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "flipType": [ + "BOTH" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Flip RGBf32 VarShape (advanced)." + }, + "baselines": { + "flip_float3_varshape_advanced[InOutDataType=float3][shape=64x1080x1920][flipType=BOTH][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2329.3383929463803, + "gpu_time_us_python": 2341.53959639714, + "gpu_noise_us_cpp": 2.396, + "gpu_noise_us_python": 3.12, + "gpu_bwutil_cpp": 0.8792196282645008, + "gpu_bwutil_python": 0.8746383398506954, + "gpu_gap_stddev_us": 1.0741624627130208 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1783.8935055208249, + "gpu_time_us_python": 1796.237197305425, + "gpu_noise_us_cpp": 3.495, + "gpu_noise_us_python": 4.2225, + "gpu_bwutil_cpp": 0.8756318663437999, + "gpu_bwutil_python": 0.8696145679361533, + "gpu_gap_stddev_us": 0.4642181346405368 + } + } + } + }, + "flip_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "flipType": [ + "BOTH" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Flip RGBAf32 (advanced)." + }, + "baselines": { + "flip_float4_advanced[InOutDataType=float4][shape=64x1080x1920][flipType=BOTH][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3038.62747916252, + "gpu_time_us_python": 3048.1370508617597, + "gpu_noise_us_cpp": 2.612, + "gpu_noise_us_python": 2.398, + "gpu_bwutil_cpp": 0.8986513496613446, + "gpu_bwutil_python": 0.895847865882249, + "gpu_gap_stddev_us": 1.1834713470251232 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2330.9723679665753, + "gpu_time_us_python": 2341.180110646125, + "gpu_noise_us_cpp": 1.6949999999999998, + "gpu_noise_us_python": 3.2474999999999996, + "gpu_bwutil_cpp": 0.8934955526364617, + "gpu_bwutil_python": 0.8896004861976776, + "gpu_gap_stddev_us": 1.732726030553619 + } + }, + "flip_float4_advanced[InOutDataType=float4][shape=64x1080x1920][flipType=BOTH][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3040.8136746969003, + "gpu_time_us_python": 3054.39966829806, + "gpu_noise_us_cpp": 1.7080000000000002, + "gpu_noise_us_python": 2.552, + "gpu_bwutil_cpp": 0.8980052615995359, + "gpu_bwutil_python": 0.8940111333469767, + "gpu_gap_stddev_us": 1.6111934900190152 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2333.7662166926, + "gpu_time_us_python": 2346.6923359398, + "gpu_noise_us_cpp": 2.415, + "gpu_noise_us_python": 5.297499999999999, + "gpu_bwutil_cpp": 0.8924258821311497, + "gpu_bwutil_python": 0.887510780807301, + "gpu_gap_stddev_us": 0.9158784022169081 + } + } + } + }, + "flip_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "flipType": [ + "BOTH" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Flip single-channel (advanced)." + }, + "baselines": { + "flip_scalar_advanced[InOutDataType=float32][shape=128x1080x1920][flipType=BOTH][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1531.9114744662602, + "gpu_time_us_python": 1541.98450024516, + "gpu_noise_us_cpp": 1.8079999999999998, + "gpu_noise_us_python": 2.1119999999999997, + "gpu_bwutil_cpp": 0.8912612783738515, + "gpu_bwutil_python": 0.8854392401438224, + "gpu_gap_stddev_us": 0.7479390117351189 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1173.811176162525, + "gpu_time_us_python": 1183.07411389525, + "gpu_noise_us_cpp": 1.585, + "gpu_noise_us_python": 3.2424999999999997, + "gpu_bwutil_cpp": 0.8871581914347616, + "gpu_bwutil_python": 0.8802126534632286, + "gpu_gap_stddev_us": 1.1055075695892553 + } + }, + "flip_scalar_advanced[InOutDataType=float32][shape=128x1080x1920][flipType=BOTH][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1768.5662607071597, + "gpu_time_us_python": 1783.0338129759803, + "gpu_noise_us_cpp": 1.128, + "gpu_noise_us_python": 2.452, + "gpu_bwutil_cpp": 0.7720002005710674, + "gpu_bwutil_python": 0.7657365465857638, + "gpu_gap_stddev_us": 1.5632317151367319 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1457.837952084275, + "gpu_time_us_python": 1471.34868674435, + "gpu_noise_us_cpp": 2.6475, + "gpu_noise_us_python": 4.1075, + "gpu_bwutil_cpp": 0.7143174009714356, + "gpu_bwutil_python": 0.7077586102287048, + "gpu_gap_stddev_us": 3.860177294226763 + } + }, + "flip_scalar_advanced[InOutDataType=uint8][shape=128x1080x1920][flipType=BOTH][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 428.14703102404, + "gpu_time_us_python": 435.7551723865201, + "gpu_noise_us_cpp": 2.694, + "gpu_noise_us_python": 2.1980000000000004, + "gpu_bwutil_cpp": 0.7972340223998943, + "gpu_bwutil_python": 0.7833157977064829, + "gpu_gap_stddev_us": 0.6478337904870141 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 356.938163299275, + "gpu_time_us_python": 364.9491026042, + "gpu_noise_us_cpp": 0.7925, + "gpu_noise_us_python": 2.67, + "gpu_bwutil_cpp": 0.7293713380463416, + "gpu_bwutil_python": 0.713361656792309, + "gpu_gap_stddev_us": 0.8440347839100019 + } + }, + "flip_scalar_advanced[InOutDataType=uint8][shape=128x1080x1920][flipType=BOTH][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 466.55256058628004, + "gpu_time_us_python": 479.44678096135993, + "gpu_noise_us_cpp": 1.7119999999999997, + "gpu_noise_us_python": 2.45, + "gpu_bwutil_cpp": 0.73160781961493, + "gpu_bwutil_python": 0.711933691608425, + "gpu_gap_stddev_us": 0.9681805947846458 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 393.19619699615004, + "gpu_time_us_python": 406.8500113365, + "gpu_noise_us_cpp": 1.8725, + "gpu_noise_us_python": 4.0075, + "gpu_bwutil_cpp": 0.6621116044397657, + "gpu_bwutil_python": 0.639963687786699, + "gpu_gap_stddev_us": 4.361711147189111 + } + } + } + }, + "flip_u8_tensor_directions_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "256x1080x1920" + ], + "flipType": [ + "HORIZONTAL", + "VERTICAL" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Flip single-channel tensor directions (advanced)." + }, + "baselines": { + "flip_u8_tensor_directions_1080p_advanced[InOutDataType=uint8][shape=256x1080x1920][flipType=HORIZONTAL][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 828.19491194568, + "gpu_time_us_python": 837.1819822684199, + "gpu_noise_us_cpp": 1.754, + "gpu_noise_us_python": 2.846, + "gpu_bwutil_cpp": 0.8242826625700518, + "gpu_bwutil_python": 0.8154346865360645, + "gpu_gap_stddev_us": 1.0177223502373802 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 695.112422586725, + "gpu_time_us_python": 704.813538232175, + "gpu_noise_us_cpp": 1.1925000000000001, + "gpu_noise_us_python": 4.1025, + "gpu_bwutil_cpp": 0.7490566416478938, + "gpu_bwutil_python": 0.7387546519026763, + "gpu_gap_stddev_us": 2.314727453124499 + } + }, + "flip_u8_tensor_directions_1080p_advanced[InOutDataType=uint8][shape=256x1080x1920][flipType=VERTICAL][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 838.06283669322, + "gpu_time_us_python": 847.46877117486, + "gpu_noise_us_cpp": 0.868, + "gpu_noise_us_python": 2.3040000000000003, + "gpu_bwutil_cpp": 0.814577068357073, + "gpu_bwutil_python": 0.8055369505231436, + "gpu_gap_stddev_us": 0.8177987921755631 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 702.9990987047751, + "gpu_time_us_python": 712.623967725775, + "gpu_noise_us_cpp": 0.9974999999999999, + "gpu_noise_us_python": 3.1475, + "gpu_bwutil_cpp": 0.7406534021975453, + "gpu_bwutil_python": 0.7306531790497091, + "gpu_gap_stddev_us": 1.7176668114082732 + } + } + } + }, + "flip_u8_varshape_directions_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "256x1080x1920" + ], + "flipType": [ + "HORIZONTAL", + "VERTICAL" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Flip single-channel varshape directions (advanced)." + }, + "baselines": { + "flip_u8_varshape_directions_1080p_advanced[InOutDataType=uint8][shape=256x1080x1920][flipType=HORIZONTAL][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 922.3973186514801, + "gpu_time_us_python": 938.7410630000398, + "gpu_noise_us_cpp": 1.158, + "gpu_noise_us_python": 2.932, + "gpu_bwutil_cpp": 0.7401005049300509, + "gpu_bwutil_python": 0.7272154641670836, + "gpu_gap_stddev_us": 0.5733237901622449 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 772.6140105443501, + "gpu_time_us_python": 790.3877766090251, + "gpu_noise_us_cpp": 1.78, + "gpu_noise_us_python": 3.3649999999999998, + "gpu_bwutil_cpp": 0.6739185861499553, + "gpu_bwutil_python": 0.6587790085281412, + "gpu_gap_stddev_us": 4.000434058017927 + } + }, + "flip_u8_varshape_directions_1080p_advanced[InOutDataType=uint8][shape=256x1080x1920][flipType=VERTICAL][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 922.64511889028, + "gpu_time_us_python": 938.2954095633, + "gpu_noise_us_cpp": 0.958, + "gpu_noise_us_python": 2.462, + "gpu_bwutil_cpp": 0.7399018747532833, + "gpu_bwutil_python": 0.7275609589112746, + "gpu_gap_stddev_us": 0.7466971540870955 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 772.8348639487751, + "gpu_time_us_python": 790.095143701, + "gpu_noise_us_cpp": 1.6625, + "gpu_noise_us_python": 4.9975000000000005, + "gpu_bwutil_cpp": 0.6737259016730577, + "gpu_bwutil_python": 0.6590146427510084, + "gpu_gap_stddev_us": 2.745828446946951 + } + } + } + }, + "flip_f32_directions_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "flipType": [ + "HORIZONTAL", + "VERTICAL" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Flip single-channel directions (advanced)." + }, + "baselines": { + "flip_f32_directions_1080p_advanced[InOutDataType=float32][shape=128x1080x1920][flipType=HORIZONTAL][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1530.15317858716, + "gpu_time_us_python": 1539.66882872428, + "gpu_noise_us_cpp": 1.5020000000000002, + "gpu_noise_us_python": 2.242, + "gpu_bwutil_cpp": 0.8922854300351732, + "gpu_bwutil_python": 0.886771171079497, + "gpu_gap_stddev_us": 1.0149272260985245 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1174.2233884021, + "gpu_time_us_python": 1185.6512108812751, + "gpu_noise_us_cpp": 2.4074999999999998, + "gpu_noise_us_python": 5.9275, + "gpu_bwutil_cpp": 0.8868467665851202, + "gpu_bwutil_python": 0.8783018010076128, + "gpu_gap_stddev_us": 2.031375253674718 + } + }, + "flip_f32_directions_1080p_advanced[InOutDataType=float32][shape=128x1080x1920][flipType=HORIZONTAL][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1765.1567475896002, + "gpu_time_us_python": 1778.8172569680398, + "gpu_noise_us_cpp": 1.0059999999999998, + "gpu_noise_us_python": 2.108, + "gpu_bwutil_cpp": 0.7734913145391962, + "gpu_bwutil_python": 0.767551507389384, + "gpu_gap_stddev_us": 1.4310074448125039 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1445.658851767575, + "gpu_time_us_python": 1460.7837238091001, + "gpu_noise_us_cpp": 2.5300000000000002, + "gpu_noise_us_python": 4.42, + "gpu_bwutil_cpp": 0.7203334938275325, + "gpu_bwutil_python": 0.7128758064499893, + "gpu_gap_stddev_us": 2.102037532801812 + } + }, + "flip_f32_directions_1080p_advanced[InOutDataType=float32][shape=128x1080x1920][flipType=VERTICAL][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1530.73136835558, + "gpu_time_us_python": 1540.1726621791, + "gpu_noise_us_cpp": 1.296, + "gpu_noise_us_python": 2.848, + "gpu_bwutil_cpp": 0.8919483649058317, + "gpu_bwutil_python": 0.886480958271514, + "gpu_gap_stddev_us": 0.9242459254774511 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1173.70935760425, + "gpu_time_us_python": 1183.2931978233, + "gpu_noise_us_cpp": 1.4775, + "gpu_noise_us_python": 3.3375, + "gpu_bwutil_cpp": 0.8872354218681084, + "gpu_bwutil_python": 0.8800527417294378, + "gpu_gap_stddev_us": 1.9066051511725106 + } + }, + "flip_f32_directions_1080p_advanced[InOutDataType=float32][shape=128x1080x1920][flipType=VERTICAL][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1765.4408736967403, + "gpu_time_us_python": 1779.5752991034399, + "gpu_noise_us_cpp": 0.9899999999999999, + "gpu_noise_us_python": 5.112, + "gpu_bwutil_cpp": 0.7733668645417053, + "gpu_bwutil_python": 0.7672251708905362, + "gpu_gap_stddev_us": 2.3109947290393325 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1436.92977171515, + "gpu_time_us_python": 1451.6081061634, + "gpu_noise_us_cpp": 2.6125, + "gpu_noise_us_python": 4.62, + "gpu_bwutil_cpp": 0.7247091877816916, + "gpu_bwutil_python": 0.7173844655928479, + "gpu_gap_stddev_us": 3.315733250258043 + } + } + } + }, + "flip_planar_nchw_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "flipType": [ + "BOTH" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Flip RGB8 planar NCHW (basic)." + }, + "baselines": { + "flip_planar_nchw_uchar3_basic[InOutDataType=uchar3][shape=128x1080x1920][flipType=BOTH][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1255.2673469781198, + "gpu_time_us_python": 1264.3958661290999, + "gpu_noise_us_cpp": 1.3760000000000001, + "gpu_noise_us_python": 2.002, + "gpu_bwutil_cpp": 0.8157625219925292, + "gpu_bwutil_python": 0.8098732684538842, + "gpu_gap_stddev_us": 1.0789513360903378 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1040.453736873025, + "gpu_time_us_python": 1049.655343840625, + "gpu_noise_us_cpp": 2.5225, + "gpu_noise_us_python": 3.6325, + "gpu_bwutil_cpp": 0.7506508780702863, + "gpu_bwutil_python": 0.7440710452232502, + "gpu_gap_stddev_us": 0.809632865640529 + } + }, + "flip_planar_nchw_uchar3_basic[InOutDataType=uchar3][shape=128x1080x1920][flipType=BOTH][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1936.5340655050197, + "gpu_time_us_python": 1950.61203472368, + "gpu_noise_us_cpp": 0.9219999999999999, + "gpu_noise_us_python": 2.884, + "gpu_bwutil_cpp": 0.5287800703046744, + "gpu_bwutil_python": 0.5249636681039936, + "gpu_gap_stddev_us": 0.796895398399446 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1561.9577030645, + "gpu_time_us_python": 1576.5252565987748, + "gpu_noise_us_cpp": 2.0375, + "gpu_noise_us_python": 3.6975, + "gpu_bwutil_cpp": 0.5000286265843586, + "gpu_bwutil_python": 0.4954076720519301, + "gpu_gap_stddev_us": 1.1005163460195149 + } + } + } + }, + "flip_planar_nchw_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "flipType": [ + "BOTH" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Flip RGB8 native-planar Tensor comparison (advanced)." + }, + "baselines": { + "flip_planar_nchw_uchar3_advanced[InOutDataType=uchar3][shape=128x1080x1920][flipType=BOTH][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1254.9038264751, + "gpu_time_us_python": 1264.89028605542, + "gpu_noise_us_cpp": 0.758, + "gpu_noise_us_python": 2.186, + "gpu_bwutil_cpp": 0.815998825059984, + "gpu_bwutil_python": 0.8095566221908621, + "gpu_gap_stddev_us": 0.8604332789263598 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1040.4476718665, + "gpu_time_us_python": 1050.847521712175, + "gpu_noise_us_cpp": 1.6949999999999998, + "gpu_noise_us_python": 3.0225, + "gpu_bwutil_cpp": 0.7506553099565768, + "gpu_bwutil_python": 0.7432285894937102, + "gpu_gap_stddev_us": 2.6667331276724275 + } + } + } + }, + "flip_planar_nchw_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "flipType": [ + "BOTH" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Flip RGBA8 planar NCHW tensor-only (advanced); RGBA8p varshape is unsupported by the Python image API." + }, + "baselines": { + "flip_planar_nchw_uchar4_advanced[InOutDataType=uchar4][shape=128x1080x1920][flipType=BOTH][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1668.5492579141799, + "gpu_time_us_python": 1679.28510236308, + "gpu_noise_us_cpp": 1.034, + "gpu_noise_us_python": 3.0, + "gpu_bwutil_cpp": 0.8182757682492301, + "gpu_bwutil_python": 0.8130448771902345, + "gpu_gap_stddev_us": 1.8669134623914865 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1381.44172928265, + "gpu_time_us_python": 1389.898664918175, + "gpu_noise_us_cpp": 1.1175000000000002, + "gpu_noise_us_python": 3.15, + "gpu_bwutil_cpp": 0.7538187578605329, + "gpu_bwutil_python": 0.7492320702818764, + "gpu_gap_stddev_us": 0.5403863510236195 + } + } + } + }, + "flip_planar_nchw_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "flipType": [ + "BOTH" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Flip RGBf32 planar NCHW (basic)." + }, + "baselines": { + "flip_planar_nchw_float3_basic[InOutDataType=float3][shape=64x1080x1920][flipType=BOTH][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2290.74389240952, + "gpu_time_us_python": 2301.85805854442, + "gpu_noise_us_cpp": 1.75, + "gpu_noise_us_python": 2.5980000000000003, + "gpu_bwutil_cpp": 0.8940327320523791, + "gpu_bwutil_python": 0.8897164391320296, + "gpu_gap_stddev_us": 1.6275225699566533 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1754.690406830925, + "gpu_time_us_python": 1764.9086738975252, + "gpu_noise_us_cpp": 2.145, + "gpu_noise_us_python": 3.6725000000000003, + "gpu_bwutil_cpp": 0.890205299076002, + "gpu_bwutil_python": 0.8850514629200528, + "gpu_gap_stddev_us": 1.0519957508265407 + } + } + } + }, + "flip_planar_nchw_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "flipType": [ + "BOTH" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Flip RGBf32 native-planar Tensor comparison (advanced)." + }, + "baselines": { + "flip_planar_nchw_float3_advanced[InOutDataType=float3][shape=64x1080x1920][flipType=BOTH][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2290.8947003970998, + "gpu_time_us_python": 2301.4165344587796, + "gpu_noise_us_cpp": 2.478, + "gpu_noise_us_python": 3.5839999999999996, + "gpu_bwutil_cpp": 0.8939738760273184, + "gpu_bwutil_python": 0.8898867460669664, + "gpu_gap_stddev_us": 0.5842229232926399 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1754.8786580979, + "gpu_time_us_python": 1764.10411256295, + "gpu_noise_us_cpp": 2.9349999999999996, + "gpu_noise_us_python": 3.21, + "gpu_bwutil_cpp": 0.8901095188215864, + "gpu_bwutil_python": 0.8854550309160536, + "gpu_gap_stddev_us": 1.8375737827001908 + } + } + } + }, + "flip_planar_nchw_float3_varshape_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "flipType": [ + "BOTH" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Flip RGBf32 planar NCHW VarShape (advanced)." + }, + "baselines": { + "flip_planar_nchw_float3_varshape_advanced[InOutDataType=float3][shape=64x1080x1920][flipType=BOTH][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2686.0848513055, + "gpu_time_us_python": 2699.8123876509803, + "gpu_noise_us_cpp": 0.9800000000000001, + "gpu_noise_us_python": 1.986, + "gpu_bwutil_cpp": 0.7624480165438183, + "gpu_bwutil_python": 0.7585713601798518, + "gpu_gap_stddev_us": 1.4490548125625047 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2141.017368715975, + "gpu_time_us_python": 2155.70642513015, + "gpu_noise_us_cpp": 1.29, + "gpu_noise_us_python": 4.7475000000000005, + "gpu_bwutil_cpp": 0.7295758694917185, + "gpu_bwutil_python": 0.7246058666117983, + "gpu_gap_stddev_us": 2.619874428430768 + } + } + } + }, + "flip_planar_nchw_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "flipType": [ + "BOTH" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Flip RGBAf32 planar NCHW (advanced)." + }, + "baselines": { + "flip_planar_nchw_float4_advanced[InOutDataType=float4][shape=64x1080x1920][flipType=BOTH][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3053.08098560422, + "gpu_time_us_python": 3064.44234964318, + "gpu_noise_us_cpp": 1.7919999999999998, + "gpu_noise_us_python": 3.45, + "gpu_bwutil_cpp": 0.8943970646771507, + "gpu_bwutil_python": 0.8910812764929981, + "gpu_gap_stddev_us": 1.4001382439963388 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2333.792001008925, + "gpu_time_us_python": 2344.0452318324997, + "gpu_noise_us_cpp": 1.7025, + "gpu_noise_us_python": 4.5225, + "gpu_bwutil_cpp": 0.8924156982111112, + "gpu_bwutil_python": 0.8885120792574238, + "gpu_gap_stddev_us": 1.425627345592116 + } + }, + "flip_planar_nchw_float4_advanced[InOutDataType=float4][shape=64x1080x1920][flipType=BOTH][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3582.9760769434806, + "gpu_time_us_python": 3599.48286601468, + "gpu_noise_us_cpp": 1.516, + "gpu_noise_us_python": 4.5040000000000004, + "gpu_bwutil_cpp": 0.7621225933978705, + "gpu_bwutil_python": 0.7586278863525882, + "gpu_gap_stddev_us": 4.242653402675951 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2845.738551156025, + "gpu_time_us_python": 2855.0646352496497, + "gpu_noise_us_cpp": 1.9549999999999998, + "gpu_noise_us_python": 5.2475000000000005, + "gpu_bwutil_cpp": 0.731870985042458, + "gpu_bwutil_python": 0.7294803727559533, + "gpu_gap_stddev_us": 5.353526581609672 + } + } + } + }, + "flip_fake_planar_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "flipType": [ + "BOTH" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Flip RGB8 fake-planar reformat-flip-reformat baseline (advanced)." + }, + "baselines": { + "flip_fake_planar_uchar3_advanced[InOutDataType=uchar3][shape=128x1080x1920][flipType=BOTH][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6254.1312360763, + "gpu_time_us_python": 6270.38712978356, + "gpu_noise_us_cpp": 4.2299999999999995, + "gpu_noise_us_python": 4.4079999999999995, + "gpu_bwutil_cpp": 0.49119545587220614, + "gpu_bwutil_python": 0.48992210028246647, + "gpu_gap_stddev_us": 3.030229300706085 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 4876.6744618232, + "gpu_time_us_python": 4893.843192590275, + "gpu_noise_us_cpp": 2.7325, + "gpu_noise_us_python": 5.4425, + "gpu_bwutil_cpp": 0.48046455344701344, + "gpu_bwutil_python": 0.47877966985465065, + "gpu_gap_stddev_us": 2.653848804397848 + } + } + } + }, + "flip_fake_planar_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "flipType": [ + "BOTH" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Flip RGBA8 fake-planar reformat-flip-reformat baseline (advanced)." + }, + "baselines": { + "flip_fake_planar_uchar4_advanced[InOutDataType=uchar4][shape=128x1080x1920][flipType=BOTH][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 7806.73392324732, + "gpu_time_us_python": 7826.68801546092, + "gpu_noise_us_cpp": 1.6600000000000001, + "gpu_noise_us_python": 3.2120000000000006, + "gpu_bwutil_cpp": 0.5246753828118869, + "gpu_bwutil_python": 0.5233376707535383, + "gpu_gap_stddev_us": 1.3003342955444304 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 6186.104379049075, + "gpu_time_us_python": 6201.2027327607, + "gpu_noise_us_cpp": 2.685, + "gpu_noise_us_python": 3.275, + "gpu_bwutil_cpp": 0.5050192457843535, + "gpu_bwutil_python": 0.5037898416852912, + "gpu_gap_stddev_us": 2.3810430268568887 + } + } + } + }, + "flip_fake_planar_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "flipType": [ + "BOTH" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Flip RGBf32 fake-planar reformat-flip-reformat baseline (advanced)." + }, + "baselines": { + "flip_fake_planar_float3_advanced[InOutDataType=float3][shape=64x1080x1920][flipType=BOTH][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 7642.89386633664, + "gpu_time_us_python": 7660.283380566181, + "gpu_noise_us_cpp": 2.3519999999999994, + "gpu_noise_us_python": 3.474, + "gpu_bwutil_cpp": 0.8038840128656497, + "gpu_bwutil_python": 0.8020591575984899, + "gpu_gap_stddev_us": 3.1125392042046363 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 6137.835316541674, + "gpu_time_us_python": 6154.347326697325, + "gpu_noise_us_cpp": 3.4699999999999998, + "gpu_noise_us_python": 6.6274999999999995, + "gpu_bwutil_cpp": 0.7634796596420625, + "gpu_bwutil_python": 0.761430671199188, + "gpu_gap_stddev_us": 2.674241904138252 + } + } + } + }, + "flip_fake_planar_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "flipType": [ + "BOTH" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Flip RGBAf32 fake-planar reformat-flip-reformat baseline (advanced)." + }, + "baselines": { + "flip_fake_planar_float4_advanced[InOutDataType=float4][shape=64x1080x1920][flipType=BOTH][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 11475.027777931839, + "gpu_time_us_python": 11490.331961891821, + "gpu_noise_us_cpp": 5.7139999999999995, + "gpu_noise_us_python": 4.694, + "gpu_bwutil_cpp": 0.7138980728511773, + "gpu_bwutil_python": 0.7129472417732048, + "gpu_gap_stddev_us": 2.051021072461809 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 9341.3001519662, + "gpu_time_us_python": 9358.06875758695, + "gpu_noise_us_cpp": 20.16, + "gpu_noise_us_python": 17.035, + "gpu_bwutil_cpp": 0.6688724991486128, + "gpu_bwutil_python": 0.6676740636487379, + "gpu_gap_stddev_us": 4.542796783597911 + } + } + } + } + } +} diff --git a/bench/config/operators/gammacontrast.json b/bench/config/operators/gammacontrast.json new file mode 100644 index 000000000..db8deb23d --- /dev/null +++ b/bench/config/operators/gammacontrast.json @@ -0,0 +1,953 @@ +{ + "benchmark": "gammacontrast", + "configs": { + "gammacontrast_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "GammaContrast RGB8 (basic)." + }, + "baselines": { + "gammacontrast_uchar3_basic[InOutDataType=uchar3][shape=64x1080x1920][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1310.24078335054, + "gpu_time_us_python": 1325.69220924108, + "gpu_noise_us_cpp": 3.0279999999999996, + "gpu_noise_us_python": 4.3, + "gpu_bwutil_cpp": 0.39077062695236964, + "gpu_bwutil_python": 0.3862171842309102, + "gpu_gap_stddev_us": 2.742515698340371 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1170.9530386403248, + "gpu_time_us_python": 1186.117098423975, + "gpu_noise_us_cpp": 3.4825000000000004, + "gpu_noise_us_python": 3.8525, + "gpu_bwutil_cpp": 0.33353097497037854, + "gpu_bwutil_python": 0.32927524769785466, + "gpu_gap_stddev_us": 3.6970461197957394 + } + }, + "gammacontrast_uchar3_basic[InOutDataType=uchar3][shape=64x1080x1920][layout=NHWC][inputKind=Tensor]": { + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1159.9696640733, + "gpu_time_us_python": 1169.8886308593248, + "gpu_noise_us_cpp": 1.665, + "gpu_noise_us_python": 5.27, + "gpu_bwutil_cpp": 0.33669350651031477, + "gpu_bwutil_python": 0.3338358477353104, + "gpu_gap_stddev_us": 3.2796065867861093 + }, + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1264.67890533282, + "gpu_time_us_python": 1277.06106711168, + "gpu_noise_us_cpp": 1.3119999999999998, + "gpu_noise_us_python": 3.724, + "gpu_bwutil_cpp": 0.40484882019053153, + "gpu_bwutil_python": 0.40092539775733577, + "gpu_gap_stddev_us": 2.6175366294872964 + } + } + } + }, + "gammacontrast_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "GammaContrast RGBA8 (advanced)." + }, + "baselines": { + "gammacontrast_uchar4_advanced[InOutDataType=uchar4][shape=64x1080x1920][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1640.0316995732403, + "gpu_time_us_python": 1655.2361220577798, + "gpu_noise_us_cpp": 2.9379999999999997, + "gpu_noise_us_python": 2.8240000000000003, + "gpu_bwutil_cpp": 0.4162547665907125, + "gpu_bwutil_python": 0.41243072855125684, + "gpu_gap_stddev_us": 0.7376589427366042 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1401.260540084325, + "gpu_time_us_python": 1419.8780929198997, + "gpu_noise_us_cpp": 3.2550000000000003, + "gpu_noise_us_python": 5.484999999999999, + "gpu_bwutil_cpp": 0.3716209480312919, + "gpu_bwutil_python": 0.36674406667281273, + "gpu_gap_stddev_us": 2.4427539126114355 + } + }, + "gammacontrast_uchar4_advanced[InOutDataType=uchar4][shape=64x1080x1920][layout=NHWC][inputKind=Tensor]": { + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1465.5732964545998, + "gpu_time_us_python": 1479.0414890299248, + "gpu_noise_us_cpp": 2.4699999999999998, + "gpu_noise_us_python": 4.052499999999999, + "gpu_bwutil_cpp": 0.35531810565481486, + "gpu_bwutil_python": 0.352066433769558, + "gpu_gap_stddev_us": 3.972760433455256 + }, + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1623.38653729454, + "gpu_time_us_python": 1635.0071447114199, + "gpu_noise_us_cpp": 1.1540000000000001, + "gpu_noise_us_python": 3.3840000000000003, + "gpu_bwutil_cpp": 0.42052284526926736, + "gpu_bwutil_python": 0.4175339017272199, + "gpu_gap_stddev_us": 1.4556201603829013 + } + } + } + }, + "gammacontrast_u8_varshape_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "GammaContrast single-channel varshape (advanced)." + }, + "baselines": { + "gammacontrast_u8_varshape_1080p_advanced[InOutDataType=uint8][shape=128x1080x1920][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 866.67865820986, + "gpu_time_us_python": 883.3099284755601, + "gpu_noise_us_cpp": 3.8920000000000003, + "gpu_noise_us_python": 3.6719999999999997, + "gpu_bwutil_cpp": 0.39384344986376996, + "gpu_bwutil_python": 0.38642748450839753, + "gpu_gap_stddev_us": 0.9447892761927782 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 704.1250450165501, + "gpu_time_us_python": 720.03215968775, + "gpu_noise_us_cpp": 3.0500000000000003, + "gpu_noise_us_python": 7.255, + "gpu_bwutil_cpp": 0.369757453138127, + "gpu_bwutil_python": 0.3615908723271946, + "gpu_gap_stddev_us": 1.6367470417291459 + } + } + } + }, + "gammacontrast_rgba_u8_varshape_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "GammaContrast RGBA8 varshape (advanced)." + }, + "baselines": { + "gammacontrast_rgba_u8_varshape_1080p_advanced[InOutDataType=uchar4][shape=128x1080x1920][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3260.5569833284403, + "gpu_time_us_python": 3280.2815870804598, + "gpu_noise_us_cpp": 2.8020000000000005, + "gpu_noise_us_python": 5.260000000000001, + "gpu_bwutil_cpp": 0.41874565470366115, + "gpu_bwutil_python": 0.4162265938450731, + "gpu_gap_stddev_us": 3.831925522176803 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2787.50589675655, + "gpu_time_us_python": 2806.4510376610997, + "gpu_noise_us_cpp": 6.9325, + "gpu_noise_us_python": 13.6125, + "gpu_bwutil_cpp": 0.373628366399444, + "gpu_bwutil_python": 0.3711089920716298, + "gpu_gap_stddev_us": 5.119740787902117 + } + } + } + }, + "gammacontrast_f32_varshape_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "GammaContrast single-channel varshape (advanced)." + }, + "baselines": { + "gammacontrast_f32_varshape_1080p_advanced[InOutDataType=float32][shape=128x1080x1920][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2670.851278305, + "gpu_time_us_python": 2689.7803676398, + "gpu_noise_us_cpp": 2.9200000000000004, + "gpu_noise_us_python": 4.394, + "gpu_bwutil_cpp": 0.5112010273484269, + "gpu_bwutil_python": 0.5076014969158225, + "gpu_gap_stddev_us": 3.7470839560080256 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2529.3571067185003, + "gpu_time_us_python": 2548.56237404195, + "gpu_noise_us_cpp": 4.2925, + "gpu_noise_us_python": 8.515, + "gpu_bwutil_cpp": 0.41174845181989994, + "gpu_bwutil_python": 0.4086474906945606, + "gpu_gap_stddev_us": 1.2041561198536948 + } + } + } + }, + "gammacontrast_planar_nchw_uchar3_lcg_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "GammaContrast RGB8 native planar NCHW with deterministic LCG input (basic)." + }, + "baselines": { + "gammacontrast_planar_nchw_uchar3_lcg_basic[InOutDataType=uchar3][shape=64x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1365.5654500763198, + "gpu_time_us_python": 1376.72916526856, + "gpu_noise_us_cpp": 2.7860000000000005, + "gpu_noise_us_python": 3.474, + "gpu_bwutil_cpp": 0.37493859564947785, + "gpu_bwutil_python": 0.37189857051950387, + "gpu_gap_stddev_us": 0.6059287218993071 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1199.34739855355, + "gpu_time_us_python": 1211.501337674125, + "gpu_noise_us_cpp": 1.965, + "gpu_noise_us_python": 3.335, + "gpu_bwutil_cpp": 0.3256361341962615, + "gpu_bwutil_python": 0.32237807402913393, + "gpu_gap_stddev_us": 4.4043639164698 + } + }, + "gammacontrast_planar_nchw_uchar3_lcg_basic[InOutDataType=uchar3][shape=64x1080x1920][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1488.6659750268798, + "gpu_time_us_python": 1502.920879805, + "gpu_noise_us_cpp": 3.12, + "gpu_noise_us_python": 4.138, + "gpu_bwutil_cpp": 0.34393424233203407, + "gpu_bwutil_python": 0.34067272760996903, + "gpu_gap_stddev_us": 1.6415496367190878 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1140.376403547, + "gpu_time_us_python": 1154.446629428375, + "gpu_noise_us_cpp": 4.9825, + "gpu_noise_us_python": 5.1425, + "gpu_bwutil_cpp": 0.3424767237823439, + "gpu_bwutil_python": 0.3383035949406282, + "gpu_gap_stddev_us": 3.140774790367177 + } + } + } + }, + "gammacontrast_planar_nchw_uchar3_lcg_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "GammaContrast RGB8 native planar NCHW Tensor with deterministic LCG input (advanced). Same-tier native reference for the advanced FakePlanar case." + }, + "baselines": { + "gammacontrast_planar_nchw_uchar3_lcg_advanced[InOutDataType=uchar3][shape=64x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1365.13747082064, + "gpu_time_us_python": 1377.1672881229601, + "gpu_noise_us_cpp": 0.9700000000000001, + "gpu_noise_us_python": 2.498, + "gpu_bwutil_cpp": 0.3750562718790159, + "gpu_bwutil_python": 0.3717795759976857, + "gpu_gap_stddev_us": 1.2852037069904392 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1199.810924280875, + "gpu_time_us_python": 1212.80840385085, + "gpu_noise_us_cpp": 2.255, + "gpu_noise_us_python": 4.595000000000001, + "gpu_bwutil_cpp": 0.3255119012785876, + "gpu_bwutil_python": 0.3220176168239575, + "gpu_gap_stddev_us": 2.9912695018947275 + } + } + } + }, + "gammacontrast_fakeplanar_nchw_uchar3_lcg_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "GammaContrast RGB8 fake-planar NCHW via reformat with deterministic LCG input (advanced)." + }, + "baselines": { + "gammacontrast_fakeplanar_nchw_uchar3_lcg_advanced[InOutDataType=uchar3][shape=64x1080x1920][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3664.8277420928002, + "gpu_time_us_python": 3681.3191154423394, + "gpu_noise_us_cpp": 4.409999999999999, + "gpu_noise_us_python": 2.882, + "gpu_bwutil_cpp": 0.4191199973448267, + "gpu_bwutil_python": 0.4172421968003171, + "gpu_gap_stddev_us": 2.7099122909243287 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 3027.2647305955, + "gpu_time_us_python": 3042.164214760125, + "gpu_noise_us_cpp": 2.5125, + "gpu_noise_us_python": 3.69, + "gpu_bwutil_cpp": 0.3870060858846147, + "gpu_bwutil_python": 0.38511220575281035, + "gpu_gap_stddev_us": 3.0047408243964098 + } + } + } + }, + "gammacontrast_float3_lcg_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "GammaContrast RGBf32 Tensor NHWC/NCHW comparison with deterministic LCG input (basic)." + }, + "baselines": { + "gammacontrast_float3_lcg_basic[InOutDataType=float3][shape=32x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1970.8811079628201, + "gpu_time_us_python": 1982.91008520412, + "gpu_noise_us_cpp": 1.35, + "gpu_noise_us_python": 2.526, + "gpu_bwutil_cpp": 0.5195667000380441, + "gpu_bwutil_python": 0.5164153288569818, + "gpu_gap_stddev_us": 0.9291011223898263 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1843.3987580233, + "gpu_time_us_python": 1855.807538891025, + "gpu_noise_us_cpp": 1.9900000000000002, + "gpu_noise_us_python": 3.9925, + "gpu_bwutil_cpp": 0.42373106328284366, + "gpu_bwutil_python": 0.4208936645202957, + "gpu_gap_stddev_us": 1.4485227325192893 + } + }, + "gammacontrast_float3_lcg_basic[InOutDataType=float3][shape=32x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1817.3670056937, + "gpu_time_us_python": 1829.37611940984, + "gpu_noise_us_cpp": 2.084, + "gpu_noise_us_python": 4.73, + "gpu_bwutil_cpp": 0.5634556265180573, + "gpu_bwutil_python": 0.5597561793685591, + "gpu_gap_stddev_us": 1.3362047219117201 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1682.643050409975, + "gpu_time_us_python": 1695.1368738306, + "gpu_noise_us_cpp": 3.8225, + "gpu_noise_us_python": 6.4075, + "gpu_bwutil_cpp": 0.4642194963353169, + "gpu_bwutil_python": 0.4607946247164466, + "gpu_gap_stddev_us": 1.8907297418446936 + } + } + } + }, + "gammacontrast_planar_nchw_float3_lcg_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "GammaContrast RGBf32 native planar NCHW Tensor with deterministic LCG input (advanced). Same-tier native reference for the advanced FakePlanar case." + }, + "baselines": { + "gammacontrast_planar_nchw_float3_lcg_advanced[InOutDataType=float3][shape=32x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1971.54259878522, + "gpu_time_us_python": 1984.3133831586797, + "gpu_noise_us_cpp": 3.2159999999999997, + "gpu_noise_us_python": 4.204, + "gpu_bwutil_cpp": 0.5193928036710296, + "gpu_bwutil_python": 0.5160490640673198, + "gpu_gap_stddev_us": 3.0936268091386934 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1843.477307252425, + "gpu_time_us_python": 1854.5573880278, + "gpu_noise_us_cpp": 2.9125, + "gpu_noise_us_python": 3.2775000000000003, + "gpu_bwutil_cpp": 0.4237155203915053, + "gpu_bwutil_python": 0.421181988697227, + "gpu_gap_stddev_us": 1.622879885228756 + } + } + } + }, + "gammacontrast_planar_nchw_float3_varshape_lcg_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "GammaContrast RGBf32 native planar NCHW VarShape with deterministic LCG input (advanced)." + }, + "baselines": { + "gammacontrast_planar_nchw_float3_varshape_lcg_advanced[InOutDataType=float3][shape=32x1080x1920][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2156.7894669467996, + "gpu_time_us_python": 2171.0704657698598, + "gpu_noise_us_cpp": 3.432, + "gpu_noise_us_python": 3.0300000000000002, + "gpu_bwutil_cpp": 0.4747822105848689, + "gpu_bwutil_python": 0.47165897485708114, + "gpu_gap_stddev_us": 1.3956816222653987 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1980.2819569586, + "gpu_time_us_python": 1994.8511052727, + "gpu_noise_us_cpp": 4.42, + "gpu_noise_us_python": 3.56, + "gpu_bwutil_cpp": 0.39443961883106277, + "gpu_bwutil_python": 0.39156002060338324, + "gpu_gap_stddev_us": 1.280383804184909 + } + } + } + }, + "gammacontrast_fakeplanar_nchw_float3_lcg_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "GammaContrast RGBf32 fake-planar NCHW via reformat with deterministic LCG input (advanced)." + }, + "baselines": { + "gammacontrast_fakeplanar_nchw_float3_lcg_advanced[InOutDataType=float3][shape=32x1080x1920][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4497.74082217894, + "gpu_time_us_python": 4516.60430261064, + "gpu_noise_us_cpp": 3.5, + "gpu_noise_us_python": 3.6740000000000004, + "gpu_bwutil_cpp": 0.683010157734149, + "gpu_bwutil_python": 0.6801572722946483, + "gpu_gap_stddev_us": 1.8804489982393093 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 3873.9114253948496, + "gpu_time_us_python": 3890.0422576528, + "gpu_noise_us_cpp": 4.932499999999999, + "gpu_noise_us_python": 4.859999999999999, + "gpu_bwutil_cpp": 0.6048448866549329, + "gpu_bwutil_python": 0.6023391961459882, + "gpu_gap_stddev_us": 2.1601485829578744 + } + } + } + }, + "gammacontrast_planar_nchw_uchar4_lcg_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "GammaContrast RGBA8 native planar NCHW tensor-only (advanced); RGBA8p varshape is unsupported by the Python image API." + }, + "baselines": { + "gammacontrast_planar_nchw_uchar4_lcg_advanced[InOutDataType=uchar4][shape=64x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1828.4329777052, + "gpu_time_us_python": 1840.1560504416004, + "gpu_noise_us_cpp": 1.908, + "gpu_noise_us_python": 2.65, + "gpu_bwutil_cpp": 0.373364162828492, + "gpu_bwutil_python": 0.3709856720737183, + "gpu_gap_stddev_us": 0.6834132112979856 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1573.5625105661752, + "gpu_time_us_python": 1586.995079864, + "gpu_noise_us_cpp": 1.8650000000000002, + "gpu_noise_us_python": 4.07, + "gpu_bwutil_cpp": 0.3309297984869844, + "gpu_bwutil_python": 0.32811720754617246, + "gpu_gap_stddev_us": 3.341100836508588 + } + } + } + }, + "gammacontrast_fakeplanar_nchw_uchar4_lcg_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "GammaContrast RGBA8 fake-planar NCHW via reformat with deterministic LCG input (advanced)." + }, + "baselines": { + "gammacontrast_fakeplanar_nchw_uchar4_lcg_advanced[InOutDataType=uchar4][shape=64x1080x1920][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4716.70951839926, + "gpu_time_us_python": 4736.6143640481805, + "gpu_noise_us_cpp": 2.9739999999999998, + "gpu_noise_us_python": 2.62, + "gpu_bwutil_cpp": 0.43420189461613373, + "gpu_bwutil_python": 0.43237698897908255, + "gpu_gap_stddev_us": 2.3491153567384764 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 3900.5471593055, + "gpu_time_us_python": 3916.5242670103503, + "gpu_noise_us_cpp": 2.7025000000000006, + "gpu_noise_us_python": 3.6725, + "gpu_bwutil_cpp": 0.40048416366498574, + "gpu_bwutil_python": 0.39884967978150054, + "gpu_gap_stddev_us": 1.790363846835425 + } + } + } + }, + "gammacontrast_planar_nchw_float4_lcg_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "GammaContrast RGBAf32 native planar NCHW with deterministic LCG input (advanced)." + }, + "baselines": { + "gammacontrast_planar_nchw_float4_lcg_advanced[InOutDataType=float4][shape=16x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1288.1426895663399, + "gpu_time_us_python": 1299.39640677162, + "gpu_noise_us_cpp": 2.2279999999999998, + "gpu_noise_us_python": 4.388, + "gpu_bwutil_cpp": 0.5299637223798045, + "gpu_bwutil_python": 0.5253739017881555, + "gpu_gap_stddev_us": 0.6842715721161534 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1203.0403677975248, + "gpu_time_us_python": 1216.359142806225, + "gpu_noise_us_cpp": 4.5125, + "gpu_noise_us_python": 4.66, + "gpu_bwutil_cpp": 0.4328484915800823, + "gpu_bwutil_python": 0.4280993242820886, + "gpu_gap_stddev_us": 2.6500316595446174 + } + }, + "gammacontrast_planar_nchw_float4_lcg_advanced[InOutDataType=float4][shape=16x1080x1920][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1406.6433282687, + "gpu_time_us_python": 1419.4393449507, + "gpu_noise_us_cpp": 1.4580000000000002, + "gpu_noise_us_python": 2.4299999999999997, + "gpu_bwutil_cpp": 0.48531847846007103, + "gpu_bwutil_python": 0.48094351948181135, + "gpu_gap_stddev_us": 0.624606792765767 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1297.7524528574, + "gpu_time_us_python": 1311.8441029630499, + "gpu_noise_us_cpp": 2.4124999999999996, + "gpu_noise_us_python": 3.875, + "gpu_bwutil_cpp": 0.4012627312744884, + "gpu_bwutil_python": 0.3969442026516494, + "gpu_gap_stddev_us": 1.466385319699836 + } + } + } + }, + "gammacontrast_fakeplanar_nchw_float4_lcg_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "GammaContrast RGBAf32 fake-planar NCHW via reformat with deterministic LCG input (advanced)." + }, + "baselines": { + "gammacontrast_fakeplanar_nchw_float4_lcg_advanced[InOutDataType=float4][shape=16x1080x1920][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3281.3534272181, + "gpu_time_us_python": 3298.9319142542, + "gpu_noise_us_cpp": 3.272, + "gpu_noise_us_python": 3.724, + "gpu_bwutil_cpp": 0.6241331598429951, + "gpu_bwutil_python": 0.6208074250679231, + "gpu_gap_stddev_us": 1.6355021780583603 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2850.4538602586, + "gpu_time_us_python": 2863.92608488145, + "gpu_noise_us_cpp": 3.75, + "gpu_noise_us_python": 6.46, + "gpu_bwutil_cpp": 0.5480067477049155, + "gpu_bwutil_python": 0.5454266316414252, + "gpu_gap_stddev_us": 1.8550383910962696 + } + } + } + }, + "gammacontrast_scalar_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "ScalarGamma" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "GammaContrast RGB8 host-scalar gamma/gain overload (basic). Same shape and gamma=0.75 as gammacontrast_uchar3_basic's Tensor row, but gamma/gain are kernel launch arguments -- no device gamma tensor is staged." + }, + "baselines": { + "gammacontrast_scalar_uchar3_basic[InOutDataType=uchar3][shape=64x1080x1920][layout=NHWC][inputKind=ScalarGamma]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1428.12523692996, + "gpu_time_us_python": 1440.10181070738, + "gpu_noise_us_cpp": 1.6300000000000001, + "gpu_noise_us_python": 2.8, + "gpu_bwutil_cpp": 0.35851299426489946, + "gpu_bwutil_python": 0.35553130693038815, + "gpu_gap_stddev_us": 2.4767620701163238 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1305.1464346857, + "gpu_time_us_python": 1314.98623054518, + "gpu_noise_us_cpp": 1.1300000000000001, + "gpu_noise_us_python": 2.3379999999999996, + "gpu_bwutil_cpp": 0.2992337417781096, + "gpu_bwutil_python": 0.2969905208176479, + "gpu_gap_stddev_us": 1.2661448969563935 + } + } + } + }, + "gammacontrast_scalar_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "ScalarGamma" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "GammaContrast RGBf32 host-scalar gamma/gain overload, interleaved and planar (advanced). Mirrors gammacontrast_float3_lcg_basic with the scalar kernels." + }, + "baselines": { + "gammacontrast_scalar_float3_advanced[InOutDataType=float3][shape=32x1080x1920][layout=NCHW][inputKind=ScalarGamma]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1943.49485718922, + "gpu_time_us_python": 1953.80446889918, + "gpu_noise_us_cpp": 1.388, + "gpu_noise_us_python": 3.66, + "gpu_bwutil_cpp": 0.5268873528418583, + "gpu_bwutil_python": 0.5241079306891545, + "gpu_gap_stddev_us": 2.4837015889639074 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1757.53427977266, + "gpu_time_us_python": 1767.3832045090198, + "gpu_noise_us_cpp": 0.6719999999999999, + "gpu_noise_us_python": 2.42, + "gpu_bwutil_cpp": 0.4444338819049259, + "gpu_bwutil_python": 0.44195924918577506, + "gpu_gap_stddev_us": 1.0441929090511126 + } + }, + "gammacontrast_scalar_float3_advanced[InOutDataType=float3][shape=32x1080x1920][layout=NHWC][inputKind=ScalarGamma]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1808.3546085285802, + "gpu_time_us_python": 1820.41807397128, + "gpu_noise_us_cpp": 2.244, + "gpu_noise_us_python": 4.992, + "gpu_bwutil_cpp": 0.5662622174172269, + "gpu_bwutil_python": 0.5625104284968926, + "gpu_gap_stddev_us": 1.2018545783117027 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1671.4310502382, + "gpu_time_us_python": 1681.1104866638002, + "gpu_noise_us_cpp": 0.6699999999999999, + "gpu_noise_us_python": 1.8980000000000001, + "gpu_bwutil_cpp": 0.4673358557602846, + "gpu_bwutil_python": 0.4646459134225037, + "gpu_gap_stddev_us": 0.4935672039227892 + } + } + } + } + } +} diff --git a/bench/config/operators/gaussian.json b/bench/config/operators/gaussian.json new file mode 100644 index 000000000..de779f10e --- /dev/null +++ b/bench/config/operators/gaussian.json @@ -0,0 +1,2472 @@ +{ + "benchmark": "gaussian", + "configs": { + "gaussian_reflect_s05_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "40x720x1280" + ], + "border": [ + "REFLECT" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "float64_axes": { + "sigma": [ + 0.5 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Gaussian RGB8 reflect s05 (advanced)." + }, + "baselines": { + "gaussian_reflect_s05_uchar3_advanced[InOutDataType=uchar3][shape=40x720x1280][border=REFLECT][layout=NHWC][inputKind=Tensor][sigma=0.5]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1070.33005020472, + "gpu_time_us_python": 1080.7965249823599, + "gpu_noise_us_cpp": 1.8160000000000003, + "gpu_noise_us_python": 3.2600000000000002, + "gpu_bwutil_cpp": 0.13287829880235097, + "gpu_bwutil_python": 0.1315915101848392 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1105.2780278114, + "gpu_time_us_python": 1115.24854377618, + "gpu_noise_us_cpp": 0.8879999999999999, + "gpu_noise_us_python": 2.524, + "gpu_bwutil_cpp": 0.09814242152461683, + "gpu_bwutil_python": 0.0972650504788307 + } + }, + "gaussian_reflect_s05_uchar3_advanced[InOutDataType=uchar3][shape=40x720x1280][border=REFLECT][layout=NHWC][inputKind=VarShape][sigma=0.5]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1180.42561174422, + "gpu_time_us_python": 1194.67280561728, + "gpu_noise_us_cpp": 1.45, + "gpu_noise_us_python": 3.438, + "gpu_bwutil_cpp": 0.12048508183049433, + "gpu_bwutil_python": 0.1190489943948376 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 994.5778795177399, + "gpu_time_us_python": 1006.3833476135402, + "gpu_noise_us_cpp": 0.9740000000000002, + "gpu_noise_us_python": 2.482, + "gpu_bwutil_cpp": 0.10906608174871665, + "gpu_bwutil_python": 0.10778687050413324 + } + } + } + }, + "gaussian_reflect_s05_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "48x720x1280" + ], + "border": [ + "REFLECT" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "float64_axes": { + "sigma": [ + 0.5 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Gaussian RGBf32 reflect s05 (advanced)." + }, + "baselines": { + "gaussian_reflect_s05_float3_advanced[InOutDataType=float3][shape=48x720x1280][border=REFLECT][layout=NHWC][inputKind=Tensor][sigma=0.5]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1297.4801263209997, + "gpu_time_us_python": 1309.6276938777798, + "gpu_noise_us_cpp": 1.3519999999999999, + "gpu_noise_us_python": 2.982, + "gpu_bwutil_cpp": 0.5261511880676942, + "gpu_bwutil_python": 0.5212737869663295 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1125.64168208842, + "gpu_time_us_python": 1136.94208011462, + "gpu_noise_us_cpp": 0.786, + "gpu_noise_us_python": 3.402, + "gpu_bwutil_cpp": 0.4625613489276351, + "gpu_bwutil_python": 0.4579666575703669 + } + }, + "gaussian_reflect_s05_float3_advanced[InOutDataType=float3][shape=48x720x1280][border=REFLECT][layout=NHWC][inputKind=VarShape][sigma=0.5]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1397.6737166057198, + "gpu_time_us_python": 1410.5322827620798, + "gpu_noise_us_cpp": 1.47, + "gpu_noise_us_python": 3.07, + "gpu_bwutil_cpp": 0.4884350255072589, + "gpu_bwutil_python": 0.4839829723029153 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1232.99427114678, + "gpu_time_us_python": 1245.901237494, + "gpu_noise_us_cpp": 0.9620000000000001, + "gpu_noise_us_python": 4.244, + "gpu_bwutil_cpp": 0.4222877786514213, + "gpu_bwutil_python": 0.4179152969984557 + } + } + } + }, + "gaussian_reflect_s05_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "40x720x1280" + ], + "border": [ + "REFLECT" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "float64_axes": { + "sigma": [ + 0.5 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Gaussian RGBA8 reflect s05 (advanced)." + }, + "baselines": { + "gaussian_reflect_s05_uchar4_advanced[InOutDataType=uchar4][shape=40x720x1280][border=REFLECT][layout=NHWC][inputKind=Tensor][sigma=0.5]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1302.4013907644799, + "gpu_time_us_python": 1315.6126449602602, + "gpu_noise_us_cpp": 1.484, + "gpu_noise_us_python": 4.76, + "gpu_bwutil_cpp": 0.14560154252908503, + "gpu_bwutil_python": 0.14414051962076996 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1145.67369141386, + "gpu_time_us_python": 1155.9287823762802, + "gpu_noise_us_cpp": 1.458, + "gpu_noise_us_python": 3.94, + "gpu_bwutil_cpp": 0.12624265319318212, + "gpu_bwutil_python": 0.12512296003079773 + } + }, + "gaussian_reflect_s05_uchar4_advanced[InOutDataType=uchar4][shape=40x720x1280][border=REFLECT][layout=NHWC][inputKind=VarShape][sigma=0.5]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1317.47218082702, + "gpu_time_us_python": 1332.0955740018399, + "gpu_noise_us_cpp": 1.254, + "gpu_noise_us_python": 4.582, + "gpu_bwutil_cpp": 0.1439359678414618, + "gpu_bwutil_python": 0.14235610006310812 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1187.9442496322001, + "gpu_time_us_python": 1200.3305843132998, + "gpu_noise_us_cpp": 1.0699999999999998, + "gpu_noise_us_python": 3.55, + "gpu_bwutil_cpp": 0.1217505611270703, + "gpu_bwutil_python": 0.12049485160622384 + } + } + } + }, + "gaussian_reflect_s05_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "32x720x1280" + ], + "border": [ + "REFLECT" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "float64_axes": { + "sigma": [ + 0.5 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Gaussian RGBAf32 reflect s05 (advanced)." + }, + "baselines": { + "gaussian_reflect_s05_float4_advanced[InOutDataType=float4][shape=32x720x1280][border=REFLECT][layout=NHWC][inputKind=Tensor][sigma=0.5]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1227.72229000074, + "gpu_time_us_python": 1240.10632059956, + "gpu_noise_us_cpp": 1.966, + "gpu_noise_us_python": 3.4939999999999998, + "gpu_bwutil_cpp": 0.4942631433335031, + "gpu_bwutil_python": 0.4893268071949873 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1137.4280502426998, + "gpu_time_us_python": 1149.9748006755199, + "gpu_noise_us_cpp": 1.056, + "gpu_noise_us_python": 3.832, + "gpu_bwutil_cpp": 0.4069051249331195, + "gpu_bwutil_python": 0.40246950836853224 + } + }, + "gaussian_reflect_s05_float4_advanced[InOutDataType=float4][shape=32x720x1280][border=REFLECT][layout=NHWC][inputKind=VarShape][sigma=0.5]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1365.7027014220798, + "gpu_time_us_python": 1379.6657975127398, + "gpu_noise_us_cpp": 1.968, + "gpu_noise_us_python": 2.81, + "gpu_bwutil_cpp": 0.4443277806202162, + "gpu_bwutil_python": 0.4398307850125686 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1238.70664860844, + "gpu_time_us_python": 1252.9127531510999, + "gpu_noise_us_cpp": 0.9019999999999999, + "gpu_noise_us_python": 4.1240000000000006, + "gpu_bwutil_cpp": 0.37363593900541925, + "gpu_bwutil_python": 0.3694024828119666 + } + } + } + }, + "gaussian_reflect_s05_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "72x720x1280" + ], + "border": [ + "REFLECT" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "float64_axes": { + "sigma": [ + 0.5 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Gaussian single-channel reflect s05 (advanced)." + }, + "baselines": { + "gaussian_reflect_s05_scalar_advanced[InOutDataType=float32][shape=72x720x1280][border=REFLECT][layout=NHWC][inputKind=Tensor][sigma=0.5]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1214.49972124386, + "gpu_time_us_python": 1226.68575568298, + "gpu_noise_us_cpp": 0.952, + "gpu_noise_us_python": 3.434, + "gpu_bwutil_cpp": 0.28105104026821615, + "gpu_bwutil_python": 0.2782604196171096 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1117.6627995712001, + "gpu_time_us_python": 1127.5454906643, + "gpu_noise_us_cpp": 0.5, + "gpu_noise_us_python": 2.4939999999999998, + "gpu_bwutil_cpp": 0.23293183244479682, + "gpu_bwutil_python": 0.2308902441231659 + } + }, + "gaussian_reflect_s05_scalar_advanced[InOutDataType=float32][shape=72x720x1280][border=REFLECT][layout=NHWC][inputKind=VarShape][sigma=0.5]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1254.1164102871403, + "gpu_time_us_python": 1267.91292829324, + "gpu_noise_us_cpp": 0.9879999999999999, + "gpu_noise_us_python": 3.7319999999999993, + "gpu_bwutil_cpp": 0.2721734090618985, + "gpu_bwutil_python": 0.2692113726722938 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1165.391509921, + "gpu_time_us_python": 1177.54620622726, + "gpu_noise_us_cpp": 1.0859999999999999, + "gpu_noise_us_python": 2.4379999999999997, + "gpu_bwutil_cpp": 0.2233921109667211, + "gpu_bwutil_python": 0.22108628401574296 + } + }, + "gaussian_reflect_s05_scalar_advanced[InOutDataType=uint8][shape=72x720x1280][border=REFLECT][layout=NHWC][inputKind=Tensor][sigma=0.5]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1228.27833062446, + "gpu_time_us_python": 1240.91140802302, + "gpu_noise_us_cpp": 1.97, + "gpu_noise_us_python": 2.902, + "gpu_bwutil_cpp": 0.06947430799384949, + "gpu_bwutil_python": 0.06876800416808249 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1044.27770743762, + "gpu_time_us_python": 1055.69660163312, + "gpu_noise_us_cpp": 0.9880000000000001, + "gpu_noise_us_python": 2.712, + "gpu_bwutil_cpp": 0.06232520168329845, + "gpu_bwutil_python": 0.06165122847635399 + } + }, + "gaussian_reflect_s05_scalar_advanced[InOutDataType=uint8][shape=72x720x1280][border=REFLECT][layout=NHWC][inputKind=VarShape][sigma=0.5]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1342.92084719292, + "gpu_time_us_python": 1358.01962959586, + "gpu_noise_us_cpp": 2.434, + "gpu_noise_us_python": 4.630000000000001, + "gpu_bwutil_cpp": 0.06354326887681805, + "gpu_bwutil_python": 0.06283772443670693 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1098.36306028194, + "gpu_time_us_python": 1110.02623175304, + "gpu_noise_us_cpp": 0.632, + "gpu_noise_us_python": 2.612, + "gpu_bwutil_cpp": 0.05925618429101178, + "gpu_bwutil_python": 0.058633651400712215 + } + } + } + }, + "gaussian_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x720x1280" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "float64_axes": { + "sigma": [ + 0.8 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Gaussian RGB8 (advanced)." + }, + "baselines": { + "gaussian_uchar3_advanced[InOutDataType=uchar3][shape=32x720x1280][border=REPLICATE][layout=NHWC][inputKind=Tensor][sigma=0.8]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1672.2118476626201, + "gpu_time_us_python": 1684.5960492806596, + "gpu_noise_us_cpp": 1.238, + "gpu_noise_us_python": 3.37, + "gpu_bwutil_cpp": 0.06804107688451584, + "gpu_bwutil_python": 0.0675410227020596 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1532.8439027797401, + "gpu_time_us_python": 1543.8289344404998, + "gpu_noise_us_cpp": 0.784, + "gpu_noise_us_python": 2.602, + "gpu_bwutil_cpp": 0.05661352229027862, + "gpu_bwutil_python": 0.056210769683229224 + } + } + } + }, + "gaussian_1080x1920_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "float64_axes": { + "sigma": [ + 0.8 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Gaussian RGB8 (basic)." + }, + "baselines": { + "gaussian_1080x1920_uchar3_basic[InOutDataType=uchar3][shape=16x1080x1920][border=REPLICATE][layout=NHWC][inputKind=Tensor][sigma=0.8]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1877.88873690834, + "gpu_time_us_python": 1889.2913408446198, + "gpu_noise_us_cpp": 1.278, + "gpu_noise_us_python": 4.35, + "gpu_bwutil_cpp": 0.0681624595249004, + "gpu_bwutil_python": 0.06775078037868958 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1724.55246167074, + "gpu_time_us_python": 1735.3462119668202, + "gpu_noise_us_cpp": 1.152, + "gpu_noise_us_python": 4.874, + "gpu_bwutil_cpp": 0.05661014711249226, + "gpu_bwutil_python": 0.056258125566073534 + } + }, + "gaussian_1080x1920_uchar3_basic[InOutDataType=uchar3][shape=16x1080x1920][border=REPLICATE][layout=NHWC][inputKind=VarShape][sigma=0.8]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1960.9760105609598, + "gpu_time_us_python": 1976.65532943736, + "gpu_noise_us_cpp": 2.804, + "gpu_noise_us_python": 6.401999999999999, + "gpu_bwutil_cpp": 0.06527395833214318, + "gpu_bwutil_python": 0.06475653176287162 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1799.9535791025598, + "gpu_time_us_python": 1812.53759655778, + "gpu_noise_us_cpp": 1.36, + "gpu_noise_us_python": 3.5379999999999994, + "gpu_bwutil_cpp": 0.05423871574993352, + "gpu_bwutil_python": 0.05386222928174168 + } + } + } + }, + "gaussian_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x720x1280" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "float64_axes": { + "sigma": [ + 0.8 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Gaussian RGBf32 (advanced)." + }, + "baselines": { + "gaussian_float3_advanced[InOutDataType=float3][shape=32x720x1280][border=REPLICATE][layout=NHWC][inputKind=Tensor][sigma=0.8]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1819.9964120036198, + "gpu_time_us_python": 1832.4581825090202, + "gpu_noise_us_cpp": 1.9020000000000004, + "gpu_noise_us_python": 3.804, + "gpu_bwutil_cpp": 0.2500634791178543, + "gpu_bwutil_python": 0.24836433520070464 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1655.11928373878, + "gpu_time_us_python": 1665.64644280168, + "gpu_noise_us_cpp": 1.3139999999999998, + "gpu_noise_us_python": 3.022, + "gpu_bwutil_cpp": 0.2097243332816264, + "gpu_bwutil_python": 0.208399171651301 + } + } + } + }, + "gaussian_1080x1920_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "float64_axes": { + "sigma": [ + 0.8 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Gaussian RGBf32 (basic)." + }, + "baselines": { + "gaussian_1080x1920_float3_basic[InOutDataType=float3][shape=16x1080x1920][border=REPLICATE][layout=NHWC][inputKind=Tensor][sigma=0.8]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2028.4718009045198, + "gpu_time_us_python": 2041.86885765224, + "gpu_noise_us_cpp": 1.502, + "gpu_noise_us_python": 3.814, + "gpu_bwutil_cpp": 0.25240902090303435, + "gpu_bwutil_python": 0.25075375459678806 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1846.47576581026, + "gpu_time_us_python": 1856.7366633591002, + "gpu_noise_us_cpp": 1.472, + "gpu_noise_us_python": 3.2920000000000003, + "gpu_bwutil_cpp": 0.21148863978499718, + "gpu_bwutil_python": 0.21032003121813275 + } + } + } + }, + "gaussian_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "32x720x1280" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "float64_axes": { + "sigma": [ + 0.8 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Gaussian RGBA8 (advanced)." + }, + "baselines": { + "gaussian_uchar4_advanced[InOutDataType=uchar4][shape=32x720x1280][border=REPLICATE][layout=NHWC][inputKind=Tensor][sigma=0.8]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2247.6805908569, + "gpu_time_us_python": 2258.6303534808, + "gpu_noise_us_cpp": 1.798, + "gpu_noise_us_python": 5.366, + "gpu_bwutil_cpp": 0.06749404044350021, + "gpu_bwutil_python": 0.06716648364938241 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1616.0382753802398, + "gpu_time_us_python": 1626.0403120672, + "gpu_noise_us_cpp": 1.1919999999999997, + "gpu_noise_us_python": 2.0140000000000002, + "gpu_bwutil_cpp": 0.07159871364030168, + "gpu_bwutil_python": 0.071158348097414 + } + } + } + }, + "gaussian_1080x1920_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "float64_axes": { + "sigma": [ + 0.8 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Gaussian RGBA8 (advanced)." + }, + "baselines": { + "gaussian_1080x1920_uchar4_advanced[InOutDataType=uchar4][shape=16x1080x1920][border=REPLICATE][layout=NHWC][inputKind=Tensor][sigma=0.8]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2522.33158755056, + "gpu_time_us_python": 2537.9310194179, + "gpu_noise_us_cpp": 3.072, + "gpu_noise_us_python": 4.432, + "gpu_bwutil_cpp": 0.06766239887217315, + "gpu_bwutil_python": 0.06724700467855772 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1816.6898988294797, + "gpu_time_us_python": 1829.2717778769197, + "gpu_noise_us_cpp": 0.754, + "gpu_noise_us_python": 4.962000000000001, + "gpu_bwutil_cpp": 0.07165204119240239, + "gpu_bwutil_python": 0.07115938277080074 + } + } + } + }, + "gaussian_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "32x720x1280" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "float64_axes": { + "sigma": [ + 0.8 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Gaussian RGBAf32 (advanced)." + }, + "baselines": { + "gaussian_float4_advanced[InOutDataType=float4][shape=32x720x1280][border=REPLICATE][layout=NHWC][inputKind=Tensor][sigma=0.8]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2415.6255518526204, + "gpu_time_us_python": 2428.9741449519597, + "gpu_noise_us_cpp": 1.592, + "gpu_noise_us_python": 4.54, + "gpu_bwutil_cpp": 0.25120605001089436, + "gpu_bwutil_python": 0.24982558921174433 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2220.39969283916, + "gpu_time_us_python": 2232.5655798506205, + "gpu_noise_us_cpp": 0.842, + "gpu_noise_us_python": 2.5040000000000004, + "gpu_bwutil_cpp": 0.20844219581215126, + "gpu_bwutil_python": 0.20730673795603843 + } + } + } + }, + "gaussian_1080x1920_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "float64_axes": { + "sigma": [ + 0.8 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Gaussian RGBAf32 (advanced)." + }, + "baselines": { + "gaussian_1080x1920_float4_advanced[InOutDataType=float4][shape=16x1080x1920][border=REPLICATE][layout=NHWC][inputKind=Tensor][sigma=0.8]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2714.9426719472003, + "gpu_time_us_python": 2728.60605094738, + "gpu_noise_us_cpp": 1.502, + "gpu_noise_us_python": 4.042, + "gpu_bwutil_cpp": 0.25144995032139494, + "gpu_bwutil_python": 0.2501909952395761 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2496.2942727720797, + "gpu_time_us_python": 2508.55616283412, + "gpu_noise_us_cpp": 0.71, + "gpu_noise_us_python": 3.864, + "gpu_bwutil_cpp": 0.20858039921329624, + "gpu_bwutil_python": 0.20756113958152533 + } + } + } + }, + "gaussian_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "64x720x1280", + "32x1080x1920" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "float64_axes": { + "sigma": [ + 0.8 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Gaussian single-channel (advanced)." + }, + "baselines": { + "gaussian_scalar_advanced[InOutDataType=float32][shape=64x720x1280][border=REPLICATE][layout=NHWC][inputKind=Tensor][sigma=0.8]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1149.1984194586798, + "gpu_time_us_python": 1161.96306875804, + "gpu_noise_us_cpp": 2.4419999999999997, + "gpu_noise_us_python": 4.506, + "gpu_bwutil_cpp": 0.2640187023699757, + "gpu_bwutil_python": 0.26111983331430694 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1069.4506747944602, + "gpu_time_us_python": 1079.3353712741798, + "gpu_noise_us_cpp": 1.1300000000000001, + "gpu_noise_us_python": 2.678, + "gpu_bwutil_cpp": 0.2163846689991412, + "gpu_bwutil_python": 0.2144031540746743 + } + }, + "gaussian_scalar_advanced[InOutDataType=uint8][shape=64x720x1280][border=REPLICATE][layout=NHWC][inputKind=Tensor][sigma=0.8]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1169.0953621330602, + "gpu_time_us_python": 1181.52214880062, + "gpu_noise_us_cpp": 1.6179999999999999, + "gpu_noise_us_python": 3.016, + "gpu_bwutil_cpp": 0.06488135520846564, + "gpu_bwutil_python": 0.06419936713124778 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1157.14804330032, + "gpu_time_us_python": 1167.47216194506, + "gpu_noise_us_cpp": 0.63, + "gpu_noise_us_python": 2.7920000000000003, + "gpu_bwutil_cpp": 0.04999632792208998, + "gpu_bwutil_python": 0.04955428942466637 + } + }, + "gaussian_scalar_advanced[InOutDataType=float32][shape=32x1080x1920][border=REPLICATE][layout=NHWC][inputKind=Tensor][sigma=0.8]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1295.14723692708, + "gpu_time_us_python": 1307.7777537311, + "gpu_noise_us_cpp": 1.5719999999999998, + "gpu_noise_us_python": 5.006, + "gpu_bwutil_cpp": 0.2635497299496351, + "gpu_bwutil_python": 0.26100626119884424 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1204.21082962012, + "gpu_time_us_python": 1216.3055032170598, + "gpu_noise_us_cpp": 0.882, + "gpu_noise_us_python": 2.33, + "gpu_bwutil_cpp": 0.21619073990823962, + "gpu_bwutil_python": 0.21404247064174098 + } + }, + "gaussian_scalar_advanced[InOutDataType=uint8][shape=32x1080x1920][border=REPLICATE][layout=NHWC][inputKind=Tensor][sigma=0.8]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1315.89240850442, + "gpu_time_us_python": 1329.5031652872199, + "gpu_noise_us_cpp": 1.9920000000000002, + "gpu_noise_us_python": 5.156000000000001, + "gpu_bwutil_cpp": 0.06484855633645645, + "gpu_bwutil_python": 0.0641848079886312 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1302.83706408368, + "gpu_time_us_python": 1314.5591279788998, + "gpu_noise_us_cpp": 0.8480000000000001, + "gpu_noise_us_python": 3.2560000000000002, + "gpu_bwutil_cpp": 0.049956191484990975, + "gpu_bwutil_python": 0.049510924850042576 + } + } + } + }, + "gaussian_s08_varshape_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "24x1080x1920" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "float64_axes": { + "sigma": [ + 0.8 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Gaussian single-channel s08 varshape (advanced)." + }, + "baselines": { + "gaussian_s08_varshape_1080p_advanced[InOutDataType=float32][shape=24x1080x1920][border=REPLICATE][layout=NHWC][inputKind=VarShape][sigma=0.8]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1078.95801904128, + "gpu_time_us_python": 1093.4355559916999, + "gpu_noise_us_cpp": 1.5, + "gpu_noise_us_python": 3.728, + "gpu_bwutil_cpp": 0.2372679711044038, + "gpu_bwutil_python": 0.23412822610340997 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 990.1806551948, + "gpu_time_us_python": 1002.91043673436, + "gpu_noise_us_cpp": 0.9039999999999999, + "gpu_noise_us_python": 2.794, + "gpu_bwutil_cpp": 0.19719080253895188, + "gpu_bwutil_python": 0.19469114930061138 + } + }, + "gaussian_s08_varshape_1080p_advanced[InOutDataType=uint8][shape=24x1080x1920][border=REPLICATE][layout=NHWC][inputKind=VarShape][sigma=0.8]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1100.7965489167202, + "gpu_time_us_python": 1114.65392988024, + "gpu_noise_us_cpp": 1.8060000000000003, + "gpu_noise_us_python": 3.876, + "gpu_bwutil_cpp": 0.05814028448504675, + "gpu_bwutil_python": 0.05741897174669012 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1045.84042093364, + "gpu_time_us_python": 1058.5668736388402, + "gpu_noise_us_cpp": 0.842, + "gpu_noise_us_python": 4.898, + "gpu_bwutil_cpp": 0.04667404373149584, + "gpu_bwutil_python": 0.046113607955849054 + } + } + } + }, + "gaussian_u8_s25_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "8x1080x1920" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "float64_axes": { + "sigma": [ + 2.5 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Gaussian single-channel s25 (advanced)." + }, + "baselines": { + "gaussian_u8_s25_1080p_advanced[InOutDataType=uint8][shape=8x1080x1920][border=REPLICATE][layout=NHWC][inputKind=Tensor][sigma=2.5]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1241.51560778532, + "gpu_time_us_python": 1253.7815589747202, + "gpu_noise_us_cpp": 2.564, + "gpu_noise_us_python": 3.374, + "gpu_bwutil_cpp": 0.01718334174690898, + "gpu_bwutil_python": 0.01701536345371664 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1142.36447496744, + "gpu_time_us_python": 1153.45671805232, + "gpu_noise_us_cpp": 0.8119999999999999, + "gpu_noise_us_python": 2.548, + "gpu_bwutil_cpp": 0.014243443367662862, + "gpu_bwutil_python": 0.01410656836839166 + } + }, + "gaussian_u8_s25_1080p_advanced[InOutDataType=uint8][shape=8x1080x1920][border=REPLICATE][layout=NHWC][inputKind=VarShape][sigma=2.5]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1343.8548319932002, + "gpu_time_us_python": 1358.69056603262, + "gpu_noise_us_cpp": 1.966, + "gpu_noise_us_python": 3.816, + "gpu_bwutil_cpp": 0.01587480949301268, + "gpu_bwutil_python": 0.01570166149107432 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1234.47919678802, + "gpu_time_us_python": 1246.04079538148, + "gpu_noise_us_cpp": 1.002, + "gpu_noise_us_python": 2.706, + "gpu_bwutil_cpp": 0.01318062372095464, + "gpu_bwutil_python": 0.01305835390605442 + } + } + } + }, + "gaussian_f32_s25_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "3x1080x1920" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "float64_axes": { + "sigma": [ + 2.5 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Gaussian single-channel s25 (advanced)." + }, + "baselines": { + "gaussian_f32_s25_1080p_advanced[InOutDataType=float32][shape=3x1080x1920][border=REPLICATE][layout=NHWC][inputKind=Tensor][sigma=2.5]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1268.50654254634, + "gpu_time_us_python": 1281.34182962694, + "gpu_noise_us_cpp": 2.4739999999999998, + "gpu_noise_us_python": 4.51, + "gpu_bwutil_cpp": 0.02522657000167266, + "gpu_bwutil_python": 0.02497417563794412 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1166.19558833364, + "gpu_time_us_python": 1176.2401512855, + "gpu_noise_us_cpp": 1.0899999999999999, + "gpu_noise_us_python": 2.512, + "gpu_bwutil_cpp": 0.02092856019369422, + "gpu_bwutil_python": 0.02074988509153514 + } + }, + "gaussian_f32_s25_1080p_advanced[InOutDataType=float32][shape=3x1080x1920][border=REPLICATE][layout=NHWC][inputKind=VarShape][sigma=2.5]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1372.4736682381601, + "gpu_time_us_python": 1387.2143004304598, + "gpu_noise_us_cpp": 2.274, + "gpu_noise_us_python": 3.3600000000000003, + "gpu_bwutil_cpp": 0.02331565956704116, + "gpu_bwutil_python": 0.02306809596881608 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1258.10778668172, + "gpu_time_us_python": 1271.49774260858, + "gpu_noise_us_cpp": 0.9000000000000001, + "gpu_noise_us_python": 2.55, + "gpu_bwutil_cpp": 0.019399608315042817, + "gpu_bwutil_python": 0.01919541766340364 + } + } + } + }, + "gaussian_reflect_s05_uchar3_planar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "24x720x1280" + ], + "border": [ + "REFLECT" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "float64_axes": { + "sigma": [ + 0.5 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Gaussian RGB8 reflect s05 (advanced) (planar NCHW)." + }, + "baselines": { + "gaussian_reflect_s05_uchar3_planar_nchw_advanced[InOutDataType=uchar3][shape=24x720x1280][border=REFLECT][layout=NCHW][inputKind=Tensor][sigma=0.5]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1229.602068814, + "gpu_time_us_python": 1242.85103574714, + "gpu_noise_us_cpp": 1.638, + "gpu_noise_us_python": 3.8980000000000006, + "gpu_bwutil_cpp": 0.06939982472951026, + "gpu_bwutil_python": 0.06866031964053479 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1044.8645602663, + "gpu_time_us_python": 1054.7891924022401, + "gpu_noise_us_cpp": 1.272, + "gpu_noise_us_python": 2.8499999999999996, + "gpu_bwutil_cpp": 0.06229020256855241, + "gpu_bwutil_python": 0.06170422907730222 + } + }, + "gaussian_reflect_s05_uchar3_planar_nchw_advanced[InOutDataType=uchar3][shape=24x720x1280][border=REFLECT][layout=NCHW][inputKind=VarShape][sigma=0.5]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1401.9307594830002, + "gpu_time_us_python": 1416.65247180286, + "gpu_noise_us_cpp": 1.7880000000000003, + "gpu_noise_us_python": 3.968, + "gpu_bwutil_cpp": 0.06086891669734842, + "gpu_bwutil_python": 0.06023648921765198 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1151.0698097586198, + "gpu_time_us_python": 1163.64832349624, + "gpu_noise_us_cpp": 0.808, + "gpu_noise_us_python": 2.87, + "gpu_bwutil_cpp": 0.05654288732051836, + "gpu_bwutil_python": 0.05593196624190746 + } + } + } + }, + "gaussian_reflect_s05_uchar3_fakeplanar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "24x720x1280" + ], + "border": [ + "REFLECT" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "float64_axes": { + "sigma": [ + 0.5 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Gaussian RGB8 reflect s05 (advanced) (fake planar NCHW_FAKE tensor)." + }, + "baselines": { + "gaussian_reflect_s05_uchar3_fakeplanar_nchw_advanced[InOutDataType=uchar3][shape=24x720x1280][border=REFLECT][layout=NCHW_FAKE][inputKind=Tensor][sigma=0.5]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1102.9947194665199, + "gpu_time_us_python": 1121.02267442398, + "gpu_noise_us_cpp": 1.2, + "gpu_noise_us_python": 3.854, + "gpu_bwutil_cpp": 0.23209732525738014, + "gpu_bwutil_python": 0.22836590502898443 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1063.26441149262, + "gpu_time_us_python": 1076.06971280286, + "gpu_noise_us_cpp": 1.624, + "gpu_noise_us_python": 2.1719999999999997, + "gpu_bwutil_cpp": 0.18363676264419898, + "gpu_bwutil_python": 0.18145163477938056 + } + } + } + }, + "gaussian_reflect_s05_float3_planar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "24x720x1280" + ], + "border": [ + "REFLECT" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "float64_axes": { + "sigma": [ + 0.5 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Gaussian RGBf32 reflect s05 (advanced) (planar NCHW)." + }, + "baselines": { + "gaussian_reflect_s05_float3_planar_nchw_advanced[InOutDataType=float3][shape=24x720x1280][border=REFLECT][layout=NCHW][inputKind=Tensor][sigma=0.5]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1215.1338221731799, + "gpu_time_us_python": 1227.84407376964, + "gpu_noise_us_cpp": 1.454, + "gpu_noise_us_python": 5.052, + "gpu_bwutil_cpp": 0.28090397381286836, + "gpu_bwutil_python": 0.27799691899241796 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1118.43494368448, + "gpu_time_us_python": 1130.72372455868, + "gpu_noise_us_cpp": 1.008, + "gpu_noise_us_python": 2.194, + "gpu_bwutil_cpp": 0.23277107012648335, + "gpu_bwutil_python": 0.23024421995291872 + } + }, + "gaussian_reflect_s05_float3_planar_nchw_advanced[InOutDataType=float3][shape=24x720x1280][border=REFLECT][layout=NCHW][inputKind=VarShape][sigma=0.5]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1294.7212205483597, + "gpu_time_us_python": 1308.82158848816, + "gpu_noise_us_cpp": 1.846, + "gpu_noise_us_python": 3.4400000000000004, + "gpu_bwutil_cpp": 0.2636364126470639, + "gpu_bwutil_python": 0.26079727220049237 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1169.11764390774, + "gpu_time_us_python": 1182.61641108718, + "gpu_noise_us_cpp": 1.2599999999999998, + "gpu_noise_us_python": 4.784000000000001, + "gpu_bwutil_cpp": 0.22268014058580493, + "gpu_bwutil_python": 0.22014086783682618 + } + } + } + }, + "gaussian_reflect_s05_float3_fakeplanar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "24x720x1280" + ], + "border": [ + "REFLECT" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "float64_axes": { + "sigma": [ + 0.5 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Gaussian RGBf32 reflect s05 (advanced) (fake planar NCHW_FAKE tensor)." + }, + "baselines": { + "gaussian_reflect_s05_float3_fakeplanar_nchw_advanced[InOutDataType=float3][shape=24x720x1280][border=REFLECT][layout=NCHW_FAKE][inputKind=Tensor][sigma=0.5]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1551.3547633014, + "gpu_time_us_python": 1568.50552122228, + "gpu_noise_us_cpp": 1.3639999999999999, + "gpu_noise_us_python": 3.092, + "gpu_bwutil_cpp": 0.6600723321531483, + "gpu_bwutil_python": 0.6528562312924855 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1299.7895622500002, + "gpu_time_us_python": 1312.18415729658, + "gpu_noise_us_cpp": 1.474, + "gpu_noise_us_python": 2.9859999999999998, + "gpu_bwutil_cpp": 0.6008798015155115, + "gpu_bwutil_python": 0.5952048346870161 + } + } + } + }, + "gaussian_reflect_s05_uchar4_planar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "24x720x1280" + ], + "border": [ + "REFLECT" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "float64_axes": { + "sigma": [ + 0.5 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Gaussian RGBA8 reflect s05 (advanced) (planar NCHW)." + }, + "baselines": { + "gaussian_reflect_s05_uchar4_planar_nchw_advanced[InOutDataType=uchar4][shape=24x720x1280][border=REFLECT][layout=NCHW][inputKind=Tensor][sigma=0.5]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1634.00655879234, + "gpu_time_us_python": 1645.69886578266, + "gpu_noise_us_cpp": 1.8379999999999996, + "gpu_noise_us_python": 3.06, + "gpu_bwutil_cpp": 0.06963169426585707, + "gpu_bwutil_python": 0.06913718312177577 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1388.20468690288, + "gpu_time_us_python": 1399.20422258318, + "gpu_noise_us_cpp": 0.9279999999999999, + "gpu_noise_us_python": 2.836, + "gpu_bwutil_cpp": 0.06251218140765324, + "gpu_bwutil_python": 0.062020999913649635 + } + } + } + }, + "gaussian_reflect_s05_uchar4_fakeplanar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "24x720x1280" + ], + "border": [ + "REFLECT" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "float64_axes": { + "sigma": [ + 0.5 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Gaussian RGBA8 reflect s05 (advanced) (fake planar NCHW_FAKE tensor)." + }, + "baselines": { + "gaussian_reflect_s05_uchar4_fakeplanar_nchw_advanced[InOutDataType=uchar4][shape=24x720x1280][border=REFLECT][layout=NCHW_FAKE][inputKind=Tensor][sigma=0.5]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1349.71865284684, + "gpu_time_us_python": 1367.0769466232, + "gpu_noise_us_cpp": 1.3539999999999999, + "gpu_noise_us_python": 5.868, + "gpu_bwutil_cpp": 0.25289433156863667, + "gpu_bwutil_python": 0.24968408204058812 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1169.03939740026, + "gpu_time_us_python": 1183.79012064096, + "gpu_noise_us_cpp": 1.7579999999999998, + "gpu_noise_us_python": 3.2920000000000003, + "gpu_bwutil_cpp": 0.22269496749616763, + "gpu_bwutil_python": 0.219920123406862 + } + } + } + }, + "gaussian_reflect_s05_float4_planar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x720x1280" + ], + "border": [ + "REFLECT" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "float64_axes": { + "sigma": [ + 0.5 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Gaussian RGBAf32 reflect s05 (advanced) (planar NCHW)." + }, + "baselines": { + "gaussian_reflect_s05_float4_planar_nchw_advanced[InOutDataType=float4][shape=16x720x1280][border=REFLECT][layout=NCHW][inputKind=Tensor][sigma=0.5]": { + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 995.81763786642, + "gpu_time_us_python": 1006.4011954424799, + "gpu_noise_us_cpp": 0.844, + "gpu_noise_us_python": 2.6339999999999995, + "gpu_bwutil_cpp": 0.23238466548757186, + "gpu_bwutil_python": 0.22994218138419126 + }, + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1081.68681019332, + "gpu_time_us_python": 1094.84817514264, + "gpu_noise_us_cpp": 0.998, + "gpu_noise_us_python": 4.554, + "gpu_bwutil_cpp": 0.28049666180617894, + "gpu_bwutil_python": 0.2771269637232633 + } + }, + "gaussian_reflect_s05_float4_planar_nchw_advanced[InOutDataType=float4][shape=16x720x1280][border=REFLECT][layout=NCHW][inputKind=VarShape][sigma=0.5]": { + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1041.8362545846999, + "gpu_time_us_python": 1053.047292556, + "gpu_noise_us_cpp": 1.344, + "gpu_noise_us_python": 2.2239999999999998, + "gpu_bwutil_cpp": 0.22212016290610595, + "gpu_bwutil_python": 0.21975576242567474 + }, + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1153.1494780723401, + "gpu_time_us_python": 1168.5254291961198, + "gpu_noise_us_cpp": 2.3680000000000003, + "gpu_noise_us_python": 3.336, + "gpu_bwutil_cpp": 0.2631142616120329, + "gpu_bwutil_python": 0.25965702809369234 + } + } + } + }, + "gaussian_reflect_s05_float4_fakeplanar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x720x1280" + ], + "border": [ + "REFLECT" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "float64_axes": { + "sigma": [ + 0.5 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Gaussian RGBAf32 reflect s05 (advanced) (fake planar NCHW_FAKE tensor)." + }, + "baselines": { + "gaussian_reflect_s05_float4_fakeplanar_nchw_advanced[InOutDataType=float4][shape=16x720x1280][border=REFLECT][layout=NCHW_FAKE][inputKind=Tensor][sigma=0.5]": { + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1354.39723040602, + "gpu_time_us_python": 1367.9122171787399, + "gpu_noise_us_cpp": 2.132, + "gpu_noise_us_python": 4.006, + "gpu_bwutil_cpp": 0.5125803921837988, + "gpu_bwutil_python": 0.5075167942871514 + }, + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1559.24318745064, + "gpu_time_us_python": 1577.1281806275601, + "gpu_noise_us_cpp": 1.5500000000000003, + "gpu_noise_us_python": 2.7300000000000004, + "gpu_bwutil_cpp": 0.5837631423975164, + "gpu_bwutil_python": 0.5771445387580032 + } + } + } + }, + "gaussian_uchar3_planar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x720x1280" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "float64_axes": { + "sigma": [ + 0.8 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Gaussian RGB8 (advanced) (planar NCHW)." + }, + "baselines": { + "gaussian_uchar3_planar_nchw_advanced[InOutDataType=uchar3][shape=32x720x1280][border=REPLICATE][layout=NCHW][inputKind=Tensor][sigma=0.8]": { + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1728.8733145285598, + "gpu_time_us_python": 1740.3775994976, + "gpu_noise_us_cpp": 1.134, + "gpu_noise_us_python": 2.472, + "gpu_bwutil_cpp": 0.05019436992810093, + "gpu_bwutil_python": 0.04986270719952792 + }, + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1745.54601830614, + "gpu_time_us_python": 1759.24288561396, + "gpu_noise_us_cpp": 1.1179999999999999, + "gpu_noise_us_python": 3.7399999999999998, + "gpu_bwutil_cpp": 0.06518246156428256, + "gpu_bwutil_python": 0.06467570441377801 + } + } + } + }, + "gaussian_uchar3_fakeplanar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x720x1280" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "float64_axes": { + "sigma": [ + 0.8 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Gaussian RGB8 (advanced) (fake planar NCHW_FAKE tensor)." + }, + "baselines": { + "gaussian_uchar3_fakeplanar_nchw_advanced[InOutDataType=uchar3][shape=32x720x1280][border=REPLICATE][layout=NCHW_FAKE][inputKind=Tensor][sigma=0.8]": { + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2054.77413857564, + "gpu_time_us_python": 2068.9112895775797, + "gpu_noise_us_cpp": 0.9019999999999999, + "gpu_noise_us_python": 4.036, + "gpu_bwutil_cpp": 0.1266995996925598, + "gpu_bwutil_python": 0.12583382487583625 + }, + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2271.7870037639195, + "gpu_time_us_python": 2290.55234708614, + "gpu_noise_us_cpp": 1.73, + "gpu_noise_us_python": 3.0, + "gpu_bwutil_cpp": 0.1502501239872361, + "gpu_bwutil_python": 0.14901916790235345 + } + } + } + }, + "gaussian_1080x1920_uchar3_planar_nchw_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "float64_axes": { + "sigma": [ + 0.8 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Gaussian RGB8 (basic) (planar NCHW)." + }, + "baselines": { + "gaussian_1080x1920_uchar3_planar_nchw_basic[InOutDataType=uchar3][shape=16x1080x1920][border=REPLICATE][layout=NCHW][inputKind=Tensor][sigma=0.8]": { + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1948.2591726982798, + "gpu_time_us_python": 1960.01079957928, + "gpu_noise_us_cpp": 1.126, + "gpu_noise_us_python": 3.353999999999999, + "gpu_bwutil_cpp": 0.05010993652171184, + "gpu_bwutil_python": 0.049809570059329254 + }, + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1967.76624927804, + "gpu_time_us_python": 1979.8650843741202, + "gpu_noise_us_cpp": 2.4520000000000004, + "gpu_noise_us_python": 4.0280000000000005, + "gpu_bwutil_cpp": 0.06504905777444314, + "gpu_bwutil_python": 0.0646523136686185 + } + }, + "gaussian_1080x1920_uchar3_planar_nchw_basic[InOutDataType=uchar3][shape=16x1080x1920][border=REPLICATE][layout=NCHW][inputKind=VarShape][sigma=0.8]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2232.1743506773005, + "gpu_time_us_python": 2246.87019966278, + "gpu_noise_us_cpp": 1.3459999999999999, + "gpu_noise_us_python": 4.206, + "gpu_bwutil_cpp": 0.05734388764839713, + "gpu_bwutil_python": 0.056969313599869176 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2063.7201317020604, + "gpu_time_us_python": 2077.98938227088, + "gpu_noise_us_cpp": 0.9960000000000001, + "gpu_noise_us_python": 2.672, + "gpu_bwutil_cpp": 0.047306385408226084, + "gpu_bwutil_python": 0.046981672644618645 + } + } + } + }, + "gaussian_1080x1920_uchar3_fakeplanar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "float64_axes": { + "sigma": [ + 0.8 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Gaussian RGB8 (advanced) (fake planar NCHW_FAKE tensor)." + }, + "baselines": { + "gaussian_1080x1920_uchar3_fakeplanar_nchw_advanced[InOutDataType=uchar3][shape=16x1080x1920][border=REPLICATE][layout=NCHW_FAKE][inputKind=Tensor][sigma=0.8]": { + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2310.06426636228, + "gpu_time_us_python": 2322.77327069526, + "gpu_noise_us_cpp": 1.704, + "gpu_noise_us_python": 2.726, + "gpu_bwutil_cpp": 0.12678497979873687, + "gpu_bwutil_python": 0.12609132461620723 + }, + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2552.8213480628597, + "gpu_time_us_python": 2572.40611691234, + "gpu_noise_us_cpp": 1.1620000000000001, + "gpu_noise_us_python": 3.1860000000000004, + "gpu_bwutil_cpp": 0.15042297443162744, + "gpu_bwutil_python": 0.1492780765077399 + } + } + } + }, + "gaussian_float3_planar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x720x1280" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "float64_axes": { + "sigma": [ + 0.8 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Gaussian RGBf32 (advanced) (planar NCHW)." + }, + "baselines": { + "gaussian_float3_planar_nchw_advanced[InOutDataType=float3][shape=32x720x1280][border=REPLICATE][layout=NCHW][inputKind=Tensor][sigma=0.8]": { + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1595.8628288499401, + "gpu_time_us_python": 1606.52492535416, + "gpu_noise_us_cpp": 0.8159999999999998, + "gpu_noise_us_python": 2.9200000000000004, + "gpu_bwutil_cpp": 0.21751168277524754, + "gpu_bwutil_python": 0.21606835100125102 + }, + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1714.6404264623798, + "gpu_time_us_python": 1728.1867219841602, + "gpu_noise_us_cpp": 1.4, + "gpu_noise_us_python": 4.85, + "gpu_bwutil_cpp": 0.26542856758929256, + "gpu_bwutil_python": 0.26334916510616335 + } + } + } + }, + "gaussian_float3_fakeplanar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x720x1280" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "float64_axes": { + "sigma": [ + 0.8 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Gaussian RGBf32 (advanced) (fake planar NCHW_FAKE tensor)." + }, + "baselines": { + "gaussian_float3_fakeplanar_nchw_advanced[InOutDataType=float3][shape=32x720x1280][border=REPLICATE][layout=NCHW_FAKE][inputKind=Tensor][sigma=0.8]": { + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2631.7171710886, + "gpu_time_us_python": 2645.6101753836797, + "gpu_noise_us_cpp": 2.8579999999999997, + "gpu_noise_us_python": 3.106, + "gpu_bwutil_cpp": 0.39569451253848237, + "gpu_bwutil_python": 0.3936167655348512 + }, + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3011.0974328516, + "gpu_time_us_python": 3032.2120367762, + "gpu_noise_us_cpp": 1.624, + "gpu_noise_us_python": 5.215999999999999, + "gpu_bwutil_cpp": 0.4534366515617275, + "gpu_bwutil_python": 0.4502801690380943 + } + } + } + }, + "gaussian_1080x1920_float3_planar_nchw_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "float64_axes": { + "sigma": [ + 0.8 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Gaussian RGBf32 (basic) (planar NCHW)." + }, + "baselines": { + "gaussian_1080x1920_float3_planar_nchw_basic[InOutDataType=float3][shape=16x1080x1920][border=REPLICATE][layout=NCHW][inputKind=Tensor][sigma=0.8]": { + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1799.6630224404203, + "gpu_time_us_python": 1810.5597862509799, + "gpu_noise_us_cpp": 1.698, + "gpu_noise_us_python": 3.3619999999999997, + "gpu_bwutil_cpp": 0.21698986099411824, + "gpu_bwutil_python": 0.21568448964121054 + }, + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1932.78198146786, + "gpu_time_us_python": 1945.26366615708, + "gpu_noise_us_cpp": 1.3399999999999999, + "gpu_noise_us_python": 3.018, + "gpu_bwutil_cpp": 0.26490523676264816, + "gpu_bwutil_python": 0.26320525442377773 + } + } + } + }, + "gaussian_1080x1920_float3_fakeplanar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "float64_axes": { + "sigma": [ + 0.8 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Gaussian RGBf32 (advanced) (fake planar NCHW_FAKE tensor)." + }, + "baselines": { + "gaussian_1080x1920_float3_fakeplanar_nchw_advanced[InOutDataType=float3][shape=16x1080x1920][border=REPLICATE][layout=NCHW_FAKE][inputKind=Tensor][sigma=0.8]": { + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2945.08190323318, + "gpu_time_us_python": 2959.1722157422, + "gpu_noise_us_cpp": 2.3400000000000003, + "gpu_noise_us_python": 3.742, + "gpu_bwutil_cpp": 0.39779048102580666, + "gpu_bwutil_python": 0.3958965720768281 + }, + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3370.0803972106396, + "gpu_time_us_python": 3388.02536698068, + "gpu_noise_us_cpp": 2.276, + "gpu_noise_us_python": 2.924, + "gpu_bwutil_cpp": 0.45577827212897615, + "gpu_bwutil_python": 0.4533642400413023 + } + } + } + }, + "gaussian_uchar4_planar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "32x720x1280" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "float64_axes": { + "sigma": [ + 0.8 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Gaussian RGBA8 (advanced) (planar NCHW)." + }, + "baselines": { + "gaussian_uchar4_planar_nchw_advanced[InOutDataType=uchar4][shape=32x720x1280][border=REPLICATE][layout=NCHW][inputKind=Tensor][sigma=0.8]": { + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2301.19779656782, + "gpu_time_us_python": 2312.06762371438, + "gpu_noise_us_cpp": 1.028, + "gpu_noise_us_python": 4.0600000000000005, + "gpu_bwutil_cpp": 0.05028087097903498, + "gpu_bwutil_python": 0.05004454822462188 + }, + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2322.5441239332804, + "gpu_time_us_python": 2335.18420447518, + "gpu_noise_us_cpp": 0.9899999999999999, + "gpu_noise_us_python": 4.378, + "gpu_bwutil_cpp": 0.06531870615604174, + "gpu_bwutil_python": 0.0649650688198811 + } + } + } + }, + "gaussian_uchar4_fakeplanar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "32x720x1280" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "float64_axes": { + "sigma": [ + 0.8 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Gaussian RGBA8 (advanced) (fake planar NCHW_FAKE tensor)." + }, + "baselines": { + "gaussian_uchar4_fakeplanar_nchw_advanced[InOutDataType=uchar4][shape=32x720x1280][border=REPLICATE][layout=NCHW_FAKE][inputKind=Tensor][sigma=0.8]": { + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2246.4915411812403, + "gpu_time_us_python": 2260.06428916168, + "gpu_noise_us_cpp": 0.792, + "gpu_noise_us_python": 1.904, + "gpu_bwutil_cpp": 0.15451592235852324, + "gpu_bwutil_python": 0.15358812457493604 + }, + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2992.18898898074, + "gpu_time_us_python": 3010.83866795512, + "gpu_noise_us_cpp": 1.302, + "gpu_noise_us_python": 3.6459999999999995, + "gpu_bwutil_cpp": 0.15210085524882633, + "gpu_bwutil_python": 0.15115881009121207 + } + } + } + }, + "gaussian_1080x1920_uchar4_planar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "float64_axes": { + "sigma": [ + 0.8 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Gaussian RGBA8 (advanced) (planar NCHW)." + }, + "baselines": { + "gaussian_1080x1920_uchar4_planar_nchw_advanced[InOutDataType=uchar4][shape=16x1080x1920][border=REPLICATE][layout=NCHW][inputKind=Tensor][sigma=0.8]": { + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2593.1227035128804, + "gpu_time_us_python": 2605.67706882232, + "gpu_noise_us_cpp": 0.9260000000000002, + "gpu_noise_us_python": 3.1800000000000006, + "gpu_bwutil_cpp": 0.050197979803307605, + "gpu_bwutil_python": 0.04995613907307276 + }, + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2618.54030089938, + "gpu_time_us_python": 2631.93000318704, + "gpu_noise_us_cpp": 1.5340000000000003, + "gpu_noise_us_python": 5.7540000000000004, + "gpu_bwutil_cpp": 0.06517704827795377, + "gpu_bwutil_python": 0.06484523702249144 + } + } + } + }, + "gaussian_1080x1920_uchar4_fakeplanar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "float64_axes": { + "sigma": [ + 0.8 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Gaussian RGBA8 (advanced) (fake planar NCHW_FAKE tensor)." + }, + "baselines": { + "gaussian_1080x1920_uchar4_fakeplanar_nchw_advanced[InOutDataType=uchar4][shape=16x1080x1920][border=REPLICATE][layout=NCHW_FAKE][inputKind=Tensor][sigma=0.8]": { + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2525.08049332488, + "gpu_time_us_python": 2538.3873925064, + "gpu_noise_us_cpp": 0.952, + "gpu_noise_us_python": 2.968, + "gpu_bwutil_cpp": 0.15465192059915894, + "gpu_bwutil_python": 0.15384126207652268 + }, + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3364.7082130414396, + "gpu_time_us_python": 3383.70006986562, + "gpu_noise_us_cpp": 1.4780000000000002, + "gpu_noise_us_python": 3.268, + "gpu_bwutil_cpp": 0.15216864412939868, + "gpu_bwutil_python": 0.15131419098555007 + } + } + } + }, + "gaussian_float4_planar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "32x720x1280" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "float64_axes": { + "sigma": [ + 0.8 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Gaussian RGBAf32 (advanced) (planar NCHW)." + }, + "baselines": { + "gaussian_float4_planar_nchw_advanced[InOutDataType=float4][shape=32x720x1280][border=REPLICATE][layout=NCHW][inputKind=Tensor][sigma=0.8]": { + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2123.25369665174, + "gpu_time_us_python": 2133.46223952402, + "gpu_noise_us_cpp": 0.9620000000000001, + "gpu_noise_us_python": 2.1959999999999997, + "gpu_bwutil_cpp": 0.21797913505056235, + "gpu_bwutil_python": 0.21693619643106263 + }, + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2279.3047948316603, + "gpu_time_us_python": 2292.46335839304, + "gpu_noise_us_cpp": 1.442, + "gpu_noise_us_python": 3.524, + "gpu_bwutil_cpp": 0.2662299499821182, + "gpu_bwutil_python": 0.264702231152517 + } + } + } + }, + "gaussian_float4_fakeplanar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "32x720x1280" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "float64_axes": { + "sigma": [ + 0.8 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Gaussian RGBAf32 (advanced) (fake planar NCHW_FAKE tensor)." + }, + "baselines": { + "gaussian_float4_fakeplanar_nchw_advanced[InOutDataType=float4][shape=32x720x1280][border=REPLICATE][layout=NCHW_FAKE][inputKind=Tensor][sigma=0.8]": { + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3775.5887764602603, + "gpu_time_us_python": 3789.823247652, + "gpu_noise_us_cpp": 2.636, + "gpu_noise_us_python": 3.8339999999999996, + "gpu_bwutil_cpp": 0.36775048708411573, + "gpu_bwutil_python": 0.3663693577576168 + }, + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4280.94481452032, + "gpu_time_us_python": 4301.97074292064, + "gpu_noise_us_cpp": 2.976, + "gpu_noise_us_python": 4.068, + "gpu_bwutil_cpp": 0.4252461514544964, + "gpu_bwutil_python": 0.42316768988897213 + } + } + } + }, + "gaussian_1080x1920_float4_planar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "float64_axes": { + "sigma": [ + 0.8 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Gaussian RGBAf32 (advanced) (planar NCHW)." + }, + "baselines": { + "gaussian_1080x1920_float4_planar_nchw_advanced[InOutDataType=float4][shape=16x1080x1920][border=REPLICATE][layout=NCHW][inputKind=Tensor][sigma=0.8]": { + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2394.12175042284, + "gpu_time_us_python": 2404.11325647714, + "gpu_noise_us_cpp": 1.2280000000000002, + "gpu_noise_us_python": 2.442, + "gpu_bwutil_cpp": 0.21748188933107798, + "gpu_bwutil_python": 0.21657810496758456 + }, + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2571.4117973547, + "gpu_time_us_python": 2584.0165246393, + "gpu_noise_us_cpp": 1.766, + "gpu_noise_us_python": 4.0760000000000005, + "gpu_bwutil_cpp": 0.26548581965074874, + "gpu_bwutil_python": 0.26419093117331116 + } + } + } + }, + "gaussian_1080x1920_float4_fakeplanar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "float64_axes": { + "sigma": [ + 0.8 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Gaussian RGBAf32 (advanced) (fake planar NCHW_FAKE tensor)." + }, + "baselines": { + "gaussian_1080x1920_float4_fakeplanar_nchw_advanced[InOutDataType=float4][shape=16x1080x1920][border=REPLICATE][layout=NCHW_FAKE][inputKind=Tensor][sigma=0.8]": { + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4248.3481989068005, + "gpu_time_us_python": 4266.061118497641, + "gpu_noise_us_cpp": 3.8920000000000003, + "gpu_noise_us_python": 5.7620000000000005, + "gpu_bwutil_cpp": 0.3676802985665465, + "gpu_bwutil_python": 0.3661539390782194 + }, + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4823.725134090981, + "gpu_time_us_python": 4844.88663673396, + "gpu_noise_us_cpp": 2.6740000000000004, + "gpu_noise_us_python": 4.728, + "gpu_bwutil_cpp": 0.4245706539188546, + "gpu_bwutil_python": 0.42271755191208193 + } + } + } + } + } +} diff --git a/bench/config/operators/gaussiannoise.json b/bench/config/operators/gaussiannoise.json new file mode 100644 index 000000000..3709b6d05 --- /dev/null +++ b/bench/config/operators/gaussiannoise.json @@ -0,0 +1,1315 @@ +{ + "benchmark": "gaussiannoise", + "configs": { + "gaussiannoise_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "2x1080x1920" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "GaussianNoise RGB8 (basic)." + }, + "baselines": { + "gaussiannoise_uchar3_basic[InOutDataType=uchar3][shape=2x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 361.08576203794, + "gpu_time_us_python": 366.2987089812, + "gpu_noise_us_cpp": 2.418, + "gpu_noise_us_python": 2.9339999999999997, + "gpu_bwutil_cpp": 0.04431110990516949, + "gpu_bwutil_python": 0.04368181930445732, + "gpu_gap_stddev_us": 2.0820643570988775 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 362.03071199712, + "gpu_time_us_python": 376.80972746113997, + "gpu_noise_us_cpp": 4.794, + "gpu_noise_us_python": 5.014, + "gpu_bwutil_cpp": 0.03371041920978664, + "gpu_bwutil_python": 0.0323919589415808, + "gpu_gap_stddev_us": 2.5243555419486845 + } + }, + "gaussiannoise_uchar3_basic[InOutDataType=uchar3][shape=2x1080x1920][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 371.4306706229801, + "gpu_time_us_python": 388.7983622498, + "gpu_noise_us_cpp": 2.998, + "gpu_noise_us_python": 3.6120000000000005, + "gpu_bwutil_cpp": 0.04307844488501356, + "gpu_bwutil_python": 0.04115605173322008, + "gpu_gap_stddev_us": 2.6823500826006033 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 374.80883685114, + "gpu_time_us_python": 385.71759496642, + "gpu_noise_us_cpp": 3.5340000000000003, + "gpu_noise_us_python": 4.1, + "gpu_bwutil_cpp": 0.03256145945198826, + "gpu_bwutil_python": 0.031642087542593976, + "gpu_gap_stddev_us": 2.455459259917178 + } + } + } + }, + "gaussiannoise_uchar3_planar_nchw_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "2x1080x1920" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "GaussianNoise RGB8 (basic) (native planar NCHW)." + }, + "baselines": { + "gaussiannoise_uchar3_planar_nchw_basic[InOutDataType=uchar3][shape=2x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 476.98404350968, + "gpu_time_us_python": 484.45329777402003, + "gpu_noise_us_cpp": 2.718, + "gpu_noise_us_python": 3.322, + "gpu_bwutil_cpp": 0.033544179924003104, + "gpu_bwutil_python": 0.03302699230324967, + "gpu_gap_stddev_us": 0.5092153324401267 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 474.50780157244, + "gpu_time_us_python": 486.77099061556, + "gpu_noise_us_cpp": 3.318, + "gpu_noise_us_python": 4.984, + "gpu_bwutil_cpp": 0.02571912657565902, + "gpu_bwutil_python": 0.02507197011107142, + "gpu_gap_stddev_us": 1.566754582763929 + } + }, + "gaussiannoise_uchar3_planar_nchw_basic[InOutDataType=uchar3][shape=2x1080x1920][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 523.49846859556, + "gpu_time_us_python": 539.6145032805999, + "gpu_noise_us_cpp": 3.3200000000000003, + "gpu_noise_us_python": 3.728, + "gpu_bwutil_cpp": 0.030564064945713898, + "gpu_bwutil_python": 0.0296515228215166, + "gpu_gap_stddev_us": 0.8387782261172183 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 508.53980064632, + "gpu_time_us_python": 520.67362164366, + "gpu_noise_us_cpp": 3.6160000000000005, + "gpu_noise_us_python": 4.7299999999999995, + "gpu_bwutil_cpp": 0.023997903222518262, + "gpu_bwutil_python": 0.023439584714928637, + "gpu_gap_stddev_us": 2.375936530328341 + } + } + } + }, + "gaussiannoise_uchar3_planar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "2x1080x1920" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "GaussianNoise RGB8 native-planar Tensor comparison (advanced)." + }, + "baselines": { + "gaussiannoise_uchar3_planar_nchw_advanced[InOutDataType=uchar3][shape=2x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 476.80537745488, + "gpu_time_us_python": 484.61284055286006, + "gpu_noise_us_cpp": 2.3579999999999997, + "gpu_noise_us_python": 3.768, + "gpu_bwutil_cpp": 0.03355671660012944, + "gpu_bwutil_python": 0.033016063393641104, + "gpu_gap_stddev_us": 0.4885686505266858 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 474.67595111846003, + "gpu_time_us_python": 486.52670321925996, + "gpu_noise_us_cpp": 3.22, + "gpu_noise_us_python": 4.684, + "gpu_bwutil_cpp": 0.02570999118004056, + "gpu_bwutil_python": 0.02508427115671718, + "gpu_gap_stddev_us": 0.9367453937275544 + } + } + } + }, + "gaussiannoise_uchar3_fakeplanar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "2x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "GaussianNoise RGB8 (advanced) (fake-planar NCHW_FAKE tensor comparison)." + }, + "baselines": { + "gaussiannoise_uchar3_fakeplanar_nchw_advanced[InOutDataType=uchar3][shape=2x1080x1920][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 421.80634105266, + "gpu_time_us_python": 441.17962210984007, + "gpu_noise_us_cpp": 2.654, + "gpu_noise_us_python": 3.2, + "gpu_bwutil_cpp": 0.11379667023817275, + "gpu_bwutil_python": 0.10879974290337073, + "gpu_gap_stddev_us": 0.3305636339505115 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 411.55085548204005, + "gpu_time_us_python": 429.56803313046004, + "gpu_noise_us_cpp": 2.254, + "gpu_noise_us_python": 3.812, + "gpu_bwutil_cpp": 0.08896307861388264, + "gpu_bwutil_python": 0.08523149191848181, + "gpu_gap_stddev_us": 1.66287279287279 + } + } + } + }, + "gaussiannoise_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "2x1080x1920" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "GaussianNoise RGBf32 tensor (basic)." + }, + "baselines": { + "gaussiannoise_float3_basic[InOutDataType=float3][shape=2x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 376.03967811468, + "gpu_time_us_python": 381.53250887922, + "gpu_noise_us_cpp": 2.114, + "gpu_noise_us_python": 2.534, + "gpu_bwutil_cpp": 0.17019551392950377, + "gpu_bwutil_python": 0.167745292447366, + "gpu_gap_stddev_us": 0.2517694009724721 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 377.06206633190004, + "gpu_time_us_python": 396.43506429384, + "gpu_noise_us_cpp": 2.822, + "gpu_noise_us_python": 7.56, + "gpu_bwutil_cpp": 0.12946698683682922, + "gpu_bwutil_python": 0.12315508162274827, + "gpu_gap_stddev_us": 3.4006950128861533 + } + } + } + }, + "gaussiannoise_float3_varshape_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "2x1080x1920" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "GaussianNoise RGBf32 var-shape (advanced)." + }, + "baselines": { + "gaussiannoise_float3_varshape_advanced[InOutDataType=float3][shape=2x1080x1920][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 389.50902817637996, + "gpu_time_us_python": 400.87002213773997, + "gpu_noise_us_cpp": 2.9579999999999997, + "gpu_noise_us_python": 2.906, + "gpu_bwutil_cpp": 0.1643099241957909, + "gpu_bwutil_python": 0.15965320984060827, + "gpu_gap_stddev_us": 0.19657280776570438 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 395.68568911482, + "gpu_time_us_python": 412.45037351818, + "gpu_noise_us_cpp": 3.4240000000000004, + "gpu_noise_us_python": 6.644, + "gpu_bwutil_cpp": 0.12337216375061344, + "gpu_bwutil_python": 0.11835956525482343, + "gpu_gap_stddev_us": 1.1878219854457464 + } + } + } + }, + "gaussiannoise_float3_planar_nchw_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "2x1080x1920" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "GaussianNoise planar RGBf32 tensor (basic)." + }, + "baselines": { + "gaussiannoise_float3_planar_nchw_basic[InOutDataType=float3][shape=2x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 508.26296966222, + "gpu_time_us_python": 514.28256424928, + "gpu_noise_us_cpp": 3.136, + "gpu_noise_us_python": 3.812, + "gpu_bwutil_cpp": 0.12591924803944993, + "gpu_bwutil_python": 0.12444597541097431, + "gpu_gap_stddev_us": 0.7706237859036889 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 510.09818249835996, + "gpu_time_us_python": 528.4074264410399, + "gpu_noise_us_cpp": 3.95, + "gpu_noise_us_python": 8.352, + "gpu_bwutil_cpp": 0.09569829699554862, + "gpu_bwutil_python": 0.09238638282009973, + "gpu_gap_stddev_us": 2.199869516584499 + } + } + } + }, + "gaussiannoise_float3_planar_nchw_varshape_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "2x1080x1920" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "GaussianNoise planar RGBf32 var-shape (advanced)." + }, + "baselines": { + "gaussiannoise_float3_planar_nchw_varshape_advanced[InOutDataType=float3][shape=2x1080x1920][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 571.8891500516199, + "gpu_time_us_python": 583.61885356044, + "gpu_noise_us_cpp": 3.094, + "gpu_noise_us_python": 4.214, + "gpu_bwutil_cpp": 0.11191008647030558, + "gpu_bwutil_python": 0.10966226003126027, + "gpu_gap_stddev_us": 2.0913915649252686 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 539.77130717806, + "gpu_time_us_python": 556.99259713826, + "gpu_noise_us_cpp": 4.736, + "gpu_noise_us_python": 6.534000000000001, + "gpu_bwutil_cpp": 0.0904375813026116, + "gpu_bwutil_python": 0.0876405954804361, + "gpu_gap_stddev_us": 1.5222780405071494 + } + } + } + }, + "gaussiannoise_float3_planar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "2x1080x1920" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "GaussianNoise RGBf32 native-planar Tensor comparison (advanced)." + }, + "baselines": { + "gaussiannoise_float3_planar_nchw_advanced[InOutDataType=float3][shape=2x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 508.12411084058004, + "gpu_time_us_python": 514.2309026114999, + "gpu_noise_us_cpp": 2.9840000000000004, + "gpu_noise_us_python": 3.46, + "gpu_bwutil_cpp": 0.12595371002386088, + "gpu_bwutil_python": 0.12445820744146349, + "gpu_gap_stddev_us": 0.6449777176876424 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 510.55043460852005, + "gpu_time_us_python": 529.4885794191599, + "gpu_noise_us_cpp": 4.1240000000000006, + "gpu_noise_us_python": 8.947999999999999, + "gpu_bwutil_cpp": 0.09561436830133643, + "gpu_bwutil_python": 0.09219744080764336, + "gpu_gap_stddev_us": 1.7982230309254423 + } + } + } + }, + "gaussiannoise_float3_fakeplanar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "2x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "GaussianNoise RGBf32 (advanced) (fake-planar NCHW_FAKE tensor comparison)." + }, + "baselines": { + "gaussiannoise_float3_fakeplanar_nchw_advanced[InOutDataType=float3][shape=2x1080x1920][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 544.1954088542, + "gpu_time_us_python": 562.6363486395401, + "gpu_noise_us_cpp": 2.7640000000000002, + "gpu_noise_us_python": 3.3160000000000003, + "gpu_bwutil_cpp": 0.3528146152634569, + "gpu_bwutil_python": 0.3412518069939309, + "gpu_gap_stddev_us": 0.8191152011885665 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 521.22148913958, + "gpu_time_us_python": 531.3719020177799, + "gpu_noise_us_cpp": 5.8100000000000005, + "gpu_noise_us_python": 4.598000000000001, + "gpu_bwutil_cpp": 0.280973102407902, + "gpu_bwutil_python": 0.2755968845339135, + "gpu_gap_stddev_us": 3.502733213788649 + } + } + } + }, + "gaussiannoise_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "2x1080x1920" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "GaussianNoise RGBA8 (advanced)." + }, + "baselines": { + "gaussiannoise_uchar4_advanced[InOutDataType=uchar4][shape=2x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 352.75533587760003, + "gpu_time_us_python": 365.50381325304, + "gpu_noise_us_cpp": 2.028, + "gpu_noise_us_python": 3.4239999999999995, + "gpu_bwutil_cpp": 0.0604762946706968, + "gpu_bwutil_python": 0.058367108313462854, + "gpu_gap_stddev_us": 0.7959626056105179 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 355.08557903220003, + "gpu_time_us_python": 368.52214968484, + "gpu_noise_us_cpp": 3.152, + "gpu_noise_us_python": 5.85, + "gpu_bwutil_cpp": 0.04582644527902983, + "gpu_bwutil_python": 0.04415431328107186, + "gpu_gap_stddev_us": 0.6961436411719073 + } + }, + "gaussiannoise_uchar4_advanced[InOutDataType=uchar4][shape=2x1080x1920][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 384.76145848974, + "gpu_time_us_python": 393.79925935354, + "gpu_noise_us_cpp": 2.918, + "gpu_noise_us_python": 3.156, + "gpu_bwutil_cpp": 0.05544562359284135, + "gpu_bwutil_python": 0.05417322096348905, + "gpu_gap_stddev_us": 0.5498153825128717 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 380.76678588884, + "gpu_time_us_python": 395.59933220428, + "gpu_noise_us_cpp": 3.052, + "gpu_noise_us_python": 4.736, + "gpu_bwutil_cpp": 0.04273562627594012, + "gpu_bwutil_python": 0.04113336977664106, + "gpu_gap_stddev_us": 1.275932607897146 + } + } + } + }, + "gaussiannoise_uchar4_planar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "2x1080x1920" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "GaussianNoise RGBA8 (advanced) (native planar NCHW)." + }, + "baselines": { + "gaussiannoise_uchar4_planar_nchw_advanced[InOutDataType=uchar4][shape=2x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 536.5934731660001, + "gpu_time_us_python": 548.2997101056401, + "gpu_noise_us_cpp": 2.39, + "gpu_noise_us_python": 3.4480000000000004, + "gpu_bwutil_cpp": 0.03975699891585082, + "gpu_bwutil_python": 0.03890841873802248, + "gpu_gap_stddev_us": 1.438584055393441 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 528.9831684253801, + "gpu_time_us_python": 540.6590992467001, + "gpu_noise_us_cpp": 3.604, + "gpu_noise_us_python": 5.202, + "gpu_bwutil_cpp": 0.03076056319555574, + "gpu_bwutil_python": 0.030096024870043904, + "gpu_gap_stddev_us": 1.1761118660564498 + } + } + } + }, + "gaussiannoise_uchar4_fakeplanar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "2x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "GaussianNoise RGBA8 (advanced) (fake-planar NCHW_FAKE tensor comparison)." + }, + "baselines": { + "gaussiannoise_uchar4_fakeplanar_nchw_advanced[InOutDataType=uchar4][shape=2x1080x1920][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 462.70822961564, + "gpu_time_us_python": 474.95530797445997, + "gpu_noise_us_cpp": 3.008, + "gpu_noise_us_python": 3.048, + "gpu_bwutil_cpp": 0.1383163242269942, + "gpu_bwutil_python": 0.13475302084794236, + "gpu_gap_stddev_us": 2.2834397709945002 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 434.6249326135, + "gpu_time_us_python": 451.77726833507995, + "gpu_noise_us_cpp": 2.7459999999999996, + "gpu_noise_us_python": 4.35, + "gpu_bwutil_cpp": 0.11231644274010108, + "gpu_bwutil_python": 0.10804986766227245, + "gpu_gap_stddev_us": 1.93335679477436 + } + } + } + }, + "gaussiannoise_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "2x1080x1920" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "GaussianNoise RGBAf32 (advanced)." + }, + "baselines": { + "gaussiannoise_float4_advanced[InOutDataType=float4][shape=2x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 379.36121082349996, + "gpu_time_us_python": 385.32930619884, + "gpu_noise_us_cpp": 2.326, + "gpu_noise_us_python": 2.5839999999999996, + "gpu_bwutil_cpp": 0.22493973387725846, + "gpu_bwutil_python": 0.2214562804042166, + "gpu_gap_stddev_us": 1.0499467353599157 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 377.39991370396, + "gpu_time_us_python": 389.0234464053, + "gpu_noise_us_cpp": 3.9, + "gpu_noise_us_python": 6.244, + "gpu_bwutil_cpp": 0.17246569111258997, + "gpu_bwutil_python": 0.16730969690468558, + "gpu_gap_stddev_us": 0.8577601150845622 + } + }, + "gaussiannoise_float4_advanced[InOutDataType=float4][shape=2x1080x1920][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 395.93769993768, + "gpu_time_us_python": 407.85151571012, + "gpu_noise_us_cpp": 2.5380000000000003, + "gpu_noise_us_python": 2.672, + "gpu_bwutil_cpp": 0.21552215206899303, + "gpu_bwutil_python": 0.20922671894028758, + "gpu_gap_stddev_us": 0.4838465711865978 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 406.2221347189, + "gpu_time_us_python": 422.3982125448, + "gpu_noise_us_cpp": 3.7239999999999993, + "gpu_noise_us_python": 5.465999999999999, + "gpu_bwutil_cpp": 0.16022981366468886, + "gpu_bwutil_python": 0.15410693067113493, + "gpu_gap_stddev_us": 2.2921070838696784 + } + } + } + }, + "gaussiannoise_float4_planar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "2x1080x1920" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "GaussianNoise RGBAf32 (advanced) (native planar NCHW)." + }, + "baselines": { + "gaussiannoise_float4_planar_nchw_advanced[InOutDataType=float4][shape=2x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 582.41424019542, + "gpu_time_us_python": 590.6031163233199, + "gpu_noise_us_cpp": 2.5799999999999996, + "gpu_noise_us_python": 3.146, + "gpu_bwutil_cpp": 0.14651682896063342, + "gpu_bwutil_python": 0.1444850837708464, + "gpu_gap_stddev_us": 0.7462010902641819 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 569.5799102507, + "gpu_time_us_python": 582.9504832985, + "gpu_noise_us_cpp": 4.696, + "gpu_noise_us_python": 6.617999999999999, + "gpu_bwutil_cpp": 0.11427367305716059, + "gpu_bwutil_python": 0.1116517238178257, + "gpu_gap_stddev_us": 1.7944104063904056 + } + }, + "gaussiannoise_float4_planar_nchw_advanced[InOutDataType=float4][shape=2x1080x1920][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 632.0855512129799, + "gpu_time_us_python": 648.3349316638199, + "gpu_noise_us_cpp": 3.0200000000000005, + "gpu_noise_us_python": 3.4299999999999997, + "gpu_bwutil_cpp": 0.13500295458732395, + "gpu_bwutil_python": 0.13162043695390419, + "gpu_gap_stddev_us": 1.6532954763867613 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 601.6654007251, + "gpu_time_us_python": 617.55858305076, + "gpu_noise_us_cpp": 4.416, + "gpu_noise_us_python": 6.295999999999999, + "gpu_bwutil_cpp": 0.10818003694078956, + "gpu_bwutil_python": 0.10539641106133169, + "gpu_gap_stddev_us": 5.037627376914284 + } + } + } + }, + "gaussiannoise_float4_fakeplanar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "2x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "GaussianNoise RGBAf32 (advanced) (fake-planar NCHW_FAKE tensor comparison)." + }, + "baselines": { + "gaussiannoise_float4_fakeplanar_nchw_advanced[InOutDataType=float4][shape=2x1080x1920][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 642.37235397502, + "gpu_time_us_python": 659.0004893111, + "gpu_noise_us_cpp": 2.342, + "gpu_noise_us_python": 2.804, + "gpu_bwutil_cpp": 0.3985228122708637, + "gpu_bwutil_python": 0.38846762121372497, + "gpu_gap_stddev_us": 1.0302253331554019 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 596.44441336188, + "gpu_time_us_python": 611.20135914458, + "gpu_noise_us_cpp": 3.9200000000000004, + "gpu_noise_us_python": 4.3100000000000005, + "gpu_bwutil_cpp": 0.3273709998721662, + "gpu_bwutil_python": 0.3194667416547604, + "gpu_gap_stddev_us": 0.5385514387578458 + } + } + } + }, + "gaussiannoise_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "GaussianNoise single-channel (advanced)." + }, + "baselines": { + "gaussiannoise_scalar_advanced[InOutDataType=float32][shape=16x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 551.1156818917, + "gpu_time_us_python": 565.2060995056801, + "gpu_noise_us_cpp": 4.61, + "gpu_noise_us_python": 5.747999999999999, + "gpu_bwutil_cpp": 0.3096754876643236, + "gpu_bwutil_python": 0.30195866405028965, + "gpu_gap_stddev_us": 1.641551655128206 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 496.75922699248, + "gpu_time_us_python": 511.88127314562007, + "gpu_noise_us_cpp": 7.2299999999999995, + "gpu_noise_us_python": 7.412000000000001, + "gpu_bwutil_cpp": 0.2620595107924695, + "gpu_bwutil_python": 0.25432457321391067, + "gpu_gap_stddev_us": 1.1440250390367546 + } + }, + "gaussiannoise_scalar_advanced[InOutDataType=float32][shape=16x1080x1920][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 582.22837558284, + "gpu_time_us_python": 589.8522503758599, + "gpu_noise_us_cpp": 5.836, + "gpu_noise_us_python": 6.742, + "gpu_bwutil_cpp": 0.2931288235190713, + "gpu_bwutil_python": 0.2893393292510036, + "gpu_gap_stddev_us": 1.249107987799667 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 517.84245486056, + "gpu_time_us_python": 531.1754820392, + "gpu_noise_us_cpp": 8.222000000000001, + "gpu_noise_us_python": 8.398, + "gpu_bwutil_cpp": 0.25139751333683036, + "gpu_bwutil_python": 0.2450673247419025, + "gpu_gap_stddev_us": 4.976369675040735 + } + }, + "gaussiannoise_scalar_advanced[InOutDataType=uint8][shape=16x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 565.4230388427201, + "gpu_time_us_python": 583.7319357075, + "gpu_noise_us_cpp": 5.0120000000000005, + "gpu_noise_us_python": 6.845999999999999, + "gpu_bwutil_cpp": 0.07546153132940228, + "gpu_bwutil_python": 0.07309489605210535, + "gpu_gap_stddev_us": 3.0275482146169126 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 575.37222260814, + "gpu_time_us_python": 594.16237525196, + "gpu_noise_us_cpp": 22.909999999999997, + "gpu_noise_us_python": 19.068, + "gpu_bwutil_cpp": 0.056571961740370935, + "gpu_bwutil_python": 0.0547723695747534, + "gpu_gap_stddev_us": 9.875734815363977 + } + }, + "gaussiannoise_scalar_advanced[InOutDataType=uint8][shape=16x1080x1920][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 603.35658225604, + "gpu_time_us_python": 616.0970268833, + "gpu_noise_us_cpp": 5.976000000000001, + "gpu_noise_us_python": 7.2700000000000005, + "gpu_bwutil_cpp": 0.07071625189476749, + "gpu_bwutil_python": 0.06925363802762702, + "gpu_gap_stddev_us": 2.558503019894013 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 625.03838452412, + "gpu_time_us_python": 637.6425285630601, + "gpu_noise_us_cpp": 16.31, + "gpu_noise_us_python": 19.119999999999997, + "gpu_bwutil_cpp": 0.0520667460477135, + "gpu_bwutil_python": 0.0510373116421539, + "gpu_gap_stddev_us": 3.998111329106287 + } + } + } + }, + "gaussiannoise_rgb_u8_perchannel_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "1x1080x1920" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "GaussianNoise RGB8 perchannel (advanced)." + }, + "baselines": { + "gaussiannoise_rgb_u8_perchannel_1080p_advanced[InOutDataType=uchar3][shape=1x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 355.43873176580007, + "gpu_time_us_python": 362.93789456226, + "gpu_noise_us_cpp": 3.1639999999999997, + "gpu_noise_us_python": 2.978, + "gpu_bwutil_cpp": 0.02250741502189872, + "gpu_bwutil_python": 0.022042419640010302, + "gpu_gap_stddev_us": 0.6526768924405005 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 354.97294508362, + "gpu_time_us_python": 368.98647400315997, + "gpu_noise_us_cpp": 2.43, + "gpu_noise_us_python": 5.037999999999999, + "gpu_bwutil_cpp": 0.0171901148561867, + "gpu_bwutil_python": 0.016536822532896938, + "gpu_gap_stddev_us": 0.8742225114414854 + } + }, + "gaussiannoise_rgb_u8_perchannel_1080p_advanced[InOutDataType=uchar3][shape=1x1080x1920][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 367.83371935670004, + "gpu_time_us_python": 381.83974007439997, + "gpu_noise_us_cpp": 2.878, + "gpu_noise_us_python": 3.6340000000000003, + "gpu_bwutil_cpp": 0.02174934813233168, + "gpu_bwutil_python": 0.02095157861125474, + "gpu_gap_stddev_us": 1.2669588406599688 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 369.25521328922, + "gpu_time_us_python": 381.06439489954, + "gpu_noise_us_cpp": 2.452, + "gpu_noise_us_python": 4.268, + "gpu_bwutil_cpp": 0.01652519609714732, + "gpu_bwutil_python": 0.01601325111739462, + "gpu_gap_stddev_us": 1.1258342526114984 + } + } + } + }, + "gaussiannoise_rgb_u8_perchannel_1080p_planar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "1x1080x1920" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "GaussianNoise RGB8 perchannel (advanced) (native planar NCHW)." + }, + "baselines": { + "gaussiannoise_rgb_u8_perchannel_1080p_planar_nchw_advanced[InOutDataType=uchar3][shape=1x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 464.35927214886, + "gpu_time_us_python": 476.47338417838, + "gpu_noise_us_cpp": 2.98, + "gpu_noise_us_python": 3.5100000000000002, + "gpu_bwutil_cpp": 0.01722809367253826, + "gpu_bwutil_python": 0.01679008252602302, + "gpu_gap_stddev_us": 0.1783187752232826 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 466.02285333348, + "gpu_time_us_python": 478.88017998530006, + "gpu_noise_us_cpp": 2.414, + "gpu_noise_us_python": 4.076, + "gpu_bwutil_cpp": 0.013093541069976861, + "gpu_bwutil_python": 0.01274178881219972, + "gpu_gap_stddev_us": 1.3606977663499773 + } + }, + "gaussiannoise_rgb_u8_perchannel_1080p_planar_nchw_advanced[InOutDataType=uchar3][shape=1x1080x1920][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 521.5150186159001, + "gpu_time_us_python": 535.11901471018, + "gpu_noise_us_cpp": 2.74, + "gpu_noise_us_python": 3.684, + "gpu_bwutil_cpp": 0.01534051733340036, + "gpu_bwutil_python": 0.01495006670132352, + "gpu_gap_stddev_us": 2.4431656908013935 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 500.97055083954, + "gpu_time_us_python": 513.89288123906, + "gpu_noise_us_cpp": 3.1420000000000003, + "gpu_noise_us_python": 4.142, + "gpu_bwutil_cpp": 0.01218038997023318, + "gpu_bwutil_python": 0.01187376804399292, + "gpu_gap_stddev_us": 2.582453702259549 + } + } + } + }, + "gaussiannoise_rgb_u8_perchannel_1080p_fakeplanar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "1x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "GaussianNoise RGB8 perchannel (advanced) (fake-planar NCHW_FAKE tensor comparison)." + }, + "baselines": { + "gaussiannoise_rgb_u8_perchannel_1080p_fakeplanar_nchw_advanced[InOutDataType=uchar3][shape=1x1080x1920][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 371.50263382139997, + "gpu_time_us_python": 387.20060790894, + "gpu_noise_us_cpp": 2.198, + "gpu_noise_us_python": 2.7139999999999995, + "gpu_bwutil_cpp": 0.06460354164956143, + "gpu_bwutil_python": 0.061984770817992405, + "gpu_gap_stddev_us": 0.6684408981782157 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 371.09066910588, + "gpu_time_us_python": 387.19716636216, + "gpu_noise_us_cpp": 1.956, + "gpu_noise_us_python": 5.63, + "gpu_bwutil_cpp": 0.04932996308005622, + "gpu_bwutil_python": 0.0472774729522569, + "gpu_gap_stddev_us": 2.0639474925792487 + } + } + } + }, + "gaussiannoise_rgb_f32_perchannel_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "1x1080x1920" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "GaussianNoise RGBf32 perchannel (advanced)." + }, + "baselines": { + "gaussiannoise_rgb_f32_perchannel_1080p_advanced[InOutDataType=float3][shape=1x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 366.23175388161997, + "gpu_time_us_python": 371.94230935563996, + "gpu_noise_us_cpp": 2.5460000000000003, + "gpu_noise_us_python": 2.506, + "gpu_bwutil_cpp": 0.08737642669700893, + "gpu_bwutil_python": 0.08603577868801389, + "gpu_gap_stddev_us": 1.1082822233420475 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 370.80855697794004, + "gpu_time_us_python": 386.99746787250007, + "gpu_noise_us_cpp": 3.4459999999999993, + "gpu_noise_us_python": 6.69, + "gpu_bwutil_cpp": 0.0658227247906522, + "gpu_bwutil_python": 0.06306802941575247, + "gpu_gap_stddev_us": 0.983321955276172 + } + }, + "gaussiannoise_rgb_f32_perchannel_1080p_advanced[InOutDataType=float3][shape=1x1080x1920][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 383.02498526323996, + "gpu_time_us_python": 393.8877241993, + "gpu_noise_us_cpp": 2.6019999999999994, + "gpu_noise_us_python": 3.178, + "gpu_bwutil_cpp": 0.0835479721378953, + "gpu_bwutil_python": 0.08124365304844397, + "gpu_gap_stddev_us": 0.6947007787310496 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 392.16352990214, + "gpu_time_us_python": 401.63660473346005, + "gpu_noise_us_cpp": 5.088, + "gpu_noise_us_python": 4.99, + "gpu_bwutil_cpp": 0.0622385096344244, + "gpu_bwutil_python": 0.060770611248949356, + "gpu_gap_stddev_us": 3.25016410695041 + } + } + } + }, + "gaussiannoise_rgb_f32_perchannel_1080p_planar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "1x1080x1920" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "GaussianNoise RGBf32 perchannel (advanced) (native planar NCHW)." + }, + "baselines": { + "gaussiannoise_rgb_f32_perchannel_1080p_planar_nchw_advanced[InOutDataType=float3][shape=1x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 485.77287510755997, + "gpu_time_us_python": 495.0257949778599, + "gpu_noise_us_cpp": 3.168, + "gpu_noise_us_python": 3.3019999999999996, + "gpu_bwutil_cpp": 0.06587442007011614, + "gpu_bwutil_python": 0.06464323588288849, + "gpu_gap_stddev_us": 0.9841569594537578 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 493.821025387, + "gpu_time_us_python": 510.74659666080004, + "gpu_noise_us_cpp": 3.2439999999999998, + "gpu_noise_us_python": 7.383999999999999, + "gpu_bwutil_cpp": 0.04942518692936284, + "gpu_bwutil_python": 0.047787290703668696, + "gpu_gap_stddev_us": 1.7521015988935569 + } + }, + "gaussiannoise_rgb_f32_perchannel_1080p_planar_nchw_advanced[InOutDataType=float3][shape=1x1080x1920][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 552.52386952252, + "gpu_time_us_python": 566.7887118055799, + "gpu_noise_us_cpp": 3.3419999999999996, + "gpu_noise_us_python": 4.192, + "gpu_bwutil_cpp": 0.05791657538566501, + "gpu_bwutil_python": 0.056458898676461165, + "gpu_gap_stddev_us": 0.18291311624832496 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 525.0753754982, + "gpu_time_us_python": 536.1247319346601, + "gpu_noise_us_cpp": 4.752, + "gpu_noise_us_python": 5.314, + "gpu_bwutil_cpp": 0.046483839742515264, + "gpu_bwutil_python": 0.04552645515169258, + "gpu_gap_stddev_us": 4.166975510352334 + } + } + } + }, + "gaussiannoise_rgb_f32_perchannel_1080p_fakeplanar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "1x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "GaussianNoise RGBf32 perchannel (advanced) (fake-planar NCHW_FAKE tensor comparison)." + }, + "baselines": { + "gaussiannoise_rgb_f32_perchannel_1080p_fakeplanar_nchw_advanced[InOutDataType=float3][shape=1x1080x1920][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 442.26808358792005, + "gpu_time_us_python": 464.37269204058003, + "gpu_noise_us_cpp": 3.242, + "gpu_noise_us_python": 3.146, + "gpu_bwutil_cpp": 0.21706428192311353, + "gpu_bwutil_python": 0.20673295847565315, + "gpu_gap_stddev_us": 1.135791587356764 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 433.13316455796, + "gpu_time_us_python": 447.59867112572, + "gpu_noise_us_cpp": 4.058, + "gpu_noise_us_python": 4.395999999999999, + "gpu_bwutil_cpp": 0.16905654295524647, + "gpu_bwutil_python": 0.16359296342695806, + "gpu_gap_stddev_us": 0.7085923270587762 + } + } + } + } + } +} diff --git a/bench/config/operators/histogram.json b/bench/config/operators/histogram.json new file mode 100644 index 000000000..c4e212b60 --- /dev/null +++ b/bench/config/operators/histogram.json @@ -0,0 +1,314 @@ +{ + "benchmark": "histogram", + "configs": { + "histogram_basic": { + "tier": "basic", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "maskMode": [ + "none" + ], + "inputKind": [ + "Tensor" + ], + "layout": [ + "NHWC", + "NCHW" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Histogram single-channel (basic)." + }, + "baselines": { + "histogram_basic[InOutDataType=uint8][shape=128x1080x1920][maskMode=none][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 410.28869908685994, + "gpu_time_us_python": 420.20409562168, + "gpu_noise_us_cpp": 1.068, + "gpu_noise_us_python": 1.8960000000000001, + "gpu_bwutil_cpp": 0.4161734594974254, + "gpu_bwutil_python": 0.4063682584021482, + "gpu_gap_stddev_us": 2.3672538164232586 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 382.74390805402, + "gpu_time_us_python": 392.09551730222, + "gpu_noise_us_cpp": 1.588, + "gpu_noise_us_python": 2.6079999999999997, + "gpu_bwutil_cpp": 0.340323196947961, + "gpu_bwutil_python": 0.33220021373387876, + "gpu_gap_stddev_us": 0.4825559082173805 + } + }, + "histogram_basic[InOutDataType=uint8][shape=128x1080x1920][maskMode=none][inputKind=Tensor][layout=NCHW]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 796.14116007302, + "gpu_time_us_python": 808.36539143062, + "gpu_noise_us_cpp": 2.346, + "gpu_noise_us_python": 3.7319999999999993, + "gpu_bwutil_cpp": 0.6432087903716622, + "gpu_bwutil_python": 0.633486532156162, + "gpu_gap_stddev_us": 2.0653806876009835 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 675.68042999086, + "gpu_time_us_python": 686.2013959895, + "gpu_noise_us_cpp": 1.6880000000000002, + "gpu_noise_us_python": 3.662, + "gpu_bwutil_cpp": 0.5780821963728663, + "gpu_bwutil_python": 0.5692183925969652, + "gpu_gap_stddev_us": 0.9302412168208769 + } + } + } + }, + "histogram_nomask_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "256x1080x1920" + ], + "maskMode": [ + "none" + ], + "inputKind": [ + "Tensor" + ], + "layout": [ + "NHWC", + "NCHW" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Histogram single-channel nomask (advanced)." + }, + "baselines": { + "histogram_nomask_1080p_advanced[InOutDataType=uint8][shape=256x1080x1920][maskMode=none][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 804.2006243443599, + "gpu_time_us_python": 813.70084093834, + "gpu_noise_us_cpp": 1.2419999999999998, + "gpu_noise_us_python": 1.796, + "gpu_bwutil_cpp": 0.4246478902482017, + "gpu_bwutil_python": 0.41969054049586657, + "gpu_gap_stddev_us": 0.9769743436274981 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 749.9753997560799, + "gpu_time_us_python": 760.4880172981, + "gpu_noise_us_cpp": 1.58, + "gpu_noise_us_python": 2.8760000000000003, + "gpu_bwutil_cpp": 0.3473649975699472, + "gpu_bwutil_python": 0.34257359705351415, + "gpu_gap_stddev_us": 1.8102221445209599 + } + }, + "histogram_nomask_1080p_advanced[InOutDataType=uint8][shape=256x1080x1920][maskMode=none][inputKind=Tensor][layout=NCHW]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1571.14640952936, + "gpu_time_us_python": 1582.86070962982, + "gpu_noise_us_cpp": 1.9700000000000002, + "gpu_noise_us_python": 4.504, + "gpu_bwutil_cpp": 0.6518611404840405, + "gpu_bwutil_python": 0.6470369056499022, + "gpu_gap_stddev_us": 0.33513018269595235 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1328.52285372108, + "gpu_time_us_python": 1343.15112991054, + "gpu_noise_us_cpp": 2.34, + "gpu_noise_us_python": 4.792, + "gpu_bwutil_cpp": 0.5880121015132643, + "gpu_bwutil_python": 0.5816178881534875, + "gpu_gap_stddev_us": 2.7515452236615148 + } + } + } + }, + "histogram_mask_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "maskMode": [ + "checkerboard" + ], + "inputKind": [ + "Tensor" + ], + "layout": [ + "NHWC", + "NCHW" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Histogram single-channel mask (advanced)." + }, + "baselines": { + "histogram_mask_1080p_advanced[InOutDataType=uint8][shape=128x1080x1920][maskMode=checkerboard][inputKind=Tensor][layout=NHWC]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 557.44994850652, + "gpu_time_us_python": 567.00936866678, + "gpu_noise_us_cpp": 1.572, + "gpu_noise_us_python": 2.5, + "gpu_bwutil_cpp": 0.6124689132689849, + "gpu_bwutil_python": 0.6021444114515987, + "gpu_gap_stddev_us": 0.7230429910041786 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 521.61305816802, + "gpu_time_us_python": 531.93822752596, + "gpu_noise_us_cpp": 2.1119999999999997, + "gpu_noise_us_python": 3.908, + "gpu_bwutil_cpp": 0.4992756525329294, + "gpu_bwutil_python": 0.4895578387652719, + "gpu_gap_stddev_us": 2.893187913121122 + } + }, + "histogram_mask_1080p_advanced[InOutDataType=uint8][shape=128x1080x1920][maskMode=checkerboard][inputKind=Tensor][layout=NCHW]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1327.0367894853398, + "gpu_time_us_python": 1339.21962136192, + "gpu_noise_us_cpp": 2.19, + "gpu_noise_us_python": 4.1259999999999994, + "gpu_bwutil_cpp": 0.7717078260532098, + "gpu_bwutil_python": 0.7646894077841433, + "gpu_gap_stddev_us": 1.9584710782431456 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1093.8570457073597, + "gpu_time_us_python": 1113.08347413456, + "gpu_noise_us_cpp": 2.182, + "gpu_noise_us_python": 3.636, + "gpu_bwutil_cpp": 0.7140786745507474, + "gpu_bwutil_python": 0.7017422170515573, + "gpu_gap_stddev_us": 3.6027757505187683 + } + } + } + }, + "histogram_native_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "maskMode": [ + "none" + ], + "inputKind": [ + "Tensor" + ], + "layout": [ + "NCHW" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Histogram single-channel native planar comparison for NCHW_FAKE (advanced)." + }, + "baselines": { + "histogram_native_nchw_advanced[InOutDataType=uint8][shape=128x1080x1920][maskMode=none][inputKind=Tensor][layout=NCHW]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 796.51854710102, + "gpu_time_us_python": 808.12972691046, + "gpu_noise_us_cpp": 1.516, + "gpu_noise_us_python": 2.6580000000000004, + "gpu_bwutil_cpp": 0.6429043063097848, + "gpu_bwutil_python": 0.6336699145861582, + "gpu_gap_stddev_us": 1.7599050361587443 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 675.3248674268201, + "gpu_time_us_python": 686.43340029764, + "gpu_noise_us_cpp": 2.388, + "gpu_noise_us_python": 3.7119999999999997, + "gpu_bwutil_cpp": 0.5783812753499399, + "gpu_bwutil_python": 0.5690185982552715, + "gpu_gap_stddev_us": 0.7988711796759239 + } + } + } + }, + "histogram_fakeplanar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "maskMode": [ + "none" + ], + "inputKind": [ + "Tensor" + ], + "layout": [ + "NCHW_FAKE" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Histogram single-channel (advanced) (fake planar NCHW_FAKE tensor-only)." + }, + "baselines": { + "histogram_fakeplanar_nchw_advanced[InOutDataType=uint8][shape=128x1080x1920][maskMode=none][inputKind=Tensor][layout=NCHW_FAKE]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 794.7644260194201, + "gpu_time_us_python": 809.4163368474, + "gpu_noise_us_cpp": 1.538, + "gpu_noise_us_python": 3.428, + "gpu_bwutil_cpp": 0.6443224630827313, + "gpu_bwutil_python": 0.6326604154841685, + "gpu_gap_stddev_us": 1.6386373071460254 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 673.7205580659199, + "gpu_time_us_python": 686.65136749, + "gpu_noise_us_cpp": 1.21, + "gpu_noise_us_python": 4.164, + "gpu_bwutil_cpp": 0.5797578795134936, + "gpu_bwutil_python": 0.5688530629200705, + "gpu_gap_stddev_us": 1.4954350534387189 + } + } + } + } + } +} diff --git a/bench/config/operators/histogrameq.json b/bench/config/operators/histogrameq.json new file mode 100644 index 000000000..241d3ede6 --- /dev/null +++ b/bench/config/operators/histogrameq.json @@ -0,0 +1,770 @@ +{ + "benchmark": "histogrameq", + "configs": { + "histogrameq_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HistogramEq RGB8 tensor (basic)." + }, + "baselines": { + "histogrameq_uchar3_basic[InOutDataType=uchar3][shape=32x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2397.84433511196, + "gpu_time_us_python": 2407.30237490958, + "gpu_noise_us_cpp": 1.766, + "gpu_noise_us_python": 4.586, + "gpu_bwutil_cpp": 0.10676480119847001, + "gpu_bwutil_python": 0.10634507817989283, + "gpu_gap_stddev_us": 1.270626111833231 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2202.779441549025, + "gpu_time_us_python": 2215.6106746588753, + "gpu_noise_us_cpp": 2.215, + "gpu_noise_us_python": 3.5774999999999997, + "gpu_bwutil_cpp": 0.08864055658293055, + "gpu_bwutil_python": 0.08812734763084695, + "gpu_gap_stddev_us": 2.7973332003234392 + } + } + } + }, + "histogrameq_uchar3_varshape_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HistogramEq RGB8 var-shape (basic)." + }, + "baselines": { + "histogrameq_uchar3_varshape_basic[InOutDataType=uchar3][shape=32x1080x1920][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2370.06707835542, + "gpu_time_us_python": 2380.8371713883, + "gpu_noise_us_cpp": 1.908, + "gpu_noise_us_python": 2.7579999999999996, + "gpu_bwutil_cpp": 0.10801568906262012, + "gpu_bwutil_python": 0.10752682342195899, + "gpu_gap_stddev_us": 1.1069477547336894 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2275.7162014837995, + "gpu_time_us_python": 2288.675496150375, + "gpu_noise_us_cpp": 2.94, + "gpu_noise_us_python": 5.0725, + "gpu_bwutil_cpp": 0.08580056676121725, + "gpu_bwutil_python": 0.08531507979083874, + "gpu_gap_stddev_us": 1.722953840872356 + } + } + } + }, + "histogrameq_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HistogramEq RGBA8 tensor (advanced)." + }, + "baselines": { + "histogrameq_uchar4_advanced[InOutDataType=uchar4][shape=32x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1695.2232353747, + "gpu_time_us_python": 1705.19616928752, + "gpu_noise_us_cpp": 2.184, + "gpu_noise_us_python": 2.8719999999999994, + "gpu_bwutil_cpp": 0.20135334047095013, + "gpu_bwutil_python": 0.20017512762771217, + "gpu_gap_stddev_us": 2.557165818239807 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1520.1910184608, + "gpu_time_us_python": 1529.7508802995499, + "gpu_noise_us_cpp": 2.5425, + "gpu_noise_us_python": 2.2975, + "gpu_bwutil_cpp": 0.1712636429675555, + "gpu_bwutil_python": 0.17019419615030243, + "gpu_gap_stddev_us": 3.080673946364281 + } + } + } + }, + "histogrameq_uchar4_varshape_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HistogramEq RGBA8 var-shape (advanced)." + }, + "baselines": { + "histogrameq_uchar4_varshape_advanced[InOutDataType=uchar4][shape=32x1080x1920][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2306.5189368563997, + "gpu_time_us_python": 2318.2228032399003, + "gpu_noise_us_cpp": 3.3840000000000003, + "gpu_noise_us_python": 2.626, + "gpu_bwutil_cpp": 0.14798827472916046, + "gpu_bwutil_python": 0.1472407228080454, + "gpu_gap_stddev_us": 1.1946339862853783 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1971.313445677875, + "gpu_time_us_python": 1983.8749886904, + "gpu_noise_us_cpp": 2.6925, + "gpu_noise_us_python": 4.43, + "gpu_bwutil_cpp": 0.13207762623098224, + "gpu_bwutil_python": 0.13123866569842427, + "gpu_gap_stddev_us": 2.8321743230941183 + } + } + } + }, + "histogrameq_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HistogramEq single-channel tensor (advanced)." + }, + "baselines": { + "histogrameq_scalar_advanced[InOutDataType=uint8][shape=16x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 409.73372517292, + "gpu_time_us_python": 418.89726199447995, + "gpu_noise_us_cpp": 2.6380000000000003, + "gpu_noise_us_python": 2.468, + "gpu_bwutil_cpp": 0.10413381329172382, + "gpu_bwutil_python": 0.1018553009205557, + "gpu_gap_stddev_us": 1.3765491067083335 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 406.651529305125, + "gpu_time_us_python": 417.167724012175, + "gpu_noise_us_cpp": 1.685, + "gpu_noise_us_python": 3.84, + "gpu_bwutil_cpp": 0.0800263065567757, + "gpu_bwutil_python": 0.07800876090207978, + "gpu_gap_stddev_us": 2.6242077568594584 + } + } + } + }, + "histogrameq_scalar_varshape_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HistogramEq single-channel var-shape (advanced)." + }, + "baselines": { + "histogrameq_scalar_varshape_advanced[InOutDataType=uint8][shape=16x1080x1920][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1621.21402003184, + "gpu_time_us_python": 1635.62147291906, + "gpu_noise_us_cpp": 3.2060000000000004, + "gpu_noise_us_python": 4.014000000000001, + "gpu_bwutil_cpp": 0.026318385373232923, + "gpu_bwutil_python": 0.026086384658223938, + "gpu_gap_stddev_us": 1.6930390955060293 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1529.4802164577, + "gpu_time_us_python": 1541.1581814837, + "gpu_noise_us_cpp": 2.0175, + "gpu_noise_us_python": 3.875, + "gpu_bwutil_cpp": 0.0212768827845815, + "gpu_bwutil_python": 0.021115645630300123, + "gpu_gap_stddev_us": 0.6540175866780233 + } + } + } + }, + "histogrameq_rgb_u8_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HistogramEq RGB8 tensor (advanced)." + }, + "baselines": { + "histogrameq_rgb_u8_1080p_advanced[InOutDataType=uchar3][shape=32x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2397.50797130392, + "gpu_time_us_python": 2407.56799893496, + "gpu_noise_us_cpp": 1.3199999999999998, + "gpu_noise_us_python": 2.44, + "gpu_bwutil_cpp": 0.1067797163548216, + "gpu_bwutil_python": 0.10633323256342926, + "gpu_gap_stddev_us": 0.8735071090436334 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2204.0322329978744, + "gpu_time_us_python": 2214.980374723775, + "gpu_noise_us_cpp": 3.1625, + "gpu_noise_us_python": 2.9175, + "gpu_bwutil_cpp": 0.08859047597123228, + "gpu_bwutil_python": 0.08815223557260857, + "gpu_gap_stddev_us": 2.4827761440233846 + } + } + } + }, + "histogrameq_rgb_u8_1080p_varshape_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HistogramEq RGB8 var-shape (advanced)." + }, + "baselines": { + "histogrameq_rgb_u8_1080p_varshape_advanced[InOutDataType=uchar3][shape=32x1080x1920][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2370.06292571422, + "gpu_time_us_python": 2381.2724935012, + "gpu_noise_us_cpp": 1.502, + "gpu_noise_us_python": 2.44, + "gpu_bwutil_cpp": 0.10801586481024517, + "gpu_bwutil_python": 0.10750717902322324, + "gpu_gap_stddev_us": 0.8987922982100147 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2274.44791064015, + "gpu_time_us_python": 2285.416604279625, + "gpu_noise_us_cpp": 1.9899999999999998, + "gpu_noise_us_python": 6.58, + "gpu_bwutil_cpp": 0.08584820551412975, + "gpu_bwutil_python": 0.08543626671749195, + "gpu_gap_stddev_us": 3.681618733707525 + } + } + } + }, + "histogrameq_planar_nchw_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HistogramEq RGB8 native planar tensor (basic)." + }, + "baselines": { + "histogrameq_planar_nchw_uchar3_basic[InOutDataType=uchar3][shape=32x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2506.26931534658, + "gpu_time_us_python": 2515.0524660903, + "gpu_noise_us_cpp": 4.311999999999999, + "gpu_noise_us_python": 2.472, + "gpu_bwutil_cpp": 0.10214595836464016, + "gpu_bwutil_python": 0.10178934778516813, + "gpu_gap_stddev_us": 1.2961009831743913 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2279.1462578165747, + "gpu_time_us_python": 2288.548219467375, + "gpu_noise_us_cpp": 2.57, + "gpu_noise_us_python": 3.9050000000000002, + "gpu_bwutil_cpp": 0.08567148700572327, + "gpu_bwutil_python": 0.08531872858001194, + "gpu_gap_stddev_us": 3.3475604116872564 + } + } + } + }, + "histogrameq_planar_nchw_uchar3_varshape_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HistogramEq RGB8 native planar var-shape (basic)." + }, + "baselines": { + "histogrameq_planar_nchw_uchar3_varshape_basic[InOutDataType=uchar3][shape=32x1080x1920][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2598.89385298758, + "gpu_time_us_python": 2610.43052505888, + "gpu_noise_us_cpp": 2.7640000000000002, + "gpu_noise_us_python": 2.92, + "gpu_bwutil_cpp": 0.09850502227391514, + "gpu_bwutil_python": 0.09806989587406376, + "gpu_gap_stddev_us": 0.8995991429544458 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2396.386526171925, + "gpu_time_us_python": 2410.38036290755, + "gpu_noise_us_cpp": 2.685, + "gpu_noise_us_python": 5.17, + "gpu_bwutil_cpp": 0.0814806091772388, + "gpu_bwutil_python": 0.08100744210616836, + "gpu_gap_stddev_us": 2.943596601830005 + } + } + } + }, + "histogrameq_planar_nchw_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HistogramEq RGBA8 native planar tensor (advanced)." + }, + "baselines": { + "histogrameq_planar_nchw_uchar4_advanced[InOutDataType=uchar4][shape=32x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2387.39628825222, + "gpu_time_us_python": 2398.1416361672004, + "gpu_noise_us_cpp": 1.786, + "gpu_noise_us_python": 2.852, + "gpu_bwutil_cpp": 0.14297541507259642, + "gpu_bwutil_python": 0.14233419281877963, + "gpu_gap_stddev_us": 1.8321415208626093 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2145.239930886425, + "gpu_time_us_python": 2155.479679689425, + "gpu_noise_us_cpp": 2.8025, + "gpu_noise_us_python": 5.1425, + "gpu_bwutil_cpp": 0.12135690615761466, + "gpu_bwutil_python": 0.12078012941547123, + "gpu_gap_stddev_us": 3.904787030114084 + } + } + } + }, + "histogrameq_fakeplanar_nchw_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HistogramEq RGBA8 fake-planar tensor comparison (advanced)." + }, + "baselines": { + "histogrameq_fakeplanar_nchw_uchar4_advanced[InOutDataType=uchar4][shape=32x1080x1920][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3369.6344318389406, + "gpu_time_us_python": 3388.6690432316, + "gpu_noise_us_cpp": 2.646, + "gpu_noise_us_python": 4.848, + "gpu_bwutil_cpp": 0.30389474974024594, + "gpu_bwutil_python": 0.3021877786916933, + "gpu_gap_stddev_us": 1.4037626774638938 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2917.3626881953496, + "gpu_time_us_python": 2931.7963029633, + "gpu_noise_us_cpp": 2.4725, + "gpu_noise_us_python": 3.85, + "gpu_bwutil_cpp": 0.267718489827214, + "gpu_bwutil_python": 0.2663998233649623, + "gpu_gap_stddev_us": 2.88506866547966 + } + } + } + }, + "histogrameq_planar_nchw_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HistogramEq single-channel native planar tensor (advanced)." + }, + "baselines": { + "histogrameq_planar_nchw_scalar_advanced[InOutDataType=uint8][shape=16x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 401.512116173, + "gpu_time_us_python": 410.98687489276, + "gpu_noise_us_cpp": 2.008, + "gpu_noise_us_python": 3.504, + "gpu_bwutil_cpp": 0.10626598561840264, + "gpu_bwutil_python": 0.10381549831589114, + "gpu_gap_stddev_us": 1.1771140486152418 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 401.46278509272497, + "gpu_time_us_python": 411.9801849519, + "gpu_noise_us_cpp": 2.5349999999999997, + "gpu_noise_us_python": 2.885, + "gpu_bwutil_cpp": 0.08106114012590171, + "gpu_bwutil_python": 0.07899093765662502, + "gpu_gap_stddev_us": 2.039128014524946 + } + } + } + }, + "histogrameq_fakeplanar_nchw_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HistogramEq single-channel fake-planar tensor comparison (advanced)." + }, + "baselines": { + "histogrameq_fakeplanar_nchw_scalar_advanced[InOutDataType=uint8][shape=16x1080x1920][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 508.6588452553, + "gpu_time_us_python": 523.90373076074, + "gpu_noise_us_cpp": 2.538, + "gpu_noise_us_python": 2.466, + "gpu_bwutil_cpp": 0.25164357792156244, + "gpu_bwutil_python": 0.24432205373546773, + "gpu_gap_stddev_us": 1.2570557670535427 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 481.53711300075, + "gpu_time_us_python": 494.3402210861001, + "gpu_noise_us_cpp": 2.5575, + "gpu_noise_us_python": 3.925, + "gpu_bwutil_cpp": 0.2027431991639782, + "gpu_bwutil_python": 0.19749122286429752, + "gpu_gap_stddev_us": 0.9477283890136896 + } + } + } + }, + "histogrameq_planar_rgb_u8_nchw_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HistogramEq RGB8 native planar tensor (advanced)." + }, + "baselines": { + "histogrameq_planar_rgb_u8_nchw_1080p_advanced[InOutDataType=uchar3][shape=32x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2506.1782486792204, + "gpu_time_us_python": 2514.47114813678, + "gpu_noise_us_cpp": 1.986, + "gpu_noise_us_python": 2.728, + "gpu_bwutil_cpp": 0.10214956567876303, + "gpu_bwutil_python": 0.10181273661670487, + "gpu_gap_stddev_us": 0.9033562469150597 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2279.23738893475, + "gpu_time_us_python": 2289.2267897935753, + "gpu_noise_us_cpp": 2.265, + "gpu_noise_us_python": 6.375, + "gpu_bwutil_cpp": 0.0856682727364658, + "gpu_bwutil_python": 0.08529307560526099, + "gpu_gap_stddev_us": 6.470645984783321 + } + } + } + }, + "histogrameq_planar_rgb_u8_nchw_1080p_varshape_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HistogramEq RGB8 native planar var-shape (advanced)." + }, + "baselines": { + "histogrameq_planar_rgb_u8_nchw_1080p_varshape_advanced[InOutDataType=uchar3][shape=32x1080x1920][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2599.4970200704597, + "gpu_time_us_python": 2611.0184117844, + "gpu_noise_us_cpp": 4.412000000000001, + "gpu_noise_us_python": 3.1079999999999997, + "gpu_bwutil_cpp": 0.09848228344794076, + "gpu_bwutil_python": 0.098047347335566, + "gpu_gap_stddev_us": 1.6144559602256696 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2396.6197514616497, + "gpu_time_us_python": 2410.85795346955, + "gpu_noise_us_cpp": 4.1125, + "gpu_noise_us_python": 6.7175, + "gpu_bwutil_cpp": 0.08147254951078439, + "gpu_bwutil_python": 0.08099128972966577, + "gpu_gap_stddev_us": 2.8365802027095772 + } + } + } + }, + "histogrameq_fakeplanar_rgb_u8_nchw_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HistogramEq RGB8 fake-planar tensor comparison (advanced)." + }, + "baselines": { + "histogrameq_fakeplanar_rgb_u8_nchw_1080p_advanced[InOutDataType=uchar3][shape=32x1080x1920][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3252.8286330905003, + "gpu_time_us_python": 3271.56822898168, + "gpu_noise_us_cpp": 2.6100000000000003, + "gpu_noise_us_python": 4.236, + "gpu_bwutil_cpp": 0.23610515527645726, + "gpu_bwutil_python": 0.23475291718067237, + "gpu_gap_stddev_us": 1.2967571380167184 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2920.1490289942753, + "gpu_time_us_python": 2933.779020348525, + "gpu_noise_us_cpp": 3.0250000000000004, + "gpu_noise_us_python": 7.6450000000000005, + "gpu_bwutil_cpp": 0.2005939867542873, + "gpu_bwutil_python": 0.19966380463797612, + "gpu_gap_stddev_us": 7.864478620938929 + } + } + } + } + } +} diff --git a/bench/config/operators/hqresize.json b/bench/config/operators/hqresize.json new file mode 100644 index 000000000..f27beb722 --- /dev/null +++ b/bench/config/operators/hqresize.json @@ -0,0 +1,5458 @@ +{ + "benchmark": "hqresize", + "configs": { + "hqresize_planar_contract_linear_nhwc_1080p_basic": { + "tier": "basic", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "interpolation": [ + "LINEAR" + ], + "resizeType": [ + "CONTRACT" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize planar floor: RGB 3-channel CONTRACT LINEAR 1080p tensor (NHWC)." + }, + "baselines": { + "hqresize_planar_contract_linear_nhwc_1080p_basic[InOutDataType=float32][shape=64x1080x1920][interpolation=LINEAR][resizeType=CONTRACT][layout=NHWC][inputKind=Tensor][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1425.08102045814, + "gpu_time_us_python": 1443.02569546258, + "gpu_noise_us_cpp": 1.932, + "gpu_noise_us_python": 4.87, + "gpu_bwutil_cpp": 0.898194740311092, + "gpu_bwutil_python": 0.8870313179128818, + "gpu_gap_stddev_us": 3.6658208063249393 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1094.9860887235602, + "gpu_time_us_python": 1110.8059572985599, + "gpu_noise_us_cpp": 2.71, + "gpu_noise_us_python": 3.06, + "gpu_bwutil_cpp": 0.8915834769217181, + "gpu_bwutil_python": 0.8788875995698019, + "gpu_gap_stddev_us": 1.7218349242649102 + } + }, + "hqresize_planar_contract_linear_nhwc_1080p_basic[InOutDataType=uint8][shape=64x1080x1920][interpolation=LINEAR][resizeType=CONTRACT][layout=NHWC][inputKind=Tensor][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 504.8362155863, + "gpu_time_us_python": 521.4428667165199, + "gpu_noise_us_cpp": 1.104, + "gpu_noise_us_python": 2.246, + "gpu_bwutil_cpp": 0.6338716910829186, + "gpu_bwutil_python": 0.6136959800149484, + "gpu_gap_stddev_us": 2.080209167440436 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 389.50201008680006, + "gpu_time_us_python": 405.52240905298004, + "gpu_noise_us_cpp": 0.9199999999999999, + "gpu_noise_us_python": 2.7660000000000005, + "gpu_bwutil_cpp": 0.626685876949653, + "gpu_bwutil_python": 0.6019275993861581, + "gpu_gap_stddev_us": 0.7992860552097307 + } + } + } + }, + "hqresize_planar_contract_linear_nchw_1080p_basic": { + "tier": "basic", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "interpolation": [ + "LINEAR" + ], + "resizeType": [ + "CONTRACT" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize planar floor: RGB 3-channel CONTRACT LINEAR 1080p tensor (native planar NCHW)." + }, + "baselines": { + "hqresize_planar_contract_linear_nchw_1080p_basic[InOutDataType=float32][shape=64x1080x1920][interpolation=LINEAR][resizeType=CONTRACT][layout=NCHW][inputKind=Tensor][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1475.00094301556, + "gpu_time_us_python": 1492.58485428192, + "gpu_noise_us_cpp": 2.254, + "gpu_noise_us_python": 4.2, + "gpu_bwutil_cpp": 0.8677963618569585, + "gpu_bwutil_python": 0.8575759231733937, + "gpu_gap_stddev_us": 2.404667463099983 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1133.56530558464, + "gpu_time_us_python": 1149.5956531357801, + "gpu_noise_us_cpp": 1.5699999999999998, + "gpu_noise_us_python": 4.1979999999999995, + "gpu_bwutil_cpp": 0.8612399478087063, + "gpu_bwutil_python": 0.8492312540399352, + "gpu_gap_stddev_us": 1.7658752087997953 + } + }, + "hqresize_planar_contract_linear_nchw_1080p_basic[InOutDataType=uint8][shape=64x1080x1920][interpolation=LINEAR][resizeType=CONTRACT][layout=NCHW][inputKind=Tensor][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 615.7923504222, + "gpu_time_us_python": 632.4285532046999, + "gpu_noise_us_cpp": 1.814, + "gpu_noise_us_python": 3.492, + "gpu_bwutil_cpp": 0.5196579792751546, + "gpu_bwutil_python": 0.5059971021670158, + "gpu_gap_stddev_us": 2.1864819072982864 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 554.699058741, + "gpu_time_us_python": 571.67942382808, + "gpu_noise_us_cpp": 0.8320000000000001, + "gpu_noise_us_python": 3.0220000000000002, + "gpu_bwutil_cpp": 0.44006534230065847, + "gpu_bwutil_python": 0.4269798906557048, + "gpu_gap_stddev_us": 1.96393056402762 + } + } + } + }, + "hqresize_planar_contract_linear_nchwfake_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "interpolation": [ + "LINEAR" + ], + "resizeType": [ + "CONTRACT" + ], + "layout": [ + "NCHW", + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Advanced HQResize native NCHW and fake-planar NCHW_FAKE comparison for RGB 3-channel CONTRACT LINEAR 1080p tensors." + }, + "baselines": { + "hqresize_planar_contract_linear_nchwfake_1080p_advanced[InOutDataType=float32][shape=64x1080x1920][interpolation=LINEAR][resizeType=CONTRACT][layout=NCHW_FAKE][inputKind=Tensor][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5043.277765274021, + "gpu_time_us_python": 5054.05855941768, + "gpu_noise_us_cpp": 1.67, + "gpu_noise_us_python": 3.7359999999999998, + "gpu_bwutil_cpp": 0.7614155206054594, + "gpu_bwutil_python": 0.7597945739273972, + "gpu_gap_stddev_us": 16.84363197283654 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4147.30334047408, + "gpu_time_us_python": 4161.53601815786, + "gpu_noise_us_cpp": 2.7600000000000002, + "gpu_noise_us_python": 3.438, + "gpu_bwutil_cpp": 0.7061994330132703, + "gpu_bwutil_python": 0.7037837739931037, + "gpu_gap_stddev_us": 0.9262572523072297 + } + }, + "hqresize_planar_contract_linear_nchwfake_1080p_advanced[InOutDataType=uint8][shape=64x1080x1920][interpolation=LINEAR][resizeType=CONTRACT][layout=NCHW_FAKE][inputKind=Tensor][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2297.03906386166, + "gpu_time_us_python": 2311.5994032126196, + "gpu_noise_us_cpp": 2.7640000000000002, + "gpu_noise_us_python": 3.29, + "gpu_bwutil_cpp": 0.41793207584827197, + "gpu_bwutil_python": 0.41530072877001095, + "gpu_gap_stddev_us": 5.865227858092986 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1770.4504221823402, + "gpu_time_us_python": 1785.0586566579, + "gpu_noise_us_cpp": 1.4099999999999997, + "gpu_noise_us_python": 4.0200000000000005, + "gpu_bwutil_cpp": 0.4135804862635283, + "gpu_bwutil_python": 0.4101933926126793, + "gpu_gap_stddev_us": 1.3832117505221015 + } + }, + "hqresize_planar_contract_linear_nchwfake_1080p_advanced[InOutDataType=float32][shape=64x1080x1920][interpolation=LINEAR][resizeType=CONTRACT][layout=NCHW][inputKind=Tensor][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1475.7201563610201, + "gpu_time_us_python": 1491.1695429256201, + "gpu_noise_us_cpp": 2.218, + "gpu_noise_us_python": 2.88, + "gpu_bwutil_cpp": 0.8673732769224669, + "gpu_bwutil_python": 0.8583870053984572, + "gpu_gap_stddev_us": 0.5726233876355302 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1133.30704842212, + "gpu_time_us_python": 1148.92450647612, + "gpu_noise_us_cpp": 1.2079999999999997, + "gpu_noise_us_python": 3.156, + "gpu_bwutil_cpp": 0.8614363507838535, + "gpu_bwutil_python": 0.8497269940285228, + "gpu_gap_stddev_us": 1.0635330530376728 + } + }, + "hqresize_planar_contract_linear_nchwfake_1080p_advanced[InOutDataType=uint8][shape=64x1080x1920][interpolation=LINEAR][resizeType=CONTRACT][layout=NCHW][inputKind=Tensor][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 615.16787741016, + "gpu_time_us_python": 632.46949148382, + "gpu_noise_us_cpp": 2.744, + "gpu_noise_us_python": 3.54, + "gpu_bwutil_cpp": 0.5201852394574671, + "gpu_bwutil_python": 0.5059599434966282, + "gpu_gap_stddev_us": 1.7512182042555442 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 554.75580982622, + "gpu_time_us_python": 571.18467290056, + "gpu_noise_us_cpp": 1.5580000000000003, + "gpu_noise_us_python": 3.5460000000000003, + "gpu_bwutil_cpp": 0.44001601755453557, + "gpu_bwutil_python": 0.427348620830721, + "gpu_gap_stddev_us": 1.3721071196905503 + } + } + } + }, + "hqresize_contract_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "interpolation": [ + "CUBIC" + ], + "resizeType": [ + "CONTRACT" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize single-channel contract (advanced)." + }, + "baselines": { + "hqresize_contract_advanced[InOutDataType=float32][shape=64x1080x1920][interpolation=CUBIC][resizeType=CONTRACT][layout=NHWC][inputKind=Tensor][antialias=0][numChannels=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 831.2649094146, + "gpu_time_us_python": 848.2254180772201, + "gpu_noise_us_cpp": 1.314, + "gpu_noise_us_python": 4.54, + "gpu_bwutil_cpp": 0.5132750662066593, + "gpu_bwutil_python": 0.5030141798774357, + "gpu_gap_stddev_us": 1.5455791592062094 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 779.7380866348, + "gpu_time_us_python": 797.71044867202, + "gpu_noise_us_cpp": 1.732, + "gpu_noise_us_python": 3.186, + "gpu_bwutil_cpp": 0.4174032007475056, + "gpu_bwutil_python": 0.40798192959552304, + "gpu_gap_stddev_us": 2.9683749325879494 + } + }, + "hqresize_contract_advanced[InOutDataType=uint8][shape=64x1080x1920][interpolation=CUBIC][resizeType=CONTRACT][layout=NHWC][inputKind=Tensor][antialias=0][numChannels=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 904.1606121878, + "gpu_time_us_python": 919.07628996046, + "gpu_noise_us_cpp": 0.9880000000000001, + "gpu_noise_us_python": 2.9699999999999998, + "gpu_bwutil_cpp": 0.11797351382785451, + "gpu_bwutil_python": 0.1160590711736417, + "gpu_gap_stddev_us": 0.7754718330832687 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 701.83335773982, + "gpu_time_us_python": 719.13137034738, + "gpu_noise_us_cpp": 0.966, + "gpu_noise_us_python": 4.026, + "gpu_bwutil_cpp": 0.1159319410503784, + "gpu_bwutil_python": 0.11314050528584266, + "gpu_gap_stddev_us": 1.322100417456425 + } + } + } + }, + "hqresize_expand_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "interpolation": [ + "CUBIC" + ], + "resizeType": [ + "EXPAND" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize single-channel expand (advanced)." + }, + "baselines": { + "hqresize_expand_advanced[InOutDataType=float32][shape=32x1080x1920][interpolation=CUBIC][resizeType=EXPAND][layout=NHWC][inputKind=Tensor][antialias=0][numChannels=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2970.8661177578397, + "gpu_time_us_python": 2989.2291171209604, + "gpu_noise_us_cpp": 2.004, + "gpu_noise_us_python": 3.466, + "gpu_bwutil_cpp": 0.2872343935492995, + "gpu_bwutil_python": 0.2854702121465671, + "gpu_gap_stddev_us": 3.09789758545048 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2489.0934098021, + "gpu_time_us_python": 2509.13701080094, + "gpu_noise_us_cpp": 2.194, + "gpu_noise_us_python": 3.5440000000000005, + "gpu_bwutil_cpp": 0.26152195894798813, + "gpu_bwutil_python": 0.2594289500814186, + "gpu_gap_stddev_us": 2.7185748877669895 + } + }, + "hqresize_expand_advanced[InOutDataType=uint8][shape=32x1080x1920][interpolation=CUBIC][resizeType=EXPAND][layout=NHWC][inputKind=Tensor][antialias=0][numChannels=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2228.15335071186, + "gpu_time_us_python": 2246.8260100909, + "gpu_noise_us_cpp": 4.748, + "gpu_noise_us_python": 4.2620000000000005, + "gpu_bwutil_cpp": 0.09574457573477482, + "gpu_bwutil_python": 0.09494890062106358, + "gpu_gap_stddev_us": 1.5986119266454721 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1972.75272595918, + "gpu_time_us_python": 1991.9961061119404, + "gpu_noise_us_cpp": 3.088, + "gpu_noise_us_python": 5.014, + "gpu_bwutil_cpp": 0.08249340390785094, + "gpu_bwutil_python": 0.08169406816344664, + "gpu_gap_stddev_us": 3.138848138519017 + } + } + } + }, + "hqresize_antialias_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "interpolation": [ + "CUBIC" + ], + "resizeType": [ + "CONTRACT" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 1 + ], + "numChannels": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize single-channel antialias (advanced)." + }, + "baselines": { + "hqresize_antialias_advanced[InOutDataType=float32][shape=64x1080x1920][interpolation=CUBIC][resizeType=CONTRACT][layout=NHWC][inputKind=Tensor][antialias=1][numChannels=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1510.4341695949802, + "gpu_time_us_python": 1528.0759679098803, + "gpu_noise_us_cpp": 1.158, + "gpu_noise_us_python": 4.101999999999999, + "gpu_bwutil_cpp": 0.2824806670309008, + "gpu_bwutil_python": 0.27921938964095616, + "gpu_gap_stddev_us": 1.4787072322482422 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1219.3134093962, + "gpu_time_us_python": 1238.0617653665402, + "gpu_noise_us_cpp": 1.3400000000000003, + "gpu_noise_us_python": 3.2980000000000005, + "gpu_bwutil_cpp": 0.2669297295941759, + "gpu_bwutil_python": 0.2628890235422731, + "gpu_gap_stddev_us": 3.5879877879947695 + } + }, + "hqresize_antialias_advanced[InOutDataType=uint8][shape=64x1080x1920][interpolation=CUBIC][resizeType=CONTRACT][layout=NHWC][inputKind=Tensor][antialias=1][numChannels=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1178.98524779654, + "gpu_time_us_python": 1196.2227772851197, + "gpu_noise_us_cpp": 1.9839999999999995, + "gpu_noise_us_python": 3.13, + "gpu_bwutil_cpp": 0.09047361063849206, + "gpu_bwutil_python": 0.08916998646732278, + "gpu_gap_stddev_us": 2.0913276847895967 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1044.6110407152198, + "gpu_time_us_python": 1062.1366830741601, + "gpu_noise_us_cpp": 1.1420000000000001, + "gpu_noise_us_python": 4.117999999999999, + "gpu_bwutil_cpp": 0.0778931975065521, + "gpu_bwutil_python": 0.07660838567489153, + "gpu_gap_stddev_us": 2.0824387309398857 + } + } + } + }, + "hqresize_contract_linear_720p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "512x720x1280" + ], + "interpolation": [ + "LINEAR" + ], + "resizeType": [ + "CONTRACT" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize single-channel contract linear (advanced); batch 512 so the constant per-iteration harness overhead stays small relative to kernel time." + }, + "baselines": { + "hqresize_contract_linear_720p_advanced[InOutDataType=float32][shape=512x720x1280][interpolation=LINEAR][resizeType=CONTRACT][layout=NHWC][inputKind=Tensor][antialias=0][numChannels=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1740.69335129522, + "gpu_time_us_python": 1758.81935342428, + "gpu_noise_us_cpp": 2.3619999999999997, + "gpu_noise_us_python": 5.0120000000000005, + "gpu_bwutil_cpp": 0.8715131931458766, + "gpu_bwutil_python": 0.8625391698332152, + "gpu_gap_stddev_us": 5.398941991212987 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1338.57967993788, + "gpu_time_us_python": 1357.01737768566, + "gpu_noise_us_cpp": 1.34, + "gpu_noise_us_python": 3.5440000000000005, + "gpu_bwutil_cpp": 0.8643961901168682, + "gpu_bwutil_python": 0.8526552036052069, + "gpu_gap_stddev_us": 4.3633438051216284 + } + }, + "hqresize_contract_linear_720p_advanced[InOutDataType=uint8][shape=512x720x1280][interpolation=LINEAR][resizeType=CONTRACT][layout=NHWC][inputKind=Tensor][antialias=0][numChannels=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 681.91745408884, + "gpu_time_us_python": 697.8000850307601, + "gpu_noise_us_cpp": 1.402, + "gpu_noise_us_python": 3.7079999999999997, + "gpu_bwutil_cpp": 0.556167893596005, + "gpu_bwutil_python": 0.5435136117927382, + "gpu_gap_stddev_us": 2.1194416974621615 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 613.7036102517, + "gpu_time_us_python": 633.1594533033601, + "gpu_noise_us_cpp": 1.1860000000000002, + "gpu_noise_us_python": 2.97, + "gpu_bwutil_cpp": 0.4714137576376126, + "gpu_bwutil_python": 0.4568795627184793, + "gpu_gap_stddev_us": 4.7267433892949935 + } + } + } + }, + "hqresize_contract_linear_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "interpolation": [ + "LINEAR" + ], + "resizeType": [ + "CONTRACT" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize single-channel contract linear (advanced)." + }, + "baselines": { + "hqresize_contract_linear_1080p_advanced[InOutDataType=float32][shape=64x1080x1920][interpolation=LINEAR][resizeType=CONTRACT][layout=NHWC][inputKind=Tensor][antialias=0][numChannels=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 504.09696866961997, + "gpu_time_us_python": 519.69162241342, + "gpu_noise_us_cpp": 1.5479999999999998, + "gpu_noise_us_python": 2.4160000000000004, + "gpu_bwutil_cpp": 0.8463984674530719, + "gpu_bwutil_python": 0.8210059010662454, + "gpu_gap_stddev_us": 1.2508468789067546 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 387.22748874260003, + "gpu_time_us_python": 400.72972719333995, + "gpu_noise_us_cpp": 1.5580000000000003, + "gpu_noise_us_python": 3.2760000000000007, + "gpu_bwutil_cpp": 0.8403954746357496, + "gpu_bwutil_python": 0.8120849561509651, + "gpu_gap_stddev_us": 1.1763048651166865 + } + }, + "hqresize_contract_linear_1080p_advanced[InOutDataType=uint8][shape=64x1080x1920][interpolation=LINEAR][resizeType=CONTRACT][layout=NHWC][inputKind=Tensor][antialias=0][numChannels=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 212.2211265, + "gpu_time_us_python": 227.63174116303998, + "gpu_noise_us_cpp": 1.01, + "gpu_noise_us_python": 2.412, + "gpu_bwutil_cpp": 0.5026281296341365, + "gpu_bwutil_python": 0.468620527353585, + "gpu_gap_stddev_us": 1.320800614753651 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 190.70930620331998, + "gpu_time_us_python": 206.45607680819998, + "gpu_noise_us_cpp": 0.6659999999999999, + "gpu_noise_us_python": 2.874, + "gpu_bwutil_cpp": 0.42664562499547803, + "gpu_bwutil_python": 0.39408172446297807, + "gpu_gap_stddev_us": 1.0676408942243916 + } + } + } + }, + "hqresize_contract_linear_4k_uint8_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "16x2160x3840" + ], + "interpolation": [ + "LINEAR" + ], + "resizeType": [ + "CONTRACT" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize single-channel contract linear (advanced)." + }, + "baselines": { + "hqresize_contract_linear_4k_uint8_advanced[InOutDataType=uint8][shape=16x2160x3840][interpolation=LINEAR][resizeType=CONTRACT][layout=NHWC][inputKind=Tensor][antialias=0][numChannels=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 199.70926033912002, + "gpu_time_us_python": 216.62179963041999, + "gpu_noise_us_cpp": 1.5579999999999998, + "gpu_noise_us_python": 3.4659999999999997, + "gpu_bwutil_cpp": 0.5341175123675164, + "gpu_bwutil_python": 0.49246162435730245, + "gpu_gap_stddev_us": 1.8743539157282156 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 179.37596160914, + "gpu_time_us_python": 194.88663458588, + "gpu_noise_us_cpp": 1.0959999999999999, + "gpu_noise_us_python": 2.05, + "gpu_bwutil_cpp": 0.4535965872641836, + "gpu_bwutil_python": 0.4174875314920854, + "gpu_gap_stddev_us": 1.1526368900946702 + } + } + } + }, + "hqresize_contract_linear_4k_f32_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "16x2160x3840" + ], + "interpolation": [ + "LINEAR" + ], + "resizeType": [ + "CONTRACT" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize single-channel contract linear (advanced)." + }, + "baselines": { + "hqresize_contract_linear_4k_f32_advanced[InOutDataType=float32][shape=16x2160x3840][interpolation=LINEAR][resizeType=CONTRACT][layout=NHWC][inputKind=Tensor][antialias=0][numChannels=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 504.35189576798, + "gpu_time_us_python": 521.3742304083, + "gpu_noise_us_cpp": 2.0919999999999996, + "gpu_noise_us_python": 3.6420000000000003, + "gpu_bwutil_cpp": 0.8459729040883703, + "gpu_bwutil_python": 0.8183605953774009, + "gpu_gap_stddev_us": 1.9318111349671845 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 386.94422926880003, + "gpu_time_us_python": 401.94429979986, + "gpu_noise_us_cpp": 1.0079999999999998, + "gpu_noise_us_python": 2.934, + "gpu_bwutil_cpp": 0.8410106162796922, + "gpu_bwutil_python": 0.8096657452604148, + "gpu_gap_stddev_us": 2.8057564998737083 + } + } + } + }, + "hqresize_contract_cubic_720p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "128x720x1280" + ], + "interpolation": [ + "CUBIC" + ], + "resizeType": [ + "CONTRACT" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize single-channel contract cubic (advanced)." + }, + "baselines": { + "hqresize_contract_cubic_720p_advanced[InOutDataType=float32][shape=128x720x1280][interpolation=CUBIC][resizeType=CONTRACT][layout=NHWC][inputKind=Tensor][antialias=0][numChannels=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 808.23355028064, + "gpu_time_us_python": 827.3353640156199, + "gpu_noise_us_cpp": 2.174, + "gpu_noise_us_python": 2.77, + "gpu_bwutil_cpp": 0.4692453489582523, + "gpu_bwutil_python": 0.45842312248809935, + "gpu_gap_stddev_us": 4.228225119949518 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 755.02533560362, + "gpu_time_us_python": 773.17784700098, + "gpu_noise_us_cpp": 1.534, + "gpu_noise_us_python": 2.79, + "gpu_bwutil_cpp": 0.38317092574994605, + "gpu_bwutil_python": 0.37414895722939856, + "gpu_gap_stddev_us": 3.582791330348175 + } + }, + "hqresize_contract_cubic_720p_advanced[InOutDataType=uint8][shape=128x720x1280][interpolation=CUBIC][resizeType=CONTRACT][layout=NHWC][inputKind=Tensor][antialias=0][numChannels=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 792.58702770816, + "gpu_time_us_python": 809.48529833722, + "gpu_noise_us_cpp": 0.978, + "gpu_noise_us_python": 3.1439999999999997, + "gpu_bwutil_cpp": 0.1196270682694425, + "gpu_bwutil_python": 0.11712986522389834, + "gpu_gap_stddev_us": 0.8234170790549626 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 615.9770741949, + "gpu_time_us_python": 633.90402862084, + "gpu_noise_us_cpp": 1.1179999999999999, + "gpu_noise_us_python": 2.4160000000000004, + "gpu_bwutil_cpp": 0.11741375185710141, + "gpu_bwutil_python": 0.11408659664437408, + "gpu_gap_stddev_us": 3.271957186823858 + } + } + } + }, + "hqresize_contract_cubic_4k_uint8_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "16x2160x3840" + ], + "interpolation": [ + "CUBIC" + ], + "resizeType": [ + "CONTRACT" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize single-channel contract cubic (advanced)." + }, + "baselines": { + "hqresize_contract_cubic_4k_uint8_advanced[InOutDataType=uint8][shape=16x2160x3840][interpolation=CUBIC][resizeType=CONTRACT][layout=NHWC][inputKind=Tensor][antialias=0][numChannels=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 625.07082243136, + "gpu_time_us_python": 642.0802366533601, + "gpu_noise_us_cpp": 2.246, + "gpu_noise_us_python": 4.574, + "gpu_bwutil_cpp": 0.17064792550145547, + "gpu_bwutil_python": 0.16612930056704847, + "gpu_gap_stddev_us": 1.6529820747621877 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 548.75462570852, + "gpu_time_us_python": 565.0485404705, + "gpu_noise_us_cpp": 1.8439999999999999, + "gpu_noise_us_python": 4.38, + "gpu_bwutil_cpp": 0.14827670086088462, + "gpu_bwutil_python": 0.14399749971549447, + "gpu_gap_stddev_us": 1.6060712033973972 + } + } + } + }, + "hqresize_contract_cubic_4k_f32_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "16x2160x3840" + ], + "interpolation": [ + "CUBIC" + ], + "resizeType": [ + "CONTRACT" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize single-channel contract cubic (advanced)." + }, + "baselines": { + "hqresize_contract_cubic_4k_f32_advanced[InOutDataType=float32][shape=16x2160x3840][interpolation=CUBIC][resizeType=CONTRACT][layout=NHWC][inputKind=Tensor][antialias=0][numChannels=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 757.27650985874, + "gpu_time_us_python": 773.5113088252, + "gpu_noise_us_cpp": 2.0380000000000003, + "gpu_noise_us_python": 2.998, + "gpu_bwutil_cpp": 0.5634232034895862, + "gpu_bwutil_python": 0.5516031652344141, + "gpu_gap_stddev_us": 2.4418263842914576 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 709.3650585171399, + "gpu_time_us_python": 726.92809521302, + "gpu_noise_us_cpp": 1.174, + "gpu_noise_us_python": 3.4799999999999995, + "gpu_bwutil_cpp": 0.45880475245980384, + "gpu_bwutil_python": 0.4477160892727934, + "gpu_gap_stddev_us": 2.2771141994187447 + } + } + } + }, + "hqresize_expand_linear_720p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "64x720x1280" + ], + "interpolation": [ + "LINEAR" + ], + "resizeType": [ + "EXPAND" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize single-channel expand linear (advanced)." + }, + "baselines": { + "hqresize_expand_linear_720p_advanced[InOutDataType=float32][shape=64x720x1280][interpolation=LINEAR][resizeType=EXPAND][layout=NHWC][inputKind=Tensor][antialias=0][numChannels=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 966.48915877208, + "gpu_time_us_python": 983.2215616193, + "gpu_noise_us_cpp": 2.7800000000000002, + "gpu_noise_us_python": 3.842, + "gpu_bwutil_cpp": 0.7848184670071061, + "gpu_bwutil_python": 0.7714700261446065, + "gpu_gap_stddev_us": 3.3144325421632703 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 739.47480669828, + "gpu_time_us_python": 754.9456212454199, + "gpu_noise_us_cpp": 2.46, + "gpu_noise_us_python": 3.5760000000000005, + "gpu_bwutil_cpp": 0.7823549737694624, + "gpu_bwutil_python": 0.7663314673606788, + "gpu_gap_stddev_us": 2.315641349249317 + } + }, + "hqresize_expand_linear_720p_advanced[InOutDataType=uint8][shape=64x720x1280][interpolation=LINEAR][resizeType=EXPAND][layout=NHWC][inputKind=Tensor][antialias=0][numChannels=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 438.94634487559995, + "gpu_time_us_python": 455.94236450608, + "gpu_noise_us_cpp": 2.3099999999999996, + "gpu_noise_us_python": 3.654000000000001, + "gpu_bwutil_cpp": 0.4320135667574414, + "gpu_bwutil_python": 0.4159218294215677, + "gpu_gap_stddev_us": 2.294300458302885 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 399.97775870384004, + "gpu_time_us_python": 417.94636126309996, + "gpu_noise_us_cpp": 0.9560000000000001, + "gpu_noise_us_python": 4.084, + "gpu_bwutil_cpp": 0.3616531149649592, + "gpu_bwutil_python": 0.3460917169467915, + "gpu_gap_stddev_us": 3.2880735611957745 + } + } + } + }, + "hqresize_expand_linear_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "interpolation": [ + "LINEAR" + ], + "resizeType": [ + "EXPAND" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize single-channel expand linear (advanced)." + }, + "baselines": { + "hqresize_expand_linear_1080p_advanced[InOutDataType=float32][shape=64x1080x1920][interpolation=LINEAR][resizeType=EXPAND][layout=NHWC][inputKind=Tensor][antialias=0][numChannels=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2161.46626924642, + "gpu_time_us_python": 2179.32842544882, + "gpu_noise_us_cpp": 4.77, + "gpu_noise_us_python": 5.851999999999999, + "gpu_bwutil_cpp": 0.7895878159175023, + "gpu_bwutil_python": 0.7831161995045312, + "gpu_gap_stddev_us": 1.5552457496949195 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1656.3862465864, + "gpu_time_us_python": 1670.9378126461802, + "gpu_noise_us_cpp": 2.6340000000000003, + "gpu_noise_us_python": 3.7880000000000003, + "gpu_bwutil_cpp": 0.7858646017988293, + "gpu_bwutil_python": 0.7790222420475148, + "gpu_gap_stddev_us": 2.1418878430443726 + } + }, + "hqresize_expand_linear_1080p_advanced[InOutDataType=uint8][shape=64x1080x1920][interpolation=LINEAR][resizeType=EXPAND][layout=NHWC][inputKind=Tensor][antialias=0][numChannels=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 889.70485600498, + "gpu_time_us_python": 906.0885769599602, + "gpu_noise_us_cpp": 1.6919999999999997, + "gpu_noise_us_python": 4.561999999999999, + "gpu_bwutil_cpp": 0.4795622500014473, + "gpu_bwutil_python": 0.47089272760295964, + "gpu_gap_stddev_us": 2.3919607349271264 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 814.1601109725, + "gpu_time_us_python": 829.9635198153599, + "gpu_noise_us_cpp": 1.2440000000000002, + "gpu_noise_us_python": 3.4260000000000006, + "gpu_bwutil_cpp": 0.3997681944250112, + "gpu_bwutil_python": 0.3921466718324843, + "gpu_gap_stddev_us": 1.2853060838210355 + } + } + } + }, + "hqresize_expand_linear_4k_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "8x2160x3840" + ], + "interpolation": [ + "LINEAR" + ], + "resizeType": [ + "EXPAND" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize single-channel expand linear (advanced)." + }, + "baselines": { + "hqresize_expand_linear_4k_advanced[InOutDataType=float32][shape=8x2160x3840][interpolation=LINEAR][resizeType=EXPAND][layout=NHWC][inputKind=Tensor][antialias=0][numChannels=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1085.78223356432, + "gpu_time_us_python": 1103.5846413889, + "gpu_noise_us_cpp": 3.4879999999999995, + "gpu_noise_us_python": 3.91, + "gpu_bwutil_cpp": 0.785916041283814, + "gpu_bwutil_python": 0.7732434914376015, + "gpu_gap_stddev_us": 2.9804752621901627 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 829.62019547278, + "gpu_time_us_python": 844.53755102176, + "gpu_noise_us_cpp": 2.292, + "gpu_noise_us_python": 3.8240000000000003, + "gpu_bwutil_cpp": 0.7845139282460485, + "gpu_bwutil_python": 0.7706624281892365, + "gpu_gap_stddev_us": 1.6859641942518242 + } + }, + "hqresize_expand_linear_4k_advanced[InOutDataType=uint8][shape=8x2160x3840][interpolation=LINEAR][resizeType=EXPAND][layout=NHWC][inputKind=Tensor][antialias=0][numChannels=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 410.59898324834, + "gpu_time_us_python": 427.19007637138003, + "gpu_noise_us_cpp": 1.5000000000000002, + "gpu_noise_us_python": 2.604, + "gpu_bwutil_cpp": 0.5195704388537277, + "gpu_bwutil_python": 0.4994043522881645, + "gpu_gap_stddev_us": 1.5256234191999045 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 376.89912411826, + "gpu_time_us_python": 394.41742599496007, + "gpu_noise_us_cpp": 1.4440000000000002, + "gpu_noise_us_python": 3.3620000000000005, + "gpu_bwutil_cpp": 0.4317698179055536, + "gpu_bwutil_python": 0.4125625442687956, + "gpu_gap_stddev_us": 4.062399808182253 + } + } + } + }, + "hqresize_expand_cubic_720p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "64x720x1280" + ], + "interpolation": [ + "CUBIC" + ], + "resizeType": [ + "EXPAND" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize single-channel expand cubic (advanced)." + }, + "baselines": { + "hqresize_expand_cubic_720p_advanced[InOutDataType=float32][shape=64x720x1280][interpolation=CUBIC][resizeType=EXPAND][layout=NHWC][inputKind=Tensor][antialias=0][numChannels=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2753.0566147394998, + "gpu_time_us_python": 2770.6199431156997, + "gpu_noise_us_cpp": 3.2979999999999996, + "gpu_noise_us_python": 4.714, + "gpu_bwutil_cpp": 0.27551893925164134, + "gpu_bwutil_python": 0.27377251553134563, + "gpu_gap_stddev_us": 1.0980879017640024 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2327.42593497058, + "gpu_time_us_python": 2344.4146985967604, + "gpu_noise_us_cpp": 1.9659999999999997, + "gpu_noise_us_python": 3.5799999999999996, + "gpu_bwutil_cpp": 0.24861182689024655, + "gpu_bwutil_python": 0.2468081870915023, + "gpu_gap_stddev_us": 1.1649234389856902 + } + }, + "hqresize_expand_cubic_720p_advanced[InOutDataType=uint8][shape=64x720x1280][interpolation=CUBIC][resizeType=EXPAND][layout=NHWC][inputKind=Tensor][antialias=0][numChannels=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2242.61300478658, + "gpu_time_us_python": 2260.4680310497997, + "gpu_noise_us_cpp": 4.428, + "gpu_noise_us_python": 5.544, + "gpu_bwutil_cpp": 0.08455755181643952, + "gpu_bwutil_python": 0.08388973402990164, + "gpu_gap_stddev_us": 2.074428341142544 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2004.30844487164, + "gpu_time_us_python": 2022.6392327885, + "gpu_noise_us_cpp": 2.9959999999999996, + "gpu_noise_us_python": 4.906000000000001, + "gpu_bwutil_cpp": 0.07217335201384932, + "gpu_bwutil_python": 0.07151839885767929, + "gpu_gap_stddev_us": 2.3488108447215006 + } + } + } + }, + "hqresize_expand_cubic_4k_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "8x2160x3840" + ], + "interpolation": [ + "CUBIC" + ], + "resizeType": [ + "EXPAND" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize single-channel expand cubic (advanced)." + }, + "baselines": { + "hqresize_expand_cubic_4k_advanced[InOutDataType=float32][shape=8x2160x3840][interpolation=CUBIC][resizeType=EXPAND][layout=NHWC][inputKind=Tensor][antialias=0][numChannels=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2843.78683377696, + "gpu_time_us_python": 2863.4042441844203, + "gpu_noise_us_cpp": 3.752, + "gpu_noise_us_python": 5.42, + "gpu_bwutil_cpp": 0.3000697054889806, + "gpu_bwutil_python": 0.2980142944444149, + "gpu_gap_stddev_us": 1.8487092365138154 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2360.1169341833397, + "gpu_time_us_python": 2380.0462121245, + "gpu_noise_us_cpp": 2.196, + "gpu_noise_us_python": 3.942, + "gpu_bwutil_cpp": 0.2758136843856869, + "gpu_bwutil_python": 0.273495120210121, + "gpu_gap_stddev_us": 3.6970997284277964 + } + }, + "hqresize_expand_cubic_4k_advanced[InOutDataType=uint8][shape=8x2160x3840][interpolation=CUBIC][resizeType=EXPAND][layout=NHWC][inputKind=Tensor][antialias=0][numChannels=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1936.8363364153404, + "gpu_time_us_python": 1957.5574787578596, + "gpu_noise_us_cpp": 4.246, + "gpu_noise_us_python": 7.014, + "gpu_bwutil_cpp": 0.110145397631549, + "gpu_bwutil_python": 0.1089800448945295, + "gpu_gap_stddev_us": 5.712118934193618 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1695.1853726593, + "gpu_time_us_python": 1713.6623879974202, + "gpu_noise_us_cpp": 3.026, + "gpu_noise_us_python": 4.33, + "gpu_bwutil_cpp": 0.09599975031061864, + "gpu_bwutil_python": 0.09496230640198824, + "gpu_gap_stddev_us": 3.118683505689332 + } + } + } + }, + "hqresize_contract_antialias_720p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "128x720x1280" + ], + "interpolation": [ + "CUBIC" + ], + "resizeType": [ + "CONTRACT" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 1 + ], + "numChannels": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize single-channel contract antialias (advanced)." + }, + "baselines": { + "hqresize_contract_antialias_720p_advanced[InOutDataType=float32][shape=128x720x1280][interpolation=CUBIC][resizeType=CONTRACT][layout=NHWC][inputKind=Tensor][antialias=1][numChannels=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1332.6549842814002, + "gpu_time_us_python": 1350.1689291444, + "gpu_noise_us_cpp": 2.734, + "gpu_noise_us_python": 3.908, + "gpu_bwutil_cpp": 0.2845904784214507, + "gpu_bwutil_python": 0.28089957856288217, + "gpu_gap_stddev_us": 3.5579114205976428 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1075.54036923838, + "gpu_time_us_python": 1093.1609926371, + "gpu_noise_us_cpp": 1.1320000000000001, + "gpu_noise_us_python": 2.7079999999999997, + "gpu_bwutil_cpp": 0.2689877665302303, + "gpu_bwutil_python": 0.26464376788334576, + "gpu_gap_stddev_us": 1.4739035579846262 + } + }, + "hqresize_contract_antialias_720p_advanced[InOutDataType=uint8][shape=128x720x1280][interpolation=CUBIC][resizeType=CONTRACT][layout=NHWC][inputKind=Tensor][antialias=1][numChannels=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1038.59170063182, + "gpu_time_us_python": 1055.9618716148002, + "gpu_noise_us_cpp": 1.42, + "gpu_noise_us_python": 2.5600000000000005, + "gpu_bwutil_cpp": 0.09129192461614376, + "gpu_bwutil_python": 0.0897908475309426, + "gpu_gap_stddev_us": 3.1571431928741993 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 921.4179963017599, + "gpu_time_us_python": 938.4141471422199, + "gpu_noise_us_cpp": 2.1460000000000004, + "gpu_noise_us_python": 3.286, + "gpu_bwutil_cpp": 0.07849505517967703, + "gpu_bwutil_python": 0.07707185365935554, + "gpu_gap_stddev_us": 1.2867813698231705 + } + } + } + }, + "hqresize_contract_antialias_4k_uint8_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "16x2160x3840" + ], + "interpolation": [ + "CUBIC" + ], + "resizeType": [ + "CONTRACT" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 1 + ], + "numChannels": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize single-channel contract antialias (advanced)." + }, + "baselines": { + "hqresize_contract_antialias_4k_uint8_advanced[InOutDataType=uint8][shape=16x2160x3840][interpolation=CUBIC][resizeType=CONTRACT][layout=NHWC][inputKind=Tensor][antialias=1][numChannels=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1162.94604903656, + "gpu_time_us_python": 1179.0879385841001, + "gpu_noise_us_cpp": 1.404, + "gpu_noise_us_python": 3.728, + "gpu_bwutil_cpp": 0.0917214421680556, + "gpu_bwutil_python": 0.09046578181553347, + "gpu_gap_stddev_us": 1.7738010574926364 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1027.7972131972401, + "gpu_time_us_python": 1045.77728857706, + "gpu_noise_us_cpp": 2.18, + "gpu_noise_us_python": 4.396, + "gpu_bwutil_cpp": 0.07916696773665602, + "gpu_bwutil_python": 0.07779931537883235, + "gpu_gap_stddev_us": 5.457811002057526 + } + } + } + }, + "hqresize_contract_antialias_4k_f32_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "16x2160x3840" + ], + "interpolation": [ + "CUBIC" + ], + "resizeType": [ + "CONTRACT" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 1 + ], + "numChannels": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize single-channel contract antialias (advanced)." + }, + "baselines": { + "hqresize_contract_antialias_4k_f32_advanced[InOutDataType=float32][shape=16x2160x3840][interpolation=CUBIC][resizeType=CONTRACT][layout=NHWC][inputKind=Tensor][antialias=1][numChannels=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1490.68344346008, + "gpu_time_us_python": 1508.1918134660402, + "gpu_noise_us_cpp": 1.6239999999999999, + "gpu_noise_us_python": 5.348, + "gpu_bwutil_cpp": 0.2862229495492793, + "gpu_bwutil_python": 0.282900521484278, + "gpu_gap_stddev_us": 1.0256098794475128 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1202.03519419566, + "gpu_time_us_python": 1218.87873112254, + "gpu_noise_us_cpp": 1.94, + "gpu_noise_us_python": 2.992, + "gpu_bwutil_cpp": 0.2707647238444113, + "gpu_bwutil_python": 0.2670154742887646, + "gpu_gap_stddev_us": 2.2676169937166004 + } + } + } + }, + "hqresize_contract_linear_antialias_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "interpolation": [ + "LINEAR" + ], + "resizeType": [ + "CONTRACT" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 1 + ], + "numChannels": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize single-channel contract linear antialias (advanced)." + }, + "baselines": { + "hqresize_contract_linear_antialias_1080p_advanced[InOutDataType=float32][shape=32x1080x1920][interpolation=LINEAR][resizeType=CONTRACT][layout=NHWC][inputKind=Tensor][antialias=1][numChannels=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 425.0483759796601, + "gpu_time_us_python": 441.38073235874, + "gpu_noise_us_cpp": 1.294, + "gpu_noise_us_python": 2.956, + "gpu_bwutil_cpp": 0.5019059576163547, + "gpu_bwutil_python": 0.48335390739809475, + "gpu_gap_stddev_us": 3.136340012767571 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 398.77494139593995, + "gpu_time_us_python": 414.79912299748, + "gpu_noise_us_cpp": 1.3359999999999999, + "gpu_noise_us_python": 2.438, + "gpu_bwutil_cpp": 0.40807058530962453, + "gpu_bwutil_python": 0.39229831273136107, + "gpu_gap_stddev_us": 0.7186433680129961 + } + }, + "hqresize_contract_linear_antialias_1080p_advanced[InOutDataType=uint8][shape=32x1080x1920][interpolation=LINEAR][resizeType=CONTRACT][layout=NHWC][inputKind=Tensor][antialias=1][numChannels=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 467.7303427789, + "gpu_time_us_python": 484.46023551950003, + "gpu_noise_us_cpp": 2.33, + "gpu_noise_us_python": 2.3920000000000003, + "gpu_bwutil_cpp": 0.11402608293395208, + "gpu_bwutil_python": 0.11009241811769996, + "gpu_gap_stddev_us": 3.1350385531265688 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 365.26249585615994, + "gpu_time_us_python": 381.3588773287, + "gpu_noise_us_cpp": 1.4200000000000002, + "gpu_noise_us_python": 2.5460000000000003, + "gpu_bwutil_cpp": 0.11137573670344843, + "gpu_bwutil_python": 0.10667296573001503, + "gpu_gap_stddev_us": 0.6151167193385584 + } + } + } + }, + "hqresize_planar_contract_nhwc_1080p_basic": { + "tier": "basic", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "interpolation": [ + "CUBIC" + ], + "resizeType": [ + "CONTRACT" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Basic HQResize interleaved (NHWC, 3-channel) CONTRACT CUBIC 1080p tensor profile; parity partner of the NCHW planar profile." + }, + "baselines": { + "hqresize_planar_contract_nhwc_1080p_basic[InOutDataType=float32][shape=64x1080x1920][interpolation=CUBIC][resizeType=CONTRACT][layout=NHWC][inputKind=Tensor][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1762.29637139628, + "gpu_time_us_python": 1780.24780213284, + "gpu_noise_us_cpp": 7.038000000000001, + "gpu_noise_us_python": 7.523999999999999, + "gpu_bwutil_cpp": 0.7263258441892586, + "gpu_bwutil_python": 0.7190030584421561, + "gpu_gap_stddev_us": 1.5567685912011011 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1464.29697880536, + "gpu_time_us_python": 1481.9343699281799, + "gpu_noise_us_cpp": 1.6059999999999999, + "gpu_noise_us_python": 3.106, + "gpu_bwutil_cpp": 0.6668009293138476, + "gpu_bwutil_python": 0.658850693183925, + "gpu_gap_stddev_us": 1.9314468601701111 + } + }, + "hqresize_planar_contract_nhwc_1080p_basic[InOutDataType=uint8][shape=64x1080x1920][interpolation=CUBIC][resizeType=CONTRACT][layout=NHWC][inputKind=Tensor][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1493.57351930364, + "gpu_time_us_python": 1512.57283999558, + "gpu_noise_us_cpp": 1.8660000000000003, + "gpu_noise_us_python": 3.8779999999999992, + "gpu_bwutil_cpp": 0.21425144693607576, + "gpu_bwutil_python": 0.21156172252167452, + "gpu_gap_stddev_us": 3.317103609440051 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1252.14122604742, + "gpu_time_us_python": 1268.8902523638799, + "gpu_noise_us_cpp": 1.3579999999999999, + "gpu_noise_us_python": 2.91, + "gpu_bwutil_cpp": 0.19494521001606085, + "gpu_bwutil_python": 0.19237196259542622, + "gpu_gap_stddev_us": 1.0851027048457382 + } + } + } + }, + "hqresize_planar_contract_nchw_1080p_basic": { + "tier": "basic", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "interpolation": [ + "CUBIC" + ], + "resizeType": [ + "CONTRACT" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Basic HQResize planar (NCHW, 3-channel) CONTRACT CUBIC 1080p tensor profile." + }, + "baselines": { + "hqresize_planar_contract_nchw_1080p_basic[InOutDataType=float32][shape=64x1080x1920][interpolation=CUBIC][resizeType=CONTRACT][layout=NCHW][inputKind=Tensor][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2453.0260477840598, + "gpu_time_us_python": 2472.1941615372402, + "gpu_noise_us_cpp": 2.332, + "gpu_noise_us_python": 4.314000000000001, + "gpu_bwutil_cpp": 0.5218068556734371, + "gpu_bwutil_python": 0.5177606791518008, + "gpu_gap_stddev_us": 4.199078988183228 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2300.69509861582, + "gpu_time_us_python": 2317.2741082380003, + "gpu_noise_us_cpp": 2.8880000000000003, + "gpu_noise_us_python": 7.588000000000001, + "gpu_bwutil_cpp": 0.42439079428622783, + "gpu_bwutil_python": 0.4213471951643658, + "gpu_gap_stddev_us": 6.856787878347922 + } + }, + "hqresize_planar_contract_nchw_1080p_basic[InOutDataType=uint8][shape=64x1080x1920][interpolation=CUBIC][resizeType=CONTRACT][layout=NCHW][inputKind=Tensor][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2648.8659246344, + "gpu_time_us_python": 2665.39023899682, + "gpu_noise_us_cpp": 2.0460000000000003, + "gpu_noise_us_python": 3.882, + "gpu_bwutil_cpp": 0.12080685966352118, + "gpu_bwutil_python": 0.12005794634484493, + "gpu_gap_stddev_us": 1.1201458694170743 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2048.03045618068, + "gpu_time_us_python": 2064.5857215979004, + "gpu_noise_us_cpp": 1.48, + "gpu_noise_us_python": 8.33, + "gpu_bwutil_cpp": 0.11918613339021016, + "gpu_bwutil_python": 0.1182272634992975, + "gpu_gap_stddev_us": 5.461217966364115 + } + } + } + }, + "hqresize_planar_contract_nchwfake_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "interpolation": [ + "CUBIC" + ], + "resizeType": [ + "CONTRACT" + ], + "layout": [ + "NCHW", + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Advanced HQResize native NCHW and fake-planar NCHW_FAKE comparison for RGB 3-channel CONTRACT CUBIC 1080p tensors." + }, + "baselines": { + "hqresize_planar_contract_nchwfake_1080p_advanced[InOutDataType=float32][shape=64x1080x1920][interpolation=CUBIC][resizeType=CONTRACT][layout=NCHW_FAKE][inputKind=Tensor][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5372.17418792396, + "gpu_time_us_python": 5391.643236038481, + "gpu_noise_us_cpp": 6.168000000000001, + "gpu_noise_us_python": 6.544, + "gpu_bwutil_cpp": 0.7148014781376892, + "gpu_bwutil_python": 0.7122194876873255, + "gpu_gap_stddev_us": 2.8030696744833055 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4519.229372910059, + "gpu_time_us_python": 4533.433140686519, + "gpu_noise_us_cpp": 2.2800000000000002, + "gpu_noise_us_python": 3.4659999999999997, + "gpu_bwutil_cpp": 0.648094585466937, + "gpu_bwutil_python": 0.6460642042767859, + "gpu_gap_stddev_us": 1.5580125079353695 + } + }, + "hqresize_planar_contract_nchwfake_1080p_advanced[InOutDataType=uint8][shape=64x1080x1920][interpolation=CUBIC][resizeType=CONTRACT][layout=NCHW_FAKE][inputKind=Tensor][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3286.7806616500397, + "gpu_time_us_python": 3304.54769260002, + "gpu_noise_us_cpp": 1.7899999999999998, + "gpu_noise_us_python": 2.04, + "gpu_bwutil_cpp": 0.29208132145150917, + "gpu_bwutil_python": 0.2905106825226662, + "gpu_gap_stddev_us": 2.587915317564042 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2636.10859942536, + "gpu_time_us_python": 2650.3746811273404, + "gpu_noise_us_cpp": 1.35, + "gpu_noise_us_python": 2.592, + "gpu_bwutil_cpp": 0.2777746412539319, + "gpu_bwutil_python": 0.27627900882261114, + "gpu_gap_stddev_us": 0.9433930227467784 + } + }, + "hqresize_planar_contract_nchwfake_1080p_advanced[InOutDataType=float32][shape=64x1080x1920][interpolation=CUBIC][resizeType=CONTRACT][layout=NCHW][inputKind=Tensor][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2451.9475782845198, + "gpu_time_us_python": 2470.44463951912, + "gpu_noise_us_cpp": 4.328, + "gpu_noise_us_python": 5.458, + "gpu_bwutil_cpp": 0.5220347010220087, + "gpu_bwutil_python": 0.5181272876082877, + "gpu_gap_stddev_us": 2.7755766844674112 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2301.43958052114, + "gpu_time_us_python": 2317.59234761166, + "gpu_noise_us_cpp": 1.9100000000000001, + "gpu_noise_us_python": 7.908000000000001, + "gpu_bwutil_cpp": 0.4242547075005432, + "gpu_bwutil_python": 0.4212898015835309, + "gpu_gap_stddev_us": 5.729566797094788 + } + }, + "hqresize_planar_contract_nchwfake_1080p_advanced[InOutDataType=uint8][shape=64x1080x1920][interpolation=CUBIC][resizeType=CONTRACT][layout=NCHW][inputKind=Tensor][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2648.0553616974803, + "gpu_time_us_python": 2667.53793952442, + "gpu_noise_us_cpp": 2.398, + "gpu_noise_us_python": 4.422, + "gpu_bwutil_cpp": 0.12084386877721816, + "gpu_bwutil_python": 0.1199612383175016, + "gpu_gap_stddev_us": 2.660840305127301 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2048.28548278666, + "gpu_time_us_python": 2067.002017889, + "gpu_noise_us_cpp": 1.506, + "gpu_noise_us_python": 4.717999999999999, + "gpu_bwutil_cpp": 0.11917129654018555, + "gpu_bwutil_python": 0.11809160432688037, + "gpu_gap_stddev_us": 1.354198224051665 + } + } + } + }, + "hqresize_planar_expand_nhwc_1080p_basic": { + "tier": "basic", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "interpolation": [ + "CUBIC" + ], + "resizeType": [ + "EXPAND" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Basic HQResize interleaved (NHWC, 3-channel) EXPAND CUBIC 1080p tensor profile; parity partner of the NCHW planar profile." + }, + "baselines": { + "hqresize_planar_expand_nhwc_1080p_basic[InOutDataType=float32][shape=16x1080x1920][interpolation=CUBIC][resizeType=EXPAND][layout=NHWC][inputKind=Tensor][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3047.31888613412, + "gpu_time_us_python": 3065.9234139977, + "gpu_noise_us_cpp": 1.7, + "gpu_noise_us_python": 2.736, + "gpu_bwutil_cpp": 0.42004221552087373, + "gpu_bwutil_python": 0.4174931968379445, + "gpu_gap_stddev_us": 1.6056637934275089 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2548.2366081808, + "gpu_time_us_python": 2565.1044623013, + "gpu_noise_us_cpp": 1.6219999999999999, + "gpu_noise_us_python": 4.9239999999999995, + "gpu_bwutil_cpp": 0.3831206999370189, + "gpu_bwutil_python": 0.3806026613802621, + "gpu_gap_stddev_us": 4.347830600934338 + } + }, + "hqresize_planar_expand_nhwc_1080p_basic[InOutDataType=uint8][shape=16x1080x1920][interpolation=CUBIC][resizeType=EXPAND][layout=NHWC][inputKind=Tensor][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2493.66778255686, + "gpu_time_us_python": 2513.26057195658, + "gpu_noise_us_cpp": 1.544, + "gpu_noise_us_python": 4.822000000000001, + "gpu_bwutil_cpp": 0.12832563592269636, + "gpu_bwutil_python": 0.12732526218189083, + "gpu_gap_stddev_us": 4.466128001589794 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2182.5071425345, + "gpu_time_us_python": 2199.20289227932, + "gpu_noise_us_cpp": 1.236, + "gpu_noise_us_python": 9.77, + "gpu_bwutil_cpp": 0.11183811566274363, + "gpu_bwutil_python": 0.11098724094474696, + "gpu_gap_stddev_us": 6.2459530589976335 + } + } + } + }, + "hqresize_planar_expand_nchw_1080p_basic": { + "tier": "basic", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "interpolation": [ + "CUBIC" + ], + "resizeType": [ + "EXPAND" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Basic HQResize planar (NCHW, 3-channel) EXPAND CUBIC 1080p tensor profile." + }, + "baselines": { + "hqresize_planar_expand_nchw_1080p_basic[InOutDataType=float32][shape=16x1080x1920][interpolation=CUBIC][resizeType=EXPAND][layout=NCHW][inputKind=Tensor][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4446.07869031132, + "gpu_time_us_python": 4466.16541159178, + "gpu_noise_us_cpp": 4.93, + "gpu_noise_us_python": 5.668, + "gpu_bwutil_cpp": 0.28789451295948515, + "gpu_bwutil_python": 0.28659990666208546, + "gpu_gap_stddev_us": 1.5573282405931175 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3725.2046069050602, + "gpu_time_us_python": 3745.07311526044, + "gpu_noise_us_cpp": 1.69, + "gpu_noise_us_python": 4.07, + "gpu_bwutil_cpp": 0.26211262003178143, + "gpu_bwutil_python": 0.2607198311797353, + "gpu_gap_stddev_us": 2.0221328604273023 + } + }, + "hqresize_planar_expand_nchw_1080p_basic[InOutDataType=uint8][shape=16x1080x1920][interpolation=CUBIC][resizeType=EXPAND][layout=NCHW][inputKind=Tensor][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3329.2463796598195, + "gpu_time_us_python": 3348.84315681454, + "gpu_noise_us_cpp": 5.194, + "gpu_noise_us_python": 7.044, + "gpu_bwutil_cpp": 0.096117966943611, + "gpu_bwutil_python": 0.09555558620840074, + "gpu_gap_stddev_us": 2.911368652090593 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2946.29688920642, + "gpu_time_us_python": 2965.0853670844403, + "gpu_noise_us_cpp": 3.632, + "gpu_noise_us_python": 6.068, + "gpu_bwutil_cpp": 0.08285307629419843, + "gpu_bwutil_python": 0.08232671706578805, + "gpu_gap_stddev_us": 2.346042028021148 + } + } + } + }, + "hqresize_planar_expand_nchwfake_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "interpolation": [ + "CUBIC" + ], + "resizeType": [ + "EXPAND" + ], + "layout": [ + "NCHW", + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Advanced HQResize native NCHW and fake-planar NCHW_FAKE comparison for RGB 3-channel EXPAND CUBIC 1080p tensors." + }, + "baselines": { + "hqresize_planar_expand_nchwfake_1080p_advanced[InOutDataType=float32][shape=16x1080x1920][interpolation=CUBIC][resizeType=EXPAND][layout=NCHW_FAKE][inputKind=Tensor][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6109.268935133719, + "gpu_time_us_python": 6133.24053927162, + "gpu_noise_us_cpp": 2.6420000000000003, + "gpu_noise_us_python": 9.124, + "gpu_bwutil_cpp": 0.6285542578531695, + "gpu_bwutil_python": 0.6260976249367192, + "gpu_gap_stddev_us": 8.58903973672077 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4972.6033584744, + "gpu_time_us_python": 4989.97347360496, + "gpu_noise_us_cpp": 2.2380000000000004, + "gpu_noise_us_python": 5.926, + "gpu_bwutil_cpp": 0.5889928372204138, + "gpu_bwutil_python": 0.5869420477849492, + "gpu_gap_stddev_us": 2.1343757064979227 + } + }, + "hqresize_planar_expand_nchwfake_1080p_advanced[InOutDataType=uint8][shape=16x1080x1920][interpolation=CUBIC][resizeType=EXPAND][layout=NCHW_FAKE][inputKind=Tensor][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3659.9510966866997, + "gpu_time_us_python": 3677.9530779581, + "gpu_noise_us_cpp": 3.442, + "gpu_noise_us_python": 4.118, + "gpu_bwutil_cpp": 0.26229990437258127, + "gpu_bwutil_python": 0.26101587896929546, + "gpu_gap_stddev_us": 1.6095758152064252 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3119.3777401505604, + "gpu_time_us_python": 3136.54196915062, + "gpu_noise_us_cpp": 8.056000000000001, + "gpu_noise_us_python": 5.288, + "gpu_bwutil_cpp": 0.2347382926292673, + "gpu_bwutil_python": 0.233454592867374, + "gpu_gap_stddev_us": 3.2002853357759835 + } + }, + "hqresize_planar_expand_nchwfake_1080p_advanced[InOutDataType=float32][shape=16x1080x1920][interpolation=CUBIC][resizeType=EXPAND][layout=NCHW][inputKind=Tensor][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4445.848717605821, + "gpu_time_us_python": 4466.300668632701, + "gpu_noise_us_cpp": 4.167999999999999, + "gpu_noise_us_python": 5.884, + "gpu_bwutil_cpp": 0.2879093200077336, + "gpu_bwutil_python": 0.2865911936908486, + "gpu_gap_stddev_us": 2.209725286689201 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3726.67883356846, + "gpu_time_us_python": 3745.010045592, + "gpu_noise_us_cpp": 2.556, + "gpu_noise_us_python": 3.4200000000000004, + "gpu_bwutil_cpp": 0.26201188502499423, + "gpu_bwutil_python": 0.2607262932706442, + "gpu_gap_stddev_us": 3.1451481472264646 + } + }, + "hqresize_planar_expand_nchwfake_1080p_advanced[InOutDataType=uint8][shape=16x1080x1920][interpolation=CUBIC][resizeType=EXPAND][layout=NCHW][inputKind=Tensor][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3329.8416643811797, + "gpu_time_us_python": 3350.3368263244, + "gpu_noise_us_cpp": 4.886, + "gpu_noise_us_python": 6.912000000000001, + "gpu_bwutil_cpp": 0.09610089500936232, + "gpu_bwutil_python": 0.09551321712604202, + "gpu_gap_stddev_us": 3.7337356099257795 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2945.7219244419603, + "gpu_time_us_python": 2966.41770042842, + "gpu_noise_us_cpp": 3.4619999999999997, + "gpu_noise_us_python": 5.290000000000001, + "gpu_bwutil_cpp": 0.0828690069578505, + "gpu_bwutil_python": 0.08229088312366, + "gpu_gap_stddev_us": 1.5574976123480913 + } + } + } + }, + "hqresize_planar_expand_linear_nhwc_1080p_basic": { + "tier": "basic", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "interpolation": [ + "LINEAR" + ], + "resizeType": [ + "EXPAND" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize RGB 3-channel EXPAND LINEAR 1080p tensor (NHWC)." + }, + "baselines": { + "hqresize_planar_expand_linear_nhwc_1080p_basic[InOutDataType=float32][shape=16x1080x1920][interpolation=LINEAR][resizeType=EXPAND][layout=NHWC][inputKind=Tensor][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1678.4310514664999, + "gpu_time_us_python": 1695.8945788113801, + "gpu_noise_us_cpp": 1.1480000000000001, + "gpu_noise_us_python": 2.658, + "gpu_bwutil_cpp": 0.762620190290974, + "gpu_bwutil_python": 0.7547719620938856, + "gpu_gap_stddev_us": 3.3298462924177836 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1319.53238351512, + "gpu_time_us_python": 1336.7058570194201, + "gpu_noise_us_cpp": 1.3499999999999999, + "gpu_noise_us_python": 3.242, + "gpu_bwutil_cpp": 0.7398987032259368, + "gpu_bwutil_python": 0.7303788007283282, + "gpu_gap_stddev_us": 2.6090363306919175 + } + }, + "hqresize_planar_expand_linear_nhwc_1080p_basic[InOutDataType=uint8][shape=16x1080x1920][interpolation=LINEAR][resizeType=EXPAND][layout=NHWC][inputKind=Tensor][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1891.1512165072804, + "gpu_time_us_python": 1910.1082333998402, + "gpu_noise_us_cpp": 1.8800000000000001, + "gpu_noise_us_python": 4.568, + "gpu_bwutil_cpp": 0.16920967044881277, + "gpu_bwutil_python": 0.16753133289393277, + "gpu_gap_stddev_us": 3.2911542167112833 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1428.5454854283398, + "gpu_time_us_python": 1446.5529352998199, + "gpu_noise_us_cpp": 1.4200000000000002, + "gpu_noise_us_python": 3.6, + "gpu_bwutil_cpp": 0.17085085790824578, + "gpu_bwutil_python": 0.16872401051078698, + "gpu_gap_stddev_us": 2.6426420985884147 + } + } + } + }, + "hqresize_planar_contract_cubic_antialias_nhwc_1080p_basic": { + "tier": "basic", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "interpolation": [ + "CUBIC" + ], + "resizeType": [ + "CONTRACT" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 1 + ], + "numChannels": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize RGB 3-channel CONTRACT CUBIC antialias 1080p tensor (NHWC)." + }, + "baselines": { + "hqresize_planar_contract_cubic_antialias_nhwc_1080p_basic[InOutDataType=float32][shape=64x1080x1920][interpolation=CUBIC][resizeType=CONTRACT][layout=NHWC][inputKind=Tensor][antialias=1][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2939.5219672394005, + "gpu_time_us_python": 2958.4829880209004, + "gpu_noise_us_cpp": 1.7880000000000003, + "gpu_noise_us_python": 3.35, + "gpu_bwutil_cpp": 0.43544537695678587, + "gpu_bwutil_python": 0.432655620609059, + "gpu_gap_stddev_us": 3.190116667412246 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2365.24315100244, + "gpu_time_us_python": 2384.0361968526, + "gpu_noise_us_cpp": 1.796, + "gpu_noise_us_python": 4.29, + "gpu_bwutil_cpp": 0.41276120656039056, + "gpu_bwutil_python": 0.4095071484196772, + "gpu_gap_stddev_us": 2.3118922034049585 + } + }, + "hqresize_planar_contract_cubic_antialias_nhwc_1080p_basic[InOutDataType=uint8][shape=64x1080x1920][interpolation=CUBIC][resizeType=CONTRACT][layout=NHWC][inputKind=Tensor][antialias=1][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2438.51579277258, + "gpu_time_us_python": 2457.3079192159, + "gpu_noise_us_cpp": 1.434, + "gpu_noise_us_python": 3.5159999999999996, + "gpu_bwutil_cpp": 0.13122811874264748, + "gpu_bwutil_python": 0.1302251310467019, + "gpu_gap_stddev_us": 2.89071838070528 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2263.4010516513, + "gpu_time_us_python": 2279.33618144928, + "gpu_noise_us_cpp": 3.4899999999999998, + "gpu_noise_us_python": 6.890000000000001, + "gpu_bwutil_cpp": 0.1078387015811155, + "gpu_bwutil_python": 0.10708377548913432, + "gpu_gap_stddev_us": 5.618383131733691 + } + } + } + }, + "hqresize_planar_contract_cubic_varshape_nhwc_1080p_basic": { + "tier": "basic", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "interpolation": [ + "CUBIC" + ], + "resizeType": [ + "CONTRACT" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize RGB8 CONTRACT CUBIC 1080p ImageBatchVarShape (NHWC)." + }, + "baselines": { + "hqresize_planar_contract_cubic_varshape_nhwc_1080p_basic[InOutDataType=uint8][shape=64x1080x1920][interpolation=CUBIC][resizeType=CONTRACT][layout=NHWC][inputKind=VarShape][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2335.89794751838, + "gpu_time_us_python": 2385.90360625812, + "gpu_noise_us_cpp": 16.808, + "gpu_noise_us_python": 15.154000000000002, + "gpu_bwutil_cpp": 0.136993833904591, + "gpu_bwutil_python": 0.1341235371834818, + "gpu_gap_stddev_us": 16.89567784552635 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1726.1501703713202, + "gpu_time_us_python": 1778.3285997889402, + "gpu_noise_us_cpp": 13.344, + "gpu_noise_us_python": 13.912, + "gpu_bwutil_cpp": 0.1414109750495977, + "gpu_bwutil_python": 0.13726270395964607, + "gpu_gap_stddev_us": 9.340033958165527 + } + } + } + }, + "hqresize_planar_contract_cubic_tensorbatch_nhwc_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "interpolation": [ + "CUBIC" + ], + "resizeType": [ + "CONTRACT" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "TensorBatch" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize RGB8 CONTRACT CUBIC 1080p TensorBatch (NHWC, advanced)." + }, + "baselines": { + "hqresize_planar_contract_cubic_tensorbatch_nhwc_1080p_advanced[InOutDataType=uint8][shape=64x1080x1920][interpolation=CUBIC][resizeType=CONTRACT][layout=NHWC][inputKind=TensorBatch][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2347.44138261146, + "gpu_time_us_python": 2403.07866553466, + "gpu_noise_us_cpp": 16.473999999999997, + "gpu_noise_us_python": 14.157999999999998, + "gpu_bwutil_cpp": 0.13632002325361064, + "gpu_bwutil_python": 0.13316542094321193, + "gpu_gap_stddev_us": 13.208973050566353 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1746.6455230685401, + "gpu_time_us_python": 1783.18855690034, + "gpu_noise_us_cpp": 12.768, + "gpu_noise_us_python": 13.552000000000001, + "gpu_bwutil_cpp": 0.1397601878739735, + "gpu_bwutil_python": 0.1368836529584833, + "gpu_gap_stddev_us": 11.26329701777928 + } + } + } + }, + "hqresize_planar_expand_linear_nchw_1080p_basic": { + "tier": "basic", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "interpolation": [ + "LINEAR" + ], + "resizeType": [ + "EXPAND" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize RGB 3-channel EXPAND LINEAR 1080p tensor (NCHW)." + }, + "baselines": { + "hqresize_planar_expand_linear_nchw_1080p_basic[InOutDataType=float32][shape=16x1080x1920][interpolation=LINEAR][resizeType=EXPAND][layout=NCHW][inputKind=Tensor][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1625.4523768053, + "gpu_time_us_python": 1642.8569466459398, + "gpu_noise_us_cpp": 5.722, + "gpu_noise_us_python": 6.346, + "gpu_bwutil_cpp": 0.7874736000126477, + "gpu_bwutil_python": 0.7791336352667715, + "gpu_gap_stddev_us": 2.406507475787795 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1244.7433395524, + "gpu_time_us_python": 1259.0830959398602, + "gpu_noise_us_cpp": 2.5500000000000003, + "gpu_noise_us_python": 2.87, + "gpu_bwutil_cpp": 0.7843163231621888, + "gpu_bwutil_python": 0.7753848118752957, + "gpu_gap_stddev_us": 0.7669640608455554 + } + }, + "hqresize_planar_expand_linear_nchw_1080p_basic[InOutDataType=uint8][shape=16x1080x1920][interpolation=LINEAR][resizeType=EXPAND][layout=NCHW][inputKind=Tensor][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 671.98655379612, + "gpu_time_us_python": 689.19301531104, + "gpu_noise_us_cpp": 1.564, + "gpu_noise_us_python": 3.322, + "gpu_bwutil_cpp": 0.4762062383859133, + "gpu_bwutil_python": 0.4643371571396173, + "gpu_gap_stddev_us": 3.410193797857394 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 615.02978079774, + "gpu_time_us_python": 631.71946583758, + "gpu_noise_us_cpp": 1.302, + "gpu_noise_us_python": 2.6919999999999997, + "gpu_bwutil_cpp": 0.39689364431647556, + "gpu_bwutil_python": 0.38639501419170197, + "gpu_gap_stddev_us": 1.8333297472633774 + } + } + } + }, + "hqresize_planar_contract_cubic_antialias_nchw_1080p_basic": { + "tier": "basic", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "interpolation": [ + "CUBIC" + ], + "resizeType": [ + "CONTRACT" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 1 + ], + "numChannels": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize RGB 3-channel CONTRACT CUBIC antialias 1080p tensor (NCHW)." + }, + "baselines": { + "hqresize_planar_contract_cubic_antialias_nchw_1080p_basic[InOutDataType=float32][shape=64x1080x1920][interpolation=CUBIC][resizeType=CONTRACT][layout=NCHW][inputKind=Tensor][antialias=1][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4472.2843055139, + "gpu_time_us_python": 4490.719093595179, + "gpu_noise_us_cpp": 1.5240000000000002, + "gpu_noise_us_python": 5.116, + "gpu_bwutil_cpp": 0.2862090757594054, + "gpu_bwutil_python": 0.2850341599854894, + "gpu_gap_stddev_us": 4.2160893313734915 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3596.00785021436, + "gpu_time_us_python": 3613.2307477375603, + "gpu_noise_us_cpp": 1.966, + "gpu_noise_us_python": 7.736, + "gpu_bwutil_cpp": 0.2715276319863369, + "gpu_bwutil_python": 0.2702314157222776, + "gpu_gap_stddev_us": 3.430237918613386 + } + }, + "hqresize_planar_contract_cubic_antialias_nchw_1080p_basic[InOutDataType=uint8][shape=64x1080x1920][interpolation=CUBIC][resizeType=CONTRACT][layout=NCHW][inputKind=Tensor][antialias=1][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3477.5200230798996, + "gpu_time_us_python": 3495.9672974215, + "gpu_noise_us_cpp": 1.588, + "gpu_noise_us_python": 3.086, + "gpu_bwutil_cpp": 0.09202027962503508, + "gpu_bwutil_python": 0.09153477524135405, + "gpu_gap_stddev_us": 1.3958054435720133 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3077.77617918524, + "gpu_time_us_python": 3097.2958784631, + "gpu_noise_us_cpp": 1.9120000000000001, + "gpu_noise_us_python": 4.851999999999999, + "gpu_bwutil_cpp": 0.07931241469495989, + "gpu_bwutil_python": 0.07881330602641526, + "gpu_gap_stddev_us": 2.0474341791806236 + } + } + } + }, + "hqresize_planar_contract_cubic_varshape_nchw_1080p_basic": { + "tier": "basic", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "interpolation": [ + "CUBIC" + ], + "resizeType": [ + "CONTRACT" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "VarShape" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize RGB8 CONTRACT CUBIC 1080p ImageBatchVarShape (NCHW)." + }, + "baselines": { + "hqresize_planar_contract_cubic_varshape_nchw_1080p_basic[InOutDataType=uint8][shape=64x1080x1920][interpolation=CUBIC][resizeType=CONTRACT][layout=NCHW][inputKind=VarShape][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2812.7674369618594, + "gpu_time_us_python": 2882.7576893150404, + "gpu_noise_us_cpp": 29.422000000000004, + "gpu_noise_us_python": 24.486, + "gpu_bwutil_cpp": 0.11376975387145247, + "gpu_bwutil_python": 0.11101450710669152, + "gpu_gap_stddev_us": 23.76709123994603 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2599.5206114305397, + "gpu_time_us_python": 2650.88180772308, + "gpu_noise_us_cpp": 20.742, + "gpu_noise_us_python": 27.988000000000007, + "gpu_bwutil_cpp": 0.0939048570750662, + "gpu_bwutil_python": 0.09209611408853273, + "gpu_gap_stddev_us": 15.550446499361739 + } + } + } + }, + "hqresize_planar_contract_cubic_tensorbatch_nchw_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "interpolation": [ + "CUBIC" + ], + "resizeType": [ + "CONTRACT" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "TensorBatch" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize RGB8 CONTRACT CUBIC 1080p TensorBatch (NCHW, advanced)." + }, + "baselines": { + "hqresize_planar_contract_cubic_tensorbatch_nchw_1080p_advanced[InOutDataType=uint8][shape=64x1080x1920][interpolation=CUBIC][resizeType=CONTRACT][layout=NCHW][inputKind=TensorBatch][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2956.81886335098, + "gpu_time_us_python": 3026.1288574281402, + "gpu_noise_us_cpp": 40.291999999999994, + "gpu_noise_us_python": 42.806, + "gpu_bwutil_cpp": 0.1082669636977599, + "gpu_bwutil_python": 0.10578981852600033, + "gpu_gap_stddev_us": 21.738741041224305 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2706.7483788191594, + "gpu_time_us_python": 2779.4402361018397, + "gpu_noise_us_cpp": 51.662, + "gpu_noise_us_python": 45.588, + "gpu_bwutil_cpp": 0.09020579144728288, + "gpu_bwutil_python": 0.08783931168043833, + "gpu_gap_stddev_us": 15.206618613898767 + } + } + } + }, + "hqresize_anisotropic_contract_480p_tensor_rgb8_basic": { + "tier": "basic", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "40x1080x1920" + ], + "interpolation": [ + "CUBIC" + ], + "resizeType": [ + "TARGET_480x864" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 1 + ], + "numChannels": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize RGB8 tensor anisotropically from 1920x1080 to 864x480." + }, + "baselines": { + "hqresize_anisotropic_contract_480p_tensor_rgb8_basic[InOutDataType=uint8][shape=40x1080x1920][interpolation=CUBIC][resizeType=TARGET_480x864][layout=NHWC][inputKind=Tensor][antialias=1][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1461.8847068808598, + "gpu_time_us_python": 1479.82286823856, + "gpu_noise_us_cpp": 1.6460000000000001, + "gpu_noise_us_python": 4.244000000000001, + "gpu_bwutil_cpp": 0.131338160799273, + "gpu_bwutil_python": 0.12974675513350328, + "gpu_gap_stddev_us": 2.265963675376293 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1393.6504703116202, + "gpu_time_us_python": 1413.96357942614, + "gpu_noise_us_cpp": 3.572, + "gpu_noise_us_python": 3.368, + "gpu_bwutil_cpp": 0.10508147957906418, + "gpu_bwutil_python": 0.10357003261545313, + "gpu_gap_stddev_us": 3.953367223279786 + } + } + } + }, + "hqresize_anisotropic_contract_480p_varshape_rgb8_basic": { + "tier": "basic", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "40x1080x1920" + ], + "interpolation": [ + "CUBIC" + ], + "resizeType": [ + "TARGET_480x864" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "int64_axes": { + "antialias": [ + 1 + ], + "numChannels": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize RGB8 ImageBatchVarShape anisotropically from 1920x1080 to 864x480." + }, + "baselines": { + "hqresize_anisotropic_contract_480p_varshape_rgb8_basic[InOutDataType=uint8][shape=40x1080x1920][interpolation=CUBIC][resizeType=TARGET_480x864][layout=NHWC][inputKind=VarShape][antialias=1][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1978.4797553562203, + "gpu_time_us_python": 2022.9336418428, + "gpu_noise_us_cpp": 10.556000000000001, + "gpu_noise_us_python": 9.108, + "gpu_bwutil_cpp": 0.09704475248960727, + "gpu_bwutil_python": 0.0949130547343012, + "gpu_gap_stddev_us": 4.520690707401093 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1385.1916671470199, + "gpu_time_us_python": 1414.8289414347, + "gpu_noise_us_cpp": 5.998, + "gpu_noise_us_python": 11.386, + "gpu_bwutil_cpp": 0.10572941746930205, + "gpu_bwutil_python": 0.1035052113175795, + "gpu_gap_stddev_us": 15.103657466336495 + } + } + } + }, + "hqresize_anisotropic_expand_4k_tensor_rgb8_basic": { + "tier": "basic", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "16x480x864" + ], + "interpolation": [ + "CUBIC" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize RGB8 tensor anisotropically from 864x480 to 3840x2160." + }, + "baselines": { + "hqresize_anisotropic_expand_4k_tensor_rgb8_basic[InOutDataType=uint8][shape=16x480x864][interpolation=CUBIC][resizeType=TARGET_2160x3840][layout=NHWC][inputKind=Tensor][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1864.5784337893197, + "gpu_time_us_python": 1882.39875487928, + "gpu_noise_us_cpp": 2.5060000000000002, + "gpu_noise_us_python": 3.368, + "gpu_bwutil_cpp": 0.1441623751931817, + "gpu_bwutil_python": 0.14279788409352345, + "gpu_gap_stddev_us": 2.1263812182769364 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1857.67457762286, + "gpu_time_us_python": 1873.49518972722, + "gpu_noise_us_cpp": 1.4540000000000002, + "gpu_noise_us_python": 5.814, + "gpu_bwutil_cpp": 0.11036974883057019, + "gpu_bwutil_python": 0.10943710399844886, + "gpu_gap_stddev_us": 5.145520504585068 + } + } + } + }, + "hqresize_anisotropic_expand_4k_varshape_rgb8_basic": { + "tier": "basic", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "16x480x864" + ], + "interpolation": [ + "CUBIC" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize RGB8 ImageBatchVarShape anisotropically from 864x480 to 3840x2160." + }, + "baselines": { + "hqresize_anisotropic_expand_4k_varshape_rgb8_basic[InOutDataType=uint8][shape=16x480x864][interpolation=CUBIC][resizeType=TARGET_2160x3840][layout=NHWC][inputKind=VarShape][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2002.30096237844, + "gpu_time_us_python": 2031.08345964836, + "gpu_noise_us_cpp": 5.2139999999999995, + "gpu_noise_us_python": 10.054, + "gpu_bwutil_cpp": 0.1342461272416978, + "gpu_bwutil_python": 0.13234556765603964, + "gpu_gap_stddev_us": 5.772901227353972 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1683.91914261134, + "gpu_time_us_python": 1709.28258738642, + "gpu_noise_us_cpp": 6.33, + "gpu_noise_us_python": 11.222000000000001, + "gpu_bwutil_cpp": 0.12176038630516992, + "gpu_bwutil_python": 0.11995490217058631, + "gpu_gap_stddev_us": 5.289530329400232 + } + } + } + }, + "hqresize_anisotropic_contract_480x864_nchw_tensor_cubic_float32_c3_aa0_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "40x1080x1920" + ], + "interpolation": [ + "CUBIC" + ], + "resizeType": [ + "TARGET_480x864" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize float32 3-channel Tensor (NCHW) anisotropically from 1920x1080 to 864x480 with CUBIC, antialias=0. Advanced normalized mirror of hqresize_planar_contract_nchw_1080p_basic; batch 64 scaled conservatively to 40." + }, + "baselines": { + "hqresize_anisotropic_contract_480x864_nchw_tensor_cubic_float32_c3_aa0_advanced[InOutDataType=float32][shape=40x1080x1920][interpolation=CUBIC][resizeType=TARGET_480x864][layout=NCHW][inputKind=Tensor][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1476.83179221744, + "gpu_time_us_python": 1495.00128158152, + "gpu_noise_us_cpp": 1.686, + "gpu_noise_us_python": 4.324, + "gpu_bwutil_cpp": 0.5200372697795671, + "gpu_bwutil_python": 0.5137198406732713, + "gpu_gap_stddev_us": 2.798350372702734 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1436.7525962505802, + "gpu_time_us_python": 1456.3066926567599, + "gpu_noise_us_cpp": 2.528, + "gpu_noise_us_python": 3.0060000000000002, + "gpu_bwutil_cpp": 0.4077588595409482, + "gpu_bwutil_python": 0.40226247901710305, + "gpu_gap_stddev_us": 3.7167735149146606 + } + } + } + }, + "hqresize_anisotropic_contract_480x864_nchw_tensor_cubic_float32_c3_aa1_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "40x1080x1920" + ], + "interpolation": [ + "CUBIC" + ], + "resizeType": [ + "TARGET_480x864" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 1 + ], + "numChannels": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize float32 3-channel Tensor (NCHW) anisotropically from 1920x1080 to 864x480 with CUBIC, antialias=1. Advanced normalized mirror of hqresize_planar_contract_cubic_antialias_nchw_1080p_basic; batch 64 scaled conservatively to 40." + }, + "baselines": { + "hqresize_anisotropic_contract_480x864_nchw_tensor_cubic_float32_c3_aa1_advanced[InOutDataType=float32][shape=40x1080x1920][interpolation=CUBIC][resizeType=TARGET_480x864][layout=NCHW][inputKind=Tensor][antialias=1][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2737.07631670904, + "gpu_time_us_python": 2755.94547496018, + "gpu_noise_us_cpp": 1.5419999999999998, + "gpu_noise_us_python": 6.616, + "gpu_bwutil_cpp": 0.2805933521437352, + "gpu_bwutil_python": 0.27867307789272366, + "gpu_gap_stddev_us": 1.9360950987624688 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2203.18587319964, + "gpu_time_us_python": 2221.48619302306, + "gpu_noise_us_cpp": 2.0, + "gpu_noise_us_python": 3.6060000000000003, + "gpu_bwutil_cpp": 0.26591007448362874, + "gpu_bwutil_python": 0.2637179646791646, + "gpu_gap_stddev_us": 1.451398335327078 + } + } + } + }, + "hqresize_anisotropic_contract_480x864_nchwfake_tensor_cubic_float32_c3_aa0_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "40x1080x1920" + ], + "interpolation": [ + "CUBIC" + ], + "resizeType": [ + "TARGET_480x864" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize float32 3-channel Tensor (NCHW_FAKE) anisotropically from 1920x1080 to 864x480 with CUBIC, antialias=0. Advanced normalized mirror of hqresize_planar_contract_nchwfake_1080p_advanced; batch 64 scaled conservatively to 40." + }, + "baselines": { + "hqresize_anisotropic_contract_480x864_nchwfake_tensor_cubic_float32_c3_aa0_advanced[InOutDataType=float32][shape=40x1080x1920][interpolation=CUBIC][resizeType=TARGET_480x864][layout=NCHW_FAKE][inputKind=Tensor][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3287.46526900492, + "gpu_time_us_python": 3295.1495908174597, + "gpu_noise_us_cpp": 5.648000000000001, + "gpu_noise_us_python": 8.174000000000001, + "gpu_bwutil_cpp": 0.7008514929127438, + "gpu_bwutil_python": 0.6992186024219255, + "gpu_gap_stddev_us": 1.8558930516269467 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2668.71325494988, + "gpu_time_us_python": 2685.5511887507596, + "gpu_noise_us_cpp": 3.7960000000000003, + "gpu_noise_us_python": 4.180000000000001, + "gpu_bwutil_cpp": 0.6584851984812408, + "gpu_bwutil_python": 0.6543527089995205, + "gpu_gap_stddev_us": 4.319243232661235 + } + } + } + }, + "hqresize_anisotropic_contract_480x864_nhwc_tensor_cubic_float32_c1_aa0_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "40x1080x1920" + ], + "interpolation": [ + "CUBIC" + ], + "resizeType": [ + "TARGET_480x864" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize float32 1-channel Tensor (NHWC) anisotropically from 1920x1080 to 864x480 with CUBIC, antialias=0. Advanced normalized mirror of hqresize_contract_advanced; batch 64 scaled conservatively to 40." + }, + "baselines": { + "hqresize_anisotropic_contract_480x864_nhwc_tensor_cubic_float32_c1_aa0_advanced[InOutDataType=float32][shape=40x1080x1920][interpolation=CUBIC][resizeType=TARGET_480x864][layout=NHWC][inputKind=Tensor][antialias=0][numChannels=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 501.05466625162006, + "gpu_time_us_python": 518.01110303122, + "gpu_noise_us_cpp": 1.9739999999999998, + "gpu_noise_us_python": 3.0580000000000003, + "gpu_bwutil_cpp": 0.5109273803662655, + "gpu_bwutil_python": 0.4942261083639914, + "gpu_gap_stddev_us": 2.8416263303410667 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 487.04637121848, + "gpu_time_us_python": 502.78181154979995, + "gpu_noise_us_cpp": 0.748, + "gpu_noise_us_python": 2.9739999999999998, + "gpu_bwutil_cpp": 0.4009462055801444, + "gpu_bwutil_python": 0.3883864599413387, + "gpu_gap_stddev_us": 1.4750309539919497 + } + } + } + }, + "hqresize_anisotropic_contract_480x864_nhwc_tensor_cubic_float32_c3_aa0_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "40x1080x1920" + ], + "interpolation": [ + "CUBIC" + ], + "resizeType": [ + "TARGET_480x864" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize float32 3-channel Tensor (NHWC) anisotropically from 1920x1080 to 864x480 with CUBIC, antialias=0. Advanced normalized mirror of hqresize_planar_contract_nhwc_1080p_basic; batch 64 scaled conservatively to 40." + }, + "baselines": { + "hqresize_anisotropic_contract_480x864_nhwc_tensor_cubic_float32_c3_aa0_advanced[InOutDataType=float32][shape=40x1080x1920][interpolation=CUBIC][resizeType=TARGET_480x864][layout=NHWC][inputKind=Tensor][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1097.9804888751, + "gpu_time_us_python": 1105.75734631672, + "gpu_noise_us_cpp": 6.16, + "gpu_noise_us_python": 6.38, + "gpu_bwutil_cpp": 0.6994688115900101, + "gpu_bwutil_python": 0.6945498748798112, + "gpu_gap_stddev_us": 1.3271579643394626 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 811.6298399987601, + "gpu_time_us_python": 828.03633867516, + "gpu_noise_us_cpp": 3.8280000000000003, + "gpu_noise_us_python": 5.1, + "gpu_bwutil_cpp": 0.7217258717356605, + "gpu_bwutil_python": 0.7074280285347923, + "gpu_gap_stddev_us": 3.4791862339022295 + } + } + } + }, + "hqresize_anisotropic_contract_480x864_nhwc_tensor_cubic_float32_c1_aa1_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "40x1080x1920" + ], + "interpolation": [ + "CUBIC" + ], + "resizeType": [ + "TARGET_480x864" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 1 + ], + "numChannels": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize float32 1-channel Tensor (NHWC) anisotropically from 1920x1080 to 864x480 with CUBIC, antialias=1. Advanced normalized mirror of hqresize_antialias_advanced; batch 64 scaled conservatively to 40." + }, + "baselines": { + "hqresize_anisotropic_contract_480x864_nhwc_tensor_cubic_float32_c1_aa1_advanced[InOutDataType=float32][shape=40x1080x1920][interpolation=CUBIC][resizeType=TARGET_480x864][layout=NHWC][inputKind=Tensor][antialias=1][numChannels=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 935.8474896948601, + "gpu_time_us_python": 951.9996074594401, + "gpu_noise_us_cpp": 1.8799999999999997, + "gpu_noise_us_python": 3.842, + "gpu_bwutil_cpp": 0.2735522629841646, + "gpu_bwutil_python": 0.2689111470856116, + "gpu_gap_stddev_us": 1.910379768162713 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 756.94969382152, + "gpu_time_us_python": 773.5633849097601, + "gpu_noise_us_cpp": 1.9620000000000002, + "gpu_noise_us_python": 4.635999999999999, + "gpu_bwutil_cpp": 0.2579825403654775, + "gpu_bwutil_python": 0.25243111026263965, + "gpu_gap_stddev_us": 2.6967191902519976 + } + } + } + }, + "hqresize_anisotropic_contract_480x864_nhwc_tensor_cubic_float32_c3_aa1_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "40x1080x1920" + ], + "interpolation": [ + "CUBIC" + ], + "resizeType": [ + "TARGET_480x864" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 1 + ], + "numChannels": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize float32 3-channel Tensor (NHWC) anisotropically from 1920x1080 to 864x480 with CUBIC, antialias=1. Advanced normalized mirror of hqresize_planar_contract_cubic_antialias_nhwc_1080p_basic; batch 64 scaled conservatively to 40." + }, + "baselines": { + "hqresize_anisotropic_contract_480x864_nhwc_tensor_cubic_float32_c3_aa1_advanced[InOutDataType=float32][shape=40x1080x1920][interpolation=CUBIC][resizeType=TARGET_480x864][layout=NHWC][inputKind=Tensor][antialias=1][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1717.0039044667399, + "gpu_time_us_python": 1737.6012257837203, + "gpu_noise_us_cpp": 1.6580000000000001, + "gpu_noise_us_python": 5.958, + "gpu_bwutil_cpp": 0.4472914615075383, + "gpu_bwutil_python": 0.4419918102757848, + "gpu_gap_stddev_us": 3.524219464564374 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1442.3377911293999, + "gpu_time_us_python": 1458.7930870734601, + "gpu_noise_us_cpp": 2.332, + "gpu_noise_us_python": 3.614, + "gpu_bwutil_cpp": 0.40612711762389947, + "gpu_bwutil_python": 0.4015445340146668, + "gpu_gap_stddev_us": 1.0585577545542075 + } + } + } + }, + "hqresize_anisotropic_contract_480x864_nchw_tensor_linear_float32_c3_aa0_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "40x1080x1920" + ], + "interpolation": [ + "LINEAR" + ], + "resizeType": [ + "TARGET_480x864" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize float32 3-channel Tensor (NCHW) anisotropically from 1920x1080 to 864x480 with LINEAR, antialias=0. Advanced normalized mirror of hqresize_planar_contract_linear_nchw_1080p_basic; batch 64 scaled conservatively to 40." + }, + "baselines": { + "hqresize_anisotropic_contract_480x864_nchw_tensor_linear_float32_c3_aa0_advanced[InOutDataType=float32][shape=40x1080x1920][interpolation=LINEAR][resizeType=TARGET_480x864][layout=NCHW][inputKind=Tensor][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 816.04928438356, + "gpu_time_us_python": 832.0383276897999, + "gpu_noise_us_cpp": 2.234, + "gpu_noise_us_python": 3.75, + "gpu_bwutil_cpp": 0.9411199916855626, + "gpu_bwutil_python": 0.9230367214710717, + "gpu_gap_stddev_us": 1.2041628985837671 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 635.62448171956, + "gpu_time_us_python": 650.4310820897799, + "gpu_noise_us_cpp": 1.516, + "gpu_noise_us_python": 2.872, + "gpu_bwutil_cpp": 0.9215569828936123, + "gpu_bwutil_python": 0.9005809908439545, + "gpu_gap_stddev_us": 0.7917119113398211 + } + } + } + }, + "hqresize_anisotropic_contract_480x864_nchwfake_tensor_linear_float32_c3_aa0_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "40x1080x1920" + ], + "interpolation": [ + "LINEAR" + ], + "resizeType": [ + "TARGET_480x864" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize float32 3-channel Tensor (NCHW_FAKE) anisotropically from 1920x1080 to 864x480 with LINEAR, antialias=0. Advanced normalized mirror of hqresize_planar_contract_linear_nchwfake_1080p_advanced; batch 64 scaled conservatively to 40." + }, + "baselines": { + "hqresize_anisotropic_contract_480x864_nchwfake_tensor_linear_float32_c3_aa0_advanced[InOutDataType=float32][shape=40x1080x1920][interpolation=LINEAR][resizeType=TARGET_480x864][layout=NCHW_FAKE][inputKind=Tensor][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2986.19577524036, + "gpu_time_us_python": 3002.5465597315197, + "gpu_noise_us_cpp": 1.8579999999999999, + "gpu_noise_us_python": 2.528, + "gpu_bwutil_cpp": 0.7715578301829776, + "gpu_bwutil_python": 0.7673579205603145, + "gpu_gap_stddev_us": 2.6807119490865596 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2456.7538214664196, + "gpu_time_us_python": 2472.68426629362, + "gpu_noise_us_cpp": 2.0140000000000002, + "gpu_noise_us_python": 3.0380000000000003, + "gpu_bwutil_cpp": 0.7152912408001787, + "gpu_bwutil_python": 0.710681750532826, + "gpu_gap_stddev_us": 2.26778369874941 + } + } + } + }, + "hqresize_anisotropic_contract_480x864_nhwc_tensor_linear_float32_c1_aa0_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "140x1080x1920" + ], + "interpolation": [ + "LINEAR" + ], + "resizeType": [ + "TARGET_480x864" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize float32 1-channel Tensor (NHWC) anisotropically from 1920x1080 to 864x480 with LINEAR, antialias=0. Advanced normalized mirror of hqresize_contract_linear_1080p_advanced; batch 140 calibrated for approximately 1 ms reference timing." + }, + "baselines": { + "hqresize_anisotropic_contract_480x864_nhwc_tensor_linear_float32_c1_aa0_advanced[InOutDataType=float32][shape=140x1080x1920][interpolation=LINEAR][resizeType=TARGET_480x864][layout=NHWC][inputKind=Tensor][antialias=0][numChannels=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 947.56693402018, + "gpu_time_us_python": 963.5136070143599, + "gpu_noise_us_cpp": 2.9699999999999998, + "gpu_noise_us_python": 2.892, + "gpu_bwutil_cpp": 0.9455800606022169, + "gpu_bwutil_python": 0.92993383434547, + "gpu_gap_stddev_us": 1.589160218179311 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 737.61224773042, + "gpu_time_us_python": 752.31587027978, + "gpu_noise_us_cpp": 1.358, + "gpu_noise_us_python": 2.744, + "gpu_bwutil_cpp": 0.9264899356733538, + "gpu_bwutil_python": 0.9083875641865365, + "gpu_gap_stddev_us": 1.7190698821824681 + } + } + } + }, + "hqresize_anisotropic_contract_480x864_nhwc_tensor_linear_float32_c3_aa0_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "40x1080x1920" + ], + "interpolation": [ + "LINEAR" + ], + "resizeType": [ + "TARGET_480x864" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize float32 3-channel Tensor (NHWC) anisotropically from 1920x1080 to 864x480 with LINEAR, antialias=0. Advanced normalized mirror of hqresize_planar_contract_linear_nhwc_1080p_basic; batch 64 scaled conservatively to 40." + }, + "baselines": { + "hqresize_anisotropic_contract_480x864_nhwc_tensor_linear_float32_c3_aa0_advanced[InOutDataType=float32][shape=40x1080x1920][interpolation=LINEAR][resizeType=TARGET_480x864][layout=NHWC][inputKind=Tensor][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 796.99027697748, + "gpu_time_us_python": 813.69393216914, + "gpu_noise_us_cpp": 2.0020000000000002, + "gpu_noise_us_python": 3.807999999999999, + "gpu_bwutil_cpp": 0.9636261415919497, + "gpu_bwutil_python": 0.9438515857455411, + "gpu_gap_stddev_us": 1.9110816846502006 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 605.7457342698999, + "gpu_time_us_python": 620.6238233240799, + "gpu_noise_us_cpp": 1.2739999999999998, + "gpu_noise_us_python": 2.478, + "gpu_bwutil_cpp": 0.9670126913763702, + "gpu_bwutil_python": 0.9438373036873985, + "gpu_gap_stddev_us": 1.220229808774056 + } + } + } + }, + "hqresize_anisotropic_contract_480x864_nhwc_tensor_linear_float32_c1_aa1_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "52x1080x1920" + ], + "interpolation": [ + "LINEAR" + ], + "resizeType": [ + "TARGET_480x864" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 1 + ], + "numChannels": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize float32 1-channel Tensor (NHWC) anisotropically from 1920x1080 to 864x480 with LINEAR, antialias=1. Advanced normalized mirror of hqresize_contract_linear_antialias_1080p_advanced; batch 52 calibrated for approximately 1 ms reference timing." + }, + "baselines": { + "hqresize_anisotropic_contract_480x864_nhwc_tensor_linear_float32_c1_aa1_advanced[InOutDataType=float32][shape=52x1080x1920][interpolation=LINEAR][resizeType=TARGET_480x864][layout=NHWC][inputKind=Tensor][antialias=1][numChannels=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 927.3223492411801, + "gpu_time_us_python": 943.77097720196, + "gpu_noise_us_cpp": 1.088, + "gpu_noise_us_python": 3.4, + "gpu_bwutil_cpp": 0.3588839467704307, + "gpu_bwutil_python": 0.352628788628656, + "gpu_gap_stddev_us": 1.5301765024406047 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 691.6107928978, + "gpu_time_us_python": 708.2409132774999, + "gpu_noise_us_cpp": 2.714, + "gpu_noise_us_python": 3.9859999999999998, + "gpu_bwutil_cpp": 0.3670393723674164, + "gpu_bwutil_python": 0.35842023247276356, + "gpu_gap_stddev_us": 3.2662843929874246 + } + } + } + }, + "hqresize_anisotropic_expand_2160x3840_nchw_tensor_cubic_float32_c3_aa0_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "16x480x864" + ], + "interpolation": [ + "CUBIC" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize float32 3-channel Tensor (NCHW) anisotropically from 864x480 to 3840x2160 with CUBIC, antialias=0. Advanced normalized mirror of hqresize_planar_expand_nchw_1080p_basic; batch 16 scaled conservatively to 16." + }, + "baselines": { + "hqresize_anisotropic_expand_2160x3840_nchw_tensor_cubic_float32_c3_aa0_advanced[InOutDataType=float32][shape=16x480x864][interpolation=CUBIC][resizeType=TARGET_2160x3840][layout=NCHW][inputKind=Tensor][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3751.180990418339, + "gpu_time_us_python": 3770.1662689891796, + "gpu_noise_us_cpp": 2.286, + "gpu_noise_us_python": 3.7299999999999995, + "gpu_bwutil_cpp": 0.2866312296630572, + "gpu_bwutil_python": 0.2851877121121805, + "gpu_gap_stddev_us": 2.678900259103877 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3290.90463664684, + "gpu_time_us_python": 3307.9353142962805, + "gpu_noise_us_cpp": 1.7939999999999998, + "gpu_noise_us_python": 9.645999999999999, + "gpu_bwutil_cpp": 0.2492298627476517, + "gpu_bwutil_python": 0.24794313966043383, + "gpu_gap_stddev_us": 6.907914704334227 + } + } + } + }, + "hqresize_anisotropic_expand_2160x3840_nchwfake_tensor_cubic_float32_c3_aa0_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "16x480x864" + ], + "interpolation": [ + "CUBIC" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize float32 3-channel Tensor (NCHW_FAKE) anisotropically from 864x480 to 3840x2160 with CUBIC, antialias=0. Advanced normalized mirror of hqresize_planar_expand_nchwfake_1080p_advanced; batch 16 scaled conservatively to 16." + }, + "baselines": { + "hqresize_anisotropic_expand_2160x3840_nchwfake_tensor_cubic_float32_c3_aa0_advanced[InOutDataType=float32][shape=16x480x864][interpolation=CUBIC][resizeType=TARGET_2160x3840][layout=NCHW_FAKE][inputKind=Tensor][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4603.15552971574, + "gpu_time_us_python": 4620.87263107296, + "gpu_noise_us_cpp": 2.844, + "gpu_noise_us_python": 3.5520000000000005, + "gpu_bwutil_cpp": 0.7007370195977147, + "gpu_bwutil_python": 0.6980502971582323, + "gpu_gap_stddev_us": 2.348639734778036 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3968.1408266580793, + "gpu_time_us_python": 3987.5633792271196, + "gpu_noise_us_cpp": 2.7380000000000004, + "gpu_noise_us_python": 4.566, + "gpu_bwutil_cpp": 0.619995651743082, + "gpu_bwutil_python": 0.616975754733022, + "gpu_gap_stddev_us": 2.149458177900448 + } + } + } + }, + "hqresize_anisotropic_expand_2160x3840_nhwc_tensor_cubic_float32_c1_aa0_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "32x480x864" + ], + "interpolation": [ + "CUBIC" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize float32 1-channel Tensor (NHWC) anisotropically from 864x480 to 3840x2160 with CUBIC, antialias=0. Advanced normalized mirror of hqresize_expand_advanced; batch 32 scaled conservatively to 32." + }, + "baselines": { + "hqresize_anisotropic_expand_2160x3840_nhwc_tensor_cubic_float32_c1_aa0_advanced[InOutDataType=float32][shape=32x480x864][interpolation=CUBIC][resizeType=TARGET_2160x3840][layout=NHWC][inputKind=Tensor][antialias=0][numChannels=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2510.04112386698, + "gpu_time_us_python": 2527.4917998361598, + "gpu_noise_us_cpp": 1.734, + "gpu_noise_us_python": 3.918, + "gpu_bwutil_cpp": 0.2855744128569196, + "gpu_bwutil_python": 0.28360259284590306, + "gpu_gap_stddev_us": 1.0847641190521538 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2203.36900452558, + "gpu_time_us_python": 2224.5015133845, + "gpu_noise_us_cpp": 1.6840000000000004, + "gpu_noise_us_python": 6.984, + "gpu_bwutil_cpp": 0.24816167077599, + "gpu_bwutil_python": 0.24579893726688723, + "gpu_gap_stddev_us": 3.305895598763934 + } + } + } + }, + "hqresize_anisotropic_expand_2160x3840_nhwc_tensor_cubic_float32_c3_aa0_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "16x480x864" + ], + "interpolation": [ + "CUBIC" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize float32 3-channel Tensor (NHWC) anisotropically from 864x480 to 3840x2160 with CUBIC, antialias=0. Advanced normalized mirror of hqresize_planar_expand_nhwc_1080p_basic; batch 16 scaled conservatively to 16." + }, + "baselines": { + "hqresize_anisotropic_expand_2160x3840_nhwc_tensor_cubic_float32_c3_aa0_advanced[InOutDataType=float32][shape=16x480x864][interpolation=CUBIC][resizeType=TARGET_2160x3840][layout=NHWC][inputKind=Tensor][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2147.0216441357998, + "gpu_time_us_python": 2164.61773699722, + "gpu_noise_us_cpp": 1.466, + "gpu_noise_us_python": 3.9279999999999995, + "gpu_bwutil_cpp": 0.5007876592021084, + "gpu_bwutil_python": 0.49671720124114316, + "gpu_gap_stddev_us": 3.1084940344655343 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2064.5365799301203, + "gpu_time_us_python": 2082.36898685614, + "gpu_noise_us_cpp": 2.784, + "gpu_noise_us_python": 3.434, + "gpu_bwutil_cpp": 0.3972285562851913, + "gpu_bwutil_python": 0.39382685886077534, + "gpu_gap_stddev_us": 1.2540124830366148 + } + } + } + }, + "hqresize_anisotropic_expand_2160x3840_nchw_tensor_linear_float32_c3_aa0_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "16x480x864" + ], + "interpolation": [ + "LINEAR" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize float32 3-channel Tensor (NCHW) anisotropically from 864x480 to 3840x2160 with LINEAR, antialias=0. Advanced normalized mirror of hqresize_planar_expand_linear_nchw_1080p_basic; batch 16 scaled conservatively to 16." + }, + "baselines": { + "hqresize_anisotropic_expand_2160x3840_nchw_tensor_linear_float32_c3_aa0_advanced[InOutDataType=float32][shape=16x480x864][interpolation=LINEAR][resizeType=TARGET_2160x3840][layout=NCHW][inputKind=Tensor][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3326.22879212358, + "gpu_time_us_python": 3346.0687834421406, + "gpu_noise_us_cpp": 3.1239999999999997, + "gpu_noise_us_python": 8.55, + "gpu_bwutil_cpp": 0.3232495938712952, + "gpu_bwutil_python": 0.3213329481323738, + "gpu_gap_stddev_us": 2.209735285077573 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3304.79696448022, + "gpu_time_us_python": 3326.05089919178, + "gpu_noise_us_cpp": 1.266, + "gpu_noise_us_python": 3.872, + "gpu_bwutil_cpp": 0.24818412816018007, + "gpu_bwutil_python": 0.24659216345769375, + "gpu_gap_stddev_us": 4.162311902287208 + } + } + } + }, + "hqresize_anisotropic_expand_2160x3840_nhwc_tensor_linear_float32_c1_aa0_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "64x480x864" + ], + "interpolation": [ + "LINEAR" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize float32 1-channel Tensor (NHWC) anisotropically from 864x480 to 3840x2160 with LINEAR, antialias=0. Advanced normalized mirror of hqresize_expand_linear_1080p_advanced; batch 64 scaled conservatively to 64." + }, + "baselines": { + "hqresize_anisotropic_expand_2160x3840_nhwc_tensor_linear_float32_c1_aa0_advanced[InOutDataType=float32][shape=64x480x864][interpolation=LINEAR][resizeType=TARGET_2160x3840][layout=NHWC][inputKind=Tensor][antialias=0][numChannels=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4432.781003650819, + "gpu_time_us_python": 4452.46985657766, + "gpu_noise_us_cpp": 1.368, + "gpu_noise_us_python": 4.616, + "gpu_bwutil_cpp": 0.3234103974132664, + "gpu_bwutil_python": 0.32197967833826013, + "gpu_gap_stddev_us": 3.15837595892466 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4403.60258527034, + "gpu_time_us_python": 4419.45615272796, + "gpu_noise_us_cpp": 3.6519999999999997, + "gpu_noise_us_python": 2.882, + "gpu_bwutil_cpp": 0.24834546889306203, + "gpu_bwutil_python": 0.2474529198238856, + "gpu_gap_stddev_us": 3.1177279324149976 + } + } + } + }, + "hqresize_anisotropic_expand_2160x3840_nhwc_tensor_linear_float32_c3_aa0_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "16x480x864" + ], + "interpolation": [ + "LINEAR" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize float32 3-channel Tensor (NHWC) anisotropically from 864x480 to 3840x2160 with LINEAR, antialias=0. Advanced normalized mirror of hqresize_planar_expand_linear_nhwc_1080p_basic; batch 16 scaled conservatively to 16." + }, + "baselines": { + "hqresize_anisotropic_expand_2160x3840_nhwc_tensor_linear_float32_c3_aa0_advanced[InOutDataType=float32][shape=16x480x864][interpolation=LINEAR][resizeType=TARGET_2160x3840][layout=NHWC][inputKind=Tensor][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1430.9504864556, + "gpu_time_us_python": 1449.6640904709798, + "gpu_noise_us_cpp": 1.5259999999999998, + "gpu_noise_us_python": 5.08, + "gpu_bwutil_cpp": 0.751390114571618, + "gpu_bwutil_python": 0.7416927739813725, + "gpu_gap_stddev_us": 3.405291767469561 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1301.46039212484, + "gpu_time_us_python": 1321.46458782962, + "gpu_noise_us_cpp": 6.4879999999999995, + "gpu_noise_us_python": 5.642, + "gpu_bwutil_cpp": 0.6301914612716399, + "gpu_bwutil_python": 0.6206716864067747, + "gpu_gap_stddev_us": 2.694934715480974 + } + } + } + }, + "hqresize_anisotropic_contract_480x864_nchw_tensor_cubic_uint8_c3_aa0_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "40x1080x1920" + ], + "interpolation": [ + "CUBIC" + ], + "resizeType": [ + "TARGET_480x864" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize uint8 3-channel Tensor (NCHW) anisotropically from 1920x1080 to 864x480 with CUBIC, antialias=0. Advanced normalized mirror of hqresize_planar_contract_nchw_1080p_basic; batch 64 scaled conservatively to 40." + }, + "baselines": { + "hqresize_anisotropic_contract_480x864_nchw_tensor_cubic_uint8_c3_aa0_advanced[InOutDataType=uint8][shape=40x1080x1920][interpolation=CUBIC][resizeType=TARGET_480x864][layout=NCHW][inputKind=Tensor][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1353.82372362806, + "gpu_time_us_python": 1371.8673898701597, + "gpu_noise_us_cpp": 1.484, + "gpu_noise_us_python": 3.5460000000000003, + "gpu_bwutil_cpp": 0.1418209575909244, + "gpu_bwutil_python": 0.13995576116794817, + "gpu_gap_stddev_us": 1.815027841471456 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1098.3829274170798, + "gpu_time_us_python": 1115.44236958334, + "gpu_noise_us_cpp": 1.0799999999999998, + "gpu_noise_us_python": 5.67, + "gpu_bwutil_cpp": 0.1333390236420206, + "gpu_bwutil_python": 0.13129772049507243, + "gpu_gap_stddev_us": 4.342909045165727 + } + } + } + }, + "hqresize_anisotropic_contract_480x864_nchw_tensor_cubic_uint8_c3_aa1_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "40x1080x1920" + ], + "interpolation": [ + "CUBIC" + ], + "resizeType": [ + "TARGET_480x864" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 1 + ], + "numChannels": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize uint8 3-channel Tensor (NCHW) anisotropically from 1920x1080 to 864x480 with CUBIC, antialias=1. Advanced normalized mirror of hqresize_planar_contract_cubic_antialias_nchw_1080p_basic; batch 64 scaled conservatively to 40." + }, + "baselines": { + "hqresize_anisotropic_contract_480x864_nchw_tensor_cubic_uint8_c3_aa1_advanced[InOutDataType=uint8][shape=40x1080x1920][interpolation=CUBIC][resizeType=TARGET_480x864][layout=NCHW][inputKind=Tensor][antialias=1][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2051.9846744573597, + "gpu_time_us_python": 2070.49760771962, + "gpu_noise_us_cpp": 1.874, + "gpu_noise_us_python": 5.882, + "gpu_bwutil_cpp": 0.09356834399712789, + "gpu_bwutil_python": 0.09273173417974657, + "gpu_gap_stddev_us": 1.3389035881835538 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1837.5225157626799, + "gpu_time_us_python": 1856.0258644950602, + "gpu_noise_us_cpp": 1.61, + "gpu_noise_us_python": 3.7700000000000005, + "gpu_bwutil_cpp": 0.07970721431982941, + "gpu_bwutil_python": 0.07891134713943539, + "gpu_gap_stddev_us": 1.163662548455833 + } + } + } + }, + "hqresize_anisotropic_contract_480x864_nchw_tensorbatch_cubic_uint8_c3_aa0_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "40x1080x1920" + ], + "interpolation": [ + "CUBIC" + ], + "resizeType": [ + "TARGET_480x864" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "TensorBatch" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize uint8 3-channel TensorBatch (NCHW) anisotropically from 1920x1080 to 864x480 with CUBIC, antialias=0. Advanced normalized mirror of hqresize_planar_contract_cubic_tensorbatch_nchw_1080p_advanced; batch 64 scaled conservatively to 40." + }, + "baselines": { + "hqresize_anisotropic_contract_480x864_nchw_tensorbatch_cubic_uint8_c3_aa0_advanced[InOutDataType=uint8][shape=40x1080x1920][interpolation=CUBIC][resizeType=TARGET_480x864][layout=NCHW][inputKind=TensorBatch][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2501.5861228010795, + "gpu_time_us_python": 2560.56745866966, + "gpu_noise_us_cpp": 33.474000000000004, + "gpu_noise_us_python": 30.845999999999997, + "gpu_bwutil_cpp": 0.07675753706050358, + "gpu_bwutil_python": 0.07498954155139811, + "gpu_gap_stddev_us": 21.79682779134194 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2096.3447827853, + "gpu_time_us_python": 2155.7140294054198, + "gpu_noise_us_cpp": 28.830000000000002, + "gpu_noise_us_python": 54.824, + "gpu_bwutil_cpp": 0.06988371245551708, + "gpu_bwutil_python": 0.06796594706671628, + "gpu_gap_stddev_us": 10.701111365608567 + } + } + } + }, + "hqresize_anisotropic_contract_480x864_nchw_varshape_cubic_uint8_c3_aa0_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "40x1080x1920" + ], + "interpolation": [ + "CUBIC" + ], + "resizeType": [ + "TARGET_480x864" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "VarShape" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize uint8 3-channel VarShape (NCHW) anisotropically from 1920x1080 to 864x480 with CUBIC, antialias=0. Advanced normalized mirror of hqresize_planar_contract_cubic_varshape_nchw_1080p_basic; batch 64 scaled conservatively to 40." + }, + "baselines": { + "hqresize_anisotropic_contract_480x864_nchw_varshape_cubic_uint8_c3_aa0_advanced[InOutDataType=uint8][shape=40x1080x1920][interpolation=CUBIC][resizeType=TARGET_480x864][layout=NCHW][inputKind=VarShape][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2356.8918185554003, + "gpu_time_us_python": 2426.5424250340398, + "gpu_noise_us_cpp": 33.117999999999995, + "gpu_noise_us_python": 27.662, + "gpu_bwutil_cpp": 0.08146601672464242, + "gpu_bwutil_python": 0.07912590840014469, + "gpu_gap_stddev_us": 17.00449434820365 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1986.7860323385999, + "gpu_time_us_python": 2029.19775866142, + "gpu_noise_us_cpp": 17.748, + "gpu_noise_us_python": 20.098, + "gpu_bwutil_cpp": 0.07370936409958093, + "gpu_bwutil_python": 0.07217215400912068, + "gpu_gap_stddev_us": 19.279777874774247 + } + } + } + }, + "hqresize_anisotropic_contract_480x864_nchwfake_tensor_cubic_uint8_c3_aa0_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "40x1080x1920" + ], + "interpolation": [ + "CUBIC" + ], + "resizeType": [ + "TARGET_480x864" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize uint8 3-channel Tensor (NCHW_FAKE) anisotropically from 1920x1080 to 864x480 with CUBIC, antialias=0. Advanced normalized mirror of hqresize_planar_contract_nchwfake_1080p_advanced; batch 64 scaled conservatively to 40." + }, + "baselines": { + "hqresize_anisotropic_contract_480x864_nchwfake_tensor_cubic_uint8_c3_aa0_advanced[InOutDataType=uint8][shape=40x1080x1920][interpolation=CUBIC][resizeType=TARGET_480x864][layout=NCHW_FAKE][inputKind=Tensor][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1874.7278615890198, + "gpu_time_us_python": 1892.61015203956, + "gpu_noise_us_cpp": 3.028, + "gpu_noise_us_python": 3.522, + "gpu_bwutil_cpp": 0.3072464124971595, + "gpu_bwutil_python": 0.30434345487830017, + "gpu_gap_stddev_us": 0.5027924348342854 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1502.75708080804, + "gpu_time_us_python": 1517.8980777233799, + "gpu_noise_us_cpp": 1.198, + "gpu_noise_us_python": 3.6, + "gpu_bwutil_cpp": 0.29235694777970045, + "gpu_bwutil_python": 0.2894370553457994, + "gpu_gap_stddev_us": 1.7491233233258385 + } + } + } + }, + "hqresize_anisotropic_contract_480x864_nhwc_tensor_cubic_uint8_c1_aa0_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "84x1080x1920" + ], + "interpolation": [ + "CUBIC" + ], + "resizeType": [ + "TARGET_480x864" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize uint8 1-channel Tensor (NHWC) anisotropically from 1920x1080 to 864x480 with CUBIC, antialias=0. Advanced normalized mirror of hqresize_contract_advanced; batch 84 calibrated for approximately 1 ms reference timing." + }, + "baselines": { + "hqresize_anisotropic_contract_480x864_nhwc_tensor_cubic_uint8_c1_aa0_advanced[InOutDataType=uint8][shape=84x1080x1920][interpolation=CUBIC][resizeType=TARGET_480x864][layout=NHWC][inputKind=Tensor][antialias=0][numChannels=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 956.90159849666, + "gpu_time_us_python": 974.65759578382, + "gpu_noise_us_cpp": 1.5739999999999998, + "gpu_noise_us_python": 3.28, + "gpu_bwutil_cpp": 0.14045360506642987, + "gpu_bwutil_python": 0.13789517428070713, + "gpu_gap_stddev_us": 0.8538920305561197 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 777.79732542528, + "gpu_time_us_python": 795.2196463014, + "gpu_noise_us_cpp": 1.4899999999999998, + "gpu_noise_us_python": 3.1420000000000003, + "gpu_bwutil_cpp": 0.13180754858881072, + "gpu_bwutil_python": 0.12891348141703626, + "gpu_gap_stddev_us": 2.7672697707435745 + } + } + } + }, + "hqresize_anisotropic_contract_480x864_nhwc_tensor_cubic_uint8_c3_aa0_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "40x1080x1920" + ], + "interpolation": [ + "CUBIC" + ], + "resizeType": [ + "TARGET_480x864" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize uint8 3-channel Tensor (NHWC) anisotropically from 1920x1080 to 864x480 with CUBIC, antialias=0. Advanced normalized mirror of hqresize_planar_contract_nhwc_1080p_basic; batch 64 scaled conservatively to 40." + }, + "baselines": { + "hqresize_anisotropic_contract_480x864_nhwc_tensor_cubic_uint8_c3_aa0_advanced[InOutDataType=uint8][shape=40x1080x1920][interpolation=CUBIC][resizeType=TARGET_480x864][layout=NHWC][inputKind=Tensor][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 773.6923546468599, + "gpu_time_us_python": 790.04143968928, + "gpu_noise_us_cpp": 1.376, + "gpu_noise_us_python": 3.9900000000000007, + "gpu_bwutil_cpp": 0.24816155589288885, + "gpu_bwutil_python": 0.24302672114469676, + "gpu_gap_stddev_us": 1.0158705781254105 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 652.29741692622, + "gpu_time_us_python": 671.1161577051199, + "gpu_noise_us_cpp": 1.356, + "gpu_noise_us_python": 3.12, + "gpu_bwutil_cpp": 0.22452491405306646, + "gpu_bwutil_python": 0.21822118869630502, + "gpu_gap_stddev_us": 2.3716288981829083 + } + } + } + }, + "hqresize_anisotropic_contract_480x864_nhwc_tensor_cubic_uint8_c1_aa1_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "40x1080x1920" + ], + "interpolation": [ + "CUBIC" + ], + "resizeType": [ + "TARGET_480x864" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 1 + ], + "numChannels": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize uint8 1-channel Tensor (NHWC) anisotropically from 1920x1080 to 864x480 with CUBIC, antialias=1. Advanced normalized mirror of hqresize_antialias_advanced; batch 64 scaled conservatively to 40." + }, + "baselines": { + "hqresize_anisotropic_contract_480x864_nhwc_tensor_cubic_uint8_c1_aa1_advanced[InOutDataType=uint8][shape=40x1080x1920][interpolation=CUBIC][resizeType=TARGET_480x864][layout=NHWC][inputKind=Tensor][antialias=1][numChannels=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 705.87458479472, + "gpu_time_us_python": 723.73901683964, + "gpu_noise_us_cpp": 2.134, + "gpu_noise_us_python": 4.486, + "gpu_bwutil_cpp": 0.09066788855321052, + "gpu_bwutil_python": 0.08843157960450097, + "gpu_gap_stddev_us": 2.902674706676006 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 632.78880870642, + "gpu_time_us_python": 648.4886746744199, + "gpu_noise_us_cpp": 1.3539999999999999, + "gpu_noise_us_python": 4.082, + "gpu_bwutil_cpp": 0.07715167914759946, + "gpu_bwutil_python": 0.07528017778387405, + "gpu_gap_stddev_us": 2.2431586053243735 + } + } + } + }, + "hqresize_anisotropic_contract_480x864_nhwc_tensorbatch_cubic_uint8_c3_aa0_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "40x1080x1920" + ], + "interpolation": [ + "CUBIC" + ], + "resizeType": [ + "TARGET_480x864" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "TensorBatch" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize uint8 3-channel TensorBatch (NHWC) anisotropically from 1920x1080 to 864x480 with CUBIC, antialias=0. Advanced normalized mirror of hqresize_planar_contract_cubic_tensorbatch_nhwc_1080p_advanced; batch 64 scaled conservatively to 40." + }, + "baselines": { + "hqresize_anisotropic_contract_480x864_nhwc_tensorbatch_cubic_uint8_c3_aa0_advanced[InOutDataType=uint8][shape=40x1080x1920][interpolation=CUBIC][resizeType=TARGET_480x864][layout=NHWC][inputKind=TensorBatch][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1005.9387934914399, + "gpu_time_us_python": 1032.5595782600399, + "gpu_noise_us_cpp": 14.463999999999999, + "gpu_noise_us_python": 13.268, + "gpu_bwutil_cpp": 0.1908784434356724, + "gpu_bwutil_python": 0.18595481360604946, + "gpu_gap_stddev_us": 8.33030215433532 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 831.8764317582202, + "gpu_time_us_python": 862.7529760566, + "gpu_noise_us_cpp": 8.77, + "gpu_noise_us_python": 14.541999999999998, + "gpu_bwutil_cpp": 0.1760487211652823, + "gpu_bwutil_python": 0.1697485971139991, + "gpu_gap_stddev_us": 10.129201713111888 + } + } + } + }, + "hqresize_anisotropic_contract_480x864_nhwc_varshape_cubic_uint8_c3_aa0_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "40x1080x1920" + ], + "interpolation": [ + "CUBIC" + ], + "resizeType": [ + "TARGET_480x864" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize uint8 3-channel VarShape (NHWC) anisotropically from 1920x1080 to 864x480 with CUBIC, antialias=0. Advanced normalized mirror of hqresize_planar_contract_cubic_varshape_nhwc_1080p_basic; batch 64 scaled conservatively to 40." + }, + "baselines": { + "hqresize_anisotropic_contract_480x864_nhwc_varshape_cubic_uint8_c3_aa0_advanced[InOutDataType=uint8][shape=40x1080x1920][interpolation=CUBIC][resizeType=TARGET_480x864][layout=NHWC][inputKind=VarShape][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1094.3890886556599, + "gpu_time_us_python": 1133.98684934632, + "gpu_noise_us_cpp": 9.719999999999999, + "gpu_noise_us_python": 11.802000000000001, + "gpu_bwutil_cpp": 0.17544378443973246, + "gpu_bwutil_python": 0.1693191720044033, + "gpu_gap_stddev_us": 7.42041587133048 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 877.7815329174798, + "gpu_time_us_python": 910.3284792613, + "gpu_noise_us_cpp": 10.565999999999999, + "gpu_noise_us_python": 15.626, + "gpu_bwutil_cpp": 0.16684302226891273, + "gpu_bwutil_python": 0.16089634517562743, + "gpu_gap_stddev_us": 10.896852455981666 + } + } + } + }, + "hqresize_anisotropic_contract_480x864_nchw_tensor_linear_uint8_c3_aa0_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "80x1080x1920" + ], + "interpolation": [ + "LINEAR" + ], + "resizeType": [ + "TARGET_480x864" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize uint8 3-channel Tensor (NCHW) anisotropically from 1920x1080 to 864x480 with LINEAR, antialias=0. Advanced normalized mirror of hqresize_planar_contract_linear_nchw_1080p_basic; batch 80 calibrated from the fastest layout for approximately 1 ms reference timing and shared across layouts." + }, + "baselines": { + "hqresize_anisotropic_contract_480x864_nchw_tensor_linear_uint8_c3_aa0_advanced[InOutDataType=uint8][shape=80x1080x1920][interpolation=LINEAR][resizeType=TARGET_480x864][layout=NCHW][inputKind=Tensor][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 601.59559942046, + "gpu_time_us_python": 617.27273658296, + "gpu_noise_us_cpp": 1.214, + "gpu_noise_us_python": 2.7, + "gpu_bwutil_cpp": 0.6383062420015927, + "gpu_bwutil_python": 0.6220961537471686, + "gpu_gap_stddev_us": 1.0609773556434134 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 539.8070257966799, + "gpu_time_us_python": 556.97148368894, + "gpu_noise_us_cpp": 1.874, + "gpu_noise_us_python": 2.154, + "gpu_bwutil_cpp": 0.5426440226234887, + "gpu_bwutil_python": 0.5258928845461964, + "gpu_gap_stddev_us": 2.6453351797665965 + } + } + } + }, + "hqresize_anisotropic_contract_480x864_nchwfake_tensor_linear_uint8_c3_aa0_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "80x1080x1920" + ], + "interpolation": [ + "LINEAR" + ], + "resizeType": [ + "TARGET_480x864" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize uint8 3-channel Tensor (NCHW_FAKE) anisotropically from 1920x1080 to 864x480 with LINEAR, antialias=0. Advanced normalized mirror of hqresize_planar_contract_linear_nchwfake_1080p_advanced; batch 80 calibrated from the fastest layout for approximately 1 ms reference timing and shared across layouts." + }, + "baselines": { + "hqresize_anisotropic_contract_480x864_nchwfake_tensor_linear_uint8_c3_aa0_advanced[InOutDataType=uint8][shape=80x1080x1920][interpolation=LINEAR][resizeType=TARGET_480x864][layout=NCHW_FAKE][inputKind=Tensor][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2694.0546801531, + "gpu_time_us_python": 2711.40926146738, + "gpu_noise_us_cpp": 1.4780000000000002, + "gpu_noise_us_python": 3.314, + "gpu_bwutil_cpp": 0.42761197390352484, + "gpu_bwutil_python": 0.4248748662805902, + "gpu_gap_stddev_us": 1.469517440570256 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2070.8487899043803, + "gpu_time_us_python": 2084.31192355048, + "gpu_noise_us_cpp": 2.19, + "gpu_noise_us_python": 2.088, + "gpu_bwutil_cpp": 0.4243025917211094, + "gpu_bwutil_python": 0.42156129498011446, + "gpu_gap_stddev_us": 0.33381391175684266 + } + } + } + }, + "hqresize_anisotropic_contract_480x864_nhwc_tensor_linear_uint8_c1_aa0_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "256x1080x1920" + ], + "interpolation": [ + "LINEAR" + ], + "resizeType": [ + "TARGET_480x864" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize uint8 1-channel Tensor (NHWC) anisotropically from 1920x1080 to 864x480 with LINEAR, antialias=0. Advanced normalized mirror of hqresize_contract_linear_1080p_advanced; batch 256 calibrated for approximately 1 ms reference timing." + }, + "baselines": { + "hqresize_anisotropic_contract_480x864_nhwc_tensor_linear_uint8_c1_aa0_advanced[InOutDataType=uint8][shape=256x1080x1920][interpolation=LINEAR][resizeType=TARGET_480x864][layout=NHWC][inputKind=Tensor][antialias=0][numChannels=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 639.60020314082, + "gpu_time_us_python": 656.2416439658001, + "gpu_noise_us_cpp": 1.846, + "gpu_noise_us_python": 3.2760000000000007, + "gpu_bwutil_cpp": 0.6404024223233538, + "gpu_bwutil_python": 0.6241698786701427, + "gpu_gap_stddev_us": 1.7201557974503483 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 573.93799077984, + "gpu_time_us_python": 590.5243498472599, + "gpu_noise_us_cpp": 1.034, + "gpu_noise_us_python": 5.138, + "gpu_bwutil_cpp": 0.5444018338480928, + "gpu_bwutil_python": 0.5290748059583373, + "gpu_gap_stddev_us": 3.693333497411088 + } + } + } + }, + "hqresize_anisotropic_contract_480x864_nhwc_tensor_linear_uint8_c3_aa0_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "80x1080x1920" + ], + "interpolation": [ + "LINEAR" + ], + "resizeType": [ + "TARGET_480x864" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize uint8 3-channel Tensor (NHWC) anisotropically from 1920x1080 to 864x480 with LINEAR, antialias=0. Advanced normalized mirror of hqresize_planar_contract_linear_nhwc_1080p_basic; batch 80 calibrated from the fastest layout for approximately 1 ms reference timing and shared across layouts." + }, + "baselines": { + "hqresize_anisotropic_contract_480x864_nhwc_tensor_linear_uint8_c3_aa0_advanced[InOutDataType=uint8][shape=80x1080x1920][interpolation=LINEAR][resizeType=TARGET_480x864][layout=NHWC][inputKind=Tensor][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 511.88465276498, + "gpu_time_us_python": 527.9688027954, + "gpu_noise_us_cpp": 1.12, + "gpu_noise_us_python": 3.2619999999999996, + "gpu_bwutil_cpp": 0.7501721732314591, + "gpu_bwutil_python": 0.7273288885260826, + "gpu_gap_stddev_us": 1.7928236882090909 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 392.51784612515996, + "gpu_time_us_python": 408.96600777408, + "gpu_noise_us_cpp": 0.726, + "gpu_noise_us_python": 3.4739999999999993, + "gpu_bwutil_cpp": 0.7462479096008658, + "gpu_bwutil_python": 0.7161938507440965, + "gpu_gap_stddev_us": 1.5381090689329158 + } + } + } + }, + "hqresize_anisotropic_contract_480x864_nhwc_tensor_linear_uint8_c1_aa1_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "72x1080x1920" + ], + "interpolation": [ + "LINEAR" + ], + "resizeType": [ + "TARGET_480x864" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 1 + ], + "numChannels": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize uint8 1-channel Tensor (NHWC) anisotropically from 1920x1080 to 864x480 with LINEAR, antialias=1. Advanced normalized mirror of hqresize_contract_linear_antialias_1080p_advanced; batch 72 calibrated for approximately 1 ms reference timing." + }, + "baselines": { + "hqresize_anisotropic_contract_480x864_nhwc_tensor_linear_uint8_c1_aa1_advanced[InOutDataType=uint8][shape=72x1080x1920][interpolation=LINEAR][resizeType=TARGET_480x864][layout=NHWC][inputKind=Tensor][antialias=1][numChannels=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 931.84102793894, + "gpu_time_us_python": 950.01340704498, + "gpu_noise_us_cpp": 1.766, + "gpu_noise_us_python": 4.4719999999999995, + "gpu_bwutil_cpp": 0.12362669608449398, + "gpu_bwutil_python": 0.1212625671717578, + "gpu_gap_stddev_us": 2.4599642834177136 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 772.3078334086601, + "gpu_time_us_python": 789.81266351742, + "gpu_noise_us_cpp": 1.2899999999999998, + "gpu_noise_us_python": 15.05, + "gpu_bwutil_cpp": 0.11378327513545572, + "gpu_bwutil_python": 0.11125717752924469, + "gpu_gap_stddev_us": 1.7633313883264221 + } + } + } + }, + "hqresize_anisotropic_expand_2160x3840_nchw_tensor_cubic_uint8_c3_aa0_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "16x480x864" + ], + "interpolation": [ + "CUBIC" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize uint8 3-channel Tensor (NCHW) anisotropically from 864x480 to 3840x2160 with CUBIC, antialias=0. Advanced normalized mirror of hqresize_planar_expand_nchw_1080p_basic; batch 16 scaled conservatively to 16." + }, + "baselines": { + "hqresize_anisotropic_expand_2160x3840_nchw_tensor_cubic_uint8_c3_aa0_advanced[InOutDataType=uint8][shape=16x480x864][interpolation=CUBIC][resizeType=TARGET_2160x3840][layout=NCHW][inputKind=Tensor][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3229.78264490758, + "gpu_time_us_python": 3247.33662826788, + "gpu_noise_us_cpp": 2.9219999999999997, + "gpu_noise_us_python": 3.186, + "gpu_bwutil_cpp": 0.08322584152363526, + "gpu_bwutil_python": 0.08277584134730878, + "gpu_gap_stddev_us": 1.5376912593666148 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3022.99125457866, + "gpu_time_us_python": 3042.89705329652, + "gpu_noise_us_cpp": 3.058, + "gpu_noise_us_python": 7.938000000000001, + "gpu_bwutil_cpp": 0.06783016235234252, + "gpu_bwutil_python": 0.06738596909960994, + "gpu_gap_stddev_us": 2.101458020082865 + } + } + } + }, + "hqresize_anisotropic_expand_2160x3840_nchwfake_tensor_cubic_uint8_c3_aa0_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "16x480x864" + ], + "interpolation": [ + "CUBIC" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize uint8 3-channel Tensor (NCHW_FAKE) anisotropically from 864x480 to 3840x2160 with CUBIC, antialias=0. Advanced normalized mirror of hqresize_planar_expand_nchwfake_1080p_advanced; batch 16 scaled conservatively to 16." + }, + "baselines": { + "hqresize_anisotropic_expand_2160x3840_nchwfake_tensor_cubic_uint8_c3_aa0_advanced[InOutDataType=uint8][shape=16x480x864][interpolation=CUBIC][resizeType=TARGET_2160x3840][layout=NCHW_FAKE][inputKind=Tensor][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2712.1424807795, + "gpu_time_us_python": 2730.3290843962995, + "gpu_noise_us_cpp": 1.7919999999999998, + "gpu_noise_us_python": 4.13, + "gpu_bwutil_cpp": 0.2973306980561058, + "gpu_bwutil_python": 0.2953501418572066, + "gpu_gap_stddev_us": 1.6462398527101434 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2556.0553579225602, + "gpu_time_us_python": 2570.6503140656396, + "gpu_noise_us_cpp": 1.47, + "gpu_noise_us_python": 2.308, + "gpu_bwutil_cpp": 0.2406370034809555, + "gpu_bwutil_python": 0.23927151171606922, + "gpu_gap_stddev_us": 1.0632697363453014 + } + } + } + }, + "hqresize_anisotropic_expand_2160x3840_nhwc_tensor_cubic_uint8_c1_aa0_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "32x480x864" + ], + "interpolation": [ + "CUBIC" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize uint8 1-channel Tensor (NHWC) anisotropically from 864x480 to 3840x2160 with CUBIC, antialias=0. Advanced normalized mirror of hqresize_expand_advanced; batch 32 scaled conservatively to 32." + }, + "baselines": { + "hqresize_anisotropic_expand_2160x3840_nhwc_tensor_cubic_uint8_c1_aa0_advanced[InOutDataType=uint8][shape=32x480x864][interpolation=CUBIC][resizeType=TARGET_2160x3840][layout=NHWC][inputKind=Tensor][antialias=0][numChannels=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2176.86699348584, + "gpu_time_us_python": 2194.4916435127802, + "gpu_noise_us_cpp": 3.3299999999999996, + "gpu_noise_us_python": 5.2700000000000005, + "gpu_bwutil_cpp": 0.08232045776307038, + "gpu_bwutil_python": 0.08165916781641074, + "gpu_gap_stddev_us": 1.9008131461685753 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2038.0087824836598, + "gpu_time_us_python": 2056.12664570076, + "gpu_noise_us_cpp": 3.8979999999999997, + "gpu_noise_us_python": 4.558, + "gpu_bwutil_cpp": 0.0670747576348147, + "gpu_bwutil_python": 0.0664816000881564, + "gpu_gap_stddev_us": 2.892653136054211 + } + } + } + }, + "hqresize_anisotropic_expand_2160x3840_nchw_tensor_linear_uint8_c3_aa0_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "16x480x864" + ], + "interpolation": [ + "LINEAR" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize uint8 3-channel Tensor (NCHW) anisotropically from 864x480 to 3840x2160 with LINEAR, antialias=0. Advanced normalized mirror of hqresize_planar_expand_linear_nchw_1080p_basic; batch 16 scaled conservatively to 16." + }, + "baselines": { + "hqresize_anisotropic_expand_2160x3840_nchw_tensor_linear_uint8_c3_aa0_advanced[InOutDataType=uint8][shape=16x480x864][interpolation=LINEAR][resizeType=TARGET_2160x3840][layout=NCHW][inputKind=Tensor][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2267.2689094199204, + "gpu_time_us_python": 2284.64266690336, + "gpu_noise_us_cpp": 2.9479999999999995, + "gpu_noise_us_python": 4.076, + "gpu_bwutil_cpp": 0.11855689511390344, + "gpu_bwutil_python": 0.1176554676738659, + "gpu_gap_stddev_us": 2.2267747488115317 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2042.60644152588, + "gpu_time_us_python": 2060.25126536906, + "gpu_noise_us_cpp": 2.164, + "gpu_noise_us_python": 4.351999999999999, + "gpu_bwutil_cpp": 0.10038690456256079, + "gpu_bwutil_python": 0.0995271822719724, + "gpu_gap_stddev_us": 2.0402223979267777 + } + } + } + }, + "hqresize_anisotropic_expand_2160x3840_nhwc_tensor_linear_uint8_c1_aa0_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "64x480x864" + ], + "interpolation": [ + "LINEAR" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize uint8 1-channel Tensor (NHWC) anisotropically from 864x480 to 3840x2160 with LINEAR, antialias=0. Advanced normalized mirror of hqresize_expand_linear_1080p_advanced; batch 64 scaled conservatively to 64." + }, + "baselines": { + "hqresize_anisotropic_expand_2160x3840_nhwc_tensor_linear_uint8_c1_aa0_advanced[InOutDataType=uint8][shape=64x480x864][interpolation=LINEAR][resizeType=TARGET_2160x3840][layout=NHWC][inputKind=Tensor][antialias=0][numChannels=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3018.3843945881795, + "gpu_time_us_python": 3039.2344526497195, + "gpu_noise_us_cpp": 3.528, + "gpu_noise_us_python": 6.264, + "gpu_bwutil_cpp": 0.11873916835824541, + "gpu_bwutil_python": 0.11792492181306122, + "gpu_gap_stddev_us": 3.654074064019381 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2718.9548663409996, + "gpu_time_us_python": 2737.71574394392, + "gpu_noise_us_cpp": 1.9100000000000001, + "gpu_noise_us_python": 5.178, + "gpu_bwutil_cpp": 0.1005541777071128, + "gpu_bwutil_python": 0.09986300257789077, + "gpu_gap_stddev_us": 3.2665395262760866 + } + } + } + }, + "hqresize_anisotropic_expand_2160x3840_nhwc_tensor_linear_uint8_c3_aa0_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "16x480x864" + ], + "interpolation": [ + "LINEAR" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "antialias": [ + 0 + ], + "numChannels": [ + 3 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "HQResize uint8 3-channel Tensor (NHWC) anisotropically from 864x480 to 3840x2160 with LINEAR, antialias=0. Advanced normalized mirror of hqresize_planar_expand_linear_nhwc_1080p_basic; batch 16 scaled conservatively to 16." + }, + "baselines": { + "hqresize_anisotropic_expand_2160x3840_nhwc_tensor_linear_uint8_c3_aa0_advanced[InOutDataType=uint8][shape=16x480x864][interpolation=LINEAR][resizeType=TARGET_2160x3840][layout=NHWC][inputKind=Tensor][antialias=0][numChannels=3]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1326.4320840499001, + "gpu_time_us_python": 1344.6388690489398, + "gpu_noise_us_cpp": 1.442, + "gpu_noise_us_python": 5.468, + "gpu_bwutil_cpp": 0.20265003307229873, + "gpu_bwutil_python": 0.19990587430044174, + "gpu_gap_stddev_us": 1.9185173735025398 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 991.5630004423199, + "gpu_time_us_python": 1010.13240226108, + "gpu_noise_us_cpp": 1.1300000000000001, + "gpu_noise_us_python": 2.732, + "gpu_bwutil_cpp": 0.20676305052411972, + "gpu_bwutil_python": 0.20296189944981666, + "gpu_gap_stddev_us": 3.3489198638110174 + } + } + } + } + } +} diff --git a/bench/config/operators/inpaint.json b/bench/config/operators/inpaint.json new file mode 100644 index 000000000..04c7a4baf --- /dev/null +++ b/bench/config/operators/inpaint.json @@ -0,0 +1,1185 @@ +{ + "benchmark": "inpaint", + "configs": { + "inpaint_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "1x720x1280" + ], + "inputKind": [ + "Tensor", + "VarShape" + ], + "layout": [ + "NHWC" + ] + }, + "float64_axes": { + "inpaintRadius": [ + 5.0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Inpaint RGB8 (basic)." + }, + "baselines": { + "inpaint_uchar3_basic[InOutDataType=uchar3][shape=1x720x1280][inputKind=Tensor][layout=NHWC][inpaintRadius=5.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5067.4497241973395, + "gpu_time_us_python": 5091.20754814144, + "gpu_noise_us_cpp": 11.508000000000001, + "gpu_noise_us_python": 12.368, + "gpu_bwutil_cpp": 0.0008185876066336801, + "gpu_bwutil_python": 0.0008147680168816401, + "gpu_gap_stddev_us": 2.618107701447399 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4859.36485146572, + "gpu_time_us_python": 4881.04216264784, + "gpu_noise_us_cpp": 15.95, + "gpu_noise_us_python": 19.278, + "gpu_bwutil_cpp": 0.0006511997633837599, + "gpu_bwutil_python": 0.0006483100654186799, + "gpu_gap_stddev_us": 4.340407846506573 + } + }, + "inpaint_uchar3_basic[InOutDataType=uchar3][shape=1x720x1280][inputKind=VarShape][layout=NHWC][inpaintRadius=5.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 7850.9096205234, + "gpu_time_us_python": 7876.7908006906, + "gpu_noise_us_cpp": 30.762, + "gpu_noise_us_python": 29.292, + "gpu_bwutil_cpp": 0.0005283670658856201, + "gpu_bwutil_python": 0.00052662987645062, + "gpu_gap_stddev_us": 11.188127053418437 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6527.5855981386, + "gpu_time_us_python": 6556.834821276281, + "gpu_noise_us_cpp": 26.932, + "gpu_noise_us_python": 29.129999999999995, + "gpu_bwutil_cpp": 0.00048477778324769997, + "gpu_bwutil_python": 0.00048260971557149995, + "gpu_gap_stddev_us": 9.90912713124396 + } + } + } + }, + "inpaint_uchar3_basic_planar_nchw": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "1x720x1280" + ], + "inputKind": [ + "Tensor", + "VarShape" + ], + "layout": [ + "NCHW" + ] + }, + "float64_axes": { + "inpaintRadius": [ + 5.0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Inpaint RGB8 (basic). (native planar NCHW)." + }, + "baselines": { + "inpaint_uchar3_basic_planar_nchw[InOutDataType=uchar3][shape=1x720x1280][inputKind=Tensor][layout=NCHW][inpaintRadius=5.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4895.887552846741, + "gpu_time_us_python": 4919.8336002873, + "gpu_noise_us_cpp": 12.662, + "gpu_noise_us_python": 14.807999999999998, + "gpu_bwutil_cpp": 0.0008472737601111001, + "gpu_bwutil_python": 0.0008431492507057599, + "gpu_gap_stddev_us": 6.893331576123074 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4628.50611201838, + "gpu_time_us_python": 4653.13838168824, + "gpu_noise_us_cpp": 10.308, + "gpu_noise_us_python": 13.463999999999999, + "gpu_bwutil_cpp": 0.0006836704396673799, + "gpu_bwutil_python": 0.00068005657844072, + "gpu_gap_stddev_us": 2.5896716318212296 + } + }, + "inpaint_uchar3_basic_planar_nchw[InOutDataType=uchar3][shape=1x720x1280][inputKind=VarShape][layout=NCHW][inpaintRadius=5.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 8131.806570483719, + "gpu_time_us_python": 8160.31195117578, + "gpu_noise_us_cpp": 25.622000000000003, + "gpu_noise_us_python": 24.018, + "gpu_bwutil_cpp": 0.0005101143735612801, + "gpu_bwutil_python": 0.0005083332721319801, + "gpu_gap_stddev_us": 9.320884934632128 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6772.640736242081, + "gpu_time_us_python": 6796.471566462021, + "gpu_noise_us_cpp": 17.424, + "gpu_noise_us_python": 20.832, + "gpu_bwutil_cpp": 0.00046723722205448007, + "gpu_bwutil_python": 0.00046559217259638, + "gpu_gap_stddev_us": 5.676515480494401 + } + } + } + }, + "inpaint_uchar3_planar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "1x720x1280" + ], + "inputKind": [ + "Tensor" + ], + "layout": [ + "NCHW" + ] + }, + "float64_axes": { + "inpaintRadius": [ + 5.0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Inpaint RGB8 native-planar Tensor comparison (advanced)." + }, + "baselines": { + "inpaint_uchar3_planar_nchw_advanced[InOutDataType=uchar3][shape=1x720x1280][inputKind=Tensor][layout=NCHW][inpaintRadius=5.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4900.105489792661, + "gpu_time_us_python": 4922.09217595114, + "gpu_noise_us_cpp": 12.370000000000001, + "gpu_noise_us_python": 13.588, + "gpu_bwutil_cpp": 0.00084654657635006, + "gpu_bwutil_python": 0.00084276646249842, + "gpu_gap_stddev_us": 4.307165272578506 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4631.0127752477, + "gpu_time_us_python": 4652.088209531061, + "gpu_noise_us_cpp": 11.14, + "gpu_noise_us_python": 12.378, + "gpu_bwutil_cpp": 0.00068330482402496, + "gpu_bwutil_python": 0.00068020989029612, + "gpu_gap_stddev_us": 3.9816310900305543 + } + } + } + }, + "inpaint_uchar3_fakeplanar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "1x720x1280" + ], + "inputKind": [ + "Tensor" + ], + "layout": [ + "NCHW_FAKE" + ] + }, + "float64_axes": { + "inpaintRadius": [ + 5.0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Inpaint RGB8 (advanced). (fake planar NCHW_FAKE)." + }, + "baselines": { + "inpaint_uchar3_fakeplanar_nchw_advanced[InOutDataType=uchar3][shape=1x720x1280][inputKind=Tensor][layout=NCHW_FAKE][inpaintRadius=5.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5099.33753426216, + "gpu_time_us_python": 5132.52204388984, + "gpu_noise_us_cpp": 10.142, + "gpu_noise_us_python": 16.264, + "gpu_bwutil_cpp": 0.0022079883475056403, + "gpu_bwutil_python": 0.00219371451303776, + "gpu_gap_stddev_us": 3.5738818021377634 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4884.069230139219, + "gpu_time_us_python": 4922.50252935136, + "gpu_noise_us_cpp": 15.213999999999999, + "gpu_noise_us_python": 16.594, + "gpu_bwutil_cpp": 0.00175859538221672, + "gpu_bwutil_python": 0.0017448223927314598, + "gpu_gap_stddev_us": 7.231851611402874 + } + } + } + }, + "inpaint_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "1x720x1280" + ], + "inputKind": [ + "Tensor", + "VarShape" + ], + "layout": [ + "NHWC" + ] + }, + "float64_axes": { + "inpaintRadius": [ + 5.0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Inpaint RGBA8 (advanced)." + }, + "baselines": { + "inpaint_uchar4_advanced[InOutDataType=uchar4][shape=1x720x1280][inputKind=Tensor][layout=NHWC][inpaintRadius=5.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6593.23469463144, + "gpu_time_us_python": 6617.12625654114, + "gpu_noise_us_cpp": 14.561999999999998, + "gpu_noise_us_python": 17.082, + "gpu_bwutil_cpp": 0.00080891052152466, + "gpu_bwutil_python": 0.00080599019208164, + "gpu_gap_stddev_us": 5.892337921031089 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6360.0418551151, + "gpu_time_us_python": 6382.30821676738, + "gpu_noise_us_cpp": 18.938, + "gpu_noise_us_python": 20.368000000000002, + "gpu_bwutil_cpp": 0.00063969931659962, + "gpu_bwutil_python": 0.00063746360089476, + "gpu_gap_stddev_us": 4.0074069246752835 + } + }, + "inpaint_uchar4_advanced[InOutDataType=uchar4][shape=1x720x1280][inputKind=VarShape][layout=NHWC][inpaintRadius=5.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 10385.08622741694, + "gpu_time_us_python": 10404.73207526366, + "gpu_noise_us_cpp": 39.326, + "gpu_noise_us_python": 55.102, + "gpu_bwutil_cpp": 0.00051355789831272, + "gpu_bwutil_python": 0.00051258809959134, + "gpu_gap_stddev_us": 23.166601959096646 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 8600.99046772918, + "gpu_time_us_python": 8631.209050978681, + "gpu_noise_us_cpp": 36.11, + "gpu_noise_us_python": 34.016, + "gpu_bwutil_cpp": 0.0004730311816776, + "gpu_bwutil_python": 0.00047136856178093997, + "gpu_gap_stddev_us": 9.767291041979114 + } + } + } + }, + "inpaint_uchar4_planar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "1x720x1280" + ], + "inputKind": [ + "Tensor" + ], + "layout": [ + "NCHW" + ] + }, + "float64_axes": { + "inpaintRadius": [ + 5.0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Inpaint RGBA8 (advanced). (native planar NCHW)." + }, + "baselines": { + "inpaint_uchar4_planar_nchw_advanced[InOutDataType=uchar4][shape=1x720x1280][inputKind=Tensor][layout=NCHW][inpaintRadius=5.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6419.48012572062, + "gpu_time_us_python": 6445.094737028419, + "gpu_noise_us_cpp": 12.736, + "gpu_noise_us_python": 13.732, + "gpu_bwutil_cpp": 0.00083080503341076, + "gpu_bwutil_python": 0.0008275036070508, + "gpu_gap_stddev_us": 7.91029215669517 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6069.140296032139, + "gpu_time_us_python": 6088.976972471361, + "gpu_noise_us_cpp": 18.621999999999996, + "gpu_noise_us_python": 21.606, + "gpu_bwutil_cpp": 0.00067036141638226, + "gpu_bwutil_python": 0.00066817498406202, + "gpu_gap_stddev_us": 2.5196626008746636 + } + } + } + }, + "inpaint_uchar4_fakeplanar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "1x720x1280" + ], + "inputKind": [ + "Tensor" + ], + "layout": [ + "NCHW_FAKE" + ] + }, + "float64_axes": { + "inpaintRadius": [ + 5.0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Inpaint RGBA8 (advanced). (fake planar NCHW_FAKE)." + }, + "baselines": { + "inpaint_uchar4_fakeplanar_nchw_advanced[InOutDataType=uchar4][shape=1x720x1280][inputKind=Tensor][layout=NCHW_FAKE][inpaintRadius=5.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6628.021538606141, + "gpu_time_us_python": 6664.9142039449, + "gpu_noise_us_cpp": 24.281999999999996, + "gpu_noise_us_python": 20.478, + "gpu_bwutil_cpp": 0.00223518076674126, + "gpu_bwutil_python": 0.00222282086430658, + "gpu_gap_stddev_us": 17.517434344009956 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6390.036468139019, + "gpu_time_us_python": 6426.35771207316, + "gpu_noise_us_cpp": 17.376, + "gpu_noise_us_python": 20.452, + "gpu_bwutil_cpp": 0.0017686068770647403, + "gpu_bwutil_python": 0.00175862920931476, + "gpu_gap_stddev_us": 11.001592087584608 + } + } + } + }, + "inpaint_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "1x720x1280" + ], + "inputKind": [ + "Tensor", + "VarShape" + ], + "layout": [ + "NHWC" + ] + }, + "float64_axes": { + "inpaintRadius": [ + 5.0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Inpaint single-channel (advanced)." + }, + "baselines": { + "inpaint_scalar_advanced[InOutDataType=uint8][shape=1x720x1280][inputKind=Tensor][layout=NHWC][inpaintRadius=5.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1972.59370975602, + "gpu_time_us_python": 1992.79795988482, + "gpu_noise_us_cpp": 6.380000000000001, + "gpu_noise_us_python": 9.036000000000001, + "gpu_bwutil_cpp": 0.00090124187454458, + "gpu_bwutil_python": 0.0008921104280455001, + "gpu_gap_stddev_us": 5.93029419786419 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1857.69291655618, + "gpu_time_us_python": 1877.5625478573002, + "gpu_noise_us_cpp": 9.258000000000001, + "gpu_noise_us_python": 21.874, + "gpu_bwutil_cpp": 0.00073003907400474, + "gpu_bwutil_python": 0.0007222859366667999, + "gpu_gap_stddev_us": 4.206083112482375 + } + }, + "inpaint_scalar_advanced[InOutDataType=uint8][shape=1x720x1280][inputKind=VarShape][layout=NHWC][inpaintRadius=5.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2936.54893925344, + "gpu_time_us_python": 2962.00990228088, + "gpu_noise_us_cpp": 9.93, + "gpu_noise_us_python": 10.908, + "gpu_bwutil_cpp": 0.0006053996080331999, + "gpu_bwutil_python": 0.0006001952839520799, + "gpu_gap_stddev_us": 4.409399860486417 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2456.40606005408, + "gpu_time_us_python": 2478.0286624627197, + "gpu_noise_us_cpp": 9.724, + "gpu_noise_us_python": 8.861999999999998, + "gpu_bwutil_cpp": 0.00055209855273942, + "gpu_bwutil_python": 0.00054727645039148, + "gpu_gap_stddev_us": 2.142803489997834 + } + } + } + }, + "inpaint_scalar_planar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "1x720x1280" + ], + "inputKind": [ + "Tensor", + "VarShape" + ], + "layout": [ + "NCHW" + ] + }, + "float64_axes": { + "inpaintRadius": [ + 5.0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Inpaint single-channel (advanced). (native planar NCHW)." + }, + "baselines": { + "inpaint_scalar_planar_nchw_advanced[InOutDataType=uint8][shape=1x720x1280][inputKind=Tensor][layout=NCHW][inpaintRadius=5.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1941.62638775416, + "gpu_time_us_python": 1960.13245023784, + "gpu_noise_us_cpp": 7.295999999999999, + "gpu_noise_us_python": 7.252, + "gpu_bwutil_cpp": 0.0009156177973477199, + "gpu_bwutil_python": 0.0009069749652812799, + "gpu_gap_stddev_us": 4.020288355762438 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1792.63386326584, + "gpu_time_us_python": 1816.0631265330799, + "gpu_noise_us_cpp": 5.17, + "gpu_noise_us_python": 9.290000000000001, + "gpu_bwutil_cpp": 0.0007565189878306199, + "gpu_bwutil_python": 0.00074678820121506, + "gpu_gap_stddev_us": 4.610300037133649 + } + }, + "inpaint_scalar_planar_nchw_advanced[InOutDataType=uint8][shape=1x720x1280][inputKind=VarShape][layout=NCHW][inpaintRadius=5.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2937.5190616664595, + "gpu_time_us_python": 2962.6657126931004, + "gpu_noise_us_cpp": 11.226, + "gpu_noise_us_python": 12.483999999999998, + "gpu_bwutil_cpp": 0.0006052007295943801, + "gpu_bwutil_python": 0.0006000637207612, + "gpu_gap_stddev_us": 2.835550999088805 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2456.5646753453, + "gpu_time_us_python": 2480.44302645358, + "gpu_noise_us_cpp": 12.422, + "gpu_noise_us_python": 14.282, + "gpu_bwutil_cpp": 0.00055206316865852, + "gpu_bwutil_python": 0.0005467482984828, + "gpu_gap_stddev_us": 0.7591336487179678 + } + } + } + }, + "inpaint_scalar_fakeplanar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "1x720x1280" + ], + "inputKind": [ + "Tensor" + ], + "layout": [ + "NCHW_FAKE" + ] + }, + "float64_axes": { + "inpaintRadius": [ + 5.0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Inpaint single-channel (advanced). (fake planar NCHW_FAKE)." + }, + "baselines": { + "inpaint_scalar_fakeplanar_nchw_advanced[InOutDataType=uint8][shape=1x720x1280][inputKind=Tensor][layout=NCHW_FAKE][inpaintRadius=5.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1993.936055804, + "gpu_time_us_python": 2040.03835347544, + "gpu_noise_us_cpp": 10.11, + "gpu_noise_us_python": 8.629999999999999, + "gpu_bwutil_cpp": 0.0020803921640976, + "gpu_bwutil_python": 0.00203348227010514, + "gpu_gap_stddev_us": 13.477864849199834 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1876.8965506324798, + "gpu_time_us_python": 1923.7167656737802, + "gpu_noise_us_cpp": 8.648, + "gpu_noise_us_python": 15.456, + "gpu_bwutil_cpp": 0.0016859482525395202, + "gpu_bwutil_python": 0.0016448572120027999, + "gpu_gap_stddev_us": 7.2367842296295075 + } + } + } + }, + "inpaint_480x640_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "1x480x640" + ], + "inputKind": [ + "Tensor", + "VarShape" + ], + "layout": [ + "NHWC" + ] + }, + "float64_axes": { + "inpaintRadius": [ + 5.0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Inpaint RGBA8 (advanced)." + }, + "baselines": { + "inpaint_480x640_uchar4_advanced[InOutDataType=uchar4][shape=1x480x640][inputKind=Tensor][layout=NHWC][inpaintRadius=5.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2516.9035676849603, + "gpu_time_us_python": 2536.94079909658, + "gpu_noise_us_cpp": 16.259999999999998, + "gpu_noise_us_python": 16.164, + "gpu_bwutil_cpp": 0.0007063429964898, + "gpu_bwutil_python": 0.00070076390984354, + "gpu_gap_stddev_us": 2.5490068627516735 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2311.17258103878, + "gpu_time_us_python": 2334.3568952884198, + "gpu_noise_us_cpp": 12.934000000000001, + "gpu_noise_us_python": 9.72, + "gpu_bwutil_cpp": 0.00058680265635824, + "gpu_bwutil_python": 0.00058096863564606, + "gpu_gap_stddev_us": 3.3421525766182136 + } + }, + "inpaint_480x640_uchar4_advanced[InOutDataType=uchar4][shape=1x480x640][inputKind=VarShape][layout=NHWC][inpaintRadius=5.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3830.66991972408, + "gpu_time_us_python": 3861.87685192282, + "gpu_noise_us_cpp": 43.337999999999994, + "gpu_noise_us_python": 38.006, + "gpu_bwutil_cpp": 0.00046409211520870004, + "gpu_bwutil_python": 0.00046034196611794006, + "gpu_gap_stddev_us": 1.7122053292111794 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3300.9837719506804, + "gpu_time_us_python": 3328.45168608306, + "gpu_noise_us_cpp": 34.408, + "gpu_noise_us_python": 55.574, + "gpu_bwutil_cpp": 0.00041083605865348, + "gpu_bwutil_python": 0.00040744341507934005, + "gpu_gap_stddev_us": 3.3635635248832734 + } + } + } + }, + "inpaint_480x640_uchar4_planar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "1x480x640" + ], + "inputKind": [ + "Tensor" + ], + "layout": [ + "NCHW" + ] + }, + "float64_axes": { + "inpaintRadius": [ + 5.0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Inpaint RGBA8 (advanced). (native planar NCHW)." + }, + "baselines": { + "inpaint_480x640_uchar4_planar_nchw_advanced[InOutDataType=uchar4][shape=1x480x640][inputKind=Tensor][layout=NCHW][inpaintRadius=5.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2477.20069379806, + "gpu_time_us_python": 2494.27887186194, + "gpu_noise_us_cpp": 10.266000000000002, + "gpu_noise_us_python": 10.98, + "gpu_bwutil_cpp": 0.000717659655491, + "gpu_bwutil_python": 0.00071274759650926, + "gpu_gap_stddev_us": 9.224809587126863 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2232.81913590458, + "gpu_time_us_python": 2258.6124865967804, + "gpu_noise_us_cpp": 7.504, + "gpu_noise_us_python": 9.452, + "gpu_bwutil_cpp": 0.00060739014812786, + "gpu_bwutil_python": 0.00060046405816416, + "gpu_gap_stddev_us": 4.471934300564402 + } + } + } + }, + "inpaint_480x640_uchar4_fakeplanar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "1x480x640" + ], + "inputKind": [ + "Tensor" + ], + "layout": [ + "NCHW_FAKE" + ] + }, + "float64_axes": { + "inpaintRadius": [ + 5.0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Inpaint RGBA8 (advanced). (fake planar NCHW_FAKE)." + }, + "baselines": { + "inpaint_480x640_uchar4_fakeplanar_nchw_advanced[InOutDataType=uchar4][shape=1x480x640][inputKind=Tensor][layout=NCHW_FAKE][inpaintRadius=5.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2534.6689407270997, + "gpu_time_us_python": 2576.92631220736, + "gpu_noise_us_cpp": 16.448, + "gpu_noise_us_python": 16.462, + "gpu_bwutil_cpp": 0.001948303722251, + "gpu_bwutil_python": 0.0019164367418609998, + "gpu_gap_stddev_us": 15.065967445176943 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2333.30903019984, + "gpu_time_us_python": 2379.10647896638, + "gpu_noise_us_cpp": 12.766, + "gpu_noise_us_python": 18.432, + "gpu_bwutil_cpp": 0.0016145427002409601, + "gpu_bwutil_python": 0.0015833658595409202, + "gpu_gap_stddev_us": 9.430708475042394 + } + } + } + }, + "inpaint_u8_radius2_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "1x1080x1920" + ], + "inputKind": [ + "Tensor", + "VarShape" + ], + "layout": [ + "NHWC" + ] + }, + "float64_axes": { + "inpaintRadius": [ + 2.0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Inpaint single-channel radius2 (advanced)." + }, + "baselines": { + "inpaint_u8_radius2_1080p_advanced[InOutDataType=uint8][shape=1x1080x1920][inputKind=Tensor][layout=NHWC][inpaintRadius=2.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1401.9227262202203, + "gpu_time_us_python": 1421.7885897512, + "gpu_noise_us_cpp": 5.414, + "gpu_noise_us_python": 4.904000000000001, + "gpu_bwutil_cpp": 0.00285324733689164, + "gpu_bwutil_python": 0.00281339001294926, + "gpu_gap_stddev_us": 1.4116890059598268 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1316.2089383608402, + "gpu_time_us_python": 1337.44501939846, + "gpu_noise_us_cpp": 3.378, + "gpu_noise_us_python": 4.142, + "gpu_bwutil_cpp": 0.00231823147550822, + "gpu_bwutil_python": 0.00228146483054688, + "gpu_gap_stddev_us": 2.4585556394669843 + } + }, + "inpaint_u8_radius2_1080p_advanced[InOutDataType=uint8][shape=1x1080x1920][inputKind=VarShape][layout=NHWC][inpaintRadius=2.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1828.5692568469399, + "gpu_time_us_python": 1852.7493167858, + "gpu_noise_us_cpp": 5.392, + "gpu_noise_us_python": 8.361999999999998, + "gpu_bwutil_cpp": 0.00218751581523344, + "gpu_bwutil_python": 0.0021589781674008603, + "gpu_gap_stddev_us": 5.179866240572435 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1545.6119665109, + "gpu_time_us_python": 1566.55862483612, + "gpu_noise_us_cpp": 4.556, + "gpu_noise_us_python": 4.368, + "gpu_bwutil_cpp": 0.0019741677589984, + "gpu_bwutil_python": 0.0019477536033099796, + "gpu_gap_stddev_us": 1.7285721102143141 + } + } + } + }, + "inpaint_u8_radius2_1080p_planar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "1x1080x1920" + ], + "inputKind": [ + "Tensor", + "VarShape" + ], + "layout": [ + "NCHW" + ] + }, + "float64_axes": { + "inpaintRadius": [ + 2.0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Inpaint single-channel radius2 (advanced). (native planar NCHW)." + }, + "baselines": { + "inpaint_u8_radius2_1080p_planar_nchw_advanced[InOutDataType=uint8][shape=1x1080x1920][inputKind=Tensor][layout=NCHW][inpaintRadius=2.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1404.15804872694, + "gpu_time_us_python": 1423.3592636608398, + "gpu_noise_us_cpp": 3.1879999999999997, + "gpu_noise_us_python": 4.856, + "gpu_bwutil_cpp": 0.00284870713614352, + "gpu_bwutil_python": 0.00281030634059122, + "gpu_gap_stddev_us": 3.8273269132527847 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1307.03278921778, + "gpu_time_us_python": 1327.8056289407, + "gpu_noise_us_cpp": 3.152, + "gpu_noise_us_python": 13.234, + "gpu_bwutil_cpp": 0.00233450491516902, + "gpu_bwutil_python": 0.0022979678640729405, + "gpu_gap_stddev_us": 0.8515821703604148 + } + }, + "inpaint_u8_radius2_1080p_planar_nchw_advanced[InOutDataType=uint8][shape=1x1080x1920][inputKind=VarShape][layout=NCHW][inpaintRadius=2.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1829.83052849978, + "gpu_time_us_python": 1849.9523382781201, + "gpu_noise_us_cpp": 5.886, + "gpu_noise_us_python": 6.6, + "gpu_bwutil_cpp": 0.00218600594267218, + "gpu_bwutil_python": 0.0021622348770163804, + "gpu_gap_stddev_us": 2.24806195345238 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1545.56879452114, + "gpu_time_us_python": 1568.5696042182801, + "gpu_noise_us_cpp": 4.034000000000001, + "gpu_noise_us_python": 9.534, + "gpu_bwutil_cpp": 0.00197420817822372, + "gpu_bwutil_python": 0.0019452454234216599, + "gpu_gap_stddev_us": 3.2591071966122684 + } + } + } + }, + "inpaint_u8_radius2_1080p_fakeplanar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "1x1080x1920" + ], + "inputKind": [ + "Tensor" + ], + "layout": [ + "NCHW_FAKE" + ] + }, + "float64_axes": { + "inpaintRadius": [ + 2.0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Inpaint single-channel radius2 (advanced). (fake planar NCHW_FAKE)." + }, + "baselines": { + "inpaint_u8_radius2_1080p_fakeplanar_nchw_advanced[InOutDataType=uint8][shape=1x1080x1920][inputKind=Tensor][layout=NCHW_FAKE][inpaintRadius=2.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1432.8544482053999, + "gpu_time_us_python": 1466.13468867666, + "gpu_noise_us_cpp": 3.9239999999999995, + "gpu_noise_us_python": 8.918000000000001, + "gpu_bwutil_cpp": 0.0065138407351374, + "gpu_bwutil_python": 0.006366145290950101, + "gpu_gap_stddev_us": 5.390478648106018 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1347.0841783658002, + "gpu_time_us_python": 1383.15076986458, + "gpu_noise_us_cpp": 4.223999999999999, + "gpu_noise_us_python": 8.678, + "gpu_bwutil_cpp": 0.005285276004237721, + "gpu_bwutil_python": 0.0051472580212711, + "gpu_gap_stddev_us": 3.854073148207933 + } + } + } + }, + "inpaint_f32_radius2_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "1x1080x1920" + ], + "inputKind": [ + "Tensor", + "VarShape" + ], + "layout": [ + "NHWC" + ] + }, + "float64_axes": { + "inpaintRadius": [ + 2.0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Inpaint single-channel radius2 (advanced)." + }, + "baselines": { + "inpaint_f32_radius2_1080p_advanced[InOutDataType=float32][shape=1x1080x1920][inputKind=Tensor][layout=NHWC][inpaintRadius=2.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1408.1167312517, + "gpu_time_us_python": 1425.1317365678199, + "gpu_noise_us_cpp": 5.0840000000000005, + "gpu_noise_us_python": 5.536, + "gpu_bwutil_cpp": 0.008522084210926002, + "gpu_bwutil_python": 0.00842042830158012, + "gpu_gap_stddev_us": 4.321775652980997 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1323.478214332, + "gpu_time_us_python": 1344.2155115379398, + "gpu_noise_us_cpp": 2.8619999999999997, + "gpu_noise_us_python": 3.8019999999999996, + "gpu_bwutil_cpp": 0.006916429674733461, + "gpu_bwutil_python": 0.0068097359899034395, + "gpu_gap_stddev_us": 1.3961191480100636 + } + }, + "inpaint_f32_radius2_1080p_advanced[InOutDataType=float32][shape=1x1080x1920][inputKind=VarShape][layout=NHWC][inpaintRadius=2.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1824.6680340375, + "gpu_time_us_python": 1850.7750345340799, + "gpu_noise_us_cpp": 7.292, + "gpu_noise_us_python": 7.1579999999999995, + "gpu_bwutil_cpp": 0.00657658298034092, + "gpu_bwutil_python": 0.00648389834874946, + "gpu_gap_stddev_us": 7.510024418289223 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1555.57929533484, + "gpu_time_us_python": 1575.2795240726, + "gpu_noise_us_cpp": 4.308, + "gpu_noise_us_python": 4.978, + "gpu_bwutil_cpp": 0.0058844621334452, + "gpu_bwutil_python": 0.005810908945007001, + "gpu_gap_stddev_us": 2.0068833012887137 + } + } + } + }, + "inpaint_f32_radius2_1080p_planar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "1x1080x1920" + ], + "inputKind": [ + "Tensor", + "VarShape" + ], + "layout": [ + "NCHW" + ] + }, + "float64_axes": { + "inpaintRadius": [ + 2.0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Inpaint single-channel radius2 (advanced). (native planar NCHW)." + }, + "baselines": { + "inpaint_f32_radius2_1080p_planar_nchw_advanced[InOutDataType=float32][shape=1x1080x1920][inputKind=Tensor][layout=NCHW][inpaintRadius=2.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1409.2211310699, + "gpu_time_us_python": 1429.6769143235601, + "gpu_noise_us_cpp": 3.474, + "gpu_noise_us_python": 7.144, + "gpu_bwutil_cpp": 0.00851541560592832, + "gpu_bwutil_python": 0.008393634634133961, + "gpu_gap_stddev_us": 2.8330489612602054 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1319.5219474554601, + "gpu_time_us_python": 1341.50373331024, + "gpu_noise_us_cpp": 3.646, + "gpu_noise_us_python": 6.531999999999999, + "gpu_bwutil_cpp": 0.00693716398264888, + "gpu_bwutil_python": 0.0068233341317769005, + "gpu_gap_stddev_us": 2.485379382188118 + } + }, + "inpaint_f32_radius2_1080p_planar_nchw_advanced[InOutDataType=float32][shape=1x1080x1920][inputKind=VarShape][layout=NCHW][inpaintRadius=2.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1825.5781434495598, + "gpu_time_us_python": 1850.9382518820398, + "gpu_noise_us_cpp": 5.0920000000000005, + "gpu_noise_us_python": 7.4079999999999995, + "gpu_bwutil_cpp": 0.00657332025498034, + "gpu_bwutil_python": 0.00648323952062588, + "gpu_gap_stddev_us": 3.6929482892890118 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1556.0952043690402, + "gpu_time_us_python": 1576.08054878368, + "gpu_noise_us_cpp": 19.158, + "gpu_noise_us_python": 8.264, + "gpu_bwutil_cpp": 0.00588260021967064, + "gpu_bwutil_python": 0.0058078811728210795, + "gpu_gap_stddev_us": 1.5711692284396603 + } + } + } + }, + "inpaint_f32_radius2_1080p_fakeplanar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "1x1080x1920" + ], + "inputKind": [ + "Tensor" + ], + "layout": [ + "NCHW_FAKE" + ] + }, + "float64_axes": { + "inpaintRadius": [ + 2.0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Inpaint single-channel radius2 (advanced). (fake planar NCHW_FAKE)." + }, + "baselines": { + "inpaint_f32_radius2_1080p_fakeplanar_nchw_advanced[InOutDataType=float32][shape=1x1080x1920][inputKind=Tensor][layout=NCHW_FAKE][inpaintRadius=2.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1441.62943265624, + "gpu_time_us_python": 1481.43312861972, + "gpu_noise_us_cpp": 4.418, + "gpu_noise_us_python": 8.392, + "gpu_bwutil_cpp": 0.02312211676359752, + "gpu_bwutil_python": 0.02250237189110722, + "gpu_gap_stddev_us": 10.939290747751738 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1356.1342851065, + "gpu_time_us_python": 1388.66812019992, + "gpu_noise_us_cpp": 3.7299999999999995, + "gpu_noise_us_python": 7.26, + "gpu_bwutil_cpp": 0.01874962582117396, + "gpu_bwutil_python": 0.0183097810541368, + "gpu_gap_stddev_us": 2.6390151052213584 + } + } + } + } + } +} diff --git a/bench/config/operators/invert.json b/bench/config/operators/invert.json new file mode 100644 index 000000000..e65c4a4a1 --- /dev/null +++ b/bench/config/operators/invert.json @@ -0,0 +1,773 @@ +{ + "benchmark": "invert", + "configs": { + "invert_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Invert RGB8 interleaved + planar, Tensor + VarShape (~1-2ms)." + }, + "baselines": { + "invert_uchar3_basic[InOutDataType=uchar3][shape=64x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 583.8602074759599, + "gpu_time_us_python": 592.8624115139, + "gpu_noise_us_cpp": 1.404, + "gpu_noise_us_python": 2.1019999999999994, + "gpu_bwutil_cpp": 0.8769228996653121, + "gpu_bwutil_python": 0.8636158829505074, + "gpu_gap_stddev_us": 1.725046039563967 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 469.56183928696, + "gpu_time_us_python": 478.07250784347997, + "gpu_noise_us_cpp": 0.966, + "gpu_noise_us_python": 2.05, + "gpu_bwutil_cpp": 0.8316473192375637, + "gpu_bwutil_python": 0.8168577471180185, + "gpu_gap_stddev_us": 1.644229500796628 + } + }, + "invert_uchar3_basic[InOutDataType=uchar3][shape=64x1080x1920][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 665.77517279466, + "gpu_time_us_python": 677.09534447724, + "gpu_noise_us_cpp": 2.4899999999999998, + "gpu_noise_us_python": 4.220000000000001, + "gpu_bwutil_cpp": 0.7690345687986406, + "gpu_bwutil_python": 0.7561884478450818, + "gpu_gap_stddev_us": 1.917749508596409 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 532.0805438434202, + "gpu_time_us_python": 544.36398398284, + "gpu_noise_us_cpp": 2.896, + "gpu_noise_us_python": 3.5980000000000003, + "gpu_bwutil_cpp": 0.733948843878024, + "gpu_bwutil_python": 0.7174147038116434, + "gpu_gap_stddev_us": 2.3331805892223008 + } + }, + "invert_uchar3_basic[InOutDataType=uchar3][shape=64x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 584.3995825439799, + "gpu_time_us_python": 592.3629753749799, + "gpu_noise_us_cpp": 1.3699999999999999, + "gpu_noise_us_python": 1.4, + "gpu_bwutil_cpp": 0.8761146415147335, + "gpu_bwutil_python": 0.8643390144226537, + "gpu_gap_stddev_us": 0.5376135533998013 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 469.49775311234, + "gpu_time_us_python": 477.81690826014, + "gpu_noise_us_cpp": 1.0639999999999998, + "gpu_noise_us_python": 2.068, + "gpu_bwutil_cpp": 0.831760765657602, + "gpu_bwutil_python": 0.8172841158027261, + "gpu_gap_stddev_us": 0.7874616429222563 + } + }, + "invert_uchar3_basic[InOutDataType=uchar3][shape=64x1080x1920][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 816.6731982793401, + "gpu_time_us_python": 828.9459070781401, + "gpu_noise_us_cpp": 1.8679999999999999, + "gpu_noise_us_python": 3.6420000000000003, + "gpu_bwutil_cpp": 0.6269431926779558, + "gpu_bwutil_python": 0.6176606671253084, + "gpu_gap_stddev_us": 1.2195908844669672 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 665.88011290856, + "gpu_time_us_python": 680.8617289804399, + "gpu_noise_us_cpp": 2.876, + "gpu_noise_us_python": 3.022, + "gpu_bwutil_cpp": 0.5864720111682313, + "gpu_bwutil_python": 0.5735944641254125, + "gpu_gap_stddev_us": 9.445484833069703 + } + } + } + }, + "invert_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "20x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Invert RGBf32 interleaved + planar, Tensor (~1-2ms)." + }, + "baselines": { + "invert_float3_basic[InOutDataType=float3][shape=20x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 722.6736327244, + "gpu_time_us_python": 731.66592492002, + "gpu_noise_us_cpp": 1.536, + "gpu_noise_us_python": 3.0100000000000002, + "gpu_bwutil_cpp": 0.8856009974073284, + "gpu_bwutil_python": 0.8747174415747763, + "gpu_gap_stddev_us": 0.5648672159949997 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 552.97606465226, + "gpu_time_us_python": 560.84366124522, + "gpu_noise_us_cpp": 1.112, + "gpu_noise_us_python": 1.766, + "gpu_bwutil_cpp": 0.8827440014720424, + "gpu_bwutil_python": 0.8703644155655894, + "gpu_gap_stddev_us": 0.8963416478761878 + } + }, + "invert_float3_basic[InOutDataType=float3][shape=20x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 721.2790485073999, + "gpu_time_us_python": 730.09721079794, + "gpu_noise_us_cpp": 1.044, + "gpu_noise_us_python": 1.906, + "gpu_bwutil_cpp": 0.8873137369537896, + "gpu_bwutil_python": 0.8765980455401262, + "gpu_gap_stddev_us": 0.5324227866198487 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 556.86113482282, + "gpu_time_us_python": 565.2790666133799, + "gpu_noise_us_cpp": 0.9, + "gpu_noise_us_python": 1.8, + "gpu_bwutil_cpp": 0.8765850238163388, + "gpu_bwutil_python": 0.8635451029576655, + "gpu_gap_stddev_us": 2.1460457883382458 + } + } + } + }, + "invert_float3_varshape_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "20x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Invert RGBf32 interleaved + planar, VarShape (~1-2ms)." + }, + "baselines": { + "invert_float3_varshape_advanced[InOutDataType=float3][shape=20x1080x1920][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 808.44957225728, + "gpu_time_us_python": 817.9951760245, + "gpu_noise_us_cpp": 1.3, + "gpu_noise_us_python": 3.0180000000000002, + "gpu_bwutil_cpp": 0.791642076822062, + "gpu_bwutil_python": 0.7824031824205655, + "gpu_gap_stddev_us": 0.5449134913271447 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 667.9275629174999, + "gpu_time_us_python": 677.1009168209, + "gpu_noise_us_cpp": 1.656, + "gpu_noise_us_python": 2.198, + "gpu_bwutil_cpp": 0.7308275298071167, + "gpu_bwutil_python": 0.7209280993085988, + "gpu_gap_stddev_us": 0.7863283151566309 + } + }, + "invert_float3_varshape_advanced[InOutDataType=float3][shape=20x1080x1920][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 732.7092248134, + "gpu_time_us_python": 742.52695271278, + "gpu_noise_us_cpp": 2.528, + "gpu_noise_us_python": 2.872, + "gpu_bwutil_cpp": 0.8734734434864514, + "gpu_bwutil_python": 0.8619260959622131, + "gpu_gap_stddev_us": 0.6907952412523671 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 563.7623467382, + "gpu_time_us_python": 573.6674827753401, + "gpu_noise_us_cpp": 2.048, + "gpu_noise_us_python": 2.7700000000000005, + "gpu_bwutil_cpp": 0.865859094529361, + "gpu_bwutil_python": 0.8509178728850021, + "gpu_gap_stddev_us": 1.1830665281893287 + } + } + } + }, + "invert_planar_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Invert RGB8 native-planar Tensor comparison (advanced)." + }, + "baselines": { + "invert_planar_uchar3_advanced[InOutDataType=uchar3][shape=64x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 584.31054555892, + "gpu_time_us_python": 592.5980620008, + "gpu_noise_us_cpp": 1.2580000000000002, + "gpu_noise_us_python": 1.752, + "gpu_bwutil_cpp": 0.876247650497666, + "gpu_bwutil_python": 0.8639947549475776, + "gpu_gap_stddev_us": 0.5590474283060547 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 469.64680241994, + "gpu_time_us_python": 477.71781650901994, + "gpu_noise_us_cpp": 1.3619999999999999, + "gpu_noise_us_python": 2.272, + "gpu_bwutil_cpp": 0.831496048341131, + "gpu_bwutil_python": 0.8174521354923856, + "gpu_gap_stddev_us": 0.7492143448957709 + } + } + } + }, + "invert_fake_planar_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Invert RGB8 fake-planar comparison (tensor-only, ~1-2ms)." + }, + "baselines": { + "invert_fake_planar_uchar3_advanced[InOutDataType=uchar3][shape=64x1080x1920][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2947.6237011934, + "gpu_time_us_python": 2964.8148811564397, + "gpu_noise_us_cpp": 2.272, + "gpu_noise_us_python": 2.4179999999999997, + "gpu_bwutil_cpp": 0.5211006833827596, + "gpu_bwutil_python": 0.5180786275949447, + "gpu_gap_stddev_us": 2.4103898915166257 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2304.73234463494, + "gpu_time_us_python": 2319.25857798642, + "gpu_noise_us_cpp": 1.1, + "gpu_noise_us_python": 2.8559999999999994, + "gpu_bwutil_cpp": 0.5083180632982816, + "gpu_bwutil_python": 0.5051338387359495, + "gpu_gap_stddev_us": 2.973043299646702 + } + } + } + }, + "invert_planar_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "20x1080x1920" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Invert RGBf32 native-planar Tensor comparison (advanced)." + }, + "baselines": { + "invert_planar_float3_advanced[InOutDataType=float3][shape=20x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 722.62959519418, + "gpu_time_us_python": 731.53785786096, + "gpu_noise_us_cpp": 2.0, + "gpu_noise_us_python": 1.9740000000000002, + "gpu_bwutil_cpp": 0.8856548393334913, + "gpu_bwutil_python": 0.8748716531566828, + "gpu_gap_stddev_us": 0.7929826551677391 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 552.9816645492, + "gpu_time_us_python": 560.2205801246599, + "gpu_noise_us_cpp": 1.216, + "gpu_noise_us_python": 2.2380000000000004, + "gpu_bwutil_cpp": 0.8827348349396275, + "gpu_bwutil_python": 0.8713299122517913, + "gpu_gap_stddev_us": 0.6267426603654053 + } + } + } + }, + "invert_fake_planar_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "20x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Invert RGBf32 fake-planar comparison (tensor-only, ~1-2ms)." + }, + "baselines": { + "invert_fake_planar_float3_advanced[InOutDataType=float3][shape=20x1080x1920][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2396.14238466532, + "gpu_time_us_python": 2413.32974067096, + "gpu_noise_us_cpp": 1.46, + "gpu_noise_us_python": 2.8939999999999997, + "gpu_bwutil_cpp": 0.8012900118003403, + "gpu_bwutil_python": 0.7955824438429624, + "gpu_gap_stddev_us": 1.638190827806551 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1927.9453501334199, + "gpu_time_us_python": 1941.4903075195598, + "gpu_noise_us_cpp": 1.794, + "gpu_noise_us_python": 2.958, + "gpu_bwutil_cpp": 0.7595686574107271, + "gpu_bwutil_python": 0.7542697939722806, + "gpu_gap_stddev_us": 1.2855257703476977 + } + } + } + }, + "invert_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "48x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Invert RGBA8 interleaved + planar, Tensor (uchar4 planar var-shape unsupported)." + }, + "baselines": { + "invert_uchar4_advanced[InOutDataType=uchar4][shape=48x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 583.84593872676, + "gpu_time_us_python": 592.57744842806, + "gpu_noise_us_cpp": 1.0900000000000003, + "gpu_noise_us_python": 2.416, + "gpu_bwutil_cpp": 0.8769444615091055, + "gpu_bwutil_python": 0.8640261647217885, + "gpu_gap_stddev_us": 0.9315094821872012 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 469.49472577568, + "gpu_time_us_python": 477.62098696486, + "gpu_noise_us_cpp": 1.432, + "gpu_noise_us_python": 1.8880000000000003, + "gpu_bwutil_cpp": 0.831765665528704, + "gpu_bwutil_python": 0.817619724058666, + "gpu_gap_stddev_us": 0.8136000380238729 + } + }, + "invert_uchar4_advanced[InOutDataType=uchar4][shape=48x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 584.2326855329, + "gpu_time_us_python": 592.55697503888, + "gpu_noise_us_cpp": 0.9880000000000001, + "gpu_noise_us_python": 2.168, + "gpu_bwutil_cpp": 0.8763645065843706, + "gpu_bwutil_python": 0.8640547016231246, + "gpu_gap_stddev_us": 0.40945667415250747 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 469.58945930476, + "gpu_time_us_python": 477.5904324831, + "gpu_noise_us_cpp": 1.1540000000000001, + "gpu_noise_us_python": 2.242, + "gpu_bwutil_cpp": 0.8315981960868699, + "gpu_bwutil_python": 0.817674280409592, + "gpu_gap_stddev_us": 0.9537497008549802 + } + } + } + }, + "invert_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Invert RGBAf32 interleaved + planar, Tensor + VarShape (~1-2ms)." + }, + "baselines": { + "invert_float4_advanced[InOutDataType=float4][shape=16x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 770.1192089960599, + "gpu_time_us_python": 779.40111870144, + "gpu_noise_us_cpp": 1.52, + "gpu_noise_us_python": 2.6919999999999997, + "gpu_bwutil_cpp": 0.8864435454436466, + "gpu_bwutil_python": 0.8758920138728339, + "gpu_gap_stddev_us": 1.690198733909267 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 588.9335617003201, + "gpu_time_us_python": 596.29289739884, + "gpu_noise_us_cpp": 1.1939999999999997, + "gpu_noise_us_python": 2.3659999999999997, + "gpu_bwutil_cpp": 0.884104057890033, + "gpu_bwutil_python": 0.8731964137948429, + "gpu_gap_stddev_us": 0.9274467727414106 + } + }, + "invert_float4_advanced[InOutDataType=float4][shape=16x1080x1920][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 856.56475825334, + "gpu_time_us_python": 867.4556908330002, + "gpu_noise_us_cpp": 0.8719999999999999, + "gpu_noise_us_python": 2.9319999999999995, + "gpu_bwutil_cpp": 0.7969828830218753, + "gpu_bwutil_python": 0.7869826003721092, + "gpu_gap_stddev_us": 2.2831832108775316 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 695.1442154889799, + "gpu_time_us_python": 706.2693541188999, + "gpu_noise_us_cpp": 1.462, + "gpu_noise_us_python": 3.4219999999999997, + "gpu_bwutil_cpp": 0.7490261134519575, + "gpu_bwutil_python": 0.737233951178586, + "gpu_gap_stddev_us": 1.3064252727766874 + } + }, + "invert_float4_advanced[InOutDataType=float4][shape=16x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 765.2279869131601, + "gpu_time_us_python": 773.56960440322, + "gpu_noise_us_cpp": 1.154, + "gpu_noise_us_python": 1.9899999999999998, + "gpu_bwutil_cpp": 0.8921098915856998, + "gpu_bwutil_python": 0.8824910139305511, + "gpu_gap_stddev_us": 0.6468252312213932 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 586.4150819255599, + "gpu_time_us_python": 594.16130801338, + "gpu_noise_us_cpp": 1.502, + "gpu_noise_us_python": 1.97, + "gpu_bwutil_cpp": 0.8879009267544973, + "gpu_bwutil_python": 0.8763269803357406, + "gpu_gap_stddev_us": 0.6012036255396437 + } + }, + "invert_float4_advanced[InOutDataType=float4][shape=16x1080x1920][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 771.4811381665, + "gpu_time_us_python": 782.00316848444, + "gpu_noise_us_cpp": 1.4660000000000002, + "gpu_noise_us_python": 3.146, + "gpu_bwutil_cpp": 0.8848796153599545, + "gpu_bwutil_python": 0.8729737925435703, + "gpu_gap_stddev_us": 0.4870175820692861 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 592.85968146638, + "gpu_time_us_python": 602.496379567, + "gpu_noise_us_cpp": 1.6380000000000003, + "gpu_noise_us_python": 3.052, + "gpu_bwutil_cpp": 0.8782509394063558, + "gpu_bwutil_python": 0.8642095424747422, + "gpu_gap_stddev_us": 1.0456065126474197 + } + } + } + }, + "invert_fake_planar_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "48x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Invert RGBA8 fake-planar comparison (tensor-only, ~1-2ms)." + }, + "baselines": { + "invert_fake_planar_uchar4_advanced[InOutDataType=uchar4][shape=48x1080x1920][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2877.9533531224397, + "gpu_time_us_python": 2897.65868570606, + "gpu_noise_us_cpp": 1.5240000000000002, + "gpu_noise_us_python": 4.595999999999999, + "gpu_bwutil_cpp": 0.5337152761667522, + "gpu_bwutil_python": 0.5300852302660128, + "gpu_gap_stddev_us": 1.804326980635012 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2264.07759846474, + "gpu_time_us_python": 2279.31730437014, + "gpu_noise_us_cpp": 1.1800000000000002, + "gpu_noise_us_python": 3.118, + "gpu_bwutil_cpp": 0.517446320291373, + "gpu_bwutil_python": 0.5139865974958544, + "gpu_gap_stddev_us": 3.1530274641446248 + } + } + } + }, + "invert_fake_planar_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Invert RGBAf32 fake-planar comparison (tensor-only, ~1-2ms)." + }, + "baselines": { + "invert_fake_planar_float4_advanced[InOutDataType=float4][shape=16x1080x1920][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2876.39248733604, + "gpu_time_us_python": 2894.47605472868, + "gpu_noise_us_cpp": 3.154, + "gpu_noise_us_python": 5.718, + "gpu_bwutil_cpp": 0.7120068173325825, + "gpu_bwutil_python": 0.7075575184087123, + "gpu_gap_stddev_us": 1.0774787338795406 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2340.57145608914, + "gpu_time_us_python": 2354.73268678248, + "gpu_noise_us_cpp": 4.4159999999999995, + "gpu_noise_us_python": 6.056, + "gpu_bwutil_cpp": 0.6673729257273262, + "gpu_bwutil_python": 0.6633594410221717, + "gpu_gap_stddev_us": 1.7529718699752883 + } + } + } + }, + "invert_uint16_advanced": { + "tier": "advanced", + "dtypes": [ + "uint16" + ], + "string_axes": { + "shape": [ + "96x1080x1920" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Invert U16 1-channel interleaved, Tensor + VarShape (single-channel lives in advanced per RGB guideline R3)." + }, + "baselines": { + "invert_uint16_advanced[InOutDataType=uint16][shape=96x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 578.6820288004199, + "gpu_time_us_python": 586.981906426, + "gpu_noise_us_cpp": 1.3039999999999998, + "gpu_noise_us_python": 2.732, + "gpu_bwutil_cpp": 0.8847709013007663, + "gpu_bwutil_python": 0.8722635855999605, + "gpu_gap_stddev_us": 0.6880135562942655 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 443.2717672987599, + "gpu_time_us_python": 451.03632728659994, + "gpu_noise_us_cpp": 1.388, + "gpu_noise_us_python": 1.6480000000000001, + "gpu_bwutil_cpp": 0.8809698501157153, + "gpu_bwutil_python": 0.8658056580084397, + "gpu_gap_stddev_us": 0.47919559598382844 + } + }, + "invert_uint16_advanced[InOutDataType=uint16][shape=96x1080x1920][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 608.3532499836801, + "gpu_time_us_python": 620.61887027174, + "gpu_noise_us_cpp": 1.886, + "gpu_noise_us_python": 3.4900000000000007, + "gpu_bwutil_cpp": 0.8416336385141502, + "gpu_bwutil_python": 0.8250084618487487, + "gpu_gap_stddev_us": 0.9766973841843505 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 468.59757365774004, + "gpu_time_us_python": 481.26708267863995, + "gpu_noise_us_cpp": 2.37, + "gpu_noise_us_python": 3.282000000000001, + "gpu_bwutil_cpp": 0.8334088741415494, + "gpu_bwutil_python": 0.811460315456776, + "gpu_gap_stddev_us": 0.529791896386675 + } + } + } + } + } +} diff --git a/bench/config/operators/jointbilateralfilter.json b/bench/config/operators/jointbilateralfilter.json new file mode 100644 index 000000000..d66939e0e --- /dev/null +++ b/bench/config/operators/jointbilateralfilter.json @@ -0,0 +1,1116 @@ +{ + "benchmark": "jointbilateralfilter", + "configs": { + "jointbilateralfilter_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "16x720x1280" + ], + "border": [ + "REFLECT" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "int64_axes": { + "diameter": [ + -1 + ] + }, + "float64_axes": { + "sigmaSpace": [ + 1.2 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "JointBilateralFilter RGB8 (basic)." + }, + "baselines": { + "jointbilateralfilter_uchar3_basic[InOutDataType=uchar3][shape=16x720x1280][border=REFLECT][layout=NHWC][inputKind=Tensor][diameter=-1][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1859.5885273262, + "gpu_time_us_python": 1872.7767773528399, + "gpu_noise_us_cpp": 2.144, + "gpu_noise_us_python": 5.0120000000000005, + "gpu_bwutil_cpp": 0.03059223739601446, + "gpu_bwutil_python": 0.030376770052921942 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1634.59492676488, + "gpu_time_us_python": 1646.23212739558, + "gpu_noise_us_cpp": 1.3239999999999998, + "gpu_noise_us_python": 3.8980000000000006, + "gpu_bwutil_cpp": 0.02654914484576066, + "gpu_bwutil_python": 0.02636119706416374 + } + }, + "jointbilateralfilter_uchar3_basic[InOutDataType=uchar3][shape=16x720x1280][border=REFLECT][layout=NHWC][inputKind=VarShape][diameter=-1][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1902.0257028666, + "gpu_time_us_python": 1915.59602631862, + "gpu_noise_us_cpp": 1.9460000000000002, + "gpu_noise_us_python": 7.9719999999999995, + "gpu_bwutil_cpp": 0.029909741155231002, + "gpu_bwutil_python": 0.029697841813624958 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1684.4908449613602, + "gpu_time_us_python": 1699.0847703005802, + "gpu_noise_us_cpp": 2.012, + "gpu_noise_us_python": 3.174, + "gpu_bwutil_cpp": 0.025762711199722475, + "gpu_bwutil_python": 0.0255416624239933 + } + } + } + }, + "jointbilateralfilter_uchar3_planar_nchw_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "16x720x1280" + ], + "border": [ + "REFLECT" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "int64_axes": { + "diameter": [ + -1 + ] + }, + "float64_axes": { + "sigmaSpace": [ + 1.2 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "JointBilateralFilter RGB8 native planar NCHW (basic)." + }, + "baselines": { + "jointbilateralfilter_uchar3_planar_nchw_basic[InOutDataType=uchar3][shape=16x720x1280][border=REFLECT][layout=NCHW][inputKind=Tensor][diameter=-1][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1608.92584323876, + "gpu_time_us_python": 1621.15715688272, + "gpu_noise_us_cpp": 1.568, + "gpu_noise_us_python": 2.742, + "gpu_bwutil_cpp": 0.0353583671266099, + "gpu_bwutil_python": 0.03509157237264095 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1445.4007724027801, + "gpu_time_us_python": 1455.75881349794, + "gpu_noise_us_cpp": 1.3359999999999999, + "gpu_noise_us_python": 2.434, + "gpu_bwutil_cpp": 0.03002429185078162, + "gpu_bwutil_python": 0.029810340437640297 + } + }, + "jointbilateralfilter_uchar3_planar_nchw_basic[InOutDataType=uchar3][shape=16x720x1280][border=REFLECT][layout=NCHW][inputKind=VarShape][diameter=-1][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1793.75689029688, + "gpu_time_us_python": 1757.84852821506, + "gpu_noise_us_cpp": 4.448, + "gpu_noise_us_python": 5.2299999999999995, + "gpu_bwutil_cpp": 0.03171500222175502, + "gpu_bwutil_python": 0.03236279801702888 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1429.9313077096601, + "gpu_time_us_python": 1403.57594573844, + "gpu_noise_us_cpp": 1.218, + "gpu_noise_us_python": 3.0380000000000003, + "gpu_bwutil_cpp": 0.030348417695993495, + "gpu_bwutil_python": 0.0309182020475745 + } + } + } + }, + "jointbilateralfilter_uchar3_fakeplanar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "16x720x1280" + ], + "border": [ + "REFLECT" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "diameter": [ + -1 + ] + }, + "float64_axes": { + "sigmaSpace": [ + 1.2 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "JointBilateralFilter RGB8 fake-planar NCHW (advanced)." + }, + "baselines": { + "jointbilateralfilter_uchar3_fakeplanar_nchw_advanced[InOutDataType=uchar3][shape=16x720x1280][border=REFLECT][layout=NCHW_FAKE][inputKind=Tensor][diameter=-1][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2156.3110693168796, + "gpu_time_us_python": 2173.33894707806, + "gpu_noise_us_cpp": 3.1079999999999997, + "gpu_noise_us_python": 4.384, + "gpu_bwutil_cpp": 0.02638257623379192, + "gpu_bwutil_python": 0.026175822510949263 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1897.94270437448, + "gpu_time_us_python": 1910.8715670766, + "gpu_noise_us_cpp": 1.512, + "gpu_noise_us_python": 3.2939999999999996, + "gpu_bwutil_cpp": 0.022864595188633343, + "gpu_bwutil_python": 0.02270941745338976 + } + } + } + }, + "jointbilateralfilter_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "16x720x1280" + ], + "border": [ + "REFLECT" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "int64_axes": { + "diameter": [ + -1 + ] + }, + "float64_axes": { + "sigmaSpace": [ + 1.2 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "JointBilateralFilter RGBA8 (advanced)." + }, + "baselines": { + "jointbilateralfilter_uchar4_advanced[InOutDataType=uchar4][shape=16x720x1280][border=REFLECT][layout=NHWC][inputKind=Tensor][diameter=-1][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2020.2859513221, + "gpu_time_us_python": 2033.0040492099201, + "gpu_noise_us_cpp": 1.6, + "gpu_noise_us_python": 2.914, + "gpu_bwutil_cpp": 0.03754518317476606, + "gpu_bwutil_python": 0.03731025447913781 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1791.39608067086, + "gpu_time_us_python": 1802.78106849328, + "gpu_noise_us_cpp": 0.8219999999999998, + "gpu_noise_us_python": 3.664, + "gpu_bwutil_cpp": 0.032300056509135054, + "gpu_bwutil_python": 0.032096043625147064 + } + }, + "jointbilateralfilter_uchar4_advanced[InOutDataType=uchar4][shape=16x720x1280][border=REFLECT][layout=NHWC][inputKind=VarShape][diameter=-1][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2068.29878403682, + "gpu_time_us_python": 2084.85729871042, + "gpu_noise_us_cpp": 1.56, + "gpu_noise_us_python": 3.8659999999999997, + "gpu_bwutil_cpp": 0.03667362920274177, + "gpu_bwutil_python": 0.03638229298397802 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1863.8848493928404, + "gpu_time_us_python": 1878.7547655484402, + "gpu_noise_us_cpp": 2.062, + "gpu_noise_us_python": 4.284000000000001, + "gpu_bwutil_cpp": 0.031043881382845202, + "gpu_bwutil_python": 0.03079743096328012 + } + } + } + }, + "jointbilateralfilter_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "32x720x1280" + ], + "border": [ + "REFLECT" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "int64_axes": { + "diameter": [ + -1 + ] + }, + "float64_axes": { + "sigmaSpace": [ + 1.2 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "JointBilateralFilter single-channel (advanced)." + }, + "baselines": { + "jointbilateralfilter_scalar_advanced[InOutDataType=uint8][shape=32x720x1280][border=REFLECT][layout=NHWC][inputKind=Tensor][diameter=-1][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3014.6192349924795, + "gpu_time_us_python": 3027.68419449582, + "gpu_noise_us_cpp": 2.232, + "gpu_noise_us_python": 3.8340000000000005, + "gpu_bwutil_cpp": 0.012580709645222099, + "gpu_bwutil_python": 0.0125264005978897 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2766.07988827464, + "gpu_time_us_python": 2779.5563369096403, + "gpu_noise_us_cpp": 1.046, + "gpu_noise_us_python": 2.446, + "gpu_bwutil_cpp": 0.01045933853055874, + "gpu_bwutil_python": 0.01040827883417906 + } + }, + "jointbilateralfilter_scalar_advanced[InOutDataType=uint8][shape=32x720x1280][border=REFLECT][layout=NHWC][inputKind=VarShape][diameter=-1][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3095.9084740391, + "gpu_time_us_python": 3113.6957692510805, + "gpu_noise_us_cpp": 2.5840000000000005, + "gpu_noise_us_python": 3.3099999999999996, + "gpu_bwutil_cpp": 0.01225037212708854, + "gpu_bwutil_python": 0.01218039553561394 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2893.88010869818, + "gpu_time_us_python": 2910.2529570801003, + "gpu_noise_us_cpp": 1.714, + "gpu_noise_us_python": 3.2879999999999994, + "gpu_bwutil_cpp": 0.0099974941510031, + "gpu_bwutil_python": 0.009940798333016278 + } + } + } + }, + "jointbilateralfilter_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "8x720x1280" + ], + "border": [ + "REFLECT" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "diameter": [ + -1 + ] + }, + "float64_axes": { + "sigmaSpace": [ + 1.2 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "JointBilateralFilter RGBf32 (basic)." + }, + "baselines": { + "jointbilateralfilter_float3_basic[InOutDataType=float3][shape=8x720x1280][border=REFLECT][layout=NHWC][inputKind=Tensor][diameter=-1][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 928.18544904384, + "gpu_time_us_python": 939.5950039464, + "gpu_noise_us_cpp": 1.4460000000000002, + "gpu_noise_us_python": 2.822, + "gpu_bwutil_cpp": 0.1225809654637571, + "gpu_bwutil_python": 0.12109239610263667 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 828.3848140369801, + "gpu_time_us_python": 839.89719084804, + "gpu_noise_us_cpp": 0.906, + "gpu_noise_us_python": 2.3360000000000003, + "gpu_bwutil_cpp": 0.10477200962675266, + "gpu_bwutil_python": 0.10333052602807627 + } + } + } + }, + "jointbilateralfilter_float3_varshape_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "8x720x1280" + ], + "border": [ + "REFLECT" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "int64_axes": { + "diameter": [ + -1 + ] + }, + "float64_axes": { + "sigmaSpace": [ + 1.2 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "JointBilateralFilter RGBf32 VarShape (advanced)." + }, + "baselines": { + "jointbilateralfilter_float3_varshape_advanced[InOutDataType=float3][shape=8x720x1280][border=REFLECT][layout=NHWC][inputKind=VarShape][diameter=-1][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 922.4334417063601, + "gpu_time_us_python": 937.8804447052, + "gpu_noise_us_cpp": 2.7859999999999996, + "gpu_noise_us_python": 5.279999999999999, + "gpu_bwutil_cpp": 0.12334566444834201, + "gpu_bwutil_python": 0.12131397967932314 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 837.61548727278, + "gpu_time_us_python": 851.06015351156, + "gpu_noise_us_cpp": 0.998, + "gpu_noise_us_python": 2.3899999999999997, + "gpu_bwutil_cpp": 0.10361817321945707, + "gpu_bwutil_python": 0.10197614609756615 + } + } + } + }, + "jointbilateralfilter_float3_planar_nchw_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "8x720x1280" + ], + "border": [ + "REFLECT" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "diameter": [ + -1 + ] + }, + "float64_axes": { + "sigmaSpace": [ + 1.2 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "JointBilateralFilter RGBf32 native planar NCHW (basic)." + }, + "baselines": { + "jointbilateralfilter_float3_planar_nchw_basic[InOutDataType=float3][shape=8x720x1280][border=REFLECT][layout=NCHW][inputKind=Tensor][diameter=-1][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 808.84158335226, + "gpu_time_us_python": 819.2939701164798, + "gpu_noise_us_cpp": 1.8980000000000001, + "gpu_noise_us_python": 1.8980000000000001, + "gpu_bwutil_cpp": 0.14066820375893146, + "gpu_bwutil_python": 0.13887332774488087 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 701.24466253588, + "gpu_time_us_python": 713.00417056086, + "gpu_noise_us_cpp": 0.5780000000000001, + "gpu_noise_us_python": 2.2039999999999997, + "gpu_bwutil_cpp": 0.12376784830650374, + "gpu_bwutil_python": 0.12171930677263061 + } + } + } + }, + "jointbilateralfilter_float3_planar_nchw_varshape_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "8x720x1280" + ], + "border": [ + "REFLECT" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "VarShape" + ] + }, + "int64_axes": { + "diameter": [ + -1 + ] + }, + "float64_axes": { + "sigmaSpace": [ + 1.2 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "JointBilateralFilter RGBf32 native planar NCHW VarShape (advanced)." + }, + "baselines": { + "jointbilateralfilter_float3_planar_nchw_varshape_advanced[InOutDataType=float3][shape=8x720x1280][border=REFLECT][layout=NCHW][inputKind=VarShape][diameter=-1][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 814.1602640967001, + "gpu_time_us_python": 829.20587629432, + "gpu_noise_us_cpp": 2.148, + "gpu_noise_us_python": 3.2960000000000003, + "gpu_bwutil_cpp": 0.1397486554957632, + "gpu_bwutil_python": 0.13721397470429098 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 735.8653472369399, + "gpu_time_us_python": 750.2407889827, + "gpu_noise_us_cpp": 1.0380000000000003, + "gpu_noise_us_python": 3.472, + "gpu_bwutil_cpp": 0.11794484193677193, + "gpu_bwutil_python": 0.11567714814951076 + } + } + } + }, + "jointbilateralfilter_float3_fakeplanar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "8x720x1280" + ], + "border": [ + "REFLECT" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "diameter": [ + -1 + ] + }, + "float64_axes": { + "sigmaSpace": [ + 1.2 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "JointBilateralFilter RGBf32 fake-planar NCHW (advanced)." + }, + "baselines": { + "jointbilateralfilter_float3_fakeplanar_nchw_advanced[InOutDataType=float3][shape=8x720x1280][border=REFLECT][layout=NCHW_FAKE][inputKind=Tensor][diameter=-1][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1191.64820196822, + "gpu_time_us_python": 1207.75328147966, + "gpu_noise_us_cpp": 1.814, + "gpu_noise_us_python": 2.724, + "gpu_bwutil_cpp": 0.09547954783347956, + "gpu_bwutil_python": 0.09420628733920841 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1064.4742000983201, + "gpu_time_us_python": 1078.02420521122, + "gpu_noise_us_cpp": 1.3659999999999999, + "gpu_noise_us_python": 3.2040000000000006, + "gpu_bwutil_cpp": 0.08153127572298866, + "gpu_bwutil_python": 0.08050524605508524 + } + } + } + }, + "jointbilateralfilter_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "8x720x1280" + ], + "border": [ + "REFLECT" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "int64_axes": { + "diameter": [ + -1 + ] + }, + "float64_axes": { + "sigmaSpace": [ + 1.2 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "JointBilateralFilter RGBAf32 (advanced)." + }, + "baselines": { + "jointbilateralfilter_float4_advanced[InOutDataType=float4][shape=8x720x1280][border=REFLECT][layout=NHWC][inputKind=Tensor][diameter=-1][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 994.58492577038, + "gpu_time_us_python": 1006.30801752608, + "gpu_noise_us_cpp": 1.152, + "gpu_noise_us_python": 3.886, + "gpu_bwutil_cpp": 0.15253013629411397, + "gpu_bwutil_python": 0.1507533172069903 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 881.03145448, + "gpu_time_us_python": 891.4326717624201, + "gpu_noise_us_cpp": 1.082, + "gpu_noise_us_python": 2.29, + "gpu_bwutil_cpp": 0.13134918128118417, + "gpu_bwutil_python": 0.12981715788054357 + } + }, + "jointbilateralfilter_float4_advanced[InOutDataType=float4][shape=8x720x1280][border=REFLECT][layout=NHWC][inputKind=VarShape][diameter=-1][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 994.5355716923399, + "gpu_time_us_python": 1007.6920493597001, + "gpu_noise_us_cpp": 1.6780000000000002, + "gpu_noise_us_python": 3.5420000000000003, + "gpu_bwutil_cpp": 0.15253786689373613, + "gpu_bwutil_python": 0.15054618160587782 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 896.9063010424601, + "gpu_time_us_python": 909.7843068034801, + "gpu_noise_us_cpp": 0.9, + "gpu_noise_us_python": 3.54, + "gpu_bwutil_cpp": 0.12902425455816438, + "gpu_bwutil_python": 0.12719818591310345 + } + } + } + }, + "jointbilateralfilter_scalar2_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "16x720x1280" + ], + "border": [ + "REFLECT" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "int64_axes": { + "diameter": [ + -1 + ] + }, + "float64_axes": { + "sigmaSpace": [ + 1.2 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "JointBilateralFilter single-channel scalar2 (advanced)." + }, + "baselines": { + "jointbilateralfilter_scalar2_advanced[InOutDataType=float32][shape=16x720x1280][border=REFLECT][layout=NHWC][inputKind=Tensor][diameter=-1][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1519.3883221649398, + "gpu_time_us_python": 1529.5562818282601, + "gpu_noise_us_cpp": 2.146, + "gpu_noise_us_python": 5.058, + "gpu_bwutil_cpp": 0.04992288652623822, + "gpu_bwutil_python": 0.0495907684228968 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1400.8495746018202, + "gpu_time_us_python": 1412.51469870402, + "gpu_noise_us_cpp": 1.4260000000000002, + "gpu_noise_us_python": 3.3260000000000005, + "gpu_bwutil_cpp": 0.04130573584368768, + "gpu_bwutil_python": 0.04096285914686684 + } + }, + "jointbilateralfilter_scalar2_advanced[InOutDataType=float32][shape=16x720x1280][border=REFLECT][layout=NHWC][inputKind=VarShape][diameter=-1][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1570.8449531810197, + "gpu_time_us_python": 1585.2492136290798, + "gpu_noise_us_cpp": 3.0680000000000005, + "gpu_noise_us_python": 6.62, + "gpu_bwutil_cpp": 0.04828741859108976, + "gpu_bwutil_python": 0.04784865547011632 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1437.3495486297597, + "gpu_time_us_python": 1452.5199495855202, + "gpu_noise_us_cpp": 1.292, + "gpu_noise_us_python": 2.888, + "gpu_bwutil_cpp": 0.04025651074613814, + "gpu_bwutil_python": 0.03983429230449442 + } + } + } + }, + "jointbilateralfilter_auto_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "int64_axes": { + "diameter": [ + -1 + ] + }, + "float64_axes": { + "sigmaSpace": [ + 1.2 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "JointBilateralFilter single-channel auto (advanced)." + }, + "baselines": { + "jointbilateralfilter_auto_1080p_advanced[InOutDataType=float32][shape=16x1080x1920][border=REFLECT][layout=NHWC][inputKind=Tensor][diameter=-1][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3385.52111741654, + "gpu_time_us_python": 3400.29440957144, + "gpu_noise_us_cpp": 6.506, + "gpu_noise_us_python": 7.696000000000001, + "gpu_bwutil_cpp": 0.050410831227241396, + "gpu_bwutil_python": 0.05019178586040464 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3124.3619772718603, + "gpu_time_us_python": 3138.49439289364, + "gpu_noise_us_cpp": 1.8240000000000003, + "gpu_noise_us_python": 3.508, + "gpu_bwutil_cpp": 0.04166992378110446, + "gpu_bwutil_python": 0.04148212472857822 + } + }, + "jointbilateralfilter_auto_1080p_advanced[InOutDataType=float32][shape=16x1080x1920][border=REFLECT][layout=NHWC][inputKind=VarShape][diameter=-1][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3502.4070666895595, + "gpu_time_us_python": 3518.75967824584, + "gpu_noise_us_cpp": 4.176, + "gpu_noise_us_python": 6.892, + "gpu_bwutil_cpp": 0.04872842192839702, + "gpu_bwutil_python": 0.048502091733870656 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3209.82876082254, + "gpu_time_us_python": 3225.44620819782, + "gpu_noise_us_cpp": 1.6280000000000001, + "gpu_noise_us_python": 5.066, + "gpu_bwutil_cpp": 0.040560403987497703, + "gpu_bwutil_python": 0.040363084128840265 + } + }, + "jointbilateralfilter_auto_1080p_advanced[InOutDataType=uint8][shape=16x1080x1920][border=REFLECT][layout=NHWC][inputKind=Tensor][diameter=-1][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3386.99624925042, + "gpu_time_us_python": 3400.6707971159994, + "gpu_noise_us_cpp": 6.556, + "gpu_noise_us_python": 7.843999999999999, + "gpu_bwutil_cpp": 0.01259721261977904, + "gpu_bwutil_python": 0.01254657100695602 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3110.6176555156203, + "gpu_time_us_python": 3123.4484385425803, + "gpu_noise_us_cpp": 2.186, + "gpu_noise_us_python": 5.614, + "gpu_bwutil_cpp": 0.01046351637687768, + "gpu_bwutil_python": 0.01042059635544212 + } + }, + "jointbilateralfilter_auto_1080p_advanced[InOutDataType=uint8][shape=16x1080x1920][border=REFLECT][layout=NHWC][inputKind=VarShape][diameter=-1][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3479.0599193837397, + "gpu_time_us_python": 3496.7466427219797, + "gpu_noise_us_cpp": 5.116, + "gpu_noise_us_python": 6.11, + "gpu_bwutil_cpp": 0.01226386810880076, + "gpu_bwutil_python": 0.012201838554592481 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3254.7879521858204, + "gpu_time_us_python": 3269.57048488204, + "gpu_noise_us_cpp": 2.274, + "gpu_noise_us_python": 6.818, + "gpu_bwutil_cpp": 0.010000038398014641, + "gpu_bwutil_python": 0.00995462847654636 + } + } + } + }, + "jointbilateralfilter_d9_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "4x1080x1920" + ], + "border": [ + "REFLECT" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "int64_axes": { + "diameter": [ + 9 + ] + }, + "float64_axes": { + "sigmaSpace": [ + 1.2 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "JointBilateralFilter single-channel d9 (advanced)." + }, + "baselines": { + "jointbilateralfilter_d9_1080p_advanced[InOutDataType=float32][shape=4x1080x1920][border=REFLECT][layout=NHWC][inputKind=Tensor][diameter=9][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2443.79089411018, + "gpu_time_us_python": 2455.55004933296, + "gpu_noise_us_cpp": 4.845999999999999, + "gpu_noise_us_python": 5.966, + "gpu_bwutil_cpp": 0.01745923732810328, + "gpu_bwutil_python": 0.017375621612229282 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2224.31492751794, + "gpu_time_us_python": 2236.000586743, + "gpu_noise_us_cpp": 1.108, + "gpu_noise_us_python": 4.91, + "gpu_bwutil_cpp": 0.01463276139920828, + "gpu_bwutil_python": 0.01455594846005152 + } + }, + "jointbilateralfilter_d9_1080p_advanced[InOutDataType=float32][shape=4x1080x1920][border=REFLECT][layout=NHWC][inputKind=VarShape][diameter=9][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2508.16512680048, + "gpu_time_us_python": 2525.41489767782, + "gpu_noise_us_cpp": 3.5479999999999996, + "gpu_noise_us_python": 6.968000000000001, + "gpu_bwutil_cpp": 0.017011136326542397, + "gpu_bwutil_python": 0.0168949259973319 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2279.2985400592397, + "gpu_time_us_python": 2293.27206910464, + "gpu_noise_us_cpp": 0.9259999999999999, + "gpu_noise_us_python": 3.0440000000000005, + "gpu_bwutil_cpp": 0.01427973255754258, + "gpu_bwutil_python": 0.014192804549651558 + } + }, + "jointbilateralfilter_d9_1080p_advanced[InOutDataType=uint8][shape=4x1080x1920][border=REFLECT][layout=NHWC][inputKind=Tensor][diameter=9][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2427.6484702842, + "gpu_time_us_python": 2442.1326109506003, + "gpu_noise_us_cpp": 2.388, + "gpu_noise_us_python": 4.86, + "gpu_bwutil_cpp": 0.0043938307727759, + "gpu_bwutil_python": 0.0043677847362305 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2210.9330843508797, + "gpu_time_us_python": 2221.6058724896, + "gpu_noise_us_cpp": 0.984, + "gpu_noise_us_python": 2.314, + "gpu_bwutil_cpp": 0.00368033005537, + "gpu_bwutil_python": 0.0036626636204427796 + } + }, + "jointbilateralfilter_d9_1080p_advanced[InOutDataType=uint8][shape=4x1080x1920][border=REFLECT][layout=NHWC][inputKind=VarShape][diameter=9][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2473.84601746958, + "gpu_time_us_python": 2489.58531511886, + "gpu_noise_us_cpp": 1.242, + "gpu_noise_us_python": 6.124, + "gpu_bwutil_cpp": 0.00431177880820094, + "gpu_bwutil_python": 0.00428451878765646 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2306.41417694278, + "gpu_time_us_python": 2321.4622256911202, + "gpu_noise_us_cpp": 0.9259999999999999, + "gpu_noise_us_python": 4.5200000000000005, + "gpu_bwutil_cpp": 0.0035279634299602595, + "gpu_bwutil_python": 0.00350510146541476 + } + } + } + }, + "jointbilateralfilter_rgb_auto_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "int64_axes": { + "diameter": [ + -1 + ] + }, + "float64_axes": { + "sigmaSpace": [ + 1.2 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "JointBilateralFilter RGB8 auto (advanced)." + }, + "baselines": { + "jointbilateralfilter_rgb_auto_1080p_advanced[InOutDataType=uchar3][shape=16x1080x1920][border=REFLECT][layout=NHWC][inputKind=Tensor][diameter=-1][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4152.30992582972, + "gpu_time_us_python": 4167.081572761899, + "gpu_noise_us_cpp": 8.758, + "gpu_noise_us_python": 11.372, + "gpu_bwutil_cpp": 0.030826225332794277, + "gpu_bwutil_python": 0.030716952186646086 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3652.9003939107197, + "gpu_time_us_python": 3665.8879683398, + "gpu_noise_us_cpp": 1.9159999999999997, + "gpu_noise_us_python": 6.7, + "gpu_bwutil_cpp": 0.026730573839048522, + "gpu_bwutil_python": 0.02663577196875206 + } + }, + "jointbilateralfilter_rgb_auto_1080p_advanced[InOutDataType=uchar3][shape=16x1080x1920][border=REFLECT][layout=NHWC][inputKind=VarShape][diameter=-1][sigmaSpace=1.2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4248.11435634802, + "gpu_time_us_python": 4266.83095996658, + "gpu_noise_us_cpp": 9.446, + "gpu_noise_us_python": 12.712, + "gpu_bwutil_cpp": 0.030131047175869417, + "gpu_bwutil_python": 0.02999886412344304 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3766.6507274496203, + "gpu_time_us_python": 3782.2826393785, + "gpu_noise_us_cpp": 2.66, + "gpu_noise_us_python": 4.872, + "gpu_bwutil_cpp": 0.02592316906967826, + "gpu_bwutil_python": 0.02581599823792502 + } + } + } + } + } +} diff --git a/bench/config/operators/jpegcompressiondistortion.json b/bench/config/operators/jpegcompressiondistortion.json new file mode 100644 index 000000000..1e3490cb5 --- /dev/null +++ b/bench/config/operators/jpegcompressiondistortion.json @@ -0,0 +1,254 @@ +{ + "benchmark": "jpegcompressiondistortion", + "configs": { + "jpegcompressiondistortion_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "JpegCompressionDistortion RGB8 interleaved + planar, Tensor + VarShape (~1-2ms)." + }, + "baselines": { + "jpegcompressiondistortion_uchar3_basic[InOutDataType=uchar3][shape=32x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 969.4108722252801, + "gpu_time_us_python": 981.6273592663802, + "gpu_noise_us_cpp": 4.1979999999999995, + "gpu_noise_us_python": 3.5479999999999996, + "gpu_bwutil_cpp": 0.2640806054661792, + "gpu_bwutil_python": 0.26079416966460034, + "gpu_gap_stddev_us": 1.3675272752331409 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 836.90044547496, + "gpu_time_us_python": 845.8242578052401, + "gpu_noise_us_cpp": 1.53, + "gpu_noise_us_python": 2.254, + "gpu_bwutil_cpp": 0.2333276477339982, + "gpu_bwutil_python": 0.23086581392682276, + "gpu_gap_stddev_us": 1.1924697064183603 + } + }, + "jpegcompressiondistortion_uchar3_basic[InOutDataType=uchar3][shape=32x1080x1920][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1139.2773932787, + "gpu_time_us_python": 1153.6458035276798, + "gpu_noise_us_cpp": 1.5859999999999999, + "gpu_noise_us_python": 2.876, + "gpu_bwutil_cpp": 0.2247090993802609, + "gpu_bwutil_python": 0.22191419976489374, + "gpu_gap_stddev_us": 2.2559955168949735 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 933.8285778734598, + "gpu_time_us_python": 949.9008707470199, + "gpu_noise_us_cpp": 2.538, + "gpu_noise_us_python": 6.662000000000001, + "gpu_bwutil_cpp": 0.20910569938589818, + "gpu_bwutil_python": 0.2055650881941972, + "gpu_gap_stddev_us": 3.5798103516478834 + } + }, + "jpegcompressiondistortion_uchar3_basic[InOutDataType=uchar3][shape=32x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 978.29463801844, + "gpu_time_us_python": 988.97592808426, + "gpu_noise_us_cpp": 1.116, + "gpu_noise_us_python": 2.118, + "gpu_bwutil_cpp": 0.2616813962934709, + "gpu_bwutil_python": 0.25885624535460694, + "gpu_gap_stddev_us": 0.9276926810954835 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 822.95355867418, + "gpu_time_us_python": 830.8514568967199, + "gpu_noise_us_cpp": 0.998, + "gpu_noise_us_python": 3.9699999999999998, + "gpu_bwutil_cpp": 0.23728330584947174, + "gpu_bwutil_python": 0.2350273430621566, + "gpu_gap_stddev_us": 4.330777647861074 + } + }, + "jpegcompressiondistortion_uchar3_basic[InOutDataType=uchar3][shape=32x1080x1920][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1027.60899840916, + "gpu_time_us_python": 1042.1085968858401, + "gpu_noise_us_cpp": 2.5380000000000003, + "gpu_noise_us_python": 3.29, + "gpu_bwutil_cpp": 0.24912757493600513, + "gpu_bwutil_python": 0.2456643148474817, + "gpu_gap_stddev_us": 1.5724403810069323 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 905.6256216573199, + "gpu_time_us_python": 918.08425292212, + "gpu_noise_us_cpp": 2.19, + "gpu_noise_us_python": 4.336, + "gpu_bwutil_cpp": 0.21562163488506564, + "gpu_bwutil_python": 0.2126921594095391, + "gpu_gap_stddev_us": 2.457490336346343 + } + } + } + }, + "jpegcompressiondistortion_uint8_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "JpegCompressionDistortion grayscale (luma-only path), Tensor + VarShape." + }, + "baselines": { + "jpegcompressiondistortion_uint8_advanced[InOutDataType=uint8][shape=32x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1007.7153036605201, + "gpu_time_us_python": 1018.4571689978599, + "gpu_noise_us_cpp": 1.356, + "gpu_noise_us_python": 2.85, + "gpu_bwutil_cpp": 0.08468032316108358, + "gpu_bwutil_python": 0.08378758839897013, + "gpu_gap_stddev_us": 1.2342686969258638 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 919.44856339286, + "gpu_time_us_python": 929.7006863316201, + "gpu_noise_us_cpp": 1.5, + "gpu_noise_us_python": 3.7520000000000002, + "gpu_bwutil_cpp": 0.07079165689520853, + "gpu_bwutil_python": 0.07001066146070331, + "gpu_gap_stddev_us": 0.8829214903870845 + } + }, + "jpegcompressiondistortion_uint8_advanced[InOutDataType=uint8][shape=32x1080x1920][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1082.92155708346, + "gpu_time_us_python": 1096.7910314380001, + "gpu_noise_us_cpp": 2.878, + "gpu_noise_us_python": 3.836, + "gpu_bwutil_cpp": 0.07880123022569943, + "gpu_bwutil_python": 0.0778053419779548, + "gpu_gap_stddev_us": 1.5801844847067328 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 984.0187055066399, + "gpu_time_us_python": 999.1342582416999, + "gpu_noise_us_cpp": 2.5919999999999996, + "gpu_noise_us_python": 4.236, + "gpu_bwutil_cpp": 0.06614538223924572, + "gpu_bwutil_python": 0.06514352489532407, + "gpu_gap_stddev_us": 2.5679966313791467 + } + } + } + }, + "jpegcompressiondistortion_layout_compare_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NCHW", + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "JpegCompressionDistortion RGB8 native-planar vs fake-planar comparison (tensor-only, same shape as the uchar3 config)." + }, + "baselines": { + "jpegcompressiondistortion_layout_compare_uchar3_advanced[InOutDataType=uchar3][shape=32x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 971.0569408506801, + "gpu_time_us_python": 981.58864792712, + "gpu_noise_us_cpp": 2.218, + "gpu_noise_us_python": 2.63, + "gpu_bwutil_cpp": 0.26363161096313514, + "gpu_bwutil_python": 0.2608041509534115, + "gpu_gap_stddev_us": 1.2999269340922361 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 835.20073806914, + "gpu_time_us_python": 846.1355937467599, + "gpu_noise_us_cpp": 1.174, + "gpu_noise_us_python": 4.25, + "gpu_bwutil_cpp": 0.23380503120581095, + "gpu_bwutil_python": 0.23078082518097415, + "gpu_gap_stddev_us": 1.8917286287034032 + } + }, + "jpegcompressiondistortion_layout_compare_uchar3_advanced[InOutDataType=uchar3][shape=32x1080x1920][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2173.45080982258, + "gpu_time_us_python": 2191.9342476497395, + "gpu_noise_us_cpp": 1.1380000000000001, + "gpu_noise_us_python": 4.362, + "gpu_bwutil_cpp": 0.3533555871326456, + "gpu_bwutil_python": 0.3503766306302796, + "gpu_gap_stddev_us": 2.0505606376310808 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1752.8469393261398, + "gpu_time_us_python": 1767.7563099512402, + "gpu_noise_us_cpp": 1.8080000000000003, + "gpu_noise_us_python": 3.226, + "gpu_bwutil_cpp": 0.33419325806683225, + "gpu_bwutil_python": 0.3313718286950985, + "gpu_gap_stddev_us": 2.130613069967876 + } + } + } + } + } +} diff --git a/bench/config/operators/label.json b/bench/config/operators/label.json new file mode 100644 index 000000000..d7bfb7033 --- /dev/null +++ b/bench/config/operators/label.json @@ -0,0 +1,486 @@ +{ + "benchmark": "label", + "configs": { + "label_basic": { + "tier": "basic", + "dtypes": [ + "uint8", + "uint32" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "runChoice": [ + "DEFAULT" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Label single-channel (basic).", + "runChoice_info": "Use DEFAULT for basic label only, or [BG][MIN][MAX][ISLAND][COUNT][STAT][MASK] to enable optional outputs." + }, + "baselines": { + "label_basic[InOutDataType=uint32][shape=32x1080x1920][runChoice=DEFAULT][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1328.867641923, + "gpu_time_us_python": 1341.2789923621401, + "gpu_noise_us_cpp": 2.47, + "gpu_noise_us_python": 3.572, + "gpu_bwutil_cpp": 0.2568614936148888, + "gpu_bwutil_python": 0.25448407839161663, + "gpu_gap_stddev_us": 1.1707890391735312 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1223.3588535039498, + "gpu_time_us_python": 1235.13411990985, + "gpu_noise_us_cpp": 1.9000000000000001, + "gpu_noise_us_python": 6.637499999999999, + "gpu_bwutil_cpp": 0.212810734428693, + "gpu_bwutil_python": 0.21078031858072038, + "gpu_gap_stddev_us": 4.183317928822705 + } + }, + "label_basic[InOutDataType=uint8][shape=32x1080x1920][runChoice=DEFAULT][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1303.84147006284, + "gpu_time_us_python": 1315.4673017051198, + "gpu_noise_us_cpp": 2.964, + "gpu_noise_us_python": 3.0759999999999996, + "gpu_bwutil_cpp": 0.16362128683102817, + "gpu_bwutil_python": 0.16217453247533672, + "gpu_gap_stddev_us": 1.426547306933808 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1270.1056454586749, + "gpu_time_us_python": 1283.3853835204002, + "gpu_noise_us_cpp": 1.165, + "gpu_noise_us_python": 3.1275, + "gpu_bwutil_cpp": 0.12811502117343815, + "gpu_bwutil_python": 0.12679024996775992, + "gpu_gap_stddev_us": 1.7519660374949275 + } + } + } + }, + "label_planar_nchw_basic": { + "tier": "basic", + "dtypes": [ + "uint8", + "uint32" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "runChoice": [ + "DEFAULT" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Label single-channel (basic) (native planar NCHW tensor).", + "runChoice_info": "Use DEFAULT for basic label only, or [BG][MIN][MAX][ISLAND][COUNT][STAT][MASK] to enable optional outputs." + }, + "baselines": { + "label_planar_nchw_basic[InOutDataType=uint32][shape=32x1080x1920][runChoice=DEFAULT][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1328.21213127482, + "gpu_time_us_python": 1339.2548422904201, + "gpu_noise_us_cpp": 1.288, + "gpu_noise_us_python": 3.12, + "gpu_bwutil_cpp": 0.25698806765454474, + "gpu_bwutil_python": 0.2548691646243925, + "gpu_gap_stddev_us": 0.6735651763219787 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1222.8758978306498, + "gpu_time_us_python": 1237.5097981218, + "gpu_noise_us_cpp": 1.1875, + "gpu_noise_us_python": 4.55, + "gpu_bwutil_cpp": 0.21289473974196782, + "gpu_bwutil_python": 0.21037894149485173, + "gpu_gap_stddev_us": 2.122734579294394 + } + }, + "label_planar_nchw_basic[InOutDataType=uint8][shape=32x1080x1920][runChoice=DEFAULT][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1303.49126741848, + "gpu_time_us_python": 1314.27925685954, + "gpu_noise_us_cpp": 2.696, + "gpu_noise_us_python": 3.106, + "gpu_bwutil_cpp": 0.1636650273311525, + "gpu_bwutil_python": 0.16232145741284754, + "gpu_gap_stddev_us": 0.33619639665749584 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1271.65811916205, + "gpu_time_us_python": 1284.785254241825, + "gpu_noise_us_cpp": 2.8575, + "gpu_noise_us_python": 5.6, + "gpu_bwutil_cpp": 0.12796017210886626, + "gpu_bwutil_python": 0.12665032886614364, + "gpu_gap_stddev_us": 2.0650533579210855 + } + } + } + }, + "label_fakeplanar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "runChoice": [ + "DEFAULT" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Label single-channel default path (advanced) (fake planar NCHW_FAKE tensor).", + "runChoice_info": "Use DEFAULT for the advanced default path, or [BG][MIN][MAX][ISLAND][COUNT][STAT][MASK] to enable optional outputs." + }, + "baselines": { + "label_fakeplanar_nchw_advanced[InOutDataType=uint8][shape=32x1080x1920][runChoice=DEFAULT][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1838.20144589642, + "gpu_time_us_python": 1856.00953112236, + "gpu_noise_us_cpp": 2.112, + "gpu_noise_us_python": 4.363999999999999, + "gpu_bwutil_cpp": 0.3481693123493358, + "gpu_bwutil_python": 0.3448278872682511, + "gpu_gap_stddev_us": 1.3618339642531996 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1756.22540981745, + "gpu_time_us_python": 1769.9246781761499, + "gpu_noise_us_cpp": 1.4125, + "gpu_noise_us_python": 3.305, + "gpu_bwutil_cpp": 0.27795566567583496, + "gpu_bwutil_python": 0.2758051706557079, + "gpu_gap_stddev_us": 0.9885967124989141 + } + } + } + }, + "label_threshold_count_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "runChoice": [ + "BGMINMAXCOUNT" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Label single-channel threshold count (advanced)." + }, + "baselines": { + "label_threshold_count_1080p_advanced[InOutDataType=uint8][shape=16x1080x1920][runChoice=BGMINMAXCOUNT][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2399.93047910012, + "gpu_time_us_python": 2426.21383823232, + "gpu_noise_us_cpp": 6.412000000000001, + "gpu_noise_us_python": 6.108, + "gpu_bwutil_cpp": 0.04444646427147174, + "gpu_bwutil_python": 0.043965003871914876, + "gpu_gap_stddev_us": 0.7390597483284708 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2166.635172120425, + "gpu_time_us_python": 2187.9921818715748, + "gpu_noise_us_cpp": 7.1850000000000005, + "gpu_noise_us_python": 9.33, + "gpu_bwutil_cpp": 0.0375497440291349, + "gpu_bwutil_python": 0.03718311825561343, + "gpu_gap_stddev_us": 3.4819685724255804 + } + } + } + }, + "label_threshold_count_planar_nchw_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "runChoice": [ + "BGMINMAXCOUNT" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Label single-channel threshold count (advanced) (native planar NCHW tensor)." + }, + "baselines": { + "label_threshold_count_planar_nchw_1080p_advanced[InOutDataType=uint8][shape=16x1080x1920][runChoice=BGMINMAXCOUNT][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2399.73412784542, + "gpu_time_us_python": 2426.05556789572, + "gpu_noise_us_cpp": 5.4079999999999995, + "gpu_noise_us_python": 6.1259999999999994, + "gpu_bwutil_cpp": 0.04445019596289078, + "gpu_bwutil_python": 0.04396780539311045, + "gpu_gap_stddev_us": 1.0447441210064858 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2166.6706928918497, + "gpu_time_us_python": 2184.8541923190996, + "gpu_noise_us_cpp": 7.155, + "gpu_noise_us_python": 7.995000000000001, + "gpu_bwutil_cpp": 0.037549145900599504, + "gpu_bwutil_python": 0.037236485479124525, + "gpu_gap_stddev_us": 2.1376585561872883 + } + } + } + }, + "label_stats_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "runChoice": [ + "BGCOUNTSTAT" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Label single-channel stats (advanced)." + }, + "baselines": { + "label_stats_1080p_advanced[InOutDataType=uint8][shape=16x1080x1920][runChoice=BGCOUNTSTAT][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2551.16002175266, + "gpu_time_us_python": 2550.20186877988, + "gpu_noise_us_cpp": 3.7640000000000002, + "gpu_noise_us_python": 5.876, + "gpu_bwutil_cpp": 0.041812152197055044, + "gpu_bwutil_python": 0.041827736585437675, + "gpu_gap_stddev_us": 1.2279821192655922 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2398.798161510325, + "gpu_time_us_python": 2414.102680075025, + "gpu_noise_us_cpp": 2.5, + "gpu_noise_us_python": 3.8649999999999998, + "gpu_bwutil_cpp": 0.033915979500193524, + "gpu_bwutil_python": 0.03370089881914992, + "gpu_gap_stddev_us": 1.6927823324979374 + } + } + } + }, + "label_stats_planar_nchw_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "runChoice": [ + "BGCOUNTSTAT" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Label single-channel stats (advanced) (native planar NCHW tensor)." + }, + "baselines": { + "label_stats_planar_nchw_1080p_advanced[InOutDataType=uint8][shape=16x1080x1920][runChoice=BGCOUNTSTAT][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2551.32114423048, + "gpu_time_us_python": 2550.21709945118, + "gpu_noise_us_cpp": 4.2780000000000005, + "gpu_noise_us_python": 3.3840000000000003, + "gpu_bwutil_cpp": 0.0418095299306717, + "gpu_bwutil_python": 0.0418275073642864, + "gpu_gap_stddev_us": 1.0518119521950233 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2399.0756998186, + "gpu_time_us_python": 2412.5474299757248, + "gpu_noise_us_cpp": 2.2275, + "gpu_noise_us_python": 4.569999999999999, + "gpu_bwutil_cpp": 0.03391203212485948, + "gpu_bwutil_python": 0.033722598529281556, + "gpu_gap_stddev_us": 1.0625421277820732 + } + } + } + }, + "label_mask_island_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "runChoice": [ + "BGCOUNTSTATISLANDMASK" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Label single-channel mask island (advanced)." + }, + "baselines": { + "label_mask_island_1080p_advanced[InOutDataType=uint8][shape=16x1080x1920][runChoice=BGCOUNTSTATISLANDMASK][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2798.65520189674, + "gpu_time_us_python": 2817.5959941361, + "gpu_noise_us_cpp": 3.758, + "gpu_noise_us_python": 5.422, + "gpu_bwutil_cpp": 0.03811455313125514, + "gpu_bwutil_python": 0.03785840392900146, + "gpu_gap_stddev_us": 2.4808129402659658 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2646.453661988025, + "gpu_time_us_python": 2664.5869412587244, + "gpu_noise_us_cpp": 2.3249999999999997, + "gpu_noise_us_python": 4.4975000000000005, + "gpu_bwutil_cpp": 0.030742152701473652, + "gpu_bwutil_python": 0.0305327977735468, + "gpu_gap_stddev_us": 2.6685645354546876 + } + } + } + }, + "label_mask_island_planar_nchw_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "runChoice": [ + "BGCOUNTSTATISLANDMASK" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Label single-channel mask island (advanced) (native planar NCHW tensor)." + }, + "baselines": { + "label_mask_island_planar_nchw_1080p_advanced[InOutDataType=uint8][shape=16x1080x1920][runChoice=BGCOUNTSTATISLANDMASK][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2798.5479760024, + "gpu_time_us_python": 2816.7686082122805, + "gpu_noise_us_cpp": 3.9859999999999998, + "gpu_noise_us_python": 4.886, + "gpu_bwutil_cpp": 0.038115989816844095, + "gpu_bwutil_python": 0.03786936645851996, + "gpu_gap_stddev_us": 1.1691498326121288 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2647.723293704875, + "gpu_time_us_python": 2664.485759670975, + "gpu_noise_us_cpp": 2.7325, + "gpu_noise_us_python": 4.5125, + "gpu_bwutil_cpp": 0.030727397913084924, + "gpu_bwutil_python": 0.030533885761955575, + "gpu_gap_stddev_us": 3.0540330850644124 + } + } + } + } + } +} diff --git a/bench/config/operators/laplacian.json b/bench/config/operators/laplacian.json new file mode 100644 index 000000000..b57e46ac9 --- /dev/null +++ b/bench/config/operators/laplacian.json @@ -0,0 +1,1834 @@ +{ + "benchmark": "laplacian", + "configs": { + "laplacian_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "int64_axes": { + "ksize": [ + 1 + ] + }, + "float64_axes": { + "scale": [ + 1.0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Laplacian RGB8 (basic)." + }, + "baselines": { + "laplacian_uchar3_basic[InOutDataType=uchar3][shape=16x1080x1920][border=REFLECT101][layout=NHWC][inputKind=Tensor][ksize=1][scale=1.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1179.20353522498, + "gpu_time_us_python": 1188.9663485844198, + "gpu_noise_us_cpp": 1.9560000000000002, + "gpu_noise_us_python": 3.7840000000000003, + "gpu_bwutil_cpp": 0.10854873749682517, + "gpu_bwutil_python": 0.1076572797575102, + "gpu_gap_stddev_us": 2.1034547153099576 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1088.79288379525, + "gpu_time_us_python": 1098.642064474725, + "gpu_noise_us_cpp": 1.2525, + "gpu_noise_us_python": 3.0225, + "gpu_bwutil_cpp": 0.0896772222654828, + "gpu_bwutil_python": 0.08887208890138511, + "gpu_gap_stddev_us": 0.8676137942038595 + } + }, + "laplacian_uchar3_basic[InOutDataType=uchar3][shape=16x1080x1920][border=REFLECT101][layout=NHWC][inputKind=VarShape][ksize=1][scale=1.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1200.29812219122, + "gpu_time_us_python": 1212.3841431869398, + "gpu_noise_us_cpp": 1.8520000000000003, + "gpu_noise_us_python": 3.3200000000000003, + "gpu_bwutil_cpp": 0.10664097756780636, + "gpu_bwutil_python": 0.10557816579915454, + "gpu_gap_stddev_us": 0.9677333817464943 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1138.976495553625, + "gpu_time_us_python": 1153.0483222563498, + "gpu_noise_us_cpp": 1.3825, + "gpu_noise_us_python": 4.720000000000001, + "gpu_bwutil_cpp": 0.08572593047417244, + "gpu_bwutil_python": 0.08468094279196361, + "gpu_gap_stddev_us": 1.3954029450608572 + } + } + } + }, + "laplacian_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "ksize": [ + 1 + ] + }, + "float64_axes": { + "scale": [ + 1.0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Laplacian RGBf32 (basic)." + }, + "baselines": { + "laplacian_float3_basic[InOutDataType=float3][shape=16x1080x1920][border=REFLECT101][layout=NHWC][inputKind=Tensor][ksize=1][scale=1.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1139.4134728904799, + "gpu_time_us_python": 1149.72402313968, + "gpu_noise_us_cpp": 1.6079999999999999, + "gpu_noise_us_python": 3.5780000000000003, + "gpu_bwutil_cpp": 0.44935740399488744, + "gpu_bwutil_python": 0.4453277596425907, + "gpu_gap_stddev_us": 1.448229353989398 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1051.133175732825, + "gpu_time_us_python": 1061.677610227675, + "gpu_noise_us_cpp": 1.255, + "gpu_noise_us_python": 2.7625, + "gpu_bwutil_cpp": 0.3715594885478164, + "gpu_bwutil_python": 0.3678687681516544, + "gpu_gap_stddev_us": 2.45295084629294 + } + } + } + }, + "laplacian_float3_varshape_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "int64_axes": { + "ksize": [ + 1 + ] + }, + "float64_axes": { + "scale": [ + 1.0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Laplacian RGBf32 VarShape (advanced)." + }, + "baselines": { + "laplacian_float3_varshape_advanced[InOutDataType=float3][shape=16x1080x1920][border=REFLECT101][layout=NHWC][inputKind=VarShape][ksize=1][scale=1.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1179.91036451336, + "gpu_time_us_python": 1191.15293526892, + "gpu_noise_us_cpp": 1.266, + "gpu_noise_us_python": 4.040000000000001, + "gpu_bwutil_cpp": 0.43393543856780054, + "gpu_bwutil_python": 0.4298387662687778, + "gpu_gap_stddev_us": 2.176057431664545 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1114.161512334925, + "gpu_time_us_python": 1126.896141845675, + "gpu_noise_us_cpp": 1.6724999999999999, + "gpu_noise_us_python": 3.2825, + "gpu_bwutil_cpp": 0.35054103920637664, + "gpu_bwutil_python": 0.34657602989151026, + "gpu_gap_stddev_us": 0.8339158706673993 + } + } + } + }, + "laplacian_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "int64_axes": { + "ksize": [ + 1 + ] + }, + "float64_axes": { + "scale": [ + 1.0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Laplacian RGBA8 (advanced)." + }, + "baselines": { + "laplacian_uchar4_advanced[InOutDataType=uchar4][shape=16x1080x1920][border=REFLECT101][layout=NHWC][inputKind=Tensor][ksize=1][scale=1.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1244.29872494938, + "gpu_time_us_python": 1255.18576523162, + "gpu_noise_us_cpp": 1.3980000000000001, + "gpu_noise_us_python": 2.9160000000000004, + "gpu_bwutil_cpp": 0.13715978951598892, + "gpu_bwutil_python": 0.13597030892462342, + "gpu_gap_stddev_us": 1.202385025648871 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1194.44652871315, + "gpu_time_us_python": 1204.5727914424, + "gpu_noise_us_cpp": 1.5975, + "gpu_noise_us_python": 3.1525, + "gpu_bwutil_cpp": 0.10899344445382358, + "gpu_bwutil_python": 0.10807862404786138, + "gpu_gap_stddev_us": 1.1829851440997132 + } + }, + "laplacian_uchar4_advanced[InOutDataType=uchar4][shape=16x1080x1920][border=REFLECT101][layout=NHWC][inputKind=VarShape][ksize=1][scale=1.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1243.0397768911598, + "gpu_time_us_python": 1255.1449186489801, + "gpu_noise_us_cpp": 3.042, + "gpu_noise_us_python": 4.2059999999999995, + "gpu_bwutil_cpp": 0.13729892639013808, + "gpu_bwutil_python": 0.13597456895375756, + "gpu_gap_stddev_us": 1.2989348762048842 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1217.742439977475, + "gpu_time_us_python": 1230.494658161175, + "gpu_noise_us_cpp": 1.57, + "gpu_noise_us_python": 3.9025, + "gpu_bwutil_cpp": 0.10690847763406702, + "gpu_bwutil_python": 0.10580177502065972, + "gpu_gap_stddev_us": 1.5050832775805187 + } + } + } + }, + "laplacian_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "int64_axes": { + "ksize": [ + 1 + ] + }, + "float64_axes": { + "scale": [ + 1.0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Laplacian RGBAf32 (advanced)." + }, + "baselines": { + "laplacian_float4_advanced[InOutDataType=float4][shape=16x1080x1920][border=REFLECT101][layout=NHWC][inputKind=Tensor][ksize=1][scale=1.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1102.91371450726, + "gpu_time_us_python": 1113.0902879330401, + "gpu_noise_us_cpp": 1.534, + "gpu_noise_us_python": 2.904, + "gpu_bwutil_cpp": 0.6189710725447618, + "gpu_bwutil_python": 0.6133109816467739, + "gpu_gap_stddev_us": 0.756733274442682 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1053.3609075844502, + "gpu_time_us_python": 1065.4924761386249, + "gpu_noise_us_cpp": 1.0775, + "gpu_noise_us_python": 3.345, + "gpu_bwutil_cpp": 0.4943644650734153, + "gpu_bwutil_python": 0.48871731473662544, + "gpu_gap_stddev_us": 2.303176185611697 + } + }, + "laplacian_float4_advanced[InOutDataType=float4][shape=16x1080x1920][border=REFLECT101][layout=NHWC][inputKind=VarShape][ksize=1][scale=1.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1163.8681655425, + "gpu_time_us_python": 1175.811887079, + "gpu_noise_us_cpp": 1.508, + "gpu_noise_us_python": 2.354, + "gpu_bwutil_cpp": 0.5865532126144408, + "gpu_bwutil_python": 0.5805941450068242, + "gpu_gap_stddev_us": 1.253249317455479 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1093.1961835478, + "gpu_time_us_python": 1106.238306067525, + "gpu_noise_us_cpp": 4.465000000000001, + "gpu_noise_us_python": 4.24, + "gpu_bwutil_cpp": 0.4763454863925534, + "gpu_bwutil_python": 0.4707351745026438, + "gpu_gap_stddev_us": 2.6204793464242924 + } + } + } + }, + "laplacian_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "int64_axes": { + "ksize": [ + 1 + ] + }, + "float64_axes": { + "scale": [ + 1.0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Laplacian single-channel (advanced)." + }, + "baselines": { + "laplacian_scalar_advanced[InOutDataType=float32][shape=16x1080x1920][border=REFLECT101][layout=NHWC][inputKind=Tensor][ksize=1][scale=1.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1081.40634973506, + "gpu_time_us_python": 1091.69814203226, + "gpu_noise_us_cpp": 1.582, + "gpu_noise_us_python": 2.608, + "gpu_bwutil_cpp": 0.15782012199668397, + "gpu_bwutil_python": 0.1563319621903469, + "gpu_gap_stddev_us": 1.1135168663637882 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1022.2895129697251, + "gpu_time_us_python": 1034.181919878175, + "gpu_noise_us_cpp": 1.3050000000000002, + "gpu_noise_us_python": 4.97, + "gpu_bwutil_cpp": 0.127347461559305, + "gpu_bwutil_python": 0.1258784274180625, + "gpu_gap_stddev_us": 2.0979675200562506 + } + }, + "laplacian_scalar_advanced[InOutDataType=float32][shape=16x1080x1920][border=REFLECT101][layout=NHWC][inputKind=VarShape][ksize=1][scale=1.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1109.8124303578, + "gpu_time_us_python": 1122.17673321656, + "gpu_noise_us_cpp": 2.648, + "gpu_noise_us_python": 2.8520000000000003, + "gpu_bwutil_cpp": 0.1537806224953831, + "gpu_bwutil_python": 0.15208595181639298, + "gpu_gap_stddev_us": 1.6794887857124827 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1056.3597979926499, + "gpu_time_us_python": 1068.6605600877, + "gpu_noise_us_cpp": 1.395, + "gpu_noise_us_python": 2.7224999999999997, + "gpu_bwutil_cpp": 0.12323924749233903, + "gpu_bwutil_python": 0.12182101651909967, + "gpu_gap_stddev_us": 0.38685839015931406 + } + }, + "laplacian_scalar_advanced[InOutDataType=uint8][shape=16x1080x1920][border=REFLECT101][layout=NHWC][inputKind=Tensor][ksize=1][scale=1.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1095.92959652734, + "gpu_time_us_python": 1106.1025690113597, + "gpu_noise_us_cpp": 2.242, + "gpu_noise_us_python": 2.598, + "gpu_bwutil_cpp": 0.0389320479841265, + "gpu_bwutil_python": 0.03857410550967692, + "gpu_gap_stddev_us": 1.260881565104812 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1030.747653078525, + "gpu_time_us_python": 1040.92928051345, + "gpu_noise_us_cpp": 1.0725, + "gpu_noise_us_python": 2.8100000000000005, + "gpu_bwutil_cpp": 0.031575772795850776, + "gpu_bwutil_python": 0.0312668681833698, + "gpu_gap_stddev_us": 0.47805797092532704 + } + }, + "laplacian_scalar_advanced[InOutDataType=uint8][shape=16x1080x1920][border=REFLECT101][layout=NHWC][inputKind=VarShape][ksize=1][scale=1.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1103.5695782749, + "gpu_time_us_python": 1115.29340973114, + "gpu_noise_us_cpp": 1.832, + "gpu_noise_us_python": 3.618, + "gpu_bwutil_cpp": 0.03866275658019256, + "gpu_bwutil_python": 0.0382560760419881, + "gpu_gap_stddev_us": 3.2304811164574057 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1058.23517115055, + "gpu_time_us_python": 1070.6068862054249, + "gpu_noise_us_cpp": 1.9874999999999998, + "gpu_noise_us_python": 4.5875, + "gpu_bwutil_cpp": 0.0307556191397922, + "gpu_bwutil_python": 0.0304004337147457, + "gpu_gap_stddev_us": 0.6301073057239067 + } + } + } + }, + "laplacian_k3_scalar_tensor_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "ksize": [ + 3 + ] + }, + "float64_axes": { + "scale": [ + 1.0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Laplacian single-channel k3 tensor (advanced)." + }, + "baselines": { + "laplacian_k3_scalar_tensor_1080p_advanced[InOutDataType=float32][shape=16x1080x1920][border=REFLECT101][layout=NHWC][inputKind=Tensor][ksize=3][scale=1.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1086.5406248768, + "gpu_time_us_python": 1096.7141376480602, + "gpu_noise_us_cpp": 1.6019999999999999, + "gpu_noise_us_python": 2.614, + "gpu_bwutil_cpp": 0.1570745181526634, + "gpu_bwutil_python": 0.155617039564133, + "gpu_gap_stddev_us": 0.9933776608769201 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1014.904145881175, + "gpu_time_us_python": 1025.583604227075, + "gpu_noise_us_cpp": 1.51, + "gpu_noise_us_python": 5.0675, + "gpu_bwutil_cpp": 0.12827466545846855, + "gpu_bwutil_python": 0.12693212976702758, + "gpu_gap_stddev_us": 3.0359221090205653 + } + }, + "laplacian_k3_scalar_tensor_1080p_advanced[InOutDataType=uint8][shape=16x1080x1920][border=REFLECT101][layout=NHWC][inputKind=Tensor][ksize=3][scale=1.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1101.1459076926399, + "gpu_time_us_python": 1110.95079994478, + "gpu_noise_us_cpp": 2.006, + "gpu_noise_us_python": 2.834, + "gpu_bwutil_cpp": 0.03874763862914322, + "gpu_bwutil_python": 0.0384056125513443, + "gpu_gap_stddev_us": 0.8905894230915461 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1017.0783147156001, + "gpu_time_us_python": 1026.155338231775, + "gpu_noise_us_cpp": 1.8874999999999997, + "gpu_noise_us_python": 2.21, + "gpu_bwutil_cpp": 0.03200014519755383, + "gpu_bwutil_python": 0.03171718796069755, + "gpu_gap_stddev_us": 0.3123493538544585 + } + } + } + }, + "laplacian_k3_scalar_varshape_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "int64_axes": { + "ksize": [ + 3 + ] + }, + "float64_axes": { + "scale": [ + 1.0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Laplacian single-channel k3 varshape (advanced)." + }, + "baselines": { + "laplacian_k3_scalar_varshape_1080p_advanced[InOutDataType=float32][shape=32x1080x1920][border=REFLECT101][layout=NHWC][inputKind=VarShape][ksize=3][scale=1.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2191.14205731764, + "gpu_time_us_python": 2205.40881622576, + "gpu_noise_us_cpp": 2.742, + "gpu_noise_us_python": 3.9219999999999997, + "gpu_bwutil_cpp": 0.15577966985860286, + "gpu_bwutil_python": 0.15477199818964948, + "gpu_gap_stddev_us": 2.0772546690488936 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2089.0471618822003, + "gpu_time_us_python": 2103.638789806925, + "gpu_noise_us_cpp": 2.1325, + "gpu_noise_us_python": 9.37, + "gpu_bwutil_cpp": 0.12463814498568758, + "gpu_bwutil_python": 0.12376997616666238, + "gpu_gap_stddev_us": 3.2384962233539367 + } + }, + "laplacian_k3_scalar_varshape_1080p_advanced[InOutDataType=uint8][shape=32x1080x1920][border=REFLECT101][layout=NHWC][inputKind=VarShape][ksize=3][scale=1.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2199.50350251108, + "gpu_time_us_python": 2213.22080589672, + "gpu_noise_us_cpp": 3.9099999999999993, + "gpu_noise_us_python": 4.562, + "gpu_bwutil_cpp": 0.03879677062075474, + "gpu_bwutil_python": 0.03855636054251484, + "gpu_gap_stddev_us": 1.0224208160638462 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2089.73123834715, + "gpu_time_us_python": 2103.3055964252753, + "gpu_noise_us_cpp": 1.6475, + "gpu_noise_us_python": 3.505, + "gpu_bwutil_cpp": 0.03114944490944902, + "gpu_bwutil_python": 0.030948588294671023, + "gpu_gap_stddev_us": 1.0618384527529787 + } + } + } + }, + "laplacian_k3_rgb_tensor_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "ksize": [ + 3 + ] + }, + "float64_axes": { + "scale": [ + 1.0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Laplacian RGB8 k3 tensor (advanced)." + }, + "baselines": { + "laplacian_k3_rgb_tensor_1080p_advanced[InOutDataType=uchar3][shape=16x1080x1920][border=REFLECT101][layout=NHWC][inputKind=Tensor][ksize=3][scale=1.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1175.68540243338, + "gpu_time_us_python": 1185.73514965714, + "gpu_noise_us_cpp": 2.036, + "gpu_noise_us_python": 2.688, + "gpu_bwutil_cpp": 0.10887325715226673, + "gpu_bwutil_python": 0.10795055622996852, + "gpu_gap_stddev_us": 0.6184384599651946 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1094.820592325625, + "gpu_time_us_python": 1104.2870119203, + "gpu_noise_us_cpp": 1.26, + "gpu_noise_us_python": 2.8499999999999996, + "gpu_bwutil_cpp": 0.08918358118982948, + "gpu_bwutil_python": 0.0884182803474436, + "gpu_gap_stddev_us": 1.28195791889981 + } + } + } + }, + "laplacian_k3_rgb_varshape_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "int64_axes": { + "ksize": [ + 3 + ] + }, + "float64_axes": { + "scale": [ + 1.0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Laplacian RGB8 k3 varshape (advanced)." + }, + "baselines": { + "laplacian_k3_rgb_varshape_1080p_advanced[InOutDataType=uchar3][shape=32x1080x1920][border=REFLECT101][layout=NHWC][inputKind=VarShape][ksize=3][scale=1.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2426.6057516942396, + "gpu_time_us_python": 2442.58954806388, + "gpu_noise_us_cpp": 2.99, + "gpu_noise_us_python": 4.653999999999999, + "gpu_bwutil_cpp": 0.10549786247712452, + "gpu_bwutil_python": 0.10480790259756503, + "gpu_gap_stddev_us": 4.703580041179779 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2272.184846982325, + "gpu_time_us_python": 2286.290282454325, + "gpu_noise_us_cpp": 1.1949999999999998, + "gpu_noise_us_python": 3.3075, + "gpu_bwutil_cpp": 0.08594376406750674, + "gpu_bwutil_python": 0.08541358996527124, + "gpu_gap_stddev_us": 1.5991319131670247 + } + } + } + }, + "laplacian_uchar3_planar_nchw_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "int64_axes": { + "ksize": [ + 1 + ] + }, + "float64_axes": { + "scale": [ + 1.0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Laplacian RGB8 (basic). (planar NCHW)" + }, + "baselines": { + "laplacian_uchar3_planar_nchw_basic[InOutDataType=uchar3][shape=16x1080x1920][border=REFLECT101][layout=NCHW][inputKind=Tensor][ksize=1][scale=1.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1301.3904662612601, + "gpu_time_us_python": 1312.5236840907, + "gpu_noise_us_cpp": 1.5939999999999999, + "gpu_noise_us_python": 2.51, + "gpu_bwutil_cpp": 0.09835689409642362, + "gpu_bwutil_python": 0.0975224810216416, + "gpu_gap_stddev_us": 1.3868610906065402 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1102.390395075525, + "gpu_time_us_python": 1112.831421367225, + "gpu_noise_us_cpp": 1.575, + "gpu_noise_us_python": 3.295, + "gpu_bwutil_cpp": 0.08857002467497707, + "gpu_bwutil_python": 0.08773887514907786, + "gpu_gap_stddev_us": 0.6504224191320866 + } + }, + "laplacian_uchar3_planar_nchw_basic[InOutDataType=uchar3][shape=16x1080x1920][border=REFLECT101][layout=NCHW][inputKind=VarShape][ksize=1][scale=1.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1323.8427195878, + "gpu_time_us_python": 1336.97355782998, + "gpu_noise_us_cpp": 3.132, + "gpu_noise_us_python": 2.906, + "gpu_bwutil_cpp": 0.09668870789525869, + "gpu_bwutil_python": 0.09573907349042127, + "gpu_gap_stddev_us": 1.3400031638144316 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1085.8706046283, + "gpu_time_us_python": 1099.2915305427, + "gpu_noise_us_cpp": 1.5375, + "gpu_noise_us_python": 5.63, + "gpu_bwutil_cpp": 0.08991739393584572, + "gpu_bwutil_python": 0.0888171528605782, + "gpu_gap_stddev_us": 1.6409065978816075 + } + } + } + }, + "laplacian_uchar3_fakeplanar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "ksize": [ + 1 + ] + }, + "float64_axes": { + "scale": [ + 1.0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Laplacian RGB8 (advanced). (fake planar NCHW_FAKE)" + }, + "baselines": { + "laplacian_uchar3_fakeplanar_nchw_advanced[InOutDataType=uchar3][shape=16x1080x1920][border=REFLECT101][layout=NCHW_FAKE][inputKind=Tensor][ksize=1][scale=1.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1784.88254102404, + "gpu_time_us_python": 1801.85569371124, + "gpu_noise_us_cpp": 1.46, + "gpu_noise_us_python": 3.3260000000000005, + "gpu_bwutil_cpp": 0.07171399475508919, + "gpu_bwutil_python": 0.07103841003154385, + "gpu_gap_stddev_us": 3.6568593461616814 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1564.5741361242, + "gpu_time_us_python": 1578.4882530657499, + "gpu_noise_us_cpp": 2.9050000000000002, + "gpu_noise_us_python": 3.5650000000000004, + "gpu_bwutil_cpp": 0.062403303956110025, + "gpu_bwutil_python": 0.06185274981278513, + "gpu_gap_stddev_us": 1.4508321566487896 + } + } + } + }, + "laplacian_float3_planar_nchw_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "ksize": [ + 1 + ] + }, + "float64_axes": { + "scale": [ + 1.0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Laplacian RGBf32 (basic). (planar NCHW)" + }, + "baselines": { + "laplacian_float3_planar_nchw_basic[InOutDataType=float3][shape=16x1080x1920][border=REFLECT101][layout=NCHW][inputKind=Tensor][ksize=1][scale=1.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1231.8842856356998, + "gpu_time_us_python": 1242.4985142917599, + "gpu_noise_us_cpp": 2.412, + "gpu_noise_us_python": 2.464, + "gpu_bwutil_cpp": 0.4156254972605896, + "gpu_bwutil_python": 0.41207441732876193, + "gpu_gap_stddev_us": 0.5102247014808662 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1157.9019542178, + "gpu_time_us_python": 1170.56543977195, + "gpu_noise_us_cpp": 3.7325, + "gpu_noise_us_python": 3.315, + "gpu_bwutil_cpp": 0.3372911547673344, + "gpu_bwutil_python": 0.33363286899585876, + "gpu_gap_stddev_us": 2.351296699651057 + } + } + } + }, + "laplacian_float3_planar_nchw_varshape_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "VarShape" + ] + }, + "int64_axes": { + "ksize": [ + 1 + ] + }, + "float64_axes": { + "scale": [ + 1.0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Laplacian RGBf32 VarShape (advanced). (planar NCHW)" + }, + "baselines": { + "laplacian_float3_planar_nchw_varshape_advanced[InOutDataType=float3][shape=16x1080x1920][border=REFLECT101][layout=NCHW][inputKind=VarShape][ksize=1][scale=1.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1388.80756806268, + "gpu_time_us_python": 1401.74648943504, + "gpu_noise_us_cpp": 1.884, + "gpu_noise_us_python": 3.754, + "gpu_bwutil_cpp": 0.3686635145762835, + "gpu_bwutil_python": 0.365260189719598, + "gpu_gap_stddev_us": 1.362123356026685 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1244.65811382335, + "gpu_time_us_python": 1258.794867485125, + "gpu_noise_us_cpp": 1.17, + "gpu_noise_us_python": 4.7775, + "gpu_bwutil_cpp": 0.31378629768542055, + "gpu_bwutil_python": 0.3102685038475042, + "gpu_gap_stddev_us": 2.163672059638426 + } + } + } + }, + "laplacian_float3_fakeplanar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "ksize": [ + 1 + ] + }, + "float64_axes": { + "scale": [ + 1.0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Laplacian RGBf32 (advanced). (fake planar NCHW_FAKE)" + }, + "baselines": { + "laplacian_float3_fakeplanar_nchw_advanced[InOutDataType=float3][shape=16x1080x1920][border=REFLECT101][layout=NCHW_FAKE][inputKind=Tensor][ksize=1][scale=1.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2480.6124625449, + "gpu_time_us_python": 2497.47093093034, + "gpu_noise_us_cpp": 3.662, + "gpu_noise_us_python": 4.016, + "gpu_bwutil_cpp": 0.2064021038415814, + "gpu_bwutil_python": 0.20500834419275998, + "gpu_gap_stddev_us": 2.11496644814884 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2150.019989547, + "gpu_time_us_python": 2163.3119904121504, + "gpu_noise_us_cpp": 2.0425000000000004, + "gpu_noise_us_python": 3.5625, + "gpu_bwutil_cpp": 0.18163737385090883, + "gpu_bwutil_python": 0.18052146384978465, + "gpu_gap_stddev_us": 1.1844505460201793 + } + } + } + }, + "laplacian_uchar4_planar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "ksize": [ + 1 + ] + }, + "float64_axes": { + "scale": [ + 1.0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Laplacian RGBA8 (advanced). (planar NCHW)" + }, + "baselines": { + "laplacian_uchar4_planar_nchw_advanced[InOutDataType=uchar4][shape=16x1080x1920][border=REFLECT101][layout=NCHW][inputKind=Tensor][ksize=1][scale=1.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1731.22385107234, + "gpu_time_us_python": 1742.87794473422, + "gpu_noise_us_cpp": 2.762, + "gpu_noise_us_python": 4.523999999999999, + "gpu_bwutil_cpp": 0.0985818939529227, + "gpu_bwutil_python": 0.09792272540766327, + "gpu_gap_stddev_us": 3.1140722598493262 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1465.6270697017248, + "gpu_time_us_python": 1475.420818423375, + "gpu_noise_us_cpp": 1.5825, + "gpu_noise_us_python": 4.0225, + "gpu_bwutil_cpp": 0.08882586233022657, + "gpu_bwutil_python": 0.08823456312880461, + "gpu_gap_stddev_us": 1.4605116801338514 + } + } + } + }, + "laplacian_uchar4_fakeplanar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "ksize": [ + 1 + ] + }, + "float64_axes": { + "scale": [ + 1.0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Laplacian RGBA8 (advanced). (fake planar NCHW_FAKE)" + }, + "baselines": { + "laplacian_uchar4_fakeplanar_nchw_advanced[InOutDataType=uchar4][shape=16x1080x1920][border=REFLECT101][layout=NCHW_FAKE][inputKind=Tensor][ksize=1][scale=1.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2023.4990633441, + "gpu_time_us_python": 2039.02046273392, + "gpu_noise_us_cpp": 1.722, + "gpu_noise_us_python": 3.874, + "gpu_bwutil_cpp": 0.08434294945657939, + "gpu_bwutil_python": 0.08370075011443516, + "gpu_gap_stddev_us": 1.980467903651177 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1811.396902807825, + "gpu_time_us_python": 1824.9680361981, + "gpu_noise_us_cpp": 1.1624999999999999, + "gpu_noise_us_python": 5.6, + "gpu_bwutil_cpp": 0.07186692894307051, + "gpu_bwutil_python": 0.07133202783132768, + "gpu_gap_stddev_us": 0.8367238722250542 + } + } + } + }, + "laplacian_float4_planar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "int64_axes": { + "ksize": [ + 1 + ] + }, + "float64_axes": { + "scale": [ + 1.0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Laplacian RGBAf32 (advanced). (planar NCHW)" + }, + "baselines": { + "laplacian_float4_planar_nchw_advanced[InOutDataType=float4][shape=16x1080x1920][border=REFLECT101][layout=NCHW][inputKind=Tensor][ksize=1][scale=1.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1638.5753577051, + "gpu_time_us_python": 1648.96202009072, + "gpu_noise_us_cpp": 1.934, + "gpu_noise_us_python": 2.976, + "gpu_bwutil_cpp": 0.41662443360706697, + "gpu_bwutil_python": 0.41399974009470125, + "gpu_gap_stddev_us": 1.0170898610319565 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1541.6040322188, + "gpu_time_us_python": 1552.356461255725, + "gpu_noise_us_cpp": 1.5599999999999998, + "gpu_noise_us_python": 3.6550000000000002, + "gpu_bwutil_cpp": 0.33779265390012275, + "gpu_bwutil_python": 0.33544380294351533, + "gpu_gap_stddev_us": 2.6056634323686167 + } + }, + "laplacian_float4_planar_nchw_advanced[InOutDataType=float4][shape=16x1080x1920][border=REFLECT101][layout=NCHW][inputKind=VarShape][ksize=1][scale=1.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1848.13363809336, + "gpu_time_us_python": 1860.92294289036, + "gpu_noise_us_cpp": 2.4479999999999995, + "gpu_noise_us_python": 4.008000000000001, + "gpu_bwutil_cpp": 0.3693840460795851, + "gpu_bwutil_python": 0.36684591298706754, + "gpu_gap_stddev_us": 1.3838874903583742 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1631.3893450303249, + "gpu_time_us_python": 1644.5361381918, + "gpu_noise_us_cpp": 2.4675000000000002, + "gpu_noise_us_python": 4.37, + "gpu_bwutil_cpp": 0.31919771453145157, + "gpu_bwutil_python": 0.3166337106898329, + "gpu_gap_stddev_us": 3.7393517630096813 + } + } + } + }, + "laplacian_float4_fakeplanar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "ksize": [ + 1 + ] + }, + "float64_axes": { + "scale": [ + 1.0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Laplacian RGBAf32 (advanced). (fake planar NCHW_FAKE)" + }, + "baselines": { + "laplacian_float4_fakeplanar_nchw_advanced[InOutDataType=float4][shape=16x1080x1920][border=REFLECT101][layout=NCHW_FAKE][inputKind=Tensor][ksize=1][scale=1.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3212.61503728164, + "gpu_time_us_python": 3230.47321759732, + "gpu_noise_us_cpp": 2.8140000000000005, + "gpu_noise_us_python": 4.406, + "gpu_bwutil_cpp": 0.21249748291212311, + "gpu_bwutil_python": 0.21132227154522476, + "gpu_gap_stddev_us": 2.352817321157631 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2805.9230116199, + "gpu_time_us_python": 2820.956172017375, + "gpu_noise_us_cpp": 3.895, + "gpu_noise_us_python": 5.3075, + "gpu_bwutil_cpp": 0.1855674695374096, + "gpu_bwutil_python": 0.18457849016896305, + "gpu_gap_stddev_us": 1.0355830238335055 + } + } + } + }, + "laplacian_scalar_planar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "ksize": [ + 1 + ] + }, + "float64_axes": { + "scale": [ + 1.0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Laplacian single-channel (advanced). (planar NCHW)" + }, + "baselines": { + "laplacian_scalar_planar_nchw_advanced[InOutDataType=float32][shape=16x1080x1920][border=REFLECT101][layout=NCHW][inputKind=Tensor][ksize=1][scale=1.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 419.68721012374, + "gpu_time_us_python": 428.66462376044, + "gpu_noise_us_cpp": 0.89, + "gpu_noise_us_python": 1.7920000000000003, + "gpu_bwutil_cpp": 0.40665509495657776, + "gpu_bwutil_python": 0.3981377021199214, + "gpu_gap_stddev_us": 1.3366041725338969 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 395.884504866075, + "gpu_time_us_python": 406.404905316575, + "gpu_noise_us_cpp": 1.91, + "gpu_noise_us_python": 4.04, + "gpu_bwutil_cpp": 0.3288395965234217, + "gpu_bwutil_python": 0.3203141228163339, + "gpu_gap_stddev_us": 2.480888963054246 + } + }, + "laplacian_scalar_planar_nchw_advanced[InOutDataType=uint8][shape=16x1080x1920][border=REFLECT101][layout=NCHW][inputKind=Tensor][ksize=1][scale=1.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 442.39643632415994, + "gpu_time_us_python": 451.64746937038, + "gpu_noise_us_cpp": 2.178, + "gpu_noise_us_python": 2.284, + "gpu_bwutil_cpp": 0.09644509131578326, + "gpu_bwutil_python": 0.09446937469296282, + "gpu_gap_stddev_us": 0.9901098696918919 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 376.13150138677497, + "gpu_time_us_python": 386.45768030810007, + "gpu_noise_us_cpp": 1.8975, + "gpu_noise_us_python": 3.3375, + "gpu_bwutil_cpp": 0.08652737690155107, + "gpu_bwutil_python": 0.08420878929081062, + "gpu_gap_stddev_us": 2.2298870496111083 + } + } + } + }, + "laplacian_scalar_fakeplanar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "ksize": [ + 1 + ] + }, + "float64_axes": { + "scale": [ + 1.0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Laplacian single-channel (advanced). (fake planar NCHW_FAKE)" + }, + "baselines": { + "laplacian_scalar_fakeplanar_nchw_advanced[InOutDataType=float32][shape=16x1080x1920][border=REFLECT101][layout=NCHW_FAKE][inputKind=Tensor][ksize=1][scale=1.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1521.2366728939, + "gpu_time_us_python": 1536.28280971568, + "gpu_noise_us_cpp": 1.7399999999999998, + "gpu_noise_us_python": 2.58, + "gpu_bwutil_cpp": 0.11219007488687731, + "gpu_bwutil_python": 0.11109101339487207, + "gpu_gap_stddev_us": 2.0761919900293595 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1433.1634326619749, + "gpu_time_us_python": 1447.22934442925, + "gpu_noise_us_cpp": 1.6475000000000002, + "gpu_noise_us_python": 6.220000000000001, + "gpu_bwutil_cpp": 0.0908335514831973, + "gpu_bwutil_python": 0.08995098434400835, + "gpu_gap_stddev_us": 1.0139378265363026 + } + }, + "laplacian_scalar_fakeplanar_nchw_advanced[InOutDataType=uint8][shape=16x1080x1920][border=REFLECT101][layout=NCHW_FAKE][inputKind=Tensor][ksize=1][scale=1.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1193.61216322218, + "gpu_time_us_python": 1210.72817206988, + "gpu_noise_us_cpp": 3.0060000000000002, + "gpu_noise_us_python": 4.991999999999999, + "gpu_bwutil_cpp": 0.0357460088227843, + "gpu_bwutil_python": 0.03524065790570028, + "gpu_gap_stddev_us": 2.367145784541859 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1106.1738236177, + "gpu_time_us_python": 1120.8557177913501, + "gpu_noise_us_cpp": 1.295, + "gpu_noise_us_python": 5.675000000000001, + "gpu_bwutil_cpp": 0.02942217697304, + "gpu_bwutil_python": 0.0290369526340428, + "gpu_gap_stddev_us": 1.5351718131742005 + } + } + } + }, + "laplacian_k3_scalar_tensor_1080p_planar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "ksize": [ + 3 + ] + }, + "float64_axes": { + "scale": [ + 1.0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Laplacian single-channel k3 tensor (advanced). (planar NCHW)" + }, + "baselines": { + "laplacian_k3_scalar_tensor_1080p_planar_nchw_advanced[InOutDataType=float32][shape=16x1080x1920][border=REFLECT101][layout=NCHW][inputKind=Tensor][ksize=3][scale=1.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 421.63062569954, + "gpu_time_us_python": 431.46396730142, + "gpu_noise_us_cpp": 0.818, + "gpu_noise_us_python": 2.844, + "gpu_bwutil_cpp": 0.40478112701573343, + "gpu_bwutil_python": 0.39555308535993, + "gpu_gap_stddev_us": 1.4435321556730125 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 394.9889501698, + "gpu_time_us_python": 405.263641314775, + "gpu_noise_us_cpp": 1.155, + "gpu_noise_us_python": 4.6725, + "gpu_bwutil_cpp": 0.329582750800176, + "gpu_bwutil_python": 0.32121041827528074, + "gpu_gap_stddev_us": 1.4347305929815268 + } + }, + "laplacian_k3_scalar_tensor_1080p_planar_nchw_advanced[InOutDataType=uint8][shape=16x1080x1920][border=REFLECT101][layout=NCHW][inputKind=Tensor][ksize=3][scale=1.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 450.67828009142005, + "gpu_time_us_python": 460.3797357454599, + "gpu_noise_us_cpp": 1.864, + "gpu_noise_us_python": 2.492, + "gpu_bwutil_cpp": 0.09467282681705433, + "gpu_bwutil_python": 0.09267735279238791, + "gpu_gap_stddev_us": 1.455427531698376 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 372.6263505419, + "gpu_time_us_python": 382.22334739685, + "gpu_noise_us_cpp": 1.0999999999999999, + "gpu_noise_us_python": 3.0225, + "gpu_bwutil_cpp": 0.08734011920638368, + "gpu_bwutil_python": 0.08514754194988056, + "gpu_gap_stddev_us": 0.569260777949609 + } + } + } + }, + "laplacian_k3_scalar_tensor_1080p_fakeplanar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "ksize": [ + 3 + ] + }, + "float64_axes": { + "scale": [ + 1.0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Laplacian single-channel k3 tensor (advanced). (fake planar NCHW_FAKE)" + }, + "baselines": { + "laplacian_k3_scalar_tensor_1080p_fakeplanar_nchw_advanced[InOutDataType=float32][shape=16x1080x1920][border=REFLECT101][layout=NCHW_FAKE][inputKind=Tensor][ksize=3][scale=1.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1525.86584957102, + "gpu_time_us_python": 1541.39099102668, + "gpu_noise_us_cpp": 3.5420000000000003, + "gpu_noise_us_python": 2.3899999999999997, + "gpu_bwutil_cpp": 0.1118496147470184, + "gpu_bwutil_python": 0.11072319636755242, + "gpu_gap_stddev_us": 2.279819670277638 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1425.193643626025, + "gpu_time_us_python": 1438.9663261637002, + "gpu_noise_us_cpp": 1.4124999999999999, + "gpu_noise_us_python": 3.5875, + "gpu_bwutil_cpp": 0.0913417374288218, + "gpu_bwutil_python": 0.09046674793515164, + "gpu_gap_stddev_us": 1.3359663843751068 + } + }, + "laplacian_k3_scalar_tensor_1080p_fakeplanar_nchw_advanced[InOutDataType=uint8][shape=16x1080x1920][border=REFLECT101][layout=NCHW_FAKE][inputKind=Tensor][ksize=3][scale=1.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1198.94330535776, + "gpu_time_us_python": 1216.3289313049797, + "gpu_noise_us_cpp": 3.6879999999999997, + "gpu_noise_us_python": 5.023999999999999, + "gpu_bwutil_cpp": 0.03558698137723372, + "gpu_bwutil_python": 0.035078355805065645, + "gpu_gap_stddev_us": 1.6797495934499416 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1092.4662516506, + "gpu_time_us_python": 1107.116896009075, + "gpu_noise_us_cpp": 1.79, + "gpu_noise_us_python": 2.9450000000000003, + "gpu_bwutil_cpp": 0.029791319412611924, + "gpu_bwutil_python": 0.029396623387464498, + "gpu_gap_stddev_us": 0.905700414677782 + } + } + } + }, + "laplacian_k3_rgb_tensor_1080p_planar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "ksize": [ + 3 + ] + }, + "float64_axes": { + "scale": [ + 1.0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Laplacian RGB8 k3 tensor (advanced). (planar NCHW)" + }, + "baselines": { + "laplacian_k3_rgb_tensor_1080p_planar_nchw_advanced[InOutDataType=uchar3][shape=16x1080x1920][border=REFLECT101][layout=NCHW][inputKind=Tensor][ksize=3][scale=1.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1325.0396102837199, + "gpu_time_us_python": 1335.73104774722, + "gpu_noise_us_cpp": 2.9140000000000006, + "gpu_noise_us_python": 3.6419999999999995, + "gpu_bwutil_cpp": 0.09660118044065846, + "gpu_bwutil_python": 0.0958281114337149, + "gpu_gap_stddev_us": 1.6405689952165108 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1092.23570529865, + "gpu_time_us_python": 1102.654999976025, + "gpu_noise_us_cpp": 1.5574999999999999, + "gpu_noise_us_python": 3.5374999999999996, + "gpu_bwutil_cpp": 0.08939366883688228, + "gpu_bwutil_python": 0.08855044835492917, + "gpu_gap_stddev_us": 1.9173279061384105 + } + } + } + }, + "laplacian_k3_rgb_tensor_1080p_fakeplanar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "ksize": [ + 3 + ] + }, + "float64_axes": { + "scale": [ + 1.0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Laplacian RGB8 k3 tensor (advanced). (fake planar NCHW_FAKE)" + }, + "baselines": { + "laplacian_k3_rgb_tensor_1080p_fakeplanar_nchw_advanced[InOutDataType=uchar3][shape=16x1080x1920][border=REFLECT101][layout=NCHW_FAKE][inputKind=Tensor][ksize=3][scale=1.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1781.1400257707198, + "gpu_time_us_python": 1797.20119835166, + "gpu_noise_us_cpp": 2.136, + "gpu_noise_us_python": 2.632, + "gpu_bwutil_cpp": 0.07186452991859173, + "gpu_bwutil_python": 0.07122246656018491, + "gpu_gap_stddev_us": 1.9921720359419366 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1570.13892427675, + "gpu_time_us_python": 1585.6006423341498, + "gpu_noise_us_cpp": 1.17, + "gpu_noise_us_python": 3.9575, + "gpu_bwutil_cpp": 0.06218220429725655, + "gpu_bwutil_python": 0.061575609568092694, + "gpu_gap_stddev_us": 2.6422673328543467 + } + } + } + }, + "laplacian_k3_rgb_varshape_1080p_planar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "VarShape" + ] + }, + "int64_axes": { + "ksize": [ + 3 + ] + }, + "float64_axes": { + "scale": [ + 1.0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Laplacian RGB8 k3 varshape (advanced). (planar NCHW)" + }, + "baselines": { + "laplacian_k3_rgb_varshape_1080p_planar_nchw_advanced[InOutDataType=uchar3][shape=32x1080x1920][border=REFLECT101][layout=NCHW][inputKind=VarShape][ksize=3][scale=1.0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2633.8878177916204, + "gpu_time_us_python": 2648.4800910949, + "gpu_noise_us_cpp": 3.21, + "gpu_noise_us_python": 5.607999999999999, + "gpu_bwutil_cpp": 0.09719527340662608, + "gpu_bwutil_python": 0.09665986620455405, + "gpu_gap_stddev_us": 3.0076923034977945 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2122.6658696202, + "gpu_time_us_python": 2137.5141070566, + "gpu_noise_us_cpp": 2.36, + "gpu_noise_us_python": 4.845000000000001, + "gpu_bwutil_cpp": 0.0919965657712925, + "gpu_bwutil_python": 0.0913571493770564, + "gpu_gap_stddev_us": 0.872091663488934 + } + } + } + } + } +} diff --git a/bench/config/operators/medianblur.json b/bench/config/operators/medianblur.json new file mode 100644 index 000000000..3a1f01062 --- /dev/null +++ b/bench/config/operators/medianblur.json @@ -0,0 +1,1202 @@ +{ + "benchmark": "medianblur", + "configs": { + "medianblur_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "4x720x1280" + ], + "kernelSize": [ + "5x5" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "MedianBlur RGB8 (basic)." + }, + "baselines": { + "medianblur_uchar3_basic[InOutDataType=uchar3][shape=4x720x1280][kernelSize=5x5][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 839.91564585788, + "gpu_time_us_python": 851.1414255873, + "gpu_noise_us_cpp": 1.408, + "gpu_noise_us_python": 2.9939999999999998, + "gpu_bwutil_cpp": 0.01693298775480222, + "gpu_bwutil_python": 0.01670979021558218 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 760.08041861904, + "gpu_time_us_python": 768.97688703328, + "gpu_noise_us_cpp": 0.708, + "gpu_noise_us_python": 2.1740000000000004, + "gpu_bwutil_cpp": 0.01427309739394542, + "gpu_bwutil_python": 0.01410783455858686 + } + }, + "medianblur_uchar3_basic[InOutDataType=uchar3][shape=4x720x1280][kernelSize=5x5][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 823.5403345982, + "gpu_time_us_python": 835.413286318, + "gpu_noise_us_cpp": 1.584, + "gpu_noise_us_python": 3.12, + "gpu_bwutil_cpp": 0.01726978026241322, + "gpu_bwutil_python": 0.0170243123761741 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 767.3993566705201, + "gpu_time_us_python": 777.40241432682, + "gpu_noise_us_cpp": 0.792, + "gpu_noise_us_python": 2.512, + "gpu_bwutil_cpp": 0.014136992393535, + "gpu_bwutil_python": 0.013955091536336759 + } + } + } + }, + "medianblur_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "4x720x1280" + ], + "kernelSize": [ + "5x5" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "MedianBlur RGBA8 (advanced)." + }, + "baselines": { + "medianblur_uchar4_advanced[InOutDataType=uchar4][shape=4x720x1280][kernelSize=5x5][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1117.1274077702599, + "gpu_time_us_python": 1129.21993325884, + "gpu_noise_us_cpp": 1.342, + "gpu_noise_us_python": 3.066, + "gpu_bwutil_cpp": 0.016974806999282423, + "gpu_bwutil_python": 0.016793070798525597 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1011.80881333594, + "gpu_time_us_python": 1022.2798291366, + "gpu_noise_us_cpp": 0.9399999999999998, + "gpu_noise_us_python": 3.1479999999999997, + "gpu_bwutil_cpp": 0.014296127724926899, + "gpu_bwutil_python": 0.014150127220245542 + } + }, + "medianblur_uchar4_advanced[InOutDataType=uchar4][shape=4x720x1280][kernelSize=5x5][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1091.53751336846, + "gpu_time_us_python": 1103.2679722772602, + "gpu_noise_us_cpp": 2.3, + "gpu_noise_us_python": 4.1, + "gpu_bwutil_cpp": 0.01737277509457804, + "gpu_bwutil_python": 0.017188120443062498 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1016.58184750218, + "gpu_time_us_python": 1028.70112523718, + "gpu_noise_us_cpp": 1.2340000000000002, + "gpu_noise_us_python": 3.002, + "gpu_bwutil_cpp": 0.0142289862112958, + "gpu_bwutil_python": 0.014062121760309059 + } + } + } + }, + "medianblur_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "4x720x1280" + ], + "kernelSize": [ + "5x5" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "MedianBlur single-channel (advanced)." + }, + "baselines": { + "medianblur_scalar_advanced[InOutDataType=float32][shape=4x720x1280][kernelSize=5x5][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 415.66202583436007, + "gpu_time_us_python": 425.4315318929201, + "gpu_noise_us_cpp": 0.9360000000000002, + "gpu_noise_us_python": 1.6239999999999999, + "gpu_bwutil_cpp": 0.045621305655302695, + "gpu_bwutil_python": 0.04457453672806244 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 379.97943294803997, + "gpu_time_us_python": 388.02570398802, + "gpu_noise_us_cpp": 1.182, + "gpu_noise_us_python": 2.3539999999999996, + "gpu_bwutil_cpp": 0.038067642712111466, + "gpu_bwutil_python": 0.03727858968651872 + } + }, + "medianblur_scalar_advanced[InOutDataType=float32][shape=4x720x1280][kernelSize=5x5][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 407.94394918628, + "gpu_time_us_python": 419.34086113112, + "gpu_noise_us_cpp": 1.3, + "gpu_noise_us_python": 3.63, + "gpu_bwutil_cpp": 0.04648491313535298, + "gpu_bwutil_python": 0.0452227543313421 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 376.6932147531, + "gpu_time_us_python": 386.53314303354, + "gpu_noise_us_cpp": 1.8820000000000001, + "gpu_noise_us_python": 2.692, + "gpu_bwutil_cpp": 0.0383990832360591, + "gpu_bwutil_python": 0.03742196504102458 + } + } + } + }, + "medianblur_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "2x720x1280" + ], + "kernelSize": [ + "5x5" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "MedianBlur RGBf32 (basic)." + }, + "baselines": { + "medianblur_float3_basic[InOutDataType=float3][shape=2x720x1280][kernelSize=5x5][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 619.4055758185399, + "gpu_time_us_python": 630.67082911376, + "gpu_noise_us_cpp": 0.9199999999999999, + "gpu_noise_us_python": 3.218, + "gpu_bwutil_cpp": 0.04592223184426222, + "gpu_bwutil_python": 0.04510285191360634 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 565.2455202779399, + "gpu_time_us_python": 573.7807058715, + "gpu_noise_us_cpp": 0.4159999999999999, + "gpu_noise_us_python": 1.564, + "gpu_bwutil_cpp": 0.038385780961223456, + "gpu_bwutil_python": 0.03781448756059006 + } + } + } + }, + "medianblur_float3_varshape_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "2x720x1280" + ], + "kernelSize": [ + "5x5" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "MedianBlur RGBf32 VarShape (advanced)." + }, + "baselines": { + "medianblur_float3_varshape_advanced[InOutDataType=float3][shape=2x720x1280][kernelSize=5x5][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 601.292994624, + "gpu_time_us_python": 613.79578678544, + "gpu_noise_us_cpp": 1.8240000000000003, + "gpu_noise_us_python": 2.6980000000000004, + "gpu_bwutil_cpp": 0.04730577561850506, + "gpu_bwutil_python": 0.04634296344619534 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 555.0818507167601, + "gpu_time_us_python": 565.16223124904, + "gpu_noise_us_cpp": 0.932, + "gpu_noise_us_python": 2.34, + "gpu_bwutil_cpp": 0.03908852353099032, + "gpu_bwutil_python": 0.0383910254946889 + } + } + } + }, + "medianblur_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "2x720x1280" + ], + "kernelSize": [ + "5x5" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "MedianBlur RGBAf32 (advanced)." + }, + "baselines": { + "medianblur_float4_advanced[InOutDataType=float4][shape=2x720x1280][kernelSize=5x5][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 821.9389250664601, + "gpu_time_us_python": 832.8873129500201, + "gpu_noise_us_cpp": 0.89, + "gpu_noise_us_python": 2.682, + "gpu_bwutil_cpp": 0.046142192831138164, + "gpu_bwutil_python": 0.04553645934855574 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 751.3523874772801, + "gpu_time_us_python": 759.93699400022, + "gpu_noise_us_cpp": 2.024, + "gpu_noise_us_python": 2.126, + "gpu_bwutil_cpp": 0.03850382783790808, + "gpu_bwutil_python": 0.0380686633794598 + } + }, + "medianblur_float4_advanced[InOutDataType=float4][shape=2x720x1280][kernelSize=5x5][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 794.77214961034, + "gpu_time_us_python": 805.68246253934, + "gpu_noise_us_cpp": 1.53, + "gpu_noise_us_python": 3.2560000000000002, + "gpu_bwutil_cpp": 0.047719454970343814, + "gpu_bwutil_python": 0.04707355508620238 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 734.6850061258401, + "gpu_time_us_python": 745.0252479923199, + "gpu_noise_us_cpp": 1.5660000000000003, + "gpu_noise_us_python": 2.54, + "gpu_bwutil_cpp": 0.03937728643287684, + "gpu_bwutil_python": 0.038830661887281674 + } + } + } + }, + "medianblur_k3_u8_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "kernelSize": [ + "3x3" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "MedianBlur single-channel k3 (advanced)." + }, + "baselines": { + "medianblur_k3_u8_1080p_advanced[InOutDataType=uint8][shape=16x1080x1920][kernelSize=3x3][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 546.14704900172, + "gpu_time_us_python": 556.81341149094, + "gpu_noise_us_cpp": 0.966, + "gpu_noise_us_python": 2.602, + "gpu_bwutil_cpp": 0.07812341716995226, + "gpu_bwutil_python": 0.07662731054286032 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 475.58063991378003, + "gpu_time_us_python": 484.48535742068, + "gpu_noise_us_cpp": 0.9380000000000001, + "gpu_noise_us_python": 2.1, + "gpu_bwutil_cpp": 0.06843432532493518, + "gpu_bwutil_python": 0.0671755178972156 + } + }, + "medianblur_k3_u8_1080p_advanced[InOutDataType=uint8][shape=16x1080x1920][kernelSize=3x3][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 511.58018171992, + "gpu_time_us_python": 523.21271561056, + "gpu_noise_us_cpp": 2.0439999999999996, + "gpu_noise_us_python": 2.98, + "gpu_bwutil_cpp": 0.08340262603334526, + "gpu_bwutil_python": 0.08154954408697368 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 495.93342073258, + "gpu_time_us_python": 507.18528785684003, + "gpu_noise_us_cpp": 0.85, + "gpu_noise_us_python": 2.3920000000000003, + "gpu_bwutil_cpp": 0.06562537147480862, + "gpu_bwutil_python": 0.06416751062083854 + } + } + } + }, + "medianblur_k3_f32_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "8x1080x1920" + ], + "kernelSize": [ + "3x3" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "MedianBlur single-channel k3 (advanced)." + }, + "baselines": { + "medianblur_k3_f32_1080p_advanced[InOutDataType=float32][shape=8x1080x1920][kernelSize=3x3][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 360.94882026228004, + "gpu_time_us_python": 371.11845848502, + "gpu_noise_us_cpp": 0.8099999999999999, + "gpu_noise_us_python": 3.402, + "gpu_bwutil_cpp": 0.23641517420465843, + "gpu_bwutil_python": 0.2299408396275202 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 329.00604687008, + "gpu_time_us_python": 337.4016989342, + "gpu_noise_us_cpp": 1.0539999999999998, + "gpu_noise_us_python": 1.9, + "gpu_bwutil_cpp": 0.19784482300020714, + "gpu_bwutil_python": 0.19291668215340113 + } + }, + "medianblur_k3_f32_1080p_advanced[InOutDataType=float32][shape=8x1080x1920][kernelSize=3x3][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 344.8465382911801, + "gpu_time_us_python": 357.65907396838, + "gpu_noise_us_cpp": 1.9980000000000004, + "gpu_noise_us_python": 2.536, + "gpu_bwutil_cpp": 0.24745931856223985, + "gpu_bwutil_python": 0.2386053933908714 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 330.76031736582, + "gpu_time_us_python": 341.78473568841997, + "gpu_noise_us_cpp": 1.996, + "gpu_noise_us_python": 2.692, + "gpu_bwutil_cpp": 0.19679347807777772, + "gpu_bwutil_python": 0.19046817754716752 + } + } + } + }, + "medianblur_k7_u8_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "2x1080x1920" + ], + "kernelSize": [ + "7x7" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "MedianBlur single-channel k7 (advanced)." + }, + "baselines": { + "medianblur_k7_u8_1080p_advanced[InOutDataType=uint8][shape=2x1080x1920][kernelSize=7x7][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1080.23885109916, + "gpu_time_us_python": 1090.75012479712, + "gpu_noise_us_cpp": 1.154, + "gpu_noise_us_python": 3.55, + "gpu_bwutil_cpp": 0.004937194668356301, + "gpu_bwutil_python": 0.00488963843480254 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1007.1527249485, + "gpu_time_us_python": 1016.1728693847801, + "gpu_noise_us_cpp": 0.52, + "gpu_noise_us_python": 3.218, + "gpu_bwutil_cpp": 0.00403936930953014, + "gpu_bwutil_python": 0.0040035342402627 + } + }, + "medianblur_k7_u8_1080p_advanced[InOutDataType=uint8][shape=2x1080x1920][kernelSize=7x7][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1086.64919536472, + "gpu_time_us_python": 1098.6595295237998, + "gpu_noise_us_cpp": 3.024, + "gpu_noise_us_python": 3.9540000000000006, + "gpu_bwutil_cpp": 0.0049080856665334, + "gpu_bwutil_python": 0.0048544303554556405 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1035.76791902722, + "gpu_time_us_python": 1046.00053418624, + "gpu_noise_us_cpp": 0.8320000000000001, + "gpu_noise_us_python": 2.55, + "gpu_bwutil_cpp": 0.00392775912858466, + "gpu_bwutil_python": 0.00388928409587248 + } + } + } + }, + "medianblur_rgb_u8_k5_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "2x1080x1920" + ], + "kernelSize": [ + "5x5" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "MedianBlur RGB8 k5 (advanced)." + }, + "baselines": { + "medianblur_rgb_u8_k5_1080p_advanced[InOutDataType=uchar3][shape=2x1080x1920][kernelSize=5x5][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 943.22888024358, + "gpu_time_us_python": 953.3132605013201, + "gpu_noise_us_cpp": 1.69, + "gpu_noise_us_python": 2.358, + "gpu_bwutil_cpp": 0.016963066981604203, + "gpu_bwutil_python": 0.01678361599257034 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 855.4284008922999, + "gpu_time_us_python": 864.26229802496, + "gpu_noise_us_cpp": 0.44000000000000006, + "gpu_noise_us_python": 2.0119999999999996, + "gpu_bwutil_cpp": 0.014267476650450538, + "gpu_bwutil_python": 0.014121657391075163 + } + }, + "medianblur_rgb_u8_k5_1080p_advanced[InOutDataType=uchar3][shape=2x1080x1920][kernelSize=5x5][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 923.8927957687599, + "gpu_time_us_python": 935.3149562695, + "gpu_noise_us_cpp": 3.902, + "gpu_noise_us_python": 5.0040000000000004, + "gpu_bwutil_cpp": 0.01731810501011116, + "gpu_bwutil_python": 0.01710678894866198 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 862.98000785844, + "gpu_time_us_python": 873.1339168894799, + "gpu_noise_us_cpp": 1.482, + "gpu_noise_us_python": 2.534, + "gpu_bwutil_cpp": 0.01414278781413236, + "gpu_bwutil_python": 0.01397812233458032 + } + } + } + }, + "medianblur_planar_nchw_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "4x720x1280" + ], + "kernelSize": [ + "5x5" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "MedianBlur RGB8 planar NCHW (basic)." + }, + "baselines": { + "medianblur_planar_nchw_uchar3_basic[InOutDataType=uchar3][shape=4x720x1280][kernelSize=5x5][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 840.5823844161802, + "gpu_time_us_python": 851.5520270032799, + "gpu_noise_us_cpp": 1.908, + "gpu_noise_us_python": 3.2020000000000004, + "gpu_bwutil_cpp": 0.016919552098824082, + "gpu_bwutil_python": 0.01670167314401658 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 761.0622556122801, + "gpu_time_us_python": 769.98661556744, + "gpu_noise_us_cpp": 0.8640000000000001, + "gpu_noise_us_python": 1.8820000000000001, + "gpu_bwutil_cpp": 0.01425466207501568, + "gpu_bwutil_python": 0.014089515475608641 + } + }, + "medianblur_planar_nchw_uchar3_basic[InOutDataType=uchar3][shape=4x720x1280][kernelSize=5x5][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 827.8367090906601, + "gpu_time_us_python": 839.61271301544, + "gpu_noise_us_cpp": 2.6220000000000003, + "gpu_noise_us_python": 3.3379999999999996, + "gpu_bwutil_cpp": 0.01718024924261894, + "gpu_bwutil_python": 0.01693925232766372 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 774.54600754662, + "gpu_time_us_python": 785.9073588215599, + "gpu_noise_us_cpp": 0.5760000000000001, + "gpu_noise_us_python": 3.768, + "gpu_bwutil_cpp": 0.014006457796707842, + "gpu_bwutil_python": 0.01380398565993746 + } + } + } + }, + "medianblur_planar_nchw_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "4x720x1280" + ], + "kernelSize": [ + "5x5" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "MedianBlur RGBA8 planar NCHW tensor-only (advanced); RGBA8p varshape is unsupported by the Python image API." + }, + "baselines": { + "medianblur_planar_nchw_uchar4_advanced[InOutDataType=uchar4][shape=4x720x1280][kernelSize=5x5][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1117.94822100366, + "gpu_time_us_python": 1128.4555746656201, + "gpu_noise_us_cpp": 1.5, + "gpu_noise_us_python": 3.3, + "gpu_bwutil_cpp": 0.01696234141810444, + "gpu_bwutil_python": 0.016804420324155418 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1013.0713356304599, + "gpu_time_us_python": 1023.21210063046, + "gpu_noise_us_cpp": 0.6120000000000001, + "gpu_noise_us_python": 1.9780000000000002, + "gpu_bwutil_cpp": 0.014278301049358058, + "gpu_bwutil_python": 0.0141366784286929 + } + } + } + }, + "medianblur_planar_nchw_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "2x720x1280" + ], + "kernelSize": [ + "5x5" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "MedianBlur RGBf32 planar NCHW (basic)." + }, + "baselines": { + "medianblur_planar_nchw_float3_basic[InOutDataType=float3][shape=2x720x1280][kernelSize=5x5][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 619.3945033452, + "gpu_time_us_python": 630.52772298468, + "gpu_noise_us_cpp": 1.204, + "gpu_noise_us_python": 2.982, + "gpu_bwutil_cpp": 0.045923252318911055, + "gpu_bwutil_python": 0.0451128440406339 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 566.3314037124399, + "gpu_time_us_python": 574.6932697297, + "gpu_noise_us_cpp": 0.634, + "gpu_noise_us_python": 2.6719999999999997, + "gpu_bwutil_cpp": 0.038312148652757505, + "gpu_bwutil_python": 0.0377542055408419 + } + } + } + }, + "medianblur_planar_nchw_float3_varshape_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "2x720x1280" + ], + "kernelSize": [ + "5x5" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "MedianBlur RGBf32 planar NCHW VarShape (advanced)." + }, + "baselines": { + "medianblur_planar_nchw_float3_varshape_advanced[InOutDataType=float3][shape=2x720x1280][kernelSize=5x5][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 598.8798316602799, + "gpu_time_us_python": 611.2521867212799, + "gpu_noise_us_cpp": 2.102, + "gpu_noise_us_python": 3.518, + "gpu_bwutil_cpp": 0.04749673292300176, + "gpu_bwutil_python": 0.046535658681214065 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 558.6299134808801, + "gpu_time_us_python": 568.1090964467, + "gpu_noise_us_cpp": 1.3279999999999998, + "gpu_noise_us_python": 2.232, + "gpu_bwutil_cpp": 0.038840050446871346, + "gpu_bwutil_python": 0.038191483672516416 + } + } + } + }, + "medianblur_planar_nchw_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "2x720x1280" + ], + "kernelSize": [ + "5x5" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "MedianBlur RGBAf32 planar NCHW (advanced)." + }, + "baselines": { + "medianblur_planar_nchw_float4_advanced[InOutDataType=float4][shape=2x720x1280][kernelSize=5x5][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 821.8098152837799, + "gpu_time_us_python": 833.2005149864201, + "gpu_noise_us_cpp": 1.666, + "gpu_noise_us_python": 2.96, + "gpu_bwutil_cpp": 0.04614959507032306, + "gpu_bwutil_python": 0.04551904994130432 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 751.63031517642, + "gpu_time_us_python": 760.28506809806, + "gpu_noise_us_cpp": 0.86, + "gpu_noise_us_python": 1.8099999999999998, + "gpu_bwutil_cpp": 0.03848945847075376, + "gpu_bwutil_python": 0.03805109722969548 + } + }, + "medianblur_planar_nchw_float4_advanced[InOutDataType=float4][shape=2x720x1280][kernelSize=5x5][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 789.98865185578, + "gpu_time_us_python": 803.24983116026, + "gpu_noise_us_cpp": 1.6939999999999997, + "gpu_noise_us_python": 3.632, + "gpu_bwutil_cpp": 0.04800867894196682, + "gpu_bwutil_python": 0.047216468645756675 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 737.9203518197799, + "gpu_time_us_python": 748.4765263520601, + "gpu_noise_us_cpp": 1.226, + "gpu_noise_us_python": 2.96, + "gpu_bwutil_cpp": 0.03920446292098184, + "gpu_bwutil_python": 0.038651351147055486 + } + } + } + }, + "medianblur_planar_nchw_rgb_u8_k5_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "2x1080x1920" + ], + "kernelSize": [ + "5x5" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "MedianBlur RGB8 k5 planar NCHW 1080p (advanced)." + }, + "baselines": { + "medianblur_planar_nchw_rgb_u8_k5_1080p_advanced[InOutDataType=uchar3][shape=2x1080x1920][kernelSize=5x5][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 944.4962693698799, + "gpu_time_us_python": 954.2439109275999, + "gpu_noise_us_cpp": 1.4819999999999998, + "gpu_noise_us_python": 3.222, + "gpu_bwutil_cpp": 0.01694031549035956, + "gpu_bwutil_python": 0.01676728297627942 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 856.0963218691601, + "gpu_time_us_python": 864.80448008094, + "gpu_noise_us_cpp": 1.718, + "gpu_noise_us_python": 2.178, + "gpu_bwutil_cpp": 0.0142563589901644, + "gpu_bwutil_python": 0.014112753936916082 + } + }, + "medianblur_planar_nchw_rgb_u8_k5_1080p_advanced[InOutDataType=uchar3][shape=2x1080x1920][kernelSize=5x5][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 929.0740463198001, + "gpu_time_us_python": 939.5036267094399, + "gpu_noise_us_cpp": 2.9419999999999993, + "gpu_noise_us_python": 2.558, + "gpu_bwutil_cpp": 0.01722153790660294, + "gpu_bwutil_python": 0.01703043430474526 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 869.0813836257399, + "gpu_time_us_python": 880.37920784718, + "gpu_noise_us_cpp": 1.64, + "gpu_noise_us_python": 2.432, + "gpu_bwutil_cpp": 0.01404328830599568, + "gpu_bwutil_python": 0.01386319293210352 + } + } + } + }, + "medianblur_fake_planar_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "4x720x1280" + ], + "kernelSize": [ + "5x5" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "MedianBlur RGB8 fake planar NCHW_FAKE tensor comparison (advanced)." + }, + "baselines": { + "medianblur_fake_planar_uchar3_advanced[InOutDataType=uchar3][shape=4x720x1280][kernelSize=5x5][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 922.34233727992, + "gpu_time_us_python": 938.2256671600801, + "gpu_noise_us_cpp": 1.7239999999999998, + "gpu_noise_us_python": 2.744, + "gpu_bwutil_cpp": 0.04625919162153238, + "gpu_bwutil_python": 0.04547630255266102 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 831.1553158549999, + "gpu_time_us_python": 844.188845654, + "gpu_noise_us_cpp": 1.4540000000000002, + "gpu_noise_us_python": 2.732, + "gpu_bwutil_cpp": 0.039157218614977056, + "gpu_bwutil_python": 0.0385524943806924 + } + } + } + }, + "medianblur_fake_planar_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "4x720x1280" + ], + "kernelSize": [ + "5x5" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "MedianBlur RGBA8 fake planar NCHW_FAKE tensor comparison (advanced)." + }, + "baselines": { + "medianblur_fake_planar_uchar4_advanced[InOutDataType=uchar4][shape=4x720x1280][kernelSize=5x5][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1220.63130303516, + "gpu_time_us_python": 1238.7692636328402, + "gpu_noise_us_cpp": 1.67, + "gpu_noise_us_python": 4.209999999999999, + "gpu_bwutil_cpp": 0.04660630819227068, + "gpu_bwutil_python": 0.04592393259124235 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1097.9700977848001, + "gpu_time_us_python": 1110.62130121156, + "gpu_noise_us_cpp": 1.02, + "gpu_noise_us_python": 5.244, + "gpu_bwutil_cpp": 0.039522357817759764, + "gpu_bwutil_python": 0.03907213386143934 + } + } + } + }, + "medianblur_fake_planar_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "2x720x1280" + ], + "kernelSize": [ + "5x5" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "MedianBlur RGBf32 fake planar NCHW_FAKE tensor comparison (advanced)." + }, + "baselines": { + "medianblur_fake_planar_float3_advanced[InOutDataType=float3][shape=2x720x1280][kernelSize=5x5][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 702.3419722978, + "gpu_time_us_python": 718.50178082832, + "gpu_noise_us_cpp": 1.0639999999999998, + "gpu_noise_us_python": 11.882, + "gpu_bwutil_cpp": 0.12149879601779583, + "gpu_bwutil_python": 0.11876628873323099 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 632.7176125846601, + "gpu_time_us_python": 644.59403372832, + "gpu_noise_us_cpp": 0.9680000000000002, + "gpu_noise_us_python": 3.8299999999999996, + "gpu_bwutil_cpp": 0.10287474424763443, + "gpu_bwutil_python": 0.10097833300177736 + } + } + } + }, + "medianblur_fake_planar_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "2x720x1280" + ], + "kernelSize": [ + "5x5" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "MedianBlur RGBAf32 fake planar NCHW_FAKE tensor comparison (advanced)." + }, + "baselines": { + "medianblur_fake_planar_float4_advanced[InOutDataType=float4][shape=2x720x1280][kernelSize=5x5][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 948.71190704872, + "gpu_time_us_python": 966.14830663584, + "gpu_noise_us_cpp": 2.7960000000000003, + "gpu_noise_us_python": 2.5999999999999996, + "gpu_bwutil_cpp": 0.11992913238859967, + "gpu_bwutil_python": 0.11776460403544542 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 854.45581179336, + "gpu_time_us_python": 867.7732298062601, + "gpu_noise_us_cpp": 1.1099999999999999, + "gpu_noise_us_python": 3.004, + "gpu_bwutil_cpp": 0.10157042000134872, + "gpu_bwutil_python": 0.10001044552756928 + } + } + } + }, + "medianblur_fake_planar_rgb_u8_k5_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "2x1080x1920" + ], + "kernelSize": [ + "5x5" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "MedianBlur RGB8 k5 fake planar NCHW_FAKE tensor comparison 1080p (advanced)." + }, + "baselines": { + "medianblur_fake_planar_rgb_u8_k5_1080p_advanced[InOutDataType=uchar3][shape=2x1080x1920][kernelSize=5x5][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1035.94845307014, + "gpu_time_us_python": 1052.7596771945398, + "gpu_noise_us_cpp": 1.7280000000000002, + "gpu_noise_us_python": 3.478, + "gpu_bwutil_cpp": 0.04633451407982662, + "gpu_bwutil_python": 0.045594663709523066 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 934.17858437496, + "gpu_time_us_python": 946.92496570524, + "gpu_noise_us_cpp": 1.5739999999999998, + "gpu_noise_us_python": 1.9380000000000002, + "gpu_bwutil_cpp": 0.039193728217911845, + "gpu_bwutil_python": 0.03866587942877232 + } + } + } + } + } +} diff --git a/bench/config/operators/minarearect.json b/bench/config/operators/minarearect.json new file mode 100644 index 000000000..6deab5ffe --- /dev/null +++ b/bench/config/operators/minarearect.json @@ -0,0 +1,192 @@ +{ + "benchmark": "minarearect", + "configs": { + "minarearect_basic": { + "tier": "basic", + "dtypes": [ + "int16" + ], + "string_axes": { + "shape": [ + "4096x1024" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "numPoints": [ + 0 + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "MinAreaRect single-channel (basic)." + }, + "baselines": { + "minarearect_basic[InOutDataType=int16][shape=4096x1024][inputKind=Tensor][numPoints=0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1567.2012868523202, + "gpu_time_us_python": 1575.35294731008, + "gpu_noise_us_cpp": 2.638, + "gpu_noise_us_python": 2.44, + "gpu_bwutil_cpp": 0.00350225559331096, + "gpu_bwutil_python": 0.00348414695196196, + "gpu_gap_stddev_us": 2.1460077794130283 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1581.6976505738598, + "gpu_time_us_python": 1596.08378549556, + "gpu_noise_us_cpp": 2.306, + "gpu_noise_us_python": 3.522, + "gpu_bwutil_cpp": 0.0026467953302707, + "gpu_bwutil_python": 0.00262290927108638, + "gpu_gap_stddev_us": 2.620973610146576 + } + } + } + }, + "minarearect_points128_advanced": { + "tier": "advanced", + "dtypes": [ + "uint16", + "int32" + ], + "string_axes": { + "shape": [ + "8192x128" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "numPoints": [ + 128 + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "MinAreaRect single-channel points128 (advanced)." + }, + "baselines": { + "minarearect_points128_advanced[InOutDataType=int32][shape=8192x128][inputKind=Tensor][numPoints=128]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2308.6690067151, + "gpu_time_us_python": 2313.79439663078, + "gpu_noise_us_cpp": 3.396, + "gpu_noise_us_python": 3.658, + "gpu_bwutil_cpp": 0.00125032484371904, + "gpu_bwutil_python": 0.0012475559124260599, + "gpu_gap_stddev_us": 1.7360530620434194 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2349.5681825084803, + "gpu_time_us_python": 2358.34539580692, + "gpu_noise_us_cpp": 5.601999999999999, + "gpu_noise_us_python": 6.098000000000001, + "gpu_bwutil_cpp": 0.0009370371692945, + "gpu_bwutil_python": 0.00093355060940194, + "gpu_gap_stddev_us": 2.935916542973137 + } + }, + "minarearect_points128_advanced[InOutDataType=uint16][shape=8192x128][inputKind=Tensor][numPoints=128]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2164.26816315484, + "gpu_time_us_python": 2179.8395119542397, + "gpu_noise_us_cpp": 5.074, + "gpu_noise_us_python": 3.224, + "gpu_bwutil_cpp": 0.00071068266908448, + "gpu_bwutil_python": 0.00070560648139634, + "gpu_gap_stddev_us": 0.8934267369944672 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2356.0393516147806, + "gpu_time_us_python": 2357.4805680583595, + "gpu_noise_us_cpp": 5.858, + "gpu_noise_us_python": 5.3420000000000005, + "gpu_bwutil_cpp": 0.00049792580537238, + "gpu_bwutil_python": 0.00049762156902022, + "gpu_gap_stddev_us": 0.8459258686206044 + } + } + } + }, + "minarearect_points512_advanced": { + "tier": "advanced", + "dtypes": [ + "uint16", + "int32" + ], + "string_axes": { + "shape": [ + "2048x512" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "numPoints": [ + 512 + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "MinAreaRect single-channel points512 (advanced)." + }, + "baselines": { + "minarearect_points512_advanced[InOutDataType=int32][shape=2048x512][inputKind=Tensor][numPoints=512]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2419.6293335694, + "gpu_time_us_python": 2438.2474695114997, + "gpu_noise_us_cpp": 4.698, + "gpu_noise_us_python": 20.436, + "gpu_bwutil_cpp": 0.00113420839604064, + "gpu_bwutil_python": 0.00112555531255224, + "gpu_gap_stddev_us": 5.981008270263927 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2454.4829387068594, + "gpu_time_us_python": 2483.94072161004, + "gpu_noise_us_cpp": 10.624, + "gpu_noise_us_python": 11.086000000000002, + "gpu_bwutil_cpp": 0.0008529813202201199, + "gpu_bwutil_python": 0.00084286174024984, + "gpu_gap_stddev_us": 3.985936776451177 + } + }, + "minarearect_points512_advanced[InOutDataType=uint16][shape=2048x512][inputKind=Tensor][numPoints=512]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2407.08504093866, + "gpu_time_us_python": 2408.5400482454797, + "gpu_noise_us_cpp": 4.496, + "gpu_noise_us_python": 5.676, + "gpu_bwutil_cpp": 0.0005799072294494199, + "gpu_bwutil_python": 0.0005795575401112999, + "gpu_gap_stddev_us": 2.1559865326270153 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2455.2365276564, + "gpu_time_us_python": 2473.9756471362803, + "gpu_noise_us_cpp": 9.872, + "gpu_noise_us_python": 10.486, + "gpu_bwutil_cpp": 0.00043371159478156003, + "gpu_bwutil_python": 0.00043044557035600006, + "gpu_gap_stddev_us": 6.957151451372472 + } + } + } + } + } +} diff --git a/bench/config/operators/minmaxloc.json b/bench/config/operators/minmaxloc.json new file mode 100644 index 000000000..8d854f2da --- /dev/null +++ b/bench/config/operators/minmaxloc.json @@ -0,0 +1,643 @@ +{ + "benchmark": "minmaxloc", + "configs": { + "minmaxloc_uint8_basic": { + "tier": "basic", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "192x1080x1920" + ], + "runChoice": [ + "MIN_MAX" + ], + "inputKind": [ + "Tensor", + "VarShape" + ], + "layout": [ + "NHWC" + ] + }, + "int64_axes": { + "maxLocations": [ + 100000 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "MinMaxLoc single-channel (basic)." + }, + "baselines": { + "minmaxloc_uint8_basic[InOutDataType=uint8][shape=192x1080x1920][runChoice=MIN_MAX][inputKind=Tensor][layout=NHWC][maxLocations=100000]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1759.8986195188402, + "gpu_time_us_python": 1774.31641079162, + "gpu_noise_us_cpp": 1.78, + "gpu_noise_us_python": 4.632000000000001, + "gpu_bwutil_cpp": 0.4031680012770451, + "gpu_bwutil_python": 0.399892136057909, + "gpu_gap_stddev_us": 1.23602962230288 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1332.855220474575, + "gpu_time_us_python": 1347.911171094375, + "gpu_noise_us_cpp": 1.3424999999999998, + "gpu_noise_us_python": 4.237500000000001, + "gpu_bwutil_cpp": 0.40604412478491714, + "gpu_bwutil_python": 0.40152130405776615, + "gpu_gap_stddev_us": 4.065494336134873 + } + }, + "minmaxloc_uint8_basic[InOutDataType=uint8][shape=192x1080x1920][runChoice=MIN_MAX][inputKind=VarShape][layout=NHWC][maxLocations=100000]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1561.6947856831998, + "gpu_time_us_python": 1579.0592936323999, + "gpu_noise_us_cpp": 1.588, + "gpu_noise_us_python": 4.644, + "gpu_bwutil_cpp": 0.4543367239370985, + "gpu_bwutil_python": 0.4493404450741906, + "gpu_gap_stddev_us": 1.7148769830655357 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1354.1666668538498, + "gpu_time_us_python": 1371.951826047925, + "gpu_noise_us_cpp": 1.8324999999999998, + "gpu_noise_us_python": 8.17, + "gpu_bwutil_cpp": 0.3996571203230355, + "gpu_bwutil_python": 0.39448280872436303, + "gpu_gap_stddev_us": 6.008023054916054 + } + } + } + }, + "minmaxloc_uint32_basic": { + "tier": "basic", + "dtypes": [ + "uint32" + ], + "string_axes": { + "shape": [ + "96x1080x1920" + ], + "runChoice": [ + "MIN_MAX" + ], + "inputKind": [ + "Tensor", + "VarShape" + ], + "layout": [ + "NHWC" + ] + }, + "int64_axes": { + "maxLocations": [ + 100000 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "MinMaxLoc single-channel (basic)." + }, + "baselines": { + "minmaxloc_uint32_basic[InOutDataType=uint32][shape=96x1080x1920][runChoice=MIN_MAX][inputKind=Tensor][layout=NHWC][maxLocations=100000]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1432.3407613209001, + "gpu_time_us_python": 1444.6177898985602, + "gpu_noise_us_cpp": 2.546, + "gpu_noise_us_python": 4.226, + "gpu_bwutil_cpp": 0.7838684931780527, + "gpu_bwutil_python": 0.7772070196483006, + "gpu_gap_stddev_us": 0.8423436626883631 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1002.9065787842501, + "gpu_time_us_python": 1016.23313115915, + "gpu_noise_us_cpp": 1.425, + "gpu_noise_us_python": 5.0874999999999995, + "gpu_bwutil_cpp": 0.8538736959197673, + "gpu_bwutil_python": 0.8426786047109299, + "gpu_gap_stddev_us": 2.2713813244696484 + } + }, + "minmaxloc_uint32_basic[InOutDataType=uint32][shape=96x1080x1920][runChoice=MIN_MAX][inputKind=VarShape][layout=NHWC][maxLocations=100000]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1458.0057435257, + "gpu_time_us_python": 1475.27437037236, + "gpu_noise_us_cpp": 1.812, + "gpu_noise_us_python": 5.118, + "gpu_bwutil_cpp": 0.7700713073078436, + "gpu_bwutil_python": 0.7610576383578366, + "gpu_gap_stddev_us": 3.176609098838498 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1052.12023883015, + "gpu_time_us_python": 1071.2634111879252, + "gpu_noise_us_cpp": 3.1475, + "gpu_noise_us_python": 4.645, + "gpu_bwutil_cpp": 0.8139287957504309, + "gpu_bwutil_python": 0.7993888748040694, + "gpu_gap_stddev_us": 1.9893620481983707 + } + } + } + }, + "minmaxloc_u8_minmax_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "512x1080x1920" + ], + "runChoice": [ + "MIN_MAX" + ], + "inputKind": [ + "Tensor", + "VarShape" + ], + "layout": [ + "NHWC" + ] + }, + "int64_axes": { + "maxLocations": [ + 1024 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "MinMaxLoc single-channel minmax (advanced)." + }, + "baselines": { + "minmaxloc_u8_minmax_1080p_advanced[InOutDataType=uint8][shape=512x1080x1920][runChoice=MIN_MAX][inputKind=Tensor][layout=NHWC][maxLocations=1024]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3588.1281491687796, + "gpu_time_us_python": 3601.8936729430598, + "gpu_noise_us_cpp": 2.654, + "gpu_noise_us_python": 2.75, + "gpu_bwutil_cpp": 0.38202039393440357, + "gpu_bwutil_python": 0.38056062552447206, + "gpu_gap_stddev_us": 0.9823871875508818 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2556.4550467248, + "gpu_time_us_python": 2570.1159860495745, + "gpu_noise_us_cpp": 1.2474999999999998, + "gpu_noise_us_python": 3.5425, + "gpu_bwutil_cpp": 0.40897116331222094, + "gpu_bwutil_python": 0.40679607257164235, + "gpu_gap_stddev_us": 1.2496847561245024 + } + }, + "minmaxloc_u8_minmax_1080p_advanced[InOutDataType=uint8][shape=512x1080x1920][runChoice=MIN_MAX][inputKind=VarShape][layout=NHWC][maxLocations=1024]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3242.96161947426, + "gpu_time_us_python": 3263.6529420877, + "gpu_noise_us_cpp": 2.3760000000000003, + "gpu_noise_us_python": 4.26, + "gpu_bwutil_cpp": 0.4226806333507277, + "gpu_bwutil_python": 0.42000136336049937, + "gpu_gap_stddev_us": 1.3307551267209927 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2628.8055783694003, + "gpu_time_us_python": 2655.48693350385, + "gpu_noise_us_cpp": 1.9675000000000002, + "gpu_noise_us_python": 4.5925, + "gpu_bwutil_cpp": 0.3977152961903118, + "gpu_bwutil_python": 0.393722344398649, + "gpu_gap_stddev_us": 3.3720497128997264 + } + } + } + }, + "minmaxloc_u8_min_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "512x1080x1920" + ], + "runChoice": [ + "MIN" + ], + "inputKind": [ + "Tensor", + "VarShape" + ], + "layout": [ + "NHWC" + ] + }, + "int64_axes": { + "maxLocations": [ + 1024 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "MinMaxLoc single-channel min (advanced)." + }, + "baselines": { + "minmaxloc_u8_min_1080p_advanced[InOutDataType=uint8][shape=512x1080x1920][runChoice=MIN][inputKind=Tensor][layout=NHWC][maxLocations=1024]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2765.88033843822, + "gpu_time_us_python": 2778.18446196038, + "gpu_noise_us_cpp": 1.468, + "gpu_noise_us_python": 3.088, + "gpu_bwutil_cpp": 0.49461176385617867, + "gpu_bwutil_python": 0.49242081022816836, + "gpu_gap_stddev_us": 1.1386563545754012 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2209.8428491584, + "gpu_time_us_python": 2224.022642289225, + "gpu_noise_us_cpp": 1.9675, + "gpu_noise_us_python": 7.82, + "gpu_bwutil_cpp": 0.47217792802510283, + "gpu_bwutil_python": 0.46916632220854787, + "gpu_gap_stddev_us": 2.179036822448952 + } + }, + "minmaxloc_u8_min_1080p_advanced[InOutDataType=uint8][shape=512x1080x1920][runChoice=MIN][inputKind=VarShape][layout=NHWC][maxLocations=1024]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2853.16890044642, + "gpu_time_us_python": 2870.7407309841597, + "gpu_noise_us_cpp": 2.224, + "gpu_noise_us_python": 3.016, + "gpu_bwutil_cpp": 0.47948011526023865, + "gpu_bwutil_python": 0.4765445634127243, + "gpu_gap_stddev_us": 2.4391407139910446 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2282.369972484975, + "gpu_time_us_python": 2307.050171134025, + "gpu_noise_us_cpp": 2.63, + "gpu_noise_us_python": 5.9775, + "gpu_bwutil_cpp": 0.45717329832250464, + "gpu_bwutil_python": 0.4522794335199731, + "gpu_gap_stddev_us": 2.685378471635955 + } + } + } + }, + "minmaxloc_u8_max_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "512x1080x1920" + ], + "runChoice": [ + "MAX" + ], + "inputKind": [ + "Tensor", + "VarShape" + ], + "layout": [ + "NHWC" + ] + }, + "int64_axes": { + "maxLocations": [ + 1024 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "MinMaxLoc single-channel max (advanced)." + }, + "baselines": { + "minmaxloc_u8_max_1080p_advanced[InOutDataType=uint8][shape=512x1080x1920][runChoice=MAX][inputKind=Tensor][layout=NHWC][maxLocations=1024]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2764.42872665734, + "gpu_time_us_python": 2776.82480619934, + "gpu_noise_us_cpp": 2.35, + "gpu_noise_us_python": 3.194, + "gpu_bwutil_cpp": 0.4948717306680469, + "gpu_bwutil_python": 0.4926624232598694, + "gpu_gap_stddev_us": 0.6064980246295231 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2209.689485572575, + "gpu_time_us_python": 2226.6128233314503, + "gpu_noise_us_cpp": 4.720000000000001, + "gpu_noise_us_python": 7.6925, + "gpu_bwutil_cpp": 0.4722116701914144, + "gpu_bwutil_python": 0.46862502443426973, + "gpu_gap_stddev_us": 4.8540360011488115 + } + }, + "minmaxloc_u8_max_1080p_advanced[InOutDataType=uint8][shape=512x1080x1920][runChoice=MAX][inputKind=VarShape][layout=NHWC][maxLocations=1024]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2853.0734815380397, + "gpu_time_us_python": 2871.66322834682, + "gpu_noise_us_cpp": 2.844, + "gpu_noise_us_python": 3.528, + "gpu_bwutil_cpp": 0.4794961740409464, + "gpu_bwutil_python": 0.47639218459934385, + "gpu_gap_stddev_us": 2.173253839206619 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2287.6619358287, + "gpu_time_us_python": 2309.717458563, + "gpu_noise_us_cpp": 1.94, + "gpu_noise_us_python": 4.9325, + "gpu_bwutil_cpp": 0.4561193130849844, + "gpu_bwutil_python": 0.45176377652184946, + "gpu_gap_stddev_us": 2.2955083110085766 + } + } + } + }, + "minmaxloc_s32_minmax_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "int32" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "runChoice": [ + "MIN_MAX" + ], + "inputKind": [ + "Tensor", + "VarShape" + ], + "layout": [ + "NHWC" + ] + }, + "int64_axes": { + "maxLocations": [ + 1024 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "MinMaxLoc single-channel minmax (advanced)." + }, + "baselines": { + "minmaxloc_s32_minmax_1080p_advanced[InOutDataType=int32][shape=128x1080x1920][runChoice=MIN_MAX][inputKind=Tensor][layout=NHWC][maxLocations=1024]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1892.6137612278003, + "gpu_time_us_python": 1906.22096531314, + "gpu_noise_us_cpp": 1.372, + "gpu_noise_us_python": 2.664, + "gpu_bwutil_cpp": 0.7221144168383165, + "gpu_bwutil_python": 0.7169595405075012, + "gpu_gap_stddev_us": 1.1373003342793666 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1323.2379610891, + "gpu_time_us_python": 1337.599378472075, + "gpu_noise_us_cpp": 1.5074999999999998, + "gpu_noise_us_python": 4.01, + "gpu_bwutil_cpp": 0.7877625032199524, + "gpu_bwutil_python": 0.7792973222936694, + "gpu_gap_stddev_us": 3.328536706220923 + } + }, + "minmaxloc_s32_minmax_1080p_advanced[InOutDataType=int32][shape=128x1080x1920][runChoice=MIN_MAX][inputKind=VarShape][layout=NHWC][maxLocations=1024]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1909.0722916028, + "gpu_time_us_python": 1947.58292630656, + "gpu_noise_us_cpp": 2.056, + "gpu_noise_us_python": 3.3880000000000003, + "gpu_bwutil_cpp": 0.7158889592697777, + "gpu_bwutil_python": 0.7017332795140894, + "gpu_gap_stddev_us": 2.038731122323606 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1390.5368183985502, + "gpu_time_us_python": 1412.2502813072251, + "gpu_noise_us_cpp": 2.3075, + "gpu_noise_us_python": 5.0175, + "gpu_bwutil_cpp": 0.7496345898429486, + "gpu_bwutil_python": 0.7381066128147692, + "gpu_gap_stddev_us": 7.1889986251525695 + } + } + } + }, + "minmaxloc_f32_minmax_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "runChoice": [ + "MIN_MAX" + ], + "inputKind": [ + "Tensor", + "VarShape" + ], + "layout": [ + "NHWC" + ] + }, + "int64_axes": { + "maxLocations": [ + 1024 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "MinMaxLoc single-channel minmax (advanced)." + }, + "baselines": { + "minmaxloc_f32_minmax_1080p_advanced[InOutDataType=float32][shape=128x1080x1920][runChoice=MIN_MAX][inputKind=Tensor][layout=NHWC][maxLocations=1024]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1897.3494527917599, + "gpu_time_us_python": 1910.6808196497, + "gpu_noise_us_cpp": 1.1400000000000001, + "gpu_noise_us_python": 3.268, + "gpu_bwutil_cpp": 0.7203121199679317, + "gpu_bwutil_python": 0.7152861507182829, + "gpu_gap_stddev_us": 1.4131014451321982 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1330.9370533587748, + "gpu_time_us_python": 1348.18374899855, + "gpu_noise_us_cpp": 3.32, + "gpu_noise_us_python": 5.3125, + "gpu_bwutil_cpp": 0.7832071287718796, + "gpu_bwutil_python": 0.7731926095508409, + "gpu_gap_stddev_us": 1.6924090792444222 + } + }, + "minmaxloc_f32_minmax_1080p_advanced[InOutDataType=float32][shape=128x1080x1920][runChoice=MIN_MAX][inputKind=VarShape][layout=NHWC][maxLocations=1024]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1913.43235095942, + "gpu_time_us_python": 1952.1831139950402, + "gpu_noise_us_cpp": 2.224, + "gpu_noise_us_python": 3.41, + "gpu_bwutil_cpp": 0.7142582888202379, + "gpu_bwutil_python": 0.7000796299321752, + "gpu_gap_stddev_us": 1.8585129458457263 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1395.201166898575, + "gpu_time_us_python": 1416.084971805025, + "gpu_noise_us_cpp": 2.1574999999999998, + "gpu_noise_us_python": 6.812499999999999, + "gpu_bwutil_cpp": 0.747127615389299, + "gpu_bwutil_python": 0.7361053303395587, + "gpu_gap_stddev_us": 5.2192200934019946 + } + } + } + }, + "minmaxloc_uint8_planar_basic": { + "tier": "basic", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "192x1080x1920" + ], + "runChoice": [ + "MIN_MAX" + ], + "inputKind": [ + "Tensor" + ], + "layout": [ + "NCHW" + ] + }, + "int64_axes": { + "maxLocations": [ + 100000 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "MinMaxLoc single-channel native-planar coverage (basic)." + }, + "baselines": { + "minmaxloc_uint8_planar_basic[InOutDataType=uint8][shape=192x1080x1920][runChoice=MIN_MAX][inputKind=Tensor][layout=NCHW][maxLocations=100000]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1759.59416849554, + "gpu_time_us_python": 1773.5004420868602, + "gpu_noise_us_cpp": 1.64, + "gpu_noise_us_python": 4.448, + "gpu_bwutil_cpp": 0.4032379810583714, + "gpu_bwutil_python": 0.4000762678403437, + "gpu_gap_stddev_us": 1.3890185085710938 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1333.2747560158498, + "gpu_time_us_python": 1345.624719710975, + "gpu_noise_us_cpp": 1.41, + "gpu_noise_us_python": 6.6475, + "gpu_bwutil_cpp": 0.4059184243860533, + "gpu_bwutil_python": 0.40218460811446494, + "gpu_gap_stddev_us": 4.956969119783395 + } + } + } + }, + "minmaxloc_uint8_fake_planar_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "192x1080x1920" + ], + "runChoice": [ + "MIN_MAX" + ], + "inputKind": [ + "Tensor" + ], + "layout": [ + "NCHW_FAKE" + ] + }, + "int64_axes": { + "maxLocations": [ + 100000 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "MinMaxLoc single-channel fake-planar reference (advanced)." + }, + "baselines": { + "minmaxloc_uint8_fake_planar_advanced[InOutDataType=uint8][shape=192x1080x1920][runChoice=MIN_MAX][inputKind=Tensor][layout=NCHW_FAKE][maxLocations=100000]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2332.99815257384, + "gpu_time_us_python": 2347.63387787002, + "gpu_noise_us_cpp": 3.25, + "gpu_noise_us_python": 3.8299999999999996, + "gpu_bwutil_cpp": 0.5235901714264044, + "gpu_bwutil_python": 0.5203261958683201, + "gpu_gap_stddev_us": 0.67988363630804 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1764.524457658425, + "gpu_time_us_python": 1776.1039650582998, + "gpu_noise_us_cpp": 1.71, + "gpu_noise_us_python": 5.04, + "gpu_bwutil_cpp": 0.5280243667063026, + "gpu_bwutil_python": 0.524582831704152, + "gpu_gap_stddev_us": 4.483118595592478 + } + } + } + } + } +} diff --git a/bench/config/operators/morphology.json b/bench/config/operators/morphology.json new file mode 100644 index 000000000..134328a1d --- /dev/null +++ b/bench/config/operators/morphology.json @@ -0,0 +1,3193 @@ +{ + "benchmark": "morphology", + "configs": { + "morphology_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "kernelSize": [ + "3x3" + ], + "morphType": [ + "DILATE" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "iteration": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Morphology RGB8 DILATE tensor (basic)." + }, + "baselines": { + "morphology_uchar3_basic[InOutDataType=uchar3][shape=32x1080x1920][kernelSize=3x3][morphType=DILATE][border=REPLICATE][layout=NHWC][inputKind=Tensor][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1513.5356742458603, + "gpu_time_us_python": 1526.4584567761399, + "gpu_noise_us_cpp": 1.4380000000000002, + "gpu_noise_us_python": 3.428, + "gpu_bwutil_cpp": 0.1691407185585359, + "gpu_bwutil_python": 0.1677092246219126 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1324.30540214276, + "gpu_time_us_python": 1335.2835216461801, + "gpu_noise_us_cpp": 1.066, + "gpu_noise_us_python": 2.514, + "gpu_bwutil_cpp": 0.14756703393936774, + "gpu_bwutil_python": 0.1463437227940502 + } + } + } + }, + "morphology_uchar3_varshape_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "kernelSize": [ + "3x3" + ], + "morphType": [ + "DILATE" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "int64_axes": { + "iteration": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Morphology RGB8 DILATE var-shape (advanced)." + }, + "baselines": { + "morphology_uchar3_varshape_advanced[InOutDataType=uchar3][shape=32x1080x1920][kernelSize=3x3][morphType=DILATE][border=REPLICATE][layout=NHWC][inputKind=VarShape][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1721.29910580096, + "gpu_time_us_python": 1735.566322011, + "gpu_noise_us_cpp": 1.244, + "gpu_noise_us_python": 5.984, + "gpu_bwutil_cpp": 0.14872521161262223, + "gpu_bwutil_python": 0.14750380883012149 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1494.31072903826, + "gpu_time_us_python": 1508.1034587494, + "gpu_noise_us_cpp": 1.2919999999999998, + "gpu_noise_us_python": 5.3740000000000006, + "gpu_bwutil_cpp": 0.13077257166376827, + "gpu_bwutil_python": 0.12956493922263318 + } + } + } + }, + "morphology_uchar3_planar_nchw_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "kernelSize": [ + "3x3" + ], + "morphType": [ + "DILATE" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "iteration": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Morphology planar RGB8 DILATE tensor (basic)." + }, + "baselines": { + "morphology_uchar3_planar_nchw_basic[InOutDataType=uchar3][shape=32x1080x1920][kernelSize=3x3][morphType=DILATE][border=REPLICATE][layout=NCHW][inputKind=Tensor][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1715.9546592463398, + "gpu_time_us_python": 1729.25634581464, + "gpu_noise_us_cpp": 2.87, + "gpu_noise_us_python": 4.109999999999999, + "gpu_bwutil_cpp": 0.14918817316794425, + "gpu_bwutil_python": 0.1480414861316633 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1399.1965169137402, + "gpu_time_us_python": 1410.79855202578, + "gpu_noise_us_cpp": 1.012, + "gpu_noise_us_python": 2.3520000000000003, + "gpu_bwutil_cpp": 0.1395944711141878, + "gpu_bwutil_python": 0.13844274981590537 + } + } + } + }, + "morphology_uchar3_planar_nchw_varshape_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "kernelSize": [ + "3x3" + ], + "morphType": [ + "DILATE" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "VarShape" + ] + }, + "int64_axes": { + "iteration": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Morphology planar RGB8 DILATE var-shape (advanced)." + }, + "baselines": { + "morphology_uchar3_planar_nchw_varshape_advanced[InOutDataType=uchar3][shape=32x1080x1920][kernelSize=3x3][morphType=DILATE][border=REPLICATE][layout=NCHW][inputKind=VarShape][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1955.9887930750203, + "gpu_time_us_python": 1970.63416337926, + "gpu_noise_us_cpp": 1.5779999999999998, + "gpu_noise_us_python": 4.186, + "gpu_bwutil_cpp": 0.13088020944293047, + "gpu_bwutil_python": 0.12990804454754354 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1696.9500118890999, + "gpu_time_us_python": 1713.03242551328, + "gpu_noise_us_cpp": 1.5539999999999998, + "gpu_noise_us_python": 6.868, + "gpu_bwutil_cpp": 0.11516772362189469, + "gpu_bwutil_python": 0.11407626995392066 + } + } + } + }, + "morphology_uchar3_fakeplanar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "kernelSize": [ + "3x3" + ], + "morphType": [ + "DILATE" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "iteration": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Morphology RGB8 (advanced) (fake-planar NCHW_FAKE tensor comparison)." + }, + "baselines": { + "morphology_uchar3_fakeplanar_nchw_advanced[InOutDataType=uchar3][shape=32x1080x1920][kernelSize=3x3][morphType=DILATE][border=REPLICATE][layout=NCHW_FAKE][inputKind=Tensor][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2852.9687550934395, + "gpu_time_us_python": 2870.88257968732, + "gpu_noise_us_cpp": 1.3279999999999998, + "gpu_noise_us_python": 4.7299999999999995, + "gpu_bwutil_cpp": 0.2691934693530495, + "gpu_bwutil_python": 0.2675136910528854 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2483.19856011628, + "gpu_time_us_python": 2495.98580602964, + "gpu_noise_us_cpp": 1.1179999999999999, + "gpu_noise_us_python": 2.406, + "gpu_bwutil_cpp": 0.2359921518213227, + "gpu_bwutil_python": 0.23478114187497878 + } + } + } + }, + "morphology_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "kernelSize": [ + "3x3" + ], + "morphType": [ + "DILATE" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "iteration": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Morphology RGBf32 DILATE tensor (basic)." + }, + "baselines": { + "morphology_float3_basic[InOutDataType=float3][shape=32x1080x1920][kernelSize=3x3][morphType=DILATE][border=REPLICATE][layout=NHWC][inputKind=Tensor][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1611.61766678856, + "gpu_time_us_python": 1624.42674984824, + "gpu_noise_us_cpp": 1.044, + "gpu_noise_us_python": 3.3979999999999997, + "gpu_bwutil_cpp": 0.6353869533781235, + "gpu_bwutil_python": 0.6303783258491361 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1402.3293622649603, + "gpu_time_us_python": 1413.09299381358, + "gpu_noise_us_cpp": 1.074, + "gpu_noise_us_python": 3.104, + "gpu_bwutil_cpp": 0.5574211249029484, + "gpu_bwutil_python": 0.5531639591250641 + } + } + } + }, + "morphology_float3_varshape_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "kernelSize": [ + "3x3" + ], + "morphType": [ + "DILATE" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "int64_axes": { + "iteration": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Morphology RGBf32 DILATE var-shape (advanced)." + }, + "baselines": { + "morphology_float3_varshape_advanced[InOutDataType=float3][shape=32x1080x1920][kernelSize=3x3][morphType=DILATE][border=REPLICATE][layout=NHWC][inputKind=VarShape][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1786.16750337548, + "gpu_time_us_python": 1801.7358570328602, + "gpu_noise_us_cpp": 1.652, + "gpu_noise_us_python": 3.1120000000000005, + "gpu_bwutil_cpp": 0.5732956124674464, + "gpu_bwutil_python": 0.568343109830377 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1592.0302504587798, + "gpu_time_us_python": 1605.5057035637, + "gpu_noise_us_cpp": 1.05, + "gpu_noise_us_python": 2.516, + "gpu_bwutil_cpp": 0.4910181594528358, + "gpu_bwutil_python": 0.4868813597250009 + } + } + } + }, + "morphology_float3_planar_nchw_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "kernelSize": [ + "3x3" + ], + "morphType": [ + "DILATE" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "iteration": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Morphology planar RGBf32 DILATE tensor (basic)." + }, + "baselines": { + "morphology_float3_planar_nchw_basic[InOutDataType=float3][shape=32x1080x1920][kernelSize=3x3][morphType=DILATE][border=REPLICATE][layout=NCHW][inputKind=Tensor][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1818.7382971030802, + "gpu_time_us_python": 1832.7663217044799, + "gpu_noise_us_cpp": 3.1819999999999995, + "gpu_noise_us_python": 4.311999999999999, + "gpu_bwutil_cpp": 0.5630283419533767, + "gpu_bwutil_python": 0.5587203357554319 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1498.97404221246, + "gpu_time_us_python": 1511.15084725034, + "gpu_noise_us_cpp": 0.9339999999999999, + "gpu_noise_us_python": 5.3020000000000005, + "gpu_bwutil_cpp": 0.5211352678750708, + "gpu_bwutil_python": 0.5169285370700428 + } + } + } + }, + "morphology_float3_planar_nchw_varshape_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "kernelSize": [ + "3x3" + ], + "morphType": [ + "DILATE" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "VarShape" + ] + }, + "int64_axes": { + "iteration": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Morphology planar RGBf32 DILATE var-shape (advanced)." + }, + "baselines": { + "morphology_float3_planar_nchw_varshape_advanced[InOutDataType=float3][shape=32x1080x1920][kernelSize=3x3][morphType=DILATE][border=REPLICATE][layout=NCHW][inputKind=VarShape][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2245.34852078978, + "gpu_time_us_python": 2260.5252480721, + "gpu_noise_us_cpp": 2.616, + "gpu_noise_us_python": 3.036, + "gpu_bwutil_cpp": 0.45605457211860506, + "gpu_bwutil_python": 0.4529926266401155 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2011.5280820055796, + "gpu_time_us_python": 2025.9313567452402, + "gpu_noise_us_cpp": 1.136, + "gpu_noise_us_python": 3.2699999999999996, + "gpu_bwutil_cpp": 0.3885032856256666, + "gpu_bwutil_python": 0.3857196146713099 + } + } + } + }, + "morphology_float3_fakeplanar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "kernelSize": [ + "3x3" + ], + "morphType": [ + "DILATE" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "iteration": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Morphology RGBf32 (advanced) (fake-planar NCHW_FAKE tensor comparison)." + }, + "baselines": { + "morphology_float3_fakeplanar_nchw_advanced[InOutDataType=float3][shape=32x1080x1920][kernelSize=3x3][morphType=DILATE][border=REPLICATE][layout=NCHW_FAKE][inputKind=Tensor][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4288.27594660094, + "gpu_time_us_python": 4306.55612064896, + "gpu_noise_us_cpp": 1.8119999999999998, + "gpu_noise_us_python": 3.0, + "gpu_bwutil_cpp": 0.7163724693609762, + "gpu_bwutil_python": 0.7133317028995956 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3591.7949489316, + "gpu_time_us_python": 3610.08987935626, + "gpu_noise_us_cpp": 5.432, + "gpu_noise_us_python": 5.925999999999999, + "gpu_bwutil_cpp": 0.6524317587591808, + "gpu_bwutil_python": 0.6491271127243751 + } + } + } + }, + "morphology_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "kernelSize": [ + "3x3" + ], + "morphType": [ + "DILATE" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "int64_axes": { + "iteration": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Morphology RGBA8 (advanced)." + }, + "baselines": { + "morphology_uchar4_advanced[InOutDataType=uchar4][shape=32x1080x1920][kernelSize=3x3][morphType=DILATE][border=REPLICATE][layout=NHWC][inputKind=Tensor][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1443.19387526534, + "gpu_time_us_python": 1454.44490473054, + "gpu_noise_us_cpp": 0.968, + "gpu_noise_us_python": 3.768, + "gpu_bwutil_cpp": 0.23651368169163528, + "gpu_bwutil_python": 0.2346840099716609 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1354.00217619234, + "gpu_time_us_python": 1364.90836345146, + "gpu_noise_us_cpp": 1.5799999999999998, + "gpu_noise_us_python": 2.626, + "gpu_bwutil_cpp": 0.19244524454710046, + "gpu_bwutil_python": 0.19090194111349154 + } + }, + "morphology_uchar4_advanced[InOutDataType=uchar4][shape=32x1080x1920][kernelSize=3x3][morphType=DILATE][border=REPLICATE][layout=NHWC][inputKind=VarShape][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1554.83617263372, + "gpu_time_us_python": 1567.85579010088, + "gpu_noise_us_cpp": 2.8080000000000003, + "gpu_noise_us_python": 3.8659999999999997, + "gpu_bwutil_cpp": 0.21953125946349314, + "gpu_bwutil_python": 0.21770845624295604 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1440.4749250946402, + "gpu_time_us_python": 1453.65919274902, + "gpu_noise_us_cpp": 1.3539999999999999, + "gpu_noise_us_python": 3.254, + "gpu_bwutil_cpp": 0.18088715054833066, + "gpu_bwutil_python": 0.17923811066757295 + } + } + } + }, + "morphology_uchar4_planar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "kernelSize": [ + "3x3" + ], + "morphType": [ + "DILATE" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "iteration": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Morphology RGBA8 (advanced) ( native planar NCHW tensor-only; RGBA8p varshape is unsupported by the Python image API)." + }, + "baselines": { + "morphology_uchar4_planar_nchw_advanced[InOutDataType=uchar4][shape=32x1080x1920][kernelSize=3x3][morphType=DILATE][border=REPLICATE][layout=NCHW][inputKind=Tensor][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2286.82473486116, + "gpu_time_us_python": 2297.1176366193, + "gpu_noise_us_cpp": 3.2739999999999996, + "gpu_noise_us_python": 4.766, + "gpu_bwutil_cpp": 0.1492610348672645, + "gpu_bwutil_python": 0.1485922752274619 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1859.21534335762, + "gpu_time_us_python": 1868.3988247149002, + "gpu_noise_us_cpp": 6.597999999999999, + "gpu_noise_us_python": 8.098, + "gpu_bwutil_cpp": 0.14007060005329494, + "gpu_bwutil_python": 0.13937585557263174 + } + } + } + }, + "morphology_uchar4_fakeplanar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "kernelSize": [ + "3x3" + ], + "morphType": [ + "DILATE" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "iteration": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Morphology RGBA8 (advanced) (fake-planar NCHW_FAKE tensor comparison)." + }, + "baselines": { + "morphology_uchar4_fakeplanar_nchw_advanced[InOutDataType=uchar4][shape=32x1080x1920][kernelSize=3x3][morphType=DILATE][border=REPLICATE][layout=NCHW_FAKE][inputKind=Tensor][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3112.4164063252997, + "gpu_time_us_python": 3128.90366137024, + "gpu_noise_us_cpp": 3.0839999999999996, + "gpu_noise_us_python": 5.1080000000000005, + "gpu_bwutil_cpp": 0.3290053023926139, + "gpu_bwutil_python": 0.3272713870206285 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2759.0857291516995, + "gpu_time_us_python": 2774.08349447738, + "gpu_noise_us_cpp": 1.0239999999999998, + "gpu_noise_us_python": 4.548, + "gpu_bwutil_cpp": 0.28318524711287807, + "gpu_bwutil_python": 0.28164706769427644 + } + } + } + }, + "morphology_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "kernelSize": [ + "3x3" + ], + "morphType": [ + "DILATE" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "int64_axes": { + "iteration": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Morphology RGBAf32 (advanced)." + }, + "baselines": { + "morphology_float4_advanced[InOutDataType=float4][shape=16x1080x1920][kernelSize=3x3][morphType=DILATE][border=REPLICATE][layout=NHWC][inputKind=Tensor][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 841.6731025040201, + "gpu_time_us_python": 852.07468105648, + "gpu_noise_us_cpp": 2.194, + "gpu_noise_us_python": 5.238, + "gpu_bwutil_cpp": 0.8110839137050931, + "gpu_bwutil_python": 0.8011855349049799 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 652.79538260794, + "gpu_time_us_python": 662.72714919488, + "gpu_noise_us_cpp": 1.49, + "gpu_noise_us_python": 2.322, + "gpu_bwutil_cpp": 0.7976172080290462, + "gpu_bwutil_python": 0.7856623689012244 + } + }, + "morphology_float4_advanced[InOutDataType=float4][shape=16x1080x1920][kernelSize=3x3][morphType=DILATE][border=REPLICATE][layout=NHWC][inputKind=VarShape][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 870.92323140116, + "gpu_time_us_python": 883.77871416492, + "gpu_noise_us_cpp": 3.3260000000000005, + "gpu_noise_us_python": 4.55, + "gpu_bwutil_cpp": 0.783844143184058, + "gpu_bwutil_python": 0.772451088528607 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 681.78449828696, + "gpu_time_us_python": 694.39285890086, + "gpu_noise_us_cpp": 2.718, + "gpu_noise_us_python": 4.0600000000000005, + "gpu_bwutil_cpp": 0.7637166725712718, + "gpu_bwutil_python": 0.7498392317908299 + } + } + } + }, + "morphology_float4_planar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "kernelSize": [ + "3x3" + ], + "morphType": [ + "DILATE" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "int64_axes": { + "iteration": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Morphology RGBAf32 (advanced) (native planar NCHW)." + }, + "baselines": { + "morphology_float4_planar_nchw_advanced[InOutDataType=float4][shape=16x1080x1920][kernelSize=3x3][morphType=DILATE][border=REPLICATE][layout=NCHW][inputKind=Tensor][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1218.3208817360398, + "gpu_time_us_python": 1229.7474147921, + "gpu_noise_us_cpp": 1.59, + "gpu_noise_us_python": 2.874, + "gpu_bwutil_cpp": 0.5603346469176713, + "gpu_bwutil_python": 0.5551297648642285 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1007.3188112974, + "gpu_time_us_python": 1020.637909563, + "gpu_noise_us_cpp": 1.222, + "gpu_noise_us_python": 3.148, + "gpu_bwutil_cpp": 0.5169672898820769, + "gpu_bwutil_python": 0.5102107599823695 + } + }, + "morphology_float4_planar_nchw_advanced[InOutDataType=float4][shape=16x1080x1920][kernelSize=3x3][morphType=DILATE][border=REPLICATE][layout=NCHW][inputKind=VarShape][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1497.77888084678, + "gpu_time_us_python": 1514.24047454942, + "gpu_noise_us_cpp": 1.5020000000000002, + "gpu_noise_us_python": 3.9580000000000006, + "gpu_bwutil_cpp": 0.4557881906644289, + "gpu_bwutil_python": 0.4508348570943769 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1346.8746702265603, + "gpu_time_us_python": 1360.47336670528, + "gpu_noise_us_cpp": 1.612, + "gpu_noise_us_python": 4.436, + "gpu_bwutil_cpp": 0.3867423560214539, + "gpu_bwutil_python": 0.38285734868723675 + } + } + } + }, + "morphology_float4_fakeplanar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "kernelSize": [ + "3x3" + ], + "morphType": [ + "DILATE" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "iteration": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Morphology RGBAf32 (advanced) (fake-planar NCHW_FAKE tensor comparison)." + }, + "baselines": { + "morphology_float4_fakeplanar_nchw_advanced[InOutDataType=float4][shape=16x1080x1920][kernelSize=3x3][morphType=DILATE][border=REPLICATE][layout=NCHW_FAKE][inputKind=Tensor][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2953.0931814978403, + "gpu_time_us_python": 2971.2166892780397, + "gpu_noise_us_cpp": 3.3519999999999994, + "gpu_noise_us_python": 4.358, + "gpu_bwutil_cpp": 0.6935101282035246, + "gpu_bwutil_python": 0.6892801413959072 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2407.1185606171803, + "gpu_time_us_python": 2421.11975458948, + "gpu_noise_us_cpp": 4.266, + "gpu_noise_us_python": 4.766, + "gpu_bwutil_cpp": 0.6489244694556401, + "gpu_bwutil_python": 0.6451707561971227 + } + } + } + }, + "morphology_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "kernelSize": [ + "3x3" + ], + "morphType": [ + "DILATE" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "int64_axes": { + "iteration": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Morphology single-channel (advanced)." + }, + "baselines": { + "morphology_scalar_advanced[InOutDataType=float32][shape=64x1080x1920][kernelSize=3x3][morphType=DILATE][border=REPLICATE][layout=NHWC][inputKind=Tensor][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1217.89089545458, + "gpu_time_us_python": 1230.1359179502401, + "gpu_noise_us_cpp": 1.124, + "gpu_noise_us_python": 2.38, + "gpu_bwutil_cpp": 0.560532343020262, + "gpu_bwutil_python": 0.5549531526110048 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1004.9928639068401, + "gpu_time_us_python": 1015.4536491025801, + "gpu_noise_us_cpp": 1.224, + "gpu_noise_us_python": 1.536, + "gpu_bwutil_cpp": 0.5181600167062793, + "gpu_bwutil_python": 0.5128141841773843 + } + }, + "morphology_scalar_advanced[InOutDataType=float32][shape=64x1080x1920][kernelSize=3x3][morphType=DILATE][border=REPLICATE][layout=NHWC][inputKind=VarShape][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1495.95475160208, + "gpu_time_us_python": 1508.3450075678397, + "gpu_noise_us_cpp": 2.2199999999999998, + "gpu_noise_us_python": 4.1899999999999995, + "gpu_bwutil_cpp": 0.45634272250119556, + "gpu_bwutil_python": 0.4525939710647604 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1308.4536603164, + "gpu_time_us_python": 1324.8394268708203, + "gpu_noise_us_cpp": 1.034, + "gpu_noise_us_python": 3.768, + "gpu_bwutil_cpp": 0.3981219853946404, + "gpu_bwutil_python": 0.39315884981475435 + } + }, + "morphology_scalar_advanced[InOutDataType=uint8][shape=64x1080x1920][kernelSize=3x3][morphType=DILATE][border=REPLICATE][layout=NHWC][inputKind=Tensor][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1172.67737047888, + "gpu_time_us_python": 1183.4795961372602, + "gpu_noise_us_cpp": 1.668, + "gpu_noise_us_python": 3.918, + "gpu_bwutil_cpp": 0.1455360819913856, + "gpu_bwutil_python": 0.14420820838179774 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 939.4788283180402, + "gpu_time_us_python": 947.8988391350998, + "gpu_noise_us_cpp": 1.0320000000000003, + "gpu_noise_us_python": 4.209999999999999, + "gpu_bwutil_cpp": 0.13859013090105377, + "gpu_bwutil_python": 0.13734871671409402 + } + }, + "morphology_scalar_advanced[InOutDataType=uint8][shape=64x1080x1920][kernelSize=3x3][morphType=DILATE][border=REPLICATE][layout=NHWC][inputKind=VarShape][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1492.63604169796, + "gpu_time_us_python": 1507.21568871962, + "gpu_noise_us_cpp": 2.132, + "gpu_noise_us_python": 11.498000000000001, + "gpu_bwutil_cpp": 0.11433948457813323, + "gpu_bwutil_python": 0.11323354356641369 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1312.37114553224, + "gpu_time_us_python": 1326.6674539159199, + "gpu_noise_us_cpp": 1.156, + "gpu_noise_us_python": 3.4580000000000006, + "gpu_bwutil_cpp": 0.09923944722040726, + "gpu_bwutil_python": 0.09816538384137982 + } + } + } + }, + "morphology_2pass_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "kernelSize": [ + "3x3" + ], + "morphType": [ + "OPEN", + "CLOSE" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "iteration": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Morphology RGB8 OPEN/CLOSE tensor (basic)." + }, + "baselines": { + "morphology_2pass_uchar3_basic[InOutDataType=uchar3][shape=16x1080x1920][kernelSize=3x3][morphType=CLOSE][border=REPLICATE][layout=NHWC][inputKind=Tensor][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1521.71308929618, + "gpu_time_us_python": 1535.24934871632, + "gpu_noise_us_cpp": 2.352, + "gpu_noise_us_python": 3.7540000000000004, + "gpu_bwutil_cpp": 0.16823210721446258, + "gpu_bwutil_python": 0.1667485466175876 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1334.7346065087, + "gpu_time_us_python": 1345.67851687364, + "gpu_noise_us_cpp": 3.7939999999999996, + "gpu_noise_us_python": 2.8880000000000003, + "gpu_bwutil_cpp": 0.1463781153340591, + "gpu_bwutil_python": 0.14518214710937033 + } + }, + "morphology_2pass_uchar3_basic[InOutDataType=uchar3][shape=16x1080x1920][kernelSize=3x3][morphType=OPEN][border=REPLICATE][layout=NHWC][inputKind=Tensor][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1521.03167523472, + "gpu_time_us_python": 1533.1256425203599, + "gpu_noise_us_cpp": 2.336, + "gpu_noise_us_python": 3.214, + "gpu_bwutil_cpp": 0.16830740282498663, + "gpu_bwutil_python": 0.16697951087403454 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1335.54280004438, + "gpu_time_us_python": 1346.28814310958, + "gpu_noise_us_cpp": 2.29, + "gpu_noise_us_python": 2.9099999999999997, + "gpu_bwutil_cpp": 0.14629062408711255, + "gpu_bwutil_python": 0.14512038068928637 + } + } + } + }, + "morphology_2pass_uchar3_varshape_open_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "kernelSize": [ + "3x3" + ], + "morphType": [ + "OPEN" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "int64_axes": { + "iteration": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Morphology RGB8 OPEN var-shape (basic)." + }, + "baselines": { + "morphology_2pass_uchar3_varshape_open_basic[InOutDataType=uchar3][shape=16x1080x1920][kernelSize=3x3][morphType=OPEN][border=REPLICATE][layout=NHWC][inputKind=VarShape][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1731.83431382604, + "gpu_time_us_python": 1748.0973751985002, + "gpu_noise_us_cpp": 2.412, + "gpu_noise_us_python": 4.366, + "gpu_bwutil_cpp": 0.14782067217436204, + "gpu_bwutil_python": 0.14644508027055764 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1510.8886629565, + "gpu_time_us_python": 1523.27577705314, + "gpu_noise_us_cpp": 2.7640000000000002, + "gpu_noise_us_python": 3.4259999999999997, + "gpu_bwutil_cpp": 0.12931006825404764, + "gpu_bwutil_python": 0.12825075925187918 + } + } + } + }, + "morphology_2pass_uchar3_varshape_close_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "kernelSize": [ + "3x3" + ], + "morphType": [ + "CLOSE" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "int64_axes": { + "iteration": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Morphology RGB8 CLOSE var-shape (advanced)." + }, + "baselines": { + "morphology_2pass_uchar3_varshape_close_advanced[InOutDataType=uchar3][shape=16x1080x1920][kernelSize=3x3][morphType=CLOSE][border=REPLICATE][layout=NHWC][inputKind=VarShape][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1732.2345339292, + "gpu_time_us_python": 1748.6673879108798, + "gpu_noise_us_cpp": 2.632, + "gpu_noise_us_python": 3.6439999999999997, + "gpu_bwutil_cpp": 0.14778650316881756, + "gpu_bwutil_python": 0.14639779663097846 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1509.6383851129801, + "gpu_time_us_python": 1524.91297459072, + "gpu_noise_us_cpp": 1.334, + "gpu_noise_us_python": 5.418000000000001, + "gpu_bwutil_cpp": 0.12941527745711182, + "gpu_bwutil_python": 0.1281140495052439 + } + } + } + }, + "morphology_uchar3_planar_nchw_2pass_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "kernelSize": [ + "3x3" + ], + "morphType": [ + "OPEN", + "CLOSE" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "iteration": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Morphology planar RGB8 OPEN/CLOSE tensor (basic)." + }, + "baselines": { + "morphology_uchar3_planar_nchw_2pass_basic[InOutDataType=uchar3][shape=16x1080x1920][kernelSize=3x3][morphType=CLOSE][border=REPLICATE][layout=NCHW][inputKind=Tensor][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1745.07174453206, + "gpu_time_us_python": 1745.6876047875398, + "gpu_noise_us_cpp": 3.478, + "gpu_noise_us_python": 5.834, + "gpu_bwutil_cpp": 0.14669906141430253, + "gpu_bwutil_python": 0.14664753423394492 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1411.4894803876, + "gpu_time_us_python": 1421.4053597186999, + "gpu_noise_us_cpp": 1.342, + "gpu_noise_us_python": 6.544, + "gpu_bwutil_cpp": 0.1383676437841693, + "gpu_bwutil_python": 0.13739265385659255 + } + }, + "morphology_uchar3_planar_nchw_2pass_basic[InOutDataType=uchar3][shape=16x1080x1920][kernelSize=3x3][morphType=OPEN][border=REPLICATE][layout=NCHW][inputKind=Tensor][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1728.7591843768998, + "gpu_time_us_python": 1744.5660491784201, + "gpu_noise_us_cpp": 2.092, + "gpu_noise_us_python": 3.194, + "gpu_bwutil_cpp": 0.14808325112863416, + "gpu_bwutil_python": 0.1467414424825997 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1411.7169489495197, + "gpu_time_us_python": 1423.9112399378398, + "gpu_noise_us_cpp": 1.002, + "gpu_noise_us_python": 5.462, + "gpu_bwutil_cpp": 0.13834253192074844, + "gpu_bwutil_python": 0.13715541438816486 + } + } + } + }, + "morphology_uchar3_planar_nchw_2pass_varshape_open_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "kernelSize": [ + "3x3" + ], + "morphType": [ + "OPEN" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "VarShape" + ] + }, + "int64_axes": { + "iteration": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Morphology planar RGB8 OPEN var-shape (basic)." + }, + "baselines": { + "morphology_uchar3_planar_nchw_2pass_varshape_open_basic[InOutDataType=uchar3][shape=16x1080x1920][kernelSize=3x3][morphType=OPEN][border=REPLICATE][layout=NCHW][inputKind=VarShape][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1967.3275203421404, + "gpu_time_us_python": 1986.75693249404, + "gpu_noise_us_cpp": 2.7800000000000002, + "gpu_noise_us_python": 6.4319999999999995, + "gpu_bwutil_cpp": 0.13012635715832627, + "gpu_bwutil_python": 0.1288541588010633 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1715.19152269228, + "gpu_time_us_python": 1730.2641336688, + "gpu_noise_us_cpp": 2.282, + "gpu_noise_us_python": 5.086, + "gpu_bwutil_cpp": 0.11391682382927219, + "gpu_bwutil_python": 0.11291281616821602 + } + } + } + }, + "morphology_uchar3_planar_nchw_2pass_varshape_close_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "kernelSize": [ + "3x3" + ], + "morphType": [ + "CLOSE" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "VarShape" + ] + }, + "int64_axes": { + "iteration": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Morphology planar RGB8 CLOSE var-shape (advanced)." + }, + "baselines": { + "morphology_uchar3_planar_nchw_2pass_varshape_close_advanced[InOutDataType=uchar3][shape=16x1080x1920][kernelSize=3x3][morphType=CLOSE][border=REPLICATE][layout=NCHW][inputKind=VarShape][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1967.6859678098601, + "gpu_time_us_python": 1983.47975224822, + "gpu_noise_us_cpp": 2.4379999999999997, + "gpu_noise_us_python": 3.976, + "gpu_bwutil_cpp": 0.13010258644228961, + "gpu_bwutil_python": 0.1290665015989357 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1715.3058677203, + "gpu_time_us_python": 1730.5303132469403, + "gpu_noise_us_cpp": 2.4899999999999998, + "gpu_noise_us_python": 4.7700000000000005, + "gpu_bwutil_cpp": 0.11390952375807059, + "gpu_bwutil_python": 0.11289723612138407 + } + } + } + }, + "morphology_uchar3_fakeplanar_nchw_2pass_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "kernelSize": [ + "3x3" + ], + "morphType": [ + "OPEN", + "CLOSE" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "iteration": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Morphology RGB8 2pass (advanced) (fake-planar NCHW_FAKE tensor comparison)." + }, + "baselines": { + "morphology_uchar3_fakeplanar_nchw_2pass_advanced[InOutDataType=uchar3][shape=16x1080x1920][kernelSize=3x3][morphType=CLOSE][border=REPLICATE][layout=NCHW_FAKE][inputKind=Tensor][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2196.56476748974, + "gpu_time_us_python": 2215.08754131822, + "gpu_noise_us_cpp": 2.41, + "gpu_noise_us_python": 4.59, + "gpu_bwutil_cpp": 0.34963768414842267, + "gpu_bwutil_python": 0.3467134150314757 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1920.8888008753397, + "gpu_time_us_python": 1934.70232876866, + "gpu_noise_us_cpp": 1.9939999999999998, + "gpu_noise_us_python": 3.138, + "gpu_bwutil_cpp": 0.30507112256717256, + "gpu_bwutil_python": 0.3028897897989124 + } + }, + "morphology_uchar3_fakeplanar_nchw_2pass_advanced[InOutDataType=uchar3][shape=16x1080x1920][kernelSize=3x3][morphType=OPEN][border=REPLICATE][layout=NCHW_FAKE][inputKind=Tensor][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2196.7848627735802, + "gpu_time_us_python": 2215.3395465285603, + "gpu_noise_us_cpp": 1.314, + "gpu_noise_us_python": 4.144, + "gpu_bwutil_cpp": 0.3496034194053882, + "gpu_bwutil_python": 0.3466743288326562 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1921.8761588270002, + "gpu_time_us_python": 1936.34322513388, + "gpu_noise_us_cpp": 2.0700000000000003, + "gpu_noise_us_python": 6.712000000000001, + "gpu_bwutil_cpp": 0.3049149014722688, + "gpu_bwutil_python": 0.3026286797257329 + } + } + } + }, + "morphology_2pass_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "kernelSize": [ + "3x3" + ], + "morphType": [ + "OPEN", + "CLOSE" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "iteration": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Morphology RGBf32 OPEN/CLOSE tensor (basic)." + }, + "baselines": { + "morphology_2pass_float3_basic[InOutDataType=float3][shape=16x1080x1920][kernelSize=3x3][morphType=CLOSE][border=REPLICATE][layout=NHWC][inputKind=Tensor][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1618.11687469478, + "gpu_time_us_python": 1630.54313504846, + "gpu_noise_us_cpp": 1.046, + "gpu_noise_us_python": 3.808, + "gpu_bwutil_cpp": 0.6328361807136827, + "gpu_bwutil_python": 0.628012733930999 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1414.20116647582, + "gpu_time_us_python": 1425.8716422168, + "gpu_noise_us_cpp": 1.152, + "gpu_noise_us_python": 2.472, + "gpu_bwutil_cpp": 0.5526072367951691, + "gpu_bwutil_python": 0.5480652403936331 + } + }, + "morphology_2pass_float3_basic[InOutDataType=float3][shape=16x1080x1920][kernelSize=3x3][morphType=OPEN][border=REPLICATE][layout=NHWC][inputKind=Tensor][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1618.4577714242798, + "gpu_time_us_python": 1633.28235508496, + "gpu_noise_us_cpp": 3.286, + "gpu_noise_us_python": 4.038, + "gpu_bwutil_cpp": 0.6327027662867726, + "gpu_bwutil_python": 0.6269598236387612 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1414.02679410812, + "gpu_time_us_python": 1425.56657458618, + "gpu_noise_us_cpp": 1.266, + "gpu_noise_us_python": 3.18, + "gpu_bwutil_cpp": 0.5526734193253672, + "gpu_bwutil_python": 0.5481843116362815 + } + } + } + }, + "morphology_2pass_float3_varshape_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "kernelSize": [ + "3x3" + ], + "morphType": [ + "OPEN", + "CLOSE" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "int64_axes": { + "iteration": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Morphology RGBf32 OPEN/CLOSE var-shape (advanced)." + }, + "baselines": { + "morphology_2pass_float3_varshape_advanced[InOutDataType=float3][shape=16x1080x1920][kernelSize=3x3][morphType=CLOSE][border=REPLICATE][layout=NHWC][inputKind=VarShape][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1797.8263736101399, + "gpu_time_us_python": 1814.24674782082, + "gpu_noise_us_cpp": 2.6719999999999997, + "gpu_noise_us_python": 4.287999999999999, + "gpu_bwutil_cpp": 0.5695784930824486, + "gpu_bwutil_python": 0.564422039245124 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1608.77974591036, + "gpu_time_us_python": 1623.4410295797002, + "gpu_noise_us_cpp": 1.89, + "gpu_noise_us_python": 3.9519999999999995, + "gpu_bwutil_cpp": 0.4857897299319811, + "gpu_bwutil_python": 0.4813740203207148 + } + }, + "morphology_2pass_float3_varshape_advanced[InOutDataType=float3][shape=16x1080x1920][kernelSize=3x3][morphType=OPEN][border=REPLICATE][layout=NHWC][inputKind=VarShape][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1797.6287083341201, + "gpu_time_us_python": 1814.73278722896, + "gpu_noise_us_cpp": 1.6880000000000002, + "gpu_noise_us_python": 3.694, + "gpu_bwutil_cpp": 0.5696412050587566, + "gpu_bwutil_python": 0.5642711418101157 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1607.46848170252, + "gpu_time_us_python": 1622.4338629891602, + "gpu_noise_us_cpp": 2.026, + "gpu_noise_us_python": 5.447999999999999, + "gpu_bwutil_cpp": 0.486171118453355, + "gpu_bwutil_python": 0.48165875879939735 + } + } + } + }, + "morphology_float3_planar_nchw_2pass_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "kernelSize": [ + "3x3" + ], + "morphType": [ + "OPEN", + "CLOSE" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "iteration": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Morphology planar RGBf32 OPEN/CLOSE tensor (basic)." + }, + "baselines": { + "morphology_float3_planar_nchw_2pass_basic[InOutDataType=float3][shape=16x1080x1920][kernelSize=3x3][morphType=CLOSE][border=REPLICATE][layout=NCHW][inputKind=Tensor][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1829.4888473774602, + "gpu_time_us_python": 1840.00784807852, + "gpu_noise_us_cpp": 1.31, + "gpu_noise_us_python": 4.7620000000000005, + "gpu_bwutil_cpp": 0.5597194857561721, + "gpu_bwutil_python": 0.5565209497809206 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1509.1809825531002, + "gpu_time_us_python": 1521.69303435058, + "gpu_noise_us_cpp": 2.8200000000000003, + "gpu_noise_us_python": 3.5759999999999996, + "gpu_bwutil_cpp": 0.5175768496414122, + "gpu_bwutil_python": 0.5133251269963621 + } + }, + "morphology_float3_planar_nchw_2pass_basic[InOutDataType=float3][shape=16x1080x1920][kernelSize=3x3][morphType=OPEN][border=REPLICATE][layout=NCHW][inputKind=Tensor][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1827.05515301136, + "gpu_time_us_python": 1841.9959143872202, + "gpu_noise_us_cpp": 1.434, + "gpu_noise_us_python": 3.402, + "gpu_bwutil_cpp": 0.5604651008169659, + "gpu_bwutil_python": 0.5559198717411443 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1507.8600736091403, + "gpu_time_us_python": 1518.85338042128, + "gpu_noise_us_cpp": 1.77, + "gpu_noise_us_python": 4.286, + "gpu_bwutil_cpp": 0.5180323164718388, + "gpu_bwutil_python": 0.5142832298038007 + } + } + } + }, + "morphology_float3_planar_nchw_2pass_varshape_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "kernelSize": [ + "3x3" + ], + "morphType": [ + "OPEN", + "CLOSE" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "VarShape" + ] + }, + "int64_axes": { + "iteration": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Morphology planar RGBf32 OPEN/CLOSE var-shape (advanced)." + }, + "baselines": { + "morphology_float3_planar_nchw_2pass_varshape_advanced[InOutDataType=float3][shape=16x1080x1920][kernelSize=3x3][morphType=CLOSE][border=REPLICATE][layout=NCHW][inputKind=VarShape][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2250.5942690671, + "gpu_time_us_python": 2266.4016998565394, + "gpu_noise_us_cpp": 3.128, + "gpu_noise_us_python": 4.997999999999999, + "gpu_bwutil_cpp": 0.454991923880079, + "gpu_bwutil_python": 0.45181849071659386 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2025.65202580546, + "gpu_time_us_python": 2042.7335652631004, + "gpu_noise_us_cpp": 2.098, + "gpu_noise_us_python": 5.284, + "gpu_bwutil_cpp": 0.3857263891922774, + "gpu_bwutil_python": 0.3824806863602002 + } + }, + "morphology_float3_planar_nchw_2pass_varshape_advanced[InOutDataType=float3][shape=16x1080x1920][kernelSize=3x3][morphType=OPEN][border=REPLICATE][layout=NCHW][inputKind=VarShape][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2250.0420857119398, + "gpu_time_us_python": 2264.04005514604, + "gpu_noise_us_cpp": 2.6719999999999997, + "gpu_noise_us_python": 4.588, + "gpu_bwutil_cpp": 0.4551035507197767, + "gpu_bwutil_python": 0.452289493376307 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2025.4287622873999, + "gpu_time_us_python": 2040.2462469537998, + "gpu_noise_us_cpp": 1.488, + "gpu_noise_us_python": 4.4079999999999995, + "gpu_bwutil_cpp": 0.38576852072624945, + "gpu_bwutil_python": 0.38295119370193204 + } + } + } + }, + "morphology_float3_fakeplanar_nchw_2pass_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "kernelSize": [ + "3x3" + ], + "morphType": [ + "OPEN", + "CLOSE" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "iteration": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Morphology RGBf32 2pass (advanced) (fake-planar NCHW_FAKE tensor comparison)." + }, + "baselines": { + "morphology_float3_fakeplanar_nchw_2pass_advanced[InOutDataType=float3][shape=16x1080x1920][kernelSize=3x3][morphType=CLOSE][border=REPLICATE][layout=NCHW_FAKE][inputKind=Tensor][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2959.3359122556603, + "gpu_time_us_python": 2978.03643098394, + "gpu_noise_us_cpp": 1.9140000000000001, + "gpu_noise_us_python": 4.4399999999999995, + "gpu_bwutil_cpp": 1.0380718654887968, + "gpu_bwutil_python": 1.0315527708234016 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2512.80118283392, + "gpu_time_us_python": 2525.12566162696, + "gpu_noise_us_cpp": 2.274, + "gpu_noise_us_python": 3.6020000000000003, + "gpu_bwutil_cpp": 0.9326573611429503, + "gpu_bwutil_python": 0.9280785435257126 + } + }, + "morphology_float3_fakeplanar_nchw_2pass_advanced[InOutDataType=float3][shape=16x1080x1920][kernelSize=3x3][morphType=OPEN][border=REPLICATE][layout=NCHW_FAKE][inputKind=Tensor][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2958.9142440346595, + "gpu_time_us_python": 2977.2635534363594, + "gpu_noise_us_cpp": 1.966, + "gpu_noise_us_python": 3.7339999999999995, + "gpu_bwutil_cpp": 1.0382198248178844, + "gpu_bwutil_python": 1.0318202424970226 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2512.5417265710603, + "gpu_time_us_python": 2526.1408837210197, + "gpu_noise_us_cpp": 2.0959999999999996, + "gpu_noise_us_python": 5.992, + "gpu_bwutil_cpp": 0.9327591414560941, + "gpu_bwutil_python": 0.9277224533742056 + } + } + } + }, + "morphology_2pass_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "kernelSize": [ + "3x3" + ], + "morphType": [ + "OPEN", + "CLOSE" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "int64_axes": { + "iteration": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Morphology RGBA8 2pass (advanced)." + }, + "baselines": { + "morphology_2pass_uchar4_advanced[InOutDataType=uchar4][shape=16x1080x1920][kernelSize=3x3][morphType=CLOSE][border=REPLICATE][layout=NHWC][inputKind=Tensor][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1454.95464492184, + "gpu_time_us_python": 1466.59715073924, + "gpu_noise_us_cpp": 1.908, + "gpu_noise_us_python": 2.7159999999999997, + "gpu_bwutil_cpp": 0.23460153086672836, + "gpu_bwutil_python": 0.23273884628008368 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1367.86207114808, + "gpu_time_us_python": 1381.1469155997402, + "gpu_noise_us_cpp": 2.6940000000000004, + "gpu_noise_us_python": 5.151999999999999, + "gpu_bwutil_cpp": 0.19045284059397555, + "gpu_bwutil_python": 0.18862292743777637 + } + }, + "morphology_2pass_uchar4_advanced[InOutDataType=uchar4][shape=16x1080x1920][kernelSize=3x3][morphType=CLOSE][border=REPLICATE][layout=NHWC][inputKind=VarShape][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1570.5749861623801, + "gpu_time_us_python": 1587.1468563622602, + "gpu_noise_us_cpp": 2.2299999999999995, + "gpu_noise_us_python": 4.359999999999999, + "gpu_bwutil_cpp": 0.21733107756428396, + "gpu_bwutil_python": 0.21506175877056383 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1463.7926044868398, + "gpu_time_us_python": 1479.1860880937397, + "gpu_noise_us_cpp": 2.292, + "gpu_noise_us_python": 5.276, + "gpu_bwutil_cpp": 0.17796188345260525, + "gpu_bwutil_python": 0.1761077239946818 + } + }, + "morphology_2pass_uchar4_advanced[InOutDataType=uchar4][shape=16x1080x1920][kernelSize=3x3][morphType=OPEN][border=REPLICATE][layout=NHWC][inputKind=Tensor][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1455.67663166526, + "gpu_time_us_python": 1467.4716128776001, + "gpu_noise_us_cpp": 3.66, + "gpu_noise_us_python": 5.210000000000001, + "gpu_bwutil_cpp": 0.23448558836535005, + "gpu_bwutil_python": 0.23259999154713804 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1367.647025533, + "gpu_time_us_python": 1378.9269120386202, + "gpu_noise_us_cpp": 1.75, + "gpu_noise_us_python": 2.7500000000000004, + "gpu_bwutil_cpp": 0.19048116785713498, + "gpu_bwutil_python": 0.18892078061477435 + } + }, + "morphology_2pass_uchar4_advanced[InOutDataType=uchar4][shape=16x1080x1920][kernelSize=3x3][morphType=OPEN][border=REPLICATE][layout=NHWC][inputKind=VarShape][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1571.142292868, + "gpu_time_us_python": 1586.05156910564, + "gpu_noise_us_cpp": 2.906, + "gpu_noise_us_python": 4.644, + "gpu_bwutil_cpp": 0.2172526247003516, + "gpu_bwutil_python": 0.21520980273902185 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1463.3942202475603, + "gpu_time_us_python": 1478.00000708146, + "gpu_noise_us_cpp": 1.7440000000000002, + "gpu_noise_us_python": 4.564, + "gpu_bwutil_cpp": 0.1780138924238873, + "gpu_bwutil_python": 0.17625051381807327 + } + } + } + }, + "morphology_uchar4_planar_nchw_2pass_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "kernelSize": [ + "3x3" + ], + "morphType": [ + "OPEN", + "CLOSE" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "iteration": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Morphology RGBA8 2pass (advanced) ( native planar NCHW tensor-only; RGBA8p varshape is unsupported by the Python image API)." + }, + "baselines": { + "morphology_uchar4_planar_nchw_2pass_advanced[InOutDataType=uchar4][shape=16x1080x1920][kernelSize=3x3][morphType=CLOSE][border=REPLICATE][layout=NCHW][inputKind=Tensor][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2332.68903336664, + "gpu_time_us_python": 2345.67198842484, + "gpu_noise_us_cpp": 2.27, + "gpu_noise_us_python": 7.056, + "gpu_bwutil_cpp": 0.1463263389194921, + "gpu_bwutil_python": 0.14551644527501592 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1874.2560895350002, + "gpu_time_us_python": 1889.7369910939601, + "gpu_noise_us_cpp": 2.418, + "gpu_noise_us_python": 3.222, + "gpu_bwutil_cpp": 0.13894002815520334, + "gpu_bwutil_python": 0.1378001629991153 + } + }, + "morphology_uchar4_planar_nchw_2pass_advanced[InOutDataType=uchar4][shape=16x1080x1920][kernelSize=3x3][morphType=OPEN][border=REPLICATE][layout=NCHW][inputKind=Tensor][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2294.23574041976, + "gpu_time_us_python": 2301.9426039598798, + "gpu_noise_us_cpp": 3.194, + "gpu_noise_us_python": 3.9560000000000004, + "gpu_bwutil_cpp": 0.14877893400804898, + "gpu_bwutil_python": 0.14828074658503382 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1876.82294934036, + "gpu_time_us_python": 1886.1172889642999, + "gpu_noise_us_cpp": 1.704, + "gpu_noise_us_python": 2.692, + "gpu_bwutil_cpp": 0.1387486773359962, + "gpu_bwutil_python": 0.13806342777400396 + } + } + } + }, + "morphology_uchar4_fakeplanar_nchw_2pass_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "kernelSize": [ + "3x3" + ], + "morphType": [ + "OPEN", + "CLOSE" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "iteration": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Morphology RGBA8 2pass (advanced) (fake-planar NCHW_FAKE tensor comparison)." + }, + "baselines": { + "morphology_uchar4_fakeplanar_nchw_2pass_advanced[InOutDataType=uchar4][shape=16x1080x1920][kernelSize=3x3][morphType=CLOSE][border=REPLICATE][layout=NCHW_FAKE][inputKind=Tensor][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2295.73432277694, + "gpu_time_us_python": 2313.1446583960196, + "gpu_noise_us_cpp": 1.198, + "gpu_noise_us_python": 5.006, + "gpu_bwutil_cpp": 0.4460467272221381, + "gpu_bwutil_python": 0.44268777943850035 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2077.84356737946, + "gpu_time_us_python": 2089.4414491481602, + "gpu_noise_us_cpp": 2.804, + "gpu_noise_us_python": 3.412, + "gpu_bwutil_cpp": 0.3760315734606561, + "gpu_bwutil_python": 0.3739368310428604 + } + }, + "morphology_uchar4_fakeplanar_nchw_2pass_advanced[InOutDataType=uchar4][shape=16x1080x1920][kernelSize=3x3][morphType=OPEN][border=REPLICATE][layout=NCHW_FAKE][inputKind=Tensor][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2296.26835333094, + "gpu_time_us_python": 2314.9245973958195, + "gpu_noise_us_cpp": 2.56, + "gpu_noise_us_python": 3.9319999999999995, + "gpu_bwutil_cpp": 0.44594290575314116, + "gpu_bwutil_python": 0.4423474910493009 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2077.2498123016403, + "gpu_time_us_python": 2090.75277686366, + "gpu_noise_us_cpp": 2.008, + "gpu_noise_us_python": 2.5839999999999996, + "gpu_bwutil_cpp": 0.37613796807196154, + "gpu_bwutil_python": 0.3737033677242308 + } + } + } + }, + "morphology_2pass_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "8x1080x1920" + ], + "kernelSize": [ + "3x3" + ], + "morphType": [ + "OPEN", + "CLOSE" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "int64_axes": { + "iteration": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Morphology RGBAf32 2pass (advanced)." + }, + "baselines": { + "morphology_2pass_float4_advanced[InOutDataType=float4][shape=8x1080x1920][kernelSize=3x3][morphType=CLOSE][border=REPLICATE][layout=NHWC][inputKind=Tensor][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 850.4796917337801, + "gpu_time_us_python": 861.0495106724, + "gpu_noise_us_cpp": 2.89, + "gpu_noise_us_python": 2.966, + "gpu_bwutil_cpp": 0.8026862107017767, + "gpu_bwutil_python": 0.7928339203730679 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 660.82698404026, + "gpu_time_us_python": 671.1758591533801, + "gpu_noise_us_cpp": 2.074, + "gpu_noise_us_python": 2.396, + "gpu_bwutil_cpp": 0.7879211281401016, + "gpu_bwutil_python": 0.7757703390831934 + } + }, + "morphology_2pass_float4_advanced[InOutDataType=float4][shape=8x1080x1920][kernelSize=3x3][morphType=CLOSE][border=REPLICATE][layout=NHWC][inputKind=VarShape][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 881.34237359942, + "gpu_time_us_python": 892.8992560978601, + "gpu_noise_us_cpp": 2.7459999999999996, + "gpu_noise_us_python": 3.4859999999999998, + "gpu_bwutil_cpp": 0.7745772206171517, + "gpu_bwutil_python": 0.7645555140284497 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 692.2504626510402, + "gpu_time_us_python": 705.5589172081, + "gpu_noise_us_cpp": 1.9, + "gpu_noise_us_python": 3.9579999999999997, + "gpu_bwutil_cpp": 0.7521602769014258, + "gpu_bwutil_python": 0.737968045327063 + } + }, + "morphology_2pass_float4_advanced[InOutDataType=float4][shape=8x1080x1920][kernelSize=3x3][morphType=OPEN][border=REPLICATE][layout=NHWC][inputKind=Tensor][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 850.17509443636, + "gpu_time_us_python": 861.42591272874, + "gpu_noise_us_cpp": 3.0379999999999994, + "gpu_noise_us_python": 2.7700000000000005, + "gpu_bwutil_cpp": 0.8029726551884411, + "gpu_bwutil_python": 0.7924916103176889 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 660.43701966558, + "gpu_time_us_python": 671.64770782928, + "gpu_noise_us_cpp": 1.314, + "gpu_noise_us_python": 2.8579999999999997, + "gpu_bwutil_cpp": 0.788387235453446, + "gpu_bwutil_python": 0.7752258140203302 + } + }, + "morphology_2pass_float4_advanced[InOutDataType=float4][shape=8x1080x1920][kernelSize=3x3][morphType=OPEN][border=REPLICATE][layout=NHWC][inputKind=VarShape][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 880.25854502892, + "gpu_time_us_python": 894.0116138592, + "gpu_noise_us_cpp": 2.502, + "gpu_noise_us_python": 4.706, + "gpu_bwutil_cpp": 0.7755304318270227, + "gpu_bwutil_python": 0.7636108097708613 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 692.41426477936, + "gpu_time_us_python": 705.7514913899801, + "gpu_noise_us_cpp": 1.886, + "gpu_noise_us_python": 4.328, + "gpu_bwutil_cpp": 0.7519841240685041, + "gpu_bwutil_python": 0.737771679743387 + } + } + } + }, + "morphology_float4_planar_nchw_2pass_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "8x1080x1920" + ], + "kernelSize": [ + "3x3" + ], + "morphType": [ + "OPEN", + "CLOSE" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "int64_axes": { + "iteration": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Morphology RGBAf32 2pass (advanced) (native planar NCHW)." + }, + "baselines": { + "morphology_float4_planar_nchw_2pass_advanced[InOutDataType=float4][shape=8x1080x1920][kernelSize=3x3][morphType=CLOSE][border=REPLICATE][layout=NCHW][inputKind=Tensor][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1226.5820929423403, + "gpu_time_us_python": 1239.6842704791802, + "gpu_noise_us_cpp": 1.116, + "gpu_noise_us_python": 2.5460000000000003, + "gpu_bwutil_cpp": 0.5565604151796333, + "gpu_bwutil_python": 0.5506791028782293 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1013.1052722212, + "gpu_time_us_python": 1024.05593605684, + "gpu_noise_us_cpp": 1.904, + "gpu_noise_us_python": 2.37, + "gpu_bwutil_cpp": 0.5140128940589849, + "gpu_bwutil_python": 0.5085069113483306 + } + }, + "morphology_float4_planar_nchw_2pass_advanced[InOutDataType=float4][shape=8x1080x1920][kernelSize=3x3][morphType=CLOSE][border=REPLICATE][layout=NCHW][inputKind=VarShape][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1504.49159921016, + "gpu_time_us_python": 1520.54334253796, + "gpu_noise_us_cpp": 2.182, + "gpu_noise_us_python": 5.01, + "gpu_bwutil_cpp": 0.4537537741096263, + "gpu_bwutil_python": 0.4489644963837269 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1356.81567990136, + "gpu_time_us_python": 1370.07874757534, + "gpu_noise_us_cpp": 2.458, + "gpu_noise_us_python": 2.886, + "gpu_bwutil_cpp": 0.3839120756584574, + "gpu_bwutil_python": 0.38018646543616524 + } + }, + "morphology_float4_planar_nchw_2pass_advanced[InOutDataType=float4][shape=8x1080x1920][kernelSize=3x3][morphType=OPEN][border=REPLICATE][layout=NCHW][inputKind=Tensor][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1225.32095962702, + "gpu_time_us_python": 1241.54852044866, + "gpu_noise_us_cpp": 3.2079999999999997, + "gpu_noise_us_python": 5.424, + "gpu_bwutil_cpp": 0.5571334883077931, + "gpu_bwutil_python": 0.5498546697088852 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1013.78268535394, + "gpu_time_us_python": 1025.9778812039199, + "gpu_noise_us_cpp": 2.6919999999999997, + "gpu_noise_us_python": 2.332, + "gpu_bwutil_cpp": 0.5136615865959546, + "gpu_bwutil_python": 0.5075221910809808 + } + }, + "morphology_float4_planar_nchw_2pass_advanced[InOutDataType=float4][shape=8x1080x1920][kernelSize=3x3][morphType=OPEN][border=REPLICATE][layout=NCHW][inputKind=VarShape][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1503.14057219736, + "gpu_time_us_python": 1518.99539557366, + "gpu_noise_us_cpp": 2.234, + "gpu_noise_us_python": 5.497999999999999, + "gpu_bwutil_cpp": 0.45416099468860704, + "gpu_bwutil_python": 0.449420104082472 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1355.8207532266802, + "gpu_time_us_python": 1369.03893850556, + "gpu_noise_us_cpp": 1.5559999999999998, + "gpu_noise_us_python": 3.08, + "gpu_bwutil_cpp": 0.38418823598988877, + "gpu_bwutil_python": 0.3804650447005441 + } + } + } + }, + "morphology_float4_fakeplanar_nchw_2pass_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "8x1080x1920" + ], + "kernelSize": [ + "3x3" + ], + "morphType": [ + "OPEN", + "CLOSE" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "iteration": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Morphology RGBAf32 2pass (advanced) (fake-planar NCHW_FAKE tensor comparison)." + }, + "baselines": { + "morphology_float4_fakeplanar_nchw_2pass_advanced[InOutDataType=float4][shape=8x1080x1920][kernelSize=3x3][morphType=CLOSE][border=REPLICATE][layout=NCHW_FAKE][inputKind=Tensor][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1909.8366018469198, + "gpu_time_us_python": 1927.51774072642, + "gpu_noise_us_cpp": 2.6479999999999997, + "gpu_noise_us_python": 4.6259999999999994, + "gpu_bwutil_cpp": 1.0723433468163683, + "gpu_bwutil_python": 1.0625079199154424 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1539.6459586780798, + "gpu_time_us_python": 1554.003804779, + "gpu_noise_us_cpp": 4.258, + "gpu_noise_us_python": 5.978, + "gpu_bwutil_cpp": 1.0145441678973577, + "gpu_bwutil_python": 1.005168387759456 + } + }, + "morphology_float4_fakeplanar_nchw_2pass_advanced[InOutDataType=float4][shape=8x1080x1920][kernelSize=3x3][morphType=OPEN][border=REPLICATE][layout=NCHW_FAKE][inputKind=Tensor][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1907.57225127284, + "gpu_time_us_python": 1925.20523327062, + "gpu_noise_us_cpp": 3.1480000000000006, + "gpu_noise_us_python": 4.536, + "gpu_bwutil_cpp": 1.0736160710413878, + "gpu_bwutil_python": 1.0637839875827577 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1540.2706057015798, + "gpu_time_us_python": 1554.0551353292199, + "gpu_noise_us_cpp": 2.9219999999999997, + "gpu_noise_us_python": 2.854, + "gpu_bwutil_cpp": 1.014132118564992, + "gpu_bwutil_python": 1.0051351507497435 + } + } + } + }, + "morphology_2pass_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "kernelSize": [ + "3x3" + ], + "morphType": [ + "OPEN", + "CLOSE" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "int64_axes": { + "iteration": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Morphology single-channel 2pass (advanced)." + }, + "baselines": { + "morphology_2pass_scalar_advanced[InOutDataType=float32][shape=32x1080x1920][kernelSize=3x3][morphType=CLOSE][border=REPLICATE][layout=NHWC][inputKind=Tensor][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1223.2714145939, + "gpu_time_us_python": 1237.9164808611, + "gpu_noise_us_cpp": 1.654, + "gpu_noise_us_python": 4.306, + "gpu_bwutil_cpp": 0.5580667940360182, + "gpu_bwutil_python": 0.551467151535206 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1011.7640071434198, + "gpu_time_us_python": 1023.04273260292, + "gpu_noise_us_cpp": 2.58, + "gpu_noise_us_python": 2.9739999999999998, + "gpu_bwutil_cpp": 0.5146951323673233, + "gpu_bwutil_python": 0.5090085744554271 + } + }, + "morphology_2pass_scalar_advanced[InOutDataType=float32][shape=32x1080x1920][kernelSize=3x3][morphType=CLOSE][border=REPLICATE][layout=NHWC][inputKind=VarShape][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1503.0261416884, + "gpu_time_us_python": 1519.2796345912602, + "gpu_noise_us_cpp": 3.3419999999999996, + "gpu_noise_us_python": 3.9599999999999995, + "gpu_bwutil_cpp": 0.45419671287439395, + "gpu_bwutil_python": 0.44933745617797854 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1317.825044742, + "gpu_time_us_python": 1333.55483157054, + "gpu_noise_us_cpp": 1.4200000000000002, + "gpu_noise_us_python": 3.46, + "gpu_bwutil_cpp": 0.39529232507234624, + "gpu_bwutil_python": 0.39060421616097774 + } + }, + "morphology_2pass_scalar_advanced[InOutDataType=float32][shape=32x1080x1920][kernelSize=3x3][morphType=OPEN][border=REPLICATE][layout=NHWC][inputKind=Tensor][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1223.2694166462202, + "gpu_time_us_python": 1236.1902760730998, + "gpu_noise_us_cpp": 1.504, + "gpu_noise_us_python": 3.526, + "gpu_bwutil_cpp": 0.5580674583688249, + "gpu_bwutil_python": 0.5522358552523001 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1011.6269367976399, + "gpu_time_us_python": 1022.3936534459799, + "gpu_noise_us_cpp": 0.9359999999999999, + "gpu_noise_us_python": 3.286, + "gpu_bwutil_cpp": 0.5147640500831352, + "gpu_bwutil_python": 0.5093370427583305 + } + }, + "morphology_2pass_scalar_advanced[InOutDataType=float32][shape=32x1080x1920][kernelSize=3x3][morphType=OPEN][border=REPLICATE][layout=NHWC][inputKind=VarShape][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1501.7919518456802, + "gpu_time_us_python": 1518.57088883712, + "gpu_noise_us_cpp": 2.186, + "gpu_noise_us_python": 4.286, + "gpu_bwutil_cpp": 0.45456947666597347, + "gpu_bwutil_python": 0.4495468325940541 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1319.00994384916, + "gpu_time_us_python": 1331.81235813564, + "gpu_noise_us_cpp": 2.2119999999999997, + "gpu_noise_us_python": 3.528, + "gpu_bwutil_cpp": 0.39494506380951633, + "gpu_bwutil_python": 0.39111078718917774 + } + }, + "morphology_2pass_scalar_advanced[InOutDataType=uint8][shape=32x1080x1920][kernelSize=3x3][morphType=CLOSE][border=REPLICATE][layout=NHWC][inputKind=Tensor][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1164.07849806992, + "gpu_time_us_python": 1170.6441822571999, + "gpu_noise_us_cpp": 1.614, + "gpu_noise_us_python": 4.539999999999999, + "gpu_bwutil_cpp": 0.14661807551167555, + "gpu_bwutil_python": 0.14578929798033263 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 946.1646182635199, + "gpu_time_us_python": 957.13071711254, + "gpu_noise_us_cpp": 2.6699999999999995, + "gpu_noise_us_python": 2.846, + "gpu_bwutil_cpp": 0.13760971292945495, + "gpu_bwutil_python": 0.13602727727153305 + } + }, + "morphology_2pass_scalar_advanced[InOutDataType=uint8][shape=32x1080x1920][kernelSize=3x3][morphType=CLOSE][border=REPLICATE][layout=NHWC][inputKind=VarShape][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1498.7439101811601, + "gpu_time_us_python": 1517.87440236962, + "gpu_noise_us_cpp": 2.504, + "gpu_noise_us_python": 5.26, + "gpu_bwutil_cpp": 0.11387359875639029, + "gpu_bwutil_python": 0.11243846875428445 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1322.28862094916, + "gpu_time_us_python": 1337.95760632622, + "gpu_noise_us_cpp": 2.43, + "gpu_noise_us_python": 5.858, + "gpu_bwutil_cpp": 0.09849760994419739, + "gpu_bwutil_python": 0.09733046003390994 + } + }, + "morphology_2pass_scalar_advanced[InOutDataType=uint8][shape=32x1080x1920][kernelSize=3x3][morphType=OPEN][border=REPLICATE][layout=NHWC][inputKind=Tensor][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1161.6631246701002, + "gpu_time_us_python": 1164.1069379384, + "gpu_noise_us_cpp": 2.1079999999999997, + "gpu_noise_us_python": 4.529999999999999, + "gpu_bwutil_cpp": 0.14691594808007968, + "gpu_bwutil_python": 0.14660787673745146 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 946.1282758936001, + "gpu_time_us_python": 955.1731994130402, + "gpu_noise_us_cpp": 1.0459999999999998, + "gpu_noise_us_python": 3.4540000000000006, + "gpu_bwutil_cpp": 0.13761475670445494, + "gpu_bwutil_python": 0.13630830612480477 + } + }, + "morphology_2pass_scalar_advanced[InOutDataType=uint8][shape=32x1080x1920][kernelSize=3x3][morphType=OPEN][border=REPLICATE][layout=NHWC][inputKind=VarShape][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1498.20721820352, + "gpu_time_us_python": 1515.2862827888, + "gpu_noise_us_cpp": 3.0060000000000002, + "gpu_noise_us_python": 4.308, + "gpu_bwutil_cpp": 0.11391402380209925, + "gpu_bwutil_python": 0.11263005388363745 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1322.75717128962, + "gpu_time_us_python": 1335.88590730982, + "gpu_noise_us_cpp": 1.9579999999999997, + "gpu_noise_us_python": 2.8760000000000003, + "gpu_bwutil_cpp": 0.09846214200303968, + "gpu_bwutil_python": 0.0974882019633915 + } + } + } + }, + "morphology_erode_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "kernelSize": [ + "3x3" + ], + "morphType": [ + "ERODE" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "int64_axes": { + "iteration": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Morphology single-channel erode (advanced)." + }, + "baselines": { + "morphology_erode_1080p_advanced[InOutDataType=float32][shape=64x1080x1920][kernelSize=3x3][morphType=ERODE][border=REPLICATE][layout=NHWC][inputKind=Tensor][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1217.94288841544, + "gpu_time_us_python": 1229.0088362553001, + "gpu_noise_us_cpp": 1.8419999999999999, + "gpu_noise_us_python": 2.8139999999999996, + "gpu_bwutil_cpp": 0.5605086010429406, + "gpu_bwutil_python": 0.5554618755494001 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1005.1285891637599, + "gpu_time_us_python": 1015.620115335, + "gpu_noise_us_cpp": 2.2439999999999998, + "gpu_noise_us_python": 2.4699999999999998, + "gpu_bwutil_cpp": 0.5180933403351495, + "gpu_bwutil_python": 0.512731140819756 + } + }, + "morphology_erode_1080p_advanced[InOutDataType=float32][shape=64x1080x1920][kernelSize=3x3][morphType=ERODE][border=REPLICATE][layout=NHWC][inputKind=VarShape][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1492.60125567692, + "gpu_time_us_python": 1506.3470165086203, + "gpu_noise_us_cpp": 2.3120000000000003, + "gpu_noise_us_python": 3.5879999999999996, + "gpu_bwutil_cpp": 0.4573686348327038, + "gpu_bwutil_python": 0.453193884480502 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1309.0034917829003, + "gpu_time_us_python": 1323.2822350243, + "gpu_noise_us_cpp": 1.508, + "gpu_noise_us_python": 3.3819999999999992, + "gpu_bwutil_cpp": 0.3979666553212048, + "gpu_bwutil_python": 0.39365694618887204 + } + }, + "morphology_erode_1080p_advanced[InOutDataType=uint8][shape=64x1080x1920][kernelSize=3x3][morphType=ERODE][border=REPLICATE][layout=NHWC][inputKind=Tensor][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1170.81669624716, + "gpu_time_us_python": 1182.65954648068, + "gpu_noise_us_cpp": 1.658, + "gpu_noise_us_python": 2.936, + "gpu_bwutil_cpp": 0.14576738013225704, + "gpu_bwutil_python": 0.14430861958421398 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 941.3840343541799, + "gpu_time_us_python": 951.1519303192399, + "gpu_noise_us_cpp": 1.422, + "gpu_noise_us_python": 3.09, + "gpu_bwutil_cpp": 0.13830852517904324, + "gpu_bwutil_python": 0.13688436780692556 + } + }, + "morphology_erode_1080p_advanced[InOutDataType=uint8][shape=64x1080x1920][kernelSize=3x3][morphType=ERODE][border=REPLICATE][layout=NHWC][inputKind=VarShape][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1489.79565472824, + "gpu_time_us_python": 1502.6691108406, + "gpu_noise_us_cpp": 3.158, + "gpu_noise_us_python": 4.851999999999999, + "gpu_bwutil_cpp": 0.11455725049191086, + "gpu_bwutil_python": 0.11357587250198116 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1313.63905613698, + "gpu_time_us_python": 1326.10888783048, + "gpu_noise_us_cpp": 2.01, + "gpu_noise_us_python": 3.8619999999999997, + "gpu_bwutil_cpp": 0.09914530476562615, + "gpu_bwutil_python": 0.09820987170430188 + } + } + } + }, + "morphology_dilate_iter2_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "kernelSize": [ + "5x5" + ], + "morphType": [ + "DILATE" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "int64_axes": { + "iteration": [ + 2 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Morphology single-channel dilate iter2 (advanced)." + }, + "baselines": { + "morphology_dilate_iter2_1080p_advanced[InOutDataType=float32][shape=16x1080x1920][kernelSize=5x5][morphType=DILATE][border=REPLICATE][layout=NHWC][inputKind=Tensor][iteration=2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1654.7950798862598, + "gpu_time_us_python": 1666.6765473620198, + "gpu_noise_us_cpp": 3.192, + "gpu_noise_us_python": 4.078, + "gpu_bwutil_cpp": 0.4125402940460717, + "gpu_bwutil_python": 0.40959847011329564 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1568.1176592433999, + "gpu_time_us_python": 1579.67876150146, + "gpu_noise_us_cpp": 1.7600000000000002, + "gpu_noise_us_python": 4.134, + "gpu_bwutil_cpp": 0.33225089034603367, + "gpu_bwutil_python": 0.3298159072228112 + } + }, + "morphology_dilate_iter2_1080p_advanced[InOutDataType=float32][shape=16x1080x1920][kernelSize=5x5][morphType=DILATE][border=REPLICATE][layout=NHWC][inputKind=VarShape][iteration=2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1698.0026941918, + "gpu_time_us_python": 1713.2567593095803, + "gpu_noise_us_cpp": 4.822, + "gpu_noise_us_python": 5.626, + "gpu_bwutil_cpp": 0.4020419747520319, + "gpu_bwutil_python": 0.3984643521949217 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1529.3345981294601, + "gpu_time_us_python": 1543.71456095466, + "gpu_noise_us_cpp": 1.7439999999999998, + "gpu_noise_us_python": 3.7880000000000003, + "gpu_bwutil_cpp": 0.340675331441913, + "gpu_bwutil_python": 0.33746728640433304 + } + }, + "morphology_dilate_iter2_1080p_advanced[InOutDataType=uint8][shape=16x1080x1920][kernelSize=5x5][morphType=DILATE][border=REPLICATE][layout=NHWC][inputKind=Tensor][iteration=2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1683.67731363176, + "gpu_time_us_python": 1698.28715689762, + "gpu_noise_us_cpp": 3.7040000000000006, + "gpu_noise_us_python": 4.148, + "gpu_bwutil_cpp": 0.10136560872304021, + "gpu_bwutil_python": 0.10049391734948938 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1549.35782718596, + "gpu_time_us_python": 1561.5565968091, + "gpu_noise_us_cpp": 1.486, + "gpu_noise_us_python": 2.634, + "gpu_bwutil_cpp": 0.08406946744400609, + "gpu_bwutil_python": 0.08341307443301707 + } + }, + "morphology_dilate_iter2_1080p_advanced[InOutDataType=uint8][shape=16x1080x1920][kernelSize=5x5][morphType=DILATE][border=REPLICATE][layout=NHWC][inputKind=VarShape][iteration=2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1685.0880615640804, + "gpu_time_us_python": 1701.0741722859398, + "gpu_noise_us_cpp": 3.0700000000000003, + "gpu_noise_us_python": 4.828, + "gpu_bwutil_cpp": 0.10128080703105335, + "gpu_bwutil_python": 0.10032897371303018 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1503.11400542714, + "gpu_time_us_python": 1516.78259838132, + "gpu_noise_us_cpp": 1.9480000000000004, + "gpu_noise_us_python": 3.184, + "gpu_bwutil_cpp": 0.08665406961334243, + "gpu_bwutil_python": 0.08586867208245999 + } + } + } + }, + "morphology_rgb_dilate_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "48x1080x1920" + ], + "kernelSize": [ + "3x3" + ], + "morphType": [ + "DILATE" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "int64_axes": { + "iteration": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Morphology RGB8 dilate (advanced)." + }, + "baselines": { + "morphology_rgb_dilate_1080p_advanced[InOutDataType=uchar3][shape=48x1080x1920][kernelSize=3x3][morphType=DILATE][border=REPLICATE][layout=NHWC][inputKind=Tensor][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2262.1193361711403, + "gpu_time_us_python": 2274.7476688278402, + "gpu_noise_us_cpp": 2.632, + "gpu_noise_us_python": 6.558000000000002, + "gpu_bwutil_cpp": 0.16975302331932274, + "gpu_bwutil_python": 0.16881040305252054 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1986.1648491166802, + "gpu_time_us_python": 1998.7148674651999, + "gpu_noise_us_cpp": 1.952, + "gpu_noise_us_python": 5.984, + "gpu_bwutil_cpp": 0.14755818880026356, + "gpu_bwutil_python": 0.14663208601439154 + } + }, + "morphology_rgb_dilate_1080p_advanced[InOutDataType=uchar3][shape=48x1080x1920][kernelSize=3x3][morphType=DILATE][border=REPLICATE][layout=NHWC][inputKind=VarShape][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2571.67193608708, + "gpu_time_us_python": 2588.53265428046, + "gpu_noise_us_cpp": 2.9259999999999997, + "gpu_noise_us_python": 4.452, + "gpu_bwutil_cpp": 0.1493197247884635, + "gpu_bwutil_python": 0.148347400852278 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2239.93822262598, + "gpu_time_us_python": 2254.93051408546, + "gpu_noise_us_cpp": 2.1, + "gpu_noise_us_python": 6.622, + "gpu_bwutil_cpp": 0.13083744615749757, + "gpu_bwutil_python": 0.12996247726401824 + } + } + } + }, + "morphology_rgb_dilate_1080p_planar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "48x1080x1920" + ], + "kernelSize": [ + "3x3" + ], + "morphType": [ + "DILATE" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "int64_axes": { + "iteration": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Morphology RGB8 dilate (advanced) (native planar NCHW)." + }, + "baselines": { + "morphology_rgb_dilate_1080p_planar_nchw_advanced[InOutDataType=uchar3][shape=48x1080x1920][kernelSize=3x3][morphType=DILATE][border=REPLICATE][layout=NCHW][inputKind=Tensor][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2584.5506579605, + "gpu_time_us_python": 2598.6523008837603, + "gpu_noise_us_cpp": 3.02, + "gpu_noise_us_python": 5.465999999999999, + "gpu_bwutil_cpp": 0.14857529662895147, + "gpu_bwutil_python": 0.14776913441501066 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2099.3550290448998, + "gpu_time_us_python": 2110.70509948638, + "gpu_noise_us_cpp": 2.05, + "gpu_noise_us_python": 2.832, + "gpu_bwutil_cpp": 0.13954497558063025, + "gpu_bwutil_python": 0.13879237564705513 + } + }, + "morphology_rgb_dilate_1080p_planar_nchw_advanced[InOutDataType=uchar3][shape=48x1080x1920][kernelSize=3x3][morphType=DILATE][border=REPLICATE][layout=NCHW][inputKind=VarShape][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2921.1886173070397, + "gpu_time_us_python": 2937.0861593251, + "gpu_noise_us_cpp": 3.5840000000000005, + "gpu_noise_us_python": 4.992, + "gpu_bwutil_cpp": 0.1314537555384397, + "gpu_bwutil_python": 0.13074207227850682 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2544.69786132614, + "gpu_time_us_python": 2559.28774032032, + "gpu_noise_us_cpp": 2.2199999999999998, + "gpu_noise_us_python": 4.6819999999999995, + "gpu_bwutil_cpp": 0.11517589221380028, + "gpu_bwutil_python": 0.11451179898574412 + } + } + } + }, + "morphology_rgb_dilate_1080p_fakeplanar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "48x1080x1920" + ], + "kernelSize": [ + "3x3" + ], + "morphType": [ + "DILATE" + ], + "border": [ + "REPLICATE" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "iteration": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Morphology RGB8 dilate (advanced) (fake-planar NCHW_FAKE tensor comparison)." + }, + "baselines": { + "morphology_rgb_dilate_1080p_fakeplanar_nchw_advanced[InOutDataType=uchar3][shape=48x1080x1920][kernelSize=3x3][morphType=DILATE][border=REPLICATE][layout=NCHW_FAKE][inputKind=Tensor][iteration=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4268.535285885, + "gpu_time_us_python": 4289.8900484634005, + "gpu_noise_us_cpp": 1.464, + "gpu_noise_us_python": 3.5940000000000003, + "gpu_bwutil_cpp": 0.26988273194148577, + "gpu_bwutil_python": 0.26853879358962934 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3722.4969177282596, + "gpu_time_us_python": 3737.86503173112, + "gpu_noise_us_cpp": 3.434, + "gpu_noise_us_python": 5.934, + "gpu_bwutil_cpp": 0.23611370965830009, + "gpu_bwutil_python": 0.23513413878133602 + } + } + } + } + } +} diff --git a/bench/config/operators/nonmaximumsuppression.json b/bench/config/operators/nonmaximumsuppression.json new file mode 100644 index 000000000..b42827352 --- /dev/null +++ b/bench/config/operators/nonmaximumsuppression.json @@ -0,0 +1,255 @@ +{ + "benchmark": "nonmaximumsuppression", + "configs": { + "nonmaximumsuppression_basic": { + "tier": "basic", + "dtypes": [ + "short4" + ], + "string_axes": { + "shape": [ + "32x4096" + ], + "inputKind": [ + "Tensor" + ] + }, + "float64_axes": { + "scoreThreshold": [ + 0.5 + ], + "iouThreshold": [ + 0.5 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "NonMaximumSuppression RGBA (basic)." + }, + "baselines": { + "nonmaximumsuppression_basic[InOutDataType=short4][shape=32x4096][inputKind=Tensor][scoreThreshold=0.5][iouThreshold=0.5]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 509.06436599095997, + "gpu_time_us_python": 519.1401052531, + "gpu_noise_us_cpp": 1.9380000000000002, + "gpu_noise_us_python": 3.12, + "gpu_bwutil_cpp": 0.0023178527636519798, + "gpu_bwutil_python": 0.002272864097837, + "gpu_gap_stddev_us": 2.011571407860709 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 517.7977392926799, + "gpu_time_us_python": 527.2234714960199, + "gpu_noise_us_cpp": 2.274, + "gpu_noise_us_python": 3.4779999999999993, + "gpu_bwutil_cpp": 0.00173862049499508, + "gpu_bwutil_python": 0.00170751575444558, + "gpu_gap_stddev_us": 3.2526740290481 + } + } + } + }, + "nonmaximumsuppression_boxes4096_advanced": { + "tier": "advanced", + "dtypes": [ + "short4" + ], + "string_axes": { + "shape": [ + "64x4096" + ], + "inputKind": [ + "Tensor" + ] + }, + "float64_axes": { + "scoreThreshold": [ + 0.5 + ], + "iouThreshold": [ + 0.5 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "NonMaximumSuppression RGBA boxes4096 (advanced)." + }, + "baselines": { + "nonmaximumsuppression_boxes4096_advanced[InOutDataType=short4][shape=64x4096][inputKind=Tensor][scoreThreshold=0.5][iouThreshold=0.5]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 641.35213183136, + "gpu_time_us_python": 649.9283951043401, + "gpu_noise_us_cpp": 26.898000000000003, + "gpu_noise_us_python": 27.274, + "gpu_bwutil_cpp": 0.00367949507700376, + "gpu_bwutil_python": 0.00363093058810348, + "gpu_gap_stddev_us": 2.922446045925447 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 624.7443781169601, + "gpu_time_us_python": 635.57327011442, + "gpu_noise_us_cpp": 16.426, + "gpu_noise_us_python": 17.273999999999997, + "gpu_bwutil_cpp": 0.0028822158829737397, + "gpu_bwutil_python": 0.00283341713136082, + "gpu_gap_stddev_us": 4.004787283509894 + } + } + } + }, + "nonmaximumsuppression_low_score_advanced": { + "tier": "advanced", + "dtypes": [ + "short4" + ], + "string_axes": { + "shape": [ + "64x4096" + ], + "inputKind": [ + "Tensor" + ] + }, + "float64_axes": { + "scoreThreshold": [ + 0.1 + ], + "iouThreshold": [ + 0.5 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "NonMaximumSuppression RGBA low score (advanced)." + }, + "baselines": { + "nonmaximumsuppression_low_score_advanced[InOutDataType=short4][shape=64x4096][inputKind=Tensor][scoreThreshold=0.1][iouThreshold=0.5]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 649.8814870232201, + "gpu_time_us_python": 659.1737912566999, + "gpu_noise_us_cpp": 26.654000000000003, + "gpu_noise_us_python": 26.516, + "gpu_bwutil_cpp": 0.00363119034159834, + "gpu_bwutil_python": 0.00358001183362626, + "gpu_gap_stddev_us": 1.6113270867461176 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 634.26206086144, + "gpu_time_us_python": 642.38146249188, + "gpu_noise_us_cpp": 11.905999999999999, + "gpu_noise_us_python": 11.308000000000002, + "gpu_bwutil_cpp": 0.00283896950546174, + "gpu_bwutil_python": 0.00280303761212002, + "gpu_gap_stddev_us": 2.067888395379013 + } + } + } + }, + "nonmaximumsuppression_boxes8192_advanced": { + "tier": "advanced", + "dtypes": [ + "short4" + ], + "string_axes": { + "shape": [ + "16x8192" + ], + "inputKind": [ + "Tensor" + ] + }, + "float64_axes": { + "scoreThreshold": [ + 0.5 + ], + "iouThreshold": [ + 0.5 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "NonMaximumSuppression RGBA boxes8192 (advanced)." + }, + "baselines": { + "nonmaximumsuppression_boxes8192_advanced[InOutDataType=short4][shape=16x8192][inputKind=Tensor][scoreThreshold=0.5][iouThreshold=0.5]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 977.2948929395001, + "gpu_time_us_python": 985.36386858884, + "gpu_noise_us_cpp": 3.364, + "gpu_noise_us_python": 4.319999999999999, + "gpu_bwutil_cpp": 0.0012073362364721598, + "gpu_bwutil_python": 0.00119744523082356, + "gpu_gap_stddev_us": 2.2388140037216697 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1002.97809297816, + "gpu_time_us_python": 1012.7631333716201, + "gpu_noise_us_cpp": 4.946, + "gpu_noise_us_python": 4.576, + "gpu_bwutil_cpp": 0.0008975710572987, + "gpu_bwutil_python": 0.00088889153733778, + "gpu_gap_stddev_us": 2.105429133471998 + } + } + } + }, + "nonmaximumsuppression_many_samples_advanced": { + "tier": "advanced", + "dtypes": [ + "short4" + ], + "string_axes": { + "shape": [ + "8192x256" + ], + "inputKind": [ + "Tensor" + ] + }, + "float64_axes": { + "scoreThreshold": [ + 0.5 + ], + "iouThreshold": [ + 0.5 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "NonMaximumSuppression RGBA many samples (advanced)." + }, + "baselines": { + "nonmaximumsuppression_many_samples_advanced[InOutDataType=short4][shape=8192x256][inputKind=Tensor][scoreThreshold=0.5][iouThreshold=0.5]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 634.50866864054, + "gpu_time_us_python": 644.88827520654, + "gpu_noise_us_cpp": 1.9979999999999998, + "gpu_noise_us_python": 2.786, + "gpu_bwutil_cpp": 0.02975340118962922, + "gpu_bwutil_python": 0.0292746224880144, + "gpu_gap_stddev_us": 1.6329929840918591 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 581.4405652019001, + "gpu_time_us_python": 591.5885361051801, + "gpu_noise_us_cpp": 2.064, + "gpu_noise_us_python": 2.996, + "gpu_bwutil_cpp": 0.0247752361534174, + "gpu_bwutil_python": 0.024349641552333663, + "gpu_gap_stddev_us": 1.7017652719534175 + } + } + } + } + } +} diff --git a/bench/config/operators/normalize.json b/bench/config/operators/normalize.json new file mode 100644 index 000000000..e5a46e276 --- /dev/null +++ b/bench/config/operators/normalize.json @@ -0,0 +1,1764 @@ +{ + "benchmark": "normalize", + "configs": { + "normalize_planar_nchw_1080p_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Normalize RGB8 planar (advanced)." + }, + "baselines": { + "normalize_planar_nchw_1080p_uchar3_advanced[InOutDataType=uchar3][shape=32x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 458.68022895248, + "gpu_time_us_python": 470.92078418252004, + "gpu_noise_us_cpp": 1.752, + "gpu_noise_us_python": 2.6659999999999995, + "gpu_bwutil_cpp": 0.5581258946180541, + "gpu_bwutil_python": 0.5436171407812032, + "gpu_gap_stddev_us": 0.692395429225779 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 454.8529300589, + "gpu_time_us_python": 468.593311262675, + "gpu_noise_us_cpp": 1.9125, + "gpu_noise_us_python": 2.8925, + "gpu_bwutil_cpp": 0.42929117424185, + "gpu_bwutil_python": 0.4167107687560697, + "gpu_gap_stddev_us": 0.9707182067890852 + } + }, + "normalize_planar_nchw_1080p_uchar3_advanced[InOutDataType=uchar3][shape=32x1080x1920][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 381.72980026952, + "gpu_time_us_python": 396.54116503654006, + "gpu_noise_us_cpp": 1.67, + "gpu_noise_us_python": 1.9780000000000002, + "gpu_bwutil_cpp": 0.6706336665283935, + "gpu_bwutil_python": 0.6455837880131481, + "gpu_gap_stddev_us": 0.9619658111130253 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 325.11960548025, + "gpu_time_us_python": 342.113534515425, + "gpu_noise_us_cpp": 1.5550000000000002, + "gpu_noise_us_python": 2.5975, + "gpu_bwutil_cpp": 0.6005804169666503, + "gpu_bwutil_python": 0.5707599424557666, + "gpu_gap_stddev_us": 1.5420290706790385 + } + } + } + }, + "normalize_planar_nchw_1080p_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Normalize RGBA8 planar (advanced) (tensor-only: RGBA8p planar var-shape unsupported by the Python image API)." + }, + "baselines": { + "normalize_planar_nchw_1080p_uchar4_advanced[InOutDataType=uchar4][shape=32x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 607.09879115972, + "gpu_time_us_python": 619.42055580824, + "gpu_noise_us_cpp": 0.924, + "gpu_noise_us_python": 3.3180000000000005, + "gpu_bwutil_cpp": 0.5622409505017725, + "gpu_bwutil_python": 0.5510548797067263, + "gpu_gap_stddev_us": 0.988225534606786 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 601.614544023275, + "gpu_time_us_python": 615.916869827925, + "gpu_noise_us_cpp": 1.5625000000000002, + "gpu_noise_us_python": 4.0375, + "gpu_bwutil_cpp": 0.4327648236383409, + "gpu_bwutil_python": 0.4227144395591745, + "gpu_gap_stddev_us": 0.9411877054544306 + } + } + } + }, + "normalize_planar_nchw_1080p_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Normalize RGBf32 planar (advanced)." + }, + "baselines": { + "normalize_planar_nchw_1080p_float3_advanced[InOutDataType=float3][shape=32x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1149.3752019120402, + "gpu_time_us_python": 1163.38214264356, + "gpu_noise_us_cpp": 1.4919999999999998, + "gpu_noise_us_python": 3.3920000000000003, + "gpu_bwutil_cpp": 0.890919383917779, + "gpu_bwutil_python": 0.8801928411377963, + "gpu_gap_stddev_us": 0.5519531564519139 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 876.8422793446, + "gpu_time_us_python": 889.462610260275, + "gpu_noise_us_cpp": 3.0349999999999997, + "gpu_noise_us_python": 3.2175, + "gpu_bwutil_cpp": 0.8907164143204618, + "gpu_bwutil_python": 0.8780797474198194, + "gpu_gap_stddev_us": 1.2329300751346945 + } + }, + "normalize_planar_nchw_1080p_float3_advanced[InOutDataType=float3][shape=32x1080x1920][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1176.72216623596, + "gpu_time_us_python": 1191.1469706012, + "gpu_noise_us_cpp": 2.56, + "gpu_noise_us_python": 4.606, + "gpu_bwutil_cpp": 0.8702141759014866, + "gpu_bwutil_python": 0.8596762659604323, + "gpu_gap_stddev_us": 1.5651311027544343 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 901.7067939850251, + "gpu_time_us_python": 918.2793801513001, + "gpu_noise_us_cpp": 3.255, + "gpu_noise_us_python": 5.17, + "gpu_bwutil_cpp": 0.8661551416761896, + "gpu_bwutil_python": 0.8505530576458653, + "gpu_gap_stddev_us": 6.248656437998007 + } + } + } + }, + "normalize_planar_nchw_1080p_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Normalize RGBAf32 planar (advanced)." + }, + "baselines": { + "normalize_planar_nchw_1080p_float4_advanced[InOutDataType=float4][shape=16x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 770.93399117538, + "gpu_time_us_python": 784.0200894719, + "gpu_noise_us_cpp": 2.046, + "gpu_noise_us_python": 1.998, + "gpu_bwutil_cpp": 0.885506508930467, + "gpu_bwutil_python": 0.8707268396648452, + "gpu_gap_stddev_us": 0.7833583634122671 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 588.8121490257749, + "gpu_time_us_python": 605.05164709565, + "gpu_noise_us_cpp": 1.4375, + "gpu_noise_us_python": 4.27, + "gpu_bwutil_cpp": 0.8842865728878128, + "gpu_bwutil_python": 0.8606180250361726, + "gpu_gap_stddev_us": 5.85513831751228 + } + }, + "normalize_planar_nchw_1080p_float4_advanced[InOutDataType=float4][shape=16x1080x1920][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 786.14744314284, + "gpu_time_us_python": 799.72243209676, + "gpu_noise_us_cpp": 2.054, + "gpu_noise_us_python": 2.94, + "gpu_bwutil_cpp": 0.868369878882479, + "gpu_bwutil_python": 0.8536301821908637, + "gpu_gap_stddev_us": 0.8330279130642091 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 603.2859299726749, + "gpu_time_us_python": 619.075278208725, + "gpu_noise_us_cpp": 2.3374999999999995, + "gpu_noise_us_python": 4.955, + "gpu_bwutil_cpp": 0.8630751428248443, + "gpu_bwutil_python": 0.8410847564013192, + "gpu_gap_stddev_us": 2.747709584637521 + } + } + } + }, + "normalize_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Normalize RGB8 (advanced)." + }, + "baselines": { + "normalize_uchar3_advanced[InOutDataType=uchar3][shape=64x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 847.7531197956199, + "gpu_time_us_python": 856.9614529365201, + "gpu_noise_us_cpp": 0.8300000000000001, + "gpu_noise_us_python": 1.916, + "gpu_bwutil_cpp": 0.6039532306325689, + "gpu_bwutil_python": 0.5974641089268775, + "gpu_gap_stddev_us": 0.5675790491203725 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 679.5455205048, + "gpu_time_us_python": 692.748437908225, + "gpu_noise_us_cpp": 1.3175, + "gpu_noise_us_python": 3.8400000000000003, + "gpu_bwutil_cpp": 0.5746797054307046, + "gpu_bwutil_python": 0.5637167363188045, + "gpu_gap_stddev_us": 1.861891753245365 + } + }, + "normalize_uchar3_advanced[InOutDataType=uchar3][shape=64x1080x1920][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 914.65104552776, + "gpu_time_us_python": 930.1700004514199, + "gpu_noise_us_cpp": 1.164, + "gpu_noise_us_python": 3.622, + "gpu_bwutil_cpp": 0.5597798553367698, + "gpu_bwutil_python": 0.5504401953433613, + "gpu_gap_stddev_us": 1.2884267179861995 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 713.8168939957001, + "gpu_time_us_python": 731.088142335125, + "gpu_noise_us_cpp": 1.275, + "gpu_noise_us_python": 3.3549999999999995, + "gpu_bwutil_cpp": 0.5470890120642433, + "gpu_bwutil_python": 0.5341593631218491, + "gpu_gap_stddev_us": 1.9331020335965607 + } + } + } + }, + "normalize_2160x3840_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "16x2160x3840" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Normalize RGB8 (advanced)." + }, + "baselines": { + "normalize_2160x3840_uchar3_advanced[InOutDataType=uchar3][shape=16x2160x3840][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 842.96754298498, + "gpu_time_us_python": 855.2729940487, + "gpu_noise_us_cpp": 2.224, + "gpu_noise_us_python": 2.5, + "gpu_bwutil_cpp": 0.60738065540931, + "gpu_bwutil_python": 0.5986413129446426, + "gpu_gap_stddev_us": 0.6871263222406987 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 679.046328169075, + "gpu_time_us_python": 694.3788787319751, + "gpu_noise_us_cpp": 2.0025, + "gpu_noise_us_python": 4.4375, + "gpu_bwutil_cpp": 0.5751032117621125, + "gpu_bwutil_python": 0.562400482322253, + "gpu_gap_stddev_us": 2.887933747384782 + } + }, + "normalize_2160x3840_uchar3_advanced[InOutDataType=uchar3][shape=16x2160x3840][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 906.0048439233, + "gpu_time_us_python": 921.062769184, + "gpu_noise_us_cpp": 1.3679999999999999, + "gpu_noise_us_python": 2.0040000000000004, + "gpu_bwutil_cpp": 0.5651208253237678, + "gpu_bwutil_python": 0.5558842574382087, + "gpu_gap_stddev_us": 1.7917408388562976 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 706.8856644574249, + "gpu_time_us_python": 724.531983856075, + "gpu_noise_us_cpp": 1.3199999999999998, + "gpu_noise_us_python": 3.0175, + "gpu_bwutil_cpp": 0.5524566953414813, + "gpu_bwutil_python": 0.5390169235690361, + "gpu_gap_stddev_us": 4.108745155179318 + } + } + } + }, + "normalize_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Normalize RGBf32 (advanced)." + }, + "baselines": { + "normalize_float3_advanced[InOutDataType=float3][shape=32x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1169.23379073628, + "gpu_time_us_python": 1177.3452203597801, + "gpu_noise_us_cpp": 1.474, + "gpu_noise_us_python": 3.4159999999999995, + "gpu_bwutil_cpp": 0.8757877279445504, + "gpu_bwutil_python": 0.869754409893201, + "gpu_gap_stddev_us": 1.0237169919486115 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 927.92929310575, + "gpu_time_us_python": 937.41730183185, + "gpu_noise_us_cpp": 2.8875, + "gpu_noise_us_python": 4.715, + "gpu_bwutil_cpp": 0.8416799299342818, + "gpu_bwutil_python": 0.8331667192243289, + "gpu_gap_stddev_us": 2.3775706873146656 + } + }, + "normalize_float3_advanced[InOutDataType=float3][shape=32x1080x1920][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1179.52582021248, + "gpu_time_us_python": 1194.5847664560001, + "gpu_noise_us_cpp": 2.5340000000000003, + "gpu_noise_us_python": 2.768, + "gpu_bwutil_cpp": 0.8681459281231929, + "gpu_bwutil_python": 0.857202534277522, + "gpu_gap_stddev_us": 1.0353158079411902 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 935.6339465507749, + "gpu_time_us_python": 951.6807028218501, + "gpu_noise_us_cpp": 3.6500000000000004, + "gpu_noise_us_python": 5.08, + "gpu_bwutil_cpp": 0.8347491161362366, + "gpu_bwutil_python": 0.8206807624776719, + "gpu_gap_stddev_us": 2.3481294713846625 + } + } + } + }, + "normalize_2160x3840_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "8x2160x3840" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Normalize RGBf32 (advanced)." + }, + "baselines": { + "normalize_2160x3840_float3_advanced[InOutDataType=float3][shape=8x2160x3840][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1163.2564144356, + "gpu_time_us_python": 1175.60585160764, + "gpu_noise_us_cpp": 2.032, + "gpu_noise_us_python": 2.836, + "gpu_bwutil_cpp": 0.8802875480304962, + "gpu_bwutil_python": 0.8710412048033991, + "gpu_gap_stddev_us": 1.3343139299572018 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 921.662749217975, + "gpu_time_us_python": 934.069912455675, + "gpu_noise_us_cpp": 2.6849999999999996, + "gpu_noise_us_python": 3.38, + "gpu_bwutil_cpp": 0.8474023259293799, + "gpu_bwutil_python": 0.8361475780161302, + "gpu_gap_stddev_us": 0.5693022803684276 + } + }, + "normalize_2160x3840_float3_advanced[InOutDataType=float3][shape=8x2160x3840][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1164.6404884027202, + "gpu_time_us_python": 1179.3204337461, + "gpu_noise_us_cpp": 2.1860000000000004, + "gpu_noise_us_python": 2.274, + "gpu_bwutil_cpp": 0.8792414294206014, + "gpu_bwutil_python": 0.8682978657483176, + "gpu_gap_stddev_us": 1.2282449058199387 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 921.0253942404499, + "gpu_time_us_python": 934.91197423795, + "gpu_noise_us_cpp": 2.3725, + "gpu_noise_us_python": 4.03, + "gpu_bwutil_cpp": 0.8479884599623807, + "gpu_bwutil_python": 0.8353936836325919, + "gpu_gap_stddev_us": 0.6927466870000215 + } + } + } + }, + "normalize_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Normalize RGBA8 (advanced)." + }, + "baselines": { + "normalize_uchar4_advanced[InOutDataType=uchar4][shape=64x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 951.5968441058, + "gpu_time_us_python": 962.3087217715599, + "gpu_noise_us_cpp": 1.206, + "gpu_noise_us_python": 2.5, + "gpu_bwutil_cpp": 0.7173941025470778, + "gpu_bwutil_python": 0.7094067424502958, + "gpu_gap_stddev_us": 1.2924635144937746 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 823.521505145475, + "gpu_time_us_python": 837.820158844, + "gpu_noise_us_cpp": 1.0499999999999998, + "gpu_noise_us_python": 5.609999999999999, + "gpu_bwutil_cpp": 0.6322791041991622, + "gpu_bwutil_python": 0.6214805481459126, + "gpu_gap_stddev_us": 2.0088268027204697 + } + }, + "normalize_uchar4_advanced[InOutDataType=uchar4][shape=64x1080x1920][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 974.7155305787801, + "gpu_time_us_python": 992.0856070661, + "gpu_noise_us_cpp": 1.028, + "gpu_noise_us_python": 2.902, + "gpu_bwutil_cpp": 0.7003779283385752, + "gpu_bwutil_python": 0.6881155613276289, + "gpu_gap_stddev_us": 1.0046970646715148 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 828.83178380955, + "gpu_time_us_python": 847.9529659773, + "gpu_noise_us_cpp": 2.395, + "gpu_noise_us_python": 4.5975, + "gpu_bwutil_cpp": 0.6282276649111559, + "gpu_bwutil_python": 0.6140579891112229, + "gpu_gap_stddev_us": 2.699900027753696 + } + } + } + }, + "normalize_2160x3840_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "16x2160x3840" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Normalize RGBA8 (advanced)." + }, + "baselines": { + "normalize_2160x3840_uchar4_advanced[InOutDataType=uchar4][shape=16x2160x3840][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 945.7386459450199, + "gpu_time_us_python": 958.4783529201599, + "gpu_noise_us_cpp": 1.4600000000000002, + "gpu_noise_us_python": 2.956, + "gpu_bwutil_cpp": 0.7218370600421855, + "gpu_bwutil_python": 0.712240977483513, + "gpu_gap_stddev_us": 1.3390741578317438 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 818.1502018825, + "gpu_time_us_python": 833.87278037025, + "gpu_noise_us_cpp": 2.675, + "gpu_noise_us_python": 4.83, + "gpu_bwutil_cpp": 0.6364254941669167, + "gpu_bwutil_python": 0.6244202821150383, + "gpu_gap_stddev_us": 2.493659580097417 + } + }, + "normalize_2160x3840_uchar4_advanced[InOutDataType=uchar4][shape=16x2160x3840][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 962.89190513556, + "gpu_time_us_python": 978.9975993800199, + "gpu_noise_us_cpp": 1.014, + "gpu_noise_us_python": 2.086, + "gpu_bwutil_cpp": 0.7089777689668764, + "gpu_bwutil_python": 0.6973127408322446, + "gpu_gap_stddev_us": 1.0363366146436706 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 821.2319656928499, + "gpu_time_us_python": 837.037902077425, + "gpu_noise_us_cpp": 2.515, + "gpu_noise_us_python": 3.27, + "gpu_bwutil_cpp": 0.6340419928405646, + "gpu_bwutil_python": 0.6220673750609782, + "gpu_gap_stddev_us": 3.4260813293755095 + } + } + } + }, + "normalize_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Normalize RGBAf32 (advanced)." + }, + "baselines": { + "normalize_float4_advanced[InOutDataType=float4][shape=16x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 771.27993818424, + "gpu_time_us_python": 783.5258549196002, + "gpu_noise_us_cpp": 1.136, + "gpu_noise_us_python": 2.322, + "gpu_bwutil_cpp": 0.8851092715102657, + "gpu_bwutil_python": 0.8712789670838615, + "gpu_gap_stddev_us": 1.5957184459626568 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 591.027034666275, + "gpu_time_us_python": 604.022452247, + "gpu_noise_us_cpp": 1.715, + "gpu_noise_us_python": 5.1925, + "gpu_bwutil_cpp": 0.8809735900672941, + "gpu_bwutil_python": 0.8620282321817588, + "gpu_gap_stddev_us": 1.8779923484486576 + } + }, + "normalize_float4_advanced[InOutDataType=float4][shape=16x1080x1920][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 774.4052255485599, + "gpu_time_us_python": 789.0838687209, + "gpu_noise_us_cpp": 2.228, + "gpu_noise_us_python": 2.4240000000000004, + "gpu_bwutil_cpp": 0.8815372970977128, + "gpu_bwutil_python": 0.8651425307986204, + "gpu_gap_stddev_us": 1.6618434374696163 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 595.2503371978, + "gpu_time_us_python": 611.9984016562249, + "gpu_noise_us_cpp": 2.2925, + "gpu_noise_us_python": 4.2825, + "gpu_bwutil_cpp": 0.8747237642852896, + "gpu_bwutil_python": 0.8507928428351916, + "gpu_gap_stddev_us": 2.4307874664397033 + } + } + } + }, + "normalize_2160x3840_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "4x2160x3840" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Normalize RGBAf32 (advanced)." + }, + "baselines": { + "normalize_2160x3840_float4_advanced[InOutDataType=float4][shape=4x2160x3840][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 769.0766659220001, + "gpu_time_us_python": 782.2658895516199, + "gpu_noise_us_cpp": 1.922, + "gpu_noise_us_python": 4.076, + "gpu_bwutil_cpp": 0.8876448626741128, + "gpu_bwutil_python": 0.8726799645808663, + "gpu_gap_stddev_us": 0.8852933471185632 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 589.5940858656751, + "gpu_time_us_python": 603.1479044007251, + "gpu_noise_us_cpp": 1.6500000000000001, + "gpu_noise_us_python": 4.0125, + "gpu_bwutil_cpp": 0.8831137064430868, + "gpu_bwutil_python": 0.8632767814253118, + "gpu_gap_stddev_us": 1.6660998911957885 + } + }, + "normalize_2160x3840_float4_advanced[InOutDataType=float4][shape=4x2160x3840][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 767.7386636748, + "gpu_time_us_python": 782.72236996468, + "gpu_noise_us_cpp": 1.524, + "gpu_noise_us_python": 2.218, + "gpu_bwutil_cpp": 0.8891916412471123, + "gpu_bwutil_python": 0.872170672047733, + "gpu_gap_stddev_us": 0.7380493333971845 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 591.1792907378999, + "gpu_time_us_python": 607.1221520556, + "gpu_noise_us_cpp": 1.6725, + "gpu_noise_us_python": 5.075, + "gpu_bwutil_cpp": 0.8807447280556548, + "gpu_bwutil_python": 0.8576522616878488, + "gpu_gap_stddev_us": 4.445576375956074 + } + } + } + }, + "normalize_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "128x1080x1920", + "32x2160x3840" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Normalize single-channel (advanced)." + }, + "baselines": { + "normalize_scalar_advanced[InOutDataType=float32][shape=128x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1789.22919808786, + "gpu_time_us_python": 1802.5016668876203, + "gpu_noise_us_cpp": 3.6740000000000004, + "gpu_noise_us_python": 3.276, + "gpu_bwutil_cpp": 0.763095985835172, + "gpu_bwutil_python": 0.7574748467976284, + "gpu_gap_stddev_us": 1.018871154893678 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1378.93623608285, + "gpu_time_us_python": 1393.745240183425, + "gpu_noise_us_cpp": 2.0774999999999997, + "gpu_noise_us_python": 4.69, + "gpu_bwutil_cpp": 0.7552806443583714, + "gpu_bwutil_python": 0.7472440296838758, + "gpu_gap_stddev_us": 1.2756327463834096 + } + }, + "normalize_scalar_advanced[InOutDataType=float32][shape=128x1080x1920][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1554.7144288601398, + "gpu_time_us_python": 1572.8082227022198, + "gpu_noise_us_cpp": 1.594, + "gpu_noise_us_python": 3.01, + "gpu_bwutil_cpp": 0.8781895967894575, + "gpu_bwutil_python": 0.8680872765223182, + "gpu_gap_stddev_us": 1.2255185648110243 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1179.942519149725, + "gpu_time_us_python": 1201.7351517327752, + "gpu_noise_us_cpp": 2.0949999999999998, + "gpu_noise_us_python": 5.1575, + "gpu_bwutil_cpp": 0.8825487773179388, + "gpu_bwutil_python": 0.8665492225351272, + "gpu_gap_stddev_us": 3.7803445590295026 + } + }, + "normalize_scalar_advanced[InOutDataType=float32][shape=32x2160x3840][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1823.52355566678, + "gpu_time_us_python": 1836.68413471938, + "gpu_noise_us_cpp": 2.9379999999999997, + "gpu_noise_us_python": 3.054, + "gpu_bwutil_cpp": 0.7487471491549341, + "gpu_bwutil_python": 0.7433787919295094, + "gpu_gap_stddev_us": 1.1320614871711672 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1405.452522644075, + "gpu_time_us_python": 1423.383347133, + "gpu_noise_us_cpp": 1.5000000000000002, + "gpu_noise_us_python": 4.1, + "gpu_bwutil_cpp": 0.7410811311131592, + "gpu_bwutil_python": 0.7317390201628162, + "gpu_gap_stddev_us": 2.1963273509507797 + } + }, + "normalize_scalar_advanced[InOutDataType=float32][shape=32x2160x3840][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1547.1666359606602, + "gpu_time_us_python": 1563.14622104164, + "gpu_noise_us_cpp": 1.6420000000000001, + "gpu_noise_us_python": 3.2700000000000005, + "gpu_bwutil_cpp": 0.8824734903365685, + "gpu_bwutil_python": 0.8734525106015099, + "gpu_gap_stddev_us": 1.067225853848066 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1169.582054715225, + "gpu_time_us_python": 1191.4733192535748, + "gpu_noise_us_cpp": 1.9625, + "gpu_noise_us_python": 5.665, + "gpu_bwutil_cpp": 0.8903660470794195, + "gpu_bwutil_python": 0.8740176982433081, + "gpu_gap_stddev_us": 4.629481519615476 + } + }, + "normalize_scalar_advanced[InOutDataType=uint8][shape=128x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 420.62763891792, + "gpu_time_us_python": 431.99426129131996, + "gpu_noise_us_cpp": 3.194, + "gpu_noise_us_python": 3.6380000000000003, + "gpu_bwutil_cpp": 0.8114882607456055, + "gpu_bwutil_python": 0.7901367558868851, + "gpu_gap_stddev_us": 0.8141424533265262 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 330.91455015655, + "gpu_time_us_python": 343.58675872317497, + "gpu_noise_us_cpp": 2.8175, + "gpu_noise_us_python": 3.9725, + "gpu_bwutil_cpp": 0.786863956345617, + "gpu_bwutil_python": 0.7577678076688665, + "gpu_gap_stddev_us": 2.047974866679015 + } + }, + "normalize_scalar_advanced[InOutDataType=uint8][shape=128x1080x1920][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 521.43879080356, + "gpu_time_us_python": 545.31870381394, + "gpu_noise_us_cpp": 3.8340000000000005, + "gpu_noise_us_python": 3.7640000000000002, + "gpu_bwutil_cpp": 0.6546019057190289, + "gpu_bwutil_python": 0.625935709174561, + "gpu_gap_stddev_us": 1.405525400893502 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 399.713034884175, + "gpu_time_us_python": 417.395309130675, + "gpu_noise_us_cpp": 2.79, + "gpu_noise_us_python": 4.2475000000000005, + "gpu_bwutil_cpp": 0.6513205315249038, + "gpu_bwutil_python": 0.6237591595509842, + "gpu_gap_stddev_us": 3.945220676260735 + } + }, + "normalize_scalar_advanced[InOutDataType=uint8][shape=32x2160x3840][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 416.12157030228, + "gpu_time_us_python": 427.9220150034201, + "gpu_noise_us_cpp": 3.1540000000000004, + "gpu_noise_us_python": 3.522, + "gpu_bwutil_cpp": 0.8202735766019875, + "gpu_bwutil_python": 0.7976553751065472, + "gpu_gap_stddev_us": 0.7216367002223595 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 321.92970212525, + "gpu_time_us_python": 333.55007356685, + "gpu_noise_us_cpp": 2.34, + "gpu_noise_us_python": 3.7600000000000002, + "gpu_bwutil_cpp": 0.8087337747862655, + "gpu_bwutil_python": 0.780543927258336, + "gpu_gap_stddev_us": 1.0070454017577466 + } + }, + "normalize_scalar_advanced[InOutDataType=uint8][shape=32x2160x3840][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 447.75070385228, + "gpu_time_us_python": 461.29018083436006, + "gpu_noise_us_cpp": 2.6719999999999997, + "gpu_noise_us_python": 3.164, + "gpu_bwutil_cpp": 0.7623298667662286, + "gpu_bwutil_python": 0.7399558800337662, + "gpu_gap_stddev_us": 1.0550193324575143 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 370.0511879791251, + "gpu_time_us_python": 387.91053567185, + "gpu_noise_us_cpp": 2.7, + "gpu_noise_us_python": 4.3275, + "gpu_bwutil_cpp": 0.7035342460209988, + "gpu_bwutil_python": 0.6711876189612072, + "gpu_gap_stddev_us": 3.0543924688333624 + } + } + } + }, + "normalize_rgb_u8_1080p_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "96x1080x1920" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "TensorScalar", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Normalize RGB8 (basic)." + }, + "baselines": { + "normalize_rgb_u8_1080p_basic[InOutDataType=uchar3][shape=96x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1264.01687067388, + "gpu_time_us_python": 1274.4799194579798, + "gpu_noise_us_cpp": 1.236, + "gpu_noise_us_python": 4.935999999999999, + "gpu_bwutil_cpp": 0.6075900958112064, + "gpu_bwutil_python": 0.602601076669601, + "gpu_gap_stddev_us": 3.703039992129263 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1010.986865537825, + "gpu_time_us_python": 1024.140037034925, + "gpu_noise_us_cpp": 2.7350000000000003, + "gpu_noise_us_python": 4.234999999999999, + "gpu_bwutil_cpp": 0.5794171073249685, + "gpu_bwutil_python": 0.5719728008988635, + "gpu_gap_stddev_us": 0.4560933504181047 + } + }, + "normalize_rgb_u8_1080p_basic[InOutDataType=uchar3][shape=96x1080x1920][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1364.74817199566, + "gpu_time_us_python": 1383.7249974519, + "gpu_noise_us_cpp": 2.13, + "gpu_noise_us_python": 5.890000000000001, + "gpu_bwutil_cpp": 0.5627429417531866, + "gpu_bwutil_python": 0.5550252248780522, + "gpu_gap_stddev_us": 0.9878341181361997 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1062.5481620998, + "gpu_time_us_python": 1082.294652475375, + "gpu_noise_us_cpp": 1.775, + "gpu_noise_us_python": 5.575, + "gpu_bwutil_cpp": 0.5513022293499232, + "gpu_bwutil_python": 0.5412523595270553, + "gpu_gap_stddev_us": 3.200130861841393 + } + }, + "normalize_rgb_u8_1080p_basic[InOutDataType=uchar3][shape=96x1080x1920][layout=NHWC][inputKind=TensorScalar]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1235.81161780892, + "gpu_time_us_python": 1253.7917464971201, + "gpu_noise_us_cpp": 1.0879999999999999, + "gpu_noise_us_python": 4.388, + "gpu_bwutil_cpp": 0.6214542767943529, + "gpu_bwutil_python": 0.6125430168837367, + "gpu_gap_stddev_us": 2.366574428345661 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 990.3001111413399, + "gpu_time_us_python": 1009.1389936625001, + "gpu_noise_us_cpp": 1.0559999999999998, + "gpu_noise_us_python": 4.552, + "gpu_bwutil_cpp": 0.5915158766813052, + "gpu_bwutil_python": 0.580469867944356, + "gpu_gap_stddev_us": 4.297481339668484 + } + } + } + }, + "normalize_rgb_f32_1080p_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Normalize RGBf32 tensor (basic)." + }, + "baselines": { + "normalize_rgb_f32_1080p_basic[InOutDataType=float3][shape=32x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1169.3189644367399, + "gpu_time_us_python": 1177.51379864968, + "gpu_noise_us_cpp": 2.4799999999999995, + "gpu_noise_us_python": 5.7, + "gpu_bwutil_cpp": 0.875723988361492, + "gpu_bwutil_python": 0.8696302769280226, + "gpu_gap_stddev_us": 1.732126795920202 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 928.1626351685001, + "gpu_time_us_python": 936.6051937827, + "gpu_noise_us_cpp": 3.1374999999999997, + "gpu_noise_us_python": 3.8, + "gpu_bwutil_cpp": 0.841469161218115, + "gpu_bwutil_python": 0.8338831799349865, + "gpu_gap_stddev_us": 0.6638105191200538 + } + } + } + }, + "normalize_rgb_f32_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Normalize RGBf32 var-shape (advanced)." + }, + "baselines": { + "normalize_rgb_f32_1080p_advanced[InOutDataType=float3][shape=32x1080x1920][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1179.54563228584, + "gpu_time_us_python": 1194.9526129053, + "gpu_noise_us_cpp": 1.8379999999999999, + "gpu_noise_us_python": 3.4619999999999997, + "gpu_bwutil_cpp": 0.8681313407677174, + "gpu_bwutil_python": 0.8569385682793762, + "gpu_gap_stddev_us": 0.9792482341528561 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 934.227747417325, + "gpu_time_us_python": 950.703166237375, + "gpu_noise_us_cpp": 2.7449999999999997, + "gpu_noise_us_python": 5.3775, + "gpu_bwutil_cpp": 0.8360045294523695, + "gpu_bwutil_python": 0.8215172579840373, + "gpu_gap_stddev_us": 1.1484186766651008 + } + } + } + }, + "normalize_planar_rgb_u8_nchw_1080p_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "96x1080x1920" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor", + "TensorScalar", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Normalize RGB8 planar (basic)." + }, + "baselines": { + "normalize_planar_rgb_u8_nchw_1080p_basic[InOutDataType=uchar3][shape=96x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1349.7410289820198, + "gpu_time_us_python": 1361.4234661516198, + "gpu_noise_us_cpp": 2.886, + "gpu_noise_us_python": 4.933999999999999, + "gpu_bwutil_cpp": 0.5690017421706216, + "gpu_bwutil_python": 0.5641178025496333, + "gpu_gap_stddev_us": 1.0514170375619871 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1337.9877757438749, + "gpu_time_us_python": 1353.54744166365, + "gpu_noise_us_cpp": 1.725, + "gpu_noise_us_python": 3.75, + "gpu_bwutil_cpp": 0.43782882894513425, + "gpu_bwutil_python": 0.4327978389449396, + "gpu_gap_stddev_us": 0.6573008352341673 + } + }, + "normalize_planar_rgb_u8_nchw_1080p_basic[InOutDataType=uchar3][shape=96x1080x1920][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1111.40697959832, + "gpu_time_us_python": 1128.2570427602, + "gpu_noise_us_cpp": 0.858, + "gpu_noise_us_python": 2.52, + "gpu_bwutil_cpp": 0.6910182262644223, + "gpu_bwutil_python": 0.6806967153106112, + "gpu_gap_stddev_us": 1.4975325522053582 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 943.806871469275, + "gpu_time_us_python": 962.5027427224, + "gpu_noise_us_cpp": 1.6475, + "gpu_noise_us_python": 6.42, + "gpu_bwutil_cpp": 0.6206648652003015, + "gpu_bwutil_python": 0.608609020082535, + "gpu_gap_stddev_us": 1.1727172497531049 + } + }, + "normalize_planar_rgb_u8_nchw_1080p_basic[InOutDataType=uchar3][shape=96x1080x1920][layout=NCHW][inputKind=TensorScalar]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1380.3880277570202, + "gpu_time_us_python": 1400.271390803, + "gpu_noise_us_cpp": 1.95, + "gpu_noise_us_python": 5.786, + "gpu_bwutil_cpp": 0.5563653758664221, + "gpu_bwutil_python": 0.5484665504456243, + "gpu_gap_stddev_us": 2.6953025602175478 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1289.4827138259602, + "gpu_time_us_python": 1306.07950169216, + "gpu_noise_us_cpp": 0.922, + "gpu_noise_us_python": 2.5620000000000003, + "gpu_bwutil_cpp": 0.4543009245022308, + "gpu_bwutil_python": 0.44852503592785486, + "gpu_gap_stddev_us": 1.1783352263464169 + } + } + } + }, + "normalize_planar_rgb_f32_nchw_1080p_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Normalize planar RGBf32 tensor (basic)." + }, + "baselines": { + "normalize_planar_rgb_f32_nchw_1080p_basic[InOutDataType=float3][shape=32x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1149.61851846182, + "gpu_time_us_python": 1163.67553101026, + "gpu_noise_us_cpp": 1.668, + "gpu_noise_us_python": 3.776, + "gpu_bwutil_cpp": 0.8907307414107837, + "gpu_bwutil_python": 0.8799713032139215, + "gpu_gap_stddev_us": 0.9133407413688591 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 876.111944290675, + "gpu_time_us_python": 890.63198366885, + "gpu_noise_us_cpp": 1.6150000000000002, + "gpu_noise_us_python": 3.1275, + "gpu_bwutil_cpp": 0.8914584049156958, + "gpu_bwutil_python": 0.8769260174484079, + "gpu_gap_stddev_us": 1.2821205261319912 + } + } + } + }, + "normalize_planar_rgb_f32_nchw_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Normalize planar RGBf32 var-shape (advanced)." + }, + "baselines": { + "normalize_planar_rgb_f32_nchw_1080p_advanced[InOutDataType=float3][shape=32x1080x1920][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1173.46667107014, + "gpu_time_us_python": 1189.97151579818, + "gpu_noise_us_cpp": 2.15, + "gpu_noise_us_python": 4.712000000000001, + "gpu_bwutil_cpp": 0.8726281277669337, + "gpu_bwutil_python": 0.8605253539206525, + "gpu_gap_stddev_us": 1.1104289119644088 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 899.250619619075, + "gpu_time_us_python": 915.174445272625, + "gpu_noise_us_cpp": 3.0949999999999998, + "gpu_noise_us_python": 5.735, + "gpu_bwutil_cpp": 0.8685205421692738, + "gpu_bwutil_python": 0.8534124197234517, + "gpu_gap_stddev_us": 1.7323796991337144 + } + } + } + }, + "normalize_fakeplanar_nchw_1080p_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Normalize RGB8 fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "normalize_fakeplanar_nchw_1080p_uchar3_advanced[InOutDataType=uchar3][shape=32x1080x1920][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1620.9780436965998, + "gpu_time_us_python": 1634.81830510888, + "gpu_noise_us_cpp": 3.94, + "gpu_noise_us_python": 3.2139999999999995, + "gpu_bwutil_cpp": 0.47379122819792074, + "gpu_bwutil_python": 0.46977962487517305, + "gpu_gap_stddev_us": 0.7335436705305447 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1277.62261396905, + "gpu_time_us_python": 1291.389955270975, + "gpu_noise_us_cpp": 2.215, + "gpu_noise_us_python": 5.944999999999999, + "gpu_bwutil_cpp": 0.4584861860738134, + "gpu_bwutil_python": 0.4535985045540897, + "gpu_gap_stddev_us": 0.4306911853331412 + } + } + } + }, + "normalize_fakeplanar_nchw_1080p_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Normalize RGBA8 fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "normalize_fakeplanar_nchw_1080p_uchar4_advanced[InOutDataType=uchar4][shape=32x1080x1920][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2019.38221230042, + "gpu_time_us_python": 2035.2468599023, + "gpu_noise_us_cpp": 1.2200000000000002, + "gpu_noise_us_python": 4.996, + "gpu_bwutil_cpp": 0.5070888650829959, + "gpu_bwutil_python": 0.5031357525799032, + "gpu_gap_stddev_us": 1.7719187505539813 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1630.14470583765, + "gpu_time_us_python": 1644.18503375475, + "gpu_noise_us_cpp": 1.165, + "gpu_noise_us_python": 2.8850000000000002, + "gpu_bwutil_cpp": 0.4791188883730786, + "gpu_bwutil_python": 0.47502730079923894, + "gpu_gap_stddev_us": 0.49665823182074137 + } + } + } + }, + "normalize_fakeplanar_nchw_1080p_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Normalize RGBf32 fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "normalize_fakeplanar_nchw_1080p_float3_advanced[InOutDataType=float3][shape=32x1080x1920][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3841.57010714206, + "gpu_time_us_python": 3854.1183798796, + "gpu_noise_us_cpp": 2.0780000000000003, + "gpu_noise_us_python": 2.7340000000000004, + "gpu_bwutil_cpp": 0.7996758925291523, + "gpu_bwutil_python": 0.7970722531120403, + "gpu_gap_stddev_us": 1.3674545471563104 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 3124.2071804072752, + "gpu_time_us_python": 3133.9140105118504, + "gpu_noise_us_cpp": 3.1774999999999998, + "gpu_noise_us_python": 5.5425, + "gpu_bwutil_cpp": 0.749969479838916, + "gpu_bwutil_python": 0.7476468498331565, + "gpu_gap_stddev_us": 2.5020807265373075 + } + } + } + }, + "normalize_fakeplanar_nchw_1080p_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Normalize RGBAf32 fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "normalize_fakeplanar_nchw_1080p_float4_advanced[InOutDataType=float4][shape=16x1080x1920][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2879.5037919886395, + "gpu_time_us_python": 2894.84091517564, + "gpu_noise_us_cpp": 2.73, + "gpu_noise_us_python": 3.3259999999999996, + "gpu_bwutil_cpp": 0.7112380192609242, + "gpu_bwutil_python": 0.7074689091747, + "gpu_gap_stddev_us": 1.1826893997331205 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2343.6604103195004, + "gpu_time_us_python": 2357.783047884, + "gpu_noise_us_cpp": 4.31, + "gpu_noise_us_python": 5.3525, + "gpu_bwutil_cpp": 0.6664945385184967, + "gpu_bwutil_python": 0.6625017342606003, + "gpu_gap_stddev_us": 1.8735737508566639 + } + } + } + }, + "normalize_fakeplanar_rgb_u8_nchw_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "96x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Normalize RGB8 fake-planar N*C=96 (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "normalize_fakeplanar_rgb_u8_nchw_1080p_advanced[InOutDataType=uchar3][shape=96x1080x1920][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4800.99079117542, + "gpu_time_us_python": 4812.5846800852, + "gpu_noise_us_cpp": 3.0440000000000005, + "gpu_noise_us_python": 3.16, + "gpu_bwutil_cpp": 0.4799044582839901, + "gpu_bwutil_python": 0.47874811885266044, + "gpu_gap_stddev_us": 0.8699421916650789 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 3768.85862759685, + "gpu_time_us_python": 3783.25664448495, + "gpu_noise_us_cpp": 1.225, + "gpu_noise_us_python": 4.3925, + "gpu_bwutil_cpp": 0.4662725712777708, + "gpu_bwutil_python": 0.46449942853232856, + "gpu_gap_stddev_us": 1.8914756605735203 + } + } + } + }, + "normalize_fakeplanar_rgb_f32_nchw_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Normalize RGBf32 fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "normalize_fakeplanar_rgb_f32_nchw_1080p_advanced[InOutDataType=float3][shape=32x1080x1920][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3841.20704983214, + "gpu_time_us_python": 3854.7947487130396, + "gpu_noise_us_cpp": 2.018, + "gpu_noise_us_python": 2.8240000000000003, + "gpu_bwutil_cpp": 0.7997513219917044, + "gpu_bwutil_python": 0.7969322090456068, + "gpu_gap_stddev_us": 1.8985414864042918 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 3124.78267167815, + "gpu_time_us_python": 3132.995226344525, + "gpu_noise_us_cpp": 3.9725, + "gpu_noise_us_python": 5.2625, + "gpu_bwutil_cpp": 0.7498310995789927, + "gpu_bwutil_python": 0.7478656657623605, + "gpu_gap_stddev_us": 2.0609611148820743 + } + } + } + } + }, + "normalize_fakeplanar_nchw_1080p_uchar3_advanced": { + "benchmark": "normalize", + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Normalize RGB8 fake-planar (reformat->interleaved->reformat) (advanced)." + } + }, + "normalize_fakeplanar_nchw_1080p_uchar4_advanced": { + "benchmark": "normalize", + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Normalize RGBA8 fake-planar (reformat->interleaved->reformat) (advanced)." + } + }, + "normalize_fakeplanar_nchw_1080p_float3_advanced": { + "benchmark": "normalize", + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Normalize RGBf32 fake-planar (reformat->interleaved->reformat) (advanced)." + } + }, + "normalize_fakeplanar_nchw_1080p_float4_advanced": { + "benchmark": "normalize", + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Normalize RGBAf32 fake-planar (reformat->interleaved->reformat) (advanced)." + } + }, + "normalize_fakeplanar_rgb_u8_nchw_1080p_advanced": { + "benchmark": "normalize", + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "96x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Normalize RGB8 fake-planar N*C=96 (reformat->interleaved->reformat) (advanced)." + } + }, + "normalize_fakeplanar_rgb_f32_nchw_1080p_advanced": { + "benchmark": "normalize", + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Normalize RGBf32 fake-planar (reformat->interleaved->reformat) (advanced)." + } + } +} diff --git a/bench/config/operators/osd.json b/bench/config/operators/osd.json new file mode 100644 index 000000000..d06d9726a --- /dev/null +++ b/bench/config/operators/osd.json @@ -0,0 +1,506 @@ +{ + "benchmark": "osd", + "configs": { + "osd_point_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3", + "uchar4" + ], + "string_axes": { + "shape": [ + "3x1080x1920" + ], + "elementType": [ + "POINT" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "numElem": [ + 100 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "OSD RGB/RGBA point at 100 elements (advanced)." + }, + "baselines": { + "osd_point_1080p_advanced[InOutDataType=uchar3][shape=3x1080x1920][elementType=POINT][layout=NHWC][inputKind=Tensor][numElem=100]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3215.3284904284, + "gpu_time_us_python": 3232.9209370649, + "gpu_noise_us_cpp": 19.308, + "gpu_noise_us_python": 19.472, + "gpu_bwutil_cpp": 0.007465549408066781, + "gpu_bwutil_python": 0.0074249639005401005, + "gpu_gap_stddev_us": 4.891137382882979 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2848.13214161174, + "gpu_time_us_python": 2864.2170080339397, + "gpu_noise_us_cpp": 16.646, + "gpu_noise_us_python": 20.387999999999998, + "gpu_bwutil_cpp": 0.006428156040402899, + "gpu_bwutil_python": 0.00639205367656596, + "gpu_gap_stddev_us": 3.2906041503025687 + } + }, + "osd_point_1080p_advanced[InOutDataType=uchar4][shape=3x1080x1920][elementType=POINT][layout=NHWC][inputKind=Tensor][numElem=100]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3215.4660263683, + "gpu_time_us_python": 3234.0571892566595, + "gpu_noise_us_cpp": 18.625999999999998, + "gpu_noise_us_python": 20.042, + "gpu_bwutil_cpp": 0.00995322236116564, + "gpu_bwutil_python": 0.009896012445151339, + "gpu_gap_stddev_us": 3.66257291096844 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2850.8818220007997, + "gpu_time_us_python": 2868.27527230432, + "gpu_noise_us_cpp": 16.881999999999998, + "gpu_noise_us_python": 16.182, + "gpu_bwutil_cpp": 0.008562235678082999, + "gpu_bwutil_python": 0.008510320595676819, + "gpu_gap_stddev_us": 3.2641790931089223 + } + }, + "osd_point_1080p_advanced[InOutDataType=uchar3][shape=3x1080x1920][elementType=POINT][layout=NCHW][inputKind=Tensor][numElem=100]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3345.7902971903195, + "gpu_time_us_python": 3359.2743231455, + "gpu_noise_us_cpp": 19.57, + "gpu_noise_us_python": 18.594, + "gpu_bwutil_cpp": 0.02152084580056628, + "gpu_bwutil_python": 0.0214344874440179, + "gpu_gap_stddev_us": 4.143939420087166 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2954.98127039736, + "gpu_time_us_python": 2966.6851390108804, + "gpu_noise_us_cpp": 16.392000000000003, + "gpu_noise_us_python": 20.306, + "gpu_bwutil_cpp": 0.018585025346988183, + "gpu_bwutil_python": 0.0185117099741816, + "gpu_gap_stddev_us": 2.8743398916151532 + } + }, + "osd_point_1080p_advanced[InOutDataType=uchar4][shape=3x1080x1920][elementType=POINT][layout=NCHW][inputKind=Tensor][numElem=100]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3380.62448224928, + "gpu_time_us_python": 3392.8662474090206, + "gpu_noise_us_cpp": 18.354, + "gpu_noise_us_python": 18.948, + "gpu_bwutil_cpp": 0.028398408323367673, + "gpu_bwutil_python": 0.02829594964158052, + "gpu_gap_stddev_us": 4.246056774430909 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2986.86460937768, + "gpu_time_us_python": 3000.84609156846, + "gpu_noise_us_cpp": 16.062, + "gpu_noise_us_python": 20.358, + "gpu_bwutil_cpp": 0.024515175123487037, + "gpu_bwutil_python": 0.024400944162402058, + "gpu_gap_stddev_us": 2.2163622743179547 + } + } + } + }, + "osd_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "3x1080x1920" + ], + "elementType": [ + "POINT" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "numElem": [ + 64 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "OSD RGB8 point (basic)." + }, + "baselines": { + "osd_basic[InOutDataType=uchar3][shape=3x1080x1920][elementType=POINT][layout=NHWC][inputKind=Tensor][numElem=64]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2080.1343240258, + "gpu_time_us_python": 2095.8291278299002, + "gpu_noise_us_cpp": 11.934000000000001, + "gpu_noise_us_python": 12.394000000000002, + "gpu_bwutil_cpp": 0.01153905645604112, + "gpu_bwutil_python": 0.011452619667839278, + "gpu_gap_stddev_us": 2.2489527306258994 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1840.7910120837398, + "gpu_time_us_python": 1855.26693750306, + "gpu_noise_us_cpp": 10.128, + "gpu_noise_us_python": 11.97, + "gpu_bwutil_cpp": 0.009945234545696461, + "gpu_bwutil_python": 0.009867643879926441, + "gpu_gap_stddev_us": 1.822700042405827 + } + }, + "osd_basic[InOutDataType=uchar3][shape=3x1080x1920][elementType=POINT][layout=NCHW][inputKind=Tensor][numElem=64]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2211.73807320946, + "gpu_time_us_python": 2226.94096429606, + "gpu_noise_us_cpp": 12.431999999999999, + "gpu_noise_us_python": 12.874, + "gpu_bwutil_cpp": 0.032554877277844584, + "gpu_bwutil_python": 0.03233265966913, + "gpu_gap_stddev_us": 3.6194834318462643 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1951.0428772415999, + "gpu_time_us_python": 1964.7468980401202, + "gpu_noise_us_cpp": 10.956, + "gpu_noise_us_python": 12.398, + "gpu_bwutil_cpp": 0.02814764995725408, + "gpu_bwutil_python": 0.027951322840356257, + "gpu_gap_stddev_us": 1.5078740182252663 + } + } + } + }, + "osd_rect_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "3x1080x1920" + ], + "elementType": [ + "RECT" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "numElem": [ + 128 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "OSD RGB8 rect (advanced)." + }, + "baselines": { + "osd_rect_1080p_advanced[InOutDataType=uchar3][shape=3x1080x1920][elementType=RECT][layout=NHWC][inputKind=Tensor][numElem=128]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1863.1419381798798, + "gpu_time_us_python": 1881.1132954217803, + "gpu_noise_us_cpp": 26.657999999999998, + "gpu_noise_us_python": 30.443999999999996, + "gpu_bwutil_cpp": 0.012884664850432801, + "gpu_bwutil_python": 0.0127615443670988, + "gpu_gap_stddev_us": 3.916559172224045 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1815.2087337427602, + "gpu_time_us_python": 1833.47936945892, + "gpu_noise_us_cpp": 26.768, + "gpu_noise_us_python": 29.101999999999997, + "gpu_bwutil_cpp": 0.01008650328253274, + "gpu_bwutil_python": 0.0099860414405954, + "gpu_gap_stddev_us": 4.774750816814977 + } + }, + "osd_rect_1080p_advanced[InOutDataType=uchar3][shape=3x1080x1920][elementType=RECT][layout=NCHW][inputKind=Tensor][numElem=128]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1973.3045978370799, + "gpu_time_us_python": 1989.27882184472, + "gpu_noise_us_cpp": 28.014, + "gpu_noise_us_python": 28.901999999999997, + "gpu_bwutil_cpp": 0.03648978603808796, + "gpu_bwutil_python": 0.036196775100562836, + "gpu_gap_stddev_us": 6.366943758055177 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1915.3776435937598, + "gpu_time_us_python": 1924.63329075914, + "gpu_noise_us_cpp": 27.137999999999998, + "gpu_noise_us_python": 27.49, + "gpu_bwutil_cpp": 0.0286728338417185, + "gpu_bwutil_python": 0.0285349520955605, + "gpu_gap_stddev_us": 2.5724646834685223 + } + } + } + }, + "osd_line_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "3x1080x1920" + ], + "elementType": [ + "LINE" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "numElem": [ + 64 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "OSD RGB8 line (advanced)." + }, + "baselines": { + "osd_line_1080p_advanced[InOutDataType=uchar3][shape=3x1080x1920][elementType=LINE][layout=NHWC][inputKind=Tensor][numElem=64]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2369.1912849122405, + "gpu_time_us_python": 2381.1409818300804, + "gpu_noise_us_cpp": 14.551999999999998, + "gpu_noise_us_python": 15.181999999999999, + "gpu_bwutil_cpp": 0.01013126177709796, + "gpu_bwutil_python": 0.01008040390953026, + "gpu_gap_stddev_us": 2.8267148758988547 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2092.0653371015806, + "gpu_time_us_python": 2106.6030305974205, + "gpu_noise_us_cpp": 13.984, + "gpu_noise_us_python": 14.608, + "gpu_bwutil_cpp": 0.00875073152005736, + "gpu_bwutil_python": 0.008690349839602901, + "gpu_gap_stddev_us": 2.7948346055546076 + } + }, + "osd_line_1080p_advanced[InOutDataType=uchar3][shape=3x1080x1920][elementType=LINE][layout=NCHW][inputKind=Tensor][numElem=64]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2486.19495051918, + "gpu_time_us_python": 2497.41549122682, + "gpu_noise_us_cpp": 14.262, + "gpu_noise_us_python": 13.825999999999999, + "gpu_bwutil_cpp": 0.028961009161580537, + "gpu_bwutil_python": 0.02883093353920572, + "gpu_gap_stddev_us": 3.3782027695524492 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2192.0029697264804, + "gpu_time_us_python": 2205.29650805284, + "gpu_noise_us_cpp": 15.094, + "gpu_noise_us_python": 14.347999999999999, + "gpu_bwutil_cpp": 0.025053466822278596, + "gpu_bwutil_python": 0.024902479048021918, + "gpu_gap_stddev_us": 2.405745584343463 + } + } + } + }, + "osd_circle_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "3x1080x1920" + ], + "elementType": [ + "CIRCLE" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "numElem": [ + 128 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "OSD RGB8 circle (advanced)." + }, + "baselines": { + "osd_circle_1080p_advanced[InOutDataType=uchar3][shape=3x1080x1920][elementType=CIRCLE][layout=NHWC][inputKind=Tensor][numElem=128]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1262.1778477790199, + "gpu_time_us_python": 1275.59490502646, + "gpu_noise_us_cpp": 21.607999999999997, + "gpu_noise_us_python": 22.366000000000003, + "gpu_bwutil_cpp": 0.01901963818710742, + "gpu_bwutil_python": 0.018819536328665377, + "gpu_gap_stddev_us": 4.71111532006676 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1190.1268463356998, + "gpu_time_us_python": 1205.12282318894, + "gpu_noise_us_cpp": 18.46, + "gpu_noise_us_python": 18.188000000000002, + "gpu_bwutil_cpp": 0.01538418120459684, + "gpu_bwutil_python": 0.015192738688223278, + "gpu_gap_stddev_us": 1.3433917251801077 + } + }, + "osd_circle_1080p_advanced[InOutDataType=uchar3][shape=3x1080x1920][elementType=CIRCLE][layout=NCHW][inputKind=Tensor][numElem=128]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1385.29766315026, + "gpu_time_us_python": 1397.09157904018, + "gpu_noise_us_cpp": 22.371999999999996, + "gpu_noise_us_python": 21.202, + "gpu_bwutil_cpp": 0.051978261547012516, + "gpu_bwutil_python": 0.05153942455504246, + "gpu_gap_stddev_us": 1.9385631545638025 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1295.9035544733802, + "gpu_time_us_python": 1306.1446618837401, + "gpu_noise_us_cpp": 18.432, + "gpu_noise_us_python": 17.944, + "gpu_bwutil_cpp": 0.04237914954836258, + "gpu_bwutil_python": 0.04204687471931162, + "gpu_gap_stddev_us": 1.2036562908423551 + } + } + } + }, + "osd_fakeplanar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3", + "uchar4" + ], + "string_axes": { + "shape": [ + "3x1080x1920" + ], + "elementType": [ + "POINT" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "numElem": [ + 100 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "OSD RGB/RGBA point (advanced) (fake planar NCHW_FAKE tensor-only)." + }, + "baselines": { + "osd_fakeplanar_nchw_advanced[InOutDataType=uchar3][shape=3x1080x1920][elementType=POINT][layout=NCHW_FAKE][inputKind=Tensor][numElem=100]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3339.0073159535, + "gpu_time_us_python": 3357.0433788299, + "gpu_noise_us_cpp": 18.662000000000003, + "gpu_noise_us_python": 20.816, + "gpu_bwutil_cpp": 0.021564539957938857, + "gpu_bwutil_python": 0.021448690089527083, + "gpu_gap_stddev_us": 3.7095656845926968 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2952.48896318318, + "gpu_time_us_python": 2968.55311786422, + "gpu_noise_us_cpp": 16.03, + "gpu_noise_us_python": 20.064, + "gpu_bwutil_cpp": 0.018600719059244743, + "gpu_bwutil_python": 0.01850009538227482, + "gpu_gap_stddev_us": 5.025018204059369 + } + }, + "osd_fakeplanar_nchw_advanced[InOutDataType=uchar4][shape=3x1080x1920][elementType=POINT][layout=NCHW_FAKE][inputKind=Tensor][numElem=100]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3374.7487964200204, + "gpu_time_us_python": 3393.2315613771802, + "gpu_noise_us_cpp": 17.826, + "gpu_noise_us_python": 19.068, + "gpu_bwutil_cpp": 0.028447855086939478, + "gpu_bwutil_python": 0.028292903112442418, + "gpu_gap_stddev_us": 6.428402642922575 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2982.6204998151998, + "gpu_time_us_python": 2999.0551607949, + "gpu_noise_us_cpp": 16.137999999999998, + "gpu_noise_us_python": 17.918, + "gpu_bwutil_cpp": 0.024550067744032643, + "gpu_bwutil_python": 0.02441554668066898, + "gpu_gap_stddev_us": 5.0520342802990985 + } + } + } + } + } +} diff --git a/bench/config/operators/padandstack.json b/bench/config/operators/padandstack.json new file mode 100644 index 000000000..a9902b9bd --- /dev/null +++ b/bench/config/operators/padandstack.json @@ -0,0 +1,717 @@ +{ + "benchmark": "padandstack", + "configs": { + "padandstack_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "int64_axes": { + "pad": [ + 0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PadAndStack RGB8 (basic)." + }, + "baselines": { + "padandstack_uchar3_basic[InOutDataType=uchar3][shape=128x1080x1920][border=REFLECT101][layout=NHWC][inputKind=VarShape][pad=0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2775.22314211966, + "gpu_time_us_python": 2791.34211275308, + "gpu_noise_us_cpp": 1.8980000000000001, + "gpu_noise_us_python": 7.164, + "gpu_bwutil_cpp": 0.36898064332191627, + "gpu_bwutil_python": 0.3668492941372685, + "gpu_gap_stddev_us": 2.6972895419534533 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2572.17048925156, + "gpu_time_us_python": 2586.13287804852, + "gpu_noise_us_cpp": 1.284, + "gpu_noise_us_python": 2.972, + "gpu_bwutil_cpp": 0.3036721582347313, + "gpu_bwutil_python": 0.3020329339330187, + "gpu_gap_stddev_us": 1.3671319446258268 + } + } + } + }, + "padandstack_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "int64_axes": { + "pad": [ + 0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PadAndStack RGBf32 (basic)." + }, + "baselines": { + "padandstack_float3_basic[InOutDataType=float3][shape=64x1080x1920][border=REFLECT101][layout=NHWC][inputKind=VarShape][pad=0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2368.5080586739, + "gpu_time_us_python": 2382.21832040182, + "gpu_noise_us_cpp": 2.418, + "gpu_noise_us_python": 6.612, + "gpu_bwutil_cpp": 0.8646796406410727, + "gpu_bwutil_python": 0.8597037114663625, + "gpu_gap_stddev_us": 1.3203872704323074 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1894.27753419908, + "gpu_time_us_python": 1906.5118217313398, + "gpu_noise_us_cpp": 2.818, + "gpu_noise_us_python": 3.976, + "gpu_bwutil_cpp": 0.8246109982569643, + "gpu_bwutil_python": 0.8193211829908336, + "gpu_gap_stddev_us": 1.74605076261967 + } + } + } + }, + "padandstack_planar_nchw_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "VarShape" + ] + }, + "int64_axes": { + "pad": [ + 0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PadAndStack RGB8 planar NCHW (basic)." + }, + "baselines": { + "padandstack_planar_nchw_uchar3_basic[InOutDataType=uchar3][shape=128x1080x1920][border=REFLECT101][layout=NCHW][inputKind=VarShape][pad=0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 7204.5831571306, + "gpu_time_us_python": 7223.032659803081, + "gpu_noise_us_cpp": 6.882000000000001, + "gpu_noise_us_python": 13.180000000000001, + "gpu_bwutil_cpp": 0.14213238307247994, + "gpu_bwutil_python": 0.14176921116543556, + "gpu_gap_stddev_us": 5.499898683382808 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6801.3629777671795, + "gpu_time_us_python": 6817.35120450446, + "gpu_noise_us_cpp": 2.472, + "gpu_noise_us_python": 5.572000000000001, + "gpu_bwutil_cpp": 0.1148481281955227, + "gpu_bwutil_python": 0.11457925920677159, + "gpu_gap_stddev_us": 3.019126780973842 + } + } + } + }, + "padandstack_planar_nchw_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "VarShape" + ] + }, + "int64_axes": { + "pad": [ + 0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PadAndStack RGBf32 planar NCHW (basic)." + }, + "baselines": { + "padandstack_planar_nchw_float3_basic[InOutDataType=float3][shape=64x1080x1920][border=REFLECT101][layout=NCHW][inputKind=VarShape][pad=0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3828.41328418612, + "gpu_time_us_python": 3845.84493141348, + "gpu_noise_us_cpp": 2.2700000000000005, + "gpu_noise_us_python": 4.32, + "gpu_bwutil_cpp": 0.5349492713684018, + "gpu_bwutil_python": 0.5325240074310766, + "gpu_gap_stddev_us": 2.437678374125727 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3548.8563138719, + "gpu_time_us_python": 3563.65355619236, + "gpu_noise_us_cpp": 2.474, + "gpu_noise_us_python": 4.0280000000000005, + "gpu_bwutil_cpp": 0.44021407016569414, + "gpu_bwutil_python": 0.43838054677243, + "gpu_gap_stddev_us": 3.5207542129934932 + } + } + } + }, + "padandstack_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "int64_axes": { + "pad": [ + 0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PadAndStack RGBA8 (advanced)." + }, + "baselines": { + "padandstack_uchar4_advanced[InOutDataType=uchar4][shape=128x1080x1920][border=REFLECT101][layout=NHWC][inputKind=VarShape][pad=0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2609.90470058928, + "gpu_time_us_python": 2629.6962796177004, + "gpu_noise_us_cpp": 4.168, + "gpu_noise_us_python": 7.264, + "gpu_bwutil_cpp": 0.5231384274292277, + "gpu_bwutil_python": 0.5191988170970081, + "gpu_gap_stddev_us": 4.987541730781766 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2504.78269196658, + "gpu_time_us_python": 2519.0684954243, + "gpu_noise_us_cpp": 3.1220000000000003, + "gpu_noise_us_python": 3.9579999999999997, + "gpu_bwutil_cpp": 0.4157978942450466, + "gpu_bwutil_python": 0.4134422916633696, + "gpu_gap_stddev_us": 2.46598185471687 + } + } + } + }, + "padandstack_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "int64_axes": { + "pad": [ + 0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PadAndStack RGBAf32 (advanced)." + }, + "baselines": { + "padandstack_float4_advanced[InOutDataType=float4][shape=64x1080x1920][border=REFLECT101][layout=NHWC][inputKind=VarShape][pad=0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3058.00858590656, + "gpu_time_us_python": 3074.0979735443602, + "gpu_noise_us_cpp": 2.576, + "gpu_noise_us_python": 10.7, + "gpu_bwutil_cpp": 0.8929560770001407, + "gpu_bwutil_python": 0.8882831970597914, + "gpu_gap_stddev_us": 3.3008022680202136 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2328.09617960892, + "gpu_time_us_python": 2340.0825999607196, + "gpu_noise_us_cpp": 2.738, + "gpu_noise_us_python": 3.5879999999999996, + "gpu_bwutil_cpp": 0.894598896693819, + "gpu_bwutil_python": 0.8900170657984205, + "gpu_gap_stddev_us": 1.4085461466549163 + } + } + } + }, + "padandstack_planar_nchw_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "VarShape" + ] + }, + "int64_axes": { + "pad": [ + 0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PadAndStack RGBA8 planar NCHW (advanced)." + }, + "baselines": { + "padandstack_planar_nchw_uchar4_advanced[InOutDataType=uchar4][shape=128x1080x1920][border=REFLECT101][layout=NCHW][inputKind=VarShape][pad=0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 8700.066329692921, + "gpu_time_us_python": 8710.63131792788, + "gpu_noise_us_cpp": 7.356, + "gpu_noise_us_python": 12.398, + "gpu_bwutil_cpp": 0.15693437607971725, + "gpu_bwutil_python": 0.15674366004260726, + "gpu_gap_stddev_us": 18.42870020116978 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 8038.586115740921, + "gpu_time_us_python": 8056.0435089372595, + "gpu_noise_us_cpp": 2.162, + "gpu_noise_us_python": 4.204, + "gpu_bwutil_cpp": 0.1295655905613024, + "gpu_bwutil_python": 0.1292858634064482, + "gpu_gap_stddev_us": 3.3526171223376116 + } + } + } + }, + "padandstack_planar_nchw_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "VarShape" + ] + }, + "int64_axes": { + "pad": [ + 0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PadAndStack RGBAf32 planar NCHW (advanced)." + }, + "baselines": { + "padandstack_planar_nchw_float4_advanced[InOutDataType=float4][shape=64x1080x1920][border=REFLECT101][layout=NCHW][inputKind=VarShape][pad=0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4719.19324099804, + "gpu_time_us_python": 4740.60992294884, + "gpu_noise_us_cpp": 1.6480000000000001, + "gpu_noise_us_python": 4.241999999999999, + "gpu_bwutil_cpp": 0.5786309725421294, + "gpu_bwutil_python": 0.5760173471100632, + "gpu_gap_stddev_us": 8.0397649717234 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4241.1354138487, + "gpu_time_us_python": 4257.46585067658, + "gpu_noise_us_cpp": 1.4060000000000001, + "gpu_noise_us_python": 3.9300000000000006, + "gpu_bwutil_cpp": 0.4911351977409707, + "gpu_bwutil_python": 0.4892535913249441, + "gpu_gap_stddev_us": 4.49245779244747 + } + } + } + }, + "padandstack_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "int64_axes": { + "pad": [ + 0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PadAndStack single-channel (advanced)." + }, + "baselines": { + "padandstack_scalar_advanced[InOutDataType=float32][shape=64x1080x1920][border=REFLECT101][layout=NHWC][inputKind=VarShape][pad=0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1312.01442545488, + "gpu_time_us_python": 1327.62777860276, + "gpu_noise_us_cpp": 1.642, + "gpu_noise_us_python": 4.247999999999999, + "gpu_bwutil_cpp": 0.5203213619738879, + "gpu_bwutil_python": 0.5142022171164313, + "gpu_gap_stddev_us": 1.577336773761722 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1258.7655390776, + "gpu_time_us_python": 1272.1843226824599, + "gpu_noise_us_cpp": 1.262, + "gpu_noise_us_python": 3.5, + "gpu_bwutil_cpp": 0.41369333423200116, + "gpu_bwutil_python": 0.4093297996537979, + "gpu_gap_stddev_us": 2.0738810822652693 + } + }, + "padandstack_scalar_advanced[InOutDataType=uint8][shape=64x1080x1920][border=REFLECT101][layout=NHWC][inputKind=VarShape][pad=0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1255.76211947168, + "gpu_time_us_python": 1270.0161406004602, + "gpu_noise_us_cpp": 2.47, + "gpu_noise_us_python": 5.132, + "gpu_bwutil_cpp": 0.1359072742072626, + "gpu_bwutil_python": 0.1343819199706381, + "gpu_gap_stddev_us": 0.9357540970545842 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1223.4149404587, + "gpu_time_us_python": 1236.66815801798, + "gpu_noise_us_cpp": 0.9939999999999998, + "gpu_noise_us_python": 4.02, + "gpu_bwutil_cpp": 0.10641264606481078, + "gpu_bwutil_python": 0.10527143124363965, + "gpu_gap_stddev_us": 1.3089670252851722 + } + } + } + }, + "padandstack_u8_constant_pad32_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "96x1080x1920" + ], + "border": [ + "CONSTANT" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "int64_axes": { + "pad": [ + 32 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PadAndStack single-channel constant pad32 (advanced)." + }, + "baselines": { + "padandstack_u8_constant_pad32_1080p_advanced[InOutDataType=uint8][shape=96x1080x1920][border=CONSTANT][layout=NHWC][inputKind=VarShape][pad=32]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2284.6961451609595, + "gpu_time_us_python": 2293.1379614719, + "gpu_noise_us_cpp": 1.388, + "gpu_noise_us_python": 4.022, + "gpu_bwutil_cpp": 0.1173505526266555, + "gpu_bwutil_python": 0.11691779704810154, + "gpu_gap_stddev_us": 2.8028093239220055 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1883.56059696066, + "gpu_time_us_python": 1896.9480291108, + "gpu_noise_us_cpp": 1.402, + "gpu_noise_us_python": 4.38, + "gpu_bwutil_cpp": 0.10860525922630895, + "gpu_bwutil_python": 0.10783528488352663, + "gpu_gap_stddev_us": 4.494358961156222 + } + } + } + }, + "padandstack_f32_constant_pad32_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "48x1080x1920" + ], + "border": [ + "CONSTANT" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "int64_axes": { + "pad": [ + 32 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PadAndStack single-channel constant pad32 (advanced)." + }, + "baselines": { + "padandstack_f32_constant_pad32_1080p_advanced[InOutDataType=float32][shape=48x1080x1920][border=CONSTANT][layout=NHWC][inputKind=VarShape][pad=32]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1189.75782534582, + "gpu_time_us_python": 1198.4073314474201, + "gpu_noise_us_cpp": 1.202, + "gpu_noise_us_python": 3.8659999999999997, + "gpu_bwutil_cpp": 0.4506961764984295, + "gpu_bwutil_python": 0.4474422112201107, + "gpu_gap_stddev_us": 2.842469058420889 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1038.19143048536, + "gpu_time_us_python": 1050.6085052323, + "gpu_noise_us_cpp": 1.1039999999999999, + "gpu_noise_us_python": 4.1, + "gpu_bwutil_cpp": 0.39396315541255705, + "gpu_bwutil_python": 0.38929869079228097, + "gpu_gap_stddev_us": 1.303881828767515 + } + } + } + }, + "padandstack_u8_reflect101_pad32_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "int64_axes": { + "pad": [ + 32 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PadAndStack single-channel reflect101 pad32 (advanced)." + }, + "baselines": { + "padandstack_u8_reflect101_pad32_1080p_advanced[InOutDataType=uint8][shape=128x1080x1920][border=REFLECT101][layout=NHWC][inputKind=VarShape][pad=32]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2716.9808933755603, + "gpu_time_us_python": 2740.68256357434, + "gpu_noise_us_cpp": 3.0620000000000003, + "gpu_noise_us_python": 8.644, + "gpu_bwutil_cpp": 0.13157041089733085, + "gpu_bwutil_python": 0.13043249843439825, + "gpu_gap_stddev_us": 2.296525386839222 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2653.2230950010403, + "gpu_time_us_python": 2670.5980749243, + "gpu_noise_us_cpp": 1.3920000000000001, + "gpu_noise_us_python": 3.53, + "gpu_bwutil_cpp": 0.10277548349376633, + "gpu_bwutil_python": 0.10210741435416915, + "gpu_gap_stddev_us": 2.2710943072982115 + } + } + } + }, + "padandstack_f32_reflect101_pad32_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "80x1080x1920" + ], + "border": [ + "REFLECT101" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "int64_axes": { + "pad": [ + 32 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PadAndStack single-channel reflect101 pad32 (advanced)." + }, + "baselines": { + "padandstack_f32_reflect101_pad32_1080p_advanced[InOutDataType=float32][shape=80x1080x1920][border=REFLECT101][layout=NHWC][inputKind=VarShape][pad=32]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1767.7124392818, + "gpu_time_us_python": 1794.76186684196, + "gpu_noise_us_cpp": 1.5720000000000003, + "gpu_noise_us_python": 5.868, + "gpu_bwutil_cpp": 0.5055607543467302, + "gpu_bwutil_python": 0.4979396024492444, + "gpu_gap_stddev_us": 4.074117647929122 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1708.9261892234601, + "gpu_time_us_python": 1724.97300306448, + "gpu_noise_us_cpp": 1.048, + "gpu_noise_us_python": 4.062, + "gpu_bwutil_cpp": 0.3989088224257582, + "gpu_bwutil_python": 0.3951982090295031, + "gpu_gap_stddev_us": 1.4778152805230085 + } + } + } + } + } +} diff --git a/bench/config/operators/pairwisematcher.json b/bench/config/operators/pairwisematcher.json new file mode 100644 index 000000000..6ab3d94b4 --- /dev/null +++ b/bench/config/operators/pairwisematcher.json @@ -0,0 +1,439 @@ +{ + "benchmark": "pairwisematcher", + "configs": { + "pairwisematcher_uint8_basic": { + "tier": "basic", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "2x5000x32" + ], + "crossCheck": [ + "T" + ], + "readNumSets": [ + "F" + ], + "writeDistances": [ + "T" + ], + "normType": [ + "HAMMING" + ], + "algoChoice": [ + "BRUTE_FORCE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "matchesPerPoint": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PairwiseMatcher single-channel (basic)." + }, + "baselines": { + "pairwisematcher_uint8_basic[InOutDataType=uint8][shape=2x5000x32][crossCheck=T][readNumSets=F][writeDistances=T][normType=HAMMING][algoChoice=BRUTE_FORCE][inputKind=Tensor][matchesPerPoint=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1784.4982090897, + "gpu_time_us_python": 1841.80141122846, + "gpu_noise_us_cpp": 10.379999999999999, + "gpu_noise_us_python": 14.824000000000002, + "gpu_bwutil_cpp": 0.00038915784745079997, + "gpu_bwutil_python": 0.00037705094254104004, + "gpu_gap_stddev_us": 6.217237531602112 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1720.68891998836, + "gpu_time_us_python": 1773.74485453868, + "gpu_noise_us_cpp": 13.801999999999998, + "gpu_noise_us_python": 16.166000000000004, + "gpu_bwutil_cpp": 0.00030782177258108, + "gpu_bwutil_python": 0.00029862058979752, + "gpu_gap_stddev_us": 7.708940828839856 + } + } + } + }, + "pairwisematcher_uint32_basic": { + "tier": "basic", + "dtypes": [ + "uint32" + ], + "string_axes": { + "shape": [ + "1x2000x32" + ], + "crossCheck": [ + "T" + ], + "readNumSets": [ + "F" + ], + "writeDistances": [ + "T" + ], + "normType": [ + "HAMMING" + ], + "algoChoice": [ + "BRUTE_FORCE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "matchesPerPoint": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PairwiseMatcher single-channel (basic)." + }, + "baselines": { + "pairwisematcher_uint32_basic[InOutDataType=uint32][shape=1x2000x32][crossCheck=T][readNumSets=F][writeDistances=T][normType=HAMMING][algoChoice=BRUTE_FORCE][inputKind=Tensor][matchesPerPoint=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2383.3671757559596, + "gpu_time_us_python": 2439.0132456054002, + "gpu_noise_us_cpp": 3.5479999999999996, + "gpu_noise_us_python": 12.132000000000001, + "gpu_bwutil_cpp": 0.00021367407356650002, + "gpu_bwutil_python": 0.00020879922954566, + "gpu_gap_stddev_us": 6.253337796421098 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2320.76809571988, + "gpu_time_us_python": 2368.65685539258, + "gpu_noise_us_cpp": 84.626, + "gpu_noise_us_python": 87.174, + "gpu_bwutil_cpp": 0.00016736709567284, + "gpu_bwutil_python": 0.0001639856548071, + "gpu_gap_stddev_us": 8.668846127017794 + } + } + } + }, + "pairwisematcher_u8_hamming_nocross_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "2x8192x32" + ], + "crossCheck": [ + "F" + ], + "readNumSets": [ + "F" + ], + "writeDistances": [ + "T" + ], + "normType": [ + "HAMMING" + ], + "algoChoice": [ + "BRUTE_FORCE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "matchesPerPoint": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PairwiseMatcher single-channel hamming nocross (advanced)." + }, + "baselines": { + "pairwisematcher_u8_hamming_nocross_advanced[InOutDataType=uint8][shape=2x8192x32][crossCheck=F][readNumSets=F][writeDistances=T][normType=HAMMING][algoChoice=BRUTE_FORCE][inputKind=Tensor][matchesPerPoint=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2377.01659604004, + "gpu_time_us_python": 2432.9836705934204, + "gpu_noise_us_cpp": 9.643999999999998, + "gpu_noise_us_python": 11.442, + "gpu_bwutil_cpp": 0.00033683635530401996, + "gpu_bwutil_python": 0.0003290879707805, + "gpu_gap_stddev_us": 6.42539623487294 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2250.33919719576, + "gpu_time_us_python": 2303.7117027707, + "gpu_noise_us_cpp": 10.148000000000001, + "gpu_noise_us_python": 13.273999999999997, + "gpu_bwutil_cpp": 0.00027137063390088, + "gpu_bwutil_python": 0.00026508812132504, + "gpu_gap_stddev_us": 9.496703871708467 + } + } + } + }, + "pairwisematcher_u8_hamming_readnumsets_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "2x5000x32" + ], + "crossCheck": [ + "T" + ], + "readNumSets": [ + "T" + ], + "writeDistances": [ + "T" + ], + "normType": [ + "HAMMING" + ], + "algoChoice": [ + "BRUTE_FORCE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "matchesPerPoint": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PairwiseMatcher single-channel hamming readnumsets (advanced)." + }, + "baselines": { + "pairwisematcher_u8_hamming_readnumsets_advanced[InOutDataType=uint8][shape=2x5000x32][crossCheck=T][readNumSets=T][writeDistances=T][normType=HAMMING][algoChoice=BRUTE_FORCE][inputKind=Tensor][matchesPerPoint=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1789.3522126333603, + "gpu_time_us_python": 1847.3521050169802, + "gpu_noise_us_cpp": 11.209999999999999, + "gpu_noise_us_python": 14.273999999999997, + "gpu_bwutil_cpp": 0.00038810253237255997, + "gpu_bwutil_python": 0.00037592057029214, + "gpu_gap_stddev_us": 9.838974633795075 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1721.6332701787, + "gpu_time_us_python": 1778.5413155198999, + "gpu_noise_us_cpp": 13.872, + "gpu_noise_us_python": 16.458, + "gpu_bwutil_cpp": 0.00030765284353579997, + "gpu_bwutil_python": 0.00029781483627998003, + "gpu_gap_stddev_us": 7.496889916141912 + } + } + } + }, + "pairwisematcher_u8_hamming_nodist_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "2x5000x32" + ], + "crossCheck": [ + "T" + ], + "readNumSets": [ + "F" + ], + "writeDistances": [ + "F" + ], + "normType": [ + "HAMMING" + ], + "algoChoice": [ + "BRUTE_FORCE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "matchesPerPoint": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PairwiseMatcher single-channel hamming nodist (advanced)." + }, + "baselines": { + "pairwisematcher_u8_hamming_nodist_advanced[InOutDataType=uint8][shape=2x5000x32][crossCheck=T][readNumSets=F][writeDistances=F][normType=HAMMING][algoChoice=BRUTE_FORCE][inputKind=Tensor][matchesPerPoint=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1784.8179442046198, + "gpu_time_us_python": 1829.90680579332, + "gpu_noise_us_cpp": 10.844, + "gpu_noise_us_python": 13.742, + "gpu_bwutil_cpp": 0.00038908842444948, + "gpu_bwutil_python": 0.00037950267329023995, + "gpu_gap_stddev_us": 4.852286896003957 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1720.78163313532, + "gpu_time_us_python": 1761.05643902058, + "gpu_noise_us_cpp": 13.651999999999997, + "gpu_noise_us_python": 15.272, + "gpu_bwutil_cpp": 0.0003078051712801, + "gpu_bwutil_python": 0.00030076918056252, + "gpu_gap_stddev_us": 5.533303972877654 + } + } + } + }, + "pairwisematcher_u8_hamming_mpp2_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "2x8192x32" + ], + "crossCheck": [ + "F" + ], + "readNumSets": [ + "F" + ], + "writeDistances": [ + "T" + ], + "normType": [ + "HAMMING" + ], + "algoChoice": [ + "BRUTE_FORCE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "matchesPerPoint": [ + 2 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PairwiseMatcher single-channel hamming mpp2 (advanced)." + }, + "baselines": { + "pairwisematcher_u8_hamming_mpp2_advanced[InOutDataType=uint8][shape=2x8192x32][crossCheck=F][readNumSets=F][writeDistances=T][normType=HAMMING][algoChoice=BRUTE_FORCE][inputKind=Tensor][matchesPerPoint=2]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2581.6644702989397, + "gpu_time_us_python": 2638.53966753728, + "gpu_noise_us_cpp": 7.353999999999999, + "gpu_noise_us_python": 11.108, + "gpu_bwutil_cpp": 0.00035910404505308, + "gpu_bwutil_python": 0.00035136371894252, + "gpu_gap_stddev_us": 8.030777411225012 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2394.7788837977396, + "gpu_time_us_python": 2447.88667048672, + "gpu_noise_us_cpp": 8.962, + "gpu_noise_us_python": 14.468, + "gpu_bwutil_cpp": 0.00029526636534586, + "gpu_bwutil_python": 0.0002888640084875, + "gpu_gap_stddev_us": 8.521084322191587 + } + } + } + }, + "pairwisematcher_f32_l2_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "2x1024x64" + ], + "crossCheck": [ + "T" + ], + "readNumSets": [ + "F" + ], + "writeDistances": [ + "T" + ], + "normType": [ + "L2" + ], + "algoChoice": [ + "BRUTE_FORCE" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "matchesPerPoint": [ + 1 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PairwiseMatcher single-channel l2 (advanced)." + }, + "baselines": { + "pairwisematcher_f32_l2_advanced[InOutDataType=float32][shape=2x1024x64][crossCheck=T][readNumSets=F][writeDistances=T][normType=L2][algoChoice=BRUTE_FORCE][inputKind=Tensor][matchesPerPoint=1]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2422.6689754269, + "gpu_time_us_python": 2479.8366863471997, + "gpu_noise_us_cpp": 1.954, + "gpu_noise_us_python": 14.342000000000002, + "gpu_bwutil_cpp": 0.00042398554360234005, + "gpu_bwutil_python": 0.0004142107187488, + "gpu_gap_stddev_us": 8.140031752798818 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2286.6472729769202, + "gpu_time_us_python": 2341.11356327958, + "gpu_noise_us_cpp": 2.044, + "gpu_noise_us_python": 7.466000000000001, + "gpu_bwutil_cpp": 0.00034261145543738, + "gpu_bwutil_python": 0.0003346448327841, + "gpu_gap_stddev_us": 10.010693422684348 + } + } + } + } + } +} diff --git a/bench/config/operators/pillowresize.json b/bench/config/operators/pillowresize.json new file mode 100644 index 000000000..bc88c02f8 --- /dev/null +++ b/bench/config/operators/pillowresize.json @@ -0,0 +1,13162 @@ +{ + "benchmark": "pillowresize", + "configs": { + "pillowresize_contract_cubic_1080p_varshape_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "96x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize contract_cubic_1080p_varshape RGB8 (basic)." + }, + "baselines": { + "pillowresize_contract_cubic_1080p_varshape_uchar3_basic[InOutDataType=uchar3][layout=NHWC][shape=96x1080x1920][resizeType=CONTRACT][interpolation=CUBIC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3417.6497497014198, + "gpu_time_us_python": 3449.2204472535605, + "gpu_noise_us_cpp": 7.366000000000001, + "gpu_noise_us_python": 7.203999999999999, + "gpu_bwutil_cpp": 0.14045057239241882, + "gpu_bwutil_python": 0.13916675910337592, + "gpu_gap_stddev_us": 5.110851306788775 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2867.9234609879604, + "gpu_time_us_python": 2902.42682110506, + "gpu_noise_us_cpp": 11.08, + "gpu_noise_us_python": 9.074000000000002, + "gpu_bwutil_cpp": 0.12772088790735514, + "gpu_bwutil_python": 0.12620676441271622, + "gpu_gap_stddev_us": 5.366887495426814 + } + } + } + }, + "pillowresize_contract_cubic_1080p_varshape_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize contract_cubic_1080p_varshape RGBf32 (advanced)." + }, + "baselines": { + "pillowresize_contract_cubic_1080p_varshape_float3_advanced[InOutDataType=float3][layout=NHWC][shape=32x1080x1920][resizeType=CONTRACT][interpolation=CUBIC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1712.8888856196602, + "gpu_time_us_python": 1736.41399269244, + "gpu_noise_us_cpp": 3.6839999999999997, + "gpu_noise_us_python": 5.564, + "gpu_bwutil_cpp": 0.37363994351076124, + "gpu_bwutil_python": 0.36858324371399787, + "gpu_gap_stddev_us": 4.680222672137642 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1372.74152258546, + "gpu_time_us_python": 1397.14606771942, + "gpu_noise_us_cpp": 3.664, + "gpu_noise_us_python": 5.998, + "gpu_bwutil_cpp": 0.35571455046277367, + "gpu_bwutil_python": 0.3495200403086077, + "gpu_gap_stddev_us": 5.139072232442477 + } + } + } + }, + "pillowresize_contract_cubic_1080p_varshape_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "96x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize contract_cubic_1080p_varshape RGBA8 mirror (advanced)." + }, + "baselines": { + "pillowresize_contract_cubic_1080p_varshape_uchar4_advanced[InOutDataType=uchar4][layout=NHWC][shape=96x1080x1920][resizeType=CONTRACT][interpolation=CUBIC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3604.5940855570793, + "gpu_time_us_python": 3641.8545107910004, + "gpu_noise_us_cpp": 4.074, + "gpu_noise_us_python": 10.145999999999999, + "gpu_bwutil_cpp": 0.17755241536565666, + "gpu_bwutil_python": 0.17573883047841918, + "gpu_gap_stddev_us": 9.678444170541818 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3427.78269475946, + "gpu_time_us_python": 3463.9491404640003, + "gpu_noise_us_cpp": 6.626, + "gpu_noise_us_python": 9.862, + "gpu_bwutil_cpp": 0.14248852229827852, + "gpu_bwutil_python": 0.14100938932884566, + "gpu_gap_stddev_us": 6.973602652485092 + } + } + } + }, + "pillowresize_contract_cubic_1080p_varshape_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize contract_cubic_1080p_varshape RGBAf32 mirror (advanced)." + }, + "baselines": { + "pillowresize_contract_cubic_1080p_varshape_float4_advanced[InOutDataType=float4][layout=NHWC][shape=32x1080x1920][resizeType=CONTRACT][interpolation=CUBIC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1974.95251771966, + "gpu_time_us_python": 1998.51746279578, + "gpu_noise_us_cpp": 4.284, + "gpu_noise_us_python": 7.5680000000000005, + "gpu_bwutil_cpp": 0.43207986259051195, + "gpu_bwutil_python": 0.42699199937589966, + "gpu_gap_stddev_us": 5.928357520774672 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1569.89125845426, + "gpu_time_us_python": 1593.83343713782, + "gpu_noise_us_cpp": 5.14, + "gpu_noise_us_python": 5.506, + "gpu_bwutil_cpp": 0.41464476723258537, + "gpu_bwutil_python": 0.408439842595821, + "gpu_gap_stddev_us": 5.361769125247814 + } + } + } + }, + "pillowresize_contract_cubic_1080p_varshape_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "64x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize contract_cubic_1080p_varshape single-channel (advanced)." + }, + "baselines": { + "pillowresize_contract_cubic_1080p_varshape_scalar_advanced[InOutDataType=float32][layout=NHWC][shape=64x1080x1920][resizeType=CONTRACT][interpolation=CUBIC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1636.1914247573197, + "gpu_time_us_python": 1666.5055648639798, + "gpu_noise_us_cpp": 4.866, + "gpu_noise_us_python": 6.208, + "gpu_bwutil_cpp": 0.2607723277240908, + "gpu_bwutil_python": 0.25603471638597075, + "gpu_gap_stddev_us": 5.3489664197230775 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1451.9302316393, + "gpu_time_us_python": 1480.73174499126, + "gpu_noise_us_cpp": 2.966, + "gpu_noise_us_python": 6.964, + "gpu_bwutil_cpp": 0.2242437054831723, + "gpu_bwutil_python": 0.21989342851108523, + "gpu_gap_stddev_us": 8.75308656109932 + } + }, + "pillowresize_contract_cubic_1080p_varshape_scalar_advanced[InOutDataType=uint8][layout=NHWC][shape=64x1080x1920][resizeType=CONTRACT][interpolation=CUBIC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1495.1350154302797, + "gpu_time_us_python": 1524.9694825129598, + "gpu_noise_us_cpp": 4.696, + "gpu_noise_us_python": 8.388, + "gpu_bwutil_cpp": 0.071344997257315, + "gpu_bwutil_python": 0.06995278639380026, + "gpu_gap_stddev_us": 6.055486741235336 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1389.2440377092, + "gpu_time_us_python": 1417.9805657990203, + "gpu_noise_us_cpp": 4.2299999999999995, + "gpu_noise_us_python": 8.372, + "gpu_bwutil_cpp": 0.05859789361949848, + "gpu_bwutil_python": 0.05741186115631819, + "gpu_gap_stddev_us": 9.519314272026312 + } + } + } + }, + "pillowresize_contract_linear_720p_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "192x720x1280" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize contract_linear_720p RGB8 (advanced)." + }, + "baselines": { + "pillowresize_contract_linear_720p_uchar3_advanced[InOutDataType=uchar3][layout=NHWC][shape=192x720x1280][resizeType=CONTRACT][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1874.1415968581198, + "gpu_time_us_python": 1891.03048527174, + "gpu_noise_us_cpp": 1.154, + "gpu_noise_us_python": 7.081999999999999, + "gpu_bwutil_cpp": 0.22766047356278327, + "gpu_bwutil_python": 0.22562742608966863, + "gpu_gap_stddev_us": 2.1474730963527406 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1565.78724249104, + "gpu_time_us_python": 1581.36250259868, + "gpu_noise_us_cpp": 1.126, + "gpu_noise_us_python": 4.318, + "gpu_bwutil_cpp": 0.2079517552359571, + "gpu_bwutil_python": 0.20590591896507301, + "gpu_gap_stddev_us": 1.7192288207534414 + } + } + } + }, + "pillowresize_contract_linear_720p_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "64x720x1280" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize contract_linear_720p RGBf32 (advanced)." + }, + "baselines": { + "pillowresize_contract_linear_720p_float3_advanced[InOutDataType=float3][layout=NHWC][shape=64x720x1280][resizeType=CONTRACT][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 847.6699404087, + "gpu_time_us_python": 862.7251522451401, + "gpu_noise_us_cpp": 2.838, + "gpu_noise_us_python": 2.402, + "gpu_bwutil_cpp": 0.6711212457886977, + "gpu_bwutil_python": 0.659413537891526, + "gpu_gap_stddev_us": 1.7267912796009568 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 696.2504038600599, + "gpu_time_us_python": 711.95023505272, + "gpu_noise_us_cpp": 0.696, + "gpu_noise_us_python": 2.952, + "gpu_bwutil_cpp": 0.6235707624942632, + "gpu_bwutil_python": 0.6098507136078852, + "gpu_gap_stddev_us": 2.1619777362179344 + } + } + } + }, + "pillowresize_contract_linear_720p_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "192x720x1280" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize contract_linear_720p RGBA8 mirror (advanced)." + }, + "baselines": { + "pillowresize_contract_linear_720p_uchar4_advanced[InOutDataType=uchar4][layout=NHWC][shape=192x720x1280][resizeType=CONTRACT][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2025.7130672854, + "gpu_time_us_python": 2042.17906192062, + "gpu_noise_us_cpp": 1.9459999999999997, + "gpu_noise_us_python": 3.4260000000000006, + "gpu_bwutil_cpp": 0.2808343100116422, + "gpu_bwutil_python": 0.2785703329779483, + "gpu_gap_stddev_us": 1.4716108090909417 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1930.1028963899, + "gpu_time_us_python": 1947.9354182113802, + "gpu_noise_us_cpp": 0.9540000000000001, + "gpu_noise_us_python": 6.566, + "gpu_bwutil_cpp": 0.22493267292237387, + "gpu_bwutil_python": 0.22287792958018987, + "gpu_gap_stddev_us": 3.6113316748956987 + } + } + } + }, + "pillowresize_contract_linear_720p_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "64x720x1280" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize contract_linear_720p RGBAf32 mirror (advanced)." + }, + "baselines": { + "pillowresize_contract_linear_720p_float4_advanced[InOutDataType=float4][layout=NHWC][shape=64x720x1280][resizeType=CONTRACT][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1109.9009002204, + "gpu_time_us_python": 1125.5338842534, + "gpu_noise_us_cpp": 1.4200000000000002, + "gpu_noise_us_python": 2.5420000000000003, + "gpu_bwutil_cpp": 0.6834115305213371, + "gpu_bwutil_python": 0.6739226480278467, + "gpu_gap_stddev_us": 2.528627508946405 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 769.61929750776, + "gpu_time_us_python": 786.5011661424201, + "gpu_noise_us_cpp": 1.452, + "gpu_noise_us_python": 4.64, + "gpu_bwutil_cpp": 0.751837777718501, + "gpu_bwutil_python": 0.7357812742121619, + "gpu_gap_stddev_us": 4.115480721898862 + } + } + } + }, + "pillowresize_contract_linear_720p_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "128x720x1280" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize contract_linear_720p single-channel (advanced)." + }, + "baselines": { + "pillowresize_contract_linear_720p_scalar_advanced[InOutDataType=float32][layout=NHWC][shape=128x720x1280][resizeType=CONTRACT][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1004.0053301592401, + "gpu_time_us_python": 1020.25180222834, + "gpu_noise_us_cpp": 1.7100000000000002, + "gpu_noise_us_python": 2.5380000000000003, + "gpu_bwutil_cpp": 0.37774672509090346, + "gpu_bwutil_python": 0.37173232419084423, + "gpu_gap_stddev_us": 2.1771901403406853 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 747.8301834485201, + "gpu_time_us_python": 762.2974361837, + "gpu_noise_us_cpp": 0.9720000000000001, + "gpu_noise_us_python": 1.886, + "gpu_bwutil_cpp": 0.38702841410000727, + "gpu_bwutil_python": 0.3796926288696273, + "gpu_gap_stddev_us": 1.3750920478405624 + } + }, + "pillowresize_contract_linear_720p_scalar_advanced[InOutDataType=uint8][layout=NHWC][shape=128x720x1280][resizeType=CONTRACT][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 782.93668552234, + "gpu_time_us_python": 799.65594510866, + "gpu_noise_us_cpp": 1.6800000000000002, + "gpu_noise_us_python": 4.854000000000001, + "gpu_bwutil_cpp": 0.12110167728897388, + "gpu_bwutil_python": 0.11857046445680955, + "gpu_gap_stddev_us": 2.0699495980120513 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 749.1904908939399, + "gpu_time_us_python": 763.83566545284, + "gpu_noise_us_cpp": 1.626, + "gpu_noise_us_python": 2.1719999999999997, + "gpu_bwutil_cpp": 0.09658023923670886, + "gpu_bwutil_python": 0.09472973974546262, + "gpu_gap_stddev_us": 1.7365627379837325 + } + } + } + }, + "pillowresize_contract_linear_1080p_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "96x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize contract_linear_1080p RGB8 (basic)." + }, + "baselines": { + "pillowresize_contract_linear_1080p_uchar3_basic[InOutDataType=uchar3][layout=NHWC][shape=96x1080x1920][resizeType=CONTRACT][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2193.92124916192, + "gpu_time_us_python": 2210.48254441848, + "gpu_noise_us_cpp": 3.308, + "gpu_noise_us_python": 3.53, + "gpu_bwutil_cpp": 0.21878678944255164, + "gpu_bwutil_python": 0.2171478962049958, + "gpu_gap_stddev_us": 1.932980686518917 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1788.0261271995398, + "gpu_time_us_python": 1803.69319013504, + "gpu_noise_us_cpp": 1.6340000000000003, + "gpu_noise_us_python": 3.718, + "gpu_bwutil_cpp": 0.20486977181527638, + "gpu_bwutil_python": 0.20308880622933406, + "gpu_gap_stddev_us": 1.9439682248726864 + } + } + } + }, + "pillowresize_contract_linear_1080p_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize contract_linear_1080p RGBf32 (basic)." + }, + "baselines": { + "pillowresize_contract_linear_1080p_float3_basic[InOutDataType=float3][layout=NHWC][shape=32x1080x1920][resizeType=CONTRACT][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 951.3760779312199, + "gpu_time_us_python": 967.02868640416, + "gpu_noise_us_cpp": 1.034, + "gpu_noise_us_python": 2.912, + "gpu_bwutil_cpp": 0.6727109781241994, + "gpu_bwutil_python": 0.6618259202651589, + "gpu_gap_stddev_us": 1.8723487045370366 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 775.544838199, + "gpu_time_us_python": 791.26383775202, + "gpu_noise_us_cpp": 1.208, + "gpu_noise_us_python": 2.138, + "gpu_bwutil_cpp": 0.6297867962109869, + "gpu_bwutil_python": 0.6173257929092284, + "gpu_gap_stddev_us": 2.693577872978561 + } + } + } + }, + "pillowresize_contract_linear_1080p_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "96x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize contract_linear_1080p RGBA8 mirror (advanced)." + }, + "baselines": { + "pillowresize_contract_linear_1080p_uchar4_advanced[InOutDataType=uchar4][layout=NHWC][shape=96x1080x1920][resizeType=CONTRACT][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2370.41502313788, + "gpu_time_us_python": 2389.3645899635603, + "gpu_noise_us_cpp": 2.08, + "gpu_noise_us_python": 5.209999999999999, + "gpu_bwutil_cpp": 0.26999550913417153, + "gpu_bwutil_python": 0.2678543523755401, + "gpu_gap_stddev_us": 4.9608162145436925 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2167.57106911412, + "gpu_time_us_python": 2183.35719402992, + "gpu_noise_us_cpp": 1.6300000000000001, + "gpu_noise_us_python": 3.81, + "gpu_bwutil_cpp": 0.22532033695058598, + "gpu_bwutil_python": 0.22369369260854782, + "gpu_gap_stddev_us": 1.8320240648341182 + } + } + } + }, + "pillowresize_contract_linear_1080p_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize contract_linear_1080p RGBAf32 mirror (advanced)." + }, + "baselines": { + "pillowresize_contract_linear_1080p_float4_advanced[InOutDataType=float4][layout=NHWC][shape=32x1080x1920][resizeType=CONTRACT][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1268.96462199534, + "gpu_time_us_python": 1286.04706844004, + "gpu_noise_us_cpp": 1.9600000000000002, + "gpu_noise_us_python": 4.3839999999999995, + "gpu_bwutil_cpp": 0.6724646667473476, + "gpu_bwutil_python": 0.6635388871298904, + "gpu_gap_stddev_us": 4.1576977431726 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 875.6597900311399, + "gpu_time_us_python": 891.1419298338, + "gpu_noise_us_cpp": 1.124, + "gpu_noise_us_python": 2.602, + "gpu_bwutil_cpp": 0.7433710303366612, + "gpu_bwutil_python": 0.7304928298157539, + "gpu_gap_stddev_us": 3.4306050852521475 + } + } + } + }, + "pillowresize_contract_linear_1080p_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "64x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize contract_linear_1080p single-channel (advanced)." + }, + "baselines": { + "pillowresize_contract_linear_1080p_scalar_advanced[InOutDataType=float32][layout=NHWC][shape=64x1080x1920][resizeType=CONTRACT][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1145.0257400094201, + "gpu_time_us_python": 1162.5319719466402, + "gpu_noise_us_cpp": 1.4440000000000002, + "gpu_noise_us_python": 3.9479999999999995, + "gpu_bwutil_cpp": 0.3726267487504418, + "gpu_bwutil_python": 0.36702277608420897, + "gpu_gap_stddev_us": 5.279619760636265 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 837.2462548997, + "gpu_time_us_python": 852.5731760835799, + "gpu_noise_us_cpp": 1.1800000000000002, + "gpu_noise_us_python": 3.9260000000000006, + "gpu_bwutil_cpp": 0.38889591724552836, + "gpu_bwutil_python": 0.381911713463353, + "gpu_gap_stddev_us": 0.821827137027877 + } + }, + "pillowresize_contract_linear_1080p_scalar_advanced[InOutDataType=uint8][layout=NHWC][shape=64x1080x1920][resizeType=CONTRACT][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 910.70034975552, + "gpu_time_us_python": 926.8739652389999, + "gpu_noise_us_cpp": 2.1959999999999997, + "gpu_noise_us_python": 4.33, + "gpu_bwutil_cpp": 0.11712637632952065, + "gpu_bwutil_python": 0.11508322745939588, + "gpu_gap_stddev_us": 1.8638213548061362 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 845.22903447924, + "gpu_time_us_python": 861.13447365242, + "gpu_noise_us_cpp": 1.6939999999999997, + "gpu_noise_us_python": 3.086, + "gpu_bwutil_cpp": 0.09630550657739109, + "gpu_bwutil_python": 0.09453454349507909, + "gpu_gap_stddev_us": 2.9975861651807874 + } + } + } + }, + "pillowresize_contract_linear_4k_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "24x2160x3840" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize contract_linear_4k RGB8 (advanced)." + }, + "baselines": { + "pillowresize_contract_linear_4k_uchar3_advanced[InOutDataType=uchar3][layout=NHWC][shape=24x2160x3840][resizeType=CONTRACT][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2345.74281389464, + "gpu_time_us_python": 2362.09935385212, + "gpu_noise_us_cpp": 1.684, + "gpu_noise_us_python": 3.658, + "gpu_bwutil_cpp": 0.20462647474972337, + "gpu_bwutil_python": 0.20320956896346618, + "gpu_gap_stddev_us": 3.42963319300148 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1810.2505020791, + "gpu_time_us_python": 1827.3669395560403, + "gpu_noise_us_cpp": 1.456, + "gpu_noise_us_python": 4.093999999999999, + "gpu_bwutil_cpp": 0.20234763012140858, + "gpu_bwutil_python": 0.2004545648856088, + "gpu_gap_stddev_us": 3.75865794458711 + } + } + } + }, + "pillowresize_contract_linear_4k_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "8x2160x3840" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize contract_linear_4k RGBf32 (advanced)." + }, + "baselines": { + "pillowresize_contract_linear_4k_float3_advanced[InOutDataType=float3][layout=NHWC][shape=8x2160x3840][resizeType=CONTRACT][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 953.2707893802799, + "gpu_time_us_python": 968.6415552390399, + "gpu_noise_us_cpp": 3.4240000000000004, + "gpu_noise_us_python": 3.0319999999999996, + "gpu_bwutil_cpp": 0.671373050020015, + "gpu_bwutil_python": 0.660724236079216, + "gpu_gap_stddev_us": 2.706474113127368 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 770.21890477632, + "gpu_time_us_python": 784.6503029456401, + "gpu_noise_us_cpp": 2.48, + "gpu_noise_us_python": 2.302, + "gpu_bwutil_cpp": 0.6341356229923947, + "gpu_bwutil_python": 0.6224674832740502, + "gpu_gap_stddev_us": 0.8650691172285528 + } + } + } + }, + "pillowresize_contract_linear_4k_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "24x2160x3840" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize contract_linear_4k RGBA8 mirror (advanced)." + }, + "baselines": { + "pillowresize_contract_linear_4k_uchar4_advanced[InOutDataType=uchar4][layout=NHWC][shape=24x2160x3840][resizeType=CONTRACT][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2547.06249429716, + "gpu_time_us_python": 2563.96849447362, + "gpu_noise_us_cpp": 3.6439999999999997, + "gpu_noise_us_python": 3.472, + "gpu_bwutil_cpp": 0.25127026425689974, + "gpu_bwutil_python": 0.24961344692077764, + "gpu_gap_stddev_us": 0.8886009719613123 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2175.24146531968, + "gpu_time_us_python": 2193.1803416144803, + "gpu_noise_us_cpp": 1.302, + "gpu_noise_us_python": 7.692, + "gpu_bwutil_cpp": 0.2245225349512947, + "gpu_bwutil_python": 0.22268542774111788, + "gpu_gap_stddev_us": 2.9438142115201353 + } + } + } + }, + "pillowresize_contract_linear_4k_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "8x2160x3840" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize contract_linear_4k RGBAf32 mirror (advanced)." + }, + "baselines": { + "pillowresize_contract_linear_4k_float4_advanced[InOutDataType=float4][layout=NHWC][shape=8x2160x3840][resizeType=CONTRACT][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1271.2414441374399, + "gpu_time_us_python": 1286.2372110685399, + "gpu_noise_us_cpp": 1.6420000000000001, + "gpu_noise_us_python": 2.874, + "gpu_bwutil_cpp": 0.6712601527712238, + "gpu_bwutil_python": 0.663434654493593, + "gpu_gap_stddev_us": 1.0971353099237058 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 865.7221419212799, + "gpu_time_us_python": 881.71411130802, + "gpu_noise_us_cpp": 1.5619999999999998, + "gpu_noise_us_python": 3.6420000000000003, + "gpu_bwutil_cpp": 0.7518946267558102, + "gpu_bwutil_python": 0.7382841726065611, + "gpu_gap_stddev_us": 3.101317930692214 + } + } + } + }, + "pillowresize_contract_linear_4k_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "16x2160x3840" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize contract_linear_4k single-channel (advanced)." + }, + "baselines": { + "pillowresize_contract_linear_4k_scalar_advanced[InOutDataType=float32][layout=NHWC][shape=16x2160x3840][resizeType=CONTRACT][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1163.3492896722598, + "gpu_time_us_python": 1179.44617945766, + "gpu_noise_us_cpp": 1.8960000000000001, + "gpu_noise_us_python": 3.976, + "gpu_bwutil_cpp": 0.36675748737498115, + "gpu_bwutil_python": 0.36175281348790467, + "gpu_gap_stddev_us": 1.5383006975225728 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 838.6959792266, + "gpu_time_us_python": 855.4464352197599, + "gpu_noise_us_cpp": 0.818, + "gpu_noise_us_python": 2.528, + "gpu_bwutil_cpp": 0.3882066103472019, + "gpu_bwutil_python": 0.38061446757557293, + "gpu_gap_stddev_us": 3.282162270475871 + } + }, + "pillowresize_contract_linear_4k_scalar_advanced[InOutDataType=uint8][layout=NHWC][shape=16x2160x3840][resizeType=CONTRACT][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 954.1686795598201, + "gpu_time_us_python": 969.7707571889999, + "gpu_noise_us_cpp": 2.5919999999999996, + "gpu_noise_us_python": 4.412, + "gpu_bwutil_cpp": 0.11179047477407907, + "gpu_bwutil_python": 0.1099921403252206, + "gpu_gap_stddev_us": 1.0931978544371204 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 848.61929474396, + "gpu_time_us_python": 864.98817531984, + "gpu_noise_us_cpp": 1.9419999999999997, + "gpu_noise_us_python": 2.836, + "gpu_bwutil_cpp": 0.09591789650224701, + "gpu_bwutil_python": 0.09410447561299862, + "gpu_gap_stddev_us": 3.1633282621317558 + } + } + } + }, + "pillowresize_contract_cubic_720p_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "192x720x1280" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize contract_cubic_720p RGB8 (advanced)." + }, + "baselines": { + "pillowresize_contract_cubic_720p_uchar3_advanced[InOutDataType=uchar3][layout=NHWC][shape=192x720x1280][resizeType=CONTRACT][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2497.72536067672, + "gpu_time_us_python": 2514.14325571056, + "gpu_noise_us_cpp": 4.093999999999999, + "gpu_noise_us_python": 5.584, + "gpu_bwutil_cpp": 0.17082249417701537, + "gpu_bwutil_python": 0.16970742210206619, + "gpu_gap_stddev_us": 3.492561338909311 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2074.01693741148, + "gpu_time_us_python": 2091.7845553927805, + "gpu_noise_us_cpp": 1.044, + "gpu_noise_us_python": 7.353999999999999, + "gpu_bwutil_cpp": 0.1570017530958845, + "gpu_bwutil_python": 0.15566569750962223, + "gpu_gap_stddev_us": 2.2559918458141275 + } + } + } + }, + "pillowresize_contract_cubic_720p_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "64x720x1280" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize contract_cubic_720p RGBf32 (advanced)." + }, + "baselines": { + "pillowresize_contract_cubic_720p_float3_advanced[InOutDataType=float3][layout=NHWC][shape=64x720x1280][resizeType=CONTRACT][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1369.44500201382, + "gpu_time_us_python": 1384.2160695164998, + "gpu_noise_us_cpp": 1.25, + "gpu_noise_us_python": 2.9479999999999995, + "gpu_bwutil_cpp": 0.41541594214037725, + "gpu_bwutil_python": 0.41098338033957954, + "gpu_gap_stddev_us": 1.3885782592972769 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1126.10261089954, + "gpu_time_us_python": 1141.04675629024, + "gpu_noise_us_cpp": 1.214, + "gpu_noise_us_python": 3.0140000000000002, + "gpu_bwutil_cpp": 0.3854145159736139, + "gpu_bwutil_python": 0.38037239802871936, + "gpu_gap_stddev_us": 1.077606355142839 + } + } + } + }, + "pillowresize_contract_cubic_720p_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "192x720x1280" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize contract_cubic_720p RGBA8 mirror (advanced)." + }, + "baselines": { + "pillowresize_contract_cubic_720p_uchar4_advanced[InOutDataType=uchar4][layout=NHWC][shape=192x720x1280][resizeType=CONTRACT][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2983.8969996996802, + "gpu_time_us_python": 3003.4358637673004, + "gpu_noise_us_cpp": 3.018, + "gpu_noise_us_python": 3.3739999999999997, + "gpu_bwutil_cpp": 0.19065354278004795, + "gpu_bwutil_python": 0.18941335787706454, + "gpu_gap_stddev_us": 4.803548632226401 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2681.78463820052, + "gpu_time_us_python": 2697.41028956568, + "gpu_noise_us_cpp": 1.6920000000000002, + "gpu_noise_us_python": 2.818, + "gpu_bwutil_cpp": 0.16189012335234182, + "gpu_bwutil_python": 0.16095101788329774, + "gpu_gap_stddev_us": 1.4411544100998268 + } + } + } + }, + "pillowresize_contract_cubic_720p_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "64x720x1280" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize contract_cubic_720p RGBAf32 mirror (advanced)." + }, + "baselines": { + "pillowresize_contract_cubic_720p_float4_advanced[InOutDataType=float4][layout=NHWC][shape=64x720x1280][resizeType=CONTRACT][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1765.6990390428, + "gpu_time_us_python": 1782.1085745560601, + "gpu_noise_us_cpp": 1.31, + "gpu_noise_us_python": 5.068, + "gpu_bwutil_cpp": 0.4295854770500263, + "gpu_bwutil_python": 0.42563052736810814, + "gpu_gap_stddev_us": 2.239304255628821 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1300.37811361036, + "gpu_time_us_python": 1319.32782669, + "gpu_noise_us_cpp": 1.9979999999999998, + "gpu_noise_us_python": 4.64, + "gpu_bwutil_cpp": 0.4449490974445765, + "gpu_bwutil_python": 0.4385602170864315, + "gpu_gap_stddev_us": 4.456545216188939 + } + } + } + }, + "pillowresize_contract_cubic_720p_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "128x720x1280" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize contract_cubic_720p single-channel (advanced)." + }, + "baselines": { + "pillowresize_contract_cubic_720p_scalar_advanced[InOutDataType=float32][layout=NHWC][shape=128x720x1280][resizeType=CONTRACT][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1357.06012133002, + "gpu_time_us_python": 1373.6955301242601, + "gpu_noise_us_cpp": 1.6640000000000001, + "gpu_noise_us_python": 6.256, + "gpu_bwutil_cpp": 0.2794714249471434, + "gpu_bwutil_python": 0.2760883412844102, + "gpu_gap_stddev_us": 2.780567613788339 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1033.81531639802, + "gpu_time_us_python": 1050.37687712666, + "gpu_noise_us_cpp": 1.094, + "gpu_noise_us_python": 4.0600000000000005, + "gpu_bwutil_cpp": 0.2798937021073966, + "gpu_bwutil_python": 0.2754818337646026, + "gpu_gap_stddev_us": 3.350597963910957 + } + }, + "pillowresize_contract_cubic_720p_scalar_advanced[InOutDataType=uint8][layout=NHWC][shape=128x720x1280][resizeType=CONTRACT][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1013.5721487524601, + "gpu_time_us_python": 1028.36732783242, + "gpu_noise_us_cpp": 1.7079999999999997, + "gpu_noise_us_python": 3.6379999999999995, + "gpu_bwutil_cpp": 0.09354549762136802, + "gpu_bwutil_python": 0.09220100020562128, + "gpu_gap_stddev_us": 3.333040634416439 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 903.3077215166201, + "gpu_time_us_python": 918.5774960133001, + "gpu_noise_us_cpp": 0.932, + "gpu_noise_us_python": 3.754, + "gpu_bwutil_cpp": 0.08009963558274893, + "gpu_bwutil_python": 0.07877029083655898, + "gpu_gap_stddev_us": 1.76981166777156 + } + } + } + }, + "pillowresize_contract_cubic_1080p_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "96x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize contract_cubic_1080p RGB8 (basic)." + }, + "baselines": { + "pillowresize_contract_cubic_1080p_uchar3_basic[InOutDataType=uchar3][layout=NHWC][shape=96x1080x1920][resizeType=CONTRACT][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2884.0171622133403, + "gpu_time_us_python": 2902.4397783373197, + "gpu_noise_us_cpp": 3.626, + "gpu_noise_us_python": 2.9160000000000004, + "gpu_bwutil_cpp": 0.16643488918282717, + "gpu_bwutil_python": 0.16537862210953752, + "gpu_gap_stddev_us": 3.9070028309418188 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2365.8933626153403, + "gpu_time_us_python": 2382.9304403000797, + "gpu_noise_us_cpp": 2.818, + "gpu_noise_us_python": 2.9979999999999998, + "gpu_bwutil_cpp": 0.15483369707379896, + "gpu_bwutil_python": 0.15372760497047025, + "gpu_gap_stddev_us": 4.4035451105657435 + } + } + } + }, + "pillowresize_contract_cubic_1080p_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize contract_cubic_1080p RGBf32 (basic)." + }, + "baselines": { + "pillowresize_contract_cubic_1080p_float3_basic[InOutDataType=float3][layout=NHWC][shape=32x1080x1920][resizeType=CONTRACT][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1551.8035898818002, + "gpu_time_us_python": 1568.3948737963601, + "gpu_noise_us_cpp": 2.2880000000000003, + "gpu_noise_us_python": 3.478, + "gpu_bwutil_cpp": 0.41242350849718035, + "gpu_bwutil_python": 0.40806268784873617, + "gpu_gap_stddev_us": 4.262353808724191 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1265.65794522958, + "gpu_time_us_python": 1282.12386424166, + "gpu_noise_us_cpp": 1.394, + "gpu_noise_us_python": 4.336, + "gpu_bwutil_cpp": 0.3857747661437576, + "gpu_bwutil_python": 0.3808200652141963, + "gpu_gap_stddev_us": 2.1199867868061526 + } + } + } + }, + "pillowresize_contract_cubic_1080p_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "96x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize contract_cubic_1080p RGBA8 mirror (advanced)." + }, + "baselines": { + "pillowresize_contract_cubic_1080p_uchar4_advanced[InOutDataType=uchar4][layout=NHWC][shape=96x1080x1920][resizeType=CONTRACT][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3397.09234495416, + "gpu_time_us_python": 3414.4947986344605, + "gpu_noise_us_cpp": 1.218, + "gpu_noise_us_python": 2.732, + "gpu_bwutil_cpp": 0.1883970785284336, + "gpu_bwutil_python": 0.18743690604871294, + "gpu_gap_stddev_us": 3.265140927640658 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3014.17398883682, + "gpu_time_us_python": 3031.23863536104, + "gpu_noise_us_cpp": 1.5259999999999998, + "gpu_noise_us_python": 3.7479999999999998, + "gpu_bwutil_cpp": 0.16203875606895748, + "gpu_bwutil_python": 0.16112783330243385, + "gpu_gap_stddev_us": 3.2166368912420853 + } + } + } + }, + "pillowresize_contract_cubic_1080p_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize contract_cubic_1080p RGBAf32 mirror (advanced)." + }, + "baselines": { + "pillowresize_contract_cubic_1080p_float4_advanced[InOutDataType=float4][layout=NHWC][shape=32x1080x1920][resizeType=CONTRACT][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2006.2994606017396, + "gpu_time_us_python": 2023.729509692, + "gpu_noise_us_cpp": 1.9, + "gpu_noise_us_python": 4.792, + "gpu_bwutil_cpp": 0.4253272290854886, + "gpu_bwutil_python": 0.4216650058105202, + "gpu_gap_stddev_us": 3.0955866654380957 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1465.46178600292, + "gpu_time_us_python": 1487.4822319723403, + "gpu_noise_us_cpp": 2.332, + "gpu_noise_us_python": 5.202, + "gpu_bwutil_cpp": 0.4441804632727382, + "gpu_bwutil_python": 0.4376381561327277, + "gpu_gap_stddev_us": 7.8665457067900775 + } + } + } + }, + "pillowresize_contract_cubic_1080p_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "64x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize contract_cubic_1080p single-channel (advanced)." + }, + "baselines": { + "pillowresize_contract_cubic_1080p_scalar_advanced[InOutDataType=float32][layout=NHWC][shape=64x1080x1920][resizeType=CONTRACT][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1581.38717489056, + "gpu_time_us_python": 1597.75045965884, + "gpu_noise_us_cpp": 3.252, + "gpu_noise_us_python": 4.128, + "gpu_bwutil_cpp": 0.269805584577588, + "gpu_bwutil_python": 0.2670428247847052, + "gpu_gap_stddev_us": 3.459302812044153 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1176.5018286376, + "gpu_time_us_python": 1193.92986995064, + "gpu_noise_us_cpp": 1.59, + "gpu_noise_us_python": 2.79, + "gpu_bwutil_cpp": 0.2766881371614845, + "gpu_bwutil_python": 0.272661817713421, + "gpu_gap_stddev_us": 4.047591471667298 + } + }, + "pillowresize_contract_cubic_1080p_scalar_advanced[InOutDataType=uint8][layout=NHWC][shape=64x1080x1920][resizeType=CONTRACT][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1184.8636849817199, + "gpu_time_us_python": 1199.8734366209999, + "gpu_noise_us_cpp": 1.6759999999999997, + "gpu_noise_us_python": 3.9699999999999998, + "gpu_bwutil_cpp": 0.09002464591139248, + "gpu_bwutil_python": 0.08889865567710328, + "gpu_gap_stddev_us": 1.5785181958407055 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1024.7959969613, + "gpu_time_us_python": 1041.80889894688, + "gpu_noise_us_cpp": 1.386, + "gpu_noise_us_python": 2.822, + "gpu_bwutil_cpp": 0.07942800038655173, + "gpu_bwutil_python": 0.07813520842591384, + "gpu_gap_stddev_us": 4.198664247988896 + } + } + } + }, + "pillowresize_contract_cubic_4k_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "24x2160x3840" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize contract_cubic_4k RGB8 (advanced)." + }, + "baselines": { + "pillowresize_contract_cubic_4k_uchar3_advanced[InOutDataType=uchar3][layout=NHWC][shape=24x2160x3840][resizeType=CONTRACT][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2957.2758747549005, + "gpu_time_us_python": 2975.63380117836, + "gpu_noise_us_cpp": 3.914, + "gpu_noise_us_python": 4.7, + "gpu_bwutil_cpp": 0.1623119368607912, + "gpu_bwutil_python": 0.1613109694401978, + "gpu_gap_stddev_us": 4.3039514461182105 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2377.50760181304, + "gpu_time_us_python": 2393.4975189838797, + "gpu_noise_us_cpp": 1.234, + "gpu_noise_us_python": 3.124, + "gpu_bwutil_cpp": 0.1540754142146682, + "gpu_bwutil_python": 0.15304461519577636, + "gpu_gap_stddev_us": 2.92958099403684 + } + } + } + }, + "pillowresize_contract_cubic_4k_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "8x2160x3840" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize contract_cubic_4k RGBf32 (advanced)." + }, + "baselines": { + "pillowresize_contract_cubic_4k_float3_advanced[InOutDataType=float3][layout=NHWC][shape=8x2160x3840][resizeType=CONTRACT][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1555.92541383654, + "gpu_time_us_python": 1571.24613815034, + "gpu_noise_us_cpp": 1.72, + "gpu_noise_us_python": 2.9840000000000004, + "gpu_bwutil_cpp": 0.4113310540477615, + "gpu_bwutil_python": 0.4073207424528724, + "gpu_gap_stddev_us": 1.5855312711287934 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1278.69510717286, + "gpu_time_us_python": 1294.3360702095802, + "gpu_noise_us_cpp": 2.8240000000000003, + "gpu_noise_us_python": 4.122, + "gpu_bwutil_cpp": 0.38182764517981366, + "gpu_bwutil_python": 0.37722257121244734, + "gpu_gap_stddev_us": 2.474353029950685 + } + } + } + }, + "pillowresize_contract_cubic_4k_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "24x2160x3840" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize contract_cubic_4k RGBA8 mirror (advanced)." + }, + "baselines": { + "pillowresize_contract_cubic_4k_uchar4_advanced[InOutDataType=uchar4][layout=NHWC][shape=24x2160x3840][resizeType=CONTRACT][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3488.8135141795997, + "gpu_time_us_python": 3508.43164854574, + "gpu_noise_us_cpp": 1.7740000000000002, + "gpu_noise_us_python": 3.396, + "gpu_bwutil_cpp": 0.18344400691223006, + "gpu_bwutil_python": 0.18241825809889414, + "gpu_gap_stddev_us": 3.3114163960503458 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3017.1171322201203, + "gpu_time_us_python": 3032.13341449806, + "gpu_noise_us_cpp": 1.61, + "gpu_noise_us_python": 2.1420000000000003, + "gpu_bwutil_cpp": 0.16187741616337023, + "gpu_bwutil_python": 0.16107700320288892, + "gpu_gap_stddev_us": 1.2873433599508963 + } + } + } + }, + "pillowresize_contract_cubic_4k_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "8x2160x3840" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize contract_cubic_4k RGBAf32 mirror (advanced)." + }, + "baselines": { + "pillowresize_contract_cubic_4k_float4_advanced[InOutDataType=float4][layout=NHWC][shape=8x2160x3840][resizeType=CONTRACT][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2024.0118895807002, + "gpu_time_us_python": 2041.98425068112, + "gpu_noise_us_cpp": 1.796, + "gpu_noise_us_python": 3.7600000000000002, + "gpu_bwutil_cpp": 0.42160518784529566, + "gpu_bwutil_python": 0.41789522536948664, + "gpu_gap_stddev_us": 2.987877761405001 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1503.15319881196, + "gpu_time_us_python": 1522.3617661923802, + "gpu_noise_us_cpp": 2.976, + "gpu_noise_us_python": 4.836, + "gpu_bwutil_cpp": 0.4330277673236873, + "gpu_bwutil_python": 0.4275743197301348, + "gpu_gap_stddev_us": 2.6687936034665625 + } + } + } + }, + "pillowresize_contract_cubic_4k_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "16x2160x3840" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize contract_cubic_4k single-channel (advanced)." + }, + "baselines": { + "pillowresize_contract_cubic_4k_scalar_advanced[InOutDataType=float32][layout=NHWC][shape=16x2160x3840][resizeType=CONTRACT][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1633.48981466144, + "gpu_time_us_python": 1650.05744083928, + "gpu_noise_us_cpp": 1.8379999999999999, + "gpu_noise_us_python": 4.8340000000000005, + "gpu_bwutil_cpp": 0.26119965560141195, + "gpu_bwutil_python": 0.2585776013508208, + "gpu_gap_stddev_us": 2.254917441073459 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1198.63363249602, + "gpu_time_us_python": 1214.15078231558, + "gpu_noise_us_cpp": 1.2279999999999998, + "gpu_noise_us_python": 3.5159999999999996, + "gpu_bwutil_cpp": 0.2715781788698208, + "gpu_bwutil_python": 0.26810476311527626, + "gpu_gap_stddev_us": 1.502990351069855 + } + }, + "pillowresize_contract_cubic_4k_scalar_advanced[InOutDataType=uint8][layout=NHWC][shape=16x2160x3840][resizeType=CONTRACT][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1226.54080982412, + "gpu_time_us_python": 1241.52745920636, + "gpu_noise_us_cpp": 1.5420000000000003, + "gpu_noise_us_python": 4.218, + "gpu_bwutil_cpp": 0.08696566588045383, + "gpu_bwutil_python": 0.08591623973387365, + "gpu_gap_stddev_us": 2.6111010261860357 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1034.60438402714, + "gpu_time_us_python": 1050.03388894732, + "gpu_noise_us_cpp": 1.032, + "gpu_noise_us_python": 2.882, + "gpu_bwutil_cpp": 0.07867154536766341, + "gpu_bwutil_python": 0.07751587511036222, + "gpu_gap_stddev_us": 1.6497471169036502 + } + } + } + }, + "pillowresize_contract_lanczos_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LANCZOS" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize single-channel contract lanczos (advanced)." + }, + "baselines": { + "pillowresize_contract_lanczos_1080p_advanced[InOutDataType=float32][layout=NHWC][shape=32x1080x1920][resizeType=CONTRACT][interpolation=LANCZOS][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 892.9747852478, + "gpu_time_us_python": 908.1666752810601, + "gpu_noise_us_cpp": 2.8, + "gpu_noise_us_python": 3.216, + "gpu_bwutil_cpp": 0.23890217549423487, + "gpu_bwutil_python": 0.23490705957147878, + "gpu_gap_stddev_us": 2.000345265693308 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 688.70078510814, + "gpu_time_us_python": 706.52699710288, + "gpu_noise_us_cpp": 0.9019999999999999, + "gpu_noise_us_python": 3.8240000000000007, + "gpu_bwutil_cpp": 0.2363470219206242, + "gpu_bwutil_python": 0.23038503450170572, + "gpu_gap_stddev_us": 3.495145220395931 + } + }, + "pillowresize_contract_lanczos_1080p_advanced[InOutDataType=float32][layout=NHWC][shape=32x1080x1920][resizeType=CONTRACT][interpolation=LANCZOS][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1108.80931443126, + "gpu_time_us_python": 1130.96424540708, + "gpu_noise_us_cpp": 5.208, + "gpu_noise_us_python": 4.618, + "gpu_bwutil_cpp": 0.19240430603335043, + "gpu_bwutil_python": 0.18864392318829531, + "gpu_gap_stddev_us": 4.459112420642809 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 987.0977316904998, + "gpu_time_us_python": 1015.5422712174801, + "gpu_noise_us_cpp": 3.822, + "gpu_noise_us_python": 6.153999999999999, + "gpu_bwutil_cpp": 0.16493751781793414, + "gpu_bwutil_python": 0.16034412167326395, + "gpu_gap_stddev_us": 5.229382902472931 + } + }, + "pillowresize_contract_lanczos_1080p_advanced[InOutDataType=uint8][layout=NHWC][shape=32x1080x1920][resizeType=CONTRACT][interpolation=LANCZOS][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 764.49738696282, + "gpu_time_us_python": 779.23310845756, + "gpu_noise_us_cpp": 1.472, + "gpu_noise_us_python": 2.7060000000000004, + "gpu_bwutil_cpp": 0.06976298532177835, + "gpu_bwutil_python": 0.06844472503414924, + "gpu_gap_stddev_us": 2.4952370185215655 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 627.332824031, + "gpu_time_us_python": 644.24327244918, + "gpu_noise_us_cpp": 0.686, + "gpu_noise_us_python": 2.9240000000000004, + "gpu_bwutil_cpp": 0.06487791348075297, + "gpu_bwutil_python": 0.06317573909684245, + "gpu_gap_stddev_us": 4.0005913558793225 + } + }, + "pillowresize_contract_lanczos_1080p_advanced[InOutDataType=uint8][layout=NHWC][shape=32x1080x1920][resizeType=CONTRACT][interpolation=LANCZOS][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1057.9828497187, + "gpu_time_us_python": 1078.701993627, + "gpu_noise_us_cpp": 5.298, + "gpu_noise_us_python": 5.026, + "gpu_bwutil_cpp": 0.05041197774290962, + "gpu_bwutil_python": 0.04944541563735058, + "gpu_gap_stddev_us": 3.275223283728619 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 972.2058587398402, + "gpu_time_us_python": 998.2662560642399, + "gpu_noise_us_cpp": 1.584, + "gpu_noise_us_python": 5.372, + "gpu_bwutil_cpp": 0.0418695524272525, + "gpu_bwutil_python": 0.040779101863638575, + "gpu_gap_stddev_us": 6.468647505122748 + } + } + } + }, + "pillowresize_contract_box_hamming_u8_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "64x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "BOX", + "HAMMING" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize single-channel contract box hamming (advanced)." + }, + "baselines": { + "pillowresize_contract_box_hamming_u8_1080p_advanced[InOutDataType=uint8][layout=NHWC][shape=64x1080x1920][resizeType=CONTRACT][interpolation=BOX][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 832.4326906591799, + "gpu_time_us_python": 847.73276277422, + "gpu_noise_us_cpp": 2.582, + "gpu_noise_us_python": 4.3, + "gpu_bwutil_cpp": 0.1281387305267555, + "gpu_bwutil_python": 0.12582772243428036, + "gpu_gap_stddev_us": 2.907197298481798 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 757.3179017045999, + "gpu_time_us_python": 772.77119134188, + "gpu_noise_us_cpp": 0.9959999999999999, + "gpu_noise_us_python": 2.08, + "gpu_bwutil_cpp": 0.10747104533956853, + "gpu_bwutil_python": 0.10532335405944218, + "gpu_gap_stddev_us": 2.639289972867218 + } + }, + "pillowresize_contract_box_hamming_u8_1080p_advanced[InOutDataType=uint8][layout=NHWC][shape=64x1080x1920][resizeType=CONTRACT][interpolation=BOX][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1164.3505034043199, + "gpu_time_us_python": 1188.96861464026, + "gpu_noise_us_cpp": 3.196, + "gpu_noise_us_python": 4.930000000000001, + "gpu_bwutil_cpp": 0.0916144388110959, + "gpu_bwutil_python": 0.0897211942109273, + "gpu_gap_stddev_us": 6.141419914611207 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1070.30184867336, + "gpu_time_us_python": 1097.39421879776, + "gpu_noise_us_cpp": 4.646, + "gpu_noise_us_python": 5.764000000000001, + "gpu_bwutil_cpp": 0.07604182449997372, + "gpu_bwutil_python": 0.0741739864795226, + "gpu_gap_stddev_us": 9.121137612707432 + } + }, + "pillowresize_contract_box_hamming_u8_1080p_advanced[InOutDataType=uint8][layout=NHWC][shape=64x1080x1920][resizeType=CONTRACT][interpolation=HAMMING][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 914.3390827270999, + "gpu_time_us_python": 928.1853977017402, + "gpu_noise_us_cpp": 1.226, + "gpu_noise_us_python": 3.37, + "gpu_bwutil_cpp": 0.11666010784337652, + "gpu_bwutil_python": 0.1149202422747615, + "gpu_gap_stddev_us": 2.1636845707747376 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 846.8466527976601, + "gpu_time_us_python": 863.47857898934, + "gpu_noise_us_cpp": 1.828, + "gpu_noise_us_python": 3.3880000000000003, + "gpu_bwutil_cpp": 0.09612136262954307, + "gpu_bwutil_python": 0.09427055865420735, + "gpu_gap_stddev_us": 3.208629723257528 + } + }, + "pillowresize_contract_box_hamming_u8_1080p_advanced[InOutDataType=uint8][layout=NHWC][shape=64x1080x1920][resizeType=CONTRACT][interpolation=HAMMING][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1252.13887241468, + "gpu_time_us_python": 1276.9919980657, + "gpu_noise_us_cpp": 3.4799999999999995, + "gpu_noise_us_python": 6.18, + "gpu_bwutil_cpp": 0.08519167381538603, + "gpu_bwutil_python": 0.08353734072071292, + "gpu_gap_stddev_us": 8.943420679542344 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1155.18268927294, + "gpu_time_us_python": 1184.30514152102, + "gpu_noise_us_cpp": 2.6879999999999997, + "gpu_noise_us_python": 5.412, + "gpu_bwutil_cpp": 0.07046966529401977, + "gpu_bwutil_python": 0.06873749353467198, + "gpu_gap_stddev_us": 3.767348162395974 + } + } + } + }, + "pillowresize_expand_lanczos_tensor_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LANCZOS" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize single-channel expand lanczos tensor (advanced)." + }, + "baselines": { + "pillowresize_expand_lanczos_tensor_1080p_advanced[InOutDataType=float32][layout=NHWC][shape=16x1080x1920][resizeType=EXPAND][interpolation=LANCZOS][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1895.30917150086, + "gpu_time_us_python": 1910.7462024511601, + "gpu_noise_us_cpp": 1.2419999999999998, + "gpu_noise_us_python": 5.958, + "gpu_bwutil_cpp": 0.22511753988756994, + "gpu_bwutil_python": 0.22329876548177943, + "gpu_gap_stddev_us": 1.7779020205770912 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1611.4679806783, + "gpu_time_us_python": 1628.20006973346, + "gpu_noise_us_cpp": 2.402, + "gpu_noise_us_python": 4.441999999999999, + "gpu_bwutil_cpp": 0.20201001553629022, + "gpu_bwutil_python": 0.19993287727295309, + "gpu_gap_stddev_us": 2.7889835253852615 + } + }, + "pillowresize_expand_lanczos_tensor_1080p_advanced[InOutDataType=uint8][layout=NHWC][shape=16x1080x1920][resizeType=EXPAND][interpolation=LANCZOS][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1627.39398665236, + "gpu_time_us_python": 1643.40434113868, + "gpu_noise_us_cpp": 4.712, + "gpu_noise_us_python": 3.3240000000000003, + "gpu_bwutil_cpp": 0.06554461064307224, + "gpu_bwutil_python": 0.06490619970329811, + "gpu_gap_stddev_us": 1.553782969761805 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1424.32776745706, + "gpu_time_us_python": 1440.68384816366, + "gpu_noise_us_cpp": 1.3359999999999999, + "gpu_noise_us_python": 3.7339999999999995, + "gpu_bwutil_cpp": 0.05715340005605616, + "gpu_bwutil_python": 0.05650685499645093, + "gpu_gap_stddev_us": 3.2006398565121517 + } + } + } + }, + "pillowresize_expand_linear_720p_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "96x720x1280" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize expand_linear_720p RGB8 (advanced)." + }, + "baselines": { + "pillowresize_expand_linear_720p_uchar3_advanced[InOutDataType=uchar3][layout=NHWC][shape=96x720x1280][resizeType=EXPAND][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6312.821729183141, + "gpu_time_us_python": 6333.742084503139, + "gpu_noise_us_cpp": 4.3100000000000005, + "gpu_noise_us_python": 6.798, + "gpu_bwutil_cpp": 0.13517479706349067, + "gpu_bwutil_python": 0.13472843702149567, + "gpu_gap_stddev_us": 6.852812963713791 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4714.061110128179, + "gpu_time_us_python": 4732.135306064, + "gpu_noise_us_cpp": 1.5139999999999998, + "gpu_noise_us_python": 3.628, + "gpu_bwutil_cpp": 0.1381388627942348, + "gpu_bwutil_python": 0.13761104907187965, + "gpu_gap_stddev_us": 4.059354920230361 + } + } + } + }, + "pillowresize_expand_linear_720p_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "32x720x1280" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize expand_linear_720p RGBf32 (advanced)." + }, + "baselines": { + "pillowresize_expand_linear_720p_float3_advanced[InOutDataType=float3][layout=NHWC][shape=32x720x1280][resizeType=EXPAND][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3111.9815555618998, + "gpu_time_us_python": 3128.33675748766, + "gpu_noise_us_cpp": 1.7239999999999998, + "gpu_noise_us_python": 4.872, + "gpu_bwutil_cpp": 0.3656123841342707, + "gpu_bwutil_python": 0.36370083506197803, + "gpu_gap_stddev_us": 1.9852150575231133 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1934.9977701551602, + "gpu_time_us_python": 1953.8629949613999, + "gpu_noise_us_cpp": 1.7919999999999998, + "gpu_noise_us_python": 2.908, + "gpu_bwutil_cpp": 0.4485565618026676, + "gpu_bwutil_python": 0.4442325584861342, + "gpu_gap_stddev_us": 1.836435347671347 + } + } + } + }, + "pillowresize_expand_linear_720p_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "96x720x1280" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize expand_linear_720p RGBA8 mirror (advanced)." + }, + "baselines": { + "pillowresize_expand_linear_720p_uchar4_advanced[InOutDataType=uchar4][layout=NHWC][shape=96x720x1280][resizeType=EXPAND][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 7998.483195900861, + "gpu_time_us_python": 8011.423984169899, + "gpu_noise_us_cpp": 3.502, + "gpu_noise_us_python": 6.182, + "gpu_bwutil_cpp": 0.14224942484995942, + "gpu_bwutil_python": 0.1420196423556016, + "gpu_gap_stddev_us": 6.019254792523598 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6555.311261359139, + "gpu_time_us_python": 6577.9270932644995, + "gpu_noise_us_cpp": 1.366, + "gpu_noise_us_python": 6.63, + "gpu_bwutil_cpp": 0.13244039936798763, + "gpu_bwutil_python": 0.13198610730493515, + "gpu_gap_stddev_us": 3.5901548300540074 + } + } + } + }, + "pillowresize_expand_linear_720p_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "32x720x1280" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize expand_linear_720p RGBAf32 mirror (advanced)." + }, + "baselines": { + "pillowresize_expand_linear_720p_float4_advanced[InOutDataType=float4][layout=NHWC][shape=32x720x1280][resizeType=EXPAND][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3646.53524937832, + "gpu_time_us_python": 3663.14480615694, + "gpu_noise_us_cpp": 1.556, + "gpu_noise_us_python": 3.7880000000000003, + "gpu_bwutil_cpp": 0.4160219309787834, + "gpu_bwutil_python": 0.4141360036398639, + "gpu_gap_stddev_us": 3.552895355408541 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2472.03359868744, + "gpu_time_us_python": 2506.6914126408, + "gpu_noise_us_cpp": 4.776000000000001, + "gpu_noise_us_python": 3.152, + "gpu_bwutil_cpp": 0.4680993137694854, + "gpu_bwutil_python": 0.46164886782541653, + "gpu_gap_stddev_us": 7.350119531783491 + } + } + } + }, + "pillowresize_expand_linear_720p_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "64x720x1280" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize expand_linear_720p single-channel (advanced)." + }, + "baselines": { + "pillowresize_expand_linear_720p_scalar_advanced[InOutDataType=float32][layout=NHWC][shape=64x720x1280][resizeType=EXPAND][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3861.99867908768, + "gpu_time_us_python": 3884.54242779652, + "gpu_noise_us_cpp": 3.6379999999999995, + "gpu_noise_us_python": 3.914, + "gpu_bwutil_cpp": 0.19640595501899216, + "gpu_bwutil_python": 0.19526637953609838, + "gpu_gap_stddev_us": 2.9836957296448934 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2985.09571787128, + "gpu_time_us_python": 3001.8629587337205, + "gpu_noise_us_cpp": 1.46, + "gpu_noise_us_python": 3.0780000000000003, + "gpu_bwutil_cpp": 0.19387324744801737, + "gpu_bwutil_python": 0.1927930125298304, + "gpu_gap_stddev_us": 2.631497157744647 + } + }, + "pillowresize_expand_linear_720p_scalar_advanced[InOutDataType=uint8][layout=NHWC][shape=64x720x1280][resizeType=EXPAND][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2141.8194236918, + "gpu_time_us_python": 2158.6728970806603, + "gpu_noise_us_cpp": 2.7, + "gpu_noise_us_python": 3.0660000000000003, + "gpu_bwutil_cpp": 0.08853688096567833, + "gpu_bwutil_python": 0.08784576810900788, + "gpu_gap_stddev_us": 3.3220277678555266 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1779.5469707051202, + "gpu_time_us_python": 1795.39838755558, + "gpu_noise_us_cpp": 1.394, + "gpu_noise_us_python": 2.618, + "gpu_bwutil_cpp": 0.08131619400092681, + "gpu_bwutil_python": 0.08059768131618024, + "gpu_gap_stddev_us": 0.33917929777170325 + } + } + } + }, + "pillowresize_expand_linear_1080p_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "48x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize expand_linear_1080p RGB8 (basic)." + }, + "baselines": { + "pillowresize_expand_linear_1080p_uchar3_basic[InOutDataType=uchar3][layout=NHWC][shape=48x1080x1920][resizeType=EXPAND][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 7591.209876898521, + "gpu_time_us_python": 7608.5496122186405, + "gpu_noise_us_cpp": 3.7399999999999998, + "gpu_noise_us_python": 6.702, + "gpu_bwutil_cpp": 0.12646224680380264, + "gpu_bwutil_python": 0.1261739760416965, + "gpu_gap_stddev_us": 6.568856037292812 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5386.6587405643595, + "gpu_time_us_python": 5404.66778591532, + "gpu_noise_us_cpp": 1.7100000000000002, + "gpu_noise_us_python": 4.395999999999999, + "gpu_bwutil_cpp": 0.1359804615876521, + "gpu_bwutil_python": 0.1355274427665744, + "gpu_gap_stddev_us": 4.138422811596538 + } + } + } + }, + "pillowresize_expand_linear_1080p_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize expand_linear_1080p RGBf32 (basic)." + }, + "baselines": { + "pillowresize_expand_linear_1080p_float3_basic[InOutDataType=float3][layout=NHWC][shape=16x1080x1920][resizeType=EXPAND][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3453.9071781994, + "gpu_time_us_python": 3472.0489874186596, + "gpu_noise_us_cpp": 1.782, + "gpu_noise_us_python": 3.6919999999999993, + "gpu_bwutil_cpp": 0.3705951434255377, + "gpu_bwutil_python": 0.36865920891564985, + "gpu_gap_stddev_us": 4.393384055409967 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2171.9702742318595, + "gpu_time_us_python": 2190.14840480666, + "gpu_noise_us_cpp": 2.9539999999999997, + "gpu_noise_us_python": 4.566000000000001, + "gpu_bwutil_cpp": 0.44956999152704186, + "gpu_bwutil_python": 0.4458390512654803, + "gpu_gap_stddev_us": 3.003673483348598 + } + } + } + }, + "pillowresize_expand_linear_1080p_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "48x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize expand_linear_1080p RGBA8 mirror (advanced)." + }, + "baselines": { + "pillowresize_expand_linear_1080p_uchar4_advanced[InOutDataType=uchar4][layout=NHWC][shape=48x1080x1920][resizeType=EXPAND][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 9534.42603217226, + "gpu_time_us_python": 9558.5090707849, + "gpu_noise_us_cpp": 5.6979999999999995, + "gpu_noise_us_python": 9.72, + "gpu_bwutil_cpp": 0.13425048868807393, + "gpu_bwutil_python": 0.13391227921662713, + "gpu_gap_stddev_us": 5.052691896687548 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 7757.11656000308, + "gpu_time_us_python": 7782.332969179281, + "gpu_noise_us_cpp": 1.9579999999999997, + "gpu_noise_us_python": 5.09, + "gpu_bwutil_cpp": 0.1258923708017289, + "gpu_bwutil_python": 0.12548429090968713, + "gpu_gap_stddev_us": 2.019609644470781 + } + } + } + }, + "pillowresize_expand_linear_1080p_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize expand_linear_1080p RGBAf32 mirror (advanced)." + }, + "baselines": { + "pillowresize_expand_linear_1080p_float4_advanced[InOutDataType=float4][layout=NHWC][shape=16x1080x1920][resizeType=EXPAND][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4017.70458826936, + "gpu_time_us_python": 4035.89725198952, + "gpu_noise_us_cpp": 2.2700000000000005, + "gpu_noise_us_python": 4.982, + "gpu_bwutil_cpp": 0.4247868158876371, + "gpu_bwutil_python": 0.4228724912225261, + "gpu_gap_stddev_us": 4.9650162143004 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2772.7530242474, + "gpu_time_us_python": 2787.41196923026, + "gpu_noise_us_cpp": 4.9079999999999995, + "gpu_noise_us_python": 4.138, + "gpu_bwutil_cpp": 0.4695092112152416, + "gpu_bwutil_python": 0.4670577151478249, + "gpu_gap_stddev_us": 6.064530270663368 + } + } + } + }, + "pillowresize_expand_linear_1080p_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize expand_linear_1080p single-channel (advanced)." + }, + "baselines": { + "pillowresize_expand_linear_1080p_scalar_advanced[InOutDataType=float32][layout=NHWC][shape=32x1080x1920][resizeType=EXPAND][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4505.20504713054, + "gpu_time_us_python": 4524.4159953934195, + "gpu_noise_us_cpp": 3.684, + "gpu_noise_us_python": 5.71, + "gpu_bwutil_cpp": 0.1894107391211552, + "gpu_bwutil_python": 0.18860665812440774, + "gpu_gap_stddev_us": 3.837730314272795 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3614.1805564925403, + "gpu_time_us_python": 3638.57802167794, + "gpu_noise_us_cpp": 2.386, + "gpu_noise_us_python": 12.128, + "gpu_bwutil_cpp": 0.18011774289071367, + "gpu_bwutil_python": 0.17891268027000903, + "gpu_gap_stddev_us": 7.32035044088338 + } + }, + "pillowresize_expand_linear_1080p_scalar_advanced[InOutDataType=uint8][layout=NHWC][shape=32x1080x1920][resizeType=EXPAND][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2506.1970019339997, + "gpu_time_us_python": 2518.92941999432, + "gpu_noise_us_cpp": 2.5060000000000002, + "gpu_noise_us_python": 3.872, + "gpu_bwutil_cpp": 0.08512246946347361, + "gpu_bwutil_python": 0.08469224493348568, + "gpu_gap_stddev_us": 0.9243670980541222 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2021.7394731604, + "gpu_time_us_python": 2039.69211027056, + "gpu_noise_us_cpp": 1.304, + "gpu_noise_us_python": 5.88, + "gpu_bwutil_cpp": 0.08052137844631116, + "gpu_bwutil_python": 0.07981422915140025, + "gpu_gap_stddev_us": 4.149176375166834 + } + } + } + }, + "pillowresize_expand_linear_4k_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "12x2160x3840" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize expand_linear_4k RGB8 (advanced)." + }, + "baselines": { + "pillowresize_expand_linear_4k_uchar3_advanced[InOutDataType=uchar3][layout=NHWC][shape=12x2160x3840][resizeType=EXPAND][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 7549.59664064292, + "gpu_time_us_python": 7568.647918599139, + "gpu_noise_us_cpp": 5.242, + "gpu_noise_us_python": 7.296000000000001, + "gpu_bwutil_cpp": 0.12715923767267429, + "gpu_bwutil_python": 0.12683913975545122, + "gpu_gap_stddev_us": 3.070035541126995 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5455.39073889868, + "gpu_time_us_python": 5473.5061916512805, + "gpu_noise_us_cpp": 3.2020000000000004, + "gpu_noise_us_python": 4.284000000000001, + "gpu_bwutil_cpp": 0.13426440926727637, + "gpu_bwutil_python": 0.13382132775291802, + "gpu_gap_stddev_us": 2.4020321699275065 + } + } + } + }, + "pillowresize_expand_linear_4k_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "4x2160x3840" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize expand_linear_4k RGBf32 (advanced)." + }, + "baselines": { + "pillowresize_expand_linear_4k_float3_advanced[InOutDataType=float3][layout=NHWC][shape=4x2160x3840][resizeType=EXPAND][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3337.2760499318, + "gpu_time_us_python": 3355.8282534280797, + "gpu_noise_us_cpp": 4.85, + "gpu_noise_us_python": 4.0280000000000005, + "gpu_bwutil_cpp": 0.38354673297651914, + "gpu_bwutil_python": 0.3814266508600014, + "gpu_gap_stddev_us": 4.9444090785192705 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2159.34965399542, + "gpu_time_us_python": 2178.55294542818, + "gpu_noise_us_cpp": 2.356, + "gpu_noise_us_python": 6.354000000000001, + "gpu_bwutil_cpp": 0.45220383042552703, + "gpu_bwutil_python": 0.44822054435012654, + "gpu_gap_stddev_us": 7.0238848199598385 + } + } + } + }, + "pillowresize_expand_linear_4k_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "12x2160x3840" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize expand_linear_4k RGBA8 mirror (advanced)." + }, + "baselines": { + "pillowresize_expand_linear_4k_uchar4_advanced[InOutDataType=uchar4][layout=NHWC][shape=12x2160x3840][resizeType=EXPAND][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 9244.97298841118, + "gpu_time_us_python": 9266.521687482342, + "gpu_noise_us_cpp": 7.260000000000001, + "gpu_noise_us_python": 12.434000000000001, + "gpu_bwutil_cpp": 0.13845378201989658, + "gpu_bwutil_python": 0.13813186235320019, + "gpu_gap_stddev_us": 3.1735065162214764 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 7827.33521217644, + "gpu_time_us_python": 7848.7199982006805, + "gpu_noise_us_cpp": 2.472, + "gpu_noise_us_python": 7.57, + "gpu_bwutil_cpp": 0.12476376966185358, + "gpu_bwutil_python": 0.1244239626232659, + "gpu_gap_stddev_us": 2.7555885890339264 + } + } + } + }, + "pillowresize_expand_linear_4k_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "4x2160x3840" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize expand_linear_4k RGBAf32 mirror (advanced)." + }, + "baselines": { + "pillowresize_expand_linear_4k_float4_advanced[InOutDataType=float4][layout=NHWC][shape=4x2160x3840][resizeType=EXPAND][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3942.4287773668198, + "gpu_time_us_python": 3958.45763310788, + "gpu_noise_us_cpp": 2.868, + "gpu_noise_us_python": 3.0319999999999996, + "gpu_bwutil_cpp": 0.4328975895120396, + "gpu_bwutil_python": 0.43114469436640956, + "gpu_gap_stddev_us": 0.7828844884390929 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2809.41775447382, + "gpu_time_us_python": 2826.29181423058, + "gpu_noise_us_cpp": 4.8420000000000005, + "gpu_noise_us_python": 3.9560000000000004, + "gpu_bwutil_cpp": 0.4633973780022987, + "gpu_bwutil_python": 0.46063304961292995, + "gpu_gap_stddev_us": 1.6000204263545592 + } + } + } + }, + "pillowresize_expand_linear_4k_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "8x2160x3840" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize expand_linear_4k single-channel (advanced)." + }, + "baselines": { + "pillowresize_expand_linear_4k_scalar_advanced[InOutDataType=float32][layout=NHWC][shape=8x2160x3840][resizeType=EXPAND][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4331.110368925901, + "gpu_time_us_python": 4351.63279730694, + "gpu_noise_us_cpp": 3.556, + "gpu_noise_us_python": 5.656000000000001, + "gpu_bwutil_cpp": 0.19702437648123863, + "gpu_bwutil_python": 0.19609530579366197, + "gpu_gap_stddev_us": 1.6159284011134485 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3613.6048827901795, + "gpu_time_us_python": 3632.82583626528, + "gpu_noise_us_cpp": 2.492, + "gpu_noise_us_python": 4.152, + "gpu_bwutil_cpp": 0.1801469384713402, + "gpu_bwutil_python": 0.17919659961761444, + "gpu_gap_stddev_us": 4.711904105171796 + } + }, + "pillowresize_expand_linear_4k_scalar_advanced[InOutDataType=uint8][layout=NHWC][shape=8x2160x3840][resizeType=EXPAND][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2646.4902752323806, + "gpu_time_us_python": 2658.40412627938, + "gpu_noise_us_cpp": 2.746, + "gpu_noise_us_python": 4.912, + "gpu_bwutil_cpp": 0.08061004241075305, + "gpu_bwutil_python": 0.08024880751064276, + "gpu_gap_stddev_us": 0.6057402678270513 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2074.05232437398, + "gpu_time_us_python": 2090.3249099276, + "gpu_noise_us_cpp": 1.8679999999999999, + "gpu_noise_us_python": 2.8579999999999997, + "gpu_bwutil_cpp": 0.0784813710733822, + "gpu_bwutil_python": 0.07787021821960752, + "gpu_gap_stddev_us": 0.6635327504246853 + } + } + } + }, + "pillowresize_expand_cubic_720p_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "96x720x1280" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize expand_cubic_720p RGB8 (advanced)." + }, + "baselines": { + "pillowresize_expand_cubic_720p_uchar3_advanced[InOutDataType=uchar3][layout=NHWC][shape=96x720x1280][resizeType=EXPAND][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6642.353878523119, + "gpu_time_us_python": 6660.06362312714, + "gpu_noise_us_cpp": 4.734, + "gpu_noise_us_python": 6.333999999999999, + "gpu_bwutil_cpp": 0.12846872646048801, + "gpu_bwutil_python": 0.12812705821656442, + "gpu_gap_stddev_us": 2.377342653818866 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5433.44392874428, + "gpu_time_us_python": 5452.93250518726, + "gpu_noise_us_cpp": 1.5939999999999999, + "gpu_noise_us_python": 4.601999999999999, + "gpu_bwutil_cpp": 0.11985839517703148, + "gpu_bwutil_python": 0.11943046261487703, + "gpu_gap_stddev_us": 5.53584052195274 + } + } + } + }, + "pillowresize_expand_cubic_720p_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "32x720x1280" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize expand_cubic_720p RGBf32 (advanced)." + }, + "baselines": { + "pillowresize_expand_cubic_720p_float3_advanced[InOutDataType=float3][layout=NHWC][shape=32x720x1280][resizeType=EXPAND][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2846.83634422038, + "gpu_time_us_python": 2868.21000196714, + "gpu_noise_us_cpp": 2.7380000000000004, + "gpu_noise_us_python": 3.8280000000000003, + "gpu_bwutil_cpp": 0.39966408724717023, + "gpu_bwutil_python": 0.3966862266507677, + "gpu_gap_stddev_us": 2.896321553516965 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2429.5024919777397, + "gpu_time_us_python": 2451.66455454482, + "gpu_noise_us_cpp": 3.63, + "gpu_noise_us_python": 5.147999999999999, + "gpu_bwutil_cpp": 0.35722937917329683, + "gpu_bwutil_python": 0.3540058352978936, + "gpu_gap_stddev_us": 4.109918443206143 + } + } + } + }, + "pillowresize_expand_cubic_720p_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "96x720x1280" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize expand_cubic_720p RGBA8 mirror (advanced)." + }, + "baselines": { + "pillowresize_expand_cubic_720p_uchar4_advanced[InOutDataType=uchar4][layout=NHWC][shape=96x720x1280][resizeType=EXPAND][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 8734.72009198414, + "gpu_time_us_python": 8755.1082150689, + "gpu_noise_us_cpp": 4.728, + "gpu_noise_us_python": 7.339999999999999, + "gpu_bwutil_cpp": 0.13025939874519918, + "gpu_bwutil_python": 0.12995605759784384, + "gpu_gap_stddev_us": 7.516572904598374 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 7674.155175282099, + "gpu_time_us_python": 7694.63833601286, + "gpu_noise_us_cpp": 1.53, + "gpu_noise_us_python": 5.7940000000000005, + "gpu_bwutil_cpp": 0.11314079976247507, + "gpu_bwutil_python": 0.11284016217129604, + "gpu_gap_stddev_us": 4.195658820551638 + } + } + } + }, + "pillowresize_expand_cubic_720p_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "32x720x1280" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize expand_cubic_720p RGBAf32 mirror (advanced)." + }, + "baselines": { + "pillowresize_expand_cubic_720p_float4_advanced[InOutDataType=float4][layout=NHWC][shape=32x720x1280][resizeType=EXPAND][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4365.040085233441, + "gpu_time_us_python": 4383.251838689799, + "gpu_noise_us_cpp": 2.632, + "gpu_noise_us_python": 4.116, + "gpu_bwutil_cpp": 0.3475427695979169, + "gpu_bwutil_python": 0.34609882540066383, + "gpu_gap_stddev_us": 2.3899543447392917 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3429.14407018462, + "gpu_time_us_python": 3447.3793753426203, + "gpu_noise_us_cpp": 2.7520000000000002, + "gpu_noise_us_python": 3.782, + "gpu_bwutil_cpp": 0.3374403492610923, + "gpu_bwutil_python": 0.33565843409119944, + "gpu_gap_stddev_us": 2.6371456013166776 + } + } + } + }, + "pillowresize_expand_cubic_720p_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "64x720x1280" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize expand_cubic_720p single-channel (advanced)." + }, + "baselines": { + "pillowresize_expand_cubic_720p_scalar_advanced[InOutDataType=float32][layout=NHWC][shape=64x720x1280][resizeType=EXPAND][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2685.29350014138, + "gpu_time_us_python": 2704.3069475440197, + "gpu_noise_us_cpp": 2.2119999999999997, + "gpu_noise_us_python": 5.336, + "gpu_bwutil_cpp": 0.2824716043487475, + "gpu_bwutil_python": 0.2804858796832876, + "gpu_gap_stddev_us": 2.0025580943964645 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2287.0655292721603, + "gpu_time_us_python": 2302.40315474768, + "gpu_noise_us_cpp": 4.638, + "gpu_noise_us_python": 5.266, + "gpu_bwutil_cpp": 0.25301470372104956, + "gpu_bwutil_python": 0.25132896494207835, + "gpu_gap_stddev_us": 2.1519016398369524 + } + }, + "pillowresize_expand_cubic_720p_scalar_advanced[InOutDataType=uint8][layout=NHWC][shape=64x720x1280][resizeType=EXPAND][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2229.66863343066, + "gpu_time_us_python": 2246.66143911222, + "gpu_noise_us_cpp": 2.398, + "gpu_noise_us_python": 6.668000000000001, + "gpu_bwutil_cpp": 0.08504858992492699, + "gpu_bwutil_python": 0.08440530339318139, + "gpu_gap_stddev_us": 0.8225580632922506 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2004.9476563830801, + "gpu_time_us_python": 2021.24089274962, + "gpu_noise_us_cpp": 1.1960000000000002, + "gpu_noise_us_python": 3.492, + "gpu_bwutil_cpp": 0.07218020697078223, + "gpu_bwutil_python": 0.0715994651175614, + "gpu_gap_stddev_us": 1.7991714556015754 + } + } + } + }, + "pillowresize_expand_cubic_1080p_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "48x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize expand_cubic_1080p RGB8 (basic)." + }, + "baselines": { + "pillowresize_expand_cubic_1080p_uchar3_basic[InOutDataType=uchar3][layout=NHWC][shape=48x1080x1920][resizeType=EXPAND][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 7840.20477831358, + "gpu_time_us_python": 7862.150442600219, + "gpu_noise_us_cpp": 6.8260000000000005, + "gpu_noise_us_python": 6.660000000000001, + "gpu_bwutil_cpp": 0.1224459142814766, + "gpu_bwutil_python": 0.12210412150619554, + "gpu_gap_stddev_us": 5.334868230301711 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6112.09598848352, + "gpu_time_us_python": 6129.83484702736, + "gpu_noise_us_cpp": 1.8399999999999999, + "gpu_noise_us_python": 4.032, + "gpu_bwutil_cpp": 0.11986126442315433, + "gpu_bwutil_python": 0.11951539018230499, + "gpu_gap_stddev_us": 2.397730187804219 + } + } + } + }, + "pillowresize_expand_cubic_1080p_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize expand_cubic_1080p RGBf32 (basic)." + }, + "baselines": { + "pillowresize_expand_cubic_1080p_float3_basic[InOutDataType=float3][layout=NHWC][shape=16x1080x1920][resizeType=EXPAND][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3287.4794156275398, + "gpu_time_us_python": 3308.69754866546, + "gpu_noise_us_cpp": 3.098, + "gpu_noise_us_python": 4.6819999999999995, + "gpu_bwutil_cpp": 0.3893561958085753, + "gpu_bwutil_python": 0.3868594078552181, + "gpu_gap_stddev_us": 3.0945497926890355 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2775.6651385306805, + "gpu_time_us_python": 2782.66843195654, + "gpu_noise_us_cpp": 3.058, + "gpu_noise_us_python": 4.942, + "gpu_bwutil_cpp": 0.35175916475297214, + "gpu_bwutil_python": 0.3508764478181711, + "gpu_gap_stddev_us": 3.6348554602217975 + } + } + } + }, + "pillowresize_expand_cubic_1080p_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "48x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize expand_cubic_1080p RGBA8 mirror (advanced)." + }, + "baselines": { + "pillowresize_expand_cubic_1080p_uchar4_advanced[InOutDataType=uchar4][layout=NHWC][shape=48x1080x1920][resizeType=EXPAND][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 10075.922416686939, + "gpu_time_us_python": 10095.935447692822, + "gpu_noise_us_cpp": 6.322, + "gpu_noise_us_python": 8.34, + "gpu_bwutil_cpp": 0.12703566933861427, + "gpu_bwutil_python": 0.12678389410674692, + "gpu_gap_stddev_us": 7.344257997513807 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 8732.81059681682, + "gpu_time_us_python": 8753.2115140454, + "gpu_noise_us_cpp": 1.6600000000000001, + "gpu_noise_us_python": 4.56, + "gpu_bwutil_cpp": 0.11184104010771467, + "gpu_bwutil_python": 0.11158090252016098, + "gpu_gap_stddev_us": 4.187806839092271 + } + } + } + }, + "pillowresize_expand_cubic_1080p_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize expand_cubic_1080p RGBAf32 mirror (advanced)." + }, + "baselines": { + "pillowresize_expand_cubic_1080p_float4_advanced[InOutDataType=float4][layout=NHWC][shape=16x1080x1920][resizeType=EXPAND][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4899.4868421626, + "gpu_time_us_python": 4918.81008522178, + "gpu_noise_us_cpp": 1.6179999999999999, + "gpu_noise_us_python": 3.5379999999999994, + "gpu_bwutil_cpp": 0.34833607637401737, + "gpu_bwutil_python": 0.34696761495086664, + "gpu_gap_stddev_us": 3.6452887415505293 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3872.02554972848, + "gpu_time_us_python": 3888.70973845354, + "gpu_noise_us_cpp": 3.09, + "gpu_noise_us_python": 7.918000000000001, + "gpu_bwutil_cpp": 0.33620112124414825, + "gpu_bwutil_python": 0.33475816247165263, + "gpu_gap_stddev_us": 6.649400878842527 + } + } + } + }, + "pillowresize_expand_cubic_1080p_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize expand_cubic_1080p single-channel (advanced)." + }, + "baselines": { + "pillowresize_expand_cubic_1080p_scalar_advanced[InOutDataType=float32][layout=NHWC][shape=32x1080x1920][resizeType=EXPAND][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3189.24407717542, + "gpu_time_us_python": 3203.67983920504, + "gpu_noise_us_cpp": 2.17, + "gpu_noise_us_python": 3.468, + "gpu_bwutil_cpp": 0.26756633204350416, + "gpu_bwutil_python": 0.2663606072134332, + "gpu_gap_stddev_us": 0.9421443900216832 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2743.3283440298396, + "gpu_time_us_python": 2759.5677302786403, + "gpu_noise_us_cpp": 2.8560000000000003, + "gpu_noise_us_python": 7.63, + "gpu_bwutil_cpp": 0.23730179926183537, + "gpu_bwutil_python": 0.23590328205395972, + "gpu_gap_stddev_us": 4.0309945558045035 + } + }, + "pillowresize_expand_cubic_1080p_scalar_advanced[InOutDataType=uint8][layout=NHWC][shape=32x1080x1920][resizeType=EXPAND][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2629.48214320234, + "gpu_time_us_python": 2642.90302828736, + "gpu_noise_us_cpp": 4.838, + "gpu_noise_us_python": 3.5780000000000003, + "gpu_bwutil_cpp": 0.08113148597133073, + "gpu_bwutil_python": 0.08071953844047466, + "gpu_gap_stddev_us": 2.48641511915234 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2263.5113430868996, + "gpu_time_us_python": 2280.26735283138, + "gpu_noise_us_cpp": 1.4240000000000002, + "gpu_noise_us_python": 3.6160000000000005, + "gpu_bwutil_cpp": 0.0719256565001919, + "gpu_bwutil_python": 0.07139879863901182, + "gpu_gap_stddev_us": 3.1471989199959878 + } + } + } + }, + "pillowresize_expand_cubic_4k_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "12x2160x3840" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize expand_cubic_4k RGB8 (advanced)." + }, + "baselines": { + "pillowresize_expand_cubic_4k_uchar3_advanced[InOutDataType=uchar3][layout=NHWC][shape=12x2160x3840][resizeType=EXPAND][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 7749.1789413220395, + "gpu_time_us_python": 7772.89234508162, + "gpu_noise_us_cpp": 5.296, + "gpu_noise_us_python": 4.6979999999999995, + "gpu_bwutil_cpp": 0.12388426671306998, + "gpu_bwutil_python": 0.1235063566865359, + "gpu_gap_stddev_us": 5.444951606838614 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6135.91107792792, + "gpu_time_us_python": 6154.204416115399, + "gpu_noise_us_cpp": 2.7399999999999998, + "gpu_noise_us_python": 4.45, + "gpu_bwutil_cpp": 0.11939406957526622, + "gpu_bwutil_python": 0.11904017360089962, + "gpu_gap_stddev_us": 3.8150329798027136 + } + } + } + }, + "pillowresize_expand_cubic_4k_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "4x2160x3840" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize expand_cubic_4k RGBf32 (advanced)." + }, + "baselines": { + "pillowresize_expand_cubic_4k_float3_advanced[InOutDataType=float3][layout=NHWC][shape=4x2160x3840][resizeType=EXPAND][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3235.3490737767997, + "gpu_time_us_python": 3253.51394925794, + "gpu_noise_us_cpp": 2.308, + "gpu_noise_us_python": 4.41, + "gpu_bwutil_cpp": 0.3956298043314614, + "gpu_bwutil_python": 0.39342107905399026, + "gpu_gap_stddev_us": 1.9798154672251058 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2749.61212022236, + "gpu_time_us_python": 2767.0809596221798, + "gpu_noise_us_cpp": 2.7199999999999998, + "gpu_noise_us_python": 3.872, + "gpu_bwutil_cpp": 0.3550929350646265, + "gpu_bwutil_python": 0.3528537368173702, + "gpu_gap_stddev_us": 2.0210949210803233 + } + } + } + }, + "pillowresize_expand_cubic_4k_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "12x2160x3840" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize expand_cubic_4k RGBA8 mirror (advanced)." + }, + "baselines": { + "pillowresize_expand_cubic_4k_uchar4_advanced[InOutDataType=uchar4][layout=NHWC][shape=12x2160x3840][resizeType=EXPAND][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 9795.583974398021, + "gpu_time_us_python": 9815.40233171898, + "gpu_noise_us_cpp": 5.634, + "gpu_noise_us_python": 8.808, + "gpu_bwutil_cpp": 0.13067133826320917, + "gpu_bwutil_python": 0.13040744721342568, + "gpu_gap_stddev_us": 4.3335602463195775 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 8768.314196707139, + "gpu_time_us_python": 8792.230158180999, + "gpu_noise_us_cpp": 1.6020000000000003, + "gpu_noise_us_python": 5.564, + "gpu_bwutil_cpp": 0.11138676728604349, + "gpu_bwutil_python": 0.11108343121628454, + "gpu_gap_stddev_us": 5.18736155825237 + } + } + } + }, + "pillowresize_expand_cubic_4k_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "4x2160x3840" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize expand_cubic_4k RGBAf32 mirror (advanced)." + }, + "baselines": { + "pillowresize_expand_cubic_4k_float4_advanced[InOutDataType=float4][layout=NHWC][shape=4x2160x3840][resizeType=EXPAND][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4747.56929649492, + "gpu_time_us_python": 4766.46110066822, + "gpu_noise_us_cpp": 2.4080000000000004, + "gpu_noise_us_python": 5.266, + "gpu_bwutil_cpp": 0.359482468198849, + "gpu_bwutil_python": 0.358057857771218, + "gpu_gap_stddev_us": 3.4892066886970987 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3870.68767254166, + "gpu_time_us_python": 3888.3588405015603, + "gpu_noise_us_cpp": 3.7620000000000005, + "gpu_noise_us_python": 6.798, + "gpu_bwutil_cpp": 0.33631882573565025, + "gpu_bwutil_python": 0.3347932020203969, + "gpu_gap_stddev_us": 4.3690851713271135 + } + } + } + }, + "pillowresize_expand_cubic_4k_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "8x2160x3840" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize expand_cubic_4k single-channel (advanced)." + }, + "baselines": { + "pillowresize_expand_cubic_4k_scalar_advanced[InOutDataType=float32][layout=NHWC][shape=8x2160x3840][resizeType=EXPAND][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3267.93778778664, + "gpu_time_us_python": 3288.9603047849596, + "gpu_noise_us_cpp": 2.3440000000000003, + "gpu_noise_us_python": 3.9299999999999997, + "gpu_bwutil_cpp": 0.261123164156444, + "gpu_bwutil_python": 0.2594541281894841, + "gpu_gap_stddev_us": 2.6720505984617495 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2754.23736979942, + "gpu_time_us_python": 2771.53706477872, + "gpu_noise_us_cpp": 2.844, + "gpu_noise_us_python": 4.6240000000000006, + "gpu_bwutil_cpp": 0.2363575634500223, + "gpu_bwutil_python": 0.23488070308337256, + "gpu_gap_stddev_us": 1.627280051465546 + } + }, + "pillowresize_expand_cubic_4k_scalar_advanced[InOutDataType=uint8][layout=NHWC][shape=8x2160x3840][resizeType=EXPAND][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2716.94252558984, + "gpu_time_us_python": 2731.35753351704, + "gpu_noise_us_cpp": 2.4160000000000004, + "gpu_noise_us_python": 4.994, + "gpu_bwutil_cpp": 0.07851979430983028, + "gpu_bwutil_python": 0.07810555865201771, + "gpu_gap_stddev_us": 5.278462917856952 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2283.67305914422, + "gpu_time_us_python": 2301.15303845172, + "gpu_noise_us_cpp": 1.3960000000000001, + "gpu_noise_us_python": 3.97, + "gpu_bwutil_cpp": 0.07128728836007411, + "gpu_bwutil_python": 0.07074715545346169, + "gpu_gap_stddev_us": 2.1316022988957286 + } + } + } + }, + "pillowresize_planar_nchw_contract_cubic_1080p_varshape_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "96x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGB8 contract_cubic_1080p_varshape planar (NCHW) (basic)." + }, + "baselines": { + "pillowresize_planar_nchw_contract_cubic_1080p_varshape_uchar3_basic[InOutDataType=uchar3][layout=NCHW][shape=96x1080x1920][resizeType=CONTRACT][interpolation=CUBIC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5988.707996550021, + "gpu_time_us_python": 6017.989496957641, + "gpu_noise_us_cpp": 13.696000000000002, + "gpu_noise_us_python": 7.091999999999999, + "gpu_bwutil_cpp": 0.08015128039613484, + "gpu_bwutil_python": 0.079761402382517, + "gpu_gap_stddev_us": 6.1052427636284605 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4405.0084634841005, + "gpu_time_us_python": 4442.9429426547595, + "gpu_noise_us_cpp": 5.715999999999999, + "gpu_noise_us_python": 7.4799999999999995, + "gpu_bwutil_cpp": 0.08314343743705077, + "gpu_bwutil_python": 0.08243559963243492, + "gpu_gap_stddev_us": 8.268619564670237 + } + } + } + }, + "pillowresize_planar_nchw_contract_cubic_1080p_varshape_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBf32 contract_cubic_1080p_varshape planar (NCHW) (advanced)." + }, + "baselines": { + "pillowresize_planar_nchw_contract_cubic_1080p_varshape_float3_advanced[InOutDataType=float3][layout=NCHW][shape=32x1080x1920][resizeType=CONTRACT][interpolation=CUBIC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2079.17702265016, + "gpu_time_us_python": 2103.7203474855996, + "gpu_noise_us_cpp": 2.91, + "gpu_noise_us_python": 6.965999999999999, + "gpu_bwutil_cpp": 0.30781684070025767, + "gpu_bwutil_python": 0.30422764763399923, + "gpu_gap_stddev_us": 2.462455091642492 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1581.30244577252, + "gpu_time_us_python": 1605.9537223902603, + "gpu_noise_us_cpp": 3.1719999999999997, + "gpu_noise_us_python": 5.402000000000001, + "gpu_bwutil_cpp": 0.3087990886678281, + "gpu_bwutil_python": 0.3040734295772975, + "gpu_gap_stddev_us": 5.667310434654387 + } + } + } + }, + "pillowresize_planar_nchw_contract_linear_720p_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "192x720x1280" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGB8 contract_linear_720p planar (NCHW) (advanced)." + }, + "baselines": { + "pillowresize_planar_nchw_contract_linear_720p_uchar3_advanced[InOutDataType=uchar3][layout=NCHW][shape=192x720x1280][resizeType=CONTRACT][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2284.38014984126, + "gpu_time_us_python": 2300.45828425554, + "gpu_noise_us_cpp": 1.206, + "gpu_noise_us_python": 3.604, + "gpu_bwutil_cpp": 0.1867760808598918, + "gpu_bwutil_python": 0.1854707361285163, + "gpu_gap_stddev_us": 2.9324121654522832 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1837.1746228309398, + "gpu_time_us_python": 1855.3128216764803, + "gpu_noise_us_cpp": 2.118, + "gpu_noise_us_python": 3.316, + "gpu_bwutil_cpp": 0.17720521110144102, + "gpu_bwutil_python": 0.17547321725091253, + "gpu_gap_stddev_us": 3.5214301287170224 + } + } + } + }, + "pillowresize_planar_nchw_contract_linear_720p_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "64x720x1280" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBf32 contract_linear_720p planar (NCHW) (advanced)." + }, + "baselines": { + "pillowresize_planar_nchw_contract_linear_720p_float3_advanced[InOutDataType=float3][layout=NCHW][shape=64x720x1280][resizeType=CONTRACT][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1222.95671963106, + "gpu_time_us_python": 1239.0967222452998, + "gpu_noise_us_cpp": 1.53, + "gpu_noise_us_python": 3.914, + "gpu_bwutil_cpp": 0.465175347436225, + "gpu_bwutil_python": 0.45912108475504965, + "gpu_gap_stddev_us": 3.821339471604442 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 740.0589804275999, + "gpu_time_us_python": 755.99856542338, + "gpu_noise_us_cpp": 2.922, + "gpu_noise_us_python": 4.578, + "gpu_bwutil_cpp": 0.5863944972413708, + "gpu_bwutil_python": 0.5740402063781248, + "gpu_gap_stddev_us": 2.2144468229462664 + } + } + } + }, + "pillowresize_planar_nchw_contract_linear_1080p_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "96x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGB8 contract_linear_1080p planar (NCHW) (basic)." + }, + "baselines": { + "pillowresize_planar_nchw_contract_linear_1080p_uchar3_basic[InOutDataType=uchar3][layout=NCHW][shape=96x1080x1920][resizeType=CONTRACT][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2666.1278293487203, + "gpu_time_us_python": 2682.45512120264, + "gpu_noise_us_cpp": 3.0100000000000002, + "gpu_noise_us_python": 3.6879999999999997, + "gpu_bwutil_cpp": 0.18003674022833932, + "gpu_bwutil_python": 0.1789409380080662, + "gpu_gap_stddev_us": 2.4603233696471443 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2107.67549226368, + "gpu_time_us_python": 2129.68646020288, + "gpu_noise_us_cpp": 5.0840000000000005, + "gpu_noise_us_python": 6.18, + "gpu_bwutil_cpp": 0.17376146717899693, + "gpu_bwutil_python": 0.1719763557302652, + "gpu_gap_stddev_us": 6.346156045411075 + } + } + } + }, + "pillowresize_planar_nchw_contract_linear_1080p_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBf32 contract_linear_1080p planar (NCHW) (basic)." + }, + "baselines": { + "pillowresize_planar_nchw_contract_linear_1080p_float3_basic[InOutDataType=float3][layout=NCHW][shape=32x1080x1920][resizeType=CONTRACT][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1378.1824740734198, + "gpu_time_us_python": 1395.15391399588, + "gpu_noise_us_cpp": 1.0879999999999999, + "gpu_noise_us_python": 4.384, + "gpu_bwutil_cpp": 0.4643799773815599, + "gpu_bwutil_python": 0.458732507504211, + "gpu_gap_stddev_us": 2.7060394602431965 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 833.97097007558, + "gpu_time_us_python": 849.9899916052002, + "gpu_noise_us_cpp": 2.0919999999999996, + "gpu_noise_us_python": 3.086, + "gpu_bwutil_cpp": 0.5854092998633134, + "gpu_bwutil_python": 0.5743925777851059, + "gpu_gap_stddev_us": 1.9733242917626246 + } + } + } + }, + "pillowresize_planar_nchw_contract_linear_4k_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "24x2160x3840" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGB8 contract_linear_4k planar (NCHW) (advanced)." + }, + "baselines": { + "pillowresize_planar_nchw_contract_linear_4k_uchar3_advanced[InOutDataType=uchar3][layout=NCHW][shape=24x2160x3840][resizeType=CONTRACT][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2704.4214643457394, + "gpu_time_us_python": 2723.0266278789395, + "gpu_noise_us_cpp": 3.442, + "gpu_noise_us_python": 4.656000000000001, + "gpu_bwutil_cpp": 0.1774873920905909, + "gpu_bwutil_python": 0.17627498814325523, + "gpu_gap_stddev_us": 2.1185152764173063 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2096.1216946471004, + "gpu_time_us_python": 2113.87392994432, + "gpu_noise_us_cpp": 1.95, + "gpu_noise_us_python": 5.2219999999999995, + "gpu_bwutil_cpp": 0.1747192559971778, + "gpu_bwutil_python": 0.17325741358127486, + "gpu_gap_stddev_us": 2.4189186954280046 + } + } + } + }, + "pillowresize_planar_nchw_contract_linear_4k_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "8x2160x3840" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBf32 contract_linear_4k planar (NCHW) (advanced)." + }, + "baselines": { + "pillowresize_planar_nchw_contract_linear_4k_float3_advanced[InOutDataType=float3][layout=NCHW][shape=8x2160x3840][resizeType=CONTRACT][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1383.16266022988, + "gpu_time_us_python": 1398.1480893993398, + "gpu_noise_us_cpp": 1.342, + "gpu_noise_us_python": 3.0180000000000002, + "gpu_bwutil_cpp": 0.46270820000415036, + "gpu_bwutil_python": 0.4577494712445766, + "gpu_gap_stddev_us": 1.2956298769315993 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 827.8118343577, + "gpu_time_us_python": 843.3101414412798, + "gpu_noise_us_cpp": 2.046, + "gpu_noise_us_python": 2.96, + "gpu_bwutil_cpp": 0.589765511229302, + "gpu_bwutil_python": 0.5789394301648163, + "gpu_gap_stddev_us": 1.6363450067504135 + } + } + } + }, + "pillowresize_planar_nchw_contract_cubic_720p_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "192x720x1280" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGB8 contract_cubic_720p planar (NCHW) (advanced)." + }, + "baselines": { + "pillowresize_planar_nchw_contract_cubic_720p_uchar3_advanced[InOutDataType=uchar3][layout=NCHW][shape=192x720x1280][resizeType=CONTRACT][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3131.73248469824, + "gpu_time_us_python": 3148.1702405213796, + "gpu_noise_us_cpp": 1.188, + "gpu_noise_us_python": 4.839999999999999, + "gpu_bwutil_cpp": 0.13624014589914052, + "gpu_bwutil_python": 0.13552887542967268, + "gpu_gap_stddev_us": 5.218707674851256 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2419.311924463, + "gpu_time_us_python": 2437.39685262188, + "gpu_noise_us_cpp": 1.81, + "gpu_noise_us_python": 5.91, + "gpu_bwutil_cpp": 0.13457503656828843, + "gpu_bwutil_python": 0.13357823295684984, + "gpu_gap_stddev_us": 3.3471636055767022 + } + } + } + }, + "pillowresize_planar_nchw_contract_cubic_720p_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "64x720x1280" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBf32 contract_cubic_720p planar (NCHW) (advanced)." + }, + "baselines": { + "pillowresize_planar_nchw_contract_cubic_720p_float3_advanced[InOutDataType=float3][layout=NCHW][shape=64x720x1280][resizeType=CONTRACT][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1689.3639091568598, + "gpu_time_us_python": 1706.61095070618, + "gpu_noise_us_cpp": 2.432, + "gpu_noise_us_python": 3.5159999999999996, + "gpu_bwutil_cpp": 0.33674757117895926, + "gpu_bwutil_python": 0.33334552148591545, + "gpu_gap_stddev_us": 3.0535124845106885 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1213.4375689316598, + "gpu_time_us_python": 1229.7767801786, + "gpu_noise_us_cpp": 2.048, + "gpu_noise_us_python": 4.182, + "gpu_bwutil_cpp": 0.35764478177291026, + "gpu_bwutil_python": 0.35290049457342487, + "gpu_gap_stddev_us": 2.282817290291329 + } + } + } + }, + "pillowresize_planar_nchw_contract_cubic_1080p_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "96x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGB8 contract_cubic_1080p planar (NCHW) (basic)." + }, + "baselines": { + "pillowresize_planar_nchw_contract_cubic_1080p_uchar3_basic[InOutDataType=uchar3][layout=NCHW][shape=96x1080x1920][resizeType=CONTRACT][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3645.6952585689805, + "gpu_time_us_python": 3660.2004175600205, + "gpu_noise_us_cpp": 1.766, + "gpu_noise_us_python": 4.81, + "gpu_bwutil_cpp": 0.13166245827939044, + "gpu_bwutil_python": 0.13114056093833415, + "gpu_gap_stddev_us": 3.283278964122429 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2785.94142967558, + "gpu_time_us_python": 2804.1910579651403, + "gpu_noise_us_cpp": 1.4959999999999998, + "gpu_noise_us_python": 3.682, + "gpu_bwutil_cpp": 0.13147077599308227, + "gpu_bwutil_python": 0.1306138449507694, + "gpu_gap_stddev_us": 3.7424494975038103 + } + } + } + }, + "pillowresize_planar_nchw_contract_cubic_1080p_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBf32 contract_cubic_1080p planar (NCHW) (basic)." + }, + "baselines": { + "pillowresize_planar_nchw_contract_cubic_1080p_float3_basic[InOutDataType=float3][layout=NCHW][shape=32x1080x1920][resizeType=CONTRACT][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1943.9457416534, + "gpu_time_us_python": 1960.57441178706, + "gpu_noise_us_cpp": 2.43, + "gpu_noise_us_python": 4.214, + "gpu_bwutil_cpp": 0.32922751880589607, + "gpu_bwutil_python": 0.3264354862184916, + "gpu_gap_stddev_us": 1.8881822779478168 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1382.7959459327, + "gpu_time_us_python": 1400.8713773600002, + "gpu_noise_us_cpp": 3.054, + "gpu_noise_us_python": 3.28, + "gpu_bwutil_cpp": 0.35305834109291573, + "gpu_bwutil_python": 0.3485170635982067, + "gpu_gap_stddev_us": 3.9308749531006706 + } + } + } + }, + "pillowresize_planar_nchw_contract_cubic_4k_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "24x2160x3840" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGB8 contract_cubic_4k planar (NCHW) (advanced)." + }, + "baselines": { + "pillowresize_planar_nchw_contract_cubic_4k_uchar3_advanced[InOutDataType=uchar3][layout=NCHW][shape=24x2160x3840][resizeType=CONTRACT][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3677.6503733356403, + "gpu_time_us_python": 3696.4864071676993, + "gpu_noise_us_cpp": 3.164, + "gpu_noise_us_python": 5.1240000000000006, + "gpu_bwutil_cpp": 0.13051828288271491, + "gpu_bwutil_python": 0.12985325537293096, + "gpu_gap_stddev_us": 3.158934190654804 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2768.5932624060597, + "gpu_time_us_python": 2785.7000950966003, + "gpu_noise_us_cpp": 1.1919999999999997, + "gpu_noise_us_python": 3.088, + "gpu_bwutil_cpp": 0.13229029656902805, + "gpu_bwutil_python": 0.1314777924192753, + "gpu_gap_stddev_us": 2.4354038075867024 + } + } + } + }, + "pillowresize_planar_nchw_contract_cubic_4k_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "8x2160x3840" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBf32 contract_cubic_4k planar (NCHW) (advanced)." + }, + "baselines": { + "pillowresize_planar_nchw_contract_cubic_4k_float3_advanced[InOutDataType=float3][layout=NCHW][shape=8x2160x3840][resizeType=CONTRACT][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1957.35520217562, + "gpu_time_us_python": 1975.18312180132, + "gpu_noise_us_cpp": 1.784, + "gpu_noise_us_python": 6.0200000000000005, + "gpu_bwutil_cpp": 0.32697212201832615, + "gpu_bwutil_python": 0.32402170200528435, + "gpu_gap_stddev_us": 3.213061197620279 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1382.3811364695798, + "gpu_time_us_python": 1399.5195366731, + "gpu_noise_us_cpp": 2.4280000000000004, + "gpu_noise_us_python": 3.19, + "gpu_bwutil_cpp": 0.3531667012376788, + "gpu_bwutil_python": 0.3488445314268326, + "gpu_gap_stddev_us": 1.0188172451869453 + } + } + } + }, + "pillowresize_planar_nchw_expand_linear_720p_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "96x720x1280" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGB8 expand_linear_720p planar (NCHW) (advanced)." + }, + "baselines": { + "pillowresize_planar_nchw_expand_linear_720p_uchar3_advanced[InOutDataType=uchar3][layout=NCHW][shape=96x720x1280][resizeType=EXPAND][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 7183.7812941414395, + "gpu_time_us_python": 7205.33796855376, + "gpu_noise_us_cpp": 4.808, + "gpu_noise_us_python": 10.17, + "gpu_bwutil_cpp": 0.11878625444749691, + "gpu_bwutil_python": 0.11843087521457007, + "gpu_gap_stddev_us": 5.763290678827399 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5454.33991581718, + "gpu_time_us_python": 5475.62365076964, + "gpu_noise_us_cpp": 2.2079999999999997, + "gpu_noise_us_python": 5.826, + "gpu_bwutil_cpp": 0.11936841031194438, + "gpu_bwutil_python": 0.11890548400718057, + "gpu_gap_stddev_us": 3.8133141508304087 + } + } + } + }, + "pillowresize_planar_nchw_expand_linear_720p_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "32x720x1280" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBf32 expand_linear_720p planar (NCHW) (advanced)." + }, + "baselines": { + "pillowresize_planar_nchw_expand_linear_720p_float3_advanced[InOutDataType=float3][layout=NCHW][shape=32x720x1280][resizeType=EXPAND][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3467.48837706158, + "gpu_time_us_python": 3483.4432476096395, + "gpu_noise_us_cpp": 1.552, + "gpu_noise_us_python": 4.404, + "gpu_bwutil_cpp": 0.3281275700828342, + "gpu_bwutil_python": 0.326624774836034, + "gpu_gap_stddev_us": 1.1966497197431074 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2848.1508995962204, + "gpu_time_us_python": 2867.3077991813598, + "gpu_noise_us_cpp": 1.7559999999999998, + "gpu_noise_us_python": 5.880000000000001, + "gpu_bwutil_cpp": 0.3047171665298807, + "gpu_bwutil_python": 0.30268144865880886, + "gpu_gap_stddev_us": 0.8769873492020378 + } + } + } + }, + "pillowresize_planar_nchw_expand_linear_1080p_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "48x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGB8 expand_linear_1080p planar (NCHW) (basic)." + }, + "baselines": { + "pillowresize_planar_nchw_expand_linear_1080p_uchar3_basic[InOutDataType=uchar3][layout=NCHW][shape=48x1080x1920][resizeType=EXPAND][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 8237.128688443041, + "gpu_time_us_python": 8258.1041151477, + "gpu_noise_us_cpp": 5.268, + "gpu_noise_us_python": 6.822, + "gpu_bwutil_cpp": 0.11654563174325576, + "gpu_bwutil_python": 0.11624958016772387, + "gpu_gap_stddev_us": 1.9479567572044376 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6156.52494408356, + "gpu_time_us_python": 6172.444120707781, + "gpu_noise_us_cpp": 3.686, + "gpu_noise_us_python": 3.6700000000000004, + "gpu_bwutil_cpp": 0.11897207296177681, + "gpu_bwutil_python": 0.11866499486579458, + "gpu_gap_stddev_us": 4.101797804429632 + } + } + } + }, + "pillowresize_planar_nchw_expand_linear_1080p_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBf32 expand_linear_1080p planar (NCHW) (basic)." + }, + "baselines": { + "pillowresize_planar_nchw_expand_linear_1080p_float3_basic[InOutDataType=float3][layout=NCHW][shape=16x1080x1920][resizeType=EXPAND][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3976.3805419678997, + "gpu_time_us_python": 3995.77802400734, + "gpu_noise_us_cpp": 4.34, + "gpu_noise_us_python": 5.032, + "gpu_bwutil_cpp": 0.3219009857195584, + "gpu_bwutil_python": 0.3203384957087679, + "gpu_gap_stddev_us": 3.7790881372811054 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3222.0774546278, + "gpu_time_us_python": 3238.9164732490804, + "gpu_noise_us_cpp": 2.558, + "gpu_noise_us_python": 4.223999999999999, + "gpu_bwutil_cpp": 0.3030241875854197, + "gpu_bwutil_python": 0.3014492944939122, + "gpu_gap_stddev_us": 3.0678786149033144 + } + } + } + }, + "pillowresize_planar_nchw_expand_linear_4k_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "12x2160x3840" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGB8 expand_linear_4k planar (NCHW) (advanced)." + }, + "baselines": { + "pillowresize_planar_nchw_expand_linear_4k_uchar3_advanced[InOutDataType=uchar3][layout=NCHW][shape=12x2160x3840][resizeType=EXPAND][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 8436.96121215814, + "gpu_time_us_python": 8458.20583979284, + "gpu_noise_us_cpp": 5.345999999999999, + "gpu_noise_us_python": 6.948, + "gpu_bwutil_cpp": 0.11378519063873108, + "gpu_bwutil_python": 0.11349940158110468, + "gpu_gap_stddev_us": 3.47357652700014 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6307.225759669001, + "gpu_time_us_python": 6325.9669387601, + "gpu_noise_us_cpp": 2.624, + "gpu_noise_us_python": 4.589999999999999, + "gpu_bwutil_cpp": 0.11612557789799334, + "gpu_bwutil_python": 0.11578262360096556, + "gpu_gap_stddev_us": 7.617751077322084 + } + } + } + }, + "pillowresize_planar_nchw_expand_linear_4k_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "4x2160x3840" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBf32 expand_linear_4k planar (NCHW) (advanced)." + }, + "baselines": { + "pillowresize_planar_nchw_expand_linear_4k_float3_advanced[InOutDataType=float3][layout=NCHW][shape=4x2160x3840][resizeType=EXPAND][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3993.40169921752, + "gpu_time_us_python": 4011.05349253088, + "gpu_noise_us_cpp": 1.7719999999999998, + "gpu_noise_us_python": 3.4299999999999997, + "gpu_bwutil_cpp": 0.32052890100120524, + "gpu_bwutil_python": 0.31911853537246004, + "gpu_gap_stddev_us": 4.2564812807218475 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3255.74771331188, + "gpu_time_us_python": 3274.43400516214, + "gpu_noise_us_cpp": 1.754, + "gpu_noise_us_python": 5.156, + "gpu_bwutil_cpp": 0.29988977778479975, + "gpu_bwutil_python": 0.2981791247003601, + "gpu_gap_stddev_us": 2.1212103455372135 + } + } + } + }, + "pillowresize_planar_nchw_expand_cubic_720p_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "96x720x1280" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGB8 expand_cubic_720p planar (NCHW) (advanced)." + }, + "baselines": { + "pillowresize_planar_nchw_expand_cubic_720p_uchar3_advanced[InOutDataType=uchar3][layout=NCHW][shape=96x720x1280][resizeType=EXPAND][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6945.33120461768, + "gpu_time_us_python": 6960.878928502361, + "gpu_noise_us_cpp": 5.696000000000001, + "gpu_noise_us_python": 11.190000000000001, + "gpu_bwutil_cpp": 0.12286451890337327, + "gpu_bwutil_python": 0.12259026353668663, + "gpu_gap_stddev_us": 7.908957585828243 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5921.8833885799, + "gpu_time_us_python": 5941.55668338878, + "gpu_noise_us_cpp": 1.746, + "gpu_noise_us_python": 5.75, + "gpu_bwutil_cpp": 0.10995694007764727, + "gpu_bwutil_python": 0.10959330177138973, + "gpu_gap_stddev_us": 5.932168448678811 + } + } + } + }, + "pillowresize_planar_nchw_expand_cubic_720p_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "32x720x1280" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBf32 expand_cubic_720p planar (NCHW) (advanced)." + }, + "baselines": { + "pillowresize_planar_nchw_expand_cubic_720p_float3_advanced[InOutDataType=float3][layout=NCHW][shape=32x720x1280][resizeType=EXPAND][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3557.4336542209594, + "gpu_time_us_python": 3573.25227040874, + "gpu_noise_us_cpp": 4.806, + "gpu_noise_us_python": 3.184, + "gpu_bwutil_cpp": 0.3198312984479522, + "gpu_bwutil_python": 0.31841559455764606, + "gpu_gap_stddev_us": 2.479530671963805 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2836.74545837302, + "gpu_time_us_python": 2854.9611415990403, + "gpu_noise_us_cpp": 5.388, + "gpu_noise_us_python": 3.978, + "gpu_bwutil_cpp": 0.30594955869621737, + "gpu_bwutil_python": 0.3039966865298284, + "gpu_gap_stddev_us": 1.6877873273859794 + } + } + } + }, + "pillowresize_planar_nchw_expand_cubic_1080p_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "48x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGB8 expand_cubic_1080p planar (NCHW) (basic)." + }, + "baselines": { + "pillowresize_planar_nchw_expand_cubic_1080p_uchar3_basic[InOutDataType=uchar3][layout=NCHW][shape=48x1080x1920][resizeType=EXPAND][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 7935.103973746261, + "gpu_time_us_python": 7953.356793522781, + "gpu_noise_us_cpp": 4.474, + "gpu_noise_us_python": 7.383999999999999, + "gpu_bwutil_cpp": 0.12098168105079403, + "gpu_bwutil_python": 0.1207039547140644, + "gpu_gap_stddev_us": 7.516985988902445 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6690.98389735426, + "gpu_time_us_python": 6709.51279732266, + "gpu_noise_us_cpp": 2.4, + "gpu_noise_us_python": 3.332, + "gpu_bwutil_cpp": 0.10947808581194726, + "gpu_bwutil_python": 0.10917470537683782, + "gpu_gap_stddev_us": 3.0211890007643296 + } + } + } + }, + "pillowresize_planar_nchw_expand_cubic_1080p_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBf32 expand_cubic_1080p planar (NCHW) (basic)." + }, + "baselines": { + "pillowresize_planar_nchw_expand_cubic_1080p_float3_basic[InOutDataType=float3][layout=NCHW][shape=16x1080x1920][resizeType=EXPAND][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4121.331443161221, + "gpu_time_us_python": 4140.22666743538, + "gpu_noise_us_cpp": 2.148, + "gpu_noise_us_python": 7.564, + "gpu_bwutil_cpp": 0.31057949988520994, + "gpu_bwutil_python": 0.3091619449163763, + "gpu_gap_stddev_us": 2.6648069500033693 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3198.76532259346, + "gpu_time_us_python": 3213.21004749868, + "gpu_noise_us_cpp": 3.252, + "gpu_noise_us_python": 4.304, + "gpu_bwutil_cpp": 0.30523995965439676, + "gpu_bwutil_python": 0.30386694319745605, + "gpu_gap_stddev_us": 1.844057270884161 + } + } + } + }, + "pillowresize_planar_nchw_expand_cubic_4k_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "12x2160x3840" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGB8 expand_cubic_4k planar (NCHW) (advanced)." + }, + "baselines": { + "pillowresize_planar_nchw_expand_cubic_4k_uchar3_advanced[InOutDataType=uchar3][layout=NCHW][shape=12x2160x3840][resizeType=EXPAND][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 7981.1520427465, + "gpu_time_us_python": 8003.48155498502, + "gpu_noise_us_cpp": 5.07, + "gpu_noise_us_python": 6.332, + "gpu_bwutil_cpp": 0.12028357996585433, + "gpu_bwutil_python": 0.11994799900882396, + "gpu_gap_stddev_us": 4.392619768123354 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6705.35185195576, + "gpu_time_us_python": 6723.74716044016, + "gpu_noise_us_cpp": 2.604, + "gpu_noise_us_python": 2.852, + "gpu_bwutil_cpp": 0.10924257606764856, + "gpu_bwutil_python": 0.10894335399647467, + "gpu_gap_stddev_us": 5.39565055529976 + } + } + } + }, + "pillowresize_planar_nchw_expand_cubic_4k_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "4x2160x3840" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBf32 expand_cubic_4k planar (NCHW) (advanced)." + }, + "baselines": { + "pillowresize_planar_nchw_expand_cubic_4k_float3_advanced[InOutDataType=float3][layout=NCHW][shape=4x2160x3840][resizeType=EXPAND][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4207.397530873541, + "gpu_time_us_python": 4223.50849469498, + "gpu_noise_us_cpp": 2.226, + "gpu_noise_us_python": 3.688, + "gpu_bwutil_cpp": 0.3042262473066494, + "gpu_bwutil_python": 0.30306588508225873, + "gpu_gap_stddev_us": 3.7170919899875767 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3180.12307915136, + "gpu_time_us_python": 3200.6330186445202, + "gpu_noise_us_cpp": 4.624, + "gpu_noise_us_python": 6.642, + "gpu_bwutil_cpp": 0.3070233135481095, + "gpu_bwutil_python": 0.305058100614355, + "gpu_gap_stddev_us": 2.8177677441946405 + } + } + } + }, + "pillowresize_planar_nchw_contract_linear_720p_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "192x720x1280" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBA8 contract_linear_720p planar (NCHW) (advanced)." + }, + "baselines": { + "pillowresize_planar_nchw_contract_linear_720p_uchar4_advanced[InOutDataType=uchar4][layout=NCHW][shape=192x720x1280][resizeType=CONTRACT][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2889.1842820178, + "gpu_time_us_python": 2907.79549993948, + "gpu_noise_us_cpp": 2.28, + "gpu_noise_us_python": 3.882, + "gpu_bwutil_cpp": 0.19690330535512052, + "gpu_bwutil_python": 0.19564328452022486, + "gpu_gap_stddev_us": 4.137030131235286 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2076.3206263727798, + "gpu_time_us_python": 2094.2490576162804, + "gpu_noise_us_cpp": 4.954000000000001, + "gpu_noise_us_python": 3.906, + "gpu_bwutil_cpp": 0.2090402814070666, + "gpu_bwutil_python": 0.20725137149874642, + "gpu_gap_stddev_us": 3.8987971287940355 + } + } + } + }, + "pillowresize_planar_nchw_contract_linear_720p_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "64x720x1280" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBAf32 contract_linear_720p planar (NCHW) (advanced)." + }, + "baselines": { + "pillowresize_planar_nchw_contract_linear_720p_float4_advanced[InOutDataType=float4][layout=NCHW][shape=64x720x1280][resizeType=CONTRACT][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1851.5805762512202, + "gpu_time_us_python": 1868.25758140736, + "gpu_noise_us_cpp": 2.4699999999999998, + "gpu_noise_us_python": 4.786, + "gpu_bwutil_cpp": 0.4096601605484471, + "gpu_bwutil_python": 0.4060051718396391, + "gpu_gap_stddev_us": 3.9515919217734345 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 997.3110611087799, + "gpu_time_us_python": 1012.8304311989199, + "gpu_noise_us_cpp": 2.224, + "gpu_noise_us_python": 3.466, + "gpu_bwutil_cpp": 0.5801394551466059, + "gpu_bwutil_python": 0.5712744215901696, + "gpu_gap_stddev_us": 3.5107777807299234 + } + } + } + }, + "pillowresize_planar_nchw_contract_linear_1080p_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "96x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBA8 contract_linear_1080p planar (NCHW) (advanced)." + }, + "baselines": { + "pillowresize_planar_nchw_contract_linear_1080p_uchar4_advanced[InOutDataType=uchar4][layout=NCHW][shape=96x1080x1920][resizeType=CONTRACT][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3365.90987841284, + "gpu_time_us_python": 3386.7886955673, + "gpu_noise_us_cpp": 2.256, + "gpu_noise_us_python": 5.916, + "gpu_bwutil_cpp": 0.1901419767291067, + "gpu_bwutil_python": 0.1889699466098824, + "gpu_gap_stddev_us": 1.9173402557487649 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2392.3091301701397, + "gpu_time_us_python": 2409.32626663492, + "gpu_noise_us_cpp": 1.268, + "gpu_noise_us_python": 3.632, + "gpu_bwutil_cpp": 0.20410549397204888, + "gpu_bwutil_python": 0.2026654625858976, + "gpu_gap_stddev_us": 1.7320930750838903 + } + } + } + }, + "pillowresize_planar_nchw_contract_linear_1080p_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBAf32 contract_linear_1080p planar (NCHW) (advanced)." + }, + "baselines": { + "pillowresize_planar_nchw_contract_linear_1080p_float4_advanced[InOutDataType=float4][layout=NCHW][shape=32x1080x1920][resizeType=CONTRACT][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2058.09892591878, + "gpu_time_us_python": 2076.2370755849597, + "gpu_noise_us_cpp": 1.672, + "gpu_noise_us_python": 3.17, + "gpu_bwutil_cpp": 0.41462232732395526, + "gpu_bwutil_python": 0.41100037197650285, + "gpu_gap_stddev_us": 1.4384994921914507 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1124.02793048202, + "gpu_time_us_python": 1142.2402208355002, + "gpu_noise_us_cpp": 2.154, + "gpu_noise_us_python": 2.8280000000000003, + "gpu_bwutil_cpp": 0.5790787718366106, + "gpu_bwutil_python": 0.569862340631845, + "gpu_gap_stddev_us": 3.3607858830920736 + } + } + } + }, + "pillowresize_planar_nchw_contract_linear_4k_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "24x2160x3840" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBA8 contract_linear_4k planar (NCHW) (advanced)." + }, + "baselines": { + "pillowresize_planar_nchw_contract_linear_4k_uchar4_advanced[InOutDataType=uchar4][layout=NCHW][shape=24x2160x3840][resizeType=CONTRACT][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3369.1784445444196, + "gpu_time_us_python": 3385.8892389245602, + "gpu_noise_us_cpp": 1.6300000000000001, + "gpu_noise_us_python": 4.032, + "gpu_bwutil_cpp": 0.18995749557963654, + "gpu_bwutil_python": 0.1890199091412439, + "gpu_gap_stddev_us": 1.3750774670397121 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2364.6912695143596, + "gpu_time_us_python": 2383.0569774031196, + "gpu_noise_us_cpp": 1.332, + "gpu_noise_us_python": 4.813999999999999, + "gpu_bwutil_cpp": 0.20648297794349063, + "gpu_bwutil_python": 0.20489224036471546, + "gpu_gap_stddev_us": 4.529942843806497 + } + } + } + }, + "pillowresize_planar_nchw_contract_linear_4k_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "8x2160x3840" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBAf32 contract_linear_4k planar (NCHW) (advanced)." + }, + "baselines": { + "pillowresize_planar_nchw_contract_linear_4k_float4_advanced[InOutDataType=float4][layout=NCHW][shape=8x2160x3840][resizeType=CONTRACT][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2062.7875386690803, + "gpu_time_us_python": 2078.94179446634, + "gpu_noise_us_cpp": 1.732, + "gpu_noise_us_python": 3.567999999999999, + "gpu_bwutil_cpp": 0.4136798986616227, + "gpu_bwutil_python": 0.41046549351457184, + "gpu_gap_stddev_us": 1.7355841093243611 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1111.5681498851802, + "gpu_time_us_python": 1125.6795978748798, + "gpu_noise_us_cpp": 2.13, + "gpu_noise_us_python": 3.8560000000000003, + "gpu_bwutil_cpp": 0.5855733531417273, + "gpu_bwutil_python": 0.5782400172077313, + "gpu_gap_stddev_us": 1.8621104075387596 + } + } + } + }, + "pillowresize_planar_nchw_contract_cubic_720p_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "192x720x1280" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBA8 contract_cubic_720p planar (NCHW) (advanced)." + }, + "baselines": { + "pillowresize_planar_nchw_contract_cubic_720p_uchar4_advanced[InOutDataType=uchar4][layout=NCHW][shape=192x720x1280][resizeType=CONTRACT][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4214.69186782832, + "gpu_time_us_python": 4232.19286536088, + "gpu_noise_us_cpp": 1.7920000000000003, + "gpu_noise_us_python": 3.4919999999999995, + "gpu_bwutil_cpp": 0.134977898315546, + "gpu_bwutil_python": 0.13441966348966955, + "gpu_gap_stddev_us": 1.9144036481523057 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2837.8847676088, + "gpu_time_us_python": 2856.6290707112, + "gpu_noise_us_cpp": 1.238, + "gpu_noise_us_python": 4.888, + "gpu_bwutil_cpp": 0.152958882970229, + "gpu_bwutil_python": 0.15195581578585374, + "gpu_gap_stddev_us": 3.9362934248388908 + } + } + } + }, + "pillowresize_planar_nchw_contract_cubic_720p_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "64x720x1280" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBAf32 contract_cubic_720p planar (NCHW) (advanced)." + }, + "baselines": { + "pillowresize_planar_nchw_contract_cubic_720p_float4_advanced[InOutDataType=float4][layout=NCHW][shape=64x720x1280][resizeType=CONTRACT][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2137.0280538350003, + "gpu_time_us_python": 2154.3723657213, + "gpu_noise_us_cpp": 2.216, + "gpu_noise_us_python": 3.6079999999999997, + "gpu_bwutil_cpp": 0.35494101203776485, + "gpu_bwutil_python": 0.35208449720200924, + "gpu_gap_stddev_us": 3.9013261997464204 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1592.4878472661399, + "gpu_time_us_python": 1609.8260666977599, + "gpu_noise_us_cpp": 2.1479999999999997, + "gpu_noise_us_python": 3.768, + "gpu_bwutil_cpp": 0.36336308803869166, + "gpu_bwutil_python": 0.35945498478047666, + "gpu_gap_stddev_us": 2.235601694932038 + } + } + } + }, + "pillowresize_planar_nchw_contract_cubic_1080p_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "96x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBA8 contract_cubic_1080p planar (NCHW) (advanced)." + }, + "baselines": { + "pillowresize_planar_nchw_contract_cubic_1080p_uchar4_advanced[InOutDataType=uchar4][layout=NCHW][shape=96x1080x1920][resizeType=CONTRACT][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4916.05138217696, + "gpu_time_us_python": 4939.1575962889, + "gpu_noise_us_cpp": 3.716, + "gpu_noise_us_python": 6.548, + "gpu_bwutil_cpp": 0.1301859544358149, + "gpu_bwutil_python": 0.129577001221653, + "gpu_gap_stddev_us": 6.0069866533374885 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3257.9837405335197, + "gpu_time_us_python": 3275.38827754538, + "gpu_noise_us_cpp": 1.1019999999999999, + "gpu_noise_us_python": 4.008, + "gpu_bwutil_cpp": 0.149887748134165, + "gpu_bwutil_python": 0.14909280755992038, + "gpu_gap_stddev_us": 2.3717550408994583 + } + } + } + }, + "pillowresize_planar_nchw_contract_cubic_1080p_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBAf32 contract_cubic_1080p planar (NCHW) (advanced)." + }, + "baselines": { + "pillowresize_planar_nchw_contract_cubic_1080p_float4_advanced[InOutDataType=float4][layout=NCHW][shape=32x1080x1920][resizeType=CONTRACT][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2441.81448788314, + "gpu_time_us_python": 2458.4734229480196, + "gpu_noise_us_cpp": 3.7959999999999994, + "gpu_noise_us_python": 3.558, + "gpu_bwutil_cpp": 0.34946712666387036, + "gpu_bwutil_python": 0.3470996187976623, + "gpu_gap_stddev_us": 3.8866483961253446 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1797.93504159752, + "gpu_time_us_python": 1816.6259453591, + "gpu_noise_us_cpp": 1.8820000000000001, + "gpu_noise_us_python": 5.5200000000000005, + "gpu_bwutil_cpp": 0.36206704288666913, + "gpu_bwutil_python": 0.3583461975502765, + "gpu_gap_stddev_us": 2.6548095439352477 + } + } + } + }, + "pillowresize_planar_nchw_contract_cubic_4k_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "24x2160x3840" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBA8 contract_cubic_4k planar (NCHW) (advanced)." + }, + "baselines": { + "pillowresize_planar_nchw_contract_cubic_4k_uchar4_advanced[InOutDataType=uchar4][layout=NCHW][shape=24x2160x3840][resizeType=CONTRACT][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4939.26601035916, + "gpu_time_us_python": 4959.605456333481, + "gpu_noise_us_cpp": 3.9739999999999993, + "gpu_noise_us_python": 9.58, + "gpu_bwutil_cpp": 0.12957406158636608, + "gpu_bwutil_python": 0.12904282907474632, + "gpu_gap_stddev_us": 6.702303234577544 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3209.20603932968, + "gpu_time_us_python": 3226.27318435884, + "gpu_noise_us_cpp": 1.252, + "gpu_noise_us_python": 4.42, + "gpu_bwutil_cpp": 0.15216240641553483, + "gpu_bwutil_python": 0.15135799009213222, + "gpu_gap_stddev_us": 2.2192326703572953 + } + } + } + }, + "pillowresize_planar_nchw_contract_cubic_4k_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "8x2160x3840" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBAf32 contract_cubic_4k planar (NCHW) (advanced)." + }, + "baselines": { + "pillowresize_planar_nchw_contract_cubic_4k_float4_advanced[InOutDataType=float4][layout=NCHW][shape=8x2160x3840][resizeType=CONTRACT][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2438.79018709492, + "gpu_time_us_python": 2452.91545851304, + "gpu_noise_us_cpp": 1.528, + "gpu_noise_us_python": 2.934, + "gpu_bwutil_cpp": 0.3499004967638471, + "gpu_bwutil_python": 0.34788543702079566, + "gpu_gap_stddev_us": 2.126034641494506 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1783.7837566747598, + "gpu_time_us_python": 1799.49706013336, + "gpu_noise_us_cpp": 3.246, + "gpu_noise_us_python": 5.976000000000001, + "gpu_bwutil_cpp": 0.36493625096076265, + "gpu_bwutil_python": 0.36175141241835507, + "gpu_gap_stddev_us": 4.658737518202378 + } + } + } + }, + "pillowresize_planar_nchw_expand_linear_720p_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "96x720x1280" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBA8 expand_linear_720p planar (NCHW) (advanced)." + }, + "baselines": { + "pillowresize_planar_nchw_expand_linear_720p_uchar4_advanced[InOutDataType=uchar4][layout=NCHW][shape=96x720x1280][resizeType=EXPAND][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 8175.51694070136, + "gpu_time_us_python": 8196.67074757232, + "gpu_noise_us_cpp": 4.958, + "gpu_noise_us_python": 6.526000000000001, + "gpu_bwutil_cpp": 0.13916905790771344, + "gpu_bwutil_python": 0.13880990180000502, + "gpu_gap_stddev_us": 4.536542948421717 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6315.02968899928, + "gpu_time_us_python": 6336.69014962626, + "gpu_noise_us_cpp": 1.5080000000000002, + "gpu_noise_us_python": 5.680000000000001, + "gpu_bwutil_cpp": 0.13747237686393915, + "gpu_bwutil_python": 0.13700476666118688, + "gpu_gap_stddev_us": 6.5190732439274 + } + } + } + }, + "pillowresize_planar_nchw_expand_linear_720p_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "32x720x1280" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBAf32 expand_linear_720p planar (NCHW) (advanced)." + }, + "baselines": { + "pillowresize_planar_nchw_expand_linear_720p_float4_advanced[InOutDataType=float4][layout=NCHW][shape=32x720x1280][resizeType=EXPAND][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3929.24481853838, + "gpu_time_us_python": 3948.4800159930805, + "gpu_noise_us_cpp": 2.646, + "gpu_noise_us_python": 5.3100000000000005, + "gpu_bwutil_cpp": 0.38608906041361174, + "gpu_bwutil_python": 0.3842082913656027, + "gpu_gap_stddev_us": 3.178972976492113 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2729.27023232022, + "gpu_time_us_python": 2784.73695918854, + "gpu_noise_us_cpp": 2.5, + "gpu_noise_us_python": 3.7619999999999996, + "gpu_bwutil_cpp": 0.424014256430984, + "gpu_bwutil_python": 0.41557274796524923, + "gpu_gap_stddev_us": 3.3711666649250134 + } + } + } + }, + "pillowresize_planar_nchw_expand_linear_1080p_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "48x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBA8 expand_linear_1080p planar (NCHW) (advanced)." + }, + "baselines": { + "pillowresize_planar_nchw_expand_linear_1080p_uchar4_advanced[InOutDataType=uchar4][layout=NCHW][shape=48x1080x1920][resizeType=EXPAND][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 9335.921817355662, + "gpu_time_us_python": 9357.28164955414, + "gpu_noise_us_cpp": 5.832, + "gpu_noise_us_python": 6.646000000000001, + "gpu_bwutil_cpp": 0.13710496749357373, + "gpu_bwutil_python": 0.13679201319676237, + "gpu_gap_stddev_us": 4.68704605272325 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 7171.783575050181, + "gpu_time_us_python": 7192.25877133622, + "gpu_noise_us_cpp": 1.5059999999999998, + "gpu_noise_us_python": 5.268, + "gpu_bwutil_cpp": 0.1361747108404835, + "gpu_bwutil_python": 0.13578810654745363, + "gpu_gap_stddev_us": 2.859983102575416 + } + } + } + }, + "pillowresize_planar_nchw_expand_linear_1080p_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBAf32 expand_linear_1080p planar (NCHW) (advanced)." + }, + "baselines": { + "pillowresize_planar_nchw_expand_linear_1080p_float4_advanced[InOutDataType=float4][layout=NCHW][shape=16x1080x1920][resizeType=EXPAND][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4331.4105017431, + "gpu_time_us_python": 4355.7605529653, + "gpu_noise_us_cpp": 3.102, + "gpu_noise_us_python": 4.63, + "gpu_bwutil_cpp": 0.3940213215982619, + "gpu_bwutil_python": 0.39181874298161457, + "gpu_gap_stddev_us": 3.5807396664868536 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3065.58632634346, + "gpu_time_us_python": 3088.3473486945395, + "gpu_noise_us_cpp": 4.32, + "gpu_noise_us_python": 6.162, + "gpu_bwutil_cpp": 0.4246840901752603, + "gpu_bwutil_python": 0.42156186474850904, + "gpu_gap_stddev_us": 3.7520455278220712 + } + } + } + }, + "pillowresize_planar_nchw_expand_linear_4k_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "12x2160x3840" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBA8 expand_linear_4k planar (NCHW) (advanced)." + }, + "baselines": { + "pillowresize_planar_nchw_expand_linear_4k_uchar4_advanced[InOutDataType=uchar4][layout=NCHW][shape=12x2160x3840][resizeType=EXPAND][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 9510.50239139126, + "gpu_time_us_python": 9536.96715037024, + "gpu_noise_us_cpp": 5.58, + "gpu_noise_us_python": 7.5840000000000005, + "gpu_bwutil_cpp": 0.13458819067890343, + "gpu_bwutil_python": 0.13421473714745077, + "gpu_gap_stddev_us": 8.631766622106182 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 7330.498886486799, + "gpu_time_us_python": 7352.76789632718, + "gpu_noise_us_cpp": 2.7399999999999998, + "gpu_noise_us_python": 8.472, + "gpu_bwutil_cpp": 0.13322434400755281, + "gpu_bwutil_python": 0.13282268767131078, + "gpu_gap_stddev_us": 5.495178383626888 + } + } + } + }, + "pillowresize_planar_nchw_expand_linear_4k_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "4x2160x3840" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBAf32 expand_linear_4k planar (NCHW) (advanced)." + }, + "baselines": { + "pillowresize_planar_nchw_expand_linear_4k_float4_advanced[InOutDataType=float4][layout=NCHW][shape=4x2160x3840][resizeType=EXPAND][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4305.97793290932, + "gpu_time_us_python": 4324.32371665686, + "gpu_noise_us_cpp": 2.256, + "gpu_noise_us_python": 3.6739999999999995, + "gpu_bwutil_cpp": 0.3963488097730868, + "gpu_bwutil_python": 0.3946671357027928, + "gpu_gap_stddev_us": 2.446912745692187 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3122.2958895564398, + "gpu_time_us_python": 3138.66193008352, + "gpu_noise_us_cpp": 3.372, + "gpu_noise_us_python": 8.834, + "gpu_bwutil_cpp": 0.41697160104735326, + "gpu_bwutil_python": 0.4147951610974638, + "gpu_gap_stddev_us": 0.7697721685813068 + } + } + } + }, + "pillowresize_planar_nchw_expand_cubic_720p_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "96x720x1280" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBA8 expand_cubic_720p planar (NCHW) (advanced)." + }, + "baselines": { + "pillowresize_planar_nchw_expand_cubic_720p_uchar4_advanced[InOutDataType=uchar4][layout=NCHW][shape=96x720x1280][resizeType=EXPAND][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 8847.48707475326, + "gpu_time_us_python": 8866.526498465639, + "gpu_noise_us_cpp": 6.012, + "gpu_noise_us_python": 13.931999999999999, + "gpu_bwutil_cpp": 0.12859919592391184, + "gpu_bwutil_python": 0.12832307321228403, + "gpu_gap_stddev_us": 3.65461542827405 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 7073.745511434199, + "gpu_time_us_python": 7092.429773389139, + "gpu_noise_us_cpp": 2.802, + "gpu_noise_us_python": 8.867999999999999, + "gpu_bwutil_cpp": 0.12273265022892348, + "gpu_bwutil_python": 0.12240888871173186, + "gpu_gap_stddev_us": 4.12407986670561 + } + } + } + }, + "pillowresize_planar_nchw_expand_cubic_720p_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "32x720x1280" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBAf32 expand_cubic_720p planar (NCHW) (advanced)." + }, + "baselines": { + "pillowresize_planar_nchw_expand_cubic_720p_float4_advanced[InOutDataType=float4][layout=NCHW][shape=32x720x1280][resizeType=EXPAND][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4985.02067865106, + "gpu_time_us_python": 5002.981821621141, + "gpu_noise_us_cpp": 2.79, + "gpu_noise_us_python": 3.4899999999999998, + "gpu_bwutil_cpp": 0.30431929900955634, + "gpu_bwutil_python": 0.3032267331036377, + "gpu_gap_stddev_us": 1.6807369815008428 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3899.1350054862205, + "gpu_time_us_python": 3923.04286223336, + "gpu_noise_us_cpp": 2.252, + "gpu_noise_us_python": 6.1, + "gpu_bwutil_cpp": 0.2968060758227871, + "gpu_bwutil_python": 0.29500144357593666, + "gpu_gap_stddev_us": 3.3083379708782124 + } + } + } + }, + "pillowresize_planar_nchw_expand_cubic_1080p_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "48x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBA8 expand_cubic_1080p planar (NCHW) (advanced)." + }, + "baselines": { + "pillowresize_planar_nchw_expand_cubic_1080p_uchar4_advanced[InOutDataType=uchar4][layout=NCHW][shape=48x1080x1920][resizeType=EXPAND][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 10071.79365539546, + "gpu_time_us_python": 10094.649299621542, + "gpu_noise_us_cpp": 6.422, + "gpu_noise_us_python": 7.8340000000000005, + "gpu_bwutil_cpp": 0.1270877673221784, + "gpu_bwutil_python": 0.1268001194575744, + "gpu_gap_stddev_us": 5.902265327225906 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 7975.3546624443, + "gpu_time_us_python": 7994.8006005027, + "gpu_noise_us_cpp": 2.9859999999999998, + "gpu_noise_us_python": 5.042, + "gpu_bwutil_cpp": 0.12246105627158135, + "gpu_bwutil_python": 0.12216222629357695, + "gpu_gap_stddev_us": 1.8627013219046817 + } + } + } + }, + "pillowresize_planar_nchw_expand_cubic_1080p_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBAf32 expand_cubic_1080p planar (NCHW) (advanced)." + }, + "baselines": { + "pillowresize_planar_nchw_expand_cubic_1080p_float4_advanced[InOutDataType=float4][layout=NCHW][shape=16x1080x1920][resizeType=EXPAND][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5669.13252512608, + "gpu_time_us_python": 5690.09806893084, + "gpu_noise_us_cpp": 2.3200000000000003, + "gpu_noise_us_python": 5.255999999999999, + "gpu_bwutil_cpp": 0.30104573307408733, + "gpu_bwutil_python": 0.29993647329679185, + "gpu_gap_stddev_us": 3.323249974616579 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4384.9395302251, + "gpu_time_us_python": 4402.38915751664, + "gpu_noise_us_cpp": 2.536, + "gpu_noise_us_python": 6.092, + "gpu_bwutil_cpp": 0.29691283487266307, + "gpu_bwutil_python": 0.2957351112720189, + "gpu_gap_stddev_us": 5.963710744658288 + } + } + } + }, + "pillowresize_planar_nchw_expand_cubic_4k_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "12x2160x3840" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBA8 expand_cubic_4k planar (NCHW) (advanced)." + }, + "baselines": { + "pillowresize_planar_nchw_expand_cubic_4k_uchar4_advanced[InOutDataType=uchar4][layout=NCHW][shape=12x2160x3840][resizeType=EXPAND][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 10130.210769653302, + "gpu_time_us_python": 10158.46499633784, + "gpu_noise_us_cpp": 6.58, + "gpu_noise_us_python": 9.016, + "gpu_bwutil_cpp": 0.12635494768961836, + "gpu_bwutil_python": 0.12600347219531344, + "gpu_gap_stddev_us": 5.642511556414962 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 7979.11689584726, + "gpu_time_us_python": 7998.658506113379, + "gpu_noise_us_cpp": 3.9579999999999997, + "gpu_noise_us_python": 7.69, + "gpu_bwutil_cpp": 0.12240062858043203, + "gpu_bwutil_python": 0.12210114988177671, + "gpu_gap_stddev_us": 4.1845232719858485 + } + } + } + }, + "pillowresize_planar_nchw_expand_cubic_4k_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "4x2160x3840" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBAf32 expand_cubic_4k planar (NCHW) (advanced)." + }, + "baselines": { + "pillowresize_planar_nchw_expand_cubic_4k_float4_advanced[InOutDataType=float4][layout=NCHW][shape=4x2160x3840][resizeType=EXPAND][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5765.27823534872, + "gpu_time_us_python": 5789.16539495638, + "gpu_noise_us_cpp": 3.3120000000000003, + "gpu_noise_us_python": 4.41, + "gpu_bwutil_cpp": 0.2960251871495269, + "gpu_bwutil_python": 0.294803818883074, + "gpu_gap_stddev_us": 2.4191045829346023 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4330.4705310689405, + "gpu_time_us_python": 4351.25051656162, + "gpu_noise_us_cpp": 2.9379999999999997, + "gpu_noise_us_python": 5.5600000000000005, + "gpu_bwutil_cpp": 0.30064405820396717, + "gpu_bwutil_python": 0.2992117624331731, + "gpu_gap_stddev_us": 3.1426478445463433 + } + } + } + }, + "pillowresize_fakeplanar_nchw_contract_linear_720p_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "192x720x1280" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGB8 contract_linear_720p fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "pillowresize_fakeplanar_nchw_contract_linear_720p_uchar3_advanced[InOutDataType=uchar3][layout=NCHW_FAKE][shape=192x720x1280][resizeType=CONTRACT][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4257.1741443569, + "gpu_time_us_python": 4275.33885826494, + "gpu_noise_us_cpp": 2.3600000000000003, + "gpu_noise_us_python": 3.59, + "gpu_bwutil_cpp": 0.3006693839468217, + "gpu_bwutil_python": 0.2993918329552604, + "gpu_gap_stddev_us": 1.7938578346724674 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3389.8755439775605, + "gpu_time_us_python": 3406.4133144734196, + "gpu_noise_us_cpp": 1.192, + "gpu_noise_us_python": 3.432, + "gpu_bwutil_cpp": 0.28805972339512825, + "gpu_bwutil_python": 0.28666331961970976, + "gpu_gap_stddev_us": 3.3818921194707054 + } + } + } + }, + "pillowresize_fakeplanar_nchw_contract_linear_720p_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "64x720x1280" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBf32 contract_linear_720p fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "pillowresize_fakeplanar_nchw_contract_linear_720p_float3_advanced[InOutDataType=float3][layout=NCHW_FAKE][shape=64x720x1280][resizeType=CONTRACT][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2455.8894003138403, + "gpu_time_us_python": 2474.27150172826, + "gpu_noise_us_cpp": 1.374, + "gpu_noise_us_python": 4.5280000000000005, + "gpu_bwutil_cpp": 0.6949283543504567, + "gpu_bwutil_python": 0.6897654382467946, + "gpu_gap_stddev_us": 1.9593249527879422 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2047.39812331834, + "gpu_time_us_python": 2062.4172890819, + "gpu_noise_us_cpp": 2.196, + "gpu_noise_us_python": 3.908, + "gpu_bwutil_cpp": 0.6358305817208292, + "gpu_bwutil_python": 0.6311988994600167, + "gpu_gap_stddev_us": 1.2561679531167893 + } + } + } + }, + "pillowresize_fakeplanar_nchw_contract_linear_720p_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "192x720x1280" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBA8 contract_linear_720p fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "pillowresize_fakeplanar_nchw_contract_linear_720p_uchar4_advanced[InOutDataType=uchar4][layout=NCHW_FAKE][shape=192x720x1280][resizeType=CONTRACT][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5158.15973087226, + "gpu_time_us_python": 5171.70568972212, + "gpu_noise_us_cpp": 1.6299999999999997, + "gpu_noise_us_python": 3.56, + "gpu_bwutil_cpp": 0.3308676854593756, + "gpu_bwutil_python": 0.3300009786066753, + "gpu_gap_stddev_us": 1.8414222780902894 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4335.49213869814, + "gpu_time_us_python": 4351.51343116592, + "gpu_noise_us_cpp": 1.134, + "gpu_noise_us_python": 3.0540000000000003, + "gpu_bwutil_cpp": 0.30030646406067996, + "gpu_bwutil_python": 0.29920111860134785, + "gpu_gap_stddev_us": 1.6355462390011863 + } + } + } + }, + "pillowresize_fakeplanar_nchw_contract_linear_720p_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "64x720x1280" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBAf32 contract_linear_720p fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "pillowresize_fakeplanar_nchw_contract_linear_720p_float4_advanced[InOutDataType=float4][layout=NCHW_FAKE][shape=64x720x1280][resizeType=CONTRACT][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3808.7275880755406, + "gpu_time_us_python": 3828.1992283734, + "gpu_noise_us_cpp": 2.118, + "gpu_noise_us_python": 7.332000000000001, + "gpu_bwutil_cpp": 0.5974583179498515, + "gpu_bwutil_python": 0.5944193522721751, + "gpu_gap_stddev_us": 1.0842249724106112 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3075.0388273378, + "gpu_time_us_python": 3093.11307898054, + "gpu_noise_us_cpp": 3.4240000000000004, + "gpu_noise_us_python": 9.846, + "gpu_bwutil_cpp": 0.5644203691470479, + "gpu_bwutil_python": 0.5611238858499545, + "gpu_gap_stddev_us": 3.3457365925376634 + } + } + } + }, + "pillowresize_fakeplanar_nchw_contract_linear_1080p_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "96x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGB8 contract_linear_1080p fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "pillowresize_fakeplanar_nchw_contract_linear_1080p_uchar3_advanced[InOutDataType=uchar3][layout=NCHW_FAKE][shape=96x1080x1920][resizeType=CONTRACT][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4883.708073542619, + "gpu_time_us_python": 4902.43645275336, + "gpu_noise_us_cpp": 1.6760000000000002, + "gpu_noise_us_python": 6.9319999999999995, + "gpu_bwutil_cpp": 0.29485836113713987, + "gpu_bwutil_python": 0.2937318118632783, + "gpu_gap_stddev_us": 1.4367528254023891 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3851.9309050413403, + "gpu_time_us_python": 3866.61772998374, + "gpu_noise_us_cpp": 3.886, + "gpu_noise_us_python": 3.902, + "gpu_bwutil_cpp": 0.28519502495760496, + "gpu_bwutil_python": 0.28411414550821634, + "gpu_gap_stddev_us": 1.5815676841788535 + } + } + } + }, + "pillowresize_fakeplanar_nchw_contract_linear_1080p_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBf32 contract_linear_1080p fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "pillowresize_fakeplanar_nchw_contract_linear_1080p_float3_advanced[InOutDataType=float3][layout=NCHW_FAKE][shape=32x1080x1920][resizeType=CONTRACT][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2765.8375766250597, + "gpu_time_us_python": 2782.4992354710403, + "gpu_noise_us_cpp": 2.774, + "gpu_noise_us_python": 4.266, + "gpu_bwutil_cpp": 0.694184208046211, + "gpu_bwutil_python": 0.6900275193660612, + "gpu_gap_stddev_us": 2.18845160356642 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2299.3546548724, + "gpu_time_us_python": 2313.3760365427197, + "gpu_noise_us_cpp": 1.5020000000000002, + "gpu_noise_us_python": 2.92, + "gpu_bwutil_cpp": 0.6369258620979135, + "gpu_bwutil_python": 0.6330662451732616, + "gpu_gap_stddev_us": 0.9087190477473862 + } + } + } + }, + "pillowresize_fakeplanar_nchw_contract_linear_1080p_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "96x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBA8 contract_linear_1080p fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "pillowresize_fakeplanar_nchw_contract_linear_1080p_uchar4_advanced[InOutDataType=uchar4][layout=NCHW_FAKE][shape=96x1080x1920][resizeType=CONTRACT][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5900.30228149054, + "gpu_time_us_python": 5918.596165679161, + "gpu_noise_us_cpp": 1.9060000000000001, + "gpu_noise_us_python": 3.396, + "gpu_bwutil_cpp": 0.32540740150454356, + "gpu_bwutil_python": 0.32440151871644113, + "gpu_gap_stddev_us": 1.5816398649338406 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4886.43915896398, + "gpu_time_us_python": 4901.99147859458, + "gpu_noise_us_cpp": 2.8480000000000003, + "gpu_noise_us_python": 3.072, + "gpu_bwutil_cpp": 0.2997527870933453, + "gpu_bwutil_python": 0.2988010994051824, + "gpu_gap_stddev_us": 4.631175661338153 + } + } + } + }, + "pillowresize_fakeplanar_nchw_contract_linear_1080p_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBAf32 contract_linear_1080p fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "pillowresize_fakeplanar_nchw_contract_linear_1080p_float4_advanced[InOutDataType=float4][layout=NCHW_FAKE][shape=32x1080x1920][resizeType=CONTRACT][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4311.36225547652, + "gpu_time_us_python": 4330.62305614862, + "gpu_noise_us_cpp": 3.386, + "gpu_noise_us_python": 7.202, + "gpu_bwutil_cpp": 0.5937798783328869, + "gpu_bwutil_python": 0.59113903769632, + "gpu_gap_stddev_us": 2.0244471216391107 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3478.66047837596, + "gpu_time_us_python": 3498.3479976653603, + "gpu_noise_us_cpp": 4.311999999999999, + "gpu_noise_us_python": 6.206, + "gpu_bwutil_cpp": 0.5612977852987064, + "gpu_bwutil_python": 0.5581375763116068, + "gpu_gap_stddev_us": 2.8575063369845615 + } + } + } + }, + "pillowresize_fakeplanar_nchw_contract_linear_4k_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "24x2160x3840" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGB8 contract_linear_4k fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "pillowresize_fakeplanar_nchw_contract_linear_4k_uchar3_advanced[InOutDataType=uchar3][layout=NCHW_FAKE][shape=24x2160x3840][resizeType=CONTRACT][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5030.12969017024, + "gpu_time_us_python": 5049.4013061523, + "gpu_noise_us_cpp": 2.754, + "gpu_noise_us_python": 3.0780000000000003, + "gpu_bwutil_cpp": 0.2862752616827434, + "gpu_bwutil_python": 0.28518266061534325, + "gpu_gap_stddev_us": 0.6631598127250781 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3865.08040020392, + "gpu_time_us_python": 3880.69850287976, + "gpu_noise_us_cpp": 1.754, + "gpu_noise_us_python": 3.214, + "gpu_bwutil_cpp": 0.2842240016909528, + "gpu_bwutil_python": 0.2830803236057956, + "gpu_gap_stddev_us": 1.8767365551486823 + } + } + } + }, + "pillowresize_fakeplanar_nchw_contract_linear_4k_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "8x2160x3840" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBf32 contract_linear_4k fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "pillowresize_fakeplanar_nchw_contract_linear_4k_float3_advanced[InOutDataType=float3][layout=NCHW_FAKE][shape=8x2160x3840][resizeType=CONTRACT][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2759.7835587930595, + "gpu_time_us_python": 2777.92358724476, + "gpu_noise_us_cpp": 1.448, + "gpu_noise_us_python": 2.4539999999999997, + "gpu_bwutil_cpp": 0.695706913345206, + "gpu_bwutil_python": 0.69116391098672, + "gpu_gap_stddev_us": 2.185825780019679 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2290.5457216045, + "gpu_time_us_python": 2305.8152029890603, + "gpu_noise_us_cpp": 2.088, + "gpu_noise_us_python": 4.232, + "gpu_bwutil_cpp": 0.6393736638858922, + "gpu_bwutil_python": 0.6351365269102793, + "gpu_gap_stddev_us": 0.9774732561615695 + } + } + } + }, + "pillowresize_fakeplanar_nchw_contract_linear_4k_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "24x2160x3840" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBA8 contract_linear_4k fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "pillowresize_fakeplanar_nchw_contract_linear_4k_uchar4_advanced[InOutDataType=uchar4][layout=NCHW_FAKE][shape=24x2160x3840][resizeType=CONTRACT][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6149.1549561663, + "gpu_time_us_python": 6168.38117227318, + "gpu_noise_us_cpp": 5.367999999999999, + "gpu_noise_us_python": 3.572, + "gpu_bwutil_cpp": 0.312238397119996, + "gpu_bwutil_python": 0.3112650147069454, + "gpu_gap_stddev_us": 2.9490309139508755 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4886.01123959948, + "gpu_time_us_python": 4901.66187639938, + "gpu_noise_us_cpp": 1.4979999999999998, + "gpu_noise_us_python": 2.586, + "gpu_bwutil_cpp": 0.2997739932040192, + "gpu_bwutil_python": 0.2988156183247666, + "gpu_gap_stddev_us": 3.2429898441209324 + } + } + } + }, + "pillowresize_fakeplanar_nchw_contract_linear_4k_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "8x2160x3840" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBAf32 contract_linear_4k fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "pillowresize_fakeplanar_nchw_contract_linear_4k_float4_advanced[InOutDataType=float4][layout=NCHW_FAKE][shape=8x2160x3840][resizeType=CONTRACT][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4307.47165518286, + "gpu_time_us_python": 4326.7424419008, + "gpu_noise_us_cpp": 2.152, + "gpu_noise_us_python": 3.558, + "gpu_bwutil_cpp": 0.594316235531456, + "gpu_bwutil_python": 0.5916692269976386, + "gpu_gap_stddev_us": 1.2570468485343642 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3480.0415476435, + "gpu_time_us_python": 3497.3035600449803, + "gpu_noise_us_cpp": 4.241999999999999, + "gpu_noise_us_python": 6.066, + "gpu_bwutil_cpp": 0.5610750174092407, + "gpu_bwutil_python": 0.5583059266754067, + "gpu_gap_stddev_us": 2.5089004254837386 + } + } + } + }, + "pillowresize_fakeplanar_nchw_contract_cubic_720p_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "192x720x1280" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGB8 contract_cubic_720p fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "pillowresize_fakeplanar_nchw_contract_cubic_720p_uchar3_advanced[InOutDataType=uchar3][layout=NCHW_FAKE][shape=192x720x1280][resizeType=CONTRACT][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4881.70732351444, + "gpu_time_us_python": 4899.2039722914, + "gpu_noise_us_cpp": 1.94, + "gpu_noise_us_python": 2.9059999999999997, + "gpu_bwutil_cpp": 0.2622038753968078, + "gpu_bwutil_python": 0.261267354463296, + "gpu_gap_stddev_us": 1.2328777452073443 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3897.9056432012803, + "gpu_time_us_python": 3913.9935300306, + "gpu_noise_us_cpp": 2.078, + "gpu_noise_us_python": 6.386, + "gpu_bwutil_cpp": 0.25053033161469773, + "gpu_bwutil_python": 0.24949848304751798, + "gpu_gap_stddev_us": 2.157452490533709 + } + } + } + }, + "pillowresize_fakeplanar_nchw_contract_cubic_720p_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "64x720x1280" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBf32 contract_cubic_720p fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "pillowresize_fakeplanar_nchw_contract_cubic_720p_float3_advanced[InOutDataType=float3][layout=NCHW_FAKE][shape=64x720x1280][resizeType=CONTRACT][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2981.87098730174, + "gpu_time_us_python": 2998.5498859768204, + "gpu_noise_us_cpp": 2.302, + "gpu_noise_us_python": 2.87, + "gpu_bwutil_cpp": 0.5723477936452233, + "gpu_bwutil_python": 0.5691640932778002, + "gpu_gap_stddev_us": 1.0327310696882352 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2486.06186908564, + "gpu_time_us_python": 2498.95855984854, + "gpu_noise_us_cpp": 2.4379999999999997, + "gpu_noise_us_python": 4.83, + "gpu_bwutil_cpp": 0.5236328246776878, + "gpu_bwutil_python": 0.5209286896081581, + "gpu_gap_stddev_us": 3.3340851853521283 + } + } + } + }, + "pillowresize_fakeplanar_nchw_contract_cubic_720p_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "192x720x1280" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBA8 contract_cubic_720p fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "pillowresize_fakeplanar_nchw_contract_cubic_720p_uchar4_advanced[InOutDataType=uchar4][layout=NCHW_FAKE][shape=192x720x1280][resizeType=CONTRACT][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6117.6457079445, + "gpu_time_us_python": 6131.45224873608, + "gpu_noise_us_cpp": 2.068, + "gpu_noise_us_python": 2.8739999999999997, + "gpu_bwutil_cpp": 0.27897490510418044, + "gpu_bwutil_python": 0.27834665662650615, + "gpu_gap_stddev_us": 2.244602089460588 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5087.094589353919, + "gpu_time_us_python": 5103.345743281579, + "gpu_noise_us_cpp": 2.65, + "gpu_noise_us_python": 4.75, + "gpu_bwutil_cpp": 0.25594982181632975, + "gpu_bwutil_python": 0.255135811716406, + "gpu_gap_stddev_us": 1.947188885691617 + } + } + } + }, + "pillowresize_fakeplanar_nchw_contract_cubic_720p_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "64x720x1280" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBAf32 contract_cubic_720p fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "pillowresize_fakeplanar_nchw_contract_cubic_720p_float4_advanced[InOutDataType=float4][layout=NCHW_FAKE][shape=64x720x1280][resizeType=CONTRACT][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4466.6551147188, + "gpu_time_us_python": 4485.29183013094, + "gpu_noise_us_cpp": 2.006, + "gpu_noise_us_python": 2.9399999999999995, + "gpu_bwutil_cpp": 0.5094540994909506, + "gpu_bwutil_python": 0.5073373674958788, + "gpu_gap_stddev_us": 1.964376254683418 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3628.3125078109797, + "gpu_time_us_python": 3644.1764665389796, + "gpu_noise_us_cpp": 3.7699999999999996, + "gpu_noise_us_python": 6.609999999999999, + "gpu_bwutil_cpp": 0.4783597588651981, + "gpu_bwutil_python": 0.47627939756165344, + "gpu_gap_stddev_us": 2.58994184070034 + } + } + } + }, + "pillowresize_fakeplanar_nchw_contract_cubic_1080p_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "96x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGB8 contract_cubic_1080p fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "pillowresize_fakeplanar_nchw_contract_cubic_1080p_uchar3_advanced[InOutDataType=uchar3][layout=NCHW_FAKE][shape=96x1080x1920][resizeType=CONTRACT][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5575.81654442678, + "gpu_time_us_python": 5591.09462526102, + "gpu_noise_us_cpp": 2.196, + "gpu_noise_us_python": 4.548, + "gpu_bwutil_cpp": 0.25825862424420964, + "gpu_bwutil_python": 0.25755265244143455, + "gpu_gap_stddev_us": 5.0270014424691 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4430.030855155741, + "gpu_time_us_python": 4444.69215853722, + "gpu_noise_us_cpp": 1.8760000000000001, + "gpu_noise_us_python": 2.9539999999999997, + "gpu_bwutil_cpp": 0.24799005312226402, + "gpu_bwutil_python": 0.2471731781617872, + "gpu_gap_stddev_us": 1.1336980146784783 + } + } + } + }, + "pillowresize_fakeplanar_nchw_contract_cubic_1080p_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBf32 contract_cubic_1080p fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "pillowresize_fakeplanar_nchw_contract_cubic_1080p_float3_advanced[InOutDataType=float3][layout=NCHW_FAKE][shape=32x1080x1920][resizeType=CONTRACT][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3370.2979507446, + "gpu_time_us_python": 3388.10607162678, + "gpu_noise_us_cpp": 1.9540000000000002, + "gpu_noise_us_python": 4.3740000000000006, + "gpu_bwutil_cpp": 0.5696827070480749, + "gpu_bwutil_python": 0.5666886026338789, + "gpu_gap_stddev_us": 1.8120877391890786 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2799.09465942586, + "gpu_time_us_python": 2814.0911027721995, + "gpu_noise_us_cpp": 2.05, + "gpu_noise_us_python": 4.744, + "gpu_bwutil_cpp": 0.5232030198801387, + "gpu_bwutil_python": 0.5204149655854983, + "gpu_gap_stddev_us": 2.7622791199456507 + } + } + } + }, + "pillowresize_fakeplanar_nchw_contract_cubic_1080p_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "96x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBA8 contract_cubic_1080p fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "pillowresize_fakeplanar_nchw_contract_cubic_1080p_uchar4_advanced[InOutDataType=uchar4][layout=NCHW_FAKE][shape=96x1080x1920][resizeType=CONTRACT][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6928.898744325321, + "gpu_time_us_python": 6946.09919711272, + "gpu_noise_us_cpp": 2.116, + "gpu_noise_us_python": 5.319999999999999, + "gpu_bwutil_cpp": 0.27710073233813887, + "gpu_bwutil_python": 0.27641453398047416, + "gpu_gap_stddev_us": 2.482243497221769 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5732.45037685736, + "gpu_time_us_python": 5747.9200839335, + "gpu_noise_us_cpp": 2.338, + "gpu_noise_us_python": 3.138, + "gpu_bwutil_cpp": 0.25552345024570805, + "gpu_bwutil_python": 0.25483662476183494, + "gpu_gap_stddev_us": 2.755042368207848 + } + } + } + }, + "pillowresize_fakeplanar_nchw_contract_cubic_1080p_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBAf32 contract_cubic_1080p fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "pillowresize_fakeplanar_nchw_contract_cubic_1080p_float4_advanced[InOutDataType=float4][layout=NCHW_FAKE][shape=32x1080x1920][resizeType=CONTRACT][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5051.2035503387, + "gpu_time_us_python": 5068.566581726021, + "gpu_noise_us_cpp": 2.2439999999999998, + "gpu_noise_us_python": 3.058, + "gpu_bwutil_cpp": 0.5068099644155637, + "gpu_bwutil_python": 0.5050738448423739, + "gpu_gap_stddev_us": 1.6956091470416519 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4096.24287299158, + "gpu_time_us_python": 4111.57314367608, + "gpu_noise_us_cpp": 4.228, + "gpu_noise_us_python": 5.394, + "gpu_bwutil_cpp": 0.47668053172113944, + "gpu_bwutil_python": 0.47490341111829604, + "gpu_gap_stddev_us": 1.2981003060270715 + } + } + } + }, + "pillowresize_fakeplanar_nchw_contract_cubic_4k_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "24x2160x3840" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGB8 contract_cubic_4k fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "pillowresize_fakeplanar_nchw_contract_cubic_4k_uchar3_advanced[InOutDataType=uchar3][layout=NCHW_FAKE][shape=24x2160x3840][resizeType=CONTRACT][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5641.72117657126, + "gpu_time_us_python": 5664.417563544341, + "gpu_noise_us_cpp": 3.378, + "gpu_noise_us_python": 5.37, + "gpu_bwutil_cpp": 0.2552416136002577, + "gpu_bwutil_python": 0.25421902164920124, + "gpu_gap_stddev_us": 2.5131465519860563 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4432.5358530267395, + "gpu_time_us_python": 4448.473314946939, + "gpu_noise_us_cpp": 1.474, + "gpu_noise_us_python": 2.778, + "gpu_bwutil_cpp": 0.2478491933431355, + "gpu_bwutil_python": 0.24696206979393615, + "gpu_gap_stddev_us": 2.5690424263476954 + } + } + } + }, + "pillowresize_fakeplanar_nchw_contract_cubic_4k_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "8x2160x3840" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBf32 contract_cubic_4k fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "pillowresize_fakeplanar_nchw_contract_cubic_4k_float3_advanced[InOutDataType=float3][layout=NCHW_FAKE][shape=8x2160x3840][resizeType=CONTRACT][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3366.94748369846, + "gpu_time_us_python": 3385.28866767878, + "gpu_noise_us_cpp": 1.864, + "gpu_noise_us_python": 5.212, + "gpu_bwutil_cpp": 0.5702496608484473, + "gpu_bwutil_python": 0.5671602466203004, + "gpu_gap_stddev_us": 2.946432939169593 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2809.37777787592, + "gpu_time_us_python": 2825.15011603812, + "gpu_noise_us_cpp": 3.154, + "gpu_noise_us_python": 5.428, + "gpu_bwutil_cpp": 0.5212850498952827, + "gpu_bwutil_python": 0.518374570337557, + "gpu_gap_stddev_us": 1.3040433268470306 + } + } + } + }, + "pillowresize_fakeplanar_nchw_contract_cubic_4k_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "24x2160x3840" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBA8 contract_cubic_4k fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "pillowresize_fakeplanar_nchw_contract_cubic_4k_uchar4_advanced[InOutDataType=uchar4][layout=NCHW_FAKE][shape=24x2160x3840][resizeType=CONTRACT][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 7092.673431502381, + "gpu_time_us_python": 7111.3728205362395, + "gpu_noise_us_cpp": 2.9720000000000004, + "gpu_noise_us_python": 4.132, + "gpu_bwutil_cpp": 0.27070222802649624, + "gpu_bwutil_python": 0.2699902234339102, + "gpu_gap_stddev_us": 3.1289816759167857 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5727.8209345268, + "gpu_time_us_python": 5744.17629127686, + "gpu_noise_us_cpp": 1.544, + "gpu_noise_us_python": 4.618, + "gpu_bwutil_cpp": 0.25572978187393575, + "gpu_bwutil_python": 0.2550014900830312, + "gpu_gap_stddev_us": 2.3918425830083225 + } + } + } + }, + "pillowresize_fakeplanar_nchw_contract_cubic_4k_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "8x2160x3840" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBAf32 contract_cubic_4k fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "pillowresize_fakeplanar_nchw_contract_cubic_4k_float4_advanced[InOutDataType=float4][layout=NCHW_FAKE][shape=8x2160x3840][resizeType=CONTRACT][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5062.7543544769, + "gpu_time_us_python": 5082.40687179562, + "gpu_noise_us_cpp": 3.028, + "gpu_noise_us_python": 4.558000000000001, + "gpu_bwutil_cpp": 0.5056536588685974, + "gpu_bwutil_python": 0.5036985152640158, + "gpu_gap_stddev_us": 2.209981328558194 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4145.4568624496, + "gpu_time_us_python": 4161.464619480161, + "gpu_noise_us_cpp": 4.768, + "gpu_noise_us_python": 6.192, + "gpu_bwutil_cpp": 0.4710204738737735, + "gpu_bwutil_python": 0.4692085540993782, + "gpu_gap_stddev_us": 1.3752830570296304 + } + } + } + }, + "pillowresize_fakeplanar_nchw_expand_linear_720p_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "96x720x1280" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGB8 expand_linear_720p fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "pillowresize_fakeplanar_nchw_expand_linear_720p_uchar3_advanced[InOutDataType=uchar3][layout=NCHW_FAKE][shape=96x720x1280][resizeType=EXPAND][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 9451.09522077766, + "gpu_time_us_python": 9474.874800222842, + "gpu_noise_us_cpp": 2.004, + "gpu_noise_us_python": 8.416, + "gpu_bwutil_cpp": 0.27086834936243476, + "gpu_bwutil_python": 0.2701884223840598, + "gpu_gap_stddev_us": 4.882675078480304 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 7207.18728640718, + "gpu_time_us_python": 7224.623212511539, + "gpu_noise_us_cpp": 1.196, + "gpu_noise_us_python": 5.006, + "gpu_bwutil_cpp": 0.27100230581003765, + "gpu_bwutil_python": 0.27034664945976683, + "gpu_gap_stddev_us": 2.5116734171812376 + } + } + } + }, + "pillowresize_fakeplanar_nchw_expand_linear_720p_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "32x720x1280" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBf32 expand_linear_720p fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "pillowresize_fakeplanar_nchw_expand_linear_720p_float3_advanced[InOutDataType=float3][layout=NCHW_FAKE][shape=32x720x1280][resizeType=EXPAND][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5836.928565003121, + "gpu_time_us_python": 5854.889069047041, + "gpu_noise_us_cpp": 3.0420000000000003, + "gpu_noise_us_python": 5.6, + "gpu_bwutil_cpp": 0.5847826384010051, + "gpu_bwutil_python": 0.5829886702735319, + "gpu_gap_stddev_us": 1.8068091261223145 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4099.595535349899, + "gpu_time_us_python": 4144.8587383593795, + "gpu_noise_us_cpp": 2.53, + "gpu_noise_us_python": 5.096, + "gpu_bwutil_cpp": 0.6350630042153573, + "gpu_bwutil_python": 0.6281304006343162, + "gpu_gap_stddev_us": 3.7645118777364677 + } + } + } + }, + "pillowresize_fakeplanar_nchw_expand_linear_720p_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "96x720x1280" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBA8 expand_linear_720p fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "pillowresize_fakeplanar_nchw_expand_linear_720p_uchar4_advanced[InOutDataType=uchar4][layout=NCHW_FAKE][shape=96x720x1280][resizeType=EXPAND][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 11929.746328081339, + "gpu_time_us_python": 11948.50010644818, + "gpu_noise_us_cpp": 3.3560000000000003, + "gpu_noise_us_python": 7.498, + "gpu_bwutil_cpp": 0.28611981315965707, + "gpu_bwutil_python": 0.2856706135294004, + "gpu_gap_stddev_us": 6.432010848831239 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 9707.59737552734, + "gpu_time_us_python": 9730.575194725581, + "gpu_noise_us_cpp": 3.4659999999999997, + "gpu_noise_us_python": 6.409999999999999, + "gpu_bwutil_cpp": 0.26826134991258344, + "gpu_bwutil_python": 0.2676274519134978, + "gpu_gap_stddev_us": 3.8710097349972292 + } + } + } + }, + "pillowresize_fakeplanar_nchw_expand_linear_720p_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "32x720x1280" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBAf32 expand_linear_720p fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "pillowresize_fakeplanar_nchw_expand_linear_720p_float4_advanced[InOutDataType=float4][layout=NCHW_FAKE][shape=32x720x1280][resizeType=EXPAND][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 7598.18553635562, + "gpu_time_us_python": 7618.49173054546, + "gpu_noise_us_cpp": 5.49, + "gpu_noise_us_python": 6.096000000000001, + "gpu_bwutil_cpp": 0.5989735057683022, + "gpu_bwutil_python": 0.5973771874260848, + "gpu_gap_stddev_us": 3.599684278898597 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5630.73778152462, + "gpu_time_us_python": 5664.4634790131, + "gpu_noise_us_cpp": 12.744, + "gpu_noise_us_python": 13.3, + "gpu_bwutil_cpp": 0.6164792549519714, + "gpu_bwutil_python": 0.6128122212596011, + "gpu_gap_stddev_us": 5.388433381273203 + } + } + } + }, + "pillowresize_fakeplanar_nchw_expand_linear_1080p_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "48x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGB8 expand_linear_1080p fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "pillowresize_fakeplanar_nchw_expand_linear_1080p_uchar3_advanced[InOutDataType=uchar3][layout=NCHW_FAKE][shape=48x1080x1920][resizeType=EXPAND][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 11063.100126515219, + "gpu_time_us_python": 11085.29871235718, + "gpu_noise_us_cpp": 3.2920000000000003, + "gpu_noise_us_python": 6.806, + "gpu_bwutil_cpp": 0.2603251411764495, + "gpu_bwutil_python": 0.2598037493968356, + "gpu_gap_stddev_us": 5.231461667171339 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 8168.77476015392, + "gpu_time_us_python": 8188.24760558138, + "gpu_noise_us_cpp": 2.186, + "gpu_noise_us_python": 3.19, + "gpu_bwutil_cpp": 0.2689680671343348, + "gpu_bwutil_python": 0.2683293018366335, + "gpu_gap_stddev_us": 2.62686776949506 + } + } + } + }, + "pillowresize_fakeplanar_nchw_expand_linear_1080p_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBf32 expand_linear_1080p fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "pillowresize_fakeplanar_nchw_expand_linear_1080p_float3_advanced[InOutDataType=float3][layout=NCHW_FAKE][shape=16x1080x1920][resizeType=EXPAND][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6519.52963364426, + "gpu_time_us_python": 6539.73667927272, + "gpu_noise_us_cpp": 3.342, + "gpu_noise_us_python": 3.404, + "gpu_bwutil_cpp": 0.5889997160496152, + "gpu_bwutil_python": 0.5871798057078806, + "gpu_gap_stddev_us": 3.5373933943737557 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4600.64583171494, + "gpu_time_us_python": 4618.90823300032, + "gpu_noise_us_cpp": 3.2020000000000004, + "gpu_noise_us_python": 3.8280000000000003, + "gpu_bwutil_cpp": 0.6366357637795057, + "gpu_bwutil_python": 0.634120052015426, + "gpu_gap_stddev_us": 1.7823022037655047 + } + } + } + }, + "pillowresize_fakeplanar_nchw_expand_linear_1080p_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "48x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBA8 expand_linear_1080p fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "pillowresize_fakeplanar_nchw_expand_linear_1080p_uchar4_advanced[InOutDataType=uchar4][layout=NCHW_FAKE][shape=48x1080x1920][resizeType=EXPAND][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 14154.77478239268, + "gpu_time_us_python": 14178.91843583842, + "gpu_noise_us_cpp": 6.14, + "gpu_noise_us_python": 7.558, + "gpu_bwutil_cpp": 0.27128673920591706, + "gpu_bwutil_python": 0.2708248407487461, + "gpu_gap_stddev_us": 2.109745889273652 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 11289.095220829602, + "gpu_time_us_python": 11301.73581353286, + "gpu_noise_us_cpp": 2.83, + "gpu_noise_us_python": 3.688, + "gpu_bwutil_cpp": 0.2594939535370746, + "gpu_bwutil_python": 0.259202223017886, + "gpu_gap_stddev_us": 3.935786376920279 + } + } + } + }, + "pillowresize_fakeplanar_nchw_expand_linear_1080p_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBAf32 expand_linear_1080p fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "pillowresize_fakeplanar_nchw_expand_linear_1080p_float4_advanced[InOutDataType=float4][layout=NCHW_FAKE][shape=16x1080x1920][resizeType=EXPAND][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 8465.40796915686, + "gpu_time_us_python": 8486.07908248898, + "gpu_noise_us_cpp": 4.968, + "gpu_noise_us_python": 6.767999999999999, + "gpu_bwutil_cpp": 0.6048145454080173, + "gpu_bwutil_python": 0.6033413708561162, + "gpu_gap_stddev_us": 4.333248293760388 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6318.39791297906, + "gpu_time_us_python": 6336.09119653694, + "gpu_noise_us_cpp": 11.844, + "gpu_noise_us_python": 12.766, + "gpu_bwutil_cpp": 0.6180614577075019, + "gpu_bwutil_python": 0.6163400895084756, + "gpu_gap_stddev_us": 7.198352330018038 + } + } + } + }, + "pillowresize_fakeplanar_nchw_expand_linear_4k_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "12x2160x3840" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGB8 expand_linear_4k fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "pillowresize_fakeplanar_nchw_expand_linear_4k_uchar3_advanced[InOutDataType=uchar3][layout=NCHW_FAKE][shape=12x2160x3840][resizeType=EXPAND][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 11038.782285607342, + "gpu_time_us_python": 11058.799353889739, + "gpu_noise_us_cpp": 5.6579999999999995, + "gpu_noise_us_python": 6.523999999999999, + "gpu_bwutil_cpp": 0.26089853620659487, + "gpu_bwutil_python": 0.2604262860580575, + "gpu_gap_stddev_us": 2.771230125855941 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 8263.537163272918, + "gpu_time_us_python": 8280.162442114979, + "gpu_noise_us_cpp": 1.9579999999999997, + "gpu_noise_us_python": 8.091999999999999, + "gpu_bwutil_cpp": 0.26586124520329935, + "gpu_bwutil_python": 0.26532819503093796, + "gpu_gap_stddev_us": 2.3342581004145515 + } + } + } + }, + "pillowresize_fakeplanar_nchw_expand_linear_4k_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "4x2160x3840" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBf32 expand_linear_4k fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "pillowresize_fakeplanar_nchw_expand_linear_4k_float3_advanced[InOutDataType=float3][layout=NCHW_FAKE][shape=4x2160x3840][resizeType=EXPAND][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6400.18940925594, + "gpu_time_us_python": 6419.71854919036, + "gpu_noise_us_cpp": 2.334, + "gpu_noise_us_python": 5.889999999999999, + "gpu_bwutil_cpp": 0.5999824798797047, + "gpu_bwutil_python": 0.5981574035509029, + "gpu_gap_stddev_us": 1.6043554531511695 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4591.582617326139, + "gpu_time_us_python": 4615.55482353824, + "gpu_noise_us_cpp": 2.9960000000000004, + "gpu_noise_us_python": 8.792, + "gpu_bwutil_cpp": 0.6378861972435893, + "gpu_bwutil_python": 0.6345773921264309, + "gpu_gap_stddev_us": 4.827129304209843 + } + } + } + }, + "pillowresize_fakeplanar_nchw_expand_linear_4k_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "12x2160x3840" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBA8 expand_linear_4k fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "pillowresize_fakeplanar_nchw_expand_linear_4k_uchar4_advanced[InOutDataType=uchar4][layout=NCHW_FAKE][shape=12x2160x3840][resizeType=EXPAND][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 13731.322559557419, + "gpu_time_us_python": 13739.859520761558, + "gpu_noise_us_cpp": 3.04, + "gpu_noise_us_python": 4.024, + "gpu_bwutil_cpp": 0.2796529085567392, + "gpu_bwutil_python": 0.27947909962441375, + "gpu_gap_stddev_us": 2.505251566215089 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 11364.52158562274, + "gpu_time_us_python": 11388.211161722738, + "gpu_noise_us_cpp": 4.0680000000000005, + "gpu_noise_us_python": 10.286, + "gpu_bwutil_cpp": 0.2577561111486024, + "gpu_bwutil_python": 0.2572215010140069, + "gpu_gap_stddev_us": 6.199123882788187 + } + } + } + }, + "pillowresize_fakeplanar_nchw_expand_linear_4k_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "4x2160x3840" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBAf32 expand_linear_4k fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "pillowresize_fakeplanar_nchw_expand_linear_4k_float4_advanced[InOutDataType=float4][layout=NCHW_FAKE][shape=4x2160x3840][resizeType=EXPAND][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 8380.12933731076, + "gpu_time_us_python": 8400.083599090538, + "gpu_noise_us_cpp": 4.6259999999999994, + "gpu_noise_us_python": 6.002, + "gpu_bwutil_cpp": 0.61096925959339, + "gpu_bwutil_python": 0.6095178236212115, + "gpu_gap_stddev_us": 2.1237621695642153 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6379.31085824962, + "gpu_time_us_python": 6398.23308382274, + "gpu_noise_us_cpp": 16.626, + "gpu_noise_us_python": 16.262, + "gpu_bwutil_cpp": 0.6121602833845146, + "gpu_bwutil_python": 0.610348512830432, + "gpu_gap_stddev_us": 5.488996866093285 + } + } + } + }, + "pillowresize_fakeplanar_nchw_expand_cubic_720p_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "96x720x1280" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGB8 expand_cubic_720p fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "pillowresize_fakeplanar_nchw_expand_cubic_720p_uchar3_advanced[InOutDataType=uchar3][layout=NCHW_FAKE][shape=96x720x1280][resizeType=EXPAND][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 9783.130535712558, + "gpu_time_us_python": 9805.48534393306, + "gpu_noise_us_cpp": 1.9340000000000004, + "gpu_noise_us_python": 13.536000000000001, + "gpu_bwutil_cpp": 0.2616753277186625, + "gpu_bwutil_python": 0.2610785459109529, + "gpu_gap_stddev_us": 5.896466443219124 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 7941.151427680759, + "gpu_time_us_python": 7958.31716837298, + "gpu_noise_us_cpp": 1.264, + "gpu_noise_us_python": 2.6660000000000004, + "gpu_bwutil_cpp": 0.245939283026463, + "gpu_bwutil_python": 0.24540888645484876, + "gpu_gap_stddev_us": 2.6611500642929773 + } + } + } + }, + "pillowresize_fakeplanar_nchw_expand_cubic_720p_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "32x720x1280" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBf32 expand_cubic_720p fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "pillowresize_fakeplanar_nchw_expand_cubic_720p_float3_advanced[InOutDataType=float3][layout=NCHW_FAKE][shape=32x720x1280][resizeType=EXPAND][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5570.3688918219195, + "gpu_time_us_python": 5593.5112656487, + "gpu_noise_us_cpp": 3.008, + "gpu_noise_us_python": 4.042, + "gpu_bwutil_cpp": 0.612766285086084, + "gpu_bwutil_python": 0.6102310374483235, + "gpu_gap_stddev_us": 3.151375178816842 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4596.38644478532, + "gpu_time_us_python": 4617.2460991444605, + "gpu_noise_us_cpp": 2.888, + "gpu_noise_us_python": 4.351999999999999, + "gpu_bwutil_cpp": 0.5664117539772351, + "gpu_bwutil_python": 0.5638530551961375, + "gpu_gap_stddev_us": 0.9971058792673834 + } + } + } + }, + "pillowresize_fakeplanar_nchw_expand_cubic_720p_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "96x720x1280" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBA8 expand_cubic_720p fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "pillowresize_fakeplanar_nchw_expand_cubic_720p_uchar4_advanced[InOutDataType=uchar4][layout=NCHW_FAKE][shape=96x720x1280][resizeType=EXPAND][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 12668.334045410098, + "gpu_time_us_python": 12694.579219818082, + "gpu_noise_us_cpp": 6.058, + "gpu_noise_us_python": 10.824000000000002, + "gpu_bwutil_cpp": 0.2694384323342671, + "gpu_bwutil_python": 0.26888131937730775, + "gpu_gap_stddev_us": 5.130055483532881 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 10847.61416393772, + "gpu_time_us_python": 10871.05301635848, + "gpu_noise_us_cpp": 5.305999999999999, + "gpu_noise_us_python": 7.036, + "gpu_bwutil_cpp": 0.240056244627931, + "gpu_bwutil_python": 0.23953828888332956, + "gpu_gap_stddev_us": 2.199227431600834 + } + } + } + }, + "pillowresize_fakeplanar_nchw_expand_cubic_720p_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "32x720x1280" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBAf32 expand_cubic_720p fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "pillowresize_fakeplanar_nchw_expand_cubic_720p_float4_advanced[InOutDataType=float4][layout=NCHW_FAKE][shape=32x720x1280][resizeType=EXPAND][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 8315.96347439668, + "gpu_time_us_python": 8336.213730432599, + "gpu_noise_us_cpp": 5.008000000000001, + "gpu_noise_us_python": 6.480000000000001, + "gpu_bwutil_cpp": 0.5472742109934065, + "gpu_bwutil_python": 0.5459447658883365, + "gpu_gap_stddev_us": 3.001190111713946 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6587.02034248828, + "gpu_time_us_python": 6603.37496955698, + "gpu_noise_us_cpp": 14.192000000000002, + "gpu_noise_us_python": 12.16, + "gpu_bwutil_cpp": 0.5269790999578211, + "gpu_bwutil_python": 0.5256750569220381, + "gpu_gap_stddev_us": 4.3712941542699335 + } + } + } + }, + "pillowresize_fakeplanar_nchw_expand_cubic_1080p_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "48x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGB8 expand_cubic_1080p fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "pillowresize_fakeplanar_nchw_expand_cubic_1080p_uchar3_advanced[InOutDataType=uchar3][layout=NCHW_FAKE][shape=48x1080x1920][resizeType=EXPAND][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 11313.9979155167, + "gpu_time_us_python": 11337.603328538942, + "gpu_noise_us_cpp": 7.276000000000001, + "gpu_noise_us_python": 8.334, + "gpu_bwutil_cpp": 0.25455210842957343, + "gpu_bwutil_python": 0.25402222458430856, + "gpu_gap_stddev_us": 7.768421881104606 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 8909.92003097906, + "gpu_time_us_python": 8927.207204273722, + "gpu_noise_us_cpp": 2.674, + "gpu_noise_us_python": 5.862, + "gpu_bwutil_cpp": 0.24659376866368438, + "gpu_bwutil_python": 0.24611737843114662, + "gpu_gap_stddev_us": 3.2275316985094444 + } + } + } + }, + "pillowresize_fakeplanar_nchw_expand_cubic_1080p_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBf32 expand_cubic_1080p fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "pillowresize_fakeplanar_nchw_expand_cubic_1080p_float3_advanced[InOutDataType=float3][layout=NCHW_FAKE][shape=16x1080x1920][resizeType=EXPAND][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6352.0973205566, + "gpu_time_us_python": 6377.4413132667005, + "gpu_noise_us_cpp": 3.56, + "gpu_noise_us_python": 6.17, + "gpu_bwutil_cpp": 0.6045248884351604, + "gpu_bwutil_python": 0.6021224344911087, + "gpu_gap_stddev_us": 4.905852501835686 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5204.10799862572, + "gpu_time_us_python": 5221.5163936014405, + "gpu_noise_us_cpp": 3.4019999999999997, + "gpu_noise_us_python": 11.559999999999999, + "gpu_bwutil_cpp": 0.5628008690528373, + "gpu_bwutil_python": 0.5609256733574997, + "gpu_gap_stddev_us": 3.060863231270861 + } + } + } + }, + "pillowresize_fakeplanar_nchw_expand_cubic_1080p_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "48x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBA8 expand_cubic_1080p fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "pillowresize_fakeplanar_nchw_expand_cubic_1080p_uchar4_advanced[InOutDataType=uchar4][layout=NCHW_FAKE][shape=48x1080x1920][resizeType=EXPAND][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 14694.380583170921, + "gpu_time_us_python": 14716.385897468059, + "gpu_noise_us_cpp": 8.604000000000001, + "gpu_noise_us_python": 9.718, + "gpu_bwutil_cpp": 0.2613245369748713, + "gpu_bwutil_python": 0.26093380612705375, + "gpu_gap_stddev_us": 5.165954684906173 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 12283.58831859766, + "gpu_time_us_python": 12301.025236220521, + "gpu_noise_us_cpp": 1.938, + "gpu_noise_us_python": 5.88, + "gpu_bwutil_cpp": 0.2384827854887206, + "gpu_bwutil_python": 0.23814487195152964, + "gpu_gap_stddev_us": 2.009787042629589 + } + } + } + }, + "pillowresize_fakeplanar_nchw_expand_cubic_1080p_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBAf32 expand_cubic_1080p fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "pillowresize_fakeplanar_nchw_expand_cubic_1080p_float4_advanced[InOutDataType=float4][layout=NCHW_FAKE][shape=16x1080x1920][resizeType=EXPAND][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 9344.26545743584, + "gpu_time_us_python": 9363.78973501696, + "gpu_noise_us_cpp": 4.664, + "gpu_noise_us_python": 8.164, + "gpu_bwutil_cpp": 0.5479297643228311, + "gpu_bwutil_python": 0.5467872477178599, + "gpu_gap_stddev_us": 2.546342008644847 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 7414.393043518039, + "gpu_time_us_python": 7430.77307308416, + "gpu_noise_us_cpp": 14.498, + "gpu_noise_us_python": 13.065999999999999, + "gpu_bwutil_cpp": 0.526695160247275, + "gpu_bwutil_python": 0.5255353282476206, + "gpu_gap_stddev_us": 4.66408753684975 + } + } + } + }, + "pillowresize_fakeplanar_nchw_expand_cubic_4k_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "12x2160x3840" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGB8 expand_cubic_4k fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "pillowresize_fakeplanar_nchw_expand_cubic_4k_uchar3_advanced[InOutDataType=uchar3][layout=NCHW_FAKE][shape=12x2160x3840][resizeType=EXPAND][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 11237.73219896394, + "gpu_time_us_python": 11262.2992391171, + "gpu_noise_us_cpp": 6.142, + "gpu_noise_us_python": 5.742, + "gpu_bwutil_cpp": 0.25627966158159265, + "gpu_bwutil_python": 0.25572071840695404, + "gpu_gap_stddev_us": 3.0429606377501317 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 8948.44326456186, + "gpu_time_us_python": 8967.55922134282, + "gpu_noise_us_cpp": 1.612, + "gpu_noise_us_python": 5.362, + "gpu_bwutil_cpp": 0.24553007051945905, + "gpu_bwutil_python": 0.24500744192660698, + "gpu_gap_stddev_us": 3.565688906715796 + } + } + } + }, + "pillowresize_fakeplanar_nchw_expand_cubic_4k_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "4x2160x3840" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBf32 expand_cubic_4k fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "pillowresize_fakeplanar_nchw_expand_cubic_4k_float3_advanced[InOutDataType=float3][layout=NCHW_FAKE][shape=4x2160x3840][resizeType=EXPAND][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6295.1475310325, + "gpu_time_us_python": 6313.774094581559, + "gpu_noise_us_cpp": 2.61, + "gpu_noise_us_python": 3.9219999999999997, + "gpu_bwutil_cpp": 0.6099937570986798, + "gpu_bwutil_python": 0.6081942001216023, + "gpu_gap_stddev_us": 1.885256846270575 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5181.377965576719, + "gpu_time_us_python": 5198.4892138818195, + "gpu_noise_us_cpp": 2.7939999999999996, + "gpu_noise_us_python": 4.896, + "gpu_bwutil_cpp": 0.5652687081731148, + "gpu_bwutil_python": 0.5634095277748754, + "gpu_gap_stddev_us": 2.5291556237922164 + } + } + } + }, + "pillowresize_fakeplanar_nchw_expand_cubic_4k_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "12x2160x3840" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBA8 expand_cubic_4k fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "pillowresize_fakeplanar_nchw_expand_cubic_4k_uchar4_advanced[InOutDataType=uchar4][layout=NCHW_FAKE][shape=12x2160x3840][resizeType=EXPAND][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 14272.659916347882, + "gpu_time_us_python": 14299.78454377912, + "gpu_noise_us_cpp": 23.869999999999997, + "gpu_noise_us_python": 8.928, + "gpu_bwutil_cpp": 0.269046238152222, + "gpu_bwutil_python": 0.26853612831408163, + "gpu_gap_stddev_us": 30.807456517714876 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 12308.889270964082, + "gpu_time_us_python": 12325.53628285722, + "gpu_noise_us_cpp": 4.29, + "gpu_noise_us_python": 10.047999999999998, + "gpu_bwutil_cpp": 0.2379919670088555, + "gpu_bwutil_python": 0.23767031520796741, + "gpu_gap_stddev_us": 2.7440069633705058 + } + } + } + }, + "pillowresize_fakeplanar_nchw_expand_cubic_4k_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "4x2160x3840" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBAf32 expand_cubic_4k fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "pillowresize_fakeplanar_nchw_expand_cubic_4k_float4_advanced[InOutDataType=float4][layout=NCHW_FAKE][shape=4x2160x3840][resizeType=EXPAND][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 9186.369773319722, + "gpu_time_us_python": 9204.61165564396, + "gpu_noise_us_cpp": 4.138000000000001, + "gpu_noise_us_python": 5.784000000000001, + "gpu_bwutil_cpp": 0.5573477075076992, + "gpu_bwutil_python": 0.5562433420998933, + "gpu_gap_stddev_us": 3.1769002030630498 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 7440.308915867479, + "gpu_time_us_python": 7450.8952533496995, + "gpu_noise_us_cpp": 17.302, + "gpu_noise_us_python": 21.41, + "gpu_bwutil_cpp": 0.5248627108922955, + "gpu_bwutil_python": 0.52411660504402, + "gpu_gap_stddev_us": 7.041763836471965 + } + } + } + }, + "pillowresize_planar_nchw_contract_cubic_1080p_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "96x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGB8 contract_cubic_1080p planar (NCHW) (advanced). Same-tier native reference for the advanced FakePlanar case." + }, + "baselines": { + "pillowresize_planar_nchw_contract_cubic_1080p_uchar3_advanced[InOutDataType=uchar3][layout=NCHW][shape=96x1080x1920][resizeType=CONTRACT][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3646.603488231, + "gpu_time_us_python": 3664.1787623403397, + "gpu_noise_us_cpp": 3.044, + "gpu_noise_us_python": 5.545999999999999, + "gpu_bwutil_cpp": 0.13162976020767897, + "gpu_bwutil_python": 0.1309987116620859, + "gpu_gap_stddev_us": 2.9823945010857438 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2791.88540215408, + "gpu_time_us_python": 2808.44152732038, + "gpu_noise_us_cpp": 2.3120000000000003, + "gpu_noise_us_python": 4.0120000000000005, + "gpu_bwutil_cpp": 0.1311714326704079, + "gpu_bwutil_python": 0.13039913928883917, + "gpu_gap_stddev_us": 2.42489475772218 + } + } + } + }, + "pillowresize_planar_nchw_contract_cubic_1080p_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBf32 contract_cubic_1080p planar (NCHW) (advanced). Same-tier native reference for the advanced FakePlanar case." + }, + "baselines": { + "pillowresize_planar_nchw_contract_cubic_1080p_float3_advanced[InOutDataType=float3][layout=NCHW][shape=32x1080x1920][resizeType=CONTRACT][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1944.86813323436, + "gpu_time_us_python": 1962.5460229798798, + "gpu_noise_us_cpp": 2.33, + "gpu_noise_us_python": 5.204, + "gpu_bwutil_cpp": 0.3290716496318771, + "gpu_bwutil_python": 0.3261088215484424, + "gpu_gap_stddev_us": 3.212780073938979 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1384.92666311834, + "gpu_time_us_python": 1402.0317220039199, + "gpu_noise_us_cpp": 1.35, + "gpu_noise_us_python": 4.868, + "gpu_bwutil_cpp": 0.35250374913068816, + "gpu_bwutil_python": 0.3482038592224097, + "gpu_gap_stddev_us": 5.224834147613535 + } + } + } + }, + "pillowresize_planar_nchw_contract_linear_1080p_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "96x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGB8 contract_linear_1080p planar (NCHW) (advanced). Same-tier native reference for the advanced FakePlanar case." + }, + "baselines": { + "pillowresize_planar_nchw_contract_linear_1080p_uchar3_advanced[InOutDataType=uchar3][layout=NCHW][shape=96x1080x1920][resizeType=CONTRACT][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2667.86646183494, + "gpu_time_us_python": 2683.96502722588, + "gpu_noise_us_cpp": 2.492, + "gpu_noise_us_python": 4.2059999999999995, + "gpu_bwutil_cpp": 0.17991964346527195, + "gpu_bwutil_python": 0.17884078690154634, + "gpu_gap_stddev_us": 2.0178756710971992 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2115.1010674429, + "gpu_time_us_python": 2128.9117680725003, + "gpu_noise_us_cpp": 1.876, + "gpu_noise_us_python": 8.006, + "gpu_bwutil_cpp": 0.17313583460857676, + "gpu_bwutil_python": 0.1720127684761493, + "gpu_gap_stddev_us": 8.662215474685231 + } + } + } + }, + "pillowresize_planar_nchw_contract_linear_1080p_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBf32 contract_linear_1080p planar (NCHW) (advanced). Same-tier native reference for the advanced FakePlanar case." + }, + "baselines": { + "pillowresize_planar_nchw_contract_linear_1080p_float3_advanced[InOutDataType=float3][layout=NCHW][shape=32x1080x1920][resizeType=CONTRACT][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1378.83248966422, + "gpu_time_us_python": 1395.4939975477598, + "gpu_noise_us_cpp": 1.3, + "gpu_noise_us_python": 4.74, + "gpu_bwutil_cpp": 0.4641616191840696, + "gpu_bwutil_python": 0.4586226389934014, + "gpu_gap_stddev_us": 2.780404131776772 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 835.5496832479399, + "gpu_time_us_python": 850.92211110942, + "gpu_noise_us_cpp": 1.02, + "gpu_noise_us_python": 4.436, + "gpu_bwutil_cpp": 0.5842788113176809, + "gpu_bwutil_python": 0.5737319416680398, + "gpu_gap_stddev_us": 4.018423091047712 + } + } + } + }, + "pillowresize_planar_nchw_expand_cubic_1080p_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "48x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGB8 expand_cubic_1080p planar (NCHW) (advanced). Same-tier native reference for the advanced FakePlanar case." + }, + "baselines": { + "pillowresize_planar_nchw_expand_cubic_1080p_uchar3_advanced[InOutDataType=uchar3][layout=NCHW][shape=48x1080x1920][resizeType=EXPAND][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 7939.571174979159, + "gpu_time_us_python": 7961.619219183859, + "gpu_noise_us_cpp": 6.542, + "gpu_noise_us_python": 11.448, + "gpu_bwutil_cpp": 0.12091381549582844, + "gpu_bwutil_python": 0.12057928610810734, + "gpu_gap_stddev_us": 6.163748592153397 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6704.8008734128, + "gpu_time_us_python": 6725.879988174781, + "gpu_noise_us_cpp": 2.5620000000000003, + "gpu_noise_us_python": 8.402000000000001, + "gpu_bwutil_cpp": 0.1092380331284865, + "gpu_bwutil_python": 0.10889473360619392, + "gpu_gap_stddev_us": 9.079892839840252 + } + } + } + }, + "pillowresize_planar_nchw_expand_cubic_1080p_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBf32 expand_cubic_1080p planar (NCHW) (advanced). Same-tier native reference for the advanced FakePlanar case." + }, + "baselines": { + "pillowresize_planar_nchw_expand_cubic_1080p_float3_advanced[InOutDataType=float3][layout=NCHW][shape=16x1080x1920][resizeType=EXPAND][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4122.9329202995605, + "gpu_time_us_python": 4142.842106740941, + "gpu_noise_us_cpp": 2.072, + "gpu_noise_us_python": 3.618, + "gpu_bwutil_cpp": 0.31045907175903487, + "gpu_bwutil_python": 0.3089675621952165, + "gpu_gap_stddev_us": 2.5396047934310313 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3201.4866986699, + "gpu_time_us_python": 3215.47439194428, + "gpu_noise_us_cpp": 2.824, + "gpu_noise_us_python": 4.008, + "gpu_bwutil_cpp": 0.30496803001445627, + "gpu_bwutil_python": 0.3036422429374096, + "gpu_gap_stddev_us": 1.7427502600046774 + } + } + } + }, + "pillowresize_planar_nchw_expand_linear_1080p_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "48x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGB8 expand_linear_1080p planar (NCHW) (advanced). Same-tier native reference for the advanced FakePlanar case." + }, + "baselines": { + "pillowresize_planar_nchw_expand_linear_1080p_uchar3_advanced[InOutDataType=uchar3][layout=NCHW][shape=48x1080x1920][resizeType=EXPAND][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 8240.286587130602, + "gpu_time_us_python": 8262.81454640046, + "gpu_noise_us_cpp": 8.79, + "gpu_noise_us_python": 8.026, + "gpu_bwutil_cpp": 0.11650098340452006, + "gpu_bwutil_python": 0.11618356222837467, + "gpu_gap_stddev_us": 9.334157722862427 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6164.854825193681, + "gpu_time_us_python": 6186.43425810208, + "gpu_noise_us_cpp": 2.5660000000000003, + "gpu_noise_us_python": 11.536000000000001, + "gpu_bwutil_cpp": 0.11879803227455141, + "gpu_bwutil_python": 0.11838526597657181, + "gpu_gap_stddev_us": 4.493844870498341 + } + } + } + }, + "pillowresize_planar_nchw_expand_linear_1080p_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGBf32 expand_linear_1080p planar (NCHW) (advanced). Same-tier native reference for the advanced FakePlanar case." + }, + "baselines": { + "pillowresize_planar_nchw_expand_linear_1080p_float3_advanced[InOutDataType=float3][layout=NCHW][shape=16x1080x1920][resizeType=EXPAND][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3977.6483641729997, + "gpu_time_us_python": 3998.2453921484203, + "gpu_noise_us_cpp": 1.934, + "gpu_noise_us_python": 9.662, + "gpu_bwutil_cpp": 0.3217985881637674, + "gpu_bwutil_python": 0.32014146333941257, + "gpu_gap_stddev_us": 3.3191442569654117 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3224.69198800592, + "gpu_time_us_python": 3242.5795912275403, + "gpu_noise_us_cpp": 1.656, + "gpu_noise_us_python": 3.5439999999999996, + "gpu_bwutil_cpp": 0.30276952277180963, + "gpu_bwutil_python": 0.30109909884028335, + "gpu_gap_stddev_us": 3.5879810704257853 + } + } + } + }, + "pillowresize_anisotropic_contract_480p_tensor_rgb8_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "48x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGB8 tensor anisotropically from 1920x1080 to 864x480." + }, + "baselines": { + "pillowresize_anisotropic_contract_480p_tensor_rgb8_basic[InOutDataType=uchar3][layout=NHWC][shape=48x1080x1920][resizeType=TARGET_480x864][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1491.53411671386, + "gpu_time_us_python": 1507.8477972591, + "gpu_noise_us_cpp": 2.4520000000000004, + "gpu_noise_us_python": 4.656000000000001, + "gpu_bwutil_cpp": 0.15447270847898525, + "gpu_bwutil_python": 0.15280219543055526, + "gpu_gap_stddev_us": 3.007174113337137 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1282.99368832604, + "gpu_time_us_python": 1300.6349086594, + "gpu_noise_us_cpp": 1.126, + "gpu_noise_us_python": 2.968, + "gpu_bwutil_cpp": 0.13702648987530466, + "gpu_bwutil_python": 0.13516323875226152, + "gpu_gap_stddev_us": 2.275116952910992 + } + } + } + }, + "pillowresize_anisotropic_contract_480p_varshape_rgb8_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGB8 ImageBatchVarShape anisotropically from 1920x1080 to 864x480." + }, + "baselines": { + "pillowresize_anisotropic_contract_480p_varshape_rgb8_basic[InOutDataType=uchar3][layout=NHWC][shape=32x1080x1920][resizeType=TARGET_480x864][interpolation=CUBIC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1177.28199788734, + "gpu_time_us_python": 1198.3280977417603, + "gpu_noise_us_cpp": 5.7139999999999995, + "gpu_noise_us_python": 6.239999999999999, + "gpu_bwutil_cpp": 0.13047524202835944, + "gpu_bwutil_python": 0.12818679217148193, + "gpu_gap_stddev_us": 2.7869830186971374 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1019.2111735151799, + "gpu_time_us_python": 1042.55806245076, + "gpu_noise_us_cpp": 5.028, + "gpu_noise_us_python": 5.4799999999999995, + "gpu_bwutil_cpp": 0.11498765727580351, + "gpu_bwutil_python": 0.11241225349857387, + "gpu_gap_stddev_us": 2.3344893902207713 + } + } + } + }, + "pillowresize_anisotropic_expand_4k_tensor_rgb8_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "12x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGB8 tensor anisotropically from 864x480 to 3840x2160." + }, + "baselines": { + "pillowresize_anisotropic_expand_4k_tensor_rgb8_basic[InOutDataType=uchar3][layout=NHWC][shape=12x480x864][resizeType=TARGET_2160x3840][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1476.9972125719, + "gpu_time_us_python": 1495.43631497458, + "gpu_noise_us_cpp": 3.2239999999999993, + "gpu_noise_us_python": 4.337999999999999, + "gpu_bwutil_cpp": 0.13649351579166816, + "gpu_bwutil_python": 0.13481122808264082, + "gpu_gap_stddev_us": 2.5046480777117415 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1204.66411145026, + "gpu_time_us_python": 1220.83134065696, + "gpu_noise_us_cpp": 1.4380000000000002, + "gpu_noise_us_python": 5.442, + "gpu_bwutil_cpp": 0.12769332157463814, + "gpu_bwutil_python": 0.1260004114916723, + "gpu_gap_stddev_us": 1.7315762579766456 + } + } + } + }, + "pillowresize_anisotropic_expand_4k_varshape_rgb8_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "8x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize RGB8 ImageBatchVarShape anisotropically from 864x480 to 3840x2160." + }, + "baselines": { + "pillowresize_anisotropic_expand_4k_varshape_rgb8_basic[InOutDataType=uchar3][layout=NHWC][shape=8x480x864][resizeType=TARGET_2160x3840][interpolation=CUBIC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1072.94396360558, + "gpu_time_us_python": 1093.7399370874398, + "gpu_noise_us_cpp": 3.318, + "gpu_noise_us_python": 5.1659999999999995, + "gpu_bwutil_cpp": 0.12526384915607894, + "gpu_bwutil_python": 0.12288637431526772, + "gpu_gap_stddev_us": 5.465507715791 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 940.8049936928601, + "gpu_time_us_python": 961.1118414324599, + "gpu_noise_us_cpp": 1.6800000000000002, + "gpu_noise_us_python": 5.244, + "gpu_bwutil_cpp": 0.10901067414804629, + "gpu_bwutil_python": 0.1067072180093904, + "gpu_gap_stddev_us": 5.336431027033282 + } + } + } + }, + "pillowresize_anisotropic_contract_480x864_nchw_tensor_cubic_float3_c3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize float3 3-channel Tensor (NCHW) anisotropically from 1920x1080 to 864x480 with CUBIC. Advanced normalized mirror of pillowresize_planar_nchw_contract_cubic_1080p_float3_basic; batch 32 scaled conservatively to 16." + }, + "baselines": { + "pillowresize_anisotropic_contract_480x864_nchw_tensor_cubic_float3_c3_advanced[InOutDataType=float3][layout=NCHW][shape=16x1080x1920][resizeType=TARGET_480x864][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 917.4849804329599, + "gpu_time_us_python": 933.7069934889199, + "gpu_noise_us_cpp": 2.6060000000000003, + "gpu_noise_us_python": 4.184, + "gpu_bwutil_cpp": 0.3348294465987813, + "gpu_bwutil_python": 0.3290189119311574, + "gpu_gap_stddev_us": 3.37762182699909 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 662.24491867538, + "gpu_time_us_python": 678.1666110249, + "gpu_noise_us_cpp": 1.4340000000000002, + "gpu_noise_us_python": 3.7920000000000003, + "gpu_bwutil_cpp": 0.3538596831222115, + "gpu_bwutil_python": 0.3455704144718914, + "gpu_gap_stddev_us": 3.5171406629343416 + } + } + } + }, + "pillowresize_anisotropic_contract_480x864_nchw_varshape_cubic_float3_c3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "20x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize float3 3-channel VarShape (NCHW) anisotropically from 1920x1080 to 864x480 with CUBIC. Advanced normalized mirror of pillowresize_planar_nchw_contract_cubic_1080p_varshape_float3_advanced; batch 20 paired with the interleaved NHWC reference-timing profile." + }, + "baselines": { + "pillowresize_anisotropic_contract_480x864_nchw_varshape_cubic_float3_c3_advanced[InOutDataType=float3][layout=NCHW][shape=20x1080x1920][resizeType=TARGET_480x864][interpolation=CUBIC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1178.9518595261202, + "gpu_time_us_python": 1200.6693532382, + "gpu_noise_us_cpp": 2.666, + "gpu_noise_us_python": 5.642, + "gpu_bwutil_cpp": 0.32571723652702195, + "gpu_bwutil_python": 0.3198371936401731, + "gpu_gap_stddev_us": 4.800129072563033 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 932.5388380669801, + "gpu_time_us_python": 954.94504699288, + "gpu_noise_us_cpp": 2.178, + "gpu_noise_us_python": 4.268, + "gpu_bwutil_cpp": 0.31415425342043174, + "gpu_bwutil_python": 0.30679650672484554, + "gpu_gap_stddev_us": 2.3649891831607497 + } + } + } + }, + "pillowresize_anisotropic_contract_480x864_nchwfake_tensor_cubic_float3_c3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize float3 3-channel Tensor (NCHW_FAKE) anisotropically from 1920x1080 to 864x480 with CUBIC. Advanced normalized mirror of pillowresize_fakeplanar_nchw_contract_cubic_1080p_float3_advanced; batch 32 scaled conservatively to 16." + }, + "baselines": { + "pillowresize_anisotropic_contract_480x864_nchwfake_tensor_cubic_float3_c3_advanced[InOutDataType=float3][layout=NCHW_FAKE][shape=16x1080x1920][resizeType=TARGET_480x864][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1619.52934834258, + "gpu_time_us_python": 1636.37207769874, + "gpu_noise_us_cpp": 2.09, + "gpu_noise_us_python": 2.34, + "gpu_bwutil_cpp": 0.5690548440274068, + "gpu_bwutil_python": 0.5631977096646045, + "gpu_gap_stddev_us": 1.5842828909026945 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1335.46618328532, + "gpu_time_us_python": 1349.44930786626, + "gpu_noise_us_cpp": 6.5120000000000005, + "gpu_noise_us_python": 5.144, + "gpu_bwutil_cpp": 0.5263644570395565, + "gpu_bwutil_python": 0.520911665315332, + "gpu_gap_stddev_us": 1.8944211745009711 + } + } + } + }, + "pillowresize_anisotropic_contract_480x864_nhwc_tensor_cubic_float3_c3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize float3 3-channel Tensor (NHWC) anisotropically from 1920x1080 to 864x480 with CUBIC. Advanced normalized mirror of pillowresize_contract_cubic_1080p_float3_basic; batch 32 scaled conservatively to 16." + }, + "baselines": { + "pillowresize_anisotropic_contract_480x864_nhwc_tensor_cubic_float3_c3_advanced[InOutDataType=float3][layout=NHWC][shape=16x1080x1920][resizeType=TARGET_480x864][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 737.44746499282, + "gpu_time_us_python": 753.8426734857601, + "gpu_noise_us_cpp": 1.828, + "gpu_noise_us_python": 3.8520000000000003, + "gpu_bwutil_cpp": 0.41657381239998303, + "gpu_bwutil_python": 0.40752247856437557, + "gpu_gap_stddev_us": 2.7240637923313997 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 593.27467290396, + "gpu_time_us_python": 608.64984452722, + "gpu_noise_us_cpp": 1.342, + "gpu_noise_us_python": 3.006, + "gpu_bwutil_cpp": 0.39498658153425603, + "gpu_bwutil_python": 0.3850230863733562, + "gpu_gap_stddev_us": 2.7028658710794176 + } + } + } + }, + "pillowresize_anisotropic_contract_480x864_nhwc_varshape_cubic_float3_c3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "20x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize float3 3-channel VarShape (NHWC) anisotropically from 1920x1080 to 864x480 with CUBIC. Advanced normalized mirror of pillowresize_contract_cubic_1080p_varshape_float3_advanced; batch 20 calibrated for approximately 1 ms reference timing." + }, + "baselines": { + "pillowresize_anisotropic_contract_480x864_nhwc_varshape_cubic_float3_c3_advanced[InOutDataType=float3][layout=NHWC][shape=20x1080x1920][resizeType=TARGET_480x864][interpolation=CUBIC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1028.88133646182, + "gpu_time_us_python": 1048.5929825777798, + "gpu_noise_us_cpp": 3.1919999999999997, + "gpu_noise_us_python": 4.288, + "gpu_bwutil_cpp": 0.37323121180501717, + "gpu_bwutil_python": 0.3662145609845818, + "gpu_gap_stddev_us": 1.0093816915451166 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 814.94699847334, + "gpu_time_us_python": 836.34457005968, + "gpu_noise_us_cpp": 2.8999999999999995, + "gpu_noise_us_python": 5.63, + "gpu_bwutil_cpp": 0.3594731767012124, + "gpu_bwutil_python": 0.3502926616016929, + "gpu_gap_stddev_us": 5.756785378413777 + } + } + } + }, + "pillowresize_anisotropic_contract_480x864_nchw_tensor_linear_float3_c3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize float3 3-channel Tensor (NCHW) anisotropically from 1920x1080 to 864x480 with LINEAR. Advanced normalized mirror of pillowresize_planar_nchw_contract_linear_1080p_float3_basic; batch 32 scaled conservatively to 16." + }, + "baselines": { + "pillowresize_anisotropic_contract_480x864_nchw_tensor_linear_float3_c3_advanced[InOutDataType=float3][layout=NCHW][shape=16x1080x1920][resizeType=TARGET_480x864][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 755.4675460780602, + "gpu_time_us_python": 772.0036779203201, + "gpu_noise_us_cpp": 2.04, + "gpu_noise_us_python": 3.5780000000000003, + "gpu_bwutil_cpp": 0.4066371300721823, + "gpu_bwutil_python": 0.3979307310980872, + "gpu_gap_stddev_us": 2.1122247872944153 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 579.2113470128, + "gpu_time_us_python": 595.70777634152, + "gpu_noise_us_cpp": 1.8479999999999996, + "gpu_noise_us_python": 3.906, + "gpu_bwutil_cpp": 0.4045502139669773, + "gpu_bwutil_python": 0.39336332935993334, + "gpu_gap_stddev_us": 2.4044404863035544 + } + } + } + }, + "pillowresize_anisotropic_contract_480x864_nchwfake_tensor_linear_float3_c3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize float3 3-channel Tensor (NCHW_FAKE) anisotropically from 1920x1080 to 864x480 with LINEAR. Advanced normalized mirror of pillowresize_fakeplanar_nchw_contract_linear_1080p_float3_advanced; batch 32 scaled conservatively to 16." + }, + "baselines": { + "pillowresize_anisotropic_contract_480x864_nchwfake_tensor_linear_float3_c3_advanced[InOutDataType=float3][layout=NCHW_FAKE][shape=16x1080x1920][resizeType=TARGET_480x864][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1531.16122704854, + "gpu_time_us_python": 1547.64197638, + "gpu_noise_us_cpp": 2.578, + "gpu_noise_us_python": 3.7920000000000003, + "gpu_bwutil_cpp": 0.6018967531363443, + "gpu_bwutil_python": 0.59548824986894, + "gpu_gap_stddev_us": 1.8176021164619152 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1245.0820667602998, + "gpu_time_us_python": 1260.03433133546, + "gpu_noise_us_cpp": 2.3339999999999996, + "gpu_noise_us_python": 5.262, + "gpu_bwutil_cpp": 0.5645571147093806, + "gpu_bwutil_python": 0.5578568996441228, + "gpu_gap_stddev_us": 1.876789585513205 + } + } + } + }, + "pillowresize_anisotropic_contract_480x864_nhwc_tensor_linear_float3_c3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize float3 3-channel Tensor (NHWC) anisotropically from 1920x1080 to 864x480 with LINEAR. Advanced normalized mirror of pillowresize_contract_linear_1080p_float3_basic; batch 32 scaled conservatively to 16." + }, + "baselines": { + "pillowresize_anisotropic_contract_480x864_nhwc_tensor_linear_float3_c3_advanced[InOutDataType=float3][layout=NHWC][shape=16x1080x1920][resizeType=TARGET_480x864][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 657.9373213496401, + "gpu_time_us_python": 670.9675477503399, + "gpu_noise_us_cpp": 2.92, + "gpu_noise_us_python": 2.4560000000000004, + "gpu_bwutil_cpp": 0.46691420440863307, + "gpu_bwutil_python": 0.4578483626889282, + "gpu_gap_stddev_us": 1.0441288901933876 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 511.7347774964401, + "gpu_time_us_python": 523.5179485963802, + "gpu_noise_us_cpp": 2.368, + "gpu_noise_us_python": 3.1919999999999993, + "gpu_bwutil_cpp": 0.4578690690150727, + "gpu_bwutil_python": 0.4475732166103653, + "gpu_gap_stddev_us": 1.78724796160203 + } + } + } + }, + "pillowresize_anisotropic_expand_2160x3840_nchw_tensor_cubic_float3_c3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "4x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize float3 3-channel Tensor (NCHW) anisotropically from 864x480 to 3840x2160 with CUBIC. Advanced normalized mirror of pillowresize_planar_nchw_expand_cubic_1080p_float3_basic; batch 16 scaled conservatively to 4." + }, + "baselines": { + "pillowresize_anisotropic_expand_2160x3840_nchw_tensor_cubic_float3_c3_advanced[InOutDataType=float3][layout=NCHW][shape=4x480x864][resizeType=TARGET_2160x3840][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 745.62914644672, + "gpu_time_us_python": 761.09384303398, + "gpu_noise_us_cpp": 1.984, + "gpu_noise_us_python": 3.632, + "gpu_bwutil_cpp": 0.36050220847844944, + "gpu_bwutil_python": 0.35318369264837574, + "gpu_gap_stddev_us": 2.5436302819267467 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 582.5773692362999, + "gpu_time_us_python": 598.15049090094, + "gpu_noise_us_cpp": 2.096, + "gpu_noise_us_python": 11.754000000000001, + "gpu_bwutil_cpp": 0.3519515236274738, + "gpu_bwutil_python": 0.34280672617221863, + "gpu_gap_stddev_us": 2.3164594891691137 + } + } + } + }, + "pillowresize_anisotropic_expand_2160x3840_nchwfake_tensor_cubic_float3_c3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "4x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize float3 3-channel Tensor (NCHW_FAKE) anisotropically from 864x480 to 3840x2160 with CUBIC. Advanced normalized mirror of pillowresize_fakeplanar_nchw_expand_cubic_1080p_float3_advanced; batch 16 scaled conservatively to 4." + }, + "baselines": { + "pillowresize_anisotropic_expand_2160x3840_nchwfake_tensor_cubic_float3_c3_advanced[InOutDataType=float3][layout=NCHW_FAKE][shape=4x480x864][resizeType=TARGET_2160x3840][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1235.60883247202, + "gpu_time_us_python": 1254.30039448828, + "gpu_noise_us_cpp": 3.278, + "gpu_noise_us_python": 3.59, + "gpu_bwutil_cpp": 0.6526341812409606, + "gpu_bwutil_python": 0.6429109773848289, + "gpu_gap_stddev_us": 2.393961293259352 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1035.0881554717, + "gpu_time_us_python": 1048.84787125382, + "gpu_noise_us_cpp": 1.6700000000000004, + "gpu_noise_us_python": 5.222, + "gpu_bwutil_cpp": 0.5942167540381335, + "gpu_bwutil_python": 0.5864256048395057, + "gpu_gap_stddev_us": 1.6698029587604375 + } + } + } + }, + "pillowresize_anisotropic_expand_2160x3840_nhwc_tensor_cubic_float3_c3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "4x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize float3 3-channel Tensor (NHWC) anisotropically from 864x480 to 3840x2160 with CUBIC. Advanced normalized mirror of pillowresize_expand_cubic_1080p_float3_basic; batch 16 scaled conservatively to 4." + }, + "baselines": { + "pillowresize_anisotropic_expand_2160x3840_nhwc_tensor_cubic_float3_c3_advanced[InOutDataType=float3][layout=NHWC][shape=4x480x864][resizeType=TARGET_2160x3840][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 612.35273740856, + "gpu_time_us_python": 628.73544080168, + "gpu_noise_us_cpp": 2.8299999999999996, + "gpu_noise_us_python": 3.2380000000000004, + "gpu_bwutil_cpp": 0.43896278376893755, + "gpu_bwutil_python": 0.4275313999962879, + "gpu_gap_stddev_us": 2.511723931009064 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 553.15353710722, + "gpu_time_us_python": 568.54363340336, + "gpu_noise_us_cpp": 1.828, + "gpu_noise_us_python": 3.3379999999999996, + "gpu_bwutil_cpp": 0.3706613160864777, + "gpu_bwutil_python": 0.36066659001061874, + "gpu_gap_stddev_us": 3.8824848558013416 + } + } + } + }, + "pillowresize_anisotropic_expand_2160x3840_nchw_tensor_linear_float3_c3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "4x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize float3 3-channel Tensor (NCHW) anisotropically from 864x480 to 3840x2160 with LINEAR. Advanced normalized mirror of pillowresize_planar_nchw_expand_linear_1080p_float3_basic; batch 16 scaled conservatively to 4." + }, + "baselines": { + "pillowresize_anisotropic_expand_2160x3840_nchw_tensor_linear_float3_c3_advanced[InOutDataType=float3][layout=NCHW][shape=4x480x864][resizeType=TARGET_2160x3840][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 673.9031629055801, + "gpu_time_us_python": 689.5614942730599, + "gpu_noise_us_cpp": 1.066, + "gpu_noise_us_python": 4.628, + "gpu_bwutil_cpp": 0.3988709289997948, + "gpu_bwutil_python": 0.3898224793840326, + "gpu_gap_stddev_us": 3.093921834322973 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 585.55940651242, + "gpu_time_us_python": 601.8624252922, + "gpu_noise_us_cpp": 1.1880000000000002, + "gpu_noise_us_python": 3.3200000000000003, + "gpu_bwutil_cpp": 0.35015343253711595, + "gpu_bwutil_python": 0.34067692667867633, + "gpu_gap_stddev_us": 1.526810795300814 + } + } + } + }, + "pillowresize_anisotropic_expand_2160x3840_nchwfake_tensor_linear_float3_c3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "4x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize float3 3-channel Tensor (NCHW_FAKE) anisotropically from 864x480 to 3840x2160 with LINEAR. Advanced normalized mirror of pillowresize_fakeplanar_nchw_expand_linear_1080p_float3_advanced; batch 16 scaled conservatively to 4." + }, + "baselines": { + "pillowresize_anisotropic_expand_2160x3840_nchwfake_tensor_linear_float3_c3_advanced[InOutDataType=float3][layout=NCHW_FAKE][shape=4x480x864][resizeType=TARGET_2160x3840][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1373.72683146572, + "gpu_time_us_python": 1391.8310250176, + "gpu_noise_us_cpp": 2.09, + "gpu_noise_us_python": 4.32, + "gpu_bwutil_cpp": 0.5870166324233088, + "gpu_bwutil_python": 0.5793813043128244, + "gpu_gap_stddev_us": 2.307814504547277 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 997.6260411700999, + "gpu_time_us_python": 1013.30150235236, + "gpu_noise_us_cpp": 1.8799999999999997, + "gpu_noise_us_python": 5.672, + "gpu_bwutil_cpp": 0.6165466387812775, + "gpu_bwutil_python": 0.6070126490961887, + "gpu_gap_stddev_us": 2.0542262681164183 + } + } + } + }, + "pillowresize_anisotropic_expand_2160x3840_nhwc_tensor_linear_float3_c3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "4x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize float3 3-channel Tensor (NHWC) anisotropically from 864x480 to 3840x2160 with LINEAR. Advanced normalized mirror of pillowresize_expand_linear_1080p_float3_basic; batch 16 scaled conservatively to 4." + }, + "baselines": { + "pillowresize_anisotropic_expand_2160x3840_nhwc_tensor_linear_float3_c3_advanced[InOutDataType=float3][layout=NHWC][shape=4x480x864][resizeType=TARGET_2160x3840][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 750.60899897636, + "gpu_time_us_python": 765.29367287432, + "gpu_noise_us_cpp": 2.5260000000000002, + "gpu_noise_us_python": 2.58, + "gpu_bwutil_cpp": 0.35810960141527737, + "gpu_bwutil_python": 0.3512396424226504, + "gpu_gap_stddev_us": 1.3594986406868457 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 516.4969372428201, + "gpu_time_us_python": 531.98422663466, + "gpu_noise_us_cpp": 1.8760000000000001, + "gpu_noise_us_python": 3.21, + "gpu_bwutil_cpp": 0.3970157292266769, + "gpu_bwutil_python": 0.385466081560928, + "gpu_gap_stddev_us": 2.516216438367737 + } + } + } + }, + "pillowresize_anisotropic_contract_480x864_nhwc_tensor_cubic_float32_c1_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize float32 1-channel Tensor (NHWC) anisotropically from 1920x1080 to 864x480 with CUBIC. Advanced normalized mirror of pillowresize_contract_cubic_1080p_scalar_advanced; batch 64 scaled conservatively to 32." + }, + "baselines": { + "pillowresize_anisotropic_contract_480x864_nhwc_tensor_cubic_float32_c1_advanced[InOutDataType=float32][layout=NHWC][shape=32x1080x1920][resizeType=TARGET_480x864][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 722.60736012852, + "gpu_time_us_python": 738.36196437592, + "gpu_noise_us_cpp": 1.076, + "gpu_noise_us_python": 3.692, + "gpu_bwutil_cpp": 0.28341839026110516, + "gpu_bwutil_python": 0.27737427865218944, + "gpu_gap_stddev_us": 2.602800267110072 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 561.59331950174, + "gpu_time_us_python": 577.24041903796, + "gpu_noise_us_cpp": 1.252, + "gpu_noise_us_python": 3.876, + "gpu_bwutil_cpp": 0.27821024925463667, + "gpu_bwutil_python": 0.27069003922212787, + "gpu_gap_stddev_us": 3.010739409535533 + } + } + } + }, + "pillowresize_anisotropic_contract_480x864_nhwc_varshape_cubic_float32_c1_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "20x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize float32 1-channel VarShape (NHWC) anisotropically from 1920x1080 to 864x480 with CUBIC. Advanced normalized mirror of pillowresize_contract_cubic_1080p_varshape_scalar_advanced; batch 64 scaled conservatively to 20." + }, + "baselines": { + "pillowresize_anisotropic_contract_480x864_nhwc_varshape_cubic_float32_c1_advanced[InOutDataType=float32][layout=NHWC][shape=20x1080x1920][resizeType=TARGET_480x864][interpolation=CUBIC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 519.49621290086, + "gpu_time_us_python": 537.7524386867201, + "gpu_noise_us_cpp": 3.854, + "gpu_noise_us_python": 3.7639999999999993, + "gpu_bwutil_cpp": 0.24640347482980265, + "gpu_bwutil_python": 0.2380492835342448, + "gpu_gap_stddev_us": 2.00388729061893 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 463.33185283538006, + "gpu_time_us_python": 485.21136307657997, + "gpu_noise_us_cpp": 3.046, + "gpu_noise_us_python": 4.218, + "gpu_bwutil_cpp": 0.21080513997546885, + "gpu_bwutil_python": 0.20130060735637353, + "gpu_gap_stddev_us": 4.461815536261023 + } + } + } + }, + "pillowresize_anisotropic_contract_480x864_nhwc_tensor_lanczos_float32_c1_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "LANCZOS" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize float32 1-channel Tensor (NHWC) anisotropically from 1920x1080 to 864x480 with LANCZOS. Advanced normalized mirror of pillowresize_contract_lanczos_1080p_advanced; batch 32 scaled conservatively to 16." + }, + "baselines": { + "pillowresize_anisotropic_contract_480x864_nhwc_tensor_lanczos_float32_c1_advanced[InOutDataType=float32][layout=NHWC][shape=16x1080x1920][resizeType=TARGET_480x864][interpolation=LANCZOS][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 451.3582813928, + "gpu_time_us_python": 465.7981268751, + "gpu_noise_us_cpp": 2.0780000000000003, + "gpu_noise_us_python": 3.322, + "gpu_bwutil_cpp": 0.22687214020137034, + "gpu_bwutil_python": 0.21984355425267363, + "gpu_gap_stddev_us": 1.683873014696523 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 362.3950914773, + "gpu_time_us_python": 378.00421252577996, + "gpu_noise_us_cpp": 1.014, + "gpu_noise_us_python": 2.286, + "gpu_bwutil_cpp": 0.21558570750671566, + "gpu_bwutil_python": 0.2067149878846492, + "gpu_gap_stddev_us": 3.4340595796327897 + } + } + } + }, + "pillowresize_anisotropic_contract_480x864_nhwc_varshape_lanczos_float32_c1_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "24x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "LANCZOS" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize float32 1-channel VarShape (NHWC) anisotropically from 1920x1080 to 864x480 with LANCZOS. Advanced normalized mirror of pillowresize_contract_lanczos_1080p_advanced; batch 24 calibrated for approximately 1 ms reference timing." + }, + "baselines": { + "pillowresize_anisotropic_contract_480x864_nhwc_varshape_lanczos_float32_c1_advanced[InOutDataType=float32][layout=NHWC][shape=24x1080x1920][resizeType=TARGET_480x864][interpolation=LANCZOS][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 844.7557331545, + "gpu_time_us_python": 867.4377755087801, + "gpu_noise_us_cpp": 2.532, + "gpu_noise_us_python": 5.6659999999999995, + "gpu_bwutil_cpp": 0.18183140585037258, + "gpu_bwutil_python": 0.17708455877850351, + "gpu_gap_stddev_us": 3.600242887559974 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 766.67400347424, + "gpu_time_us_python": 789.5579919613599, + "gpu_noise_us_cpp": 2.916, + "gpu_noise_us_python": 4.292, + "gpu_bwutil_cpp": 0.1528697168427287, + "gpu_bwutil_python": 0.14845703752596584, + "gpu_gap_stddev_us": 3.9807995221662753 + } + } + } + }, + "pillowresize_anisotropic_contract_480x864_nhwc_tensor_linear_float32_c1_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize float32 1-channel Tensor (NHWC) anisotropically from 1920x1080 to 864x480 with LINEAR. Advanced normalized mirror of pillowresize_contract_linear_1080p_scalar_advanced; batch 64 scaled conservatively to 32." + }, + "baselines": { + "pillowresize_anisotropic_contract_480x864_nhwc_tensor_linear_float32_c1_advanced[InOutDataType=float32][layout=NHWC][shape=32x1080x1920][resizeType=TARGET_480x864][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 584.8691824989401, + "gpu_time_us_python": 600.0440434260602, + "gpu_noise_us_cpp": 1.6780000000000002, + "gpu_noise_us_python": 3.028, + "gpu_bwutil_cpp": 0.3501644011543291, + "gpu_bwutil_python": 0.3413152799601456, + "gpu_gap_stddev_us": 2.3406856737894057 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 499.65584790654003, + "gpu_time_us_python": 514.43097780016, + "gpu_noise_us_cpp": 2.204, + "gpu_noise_us_python": 3.036, + "gpu_bwutil_cpp": 0.3126941669700763, + "gpu_bwutil_python": 0.30373290563929894, + "gpu_gap_stddev_us": 2.3127417220525834 + } + } + } + }, + "pillowresize_anisotropic_expand_2160x3840_nhwc_tensor_cubic_float32_c1_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "8x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize float32 1-channel Tensor (NHWC) anisotropically from 864x480 to 3840x2160 with CUBIC. Advanced normalized mirror of pillowresize_expand_cubic_1080p_scalar_advanced; batch 32 scaled conservatively to 8." + }, + "baselines": { + "pillowresize_anisotropic_expand_2160x3840_nhwc_tensor_cubic_float32_c1_advanced[InOutDataType=float32][layout=NHWC][shape=8x480x864][resizeType=TARGET_2160x3840][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 514.56861843472, + "gpu_time_us_python": 529.9683514077399, + "gpu_noise_us_cpp": 1.3900000000000001, + "gpu_noise_us_python": 3.6060000000000003, + "gpu_bwutil_cpp": 0.3482541381625358, + "gpu_bwutil_python": 0.33814526544911144, + "gpu_gap_stddev_us": 2.542069991631377 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 469.48465821696, + "gpu_time_us_python": 484.81371853976, + "gpu_noise_us_cpp": 1.2159999999999997, + "gpu_noise_us_python": 2.402, + "gpu_bwutil_cpp": 0.2911702929083219, + "gpu_bwutil_python": 0.28197527516437604, + "gpu_gap_stddev_us": 3.2439756348796345 + } + } + } + }, + "pillowresize_anisotropic_expand_2160x3840_nhwc_tensor_lanczos_float32_c1_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "4x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "LANCZOS" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize float32 1-channel Tensor (NHWC) anisotropically from 864x480 to 3840x2160 with LANCZOS. Advanced normalized mirror of pillowresize_expand_lanczos_tensor_1080p_advanced; batch 16 scaled conservatively to 4." + }, + "baselines": { + "pillowresize_anisotropic_expand_2160x3840_nhwc_tensor_lanczos_float32_c1_advanced[InOutDataType=float32][layout=NHWC][shape=4x480x864][resizeType=TARGET_2160x3840][interpolation=LANCZOS][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 335.78923409924, + "gpu_time_us_python": 349.80334299870003, + "gpu_noise_us_cpp": 2.444, + "gpu_noise_us_python": 2.758, + "gpu_bwutil_cpp": 0.2668347835653734, + "gpu_bwutil_python": 0.25615870850967337, + "gpu_gap_stddev_us": 2.428745738093706 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 296.08000562534, + "gpu_time_us_python": 310.91757222254, + "gpu_noise_us_cpp": 1.7260000000000002, + "gpu_noise_us_python": 2.146, + "gpu_bwutil_cpp": 0.23085670178560588, + "gpu_bwutil_python": 0.21985206720675315, + "gpu_gap_stddev_us": 1.9079290336094146 + } + } + } + }, + "pillowresize_anisotropic_expand_2160x3840_nhwc_tensor_linear_float32_c1_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "8x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize float32 1-channel Tensor (NHWC) anisotropically from 864x480 to 3840x2160 with LINEAR. Advanced normalized mirror of pillowresize_expand_linear_1080p_scalar_advanced; batch 32 scaled conservatively to 8." + }, + "baselines": { + "pillowresize_anisotropic_expand_2160x3840_nhwc_tensor_linear_float32_c1_advanced[InOutDataType=float32][layout=NHWC][shape=8x480x864][resizeType=TARGET_2160x3840][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 950.23719374274, + "gpu_time_us_python": 964.6747774773801, + "gpu_noise_us_cpp": 3.108, + "gpu_noise_us_python": 3.95, + "gpu_bwutil_cpp": 0.1885848895784235, + "gpu_bwutil_python": 0.18576374984344415, + "gpu_gap_stddev_us": 1.9562611236009482 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 812.1477018790799, + "gpu_time_us_python": 830.5139484391999, + "gpu_noise_us_cpp": 1.024, + "gpu_noise_us_python": 2.1479999999999997, + "gpu_bwutil_cpp": 0.1683409565243913, + "gpu_bwutil_python": 0.16464306752030838, + "gpu_gap_stddev_us": 5.617892938541801 + } + } + } + }, + "pillowresize_anisotropic_contract_480x864_nchw_tensor_cubic_float4_c4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize float4 4-channel Tensor (NCHW) anisotropically from 1920x1080 to 864x480 with CUBIC. Advanced normalized mirror of pillowresize_planar_nchw_contract_cubic_1080p_float4_advanced; batch 32 scaled conservatively to 16." + }, + "baselines": { + "pillowresize_anisotropic_contract_480x864_nchw_tensor_cubic_float4_c4_advanced[InOutDataType=float4][layout=NCHW][shape=16x1080x1920][resizeType=TARGET_480x864][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1240.8872841608, + "gpu_time_us_python": 1259.7540099662199, + "gpu_noise_us_cpp": 2.106, + "gpu_noise_us_python": 4.3340000000000005, + "gpu_bwutil_cpp": 0.33008665881645893, + "gpu_bwutil_python": 0.32515015231611494, + "gpu_gap_stddev_us": 6.197709733429327 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 869.2727902795599, + "gpu_time_us_python": 885.7684722677201, + "gpu_noise_us_cpp": 1.238, + "gpu_noise_us_python": 4.924, + "gpu_bwutil_cpp": 0.3594507534300994, + "gpu_bwutil_python": 0.352764861846284, + "gpu_gap_stddev_us": 1.9728296755918384 + } + } + } + }, + "pillowresize_anisotropic_contract_480x864_nchwfake_tensor_cubic_float4_c4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize float4 4-channel Tensor (NCHW_FAKE) anisotropically from 1920x1080 to 864x480 with CUBIC. Advanced normalized mirror of pillowresize_fakeplanar_nchw_contract_cubic_1080p_float4_advanced; batch 32 scaled conservatively to 16." + }, + "baselines": { + "pillowresize_anisotropic_contract_480x864_nchwfake_tensor_cubic_float4_c4_advanced[InOutDataType=float4][layout=NCHW_FAKE][shape=16x1080x1920][resizeType=TARGET_480x864][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2430.37344228872, + "gpu_time_us_python": 2446.64223495034, + "gpu_noise_us_cpp": 2.1659999999999995, + "gpu_noise_us_python": 3.4560000000000004, + "gpu_bwutil_cpp": 0.5056013961084334, + "gpu_bwutil_python": 0.5022395685976745, + "gpu_gap_stddev_us": 1.030537651089614 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1967.2035546840802, + "gpu_time_us_python": 1981.7862685166197, + "gpu_noise_us_cpp": 2.8560000000000003, + "gpu_noise_us_python": 4.316, + "gpu_bwutil_cpp": 0.4764278957966349, + "gpu_bwutil_python": 0.4729214805310765, + "gpu_gap_stddev_us": 0.9105836048653227 + } + } + } + }, + "pillowresize_anisotropic_contract_480x864_nhwc_tensor_cubic_float4_c4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize float4 4-channel Tensor (NHWC) anisotropically from 1920x1080 to 864x480 with CUBIC. Advanced normalized mirror of pillowresize_contract_cubic_1080p_float4_advanced; batch 32 scaled conservatively to 16." + }, + "baselines": { + "pillowresize_anisotropic_contract_480x864_nhwc_tensor_cubic_float4_c4_advanced[InOutDataType=float4][layout=NHWC][shape=16x1080x1920][resizeType=TARGET_480x864][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 946.0469280098399, + "gpu_time_us_python": 959.73510043036, + "gpu_noise_us_cpp": 3.25, + "gpu_noise_us_python": 2.402, + "gpu_bwutil_cpp": 0.4329595785229355, + "gpu_bwutil_python": 0.4267860812077989, + "gpu_gap_stddev_us": 1.7183364708725475 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 691.6692362974, + "gpu_time_us_python": 706.60794619596, + "gpu_noise_us_cpp": 1.682, + "gpu_noise_us_python": 3.0839999999999996, + "gpu_bwutil_cpp": 0.45168794201498397, + "gpu_bwutil_python": 0.44215067447609424, + "gpu_gap_stddev_us": 2.166433640750834 + } + } + } + }, + "pillowresize_anisotropic_contract_480x864_nhwc_varshape_cubic_float4_c4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "8x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize float4 4-channel VarShape (NHWC) anisotropically from 1920x1080 to 864x480 with CUBIC. Advanced normalized mirror of pillowresize_contract_cubic_1080p_varshape_float4_advanced; batch 32 scaled conservatively to 8." + }, + "baselines": { + "pillowresize_anisotropic_contract_480x864_nhwc_varshape_cubic_float4_c4_advanced[InOutDataType=float4][layout=NHWC][shape=8x1080x1920][resizeType=TARGET_480x864][interpolation=CUBIC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 507.38469068206007, + "gpu_time_us_python": 525.83121190098, + "gpu_noise_us_cpp": 3.4199999999999995, + "gpu_noise_us_python": 3.4240000000000004, + "gpu_bwutil_cpp": 0.40365322633133205, + "gpu_bwutil_python": 0.38951691102565333, + "gpu_gap_stddev_us": 2.4964708206810338 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 399.87325461728, + "gpu_time_us_python": 417.21829033324, + "gpu_noise_us_cpp": 3.2079999999999997, + "gpu_noise_us_python": 4.008, + "gpu_bwutil_cpp": 0.3906635540752765, + "gpu_bwutil_python": 0.3744497699965863, + "gpu_gap_stddev_us": 1.9491260244681028 + } + } + } + }, + "pillowresize_anisotropic_contract_480x864_nchw_tensor_linear_float4_c4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize float4 4-channel Tensor (NCHW) anisotropically from 1920x1080 to 864x480 with LINEAR. Advanced normalized mirror of pillowresize_planar_nchw_contract_linear_1080p_float4_advanced; batch 32 scaled conservatively to 16." + }, + "baselines": { + "pillowresize_anisotropic_contract_480x864_nchw_tensor_linear_float4_c4_advanced[InOutDataType=float4][layout=NCHW][shape=16x1080x1920][resizeType=TARGET_480x864][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1025.67952754064, + "gpu_time_us_python": 1041.67882420662, + "gpu_noise_us_cpp": 1.026, + "gpu_noise_us_python": 2.588, + "gpu_bwutil_cpp": 0.3993453134073933, + "gpu_bwutil_python": 0.3932126443639826, + "gpu_gap_stddev_us": 1.2624638304027758 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 787.46378855104, + "gpu_time_us_python": 804.7934972759001, + "gpu_noise_us_cpp": 1.718, + "gpu_noise_us_python": 2.528, + "gpu_bwutil_cpp": 0.3967716079416655, + "gpu_bwutil_python": 0.3882422612376, + "gpu_gap_stddev_us": 3.884586227848841 + } + } + } + }, + "pillowresize_anisotropic_contract_480x864_nchwfake_tensor_linear_float4_c4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize float4 4-channel Tensor (NCHW_FAKE) anisotropically from 1920x1080 to 864x480 with LINEAR. Advanced normalized mirror of pillowresize_fakeplanar_nchw_contract_linear_1080p_float4_advanced; batch 32 scaled conservatively to 16." + }, + "baselines": { + "pillowresize_anisotropic_contract_480x864_nchwfake_tensor_linear_float4_c4_advanced[InOutDataType=float4][layout=NCHW_FAKE][shape=16x1080x1920][resizeType=TARGET_480x864][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2357.00752896992, + "gpu_time_us_python": 2373.8019781292, + "gpu_noise_us_cpp": 2.4419999999999997, + "gpu_noise_us_python": 2.9799999999999995, + "gpu_bwutil_cpp": 0.5213390932729222, + "gpu_bwutil_python": 0.517650880280551, + "gpu_gap_stddev_us": 1.6895916090176328 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1911.0758138934998, + "gpu_time_us_python": 1925.8262715756398, + "gpu_noise_us_cpp": 2.9199999999999995, + "gpu_noise_us_python": 4.392, + "gpu_bwutil_cpp": 0.49041572996300326, + "gpu_bwutil_python": 0.48666015843061816, + "gpu_gap_stddev_us": 1.7623397602036228 + } + } + } + }, + "pillowresize_anisotropic_contract_480x864_nhwc_tensor_linear_float4_c4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize float4 4-channel Tensor (NHWC) anisotropically from 1920x1080 to 864x480 with LINEAR. Advanced normalized mirror of pillowresize_contract_linear_1080p_float4_advanced; batch 32 scaled conservatively to 16." + }, + "baselines": { + "pillowresize_anisotropic_contract_480x864_nhwc_tensor_linear_float4_c4_advanced[InOutDataType=float4][layout=NHWC][shape=16x1080x1920][resizeType=TARGET_480x864][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 874.0871432748202, + "gpu_time_us_python": 888.0921603687999, + "gpu_noise_us_cpp": 2.5740000000000003, + "gpu_noise_us_python": 2.7920000000000003, + "gpu_bwutil_cpp": 0.4686035194239415, + "gpu_bwutil_python": 0.46121731729277016, + "gpu_gap_stddev_us": 2.123696941013937 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 640.0356294655, + "gpu_time_us_python": 654.6434775596802, + "gpu_noise_us_cpp": 2.154, + "gpu_noise_us_python": 3.242, + "gpu_bwutil_cpp": 0.4881107374378435, + "gpu_bwutil_python": 0.4772238396106756, + "gpu_gap_stddev_us": 1.7466760347272823 + } + } + } + }, + "pillowresize_anisotropic_expand_2160x3840_nchw_tensor_cubic_float4_c4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "4x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize float4 4-channel Tensor (NCHW) anisotropically from 864x480 to 3840x2160 with CUBIC. Advanced normalized mirror of pillowresize_planar_nchw_expand_cubic_1080p_float4_advanced; batch 16 scaled conservatively to 4." + }, + "baselines": { + "pillowresize_anisotropic_expand_2160x3840_nchw_tensor_cubic_float4_c4_advanced[InOutDataType=float4][layout=NCHW][shape=4x480x864][resizeType=TARGET_2160x3840][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1005.62724454334, + "gpu_time_us_python": 1022.3345950447001, + "gpu_noise_us_cpp": 3.316, + "gpu_noise_us_python": 3.4259999999999997, + "gpu_bwutil_cpp": 0.3563949205251703, + "gpu_bwutil_python": 0.35057366219697533, + "gpu_gap_stddev_us": 2.6176322734833213 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 750.19162237738, + "gpu_time_us_python": 766.11456706872, + "gpu_noise_us_cpp": 1.3800000000000001, + "gpu_noise_us_python": 2.8899999999999997, + "gpu_bwutil_cpp": 0.3644308905057174, + "gpu_bwutil_python": 0.35686262788469464, + "gpu_gap_stddev_us": 2.3797826720925688 + } + } + } + }, + "pillowresize_anisotropic_expand_2160x3840_nchwfake_tensor_cubic_float4_c4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "4x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize float4 4-channel Tensor (NCHW_FAKE) anisotropically from 864x480 to 3840x2160 with CUBIC. Advanced normalized mirror of pillowresize_fakeplanar_nchw_expand_cubic_1080p_float4_advanced; batch 16 scaled conservatively to 4." + }, + "baselines": { + "pillowresize_anisotropic_expand_2160x3840_nchwfake_tensor_cubic_float4_c4_advanced[InOutDataType=float4][layout=NCHW_FAKE][shape=4x480x864][resizeType=TARGET_2160x3840][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1817.9472809252202, + "gpu_time_us_python": 1834.5027392797601, + "gpu_noise_us_cpp": 3.81, + "gpu_noise_us_python": 3.338, + "gpu_bwutil_cpp": 0.5914365747617667, + "gpu_bwutil_python": 0.5860992867537715, + "gpu_gap_stddev_us": 1.289151410730814 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1470.82419191054, + "gpu_time_us_python": 1483.16583841486, + "gpu_noise_us_cpp": 3.4559999999999995, + "gpu_noise_us_python": 4.75, + "gpu_bwutil_cpp": 0.557568759207399, + "gpu_bwutil_python": 0.5529305223663533, + "gpu_gap_stddev_us": 0.8267370496845943 + } + } + } + }, + "pillowresize_anisotropic_expand_2160x3840_nhwc_tensor_cubic_float4_c4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "4x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize float4 4-channel Tensor (NHWC) anisotropically from 864x480 to 3840x2160 with CUBIC. Advanced normalized mirror of pillowresize_expand_cubic_1080p_float4_advanced; batch 16 scaled conservatively to 4." + }, + "baselines": { + "pillowresize_anisotropic_expand_2160x3840_nhwc_tensor_cubic_float4_c4_advanced[InOutDataType=float4][layout=NHWC][shape=4x480x864][resizeType=TARGET_2160x3840][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 966.40359912506, + "gpu_time_us_python": 981.55439713418, + "gpu_noise_us_cpp": 1.752, + "gpu_noise_us_python": 4.54, + "gpu_bwutil_cpp": 0.37085996837879837, + "gpu_bwutil_python": 0.36513702690203953, + "gpu_gap_stddev_us": 1.7615430882096716 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 809.3236550086601, + "gpu_time_us_python": 823.8016518261799, + "gpu_noise_us_cpp": 1.6960000000000002, + "gpu_noise_us_python": 3.188, + "gpu_bwutil_cpp": 0.33778586248546477, + "gpu_bwutil_python": 0.33185899103190775, + "gpu_gap_stddev_us": 2.366970812151366 + } + } + } + }, + "pillowresize_anisotropic_expand_2160x3840_nchw_tensor_linear_float4_c4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "4x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize float4 4-channel Tensor (NCHW) anisotropically from 864x480 to 3840x2160 with LINEAR. Advanced normalized mirror of pillowresize_planar_nchw_expand_linear_1080p_float4_advanced; batch 16 scaled conservatively to 4." + }, + "baselines": { + "pillowresize_anisotropic_expand_2160x3840_nchw_tensor_linear_float4_c4_advanced[InOutDataType=float4][layout=NCHW][shape=4x480x864][resizeType=TARGET_2160x3840][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 902.4287956108999, + "gpu_time_us_python": 920.35586396138, + "gpu_noise_us_cpp": 2.1059999999999994, + "gpu_noise_us_python": 4.608, + "gpu_bwutil_cpp": 0.39715073500125636, + "gpu_bwutil_python": 0.3894164721820161, + "gpu_gap_stddev_us": 1.7087771919228634 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 734.12380022668, + "gpu_time_us_python": 752.5809066816399, + "gpu_noise_us_cpp": 1.5080000000000002, + "gpu_noise_us_python": 2.65, + "gpu_bwutil_cpp": 0.37238897020437156, + "gpu_bwutil_python": 0.3632739639237192, + "gpu_gap_stddev_us": 4.330086637306234 + } + } + } + }, + "pillowresize_anisotropic_expand_2160x3840_nchwfake_tensor_linear_float4_c4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "4x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize float4 4-channel Tensor (NCHW_FAKE) anisotropically from 864x480 to 3840x2160 with LINEAR. Advanced normalized mirror of pillowresize_fakeplanar_nchw_expand_linear_1080p_float4_advanced; batch 16 scaled conservatively to 4." + }, + "baselines": { + "pillowresize_anisotropic_expand_2160x3840_nchwfake_tensor_linear_float4_c4_advanced[InOutDataType=float4][layout=NCHW_FAKE][shape=4x480x864][resizeType=TARGET_2160x3840][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1719.2917760103799, + "gpu_time_us_python": 1735.10089479638, + "gpu_noise_us_cpp": 2.8560000000000003, + "gpu_noise_us_python": 5.595999999999999, + "gpu_bwutil_cpp": 0.6253739900965212, + "gpu_bwutil_python": 0.6196758616194598, + "gpu_gap_stddev_us": 0.9128554280214839 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1294.07159436954, + "gpu_time_us_python": 1307.9761938249399, + "gpu_noise_us_cpp": 4.128, + "gpu_noise_us_python": 6.4079999999999995, + "gpu_bwutil_cpp": 0.6337329011156642, + "gpu_bwutil_python": 0.6269982571877897, + "gpu_gap_stddev_us": 1.8228256730378227 + } + } + } + }, + "pillowresize_anisotropic_expand_2160x3840_nhwc_tensor_linear_float4_c4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "4x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize float4 4-channel Tensor (NHWC) anisotropically from 864x480 to 3840x2160 with LINEAR. Advanced normalized mirror of pillowresize_expand_linear_1080p_float4_advanced; batch 16 scaled conservatively to 4." + }, + "baselines": { + "pillowresize_anisotropic_expand_2160x3840_nhwc_tensor_linear_float4_c4_advanced[InOutDataType=float4][layout=NHWC][shape=4x480x864][resizeType=TARGET_2160x3840][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 866.5739845239001, + "gpu_time_us_python": 882.53885983652, + "gpu_noise_us_cpp": 1.3820000000000001, + "gpu_noise_us_python": 3.2700000000000005, + "gpu_bwutil_cpp": 0.4135830753357788, + "gpu_bwutil_python": 0.40610857688715696, + "gpu_gap_stddev_us": 3.341045103490973 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 632.2928655238601, + "gpu_time_us_python": 649.2527035781999, + "gpu_noise_us_cpp": 1.508, + "gpu_noise_us_python": 3.778, + "gpu_bwutil_cpp": 0.4323905858697903, + "gpu_bwutil_python": 0.42110847704271626, + "gpu_gap_stddev_us": 3.0691436040418303 + } + } + } + }, + "pillowresize_anisotropic_contract_480x864_nchw_tensor_cubic_uchar3_c3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "48x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize uchar3 3-channel Tensor (NCHW) anisotropically from 1920x1080 to 864x480 with CUBIC. Advanced normalized mirror of pillowresize_planar_nchw_contract_cubic_1080p_uchar3_basic; batch 96 scaled conservatively to 48." + }, + "baselines": { + "pillowresize_anisotropic_contract_480x864_nchw_tensor_cubic_uchar3_c3_advanced[InOutDataType=uchar3][layout=NCHW][shape=48x1080x1920][resizeType=TARGET_480x864][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1624.66451016436, + "gpu_time_us_python": 1641.70233008062, + "gpu_noise_us_cpp": 2.2920000000000003, + "gpu_noise_us_python": 4.2780000000000005, + "gpu_bwutil_cpp": 0.14181424591122208, + "gpu_bwutil_python": 0.14034357997731362, + "gpu_gap_stddev_us": 4.956653680969745 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1357.410512686, + "gpu_time_us_python": 1373.78062123064, + "gpu_noise_us_cpp": 0.932, + "gpu_noise_us_python": 2.768, + "gpu_bwutil_cpp": 0.12950638905673678, + "gpu_bwutil_python": 0.12796218847432958, + "gpu_gap_stddev_us": 1.9407532178440525 + } + } + } + }, + "pillowresize_anisotropic_contract_480x864_nchw_varshape_cubic_uchar3_c3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize uchar3 3-channel VarShape (NCHW) anisotropically from 1920x1080 to 864x480 with CUBIC. Advanced normalized mirror of pillowresize_planar_nchw_contract_cubic_1080p_varshape_uchar3_basic; batch 96 scaled conservatively to 32." + }, + "baselines": { + "pillowresize_anisotropic_contract_480x864_nchw_varshape_cubic_uchar3_c3_advanced[InOutDataType=uchar3][layout=NCHW][shape=32x1080x1920][resizeType=TARGET_480x864][interpolation=CUBIC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1668.44145327286, + "gpu_time_us_python": 1687.6128190489603, + "gpu_noise_us_cpp": 4.6579999999999995, + "gpu_noise_us_python": 4.242, + "gpu_bwutil_cpp": 0.09206375260232406, + "gpu_bwutil_python": 0.09101855422835149, + "gpu_gap_stddev_us": 2.2216655221978954 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1321.12272924736, + "gpu_time_us_python": 1345.5014856745202, + "gpu_noise_us_cpp": 2.25, + "gpu_noise_us_python": 3.9739999999999993, + "gpu_bwutil_cpp": 0.08870509816560117, + "gpu_bwutil_python": 0.0871056731635133, + "gpu_gap_stddev_us": 5.685498374756044 + } + } + } + }, + "pillowresize_anisotropic_contract_480x864_nchwfake_tensor_cubic_uchar3_c3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "48x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize uchar3 3-channel Tensor (NCHW_FAKE) anisotropically from 1920x1080 to 864x480 with CUBIC. Advanced normalized mirror of pillowresize_fakeplanar_nchw_contract_cubic_1080p_uchar3_advanced; batch 96 scaled conservatively to 48." + }, + "baselines": { + "pillowresize_anisotropic_contract_480x864_nchwfake_tensor_cubic_uchar3_c3_advanced[InOutDataType=uchar3][layout=NCHW_FAKE][shape=48x1080x1920][resizeType=TARGET_480x864][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2813.45031824, + "gpu_time_us_python": 2831.93760561134, + "gpu_noise_us_cpp": 3.012, + "gpu_noise_us_python": 3.124, + "gpu_bwutil_cpp": 0.24567753479721594, + "gpu_bwutil_python": 0.24407349925878857, + "gpu_gap_stddev_us": 1.1522185418938353 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2300.05660161994, + "gpu_time_us_python": 2315.3489501964204, + "gpu_noise_us_cpp": 1.556, + "gpu_noise_us_python": 4.372, + "gpu_bwutil_cpp": 0.22925308988843446, + "gpu_bwutil_python": 0.22773915801369532, + "gpu_gap_stddev_us": 2.1971939658936064 + } + } + } + }, + "pillowresize_anisotropic_contract_480x864_nchw_tensor_linear_uchar3_c3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "48x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize uchar3 3-channel Tensor (NCHW) anisotropically from 1920x1080 to 864x480 with LINEAR. Advanced normalized mirror of pillowresize_planar_nchw_contract_linear_1080p_uchar3_basic; batch 96 scaled conservatively to 48." + }, + "baselines": { + "pillowresize_anisotropic_contract_480x864_nchw_tensor_linear_uchar3_c3_advanced[InOutDataType=uchar3][layout=NCHW][shape=48x1080x1920][resizeType=TARGET_480x864][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1231.1350248054998, + "gpu_time_us_python": 1246.65822096916, + "gpu_noise_us_cpp": 1.3859999999999997, + "gpu_noise_us_python": 5.436, + "gpu_bwutil_cpp": 0.18714477586538794, + "gpu_bwutil_python": 0.184814830025874, + "gpu_gap_stddev_us": 1.620889065001004 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1064.24974693124, + "gpu_time_us_python": 1081.4868276412599, + "gpu_noise_us_cpp": 2.9680000000000004, + "gpu_noise_us_python": 3.178, + "gpu_bwutil_cpp": 0.16517216493565257, + "gpu_bwutil_python": 0.1625427559358971, + "gpu_gap_stddev_us": 2.2915819639208563 + } + } + } + }, + "pillowresize_anisotropic_contract_480x864_nchwfake_tensor_linear_uchar3_c3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "48x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize uchar3 3-channel Tensor (NCHW_FAKE) anisotropically from 1920x1080 to 864x480 with LINEAR. Advanced normalized mirror of pillowresize_fakeplanar_nchw_contract_linear_1080p_uchar3_advanced; batch 96 scaled conservatively to 48." + }, + "baselines": { + "pillowresize_anisotropic_contract_480x864_nchwfake_tensor_linear_uchar3_c3_advanced[InOutDataType=uchar3][layout=NCHW_FAKE][shape=48x1080x1920][resizeType=TARGET_480x864][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2462.86247992042, + "gpu_time_us_python": 2480.68161813334, + "gpu_noise_us_cpp": 2.762, + "gpu_noise_us_python": 3.4, + "gpu_bwutil_cpp": 0.2806493832660316, + "gpu_bwutil_python": 0.27863349345269695, + "gpu_gap_stddev_us": 2.405494033249843 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1976.53919114906, + "gpu_time_us_python": 1990.96003815876, + "gpu_noise_us_cpp": 2.6299999999999994, + "gpu_noise_us_python": 3.8259999999999996, + "gpu_bwutil_cpp": 0.2667667664479726, + "gpu_bwutil_python": 0.26483768261896606, + "gpu_gap_stddev_us": 2.0281999736034106 + } + } + } + }, + "pillowresize_anisotropic_contract_480x864_nhwc_tensor_linear_uchar3_c3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "48x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize uchar3 3-channel Tensor (NHWC) anisotropically from 1920x1080 to 864x480 with LINEAR. Advanced normalized mirror of pillowresize_contract_linear_1080p_uchar3_basic; batch 96 scaled conservatively to 48." + }, + "baselines": { + "pillowresize_anisotropic_contract_480x864_nhwc_tensor_linear_uchar3_c3_advanced[InOutDataType=uchar3][layout=NHWC][shape=48x1080x1920][resizeType=TARGET_480x864][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1140.82815109516, + "gpu_time_us_python": 1155.7422583797202, + "gpu_noise_us_cpp": 2.5260000000000002, + "gpu_noise_us_python": 3.9819999999999993, + "gpu_bwutil_cpp": 0.20195908412237737, + "gpu_bwutil_python": 0.19935318214736464, + "gpu_gap_stddev_us": 0.9563814303109269 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 959.5089389532001, + "gpu_time_us_python": 975.30594058654, + "gpu_noise_us_cpp": 1.048, + "gpu_noise_us_python": 2.94, + "gpu_bwutil_cpp": 0.18321680713019758, + "gpu_bwutil_python": 0.18024389467655194, + "gpu_gap_stddev_us": 3.056129479152607 + } + } + } + }, + "pillowresize_anisotropic_expand_2160x3840_nchw_tensor_cubic_uchar3_c3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "12x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize uchar3 3-channel Tensor (NCHW) anisotropically from 864x480 to 3840x2160 with CUBIC. Advanced normalized mirror of pillowresize_planar_nchw_expand_cubic_1080p_uchar3_basic; batch 48 scaled conservatively to 12." + }, + "baselines": { + "pillowresize_anisotropic_expand_2160x3840_nchw_tensor_cubic_uchar3_c3_advanced[InOutDataType=uchar3][layout=NCHW][shape=12x480x864][resizeType=TARGET_2160x3840][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1404.10111036968, + "gpu_time_us_python": 1420.12084275172, + "gpu_noise_us_cpp": 1.48, + "gpu_noise_us_python": 3.806, + "gpu_bwutil_cpp": 0.14357978067874122, + "gpu_bwutil_python": 0.1419604196202276, + "gpu_gap_stddev_us": 2.8767344886966884 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1252.05551992688, + "gpu_time_us_python": 1267.7840724082998, + "gpu_noise_us_cpp": 1.1780000000000002, + "gpu_noise_us_python": 5.351999999999999, + "gpu_bwutil_cpp": 0.12285064455620072, + "gpu_bwutil_python": 0.12132569347350682, + "gpu_gap_stddev_us": 5.161194068355752 + } + } + } + }, + "pillowresize_anisotropic_expand_2160x3840_nchwfake_tensor_cubic_uchar3_c3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "12x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize uchar3 3-channel Tensor (NCHW_FAKE) anisotropically from 864x480 to 3840x2160 with CUBIC. Advanced normalized mirror of pillowresize_fakeplanar_nchw_expand_cubic_1080p_uchar3_advanced; batch 48 scaled conservatively to 12." + }, + "baselines": { + "pillowresize_anisotropic_expand_2160x3840_nchwfake_tensor_cubic_uchar3_c3_advanced[InOutDataType=uchar3][layout=NCHW_FAKE][shape=12x480x864][resizeType=TARGET_2160x3840][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2120.9715544675996, + "gpu_time_us_python": 2141.05277102216, + "gpu_noise_us_cpp": 1.8980000000000001, + "gpu_noise_us_python": 4.146, + "gpu_bwutil_cpp": 0.2851527621908647, + "gpu_bwutil_python": 0.28247823240784087, + "gpu_gap_stddev_us": 2.892369753865936 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1733.5499624119998, + "gpu_time_us_python": 1748.8340515954399, + "gpu_noise_us_cpp": 1.41, + "gpu_noise_us_python": 4.112, + "gpu_bwutil_cpp": 0.2661631530439122, + "gpu_bwutil_python": 0.2638376820626566, + "gpu_gap_stddev_us": 1.8304706035389877 + } + } + } + }, + "pillowresize_anisotropic_expand_2160x3840_nchw_tensor_linear_uchar3_c3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "12x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize uchar3 3-channel Tensor (NCHW) anisotropically from 864x480 to 3840x2160 with LINEAR. Advanced normalized mirror of pillowresize_planar_nchw_expand_linear_1080p_uchar3_basic; batch 48 scaled conservatively to 12." + }, + "baselines": { + "pillowresize_anisotropic_expand_2160x3840_nchw_tensor_linear_uchar3_c3_advanced[InOutDataType=uchar3][layout=NCHW][shape=12x480x864][resizeType=TARGET_2160x3840][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1373.7419184532598, + "gpu_time_us_python": 1387.65662095526, + "gpu_noise_us_cpp": 2.466, + "gpu_noise_us_python": 3.164, + "gpu_bwutil_cpp": 0.14675268739992128, + "gpu_bwutil_python": 0.14528142512069514, + "gpu_gap_stddev_us": 1.8304654416630515 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1007.0084544178801, + "gpu_time_us_python": 1024.4850089895401, + "gpu_noise_us_cpp": 1.528, + "gpu_noise_us_python": 4.16, + "gpu_bwutil_cpp": 0.15272971292297366, + "gpu_bwutil_python": 0.15012944168683223, + "gpu_gap_stddev_us": 2.5353176874328773 + } + } + } + }, + "pillowresize_anisotropic_expand_2160x3840_nchwfake_tensor_linear_uchar3_c3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "12x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize uchar3 3-channel Tensor (NCHW_FAKE) anisotropically from 864x480 to 3840x2160 with LINEAR. Advanced normalized mirror of pillowresize_fakeplanar_nchw_expand_linear_1080p_uchar3_advanced; batch 48 scaled conservatively to 12." + }, + "baselines": { + "pillowresize_anisotropic_expand_2160x3840_nchwfake_tensor_linear_uchar3_c3_advanced[InOutDataType=uchar3][layout=NCHW_FAKE][shape=12x480x864][resizeType=TARGET_2160x3840][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2087.5911756356, + "gpu_time_us_python": 2104.02487374482, + "gpu_noise_us_cpp": 3.364, + "gpu_noise_us_python": 5.215999999999999, + "gpu_bwutil_cpp": 0.28971212297043736, + "gpu_bwutil_python": 0.2874493273128666, + "gpu_gap_stddev_us": 1.4683102381165782 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1565.8176158131403, + "gpu_time_us_python": 1582.41863092498, + "gpu_noise_us_cpp": 3.774, + "gpu_noise_us_python": 5.254, + "gpu_bwutil_cpp": 0.2946544291271692, + "gpu_bwutil_python": 0.29157049865350115, + "gpu_gap_stddev_us": 3.785027242834 + } + } + } + }, + "pillowresize_anisotropic_expand_2160x3840_nhwc_tensor_linear_uchar3_c3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "12x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize uchar3 3-channel Tensor (NHWC) anisotropically from 864x480 to 3840x2160 with LINEAR. Advanced normalized mirror of pillowresize_expand_linear_1080p_uchar3_basic; batch 48 scaled conservatively to 12." + }, + "baselines": { + "pillowresize_anisotropic_expand_2160x3840_nhwc_tensor_linear_uchar3_c3_advanced[InOutDataType=uchar3][layout=NHWC][shape=12x480x864][resizeType=TARGET_2160x3840][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1442.43937073078, + "gpu_time_us_python": 1459.7818195430202, + "gpu_noise_us_cpp": 3.184, + "gpu_noise_us_python": 4.786, + "gpu_bwutil_cpp": 0.13976334871702872, + "gpu_bwutil_python": 0.1381034633586653, + "gpu_gap_stddev_us": 3.150569462326022 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1038.96268563928, + "gpu_time_us_python": 1055.2795754902202, + "gpu_noise_us_cpp": 1.6199999999999999, + "gpu_noise_us_python": 2.4259999999999997, + "gpu_bwutil_cpp": 0.148045498725923, + "gpu_bwutil_python": 0.14575844979213998, + "gpu_gap_stddev_us": 1.9628018052644727 + } + } + } + }, + "pillowresize_anisotropic_contract_480x864_nchw_tensor_cubic_uchar4_c4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "48x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize uchar4 4-channel Tensor (NCHW) anisotropically from 1920x1080 to 864x480 with CUBIC. Advanced normalized mirror of pillowresize_planar_nchw_contract_cubic_1080p_uchar4_advanced; batch 96 scaled conservatively to 48." + }, + "baselines": { + "pillowresize_anisotropic_contract_480x864_nchw_tensor_cubic_uchar4_c4_advanced[InOutDataType=uchar4][layout=NCHW][shape=48x1080x1920][resizeType=TARGET_480x864][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2074.2753891905, + "gpu_time_us_python": 2089.27397966382, + "gpu_noise_us_cpp": 1.378, + "gpu_noise_us_python": 3.4519999999999995, + "gpu_bwutil_cpp": 0.14810030567498467, + "gpu_bwutil_python": 0.14703731506943293, + "gpu_gap_stddev_us": 3.868501142119327 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1536.79876599912, + "gpu_time_us_python": 1554.80142705232, + "gpu_noise_us_cpp": 1.124, + "gpu_noise_us_python": 3.0919999999999996, + "gpu_bwutil_cpp": 0.15251153861076433, + "gpu_bwutil_python": 0.15074825887055762, + "gpu_gap_stddev_us": 2.374628102120065 + } + } + } + }, + "pillowresize_anisotropic_contract_480x864_nchwfake_tensor_cubic_uchar4_c4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "48x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize uchar4 4-channel Tensor (NCHW_FAKE) anisotropically from 1920x1080 to 864x480 with CUBIC. Advanced normalized mirror of pillowresize_fakeplanar_nchw_contract_cubic_1080p_uchar4_advanced; batch 96 scaled conservatively to 48." + }, + "baselines": { + "pillowresize_anisotropic_contract_480x864_nchwfake_tensor_cubic_uchar4_c4_advanced[InOutDataType=uchar4][layout=NCHW_FAKE][shape=48x1080x1920][resizeType=TARGET_480x864][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3347.2675609587995, + "gpu_time_us_python": 3362.67678260798, + "gpu_noise_us_cpp": 2.3579999999999997, + "gpu_noise_us_python": 2.7439999999999998, + "gpu_bwutil_cpp": 0.27532948608563956, + "gpu_bwutil_python": 0.274067679658545, + "gpu_gap_stddev_us": 2.0707834432266106 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2811.82062925426, + "gpu_time_us_python": 2826.7178800027596, + "gpu_noise_us_cpp": 1.7100000000000002, + "gpu_noise_us_python": 5.014, + "gpu_bwutil_cpp": 0.25003073109557394, + "gpu_bwutil_python": 0.24871275830815995, + "gpu_gap_stddev_us": 0.7020260445034358 + } + } + } + }, + "pillowresize_anisotropic_contract_480x864_nhwc_tensor_cubic_uchar4_c4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "48x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize uchar4 4-channel Tensor (NHWC) anisotropically from 1920x1080 to 864x480 with CUBIC. Advanced normalized mirror of pillowresize_contract_cubic_1080p_uchar4_advanced; batch 96 scaled conservatively to 48." + }, + "baselines": { + "pillowresize_anisotropic_contract_480x864_nhwc_tensor_cubic_uchar4_c4_advanced[InOutDataType=uchar4][layout=NHWC][shape=48x1080x1920][resizeType=TARGET_480x864][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1607.61435887748, + "gpu_time_us_python": 1624.27205952328, + "gpu_noise_us_cpp": 2.29, + "gpu_noise_us_python": 3.1339999999999995, + "gpu_bwutil_cpp": 0.19109103031694022, + "gpu_bwutil_python": 0.18913229556544525, + "gpu_gap_stddev_us": 2.732948226784582 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1469.5862283863398, + "gpu_time_us_python": 1487.61509995704, + "gpu_noise_us_cpp": 3.9240000000000004, + "gpu_noise_us_python": 5.159999999999999, + "gpu_bwutil_cpp": 0.15949410285404308, + "gpu_bwutil_python": 0.15756690484437613, + "gpu_gap_stddev_us": 2.9947829510659685 + } + } + } + }, + "pillowresize_anisotropic_contract_480x864_nhwc_varshape_cubic_uchar4_c4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize uchar4 4-channel VarShape (NHWC) anisotropically from 1920x1080 to 864x480 with CUBIC. Advanced normalized mirror of pillowresize_contract_cubic_1080p_varshape_uchar4_advanced; batch 96 scaled conservatively to 32." + }, + "baselines": { + "pillowresize_anisotropic_contract_480x864_nhwc_varshape_cubic_uchar4_c4_advanced[InOutDataType=uchar4][layout=NHWC][shape=32x1080x1920][resizeType=TARGET_480x864][interpolation=CUBIC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1156.2302500108601, + "gpu_time_us_python": 1179.1549300890802, + "gpu_noise_us_cpp": 4.482, + "gpu_noise_us_python": 5.0280000000000005, + "gpu_bwutil_cpp": 0.1771357971453254, + "gpu_bwutil_python": 0.17369488441881745, + "gpu_gap_stddev_us": 2.486314034304106 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1124.05292933176, + "gpu_time_us_python": 1147.47320049328, + "gpu_noise_us_cpp": 2.492, + "gpu_noise_us_python": 4.582, + "gpu_bwutil_cpp": 0.13902293896209206, + "gpu_bwutil_python": 0.13619676563374847, + "gpu_gap_stddev_us": 4.2275355388226465 + } + } + } + }, + "pillowresize_anisotropic_contract_480x864_nchw_tensor_linear_uchar4_c4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "48x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize uchar4 4-channel Tensor (NCHW) anisotropically from 1920x1080 to 864x480 with LINEAR. Advanced normalized mirror of pillowresize_planar_nchw_contract_linear_1080p_uchar4_advanced; batch 96 scaled conservatively to 48." + }, + "baselines": { + "pillowresize_anisotropic_contract_480x864_nchw_tensor_linear_uchar4_c4_advanced[InOutDataType=uchar4][layout=NCHW][shape=48x1080x1920][resizeType=TARGET_480x864][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1533.27590394618, + "gpu_time_us_python": 1548.99846727458, + "gpu_noise_us_cpp": 1.596, + "gpu_noise_us_python": 3.718, + "gpu_bwutil_cpp": 0.20035553738107176, + "gpu_bwutil_python": 0.19832208404975243, + "gpu_gap_stddev_us": 1.5404424670871333 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1147.99054803344, + "gpu_time_us_python": 1165.06490171214, + "gpu_noise_us_cpp": 1.204, + "gpu_noise_us_python": 2.8800000000000003, + "gpu_bwutil_cpp": 0.20415161795837644, + "gpu_bwutil_python": 0.20115879421038532, + "gpu_gap_stddev_us": 1.3775670748325453 + } + } + } + }, + "pillowresize_anisotropic_contract_480x864_nchwfake_tensor_linear_uchar4_c4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "48x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize uchar4 4-channel Tensor (NCHW_FAKE) anisotropically from 1920x1080 to 864x480 with LINEAR. Advanced normalized mirror of pillowresize_fakeplanar_nchw_contract_linear_1080p_uchar4_advanced; batch 96 scaled conservatively to 48." + }, + "baselines": { + "pillowresize_anisotropic_contract_480x864_nchwfake_tensor_linear_uchar4_c4_advanced[InOutDataType=uchar4][layout=NCHW_FAKE][shape=48x1080x1920][resizeType=TARGET_480x864][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2854.84687469218, + "gpu_time_us_python": 2871.82744747662, + "gpu_noise_us_cpp": 2.932, + "gpu_noise_us_python": 4.401999999999999, + "gpu_bwutil_cpp": 0.32281971538085974, + "gpu_bwutil_python": 0.3209110700480099, + "gpu_gap_stddev_us": 1.7441831185388668 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2429.1266735812396, + "gpu_time_us_python": 2444.55101807284, + "gpu_noise_us_cpp": 3.3679999999999994, + "gpu_noise_us_python": 2.582, + "gpu_bwutil_cpp": 0.2894100723610258, + "gpu_bwutil_python": 0.287587796606869, + "gpu_gap_stddev_us": 2.4238845075446913 + } + } + } + }, + "pillowresize_anisotropic_contract_480x864_nhwc_tensor_linear_uchar4_c4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "48x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize uchar4 4-channel Tensor (NHWC) anisotropically from 1920x1080 to 864x480 with LINEAR. Advanced normalized mirror of pillowresize_contract_linear_1080p_uchar4_advanced; batch 96 scaled conservatively to 48." + }, + "baselines": { + "pillowresize_anisotropic_contract_480x864_nhwc_tensor_linear_uchar4_c4_advanced[InOutDataType=uchar4][layout=NHWC][shape=48x1080x1920][resizeType=TARGET_480x864][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1116.5137784064198, + "gpu_time_us_python": 1132.3842712946603, + "gpu_noise_us_cpp": 1.386, + "gpu_noise_us_python": 3.686, + "gpu_bwutil_cpp": 0.275142691086122, + "gpu_bwutil_python": 0.27128944181010084, + "gpu_gap_stddev_us": 3.628068508196693 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1090.0276371276, + "gpu_time_us_python": 1107.12023406384, + "gpu_noise_us_cpp": 1.2719999999999998, + "gpu_noise_us_python": 3.8659999999999997, + "gpu_bwutil_cpp": 0.21503254532744567, + "gpu_bwutil_python": 0.21171363556489994, + "gpu_gap_stddev_us": 4.226856566302453 + } + } + } + }, + "pillowresize_anisotropic_expand_2160x3840_nchw_tensor_cubic_uchar4_c4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "12x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize uchar4 4-channel Tensor (NCHW) anisotropically from 864x480 to 3840x2160 with CUBIC. Advanced normalized mirror of pillowresize_planar_nchw_expand_cubic_1080p_uchar4_advanced; batch 48 scaled conservatively to 12." + }, + "baselines": { + "pillowresize_anisotropic_expand_2160x3840_nchw_tensor_cubic_uchar4_c4_advanced[InOutDataType=uchar4][layout=NCHW][shape=12x480x864][resizeType=TARGET_2160x3840][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1789.92713298112, + "gpu_time_us_python": 1805.7599131151396, + "gpu_noise_us_cpp": 2.15, + "gpu_noise_us_python": 3.656, + "gpu_bwutil_cpp": 0.15017407274726852, + "gpu_bwutil_python": 0.14885762417036547, + "gpu_gap_stddev_us": 3.2411191525024523 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1537.1499871876201, + "gpu_time_us_python": 1552.96780919832, + "gpu_noise_us_cpp": 2.2920000000000003, + "gpu_noise_us_python": 3.0379999999999994, + "gpu_bwutil_cpp": 0.13341826284672298, + "gpu_bwutil_python": 0.1320593229252901, + "gpu_gap_stddev_us": 2.601364270354903 + } + } + } + }, + "pillowresize_anisotropic_expand_2160x3840_nchwfake_tensor_cubic_uchar4_c4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "12x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize uchar4 4-channel Tensor (NCHW_FAKE) anisotropically from 864x480 to 3840x2160 with CUBIC. Advanced normalized mirror of pillowresize_fakeplanar_nchw_expand_cubic_1080p_uchar4_advanced; batch 48 scaled conservatively to 12." + }, + "baselines": { + "pillowresize_anisotropic_expand_2160x3840_nchwfake_tensor_cubic_uchar4_c4_advanced[InOutDataType=uchar4][layout=NCHW_FAKE][shape=12x480x864][resizeType=TARGET_2160x3840][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2890.1965218028404, + "gpu_time_us_python": 2907.24701339688, + "gpu_noise_us_cpp": 1.3539999999999999, + "gpu_noise_us_python": 3.022, + "gpu_bwutil_cpp": 0.2790124408321158, + "gpu_bwutil_python": 0.2773761263835776, + "gpu_gap_stddev_us": 1.4359574806459552 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2477.61752385796, + "gpu_time_us_python": 2494.93561197388, + "gpu_noise_us_cpp": 5.247999999999999, + "gpu_noise_us_python": 3.464, + "gpu_bwutil_cpp": 0.24829278124888066, + "gpu_bwutil_python": 0.24657480790240277, + "gpu_gap_stddev_us": 4.968483841988111 + } + } + } + }, + "pillowresize_anisotropic_expand_2160x3840_nhwc_tensor_cubic_uchar4_c4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "12x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize uchar4 4-channel Tensor (NHWC) anisotropically from 864x480 to 3840x2160 with CUBIC. Advanced normalized mirror of pillowresize_expand_cubic_1080p_uchar4_advanced; batch 48 scaled conservatively to 12." + }, + "baselines": { + "pillowresize_anisotropic_expand_2160x3840_nhwc_tensor_cubic_uchar4_c4_advanced[InOutDataType=uchar4][layout=NHWC][shape=12x480x864][resizeType=TARGET_2160x3840][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2034.56686134256, + "gpu_time_us_python": 2051.84221885112, + "gpu_noise_us_cpp": 3.558, + "gpu_noise_us_python": 4.354, + "gpu_bwutil_cpp": 0.1321167490229212, + "gpu_bwutil_python": 0.13100491981179802, + "gpu_gap_stddev_us": 3.745234221221873 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1819.0782995177, + "gpu_time_us_python": 1838.1213167386202, + "gpu_noise_us_cpp": 4.4719999999999995, + "gpu_noise_us_python": 3.4579999999999997, + "gpu_bwutil_cpp": 0.11273395676304407, + "gpu_bwutil_python": 0.11156980766197369, + "gpu_gap_stddev_us": 4.29844879484639 + } + } + } + }, + "pillowresize_anisotropic_expand_2160x3840_nchw_tensor_linear_uchar4_c4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "layout": [ + "NCHW" + ], + "shape": [ + "12x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize uchar4 4-channel Tensor (NCHW) anisotropically from 864x480 to 3840x2160 with LINEAR. Advanced normalized mirror of pillowresize_planar_nchw_expand_linear_1080p_uchar4_advanced; batch 48 scaled conservatively to 12." + }, + "baselines": { + "pillowresize_anisotropic_expand_2160x3840_nchw_tensor_linear_uchar4_c4_advanced[InOutDataType=uchar4][layout=NCHW][shape=12x480x864][resizeType=TARGET_2160x3840][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1740.6378866897, + "gpu_time_us_python": 1757.3658976521, + "gpu_noise_us_cpp": 1.4579999999999997, + "gpu_noise_us_python": 2.896, + "gpu_bwutil_cpp": 0.1544263680586487, + "gpu_bwutil_python": 0.15295661196506755, + "gpu_gap_stddev_us": 1.2498530041810259 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1457.7880093817398, + "gpu_time_us_python": 1477.7825908157, + "gpu_noise_us_cpp": 3.4480000000000004, + "gpu_noise_us_python": 3.168, + "gpu_bwutil_cpp": 0.14066172534348267, + "gpu_bwutil_python": 0.13876200144650136, + "gpu_gap_stddev_us": 3.6805622186464224 + } + } + } + }, + "pillowresize_anisotropic_expand_2160x3840_nchwfake_tensor_linear_uchar4_c4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "layout": [ + "NCHW_FAKE" + ], + "shape": [ + "12x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize uchar4 4-channel Tensor (NCHW_FAKE) anisotropically from 864x480 to 3840x2160 with LINEAR. Advanced normalized mirror of pillowresize_fakeplanar_nchw_expand_linear_1080p_uchar4_advanced; batch 48 scaled conservatively to 12." + }, + "baselines": { + "pillowresize_anisotropic_expand_2160x3840_nchwfake_tensor_linear_uchar4_c4_advanced[InOutDataType=uchar4][layout=NCHW_FAKE][shape=12x480x864][resizeType=TARGET_2160x3840][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2701.60715400528, + "gpu_time_us_python": 2720.0192489485403, + "gpu_noise_us_cpp": 1.7559999999999998, + "gpu_noise_us_python": 4.422, + "gpu_bwutil_cpp": 0.29848919409835695, + "gpu_bwutil_python": 0.2964694697924253, + "gpu_gap_stddev_us": 4.948346746013197 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2226.8686502153396, + "gpu_time_us_python": 2241.70342452796, + "gpu_noise_us_cpp": 1.356, + "gpu_noise_us_python": 2.842, + "gpu_bwutil_cpp": 0.2762410627606273, + "gpu_bwutil_python": 0.2744107340676026, + "gpu_gap_stddev_us": 2.3160030527273077 + } + } + } + }, + "pillowresize_anisotropic_expand_2160x3840_nhwc_tensor_linear_uchar4_c4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "12x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize uchar4 4-channel Tensor (NHWC) anisotropically from 864x480 to 3840x2160 with LINEAR. Advanced normalized mirror of pillowresize_expand_linear_1080p_uchar4_advanced; batch 48 scaled conservatively to 12." + }, + "baselines": { + "pillowresize_anisotropic_expand_2160x3840_nhwc_tensor_linear_uchar4_c4_advanced[InOutDataType=uchar4][layout=NHWC][shape=12x480x864][resizeType=TARGET_2160x3840][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1846.40902824256, + "gpu_time_us_python": 1862.6363173672003, + "gpu_noise_us_cpp": 1.8099999999999998, + "gpu_noise_us_python": 2.774, + "gpu_bwutil_cpp": 0.1455800761699971, + "gpu_bwutil_python": 0.1443120183821495, + "gpu_gap_stddev_us": 2.409291265437228 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1567.7579516824799, + "gpu_time_us_python": 1583.8709809976401, + "gpu_noise_us_cpp": 1.284, + "gpu_noise_us_python": 3.0220000000000002, + "gpu_bwutil_cpp": 0.13079930584454008, + "gpu_bwutil_python": 0.12946984005238715, + "gpu_gap_stddev_us": 1.9157137191449258 + } + } + } + }, + "pillowresize_anisotropic_contract_480x864_nhwc_tensor_box_uint8_c1_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "BOX" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize uint8 1-channel Tensor (NHWC) anisotropically from 1920x1080 to 864x480 with BOX. Advanced normalized mirror of pillowresize_contract_box_hamming_u8_1080p_advanced; batch 64 scaled conservatively to 32." + }, + "baselines": { + "pillowresize_anisotropic_contract_480x864_nhwc_tensor_box_uint8_c1_advanced[InOutDataType=uint8][layout=NHWC][shape=32x1080x1920][resizeType=TARGET_480x864][interpolation=BOX][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 429.9844559079, + "gpu_time_us_python": 445.27751131646, + "gpu_noise_us_cpp": 1.48, + "gpu_noise_us_python": 2.598, + "gpu_bwutil_cpp": 0.1190746181182033, + "gpu_bwutil_python": 0.11499194439348943, + "gpu_gap_stddev_us": 3.278495535481502 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 385.80121298974, + "gpu_time_us_python": 401.77562069472003, + "gpu_noise_us_cpp": 2.508, + "gpu_noise_us_python": 3.146, + "gpu_bwutil_cpp": 0.10125147857883161, + "gpu_bwutil_python": 0.09724144773762314, + "gpu_gap_stddev_us": 3.3744622802204094 + } + } + } + }, + "pillowresize_anisotropic_contract_480x864_nhwc_varshape_box_uint8_c1_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "52x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "BOX" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize uint8 1-channel VarShape (NHWC) anisotropically from 1920x1080 to 864x480 with BOX. Advanced normalized mirror of pillowresize_contract_box_hamming_u8_1080p_advanced; batch 52 calibrated for approximately 1 ms reference timing." + }, + "baselines": { + "pillowresize_anisotropic_contract_480x864_nhwc_varshape_box_uint8_c1_advanced[InOutDataType=uint8][layout=NHWC][shape=52x1080x1920][resizeType=TARGET_480x864][interpolation=BOX][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 895.5142234642401, + "gpu_time_us_python": 918.9496420785802, + "gpu_noise_us_cpp": 5.068, + "gpu_noise_us_python": 6.377999999999999, + "gpu_bwutil_cpp": 0.09291114833038586, + "gpu_bwutil_python": 0.09054821994157268, + "gpu_gap_stddev_us": 5.585901303060699 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 831.3344146356601, + "gpu_time_us_python": 858.1740009128, + "gpu_noise_us_cpp": 4.714, + "gpu_noise_us_python": 7.112, + "gpu_bwutil_cpp": 0.07635926273647284, + "gpu_bwutil_python": 0.07398638575195175, + "gpu_gap_stddev_us": 6.2235223551842855 + } + } + } + }, + "pillowresize_anisotropic_contract_480x864_nhwc_tensor_cubic_uint8_c1_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize uint8 1-channel Tensor (NHWC) anisotropically from 1920x1080 to 864x480 with CUBIC. Advanced normalized mirror of pillowresize_contract_cubic_1080p_scalar_advanced; batch 64 scaled conservatively to 32." + }, + "baselines": { + "pillowresize_anisotropic_contract_480x864_nhwc_tensor_cubic_uint8_c1_advanced[InOutDataType=uint8][layout=NHWC][shape=32x1080x1920][resizeType=TARGET_480x864][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 582.30665129418, + "gpu_time_us_python": 596.59210025986, + "gpu_noise_us_cpp": 1.4659999999999997, + "gpu_noise_us_python": 2.764, + "gpu_bwutil_cpp": 0.08792670779554539, + "gpu_bwutil_python": 0.08582268365151949, + "gpu_gap_stddev_us": 2.260672296149195 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 501.7293497387401, + "gpu_time_us_python": 517.0855586283, + "gpu_noise_us_cpp": 1.384, + "gpu_noise_us_python": 2.6199999999999997, + "gpu_bwutil_cpp": 0.07786264060431997, + "gpu_bwutil_python": 0.07555666478723713, + "gpu_gap_stddev_us": 2.5409064121156826 + } + } + } + }, + "pillowresize_anisotropic_contract_480x864_nhwc_varshape_cubic_uint8_c1_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "40x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize uint8 1-channel VarShape (NHWC) anisotropically from 1920x1080 to 864x480 with CUBIC. Advanced normalized mirror of pillowresize_contract_cubic_1080p_varshape_scalar_advanced; batch 40 calibrated for approximately 1 ms reference timing." + }, + "baselines": { + "pillowresize_anisotropic_contract_480x864_nhwc_varshape_cubic_uint8_c1_advanced[InOutDataType=uint8][layout=NHWC][shape=40x1080x1920][resizeType=TARGET_480x864][interpolation=CUBIC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 930.84587675906, + "gpu_time_us_python": 952.0633284163399, + "gpu_noise_us_cpp": 6.33, + "gpu_noise_us_python": 5.564, + "gpu_bwutil_cpp": 0.068759199360069, + "gpu_bwutil_python": 0.0672265734518188, + "gpu_gap_stddev_us": 1.2212765488046367 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 845.89723090238, + "gpu_time_us_python": 870.3182500309, + "gpu_noise_us_cpp": 4.863999999999999, + "gpu_noise_us_python": 6.794, + "gpu_bwutil_cpp": 0.0577329865336481, + "gpu_bwutil_python": 0.056114543850304344, + "gpu_gap_stddev_us": 2.1125126222862955 + } + } + } + }, + "pillowresize_anisotropic_contract_480x864_nhwc_tensor_hamming_uint8_c1_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "HAMMING" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize uint8 1-channel Tensor (NHWC) anisotropically from 1920x1080 to 864x480 with HAMMING. Advanced normalized mirror of pillowresize_contract_box_hamming_u8_1080p_advanced; batch 64 scaled conservatively to 32." + }, + "baselines": { + "pillowresize_anisotropic_contract_480x864_nhwc_tensor_hamming_uint8_c1_advanced[InOutDataType=uint8][layout=NHWC][shape=32x1080x1920][resizeType=TARGET_480x864][interpolation=HAMMING][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 466.69871332264, + "gpu_time_us_python": 481.51760659690007, + "gpu_noise_us_cpp": 2.274, + "gpu_noise_us_python": 2.906, + "gpu_bwutil_cpp": 0.10970736987299548, + "gpu_bwutil_python": 0.10633234456822549, + "gpu_gap_stddev_us": 1.832302532914528 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 434.87087149260003, + "gpu_time_us_python": 450.09201787532004, + "gpu_noise_us_cpp": 0.89, + "gpu_noise_us_python": 4.324, + "gpu_bwutil_cpp": 0.08983412790275469, + "gpu_bwutil_python": 0.08679814855243068, + "gpu_gap_stddev_us": 1.5847786405361788 + } + } + } + }, + "pillowresize_anisotropic_contract_480x864_nhwc_varshape_hamming_uint8_c1_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "48x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "HAMMING" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize uint8 1-channel VarShape (NHWC) anisotropically from 1920x1080 to 864x480 with HAMMING. Advanced normalized mirror of pillowresize_contract_box_hamming_u8_1080p_advanced; batch 48 calibrated for approximately 1 ms reference timing." + }, + "baselines": { + "pillowresize_anisotropic_contract_480x864_nhwc_varshape_hamming_uint8_c1_advanced[InOutDataType=uint8][layout=NHWC][shape=48x1080x1920][resizeType=TARGET_480x864][interpolation=HAMMING][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 897.4253929591799, + "gpu_time_us_python": 919.8197485268399, + "gpu_noise_us_cpp": 5.136, + "gpu_noise_us_python": 3.772, + "gpu_bwutil_cpp": 0.08558210941841873, + "gpu_bwutil_python": 0.08350212267976204, + "gpu_gap_stddev_us": 3.141941023021052 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 839.4891854309601, + "gpu_time_us_python": 865.5135278069199, + "gpu_noise_us_cpp": 4.898, + "gpu_noise_us_python": 4.504, + "gpu_bwutil_cpp": 0.06980644946624781, + "gpu_bwutil_python": 0.0677095045580611, + "gpu_gap_stddev_us": 4.59569964927191 + } + } + } + }, + "pillowresize_anisotropic_contract_480x864_nhwc_tensor_lanczos_uint8_c1_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "36x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "LANCZOS" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize uint8 1-channel Tensor (NHWC) anisotropically from 1920x1080 to 864x480 with LANCZOS. Advanced normalized mirror of pillowresize_contract_lanczos_1080p_advanced; batch 36 calibrated for approximately 1 ms reference timing." + }, + "baselines": { + "pillowresize_anisotropic_contract_480x864_nhwc_tensor_lanczos_uint8_c1_advanced[InOutDataType=uint8][layout=NHWC][shape=36x1080x1920][resizeType=TARGET_480x864][interpolation=LANCZOS][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 807.29233171486, + "gpu_time_us_python": 822.83029154266, + "gpu_noise_us_cpp": 1.498, + "gpu_noise_us_python": 3.496, + "gpu_bwutil_cpp": 0.07134993751252676, + "gpu_bwutil_python": 0.0700040111932125, + "gpu_gap_stddev_us": 3.171341161275361 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 694.97288974112, + "gpu_time_us_python": 710.69371558162, + "gpu_noise_us_cpp": 1.102, + "gpu_noise_us_python": 5.308, + "gpu_bwutil_cpp": 0.06324042445552938, + "gpu_bwutil_python": 0.06184257543199521, + "gpu_gap_stddev_us": 3.1369006563629727 + } + } + } + }, + "pillowresize_anisotropic_contract_480x864_nhwc_varshape_lanczos_uint8_c1_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "28x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "LANCZOS" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize uint8 1-channel VarShape (NHWC) anisotropically from 1920x1080 to 864x480 with LANCZOS. Advanced normalized mirror of pillowresize_contract_lanczos_1080p_advanced; batch 28 calibrated for approximately 1 ms reference timing." + }, + "baselines": { + "pillowresize_anisotropic_contract_480x864_nhwc_varshape_lanczos_uint8_c1_advanced[InOutDataType=uint8][layout=NHWC][shape=28x1080x1920][resizeType=TARGET_480x864][interpolation=LANCZOS][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 926.4193949869199, + "gpu_time_us_python": 946.357908528, + "gpu_noise_us_cpp": 3.152, + "gpu_noise_us_python": 3.4400000000000004, + "gpu_bwutil_cpp": 0.04835977030164748, + "gpu_bwutil_python": 0.047340572254593836, + "gpu_gap_stddev_us": 1.3073553469871426 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 868.75508885248, + "gpu_time_us_python": 891.30039926314, + "gpu_noise_us_cpp": 3.7060000000000004, + "gpu_noise_us_python": 4.874, + "gpu_bwutil_cpp": 0.03935211973674194, + "gpu_bwutil_python": 0.038359650009673195, + "gpu_gap_stddev_us": 4.070280298441743 + } + } + } + }, + "pillowresize_anisotropic_contract_480x864_nhwc_tensor_linear_uint8_c1_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize uint8 1-channel Tensor (NHWC) anisotropically from 1920x1080 to 864x480 with LINEAR. Advanced normalized mirror of pillowresize_contract_linear_1080p_scalar_advanced; batch 64 scaled conservatively to 32." + }, + "baselines": { + "pillowresize_anisotropic_contract_480x864_nhwc_tensor_linear_uint8_c1_advanced[InOutDataType=uint8][layout=NHWC][shape=32x1080x1920][resizeType=TARGET_480x864][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 463.86119183072003, + "gpu_time_us_python": 477.79582274637994, + "gpu_noise_us_cpp": 2.4419999999999997, + "gpu_noise_us_python": 2.706, + "gpu_bwutil_cpp": 0.11037842471859445, + "gpu_bwutil_python": 0.1071604457254246, + "gpu_gap_stddev_us": 1.2051127196576075 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 431.71709553358005, + "gpu_time_us_python": 448.22264662021996, + "gpu_noise_us_cpp": 1.9920000000000002, + "gpu_noise_us_python": 2.918, + "gpu_bwutil_cpp": 0.09048749022535411, + "gpu_bwutil_python": 0.08716121762011278, + "gpu_gap_stddev_us": 2.6540258750114356 + } + } + } + }, + "pillowresize_anisotropic_expand_2160x3840_nhwc_tensor_cubic_uint8_c1_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "8x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize uint8 1-channel Tensor (NHWC) anisotropically from 864x480 to 3840x2160 with CUBIC. Advanced normalized mirror of pillowresize_expand_cubic_1080p_scalar_advanced; batch 32 scaled conservatively to 8." + }, + "baselines": { + "pillowresize_anisotropic_expand_2160x3840_nhwc_tensor_cubic_uint8_c1_advanced[InOutDataType=uint8][layout=NHWC][shape=8x480x864][resizeType=TARGET_2160x3840][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 430.46124421734004, + "gpu_time_us_python": 445.37655985042, + "gpu_noise_us_cpp": 1.216, + "gpu_noise_us_python": 2.6839999999999997, + "gpu_bwutil_cpp": 0.10407459691997714, + "gpu_bwutil_python": 0.10059322462528317, + "gpu_gap_stddev_us": 2.7563534258910893 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 385.68325334208004, + "gpu_time_us_python": 400.55025746627996, + "gpu_noise_us_cpp": 1.1320000000000001, + "gpu_noise_us_python": 2.9259999999999997, + "gpu_bwutil_cpp": 0.08863208366219708, + "gpu_bwutil_python": 0.08534752242507745, + "gpu_gap_stddev_us": 2.2179591925113225 + } + } + } + }, + "pillowresize_anisotropic_expand_2160x3840_nhwc_tensor_lanczos_uint8_c1_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "4x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "LANCZOS" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize uint8 1-channel Tensor (NHWC) anisotropically from 864x480 to 3840x2160 with LANCZOS. Advanced normalized mirror of pillowresize_expand_lanczos_tensor_1080p_advanced; batch 16 scaled conservatively to 4." + }, + "baselines": { + "pillowresize_anisotropic_expand_2160x3840_nhwc_tensor_lanczos_uint8_c1_advanced[InOutDataType=uint8][layout=NHWC][shape=4x480x864][resizeType=TARGET_2160x3840][interpolation=LANCZOS][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 280.68238131123996, + "gpu_time_us_python": 294.84858408580004, + "gpu_noise_us_cpp": 2.092, + "gpu_noise_us_python": 3.032, + "gpu_bwutil_cpp": 0.0798061387002993, + "gpu_bwutil_python": 0.07597504652610115, + "gpu_gap_stddev_us": 2.017721372125302 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 260.46992641758, + "gpu_time_us_python": 275.90132862562, + "gpu_noise_us_cpp": 1.424, + "gpu_noise_us_python": 3.2619999999999996, + "gpu_bwutil_cpp": 0.06562056606171564, + "gpu_bwutil_python": 0.06195380569392091, + "gpu_gap_stddev_us": 2.6305200439916625 + } + } + } + }, + "pillowresize_anisotropic_expand_2160x3840_nhwc_tensor_linear_uint8_c1_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "layout": [ + "NHWC" + ], + "shape": [ + "8x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "PillowResize uint8 1-channel Tensor (NHWC) anisotropically from 864x480 to 3840x2160 with LINEAR. Advanced normalized mirror of pillowresize_expand_linear_1080p_scalar_advanced; batch 32 scaled conservatively to 8." + }, + "baselines": { + "pillowresize_anisotropic_expand_2160x3840_nhwc_tensor_linear_uint8_c1_advanced[InOutDataType=uint8][layout=NHWC][shape=8x480x864][resizeType=TARGET_2160x3840][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 404.20307744386, + "gpu_time_us_python": 419.1313810764, + "gpu_noise_us_cpp": 2.0800000000000005, + "gpu_noise_us_python": 2.676, + "gpu_bwutil_cpp": 0.11083594667496115, + "gpu_bwutil_python": 0.10689274236862616, + "gpu_gap_stddev_us": 2.466094955146973 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 334.10700824912, + "gpu_time_us_python": 349.96654139742003, + "gpu_noise_us_cpp": 1.796, + "gpu_noise_us_python": 3.13, + "gpu_bwutil_cpp": 0.1023087638957264, + "gpu_bwutil_python": 0.09768743767437044, + "gpu_gap_stddev_us": 2.5184112030925734 + } + } + } + } + } +} diff --git a/bench/config/operators/posterize.json b/bench/config/operators/posterize.json new file mode 100644 index 000000000..21ede64e9 --- /dev/null +++ b/bench/config/operators/posterize.json @@ -0,0 +1,344 @@ +{ + "benchmark": "posterize", + "configs": { + "posterize_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Posterize RGB8 interleaved + planar, Tensor + VarShape (~1-2ms)." + }, + "baselines": { + "posterize_uchar3_basic[InOutDataType=uchar3][shape=64x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 596.43506693696, + "gpu_time_us_python": 604.74175923108, + "gpu_noise_us_cpp": 1.356, + "gpu_noise_us_python": 2.3999999999999995, + "gpu_bwutil_cpp": 0.8584338751652231, + "gpu_bwutil_python": 0.8466425697819595, + "gpu_gap_stddev_us": 0.29407829637670346 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 484.585736436625, + "gpu_time_us_python": 493.188336638125, + "gpu_noise_us_cpp": 1.2025, + "gpu_noise_us_python": 2.95, + "gpu_bwutil_cpp": 0.8058621803554492, + "gpu_bwutil_python": 0.7918109634326179, + "gpu_gap_stddev_us": 1.5435059647685747 + } + }, + "posterize_uchar3_basic[InOutDataType=uchar3][shape=64x1080x1920][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 659.14827001718, + "gpu_time_us_python": 670.7107653019, + "gpu_noise_us_cpp": 1.8359999999999999, + "gpu_noise_us_python": 3.318, + "gpu_bwutil_cpp": 0.7767604735371865, + "gpu_bwutil_python": 0.7633699493897079, + "gpu_gap_stddev_us": 0.3368065285415382 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 550.2040810484999, + "gpu_time_us_python": 563.020823081475, + "gpu_noise_us_cpp": 2.8375, + "gpu_noise_us_python": 5.890000000000001, + "gpu_bwutil_cpp": 0.7097636863202459, + "gpu_bwutil_python": 0.6936095843794943, + "gpu_gap_stddev_us": 1.6231768087081824 + } + }, + "posterize_uchar3_basic[InOutDataType=uchar3][shape=64x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 994.0463455700199, + "gpu_time_us_python": 1003.7342815764399, + "gpu_noise_us_cpp": 1.518, + "gpu_noise_us_python": 2.006, + "gpu_bwutil_cpp": 0.5150833819579752, + "gpu_bwutil_python": 0.5101095858689806, + "gpu_gap_stddev_us": 1.4293221090305912 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 977.607267350325, + "gpu_time_us_python": 986.33086773385, + "gpu_noise_us_cpp": 1.4, + "gpu_noise_us_python": 2.5325, + "gpu_bwutil_cpp": 0.3994730447722413, + "gpu_bwutil_python": 0.3959425837885526, + "gpu_gap_stddev_us": 0.838625366297383 + } + }, + "posterize_uchar3_basic[InOutDataType=uchar3][shape=64x1080x1920][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1049.74354207046, + "gpu_time_us_python": 1062.7017086809599, + "gpu_noise_us_cpp": 2.144, + "gpu_noise_us_python": 4.048, + "gpu_bwutil_cpp": 0.48774880622307026, + "gpu_bwutil_python": 0.4818024212627875, + "gpu_gap_stddev_us": 1.2809486784884052 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1015.9652148753501, + "gpu_time_us_python": 1025.478139957875, + "gpu_noise_us_cpp": 2.0775, + "gpu_noise_us_python": 2.6925, + "gpu_bwutil_cpp": 0.3843896364338945, + "gpu_bwutil_python": 0.38081877310404966, + "gpu_gap_stddev_us": 1.829176735871719 + } + } + } + }, + "posterize_uint16_advanced": { + "tier": "advanced", + "dtypes": [ + "uint16" + ], + "string_axes": { + "shape": [ + "96x1080x1920" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Posterize U16 1-channel interleaved, Tensor + VarShape (~1-2ms; single-channel lives in advanced per RGB guideline R3; planar 1-channel is degenerate)." + }, + "baselines": { + "posterize_uint16_advanced[InOutDataType=uint16][shape=96x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 578.02978051998, + "gpu_time_us_python": 586.28703234178, + "gpu_noise_us_cpp": 1.7200000000000002, + "gpu_noise_us_python": 2.0380000000000003, + "gpu_bwutil_cpp": 0.8857676085749588, + "gpu_bwutil_python": 0.8732933821917603, + "gpu_gap_stddev_us": 0.5930502432831234 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 443.97739739755, + "gpu_time_us_python": 452.4764124129, + "gpu_noise_us_cpp": 1.6975, + "gpu_noise_us_python": 2.625, + "gpu_bwutil_cpp": 0.8795685304055314, + "gpu_bwutil_python": 0.8630585775497561, + "gpu_gap_stddev_us": 1.9363755656253587 + } + }, + "posterize_uint16_advanced[InOutDataType=uint16][shape=96x1080x1920][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1539.7848307002, + "gpu_time_us_python": 1552.866122076, + "gpu_noise_us_cpp": 3.684, + "gpu_noise_us_python": 3.222, + "gpu_bwutil_cpp": 0.33252391038065837, + "gpu_bwutil_python": 0.32971921057677556, + "gpu_gap_stddev_us": 2.3038641748672974 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1498.9019059974999, + "gpu_time_us_python": 1512.8357080034498, + "gpu_noise_us_cpp": 2.9675000000000002, + "gpu_noise_us_python": 4.87, + "gpu_bwutil_cpp": 0.2605373157160161, + "gpu_bwutil_python": 0.25813492534186805, + "gpu_gap_stddev_us": 2.738654067684004 + } + } + } + }, + "posterize_fake_planar_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Posterize RGB8 fake-planar comparison (tensor-only, ~1-2ms)." + }, + "baselines": { + "posterize_fake_planar_uchar3_advanced[InOutDataType=uchar3][shape=64x1080x1920][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3358.84561920162, + "gpu_time_us_python": 3375.93253977658, + "gpu_noise_us_cpp": 1.19, + "gpu_noise_us_python": 2.4379999999999997, + "gpu_bwutil_cpp": 0.457305590841569, + "gpu_bwutil_python": 0.4549909646383791, + "gpu_gap_stddev_us": 1.3910898381081835 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2825.951452216525, + "gpu_time_us_python": 2843.0143719399753, + "gpu_noise_us_cpp": 1.3575000000000002, + "gpu_noise_us_python": 3.0275, + "gpu_bwutil_cpp": 0.41456708411046067, + "gpu_bwutil_python": 0.4120759080109166, + "gpu_gap_stddev_us": 3.483500499696936 + } + } + } + }, + "posterize_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "52x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Posterize RGBA8 interleaved + planar, Tensor (uchar4 planar var-shape unsupported)." + }, + "baselines": { + "posterize_uchar4_advanced[InOutDataType=uchar4][shape=52x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 645.9951470325401, + "gpu_time_us_python": 654.7416514357201, + "gpu_noise_us_cpp": 1.148, + "gpu_noise_us_python": 2.4719999999999995, + "gpu_bwutil_cpp": 0.8586236227665657, + "gpu_bwutil_python": 0.8471561746192797, + "gpu_gap_stddev_us": 1.2758678317516967 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 524.3750113063, + "gpu_time_us_python": 533.1558078734249, + "gpu_noise_us_cpp": 1.6025, + "gpu_noise_us_python": 3.02, + "gpu_bwutil_cpp": 0.8067741826492989, + "gpu_bwutil_python": 0.7934927883817133, + "gpu_gap_stddev_us": 1.1974114778980238 + } + }, + "posterize_uchar4_advanced[InOutDataType=uchar4][shape=52x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 809.2836182225601, + "gpu_time_us_python": 818.0104626463401, + "gpu_noise_us_cpp": 1.034, + "gpu_noise_us_python": 2.1759999999999997, + "gpu_bwutil_cpp": 0.6854007608939882, + "gpu_bwutil_python": 0.6780852333747227, + "gpu_gap_stddev_us": 0.7834613448443268 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 795.64885150345, + "gpu_time_us_python": 804.83538538595, + "gpu_noise_us_cpp": 0.9075, + "gpu_noise_us_python": 3.805, + "gpu_bwutil_cpp": 0.5317294602046404, + "gpu_bwutil_python": 0.5256579107926712, + "gpu_gap_stddev_us": 0.8130290381593471 + } + } + } + }, + "posterize_fake_planar_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "52x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Posterize RGBA8 fake-planar comparison (tensor-only, ~1-2ms)." + }, + "baselines": { + "posterize_fake_planar_uchar4_advanced[InOutDataType=uchar4][shape=52x1080x1920][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3297.3103950794, + "gpu_time_us_python": 3315.15959752228, + "gpu_noise_us_cpp": 2.2560000000000002, + "gpu_noise_us_python": 2.4040000000000004, + "gpu_bwutil_cpp": 0.5046594000793849, + "gpu_bwutil_python": 0.501942716275712, + "gpu_gap_stddev_us": 1.7111380433624945 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2756.1640855061505, + "gpu_time_us_python": 2769.2506823711, + "gpu_noise_us_cpp": 1.605, + "gpu_noise_us_python": 4.3525, + "gpu_bwutil_cpp": 0.4604880395678661, + "gpu_bwutil_python": 0.4583115271224433, + "gpu_gap_stddev_us": 0.49387256485707903 + } + } + } + } + } +} diff --git a/bench/config/operators/randomresizedcrop.json b/bench/config/operators/randomresizedcrop.json new file mode 100644 index 000000000..7baa6756d --- /dev/null +++ b/bench/config/operators/randomresizedcrop.json @@ -0,0 +1,1191 @@ +{ + "benchmark": "randomresizedcrop", + "configs": { + "randomresizedcrop_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "RandomResizedCrop RGB8 (basic)." + }, + "baselines": { + "randomresizedcrop_uchar3_basic[InOutDataType=uchar3][shape=32x1080x1920][resizeType=EXPAND][interpolation=LINEAR][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2877.98405289646, + "gpu_time_us_python": 2891.93607472822, + "gpu_noise_us_cpp": 3.942, + "gpu_noise_us_python": 4.292, + "gpu_bwutil_cpp": 0.22237889991645726, + "gpu_bwutil_python": 0.2213059411938183, + "gpu_gap_stddev_us": 2.172383810328578 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1796.16732546935, + "gpu_time_us_python": 1810.9976402309749, + "gpu_noise_us_cpp": 10.537500000000001, + "gpu_noise_us_python": 10.4675, + "gpu_bwutil_cpp": 0.27179613893440135, + "gpu_bwutil_python": 0.2695734323300849, + "gpu_gap_stddev_us": 4.871877624166593 + } + }, + "randomresizedcrop_uchar3_basic[InOutDataType=uchar3][shape=32x1080x1920][resizeType=EXPAND][interpolation=LINEAR][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2889.21009644688, + "gpu_time_us_python": 2905.6441240723, + "gpu_noise_us_cpp": 4.1240000000000006, + "gpu_noise_us_python": 4.792, + "gpu_bwutil_cpp": 0.22151533781733365, + "gpu_bwutil_python": 0.22026189636580687, + "gpu_gap_stddev_us": 2.0928937848328184 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1885.5640508717752, + "gpu_time_us_python": 1901.42879706755, + "gpu_noise_us_cpp": 16.7875, + "gpu_noise_us_python": 16.0775, + "gpu_bwutil_cpp": 0.25890699229476155, + "gpu_bwutil_python": 0.2567445377037078, + "gpu_gap_stddev_us": 2.572130550562481 + } + } + } + }, + "randomresizedcrop_planar_nchw_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "RandomResizedCrop RGB8 native planar (basic)." + }, + "baselines": { + "randomresizedcrop_planar_nchw_uchar3_basic[InOutDataType=uchar3][shape=32x1080x1920][resizeType=EXPAND][interpolation=LINEAR][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3131.19279143982, + "gpu_time_us_python": 3145.7535976171, + "gpu_noise_us_cpp": 8.834, + "gpu_noise_us_python": 10.008000000000001, + "gpu_bwutil_cpp": 0.20439622554532383, + "gpu_bwutil_python": 0.20345045141130252, + "gpu_gap_stddev_us": 1.5052489107664733 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2477.18320174315, + "gpu_time_us_python": 2491.97075550415, + "gpu_noise_us_cpp": 3.3125, + "gpu_noise_us_python": 7.8875, + "gpu_bwutil_cpp": 0.19707740267005944, + "gpu_bwutil_python": 0.19591018875757, + "gpu_gap_stddev_us": 4.322781467239164 + } + }, + "randomresizedcrop_planar_nchw_uchar3_basic[InOutDataType=uchar3][shape=32x1080x1920][resizeType=EXPAND][interpolation=LINEAR][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3475.47817359044, + "gpu_time_us_python": 3493.1541350152206, + "gpu_noise_us_cpp": 25.964000000000006, + "gpu_noise_us_python": 26.454, + "gpu_bwutil_cpp": 0.18414813919876893, + "gpu_bwutil_python": 0.18321649874350582, + "gpu_gap_stddev_us": 1.6086469950734206 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2651.42142702655, + "gpu_time_us_python": 2664.5548352271253, + "gpu_noise_us_cpp": 6.67, + "gpu_noise_us_python": 14.254999999999999, + "gpu_bwutil_cpp": 0.1841206908960312, + "gpu_bwutil_python": 0.18321425035545205, + "gpu_gap_stddev_us": 6.62111927826838 + } + } + } + }, + "randomresizedcrop_fake_planar_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "RandomResizedCrop RGB8 fake-planar reformat comparison (advanced)." + }, + "baselines": { + "randomresizedcrop_fake_planar_uchar3_advanced[InOutDataType=uchar3][shape=32x1080x1920][resizeType=EXPAND][interpolation=LINEAR][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5194.36546187006, + "gpu_time_us_python": 5212.09227734678, + "gpu_noise_us_cpp": 2.3600000000000003, + "gpu_noise_us_python": 2.2079999999999997, + "gpu_bwutil_cpp": 0.3696333393365258, + "gpu_bwutil_python": 0.3683760835474129, + "gpu_gap_stddev_us": 1.0695786771052025 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 3657.2217123355, + "gpu_time_us_python": 3673.489943604, + "gpu_noise_us_cpp": 11.805, + "gpu_noise_us_python": 11.739999999999998, + "gpu_bwutil_cpp": 0.40043505113335953, + "gpu_bwutil_python": 0.3986609669287068, + "gpu_gap_stddev_us": 5.948993228968107 + } + } + } + }, + "randomresizedcrop_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "RandomResizedCrop RGBf32 (basic)." + }, + "baselines": { + "randomresizedcrop_float3_basic[InOutDataType=float3][shape=16x1080x1920][resizeType=EXPAND][interpolation=LINEAR][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1544.14114188694, + "gpu_time_us_python": 1556.6824727437001, + "gpu_noise_us_cpp": 20.122, + "gpu_noise_us_python": 20.336, + "gpu_bwutil_cpp": 0.8289426593816127, + "gpu_bwutil_python": 0.8222631358172852, + "gpu_gap_stddev_us": 1.2304331856611437 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1206.548939579825, + "gpu_time_us_python": 1221.9952214369, + "gpu_noise_us_cpp": 12.255, + "gpu_noise_us_python": 12.8675, + "gpu_bwutil_cpp": 0.8091524796332827, + "gpu_bwutil_python": 0.7989221066303118, + "gpu_gap_stddev_us": 2.258575710164982 + } + } + } + }, + "randomresizedcrop_float3_varshape_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "RandomResizedCrop RGBf32 VarShape (advanced)." + }, + "baselines": { + "randomresizedcrop_float3_varshape_advanced[InOutDataType=float3][shape=16x1080x1920][resizeType=EXPAND][interpolation=LINEAR][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1678.2625003180601, + "gpu_time_us_python": 1693.46075589588, + "gpu_noise_us_cpp": 14.702000000000002, + "gpu_noise_us_python": 14.556000000000001, + "gpu_bwutil_cpp": 0.7626968642276932, + "gpu_bwutil_python": 0.755850121562774, + "gpu_gap_stddev_us": 1.873270262358198 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1346.570651243275, + "gpu_time_us_python": 1362.765121418475, + "gpu_noise_us_cpp": 7.7225, + "gpu_noise_us_python": 6.8175, + "gpu_bwutil_cpp": 0.725058571437676, + "gpu_bwutil_python": 0.7164563147388565, + "gpu_gap_stddev_us": 4.031063472205086 + } + } + } + }, + "randomresizedcrop_planar_nchw_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "RandomResizedCrop RGBf32 native planar (basic)." + }, + "baselines": { + "randomresizedcrop_planar_nchw_float3_basic[InOutDataType=float3][shape=16x1080x1920][resizeType=EXPAND][interpolation=LINEAR][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2212.00486380308, + "gpu_time_us_python": 2226.95453842124, + "gpu_noise_us_cpp": 18.75, + "gpu_noise_us_python": 19.598, + "gpu_bwutil_cpp": 0.5786647649418112, + "gpu_bwutil_python": 0.5747813659848583, + "gpu_gap_stddev_us": 1.7492395688651545 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1974.155493146275, + "gpu_time_us_python": 1989.2149161634752, + "gpu_noise_us_cpp": 16.544999999999998, + "gpu_noise_us_python": 17.1675, + "gpu_bwutil_cpp": 0.4945334923803496, + "gpu_bwutil_python": 0.4907865004050601, + "gpu_gap_stddev_us": 2.6276569718371023 + } + } + } + }, + "randomresizedcrop_planar_nchw_float3_varshape_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "RandomResizedCrop RGBf32 native planar VarShape (advanced)." + }, + "baselines": { + "randomresizedcrop_planar_nchw_float3_varshape_advanced[InOutDataType=float3][shape=16x1080x1920][resizeType=EXPAND][interpolation=LINEAR][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2741.43457565356, + "gpu_time_us_python": 2757.7016455933, + "gpu_noise_us_cpp": 21.648, + "gpu_noise_us_python": 22.814, + "gpu_bwutil_cpp": 0.4669127876706841, + "gpu_bwutil_python": 0.46415694904339355, + "gpu_gap_stddev_us": 3.760187493742185 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2444.839131890475, + "gpu_time_us_python": 2461.84431535635, + "gpu_noise_us_cpp": 22.340000000000003, + "gpu_noise_us_python": 23.182499999999997, + "gpu_bwutil_cpp": 0.3993570188597476, + "gpu_bwutil_python": 0.39659828816424814, + "gpu_gap_stddev_us": 0.8792893618766012 + } + } + } + }, + "randomresizedcrop_fake_planar_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "RandomResizedCrop RGBf32 fake-planar reformat comparison (advanced)." + }, + "baselines": { + "randomresizedcrop_fake_planar_float3_advanced[InOutDataType=float3][shape=16x1080x1920][resizeType=EXPAND][interpolation=LINEAR][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4602.90978865184, + "gpu_time_us_python": 4622.18334544786, + "gpu_noise_us_cpp": 20.338, + "gpu_noise_us_python": 20.970000000000002, + "gpu_bwutil_cpp": 0.8342564320436093, + "gpu_bwutil_python": 0.8307776114294094, + "gpu_gap_stddev_us": 0.999266094726021 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 3628.8756861326, + "gpu_time_us_python": 3646.13228777175, + "gpu_noise_us_cpp": 12.3175, + "gpu_noise_us_python": 13.555, + "gpu_bwutil_cpp": 0.8070885595167897, + "gpu_bwutil_python": 0.8032688204658598, + "gpu_gap_stddev_us": 1.7361424409162183 + } + } + } + }, + "randomresizedcrop_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "RandomResizedCrop RGBA8 (advanced)." + }, + "baselines": { + "randomresizedcrop_uchar4_advanced[InOutDataType=uchar4][shape=32x1080x1920][resizeType=EXPAND][interpolation=LINEAR][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3571.7487564892194, + "gpu_time_us_python": 3586.05095250258, + "gpu_noise_us_cpp": 2.052, + "gpu_noise_us_python": 2.952, + "gpu_bwutil_cpp": 0.2389136856574355, + "gpu_bwutil_python": 0.23796088227681947, + "gpu_gap_stddev_us": 1.3336139765360762 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2281.6759282295498, + "gpu_time_us_python": 2296.1370106077748, + "gpu_noise_us_cpp": 2.14, + "gpu_noise_us_python": 6.1175, + "gpu_bwutil_cpp": 0.28528607189151106, + "gpu_bwutil_python": 0.2834908955551082, + "gpu_gap_stddev_us": 1.1665665549975321 + } + }, + "randomresizedcrop_uchar4_advanced[InOutDataType=uchar4][shape=32x1080x1920][resizeType=EXPAND][interpolation=LINEAR][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3603.1838853018003, + "gpu_time_us_python": 3620.13001099378, + "gpu_noise_us_cpp": 4.5440000000000005, + "gpu_noise_us_python": 2.9459999999999997, + "gpu_bwutil_cpp": 0.23682926448556074, + "gpu_bwutil_python": 0.23572074089939105, + "gpu_gap_stddev_us": 2.56658127316559 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2330.291384002725, + "gpu_time_us_python": 2349.14384429625, + "gpu_noise_us_cpp": 3.3775000000000004, + "gpu_noise_us_python": 6.9125, + "gpu_bwutil_cpp": 0.27933065240215815, + "gpu_bwutil_python": 0.2770897199602219, + "gpu_gap_stddev_us": 4.529430031439828 + } + } + } + }, + "randomresizedcrop_planar_nchw_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "RandomResizedCrop RGBA8 native planar tensor (advanced)." + }, + "baselines": { + "randomresizedcrop_planar_nchw_uchar4_advanced[InOutDataType=uchar4][shape=32x1080x1920][resizeType=EXPAND][interpolation=LINEAR][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3930.9311933814997, + "gpu_time_us_python": 3944.44800466296, + "gpu_noise_us_cpp": 15.788, + "gpu_noise_us_python": 15.882000000000001, + "gpu_bwutil_cpp": 0.21708346254478625, + "gpu_bwutil_python": 0.21633948656365787, + "gpu_gap_stddev_us": 0.9590228847939501 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 3101.408838197275, + "gpu_time_us_python": 3116.672168508475, + "gpu_noise_us_cpp": 5.255000000000001, + "gpu_noise_us_python": 11.2425, + "gpu_bwutil_cpp": 0.20988241247507058, + "gpu_bwutil_python": 0.20885348647639496, + "gpu_gap_stddev_us": 3.3353215005376935 + } + } + } + }, + "randomresizedcrop_fake_planar_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "RandomResizedCrop RGBA8 fake-planar reformat comparison (advanced)." + }, + "baselines": { + "randomresizedcrop_fake_planar_uchar4_advanced[InOutDataType=uchar4][shape=32x1080x1920][resizeType=EXPAND][interpolation=LINEAR][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6653.342992381001, + "gpu_time_us_python": 6672.815164766781, + "gpu_noise_us_cpp": 1.544, + "gpu_noise_us_python": 4.498, + "gpu_bwutil_cpp": 0.3847704828103474, + "gpu_bwutil_python": 0.38364754424463365, + "gpu_gap_stddev_us": 5.957185298928513 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 4648.172991524574, + "gpu_time_us_python": 4663.8190418381, + "gpu_noise_us_cpp": 2.27, + "gpu_noise_us_python": 3.0825, + "gpu_bwutil_cpp": 0.4200925435555369, + "gpu_bwutil_python": 0.41868343075726766, + "gpu_gap_stddev_us": 1.5717097387238017 + } + } + } + }, + "randomresizedcrop_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "RandomResizedCrop RGBAf32 (advanced)." + }, + "baselines": { + "randomresizedcrop_float4_advanced[InOutDataType=float4][shape=16x1080x1920][resizeType=EXPAND][interpolation=LINEAR][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1874.47040097578, + "gpu_time_us_python": 1888.67439002848, + "gpu_noise_us_cpp": 37.94, + "gpu_noise_us_python": 37.826, + "gpu_bwutil_cpp": 0.9104796152265464, + "gpu_bwutil_python": 0.9036322055083165, + "gpu_gap_stddev_us": 1.6200605980738543 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1391.2834850365, + "gpu_time_us_python": 1401.241290383, + "gpu_noise_us_cpp": 23.3675, + "gpu_noise_us_python": 23.7575, + "gpu_bwutil_cpp": 0.935607654893013, + "gpu_bwutil_python": 0.9289663699194491, + "gpu_gap_stddev_us": 4.013123605201231 + } + }, + "randomresizedcrop_float4_advanced[InOutDataType=float4][shape=16x1080x1920][resizeType=EXPAND][interpolation=LINEAR][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1870.26574444436, + "gpu_time_us_python": 1885.3478403057802, + "gpu_noise_us_cpp": 39.086, + "gpu_noise_us_python": 38.99400000000001, + "gpu_bwutil_cpp": 0.9125264956603871, + "gpu_bwutil_python": 0.9052271626347597, + "gpu_gap_stddev_us": 2.454916264202451 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1459.877710997075, + "gpu_time_us_python": 1472.9359916199749, + "gpu_noise_us_cpp": 16.07, + "gpu_noise_us_python": 15.7675, + "gpu_bwutil_cpp": 0.8916595867539052, + "gpu_bwutil_python": 0.8837515572164454, + "gpu_gap_stddev_us": 1.709359082547909 + } + } + } + }, + "randomresizedcrop_planar_nchw_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "RandomResizedCrop RGBAf32 native planar (advanced)." + }, + "baselines": { + "randomresizedcrop_planar_nchw_float4_advanced[InOutDataType=float4][shape=16x1080x1920][resizeType=EXPAND][interpolation=LINEAR][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2798.84231090542, + "gpu_time_us_python": 2812.7293887536603, + "gpu_noise_us_cpp": 27.274, + "gpu_noise_us_python": 27.682, + "gpu_bwutil_cpp": 0.6097801941619533, + "gpu_bwutil_python": 0.6067697509645841, + "gpu_gap_stddev_us": 1.1952793463057076 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2463.89856209965, + "gpu_time_us_python": 2479.83551999705, + "gpu_noise_us_cpp": 22.689999999999998, + "gpu_noise_us_python": 22.335, + "gpu_bwutil_cpp": 0.5283122567765938, + "gpu_bwutil_python": 0.5249135386197572, + "gpu_gap_stddev_us": 4.092337908914409 + } + }, + "randomresizedcrop_planar_nchw_float4_advanced[InOutDataType=float4][shape=16x1080x1920][resizeType=EXPAND][interpolation=LINEAR][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3487.7895149919204, + "gpu_time_us_python": 3505.4364422956596, + "gpu_noise_us_cpp": 31.586000000000002, + "gpu_noise_us_python": 32.24, + "gpu_bwutil_cpp": 0.48932895900162077, + "gpu_bwutil_python": 0.48686500495634616, + "gpu_gap_stddev_us": 3.934133247153634 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 3099.507009566225, + "gpu_time_us_python": 3118.9421973110248, + "gpu_noise_us_cpp": 30.89, + "gpu_noise_us_python": 31.2825, + "gpu_bwutil_cpp": 0.4200195849134911, + "gpu_bwutil_python": 0.41738971757778653, + "gpu_gap_stddev_us": 5.136035663267151 + } + } + } + }, + "randomresizedcrop_fake_planar_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "RandomResizedCrop RGBAf32 fake-planar reformat comparison (advanced)." + }, + "baselines": { + "randomresizedcrop_fake_planar_float4_advanced[InOutDataType=float4][shape=16x1080x1920][resizeType=EXPAND][interpolation=LINEAR][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6314.97215270992, + "gpu_time_us_python": 6329.149425029721, + "gpu_noise_us_cpp": 41.61, + "gpu_noise_us_python": 40.47, + "gpu_bwutil_cpp": 0.810771604715525, + "gpu_bwutil_python": 0.8089570998621275, + "gpu_gap_stddev_us": 10.618151590977972 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 4909.9924564361, + "gpu_time_us_python": 4925.348230436675, + "gpu_noise_us_cpp": 27.215, + "gpu_noise_us_python": 27.450000000000003, + "gpu_bwutil_cpp": 0.7953342780843351, + "gpu_bwutil_python": 0.7928546153968932, + "gpu_gap_stddev_us": 1.8485719695406428 + } + } + } + }, + "randomresizedcrop_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "RandomResizedCrop single-channel (advanced)." + }, + "baselines": { + "randomresizedcrop_scalar_advanced[InOutDataType=float32][shape=32x1080x1920][resizeType=EXPAND][interpolation=LINEAR][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1870.65831007382, + "gpu_time_us_python": 1884.1480747321398, + "gpu_noise_us_cpp": 4.048, + "gpu_noise_us_python": 5.384, + "gpu_bwutil_cpp": 0.45616998546545, + "gpu_bwutil_python": 0.4529042518361802, + "gpu_gap_stddev_us": 0.6451829169252149 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1788.1371096861249, + "gpu_time_us_python": 1802.1153587307, + "gpu_noise_us_cpp": 3.2049999999999996, + "gpu_noise_us_python": 5.8500000000000005, + "gpu_bwutil_cpp": 0.3640144160240364, + "gpu_bwutil_python": 0.3611961747572858, + "gpu_gap_stddev_us": 1.8897908674932227 + } + }, + "randomresizedcrop_scalar_advanced[InOutDataType=float32][shape=32x1080x1920][resizeType=EXPAND][interpolation=LINEAR][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2140.6171016100197, + "gpu_time_us_python": 2155.12116242392, + "gpu_noise_us_cpp": 3.5159999999999996, + "gpu_noise_us_python": 5.478, + "gpu_bwutil_cpp": 0.3986415115069747, + "gpu_bwutil_python": 0.39595782486068304, + "gpu_gap_stddev_us": 1.737317451095356 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2001.3907091936248, + "gpu_time_us_python": 2017.3250862666998, + "gpu_noise_us_cpp": 2.9749999999999996, + "gpu_noise_us_python": 6.785, + "gpu_bwutil_cpp": 0.3252266602827922, + "gpu_bwutil_python": 0.3226599590874248, + "gpu_gap_stddev_us": 1.8942375373356428 + } + }, + "randomresizedcrop_scalar_advanced[InOutDataType=uint8][shape=32x1080x1920][resizeType=EXPAND][interpolation=LINEAR][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1490.80872097776, + "gpu_time_us_python": 1504.7883619392, + "gpu_noise_us_cpp": 2.594, + "gpu_noise_us_python": 3.218, + "gpu_bwutil_cpp": 0.1430998317889387, + "gpu_bwutil_python": 0.14177131045479277, + "gpu_gap_stddev_us": 2.1760307772212015 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1019.8885080999501, + "gpu_time_us_python": 1032.0222671189, + "gpu_noise_us_cpp": 3.2199999999999998, + "gpu_noise_us_python": 3.9625000000000004, + "gpu_bwutil_cpp": 0.1595545988321196, + "gpu_bwutil_python": 0.15768029757840615, + "gpu_gap_stddev_us": 0.81150661363718 + } + }, + "randomresizedcrop_scalar_advanced[InOutDataType=uint8][shape=32x1080x1920][resizeType=EXPAND][interpolation=LINEAR][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1506.1362148036, + "gpu_time_us_python": 1521.5227045646, + "gpu_noise_us_cpp": 4.288, + "gpu_noise_us_python": 3.8879999999999995, + "gpu_bwutil_cpp": 0.1416436817995658, + "gpu_bwutil_python": 0.14021102380069733, + "gpu_gap_stddev_us": 1.8451371073690328 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1026.343093831775, + "gpu_time_us_python": 1043.2085141632251, + "gpu_noise_us_cpp": 3.7024999999999997, + "gpu_noise_us_python": 7.635, + "gpu_bwutil_cpp": 0.15854978410885268, + "gpu_bwutil_python": 0.15598193759310933, + "gpu_gap_stddev_us": 2.071334435919994 + } + } + } + }, + "randomresizedcrop_contract_linear_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "256x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "RandomResizedCrop single-channel contract linear (advanced)." + }, + "baselines": { + "randomresizedcrop_contract_linear_1080p_advanced[InOutDataType=float32][shape=256x1080x1920][resizeType=CONTRACT][interpolation=LINEAR][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1278.6329121404399, + "gpu_time_us_python": 1288.85932772374, + "gpu_noise_us_cpp": 18.552, + "gpu_noise_us_python": 18.534, + "gpu_bwutil_cpp": 1.3347598298134018, + "gpu_bwutil_python": 1.3241701202211735, + "gpu_gap_stddev_us": 1.0683010671793127 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1106.3054773622, + "gpu_time_us_python": 1119.3174369514, + "gpu_noise_us_cpp": 9.3375, + "gpu_noise_us_python": 9.685, + "gpu_bwutil_cpp": 1.1766398581272177, + "gpu_bwutil_python": 1.1629764651163776, + "gpu_gap_stddev_us": 2.016937985340605 + } + }, + "randomresizedcrop_contract_linear_1080p_advanced[InOutDataType=float32][shape=256x1080x1920][resizeType=CONTRACT][interpolation=LINEAR][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1624.7670508178203, + "gpu_time_us_python": 1642.3028573911001, + "gpu_noise_us_cpp": 19.383999999999997, + "gpu_noise_us_python": 13.952000000000002, + "gpu_bwutil_cpp": 1.0504270901783237, + "gpu_bwutil_python": 1.0391958237673946, + "gpu_gap_stddev_us": 6.291721601011795 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1434.3661154245, + "gpu_time_us_python": 1462.5819658231, + "gpu_noise_us_cpp": 16.1675, + "gpu_noise_us_python": 22.3775, + "gpu_bwutil_cpp": 0.9078668546222867, + "gpu_bwutil_python": 0.8903131275201219, + "gpu_gap_stddev_us": 8.41879642108917 + } + }, + "randomresizedcrop_contract_linear_1080p_advanced[InOutDataType=uint8][shape=256x1080x1920][resizeType=CONTRACT][interpolation=LINEAR][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 794.21668528458, + "gpu_time_us_python": 805.7029998176001, + "gpu_noise_us_cpp": 3.69, + "gpu_noise_us_python": 4.166, + "gpu_bwutil_cpp": 0.5372205572163157, + "gpu_bwutil_python": 0.5295617242520045, + "gpu_gap_stddev_us": 0.6898108933695701 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 578.7569926915501, + "gpu_time_us_python": 592.578806841525, + "gpu_noise_us_cpp": 5.7075, + "gpu_noise_us_python": 9.065, + "gpu_bwutil_cpp": 0.5623519834846532, + "gpu_bwutil_python": 0.5492556102574379, + "gpu_gap_stddev_us": 2.8088407078456243 + } + }, + "randomresizedcrop_contract_linear_1080p_advanced[InOutDataType=uint8][shape=256x1080x1920][resizeType=CONTRACT][interpolation=LINEAR][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 888.3301385673, + "gpu_time_us_python": 909.0630800160201, + "gpu_noise_us_cpp": 5.144, + "gpu_noise_us_python": 6.764, + "gpu_bwutil_cpp": 0.4803046378650754, + "gpu_bwutil_python": 0.46934915776382996, + "gpu_gap_stddev_us": 1.4878620750046438 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 687.3986366007, + "gpu_time_us_python": 714.994236962825, + "gpu_noise_us_cpp": 10.525, + "gpu_noise_us_python": 23.257499999999997, + "gpu_bwutil_cpp": 0.47402753797405656, + "gpu_bwutil_python": 0.45568195312994886, + "gpu_gap_stddev_us": 0.5926812841392807 + } + } + } + }, + "randomresizedcrop_expand_cubic_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "RandomResizedCrop single-channel expand cubic (advanced)." + }, + "baselines": { + "randomresizedcrop_expand_cubic_1080p_advanced[InOutDataType=float32][shape=16x1080x1920][resizeType=EXPAND][interpolation=CUBIC][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2240.7241438115802, + "gpu_time_us_python": 2254.51180425922, + "gpu_noise_us_cpp": 3.9, + "gpu_noise_us_python": 4.136, + "gpu_bwutil_cpp": 0.1904158041224686, + "gpu_bwutil_python": 0.18925113777811114, + "gpu_gap_stddev_us": 2.851587587331887 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2163.409602229125, + "gpu_time_us_python": 2179.083279635, + "gpu_noise_us_cpp": 2.7199999999999998, + "gpu_noise_us_python": 6.202500000000001, + "gpu_bwutil_cpp": 0.15044179986880862, + "gpu_bwutil_python": 0.14935820350357118, + "gpu_gap_stddev_us": 2.201020514233813 + } + }, + "randomresizedcrop_expand_cubic_1080p_advanced[InOutDataType=float32][shape=16x1080x1920][resizeType=EXPAND][interpolation=CUBIC][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2464.28952646168, + "gpu_time_us_python": 2479.6241314613, + "gpu_noise_us_cpp": 4.672, + "gpu_noise_us_python": 5.772, + "gpu_bwutil_cpp": 0.17314114533243985, + "gpu_bwutil_python": 0.17207012656066548, + "gpu_gap_stddev_us": 2.8527781308962092 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2368.9030617475, + "gpu_time_us_python": 2384.3260081545, + "gpu_noise_us_cpp": 5.914999999999999, + "gpu_noise_us_python": 3.8775000000000004, + "gpu_bwutil_cpp": 0.13739211186038397, + "gpu_bwutil_python": 0.13649798516801367, + "gpu_gap_stddev_us": 4.861451054646992 + } + }, + "randomresizedcrop_expand_cubic_1080p_advanced[InOutDataType=uint8][shape=16x1080x1920][resizeType=EXPAND][interpolation=CUBIC][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2310.95375927212, + "gpu_time_us_python": 2324.6865391441797, + "gpu_noise_us_cpp": 4.6899999999999995, + "gpu_noise_us_python": 6.447999999999999, + "gpu_bwutil_cpp": 0.04615738223562228, + "gpu_bwutil_python": 0.04588467309016822, + "gpu_gap_stddev_us": 1.0408168612318172 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2251.907267987025, + "gpu_time_us_python": 2265.408809824675, + "gpu_noise_us_cpp": 2.88, + "gpu_noise_us_python": 4.4025, + "gpu_bwutil_cpp": 0.03613227594272547, + "gpu_bwutil_python": 0.03591726790639418, + "gpu_gap_stddev_us": 1.8783761145589202 + } + }, + "randomresizedcrop_expand_cubic_1080p_advanced[InOutDataType=uint8][shape=16x1080x1920][resizeType=EXPAND][interpolation=CUBIC][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2535.6391261321796, + "gpu_time_us_python": 2551.75280653548, + "gpu_noise_us_cpp": 5.347999999999999, + "gpu_noise_us_python": 6.036, + "gpu_bwutil_cpp": 0.04206714617973234, + "gpu_bwutil_python": 0.04180141659871832, + "gpu_gap_stddev_us": 3.104838425812091 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2363.4495583240496, + "gpu_time_us_python": 2379.656559730175, + "gpu_noise_us_cpp": 3.7800000000000002, + "gpu_noise_us_python": 4.805, + "gpu_bwutil_cpp": 0.03442702850329118, + "gpu_bwutil_python": 0.03419143356883288, + "gpu_gap_stddev_us": 3.98563206064673 + } + } + } + } + } +} diff --git a/bench/config/operators/reformat.json b/bench/config/operators/reformat.json new file mode 100644 index 000000000..48c1bf220 --- /dev/null +++ b/bench/config/operators/reformat.json @@ -0,0 +1,381 @@ +{ + "benchmark": "reformat", + "configs": { + "reformat_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "rowAlignment": [ + 0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Reformat RGB8 (basic)." + }, + "baselines": { + "reformat_uchar3_basic[InOutDataType=uchar3][shape=128x1080x1920][inputKind=Tensor][rowAlignment=0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3187.86490718016, + "gpu_time_us_python": 3198.73493774022, + "gpu_noise_us_cpp": 1.4420000000000002, + "gpu_noise_us_python": 5.0920000000000005, + "gpu_bwutil_cpp": 0.3212182405111711, + "gpu_bwutil_python": 0.3201266361800702, + "gpu_gap_stddev_us": 1.7544275510115164 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2434.4744321211597, + "gpu_time_us_python": 2445.52437773024, + "gpu_noise_us_cpp": 1.4380000000000002, + "gpu_noise_us_python": 5.006, + "gpu_bwutil_cpp": 0.3208154706316497, + "gpu_bwutil_python": 0.3193659497118863, + "gpu_gap_stddev_us": 1.3762041539992755 + } + } + } + }, + "reformat_uint8_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "96x1080x1920" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "rowAlignment": [ + 0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Reformat single-channel uint8 (advanced)." + }, + "baselines": { + "reformat_uint8_advanced[InOutDataType=uint8][shape=96x1080x1920][inputKind=Tensor][rowAlignment=0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 296.06653246532, + "gpu_time_us_python": 305.1205903502, + "gpu_noise_us_cpp": 1.328, + "gpu_noise_us_python": 2.446, + "gpu_bwutil_cpp": 0.8646755113398772, + "gpu_bwutil_python": 0.8390366853073775, + "gpu_gap_stddev_us": 1.1818311910171986 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 227.15142118734002, + "gpu_time_us_python": 236.23237811498, + "gpu_noise_us_cpp": 1.6920000000000002, + "gpu_noise_us_python": 4.08, + "gpu_bwutil_cpp": 0.8595812505383374, + "gpu_bwutil_python": 0.8265654281661854, + "gpu_gap_stddev_us": 1.2750852602677663 + } + } + } + }, + "reformat_uint8_pitched_fallback_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "512x480x640" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "rowAlignment": [ + 256 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Reformat pitched single-channel uint8 on the calibrated single-call kernel fallback (advanced)." + }, + "baselines": { + "reformat_uint8_pitched_fallback_advanced[InOutDataType=uint8][shape=512x480x640][inputKind=Tensor][rowAlignment=256]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 675.74752056462, + "gpu_time_us_python": 685.9330203795, + "gpu_noise_us_cpp": 1.26, + "gpu_noise_us_python": 2.93, + "gpu_bwutil_cpp": 0.29933090638945953, + "gpu_bwutil_python": 0.29488835346937103, + "gpu_gap_stddev_us": 1.3402941701498188 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 565.71913578948, + "gpu_time_us_python": 575.0775823319, + "gpu_noise_us_cpp": 1.7, + "gpu_noise_us_python": 4.236, + "gpu_bwutil_cpp": 0.27270650189590084, + "gpu_bwutil_python": 0.26827107974464315, + "gpu_gap_stddev_us": 1.3362217459056451 + } + } + } + }, + "reformat_uint8_pitched_large_image_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "rowAlignment": [ + 256 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Reformat pitched single-channel uint8 on the calibrated single-call large-image copy path (advanced)." + }, + "baselines": { + "reformat_uint8_pitched_large_image_advanced[InOutDataType=uint8][shape=128x1080x1920][inputKind=Tensor][rowAlignment=256]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1035.8355848474598, + "gpu_time_us_python": 1044.3667352627199, + "gpu_noise_us_cpp": 5.074, + "gpu_noise_us_python": 5.632000000000001, + "gpu_bwutil_cpp": 0.32954885932917477, + "gpu_bwutil_python": 0.3268539649429791, + "gpu_gap_stddev_us": 3.5812250381560133 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1063.64163899022, + "gpu_time_us_python": 1075.20899174402, + "gpu_noise_us_cpp": 2.2039999999999997, + "gpu_noise_us_python": 8.358, + "gpu_bwutil_cpp": 0.2447714312280504, + "gpu_bwutil_python": 0.24213594074549122, + "gpu_gap_stddev_us": 1.4495136207740464 + } + } + } + }, + "reformat_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "rowAlignment": [ + 0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Reformat RGBA8 (advanced)." + }, + "baselines": { + "reformat_uchar4_advanced[InOutDataType=uchar4][shape=64x1080x1920][inputKind=Tensor][rowAlignment=0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2121.72273619696, + "gpu_time_us_python": 2132.06868010048, + "gpu_noise_us_cpp": 1.8719999999999999, + "gpu_noise_us_python": 4.878, + "gpu_bwutil_cpp": 0.32175118531101765, + "gpu_bwutil_python": 0.3201900943514781, + "gpu_gap_stddev_us": 1.5356788858246493 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1630.60999489444, + "gpu_time_us_python": 1639.88258153002, + "gpu_noise_us_cpp": 1.31, + "gpu_noise_us_python": 2.758, + "gpu_bwutil_cpp": 0.3193149853946418, + "gpu_bwutil_python": 0.3175097001507445, + "gpu_gap_stddev_us": 0.9332964028741088 + } + } + } + }, + "reformat_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "rowAlignment": [ + 0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Reformat single-channel (advanced)." + }, + "baselines": { + "reformat_scalar_advanced[InOutDataType=float32][shape=128x1080x1920][inputKind=Tensor][rowAlignment=0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1730.08828475554, + "gpu_time_us_python": 1739.2583135101, + "gpu_noise_us_cpp": 1.922, + "gpu_noise_us_python": 2.7640000000000002, + "gpu_bwutil_cpp": 0.7891718555974421, + "gpu_bwutil_python": 0.7850107830825429, + "gpu_gap_stddev_us": 0.8894379593582329 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1615.90944782374, + "gpu_time_us_python": 1626.3716740758002, + "gpu_noise_us_cpp": 1.3199999999999998, + "gpu_noise_us_python": 2.784, + "gpu_bwutil_cpp": 0.6444407065418785, + "gpu_bwutil_python": 0.6402984857339306, + "gpu_gap_stddev_us": 2.484560859467395 + } + } + } + }, + "reformat_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "rowAlignment": [ + 0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Reformat RGBf32 (basic)." + }, + "baselines": { + "reformat_float3_basic[InOutDataType=float3][shape=32x1080x1920][inputKind=Tensor][rowAlignment=0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1537.68868241568, + "gpu_time_us_python": 1547.02251546172, + "gpu_noise_us_cpp": 1.3, + "gpu_noise_us_python": 2.298, + "gpu_bwutil_cpp": 0.6659346078565734, + "gpu_bwutil_python": 0.6619169651854969, + "gpu_gap_stddev_us": 0.795856943656914 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1322.76251646838, + "gpu_time_us_python": 1331.91808703294, + "gpu_noise_us_cpp": 1.9660000000000004, + "gpu_noise_us_python": 3.1439999999999997, + "gpu_bwutil_cpp": 0.5904460348781173, + "gpu_bwutil_python": 0.5863876044906473, + "gpu_gap_stddev_us": 0.7996350776454937 + } + } + } + }, + "reformat_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "inputKind": [ + "Tensor" + ] + }, + "int64_axes": { + "rowAlignment": [ + 0 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Reformat RGBAf32 (advanced)." + }, + "baselines": { + "reformat_float4_advanced[InOutDataType=float4][shape=32x1080x1920][inputKind=Tensor][rowAlignment=0]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2674.9560599631004, + "gpu_time_us_python": 2685.4639098998805, + "gpu_noise_us_cpp": 1.9619999999999997, + "gpu_noise_us_python": 4.174, + "gpu_bwutil_cpp": 0.5104134024276253, + "gpu_bwutil_python": 0.5084164065861879, + "gpu_gap_stddev_us": 1.9005271460642323 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2324.5181631158403, + "gpu_time_us_python": 2333.7571019653797, + "gpu_noise_us_cpp": 3.586, + "gpu_noise_us_python": 5.4719999999999995, + "gpu_bwutil_cpp": 0.4479884846799965, + "gpu_bwutil_python": 0.4462152475596023, + "gpu_gap_stddev_us": 1.0346215899293107 + } + } + } + } + } +} diff --git a/bench/config/operators/remap.json b/bench/config/operators/remap.json new file mode 100644 index 000000000..b98f2de4b --- /dev/null +++ b/bench/config/operators/remap.json @@ -0,0 +1,721 @@ +{ + "benchmark": "remap", + "configs": { + "remap_tensor_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "mapType": [ + "DENSE" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Remap RGB8 tensor (basic)." + }, + "baselines": { + "remap_tensor_uchar3_basic[InOutDataType=uchar3][shape=16x1080x1920][mapType=DENSE][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 673.9441964012, + "gpu_time_us_python": 687.34780225596, + "gpu_noise_us_cpp": 2.54, + "gpu_noise_us_python": 2.692, + "gpu_bwutil_cpp": 0.4431642922104812, + "gpu_bwutil_python": 0.43452299434440605, + "gpu_gap_stddev_us": 1.1765439126485593 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 445.42168147815, + "gpu_time_us_python": 457.269428883875, + "gpu_noise_us_cpp": 1.5325, + "gpu_noise_us_python": 3.0225000000000004, + "gpu_bwutil_cpp": 0.5114788218328481, + "gpu_bwutil_python": 0.49822165146587105, + "gpu_gap_stddev_us": 0.5016996994321169 + } + } + } + }, + "remap_tensor_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "mapType": [ + "DENSE" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Remap RGBA8 tensor (advanced)." + }, + "baselines": { + "remap_tensor_uchar4_advanced[InOutDataType=uchar4][shape=16x1080x1920][mapType=DENSE][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 417.5316979318401, + "gpu_time_us_python": 430.09944170678, + "gpu_noise_us_cpp": 0.9100000000000001, + "gpu_noise_us_python": 2.7439999999999998, + "gpu_bwutil_cpp": 0.8175031003373994, + "gpu_bwutil_python": 0.7936269181668238, + "gpu_gap_stddev_us": 1.7465135559259164 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 311.279972840375, + "gpu_time_us_python": 325.5256006257, + "gpu_noise_us_cpp": 0.97, + "gpu_noise_us_python": 4.4399999999999995, + "gpu_bwutil_cpp": 0.8363715723398388, + "gpu_bwutil_python": 0.799819832290862, + "gpu_gap_stddev_us": 2.0309197610015794 + } + } + } + }, + "remap_tensor_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "mapType": [ + "DENSE" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Remap single-channel tensor (advanced)." + }, + "baselines": { + "remap_tensor_scalar_advanced[InOutDataType=float32][shape=16x1080x1920][mapType=DENSE][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 418.20401758854007, + "gpu_time_us_python": 430.2840637132, + "gpu_noise_us_cpp": 1.176, + "gpu_noise_us_python": 2.052, + "gpu_bwutil_cpp": 0.8161893489559923, + "gpu_bwutil_python": 0.793276796764108, + "gpu_gap_stddev_us": 0.7329084773290292 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 309.92069328095, + "gpu_time_us_python": 324.11286933302495, + "gpu_noise_us_cpp": 1.4925, + "gpu_noise_us_python": 3.4274999999999998, + "gpu_bwutil_cpp": 0.8400408713512978, + "gpu_bwutil_python": 0.803361900876735, + "gpu_gap_stddev_us": 3.968075057753938 + } + }, + "remap_tensor_scalar_advanced[InOutDataType=uint8][shape=16x1080x1920][mapType=DENSE][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 387.07229414874, + "gpu_time_us_python": 398.9987362927601, + "gpu_noise_us_cpp": 1.462, + "gpu_noise_us_python": 3.2320000000000007, + "gpu_bwutil_cpp": 0.5511463127250847, + "gpu_bwutil_python": 0.5346732299782218, + "gpu_gap_stddev_us": 0.5098199048463476 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 298.8606777578, + "gpu_time_us_python": 312.69085546177496, + "gpu_noise_us_cpp": 1.3, + "gpu_noise_us_python": 3.3699999999999997, + "gpu_bwutil_cpp": 0.5444766340326138, + "gpu_bwutil_python": 0.5204467231947073, + "gpu_gap_stddev_us": 3.342425288359025 + } + } + } + }, + "remap_varshape_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "mapType": [ + "DENSE" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Remap RGB8 varshape (basic)." + }, + "baselines": { + "remap_varshape_uchar3_basic[InOutDataType=uchar3][shape=32x1080x1920][mapType=DENSE][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1428.56293240068, + "gpu_time_us_python": 1445.12868218142, + "gpu_noise_us_cpp": 1.572, + "gpu_noise_us_python": 3.1300000000000003, + "gpu_bwutil_cpp": 0.41813736526092127, + "gpu_bwutil_python": 0.41334382240049583, + "gpu_gap_stddev_us": 1.7078230968585808 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 994.5397216232, + "gpu_time_us_python": 1011.118628200725, + "gpu_noise_us_cpp": 1.5150000000000001, + "gpu_noise_us_python": 3.5425, + "gpu_bwutil_cpp": 0.45814879997150637, + "gpu_bwutil_python": 0.4506460419684864, + "gpu_gap_stddev_us": 1.8523399041129665 + } + } + } + }, + "remap_varshape_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "mapType": [ + "DENSE" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Remap RGBA8 varshape (advanced)." + }, + "baselines": { + "remap_varshape_uchar4_advanced[InOutDataType=uchar4][shape=64x1080x1920][mapType=DENSE][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1564.4862406362402, + "gpu_time_us_python": 1581.2062844141399, + "gpu_noise_us_cpp": 1.0559999999999998, + "gpu_noise_us_python": 2.584, + "gpu_bwutil_cpp": 0.8727050600047882, + "gpu_bwutil_python": 0.863476685796807, + "gpu_gap_stddev_us": 0.6585991823465694 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1193.9401033579252, + "gpu_time_us_python": 1212.3640148692, + "gpu_noise_us_cpp": 2.2175, + "gpu_noise_us_python": 4.5975, + "gpu_bwutil_cpp": 0.872251605882271, + "gpu_bwutil_python": 0.8589733317757109, + "gpu_gap_stddev_us": 3.391472137604083 + } + } + } + }, + "remap_varshape_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "mapType": [ + "DENSE" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Remap single-channel varshape (advanced)." + }, + "baselines": { + "remap_varshape_scalar_advanced[InOutDataType=float32][shape=64x1080x1920][mapType=DENSE][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1562.8712025487998, + "gpu_time_us_python": 1578.82121209074, + "gpu_noise_us_cpp": 1.188, + "gpu_noise_us_python": 4.266, + "gpu_bwutil_cpp": 0.873606642442077, + "gpu_bwutil_python": 0.864781786297864, + "gpu_gap_stddev_us": 1.101819723901793 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1207.28847382595, + "gpu_time_us_python": 1224.028744980625, + "gpu_noise_us_cpp": 2.0124999999999997, + "gpu_noise_us_python": 3.6999999999999997, + "gpu_bwutil_cpp": 0.8626104975681497, + "gpu_bwutil_python": 0.850808770118269, + "gpu_gap_stddev_us": 1.0574659503830668 + } + }, + "remap_varshape_scalar_advanced[InOutDataType=uint8][shape=64x1080x1920][mapType=DENSE][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1482.32056600804, + "gpu_time_us_python": 1499.0282103057602, + "gpu_noise_us_cpp": 2.628, + "gpu_noise_us_python": 3.024, + "gpu_bwutil_cpp": 0.5756745723201149, + "gpu_bwutil_python": 0.5692584238184406, + "gpu_gap_stddev_us": 0.6120344340035588 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1167.5538944315, + "gpu_time_us_python": 1184.775497881275, + "gpu_noise_us_cpp": 1.3325, + "gpu_noise_us_python": 4.517499999999999, + "gpu_bwutil_cpp": 0.5574939062992372, + "gpu_bwutil_python": 0.5493819550885302, + "gpu_gap_stddev_us": 1.5863366140946957 + } + } + } + }, + "remap_relative_tensor_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "mapType": [ + "RELATIVE" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Remap single-channel relative tensor (advanced)." + }, + "baselines": { + "remap_relative_tensor_1080p_advanced[InOutDataType=float32][shape=16x1080x1920][mapType=RELATIVE][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3992.8490979330395, + "gpu_time_us_python": 4007.16881070816, + "gpu_noise_us_cpp": 4.082, + "gpu_noise_us_python": 5.566, + "gpu_bwutil_cpp": 0.04274363622242674, + "gpu_bwutil_python": 0.04259093849817998, + "gpu_gap_stddev_us": 1.5464366272917134 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 3768.528988920175, + "gpu_time_us_python": 3786.055926864325, + "gpu_noise_us_cpp": 3.265, + "gpu_noise_us_python": 4.995, + "gpu_bwutil_cpp": 0.03454630097195267, + "gpu_bwutil_python": 0.034386945453017376, + "gpu_gap_stddev_us": 4.475042612096792 + } + }, + "remap_relative_tensor_1080p_advanced[InOutDataType=uint8][shape=16x1080x1920][mapType=RELATIVE][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4019.43079977232, + "gpu_time_us_python": 4036.8304497268605, + "gpu_noise_us_cpp": 4.412, + "gpu_noise_us_python": 8.532, + "gpu_bwutil_cpp": 0.010615495168285138, + "gpu_bwutil_python": 0.010569731326178519, + "gpu_gap_stddev_us": 3.629411838169495 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 3789.0678493574746, + "gpu_time_us_python": 3806.867304492075, + "gpu_noise_us_cpp": 3.1, + "gpu_noise_us_python": 6.0175, + "gpu_bwutil_cpp": 0.008589982554000175, + "gpu_bwutil_python": 0.008549784766076774, + "gpu_gap_stddev_us": 3.1752794818102195 + } + } + } + }, + "remap_relative_varshape_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "mapType": [ + "RELATIVE" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Remap single-channel relative varshape (advanced)." + }, + "baselines": { + "remap_relative_varshape_1080p_advanced[InOutDataType=float32][shape=16x1080x1920][mapType=RELATIVE][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4212.36388524368, + "gpu_time_us_python": 4229.8092598295, + "gpu_noise_us_cpp": 4.483999999999999, + "gpu_noise_us_python": 5.5920000000000005, + "gpu_bwutil_cpp": 0.04051619284790476, + "gpu_bwutil_python": 0.040349097700871764, + "gpu_gap_stddev_us": 1.5491571673775575 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 3948.3236854329, + "gpu_time_us_python": 3967.69164694385, + "gpu_noise_us_cpp": 4.027499999999999, + "gpu_noise_us_python": 9.1475, + "gpu_bwutil_cpp": 0.03297312950515027, + "gpu_bwutil_python": 0.03281203142670645, + "gpu_gap_stddev_us": 3.695685201693771 + } + }, + "remap_relative_varshape_1080p_advanced[InOutDataType=uint8][shape=16x1080x1920][mapType=RELATIVE][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4225.95988227818, + "gpu_time_us_python": 4244.15534097592, + "gpu_noise_us_cpp": 7.108000000000001, + "gpu_noise_us_python": 6.411999999999999, + "gpu_bwutil_cpp": 0.01009669716612582, + "gpu_bwutil_python": 0.010053411583715099, + "gpu_gap_stddev_us": 1.4687765073857948 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 3989.4871921352005, + "gpu_time_us_python": 4005.806108079675, + "gpu_noise_us_cpp": 4.03, + "gpu_noise_us_python": 4.305, + "gpu_bwutil_cpp": 0.008158409098806826, + "gpu_bwutil_python": 0.00812523653595805, + "gpu_gap_stddev_us": 2.227443733824062 + } + } + } + }, + "remap_planar_nchw_tensor_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "mapType": [ + "DENSE" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Remap RGB8 tensor (basic) (planar NCHW)." + }, + "baselines": { + "remap_planar_nchw_tensor_uchar3_basic[InOutDataType=uchar3][shape=16x1080x1920][mapType=DENSE][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1088.8010977168, + "gpu_time_us_python": 1101.5967363132, + "gpu_noise_us_cpp": 1.362, + "gpu_noise_us_python": 2.732, + "gpu_bwutil_cpp": 0.27430818515715405, + "gpu_bwutil_python": 0.2711219777426052, + "gpu_gap_stddev_us": 0.7937159582485137 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 685.013548220275, + "gpu_time_us_python": 697.0826628612999, + "gpu_noise_us_cpp": 1.705, + "gpu_noise_us_python": 3.0324999999999998, + "gpu_bwutil_cpp": 0.33262001990214224, + "gpu_bwutil_python": 0.326872905646594, + "gpu_gap_stddev_us": 1.5381318358486438 + } + } + } + }, + "remap_planar_nchw_tensor_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "mapType": [ + "DENSE" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Remap RGBA8 tensor (advanced) (planar NCHW)." + }, + "baselines": { + "remap_planar_nchw_tensor_uchar4_advanced[InOutDataType=uchar4][shape=16x1080x1920][mapType=DENSE][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1524.42060086868, + "gpu_time_us_python": 1537.86834441806, + "gpu_noise_us_cpp": 1.9, + "gpu_noise_us_python": 4.144, + "gpu_bwutil_cpp": 0.2239105247540421, + "gpu_bwutil_python": 0.2219524240026542, + "gpu_gap_stddev_us": 0.6175230770638319 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 987.9596260942001, + "gpu_time_us_python": 1002.776314382875, + "gpu_noise_us_cpp": 2.2975, + "gpu_noise_us_python": 3.9575000000000005, + "gpu_bwutil_cpp": 0.2635944181671688, + "gpu_bwutil_python": 0.2597082743970643, + "gpu_gap_stddev_us": 1.5415164058548925 + } + } + } + }, + "remap_planar_nchw_varshape_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "mapType": [ + "DENSE" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Remap RGB8 varshape (basic) (planar NCHW)." + }, + "baselines": { + "remap_planar_nchw_varshape_uchar3_basic[InOutDataType=uchar3][shape=32x1080x1920][mapType=DENSE][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2382.25238856596, + "gpu_time_us_python": 2398.84187516708, + "gpu_noise_us_cpp": 1.302, + "gpu_noise_us_python": 4.252, + "gpu_bwutil_cpp": 0.25074357288189836, + "gpu_bwutil_python": 0.2490096021901617, + "gpu_gap_stddev_us": 1.5944298500479868 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1563.628256667625, + "gpu_time_us_python": 1581.60255970255, + "gpu_noise_us_cpp": 1.4400000000000002, + "gpu_noise_us_python": 7.59, + "gpu_bwutil_cpp": 0.291419363299128, + "gpu_bwutil_python": 0.2880907617872052, + "gpu_gap_stddev_us": 5.33753982481769 + } + } + } + }, + "remap_fakeplanar_nchw_tensor_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "mapType": [ + "DENSE" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Remap RGB8 tensor (advanced) (fake planar NCHW_FAKE)." + }, + "baselines": { + "remap_fakeplanar_nchw_tensor_uchar3_advanced[InOutDataType=uchar3][shape=16x1080x1920][mapType=DENSE][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1278.7398371160598, + "gpu_time_us_python": 1296.93555458632, + "gpu_noise_us_cpp": 2.948, + "gpu_noise_us_python": 4.486, + "gpu_bwutil_cpp": 0.23356376022965022, + "gpu_bwutil_python": 0.2302872953495771, + "gpu_gap_stddev_us": 2.4759581946912705 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 918.4792931057749, + "gpu_time_us_python": 933.772834271975, + "gpu_noise_us_cpp": 1.8775, + "gpu_noise_us_python": 5.185, + "gpu_bwutil_cpp": 0.248027087318382, + "gpu_bwutil_python": 0.24396115532616178, + "gpu_gap_stddev_us": 2.3511900794379663 + } + } + } + }, + "remap_fakeplanar_nchw_tensor_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "mapType": [ + "DENSE" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Remap RGBA8 tensor (advanced) (fake planar NCHW_FAKE)." + }, + "baselines": { + "remap_fakeplanar_nchw_tensor_uchar4_advanced[InOutDataType=uchar4][shape=16x1080x1920][mapType=DENSE][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1194.31073711026, + "gpu_time_us_python": 1211.34947267702, + "gpu_noise_us_cpp": 2.0919999999999996, + "gpu_noise_us_python": 3.096, + "gpu_bwutil_cpp": 0.28579965555936404, + "gpu_bwutil_python": 0.28177990985325563, + "gpu_gap_stddev_us": 1.3934206114800665 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 925.159531983325, + "gpu_time_us_python": 941.134228102175, + "gpu_noise_us_cpp": 1.305, + "gpu_noise_us_python": 4.9475, + "gpu_bwutil_cpp": 0.2814069799795094, + "gpu_bwutil_python": 0.27663082938467426, + "gpu_gap_stddev_us": 1.1581858312746878 + } + } + } + } + } +} diff --git a/bench/config/operators/resize.json b/bench/config/operators/resize.json new file mode 100644 index 000000000..db9705584 --- /dev/null +++ b/bench/config/operators/resize.json @@ -0,0 +1,11420 @@ +{ + "benchmark": "resize", + "configs": { + "resize_contract_area_tensor_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "AREA" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGB8 contract area tensor (basic)." + }, + "baselines": { + "resize_contract_area_tensor_uchar3_basic[InOutDataType=uchar3][shape=32x1080x1920][resizeType=CONTRACT][interpolation=AREA][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 201.13007667678, + "gpu_time_us_python": 210.96058059666, + "gpu_noise_us_cpp": 1.0260000000000002, + "gpu_noise_us_python": 2.87, + "gpu_bwutil_cpp": 0.7955058393267159, + "gpu_bwutil_python": 0.7584417601906086, + "gpu_gap_stddev_us": 0.6523036724958258 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 153.40894716802, + "gpu_time_us_python": 161.63481881922002, + "gpu_noise_us_cpp": 1.1460000000000001, + "gpu_noise_us_python": 2.434, + "gpu_bwutil_cpp": 0.7954988074267326, + "gpu_bwutil_python": 0.7550552580025618, + "gpu_gap_stddev_us": 0.8989472359221989 + } + } + } + }, + "resize_contract_area_tensor_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "AREA" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBf32 contract area tensor (basic)." + }, + "baselines": { + "resize_contract_area_tensor_float3_basic[InOutDataType=float3][shape=16x1080x1920][resizeType=CONTRACT][interpolation=AREA][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 372.27513069968, + "gpu_time_us_python": 381.9017069953, + "gpu_noise_us_cpp": 1.334, + "gpu_noise_us_python": 2.512, + "gpu_bwutil_cpp": 0.8595797861294441, + "gpu_bwutil_python": 0.8379199132613977, + "gpu_gap_stddev_us": 1.0731187381704745 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 284.06863568038, + "gpu_time_us_python": 292.77986438708, + "gpu_noise_us_cpp": 1.204, + "gpu_noise_us_python": 2.108, + "gpu_bwutil_cpp": 0.8591931324349277, + "gpu_bwutil_python": 0.8336473068846896, + "gpu_gap_stddev_us": 1.1620054939401692 + } + } + } + }, + "resize_contract_area_tensor_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "AREA" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBA8 contract area tensor (advanced)." + }, + "baselines": { + "resize_contract_area_tensor_uchar4_advanced[InOutDataType=uchar4][shape=32x1080x1920][resizeType=CONTRACT][interpolation=AREA][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 255.97621188514, + "gpu_time_us_python": 265.98240283344, + "gpu_noise_us_cpp": 1.1039999999999999, + "gpu_noise_us_python": 2.352, + "gpu_bwutil_cpp": 0.83341117763236, + "gpu_bwutil_python": 0.8020708048757298, + "gpu_gap_stddev_us": 1.3114621978956569 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 196.90905191496, + "gpu_time_us_python": 205.98887918484002, + "gpu_noise_us_cpp": 1.32, + "gpu_noise_us_python": 2.714, + "gpu_bwutil_cpp": 0.8263414502371044, + "gpu_bwutil_python": 0.7900128346301152, + "gpu_gap_stddev_us": 2.410004493281469 + } + } + } + }, + "resize_contract_area_tensor_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "AREA" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBAf32 contract area tensor (advanced)." + }, + "baselines": { + "resize_contract_area_tensor_float4_advanced[InOutDataType=float4][shape=16x1080x1920][resizeType=CONTRACT][interpolation=AREA][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 487.29535674312, + "gpu_time_us_python": 497.8363782627401, + "gpu_noise_us_cpp": 1.7079999999999997, + "gpu_noise_us_python": 2.8, + "gpu_bwutil_cpp": 0.8755814982987122, + "gpu_bwutil_python": 0.8570471669452002, + "gpu_gap_stddev_us": 1.629685702316838 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 365.88944288519997, + "gpu_time_us_python": 374.28438118056, + "gpu_noise_us_cpp": 0.8200000000000001, + "gpu_noise_us_python": 2.214, + "gpu_bwutil_cpp": 0.8894070515717377, + "gpu_bwutil_python": 0.8694589670300628, + "gpu_gap_stddev_us": 0.39824165487376634 + } + } + } + }, + "resize_contract_area_tensor_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "AREA" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize single-channel contract area tensor (advanced)." + }, + "baselines": { + "resize_contract_area_tensor_scalar_advanced[InOutDataType=float32][shape=128x1080x1920][resizeType=CONTRACT][interpolation=AREA][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 949.8073320045199, + "gpu_time_us_python": 960.2381480150801, + "gpu_noise_us_cpp": 1.366, + "gpu_noise_us_python": 2.274, + "gpu_bwutil_cpp": 0.8984279483466434, + "gpu_bwutil_python": 0.8886689192835213, + "gpu_gap_stddev_us": 0.8153503396707159 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 713.0303637254199, + "gpu_time_us_python": 723.2796619664, + "gpu_noise_us_cpp": 1.6939999999999997, + "gpu_noise_us_python": 4.32, + "gpu_bwutil_cpp": 0.912791219050401, + "gpu_bwutil_python": 0.8998637895927815, + "gpu_gap_stddev_us": 1.9849006604364792 + } + }, + "resize_contract_area_tensor_scalar_advanced[InOutDataType=uint8][shape=128x1080x1920][resizeType=CONTRACT][interpolation=AREA][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 259.66618335754004, + "gpu_time_us_python": 269.50948575528, + "gpu_noise_us_cpp": 2.116, + "gpu_noise_us_python": 3.158, + "gpu_bwutil_cpp": 0.8215679226373165, + "gpu_bwutil_python": 0.7915699632932139, + "gpu_gap_stddev_us": 1.0575622679353827 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 194.1237309236, + "gpu_time_us_python": 203.16485758248, + "gpu_noise_us_cpp": 1.1239999999999999, + "gpu_noise_us_python": 2.592, + "gpu_bwutil_cpp": 0.8381975353403914, + "gpu_bwutil_python": 0.8009420714354096, + "gpu_gap_stddev_us": 1.3405313220419992 + } + } + } + }, + "resize_contract_area_varshape_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "AREA" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGB8 contract area varshape (basic)." + }, + "baselines": { + "resize_contract_area_varshape_uchar3_basic[InOutDataType=uchar3][shape=32x1080x1920][resizeType=CONTRACT][interpolation=AREA][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 211.6137090697, + "gpu_time_us_python": 222.93080329376, + "gpu_noise_us_cpp": 2.512, + "gpu_noise_us_python": 2.9699999999999998, + "gpu_bwutil_cpp": 0.7561601830217092, + "gpu_bwutil_python": 0.7177770874433244, + "gpu_gap_stddev_us": 1.3041419867633728 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 165.25386313578002, + "gpu_time_us_python": 175.15097008204, + "gpu_noise_us_cpp": 1.6239999999999999, + "gpu_noise_us_python": 2.836, + "gpu_bwutil_cpp": 0.7385113197254991, + "gpu_bwutil_python": 0.6969117803843005, + "gpu_gap_stddev_us": 1.9699812991596868 + } + } + } + }, + "resize_contract_area_varshape_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "AREA" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBf32 contract area varshape (advanced)." + }, + "baselines": { + "resize_contract_area_varshape_float3_advanced[InOutDataType=float3][shape=16x1080x1920][resizeType=CONTRACT][interpolation=AREA][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 390.36313377740004, + "gpu_time_us_python": 401.22398377944, + "gpu_noise_us_cpp": 1.97, + "gpu_noise_us_python": 2.854, + "gpu_bwutil_cpp": 0.8197523058223066, + "gpu_bwutil_python": 0.7975660408570269, + "gpu_gap_stddev_us": 0.5744264645536713 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 298.61789148658005, + "gpu_time_us_python": 308.19083055740003, + "gpu_noise_us_cpp": 2.944, + "gpu_noise_us_python": 3.066, + "gpu_bwutil_cpp": 0.8173385740864294, + "gpu_bwutil_python": 0.7919588027849478, + "gpu_gap_stddev_us": 0.6158171257990586 + } + } + } + }, + "resize_contract_area_varshape_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "AREA" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBA8 contract area varshape (advanced)." + }, + "baselines": { + "resize_contract_area_varshape_uchar4_advanced[InOutDataType=uchar4][shape=32x1080x1920][resizeType=CONTRACT][interpolation=AREA][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 296.12538642850006, + "gpu_time_us_python": 309.32357790085996, + "gpu_noise_us_cpp": 2.4979999999999998, + "gpu_noise_us_python": 3.692, + "gpu_bwutil_cpp": 0.7204160508115429, + "gpu_bwutil_python": 0.6896968874683861, + "gpu_gap_stddev_us": 1.7670607197381027 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 227.64571794755997, + "gpu_time_us_python": 239.0859795758, + "gpu_noise_us_cpp": 2.912, + "gpu_noise_us_python": 3.806, + "gpu_bwutil_cpp": 0.7147836952814608, + "gpu_bwutil_python": 0.6806714270081271, + "gpu_gap_stddev_us": 2.5780629530877475 + } + } + } + }, + "resize_contract_area_varshape_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "AREA" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBAf32 contract area varshape (advanced)." + }, + "baselines": { + "resize_contract_area_varshape_float4_advanced[InOutDataType=float4][shape=16x1080x1920][resizeType=CONTRACT][interpolation=AREA][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 493.98327280118, + "gpu_time_us_python": 505.43845669832, + "gpu_noise_us_cpp": 2.4579999999999997, + "gpu_noise_us_python": 2.5060000000000002, + "gpu_bwutil_cpp": 0.8637282185987525, + "gpu_bwutil_python": 0.8441529111920107, + "gpu_gap_stddev_us": 0.1835644234301337 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 374.40321925428, + "gpu_time_us_python": 384.90628130438, + "gpu_noise_us_cpp": 2.418, + "gpu_noise_us_python": 3.436, + "gpu_bwutil_cpp": 0.8691940637885693, + "gpu_bwutil_python": 0.8454775615424651, + "gpu_gap_stddev_us": 0.5092063812567755 + } + } + } + }, + "resize_contract_area_varshape_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "AREA" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize single-channel contract area varshape (advanced)." + }, + "baselines": { + "resize_contract_area_varshape_scalar_advanced[InOutDataType=float32][shape=128x1080x1920][resizeType=CONTRACT][interpolation=AREA][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 974.2803494784999, + "gpu_time_us_python": 990.91338994738, + "gpu_noise_us_cpp": 4.45, + "gpu_noise_us_python": 5.338, + "gpu_bwutil_cpp": 0.8758638047881053, + "gpu_bwutil_python": 0.861164171337151, + "gpu_gap_stddev_us": 0.7204430767725037 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 752.40199408634, + "gpu_time_us_python": 767.0150635408401, + "gpu_noise_us_cpp": 4.5440000000000005, + "gpu_noise_us_python": 5.87, + "gpu_bwutil_cpp": 0.8650621835465927, + "gpu_bwutil_python": 0.8485930211728929, + "gpu_gap_stddev_us": 2.7880479440104406 + } + }, + "resize_contract_area_varshape_scalar_advanced[InOutDataType=uint8][shape=128x1080x1920][resizeType=CONTRACT][interpolation=AREA][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 286.94619986812, + "gpu_time_us_python": 303.10779390524, + "gpu_noise_us_cpp": 3.8560000000000003, + "gpu_noise_us_python": 4.2219999999999995, + "gpu_bwutil_cpp": 0.7434881373950157, + "gpu_bwutil_python": 0.7038884457954703, + "gpu_gap_stddev_us": 2.251240343639516 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 217.23337445095999, + "gpu_time_us_python": 230.75694910776002, + "gpu_noise_us_cpp": 3.6499999999999995, + "gpu_noise_us_python": 4.8020000000000005, + "gpu_bwutil_cpp": 0.7493224976724067, + "gpu_bwutil_python": 0.7053593053173536, + "gpu_gap_stddev_us": 1.5421223097536314 + } + } + } + }, + "resize_contract_linear_720p_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "64x720x1280" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBf32 contract linear (advanced)." + }, + "baselines": { + "resize_contract_linear_720p_float3_advanced[InOutDataType=float3][shape=64x720x1280][resizeType=CONTRACT][interpolation=LINEAR][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 648.67268828524, + "gpu_time_us_python": 658.3736546403, + "gpu_noise_us_cpp": 1.066, + "gpu_noise_us_python": 2.1879999999999997, + "gpu_bwutil_cpp": 0.8770046321374118, + "gpu_bwutil_python": 0.8640833584882113, + "gpu_gap_stddev_us": 0.7916367395554282 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 495.19947414852004, + "gpu_time_us_python": 504.08542565306004, + "gpu_noise_us_cpp": 1.3559999999999999, + "gpu_noise_us_python": 3.044, + "gpu_bwutil_cpp": 0.8762112814484604, + "gpu_bwutil_python": 0.8607656003030646, + "gpu_gap_stddev_us": 0.5287709335784745 + } + } + } + }, + "resize_contract_linear_720p_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "64x720x1280" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBAf32 contract linear (advanced)." + }, + "baselines": { + "resize_contract_linear_720p_float4_advanced[InOutDataType=float4][shape=64x720x1280][resizeType=CONTRACT][interpolation=LINEAR][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 848.59957581856, + "gpu_time_us_python": 858.50294721016, + "gpu_noise_us_cpp": 1.5539999999999998, + "gpu_noise_us_python": 2.432, + "gpu_bwutil_cpp": 0.8938475578916121, + "gpu_bwutil_python": 0.8835369668150641, + "gpu_gap_stddev_us": 0.6357331182376809 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 635.8702592427801, + "gpu_time_us_python": 645.29135566434, + "gpu_noise_us_cpp": 1.702, + "gpu_noise_us_python": 3.6819999999999995, + "gpu_bwutil_cpp": 0.909826640143341, + "gpu_bwutil_python": 0.8965436474043391, + "gpu_gap_stddev_us": 0.6368450357576543 + } + } + } + }, + "resize_contract_linear_720p_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "128x720x1280" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize single-channel contract linear (advanced)." + }, + "baselines": { + "resize_contract_linear_720p_scalar_advanced[InOutDataType=float32][shape=128x720x1280][resizeType=CONTRACT][interpolation=LINEAR][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 438.82603816811996, + "gpu_time_us_python": 448.99287912206, + "gpu_noise_us_cpp": 1.284, + "gpu_noise_us_python": 1.8820000000000001, + "gpu_bwutil_cpp": 0.8642588345415569, + "gpu_bwutil_python": 0.8446923681254981, + "gpu_gap_stddev_us": 0.97955012858665 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 333.32461638791995, + "gpu_time_us_python": 342.25027381908, + "gpu_noise_us_cpp": 1.108, + "gpu_noise_us_python": 2.7119999999999997, + "gpu_bwutil_cpp": 0.8678224272729214, + "gpu_bwutil_python": 0.8452052047588745, + "gpu_gap_stddev_us": 1.182752355916054 + } + } + } + }, + "resize_contract_linear_1080p_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBf32 contract linear (basic)." + }, + "baselines": { + "resize_contract_linear_1080p_float3_basic[InOutDataType=float3][shape=32x1080x1920][resizeType=CONTRACT][interpolation=LINEAR][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 726.72924198942, + "gpu_time_us_python": 736.48077462582, + "gpu_noise_us_cpp": 1.3459999999999999, + "gpu_noise_us_python": 2.976, + "gpu_bwutil_cpp": 0.8806581702148157, + "gpu_bwutil_python": 0.8689992209309297, + "gpu_gap_stddev_us": 1.2925726817884229 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 555.3099746145601, + "gpu_time_us_python": 564.77320139444, + "gpu_noise_us_cpp": 1.766, + "gpu_noise_us_python": 2.8620000000000005, + "gpu_bwutil_cpp": 0.8790350517297016, + "gpu_bwutil_python": 0.8643175124705056, + "gpu_gap_stddev_us": 1.9050909502765803 + } + } + } + }, + "resize_contract_linear_1080p_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBAf32 contract linear (advanced)." + }, + "baselines": { + "resize_contract_linear_1080p_float4_advanced[InOutDataType=float4][shape=32x1080x1920][resizeType=CONTRACT][interpolation=LINEAR][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 952.3799439346401, + "gpu_time_us_python": 962.6045805738801, + "gpu_noise_us_cpp": 1.45, + "gpu_noise_us_python": 4.148, + "gpu_bwutil_cpp": 0.8960012897222678, + "gpu_bwutil_python": 0.8864853217722388, + "gpu_gap_stddev_us": 1.2333980868177428 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 713.1176975904, + "gpu_time_us_python": 722.1121331956799, + "gpu_noise_us_cpp": 1.284, + "gpu_noise_us_python": 1.9159999999999997, + "gpu_bwutil_cpp": 0.9126800923448618, + "gpu_bwutil_python": 0.9013127419376449, + "gpu_gap_stddev_us": 1.6181195262331378 + } + } + } + }, + "resize_contract_linear_1080p_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize single-channel contract linear (advanced)." + }, + "baselines": { + "resize_contract_linear_1080p_scalar_advanced[InOutDataType=float32][shape=64x1080x1920][resizeType=CONTRACT][interpolation=LINEAR][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 491.89578461969995, + "gpu_time_us_python": 502.90429750168005, + "gpu_noise_us_cpp": 1.1960000000000002, + "gpu_noise_us_python": 2.474, + "gpu_bwutil_cpp": 0.8673927688997691, + "gpu_bwutil_python": 0.8484194758838675, + "gpu_gap_stddev_us": 2.4959429683604686 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 374.48382852282003, + "gpu_time_us_python": 383.36729182235996, + "gpu_noise_us_cpp": 1.9440000000000002, + "gpu_noise_us_python": 2.734, + "gpu_bwutil_cpp": 0.868995036504435, + "gpu_bwutil_python": 0.8488641032110744, + "gpu_gap_stddev_us": 0.7198024824075215 + } + } + } + }, + "resize_contract_linear_4k_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "8x2160x3840" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBf32 contract linear (advanced)." + }, + "baselines": { + "resize_contract_linear_4k_float3_advanced[InOutDataType=float3][shape=8x2160x3840][resizeType=CONTRACT][interpolation=LINEAR][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 726.6706613940202, + "gpu_time_us_python": 737.4255394202801, + "gpu_noise_us_cpp": 1.1199999999999999, + "gpu_noise_us_python": 2.206, + "gpu_bwutil_cpp": 0.8807291419461609, + "gpu_bwutil_python": 0.8678900160923171, + "gpu_gap_stddev_us": 2.270933841295752 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 555.09049167866, + "gpu_time_us_python": 564.07559349142, + "gpu_noise_us_cpp": 1.478, + "gpu_noise_us_python": 2.866, + "gpu_bwutil_cpp": 0.8793821310615915, + "gpu_bwutil_python": 0.865379067143816, + "gpu_gap_stddev_us": 0.8513320473930771 + } + } + } + }, + "resize_contract_linear_4k_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "8x2160x3840" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBAf32 contract linear (advanced)." + }, + "baselines": { + "resize_contract_linear_4k_float4_advanced[InOutDataType=float4][shape=8x2160x3840][resizeType=CONTRACT][interpolation=LINEAR][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 951.92829356894, + "gpu_time_us_python": 963.6965145972401, + "gpu_noise_us_cpp": 1.1760000000000002, + "gpu_noise_us_python": 3.0940000000000003, + "gpu_bwutil_cpp": 0.896426268186611, + "gpu_bwutil_python": 0.885483061734379, + "gpu_gap_stddev_us": 1.9434787548617003 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 713.0638037990601, + "gpu_time_us_python": 723.71750740476, + "gpu_noise_us_cpp": 0.8879999999999999, + "gpu_noise_us_python": 2.9400000000000004, + "gpu_bwutil_cpp": 0.9127484854669673, + "gpu_bwutil_python": 0.8993255882628004, + "gpu_gap_stddev_us": 3.5124991482362975 + } + } + } + }, + "resize_contract_linear_4k_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "16x2160x3840" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize single-channel contract linear (advanced)." + }, + "baselines": { + "resize_contract_linear_4k_scalar_advanced[InOutDataType=float32][shape=16x2160x3840][resizeType=CONTRACT][interpolation=LINEAR][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 490.76264277388, + "gpu_time_us_python": 501.37375971131996, + "gpu_noise_us_cpp": 1.184, + "gpu_noise_us_python": 2.886, + "gpu_bwutil_cpp": 0.8693952521454502, + "gpu_bwutil_python": 0.8509958180101238, + "gpu_gap_stddev_us": 0.32660078380486446 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 373.09536347626, + "gpu_time_us_python": 381.98472553604, + "gpu_noise_us_cpp": 1.786, + "gpu_noise_us_python": 2.684, + "gpu_bwutil_cpp": 0.8722290173778019, + "gpu_bwutil_python": 0.8519439996493736, + "gpu_gap_stddev_us": 1.1861450253048342 + } + } + } + }, + "resize_contract_cubic_720p_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "64x720x1280" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBf32 contract cubic (advanced)." + }, + "baselines": { + "resize_contract_cubic_720p_float3_advanced[InOutDataType=float3][shape=64x720x1280][resizeType=CONTRACT][interpolation=CUBIC][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 695.15775379202, + "gpu_time_us_python": 703.9562758866799, + "gpu_noise_us_cpp": 1.832, + "gpu_noise_us_python": 2.404, + "gpu_bwutil_cpp": 0.818360048230325, + "gpu_bwutil_python": 0.8081344869148188, + "gpu_gap_stddev_us": 1.0083583418804989 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 611.62126414986, + "gpu_time_us_python": 620.04305201036, + "gpu_noise_us_cpp": 2.3879999999999995, + "gpu_noise_us_python": 3.2659999999999996, + "gpu_bwutil_cpp": 0.70943599444059, + "gpu_bwutil_python": 0.6997943868908971, + "gpu_gap_stddev_us": 1.2282542201456843 + } + } + } + }, + "resize_contract_cubic_720p_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "32x720x1280" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBAf32 contract cubic (advanced)." + }, + "baselines": { + "resize_contract_cubic_720p_float4_advanced[InOutDataType=float4][shape=32x720x1280][resizeType=CONTRACT][interpolation=CUBIC][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 466.1821540755401, + "gpu_time_us_python": 475.92362559238, + "gpu_noise_us_cpp": 1.7719999999999998, + "gpu_noise_us_python": 2.7199999999999998, + "gpu_bwutil_cpp": 0.8135512361829372, + "gpu_bwutil_python": 0.7969057872432007, + "gpu_gap_stddev_us": 1.067088367300318 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 350.8118326768, + "gpu_time_us_python": 359.48519800046, + "gpu_noise_us_cpp": 1.302, + "gpu_noise_us_python": 2.792, + "gpu_bwutil_cpp": 0.824562689064017, + "gpu_bwutil_python": 0.8046866942054278, + "gpu_gap_stddev_us": 1.9084795546827908 + } + } + } + }, + "resize_contract_cubic_720p_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "128x720x1280" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize single-channel contract cubic (advanced)." + }, + "baselines": { + "resize_contract_cubic_720p_scalar_advanced[InOutDataType=float32][shape=128x720x1280][resizeType=CONTRACT][interpolation=CUBIC][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 504.45187901134005, + "gpu_time_us_python": 515.4309835183, + "gpu_noise_us_cpp": 1.0459999999999998, + "gpu_noise_us_python": 2.682, + "gpu_bwutil_cpp": 0.7518258303143527, + "gpu_bwutil_python": 0.7358164907750924, + "gpu_gap_stddev_us": 1.2673478584789988 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 459.56125895735994, + "gpu_time_us_python": 469.13674277456005, + "gpu_noise_us_cpp": 0.9279999999999999, + "gpu_noise_us_python": 3.6160000000000005, + "gpu_bwutil_cpp": 0.6295186777296508, + "gpu_bwutil_python": 0.6166737086545796, + "gpu_gap_stddev_us": 1.1366522039367493 + } + } + } + }, + "resize_contract_cubic_1080p_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize RGBf32 contract cubic (basic)." + }, + "baselines": { + "resize_contract_cubic_1080p_float3_basic[InOutDataType=float3][shape=32x1080x1920][resizeType=CONTRACT][interpolation=CUBIC][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 785.4555172232, + "gpu_time_us_python": 794.9885701617801, + "gpu_noise_us_cpp": 1.886, + "gpu_noise_us_python": 3.684, + "gpu_bwutil_cpp": 0.8148140274939897, + "gpu_bwutil_python": 0.8050462054799329, + "gpu_gap_stddev_us": 1.762935046063058 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 686.4130067471, + "gpu_time_us_python": 694.5379105477801, + "gpu_noise_us_cpp": 2.4539999999999997, + "gpu_noise_us_python": 3.104, + "gpu_bwutil_cpp": 0.711157858611102, + "gpu_bwutil_python": 0.7028341160421807, + "gpu_gap_stddev_us": 1.4151935491163967 + } + } + } + }, + "resize_contract_cubic_1080p_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize RGBAf32 contract cubic (advanced)." + }, + "baselines": { + "resize_contract_cubic_1080p_float4_advanced[InOutDataType=float4][shape=16x1080x1920][resizeType=CONTRACT][interpolation=CUBIC][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 525.98016862292, + "gpu_time_us_python": 536.16126231054, + "gpu_noise_us_cpp": 1.9460000000000002, + "gpu_noise_us_python": 3.3920000000000003, + "gpu_bwutil_cpp": 0.8111854100664153, + "gpu_bwutil_python": 0.7957880365901079, + "gpu_gap_stddev_us": 1.2559287203986553 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 394.27143148508, + "gpu_time_us_python": 403.1922601208, + "gpu_noise_us_cpp": 1.434, + "gpu_noise_us_python": 3.3660000000000005, + "gpu_bwutil_cpp": 0.8253808771671383, + "gpu_bwutil_python": 0.8071366822604217, + "gpu_gap_stddev_us": 2.2282022933398027 + } + } + } + }, + "resize_contract_cubic_1080p_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize single-channel contract cubic (advanced)." + }, + "baselines": { + "resize_contract_cubic_1080p_scalar_advanced[InOutDataType=float32][shape=64x1080x1920][resizeType=CONTRACT][interpolation=CUBIC][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 572.8944773322598, + "gpu_time_us_python": 584.7902941186201, + "gpu_noise_us_cpp": 1.386, + "gpu_noise_us_python": 2.1420000000000003, + "gpu_bwutil_cpp": 0.7447591452374087, + "gpu_bwutil_python": 0.7296134147511091, + "gpu_gap_stddev_us": 2.5421511700189394 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 520.4588186810599, + "gpu_time_us_python": 529.99140932568, + "gpu_noise_us_cpp": 0.9339999999999999, + "gpu_noise_us_python": 1.954, + "gpu_bwutil_cpp": 0.6253460193112399, + "gpu_bwutil_python": 0.6141164223090824, + "gpu_gap_stddev_us": 1.0783679918260092 + } + } + } + }, + "resize_contract_cubic_4k_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "8x2160x3840" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBf32 contract cubic (advanced)." + }, + "baselines": { + "resize_contract_cubic_4k_float3_advanced[InOutDataType=float3][shape=8x2160x3840][resizeType=CONTRACT][interpolation=CUBIC][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 795.48805202752, + "gpu_time_us_python": 804.48274078742, + "gpu_noise_us_cpp": 1.9280000000000002, + "gpu_noise_us_python": 4.206, + "gpu_bwutil_cpp": 0.8045375841574577, + "gpu_bwutil_python": 0.7955426384112666, + "gpu_gap_stddev_us": 0.7427712122504788 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 690.669343968, + "gpu_time_us_python": 699.881419744, + "gpu_noise_us_cpp": 2.5759999999999996, + "gpu_noise_us_python": 3.21, + "gpu_bwutil_cpp": 0.7067728510497158, + "gpu_bwutil_python": 0.6974837306927087, + "gpu_gap_stddev_us": 2.0327117086725077 + } + } + } + }, + "resize_contract_cubic_4k_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "4x2160x3840" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBAf32 contract cubic (advanced)." + }, + "baselines": { + "resize_contract_cubic_4k_float4_advanced[InOutDataType=float4][shape=4x2160x3840][resizeType=CONTRACT][interpolation=CUBIC][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 526.6948802089, + "gpu_time_us_python": 536.97517235376, + "gpu_noise_us_cpp": 2.032, + "gpu_noise_us_python": 2.8320000000000003, + "gpu_bwutil_cpp": 0.8100863605963815, + "gpu_bwutil_python": 0.794584263771432, + "gpu_gap_stddev_us": 1.2307683080276253 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 393.05670208106005, + "gpu_time_us_python": 400.84608806444004, + "gpu_noise_us_cpp": 1.2959999999999998, + "gpu_noise_us_python": 2.154, + "gpu_bwutil_cpp": 0.8279327974179196, + "gpu_bwutil_python": 0.8118434015737345, + "gpu_gap_stddev_us": 0.19702612803140876 + } + } + } + }, + "resize_contract_cubic_4k_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "16x2160x3840" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize single-channel contract cubic (advanced)." + }, + "baselines": { + "resize_contract_cubic_4k_scalar_advanced[InOutDataType=float32][shape=16x2160x3840][resizeType=CONTRACT][interpolation=CUBIC][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 564.5466962058201, + "gpu_time_us_python": 574.8177412009999, + "gpu_noise_us_cpp": 0.952, + "gpu_noise_us_python": 2.322, + "gpu_bwutil_cpp": 0.7557716745284406, + "gpu_bwutil_python": 0.7422683588382208, + "gpu_gap_stddev_us": 1.0569912188689319 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 515.38177071976, + "gpu_time_us_python": 524.86675190696, + "gpu_noise_us_cpp": 1.136, + "gpu_noise_us_python": 3.728, + "gpu_bwutil_cpp": 0.6315049201426255, + "gpu_bwutil_python": 0.6200716127370052, + "gpu_gap_stddev_us": 1.2299606093075668 + } + } + } + }, + "resize_expand_linear_720p_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "64x720x1280" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGB8 expand linear (advanced)." + }, + "baselines": { + "resize_expand_linear_720p_uchar3_advanced[InOutDataType=uchar3][shape=64x720x1280][resizeType=EXPAND][interpolation=LINEAR][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1092.14523691904, + "gpu_time_us_python": 1105.0356114636602, + "gpu_noise_us_cpp": 1.482, + "gpu_noise_us_python": 2.234, + "gpu_bwutil_cpp": 0.5208929698760982, + "gpu_bwutil_python": 0.5148154331437158, + "gpu_gap_stddev_us": 2.2811309916699622 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 901.48206114688, + "gpu_time_us_python": 910.58312914622, + "gpu_noise_us_cpp": 1.814, + "gpu_noise_us_python": 2.6639999999999997, + "gpu_bwutil_cpp": 0.481380534152553, + "gpu_bwutil_python": 0.47656333017846747, + "gpu_gap_stddev_us": 0.7589578141868188 + } + } + } + }, + "resize_expand_linear_720p_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "16x720x1280" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBf32 expand linear (advanced)." + }, + "baselines": { + "resize_expand_linear_720p_float3_advanced[InOutDataType=float3][shape=16x720x1280][resizeType=EXPAND][interpolation=LINEAR][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 741.8477608884399, + "gpu_time_us_python": 752.35284412466, + "gpu_noise_us_cpp": 1.488, + "gpu_noise_us_python": 2.5940000000000003, + "gpu_bwutil_cpp": 0.7668551508233444, + "gpu_bwutil_python": 0.7561472931521432, + "gpu_gap_stddev_us": 1.3238769788148743 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 581.84165233786, + "gpu_time_us_python": 588.8811381622, + "gpu_noise_us_cpp": 2.168, + "gpu_noise_us_python": 3.7640000000000002, + "gpu_bwutil_cpp": 0.7457329070393927, + "gpu_bwutil_python": 0.7368200750816071, + "gpu_gap_stddev_us": 0.7293294023366246 + } + } + } + }, + "resize_expand_linear_720p_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "64x720x1280" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBA8 expand linear (advanced)." + }, + "baselines": { + "resize_expand_linear_720p_uchar4_advanced[InOutDataType=uchar4][shape=64x720x1280][resizeType=EXPAND][interpolation=LINEAR][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1350.71092040338, + "gpu_time_us_python": 1361.72694221154, + "gpu_noise_us_cpp": 1.658, + "gpu_noise_us_python": 3.0940000000000003, + "gpu_bwutil_cpp": 0.5615717410944402, + "gpu_bwutil_python": 0.5570287538624467, + "gpu_gap_stddev_us": 1.3199266535936154 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1148.09302829056, + "gpu_time_us_python": 1158.44952645596, + "gpu_noise_us_cpp": 3.3939999999999997, + "gpu_noise_us_python": 3.16, + "gpu_bwutil_cpp": 0.503982161271512, + "gpu_bwutil_python": 0.4994790859658135, + "gpu_gap_stddev_us": 1.788292834354912 + } + } + } + }, + "resize_expand_linear_720p_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x720x1280" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBAf32 expand linear (advanced)." + }, + "baselines": { + "resize_expand_linear_720p_float4_advanced[InOutDataType=float4][shape=16x720x1280][resizeType=EXPAND][interpolation=LINEAR][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 964.8781320857, + "gpu_time_us_python": 975.40344504274, + "gpu_noise_us_cpp": 4.348000000000001, + "gpu_noise_us_python": 5.136000000000001, + "gpu_bwutil_cpp": 0.7861292362502817, + "gpu_bwutil_python": 0.7776464587999118, + "gpu_gap_stddev_us": 1.6179789805673102 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 709.1203362016801, + "gpu_time_us_python": 717.52032258082, + "gpu_noise_us_cpp": 1.9619999999999997, + "gpu_noise_us_python": 4.202, + "gpu_bwutil_cpp": 0.815843765695478, + "gpu_bwutil_python": 0.8062986663456556, + "gpu_gap_stddev_us": 1.9097317302347026 + } + } + } + }, + "resize_expand_linear_720p_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "128x720x1280" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize single-channel expand linear (advanced)." + }, + "baselines": { + "resize_expand_linear_720p_scalar_advanced[InOutDataType=float32][shape=128x720x1280][resizeType=EXPAND][interpolation=LINEAR][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1838.10408333556, + "gpu_time_us_python": 1849.2732319761199, + "gpu_noise_us_cpp": 2.278, + "gpu_noise_us_python": 3.3600000000000003, + "gpu_bwutil_cpp": 0.825327106478475, + "gpu_bwutil_python": 0.8203426681619055, + "gpu_gap_stddev_us": 1.6624434750331996 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1667.19500719984, + "gpu_time_us_python": 1677.4369837267798, + "gpu_noise_us_cpp": 1.016, + "gpu_noise_us_python": 3.3120000000000003, + "gpu_bwutil_cpp": 0.6940589240200826, + "gpu_bwutil_python": 0.6898153302808737, + "gpu_gap_stddev_us": 1.5993501045242027 + } + }, + "resize_expand_linear_720p_scalar_advanced[InOutDataType=uint8][shape=128x720x1280][resizeType=EXPAND][interpolation=LINEAR][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 782.1552845326601, + "gpu_time_us_python": 793.54596545512, + "gpu_noise_us_cpp": 1.0599999999999998, + "gpu_noise_us_python": 3.056, + "gpu_bwutil_cpp": 0.4848925782484231, + "gpu_bwutil_python": 0.4779375859570417, + "gpu_gap_stddev_us": 1.9494298243272754 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 739.5027312902399, + "gpu_time_us_python": 749.03851947184, + "gpu_noise_us_cpp": 1.2499999999999998, + "gpu_noise_us_python": 3.382, + "gpu_bwutil_cpp": 0.3912235807291684, + "gpu_bwutil_python": 0.38622695743159613, + "gpu_gap_stddev_us": 1.7960529586932692 + } + } + } + }, + "resize_expand_linear_1080p_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGB8 expand linear (basic)." + }, + "baselines": { + "resize_expand_linear_1080p_uchar3_basic[InOutDataType=uchar3][shape=32x1080x1920][resizeType=EXPAND][interpolation=LINEAR][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1227.9834822738799, + "gpu_time_us_python": 1240.17431472668, + "gpu_noise_us_cpp": 1.6480000000000001, + "gpu_noise_us_python": 5.973999999999999, + "gpu_bwutil_cpp": 0.5211818100473169, + "gpu_bwutil_python": 0.516060108061661, + "gpu_gap_stddev_us": 1.978918740896097 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1012.47885824154, + "gpu_time_us_python": 1023.51007952776, + "gpu_noise_us_cpp": 2.7880000000000003, + "gpu_noise_us_python": 2.7299999999999995, + "gpu_bwutil_cpp": 0.4821714005491248, + "gpu_bwutil_python": 0.4769887601542585, + "gpu_gap_stddev_us": 2.613808364381393 + } + } + } + }, + "resize_expand_linear_1080p_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "8x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBf32 expand linear (basic)." + }, + "baselines": { + "resize_expand_linear_1080p_float3_basic[InOutDataType=float3][shape=8x1080x1920][resizeType=EXPAND][interpolation=LINEAR][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 835.4985211888, + "gpu_time_us_python": 846.2207895142799, + "gpu_noise_us_cpp": 1.1019999999999999, + "gpu_noise_us_python": 2.6420000000000003, + "gpu_bwutil_cpp": 0.7660104413704956, + "gpu_bwutil_python": 0.7563070046911021, + "gpu_gap_stddev_us": 1.42890095859783 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 654.2485787637, + "gpu_time_us_python": 660.83863558424, + "gpu_noise_us_cpp": 2.436, + "gpu_noise_us_python": 3.6620000000000004, + "gpu_bwutil_cpp": 0.7461013833822763, + "gpu_bwutil_python": 0.7386608517291201, + "gpu_gap_stddev_us": 0.3942012633767961 + } + } + } + }, + "resize_expand_linear_1080p_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBA8 expand linear (advanced)." + }, + "baselines": { + "resize_expand_linear_1080p_uchar4_advanced[InOutDataType=uchar4][shape=32x1080x1920][resizeType=EXPAND][interpolation=LINEAR][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1517.4805528074799, + "gpu_time_us_python": 1529.9084349375798, + "gpu_noise_us_cpp": 1.144, + "gpu_noise_us_python": 3.8120000000000003, + "gpu_bwutil_cpp": 0.5623372482244042, + "gpu_bwutil_python": 0.5577678371645574, + "gpu_gap_stddev_us": 2.063824425058515 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1291.39656593952, + "gpu_time_us_python": 1300.6024229913, + "gpu_noise_us_cpp": 1.1360000000000001, + "gpu_noise_us_python": 2.388, + "gpu_bwutil_cpp": 0.5040680834814701, + "gpu_bwutil_python": 0.5005006752039132, + "gpu_gap_stddev_us": 0.38585540491176346 + } + } + } + }, + "resize_expand_linear_1080p_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "8x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBAf32 expand linear (advanced)." + }, + "baselines": { + "resize_expand_linear_1080p_float4_advanced[InOutDataType=float4][shape=8x1080x1920][resizeType=EXPAND][interpolation=LINEAR][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1085.61377071192, + "gpu_time_us_python": 1096.8348395953599, + "gpu_noise_us_cpp": 4.622, + "gpu_noise_us_python": 5.33, + "gpu_bwutil_cpp": 0.7860380712329447, + "gpu_bwutil_python": 0.7779974386956627, + "gpu_gap_stddev_us": 1.9101252411156764 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 796.45511085766, + "gpu_time_us_python": 803.5803387367799, + "gpu_noise_us_cpp": 2.016, + "gpu_noise_us_python": 3.34, + "gpu_bwutil_cpp": 0.8171806108886737, + "gpu_bwutil_python": 0.8099348992211599, + "gpu_gap_stddev_us": 0.37922350394617665 + } + } + } + }, + "resize_expand_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize single-channel expand (advanced)." + }, + "baselines": { + "resize_expand_advanced[InOutDataType=float32][shape=32x1080x1920][resizeType=EXPAND][interpolation=LINEAR][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1038.82296392034, + "gpu_time_us_python": 1051.23750950678, + "gpu_noise_us_cpp": 1.45, + "gpu_noise_us_python": 3.15, + "gpu_bwutil_cpp": 0.821442619815727, + "gpu_bwutil_python": 0.811744724567172, + "gpu_gap_stddev_us": 1.9677660796956975 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 944.4577339100999, + "gpu_time_us_python": 953.9470525577999, + "gpu_noise_us_cpp": 1.238, + "gpu_noise_us_python": 2.82, + "gpu_bwutil_cpp": 0.6891607659136192, + "gpu_bwutil_python": 0.682302649265687, + "gpu_gap_stddev_us": 0.44944126495249415 + } + }, + "resize_expand_advanced[InOutDataType=uint8][shape=32x1080x1920][resizeType=EXPAND][interpolation=LINEAR][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 474.56105696826, + "gpu_time_us_python": 486.16179748586, + "gpu_noise_us_cpp": 1.3559999999999999, + "gpu_noise_us_python": 2.81, + "gpu_bwutil_cpp": 0.4495415114683931, + "gpu_bwutil_python": 0.4388231812686497, + "gpu_gap_stddev_us": 1.4636169707737325 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 447.59301089118, + "gpu_time_us_python": 457.19499778851997, + "gpu_noise_us_cpp": 1.312, + "gpu_noise_us_python": 3.2120000000000006, + "gpu_bwutil_cpp": 0.3635767625606181, + "gpu_bwutil_python": 0.355924383597651, + "gpu_gap_stddev_us": 1.2550232760570068 + } + } + } + }, + "resize_expand_linear_4k_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "8x2160x3840" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGB8 expand linear (advanced)." + }, + "baselines": { + "resize_expand_linear_4k_uchar3_advanced[InOutDataType=uchar3][shape=8x2160x3840][resizeType=EXPAND][interpolation=LINEAR][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1228.11811498354, + "gpu_time_us_python": 1240.31518069014, + "gpu_noise_us_cpp": 1.7, + "gpu_noise_us_python": 4.012, + "gpu_bwutil_cpp": 0.5211239347634633, + "gpu_bwutil_python": 0.5160024293586714, + "gpu_gap_stddev_us": 2.932528240285925 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1013.9164621811, + "gpu_time_us_python": 1024.1995585717, + "gpu_noise_us_cpp": 1.766, + "gpu_noise_us_python": 2.57, + "gpu_bwutil_cpp": 0.48149297275341246, + "gpu_bwutil_python": 0.47664951617111884, + "gpu_gap_stddev_us": 3.0797301942230524 + } + } + } + }, + "resize_expand_linear_4k_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "2x2160x3840" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBf32 expand linear (advanced)." + }, + "baselines": { + "resize_expand_linear_4k_float3_advanced[InOutDataType=float3][shape=2x2160x3840][resizeType=EXPAND][interpolation=LINEAR][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 834.3089581463598, + "gpu_time_us_python": 845.1959483682, + "gpu_noise_us_cpp": 1.214, + "gpu_noise_us_python": 2.914, + "gpu_bwutil_cpp": 0.7671033100070161, + "gpu_bwutil_python": 0.7572229250798903, + "gpu_gap_stddev_us": 0.7130518401895289 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 652.7816175361, + "gpu_time_us_python": 659.5652553228799, + "gpu_noise_us_cpp": 2.38, + "gpu_noise_us_python": 3.1660000000000004, + "gpu_bwutil_cpp": 0.7477779825664463, + "gpu_bwutil_python": 0.7400880853934657, + "gpu_gap_stddev_us": 0.8016191626625483 + } + } + } + }, + "resize_expand_linear_4k_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "8x2160x3840" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBA8 expand linear (advanced)." + }, + "baselines": { + "resize_expand_linear_4k_uchar4_advanced[InOutDataType=uchar4][shape=8x2160x3840][resizeType=EXPAND][interpolation=LINEAR][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1517.74269485086, + "gpu_time_us_python": 1529.4644871095402, + "gpu_noise_us_cpp": 1.766, + "gpu_noise_us_python": 3.3999999999999995, + "gpu_bwutil_cpp": 0.5622410598501285, + "gpu_bwutil_python": 0.5579348750756778, + "gpu_gap_stddev_us": 2.975349305106036 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1291.62886941424, + "gpu_time_us_python": 1300.1471552758, + "gpu_noise_us_cpp": 0.9099999999999999, + "gpu_noise_us_python": 5.081999999999999, + "gpu_bwutil_cpp": 0.5039752866443873, + "gpu_bwutil_python": 0.5006556763892237, + "gpu_gap_stddev_us": 3.5462632091355273 + } + } + } + }, + "resize_expand_linear_4k_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "2x2160x3840" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBAf32 expand linear (advanced)." + }, + "baselines": { + "resize_expand_linear_4k_float4_advanced[InOutDataType=float4][shape=2x2160x3840][resizeType=EXPAND][interpolation=LINEAR][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1088.29917772952, + "gpu_time_us_python": 1097.85645792354, + "gpu_noise_us_cpp": 4.014, + "gpu_noise_us_python": 4.712000000000001, + "gpu_bwutil_cpp": 0.7840981477070204, + "gpu_bwutil_python": 0.7772723387175355, + "gpu_gap_stddev_us": 0.29377070050523624 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 795.0907321581401, + "gpu_time_us_python": 802.5369226611599, + "gpu_noise_us_cpp": 2.0200000000000005, + "gpu_noise_us_python": 3.0919999999999996, + "gpu_bwutil_cpp": 0.818583035246659, + "gpu_bwutil_python": 0.8109894102594355, + "gpu_gap_stddev_us": 1.1251450560403535 + } + } + } + }, + "resize_expand_linear_4k_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "8x2160x3840" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize single-channel expand linear (advanced)." + }, + "baselines": { + "resize_expand_linear_4k_scalar_advanced[InOutDataType=float32][shape=8x2160x3840][resizeType=EXPAND][interpolation=LINEAR][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1040.0228626005203, + "gpu_time_us_python": 1051.28841455302, + "gpu_noise_us_cpp": 1.544, + "gpu_noise_us_python": 2.35, + "gpu_bwutil_cpp": 0.8204948256671329, + "gpu_bwutil_python": 0.8117072276380043, + "gpu_gap_stddev_us": 2.6542429669832943 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 940.1445890761, + "gpu_time_us_python": 950.90425811908, + "gpu_noise_us_cpp": 1.0859999999999999, + "gpu_noise_us_python": 3.4539999999999997, + "gpu_bwutil_cpp": 0.6923287170551918, + "gpu_bwutil_python": 0.6845053395874668, + "gpu_gap_stddev_us": 1.4333148140110523 + } + }, + "resize_expand_linear_4k_scalar_advanced[InOutDataType=uint8][shape=8x2160x3840][resizeType=EXPAND][interpolation=LINEAR][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 445.38940860426, + "gpu_time_us_python": 455.89573888946006, + "gpu_noise_us_cpp": 1.048, + "gpu_noise_us_python": 2.724, + "gpu_bwutil_cpp": 0.4789847265757853, + "gpu_bwutil_python": 0.4679486710166496, + "gpu_gap_stddev_us": 1.2316705220910296 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 420.32611508598, + "gpu_time_us_python": 431.6881360289, + "gpu_noise_us_cpp": 2.0840000000000005, + "gpu_noise_us_python": 3.6740000000000004, + "gpu_bwutil_cpp": 0.3871645139934255, + "gpu_bwutil_python": 0.3769556319751572, + "gpu_gap_stddev_us": 1.841009531125834 + } + } + } + }, + "resize_expand_cubic_720p_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "16x720x1280" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGB8 expand cubic (advanced)." + }, + "baselines": { + "resize_expand_cubic_720p_uchar3_advanced[InOutDataType=uchar3][shape=16x720x1280][resizeType=EXPAND][interpolation=CUBIC][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 564.15378144734, + "gpu_time_us_python": 575.46173334636, + "gpu_noise_us_cpp": 1.278, + "gpu_noise_us_python": 2.186, + "gpu_bwutil_cpp": 0.25210016395460205, + "gpu_bwutil_python": 0.2471477388259961, + "gpu_gap_stddev_us": 2.1477636702864964 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 417.69820165849995, + "gpu_time_us_python": 426.65325037111995, + "gpu_noise_us_cpp": 1.218, + "gpu_noise_us_python": 9.388000000000002, + "gpu_bwutil_cpp": 0.2597273399731465, + "gpu_bwutil_python": 0.25426623180441277, + "gpu_gap_stddev_us": 0.9712635437842495 + } + } + } + }, + "resize_expand_cubic_720p_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "16x720x1280" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBf32 expand cubic (advanced)." + }, + "baselines": { + "resize_expand_cubic_720p_float3_advanced[InOutDataType=float3][shape=16x720x1280][resizeType=EXPAND][interpolation=CUBIC][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 928.95538392296, + "gpu_time_us_python": 939.7874847420201, + "gpu_noise_us_cpp": 1.5580000000000003, + "gpu_noise_us_python": 2.838, + "gpu_bwutil_cpp": 0.6123979395993991, + "gpu_bwutil_python": 0.6053407562123773, + "gpu_gap_stddev_us": 1.0668319015175234 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 771.80097024942, + "gpu_time_us_python": 781.36578766962, + "gpu_noise_us_cpp": 0.954, + "gpu_noise_us_python": 3.036, + "gpu_bwutil_cpp": 0.5622672175200434, + "gpu_bwutil_python": 0.5553755889852712, + "gpu_gap_stddev_us": 1.1015730451335184 + } + } + } + }, + "resize_expand_cubic_720p_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "16x720x1280" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBA8 expand cubic (advanced)." + }, + "baselines": { + "resize_expand_cubic_720p_uchar4_advanced[InOutDataType=uchar4][shape=16x720x1280][resizeType=EXPAND][interpolation=CUBIC][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 669.4147988914999, + "gpu_time_us_python": 679.3991304068, + "gpu_noise_us_cpp": 0.9560000000000001, + "gpu_noise_us_python": 2.3160000000000003, + "gpu_bwutil_cpp": 0.2832782809059343, + "gpu_bwutil_python": 0.27911522248861437, + "gpu_gap_stddev_us": 0.57149809480166 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 568.7396817331801, + "gpu_time_us_python": 577.47530641644, + "gpu_noise_us_cpp": 1.152, + "gpu_noise_us_python": 2.768, + "gpu_bwutil_cpp": 0.25434094453475997, + "gpu_bwutil_python": 0.25048863405561755, + "gpu_gap_stddev_us": 0.7036904459290799 + } + } + } + }, + "resize_expand_cubic_720p_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x720x1280" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBAf32 expand cubic (advanced)." + }, + "baselines": { + "resize_expand_cubic_720p_float4_advanced[InOutDataType=float4][shape=16x720x1280][resizeType=EXPAND][interpolation=CUBIC][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1043.8094426909, + "gpu_time_us_python": 1055.76362396788, + "gpu_noise_us_cpp": 1.2600000000000002, + "gpu_noise_us_python": 3.346, + "gpu_bwutil_cpp": 0.7266844605369123, + "gpu_bwutil_python": 0.7184598915408955, + "gpu_gap_stddev_us": 1.524092705542685 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 882.9469651096199, + "gpu_time_us_python": 892.8374683239801, + "gpu_noise_us_cpp": 1.734, + "gpu_noise_us_python": 4.390000000000001, + "gpu_bwutil_cpp": 0.6552840634603591, + "gpu_bwutil_python": 0.6480204247779244, + "gpu_gap_stddev_us": 2.142212553946587 + } + } + } + }, + "resize_expand_cubic_720p_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "32x720x1280" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize single-channel expand cubic (advanced)." + }, + "baselines": { + "resize_expand_cubic_720p_scalar_advanced[InOutDataType=float32][shape=32x720x1280][resizeType=EXPAND][interpolation=CUBIC][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 486.02141766866, + "gpu_time_us_python": 496.778941357, + "gpu_noise_us_cpp": 1.8239999999999998, + "gpu_noise_us_python": 2.512, + "gpu_bwutil_cpp": 0.7803344812386414, + "gpu_bwutil_python": 0.7634372537728465, + "gpu_gap_stddev_us": 0.47315596833331425 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 434.12942637567994, + "gpu_time_us_python": 442.97320165926004, + "gpu_noise_us_cpp": 1.016, + "gpu_noise_us_python": 2.94, + "gpu_bwutil_cpp": 0.666337711363577, + "gpu_bwutil_python": 0.6530529894545435, + "gpu_gap_stddev_us": 1.1381622527550226 + } + }, + "resize_expand_cubic_720p_scalar_advanced[InOutDataType=uint8][shape=32x720x1280][resizeType=EXPAND][interpolation=CUBIC][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 313.36856289852, + "gpu_time_us_python": 323.78475775088003, + "gpu_noise_us_cpp": 1.1800000000000002, + "gpu_noise_us_python": 2.992, + "gpu_bwutil_cpp": 0.30256903212959874, + "gpu_bwutil_python": 0.2928352116379441, + "gpu_gap_stddev_us": 1.2193291361727308 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 263.66716764258, + "gpu_time_us_python": 272.73081566892, + "gpu_noise_us_cpp": 1.004, + "gpu_noise_us_python": 2.206, + "gpu_bwutil_cpp": 0.274304958545213, + "gpu_bwutil_python": 0.2651909188269088, + "gpu_gap_stddev_us": 1.028035347871199 + } + } + } + }, + "resize_expand_cubic_1080p_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "8x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize RGB8 expand cubic (basic)." + }, + "baselines": { + "resize_expand_cubic_1080p_uchar3_basic[InOutDataType=uchar3][shape=8x1080x1920][resizeType=EXPAND][interpolation=CUBIC][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 632.7403540450799, + "gpu_time_us_python": 643.99171721108, + "gpu_noise_us_cpp": 1.4580000000000002, + "gpu_noise_us_python": 2.5420000000000003, + "gpu_bwutil_cpp": 0.25286971275706216, + "gpu_bwutil_python": 0.2484528617763932, + "gpu_gap_stddev_us": 2.2416684598947105 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 468.33763172552, + "gpu_time_us_python": 478.29672605666, + "gpu_noise_us_cpp": 1.028, + "gpu_noise_us_python": 2.402, + "gpu_bwutil_cpp": 0.26060192218818395, + "gpu_bwutil_python": 0.2551797425702652, + "gpu_gap_stddev_us": 1.7558367300324644 + } + } + } + }, + "resize_expand_cubic_1080p_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "8x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize RGBf32 expand cubic (basic)." + }, + "baselines": { + "resize_expand_cubic_1080p_float3_basic[InOutDataType=float3][shape=8x1080x1920][resizeType=EXPAND][interpolation=CUBIC][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1040.76455614702, + "gpu_time_us_python": 1051.2704562433598, + "gpu_noise_us_cpp": 1.552, + "gpu_noise_us_python": 2.3920000000000003, + "gpu_bwutil_cpp": 0.6149353316380132, + "gpu_bwutil_python": 0.6087891067181429, + "gpu_gap_stddev_us": 1.4372207817668479 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 866.4452010969001, + "gpu_time_us_python": 876.4872281519599, + "gpu_noise_us_cpp": 1.2299999999999998, + "gpu_noise_us_python": 3.222, + "gpu_bwutil_cpp": 0.5634559605391137, + "gpu_bwutil_python": 0.5569773641796082, + "gpu_gap_stddev_us": 1.8879709337419766 + } + } + } + }, + "resize_expand_cubic_1080p_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "8x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize RGBA8 expand cubic (advanced)." + }, + "baselines": { + "resize_expand_cubic_1080p_uchar4_advanced[InOutDataType=uchar4][shape=8x1080x1920][resizeType=EXPAND][interpolation=CUBIC][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 751.20767404702, + "gpu_time_us_python": 761.6885477400599, + "gpu_noise_us_cpp": 1.624, + "gpu_noise_us_python": 3.29, + "gpu_bwutil_cpp": 0.2839880761999366, + "gpu_bwutil_python": 0.280080087621946, + "gpu_gap_stddev_us": 0.7387036625347453 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 638.32105321932, + "gpu_time_us_python": 647.89773262942, + "gpu_noise_us_cpp": 0.6839999999999999, + "gpu_noise_us_python": 3.2380000000000004, + "gpu_bwutil_cpp": 0.25494386120547224, + "gpu_bwutil_python": 0.25117564532919834, + "gpu_gap_stddev_us": 0.5359929990652071 + } + } + } + }, + "resize_expand_cubic_1080p_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "8x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize RGBAf32 expand cubic (advanced)." + }, + "baselines": { + "resize_expand_cubic_1080p_float4_advanced[InOutDataType=float4][shape=8x1080x1920][resizeType=EXPAND][interpolation=CUBIC][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1173.1594322265, + "gpu_time_us_python": 1184.1849948269198, + "gpu_noise_us_cpp": 1.748, + "gpu_noise_us_python": 3.338, + "gpu_bwutil_cpp": 0.7273822046549017, + "gpu_bwutil_python": 0.7206088965018628, + "gpu_gap_stddev_us": 1.3832191979673374 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 988.1194384302798, + "gpu_time_us_python": 998.2335369046999, + "gpu_noise_us_cpp": 1.4140000000000001, + "gpu_noise_us_python": 7.9239999999999995, + "gpu_bwutil_cpp": 0.6587337852784116, + "gpu_bwutil_python": 0.6520568969149296, + "gpu_gap_stddev_us": 0.5707055754507658 + } + } + } + }, + "resize_expand_cubic_1080p_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize single-channel expand cubic (advanced)." + }, + "baselines": { + "resize_expand_cubic_1080p_scalar_advanced[InOutDataType=float32][shape=32x1080x1920][resizeType=EXPAND][interpolation=CUBIC][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1080.65809843864, + "gpu_time_us_python": 1090.81486652304, + "gpu_noise_us_cpp": 2.726, + "gpu_noise_us_python": 3.376, + "gpu_bwutil_cpp": 0.7896428350440001, + "gpu_bwutil_python": 0.7822899961131057, + "gpu_gap_stddev_us": 0.8995272939031894 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 966.19692010436, + "gpu_time_us_python": 974.9302889596199, + "gpu_noise_us_cpp": 1.416, + "gpu_noise_us_python": 3.3020000000000005, + "gpu_bwutil_cpp": 0.6736407038866238, + "gpu_bwutil_python": 0.6676084411963386, + "gpu_gap_stddev_us": 1.6633986981103746 + } + }, + "resize_expand_cubic_1080p_scalar_advanced[InOutDataType=uint8][shape=32x1080x1920][resizeType=EXPAND][interpolation=CUBIC][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 732.6176783299801, + "gpu_time_us_python": 742.9788014710199, + "gpu_noise_us_cpp": 1.416, + "gpu_noise_us_python": 2.626, + "gpu_bwutil_cpp": 0.29119524793695717, + "gpu_bwutil_python": 0.28713610949984075, + "gpu_gap_stddev_us": 0.848962462157872 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 619.1625209803, + "gpu_time_us_python": 628.2481046343399, + "gpu_noise_us_cpp": 2.38, + "gpu_noise_us_python": 2.548, + "gpu_bwutil_cpp": 0.2628320535646938, + "gpu_bwutil_python": 0.2590267639142104, + "gpu_gap_stddev_us": 0.7517564409022518 + } + } + } + }, + "resize_expand_cubic_4k_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "2x2160x3840" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize RGB8 expand cubic (advanced)." + }, + "baselines": { + "resize_expand_cubic_4k_uchar3_advanced[InOutDataType=uchar3][shape=2x2160x3840][resizeType=EXPAND][interpolation=CUBIC][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 632.9814711967599, + "gpu_time_us_python": 643.48664294786, + "gpu_noise_us_cpp": 1.216, + "gpu_noise_us_python": 3.6099999999999994, + "gpu_bwutil_cpp": 0.2527735579380389, + "gpu_bwutil_python": 0.2486458049143474, + "gpu_gap_stddev_us": 1.0467399963140531 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 468.09936077122, + "gpu_time_us_python": 478.16806899780005, + "gpu_noise_us_cpp": 2.004, + "gpu_noise_us_python": 4.388, + "gpu_bwutil_cpp": 0.26073166951135024, + "gpu_bwutil_python": 0.255245989809466, + "gpu_gap_stddev_us": 1.4068614807398707 + } + } + } + }, + "resize_expand_cubic_4k_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "2x2160x3840" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize RGBf32 expand cubic (advanced)." + }, + "baselines": { + "resize_expand_cubic_4k_float3_advanced[InOutDataType=float3][shape=2x2160x3840][resizeType=EXPAND][interpolation=CUBIC][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1040.40874208194, + "gpu_time_us_python": 1051.6133421751601, + "gpu_noise_us_cpp": 1.332, + "gpu_noise_us_python": 3.5519999999999996, + "gpu_bwutil_cpp": 0.615144482639248, + "gpu_bwutil_python": 0.6085902767507854, + "gpu_gap_stddev_us": 1.3464886354166334 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 868.33375892198, + "gpu_time_us_python": 878.2656108395, + "gpu_noise_us_cpp": 1.1179999999999999, + "gpu_noise_us_python": 4.518, + "gpu_bwutil_cpp": 0.5622310170715158, + "gpu_bwutil_python": 0.555869793347173, + "gpu_gap_stddev_us": 0.8861436754548925 + } + } + } + }, + "resize_expand_cubic_4k_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "2x2160x3840" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize RGBA8 expand cubic (advanced)." + }, + "baselines": { + "resize_expand_cubic_4k_uchar4_advanced[InOutDataType=uchar4][shape=2x2160x3840][resizeType=EXPAND][interpolation=CUBIC][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 750.72810520124, + "gpu_time_us_python": 762.54180054758, + "gpu_noise_us_cpp": 1.068, + "gpu_noise_us_python": 2.456, + "gpu_bwutil_cpp": 0.28416985374360415, + "gpu_bwutil_python": 0.2797683820755766, + "gpu_gap_stddev_us": 3.455864675993403 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 638.4568444613, + "gpu_time_us_python": 647.50535962622, + "gpu_noise_us_cpp": 1.096, + "gpu_noise_us_python": 2.5620000000000003, + "gpu_bwutil_cpp": 0.2548909751020023, + "gpu_bwutil_python": 0.2513285715423551, + "gpu_gap_stddev_us": 0.33428964523693583 + } + } + } + }, + "resize_expand_cubic_4k_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "2x2160x3840" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize RGBAf32 expand cubic (advanced)." + }, + "baselines": { + "resize_expand_cubic_4k_float4_advanced[InOutDataType=float4][shape=2x2160x3840][resizeType=EXPAND][interpolation=CUBIC][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1180.46410663922, + "gpu_time_us_python": 1192.6080736879999, + "gpu_noise_us_cpp": 1.768, + "gpu_noise_us_python": 3.6239999999999997, + "gpu_bwutil_cpp": 0.7228810855950668, + "gpu_bwutil_python": 0.7155214356991262, + "gpu_gap_stddev_us": 2.2820347392725067 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1014.60275223592, + "gpu_time_us_python": 1024.49464902064, + "gpu_noise_us_cpp": 1.8040000000000003, + "gpu_noise_us_python": 3.682, + "gpu_bwutil_cpp": 0.641531547911991, + "gpu_bwutil_python": 0.6353377060624209, + "gpu_gap_stddev_us": 0.355881790631847 + } + } + } + }, + "resize_expand_cubic_4k_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "8x2160x3840" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize single-channel expand cubic (advanced)." + }, + "baselines": { + "resize_expand_cubic_4k_scalar_advanced[InOutDataType=float32][shape=8x2160x3840][resizeType=EXPAND][interpolation=CUBIC][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1080.5998365426399, + "gpu_time_us_python": 1092.8578611097398, + "gpu_noise_us_cpp": 2.448, + "gpu_noise_us_python": 6.510000000000001, + "gpu_bwutil_cpp": 0.789685013845417, + "gpu_bwutil_python": 0.7808339006761533, + "gpu_gap_stddev_us": 3.2725580931724694 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 958.3759255382399, + "gpu_time_us_python": 967.7368983963601, + "gpu_noise_us_cpp": 1.9140000000000001, + "gpu_noise_us_python": 3.31, + "gpu_bwutil_cpp": 0.6791518307699673, + "gpu_bwutil_python": 0.6725920425992264, + "gpu_gap_stddev_us": 2.0054015138915027 + } + }, + "resize_expand_cubic_4k_scalar_advanced[InOutDataType=uint8][shape=8x2160x3840][resizeType=EXPAND][interpolation=CUBIC][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 688.07429944162, + "gpu_time_us_python": 698.5247401994, + "gpu_noise_us_cpp": 1.1480000000000001, + "gpu_noise_us_python": 2.7260000000000004, + "gpu_bwutil_cpp": 0.3100457297507889, + "gpu_bwutil_python": 0.3054073685360549, + "gpu_gap_stddev_us": 0.7673878523933096 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 581.06922192536, + "gpu_time_us_python": 590.7912455779, + "gpu_noise_us_cpp": 1.216, + "gpu_noise_us_python": 3.278, + "gpu_bwutil_cpp": 0.28006184718040794, + "gpu_bwutil_python": 0.27545929856528123, + "gpu_gap_stddev_us": 1.9606213431511 + } + } + } + }, + "resize_expand_linear_varshape_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "128x720x1280", + "32x1080x1920", + "8x2160x3840" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize single-channel expand linear varshape (advanced)." + }, + "baselines": { + "resize_expand_linear_varshape_advanced[InOutDataType=float32][shape=128x720x1280][resizeType=EXPAND][interpolation=LINEAR][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3205.4290564792595, + "gpu_time_us_python": 3226.0279649343, + "gpu_noise_us_cpp": 3.518, + "gpu_noise_us_python": 8.164, + "gpu_bwutil_cpp": 0.4732729090192834, + "gpu_bwutil_python": 0.4702517727825032, + "gpu_gap_stddev_us": 7.19520605014752 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2851.0701535200997, + "gpu_time_us_python": 2868.89457824566, + "gpu_noise_us_cpp": 3.778, + "gpu_noise_us_python": 5.723999999999999, + "gpu_bwutil_cpp": 0.40588885131449326, + "gpu_bwutil_python": 0.4033525165980073, + "gpu_gap_stddev_us": 5.2006584537128155 + } + }, + "resize_expand_linear_varshape_advanced[InOutDataType=float32][shape=32x1080x1920][resizeType=EXPAND][interpolation=LINEAR][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1802.1250801977803, + "gpu_time_us_python": 1816.3667433503201, + "gpu_noise_us_cpp": 3.55, + "gpu_noise_us_python": 3.9539999999999997, + "gpu_bwutil_cpp": 0.47351645948312376, + "gpu_bwutil_python": 0.46980386085578657, + "gpu_gap_stddev_us": 2.5971301770557957 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1594.9448754418197, + "gpu_time_us_python": 1611.86631836816, + "gpu_noise_us_cpp": 1.6300000000000001, + "gpu_noise_us_python": 7.852000000000001, + "gpu_bwutil_cpp": 0.40812770285637745, + "gpu_bwutil_python": 0.40385133909615867, + "gpu_gap_stddev_us": 3.6203833301630715 + } + }, + "resize_expand_linear_varshape_advanced[InOutDataType=float32][shape=8x2160x3840][resizeType=EXPAND][interpolation=LINEAR][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1791.28759333062, + "gpu_time_us_python": 1803.3509319634402, + "gpu_noise_us_cpp": 2.366, + "gpu_noise_us_python": 3.59, + "gpu_bwutil_cpp": 0.47638172568896275, + "gpu_bwutil_python": 0.47319477419366623, + "gpu_gap_stddev_us": 2.596439334612648 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1596.7087311467799, + "gpu_time_us_python": 1610.26450574596, + "gpu_noise_us_cpp": 1.35, + "gpu_noise_us_python": 3.8760000000000003, + "gpu_bwutil_cpp": 0.40768173624716686, + "gpu_bwutil_python": 0.404259619189943, + "gpu_gap_stddev_us": 3.9992523875303965 + } + }, + "resize_expand_linear_varshape_advanced[InOutDataType=uint8][shape=128x720x1280][resizeType=EXPAND][interpolation=LINEAR][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 643.5494701264801, + "gpu_time_us_python": 661.09447689564, + "gpu_noise_us_cpp": 6.034000000000001, + "gpu_noise_us_python": 5.796, + "gpu_bwutil_cpp": 0.5893371779477841, + "gpu_bwutil_python": 0.5737244153704879, + "gpu_gap_stddev_us": 3.80925376336894 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 611.8853646192999, + "gpu_time_us_python": 628.17367583496, + "gpu_noise_us_cpp": 3.382, + "gpu_noise_us_python": 5.462, + "gpu_bwutil_cpp": 0.47279462413891843, + "gpu_bwutil_python": 0.4605261360963592, + "gpu_gap_stddev_us": 3.6311829491081844 + } + }, + "resize_expand_linear_varshape_advanced[InOutDataType=uint8][shape=32x1080x1920][resizeType=EXPAND][interpolation=LINEAR][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 378.70436682474, + "gpu_time_us_python": 392.50079494504, + "gpu_noise_us_cpp": 1.502, + "gpu_noise_us_python": 3.1740000000000004, + "gpu_bwutil_cpp": 0.5633289635742351, + "gpu_bwutil_python": 0.5435413920716291, + "gpu_gap_stddev_us": 1.89367389904656 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 361.38099401528, + "gpu_time_us_python": 375.23868468982, + "gpu_noise_us_cpp": 1.7479999999999998, + "gpu_noise_us_python": 10.441999999999998, + "gpu_bwutil_cpp": 0.4502938793856924, + "gpu_bwutil_python": 0.43364956723235065, + "gpu_gap_stddev_us": 1.7255560711786349 + } + }, + "resize_expand_linear_varshape_advanced[InOutDataType=uint8][shape=8x2160x3840][resizeType=EXPAND][interpolation=LINEAR][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 350.52328568494, + "gpu_time_us_python": 362.4878728382, + "gpu_noise_us_cpp": 2.106, + "gpu_noise_us_python": 2.0100000000000002, + "gpu_bwutil_cpp": 0.6086199182740248, + "gpu_bwutil_python": 0.5885287046822733, + "gpu_gap_stddev_us": 0.8113363927171121 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 334.81141025444, + "gpu_time_us_python": 347.78989062347995, + "gpu_noise_us_cpp": 2.2139999999999995, + "gpu_noise_us_python": 2.7359999999999998, + "gpu_bwutil_cpp": 0.4860462388911205, + "gpu_bwutil_python": 0.4679833175541404, + "gpu_gap_stddev_us": 3.939518598110895 + } + } + } + }, + "resize_planar_nchw_contract_area_tensor_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "AREA" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGB8 contract area tensor planar (basic)." + }, + "baselines": { + "resize_planar_nchw_contract_area_tensor_uchar3_basic[InOutDataType=uchar3][shape=32x1080x1920][resizeType=CONTRACT][interpolation=AREA][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 201.22329085788, + "gpu_time_us_python": 211.21493361970002, + "gpu_noise_us_cpp": 1.4140000000000001, + "gpu_noise_us_python": 2.268, + "gpu_bwutil_cpp": 0.7951370361347067, + "gpu_bwutil_python": 0.7575273309695472, + "gpu_gap_stddev_us": 0.7117223807085713 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 150.75210018063999, + "gpu_time_us_python": 159.60136602726, + "gpu_noise_us_cpp": 0.9400000000000001, + "gpu_noise_us_python": 2.962, + "gpu_bwutil_cpp": 0.8095303712274784, + "gpu_bwutil_python": 0.7646989492228226, + "gpu_gap_stddev_us": 1.0430725830205818 + } + } + } + }, + "resize_planar_nchw_contract_area_tensor_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "AREA" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGB8 contract area tensor planar (advanced)." + }, + "baselines": { + "resize_planar_nchw_contract_area_tensor_uchar3_advanced[InOutDataType=uchar3][shape=32x1080x1920][resizeType=CONTRACT][interpolation=AREA][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 201.48702338828, + "gpu_time_us_python": 211.09740512218, + "gpu_noise_us_cpp": 1.3679999999999999, + "gpu_noise_us_python": 2.396, + "gpu_bwutil_cpp": 0.7940966130055708, + "gpu_bwutil_python": 0.7579552854274241, + "gpu_gap_stddev_us": 0.8177506532742437 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 150.68099785954, + "gpu_time_us_python": 159.70077683696, + "gpu_noise_us_cpp": 1.102, + "gpu_noise_us_python": 3.01, + "gpu_bwutil_cpp": 0.809906741481076, + "gpu_bwutil_python": 0.7642310319200615, + "gpu_gap_stddev_us": 1.1231147940178747 + } + } + } + }, + "resize_planar_nchw_contract_area_tensor_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "AREA" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBf32 contract area tensor planar (basic)." + }, + "baselines": { + "resize_planar_nchw_contract_area_tensor_float3_basic[InOutDataType=float3][shape=16x1080x1920][resizeType=CONTRACT][interpolation=AREA][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 370.55570198576004, + "gpu_time_us_python": 380.8063199293, + "gpu_noise_us_cpp": 1.1320000000000001, + "gpu_noise_us_python": 2.2459999999999996, + "gpu_bwutil_cpp": 0.863568239120527, + "gpu_bwutil_python": 0.840324876651968, + "gpu_gap_stddev_us": 0.7897131794481113 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 279.30229214164, + "gpu_time_us_python": 288.04282282436, + "gpu_noise_us_cpp": 1.1460000000000001, + "gpu_noise_us_python": 2.0700000000000003, + "gpu_bwutil_cpp": 0.8738559560903332, + "gpu_bwutil_python": 0.8473566635387358, + "gpu_gap_stddev_us": 1.2405386861764414 + } + } + } + }, + "resize_planar_nchw_contract_area_tensor_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "AREA" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBf32 contract area tensor planar (advanced)." + }, + "baselines": { + "resize_planar_nchw_contract_area_tensor_float3_advanced[InOutDataType=float3][shape=16x1080x1920][resizeType=CONTRACT][interpolation=AREA][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 370.36129174216, + "gpu_time_us_python": 381.04993336904, + "gpu_noise_us_cpp": 1.026, + "gpu_noise_us_python": 2.414, + "gpu_bwutil_cpp": 0.8640212673657125, + "gpu_bwutil_python": 0.8397932602095144, + "gpu_gap_stddev_us": 1.4219841521452339 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 279.20004485368, + "gpu_time_us_python": 288.29467304822003, + "gpu_noise_us_cpp": 1.2680000000000002, + "gpu_noise_us_python": 2.6900000000000004, + "gpu_bwutil_cpp": 0.8741734661263088, + "gpu_bwutil_python": 0.8466209920017486, + "gpu_gap_stddev_us": 1.4044057047410432 + } + } + } + }, + "resize_planar_nchw_contract_area_tensor_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "AREA" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBA8 contract area tensor planar (advanced) (tensor-only: RGBA8p planar var-shape unsupported by the Python image API)." + }, + "baselines": { + "resize_planar_nchw_contract_area_tensor_uchar4_advanced[InOutDataType=uchar4][shape=32x1080x1920][resizeType=CONTRACT][interpolation=AREA][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 259.72911877139995, + "gpu_time_us_python": 270.52791878836, + "gpu_noise_us_cpp": 1.29, + "gpu_noise_us_python": 2.596, + "gpu_bwutil_cpp": 0.8213703317545914, + "gpu_bwutil_python": 0.7885954036231525, + "gpu_gap_stddev_us": 0.9593363646016778 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 194.41427769173998, + "gpu_time_us_python": 203.28731132892, + "gpu_noise_us_cpp": 1.476, + "gpu_noise_us_python": 2.742, + "gpu_bwutil_cpp": 0.8369460948530415, + "gpu_bwutil_python": 0.8004635360770402, + "gpu_gap_stddev_us": 1.2723711424070105 + } + } + } + }, + "resize_planar_nchw_contract_area_tensor_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "AREA" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBAf32 contract area tensor planar (advanced)." + }, + "baselines": { + "resize_planar_nchw_contract_area_tensor_float4_advanced[InOutDataType=float4][shape=16x1080x1920][resizeType=CONTRACT][interpolation=AREA][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 486.32876296812003, + "gpu_time_us_python": 496.5160350878999, + "gpu_noise_us_cpp": 1.1260000000000001, + "gpu_noise_us_python": 2.182, + "gpu_bwutil_cpp": 0.8773223157418515, + "gpu_bwutil_python": 0.859329678702417, + "gpu_gap_stddev_us": 1.8486238680151068 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 366.23282784877995, + "gpu_time_us_python": 375.25849346160004, + "gpu_noise_us_cpp": 1.09, + "gpu_noise_us_python": 2.884, + "gpu_bwutil_cpp": 0.8885749528620817, + "gpu_bwutil_python": 0.8672099708090106, + "gpu_gap_stddev_us": 0.8842587727809635 + } + } + } + }, + "resize_planar_nchw_contract_area_varshape_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "AREA" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGB8 contract area varshape planar (basic)." + }, + "baselines": { + "resize_planar_nchw_contract_area_varshape_uchar3_basic[InOutDataType=uchar3][shape=32x1080x1920][resizeType=CONTRACT][interpolation=AREA][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 217.40557733276, + "gpu_time_us_python": 229.59515925443998, + "gpu_noise_us_cpp": 2.054, + "gpu_noise_us_python": 4.008, + "gpu_bwutil_cpp": 0.7359653479207875, + "gpu_bwutil_python": 0.696940646430421, + "gpu_gap_stddev_us": 1.4888144970650274 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 160.93846149008, + "gpu_time_us_python": 171.90420424994, + "gpu_noise_us_cpp": 2.0340000000000003, + "gpu_noise_us_python": 4.069999999999999, + "gpu_bwutil_cpp": 0.7583339910815732, + "gpu_bwutil_python": 0.7100713760225681, + "gpu_gap_stddev_us": 1.808370723577178 + } + } + } + }, + "resize_planar_nchw_contract_area_varshape_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "AREA" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBf32 contract area varshape planar (advanced)." + }, + "baselines": { + "resize_planar_nchw_contract_area_varshape_float3_advanced[InOutDataType=float3][shape=16x1080x1920][resizeType=CONTRACT][interpolation=AREA][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 373.72098814308, + "gpu_time_us_python": 385.75689736161996, + "gpu_noise_us_cpp": 1.8739999999999999, + "gpu_noise_us_python": 2.5699999999999994, + "gpu_bwutil_cpp": 0.8562683309836829, + "gpu_bwutil_python": 0.8295660885858454, + "gpu_gap_stddev_us": 1.0349430359291096 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 284.42096309330003, + "gpu_time_us_python": 294.74291207246, + "gpu_noise_us_cpp": 2.21, + "gpu_noise_us_python": 3.2659999999999996, + "gpu_bwutil_cpp": 0.8581443143551869, + "gpu_bwutil_python": 0.8280887421123893, + "gpu_gap_stddev_us": 0.3967684163727875 + } + } + } + }, + "resize_planar_nchw_contract_area_varshape_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "AREA" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBAf32 contract area varshape planar (advanced)." + }, + "baselines": { + "resize_planar_nchw_contract_area_varshape_float4_advanced[InOutDataType=float4][shape=16x1080x1920][resizeType=CONTRACT][interpolation=AREA][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 489.14502358976006, + "gpu_time_us_python": 501.02002071426006, + "gpu_noise_us_cpp": 1.5879999999999999, + "gpu_noise_us_python": 3.0279999999999996, + "gpu_bwutil_cpp": 0.8722729355653895, + "gpu_bwutil_python": 0.8516060966147597, + "gpu_gap_stddev_us": 1.3000197812905474 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 373.15271561604, + "gpu_time_us_python": 385.47609154166, + "gpu_noise_us_cpp": 2.124, + "gpu_noise_us_python": 4.674, + "gpu_bwutil_cpp": 0.872102978737961, + "gpu_bwutil_python": 0.8442589094772224, + "gpu_gap_stddev_us": 2.287545440247541 + } + } + } + }, + "resize_planar_nchw_contract_linear_720p_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "64x720x1280" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBf32 contract linear planar (advanced)." + }, + "baselines": { + "resize_planar_nchw_contract_linear_720p_float3_advanced[InOutDataType=float3][shape=64x720x1280][resizeType=CONTRACT][interpolation=LINEAR][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 647.00022763364, + "gpu_time_us_python": 658.11756480502, + "gpu_noise_us_cpp": 1.6179999999999999, + "gpu_noise_us_python": 3.9480000000000004, + "gpu_bwutil_cpp": 0.8792716628195427, + "gpu_bwutil_python": 0.8644250644960596, + "gpu_gap_stddev_us": 2.265265725923653 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 491.43212001989997, + "gpu_time_us_python": 499.734449819, + "gpu_noise_us_cpp": 1.366, + "gpu_noise_us_python": 2.6180000000000003, + "gpu_bwutil_cpp": 0.8829309561543063, + "gpu_bwutil_python": 0.8682595213813571, + "gpu_gap_stddev_us": 0.8757157787181137 + } + } + } + }, + "resize_planar_nchw_contract_linear_720p_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "64x720x1280" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBAf32 contract linear planar (advanced)." + }, + "baselines": { + "resize_planar_nchw_contract_linear_720p_float4_advanced[InOutDataType=float4][shape=64x720x1280][resizeType=CONTRACT][interpolation=LINEAR][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 854.7086944351998, + "gpu_time_us_python": 866.4291779125, + "gpu_noise_us_cpp": 1.486, + "gpu_noise_us_python": 3.7339999999999995, + "gpu_bwutil_cpp": 0.8874586598101256, + "gpu_bwutil_python": 0.8754579422469397, + "gpu_gap_stddev_us": 2.1396172582916138 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 648.79658889168, + "gpu_time_us_python": 658.49897047128, + "gpu_noise_us_cpp": 2.16, + "gpu_noise_us_python": 3.4480000000000004, + "gpu_bwutil_cpp": 0.8917002666036626, + "gpu_bwutil_python": 0.8785631491190429, + "gpu_gap_stddev_us": 0.8429208786795738 + } + } + } + }, + "resize_planar_nchw_contract_linear_1080p_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBf32 contract linear planar (basic)." + }, + "baselines": { + "resize_planar_nchw_contract_linear_1080p_float3_basic[InOutDataType=float3][shape=32x1080x1920][resizeType=CONTRACT][interpolation=LINEAR][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 726.43211807024, + "gpu_time_us_python": 737.2894524153801, + "gpu_noise_us_cpp": 1.3599999999999999, + "gpu_noise_us_python": 2.568, + "gpu_bwutil_cpp": 0.8810183685610552, + "gpu_bwutil_python": 0.868047313923028, + "gpu_gap_stddev_us": 1.4184636106529036 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 552.9228865990999, + "gpu_time_us_python": 561.26549345458, + "gpu_noise_us_cpp": 1.454, + "gpu_noise_us_python": 2.556, + "gpu_bwutil_cpp": 0.8828282383554373, + "gpu_bwutil_python": 0.8697123247538329, + "gpu_gap_stddev_us": 1.2873251196337616 + } + } + } + }, + "resize_planar_nchw_contract_linear_1080p_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBf32 contract linear planar (advanced)." + }, + "baselines": { + "resize_planar_nchw_contract_linear_1080p_float3_advanced[InOutDataType=float3][shape=32x1080x1920][resizeType=CONTRACT][interpolation=LINEAR][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 726.4315246498601, + "gpu_time_us_python": 736.94690136344, + "gpu_noise_us_cpp": 1.344, + "gpu_noise_us_python": 2.584, + "gpu_bwutil_cpp": 0.8810190465040385, + "gpu_bwutil_python": 0.8684479872107355, + "gpu_gap_stddev_us": 0.2847807973312115 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 552.9793019393999, + "gpu_time_us_python": 561.87721987368, + "gpu_noise_us_cpp": 1.45, + "gpu_noise_us_python": 3.838, + "gpu_bwutil_cpp": 0.8827393828501272, + "gpu_bwutil_python": 0.8687653244734539, + "gpu_gap_stddev_us": 1.1059369720079972 + } + } + } + }, + "resize_planar_nchw_contract_linear_1080p_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBAf32 contract linear planar (advanced)." + }, + "baselines": { + "resize_planar_nchw_contract_linear_1080p_float4_advanced[InOutDataType=float4][shape=32x1080x1920][resizeType=CONTRACT][interpolation=LINEAR][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 961.0280680469601, + "gpu_time_us_python": 971.57739816994, + "gpu_noise_us_cpp": 1.1960000000000002, + "gpu_noise_us_python": 2.286, + "gpu_bwutil_cpp": 0.8879383673396921, + "gpu_bwutil_python": 0.8782982328939596, + "gpu_gap_stddev_us": 1.376720304553416 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 731.52396404022, + "gpu_time_us_python": 740.9720037604, + "gpu_noise_us_cpp": 1.7799999999999998, + "gpu_noise_us_python": 3.492, + "gpu_bwutil_cpp": 0.8897150542657662, + "gpu_bwutil_python": 0.8783758922919997, + "gpu_gap_stddev_us": 1.6411158568276052 + } + } + } + }, + "resize_planar_nchw_contract_linear_4k_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "8x2160x3840" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBf32 contract linear planar (advanced)." + }, + "baselines": { + "resize_planar_nchw_contract_linear_4k_float3_advanced[InOutDataType=float3][shape=8x2160x3840][resizeType=CONTRACT][interpolation=LINEAR][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 724.7479736367999, + "gpu_time_us_python": 735.5269905600001, + "gpu_noise_us_cpp": 1.142, + "gpu_noise_us_python": 2.918, + "gpu_bwutil_cpp": 0.8830662388247911, + "gpu_bwutil_python": 0.8701264980305462, + "gpu_gap_stddev_us": 0.9423144045989923 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 550.6177827296, + "gpu_time_us_python": 560.87234036308, + "gpu_noise_us_cpp": 1.418, + "gpu_noise_us_python": 4.604, + "gpu_bwutil_cpp": 0.886525295144342, + "gpu_bwutil_python": 0.8703271156434301, + "gpu_gap_stddev_us": 1.6030788737000272 + } + } + } + }, + "resize_planar_nchw_contract_linear_4k_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "8x2160x3840" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBAf32 contract linear planar (advanced)." + }, + "baselines": { + "resize_planar_nchw_contract_linear_4k_float4_advanced[InOutDataType=float4][shape=8x2160x3840][resizeType=CONTRACT][interpolation=LINEAR][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 957.96653109944, + "gpu_time_us_python": 969.0182235060402, + "gpu_noise_us_cpp": 1.1060000000000003, + "gpu_noise_us_python": 2.258, + "gpu_bwutil_cpp": 0.8907757298739852, + "gpu_bwutil_python": 0.880618783203351, + "gpu_gap_stddev_us": 1.7847467304809865 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 727.97252116502, + "gpu_time_us_python": 737.8854208406799, + "gpu_noise_us_cpp": 1.8, + "gpu_noise_us_python": 3.482, + "gpu_bwutil_cpp": 0.894055630519518, + "gpu_bwutil_python": 0.8820522255243224, + "gpu_gap_stddev_us": 1.9142522051512394 + } + } + } + }, + "resize_planar_nchw_contract_cubic_720p_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "64x720x1280" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBf32 contract cubic planar (advanced)." + }, + "baselines": { + "resize_planar_nchw_contract_cubic_720p_float3_advanced[InOutDataType=float3][shape=64x720x1280][resizeType=CONTRACT][interpolation=CUBIC][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 998.6438334173799, + "gpu_time_us_python": 1008.6569780558, + "gpu_noise_us_cpp": 1.246, + "gpu_noise_us_python": 2.342, + "gpu_bwutil_cpp": 0.5696644774293056, + "gpu_bwutil_python": 0.5640083580018278, + "gpu_gap_stddev_us": 0.6731555486565773 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 617.83377975562, + "gpu_time_us_python": 627.50385550082, + "gpu_noise_us_cpp": 1.2079999999999997, + "gpu_noise_us_python": 2.836, + "gpu_bwutil_cpp": 0.7022959895966524, + "gpu_bwutil_python": 0.6914745671297006, + "gpu_gap_stddev_us": 1.1208022958236117 + } + } + } + }, + "resize_planar_nchw_contract_cubic_720p_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "32x720x1280" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBAf32 contract cubic planar (advanced)." + }, + "baselines": { + "resize_planar_nchw_contract_cubic_720p_float4_advanced[InOutDataType=float4][shape=32x720x1280][resizeType=CONTRACT][interpolation=CUBIC][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 676.6351832228, + "gpu_time_us_python": 686.45495900416, + "gpu_noise_us_cpp": 1.124, + "gpu_noise_us_python": 1.9840000000000004, + "gpu_bwutil_cpp": 0.5605097205936274, + "gpu_bwutil_python": 0.5524910075065942, + "gpu_gap_stddev_us": 0.8484632558809296 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 416.0814323421599, + "gpu_time_us_python": 425.62951732296006, + "gpu_noise_us_cpp": 1.166, + "gpu_noise_us_python": 2.65, + "gpu_bwutil_cpp": 0.6952168826641059, + "gpu_bwutil_python": 0.6796257936053911, + "gpu_gap_stddev_us": 2.2162693536323608 + } + } + } + }, + "resize_planar_nchw_contract_cubic_1080p_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize RGBf32 contract cubic planar (basic)." + }, + "baselines": { + "resize_planar_nchw_contract_cubic_1080p_float3_basic[InOutDataType=float3][shape=32x1080x1920][resizeType=CONTRACT][interpolation=CUBIC][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1124.1062980104198, + "gpu_time_us_python": 1135.21060854498, + "gpu_noise_us_cpp": 1.248, + "gpu_noise_us_python": 2.966, + "gpu_bwutil_cpp": 0.5693436448472705, + "gpu_bwutil_python": 0.5637735945815889, + "gpu_gap_stddev_us": 1.2533375761158916 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 696.90044124812, + "gpu_time_us_python": 707.00720218608, + "gpu_noise_us_cpp": 0.884, + "gpu_noise_us_python": 2.902, + "gpu_bwutil_cpp": 0.7004440872727447, + "gpu_bwutil_python": 0.6904345118928374, + "gpu_gap_stddev_us": 1.4262262244454778 + } + } + } + }, + "resize_planar_nchw_contract_cubic_1080p_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize RGBf32 contract cubic planar (advanced)." + }, + "baselines": { + "resize_planar_nchw_contract_cubic_1080p_float3_advanced[InOutDataType=float3][shape=32x1080x1920][resizeType=CONTRACT][interpolation=CUBIC][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1124.19968637066, + "gpu_time_us_python": 1135.03219207482, + "gpu_noise_us_cpp": 0.874, + "gpu_noise_us_python": 3.114, + "gpu_bwutil_cpp": 0.5692960843754157, + "gpu_bwutil_python": 0.563860937144895, + "gpu_gap_stddev_us": 1.9738358693479112 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 696.98739109492, + "gpu_time_us_python": 707.37662181036, + "gpu_noise_us_cpp": 0.9380000000000001, + "gpu_noise_us_python": 3.7800000000000002, + "gpu_bwutil_cpp": 0.7003569850526362, + "gpu_bwutil_python": 0.6900681078408617, + "gpu_gap_stddev_us": 1.732608291567569 + } + } + } + }, + "resize_planar_nchw_contract_cubic_1080p_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize RGBAf32 contract cubic planar (advanced)." + }, + "baselines": { + "resize_planar_nchw_contract_cubic_1080p_float4_advanced[InOutDataType=float4][shape=16x1080x1920][resizeType=CONTRACT][interpolation=CUBIC][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 760.2259228033801, + "gpu_time_us_python": 771.3597039763599, + "gpu_noise_us_cpp": 1.1960000000000002, + "gpu_noise_us_python": 3.166, + "gpu_bwutil_cpp": 0.5612376224731891, + "gpu_bwutil_python": 0.5531374701719914, + "gpu_gap_stddev_us": 1.312772656644793 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 467.81425352061996, + "gpu_time_us_python": 478.21187449708003, + "gpu_noise_us_cpp": 1.208, + "gpu_noise_us_python": 3.1380000000000003, + "gpu_bwutil_cpp": 0.695630857915102, + "gpu_bwutil_python": 0.6805073827632061, + "gpu_gap_stddev_us": 2.0700932452771106 + } + } + } + }, + "resize_planar_nchw_contract_cubic_4k_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "8x2160x3840" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBf32 contract cubic planar (advanced)." + }, + "baselines": { + "resize_planar_nchw_contract_cubic_4k_float3_advanced[InOutDataType=float3][shape=8x2160x3840][resizeType=CONTRACT][interpolation=CUBIC][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1128.4196294716999, + "gpu_time_us_python": 1140.64856564316, + "gpu_noise_us_cpp": 1.3719999999999999, + "gpu_noise_us_python": 3.2359999999999998, + "gpu_bwutil_cpp": 0.5671682794037975, + "gpu_bwutil_python": 0.5610866473317284, + "gpu_gap_stddev_us": 2.84896359052433 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 695.8754368038, + "gpu_time_us_python": 705.69260426828, + "gpu_noise_us_cpp": 0.75, + "gpu_noise_us_python": 2.5959999999999996, + "gpu_bwutil_cpp": 0.701475900909142, + "gpu_bwutil_python": 0.6917198908812292, + "gpu_gap_stddev_us": 1.0652224995796558 + } + } + } + }, + "resize_planar_nchw_contract_cubic_4k_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "4x2160x3840" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBAf32 contract cubic planar (advanced)." + }, + "baselines": { + "resize_planar_nchw_contract_cubic_4k_float4_advanced[InOutDataType=float4][shape=4x2160x3840][resizeType=CONTRACT][interpolation=CUBIC][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 763.7221386163999, + "gpu_time_us_python": 774.9057144782199, + "gpu_noise_us_cpp": 1.036, + "gpu_noise_us_python": 3.868, + "gpu_bwutil_cpp": 0.5586693750298848, + "gpu_bwutil_python": 0.5506056730670184, + "gpu_gap_stddev_us": 1.4203377413833245 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 467.73301312658, + "gpu_time_us_python": 477.0975444066, + "gpu_noise_us_cpp": 1.1280000000000001, + "gpu_noise_us_python": 3.718, + "gpu_bwutil_cpp": 0.6957505737594567, + "gpu_bwutil_python": 0.6820974285904343, + "gpu_gap_stddev_us": 1.393899836189297 + } + } + } + }, + "resize_planar_nchw_expand_linear_720p_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "64x720x1280" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGB8 expand linear planar (advanced)." + }, + "baselines": { + "resize_planar_nchw_expand_linear_720p_uchar3_advanced[InOutDataType=uchar3][shape=64x720x1280][resizeType=EXPAND][interpolation=LINEAR][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1166.76393719706, + "gpu_time_us_python": 1178.71946034906, + "gpu_noise_us_cpp": 2.038, + "gpu_noise_us_python": 2.29, + "gpu_bwutil_cpp": 0.4875810254280671, + "gpu_bwutil_python": 0.4826371799907892, + "gpu_gap_stddev_us": 3.303095766541948 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1104.7719919307199, + "gpu_time_us_python": 1113.90432567708, + "gpu_noise_us_cpp": 1.088, + "gpu_noise_us_python": 4.476, + "gpu_bwutil_cpp": 0.39281075434606605, + "gpu_bwutil_python": 0.3895863457034379, + "gpu_gap_stddev_us": 3.8596441264156907 + } + } + } + }, + "resize_planar_nchw_expand_linear_720p_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "16x720x1280" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBf32 expand linear planar (advanced)." + }, + "baselines": { + "resize_planar_nchw_expand_linear_720p_float3_advanced[InOutDataType=float3][shape=16x720x1280][resizeType=EXPAND][interpolation=LINEAR][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 696.2708894944401, + "gpu_time_us_python": 708.1826794234801, + "gpu_noise_us_cpp": 1.616, + "gpu_noise_us_python": 2.39, + "gpu_bwutil_cpp": 0.8170513211473416, + "gpu_bwutil_python": 0.8033147226487841, + "gpu_gap_stddev_us": 2.1847873994637577 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 631.76428987498, + "gpu_time_us_python": 639.65730307466, + "gpu_noise_us_cpp": 1.592, + "gpu_noise_us_python": 2.782, + "gpu_bwutil_cpp": 0.6868391930473055, + "gpu_bwutil_python": 0.6783533964569798, + "gpu_gap_stddev_us": 1.5432468760004516 + } + } + } + }, + "resize_planar_nchw_expand_linear_720p_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "64x720x1280" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBA8 expand linear planar (advanced) (tensor-only: RGBA8p planar var-shape unsupported by the Python image API)." + }, + "baselines": { + "resize_planar_nchw_expand_linear_720p_uchar4_advanced[InOutDataType=uchar4][shape=64x720x1280][resizeType=EXPAND][interpolation=LINEAR][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1550.79332924572, + "gpu_time_us_python": 1562.54597902664, + "gpu_noise_us_cpp": 1.492, + "gpu_noise_us_python": 2.726, + "gpu_bwutil_cpp": 0.4891190273527556, + "gpu_bwutil_python": 0.48544004220257764, + "gpu_gap_stddev_us": 2.9430583865628295 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1469.6701635898, + "gpu_time_us_python": 1481.1066554551599, + "gpu_noise_us_cpp": 1.1280000000000001, + "gpu_noise_us_python": 4.504, + "gpu_bwutil_cpp": 0.39371018434313987, + "gpu_bwutil_python": 0.39066846773157093, + "gpu_gap_stddev_us": 1.9899515857199426 + } + } + } + }, + "resize_planar_nchw_expand_linear_720p_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x720x1280" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBAf32 expand linear planar (advanced)." + }, + "baselines": { + "resize_planar_nchw_expand_linear_720p_float4_advanced[InOutDataType=float4][shape=16x720x1280][resizeType=EXPAND][interpolation=LINEAR][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 924.5896307304199, + "gpu_time_us_python": 935.1626579440999, + "gpu_noise_us_cpp": 1.552, + "gpu_noise_us_python": 3.3860000000000006, + "gpu_bwutil_cpp": 0.8203840291457141, + "gpu_bwutil_python": 0.8111092886271077, + "gpu_gap_stddev_us": 0.8464920990228898 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 839.4001472569801, + "gpu_time_us_python": 848.76046197826, + "gpu_noise_us_cpp": 1.3279999999999998, + "gpu_noise_us_python": 4.712000000000001, + "gpu_bwutil_cpp": 0.6892579023828752, + "gpu_bwutil_python": 0.6816439328258355, + "gpu_gap_stddev_us": 3.4300211892719554 + } + } + } + }, + "resize_planar_nchw_expand_linear_1080p_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGB8 expand linear planar (basic)." + }, + "baselines": { + "resize_planar_nchw_expand_linear_1080p_uchar3_basic[InOutDataType=uchar3][shape=32x1080x1920][resizeType=EXPAND][interpolation=LINEAR][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1397.71419177504, + "gpu_time_us_python": 1408.5020459017, + "gpu_noise_us_cpp": 1.09, + "gpu_noise_us_python": 3.2980000000000005, + "gpu_bwutil_cpp": 0.4578924838668625, + "gpu_bwutil_python": 0.45438585307828594, + "gpu_gap_stddev_us": 2.8861812882731197 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1324.08289077574, + "gpu_time_us_python": 1334.71640226624, + "gpu_noise_us_cpp": 2.4479999999999995, + "gpu_noise_us_python": 3.6, + "gpu_bwutil_cpp": 0.3687173733604911, + "gpu_bwutil_python": 0.3657786212969919, + "gpu_gap_stddev_us": 2.0178893103268454 + } + } + } + }, + "resize_planar_nchw_expand_linear_1080p_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGB8 expand linear planar (advanced)." + }, + "baselines": { + "resize_planar_nchw_expand_linear_1080p_uchar3_advanced[InOutDataType=uchar3][shape=32x1080x1920][resizeType=EXPAND][interpolation=LINEAR][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1397.8558507940402, + "gpu_time_us_python": 1407.9620545676996, + "gpu_noise_us_cpp": 2.004, + "gpu_noise_us_python": 2.118, + "gpu_bwutil_cpp": 0.4578464739969907, + "gpu_bwutil_python": 0.4545598532519198, + "gpu_gap_stddev_us": 0.3890271201140599 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1324.26300764794, + "gpu_time_us_python": 1334.2482523533, + "gpu_noise_us_cpp": 1.8539999999999999, + "gpu_noise_us_python": 3.964, + "gpu_bwutil_cpp": 0.3686694966358897, + "gpu_bwutil_python": 0.3659060178120773, + "gpu_gap_stddev_us": 1.083572011428458 + } + } + } + }, + "resize_planar_nchw_expand_linear_1080p_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "8x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBf32 expand linear planar (basic)." + }, + "baselines": { + "resize_planar_nchw_expand_linear_1080p_float3_basic[InOutDataType=float3][shape=8x1080x1920][resizeType=EXPAND][interpolation=LINEAR][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 781.9078324361999, + "gpu_time_us_python": 792.3703106089399, + "gpu_noise_us_cpp": 1.9, + "gpu_noise_us_python": 2.628, + "gpu_bwutil_cpp": 0.8185109272083745, + "gpu_bwutil_python": 0.8077045106267228, + "gpu_gap_stddev_us": 1.2120812408809236 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 711.18854194752, + "gpu_time_us_python": 721.1199317857001, + "gpu_noise_us_cpp": 1.2280000000000002, + "gpu_noise_us_python": 4.2700000000000005, + "gpu_bwutil_cpp": 0.6864055872504299, + "gpu_bwutil_python": 0.6769416487352248, + "gpu_gap_stddev_us": 1.3105466928764036 + } + } + } + }, + "resize_planar_nchw_expand_linear_1080p_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "8x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBf32 expand linear planar (advanced)." + }, + "baselines": { + "resize_planar_nchw_expand_linear_1080p_float3_advanced[InOutDataType=float3][shape=8x1080x1920][resizeType=EXPAND][interpolation=LINEAR][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 781.9276729598199, + "gpu_time_us_python": 792.57085218288, + "gpu_noise_us_cpp": 1.59, + "gpu_noise_us_python": 2.934, + "gpu_bwutil_cpp": 0.8184901453149154, + "gpu_bwutil_python": 0.8075002368031138, + "gpu_gap_stddev_us": 1.0770776311064547 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 711.1581518680999, + "gpu_time_us_python": 719.78723170272, + "gpu_noise_us_cpp": 1.4700000000000002, + "gpu_noise_us_python": 2.128, + "gpu_bwutil_cpp": 0.6864376341622371, + "gpu_bwutil_python": 0.6782024276116292, + "gpu_gap_stddev_us": 0.5404915292261883 + } + } + } + }, + "resize_planar_nchw_expand_linear_1080p_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBA8 expand linear planar (advanced) (tensor-only: RGBA8p planar var-shape unsupported by the Python image API)." + }, + "baselines": { + "resize_planar_nchw_expand_linear_1080p_uchar4_advanced[InOutDataType=uchar4][shape=32x1080x1920][resizeType=EXPAND][interpolation=LINEAR][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1858.74863956956, + "gpu_time_us_python": 1869.8913511364801, + "gpu_noise_us_cpp": 1.818, + "gpu_noise_us_python": 3.5760000000000005, + "gpu_bwutil_cpp": 0.4590927320535464, + "gpu_bwutil_python": 0.4563574824028393, + "gpu_gap_stddev_us": 1.4003588635298387 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1762.17021948664, + "gpu_time_us_python": 1771.7704408083403, + "gpu_noise_us_cpp": 1.5179999999999998, + "gpu_noise_us_python": 2.7359999999999998, + "gpu_bwutil_cpp": 0.36940627810165, + "gpu_bwutil_python": 0.36739986252663337, + "gpu_gap_stddev_us": 1.0937863611510033 + } + } + } + }, + "resize_planar_nchw_expand_linear_1080p_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "8x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBAf32 expand linear planar (advanced)." + }, + "baselines": { + "resize_planar_nchw_expand_linear_1080p_float4_advanced[InOutDataType=float4][shape=8x1080x1920][resizeType=EXPAND][interpolation=LINEAR][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1039.21046543908, + "gpu_time_us_python": 1049.7331343950202, + "gpu_noise_us_cpp": 1.9440000000000002, + "gpu_noise_us_python": 4.146, + "gpu_bwutil_cpp": 0.8211363161648914, + "gpu_bwutil_python": 0.8129054775665043, + "gpu_gap_stddev_us": 0.930943632039691 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 944.74770590678, + "gpu_time_us_python": 954.0000795440399, + "gpu_noise_us_cpp": 1.3379999999999999, + "gpu_noise_us_python": 2.706, + "gpu_bwutil_cpp": 0.6889475478360095, + "gpu_bwutil_python": 0.6822612440808001, + "gpu_gap_stddev_us": 0.6715452407851467 + } + } + } + }, + "resize_planar_nchw_expand_linear_4k_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "8x2160x3840" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGB8 expand linear planar (advanced)." + }, + "baselines": { + "resize_planar_nchw_expand_linear_4k_uchar3_advanced[InOutDataType=uchar3][shape=8x2160x3840][resizeType=EXPAND][interpolation=LINEAR][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1311.45523583998, + "gpu_time_us_python": 1322.4835874083801, + "gpu_noise_us_cpp": 1.7259999999999998, + "gpu_noise_us_python": 4.328, + "gpu_bwutil_cpp": 0.4880103355988825, + "gpu_bwutil_python": 0.48394061674065014, + "gpu_gap_stddev_us": 2.481820411079216 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1242.0886494502201, + "gpu_time_us_python": 1252.47200626204, + "gpu_noise_us_cpp": 1.4940000000000002, + "gpu_noise_us_python": 2.882, + "gpu_bwutil_cpp": 0.393056597091482, + "gpu_bwutil_python": 0.3897938780189283, + "gpu_gap_stddev_us": 1.5663890783915457 + } + } + } + }, + "resize_planar_nchw_expand_linear_4k_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "2x2160x3840" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBf32 expand linear planar (advanced)." + }, + "baselines": { + "resize_planar_nchw_expand_linear_4k_float3_advanced[InOutDataType=float3][shape=2x2160x3840][resizeType=EXPAND][interpolation=LINEAR][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 782.95359972858, + "gpu_time_us_python": 793.55231548378, + "gpu_noise_us_cpp": 1.7020000000000004, + "gpu_noise_us_python": 3.176, + "gpu_bwutil_cpp": 0.8174176667571096, + "gpu_bwutil_python": 0.8065029407325278, + "gpu_gap_stddev_us": 1.4839159661457062 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 708.05678525804, + "gpu_time_us_python": 717.4360717624801, + "gpu_noise_us_cpp": 1.074, + "gpu_noise_us_python": 3.192, + "gpu_bwutil_cpp": 0.6894411991443992, + "gpu_bwutil_python": 0.6804277732169005, + "gpu_gap_stddev_us": 0.5391853515378785 + } + } + } + }, + "resize_planar_nchw_expand_linear_4k_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "8x2160x3840" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBA8 expand linear planar (advanced) (tensor-only: RGBA8p planar var-shape unsupported by the Python image API)." + }, + "baselines": { + "resize_planar_nchw_expand_linear_4k_uchar4_advanced[InOutDataType=uchar4][shape=8x2160x3840][resizeType=EXPAND][interpolation=LINEAR][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1744.2494431876198, + "gpu_time_us_python": 1755.65177998984, + "gpu_noise_us_cpp": 1.732, + "gpu_noise_us_python": 2.906, + "gpu_bwutil_cpp": 0.4892286247712492, + "gpu_bwutil_python": 0.4860517625006394, + "gpu_gap_stddev_us": 1.1806632799408676 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1652.5940754341002, + "gpu_time_us_python": 1663.2706144511599, + "gpu_noise_us_cpp": 1.1560000000000001, + "gpu_noise_us_python": 3.182, + "gpu_bwutil_cpp": 0.3938981483280675, + "gpu_bwutil_python": 0.3913685535632786, + "gpu_gap_stddev_us": 1.3715752328765405 + } + } + } + }, + "resize_planar_nchw_expand_linear_4k_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "2x2160x3840" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBAf32 expand linear planar (advanced)." + }, + "baselines": { + "resize_planar_nchw_expand_linear_4k_float4_advanced[InOutDataType=float4][shape=2x2160x3840][resizeType=EXPAND][interpolation=LINEAR][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1040.3772197817998, + "gpu_time_us_python": 1050.88453596848, + "gpu_noise_us_cpp": 1.7560000000000002, + "gpu_noise_us_python": 3.35, + "gpu_bwutil_cpp": 0.8202154490384995, + "gpu_bwutil_python": 0.8120147688338972, + "gpu_gap_stddev_us": 0.5826923604003749 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 939.3583045401599, + "gpu_time_us_python": 950.2236318011, + "gpu_noise_us_cpp": 2.6860000000000004, + "gpu_noise_us_python": 3.4240000000000004, + "gpu_bwutil_cpp": 0.6928997853247821, + "gpu_bwutil_python": 0.684984054872664, + "gpu_gap_stddev_us": 2.2668138590099605 + } + } + } + }, + "resize_planar_nchw_expand_cubic_720p_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "16x720x1280" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGB8 expand cubic planar (advanced)." + }, + "baselines": { + "resize_planar_nchw_expand_cubic_720p_uchar3_advanced[InOutDataType=uchar3][shape=16x720x1280][resizeType=EXPAND][interpolation=CUBIC][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 463.24556100334, + "gpu_time_us_python": 475.22068382932, + "gpu_noise_us_cpp": 0.866, + "gpu_noise_us_python": 2.774, + "gpu_bwutil_cpp": 0.3070146581043313, + "gpu_bwutil_python": 0.2992836290183677, + "gpu_gap_stddev_us": 2.10621931226558 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 390.79961462282006, + "gpu_time_us_python": 400.86516914358, + "gpu_noise_us_cpp": 1.4, + "gpu_noise_us_python": 2.088, + "gpu_bwutil_cpp": 0.277608274315603, + "gpu_bwutil_python": 0.27064702885382047, + "gpu_gap_stddev_us": 1.3736638555904357 + } + } + } + }, + "resize_planar_nchw_expand_cubic_720p_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "16x720x1280" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBf32 expand cubic planar (advanced)." + }, + "baselines": { + "resize_planar_nchw_expand_cubic_720p_float3_advanced[InOutDataType=float3][shape=16x720x1280][resizeType=EXPAND][interpolation=CUBIC][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 723.76971796064, + "gpu_time_us_python": 734.3905736623599, + "gpu_noise_us_cpp": 2.108, + "gpu_noise_us_python": 3.2699999999999996, + "gpu_bwutil_cpp": 0.7860085533816638, + "gpu_bwutil_python": 0.7746410367213541, + "gpu_gap_stddev_us": 0.7727452154501216 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 647.12313057866, + "gpu_time_us_python": 658.5743202176, + "gpu_noise_us_cpp": 1.3820000000000001, + "gpu_noise_us_python": 3.9559999999999995, + "gpu_bwutil_cpp": 0.6705338306032521, + "gpu_bwutil_python": 0.6589143803470139, + "gpu_gap_stddev_us": 3.0667961172849214 + } + } + } + }, + "resize_planar_nchw_expand_cubic_720p_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "16x720x1280" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBA8 expand cubic planar (advanced) (tensor-only: RGBA8p planar var-shape unsupported by the Python image API)." + }, + "baselines": { + "resize_planar_nchw_expand_cubic_720p_uchar4_advanced[InOutDataType=uchar4][shape=16x720x1280][resizeType=EXPAND][interpolation=CUBIC][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 613.1775386879, + "gpu_time_us_python": 623.73382707324, + "gpu_noise_us_cpp": 1.09, + "gpu_noise_us_python": 2.216, + "gpu_bwutil_cpp": 0.309259908797155, + "gpu_bwutil_python": 0.3040274094884491, + "gpu_gap_stddev_us": 1.5668658189212152 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 517.92274711168, + "gpu_time_us_python": 527.02339444544, + "gpu_noise_us_cpp": 1.078, + "gpu_noise_us_python": 3.2079999999999997, + "gpu_bwutil_cpp": 0.279296648200564, + "gpu_bwutil_python": 0.2744617770889216, + "gpu_gap_stddev_us": 1.3244947990145066 + } + } + } + }, + "resize_planar_nchw_expand_cubic_720p_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x720x1280" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBAf32 expand cubic planar (advanced)." + }, + "baselines": { + "resize_planar_nchw_expand_cubic_720p_float4_advanced[InOutDataType=float4][shape=16x720x1280][resizeType=EXPAND][interpolation=CUBIC][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 961.44666513506, + "gpu_time_us_python": 972.7813712863799, + "gpu_noise_us_cpp": 2.4159999999999995, + "gpu_noise_us_python": 3.5700000000000003, + "gpu_bwutil_cpp": 0.788935096834049, + "gpu_bwutil_python": 0.7797434388484237, + "gpu_gap_stddev_us": 1.789864267421964 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 858.32802634772, + "gpu_time_us_python": 869.1279441345401, + "gpu_noise_us_cpp": 1.668, + "gpu_noise_us_python": 3.338, + "gpu_bwutil_cpp": 0.6740470309072543, + "gpu_bwutil_python": 0.6656901189903544, + "gpu_gap_stddev_us": 2.618048677685145 + } + } + } + }, + "resize_planar_nchw_expand_cubic_1080p_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "8x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize RGB8 expand cubic planar (basic)." + }, + "baselines": { + "resize_planar_nchw_expand_cubic_1080p_uchar3_basic[InOutDataType=uchar3][shape=8x1080x1920][resizeType=EXPAND][interpolation=CUBIC][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 552.913627826, + "gpu_time_us_python": 564.9379483001601, + "gpu_noise_us_cpp": 1.08, + "gpu_noise_us_python": 2.858, + "gpu_bwutil_cpp": 0.28937780440626903, + "gpu_bwutil_python": 0.2832179042314135, + "gpu_gap_stddev_us": 1.834229349355586 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 466.84817166502, + "gpu_time_us_python": 476.94381285782003, + "gpu_noise_us_cpp": 1.002, + "gpu_noise_us_python": 3.004, + "gpu_bwutil_cpp": 0.26143510419329935, + "gpu_bwutil_python": 0.255896934951079, + "gpu_gap_stddev_us": 2.325751119685661 + } + } + } + }, + "resize_planar_nchw_expand_cubic_1080p_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "8x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize RGB8 expand cubic planar (advanced)." + }, + "baselines": { + "resize_planar_nchw_expand_cubic_1080p_uchar3_advanced[InOutDataType=uchar3][shape=8x1080x1920][resizeType=EXPAND][interpolation=CUBIC][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 552.94890467358, + "gpu_time_us_python": 563.3985905600401, + "gpu_noise_us_cpp": 1.294, + "gpu_noise_us_python": 1.8700000000000003, + "gpu_bwutil_cpp": 0.28935936794137096, + "gpu_bwutil_python": 0.2839937740241001, + "gpu_gap_stddev_us": 1.2190598358065363 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 466.96707006076, + "gpu_time_us_python": 476.45933552646, + "gpu_noise_us_cpp": 1.384, + "gpu_noise_us_python": 3.0679999999999996, + "gpu_bwutil_cpp": 0.2613693012218946, + "gpu_bwutil_python": 0.2561510064579867, + "gpu_gap_stddev_us": 1.1770719413607327 + } + } + } + }, + "resize_planar_nchw_expand_cubic_1080p_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "8x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize RGBf32 expand cubic planar (basic)." + }, + "baselines": { + "resize_planar_nchw_expand_cubic_1080p_float3_basic[InOutDataType=float3][shape=8x1080x1920][resizeType=EXPAND][interpolation=CUBIC][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 813.2980334680799, + "gpu_time_us_python": 825.7747937451198, + "gpu_noise_us_cpp": 2.118, + "gpu_noise_us_python": 3.918, + "gpu_bwutil_cpp": 0.7869198868296012, + "gpu_bwutil_python": 0.7750368771729137, + "gpu_gap_stddev_us": 2.5780261620336766 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 727.1266631769801, + "gpu_time_us_python": 736.73738486562, + "gpu_noise_us_cpp": 1.558, + "gpu_noise_us_python": 4.012, + "gpu_bwutil_cpp": 0.6713581758557575, + "gpu_bwutil_python": 0.6626145930129552, + "gpu_gap_stddev_us": 3.16632510265708 + } + } + } + }, + "resize_planar_nchw_expand_cubic_1080p_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "8x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize RGBf32 expand cubic planar (advanced)." + }, + "baselines": { + "resize_planar_nchw_expand_cubic_1080p_float3_advanced[InOutDataType=float3][shape=8x1080x1920][resizeType=EXPAND][interpolation=CUBIC][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 813.42703042088, + "gpu_time_us_python": 823.8396652826601, + "gpu_noise_us_cpp": 2.2359999999999998, + "gpu_noise_us_python": 3.47, + "gpu_bwutil_cpp": 0.7867947874518842, + "gpu_bwutil_python": 0.7768505327386547, + "gpu_gap_stddev_us": 0.48458430072346304 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 727.1010094283599, + "gpu_time_us_python": 737.1069361712999, + "gpu_noise_us_cpp": 1.554, + "gpu_noise_us_python": 4.62, + "gpu_bwutil_cpp": 0.6713782452913115, + "gpu_bwutil_python": 0.6622852350984625, + "gpu_gap_stddev_us": 3.0299789963540764 + } + } + } + }, + "resize_planar_nchw_expand_cubic_1080p_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "8x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize RGBA8 expand cubic planar (advanced) (tensor-only: RGBA8p planar var-shape unsupported by the Python image API)." + }, + "baselines": { + "resize_planar_nchw_expand_cubic_1080p_uchar4_advanced[InOutDataType=uchar4][shape=8x1080x1920][resizeType=EXPAND][interpolation=CUBIC][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 732.5539098031, + "gpu_time_us_python": 743.70681864326, + "gpu_noise_us_cpp": 1.256, + "gpu_noise_us_python": 3.522, + "gpu_bwutil_cpp": 0.29122039249163445, + "gpu_bwutil_python": 0.28685253046980896, + "gpu_gap_stddev_us": 1.3230538022826757 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 619.4717927477201, + "gpu_time_us_python": 628.83976585172, + "gpu_noise_us_cpp": 2.5340000000000003, + "gpu_noise_us_python": 3.0060000000000002, + "gpu_bwutil_cpp": 0.26270328354366007, + "gpu_bwutil_python": 0.2587784054419346, + "gpu_gap_stddev_us": 1.798357461351853 + } + } + } + }, + "resize_planar_nchw_expand_cubic_1080p_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "8x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize RGBAf32 expand cubic planar (advanced)." + }, + "baselines": { + "resize_planar_nchw_expand_cubic_1080p_float4_advanced[InOutDataType=float4][shape=8x1080x1920][resizeType=EXPAND][interpolation=CUBIC][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1081.14803299816, + "gpu_time_us_python": 1092.21186519812, + "gpu_noise_us_cpp": 2.668, + "gpu_noise_us_python": 3.9599999999999995, + "gpu_bwutil_cpp": 0.7892844875168363, + "gpu_bwutil_python": 0.7812925394529142, + "gpu_gap_stddev_us": 2.726957272161935 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 965.3526360264799, + "gpu_time_us_python": 974.9421723940001, + "gpu_noise_us_cpp": 2.124, + "gpu_noise_us_python": 3.7040000000000006, + "gpu_bwutil_cpp": 0.6742427058534206, + "gpu_bwutil_python": 0.6676107051800845, + "gpu_gap_stddev_us": 0.6458685778024903 + } + } + } + }, + "resize_planar_nchw_expand_cubic_4k_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "2x2160x3840" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize RGB8 expand cubic planar (advanced)." + }, + "baselines": { + "resize_planar_nchw_expand_cubic_4k_uchar3_advanced[InOutDataType=uchar3][shape=2x2160x3840][resizeType=EXPAND][interpolation=CUBIC][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 519.77503362504, + "gpu_time_us_python": 531.45725576112, + "gpu_noise_us_cpp": 1.118, + "gpu_noise_us_python": 3.1900000000000004, + "gpu_bwutil_cpp": 0.3078278643753865, + "gpu_bwutil_python": 0.3010661635320845, + "gpu_gap_stddev_us": 1.4228401488304683 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 438.3689266312999, + "gpu_time_us_python": 448.17192242904, + "gpu_noise_us_cpp": 1.8020000000000003, + "gpu_noise_us_python": 3.0140000000000002, + "gpu_bwutil_cpp": 0.2784156352661359, + "gpu_bwutil_python": 0.27233311792975107, + "gpu_gap_stddev_us": 1.2472605969952206 + } + } + } + }, + "resize_planar_nchw_expand_cubic_4k_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "2x2160x3840" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize RGBf32 expand cubic planar (advanced)." + }, + "baselines": { + "resize_planar_nchw_expand_cubic_4k_float3_advanced[InOutDataType=float3][shape=2x2160x3840][resizeType=EXPAND][interpolation=CUBIC][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 813.3678516009601, + "gpu_time_us_python": 823.37414932088, + "gpu_noise_us_cpp": 2.0460000000000003, + "gpu_noise_us_python": 3.312, + "gpu_bwutil_cpp": 0.7868518781359161, + "gpu_bwutil_python": 0.7772895842199118, + "gpu_gap_stddev_us": 0.5109929512930411 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 721.03507037562, + "gpu_time_us_python": 730.51368908556, + "gpu_noise_us_cpp": 2.0140000000000002, + "gpu_noise_us_python": 2.8459999999999996, + "gpu_bwutil_cpp": 0.6770533845203808, + "gpu_bwutil_python": 0.6682660939707565, + "gpu_gap_stddev_us": 0.4841934358239881 + } + } + } + }, + "resize_planar_nchw_expand_cubic_4k_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "2x2160x3840" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize RGBA8 expand cubic planar (advanced) (tensor-only: RGBA8p planar var-shape unsupported by the Python image API)." + }, + "baselines": { + "resize_planar_nchw_expand_cubic_4k_uchar4_advanced[InOutDataType=uchar4][shape=2x2160x3840][resizeType=EXPAND][interpolation=CUBIC][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 688.35385254646, + "gpu_time_us_python": 697.8889626277399, + "gpu_noise_us_cpp": 1.17, + "gpu_noise_us_python": 2.6, + "gpu_bwutil_cpp": 0.30992049187752846, + "gpu_bwutil_python": 0.30568611169582566, + "gpu_gap_stddev_us": 0.4132384342780733 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 581.2740518711801, + "gpu_time_us_python": 590.3980810713599, + "gpu_noise_us_cpp": 0.9339999999999999, + "gpu_noise_us_python": 3.914, + "gpu_bwutil_cpp": 0.27996366378536147, + "gpu_bwutil_python": 0.27563969051775994, + "gpu_gap_stddev_us": 0.40359573360710954 + } + } + } + }, + "resize_planar_nchw_expand_cubic_4k_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "2x2160x3840" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize RGBAf32 expand cubic planar (advanced)." + }, + "baselines": { + "resize_planar_nchw_expand_cubic_4k_float4_advanced[InOutDataType=float4][shape=2x2160x3840][resizeType=EXPAND][interpolation=CUBIC][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1080.4841940772399, + "gpu_time_us_python": 1091.27069266622, + "gpu_noise_us_cpp": 2.4720000000000004, + "gpu_noise_us_python": 3.6759999999999997, + "gpu_bwutil_cpp": 0.7897698230296806, + "gpu_bwutil_python": 0.7819634003903584, + "gpu_gap_stddev_us": 1.5209995028892267 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 958.8003203982, + "gpu_time_us_python": 969.5281919532599, + "gpu_noise_us_cpp": 1.9579999999999997, + "gpu_noise_us_python": 3.818, + "gpu_bwutil_cpp": 0.6788489021329094, + "gpu_bwutil_python": 0.6713437178420962, + "gpu_gap_stddev_us": 2.0514734110457673 + } + } + } + }, + "resize_fakeplanar_nchw_contract_area_tensor_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "AREA" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGB8 contract area tensor fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "resize_fakeplanar_nchw_contract_area_tensor_uchar3_advanced[InOutDataType=uchar3][shape=32x1080x1920][resizeType=CONTRACT][interpolation=AREA][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1102.1776409723402, + "gpu_time_us_python": 1118.8588590342201, + "gpu_noise_us_cpp": 1.5060000000000002, + "gpu_noise_us_python": 2.498, + "gpu_bwutil_cpp": 0.4355033939945108, + "gpu_bwutil_python": 0.42901144458523544, + "gpu_gap_stddev_us": 0.9519581810686664 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 846.3577376505, + "gpu_time_us_python": 860.13599012338, + "gpu_noise_us_cpp": 1.858, + "gpu_noise_us_python": 4.722, + "gpu_bwutil_cpp": 0.43256487939314175, + "gpu_bwutil_python": 0.425633513210441, + "gpu_gap_stddev_us": 1.6955740657801806 + } + } + } + }, + "resize_fakeplanar_nchw_contract_area_tensor_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "AREA" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBf32 contract area tensor fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "resize_fakeplanar_nchw_contract_area_tensor_float3_advanced[InOutDataType=float3][shape=16x1080x1920][resizeType=CONTRACT][interpolation=AREA][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1276.50361741546, + "gpu_time_us_python": 1294.33664746142, + "gpu_noise_us_cpp": 1.69, + "gpu_noise_us_python": 6.0, + "gpu_bwutil_cpp": 0.7520579298811367, + "gpu_bwutil_python": 0.7417021829903282, + "gpu_gap_stddev_us": 2.4006229804416974 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1047.7015305762, + "gpu_time_us_python": 1060.32992205944, + "gpu_noise_us_cpp": 1.814, + "gpu_noise_us_python": 2.962, + "gpu_bwutil_cpp": 0.6988675774636834, + "gpu_bwutil_python": 0.6905442249638453, + "gpu_gap_stddev_us": 0.8320434358059431 + } + } + } + }, + "resize_fakeplanar_nchw_contract_area_tensor_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "AREA" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBA8 contract area tensor fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "resize_fakeplanar_nchw_contract_area_tensor_uchar4_advanced[InOutDataType=uchar4][shape=32x1080x1920][resizeType=CONTRACT][interpolation=AREA][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1439.91175161712, + "gpu_time_us_python": 1457.1717192535202, + "gpu_noise_us_cpp": 1.5779999999999998, + "gpu_noise_us_python": 2.572, + "gpu_bwutil_cpp": 0.4444735887307763, + "gpu_bwutil_python": 0.43920821040534574, + "gpu_gap_stddev_us": 0.8780488444935068 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1108.9386123086, + "gpu_time_us_python": 1121.9123809856399, + "gpu_noise_us_cpp": 2.09, + "gpu_noise_us_python": 3.2920000000000003, + "gpu_bwutil_cpp": 0.4401862206691608, + "gpu_bwutil_python": 0.4350953833896514, + "gpu_gap_stddev_us": 0.4026144274204899 + } + } + } + }, + "resize_fakeplanar_nchw_contract_area_tensor_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "AREA" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBAf32 contract area tensor fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "resize_fakeplanar_nchw_contract_area_tensor_float4_advanced[InOutDataType=float4][shape=16x1080x1920][resizeType=CONTRACT][interpolation=AREA][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2005.86670949718, + "gpu_time_us_python": 2023.65394069298, + "gpu_noise_us_cpp": 1.558, + "gpu_noise_us_python": 4.07, + "gpu_bwutil_cpp": 0.6381349215359872, + "gpu_bwutil_python": 0.6325245997379187, + "gpu_gap_stddev_us": 0.7907629844732221 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1669.1614290872599, + "gpu_time_us_python": 1684.0424379246, + "gpu_noise_us_cpp": 3.282, + "gpu_noise_us_python": 4.266, + "gpu_bwutil_cpp": 0.5848875675723129, + "gpu_bwutil_python": 0.5797199839614947, + "gpu_gap_stddev_us": 2.8088788998735863 + } + } + } + }, + "resize_fakeplanar_nchw_contract_linear_720p_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "64x720x1280" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBf32 contract linear fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "resize_fakeplanar_nchw_contract_linear_720p_float3_advanced[InOutDataType=float3][shape=64x720x1280][resizeType=CONTRACT][interpolation=LINEAR][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2252.25948772676, + "gpu_time_us_python": 2270.4314333605, + "gpu_noise_us_cpp": 1.666, + "gpu_noise_us_python": 3.8560000000000003, + "gpu_bwutil_cpp": 0.7577619738328871, + "gpu_bwutil_python": 0.7516982859919153, + "gpu_gap_stddev_us": 1.5275540766859472 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1845.57778747638, + "gpu_time_us_python": 1858.9770476022998, + "gpu_noise_us_cpp": 2.02, + "gpu_noise_us_python": 3.6479999999999997, + "gpu_bwutil_cpp": 0.7053060235940283, + "gpu_bwutil_python": 0.7002222283162447, + "gpu_gap_stddev_us": 0.880645721608786 + } + } + } + }, + "resize_fakeplanar_nchw_contract_linear_720p_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "64x720x1280" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBAf32 contract linear fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "resize_fakeplanar_nchw_contract_linear_720p_float4_advanced[InOutDataType=float4][shape=64x720x1280][resizeType=CONTRACT][interpolation=LINEAR][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3542.9927538351003, + "gpu_time_us_python": 3562.3892602785795, + "gpu_noise_us_cpp": 2.686, + "gpu_noise_us_python": 4.8100000000000005, + "gpu_bwutil_cpp": 0.6422768197949854, + "gpu_bwutil_python": 0.6387778760496935, + "gpu_gap_stddev_us": 2.9770405834606333 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2943.25794358254, + "gpu_time_us_python": 2958.3655155406, + "gpu_noise_us_cpp": 3.3579999999999997, + "gpu_noise_us_python": 4.068, + "gpu_bwutil_cpp": 0.5896849397794366, + "gpu_bwutil_python": 0.5866734739036946, + "gpu_gap_stddev_us": 2.315940706086484 + } + } + } + }, + "resize_fakeplanar_nchw_contract_linear_1080p_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBf32 contract linear fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "resize_fakeplanar_nchw_contract_linear_1080p_float3_advanced[InOutDataType=float3][shape=32x1080x1920][resizeType=CONTRACT][interpolation=LINEAR][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2534.40531027914, + "gpu_time_us_python": 2555.0908830332, + "gpu_noise_us_cpp": 1.5239999999999998, + "gpu_noise_us_python": 5.254, + "gpu_bwutil_cpp": 0.7575799170107196, + "gpu_bwutil_python": 0.7514456819653483, + "gpu_gap_stddev_us": 3.017678805973279 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2079.5818244491, + "gpu_time_us_python": 2093.40010126426, + "gpu_noise_us_cpp": 1.94, + "gpu_noise_us_python": 4.222, + "gpu_bwutil_cpp": 0.7041847036619752, + "gpu_bwutil_python": 0.6995363032707871, + "gpu_gap_stddev_us": 1.2480140887453142 + } + } + } + }, + "resize_fakeplanar_nchw_contract_linear_1080p_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBAf32 contract linear fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "resize_fakeplanar_nchw_contract_linear_1080p_float4_advanced[InOutDataType=float4][shape=32x1080x1920][resizeType=CONTRACT][interpolation=LINEAR][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3988.46479590444, + "gpu_time_us_python": 4009.5028347439, + "gpu_noise_us_cpp": 2.1999999999999997, + "gpu_noise_us_python": 6.794, + "gpu_bwutil_cpp": 0.6418583516722158, + "gpu_bwutil_python": 0.6384908819138706, + "gpu_gap_stddev_us": 4.5641316568056824 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3318.58670146834, + "gpu_time_us_python": 3333.1562983780195, + "gpu_noise_us_cpp": 3.846, + "gpu_noise_us_python": 4.62, + "gpu_bwutil_cpp": 0.5883663086160785, + "gpu_bwutil_python": 0.5857944450754589, + "gpu_gap_stddev_us": 1.2268909365744574 + } + } + } + }, + "resize_fakeplanar_nchw_contract_linear_4k_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "8x2160x3840" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBf32 contract linear fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "resize_fakeplanar_nchw_contract_linear_4k_float3_advanced[InOutDataType=float3][shape=8x2160x3840][resizeType=CONTRACT][interpolation=LINEAR][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2527.9157871139796, + "gpu_time_us_python": 2546.7899833111396, + "gpu_noise_us_cpp": 1.7419999999999998, + "gpu_noise_us_python": 3.63, + "gpu_bwutil_cpp": 0.7595244812678585, + "gpu_bwutil_python": 0.7538956631697336, + "gpu_gap_stddev_us": 1.2119021092467412 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2075.1085502056603, + "gpu_time_us_python": 2090.2268250782604, + "gpu_noise_us_cpp": 2.7760000000000002, + "gpu_noise_us_python": 5.8020000000000005, + "gpu_bwutil_cpp": 0.7057029663789061, + "gpu_bwutil_python": 0.7005984892044751, + "gpu_gap_stddev_us": 0.557798791489643 + } + } + } + }, + "resize_fakeplanar_nchw_contract_linear_4k_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "8x2160x3840" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBAf32 contract linear fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "resize_fakeplanar_nchw_contract_linear_4k_float4_advanced[InOutDataType=float4][shape=8x2160x3840][resizeType=CONTRACT][interpolation=LINEAR][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3983.52391745832, + "gpu_time_us_python": 4003.7262205093202, + "gpu_noise_us_cpp": 2.37, + "gpu_noise_us_python": 6.642, + "gpu_bwutil_cpp": 0.6426552130868443, + "gpu_bwutil_python": 0.6394130294756224, + "gpu_gap_stddev_us": 1.9979782706111215 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3328.453484234, + "gpu_time_us_python": 3343.38748041784, + "gpu_noise_us_cpp": 3.95, + "gpu_noise_us_python": 5.348, + "gpu_bwutil_cpp": 0.5866222093072275, + "gpu_bwutil_python": 0.5840021544603775, + "gpu_gap_stddev_us": 0.668534469432592 + } + } + } + }, + "resize_fakeplanar_nchw_contract_cubic_720p_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "64x720x1280" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBf32 contract cubic fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "resize_fakeplanar_nchw_contract_cubic_720p_float3_advanced[InOutDataType=float3][shape=64x720x1280][resizeType=CONTRACT][interpolation=CUBIC][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2295.02804969324, + "gpu_time_us_python": 2313.42713064044, + "gpu_noise_us_cpp": 1.512, + "gpu_noise_us_python": 4.497999999999999, + "gpu_bwutil_cpp": 0.7436420152530161, + "gpu_bwutil_python": 0.7377316601812307, + "gpu_gap_stddev_us": 2.8091932810201405 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1959.93910059326, + "gpu_time_us_python": 1972.73433801866, + "gpu_noise_us_cpp": 2.728, + "gpu_noise_us_python": 3.5879999999999996, + "gpu_bwutil_cpp": 0.6641550430886438, + "gpu_bwutil_python": 0.6598458378817463, + "gpu_gap_stddev_us": 1.1855985161068947 + } + } + } + }, + "resize_fakeplanar_nchw_contract_cubic_720p_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "32x720x1280" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBAf32 contract cubic fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "resize_fakeplanar_nchw_contract_cubic_720p_float4_advanced[InOutDataType=float4][shape=32x720x1280][resizeType=CONTRACT][interpolation=CUBIC][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1809.3445686035, + "gpu_time_us_python": 1827.60938835632, + "gpu_noise_us_cpp": 2.1879999999999997, + "gpu_noise_us_python": 5.798, + "gpu_bwutil_cpp": 0.6288403113201279, + "gpu_bwutil_python": 0.6225560662017743, + "gpu_gap_stddev_us": 1.5541185174275858 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1495.77642915622, + "gpu_time_us_python": 1509.08984951224, + "gpu_noise_us_cpp": 3.2620000000000005, + "gpu_noise_us_python": 3.8620000000000005, + "gpu_bwutil_cpp": 0.5801654618922036, + "gpu_bwutil_python": 0.5750471438209456, + "gpu_gap_stddev_us": 2.0492128791314457 + } + } + } + }, + "resize_fakeplanar_nchw_contract_cubic_1080p_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize RGBf32 contract cubic fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "resize_fakeplanar_nchw_contract_cubic_1080p_float3_advanced[InOutDataType=float3][shape=32x1080x1920][resizeType=CONTRACT][interpolation=CUBIC][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2589.1266013906798, + "gpu_time_us_python": 2608.8926030239195, + "gpu_noise_us_cpp": 1.61, + "gpu_noise_us_python": 3.8899999999999997, + "gpu_bwutil_cpp": 0.7415692629573065, + "gpu_bwutil_python": 0.7359491019846658, + "gpu_gap_stddev_us": 3.2901795205680413 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2208.62399228808, + "gpu_time_us_python": 2223.1365704242203, + "gpu_noise_us_cpp": 1.8679999999999999, + "gpu_noise_us_python": 2.998, + "gpu_bwutil_cpp": 0.6630463924307681, + "gpu_bwutil_python": 0.6587175060871461, + "gpu_gap_stddev_us": 1.8756559469599445 + } + } + } + }, + "resize_fakeplanar_nchw_contract_cubic_1080p_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize RGBAf32 contract cubic fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "resize_fakeplanar_nchw_contract_cubic_1080p_float4_advanced[InOutDataType=float4][shape=16x1080x1920][resizeType=CONTRACT][interpolation=CUBIC][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2040.63451476458, + "gpu_time_us_python": 2058.5714463534, + "gpu_noise_us_cpp": 2.662, + "gpu_noise_us_python": 4.176, + "gpu_bwutil_cpp": 0.6272620289980281, + "gpu_bwutil_python": 0.6217968521477684, + "gpu_gap_stddev_us": 1.5703137184827363 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1686.1857274169402, + "gpu_time_us_python": 1699.94728064396, + "gpu_noise_us_cpp": 2.7439999999999998, + "gpu_noise_us_python": 4.462, + "gpu_bwutil_cpp": 0.5789838671441423, + "gpu_bwutil_python": 0.5742963817798994, + "gpu_gap_stddev_us": 1.643360176629806 + } + } + } + }, + "resize_fakeplanar_nchw_contract_cubic_4k_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "8x2160x3840" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBf32 contract cubic fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "resize_fakeplanar_nchw_contract_cubic_4k_float3_advanced[InOutDataType=float3][shape=8x2160x3840][resizeType=CONTRACT][interpolation=CUBIC][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2592.73210800796, + "gpu_time_us_python": 2611.55479319414, + "gpu_noise_us_cpp": 1.568, + "gpu_noise_us_python": 2.732, + "gpu_bwutil_cpp": 0.7405380064136882, + "gpu_bwutil_python": 0.7352006238420042, + "gpu_gap_stddev_us": 1.9550471255391504 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2208.61559467938, + "gpu_time_us_python": 2223.48728739424, + "gpu_noise_us_cpp": 2.428, + "gpu_noise_us_python": 4.378, + "gpu_bwutil_cpp": 0.6630485128507732, + "gpu_bwutil_python": 0.658612204362937, + "gpu_gap_stddev_us": 1.8347273843782068 + } + } + } + }, + "resize_fakeplanar_nchw_contract_cubic_4k_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "4x2160x3840" + ], + "resizeType": [ + "CONTRACT" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBAf32 contract cubic fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "resize_fakeplanar_nchw_contract_cubic_4k_float4_advanced[InOutDataType=float4][shape=4x2160x3840][resizeType=CONTRACT][interpolation=CUBIC][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2039.41398006452, + "gpu_time_us_python": 2057.03629411472, + "gpu_noise_us_cpp": 2.678, + "gpu_noise_us_python": 3.466, + "gpu_bwutil_cpp": 0.6276382950937157, + "gpu_bwutil_python": 0.6222588185481441, + "gpu_gap_stddev_us": 1.927833869137475 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1692.9319352716398, + "gpu_time_us_python": 1706.06084437592, + "gpu_noise_us_cpp": 2.804, + "gpu_noise_us_python": 4.529999999999999, + "gpu_bwutil_cpp": 0.5766761135426904, + "gpu_bwutil_python": 0.5722382932474798, + "gpu_gap_stddev_us": 1.4190283951008458 + } + } + } + }, + "resize_fakeplanar_nchw_expand_linear_720p_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "64x720x1280" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGB8 expand linear fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "resize_fakeplanar_nchw_expand_linear_720p_uchar3_advanced[InOutDataType=uchar3][shape=64x720x1280][resizeType=EXPAND][interpolation=LINEAR][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3190.0866152364597, + "gpu_time_us_python": 3208.2153718604995, + "gpu_noise_us_cpp": 1.384, + "gpu_noise_us_python": 3.317999999999999, + "gpu_bwutil_cpp": 0.534992414013074, + "gpu_bwutil_python": 0.5319687179555002, + "gpu_gap_stddev_us": 1.6776394263068122 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2575.33918390664, + "gpu_time_us_python": 2592.29109113352, + "gpu_noise_us_cpp": 1.4140000000000001, + "gpu_noise_us_python": 6.747999999999999, + "gpu_bwutil_cpp": 0.5054675485517389, + "gpu_bwutil_python": 0.5021623133833245, + "gpu_gap_stddev_us": 3.1266480788286426 + } + } + } + }, + "resize_fakeplanar_nchw_expand_linear_720p_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "16x720x1280" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBf32 expand linear fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "resize_fakeplanar_nchw_expand_linear_720p_float3_advanced[InOutDataType=float3][shape=16x720x1280][resizeType=EXPAND][interpolation=LINEAR][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2109.0700638393996, + "gpu_time_us_python": 2127.42420253104, + "gpu_noise_us_cpp": 1.3940000000000001, + "gpu_noise_us_python": 3.2520000000000002, + "gpu_bwutil_cpp": 0.8092039952216947, + "gpu_bwutil_python": 0.802222873862166, + "gpu_gap_stddev_us": 1.3398891900490624 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1666.6696829363602, + "gpu_time_us_python": 1680.27618907436, + "gpu_noise_us_cpp": 2.226, + "gpu_noise_us_python": 3.87, + "gpu_bwutil_cpp": 0.7810157407803366, + "gpu_bwutil_python": 0.7746918303697647, + "gpu_gap_stddev_us": 1.1798759914768986 + } + } + } + }, + "resize_fakeplanar_nchw_expand_linear_720p_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "64x720x1280" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBA8 expand linear fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "resize_fakeplanar_nchw_expand_linear_720p_uchar4_advanced[InOutDataType=uchar4][shape=64x720x1280][resizeType=EXPAND][interpolation=LINEAR][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3979.94668824328, + "gpu_time_us_python": 4001.6782109698797, + "gpu_noise_us_cpp": 2.0439999999999996, + "gpu_noise_us_python": 5.772, + "gpu_bwutil_cpp": 0.5717567904373102, + "gpu_bwutil_python": 0.568651582930588, + "gpu_gap_stddev_us": 0.9454951003769825 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3268.6601760791996, + "gpu_time_us_python": 3278.95370055284, + "gpu_noise_us_cpp": 1.828, + "gpu_noise_us_python": 3.9, + "gpu_bwutil_cpp": 0.5310095530548399, + "gpu_bwutil_python": 0.5293432663748967, + "gpu_gap_stddev_us": 1.1671051201436626 + } + } + } + }, + "resize_fakeplanar_nchw_expand_linear_720p_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x720x1280" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBAf32 expand linear fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "resize_fakeplanar_nchw_expand_linear_720p_float4_advanced[InOutDataType=float4][shape=16x720x1280][resizeType=EXPAND][interpolation=LINEAR][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2941.95246973698, + "gpu_time_us_python": 2961.0465711705197, + "gpu_noise_us_cpp": 5.2379999999999995, + "gpu_noise_us_python": 6.648000000000001, + "gpu_bwutil_cpp": 0.7734857284879668, + "gpu_bwutil_python": 0.768497736408295, + "gpu_gap_stddev_us": 0.9868715696939151 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2274.38888636496, + "gpu_time_us_python": 2284.82211373064, + "gpu_noise_us_cpp": 7.078, + "gpu_noise_us_python": 7.628, + "gpu_bwutil_cpp": 0.7631031176985849, + "gpu_bwutil_python": 0.7596185946968935, + "gpu_gap_stddev_us": 0.7012010119712785 + } + } + } + }, + "resize_fakeplanar_nchw_expand_linear_1080p_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGB8 expand linear fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "resize_fakeplanar_nchw_expand_linear_1080p_uchar3_advanced[InOutDataType=uchar3][shape=32x1080x1920][resizeType=EXPAND][interpolation=LINEAR][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3547.81386415718, + "gpu_time_us_python": 3567.12343586278, + "gpu_noise_us_cpp": 1.654, + "gpu_noise_us_python": 4.71, + "gpu_bwutil_cpp": 0.5411802954283426, + "gpu_bwutil_python": 0.5382510840037433, + "gpu_gap_stddev_us": 2.023857521070886 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2879.59878742378, + "gpu_time_us_python": 2894.88828067544, + "gpu_noise_us_cpp": 1.226, + "gpu_noise_us_python": 4.582, + "gpu_bwutil_cpp": 0.5085676156474875, + "gpu_bwutil_python": 0.5058805714985896, + "gpu_gap_stddev_us": 0.9750421912302938 + } + } + } + }, + "resize_fakeplanar_nchw_expand_linear_1080p_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "8x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBf32 expand linear fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "resize_fakeplanar_nchw_expand_linear_1080p_float3_advanced[InOutDataType=float3][shape=8x1080x1920][resizeType=EXPAND][interpolation=LINEAR][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2371.4699466273, + "gpu_time_us_python": 2391.3326281592404, + "gpu_noise_us_cpp": 1.834, + "gpu_noise_us_python": 6.344, + "gpu_bwutil_cpp": 0.8096252337201637, + "gpu_bwutil_python": 0.802900162856085, + "gpu_gap_stddev_us": 3.2305864990020052 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1869.76434632908, + "gpu_time_us_python": 1881.2105216437399, + "gpu_noise_us_cpp": 1.9939999999999998, + "gpu_noise_us_python": 4.343999999999999, + "gpu_bwutil_cpp": 0.7832040452729381, + "gpu_bwutil_python": 0.7784389437396071, + "gpu_gap_stddev_us": 1.337010694452952 + } + } + } + }, + "resize_fakeplanar_nchw_expand_linear_1080p_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBA8 expand linear fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "resize_fakeplanar_nchw_expand_linear_1080p_uchar4_advanced[InOutDataType=uchar4][shape=32x1080x1920][resizeType=EXPAND][interpolation=LINEAR][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4605.021091807901, + "gpu_time_us_python": 4624.81060952846, + "gpu_noise_us_cpp": 2.166, + "gpu_noise_us_python": 4.88, + "gpu_bwutil_cpp": 0.5559160099858962, + "gpu_bwutil_python": 0.5535367709677248, + "gpu_gap_stddev_us": 2.413434372784684 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3661.7226488347806, + "gpu_time_us_python": 3676.9679016163996, + "gpu_noise_us_cpp": 2.762, + "gpu_noise_us_python": 4.4719999999999995, + "gpu_bwutil_cpp": 0.5332622546981831, + "gpu_bwutil_python": 0.5310488849998007, + "gpu_gap_stddev_us": 3.321187276879864 + } + } + } + }, + "resize_fakeplanar_nchw_expand_linear_1080p_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "8x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBAf32 expand linear fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "resize_fakeplanar_nchw_expand_linear_1080p_float4_advanced[InOutDataType=float4][shape=8x1080x1920][resizeType=EXPAND][interpolation=LINEAR][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3309.7646750901395, + "gpu_time_us_python": 3331.1940670933, + "gpu_noise_us_cpp": 4.688, + "gpu_noise_us_python": 6.5280000000000005, + "gpu_bwutil_cpp": 0.773469724476962, + "gpu_bwutil_python": 0.7684944892095877, + "gpu_gap_stddev_us": 2.424979875495492 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2558.94021452686, + "gpu_time_us_python": 2574.82286808288, + "gpu_noise_us_cpp": 7.634, + "gpu_noise_us_python": 8.858, + "gpu_bwutil_cpp": 0.76302779963005, + "gpu_bwutil_python": 0.7583213456187293, + "gpu_gap_stddev_us": 1.934209689987429 + } + } + } + }, + "resize_fakeplanar_nchw_expand_linear_4k_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "8x2160x3840" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGB8 expand linear fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "resize_fakeplanar_nchw_expand_linear_4k_uchar3_advanced[InOutDataType=uchar3][shape=8x2160x3840][resizeType=EXPAND][interpolation=LINEAR][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3558.70860596774, + "gpu_time_us_python": 3577.70893450948, + "gpu_noise_us_cpp": 1.258, + "gpu_noise_us_python": 3.9619999999999997, + "gpu_bwutil_cpp": 0.5395232181701185, + "gpu_bwutil_python": 0.5366576890055655, + "gpu_gap_stddev_us": 1.6890602868201499 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2890.4172169314797, + "gpu_time_us_python": 2904.7077394429803, + "gpu_noise_us_cpp": 2.026, + "gpu_noise_us_python": 4.9, + "gpu_bwutil_cpp": 0.5066640231203321, + "gpu_bwutil_python": 0.504170305751104, + "gpu_gap_stddev_us": 1.0909578435565113 + } + } + } + }, + "resize_fakeplanar_nchw_expand_linear_4k_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "2x2160x3840" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBf32 expand linear fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "resize_fakeplanar_nchw_expand_linear_4k_float3_advanced[InOutDataType=float3][shape=2x2160x3840][resizeType=EXPAND][interpolation=LINEAR][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2369.2615945383595, + "gpu_time_us_python": 2387.57015319092, + "gpu_noise_us_cpp": 1.9759999999999998, + "gpu_noise_us_python": 5.292, + "gpu_bwutil_cpp": 0.8103800556272731, + "gpu_bwutil_python": 0.8041662032858605, + "gpu_gap_stddev_us": 1.9718679317365126 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1867.4235256749803, + "gpu_time_us_python": 1881.37989976348, + "gpu_noise_us_cpp": 3.944, + "gpu_noise_us_python": 3.8760000000000003, + "gpu_bwutil_cpp": 0.7841859015926058, + "gpu_bwutil_python": 0.7783691762863529, + "gpu_gap_stddev_us": 2.3605855608640773 + } + } + } + }, + "resize_fakeplanar_nchw_expand_linear_4k_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "8x2160x3840" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBA8 expand linear fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "resize_fakeplanar_nchw_expand_linear_4k_uchar4_advanced[InOutDataType=uchar4][shape=8x2160x3840][resizeType=EXPAND][interpolation=LINEAR][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4502.44027035572, + "gpu_time_us_python": 4523.09577465052, + "gpu_noise_us_cpp": 1.7340000000000004, + "gpu_noise_us_python": 5.61, + "gpu_bwutil_cpp": 0.5685817365512433, + "gpu_bwutil_python": 0.5659854995833786, + "gpu_gap_stddev_us": 1.8652087643152642 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3653.33817004136, + "gpu_time_us_python": 3667.32615749748, + "gpu_noise_us_cpp": 1.536, + "gpu_noise_us_python": 6.356, + "gpu_bwutil_cpp": 0.5344854100420005, + "gpu_bwutil_python": 0.5324456956909251, + "gpu_gap_stddev_us": 1.8742460159146477 + } + } + } + }, + "resize_fakeplanar_nchw_expand_linear_4k_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "2x2160x3840" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBAf32 expand linear fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "resize_fakeplanar_nchw_expand_linear_4k_float4_advanced[InOutDataType=float4][shape=2x2160x3840][resizeType=EXPAND][interpolation=LINEAR][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3307.44989859424, + "gpu_time_us_python": 3329.01450941852, + "gpu_noise_us_cpp": 4.692, + "gpu_noise_us_python": 5.586, + "gpu_bwutil_cpp": 0.7740116647955506, + "gpu_bwutil_python": 0.7689984844643118, + "gpu_gap_stddev_us": 3.2387721855209373 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2566.38007942506, + "gpu_time_us_python": 2582.4409730655198, + "gpu_noise_us_cpp": 8.575999999999999, + "gpu_noise_us_python": 10.65, + "gpu_bwutil_cpp": 0.7608157695510439, + "gpu_bwutil_python": 0.7560843530784449, + "gpu_gap_stddev_us": 2.3378386853298396 + } + } + } + }, + "resize_fakeplanar_nchw_expand_cubic_720p_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "16x720x1280" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGB8 expand cubic fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "resize_fakeplanar_nchw_expand_cubic_720p_uchar3_advanced[InOutDataType=uchar3][shape=16x720x1280][resizeType=EXPAND][interpolation=CUBIC][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1107.0259487798999, + "gpu_time_us_python": 1124.38127902884, + "gpu_noise_us_cpp": 1.7759999999999998, + "gpu_noise_us_python": 3.628, + "gpu_bwutil_cpp": 0.3854184736121784, + "gpu_bwutil_python": 0.3794690601822521, + "gpu_gap_stddev_us": 1.3006495739206954 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 850.4474609417, + "gpu_time_us_python": 864.31906650408, + "gpu_noise_us_cpp": 1.098, + "gpu_noise_us_python": 3.598, + "gpu_bwutil_cpp": 0.3826718411785779, + "gpu_bwutil_python": 0.37652495214972465, + "gpu_gap_stddev_us": 1.5997489402051606 + } + } + } + }, + "resize_fakeplanar_nchw_expand_cubic_720p_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "16x720x1280" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBf32 expand cubic fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "resize_fakeplanar_nchw_expand_cubic_720p_float3_advanced[InOutDataType=float3][shape=16x720x1280][resizeType=EXPAND][interpolation=CUBIC][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2296.8202978933796, + "gpu_time_us_python": 2316.3321448757, + "gpu_noise_us_cpp": 1.65, + "gpu_noise_us_python": 3.8839999999999995, + "gpu_bwutil_cpp": 0.7430576691302713, + "gpu_bwutil_python": 0.7367982908955535, + "gpu_gap_stddev_us": 2.450198554717251 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1856.7130906726802, + "gpu_time_us_python": 1872.1523107375, + "gpu_noise_us_cpp": 1.7579999999999998, + "gpu_noise_us_python": 4.928, + "gpu_bwutil_cpp": 0.7010943858220702, + "gpu_bwutil_python": 0.6953123481055321, + "gpu_gap_stddev_us": 1.5631228038464007 + } + } + } + }, + "resize_fakeplanar_nchw_expand_cubic_720p_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "16x720x1280" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBA8 expand cubic fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "resize_fakeplanar_nchw_expand_cubic_720p_uchar4_advanced[InOutDataType=uchar4][shape=16x720x1280][resizeType=EXPAND][interpolation=CUBIC][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1347.7249468526, + "gpu_time_us_python": 1367.32727572278, + "gpu_noise_us_cpp": 1.528, + "gpu_noise_us_python": 5.262, + "gpu_bwutil_cpp": 0.42211151275459013, + "gpu_bwutil_python": 0.416060247102832, + "gpu_gap_stddev_us": 2.484983784049926 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1113.5756413684, + "gpu_time_us_python": 1127.156166661, + "gpu_noise_us_cpp": 1.3379999999999999, + "gpu_noise_us_python": 4.116, + "gpu_bwutil_cpp": 0.3896740490735251, + "gpu_bwutil_python": 0.38496881800625216, + "gpu_gap_stddev_us": 2.0524627528459343 + } + } + } + }, + "resize_fakeplanar_nchw_expand_cubic_720p_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x720x1280" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Resize RGBAf32 expand cubic fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "resize_fakeplanar_nchw_expand_cubic_720p_float4_advanced[InOutDataType=float4][shape=16x720x1280][resizeType=EXPAND][interpolation=CUBIC][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3021.93749037128, + "gpu_time_us_python": 3041.18739616294, + "gpu_noise_us_cpp": 3.8880000000000003, + "gpu_noise_us_python": 6.656000000000001, + "gpu_bwutil_cpp": 0.7530133755775177, + "gpu_bwutil_python": 0.7482486106670416, + "gpu_gap_stddev_us": 3.2344032327880647 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2465.3318367474, + "gpu_time_us_python": 2476.68410818956, + "gpu_noise_us_cpp": 7.394, + "gpu_noise_us_python": 7.068, + "gpu_bwutil_cpp": 0.7040111162635143, + "gpu_bwutil_python": 0.7007809098321037, + "gpu_gap_stddev_us": 4.817516913908882 + } + } + } + }, + "resize_fakeplanar_nchw_expand_cubic_1080p_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "8x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize RGB8 expand cubic fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "resize_fakeplanar_nchw_expand_cubic_1080p_uchar3_advanced[InOutDataType=uchar3][shape=8x1080x1920][resizeType=EXPAND][interpolation=CUBIC][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1230.52367899284, + "gpu_time_us_python": 1248.9145837909, + "gpu_noise_us_cpp": 1.584, + "gpu_noise_us_python": 2.9299999999999997, + "gpu_bwutil_cpp": 0.3900788961730914, + "gpu_bwutil_python": 0.3843356979965984, + "gpu_gap_stddev_us": 2.0807106328431946 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 949.8734154732399, + "gpu_time_us_python": 962.1845487356599, + "gpu_noise_us_cpp": 1.066, + "gpu_noise_us_python": 5.787999999999999, + "gpu_bwutil_cpp": 0.38544510480089944, + "gpu_bwutil_python": 0.3805050786293126, + "gpu_gap_stddev_us": 3.895094711078281 + } + } + } + }, + "resize_fakeplanar_nchw_expand_cubic_1080p_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "8x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize RGBf32 expand cubic fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "resize_fakeplanar_nchw_expand_cubic_1080p_float3_advanced[InOutDataType=float3][shape=8x1080x1920][resizeType=EXPAND][interpolation=CUBIC][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2578.47407318628, + "gpu_time_us_python": 2597.6793199879403, + "gpu_noise_us_cpp": 2.434, + "gpu_noise_us_python": 3.7159999999999997, + "gpu_bwutil_cpp": 0.7446277287238521, + "gpu_bwutil_python": 0.7391230083127941, + "gpu_gap_stddev_us": 2.561065683490094 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2082.20685123734, + "gpu_time_us_python": 2094.5800782839, + "gpu_noise_us_cpp": 2.672, + "gpu_noise_us_python": 3.6959999999999993, + "gpu_bwutil_cpp": 0.7033160212636792, + "gpu_bwutil_python": 0.6991586387739658, + "gpu_gap_stddev_us": 1.4945568881313231 + } + } + } + }, + "resize_fakeplanar_nchw_expand_cubic_1080p_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "8x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize RGBA8 expand cubic fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "resize_fakeplanar_nchw_expand_cubic_1080p_uchar4_advanced[InOutDataType=uchar4][shape=8x1080x1920][resizeType=EXPAND][interpolation=CUBIC][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1539.26549967076, + "gpu_time_us_python": 1559.65178553634, + "gpu_noise_us_cpp": 1.1400000000000001, + "gpu_noise_us_python": 3.1239999999999997, + "gpu_bwutil_cpp": 0.4157837494719135, + "gpu_bwutil_python": 0.41034886346214794, + "gpu_gap_stddev_us": 2.4803360638365843 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1246.3650720506398, + "gpu_time_us_python": 1260.10458604864, + "gpu_noise_us_cpp": 1.4520000000000002, + "gpu_noise_us_python": 4.154, + "gpu_bwutil_cpp": 0.39167762397568995, + "gpu_bwutil_python": 0.3874049957462299, + "gpu_gap_stddev_us": 1.5902422632729893 + } + } + } + }, + "resize_fakeplanar_nchw_expand_cubic_1080p_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "8x1080x1920" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize RGBAf32 expand cubic fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "resize_fakeplanar_nchw_expand_cubic_1080p_float4_advanced[InOutDataType=float4][shape=8x1080x1920][resizeType=EXPAND][interpolation=CUBIC][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3398.1522779206402, + "gpu_time_us_python": 3416.8998280086394, + "gpu_noise_us_cpp": 4.154000000000001, + "gpu_noise_us_python": 4.894, + "gpu_bwutil_cpp": 0.7533514892217006, + "gpu_bwutil_python": 0.74921818287481, + "gpu_gap_stddev_us": 1.3454624008255096 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2764.33779643128, + "gpu_time_us_python": 2780.38009788522, + "gpu_noise_us_cpp": 7.775999999999999, + "gpu_noise_us_python": 7.965999999999999, + "gpu_bwutil_cpp": 0.7063427850070011, + "gpu_bwutil_python": 0.7022644649167186, + "gpu_gap_stddev_us": 2.7802483566390372 + } + } + } + }, + "resize_fakeplanar_nchw_expand_cubic_4k_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "2x2160x3840" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize RGB8 expand cubic fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "resize_fakeplanar_nchw_expand_cubic_4k_uchar3_advanced[InOutDataType=uchar3][shape=2x2160x3840][resizeType=EXPAND][interpolation=CUBIC][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1232.5232959053199, + "gpu_time_us_python": 1249.42119157488, + "gpu_noise_us_cpp": 1.482, + "gpu_noise_us_python": 2.0300000000000002, + "gpu_bwutil_cpp": 0.38944597973368433, + "gpu_bwutil_python": 0.38417877279963164, + "gpu_gap_stddev_us": 0.6361314474868454 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 952.14869264556, + "gpu_time_us_python": 964.4641784447, + "gpu_noise_us_cpp": 0.974, + "gpu_noise_us_python": 4.1899999999999995, + "gpu_bwutil_cpp": 0.38452356351889894, + "gpu_bwutil_python": 0.379611523578449, + "gpu_gap_stddev_us": 3.196257621920611 + } + } + } + }, + "resize_fakeplanar_nchw_expand_cubic_4k_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "2x2160x3840" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize RGBf32 expand cubic fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "resize_fakeplanar_nchw_expand_cubic_4k_float3_advanced[InOutDataType=float3][shape=2x2160x3840][resizeType=EXPAND][interpolation=CUBIC][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2575.9880766024403, + "gpu_time_us_python": 2594.24590327072, + "gpu_noise_us_cpp": 1.9439999999999997, + "gpu_noise_us_python": 3.6800000000000006, + "gpu_bwutil_cpp": 0.745346395209408, + "gpu_bwutil_python": 0.7401004995525773, + "gpu_gap_stddev_us": 1.142445466109426 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2084.6771210349602, + "gpu_time_us_python": 2099.0901337384, + "gpu_noise_us_cpp": 2.134, + "gpu_noise_us_python": 5.4719999999999995, + "gpu_bwutil_cpp": 0.7024821918212747, + "gpu_bwutil_python": 0.697655868044454, + "gpu_gap_stddev_us": 4.153817075098159 + } + } + } + }, + "resize_fakeplanar_nchw_expand_cubic_4k_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "2x2160x3840" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize RGBA8 expand cubic fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "resize_fakeplanar_nchw_expand_cubic_4k_uchar4_advanced[InOutDataType=uchar4][shape=2x2160x3840][resizeType=EXPAND][interpolation=CUBIC][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1515.7488919800999, + "gpu_time_us_python": 1533.28035120722, + "gpu_noise_us_cpp": 1.5339999999999998, + "gpu_noise_us_python": 2.896, + "gpu_bwutil_cpp": 0.42223438421530446, + "gpu_bwutil_python": 0.41740693644705934, + "gpu_gap_stddev_us": 2.165232365518195 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1243.8962616335398, + "gpu_time_us_python": 1257.2501919483198, + "gpu_noise_us_cpp": 1.3, + "gpu_noise_us_python": 11.016, + "gpu_bwutil_cpp": 0.39245607638925695, + "gpu_bwutil_python": 0.3882821917716828, + "gpu_gap_stddev_us": 4.150265534051968 + } + } + } + }, + "resize_fakeplanar_nchw_expand_cubic_4k_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "2x2160x3840" + ], + "resizeType": [ + "EXPAND" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize RGBAf32 expand cubic fake-planar (reformat->interleaved->reformat) (advanced)." + }, + "baselines": { + "resize_fakeplanar_nchw_expand_cubic_4k_float4_advanced[InOutDataType=float4][shape=2x2160x3840][resizeType=EXPAND][interpolation=CUBIC][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3401.43737470776, + "gpu_time_us_python": 3422.34782890138, + "gpu_noise_us_cpp": 3.8880000000000003, + "gpu_noise_us_python": 4.978, + "gpu_bwutil_cpp": 0.7526248127222048, + "gpu_bwutil_python": 0.7480261821927842, + "gpu_gap_stddev_us": 2.388480776386703 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2801.9855476704997, + "gpu_time_us_python": 2817.9614539985205, + "gpu_noise_us_cpp": 8.532000000000002, + "gpu_noise_us_python": 9.75, + "gpu_bwutil_cpp": 0.6968515688944537, + "gpu_bwutil_python": 0.6929021122408613, + "gpu_gap_stddev_us": 1.4203149346509383 + } + } + } + }, + "resize_anisotropic_contract_480p_tensor_rgb8_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "AREA" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize RGB8 tensor anisotropically from 1920x1080 to 864x480." + }, + "baselines": { + "resize_anisotropic_contract_480p_tensor_rgb8_basic[InOutDataType=uchar3][shape=32x1080x1920][resizeType=TARGET_480x864][interpolation=AREA][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 450.0944080288999, + "gpu_time_us_python": 461.07706223172, + "gpu_noise_us_cpp": 1.024, + "gpu_noise_us_python": 2.844, + "gpu_bwutil_cpp": 0.3412634775575147, + "gpu_bwutil_python": 0.333136426006697, + "gpu_gap_stddev_us": 0.7003109171848595 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 330.74704466983997, + "gpu_time_us_python": 341.98616463672, + "gpu_noise_us_cpp": 1.484, + "gpu_noise_us_python": 2.862, + "gpu_bwutil_cpp": 0.35425094967853415, + "gpu_bwutil_python": 0.3426241842088875, + "gpu_gap_stddev_us": 2.1759322433577912 + } + } + } + }, + "resize_anisotropic_contract_480p_varshape_rgb8_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "AREA" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize RGB8 ImageBatchVarShape anisotropically from 1920x1080 to 864x480." + }, + "baselines": { + "resize_anisotropic_contract_480p_varshape_rgb8_basic[InOutDataType=uchar3][shape=32x1080x1920][resizeType=TARGET_480x864][interpolation=AREA][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 752.14042640656, + "gpu_time_us_python": 765.72913590386, + "gpu_noise_us_cpp": 1.6440000000000001, + "gpu_noise_us_python": 2.776, + "gpu_bwutil_cpp": 0.20421847198537776, + "gpu_bwutil_python": 0.20059419087450933, + "gpu_gap_stddev_us": 2.937998109699697 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 663.5525602169, + "gpu_time_us_python": 675.7901194689199, + "gpu_noise_us_cpp": 2.4859999999999998, + "gpu_noise_us_python": 3.5739999999999994, + "gpu_bwutil_cpp": 0.17657791647575677, + "gpu_bwutil_python": 0.17337639561425724, + "gpu_gap_stddev_us": 1.215731966055482 + } + } + } + }, + "resize_anisotropic_expand_4k_tensor_rgb8_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize RGB8 tensor anisotropically from 864x480 to 3840x2160." + }, + "baselines": { + "resize_anisotropic_expand_4k_tensor_rgb8_basic[InOutDataType=uchar3][shape=32x480x864][resizeType=TARGET_2160x3840][interpolation=LINEAR][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2091.89371466634, + "gpu_time_us_python": 2104.7077933446003, + "gpu_noise_us_cpp": 1.668, + "gpu_noise_us_python": 3.854, + "gpu_bwutil_cpp": 0.2569928764627452, + "gpu_bwutil_python": 0.25542835331727265, + "gpu_gap_stddev_us": 1.9370262614705083 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1547.3677465714202, + "gpu_time_us_python": 1557.59144312022, + "gpu_noise_us_cpp": 1.4920000000000002, + "gpu_noise_us_python": 5.794, + "gpu_bwutil_cpp": 0.265029190628205, + "gpu_bwutil_python": 0.26328462625155424, + "gpu_gap_stddev_us": 4.111940695981299 + } + } + } + }, + "resize_anisotropic_expand_4k_varshape_rgb8_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "16x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize RGB8 ImageBatchVarShape anisotropically from 864x480 to 3840x2160." + }, + "baselines": { + "resize_anisotropic_expand_4k_varshape_rgb8_basic[InOutDataType=uchar3][shape=16x480x864][resizeType=TARGET_2160x3840][interpolation=LINEAR][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1237.55313480488, + "gpu_time_us_python": 1252.67842478224, + "gpu_noise_us_cpp": 1.6, + "gpu_noise_us_python": 3.342, + "gpu_bwutil_cpp": 0.21720379746103174, + "gpu_bwutil_python": 0.21458252304731226, + "gpu_gap_stddev_us": 2.4551084663090066 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 803.30614330804, + "gpu_time_us_python": 814.2572291869401, + "gpu_noise_us_cpp": 1.426, + "gpu_noise_us_python": 2.886, + "gpu_bwutil_cpp": 0.2552520501812329, + "gpu_bwutil_python": 0.25181676250297985, + "gpu_gap_stddev_us": 0.39612903338603633 + } + } + } + }, + "resize_anisotropic_contract_480x864_nchw_tensor_area_float3_c3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "36x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "AREA" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize float3 3-channel Tensor (NCHW) anisotropically from 1920x1080 to 864x480 with AREA. Advanced normalized mirror of resize_planar_nchw_contract_area_tensor_float3_basic; batch 36 paired with the interleaved NHWC reference-timing profile." + }, + "baselines": { + "resize_anisotropic_contract_480x864_nchw_tensor_area_float3_c3_advanced[InOutDataType=float3][shape=36x1080x1920][resizeType=TARGET_480x864][interpolation=AREA][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1994.98148415576, + "gpu_time_us_python": 2007.4168533324598, + "gpu_noise_us_cpp": 1.6919999999999997, + "gpu_noise_us_python": 3.434, + "gpu_bwutil_cpp": 0.3464702194251258, + "gpu_bwutil_python": 0.3443240136303517, + "gpu_gap_stddev_us": 2.013104730355583 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1919.3332785432601, + "gpu_time_us_python": 1928.92434068592, + "gpu_noise_us_cpp": 1.6079999999999999, + "gpu_noise_us_python": 2.822, + "gpu_bwutil_cpp": 0.2747179089436861, + "gpu_bwutil_python": 0.2733504322445578, + "gpu_gap_stddev_us": 1.1634391279821337 + } + } + } + }, + "resize_anisotropic_contract_480x864_nchw_varshape_area_float3_c3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "AREA" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize float3 3-channel VarShape (NCHW) anisotropically from 1920x1080 to 864x480 with AREA. Advanced normalized mirror of resize_planar_nchw_contract_area_varshape_float3_advanced; batch 16 scaled conservatively to 16." + }, + "baselines": { + "resize_anisotropic_contract_480x864_nchw_varshape_area_float3_c3_advanced[InOutDataType=float3][shape=16x1080x1920][resizeType=TARGET_480x864][interpolation=AREA][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1010.8413163088601, + "gpu_time_us_python": 1024.64583059446, + "gpu_noise_us_cpp": 1.6799999999999997, + "gpu_noise_us_python": 3.016, + "gpu_bwutil_cpp": 0.3039063031650933, + "gpu_bwutil_python": 0.29981410446628204, + "gpu_gap_stddev_us": 1.8003609796060074 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 912.34358262502, + "gpu_time_us_python": 924.4432855325001, + "gpu_noise_us_cpp": 2.9739999999999998, + "gpu_noise_us_python": 5.497999999999999, + "gpu_bwutil_cpp": 0.25683345961320225, + "gpu_bwutil_python": 0.25346714706260365, + "gpu_gap_stddev_us": 2.5923340153570344 + } + } + } + }, + "resize_anisotropic_contract_480x864_nchwfake_tensor_area_float3_c3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "36x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "AREA" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize float3 3-channel Tensor (NCHW_FAKE) anisotropically from 1920x1080 to 864x480 with AREA. Advanced normalized mirror of resize_fakeplanar_nchw_contract_area_tensor_float3_advanced; batch 36 paired with the interleaved NHWC reference-timing profile." + }, + "baselines": { + "resize_anisotropic_contract_480x864_nchwfake_tensor_area_float3_c3_advanced[InOutDataType=float3][shape=36x1080x1920][resizeType=TARGET_480x864][interpolation=AREA][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2794.55749246804, + "gpu_time_us_python": 2814.1712234856195, + "gpu_noise_us_cpp": 2.036, + "gpu_noise_us_python": 4.340000000000001, + "gpu_bwutil_cpp": 0.7420192696347987, + "gpu_bwutil_python": 0.736846968904058, + "gpu_gap_stddev_us": 2.188041860578102 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2299.89836587812, + "gpu_time_us_python": 2313.67621346468, + "gpu_noise_us_cpp": 2.5340000000000003, + "gpu_noise_us_python": 4.474, + "gpu_bwutil_cpp": 0.6876663684176955, + "gpu_bwutil_python": 0.6835717543739827, + "gpu_gap_stddev_us": 1.705284622092932 + } + } + } + }, + "resize_anisotropic_contract_480x864_nhwc_tensor_area_float3_c3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "36x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "AREA" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize float3 3-channel Tensor (NHWC) anisotropically from 1920x1080 to 864x480 with AREA. Advanced normalized mirror of resize_contract_area_tensor_float3_basic; batch 36 calibrated for approximately 1 ms reference timing." + }, + "baselines": { + "resize_anisotropic_contract_480x864_nhwc_tensor_area_float3_c3_advanced[InOutDataType=float3][shape=36x1080x1920][resizeType=TARGET_480x864][interpolation=AREA][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 830.15629588844, + "gpu_time_us_python": 839.57839335516, + "gpu_noise_us_cpp": 2.206, + "gpu_noise_us_python": 2.4959999999999996, + "gpu_bwutil_cpp": 0.8326150791483073, + "gpu_bwutil_python": 0.8232730833325143, + "gpu_gap_stddev_us": 1.025622084604911 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 648.161409, + "gpu_time_us_python": 653.94822987838, + "gpu_noise_us_cpp": 2.8600000000000003, + "gpu_noise_us_python": 3.082, + "gpu_bwutil_cpp": 0.8133569218260688, + "gpu_bwutil_python": 0.8061639836207763, + "gpu_gap_stddev_us": 1.6489189245005929 + } + } + } + }, + "resize_anisotropic_contract_480x864_nhwc_varshape_area_float3_c3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "AREA" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize float3 3-channel VarShape (NHWC) anisotropically from 1920x1080 to 864x480 with AREA. Advanced normalized mirror of resize_contract_area_varshape_float3_advanced; batch 16 scaled conservatively to 16." + }, + "baselines": { + "resize_anisotropic_contract_480x864_nhwc_varshape_area_float3_c3_advanced[InOutDataType=float3][shape=16x1080x1920][resizeType=TARGET_480x864][interpolation=AREA][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 508.48406509628, + "gpu_time_us_python": 521.46520064626, + "gpu_noise_us_cpp": 1.574, + "gpu_noise_us_python": 2.732, + "gpu_bwutil_cpp": 0.6041495739278073, + "gpu_bwutil_python": 0.5891175349125275, + "gpu_gap_stddev_us": 2.7007184152095856 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 418.45266676604007, + "gpu_time_us_python": 429.41725379438003, + "gpu_noise_us_cpp": 1.5719999999999998, + "gpu_noise_us_python": 4.008, + "gpu_bwutil_cpp": 0.559988621702886, + "gpu_bwutil_python": 0.5456713668172289, + "gpu_gap_stddev_us": 1.3870582501308386 + } + } + } + }, + "resize_anisotropic_contract_480x864_nchw_tensor_cubic_float3_c3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize float3 3-channel Tensor (NCHW) anisotropically from 1920x1080 to 864x480 with CUBIC. Advanced normalized mirror of resize_planar_nchw_contract_cubic_1080p_float3_basic; batch 32 scaled conservatively to 32." + }, + "baselines": { + "resize_anisotropic_contract_480x864_nchw_tensor_cubic_float3_c3_advanced[InOutDataType=float3][shape=32x1080x1920][resizeType=TARGET_480x864][interpolation=CUBIC][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 916.67361248156, + "gpu_time_us_python": 930.1343349270398, + "gpu_noise_us_cpp": 1.4280000000000002, + "gpu_noise_us_python": 2.3240000000000003, + "gpu_bwutil_cpp": 0.670250724400024, + "gpu_bwutil_python": 0.6605551496221359, + "gpu_gap_stddev_us": 2.7641742392904054 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 639.1101090996799, + "gpu_time_us_python": 648.81935164644, + "gpu_noise_us_cpp": 1.6039999999999999, + "gpu_noise_us_python": 3.3560000000000003, + "gpu_bwutil_cpp": 0.7332249271496056, + "gpu_bwutil_python": 0.7222537352974988, + "gpu_gap_stddev_us": 1.2560040131828016 + } + } + } + }, + "resize_anisotropic_contract_480x864_nchwfake_tensor_cubic_float3_c3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize float3 3-channel Tensor (NCHW_FAKE) anisotropically from 1920x1080 to 864x480 with CUBIC. Advanced normalized mirror of resize_fakeplanar_nchw_contract_cubic_1080p_float3_advanced; batch 32 scaled conservatively to 32." + }, + "baselines": { + "resize_anisotropic_contract_480x864_nchwfake_tensor_cubic_float3_c3_advanced[InOutDataType=float3][shape=32x1080x1920][resizeType=TARGET_480x864][interpolation=CUBIC][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2488.24896765222, + "gpu_time_us_python": 2504.48321132842, + "gpu_noise_us_cpp": 2.374, + "gpu_noise_us_python": 4.728, + "gpu_bwutil_cpp": 0.7407667511853402, + "gpu_bwutil_python": 0.7359664234403844, + "gpu_gap_stddev_us": 1.3731354579272903 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2078.08221728344, + "gpu_time_us_python": 2092.0690199740798, + "gpu_noise_us_cpp": 5.675999999999999, + "gpu_noise_us_python": 6.278, + "gpu_bwutil_cpp": 0.6765161479871125, + "gpu_bwutil_python": 0.6719891095060203, + "gpu_gap_stddev_us": 2.6965910291066373 + } + } + } + }, + "resize_anisotropic_contract_480x864_nhwc_tensor_cubic_float3_c3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize float3 3-channel Tensor (NHWC) anisotropically from 1920x1080 to 864x480 with CUBIC. Advanced normalized mirror of resize_contract_cubic_1080p_float3_basic; batch 32 scaled conservatively to 32." + }, + "baselines": { + "resize_anisotropic_contract_480x864_nhwc_tensor_cubic_float3_c3_advanced[InOutDataType=float3][shape=32x1080x1920][resizeType=TARGET_480x864][interpolation=CUBIC][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 748.27607636122, + "gpu_time_us_python": 757.61597427682, + "gpu_noise_us_cpp": 2.13, + "gpu_noise_us_python": 2.952, + "gpu_bwutil_cpp": 0.8210877380711825, + "gpu_bwutil_python": 0.810969060053264, + "gpu_gap_stddev_us": 1.839730065804893 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 613.27631494054, + "gpu_time_us_python": 621.4191240535399, + "gpu_noise_us_cpp": 4.178, + "gpu_noise_us_python": 4.958, + "gpu_bwutil_cpp": 0.7641250505760098, + "gpu_bwutil_python": 0.7541347645784582, + "gpu_gap_stddev_us": 1.9745013720274396 + } + } + } + }, + "resize_anisotropic_contract_480x864_nchw_tensor_linear_float3_c3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize float3 3-channel Tensor (NCHW) anisotropically from 1920x1080 to 864x480 with LINEAR. Advanced normalized mirror of resize_planar_nchw_contract_linear_1080p_float3_basic; batch 32 scaled conservatively to 32." + }, + "baselines": { + "resize_anisotropic_contract_480x864_nchw_tensor_linear_float3_c3_advanced[InOutDataType=float3][shape=32x1080x1920][resizeType=TARGET_480x864][interpolation=LINEAR][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 649.590490112, + "gpu_time_us_python": 660.84341773382, + "gpu_noise_us_cpp": 1.456, + "gpu_noise_us_python": 3.4800000000000004, + "gpu_bwutil_cpp": 0.9458267234264213, + "gpu_bwutil_python": 0.9297309869504767, + "gpu_gap_stddev_us": 2.5543151423014483 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 489.96855949326, + "gpu_time_us_python": 498.28218977628, + "gpu_noise_us_cpp": 1.8039999999999998, + "gpu_noise_us_python": 2.3979999999999997, + "gpu_bwutil_cpp": 0.9564112528188169, + "gpu_bwutil_python": 0.9404551461043326, + "gpu_gap_stddev_us": 0.38317168985738104 + } + } + } + }, + "resize_anisotropic_contract_480x864_nchwfake_tensor_linear_float3_c3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize float3 3-channel Tensor (NCHW_FAKE) anisotropically from 1920x1080 to 864x480 with LINEAR. Advanced normalized mirror of resize_fakeplanar_nchw_contract_linear_1080p_float3_advanced; batch 32 scaled conservatively to 32." + }, + "baselines": { + "resize_anisotropic_contract_480x864_nchwfake_tensor_linear_float3_c3_advanced[InOutDataType=float3][shape=32x1080x1920][resizeType=TARGET_480x864][interpolation=LINEAR][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2393.25606339696, + "gpu_time_us_python": 2411.57255674886, + "gpu_noise_us_cpp": 1.6239999999999999, + "gpu_noise_us_python": 4.336, + "gpu_bwutil_cpp": 0.7701695856332611, + "gpu_bwutil_python": 0.7643205451634405, + "gpu_gap_stddev_us": 1.6800689147792838 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1968.0705537182798, + "gpu_time_us_python": 1981.6328947714203, + "gpu_noise_us_cpp": 2.0279999999999996, + "gpu_noise_us_python": 4.124, + "gpu_bwutil_cpp": 0.7143207810563232, + "gpu_bwutil_python": 0.7094314150912113, + "gpu_gap_stddev_us": 0.9644297692105466 + } + } + } + }, + "resize_anisotropic_contract_480x864_nhwc_tensor_linear_float3_c3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize float3 3-channel Tensor (NHWC) anisotropically from 1920x1080 to 864x480 with LINEAR. Advanced normalized mirror of resize_contract_linear_1080p_float3_basic; batch 32 scaled conservatively to 32." + }, + "baselines": { + "resize_anisotropic_contract_480x864_nhwc_tensor_linear_float3_c3_advanced[InOutDataType=float3][shape=32x1080x1920][resizeType=TARGET_480x864][interpolation=LINEAR][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 643.86067491568, + "gpu_time_us_python": 654.73330386002, + "gpu_noise_us_cpp": 1.36, + "gpu_noise_us_python": 4.216, + "gpu_bwutil_cpp": 0.9542438445856704, + "gpu_bwutil_python": 0.9383975209382271, + "gpu_gap_stddev_us": 0.3550462112102643 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 487.90013151288, + "gpu_time_us_python": 495.81117484083995, + "gpu_noise_us_cpp": 1.9440000000000002, + "gpu_noise_us_python": 2.178, + "gpu_bwutil_cpp": 0.9604666698617571, + "gpu_bwutil_python": 0.9451410893889705, + "gpu_gap_stddev_us": 0.26114621869767213 + } + } + } + }, + "resize_anisotropic_expand_2160x3840_nchw_tensor_cubic_float3_c3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "8x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize float3 3-channel Tensor (NCHW) anisotropically from 864x480 to 3840x2160 with CUBIC. Advanced normalized mirror of resize_planar_nchw_expand_cubic_1080p_float3_basic; batch 8 scaled conservatively to 8." + }, + "baselines": { + "resize_anisotropic_expand_2160x3840_nchw_tensor_cubic_float3_c3_advanced[InOutDataType=float3][shape=8x480x864][resizeType=TARGET_2160x3840][interpolation=CUBIC][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2204.2031480554597, + "gpu_time_us_python": 2213.6410903863402, + "gpu_noise_us_cpp": 1.714, + "gpu_noise_us_python": 3.2099999999999995, + "gpu_bwutil_cpp": 0.24389887077783085, + "gpu_bwutil_python": 0.24285940306300832, + "gpu_gap_stddev_us": 2.4624946101486063 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1835.64051615274, + "gpu_time_us_python": 1846.58318143016, + "gpu_noise_us_cpp": 1.468, + "gpu_noise_us_python": 4.884, + "gpu_bwutil_cpp": 0.22341054939410104, + "gpu_bwutil_python": 0.22208037051375848, + "gpu_gap_stddev_us": 2.4219330974492004 + } + } + } + }, + "resize_anisotropic_expand_2160x3840_nchwfake_tensor_cubic_float3_c3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "8x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize float3 3-channel Tensor (NCHW_FAKE) anisotropically from 864x480 to 3840x2160 with CUBIC. Advanced normalized mirror of resize_fakeplanar_nchw_expand_cubic_1080p_float3_advanced; batch 8 scaled conservatively to 8." + }, + "baselines": { + "resize_anisotropic_expand_2160x3840_nchwfake_tensor_cubic_float3_c3_advanced[InOutDataType=float3][shape=8x480x864][resizeType=TARGET_2160x3840][interpolation=CUBIC][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2693.40628347086, + "gpu_time_us_python": 2710.1602210793003, + "gpu_noise_us_cpp": 3.8299999999999996, + "gpu_noise_us_python": 4.358, + "gpu_bwutil_cpp": 0.5987960708526876, + "gpu_bwutil_python": 0.5950943524155277, + "gpu_gap_stddev_us": 1.3213828996731376 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2132.24362319784, + "gpu_time_us_python": 2144.332648682, + "gpu_noise_us_cpp": 1.89, + "gpu_noise_us_python": 6.256, + "gpu_bwutil_cpp": 0.5769328286448255, + "gpu_bwutil_python": 0.5736771438666747, + "gpu_gap_stddev_us": 3.3585970874504927 + } + } + } + }, + "resize_anisotropic_expand_2160x3840_nhwc_tensor_cubic_float3_c3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "8x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize float3 3-channel Tensor (NHWC) anisotropically from 864x480 to 3840x2160 with CUBIC. Advanced normalized mirror of resize_expand_cubic_1080p_float3_basic; batch 8 scaled conservatively to 8." + }, + "baselines": { + "resize_anisotropic_expand_2160x3840_nhwc_tensor_cubic_float3_c3_advanced[InOutDataType=float3][shape=8x480x864][resizeType=TARGET_2160x3840][interpolation=CUBIC][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1459.8069611478, + "gpu_time_us_python": 1470.05891700026, + "gpu_noise_us_cpp": 2.71, + "gpu_noise_us_python": 3.662, + "gpu_bwutil_cpp": 0.3682690311998964, + "gpu_bwutil_python": 0.3657007904917461, + "gpu_gap_stddev_us": 1.5444939886664109 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1178.4135546817, + "gpu_time_us_python": 1188.99354274828, + "gpu_noise_us_cpp": 3.838, + "gpu_noise_us_python": 3.246, + "gpu_bwutil_cpp": 0.3480000612074027, + "gpu_bwutil_python": 0.34490221890827677, + "gpu_gap_stddev_us": 2.49225974434508 + } + } + } + }, + "resize_anisotropic_expand_2160x3840_nchw_tensor_linear_float3_c3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "8x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize float3 3-channel Tensor (NCHW) anisotropically from 864x480 to 3840x2160 with LINEAR. Advanced normalized mirror of resize_planar_nchw_expand_linear_1080p_float3_basic; batch 8 scaled conservatively to 8." + }, + "baselines": { + "resize_anisotropic_expand_2160x3840_nchw_tensor_linear_float3_c3_advanced[InOutDataType=float3][shape=8x480x864][resizeType=TARGET_2160x3840][interpolation=LINEAR][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 766.1062895625801, + "gpu_time_us_python": 776.65032407588, + "gpu_noise_us_cpp": 1.762, + "gpu_noise_us_python": 2.45, + "gpu_bwutil_cpp": 0.7017321898040401, + "gpu_bwutil_python": 0.6922067705410415, + "gpu_gap_stddev_us": 0.9919089934643198 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 744.1030047000601, + "gpu_time_us_python": 754.07303997998, + "gpu_noise_us_cpp": 0.9059999999999999, + "gpu_noise_us_python": 2.8, + "gpu_bwutil_cpp": 0.5511296641678344, + "gpu_bwutil_python": 0.5438235882884922, + "gpu_gap_stddev_us": 1.797937799410573 + } + } + } + }, + "resize_anisotropic_expand_2160x3840_nchwfake_tensor_linear_float3_c3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "8x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize float3 3-channel Tensor (NCHW_FAKE) anisotropically from 864x480 to 3840x2160 with LINEAR. Advanced normalized mirror of resize_fakeplanar_nchw_expand_linear_1080p_float3_advanced; batch 8 scaled conservatively to 8." + }, + "baselines": { + "resize_anisotropic_expand_2160x3840_nchwfake_tensor_linear_float3_c3_advanced[InOutDataType=float3][shape=8x480x864][resizeType=TARGET_2160x3840][interpolation=LINEAR][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1940.5700715709002, + "gpu_time_us_python": 1957.62080047276, + "gpu_noise_us_cpp": 2.2640000000000002, + "gpu_noise_us_python": 2.9379999999999997, + "gpu_bwutil_cpp": 0.831096596436845, + "gpu_bwutil_python": 0.8238581692933622, + "gpu_gap_stddev_us": 0.6773900015041346 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1498.13707934622, + "gpu_time_us_python": 1510.0965390721801, + "gpu_noise_us_cpp": 1.95, + "gpu_noise_us_python": 3.558, + "gpu_bwutil_cpp": 0.8210878200160115, + "gpu_bwutil_python": 0.814586717495531, + "gpu_gap_stddev_us": 2.3704710947047616 + } + } + } + }, + "resize_anisotropic_expand_2160x3840_nhwc_tensor_linear_float3_c3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "8x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize float3 3-channel Tensor (NHWC) anisotropically from 864x480 to 3840x2160 with LINEAR. Advanced normalized mirror of resize_expand_linear_1080p_float3_basic; batch 8 scaled conservatively to 8." + }, + "baselines": { + "resize_anisotropic_expand_2160x3840_nhwc_tensor_linear_float3_c3_advanced[InOutDataType=float3][shape=8x480x864][resizeType=TARGET_2160x3840][interpolation=LINEAR][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 706.0581236514801, + "gpu_time_us_python": 717.5131160783001, + "gpu_noise_us_cpp": 1.238, + "gpu_noise_us_python": 3.848, + "gpu_bwutil_cpp": 0.7614122541208375, + "gpu_bwutil_python": 0.7492597987345921, + "gpu_gap_stddev_us": 1.6311642850892456 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 535.20222290336, + "gpu_time_us_python": 545.2449236692199, + "gpu_noise_us_cpp": 2.7239999999999998, + "gpu_noise_us_python": 3.3480000000000003, + "gpu_bwutil_cpp": 0.7661295759090059, + "gpu_bwutil_python": 0.7520274445287582, + "gpu_gap_stddev_us": 2.5848268343671665 + } + } + } + }, + "resize_anisotropic_contract_480x864_nhwc_tensor_area_float32_c1_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "AREA" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize float32 1-channel Tensor (NHWC) anisotropically from 1920x1080 to 864x480 with AREA. Advanced normalized mirror of resize_contract_area_tensor_scalar_advanced; batch 128 scaled conservatively to 128." + }, + "baselines": { + "resize_anisotropic_contract_480x864_nhwc_tensor_area_float32_c1_advanced[InOutDataType=float32][shape=128x1080x1920][resizeType=TARGET_480x864][interpolation=AREA][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1136.42021699526, + "gpu_time_us_python": 1146.94714370812, + "gpu_noise_us_cpp": 1.01, + "gpu_noise_us_python": 2.942, + "gpu_bwutil_cpp": 0.7208608724940979, + "gpu_bwutil_python": 0.7142442074200911, + "gpu_gap_stddev_us": 1.9304668222387917 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 844.77521975242, + "gpu_time_us_python": 854.05220667174, + "gpu_noise_us_cpp": 1.3980000000000001, + "gpu_noise_us_python": 3.3600000000000003, + "gpu_bwutil_cpp": 0.7396340818305192, + "gpu_bwutil_python": 0.7315954933623582, + "gpu_gap_stddev_us": 2.381104971289791 + } + } + } + }, + "resize_anisotropic_contract_480x864_nhwc_varshape_area_float32_c1_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "AREA" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize float32 1-channel VarShape (NHWC) anisotropically from 1920x1080 to 864x480 with AREA. Advanced normalized mirror of resize_contract_area_varshape_scalar_advanced; batch 128 scaled conservatively to 128." + }, + "baselines": { + "resize_anisotropic_contract_480x864_nhwc_varshape_area_float32_c1_advanced[InOutDataType=float32][shape=128x1080x1920][resizeType=TARGET_480x864][interpolation=AREA][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2238.9632331473, + "gpu_time_us_python": 2258.0159556758, + "gpu_noise_us_cpp": 6.942, + "gpu_noise_us_python": 6.506, + "gpu_bwutil_cpp": 0.36588468713410094, + "gpu_bwutil_python": 0.36279714671321794, + "gpu_gap_stddev_us": 3.6610011966675726 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1785.77607122312, + "gpu_time_us_python": 1802.1955597780398, + "gpu_noise_us_cpp": 3.838, + "gpu_noise_us_python": 6.984, + "gpu_bwutil_cpp": 0.34990041701720187, + "gpu_bwutil_python": 0.34670920445202136, + "gpu_gap_stddev_us": 4.320953323958714 + } + } + } + }, + "resize_anisotropic_contract_480x864_nhwc_tensor_cubic_float32_c1_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize float32 1-channel Tensor (NHWC) anisotropically from 1920x1080 to 864x480 with CUBIC. Advanced normalized mirror of resize_contract_cubic_1080p_scalar_advanced; batch 64 scaled conservatively to 64." + }, + "baselines": { + "resize_anisotropic_contract_480x864_nhwc_tensor_cubic_float32_c1_advanced[InOutDataType=float32][shape=64x1080x1920][resizeType=TARGET_480x864][interpolation=CUBIC][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 529.62390845566, + "gpu_time_us_python": 538.85473753598, + "gpu_noise_us_cpp": 1.876, + "gpu_noise_us_python": 2.494, + "gpu_bwutil_cpp": 0.7733791607179347, + "gpu_bwutil_python": 0.7601315919504467, + "gpu_gap_stddev_us": 0.7700536241578417 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 421.98550567420006, + "gpu_time_us_python": 430.71138261934004, + "gpu_noise_us_cpp": 1.3780000000000001, + "gpu_noise_us_python": 3.47, + "gpu_bwutil_cpp": 0.7404145811723414, + "gpu_bwutil_python": 0.7253671516848408, + "gpu_gap_stddev_us": 2.224190516303029 + } + } + } + }, + "resize_anisotropic_contract_480x864_nhwc_tensor_linear_float32_c1_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "144x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize float32 1-channel Tensor (NHWC) anisotropically from 1920x1080 to 864x480 with LINEAR. Advanced normalized mirror of resize_contract_linear_1080p_scalar_advanced; batch 144 calibrated for approximately 1 ms reference timing." + }, + "baselines": { + "resize_anisotropic_contract_480x864_nhwc_tensor_linear_float32_c1_advanced[InOutDataType=float32][shape=144x1080x1920][resizeType=TARGET_480x864][interpolation=LINEAR][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 963.4516704540401, + "gpu_time_us_python": 973.3672809719801, + "gpu_noise_us_cpp": 1.908, + "gpu_noise_us_python": 2.2960000000000003, + "gpu_bwutil_cpp": 0.9565609262573357, + "gpu_bwutil_python": 0.9468182437749638, + "gpu_gap_stddev_us": 1.2226288803486474 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 725.8203612517, + "gpu_time_us_python": 735.1144679613999, + "gpu_noise_us_cpp": 1.3719999999999999, + "gpu_noise_us_python": 4.39, + "gpu_bwutil_cpp": 0.9684435345308107, + "gpu_bwutil_python": 0.9562042044563956, + "gpu_gap_stddev_us": 1.6001243769118805 + } + } + } + }, + "resize_anisotropic_expand_2160x3840_nhwc_tensor_cubic_float32_c1_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "32x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize float32 1-channel Tensor (NHWC) anisotropically from 864x480 to 3840x2160 with CUBIC. Advanced normalized mirror of resize_expand_cubic_1080p_scalar_advanced; batch 32 scaled conservatively to 32." + }, + "baselines": { + "resize_anisotropic_expand_2160x3840_nhwc_tensor_cubic_float32_c1_advanced[InOutDataType=float32][shape=32x480x864][resizeType=TARGET_2160x3840][interpolation=CUBIC][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2934.4594962945403, + "gpu_time_us_python": 2946.8244685682603, + "gpu_noise_us_cpp": 2.4619999999999997, + "gpu_noise_us_python": 3.8240000000000003, + "gpu_bwutil_cpp": 0.2442709391755306, + "gpu_bwutil_python": 0.24324584728571624, + "gpu_gap_stddev_us": 2.07540158760619 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2444.2338228225394, + "gpu_time_us_python": 2455.91611978844, + "gpu_noise_us_cpp": 1.528, + "gpu_noise_us_python": 3.9859999999999998, + "gpu_bwutil_cpp": 0.22371159161067702, + "gpu_bwutil_python": 0.22264628288892102, + "gpu_gap_stddev_us": 2.526065921662834 + } + } + } + }, + "resize_anisotropic_expand_2160x3840_nhwc_tensor_linear_float32_c1_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "32x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize float32 1-channel Tensor (NHWC) anisotropically from 864x480 to 3840x2160 with LINEAR. Advanced normalized mirror of resize_expand_advanced; batch 32 scaled conservatively to 32." + }, + "baselines": { + "resize_anisotropic_expand_2160x3840_nhwc_tensor_linear_float32_c1_advanced[InOutDataType=float32][shape=32x480x864][resizeType=TARGET_2160x3840][interpolation=LINEAR][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1017.0318842369, + "gpu_time_us_python": 1028.87319943954, + "gpu_noise_us_cpp": 1.8920000000000001, + "gpu_noise_us_python": 2.71, + "gpu_bwutil_cpp": 0.7047976949132767, + "gpu_bwutil_python": 0.6966875019777674, + "gpu_gap_stddev_us": 2.319191189211368 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 988.8210387003401, + "gpu_time_us_python": 999.08620441112, + "gpu_noise_us_cpp": 1.418, + "gpu_noise_us_python": 3.4159999999999995, + "gpu_bwutil_cpp": 0.5529803887461158, + "gpu_bwutil_python": 0.5472788251158629, + "gpu_gap_stddev_us": 2.5894465025991944 + } + } + } + }, + "resize_anisotropic_expand_2160x3840_nhwc_varshape_linear_float32_c1_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "32x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize float32 1-channel VarShape (NHWC) anisotropically from 864x480 to 3840x2160 with LINEAR. Advanced normalized mirror of resize_expand_linear_varshape_advanced; batch 32 scaled conservatively to 32." + }, + "baselines": { + "resize_anisotropic_expand_2160x3840_nhwc_varshape_linear_float32_c1_advanced[InOutDataType=float32][shape=32x480x864][resizeType=TARGET_2160x3840][interpolation=LINEAR][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1390.19197874668, + "gpu_time_us_python": 1403.76276244592, + "gpu_noise_us_cpp": 1.7, + "gpu_noise_us_python": 4.667999999999999, + "gpu_bwutil_cpp": 0.5156147469582142, + "gpu_bwutil_python": 0.510629992708622, + "gpu_gap_stddev_us": 0.8511609675208878 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1224.75773178448, + "gpu_time_us_python": 1237.43889500942, + "gpu_noise_us_cpp": 1.634, + "gpu_noise_us_python": 4.952, + "gpu_bwutil_cpp": 0.44644929593430704, + "gpu_bwutil_python": 0.44186132930930666, + "gpu_gap_stddev_us": 1.6803397742820898 + } + } + } + }, + "resize_anisotropic_contract_480x864_nchw_tensor_area_float4_c4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "AREA" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize float4 4-channel Tensor (NCHW) anisotropically from 1920x1080 to 864x480 with AREA. Advanced normalized mirror of resize_planar_nchw_contract_area_tensor_float4_advanced; batch 16 scaled conservatively to 16." + }, + "baselines": { + "resize_anisotropic_contract_480x864_nchw_tensor_area_float4_c4_advanced[InOutDataType=float4][shape=16x1080x1920][resizeType=TARGET_480x864][interpolation=AREA][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1185.99445921066, + "gpu_time_us_python": 1197.9873277874801, + "gpu_noise_us_cpp": 1.474, + "gpu_noise_us_python": 2.932, + "gpu_bwutil_cpp": 0.34536528312131576, + "gpu_bwutil_python": 0.3419084886165059, + "gpu_gap_stddev_us": 1.961287507562216 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1136.8827719060198, + "gpu_time_us_python": 1147.24744070914, + "gpu_noise_us_cpp": 1.124, + "gpu_noise_us_python": 3.2340000000000004, + "gpu_bwutil_cpp": 0.2748348628363126, + "gpu_bwutil_python": 0.2723483287249734, + "gpu_gap_stddev_us": 1.5122192420507121 + } + } + } + }, + "resize_anisotropic_contract_480x864_nchw_varshape_area_float4_c4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "AREA" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize float4 4-channel VarShape (NCHW) anisotropically from 1920x1080 to 864x480 with AREA. Advanced normalized mirror of resize_planar_nchw_contract_area_varshape_float4_advanced; batch 16 scaled conservatively to 16." + }, + "baselines": { + "resize_anisotropic_contract_480x864_nchw_varshape_area_float4_c4_advanced[InOutDataType=float4][shape=16x1080x1920][resizeType=TARGET_480x864][interpolation=AREA][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1324.6726676700998, + "gpu_time_us_python": 1337.8145903765403, + "gpu_noise_us_cpp": 1.884, + "gpu_noise_us_python": 3.96, + "gpu_bwutil_cpp": 0.30920908400578095, + "gpu_bwutil_python": 0.30617218036547245, + "gpu_gap_stddev_us": 1.0259962852239575 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1194.36063563596, + "gpu_time_us_python": 1206.14240022098, + "gpu_noise_us_cpp": 1.6599999999999997, + "gpu_noise_us_python": 3.9620000000000006, + "gpu_bwutil_cpp": 0.2615869320592338, + "gpu_bwutil_python": 0.25903082991181675, + "gpu_gap_stddev_us": 0.7321085637009608 + } + } + } + }, + "resize_anisotropic_contract_480x864_nchwfake_tensor_area_float4_c4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "AREA" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize float4 4-channel Tensor (NCHW_FAKE) anisotropically from 1920x1080 to 864x480 with AREA. Advanced normalized mirror of resize_fakeplanar_nchw_contract_area_tensor_float4_advanced; batch 16 scaled conservatively to 16." + }, + "baselines": { + "resize_anisotropic_contract_480x864_nchwfake_tensor_area_float4_c4_advanced[InOutDataType=float4][shape=16x1080x1920][resizeType=TARGET_480x864][interpolation=AREA][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1969.6282338878598, + "gpu_time_us_python": 1987.3747209044202, + "gpu_noise_us_cpp": 2.072, + "gpu_noise_us_python": 2.79, + "gpu_bwutil_cpp": 0.6238821713042405, + "gpu_bwutil_python": 0.618311399499879, + "gpu_gap_stddev_us": 0.6479023850830279 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1634.38310772518, + "gpu_time_us_python": 1649.4234954055403, + "gpu_noise_us_cpp": 3.304, + "gpu_noise_us_python": 4.834, + "gpu_bwutil_cpp": 0.5734403973685639, + "gpu_bwutil_python": 0.5682126182014604, + "gpu_gap_stddev_us": 2.6098377423961754 + } + } + } + }, + "resize_anisotropic_contract_480x864_nhwc_tensor_area_float4_c4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "AREA" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize float4 4-channel Tensor (NHWC) anisotropically from 1920x1080 to 864x480 with AREA. Advanced normalized mirror of resize_contract_area_tensor_float4_advanced; batch 16 scaled conservatively to 16." + }, + "baselines": { + "resize_anisotropic_contract_480x864_nhwc_tensor_area_float4_c4_advanced[InOutDataType=float4][shape=16x1080x1920][resizeType=TARGET_480x864][interpolation=AREA][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 494.97554801872, + "gpu_time_us_python": 505.06982212113996, + "gpu_noise_us_cpp": 1.634, + "gpu_noise_us_python": 2.19, + "gpu_bwutil_cpp": 0.8275157870320478, + "gpu_bwutil_python": 0.810983649158014, + "gpu_gap_stddev_us": 1.535736472130475 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 364.80447439415997, + "gpu_time_us_python": 372.82499718462003, + "gpu_noise_us_cpp": 1.786, + "gpu_noise_us_python": 2.7299999999999995, + "gpu_bwutil_cpp": 0.8563712966396894, + "gpu_bwutil_python": 0.8379537706700914, + "gpu_gap_stddev_us": 0.5954924926661435 + } + } + } + }, + "resize_anisotropic_contract_480x864_nhwc_varshape_area_float4_c4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "AREA" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize float4 4-channel VarShape (NHWC) anisotropically from 1920x1080 to 864x480 with AREA. Advanced normalized mirror of resize_contract_area_varshape_float4_advanced; batch 16 scaled conservatively to 16." + }, + "baselines": { + "resize_anisotropic_contract_480x864_nhwc_varshape_area_float4_c4_advanced[InOutDataType=float4][shape=16x1080x1920][resizeType=TARGET_480x864][interpolation=AREA][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 586.33144824426, + "gpu_time_us_python": 598.03415678102, + "gpu_noise_us_cpp": 1.73, + "gpu_noise_us_python": 2.0699999999999994, + "gpu_bwutil_cpp": 0.6985825020604238, + "gpu_bwutil_python": 0.6849119501607169, + "gpu_gap_stddev_us": 1.2564095080550681 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 437.26677365203994, + "gpu_time_us_python": 449.99035525352, + "gpu_noise_us_cpp": 1.746, + "gpu_noise_us_python": 3.714, + "gpu_bwutil_cpp": 0.7144571334652917, + "gpu_bwutil_python": 0.6942566148367783, + "gpu_gap_stddev_us": 1.480869388472238 + } + } + } + }, + "resize_anisotropic_contract_480x864_nchw_tensor_cubic_float4_c4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize float4 4-channel Tensor (NCHW) anisotropically from 1920x1080 to 864x480 with CUBIC. Advanced normalized mirror of resize_planar_nchw_contract_cubic_1080p_float4_advanced; batch 32 paired with the interleaved NHWC reference-timing profile." + }, + "baselines": { + "resize_anisotropic_contract_480x864_nchw_tensor_cubic_float4_c4_advanced[InOutDataType=float4][shape=32x1080x1920][resizeType=TARGET_480x864][interpolation=CUBIC][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1220.7966624620199, + "gpu_time_us_python": 1231.82930646338, + "gpu_noise_us_cpp": 1.4140000000000001, + "gpu_noise_us_python": 2.4899999999999998, + "gpu_bwutil_cpp": 0.6710392574129049, + "gpu_bwutil_python": 0.6650274528091737, + "gpu_gap_stddev_us": 2.9370452214331766 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 843.6937632502801, + "gpu_time_us_python": 853.49757413984, + "gpu_noise_us_cpp": 1.4540000000000002, + "gpu_noise_us_python": 3.1120000000000005, + "gpu_bwutil_cpp": 0.7405707857632384, + "gpu_bwutil_python": 0.7320657201181031, + "gpu_gap_stddev_us": 1.7605839743350027 + } + } + } + }, + "resize_anisotropic_contract_480x864_nchwfake_tensor_cubic_float4_c4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize float4 4-channel Tensor (NCHW_FAKE) anisotropically from 1920x1080 to 864x480 with CUBIC. Advanced normalized mirror of resize_fakeplanar_nchw_contract_cubic_1080p_float4_advanced; batch 32 paired with the interleaved NHWC reference-timing profile." + }, + "baselines": { + "resize_anisotropic_contract_480x864_nchwfake_tensor_cubic_float4_c4_advanced[InOutDataType=float4][shape=32x1080x1920][resizeType=TARGET_480x864][interpolation=CUBIC][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3926.1706228095004, + "gpu_time_us_python": 3944.27203312512, + "gpu_noise_us_cpp": 2.8200000000000003, + "gpu_noise_us_python": 4.0920000000000005, + "gpu_bwutil_cpp": 0.6259609661282151, + "gpu_bwutil_python": 0.6230881537250311, + "gpu_gap_stddev_us": 0.914148396576131 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3259.0153960437997, + "gpu_time_us_python": 3274.9884673526803, + "gpu_noise_us_cpp": 3.5799999999999996, + "gpu_noise_us_python": 7.048, + "gpu_bwutil_cpp": 0.5751560501475943, + "gpu_bwutil_python": 0.5723511722775474, + "gpu_gap_stddev_us": 1.922913470448911 + } + } + } + }, + "resize_anisotropic_contract_480x864_nhwc_tensor_cubic_float4_c4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize float4 4-channel Tensor (NHWC) anisotropically from 1920x1080 to 864x480 with CUBIC. Advanced normalized mirror of resize_contract_cubic_1080p_float4_advanced; batch 32 calibrated for approximately 1 ms reference timing." + }, + "baselines": { + "resize_anisotropic_contract_480x864_nhwc_tensor_cubic_float4_c4_advanced[InOutDataType=float4][shape=32x1080x1920][resizeType=TARGET_480x864][interpolation=CUBIC][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 971.3925973850801, + "gpu_time_us_python": 982.62343828296, + "gpu_noise_us_cpp": 2.132, + "gpu_noise_us_python": 3.6940000000000004, + "gpu_bwutil_cpp": 0.8433256235987538, + "gpu_bwutil_python": 0.8336880784362546, + "gpu_gap_stddev_us": 1.183878616419046 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 724.46608851156, + "gpu_time_us_python": 733.44119482854, + "gpu_noise_us_cpp": 1.6119999999999997, + "gpu_noise_us_python": 3.5159999999999996, + "gpu_bwutil_cpp": 0.8624477476208938, + "gpu_bwutil_python": 0.8519028881413518, + "gpu_gap_stddev_us": 2.1157527563293606 + } + } + } + }, + "resize_anisotropic_contract_480x864_nchw_tensor_linear_float4_c4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize float4 4-channel Tensor (NCHW) anisotropically from 1920x1080 to 864x480 with LINEAR. Advanced normalized mirror of resize_planar_nchw_contract_linear_1080p_float4_advanced; batch 32 scaled conservatively to 32." + }, + "baselines": { + "resize_anisotropic_contract_480x864_nchw_tensor_linear_float4_c4_advanced[InOutDataType=float4][shape=32x1080x1920][resizeType=TARGET_480x864][interpolation=LINEAR][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 859.06341952972, + "gpu_time_us_python": 868.91881391184, + "gpu_noise_us_cpp": 1.6099999999999999, + "gpu_noise_us_python": 2.55, + "gpu_bwutil_cpp": 0.9535967836608826, + "gpu_bwutil_python": 0.9427842310368575, + "gpu_gap_stddev_us": 1.8651625429370975 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 647.28991148588, + "gpu_time_us_python": 655.7994950688001, + "gpu_noise_us_cpp": 1.534, + "gpu_noise_us_python": 3.2019999999999995, + "gpu_bwutil_cpp": 0.9652777319671738, + "gpu_bwutil_python": 0.9527561240773028, + "gpu_gap_stddev_us": 1.2423179635453931 + } + } + } + }, + "resize_anisotropic_contract_480x864_nchwfake_tensor_linear_float4_c4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize float4 4-channel Tensor (NCHW_FAKE) anisotropically from 1920x1080 to 864x480 with LINEAR. Advanced normalized mirror of resize_fakeplanar_nchw_contract_linear_1080p_float4_advanced; batch 32 scaled conservatively to 32." + }, + "baselines": { + "resize_anisotropic_contract_480x864_nchwfake_tensor_linear_float4_c4_advanced[InOutDataType=float4][shape=32x1080x1920][resizeType=TARGET_480x864][interpolation=LINEAR][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3794.04484248708, + "gpu_time_us_python": 3812.6190347619204, + "gpu_noise_us_cpp": 2.902, + "gpu_noise_us_python": 4.37, + "gpu_bwutil_cpp": 0.6477602190002703, + "gpu_bwutil_python": 0.6446052979057987, + "gpu_gap_stddev_us": 3.6991856524600157 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3172.18357460402, + "gpu_time_us_python": 3186.9343528264203, + "gpu_noise_us_cpp": 3.936, + "gpu_noise_us_python": 4.540000000000001, + "gpu_bwutil_cpp": 0.5909000302108621, + "gpu_bwutil_python": 0.5881655207530989, + "gpu_gap_stddev_us": 2.568603854041504 + } + } + } + }, + "resize_anisotropic_contract_480x864_nhwc_tensor_linear_float4_c4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize float4 4-channel Tensor (NHWC) anisotropically from 1920x1080 to 864x480 with LINEAR. Advanced normalized mirror of resize_contract_linear_1080p_float4_advanced; batch 32 scaled conservatively to 32." + }, + "baselines": { + "resize_anisotropic_contract_480x864_nhwc_tensor_linear_float4_c4_advanced[InOutDataType=float4][shape=32x1080x1920][resizeType=TARGET_480x864][interpolation=LINEAR][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 833.3327291501, + "gpu_time_us_python": 843.6267476570401, + "gpu_noise_us_cpp": 1.02, + "gpu_noise_us_python": 3.1480000000000006, + "gpu_bwutil_cpp": 0.9830409795760394, + "gpu_bwutil_python": 0.9710473983289661, + "gpu_gap_stddev_us": 0.8740161932795872 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 626.8112430076799, + "gpu_time_us_python": 636.7424584693799, + "gpu_noise_us_cpp": 1.432, + "gpu_noise_us_python": 3.4099999999999993, + "gpu_bwutil_cpp": 0.9968140599373221, + "gpu_bwutil_python": 0.9812765968641826, + "gpu_gap_stddev_us": 1.8281705336875889 + } + } + } + }, + "resize_anisotropic_expand_2160x3840_nchw_tensor_cubic_float4_c4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "8x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize float4 4-channel Tensor (NCHW) anisotropically from 864x480 to 3840x2160 with CUBIC. Advanced normalized mirror of resize_planar_nchw_expand_cubic_1080p_float4_advanced; batch 8 scaled conservatively to 8." + }, + "baselines": { + "resize_anisotropic_expand_2160x3840_nchw_tensor_cubic_float4_c4_advanced[InOutDataType=float4][shape=8x480x864][resizeType=TARGET_2160x3840][interpolation=CUBIC][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2933.87590658152, + "gpu_time_us_python": 2946.8406061466603, + "gpu_noise_us_cpp": 2.528, + "gpu_noise_us_python": 3.822, + "gpu_bwutil_cpp": 0.2443194533103032, + "gpu_bwutil_python": 0.24324447079664865, + "gpu_gap_stddev_us": 3.1354230305551694 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2445.07646988594, + "gpu_time_us_python": 2453.9932126864396, + "gpu_noise_us_cpp": 2.464, + "gpu_noise_us_python": 2.45, + "gpu_bwutil_cpp": 0.22363422816180303, + "gpu_bwutil_python": 0.2228219732607733, + "gpu_gap_stddev_us": 1.3738500974368717 + } + } + } + }, + "resize_anisotropic_expand_2160x3840_nchwfake_tensor_cubic_float4_c4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "8x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize float4 4-channel Tensor (NCHW_FAKE) anisotropically from 864x480 to 3840x2160 with CUBIC. Advanced normalized mirror of resize_fakeplanar_nchw_expand_cubic_1080p_float4_advanced; batch 8 scaled conservatively to 8." + }, + "baselines": { + "resize_anisotropic_expand_2160x3840_nchwfake_tensor_cubic_float4_c4_advanced[InOutDataType=float4][shape=8x480x864][resizeType=TARGET_2160x3840][interpolation=CUBIC][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3193.9234027379202, + "gpu_time_us_python": 3214.28346450506, + "gpu_noise_us_cpp": 3.5060000000000002, + "gpu_noise_us_python": 5.382, + "gpu_bwutil_cpp": 0.6732794460112405, + "gpu_bwutil_python": 0.6690147472522241, + "gpu_gap_stddev_us": 3.0155457528806 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2640.9731054776, + "gpu_time_us_python": 2653.5576346465, + "gpu_noise_us_cpp": 7.008, + "gpu_noise_us_python": 7.42, + "gpu_bwutil_cpp": 0.6210617442367514, + "gpu_bwutil_python": 0.6181169669835755, + "gpu_gap_stddev_us": 0.7792700178844607 + } + } + } + }, + "resize_anisotropic_expand_2160x3840_nhwc_tensor_cubic_float4_c4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "8x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize float4 4-channel Tensor (NHWC) anisotropically from 864x480 to 3840x2160 with CUBIC. Advanced normalized mirror of resize_expand_cubic_1080p_float4_advanced; batch 8 scaled conservatively to 8." + }, + "baselines": { + "resize_anisotropic_expand_2160x3840_nhwc_tensor_cubic_float4_c4_advanced[InOutDataType=float4][shape=8x480x864][resizeType=TARGET_2160x3840][interpolation=CUBIC][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1497.20635705768, + "gpu_time_us_python": 1507.4681802071002, + "gpu_noise_us_cpp": 1.9420000000000002, + "gpu_noise_us_python": 4.776, + "gpu_bwutil_cpp": 0.4787605921188024, + "gpu_bwutil_python": 0.47550105703176887, + "gpu_gap_stddev_us": 1.301071677753948 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1325.30251473254, + "gpu_time_us_python": 1335.35246002854, + "gpu_noise_us_cpp": 3.804, + "gpu_noise_us_python": 2.7279999999999998, + "gpu_bwutil_cpp": 0.41257944763214455, + "gpu_bwutil_python": 0.4094813521174093, + "gpu_gap_stddev_us": 2.5622970777819596 + } + } + } + }, + "resize_anisotropic_expand_2160x3840_nchw_tensor_linear_float4_c4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "8x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize float4 4-channel Tensor (NCHW) anisotropically from 864x480 to 3840x2160 with LINEAR. Advanced normalized mirror of resize_planar_nchw_expand_linear_1080p_float4_advanced; batch 8 scaled conservatively to 8." + }, + "baselines": { + "resize_anisotropic_expand_2160x3840_nchw_tensor_linear_float4_c4_advanced[InOutDataType=float4][shape=8x480x864][resizeType=TARGET_2160x3840][interpolation=LINEAR][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1017.3227901439599, + "gpu_time_us_python": 1028.5498353497999, + "gpu_noise_us_cpp": 1.6320000000000001, + "gpu_noise_us_python": 2.384, + "gpu_bwutil_cpp": 0.7045962531751633, + "gpu_bwutil_python": 0.6969074803644351, + "gpu_gap_stddev_us": 2.086648177210999 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 988.2457507655402, + "gpu_time_us_python": 999.97376160832, + "gpu_noise_us_cpp": 2.8519999999999994, + "gpu_noise_us_python": 4.6899999999999995, + "gpu_bwutil_cpp": 0.5532950397566193, + "gpu_bwutil_python": 0.5468102231207912, + "gpu_gap_stddev_us": 1.5051387504870593 + } + } + } + }, + "resize_anisotropic_expand_2160x3840_nchwfake_tensor_linear_float4_c4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "8x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize float4 4-channel Tensor (NCHW_FAKE) anisotropically from 864x480 to 3840x2160 with LINEAR. Advanced normalized mirror of resize_fakeplanar_nchw_expand_linear_1080p_float4_advanced; batch 8 scaled conservatively to 8." + }, + "baselines": { + "resize_anisotropic_expand_2160x3840_nchwfake_tensor_linear_float4_c4_advanced[InOutDataType=float4][shape=8x480x864][resizeType=TARGET_2160x3840][interpolation=LINEAR][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2537.6271623553203, + "gpu_time_us_python": 2553.96012087694, + "gpu_noise_us_cpp": 4.7379999999999995, + "gpu_noise_us_python": 5.254, + "gpu_bwutil_cpp": 0.8474059076302514, + "gpu_bwutil_python": 0.8419869300741917, + "gpu_gap_stddev_us": 1.4570708582360343 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1966.1626517772397, + "gpu_time_us_python": 1979.0645926017, + "gpu_noise_us_cpp": 4.918000000000001, + "gpu_noise_us_python": 8.33, + "gpu_bwutil_cpp": 0.8341811920858486, + "gpu_bwutil_python": 0.8287434462006589, + "gpu_gap_stddev_us": 2.419666107593475 + } + } + } + }, + "resize_anisotropic_expand_2160x3840_nhwc_tensor_linear_float4_c4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "8x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize float4 4-channel Tensor (NHWC) anisotropically from 864x480 to 3840x2160 with LINEAR. Advanced normalized mirror of resize_expand_linear_1080p_float4_advanced; batch 8 scaled conservatively to 8." + }, + "baselines": { + "resize_anisotropic_expand_2160x3840_nhwc_tensor_linear_float4_c4_advanced[InOutDataType=float4][shape=8x480x864][resizeType=TARGET_2160x3840][interpolation=LINEAR][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 841.5426257875, + "gpu_time_us_python": 852.1840301164, + "gpu_noise_us_cpp": 3.062, + "gpu_noise_us_python": 4.464, + "gpu_bwutil_cpp": 0.8517692353256106, + "gpu_bwutil_python": 0.8411349006327512, + "gpu_gap_stddev_us": 1.5190686326321985 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 663.1175219874201, + "gpu_time_us_python": 669.66520861872, + "gpu_noise_us_cpp": 2.84, + "gpu_noise_us_python": 2.9179999999999997, + "gpu_bwutil_cpp": 0.8244575686390492, + "gpu_bwutil_python": 0.8163968272519311, + "gpu_gap_stddev_us": 0.507573473350835 + } + } + } + }, + "resize_anisotropic_contract_480x864_nchw_tensor_area_uchar3_c3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "AREA" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize uchar3 3-channel Tensor (NCHW) anisotropically from 1920x1080 to 864x480 with AREA. Advanced normalized mirror of resize_planar_nchw_contract_area_tensor_uchar3_basic; batch 32 scaled conservatively to 32." + }, + "baselines": { + "resize_anisotropic_contract_480x864_nchw_tensor_area_uchar3_c3_advanced[InOutDataType=uchar3][shape=32x1080x1920][resizeType=TARGET_480x864][interpolation=AREA][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 657.88049952372, + "gpu_time_us_python": 668.01361072276, + "gpu_noise_us_cpp": 1.152, + "gpu_noise_us_python": 2.4379999999999997, + "gpu_bwutil_cpp": 0.23347947365829466, + "gpu_bwutil_python": 0.22993780436399575, + "gpu_gap_stddev_us": 0.9204771510336557 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 495.84734243878, + "gpu_time_us_python": 504.71290257738, + "gpu_noise_us_cpp": 1.518, + "gpu_noise_us_python": 3.4619999999999997, + "gpu_bwutil_cpp": 0.23630488450693937, + "gpu_bwutil_python": 0.23214987663967684, + "gpu_gap_stddev_us": 0.8762801424235319 + } + } + } + }, + "resize_anisotropic_contract_480x864_nchw_varshape_area_uchar3_c3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "AREA" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize uchar3 3-channel VarShape (NCHW) anisotropically from 1920x1080 to 864x480 with AREA. Advanced normalized mirror of resize_planar_nchw_contract_area_varshape_uchar3_basic; batch 32 scaled conservatively to 32." + }, + "baselines": { + "resize_anisotropic_contract_480x864_nchw_varshape_area_uchar3_c3_advanced[InOutDataType=uchar3][shape=32x1080x1920][resizeType=TARGET_480x864][interpolation=AREA][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1243.58708612914, + "gpu_time_us_python": 1259.31550381524, + "gpu_noise_us_cpp": 5.359999999999999, + "gpu_noise_us_python": 7.208000000000001, + "gpu_bwutil_cpp": 0.12351425429731955, + "gpu_bwutil_python": 0.12197138324791137, + "gpu_gap_stddev_us": 2.726009956736236 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1154.15548546396, + "gpu_time_us_python": 1165.3796001411, + "gpu_noise_us_cpp": 7.481999999999999, + "gpu_noise_us_python": 8.6, + "gpu_bwutil_cpp": 0.10153197706954524, + "gpu_bwutil_python": 0.1005547782476093, + "gpu_gap_stddev_us": 1.051941838576333 + } + } + } + }, + "resize_anisotropic_contract_480x864_nchwfake_tensor_area_uchar3_c3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "AREA" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize uchar3 3-channel Tensor (NCHW_FAKE) anisotropically from 1920x1080 to 864x480 with AREA. Advanced normalized mirror of resize_fakeplanar_nchw_contract_area_tensor_uchar3_advanced; batch 32 scaled conservatively to 32." + }, + "baselines": { + "resize_anisotropic_contract_480x864_nchwfake_tensor_area_uchar3_c3_advanced[InOutDataType=uchar3][shape=32x1080x1920][resizeType=TARGET_480x864][interpolation=AREA][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1335.13063210038, + "gpu_time_us_python": 1352.6263011086799, + "gpu_noise_us_cpp": 1.184, + "gpu_noise_us_python": 2.536, + "gpu_bwutil_cpp": 0.3451366694405789, + "gpu_bwutil_python": 0.3406722578242717, + "gpu_gap_stddev_us": 1.4535637133050816 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1015.03533600348, + "gpu_time_us_python": 1029.49668497542, + "gpu_noise_us_cpp": 1.2440000000000002, + "gpu_noise_us_python": 2.852, + "gpu_bwutil_cpp": 0.3462645338076354, + "gpu_bwutil_python": 0.3414061085199303, + "gpu_gap_stddev_us": 2.678272487773715 + } + } + } + }, + "resize_anisotropic_expand_2160x3840_nchw_tensor_cubic_uchar3_c3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "8x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize uchar3 3-channel Tensor (NCHW) anisotropically from 864x480 to 3840x2160 with CUBIC. Advanced normalized mirror of resize_planar_nchw_expand_cubic_1080p_uchar3_basic; batch 8 scaled conservatively to 8." + }, + "baselines": { + "resize_anisotropic_expand_2160x3840_nchw_tensor_cubic_uchar3_c3_advanced[InOutDataType=uchar3][shape=8x480x864][resizeType=TARGET_2160x3840][interpolation=CUBIC][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2014.85151681898, + "gpu_time_us_python": 2026.4536869140798, + "gpu_noise_us_cpp": 2.302, + "gpu_noise_us_python": 3.314, + "gpu_bwutil_cpp": 0.06670503396599692, + "gpu_bwutil_python": 0.06632316476009238, + "gpu_gap_stddev_us": 2.4186697088012297 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1666.0171695574402, + "gpu_time_us_python": 1677.2241660023599, + "gpu_noise_us_cpp": 1.5000000000000002, + "gpu_noise_us_python": 3.466, + "gpu_bwutil_cpp": 0.06153855155667294, + "gpu_bwutil_python": 0.061127401416212604, + "gpu_gap_stddev_us": 2.3257710738007753 + } + } + } + }, + "resize_anisotropic_expand_2160x3840_nchwfake_tensor_cubic_uchar3_c3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "8x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize uchar3 3-channel Tensor (NCHW_FAKE) anisotropically from 864x480 to 3840x2160 with CUBIC. Advanced normalized mirror of resize_fakeplanar_nchw_expand_cubic_1080p_uchar3_advanced; batch 8 scaled conservatively to 8." + }, + "baselines": { + "resize_anisotropic_expand_2160x3840_nchwfake_tensor_cubic_uchar3_c3_advanced[InOutDataType=uchar3][shape=8x480x864][resizeType=TARGET_2160x3840][interpolation=CUBIC][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1794.48755684878, + "gpu_time_us_python": 1813.26440186432, + "gpu_noise_us_cpp": 1.764, + "gpu_noise_us_python": 2.832, + "gpu_bwutil_cpp": 0.22468904688541652, + "gpu_bwutil_python": 0.2223632734249073, + "gpu_gap_stddev_us": 2.034125070968968 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1498.4353177010798, + "gpu_time_us_python": 1513.0178397119798, + "gpu_noise_us_cpp": 2.4859999999999998, + "gpu_noise_us_python": 2.472, + "gpu_bwutil_cpp": 0.2052533432249677, + "gpu_bwutil_python": 0.20327119747130046, + "gpu_gap_stddev_us": 2.2914028244339373 + } + } + } + }, + "resize_anisotropic_expand_2160x3840_nhwc_tensor_cubic_uchar3_c3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "8x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize uchar3 3-channel Tensor (NHWC) anisotropically from 864x480 to 3840x2160 with CUBIC. Advanced normalized mirror of resize_expand_cubic_1080p_uchar3_basic; batch 8 scaled conservatively to 8." + }, + "baselines": { + "resize_anisotropic_expand_2160x3840_nhwc_tensor_cubic_uchar3_c3_advanced[InOutDataType=uchar3][shape=8x480x864][resizeType=TARGET_2160x3840][interpolation=CUBIC][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1358.9608388095198, + "gpu_time_us_python": 1371.13159629218, + "gpu_noise_us_cpp": 1.302, + "gpu_noise_us_python": 3.864, + "gpu_bwutil_cpp": 0.09889980344992591, + "gpu_bwutil_python": 0.09802198689732018, + "gpu_gap_stddev_us": 2.5991171871750907 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1140.54178766564, + "gpu_time_us_python": 1149.88647536828, + "gpu_noise_us_cpp": 0.9339999999999999, + "gpu_noise_us_python": 2.6439999999999997, + "gpu_bwutil_cpp": 0.0898907475564544, + "gpu_bwutil_python": 0.08916050324177421, + "gpu_gap_stddev_us": 0.7688748174296698 + } + } + } + }, + "resize_anisotropic_expand_2160x3840_nchw_tensor_linear_uchar3_c3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize uchar3 3-channel Tensor (NCHW) anisotropically from 864x480 to 3840x2160 with LINEAR. Advanced normalized mirror of resize_planar_nchw_expand_linear_1080p_uchar3_basic; batch 32 scaled conservatively to 32." + }, + "baselines": { + "resize_anisotropic_expand_2160x3840_nchw_tensor_linear_uchar3_c3_advanced[InOutDataType=uchar3][shape=32x480x864][resizeType=TARGET_2160x3840][interpolation=LINEAR][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3554.37035896402, + "gpu_time_us_python": 3567.6018091274004, + "gpu_noise_us_cpp": 2.398, + "gpu_noise_us_python": 4.978, + "gpu_bwutil_cpp": 0.15125091180140693, + "gpu_bwutil_python": 0.1506899612807368, + "gpu_gap_stddev_us": 0.9732875087011809 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2820.5124346704806, + "gpu_time_us_python": 2832.8199519653, + "gpu_noise_us_cpp": 1.844, + "gpu_noise_us_python": 4.178, + "gpu_bwutil_cpp": 0.14540036006927887, + "gpu_bwutil_python": 0.1447666926666023, + "gpu_gap_stddev_us": 3.2349098990948773 + } + } + } + }, + "resize_anisotropic_expand_2160x3840_nchwfake_tensor_linear_uchar3_c3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize uchar3 3-channel Tensor (NCHW_FAKE) anisotropically from 864x480 to 3840x2160 with LINEAR. Advanced normalized mirror of resize_fakeplanar_nchw_expand_linear_1080p_uchar3_advanced; batch 32 scaled conservatively to 32." + }, + "baselines": { + "resize_anisotropic_expand_2160x3840_nchwfake_tensor_linear_uchar3_c3_advanced[InOutDataType=uchar3][shape=32x480x864][resizeType=TARGET_2160x3840][interpolation=LINEAR][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3781.6895714392203, + "gpu_time_us_python": 3803.7131649074604, + "gpu_noise_us_cpp": 1.366, + "gpu_noise_us_python": 4.646, + "gpu_bwutil_cpp": 0.42647709638286974, + "gpu_bwutil_python": 0.4240072184738894, + "gpu_gap_stddev_us": 3.2631969818541 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2933.7084853060396, + "gpu_time_us_python": 2947.79456492158, + "gpu_noise_us_cpp": 1.6560000000000001, + "gpu_noise_us_python": 3.6079999999999997, + "gpu_bwutil_cpp": 0.41933049437044756, + "gpu_bwutil_python": 0.4173269426159095, + "gpu_gap_stddev_us": 2.281009231203469 + } + } + } + }, + "resize_anisotropic_contract_480x864_nchw_tensor_area_uchar4_c4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "AREA" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize uchar4 4-channel Tensor (NCHW) anisotropically from 1920x1080 to 864x480 with AREA. Advanced normalized mirror of resize_planar_nchw_contract_area_tensor_uchar4_advanced; batch 32 scaled conservatively to 32." + }, + "baselines": { + "resize_anisotropic_contract_480x864_nchw_tensor_area_uchar4_c4_advanced[InOutDataType=uchar4][shape=32x1080x1920][resizeType=TARGET_480x864][interpolation=AREA][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 868.77853267162, + "gpu_time_us_python": 880.44298958538, + "gpu_noise_us_cpp": 1.32, + "gpu_noise_us_python": 2.5759999999999996, + "gpu_bwutil_cpp": 0.2357345514610011, + "gpu_bwutil_python": 0.23261384302410176, + "gpu_gap_stddev_us": 1.731002483897278 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 656.03095850126, + "gpu_time_us_python": 665.43194092782, + "gpu_noise_us_cpp": 1.332, + "gpu_noise_us_python": 3.0740000000000007, + "gpu_bwutil_cpp": 0.23814286439936033, + "gpu_bwutil_python": 0.23477883781755304, + "gpu_gap_stddev_us": 0.4590200083190784 + } + } + } + }, + "resize_anisotropic_contract_480x864_nchwfake_tensor_area_uchar4_c4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "AREA" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize uchar4 4-channel Tensor (NCHW_FAKE) anisotropically from 1920x1080 to 864x480 with AREA. Advanced normalized mirror of resize_fakeplanar_nchw_contract_area_tensor_uchar4_advanced; batch 32 scaled conservatively to 32." + }, + "baselines": { + "resize_anisotropic_contract_480x864_nchwfake_tensor_area_uchar4_c4_advanced[InOutDataType=uchar4][shape=32x1080x1920][resizeType=TARGET_480x864][interpolation=AREA][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1743.3159098028602, + "gpu_time_us_python": 1760.99536091798, + "gpu_noise_us_cpp": 1.44, + "gpu_noise_us_python": 3.418, + "gpu_bwutil_cpp": 0.3524334962157506, + "gpu_bwutil_python": 0.3488960728237512, + "gpu_gap_stddev_us": 1.0931693436978194 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1284.9263182990198, + "gpu_time_us_python": 1299.90871763204, + "gpu_noise_us_cpp": 1.136, + "gpu_noise_us_python": 3.596, + "gpu_bwutil_cpp": 0.3647112678687046, + "gpu_bwutil_python": 0.3605014460059179, + "gpu_gap_stddev_us": 2.446922449351933 + } + } + } + }, + "resize_anisotropic_contract_480x864_nhwc_tensor_area_uchar4_c4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "AREA" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize uchar4 4-channel Tensor (NHWC) anisotropically from 1920x1080 to 864x480 with AREA. Advanced normalized mirror of resize_contract_area_tensor_uchar4_advanced; batch 32 scaled conservatively to 32." + }, + "baselines": { + "resize_anisotropic_contract_480x864_nhwc_tensor_area_uchar4_c4_advanced[InOutDataType=uchar4][shape=32x1080x1920][resizeType=TARGET_480x864][interpolation=AREA][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 581.0620006200801, + "gpu_time_us_python": 591.90180170726, + "gpu_noise_us_cpp": 1.1199999999999999, + "gpu_noise_us_python": 2.652, + "gpu_bwutil_cpp": 0.3524588211263745, + "gpu_bwutil_python": 0.3460066707413987, + "gpu_gap_stddev_us": 2.1311133786691903 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 384.50354733774003, + "gpu_time_us_python": 393.70000948076, + "gpu_noise_us_cpp": 1.1500000000000001, + "gpu_noise_us_python": 2.684, + "gpu_bwutil_cpp": 0.40628610457904085, + "gpu_bwutil_python": 0.39679085791151564, + "gpu_gap_stddev_us": 0.6999269170200404 + } + } + } + }, + "resize_anisotropic_contract_480x864_nhwc_varshape_area_uchar4_c4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "AREA" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize uchar4 4-channel VarShape (NHWC) anisotropically from 1920x1080 to 864x480 with AREA. Advanced normalized mirror of resize_contract_area_varshape_uchar4_advanced; batch 32 scaled conservatively to 32." + }, + "baselines": { + "resize_anisotropic_contract_480x864_nhwc_varshape_area_uchar4_c4_advanced[InOutDataType=uchar4][shape=32x1080x1920][resizeType=TARGET_480x864][interpolation=AREA][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 868.0747679173801, + "gpu_time_us_python": 881.4607423247598, + "gpu_noise_us_cpp": 2.1580000000000004, + "gpu_noise_us_python": 2.836, + "gpu_bwutil_cpp": 0.23592482225357242, + "gpu_bwutil_python": 0.2323420327309306, + "gpu_gap_stddev_us": 1.311022688275119 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 519.09111476986, + "gpu_time_us_python": 532.55255040114, + "gpu_noise_us_cpp": 2.166, + "gpu_noise_us_python": 3.932, + "gpu_bwutil_cpp": 0.3009466305535052, + "gpu_bwutil_python": 0.2933206075732153, + "gpu_gap_stddev_us": 5.128266676131258 + } + } + } + }, + "resize_anisotropic_expand_2160x3840_nchw_tensor_cubic_uchar4_c4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "8x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize uchar4 4-channel Tensor (NCHW) anisotropically from 864x480 to 3840x2160 with CUBIC. Advanced normalized mirror of resize_planar_nchw_expand_cubic_1080p_uchar4_advanced; batch 8 scaled conservatively to 8." + }, + "baselines": { + "resize_anisotropic_expand_2160x3840_nchw_tensor_cubic_uchar4_c4_advanced[InOutDataType=uchar4][shape=8x480x864][resizeType=TARGET_2160x3840][interpolation=CUBIC][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2682.0563332926604, + "gpu_time_us_python": 2693.60641017704, + "gpu_noise_us_cpp": 2.2600000000000002, + "gpu_noise_us_python": 4.252000000000001, + "gpu_bwutil_cpp": 0.06681476924974492, + "gpu_bwutil_python": 0.06652835122003134, + "gpu_gap_stddev_us": 2.446788769417803 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2217.46947469916, + "gpu_time_us_python": 2228.39572177858, + "gpu_noise_us_cpp": 2.392, + "gpu_noise_us_python": 3.81, + "gpu_bwutil_cpp": 0.06164663369605482, + "gpu_bwutil_python": 0.06134428450411929, + "gpu_gap_stddev_us": 2.107037666406837 + } + } + } + }, + "resize_anisotropic_expand_2160x3840_nchwfake_tensor_cubic_uchar4_c4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "8x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize uchar4 4-channel Tensor (NCHW_FAKE) anisotropically from 864x480 to 3840x2160 with CUBIC. Advanced normalized mirror of resize_fakeplanar_nchw_expand_cubic_1080p_uchar4_advanced; batch 8 scaled conservatively to 8." + }, + "baselines": { + "resize_anisotropic_expand_2160x3840_nchwfake_tensor_cubic_uchar4_c4_advanced[InOutDataType=uchar4][shape=8x480x864][resizeType=TARGET_2160x3840][interpolation=CUBIC][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2283.09362368144, + "gpu_time_us_python": 2300.81903737614, + "gpu_noise_us_cpp": 1.49, + "gpu_noise_us_python": 2.752, + "gpu_bwutil_cpp": 0.2354706491568571, + "gpu_bwutil_python": 0.23365623510728914, + "gpu_gap_stddev_us": 1.872512358709726 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1851.73529280798, + "gpu_time_us_python": 1864.239459073, + "gpu_noise_us_cpp": 1.284, + "gpu_noise_us_python": 6.118, + "gpu_bwutil_cpp": 0.2214572494031466, + "gpu_bwutil_python": 0.21996591962752818, + "gpu_gap_stddev_us": 6.161617462763741 + } + } + } + }, + "resize_anisotropic_expand_2160x3840_nhwc_tensor_cubic_uchar4_c4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "8x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize uchar4 4-channel Tensor (NHWC) anisotropically from 864x480 to 3840x2160 with CUBIC. Advanced normalized mirror of resize_expand_cubic_1080p_uchar4_advanced; batch 8 scaled conservatively to 8." + }, + "baselines": { + "resize_anisotropic_expand_2160x3840_nhwc_tensor_cubic_uchar4_c4_advanced[InOutDataType=uchar4][shape=8x480x864][resizeType=TARGET_2160x3840][interpolation=CUBIC][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1709.2035688288001, + "gpu_time_us_python": 1719.6741985429799, + "gpu_noise_us_cpp": 1.748, + "gpu_noise_us_python": 3.034, + "gpu_bwutil_cpp": 0.10484457957724873, + "gpu_bwutil_python": 0.10420609702931802, + "gpu_gap_stddev_us": 2.1153241101225393 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1408.2929004771, + "gpu_time_us_python": 1417.8537677986599, + "gpu_noise_us_cpp": 1.6219999999999999, + "gpu_noise_us_python": 3.1079999999999997, + "gpu_bwutil_cpp": 0.09706712698215514, + "gpu_bwutil_python": 0.09641145676822675, + "gpu_gap_stddev_us": 1.0092456724362642 + } + } + } + }, + "resize_anisotropic_expand_2160x3840_nchw_tensor_linear_uchar4_c4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "32x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize uchar4 4-channel Tensor (NCHW) anisotropically from 864x480 to 3840x2160 with LINEAR. Advanced normalized mirror of resize_planar_nchw_expand_linear_1080p_uchar4_advanced; batch 32 scaled conservatively to 32." + }, + "baselines": { + "resize_anisotropic_expand_2160x3840_nchw_tensor_linear_uchar4_c4_advanced[InOutDataType=uchar4][shape=32x480x864][resizeType=TARGET_2160x3840][interpolation=LINEAR][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4736.30140232586, + "gpu_time_us_python": 4751.6420814226, + "gpu_noise_us_cpp": 1.818, + "gpu_noise_us_python": 5.364, + "gpu_bwutil_cpp": 0.1513422355779715, + "gpu_bwutil_python": 0.15085354324770126, + "gpu_gap_stddev_us": 3.856853595205665 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3756.87684084212, + "gpu_time_us_python": 3769.7079729906, + "gpu_noise_us_cpp": 2.2680000000000002, + "gpu_noise_us_python": 5.112, + "gpu_bwutil_cpp": 0.1455482681217745, + "gpu_bwutil_python": 0.1450532437386085, + "gpu_gap_stddev_us": 2.4282096556017607 + } + } + } + }, + "resize_anisotropic_expand_2160x3840_nchwfake_tensor_linear_uchar4_c4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "32x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize uchar4 4-channel Tensor (NCHW_FAKE) anisotropically from 864x480 to 3840x2160 with LINEAR. Advanced normalized mirror of resize_fakeplanar_nchw_expand_linear_1080p_uchar4_advanced; batch 32 scaled conservatively to 32." + }, + "baselines": { + "resize_anisotropic_expand_2160x3840_nchwfake_tensor_linear_uchar4_c4_advanced[InOutDataType=uchar4][shape=32x480x864][resizeType=TARGET_2160x3840][interpolation=LINEAR][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4813.942188482941, + "gpu_time_us_python": 4834.83962095694, + "gpu_noise_us_cpp": 2.08, + "gpu_noise_us_python": 4.308, + "gpu_bwutil_cpp": 0.4467029755924904, + "gpu_bwutil_python": 0.4447722279063081, + "gpu_gap_stddev_us": 2.557727157019434 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3633.6064229001395, + "gpu_time_us_python": 3647.07924512104, + "gpu_noise_us_cpp": 2.246, + "gpu_noise_us_python": 4.098, + "gpu_bwutil_cpp": 0.45141775630726544, + "gpu_bwutil_python": 0.4497504392499125, + "gpu_gap_stddev_us": 1.154995376533986 + } + } + } + }, + "resize_anisotropic_expand_2160x3840_nhwc_tensor_linear_uchar4_c4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "32x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize uchar4 4-channel Tensor (NHWC) anisotropically from 864x480 to 3840x2160 with LINEAR. Advanced normalized mirror of resize_expand_linear_1080p_uchar4_advanced; batch 32 scaled conservatively to 32." + }, + "baselines": { + "resize_anisotropic_expand_2160x3840_nhwc_tensor_linear_uchar4_c4_advanced[InOutDataType=uchar4][shape=32x480x864][resizeType=TARGET_2160x3840][interpolation=LINEAR][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2565.17640619854, + "gpu_time_us_python": 2576.92246965772, + "gpu_noise_us_cpp": 2.364, + "gpu_noise_us_python": 3.5260000000000007, + "gpu_bwutil_cpp": 0.2794356043249053, + "gpu_bwutil_python": 0.27816213044843663, + "gpu_gap_stddev_us": 2.7764957689451784 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1906.0353954733, + "gpu_time_us_python": 1916.44777041762, + "gpu_noise_us_cpp": 1.8960000000000001, + "gpu_noise_us_python": 3.0960000000000005, + "gpu_bwutil_cpp": 0.2868782767972543, + "gpu_bwutil_python": 0.2853152956287247, + "gpu_gap_stddev_us": 2.1516410949355205 + } + } + } + }, + "resize_anisotropic_contract_480x864_nhwc_tensor_area_uint8_c1_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "AREA" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize uint8 1-channel Tensor (NHWC) anisotropically from 1920x1080 to 864x480 with AREA. Advanced normalized mirror of resize_contract_area_tensor_scalar_advanced; batch 128 scaled conservatively to 128." + }, + "baselines": { + "resize_anisotropic_contract_480x864_nhwc_tensor_area_uint8_c1_advanced[InOutDataType=uint8][shape=128x1080x1920][resizeType=TARGET_480x864][interpolation=AREA][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 868.3220472119799, + "gpu_time_us_python": 879.1319312489201, + "gpu_noise_us_cpp": 1.176, + "gpu_noise_us_python": 3.6700000000000004, + "gpu_bwutil_cpp": 0.2358584979561308, + "gpu_bwutil_python": 0.23295933740779895, + "gpu_gap_stddev_us": 1.6929295413297274 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 655.23199357752, + "gpu_time_us_python": 665.00697132204, + "gpu_noise_us_cpp": 2.354, + "gpu_noise_us_python": 4.728, + "gpu_bwutil_cpp": 0.23842878034390425, + "gpu_bwutil_python": 0.23493146135010035, + "gpu_gap_stddev_us": 1.3965838850799017 + } + } + } + }, + "resize_anisotropic_contract_480x864_nhwc_varshape_area_uint8_c1_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "resizeType": [ + "TARGET_480x864" + ], + "interpolation": [ + "AREA" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize uint8 1-channel VarShape (NHWC) anisotropically from 1920x1080 to 864x480 with AREA. Advanced normalized mirror of resize_contract_area_varshape_scalar_advanced; batch 128 scaled conservatively to 128." + }, + "baselines": { + "resize_anisotropic_contract_480x864_nhwc_varshape_area_uint8_c1_advanced[InOutDataType=uint8][shape=128x1080x1920][resizeType=TARGET_480x864][interpolation=AREA][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1336.10180229272, + "gpu_time_us_python": 1354.2811415973601, + "gpu_noise_us_cpp": 4.664, + "gpu_noise_us_python": 6.728, + "gpu_bwutil_cpp": 0.15328293749639618, + "gpu_bwutil_python": 0.15122583563008324, + "gpu_gap_stddev_us": 1.9337733666329464 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1100.2218510532198, + "gpu_time_us_python": 1115.515823149, + "gpu_noise_us_cpp": 4.492, + "gpu_noise_us_python": 6.74, + "gpu_bwutil_cpp": 0.14198682158178663, + "gpu_bwutil_python": 0.14003557783031276, + "gpu_gap_stddev_us": 3.439907905920827 + } + } + } + }, + "resize_anisotropic_expand_2160x3840_nhwc_tensor_cubic_uint8_c1_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "32x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "CUBIC" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize uint8 1-channel Tensor (NHWC) anisotropically from 864x480 to 3840x2160 with CUBIC. Advanced normalized mirror of resize_expand_cubic_1080p_scalar_advanced; batch 32 scaled conservatively to 32." + }, + "baselines": { + "resize_anisotropic_expand_2160x3840_nhwc_tensor_cubic_uint8_c1_advanced[InOutDataType=uint8][shape=32x480x864][resizeType=TARGET_2160x3840][interpolation=CUBIC][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2682.40926168618, + "gpu_time_us_python": 2694.04722411522, + "gpu_noise_us_cpp": 2.06, + "gpu_noise_us_python": 2.9859999999999998, + "gpu_bwutil_cpp": 0.06680598025010455, + "gpu_bwutil_python": 0.06651744667905234, + "gpu_gap_stddev_us": 1.724347439526022 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2217.0177085317605, + "gpu_time_us_python": 2228.5327442034604, + "gpu_noise_us_cpp": 1.598, + "gpu_noise_us_python": 6.316000000000001, + "gpu_bwutil_cpp": 0.0616590355725861, + "gpu_bwutil_python": 0.06134080824576954, + "gpu_gap_stddev_us": 1.167415010243577 + } + } + } + }, + "resize_anisotropic_expand_2160x3840_nhwc_tensor_linear_uint8_c1_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "32x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize uint8 1-channel Tensor (NHWC) anisotropically from 864x480 to 3840x2160 with LINEAR. Advanced normalized mirror of resize_expand_advanced; batch 32 scaled conservatively to 32." + }, + "baselines": { + "resize_anisotropic_expand_2160x3840_nhwc_tensor_linear_uint8_c1_advanced[InOutDataType=uint8][shape=32x480x864][resizeType=TARGET_2160x3840][interpolation=LINEAR][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1192.87333258668, + "gpu_time_us_python": 1204.4346252211203, + "gpu_noise_us_cpp": 1.614, + "gpu_noise_us_python": 2.666, + "gpu_bwutil_cpp": 0.15022640619851607, + "gpu_bwutil_python": 0.1487845264047319, + "gpu_gap_stddev_us": 2.236964100050133 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 947.0586649213601, + "gpu_time_us_python": 956.82530063012, + "gpu_noise_us_cpp": 1.3679999999999999, + "gpu_noise_us_python": 3.374, + "gpu_bwutil_cpp": 0.14434203289337275, + "gpu_bwutil_python": 0.14286726130948316, + "gpu_gap_stddev_us": 0.994699720797654 + } + } + } + }, + "resize_anisotropic_expand_2160x3840_nhwc_varshape_linear_uint8_c1_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "32x480x864" + ], + "resizeType": [ + "TARGET_2160x3840" + ], + "interpolation": [ + "LINEAR" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 500, + "metadata": { + "description": "Resize uint8 1-channel VarShape (NHWC) anisotropically from 864x480 to 3840x2160 with LINEAR. Advanced normalized mirror of resize_expand_linear_varshape_advanced; batch 32 scaled conservatively to 32." + }, + "baselines": { + "resize_anisotropic_expand_2160x3840_nhwc_varshape_linear_uint8_c1_advanced[InOutDataType=uint8][shape=32x480x864][resizeType=TARGET_2160x3840][interpolation=LINEAR][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1125.6150279522, + "gpu_time_us_python": 1140.2869409176399, + "gpu_noise_us_cpp": 1.4140000000000001, + "gpu_noise_us_python": 3.586, + "gpu_bwutil_cpp": 0.15920274354257372, + "gpu_bwutil_python": 0.15715393672069394, + "gpu_gap_stddev_us": 1.910707837506795 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 891.43672187782, + "gpu_time_us_python": 902.98728720694, + "gpu_noise_us_cpp": 1.7, + "gpu_noise_us_python": 3.6400000000000006, + "gpu_bwutil_cpp": 0.15334528650712667, + "gpu_bwutil_python": 0.15138227714092273, + "gpu_gap_stddev_us": 0.6942740263917528 + } + } + } + } + } +} diff --git a/bench/config/operators/resizecropconvertreformat.json b/bench/config/operators/resizecropconvertreformat.json new file mode 100644 index 000000000..0e37f8a09 --- /dev/null +++ b/bench/config/operators/resizecropconvertreformat.json @@ -0,0 +1,263 @@ +{ + "benchmark": "resizecropconvertreformat", + "configs": { + "resizecropconvertreformat_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "256x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ResizeCropConvertReformat RGB8 (basic)." + }, + "baselines": { + "resizecropconvertreformat_uchar3_basic[InOutDataType=uchar3][shape=256x1080x1920][layout=NHWC][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1237.58358654726, + "gpu_time_us_python": 1249.62869610532, + "gpu_noise_us_cpp": 1.2580000000000002, + "gpu_noise_us_python": 2.9519999999999995, + "gpu_bwutil_cpp": 0.5246474439744793, + "gpu_bwutil_python": 1.5587735892368104, + "gpu_gap_stddev_us": 1.3131923623231867 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 743.856364365025, + "gpu_time_us_python": 753.909595314825, + "gpu_noise_us_cpp": 1.0875, + "gpu_noise_us_python": 3.5675, + "gpu_bwutil_cpp": 0.6657661934698191, + "gpu_bwutil_python": 1.970649221012723, + "gpu_gap_stddev_us": 2.247403395742407 + } + }, + "resizecropconvertreformat_uchar3_basic[InOutDataType=uchar3][shape=256x1080x1920][layout=NHWC][interpolation=LINEAR][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1375.19146112506, + "gpu_time_us_python": 1392.92832971576, + "gpu_noise_us_cpp": 0.8800000000000001, + "gpu_noise_us_python": 3.9440000000000004, + "gpu_bwutil_cpp": 0.4721507122984828, + "gpu_bwutil_python": 1.3984144441696866, + "gpu_gap_stddev_us": 0.9730662139463069 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 810.2707186508751, + "gpu_time_us_python": 826.9026755287, + "gpu_noise_us_cpp": 2.6700000000000004, + "gpu_noise_us_python": 4.55, + "gpu_bwutil_cpp": 0.6112203594405593, + "gpu_bwutil_python": 1.796757117822376, + "gpu_gap_stddev_us": 1.4644299033742778 + } + }, + "resizecropconvertreformat_uchar3_basic[InOutDataType=uchar3][shape=256x1080x1920][layout=NCHW][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1281.5596151282803, + "gpu_time_us_python": 1293.4925035102601, + "gpu_noise_us_cpp": 1.2120000000000002, + "gpu_noise_us_python": 2.6079999999999997, + "gpu_bwutil_cpp": 0.5066457893754522, + "gpu_bwutil_python": 1.5059106853230653, + "gpu_gap_stddev_us": 2.4691283018091523 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 922.54699608375, + "gpu_time_us_python": 934.926556897575, + "gpu_noise_us_cpp": 0.9400000000000001, + "gpu_noise_us_python": 3.58, + "gpu_bwutil_cpp": 0.5368684937309579, + "gpu_bwutil_python": 1.589231937878703, + "gpu_gap_stddev_us": 1.9693734205343092 + } + }, + "resizecropconvertreformat_uchar3_basic[InOutDataType=uchar3][shape=256x1080x1920][layout=NCHW][interpolation=LINEAR][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1397.8940891975, + "gpu_time_us_python": 1414.34938190608, + "gpu_noise_us_cpp": 1.8219999999999998, + "gpu_noise_us_python": 3.974, + "gpu_bwutil_cpp": 0.46448211220302743, + "gpu_bwutil_python": 1.3772362992487, + "gpu_gap_stddev_us": 1.2614392604062314 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 966.7679216315751, + "gpu_time_us_python": 984.679682289575, + "gpu_noise_us_cpp": 1.9475, + "gpu_noise_us_python": 4.915000000000001, + "gpu_bwutil_cpp": 0.512298885459645, + "gpu_bwutil_python": 1.5088654581037475, + "gpu_gap_stddev_us": 3.1147831467476372 + } + } + } + }, + "resizecropconvertreformat_fakeplanar_nchw_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "256x1080x1920" + ], + "layout": [ + "NCHW", + "NCHW_FAKE" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ResizeCropConvertReformat RGB8 fake-planar tensor comparison (advanced)." + }, + "baselines": { + "resizecropconvertreformat_fakeplanar_nchw_advanced[InOutDataType=uchar3][shape=256x1080x1920][layout=NCHW_FAKE][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 7589.96864954624, + "gpu_time_us_python": 7605.3224794791795, + "gpu_noise_us_cpp": 1.734, + "gpu_noise_us_python": 2.5279999999999996, + "gpu_bwutil_cpp": 0.35537635374838417, + "gpu_bwutil_python": 1.0639766890695472, + "gpu_gap_stddev_us": 1.373698446438156 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 5574.122740565826, + "gpu_time_us_python": 5587.674229847575, + "gpu_noise_us_cpp": 3.92, + "gpu_noise_us_python": 2.6625, + "gpu_bwutil_cpp": 0.3690768195607468, + "gpu_bwutil_python": 1.1045459423243456, + "gpu_gap_stddev_us": 2.6089583541710324 + } + }, + "resizecropconvertreformat_fakeplanar_nchw_advanced[InOutDataType=uchar3][shape=256x1080x1920][layout=NCHW][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1281.5464008453, + "gpu_time_us_python": 1293.91307653914, + "gpu_noise_us_cpp": 0.9780000000000001, + "gpu_noise_us_python": 2.346, + "gpu_bwutil_cpp": 0.5066506578299913, + "gpu_bwutil_python": 1.5054222563946258, + "gpu_gap_stddev_us": 1.494269315642786 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 921.928791262975, + "gpu_time_us_python": 934.0264545670501, + "gpu_noise_us_cpp": 3.0125, + "gpu_noise_us_python": 3.1325, + "gpu_bwutil_cpp": 0.5372234533750618, + "gpu_bwutil_python": 1.5908311593970812, + "gpu_gap_stddev_us": 2.0177016724833257 + } + } + } + }, + "resizecropconvertreformat_rgb_nearest_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "256x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "interpolation": [ + "NEAREST" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "ResizeCropConvertReformat RGB8 nearest (advanced)." + }, + "baselines": { + "resizecropconvertreformat_rgb_nearest_1080p_advanced[InOutDataType=uchar3][shape=256x1080x1920][layout=NHWC][interpolation=NEAREST][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 737.50489831018, + "gpu_time_us_python": 753.46486568086, + "gpu_noise_us_cpp": 2.1079999999999997, + "gpu_noise_us_python": 3.7640000000000002, + "gpu_bwutil_cpp": 0.8803947806058797, + "gpu_bwutil_python": 2.58525216878771, + "gpu_gap_stddev_us": 1.9821261338872913 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 597.6400912608, + "gpu_time_us_python": 614.7611693306251, + "gpu_noise_us_cpp": 2.6125000000000003, + "gpu_noise_us_python": 5.1775, + "gpu_bwutil_cpp": 0.8286428172102861, + "gpu_bwutil_python": 2.416683947061225, + "gpu_gap_stddev_us": 1.2789482991878505 + } + }, + "resizecropconvertreformat_rgb_nearest_1080p_advanced[InOutDataType=uchar3][shape=256x1080x1920][layout=NCHW][interpolation=NEAREST][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 784.08958309082, + "gpu_time_us_python": 800.34033305852, + "gpu_noise_us_cpp": 2.104, + "gpu_noise_us_python": 2.928, + "gpu_bwutil_cpp": 0.8280884546634288, + "gpu_bwutil_python": 2.433828330487372, + "gpu_gap_stddev_us": 1.0169014548179458 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 695.106051173025, + "gpu_time_us_python": 710.65028342805, + "gpu_noise_us_cpp": 2.0725, + "gpu_noise_us_python": 4.305, + "gpu_bwutil_cpp": 0.7124534508524749, + "gpu_bwutil_python": 2.0906131297276427, + "gpu_gap_stddev_us": 2.0508027248194574 + } + } + } + } + } +} diff --git a/bench/config/operators/rotate.json b/bench/config/operators/rotate.json new file mode 100644 index 000000000..863bc2c89 --- /dev/null +++ b/bench/config/operators/rotate.json @@ -0,0 +1,1236 @@ +{ + "benchmark": "rotate", + "configs": { + "rotate_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NHWC" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Rotate RGB8 (basic)." + }, + "baselines": { + "rotate_uchar3_basic[InOutDataType=uchar3][shape=32x1080x1920][layout=NHWC][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1906.22948731722, + "gpu_time_us_python": 1918.068011311, + "gpu_noise_us_cpp": 3.2079999999999997, + "gpu_noise_us_python": 4.8, + "gpu_bwutil_cpp": 0.10645186839928941, + "gpu_bwutil_python": 0.10579481921405867, + "gpu_gap_stddev_us": 2.3415772166821855 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1636.88681094312, + "gpu_time_us_python": 1649.30215695494, + "gpu_noise_us_cpp": 1.3459999999999999, + "gpu_noise_us_python": 4.569999999999999, + "gpu_bwutil_cpp": 0.09456471610962464, + "gpu_bwutil_python": 0.09385110509637952, + "gpu_gap_stddev_us": 3.191874560747115 + } + }, + "rotate_uchar3_basic[InOutDataType=uchar3][shape=32x1080x1920][layout=NHWC][interpolation=CUBIC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2015.07735235456, + "gpu_time_us_python": 2031.04180801326, + "gpu_noise_us_cpp": 5.804, + "gpu_noise_us_python": 5.33, + "gpu_bwutil_cpp": 0.10070222390187962, + "gpu_bwutil_python": 0.09991032624543637, + "gpu_gap_stddev_us": 3.385876293995668 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1736.9284966272396, + "gpu_time_us_python": 1753.30212743904, + "gpu_noise_us_cpp": 1.196, + "gpu_noise_us_python": 5.162, + "gpu_bwutil_cpp": 0.08911748369298919, + "gpu_bwutil_python": 0.08828428499312561, + "gpu_gap_stddev_us": 4.249704994887035 + } + } + } + }, + "rotate_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NHWC" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Rotate RGBA8 (advanced)." + }, + "baselines": { + "rotate_uchar4_advanced[InOutDataType=uchar4][shape=32x1080x1920][layout=NHWC][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2360.02751115216, + "gpu_time_us_python": 2374.43186112164, + "gpu_noise_us_cpp": 0.994, + "gpu_noise_us_python": 4.336, + "gpu_bwutil_cpp": 0.11464377542390773, + "gpu_bwutil_python": 0.11394817105307091, + "gpu_gap_stddev_us": 5.040489259534964 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1799.92707771356, + "gpu_time_us_python": 1811.4334362415, + "gpu_noise_us_cpp": 1.81, + "gpu_noise_us_python": 3.0140000000000002, + "gpu_bwutil_cpp": 0.11466517183753666, + "gpu_bwutil_python": 0.11393559426178719, + "gpu_gap_stddev_us": 2.045244906509899 + } + }, + "rotate_uchar4_advanced[InOutDataType=uchar4][shape=32x1080x1920][layout=NHWC][interpolation=CUBIC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2475.14207967154, + "gpu_time_us_python": 2492.8917482347197, + "gpu_noise_us_cpp": 1.6780000000000002, + "gpu_noise_us_python": 4.390000000000001, + "gpu_bwutil_cpp": 0.1093118710431851, + "gpu_bwutil_python": 0.10853443598765258, + "gpu_gap_stddev_us": 7.196526333930744 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1882.6566378964799, + "gpu_time_us_python": 1897.9654202491802, + "gpu_noise_us_cpp": 1.396, + "gpu_noise_us_python": 3.7079999999999997, + "gpu_bwutil_cpp": 0.10962636108398442, + "gpu_bwutil_python": 0.10873958321787583, + "gpu_gap_stddev_us": 4.391326237830779 + } + } + } + }, + "rotate_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NHWC" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Rotate RGBf32 (basic)." + }, + "baselines": { + "rotate_float3_basic[InOutDataType=float3][shape=32x1080x1920][layout=NHWC][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4896.44769503874, + "gpu_time_us_python": 4908.929582382919, + "gpu_noise_us_cpp": 4.856, + "gpu_noise_us_python": 6.144, + "gpu_bwutil_cpp": 0.1657702069155707, + "gpu_bwutil_python": 0.1653486678209, + "gpu_gap_stddev_us": 4.908318078084093 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4303.7556884025, + "gpu_time_us_python": 4316.65106438388, + "gpu_noise_us_cpp": 1.502, + "gpu_noise_us_python": 3.818, + "gpu_bwutil_cpp": 0.14386683185512714, + "gpu_bwutil_python": 0.1434376099207969, + "gpu_gap_stddev_us": 2.605378879174263 + } + } + } + }, + "rotate_float3_varshape_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NHWC" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Rotate RGBf32 VarShape (advanced)." + }, + "baselines": { + "rotate_float3_varshape_advanced[InOutDataType=float3][shape=32x1080x1920][layout=NHWC][interpolation=CUBIC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5111.89044240052, + "gpu_time_us_python": 5129.40408161702, + "gpu_noise_us_cpp": 4.3420000000000005, + "gpu_noise_us_python": 7.296000000000001, + "gpu_bwutil_cpp": 0.1587838343020385, + "gpu_bwutil_python": 0.15824163793871904, + "gpu_gap_stddev_us": 4.178861397992216 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4486.255276499721, + "gpu_time_us_python": 4502.7476167347, + "gpu_noise_us_cpp": 1.5939999999999999, + "gpu_noise_us_python": 4.2219999999999995, + "gpu_bwutil_cpp": 0.13801416563649022, + "gpu_bwutil_python": 0.1375074879528686, + "gpu_gap_stddev_us": 3.692092262333511 + } + } + } + }, + "rotate_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NHWC" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Rotate RGBAf32 (advanced)." + }, + "baselines": { + "rotate_float4_advanced[InOutDataType=float4][shape=32x1080x1920][layout=NHWC][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2495.2503095805796, + "gpu_time_us_python": 2507.42580842966, + "gpu_noise_us_cpp": 6.35, + "gpu_noise_us_python": 2.7479999999999998, + "gpu_bwutil_cpp": 0.4337231117150891, + "gpu_bwutil_python": 0.43161642649580384, + "gpu_gap_stddev_us": 3.2504164237105804 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2310.4191540357, + "gpu_time_us_python": 2321.6042213931, + "gpu_noise_us_cpp": 1.222, + "gpu_noise_us_python": 3.246, + "gpu_bwutil_cpp": 0.3573046860645768, + "gpu_bwutil_python": 0.3555860990187888, + "gpu_gap_stddev_us": 1.3118632541202968 + } + }, + "rotate_float4_advanced[InOutDataType=float4][shape=32x1080x1920][layout=NHWC][interpolation=CUBIC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2759.04312081384, + "gpu_time_us_python": 2773.78302605012, + "gpu_noise_us_cpp": 3.842, + "gpu_noise_us_python": 4.35, + "gpu_bwutil_cpp": 0.3922542397437679, + "gpu_bwutil_python": 0.39016991223472475, + "gpu_gap_stddev_us": 4.040222676009632 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2502.8337982792796, + "gpu_time_us_python": 2518.50754968712, + "gpu_noise_us_cpp": 1.242, + "gpu_noise_us_python": 7.344000000000001, + "gpu_bwutil_cpp": 0.329838474525874, + "gpu_bwutil_python": 0.3277864361280523, + "gpu_gap_stddev_us": 2.539330464075764 + } + } + } + }, + "rotate_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "layout": [ + "NHWC" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Rotate single-channel (advanced)." + }, + "baselines": { + "rotate_scalar_advanced[InOutDataType=float32][shape=64x1080x1920][layout=NHWC][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3233.7657433289396, + "gpu_time_us_python": 3245.91683874592, + "gpu_noise_us_cpp": 3.4479999999999995, + "gpu_noise_us_python": 5.790000000000001, + "gpu_bwutil_cpp": 0.16733551478965047, + "gpu_bwutil_python": 0.16670924416341443, + "gpu_gap_stddev_us": 3.9515376165547873 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2527.19570095142, + "gpu_time_us_python": 2538.52344605718, + "gpu_noise_us_cpp": 0.954, + "gpu_noise_us_python": 2.48, + "gpu_bwutil_cpp": 0.16333284858965572, + "gpu_bwutil_python": 0.16260467165087555, + "gpu_gap_stddev_us": 1.313950010749054 + } + }, + "rotate_scalar_advanced[InOutDataType=float32][shape=64x1080x1920][layout=NHWC][interpolation=CUBIC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3671.8007484359796, + "gpu_time_us_python": 3688.445704154, + "gpu_noise_us_cpp": 4.6080000000000005, + "gpu_noise_us_python": 5.892, + "gpu_bwutil_cpp": 0.14737292583484418, + "gpu_bwutil_python": 0.14670798133722635, + "gpu_gap_stddev_us": 4.8007846826078975 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2930.17199663414, + "gpu_time_us_python": 2944.9711353908397, + "gpu_noise_us_cpp": 1.75, + "gpu_noise_us_python": 2.8120000000000003, + "gpu_bwutil_cpp": 0.14087046385771443, + "gpu_bwutil_python": 0.140162744883091, + "gpu_gap_stddev_us": 1.0032369356699165 + } + }, + "rotate_scalar_advanced[InOutDataType=uint8][shape=64x1080x1920][layout=NHWC][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2469.61754714368, + "gpu_time_us_python": 2482.31481040384, + "gpu_noise_us_cpp": 3.1979999999999995, + "gpu_noise_us_python": 4.444000000000001, + "gpu_bwutil_cpp": 0.05477813073978359, + "gpu_bwutil_python": 0.05449798584054716, + "gpu_gap_stddev_us": 3.418050495726482 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2180.2953267743796, + "gpu_time_us_python": 2191.4396413040204, + "gpu_noise_us_cpp": 2.9720000000000004, + "gpu_noise_us_python": 3.5900000000000007, + "gpu_bwutil_cpp": 0.047330327692512164, + "gpu_bwutil_python": 0.04708922970350066, + "gpu_gap_stddev_us": 1.9791060365452289 + } + }, + "rotate_scalar_advanced[InOutDataType=uint8][shape=64x1080x1920][layout=NHWC][interpolation=CUBIC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2983.7487754367, + "gpu_time_us_python": 3000.3175673030005, + "gpu_noise_us_cpp": 1.396, + "gpu_noise_us_python": 3.7299999999999995, + "gpu_bwutil_cpp": 0.04533937895270494, + "gpu_bwutil_python": 0.04508914971941954, + "gpu_gap_stddev_us": 5.327172788827083 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2607.61885727702, + "gpu_time_us_python": 2621.96795510504, + "gpu_noise_us_cpp": 1.43, + "gpu_noise_us_python": 2.416, + "gpu_bwutil_cpp": 0.039573567626243845, + "gpu_bwutil_python": 0.03935720210307392, + "gpu_gap_stddev_us": 1.6582679654591963 + } + } + } + }, + "rotate_linear_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "layout": [ + "NHWC" + ], + "interpolation": [ + "LINEAR" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Rotate single-channel linear (advanced)." + }, + "baselines": { + "rotate_linear_1080p_advanced[InOutDataType=float32][shape=128x1080x1920][layout=NHWC][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3233.8314239795, + "gpu_time_us_python": 3245.3816497003804, + "gpu_noise_us_cpp": 2.6580000000000004, + "gpu_noise_us_python": 3.6, + "gpu_bwutil_cpp": 0.3346631242336208, + "gpu_bwutil_python": 0.3334720778808127, + "gpu_gap_stddev_us": 1.4537935609067592 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2328.68807501928, + "gpu_time_us_python": 2339.0234082453, + "gpu_noise_us_cpp": 1.596, + "gpu_noise_us_python": 4.308, + "gpu_bwutil_cpp": 0.3545102581080083, + "gpu_bwutil_python": 0.35294174422111035, + "gpu_gap_stddev_us": 1.8334846536442981 + } + }, + "rotate_linear_1080p_advanced[InOutDataType=float32][shape=128x1080x1920][layout=NHWC][interpolation=LINEAR][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3905.11812352212, + "gpu_time_us_python": 3924.1631887852805, + "gpu_noise_us_cpp": 3.146, + "gpu_noise_us_python": 6.864, + "gpu_bwutil_cpp": 0.2771353967798353, + "gpu_bwutil_python": 0.27579065714462325, + "gpu_gap_stddev_us": 3.889870651118741 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3203.03950261672, + "gpu_time_us_python": 3219.0927721883, + "gpu_noise_us_cpp": 2.1399999999999997, + "gpu_noise_us_python": 3.0500000000000003, + "gpu_bwutil_cpp": 0.25774104066504383, + "gpu_bwutil_python": 0.25645318911903103, + "gpu_gap_stddev_us": 1.4102831721163875 + } + }, + "rotate_linear_1080p_advanced[InOutDataType=uint8][shape=128x1080x1920][layout=NHWC][interpolation=LINEAR][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2389.23193068728, + "gpu_time_us_python": 2402.1388751944805, + "gpu_noise_us_cpp": 1.486, + "gpu_noise_us_python": 4.324, + "gpu_bwutil_cpp": 0.11324243237838774, + "gpu_bwutil_python": 0.11263379970022432, + "gpu_gap_stddev_us": 3.5610526412570835 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1620.80432045906, + "gpu_time_us_python": 1631.02648673558, + "gpu_noise_us_cpp": 1.516, + "gpu_noise_us_python": 3.1239999999999997, + "gpu_bwutil_cpp": 0.12733691197072092, + "gpu_bwutil_python": 0.1265401247649845, + "gpu_gap_stddev_us": 1.558768771023414 + } + }, + "rotate_linear_1080p_advanced[InOutDataType=uint8][shape=128x1080x1920][layout=NHWC][interpolation=LINEAR][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2798.89795625396, + "gpu_time_us_python": 2819.26110489682, + "gpu_noise_us_cpp": 2.2119999999999997, + "gpu_noise_us_python": 5.1259999999999994, + "gpu_bwutil_cpp": 0.09666747757257761, + "gpu_bwutil_python": 0.09596942683680199, + "gpu_gap_stddev_us": 5.5833840066941836 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1937.48804852694, + "gpu_time_us_python": 1953.7156521820002, + "gpu_noise_us_cpp": 1.578, + "gpu_noise_us_python": 3.312, + "gpu_bwutil_cpp": 0.10652431717286012, + "gpu_bwutil_python": 0.1056385061958296, + "gpu_gap_stddev_us": 1.049427157979038 + } + } + } + }, + "rotate_nearest_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "layout": [ + "NHWC" + ], + "interpolation": [ + "NEAREST" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Rotate single-channel nearest (advanced)." + }, + "baselines": { + "rotate_nearest_1080p_advanced[InOutDataType=float32][shape=128x1080x1920][layout=NHWC][interpolation=NEAREST][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1903.5089366363202, + "gpu_time_us_python": 1913.79817514922, + "gpu_noise_us_cpp": 1.1460000000000001, + "gpu_noise_us_python": 2.972, + "gpu_bwutil_cpp": 0.5685521009407027, + "gpu_bwutil_python": 0.5654950539560877, + "gpu_gap_stddev_us": 1.259927835204898 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1710.58752079946, + "gpu_time_us_python": 1721.6512111684801, + "gpu_noise_us_cpp": 0.9560000000000001, + "gpu_noise_us_python": 3.154, + "gpu_bwutil_cpp": 0.4825629614402137, + "gpu_bwutil_python": 0.47945951706664597, + "gpu_gap_stddev_us": 1.067541373498413 + } + }, + "rotate_nearest_1080p_advanced[InOutDataType=float32][shape=128x1080x1920][layout=NHWC][interpolation=NEAREST][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2465.2117154177004, + "gpu_time_us_python": 2483.08341828992, + "gpu_noise_us_cpp": 2.77, + "gpu_noise_us_python": 3.3240000000000003, + "gpu_bwutil_cpp": 0.439008480073813, + "gpu_bwutil_python": 0.4358487779476568, + "gpu_gap_stddev_us": 2.6025766150930214 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2170.4832746079196, + "gpu_time_us_python": 2189.55491782032, + "gpu_noise_us_cpp": 1.8059999999999998, + "gpu_noise_us_python": 2.446, + "gpu_bwutil_cpp": 0.3803310128809781, + "gpu_bwutil_python": 0.37701441445988043, + "gpu_gap_stddev_us": 3.5672218109582428 + } + }, + "rotate_nearest_1080p_advanced[InOutDataType=uint8][shape=128x1080x1920][layout=NHWC][interpolation=NEAREST][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 942.8278844081, + "gpu_time_us_python": 953.7998771234201, + "gpu_noise_us_cpp": 1.7600000000000002, + "gpu_noise_us_python": 4.07, + "gpu_bwutil_cpp": 0.2869682915087505, + "gpu_bwutil_python": 0.28366914148140976, + "gpu_gap_stddev_us": 2.4275876621738295 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 921.7884566897599, + "gpu_time_us_python": 933.0331547853999, + "gpu_noise_us_cpp": 2.258, + "gpu_noise_us_python": 3.5699999999999994, + "gpu_bwutil_cpp": 0.2239000244137112, + "gpu_bwutil_python": 0.22120305128703452, + "gpu_gap_stddev_us": 1.6439578888624402 + } + }, + "rotate_nearest_1080p_advanced[InOutDataType=uint8][shape=128x1080x1920][layout=NHWC][interpolation=NEAREST][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1307.74616696142, + "gpu_time_us_python": 1324.66347657194, + "gpu_noise_us_cpp": 1.32, + "gpu_noise_us_python": 3.386, + "gpu_bwutil_cpp": 0.20689228427630663, + "gpu_bwutil_python": 0.204250561854011, + "gpu_gap_stddev_us": 2.6237192465148227 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1034.88920705868, + "gpu_time_us_python": 1050.8595172551402, + "gpu_noise_us_cpp": 2.416, + "gpu_noise_us_python": 2.816, + "gpu_bwutil_cpp": 0.19942815893539054, + "gpu_bwutil_python": 0.19639591525632213, + "gpu_gap_stddev_us": 1.7432279923849512 + } + } + } + }, + "rotate_planar_nchw_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NCHW" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Rotate RGB8 planar NCHW (basic)." + }, + "baselines": { + "rotate_planar_nchw_uchar3_basic[InOutDataType=uchar3][shape=32x1080x1920][layout=NCHW][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3148.31873059268, + "gpu_time_us_python": 3160.85382483424, + "gpu_noise_us_cpp": 1.576, + "gpu_noise_us_python": 3.754, + "gpu_bwutil_cpp": 0.06445406281643805, + "gpu_bwutil_python": 0.06419833202264005, + "gpu_gap_stddev_us": 3.3270428579523736 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3032.90842434614, + "gpu_time_us_python": 3044.49000884168, + "gpu_noise_us_cpp": 1.53, + "gpu_noise_us_python": 4.151999999999999, + "gpu_bwutil_cpp": 0.05103618259303836, + "gpu_bwutil_python": 0.05084230872344634, + "gpu_gap_stddev_us": 1.5299005887584955 + } + }, + "rotate_planar_nchw_uchar3_basic[InOutDataType=uchar3][shape=32x1080x1920][layout=NCHW][interpolation=CUBIC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3637.57745217582, + "gpu_time_us_python": 3652.8127856876004, + "gpu_noise_us_cpp": 1.936, + "gpu_noise_us_python": 2.84, + "gpu_bwutil_cpp": 0.05578487639271568, + "gpu_bwutil_python": 0.05555216855189048, + "gpu_gap_stddev_us": 2.962134602074648 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3150.7516899286798, + "gpu_time_us_python": 3166.24457818446, + "gpu_noise_us_cpp": 1.34, + "gpu_noise_us_python": 4.692, + "gpu_bwutil_cpp": 0.04912768164302832, + "gpu_bwutil_python": 0.04888721892627608, + "gpu_gap_stddev_us": 2.464484765032537 + } + } + } + }, + "rotate_planar_nchw_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NCHW" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Rotate RGBA8 planar NCHW, tensor-only (advanced)." + }, + "baselines": { + "rotate_planar_nchw_uchar4_advanced[InOutDataType=uchar4][shape=32x1080x1920][layout=NCHW][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4242.4516878289, + "gpu_time_us_python": 4258.66677720664, + "gpu_noise_us_cpp": 1.5299999999999998, + "gpu_noise_us_python": 6.564, + "gpu_bwutil_cpp": 0.06377497043169478, + "gpu_bwutil_python": 0.06353206018498996, + "gpu_gap_stddev_us": 7.580438206861243 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3735.0737244963007, + "gpu_time_us_python": 3748.4219214453797, + "gpu_noise_us_cpp": 2.226, + "gpu_noise_us_python": 5.779999999999999, + "gpu_bwutil_cpp": 0.05525596017760944, + "gpu_bwutil_python": 0.0550588755547172, + "gpu_gap_stddev_us": 1.352667978098528 + } + } + } + }, + "rotate_planar_nchw_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NCHW" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Rotate RGBf32 planar NCHW (basic)." + }, + "baselines": { + "rotate_planar_nchw_float3_basic[InOutDataType=float3][shape=32x1080x1920][layout=NCHW][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4622.8342473386, + "gpu_time_us_python": 4637.26561334392, + "gpu_noise_us_cpp": 4.572, + "gpu_noise_us_python": 5.994, + "gpu_bwutil_cpp": 0.17558165817544522, + "gpu_bwutil_python": 0.17503524031871318, + "gpu_gap_stddev_us": 3.1496414852469674 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3643.61301145544, + "gpu_time_us_python": 3655.92281366316, + "gpu_noise_us_cpp": 1.43, + "gpu_noise_us_python": 3.14, + "gpu_bwutil_cpp": 0.1699268374940712, + "gpu_bwutil_python": 0.16935322385975407, + "gpu_gap_stddev_us": 2.082611263677567 + } + } + } + }, + "rotate_planar_nchw_float3_varshape_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NCHW" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Rotate RGBf32 planar NCHW VarShape (advanced)." + }, + "baselines": { + "rotate_planar_nchw_float3_varshape_advanced[InOutDataType=float3][shape=32x1080x1920][layout=NCHW][interpolation=CUBIC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5094.0810910438595, + "gpu_time_us_python": 5110.3565244090405, + "gpu_noise_us_cpp": 5.234, + "gpu_noise_us_python": 6.790000000000001, + "gpu_bwutil_cpp": 0.1593389798019984, + "gpu_bwutil_python": 0.15883161739998314, + "gpu_gap_stddev_us": 3.0970942550322436 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4125.73618346704, + "gpu_time_us_python": 4142.031024251641, + "gpu_noise_us_cpp": 1.9439999999999997, + "gpu_noise_us_python": 4.07, + "gpu_bwutil_cpp": 0.15007143880481486, + "gpu_bwutil_python": 0.14948360964197105, + "gpu_gap_stddev_us": 5.3726402769701895 + } + } + } + }, + "rotate_planar_nchw_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NCHW" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Rotate RGBAf32 planar NCHW (advanced)." + }, + "baselines": { + "rotate_planar_nchw_float4_advanced[InOutDataType=float4][shape=32x1080x1920][layout=NCHW][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6117.460923078521, + "gpu_time_us_python": 6128.7448906316395, + "gpu_noise_us_cpp": 5.414, + "gpu_noise_us_python": 5.952, + "gpu_bwutil_cpp": 0.17691101021820527, + "gpu_bwutil_python": 0.17658541152218413, + "gpu_gap_stddev_us": 3.9791972788878858 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4675.6906393668805, + "gpu_time_us_python": 4687.1190813803605, + "gpu_noise_us_cpp": 3.02, + "gpu_noise_us_python": 2.662, + "gpu_bwutil_cpp": 0.1765603572963963, + "gpu_bwutil_python": 0.1761276344345752, + "gpu_gap_stddev_us": 4.3707911770779475 + } + }, + "rotate_planar_nchw_float4_advanced[InOutDataType=float4][shape=32x1080x1920][layout=NCHW][interpolation=CUBIC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6575.471901732701, + "gpu_time_us_python": 6592.49557123164, + "gpu_noise_us_cpp": 6.3100000000000005, + "gpu_noise_us_python": 8.632, + "gpu_bwutil_cpp": 0.16458856843444908, + "gpu_bwutil_python": 0.16416353507424802, + "gpu_gap_stddev_us": 3.027402383148105 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5247.93738548442, + "gpu_time_us_python": 5262.5612438751195, + "gpu_noise_us_cpp": 1.884, + "gpu_noise_us_python": 2.896, + "gpu_bwutil_cpp": 0.15730741630468567, + "gpu_bwutil_python": 0.15686942823266298, + "gpu_gap_stddev_us": 3.8974572337961835 + } + } + } + }, + "rotate_planar_nchw_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NCHW" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Rotate RGB8 native-planar Tensor comparison (advanced)." + }, + "baselines": { + "rotate_planar_nchw_uchar3_advanced[InOutDataType=uchar3][shape=32x1080x1920][layout=NCHW][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3151.33954882616, + "gpu_time_us_python": 3160.94961372352, + "gpu_noise_us_cpp": 3.878, + "gpu_noise_us_python": 4.676, + "gpu_bwutil_cpp": 0.06439224291444368, + "gpu_bwutil_python": 0.06419641644612543, + "gpu_gap_stddev_us": 3.1406198037128052 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3033.23605489164, + "gpu_time_us_python": 3044.5940765772602, + "gpu_noise_us_cpp": 1.796, + "gpu_noise_us_python": 3.0759999999999996, + "gpu_bwutil_cpp": 0.05103081471407374, + "gpu_bwutil_python": 0.05084022683838853, + "gpu_gap_stddev_us": 1.1338176962737816 + } + } + } + }, + "rotate_fake_planar_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Rotate RGB8 fake-planar reformat-rotate-reformat (advanced)." + }, + "baselines": { + "rotate_fake_planar_uchar3_advanced[InOutDataType=uchar3][shape=32x1080x1920][layout=NCHW_FAKE][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3102.1511278034004, + "gpu_time_us_python": 3120.72268455848, + "gpu_noise_us_cpp": 2.148, + "gpu_noise_us_python": 3.446, + "gpu_bwutil_cpp": 0.23046057820370475, + "gpu_bwutil_python": 0.22908894157898002, + "gpu_gap_stddev_us": 2.53173694844139 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2570.37032596546, + "gpu_time_us_python": 2585.5824635474, + "gpu_noise_us_cpp": 1.654, + "gpu_noise_us_python": 4.215999999999999, + "gpu_bwutil_cpp": 0.21215666825975013, + "gpu_bwutil_python": 0.21090909610107436, + "gpu_gap_stddev_us": 1.0362443681182263 + } + } + } + }, + "rotate_fake_planar_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Rotate RGBA8 fake-planar reformat-rotate-reformat (advanced)." + }, + "baselines": { + "rotate_fake_planar_uchar4_advanced[InOutDataType=uchar4][shape=32x1080x1920][layout=NCHW_FAKE][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3904.4105644867604, + "gpu_time_us_python": 3923.7631946801603, + "gpu_noise_us_cpp": 1.736, + "gpu_noise_us_python": 5.014, + "gpu_bwutil_cpp": 0.2441420455676563, + "gpu_bwutil_python": 0.24293775671895404, + "gpu_gap_stddev_us": 0.7698906128879935 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3011.18965817806, + "gpu_time_us_python": 3026.96118818982, + "gpu_noise_us_cpp": 1.19, + "gpu_noise_us_python": 3.3920000000000003, + "gpu_bwutil_cpp": 0.24146384245272295, + "gpu_bwutil_python": 0.24020497414487857, + "gpu_gap_stddev_us": 2.6391430186896234 + } + } + } + }, + "rotate_planar_nchw_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NCHW" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Rotate RGBf32 native-planar Tensor comparison (advanced)." + }, + "baselines": { + "rotate_planar_nchw_float3_advanced[InOutDataType=float3][shape=32x1080x1920][layout=NCHW][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4624.34651525728, + "gpu_time_us_python": 4638.155731143321, + "gpu_noise_us_cpp": 4.773999999999999, + "gpu_noise_us_python": 5.734, + "gpu_bwutil_cpp": 0.1755242980513398, + "gpu_bwutil_python": 0.17500160464313413, + "gpu_gap_stddev_us": 3.342313391982443 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3643.34242405224, + "gpu_time_us_python": 3654.7129297958404, + "gpu_noise_us_cpp": 1.192, + "gpu_noise_us_python": 2.63, + "gpu_bwutil_cpp": 0.16993934789479273, + "gpu_bwutil_python": 0.16941041757831252, + "gpu_gap_stddev_us": 0.7509437583735256 + } + } + } + }, + "rotate_fake_planar_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Rotate RGBf32 fake-planar reformat-rotate-reformat (advanced)." + }, + "baselines": { + "rotate_fake_planar_float3_advanced[InOutDataType=float3][shape=32x1080x1920][layout=NCHW_FAKE][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 7578.78320238376, + "gpu_time_us_python": 7597.949632008819, + "gpu_noise_us_cpp": 2.366, + "gpu_noise_us_python": 5.52, + "gpu_bwutil_cpp": 0.3773284445582205, + "gpu_bwutil_python": 0.3763762497539668, + "gpu_gap_stddev_us": 3.353059125868532 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6493.31987576602, + "gpu_time_us_python": 6510.46976431814, + "gpu_noise_us_cpp": 4.542, + "gpu_noise_us_python": 6.708, + "gpu_bwutil_cpp": 0.33592222602798194, + "gpu_bwutil_python": 0.3350364894857901, + "gpu_gap_stddev_us": 2.2450710610233506 + } + } + } + }, + "rotate_fake_planar_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "interpolation": [ + "CUBIC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Rotate RGBAf32 fake-planar reformat-rotate-reformat (advanced)." + }, + "baselines": { + "rotate_fake_planar_float4_advanced[InOutDataType=float4][shape=32x1080x1920][layout=NCHW_FAKE][interpolation=CUBIC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6718.31311175694, + "gpu_time_us_python": 6740.90576673804, + "gpu_noise_us_cpp": 5.6579999999999995, + "gpu_noise_us_python": 8.838000000000001, + "gpu_bwutil_cpp": 0.5675399953836736, + "gpu_bwutil_python": 0.5656376520946393, + "gpu_gap_stddev_us": 4.339085160091273 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5815.60400031328, + "gpu_time_us_python": 5830.8043426489, + "gpu_noise_us_cpp": 8.564, + "gpu_noise_us_python": 9.134, + "gpu_bwutil_cpp": 0.5000681227873842, + "gpu_bwutil_python": 0.49876527311925517, + "gpu_gap_stddev_us": 1.7958012156652525 + } + } + } + } + } +} diff --git a/bench/config/operators/sift.json b/bench/config/operators/sift.json new file mode 100644 index 000000000..fefbb6891 --- /dev/null +++ b/bench/config/operators/sift.json @@ -0,0 +1,365 @@ +{ + "benchmark": "sift", + "configs": { + "sift_basic": { + "tier": "basic", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "1x720x1280" + ], + "expandInput": [ + "Y" + ], + "inputKind": [ + "Tensor" + ], + "layout": [ + "NHWC", + "NCHW" + ] + }, + "int64_axes": { + "maxCapacity": [ + 10000 + ], + "numOctaveLayers": [ + 3 + ] + }, + "float64_axes": { + "contrastThreshold": [ + 0.04 + ], + "edgeThreshold": [ + 10.0 + ], + "initSigma": [ + 1.6 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "SIFT single-channel (basic)." + }, + "baselines": { + "sift_basic[InOutDataType=uint8][shape=1x720x1280][expandInput=Y][inputKind=Tensor][layout=NHWC][maxCapacity=10000][numOctaveLayers=3][contrastThreshold=0.04][edgeThreshold=10.0][initSigma=1.6]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3054.99386047564, + "gpu_time_us_python": 3069.35758881448, + "gpu_noise_us_cpp": 2.784, + "gpu_noise_us_python": 3.6480000000000006, + "gpu_bwutil_cpp": 0.14949672157326843, + "gpu_bwutil_python": 0.14879713102166842, + "gpu_gap_stddev_us": 1.0103205865364049 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2771.5511580135003, + "gpu_time_us_python": 2788.88070048, + "gpu_noise_us_cpp": 2.722, + "gpu_noise_us_python": 3.7439999999999998, + "gpu_bwutil_cpp": 0.12569204095169279, + "gpu_bwutil_python": 0.12491055683413319, + "gpu_gap_stddev_us": 2.1473170515409423 + } + }, + "sift_basic[InOutDataType=uint8][shape=1x720x1280][expandInput=Y][inputKind=Tensor][layout=NCHW][maxCapacity=10000][numOctaveLayers=3][contrastThreshold=0.04][edgeThreshold=10.0][initSigma=1.6]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3054.7813000063597, + "gpu_time_us_python": 3065.23150876444, + "gpu_noise_us_cpp": 2.2939999999999996, + "gpu_noise_us_python": 4.0440000000000005, + "gpu_bwutil_cpp": 0.14950715280373875, + "gpu_bwutil_python": 0.14899744625732175, + "gpu_gap_stddev_us": 9.332695730783799 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2774.4168928935, + "gpu_time_us_python": 2792.92196806954, + "gpu_noise_us_cpp": 2.6839999999999997, + "gpu_noise_us_python": 7.147999999999999, + "gpu_bwutil_cpp": 0.12556175219930868, + "gpu_bwutil_python": 0.12473035764323162, + "gpu_gap_stddev_us": 3.733349724409839 + } + } + } + }, + "sift_original_default_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "2x1080x1920" + ], + "expandInput": [ + "N" + ], + "inputKind": [ + "Tensor" + ], + "layout": [ + "NHWC", + "NCHW" + ] + }, + "int64_axes": { + "maxCapacity": [ + 10000 + ], + "numOctaveLayers": [ + 3 + ] + }, + "float64_axes": { + "contrastThreshold": [ + 0.04 + ], + "edgeThreshold": [ + 10.0 + ], + "initSigma": [ + 1.6 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "SIFT single-channel original default (advanced)." + }, + "baselines": { + "sift_original_default_1080p_advanced[InOutDataType=uint8][shape=2x1080x1920][expandInput=N][inputKind=Tensor][layout=NHWC][maxCapacity=10000][numOctaveLayers=3][contrastThreshold=0.04][edgeThreshold=10.0][initSigma=1.6]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3515.1670158095403, + "gpu_time_us_python": 3530.0868858006993, + "gpu_noise_us_cpp": 2.888, + "gpu_noise_us_python": 4.12, + "gpu_bwutil_cpp": 0.14698591951506756, + "gpu_bwutil_python": 0.14636401154167006, + "gpu_gap_stddev_us": 4.808332357948431 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3180.99447451306, + "gpu_time_us_python": 3198.56521595974, + "gpu_noise_us_cpp": 2.568, + "gpu_noise_us_python": 6.103999999999999, + "gpu_bwutil_cpp": 0.12389284086344794, + "gpu_bwutil_python": 0.12321417411550159, + "gpu_gap_stddev_us": 3.9219927382158324 + } + }, + "sift_original_default_1080p_advanced[InOutDataType=uint8][shape=2x1080x1920][expandInput=N][inputKind=Tensor][layout=NCHW][maxCapacity=10000][numOctaveLayers=3][contrastThreshold=0.04][edgeThreshold=10.0][initSigma=1.6]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3513.89510212738, + "gpu_time_us_python": 3525.9240509105393, + "gpu_noise_us_cpp": 2.374, + "gpu_noise_us_python": 3.2040000000000006, + "gpu_bwutil_cpp": 0.1470384560757813, + "gpu_bwutil_python": 0.14653646952755156, + "gpu_gap_stddev_us": 6.7316227423202095 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3184.44714781252, + "gpu_time_us_python": 3199.6064307301203, + "gpu_noise_us_cpp": 2.116, + "gpu_noise_us_python": 6.066000000000001, + "gpu_bwutil_cpp": 0.12375860968693875, + "gpu_bwutil_python": 0.12317254463949437, + "gpu_gap_stddev_us": 4.598923454283452 + } + } + } + }, + "sift_original_layers5_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "1x1080x1920" + ], + "expandInput": [ + "N" + ], + "inputKind": [ + "Tensor" + ], + "layout": [ + "NHWC", + "NCHW" + ] + }, + "int64_axes": { + "maxCapacity": [ + 10000 + ], + "numOctaveLayers": [ + 5 + ] + }, + "float64_axes": { + "contrastThreshold": [ + 0.04 + ], + "edgeThreshold": [ + 10.0 + ], + "initSigma": [ + 1.6 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "SIFT single-channel original layers5 (advanced)." + }, + "baselines": { + "sift_original_layers5_1080p_advanced[InOutDataType=uint8][shape=1x1080x1920][expandInput=N][inputKind=Tensor][layout=NHWC][maxCapacity=10000][numOctaveLayers=5][contrastThreshold=0.04][edgeThreshold=10.0][initSigma=1.6]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2598.61412907072, + "gpu_time_us_python": 2613.40294634452, + "gpu_noise_us_cpp": 2.358, + "gpu_noise_us_python": 6.5, + "gpu_bwutil_cpp": 0.1322539784783956, + "gpu_bwutil_python": 0.1315046548086874, + "gpu_gap_stddev_us": 3.2164378500361193 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2405.7233061966203, + "gpu_time_us_python": 2420.30675837852, + "gpu_noise_us_cpp": 2.14, + "gpu_noise_us_python": 4.303999999999999, + "gpu_bwutil_cpp": 0.10896350781069035, + "gpu_bwutil_python": 0.10830630454268735, + "gpu_gap_stddev_us": 2.170127044464038 + } + }, + "sift_original_layers5_1080p_advanced[InOutDataType=uint8][shape=1x1080x1920][expandInput=N][inputKind=Tensor][layout=NCHW][maxCapacity=10000][numOctaveLayers=5][contrastThreshold=0.04][edgeThreshold=10.0][initSigma=1.6]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2599.2385061233404, + "gpu_time_us_python": 2609.38861805854, + "gpu_noise_us_cpp": 2.106, + "gpu_noise_us_python": 3.8579999999999997, + "gpu_bwutil_cpp": 0.13222171811411515, + "gpu_bwutil_python": 0.13170732500434862, + "gpu_gap_stddev_us": 13.13279075303952 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2408.83198314042, + "gpu_time_us_python": 2422.6085596638, + "gpu_noise_us_cpp": 2.992, + "gpu_noise_us_python": 3.636, + "gpu_bwutil_cpp": 0.10882263000896422, + "gpu_bwutil_python": 0.10820394603077688, + "gpu_gap_stddev_us": 2.7822749540342833 + } + } + } + }, + "sift_feature_heavy_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "1x1080x1920" + ], + "expandInput": [ + "N" + ], + "inputKind": [ + "Tensor" + ], + "layout": [ + "NHWC", + "NCHW" + ] + }, + "int64_axes": { + "maxCapacity": [ + 20000 + ], + "numOctaveLayers": [ + 3 + ] + }, + "float64_axes": { + "contrastThreshold": [ + 0.01 + ], + "edgeThreshold": [ + 15.0 + ], + "initSigma": [ + 1.6 + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "SIFT single-channel feature heavy (advanced)." + }, + "baselines": { + "sift_feature_heavy_1080p_advanced[InOutDataType=uint8][shape=1x1080x1920][expandInput=N][inputKind=Tensor][layout=NHWC][maxCapacity=20000][numOctaveLayers=3][contrastThreshold=0.01][edgeThreshold=15.0][initSigma=1.6]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2263.4956496860796, + "gpu_time_us_python": 2280.08530985806, + "gpu_noise_us_cpp": 2.4360000000000004, + "gpu_noise_us_python": 4.220000000000001, + "gpu_bwutil_cpp": 0.11457642108497754, + "gpu_bwutil_python": 0.11374286123930091, + "gpu_gap_stddev_us": 3.742067615266615 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2077.58520503508, + "gpu_time_us_python": 2094.39289816664, + "gpu_noise_us_cpp": 2.682, + "gpu_noise_us_python": 8.148, + "gpu_bwutil_cpp": 0.09521272945642965, + "gpu_bwutil_python": 0.09444777082834219, + "gpu_gap_stddev_us": 2.038377190556721 + } + }, + "sift_feature_heavy_1080p_advanced[InOutDataType=uint8][shape=1x1080x1920][expandInput=N][inputKind=Tensor][layout=NCHW][maxCapacity=20000][numOctaveLayers=3][contrastThreshold=0.01][edgeThreshold=15.0][initSigma=1.6]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2264.2493386819, + "gpu_time_us_python": 2277.9400299378, + "gpu_noise_us_cpp": 2.582, + "gpu_noise_us_python": 3.4219999999999997, + "gpu_bwutil_cpp": 0.11453854700889984, + "gpu_bwutil_python": 0.11384904426338464, + "gpu_gap_stddev_us": 9.599749828414854 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2080.00300519526, + "gpu_time_us_python": 2095.3811493975, + "gpu_noise_us_cpp": 2.57, + "gpu_noise_us_python": 4.3420000000000005, + "gpu_bwutil_cpp": 0.0951021020988512, + "gpu_bwutil_python": 0.09440433155996716, + "gpu_gap_stddev_us": 3.284912499847716 + } + } + } + } + } +} diff --git a/bench/config/operators/solarize.json b/bench/config/operators/solarize.json new file mode 100644 index 000000000..45a8ceabf --- /dev/null +++ b/bench/config/operators/solarize.json @@ -0,0 +1,683 @@ +{ + "benchmark": "solarize", + "configs": { + "solarize_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Solarize RGB8 interleaved + planar, Tensor + VarShape (~1-2ms)." + }, + "baselines": { + "solarize_uchar3_basic[InOutDataType=uchar3][shape=32x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 332.97607159752005, + "gpu_time_us_python": 340.93801862277996, + "gpu_noise_us_cpp": 1.108, + "gpu_noise_us_python": 1.8199999999999998, + "gpu_bwutil_cpp": 0.7688252190376027, + "gpu_bwutil_python": 0.7508705734919443, + "gpu_gap_stddev_us": 0.18100214662051456 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 285.23471766975, + "gpu_time_us_python": 294.2426124914, + "gpu_noise_us_cpp": 1.27, + "gpu_noise_us_python": 3.755, + "gpu_bwutil_cpp": 0.6845737053318021, + "gpu_bwutil_python": 0.6636433935151229, + "gpu_gap_stddev_us": 1.1040352213835158 + } + }, + "solarize_uchar3_basic[InOutDataType=uchar3][shape=32x1080x1920][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 381.62724795273994, + "gpu_time_us_python": 392.18697733808006, + "gpu_noise_us_cpp": 2.4200000000000004, + "gpu_noise_us_python": 3.7239999999999993, + "gpu_bwutil_cpp": 0.6708136521954939, + "gpu_bwutil_python": 0.6527517439679181, + "gpu_gap_stddev_us": 0.6522339242393057 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 352.05807993282497, + "gpu_time_us_python": 362.57868682665, + "gpu_noise_us_cpp": 1.7149999999999999, + "gpu_noise_us_python": 2.9775, + "gpu_bwutil_cpp": 0.554624712394257, + "gpu_bwutil_python": 0.5385288402638216, + "gpu_gap_stddev_us": 0.7245497778279286 + } + }, + "solarize_uchar3_basic[InOutDataType=uchar3][shape=32x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 503.4178194606, + "gpu_time_us_python": 511.54092812444003, + "gpu_noise_us_cpp": 0.8640000000000001, + "gpu_noise_us_python": 2.584, + "gpu_bwutil_cpp": 0.5085242189148123, + "gpu_bwutil_python": 0.5004488372417566, + "gpu_gap_stddev_us": 0.19492914844310646 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 493.7526536741, + "gpu_time_us_python": 502.53447765985, + "gpu_noise_us_cpp": 1.9825000000000002, + "gpu_noise_us_python": 3.715, + "gpu_bwutil_cpp": 0.3954695828179995, + "gpu_bwutil_python": 0.38855415655883774, + "gpu_gap_stddev_us": 1.19794734114633 + } + }, + "solarize_uchar3_basic[InOutDataType=uchar3][shape=32x1080x1920][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 543.3468718104399, + "gpu_time_us_python": 553.8913675793999, + "gpu_noise_us_cpp": 1.848, + "gpu_noise_us_python": 2.532, + "gpu_bwutil_cpp": 0.4711556377291058, + "gpu_bwutil_python": 0.46218673600086246, + "gpu_gap_stddev_us": 0.7032400677524693 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 515.9510787437, + "gpu_time_us_python": 527.20452498065, + "gpu_noise_us_cpp": 1.3875, + "gpu_noise_us_python": 3.5924999999999994, + "gpu_bwutil_cpp": 0.3784444774293846, + "gpu_bwutil_python": 0.3703650677955442, + "gpu_gap_stddev_us": 0.39040300485961577 + } + } + } + }, + "solarize_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "20x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Solarize RGBf32 interleaved + planar, Tensor (~1-2ms)." + }, + "baselines": { + "solarize_float3_basic[InOutDataType=float3][shape=20x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 722.2786437989, + "gpu_time_us_python": 731.39363088546, + "gpu_noise_us_cpp": 1.1940000000000002, + "gpu_noise_us_python": 1.8720000000000003, + "gpu_bwutil_cpp": 0.8860846673781302, + "gpu_bwutil_python": 0.875045646294191, + "gpu_gap_stddev_us": 1.7863993260936863 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 553.09511420845, + "gpu_time_us_python": 561.824985655975, + "gpu_noise_us_cpp": 1.2225000000000001, + "gpu_noise_us_python": 2.8425, + "gpu_bwutil_cpp": 0.8825534935058355, + "gpu_bwutil_python": 0.8688489669083944, + "gpu_gap_stddev_us": 2.0342144876884163 + } + }, + "solarize_float3_basic[InOutDataType=float3][shape=20x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 720.56659403202, + "gpu_time_us_python": 729.0840045705199, + "gpu_noise_us_cpp": 0.858, + "gpu_noise_us_python": 2.62, + "gpu_bwutil_cpp": 0.888189978508682, + "gpu_bwutil_python": 0.8778139935077738, + "gpu_gap_stddev_us": 0.2453159061321157 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 556.6562573316501, + "gpu_time_us_python": 565.648449180875, + "gpu_noise_us_cpp": 1.0675, + "gpu_noise_us_python": 2.0675, + "gpu_bwutil_cpp": 0.8769076972858791, + "gpu_bwutil_python": 0.8629826121276187, + "gpu_gap_stddev_us": 2.3393384771955845 + } + } + } + }, + "solarize_float3_varshape_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "20x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Solarize RGBf32 interleaved + planar, VarShape (~1-2ms)." + }, + "baselines": { + "solarize_float3_varshape_advanced[InOutDataType=float3][shape=20x1080x1920][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 813.3312772233201, + "gpu_time_us_python": 822.98096218798, + "gpu_noise_us_cpp": 0.938, + "gpu_noise_us_python": 1.8800000000000001, + "gpu_bwutil_cpp": 0.7868873716663738, + "gpu_bwutil_python": 0.7776613348570892, + "gpu_gap_stddev_us": 0.7053726298846005 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 674.297427067825, + "gpu_time_us_python": 684.3431279442001, + "gpu_noise_us_cpp": 1.68, + "gpu_noise_us_python": 3.0175, + "gpu_bwutil_cpp": 0.7239186003163823, + "gpu_bwutil_python": 0.7132913677457626, + "gpu_gap_stddev_us": 0.8550927810920528 + } + }, + "solarize_float3_varshape_advanced[InOutDataType=float3][shape=20x1080x1920][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 732.3318592637, + "gpu_time_us_python": 742.3481850465, + "gpu_noise_us_cpp": 2.932, + "gpu_noise_us_python": 2.668, + "gpu_bwutil_cpp": 0.8739210290121717, + "gpu_bwutil_python": 0.8621298035750952, + "gpu_gap_stddev_us": 0.36659417434879826 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 563.2690443216, + "gpu_time_us_python": 573.2177262732001, + "gpu_noise_us_cpp": 2.3375, + "gpu_noise_us_python": 3.2474999999999996, + "gpu_bwutil_cpp": 0.8666146149154412, + "gpu_bwutil_python": 0.8515742303416957, + "gpu_gap_stddev_us": 0.6070074693268925 + } + } + } + }, + "solarize_fake_planar_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Solarize RGB8 fake-planar comparison (tensor-only, ~1-2ms)." + }, + "baselines": { + "solarize_fake_planar_uchar3_advanced[InOutDataType=uchar3][shape=32x1080x1920][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1697.0544641081797, + "gpu_time_us_python": 1713.83936503158, + "gpu_noise_us_cpp": 1.244, + "gpu_noise_us_python": 2.1580000000000004, + "gpu_bwutil_cpp": 0.4525489792665966, + "gpu_bwutil_python": 0.44811670472118503, + "gpu_gap_stddev_us": 1.2144770849225481 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1428.5462292278999, + "gpu_time_us_python": 1443.276193341125, + "gpu_noise_us_cpp": 1.555, + "gpu_noise_us_python": 3.3200000000000003, + "gpu_bwutil_cpp": 0.41004882444029667, + "gpu_bwutil_python": 0.405859379683998, + "gpu_gap_stddev_us": 2.5682337662529373 + } + } + } + }, + "solarize_fake_planar_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "20x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Solarize RGBf32 fake-planar comparison (tensor-only, ~1-2ms)." + }, + "baselines": { + "solarize_fake_planar_float3_advanced[InOutDataType=float3][shape=20x1080x1920][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2397.85097440078, + "gpu_time_us_python": 2413.9224841044, + "gpu_noise_us_cpp": 1.72, + "gpu_noise_us_python": 2.0220000000000002, + "gpu_bwutil_cpp": 0.8007170291635969, + "gpu_bwutil_python": 0.7953860568590686, + "gpu_gap_stddev_us": 1.1558031130534587 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1930.6211687051, + "gpu_time_us_python": 1944.5164058097, + "gpu_noise_us_cpp": 1.8275, + "gpu_noise_us_python": 4.05, + "gpu_bwutil_cpp": 0.7585173235792522, + "gpu_bwutil_python": 0.7530973900686119, + "gpu_gap_stddev_us": 0.5162573239908739 + } + } + } + }, + "solarize_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "24x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Solarize RGBA8 interleaved + planar, Tensor (uchar4 planar var-shape unsupported)." + }, + "baselines": { + "solarize_uchar4_advanced[InOutDataType=uchar4][shape=24x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 332.5775162465, + "gpu_time_us_python": 341.25361305271997, + "gpu_noise_us_cpp": 0.616, + "gpu_noise_us_python": 2.282, + "gpu_bwutil_cpp": 0.7697464395488718, + "gpu_bwutil_python": 0.7501769163957782, + "gpu_gap_stddev_us": 0.17770420840778975 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 284.93348761202503, + "gpu_time_us_python": 295.3054867796, + "gpu_noise_us_cpp": 1.2425000000000002, + "gpu_noise_us_python": 3.07, + "gpu_bwutil_cpp": 0.6852990572280314, + "gpu_bwutil_python": 0.6612601839339358, + "gpu_gap_stddev_us": 1.6476136517154714 + } + }, + "solarize_uchar4_advanced[InOutDataType=uchar4][shape=24x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 425.42264934941994, + "gpu_time_us_python": 433.82473178213996, + "gpu_noise_us_cpp": 2.168, + "gpu_noise_us_python": 1.706, + "gpu_bwutil_cpp": 0.6017553247162221, + "gpu_bwutil_python": 0.5901007583787579, + "gpu_gap_stddev_us": 0.3250903803613613 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 372.7619097373, + "gpu_time_us_python": 381.748898051975, + "gpu_noise_us_cpp": 1.5999999999999999, + "gpu_noise_us_python": 2.285, + "gpu_bwutil_cpp": 0.5238275683125694, + "gpu_bwutil_python": 0.511486764511903, + "gpu_gap_stddev_us": 0.6631346802463446 + } + } + } + }, + "solarize_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Solarize RGBAf32 interleaved + planar, Tensor + VarShape (~1-2ms)." + }, + "baselines": { + "solarize_float4_advanced[InOutDataType=float4][shape=16x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 769.57728253872, + "gpu_time_us_python": 778.47129754464, + "gpu_noise_us_cpp": 1.234, + "gpu_noise_us_python": 3.1919999999999997, + "gpu_bwutil_cpp": 0.8870671509987547, + "gpu_bwutil_python": 0.8769336351246935, + "gpu_gap_stddev_us": 1.0601808245938455 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 589.0179383754249, + "gpu_time_us_python": 596.8371679948, + "gpu_noise_us_cpp": 1.755, + "gpu_noise_us_python": 2.3175, + "gpu_bwutil_cpp": 0.8839767905011959, + "gpu_bwutil_python": 0.8723973183385002, + "gpu_gap_stddev_us": 0.617862117122236 + } + }, + "solarize_float4_advanced[InOutDataType=float4][shape=16x1080x1920][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 862.93964279104, + "gpu_time_us_python": 872.4449132162201, + "gpu_noise_us_cpp": 0.8639999999999999, + "gpu_noise_us_python": 1.8820000000000001, + "gpu_bwutil_cpp": 0.791094553546859, + "gpu_bwutil_python": 0.782476279492316, + "gpu_gap_stddev_us": 1.4519184804423684 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 699.7775818081251, + "gpu_time_us_python": 712.94110975395, + "gpu_noise_us_cpp": 1.4925000000000002, + "gpu_noise_us_python": 3.8899999999999997, + "gpu_bwutil_cpp": 0.7440634998884649, + "gpu_bwutil_python": 0.7303376351342398, + "gpu_gap_stddev_us": 2.617810865278185 + } + }, + "solarize_float4_advanced[InOutDataType=float4][shape=16x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 764.5098382950799, + "gpu_time_us_python": 774.60350224222, + "gpu_noise_us_cpp": 1.048, + "gpu_noise_us_python": 3.0780000000000003, + "gpu_bwutil_cpp": 0.8929468740595796, + "gpu_bwutil_python": 0.8813155695513057, + "gpu_gap_stddev_us": 1.9790911996682874 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 586.5679539288999, + "gpu_time_us_python": 594.9454980534249, + "gpu_noise_us_cpp": 1.2325, + "gpu_noise_us_python": 2.1750000000000003, + "gpu_bwutil_cpp": 0.8876693895211969, + "gpu_bwutil_python": 0.8751698633664546, + "gpu_gap_stddev_us": 0.15054156346099878 + } + }, + "solarize_float4_advanced[InOutDataType=float4][shape=16x1080x1920][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 770.58426079376, + "gpu_time_us_python": 781.79473737718, + "gpu_noise_us_cpp": 2.17, + "gpu_noise_us_python": 3.022, + "gpu_bwutil_cpp": 0.8859080400152568, + "gpu_bwutil_python": 0.8732057472715896, + "gpu_gap_stddev_us": 0.9490918425596444 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 593.019677740925, + "gpu_time_us_python": 603.692674224325, + "gpu_noise_us_cpp": 1.92, + "gpu_noise_us_python": 2.6375, + "gpu_bwutil_cpp": 0.8780119672049252, + "gpu_bwutil_python": 0.8624908399106359, + "gpu_gap_stddev_us": 0.827707701228162 + } + } + } + }, + "solarize_fake_planar_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "24x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Solarize RGBA8 fake-planar comparison (tensor-only, ~1-2ms)." + }, + "baselines": { + "solarize_fake_planar_uchar4_advanced[InOutDataType=uchar4][shape=24x1080x1920][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1586.59470141682, + "gpu_time_us_python": 1602.6472913931998, + "gpu_noise_us_cpp": 2.168, + "gpu_noise_us_python": 5.263999999999999, + "gpu_bwutil_cpp": 0.48405594165951094, + "gpu_bwutil_python": 0.47920727318514994, + "gpu_gap_stddev_us": 1.4425628876198333 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1289.872836102775, + "gpu_time_us_python": 1305.35797693235, + "gpu_noise_us_cpp": 2.7225, + "gpu_noise_us_python": 3.01, + "gpu_bwutil_cpp": 0.45413527315876195, + "gpu_bwutil_python": 0.4487404381096707, + "gpu_gap_stddev_us": 4.620878931276038 + } + } + } + }, + "solarize_fake_planar_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Solarize RGBAf32 fake-planar comparison (tensor-only, ~1-2ms)." + }, + "baselines": { + "solarize_fake_planar_float4_advanced[InOutDataType=float4][shape=16x1080x1920][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2879.3208544281, + "gpu_time_us_python": 2897.10786041166, + "gpu_noise_us_cpp": 2.898, + "gpu_noise_us_python": 3.81, + "gpu_bwutil_cpp": 0.7112788462694795, + "gpu_bwutil_python": 0.7069119790127186, + "gpu_gap_stddev_us": 1.3219475759419168 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2342.683287981475, + "gpu_time_us_python": 2356.802055379975, + "gpu_noise_us_cpp": 4.62, + "gpu_noise_us_python": 5.42, + "gpu_bwutil_cpp": 0.666772626954362, + "gpu_bwutil_python": 0.6627782474953778, + "gpu_gap_stddev_us": 3.684089913044668 + } + } + } + }, + "solarize_uint16_advanced": { + "tier": "advanced", + "dtypes": [ + "uint16" + ], + "string_axes": { + "shape": [ + "96x1080x1920" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Solarize U16 1-channel interleaved, Tensor + VarShape (single-channel lives in advanced per RGB guideline R3)." + }, + "baselines": { + "solarize_uint16_advanced[InOutDataType=uint16][shape=96x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 578.04586232116, + "gpu_time_us_python": 586.37771551488, + "gpu_noise_us_cpp": 1.0960000000000003, + "gpu_noise_us_python": 2.4560000000000004, + "gpu_bwutil_cpp": 0.8857429352352654, + "gpu_bwutil_python": 0.8731580226126106, + "gpu_gap_stddev_us": 0.626668742149005 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 443.54018625725, + "gpu_time_us_python": 451.362556498025, + "gpu_noise_us_cpp": 1.085, + "gpu_noise_us_python": 2.6325, + "gpu_bwutil_cpp": 0.8804365795209546, + "gpu_bwutil_python": 0.8651775965684387, + "gpu_gap_stddev_us": 0.42109756940270693 + } + }, + "solarize_uint16_advanced[InOutDataType=uint16][shape=96x1080x1920][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1547.08446028788, + "gpu_time_us_python": 1560.07727300616, + "gpu_noise_us_cpp": 2.6320000000000006, + "gpu_noise_us_python": 3.5160000000000005, + "gpu_bwutil_cpp": 0.33094538502368903, + "gpu_bwutil_python": 0.3281893425926515, + "gpu_gap_stddev_us": 1.4296427673658432 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1502.7556254266751, + "gpu_time_us_python": 1515.277891128175, + "gpu_noise_us_cpp": 5.0875, + "gpu_noise_us_python": 5.0024999999999995, + "gpu_bwutil_cpp": 0.2598705020245271, + "gpu_bwutil_python": 0.25772255086827583, + "gpu_gap_stddev_us": 1.0358145385250521 + } + } + } + } + } +} diff --git a/bench/config/operators/stack.json b/bench/config/operators/stack.json new file mode 100644 index 000000000..fd1859e92 --- /dev/null +++ b/bench/config/operators/stack.json @@ -0,0 +1,820 @@ +{ + "benchmark": "stack", + "configs": { + "stack_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "32x2160x3840" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Stack RGB8 tensor/var-shape layouts (basic)." + }, + "baselines": { + "stack_uchar3_basic[InOutDataType=uchar3][shape=32x2160x3840][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1165.53398298658, + "gpu_time_us_python": 1176.7132369264, + "gpu_noise_us_cpp": 2.496, + "gpu_noise_us_python": 2.444, + "gpu_bwutil_cpp": 0.8785673589222462, + "gpu_bwutil_python": 0.8702217510750827, + "gpu_gap_stddev_us": 1.7483239382939937 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 894.835557980025, + "gpu_time_us_python": 908.236959001125, + "gpu_noise_us_cpp": 3.0949999999999998, + "gpu_noise_us_python": 5.9925, + "gpu_bwutil_cpp": 0.8728144283097025, + "gpu_bwutil_python": 0.859956237036176, + "gpu_gap_stddev_us": 4.486583157176259 + } + }, + "stack_uchar3_basic[InOutDataType=uchar3][shape=32x2160x3840][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1165.6721023071002, + "gpu_time_us_python": 1177.3896745408201, + "gpu_noise_us_cpp": 2.426, + "gpu_noise_us_python": 3.4, + "gpu_bwutil_cpp": 0.8784633030280048, + "gpu_bwutil_python": 0.8697227834895582, + "gpu_gap_stddev_us": 2.4065144786925083 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 895.9037947376501, + "gpu_time_us_python": 906.655670991425, + "gpu_noise_us_cpp": 1.97, + "gpu_noise_us_python": 9.4525, + "gpu_bwutil_cpp": 0.8717794519407484, + "gpu_bwutil_python": 0.8614404882689227, + "gpu_gap_stddev_us": 2.0056016572968884 + } + }, + "stack_uchar3_basic[InOutDataType=uchar3][shape=32x2160x3840][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1154.8237579758202, + "gpu_time_us_python": 1164.77287902382, + "gpu_noise_us_cpp": 1.8980000000000001, + "gpu_noise_us_python": 2.71, + "gpu_bwutil_cpp": 0.8867154967381123, + "gpu_bwutil_python": 0.8791414658867949, + "gpu_gap_stddev_us": 0.30337503309402664 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 880.923402749, + "gpu_time_us_python": 890.04945541335, + "gpu_noise_us_cpp": 1.73, + "gpu_noise_us_python": 4.5325, + "gpu_bwutil_cpp": 0.886590403869275, + "gpu_bwutil_python": 0.8775059962739091, + "gpu_gap_stddev_us": 2.172292446517574 + } + }, + "stack_uchar3_basic[InOutDataType=uchar3][shape=32x2160x3840][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1150.30805922426, + "gpu_time_us_python": 1160.2270928797402, + "gpu_noise_us_cpp": 1.5, + "gpu_noise_us_python": 2.158, + "gpu_bwutil_cpp": 0.8901963299948894, + "gpu_bwutil_python": 0.8825859675875121, + "gpu_gap_stddev_us": 0.611033721095115 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 877.850779158975, + "gpu_time_us_python": 889.9781435011249, + "gpu_noise_us_cpp": 1.6325, + "gpu_noise_us_python": 2.5425, + "gpu_bwutil_cpp": 0.8896924721660284, + "gpu_bwutil_python": 0.8775928060626924, + "gpu_gap_stddev_us": 5.9056116624001485 + } + } + } + }, + "stack_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "16x2160x3840" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Stack RGBA8 (advanced)." + }, + "baselines": { + "stack_uchar4_advanced[InOutDataType=uchar4][shape=16x2160x3840][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 777.01375373398, + "gpu_time_us_python": 787.42379123602, + "gpu_noise_us_cpp": 2.3600000000000003, + "gpu_noise_us_python": 2.952, + "gpu_bwutil_cpp": 0.8785774140558187, + "gpu_bwutil_python": 0.8669638573160429, + "gpu_gap_stddev_us": 1.3996437699283726 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 596.9633273587999, + "gpu_time_us_python": 607.251104448075, + "gpu_noise_us_cpp": 3.51, + "gpu_noise_us_python": 4.82, + "gpu_bwutil_cpp": 0.8722183686297269, + "gpu_bwutil_python": 0.8574619074806291, + "gpu_gap_stddev_us": 2.710862412624438 + } + }, + "stack_uchar4_advanced[InOutDataType=uchar4][shape=16x2160x3840][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 776.5404531248602, + "gpu_time_us_python": 786.9641425798601, + "gpu_noise_us_cpp": 1.7239999999999998, + "gpu_noise_us_python": 3.9579999999999997, + "gpu_bwutil_cpp": 0.8791132117465933, + "gpu_bwutil_python": 0.8674698943127475, + "gpu_gap_stddev_us": 1.116189926806867 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 596.605081234175, + "gpu_time_us_python": 607.14987285055, + "gpu_noise_us_cpp": 1.57, + "gpu_noise_us_python": 3.8875, + "gpu_bwutil_cpp": 0.8727399583801723, + "gpu_bwutil_python": 0.8576014143958499, + "gpu_gap_stddev_us": 2.519482148855591 + } + } + } + }, + "stack_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "64x2160x3840" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Stack single-channel (advanced)." + }, + "baselines": { + "stack_scalar_advanced[InOutDataType=uint8][shape=64x2160x3840][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 811.28106827098, + "gpu_time_us_python": 823.1586938370001, + "gpu_noise_us_cpp": 2.4720000000000004, + "gpu_noise_us_python": 4.726000000000001, + "gpu_bwutil_cpp": 0.8414675653442968, + "gpu_bwutil_python": 0.8293291364892511, + "gpu_gap_stddev_us": 1.9366869095276351 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 632.689848595375, + "gpu_time_us_python": 643.35254694405, + "gpu_noise_us_cpp": 4.494999999999999, + "gpu_noise_us_python": 4.78, + "gpu_bwutil_cpp": 0.8230445262594368, + "gpu_bwutil_python": 0.8093895291310607, + "gpu_gap_stddev_us": 3.9495210960911407 + } + }, + "stack_scalar_advanced[InOutDataType=uint8][shape=64x2160x3840][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 811.71287023774, + "gpu_time_us_python": 823.1350232690199, + "gpu_noise_us_cpp": 4.31, + "gpu_noise_us_python": 4.174, + "gpu_bwutil_cpp": 0.8410199458436651, + "gpu_bwutil_python": 0.8293544332269136, + "gpu_gap_stddev_us": 2.385495519568572 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 632.9291133477, + "gpu_time_us_python": 644.771974940875, + "gpu_noise_us_cpp": 4.285, + "gpu_noise_us_python": 8.015, + "gpu_bwutil_cpp": 0.8227591056138241, + "gpu_bwutil_python": 0.8075885831049765, + "gpu_gap_stddev_us": 3.804685428510398 + } + } + } + }, + "stack_multi_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "8x2160x3840" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Stack RGBf32 tensor/var-shape layouts (basic)." + }, + "baselines": { + "stack_multi_float3_basic[InOutDataType=float3][shape=8x2160x3840][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1151.68526664782, + "gpu_time_us_python": 1161.9553817622002, + "gpu_noise_us_cpp": 2.7479999999999998, + "gpu_noise_us_python": 3.5700000000000003, + "gpu_bwutil_cpp": 0.8891318497050411, + "gpu_bwutil_python": 0.8812734296941402, + "gpu_gap_stddev_us": 0.888319049488295 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 875.684951047725, + "gpu_time_us_python": 887.306637542975, + "gpu_noise_us_cpp": 1.505, + "gpu_noise_us_python": 3.2725, + "gpu_bwutil_cpp": 0.8918939256495086, + "gpu_bwutil_python": 0.8802171436713727, + "gpu_gap_stddev_us": 1.7068383164283665 + } + }, + "stack_multi_float3_basic[InOutDataType=float3][shape=8x2160x3840][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1151.3508293606199, + "gpu_time_us_python": 1161.23972148802, + "gpu_noise_us_cpp": 2.814, + "gpu_noise_us_python": 2.364, + "gpu_bwutil_cpp": 0.8893901323418112, + "gpu_bwutil_python": 0.8818166175236902, + "gpu_gap_stddev_us": 1.005019367045287 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 876.50389716055, + "gpu_time_us_python": 886.8957279288251, + "gpu_noise_us_cpp": 1.6724999999999999, + "gpu_noise_us_python": 2.9225, + "gpu_bwutil_cpp": 0.8910608820019796, + "gpu_bwutil_python": 0.8806239831513845, + "gpu_gap_stddev_us": 1.688164767681041 + } + }, + "stack_multi_float3_basic[InOutDataType=float3][shape=8x2160x3840][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1148.7044400031, + "gpu_time_us_python": 1158.1686482384798, + "gpu_noise_us_cpp": 3.086, + "gpu_noise_us_python": 2.654, + "gpu_bwutil_cpp": 0.8914391048668027, + "gpu_bwutil_python": 0.8841546954795522, + "gpu_gap_stddev_us": 0.4781049941441192 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 872.97290662225, + "gpu_time_us_python": 882.083263065, + "gpu_noise_us_cpp": 1.305, + "gpu_noise_us_python": 2.655, + "gpu_bwutil_cpp": 0.8946635470361401, + "gpu_bwutil_python": 0.8854242682611744, + "gpu_gap_stddev_us": 1.3630746263833262 + } + }, + "stack_multi_float3_basic[InOutDataType=float3][shape=8x2160x3840][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1148.58418757758, + "gpu_time_us_python": 1158.0047916935202, + "gpu_noise_us_cpp": 2.208, + "gpu_noise_us_python": 2.6540000000000004, + "gpu_bwutil_cpp": 0.8915324339037589, + "gpu_bwutil_python": 0.8842799337254537, + "gpu_gap_stddev_us": 0.8130025366025515 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 872.9577416534249, + "gpu_time_us_python": 883.153812219775, + "gpu_noise_us_cpp": 1.1425, + "gpu_noise_us_python": 2.6775, + "gpu_bwutil_cpp": 0.8946792952403341, + "gpu_bwutil_python": 0.8843560269443733, + "gpu_gap_stddev_us": 2.276942172524283 + } + } + } + }, + "stack_multi_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "4x2160x3840" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Stack RGBAf32 multi (advanced)." + }, + "baselines": { + "stack_multi_float4_advanced[InOutDataType=float4][shape=4x2160x3840][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 770.7934677416999, + "gpu_time_us_python": 780.3572508418199, + "gpu_noise_us_cpp": 1.866, + "gpu_noise_us_python": 2.166, + "gpu_bwutil_cpp": 0.8856674356643234, + "gpu_bwutil_python": 0.8748140092437422, + "gpu_gap_stddev_us": 0.8684290747756848 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 587.70948047785, + "gpu_time_us_python": 596.788688910475, + "gpu_noise_us_cpp": 1.0499999999999998, + "gpu_noise_us_python": 3.32, + "gpu_bwutil_cpp": 0.8859457001672083, + "gpu_bwutil_python": 0.8724685873849475, + "gpu_gap_stddev_us": 0.8778487956068941 + } + }, + "stack_multi_float4_advanced[InOutDataType=float4][shape=4x2160x3840][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 769.72977561212, + "gpu_time_us_python": 779.1705824377, + "gpu_noise_us_cpp": 1.7120000000000002, + "gpu_noise_us_python": 2.146, + "gpu_bwutil_cpp": 0.8868913666798635, + "gpu_bwutil_python": 0.8761461099587505, + "gpu_gap_stddev_us": 0.8747973078553921 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 586.8108684792, + "gpu_time_us_python": 596.4392175248499, + "gpu_noise_us_cpp": 1.4025, + "gpu_noise_us_python": 2.5749999999999997, + "gpu_bwutil_cpp": 0.8873019856703791, + "gpu_bwutil_python": 0.8729783811371716, + "gpu_gap_stddev_us": 1.1460475174096518 + } + } + } + }, + "stack_multi_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "32x2160x3840" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Stack single-channel multi (advanced)." + }, + "baselines": { + "stack_multi_scalar_advanced[InOutDataType=float32][shape=32x2160x3840][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1544.9201410198998, + "gpu_time_us_python": 1557.6095969039602, + "gpu_noise_us_cpp": 2.4899999999999998, + "gpu_noise_us_python": 4.544, + "gpu_bwutil_cpp": 0.8837567902946046, + "gpu_bwutil_python": 0.8765572445959542, + "gpu_gap_stddev_us": 1.885579763998959 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1183.195328377125, + "gpu_time_us_python": 1192.353443021525, + "gpu_noise_us_cpp": 5.59, + "gpu_noise_us_python": 4.3925, + "gpu_bwutil_cpp": 0.8801349397330959, + "gpu_bwutil_python": 0.8733664664352603, + "gpu_gap_stddev_us": 2.5174465382237194 + } + }, + "stack_multi_scalar_advanced[InOutDataType=float32][shape=32x2160x3840][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1544.8910639609398, + "gpu_time_us_python": 1557.06262277516, + "gpu_noise_us_cpp": 2.7199999999999998, + "gpu_noise_us_python": 5.617999999999999, + "gpu_bwutil_cpp": 0.8837732851325271, + "gpu_bwutil_python": 0.8768655587293448, + "gpu_gap_stddev_us": 1.6911527264903947 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1180.92997660075, + "gpu_time_us_python": 1193.9684289281502, + "gpu_noise_us_cpp": 3.645, + "gpu_noise_us_python": 4.0200000000000005, + "gpu_bwutil_cpp": 0.881817855487748, + "gpu_bwutil_python": 0.8721958706705466, + "gpu_gap_stddev_us": 2.3719112519511754 + } + } + } + }, + "stack_uint8_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "256x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Stack single-channel (advanced)." + }, + "baselines": { + "stack_uint8_1080p_advanced[InOutDataType=uint8][shape=256x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 982.8162082086401, + "gpu_time_us_python": 1003.32671816392, + "gpu_noise_us_cpp": 7.209999999999999, + "gpu_noise_us_python": 10.876, + "gpu_bwutil_cpp": 0.6946047200232548, + "gpu_bwutil_python": 0.6804548113684031, + "gpu_gap_stddev_us": 8.06029350666022 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 942.1710411578249, + "gpu_time_us_python": 966.0785360095749, + "gpu_noise_us_cpp": 19.84, + "gpu_noise_us_python": 30.46, + "gpu_bwutil_cpp": 0.5530703275240998, + "gpu_bwutil_python": 0.5391031205538691, + "gpu_gap_stddev_us": 14.223183259052027 + } + }, + "stack_uint8_1080p_advanced[InOutDataType=uint8][shape=256x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 986.14082450684, + "gpu_time_us_python": 1003.4030574014399, + "gpu_noise_us_cpp": 4.938, + "gpu_noise_us_python": 8.364, + "gpu_bwutil_cpp": 0.6922630074795603, + "gpu_bwutil_python": 0.6803869181347781, + "gpu_gap_stddev_us": 6.687614712240164 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 943.2378350138999, + "gpu_time_us_python": 962.1804057564, + "gpu_noise_us_cpp": 15.055, + "gpu_noise_us_python": 18.384999999999998, + "gpu_bwutil_cpp": 0.5524337608394971, + "gpu_bwutil_python": 0.5414438130199757, + "gpu_gap_stddev_us": 10.812857269015348 + } + } + } + }, + "stack_multi_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3", + "float32", + "uchar4" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Stack single-channel/RGB/RGBA multi (advanced)." + }, + "baselines": { + "stack_multi_1080p_advanced[InOutDataType=float32][shape=128x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1612.73349559826, + "gpu_time_us_python": 1628.84667953802, + "gpu_noise_us_cpp": 6.6, + "gpu_noise_us_python": 7.386, + "gpu_bwutil_cpp": 0.8465963267712338, + "gpu_bwutil_python": 0.8382233808529163, + "gpu_gap_stddev_us": 3.374340601829655 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1253.9479790098999, + "gpu_time_us_python": 1265.87318709465, + "gpu_noise_us_cpp": 6.797499999999999, + "gpu_noise_us_python": 9.9025, + "gpu_bwutil_cpp": 0.8305606413448059, + "gpu_bwutil_python": 0.8227168033602303, + "gpu_gap_stddev_us": 1.8504914752030448 + } + }, + "stack_multi_1080p_advanced[InOutDataType=uchar3][shape=128x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1242.34397792332, + "gpu_time_us_python": 1256.56461044288, + "gpu_noise_us_cpp": 6.168000000000001, + "gpu_noise_us_python": 4.8759999999999994, + "gpu_bwutil_cpp": 0.8242536689608133, + "gpu_bwutil_python": 0.8149274751083982, + "gpu_gap_stddev_us": 4.627881608811844 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 979.5214198467, + "gpu_time_us_python": 990.375155035825, + "gpu_noise_us_cpp": 7.5625, + "gpu_noise_us_python": 9.9225, + "gpu_bwutil_cpp": 0.7974657578411052, + "gpu_bwutil_python": 0.788682153743581, + "gpu_gap_stddev_us": 2.9633706207693273 + } + }, + "stack_multi_1080p_advanced[InOutDataType=uchar4][shape=128x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1613.7324274858001, + "gpu_time_us_python": 1627.7450772074403, + "gpu_noise_us_cpp": 6.389999999999999, + "gpu_noise_us_python": 5.646, + "gpu_bwutil_cpp": 0.8460732814847457, + "gpu_bwutil_python": 0.8387894429428482, + "gpu_gap_stddev_us": 2.8997563039478 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1251.705317918825, + "gpu_time_us_python": 1264.905170044175, + "gpu_noise_us_cpp": 3.7725, + "gpu_noise_us_python": 6.495000000000001, + "gpu_bwutil_cpp": 0.8320427201927827, + "gpu_bwutil_python": 0.8233316015120737, + "gpu_gap_stddev_us": 2.7250612988260174 + } + }, + "stack_multi_1080p_advanced[InOutDataType=float32][shape=128x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1614.50198670472, + "gpu_time_us_python": 1627.29823604804, + "gpu_noise_us_cpp": 8.17, + "gpu_noise_us_python": 5.224, + "gpu_bwutil_cpp": 0.845670531081882, + "gpu_bwutil_python": 0.8390192711674864, + "gpu_gap_stddev_us": 2.354875039683899 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1254.4218185167501, + "gpu_time_us_python": 1265.744119937275, + "gpu_noise_us_cpp": 7.279999999999999, + "gpu_noise_us_python": 5.614999999999999, + "gpu_bwutil_cpp": 0.8302497074592314, + "gpu_bwutil_python": 0.8228022073812425, + "gpu_gap_stddev_us": 2.3980225485605935 + } + }, + "stack_multi_1080p_advanced[InOutDataType=uchar3][shape=128x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1300.1876225126202, + "gpu_time_us_python": 1312.06704353748, + "gpu_noise_us_cpp": 7.816, + "gpu_noise_us_python": 4.196, + "gpu_bwutil_cpp": 0.7875804579052692, + "gpu_bwutil_python": 0.7804502461156315, + "gpu_gap_stddev_us": 4.14728553164596 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1225.089594411025, + "gpu_time_us_python": 1239.2558435199, + "gpu_noise_us_cpp": 6.69, + "gpu_noise_us_python": 8.5725, + "gpu_bwutil_cpp": 0.6375698161849036, + "gpu_bwutil_python": 0.6302682769632756, + "gpu_gap_stddev_us": 3.444333444142253 + } + }, + "stack_multi_1080p_advanced[InOutDataType=uchar4][shape=128x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1700.50678469536, + "gpu_time_us_python": 1714.9744030591198, + "gpu_noise_us_cpp": 10.016000000000002, + "gpu_noise_us_python": 5.042, + "gpu_bwutil_cpp": 0.80289802527616, + "gpu_bwutil_python": 0.7961270682819543, + "gpu_gap_stddev_us": 3.1532219032089284 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1597.9671226647501, + "gpu_time_us_python": 1611.3374725972499, + "gpu_noise_us_cpp": 4.0, + "gpu_noise_us_python": 8.195, + "gpu_bwutil_cpp": 0.651720179381901, + "gpu_bwutil_python": 0.6463246029053437, + "gpu_gap_stddev_us": 3.123193735916925 + } + } + } + }, + "stack_uchar3_layout_compare_4k_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "4x2160x3840" + ], + "layout": [ + "NHWC", + "NCHW", + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Stack RGB8 TensorBatch interleaved/native/fake-planar 4K comparison at N=4 (advanced)." + }, + "baselines": { + "stack_uchar3_layout_compare_4k_advanced[InOutDataType=uchar3][shape=4x2160x3840][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 154.61205357756, + "gpu_time_us_python": 163.56320721624, + "gpu_noise_us_cpp": 1.9260000000000002, + "gpu_noise_us_python": 2.382, + "gpu_bwutil_cpp": 0.827879150488336, + "gpu_bwutil_python": 0.7825738637889168, + "gpu_gap_stddev_us": 0.1997564661136821 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 121.750927237825, + "gpu_time_us_python": 130.66580340452498, + "gpu_noise_us_cpp": 1.6400000000000001, + "gpu_noise_us_python": 2.5275, + "gpu_bwutil_cpp": 0.8018884244136989, + "gpu_bwutil_python": 0.7471667131128408, + "gpu_gap_stddev_us": 0.47063205486732496 + } + }, + "stack_uchar3_layout_compare_4k_advanced[InOutDataType=uchar3][shape=4x2160x3840][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 785.99831590966, + "gpu_time_us_python": 811.23463078982, + "gpu_noise_us_cpp": 3.2560000000000002, + "gpu_noise_us_python": 5.4159999999999995, + "gpu_bwutil_cpp": 0.48855101874238993, + "gpu_bwutil_python": 0.47335628244829675, + "gpu_gap_stddev_us": 2.6371694656249476 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 629.262455816375, + "gpu_time_us_python": 649.263121725525, + "gpu_noise_us_cpp": 1.9175, + "gpu_noise_us_python": 3.2925, + "gpu_bwutil_cpp": 0.4654387395823721, + "gpu_bwutil_python": 0.45110150912197117, + "gpu_gap_stddev_us": 1.1425171401218708 + } + }, + "stack_uchar3_layout_compare_4k_advanced[InOutDataType=uchar3][shape=4x2160x3840][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 154.18363880377998, + "gpu_time_us_python": 163.49338864274, + "gpu_noise_us_cpp": 1.3279999999999998, + "gpu_noise_us_python": 2.652, + "gpu_bwutil_cpp": 0.8301792828509574, + "gpu_bwutil_python": 0.7829094089626244, + "gpu_gap_stddev_us": 0.35568533927593593 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 122.001859095525, + "gpu_time_us_python": 131.35485512230002, + "gpu_noise_us_cpp": 1.4124999999999999, + "gpu_noise_us_python": 2.775, + "gpu_bwutil_cpp": 0.8002449126460426, + "gpu_bwutil_python": 0.7433389775551922, + "gpu_gap_stddev_us": 1.3311132710054494 + } + } + } + } + } +} diff --git a/bench/config/operators/threshold.json b/bench/config/operators/threshold.json new file mode 100644 index 000000000..f3ae203bc --- /dev/null +++ b/bench/config/operators/threshold.json @@ -0,0 +1,322 @@ +{ + "benchmark": "threshold", + "configs": { + "threshold_uint8_basic": { + "tier": "basic", + "dtypes": [ + "uint8" + ], + "string_axes": { + "shape": [ + "256x1080x1920" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Threshold single-channel uint8 interleaved (basic)." + }, + "baselines": { + "threshold_uint8_basic[InOutDataType=uint8][shape=256x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1735.9940515578, + "gpu_time_us_python": 1748.1818798294996, + "gpu_noise_us_cpp": 2.12, + "gpu_noise_us_python": 4.51, + "gpu_bwutil_cpp": 0.3932436223117123, + "gpu_bwutil_python": 0.39050215776903785, + "gpu_gap_stddev_us": 1.7248323602757796 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1652.458484629825, + "gpu_time_us_python": 1664.188189058475, + "gpu_noise_us_cpp": 1.7825000000000002, + "gpu_noise_us_python": 3.1224999999999996, + "gpu_bwutil_cpp": 0.3151219946203439, + "gpu_bwutil_python": 0.31290218354548827, + "gpu_gap_stddev_us": 1.9644279444684611 + } + }, + "threshold_uint8_basic[InOutDataType=uint8][shape=256x1080x1920][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1289.5798255903, + "gpu_time_us_python": 1306.8399737141401, + "gpu_noise_us_cpp": 3.38, + "gpu_noise_us_python": 4.072, + "gpu_bwutil_cpp": 0.5293714715749651, + "gpu_bwutil_python": 0.522380141369706, + "gpu_gap_stddev_us": 1.2021378572241874 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1069.4564720225499, + "gpu_time_us_python": 1090.455545729825, + "gpu_noise_us_cpp": 4.5375, + "gpu_noise_us_python": 7.7325, + "gpu_bwutil_cpp": 0.48686736246439166, + "gpu_bwutil_python": 0.47749527960127064, + "gpu_gap_stddev_us": 4.952329967109745 + } + } + } + }, + "threshold_float32_basic": { + "tier": "basic", + "dtypes": [ + "float32" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Threshold single-channel float32 interleaved (basic)." + }, + "baselines": { + "threshold_float32_basic[InOutDataType=float32][shape=128x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1529.2902726952202, + "gpu_time_us_python": 1543.81212819966, + "gpu_noise_us_cpp": 2.924, + "gpu_noise_us_python": 3.47, + "gpu_bwutil_cpp": 0.8927888911445624, + "gpu_bwutil_python": 0.884393763617414, + "gpu_gap_stddev_us": 3.0958414098523113 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1162.845333746425, + "gpu_time_us_python": 1176.3118143092502, + "gpu_noise_us_cpp": 2.7725, + "gpu_noise_us_python": 3.59, + "gpu_bwutil_cpp": 0.895524418906048, + "gpu_bwutil_python": 0.8852734971059029, + "gpu_gap_stddev_us": 1.0355826239953436 + } + }, + "threshold_float32_basic[InOutDataType=float32][shape=128x1080x1920][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1589.2091112800401, + "gpu_time_us_python": 1606.35142677864, + "gpu_noise_us_cpp": 4.606, + "gpu_noise_us_python": 5.828, + "gpu_bwutil_cpp": 0.8591277765518669, + "gpu_bwutil_python": 0.849960696618773, + "gpu_gap_stddev_us": 2.9719284449159082 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 1213.490127331775, + "gpu_time_us_python": 1232.88792599085, + "gpu_noise_us_cpp": 5.06, + "gpu_noise_us_python": 5.842499999999999, + "gpu_bwutil_cpp": 0.8581496314978942, + "gpu_bwutil_python": 0.8446530706258584, + "gpu_gap_stddev_us": 3.329668887496236 + } + } + } + }, + "threshold_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Threshold RGB8 interleaved + native planar (basic)." + }, + "baselines": { + "threshold_uchar3_basic[InOutDataType=uchar3][shape=64x1080x1920][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 909.0628851219801, + "gpu_time_us_python": 922.55005546764, + "gpu_noise_us_cpp": 1.8959999999999997, + "gpu_noise_us_python": 3.598, + "gpu_bwutil_cpp": 0.5632196946193999, + "gpu_bwutil_python": 0.5549883533492964, + "gpu_gap_stddev_us": 1.2132086474653612 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 914.0046068545249, + "gpu_time_us_python": 927.4484055987, + "gpu_noise_us_cpp": 1.75, + "gpu_noise_us_python": 4.2975, + "gpu_bwutil_cpp": 0.4273027453145592, + "gpu_bwutil_python": 0.421100051939344, + "gpu_gap_stddev_us": 1.0419407364018216 + } + }, + "threshold_uchar3_basic[InOutDataType=uchar3][shape=64x1080x1920][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 443.86740534062, + "gpu_time_us_python": 459.15150545405993, + "gpu_noise_us_cpp": 2.26, + "gpu_noise_us_python": 3.184, + "gpu_bwutil_cpp": 1.153499045182577, + "gpu_bwutil_python": 1.1151022922520348, + "gpu_gap_stddev_us": 0.43096574440847196 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 371.02962341192506, + "gpu_time_us_python": 385.730954477525, + "gpu_noise_us_cpp": 2.7125, + "gpu_noise_us_python": 5.1475, + "gpu_bwutil_cpp": 1.052532433963632, + "gpu_bwutil_python": 1.0124247561049478, + "gpu_gap_stddev_us": 1.8537230069824786 + } + }, + "threshold_uchar3_basic[InOutDataType=uchar3][shape=64x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3445.6954688241594, + "gpu_time_us_python": 3463.4552715757004, + "gpu_noise_us_cpp": 3.096, + "gpu_noise_us_python": 5.794, + "gpu_bwutil_cpp": 0.14859169196376038, + "gpu_bwutil_python": 0.14782994263457253, + "gpu_gap_stddev_us": 4.892290108087733 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 3410.50966022215, + "gpu_time_us_python": 3426.261063283625, + "gpu_noise_us_cpp": 2.865, + "gpu_noise_us_python": 12.495000000000001, + "gpu_bwutil_cpp": 0.11451586771036358, + "gpu_bwutil_python": 0.11398897985459555, + "gpu_gap_stddev_us": 0.8518446098221016 + } + }, + "threshold_uchar3_basic[InOutDataType=uchar3][shape=64x1080x1920][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4041.1184375614002, + "gpu_time_us_python": 4057.4116768375197, + "gpu_noise_us_cpp": 4.0280000000000005, + "gpu_noise_us_python": 5.246, + "gpu_bwutil_cpp": 0.1266981334647656, + "gpu_bwutil_python": 0.12618943411503958, + "gpu_gap_stddev_us": 0.874473862607912 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 3900.64247785075, + "gpu_time_us_python": 3921.060800380375, + "gpu_noise_us_cpp": 3.1875, + "gpu_noise_us_python": 6.34, + "gpu_bwutil_cpp": 0.10012615239535502, + "gpu_bwutil_python": 0.09960378296593914, + "gpu_gap_stddev_us": 2.561642992496283 + } + } + } + }, + "threshold_fakeplanar_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "layout": [ + "NCHW", + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "Threshold RGB8 fake-planar comparison (tensor-only)." + }, + "baselines": { + "threshold_fakeplanar_uchar3_advanced[InOutDataType=uchar3][shape=64x1080x1920][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3446.7110464017205, + "gpu_time_us_python": 3462.3729151042403, + "gpu_noise_us_cpp": 3.2380000000000004, + "gpu_noise_us_python": 4.508, + "gpu_bwutil_cpp": 0.14854798137816633, + "gpu_bwutil_python": 0.1478760318403714, + "gpu_gap_stddev_us": 2.869073877023882 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 3410.52114825265, + "gpu_time_us_python": 3425.752343576175, + "gpu_noise_us_cpp": 3.135, + "gpu_noise_us_python": 4.574999999999999, + "gpu_bwutil_cpp": 0.11451549755968757, + "gpu_bwutil_python": 0.11400660157449369, + "gpu_gap_stddev_us": 1.2560064499520416 + } + }, + "threshold_fakeplanar_uchar3_advanced[InOutDataType=uchar3][shape=64x1080x1920][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3280.77625918696, + "gpu_time_us_python": 3299.27145305428, + "gpu_noise_us_cpp": 1.3219999999999998, + "gpu_noise_us_python": 2.944, + "gpu_bwutil_cpp": 0.15606083221701578, + "gpu_bwutil_python": 0.15518602420303154, + "gpu_gap_stddev_us": 1.642486618865842 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 4, + "gpu_time_us_cpp": 2764.8500200099, + "gpu_time_us_python": 2779.534788664375, + "gpu_noise_us_cpp": 2.0425, + "gpu_noise_us_python": 3.5500000000000003, + "gpu_bwutil_cpp": 0.1412451082128433, + "gpu_bwutil_python": 0.14049879733607634, + "gpu_gap_stddev_us": 1.0347688275565199 + } + } + } + } + } +} diff --git a/bench/config/operators/warpaffine.json b/bench/config/operators/warpaffine.json new file mode 100644 index 000000000..e422caa0e --- /dev/null +++ b/bench/config/operators/warpaffine.json @@ -0,0 +1,1414 @@ +{ + "benchmark": "warpaffine", + "configs": { + "warpaffine_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT" + ], + "interpolation": [ + "CUBIC" + ], + "inverseMap": [ + "Y" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "WarpAffine RGB8 (basic)." + }, + "baselines": { + "warpaffine_uchar3_basic[InOutDataType=uchar3][shape=16x1080x1920][border=REFLECT][interpolation=CUBIC][inverseMap=Y][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2041.57965125102, + "gpu_time_us_python": 2058.92148838662, + "gpu_noise_us_cpp": 1.084, + "gpu_noise_us_python": 4.016, + "gpu_bwutil_cpp": 0.06269657246230456, + "gpu_bwutil_python": 0.06216852220648132, + "gpu_gap_stddev_us": 1.045284418520899 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1743.37195707014, + "gpu_time_us_python": 1760.54152060612, + "gpu_noise_us_cpp": 1.864, + "gpu_noise_us_python": 4.302, + "gpu_bwutil_cpp": 0.056009069510216024, + "gpu_bwutil_python": 0.055461870630644616, + "gpu_gap_stddev_us": 1.3311522236221094 + } + }, + "warpaffine_uchar3_basic[InOutDataType=uchar3][shape=16x1080x1920][border=REFLECT][interpolation=CUBIC][inverseMap=Y][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2199.15143523294, + "gpu_time_us_python": 2220.95992459656, + "gpu_noise_us_cpp": 1.6, + "gpu_noise_us_python": 4.279999999999999, + "gpu_bwutil_cpp": 0.05820430135404882, + "gpu_bwutil_python": 0.05763290401119039, + "gpu_gap_stddev_us": 3.0561740879139676 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1911.59522306092, + "gpu_time_us_python": 1932.9369996829398, + "gpu_noise_us_cpp": 2.166, + "gpu_noise_us_python": 3.9799999999999995, + "gpu_bwutil_cpp": 0.05108039701661558, + "gpu_bwutil_python": 0.050515202564172676, + "gpu_gap_stddev_us": 3.797848647381082 + } + } + } + }, + "warpaffine_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT" + ], + "interpolation": [ + "CUBIC" + ], + "inverseMap": [ + "Y" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "WarpAffine RGBA8 (advanced)." + }, + "baselines": { + "warpaffine_uchar4_advanced[InOutDataType=uchar4][shape=16x1080x1920][border=REFLECT][interpolation=CUBIC][inverseMap=Y][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1804.5090359749, + "gpu_time_us_python": 1822.851265048, + "gpu_noise_us_cpp": 1.2899999999999998, + "gpu_noise_us_python": 5.1, + "gpu_bwutil_cpp": 0.09457791556965396, + "gpu_bwutil_python": 0.09362654019201333, + "gpu_gap_stddev_us": 3.258952058793939 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1502.7182247982, + "gpu_time_us_python": 1519.94639246598, + "gpu_noise_us_cpp": 1.068, + "gpu_noise_us_python": 3.964, + "gpu_bwutil_cpp": 0.08663792360390107, + "gpu_bwutil_python": 0.08565581622602951, + "gpu_gap_stddev_us": 1.1270285420462691 + } + }, + "warpaffine_uchar4_advanced[InOutDataType=uchar4][shape=16x1080x1920][border=REFLECT][interpolation=CUBIC][inverseMap=Y][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1899.8256826039003, + "gpu_time_us_python": 1920.37072697732, + "gpu_noise_us_cpp": 1.698, + "gpu_noise_us_python": 3.07, + "gpu_bwutil_cpp": 0.08983284837967116, + "gpu_bwutil_python": 0.08887209749013215, + "gpu_gap_stddev_us": 3.0656269598966372 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1594.4283247796, + "gpu_time_us_python": 1614.7652079493, + "gpu_noise_us_cpp": 2.2, + "gpu_noise_us_python": 3.87, + "gpu_bwutil_cpp": 0.08165461181394015, + "gpu_bwutil_python": 0.08062577826596946, + "gpu_gap_stddev_us": 1.1225301020422203 + } + } + } + }, + "warpaffine_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT" + ], + "interpolation": [ + "CUBIC" + ], + "inverseMap": [ + "Y" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "WarpAffine RGBf32 (basic)." + }, + "baselines": { + "warpaffine_float3_basic[InOutDataType=float3][shape=16x1080x1920][border=REFLECT][interpolation=CUBIC][inverseMap=Y][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4918.625276228899, + "gpu_time_us_python": 4939.3142438402, + "gpu_noise_us_cpp": 3.186, + "gpu_noise_us_python": 3.812, + "gpu_bwutil_cpp": 0.10409413351834503, + "gpu_bwutil_python": 0.10365817060184276, + "gpu_gap_stddev_us": 4.4891691815874495 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4414.41821097168, + "gpu_time_us_python": 4435.35591222966, + "gpu_noise_us_cpp": 2.102, + "gpu_noise_us_python": 5.465999999999999, + "gpu_bwutil_cpp": 0.08847739151258804, + "gpu_bwutil_python": 0.08806066406540844, + "gpu_gap_stddev_us": 4.158986354342525 + } + } + } + }, + "warpaffine_float3_varshape_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT" + ], + "interpolation": [ + "CUBIC" + ], + "inverseMap": [ + "Y" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "WarpAffine RGBf32 VarShape (advanced)." + }, + "baselines": { + "warpaffine_float3_varshape_advanced[InOutDataType=float3][shape=16x1080x1920][border=REFLECT][interpolation=CUBIC][inverseMap=Y][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4997.296694213221, + "gpu_time_us_python": 5020.262359619081, + "gpu_noise_us_cpp": 1.7280000000000002, + "gpu_noise_us_python": 2.8320000000000003, + "gpu_bwutil_cpp": 0.1024554007987281, + "gpu_bwutil_python": 0.10198675518324388, + "gpu_gap_stddev_us": 4.392156328592153 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4465.89008498006, + "gpu_time_us_python": 4491.497934880361, + "gpu_noise_us_cpp": 2.642, + "gpu_noise_us_python": 7.802, + "gpu_bwutil_cpp": 0.08745786369853284, + "gpu_bwutil_python": 0.08695598443244898, + "gpu_gap_stddev_us": 10.528276729910719 + } + } + } + }, + "warpaffine_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT" + ], + "interpolation": [ + "CUBIC" + ], + "inverseMap": [ + "Y" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "WarpAffine RGBAf32 (advanced)." + }, + "baselines": { + "warpaffine_float4_advanced[InOutDataType=float4][shape=16x1080x1920][border=REFLECT][interpolation=CUBIC][inverseMap=Y][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2567.6758206619998, + "gpu_time_us_python": 2584.8166745962, + "gpu_noise_us_cpp": 5.242, + "gpu_noise_us_python": 5.474, + "gpu_bwutil_cpp": 0.2658695060601769, + "gpu_bwutil_python": 0.26410653478859836, + "gpu_gap_stddev_us": 1.5373483102008272 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2225.9064224504, + "gpu_time_us_python": 2244.25681742296, + "gpu_noise_us_cpp": 4.0040000000000004, + "gpu_noise_us_python": 5.686, + "gpu_bwutil_cpp": 0.23393102166221094, + "gpu_bwutil_python": 0.23201549622759637, + "gpu_gap_stddev_us": 2.381376927889071 + } + }, + "warpaffine_float4_advanced[InOutDataType=float4][shape=16x1080x1920][border=REFLECT][interpolation=CUBIC][inverseMap=Y][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2607.98726677888, + "gpu_time_us_python": 2631.0315338195, + "gpu_noise_us_cpp": 3.0, + "gpu_noise_us_python": 5.598, + "gpu_bwutil_cpp": 0.2617599986932925, + "gpu_bwutil_python": 0.2594677411319487, + "gpu_gap_stddev_us": 3.108878812289067 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2320.67872375576, + "gpu_time_us_python": 2342.96837021404, + "gpu_noise_us_cpp": 1.752, + "gpu_noise_us_python": 6.558, + "gpu_bwutil_cpp": 0.2243939524829285, + "gpu_bwutil_python": 0.22225962421519557, + "gpu_gap_stddev_us": 1.3879108337437756 + } + } + } + }, + "warpaffine_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT" + ], + "interpolation": [ + "CUBIC" + ], + "inverseMap": [ + "Y" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "WarpAffine single-channel (advanced)." + }, + "baselines": { + "warpaffine_scalar_advanced[InOutDataType=float32][shape=16x1080x1920][border=REFLECT][interpolation=CUBIC][inverseMap=Y][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1507.0615438093198, + "gpu_time_us_python": 1524.9442142965802, + "gpu_noise_us_cpp": 1.048, + "gpu_noise_us_python": 4.682, + "gpu_bwutil_cpp": 0.11324468702014381, + "gpu_bwutil_python": 0.11191681416925721, + "gpu_gap_stddev_us": 1.3810753694149502 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1205.2373926129198, + "gpu_time_us_python": 1223.5619136078199, + "gpu_noise_us_cpp": 0.9920000000000002, + "gpu_noise_us_python": 3.832, + "gpu_bwutil_cpp": 0.10802228295630853, + "gpu_bwutil_python": 0.1064069963028792, + "gpu_gap_stddev_us": 3.1933419597542994 + } + }, + "warpaffine_scalar_advanced[InOutDataType=float32][shape=16x1080x1920][border=REFLECT][interpolation=CUBIC][inverseMap=Y][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1619.5174626380801, + "gpu_time_us_python": 1641.2627980456, + "gpu_noise_us_cpp": 1.416, + "gpu_noise_us_python": 2.7800000000000002, + "gpu_bwutil_cpp": 0.10538124334649175, + "gpu_bwutil_python": 0.10398542110771972, + "gpu_gap_stddev_us": 3.793027081831025 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1366.1976492539, + "gpu_time_us_python": 1387.77678218438, + "gpu_noise_us_cpp": 1.6199999999999999, + "gpu_noise_us_python": 4.336, + "gpu_bwutil_cpp": 0.09529614934404754, + "gpu_bwutil_python": 0.09381533160640776, + "gpu_gap_stddev_us": 3.5894035429880797 + } + }, + "warpaffine_scalar_advanced[InOutDataType=uint8][shape=16x1080x1920][border=REFLECT][interpolation=CUBIC][inverseMap=Y][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1304.70936931664, + "gpu_time_us_python": 1321.9849283201597, + "gpu_noise_us_cpp": 1.1720000000000002, + "gpu_noise_us_python": 2.43, + "gpu_bwutil_cpp": 0.0327020828667635, + "gpu_bwutil_python": 0.03227477603005878, + "gpu_gap_stddev_us": 1.4419000981021486 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1089.5497862803, + "gpu_time_us_python": 1107.8447540583, + "gpu_noise_us_cpp": 0.8720000000000001, + "gpu_noise_us_python": 4.279999999999999, + "gpu_bwutil_cpp": 0.02987299870940708, + "gpu_bwutil_python": 0.029380142305521238, + "gpu_gap_stddev_us": 2.8926812336523025 + } + }, + "warpaffine_scalar_advanced[InOutDataType=uint8][shape=16x1080x1920][border=REFLECT][interpolation=CUBIC][inverseMap=Y][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1378.73161074874, + "gpu_time_us_python": 1399.7829281950399, + "gpu_noise_us_cpp": 1.656, + "gpu_noise_us_python": 4.564, + "gpu_bwutil_cpp": 0.03094635988194766, + "gpu_bwutil_python": 0.030481043798994738, + "gpu_gap_stddev_us": 2.435717624561884 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1247.03926912274, + "gpu_time_us_python": 1269.24822601542, + "gpu_noise_us_cpp": 1.544, + "gpu_noise_us_python": 5.746, + "gpu_bwutil_cpp": 0.026100484701862096, + "gpu_bwutil_python": 0.02564415566001012, + "gpu_gap_stddev_us": 2.644720184475796 + } + } + } + }, + "warpaffine_linear_forward_tensor_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "8x1080x1920" + ], + "border": [ + "CONSTANT" + ], + "interpolation": [ + "LINEAR" + ], + "inverseMap": [ + "N" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "WarpAffine single-channel linear forward tensor (advanced)." + }, + "baselines": { + "warpaffine_linear_forward_tensor_1080p_advanced[InOutDataType=float32][shape=8x1080x1920][border=CONSTANT][interpolation=LINEAR][inverseMap=N][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 316.75044361953996, + "gpu_time_us_python": 333.67845798112, + "gpu_noise_us_cpp": 1.0399999999999998, + "gpu_noise_us_python": 2.5620000000000003, + "gpu_bwutil_cpp": 0.26940870063703226, + "gpu_bwutil_python": 0.25576206515810884, + "gpu_gap_stddev_us": 2.3792499363040744 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 255.8461313928, + "gpu_time_us_python": 272.68946988454, + "gpu_noise_us_cpp": 1.3119999999999998, + "gpu_noise_us_python": 3.412, + "gpu_bwutil_cpp": 0.2544620140709616, + "gpu_bwutil_python": 0.23872881267091745, + "gpu_gap_stddev_us": 0.6800847031525936 + } + }, + "warpaffine_linear_forward_tensor_1080p_advanced[InOutDataType=uint8][shape=8x1080x1920][border=CONSTANT][interpolation=LINEAR][inverseMap=N][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 320.95609117628, + "gpu_time_us_python": 337.06281672816004, + "gpu_noise_us_cpp": 0.994, + "gpu_noise_us_python": 2.104, + "gpu_bwutil_cpp": 0.06646899677441917, + "gpu_bwutil_python": 0.0632943594673023, + "gpu_gap_stddev_us": 1.1354256374810663 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 262.01986434006005, + "gpu_time_us_python": 279.84143132326, + "gpu_noise_us_cpp": 0.9179999999999999, + "gpu_noise_us_python": 4.788, + "gpu_bwutil_cpp": 0.06210738651272034, + "gpu_bwutil_python": 0.058155513047282205, + "gpu_gap_stddev_us": 1.4534736875273508 + } + } + } + }, + "warpaffine_linear_forward_varshape_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "border": [ + "CONSTANT" + ], + "interpolation": [ + "LINEAR" + ], + "inverseMap": [ + "N" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "WarpAffine single-channel linear forward varshape (advanced)." + }, + "baselines": { + "warpaffine_linear_forward_varshape_1080p_advanced[InOutDataType=float32][shape=64x1080x1920][border=CONSTANT][interpolation=LINEAR][inverseMap=N][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2274.41003756084, + "gpu_time_us_python": 2297.0320142557803, + "gpu_noise_us_cpp": 1.364, + "gpu_noise_us_python": 5.741999999999999, + "gpu_bwutil_cpp": 0.30015115427512173, + "gpu_bwutil_python": 0.2971955553917976, + "gpu_gap_stddev_us": 2.3925973331621337 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2089.4906140075996, + "gpu_time_us_python": 2111.15230329724, + "gpu_noise_us_cpp": 1.828, + "gpu_noise_us_python": 3.5460000000000003, + "gpu_bwutil_cpp": 0.24923143633561518, + "gpu_bwutil_python": 0.24667185738559733, + "gpu_gap_stddev_us": 1.9916425636056034 + } + }, + "warpaffine_linear_forward_varshape_1080p_advanced[InOutDataType=uint8][shape=64x1080x1920][border=CONSTANT][interpolation=LINEAR][inverseMap=N][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2697.1455635562597, + "gpu_time_us_python": 2719.4156021701, + "gpu_noise_us_cpp": 1.57, + "gpu_noise_us_python": 4.101999999999999, + "gpu_bwutil_cpp": 0.0632767909970208, + "gpu_bwutil_python": 0.06275868336892007, + "gpu_gap_stddev_us": 2.6308168974093844 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2394.7283554984997, + "gpu_time_us_python": 2416.1155775803, + "gpu_noise_us_cpp": 1.7060000000000002, + "gpu_noise_us_python": 3.536, + "gpu_bwutil_cpp": 0.05436600903254056, + "gpu_bwutil_python": 0.05388437259336119, + "gpu_gap_stddev_us": 1.4278697226198482 + } + } + } + }, + "warpaffine_nearest_inverse_tensor_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "128x1080x1920" + ], + "border": [ + "REPLICATE" + ], + "interpolation": [ + "NEAREST" + ], + "inverseMap": [ + "Y" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "WarpAffine single-channel nearest inverse tensor (advanced)." + }, + "baselines": { + "warpaffine_nearest_inverse_tensor_1080p_advanced[InOutDataType=float32][shape=128x1080x1920][border=REPLICATE][interpolation=NEAREST][inverseMap=Y][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1318.43774895916, + "gpu_time_us_python": 1333.96046453328, + "gpu_noise_us_cpp": 1.4440000000000002, + "gpu_noise_us_python": 1.9119999999999997, + "gpu_bwutil_cpp": 1.0355695650986494, + "gpu_bwutil_python": 1.0235192466669614, + "gpu_gap_stddev_us": 0.8225983849314865 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1237.38910348184, + "gpu_time_us_python": 1254.1032565158998, + "gpu_noise_us_cpp": 0.978, + "gpu_noise_us_python": 2.8459999999999996, + "gpu_bwutil_cpp": 0.8416649025955152, + "gpu_bwutil_python": 0.8304540114491132, + "gpu_gap_stddev_us": 1.0929613086269212 + } + }, + "warpaffine_nearest_inverse_tensor_1080p_advanced[InOutDataType=uint8][shape=128x1080x1920][border=REPLICATE][interpolation=NEAREST][inverseMap=Y][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1197.9398606496, + "gpu_time_us_python": 1214.38986721304, + "gpu_noise_us_cpp": 0.9359999999999999, + "gpu_noise_us_python": 2.6860000000000004, + "gpu_bwutil_cpp": 0.28493376997490616, + "gpu_bwutil_python": 0.28107431583521086, + "gpu_gap_stddev_us": 0.6743553843047759 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1156.15151569652, + "gpu_time_us_python": 1173.19145646166, + "gpu_noise_us_cpp": 1.736, + "gpu_noise_us_python": 4.645999999999999, + "gpu_bwutil_cpp": 0.22521281060512086, + "gpu_bwutil_python": 0.2219437404584816, + "gpu_gap_stddev_us": 1.2562168012496884 + } + } + } + }, + "warpaffine_nearest_inverse_varshape_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "border": [ + "REPLICATE" + ], + "interpolation": [ + "NEAREST" + ], + "inverseMap": [ + "Y" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "WarpAffine single-channel nearest inverse varshape (advanced)." + }, + "baselines": { + "warpaffine_nearest_inverse_varshape_1080p_advanced[InOutDataType=float32][shape=64x1080x1920][border=REPLICATE][interpolation=NEAREST][inverseMap=Y][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1057.9466529032802, + "gpu_time_us_python": 1078.9715131345201, + "gpu_noise_us_cpp": 1.5, + "gpu_noise_us_python": 3.56, + "gpu_bwutil_cpp": 0.6452759511466388, + "gpu_bwutil_python": 0.6327058558927332, + "gpu_gap_stddev_us": 3.3314231140657977 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1016.2093846641201, + "gpu_time_us_python": 1040.7649689647799, + "gpu_noise_us_cpp": 2.34, + "gpu_noise_us_python": 6.156000000000001, + "gpu_bwutil_cpp": 0.5124418399029926, + "gpu_bwutil_python": 0.500332949453526, + "gpu_gap_stddev_us": 5.366216331474318 + } + }, + "warpaffine_nearest_inverse_varshape_1080p_advanced[InOutDataType=uint8][shape=64x1080x1920][border=REPLICATE][interpolation=NEAREST][inverseMap=Y][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1027.1492039570599, + "gpu_time_us_python": 1048.12070223714, + "gpu_noise_us_cpp": 1.6420000000000001, + "gpu_noise_us_python": 5.294, + "gpu_bwutil_cpp": 0.16615605638699765, + "gpu_bwutil_python": 0.16283269823674643, + "gpu_gap_stddev_us": 2.494528695978771 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 989.8907927713401, + "gpu_time_us_python": 1011.7460790642801, + "gpu_noise_us_cpp": 3.38, + "gpu_noise_us_python": 6.248, + "gpu_bwutil_cpp": 0.1315178535290873, + "gpu_bwutil_python": 0.1286721650830098, + "gpu_gap_stddev_us": 3.7605402322353196 + } + } + } + }, + "warpaffine_planar_nchw_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT" + ], + "interpolation": [ + "CUBIC" + ], + "inverseMap": [ + "Y" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "WarpAffine uchar3 planar NCHW (RGB-standard)." + }, + "baselines": { + "warpaffine_planar_nchw_uchar3_basic[InOutDataType=uchar3][shape=16x1080x1920][border=REFLECT][interpolation=CUBIC][inverseMap=Y][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2732.82232595522, + "gpu_time_us_python": 2754.24472379498, + "gpu_noise_us_cpp": 1.4340000000000002, + "gpu_noise_us_python": 8.687999999999999, + "gpu_bwutil_cpp": 0.046838035278959, + "gpu_bwutil_python": 0.046474009174134155, + "gpu_gap_stddev_us": 7.222620894293659 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2337.45480431428, + "gpu_time_us_python": 2355.82445557508, + "gpu_noise_us_cpp": 1.984, + "gpu_noise_us_python": 3.65, + "gpu_bwutil_cpp": 0.041772810334479285, + "gpu_bwutil_python": 0.04144707655995334, + "gpu_gap_stddev_us": 2.6971425690462594 + } + }, + "warpaffine_planar_nchw_uchar3_basic[InOutDataType=uchar3][shape=16x1080x1920][border=REFLECT][interpolation=CUBIC][inverseMap=Y][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2944.30564243716, + "gpu_time_us_python": 2965.9328213859603, + "gpu_noise_us_cpp": 4.368, + "gpu_noise_us_python": 4.024, + "gpu_bwutil_cpp": 0.04347380612246738, + "gpu_bwutil_python": 0.04315680078791991, + "gpu_gap_stddev_us": 3.4573227888290505 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2427.1467292608804, + "gpu_time_us_python": 2450.3301024777197, + "gpu_noise_us_cpp": 1.698, + "gpu_noise_us_python": 3.444, + "gpu_bwutil_cpp": 0.04022948125557334, + "gpu_bwutil_python": 0.03985035891694834, + "gpu_gap_stddev_us": 6.496887936550237 + } + } + } + }, + "warpaffine_planar_nchw_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT" + ], + "interpolation": [ + "CUBIC" + ], + "inverseMap": [ + "Y" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "WarpAffine RGB8 native-planar Tensor comparison (advanced)." + }, + "baselines": { + "warpaffine_planar_nchw_uchar3_advanced[InOutDataType=uchar3][shape=16x1080x1920][border=REFLECT][interpolation=CUBIC][inverseMap=Y][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2731.8406146505404, + "gpu_time_us_python": 2752.6725354629207, + "gpu_noise_us_cpp": 1.716, + "gpu_noise_us_python": 6.0699999999999985, + "gpu_bwutil_cpp": 0.04685487757541072, + "gpu_bwutil_python": 0.04650059509845244, + "gpu_gap_stddev_us": 7.021818916508744 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2337.1663874606998, + "gpu_time_us_python": 2357.33392948846, + "gpu_noise_us_cpp": 1.7420000000000002, + "gpu_noise_us_python": 4.4, + "gpu_bwutil_cpp": 0.041777915585830896, + "gpu_bwutil_python": 0.04142120819126888, + "gpu_gap_stddev_us": 4.368675158357251 + } + } + } + }, + "warpaffine_fake_planar_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT" + ], + "interpolation": [ + "CUBIC" + ], + "inverseMap": [ + "Y" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "WarpAffine uchar3 fake-planar reformat-warp-reformat baseline." + }, + "baselines": { + "warpaffine_fake_planar_uchar3_advanced[InOutDataType=uchar3][shape=16x1080x1920][border=REFLECT][interpolation=CUBIC][inverseMap=Y][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2645.2787083073404, + "gpu_time_us_python": 2662.06443035318, + "gpu_noise_us_cpp": 1.504, + "gpu_noise_us_python": 5.367999999999999, + "gpu_bwutil_cpp": 0.14516445915862344, + "gpu_bwutil_python": 0.14424911679809674, + "gpu_gap_stddev_us": 1.5018264084182202 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2219.0117627418795, + "gpu_time_us_python": 2233.0187011295993, + "gpu_noise_us_cpp": 4.394, + "gpu_noise_us_python": 2.668, + "gpu_bwutil_cpp": 0.13200340021270182, + "gpu_bwutil_python": 0.1311763379979352, + "gpu_gap_stddev_us": 1.8654393060159442 + } + } + } + }, + "warpaffine_planar_nchw_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT" + ], + "interpolation": [ + "CUBIC" + ], + "inverseMap": [ + "Y" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "WarpAffine uchar4 planar NCHW tensor-only (RGBA8p varshape unsupported by the Python image API) (RGB-standard)." + }, + "baselines": { + "warpaffine_planar_nchw_uchar4_advanced[InOutDataType=uchar4][shape=16x1080x1920][border=REFLECT][interpolation=CUBIC][inverseMap=Y][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3506.40923976892, + "gpu_time_us_python": 3525.06081554248, + "gpu_noise_us_cpp": 2.412, + "gpu_noise_us_python": 2.7560000000000002, + "gpu_bwutil_cpp": 0.04867278301568176, + "gpu_bwutil_python": 0.04841526664576522, + "gpu_gap_stddev_us": 1.8669325747154188 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2954.78615818886, + "gpu_time_us_python": 2973.94707074852, + "gpu_noise_us_cpp": 2.11, + "gpu_noise_us_python": 3.6020000000000003, + "gpu_bwutil_cpp": 0.04406066815323408, + "gpu_bwutil_python": 0.04377728027893877, + "gpu_gap_stddev_us": 4.052052142399136 + } + } + } + }, + "warpaffine_fake_planar_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT" + ], + "interpolation": [ + "CUBIC" + ], + "inverseMap": [ + "Y" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "WarpAffine uchar4 fake-planar reformat-warp-reformat baseline." + }, + "baselines": { + "warpaffine_fake_planar_uchar4_advanced[InOutDataType=uchar4][shape=16x1080x1920][border=REFLECT][interpolation=CUBIC][inverseMap=Y][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2580.912872688, + "gpu_time_us_python": 2597.7866472656797, + "gpu_noise_us_cpp": 1.932, + "gpu_noise_us_python": 2.5759999999999996, + "gpu_bwutil_cpp": 0.19837968001772688, + "gpu_bwutil_python": 0.19709120110945139, + "gpu_gap_stddev_us": 1.0983790576643269 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2117.9625419236404, + "gpu_time_us_python": 2133.86877644616, + "gpu_noise_us_cpp": 1.128, + "gpu_noise_us_python": 6.322000000000001, + "gpu_bwutil_cpp": 0.18440028920762147, + "gpu_bwutil_python": 0.18302472411111098, + "gpu_gap_stddev_us": 1.908986977957417 + } + } + } + }, + "warpaffine_planar_nchw_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT" + ], + "interpolation": [ + "CUBIC" + ], + "inverseMap": [ + "Y" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "WarpAffine float3 planar NCHW (RGB-standard)." + }, + "baselines": { + "warpaffine_planar_nchw_float3_basic[InOutDataType=float3][shape=16x1080x1920][border=REFLECT][interpolation=CUBIC][inverseMap=Y][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4508.71231385636, + "gpu_time_us_python": 4527.60504143576, + "gpu_noise_us_cpp": 2.028, + "gpu_noise_us_python": 3.3619999999999997, + "gpu_bwutil_cpp": 0.11355792867515306, + "gpu_bwutil_python": 0.11308408800821827, + "gpu_gap_stddev_us": 1.8190107896455079 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3604.7220880193004, + "gpu_time_us_python": 3623.8026199059996, + "gpu_noise_us_cpp": 1.222, + "gpu_noise_us_python": 3.668, + "gpu_bwutil_cpp": 0.10835115815787384, + "gpu_bwutil_python": 0.10777955481962873, + "gpu_gap_stddev_us": 2.302343159782529 + } + } + } + }, + "warpaffine_planar_nchw_float3_varshape_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT" + ], + "interpolation": [ + "CUBIC" + ], + "inverseMap": [ + "Y" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "WarpAffine float3 planar NCHW (RGB-standard)." + }, + "baselines": { + "warpaffine_planar_nchw_float3_varshape_advanced[InOutDataType=float3][shape=16x1080x1920][border=REFLECT][interpolation=CUBIC][inverseMap=Y][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4962.09517273242, + "gpu_time_us_python": 4985.69532469202, + "gpu_noise_us_cpp": 2.572, + "gpu_noise_us_python": 7.395999999999999, + "gpu_bwutil_cpp": 0.10318225661011719, + "gpu_bwutil_python": 0.10269390401433125, + "gpu_gap_stddev_us": 2.9499462144430826 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4301.523248793021, + "gpu_time_us_python": 4323.44493169238, + "gpu_noise_us_cpp": 3.2560000000000002, + "gpu_noise_us_python": 4.402, + "gpu_bwutil_cpp": 0.09079933676455963, + "gpu_bwutil_python": 0.09033940898381951, + "gpu_gap_stddev_us": 3.4011505418206416 + } + } + } + }, + "warpaffine_planar_nchw_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT" + ], + "interpolation": [ + "CUBIC" + ], + "inverseMap": [ + "Y" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "WarpAffine RGBf32 native-planar Tensor comparison (advanced)." + }, + "baselines": { + "warpaffine_planar_nchw_float3_advanced[InOutDataType=float3][shape=16x1080x1920][border=REFLECT][interpolation=CUBIC][inverseMap=Y][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4508.116199289, + "gpu_time_us_python": 4527.67454215454, + "gpu_noise_us_cpp": 2.116, + "gpu_noise_us_python": 3.904, + "gpu_bwutil_cpp": 0.11357294510955614, + "gpu_bwutil_python": 0.11308237319629474, + "gpu_gap_stddev_us": 2.187313595518417 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3604.9030603547194, + "gpu_time_us_python": 3624.5142347351393, + "gpu_noise_us_cpp": 1.1480000000000001, + "gpu_noise_us_python": 3.71, + "gpu_bwutil_cpp": 0.10834574852000307, + "gpu_bwutil_python": 0.10775932429683639, + "gpu_gap_stddev_us": 1.9304139973309729 + } + } + } + }, + "warpaffine_fake_planar_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT" + ], + "interpolation": [ + "CUBIC" + ], + "inverseMap": [ + "Y" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "WarpAffine float3 fake-planar reformat-warp-reformat baseline." + }, + "baselines": { + "warpaffine_fake_planar_float3_advanced[InOutDataType=float3][shape=16x1080x1920][border=REFLECT][interpolation=CUBIC][inverseMap=Y][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6251.96271937064, + "gpu_time_us_python": 6271.99996471398, + "gpu_noise_us_cpp": 1.9759999999999998, + "gpu_noise_us_python": 4.808, + "gpu_bwutil_cpp": 0.24568296717450572, + "gpu_bwutil_python": 0.2448980989282606, + "gpu_gap_stddev_us": 1.1848758452302088 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5507.85473132474, + "gpu_time_us_python": 5524.0894047649, + "gpu_noise_us_cpp": 1.8739999999999999, + "gpu_noise_us_python": 6.236, + "gpu_bwutil_cpp": 0.2127247498827458, + "gpu_bwutil_python": 0.2121008724023426, + "gpu_gap_stddev_us": 2.155971997200262 + } + } + } + }, + "warpaffine_planar_nchw_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT" + ], + "interpolation": [ + "CUBIC" + ], + "inverseMap": [ + "Y" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "WarpAffine float4 planar NCHW (RGB-standard)." + }, + "baselines": { + "warpaffine_planar_nchw_float4_advanced[InOutDataType=float4][shape=16x1080x1920][border=REFLECT][interpolation=CUBIC][inverseMap=Y][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6009.890179406981, + "gpu_time_us_python": 6029.92152940656, + "gpu_noise_us_cpp": 2.672, + "gpu_noise_us_python": 7.702000000000001, + "gpu_bwutil_cpp": 0.11359055042563179, + "gpu_bwutil_python": 0.11321330636336573, + "gpu_gap_stddev_us": 6.200654817024535 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4805.06459569718, + "gpu_time_us_python": 4826.6144306808, + "gpu_noise_us_cpp": 1.8259999999999998, + "gpu_noise_us_python": 4.736, + "gpu_bwutil_cpp": 0.10837906290944335, + "gpu_bwutil_python": 0.10789339256400261, + "gpu_gap_stddev_us": 4.972994001489226 + } + }, + "warpaffine_planar_nchw_float4_advanced[InOutDataType=float4][shape=16x1080x1920][border=REFLECT][interpolation=CUBIC][inverseMap=Y][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 6614.107576169439, + "gpu_time_us_python": 6639.675273393261, + "gpu_noise_us_cpp": 1.738, + "gpu_noise_us_python": 8.854, + "gpu_bwutil_cpp": 0.10321374421245388, + "gpu_bwutil_python": 0.10281632551886304, + "gpu_gap_stddev_us": 2.796322153956738 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 5731.031740795419, + "gpu_time_us_python": 5757.880913177921, + "gpu_noise_us_cpp": 2.304, + "gpu_noise_us_python": 9.225999999999999, + "gpu_bwutil_cpp": 0.09086782718650552, + "gpu_bwutil_python": 0.09044234381335581, + "gpu_gap_stddev_us": 5.238270420956939 + } + } + } + }, + "warpaffine_fake_planar_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT" + ], + "interpolation": [ + "CUBIC" + ], + "inverseMap": [ + "Y" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "WarpAffine float4 fake-planar reformat-warp-reformat baseline." + }, + "baselines": { + "warpaffine_fake_planar_float4_advanced[InOutDataType=float4][shape=16x1080x1920][border=REFLECT][interpolation=CUBIC][inverseMap=Y][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4661.90411073186, + "gpu_time_us_python": 4681.500456068219, + "gpu_noise_us_cpp": 5.794, + "gpu_noise_us_python": 5.988000000000001, + "gpu_bwutil_cpp": 0.43930765763071566, + "gpu_bwutil_python": 0.43746837888554857, + "gpu_gap_stddev_us": 1.621299930733529 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3971.7044201870995, + "gpu_time_us_python": 3987.5360830671802, + "gpu_noise_us_cpp": 5.718, + "gpu_noise_us_python": 6.824, + "gpu_bwutil_cpp": 0.39329722203351614, + "gpu_bwutil_python": 0.3917360600406934, + "gpu_gap_stddev_us": 2.5556994962998787 + } + } + } + } + } +} diff --git a/bench/config/operators/warpperspective.json b/bench/config/operators/warpperspective.json new file mode 100644 index 000000000..c646b7985 --- /dev/null +++ b/bench/config/operators/warpperspective.json @@ -0,0 +1,1383 @@ +{ + "benchmark": "warpperspective", + "configs": { + "warpperspective_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT" + ], + "interpolation": [ + "CUBIC" + ], + "inverseMap": [ + "Y" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "WarpPerspective RGB8 (basic)." + }, + "baselines": { + "warpperspective_uchar3_basic[InOutDataType=uchar3][shape=16x1080x1920][border=REFLECT][interpolation=CUBIC][inverseMap=Y][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1590.79693721816, + "gpu_time_us_python": 1603.04180976414, + "gpu_noise_us_cpp": 2.29, + "gpu_noise_us_python": 3.0, + "gpu_bwutil_cpp": 0.08046284337287762, + "gpu_bwutil_python": 0.07984826588125916, + "gpu_gap_stddev_us": 0.8957076449179396 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1333.9024158551201, + "gpu_time_us_python": 1346.0219510449401, + "gpu_noise_us_cpp": 2.716, + "gpu_noise_us_python": 3.526, + "gpu_bwutil_cpp": 0.07320199719862172, + "gpu_bwutil_python": 0.07254231316299904, + "gpu_gap_stddev_us": 0.8507051029533244 + } + }, + "warpperspective_uchar3_basic[InOutDataType=uchar3][shape=16x1080x1920][border=REFLECT][interpolation=CUBIC][inverseMap=Y][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1830.5442465482404, + "gpu_time_us_python": 1851.41439222816, + "gpu_noise_us_cpp": 2.7559999999999993, + "gpu_noise_us_python": 5.144, + "gpu_bwutil_cpp": 0.0699246016025201, + "gpu_bwutil_python": 0.06913652755622095, + "gpu_gap_stddev_us": 2.2726977149412266 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1534.3140648302, + "gpu_time_us_python": 1554.5230976103198, + "gpu_noise_us_cpp": 2.088, + "gpu_noise_us_python": 3.7120000000000006, + "gpu_bwutil_cpp": 0.06364075785645942, + "gpu_bwutil_python": 0.06281425142692251, + "gpu_gap_stddev_us": 2.6159915041934902 + } + } + } + }, + "warpperspective_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT" + ], + "interpolation": [ + "CUBIC" + ], + "inverseMap": [ + "Y" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "WarpPerspective RGBA8 (advanced)." + }, + "baselines": { + "warpperspective_uchar4_advanced[InOutDataType=uchar4][shape=16x1080x1920][border=REFLECT][interpolation=CUBIC][inverseMap=Y][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1644.3248039653797, + "gpu_time_us_python": 1656.479241265, + "gpu_noise_us_cpp": 2.41, + "gpu_noise_us_python": 3.686, + "gpu_bwutil_cpp": 0.1037913757515101, + "gpu_bwutil_python": 0.10302981759780498, + "gpu_gap_stddev_us": 0.4334778124751494 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1497.1976993338799, + "gpu_time_us_python": 1510.44143069152, + "gpu_noise_us_cpp": 0.992, + "gpu_noise_us_python": 3.582, + "gpu_bwutil_cpp": 0.08695797646453239, + "gpu_bwutil_python": 0.08619389881807724, + "gpu_gap_stddev_us": 1.4971211299522942 + } + }, + "warpperspective_uchar4_advanced[InOutDataType=uchar4][shape=16x1080x1920][border=REFLECT][interpolation=CUBIC][inverseMap=Y][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1711.44364053674, + "gpu_time_us_python": 1732.0455957212203, + "gpu_noise_us_cpp": 2.566, + "gpu_noise_us_python": 4.058, + "gpu_bwutil_cpp": 0.09972094241891712, + "gpu_bwutil_python": 0.09853507114400097, + "gpu_gap_stddev_us": 2.1784271063261142 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1544.1547290856001, + "gpu_time_us_python": 1567.39981152584, + "gpu_noise_us_cpp": 1.292, + "gpu_noise_us_python": 5.048, + "gpu_bwutil_cpp": 0.08431377525441387, + "gpu_bwutil_python": 0.0830639551391731, + "gpu_gap_stddev_us": 3.5538096626890825 + } + } + } + }, + "warpperspective_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT" + ], + "interpolation": [ + "CUBIC" + ], + "inverseMap": [ + "Y" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "WarpPerspective RGBf32 (basic)." + }, + "baselines": { + "warpperspective_float3_basic[InOutDataType=float3][shape=16x1080x1920][border=REFLECT][interpolation=CUBIC][inverseMap=Y][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1318.5908094204801, + "gpu_time_us_python": 1330.59115277524, + "gpu_noise_us_cpp": 1.964, + "gpu_noise_us_python": 3.12, + "gpu_bwutil_cpp": 0.38829342894856567, + "gpu_bwutil_python": 0.38479180583786776, + "gpu_gap_stddev_us": 1.1482324249769444 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1274.71982034346, + "gpu_time_us_python": 1289.33504608336, + "gpu_noise_us_cpp": 2.818, + "gpu_noise_us_python": 4.4719999999999995, + "gpu_bwutil_cpp": 0.30640297892109164, + "gpu_bwutil_python": 0.3029281010927045, + "gpu_gap_stddev_us": 1.546517710356758 + } + } + } + }, + "warpperspective_float3_varshape_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT" + ], + "interpolation": [ + "CUBIC" + ], + "inverseMap": [ + "Y" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "WarpPerspective RGBf32 VarShape (advanced)." + }, + "baselines": { + "warpperspective_float3_varshape_advanced[InOutDataType=float3][shape=16x1080x1920][border=REFLECT][interpolation=CUBIC][inverseMap=Y][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1557.64903015224, + "gpu_time_us_python": 1581.2128902515801, + "gpu_noise_us_cpp": 2.3240000000000003, + "gpu_noise_us_python": 5.622, + "gpu_bwutil_cpp": 0.3287006727875778, + "gpu_bwutil_python": 0.323805419401093, + "gpu_gap_stddev_us": 4.914403596337167 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1472.9852522055799, + "gpu_time_us_python": 1493.1279188657602, + "gpu_noise_us_cpp": 2.928, + "gpu_noise_us_python": 5.51, + "gpu_bwutil_cpp": 0.26516289464672144, + "gpu_bwutil_python": 0.2615866036977746, + "gpu_gap_stddev_us": 1.5748909227480798 + } + } + } + }, + "warpperspective_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT" + ], + "interpolation": [ + "CUBIC" + ], + "inverseMap": [ + "Y" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "WarpPerspective RGBAf32 (advanced)." + }, + "baselines": { + "warpperspective_float4_advanced[InOutDataType=float4][shape=16x1080x1920][border=REFLECT][interpolation=CUBIC][inverseMap=Y][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1129.55504376604, + "gpu_time_us_python": 1143.0579137878199, + "gpu_noise_us_cpp": 1.168, + "gpu_noise_us_python": 2.824, + "gpu_bwutil_cpp": 0.6043679294765443, + "gpu_bwutil_python": 0.5972341997281344, + "gpu_gap_stddev_us": 3.6012643488794556 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1136.9419146519, + "gpu_time_us_python": 1150.92648845264, + "gpu_noise_us_cpp": 1.3519999999999999, + "gpu_noise_us_python": 4.922, + "gpu_bwutil_cpp": 0.45804338494985963, + "gpu_bwutil_python": 0.4524897738803073, + "gpu_gap_stddev_us": 3.3582082524789856 + } + }, + "warpperspective_float4_advanced[InOutDataType=float4][shape=16x1080x1920][border=REFLECT][interpolation=CUBIC][inverseMap=Y][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1324.22267444546, + "gpu_time_us_python": 1346.43774378278, + "gpu_noise_us_cpp": 1.286, + "gpu_noise_us_python": 3.1959999999999997, + "gpu_bwutil_cpp": 0.5155227731892603, + "gpu_bwutil_python": 0.507027524663483, + "gpu_gap_stddev_us": 6.3655609860758755 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1310.90653264134, + "gpu_time_us_python": 1331.75487055082, + "gpu_noise_us_cpp": 2.3899999999999997, + "gpu_noise_us_python": 4.22, + "gpu_bwutil_cpp": 0.397260184582247, + "gpu_bwutil_python": 0.3910467775699975, + "gpu_gap_stddev_us": 3.6267488867276487 + } + } + } + }, + "warpperspective_scalar_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "border": [ + "REFLECT" + ], + "interpolation": [ + "CUBIC" + ], + "inverseMap": [ + "Y" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "WarpPerspective single-channel (advanced)." + }, + "baselines": { + "warpperspective_scalar_advanced[InOutDataType=float32][shape=32x1080x1920][border=REFLECT][interpolation=CUBIC][inverseMap=Y][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1904.15123484348, + "gpu_time_us_python": 1916.14477288626, + "gpu_noise_us_cpp": 2.508, + "gpu_noise_us_python": 2.2540000000000004, + "gpu_bwutil_cpp": 0.1792575093442003, + "gpu_bwutil_python": 0.1781355846608344, + "gpu_gap_stddev_us": 0.9521107476205827 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1964.9341263555202, + "gpu_time_us_python": 1983.807655976, + "gpu_noise_us_cpp": 1.172, + "gpu_noise_us_python": 4.328, + "gpu_bwutil_cpp": 0.13251655466969114, + "gpu_bwutil_python": 0.1312468595160549, + "gpu_gap_stddev_us": 11.496257545645884 + } + }, + "warpperspective_scalar_advanced[InOutDataType=float32][shape=32x1080x1920][border=REFLECT][interpolation=CUBIC][inverseMap=Y][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2140.0007842952, + "gpu_time_us_python": 2160.31867306804, + "gpu_noise_us_cpp": 1.864, + "gpu_noise_us_python": 3.284, + "gpu_bwutil_cpp": 0.15950153889019286, + "gpu_bwutil_python": 0.15800158688039256, + "gpu_gap_stddev_us": 1.7589819253235166 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2102.78768641618, + "gpu_time_us_python": 2127.5101996992403, + "gpu_noise_us_cpp": 5.0200000000000005, + "gpu_noise_us_python": 6.25, + "gpu_bwutil_cpp": 0.12382467537280617, + "gpu_bwutil_python": 0.12238218486983661, + "gpu_gap_stddev_us": 5.632897324058227 + } + }, + "warpperspective_scalar_advanced[InOutDataType=uint8][shape=32x1080x1920][border=REFLECT][interpolation=CUBIC][inverseMap=Y][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2007.80024681088, + "gpu_time_us_python": 2020.6525810302799, + "gpu_noise_us_cpp": 1.238, + "gpu_noise_us_python": 2.638, + "gpu_bwutil_cpp": 0.04250092608466024, + "gpu_bwutil_python": 0.04223061928041818, + "gpu_gap_stddev_us": 1.1293795291387032 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2017.10769999722, + "gpu_time_us_python": 2036.884306942, + "gpu_noise_us_cpp": 0.796, + "gpu_noise_us_python": 4.564, + "gpu_bwutil_cpp": 0.0322721319752072, + "gpu_bwutil_python": 0.03195698454175594, + "gpu_gap_stddev_us": 11.837142882322532 + } + }, + "warpperspective_scalar_advanced[InOutDataType=uint8][shape=32x1080x1920][border=REFLECT][interpolation=CUBIC][inverseMap=Y][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2276.91606391556, + "gpu_time_us_python": 2298.0069451614204, + "gpu_noise_us_cpp": 1.5699999999999998, + "gpu_noise_us_python": 4.683999999999999, + "gpu_bwutil_cpp": 0.037477607465757304, + "gpu_bwutil_python": 0.0371337363398605, + "gpu_gap_stddev_us": 3.3738932787844873 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2195.49261467968, + "gpu_time_us_python": 2223.8809651587403, + "gpu_noise_us_cpp": 1.9840000000000004, + "gpu_noise_us_python": 5.954000000000001, + "gpu_bwutil_cpp": 0.02965010482907432, + "gpu_bwutil_python": 0.02926979263458624, + "gpu_gap_stddev_us": 12.491787576072772 + } + } + } + }, + "warpperspective_linear_forward_tensor_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "CONSTANT" + ], + "interpolation": [ + "LINEAR" + ], + "inverseMap": [ + "N" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "WarpPerspective single-channel linear forward tensor (advanced)." + }, + "baselines": { + "warpperspective_linear_forward_tensor_1080p_advanced[InOutDataType=float32][shape=16x1080x1920][border=CONSTANT][interpolation=LINEAR][inverseMap=N][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 594.9007981161001, + "gpu_time_us_python": 607.7246679899399, + "gpu_noise_us_cpp": 1.076, + "gpu_noise_us_python": 2.3099999999999996, + "gpu_bwutil_cpp": 0.2869106228549977, + "gpu_bwutil_python": 0.28087158470446594, + "gpu_gap_stddev_us": 2.3495025364693936 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 498.54692085272, + "gpu_time_us_python": 512.03610282528, + "gpu_noise_us_cpp": 0.942, + "gpu_noise_us_python": 3.108, + "gpu_bwutil_cpp": 0.26115386778299815, + "gpu_bwutil_python": 0.2542680318740876, + "gpu_gap_stddev_us": 2.2299769548485973 + } + }, + "warpperspective_linear_forward_tensor_1080p_advanced[InOutDataType=uint8][shape=16x1080x1920][border=CONSTANT][interpolation=LINEAR][inverseMap=N][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 556.57921670874, + "gpu_time_us_python": 569.9768936460999, + "gpu_noise_us_cpp": 1.338, + "gpu_noise_us_python": 2.434, + "gpu_bwutil_cpp": 0.07666592267095125, + "gpu_bwutil_python": 0.07486916768505575, + "gpu_gap_stddev_us": 2.620275273289855 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 515.3579924725, + "gpu_time_us_python": 528.3572773551, + "gpu_noise_us_cpp": 0.786, + "gpu_noise_us_python": 2.868, + "gpu_bwutil_cpp": 0.0631532894668849, + "gpu_bwutil_python": 0.06160098800810142, + "gpu_gap_stddev_us": 2.043624931215117 + } + } + } + }, + "warpperspective_linear_forward_varshape_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "border": [ + "CONSTANT" + ], + "interpolation": [ + "LINEAR" + ], + "inverseMap": [ + "N" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "WarpPerspective single-channel linear forward varshape (advanced)." + }, + "baselines": { + "warpperspective_linear_forward_varshape_1080p_advanced[InOutDataType=float32][shape=64x1080x1920][border=CONSTANT][interpolation=LINEAR][inverseMap=N][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1891.01846451144, + "gpu_time_us_python": 1911.5400909991004, + "gpu_noise_us_cpp": 1.5959999999999999, + "gpu_noise_us_python": 2.53, + "gpu_bwutil_cpp": 0.3610048207033599, + "gpu_bwutil_python": 0.35712970265509403, + "gpu_gap_stddev_us": 2.2978264853772576 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1877.56097057474, + "gpu_time_us_python": 1901.6635467821798, + "gpu_noise_us_cpp": 1.318, + "gpu_noise_us_python": 6.013999999999999, + "gpu_bwutil_cpp": 0.27734804773888655, + "gpu_bwutil_python": 0.27384325277327576, + "gpu_gap_stddev_us": 7.109245126737227 + } + }, + "warpperspective_linear_forward_varshape_1080p_advanced[InOutDataType=uint8][shape=64x1080x1920][border=CONSTANT][interpolation=LINEAR][inverseMap=N][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1956.5839964896, + "gpu_time_us_python": 1978.3793042024797, + "gpu_noise_us_cpp": 1.56, + "gpu_noise_us_python": 4.8, + "gpu_bwutil_cpp": 0.08722687963209877, + "gpu_bwutil_python": 0.08626612228361359, + "gpu_gap_stddev_us": 3.377484508991962 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1958.0722505353199, + "gpu_time_us_python": 1981.5246614681, + "gpu_noise_us_cpp": 1.276, + "gpu_noise_us_python": 6.478, + "gpu_bwutil_cpp": 0.06648617778045508, + "gpu_bwutil_python": 0.06570019040719506, + "gpu_gap_stddev_us": 4.199202754580383 + } + } + } + }, + "warpperspective_nearest_inverse_tensor_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REPLICATE" + ], + "interpolation": [ + "NEAREST" + ], + "inverseMap": [ + "Y" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "WarpPerspective single-channel nearest inverse tensor (advanced)." + }, + "baselines": { + "warpperspective_nearest_inverse_tensor_1080p_advanced[InOutDataType=float32][shape=16x1080x1920][border=REPLICATE][interpolation=NEAREST][inverseMap=Y][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 179.18625202152003, + "gpu_time_us_python": 190.98862811781999, + "gpu_noise_us_cpp": 0.9140000000000003, + "gpu_noise_us_python": 2.902, + "gpu_bwutil_cpp": 0.9524828226473705, + "gpu_bwutil_python": 0.8936734677784595, + "gpu_gap_stddev_us": 0.8938232045924165 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 183.76237527322, + "gpu_time_us_python": 196.07035097007997, + "gpu_noise_us_cpp": 1.166, + "gpu_noise_us_python": 2.828, + "gpu_bwutil_cpp": 0.7084648173102475, + "gpu_bwutil_python": 0.6639732128648086, + "gpu_gap_stddev_us": 2.214986255056303 + } + }, + "warpperspective_nearest_inverse_tensor_1080p_advanced[InOutDataType=uint8][shape=16x1080x1920][border=REPLICATE][interpolation=NEAREST][inverseMap=Y][layout=NHWC][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 174.37983617148, + "gpu_time_us_python": 185.76735212586, + "gpu_noise_us_cpp": 0.8160000000000001, + "gpu_noise_us_python": 1.842, + "gpu_bwutil_cpp": 0.24468421139866522, + "gpu_bwutil_python": 0.22968710308991605, + "gpu_gap_stddev_us": 0.7389647517341935 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 180.67159557781997, + "gpu_time_us_python": 192.12230141322, + "gpu_noise_us_cpp": 1.5859999999999999, + "gpu_noise_us_python": 3.0439999999999996, + "gpu_bwutil_cpp": 0.18014848288463428, + "gpu_bwutil_python": 0.16939366746163736, + "gpu_gap_stddev_us": 1.49199125260417 + } + } + } + }, + "warpperspective_nearest_inverse_varshape_1080p_advanced": { + "tier": "advanced", + "dtypes": [ + "uint8", + "float32" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "border": [ + "REPLICATE" + ], + "interpolation": [ + "NEAREST" + ], + "inverseMap": [ + "Y" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "WarpPerspective single-channel nearest inverse varshape (advanced)." + }, + "baselines": { + "warpperspective_nearest_inverse_varshape_1080p_advanced[InOutDataType=float32][shape=64x1080x1920][border=REPLICATE][interpolation=NEAREST][inverseMap=Y][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1050.8740873310799, + "gpu_time_us_python": 1076.06455581826, + "gpu_noise_us_cpp": 2.5940000000000003, + "gpu_noise_us_python": 4.024, + "gpu_bwutil_cpp": 0.6496206746041799, + "gpu_bwutil_python": 0.6344247950133151, + "gpu_gap_stddev_us": 4.261351681349347 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1086.6905862257197, + "gpu_time_us_python": 1108.8293596274202, + "gpu_noise_us_cpp": 2.12, + "gpu_noise_us_python": 4.787999999999999, + "gpu_bwutil_cpp": 0.479185327578563, + "gpu_bwutil_python": 0.4696302794844055, + "gpu_gap_stddev_us": 4.214490421372804 + } + }, + "warpperspective_nearest_inverse_varshape_1080p_advanced[InOutDataType=uint8][shape=64x1080x1920][border=REPLICATE][interpolation=NEAREST][inverseMap=Y][layout=NHWC][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1044.83453938356, + "gpu_time_us_python": 1065.18501016868, + "gpu_noise_us_cpp": 1.484, + "gpu_noise_us_python": 3.87, + "gpu_bwutil_cpp": 0.16334406411534313, + "gpu_bwutil_python": 0.16022535657412923, + "gpu_gap_stddev_us": 2.879891694447577 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1077.35596555014, + "gpu_time_us_python": 1097.17427281948, + "gpu_noise_us_cpp": 2.71, + "gpu_noise_us_python": 4.296, + "gpu_bwutil_cpp": 0.12083456723635233, + "gpu_bwutil_python": 0.11865502132059935, + "gpu_gap_stddev_us": 3.684820535211657 + } + } + } + }, + "warpperspective_planar_nchw_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT" + ], + "interpolation": [ + "CUBIC" + ], + "inverseMap": [ + "Y" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "WarpPerspective uchar3 NCHW (basic)." + }, + "baselines": { + "warpperspective_planar_nchw_uchar3_basic[InOutDataType=uchar3][shape=16x1080x1920][border=REFLECT][interpolation=CUBIC][inverseMap=Y][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1950.11669554448, + "gpu_time_us_python": 1962.9040226339798, + "gpu_noise_us_cpp": 2.4939999999999998, + "gpu_noise_us_python": 4.382, + "gpu_bwutil_cpp": 0.06563711438888624, + "gpu_bwutil_python": 0.06520953957586095, + "gpu_gap_stddev_us": 0.7081649309137591 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1803.2581797517, + "gpu_time_us_python": 1815.79933887792, + "gpu_noise_us_cpp": 1.4280000000000002, + "gpu_noise_us_python": 3.678, + "gpu_bwutil_cpp": 0.05414549438906539, + "gpu_bwutil_python": 0.05377168432137223, + "gpu_gap_stddev_us": 0.7938688439857113 + } + }, + "warpperspective_planar_nchw_uchar3_basic[InOutDataType=uchar3][shape=16x1080x1920][border=REFLECT][interpolation=CUBIC][inverseMap=Y][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2053.7309615338, + "gpu_time_us_python": 2074.40400714712, + "gpu_noise_us_cpp": 2.574, + "gpu_noise_us_python": 4.672, + "gpu_bwutil_cpp": 0.0623256339327667, + "gpu_bwutil_python": 0.06170475262610896, + "gpu_gap_stddev_us": 3.7462460810651197 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1898.86459704476, + "gpu_time_us_python": 1923.2702519991599, + "gpu_noise_us_cpp": 2.2939999999999996, + "gpu_noise_us_python": 5.683999999999999, + "gpu_bwutil_cpp": 0.0514189750053486, + "gpu_bwutil_python": 0.05076636026063645, + "gpu_gap_stddev_us": 1.978855944562542 + } + } + } + }, + "warpperspective_planar_nchw_rgb3_tensor_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3", + "float3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT" + ], + "interpolation": [ + "CUBIC" + ], + "inverseMap": [ + "Y" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "WarpPerspective RGB NCHW Tensor FakePlanar controls (advanced)." + }, + "baselines": { + "warpperspective_planar_nchw_rgb3_tensor_advanced[InOutDataType=float3][shape=16x1080x1920][border=REFLECT][interpolation=CUBIC][inverseMap=Y][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2861.6216106847596, + "gpu_time_us_python": 2874.5830795856405, + "gpu_noise_us_cpp": 1.6039999999999999, + "gpu_noise_us_python": 2.0, + "gpu_bwutil_cpp": 0.17891954795630938, + "gpu_bwutil_python": 0.17811282416606744, + "gpu_gap_stddev_us": 0.9303209538380937 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2962.41516172016, + "gpu_time_us_python": 2976.8747872344798, + "gpu_noise_us_cpp": 1.8239999999999998, + "gpu_noise_us_python": 4.086, + "gpu_bwutil_cpp": 0.1318367370758145, + "gpu_bwutil_python": 0.13119535021014045, + "gpu_gap_stddev_us": 2.286772392350514 + } + }, + "warpperspective_planar_nchw_rgb3_tensor_advanced[InOutDataType=uchar3][shape=16x1080x1920][border=REFLECT][interpolation=CUBIC][inverseMap=Y][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1950.23417546762, + "gpu_time_us_python": 1962.6960217952196, + "gpu_noise_us_cpp": 1.6, + "gpu_noise_us_python": 2.508, + "gpu_bwutil_cpp": 0.06563317270663646, + "gpu_bwutil_python": 0.06521649853338458, + "gpu_gap_stddev_us": 1.249285609196094 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1803.370927728, + "gpu_time_us_python": 1817.17144589778, + "gpu_noise_us_cpp": 1.636, + "gpu_noise_us_python": 3.992, + "gpu_bwutil_cpp": 0.0541423055368205, + "gpu_bwutil_python": 0.05373190713559702, + "gpu_gap_stddev_us": 2.3500884532828814 + } + } + } + }, + "warpperspective_fake_planar_uchar3_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT" + ], + "interpolation": [ + "CUBIC" + ], + "inverseMap": [ + "Y" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "WarpPerspective uchar3 NCHW_FAKE (advanced)." + }, + "baselines": { + "warpperspective_fake_planar_uchar3_advanced[InOutDataType=uchar3][shape=16x1080x1920][border=REFLECT][interpolation=CUBIC][inverseMap=Y][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2197.53637857598, + "gpu_time_us_python": 2214.214776662, + "gpu_noise_us_cpp": 2.5959999999999996, + "gpu_noise_us_python": 3.278, + "gpu_bwutil_cpp": 0.17474129640827107, + "gpu_bwutil_python": 0.17342516501339036, + "gpu_gap_stddev_us": 0.8270830598159147 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 1815.849030586, + "gpu_time_us_python": 1830.90815006124, + "gpu_noise_us_cpp": 1.8780000000000001, + "gpu_noise_us_python": 3.9840000000000004, + "gpu_bwutil_cpp": 0.16130420678636237, + "gpu_bwutil_python": 0.15997560539586272, + "gpu_gap_stddev_us": 1.4238078625581312 + } + } + } + }, + "warpperspective_planar_nchw_float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT" + ], + "interpolation": [ + "CUBIC" + ], + "inverseMap": [ + "Y" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "WarpPerspective float3 NCHW (basic)." + }, + "baselines": { + "warpperspective_planar_nchw_float3_basic[InOutDataType=float3][shape=16x1080x1920][border=REFLECT][interpolation=CUBIC][inverseMap=Y][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2861.50990453626, + "gpu_time_us_python": 2874.76046191856, + "gpu_noise_us_cpp": 1.3820000000000001, + "gpu_noise_us_python": 2.9080000000000004, + "gpu_bwutil_cpp": 0.17892653242894246, + "gpu_bwutil_python": 0.1781018559581376, + "gpu_gap_stddev_us": 1.2001991650568309 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2962.20689019596, + "gpu_time_us_python": 2976.49633784218, + "gpu_noise_us_cpp": 1.5320000000000003, + "gpu_noise_us_python": 3.354, + "gpu_bwutil_cpp": 0.1318455486418419, + "gpu_bwutil_python": 0.13121390666274188, + "gpu_gap_stddev_us": 2.163298608345493 + } + } + } + }, + "warpperspective_planar_nchw_float3_varshape_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT" + ], + "interpolation": [ + "CUBIC" + ], + "inverseMap": [ + "Y" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "WarpPerspective float3 NCHW VarShape (advanced)." + }, + "baselines": { + "warpperspective_planar_nchw_float3_varshape_advanced[InOutDataType=float3][shape=16x1080x1920][border=REFLECT][interpolation=CUBIC][inverseMap=Y][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3301.4003151341, + "gpu_time_us_python": 3325.2746773435, + "gpu_noise_us_cpp": 2.9379999999999997, + "gpu_noise_us_python": 6.306, + "gpu_bwutil_cpp": 0.15508572444296911, + "gpu_bwutil_python": 0.15397258548861123, + "gpu_gap_stddev_us": 4.732566738507773 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3272.0189606116796, + "gpu_time_us_python": 3293.30702368354, + "gpu_noise_us_cpp": 2.224, + "gpu_noise_us_python": 4.064, + "gpu_bwutil_cpp": 0.11936200389221223, + "gpu_bwutil_python": 0.1185903832093266, + "gpu_gap_stddev_us": 2.3797697853448248 + } + } + } + }, + "warpperspective_fake_planar_float3_advanced": { + "tier": "advanced", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT" + ], + "interpolation": [ + "CUBIC" + ], + "inverseMap": [ + "Y" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "WarpPerspective float3 NCHW_FAKE (advanced)." + }, + "baselines": { + "warpperspective_fake_planar_float3_advanced[InOutDataType=float3][shape=16x1080x1920][border=REFLECT][interpolation=CUBIC][inverseMap=Y][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2662.36500879946, + "gpu_time_us_python": 2681.60762533222, + "gpu_noise_us_cpp": 2.8400000000000007, + "gpu_noise_us_python": 2.8980000000000006, + "gpu_bwutil_cpp": 0.5769323541751203, + "gpu_bwutil_python": 0.5727940917394676, + "gpu_gap_stddev_us": 2.33284142376555 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2379.85432809742, + "gpu_time_us_python": 2394.3387319055, + "gpu_noise_us_cpp": 2.58, + "gpu_noise_us_python": 4.176, + "gpu_bwutil_cpp": 0.4922842039747361, + "gpu_bwutil_python": 0.4893077159584884, + "gpu_gap_stddev_us": 1.8102834498582103 + } + } + } + }, + "warpperspective_planar_nchw_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT" + ], + "interpolation": [ + "CUBIC" + ], + "inverseMap": [ + "Y" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "WarpPerspective uchar4 NCHW (advanced)." + }, + "baselines": { + "warpperspective_planar_nchw_uchar4_advanced[InOutDataType=uchar4][shape=16x1080x1920][border=REFLECT][interpolation=CUBIC][inverseMap=Y][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2361.63969129882, + "gpu_time_us_python": 2375.97751032628, + "gpu_noise_us_cpp": 1.734, + "gpu_noise_us_python": 3.6079999999999997, + "gpu_bwutil_cpp": 0.07226618918875119, + "gpu_bwutil_python": 0.07183017960343745, + "gpu_gap_stddev_us": 2.479525887007859 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2205.08652244398, + "gpu_time_us_python": 2220.8100740990403, + "gpu_noise_us_cpp": 1.4919999999999998, + "gpu_noise_us_python": 5.307999999999999, + "gpu_bwutil_cpp": 0.0590382745638652, + "gpu_bwutil_python": 0.05861957627015359, + "gpu_gap_stddev_us": 2.9778814414174506 + } + } + } + }, + "warpperspective_fake_planar_uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT" + ], + "interpolation": [ + "CUBIC" + ], + "inverseMap": [ + "Y" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "WarpPerspective uchar4 NCHW_FAKE (advanced)." + }, + "baselines": { + "warpperspective_fake_planar_uchar4_advanced[InOutDataType=uchar4][shape=16x1080x1920][border=REFLECT][interpolation=CUBIC][inverseMap=Y][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2425.2182217388204, + "gpu_time_us_python": 2443.45091106816, + "gpu_noise_us_cpp": 1.8679999999999999, + "gpu_noise_us_python": 2.868, + "gpu_bwutil_cpp": 0.21111525816891624, + "gpu_bwutil_python": 0.20954018259681712, + "gpu_gap_stddev_us": 1.7453963432788522 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2120.63661758396, + "gpu_time_us_python": 2135.08615637812, + "gpu_noise_us_cpp": 0.8319999999999999, + "gpu_noise_us_python": 4.454, + "gpu_bwutil_cpp": 0.1841605187135952, + "gpu_bwutil_python": 0.18291397109975455, + "gpu_gap_stddev_us": 1.1075613042610695 + } + } + } + }, + "warpperspective_planar_nchw_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT" + ], + "interpolation": [ + "CUBIC" + ], + "inverseMap": [ + "Y" + ], + "layout": [ + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "WarpPerspective float4 NCHW (advanced)." + }, + "baselines": { + "warpperspective_planar_nchw_float4_advanced[InOutDataType=float4][shape=16x1080x1920][border=REFLECT][interpolation=CUBIC][inverseMap=Y][layout=NCHW][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3815.7932267044002, + "gpu_time_us_python": 3829.70730319162, + "gpu_noise_us_cpp": 3.9940000000000007, + "gpu_noise_us_python": 3.65, + "gpu_bwutil_cpp": 0.17890558087059283, + "gpu_bwutil_python": 0.17825561360675085, + "gpu_gap_stddev_us": 1.85742523584637 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3947.7168706097204, + "gpu_time_us_python": 3962.06519521196, + "gpu_noise_us_cpp": 1.2959999999999998, + "gpu_noise_us_python": 3.248, + "gpu_bwutil_cpp": 0.13190888521089564, + "gpu_bwutil_python": 0.13143187271098752, + "gpu_gap_stddev_us": 2.2325394103152036 + } + }, + "warpperspective_planar_nchw_float4_advanced[InOutDataType=float4][shape=16x1080x1920][border=REFLECT][interpolation=CUBIC][inverseMap=Y][layout=NCHW][inputKind=VarShape]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4407.0157971298, + "gpu_time_us_python": 4430.1257936577595, + "gpu_noise_us_cpp": 1.9619999999999997, + "gpu_noise_us_python": 4.408, + "gpu_bwutil_cpp": 0.15490455830170274, + "gpu_bwutil_python": 0.15409662915887412, + "gpu_gap_stddev_us": 4.073923473018043 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 4358.88909377422, + "gpu_time_us_python": 4381.47953943328, + "gpu_noise_us_cpp": 2.5660000000000003, + "gpu_noise_us_python": 5.524, + "gpu_bwutil_cpp": 0.11946627898538831, + "gpu_bwutil_python": 0.11884974904742666, + "gpu_gap_stddev_us": 2.2369671400542646 + } + } + } + }, + "warpperspective_fake_planar_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "16x1080x1920" + ], + "border": [ + "REFLECT" + ], + "interpolation": [ + "CUBIC" + ], + "inverseMap": [ + "Y" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "WarpPerspective float4 NCHW_FAKE (advanced)." + }, + "baselines": { + "warpperspective_fake_planar_float4_advanced[InOutDataType=float4][shape=16x1080x1920][border=REFLECT][interpolation=CUBIC][inverseMap=Y][layout=NCHW_FAKE][inputKind=Tensor]": { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 3238.19253383532, + "gpu_time_us_python": 3257.84205286014, + "gpu_noise_us_cpp": 2.692, + "gpu_noise_us_python": 3.944, + "gpu_bwutil_cpp": 0.632456362461306, + "gpu_bwutil_python": 0.6286439500222732, + "gpu_gap_stddev_us": 3.4134142730165076 + }, + "H100_PCIe_350W_1095MHz": { + "n_runs": 5, + "gpu_time_us_cpp": 2895.7191718471204, + "gpu_time_us_python": 2912.8177239530205, + "gpu_noise_us_cpp": 4.354, + "gpu_noise_us_python": 7.562, + "gpu_bwutil_cpp": 0.5394385686918455, + "gpu_bwutil_python": 0.536269988339168, + "gpu_gap_stddev_us": 3.9376182335776195 + } + } + } + } + } +} diff --git a/bench/config/sku_map.json b/bench/config/sku_map.json new file mode 100644 index 000000000..3dccebf2d --- /dev/null +++ b/bench/config/sku_map.json @@ -0,0 +1,16 @@ +{ + "entries": [ + { + "gpu_name": "NVIDIA H100 PCIe", + "power_cap_w": 350, + "locked_sm_clock_mhz": 1095, + "stem": "H100_PCIe_350W_1095MHz" + }, + { + "gpu_name": "NVIDIA A100-PCIE-40GB", + "power_cap_w": 250, + "locked_sm_clock_mhz": 1095, + "stem": "A100_PCIE_40GB_250W_1095MHz" + } + ] +} diff --git a/bench/cpp/.gitignore b/bench/cpp/.gitignore new file mode 100644 index 000000000..0ba1312cd --- /dev/null +++ b/bench/cpp/.gitignore @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Generated benchmark configuration headers +ops/generated/ diff --git a/bench/cpp/BenchFillKernels.cu b/bench/cpp/BenchFillKernels.cu new file mode 100644 index 000000000..5777019e4 --- /dev/null +++ b/bench/cpp/BenchFillKernels.cu @@ -0,0 +1,180 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "BenchFillKernels.hpp" + +#include +#include + +namespace benchutils { + +namespace { + +// Numerical Recipes LCG — 32-bit; not cryptographic, fine for varied test data. +inline __device__ std::uint32_t lcg_step(std::uint32_t s) +{ + return s * 1664525u + 1013904223u; +} + +inline __device__ std::uint32_t hash32(std::uint64_t seed, std::uint64_t idx) +{ + std::uint32_t s = static_cast(seed ^ idx) ^ static_cast(seed >> 32); + s = lcg_step(s); + s = lcg_step(s); + s = lcg_step(s); + return s; +} + +template +inline __device__ T sample_typed(std::uint32_t s) +{ + if constexpr (std::is_floating_point_v) + { + // Default float range in host LcgValues is uniform_real_distribution(-1, +1). + constexpr T kInv = static_cast(2.0) / static_cast(4294967295.0); + return static_cast(-1) + static_cast(s) * kInv; + } + else + { + // For integral T, truncating the 32-bit word produces uniform values + // across the full T range (matches uniform_int_distribution::min..max). + return static_cast(s); + } +} + +template +__global__ void randomFillTypedKernel(T *data, size_t n_elements, std::uint64_t seed) +{ + size_t idx = static_cast(blockIdx.x) * static_cast(blockDim.x) + static_cast(threadIdx.x); + if (idx >= n_elements) + { + return; + } + data[idx] = sample_typed(hash32(seed, idx)); +} + +// Tensor-rank checkerboard: parity from (coord0 + coord1 + coord2 + coord3), +// matching FillBuffer's per-element iteration over a 4-D shape. For ranks < 4 +// the caller passes upper strides of 1 so the unused coords collapse to 0. +template +__global__ void checkerboardTensorTypedKernel(T *data, size_t n_elements, size_t s0, size_t s1, size_t s2, T hi, T lo) +{ + size_t idx = static_cast(blockIdx.x) * static_cast(blockDim.x) + static_cast(threadIdx.x); + if (idx >= n_elements) + { + return; + } + size_t c0 = idx / s0; + size_t r0 = idx - c0 * s0; + size_t c1 = r0 / s1; + size_t r1 = r0 - c1 * s1; + size_t c2 = r1 / s2; + size_t c3 = r1 - c2 * s2; + data[idx] = ((c0 + c1 + c2 + c3) & 1ull) ? hi : lo; +} + +// 2-D image checkerboard: parity from (h + w) only — every BT-component within +// a pixel gets the same scalar value, matching FillImageBatch's host pattern +// (one VT-sized write per pixel with all components = val). +template +__global__ void checkerboardImageTypedKernel(T *data, size_t n_elements, size_t row_pitch_T, size_t pixel_size_T, T hi, + T lo) +{ + size_t idx = static_cast(blockIdx.x) * static_cast(blockDim.x) + static_cast(threadIdx.x); + if (idx >= n_elements) + { + return; + } + size_t h = idx / row_pitch_T; + size_t in_row = idx - h * row_pitch_T; + size_t w = in_row / pixel_size_T; + data[idx] = ((h + w) & 1ull) ? hi : lo; +} + +constexpr int kThreads = 256; + +inline size_t numBlocks(size_t n_elements) +{ + return (n_elements + kThreads - 1) / kThreads; +} + +} // namespace + +template +void launchRandomFillTyped(T *dptr, size_t n_elements, std::uint64_t seed, cudaStream_t stream) +{ + if (n_elements == 0 || dptr == nullptr) + { + return; + } + randomFillTypedKernel<<>>(dptr, n_elements, seed); +} + +template +void launchCheckerboardTensorTyped(T *dptr, size_t n_elements, size_t s0, size_t s1, size_t s2, T hi, T lo, + cudaStream_t stream) +{ + if (n_elements == 0 || dptr == nullptr) + { + return; + } + if (s0 == 0) + { + s0 = n_elements; + } + if (s1 == 0) + { + s1 = 1; + } + if (s2 == 0) + { + s2 = 1; + } + checkerboardTensorTypedKernel + <<>>(dptr, n_elements, s0, s1, s2, hi, lo); +} + +template +void launchCheckerboardImageTyped(T *dptr, size_t n_elements, size_t row_pitch_T, size_t pixel_size_T, T hi, T lo, + cudaStream_t stream) +{ + if (n_elements == 0 || dptr == nullptr || row_pitch_T == 0 || pixel_size_T == 0) + { + return; + } + checkerboardImageTypedKernel + <<>>(dptr, n_elements, row_pitch_T, pixel_size_T, hi, lo); +} + +// Explicit instantiations for the element types the bench uses. +#define CVCUDA_BENCH_INSTANTIATE_FILL(T) \ + template void launchRandomFillTyped(T *, size_t, std::uint64_t, cudaStream_t); \ + template void launchCheckerboardTensorTyped(T *, size_t, size_t, size_t, size_t, T, T, cudaStream_t); \ + template void launchCheckerboardImageTyped(T *, size_t, size_t, size_t, T, T, cudaStream_t); + +CVCUDA_BENCH_INSTANTIATE_FILL(std::uint8_t) +CVCUDA_BENCH_INSTANTIATE_FILL(std::uint16_t) +CVCUDA_BENCH_INSTANTIATE_FILL(std::uint32_t) +CVCUDA_BENCH_INSTANTIATE_FILL(std::int8_t) +CVCUDA_BENCH_INSTANTIATE_FILL(std::int16_t) +CVCUDA_BENCH_INSTANTIATE_FILL(std::int32_t) +CVCUDA_BENCH_INSTANTIATE_FILL(float) +CVCUDA_BENCH_INSTANTIATE_FILL(double) + +#undef CVCUDA_BENCH_INSTANTIATE_FILL + +} // namespace benchutils diff --git a/bench/cpp/BenchFillKernels.hpp b/bench/cpp/BenchFillKernels.hpp new file mode 100644 index 000000000..1a48b2796 --- /dev/null +++ b/bench/cpp/BenchFillKernels.hpp @@ -0,0 +1,94 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#ifndef CVCUDA_BENCH_FILL_KERNELS_HPP +#define CVCUDA_BENCH_FILL_KERNELS_HPP + +#include + +#include +#include + +namespace benchutils { + +/** + * Fill a typed device buffer with an LCG-based pseudo-random stream. + * + * Replaces the host-side std::uniform_int_distribution + cudaMemcpy path, + * which on this codebase can spend 1-6 seconds on a single 0.5-1.6 GB fill. + * The LCG output is uniform-enough for benchmark workloads where we only + * want varied data so the kernel doesn't take a constant-input fast path — + * we are NOT relying on statistical randomness. + * + * Distribution by element type: + * - integer T: uniform across the full type range (matches the default + * host LcgValues() with no args). + * - float T: uniform in [-1, +1] (matches the default for floating + * point in host LcgValues() with no args). + * + * Explicit instantiations are provided in BenchFillKernels.cu for the + * element types the bench uses: uint8_t, uint16_t, uint32_t, int8_t, + * int16_t, int32_t, float, double. + */ +template +void launchRandomFillTyped(T *dptr, size_t n_elements, std::uint64_t seed, cudaStream_t stream); + +/** + * Fill an N-D tensor with a per-element checkerboard, parity from full coords. + * + * Mirrors the host FillTensor / FillBuffer behaviour: iteration is per BT + * element, parity sums all four coords (n + h + w + c) so adjacent components + * within a pixel alternate. Strides are passed in T-elements (not bytes); the + * inner-most stride is 1 (implicit). + * + * coord0 = idx / s0 + * r0 = idx - coord0 * s0 + * coord1 = r0 / s1 + * r1 = r0 - coord1 * s1 + * coord2 = r1 / s2 + * coord3 = r1 - coord2 * s2 + * data[idx] = ((coord0 + coord1 + coord2 + coord3) & 1) ? hi : lo + * + * For ranks < 4 the upper strides should be 1 so the unused coords collapse + * to 0 (matching FillBuffer's degenerate-loop behaviour). + */ +template +void launchCheckerboardTensorTyped(T *dptr, size_t n_elements, size_t s0, size_t s1, size_t s2, T hi, T lo, + cudaStream_t stream); + +/** + * Fill a 2-D image buffer with a per-PIXEL uniform checkerboard. + * + * Mirrors the host FillImageBatch behaviour: iteration is per pixel, each + * pixel's VT-sized cell gets all components set to the same scalar value + * `((h + w) & 1) ? hi : lo`. Required to match the host pattern for + * multi-channel formats (uchar3 / uchar4 / float3 / float4) where the + * tensor-rank kernel would alternate within a pixel and produce a + * different data layout. + * + * @param dptr Device buffer (typed pointer). + * @param n_elements Total T elements in the image (= H * row_pitch_T). + * @param row_pitch_T Row stride in T-elements (= bytes_per_row / sizeof(T)). + * @param pixel_size_T Pixel size in T-elements (= NumElements). + */ +template +void launchCheckerboardImageTyped(T *dptr, size_t n_elements, size_t row_pitch_T, size_t pixel_size_T, T hi, T lo, + cudaStream_t stream); + +} // namespace benchutils + +#endif // CVCUDA_BENCH_FILL_KERNELS_HPP diff --git a/bench/cpp/CMakeLists.txt b/bench/cpp/CMakeLists.txt new file mode 100644 index 000000000..9dda120e3 --- /dev/null +++ b/bench/cpp/CMakeLists.txt @@ -0,0 +1,219 @@ +# SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +project(cvcuda_bench) + +# Standalone-bench mode: when invoked with BUILD_LIB=OFF (top-level), the +# cvcuda + nvcv_types targets aren't defined in this tree. Pull them in via +# the CMake config files installed by cvcuda-dev. This lets a CI test pod +# compile bench against a pre-built cvcuda from a manylinux artifact, without +# having to rebuild the lib itself. When BUILD_LIB=ON, the in-tree targets +# already exist and these find_package calls are skipped. +if(NOT TARGET cvcuda) + find_package(nvcv_types CONFIG REQUIRED) + find_package(cvcuda CONFIG REQUIRED) +endif() + +set(bench_sources + ops/BenchJpegCompressionDistortion.cpp + ops/BenchAdjustHue.cpp + ops/BenchAdjustSaturation.cpp + ops/BenchAdjustSharpness.cpp + ops/BenchAdjustContrast.cpp + ops/BenchInvert.cpp + ops/BenchSolarize.cpp + ops/BenchPosterize.cpp + ops/BenchAutoContrast.cpp + ops/BenchMinMaxLoc.cpp + ops/BenchNonMaximumSuppression.cpp + ops/BenchRemap.cpp + ops/BenchGaussian.cpp + ops/BenchLaplacian.cpp + ops/BenchAverageBlur.cpp + ops/BenchMedianBlur.cpp + ops/BenchBilateralFilter.cpp + ops/BenchSIFT.cpp + ops/BenchReformat.cpp + ops/BenchResize.cpp + ops/BenchFlip.cpp + ops/BenchRotate.cpp + ops/BenchPillowResize.cpp + ops/BenchHQResize.cpp + ops/BenchCenterCrop.cpp + ops/BenchWarpPerspective.cpp + ops/BenchWarpAffine.cpp + ops/BenchThreshold.cpp + ops/BenchRandomResizedCrop.cpp + ops/BenchNormalize.cpp + ops/BenchMorphology.cpp + ops/BenchAdaptiveThreshold.cpp + ops/BenchCvtColor.cpp + ops/BenchAdvCvtColor.cpp + ops/BenchBndBox.cpp + ops/BenchBoxBlur.cpp + ops/BenchBrightnessContrast.cpp + ops/BenchChannelReorder.cpp + ops/BenchColorTwist.cpp + ops/BenchComposite.cpp + ops/BenchConv2D.cpp + ops/BenchConvertTo.cpp + ops/BenchCopyMakeBorder.cpp + ops/BenchCropFlipNormalizeReformat.cpp + ops/BenchResizeCropConvertReformat.cpp + ops/BenchCustomCrop.cpp + ops/BenchErase.cpp + ops/BenchGammaContrast.cpp + ops/BenchGaussianNoise.cpp + ops/BenchHistogramEq.cpp + ops/BenchHistogram.cpp + ops/BenchInpaint.cpp + ops/BenchJointBilateralFilter.cpp + ops/BenchMinAreaRect.cpp + ops/BenchPadAndStack.cpp + ops/BenchOSD.cpp + ops/BenchLabel.cpp + ops/BenchPairwiseMatcher.cpp + ops/BenchStack.cpp + ops/BenchFindHomography.cpp + ops/BenchCLAHE.cpp +) + +# Generate benchmark configuration headers from per-operator JSON +set(CONFIG_OPERATOR_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../config/operators") +set(CONFIG_GENERATOR "${CMAKE_CURRENT_SOURCE_DIR}/GenerateBenchConfig.cmake") +set(AXIS_ORDER_HELPER "${CMAKE_CURRENT_SOURCE_DIR}/../config/axis_order.py") +set(GENERATED_DIR "${CMAKE_CURRENT_BINARY_DIR}/ops/generated") +file(MAKE_DIRECTORY "${GENERATED_DIR}") + +# Create a list to store all generated headers +set(ALL_GENERATED_HEADERS "") + +# Create custom commands for each benchmark configuration header +# These will regenerate automatically when the matching operator config changes +foreach(bench_source IN LISTS bench_sources) + # Extract operator name from filename: ops/BenchResize.cpp -> Resize + get_filename_component(bench_file_name "${bench_source}" NAME_WLE) + string(REPLACE "Bench" "" OPERATOR_CAMEL "${bench_file_name}") + + # Convert to lowercase for JSON lookup: Resize -> resize + string(TOLOWER "${OPERATOR_CAMEL}" OPERATOR_LOWER) + + set(CONFIG_JSON_FILE "${CONFIG_OPERATOR_DIR}/${OPERATOR_LOWER}.json") + set(OUTPUT_HEADER "${GENERATED_DIR}/Bench${OPERATOR_CAMEL}Config.hpp") + + # Add custom command to generate header at build time + add_custom_command( + OUTPUT "${OUTPUT_HEADER}" + COMMAND ${CMAKE_COMMAND} + -DJSON_FILE=${CONFIG_JSON_FILE} + -DOPERATOR=${OPERATOR_LOWER} + -DOPERATOR_CAMEL=${OPERATOR_CAMEL} + -DOUTPUT_FILE=${OUTPUT_HEADER} + -DAXIS_ORDER_HELPER=${AXIS_ORDER_HELPER} + -P "${CONFIG_GENERATOR}" + DEPENDS + "${CONFIG_JSON_FILE}" + "${CONFIG_GENERATOR}" + "${AXIS_ORDER_HELPER}" + COMMENT "Generating config header for ${OPERATOR_CAMEL} from operators/${OPERATOR_LOWER}.json" + VERBATIM + ) + + list(APPEND ALL_GENERATED_HEADERS "${OUTPUT_HEADER}") +endforeach() + +list(LENGTH bench_sources NUM_BENCHMARKS) +message(STATUS "Configured ${NUM_BENCHMARKS} benchmark config header generation rules") + +# Metatarget for all benchmarks +add_custom_target(bench_all) + +# Shared CUDA fill helpers — typed device-side random / checkerboard fills. +# Built once and linked into every bench executable; replaces the host-side +# uniform_int_distribution + cudaMemcpy round-trip that dominated big-tensor +# setup time on multi-hundred-MB src tensors (1-6 s host fill -> ms GPU fill). +add_library(cvcuda_bench_fill_kernels STATIC BenchFillKernels.cu) +set_target_properties(cvcuda_bench_fill_kernels PROPERTIES + POSITION_INDEPENDENT_CODE ON + CUDA_SEPARABLE_COMPILATION OFF) +target_include_directories(cvcuda_bench_fill_kernels PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +# Link cudart so the CUDA toolkit include dir is exposed as -isystem (matching +# every other .cu in the tree). Without this the nvcc host pass treats CUDA's +# own headers as user code and the global -Wredundant-decls / -Werror in +# ConfigCompiler.cmake fires on duplicate decls inside CUDA itself. +target_link_libraries(cvcuda_bench_fill_kernels PUBLIC CUDA::cudart_static) + +foreach(bench_source IN LISTS bench_sources) + get_filename_component(bench_file_name "${bench_source}" NAME_WLE) + string(REPLACE "Bench" "bench_" algo_name ${bench_file_name}) + string(TOLOWER ${algo_name} bench_name) + + # Extract operator name to find corresponding generated header + string(REPLACE "Bench" "" OPERATOR_CAMEL "${bench_file_name}") + set(GENERATED_HEADER "${GENERATED_DIR}/Bench${OPERATOR_CAMEL}Config.hpp") + + # Add executable with generated header as a source dependency + add_executable(${bench_name} "${bench_source}" "${GENERATED_HEADER}") + target_link_libraries(${bench_name} PRIVATE cvcuda::nvbench::main cvcuda cvcuda_bench_fill_kernels) + set_target_properties(${bench_name} PROPERTIES COMPILE_FEATURES cuda_std_17) + + # Add generated config headers directory to include path + target_include_directories(${bench_name} PRIVATE "${CMAKE_CURRENT_BINARY_DIR}") + + # Ensure benchmark config JSON is up-to-date before building + add_dependencies(${bench_name} copy_bench_config) + + add_dependencies(bench_all ${bench_name}) + + # Install benchmark executable + install(TARGETS ${bench_name} + DESTINATION ${CMAKE_INSTALL_BINDIR} + COMPONENT bench) +endforeach() + +# Install benchmark runner scripts +install(PROGRAMS + ${CMAKE_CURRENT_SOURCE_DIR}/../run_bench.py + ${CMAKE_CURRENT_SOURCE_DIR}/../compare_wheels.py + DESTINATION ${CMAKE_INSTALL_BINDIR} + COMPONENT bench) + +set(BENCH_INTERNAL_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../_internal") +set(BENCH_INTERNAL_OUTPUT_DIR "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/_internal") +set(BENCH_INTERNAL_MODULES + __init__.py + axes.py + baselines.py + quality.py + warmup.py) +set(BENCH_INTERNAL_SOURCES "") +file(MAKE_DIRECTORY "${BENCH_INTERNAL_OUTPUT_DIR}") +foreach(module IN LISTS BENCH_INTERNAL_MODULES) + list(APPEND BENCH_INTERNAL_SOURCES "${BENCH_INTERNAL_SOURCE_DIR}/${module}") + configure_file("${BENCH_INTERNAL_SOURCE_DIR}/${module}" + "${BENCH_INTERNAL_OUTPUT_DIR}/${module}" + COPYONLY) +endforeach() +install(FILES ${BENCH_INTERNAL_SOURCES} + DESTINATION ${CMAKE_INSTALL_BINDIR}/_internal + COMPONENT bench) + +# Copy script to build directory +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/../run_bench.py + ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/run_bench.py + COPYONLY) +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/../compare_wheels.py + ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/compare_wheels.py + COPYONLY) diff --git a/bench/cpp/CppBenchUtils.hpp b/bench/cpp/CppBenchUtils.hpp new file mode 100644 index 000000000..c6ec86efb --- /dev/null +++ b/bench/cpp/CppBenchUtils.hpp @@ -0,0 +1,916 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#ifndef CVCUDA_CPP_BENCH_UTILS_HPP +#define CVCUDA_CPP_BENCH_UTILS_HPP + +#include "BenchFillKernels.hpp" +#include "WarmupPolicy.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace benchutils { + +class InvalidBenchmarkDataError : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + +} // namespace benchutils + +#define CVCUDA_CHECK_DATA(data) \ + if (!data) \ + { \ + throw benchutils::InvalidBenchmarkDataError("Invalid data"); \ + } + +#define CVCUDA_BENCH_SKIP_ERRORS(state) \ + catch (const nvcv::Exception &err) \ + { \ + (state).skip(err.what()); \ + } \ + catch (const std::invalid_argument &err) \ + { \ + (state).skip(err.what()); \ + } \ + catch (const std::out_of_range &err) \ + { \ + (state).skip(err.what()); \ + } \ + catch (const std::length_error &err) \ + { \ + (state).skip(err.what()); \ + } \ + catch (const std::runtime_error &err) \ + { \ + (state).skip(err.what()); \ + } \ + catch (const std::bad_alloc &err) \ + { \ + (state).skip(err.what()); \ + } + +#define CUDA_CHECK_ERROR(RC) \ + { \ + benchutils::cudaCheckError((RC), __FILE__, __LINE__); \ + } + +namespace benchutils { + +inline void cudaCheckError(cudaError_t code, const char *file, int line) +{ + if (code != cudaSuccess) + { + fprintf(stderr, "\nE In CUDA: %s %s %d\n", cudaGetErrorString(code), file, line); + exit(code); + } +} + +template> +inline RT GetShape(const std::string &shapeStr, const std::string &delimiter = "x") +{ + std::string str = shapeStr; + RT shape; + for (int i = 0; i < N; ++i) + { + size_t pos = str.find(delimiter); + + if ((pos == std::string::npos && i != (N - 1)) || (pos != std::string::npos && i == (N - 1))) + { + throw std::invalid_argument("Expecting " + std::to_string(N) + "-rank shape in " + shapeStr + + " (pass shape separated by " + delimiter + ")"); + } + + nvcv::cuda::GetElement(shape, i) = std::stoi(str.substr(0, pos)); + + str.erase(0, pos + delimiter.length()); + } + + return shape; +} + +inline long3 GetResizeOutputShape(long3 srcShape, const std::string &resizeType) +{ + if (resizeType == "EXPAND") + { + return long3{srcShape.x, srcShape.y * 2, srcShape.z * 2}; + } + if (resizeType == "CONTRACT") + { + return long3{srcShape.x, srcShape.y / 2, srcShape.z / 2}; + } + + if (constexpr std::string_view targetPrefix = "TARGET_"; resizeType.rfind(targetPrefix, 0) == 0) + { + long2 target = GetShape<2>(resizeType.substr(targetPrefix.size())); + if (target.x <= 0 || target.y <= 0) + { + throw std::invalid_argument("Resize target dimensions must be positive in " + resizeType); + } + return long3{srcShape.x, target.x, target.y}; + } + + throw std::invalid_argument("Invalid resizeType = " + resizeType); +} + +template +inline T GetIntParam(const nvbench::state &state, const std::string &name) +{ + static_assert(std::is_integral_v); + + const std::int64_t value = state.get_int64(name); + if (value < static_cast(std::numeric_limits::lowest()) + || value > static_cast(std::numeric_limits::max())) + { + throw std::invalid_argument("Benchmark parameter " + name + " is out of range"); + } + + return static_cast(value); +} + +// The "inputKind" config axis selects which nvcv container a benchmark builds for +// its batch: a single dense Tensor, or an ImageBatchVarShape. Mirrors the other +// string-axis helpers (GetBorderType, GetInterpolationType): maps the axis string +// to a typed value and throws on anything unexpected (no silent fallback). +enum class InputKind +{ + Tensor, + VarShape +}; + +inline InputKind GetInputKind(const std::string &inputKind) +{ + if (inputKind == "Tensor") + { + return InputKind::Tensor; + } + if (inputKind == "VarShape") + { + return InputKind::VarShape; + } + throw std::invalid_argument("Unexpected inputKind = " + inputKind); +} + +inline std::string GetStringParam(const nvbench::state &state, std::string_view name, std::string_view defaultValue) +{ + return state.get_string_or_default(std::string{name}, std::string{defaultValue}); +} + +template +inline nvcv::DataType GetDataType() +{ +#define CVCUDA_BENCH_GET_DATA_TYPE(TYPE, DATA_TYPE) \ + if constexpr (std::is_same_v) \ + { \ + return DATA_TYPE; \ + } + + CVCUDA_BENCH_GET_DATA_TYPE(uint8_t, nvcv::TYPE_U8); + CVCUDA_BENCH_GET_DATA_TYPE(uint16_t, nvcv::TYPE_U16); + CVCUDA_BENCH_GET_DATA_TYPE(uint32_t, nvcv::TYPE_U32); + + CVCUDA_BENCH_GET_DATA_TYPE(uchar3, nvcv::TYPE_3U8); + CVCUDA_BENCH_GET_DATA_TYPE(uchar4, nvcv::TYPE_4U8); + CVCUDA_BENCH_GET_DATA_TYPE(float, nvcv::TYPE_F32); + + CVCUDA_BENCH_GET_DATA_TYPE(float3, nvcv::TYPE_3F32); + CVCUDA_BENCH_GET_DATA_TYPE(float4, nvcv::TYPE_4F32); + + CVCUDA_BENCH_GET_DATA_TYPE(int, nvcv::TYPE_S32); + + CVCUDA_BENCH_GET_DATA_TYPE(short, nvcv::TYPE_S16); + + CVCUDA_BENCH_GET_DATA_TYPE(ushort3, nvcv::TYPE_3U16); + CVCUDA_BENCH_GET_DATA_TYPE(ushort4, nvcv::TYPE_4U16); + CVCUDA_BENCH_GET_DATA_TYPE(short4, nvcv::TYPE_4S16); + +#undef CVCUDA_BENCH_GET_DATA_TYPE + + throw std::invalid_argument("Unexpected data type"); +} + +template +inline nvcv::ImageFormat GetFormat() +{ + nvcv::DataType dataType = GetDataType(); + nvcv::Swizzle swizzle; + if constexpr (nvcv::cuda::NumElements == 1) + { + swizzle = nvcv::Swizzle::S_X000; + } + else if constexpr (nvcv::cuda::NumElements == 2) + { + swizzle = nvcv::Swizzle::S_XY00; + } + else if constexpr (nvcv::cuda::NumElements == 3) + { + swizzle = nvcv::Swizzle::S_XYZ0; + } + else + { + static_assert(nvcv::cuda::NumElements == 4, "Unexpected benchmark image channel count"); + swizzle = nvcv::Swizzle::S_XYZW; + } + return nvcv::ImageFormat{nvcv::MemLayout::PL, dataType.dataKind(), swizzle, dataType.packing()}; +} + +template +inline nvcv::ImageFormat GetRGBFormat() +{ + using BT = typename nvcv::cuda::BaseType; + constexpr int C = nvcv::cuda::NumElements; + + static_assert(C == 3, "RGB benchmark formats require three channels"); + if constexpr (std::is_same_v) + { + return nvcv::FMT_RGB8; + } + else if constexpr (std::is_same_v) + { + return nvcv::FMT_RGBf32; + } + else + { + throw std::invalid_argument("Unsupported RGB benchmark data type"); + } +} + +inline NVCVBorderType GetBorderType(const std::string &border) +{ +#define CVCUDA_BENCH_GET_BORDER_TYPE(BORDER) \ + if (border == #BORDER) \ + { \ + return NVCV_BORDER_##BORDER; \ + } + + CVCUDA_BENCH_GET_BORDER_TYPE(CONSTANT); + CVCUDA_BENCH_GET_BORDER_TYPE(REPLICATE); + CVCUDA_BENCH_GET_BORDER_TYPE(REFLECT); + CVCUDA_BENCH_GET_BORDER_TYPE(WRAP); + CVCUDA_BENCH_GET_BORDER_TYPE(REFLECT101); + +#undef CVCUDA_BENCH_GET_BORDER_TYPE + + throw std::invalid_argument("Unexpected border type = " + border); +} + +inline NVCVNormType GetNormType(const std::string &normType) +{ +#define CVCUDA_BENCH_GET_NORM_TYPE(NORM) \ + if (normType == #NORM) \ + { \ + return NVCV_NORM_##NORM; \ + } + + CVCUDA_BENCH_GET_NORM_TYPE(HAMMING); + CVCUDA_BENCH_GET_NORM_TYPE(L1); + CVCUDA_BENCH_GET_NORM_TYPE(L2); + +#undef CVCUDA_BENCH_GET_NORM_TYPE + + throw std::invalid_argument("Unexpected norm type = " + normType); +} + +inline NVCVInterpolationType GetInterpolationType(const std::string &interpolation) +{ +#define CVCUDA_BENCH_GET_INTERPOLATION_TYPE(INTERP) \ + if (interpolation == #INTERP) \ + { \ + return NVCV_INTERP_##INTERP; \ + } + + CVCUDA_BENCH_GET_INTERPOLATION_TYPE(NEAREST); + CVCUDA_BENCH_GET_INTERPOLATION_TYPE(LINEAR); + CVCUDA_BENCH_GET_INTERPOLATION_TYPE(CUBIC); + CVCUDA_BENCH_GET_INTERPOLATION_TYPE(AREA); + CVCUDA_BENCH_GET_INTERPOLATION_TYPE(LANCZOS); + CVCUDA_BENCH_GET_INTERPOLATION_TYPE(GAUSSIAN); + CVCUDA_BENCH_GET_INTERPOLATION_TYPE(HAMMING); + CVCUDA_BENCH_GET_INTERPOLATION_TYPE(BOX); + +#undef CVCUDA_BENCH_GET_INTERPOLATION_TYPE + + throw std::invalid_argument("Unexpected interpolation type = " + interpolation); +} + +template, const T, T>> +inline RT &ValueAt(VecType &vec, const ST &strides, const ST &coord) +{ + return *reinterpret_cast(&vec[nvcv::cuda::dot(coord, strides)]); +} + +static std::default_random_engine DefaultGenerator(unsigned long int seed = 0) +{ + static std::default_random_engine defaultRandomGenerator{std::random_device{}()}; + + defaultRandomGenerator.seed(seed); + + return defaultRandomGenerator; +} + +template +struct LcgGenerator +{ + using BT = nvcv::cuda::BaseType; + using RE = std::default_random_engine; + using UD = std::conditional_t, std::uniform_real_distribution, + std::uniform_int_distribution>; + + VT operator()() + { + VT ret; + for (int i = 0; i < nvcv::cuda::NumElements; ++i) + { + nvcv::cuda::GetElement(ret, i) = uniformDistribution(randomGenerator); + } + return ret; + } + + VT operator()(const long4_16a &) + { + return operator()(); + } + + UD uniformDistribution; + RE randomGenerator; +}; + +template, typename BT = typename R::BT, typename RE = typename R::RE, + typename UD = typename R::UD> +inline auto LcgValues(BT min = std::is_integral_v ? nvcv::cuda::TypeTraits::min : -1, + BT max = std::is_integral_v ? nvcv::cuda::TypeTraits::max : +1, + RE rng = DefaultGenerator()) +{ + return R{UD(min, max), rng}; +} + +/** + * Per-element checkerboard generator: ((sum of coords) & 1) ? hi : lo. + * + * Faster than LcgValues() for filling host buffers prior to cudaMemcpy: + * a deterministic per-element computation avoids the per-element cost of + * std::uniform_int_distribution while still producing varied data so the + * kernel doesn't take a constant-input fast path. + * + * Uses (x + y + z + w) & 1 so the pattern is correct regardless of buffer rank + * (1-D up to 4-D, including the 2-D per-image fills used by FillImageBatch). + */ +template +struct Checkerboard +{ + using BT = nvcv::cuda::BaseType; + + VT operator()(const long4_16a &coord) const + { + const bool on = ((coord.x + coord.y + coord.z + coord.w) & 1L) != 0; + const BT val = on ? hi : lo; + VT ret; + for (int i = 0; i < nvcv::cuda::NumElements; ++i) + { + nvcv::cuda::GetElement(ret, i) = val; + } + return ret; + } + + VT operator()() const + { + VT ret; + for (int i = 0; i < nvcv::cuda::NumElements; ++i) + { + nvcv::cuda::GetElement(ret, i) = hi; + } + return ret; + } + + BT hi; + BT lo; +}; + +template> +inline auto CheckerboardValues(BT hi = std::is_integral_v ? nvcv::cuda::TypeTraits::max : static_cast(1), + BT lo = static_cast(0)) +{ + return Checkerboard{hi, lo}; +} + +template +inline void FillBufferPlane(std::vector &vec, const ST &shape, const ST &strides, VG valuesGenerator, long x, + long y, long z) +{ + for (long w = 0; w < (nvcv::cuda::NumElements == 4 ? nvcv::cuda::GetElement(shape, 3) : 1); ++w) + { + long4_16a coord16a{x, y, z, w}; + ST stCoord = nvcv::cuda::DropCast>( + nvcv::cuda::StaticCast>(coord16a)); + + ValueAt(vec, strides, stCoord) = valuesGenerator(coord16a); + } +} + +template +inline void FillBuffer(std::vector &vec, const ST &shape, const ST &strides, VG valuesGenerator) +{ + for (long x = 0; x < (nvcv::cuda::NumElements >= 1 ? nvcv::cuda::GetElement(shape, 0) : 1); ++x) + { + for (long y = 0; y < (nvcv::cuda::NumElements >= 2 ? nvcv::cuda::GetElement(shape, 1) : 1); ++y) + { + for (long z = 0; z < (nvcv::cuda::NumElements >= 3 ? nvcv::cuda::GetElement(shape, 2) : 1); ++z) + { + FillBufferPlane(vec, shape, strides, valuesGenerator, x, y, z); + } + } + } +} + +template +inline void FillTensor(const nvcv::Tensor &tensor, VG valuesGenerator) +{ + using longR = nvcv::cuda::MakeType; + + auto tensorData = tensor.exportData(); + CVCUDA_CHECK_DATA(tensorData); + + longR strides; + longR shape; + + for (int i = 0; i < RANK; ++i) + { + nvcv::cuda::GetElement(strides, i) = tensorData->stride(i); + nvcv::cuda::GetElement(shape, i) = tensorData->shape(i); + } + + long bufSize{nvcv::cuda::GetElement(strides, 0) * nvcv::cuda::GetElement(shape, 0)}; + + // GPU-side fast paths for the bench-internal generators. Avoids the + // host fill + cudaMemcpy round-trip (which on the C++ side was burning + // 1-6 s per fill on multi-hundred-MB tensors). Each typed kernel writes + // BT-typed elements directly to device memory and matches the default + // host-generator distribution (full type range for integers; [-1, +1] + // for floats), so swapping host -> GPU is a no-op for downstream kernels. + if constexpr (std::is_same_v>) + { + using BT = typename LcgGenerator::BT; + // Only take the GPU path when the host generator's range is the + // default full range — that's what the big-tensor src fills use. + // Anything narrower (e.g. LcgValues(0.f, 1.f) for the + // small parameter tensors) falls through to the host implementation + // so the value distribution stays identical. + // Range-match check: the GPU LCG kernel produces full type range for + // ints (matching LcgValues()'s integer default), and [-1, +1] + // for floats (matching LcgValues()'s float default — note that + // TypeTraits::min is FLT_MIN, not -1, so we can't use it here). + const bool defaultRange = std::is_floating_point_v + ? (valuesGenerator.uniformDistribution.min() == static_cast(-1) + && valuesGenerator.uniformDistribution.max() == static_cast(1)) + : (valuesGenerator.uniformDistribution.min() == nvcv::cuda::TypeTraits::min + && valuesGenerator.uniformDistribution.max() == nvcv::cuda::TypeTraits::max); + if (defaultRange) + { + const size_t n_elements = static_cast(bufSize) / sizeof(BT); + launchRandomFillTyped(reinterpret_cast(tensorData->basePtr()), n_elements, + /*seed*/ 0x9e3779b97f4a7c15ULL, /*stream*/ nullptr); + CUDA_CHECK_ERROR(cudaStreamSynchronize(nullptr)); + return; + } + } + if constexpr (std::is_same_v>) + { + using BT = typename Checkerboard::BT; + // Per-element coord-sum pattern matching FillBuffer's iteration over + // a 4-D shape. Strides come straight from the tensor in BT-element + // units (bench tensors are tightly packed, inner-most stride == 1). + const size_t n_elements = static_cast(bufSize) / sizeof(BT); + const size_t s0 + = (RANK >= 1) ? static_cast(nvcv::cuda::GetElement(strides, 0)) / sizeof(BT) : n_elements; + const size_t s1 = (RANK >= 2) ? static_cast(nvcv::cuda::GetElement(strides, 1)) / sizeof(BT) : 1; + const size_t s2 = (RANK >= 3) ? static_cast(nvcv::cuda::GetElement(strides, 2)) / sizeof(BT) : 1; + launchCheckerboardTensorTyped(reinterpret_cast(tensorData->basePtr()), n_elements, s0, s1, s2, + valuesGenerator.hi, valuesGenerator.lo, /*stream*/ nullptr); + CUDA_CHECK_ERROR(cudaStreamSynchronize(nullptr)); + return; + } + + // Fallback: arbitrary generator (lambdas, etc.) — host fill then memcpy. + std::vector tensorVec(bufSize); + + FillBuffer(tensorVec, shape, strides, valuesGenerator); + + CUDA_CHECK_ERROR(cudaMemcpy(tensorData->basePtr(), tensorVec.data(), bufSize, cudaMemcpyHostToDevice)); +} + +template +inline void FillTensor(const nvcv::Tensor &tensor, VG valuesGenerator) +{ + switch (tensor.rank()) + { +#define CVCUDA_BENCH_FILL_TENSOR_CASE(RANK) \ +case RANK: \ + FillTensor(tensor, valuesGenerator); \ + break + + CVCUDA_BENCH_FILL_TENSOR_CASE(1); + CVCUDA_BENCH_FILL_TENSOR_CASE(2); + CVCUDA_BENCH_FILL_TENSOR_CASE(3); + CVCUDA_BENCH_FILL_TENSOR_CASE(4); + +#undef CVCUDA_BENCH_FILL_TENSOR_CASE + default: + throw std::invalid_argument("Tensor has rank not in [1, 4]"); + } +} + +template +inline void FillImageBatch(nvcv::ImageBatchVarShape &imageBatch, long2 size, long2 varSize, VG valuesGenerator, + nvcv::ImageFormat format = GetFormat()) +{ + auto randomWidth = LcgValues(static_cast(size.x - varSize.x), static_cast(size.x)); + auto randomHeight = LcgValues(static_cast(size.y - varSize.y), static_cast(size.y)); + + for (int i = 0; i < imageBatch.capacity(); ++i) + { + nvcv::Image image(nvcv::Size2D{randomWidth(), randomHeight()}, format); + + auto data = image.exportData(); + CVCUDA_CHECK_DATA(data); + + long2 strides{data->plane(0).rowStride, sizeof(VT)}; + long2 shape{data->plane(0).height, data->plane(0).width}; + + long bufSize = strides.x * shape.x; + + // GPU-side fast paths — same conditions as FillTensor. Avoids the + // per-image host fill + cudaMemcpy2D round-trip. + bool gpu_path = false; + if constexpr (std::is_same_v>) + { + using BT = typename LcgGenerator::BT; + // See FillTensor's matching range check for the float vs. int rationale. + const bool defaultRange + = std::is_floating_point_v + ? (valuesGenerator.uniformDistribution.min() == static_cast(-1) + && valuesGenerator.uniformDistribution.max() == static_cast(1)) + : (valuesGenerator.uniformDistribution.min() == nvcv::cuda::TypeTraits::min + && valuesGenerator.uniformDistribution.max() == nvcv::cuda::TypeTraits::max); + if (defaultRange) + { + const size_t n_elements = static_cast(bufSize) / sizeof(BT); + launchRandomFillTyped(reinterpret_cast(data->plane(0).basePtr), n_elements, + /*seed*/ 0x9e3779b97f4a7c15ULL + static_cast(i), + /*stream*/ nullptr); + gpu_path = true; + } + } + else if constexpr (std::is_same_v>) + { + using BT = typename Checkerboard::BT; + const size_t n_elements = static_cast(bufSize) / sizeof(BT); + const size_t row_pitch_T = std::max(1, static_cast(strides.x) / sizeof(BT)); + const size_t pixel_size_T = std::max(1, sizeof(VT) / sizeof(BT)); + launchCheckerboardImageTyped(reinterpret_cast(data->plane(0).basePtr), n_elements, row_pitch_T, + pixel_size_T, valuesGenerator.hi, valuesGenerator.lo, /*stream*/ nullptr); + gpu_path = true; + } + + if (!gpu_path) + { + std::vector imageBuffer(bufSize); + FillBuffer(imageBuffer, shape, strides, valuesGenerator); + CUDA_CHECK_ERROR(cudaMemcpy2D(data->plane(0).basePtr, strides.x, imageBuffer.data(), strides.x, strides.x, + data->plane(0).height, cudaMemcpyHostToDevice)); + } + + imageBatch.pushBack(image); + } + + // Make sure all per-image GPU-fill kernels are done before the bench reads them. + if constexpr (std::is_same_v> || std::is_same_v>) + { + CUDA_CHECK_ERROR(cudaStreamSynchronize(nullptr)); + } +} + +/** + * Fill an ImageBatchVarShape with images matching the shapes from a source batch. + * + * This ensures dst has the same per-sample dimensions as src, which is required + * by most CV-CUDA operators. Use this instead of FillImageBatch for dst when + * src/dst shape matching is required. + * + * @param imageBatch The destination ImageBatchVarShape to fill (must have sufficient capacity) + * @param srcBatch The source ImageBatchVarShape to copy shapes from + * @param valuesGenerator A callable that takes (const long4_16a &) and returns VT + * + * Example: + * benchutils::FillImageBatch(src, long2{W, H}, long2{0, 0}, LcgValues()); + * benchutils::FillImageBatchLike(dst, src, [](const long4_16a &) { return T{0}; }); + */ +template +inline void FillImageBatchLike(nvcv::ImageBatchVarShape &imageBatch, const nvcv::ImageBatchVarShape &srcBatch, + VG valuesGenerator, std::optional requestedFormat = std::nullopt) +{ + for (int i = 0; i < srcBatch.numImages(); ++i) + { + nvcv::Image srcImage = srcBatch[i]; + auto srcData = srcImage.exportData(); + CVCUDA_CHECK_DATA(srcData); + + nvcv::ImageFormat format = requestedFormat.value_or(srcImage.format()); + if (!requestedFormat && (format.numPlanes() != 1 || format.planeDataType(0) != GetDataType())) + { + // Some operators derive single-channel auxiliary batches (for example, Composite masks) + // from RGB inputs. Preserve semantic metadata only when it describes VT. + format = GetFormat(); + } + nvcv::Image image(nvcv::Size2D{srcData->plane(0).width, srcData->plane(0).height}, format); + + auto data = image.exportData(); + CVCUDA_CHECK_DATA(data); + + long2 strides{data->plane(0).rowStride, sizeof(VT)}; + long2 shape{data->plane(0).height, data->plane(0).width}; + + std::vector imageBuffer(strides.x * shape.x); + + FillBuffer(imageBuffer, shape, strides, valuesGenerator); + + CUDA_CHECK_ERROR(cudaMemcpy2D(data->plane(0).basePtr, strides.x, imageBuffer.data(), strides.x, strides.x, + data->plane(0).height, cudaMemcpyHostToDevice)); + + imageBatch.pushBack(image); + } +} + +/** + * Map a packed vector type to its planar (NCHW) NVCV image format, in the + * RGB/RGBA convention: uchar3->RGB8p, uchar4->RGBA8p, float3->RGBf32p, + * float4->RGBAf32p. Shared by the planar paths of the bench operators so the + * mapping is not duplicated across bench translation units. + */ +template +inline nvcv::ImageFormat GetPlanarFormat() +{ + using BT = typename nvcv::cuda::BaseType; + constexpr int C = nvcv::cuda::NumElements; + + if constexpr (std::is_same_v && C == 3) + { + return nvcv::FMT_RGB8p; + } + else if constexpr (std::is_same_v && C == 4) + { + return nvcv::FMT_RGBA8p; + } + else if constexpr (std::is_same_v && C == 3) + { + return nvcv::FMT_RGBf32p; + } + else if constexpr (std::is_same_v && C == 4) + { + return nvcv::FMT_RGBAf32p; + } + else + { + throw std::invalid_argument("Unsupported planar benchmark data type"); + } +} + +// Fill a var-shape batch with planar images of (up to) the given size. `checker` selects between a +// checkerboard pattern (inputs) and a zero fill (outputs); the latter is useful when the destination +// images differ in size from the sources (e.g. Resize/PillowResize) and cannot be built "like" the +// source batch. +template +inline void FillPlanarImageBatch(nvcv::ImageBatchVarShape &imageBatch, long2 size, long2 varSize, bool checker = true) +{ + using BT = typename nvcv::cuda::BaseType; + + nvcv::ImageFormat format = GetPlanarFormat(); + auto randomWidth = LcgValues(static_cast(size.x - varSize.x), static_cast(size.x)); + auto randomHeight = LcgValues(static_cast(size.y - varSize.y), static_cast(size.y)); + + const BT hi = std::is_integral_v ? nvcv::cuda::TypeTraits::max : static_cast(1); + const auto lo = static_cast(0); + + for (int i = 0; i < imageBatch.capacity(); ++i) + { + nvcv::Image image(nvcv::Size2D{randomWidth(), randomHeight()}, format); + + auto data = image.exportData(); + CVCUDA_CHECK_DATA(data); + + for (int p = 0; p < format.numPlanes(); ++p) + { + if (checker) + { + const size_t nElements + = static_cast(data->plane(p).rowStride) * data->plane(p).height / sizeof(BT); + const size_t rowPitch = static_cast(data->plane(p).rowStride) / sizeof(BT); + + launchCheckerboardImageTyped(reinterpret_cast(data->plane(p).basePtr), nElements, rowPitch, 1, + hi, lo, /*stream*/ nullptr); + } + else + { + CUDA_CHECK_ERROR(cudaMemset2D(data->plane(p).basePtr, data->plane(p).rowStride, 0, + data->plane(p).rowStride, data->plane(p).height)); + } + } + + imageBatch.pushBack(image); + } + + CUDA_CHECK_ERROR(cudaStreamSynchronize(nullptr)); +} + +// Fill a planar var-shape batch with the same deterministic LCG stream used by +// FillImageBatch's packed interleaved fast path. Use this for data-dependent +// operators whose Python benchmarks use create_image_batch_varshape(..., +// fill_mode="lcg") for planar images. +template +inline void FillPlanarImageBatchLcg(nvcv::ImageBatchVarShape &imageBatch, long2 size, long2 varSize) +{ + using BT = typename nvcv::cuda::BaseType; + + nvcv::ImageFormat format = GetPlanarFormat(); + auto randomWidth = LcgValues(static_cast(size.x - varSize.x), static_cast(size.x)); + auto randomHeight = LcgValues(static_cast(size.y - varSize.y), static_cast(size.y)); + + for (int i = 0; i < imageBatch.capacity(); ++i) + { + nvcv::Image image(nvcv::Size2D{randomWidth(), randomHeight()}, format); + + auto data = image.exportData(); + CVCUDA_CHECK_DATA(data); + + for (int p = 0; p < format.numPlanes(); ++p) + { + const size_t nElements = static_cast(data->plane(p).rowStride) * data->plane(p).height / sizeof(BT); + launchRandomFillTyped(reinterpret_cast(data->plane(p).basePtr), nElements, + /*seed*/ 0x9e3779b97f4a7c15ULL + static_cast(i), + /*stream*/ nullptr); + } + + imageBatch.pushBack(image); + } + + CUDA_CHECK_ERROR(cudaStreamSynchronize(nullptr)); +} + +/** + * Fill an ImageBatchVarShape with planar (NCHW) images matching the per-sample + * sizes of a source batch, zero-initialized. Use for dst when src/dst shapes + * must match (e.g. Normalize). + */ +template +inline void FillPlanarImageBatchLike(nvcv::ImageBatchVarShape &imageBatch, const nvcv::ImageBatchVarShape &srcBatch) +{ + nvcv::ImageFormat format = GetPlanarFormat(); + + for (int i = 0; i < srcBatch.numImages(); ++i) + { + nvcv::Image srcImage = srcBatch[i]; + auto srcData = srcImage.exportData(); + CVCUDA_CHECK_DATA(srcData); + + nvcv::Image image(nvcv::Size2D{srcData->plane(0).width, srcData->plane(0).height}, format); + + auto data = image.exportData(); + CVCUDA_CHECK_DATA(data); + + for (int p = 0; p < format.numPlanes(); ++p) + { + CUDA_CHECK_ERROR(cudaMemset2D(data->plane(p).basePtr, data->plane(p).rowStride, 0, data->plane(p).rowStride, + data->plane(p).height)); + } + + imageBatch.pushBack(image); + } + + CUDA_CHECK_ERROR(cudaStreamSynchronize(nullptr)); +} + +/** + * Run warmup iterations to stabilize GPU state before benchmarking. + * + * This function runs the operator multiple times before the timed benchmark loop. + * This helps stabilize GPU frequency/power state and can reduce benchmark noise, + * especially for operators with small execution times or non-power-of-2 data types. + * + * Creates and destroys a CUDA stream internally. If iterations is 0, does nothing. + * + * @param iterations Number of warmup iterations (0 = disabled, use BENCH__WARMUP_ITERATIONS from config) + * @param func The operator invocation lambda, taking cudaStream_t as parameter + * + * Example: + * benchutils::warmup(BENCH_STACK_WARMUP_ITERATIONS, [&op, &src, &dst](cudaStream_t s) { + * op(s, src, dst); + * }); + */ +template +inline void warmup(int iterations, Func &&func) +{ + iterations = resolve_warmup_iterations(iterations); + if (iterations <= 0) + return; + + cudaStream_t stream; + CUDA_CHECK_ERROR(cudaStreamCreate(&stream)); + for (int i = 0; i < iterations; ++i) + { + func(stream); + } + CUDA_CHECK_ERROR(cudaStreamSynchronize(stream)); + CUDA_CHECK_ERROR(cudaStreamDestroy(stream)); +} + +/** + * Execute benchmark with sync tag enabled (matching Python benchmarks' sync=True default). + * + * This helper function wraps state.exec() with exec_tag::sync, which: + * - Disables nvbench's deadlock detection + * - Uses CPU-based timing instead of GPU events + * - Indicates that the kernel may perform internal synchronization + * + * All CV-CUDA operators may perform internal synchronization (especially in VarShape paths), + * so this should be used by default for all benchmarks to match Python behavior. + * + * @param state The nvbench state object + * @param func The lambda/function to execute, taking nvbench::launch& as parameter + * + * Example: + * benchutils::exec_with_sync(state, [&op, &src, &dst](nvbench::launch &launch) { + * op(launch.get_stream(), src, dst); + * }); + */ +template +inline void exec_with_sync(nvbench::state &state, Func &&func) +{ + state.exec(nvbench::exec_tag::sync, std::forward(func)); +} + +/** + * Combined warmup and benchmark execution. + * + * Runs warmup iterations then executes the timed benchmark, avoiding lambda duplication. + * The provided function should take a cudaStream_t parameter. + * + * @param state The nvbench state object + * @param warmup_iterations Number of warmup iterations (0 = disabled) + * @param func The operator invocation lambda, taking cudaStream_t as parameter + * + * Example: + * benchutils::warmup_and_exec(state, BENCH_COMPOSITE_WARMUP_ITERATIONS, + * [&op, &fg, &bg, &mask, &dst](cudaStream_t s) { op(s, fg, bg, mask, dst); }); + */ +template +inline void warmup_and_exec(nvbench::state &state, int warmup_iterations, Func &&func) +{ + warmup(warmup_iterations, func); + exec_with_sync(state, [&func](nvbench::launch &launch) { func(launch.get_stream()); }); +} + +} // namespace benchutils + +#endif // CVCUDA_CPP_BENCH_UTILS_HPP diff --git a/bench/cpp/GenerateBenchConfig.cmake b/bench/cpp/GenerateBenchConfig.cmake new file mode 100644 index 000000000..6632b06aa --- /dev/null +++ b/bench/cpp/GenerateBenchConfig.cmake @@ -0,0 +1,337 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Script to generate C++ benchmark configuration headers from JSON +# +# This script generates BenchConfig.hpp headers containing +# macro definitions for benchmark axis configurations. +# +# Usage: +# cmake -DJSON_FILE= -DOPERATOR= -DOPERATOR_CAMEL= -DOUTPUT_FILE= -P GenerateBenchConfig.cmake +# +# Parameters: +# JSON_FILE - Path to an operator JSON file +# OPERATOR - Lowercase operator name (e.g., "resize", "adaptivethreshold") +# OPERATOR_CAMEL - CamelCase operator name (e.g., "Resize", "AdaptiveThreshold") +# OUTPUT_FILE - Output path for generated header file +# +# Output: +# Generates a header file with format BenchConfig.hpp containing +# a BENCH__AXES macro that can be used in NVBENCH_BENCH_TYPES declarations. +# +# Aggregation semantics: +# Each operator JSON holds multiple config entries for the operator (e.g. one +# per tier × variant). This generator unions per-operator metadata across ALL +# entries with `benchmark` == OPERATOR so the compiled binary's axis +# registry is a superset of every value any tier asks for at runtime: +# - dtypes: union of dtype lists → nvbench::type_list<...> +# - string_axes: per-axis-name union of string values +# - int64_axes: per-axis-name union of int64 values +# - float64_axes: per-axis-name union of float64 values +# - warmup: first-match wins (single scalar; no obvious merge semantic) +# +# Without union behavior, an axis or dtype that lives only in one tier's +# entry would silently desync from the binary's compile-time registry — +# nvbench would error at runtime with "Unknown axis" when run_bench.py +# passes the override CLI flag. + +file(READ "${JSON_FILE}" JSON_CONTENT) + +set(CONFIG_ROOT "${JSON_CONTENT}") +set(TOP_LEVEL_BENCHMARK "") +string(JSON _CONFIGS_OBJ ERROR_VARIABLE CONFIGS_ERR GET "${JSON_CONTENT}" "configs") +string(JSON _TOP_BENCHMARK ERROR_VARIABLE TOP_BENCHMARK_ERR GET "${JSON_CONTENT}" "benchmark") +if(NOT CONFIGS_ERR AND NOT TOP_BENCHMARK_ERR) + set(CONFIG_ROOT "${_CONFIGS_OBJ}") + set(TOP_LEVEL_BENCHMARK "${_TOP_BENCHMARK}") +endif() + +if(NOT DEFINED AXIS_ORDER_HELPER) + set(AXIS_ORDER_HELPER "${CMAKE_CURRENT_LIST_DIR}/../config/axis_order.py") +endif() +find_program(AXIS_ORDER_PYTHON_EXECUTABLE NAMES python3 REQUIRED) +execute_process( + COMMAND "${AXIS_ORDER_PYTHON_EXECUTABLE}" "${AXIS_ORDER_HELPER}" + --config "${JSON_FILE}" --operator "${OPERATOR}" + RESULT_VARIABLE AXIS_ORDER_RESULT + OUTPUT_VARIABLE AXIS_ORDER_OUTPUT + ERROR_VARIABLE AXIS_ORDER_ERROR + OUTPUT_STRIP_TRAILING_WHITESPACE +) +if(NOT AXIS_ORDER_RESULT EQUAL 0) + message(FATAL_ERROR "Failed to resolve axis order: ${AXIS_ORDER_ERROR}") +endif() +string(REPLACE "\n" ";" AXIS_ORDER "${AXIS_ORDER_OUTPUT}") + +# --------------------------------------------------------------------------- +# Pass 1: scan ALL top-level entries; for each entry whose "benchmark" field +# matches OPERATOR, accumulate dtypes and per-axis values. +# +# CMake has no dict type — we fake one by maintaining ordered "name" lists +# per axis category, plus a per-(name) values list whose variable name is +# constructed from the axis name. This preserves first-seen ordering so the +# generated header is deterministic across runs. +# --------------------------------------------------------------------------- + +set(ALL_DTYPES "") +set(STRING_AXIS_NAMES "") +set(INT64_AXIS_NAMES "") +set(FLOAT64_AXIS_NAMES "") +set(WARMUP_ITERATIONS "") +set(FOUND_CONFIG FALSE) + +string(JSON NUM_ENTRIES LENGTH "${CONFIG_ROOT}") +if(NUM_ENTRIES GREATER 0) + math(EXPR NUM_ENTRIES_MINUS_1 "${NUM_ENTRIES} - 1") + foreach(IDX RANGE 0 ${NUM_ENTRIES_MINUS_1}) + string(JSON ENTRY_KEY MEMBER "${CONFIG_ROOT}" ${IDX}) + string(JSON ENTRY_CONFIG GET "${CONFIG_ROOT}" "${ENTRY_KEY}") + if(TOP_LEVEL_BENCHMARK STREQUAL "") + string(JSON BENCHMARK_FIELD ERROR_VARIABLE BM_ERR GET "${ENTRY_CONFIG}" "benchmark") + else() + set(BENCHMARK_FIELD "${TOP_LEVEL_BENCHMARK}") + set(BM_ERR "") + endif() + + # Skip entries that don't target this operator. Entries with no + # "benchmark" field in old-shape files are invalid but not fatal here — they're caught + # at run time by load_config.py's validator. + if(BM_ERR OR NOT BENCHMARK_FIELD STREQUAL "${OPERATOR}") + continue() + endif() + + set(FOUND_CONFIG TRUE) + + # warmup_iterations: first-match wins. + if(WARMUP_ITERATIONS STREQUAL "") + string(JSON WARMUP_VAL ERROR_VARIABLE WARMUP_ERR GET "${ENTRY_CONFIG}" "warmup_iterations") + if(NOT WARMUP_ERR) + set(WARMUP_ITERATIONS "${WARMUP_VAL}") + endif() + endif() + + # dtypes: union. + string(JSON ENTRY_DTYPES ERROR_VARIABLE DTYPE_ERR GET "${ENTRY_CONFIG}" "dtypes") + if(NOT DTYPE_ERR) + string(JSON NUM_ENTRY_DTYPES LENGTH "${ENTRY_DTYPES}") + if(NUM_ENTRY_DTYPES GREATER 0) + math(EXPR NUM_ENTRY_DTYPES_MINUS_1 "${NUM_ENTRY_DTYPES} - 1") + foreach(DIDX RANGE 0 ${NUM_ENTRY_DTYPES_MINUS_1}) + string(JSON DTYPE GET "${ENTRY_DTYPES}" ${DIDX}) + list(APPEND ALL_DTYPES "${DTYPE}") + endforeach() + endif() + endif() + + # string_axes / int64_axes / float64_axes: per-axis-name union. + # We expand the same loop body for each category since CMake lacks + # closures. + foreach(CATEGORY string_axes int64_axes float64_axes) + string(JSON CAT_OBJ ERROR_VARIABLE CAT_ERR GET "${ENTRY_CONFIG}" "${CATEGORY}") + if(CAT_ERR) + continue() + endif() + string(JSON NUM_AXES LENGTH "${CAT_OBJ}") + if(NUM_AXES LESS_EQUAL 0) + continue() + endif() + + if(CATEGORY STREQUAL "string_axes") + set(NAMES_VAR STRING_AXIS_NAMES) + set(PFX STRING_AXIS_VALUES_) + elseif(CATEGORY STREQUAL "int64_axes") + set(NAMES_VAR INT64_AXIS_NAMES) + set(PFX INT64_AXIS_VALUES_) + else() + set(NAMES_VAR FLOAT64_AXIS_NAMES) + set(PFX FLOAT64_AXIS_VALUES_) + endif() + + math(EXPR NUM_AXES_MINUS_1 "${NUM_AXES} - 1") + foreach(AIDX RANGE 0 ${NUM_AXES_MINUS_1}) + string(JSON AXIS_NAME MEMBER "${CAT_OBJ}" ${AIDX}) + string(JSON AXIS_VALUES GET "${CAT_OBJ}" "${AXIS_NAME}") + + # First-seen tracking: append to ordered names list iff new. + set(NAMES_LIST "${${NAMES_VAR}}") + list(FIND NAMES_LIST "${AXIS_NAME}" _SEEN_IDX) + if(_SEEN_IDX EQUAL -1) + list(APPEND NAMES_LIST "${AXIS_NAME}") + set(${NAMES_VAR} "${NAMES_LIST}") + endif() + + # Append this entry's values to the per-axis values list. + # We dedupe the merged list at emit time, not here, so the + # first-seen value ordering is preserved. + string(JSON NUM_VALUES LENGTH "${AXIS_VALUES}") + if(NUM_VALUES GREATER 0) + math(EXPR NUM_VALUES_MINUS_1 "${NUM_VALUES} - 1") + foreach(VIDX RANGE 0 ${NUM_VALUES_MINUS_1}) + string(JSON VAL GET "${AXIS_VALUES}" ${VIDX}) + list(APPEND ${PFX}${AXIS_NAME} "${VAL}") + endforeach() + endif() + endforeach() + endforeach() + endforeach() +endif() + +if(NOT FOUND_CONFIG) + message(FATAL_ERROR "Operator '${OPERATOR}' not found in ${JSON_FILE}") +endif() + +# Dedupe dtypes (preserves first-seen order; CMake's REMOVE_DUPLICATES is +# stable from 3.0+). +list(REMOVE_DUPLICATES ALL_DTYPES) + +# Map dtype strings to C++ types +set(TYPES_LIST "") +foreach(DTYPE IN LISTS ALL_DTYPES) + if(DTYPE STREQUAL "uint8") + list(APPEND TYPES_LIST "uint8_t") + elseif(DTYPE STREQUAL "uint16") + list(APPEND TYPES_LIST "uint16_t") + elseif(DTYPE STREQUAL "uint32") + list(APPEND TYPES_LIST "uint32_t") + elseif(DTYPE STREQUAL "uint64") + list(APPEND TYPES_LIST "uint64_t") + elseif(DTYPE STREQUAL "int8") + list(APPEND TYPES_LIST "int8_t") + elseif(DTYPE STREQUAL "int16") + list(APPEND TYPES_LIST "int16_t") + elseif(DTYPE STREQUAL "int32") + list(APPEND TYPES_LIST "int32_t") + elseif(DTYPE STREQUAL "int64") + list(APPEND TYPES_LIST "int64_t") + elseif(DTYPE STREQUAL "float32") + list(APPEND TYPES_LIST "float") + elseif(DTYPE STREQUAL "float64") + list(APPEND TYPES_LIST "double") + elseif(DTYPE STREQUAL "bool") + list(APPEND TYPES_LIST "bool") + elseif(DTYPE STREQUAL "uchar3") + list(APPEND TYPES_LIST "uchar3") + elseif(DTYPE STREQUAL "uchar4") + list(APPEND TYPES_LIST "uchar4") + elseif(DTYPE STREQUAL "float3") + list(APPEND TYPES_LIST "float3") + elseif(DTYPE STREQUAL "float4") + list(APPEND TYPES_LIST "float4") + elseif(DTYPE STREQUAL "short2") + list(APPEND TYPES_LIST "short2") + elseif(DTYPE STREQUAL "ushort3") + list(APPEND TYPES_LIST "ushort3") + elseif(DTYPE STREQUAL "ushort4") + list(APPEND TYPES_LIST "ushort4") + elseif(DTYPE STREQUAL "short4") + list(APPEND TYPES_LIST "short4") + else() + message(FATAL_ERROR "Unsupported dtype '${DTYPE}' for operator '${OPERATOR}'. " + "Extend GenerateBenchConfig.cmake to handle this type.") + endif() +endforeach() +string(REPLACE ";" ", " TYPES_STR "${TYPES_LIST}") + +# --------------------------------------------------------------------------- +# Pass 2: emit per-axis macro lines for each category. +# --------------------------------------------------------------------------- + +# Helper to emit " .add__axis(\"name\", {v1, v2, ...}) \\\n" for one axis. +# CMake macros don't return; we accumulate into a caller-named output var. +macro(emit_axis_line OUT_VAR ADDER NAME VALUES_LIST_VAR QUOTE_VALUES) + set(_VALS "${${VALUES_LIST_VAR}}") + list(REMOVE_DUPLICATES _VALS) + set(_FORMATTED "") + foreach(_V IN LISTS _VALS) + # Macro args aren't bound as variables; STREQUAL on the substituted + # text dodges CMake policy CMP0012 boolean-literal warnings. + if("${QUOTE_VALUES}" STREQUAL "TRUE") + list(APPEND _FORMATTED "\"${_V}\"") + else() + list(APPEND _FORMATTED "${_V}") + endif() + endforeach() + string(REPLACE ";" ", " _STR "${_FORMATTED}") + string(APPEND ${OUT_VAR} " .${ADDER}(\"${NAME}\", {${_STR}}) \\\n") +endmacro() + +set(ALL_AXES_CODE "") +foreach(NAME IN LISTS AXIS_ORDER) + list(FIND STRING_AXIS_NAMES "${NAME}" STRING_IDX) + list(FIND INT64_AXIS_NAMES "${NAME}" INT64_IDX) + list(FIND FLOAT64_AXIS_NAMES "${NAME}" FLOAT64_IDX) + if(NOT STRING_IDX EQUAL -1) + emit_axis_line(ALL_AXES_CODE "add_string_axis" "${NAME}" STRING_AXIS_VALUES_${NAME} TRUE) + elseif(NOT INT64_IDX EQUAL -1) + emit_axis_line(ALL_AXES_CODE "add_int64_axis" "${NAME}" INT64_AXIS_VALUES_${NAME} FALSE) + elseif(NOT FLOAT64_IDX EQUAL -1) + emit_axis_line(ALL_AXES_CODE "add_float64_axis" "${NAME}" FLOAT64_AXIS_VALUES_${NAME} FALSE) + else() + message(FATAL_ERROR "axis_order references undefined axis '${NAME}'") + endif() +endforeach() + +set(DISCOVERED_AXIS_NAMES ${STRING_AXIS_NAMES} ${INT64_AXIS_NAMES} ${FLOAT64_AXIS_NAMES}) +list(REMOVE_DUPLICATES DISCOVERED_AXIS_NAMES) +foreach(NAME IN LISTS DISCOVERED_AXIS_NAMES) + list(FIND AXIS_ORDER "${NAME}" ORDERED_IDX) + if(ORDERED_IDX EQUAL -1) + message(FATAL_ERROR "Axis-order helper omitted discovered axis '${NAME}'") + endif() +endforeach() + +# warmup_iterations default = 0 if the operator never set one. +if(WARMUP_ITERATIONS STREQUAL "") + set(WARMUP_ITERATIONS 0) +endif() + +# Convert operator name to uppercase for header guard and macro +string(TOUPPER "${OPERATOR}" OPERATOR_UPPER) + +# Remove trailing backslash and newline from the final combined axes code (last 3 chars: space, backslash, newline) +string(LENGTH "${ALL_AXES_CODE}" CODE_LENGTH) +if(CODE_LENGTH GREATER 3) + math(EXPR TRIM_LENGTH "${CODE_LENGTH} - 3") + string(SUBSTRING "${ALL_AXES_CODE}" 0 ${TRIM_LENGTH} ALL_AXES_CODE) +endif() + +# Generate the header content +set(HEADER_CONTENT "// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: bench/config/operators/${OPERATOR}.json +// Operator: ${OPERATOR} +// Generation script: bench/cpp/GenerateBenchConfig.cmake +// +// To modify benchmark parameters: +// 1. Edit bench/config/operators/${OPERATOR}.json +// 2. Rebuild the project (cmake --build) +// +// This file provides default axis configurations for the ${OPERATOR_CAMEL} operator benchmark. +// CLI arguments can still override these defaults at runtime. +// +// dtypes and per-axis values are unioned across ALL entries in +// the operator JSON whose `benchmark` field equals \"${OPERATOR}\". This +// keeps the compile-time axis registry a superset of every value any tier +// asks for, so --tier-driven --axis overrides at runtime always bind to +// existing axes. + +#ifndef CVCUDA_BENCH_${OPERATOR_UPPER}_CONFIG_HPP +#define CVCUDA_BENCH_${OPERATOR_UPPER}_CONFIG_HPP + +#include + +// Auto-generated type list from dtypes in the operator config JSON +// Use BENCH_${OPERATOR_UPPER}_TYPES in NVBENCH_BENCH_TYPES to use config-based types +#define BENCH_${OPERATOR_UPPER}_TYPES nvbench::type_list<${TYPES_STR}> + +// Number of warmup iterations to run before benchmarking (0 = disabled) +#define BENCH_${OPERATOR_UPPER}_WARMUP_ITERATIONS ${WARMUP_ITERATIONS} + +#define BENCH_${OPERATOR_UPPER}_AXES \\ + .set_type_axes_names({\"InOutDataType\"}) \\ +${ALL_AXES_CODE} + +#endif // CVCUDA_BENCH_${OPERATOR_UPPER}_CONFIG_HPP +") + +# Write the generated file +file(WRITE "${OUTPUT_FILE}" "${HEADER_CONTENT}") diff --git a/bench/cpp/WarmupPolicy.hpp b/bench/cpp/WarmupPolicy.hpp new file mode 100644 index 000000000..0dae186a5 --- /dev/null +++ b/bench/cpp/WarmupPolicy.hpp @@ -0,0 +1,61 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef CVCUDA_WARMUP_POLICY_HPP +#define CVCUDA_WARMUP_POLICY_HPP + +#include +#include +#include +#include +#include +#include + +namespace benchutils { + +constexpr const char *WARMUP_CAP_ENV = "CVCUDA_BENCH_WARMUP_CAP"; + +inline int parse_warmup_cap(std::string_view rawValue) +{ + if (rawValue.empty()) + { + throw std::invalid_argument(std::string(WARMUP_CAP_ENV) + " must be a nonnegative decimal integer"); + } + + int cap = 0; + for (char ch : rawValue) + { + if (ch < '0' || ch > '9') + { + throw std::invalid_argument(std::string(WARMUP_CAP_ENV) + " must be a nonnegative decimal integer"); + } + + int digit = ch - '0'; + if (cap > (std::numeric_limits::max() - digit) / 10) + { + throw std::invalid_argument(std::string(WARMUP_CAP_ENV) + " exceeds the supported integer range"); + } + cap = cap * 10 + digit; + } + return cap; +} + +inline int resolve_warmup_iterations(int configuredIterations, const char *rawCap) +{ + if (configuredIterations <= 0 || rawCap == nullptr) + { + return configuredIterations; + } + return std::min(configuredIterations, parse_warmup_cap(rawCap)); +} + +inline int resolve_warmup_iterations(int configuredIterations) +{ + return resolve_warmup_iterations(configuredIterations, std::getenv(WARMUP_CAP_ENV)); +} + +} // namespace benchutils + +#endif // CVCUDA_WARMUP_POLICY_HPP diff --git a/bench/cpp/ops/BenchAdaptiveThreshold.cpp b/bench/cpp/ops/BenchAdaptiveThreshold.cpp new file mode 100644 index 000000000..ac22f4fc2 --- /dev/null +++ b/bench/cpp/ops/BenchAdaptiveThreshold.cpp @@ -0,0 +1,134 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchAdaptiveThresholdConfig.hpp" + +#include +#include + +#include + +template +inline void adaptivethreshold(nvbench::state &state, nvbench::type_list) +try +{ + int3 shape = benchutils::GetShape<3, int3>(state.get_string("shape")); + auto layout = benchutils::GetStringParam(state, "layout", "NHWC"); + const benchutils::InputKind inputKind = benchutils::GetInputKind(state.get_string("inputKind")); + int blockSize = benchutils::GetIntParam(state, "blockSize"); + + const bool isPlanar = layout == "NCHW"; + const bool isFakePlanar = layout == "NCHW_FAKE"; + if (layout != "NHWC" && layout != "NCHW" && layout != "NCHW_FAKE") + { + state.skip("AdaptiveThreshold benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts"); + return; + } + if ((isPlanar || isFakePlanar) && inputKind == benchutils::InputKind::VarShape) + { + state.skip("Planar AdaptiveThreshold benchmark rows are tensor-only"); + return; + } + + NVCVThresholdType threshType = NVCV_THRESH_BINARY; + NVCVAdaptiveThresholdType adaptType = NVCV_ADAPTIVE_THRESH_GAUSSIAN_C; + + double maxValue = 123.; + double c = -2.3; + + const long bytes = static_cast(shape.x) * shape.y * shape.z * sizeof(T); + if (isFakePlanar) + { + state.add_global_memory_reads(3 * bytes); + state.add_global_memory_writes(3 * bytes); + } + else + { + state.add_global_memory_reads(bytes); + state.add_global_memory_writes(bytes); + } + + cvcuda::AdaptiveThreshold op(blockSize, shape.x); + + // clang-format off + + if (isFakePlanar) + { + nvcv::Tensor src ({{shape.x, 1, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + nvcv::Tensor interSrc({{shape.x, shape.y, shape.z, 1}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor interDst({{shape.x, shape.y, shape.z, 1}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst ({{shape.x, 1, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + cvcuda::Reformat reformatOp; + + benchutils::warmup_and_exec(state, BENCH_ADAPTIVETHRESHOLD_WARMUP_ITERATIONS, + [&op, &reformatOp, &src, &interSrc, &interDst, &dst, &maxValue, &adaptType, &threshType, &blockSize, &c](cudaStream_t s) { + reformatOp(s, src, interSrc); + op(s, interSrc, interDst, maxValue, adaptType, threshType, blockSize, c); + reformatOp(s, interDst, dst); + }); + } + else if (inputKind == benchutils::InputKind::Tensor) + { + nvcv::Tensor src = isPlanar + ? nvcv::Tensor({{shape.x, 1, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, 1}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst = isPlanar + ? nvcv::Tensor({{shape.x, 1, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, 1}, "NHWC"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + benchutils::warmup_and_exec(state, BENCH_ADAPTIVETHRESHOLD_WARMUP_ITERATIONS, + [&op, &src, &dst, &maxValue, &adaptType, &threshType, &blockSize, &c](cudaStream_t s) { + op(s, src, dst, maxValue, adaptType, threshType, blockSize, c); + }); + } + else // zero and positive var shape means use ImageBatchVarShape + { + nvcv::ImageBatchVarShape src(shape.x); + nvcv::ImageBatchVarShape dst(shape.x); + + benchutils::FillImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}, + benchutils::CheckerboardValues()); + // Use FillImageBatchLike to ensure dst has same per-sample shapes as src + benchutils::FillImageBatchLike(dst, src, [](const long4_16a &) { return T{0}; }); + + nvcv::Tensor maxValueTensor({{shape.x}, "N"}, nvcv::TYPE_F64); + nvcv::Tensor blockSizeTensor({{shape.x}, "N"}, nvcv::TYPE_S32); + nvcv::Tensor cTensor({{shape.x}, "N"}, nvcv::TYPE_F64); + + benchutils::FillTensor(maxValueTensor, [&maxValue](const long4_16a &){ return maxValue; }); + benchutils::FillTensor(blockSizeTensor, [&blockSize](const long4_16a &){ return blockSize; }); + benchutils::FillTensor(cTensor, [&c](const long4_16a &){ return c; }); + + benchutils::warmup_and_exec(state, BENCH_ADAPTIVETHRESHOLD_WARMUP_ITERATIONS, + [&op, &src, &dst, &maxValueTensor, &adaptType, &threshType, &blockSizeTensor, &cTensor](cudaStream_t s) { + op(s, src, dst, maxValueTensor, adaptType, threshType, blockSizeTensor, cTensor); + }); + } +} +CVCUDA_BENCH_SKIP_ERRORS(state) + +// clang-format on + +// Use auto-generated type list from bench_params.json +NVBENCH_BENCH_TYPES(adaptivethreshold, NVBENCH_TYPE_AXES(BENCH_ADAPTIVETHRESHOLD_TYPES)) +BENCH_ADAPTIVETHRESHOLD_AXES; diff --git a/bench/cpp/ops/BenchAdjustContrast.cpp b/bench/cpp/ops/BenchAdjustContrast.cpp new file mode 100644 index 000000000..cdd533547 --- /dev/null +++ b/bench/cpp/ops/BenchAdjustContrast.cpp @@ -0,0 +1,135 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchAdjustContrastConfig.hpp" + +#include +#include + +#include + +// Benchmark for the Adjust Contrast operator across the interleaved (NHWC), native planar (NCHW), +// fake-planar (NCHW_FAKE), and ImageBatchVarShape paths. AdjustContrast is a two-pass operator: a +// per-image grayscale-mean reduction followed by the clamped affine blend. +template +inline void adjustcontrast(nvbench::state &state, nvbench::type_list) +try +{ + int3 shape = benchutils::GetShape<3, int3>(state.get_string("shape")); + auto layout = benchutils::GetStringParam(state, "layout", "NHWC"); + const benchutils::InputKind inputKind = benchutils::GetInputKind(state.get_string("inputKind")); + + const bool isPlanar = layout == "NCHW"; + const bool isFakePlanar = layout == "NCHW_FAKE"; + if (layout != "NHWC" && layout != "NCHW" && layout != "NCHW_FAKE") + { + state.skip("AdjustContrast benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts"); + return; + } + if (isFakePlanar && inputKind == benchutils::InputKind::VarShape) + { + state.skip("Fake-planar (NCHW_FAKE) adjust_contrast benchmark is tensor-only"); + return; + } + + using BT = typename nvcv::cuda::BaseType; + + // Fixed contrast factor > 1: blends away from the grayscale mean and forces the [0, bound] clamp + // on both ends, matching the correctness matrix's work path. + const double contrastFactor = 1.5; + + int ch = nvcv::cuda::NumElements; + + // AdjustContrast reads the image once for the mean reduction and once for the blend, and writes + // it once (plus the reformat traffic on the fake-planar path). + const long bytes = static_cast(shape.x) * shape.y * shape.z * sizeof(T); + if (isFakePlanar) + { + state.add_global_memory_reads(4 * bytes); + state.add_global_memory_writes(3 * bytes); + } + else + { + state.add_global_memory_reads(2 * bytes); + state.add_global_memory_writes(bytes); + } + + cvcuda::AdjustContrast op; + + // clang-format off + if (isFakePlanar) // tensor-only: planar->interleaved->adjust->interleaved->planar + { + nvcv::Tensor src ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + nvcv::Tensor interSrc({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor interDst({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + cvcuda::Reformat reformatOp; + + benchutils::warmup_and_exec(state, BENCH_ADJUSTCONTRAST_WARMUP_ITERATIONS, + [&op, &reformatOp, &src, &interSrc, &interDst, &dst, contrastFactor](cudaStream_t s) { + reformatOp(s, src, interSrc); // NCHW -> NHWC + op(s, interSrc, interDst, contrastFactor); // interleaved adjust + reformatOp(s, interDst, dst); // NHWC -> NCHW + }); + } + else if (inputKind == benchutils::InputKind::Tensor) + { + nvcv::Tensor src = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + benchutils::warmup_and_exec(state, BENCH_ADJUSTCONTRAST_WARMUP_ITERATIONS, + [&op, &src, &dst, contrastFactor](cudaStream_t s) { op(s, src, dst, contrastFactor); }); + } + else // ImageBatchVarShape + { + nvcv::ImageBatchVarShape src(shape.x); + nvcv::ImageBatchVarShape dst(shape.x); + + if (isPlanar) + { + benchutils::FillPlanarImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}); + benchutils::FillPlanarImageBatch(dst, long2{shape.z, shape.y}, long2{0, 0}); + } + else + { + benchutils::FillImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}, + benchutils::CheckerboardValues()); + benchutils::FillImageBatchLike(dst, src, [](const long4_16a &) { return T{0}; }); + } + + benchutils::warmup_and_exec(state, BENCH_ADJUSTCONTRAST_WARMUP_ITERATIONS, + [&op, &src, &dst, contrastFactor](cudaStream_t s) { op(s, src, dst, contrastFactor); }); + } +} +CVCUDA_BENCH_SKIP_ERRORS(state) + +// clang-format on + +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(adjustcontrast, NVBENCH_TYPE_AXES(BENCH_ADJUSTCONTRAST_TYPES)) +BENCH_ADJUSTCONTRAST_AXES; diff --git a/bench/cpp/ops/BenchAdjustHue.cpp b/bench/cpp/ops/BenchAdjustHue.cpp new file mode 100644 index 000000000..667693846 --- /dev/null +++ b/bench/cpp/ops/BenchAdjustHue.cpp @@ -0,0 +1,135 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchAdjustHueConfig.hpp" + +#include +#include + +#include + +// Hue factor is irrelevant to throughput; use a fixed mid-range value. +static constexpr double kHue = 0.25; + +template +inline void adjusthue(nvbench::state &state, nvbench::type_list) +try +{ + int3 shape = benchutils::GetShape<3, int3>(state.get_string("shape")); + auto layout = benchutils::GetStringParam(state, "layout", "NHWC"); + const benchutils::InputKind inputKind = benchutils::GetInputKind(state.get_string("inputKind")); + + const bool isPlanar = layout == "NCHW"; + const bool isFakePlanar = layout == "NCHW_FAKE"; + if (layout != "NHWC" && layout != "NCHW" && layout != "NCHW_FAKE") + { + state.skip("AdjustHue benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts"); + return; + } + // NCHW_FAKE ("fake planar") is a tensor-only comparison path: planar data is reformatted to + // interleaved, adjusted with the interleaved kernel, and reformatted back — all timed together — + // so the native planar path (NCHW) can be shown to beat the naive convert->op->convert pipeline. + if (isFakePlanar && inputKind == benchutils::InputKind::VarShape) + { + state.skip("Fake-planar (NCHW_FAKE) adjusthue benchmark is tensor-only"); + return; + } + + using BT = typename nvcv::cuda::BaseType; + + int ch = nvcv::cuda::NumElements; + + // AdjustHue preserves size, so src and dst hold the same number of bytes. + const long bytes = static_cast(shape.x) * shape.y * shape.z * sizeof(T); + if (isFakePlanar) + { + // reformat(NCHW->NHWC) + adjust + reformat(NHWC->NCHW): reads src + interleaved src + dst, + // writes interleaved src + interleaved dst + dst. + state.add_global_memory_reads(3 * bytes); + state.add_global_memory_writes(3 * bytes); + } + else + { + state.add_global_memory_reads(bytes); + state.add_global_memory_writes(bytes); + } + + cvcuda::AdjustHue op; + + // clang-format off + if (isFakePlanar) // tensor-only: planar->interleaved->adjust->interleaved->planar + { + nvcv::Tensor src ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + nvcv::Tensor interSrc({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor interDst({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + cvcuda::Reformat reformatOp; + + benchutils::warmup_and_exec(state, BENCH_ADJUSTHUE_WARMUP_ITERATIONS, + [&op, &reformatOp, &src, &interSrc, &interDst, &dst](cudaStream_t s) { + reformatOp(s, src, interSrc); // NCHW -> NHWC + op(s, interSrc, interDst, kHue); // interleaved adjust + reformatOp(s, interDst, dst); // NHWC -> NCHW + }); + } + else if (inputKind == benchutils::InputKind::Tensor) + { + nvcv::Tensor src = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + benchutils::warmup_and_exec(state, BENCH_ADJUSTHUE_WARMUP_ITERATIONS, + [&op, &src, &dst](cudaStream_t s) { op(s, src, dst, kHue); }); + } + else // ImageBatchVarShape + { + nvcv::ImageBatchVarShape src(shape.x); + nvcv::ImageBatchVarShape dst(shape.x); + + if (isPlanar) + { + benchutils::FillPlanarImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}); + benchutils::FillPlanarImageBatch(dst, long2{shape.z, shape.y}, long2{0, 0}); + } + else + { + benchutils::FillImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}, + benchutils::CheckerboardValues(), benchutils::GetRGBFormat()); + benchutils::FillImageBatchLike(dst, src, [](const long4_16a &) { return T{0}; }); + } + + benchutils::warmup_and_exec(state, BENCH_ADJUSTHUE_WARMUP_ITERATIONS, + [&op, &src, &dst](cudaStream_t s) { op(s, src, dst, kHue); }); + } +} +CVCUDA_BENCH_SKIP_ERRORS(state) + +// clang-format on + +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(adjusthue, NVBENCH_TYPE_AXES(BENCH_ADJUSTHUE_TYPES)) +BENCH_ADJUSTHUE_AXES; diff --git a/bench/cpp/ops/BenchAdjustSaturation.cpp b/bench/cpp/ops/BenchAdjustSaturation.cpp new file mode 100644 index 000000000..9d2fa7f4c --- /dev/null +++ b/bench/cpp/ops/BenchAdjustSaturation.cpp @@ -0,0 +1,135 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchAdjustSaturationConfig.hpp" + +#include +#include + +#include + +// Saturation factor is irrelevant to throughput; use a fixed mid-range value. +static constexpr double kSaturation = 0.5; + +template +inline void adjustsaturation(nvbench::state &state, nvbench::type_list) +try +{ + int3 shape = benchutils::GetShape<3, int3>(state.get_string("shape")); + auto layout = benchutils::GetStringParam(state, "layout", "NHWC"); + const benchutils::InputKind inputKind = benchutils::GetInputKind(state.get_string("inputKind")); + + const bool isPlanar = layout == "NCHW"; + const bool isFakePlanar = layout == "NCHW_FAKE"; + if (layout != "NHWC" && layout != "NCHW" && layout != "NCHW_FAKE") + { + state.skip("AdjustSaturation benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts"); + return; + } + // NCHW_FAKE ("fake planar") is a tensor-only comparison path: planar data is reformatted to + // interleaved, adjusted with the interleaved kernel, and reformatted back — all timed together — + // so the native planar path (NCHW) can be shown to beat the naive convert->op->convert pipeline. + if (isFakePlanar && inputKind == benchutils::InputKind::VarShape) + { + state.skip("Fake-planar (NCHW_FAKE) adjustsaturation benchmark is tensor-only"); + return; + } + + using BT = typename nvcv::cuda::BaseType; + + int ch = nvcv::cuda::NumElements; + + // AdjustSaturation preserves size, so src and dst hold the same number of bytes. + const long bytes = static_cast(shape.x) * shape.y * shape.z * sizeof(T); + if (isFakePlanar) + { + // reformat(NCHW->NHWC) + adjust + reformat(NHWC->NCHW): reads src + interleaved src + dst, + // writes interleaved src + interleaved dst + dst. + state.add_global_memory_reads(3 * bytes); + state.add_global_memory_writes(3 * bytes); + } + else + { + state.add_global_memory_reads(bytes); + state.add_global_memory_writes(bytes); + } + + cvcuda::AdjustSaturation op; + + // clang-format off + if (isFakePlanar) // tensor-only: planar->interleaved->adjust->interleaved->planar + { + nvcv::Tensor src ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + nvcv::Tensor interSrc({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor interDst({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + cvcuda::Reformat reformatOp; + + benchutils::warmup_and_exec(state, BENCH_ADJUSTSATURATION_WARMUP_ITERATIONS, + [&op, &reformatOp, &src, &interSrc, &interDst, &dst](cudaStream_t s) { + reformatOp(s, src, interSrc); // NCHW -> NHWC + op(s, interSrc, interDst, kSaturation); // interleaved adjust + reformatOp(s, interDst, dst); // NHWC -> NCHW + }); + } + else if (inputKind == benchutils::InputKind::Tensor) + { + nvcv::Tensor src = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + benchutils::warmup_and_exec(state, BENCH_ADJUSTSATURATION_WARMUP_ITERATIONS, + [&op, &src, &dst](cudaStream_t s) { op(s, src, dst, kSaturation); }); + } + else // ImageBatchVarShape + { + nvcv::ImageBatchVarShape src(shape.x); + nvcv::ImageBatchVarShape dst(shape.x); + + if (isPlanar) + { + benchutils::FillPlanarImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}); + benchutils::FillPlanarImageBatch(dst, long2{shape.z, shape.y}, long2{0, 0}); + } + else + { + benchutils::FillImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}, + benchutils::CheckerboardValues(), benchutils::GetRGBFormat()); + benchutils::FillImageBatchLike(dst, src, [](const long4_16a &) { return T{0}; }); + } + + benchutils::warmup_and_exec(state, BENCH_ADJUSTSATURATION_WARMUP_ITERATIONS, + [&op, &src, &dst](cudaStream_t s) { op(s, src, dst, kSaturation); }); + } +} +CVCUDA_BENCH_SKIP_ERRORS(state) + +// clang-format on + +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(adjustsaturation, NVBENCH_TYPE_AXES(BENCH_ADJUSTSATURATION_TYPES)) +BENCH_ADJUSTSATURATION_AXES; diff --git a/bench/cpp/ops/BenchAdjustSharpness.cpp b/bench/cpp/ops/BenchAdjustSharpness.cpp new file mode 100644 index 000000000..556c44a21 --- /dev/null +++ b/bench/cpp/ops/BenchAdjustSharpness.cpp @@ -0,0 +1,135 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchAdjustSharpnessConfig.hpp" + +#include +#include + +#include + +// Sharpness factor is irrelevant to throughput; use a fixed value that exercises the sharpen path. +static constexpr float kSharpnessFactor = 2.0f; + +template +inline void adjustsharpness(nvbench::state &state, nvbench::type_list) +try +{ + int3 shape = benchutils::GetShape<3, int3>(state.get_string("shape")); + auto layout = benchutils::GetStringParam(state, "layout", "NHWC"); + const benchutils::InputKind inputKind = benchutils::GetInputKind(state.get_string("inputKind")); + + const bool isPlanar = layout == "NCHW"; + const bool isFakePlanar = layout == "NCHW_FAKE"; + if (layout != "NHWC" && layout != "NCHW" && layout != "NCHW_FAKE") + { + state.skip("AdjustSharpness benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts"); + return; + } + // NCHW_FAKE ("fake planar") is a tensor-only comparison path: planar data is reformatted to + // interleaved, sharpened with the interleaved kernel, and reformatted back — all timed together + // — so the native planar path (NCHW) can be shown to beat the naive convert->op->convert pipeline. + if (isFakePlanar && inputKind == benchutils::InputKind::VarShape) + { + state.skip("Fake-planar (NCHW_FAKE) adjustsharpness benchmark is tensor-only"); + return; + } + + using BT = typename nvcv::cuda::BaseType; + + int ch = nvcv::cuda::NumElements; + + // AdjustSharpness preserves size, so src and dst hold the same number of bytes. + const long bytes = static_cast(shape.x) * shape.y * shape.z * sizeof(T); + if (isFakePlanar) + { + // reformat(NCHW->NHWC) + adjustsharpness + reformat(NHWC->NCHW): reads src + interleaved src + // + dst, writes interleaved src + interleaved dst + dst. + state.add_global_memory_reads(3 * bytes); + state.add_global_memory_writes(3 * bytes); + } + else + { + state.add_global_memory_reads(bytes); + state.add_global_memory_writes(bytes); + } + + cvcuda::AdjustSharpness op; + + // clang-format off + if (isFakePlanar) // tensor-only: planar->interleaved->adjustsharpness->interleaved->planar + { + nvcv::Tensor src ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + nvcv::Tensor interSrc({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor interDst({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + cvcuda::Reformat reformatOp; + + benchutils::warmup_and_exec(state, BENCH_ADJUSTSHARPNESS_WARMUP_ITERATIONS, + [&op, &reformatOp, &src, &interSrc, &interDst, &dst](cudaStream_t s) { + reformatOp(s, src, interSrc); // NCHW -> NHWC + op(s, interSrc, interDst, kSharpnessFactor); // interleaved adjustsharpness + reformatOp(s, interDst, dst); // NHWC -> NCHW + }); + } + else if (inputKind == benchutils::InputKind::Tensor) + { + nvcv::Tensor src = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + benchutils::warmup_and_exec(state, BENCH_ADJUSTSHARPNESS_WARMUP_ITERATIONS, + [&op, &src, &dst](cudaStream_t s) { op(s, src, dst, kSharpnessFactor); }); + } + else // ImageBatchVarShape + { + nvcv::ImageBatchVarShape src(shape.x); + nvcv::ImageBatchVarShape dst(shape.x); + + if (isPlanar) + { + benchutils::FillPlanarImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}); + benchutils::FillPlanarImageBatch(dst, long2{shape.z, shape.y}, long2{0, 0}); + } + else + { + benchutils::FillImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}, + benchutils::CheckerboardValues()); + benchutils::FillImageBatchLike(dst, src, [](const long4_16a &) { return T{0}; }); + } + + benchutils::warmup_and_exec(state, BENCH_ADJUSTSHARPNESS_WARMUP_ITERATIONS, + [&op, &src, &dst](cudaStream_t s) { op(s, src, dst, kSharpnessFactor); }); + } +} +CVCUDA_BENCH_SKIP_ERRORS(state) + +// clang-format on + +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(adjustsharpness, NVBENCH_TYPE_AXES(BENCH_ADJUSTSHARPNESS_TYPES)) +BENCH_ADJUSTSHARPNESS_AXES; diff --git a/bench/cpp/ops/BenchAdvCvtColor.cpp b/bench/cpp/ops/BenchAdvCvtColor.cpp new file mode 100644 index 000000000..40eb61b89 --- /dev/null +++ b/bench/cpp/ops/BenchAdvCvtColor.cpp @@ -0,0 +1,250 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchAdvCvtColorConfig.hpp" + +#include +#include + +#include + +#include +#include +#include +#include + +enum class ConversionShape +{ + kInterleaved444, + kRgbToNv, + kNvToRgb +}; + +struct ConversionInfo +{ + NVCVColorConversionCode code; + ConversionShape shape; +}; + +struct ConversionTensorShape +{ + int srcH; + int dstH; + int srcC; + int dstC; +}; + +inline ConversionInfo GetConversionInfo(const std::string &code) // NOSONAR: S3776 is misattributed to this lookup. +{ + // clang-format off + static const std::map> codeMap { + { "BGR2YUV", {NVCV_COLOR_BGR2YUV, ConversionShape::kInterleaved444}}, + { "RGB2YUV", {NVCV_COLOR_RGB2YUV, ConversionShape::kInterleaved444}}, + { "YUV2BGR", {NVCV_COLOR_YUV2BGR, ConversionShape::kInterleaved444}}, + { "YUV2RGB", {NVCV_COLOR_YUV2RGB, ConversionShape::kInterleaved444}}, + {"RGB2YUV_NV12", {NVCV_COLOR_RGB2YUV_NV12, ConversionShape::kRgbToNv}}, + {"BGR2YUV_NV21", {NVCV_COLOR_BGR2YUV_NV21, ConversionShape::kRgbToNv}}, + {"YUV2RGB_NV12", {NVCV_COLOR_YUV2RGB_NV12, ConversionShape::kNvToRgb}}, + {"YUV2BGR_NV21", {NVCV_COLOR_YUV2BGR_NV21, ConversionShape::kNvToRgb}}, + }; + // clang-format on + + if (auto it = codeMap.find(code); it != codeMap.end()) + { + return it->second; + } + + throw std::invalid_argument("Unrecognized AdvCvtColor conversion code"); +} + +inline bool IsPlanarLayout(std::string_view layout) +{ + return layout == "NCHW"; +} + +inline bool IsFakePlanarLayout(std::string_view layout) +{ + return layout == "NCHW_FAKE"; +} + +inline bool IsSupportedLayout(std::string_view layout) +{ + return layout == "NHWC" || IsPlanarLayout(layout) || IsFakePlanarLayout(layout); +} + +inline bool ValidateLayout(nvbench::state &state, std::string_view layout, benchutils::InputKind inputKind) +{ + if (!IsSupportedLayout(layout)) + { + state.skip("AdvCvtColor benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts"); + return false; + } + if ((IsPlanarLayout(layout) || IsFakePlanarLayout(layout)) && inputKind != benchutils::InputKind::Tensor) + { + state.skip("Planar AdvCvtColor benchmark is tensor-only"); + return false; + } + return true; +} + +inline ConversionTensorShape GetConversionTensorShape(ConversionShape conversionShape, int3 shape, int rgbChannels) +{ + ConversionTensorShape tensorShape{shape.y, shape.y, rgbChannels, rgbChannels}; + + switch (conversionShape) + { + case ConversionShape::kInterleaved444: + if (rgbChannels != 3) + { + throw std::invalid_argument("Interleaved 444 conversion requires uchar3"); + } + tensorShape.srcC = 3; + tensorShape.dstC = 3; + break; + case ConversionShape::kRgbToNv: + if (rgbChannels != 3 && rgbChannels != 4) + { + throw std::invalid_argument("RGB/BGR to NV conversion requires uchar3 or uchar4"); + } + if (shape.y % 2 != 0 || shape.z % 2 != 0) + { + throw std::invalid_argument("NV conversion requires even height and width"); + } + tensorShape.dstH = shape.y * 3 / 2; + tensorShape.dstC = 1; + break; + case ConversionShape::kNvToRgb: + if (rgbChannels != 3 && rgbChannels != 4) + { + throw std::invalid_argument("NV to RGB/BGR conversion requires uchar3 or uchar4"); + } + if (shape.y % 2 != 0 || shape.z % 2 != 0) + { + throw std::invalid_argument("NV conversion requires even height and width"); + } + tensorShape.srcH = shape.y * 3 / 2; + tensorShape.srcC = 1; + break; + } + + return tensorShape; +} + +template +inline void advcvtcolor(nvbench::state &state, nvbench::type_list) +try +{ + int3 shape = benchutils::GetShape<3, int3>(state.get_string("shape")); + auto layout = benchutils::GetStringParam(state, "layout", "NHWC"); + const benchutils::InputKind inputKind = benchutils::GetInputKind(state.get_string("inputKind")); + + using BT = typename nvcv::cuda::BaseType; + + int rgbChannels = nvcv::cuda::NumElements; + + if (!ValidateLayout(state, layout, inputKind)) + { + return; + } + const bool isPlanar = IsPlanarLayout(layout); + const bool isFakePlanar = IsFakePlanarLayout(layout); + + ConversionInfo conversion = GetConversionInfo(state.get_string("code")); + NVCVColorConversionCode code = conversion.code; + nvcv::ColorSpec colorSpec{NVCV_COLOR_SPEC_BT2020}; + + ConversionTensorShape tensorShape = GetConversionTensorShape(conversion.shape, shape, rgbChannels); + + int64_t srcBytes = shape.x * tensorShape.srcH * shape.z * tensorShape.srcC * sizeof(BT); + int64_t dstBytes = shape.x * tensorShape.dstH * shape.z * tensorShape.dstC * sizeof(BT); + if (isFakePlanar) + { + state.add_global_memory_reads(2 * srcBytes + dstBytes); + state.add_global_memory_writes(srcBytes + 2 * dstBytes); + } + else + { + state.add_global_memory_reads(srcBytes); + state.add_global_memory_writes(dstBytes); + } + + cvcuda::AdvCvtColor op; + auto dtype = benchutils::GetDataType(); + + auto makeTensor = [dtype, &shape](int height, int channels, std::string_view tensorLayout) + { + if (tensorLayout == "NCHW") + { + return nvcv::Tensor( + { + {shape.x, channels, height, shape.z}, + "NCHW" + }, + dtype); + } + return nvcv::Tensor( + { + {shape.x, height, shape.z, channels}, + "NHWC" + }, + dtype); + }; + + // clang-format off + + if (isFakePlanar) // tensor-only: planar→interleaved→advcvtcolor→interleaved→planar + { + nvcv::Tensor src = makeTensor(tensorShape.srcH, tensorShape.srcC, "NCHW"); + nvcv::Tensor interSrc = makeTensor(tensorShape.srcH, tensorShape.srcC, "NHWC"); + nvcv::Tensor interDst = makeTensor(tensorShape.dstH, tensorShape.dstC, "NHWC"); + nvcv::Tensor dst = makeTensor(tensorShape.dstH, tensorShape.dstC, "NCHW"); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + cvcuda::Reformat reformatOp; + + benchutils::warmup_and_exec(state, BENCH_ADVCVTCOLOR_WARMUP_ITERATIONS, + [&op, &reformatOp, &src, &interSrc, &interDst, &dst, &code, &colorSpec](cudaStream_t s) { + reformatOp(s, src, interSrc); + op(s, interSrc, interDst, code, colorSpec); + reformatOp(s, interDst, dst); + }); + } + else if (inputKind == benchutils::InputKind::Tensor) + { + nvcv::Tensor src = makeTensor(tensorShape.srcH, tensorShape.srcC, isPlanar ? "NCHW" : "NHWC"); + nvcv::Tensor dst = makeTensor(tensorShape.dstH, tensorShape.dstC, isPlanar ? "NCHW" : "NHWC"); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + benchutils::warmup_and_exec(state, BENCH_ADVCVTCOLOR_WARMUP_ITERATIONS, + [&op, &src, &dst, &code, &colorSpec](cudaStream_t s) { op(s, src, dst, code, colorSpec); }); + } + else // zero and positive var shape means use ImageBatchVarShape + { + throw std::invalid_argument("ImageBatchVarShape not implemented for this operator"); + } +} +CVCUDA_BENCH_SKIP_ERRORS(state) + +// clang-format on + +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(advcvtcolor, NVBENCH_TYPE_AXES(BENCH_ADVCVTCOLOR_TYPES)) +BENCH_ADVCVTCOLOR_AXES; diff --git a/bench/cpp/ops/BenchAutoContrast.cpp b/bench/cpp/ops/BenchAutoContrast.cpp new file mode 100644 index 000000000..ea5b24bfd --- /dev/null +++ b/bench/cpp/ops/BenchAutoContrast.cpp @@ -0,0 +1,136 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchAutoContrastConfig.hpp" + +#include +#include + +#include + +#include + +// Benchmark for the Auto Contrast operator. Exercises the interleaved (NHWC), native planar (NCHW), +// and fake-planar (NCHW_FAKE) layouts for both Tensor and ImageBatchVarShape inputs, matching the +// operator's declared support matrix. Modeled on BenchBrightnessContrast.cpp. +template +inline void autocontrast(nvbench::state &state, nvbench::type_list) +try +{ + int3 shape = benchutils::GetShape<3, int3>(state.get_string("shape")); + auto layout = benchutils::GetStringParam(state, "layout", "NHWC"); + const benchutils::InputKind inputKind = benchutils::GetInputKind(state.get_string("inputKind")); + + using BT = typename nvcv::cuda::BaseType; + + int ch = nvcv::cuda::NumElements; + + const bool isPlanar = layout == "NCHW"; + const bool isFakePlanar = layout == "NCHW_FAKE"; + if (layout != "NHWC" && layout != "NCHW" && layout != "NCHW_FAKE") + { + state.skip("AutoContrast benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts"); + return; + } + if (isFakePlanar && inputKind == benchutils::InputKind::VarShape) + { + state.skip("Fake-planar (NCHW_FAKE) AutoContrast benchmark is tensor-only"); + return; + } + if (isPlanar && inputKind == benchutils::InputKind::VarShape && ch == 4 && std::is_same_v) + { + state.skip("uchar4 planar ImageBatchVarShape is not supported by the Python image API"); + return; + } + + const long bytes = static_cast(shape.x) * shape.y * shape.z * sizeof(T); + if (isFakePlanar) + { + state.add_global_memory_reads(3 * bytes); + state.add_global_memory_writes(3 * bytes); + } + else + { + state.add_global_memory_reads(bytes); + state.add_global_memory_writes(bytes); + } + + cvcuda::AutoContrast op; + + // clang-format off + if (isFakePlanar) // tensor-only: planar->interleaved->autocontrast->interleaved->planar + { + nvcv::Tensor src ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + nvcv::Tensor interSrc({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor interDst({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + cvcuda::Reformat reformatOp; + + benchutils::warmup_and_exec(state, BENCH_AUTOCONTRAST_WARMUP_ITERATIONS, + [&op, &reformatOp, &src, &interSrc, &interDst, &dst](cudaStream_t s) { + reformatOp(s, src, interSrc); // NCHW -> NHWC + op(s, interSrc, interDst); + reformatOp(s, interDst, dst); // NHWC -> NCHW + }); + } + else if (inputKind == benchutils::InputKind::Tensor) + { + nvcv::Tensor src = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + benchutils::warmup_and_exec(state, BENCH_AUTOCONTRAST_WARMUP_ITERATIONS, + [&op, &src, &dst](cudaStream_t s) { op(s, src, dst); }); + } + else // ImageBatchVarShape + { + nvcv::ImageBatchVarShape src(shape.x); + nvcv::ImageBatchVarShape dst(shape.x); + + if (isPlanar) + { + benchutils::FillPlanarImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}); + benchutils::FillPlanarImageBatch(dst, long2{shape.z, shape.y}, long2{0, 0}); + } + else + { + benchutils::FillImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}, + benchutils::CheckerboardValues()); + benchutils::FillImageBatchLike(dst, src, [](const long4_16a &) { return T{0}; }); + } + + benchutils::warmup_and_exec(state, BENCH_AUTOCONTRAST_WARMUP_ITERATIONS, + [&op, &src, &dst](cudaStream_t s) { op(s, src, dst); }); + } +} +CVCUDA_BENCH_SKIP_ERRORS(state) + +// clang-format on + +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(autocontrast, NVBENCH_TYPE_AXES(BENCH_AUTOCONTRAST_TYPES)) +BENCH_AUTOCONTRAST_AXES; diff --git a/bench/cpp/ops/BenchAverageBlur.cpp b/bench/cpp/ops/BenchAverageBlur.cpp new file mode 100644 index 000000000..c6739d303 --- /dev/null +++ b/bench/cpp/ops/BenchAverageBlur.cpp @@ -0,0 +1,135 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchAverageBlurConfig.hpp" + +#include +#include + +#include + +template +inline void averageblur(nvbench::state &state, nvbench::type_list) +try +{ + int3 shape = benchutils::GetShape<3, int3>(state.get_string("shape")); + const benchutils::InputKind inputKind = benchutils::GetInputKind(state.get_string("inputKind")); + auto layout = benchutils::GetStringParam(state, "layout", "NHWC"); + int2 kernelSize = nvcv::cuda::StaticCast(benchutils::GetShape<2>(state.get_string("kernelSize"))); + + using BT = typename nvcv::cuda::BaseType; + + int ch = nvcv::cuda::NumElements; + + const bool isPlanar = layout == "NCHW"; + const bool isFakePlanar = layout == "NCHW_FAKE"; + if (layout != "NHWC" && layout != "NCHW" && layout != "NCHW_FAKE") + { + state.skip("AverageBlur benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts"); + return; + } + if (isFakePlanar && inputKind == benchutils::InputKind::VarShape) + { + state.skip("Fake-planar (NCHW_FAKE) AverageBlur benchmark is tensor-only"); + return; + } + + NVCVBorderType borderType = benchutils::GetBorderType(state.get_string("border")); + + nvcv::Size2D kernelSize2d{kernelSize.x, kernelSize.y}; + int2 kernelAnchor{-1, -1}; + + state.add_global_memory_reads(shape.x * shape.y * shape.z * sizeof(T)); + state.add_global_memory_writes(shape.x * shape.y * shape.z * sizeof(T)); + + cvcuda::AverageBlur op(kernelSize2d, shape.x); + + // clang-format off + + if (isFakePlanar) // tensor-only: planar->interleaved->averageblur->interleaved->planar + { + nvcv::Tensor src ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + nvcv::Tensor interSrc({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor interDst({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + cvcuda::Reformat reformatOp; + + benchutils::warmup_and_exec(state, BENCH_AVERAGEBLUR_WARMUP_ITERATIONS, + [&op, &reformatOp, &src, &interSrc, &interDst, &dst, &kernelSize2d, &kernelAnchor, &borderType](cudaStream_t s) { + reformatOp(s, src, interSrc); + op(s, interSrc, interDst, kernelSize2d, kernelAnchor, borderType); + reformatOp(s, interDst, dst); + }); + } + else if (inputKind == benchutils::InputKind::Tensor) + { + nvcv::Tensor src = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + benchutils::warmup_and_exec(state, BENCH_AVERAGEBLUR_WARMUP_ITERATIONS, + [&op, &src, &dst, &kernelSize2d, &kernelAnchor, &borderType](cudaStream_t s) { + op(s, src, dst, kernelSize2d, kernelAnchor, borderType); + }); + } + else // zero and positive var shape means use ImageBatchVarShape + { + nvcv::ImageBatchVarShape src(shape.x); + nvcv::ImageBatchVarShape dst(shape.x); + + if (isPlanar) + { + benchutils::FillPlanarImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}); + benchutils::FillPlanarImageBatchLike(dst, src); + } + else + { + benchutils::FillImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}, + benchutils::CheckerboardValues()); + // Use FillImageBatchLike to ensure dst has same per-sample shapes as src + benchutils::FillImageBatchLike(dst, src, [](const long4_16a &) { return T{0}; }); + } + + nvcv::Tensor kernelSizeTensor({{shape.x}, "N"}, nvcv::TYPE_2S32); + nvcv::Tensor kernelAnchorTensor({{shape.x}, "N"}, nvcv::TYPE_2S32); + + benchutils::FillTensor(kernelSizeTensor, [&kernelSize](const long4_16a &){ return kernelSize; }); + benchutils::FillTensor(kernelAnchorTensor, [&kernelAnchor](const long4_16a &){ return kernelAnchor; }); + + benchutils::warmup_and_exec(state, BENCH_AVERAGEBLUR_WARMUP_ITERATIONS, + [&op, &src, &dst, &kernelSizeTensor, &kernelAnchorTensor, &borderType](cudaStream_t s) { + op(s, src, dst, kernelSizeTensor, kernelAnchorTensor, borderType); + }); + } +} +CVCUDA_BENCH_SKIP_ERRORS(state) + +// clang-format on + +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(averageblur, NVBENCH_TYPE_AXES(BENCH_AVERAGEBLUR_TYPES)) +BENCH_AVERAGEBLUR_AXES; diff --git a/bench/cpp/ops/BenchBilateralFilter.cpp b/bench/cpp/ops/BenchBilateralFilter.cpp new file mode 100644 index 000000000..66f63feee --- /dev/null +++ b/bench/cpp/ops/BenchBilateralFilter.cpp @@ -0,0 +1,136 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchBilateralFilterConfig.hpp" + +#include +#include + +#include + +template +inline void bilateralfilter(nvbench::state &state, nvbench::type_list) +try +{ + int3 shape = benchutils::GetShape<3, int3>(state.get_string("shape")); + const benchutils::InputKind inputKind = benchutils::GetInputKind(state.get_string("inputKind")); + auto layout = benchutils::GetStringParam(state, "layout", "NHWC"); + int diameter = benchutils::GetIntParam(state, "diameter"); + auto sigmaSpace = static_cast(state.get_float64("sigmaSpace")); + float sigmaColor = -1.f; + + using BT = typename nvcv::cuda::BaseType; + + int ch = nvcv::cuda::NumElements; + + const bool isPlanar = layout == "NCHW"; + const bool isFakePlanar = layout == "NCHW_FAKE"; + if (layout != "NHWC" && layout != "NCHW" && layout != "NCHW_FAKE") + { + state.skip("BilateralFilter benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts"); + return; + } + if (isFakePlanar && inputKind == benchutils::InputKind::VarShape) + { + state.skip("Fake-planar (NCHW_FAKE) BilateralFilter benchmark is tensor-only"); + return; + } + + NVCVBorderType borderType = benchutils::GetBorderType(state.get_string("border")); + + state.add_global_memory_reads(shape.x * shape.y * shape.z * sizeof(T)); + state.add_global_memory_writes(shape.x * shape.y * shape.z * sizeof(T)); + + cvcuda::BilateralFilter op; + + // clang-format off + + if (isFakePlanar) // tensor-only: planar->interleaved->bilateralfilter->interleaved->planar + { + nvcv::Tensor src ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + nvcv::Tensor interSrc({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor interDst({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::LcgValues()); + + cvcuda::Reformat reformatOp; + + benchutils::warmup_and_exec(state, BENCH_BILATERALFILTER_WARMUP_ITERATIONS, + [&op, &reformatOp, &src, &interSrc, &interDst, &dst, &diameter, &sigmaColor, &sigmaSpace, &borderType](cudaStream_t s) { + reformatOp(s, src, interSrc); + op(s, interSrc, interDst, diameter, sigmaColor, sigmaSpace, borderType); + reformatOp(s, interDst, dst); + }); + } + else if (inputKind == benchutils::InputKind::Tensor) + { + nvcv::Tensor src = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::LcgValues()); + + benchutils::warmup_and_exec(state, BENCH_BILATERALFILTER_WARMUP_ITERATIONS, + [&op, &src, &dst, &diameter, &sigmaColor, &sigmaSpace, &borderType](cudaStream_t s) { + op(s, src, dst, diameter, sigmaColor, sigmaSpace, borderType); + }); + } + else // zero and positive var shape means use ImageBatchVarShape + { + nvcv::ImageBatchVarShape src(shape.x); + nvcv::ImageBatchVarShape dst(shape.x); + + if (isPlanar) + { + benchutils::FillPlanarImageBatchLcg(src, long2{shape.z, shape.y}, long2{0, 0}); + benchutils::FillPlanarImageBatchLike(dst, src); + } + else + { + benchutils::FillImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}, + benchutils::LcgValues()); + // Use FillImageBatchLike to ensure dst has same per-sample shapes as src + benchutils::FillImageBatchLike(dst, src, [](const long4_16a &) { return T{0}; }); + } + + nvcv::Tensor diameterTensor({{shape.x}, "N"}, nvcv::TYPE_S32); + nvcv::Tensor sigmaSpaceTensor({{shape.x}, "N"}, nvcv::TYPE_F32); + nvcv::Tensor sigmaColorTensor({{shape.x}, "N"}, nvcv::TYPE_F32); + + benchutils::FillTensor(diameterTensor, [&diameter](auto &){ return diameter; }); + benchutils::FillTensor(sigmaSpaceTensor, [&sigmaSpace](auto &){ return sigmaSpace; }); + benchutils::FillTensor(sigmaColorTensor, [&sigmaColor](auto &){ return sigmaColor; }); + + benchutils::warmup_and_exec(state, BENCH_BILATERALFILTER_WARMUP_ITERATIONS, + [&op, &src, &dst, &diameterTensor, &sigmaColorTensor, &sigmaSpaceTensor, &borderType](cudaStream_t s) { + op(s, src, dst, diameterTensor, sigmaColorTensor, sigmaSpaceTensor, borderType); + }); + } +} +CVCUDA_BENCH_SKIP_ERRORS(state) + +// clang-format on + +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(bilateralfilter, NVBENCH_TYPE_AXES(BENCH_BILATERALFILTER_TYPES)) +BENCH_BILATERALFILTER_AXES; diff --git a/bench/cpp/ops/BenchBndBox.cpp b/bench/cpp/ops/BenchBndBox.cpp new file mode 100644 index 000000000..bb5e78af7 --- /dev/null +++ b/bench/cpp/ops/BenchBndBox.cpp @@ -0,0 +1,144 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchBndBoxConfig.hpp" + +#include +#include +#include + +#include + +#include +#include +#include +#include + +class BenchBndBoxError : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + +template +inline void bndbox(nvbench::state &state, nvbench::type_list) +try +{ + int3 shape = benchutils::GetShape<3, int3>(state.get_string("shape")); + const benchutils::InputKind inputKind = benchutils::GetInputKind(state.get_string("inputKind")); + int numBoxes = benchutils::GetIntParam(state, "numBoxes"); + auto layout = benchutils::GetStringParam(state, "layout", "NHWC"); + + using BT = typename nvcv::cuda::BaseType; + + int ch = nvcv::cuda::NumElements; + + const bool isPlanar = layout == "NCHW"; + const bool isFakePlanar = layout == "NCHW_FAKE"; + if (layout != "NHWC" && layout != "NCHW" && layout != "NCHW_FAKE") + { + state.skip("BndBox benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts"); + return; + } + if ((isPlanar || isFakePlanar) && inputKind != benchutils::InputKind::Tensor) + { + state.skip("Planar BndBox benchmark is tensor-only"); + return; + } + + NVCVBndBoxI bndBox{ + {43, 21, 12, 34}, // box x, y position w, h size + 2, // box thickness + { 0, 0, 0, 255}, // box border color + { 0, 0, 0, 0} // box fill color + }; + + std::vector flatBoxes(static_cast(shape.x) * numBoxes, bndBox); + std::vector numBoxesPerBatch(shape.x, numBoxes); + + NVCVBndBoxesI bndBoxes = nullptr; + if (nvcvBndBoxesIConstruct(&bndBoxes, flatBoxes.data(), numBoxesPerBatch.data(), shape.x) != NVCV_SUCCESS) + { + throw BenchBndBoxError("nvcvBndBoxesIConstruct failed"); + } + auto bndBoxesGuard = std::unique_ptr( + bndBoxes, [](NVCVBndBoxesI h) { nvcvBndBoxesIDestroy(h); }); + + const long imageBytes = static_cast(shape.x) * shape.y * shape.z * sizeof(T); + const long boxBytes = static_cast(shape.x) * numBoxes * sizeof(NVCVBndBoxI); + if (isPlanar || isFakePlanar) + { + // Account for source, interleaved temporary, and destination image traffic. + state.add_global_memory_reads(3 * imageBytes + boxBytes); + state.add_global_memory_writes(3 * imageBytes); + } + else + { + state.add_global_memory_reads(imageBytes + boxBytes); + state.add_global_memory_writes(imageBytes); + } + + cvcuda::BndBox op; + + // clang-format off + + if (isFakePlanar) + { + nvcv::Tensor src ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + nvcv::Tensor interSrc({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor interDst({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + cvcuda::Reformat reformatOp; + + benchutils::warmup_and_exec(state, BENCH_BNDBOX_WARMUP_ITERATIONS, + [&op, &reformatOp, &src, &interSrc, &interDst, &dst, &bndBoxes](cudaStream_t s) { + reformatOp(s, src, interSrc); + op(s, interSrc, interDst, bndBoxes); + reformatOp(s, interDst, dst); + }); + } + else if (inputKind == benchutils::InputKind::Tensor) + { + nvcv::Tensor src = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + benchutils::warmup_and_exec(state, BENCH_BNDBOX_WARMUP_ITERATIONS, + [&op, &src, &dst, &bndBoxes](cudaStream_t s) { op(s, src, dst, bndBoxes); }); + } + else // zero and positive var shape means use ImageBatchVarShape + { + throw std::invalid_argument("ImageBatchVarShape not implemented for this operator"); + } +} +CVCUDA_BENCH_SKIP_ERRORS(state) + +// clang-format on + +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(bndbox, NVBENCH_TYPE_AXES(BENCH_BNDBOX_TYPES)) +BENCH_BNDBOX_AXES; diff --git a/bench/cpp/ops/BenchBoxBlur.cpp b/bench/cpp/ops/BenchBoxBlur.cpp new file mode 100644 index 000000000..11a79df09 --- /dev/null +++ b/bench/cpp/ops/BenchBoxBlur.cpp @@ -0,0 +1,172 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchBoxBlurConfig.hpp" + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +class BenchBoxBlurError : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + +inline NVCVBlurBoxI MakeBoxBlurBox(const std::string &boxPattern, int2 boxSize, int3 shape, int kernelSize, + int numBoxes, int boxIdx) +{ + int boxWidth = std::max(3, std::min(boxSize.x, shape.z)); + int boxHeight = std::max(3, std::min(boxSize.y, shape.y)); + int x = 43; + int y = 21; + + if (boxPattern == "grid") + { + int gridCols = 1; + while (gridCols * gridCols < numBoxes) + { + ++gridCols; + } + int gridRows = (numBoxes + gridCols - 1) / gridCols; + int col = boxIdx % gridCols; + int row = boxIdx / gridCols; + int maxX = std::max(0, shape.z - boxWidth); + int maxY = std::max(0, shape.y - boxHeight); + x = gridCols <= 1 ? maxX / 2 : col * maxX / (gridCols - 1); + y = gridRows <= 1 ? maxY / 2 : row * maxY / (gridRows - 1); + } + else if (boxPattern != "fixed") + { + throw std::invalid_argument("Unexpected boxPattern = " + boxPattern); + } + + return NVCVBlurBoxI{ + {x, y, boxWidth, boxHeight}, + kernelSize + }; +} + +template +inline void boxblur(nvbench::state &state, nvbench::type_list) +try +{ + int3 shape = benchutils::GetShape<3, int3>(state.get_string("shape")); + const benchutils::InputKind inputKind = benchutils::GetInputKind(state.get_string("inputKind")); + int numBoxes = benchutils::GetIntParam(state, "numBoxes"); + int kernelSize = benchutils::GetIntParam(state, "kernelSize"); + int2 boxSize = benchutils::GetShape<2, int2>(state.get_string("boxSize")); + auto boxPattern = state.get_string("boxPattern"); + auto layout = benchutils::GetStringParam(state, "layout", "NHWC"); + + using BT = typename nvcv::cuda::BaseType; + + int ch = nvcv::cuda::NumElements; + + const bool isPlanar = layout == "NCHW"; + const bool isFakePlanar = layout == "NCHW_FAKE"; + if (layout != "NHWC" && layout != "NCHW" && layout != "NCHW_FAKE") + { + state.skip("BoxBlur benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts"); + return; + } + if (isFakePlanar && inputKind != benchutils::InputKind::Tensor) + { + state.skip("Fake-planar (NCHW_FAKE) BoxBlur benchmark is tensor-only"); + return; + } + + std::vector flatBoxes; + flatBoxes.reserve(static_cast(shape.x) * numBoxes); + for (long n = 0; n < shape.x; ++n) + { + for (int i = 0; i < numBoxes; ++i) + { + flatBoxes.push_back(MakeBoxBlurBox(boxPattern, boxSize, shape, kernelSize, numBoxes, i)); + } + } + std::vector numBoxesPerBatch(shape.x, numBoxes); + + NVCVBlurBoxesI blurBoxes = nullptr; + if (nvcvBlurBoxesIConstruct(&blurBoxes, flatBoxes.data(), numBoxesPerBatch.data(), shape.x) != NVCV_SUCCESS) + { + throw BenchBoxBlurError("nvcvBlurBoxesIConstruct failed"); + } + auto blurBoxesGuard = std::unique_ptr( + blurBoxes, [](NVCVBlurBoxesI h) { nvcvBlurBoxesIDestroy(h); }); + + state.add_global_memory_reads(shape.x * shape.y * shape.z * sizeof(T) + shape.x * numBoxes * sizeof(NVCVBlurBoxI)); + state.add_global_memory_writes(shape.x * shape.y * shape.z * sizeof(T)); + + cvcuda::BoxBlur op; + + // clang-format off + + if (isFakePlanar) + { + nvcv::Tensor src ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + nvcv::Tensor interSrc({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor interDst({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + cvcuda::Reformat reformatOp; + + benchutils::warmup_and_exec(state, BENCH_BOXBLUR_WARMUP_ITERATIONS, + [&op, &reformatOp, &src, &interSrc, &interDst, &dst, &blurBoxes](cudaStream_t s) { + reformatOp(s, src, interSrc); + op(s, interSrc, interDst, blurBoxes); + reformatOp(s, interDst, dst); + }); + } + else if (inputKind == benchutils::InputKind::Tensor) + { + nvcv::Tensor src = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + benchutils::warmup_and_exec(state, BENCH_BOXBLUR_WARMUP_ITERATIONS, + [&op, &src, &dst, &blurBoxes](cudaStream_t s) { op(s, src, dst, blurBoxes); }); + } + else // zero and positive var shape means use ImageBatchVarShape + { + throw std::invalid_argument("ImageBatchVarShape not implemented for this operator"); + } +} +CVCUDA_BENCH_SKIP_ERRORS(state) + +// clang-format on + +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(boxblur, NVBENCH_TYPE_AXES(BENCH_BOXBLUR_TYPES)) +BENCH_BOXBLUR_AXES; diff --git a/bench/cpp/ops/BenchBrightnessContrast.cpp b/bench/cpp/ops/BenchBrightnessContrast.cpp new file mode 100644 index 000000000..780d5996b --- /dev/null +++ b/bench/cpp/ops/BenchBrightnessContrast.cpp @@ -0,0 +1,152 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchBrightnessContrastConfig.hpp" + +#include +#include + +#include + +#include + +template +inline void brightnesscontrast(nvbench::state &state, nvbench::type_list) +try +{ + int3 shape = benchutils::GetShape<3, int3>(state.get_string("shape")); + auto layout = benchutils::GetStringParam(state, "layout", "NHWC"); + const benchutils::InputKind inputKind = benchutils::GetInputKind(state.get_string("inputKind")); + + using BT = typename nvcv::cuda::BaseType; + + int ch = nvcv::cuda::NumElements; + + const bool isPlanar = layout == "NCHW"; + const bool isFakePlanar = layout == "NCHW_FAKE"; + if (layout != "NHWC" && layout != "NCHW" && layout != "NCHW_FAKE") + { + state.skip("BrightnessContrast benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts"); + return; + } + if (isFakePlanar && inputKind == benchutils::InputKind::VarShape) + { + state.skip("Fake-planar (NCHW_FAKE) BrightnessContrast benchmark is tensor-only"); + return; + } + if (isPlanar && inputKind == benchutils::InputKind::VarShape && ch == 4 && std::is_same_v) + { + state.skip("uchar4 planar ImageBatchVarShape is not supported by the Python image API"); + return; + } + + const long bytes = static_cast(shape.x) * shape.y * shape.z * sizeof(T); + const long argBytes = static_cast(shape.x) * sizeof(float) * 4; + if (isFakePlanar) + { + state.add_global_memory_reads(3 * bytes + argBytes); + state.add_global_memory_writes(3 * bytes); + } + else + { + state.add_global_memory_reads(bytes + argBytes); + state.add_global_memory_writes(bytes); + } + + cvcuda::BrightnessContrast op; + + // clang-format off + + nvcv::Tensor brightness({{shape.x}, "N"}, nvcv::TYPE_F32); + nvcv::Tensor contrast({{shape.x}, "N"}, nvcv::TYPE_F32); + nvcv::Tensor brightnessShift({{shape.x}, "N"}, nvcv::TYPE_F32); + nvcv::Tensor contrastCenter({{shape.x}, "N"}, nvcv::TYPE_F32); + + // All 4 param tensors use the deterministic LCG GPU fast path so the + // bytes match the Python bench's fill_mode="lcg" exactly. + benchutils::FillTensor(brightness, benchutils::LcgValues()); + benchutils::FillTensor(contrast, benchutils::LcgValues()); + benchutils::FillTensor(brightnessShift, benchutils::LcgValues()); + benchutils::FillTensor(contrastCenter, benchutils::LcgValues()); + + if (isFakePlanar) // tensor-only: planar->interleaved->brightnesscontrast->interleaved->planar + { + nvcv::Tensor src ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + nvcv::Tensor interSrc({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor interDst({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + cvcuda::Reformat reformatOp; + + benchutils::warmup_and_exec(state, BENCH_BRIGHTNESSCONTRAST_WARMUP_ITERATIONS, + [&op, &reformatOp, &src, &interSrc, &interDst, &dst, &brightness, &contrast, &brightnessShift, &contrastCenter](cudaStream_t s) { + reformatOp(s, src, interSrc); // NCHW -> NHWC + op(s, interSrc, interDst, brightness, contrast, brightnessShift, contrastCenter); + reformatOp(s, interDst, dst); // NHWC -> NCHW + }); + } + else if (inputKind == benchutils::InputKind::Tensor) + { + nvcv::Tensor src = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + benchutils::warmup_and_exec(state, BENCH_BRIGHTNESSCONTRAST_WARMUP_ITERATIONS, + [&op, &src, &dst, &brightness, &contrast, &brightnessShift, &contrastCenter](cudaStream_t s) { + op(s, src, dst, brightness, contrast, brightnessShift, contrastCenter); + }); + } + else // zero and positive var shape means use ImageBatchVarShape + { + nvcv::ImageBatchVarShape src(shape.x); + nvcv::ImageBatchVarShape dst(shape.x); + + if (isPlanar) + { + benchutils::FillPlanarImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}); + benchutils::FillPlanarImageBatch(dst, long2{shape.z, shape.y}, long2{0, 0}); + } + else + { + benchutils::FillImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}, + benchutils::CheckerboardValues()); + // Use FillImageBatchLike to ensure dst has same per-sample shapes as src + benchutils::FillImageBatchLike(dst, src, [](const long4_16a &) { return T{0}; }); + } + + benchutils::warmup_and_exec(state, BENCH_BRIGHTNESSCONTRAST_WARMUP_ITERATIONS, + [&op, &src, &dst, &brightness, &contrast, &brightnessShift, &contrastCenter](cudaStream_t s) { + op(s, src, dst, brightness, contrast, brightnessShift, contrastCenter); + }); + } +} +CVCUDA_BENCH_SKIP_ERRORS(state) + +// clang-format on + +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(brightnesscontrast, NVBENCH_TYPE_AXES(BENCH_BRIGHTNESSCONTRAST_TYPES)) +BENCH_BRIGHTNESSCONTRAST_AXES; diff --git a/bench/cpp/ops/BenchCLAHE.cpp b/bench/cpp/ops/BenchCLAHE.cpp new file mode 100644 index 000000000..80581306b --- /dev/null +++ b/bench/cpp/ops/BenchCLAHE.cpp @@ -0,0 +1,113 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchCLAHEConfig.hpp" + +#include +#include + +#include + +template +inline void clahe(nvbench::state &state, nvbench::type_list) +try +{ + int3 shape = benchutils::GetShape<3, int3>(state.get_string("shape")); + const benchutils::InputKind inputKind = benchutils::GetInputKind(state.get_string("inputKind")); + auto layout = benchutils::GetStringParam(state, "layout", "NHWC"); + int tilesX = benchutils::GetIntParam(state, "tilesX"); + int tilesY = benchutils::GetIntParam(state, "tilesY"); + int clip10 = benchutils::GetIntParam(state, "clip10"); + + const bool isPlanar = layout == "NCHW"; + const bool isFakePlanar = layout == "NCHW_FAKE"; + if (layout != "NHWC" && layout != "NCHW" && layout != "NCHW_FAKE") + { + state.skip("CLAHE benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts"); + return; + } + if (isFakePlanar && inputKind == benchutils::InputKind::VarShape) + { + state.skip("Fake-planar (NCHW_FAKE) CLAHE benchmark is tensor-only"); + return; + } + + const auto clipLimit = static_cast(static_cast(clip10) / 10.0); + + using BT = typename nvcv::cuda::BaseType; + int ch = nvcv::cuda::NumElements; + + const long bytes = static_cast(shape.x) * shape.y * shape.z * sizeof(T); + state.add_global_memory_reads(isFakePlanar ? 3 * bytes : bytes); + state.add_global_memory_writes(isFakePlanar ? 3 * bytes : bytes); + + cvcuda::CLAHE op(shape.x, tilesX, tilesY); + + // clang-format off + + if (isFakePlanar) + { + nvcv::Tensor src ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + nvcv::Tensor interSrc({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor interDst({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::LcgValues()); + + cvcuda::Reformat reformatOp; + benchutils::warmup_and_exec(state, BENCH_CLAHE_WARMUP_ITERATIONS, + [&op, &reformatOp, &src, &interSrc, &interDst, &dst, clipLimit](cudaStream_t s) { + reformatOp(s, src, interSrc); + op(s, interSrc, interDst, clipLimit); + reformatOp(s, interDst, dst); + }); + } + else if (inputKind == benchutils::InputKind::Tensor) + { + nvcv::Tensor src = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + benchutils::FillTensor(src, benchutils::LcgValues()); + + benchutils::warmup_and_exec(state, BENCH_CLAHE_WARMUP_ITERATIONS, + [&op, &src, &dst, clipLimit](cudaStream_t s) { op(s, src, dst, clipLimit); }); + } + else + { + nvcv::ImageBatchVarShape src(shape.x); + nvcv::ImageBatchVarShape dst(shape.x); + benchutils::FillImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}, + benchutils::LcgValues()); + benchutils::FillImageBatchLike(dst, src, [](const long4_16a &) { return T{0}; }); + + benchutils::warmup_and_exec(state, BENCH_CLAHE_WARMUP_ITERATIONS, + [&op, &src, &dst, clipLimit](cudaStream_t s) { op(s, src, dst, clipLimit); }); + } + + // clang-format on +} + +CVCUDA_BENCH_SKIP_ERRORS(state) + +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(clahe, NVBENCH_TYPE_AXES(BENCH_CLAHE_TYPES)) +BENCH_CLAHE_AXES; diff --git a/bench/cpp/ops/BenchCenterCrop.cpp b/bench/cpp/ops/BenchCenterCrop.cpp new file mode 100644 index 000000000..15cfe8f30 --- /dev/null +++ b/bench/cpp/ops/BenchCenterCrop.cpp @@ -0,0 +1,131 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchCenterCropConfig.hpp" + +#include +#include + +#include + +template +inline void centercrop(nvbench::state &state, nvbench::type_list) +try +{ + long3 srcShape = benchutils::GetShape<3>(state.get_string("shape")); + auto layout = benchutils::GetStringParam(state, "layout", "NHWC"); + const benchutils::InputKind inputKind = benchutils::GetInputKind(state.get_string("inputKind")); + + using BT = typename nvcv::cuda::BaseType; + + int ch = nvcv::cuda::NumElements; + + const bool isPlanar = layout == "NCHW"; + const bool isFakePlanar = layout == "NCHW_FAKE"; + if (layout != "NHWC" && layout != "NCHW" && layout != "NCHW_FAKE") + { + state.skip("CenterCrop benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts"); + return; + } + // CenterCrop is tensor-only; the native planar (NCHW) and fake-planar (NCHW_FAKE) paths are too. + if ((isPlanar || isFakePlanar) && inputKind != benchutils::InputKind::Tensor) + { + state.skip("Planar CenterCrop benchmark is tensor-only"); + return; + } + + nvcv::Size2D cropSize; + + if (state.get_string("cropType") == "SAME") + { + cropSize = nvcv::Size2D{(int)srcShape.z, (int)srcShape.y}; + } + else if (state.get_string("cropType") == "QUARTER") + { + cropSize = nvcv::Size2D{(int)srcShape.z / 2, (int)srcShape.y / 2}; + } + else + { + throw std::invalid_argument("Invalid cropType = " + state.get_string("cropType")); + } + + long3 dstShape{srcShape.x, cropSize.h, cropSize.w}; + + const long fullBytes = srcShape.x * srcShape.y * srcShape.z * sizeof(T); + const long cropBytes = dstShape.x * dstShape.y * dstShape.z * sizeof(T); + if (isFakePlanar) + { + // reformat(NCHW→NHWC) + crop + reformat(NHWC→NCHW). + state.add_global_memory_reads(fullBytes + 2 * cropBytes); + state.add_global_memory_writes(fullBytes + 2 * cropBytes); + } + else + { + state.add_global_memory_reads(cropBytes); + state.add_global_memory_writes(cropBytes); + } + + cvcuda::CenterCrop op; + + // clang-format off + + if (isFakePlanar) // tensor-only: planar→interleaved→crop→interleaved→planar + { + nvcv::Tensor src ({{srcShape.x, ch, srcShape.y, srcShape.z}, "NCHW"}, benchutils::GetDataType()); + nvcv::Tensor interSrc({{srcShape.x, srcShape.y, srcShape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor interDst({{dstShape.x, dstShape.y, dstShape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst ({{dstShape.x, ch, dstShape.y, dstShape.z}, "NCHW"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + cvcuda::Reformat reformatOp; + + benchutils::warmup_and_exec(state, BENCH_CENTERCROP_WARMUP_ITERATIONS, + [&op, &reformatOp, &src, &interSrc, &interDst, &dst, &cropSize](cudaStream_t s) { + reformatOp(s, src, interSrc); // NCHW → NHWC + op(s, interSrc, interDst, cropSize); // interleaved crop + reformatOp(s, interDst, dst); // NHWC → NCHW + }); + } + else if (inputKind == benchutils::InputKind::Tensor) + { + nvcv::Tensor src = isPlanar + ? nvcv::Tensor({{srcShape.x, ch, srcShape.y, srcShape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{srcShape.x, srcShape.y, srcShape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst = isPlanar + ? nvcv::Tensor({{dstShape.x, ch, dstShape.y, dstShape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{dstShape.x, dstShape.y, dstShape.z, ch}, "NHWC"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + benchutils::warmup_and_exec(state, BENCH_CENTERCROP_WARMUP_ITERATIONS, + [&op, &src, &dst, &cropSize](cudaStream_t s) { op(s, src, dst, cropSize); }); + } + else // zero and positive var shape means use ImageBatchVarShape + { + throw std::invalid_argument("ImageBatchVarShape not implemented for this operator"); + } +} +CVCUDA_BENCH_SKIP_ERRORS(state) + +// clang-format on + +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(centercrop, NVBENCH_TYPE_AXES(BENCH_CENTERCROP_TYPES)) +BENCH_CENTERCROP_AXES; diff --git a/bench/cpp/ops/BenchChannelReorder.cpp b/bench/cpp/ops/BenchChannelReorder.cpp new file mode 100644 index 000000000..50e299a28 --- /dev/null +++ b/bench/cpp/ops/BenchChannelReorder.cpp @@ -0,0 +1,259 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchChannelReorderConfig.hpp" + +#include +#include + +#include + +#include +#include +#include +#include +#include + +namespace { + +enum class ChannelReorderLayout +{ + NHWC, + NCHW, + NCHWFake, + Invalid +}; + +enum class ChannelOrderPattern +{ + Rotate, + ZeroFill +}; + +ChannelReorderLayout GetChannelReorderLayout(nvbench::state &state, std::string_view layout, + benchutils::InputKind inputKind) +{ + if (layout == "NHWC") + { + return ChannelReorderLayout::NHWC; + } + if (layout == "NCHW") + { + return ChannelReorderLayout::NCHW; + } + if (layout == "NCHW_FAKE") + { + if (inputKind == benchutils::InputKind::VarShape) + { + state.skip("Fake-planar (NCHW_FAKE) ChannelReorder benchmark is tensor-only"); + return ChannelReorderLayout::Invalid; + } + return ChannelReorderLayout::NCHWFake; + } + + state.skip("ChannelReorder benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts"); + return ChannelReorderLayout::Invalid; +} + +bool IsPlanar(ChannelReorderLayout layout) +{ + return layout == ChannelReorderLayout::NCHW; +} + +bool IsFakePlanar(ChannelReorderLayout layout) +{ + return layout == ChannelReorderLayout::NCHWFake; +} + +ChannelOrderPattern GetChannelOrderPattern(std::string_view orderPattern) +{ + if (orderPattern == "rotate") + { + return ChannelOrderPattern::Rotate; + } + if (orderPattern == "zero_fill") + { + return ChannelOrderPattern::ZeroFill; + } + + throw std::invalid_argument("Invalid orderPattern = " + std::string(orderPattern)); +} + +template +std::array MakeChannelOrder(ChannelOrderPattern pattern) +{ + std::array order; + std::iota(order.begin(), order.end(), 0); + if (pattern == ChannelOrderPattern::Rotate) + { + std::rotate(order.begin(), order.begin() + 1, order.end()); + } + else if constexpr (NumChannels > 1) + { + order[1] = -1; + } + return order; +} + +void AddChannelReorderMemoryTraffic(nvbench::state &state, long bytes, ChannelReorderLayout layout) +{ + const long traffic = IsFakePlanar(layout) ? 3 * bytes : bytes; + state.add_global_memory_reads(traffic); + state.add_global_memory_writes(traffic); +} + +template +nvcv::Tensor MakeChannelReorderTensor(const int3 &shape, int channels, bool planar) +{ + if (planar) + { + return nvcv::Tensor( + { + {shape.x, channels, shape.y, shape.z}, + "NCHW" + }, + benchutils::GetDataType()); + } + return nvcv::Tensor( + { + {shape.x, shape.y, shape.z, channels}, + "NHWC" + }, + benchutils::GetDataType()); +} + +template +void RunFakePlanarChannelReorder(nvbench::state &state, cvcuda::ChannelReorder &op, const int3 &shape, + const std::array &order) +{ + nvcv::Tensor src = MakeChannelReorderTensor(shape, NumChannels, true); + nvcv::Tensor interSrc = MakeChannelReorderTensor(shape, NumChannels, false); + nvcv::Tensor interDst = MakeChannelReorderTensor(shape, NumChannels, false); + nvcv::Tensor dst = MakeChannelReorderTensor(shape, NumChannels, true); + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + cvcuda::Reformat reformat; + benchutils::warmup_and_exec(state, BENCH_CHANNELREORDER_WARMUP_ITERATIONS, + [&op, &reformat, &src, &interSrc, &interDst, &dst, &order](cudaStream_t stream) + { + reformat(stream, src, interSrc); + op(stream, interSrc, interDst, order.data(), NumChannels); + reformat(stream, interDst, dst); + }); +} + +template +void RunNativeTensorChannelReorder(nvbench::state &state, cvcuda::ChannelReorder &op, const int3 &shape, bool planar, + const std::array &order) +{ + nvcv::Tensor src = MakeChannelReorderTensor(shape, NumChannels, planar); + nvcv::Tensor dst = MakeChannelReorderTensor(shape, NumChannels, planar); + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + benchutils::warmup_and_exec(state, BENCH_CHANNELREORDER_WARMUP_ITERATIONS, + [&op, &src, &dst, &order](cudaStream_t stream) + { op(stream, src, dst, order.data(), NumChannels); }); +} + +template +void RunTensorChannelReorder(nvbench::state &state, cvcuda::ChannelReorder &op, const int3 &shape, + ChannelReorderLayout layout, ChannelOrderPattern pattern) +{ + using BT = typename nvcv::cuda::BaseType; + constexpr int NumChannels = nvcv::cuda::NumElements; + const auto order = MakeChannelOrder(pattern); + + if (IsFakePlanar(layout)) + { + RunFakePlanarChannelReorder(state, op, shape, order); + return; + } + RunNativeTensorChannelReorder(state, op, shape, IsPlanar(layout), order); +} + +template +void RunVarShapeChannelReorder(nvbench::state &state, cvcuda::ChannelReorder &op, const int3 &shape, bool planar, + ChannelOrderPattern pattern) +{ + constexpr int NumChannels = nvcv::cuda::NumElements; + nvcv::Tensor orders( + { + {shape.x, 4}, + "NC" + }, + nvcv::TYPE_S32); + if (pattern == ChannelOrderPattern::Rotate) + { + benchutils::FillTensor(orders, + [](const long4_16a &coord) { return (int)((coord.x + coord.y) % NumChannels); }); + } + else + { + benchutils::FillTensor( + orders, [](const long4_16a &coord) { return coord.y == 1 ? -1 : (int)(coord.y % NumChannels); }); + } + + nvcv::ImageBatchVarShape src(shape.x); + nvcv::ImageBatchVarShape dst(shape.x); + if (planar) + { + benchutils::FillPlanarImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}); + benchutils::FillPlanarImageBatchLike(dst, src); + } + else + { + benchutils::FillImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}, benchutils::CheckerboardValues()); + benchutils::FillImageBatchLike(dst, src, [](const long4_16a &) { return T{0}; }); + } + + benchutils::warmup_and_exec(state, BENCH_CHANNELREORDER_WARMUP_ITERATIONS, + [&op, &src, &dst, &orders](cudaStream_t stream) { op(stream, src, dst, orders); }); +} + +} // namespace + +template +inline void channelreorder(nvbench::state &state, nvbench::type_list) +try +{ + const int3 shape = benchutils::GetShape<3, int3>(state.get_string("shape")); + const benchutils::InputKind inputKind = benchutils::GetInputKind(state.get_string("inputKind")); + const auto layoutString = benchutils::GetStringParam(state, "layout", "NHWC"); + const auto layout = GetChannelReorderLayout(state, layoutString, inputKind); + if (layout == ChannelReorderLayout::Invalid) + { + return; + } + const auto pattern = GetChannelOrderPattern(state.get_string("orderPattern")); + + const long bytes = static_cast(shape.x) * shape.y * shape.z * sizeof(T); + AddChannelReorderMemoryTraffic(state, bytes, layout); + + cvcuda::ChannelReorder op; + if (inputKind == benchutils::InputKind::Tensor) + { + RunTensorChannelReorder(state, op, shape, layout, pattern); + return; + } + RunVarShapeChannelReorder(state, op, shape, IsPlanar(layout), pattern); +} +CVCUDA_BENCH_SKIP_ERRORS(state) + +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(channelreorder, NVBENCH_TYPE_AXES(BENCH_CHANNELREORDER_TYPES)) +BENCH_CHANNELREORDER_AXES; diff --git a/bench/cpp/ops/BenchColorTwist.cpp b/bench/cpp/ops/BenchColorTwist.cpp new file mode 100644 index 000000000..5cffe0325 --- /dev/null +++ b/bench/cpp/ops/BenchColorTwist.cpp @@ -0,0 +1,150 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchColorTwistConfig.hpp" + +#include +#include + +#include + +template +inline void colortwist(nvbench::state &state, nvbench::type_list) +try +{ + int3 shape = benchutils::GetShape<3, int3>(state.get_string("shape")); + const benchutils::InputKind inputKind = benchutils::GetInputKind(state.get_string("inputKind")); + auto twistMode = state.get_string("twistMode"); + auto layout = benchutils::GetStringParam(state, "layout", "NHWC"); + + using BT = typename nvcv::cuda::BaseType; + + int ch = nvcv::cuda::NumElements; + + const bool isPlanar = layout == "NCHW"; + const bool isFakePlanar = layout == "NCHW_FAKE"; + if (layout != "NHWC" && layout != "NCHW" && layout != "NCHW_FAKE") + { + state.skip("ColorTwist benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts"); + return; + } + if (isFakePlanar && inputKind == benchutils::InputKind::VarShape) + { + state.skip("Fake-planar (NCHW_FAKE) ColorTwist benchmark is tensor-only"); + return; + } + if (isPlanar && inputKind == benchutils::InputKind::VarShape && std::is_same_v && ch == 4) + { + state.skip("RGBA8p varshape is unsupported by the Python image API"); + return; + } + + const long imageBytes = static_cast(shape.x) * shape.y * shape.z * sizeof(T); + if (isFakePlanar) + { + state.add_global_memory_reads(3 * imageBytes); + state.add_global_memory_writes(3 * imageBytes); + } + else + { + state.add_global_memory_reads(imageBytes); + state.add_global_memory_writes(imageBytes); + } + + cvcuda::ColorTwist op; + + // clang-format off + + nvcv::Tensor twist; + + if (twistMode == "per_sample") + { + twist = nvcv::Tensor({{shape.x, 3}, "NH"}, nvcv::TYPE_4F32); + } + else if (twistMode == "global") + { + twist = nvcv::Tensor({{3}, "H"}, nvcv::TYPE_4F32); + } + else + { + throw std::invalid_argument("Invalid twistMode = " + twistMode); + } + benchutils::FillTensor(twist, benchutils::LcgValues()); + + if (isFakePlanar) + { + nvcv::Tensor src ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + nvcv::Tensor interSrc({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor interDst({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + cvcuda::Reformat reformatOp; + + benchutils::warmup_and_exec(state, BENCH_COLORTWIST_WARMUP_ITERATIONS, + [&op, &reformatOp, &src, &interSrc, &interDst, &dst, &twist](cudaStream_t s) { + reformatOp(s, src, interSrc); + op(s, interSrc, interDst, twist); + reformatOp(s, interDst, dst); + }); + } + else if (inputKind == benchutils::InputKind::Tensor) + { + nvcv::Tensor src = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + benchutils::warmup_and_exec(state, BENCH_COLORTWIST_WARMUP_ITERATIONS, + [&op, &src, &dst, &twist](cudaStream_t s) { op(s, src, dst, twist); }); + } + else // zero and positive var shape means use ImageBatchVarShape + { + nvcv::ImageBatchVarShape src(shape.x); + nvcv::ImageBatchVarShape dst(shape.x); + + if (isPlanar) + { + benchutils::FillPlanarImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}); + benchutils::FillPlanarImageBatchLike(dst, src); + } + else + { + benchutils::FillImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}, + benchutils::CheckerboardValues()); + // Use FillImageBatchLike to ensure dst has same per-sample shapes as src + benchutils::FillImageBatchLike(dst, src, [](const long4_16a &) { return T{0}; }); + } + + benchutils::warmup_and_exec(state, BENCH_COLORTWIST_WARMUP_ITERATIONS, + [&op, &src, &dst, &twist](cudaStream_t s) { op(s, src, dst, twist); }); + } +} +CVCUDA_BENCH_SKIP_ERRORS(state) + +// clang-format on + +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(colortwist, NVBENCH_TYPE_AXES(BENCH_COLORTWIST_TYPES)) +BENCH_COLORTWIST_AXES; diff --git a/bench/cpp/ops/BenchComposite.cpp b/bench/cpp/ops/BenchComposite.cpp new file mode 100644 index 000000000..7e96b7692 --- /dev/null +++ b/bench/cpp/ops/BenchComposite.cpp @@ -0,0 +1,326 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchCompositeConfig.hpp" + +#include +#include + +#include + +#include +#include +#include + +enum class CompositeLayout +{ + NHWC, + NCHW, + NCHW_FAKE, + INVALID +}; + +inline bool IsPlanar(CompositeLayout layout) +{ + return layout == CompositeLayout::NCHW; +} + +inline bool IsFakePlanar(CompositeLayout layout) +{ + return layout == CompositeLayout::NCHW_FAKE; +} + +inline void ValidateOutChannels(int outChannels) +{ + if (outChannels == 3 || outChannels == 4) + { + return; + } + + throw std::invalid_argument("Invalid outChannels = " + std::to_string(outChannels)); +} + +inline CompositeLayout ParseCompositeLayout(nvbench::state &state, std::string_view layout) +{ + if (layout == "NHWC") + { + return CompositeLayout::NHWC; + } + if (layout == "NCHW") + { + return CompositeLayout::NCHW; + } + if (layout == "NCHW_FAKE") + { + return CompositeLayout::NCHW_FAKE; + } + + state.skip("Composite benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts"); + return CompositeLayout::INVALID; +} + +inline CompositeLayout ValidateCompositeBenchParams(nvbench::state &state, std::string_view layout, int outChannels) +{ + ValidateOutChannels(outChannels); + return ParseCompositeLayout(state, layout); +} + +inline bool SkipUnsupportedCompositeCase(nvbench::state &state, benchutils::InputKind inputKind, CompositeLayout layout) +{ + if (IsFakePlanar(layout) && inputKind == benchutils::InputKind::VarShape) + { + state.skip("Fake-planar (NCHW_FAKE) Composite benchmark is tensor-only"); + return true; + } + + return false; +} + +template +void RunFakePlanarComposite(nvbench::state &state, cvcuda::Composite &op, int3 shape, int outChannels) +{ + using BT = typename nvcv::cuda::BaseType; + + const int ch = nvcv::cuda::NumElements; + + nvcv::Tensor fg( + { + {shape.x, ch, shape.y, shape.z}, + "NCHW" + }, + benchutils::GetDataType()); + nvcv::Tensor bg( + { + {shape.x, ch, shape.y, shape.z}, + "NCHW" + }, + benchutils::GetDataType()); + nvcv::Tensor mask( + { + {shape.x, 1, shape.y, shape.z}, + "NCHW" + }, + benchutils::GetDataType()); + nvcv::Tensor interFg( + { + {shape.x, shape.y, shape.z, ch}, + "NHWC" + }, + benchutils::GetDataType()); + nvcv::Tensor interBg( + { + {shape.x, shape.y, shape.z, ch}, + "NHWC" + }, + benchutils::GetDataType()); + nvcv::Tensor interMask( + { + {shape.x, shape.y, shape.z, 1}, + "NHWC" + }, + benchutils::GetDataType()); + nvcv::Tensor interDst( + { + {shape.x, shape.y, shape.z, outChannels}, + "NHWC" + }, + benchutils::GetDataType()); + nvcv::Tensor dst( + { + {shape.x, outChannels, shape.y, shape.z}, + "NCHW" + }, + benchutils::GetDataType()); + + benchutils::FillTensor(fg, benchutils::CheckerboardValues()); + benchutils::FillTensor(bg, benchutils::CheckerboardValues()); + benchutils::FillTensor(mask, [](const long4_16a &) { return 1; }); + + cvcuda::Reformat reformatOp; + + benchutils::warmup_and_exec( + state, BENCH_COMPOSITE_WARMUP_ITERATIONS, + [&op, &reformatOp, &fg, &bg, &mask, &interFg, &interBg, &interMask, &interDst, &dst](cudaStream_t s) + { + reformatOp(s, fg, interFg); + reformatOp(s, bg, interBg); + reformatOp(s, mask, interMask); + op(s, interFg, interBg, interMask, interDst); + reformatOp(s, interDst, dst); + }); +} + +template +nvcv::Tensor MakeCompositeTensor(int3 shape, int channels, bool isPlanar) +{ + if (isPlanar) + { + return nvcv::Tensor( + { + {shape.x, channels, shape.y, shape.z}, + "NCHW" + }, + benchutils::GetDataType()); + } + + return nvcv::Tensor( + { + {shape.x, shape.y, shape.z, channels}, + "NHWC" + }, + benchutils::GetDataType()); +} + +template +void RunTensorComposite(nvbench::state &state, cvcuda::Composite &op, int3 shape, bool isPlanar, int outChannels) +{ + using BT = typename nvcv::cuda::BaseType; + + const int ch = nvcv::cuda::NumElements; + + nvcv::Tensor fg = MakeCompositeTensor(shape, ch, isPlanar); + nvcv::Tensor bg = MakeCompositeTensor(shape, ch, isPlanar); + nvcv::Tensor mask = MakeCompositeTensor(shape, 1, isPlanar); + nvcv::Tensor dst = MakeCompositeTensor(shape, outChannels, isPlanar); + + benchutils::FillTensor(fg, benchutils::CheckerboardValues()); + benchutils::FillTensor(bg, benchutils::CheckerboardValues()); + benchutils::FillTensor(mask, [](const long4_16a &) { return 1; }); + + benchutils::warmup_and_exec(state, BENCH_COMPOSITE_WARMUP_ITERATIONS, + [&op, &fg, &bg, &mask, &dst](cudaStream_t s) { op(s, fg, bg, mask, dst); }); +} + +template +void FillCompositeVarShapeInputs(nvcv::ImageBatchVarShape &fg, nvcv::ImageBatchVarShape &bg, + nvcv::ImageBatchVarShape &mask, int3 shape, bool isPlanar) +{ + if (isPlanar) + { + benchutils::FillPlanarImageBatch(fg, long2{shape.z, shape.y}, long2{0, 0}); + benchutils::FillPlanarImageBatch(bg, long2{shape.z, shape.y}, long2{0, 0}); + } + else + { + benchutils::FillImageBatch(fg, long2{shape.z, shape.y}, long2{0, 0}, benchutils::CheckerboardValues()); + benchutils::FillImageBatchLike(bg, fg, benchutils::CheckerboardValues()); + } + + benchutils::FillImageBatchLike(mask, fg, [](const long4_16a &) { return 1; }); +} + +template +void FillCompositeVarShapeOutput(nvcv::ImageBatchVarShape &dst, const nvcv::ImageBatchVarShape &fg, bool isPlanar) +{ + if (isPlanar) + { + benchutils::FillPlanarImageBatchLike(dst, fg); + } + else + { + nvcv::ImageFormat format; + if constexpr (nvcv::cuda::NumElements == 3) + { + format = nvcv::FMT_RGB8; + } + else + { + static_assert(nvcv::cuda::NumElements == 4); + format = nvcv::FMT_RGBA8; + } + benchutils::FillImageBatchLike( + dst, fg, [](const long4_16a &) { return OutT{0}; }, format); + } +} + +template +void RunVarShapeComposite(nvbench::state &state, cvcuda::Composite &op, int3 shape, bool isPlanar, int outChannels) +{ + nvcv::ImageBatchVarShape fg(shape.x); + nvcv::ImageBatchVarShape bg(shape.x); + nvcv::ImageBatchVarShape mask(shape.x); + nvcv::ImageBatchVarShape dst(shape.x); + + FillCompositeVarShapeInputs(fg, bg, mask, shape, isPlanar); + if (outChannels == 3) + { + FillCompositeVarShapeOutput(dst, fg, isPlanar); + } + else + { + FillCompositeVarShapeOutput(dst, fg, isPlanar); + } + + benchutils::warmup_and_exec(state, BENCH_COMPOSITE_WARMUP_ITERATIONS, + [&op, &fg, &bg, &mask, &dst](cudaStream_t s) { op(s, fg, bg, mask, dst); }); +} + +template +bool DispatchCompositeRun(nvbench::state &state, cvcuda::Composite &op, int3 shape, benchutils::InputKind inputKind, + CompositeLayout layout, int outChannels) +{ + if (SkipUnsupportedCompositeCase(state, inputKind, layout)) + { + return false; + } + + if (IsFakePlanar(layout)) + { + RunFakePlanarComposite(state, op, shape, outChannels); + return true; + } + + if (inputKind == benchutils::InputKind::Tensor) + { + RunTensorComposite(state, op, shape, IsPlanar(layout), outChannels); + return true; + } + + RunVarShapeComposite(state, op, shape, IsPlanar(layout), outChannels); + return true; +} + +template +inline void composite(nvbench::state &state, nvbench::type_list) +try +{ + int3 shape = benchutils::GetShape<3, int3>(state.get_string("shape")); + const benchutils::InputKind inputKind = benchutils::GetInputKind(state.get_string("inputKind")); + auto layout = benchutils::GetStringParam(state, "layout", "NHWC"); + int outChannels = benchutils::GetIntParam(state, "outChannels"); + + CompositeLayout compositeLayout = ValidateCompositeBenchParams(state, layout, outChannels); + if (compositeLayout == CompositeLayout::INVALID) + { + return; + } + + using BT = typename nvcv::cuda::BaseType; + + state.add_global_memory_reads(shape.x * shape.y * shape.z * (sizeof(T) * 2 + sizeof(M))); + state.add_global_memory_writes(shape.x * shape.y * shape.z * outChannels * sizeof(BT)); + + cvcuda::Composite op; + DispatchCompositeRun(state, op, shape, inputKind, compositeLayout, outChannels); +} +CVCUDA_BENCH_SKIP_ERRORS(state) + +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(composite, NVBENCH_TYPE_AXES(BENCH_COMPOSITE_TYPES)) +BENCH_COMPOSITE_AXES; diff --git a/bench/cpp/ops/BenchConv2D.cpp b/bench/cpp/ops/BenchConv2D.cpp new file mode 100644 index 000000000..76e8685bd --- /dev/null +++ b/bench/cpp/ops/BenchConv2D.cpp @@ -0,0 +1,108 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchConv2DConfig.hpp" + +#include + +#include + +template +inline void conv2d(nvbench::state &state, nvbench::type_list) +try +{ + int3 shape = benchutils::GetShape<3, int3>(state.get_string("shape")); + const benchutils::InputKind inputKind = benchutils::GetInputKind(state.get_string("inputKind")); + auto layout = benchutils::GetStringParam(state, "layout", "NHWC"); + int2 kernelSize = nvcv::cuda::StaticCast(benchutils::GetShape<2>(state.get_string("kernelSize"))); + + NVCVBorderType borderType = benchutils::GetBorderType(state.get_string("border")); + + if (layout != "NHWC" && layout != "NCHW") + { + state.skip("Conv2D benchmark supports only NHWC and NCHW layouts"); + return; + } + + constexpr int ch = nvcv::cuda::NumElements; + const bool isPlanar = layout == "NCHW"; + if (isPlanar && ch == 1) + { + state.skip("Single-channel Conv2D has no distinct planar image-batch layout"); + return; + } + if (isPlanar && ch == 2) + { + state.skip("Planar Conv2D benchmark does not support 2-channel layouts"); + return; + } + + state.add_global_memory_reads(shape.x * shape.y * shape.z * sizeof(T)); + state.add_global_memory_writes(shape.x * shape.y * shape.z * sizeof(T)); + + cvcuda::Conv2D op; + + // clang-format off + + nvcv::Tensor kernelAnchor({{shape.x}, "N"}, nvcv::TYPE_2S32); + + benchutils::FillTensor(kernelAnchor, [](auto &){ return int2{-1, -1}; }); + + if (inputKind == benchutils::InputKind::Tensor) + { + throw std::invalid_argument("Tensor not implemented for this operator"); + } + else // zero and positive var shape means use ImageBatchVarShape + { + nvcv::ImageBatchVarShape src(shape.x); + nvcv::ImageBatchVarShape dst(shape.x); + nvcv::ImageBatchVarShape kernel(shape.x); + + if (isPlanar) + { + benchutils::FillPlanarImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}); + benchutils::FillPlanarImageBatchLike(dst, src); + } + else + { + benchutils::FillImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}, + benchutils::CheckerboardValues()); + // Use FillImageBatchLike to ensure dst has same per-sample shapes as src + benchutils::FillImageBatchLike(dst, src, [](const long4_16a &) { return T{0}; }); + } + + // Use default-range LcgValues so the GPU LCG fast path runs on the + // device; the Python bench's fill_mode="lcg" produces bit-identical + // float32 weights via the same kernel. + benchutils::FillImageBatch(kernel, long2{kernelSize.x, kernelSize.y}, long2{0, 0}, + benchutils::LcgValues()); + + benchutils::warmup_and_exec(state, BENCH_CONV2D_WARMUP_ITERATIONS, + [&op, &src, &dst, &kernel, &kernelAnchor, &borderType](cudaStream_t s) { + op(s, src, dst, kernel, kernelAnchor, borderType); + }); + } +} +CVCUDA_BENCH_SKIP_ERRORS(state) + +// clang-format on + +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(conv2d, NVBENCH_TYPE_AXES(BENCH_CONV2D_TYPES)) +BENCH_CONV2D_AXES; diff --git a/bench/cpp/ops/BenchConvertTo.cpp b/bench/cpp/ops/BenchConvertTo.cpp new file mode 100644 index 000000000..a49db0a37 --- /dev/null +++ b/bench/cpp/ops/BenchConvertTo.cpp @@ -0,0 +1,185 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchConvertToConfig.hpp" + +#include +#include + +#include + +inline nvcv::DataType GetConvertToDataType(const std::string &dtype) +{ + if (dtype == "uint8") + { + return nvcv::TYPE_U8; + } + else if (dtype == "uint16") + { + return nvcv::TYPE_U16; + } + else if (dtype == "int16") + { + return nvcv::TYPE_S16; + } + else if (dtype == "int32") + { + return nvcv::TYPE_S32; + } + else if (dtype == "float32") + { + return nvcv::TYPE_F32; + } + else if (dtype == "float64") + { + return nvcv::TYPE_F64; + } + + throw std::invalid_argument("Invalid outDataType = " + dtype); +} + +inline long GetConvertToTypeSize(const std::string &dtype) +{ + if (dtype == "uint8") + { + return 1; + } + else if (dtype == "uint16" || dtype == "int16") + { + return 2; + } + else if (dtype == "int32" || dtype == "float32") + { + return 4; + } + else if (dtype == "float64") + { + return 8; + } + + throw std::invalid_argument("Invalid outDataType = " + dtype); +} + +inline std::pair GetConvertToScale(const std::string &scaleMode) +{ + if (scaleMode == "affine") + { + return {0.123, 0.456}; + } + else if (scaleMode == "identity") + { + return {1.0, 0.0}; + } + + throw std::invalid_argument("Invalid scaleMode = " + scaleMode); +} + +template +inline void convertto(nvbench::state &state, nvbench::type_list) +try +{ + int3 shape = benchutils::GetShape<3, int3>(state.get_string("shape")); + auto outDataType = state.get_string("outDataType"); + auto scaleMode = state.get_string("scaleMode"); + auto layout = benchutils::GetStringParam(state, "layout", "NHWC"); + const benchutils::InputKind inputKind = benchutils::GetInputKind(state.get_string("inputKind")); + + auto [alpha, beta] = GetConvertToScale(scaleMode); + + using BT = typename nvcv::cuda::BaseType; + + int ch = nvcv::cuda::NumElements; + + const bool isPlanar = layout == "NCHW"; + const bool isFakePlanar = layout == "NCHW_FAKE"; + if (layout != "NHWC" && layout != "NCHW" && layout != "NCHW_FAKE") + { + state.skip("ConvertTo benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts"); + return; + } + // ConvertTo is tensor-only; the native planar (NCHW) and fake-planar (NCHW_FAKE) paths are too. + if ((isPlanar || isFakePlanar) && inputKind != benchutils::InputKind::Tensor) + { + state.skip("Planar ConvertTo benchmark is tensor-only"); + return; + } + + const long srcBytes = shape.x * shape.y * shape.z * sizeof(T); + const long dstBytes = static_cast(shape.x) * shape.y * shape.z * ch * GetConvertToTypeSize(outDataType); + if (isFakePlanar) + { + // reformat(NCHW→NHWC) + convert + reformat(NHWC→NCHW): reads src twice + dst once, + // writes the interleaved src once + dst twice. + state.add_global_memory_reads(2 * srcBytes + dstBytes); + state.add_global_memory_writes(srcBytes + 2 * dstBytes); + } + else + { + state.add_global_memory_reads(srcBytes); + state.add_global_memory_writes(dstBytes); + } + + cvcuda::ConvertTo op; + + // clang-format off + + if (isFakePlanar) // tensor-only: planar→interleaved→convert→interleaved→planar + { + nvcv::Tensor src ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + nvcv::Tensor interSrc({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor interDst({{shape.x, shape.y, shape.z, ch}, "NHWC"}, GetConvertToDataType(outDataType)); + nvcv::Tensor dst ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, GetConvertToDataType(outDataType)); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + cvcuda::Reformat reformatOp; + + benchutils::warmup_and_exec(state, BENCH_CONVERTTO_WARMUP_ITERATIONS, + [&op, &reformatOp, &src, &interSrc, &interDst, &dst, &alpha, &beta](cudaStream_t s) { + reformatOp(s, src, interSrc); // NCHW → NHWC + op(s, interSrc, interDst, alpha, beta); // interleaved convert + reformatOp(s, interDst, dst); // NHWC → NCHW + }); + } + else if (inputKind == benchutils::InputKind::Tensor) + { + nvcv::Tensor src = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, GetConvertToDataType(outDataType)) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, GetConvertToDataType(outDataType)); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + benchutils::warmup_and_exec(state, BENCH_CONVERTTO_WARMUP_ITERATIONS, + [&op, &src, &dst, &alpha, &beta](cudaStream_t s) { op(s, src, dst, alpha, beta); }); + } + else // zero and positive var shape means use ImageBatchVarShape + { + throw std::invalid_argument("ImageBatchVarShape not implemented for this operator"); + } +} +CVCUDA_BENCH_SKIP_ERRORS(state) + +// clang-format on + +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(convertto, NVBENCH_TYPE_AXES(BENCH_CONVERTTO_TYPES)) +BENCH_CONVERTTO_AXES; diff --git a/bench/cpp/ops/BenchCopyMakeBorder.cpp b/bench/cpp/ops/BenchCopyMakeBorder.cpp new file mode 100644 index 000000000..9ba5016d9 --- /dev/null +++ b/bench/cpp/ops/BenchCopyMakeBorder.cpp @@ -0,0 +1,152 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchCopyMakeBorderConfig.hpp" + +#include +#include + +#include + +template +inline void copymakeborder(nvbench::state &state, nvbench::type_list) +try +{ + long3 srcShape = benchutils::GetShape<3>(state.get_string("shape")); + auto layout = benchutils::GetStringParam(state, "layout", "NHWC"); + const benchutils::InputKind inputKind = benchutils::GetInputKind(state.get_string("inputKind")); + + using BT = typename nvcv::cuda::BaseType; + + int ch = nvcv::cuda::NumElements; + + const bool isPlanar = layout == "NCHW"; + const bool isFakePlanar = layout == "NCHW_FAKE"; + if (layout != "NHWC" && layout != "NCHW" && layout != "NCHW_FAKE") + { + state.skip("CopyMakeBorder benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts"); + return; + } + if (isFakePlanar && inputKind == benchutils::InputKind::VarShape) + { + state.skip("Fake-planar (NCHW_FAKE) CopyMakeBorder benchmark is tensor-only"); + return; + } + + NVCVBorderType borderType = benchutils::GetBorderType(state.get_string("border")); + + float4 borderValue{0.f, 0.f, 0.f, 0.f}; + + auto top = static_cast(srcShape.y / 2); + auto left = static_cast(srcShape.z / 2); + + long3 dstShape{srcShape.x, top + srcShape.y, left + srcShape.z}; + + const long srcBytes = srcShape.x * srcShape.y * srcShape.z * sizeof(T); + const long dstBytes = dstShape.x * dstShape.y * dstShape.z * sizeof(T); + if (isFakePlanar) + { + state.add_global_memory_reads(2 * srcBytes + dstBytes); + state.add_global_memory_writes(srcBytes + 2 * dstBytes); + } + else + { + state.add_global_memory_reads(srcBytes); + state.add_global_memory_writes(dstBytes); + } + + cvcuda::CopyMakeBorder op; + + // clang-format off + + if (isFakePlanar) + { + nvcv::Tensor src ({{srcShape.x, ch, srcShape.y, srcShape.z}, "NCHW"}, benchutils::GetDataType()); + nvcv::Tensor interSrc({{srcShape.x, srcShape.y, srcShape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor interDst({{dstShape.x, dstShape.y, dstShape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst ({{dstShape.x, ch, dstShape.y, dstShape.z}, "NCHW"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + cvcuda::Reformat reformatOp; + + benchutils::warmup_and_exec(state, BENCH_COPYMAKEBORDER_WARMUP_ITERATIONS, + [&op, &reformatOp, &src, &interSrc, &interDst, &dst, &top, &left, &borderType, &borderValue](cudaStream_t s) { + reformatOp(s, src, interSrc); + op(s, interSrc, interDst, top, left, borderType, borderValue); + reformatOp(s, interDst, dst); + }); + } + else if (inputKind == benchutils::InputKind::Tensor) + { + nvcv::Tensor src = isPlanar + ? nvcv::Tensor({{srcShape.x, ch, srcShape.y, srcShape.z}, "NCHW"}, + benchutils::GetDataType()) + : nvcv::Tensor({{srcShape.x, srcShape.y, srcShape.z, ch}, "NHWC"}, + benchutils::GetDataType()); + nvcv::Tensor dst = isPlanar + ? nvcv::Tensor({{dstShape.x, ch, dstShape.y, dstShape.z}, "NCHW"}, + benchutils::GetDataType()) + : nvcv::Tensor({{dstShape.x, dstShape.y, dstShape.z, ch}, "NHWC"}, + benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + benchutils::warmup_and_exec(state, BENCH_COPYMAKEBORDER_WARMUP_ITERATIONS, + [&op, &src, &dst, &top, &left, &borderType, &borderValue](cudaStream_t s) { + op(s, src, dst, top, left, borderType, borderValue); + }); + } + else // zero and positive var shape means use ImageBatchVarShape + { + nvcv::ImageBatchVarShape src(static_cast(srcShape.x)); + nvcv::ImageBatchVarShape dst(static_cast(dstShape.x)); + + if (isPlanar) + { + benchutils::FillPlanarImageBatch(src, long2{srcShape.z, srcShape.y}, long2{0, 0}); + benchutils::FillPlanarImageBatch(dst, long2{dstShape.z, dstShape.y}, long2{0, 0}, false); + } + else + { + benchutils::FillImageBatch(src, long2{srcShape.z, srcShape.y}, long2{0, 0}, + benchutils::CheckerboardValues()); + benchutils::FillImageBatch(dst, long2{dstShape.z, dstShape.y}, long2{0, 0}, + [](const long4_16a &) { return T{0}; }); + } + + nvcv::Tensor topTensor({{srcShape.x, 1, 1, 1}, "NHWC"}, nvcv::TYPE_S32); + nvcv::Tensor leftTensor({{srcShape.x, 1, 1, 1}, "NHWC"}, nvcv::TYPE_S32); + + benchutils::FillTensor(topTensor, [&top](const long4_16a &){ return top; }); + benchutils::FillTensor(leftTensor, [&left](const long4_16a &){ return left; }); + + benchutils::warmup_and_exec(state, BENCH_COPYMAKEBORDER_WARMUP_ITERATIONS, + [&op, &src, &dst, &topTensor, &leftTensor, &borderType, &borderValue](cudaStream_t s) { + op(s, src, dst, topTensor, leftTensor, borderType, borderValue); + }); + } +} +CVCUDA_BENCH_SKIP_ERRORS(state) + +// clang-format on + +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(copymakeborder, NVBENCH_TYPE_AXES(BENCH_COPYMAKEBORDER_TYPES)) +BENCH_COPYMAKEBORDER_AXES; diff --git a/bench/cpp/ops/BenchCropFlipNormalizeReformat.cpp b/bench/cpp/ops/BenchCropFlipNormalizeReformat.cpp new file mode 100644 index 000000000..73750ee18 --- /dev/null +++ b/bench/cpp/ops/BenchCropFlipNormalizeReformat.cpp @@ -0,0 +1,167 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchCropFlipNormalizeReformatConfig.hpp" + +#include +#include + +#include + +inline uint32_t GetCropFlipNormalizeReformatFlags(const std::string &flagsMode) +{ + if (flagsMode == "normal") + { + return 0; + } + else if (flagsMode == "stddev") + { + return CVCUDA_NORMALIZE_SCALE_IS_STDDEV; + } + + throw std::invalid_argument("Invalid flagsMode = " + flagsMode); +} + +template +inline void cropflipnormalizereformat(nvbench::state &state, nvbench::type_list) +try +{ + long3 srcShape = benchutils::GetShape<3>(state.get_string("shape")); + auto cropMode = state.get_string("cropMode"); + auto flagsMode = state.get_string("flagsMode"); + const benchutils::InputKind inputKind = benchutils::GetInputKind(state.get_string("inputKind")); + const std::string srcLayout = state.get_string("srcLayout"); + const std::string dstLayout = state.get_string("layout"); + long3 dstShape = srcShape; + + if (cropMode != "full" && cropMode != "padded16") + { + throw std::invalid_argument("Invalid cropMode = " + cropMode); + } + + using BT = typename nvcv::cuda::BaseType; + + int ch = nvcv::cuda::NumElements; + + if ((srcLayout != "NHWC" && srcLayout != "NCHW") || (dstLayout != "NHWC" && dstLayout != "NCHW")) + { + state.skip("CropFlipNormalizeReformat benchmark supports only NHWC and NCHW source/output layouts"); + return; + } + + const bool srcPlanar = srcLayout == "NCHW"; + const bool dstPlanar = dstLayout == "NCHW"; + + NVCVBorderType borderType = benchutils::GetBorderType(state.get_string("border")); + + float borderValue{0.f}; + + float globalScale = 1.234f; + float globalShift = 2.345f; + float epsilon = 12.34f; + uint32_t flags = GetCropFlipNormalizeReformatFlags(flagsMode); + + long3 baseShape{srcShape.x, 1, 1}; + long3 scaleShape{srcShape.x, 1, 1}; + long3 cropShape{srcShape.x, 1, 1}; + + state.add_global_memory_reads(srcShape.x * srcShape.y * srcShape.z * sizeof(T) + + baseShape.x * baseShape.y * baseShape.z * sizeof(float) + + scaleShape.x * scaleShape.y * scaleShape.z * sizeof(float) + + cropShape.x * cropShape.y * cropShape.z * sizeof(int) * 4); + state.add_global_memory_writes(dstShape.x * dstShape.y * dstShape.z * sizeof(T)); + + cvcuda::CropFlipNormalizeReformat op; + + // clang-format off + + nvcv::Tensor dst = dstPlanar + ? nvcv::Tensor({{dstShape.x, ch, dstShape.y, dstShape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{dstShape.x, dstShape.y, dstShape.z, ch}, "NHWC"}, benchutils::GetDataType()); + + nvcv::Tensor flipCode({{srcShape.x}, "N"}, nvcv::TYPE_S32); + + nvcv::Tensor base({{baseShape.x, baseShape.y, baseShape.z, 1}, "NHWC"}, nvcv::TYPE_F32); + nvcv::Tensor scale({{scaleShape.x, scaleShape.y, scaleShape.z, 1}, "NHWC"}, nvcv::TYPE_F32); + + nvcv::Tensor crop({{cropShape.x, cropShape.y, cropShape.z, 4}, "NHWC"}, nvcv::TYPE_S32); + + benchutils::FillTensor(flipCode, [](auto &){ return -1; }); + + // base, scale: default-range LcgValues() so the GPU LCG fast + // path produces float [-1, +1] bytes identical to the Python bench's + // create_tensor(..., fill_mode="lcg"). + benchutils::FillTensor(base, benchutils::LcgValues()); + benchutils::FillTensor(scale, benchutils::LcgValues()); + + // Always crop entire source image for easy bandwidth calculations + benchutils::FillTensor(crop, [&srcShape, &cropMode](const long4_16a &c) + { + if (c.w == 0) + { + return cropMode == "padded16" ? -16 : 0; + } + else if (c.w == 1) + { + return cropMode == "padded16" ? -16 : 0; + } + else if (c.w == 2) + { + return (int)srcShape.z; + } + else if (c.w == 3) + { + return (int)srcShape.y; + } + return 0; + }); + + if (inputKind == benchutils::InputKind::Tensor) + { + throw std::invalid_argument("Tensor not implemented for this operator"); + } + else // zero and positive var shape means use ImageBatchVarShape + { + nvcv::ImageBatchVarShape src(static_cast(srcShape.x)); + + if (srcPlanar && ch > 1) + { + benchutils::FillPlanarImageBatch(src, long2{srcShape.z, srcShape.y}, long2{0, 0}); + } + else + { + benchutils::FillImageBatch(src, long2{srcShape.z, srcShape.y}, long2{0, 0}, + benchutils::CheckerboardValues()); + } + + benchutils::warmup_and_exec(state, BENCH_CROPFLIPNORMALIZEREFORMAT_WARMUP_ITERATIONS, + [&op, &src, &dst, &crop, &borderType, &borderValue, &flipCode, &base, &scale, &globalScale, + &globalShift, &epsilon, &flags](cudaStream_t s) { + op(s, src, dst, crop, borderType, borderValue, flipCode, base, scale, globalScale, + globalShift, epsilon, flags); + }); + } +} +CVCUDA_BENCH_SKIP_ERRORS(state) + +// clang-format on + +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(cropflipnormalizereformat, NVBENCH_TYPE_AXES(BENCH_CROPFLIPNORMALIZEREFORMAT_TYPES)) +BENCH_CROPFLIPNORMALIZEREFORMAT_AXES; diff --git a/bench/cpp/ops/BenchCustomCrop.cpp b/bench/cpp/ops/BenchCustomCrop.cpp new file mode 100644 index 000000000..4577171eb --- /dev/null +++ b/bench/cpp/ops/BenchCustomCrop.cpp @@ -0,0 +1,125 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchCustomCropConfig.hpp" + +#include +#include + +#include + +template +inline void customcrop(nvbench::state &state, nvbench::type_list) +try +{ + int3 shape = benchutils::GetShape<3, int3>(state.get_string("shape")); + auto cropMode = state.get_string("cropMode"); + auto layout = benchutils::GetStringParam(state, "layout", "NHWC"); + const benchutils::InputKind inputKind = benchutils::GetInputKind(state.get_string("inputKind")); + + using BT = typename nvcv::cuda::BaseType; + + int ch = nvcv::cuda::NumElements; + + const bool isPlanar = layout == "NCHW"; + const bool isFakePlanar = layout == "NCHW_FAKE"; + if (layout != "NHWC" && layout != "NCHW" && layout != "NCHW_FAKE") + { + state.skip("CustomCrop benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts"); + return; + } + // CustomCrop is tensor-only; the native planar (NCHW) and fake-planar (NCHW_FAKE) paths are too. + if ((isPlanar || isFakePlanar) && inputKind != benchutils::InputKind::Tensor) + { + state.skip("Planar CustomCrop benchmark is tensor-only"); + return; + } + + NVCVRectI cropRect{0, 0, shape.z, shape.y}; + if (cropMode == "center_half") + { + cropRect = NVCVRectI{shape.z / 4, shape.y / 4, shape.z / 2, shape.y / 2}; + } + else if (cropMode != "full") + { + throw std::invalid_argument("Invalid cropMode = " + cropMode); + } + + const long fullBytes = static_cast(shape.x) * shape.y * shape.z * sizeof(T); + const long cropBytes = static_cast(shape.x) * cropRect.height * cropRect.width * sizeof(T); + if (isFakePlanar) + { + // reformat(NCHW→NHWC) + crop + reformat(NHWC→NCHW). + state.add_global_memory_reads(fullBytes + 2 * cropBytes); + state.add_global_memory_writes(fullBytes + 2 * cropBytes); + } + else + { + state.add_global_memory_reads(cropBytes); + state.add_global_memory_writes(cropBytes); + } + + cvcuda::CustomCrop op; + + // clang-format off + + if (isFakePlanar) // tensor-only: planar→interleaved→crop→interleaved→planar + { + nvcv::Tensor src ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + nvcv::Tensor interSrc({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor interDst({{shape.x, cropRect.height, cropRect.width, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst ({{shape.x, ch, cropRect.height, cropRect.width}, "NCHW"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + cvcuda::Reformat reformatOp; + + benchutils::warmup_and_exec(state, BENCH_CUSTOMCROP_WARMUP_ITERATIONS, + [&op, &reformatOp, &src, &interSrc, &interDst, &dst, &cropRect](cudaStream_t s) { + reformatOp(s, src, interSrc); // NCHW → NHWC + op(s, interSrc, interDst, cropRect); // interleaved crop + reformatOp(s, interDst, dst); // NHWC → NCHW + }); + } + else if (inputKind == benchutils::InputKind::Tensor) + { + nvcv::Tensor src = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst = isPlanar + ? nvcv::Tensor({{shape.x, ch, cropRect.height, cropRect.width}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, cropRect.height, cropRect.width, ch}, "NHWC"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + benchutils::warmup_and_exec(state, BENCH_CUSTOMCROP_WARMUP_ITERATIONS, + [&op, &src, &dst, &cropRect](cudaStream_t s) { op(s, src, dst, cropRect); }); + } + else // zero and positive var shape means use ImageBatchVarShape + { + throw std::invalid_argument("ImageBatchVarShape not implemented for this operator"); + } +} +CVCUDA_BENCH_SKIP_ERRORS(state) + +// clang-format on + +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(customcrop, NVBENCH_TYPE_AXES(BENCH_CUSTOMCROP_TYPES)) +BENCH_CUSTOMCROP_AXES; diff --git a/bench/cpp/ops/BenchCvtColor.cpp b/bench/cpp/ops/BenchCvtColor.cpp new file mode 100644 index 000000000..55abd67a0 --- /dev/null +++ b/bench/cpp/ops/BenchCvtColor.cpp @@ -0,0 +1,399 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchCvtColorConfig.hpp" + +#include +#include + +#include + +#include +#include +#include +#include + +using ConvCodeToFormat = std::tuple; +using CodeMap = std::map>; + +template +inline float bytesPerPixel(NVCVImageFormat imgFormat); + +namespace { + +enum class CvtColorLayout +{ + NHWC, + NCHW, + NCHWFake +}; + +CvtColorLayout GetCvtColorLayout(std::string_view layout, benchutils::InputKind inputKind) +{ + if (layout == "NHWC") + { + return CvtColorLayout::NHWC; + } + if (layout == "NCHW") + { + return CvtColorLayout::NCHW; + } + if (layout == "NCHW_FAKE") + { + if (inputKind == benchutils::InputKind::VarShape) + { + throw std::invalid_argument("Fake-planar (NCHW_FAKE) CvtColor benchmark is tensor-only"); + } + return CvtColorLayout::NCHWFake; + } + + throw std::invalid_argument("CvtColor benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts"); +} + +bool IsPlanar(CvtColorLayout layout) +{ + return layout == CvtColorLayout::NCHW; +} + +bool IsFakePlanar(CvtColorLayout layout) +{ + return layout == CvtColorLayout::NCHWFake; +} + +int NumFormatChannels(nvcv::ImageFormat format) +{ + return format.numPlanes() == 1 ? format.planeNumChannels(0) : format.numPlanes(); +} + +nvcv::ImageFormat PlanarVarShapeFormat(nvcv::ImageFormat format) +{ + if (format == NVCV_IMAGE_FORMAT_RGB8) + { + return nvcv::FMT_RGB8p; + } + if (format == NVCV_IMAGE_FORMAT_RGBA8) + { + return nvcv::FMT_RGBA8p; + } + + throw std::invalid_argument("Planar CvtColor var-shape benchmark supports only RGB8p/RGBA8p formats"); +} + +template +void AddCvtColorMemoryTraffic(nvbench::state &state, size_t pixelCount, NVCVImageFormat inFormat, + NVCVImageFormat outFormat, CvtColorLayout layout) +{ + const auto srcBytes = static_cast(static_cast(pixelCount) * bytesPerPixel(inFormat)); + const auto dstBytes = static_cast(static_cast(pixelCount) * bytesPerPixel(outFormat)); + + if (IsFakePlanar(layout)) + { + state.add_global_memory_reads(2 * srcBytes + dstBytes); + state.add_global_memory_writes(srcBytes + 2 * dstBytes); + } + else + { + state.add_global_memory_reads(srcBytes); + state.add_global_memory_writes(dstBytes); + } +} + +template +nvcv::Tensor CreatePlanarTensor(int numImages, int imgWidth, int imgHeight, nvcv::ImageFormat format) +{ + return nvcv::Tensor( + { + {numImages, NumFormatChannels(format), imgHeight, imgWidth}, + "NCHW" + }, + benchutils::GetDataType()); +} + +void FillPlanarPlaneData(std::vector &planeData, int imageIndex, int planeIndex, bool checker) +{ + if (!checker) + { + return; + } + + for (size_t idx = 0; idx < planeData.size(); ++idx) + { + planeData[idx] = static_cast(((imageIndex + planeIndex + idx) & 1) ? 255 : 0); + } +} + +void FillPlanarImage(nvcv::Image &image, int imageIndex, int channels, bool checker) +{ + auto data = image.exportData(); + CVCUDA_CHECK_DATA(data); + + for (int p = 0; p < channels; ++p) + { + const auto &plane = data->plane(p); + std::vector planeData(static_cast(plane.rowStride) * plane.height); + FillPlanarPlaneData(planeData, imageIndex, p, checker); + CUDA_CHECK_ERROR(cudaMemcpy2D(plane.basePtr, plane.rowStride, planeData.data(), plane.rowStride, + plane.rowStride, plane.height, cudaMemcpyHostToDevice)); + } +} + +void FillPlanarImageBatch(nvcv::ImageBatchVarShape &batch, int3 shape, nvcv::ImageFormat format, bool checker) +{ + const int channels = NumFormatChannels(format); + for (int i = 0; i < shape.x; ++i) + { + nvcv::Image image(nvcv::Size2D{shape.z, shape.y}, format); + FillPlanarImage(image, i, channels, checker); + + batch.pushBack(image); + } +} + +} // namespace + +inline static ConvCodeToFormat str2Frmt(const std::string &str) +{ + // clang-format off + static const CodeMap codeMap { + { "RGB2BGR", {NVCV_COLOR_RGB2BGR, NVCV_IMAGE_FORMAT_RGB8, NVCV_IMAGE_FORMAT_BGR8 }}, + { "RGB2RGBA", {NVCV_COLOR_RGB2RGBA, NVCV_IMAGE_FORMAT_RGB8, NVCV_IMAGE_FORMAT_RGBA8}}, + { "RGBA2RGB", {NVCV_COLOR_RGBA2RGB, NVCV_IMAGE_FORMAT_RGBA8, NVCV_IMAGE_FORMAT_RGB8 }}, + { "RGB2GRAY", {NVCV_COLOR_RGB2GRAY, NVCV_IMAGE_FORMAT_RGB8, NVCV_IMAGE_FORMAT_Y8 }}, + { "GRAY2RGB", {NVCV_COLOR_GRAY2RGB, NVCV_IMAGE_FORMAT_Y8, NVCV_IMAGE_FORMAT_RGB8 }}, + { "RGB2HSV", {NVCV_COLOR_RGB2HSV, NVCV_IMAGE_FORMAT_RGB8, NVCV_IMAGE_FORMAT_HSV8 }}, + { "HSV2RGB", {NVCV_COLOR_HSV2RGB, NVCV_IMAGE_FORMAT_HSV8, NVCV_IMAGE_FORMAT_RGB8 }}, + { "RGB2YUV", {NVCV_COLOR_RGB2YUV, NVCV_IMAGE_FORMAT_RGB8, NVCV_IMAGE_FORMAT_YUV8 }}, + { "YUV2RGB", {NVCV_COLOR_YUV2RGB, NVCV_IMAGE_FORMAT_YUV8, NVCV_IMAGE_FORMAT_RGB8 }}, + {"RGB2YUV_NV12", {NVCV_COLOR_RGB2YUV_NV12, NVCV_IMAGE_FORMAT_RGB8, NVCV_IMAGE_FORMAT_NV12 }}, + {"YUV2RGB_NV12", {NVCV_COLOR_YUV2RGB_NV12, NVCV_IMAGE_FORMAT_NV12, NVCV_IMAGE_FORMAT_RGB8 }}, + }; + // clang-format on + + if (auto it = codeMap.find(str); it != codeMap.end()) + { + return it->second; + } + else + { + throw std::invalid_argument("Unrecognized color code"); + } +} + +template +inline float bytesPerPixel(NVCVImageFormat imgFormat) +{ +#define BPP_CASE(frmt, bytes) \ + case frmt: \ + return bytes * sizeof(BT) + + switch (imgFormat) + { + BPP_CASE(NVCV_IMAGE_FORMAT_RGB8, 3); + BPP_CASE(NVCV_IMAGE_FORMAT_BGR8, 3); + BPP_CASE(NVCV_IMAGE_FORMAT_HSV8, 3); + BPP_CASE(NVCV_IMAGE_FORMAT_RGBA8, 4); + BPP_CASE(NVCV_IMAGE_FORMAT_YUV8, 3); + BPP_CASE(NVCV_IMAGE_FORMAT_NV12, 1.5f); + BPP_CASE(NVCV_IMAGE_FORMAT_Y8, 1); + default: + throw std::invalid_argument("Unrecognized format"); + } +#undef BPP_CASE +} + +// Adapted from src/util/TensorDataUtils.hpp +inline static nvcv::Tensor CreateTensor(int numImages, int imgWidth, int imgHeight, const nvcv::ImageFormat &imgFormat) +{ + if (imgFormat == NVCV_IMAGE_FORMAT_NV12 || imgFormat == NVCV_IMAGE_FORMAT_NV12_ER + || imgFormat == NVCV_IMAGE_FORMAT_NV21 || imgFormat == NVCV_IMAGE_FORMAT_NV21_ER) + { + if (imgHeight % 2 != 0 || imgWidth % 2 != 0) + { + throw std::invalid_argument("Invalid height"); + } + + int height420 = (imgHeight * 3) / 2; + + return nvcv::Tensor(numImages, {imgWidth, height420}, nvcv::ImageFormat(NVCV_IMAGE_FORMAT_Y8)); + } + else + { + return nvcv::Tensor(numImages, {imgWidth, imgHeight}, imgFormat); + } +} + +inline static bool IsSubsampledFormat(nvcv::ImageFormat format) +{ + const auto chromaSubsampling = format.chromaSubsampling(); + return chromaSubsampling != nvcv::ChromaSubsampling::NONE && chromaSubsampling != nvcv::ChromaSubsampling::CSS_444; +} + +inline static bool HasSubsampledFormat(nvcv::ImageFormat inFormat, nvcv::ImageFormat outFormat) +{ + return IsSubsampledFormat(inFormat) || IsSubsampledFormat(outFormat); +} + +template +void RunFakePlanarBenchmark(nvbench::state &state, cvcuda::CvtColor &op, int3 shape, nvcv::ImageFormat inFormat, + nvcv::ImageFormat outFormat, NVCVColorConversionCode code) +{ + nvcv::Tensor src = CreatePlanarTensor(shape.x, shape.z, shape.y, inFormat); + nvcv::Tensor interSrc = CreateTensor(shape.x, shape.z, shape.y, inFormat); + nvcv::Tensor interDst = CreateTensor(shape.x, shape.z, shape.y, outFormat); + nvcv::Tensor dst = CreatePlanarTensor(shape.x, shape.z, shape.y, outFormat); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + cvcuda::Reformat reformatOp; + + benchutils::warmup_and_exec(state, BENCH_CVTCOLOR_WARMUP_ITERATIONS, + [&op, &reformatOp, &src, &interSrc, &interDst, &dst, &code](cudaStream_t s) + { + reformatOp(s, src, interSrc); + op(s, interSrc, interDst, code); + reformatOp(s, interDst, dst); + }); +} + +template +void RunTensorBenchmark(nvbench::state &state, cvcuda::CvtColor &op, int3 shape, nvcv::ImageFormat inFormat, + nvcv::ImageFormat outFormat, CvtColorLayout layout, NVCVColorConversionCode code) +{ + nvcv::Tensor src = IsPlanar(layout) ? CreatePlanarTensor(shape.x, shape.z, shape.y, inFormat) + : CreateTensor(shape.x, shape.z, shape.y, inFormat); + nvcv::Tensor dst = IsPlanar(layout) ? CreatePlanarTensor(shape.x, shape.z, shape.y, outFormat) + : CreateTensor(shape.x, shape.z, shape.y, outFormat); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + benchutils::warmup_and_exec(state, BENCH_CVTCOLOR_WARMUP_ITERATIONS, + [&op, &src, &dst, &code](cudaStream_t s) { op(s, src, dst, code); }); +} + +inline static void FillInterleavedImageBatch(nvcv::ImageBatchVarShape &src, nvcv::ImageBatchVarShape &dst, + std::vector &imgSrc, std::vector &imgDst, + std::vector> &srcVec, int3 shape, + nvcv::ImageFormat inFormat, nvcv::ImageFormat outFormat) +{ + for (int i = 0; i < shape.x; i++) + { + imgSrc.emplace_back(nvcv::Size2D{shape.z, shape.y}, inFormat); + imgDst.emplace_back(nvcv::Size2D{shape.z, shape.y}, outFormat); + + int srcRowStride = imgSrc[i].size().w * inFormat.planePixelStrideBytes(0); + int srcBufSize = imgSrc[i].size().h * srcRowStride; + srcVec[i].resize(srcBufSize); + for (int idx = 0; idx < srcBufSize; idx++) + { + srcVec[i][idx] = static_cast(((i + idx) & 1) ? 255 : 0); + } + + auto imgData = imgSrc[i].exportData(); + CUDA_CHECK_ERROR(cudaMemcpy2D(imgData->plane(0).basePtr, imgData->plane(0).rowStride, srcVec[i].data(), + srcRowStride, srcRowStride, imgSrc[i].size().h, cudaMemcpyHostToDevice)); + } + src.pushBack(imgSrc.begin(), imgSrc.end()); + dst.pushBack(imgDst.begin(), imgDst.end()); +} + +inline static void RunVarShapeBenchmark(nvbench::state &state, cvcuda::CvtColor &op, int3 shape, + nvcv::ImageFormat inFormat, nvcv::ImageFormat outFormat, CvtColorLayout layout, + NVCVColorConversionCode code) +{ + if (HasSubsampledFormat(inFormat, outFormat)) + { + state.skip("Skipping formats that have subsampled planes for the varshape benchmark"); + return; + } + // Also skip YUV8 (planar in Python as YUV8p, not supported with ImageBatchVarShape) + if (inFormat == NVCV_IMAGE_FORMAT_YUV8 || outFormat == NVCV_IMAGE_FORMAT_YUV8) + { + state.skip("Skipping YUV8 format for varshape benchmark (planar format limitation)"); + return; + } + + std::vector imgSrc; + std::vector imgDst; + nvcv::ImageBatchVarShape src(shape.x); + nvcv::ImageBatchVarShape dst(shape.x); + std::vector> srcVec(shape.x); + + // Per-byte checkerboard fill (varied without paying random-distribution cost). + if (IsPlanar(layout)) + { + FillPlanarImageBatch(src, shape, PlanarVarShapeFormat(inFormat), true); + FillPlanarImageBatch(dst, shape, PlanarVarShapeFormat(outFormat), false); + } + else + { + FillInterleavedImageBatch(src, dst, imgSrc, imgDst, srcVec, shape, inFormat, outFormat); + } + + benchutils::warmup_and_exec(state, BENCH_CVTCOLOR_WARMUP_ITERATIONS, + [&op, &src, &dst, &code](cudaStream_t s) { op(s, src, dst, code); }); +} + +template +inline void cvtcolor(nvbench::state &state, nvbench::type_list) +try +{ + using BT = typename nvcv::cuda::BaseType; + + int3 shape = benchutils::GetShape<3, int3>(state.get_string("shape")); + const benchutils::InputKind inputKind = benchutils::GetInputKind(state.get_string("inputKind")); + auto layoutStr = benchutils::GetStringParam(state, "layout", "NHWC"); + using BaseT = nvcv::cuda::BaseType; + + auto [code, inFormatValue, outFormatValue] = str2Frmt(state.get_string("code")); + + nvcv::ImageFormat inFormat{inFormatValue}; + nvcv::ImageFormat outFormat{outFormatValue}; + CvtColorLayout layout = GetCvtColorLayout(layoutStr, inputKind); + + if ((IsPlanar(layout) || IsFakePlanar(layout)) && HasSubsampledFormat(inFormat, outFormat)) + { + state.skip("Skipping subsampled YUV CvtColor formats for planar benchmarks"); + return; + } + + const size_t pixelCount + = static_cast(shape.x) * static_cast(shape.y) * static_cast(shape.z); + AddCvtColorMemoryTraffic(state, pixelCount, inFormatValue, outFormatValue, layout); + + cvcuda::CvtColor op; + + if (IsFakePlanar(layout)) + { + RunFakePlanarBenchmark(state, op, shape, inFormat, outFormat, code); + } + else if (inputKind == benchutils::InputKind::Tensor) + { + RunTensorBenchmark(state, op, shape, inFormat, outFormat, layout, code); + } + else // zero and positive var shape means use ImageBatchVarShape + { + RunVarShapeBenchmark(state, op, shape, inFormat, outFormat, layout, code); + } +} + +CVCUDA_BENCH_SKIP_ERRORS(state) + +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(cvtcolor, NVBENCH_TYPE_AXES(BENCH_CVTCOLOR_TYPES)) +BENCH_CVTCOLOR_AXES; diff --git a/bench/cpp/ops/BenchErase.cpp b/bench/cpp/ops/BenchErase.cpp new file mode 100644 index 000000000..27a228470 --- /dev/null +++ b/bench/cpp/ops/BenchErase.cpp @@ -0,0 +1,198 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchEraseConfig.hpp" + +#include +#include + +#include + +#include +#include + +inline bool GetEraseRandomMode(const std::string &randomMode) +{ + if (randomMode == "random") + { + return true; + } + else if (randomMode == "constant") + { + return false; + } + + throw std::invalid_argument("Invalid randomMode = " + randomMode); +} + +template +inline const char *GetEraseSkipReason(std::string_view layout, benchutils::InputKind inputKind, bool regionMode, + int channels) +{ + const bool isPlanar = layout == "NCHW"; + const bool isFakePlanar = layout == "NCHW_FAKE"; + + if (layout != "NHWC" && !isPlanar && !isFakePlanar) + return "Erase benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts"; + if (isFakePlanar && inputKind == benchutils::InputKind::VarShape) + return "Fake-planar (NCHW_FAKE) erase benchmark is tensor-only"; + if (regionMode && inputKind == benchutils::InputKind::VarShape) + return "Torchvision Erase region benchmark is tensor-only"; + if (!regionMode && isPlanar && channels == 2) + return "Planar Erase benchmark does not support 2-channel layouts"; + if (isPlanar && inputKind == benchutils::InputKind::VarShape && channels == 1) + return "Single-channel varshape Erase has no distinct planar image layout"; + if (isPlanar && inputKind == benchutils::InputKind::VarShape && std::is_same_v && channels == 4) + return "RGBA8p varshape is unsupported by the Python image API"; + + return nullptr; +} + +template +inline void erase(nvbench::state &state, nvbench::type_list) +try +{ + int3 shape = benchutils::GetShape<3, int3>(state.get_string("shape")); + auto randomMode = state.get_string("randomMode"); + auto layout = benchutils::GetStringParam(state, "layout", "NHWC"); + const benchutils::InputKind inputKind = benchutils::GetInputKind(state.get_string("inputKind")); + int numErase = benchutils::GetIntParam(state, "numErase"); + + using BT = typename nvcv::cuda::BaseType; + + int ch = nvcv::cuda::NumElements; + + const bool regionMode = randomMode == "torchvision"; + bool random = regionMode ? false : GetEraseRandomMode(randomMode); + int seed = 0; + + const bool isPlanar = layout == "NCHW"; + const bool isFakePlanar = layout == "NCHW_FAKE"; + if (const char *reason = GetEraseSkipReason(layout, inputKind, regionMode, ch)) + { + state.skip(reason); + return; + } + + const long imageBytes = static_cast(shape.x) * shape.y * shape.z * sizeof(T); + const int regionHeight = std::max(shape.y / 4, 1); + const int regionWidth = std::max(shape.z / 4, 1); + const long paramBytes + = regionMode ? static_cast(ch) * regionHeight * regionWidth * sizeof(float) + : static_cast(numErase) * (sizeof(int2) + sizeof(int3) + ch * sizeof(float) + sizeof(int)); + if (isFakePlanar) + { + state.add_global_memory_reads(3 * imageBytes + paramBytes); + state.add_global_memory_writes(3 * imageBytes); + } + else + { + state.add_global_memory_reads(imageBytes + paramBytes); + state.add_global_memory_writes(imageBytes); + } + + cvcuda::Erase op(numErase); + + // clang-format off + + nvcv::Tensor anchor({{numErase}, "N"}, nvcv::TYPE_2S32); + nvcv::Tensor erasing({{numErase}, "N"}, nvcv::TYPE_3S32); + nvcv::Tensor values({{numErase * ch}, "N"}, nvcv::TYPE_F32); + nvcv::Tensor imgIdx({{numErase}, "N"}, nvcv::TYPE_S32); + nvcv::Tensor regionValues({{ch, regionHeight, regionWidth}, "CHW"}, nvcv::TYPE_F32); + + int eraseMask = (1 << ch) - 1; + + benchutils::FillTensor(anchor, [](const long4_16a &){ return int2{0, 0}; }); + benchutils::FillTensor(erasing, [&eraseMask](const long4_16a &){ return int3{10, 10, eraseMask}; }); + benchutils::FillTensor(values, [](const long4_16a &){ return 1.f; }); + benchutils::FillTensor(imgIdx, [](const long4_16a &){ return 0; }); + benchutils::FillTensor(regionValues, [](const long4_16a &){ return 1.f; }); + + if (isFakePlanar) // tensor-only: planar→interleaved→erase→interleaved→planar + { + nvcv::Tensor src ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + nvcv::Tensor interSrc({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor interDst({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + cvcuda::Reformat reformatOp; + + benchutils::warmup_and_exec(state, BENCH_ERASE_WARMUP_ITERATIONS, + [&op, &reformatOp, &src, &interSrc, &interDst, &dst, &anchor, &erasing, &values, &imgIdx, ®ionValues, regionMode, regionHeight, regionWidth, &random, &seed](cudaStream_t s) { + reformatOp(s, src, interSrc); // NCHW → NHWC + if (regionMode) + op(s, interSrc, interDst, 0, 0, regionHeight, regionWidth, regionValues); + else + op(s, interSrc, interDst, anchor, erasing, values, imgIdx, random, seed); + reformatOp(s, interDst, dst); // NHWC → NCHW + }); + } + else if (inputKind == benchutils::InputKind::Tensor) + { + nvcv::Tensor src = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + benchutils::warmup_and_exec(state, BENCH_ERASE_WARMUP_ITERATIONS, + [&op, &src, &dst, &anchor, &erasing, &values, &imgIdx, ®ionValues, regionMode, regionHeight, regionWidth, &random, &seed](cudaStream_t s) { + if (regionMode) + op(s, src, dst, 0, 0, regionHeight, regionWidth, regionValues); + else + op(s, src, dst, anchor, erasing, values, imgIdx, random, seed); + }); + } + else // zero and positive var shape means use ImageBatchVarShape + { + nvcv::ImageBatchVarShape src(shape.x); + nvcv::ImageBatchVarShape dst(shape.x); + + if (isPlanar) + { + benchutils::FillPlanarImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}); + benchutils::FillPlanarImageBatchLike(dst, src); + } + else + { + benchutils::FillImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}, + benchutils::CheckerboardValues()); + // Use FillImageBatchLike to ensure dst has same per-sample shapes as src + benchutils::FillImageBatchLike(dst, src, [](const long4_16a &) { return T{0}; }); + } + + benchutils::warmup_and_exec(state, BENCH_ERASE_WARMUP_ITERATIONS, + [&op, &src, &dst, &anchor, &erasing, &values, &imgIdx, &random, &seed](cudaStream_t s) { + op(s, src, dst, anchor, erasing, values, imgIdx, random, seed); + }); + } +} +CVCUDA_BENCH_SKIP_ERRORS(state) + +// clang-format on + +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(erase, NVBENCH_TYPE_AXES(BENCH_ERASE_TYPES)) +BENCH_ERASE_AXES; diff --git a/bench/cpp/ops/BenchFindHomography.cpp b/bench/cpp/ops/BenchFindHomography.cpp new file mode 100644 index 000000000..c2305b083 --- /dev/null +++ b/bench/cpp/ops/BenchFindHomography.cpp @@ -0,0 +1,164 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchFindHomographyConfig.hpp" + +#include + +#include + +template +void fill_src_grid_replicated(std::vector &vec, std::size_t numSamples, std::size_t numPoints) +{ + std::size_t gridSide = 1; + while (gridSide * gridSide < numPoints) + { + ++gridSide; + } + + const T gridScale = static_cast(2) / static_cast(gridSide - 1); + const std::size_t sampleSize = numPoints * 2; + for (std::size_t j = 0; j < numPoints; j++) + { + // A two-dimensional grid makes the four-point workload the corners of a square. + T x = static_cast(-1) + static_cast(j % gridSide) * gridScale; + T y = static_cast(-1) + static_cast(j / gridSide) * gridScale; + std::size_t baseIdx = j * 2; + + for (std::size_t i = 0; i < numSamples; i++) + { + vec[i * sampleSize + baseIdx] = x; + vec[i * sampleSize + baseIdx + 1] = y; + } + } +} + +template +void fill_dst_projective_replicated(const std::vector &srcVec, const std::vector &transform, + std::vector &dstVec, std::size_t numSamples, std::size_t numPoints) +{ + const T *model = transform.data(); + std::size_t sampleSize = numPoints * 2; + + for (std::size_t j = 0; j < numPoints; j++) + { + std::size_t baseIdx = j * 2; + T x = srcVec[baseIdx]; + T y = srcVec[baseIdx + 1]; + + // For x,y in [-1,1], this fixed transform keeps w in [0.965,1.035]. + T w = model[6] * x + model[7] * y + model[8]; + T x_transformed = (model[0] * x + model[1] * y + model[2]) / w; + T y_transformed = (model[3] * x + model[4] * y + model[5]) / w; + + for (std::size_t i = 0; i < numSamples; i++) + { + dstVec[i * sampleSize + baseIdx] = x_transformed; + dstVec[i * sampleSize + baseIdx + 1] = y_transformed; + } + } +} + +template +void fill_tensor(nvcv::Tensor &tensor, const std::vector &vec) +{ + auto tensorData = tensor.exportData(); + CVCUDA_CHECK_DATA(tensorData); + + long3 strides{tensorData->stride(0), tensorData->stride(1)}; + long3 shape{tensorData->shape(0), tensorData->shape(1)}; + long bufSize{nvcv::cuda::GetElement(strides, 0) * nvcv::cuda::GetElement(shape, 0)}; + CVCUDA_CHECK_DATA((bufSize == static_cast(vec.size() * sizeof(T)))); + + CUDA_CHECK_ERROR(cudaMemcpy(tensorData->basePtr(), vec.data(), bufSize, cudaMemcpyHostToDevice)); +} + +template +inline void findhomography(nvbench::state &state, nvbench::type_list) +try +{ + int2 shape = benchutils::GetShape<2, int2>(state.get_string("shape")); + const benchutils::InputKind inputKind = benchutils::GetInputKind(state.get_string("inputKind")); + + std::vector srcVec(2 * shape.x * shape.y); + std::vector dstVec(2 * shape.x * shape.y); + std::vector transform = {static_cast(1.05), static_cast(0.08), static_cast(0.15), + static_cast(-0.04), static_cast(0.97), static_cast(-0.10), + static_cast(0.015), static_cast(-0.020), static_cast(1.0)}; + + fill_src_grid_replicated(srcVec, shape.x, shape.y); + fill_dst_projective_replicated(srcVec, transform, dstVec, shape.x, shape.y); + + state.add_global_memory_reads(shape.x * shape.y * 4 * sizeof(T)); + state.add_global_memory_writes(shape.x * 3 * 3 * sizeof(T)); + + cvcuda::FindHomography op(shape.x, shape.y); + + if (inputKind == benchutils::InputKind::Tensor) + { + // clang-format off + nvcv::Tensor src({{shape.x, shape.y}, "NW"}, nvcv::TYPE_2F32); + nvcv::Tensor dst({{shape.x, shape.y}, "NW"}, nvcv::TYPE_2F32); + nvcv::Tensor models({{shape.x, 3, 3}, "NHW"}, benchutils::GetDataType()); + // clang-format on + + fill_tensor(src, srcVec); + fill_tensor(dst, dstVec); + + benchutils::warmup_and_exec(state, BENCH_FINDHOMOGRAPHY_WARMUP_ITERATIONS, + [&op, &src, &dst, &models](cudaStream_t s) { op(s, src, dst, models); }); + } + else + { + // FindHomography's variable-shape API uses TensorBatch rather than ImageBatchVarShape. + nvcv::TensorBatch src(shape.x); + nvcv::TensorBatch dst(shape.x); + nvcv::TensorBatch models(shape.x); + + const std::size_t sampleElements = 2 * static_cast(shape.y); + const std::vector srcSample(srcVec.begin(), srcVec.begin() + sampleElements); + const std::vector dstSample(dstVec.begin(), dstVec.begin() + sampleElements); + + for (int i = 0; i < shape.x; ++i) + { + // clang-format off + nvcv::Tensor srcTensor({{1, shape.y}, "NW"}, nvcv::TYPE_2F32); + nvcv::Tensor dstTensor({{1, shape.y}, "NW"}, nvcv::TYPE_2F32); + nvcv::Tensor modelTensor({{1, 3, 3}, "NHW"}, benchutils::GetDataType()); + // clang-format on + + fill_tensor(srcTensor, srcSample); + fill_tensor(dstTensor, dstSample); + src.pushBack(srcTensor); + dst.pushBack(dstTensor); + models.pushBack(modelTensor); + } + + benchutils::warmup_and_exec(state, BENCH_FINDHOMOGRAPHY_WARMUP_ITERATIONS, + [&op, &src, &dst, &models](cudaStream_t s) { op(s, src, dst, models); }); + } +} + +CVCUDA_BENCH_SKIP_ERRORS(state) + +// clang-format on + +using FindHomographyTypes = nvbench::type_list; + +NVBENCH_BENCH_TYPES(findhomography, NVBENCH_TYPE_AXES(FindHomographyTypes)) +BENCH_FINDHOMOGRAPHY_AXES; diff --git a/bench/cpp/ops/BenchFlip.cpp b/bench/cpp/ops/BenchFlip.cpp new file mode 100644 index 000000000..08a5637e8 --- /dev/null +++ b/bench/cpp/ops/BenchFlip.cpp @@ -0,0 +1,156 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchFlipConfig.hpp" + +#include +#include + +#include + +template +inline void flip(nvbench::state &state, nvbench::type_list) +try +{ + int3 shape = benchutils::GetShape<3, int3>(state.get_string("shape")); + auto layout = benchutils::GetStringParam(state, "layout", "NHWC"); + const benchutils::InputKind inputKind = benchutils::GetInputKind(state.get_string("inputKind")); + int flipCode; + + if (state.get_string("flipType") == "HORIZONTAL") + { + flipCode = 0; + } + else if (state.get_string("flipType") == "VERTICAL") + { + flipCode = 1; + } + else if (state.get_string("flipType") == "BOTH") + { + flipCode = -1; + } + else + { + throw std::invalid_argument("Invalid flipType = " + state.get_string("flipType")); + } + + const bool isPlanar = layout == "NCHW"; + const bool isFakePlanar = layout == "NCHW_FAKE"; + if (layout != "NHWC" && layout != "NCHW" && layout != "NCHW_FAKE") + { + state.skip("Flip benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts"); + return; + } + // NCHW_FAKE ("fake planar") is a tensor-only comparison path: planar data is reformatted to + // interleaved, flipped with the interleaved kernel, and reformatted back to planar — all timed + // together — so the native planar path (NCHW) can be shown to be faster than this naive + // convert→flip→convert pipeline. + if (isFakePlanar && inputKind == benchutils::InputKind::VarShape) + { + state.skip("Fake-planar (NCHW_FAKE) flip benchmark is tensor-only"); + return; + } + + using BT = typename nvcv::cuda::BaseType; + + int ch = nvcv::cuda::NumElements; + + // Flip preserves size, so src and dst hold the same number of bytes. + const long bytes = static_cast(shape.x) * shape.y * shape.z * sizeof(T); + if (isFakePlanar) + { + // reformat(NCHW→NHWC) + flip + reformat(NHWC→NCHW): reads src + interleaved src + dst, + // writes interleaved src + interleaved dst + dst. + state.add_global_memory_reads(3 * bytes); + state.add_global_memory_writes(3 * bytes); + } + else + { + state.add_global_memory_reads(bytes); + state.add_global_memory_writes(bytes); + } + + cvcuda::Flip op; + + // clang-format off + if (isFakePlanar) // tensor-only: planar→interleaved→flip→interleaved→planar + { + nvcv::Tensor src ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + nvcv::Tensor interSrc({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor interDst({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + cvcuda::Reformat reformatOp; + + benchutils::warmup_and_exec(state, BENCH_FLIP_WARMUP_ITERATIONS, + [&op, &reformatOp, &src, &interSrc, &interDst, &dst, &flipCode](cudaStream_t s) { + reformatOp(s, src, interSrc); // NCHW → NHWC + op(s, interSrc, interDst, flipCode); // interleaved flip + reformatOp(s, interDst, dst); // NHWC → NCHW + }); + } + else if (inputKind == benchutils::InputKind::Tensor) + { + nvcv::Tensor src = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + benchutils::warmup_and_exec(state, BENCH_FLIP_WARMUP_ITERATIONS, + [&op, &src, &dst, &flipCode](cudaStream_t s) { op(s, src, dst, flipCode); }); + } + else // zero and positive var shape means use ImageBatchVarShape + { + nvcv::ImageBatchVarShape src(shape.x); + nvcv::ImageBatchVarShape dst(shape.x); + + if (isPlanar) + { + benchutils::FillPlanarImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}); + benchutils::FillPlanarImageBatch(dst, long2{shape.z, shape.y}, long2{0, 0}); + } + else + { + benchutils::FillImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}, + benchutils::CheckerboardValues()); + // Use FillImageBatchLike to ensure dst has same per-sample shapes as src + benchutils::FillImageBatchLike(dst, src, [](const long4_16a &) { return T{0}; }); + } + + nvcv::Tensor flipCodeTensor({{shape.x}, "N"}, nvcv::TYPE_S32); + + benchutils::FillTensor(flipCodeTensor, [&flipCode](const long4_16a &){ return flipCode; }); + + benchutils::warmup_and_exec(state, BENCH_FLIP_WARMUP_ITERATIONS, + [&op, &src, &dst, &flipCodeTensor](cudaStream_t s) { op(s, src, dst, flipCodeTensor); }); + } +} +CVCUDA_BENCH_SKIP_ERRORS(state) + +// clang-format on + +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(flip, NVBENCH_TYPE_AXES(BENCH_FLIP_TYPES)) +BENCH_FLIP_AXES; diff --git a/bench/cpp/ops/BenchGammaContrast.cpp b/bench/cpp/ops/BenchGammaContrast.cpp new file mode 100644 index 000000000..68529dc97 --- /dev/null +++ b/bench/cpp/ops/BenchGammaContrast.cpp @@ -0,0 +1,159 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchGammaContrastConfig.hpp" + +#include +#include + +#include + +#include + +template +inline void gammacontrast(nvbench::state &state, nvbench::type_list) +try +{ + int3 shape = benchutils::GetShape<3, int3>(state.get_string("shape")); + auto layout = benchutils::GetStringParam(state, "layout", "NHWC"); + const auto inputKindStr = state.get_string("inputKind"); + + using BT = typename nvcv::cuda::BaseType; + + int ch = nvcv::cuda::NumElements; + + const bool isPlanar = layout == "NCHW"; + const bool isFakePlanar = layout == "NCHW_FAKE"; + // ScalarGamma benches the host-scalar gamma/gain overload: same dense-tensor input as Tensor, + // but gamma/gain are kernel launch arguments -- no device gamma tensor is staged or read. + const bool isScalarGamma = inputKindStr == "ScalarGamma"; + const bool isVarShape = inputKindStr == "VarShape"; + if (inputKindStr != "Tensor" && !isVarShape && !isScalarGamma) + { + state.skip("GammaContrast benchmark supports only Tensor, VarShape, and ScalarGamma input kinds"); + return; + } + if (layout != "NHWC" && layout != "NCHW" && layout != "NCHW_FAKE") + { + state.skip("GammaContrast benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts"); + return; + } + if (isFakePlanar && inputKindStr != "Tensor") + { + state.skip("Fake-planar (NCHW_FAKE) GammaContrast benchmark is tensor-only"); + return; + } + if (isPlanar && isVarShape && std::is_same_v && ch == 4) + { + state.skip("RGBA8p varshape is unsupported by the Python image API"); + return; + } + + const long imageBytes = static_cast(shape.x) * shape.y * shape.z * sizeof(T); + const long gammaBytes = isScalarGamma ? 0 : static_cast(shape.x) * ch * sizeof(float); + if (isFakePlanar) + { + state.add_global_memory_reads(3 * imageBytes + gammaBytes); + state.add_global_memory_writes(3 * imageBytes); + } + else + { + state.add_global_memory_reads(imageBytes + gammaBytes); + state.add_global_memory_writes(imageBytes); + } + + cvcuda::GammaContrast op(shape.x, ch); + + // clang-format off + + nvcv::Tensor gamma({{shape.x * ch}, "N"}, nvcv::TYPE_F32); + + // gamma: deterministic constant 0.75 (Python bench mirrors this). The ScalarGamma rows pass the + // same 0.75 by value with gain 1.0 -- bit-exact with the Tensor rows -- and never read this tensor. + benchutils::FillTensor(gamma, [](const long4_16a &) { return 0.75f; }); + + if (isFakePlanar) // tensor-only: planar->interleaved->gammacontrast->interleaved->planar + { + nvcv::Tensor src ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + nvcv::Tensor interSrc({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor interDst({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::LcgValues()); + + cvcuda::Reformat reformatOp; + + benchutils::warmup_and_exec(state, BENCH_GAMMACONTRAST_WARMUP_ITERATIONS, + [&op, &reformatOp, &src, &interSrc, &interDst, &dst, &gamma](cudaStream_t s) { + reformatOp(s, src, interSrc); // NCHW -> NHWC + op(s, interSrc, interDst, gamma); + reformatOp(s, interDst, dst); // NHWC -> NCHW + }); + } + else if (inputKindStr == "Tensor" || isScalarGamma) + { + nvcv::Tensor src = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::LcgValues()); + + if (isScalarGamma) + { + benchutils::warmup_and_exec(state, BENCH_GAMMACONTRAST_WARMUP_ITERATIONS, + [&op, &src, &dst](cudaStream_t s) { op(s, src, dst, 0.75f, 1.0f); }); + } + else + { + benchutils::warmup_and_exec(state, BENCH_GAMMACONTRAST_WARMUP_ITERATIONS, + [&op, &src, &dst, &gamma](cudaStream_t s) { op(s, src, dst, gamma); }); + } + } + else // zero and positive var shape means use ImageBatchVarShape + { + nvcv::ImageBatchVarShape src(shape.x); + nvcv::ImageBatchVarShape dst(shape.x); + + if (isPlanar) + { + benchutils::FillPlanarImageBatchLcg(src, long2{shape.z, shape.y}, long2{0, 0}); + benchutils::FillPlanarImageBatchLike(dst, src); + } + else + { + benchutils::FillImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}, + benchutils::LcgValues()); + // Use FillImageBatchLike to ensure dst has same per-sample shapes as src + benchutils::FillImageBatchLike(dst, src, [](const long4_16a &) { return T{0}; }); + } + + benchutils::warmup_and_exec(state, BENCH_GAMMACONTRAST_WARMUP_ITERATIONS, + [&op, &src, &dst, &gamma](cudaStream_t s) { op(s, src, dst, gamma); }); + } +} +CVCUDA_BENCH_SKIP_ERRORS(state) + +// clang-format on + +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(gammacontrast, NVBENCH_TYPE_AXES(BENCH_GAMMACONTRAST_TYPES)) +BENCH_GAMMACONTRAST_AXES; diff --git a/bench/cpp/ops/BenchGaussian.cpp b/bench/cpp/ops/BenchGaussian.cpp new file mode 100644 index 000000000..4ed46bd1d --- /dev/null +++ b/bench/cpp/ops/BenchGaussian.cpp @@ -0,0 +1,233 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchGaussianConfig.hpp" + +#include +#include + +#include + +#include + +namespace { + +enum class GaussianLayout +{ + NHWC, + NCHW, + NCHWFake +}; + +GaussianLayout GetGaussianLayout(std::string_view layout, benchutils::InputKind inputKind) +{ + if (layout == "NHWC") + { + return GaussianLayout::NHWC; + } + if (layout == "NCHW") + { + return GaussianLayout::NCHW; + } + if (layout == "NCHW_FAKE") + { + if (inputKind == benchutils::InputKind::VarShape) + { + throw std::invalid_argument("Fake-planar (NCHW_FAKE) Gaussian benchmark is tensor-only"); + } + return GaussianLayout::NCHWFake; + } + + throw std::invalid_argument("Gaussian benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts"); +} + +bool IsPlanar(GaussianLayout layout) +{ + return layout == GaussianLayout::NCHW; +} + +bool IsFakePlanar(GaussianLayout layout) +{ + return layout == GaussianLayout::NCHWFake; +} + +void AddGaussianMemoryTraffic(nvbench::state &state, long bytes, GaussianLayout layout) +{ + if (IsFakePlanar(layout)) + { + state.add_global_memory_reads(3 * bytes); + state.add_global_memory_writes(3 * bytes); + } + else + { + state.add_global_memory_reads(bytes); + state.add_global_memory_writes(bytes); + } +} + +template +void ValidatePlanarChannels(GaussianLayout layout) +{ + if (!IsPlanar(layout)) + { + return; + } + + constexpr int ch = nvcv::cuda::NumElements; + if constexpr (ch == 2) + { + throw std::invalid_argument("Gaussian planar benchmark does not support 2-channel formats"); + } + if constexpr (ch == 1) + { + throw std::invalid_argument("Gaussian planar benchmark requires multi-channel planar formats"); + } +} + +template +nvcv::Tensor MakeGaussianTensor(int3 shape, GaussianLayout layout) +{ + using BT = typename nvcv::cuda::BaseType; + constexpr int ch = nvcv::cuda::NumElements; + + if (IsPlanar(layout)) + { + return nvcv::Tensor( + nvcv::TensorShape{ + {shape.x, ch, shape.y, shape.z}, + "NCHW" + }, + benchutils::GetDataType()); + } + + return nvcv::Tensor( + nvcv::TensorShape{ + {shape.x, shape.y, shape.z, ch}, + "NHWC" + }, + benchutils::GetDataType()); +} + +template +void FillGaussianImageBatches(nvcv::ImageBatchVarShape &src, nvcv::ImageBatchVarShape &dst, int3 shape, + GaussianLayout layout) +{ + if (IsPlanar(layout)) + { + benchutils::FillPlanarImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}); + benchutils::FillPlanarImageBatchLike(dst, src); + } + else + { + benchutils::FillImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}, benchutils::CheckerboardValues()); + benchutils::FillImageBatchLike(dst, src, [](const long4_16a &) { return T{0}; }); + } +} + +} // namespace + +template +inline void gaussian(nvbench::state &state, nvbench::type_list) +try +{ + int3 shape = benchutils::GetShape<3, int3>(state.get_string("shape")); + auto layoutStr = benchutils::GetStringParam(state, "layout", "NHWC"); + const benchutils::InputKind inputKind = benchutils::GetInputKind(state.get_string("inputKind")); + double sigma = state.get_float64("sigma"); + + NVCVBorderType borderType = benchutils::GetBorderType(state.get_string("border")); + + GaussianLayout layout = GetGaussianLayout(layoutStr, inputKind); + + int kernelSize = (int)std::round(sigma * (std::is_same_v, uint8_t> ? 3 : 4) * 2 + 1) | 1; + int2 ksize2{kernelSize, kernelSize}; + + nvcv::Size2D kernelSize2{kernelSize, kernelSize}; + double2 sigma2{sigma, sigma}; + + const long bytes = static_cast(shape.x) * shape.y * shape.z * sizeof(T); + AddGaussianMemoryTraffic(state, bytes, layout); + + cvcuda::Gaussian op(kernelSize2, shape.x); + + // clang-format off + + if (IsFakePlanar(layout)) // tensor-only: planar->interleaved->Gaussian->interleaved->planar + { + using BT = typename nvcv::cuda::BaseType; + int ch = nvcv::cuda::NumElements; + + nvcv::Tensor src ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + nvcv::Tensor interSrc({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor interDst({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + cvcuda::Reformat reformatOp; + + benchutils::warmup_and_exec(state, BENCH_GAUSSIAN_WARMUP_ITERATIONS, + [&op, &reformatOp, &src, &interSrc, &interDst, &dst, &kernelSize2, &sigma2, &borderType](cudaStream_t s) { + reformatOp(s, src, interSrc); // NCHW -> NHWC + op(s, interSrc, interDst, kernelSize2, sigma2, borderType); // interleaved Gaussian + reformatOp(s, interDst, dst); // NHWC -> NCHW + }); + } + else if (inputKind == benchutils::InputKind::Tensor) + { + using BT = typename nvcv::cuda::BaseType; + + ValidatePlanarChannels(layout); + nvcv::Tensor src = MakeGaussianTensor(shape, layout); + nvcv::Tensor dst = MakeGaussianTensor(shape, layout); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + benchutils::warmup_and_exec(state, BENCH_GAUSSIAN_WARMUP_ITERATIONS, + [&op, &src, &dst, &kernelSize2, &sigma2, &borderType](cudaStream_t s) { + op(s, src, dst, kernelSize2, sigma2, borderType); + }); + } + else // zero and positive var shape means use ImageBatchVarShape + { + ValidatePlanarChannels(layout); + + nvcv::ImageBatchVarShape src(shape.x); + nvcv::ImageBatchVarShape dst(shape.x); + FillGaussianImageBatches(src, dst, shape, layout); + + nvcv::Tensor kernelSizeTensor({{shape.x}, "N"}, nvcv::TYPE_2S32); + nvcv::Tensor sigmaTensor({{shape.x}, "N"}, nvcv::TYPE_2F64); + + benchutils::FillTensor(kernelSizeTensor, [&ksize2](const long4_16a &){ return ksize2; }); + benchutils::FillTensor(sigmaTensor, [&sigma2](const long4_16a &){ return sigma2; }); + + benchutils::warmup_and_exec(state, BENCH_GAUSSIAN_WARMUP_ITERATIONS, + [&op, &src, &dst, &kernelSizeTensor, &sigmaTensor, &borderType](cudaStream_t s) { + op(s, src, dst, kernelSizeTensor, sigmaTensor, borderType); + }); + } +} +CVCUDA_BENCH_SKIP_ERRORS(state) + +// clang-format on + +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(gaussian, NVBENCH_TYPE_AXES(BENCH_GAUSSIAN_TYPES)) +BENCH_GAUSSIAN_AXES; diff --git a/bench/cpp/ops/BenchGaussianNoise.cpp b/bench/cpp/ops/BenchGaussianNoise.cpp new file mode 100644 index 000000000..527d90507 --- /dev/null +++ b/bench/cpp/ops/BenchGaussianNoise.cpp @@ -0,0 +1,132 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchGaussianNoiseConfig.hpp" + +#include +#include + +#include + +template +inline void gaussiannoise(nvbench::state &state, nvbench::type_list) +try +{ + int3 shape = benchutils::GetShape<3, int3>(state.get_string("shape")); + auto layout = benchutils::GetStringParam(state, "layout", "NHWC"); + const benchutils::InputKind inputKind = benchutils::GetInputKind(state.get_string("inputKind")); + using BT = typename nvcv::cuda::BaseType; + int ch = nvcv::cuda::NumElements; + + bool perCh = ch > 1; + bool isPlanar = layout == "NCHW"; + bool isFakePlanar = layout == "NCHW_FAKE"; + + if (layout != "NHWC" && layout != "NCHW" && layout != "NCHW_FAKE") + { + state.skip("GaussianNoise benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts"); + return; + } + if (isFakePlanar && inputKind == benchutils::InputKind::VarShape) + { + state.skip("Fake-planar (NCHW_FAKE) GaussianNoise benchmark is tensor-only"); + return; + } + + unsigned long long int seed = 12345; + + const int64_t bytes = static_cast(shape.x) * shape.y * shape.z * sizeof(T); + state.add_global_memory_reads((isFakePlanar ? 3 : 1) * bytes); + state.add_global_memory_writes((isFakePlanar ? 3 : 1) * bytes); + + cvcuda::GaussianNoise op(shape.x); + + // clang-format off + + nvcv::Tensor mu({{shape.x}, "N"}, nvcv::TYPE_F32); + nvcv::Tensor sigma({{shape.x}, "N"}, nvcv::TYPE_F32); + + benchutils::FillTensor(mu, benchutils::LcgValues(.0f, 1.f)); + benchutils::FillTensor(sigma, benchutils::LcgValues(.05f, .1f)); + + if (isFakePlanar) + { + nvcv::Tensor src ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + nvcv::Tensor interSrc({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor interDst({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::LcgValues()); + + cvcuda::Reformat reformatOp; + + benchutils::warmup_and_exec(state, BENCH_GAUSSIANNOISE_WARMUP_ITERATIONS, + [&op, &reformatOp, &src, &interSrc, &interDst, &dst, &mu, &sigma, &perCh, &seed](cudaStream_t s) { + reformatOp(s, src, interSrc); + op(s, interSrc, interDst, mu, sigma, perCh, seed); + reformatOp(s, interDst, dst); + }); + } + else if (inputKind == benchutils::InputKind::Tensor) + { + nvcv::Tensor src = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::LcgValues()); + + benchutils::warmup_and_exec(state, BENCH_GAUSSIANNOISE_WARMUP_ITERATIONS, + [&op, &src, &dst, &mu, &sigma, &perCh, &seed](cudaStream_t s) { + op(s, src, dst, mu, sigma, perCh, seed); + }); + } + else // zero and positive var shape means use ImageBatchVarShape + { + nvcv::ImageBatchVarShape src(shape.x); + nvcv::ImageBatchVarShape dst(shape.x); + + if (isPlanar) + { + benchutils::FillPlanarImageBatchLcg(src, long2{shape.z, shape.y}, long2{0, 0}); + benchutils::FillPlanarImageBatchLike(dst, src); + } + else + { + benchutils::FillImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}, + benchutils::LcgValues()); + // Use FillImageBatchLike to ensure dst has same per-sample shapes as src + benchutils::FillImageBatchLike(dst, src, [](const long4_16a &) { return T{0}; }); + } + + benchutils::warmup_and_exec(state, BENCH_GAUSSIANNOISE_WARMUP_ITERATIONS, + [&op, &src, &dst, &mu, &sigma, &perCh, &seed](cudaStream_t s) { + op(s, src, dst, mu, sigma, perCh, seed); + }); + } +} +CVCUDA_BENCH_SKIP_ERRORS(state) + +// clang-format on + +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(gaussiannoise, NVBENCH_TYPE_AXES(BENCH_GAUSSIANNOISE_TYPES)) +BENCH_GAUSSIANNOISE_AXES; diff --git a/bench/cpp/ops/BenchHQResize.cpp b/bench/cpp/ops/BenchHQResize.cpp new file mode 100644 index 000000000..c22bf260c --- /dev/null +++ b/bench/cpp/ops/BenchHQResize.cpp @@ -0,0 +1,302 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchHQResizeConfig.hpp" + +#include +#include + +#include + +struct HQResizeBenchParams +{ + long3 srcShape; + long3 dstShape; + nvcv::Size2D srcSize; + nvcv::Size2D dstSize; + nvcv::DataType dtype; + NVCVInterpolationType interpolation; + bool antialias; + bool planar; + int channels; +}; + +inline cvcuda::UniqueWorkspace AllocateHQResizeTensorWorkspace(cvcuda::HQResize &op, const HQResizeBenchParams ¶ms) +{ + HQResizeTensorShapeI inShapeDesc{ + {params.srcSize.h, params.srcSize.w}, + 2, + params.channels + }; + HQResizeTensorShapeI outShapeDesc{ + {params.dstSize.h, params.dstSize.w}, + 2, + params.channels + }; + return cvcuda::AllocateWorkspace(op.getWorkspaceRequirements(static_cast(params.srcShape.x), inShapeDesc, + outShapeDesc, params.interpolation, + params.interpolation, params.antialias)); +} + +inline cvcuda::UniqueWorkspace AllocateHQResizeBatchWorkspace(cvcuda::HQResize &op, const HQResizeBenchParams ¶ms) +{ + // ImageBatch and TensorBatch planar inputs are expanded internally to + // one single-channel sample per plane. Size the workspace for those + // N*C samples, matching the operator's planar parity tests. + const int wsChannels = params.planar ? 1 : params.channels; + const int repetitions = params.planar ? params.channels : 1; + const int wsSamples = static_cast(params.srcShape.x) * repetitions; + std::vector inShapes; + std::vector outShapes; + inShapes.reserve(wsSamples); + outShapes.reserve(wsSamples); + for (int i = 0; i < wsSamples; ++i) + { + inShapes.push_back(HQResizeTensorShapeI{ + {params.srcSize.h, params.srcSize.w}, + 2, + wsChannels + }); + outShapes.push_back(HQResizeTensorShapeI{ + {params.dstSize.h, params.dstSize.w}, + 2, + wsChannels + }); + } + HQResizeTensorShapesI inShapeDesc{inShapes.data(), wsSamples, 2, wsChannels}; + HQResizeTensorShapesI outShapeDesc{outShapes.data(), wsSamples, 2, wsChannels}; + return cvcuda::AllocateWorkspace(op.getWorkspaceRequirements( + wsSamples, inShapeDesc, outShapeDesc, params.interpolation, params.interpolation, params.antialias)); +} + +template +inline void RunFakePlanarHQResizeBench(nvbench::state &state, cvcuda::HQResize &op, const HQResizeBenchParams ¶ms) +{ + cvcuda::UniqueWorkspace ws = AllocateHQResizeTensorWorkspace(op, params); + + // clang-format off + nvcv::Tensor src ({{params.srcShape.x, params.channels, params.srcShape.y, params.srcShape.z}, "NCHW"}, params.dtype); + nvcv::Tensor interSrc({{params.srcShape.x, params.srcShape.y, params.srcShape.z, params.channels}, "NHWC"}, params.dtype); + nvcv::Tensor interDst({{params.dstShape.x, params.dstShape.y, params.dstShape.z, params.channels}, "NHWC"}, params.dtype); + nvcv::Tensor dst ({{params.dstShape.x, params.channels, params.dstShape.y, params.dstShape.z}, "NCHW"}, params.dtype); + // clang-format on + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + cvcuda::Reformat reformatOp; + benchutils::warmup_and_exec(state, BENCH_HQRESIZE_WARMUP_ITERATIONS, + [&op, &reformatOp, &ws, &src, &interSrc, &interDst, &dst, + interpolation = params.interpolation, antialias = params.antialias](cudaStream_t s) + { + reformatOp(s, src, interSrc); // NCHW->NHWC + op(s, ws.get(), interSrc, interDst, interpolation, interpolation, antialias); + reformatOp(s, interDst, dst); // NHWC->NCHW + }); +} + +template +inline void RunTensorHQResizeBench(nvbench::state &state, cvcuda::HQResize &op, const HQResizeBenchParams ¶ms) +{ + cvcuda::UniqueWorkspace ws = AllocateHQResizeTensorWorkspace(op, params); + + // clang-format off + nvcv::Tensor src = params.planar ? nvcv::Tensor({{params.srcShape.x, params.channels, params.srcShape.y, params.srcShape.z}, "NCHW"}, params.dtype) + : nvcv::Tensor({{params.srcShape.x, params.srcShape.y, params.srcShape.z, params.channels}, "NHWC"}, params.dtype); + nvcv::Tensor dst = params.planar ? nvcv::Tensor({{params.dstShape.x, params.channels, params.dstShape.y, params.dstShape.z}, "NCHW"}, params.dtype) + : nvcv::Tensor({{params.dstShape.x, params.dstShape.y, params.dstShape.z, params.channels}, "NHWC"}, params.dtype); + // clang-format on + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + benchutils::warmup_and_exec( + state, BENCH_HQRESIZE_WARMUP_ITERATIONS, + [&op, &ws, &src, &dst, interpolation = params.interpolation, antialias = params.antialias](cudaStream_t s) + { op(s, ws.get(), src, dst, interpolation, interpolation, antialias); }); +} + +template +inline void FillHQResizeImageBatches(nvcv::ImageBatchVarShape &src, nvcv::ImageBatchVarShape &dst, + const HQResizeBenchParams ¶ms) +{ + if (params.planar) + { + benchutils::FillPlanarImageBatch(src, long2{params.srcShape.z, params.srcShape.y}, long2{0, 0}, true); + benchutils::FillPlanarImageBatch(dst, long2{params.dstShape.z, params.dstShape.y}, long2{0, 0}, false); + return; + } + + benchutils::FillImageBatch(src, long2{params.srcShape.z, params.srcShape.y}, long2{0, 0}, + benchutils::CheckerboardValues()); + benchutils::FillImageBatch(dst, long2{params.dstShape.z, params.dstShape.y}, long2{0, 0}, + [](const long4_16a &) { return T{0}; }); +} + +template +inline void RunVarShapeHQResizeBench(nvbench::state &state, cvcuda::HQResize &op, const HQResizeBenchParams ¶ms) +{ + if (params.channels != 3) + { + state.skip("HQResize ImageBatchVarShape benchmark currently requires three channels"); + return; + } + + cvcuda::UniqueWorkspace ws = AllocateHQResizeBatchWorkspace(op, params); + + nvcv::ImageBatchVarShape src(static_cast(params.srcShape.x)); + nvcv::ImageBatchVarShape dst(static_cast(params.dstShape.x)); + + if constexpr (std::is_same_v) + { + FillHQResizeImageBatches(src, dst, params); + } + else if constexpr (std::is_same_v) + { + FillHQResizeImageBatches(src, dst, params); + } + else + { + state.skip("Unsupported HQResize ImageBatchVarShape benchmark data type"); + return; + } + + benchutils::warmup_and_exec( + state, BENCH_HQRESIZE_WARMUP_ITERATIONS, + [&op, &ws, &src, &dst, interpolation = params.interpolation, antialias = params.antialias](cudaStream_t s) + { op(s, ws.get(), src, dst, interpolation, interpolation, antialias); }); +} + +template +inline void RunTensorBatchHQResizeBench(nvbench::state &state, cvcuda::HQResize &op, const HQResizeBenchParams ¶ms) +{ + cvcuda::UniqueWorkspace ws = AllocateHQResizeBatchWorkspace(op, params); + + nvcv::TensorBatch srcTensors(static_cast(params.srcShape.x)); + nvcv::TensorBatch dstTensors(static_cast(params.dstShape.x)); + for (int i = 0; i < params.srcShape.x; ++i) + { + // clang-format off + nvcv::Tensor src = params.planar ? nvcv::Tensor({{params.channels, params.srcShape.y, params.srcShape.z}, "CHW"}, params.dtype) + : nvcv::Tensor({{params.srcShape.y, params.srcShape.z, params.channels}, "HWC"}, params.dtype); + nvcv::Tensor dst = params.planar ? nvcv::Tensor({{params.channels, params.dstShape.y, params.dstShape.z}, "CHW"}, params.dtype) + : nvcv::Tensor({{params.dstShape.y, params.dstShape.z, params.channels}, "HWC"}, params.dtype); + // clang-format on + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + srcTensors.pushBack(src); + dstTensors.pushBack(dst); + } + + benchutils::warmup_and_exec(state, BENCH_HQRESIZE_WARMUP_ITERATIONS, + [&op, &ws, &srcTensors, &dstTensors, interpolation = params.interpolation, + antialias = params.antialias](cudaStream_t s) + { op(s, ws.get(), srcTensors, dstTensors, interpolation, interpolation, antialias); }); +} + +template +inline void hqresize(nvbench::state &state, nvbench::type_list) +try +{ + long3 srcShape = benchutils::GetShape<3>(state.get_string("shape")); + bool antialias = benchutils::GetIntParam(state, "antialias"); + NVCVInterpolationType interpolation = benchutils::GetInterpolationType(state.get_string("interpolation")); + const std::string inputKind = state.get_string("inputKind"); + auto layout = benchutils::GetStringParam(state, "layout", "NHWC"); + if (layout != "NHWC" && layout != "NCHW" && layout != "NCHW_FAKE") + { + state.skip("HQResize benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts"); + return; + } + if (inputKind != "Tensor" && inputKind != "VarShape" && inputKind != "TensorBatch") + { + state.skip("HQResize benchmark supports only Tensor, VarShape, and TensorBatch input kinds"); + return; + } + const bool planar = layout == "NCHW"; + const bool fakePlanar = layout == "NCHW_FAKE"; + // Channel count is an explicit axis (default 1): the legacy single-channel NHWC profiles omit it, + // while the planar-comparison profiles (NHWC/NCHW/NCHW_FAKE) set numChannels=3 so the three layouts + // are compared at parity. + const auto channels = static_cast(state.get_int64_or_default("numChannels", 1)); + + // NCHW_FAKE ("fake planar") is a tensor-only comparison path: planar data is reformatted to + // interleaved, resized with the interleaved kernel, and reformatted back to planar -- all timed + // together -- so the native planar (NCHW) path can be shown faster than this convert->resize->convert + // pipeline. + if (fakePlanar && inputKind != "Tensor") + { + state.skip("Fake-planar (NCHW_FAKE) HQResize benchmark is tensor-only"); + return; + } + + const std::string resizeType = state.get_string("resizeType"); + long3 dstShape = benchutils::GetResizeOutputShape(srcShape, resizeType); + if (dstShape.y >= srcShape.y && dstShape.z >= srcShape.z && (dstShape.y > srcShape.y || dstShape.z > srcShape.z) + && antialias) + { + state.skip("Antialias is no-op for expanding"); + return; + } + + nvcv::Size2D srcSize{(int)srcShape.z, (int)srcShape.y}; + nvcv::Size2D dstSize{(int)dstShape.z, (int)dstShape.y}; + + nvcv::DataType dtype{benchutils::GetDataType()}; + + const long srcBytes = srcShape.x * srcShape.y * srcShape.z * channels * static_cast(sizeof(T)); + const long dstBytes = dstShape.x * dstShape.y * dstShape.z * channels * static_cast(sizeof(T)); + if (fakePlanar) + { + // reformat(NCHW->NHWC) + resize + reformat(NHWC->NCHW): reads src twice + dst once, + // writes the interleaved src once + dst twice. + state.add_global_memory_reads(2 * srcBytes + dstBytes); + state.add_global_memory_writes(srcBytes + 2 * dstBytes); + } + else + { + state.add_global_memory_reads(srcBytes); + state.add_global_memory_writes(dstBytes); + } + + const HQResizeBenchParams params{srcShape, dstShape, srcSize, dstSize, dtype, + interpolation, antialias, planar, channels}; + cvcuda::HQResize op; + + if (fakePlanar) // tensor-only: NCHW -> NHWC -> hqresize -> NHWC -> NCHW + { + RunFakePlanarHQResizeBench(state, op, params); + } + else if (inputKind == "Tensor") + { + RunTensorHQResizeBench(state, op, params); + } + else if (inputKind == "VarShape") + { + RunVarShapeHQResizeBench(state, op, params); + } + else // TensorBatch + { + RunTensorBatchHQResizeBench(state, op, params); + } +} + +CVCUDA_BENCH_SKIP_ERRORS(state) + +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(hqresize, NVBENCH_TYPE_AXES(BENCH_HQRESIZE_TYPES)) +BENCH_HQRESIZE_AXES; diff --git a/bench/cpp/ops/BenchHistogram.cpp b/bench/cpp/ops/BenchHistogram.cpp new file mode 100644 index 000000000..412cb9597 --- /dev/null +++ b/bench/cpp/ops/BenchHistogram.cpp @@ -0,0 +1,260 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchHistogramConfig.hpp" + +#include +#include + +#include + +#include +#include + +namespace { + +bool IsHistogramLayoutSupported(std::string_view layout) +{ + return layout == "NHWC" || layout == "NCHW" || layout == "NCHW_FAKE"; +} + +bool IsPlanarBenchmarkLayout(std::string_view layout) +{ + return layout == "NCHW" || layout == "NCHW_FAKE"; +} + +bool UseHistogramMask(std::string_view maskMode) +{ + if (maskMode == "checkerboard") + { + return true; + } + if (maskMode == "none") + { + return false; + } + throw std::invalid_argument("Unsupported maskMode: " + std::string(maskMode)); +} + +struct HistogramBenchParams +{ + int3 shape; + benchutils::InputKind inputKind; + bool useMask; + std::string layout; + bool isPlanar; + bool isFakePlanar; +}; + +inline HistogramBenchParams ParseHistogramParams(nvbench::state &state) +{ + std::string layout = benchutils::GetStringParam(state, "layout", "NHWC"); + return { + benchutils::GetShape<3, int3>(state.get_string("shape")), + benchutils::GetInputKind(state.get_string("inputKind")), + UseHistogramMask(state.get_string("maskMode")), + layout, + layout == "NCHW", + layout == "NCHW_FAKE", + }; +} + +template +void AddMemoryCounters(nvbench::state &state, int3 shape, bool useMask, bool hasPlanarAdapter) +{ + constexpr int numBins = 256; + + const long imageBytes = static_cast(shape.x) * shape.y * shape.z * sizeof(T); + const long maskBytes = static_cast(shape.x) * shape.y * shape.z * sizeof(uint8_t); + const long histBytes = static_cast(shape.x) * numBins * sizeof(int); + + if (hasPlanarAdapter) + { + state.add_global_memory_reads(2 * imageBytes + (useMask ? 2 * maskBytes : 0)); + state.add_global_memory_writes(imageBytes + (useMask ? maskBytes : 0) + histBytes); + } + else + { + state.add_global_memory_reads(imageBytes + (useMask ? maskBytes : 0)); + state.add_global_memory_writes(histBytes); + } +} + +template +bool ValidateAndAccount(nvbench::state &state, const HistogramBenchParams ¶ms) +{ + if (!IsHistogramLayoutSupported(params.layout)) + { + state.skip("Histogram benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts"); + return false; + } + if (IsPlanarBenchmarkLayout(params.layout) && params.inputKind != benchutils::InputKind::Tensor) + { + state.skip("Planar Histogram benchmark is tensor-only"); + return false; + } + + AddMemoryCounters(state, params.shape, params.useMask, params.isPlanar || params.isFakePlanar); + return true; +} + +template +void RunFakePlanarHistogram(nvbench::state &state, int3 shape, bool useMask, cvcuda::Histogram &op) +{ + constexpr int numBins = 256; + + nvcv::Tensor src( + { + {shape.x, 1, shape.y, shape.z}, + "NCHW" + }, + benchutils::GetDataType()); + nvcv::Tensor interSrc( + { + {shape.x, shape.y, shape.z, 1}, + "NHWC" + }, + benchutils::GetDataType()); + nvcv::Tensor hist( + { + {shape.x, numBins, 1}, + "HWC" + }, + nvcv::TYPE_S32); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + benchutils::FillTensor(hist, [](auto &) { return 0; }); + + cvcuda::Reformat reformatOp; + + if (useMask) + { + nvcv::Tensor mask( + { + {shape.x, 1, shape.y, shape.z}, + "NCHW" + }, + nvcv::TYPE_U8); + nvcv::Tensor interMask( + { + {shape.x, shape.y, shape.z, 1}, + "NHWC" + }, + nvcv::TYPE_U8); + + benchutils::FillTensor(mask, benchutils::CheckerboardValues()); + + benchutils::warmup_and_exec(state, BENCH_HISTOGRAM_WARMUP_ITERATIONS, + [&op, &reformatOp, &src, &interSrc, &mask, &interMask, &hist](cudaStream_t s) + { + reformatOp(s, src, interSrc); + reformatOp(s, mask, interMask); + op(s, interSrc, nvcv::OptionalTensorConstRef{interMask}, hist); + }); + return; + } + + benchutils::warmup_and_exec(state, BENCH_HISTOGRAM_WARMUP_ITERATIONS, + [&op, &reformatOp, &src, &interSrc, &hist](cudaStream_t s) + { + reformatOp(s, src, interSrc); + op(s, interSrc, nvcv::OptionalTensorConstRef{nvcv::NullOpt}, hist); + }); +} + +template +void RunTensorHistogram(nvbench::state &state, int3 shape, bool useMask, bool isPlanar, cvcuda::Histogram &op) +{ + constexpr int numBins = 256; + + nvcv::Tensor src = isPlanar ? nvcv::Tensor( + { + {shape.x, 1, shape.y, shape.z}, + "NCHW" + }, + benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, 1}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor hist( + { + {shape.x, numBins, 1}, + "HWC" + }, + nvcv::TYPE_S32); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + benchutils::FillTensor(hist, [](auto &) { return 0; }); + + if (useMask) + { + nvcv::Tensor mask = isPlanar ? nvcv::Tensor( + { + {shape.x, 1, shape.y, shape.z}, + "NCHW" + }, + nvcv::TYPE_U8) + : nvcv::Tensor({{shape.x, shape.y, shape.z, 1}, "NHWC"}, nvcv::TYPE_U8); + + benchutils::FillTensor(mask, benchutils::CheckerboardValues()); + + benchutils::warmup_and_exec(state, BENCH_HISTOGRAM_WARMUP_ITERATIONS, + [&op, &src, &mask, &hist](cudaStream_t s) + { op(s, src, nvcv::OptionalTensorConstRef{mask}, hist); }); + return; + } + + benchutils::warmup_and_exec(state, BENCH_HISTOGRAM_WARMUP_ITERATIONS, + [&op, &src, &hist](cudaStream_t s) + { op(s, src, nvcv::OptionalTensorConstRef{nvcv::NullOpt}, hist); }); +} + +template +void DispatchHistogram(nvbench::state &state, const HistogramBenchParams ¶ms, cvcuda::Histogram &op) +{ + if (params.isFakePlanar) + { + RunFakePlanarHistogram(state, params.shape, params.useMask, op); + } + else if (params.inputKind == benchutils::InputKind::Tensor) + { + RunTensorHistogram(state, params.shape, params.useMask, params.isPlanar, op); + } + else // zero and positive var shape means use ImageBatchVarShape + { + throw std::invalid_argument("ImageBatchVarShape not implemented for this operator"); + } +} + +} // namespace + +template +inline void histogram(nvbench::state &state, nvbench::type_list) +try +{ + const HistogramBenchParams params = ParseHistogramParams(state); + if (!ValidateAndAccount(state, params)) + return; + + cvcuda::Histogram op; + DispatchHistogram(state, params, op); +} +CVCUDA_BENCH_SKIP_ERRORS(state) + +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(histogram, NVBENCH_TYPE_AXES(BENCH_HISTOGRAM_TYPES)) +BENCH_HISTOGRAM_AXES; diff --git a/bench/cpp/ops/BenchHistogramEq.cpp b/bench/cpp/ops/BenchHistogramEq.cpp new file mode 100644 index 000000000..623020d18 --- /dev/null +++ b/bench/cpp/ops/BenchHistogramEq.cpp @@ -0,0 +1,124 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchHistogramEqConfig.hpp" + +#include +#include + +#include + +template +inline void histogrameq(nvbench::state &state, nvbench::type_list) +try +{ + int3 shape = benchutils::GetShape<3, int3>(state.get_string("shape")); + auto layout = benchutils::GetStringParam(state, "layout", "NHWC"); + const benchutils::InputKind inputKind = benchutils::GetInputKind(state.get_string("inputKind")); + + using BT = typename nvcv::cuda::BaseType; + int ch = nvcv::cuda::NumElements; + + const bool isPlanar = layout == "NCHW"; + const bool isFakePlanar = layout == "NCHW_FAKE"; + if (layout != "NHWC" && layout != "NCHW" && layout != "NCHW_FAKE") + { + state.skip("HistogramEq benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts"); + return; + } + if (isFakePlanar && inputKind == benchutils::InputKind::VarShape) + { + state.skip("Fake-planar (NCHW_FAKE) HistogramEq benchmark is tensor-only"); + return; + } + if (isPlanar && inputKind == benchutils::InputKind::VarShape && ch == 4) + { + state.skip("uchar4 planar var-shape HistogramEq benchmark is unsupported by the image API"); + return; + } + + const int64_t bytes = static_cast(shape.x) * shape.y * shape.z * sizeof(T); + state.add_global_memory_reads((isFakePlanar ? 3 : 1) * bytes); + state.add_global_memory_writes((isFakePlanar ? 3 : 1) * bytes); + + cvcuda::HistogramEq op(shape.x); + + // clang-format off + + if (isFakePlanar) + { + nvcv::Tensor src ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + nvcv::Tensor interSrc({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor interDst({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::LcgValues()); + + cvcuda::Reformat reformatOp; + + benchutils::warmup_and_exec(state, BENCH_HISTOGRAMEQ_WARMUP_ITERATIONS, + [&op, &reformatOp, &src, &interSrc, &interDst, &dst](cudaStream_t s) { + reformatOp(s, src, interSrc); + op(s, interSrc, interDst); + reformatOp(s, interDst, dst); + }); + } + else if (inputKind == benchutils::InputKind::Tensor) + { + nvcv::Tensor src = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::LcgValues()); + + benchutils::warmup_and_exec(state, BENCH_HISTOGRAMEQ_WARMUP_ITERATIONS, + [&op, &src, &dst](cudaStream_t s) { op(s, src, dst); }); + } + else // zero and positive var shape means use ImageBatchVarShape + { + nvcv::ImageBatchVarShape src(shape.x); + nvcv::ImageBatchVarShape dst(shape.x); + + if (isPlanar) + { + benchutils::FillPlanarImageBatchLcg(src, long2{shape.z, shape.y}, long2{0, 0}); + benchutils::FillPlanarImageBatchLike(dst, src); + } + else + { + benchutils::FillImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}, + benchutils::LcgValues()); + // Use FillImageBatchLike to ensure dst has same per-sample shapes as src + benchutils::FillImageBatchLike(dst, src, [](const long4_16a &) { return T{0}; }); + } + + benchutils::warmup_and_exec(state, BENCH_HISTOGRAMEQ_WARMUP_ITERATIONS, + [&op, &src, &dst](cudaStream_t s) { op(s, src, dst); }); + } +} +CVCUDA_BENCH_SKIP_ERRORS(state) + +// clang-format on + +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(histogrameq, NVBENCH_TYPE_AXES(BENCH_HISTOGRAMEQ_TYPES)) +BENCH_HISTOGRAMEQ_AXES; diff --git a/bench/cpp/ops/BenchInpaint.cpp b/bench/cpp/ops/BenchInpaint.cpp new file mode 100644 index 000000000..248dd1423 --- /dev/null +++ b/bench/cpp/ops/BenchInpaint.cpp @@ -0,0 +1,237 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchInpaintConfig.hpp" + +#include +#include + +#include + +#include + +struct InpaintBenchLayout +{ + benchutils::InputKind inputKind; + bool isPlanar; + bool isFakePlanar; + const char *skipMessage; +}; + +template +inline void AddInpaintMemoryCounters(nvbench::state &state, const int3 &shape, bool isFakePlanar) +{ + const long imageBytes = static_cast(shape.x) * shape.y * shape.z * sizeof(T); + const long maskBytes = static_cast(shape.x) * shape.y * shape.z * sizeof(uint8_t); + if (isFakePlanar) + { + // reformat(NCHW->NHWC) + inpaint + reformat(NHWC->NCHW), with the same NHWC mask stripe. + state.add_global_memory_reads(3 * imageBytes + maskBytes); + state.add_global_memory_writes(3 * imageBytes); + return; + } + + state.add_global_memory_reads(imageBytes + maskBytes); + state.add_global_memory_writes(imageBytes); +} + +template +inline nvcv::Tensor MakeInpaintTensor(const int3 &shape, int channels, bool isPlanar) +{ + if (isPlanar) + { + return nvcv::Tensor( + { + {shape.x, channels, shape.y, shape.z}, + "NCHW" + }, + benchutils::GetDataType()); + } + return nvcv::Tensor( + { + {shape.x, shape.y, shape.z, channels}, + "NHWC" + }, + benchutils::GetDataType()); +} + +inline void FillInpaintMask(nvcv::Tensor &mask) +{ + // Mask semantics: 0 = known (don't inpaint), non-zero = needs inpainting. + // Use deterministic stripe pattern (every other row) for exactly 50% coverage. + benchutils::FillTensor(mask, [](const long4_16a &c) -> uint8_t { return (c.y % 2 == 1) ? 1 : 0; }); +} + +template +inline void RunFakePlanarInpaintBench(nvbench::state &state, cvcuda::Inpaint &op, const int3 &shape, int channels, + double inpaintRadius) +{ + nvcv::Tensor src = MakeInpaintTensor(shape, channels, true); + nvcv::Tensor interSrc = MakeInpaintTensor(shape, channels, false); + nvcv::Tensor interDst = MakeInpaintTensor(shape, channels, false); + nvcv::Tensor dst = MakeInpaintTensor(shape, channels, true); + nvcv::Tensor mask( + { + {shape.x, shape.y, shape.z, 1}, + "NHWC" + }, + nvcv::TYPE_U8); + + benchutils::FillTensor(src, benchutils::LcgValues()); + FillInpaintMask(mask); + + cvcuda::Reformat reformatOp; + benchutils::warmup_and_exec( + state, BENCH_INPAINT_WARMUP_ITERATIONS, + [&op, &reformatOp, &src, &interSrc, &interDst, &dst, &mask, inpaintRadius](cudaStream_t s) + { + reformatOp(s, src, interSrc); + op(s, interSrc, mask, interDst, inpaintRadius); + reformatOp(s, interDst, dst); + }); +} + +template +inline void RunTensorInpaintBench(nvbench::state &state, cvcuda::Inpaint &op, const int3 &shape, int channels, + bool isPlanar, double inpaintRadius) +{ + nvcv::Tensor src = MakeInpaintTensor(shape, channels, isPlanar); + nvcv::Tensor dst = MakeInpaintTensor(shape, channels, isPlanar); + nvcv::Tensor mask( + { + {shape.x, shape.y, shape.z, 1}, + "NHWC" + }, + nvcv::TYPE_U8); + + benchutils::FillTensor(src, benchutils::LcgValues()); + FillInpaintMask(mask); + + benchutils::warmup_and_exec(state, BENCH_INPAINT_WARMUP_ITERATIONS, + [&op, &src, &mask, &dst, inpaintRadius](cudaStream_t s) + { op(s, src, mask, dst, inpaintRadius); }); +} + +template +inline void FillInpaintImageBatch(nvcv::ImageBatchVarShape &src, nvcv::ImageBatchVarShape &dst, const int3 &shape, + bool isPlanar) +{ + if (constexpr int kChannels = nvcv::cuda::NumElements; isPlanar && kChannels > 1) + { + benchutils::FillPlanarImageBatchLcg(src, long2{shape.z, shape.y}, long2{0, 0}); + benchutils::FillPlanarImageBatchLike(dst, src); + return; + } + + benchutils::FillImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}, benchutils::LcgValues()); + // Use FillImageBatchLike to ensure dst has same per-sample shapes as src. + benchutils::FillImageBatchLike(dst, src, [](const long4_16a &) { return T{0}; }); +} + +template +inline void RunVarShapeInpaintBench(nvbench::state &state, cvcuda::Inpaint &op, const int3 &shape, bool isPlanar, + double inpaintRadius) +{ + if (isPlanar && std::is_same_v && nvcv::cuda::NumElements == 4) + { + state.skip("uchar4 planar var-shape Inpaint benchmark is unsupported by the Python image API"); + return; + } + + nvcv::ImageBatchVarShape src(shape.x); + nvcv::ImageBatchVarShape dst(shape.x); + nvcv::ImageBatchVarShape mask(shape.x); + + FillInpaintImageBatch(src, dst, shape, isPlanar); + + // Mask semantics: 0 = known (don't inpaint), non-zero = needs inpainting. + // Use deterministic stripe pattern (every other row) for exactly 50% coverage. + benchutils::FillImageBatch(mask, long2{shape.z, shape.y}, long2{0, 0}, + [](const long4_16a &c) -> uint8_t { return (c.y % 2 == 1) ? 1 : 0; }); + + benchutils::warmup_and_exec(state, BENCH_INPAINT_WARMUP_ITERATIONS, + [&op, &src, &mask, &dst, inpaintRadius](cudaStream_t s) + { op(s, src, mask, dst, inpaintRadius); }); +} + +inline InpaintBenchLayout ValidateInpaintLayout(nvbench::state &state) +{ + auto layout = benchutils::GetStringParam(state, "layout", "NHWC"); + const benchutils::InputKind inputKind = benchutils::GetInputKind(state.get_string("inputKind")); + + const bool isPlanar = layout == "NCHW"; + const bool isFakePlanar = layout == "NCHW_FAKE"; + if (layout != "NHWC" && layout != "NCHW" && layout != "NCHW_FAKE") + { + return {inputKind, isPlanar, isFakePlanar, "Inpaint benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts"}; + } + if (isFakePlanar && inputKind == benchutils::InputKind::VarShape) + { + return {inputKind, isPlanar, isFakePlanar, "Fake-planar (NCHW_FAKE) Inpaint benchmark is tensor-only"}; + } + return {inputKind, isPlanar, isFakePlanar, nullptr}; +} + +template +inline void ExecuteInpaintPath(nvbench::state &state, const int3 &shape, int channels, const InpaintBenchLayout &layout, + double inpaintRadius) +{ + AddInpaintMemoryCounters(state, shape, layout.isFakePlanar); + + cvcuda::Inpaint op(shape.x, nvcv::Size2D{shape.z, shape.y}); + + if (layout.isFakePlanar) // tensor-only: planar->interleaved->inpaint->interleaved->planar + { + RunFakePlanarInpaintBench(state, op, shape, channels, inpaintRadius); + return; + } + if (layout.inputKind == benchutils::InputKind::Tensor) + { + RunTensorInpaintBench(state, op, shape, channels, layout.isPlanar, inpaintRadius); + return; + } + + RunVarShapeInpaintBench(state, op, shape, layout.isPlanar, inpaintRadius); +} + +template +inline void inpaint(nvbench::state &state, nvbench::type_list) +try +{ + int3 shape = benchutils::GetShape<3, int3>(state.get_string("shape")); + auto layout = ValidateInpaintLayout(state); + if (layout.skipMessage != nullptr) + { + state.skip(layout.skipMessage); + return; + } + + // Use base type and channel count for consistent tensor creation (matches BenchHistogramEq pattern) + using BT = typename nvcv::cuda::BaseType; + int ch = nvcv::cuda::NumElements; + + double inpaintRadius = state.get_float64("inpaintRadius"); + + ExecuteInpaintPath(state, shape, ch, layout, inpaintRadius); +} +CVCUDA_BENCH_SKIP_ERRORS(state) + +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(inpaint, NVBENCH_TYPE_AXES(BENCH_INPAINT_TYPES)) +BENCH_INPAINT_AXES; diff --git a/bench/cpp/ops/BenchInvert.cpp b/bench/cpp/ops/BenchInvert.cpp new file mode 100644 index 000000000..9bbb38b8e --- /dev/null +++ b/bench/cpp/ops/BenchInvert.cpp @@ -0,0 +1,132 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchInvertConfig.hpp" + +#include +#include + +#include + +template +inline void invert(nvbench::state &state, nvbench::type_list) +try +{ + int3 shape = benchutils::GetShape<3, int3>(state.get_string("shape")); + auto layout = benchutils::GetStringParam(state, "layout", "NHWC"); + const benchutils::InputKind inputKind = benchutils::GetInputKind(state.get_string("inputKind")); + + const bool isPlanar = layout == "NCHW"; + const bool isFakePlanar = layout == "NCHW_FAKE"; + if (layout != "NHWC" && layout != "NCHW" && layout != "NCHW_FAKE") + { + state.skip("Invert benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts"); + return; + } + // NCHW_FAKE ("fake planar") is a tensor-only comparison path: planar data is reformatted to + // interleaved, inverted with the interleaved kernel, and reformatted back — all timed together — + // so the native planar path (NCHW) can be shown to beat the naive convert->op->convert pipeline. + if (isFakePlanar && inputKind == benchutils::InputKind::VarShape) + { + state.skip("Fake-planar (NCHW_FAKE) invert benchmark is tensor-only"); + return; + } + + using BT = typename nvcv::cuda::BaseType; + + int ch = nvcv::cuda::NumElements; + + // Invert preserves size, so src and dst hold the same number of bytes. + const long bytes = static_cast(shape.x) * shape.y * shape.z * sizeof(T); + if (isFakePlanar) + { + // reformat(NCHW->NHWC) + invert + reformat(NHWC->NCHW): reads src + interleaved src + dst, + // writes interleaved src + interleaved dst + dst. + state.add_global_memory_reads(3 * bytes); + state.add_global_memory_writes(3 * bytes); + } + else + { + state.add_global_memory_reads(bytes); + state.add_global_memory_writes(bytes); + } + + cvcuda::Invert op; + + // clang-format off + if (isFakePlanar) // tensor-only: planar->interleaved->invert->interleaved->planar + { + nvcv::Tensor src ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + nvcv::Tensor interSrc({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor interDst({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + cvcuda::Reformat reformatOp; + + benchutils::warmup_and_exec(state, BENCH_INVERT_WARMUP_ITERATIONS, + [&op, &reformatOp, &src, &interSrc, &interDst, &dst](cudaStream_t s) { + reformatOp(s, src, interSrc); // NCHW -> NHWC + op(s, interSrc, interDst); // interleaved invert + reformatOp(s, interDst, dst); // NHWC -> NCHW + }); + } + else if (inputKind == benchutils::InputKind::Tensor) + { + nvcv::Tensor src = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + benchutils::warmup_and_exec(state, BENCH_INVERT_WARMUP_ITERATIONS, + [&op, &src, &dst](cudaStream_t s) { op(s, src, dst); }); + } + else // ImageBatchVarShape + { + nvcv::ImageBatchVarShape src(shape.x); + nvcv::ImageBatchVarShape dst(shape.x); + + if (isPlanar) + { + benchutils::FillPlanarImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}); + benchutils::FillPlanarImageBatch(dst, long2{shape.z, shape.y}, long2{0, 0}); + } + else + { + benchutils::FillImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}, + benchutils::CheckerboardValues()); + benchutils::FillImageBatchLike(dst, src, [](const long4_16a &) { return T{0}; }); + } + + benchutils::warmup_and_exec(state, BENCH_INVERT_WARMUP_ITERATIONS, + [&op, &src, &dst](cudaStream_t s) { op(s, src, dst); }); + } +} +CVCUDA_BENCH_SKIP_ERRORS(state) + +// clang-format on + +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(invert, NVBENCH_TYPE_AXES(BENCH_INVERT_TYPES)) +BENCH_INVERT_AXES; diff --git a/bench/cpp/ops/BenchJointBilateralFilter.cpp b/bench/cpp/ops/BenchJointBilateralFilter.cpp new file mode 100644 index 000000000..42ff614b4 --- /dev/null +++ b/bench/cpp/ops/BenchJointBilateralFilter.cpp @@ -0,0 +1,142 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchJointBilateralFilterConfig.hpp" + +#include +#include + +#include + +template +inline void jointbilateralfilter(nvbench::state &state, nvbench::type_list) +try +{ + int3 shape = benchutils::GetShape<3, int3>(state.get_string("shape")); + const benchutils::InputKind inputKind = benchutils::GetInputKind(state.get_string("inputKind")); + auto layout = benchutils::GetStringParam(state, "layout", "NHWC"); + int diameter = benchutils::GetIntParam(state, "diameter"); + auto sigmaSpace = static_cast(state.get_float64("sigmaSpace")); + float sigmaColor = -1.f; + + using BT = typename nvcv::cuda::BaseType; + int ch = nvcv::cuda::NumElements; + + const bool isPlanar = layout == "NCHW"; + const bool isFakePlanar = layout == "NCHW_FAKE"; + if (layout != "NHWC" && layout != "NCHW" && layout != "NCHW_FAKE") + { + state.skip("JointBilateralFilter benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts"); + return; + } + if (isFakePlanar && inputKind == benchutils::InputKind::VarShape) + { + state.skip("Fake-planar (NCHW_FAKE) JointBilateralFilter benchmark is tensor-only"); + return; + } + + NVCVBorderType borderType = benchutils::GetBorderType(state.get_string("border")); + + state.add_global_memory_reads(shape.x * shape.y * shape.z * sizeof(T)); + state.add_global_memory_writes(shape.x * shape.y * shape.z * sizeof(T)); + + cvcuda::JointBilateralFilter op; + + // clang-format off + + if (isFakePlanar) // tensor-only: planar->interleaved->jointbilateralfilter->interleaved->planar + { + nvcv::Tensor src ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + nvcv::Tensor interSrc({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor interDst({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::LcgValues()); + + cvcuda::Reformat reformatOp; + benchutils::warmup_and_exec(state, BENCH_JOINTBILATERALFILTER_WARMUP_ITERATIONS, + [&op, &reformatOp, &src, &interSrc, &interDst, &dst, &diameter, &sigmaColor, &sigmaSpace, &borderType](cudaStream_t s) { + reformatOp(s, src, interSrc); + op(s, interSrc, interSrc, interDst, diameter, sigmaColor, sigmaSpace, borderType); + reformatOp(s, interDst, dst); + }); + } + else if (inputKind == benchutils::InputKind::Tensor) + { + nvcv::Tensor src = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor color = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::LcgValues()); + benchutils::FillTensor(color, benchutils::LcgValues()); + + benchutils::warmup_and_exec(state, BENCH_JOINTBILATERALFILTER_WARMUP_ITERATIONS, + [&op, &src, &color, &dst, &diameter, &sigmaColor, &sigmaSpace, &borderType](cudaStream_t s) { + op(s, src, color, dst, diameter, sigmaColor, sigmaSpace, borderType); + }); + } + else // zero and positive var shape means use ImageBatchVarShape + { + nvcv::ImageBatchVarShape src(shape.x); + nvcv::ImageBatchVarShape color(shape.x); + nvcv::ImageBatchVarShape dst(shape.x); + + if (isPlanar) + { + benchutils::FillPlanarImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}); + color.pushBack(src.begin(), src.end()); + benchutils::FillPlanarImageBatchLike(dst, src); + } + else + { + benchutils::FillImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}, + benchutils::LcgValues()); + // color can alias src (guidance = input is valid use case) + color.pushBack(src.begin(), src.end()); + // Use FillImageBatchLike to ensure dst has same per-sample shapes as src + benchutils::FillImageBatchLike(dst, src, [](const long4_16a &) { return T{0}; }); + } + + nvcv::Tensor diameterTensor({{shape.x}, "N"}, nvcv::TYPE_S32); + nvcv::Tensor sigmaSpaceTensor({{shape.x}, "N"}, nvcv::TYPE_F32); + nvcv::Tensor sigmaColorTensor({{shape.x}, "N"}, nvcv::TYPE_F32); + + benchutils::FillTensor(diameterTensor, [&diameter](const long4_16a &){ return diameter; }); + benchutils::FillTensor(sigmaSpaceTensor, [&sigmaSpace](const long4_16a &){ return sigmaSpace; }); + benchutils::FillTensor(sigmaColorTensor, [&sigmaColor](const long4_16a &){ return sigmaColor; }); + + benchutils::warmup_and_exec(state, BENCH_JOINTBILATERALFILTER_WARMUP_ITERATIONS, + [&op, &src, &color, &dst, &diameterTensor, &sigmaColorTensor, &sigmaSpaceTensor, &borderType](cudaStream_t s) { + op(s, src, color, dst, diameterTensor, sigmaColorTensor, sigmaSpaceTensor, borderType); + }); + } +} +CVCUDA_BENCH_SKIP_ERRORS(state) + +// clang-format on + +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(jointbilateralfilter, NVBENCH_TYPE_AXES(BENCH_JOINTBILATERALFILTER_TYPES)) +BENCH_JOINTBILATERALFILTER_AXES; diff --git a/bench/cpp/ops/BenchJpegCompressionDistortion.cpp b/bench/cpp/ops/BenchJpegCompressionDistortion.cpp new file mode 100644 index 000000000..c52daae66 --- /dev/null +++ b/bench/cpp/ops/BenchJpegCompressionDistortion.cpp @@ -0,0 +1,146 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchJpegCompressionDistortionConfig.hpp" + +#include +#include + +#include + +// Quality only scales the quantization tables (built once per CUDA block from constant data), so +// it does not affect throughput; use a fixed mid-range value. +static constexpr int kQuality = 50; + +template +inline void jpegcompressiondistortion(nvbench::state &state, nvbench::type_list) +try +{ + int3 shape = benchutils::GetShape<3, int3>(state.get_string("shape")); + auto layout = benchutils::GetStringParam(state, "layout", "NHWC"); + const benchutils::InputKind inputKind = benchutils::GetInputKind(state.get_string("inputKind")); + + const bool isPlanar = layout == "NCHW"; + const bool isFakePlanar = layout == "NCHW_FAKE"; + if (layout != "NHWC" && layout != "NCHW" && layout != "NCHW_FAKE") + { + state.skip("JpegCompressionDistortion benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts"); + return; + } + // NCHW_FAKE ("fake planar") is a tensor-only comparison path: planar data is reformatted to + // interleaved, distorted with the interleaved kernel, and reformatted back — all timed together — + // so the native planar path (NCHW) can be shown to beat the naive convert->op->convert pipeline. + if (isFakePlanar && inputKind == benchutils::InputKind::VarShape) + { + state.skip("Fake-planar (NCHW_FAKE) jpegcompressiondistortion benchmark is tensor-only"); + return; + } + + using BT = typename nvcv::cuda::BaseType; + + int ch = nvcv::cuda::NumElements; + + // JpegCompressionDistortion preserves size, so src and dst hold the same number of bytes. + const long bytes = static_cast(shape.x) * shape.y * shape.z * sizeof(T); + if (isFakePlanar) + { + // reformat(NCHW->NHWC) + distort + reformat(NHWC->NCHW): reads src + interleaved src + dst, + // writes interleaved src + interleaved dst + dst. + state.add_global_memory_reads(3 * bytes); + state.add_global_memory_writes(3 * bytes); + } + else + { + state.add_global_memory_reads(bytes); + state.add_global_memory_writes(bytes); + } + + cvcuda::JpegCompressionDistortion op; + + // clang-format off + if (isFakePlanar) // tensor-only: planar->interleaved->distort->interleaved->planar + { + nvcv::Tensor src ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + nvcv::Tensor interSrc({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor interDst({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + cvcuda::Reformat reformatOp; + + benchutils::warmup_and_exec(state, BENCH_JPEGCOMPRESSIONDISTORTION_WARMUP_ITERATIONS, + [&op, &reformatOp, &src, &interSrc, &interDst, &dst](cudaStream_t s) { + reformatOp(s, src, interSrc); // NCHW -> NHWC + op(s, interSrc, interDst, kQuality); // interleaved distortion + reformatOp(s, interDst, dst); // NHWC -> NCHW + }); + } + else if (inputKind == benchutils::InputKind::Tensor) + { + nvcv::Tensor src = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + benchutils::warmup_and_exec(state, BENCH_JPEGCOMPRESSIONDISTORTION_WARMUP_ITERATIONS, + [&op, &src, &dst](cudaStream_t s) { op(s, src, dst, kQuality); }); + } + else // ImageBatchVarShape + { + nvcv::ImageBatchVarShape src(shape.x); + nvcv::ImageBatchVarShape dst(shape.x); + + if (isPlanar) + { + benchutils::FillPlanarImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}); + benchutils::FillPlanarImageBatch(dst, long2{shape.z, shape.y}, long2{0, 0}); + } + else + { + // The 1-channel (grayscale/luma) profile uses the non-color U8 format; only the + // 3-channel profile carries the RGB color model the operator requires. + if constexpr (nvcv::cuda::NumElements == 3) + { + benchutils::FillImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}, + benchutils::CheckerboardValues(), benchutils::GetRGBFormat()); + } + else + { + benchutils::FillImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}, + benchutils::CheckerboardValues(), nvcv::FMT_U8); + } + benchutils::FillImageBatchLike(dst, src, [](const long4_16a &) { return T{0}; }); + } + + benchutils::warmup_and_exec(state, BENCH_JPEGCOMPRESSIONDISTORTION_WARMUP_ITERATIONS, + [&op, &src, &dst](cudaStream_t s) { op(s, src, dst, kQuality); }); + } +} +CVCUDA_BENCH_SKIP_ERRORS(state) + +// clang-format on + +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(jpegcompressiondistortion, NVBENCH_TYPE_AXES(BENCH_JPEGCOMPRESSIONDISTORTION_TYPES)) +BENCH_JPEGCOMPRESSIONDISTORTION_AXES; diff --git a/bench/cpp/ops/BenchLabel.cpp b/bench/cpp/ops/BenchLabel.cpp new file mode 100644 index 000000000..538f41c90 --- /dev/null +++ b/bench/cpp/ops/BenchLabel.cpp @@ -0,0 +1,269 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchLabelConfig.hpp" + +#include +#include + +#include + +#include +#include +#include + +inline bool HasRunChoice(std::string_view runChoice, std::string_view choice) +{ + return runChoice.find(choice) != std::string::npos; +} + +struct LabelLayout +{ + std::string runChoice; + bool isPlanar = false; + bool isFakePlanar = false; + bool usePlanarLayout = false; + bool valid = true; +}; + +template +struct LabelTensors +{ + nvcv::Tensor src; + nvcv::Tensor dst; +}; + +inline LabelLayout ParseLabelLayout(nvbench::state &state, std::string runChoice) +{ + if (runChoice == "DEFAULT") + { + runChoice = ""; + } + + const std::string layout = benchutils::GetStringParam(state, "layout", "NHWC"); + if (layout != "NHWC" && layout != "NCHW" && layout != "NCHW_FAKE") + { + state.skip("Label benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts"); + return {"", false, false, false, false}; + } + + const bool isPlanar = layout == "NCHW"; + const bool isFakePlanar = layout == "NCHW_FAKE"; + + return {std::move(runChoice), isPlanar, isFakePlanar, isPlanar || isFakePlanar, true}; +} + +template +void SetupOptionalLabelInputs(const std::string &runChoice, long3 srcShape, long3 staShape, bool usePlanarLayout, + nvcv::Tensor &bgT, nvcv::Tensor &minT, nvcv::Tensor &maxT, nvcv::Tensor &mszT, + nvcv::Tensor &countT, nvcv::Tensor &statsT, nvcv::Tensor &maskT) +{ + if (HasRunChoice(runChoice, "BG")) + { + bgT = nvcv::Tensor({{srcShape.x}, "N"}, benchutils::GetDataType()); + + benchutils::FillTensor(bgT, benchutils::LcgValues()); + } + if (HasRunChoice(runChoice, "MIN")) + { + minT = nvcv::Tensor({{srcShape.x}, "N"}, benchutils::GetDataType()); + + const ST minThreshold = std::is_signed_v ? static_cast(-64) : static_cast(64); + benchutils::FillTensor(minT, benchutils::LcgValues(minThreshold, minThreshold)); + } + if (HasRunChoice(runChoice, "MAX")) + { + maxT = nvcv::Tensor({{srcShape.x}, "N"}, benchutils::GetDataType()); + + const ST maxThreshold = std::is_signed_v ? static_cast(64) : static_cast(192); + benchutils::FillTensor(maxT, benchutils::LcgValues(maxThreshold, maxThreshold)); + } + if (HasRunChoice(runChoice, "ISLAND")) + { + mszT = nvcv::Tensor({{srcShape.x}, "N"}, benchutils::GetDataType
()); + + benchutils::FillTensor
(mszT, benchutils::LcgValues
(16, 16)); + } + if (HasRunChoice(runChoice, "COUNT")) + { + countT = nvcv::Tensor({{srcShape.x}, "N"}, benchutils::GetDataType
()); + } + if (HasRunChoice(runChoice, "STAT")) + { + statsT = nvcv::Tensor( + { + {staShape.x, staShape.y, staShape.z}, + "NMA" + }, + benchutils::GetDataType
()); + } + if (HasRunChoice(runChoice, "MASK")) + { + maskT = usePlanarLayout ? nvcv::Tensor( + { + {srcShape.x, 1, srcShape.y, srcShape.z}, + "NCHW" + }, + nvcv::TYPE_U8) + : nvcv::Tensor({{srcShape.x, srcShape.y, srcShape.z, 1}, "NHWC"}, nvcv::TYPE_U8); + + benchutils::FillTensor(maskT, benchutils::CheckerboardValues()); + } +} + +template +LabelTensors BuildLabelTensors(long3 srcShape, long3 dstShape, bool usePlanarLayout) +{ + if (usePlanarLayout) + { + return { + nvcv::Tensor({{srcShape.x, 1, srcShape.y, srcShape.z}, "NCHW"}, + benchutils::GetDataType()), + nvcv::Tensor({{dstShape.x, 1, dstShape.y, dstShape.z}, "NCHW"}, + benchutils::GetDataType
()) + }; + } + + return { + nvcv::Tensor({{srcShape.x, srcShape.y, srcShape.z, 1}, "NHWC"}, + benchutils::GetDataType()), + nvcv::Tensor({{dstShape.x, dstShape.y, dstShape.z, 1}, "NHWC"}, + benchutils::GetDataType
()) + }; +} + +template +void RunLabelFakePlanar(nvbench::state &state, cvcuda::Label &op, cvcuda::Reformat &reformatOp, nvcv::Tensor &src, + nvcv::Tensor &dst, long3 srcShape, long3 dstShape, nvcv::Tensor &bgT, nvcv::Tensor &minT, + nvcv::Tensor &maxT, nvcv::Tensor &mszT, nvcv::Tensor &countT, nvcv::Tensor &statsT, + nvcv::Tensor &maskT, NVCVConnectivityType conn, NVCVLabelType alab, NVCVLabelMaskType mType) +{ + nvcv::Tensor interSrc( + { + {srcShape.x, srcShape.y, srcShape.z, 1}, + "NHWC" + }, + benchutils::GetDataType()); + nvcv::Tensor interDst( + { + {dstShape.x, dstShape.y, dstShape.z, 1}, + "NHWC" + }, + benchutils::GetDataType
()); + nvcv::Tensor interMaskT; + if (maskT) + { + interMaskT = nvcv::Tensor( + { + {srcShape.x, srcShape.y, srcShape.z, 1}, + "NHWC" + }, + nvcv::TYPE_U8); + } + + benchutils::warmup_and_exec(state, BENCH_LABEL_WARMUP_ITERATIONS, + [&op, &reformatOp, &src, &dst, &interSrc, &interDst, &bgT, &minT, &maxT, &mszT, &countT, + &statsT, &maskT, &interMaskT, &conn, &alab, &mType](cudaStream_t s) + { + reformatOp(s, src, interSrc); + if (maskT) + { + reformatOp(s, maskT, interMaskT); + } + op(s, interSrc, interDst, bgT, minT, maxT, mszT, countT, statsT, interMaskT, conn, + alab, mType); + reformatOp(s, interDst, dst); + }); +} + +template +inline void label(nvbench::state &state, nvbench::type_list) +try +{ + // Use int (S32) for label output dtype to match Python benchmark + using DT = int; + + long3 srcShape = benchutils::GetShape<3>(state.get_string("shape")); + long3 dstShape = srcShape; + + LabelLayout layout = ParseLabelLayout(state, state.get_string("runChoice")); + if (!layout.valid) + { + return; + } + + // Use [BG][MIN][MAX][ISLAND][COUNT][STAT][MASK] in runChoice to run Label with: + // background; minThreshold; maxThreshold; island removal; count; statistics; mask + + long3 staShape{srcShape.x, 10000, 7}; // using fixed 10K max. cap. and 2D problem + + NVCVConnectivityType conn = NVCV_CONNECTIVITY_4_2D; + NVCVLabelType alab = NVCV_LABEL_FAST; + NVCVLabelMaskType mType = NVCV_REMOVE_ISLANDS_OUTSIDE_MASK_ONLY; + + nvcv::Tensor bgT; + nvcv::Tensor minT; + nvcv::Tensor maxT; + nvcv::Tensor countT; + nvcv::Tensor statsT; + nvcv::Tensor mszT; + nvcv::Tensor maskT; + + cvcuda::Label op; + + const long imageBytes = srcShape.x * srcShape.y * srcShape.z * sizeof(ST); + const long outBytes = dstShape.x * dstShape.y * dstShape.z * sizeof(DT); + if (layout.isFakePlanar) + { + state.add_global_memory_reads(2 * imageBytes + outBytes); + state.add_global_memory_writes(imageBytes + 2 * outBytes); + } + else + { + state.add_global_memory_reads(imageBytes); + state.add_global_memory_writes(outBytes); + } + + SetupOptionalLabelInputs(layout.runChoice, srcShape, staShape, layout.usePlanarLayout, bgT, minT, maxT, + mszT, countT, statsT, maskT); + + LabelTensors tensors = BuildLabelTensors(srcShape, dstShape, layout.usePlanarLayout); + + benchutils::FillTensor(tensors.src, benchutils::LcgValues()); + + if (layout.isFakePlanar) + { + cvcuda::Reformat reformatOp; + RunLabelFakePlanar(state, op, reformatOp, tensors.src, tensors.dst, srcShape, dstShape, bgT, minT, maxT, + mszT, countT, statsT, maskT, conn, alab, mType); + } + else + { + benchutils::warmup_and_exec( + state, BENCH_LABEL_WARMUP_ITERATIONS, + [&op, &src = tensors.src, &dst = tensors.dst, &bgT, &minT, &maxT, &mszT, &countT, &statsT, &maskT, &conn, + &alab, &mType](cudaStream_t s) + { op(s, src, dst, bgT, minT, maxT, mszT, countT, statsT, maskT, conn, alab, mType); }); + } +} +CVCUDA_BENCH_SKIP_ERRORS(state) + +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(label, NVBENCH_TYPE_AXES(BENCH_LABEL_TYPES)) +BENCH_LABEL_AXES; diff --git a/bench/cpp/ops/BenchLaplacian.cpp b/bench/cpp/ops/BenchLaplacian.cpp new file mode 100644 index 000000000..9149a7ebc --- /dev/null +++ b/bench/cpp/ops/BenchLaplacian.cpp @@ -0,0 +1,133 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchLaplacianConfig.hpp" + +#include +#include + +#include + +template +inline void laplacian(nvbench::state &state, nvbench::type_list) +try +{ + int3 shape = benchutils::GetShape<3, int3>(state.get_string("shape")); + const benchutils::InputKind inputKind = benchutils::GetInputKind(state.get_string("inputKind")); + auto layout = benchutils::GetStringParam(state, "layout", "NHWC"); + int ksize = benchutils::GetIntParam(state, "ksize"); + auto scale = static_cast(state.get_float64("scale")); + + using BT = typename nvcv::cuda::BaseType; + + int ch = nvcv::cuda::NumElements; + + const bool isPlanar = layout == "NCHW"; + const bool isFakePlanar = layout == "NCHW_FAKE"; + if (layout != "NHWC" && layout != "NCHW" && layout != "NCHW_FAKE") + { + state.skip("Laplacian benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts"); + return; + } + if (isFakePlanar && inputKind == benchutils::InputKind::VarShape) + { + state.skip("Fake-planar (NCHW_FAKE) Laplacian benchmark is tensor-only"); + return; + } + + NVCVBorderType borderType = benchutils::GetBorderType(state.get_string("border")); + + state.add_global_memory_reads(shape.x * shape.y * shape.z * sizeof(T)); + state.add_global_memory_writes(shape.x * shape.y * shape.z * sizeof(T)); + + cvcuda::Laplacian op; + + // clang-format off + + if (isFakePlanar) // tensor-only: planar->interleaved->laplacian->interleaved->planar + { + nvcv::Tensor src ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + nvcv::Tensor interSrc({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor interDst({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + cvcuda::Reformat reformatOp; + + benchutils::warmup_and_exec(state, BENCH_LAPLACIAN_WARMUP_ITERATIONS, + [&op, &reformatOp, &src, &interSrc, &interDst, &dst, &ksize, &scale, &borderType](cudaStream_t s) { + reformatOp(s, src, interSrc); + op(s, interSrc, interDst, ksize, scale, borderType); + reformatOp(s, interDst, dst); + }); + } + else if (inputKind == benchutils::InputKind::Tensor) + { + nvcv::Tensor src = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + benchutils::warmup_and_exec(state, BENCH_LAPLACIAN_WARMUP_ITERATIONS, + [&op, &src, &dst, &ksize, &scale, &borderType](cudaStream_t s) { + op(s, src, dst, ksize, scale, borderType); + }); + } + else // zero and positive var shape means use ImageBatchVarShape + { + nvcv::ImageBatchVarShape src(shape.x); + nvcv::ImageBatchVarShape dst(shape.x); + + if (isPlanar) + { + benchutils::FillPlanarImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}); + benchutils::FillPlanarImageBatchLike(dst, src); + } + else + { + benchutils::FillImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}, + benchutils::CheckerboardValues()); + // Use FillImageBatchLike to ensure dst has same per-sample shapes as src + benchutils::FillImageBatchLike(dst, src, [](const long4_16a &) { return T{0}; }); + } + + nvcv::Tensor ksizeTensor({{shape.x}, "N"}, nvcv::TYPE_S32); + nvcv::Tensor scaleTensor({{shape.x}, "N"}, nvcv::TYPE_F32); + + benchutils::FillTensor(ksizeTensor, [&ksize](const long4_16a &){ return ksize; }); + benchutils::FillTensor(scaleTensor, [&scale](const long4_16a &){ return scale; }); + + benchutils::warmup_and_exec(state, BENCH_LAPLACIAN_WARMUP_ITERATIONS, + [&op, &src, &dst, &ksizeTensor, &scaleTensor, &borderType](cudaStream_t s) { + op(s, src, dst, ksizeTensor, scaleTensor, borderType); + }); + } +} +CVCUDA_BENCH_SKIP_ERRORS(state) + +// clang-format on + +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(laplacian, NVBENCH_TYPE_AXES(BENCH_LAPLACIAN_TYPES)) +BENCH_LAPLACIAN_AXES; diff --git a/bench/cpp/ops/BenchMedianBlur.cpp b/bench/cpp/ops/BenchMedianBlur.cpp new file mode 100644 index 000000000..579ef28cd --- /dev/null +++ b/bench/cpp/ops/BenchMedianBlur.cpp @@ -0,0 +1,210 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchMedianBlurConfig.hpp" + +#include +#include + +#include + +template +inline void FillPlanarImageBatch(nvcv::ImageBatchVarShape &imageBatch, long2 size, long2 varSize, VG valuesGenerator) +{ + using BT = typename nvcv::cuda::BaseType; + + nvcv::ImageFormat format = benchutils::GetPlanarFormat(); + auto randomWidth = benchutils::LcgValues(static_cast(size.x - varSize.x), static_cast(size.x)); + auto randomHeight = benchutils::LcgValues(static_cast(size.y - varSize.y), static_cast(size.y)); + + for (int i = 0; i < imageBatch.capacity(); ++i) + { + nvcv::Image image(nvcv::Size2D{randomWidth(), randomHeight()}, format); + + auto data = image.exportData(); + CVCUDA_CHECK_DATA(data); + + for (int p = 0; p < format.numPlanes(); ++p) + { + long2 strides{data->plane(p).rowStride, sizeof(BT)}; + long2 shape{data->plane(p).height, data->plane(p).width}; + + std::vector imageBuffer(strides.x * shape.x); + + benchutils::FillBuffer(imageBuffer, shape, strides, valuesGenerator); + CUDA_CHECK_ERROR(cudaMemcpy2D(data->plane(p).basePtr, strides.x, imageBuffer.data(), strides.x, strides.x, + data->plane(p).height, cudaMemcpyHostToDevice)); + } + + imageBatch.pushBack(image); + } +} + +template +inline void medianblur(nvbench::state &state, nvbench::type_list) +try +{ + int3 shape = benchutils::GetShape<3, int3>(state.get_string("shape")); + auto layout = benchutils::GetStringParam(state, "layout", "NHWC"); + const benchutils::InputKind inputKind = benchutils::GetInputKind(state.get_string("inputKind")); + int2 kernelSize = nvcv::cuda::StaticCast(benchutils::GetShape<2>(state.get_string("kernelSize"))); + + const bool isPlanar = layout == "NCHW"; + const bool isFakePlanar = layout == "NCHW_FAKE"; + if (layout != "NHWC" && layout != "NCHW" && layout != "NCHW_FAKE") + { + state.skip("MedianBlur benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts"); + return; + } + if (isFakePlanar && inputKind == benchutils::InputKind::VarShape) + { + state.skip("Fake-planar (NCHW_FAKE) MedianBlur benchmark is tensor-only"); + return; + } + + using BT = typename nvcv::cuda::BaseType; + + int ch = nvcv::cuda::NumElements; + + nvcv::Size2D kernelSize2d{kernelSize.x, kernelSize.y}; + + const long bytes = static_cast(shape.x) * shape.y * shape.z * sizeof(T); + if (isFakePlanar) + { + state.add_global_memory_reads(3 * bytes); + state.add_global_memory_writes(3 * bytes); + } + else + { + state.add_global_memory_reads(bytes); + state.add_global_memory_writes(bytes); + } + + cvcuda::MedianBlur op(shape.x); + + // clang-format off + + // Use horizontal DECREASING gradient pattern for deterministic timing. + // Random data causes the quickselect median algorithm to have variable performance. + // Decreasing gradient: pixel value = 255 - (column_index % 256) + // Note: For Tensor (4D NHWC), column is c.z; for ImageBatch (2D HW), column is c.y + auto gradientValue = [](long col) -> BT + { + if constexpr (std::is_floating_point_v) + return static_cast(255 - (col % 256)) / BT{255}; + else + return static_cast(255 - (col % 256)); + }; + auto gradientPatternTensor = [&gradientValue](const long4_16a &c) -> BT + { + return gradientValue(c.z); // c.z = W for NHWC + }; + auto gradientPatternTensorPlanar = [&gradientValue](const long4_16a &c) -> BT + { + return gradientValue(c.w); // c.w = W for NCHW + }; + auto gradientPatternImage = [&gradientValue](const long4_16a &c) -> T + { + const BT val = gradientValue(c.y); // c.y = W for HW (FillBuffer uses x=H, y=W) + T ret; + for (int i = 0; i < nvcv::cuda::NumElements; ++i) + { + nvcv::cuda::GetElement(ret, i) = val; + } + return ret; + }; + auto gradientPatternPlane = [&gradientValue](const long4_16a &c) -> BT + { + return gradientValue(c.y); // c.y = W for HW planes + }; + + if (isFakePlanar) // tensor-only: planar→interleaved→medianblur→interleaved→planar + { + nvcv::Tensor src ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + nvcv::Tensor interSrc({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor interDst({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, gradientPatternTensorPlanar); + + cvcuda::Reformat reformatOp; + + benchutils::warmup_and_exec(state, BENCH_MEDIANBLUR_WARMUP_ITERATIONS, + [&op, &reformatOp, &src, &interSrc, &interDst, &dst, &kernelSize2d](cudaStream_t s) { + reformatOp(s, src, interSrc); + op(s, interSrc, interDst, kernelSize2d); + reformatOp(s, interDst, dst); + }); + } + else if (inputKind == benchutils::InputKind::Tensor) + { + nvcv::Tensor src = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + + if (isPlanar) + { + benchutils::FillTensor(src, gradientPatternTensorPlanar); + } + else + { + benchutils::FillTensor(src, gradientPatternTensor); + } + + benchutils::warmup_and_exec(state, BENCH_MEDIANBLUR_WARMUP_ITERATIONS, + [&op, &src, &dst, &kernelSize2d](cudaStream_t s) { op(s, src, dst, kernelSize2d); }); + } + else // zero and positive var shape means use ImageBatchVarShape + { + nvcv::ImageBatchVarShape src(shape.x); + nvcv::ImageBatchVarShape dst(shape.x); + + if (isPlanar) + { + FillPlanarImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}, gradientPatternPlane); + benchutils::FillPlanarImageBatchLike(dst, src); + } + else + { + benchutils::FillImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}, + gradientPatternImage); + // Use FillImageBatchLike to ensure dst has same per-sample shapes as src + benchutils::FillImageBatchLike(dst, src, [](const long4_16a &) { return T{0}; }); + } + + // Kernel size tensor must be 1D tensor of int2 (TYPE_2S32), matching TestOpMedianBlur.cpp + nvcv::Tensor kernelSizeTensor(nvcv::TensorShape({shape.x}, "N"), nvcv::TYPE_2S32); + + benchutils::FillTensor(kernelSizeTensor, + [&kernelSize](const long4_16a &) { return kernelSize; }); + + benchutils::warmup_and_exec(state, BENCH_MEDIANBLUR_WARMUP_ITERATIONS, + [&op, &src, &dst, &kernelSizeTensor](cudaStream_t s) { op(s, src, dst, kernelSizeTensor); }); + } +} +CVCUDA_BENCH_SKIP_ERRORS(state) + +// clang-format on + +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(medianblur, NVBENCH_TYPE_AXES(BENCH_MEDIANBLUR_TYPES)) +BENCH_MEDIANBLUR_AXES; diff --git a/bench/cpp/ops/BenchMinAreaRect.cpp b/bench/cpp/ops/BenchMinAreaRect.cpp new file mode 100644 index 000000000..8e5b5578d --- /dev/null +++ b/bench/cpp/ops/BenchMinAreaRect.cpp @@ -0,0 +1,73 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchMinAreaRectConfig.hpp" + +#include + +#include + +template +inline void minarearect(nvbench::state &state, nvbench::type_list) +try +{ + int2 shape = benchutils::GetShape<2, int2>(state.get_string("shape")); + const benchutils::InputKind inputKind = benchutils::GetInputKind(state.get_string("inputKind")); + int numPoints = benchutils::GetIntParam(state, "numPoints"); + + if (numPoints < 0 || numPoints > shape.y) + { + throw std::invalid_argument("numPoints must be 0 or in [1, max_points]"); + } + + state.add_global_memory_reads(shape.x * shape.y * sizeof(T)); + state.add_global_memory_writes(shape.x * 8 * sizeof(float) + shape.x * sizeof(int)); + + cvcuda::MinAreaRect op(shape.x); + + // clang-format off + + if (inputKind == benchutils::InputKind::Tensor) + { + nvcv::Tensor src({{shape.x, shape.y, 2}, "NWC"}, benchutils::GetDataType()); + nvcv::Tensor dst({{shape.x, 8}, "NW"}, nvcv::TYPE_F32); + nvcv::Tensor points({{1, shape.x}, "NW"}, nvcv::TYPE_S32); + + benchutils::FillTensor(src, benchutils::LcgValues()); + benchutils::FillTensor(dst, [](const long4_16a &) { return 0.f; }); // Output only, zero-fill + // numPoints=0 preserves the original deterministic [10, 100] cycle. + benchutils::FillTensor(points, [numPoints](const long4_16a &c) { + return numPoints > 0 ? numPoints : 10 + static_cast(c.y % 91); + }); + + benchutils::warmup_and_exec(state, BENCH_MINAREARECT_WARMUP_ITERATIONS, + [&op, &src, &dst, &points, &shape](cudaStream_t s) { op(s, src, dst, points, shape.x); }); + } + else // zero and positive var shape means use ImageBatchVarShape + { + throw std::invalid_argument("ImageBatchVarShape not implemented for this operator"); + } +} +CVCUDA_BENCH_SKIP_ERRORS(state) + +// clang-format on + +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(minarearect, NVBENCH_TYPE_AXES(BENCH_MINAREARECT_TYPES)) +BENCH_MINAREARECT_AXES; diff --git a/bench/cpp/ops/BenchMinMaxLoc.cpp b/bench/cpp/ops/BenchMinMaxLoc.cpp new file mode 100644 index 000000000..50c0ef097 --- /dev/null +++ b/bench/cpp/ops/BenchMinMaxLoc.cpp @@ -0,0 +1,188 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchMinMaxLocConfig.hpp" + +#include +#include + +#include + +template +inline nvcv::DataType GetValueDataType() +{ + if constexpr (std::is_integral_v && std::is_signed_v) + { + return nvcv::TYPE_S32; + } + else if constexpr (std::is_integral_v && std::is_unsigned_v) + { + return nvcv::TYPE_U32; + } + else if constexpr (std::is_same_v) + { + return nvcv::TYPE_F32; + } + else if constexpr (std::is_same_v) + { + return nvcv::TYPE_F64; + } + else + { + throw std::invalid_argument("Unsupported MinMaxLoc input data type"); + } +} + +template +inline size_t GetValueDataSize() +{ + if constexpr (std::is_integral_v) + { + return sizeof(int32_t); + } + else + { + return sizeof(T); + } +} + +template +inline void minmaxloc(nvbench::state &state, nvbench::type_list) +try +{ + int3 shape = benchutils::GetShape<3, int3>(state.get_string("shape")); + const benchutils::InputKind inputKind = benchutils::GetInputKind(state.get_string("inputKind")); + int maxLocs = benchutils::GetIntParam(state, "maxLocations"); + std::string runChoice = state.get_string("runChoice"); + std::string layout = benchutils::GetStringParam(state, "layout", "NHWC"); + + const bool isPlanar = layout == "NCHW"; + const bool isFakePlanar = layout == "NCHW_FAKE"; + + if (layout != "NHWC" && !isPlanar && !isFakePlanar) + { + state.skip("MinMaxLoc benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts"); + return; + } + if (inputKind != benchutils::InputKind::Tensor && layout != "NHWC") + { + state.skip("Planar MinMaxLoc benchmark is tensor-only"); + return; + } + + bool runMin = runChoice == "MIN" || runChoice == "MIN_MAX"; + bool runMax = runChoice == "MAX" || runChoice == "MIN_MAX"; + + if (!runMin && !runMax) + { + throw std::invalid_argument("runChoice must be MIN, MAX, or MIN_MAX"); + } + + // clang-format off + + nvcv::DataType valueType = GetValueDataType(); + + nvcv::Tensor minVal({{shape.x}, "N"}, valueType); + nvcv::Tensor minLoc({{shape.x, maxLocs}, "NM"}, nvcv::TYPE_2S32); + nvcv::Tensor numMin({{shape.x}, "N"}, nvcv::TYPE_S32); + + nvcv::Tensor maxVal({{shape.x}, "N"}, valueType); + nvcv::Tensor maxLoc({{shape.x, maxLocs}, "NM"}, nvcv::TYPE_2S32); + nvcv::Tensor numMax({{shape.x}, "N"}, nvcv::TYPE_S32); + + // clang-format on + + // R/W bandwidth rationale: + // 1 read to find min/max + 1 read to collect their locations + // 1 or 2 writes of min/max values, locations (2S32) and quantity (S32) + const size_t inputBytes = static_cast(shape.x) * shape.y * shape.z * sizeof(T); + state.add_global_memory_reads(inputBytes * (isFakePlanar ? 3 : 2)); + state.add_global_memory_writes(shape.x * (GetValueDataSize() + maxLocs * sizeof(int2) + sizeof(int)) + * (static_cast(runMin) + static_cast(runMax)) + + (isFakePlanar ? inputBytes : 0)); + + cvcuda::MinMaxLoc op; + + const nvcv::Tensor noTensor{nullptr}; + + auto runOp = [&op, &runChoice, &minVal, &minLoc, &numMin, &maxVal, &maxLoc, &numMax, &noTensor](cudaStream_t s, + const auto &src) + { + if (runChoice == "MIN") + { + op(s, src, minVal, minLoc, numMin, noTensor, noTensor, noTensor); + } + else if (runChoice == "MAX") + { + op(s, src, noTensor, noTensor, noTensor, maxVal, maxLoc, numMax); + } + else + { + op(s, src, minVal, minLoc, numMin, maxVal, maxLoc, numMax); + } + }; + + // clang-format off + + if (inputKind == benchutils::InputKind::Tensor) + { + nvcv::Tensor src = (isPlanar || isFakePlanar) + ? nvcv::Tensor({{shape.x, 1, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, 1}, "NHWC"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::LcgValues()); + + if (isFakePlanar) + { + nvcv::Tensor interSrc({{shape.x, shape.y, shape.z, 1}, "NHWC"}, benchutils::GetDataType()); + cvcuda::Reformat reformatOp; + + benchutils::warmup_and_exec(state, BENCH_MINMAXLOC_WARMUP_ITERATIONS, + [&runOp, &reformatOp, &src, &interSrc](cudaStream_t s) { + reformatOp(s, src, interSrc); + runOp(s, interSrc); + }); + return; + } + + benchutils::warmup_and_exec(state, BENCH_MINMAXLOC_WARMUP_ITERATIONS, + [&runOp, &src](cudaStream_t s) { + runOp(s, src); + }); + } + else // zero and positive var shape means use ImageBatchVarShape + { + nvcv::ImageBatchVarShape src(shape.x); + + benchutils::FillImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}, + benchutils::LcgValues()); + + benchutils::warmup_and_exec(state, BENCH_MINMAXLOC_WARMUP_ITERATIONS, + [&runOp, &src](cudaStream_t s) { + runOp(s, src); + }); + } +} +CVCUDA_BENCH_SKIP_ERRORS(state) + +// clang-format on + +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(minmaxloc, NVBENCH_TYPE_AXES(BENCH_MINMAXLOC_TYPES)) +BENCH_MINMAXLOC_AXES; diff --git a/bench/cpp/ops/BenchMorphology.cpp b/bench/cpp/ops/BenchMorphology.cpp new file mode 100644 index 000000000..68393d54c --- /dev/null +++ b/bench/cpp/ops/BenchMorphology.cpp @@ -0,0 +1,281 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchMorphologyConfig.hpp" + +#include +#include + +#include + +#include + +namespace { + +NVCVMorphologyType GetMorphologyType(const std::string &morphTypeStr) +{ + if (morphTypeStr == "ERODE") + { + return NVCV_ERODE; + } + if (morphTypeStr == "DILATE") + { + return NVCV_DILATE; + } + if (morphTypeStr == "OPEN") + { + return NVCV_OPEN; + } + if (morphTypeStr == "CLOSE") + { + return NVCV_CLOSE; + } + throw std::invalid_argument("Unexpected morphology type = " + morphTypeStr); +} + +bool NeedsWorkspace(NVCVMorphologyType morphType, int iteration) +{ + return morphType == NVCV_OPEN || morphType == NVCV_CLOSE || iteration > 1; +} + +void AddMemoryCounters(nvbench::state &state, int3 shape, long elementSize, int channels, int bwIteration, + bool isFakePlanar) +{ + const long bytes = static_cast(shape.x) * shape.y * shape.z * channels * elementSize * bwIteration; + const long scale = isFakePlanar ? 3 : 1; + + state.add_global_memory_reads(scale * bytes); + state.add_global_memory_writes(scale * bytes); +} + +template +nvcv::Tensor MakeTensor(int3 shape, int channels, bool isPlanar) +{ + return isPlanar ? nvcv::Tensor( + { + {shape.x, channels, shape.y, shape.z}, + "NCHW" + }, + benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, channels}, "NHWC"}, benchutils::GetDataType()); +} + +template +void RunFakePlanarTensor(nvbench::state &state, cvcuda::Morphology &op, int3 shape, int channels, bool needsWorkspace, + NVCVMorphologyType morphType, nvcv::Size2D mask, int2 anchor, int iteration, + NVCVBorderType borderType) +{ + nvcv::Tensor src( + { + {shape.x, channels, shape.y, shape.z}, + "NCHW" + }, + benchutils::GetDataType()); + nvcv::Tensor interSrc( + { + {shape.x, shape.y, shape.z, channels}, + "NHWC" + }, + benchutils::GetDataType()); + nvcv::Tensor interDst( + { + {shape.x, shape.y, shape.z, channels}, + "NHWC" + }, + benchutils::GetDataType()); + nvcv::Tensor dst( + { + {shape.x, channels, shape.y, shape.z}, + "NCHW" + }, + benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + nvcv::Tensor workspace{nullptr}; + if (needsWorkspace) + { + workspace = nvcv::Tensor( + { + {shape.x, shape.y, shape.z, channels}, + "NHWC" + }, + benchutils::GetDataType()); + } + + const nvcv::OptionalTensorConstRef workspaceRef + = workspace ? nvcv::OptionalTensorConstRef{std::cref(workspace)} : nvcv::OptionalTensorConstRef{nvcv::NullOpt}; + + cvcuda::Reformat reformatOp; + + benchutils::warmup_and_exec(state, BENCH_MORPHOLOGY_WARMUP_ITERATIONS, + [&op, &reformatOp, &src, &interSrc, &interDst, &dst, &workspaceRef, morphType, mask, + anchor, iteration, borderType](cudaStream_t s) + { + reformatOp(s, src, interSrc); + op(s, interSrc, interDst, workspaceRef, morphType, mask, anchor, iteration, + borderType); + reformatOp(s, interDst, dst); + }); +} + +template +void RunTensor(nvbench::state &state, cvcuda::Morphology &op, int3 shape, int channels, bool isPlanar, + bool isFakePlanar, bool needsWorkspace, NVCVMorphologyType morphType, nvcv::Size2D mask, int2 anchor, + int iteration, NVCVBorderType borderType) +{ + if (isFakePlanar) + { + RunFakePlanarTensor(state, op, shape, channels, needsWorkspace, morphType, mask, anchor, iteration, + borderType); + return; + } + + nvcv::Tensor src = MakeTensor(shape, channels, isPlanar); + nvcv::Tensor dst = MakeTensor(shape, channels, isPlanar); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + nvcv::Tensor workspace{nullptr}; + if (needsWorkspace) + { + workspace = MakeTensor(shape, channels, isPlanar); + } + + const nvcv::OptionalTensorConstRef workspaceRef + = workspace ? nvcv::OptionalTensorConstRef{std::cref(workspace)} : nvcv::OptionalTensorConstRef{nvcv::NullOpt}; + + benchutils::warmup_and_exec( + state, BENCH_MORPHOLOGY_WARMUP_ITERATIONS, + [&op, &src, &dst, &workspaceRef, morphType, mask, anchor, iteration, borderType](cudaStream_t s) + { op(s, src, dst, workspaceRef, morphType, mask, anchor, iteration, borderType); }); +} + +template +void RunVarShape(nvbench::state &state, cvcuda::Morphology &op, int3 shape, bool isPlanar, bool needsWorkspace, + NVCVMorphologyType morphType, nvcv::Size2D mask, int2 anchor, int iteration, NVCVBorderType borderType) +{ + nvcv::ImageBatchVarShape src(shape.x); + nvcv::ImageBatchVarShape dst(shape.x); + + if (isPlanar) + { + benchutils::FillPlanarImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}); + benchutils::FillPlanarImageBatchLike(dst, src); + } + else + { + benchutils::FillImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}, benchutils::CheckerboardValues()); + // Use FillImageBatchLike to ensure dst has same per-sample shapes as src. + benchutils::FillImageBatchLike(dst, src, [](const long4_16a &) { return T{0}; }); + } + + nvcv::Tensor maskTensor({{shape.x}, "N"}, nvcv::TYPE_2S32); + nvcv::Tensor anchorTensor({{shape.x}, "N"}, nvcv::TYPE_2S32); + + benchutils::FillTensor(maskTensor, [mask](const long4_16a &) { return int2{mask.w, mask.h}; }); + benchutils::FillTensor(anchorTensor, [anchor](const long4_16a &) { return anchor; }); + + nvcv::ImageBatchVarShape workspace{nullptr}; + if (needsWorkspace) + { + workspace = nvcv::ImageBatchVarShape(shape.x); + if (isPlanar) + { + benchutils::FillPlanarImageBatchLike(workspace, src); + } + else + { + // Create separate workspace images with no memory aliasing with dst. + benchutils::FillImageBatch(workspace, long2{shape.z, shape.y}, long2{0, 0}, + [](const long4_16a &) { return T{0}; }); + } + } + + const nvcv::OptionalImageBatchVarShapeConstRef workspaceRef + = workspace ? nvcv::OptionalImageBatchVarShapeConstRef{std::cref(workspace)} + : nvcv::OptionalImageBatchVarShapeConstRef{nvcv::NullOpt}; + + benchutils::warmup_and_exec( + state, BENCH_MORPHOLOGY_WARMUP_ITERATIONS, + [&op, &src, &dst, &workspaceRef, morphType, &maskTensor, &anchorTensor, iteration, borderType](cudaStream_t s) + { op(s, src, dst, workspaceRef, morphType, maskTensor, anchorTensor, iteration, borderType); }); +} + +} // namespace + +template +inline void morphology(nvbench::state &state, nvbench::type_list) +try +{ + int3 shape = benchutils::GetShape<3, int3>(state.get_string("shape")); + auto layout = benchutils::GetStringParam(state, "layout", "NHWC"); + const benchutils::InputKind inputKind = benchutils::GetInputKind(state.get_string("inputKind")); + int iteration = benchutils::GetIntParam(state, "iteration"); + int2 kernelSize = nvcv::cuda::StaticCast(benchutils::GetShape<2>(state.get_string("kernelSize"))); + + NVCVBorderType borderType = benchutils::GetBorderType(state.get_string("border")); + NVCVMorphologyType morphType = GetMorphologyType(state.get_string("morphType")); + + nvcv::Size2D mask{kernelSize.x, kernelSize.y}; + int2 anchor{-1, -1}; + + int bwIteration = (morphType == NVCV_OPEN || morphType == NVCV_CLOSE || iteration > 1) ? 2 * iteration : iteration; + + const bool needsWorkspace = NeedsWorkspace(morphType, iteration); + const bool isPlanar = layout == "NCHW"; + const bool isFakePlanar = layout == "NCHW_FAKE"; + if (layout != "NHWC" && layout != "NCHW" && layout != "NCHW_FAKE") + { + state.skip("Morphology benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts"); + return; + } + if (isFakePlanar && inputKind == benchutils::InputKind::VarShape) + { + state.skip("Fake-planar (NCHW_FAKE) Morphology benchmark is tensor-only"); + return; + } + + using BT = typename nvcv::cuda::BaseType; + + int ch = nvcv::cuda::NumElements; + + AddMemoryCounters(state, shape, sizeof(BT), ch, bwIteration, isFakePlanar); + + cvcuda::Morphology op; + + // clang-format off + + if (inputKind == benchutils::InputKind::Tensor) + { + RunTensor(state, op, shape, ch, isPlanar, isFakePlanar, needsWorkspace, morphType, mask, anchor, + iteration, borderType); + } + else // zero and positive var shape means use ImageBatchVarShape + { + RunVarShape(state, op, shape, isPlanar, needsWorkspace, morphType, mask, anchor, iteration, borderType); + } +} +CVCUDA_BENCH_SKIP_ERRORS(state) + +// clang-format on + +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(morphology, NVBENCH_TYPE_AXES(BENCH_MORPHOLOGY_TYPES)) +BENCH_MORPHOLOGY_AXES; diff --git a/bench/cpp/ops/BenchNonMaximumSuppression.cpp b/bench/cpp/ops/BenchNonMaximumSuppression.cpp new file mode 100644 index 000000000..cf46dbdbe --- /dev/null +++ b/bench/cpp/ops/BenchNonMaximumSuppression.cpp @@ -0,0 +1,71 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchNonMaximumSuppressionConfig.hpp" + +#include + +#include + +template +inline void nms(nvbench::state &state, nvbench::type_list) +try +{ + int2 shape = benchutils::GetShape<2, int2>(state.get_string("shape")); + auto scThr = static_cast(state.get_float64("scoreThreshold")); + auto iouThr = static_cast(state.get_float64("iouThreshold")); + + // R/W bandwidth rationale: + // 1 read of scores (F32) to mask out lower scores boxes + 1 read of boxes (4S16) for IoU threshold + // 2 writes of masks (U8) by score and IoU thresholds + state.add_global_memory_reads(shape.x * shape.y * (sizeof(T) + sizeof(S))); + state.add_global_memory_writes(shape.x * shape.y * sizeof(M) * 2); + + cvcuda::NonMaximumSuppression op; + + // clang-format off + + // Config only specifies the Tensor variant (inputKind=Tensor); no ImageBatchVarShape support + nvcv::Tensor srcBB({{shape.x, shape.y}, "NB"}, benchutils::GetDataType()); + nvcv::Tensor srcSc({{shape.x, shape.y}, "NB"}, benchutils::GetDataType()); + nvcv::Tensor dstMk({{shape.x, shape.y}, "NB"}, benchutils::GetDataType()); + + // srcBB: deterministic per-element [10, 50] cycle. The Python bench + // mirrors this with `10 + (b*7 + c*11 + n*3) % 41` over (N, num_boxes, 4), + // giving bit-identical bytes for the box coordinates. + benchutils::FillTensor(srcBB, [](const long4_16a &c) { + long n = c.x; + long b = c.y; + auto v = [&b, &n](long ch) { return (short)(10 + (b * 7 + ch * 11 + n * 3) % 41); }; + return T{v(0), v(1), v(2), v(3)}; + }); + benchutils::FillTensor(srcSc, benchutils::LcgValues()); + // Output tensor - zero-fill (output only, no need for random values) + benchutils::FillTensor(dstMk, [](const long4_16a &) { return M{0}; }); + + benchutils::warmup_and_exec(state, BENCH_NONMAXIMUMSUPPRESSION_WARMUP_ITERATIONS, + [&op, &srcBB, &dstMk, &srcSc, &scThr, &iouThr](cudaStream_t s) { op(s, srcBB, dstMk, srcSc, scThr, iouThr); }); +} +CVCUDA_BENCH_SKIP_ERRORS(state) + +// clang-format on + +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(nms, NVBENCH_TYPE_AXES(BENCH_NONMAXIMUMSUPPRESSION_TYPES)) +BENCH_NONMAXIMUMSUPPRESSION_AXES; diff --git a/bench/cpp/ops/BenchNormalize.cpp b/bench/cpp/ops/BenchNormalize.cpp new file mode 100644 index 000000000..6cb49efa3 --- /dev/null +++ b/bench/cpp/ops/BenchNormalize.cpp @@ -0,0 +1,211 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchNormalizeConfig.hpp" + +#include +#include + +#include + +template +nvcv::Tensor MakeNormalizeTensor(const long3 &shape, int channels, bool isPlanar) +{ + if (isPlanar) + { + return nvcv::Tensor( + { + {shape.x, channels, shape.y, shape.z}, + "NCHW" + }, + benchutils::GetDataType()); + } + return nvcv::Tensor( + { + {shape.x, shape.y, shape.z, channels}, + "NHWC" + }, + benchutils::GetDataType()); +} + +template +void RunScalarNormalizeBench(nvbench::state &state, cvcuda::Normalize &op, const long3 &srcShape, const long3 &dstShape, + int channels, bool isPlanar, bool isFakePlanar, float globalScale, float globalShift, + float epsilon, uint32_t flags) +{ + if (isFakePlanar) + { + state.skip("Scalar-parameter normalize benchmark supports only native NHWC and NCHW tensors"); + return; + } + + auto src = MakeNormalizeTensor(srcShape, channels, isPlanar); + auto dst = MakeNormalizeTensor(dstShape, channels, isPlanar); + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + const float4 baseValue = {0.25f, 0.5f, 0.75f, 1.f}; + const float4 scaleValue = {1.25f, 1.5f, 1.75f, 2.f}; + + benchutils::warmup_and_exec( + state, BENCH_NORMALIZE_WARMUP_ITERATIONS, + [&op, &src, &baseValue, &scaleValue, channels, &dst, globalScale, globalShift, epsilon, + flags](cudaStream_t stream) + { op(stream, src, baseValue, scaleValue, channels, channels, dst, globalScale, globalShift, epsilon, flags); }); +} + +template +inline void normalize(nvbench::state &state, nvbench::type_list) +try +{ + long3 srcShape = benchutils::GetShape<3>(state.get_string("shape")); + auto layout = benchutils::GetStringParam(state, "layout", "NHWC"); + const std::string inputKindName = state.get_string("inputKind"); + const bool scalarParams = inputKindName == "TensorScalar"; + const benchutils::InputKind inputKind + = scalarParams ? benchutils::InputKind::Tensor : benchutils::GetInputKind(inputKindName); + long3 dstShape = srcShape; + + float globalScale = 1.234f; + float globalShift = 2.345f; + float epsilon = 12.34f; + uint32_t flags = CVCUDA_NORMALIZE_SCALE_IS_STDDEV; + + using BT = typename nvcv::cuda::BaseType; + + int ch = nvcv::cuda::NumElements; + + const bool isPlanar = layout == "NCHW"; + const bool isFakePlanar = layout == "NCHW_FAKE"; + if (layout != "NHWC" && layout != "NCHW" && layout != "NCHW_FAKE") + { + state.skip("Normalize benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts"); + return; + } + // NCHW_FAKE ("fake planar") is a tensor-only comparison path: planar data is reformatted to + // interleaved, normalized with the interleaved kernel, and reformatted back to planar — all timed + // together — so the native planar path (NCHW) can be shown to be faster than this naive + // convert→normalize→convert pipeline. + if (isFakePlanar && inputKind == benchutils::InputKind::VarShape) + { + state.skip("Fake-planar (NCHW_FAKE) normalize benchmark is tensor-only"); + return; + } + + const long paramSamples = (isPlanar && inputKind == benchutils::InputKind::VarShape) ? 1 : srcShape.x; + const long paramBytes = scalarParams ? 0 : paramSamples * ch * sizeof(float) * 2; + const long bytes = srcShape.x * srcShape.y * srcShape.z * sizeof(T); + if (isFakePlanar) + { + // reformat(NCHW→NHWC) + normalize + reformat(NHWC→NCHW): reads src + interleaved src + dst + // (plus base/scale once), writes interleaved src + interleaved dst + dst. Normalize preserves + // size, so every transfer moves `bytes`. + state.add_global_memory_reads(3 * bytes + paramBytes); + state.add_global_memory_writes(3 * bytes); + } + else + { + state.add_global_memory_reads(bytes + paramBytes); + state.add_global_memory_writes(dstShape.x * dstShape.y * dstShape.z * sizeof(T)); + } + + cvcuda::Normalize op; + + // clang-format off + + if (scalarParams) + { + RunScalarNormalizeBench(state, op, srcShape, dstShape, ch, isPlanar, isFakePlanar, globalScale, + globalShift, epsilon, flags); + return; + } + + // Fake-planar runs the interleaved kernel, so its base/scale are interleaved (NHWC) like the + // interleaved path; only native planar (NCHW) uses planar params. + nvcv::Tensor base = isPlanar ? nvcv::Tensor({{paramSamples, ch, 1, 1}, "NCHW"}, nvcv::TYPE_F32) + : nvcv::Tensor({{paramSamples, 1, 1, ch}, "NHWC"}, nvcv::TYPE_F32); + nvcv::Tensor scale = isPlanar ? nvcv::Tensor({{paramSamples, ch, 1, 1}, "NCHW"}, nvcv::TYPE_F32) + : nvcv::Tensor({{paramSamples, 1, 1, ch}, "NHWC"}, nvcv::TYPE_F32); + + // base, scale: default-range LcgValues() so the GPU LCG fast + // path produces float [-1, +1] bytes identical to the Python bench's + // create_tensor(..., fill_mode="lcg"). + benchutils::FillTensor(base, benchutils::LcgValues()); + benchutils::FillTensor(scale, benchutils::LcgValues()); + + if (isFakePlanar) // tensor-only: planar→interleaved→normalize→interleaved→planar + { + nvcv::Tensor src ({{srcShape.x, ch, srcShape.y, srcShape.z}, "NCHW"}, benchutils::GetDataType()); + nvcv::Tensor interSrc({{srcShape.x, srcShape.y, srcShape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor interDst({{dstShape.x, dstShape.y, dstShape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst ({{dstShape.x, ch, dstShape.y, dstShape.z}, "NCHW"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + cvcuda::Reformat reformatOp; + + benchutils::warmup_and_exec(state, BENCH_NORMALIZE_WARMUP_ITERATIONS, + [&op, &reformatOp, &src, &interSrc, &interDst, &dst, &base, &scale, &globalScale, &globalShift, &epsilon, &flags](cudaStream_t s) { + reformatOp(s, src, interSrc); // NCHW → NHWC + op(s, interSrc, base, scale, interDst, globalScale, globalShift, epsilon, flags); // interleaved + reformatOp(s, interDst, dst); // NHWC → NCHW + }); + } + else if (inputKind == benchutils::InputKind::Tensor) + { + auto src = MakeNormalizeTensor(srcShape, ch, isPlanar); + auto dst = MakeNormalizeTensor(dstShape, ch, isPlanar); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + benchutils::warmup_and_exec(state, BENCH_NORMALIZE_WARMUP_ITERATIONS, + [&op, &src, &base, &scale, &dst, &globalScale, &globalShift, &epsilon, &flags](cudaStream_t s) { + op(s, src, base, scale, dst, globalScale, globalShift, epsilon, flags); + }); + } + else // zero and positive var shape means use ImageBatchVarShape + { + nvcv::ImageBatchVarShape src(static_cast(srcShape.x)); + nvcv::ImageBatchVarShape dst(static_cast(dstShape.x)); + + if (isPlanar) + { + benchutils::FillPlanarImageBatch(src, long2{srcShape.z, srcShape.y}, long2{0, 0}); + benchutils::FillPlanarImageBatchLike(dst, src); + } + else + { + benchutils::FillImageBatch(src, long2{srcShape.z, srcShape.y}, long2{0, 0}, + benchutils::CheckerboardValues()); + // Use FillImageBatchLike to ensure dst has same per-sample shapes as src + benchutils::FillImageBatchLike(dst, src, [](const long4_16a &) { return T{0}; }); + } + + benchutils::warmup_and_exec(state, BENCH_NORMALIZE_WARMUP_ITERATIONS, + [&op, &src, &base, &scale, &dst, &globalScale, &globalShift, &epsilon, &flags](cudaStream_t s) { + op(s, src, base, scale, dst, globalScale, globalShift, epsilon, flags); + }); + } +} +CVCUDA_BENCH_SKIP_ERRORS(state) + +// clang-format on + +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(normalize, NVBENCH_TYPE_AXES(BENCH_NORMALIZE_TYPES)) +BENCH_NORMALIZE_AXES; diff --git a/bench/cpp/ops/BenchOSD.cpp b/bench/cpp/ops/BenchOSD.cpp new file mode 100644 index 000000000..a08a1fc85 --- /dev/null +++ b/bench/cpp/ops/BenchOSD.cpp @@ -0,0 +1,194 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchOSDConfig.hpp" + +#include +#include +#include + +#include + +#include +#include +#include +#include + +class BenchOSDError : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + +template +inline void osd(nvbench::state &state, nvbench::type_list) +try +{ + int3 shape = benchutils::GetShape<3, int3>(state.get_string("shape")); + const benchutils::InputKind inputKind = benchutils::GetInputKind(state.get_string("inputKind")); + int numElem = benchutils::GetIntParam(state, "numElem"); + auto elemType = state.get_string("elementType"); + auto layout = benchutils::GetStringParam(state, "layout", "NHWC"); + + int ch = nvcv::cuda::NumElements; + + using BT = nvcv::cuda::BaseType; + + const bool isPlanar = layout == "NCHW"; + const bool isFakePlanar = layout == "NCHW_FAKE"; + if (layout != "NHWC" && layout != "NCHW" && layout != "NCHW_FAKE") + { + state.skip("OSD benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts"); + return; + } + if ((isPlanar || isFakePlanar) && inputKind != benchutils::InputKind::Tensor) + { + state.skip("Planar OSD benchmark is tensor-only"); + return; + } + + NVCVPoint point; + point.centerPos.x = shape.z / 2; + point.centerPos.y = shape.y / 2; + point.radius = std::min(shape.z, shape.y) / 2; + point.color = {0, 0, 0, 255}; + + NVCVBndBoxI bndBox; + bndBox.box.x = shape.z / 4; + bndBox.box.y = shape.y / 4; + bndBox.box.width = shape.z / 2; + bndBox.box.height = shape.y / 2; + bndBox.thickness = 3; + bndBox.fillColor = {0, 128, 255, 0}; + bndBox.borderColor = {255, 255, 0, 255}; + + NVCVLine line; + line.pos0.x = shape.z / 8; + line.pos0.y = shape.y / 8; + line.pos1.x = 7 * shape.z / 8; + line.pos1.y = 7 * shape.y / 8; + line.thickness = 4; + line.color = {255, 0, 0, 255}; + line.interpolation = true; + + NVCVCircle circle; + circle.centerPos.x = shape.z / 2; + circle.centerPos.y = shape.y / 2; + circle.radius = std::min(shape.z, shape.y) / 4; + circle.thickness = 4; + circle.borderColor = {0, 255, 255, 255}; + circle.bgColor = {255, 0, 255, 0}; + + NVCVOSDType osdType = NVCV_OSD_POINT; + const void *payload = &point; + if (elemType == "RECT") + { + osdType = NVCV_OSD_RECT; + payload = &bndBox; + } + else if (elemType == "LINE") + { + osdType = NVCV_OSD_LINE; + payload = &line; + } + else if (elemType == "CIRCLE") + { + osdType = NVCV_OSD_CIRCLE; + payload = &circle; + } + else if (elemType != "POINT") + { + throw std::invalid_argument("Unsupported OSD elementType: " + elemType); + } + + const int32_t totalElems = shape.x * numElem; + std::vector types(totalElems, osdType); + std::vector payloads(totalElems, payload); + std::vector numElementsPerBatch(shape.x, numElem); + + NVCVElements ctxHandle = nullptr; + if (nvcvElementsConstruct(&ctxHandle, types.data(), payloads.data(), numElementsPerBatch.data(), shape.x) + != NVCV_SUCCESS) + { + throw BenchOSDError("nvcvElementsConstruct failed"); + } + auto ctxGuard = std::unique_ptr( + ctxHandle, [](NVCVElements h) { nvcvElementsDestroy(h); }); + + const long imageBytes = static_cast(shape.x) * shape.y * shape.z * sizeof(T); + const long elemBytes = static_cast(numElem) * sizeof(int) * 16; + if (isPlanar || isFakePlanar) + { + state.add_global_memory_reads(3 * imageBytes + elemBytes); + state.add_global_memory_writes(3 * imageBytes); + } + else + { + state.add_global_memory_reads(imageBytes + elemBytes); + state.add_global_memory_writes(imageBytes); + } + + cvcuda::OSD op; + + // clang-format off + + if (isFakePlanar) + { + nvcv::Tensor src ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + nvcv::Tensor interSrc({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor interDst({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + cvcuda::Reformat reformatOp; + + benchutils::warmup_and_exec(state, BENCH_OSD_WARMUP_ITERATIONS, + [&op, &reformatOp, &src, &interSrc, &interDst, &dst, &ctxHandle](cudaStream_t s) { + reformatOp(s, src, interSrc); + op(s, interSrc, interDst, ctxHandle); + reformatOp(s, interDst, dst); + }); + } + else if (inputKind == benchutils::InputKind::Tensor) + { + nvcv::Tensor src = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + benchutils::warmup_and_exec(state, BENCH_OSD_WARMUP_ITERATIONS, + [&op, &src, &dst, &ctxHandle](cudaStream_t s) { op(s, src, dst, ctxHandle); }); + } + else // zero and positive var shape means use ImageBatchVarShape + { + throw std::invalid_argument("ImageBatchVarShape not implemented for this operator"); + } +} +CVCUDA_BENCH_SKIP_ERRORS(state) + +// clang-format on + +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(osd, NVBENCH_TYPE_AXES(BENCH_OSD_TYPES)) +BENCH_OSD_AXES; diff --git a/bench/cpp/ops/BenchPadAndStack.cpp b/bench/cpp/ops/BenchPadAndStack.cpp new file mode 100644 index 000000000..f78982c64 --- /dev/null +++ b/bench/cpp/ops/BenchPadAndStack.cpp @@ -0,0 +1,125 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchPadAndStackConfig.hpp" + +#include + +#include + +#include + +template +inline void padandstack(nvbench::state &state, nvbench::type_list) +try +{ + int3 shape = benchutils::GetShape<3, int3>(state.get_string("shape")); + const benchutils::InputKind inputKind = benchutils::GetInputKind(state.get_string("inputKind")); + auto layout = benchutils::GetStringParam(state, "layout", "NHWC"); + int pad = benchutils::GetIntParam(state, "pad"); + const bool isPlanar = layout == "NCHW"; + if (layout != "NHWC" && layout != "NCHW") + { + state.skip("PadAndStack benchmark supports only NHWC and NCHW layouts"); + return; + } + if (isPlanar && nvcv::cuda::NumElements == 2) + { + state.skip("Planar PadAndStack benchmark does not support 2-channel layouts"); + return; + } + if (isPlanar && nvcv::cuda::NumElements == 1) + { + state.skip("Single-channel PadAndStack has no distinct planar image-batch format"); + return; + } + + auto numBatches = shape.x; + auto srcHeight = shape.y; + auto srcWidth = shape.z; + + int dstHeight = srcHeight + 2 * pad; + int dstWidth = srcWidth + 2 * pad; + + NVCVBorderType borderType = benchutils::GetBorderType(state.get_string("border")); + float borderValue{0.f}; + + // Memory accounting: read all src images + top/left, write dst tensor + state.add_global_memory_reads(numBatches * srcHeight * srcWidth * sizeof(T) + numBatches * sizeof(int) * 2); + state.add_global_memory_writes(numBatches * dstHeight * dstWidth * sizeof(T)); + + // PadAndStack only supports ImageBatchVarShape input + if (inputKind == benchutils::InputKind::Tensor) + { + state.skip("PadAndStack only supports ImageBatchVarShape input (inputKind=VarShape)"); + return; + } + + nvcv::ImageBatchVarShape src(numBatches); + if (isPlanar) + { + benchutils::FillPlanarImageBatch(src, long2{srcWidth, srcHeight}, long2{0, 0}); + } + else + { + benchutils::FillImageBatch(src, long2{srcWidth, srcHeight}, long2{0, 0}, + benchutils::CheckerboardValues()); + } + + using BT = typename nvcv::cuda::BaseType; + constexpr int channels = nvcv::cuda::NumElements; + nvcv::Tensor dst + = isPlanar ? nvcv::Tensor( + { + {numBatches, channels, dstHeight, dstWidth}, + "NCHW" + }, + benchutils::GetDataType()) + : nvcv::Tensor({{numBatches, dstHeight, dstWidth, channels}, "NHWC"}, benchutils::GetDataType()); + + // Create top/left padding tensors (OLD style - 4D NHWC) + nvcv::Tensor top( + { + {numBatches, 1, 1, 1}, + "NHWC" + }, + nvcv::TYPE_S32); + nvcv::Tensor left( + { + {numBatches, 1, 1, 1}, + "NHWC" + }, + nvcv::TYPE_S32); + + // Fill top/left using FillTensor (OLD style) + benchutils::FillTensor(top, [dstHeight, srcHeight](const long4_16a &) { return (dstHeight - srcHeight) / 2; }); + benchutils::FillTensor(left, [dstWidth, srcWidth](const long4_16a &) { return (dstWidth - srcWidth) / 2; }); + + // Create operator + cvcuda::PadAndStack op; + + benchutils::warmup_and_exec(state, BENCH_PADANDSTACK_WARMUP_ITERATIONS, + [&op, &src, &dst, &top, &left, &borderType, &borderValue](cudaStream_t s) + { op(s, src, dst, top, left, borderType, borderValue); }); +} + +CVCUDA_BENCH_SKIP_ERRORS(state) + +// Use auto-generated type list from bench_params.json +NVBENCH_BENCH_TYPES(padandstack, NVBENCH_TYPE_AXES(BENCH_PADANDSTACK_TYPES)) +BENCH_PADANDSTACK_AXES; diff --git a/bench/BenchPairwiseMatcher.cpp b/bench/cpp/ops/BenchPairwiseMatcher.cpp similarity index 62% rename from bench/BenchPairwiseMatcher.cpp rename to bench/cpp/ops/BenchPairwiseMatcher.cpp index 3119ee2e9..7afd040ed 100644 --- a/bench/BenchPairwiseMatcher.cpp +++ b/bench/cpp/ops/BenchPairwiseMatcher.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,19 +15,20 @@ * limitations under the License. */ -#include "BenchUtils.hpp" +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchPairwiseMatcherConfig.hpp" #include #include template -inline void PairwiseMatcher(nvbench::state &state, nvbench::type_list) +inline void pairwisematcher(nvbench::state &state, nvbench::type_list) try { - long3 shape = benchutils::GetShape<3>(state.get_string("shape")); + int3 shape = benchutils::GetShape<3, int3>(state.get_string("shape")); - int matchesPerPoint = static_cast(state.get_int64("matchesPerPoint")); + int matchesPerPoint = benchutils::GetIntParam(state, "matchesPerPoint"); bool crossCheck = state.get_string("crossCheck") == "T"; bool readNumSets = state.get_string("readNumSets") == "T"; @@ -62,7 +63,9 @@ try nvcv::Tensor numMatches({{shape.x}, "N"}, nvcv::TYPE_S32); - nvcv::Tensor numSet1, numSet2, distances; + nvcv::Tensor numSet1; + nvcv::Tensor numSet2; + nvcv::Tensor distances; if (readNumSets) { @@ -77,32 +80,21 @@ try distances = nvcv::Tensor({{shape.x, maxMatches}, "NM"}, nvcv::TYPE_F32); } - benchutils::FillTensor(set1, benchutils::RandomValues()); - benchutils::FillTensor(set2, benchutils::RandomValues()); + benchutils::FillTensor(set1, benchutils::LcgValues()); + benchutils::FillTensor(set2, benchutils::LcgValues()); - state.exec(nvbench::exec_tag::sync, - [&op, &set1, &set2, &numSet1, &numSet2, &matches, &numMatches, &distances, &crossCheck, - &matchesPerPoint, &normType](nvbench::launch &launch) - { - op(launch.get_stream(), set1, set2, numSet1, numSet2, matches, numMatches, distances, crossCheck, - matchesPerPoint, normType); - }); -} -catch (const std::exception &err) -{ - state.skip(err.what()); + benchutils::warmup_and_exec(state, BENCH_PAIRWISEMATCHER_WARMUP_ITERATIONS, + [&op, &set1, &set2, &numSet1, &numSet2, &matches, &numMatches, &distances, &crossCheck, + &matchesPerPoint, &normType](cudaStream_t s) { + op(s, set1, set2, numSet1, numSet2, matches, numMatches, distances, crossCheck, + matchesPerPoint, normType); + }); } +CVCUDA_BENCH_SKIP_ERRORS(state) // clang-format on -using PairwiseMatcherTypes = nvbench::type_list; - -NVBENCH_BENCH_TYPES(PairwiseMatcher, NVBENCH_TYPE_AXES(PairwiseMatcherTypes)) - .set_type_axes_names({"InOutDataType"}) - .add_string_axis("shape", {"1x10000x32"}) - .add_int64_axis("matchesPerPoint", {1}) - .add_string_axis("crossCheck", {"T"}) - .add_string_axis("readNumSets", {"F"}) - .add_string_axis("writeDistances", {"T"}) - .add_string_axis("normType", {"HAMMING"}) - .add_string_axis("algoChoice", {"BRUTE_FORCE"}); +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(pairwisematcher, NVBENCH_TYPE_AXES(BENCH_PAIRWISEMATCHER_TYPES)) +BENCH_PAIRWISEMATCHER_AXES; diff --git a/bench/cpp/ops/BenchPillowResize.cpp b/bench/cpp/ops/BenchPillowResize.cpp new file mode 100644 index 000000000..f1a137757 --- /dev/null +++ b/bench/cpp/ops/BenchPillowResize.cpp @@ -0,0 +1,150 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchPillowResizeConfig.hpp" + +#include +#include + +#include + +template +inline void pillowresize(nvbench::state &state, nvbench::type_list) +try +{ + long3 srcShape = benchutils::GetShape<3>(state.get_string("shape")); + auto layout = benchutils::GetStringParam(state, "layout", "NHWC"); + const benchutils::InputKind inputKind = benchutils::GetInputKind(state.get_string("inputKind")); + + NVCVInterpolationType interpType = benchutils::GetInterpolationType(state.get_string("interpolation")); + + const bool isPlanar = layout == "NCHW"; + const bool isFakePlanar = layout == "NCHW_FAKE"; + if (layout != "NHWC" && layout != "NCHW" && layout != "NCHW_FAKE") + { + state.skip("PillowResize benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts"); + return; + } + // NCHW_FAKE ("fake planar") is a tensor-only comparison path: planar data is + // reformatted to interleaved, resized with the interleaved kernel, and reformatted + // back to planar — all timed together — so the native planar path (NCHW) can be + // shown to be faster than this naive convert->resize->convert pipeline. + if (isFakePlanar && inputKind != benchutils::InputKind::Tensor) + { + state.skip("Fake-planar (NCHW_FAKE) PillowResize benchmark is tensor-only"); + return; + } + + const std::string resizeType = state.get_string("resizeType"); + long3 dstShape = benchutils::GetResizeOutputShape(srcShape, resizeType); + + nvcv::Size2D srcSize{static_cast(srcShape.z), static_cast(srcShape.y)}; + nvcv::Size2D dstSize{static_cast(dstShape.z), static_cast(dstShape.y)}; + + using BT = typename nvcv::cuda::BaseType; + const int ch = nvcv::cuda::NumElements; + + // Planar (NCHW) profiles use a packed T whose planar image format carries one plane per channel; + // interleaved (NHWC) profiles use the matching interleaved format for T (single-channel U8/F32, + // RGB uchar3/float3, or RGBA uchar4/float4). Either way the per-channel scalar type is BT. + nvcv::DataType scalarDType{benchutils::GetDataType()}; + nvcv::ImageFormat fmt = isPlanar ? benchutils::GetPlanarFormat() : benchutils::GetFormat(); + + const long srcBytes = srcShape.x * srcShape.y * srcShape.z * static_cast(sizeof(T)); + const long dstBytes = dstShape.x * dstShape.y * dstShape.z * static_cast(sizeof(T)); + if (isFakePlanar) + { + // reformat(NCHW->NHWC) + resize + reformat(NHWC->NCHW): reads src twice + dst once, + // writes the interleaved src once + dst twice. + state.add_global_memory_reads(2 * srcBytes + dstBytes); + state.add_global_memory_writes(srcBytes + 2 * dstBytes); + } + else + { + state.add_global_memory_reads(srcBytes); + state.add_global_memory_writes(dstBytes); + } + + cvcuda::PillowResize op; + cvcuda::UniqueWorkspace ws = cvcuda::AllocateWorkspace( + op.getWorkspaceRequirements(static_cast(srcShape.x), srcSize, dstSize, fmt)); + + // clang-format off + + if (isFakePlanar) // tensor-only: planar->interleaved->resize->interleaved->planar + { + nvcv::Tensor src ({{srcShape.x, ch, srcShape.y, srcShape.z}, "NCHW"}, scalarDType); + nvcv::Tensor interSrc({{srcShape.x, srcShape.y, srcShape.z, ch}, "NHWC"}, scalarDType); + nvcv::Tensor interDst({{dstShape.x, dstShape.y, dstShape.z, ch}, "NHWC"}, scalarDType); + nvcv::Tensor dst ({{dstShape.x, ch, dstShape.y, dstShape.z}, "NCHW"}, scalarDType); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + cvcuda::Reformat reformatOp; + + benchutils::warmup_and_exec(state, BENCH_PILLOWRESIZE_WARMUP_ITERATIONS, + [&op, &reformatOp, &ws, &src, &interSrc, &interDst, &dst, &interpType](cudaStream_t s) { + reformatOp(s, src, interSrc); // NCHW -> NHWC + op(s, ws.get(), interSrc, interDst, interpType); // interleaved PillowResize + reformatOp(s, interDst, dst); // NHWC -> NCHW + }); + } + else if (inputKind == benchutils::InputKind::Tensor) + { + nvcv::Tensor src = isPlanar + ? nvcv::Tensor({{srcShape.x, ch, srcShape.y, srcShape.z}, "NCHW"}, scalarDType) + : nvcv::Tensor({{srcShape.x, srcShape.y, srcShape.z, ch}, "NHWC"}, scalarDType); + nvcv::Tensor dst = isPlanar + ? nvcv::Tensor({{dstShape.x, ch, dstShape.y, dstShape.z}, "NCHW"}, scalarDType) + : nvcv::Tensor({{dstShape.x, dstShape.y, dstShape.z, ch}, "NHWC"}, scalarDType); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + benchutils::warmup_and_exec(state, BENCH_PILLOWRESIZE_WARMUP_ITERATIONS, + [&op, &ws, &src, &dst, &interpType](cudaStream_t s) { op(s, ws.get(), src, dst, interpType); }); + } + else // ImageBatchVarShape + { + nvcv::ImageBatchVarShape src(static_cast(srcShape.x)); + nvcv::ImageBatchVarShape dst(static_cast(dstShape.x)); + + if (isPlanar) + { + benchutils::FillPlanarImageBatch(src, long2{srcShape.z, srcShape.y}, long2{0, 0}, /*checker*/ true); + benchutils::FillPlanarImageBatch(dst, long2{dstShape.z, dstShape.y}, long2{0, 0}, /*checker*/ false); + } + else + { + benchutils::FillImageBatch(src, long2{srcShape.z, srcShape.y}, long2{0, 0}, + benchutils::CheckerboardValues()); + benchutils::FillImageBatch(dst, long2{dstShape.z, dstShape.y}, long2{0, 0}, + [](const long4_16a &) { return T{0}; }); + } + + benchutils::warmup_and_exec(state, BENCH_PILLOWRESIZE_WARMUP_ITERATIONS, + [&op, &ws, &src, &dst, &interpType](cudaStream_t s) { op(s, ws.get(), src, dst, interpType); }); + } +} +CVCUDA_BENCH_SKIP_ERRORS(state) + +// clang-format on + +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(pillowresize, NVBENCH_TYPE_AXES(BENCH_PILLOWRESIZE_TYPES)) +BENCH_PILLOWRESIZE_AXES; diff --git a/bench/cpp/ops/BenchPosterize.cpp b/bench/cpp/ops/BenchPosterize.cpp new file mode 100644 index 000000000..224877763 --- /dev/null +++ b/bench/cpp/ops/BenchPosterize.cpp @@ -0,0 +1,137 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchPosterizeConfig.hpp" + +#include +#include + +#include + +template +inline void posterize(nvbench::state &state, nvbench::type_list) +try +{ + int3 shape = benchutils::GetShape<3, int3>(state.get_string("shape")); + auto layout = benchutils::GetStringParam(state, "layout", "NHWC"); + const benchutils::InputKind inputKind = benchutils::GetInputKind(state.get_string("inputKind")); + + // bits is irrelevant to throughput (a single bitwise-and per element); use a fixed mid-range value. + // constexpr so it needs no lambda capture (it is a constant expression). + constexpr int32_t bits = 4; + + const bool isPlanar = layout == "NCHW"; + const bool isFakePlanar = layout == "NCHW_FAKE"; + if (layout != "NHWC" && layout != "NCHW" && layout != "NCHW_FAKE") + { + state.skip("Posterize benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts"); + return; + } + // NCHW_FAKE ("fake planar") is a tensor-only comparison path: planar data is reformatted to + // interleaved, posterized with the interleaved kernel, and reformatted back — all timed + // together — so the native planar path (NCHW) can be shown to beat the naive + // convert->op->convert pipeline. + if (isFakePlanar && inputKind == benchutils::InputKind::VarShape) + { + state.skip("Fake-planar (NCHW_FAKE) posterize benchmark is tensor-only"); + return; + } + + using BT = typename nvcv::cuda::BaseType; + + int ch = nvcv::cuda::NumElements; + + // Posterize preserves size, so src and dst hold the same number of bytes. + const long bytes = static_cast(shape.x) * shape.y * shape.z * sizeof(T); + if (isFakePlanar) + { + // reformat(NCHW->NHWC) + posterize + reformat(NHWC->NCHW): reads src + interleaved src + dst, + // writes interleaved src + interleaved dst + dst. + state.add_global_memory_reads(3 * bytes); + state.add_global_memory_writes(3 * bytes); + } + else + { + state.add_global_memory_reads(bytes); + state.add_global_memory_writes(bytes); + } + + cvcuda::Posterize op; + + // clang-format off + if (isFakePlanar) // tensor-only: planar->interleaved->posterize->interleaved->planar + { + nvcv::Tensor src ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + nvcv::Tensor interSrc({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor interDst({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + cvcuda::Reformat reformatOp; + + benchutils::warmup_and_exec(state, BENCH_POSTERIZE_WARMUP_ITERATIONS, + [&op, &reformatOp, &src, &interSrc, &interDst, &dst](cudaStream_t s) { + reformatOp(s, src, interSrc); // NCHW -> NHWC + op(s, interSrc, interDst, bits); // interleaved posterize + reformatOp(s, interDst, dst); // NHWC -> NCHW + }); + } + else if (inputKind == benchutils::InputKind::Tensor) + { + nvcv::Tensor src = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + benchutils::warmup_and_exec(state, BENCH_POSTERIZE_WARMUP_ITERATIONS, + [&op, &src, &dst](cudaStream_t s) { op(s, src, dst, bits); }); + } + else // ImageBatchVarShape + { + nvcv::ImageBatchVarShape src(shape.x); + nvcv::ImageBatchVarShape dst(shape.x); + + if (isPlanar) + { + benchutils::FillPlanarImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}); + benchutils::FillPlanarImageBatch(dst, long2{shape.z, shape.y}, long2{0, 0}); + } + else + { + benchutils::FillImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}, + benchutils::CheckerboardValues()); + benchutils::FillImageBatchLike(dst, src, [](const long4_16a &) { return T{0}; }); + } + + benchutils::warmup_and_exec(state, BENCH_POSTERIZE_WARMUP_ITERATIONS, + [&op, &src, &dst](cudaStream_t s) { op(s, src, dst, bits); }); + } +} +CVCUDA_BENCH_SKIP_ERRORS(state) + +// clang-format on + +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(posterize, NVBENCH_TYPE_AXES(BENCH_POSTERIZE_TYPES)) +BENCH_POSTERIZE_AXES; diff --git a/bench/cpp/ops/BenchRandomResizedCrop.cpp b/bench/cpp/ops/BenchRandomResizedCrop.cpp new file mode 100644 index 000000000..e805e4ede --- /dev/null +++ b/bench/cpp/ops/BenchRandomResizedCrop.cpp @@ -0,0 +1,157 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchRandomResizedCropConfig.hpp" + +#include +#include + +#include + +template +inline void randomresizedcrop(nvbench::state &state, nvbench::type_list) +try +{ + long3 srcShape = benchutils::GetShape<3>(state.get_string("shape")); + auto layout = benchutils::GetStringParam(state, "layout", "NHWC"); + const benchutils::InputKind inputKind = benchutils::GetInputKind(state.get_string("inputKind")); + + NVCVInterpolationType interpType = benchutils::GetInterpolationType(state.get_string("interpolation")); + + long3 dstShape; + + if (state.get_string("resizeType") == "EXPAND") + { + dstShape = long3{srcShape.x, srcShape.y * 2, srcShape.z * 2}; + } + else if (state.get_string("resizeType") == "CONTRACT") + { + dstShape = long3{srcShape.x, srcShape.y / 2, srcShape.z / 2}; + } + else + { + throw std::invalid_argument("Invalid resizeType = " + state.get_string("resizeType")); + } + + double minScale = 0.08; + double maxScale = 1.0; + double minRatio = 0.5; + double maxRatio = 2.0; + uint32_t seed = 1234; + + const bool isPlanar = layout == "NCHW"; + const bool isFakePlanar = layout == "NCHW_FAKE"; + if (layout != "NHWC" && layout != "NCHW" && layout != "NCHW_FAKE") + { + state.skip("RandomResizedCrop benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts"); + return; + } + if (isFakePlanar && inputKind == benchutils::InputKind::VarShape) + { + state.skip("Fake-planar (NCHW_FAKE) RandomResizedCrop benchmark is tensor-only"); + return; + } + + const long srcBytes = srcShape.x * srcShape.y * srcShape.z * sizeof(T); + const long dstBytes = dstShape.x * dstShape.y * dstShape.z * sizeof(T); + if (isFakePlanar) + { + // reformat(NCHW->NHWC) + random-resized-crop + reformat(NHWC->NCHW): + // reads src twice + dst once, writes the interleaved src once + dst twice. + state.add_global_memory_reads(2 * srcBytes + dstBytes); + state.add_global_memory_writes(srcBytes + 2 * dstBytes); + } + else + { + state.add_global_memory_reads(srcBytes); + state.add_global_memory_writes(dstBytes); + } + + cvcuda::RandomResizedCrop op(minScale, maxScale, minRatio, maxRatio, static_cast(srcShape.x), seed); + using BT = typename nvcv::cuda::BaseType; + int ch = nvcv::cuda::NumElements; + + // clang-format off + + if (isFakePlanar) // tensor-only: planar -> interleaved -> random-resized-crop -> interleaved -> planar + { + nvcv::Tensor src ({{srcShape.x, ch, srcShape.y, srcShape.z}, "NCHW"}, benchutils::GetDataType()); + nvcv::Tensor interSrc({{srcShape.x, srcShape.y, srcShape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor interDst({{dstShape.x, dstShape.y, dstShape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst ({{dstShape.x, ch, dstShape.y, dstShape.z}, "NCHW"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + cvcuda::Reformat reformatOp; + + benchutils::warmup_and_exec(state, BENCH_RANDOMRESIZEDCROP_WARMUP_ITERATIONS, + [&op, &reformatOp, &src, &interSrc, &interDst, &dst, &interpType](cudaStream_t s) { + reformatOp(s, src, interSrc); + op(s, interSrc, interDst, interpType); + reformatOp(s, interDst, dst); + }); + } + else if (inputKind == benchutils::InputKind::Tensor) + { + nvcv::Tensor src = isPlanar + ? nvcv::Tensor({{srcShape.x, ch, srcShape.y, srcShape.z}, "NCHW"}, + benchutils::GetDataType()) + : nvcv::Tensor({{srcShape.x, srcShape.y, srcShape.z, ch}, "NHWC"}, + benchutils::GetDataType()); + nvcv::Tensor dst = isPlanar + ? nvcv::Tensor({{dstShape.x, ch, dstShape.y, dstShape.z}, "NCHW"}, + benchutils::GetDataType()) + : nvcv::Tensor({{dstShape.x, dstShape.y, dstShape.z, ch}, "NHWC"}, + benchutils::GetDataType()); + + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + benchutils::warmup_and_exec(state, BENCH_RANDOMRESIZEDCROP_WARMUP_ITERATIONS, + [&op, &src, &dst, &interpType](cudaStream_t s) { op(s, src, dst, interpType); }); + } + else // zero and positive var shape means use ImageBatchVarShape + { + nvcv::ImageBatchVarShape src(static_cast(srcShape.x)); + nvcv::ImageBatchVarShape dst(static_cast(dstShape.x)); + + if (isPlanar) + { + benchutils::FillPlanarImageBatch(src, long2{srcShape.z, srcShape.y}, long2{0, 0}); + benchutils::FillPlanarImageBatch(dst, long2{dstShape.z, dstShape.y}, long2{0, 0}); + } + else + { + benchutils::FillImageBatch(src, long2{srcShape.z, srcShape.y}, long2{0, 0}, + benchutils::CheckerboardValues()); + benchutils::FillImageBatch(dst, long2{dstShape.z, dstShape.y}, long2{0, 0}, + [](const long4_16a &) { return T{0}; }); + } + + benchutils::warmup_and_exec(state, BENCH_RANDOMRESIZEDCROP_WARMUP_ITERATIONS, + [&op, &src, &dst, &interpType](cudaStream_t s) { op(s, src, dst, interpType); }); + } +} +CVCUDA_BENCH_SKIP_ERRORS(state) + +// clang-format on + +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(randomresizedcrop, NVBENCH_TYPE_AXES(BENCH_RANDOMRESIZEDCROP_TYPES)) +BENCH_RANDOMRESIZEDCROP_AXES; diff --git a/bench/cpp/ops/BenchReformat.cpp b/bench/cpp/ops/BenchReformat.cpp new file mode 100644 index 000000000..a4a1f0a85 --- /dev/null +++ b/bench/cpp/ops/BenchReformat.cpp @@ -0,0 +1,73 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchReformatConfig.hpp" + +#include + +#include + +template +inline void reformat(nvbench::state &state, nvbench::type_list) +try +{ + int3 shape = benchutils::GetShape<3, int3>(state.get_string("shape")); + const benchutils::InputKind inputKind = benchutils::GetInputKind(state.get_string("inputKind")); + const int rowAlignment = benchutils::GetIntParam(state, "rowAlignment"); + + if (rowAlignment < 0) + { + throw std::invalid_argument("rowAlignment must be non-negative"); + } + + state.add_global_memory_reads(shape.x * shape.y * shape.z * sizeof(T)); + state.add_global_memory_writes(shape.x * shape.y * shape.z * sizeof(T)); + + cvcuda::Reformat op; + + // clang-format off + + if (inputKind == benchutils::InputKind::Tensor) + { + using BT = typename nvcv::cuda::BaseType; + int ch = nvcv::cuda::NumElements; + + nvcv::MemAlignment bufferAlignment; + bufferAlignment.rowAddr(rowAlignment); + + nvcv::Tensor src({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType(), bufferAlignment); + nvcv::Tensor dst({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType(), bufferAlignment); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + benchutils::warmup_and_exec(state, BENCH_REFORMAT_WARMUP_ITERATIONS, + [&op, &src, &dst](cudaStream_t s) { op(s, src, dst); }); + } + else + { + throw std::invalid_argument("ImageBatchVarShape not implemented for this operator"); + } +} +CVCUDA_BENCH_SKIP_ERRORS(state) + +// clang-format on + +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(reformat, NVBENCH_TYPE_AXES(BENCH_REFORMAT_TYPES)) +BENCH_REFORMAT_AXES; diff --git a/bench/cpp/ops/BenchRemap.cpp b/bench/cpp/ops/BenchRemap.cpp new file mode 100644 index 000000000..105f38633 --- /dev/null +++ b/bench/cpp/ops/BenchRemap.cpp @@ -0,0 +1,162 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchRemapConfig.hpp" + +#include +#include + +#include + +template +inline void remap(nvbench::state &state, nvbench::type_list) +try +{ + long3 srcShape = benchutils::GetShape<3>(state.get_string("shape")); + const benchutils::InputKind inputKind = benchutils::GetInputKind(state.get_string("inputKind")); + auto layout = benchutils::GetStringParam(state, "layout", "NHWC"); + long3 dstShape = srcShape; + long3 mapShape; + + const bool isPlanar = layout == "NCHW"; + const bool isFakePlanar = layout == "NCHW_FAKE"; + if (layout != "NHWC" && layout != "NCHW" && layout != "NCHW_FAKE") + { + state.skip("Remap benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts"); + return; + } + // NCHW_FAKE ("fake planar") is the tensor-only convert-remap-convert comparison baseline. + if (isFakePlanar && inputKind == benchutils::InputKind::VarShape) + { + state.skip("Fake-planar (NCHW_FAKE) remap benchmark is tensor-only"); + return; + } + + NVCVInterpolationType srcInterp; + NVCVInterpolationType mapInterp; + NVCVBorderType borderType; + NVCVRemapMapValueType mapValueType; + + bool alignCorners{true}; + float4 borderValue{0, 0, 0, 0}; + + if (state.get_string("mapType") == "DENSE") + { + srcInterp = NVCV_INTERP_NEAREST; + mapInterp = NVCV_INTERP_NEAREST; + borderType = NVCV_BORDER_CONSTANT; + mapValueType = NVCV_REMAP_ABSOLUTE_NORMALIZED; + mapShape = srcShape; + } + else if (state.get_string("mapType") == "RELATIVE") + { + srcInterp = NVCV_INTERP_CUBIC; + mapInterp = NVCV_INTERP_CUBIC; + borderType = NVCV_BORDER_REFLECT101; + mapValueType = NVCV_REMAP_RELATIVE_NORMALIZED; + mapShape = long3{srcShape.x, 4, 4}; + } + else + { + throw std::invalid_argument("Invalid mapType = " + state.get_string("mapType")); + } + + state.add_global_memory_reads(srcShape.x * srcShape.y * srcShape.z * sizeof(T) + + mapShape.x * mapShape.y * mapShape.z * sizeof(float2)); + state.add_global_memory_writes(dstShape.x * dstShape.y * dstShape.z * sizeof(T)); + + cvcuda::Remap op; + + // clang-format off + + nvcv::Tensor map({{mapShape.x, mapShape.y, mapShape.z, 1}, "NHWC"}, nvcv::TYPE_2F32); + + // Map tensor stays random: its values determine the source-pixel access + // pattern (cache behaviour), not just sample data — checkerboard would + // give pattern-perfect cache hits and skew kernel timing. + benchutils::FillTensor(map, benchutils::LcgValues()); + + using BT = typename nvcv::cuda::BaseType; + int ch = nvcv::cuda::NumElements; + + if (isFakePlanar) // tensor-only: planar->interleaved->remap->interleaved->planar + { + nvcv::Tensor src ({{srcShape.x, ch, srcShape.y, srcShape.z}, "NCHW"}, benchutils::GetDataType()); + nvcv::Tensor interSrc({{srcShape.x, srcShape.y, srcShape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor interDst({{dstShape.x, dstShape.y, dstShape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst ({{dstShape.x, ch, dstShape.y, dstShape.z}, "NCHW"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + cvcuda::Reformat reformatOp; + + benchutils::warmup_and_exec(state, BENCH_REMAP_WARMUP_ITERATIONS, + [&op, &reformatOp, &src, &interSrc, &interDst, &dst, &map, &srcInterp, &mapInterp, &mapValueType, &alignCorners, &borderType, &borderValue](cudaStream_t s) { + reformatOp(s, src, interSrc); + op(s, interSrc, interDst, map, srcInterp, mapInterp, mapValueType, alignCorners, borderType, borderValue); + reformatOp(s, interDst, dst); + }); + } + else if (inputKind == benchutils::InputKind::Tensor) + { + nvcv::Tensor src = isPlanar + ? nvcv::Tensor({{srcShape.x, ch, srcShape.y, srcShape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{srcShape.x, srcShape.y, srcShape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst = isPlanar + ? nvcv::Tensor({{dstShape.x, ch, dstShape.y, dstShape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{dstShape.x, dstShape.y, dstShape.z, ch}, "NHWC"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + benchutils::warmup_and_exec(state, BENCH_REMAP_WARMUP_ITERATIONS, + [&op, &src, &dst, &map, &srcInterp, &mapInterp, &mapValueType, &alignCorners, &borderType, &borderValue](cudaStream_t s) { + op(s, src, dst, map, srcInterp, mapInterp, mapValueType, alignCorners, borderType, borderValue); + }); + } + else // zero and positive var shape means use ImageBatchVarShape + { + nvcv::ImageBatchVarShape src(static_cast(srcShape.x)); + nvcv::ImageBatchVarShape dst(static_cast(dstShape.x)); + + if (isPlanar) + { + benchutils::FillPlanarImageBatch(src, long2{srcShape.z, srcShape.y}, long2{0, 0}); + benchutils::FillPlanarImageBatch(dst, long2{dstShape.z, dstShape.y}, long2{0, 0}); + } + else + { + benchutils::FillImageBatch(src, long2{srcShape.z, srcShape.y}, long2{0, 0}, + benchutils::CheckerboardValues()); + // Use FillImageBatchLike to ensure dst has same per-sample shapes as src + benchutils::FillImageBatchLike(dst, src, [](const long4_16a &) { return T{0}; }); + } + + benchutils::warmup_and_exec(state, BENCH_REMAP_WARMUP_ITERATIONS, + [&op, &src, &dst, &map, &srcInterp, &mapInterp, &mapValueType, &alignCorners, &borderType, &borderValue](cudaStream_t s) { + op(s, src, dst, map, srcInterp, mapInterp, mapValueType, alignCorners, borderType, borderValue); + }); + } +} +CVCUDA_BENCH_SKIP_ERRORS(state) + +// clang-format on + +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(remap, NVBENCH_TYPE_AXES(BENCH_REMAP_TYPES)) +BENCH_REMAP_AXES; diff --git a/bench/cpp/ops/BenchResize.cpp b/bench/cpp/ops/BenchResize.cpp new file mode 100644 index 000000000..e8e92714f --- /dev/null +++ b/bench/cpp/ops/BenchResize.cpp @@ -0,0 +1,145 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchResizeConfig.hpp" + +#include +#include + +#include + +template +inline void resize(nvbench::state &state, nvbench::type_list) +try +{ + long3 srcShape = benchutils::GetShape<3>(state.get_string("shape")); + auto layout = benchutils::GetStringParam(state, "layout", "NHWC"); + const benchutils::InputKind inputKind = benchutils::GetInputKind(state.get_string("inputKind")); + + NVCVInterpolationType interpType = benchutils::GetInterpolationType(state.get_string("interpolation")); + + const std::string resizeType = state.get_string("resizeType"); + long3 dstShape = benchutils::GetResizeOutputShape(srcShape, resizeType); + + const bool isPlanar = layout == "NCHW"; + const bool isFakePlanar = layout == "NCHW_FAKE"; + if (layout != "NHWC" && layout != "NCHW" && layout != "NCHW_FAKE") + { + state.skip("Resize benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts"); + return; + } + // NCHW_FAKE ("fake planar") is a tensor-only comparison path: planar data is + // reformatted to interleaved, resized with the interleaved kernel, and reformatted + // back to planar — all timed together — so the native planar path (NCHW) can be + // shown to be faster than this naive convert→resize→convert pipeline. + if (isFakePlanar && inputKind == benchutils::InputKind::VarShape) + { + state.skip("Fake-planar (NCHW_FAKE) resize benchmark is tensor-only"); + return; + } + + const long srcBytes = srcShape.x * srcShape.y * srcShape.z * sizeof(T); + const long dstBytes = dstShape.x * dstShape.y * dstShape.z * sizeof(T); + if (isFakePlanar) + { + // reformat(NCHW→NHWC) + resize + reformat(NHWC→NCHW): reads src twice + dst once, + // writes the interleaved src once + dst twice. + state.add_global_memory_reads(2 * srcBytes + dstBytes); + state.add_global_memory_writes(srcBytes + 2 * dstBytes); + } + else + { + state.add_global_memory_reads(srcBytes); + state.add_global_memory_writes(dstBytes); + } + + cvcuda::Resize op; + + using BT = typename nvcv::cuda::BaseType; + int ch = nvcv::cuda::NumElements; + + // clang-format off + + if (isFakePlanar) // tensor-only: planar→interleaved→resize→interleaved→planar + { + nvcv::Tensor src ({{srcShape.x, ch, srcShape.y, srcShape.z}, "NCHW"}, benchutils::GetDataType()); + nvcv::Tensor interSrc({{srcShape.x, srcShape.y, srcShape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor interDst({{dstShape.x, dstShape.y, dstShape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst ({{dstShape.x, ch, dstShape.y, dstShape.z}, "NCHW"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + cvcuda::Reformat reformatOp; + + benchutils::warmup_and_exec(state, BENCH_RESIZE_WARMUP_ITERATIONS, + [&op, &reformatOp, &src, &interSrc, &interDst, &dst, &interpType](cudaStream_t s) { + reformatOp(s, src, interSrc); // NCHW → NHWC + op(s, interSrc, interDst, interpType); // interleaved resize + reformatOp(s, interDst, dst); // NHWC → NCHW + }); + } + else if (inputKind == benchutils::InputKind::Tensor) + { + nvcv::Tensor src = isPlanar + ? nvcv::Tensor({{srcShape.x, ch, srcShape.y, srcShape.z}, "NCHW"}, + benchutils::GetDataType()) + : nvcv::Tensor({{srcShape.x, srcShape.y, srcShape.z, ch}, "NHWC"}, + benchutils::GetDataType()); + nvcv::Tensor dst = isPlanar + ? nvcv::Tensor({{dstShape.x, ch, dstShape.y, dstShape.z}, "NCHW"}, + benchutils::GetDataType()) + : nvcv::Tensor({{dstShape.x, dstShape.y, dstShape.z, ch}, "NHWC"}, + benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + benchutils::warmup_and_exec(state, BENCH_RESIZE_WARMUP_ITERATIONS, + [&op, &src, &dst, &interpType](cudaStream_t s) { op(s, src, dst, interpType); }); + } + else // zero and positive var shape means use ImageBatchVarShape + { + nvcv::ImageBatchVarShape src(static_cast(srcShape.x)); + nvcv::ImageBatchVarShape dst(static_cast(dstShape.x)); + + if (isPlanar) + { + // Resize src/dst differ in size, so fill dst at its own dst size + // (its content is irrelevant — the operator overwrites it). + benchutils::FillPlanarImageBatch(src, long2{srcShape.z, srcShape.y}, long2{0, 0}); + benchutils::FillPlanarImageBatch(dst, long2{dstShape.z, dstShape.y}, long2{0, 0}); + } + else + { + benchutils::FillImageBatch(src, long2{srcShape.z, srcShape.y}, long2{0, 0}, + benchutils::CheckerboardValues()); + benchutils::FillImageBatch(dst, long2{dstShape.z, dstShape.y}, long2{0, 0}, + [](const long4_16a &) { return T{0}; }); + } + + benchutils::warmup_and_exec(state, BENCH_RESIZE_WARMUP_ITERATIONS, + [&op, &src, &dst, &interpType](cudaStream_t s) { op(s, src, dst, interpType); }); + } +} +CVCUDA_BENCH_SKIP_ERRORS(state) + +// clang-format on + +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(resize, NVBENCH_TYPE_AXES(BENCH_RESIZE_TYPES)) +BENCH_RESIZE_AXES; diff --git a/bench/cpp/ops/BenchResizeCropConvertReformat.cpp b/bench/cpp/ops/BenchResizeCropConvertReformat.cpp new file mode 100644 index 000000000..776a984da --- /dev/null +++ b/bench/cpp/ops/BenchResizeCropConvertReformat.cpp @@ -0,0 +1,169 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchResizeCropConvertReformatConfig.hpp" + +#include +#include +#include + +#include + +template +inline void resizecropconvertreformat(nvbench::state &state, nvbench::type_list) +try +{ + long3 srcShape = benchutils::GetShape<3>(state.get_string("shape")); + const benchutils::InputKind inputKind = benchutils::GetInputKind(state.get_string("inputKind")); + const std::string layout = state.get_string("layout"); + + NVCVInterpolationType interpType = benchutils::GetInterpolationType(state.get_string("interpolation")); + + using BT = nvcv::cuda::BaseType; + long nc = nvcv::cuda::NumElements; + + const bool isPlanar = layout == "NCHW"; + const bool isFakePlanar = layout == "NCHW_FAKE"; + + if (layout != "NHWC" && layout != "NCHW" && layout != "NCHW_FAKE") + { + state.skip("ResizeCropConvertReformat benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts"); + return; + } + + if (isFakePlanar && inputKind != benchutils::InputKind::Tensor) + { + state.skip("Fake-planar (NCHW_FAKE) ResizeCropConvertReformat benchmark is tensor-only"); + return; + } + + // Guard against degenerate shapes that would cause divide-by-zero or invalid crops + if (srcShape.y <= 1 || srcShape.z <= 1) + { + state.skip("Height and width must be > 1 for resize/crop operations"); + return; + } + + // Resize to 0.5x (shrink by half) and crop + // NVCVSize2D: {width, height} - srcShape.z is width, srcShape.y is height + auto resize = NVCVSize2D{static_cast(srcShape.z / 2), static_cast(srcShape.y / 2)}; + + // Crop region at origin, clamped to valid range + int2 cropPos{0, 0}; + int crop_w = std::max(1, std::min(512, resize.w - 1)); + int crop_h = std::max(1, std::min(512, resize.h - 1)); + int2 cropSize{crop_w, crop_h}; + + // NO_OP channel manipulation + NVCVChannelManip manip = NVCV_CHANNEL_NO_OP; + + long3 dstShape{srcShape.x, cropSize.y, cropSize.x}; + + // Calculate actual source region read (crop maps back to a smaller region in source) + // scale = src_size / resize_size, src_region = (crop_pos + crop_size) * scale + margin + float scale_x = static_cast(srcShape.z) / static_cast(resize.w); + float scale_y = static_cast(srcShape.y) / static_cast(resize.h); + int src_region_w = std::min(static_cast(std::ceil(static_cast(cropPos.x + crop_w) * scale_x)) + 2, + static_cast(srcShape.z)); + int src_region_h = std::min(static_cast(std::ceil(static_cast(cropPos.y + crop_h) * scale_y)) + 2, + static_cast(srcShape.y)); + + const long srcBytes = srcShape.x * srcShape.y * srcShape.z * sizeof(BT) * nc; + const long dstBytes = dstShape.x * dstShape.y * dstShape.z * sizeof(BT) * nc; + + // Memory: read source region, write destination crop. Fake planar also reformats the full source. + state.add_global_memory_reads(srcShape.x * src_region_h * src_region_w * sizeof(BT) * nc + + (isFakePlanar ? srcBytes : 0)); + state.add_global_memory_writes(dstBytes + (isFakePlanar ? srcBytes : 0)); + + cvcuda::ResizeCropConvertReformat op; + + // clang-format off + + if (inputKind == benchutils::InputKind::Tensor) + { + if (isPlanar || isFakePlanar) + { + nvcv::Tensor src({{srcShape.x, nc, srcShape.y, srcShape.z}, "NCHW"}, benchutils::GetDataType()); + nvcv::Tensor dst({{dstShape.x, nc, dstShape.y, dstShape.z}, "NCHW"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + if (isFakePlanar) + { + nvcv::Tensor tmp({{srcShape.x, srcShape.y, srcShape.z, nc}, "NHWC"}, benchutils::GetDataType()); + cvcuda::Reformat reformatOp; + + benchutils::warmup_and_exec(state, BENCH_RESIZECROPCONVERTREFORMAT_WARMUP_ITERATIONS, + [&op, &reformatOp, &src, &tmp, &dst, &resize, &interpType, &cropPos, &manip](cudaStream_t s) { + reformatOp(s, src, tmp); + op(s, tmp, dst, resize, interpType, cropPos, manip); + }); + } + else + { + benchutils::warmup_and_exec(state, BENCH_RESIZECROPCONVERTREFORMAT_WARMUP_ITERATIONS, + [&op, &src, &dst, &resize, &interpType, &cropPos, &manip](cudaStream_t s) { + op(s, src, dst, resize, interpType, cropPos, manip); + }); + } + } + else + { + nvcv::Tensor src({{srcShape.x, srcShape.y, srcShape.z, nc}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst({{dstShape.x, dstShape.y, dstShape.z, nc}, "NHWC"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + benchutils::warmup_and_exec(state, BENCH_RESIZECROPCONVERTREFORMAT_WARMUP_ITERATIONS, + [&op, &src, &dst, &resize, &interpType, &cropPos, &manip](cudaStream_t s) { + op(s, src, dst, resize, interpType, cropPos, manip); + }); + } + } + else // zero and positive var shape means use ImageBatchVarShape + { + nvcv::ImageBatchVarShape src(static_cast(srcShape.x)); + nvcv::Tensor dst = isPlanar + ? nvcv::Tensor({{dstShape.x, nc, dstShape.y, dstShape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{dstShape.x, dstShape.y, dstShape.z, nc}, "NHWC"}, benchutils::GetDataType()); + + if (isPlanar) + { + benchutils::FillPlanarImageBatch(src, long2{srcShape.z, srcShape.y}, long2{0, 0}); + } + else + { + benchutils::FillImageBatch(src, long2{srcShape.z, srcShape.y}, long2{0, 0}, + benchutils::CheckerboardValues()); + } + + benchutils::warmup_and_exec(state, BENCH_RESIZECROPCONVERTREFORMAT_WARMUP_ITERATIONS, + [&op, &src, &dst, &resize, &interpType, &cropPos, &manip](cudaStream_t s) { + op(s, src, dst, resize, interpType, cropPos, manip); + }); + } +} +CVCUDA_BENCH_SKIP_ERRORS(state) + +// clang-format on + +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(resizecropconvertreformat, NVBENCH_TYPE_AXES(BENCH_RESIZECROPCONVERTREFORMAT_TYPES)) +BENCH_RESIZECROPCONVERTREFORMAT_AXES; diff --git a/bench/cpp/ops/BenchRotate.cpp b/bench/cpp/ops/BenchRotate.cpp new file mode 100644 index 000000000..3c3e8a416 --- /dev/null +++ b/bench/cpp/ops/BenchRotate.cpp @@ -0,0 +1,232 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchRotateConfig.hpp" + +#include +#include + +#include + +#include +#include + +namespace { + +// Sutherland-Hodgman half-plane clip. axis=0 clips on x, axis=1 on y. +// keepAbove=true keeps points whose coord >= bound; false keeps coord <= bound. +inline std::vector clipHalfPlane(const std::vector &in, int axis, double bound, bool keepAbove) +{ + std::vector out; + if (in.empty()) + return out; + auto coord = [axis](double2 p) + { + return axis == 0 ? p.x : p.y; + }; + auto inside = [&coord, &keepAbove, &bound](double2 p) + { + return keepAbove ? coord(p) >= bound : coord(p) <= bound; + }; + auto inter = [&coord, &bound](double2 a, double2 b) -> double2 + { + double t = (bound - coord(a)) / (coord(b) - coord(a)); + return {a.x + t * (b.x - a.x), a.y + t * (b.y - a.y)}; + }; + for (size_t i = 0; i < in.size(); ++i) + { + double2 cur = in[i]; + double2 prev = in[(i + in.size() - 1) % in.size()]; + bool ci = inside(cur); + bool pi = inside(prev); + if (ci) + { + if (!pi) + out.push_back(inter(prev, cur)); + out.push_back(cur); + } + else if (pi) + { + out.push_back(inter(prev, cur)); + } + } + return out; +} + +// Fraction of dst pixels in [0,W]x[0,H] whose inverse rotation maps inside source. +// The rotate kernel skips both read and write outside this region, so the byte model +// must be scaled by this fraction to track HBM traffic instead of nominal coverage. +inline double rotateInBoundsFraction(double angleDeg, double xShift, double yShift, double W, double H) +{ + const double th = angleDeg * M_PI / 180.0; + const double c = std::cos(th); + const double s = std::sin(th); + // Kernel mapping is src = R(+θ) * (dst - shift), so the in-bounds dst region is + // the image of [0,W]x[0,H] under dst = R(-θ) * src + shift. + auto map = [&c, &s, &xShift, &yShift](double x, double y) + { + return double2{c * x + s * y + xShift, -s * x + c * y + yShift}; + }; + std::vector p = {map(0.0, 0.0), map(W, 0.0), map(W, H), map(0.0, H)}; + p = clipHalfPlane(p, 0, 0.0, true); + p = clipHalfPlane(p, 0, W, false); + p = clipHalfPlane(p, 1, 0.0, true); + p = clipHalfPlane(p, 1, H, false); + if (p.size() < 3) + return 0.0; + double area = 0.0; + for (size_t i = 0; i < p.size(); ++i) + { + size_t j = (i + 1) % p.size(); + area += p[i].x * p[j].y - p[j].x * p[i].y; + } + return std::abs(area) * 0.5 / (W * H); +} + +} // namespace + +template +inline void rotate(nvbench::state &state, nvbench::type_list) +try +{ + int3 shape = benchutils::GetShape<3, int3>(state.get_string("shape")); + auto layout = benchutils::GetStringParam(state, "layout", "NHWC"); + const benchutils::InputKind inputKind = benchutils::GetInputKind(state.get_string("inputKind")); + + NVCVInterpolationType interpType = benchutils::GetInterpolationType(state.get_string("interpolation")); + + const bool isPlanar = layout == "NCHW"; + const bool isFakePlanar = layout == "NCHW_FAKE"; + if (layout != "NHWC" && layout != "NCHW" && layout != "NCHW_FAKE") + { + state.skip("Rotate benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts"); + return; + } + // NCHW_FAKE ("fake planar") is a tensor-only comparison path: planar data is reformatted to + // interleaved, rotated with the interleaved kernel, and reformatted back to planar — all timed + // together — so the native planar path (NCHW) can be shown to be faster than this naive + // convert→rotate→convert pipeline. + if (isFakePlanar && inputKind == benchutils::InputKind::VarShape) + { + state.skip("Fake-planar (NCHW_FAKE) rotate benchmark is tensor-only"); + return; + } + + // Rotation around image center: keeps the bulk of dst pixels inside source so + // the kernel actually exercises the read+interp+write path rather than the + // out-of-bounds early-exit guard at rotate.cu:61. + const double angleDeg = 30.0; + const double th = angleDeg * M_PI / 180.0; + const double cx = static_cast(shape.z) * 0.5; + const double cy = static_cast(shape.y) * 0.5; + const double xShift = cx * (1.0 - std::cos(th)) - cy * std::sin(th); + const double yShift = cy * (1.0 - std::cos(th)) + cx * std::sin(th); + double2 shift{xShift, yShift}; + + const double inBoundsFrac + = rotateInBoundsFraction(angleDeg, xShift, yShift, static_cast(shape.z), static_cast(shape.y)); + const double fullBytes = static_cast(shape.x) * shape.y * shape.z * sizeof(T); + // Native rotate only touches the in-bounds region; the two reformats in the fake-planar path move + // the full tensor each way (reformat is full-coverage), so add 2*fullBytes on top of the rotate. + const auto bytesIO = static_cast((isFakePlanar ? 2.0 * fullBytes : 0.0) + inBoundsFrac * fullBytes); + state.add_global_memory_reads(bytesIO); + state.add_global_memory_writes(bytesIO); + + cvcuda::Rotate op(shape.x); + + // clang-format off + + if (isFakePlanar) // tensor-only: planar→interleaved→rotate→interleaved→planar + { + using BT = typename nvcv::cuda::BaseType; + int ch = nvcv::cuda::NumElements; + + nvcv::Tensor src ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + nvcv::Tensor interSrc({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor interDst({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::LcgValues()); + + cvcuda::Reformat reformatOp; + + benchutils::warmup_and_exec(state, BENCH_ROTATE_WARMUP_ITERATIONS, + [&op, &reformatOp, &src, &interSrc, &interDst, &dst, &angleDeg, &shift, &interpType](cudaStream_t s) { + reformatOp(s, src, interSrc); // NCHW → NHWC + op(s, interSrc, interDst, angleDeg, shift, interpType); // interleaved rotate + reformatOp(s, interDst, dst); // NHWC → NCHW + }); + } + else if (inputKind == benchutils::InputKind::Tensor) + { + using BT = typename nvcv::cuda::BaseType; + int ch = nvcv::cuda::NumElements; + + nvcv::Tensor src = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::LcgValues()); + + benchutils::warmup_and_exec(state, BENCH_ROTATE_WARMUP_ITERATIONS, + [&op, &src, &dst, &angleDeg, &shift, &interpType](cudaStream_t s) { + op(s, src, dst, angleDeg, shift, interpType); + }); + } + else // zero and positive var shape means use ImageBatchVarShape + { + nvcv::ImageBatchVarShape src(shape.x); + nvcv::ImageBatchVarShape dst(shape.x); + + if (isPlanar) + { + benchutils::FillPlanarImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}); + benchutils::FillPlanarImageBatch(dst, long2{shape.z, shape.y}, long2{0, 0}); + } + else + { + benchutils::FillImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}, + benchutils::LcgValues()); + // Use FillImageBatchLike to ensure dst has same per-sample shapes as src + benchutils::FillImageBatchLike(dst, src, [](const long4_16a &) { return T{0}; }); + } + + nvcv::Tensor angleDegTensor({{shape.x}, "N"}, nvcv::TYPE_F64); + nvcv::Tensor shiftTensor({{shape.x, 2}, "NW"}, nvcv::TYPE_F64); + + benchutils::FillTensor(angleDegTensor, [&angleDeg](const long4_16a &){ return angleDeg; }); + benchutils::FillTensor( + shiftTensor, [&shift](const long4_16a &c) { return nvcv::cuda::GetElement(shift, static_cast(c.y)); }); + + benchutils::warmup_and_exec(state, BENCH_ROTATE_WARMUP_ITERATIONS, + [&op, &src, &dst, &angleDegTensor, &shiftTensor, &interpType](cudaStream_t s) { + op(s, src, dst, angleDegTensor, shiftTensor, interpType); + }); + } +} +CVCUDA_BENCH_SKIP_ERRORS(state) + +// clang-format on + +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(rotate, NVBENCH_TYPE_AXES(BENCH_ROTATE_TYPES)) +BENCH_ROTATE_AXES; diff --git a/bench/BenchSIFT.cpp b/bench/cpp/ops/BenchSIFT.cpp similarity index 52% rename from bench/BenchSIFT.cpp rename to bench/cpp/ops/BenchSIFT.cpp index 804c3598e..8ef7575c6 100644 --- a/bench/BenchSIFT.cpp +++ b/bench/cpp/ops/BenchSIFT.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,37 +15,46 @@ * limitations under the License. */ -#include "BenchUtils.hpp" +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchSIFTConfig.hpp" #include #include +#include + template -inline void SIFT(nvbench::state &state, nvbench::type_list) +inline void sift(nvbench::state &state, nvbench::type_list) try { - long3 shape = benchutils::GetShape<3>(state.get_string("shape")); - long varShape = state.get_int64("varShape"); - int capacity = static_cast(state.get_int64("maxCapacity")); - int numOctLayers = static_cast(state.get_int64("numOctaveLayers")); - float contThr = static_cast(state.get_float64("contrastThreshold")); - float edgeThr = static_cast(state.get_float64("edgeThreshold")); - float initSigma = static_cast(state.get_float64("initSigma")); + int3 shape = benchutils::GetShape<3, int3>(state.get_string("shape")); + const benchutils::InputKind inputKind = benchutils::GetInputKind(state.get_string("inputKind")); + const std::string layout = state.get_string("layout"); + auto capacity = benchutils::GetIntParam(state, "maxCapacity"); + auto numOctLayers = benchutils::GetIntParam(state, "numOctaveLayers"); + auto contThr = static_cast(state.get_float64("contrastThreshold")); + auto edgeThr = static_cast(state.get_float64("edgeThreshold")); + auto initSigma = static_cast(state.get_float64("initSigma")); NVCVSIFTFlagType flags; int3 maxShape; + if (layout != "NHWC" && layout != "NCHW") + { + throw std::invalid_argument("Invalid layout = " + layout); + } + if (state.get_string("expandInput") == "Y") { flags = NVCV_SIFT_USE_EXPANDED_INPUT; - maxShape = int3{(int)shape.z * 2, (int)shape.y * 2, (int)shape.x}; + maxShape = int3{shape.z * 2, shape.y * 2, shape.x}; } else if (state.get_string("expandInput") == "N") { flags = NVCV_SIFT_USE_ORIGINAL_INPUT; - maxShape = int3{(int)shape.z, (int)shape.y, (int)shape.x}; + maxShape = int3{shape.z, shape.y, shape.x}; } else { @@ -66,44 +75,32 @@ try // clang-format off - if (varShape < 0) // negative var shape means use Tensor + if (inputKind == benchutils::InputKind::Tensor) { - nvcv::Tensor src({{shape.x, shape.y, shape.z, 1}, "NHWC"}, nvcv::TYPE_U8); + nvcv::Tensor src = layout == "NCHW" ? nvcv::Tensor({{shape.x, 1, shape.y, shape.z}, "NCHW"}, nvcv::TYPE_U8) + : nvcv::Tensor({{shape.x, shape.y, shape.z, 1}, "NHWC"}, nvcv::TYPE_U8); nvcv::Tensor dstC({{shape.x, capacity}, "NM"}, nvcv::TYPE_4F32); nvcv::Tensor dstM({{shape.x, capacity}, "NM"}, nvcv::TYPE_3F32); nvcv::Tensor dstD({{shape.x, capacity, 128}, "NMD"}, nvcv::TYPE_U8); nvcv::Tensor dstN({{shape.x}, "N"}, nvcv::TYPE_S32); - benchutils::FillTensor(src, benchutils::RandomValues()); + benchutils::FillTensor(src, benchutils::LcgValues()); - state.exec(nvbench::exec_tag::sync, - [&op, &src, &dstC, &dstM, &dstD, &dstN, &numOctLayers, &contThr, &edgeThr, &initSigma, &flags] - (nvbench::launch &launch) - { - op(launch.get_stream(), src, dstC, dstM, dstD, dstN, numOctLayers, contThr, edgeThr, initSigma, flags); - }); + benchutils::warmup_and_exec(state, BENCH_SIFT_WARMUP_ITERATIONS, + [&op, &src, &dstC, &dstM, &dstD, &dstN, &numOctLayers, &contThr, &edgeThr, &initSigma, &flags](cudaStream_t s) { + op(s, src, dstC, dstM, dstD, dstN, numOctLayers, contThr, edgeThr, initSigma, flags); + }); } else // zero and positive var shape means use ImageBatchVarShape { throw std::invalid_argument("ImageBatchVarShape not implemented for this operator"); } } -catch (const std::exception &err) -{ - state.skip(err.what()); -} +CVCUDA_BENCH_SKIP_ERRORS(state) // clang-format on -using SIFTTypes = nvbench::type_list; - -NVBENCH_BENCH_TYPES(SIFT, NVBENCH_TYPE_AXES(SIFTTypes)) - .set_type_axes_names({"InOutDataType"}) - .add_string_axis("shape", {"1x1080x1920"}) - .add_int64_axis("varShape", {-1}) - .add_int64_axis("maxCapacity", {10000}) - .add_int64_axis("numOctaveLayers", {3}) - .add_float64_axis("contrastThreshold", {0.04}) - .add_float64_axis("edgeThreshold", {10.0}) - .add_float64_axis("initSigma", {1.6}) - .add_string_axis("expandInput", {"Y"}); +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(sift, NVBENCH_TYPE_AXES(BENCH_SIFT_TYPES)) +BENCH_SIFT_AXES; diff --git a/bench/cpp/ops/BenchSolarize.cpp b/bench/cpp/ops/BenchSolarize.cpp new file mode 100644 index 000000000..7b4bcf2b1 --- /dev/null +++ b/bench/cpp/ops/BenchSolarize.cpp @@ -0,0 +1,139 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchSolarizeConfig.hpp" + +#include +#include + +#include + +template +inline void solarize(nvbench::state &state, nvbench::type_list) +try +{ + int3 shape = benchutils::GetShape<3, int3>(state.get_string("shape")); + auto layout = benchutils::GetStringParam(state, "layout", "NHWC"); + const benchutils::InputKind inputKind = benchutils::GetInputKind(state.get_string("inputKind")); + + const bool isPlanar = layout == "NCHW"; + const bool isFakePlanar = layout == "NCHW_FAKE"; + if (layout != "NHWC" && layout != "NCHW" && layout != "NCHW_FAKE") + { + state.skip("Solarize benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts"); + return; + } + // NCHW_FAKE ("fake planar") is a tensor-only comparison path: planar data is reformatted to + // interleaved, solarized with the interleaved kernel, and reformatted back — all timed + // together — so the native planar path (NCHW) can be shown to beat the naive + // convert->op->convert pipeline. + if (isFakePlanar && inputKind == benchutils::InputKind::VarShape) + { + state.skip("Fake-planar (NCHW_FAKE) solarize benchmark is tensor-only"); + return; + } + + using BT = typename nvcv::cuda::BaseType; + + // Keep the threshold at mid-range for each base type so both the pass-through and the invert + // branch are exercised. Float inputs are checkerboard {0,1}, so a fixed 128.0 would never + // invert them and would bias the float baselines. + const double threshold + = std::is_floating_point_v ? 0.5 : static_cast(nvcv::cuda::TypeTraits::max) / 2.0; + + int ch = nvcv::cuda::NumElements; + + // Solarize preserves size, so src and dst hold the same number of bytes. + const long bytes = static_cast(shape.x) * shape.y * shape.z * sizeof(T); + if (isFakePlanar) + { + // reformat(NCHW->NHWC) + solarize + reformat(NHWC->NCHW): reads src + interleaved src + dst, + // writes interleaved src + interleaved dst + dst. + state.add_global_memory_reads(3 * bytes); + state.add_global_memory_writes(3 * bytes); + } + else + { + state.add_global_memory_reads(bytes); + state.add_global_memory_writes(bytes); + } + + cvcuda::Solarize op; + + // clang-format off + if (isFakePlanar) // tensor-only: planar->interleaved->solarize->interleaved->planar + { + nvcv::Tensor src ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + nvcv::Tensor interSrc({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor interDst({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + cvcuda::Reformat reformatOp; + + benchutils::warmup_and_exec(state, BENCH_SOLARIZE_WARMUP_ITERATIONS, + [&op, &reformatOp, &src, &interSrc, &interDst, &dst, threshold](cudaStream_t s) { + reformatOp(s, src, interSrc); // NCHW -> NHWC + op(s, interSrc, interDst, threshold); // interleaved solarize + reformatOp(s, interDst, dst); // NHWC -> NCHW + }); + } + else if (inputKind == benchutils::InputKind::Tensor) + { + nvcv::Tensor src = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + benchutils::warmup_and_exec(state, BENCH_SOLARIZE_WARMUP_ITERATIONS, + [&op, &src, &dst, threshold](cudaStream_t s) { op(s, src, dst, threshold); }); + } + else // ImageBatchVarShape + { + nvcv::ImageBatchVarShape src(shape.x); + nvcv::ImageBatchVarShape dst(shape.x); + + if (isPlanar) + { + benchutils::FillPlanarImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}); + benchutils::FillPlanarImageBatch(dst, long2{shape.z, shape.y}, long2{0, 0}); + } + else + { + benchutils::FillImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}, + benchutils::CheckerboardValues()); + benchutils::FillImageBatchLike(dst, src, [](const long4_16a &) { return T{0}; }); + } + + benchutils::warmup_and_exec(state, BENCH_SOLARIZE_WARMUP_ITERATIONS, + [&op, &src, &dst, threshold](cudaStream_t s) { op(s, src, dst, threshold); }); + } +} +CVCUDA_BENCH_SKIP_ERRORS(state) + +// clang-format on + +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(solarize, NVBENCH_TYPE_AXES(BENCH_SOLARIZE_TYPES)) +BENCH_SOLARIZE_AXES; diff --git a/bench/cpp/ops/BenchStack.cpp b/bench/cpp/ops/BenchStack.cpp new file mode 100644 index 000000000..303a96711 --- /dev/null +++ b/bench/cpp/ops/BenchStack.cpp @@ -0,0 +1,166 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchStackConfig.hpp" + +#include +#include + +#include + +#include + +template +inline void RunFakePlanarStackBench(nvbench::state &state, cvcuda::Stack &op, const int3 &shape, int ch) +{ + nvcv::TensorBatch interSrc(nvcv::TensorBatch::CalcRequirements(shape.x)); + std::vector planarSrcTensors; + std::vector interSrcTensors; + planarSrcTensors.reserve(shape.x); + interSrcTensors.reserve(shape.x); + + for (int i = 0; i < shape.x; ++i) + { + planarSrcTensors.emplace_back(nvcv::Tensor( + { + {ch, shape.y, shape.z}, + "CHW" + }, + benchutils::GetDataType())); + interSrcTensors.emplace_back(nvcv::Tensor( + { + {shape.y, shape.z, ch}, + "HWC" + }, + benchutils::GetDataType())); + benchutils::FillTensor(planarSrcTensors.back(), benchutils::CheckerboardValues()); + interSrc.pushBack(interSrcTensors.back()); + } + + nvcv::Tensor interDst( + { + {shape.x, shape.y, shape.z, ch}, + "NHWC" + }, + benchutils::GetDataType()); + nvcv::Tensor dst( + { + {shape.x, ch, shape.y, shape.z}, + "NCHW" + }, + benchutils::GetDataType()); + cvcuda::Reformat reformatOp; + + benchutils::warmup_and_exec( + state, BENCH_STACK_WARMUP_ITERATIONS, + [&op, &reformatOp, &planarSrcTensors, &interSrcTensors, &interSrc, &interDst, &dst](cudaStream_t s) + { + for (size_t i = 0; i < planarSrcTensors.size(); ++i) + { + reformatOp(s, planarSrcTensors[i], interSrcTensors[i]); // CHW -> HWC + } + op(s, interSrc, interDst); // interleaved Stack + reformatOp(s, interDst, dst); // NHWC -> NCHW + }); +} + +template +inline void stack(nvbench::state &state, nvbench::type_list) +try +{ + int3 shape = benchutils::GetShape<3, int3>(state.get_string("shape")); + auto layout = benchutils::GetStringParam(state, "layout", "NHWC"); + const benchutils::InputKind inputKind = benchutils::GetInputKind(state.get_string("inputKind")); + + const bool isPlanar = layout == "NCHW"; + const bool isFakePlanar = layout == "NCHW_FAKE"; + if (layout != "NHWC" && layout != "NCHW" && layout != "NCHW_FAKE") + { + state.skip("Stack benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts"); + return; + } + if (isFakePlanar && inputKind != benchutils::InputKind::Tensor) + { + state.skip("Fake-planar (NCHW_FAKE) Stack benchmark is TensorBatch-only"); + return; + } + + using BT = typename nvcv::cuda::BaseType; + + int ch = nvcv::cuda::NumElements; + + const long bytes = static_cast(shape.x) * shape.y * shape.z * sizeof(T); + state.add_global_memory_reads((isFakePlanar ? 3 : 1) * bytes); + state.add_global_memory_writes((isFakePlanar ? 3 : 1) * bytes); + + cvcuda::Stack op; + + // clang-format off + + if (isFakePlanar) + { + RunFakePlanarStackBench(state, op, shape, ch); + } + else if (inputKind == benchutils::InputKind::Tensor) + { + nvcv::Tensor dst = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::TensorBatch src(nvcv::TensorBatch::CalcRequirements(shape.x)); + + for (int i = 0 ; i < shape.x; i++) + { + nvcv::Tensor srcIn = isPlanar + ? nvcv::Tensor({{ch, shape.y, shape.z}, "CHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.y, shape.z, ch}, "HWC"}, benchutils::GetDataType()); + benchutils::FillTensor(srcIn, benchutils::CheckerboardValues()); + src.pushBack(srcIn); + } + + benchutils::warmup_and_exec(state, BENCH_STACK_WARMUP_ITERATIONS, + [&op, &src, &dst](cudaStream_t s) { op(s, src, dst); }); + } + else + { + nvcv::Tensor dst = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::ImageBatchVarShape src(shape.x); + if (isPlanar && ch > 1) + { + benchutils::FillPlanarImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}); + } + else + { + benchutils::FillImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}, + benchutils::CheckerboardValues()); + } + + benchutils::warmup_and_exec(state, BENCH_STACK_WARMUP_ITERATIONS, + [&op, &src, &dst](cudaStream_t s) { op(s, src, dst); }); + } + +} +CVCUDA_BENCH_SKIP_ERRORS(state) + +// clang-format on + +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(stack, NVBENCH_TYPE_AXES(BENCH_STACK_TYPES)) +BENCH_STACK_AXES; diff --git a/bench/cpp/ops/BenchThreshold.cpp b/bench/cpp/ops/BenchThreshold.cpp new file mode 100644 index 000000000..63e08e04d --- /dev/null +++ b/bench/cpp/ops/BenchThreshold.cpp @@ -0,0 +1,135 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchThresholdConfig.hpp" + +#include +#include + +#include + +template +inline void threshold(nvbench::state &state, nvbench::type_list) +try +{ + int3 shape = benchutils::GetShape<3, int3>(state.get_string("shape")); + const benchutils::InputKind inputKind = benchutils::GetInputKind(state.get_string("inputKind")); + auto layout = benchutils::GetStringParam(state, "layout", "NHWC"); + + using BT = typename nvcv::cuda::BaseType; + + int ch = nvcv::cuda::NumElements; + + const bool isPlanar = layout == "NCHW"; + const bool isFakePlanar = layout == "NCHW_FAKE"; + if (layout != "NHWC" && layout != "NCHW" && layout != "NCHW_FAKE") + { + state.skip("Threshold benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts"); + return; + } + if (isFakePlanar && inputKind == benchutils::InputKind::VarShape) + { + state.skip("Fake-planar (NCHW_FAKE) Threshold benchmark is tensor-only"); + return; + } + if (isPlanar && inputKind == benchutils::InputKind::VarShape && ch == 1) + { + state.skip("Single-channel varshape Threshold has no distinct planar image layout"); + return; + } + + uint32_t threshType = NVCV_THRESH_BINARY | (std::is_same_v && ch == 1 ? NVCV_THRESH_OTSU : 0); + + state.add_global_memory_reads(shape.x * shape.y * shape.z * sizeof(T)); + state.add_global_memory_writes(shape.x * shape.y * shape.z * sizeof(T)); + + cvcuda::Threshold op(threshType, shape.x); + + // clang-format off + + nvcv::Tensor thresh({{shape.x}, "N"}, nvcv::TYPE_F64); + nvcv::Tensor maxval({{shape.x}, "N"}, nvcv::TYPE_F64); + + // Default-range LcgValues() so the GPU LCG fast path produces + // float64 [-1, +1] bytes identical to the Python bench's + // create_tensor(..., dtype="float64", fill_mode="lcg"). + benchutils::FillTensor(thresh, benchutils::LcgValues()); + benchutils::FillTensor(maxval, benchutils::LcgValues()); + + if (isFakePlanar) // tensor-only: planar->interleaved->threshold->interleaved->planar + { + nvcv::Tensor src ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + nvcv::Tensor interSrc({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor interDst({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + cvcuda::Reformat reformatOp; + + benchutils::warmup_and_exec(state, BENCH_THRESHOLD_WARMUP_ITERATIONS, + [&op, &reformatOp, &src, &interSrc, &interDst, &dst, &thresh, &maxval](cudaStream_t s) { + reformatOp(s, src, interSrc); + op(s, interSrc, interDst, thresh, maxval); + reformatOp(s, interDst, dst); + }); + } + else if (inputKind == benchutils::InputKind::Tensor) + { + nvcv::Tensor src = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + benchutils::warmup_and_exec(state, BENCH_THRESHOLD_WARMUP_ITERATIONS, + [&op, &src, &dst, &thresh, &maxval](cudaStream_t s) { op(s, src, dst, thresh, maxval); }); + } + else // zero and positive var shape means use ImageBatchVarShape + { + nvcv::ImageBatchVarShape src(shape.x); + nvcv::ImageBatchVarShape dst(shape.x); + + if (isPlanar) + { + benchutils::FillPlanarImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}); + benchutils::FillPlanarImageBatchLike(dst, src); + } + else + { + benchutils::FillImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}, + benchutils::CheckerboardValues()); + // Use FillImageBatchLike to ensure dst has same per-sample shapes as src + benchutils::FillImageBatchLike(dst, src, [](const long4_16a &) { return T{0}; }); + } + + benchutils::warmup_and_exec(state, BENCH_THRESHOLD_WARMUP_ITERATIONS, + [&op, &src, &dst, &thresh, &maxval](cudaStream_t s) { op(s, src, dst, thresh, maxval); }); + } +} +CVCUDA_BENCH_SKIP_ERRORS(state) + +// clang-format on + +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(threshold, NVBENCH_TYPE_AXES(BENCH_THRESHOLD_TYPES)) +BENCH_THRESHOLD_AXES; diff --git a/bench/cpp/ops/BenchWarpAffine.cpp b/bench/cpp/ops/BenchWarpAffine.cpp new file mode 100644 index 000000000..5756bcdfb --- /dev/null +++ b/bench/cpp/ops/BenchWarpAffine.cpp @@ -0,0 +1,149 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchWarpAffineConfig.hpp" + +#include +#include + +#include + +template +inline void warpaffine(nvbench::state &state, nvbench::type_list) +try +{ + int3 shape = benchutils::GetShape<3, int3>(state.get_string("shape")); + auto layout = benchutils::GetStringParam(state, "layout", "NHWC"); + const benchutils::InputKind inputKind = benchutils::GetInputKind(state.get_string("inputKind")); + + NVCVBorderType borderType = benchutils::GetBorderType(state.get_string("border")); + NVCVInterpolationType interpType = benchutils::GetInterpolationType(state.get_string("interpolation")); + + int flags = interpType | ((state.get_string("inverseMap") == "Y") ? NVCV_WARP_INVERSE_MAP : 0); + + float4 borderValue{0, 0, 0, 0}; + + NVCVAffineTransform transMatrix{2.f, 2.f, 0.f, 3.f, 1.f, 0.f}; + + const bool isPlanar = layout == "NCHW"; + const bool isFakePlanar = layout == "NCHW_FAKE"; + if (layout != "NHWC" && layout != "NCHW" && layout != "NCHW_FAKE") + { + state.skip("WarpAffine benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts"); + return; + } + // NCHW_FAKE ("fake planar") is a tensor-only comparison path: planar data is reformatted to + // interleaved, warped with the interleaved kernel, and reformatted back to planar — all timed + // together — so the native planar path (NCHW) can be shown to be faster than this naive + // convert→warp→convert pipeline. + if (isFakePlanar && inputKind == benchutils::InputKind::VarShape) + { + state.skip("Fake-planar (NCHW_FAKE) WarpAffine benchmark is tensor-only"); + return; + } + + using BT = typename nvcv::cuda::BaseType; + int ch = nvcv::cuda::NumElements; + + // WarpAffine preserves size, so src and dst hold the same number of bytes. + const long bytes = static_cast(shape.x) * shape.y * shape.z * sizeof(T); + if (isFakePlanar) + { + state.add_global_memory_reads(3 * bytes + 6 * sizeof(float)); + state.add_global_memory_writes(3 * bytes); + } + else + { + state.add_global_memory_reads(bytes + 6 * sizeof(float)); + state.add_global_memory_writes(bytes); + } + + cvcuda::WarpAffine op(shape.x); + + // clang-format off + + if (isFakePlanar) // tensor-only: planar→interleaved→warp→interleaved→planar + { + nvcv::Tensor src ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + nvcv::Tensor interSrc({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor interDst({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + cvcuda::Reformat reformatOp; + + benchutils::warmup_and_exec(state, BENCH_WARPAFFINE_WARMUP_ITERATIONS, + [&op, &reformatOp, &src, &interSrc, &interDst, &dst, &transMatrix, &flags, &borderType, &borderValue](cudaStream_t s) { + reformatOp(s, src, interSrc); // NCHW → NHWC + op(s, interSrc, interDst, transMatrix, flags, borderType, borderValue); // interleaved warp + reformatOp(s, interDst, dst); // NHWC → NCHW + }); + } + else if (inputKind == benchutils::InputKind::Tensor) + { + nvcv::Tensor src = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + benchutils::warmup_and_exec(state, BENCH_WARPAFFINE_WARMUP_ITERATIONS, + [&op, &src, &dst, &transMatrix, &flags, &borderType, &borderValue](cudaStream_t s) { + op(s, src, dst, transMatrix, flags, borderType, borderValue); + }); + } + else // zero and positive var shape means use ImageBatchVarShape + { + nvcv::ImageBatchVarShape src(shape.x); + nvcv::ImageBatchVarShape dst(shape.x); + + if (isPlanar) + { + benchutils::FillPlanarImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}); + benchutils::FillPlanarImageBatch(dst, long2{shape.z, shape.y}, long2{0, 0}); + } + else + { + benchutils::FillImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}, + benchutils::CheckerboardValues()); + // Use FillImageBatchLike to ensure dst has same per-sample shapes as src + benchutils::FillImageBatchLike(dst, src, [](const long4_16a &) { return T{0}; }); + } + + nvcv::Tensor transMatrixTensor({{shape.x, 6}, "NW"}, nvcv::TYPE_F32); + + benchutils::FillTensor(transMatrixTensor, [&transMatrix](const long4_16a &c){ return transMatrix[c.y]; }); + + benchutils::warmup_and_exec(state, BENCH_WARPAFFINE_WARMUP_ITERATIONS, + [&op, &src, &dst, &transMatrixTensor, &flags, &borderType, &borderValue](cudaStream_t s) { + op(s, src, dst, transMatrixTensor, flags, borderType, borderValue); + }); + } +} +CVCUDA_BENCH_SKIP_ERRORS(state) + +// clang-format on + +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(warpaffine, NVBENCH_TYPE_AXES(BENCH_WARPAFFINE_TYPES)) +BENCH_WARPAFFINE_AXES; diff --git a/bench/cpp/ops/BenchWarpPerspective.cpp b/bench/cpp/ops/BenchWarpPerspective.cpp new file mode 100644 index 000000000..ec526aac9 --- /dev/null +++ b/bench/cpp/ops/BenchWarpPerspective.cpp @@ -0,0 +1,144 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/BenchWarpPerspectiveConfig.hpp" + +#include +#include + +#include + +template +inline void warpperspective(nvbench::state &state, nvbench::type_list) +try +{ + int3 shape = benchutils::GetShape<3, int3>(state.get_string("shape")); + auto layout = benchutils::GetStringParam(state, "layout", "NHWC"); + const benchutils::InputKind inputKind = benchutils::GetInputKind(state.get_string("inputKind")); + + NVCVBorderType borderType = benchutils::GetBorderType(state.get_string("border")); + NVCVInterpolationType interpType = benchutils::GetInterpolationType(state.get_string("interpolation")); + + int flags = interpType | ((state.get_string("inverseMap") == "Y") ? NVCV_WARP_INVERSE_MAP : 0); + + float4 borderValue{0, 0, 0, 0}; + + NVCVPerspectiveTransform transMatrix{0.27f, 0.16f, 0.00f, -0.11f, 0.61f, 0.65f, -0.09f, 0.06f, 1.00f}; + + const bool isPlanar = layout == "NCHW"; + const bool isFakePlanar = layout == "NCHW_FAKE"; + if (layout != "NHWC" && layout != "NCHW" && layout != "NCHW_FAKE") + { + state.skip("WarpPerspective benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts"); + return; + } + if (isFakePlanar && inputKind == benchutils::InputKind::VarShape) + { + state.skip("Fake-planar (NCHW_FAKE) WarpPerspective benchmark is tensor-only"); + return; + } + + using BT = typename nvcv::cuda::BaseType; + int ch = nvcv::cuda::NumElements; + + const long bytes = static_cast(shape.x) * shape.y * shape.z * sizeof(T); + if (isFakePlanar) + { + state.add_global_memory_reads(3 * bytes + 9 * sizeof(float)); + state.add_global_memory_writes(3 * bytes); + } + else + { + state.add_global_memory_reads(bytes + 9 * sizeof(float)); + state.add_global_memory_writes(bytes); + } + + cvcuda::WarpPerspective op(shape.x); + + // clang-format off + + if (isFakePlanar) // tensor-only: planar -> interleaved -> warp -> interleaved -> planar + { + nvcv::Tensor src ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + nvcv::Tensor interSrc({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor interDst({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst ({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + cvcuda::Reformat reformatOp; + + benchutils::warmup_and_exec(state, BENCH_WARPPERSPECTIVE_WARMUP_ITERATIONS, + [&op, &reformatOp, &src, &interSrc, &interDst, &dst, &transMatrix, &flags, &borderType, &borderValue](cudaStream_t s) { + reformatOp(s, src, interSrc); + op(s, interSrc, interDst, transMatrix, flags, borderType, borderValue); + reformatOp(s, interDst, dst); + }); + } + else if (inputKind == benchutils::InputKind::Tensor) + { + nvcv::Tensor src = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + nvcv::Tensor dst = isPlanar + ? nvcv::Tensor({{shape.x, ch, shape.y, shape.z}, "NCHW"}, benchutils::GetDataType()) + : nvcv::Tensor({{shape.x, shape.y, shape.z, ch}, "NHWC"}, benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + benchutils::warmup_and_exec(state, BENCH_WARPPERSPECTIVE_WARMUP_ITERATIONS, + [&op, &src, &dst, &transMatrix, &flags, &borderType, &borderValue](cudaStream_t s) { + op(s, src, dst, transMatrix, flags, borderType, borderValue); + }); + } + else // zero and positive var shape means use ImageBatchVarShape + { + nvcv::ImageBatchVarShape src(shape.x); + nvcv::ImageBatchVarShape dst(shape.x); + + if (isPlanar) + { + benchutils::FillPlanarImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}); + benchutils::FillPlanarImageBatch(dst, long2{shape.z, shape.y}, long2{0, 0}); + } + else + { + benchutils::FillImageBatch(src, long2{shape.z, shape.y}, long2{0, 0}, + benchutils::CheckerboardValues()); + // Use FillImageBatchLike to ensure dst has same per-sample shapes as src + benchutils::FillImageBatchLike(dst, src, [](const long4_16a &) { return T{0}; }); + } + + nvcv::Tensor transMatrixTensor({{shape.x, 9}, "NW"}, nvcv::TYPE_F32); + + benchutils::FillTensor(transMatrixTensor, [&transMatrix](auto &c){ return transMatrix[c.y]; }); + + benchutils::warmup_and_exec(state, BENCH_WARPPERSPECTIVE_WARMUP_ITERATIONS, + [&op, &src, &dst, &transMatrixTensor, &flags, &borderType, &borderValue](cudaStream_t s) { + op(s, src, dst, transMatrixTensor, flags, borderType, borderValue); + }); + } +} +CVCUDA_BENCH_SKIP_ERRORS(state) + +// clang-format on + +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(warpperspective, NVBENCH_TYPE_AXES(BENCH_WARPPERSPECTIVE_TYPES)) +BENCH_WARPPERSPECTIVE_AXES; diff --git a/bench/python/CMakeLists.txt b/bench/python/CMakeLists.txt new file mode 100644 index 000000000..f7fdd5eea --- /dev/null +++ b/bench/python/CMakeLists.txt @@ -0,0 +1,116 @@ +# SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +project(cvcuda_bench_python) + +# Copy Python benchmark scripts to build directory +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/python_bench_utils.py + ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/python_bench_utils.py + COPYONLY) + +# List of all Python benchmark scripts +set(python_bench_scripts + ops/bench_jpegcompressiondistortion.py + ops/bench_adjusthue.py + ops/bench_adjustsaturation.py + ops/bench_adjustsharpness.py + ops/bench_adjustcontrast.py + ops/bench_invert.py + ops/bench_solarize.py + ops/bench_posterize.py + ops/bench_autocontrast.py + ops/bench_adaptivethreshold.py + ops/bench_advcvtcolor.py + ops/bench_averageblur.py + ops/bench_bilateralfilter.py + ops/bench_bndbox.py + ops/bench_boxblur.py + ops/bench_brightnesscontrast.py + ops/bench_centercrop.py + ops/bench_channelreorder.py + ops/bench_clahe.py + ops/bench_colortwist.py + ops/bench_composite.py + ops/bench_conv2d.py + ops/bench_convertto.py + ops/bench_copymakeborder.py + ops/bench_cropflipnormalizereformat.py + ops/bench_customcrop.py + ops/bench_cvtcolor.py + ops/bench_erase.py + ops/bench_findhomography.py + ops/bench_flip.py + ops/bench_gammacontrast.py + ops/bench_gaussian.py + ops/bench_gaussiannoise.py + ops/bench_histogram.py + ops/bench_histogrameq.py + ops/bench_hqresize.py + ops/bench_inpaint.py + ops/bench_jointbilateralfilter.py + ops/bench_label.py + ops/bench_laplacian.py + ops/bench_medianblur.py + ops/bench_minarearect.py + ops/bench_minmaxloc.py + ops/bench_morphology.py + ops/bench_nonmaximumsuppression.py + ops/bench_normalize.py + ops/bench_osd.py + ops/bench_padandstack.py + ops/bench_pairwisematcher.py + ops/bench_pillowresize.py + ops/bench_randomresizedcrop.py + ops/bench_reformat.py + ops/bench_remap.py + ops/bench_resize.py + ops/bench_resizecropconvertreformat.py + ops/bench_rotate.py + ops/bench_sift.py + ops/bench_stack.py + ops/bench_threshold.py + ops/bench_warpaffine.py + ops/bench_warpperspective.py +) + +# Copy benchmark scripts to build directory (flattened - no ops subdirectory) +foreach(script IN LISTS python_bench_scripts) + get_filename_component(script_name "${script}" NAME) + configure_file(${CMAKE_CURRENT_SOURCE_DIR}/${script} + ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${script_name} + COPYONLY) +endforeach() + +# Install Python benchmark scripts +install(PROGRAMS + ${CMAKE_CURRENT_SOURCE_DIR}/python_bench_utils.py + ${CMAKE_CURRENT_SOURCE_DIR}/install_bench_dependencies.sh + DESTINATION ${CMAKE_INSTALL_BINDIR} + COMPONENT bench) + +# Install requirements files for dependency management +install(FILES + ${CMAKE_CURRENT_SOURCE_DIR}/requirements.bench.common.txt + ${CMAKE_CURRENT_SOURCE_DIR}/requirements.bench.cu12.txt + ${CMAKE_CURRENT_SOURCE_DIR}/requirements.bench.cu13.txt + DESTINATION ${CMAKE_INSTALL_BINDIR} + COMPONENT bench) + +# Install benchmark scripts (flattened - no ops subdirectory) +foreach(script IN LISTS python_bench_scripts) + install(PROGRAMS ${CMAKE_CURRENT_SOURCE_DIR}/${script} + DESTINATION ${CMAKE_INSTALL_BINDIR} + COMPONENT bench) +endforeach() diff --git a/bench/python/README.md b/bench/python/README.md deleted file mode 100644 index 41f0c2854..000000000 --- a/bench/python/README.md +++ /dev/null @@ -1,280 +0,0 @@ - -[//]: # "SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved." -[//]: # "SPDX-License-Identifier: Apache-2.0" -[//]: # "" -[//]: # "Licensed 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." - - -# Python Operator Performance Benchmarking - -Using various performance benchmarking scripts that ships with CV-CUDA samples, we can measure and report the performance of various CV-CUDA operators from Python. - -The following scripts are part of the performance benchmarking tools in CV-CUDA. - -1. `bench/python/benchmark.py` -2. `bench/python/perf_utils.py` -3. `bench/python/bench_utils.py` - -We use NVIDIA NSYS to profile the code for its CPU and GPU run-times. Profiling is done at the operator level using NVTX style markers to annotate the code using `push_range()` and `pop_range()` methods, then benchmarking is run to collect the timing information of all such ranges. Please refer to the NVIDIA NSIGHT user guide to learn more about NSIGHT and NVTX trace (https://docs.nvidia.com/nsight-systems/UserGuide/index.html) - -## Installation - -### Prerequisites - -- **CUDA Toolkit**: CUDA 12.2+ or CUDA 13.0+ -- **Python**: 3.10 - 3.13 (3.10+ recommended for benchmarking) -- **System**: Ubuntu 22.04+ or compatible Linux distribution - -### System Dependencies - -Run the installation script to install system dependencies, NVIDIA Nsight Systems, and Python packages: - -```shell -cd bench/python -./install_dependencies.sh -``` - -**Note:** The script will prompt for your sudo password when installing system packages. - -This script will: -- Detect your CUDA version (supports CUDA 12 and CUDA 13) -- Install NVIDIA Nsight Systems 2025.5.1 (required for profiling) -- Install ffmpeg and other media processing libraries -- Set up build tools and dependencies -- **Create a Python virtual environment and install all required Python packages** - -**Note**: If you already have NSYS installed, the script will skip the installation. Minimum required version: NSYS 2025.5.1. - -After installation, activate the virtual environment: -```shell -source venv_bench/bin/activate -``` - -### Python Environment - -The `install_dependencies.sh` script automatically sets up a Python virtual environment and installs all required dependencies from self-contained requirements files in the `bench/python/` directory. - -**Manual Installation (if needed):** - -If you need to reinstall Python packages or set up on a system where you've already installed system dependencies: - -```shell -cd bench/python - -# Ensure you have system dependencies installed -# If not, run: sudo ./install_dependencies.sh - -# Activate the virtual environment (created by install_dependencies.sh) -source venv_bench/bin/activate - -# Reinstall Python packages for your CUDA version -# For CUDA 12: -python3 -m pip install -r requirements_cu12.txt - -# For CUDA 13: -python3 -m pip install -r requirements_cu13.txt -``` - -**Note:** All Python dependencies are self-contained in three requirements files: -- `requirements_common.txt`: Common dependencies (NumPy, benchmarking tools) -- `requirements_cu12.txt`: CUDA 12-specific packages (includes common requirements) -- `requirements_cu13.txt`: CUDA 13-specific packages (includes common requirements) - -## benchmark.py - -This is the main launcher script responsible for launching operator benchmarks. After launching it: - -1. Coordinates the entire process of benchmarking with NSYS. -2. Parses various results returned by NSYS. -3. Stores per-process results in a JSON file. -4. Automatically calculates average numbers across all processes and also saves them in a JSON file. - -## perf_utils.py - -This file holds the data structures and functions most commonly used during the benchmarking process: - -1. It contains the `CvCudaPerf` class to hold performance data with an API similar to NVTX. -2. Provides a way to maximize the GPU clocks before benchmarking. -3. Provides a command-line argument parser that can be shared across all benchmarks to maintain uniformity in the way of passing the inputs. - -The `CvCudaPerf` class is used to mark the portions of code that one wants to benchmark using its `push_range()` and `pop_range()` methods. These methods are similar to NVTX except that it adds two new features on top that allows us to compute more detailed numbers: -1. It can record the start of a batch. A batch is a logical group of operations that often repeats. -2. It can record the end of a batch, with its batch size. - -## About the Operator Benchmarks - -Operators for which a test case has been implemented in the `all_ops` folder can be benchmarked. The following statements are true for all such test cases: - -1. All inherit from a base class called `AbstractOpBase` which allows them to expose benchmarking capabilities in a consistent manner. They all have a setup stage, a run stage and an optional visualization stage. By default, the visualization is turned off. -2. All receive the same input image. Some operators may need to read additional data. Such data is always read from the `assets` directory. -3. All run for a number of iterations (default is set to 10) and a batch size (default is set to 32). -4. The script `benchmark.py` handles overall benchmarking. It launches the runs, monitors it, communicates with NSYS and saves the results of a run in a JSON file. Various settings such as using warm-up (default is set to 1 iteration) are handled here. -5. One or more benchmark runs can be compared and summarized in a table showing only the important information from the detailed JSON files. - -## Setting up the environment - -1. Follow [Setting up the environment](../../samples/README.md#setting-up-the-environment) section of the CV-CUDA samples. Note: The step asking to install dependencies can be ignored if you are only interested in benchmarking the operators (and not the samples). - - -## Running the benchmark - -The script `run_bench.py` together with `benchmark.py` can be used to automatically benchmark all supported CV-CUDA operators in Python. Additionally, one or more runs can be summarized and compared in a table using the functionality provided by `bench_utils.py` - - -### To run the operator benchmarks - -```bash -python3 bench/python/benchmark.py -o bench/python/run_bench.py -``` -- Where: - 1. An `OUTPUT_DIR` must be given to store various benchmark artifacts. -- Upon running it will: - 1. Ask the `benchmark.py` to launch the `run_bench.py`. - 2. `run_bench.py` will then find out all the operators that can be benchmarked. - 3. Run those one by one, through all the stages, such as setup, run and visualization (if enabled). - 4. Store the artifacts in the output folder. This is where the `benchmark.py` style `benchmark_mean.json` would be stored. - -Once a run is completed, one can use the `bench_utils.py` to summarize it. Additionally, we can use the same script to compare multiple different runs. - -### To summarize one run only - -```bash -python3 bench/python/bench_utils.py -o -b -bn baseline -``` -- Where: - 1. A `OUTPUT_DIR` must be given to store the summary table as a CSV file. - 2. The first run's `benchmark_mean.json` path must be given as `b`. - 3. The display name of the first run must be given as `bn`. -- Upon running it will: - 1. Grab appropriate values from the JSON file for all the operators and put it in a table format. - 2. Save the table as a CSV file. - -The output CSV file will be stored in the `OUTPUT_DIR` with current date and time on it. - -NOTE: `benchmark.py` will produce additional JSON files (and visualization files if it was enabled). These files provide way more detailed information compared to the CSV and is usually only meant for debugging purposes. - - -### To summarize and compare multiple runs - -```bash -python3 bench/python/bench_utils.py -o -b -bn baseline \ - -c -cn run_2 \ - -c -cn run_3 -``` -- Where: - 1. An `OUTPUT_DIR` must be given to store the summary table as a CSV file. - 2. The first run's `benchmark_mean.json` path is given as `b`. - 3. The display name of the first run is given as `bn`. - 4. The second run's `benchmark_mean.json` path is given as `c`. - 5. The display name of the second run is given as `cn`. - 6. The third run's `benchmark_mean.json` path is given as `c`. - 7. The display name of the third run must be given as `cn`. - 8. Options `c` and `cn` can be repeated as zero or more times to cover all the runs. -- Upon running it will: - 1. Grab appropriate values from the JSON file for all the operators and put it in a table format. - 2. Save the table as a CSV file. - - -## Interpreting the results - -Upon a successful completion of the benchmarking process, we get several files with performance data: - -### Understanding the benchmark JSON files - -The `benchmark.py` script produces the following files: - -1. **Per process statistics** in `benchmark.json` files. These are stored in `/proc_X_gpu_Y` where `OUTPUT_DIR` is the directory used to store the output, `X` is the CPU index and `Y` is the GPU index. - - In each `benchmark.json` file one can see an overall structure like this: - ```json - { - "data": {}, - "mean_data": {}, - "meta": {} - } - ``` - - - The `data` key stores the per batch data maintaining the hierarchy of the pipeline. At each non-batch level, it stores the following information: - ```json - "pipeline": { - "batch_0": { - "stage_1": { - "cpu_time": 300.797, - "gpu_time": 15.2345 - }, - "stage_2": { - "cpu_time": 300.416, - "gpu_time": 28.5678 - }, - "cpu_time": 601.373, - "gpu_time": 43.8023, - "total_items": 1, - "cpu_time_per_item": 601.373, - "gpu_time_per_item": 43.8023 - } - } - ``` - One can see the `cpu_time` and `gpu_time` per stage (in milliseconds). The `total_items` (i.e. the batch size) is also tracked at the batch level and per item numbers are computed from it. - - - At the batch level, the statistics are aggregated from all the batches and reported considering the warm-up batches. The `*_minus_warmup` timings are the ones which ignore the warm-up batches from the computation: - ```json - { - "cpu_time": 1805.027, - "gpu_time": 131.4069, - "cpu_time_per_item": 601.676, - "gpu_time_per_item": 43.8023, - "total_items": 3, - "cpu_time_minus_warmup": 601.759, - "gpu_time_minus_warmup": 87.6046, - "cpu_time_per_item_minus_warmup": 601.759, - "gpu_time_per_item_minus_warmup": 43.8023, - "total_items_minus_warmup": 2 - } - ``` - - - The `mean_data` key stores the average of all numbers across all the batches. - - The `meta` key stores various metadata about the run. This may be useful for reproducibility purposes. - -2. **Overall statistics** in the `benchmark_mean.json` file. This file will be stored in ``. - - In `benchmark_mean.json` file one can see an overall structure like this: - ```json - { - "mean_all_batches": {}, - "mean_data": {}, - "meta": {} - } - ``` - - - The `mean_all_batches` key stores average per batch numbers from all processes launched by the `benchmark.py`. These are essentially the mean of the `data` field reported in the per process' `benchmark.json` file and maintains the overall pipeline hierarchy. - - The `mean_data` key stores the average numbers from all batches from all processes. These are essentially the mean of the `mean_data` reported in the per process' `benchmark.json` file. - - The `meta` key stores various metadata about the run. This may be useful for reproducibility purposes. - -**NOTE**: `benchmark.py` will produce additional JSON files (and visualization files if it was enabled). These files provide way more detailed information compared to the CSV and is usually only meant for debugging purposes. - -### Understanding the summary CSV files - -Upon a successful completion of the `bench_utils.py` script, we get a CSV file that summarizes the benchmark results. - -- If you ran it only on one run, your CSV will only have four columns - showing data only from that run: - 1. `index`: from 0 to N-1 for all the N operators benchmarked - 2. `operator name` The name of the operator - 3. `baseline run time (ms)`: The first run's time in milliseconds, averaged across M iterations (default is 10, with warm-up runs discarded) - 4. `run time params`: Any helpful parameters supplied to the operator as it ran in first run. Only lists primitive data-types. - -- If you ran it on more than one runs, your CSV file will have additional columns - comparing data of those runs with the baseline run. Additional columns, per run, would be: - 1. `run i time (ms)`: The ith run's time in milliseconds, averaged across M iterations (default is 10, with warm-up runs discarded) - 2. `run i v/s baseline speed-up`: The speed-up factor. This is calculated by dividing `run i time (ms)` by `baseline run time (ms)`. - -## Regarding maximizing the clocks - -Often during the GPU benchmarking process one would like to set the GPU clocks and power to their maximum settings. While the `nvidia-smi` command and `nvml` APIs both provide various options to do so, we have consolidated these into a convenient function call `maximize_clocks()` in the `perf_utils.py` script. One can easily turn it on during the benchmarking process by passing the `--maximize_clocks` flag to the `benchmark.py` script. This will also bring the clocks down to its original values once the process is over. diff --git a/bench/python/all_ops/op_adaptivethreshold.py b/bench/python/all_ops/op_adaptivethreshold.py deleted file mode 100644 index a38728382..000000000 --- a/bench/python/all_ops/op_adaptivethreshold.py +++ /dev/null @@ -1,42 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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: One must import PyCuda driver first, before CVCUDA or VPF otherwise -# things may throw unexpected errors. -import pycuda.driver as cuda # noqa: F401 - -from bench_utils import AbstractOpBase -import cvcuda - - -class OpAdaptiveThreshold(AbstractOpBase): - def setup(self, input): - super().setup(input) - self.maxval = 255.0 - self.adaptive_method = cvcuda.AdaptiveThresholdType.GAUSSIAN_C - self.threshold_type = cvcuda.ThresholdType.BINARY - self.block_size = 11 - self.c = 2 - self.grayscale_input = cvcuda.cvtcolor(input, cvcuda.ColorConversion.RGB2GRAY) - - def run(self, input): - return cvcuda.adaptivethreshold( - self.grayscale_input, - max_value=self.maxval, - adaptive_method=self.adaptive_method, - threshold_type=self.threshold_type, - block_size=self.block_size, - c=self.c, - ) diff --git a/bench/python/all_ops/op_as_image.py b/bench/python/all_ops/op_as_image.py deleted file mode 100644 index e2230ee5d..000000000 --- a/bench/python/all_ops/op_as_image.py +++ /dev/null @@ -1,39 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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 cvcuda - -# NOTE: One must import PyCuda driver first, before CVCUDA or VPF otherwise -# things may throw unexpected errors. -import pycuda.driver as cuda # noqa: F401 -from bench_utils import AbstractOpBase - - -class OpAsImageFromCVCUDAImage(AbstractOpBase): - def setup(self, input): - super().setup(input) - # dummy run that does not use cache - img = cvcuda.Image((128, 128), cvcuda.Format.RGBA8) - - self.imglist = [] - for _ in range(10): - img = cvcuda.Image((128, 128), cvcuda.Format.RGBA8) - self.imglist.append(img.cuda()) - self.cycle = 0 - - def run(self, input): - cvcuda.as_image(self.imglist[self.cycle % len(self.imglist)]) - self.cycle += 1 - return diff --git a/bench/python/all_ops/op_as_images.py b/bench/python/all_ops/op_as_images.py deleted file mode 100644 index 2ada46445..000000000 --- a/bench/python/all_ops/op_as_images.py +++ /dev/null @@ -1,43 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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 cvcuda - -# NOTE: One must import PyCuda driver first, before CVCUDA or VPF otherwise -# things may throw unexpected errors. -import pycuda.driver as cuda # noqa: F401 -from bench_utils import AbstractOpBase - - -class OpAsImagesFromCVCUDAImage(AbstractOpBase): - def setup(self, input): - super().setup(input) - # dummy run that does not use cache - cvcuda.ImageBatchVarShape(100) - img = cvcuda.Image((128, 128), cvcuda.Format.RGBA8) - - self.imglists = [] - for _ in range(10): - imglist = [] - for _ in range(100): - img = cvcuda.Image((128, 128), cvcuda.Format.RGBA8) - imglist.append(img.cuda()) - self.imglists.append(imglist) - self.cycle = 0 - - def run(self, input): - cvcuda.as_images(self.imglists[self.cycle % len(self.imglists)]) - self.cycle += 1 - return diff --git a/bench/python/all_ops/op_averageblur.py b/bench/python/all_ops/op_averageblur.py deleted file mode 100644 index 444a65b64..000000000 --- a/bench/python/all_ops/op_averageblur.py +++ /dev/null @@ -1,33 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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: One must import PyCuda driver first, before CVCUDA or VPF otherwise -# things may throw unexpected errors. -import pycuda.driver as cuda # noqa: F401 - -from bench_utils import AbstractOpBase -import cvcuda - - -class OpAverageBlur(AbstractOpBase): - def setup(self, input): - super().setup(input) - self.kernel_size = (3, 3) - self.kernel_anchor = (-1, -1) - - def run(self, input): - return cvcuda.averageblur( - input, kernel_size=self.kernel_size, kernel_anchor=self.kernel_anchor - ) diff --git a/bench/python/all_ops/op_blurbox.py b/bench/python/all_ops/op_blurbox.py deleted file mode 100644 index 18405cc01..000000000 --- a/bench/python/all_ops/op_blurbox.py +++ /dev/null @@ -1,81 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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: One must import PyCuda driver first, before CVCUDA or VPF otherwise -# things may throw unexpected errors. -import pycuda.driver as cuda # noqa: F401 - -from bench_utils import AbstractOpBase -import cvcuda -import torch -from torchvision.io import read_image -import os - - -class OpBlurBox(AbstractOpBase): - def setup(self, input): - super().setup(input) - self.kernel_size = 5 - - data = read_image(os.path.join(self.assets_dir, "brooklyn.jpg")) - data = data.moveaxis(0, -1).contiguous() # From CHW to HWC - data = data.cuda(self.device_id) - data = [data.clone() for _ in range(input.shape[0])] - data = torch.stack(data) - self.input = cvcuda.as_tensor(data, "NHWC") - - bboxes = torch.load( - os.path.join(self.assets_dir, "brooklyn_bboxes.pt"), - map_location="cuda:%d" % self.device_id, - ) - bboxes = [bboxes[0].clone() for _ in range(input.shape[0])] - self.bboxes_pyt = torch.stack(bboxes) - bboxes = cvcuda.as_tensor(self.bboxes_pyt) - - scores = torch.load( - os.path.join(self.assets_dir, "brooklyn_scores.pt"), - map_location="cuda:%d" % self.device_id, - ) - scores = [scores[0].clone() for _ in range(input.shape[0])] - scores = torch.stack(scores) - scores = cvcuda.as_tensor(scores) - - self.nms_masks_pyt = torch.load( - os.path.join(self.assets_dir, "brooklyn_nms_masks.pt"), - map_location="cuda:%d" % self.device_id, - ) - - def run(self, input): - blur_boxes = [] - # Create an array of bounding boxes with render settings. - for current_boxes, current_masks in zip(self.bboxes_pyt, self.nms_masks_pyt): - filtered_boxes = current_boxes[current_masks] - BlurBoxI_list = [] - - for box in filtered_boxes: - BlurBoxI_list.append( - cvcuda.BlurBoxI( - box=tuple(box), - kernelSize=self.kernel_size, - ) - ) - - blur_boxes.append(BlurBoxI_list) - - batch_blur_boxes = cvcuda.BlurBoxesI(boxes=blur_boxes) - - cvcuda.boxblur_into(self.input, self.input, batch_blur_boxes) - - return self.input diff --git a/bench/python/all_ops/op_boundingbox.py b/bench/python/all_ops/op_boundingbox.py deleted file mode 100644 index 93b1fc2ac..000000000 --- a/bench/python/all_ops/op_boundingbox.py +++ /dev/null @@ -1,85 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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: One must import PyCuda driver first, before CVCUDA or VPF otherwise -# things may throw unexpected errors. -import pycuda.driver as cuda # noqa: F401 - -from bench_utils import AbstractOpBase -import cvcuda -import torch -from torchvision.io import read_image -import os - - -class OpBoundingBox(AbstractOpBase): - def setup(self, input): - super().setup(input) - self.border_color = (0, 255, 0, 255) - self.fill_color = (0, 0, 255, 0) - self.thickness = 5 - - data = read_image(os.path.join(self.assets_dir, "brooklyn.jpg")) - data = data.moveaxis(0, -1).contiguous() # From CHW to HWC - data = data.cuda(self.device_id) - data = [data.clone() for _ in range(input.shape[0])] - data = torch.stack(data) - self.input = cvcuda.as_tensor(data, "NHWC") - - bboxes = torch.load( - os.path.join(self.assets_dir, "brooklyn_bboxes.pt"), - map_location="cuda:%d" % self.device_id, - ) - bboxes = [bboxes[0].clone() for _ in range(input.shape[0])] - self.bboxes_pyt = torch.stack(bboxes) - bboxes = cvcuda.as_tensor(self.bboxes_pyt) - - scores = torch.load( - os.path.join(self.assets_dir, "brooklyn_scores.pt"), - map_location="cuda:%d" % self.device_id, - ) - scores = [scores[0].clone() for _ in range(input.shape[0])] - scores = torch.stack(scores) - scores = cvcuda.as_tensor(scores) - - self.nms_masks_pyt = torch.load( - os.path.join(self.assets_dir, "brooklyn_nms_masks.pt"), - map_location="cuda:%d" % self.device_id, - ) - - def run(self, input): - bounding_boxes = [] - # Create an array of bounding boxes with render settings. - for current_boxes, current_masks in zip(self.bboxes_pyt, self.nms_masks_pyt): - filtered_boxes = current_boxes[current_masks] - BndBoxI_list = [] - - for box in filtered_boxes: - BndBoxI_list.append( - cvcuda.BndBoxI( - box=tuple(box), - thickness=self.thickness, - borderColor=self.border_color, - fillColor=self.fill_color, - ) - ) - - bounding_boxes.append(BndBoxI_list) - - batch_bounding_boxes = cvcuda.BndBoxesI(boxes=bounding_boxes) - - cvcuda.bndbox_into(self.input, self.input, batch_bounding_boxes) - - return self.input diff --git a/bench/python/all_ops/op_brightnesscontrast.py b/bench/python/all_ops/op_brightnesscontrast.py deleted file mode 100644 index 11994e57c..000000000 --- a/bench/python/all_ops/op_brightnesscontrast.py +++ /dev/null @@ -1,47 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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: One must import PyCuda driver first, before CVCUDA or VPF otherwise -# things may throw unexpected errors. -import pycuda.driver as cuda # noqa: F401 - -from bench_utils import AbstractOpBase -import cvcuda -import torch - - -class OpBrightnessContrast(AbstractOpBase): - def setup(self, input): - super().setup(input) - brightness = torch.tensor([1.2]).cuda(self.device_id) - self.brightness = cvcuda.as_tensor(brightness, "N") - - contrast = torch.tensor([0.7]).cuda(self.device_id) - self.contrast = cvcuda.as_tensor(contrast, "N") - - brightness_shift = torch.tensor([130.0]).cuda(self.device_id) - self.brightness_shift = cvcuda.as_tensor(brightness_shift, "N") - - contrast_center = torch.tensor([0.5]).cuda(self.device_id) - self.contrast_center = cvcuda.as_tensor(contrast_center, "N") - - def run(self, input): - return cvcuda.brightness_contrast( - input, - brightness=self.brightness, - contrast=self.contrast, - brightness_shift=self.brightness_shift, - contrast_center=self.contrast_center, - ) diff --git a/bench/python/all_ops/op_cache_limit.py b/bench/python/all_ops/op_cache_limit.py deleted file mode 100644 index 79e0deda0..000000000 --- a/bench/python/all_ops/op_cache_limit.py +++ /dev/null @@ -1,114 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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 cvcuda -import torch - -# NOTE: One must import PyCuda driver first, before CVCUDA or VPF otherwise -# things may throw unexpected errors. -import pycuda.driver as cuda # noqa: F401 -from bench_utils import AbstractOpBase - -# For the following setup depicted in the table, we have to repeatedly call the functions: cudaMalloc and/or -# cudaFree. -# -# --------------------------------------------------------------------- -# | shape\cache limit | small large | -# |-------------------------------------------------------------------| -# | non-random | cudaMalloc + cudaFree - (best-case) | -# | random | cudaMalloc + cudaFree cudaMalloc | -# --------------------------------------------------------------------- -# -# Due to the this table, we benchmark three scenarios: {non-random, small}, {non-random, large}, -# {random, large} - - -# Base class for cache limit benchmarks, to ensure all three classes have the same overhead, leading to -# consistent numbers. -class BaseOpCacheLimit(AbstractOpBase): - def setup(self, input, new_cache_limit, low, high): - super().setup(input) - - # make this benchmark compatible with older cvcuda versions - if hasattr(cvcuda, "set_cache_limit_inbytes"): - cvcuda.set_cache_limit_inbytes(new_cache_limit) - - # We don't have access to the outer benchmark iterations (default=10), so we have to create our own - # counter. - self.max_iter_outer = 10 - self.iter_outer = 0 - - # Number of "random" tensors created per benchmarked run - self.n_tensors = 20 - self.hw = torch.randint( - low=low, high=high, size=(self.max_iter_outer, 2, self.n_tensors) - ) - - def run(self, input): - # If we exceed the outer bench iterations, we return. - # If we didn't return, we might re-use the cache, which we specifically don't want for - # "OpCacheLimitLargeAndRandom". - # For the other classes (OpCacheLimitZero, OpCacheLimitLarge), we could continue running the - # benchmarks, but then we would not get comparable numbers between all three classes - if self.iter_outer >= self.max_iter_outer: - return - - for ii in range(self.n_tensors): - shape = ( - self.hw[self.iter_outer, 0, ii].item(), - self.hw[self.iter_outer, 1, ii].item(), - 3, - ) - _ = cvcuda.Tensor(shape, cvcuda.Type.F32, cvcuda.TensorLayout.HWC) - - self.iter_outer += 1 - return - - -# This is the {non-random, small} case. The smallest we can choose is 0, so we set the cache limit to 0 and -# effectively disable the cache -class OpCacheLimitZero(BaseOpCacheLimit): - def setup(self, input): - # Set the cache limit to 0 for this benchmark - # low=1000, high=1001 results in always creating tensor's of shape (1000,1000,3) - super().setup(input, 0, low=1000, high=1001) - - def run(self, input): - super().run(input) - - -# This is the {non-random, large} case. This is the best case scenario, always re-using the cache -class OpCacheLimitLarge(BaseOpCacheLimit): - def setup(self, input): - # Set the cache limit to the total gpu memory for this benchmark - # low=1000, high=1001 results in always creating tensor's of shape (1000,1000,3) - total = torch.cuda.mem_get_info()[1] - super().setup(input, total, low=1000, high=1001) - - def run(self, input): - super().run(input) - - -# This is the {random, large} case. This is the worst case scenario, never re-using the cache -class OpCacheLimitLargeAndRandom(BaseOpCacheLimit): - def setup(self, input): - # Set the cache limit to the total gpu memory for this benchmark - # low=1000, high=2000 results in always creating tensor's of random shape - # between [(1000,1000,3), (1999,1999,3)] - total = torch.cuda.mem_get_info()[1] - super().setup(input, total, low=1000, high=2000) - - def run(self, input): - super().run(input) diff --git a/bench/python/all_ops/op_centercrop.py b/bench/python/all_ops/op_centercrop.py deleted file mode 100644 index 9de2cb38f..000000000 --- a/bench/python/all_ops/op_centercrop.py +++ /dev/null @@ -1,34 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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: One must import PyCuda driver first, before CVCUDA or VPF otherwise -# things may throw unexpected errors. -import pycuda.driver as cuda # noqa: F401 - -from bench_utils import AbstractOpBase -import cvcuda - - -class OpCenterCrop(AbstractOpBase): - def setup(self, input): - super().setup(input) - width, height = input.shape[2], input.shape[1] - self.crop_size = [width // 2, height // 2] - - def run(self, input): - return cvcuda.center_crop( - input, - self.crop_size, - ) diff --git a/bench/python/all_ops/op_composite.py b/bench/python/all_ops/op_composite.py deleted file mode 100644 index 5c7ca58d6..000000000 --- a/bench/python/all_ops/op_composite.py +++ /dev/null @@ -1,53 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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: One must import PyCuda driver first, before CVCUDA or VPF otherwise -# things may throw unexpected errors. -import pycuda.driver as cuda # noqa: F401 - -from bench_utils import AbstractOpBase -import cvcuda -import torch -from torchvision.io import read_image -import os - - -class OpComposite(AbstractOpBase): - def setup(self, input): - super().setup(input) - data = read_image(os.path.join(self.assets_dir, "brooklyn.jpg")) - data = data.moveaxis(0, -1).contiguous() # From CHW to HWC - data = data.cuda(self.device_id) - data = [data.clone() for _ in range(input.shape[0])] - data = torch.stack(data) - self.input = cvcuda.as_tensor(data, "NHWC") - self.blurred_input = cvcuda.gaussian( - self.input, kernel_size=(15, 15), sigma=(5, 5) - ) - - mask = read_image(os.path.join(self.assets_dir, "brooklyn_mask.jpg")) - mask = mask.moveaxis(0, -1).contiguous() # From CHW to HWC - mask = mask.cuda(self.device_id) - mask = [mask.clone() for _ in range(input.shape[0])] - mask = torch.stack(mask) - self.class_masks = cvcuda.as_tensor(mask, "NHWC") - - def run(self, input): - return cvcuda.composite( - self.input, - self.blurred_input, - self.class_masks, - 3, - ) diff --git a/bench/python/all_ops/op_convertto.py b/bench/python/all_ops/op_convertto.py deleted file mode 100644 index a04c71544..000000000 --- a/bench/python/all_ops/op_convertto.py +++ /dev/null @@ -1,35 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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: One must import PyCuda driver first, before CVCUDA or VPF otherwise -# things may throw unexpected errors. -import pycuda.driver as cuda # noqa: F401 - -from bench_utils import AbstractOpBase -import cvcuda - - -class OpConvertTo(AbstractOpBase): - def setup(self, input): - super().setup(input) - self.target_dtype = cvcuda.Type.F32 - self.offset = 10.2 - self.scale = 1 / 255.0 - - def run(self, input): - return cvcuda.convertto(input, self.target_dtype, self.offset, self.scale) - - def visualize(self): - pass diff --git a/bench/python/all_ops/op_copymakeborder.py b/bench/python/all_ops/op_copymakeborder.py deleted file mode 100644 index b8fac51f9..000000000 --- a/bench/python/all_ops/op_copymakeborder.py +++ /dev/null @@ -1,43 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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: One must import PyCuda driver first, before CVCUDA or VPF otherwise -# things may throw unexpected errors. -import pycuda.driver as cuda # noqa: F401 - -from bench_utils import AbstractOpBase -import cvcuda - - -class OpCopyMakeBorder(AbstractOpBase): - def setup(self, input): - super().setup(input) - self.border_mode = cvcuda.Border.CONSTANT - self.border_values = [255, 0, 0] # Border values for 3 channel RGB input. - self.top = 30 - self.left = 40 - self.bottom = 50 - self.right = 60 - - def run(self, input): - return cvcuda.copymakeborder( - input, - border_mode=self.border_mode, - border_value=self.border_values, - top=self.top, - bottom=self.bottom, - left=self.left, - right=self.right, - ) diff --git a/bench/python/all_ops/op_customcrop.py b/bench/python/all_ops/op_customcrop.py deleted file mode 100644 index 1941e3216..000000000 --- a/bench/python/all_ops/op_customcrop.py +++ /dev/null @@ -1,30 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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: One must import PyCuda driver first, before CVCUDA or VPF otherwise -# things may throw unexpected errors. -import pycuda.driver as cuda # noqa: F401 - -from bench_utils import AbstractOpBase -import cvcuda - - -class OpCustomCrop(AbstractOpBase): - def setup(self, input): - super().setup(input) - self.rectI = cvcuda.RectI(x=30, y=40, width=420, height=390) - - def run(self, input): - return cvcuda.customcrop(input, self.rectI) diff --git a/bench/python/all_ops/op_cvtcolor.py b/bench/python/all_ops/op_cvtcolor.py deleted file mode 100644 index a48a5a2c2..000000000 --- a/bench/python/all_ops/op_cvtcolor.py +++ /dev/null @@ -1,37 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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: One must import PyCuda driver first, before CVCUDA or VPF otherwise -# things may throw unexpected errors. -import pycuda.driver as cuda # noqa: F401 - -from bench_utils import AbstractOpBase -import cvcuda - - -class OpCvtColorRGB2GRAY(AbstractOpBase): - def setup(self, input): - super().setup(input) - - def run(self, input): - return cvcuda.cvtcolor(input, cvcuda.ColorConversion.RGB2GRAY) - - -class OpCvtColorRGB2BGR(AbstractOpBase): - def setup(self, input): - super().setup(input) - - def run(self, input): - return cvcuda.cvtcolor(input, cvcuda.ColorConversion.RGB2BGR) diff --git a/bench/python/all_ops/op_flip.py b/bench/python/all_ops/op_flip.py deleted file mode 100644 index 9f47c1b01..000000000 --- a/bench/python/all_ops/op_flip.py +++ /dev/null @@ -1,48 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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: One must import PyCuda driver first, before CVCUDA or VPF otherwise -# things may throw unexpected errors. -import pycuda.driver as cuda # noqa: F401 - -from bench_utils import AbstractOpBase -import cvcuda - - -class OpFlipX(AbstractOpBase): - def setup(self, input): - super().setup(input) - self.flip_code = 0 # means flipping around x axis. - - def run(self, input): - return cvcuda.flip(input, flipCode=self.flip_code) - - -class OpFlipY(AbstractOpBase): - def setup(self, input): - super().setup(input) - self.flip_code = 1 # means flipping around y axis. - - def run(self, input): - return cvcuda.flip(input, flipCode=self.flip_code) - - -class OpFlipXY(AbstractOpBase): - def setup(self, input): - super().setup(input) - self.flip_code = -1 # means flipping around x and y axis. - - def run(self, input): - return cvcuda.flip(input, flipCode=self.flip_code) diff --git a/bench/python/all_ops/op_gaussianblur.py b/bench/python/all_ops/op_gaussianblur.py deleted file mode 100644 index 918caf4b0..000000000 --- a/bench/python/all_ops/op_gaussianblur.py +++ /dev/null @@ -1,31 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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: One must import PyCuda driver first, before CVCUDA or VPF otherwise -# things may throw unexpected errors. -import pycuda.driver as cuda # noqa: F401 - -from bench_utils import AbstractOpBase -import cvcuda - - -class OpGaussianBlur(AbstractOpBase): - def setup(self, input): - super().setup(input) - self.kernel_size = (3, 3) - self.sigma = (5, 5) - - def run(self, input): - return cvcuda.gaussian(input, kernel_size=self.kernel_size, sigma=self.sigma) diff --git a/bench/python/all_ops/op_hqresize.py b/bench/python/all_ops/op_hqresize.py deleted file mode 100644 index 0296eecad..000000000 --- a/bench/python/all_ops/op_hqresize.py +++ /dev/null @@ -1,55 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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: One must import PyCuda driver first, before CVCUDA or VPF otherwise -# things may throw unexpected errors. -import pycuda.driver as cuda # noqa: F401 - -from bench_utils import AbstractOpBase -import cvcuda - - -class OpHqResizeDown(AbstractOpBase): - def setup(self, input): - super().setup(input) - self.resize_width = 640 - self.resize_height = 420 - - def run(self, input): - return cvcuda.hq_resize( - input, - ( - self.resize_height, - self.resize_width, - ), - interpolation=cvcuda.Interp.NEAREST, - ) - - -class OpHqResizeUp(AbstractOpBase): - def setup(self, input): - super().setup(input) - self.resize_width = 1920 - self.resize_height = 1280 - - def run(self, input): - return cvcuda.hq_resize( - input, - ( - self.resize_height, - self.resize_width, - ), - interpolation=cvcuda.Interp.LINEAR, - ) diff --git a/bench/python/all_ops/op_inpaint.py b/bench/python/all_ops/op_inpaint.py deleted file mode 100644 index 2947d6465..000000000 --- a/bench/python/all_ops/op_inpaint.py +++ /dev/null @@ -1,57 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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: One must import PyCuda driver first, before CVCUDA or VPF otherwise -# things may throw unexpected errors. -import pycuda.driver as cuda # noqa: F401 - -from bench_utils import AbstractOpBase -import cvcuda -import torch -from torchvision.io import read_image -import os - - -class OpInpaint(AbstractOpBase): - def setup(self, input): - super().setup(input) - data = read_image(os.path.join(self.assets_dir, "brooklyn.jpg")) - mask = read_image(os.path.join(self.assets_dir, "countour_lines.jpg")) - # Binarize the mask - mask[mask <= 50] = 0 - mask[mask > 50] = 255 - - # Add scratch marks on the top of the input data and convert it to tensor - mask3 = mask.repeat(3, 1, 1) - data[mask3 > 0] = mask3[mask3 > 0] - data = data.moveaxis(0, -1).contiguous() # From CHW to HWC - data = [data.clone() for _ in range(input.shape[0])] - data = torch.stack(data) - data = data.cuda(self.device_id) - self.data = cvcuda.as_tensor(data, "NHWC") - - mask = torch.unsqueeze(mask[0], -1) # 3 channel chw to 1 channel hwc mask - mask = [mask.clone() for _ in range(input.shape[0])] - mask = torch.stack(mask) - mask = mask.cuda(self.device_id) - self.masks = cvcuda.as_tensor(mask, "NHWC") - self.inpaint_radius = 3 - - def run(self, input): - return cvcuda.inpaint( - self.data, - self.masks, - self.inpaint_radius, - ) diff --git a/bench/python/all_ops/op_jointbilateral.py b/bench/python/all_ops/op_jointbilateral.py deleted file mode 100644 index 454ff2b47..000000000 --- a/bench/python/all_ops/op_jointbilateral.py +++ /dev/null @@ -1,56 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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: One must import PyCuda driver first, before CVCUDA or VPF otherwise -# things may throw unexpected errors. -import pycuda.driver as cuda # noqa: F401 - -from bench_utils import AbstractOpBase -import cvcuda -import torch -from torchvision.io import read_image -import os - - -class OpJointBilateral(AbstractOpBase): - def setup(self, input): - super().setup(input) - self.diameter = 5 - self.sigma_color = 50 - self.sigma_space = 1 - - data = read_image(os.path.join(self.assets_dir, "brooklyn.jpg")) - data = data.moveaxis(0, -1).contiguous() # From CHW to HWC - data = [data.clone() for _ in range(input.shape[0])] - data = torch.stack(data) - data = data.cuda(self.device_id) - data = cvcuda.as_tensor(data, "NHWC") - self.grayscale_input = cvcuda.cvtcolor(data, cvcuda.ColorConversion.RGB2GRAY) - - mask = read_image(os.path.join(self.assets_dir, "brooklyn_mask.jpg")) - mask = mask.moveaxis(0, -1).contiguous() # From CHW to HWC - mask = [mask.clone() for _ in range(input.shape[0])] - mask = torch.stack(mask) - mask = mask.cuda(self.device_id) - self.class_masks = cvcuda.as_tensor(mask, "NHWC") - - def run(self, input): - return cvcuda.joint_bilateral_filter( - self.class_masks, - self.grayscale_input, - diameter=self.diameter, - sigma_color=self.sigma_color, - sigma_space=self.sigma_space, - ) diff --git a/bench/python/all_ops/op_laplacian.py b/bench/python/all_ops/op_laplacian.py deleted file mode 100644 index 440910629..000000000 --- a/bench/python/all_ops/op_laplacian.py +++ /dev/null @@ -1,31 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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: One must import PyCuda driver first, before CVCUDA or VPF otherwise -# things may throw unexpected errors. -import pycuda.driver as cuda # noqa: F401 - -from bench_utils import AbstractOpBase -import cvcuda - - -class OpLaplacian(AbstractOpBase): - def setup(self, input): - super().setup(input) - self.kernel_size = 3 - self.scale = 2.0 - - def run(self, input): - return cvcuda.laplacian(input, ksize=self.kernel_size, scale=self.scale) diff --git a/bench/python/all_ops/op_morphology.py b/bench/python/all_ops/op_morphology.py deleted file mode 100644 index 190b2e0a5..000000000 --- a/bench/python/all_ops/op_morphology.py +++ /dev/null @@ -1,106 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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: One must import PyCuda driver first, before CVCUDA or VPF otherwise -# things may throw unexpected errors. -import pycuda.driver as cuda # noqa: F401 - -from bench_utils import AbstractOpBase -import cvcuda -import torch - - -class MorphologyBase: - def __init__(self, device_id, input, morphology_type): - self.device_id = device_id - self.mask_size = [5, 5] - self.anchor = [-1, -1] - self.num_iterations = 3 - self.border_type = cvcuda.Border.CONSTANT - self.morphology_type = morphology_type - - # Morphology requires binary input, with mostly white foreground - threshold_value = torch.tensor([150.0] * input.shape[0]) - threshold_value = threshold_value.type(torch.float64) - threshold_value = threshold_value.cuda(self.device_id) - threshold_value = cvcuda.as_tensor(threshold_value, "N") - - maxval = torch.tensor([255.0] * input.shape[0]) - maxval = maxval.type(torch.float64) - maxval = maxval.cuda(self.device_id) - maxval = cvcuda.as_tensor(maxval, "N") - self.binary_input = cvcuda.threshold( - input, threshold_value, maxval, type=cvcuda.ThresholdType.BINARY - ) - - if self.num_iterations > 1: - self.workspace = cvcuda.Tensor(input.shape, input.dtype, "NHWC") - else: - self.workspace = None - - def __call__(self): - return cvcuda.morphology( - self.binary_input, - self.morphology_type, - maskSize=self.mask_size, - anchor=self.anchor, - workspace=self.workspace, - iteration=self.num_iterations, - border=self.border_type, - ) - - -class OpMorphologyOpen(AbstractOpBase): - def setup(self, input): - super().setup(input) - self.MorphologyBase = MorphologyBase( - self.device_id, input, cvcuda.MorphologyType.OPEN - ) - - def run(self, input): - return self.MorphologyBase() - - -class OpMorphologyClose(AbstractOpBase): - def setup(self, input): - super().setup(input) - self.MorphologyBase = MorphologyBase( - self.device_id, input, cvcuda.MorphologyType.CLOSE - ) - - def run(self, input): - return self.MorphologyBase() - - -class OpMorphologyDilate(AbstractOpBase): - def setup(self, input): - super().setup(input) - self.MorphologyBase = MorphologyBase( - self.device_id, input, cvcuda.MorphologyType.DILATE - ) - - def run(self, input): - return self.MorphologyBase() - - -class OpMorphologyErode(AbstractOpBase): - def setup(self, input): - super().setup(input) - self.MorphologyBase = MorphologyBase( - self.device_id, input, cvcuda.MorphologyType.ERODE - ) - - def run(self, input): - return self.MorphologyBase() diff --git a/bench/python/all_ops/op_nms.py b/bench/python/all_ops/op_nms.py deleted file mode 100644 index db2425ace..000000000 --- a/bench/python/all_ops/op_nms.py +++ /dev/null @@ -1,53 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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: One must import PyCuda driver first, before CVCUDA or VPF otherwise -# things may throw unexpected errors. -import pycuda.driver as cuda # noqa: F401 - -from bench_utils import AbstractOpBase -import cvcuda -import torch -import os - - -class OpNMS(AbstractOpBase): - def setup(self, input): - super().setup(input) - bboxes = torch.load( - os.path.join(self.assets_dir, "brooklyn_bboxes.pt"), - map_location="cuda:%d" % self.device_id, - ) - bboxes = [bboxes[0].clone() for _ in range(input.shape[0])] - bboxes = torch.stack(bboxes) - self.bboxes = cvcuda.as_tensor(bboxes) - - scores = torch.load( - os.path.join(self.assets_dir, "brooklyn_scores.pt"), - map_location="cuda:%d" % self.device_id, - ) - scores = [scores[0].clone() for _ in range(input.shape[0])] - scores = torch.stack(scores) - self.scores = cvcuda.as_tensor(scores) - self.confidence_threshold = 0.9 - self.iou_threshold = 0.2 - - def run(self, input): - return cvcuda.nms( - self.bboxes, self.scores, self.confidence_threshold, self.iou_threshold - ) - - def visualize(self): - pass diff --git a/bench/python/all_ops/op_normalize.py b/bench/python/all_ops/op_normalize.py deleted file mode 100644 index 546896997..000000000 --- a/bench/python/all_ops/op_normalize.py +++ /dev/null @@ -1,46 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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: One must import PyCuda driver first, before CVCUDA or VPF otherwise -# things may throw unexpected errors. -import pycuda.driver as cuda # noqa: F401 - -from bench_utils import AbstractOpBase -import cvcuda -import torch - - -class OpNormalize(AbstractOpBase): - def setup(self, input): - super().setup(input) - mean_tensor = ( - torch.Tensor([0.485, 0.456, 0.406]).reshape(1, 1, 1, 3).cuda(self.device_id) - ) - self.mean_tensor = cvcuda.as_tensor(mean_tensor, "NHWC") - stddev_tensor = ( - torch.Tensor([0.229, 0.224, 0.225]).reshape(1, 1, 1, 3).cuda(self.device_id) - ) - self.stddev_tensor = cvcuda.as_tensor(stddev_tensor, "NHWC") - - def run(self, input): - return cvcuda.normalize( - input, - base=self.mean_tensor, - scale=self.stddev_tensor, - flags=cvcuda.NormalizeFlags.SCALE_IS_STDDEV, - ) - - def visualize(self): - pass diff --git a/bench/python/all_ops/op_randomresizedcrop.py b/bench/python/all_ops/op_randomresizedcrop.py deleted file mode 100644 index 8ea99d734..000000000 --- a/bench/python/all_ops/op_randomresizedcrop.py +++ /dev/null @@ -1,45 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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: One must import PyCuda driver first, before CVCUDA or VPF otherwise -# things may throw unexpected errors. -import pycuda.driver as cuda # noqa: F401 - -from bench_utils import AbstractOpBase -import cvcuda - - -class OpRandomResizedCrop(AbstractOpBase): - def setup(self, input): - super().setup(input) - self.resized_shape = (input.shape[0], 320, 580, 3) - self.min_scale = 0.08 - self.max_scale = 1.0 - self.min_ratio = 0.75 - self.max_ratio = 1.33333333 - self.interpolation_type = cvcuda.Interp.LINEAR - self.seed = 4 - - def run(self, input): - return cvcuda.random_resized_crop( - input, - self.resized_shape, - self.min_scale, - self.max_scale, - self.min_ratio, - self.max_ratio, - self.interpolation_type, - self.seed, - ) diff --git a/bench/python/all_ops/op_reformat.py b/bench/python/all_ops/op_reformat.py deleted file mode 100644 index c1c9bfa20..000000000 --- a/bench/python/all_ops/op_reformat.py +++ /dev/null @@ -1,44 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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: One must import PyCuda driver first, before CVCUDA or VPF otherwise -# things may throw unexpected errors. -import pycuda.driver as cuda # noqa: F401 - -from bench_utils import AbstractOpBase -import cvcuda - - -class OpReformatNCHWToNHWC(AbstractOpBase): - def setup(self, input): - super().setup(input) - self.input_nchw = cvcuda.reformat(input, "NCHW") - - def run(self, input): - return cvcuda.reformat(self.input_nchw, "NHWC") - - def visualize(self): - pass - - -class OpReformatNHWCToNCHW(AbstractOpBase): - def setup(self, input): - super().setup(input) - - def run(self, input): - return cvcuda.reformat(input, "NCHW") - - def visualize(self): - pass diff --git a/bench/python/all_ops/op_remap.py b/bench/python/all_ops/op_remap.py deleted file mode 100644 index 345e53125..000000000 --- a/bench/python/all_ops/op_remap.py +++ /dev/null @@ -1,54 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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: One must import PyCuda driver first, before CVCUDA or VPF otherwise -# things may throw unexpected errors. -import pycuda.driver as cuda # noqa: F401 - -from bench_utils import AbstractOpBase -import cvcuda -import numpy as np -import torch - - -class OpRemap(AbstractOpBase): - def setup(self, input): - super().setup(input) - batch_size, width, height = input.shape[0], input.shape[2], input.shape[1] - batch_map = np.stack([self.flipH(w=width, h=height) for _ in range(batch_size)]) - batch_map = torch.as_tensor(batch_map, device="cuda") - self.batch_map = cvcuda.as_tensor(batch_map, "NHWC") - self.src_interp = cvcuda.Interp.LINEAR - self.map_interp = cvcuda.Interp.LINEAR - self.map_type = cvcuda.Remap.ABSOLUTE - self.align_corners = True - self.border_type = cvcuda.Border.CONSTANT - self.border_value = np.array([], dtype=np.float32) - - def flipH(self, w, h): - mesh = np.meshgrid(np.arange(w)[::-1], np.arange(h)) - return np.stack(mesh, axis=2).astype(np.float32) - - def run(self, input): - return cvcuda.remap( - input, - self.batch_map, - self.src_interp, - self.map_interp, - self.map_type, - align_corners=self.align_corners, - border=self.border_type, - border_value=self.border_value, - ) diff --git a/bench/python/all_ops/op_reshape.py b/bench/python/all_ops/op_reshape.py deleted file mode 100644 index d4d01e4c1..000000000 --- a/bench/python/all_ops/op_reshape.py +++ /dev/null @@ -1,33 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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: One must import PyCuda driver first, before CVCUDA or VPF otherwise -# things may throw unexpected errors. -import pycuda.driver as cuda # noqa: F401 - -from bench_utils import AbstractOpBase -import cvcuda - - -class OpReshape(AbstractOpBase): - def setup(self, input): - super().setup(input) - self.shape = input.shape[::-1] # Reverse everything out - - def run(self, input): - return cvcuda.reshape(input, shape=self.shape) - - def visualize(self): - pass diff --git a/bench/python/all_ops/op_resize.py b/bench/python/all_ops/op_resize.py deleted file mode 100644 index 08046da53..000000000 --- a/bench/python/all_ops/op_resize.py +++ /dev/null @@ -1,59 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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: One must import PyCuda driver first, before CVCUDA or VPF otherwise -# things may throw unexpected errors. -import pycuda.driver as cuda # noqa: F401 - -from bench_utils import AbstractOpBase -import cvcuda - - -class OpResizeDown(AbstractOpBase): - def setup(self, input): - super().setup(input) - self.resize_width = 640 - self.resize_height = 420 - - def run(self, input): - return cvcuda.resize( - input, - ( - input.shape[0], - self.resize_height, - self.resize_width, - input.shape[3], - ), - cvcuda.Interp.AREA, - ) - - -class OpResizeUp(AbstractOpBase): - def setup(self, input): - super().setup(input) - self.resize_width = 1920 - self.resize_height = 1280 - - def run(self, input): - return cvcuda.resize( - input, - ( - input.shape[0], - self.resize_height, - self.resize_width, - input.shape[3], - ), - cvcuda.Interp.LINEAR, - ) diff --git a/bench/python/all_ops/op_resize_crop_convert_reformat.py b/bench/python/all_ops/op_resize_crop_convert_reformat.py deleted file mode 100644 index 02ada24a7..000000000 --- a/bench/python/all_ops/op_resize_crop_convert_reformat.py +++ /dev/null @@ -1,44 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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: One must import PyCuda driver first, before CVCUDA or VPF otherwise -# things may throw unexpected errors. -import pycuda.driver as cuda # noqa: F401 - -from bench_utils import AbstractOpBase -import cvcuda - - -class OpResizeCropConvertReformat(AbstractOpBase): - def setup(self, input): - super().setup(input) - resize = 256 - crop = 224 - delta_shape = resize - crop - start = delta_shape // 2 - self.resize_dim = (resize, resize) - self.resize_interpolation = cvcuda.Interp.LINEAR - self.crop_rect = cvcuda.RectI(start, start, crop, crop) - - def run(self, input): - return cvcuda.resize_crop_convert_reformat( - input, - self.resize_dim, - self.resize_interpolation, - self.crop_rect, - layout="NHWC", - data_type=cvcuda.Type.U8, - manip=cvcuda.ChannelManip.REVERSE, - ) diff --git a/bench/python/all_ops/op_rotate.py b/bench/python/all_ops/op_rotate.py deleted file mode 100644 index f542c0da9..000000000 --- a/bench/python/all_ops/op_rotate.py +++ /dev/null @@ -1,32 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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: One must import PyCuda driver first, before CVCUDA or VPF otherwise -# things may throw unexpected errors. -import pycuda.driver as cuda # noqa: F401 - -from bench_utils import AbstractOpBase -import cvcuda - - -class OpRotate(AbstractOpBase): - def setup(self, input): - super().setup(input) - self.angle_deg = 40 - self.shift = [input.shape[2] // 4, input.shape[1] // 4] - self.interpolation_type = cvcuda.Interp.LINEAR - - def run(self, input): - return cvcuda.rotate(input, self.angle_deg, self.shift, self.interpolation_type) diff --git a/bench/python/all_ops/op_sift.py b/bench/python/all_ops/op_sift.py deleted file mode 100644 index 98e61ecae..000000000 --- a/bench/python/all_ops/op_sift.py +++ /dev/null @@ -1,48 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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: One must import PyCuda driver first, before CVCUDA or VPF otherwise -# things may throw unexpected errors. -import pycuda.driver as cuda # noqa: F401 - -from bench_utils import AbstractOpBase -import cvcuda - - -class OpSIFT(AbstractOpBase): - def setup(self, input): - super().setup(input) - self.max_features = 100 - self.num_octave_layers = 3 - self.contrast_threshold = 0.04 - self.edge_threshold = 10.0 - self.init_sigma = 1.6 - self.grayscale_input = cvcuda.cvtcolor( - self.input, cvcuda.ColorConversion.RGB2GRAY - ) - - def run(self, input): - return cvcuda.sift( - self.grayscale_input, - self.max_features, - self.num_octave_layers, - self.contrast_threshold, - self.edge_threshold, - self.init_sigma, - flags=cvcuda.SIFT.USE_EXPANDED_INPUT, - ) - - def visualize(self): - pass diff --git a/bench/python/all_ops/op_threshold.py b/bench/python/all_ops/op_threshold.py deleted file mode 100644 index 1bc6979c0..000000000 --- a/bench/python/all_ops/op_threshold.py +++ /dev/null @@ -1,43 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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: One must import PyCuda driver first, before CVCUDA or VPF otherwise -# things may throw unexpected errors. -import pycuda.driver as cuda # noqa: F401 - -from bench_utils import AbstractOpBase -import cvcuda -import torch - - -class OpThreshold(AbstractOpBase): - def setup(self, input): - super().setup(input) - threshold = torch.tensor([150.0] * input.shape[0]) - threshold = threshold.type(torch.float64) - threshold = threshold.cuda(self.device_id) - self.threshold = cvcuda.as_tensor(threshold, "N") - - maxval = torch.tensor([255.0] * input.shape[0]) - maxval = maxval.type(torch.float64) - maxval = maxval.cuda(self.device_id) - self.maxval = cvcuda.as_tensor(maxval, "N") - - self.threshold_type = cvcuda.ThresholdType.BINARY - - def run(self, input): - return cvcuda.threshold( - input, thresh=self.threshold, maxval=self.maxval, type=self.threshold_type - ) diff --git a/bench/python/all_ops/op_warpaffine.py b/bench/python/all_ops/op_warpaffine.py deleted file mode 100644 index a983c4aa3..000000000 --- a/bench/python/all_ops/op_warpaffine.py +++ /dev/null @@ -1,62 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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: One must import PyCuda driver first, before CVCUDA or VPF otherwise -# things may throw unexpected errors. -import pycuda.driver as cuda # noqa: F401 - -from bench_utils import AbstractOpBase -import cvcuda -import numpy as np - - -class OpWarpAffine(AbstractOpBase): - def setup(self, input): - super().setup(input) - self.xform = np.array( - [[1.26666667, 0.6, -83.33333333], [-0.33333333, 1.0, 66.66666667]] - ) - self.flags = cvcuda.Interp.LINEAR - self.border_mode = cvcuda.Border.CONSTANT - self.border_value = [] - - def run(self, input): - return cvcuda.warp_affine( - input, - self.xform, - flags=self.flags, - border_mode=self.border_mode, - border_value=self.border_value, - ) - - -class OpWarpAffineInverse(AbstractOpBase): - def setup(self, input): - super().setup(input) - self.xform = np.array( - [[1.26666667, 0.6, -83.33333333], [-0.33333333, 1.0, 66.66666667]] - ) - self.flags = cvcuda.Interp.LINEAR - self.border_mode = cvcuda.Border.CONSTANT - self.border_value = [] - - def run(self, input): - return cvcuda.warp_affine( - input, - self.xform, - flags=self.flags, - border_mode=self.border_mode, - border_value=self.border_value, - ) diff --git a/bench/python/all_ops/op_warpperspective.py b/bench/python/all_ops/op_warpperspective.py deleted file mode 100644 index 528b82df6..000000000 --- a/bench/python/all_ops/op_warpperspective.py +++ /dev/null @@ -1,72 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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: One must import PyCuda driver first, before CVCUDA or VPF otherwise -# things may throw unexpected errors. -import pycuda.driver as cuda # noqa: F401 - -from bench_utils import AbstractOpBase -import cvcuda -import numpy as np - - -class OpWarpPerspective(AbstractOpBase): - def setup(self, input): - super().setup(input) - self.xform = np.array( - [ - [3.46153846e-01, 3.33031674e-01, 1.28000000e02], - [0.00000000e00, 6.92307692e-01, 0.00000000e00], - [-4.50721154e-04, 5.65610860e-04, 1.00000000e00], - ], - np.float32, - ) - self.flags = cvcuda.Interp.LINEAR - self.border_mode = cvcuda.Border.CONSTANT - self.border_value = [] - - def run(self, input): - return cvcuda.warp_perspective( - input, - self.xform, - flags=self.flags, - border_mode=self.border_mode, - border_value=self.border_value, - ) - - -class OpWarpPerspectiveInverse(AbstractOpBase): - def setup(self, input): - super().setup(input) - self.xform = np.array( - [ - [3.46153846e-01, 3.33031674e-01, 1.28000000e02], - [0.00000000e00, 6.92307692e-01, 0.00000000e00], - [-4.50721154e-04, 5.65610860e-04, 1.00000000e00], - ], - np.float32, - ) - self.flags = cvcuda.Interp.LINEAR | cvcuda.Interp.WARP_INVERSE_MAP - self.border_mode = cvcuda.Border.CONSTANT - self.border_value = [] - - def run(self, input): - return cvcuda.warp_perspective( - input, - self.xform, - flags=self.flags, - border_mode=self.border_mode, - border_value=self.border_value, - ) diff --git a/bench/python/assets/NOTICE.md b/bench/python/assets/NOTICE.md deleted file mode 100644 index acde2a72e..000000000 --- a/bench/python/assets/NOTICE.md +++ /dev/null @@ -1,19 +0,0 @@ - -[//]: # "SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved." -[//]: # "SPDX-License-Identifier: Apache-2.0" -[//]: # "" -[//]: # "Licensed 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." - - -The data files obtained from the following sources : - -- brooklyn.jpg is obtained from [pexels](https://www.pexels.com/photo/people-across-on-intersection-1486222/) under Pexels license diff --git a/bench/python/assets/brooklyn.jpg b/bench/python/assets/brooklyn.jpg deleted file mode 100644 index 89e67551c..000000000 --- a/bench/python/assets/brooklyn.jpg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:23e406cb60c8f9200d4bc5d95e8b9d8a8168876390f0c6b08c837b90795d4beb -size 247098 diff --git a/bench/python/assets/brooklyn_bboxes.pt b/bench/python/assets/brooklyn_bboxes.pt deleted file mode 100644 index 69bc4260c..000000000 --- a/bench/python/assets/brooklyn_bboxes.pt +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:542f7546395189c44d7abbb02a148d2d769e684c49ba7de64bd7a173e43664f2 -size 196520 diff --git a/bench/python/assets/brooklyn_mask.jpg b/bench/python/assets/brooklyn_mask.jpg deleted file mode 100644 index 6f839400b..000000000 --- a/bench/python/assets/brooklyn_mask.jpg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:efe74dbed044a192d00cf4a8b44cf89620b5fff29265bc847ced7d60dcab047e -size 44761 diff --git a/bench/python/assets/brooklyn_nms_masks.pt b/bench/python/assets/brooklyn_nms_masks.pt deleted file mode 100644 index 0b97d7a0d..000000000 --- a/bench/python/assets/brooklyn_nms_masks.pt +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6372f67b11170d21d43d0eef212c59de2a867f877e22c34828ce83129d07efc8 -size 196620 diff --git a/bench/python/assets/brooklyn_scores.pt b/bench/python/assets/brooklyn_scores.pt deleted file mode 100644 index fdb4a3b29..000000000 --- a/bench/python/assets/brooklyn_scores.pt +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:344657df3ff3035582d34ba9cec56e8cb49e80cc0003ffbe6c98847f74be7d39 -size 98600 diff --git a/bench/python/assets/countour_lines.jpg b/bench/python/assets/countour_lines.jpg deleted file mode 100644 index 8d5c50219..000000000 --- a/bench/python/assets/countour_lines.jpg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4d59100dbefed479522bd91fbeb18b57f8a2fe58797f5f1192385bb774645753 -size 26397 diff --git a/bench/python/batch.py b/bench/python/batch.py deleted file mode 100644 index a8c6fb28a..000000000 --- a/bench/python/batch.py +++ /dev/null @@ -1,45 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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. - -from typing import Union, List -import numpy as np -import cvcuda -import torch - - -class Batch: - """ - This object helps us keep track of the data associated with a batch used - throughout the deep learning pipelines of CVCUDA. - In addition of tracking the data tensors associated with the batch, it - allows tracking the index of the batch and any filename information one - wants to attach (i.e. which files did the data come from). - """ - - def __init__( - self, - batch_idx: int, - data: Union[cvcuda.Tensor, np.ndarray, torch.Tensor], - fileinfo: Union[str, List[str]], - ): - """ - Initializes a new instance of the `Batch` class. - :param batch_idx: A zero based int specifying the index of this batch. - :param data: The data associated with this batch. Either a torch/CVCUDA tensor or a numpy array. - :param fileinfo: Either a string or list or strings specifying any filename information of this batch. - """ - self.batch_idx = batch_idx - self.data = data - self.fileinfo = fileinfo diff --git a/bench/python/bench_utils.py b/bench/python/bench_utils.py deleted file mode 100644 index 927a2a1ac..000000000 --- a/bench/python/bench_utils.py +++ /dev/null @@ -1,417 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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: One must import PyCuda driver first, before CVCUDA or VPF otherwise -# things may throw unexpected errors. -import pycuda.driver as cuda # noqa: F401 -import torch -from PIL import Image -import os -import logging -import inspect -import argparse -import importlib -from glob import glob -from pathlib import Path -from datetime import datetime -from abc import ABC, abstractmethod -import numpy as np -import json -import pandas -import cvcuda - -logger = logging.getLogger(__name__) -logging.basicConfig( - format="[%(name)s:%(lineno)d] %(asctime)s %(levelname)-6s %(message)s", - level=logging.INFO, - datefmt="%Y-%m-%d %H:%M:%S", -) - - -class AbstractOpBase(ABC): - """ - This is an abstract base class of all the operators that can be benchmarked. - It provides basic functionality and guarantees uniformity across operator test cases. - Concrete implementation of all the abstract methods of this class must be provided by - the class inheriting from this. - """ - - def __init__(self, device_id, input, output_dir=None, should_visualize=False): - """ - Initializes a new instances of this class. - :param device_id: The GPU device id that to use by this operator. - :param input: The input tensor to run the operator on. - :param output_dir: The directory where artifacts should be stored. - :param should_visualize: A flag specifying whether the output from the operator - should be visualized and written to the disk or not. - """ - self.device_id = device_id - self.input = input - self.output_dir = output_dir - self.should_visualize = should_visualize - if self.output_dir: - if not os.path.isdir(self.output_dir): - raise ValueError("A valid output_dir must be given.") - self.op_output = None - - self.assets_dir = os.path.join( - Path(os.path.abspath(__file__)).parents[0], "assets" - ) - self.setup(self.input) - - def __call__(self, input): - """ - Runs the operator on a given input. Also visualizes the output if visualization was set to True. - :param input: The input tensor to run the operator on. - :returns: True if the operator executed successfully, False otherwise. - """ - try: - self.op_output = self.run(input) - - if self.should_visualize and self.output_dir: - self.visualize() - - return True - except Exception as e: - logger.error( - "Unable to run the op %s due to error: %s" - % (self.__class__.__name__, str(e)) - ) - return False - - @abstractmethod - def setup(self, input): - """ - Performs various setup activities to set this operator before it can be run. - :param input: The input tensor to run the operator on. - """ - if hasattr(cvcuda, "clear_cache"): - cvcuda.clear_cache() - - @abstractmethod - def run(self, input): - """ - Runs the operator and returns the result. - :param input: The input tensor to run the operator on. - :returns: The result from the operator's run. - """ - pass - - def get_params_info(self, primitive_types_only=True): - """ - Returns a dictionary with keys being the variable names initialized exclusively during the setup call - # and values being their values. Useful to log if someone wants to know what parameters were used to - initialize the operator in the setup function call. - :param primitive_types_only: Only includes attributes with primitive data-types if True. Primitive - data types are bool, str, int, float, tuple and None. - """ - primitives = (bool, str, int, float, tuple, type(None)) - - # Get all global names (e.g variables + function names) used by the setup function. - all_global_names_setup_func = set(self.setup.__code__.co_names) - - # Get all the global names (e.g variables + function names) used by the __init__ function. - all_global_names_init_func = set(self.__init__.__code__.co_names) - - # Remove the names already used by __init__ from the ones used by setup to get a list of names - # which are exclusively used by setup - all_global_names_setup_func -= all_global_names_init_func - - # Get all the variables of this class. - all_vars_info = vars(self) - all_vars_names = set(all_vars_info.keys()) - - # Figure out all global variables only by intersecting the all_vars_names with - # all_global_names_setup_func. - # That will eliminate the global function names from all_global_names_setup_func. - vars_names_of_setup_function = all_vars_names.intersection( - all_global_names_setup_func - ) - - if primitive_types_only: - vars_info_of_setup_function = { - v: all_vars_info[v] - for v in vars_names_of_setup_function - if isinstance(all_vars_info[v], primitives) - } - else: - vars_info_of_setup_function = { - v: all_vars_info[v] for v in vars_names_of_setup_function - } - - return vars_info_of_setup_function - - def _setup_clear_output_dir(self, filename_ends_with): - output_dir = os.path.join(self.output_dir, self.__class__.__name__) - - # Clear out the output directory or create it - if not os.path.isdir(output_dir): - os.makedirs(output_dir) - else: - for file in os.listdir(output_dir): - if os.path.isfile(file) and file.endswith(filename_ends_with): - os.remove(file) - - return output_dir - - def visualize(self): - """ - Attempts to visualize the output produced by the operator as an image by writing it - down to the disk. May raise exceptions if visualization is not successful. - """ - output_dir = self._setup_clear_output_dir(filename_ends_with="_op_out.jpg") - if self.op_output is None: - raise TypeError( - "Visualization Error: Operator did not return any value as output to visualize." - ) - - op_output_npy = ( - torch.as_tensor(self.op_output.cuda(), device="cuda:%d" % self.device_id) - .cpu() - .numpy() - ) - if op_output_npy.dtype == np.uint8: - for i, npy_img in enumerate(op_output_npy): - if npy_img.shape[-1] == 1: - # Need to drop the 1 from the channels dimension if dealing with - # grayscale in PIL - npy_img = npy_img[..., 0] - out_file_name = "img_%d_op_out.jpg" % i - # Visualize as image - pil_img = Image.fromarray(npy_img) - pil_img.save(os.path.join(output_dir, out_file_name)) - - else: - raise TypeError( - "Visualization Error: Unsupported dtype for visualization: %s" - % str(op_output_npy.dtype) - ) - - -def get_benchmark_eligible_ops_info(): - """ - Prepares list of tuples : op-class-name (str) and class for all the operators that can be benchmarked. - """ - class_members = [] - - for file in glob( - os.path.join(os.path.dirname(os.path.abspath(__file__)), "all_ops", "*.py") - ): - name = os.path.splitext(os.path.basename(file))[0] - module = importlib.import_module("all_ops." + name) - all_members = inspect.getmembers(module, inspect.isclass) - op_members = [x for x in all_members if x[0].startswith("Op")] - - class_members.extend(op_members) - - return class_members - - -def summarize_runs( - baseline_run_json_path, - baseline_run_name="baseline", - compare_run_json_paths=[], - compare_run_names=[], -): - """ - Summarizes one or more benchmark runs and prepares a pandas table showing the per operator run-time - and speed-up numbers. - :param baseline_run_json_path: Path to where the benchmark.py styled JSON of the first run is stored. - :param baseline_run_name: The display name of the column representing the first run in the table. - :param compare_run_json_paths: Optional. A list of path to where the benchmark.py styled JSON of - the other runs are stored. These runs are compared with the baseline run. - :param compare_run_names: A list of display names of the column representing the comparison runs - in the table. This must be of the same length as the `compare_run_json_paths`. - :returns: A pandas table with the operator name, its run time from the baseline run and the params. - used to launch those runs. If compare runs are given, it also returns their run times and the speed-up - compared to the baseline run. The speedup is simply the run time of an operator from the compare run - divided by its run time from the baseline run. If an operator's run time or speedup factor is not - available, it simply puts "N/A". - """ - if os.path.isfile(baseline_run_json_path): - with open(baseline_run_json_path, "r") as f: - baseline_perf = json.loads(f.read()) - else: - raise ValueError( - "baseline_run_json_path does not exist: %s" % baseline_run_json_path - ) - - if len(compare_run_json_paths) != len(compare_run_names): - raise ValueError( - "Length mismatch between the number of given JSON paths for comparison and" - "their run names. %d v/s %d. Each JSON must have its corresponding run name." - % (len(compare_run_json_paths), len(compare_run_names)) - ) - - # Read all the comparison related JSON files, one by one, if any. - compare_perfs = {} - for compare_json_path, compare_run_name in zip( - compare_run_json_paths, compare_run_names - ): - if os.path.isfile(compare_json_path): - with open(compare_json_path, "r") as f: - compare_perfs[compare_run_name] = json.loads(f.read()) - else: - raise ValueError("compare_json_path does not exist: %s" % compare_json_path) - - results = [] - - for op in baseline_perf["data_mean_all_procs"]["run_bench"]: - if op.startswith("Op"): - op_name = op[2:] - - row_dict = {} - - # Fetch the time and parameters from the JSON for baseline run. - baseline_run_time = baseline_perf["data_mean_all_procs"]["run_bench"][op][ - "run_op" - ]["cpu_time_minus_warmup_per_item"]["mean"] - - op_params = list( - baseline_perf["data_mean_all_procs"]["run_bench"][op][ - "op_params" - ].keys() - )[0] - - row_dict["operator name"] = op_name - row_dict["%s time (ms)" % baseline_run_name] = baseline_run_time - - if compare_perfs: - # Fetch the time from the JSON for all comparison runs. - for compare_run_name in compare_perfs: - # Check if the OP was present. - if ( - op - in compare_perfs[compare_run_name]["data_mean_all_procs"][ - "run_bench" - ] - ): - compare_run_time = compare_perfs[compare_run_name][ - "data_mean_all_procs" - ]["run_bench"][op]["run_op"]["cpu_time_minus_warmup_per_item"][ - "mean" - ] - else: - compare_run_time = None - - row_dict["%s time (ms)" % compare_run_name] = ( - compare_run_time if compare_run_time else "N/A" - ) - - if baseline_run_time and compare_run_time: - speedup = round(compare_run_time / baseline_run_time, 3) - else: - speedup = "N/A" - row_dict[ - "%s v/s %s speed-up" % (compare_run_name, baseline_run_name) - ] = speedup - - row_dict["run time params"] = op_params - - results.append(row_dict) - - pandas.set_option("display.max_colwidth", 100) - - df = pandas.DataFrame.from_dict(results) - - return df - - -def main(): - """ - The main function. This will run the comparison function to compare two benchmarking runs. - """ - parser = argparse.ArgumentParser("Summarize and compare benchmarking runs.") - - parser.add_argument( - "-o", - "--output-dir", - type=str, - required=True, - help="The output directory where you want to store the result summary as a CSV file.", - ) - - parser.add_argument( - "-b", - "--baseline-json", - type=str, - required=True, - help="Path where the benchmark.py styled JSON of the baseline run is stored.", - ) - parser.add_argument( - "-bn", - "--baseline-name", - type=str, - required=True, - help="The name of the column representing the baseline run in the output table.", - ) - parser.add_argument( - "-c", - "--compare-jsons", - action="append", - required=False, - help="Optional. List of paths where the benchmark.py styled JSON of the comparison run are stored.", - ) - parser.add_argument( - "-cn", - "--compare-names", - action="append", - required=False, - help="Optional. List of names of the column representing the comparison runs in the output table.", - ) - - args = parser.parse_args() - - if not os.path.isdir(args.output_dir): - raise ValueError("output-dir does not exist: %s" % args.output_dir) - - if not os.path.isfile(args.baseline_json): - raise ValueError("baseline-json does not exist: %s" % args.baseline_json) - - args.compare_jsons = args.compare_jsons if args.compare_jsons else [] - args.compare_names = args.compare_names if args.compare_names else [] - - if len(args.compare_jsons) != len(args.compare_names): - raise ValueError( - "Length mismatch between the number of given JSON paths for comparison and" - "their run names. %d v/s %d. Each JSON must have its corresponding run name." - % (len(args.compare_jsons), len(args.compare_names)) - ) - - logger.info( - "Summarizing a total of %d runs. All times are in milliseconds" - % (len(args.compare_jsons) + 1) - ) - - df = summarize_runs( - baseline_run_json_path=args.baseline_json, - baseline_run_name=args.baseline_name, - compare_run_json_paths=args.compare_jsons, - compare_run_names=args.compare_names, - ) - - csv_path = os.path.join( - args.output_dir, - "summarize_runs.%s.csv" % datetime.now(), - ) - df.to_csv(csv_path) - - logger.info("Wrote comparison CSV to: %s" % csv_path) - - -if __name__ == "__main__": - # If this was called on its own, we will run the summarize_runs function to summarize - # and compare two runs. - main() diff --git a/bench/python/benchmark.py b/bench/python/benchmark.py deleted file mode 100644 index 195129076..000000000 --- a/bench/python/benchmark.py +++ /dev/null @@ -1,1576 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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: One must import PyCuda driver first, before CVCUDA or VPF otherwise -# things may throw unexpected errors. -import pycuda.driver as cuda # noqa: F401 - -import os -import sys -import json -import time -import logging -import argparse -import subprocess -import numpy as np -import pandas as pd -import multiprocessing as mp -import matplotlib.pyplot as plt - -from perf_utils import maximize_clocks, reset_clocks # noqa: E402 - - -class NvtxRangeTimeInfo: - """ - A data class to hold the time information of an NVTX range. - """ - - def __init__(self, start_ms, end_ms): - """ - :param start_ms: The start time in milliseconds for this NVTX range. - :param end_ms: The end time in milliseconds for this NVTX range. - """ - self.start_ms = start_ms - self.end_ms = end_ms - - @property - def duration_ms(self): - """ - Returns the total execution time of this NVTX range. - """ - return self.end_ms - self.start_ms - - -class NvtxRange: - """ - A data class representing an NVTX range with its CPU and GPU time information. - """ - - def __init__( - self, flat_name, parent_range_id, cpu_time_info=None, gpu_time_info=None - ): - """ - :param flat_name: The flat name of the NVTX range - as represented in NSYS reports. - :param parent_range_id: The integer range ID of the parent range of this NVTX range. - :param cpu_time_info: An `NvtxRangeTimeInfo` holding the CPU timing information of this NVTX range. - :param gpu_time_info: An `NvtxRangeTimeInfo` holding the GPU timing information of this NVTX range. - """ - self.flat_name = flat_name - self.parent_range_id = parent_range_id - self.cpu_time_info = cpu_time_info - self.gpu_time_info = gpu_time_info - - -def parse_nvtx_pushpop_trace_json(json_path): - """ - Parses the nvtx_pushpop_trace JSON generated by NSYS and returns a dictionary - keyed by process_id, thread_id and range_id. The values are various fields - important to the benchmarking process. - :param json_path: Full path to the nvtx_pushpop_trace.json file. - """ - # - # The nvtx_pushpop_trace JSON has the following structure. It is a list of - # dictionaries. - # e.g. - # [ { - # "Start (ns)": 2372801266, - # "End (ns)" : 13528369268, - # ... - # }, - # ... - # ] - # - # We will store the parsed data in the range_info dictionary. The overall - # structure of the dictionary is: - # range_info = { - # process_id : { - # thread_id : { - # range_id : NvtxRange(flat_name, parent_range_id, duration_ms) - # } - # } - # } - # - # - range_info = {} - - # Check if the file was empty or not. Empty file means no ops were recorded. - if os.stat(json_path).st_size == 0: - return range_info - - # Read the JSON. - with open(json_path, "r") as f: - json_data = json.loads(f.read()) - - for row in json_data: - # Grab the necessary values from the JSON file. - flat_name = row["Name"] - start_ns = float(row["Start (ns)"]) - end_ns = float(row["End (ns)"]) - range_id = row["RangeId"] - parent_range_id = row["ParentId"] - process_id = row["PID"] - thread_id = row["TID"] - - # Process a bit. Conversion from nano to milliseconds. - start_ms = round(start_ns / 10**6, 4) - end_ms = round(end_ns / 10**6, 4) - parent_range_id = None if parent_range_id == "None" else parent_range_id - - # Save it in our dictionary at the process id and thread id level. - if process_id not in range_info: - range_info[process_id] = {} - if thread_id not in range_info[process_id]: - range_info[process_id][thread_id] = {} - - # We wills save it using the Nvtx objects. - cpu_time_info = NvtxRangeTimeInfo(start_ms, end_ms) - nvtx_range = NvtxRange(flat_name, parent_range_id, cpu_time_info) - - range_info[process_id][thread_id][range_id] = nvtx_range - - return range_info - - -def parse_nvtx_gpu_proj_trace_json(json_path): - """ - Parses the nvtx_gpu_proj_trace JSON generated by NSYS and returns a dictionary - keyed by process_id, thread_id and range_id. The values are various fields - important to the benchmarking process. - :param json_path: Full path to the nvtx_gpu_proj_trace.json file. - """ - - # - # The nvtx_gpu_proj_trace JSON has the following structure. It is a list of - # dictionaries. - # e.g. - # [ { - # "Projected Start (ns)": 2372801266, - # "Projected Duration (ns)" : 13528369268, - # ... - # }, - # ... - # ] - # - # We will store the parsed data in the range_info dictionary. The overall - # structure of the dictionary is: - # range_info = { - # process_id : { - # thread_id : { - # range_id : NvtxRange(flat_name, parent_range_id, cpu_duration_ms, gpu_duration_ms) - # } - # } - # } - # - # NOTE: Even though this report returns the cpu_duration_ms and gpu_duration_ms, it will - # only do so for operations which had gpu_duration_ms > 0. For pure CPU operations, - # this report will not even return those ranges. That is the reason why we need to - # query the pushpop_trace report. - # - range_info = {} - - # Check if the file was empty or not. Empty file means no GPU ops were recorded. - if os.stat(json_path).st_size == 0: - return range_info - - # Read the JSON. - with open(json_path, "r") as f: - json_data = json.loads(f.read()) - - for row in json_data: - # Grab the necessary values from the JSON file. - range_id = row["RangeId"] - - if not range_id or range_id == "None": - continue - - flat_name = row["Name"] - cpu_start_ns = float(row["Orig Start (ns)"]) - cpu_duration_ns = float(row["Orig Duration (ns)"]) - cpu_end_ns = cpu_start_ns + cpu_duration_ns - - gpu_start_ns = float(row["Projected Start (ns)"]) - gpu_duration_ns = float(row["Projected Duration (ns)"]) - gpu_end_ns = gpu_start_ns + gpu_duration_ns - - parent_range_id = row["ParentId"] - process_id = row["PID"] - thread_id = row["TID"] - - # Process a bit. Conversion from nano to milliseconds. - cpu_start_ms = round(cpu_start_ns / 10**6, 4) - cpu_end_ms = round(cpu_end_ns / 10**6, 4) - - gpu_start_ms = round(gpu_start_ns / 10**6, 4) - gpu_end_ms = round(gpu_end_ns / 10**6, 4) - - # Save it in our dictionary at the process id and thread id level. - if process_id not in range_info: - range_info[process_id] = {} - if thread_id not in range_info[process_id]: - range_info[process_id][thread_id] = {} - - # We wills save it using the Nvtx objects. - cpu_time_info = NvtxRangeTimeInfo(cpu_start_ms, cpu_end_ms) - gpu_time_info = NvtxRangeTimeInfo(gpu_start_ms, gpu_end_ms) - nvtx_range = NvtxRange(flat_name, parent_range_id, cpu_time_info, gpu_time_info) - - range_info[process_id][thread_id][range_id] = nvtx_range - - return range_info - - -def expand_nvtx_range_names(range_info): - """ - Converts a hierarchical NVTX range tree with parent-child relationship into a flat - tree by adding the names of parent nodes in-front of all the child nodes. - Hence, a tree like the following: - root - child_a - sub_child_a - child_b - child_c - sub_child_c - - becomes: - root - root.child_a - root.child_a.sub_child_a - root.child_b - root.child_c - root.child_c.sub_child-c - - :param range_info: The range_info dictionary returned by the parsing functions. - """ - final_dict = {} - - # Loop over all the process from the range info dictionary. - for process_id in range_info: - if process_id not in final_dict: - final_dict[process_id] = {} - - # Loop over all the threads from the range info dictionary. - for thread_id in range_info[process_id]: - if thread_id not in final_dict[process_id]: - final_dict[process_id][thread_id] = {} - - # Loop over all the ranges from the range info dictionary. - for range_id in range_info[process_id][thread_id]: - - # Fetch the range information. - nvtx_range = range_info[process_id][thread_id][range_id] - - # There are two cases to consider: - # 1. This was a root node (i.e no parent) - # 2. This is not a root node (i.e has a parent) - # - my_parent_id = nvtx_range.parent_range_id - if my_parent_id and my_parent_id != "None": - # This is not a root node. Get the information of its parent. - parent_nvtx_range = range_info[process_id][thread_id][my_parent_id] - # prepend parent's name in the child's name - new_name = os.path.join( - parent_nvtx_range.flat_name, nvtx_range.flat_name - ) - - # Most important to update our existing range info dictionary - # so any nested children will end up using the new, fully - # qualified name of this range. - nvtx_range.flat_name = new_name - range_info[process_id][thread_id][range_id] = nvtx_range - - # And add it to our dictionary. - final_dict[process_id][thread_id][new_name] = nvtx_range - - else: - # This is a root node. Nothing else needs to be done other than - # simply adding this in our final dictionary. - final_dict[process_id][thread_id][nvtx_range.flat_name] = nvtx_range - - return final_dict - - -def merge_cpu_and_gpu_ranges(cpu_range_info, gpu_range_info): - """ - Merges the CPU and GPU NVTX range information dictionaries into one such that it - contains all the information. The keys will be flat expressions of NVTX range names - and values will be a tuple of the CPU and GPU timings. - - NOTE: This is the function where the `NvtxRange` and `NvtxRangeTimeInfo` instances are - used and converted into a couple of floating point duration values. In other - words, even though the values of the `cpu_range_info` and `gpu_range_info` - were `NvtxRange` objects, the mean can not be those (because mean of start - and end times does not make sense.) Hence asking this function to calculate the - mean means that only duration will be used and returned from those objects. - - :param cpu_range_info: The CPU range_info dictionary returned by the parsing functions. - :param gpu_range_info: The GPU range_info dictionary returned by the parsing functions. - """ - - # Loop over all the keys in the cpu_range_info because it will have all the keys. - # The gpu_range_info, may not have keys which were purely CPU code. - all_ranges_info = {} - for process_id in cpu_range_info: - if process_id not in all_ranges_info: - all_ranges_info[process_id] = {} - - for thread_id in cpu_range_info[process_id]: - if thread_id not in all_ranges_info[process_id]: - all_ranges_info[process_id][thread_id] = {} - - for range_name in cpu_range_info[process_id][thread_id]: - nvtx_range = cpu_range_info[process_id][thread_id][range_name] - - gpu_time_info = NvtxRangeTimeInfo(0, 0) # Initially it is set to zero. - - if process_id in gpu_range_info: - if thread_id in gpu_range_info[process_id]: - if range_name in gpu_range_info[process_id][thread_id]: - gpu_time_info = gpu_range_info[process_id][thread_id][ - range_name - ].gpu_time_info - - nvtx_range.gpu_time_info = gpu_time_info - - all_ranges_info[process_id][thread_id][range_name] = nvtx_range - - return all_ranges_info - - -def calc_mean_ranges(all_range_info): - """ - Calculates the mean of all NVTX ranges present in the all_range_info. Since NVTX ranges - can be reported per process, per thread, we need to have a way to average those numbers. - The mean here is computed by taking the average of all the numbers per process per thread. - """ - mean_range_info = {} - - # Aggregate all the values in a list keyed by the range names. - for process_id in all_range_info: - for thread_id in all_range_info[process_id]: - for range_name in all_range_info[process_id][thread_id]: - if range_name not in mean_range_info: - mean_range_info[range_name] = ( - [], - [], - ) # Mean lists for CPU and GPU time info - - cpu_time = all_range_info[process_id][thread_id][ - range_name - ].cpu_time_info.duration_ms - gpu_time = all_range_info[process_id][thread_id][ - range_name - ].gpu_time_info.duration_ms - - mean_range_info[range_name][0].append(cpu_time) - mean_range_info[range_name][1].append(gpu_time) - - # Replace the list with the mean value. - for range_name in mean_range_info: - if len(mean_range_info[range_name]): - cpu_ranges_list = mean_range_info[range_name][0] - gpu_ranges_list = mean_range_info[range_name][1] - - avg_cpu_time = round(sum(cpu_ranges_list) / len(cpu_ranges_list), 4) - avg_gpu_time = round(sum(gpu_ranges_list) / len(gpu_ranges_list), 4) - - mean_range_info[range_name] = (avg_cpu_time, avg_gpu_time) - else: - mean_range_info[range_name] = (0, 0) - - return mean_range_info - - -class NumpyValuesEncoder(json.JSONEncoder): - """ - Helps encode various Numpy data-types correctly in the JSON encoder. - """ - - def default(self, obj): - if isinstance(obj, np.integer): - return int(obj) - if isinstance(obj, np.floating): - return float(obj) - if isinstance(obj, np.ndarray): - return obj.tolist() - return super(NumpyValuesEncoder, self).default(obj) - - -def recurse_gather_dict(input_dict, target_dict): - """ - Recursively gathers values of all keys of input_dict in another dictionary. - This is useful for computing various stats on the data such as mean, median or - std-dev of all the keys in a dictionary. - This function collects data in a list. - :param input_dict: The dictionary that should be used as input. - :param target_dict: The single dictionary in which all the sums should be gathered. - """ - assert type(input_dict) is type(target_dict) - assert isinstance(input_dict, dict) - # Loop over all the keys in the input dictionary. - for key in input_dict: - if key in ["total_items", "total_items_warmup", "total_items_minus_warmup"]: - continue # We skip these key. - - # Check if the value is another dictionary. - elif isinstance(input_dict[key], dict): - # Create this if our target_dict did not already have it. - if key not in target_dict: - target_dict[key] = {} - - # Recurse the same function again. - recurse_gather_dict(input_dict[key], target_dict[key]) - - # Check if the value is a list or tuple. We will store inside a list of lists. - elif isinstance(input_dict[key], list) or isinstance(input_dict[key], tuple): - # Create this if our target_dict did not already have it. - if key not in target_dict: - target_dict[key] = [] - for _ in range(len(input_dict[key])): - target_dict[key].append([]) # This creates list of lists. - - for i in range(len(input_dict[key])): - target_dict[key][i].append(input_dict[key][i]) - - # For anything else, we assume it was a number. We will store inside a list. - else: - if key not in target_dict: - target_dict[key] = [] - - target_dict[key].append(input_dict[key]) - - -def recurse_calc_stats_dict( - input_dict, - compute_mean_only=False, - compute_throughput=False, - throughput_multiplier=1, -): - """ - Recursively calculates various stats on the value of all keys of input_dict. - :param input_dict: The dictionary that should be used as input. - :param compute_mean_only: A flag indicating whether only the mean should be computed or not. - Computes a lot of other stats (e.g. median, min, max...) if set to False. - :param compute_throughput: A flag indicating whether throughput should be computed or not. - Only set to True when running in parallel with all resources maximized otherwise throughput - calculation may give incorrect results. - :param throughput_multiplier: A number with which the throughput is multiplied to calculate the - total throughput. Usually set to the number of parallel processes or threads executing in parallel. - """ - # Loop over all the keys in the input dictionary. - for key in list(input_dict.keys()): - # Check if the value is another dictionary. - if isinstance(input_dict[key], dict): - recurse_calc_stats_dict( - input_dict[key], - compute_mean_only, - compute_throughput, - throughput_multiplier, - ) - - else: - assert isinstance(input_dict[key], list) - - # Compute all stats. - if compute_mean_only: - stats_dict = { - "total_items": len(input_dict[key]), - "mean": round(np.mean(input_dict[key], axis=-1), 4), - } - else: - stats_dict = { - "total_items": len(input_dict[key]), - "min": round(np.min(input_dict[key], axis=-1), 4), - "max": round(np.max(input_dict[key], axis=-1), 4), - "mean": round(np.mean(input_dict[key], axis=-1), 4), - "std": round(np.std(input_dict[key], axis=-1), 4), - "median": round(np.median(input_dict[key], axis=-1), 4), - "percentile_16": round( - np.percentile(input_dict[key], 16, axis=-1), 4 - ), - "percentile_84": round( - np.percentile(input_dict[key], 84, axis=-1), 4 - ), - "percentile_95": round( - np.percentile(input_dict[key], 95, axis=-1), 4 - ), - } - - if compute_throughput: - throughput_unit = ( - "frames_per_second" - if "_per_item" in key - else "batches_per_second" - ) - stats_dict["throughput"] = { - "multiplier": throughput_multiplier, - "unit": throughput_unit, - # NOTE: Minimum throughput corresponds to maximum latency. - "min": round( - 1000 * throughput_multiplier / stats_dict["max"], 2 - ) - if stats_dict["max"] > 0 - else 0, - # NOTE: Maximum throughput corresponds to minimum latency. - "max": round( - 1000 * throughput_multiplier / stats_dict["min"], 2 - ) - if stats_dict["min"] > 0 - else 0, - "mean": round( - 1000 * throughput_multiplier / stats_dict["mean"], 2 - ) - if stats_dict["mean"] > 0 - else 0, - "median": round( - 1000 * throughput_multiplier / stats_dict["median"], 2 - ) - if stats_dict["median"] > 0 - else 0, - "percentile_68_range": [ - round( - 1000 - * throughput_multiplier - / stats_dict["percentile_84"], - 2, - ), - round( - 1000 - * throughput_multiplier - / stats_dict["percentile_16"], - 2, - ), - ] - if stats_dict["mean"] > 0 - else [0, 0], - "percentile_95": round( - 1000 * throughput_multiplier / stats_dict["percentile_95"], - 2, - ) - if stats_dict["percentile_95"] > 0 - else 0, - } - - # Assign in-place. - input_dict[key] = stats_dict - - -def unflatten_process_benchmark_dict(benchmark_dict, warmup_batches): - """ - Un-flattens (i.e expands) the data present in benchmark_dict and also calculates - additions numbers. - """ - # This function needs to do a few different things. Here is the overall flow: - # - # 1. It has to expand the keys - # so 'run_sample/pipeline/batch_0/preprocess.cvcuda' from NSYS json - # becomes the following nested dictionary: - # run_sample : { - # pipeline : { - # batch_0 : { - # preprocess.cvcuda : {"cpu_time": 0, "gpu_time": 0} - # } - # } - # } - # - # 2. Then it has to compute total of CPU and GPU times by aggregating those - # numbers at each level. In doing so, it has to account for warm-up batches - # i.e. batches whose timings should not be counted towards the total. - # run_sample : { - # pipeline : { - # batch_0 : { - # preprocess.cvcuda : {cpu_time: 0, gpu_time: 0} - # postprocess.cvcuda : {cpu_time: 0, gpu_time: 0} - # cpu_time : 0.0 - # gpu_time : 0.0 - # } - # } - # } - # - # - # 3. It also has to compute those times per frame/item. For this to happen, it needs - # the batch size information (i.e. how many items/frames were inside a batch) and - # also the information on which keys were "inside" a batch and which were not. - # These two pieces of information is taken from the benchmark.json - # pipeline : { - # batch_0 : { - # preprocess.cvcuda : {cpu_time: 0, gpu_time: 0} - # postprocess.cvcuda : {cpu_time: 0, gpu_time: 0} - # cpu_time : 0.0 - # gpu_time : 0.0 - # cpu_time_per_item: 0.0 - # gpu_time_per_item: 0.0 - # } - # } - # } - # - # 4. Finally, it computes various stats (e.g mean, median) of the timings from all - # the batches. In other words, it computes how much range X would take on an - # average when it is averaged across all the batches. To do this, we again use - # the information present inside benchmark.json and apply basic recursion math. - # - - unfltten_data_dict = {} # This is where we will store un-flattened data for now. - - # Maintains the total time of all warm-up batches. - # this is keyed by the batch level prefix and values will be the time. - total_warmup_cpu_time = {} - total_warmup_gpu_time = {} - # Maintain a count of total number of frames processed with counting the warm-up. - total_items = {} - # Maintain a count of total number of frames processed without counting the warm-up. - total_items_minus_warmup = {} - # Maintain pointers to the batch level sub-dictionaries. The keys will still be - # the batch level prefix and values will be the dictionary. - batch_dicts = {} - - # Loop over all the paths stored as keys in the input dictionary. - for path in benchmark_dict["data"]: - # Split the path expression by / - parts = path.split("/") - # Maintain a pointer to the dictionary current being traversed, initially set - # to the empty results dictionary. - current_dict = unfltten_data_dict - # Loop over all but the last part, last part will be set as a value. - for p in parts[:-1]: - # Add the key if not already added, with a blank dict as its value. - if p not in current_dict: - current_dict[p] = {} - - # Update the dict pointer with nested expression. - current_dict = current_dict[p] - - # Once all the sub-dictionaries are created, we need to assign them the correct - # value. We need to be careful here because our expressions may have - # nested keys such as: - # batch_0: - # pre_process: - # post_process: - # In the example above, we will have 3 numbers (2 for stages and 1 for overall batch) - - cpu_time = benchmark_dict["data"][path][0] - gpu_time = benchmark_dict["data"][path][1] - - if parts[-1] not in current_dict: - current_dict[parts[-1]] = {} - - current_dict[parts[-1]]["cpu_time"] = cpu_time - current_dict[parts[-1]]["gpu_time"] = gpu_time - - # Now we will check at which exact level this path sits. - # There are 3 possibilities: - # 1. Exactly at the batch level. - # 2. Inside the batch. - # 3. Outside of/above the batch. - # - # Based on its placement, it would receive different treatments. - - # Check if this was at the batch level. - if path in benchmark_dict["batch_info"]: - # We are exactly at a batch level. - batch_idx, batch_size = benchmark_dict["batch_info"][path] - # Also find out the batch level prefix. i.e. one level above. - batch_level_prefix = os.path.dirname(path) - batch_dicts[batch_level_prefix] = current_dict - - # Add total items - current_dict[parts[-1]]["total_items"] = batch_size - - # Computer per item. - if batch_size > 0: - current_dict[parts[-1]]["cpu_time_per_item"] = round( - current_dict[parts[-1]]["cpu_time"] / batch_size, 4 - ) - - current_dict[parts[-1]]["gpu_time_per_item"] = round( - current_dict[parts[-1]]["gpu_time"] / batch_size, 4 - ) - - # Pass the total_items information to all the children of this batch - # i.e. keys which were present in inside_batch_info - # unless they already had it before (i.e. very weird case where someone - # inserted a batch in a batch with different inner batch size). - def _recurse_update_children_total_items(in_dict): - for k in list(in_dict.keys()): - if isinstance(in_dict[k], dict): - _recurse_update_children_total_items(in_dict[k]) - else: - # Add total items if not already present. - if "total_items" not in in_dict: - in_dict["total_items"] = batch_size - in_dict["cpu_time_per_item"] = round( - in_dict["cpu_time"] / batch_size, 4 - ) - in_dict["gpu_time_per_item"] = round( - in_dict["gpu_time"] / batch_size, 4 - ) - - # Apply it. - if batch_size > 0: - _recurse_update_children_total_items(current_dict[parts[-1]]) - - # Maintain global counts of various batch level stats - # for example, counting the total items seen at this - # batch level. - if batch_level_prefix not in total_items: - total_items[batch_level_prefix] = 0 - total_items_minus_warmup[batch_level_prefix] = 0 - total_warmup_cpu_time[batch_level_prefix] = 0 - total_warmup_gpu_time[batch_level_prefix] = 0 - - # Add to the totals at this batch level. - total_items[batch_level_prefix] += batch_size - - # Check if this batch was not in the warm-up period. - # Batches from the front and end are ignored that fall under the warm-up period. - if batch_size > 0: - if ( - batch_idx + 1 > warmup_batches - and (batch_idx + 1 + warmup_batches) - <= benchmark_dict["meta"]["total_batches"][batch_level_prefix] - ): - # This is a non-warmup batch. - total_items_minus_warmup[batch_level_prefix] += batch_size - - else: - # This is a warm-up batch. Add its timings so that we can - # subtract it later from the totals. - total_warmup_cpu_time[batch_level_prefix] += current_dict[ - parts[-1] - ]["cpu_time"] - total_warmup_gpu_time[batch_level_prefix] += current_dict[ - parts[-1] - ]["gpu_time"] - - elif path in benchmark_dict["inside_batch_info"]: - # We could be inside a batch. Nothing to do here. We won't have the total_items yet - # at this level. We will update it when we come on a parent level. - pass - else: - # We are one or more levels outside/above the batch level. - # We will need to correctly pass the total items stats here. - current_dict[parts[-1]]["cpu_time_per_item"] = 0 - current_dict[parts[-1]]["gpu_time_per_item"] = 0 - - # For cases where we are one level above the batch level, we can directly use the - # stats stored in our dictionaries. Path's value here will be equal to batch_level_prefix - - if path in total_items: - # We are exactly one level outside/above the batch level. - if total_items[path] > 0: - current_dict[parts[-1]]["cpu_time_per_item"] = round( - current_dict[parts[-1]]["cpu_time"] / total_items[path], - 4, - ) - current_dict[parts[-1]]["gpu_time_per_item"] = round( - current_dict[parts[-1]]["gpu_time"] / total_items[path], - 4, - ) - current_dict[parts[-1]]["total_items"] = total_items[path] - - else: - # We are more than one level outside/above the batch level. - # We will need to report stats summing up all the nested batch levels. - total_items_above_level = 0 - - for k in total_items: - if k.startswith(path): - total_items_above_level += total_items[k] - - if total_items_above_level > 0: - current_dict[parts[-1]]["cpu_time_per_item"] = round( - current_dict[parts[-1]]["cpu_time"] / total_items_above_level, - 4, - ) - current_dict[parts[-1]]["gpu_time_per_item"] = round( - current_dict[parts[-1]]["gpu_time"] / total_items_above_level, - 4, - ) - current_dict[parts[-1]]["total_items"] = total_items_above_level - - # Add warm-up related keys exactly at the batch level dictionaries. - # Warm-up time is the time taken by the warm-up number of batches - # at the beginning and at the end of the pipeline. - # So for any warm-up batches > 0, we add up their run times and - # subtract those from the total run time at the end. - for batch_level_prefix in batch_dicts: - batch_dict = batch_dicts[batch_level_prefix] - - batch_dict["total_items_warmup"] = ( - batch_dict["total_items"] - total_items_minus_warmup[batch_level_prefix] - ) - - batch_dict["total_items_minus_warmup"] = total_items_minus_warmup[ - batch_level_prefix - ] - - batch_dict["cpu_time_minus_warmup"] = round( - (batch_dict["cpu_time"] - total_warmup_cpu_time[batch_level_prefix]), 4 - ) - batch_dict["gpu_time_minus_warmup"] = round( - (batch_dict["gpu_time"] - total_warmup_gpu_time[batch_level_prefix]), 4 - ) - - batch_dict["cpu_time_minus_warmup_per_item"] = 0 - batch_dict["gpu_time_minus_warmup_per_item"] = 0 - - if total_items_minus_warmup[batch_level_prefix] > 0: - batch_dict["cpu_time_minus_warmup_per_item"] = round( - batch_dict["cpu_time_minus_warmup"] - / total_items_minus_warmup[batch_level_prefix], - 4, - ) - batch_dict["gpu_time_minus_warmup_per_item"] = round( - batch_dict["gpu_time_minus_warmup"] - / total_items_minus_warmup[batch_level_prefix], - 4, - ) - - # The processing is over. So we assign the expanded version of data into the - # original benchmark dictionary. - benchmark_dict["data"] = unfltten_data_dict - - # Finally, process the batches to calculate various stats on the batch timings. - # i.e. how much did range X took on an average across all the batches. - # Again, we will not use any batches that are warm-up batches in this calculation. - # For this to happen, we need to rely on the batch_info keys. Those are the - # markers telling us what constitutes as "inside a batch". Then we build a union - # of all the keys inside the batch, sum it up and find out the average. - # - # NOTE: Although not required, it would be good idea that to name the ranges inside - # various batches the same name. - # i.e. decode of batch_0 and batch_1 both be called 'decode'. - # It won't be an issue if that is not the case. Just that the average value - # will be averaged on non-uniform number of samples. - # e.g. if there is a range that is only used during last 3 batches, its mean - # value will be a mean over 3 samples compared to a range which is used - # during all the batches. Our division logic takes care of properly - # dividing with the current count anyway. - # - data_stats = {} - for batch_range_name in benchmark_dict["batch_info"]: - batch_idx, batch_size = benchmark_dict["batch_info"][batch_range_name] - # Next, we find out the batch level prefix. This is the key in which - # the batches are nested. One profiling session can have multiple levels - # at which batches may be used. - # e.g. - # program_X: - # method_A: - # batch_1 - # batch_2 - # method_B: - # batch_1 - # batch_2 - # - # We need to find mean at these two levels (i.e. method_A and method_B) - # in this case. - # programA/method_A and program_A/method_B are the batch level prefix here. - # We can easily get those by using the dirname method since those are like - # the directory names in a path. - batch_level_prefix = os.path.dirname(batch_range_name) - - if ( - batch_size > 0 - and batch_idx + 1 > warmup_batches - and (batch_idx + 1 + warmup_batches) - <= benchmark_dict["meta"]["total_batches"][batch_level_prefix] - ): - # Keep on updating the data_stats dictionary. This will - # create a dictionary that is union of all the dictionaries of the batch level. - nested_keys = batch_range_name.split("/") - source_dict = benchmark_dict["data"] - for k in nested_keys: - source_dict = source_dict[k] - - # Need to recursively update the data_stats based on - # the source_dict. We will sum the values up. - if batch_level_prefix not in data_stats: - data_stats[batch_level_prefix] = {} - - recurse_gather_dict(source_dict, target_dict=data_stats[batch_level_prefix]) - - # Once all the numbers are gathered, we need to divide by the length to figure - # out the mean values. - recurse_calc_stats_dict(data_stats) - benchmark_dict["data_stats_minus_warmup"] = data_stats - - # Remove the batch_info and inside_batch_info keys as they are no longer needed. - del benchmark_dict["inside_batch_info"] - - -def benchmark_script( - process_idx, - device_id, - output_dir, - warmup_batches, - script, - args, -): - """ - Main function responsible for running an arbitrary python script and benchmarking it. - :param process_idx: The 0-based index of this process. - :param device_id: The GPU device id to use. - :param output_dir: The output directory to use to store artifacts. - :param warmup_batches: The numbers of batches that should be ignored from benchmarking. - :param script: The python script to execute. - :param args: Any optional command line arguments that should be passed to the script. - """ - - # Make a copy of the environment variables and add our own env-vars to it. - my_env = os.environ.copy() - # Change the CUDA visible devices for this process. - my_env["CUDA_VISIBLE_DEVICES"] = str(device_id) - # Add the benchmark flag so that perf_utils knows that this is a benchmark run. - my_env["BENCHMARK_PY"] = "1" - - # Set a path to store the SQLITE report created by NSYS. - out_sqlite_path = os.path.join(output_dir, "perf_report") - # Set a path to the benchmark.json created by the script. - benchmark_json_path = os.path.join(output_dir, "benchmark.json") - - # Remove any existing benchmark.json files. - if os.path.isfile(benchmark_json_path): - os.remove(benchmark_json_path) - - # Setup the command that will launch nsys and ask it to benchmark the script - # that we were interested in. - nsys_root_path = "/opt/nvidia/nsight-systems/2025.5.1/" - nsys_binary_path = os.path.join(nsys_root_path, "bin/nsys") - nsys_reports_path = os.path.join(nsys_root_path, "target-linux-x64/reports") - nsys_gpu_proj_trace_report_path = os.path.join( - nsys_reports_path, "nvtx_gpu_proj_trace" - ) - nsys_pushpop_trace_report_path = os.path.join( - nsys_reports_path, "nvtx_pushpop_trace" - ) - - if not os.path.isfile(nsys_binary_path): - raise ValueError( - "Unable to locate nsys binary at %s. Make sure you have nsight-systems installed." - % nsys_binary_path - ) - - cmd = [ - nsys_binary_path, - "profile", - "--export", - "sqlite", - "-o", - out_sqlite_path, - "--force-overwrite", - "true", - "--trace", - "cuda,nvtx", - "--trace-fork-before-exec=true", - "--gpu-video-device", - "all", - sys.executable, - script, - *args, - ] - # Start the sub-process and wait for its completion. - subproc = subprocess.Popen(cmd, stdout=None, stderr=None, env=my_env) - subproc.wait() - - # Check if the subprocess was completed successfully. Proceed further only if yes. - if subproc.returncode: - return subproc.returncode, output_dir - - # Also if the script actually ran in the benchmark mode using our own perf_utils, it - # must have generated a benchmark.json file for this process. That file may not be - # it its final form (i.e. may still contain zero as perf values) but at-least it must be present. - if not os.path.isfile(benchmark_json_path): - logging.error( - "benchmark.json was not found for process: %d at: %s. " - "Did the script forget to call CvCudaPerf.finalize()?" - % (process_idx, benchmark_json_path) - ) - return 1, output_dir - - # Open and read the benchmark.json. - with open(benchmark_json_path, "r") as f: - benchmark_dict = json.loads(f.read()) - - # Second step is to generate a JSON file from the SQLITE database generated by nsys. - # We do this using nsys's stat command. We run it and wait for its completion. - cmd2 = [ - nsys_binary_path, - "stats", - "--force-overwrite", - "true", - "-r", - "%s,%s" % (nsys_gpu_proj_trace_report_path, nsys_pushpop_trace_report_path), - "-f", - "json", - "-o", - out_sqlite_path, - out_sqlite_path + ".sqlite", - ] - subproc = subprocess.Popen(cmd2, stdout=None, stderr=None, env=my_env) - subproc.wait() - - if subproc.returncode: - return subproc.returncode, output_dir - - # Third step is to parse, process and merge the 2 JSONs generated by nsys above - # One JSON is for CPU push-pop times and the other is for the GPU times. - cpu_json_path = os.path.join(output_dir, "perf_report_nvtx_pushpop_trace.json") - gpu_json_path = os.path.join(output_dir, "perf_report_nvtx_gpu_proj_trace.json") - - # Debug: Check JSON file sizes and existence - logger = logging.getLogger("benchmark.py") - cpu_json_size = ( - os.path.getsize(cpu_json_path) if os.path.exists(cpu_json_path) else 0 - ) - gpu_json_size = ( - os.path.getsize(gpu_json_path) if os.path.exists(gpu_json_path) else 0 - ) - logger.info( - f"NVTX JSON files - CPU: {cpu_json_size} bytes, GPU: {gpu_json_size} bytes" - ) - - cpu_range_info = parse_nvtx_pushpop_trace_json(cpu_json_path) - gpu_range_info = parse_nvtx_gpu_proj_trace_json(gpu_json_path) - - # Debug: Count ranges parsed - cpu_range_count = ( - sum( - len(threads) - for process in cpu_range_info.values() - for threads in process.values() - ) - if cpu_range_info - else 0 - ) - gpu_range_count = ( - sum( - len(threads) - for process in gpu_range_info.values() - for threads in process.values() - ) - if gpu_range_info - else 0 - ) - logger.info(f"Parsed NVTX ranges - CPU: {cpu_range_count}, GPU: {gpu_range_count}") - - if cpu_range_count == 0 and gpu_range_count == 0: - logger.warning( - "WARNING: No NVTX ranges captured! This may indicate:\n" - " 1. NVTX Python package incompatibility with Python version\n" - " 2. nsys unable to hook into the Python process\n" - " 3. NVTX markers not being emitted properly" - ) - # Process - cpu_range_info = expand_nvtx_range_names(cpu_range_info) - gpu_range_info = expand_nvtx_range_names(gpu_range_info) - # Merge - all_range_info = merge_cpu_and_gpu_ranges(cpu_range_info, gpu_range_info) - # Calculate averages across processes/threads - mean_ranges_info = calc_mean_ranges(all_range_info) - - # Final step is to pull the data from the all_range_info we generated above and fill - # it in the benchmark_dict. - # Debug: Show what keys the benchmark dict has - benchmark_keys = list(benchmark_dict["data"].keys()) - logger.info( - f"Benchmark dict has {len(benchmark_keys)} keys. Sample (first 5): {benchmark_keys[:5]}" - ) - logger.info( - f"NVTX ranges has {len(mean_ranges_info)} keys. Sample (first 5): {list(mean_ranges_info.keys())[:5]}" - ) - logger.info(f"obj_name prepend: '{benchmark_dict['meta']['obj_name']}'") - - matched_keys = 0 - unmatched_keys = [] - for k in mean_ranges_info: - # Prepare the key to look for by prepending the obj_name value to the key. - # obj_name is the key that sits at the root level and NSYS does not know about it. - # Remove all ':' from NVTX keys (NSYS uses ':' as hierarchy markers) - clean_key = k.replace(":", "") - prepended_key = os.path.join(benchmark_dict["meta"]["obj_name"], clean_key) - if prepended_key in benchmark_dict["data"]: - benchmark_dict["data"][prepended_key] = mean_ranges_info[k] - matched_keys += 1 - else: - unmatched_keys.append(k) - - logger.info( - f"Key matching: {matched_keys} matched, " - f"{len(unmatched_keys)} unmatched out of {len(mean_ranges_info)} total" - ) - if len(unmatched_keys) > 0 and len(unmatched_keys) <= 5: - logger.info(f"Sample unmatched keys: {unmatched_keys[:5]}") - elif len(unmatched_keys) > 5: - logger.info(f"Sample unmatched keys (first 5): {unmatched_keys[:5]}") - - # Un-flatten the benchmark dictionary and compute additional stats such as - # per batch and per frame numbers. We rely on our own data that was saved in the - # batch info key when benchmark.json was first created by perf_utils. This calculation - # is not done by NSYS. - unflatten_process_benchmark_dict(benchmark_dict, warmup_batches) - - # Write the updated benchmark dictionary. - with open(benchmark_json_path, "w") as f: - f.write(json.dumps(benchmark_dict, indent=4, cls=NumpyValuesEncoder)) - - # Delete the temporary files. - os.remove(os.path.join(output_dir, "perf_report_nvtx_pushpop_trace.json")) - os.remove(os.path.join(output_dir, "perf_report_nvtx_gpu_proj_trace.json")) - os.remove(os.path.join(output_dir, "perf_report.sqlite")) - - return 0, output_dir - - -def monitor_gpu_metrics(list_of_device_ids, terminate_event, gpu_metrics_info): - """ - Monitors various GPU metrics directly from NVIDIA-smi. These metrics are not accessible - from NSYS as of now. The monitoring stays on until an event is received. - :param list_of_device_ids: A list of string values of the GPU-ids that are being used in this - benchmark run. - :param terminate_event: An event that can mark an end of the monitoring process. - :param gpu_metrics_info: A multiprocessing share dictionary to store the results of monitoring. - """ - # Initialize the gpu_metrics_info dictionary for the first time. We will save the - # following pieces of information per GPU: - # - # 1) The total GPU power drawn in Watts - # 2) The GPU utilization in %. - # 3) The GPU temperature - # 4) The clock event reasons active. (shows any reasons why the GPU clock was changed.) - - # We will work in a local dictionary first. Only when we are done that we would - # transfer its contents to the mp managed dictionary. Because otherwise the mp - # managed dictionary has no way of knowing when a nested key-value changes and it - # won't update/save it. - gpu_metrics_info_local = { - "power.draw.watts": {}, - "utilization.gpu": {}, - "temperature.gpu": {}, - "clocks_event_reasons": {}, - "clocks.current.graphics": {}, - } - for device_id in list_of_device_ids: - for k in gpu_metrics_info_local: - gpu_metrics_info_local[k]["GPU: %s" % device_id] = [] - - # Begin the monitoring loop. Continue till we are asked to stopped by the event. - while not terminate_event.is_set(): - # Use nvidia-smi to get power draw and GPU utilization numbers for all GPUs. - proc_ret = subprocess.run( - [ - "nvidia-smi", - "-i=%s" % ",".join(list_of_device_ids), - "--query-gpu=power.draw,utilization.gpu,temperature.gpu," - "clocks_event_reasons.active,clocks.current.graphics", - "--format=csv,nounits,noheader", - ], - stdout=subprocess.PIPE, - ) - if proc_ret.returncode == 0: - outputs = proc_ret.stdout.decode().strip().split("\n") - for idx, device_id in enumerate(list_of_device_ids): - ( - power_draw, - gpu_util, - gpu_temp, - clocks_event_reasons, - graphics_clock, - ) = outputs[idx].split(",") - power_draw = float(power_draw) - gpu_util = float(gpu_util) - gpu_temp = float(gpu_temp) - clocks_event_reasons = int(clocks_event_reasons, 16) # Hex to Decimal - graphics_clock = float(graphics_clock) - - gpu_metrics_info_local["power.draw.watts"][ - "GPU: %s" % device_id - ].append(power_draw) - gpu_metrics_info_local["utilization.gpu"]["GPU: %s" % device_id].append( - gpu_util - ) - gpu_metrics_info_local["temperature.gpu"]["GPU: %s" % device_id].append( - gpu_temp - ) - gpu_metrics_info_local["clocks_event_reasons"][ - "GPU: %s" % device_id - ].append(clocks_event_reasons) - gpu_metrics_info_local["clocks.current.graphics"][ - "GPU: %s" % device_id - ].append(graphics_clock) - else: - for device_id in list_of_device_ids: - for k in gpu_metrics_info_local.keys(): - gpu_metrics_info_local[k]["GPU: %s" % device_id].append(0.0) - - # Sleep a bit - time.sleep(0.3) # 300 milliseconds - - # Update the mp managed dictionary - gpu_metrics_info.update(gpu_metrics_info_local) - - -def plot_gpu_metrics(gpu_metrics_info, output_dir): - """ - Plots GPU metrics as a matplotlib plot. - """ - for metric_name in gpu_metrics_info: - # Create pandas data frame. - df = pd.DataFrame(gpu_metrics_info[metric_name]) - ax = df.plot(title=metric_name) - ax.set_xlabel("Execution time") - ax.set_ylabel(metric_name) - fig = ax.get_figure() - fig.savefig(os.path.join(output_dir, "plot.%s.jpg" % metric_name)) - plt.close(fig) - - -def main(): - parser = argparse.ArgumentParser( - "Performance benchmarking script for CV-CUDA.", - formatter_class=argparse.ArgumentDefaultsHelpFormatter, - ) - - parser.add_argument( - "-np", - "--num_processes", - type=int, - default=1, - help="The number of processes to spawn.", - ) - - parser.add_argument( - "-ng", "--num_gpus", type=int, default=1, help="The number of GPUs to use." - ) - - parser.add_argument( - "-go", - "--gpu_offset_id", - type=int, - default=0, - help="Offset for the GPU ids, assuming the GPUs are stacked together in a multi-GPU node.", - ) - - parser.add_argument( - "-ll", - "--log_level", - type=str, - choices=["info", "error", "debug", "warning"], - default="info", - help="Sets the desired logging level. Affects the std-out printed by the " - "sample when it is run.", - ) - - parser.add_argument( - "-o", - "--output_dir", - default="/tmp", - type=str, - help="The folder where the output results should be stored.", - ) - - parser.add_argument( - "-w", - "--warmup_batches", - type=int, - default=1, - help="Sets the number of batches that should be ignored from being counted in " - "the totals of the performance benchmarking numbers. These many batches are ignored" - " from the front and the end.", - ) - - parser.add_argument( - "-m", - "--maximize_clocks", - action="store_true", - help="Maximizes the GPU clocks and power limits before running the benchmark. " - "Clocks are not maximized by default.", - ) - - parser.add_argument( - "script", - help="The script that you want to benchmark.", - ) - - parser.add_argument( - "args", - nargs=argparse.REMAINDER, - help="Any command-line arguments that should be passed to the script being benchmarked.", - ) - - args = parser.parse_args() - - if not os.path.isfile(args.script): - raise ValueError("Script file does not exist at: %s" % args.script) - - logging.basicConfig( - format="[%(name)s:%(lineno)d] %(asctime)s %(levelname)-6s %(message)s", - level=getattr(logging, args.log_level.upper()), - datefmt="%Y-%m-%d %H:%M:%S", - ) - logger = logging.getLogger("benchmark.py") - - # Check and raise error if the output directory exists in the child process's args. - if "-o" in args.args or "--output_dir" in args.args: - raise ValueError( - "The output directory must only be specified once for benchmark.py. " - "Do not specify it in the command line arguments of the script to be benchmarked." - ) - - # Maximize the clocks. - clocks_info = [] - all_device_ids = [] - for gpu_idx in range(args.num_gpus): - device_id = args.gpu_offset_id + gpu_idx - - all_device_ids.append(str(device_id)) - if args.maximize_clocks: - ( - did_maximize_clocks, - was_persistence_mode_on, - current_power_limit, - ) = maximize_clocks(logger, device_id) - clocks_info.append((was_persistence_mode_on, current_power_limit)) - - # We will start multiple processes, per num_processes per num_gpus in Pool to run the benchmarks. - pool = mp.Pool() - # Create an event to signal other processes (e.g. monitor_gpu_metrics to stop when main pool has stopped) - pool_terminate_event = mp.Event() - # Create a shared dict to retrieve the results of monitor_gpu_metrics - mp_manager = mp.Manager() - gpu_metrics_info = mp_manager.dict() - # Finally allocate a list to store the Pool's process's results. - results = [] - - # Begin by starting one process to keep on monitoring various GPU metrics that are not available via NSYS. - gpu_metric_monitor_proc = mp.Process( - target=monitor_gpu_metrics, - args=(all_device_ids, pool_terminate_event, gpu_metrics_info), - ) - gpu_metric_monitor_proc.start() - - # Then we start the multiprocessing Pool. - for gpu_idx in range(args.num_gpus): - for process_idx in range(args.num_processes): - # Since each the output of each process needs to be stored in a different directory, - # we will create the directory based on the process index and the GPU index. - proc_output_dir = os.path.join( - args.output_dir, "proc_%d_gpu_%d" % (process_idx, gpu_idx) - ) - if not os.path.exists(proc_output_dir): - os.makedirs(proc_output_dir) - - # Supply additional command-line arguments to make sure each process - # behaves correctly. - proc_device_id = str(args.gpu_offset_id + gpu_idx) - proc_args = args.args.copy() - # The following will make sure that it inserts the additional args - # only at the beginning of the list so that it doesn't interfere with a - # potentially argparse.REMAINDER style arg present at the end. - - # Need to set this to 0 because once CUDA_VISIBLE_DEVICES is used, - # the process won't be able to see other gpus - proc_args[:0] = ["--device_id", "0"] - proc_args[:0] = ["--output_dir", proc_output_dir] - # Start the pool. - result = pool.apply_async( - benchmark_script, - args=( - process_idx, - proc_device_id, - proc_output_dir, - args.warmup_batches, - args.script, - proc_args, - ), - ) - logger.info("Launched process: %d. gpu-idx: %d" % (process_idx, gpu_idx)) - results.append(result) - - # Close the pool and wait everything to finish. - pool.close() - pool.join() - - # Set the terminate event so other processes know that the Pool has finished. - pool_terminate_event.set() - # Wait for the gpu_metric_monitor process to finish. - gpu_metric_monitor_proc.join() - - # Reset the clocks. - if args.maximize_clocks: - for gpu_idx in range(args.num_gpus): - device_id = args.gpu_offset_id + gpu_idx - - was_persistence_mode_on, current_power_limit = clocks_info[gpu_idx] - - reset_clocks( - logger, - device_id, - was_persistence_mode_on, - current_power_limit, - ) - else: - logger.warning("Clocks were not maximized during this run.") - - # We must create a copy of gpu_metrics_info to detach it from multiprocessing. - gpu_metrics_info = gpu_metrics_info.copy() - - # Plot the GPU metrics. - plot_gpu_metrics(gpu_metrics_info, args.output_dir) - - # Now we need to : - # 1) Write the gpu_metrics_info in to the benchmark.json files stored per - # process and - # 2) Calculate various stats at the all processes level. - # e.g. If we ran 1 or more processes, there will be a benchmark_mean.json - # created in the output root folder with mean and other stats computed from - # all benchmark.json files of all the processes. - # This can only be done if all processes finished without error. - # So we will check that first and if that is the case, we will - # read their benchmark.json data in a list to later calculate various stats. - all_data_dicts = [] - for r in results: - # Grab the return result from the pool. - proc_ret_code, proc_output_dir = r.get() - if proc_ret_code: - # Any non-zero return code mean the process failed. - raise Exception( - "Process: %d on gpu: %d exited with a non-zero return code: %d" - % (process_idx, gpu_idx, proc_ret_code) - ) - else: - # Zero return code means success. Read the benchmark.json. - with open(os.path.join(proc_output_dir, "benchmark.json"), "r") as f: - benchmark_dict = json.loads(f.read()) - - # Update this benchmark dict with GPU metrics for this GPU id. - for metric_name in gpu_metrics_info: - device_id_of_this_proc = benchmark_dict["meta"]["device"]["id"] - benchmark_dict["gpu_metrics"][metric_name] = gpu_metrics_info[ - metric_name - ]["GPU: %d" % device_id_of_this_proc] - - with open(os.path.join(proc_output_dir, "benchmark.json"), "w") as f: - f.write(json.dumps(benchmark_dict, indent=4, cls=NumpyValuesEncoder)) - - # Append to our list of data dict. - all_data_dicts.append(benchmark_dict["data"]) - - # 1) Compute mean of the data field from all processes... - data_mean_all_procs = {} - # First recursively collect all values from all the data dictionaries of all processes. - for data_dict in all_data_dicts: - recurse_gather_dict(data_dict, data_mean_all_procs) - # And then compute just the mean over this. - recurse_calc_stats_dict(data_mean_all_procs, compute_mean_only=True) - - # 2) Compute various stats of the data_stats_minus_warmup field from all processes... - # Now compute all the stats (such as mean, median etc) for all processes from all numbers. - # NOTE: We have already computed these stats per process in the benchmark.json's - # data_stats_minus_warmup field. This time, we want to do it over all the processes. Instead - # of taking mean of those numbers, we will calculate the freshly, combining all data points. - # This results in much accurate statistics. - # We will use last process's benchmark_dict to use query some important fields such as - # batch_info and total_batches etc. This assumes that all processes ran the same code. - data_stats_all_procs = {} - for batch_range_name in benchmark_dict["batch_info"]: - batch_idx, batch_size = benchmark_dict["batch_info"][batch_range_name] - # Next, we find out the batch level prefix. This is the key in which - # the batches are nested. One profiling session can have multiple levels - # at which batches may be used. - # e.g. - # program_X: - # method_A: - # batch_1 - # batch_2 - # method_B: - # batch_1 - # batch_2 - # - # We need to find mean at these two levels (i.e. method_A and method_B) - # in this case. - # programA/method_A and program_A/method_B are the batch level prefix here. - # We can easily get those by using the dirname method since those are like - # the directory names in a path. - batch_level_prefix = os.path.dirname(batch_range_name) - - if ( - batch_size > 0 - and batch_idx + 1 > args.warmup_batches - and (batch_idx + 1 + args.warmup_batches) - <= benchmark_dict["meta"]["total_batches"][batch_level_prefix] - ): - # Keep on updating the data_stats dictionary. This will create a dictionary - # that is union of all the dictionaries at the batch level for all processes. - nested_keys = batch_range_name.split("/") - - for data_dict in all_data_dicts: - source_dict = data_dict - - # Go deep down the nested key path from the root. - for k in nested_keys: - source_dict = source_dict[k] - - # Need to recursively update the data_stats_all_procs based on - # the source_dict. We will sum the values up. - if batch_level_prefix not in data_stats_all_procs: - data_stats_all_procs[batch_level_prefix] = {} - - recurse_gather_dict( - source_dict, target_dict=data_stats_all_procs[batch_level_prefix] - ) - - # Once all the data points are gathered, we need to divide by the length to figure - # out the mean values. - recurse_calc_stats_dict( - data_stats_all_procs, - compute_throughput=True, - throughput_multiplier=args.num_gpus * args.num_processes, - ) - - # 3). Compute stats of of all GPU metrics for all GPUs involved. - gpu_metrics_all_procs = {} - for metric_name in gpu_metrics_info: - gpu_metrics_all_procs[metric_name] = [] - for device_id in gpu_metrics_info[metric_name]: - # Gather all - gpu_metrics_all_procs[metric_name].extend( - gpu_metrics_info[metric_name][device_id] - ) - - # Compute stats. - recurse_calc_stats_dict( - gpu_metrics_all_procs, - ) - - mean_benchmark_data = { - "data_mean_all_procs": data_mean_all_procs, - "data_stats_minus_warmup_all_procs": data_stats_all_procs, - "gpu_metrics_all_procs": gpu_metrics_all_procs, - "meta": {"args": {}}, - } - for arg in vars(args): - mean_benchmark_data["meta"]["args"][arg] = getattr(args, arg) - - # Write it in a file. - mean_benchmark_json_path = os.path.join(args.output_dir, "benchmark_mean.json") - with open(mean_benchmark_json_path, "w") as f: - f.write(json.dumps(mean_benchmark_data, indent=4, cls=NumpyValuesEncoder)) - logger.info( - "Benchmarking completed successfully. Results saved at: %s" - % mean_benchmark_json_path - ) - - -if __name__ == "__main__": - main() diff --git a/bench/python/install_bench_dependencies.sh b/bench/python/install_bench_dependencies.sh new file mode 100755 index 000000000..85156a382 --- /dev/null +++ b/bench/python/install_bench_dependencies.sh @@ -0,0 +1,180 @@ +#!/bin/bash -e + +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +# This script installs dependencies for CV-CUDA nvbench-based Python benchmarks. +# All packages (including cuda-bench from PyPI) are declared in requirements.bench.cu{12,13}.txt. +# When running inside a Docker devel image, all dependencies are pre-installed and +# this script is effectively a no-op. +# +# Usage: +# install_bench_dependencies.sh + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SEPARATOR_LINE="=======================================" + +echo "$SEPARATOR_LINE" +echo "CV-CUDA Benchmark Dependency Installer" +echo "$SEPARATOR_LINE" +echo "" + +# Check Python version compatibility +PYTHON_VERSION=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")') +PYTHON_MINOR=$(echo "$PYTHON_VERSION" | cut -d. -f2) + +echo "Detected Python version: $PYTHON_VERSION" + +# Validate Python version (nvbench requires Python 3.10+) +if [[ "$PYTHON_MINOR" -lt 10 ]]; then + echo "Error: Python $PYTHON_VERSION is not compatible with nvbench (requires Python 3.10+)" >&2 + echo "Please use Python 3.10 or higher" >&2 + exit 1 +fi + +echo "✓ Python version compatible" +echo "" + +# Check CUDA version. Begin by checking if nvcc command exists. +if command -v nvcc >/dev/null 2>&1; then + # Get CUDA version from nvcc output + CUDA_VERSION=$(nvcc --version | grep "release" | awk '{print $5}' | tr -d ',') + + # Extract major version number + CUDA_MAJOR_VERSION=$(echo "$CUDA_VERSION" | cut -d. -f1) + + echo "Detected CUDA version: $CUDA_VERSION (major: $CUDA_MAJOR_VERSION)" + + # Check major version to determine CUDA version + if [[ "$CUDA_MAJOR_VERSION" -eq 12 ]] || [[ "$CUDA_MAJOR_VERSION" -eq 13 ]]; then + echo "✓ CUDA $CUDA_MAJOR_VERSION is supported" + else + echo "Warning: CUDA $CUDA_MAJOR_VERSION may not be fully supported (expecting CUDA 12 or 13)" + echo "Proceeding anyway..." + fi +else + echo "Error: CUDA is not installed or nvcc is not in PATH" >&2 + echo "Please install CUDA Toolkit 12 or 13" >&2 + exit 1 +fi + +echo "" + +# Check for CUPTI library (required by nvbench) +CUPTI_FOUND=false +for CUPTI_PATH in /usr/local/cuda/lib64/libcupti.so /usr/local/cuda-${CUDA_MAJOR_VERSION}/lib64/libcupti.so /usr/local/cuda-${CUDA_VERSION}/lib64/libcupti.so; do + if [[ -f "$CUPTI_PATH" ]]; then + echo "✓ Found CUPTI library: $CUPTI_PATH" + CUPTI_FOUND=true + break + fi +done + +if [[ "$CUPTI_FOUND" == false ]]; then + echo "Warning: CUPTI library not found in standard locations" + echo "nvbench requires libcupti.so from CUDA Toolkit" + echo "Please ensure CUDA Toolkit is fully installed" + echo "" +fi + +# Check LD_LIBRARY_PATH +if [[ ":$LD_LIBRARY_PATH:" == *":/usr/local/cuda/lib64:"* ]] || [[ ":$LD_LIBRARY_PATH:" == *":/usr/local/cuda-${CUDA_MAJOR_VERSION}/lib64:"* ]]; then + echo "✓ LD_LIBRARY_PATH includes CUDA libraries" +else + echo "⚠ Warning: LD_LIBRARY_PATH may not include CUDA libraries" + echo " You may need to set:" + echo " export LD_LIBRARY_PATH=/usr/local/cuda/lib64:\$LD_LIBRARY_PATH" + echo " or add it to your ~/.bashrc" + echo "" +fi + +# Check if pip is available +if ! command -v pip3 &> /dev/null && ! command -v pip &> /dev/null; then + echo "Error: pip is not installed. Please install python3-pip." >&2 + exit 1 +fi + +# Check if we're in a virtual environment (recommended for CI) +if [[ -z "$VIRTUAL_ENV" ]]; then + echo "Note: Not running in a virtual environment." + echo "For isolated installations, consider using:" + echo " python3 -m venv venv_bench" + echo " source venv_bench/bin/activate" + echo "" +fi + +# Upgrade pip (only if in virtual environment to avoid PEP 668 issues on Python 3.12+) +if [[ -n "$VIRTUAL_ENV" ]]; then + echo "Upgrading pip in virtual environment..." + python3 -m pip install --upgrade pip +else + echo "Skipping pip upgrade (not in virtual environment)" +fi + +echo "" +echo "Installing Python dependencies for CUDA $CUDA_MAJOR_VERSION..." +echo "" + +REQUIREMENTS_FILE="$SCRIPT_DIR/requirements.bench.cu${CUDA_MAJOR_VERSION}.txt" + +# Check that required requirements files have been generated. +if [[ ! -f "$REQUIREMENTS_FILE" ]]; then + echo "Error: the following requirements file is missing:" >&2 + echo " $REQUIREMENTS_FILE" >&2 + echo "Generate it first by running from the repository root:" >&2 + echo " bash generate_requirements.sh" >&2 + exit 1 +fi + +echo "Installing from: $REQUIREMENTS_FILE" +python3 -m pip install -r "$REQUIREMENTS_FILE" + +echo "" +echo "Checking cvcuda..." + +if ! python3 -m pip show cvcuda-cu${CUDA_MAJOR_VERSION} >/dev/null 2>&1; then + echo "Installing cvcuda for CUDA $CUDA_MAJOR_VERSION..." + python3 -m pip install cvcuda-cu${CUDA_MAJOR_VERSION} +else + echo "✓ cvcuda-cu${CUDA_MAJOR_VERSION} already installed, skipping." +fi + +echo "" +echo "$SEPARATOR_LINE" +echo "✓ Dependency installation complete!" +echo "$SEPARATOR_LINE" +echo "" +echo "You can now run benchmarks:" +echo " cd $(dirname "$SCRIPT_DIR")/bin" +echo "" +echo " # Run all benchmarks (C++ and Python)" +echo " python3 run_bench.py ." +echo "" +echo " # Run only C++ or Python" +echo " python3 run_bench.py . --lang cpp" +echo " python3 run_bench.py . --lang python" +echo "" +echo "Or run individual benchmarks:" +echo "" +echo " Python benchmarks:" +echo " python3 bench_resize.py --axis shape=1x1080x1920 --axis dtype=uint8" +echo " python3 bench_gaussian.py --axis shape=1x1080x1920" +echo "" +echo " C++ benchmarks:" +echo " ./bench_resize --axis shape=1x1080x1920 --csv out.csv" +echo " ./bench_gaussian --axis shape=1x1080x1920 --csv out.csv" +echo "" diff --git a/bench/python/install_dependencies.sh b/bench/python/install_dependencies.sh deleted file mode 100755 index 22cf8adb3..000000000 --- a/bench/python/install_dependencies.sh +++ /dev/null @@ -1,160 +0,0 @@ -#!/bin/bash -e - -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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. - -# This script installs all the dependencies required to run CV-CUDA benchmarks. -# It uses the /tmp folder to download temporary data and libraries. - -# Check CUDA version. Begin by checking if nvcc command exists. -if command -v nvcc >/dev/null 2>&1; then - # Get CUDA version from nvcc output - CUDA_VERSION=$(nvcc --version | grep "release" | awk '{print $5}') - - # Extract major version number - CUDA_MAJOR_VERSION=$(echo "$CUDA_VERSION" | cut -d. -f1) - - # Check major version to determine CUDA version - if [ "$CUDA_MAJOR_VERSION" -eq 12 ] || [ "$CUDA_MAJOR_VERSION" -eq 13 ]; then - echo "CUDA $CUDA_MAJOR_VERSION is installed." - else - echo "Unknown/Unsupported CUDA version." - exit 1 - fi -else - echo "CUDA is not installed." - exit 1 -fi - -# Check Python version compatibility -PYTHON_VERSION=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")') -PYTHON_MINOR=$(echo "$PYTHON_VERSION" | cut -d. -f2) - -echo "Detected Python version: $PYTHON_VERSION" - -# Validate Python version (benchmarking requires Python 3.10-3.13 per README.md) -if [ "$PYTHON_MINOR" -lt 10 ] || [ "$PYTHON_MINOR" -gt 13 ]; then - echo "Error: Python $PYTHON_VERSION is not compatible with benchmarking (requires Python 3.10-3.13)" - echo "Please use a compatible Python version" - exit 1 -fi - -set -e # Exit script if any command fails - -# Get script directory (before changing to /tmp) -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" - -# Detect if we need sudo (not needed in Docker containers running as root) -SUDO="" -if [ "$EUID" -ne 0 ]; then - SUDO="sudo" -fi - -# Install basic packages first. -cd /tmp - -# Ensure software-properties-common is installed (usually pre-installed in Docker) -$SUDO apt-get install -y --no-install-recommends software-properties-common 2>/dev/null || true -# Add PPA repository -$SUDO add-apt-repository -y ppa:ubuntu-toolchain-r/test -# Single apt-get update for all subsequent installations -$SUDO apt-get update - -$SUDO apt-get install -y --no-install-recommends \ - build-essential \ - wget \ - yasm \ - unzip \ - cmake \ - git - -# install g++ -$SUDO apt-get install -y --no-install-recommends \ - gcc-11 g++-11 \ - ninja-build - -$SUDO update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-11 11 -$SUDO update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-11 11 -$SUDO update-alternatives --set gcc /usr/bin/gcc-11 -$SUDO update-alternatives --set g++ /usr/bin/g++-11 - -# Install Python and gtest -$SUDO apt-get install -y --no-install-recommends \ - libgtest-dev \ - libgmock-dev \ - python3-pip \ - ninja-build ccache \ - mlocate && $SUDO updatedb - -# Install ffmpeg and other libraries needed for VPF. -# Note: We are not installing either libnv-encode or decode libraries here. -$SUDO apt-get install -y --no-install-recommends \ - ffmpeg \ - libavfilter-dev \ - libavformat-dev \ - libavcodec-dev \ - libswresample-dev \ - libavutil-dev\ - -# Install libssl 1.1.1 -cd /tmp -wget http://archive.ubuntu.com/ubuntu/pool/main/o/openssl/libssl1.1_1.1.0g-2ubuntu4_amd64.deb -$SUDO dpkg -i libssl1.1_1.1.0g-2ubuntu4_amd64.deb - -# Install NVIDIA NSIGHT Systems 2025.5.1 -# Note: Update the build number (currently .1) if a newer build is available -# from https://developer.nvidia.com/nsight-systems -cd /tmp -wget https://developer.download.nvidia.com/devtools/nsight-systems/nsight-systems-2025.5.1_2025.5.1.121-1_amd64.deb -$SUDO apt-get install -y \ - libsm6 \ - libxrender1 \ - libfontconfig1 \ - libxext6 \ - libx11-dev \ - libxkbfile-dev \ - /tmp/nsight-systems-2025.5.1_2025.5.1.121-1_amd64.deb - -echo "export PATH=$PATH:/opt/tensorrt/bin" >> ~/.bashrc - -# Install Python packages for benchmarking -echo "Installing Python dependencies for benchmarking..." - -# Create virtual environment if it doesn't exist -if [ ! -d "$SCRIPT_DIR/venv_bench" ]; then - echo "Creating virtual environment..." - python3 -m venv "$SCRIPT_DIR/venv_bench" -fi - -# Activate virtual environment -echo "Activating virtual environment..." -source "$SCRIPT_DIR/venv_bench/bin/activate" - -# Upgrade pip -echo "Upgrading pip..." -python3 -m pip install --upgrade pip - -# Install CUDA-specific requirements (includes common requirements) -echo "Installing benchmark dependencies for CUDA $CUDA_MAJOR_VERSION..." -python3 -m pip install -r "$SCRIPT_DIR/requirements_cu${CUDA_MAJOR_VERSION}.txt" - -echo "" -echo "Python dependencies installation complete!" -echo "" -echo "To activate the virtual environment, run:" -echo " source $SCRIPT_DIR/venv_bench/bin/activate" -echo "" - -# Done diff --git a/bench/python/nvcodec_utils.py b/bench/python/nvcodec_utils.py deleted file mode 100644 index e0b8dcaa9..000000000 --- a/bench/python/nvcodec_utils.py +++ /dev/null @@ -1,635 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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. - -""" -nvcodec_utils - -This file hosts various helpers for NV codecs exist. -""" - - -import os -import av -import logging -import glob -import numpy as np -import torch -import cvcuda -from fractions import Fraction -import itertools -import PyNvVideoCodec as nvvc -from nvidia import nvimgcodec - -from batch import Batch # noqa: E402 - -pixel_format_to_cvcuda_code = { - nvvc.Pixel_Format.YUV444: cvcuda.ColorConversion.YUV2RGB, - nvvc.Pixel_Format.NV12: cvcuda.ColorConversion.YUV2RGB_NV12, -} - - -class AppCAI: - def __init__(self, shape, stride, typestr, gpualloc): - self.__cuda_array_interface__ = { - "shape": shape, - "strides": stride, - "data": (int(gpualloc), False), - "typestr": typestr, - "version": 3, - } - - -# docs_tag: begin_videobatchdecoder_pyvideocodec -class VideoBatchDecoder: - def __init__( - self, - input_path, - batch_size, - device_id, - cuda_ctx, - cuda_stream, - cvcuda_perf, - ): - # docs_tag: begin_init_videobatchdecoder_pyvideocodec - self.logger = logging.getLogger(__name__) - self.input_path = input_path - self.batch_size = batch_size - self.device_id = device_id - self.cuda_ctx = cuda_ctx - self.cuda_stream = cuda_stream - self.cvcuda_perf = cvcuda_perf - self.total_decoded = 0 - self.batch_idx = 0 - self.decoder = None - self.cvcuda_RGBtensor_batch = None - nvDemux = nvvc.PyNvDemuxer(self.input_path) - self.fps = nvDemux.FrameRate() - self.logger.info("Using PyNvVideoCodec decoder version: %s" % nvvc.__version__) - # docs_tag: end_init_videobatchdecoder_pyvideocodec - - # docs_tag: begin_call_videobatchdecoder_pyvideocodec - def __call__(self): - self.cvcuda_perf.push_range("decoder.pyVideoCodec") - - # docs_tag: begin_alloc_videobatchdecoder_pyvideocodec - # Check if we need to allocate the decoder for its first use. - if self.decoder is None: - self.decoder = nvVideoDecoder( - self.input_path, self.device_id, self.cuda_ctx, self.cuda_stream - ) - # docs_tag: end_alloc_videobatchdecoder_pyvideocodec - - # docs_tag: begin_decode_videobatchdecoder_pyvideocodec - # Get the NHWC YUV tensor from the decoder - cvcuda_YUVtensor = self.decoder.get_next_frames(self.batch_size) - - # Check if we are done decoding - if cvcuda_YUVtensor is None: - self.cvcuda_perf.pop_range() - return None - - # Check the code for the color conversion based in the pixel format - cvcuda_code = pixel_format_to_cvcuda_code.get(self.decoder.pixelFormat) - if cvcuda_code is None: - raise ValueError(f"Unsupported pixel format: {self.decoder.pixelFormat}") - - # Check layout to make sure it is what we expected - if cvcuda_YUVtensor.layout != "NHWC": - raise ValueError("Unexpected tensor layout, NHWC expected.") - - # this may be different than batch size since last frames may not be a multiple of batch size - actual_batch_size = cvcuda_YUVtensor.shape[0] - - # docs_tag: end_decode_videobatchdecoder_pyvideocodec - - # docs_tag: begin_convert_videobatchdecoder_pyvideocodec - # Create a CVCUDA tensor for color conversion YUV->RGB - # Allocate only for the first time or for the last batch. - if not self.cvcuda_RGBtensor_batch or actual_batch_size != self.batch_size: - self.cvcuda_RGBtensor_batch = cvcuda.Tensor( - (actual_batch_size, self.decoder.h, self.decoder.w, 3), - cvcuda.Type.U8, - cvcuda.TensorLayout.NHWC, - ) - - # Convert from YUV to RGB. Conversion code is based on the pixel format. - cvcuda.cvtcolor_into(self.cvcuda_RGBtensor_batch, cvcuda_YUVtensor, cvcuda_code) - - self.total_decoded += actual_batch_size - # docs_tag: end_convert_videobatchdecoder_pyvideocodec - - # docs_tag: begin_batch_videobatchdecoder_pyvideocodec - # Create a batch instance and set its properties. - batch = Batch( - batch_idx=self.batch_idx, - data=self.cvcuda_RGBtensor_batch, - fileinfo=self.input_path, - ) - self.batch_idx += 1 - - self.cvcuda_perf.pop_range() - return batch - # docs_tag: end_call_videobatchdecoder_pyvideocodec - - def start(self): - pass - - def join(self): - pass - - -# docs_tag: end_videobatchdecoder_pyvideocodec - -# docs_tag: begin_imp_nvvideodecoder -class nvVideoDecoder: - def __init__(self, enc_file, device_id, cuda_ctx, stream): - """ - Create instance of HW-accelerated video decoder. - :param enc_file: Full path to the MP4 file that needs to be decoded. - :param device_id: id of video card which will be used for decoding & processing. - :param cuda_ctx: A cuda context object. - """ - self.device_id = device_id - self.cuda_ctx = cuda_ctx - self.input_path = enc_file - self.stream = stream - # Demuxer is instantiated only to collect required information about - # certain video file properties. - self.nvDemux = nvvc.PyNvDemuxer(self.input_path) - self.nvDec = nvvc.CreateDecoder( - gpuid=0, - codec=self.nvDemux.GetNvCodecId(), - cudacontext=self.cuda_ctx.handle, - cudastream=self.stream.handle, - enableasyncallocations=False, - ) - - self.w, self.h = self.nvDemux.Width(), self.nvDemux.Height() - self.pixelFormat = self.nvDec.GetPixelFormat() - # In case sample aspect ratio isn't 1:1 we will re-scale the decoded - # frame to maintain uniform 1:1 ratio across the pipeline. - sar = 8.0 / 9.0 - self.fixed_h = self.h - self.fixed_w = int(self.w * sar) - - # frame iterator - def generate_decoded_frames(self): - for packet in self.nvDemux: - for decodedFrame in self.nvDec.Decode(packet): - cvcudaTensor = cvcuda.as_tensor( - cvcuda.as_image(decodedFrame.nvcv_image(), cvcuda.Format.U8) - ) - if cvcudaTensor.layout == "NCHW": - # This will re-format the NCHW tensor to a NHWC tensor which will create - # a copy in the CUDA device decoded frame will go out of scope and the - # backing memory will be available by the decoder. - yield cvcuda.reformat(cvcudaTensor, "NHWC") - else: - raise ValueError("Unexpected tensor layout, NCHW expected.") - - def get_next_frames(self, N): - decoded_frames = list(itertools.islice(self.generate_decoded_frames(), N)) - if len(decoded_frames) == 0: - return None - elif len(decoded_frames) == 1: # this case we dont need stack the tensor - return decoded_frames[0] - else: - # convert from list of tensors to a single tensor (NHWC) - tensorNHWC = cvcuda.stack(decoded_frames) - return tensorNHWC - - -# docs_tag: end_imp_nvvideodecoder - -# docs_tag: begin_init_videobatchencoder_pyvideocodec -class VideoBatchEncoder: - def __init__( - self, - output_path, - fps, - device_id, - cuda_ctx, - cuda_stream, - cvcuda_perf, - ): - self.logger = logging.getLogger(__name__) - self.output_path = output_path - self.fps = fps - self.device_id = device_id - self.cuda_ctx = cuda_ctx - self.cuda_stream = cuda_stream - self.cvcuda_perf = cvcuda_perf - - self.encoder = None - self.cvcuda_HWCtensor_batch = None - self.cvcuda_YUVtensor_batch = None - self.input_layout = "NCHW" - self.gpu_input = True - self.output_file_name = None - - self.logger.info("Using PyNvVideoCodec encoder version: %s" % nvvc.__version__) - # docs_tag: end_init_videobatchencoder_pyvideocodec - - # docs_tag: begin_call_videobatchencoder_pyvideocodec - def __call__(self, batch): - self.cvcuda_perf.push_range("encoder.pyVideoCodec") - - # Get the name of the original video file read by the decoder. We would use - # the same filename to save the output video. - file_name = os.path.splitext(os.path.basename(batch.fileinfo))[0] - self.output_file_name = os.path.join(self.output_path, "out_%s.mp4" % file_name) - - assert isinstance(batch.data, torch.Tensor) - - # docs_tag: begin_alloc_cvcuda_videobatchencoder_pyvideocodec - # Check if we need to allocate the encoder for its first use. - if self.encoder is None: - self.encoder = nvVideoEncoder( - self.device_id, - batch.data.shape[3], - batch.data.shape[2], - self.fps, - self.output_file_name, - self.cuda_ctx, - self.cuda_stream, - "NV12", - ) - # docs_tag: end_alloc_cvcuda_videobatchencoder_pyvideocodec - - # docs_tag: begin_convert_videobatchencoder_pyvideocodec - - # Create 2 CVCUDA tensors: reformat NCHW->NHWC and color conversion RGB->YUV - current_batch_size = batch.data.shape[0] - height, width = batch.data.shape[2], batch.data.shape[3] - - # Allocate only for the first time or for the last batch. - if ( - not self.cvcuda_HWCtensor_batch - or current_batch_size != self.cvcuda_HWCtensor_batch.shape[0] - ): - self.cvcuda_HWCtensor_batch = cvcuda.Tensor( - (current_batch_size, height, width, 3), - cvcuda.Type.U8, - cvcuda.TensorLayout.NHWC, - ) - self.cvcuda_YUVtensor_batch = cvcuda.Tensor( - (current_batch_size, (height // 2) * 3, width, 1), - cvcuda.Type.U8, - cvcuda.TensorLayout.NHWC, - ) - - # Convert RGB to NV12, in batch, before sending it over to pyVideoCodec. - # Convert to CVCUDA tensor - cvcuda_tensor = cvcuda.as_tensor(batch.data, cvcuda.TensorLayout.NCHW) - - # Reformat NCHW to NHWC - cvcuda.reformat_into(self.cvcuda_HWCtensor_batch, cvcuda_tensor) - - # Color convert from RGB to YUV_NV12 - cvcuda.cvtcolor_into( - self.cvcuda_YUVtensor_batch, - self.cvcuda_HWCtensor_batch, - cvcuda.ColorConversion.RGB2YUV_NV12, - ) - - # Convert back to torch tensor we are NV12 - tensor = torch.as_tensor(self.cvcuda_YUVtensor_batch.cuda(), device="cuda") - # docs_tag: end_convert_videobatchencoder_pyvideocodec - - # docs_tag: begin_encode_videobatchencoder_pyvideocodec - # Encode frames from the batch one by one using pyVideoCodec. - for img_idx in range(tensor.shape[0]): - img = tensor[img_idx] - self.encoder.encode_from_tensor(img) - - self.cvcuda_perf.pop_range() - - def start(self): - pass - - def join(self): - # self.encoder.flush() - self.logger.info("Wrote: %s" % self.output_file_name) - - -# docs_tag: end_init_videobatchencoder_pyvideocodec - -# docs_tag: begin_imp_nvvideoencoder -class nvVideoEncoder: - def __init__( - self, - device_id, - width, - height, - fps, - enc_file, - cuda_ctx, - cuda_stream, - format, - ): - """ - Create instance of HW-accelerated video encoder. - :param device_id: id of video card which will be used for encoding & processing. - :param width: encoded frame width. - :param height: encoded frame height. - :param fps: The FPS at which the encoding should happen. - :param enc_file: path to encoded video file. - :param cuda_ctx: A cuda context object - :param format: The format of the encoded video file. - (e.g. "NV12", "YUV444" see NvPyVideoEncoder docs for more info) - """ - self.device_id = device_id - self.fps = round(Fraction(fps), 6) - self.enc_file = enc_file - self.cuda_ctx = cuda_ctx - self.cuda_stream = cuda_stream - - self.pts_time = 0 - self.delta_t = 1 # Increment the packets' timestamp by this much. - self.encoded_frame = np.ndarray(shape=(0), dtype=np.uint8) - self.container = av.open(enc_file, "w") - self.avstream = self.container.add_stream("h264", rate=self.fps) - - aligned_value = 0 - if width % 16 != 0: - aligned_value = 16 - (width % 16) - aligned_width = width + aligned_value - width = aligned_width - - self.avstream.width = width - self.avstream.height = height - - self.avstream.time_base = 1 / Fraction(self.fps) - self.surface = None - self.surf_plane = None - - self.tmpTensor = None - - self.nvEnc = nvvc.CreateEncoder( - self.avstream.width, - self.avstream.height, - format, - codec="h264", - preset="P4", - cudastream=cuda_stream.handle, - ) - - def width(self): - """ - Gets the actual video frame width from the encoder. - """ - return self.nvEnc.Width() - - def height(self): - """ - Gets the actual video frame height from the encoder. - """ - return self.nvEnc.Height() - - # docs_tag: begin_imp_nvvideoencoder - - def encode_from_tensor(self, tensor): - - # Create a CUDA array interface object wit 2 planes one for luma and CrCb for NV12 - objCAI = [] - # Need to compute the address of the Y plane and the interleaved chroma plane - data = ( - tensor.storage().data_ptr() - + tensor.storage_offset() * tensor.element_size() - ) - objCAI.append( - AppCAI( - (self.avstream.height, self.avstream.width, 1), - (self.avstream.width, 1, 1), - "|u1", - data, - ) - ) - chromaAlloc = int(data) + self.avstream.width * self.avstream.height - objCAI.append( - AppCAI( - (int(self.avstream.height / 2), int(self.avstream.width / 2), 2), - (self.avstream.width, 2, 1), - "|u1", - chromaAlloc, - ) - ) - # Encode the frame takes CUDA array interface object as input - self.encoded_frame = self.nvEnc.Encode(objCAI) - self.write_frame( - self.encoded_frame, - self.pts_time, - self.fps, - self.avstream, - self.container, - ) - self.pts_time += self.delta_t - - # docs_tag: end_imp_nvvideoencoder - - # docs_tag: begin_writeframe_nvvideoencoder - def write_frame(self, encoded_frame, pts_time, fps, stream, container): - encoded_bytes = bytearray(encoded_frame) - pkt = av.packet.Packet(encoded_bytes) - pkt.pts = pts_time - pkt.dts = pts_time - pkt.stream = stream - pkt.time_base = 1 / Fraction(fps) - container.mux(pkt) - - # docs_tag: end_writeframe_nvvideoencoder - - def flush(self): - encoded_bytes = self.nvEnc.EndEncode() - if encoded_bytes: - self.write_frame( - encoded_bytes, - self.pts_time, - self.fps, - self.avstream, - self.container, - ) - self.pts_time += self.delta_t - self.container.close() - - -# docs_tag: end_imp_nvvideoencoder - -# docs_tag: begin_imagebatchdecoder_nvimagecodec -class ImageBatchDecoder: - def __init__( - self, - input_path, - batch_size, - device_id, - cuda_ctx, - cuda_stream, - cvcuda_perf, - ): - - # docs_tag: begin_init_imagebatchdecoder_nvimagecodec - self.logger = logging.getLogger(__name__) - self.batch_size = batch_size - self.input_path = input_path - self.device_id = device_id - self.total_decoded = 0 - self.batch_idx = 0 - self.cuda_ctx = cuda_ctx - self.cuda_stream = cuda_stream - self.cvcuda_perf = cvcuda_perf - self.decoder = nvimgcodec.Decoder(device_id=device_id) - - # docs_tag: begin_parse_imagebatchdecoder_nvimagecodec - if os.path.isfile(self.input_path): - if os.path.splitext(self.input_path)[1] == ".jpg": - # Read the input image file. - self.file_names = [self.input_path] * self.batch_size - # We will use the nvImageCodec based decoder on the GPU in case of images. - # This will be allocated once during the first run or whenever a batch - # size change happens. - else: - raise ValueError("Unable to read file %s as image." % self.input_path) - - elif os.path.isdir(self.input_path): - # It is a directory. Grab file names of all JPG images. - self.file_names = glob.glob(os.path.join(self.input_path, "*.jpg")) - self.logger.info("Found a total of %d JPEG images." % len(self.file_names)) - - else: - raise ValueError( - "Unknown expression given as input_path: %s." % self.input_path - ) - - # docs_tag: end_parse_imagebatchdecoder_nvimagecodec - - # docs_tag: begin_batch_imagebatchdecoder_nvimagecodec - self.file_name_batches = [ - self.file_names[i : i + self.batch_size] # noqa: E203 - for i in range(0, len(self.file_names), self.batch_size) - ] - # docs_tag: end_batch_imagebatchdecoder_nvimagecodec - - self.max_image_size = 1024 * 1024 * 3 # Maximum possible image size. - - self.logger.info( - "Using nvImageCodec decoder version: %s" % nvimgcodec.__version__ - ) - - # docs_tag: end_init_imagebatchdecoder_nvimagecodec - - def __call__(self): - if self.total_decoded == len(self.file_names): - return None - - # docs_tag: begin_call_imagebatchdecoder_nvimagecodec - self.cvcuda_perf.push_range("decoder.nvimagecodec") - - file_name_batch = self.file_name_batches[self.batch_idx] - - data_batch = [open(path, "rb").read() for path in file_name_batch] - - # docs_tag: begin_decode_imagebatchdecoder_nvimagecodec - - tensor_list = [] - image_list = self.decoder.decode(data_batch, cuda_stream=self.cuda_stream) - - # Convert the decoded images to cvcuda tensors in a list. - for i in range(len(image_list)): - tensor_list.append(cvcuda.as_tensor(image_list[i], "HWC")) - - # Stack the list of tensors to a single NHWC tensor. - cvcuda_decoded_tensor = cvcuda.stack(tensor_list) - self.total_decoded += len(tensor_list) - # docs_tag: end_decode_imagebatchdecoder_nvimagecodec - - # docs_tag: begin_return_imagebatchdecoder_nvimagecodec - batch = Batch( - batch_idx=self.batch_idx, - data=cvcuda_decoded_tensor, - fileinfo=file_name_batch, - ) - self.batch_idx += 1 - - # docs_tag: end_return_imagebatchdecoder_nvimagecodec - - self.cvcuda_perf.pop_range() - # docs_tag: end_call_imagebatchdecoder_nvimagecodec - return batch - - def start(self): - pass - - def join(self): - pass - - -# docs_tag: end_imagebatchdecoder_nvimagecodec - -# docs_tag: begin_imagebatchencoder_nvimagecodec -class ImageBatchEncoder: - def __init__( - self, - output_path, - device_id, - cvcuda_perf, - ): - # docs_tag: begin_init_imagebatchencoder_nvimagecodec - self.logger = logging.getLogger(__name__) - self.encoder = nvimgcodec.Encoder(device_id=device_id) - self.input_layout = "NHWC" - self.gpu_input = True - self.output_path = output_path - self.device_id = device_id - self.cvcuda_perf = cvcuda_perf - - self.logger.info( - "Using nvImageCodec encoder version: %s" % nvimgcodec.__version__ - ) - # docs_tag: end_init_init_imagebatchencoder_nvimagecodec - - # docs_tag: begin_call_imagebatchencoder_nvimagecodec - def __call__(self, batch): - self.cvcuda_perf.push_range("encoder.nvimagecodec") - - assert isinstance(batch.data, torch.Tensor) - - image_tensors_nhwc = batch.data - # Create an empty list to store filenames - filenames = [] - hwctensor_list = [] - # Iterate through each image to prepare the filenames - for img_idx in range(image_tensors_nhwc.shape[0]): - img_name = os.path.splitext(os.path.basename(batch.fileinfo[img_idx]))[0] - results_path = os.path.join(self.output_path, f"out_{img_name}.jpg") - self.logger.info(f"Preparing to save the image to: {results_path}") - # Add the filename to the list - filenames.append(results_path) - # Add the image tensor CAI to a CAI list from an NCHW tensor - # (this was a stacked tensor if N images) - hwctensor_list.append(image_tensors_nhwc[img_idx].cuda()) - - # Pass the image tensors and filenames to the encoder. - self.encoder.write(filenames, hwctensor_list) - self.cvcuda_perf.pop_range() - # docs_tag: end_call_imagebatchencoder_nvimagecodec - - def start(self): - pass - - def join(self): - pass - - -# docs_tag: end_imagebatchencoder_nvimagecodec diff --git a/bench/python/ops/bench_adaptivethreshold.py b/bench/python/ops/bench_adaptivethreshold.py new file mode 100755 index 000000000..c32bf53bb --- /dev/null +++ b/bench/python/ops/bench_adaptivethreshold.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""CV-CUDA AdaptiveThreshold operator benchmark - Python equivalent of BenchAdaptiveThreshold.cpp""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + get_input_kind, + parse_shape, + get_dtype, + get_dtype_size, + create_tensor, + create_image_batch_varshape, + create_stream_cache, + get_format_from_dtype, + run_benchmark, +) + + +def adaptivethreshold(state): + """AdaptiveThreshold operator benchmark matching C++ BenchAdaptiveThreshold.cpp""" + + N, H, W = parse_shape(state.get_string("shape")) + dtype = get_dtype(state.get_string("InOutDataType")) + device_id = state.get_device() + input_kind = get_input_kind(state.get_string("inputKind")) + try: + layout = state.get_string("layout") + except Exception: + layout = "NHWC" + if layout not in ("NHWC", "NCHW", "NCHW_FAKE"): + state.skip( + "AdaptiveThreshold benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts" + ) + return None + is_planar = layout == "NCHW" + is_fake_planar = layout == "NCHW_FAKE" + if (is_planar or is_fake_planar) and input_kind == "VarShape": + state.skip("Planar AdaptiveThreshold benchmark rows are tensor-only") + return None + # Adaptive threshold parameters (match C++ values) + maxValue = 123.0 + adaptiveMethod = cvcuda.AdaptiveThresholdType.GAUSSIAN_C + thresholdType = cvcuda.ThresholdType.BINARY + blockSize = int(state.get_int64("blockSize")) # Get from config like C++ + c = -2.3 + + # Report memory (match C++) + dtype_size = get_dtype_size(dtype) + bytes_ = N * H * W * dtype_size + if is_fake_planar: + state.add_global_memory_reads(3 * bytes_) + state.add_global_memory_writes(3 * bytes_) + else: + state.add_global_memory_reads(bytes_) + state.add_global_memory_writes(bytes_) + + get_stream = create_stream_cache() + + def do_adaptivethreshold(dst, src, stream): + cvcuda.adaptivethreshold_into( + dst, + src, + maxValue, + adaptiveMethod, + thresholdType, + blockSize, + c, + stream=stream, + ) + + if is_fake_planar: + src = create_tensor( + (N, 1, H, W), dtype, device_id, layout="NCHW", fill_mode="checkerboard" + ) + inter_src = create_tensor( + (N, H, W, 1), dtype, device_id, layout="NHWC", fill_mode=0 + ) + inter_dst = create_tensor( + (N, H, W, 1), dtype, device_id, layout="NHWC", fill_mode=0 + ) + dst = create_tensor((N, 1, H, W), dtype, device_id, layout="NCHW", fill_mode=0) + + def run_fake(launch): + stream = get_stream(launch) + cvcuda.reformat_into(inter_src, src, stream=stream) + do_adaptivethreshold(inter_dst, inter_src, stream) + cvcuda.reformat_into(dst, inter_dst, stream=stream) + + return run_fake + + if input_kind == "Tensor": # Tensor mode (matches C++ line 47) + tensor_shape = (N, 1, H, W) if is_planar else (N, H, W, 1) + src = create_tensor( + tensor_shape, dtype, device_id, layout=layout, fill_mode="checkerboard" + ) + dst = create_tensor(tensor_shape, dtype, device_id, layout=layout, fill_mode=0) + + def run(launch): + do_adaptivethreshold(dst, src, get_stream(launch)) + + return run + + else: # ImageBatchVarShape mode (matches C++ line 60) + # Determine image format (single channel grayscale) + dtype_str = state.get_string("InOutDataType") + img_format = get_format_from_dtype(dtype_str, 1) + + # Create src and dst batches independently using utility + src = create_image_batch_varshape( + (N, H, W, 1), + 0, + img_format, + dtype_str, + device_id, + fill_mode="checkerboard", + ) + dst = create_image_batch_varshape( + (N, H, W, 1), 0, img_format, dtype_str, device_id, fill_mode=0 + ) + + # Create parameter tensors (match C++ lines 69-75) + maxValue_tensor = create_tensor( + (N,), cvcuda.Type.F64, device_id, layout="N", fill_mode=maxValue + ) + blockSize_tensor = create_tensor( + (N,), cvcuda.Type.S32, device_id, layout="N", fill_mode=blockSize + ) + c_tensor = create_tensor( + (N,), cvcuda.Type.F64, device_id, layout="N", fill_mode=c + ) + + def run(launch): + cvcuda.adaptivethreshold_into( + dst, + src, + maxValue_tensor, + adaptiveMethod, + thresholdType, + blockSize, # max_block_size (int) + blockSize_tensor, # block_size tensor + c_tensor, + stream=get_stream(launch), + ) + + return run + + +if __name__ == "__main__": + run_benchmark("adaptivethreshold", adaptivethreshold) diff --git a/bench/python/ops/bench_adjustcontrast.py b/bench/python/ops/bench_adjustcontrast.py new file mode 100755 index 000000000..6c51e267f --- /dev/null +++ b/bench/python/ops/bench_adjustcontrast.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""CV-CUDA Adjust Contrast operator benchmark - Python equivalent of BenchAdjustContrast.cpp""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + get_input_kind, + parse_shape, + get_dtype, + get_dtype_size, + get_num_channels, + get_format_from_dtype, + create_tensor, + create_image_batch_varshape, + create_stream_cache, + run_benchmark, +) + +# Contrast factor is irrelevant to throughput; use a fixed value > 1 that clamps. +CONTRAST_FACTOR = 1.5 + + +def adjustcontrast(state): + """Adjust Contrast operator benchmark matching C++ BenchAdjustContrast.cpp""" + + N, H, W = parse_shape(state.get_string("shape")) + dtype_str = state.get_string("InOutDataType") + dtype = get_dtype(dtype_str) + num_channels = get_num_channels(dtype_str) + try: + layout = state.get_string("layout") + except Exception: + layout = "NHWC" + input_kind = get_input_kind(state.get_string("inputKind")) + device_id = state.get_device() + + if layout not in ("NHWC", "NCHW", "NCHW_FAKE"): + state.skip( + "AdjustContrast benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts" + ) + return None + + is_planar = layout == "NCHW" + is_fake_planar = layout == "NCHW_FAKE" + if is_fake_planar and input_kind == "VarShape": + state.skip("Fake-planar (NCHW_FAKE) adjust_contrast benchmark is tensor-only") + return None + + # AdjustContrast reads the image twice (mean reduction + blend) and writes it once. + dtype_size = get_dtype_size(dtype_str) + bytes_ = N * H * W * dtype_size + if is_fake_planar: + state.add_global_memory_reads(4 * bytes_) + state.add_global_memory_writes(3 * bytes_) + else: + state.add_global_memory_reads(2 * bytes_) + state.add_global_memory_writes(bytes_) + + get_stream = create_stream_cache() + + if is_fake_planar: + src = create_tensor( + (N, num_channels, H, W), + dtype, + device_id, + layout="NCHW", + fill_mode="checkerboard", + ) + inter_src = create_tensor( + (N, H, W, num_channels), dtype, device_id, layout="NHWC", fill_mode=0 + ) + inter_dst = create_tensor( + (N, H, W, num_channels), dtype, device_id, layout="NHWC", fill_mode=0 + ) + dst = create_tensor( + (N, num_channels, H, W), dtype, device_id, layout="NCHW", fill_mode=0 + ) + + def run_fake(launch): + stream = get_stream(launch) + cvcuda.reformat_into(inter_src, src, stream=stream) + cvcuda.adjust_contrast_into( + inter_dst, inter_src, CONTRAST_FACTOR, stream=stream + ) + cvcuda.reformat_into(dst, inter_dst, stream=stream) + + return run_fake + + if input_kind == "Tensor": + shape = (N, num_channels, H, W) if is_planar else (N, H, W, num_channels) + src = create_tensor( + shape, dtype, device_id, layout=layout, fill_mode="checkerboard" + ) + dst = create_tensor(shape, dtype, device_id, layout=layout, fill_mode=0) + else: # ImageBatchVarShape + img_format = get_format_from_dtype(dtype_str, num_channels, planar=is_planar) + src = create_image_batch_varshape( + (N, H, W, num_channels), + 0, + img_format, + dtype, + device_id, + fill_mode="checkerboard", + ) + dst = create_image_batch_varshape( + (N, H, W, num_channels), 0, img_format, dtype, device_id, fill_mode=0 + ) + + def run(launch): + cvcuda.adjust_contrast_into( + dst, src, CONTRAST_FACTOR, stream=get_stream(launch) + ) + + return run + + +if __name__ == "__main__": + run_benchmark("adjustcontrast", adjustcontrast) diff --git a/bench/python/ops/bench_adjusthue.py b/bench/python/ops/bench_adjusthue.py new file mode 100755 index 000000000..0ed754f6e --- /dev/null +++ b/bench/python/ops/bench_adjusthue.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""CV-CUDA Adjust Hue operator benchmark - Python equivalent of BenchAdjustHue.cpp""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + get_input_kind, + parse_shape, + get_dtype, + get_dtype_size, + get_num_channels, + get_format_from_dtype, + create_tensor, + create_image_batch_varshape, + create_stream_cache, + run_benchmark, +) + +# Hue factor is irrelevant to throughput; use a fixed mid-range value. +HUE = 0.25 + + +def adjusthue(state): + """Adjust Hue operator benchmark matching C++ BenchAdjustHue.cpp""" + + N, H, W = parse_shape(state.get_string("shape")) + dtype_str = state.get_string("InOutDataType") + dtype = get_dtype(dtype_str) + num_channels = get_num_channels(dtype_str) + try: + layout = state.get_string("layout") + except Exception: + layout = "NHWC" + input_kind = get_input_kind(state.get_string("inputKind")) + device_id = state.get_device() + + if layout not in ("NHWC", "NCHW", "NCHW_FAKE"): + state.skip( + "AdjustHue benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts" + ) + return None + + is_planar = layout == "NCHW" + is_fake_planar = layout == "NCHW_FAKE" + if is_fake_planar and input_kind == "VarShape": + state.skip("Fake-planar (NCHW_FAKE) adjusthue benchmark is tensor-only") + return None + + # AdjustHue preserves size. + dtype_size = get_dtype_size(dtype_str) + bytes_ = N * H * W * dtype_size + if is_fake_planar: + # reformat(NCHW->NHWC) + adjust + reformat(NHWC->NCHW) + state.add_global_memory_reads(3 * bytes_) + state.add_global_memory_writes(3 * bytes_) + else: + state.add_global_memory_reads(bytes_) + state.add_global_memory_writes(bytes_) + + get_stream = create_stream_cache() + + if is_fake_planar: + src = create_tensor( + (N, num_channels, H, W), + dtype, + device_id, + layout="NCHW", + fill_mode="checkerboard", + ) + inter_src = create_tensor( + (N, H, W, num_channels), dtype, device_id, layout="NHWC", fill_mode=0 + ) + inter_dst = create_tensor( + (N, H, W, num_channels), dtype, device_id, layout="NHWC", fill_mode=0 + ) + dst = create_tensor( + (N, num_channels, H, W), dtype, device_id, layout="NCHW", fill_mode=0 + ) + + def run_fake(launch): + stream = get_stream(launch) + cvcuda.reformat_into(inter_src, src, stream=stream) + cvcuda.adjust_hue_into(inter_dst, inter_src, HUE, stream=stream) + cvcuda.reformat_into(dst, inter_dst, stream=stream) + + return run_fake + + if input_kind == "Tensor": + shape = (N, num_channels, H, W) if is_planar else (N, H, W, num_channels) + src = create_tensor( + shape, dtype, device_id, layout=layout, fill_mode="checkerboard" + ) + dst = create_tensor(shape, dtype, device_id, layout=layout, fill_mode=0) + else: # ImageBatchVarShape + img_format = get_format_from_dtype(dtype_str, num_channels, planar=is_planar) + src = create_image_batch_varshape( + (N, H, W, num_channels), + 0, + img_format, + dtype, + device_id, + fill_mode="checkerboard", + ) + dst = create_image_batch_varshape( + (N, H, W, num_channels), 0, img_format, dtype, device_id, fill_mode=0 + ) + + def run(launch): + cvcuda.adjust_hue_into(dst, src, HUE, stream=get_stream(launch)) + + return run + + +if __name__ == "__main__": + run_benchmark("adjusthue", adjusthue) diff --git a/bench/python/ops/bench_adjustsaturation.py b/bench/python/ops/bench_adjustsaturation.py new file mode 100755 index 000000000..9356631f9 --- /dev/null +++ b/bench/python/ops/bench_adjustsaturation.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""CV-CUDA Adjust Saturation operator benchmark - Python equivalent of BenchAdjustSaturation.cpp""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + get_input_kind, + parse_shape, + get_dtype, + get_dtype_size, + get_num_channels, + get_format_from_dtype, + create_tensor, + create_image_batch_varshape, + create_stream_cache, + run_benchmark, +) + +# Saturation factor is irrelevant to throughput; use a fixed mid-range value. +SATURATION = 0.5 + + +def adjustsaturation(state): + """Adjust Saturation operator benchmark matching C++ BenchAdjustSaturation.cpp""" + + N, H, W = parse_shape(state.get_string("shape")) + dtype_str = state.get_string("InOutDataType") + dtype = get_dtype(dtype_str) + num_channels = get_num_channels(dtype_str) + try: + layout = state.get_string("layout") + except Exception: + layout = "NHWC" + input_kind = get_input_kind(state.get_string("inputKind")) + device_id = state.get_device() + + if layout not in ("NHWC", "NCHW", "NCHW_FAKE"): + state.skip( + "AdjustSaturation benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts" + ) + return None + + is_planar = layout == "NCHW" + is_fake_planar = layout == "NCHW_FAKE" + if is_fake_planar and input_kind == "VarShape": + state.skip("Fake-planar (NCHW_FAKE) adjustsaturation benchmark is tensor-only") + return None + + # AdjustSaturation preserves size. + dtype_size = get_dtype_size(dtype_str) + bytes_ = N * H * W * dtype_size + if is_fake_planar: + # reformat(NCHW->NHWC) + adjust + reformat(NHWC->NCHW) + state.add_global_memory_reads(3 * bytes_) + state.add_global_memory_writes(3 * bytes_) + else: + state.add_global_memory_reads(bytes_) + state.add_global_memory_writes(bytes_) + + get_stream = create_stream_cache() + + if is_fake_planar: + src = create_tensor( + (N, num_channels, H, W), + dtype, + device_id, + layout="NCHW", + fill_mode="checkerboard", + ) + inter_src = create_tensor( + (N, H, W, num_channels), dtype, device_id, layout="NHWC", fill_mode=0 + ) + inter_dst = create_tensor( + (N, H, W, num_channels), dtype, device_id, layout="NHWC", fill_mode=0 + ) + dst = create_tensor( + (N, num_channels, H, W), dtype, device_id, layout="NCHW", fill_mode=0 + ) + + def run_fake(launch): + stream = get_stream(launch) + cvcuda.reformat_into(inter_src, src, stream=stream) + cvcuda.adjust_saturation_into( + inter_dst, inter_src, SATURATION, stream=stream + ) + cvcuda.reformat_into(dst, inter_dst, stream=stream) + + return run_fake + + if input_kind == "Tensor": + shape = (N, num_channels, H, W) if is_planar else (N, H, W, num_channels) + src = create_tensor( + shape, dtype, device_id, layout=layout, fill_mode="checkerboard" + ) + dst = create_tensor(shape, dtype, device_id, layout=layout, fill_mode=0) + else: # ImageBatchVarShape + img_format = get_format_from_dtype(dtype_str, num_channels, planar=is_planar) + src = create_image_batch_varshape( + (N, H, W, num_channels), + 0, + img_format, + dtype, + device_id, + fill_mode="checkerboard", + ) + dst = create_image_batch_varshape( + (N, H, W, num_channels), 0, img_format, dtype, device_id, fill_mode=0 + ) + + def run(launch): + cvcuda.adjust_saturation_into(dst, src, SATURATION, stream=get_stream(launch)) + + return run + + +if __name__ == "__main__": + run_benchmark("adjustsaturation", adjustsaturation) diff --git a/bench/python/ops/bench_adjustsharpness.py b/bench/python/ops/bench_adjustsharpness.py new file mode 100755 index 000000000..9a028ec86 --- /dev/null +++ b/bench/python/ops/bench_adjustsharpness.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""CV-CUDA Adjust Sharpness operator benchmark - Python equivalent of BenchAdjustSharpness.cpp""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + get_input_kind, + parse_shape, + get_dtype, + get_dtype_size, + get_num_channels, + get_format_from_dtype, + create_tensor, + create_image_batch_varshape, + create_stream_cache, + run_benchmark, +) + +# Sharpness factor is irrelevant to throughput; use a fixed value that exercises the sharpen path. +SHARPNESS_FACTOR = 2.0 + + +def adjustsharpness(state): + """Adjust Sharpness operator benchmark matching C++ BenchAdjustSharpness.cpp""" + + N, H, W = parse_shape(state.get_string("shape")) + dtype_str = state.get_string("InOutDataType") + dtype = get_dtype(dtype_str) + num_channels = get_num_channels(dtype_str) + try: + layout = state.get_string("layout") + except Exception: + layout = "NHWC" + input_kind = get_input_kind(state.get_string("inputKind")) + device_id = state.get_device() + + if layout not in ("NHWC", "NCHW", "NCHW_FAKE"): + state.skip( + "AdjustSharpness benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts" + ) + return None + + is_planar = layout == "NCHW" + is_fake_planar = layout == "NCHW_FAKE" + if is_fake_planar and input_kind == "VarShape": + state.skip("Fake-planar (NCHW_FAKE) adjustsharpness benchmark is tensor-only") + return None + + # AdjustSharpness preserves size. + dtype_size = get_dtype_size(dtype_str) + bytes_ = N * H * W * dtype_size + if is_fake_planar: + # reformat(NCHW->NHWC) + adjustsharpness + reformat(NHWC->NCHW) + state.add_global_memory_reads(3 * bytes_) + state.add_global_memory_writes(3 * bytes_) + else: + state.add_global_memory_reads(bytes_) + state.add_global_memory_writes(bytes_) + + get_stream = create_stream_cache() + + if is_fake_planar: + src = create_tensor( + (N, num_channels, H, W), + dtype, + device_id, + layout="NCHW", + fill_mode="checkerboard", + ) + inter_src = create_tensor( + (N, H, W, num_channels), dtype, device_id, layout="NHWC", fill_mode=0 + ) + inter_dst = create_tensor( + (N, H, W, num_channels), dtype, device_id, layout="NHWC", fill_mode=0 + ) + dst = create_tensor( + (N, num_channels, H, W), dtype, device_id, layout="NCHW", fill_mode=0 + ) + + def run_fake(launch): + stream = get_stream(launch) + cvcuda.reformat_into(inter_src, src, stream=stream) + cvcuda.adjust_sharpness_into( + inter_dst, inter_src, SHARPNESS_FACTOR, stream=stream + ) + cvcuda.reformat_into(dst, inter_dst, stream=stream) + + return run_fake + + if input_kind == "Tensor": + shape = (N, num_channels, H, W) if is_planar else (N, H, W, num_channels) + src = create_tensor( + shape, dtype, device_id, layout=layout, fill_mode="checkerboard" + ) + dst = create_tensor(shape, dtype, device_id, layout=layout, fill_mode=0) + else: # ImageBatchVarShape + img_format = get_format_from_dtype(dtype_str, num_channels, planar=is_planar) + src = create_image_batch_varshape( + (N, H, W, num_channels), + 0, + img_format, + dtype, + device_id, + fill_mode="checkerboard", + ) + dst = create_image_batch_varshape( + (N, H, W, num_channels), 0, img_format, dtype, device_id, fill_mode=0 + ) + + def run(launch): + cvcuda.adjust_sharpness_into( + dst, src, SHARPNESS_FACTOR, stream=get_stream(launch) + ) + + return run + + +if __name__ == "__main__": + run_benchmark("adjustsharpness", adjustsharpness) diff --git a/bench/python/ops/bench_advcvtcolor.py b/bench/python/ops/bench_advcvtcolor.py new file mode 100755 index 000000000..9b18d8f59 --- /dev/null +++ b/bench/python/ops/bench_advcvtcolor.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""CV-CUDA AdvCvtColor operator benchmark - Python equivalent of BenchAdvCvtColor.cpp""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + get_input_kind, + parse_shape, + get_dtype, + get_num_channels, + get_dtype_size, + create_tensor, + create_stream_cache, + run_benchmark, +) + + +def advcvtcolor(state): + """AdvCvtColor operator benchmark matching C++ BenchAdvCvtColor.cpp""" + + N, H, W = parse_shape(state.get_string("shape")) + dtype_str = state.get_string("InOutDataType") + dtype = get_dtype(dtype_str) + rgb_channels = get_num_channels(dtype_str) + code_str = state.get_string("code") + input_kind = get_input_kind(state.get_string("inputKind")) + try: + layout = state.get_string("layout") + except (KeyError, RuntimeError): + layout = "NHWC" + + if layout not in ("NHWC", "NCHW", "NCHW_FAKE"): + state.skip( + "AdvCvtColor benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts" + ) + return None + + is_planar = layout == "NCHW" + is_fake_planar = layout == "NCHW_FAKE" + + if input_kind == "VarShape": # ImageBatchVarShape mode + state.skip("ImageBatchVarShape not implemented for this benchmark") + return None + + device_id = state.get_device() + + code_map = { + "BGR2YUV": (cvcuda.ColorConversion.BGR2YUV, "444"), + "RGB2YUV": (cvcuda.ColorConversion.RGB2YUV, "444"), + "YUV2BGR": (cvcuda.ColorConversion.YUV2BGR, "444"), + "YUV2RGB": (cvcuda.ColorConversion.YUV2RGB, "444"), + "RGB2YUV_NV12": (cvcuda.ColorConversion.RGB2YUV_NV12, "rgb_to_nv"), + "BGR2YUV_NV21": (cvcuda.ColorConversion.BGR2YUV_NV21, "rgb_to_nv"), + "YUV2RGB_NV12": (cvcuda.ColorConversion.YUV2RGB_NV12, "nv_to_rgb"), + "YUV2BGR_NV21": (cvcuda.ColorConversion.YUV2BGR_NV21, "nv_to_rgb"), + } + code, conversion_shape = code_map[code_str] + spec = cvcuda.ColorSpec.BT2020 + + if dtype != cvcuda.Type.U8: + state.skip("AdvCvtColor benchmark supports uint8 vector types only") + return None + + src_h = H + dst_h = H + src_c = rgb_channels + dst_c = rgb_channels + + if conversion_shape == "444": + if rgb_channels != 3: + state.skip("Interleaved 444 conversion requires uchar3") + return None + src_c = 3 + dst_c = 3 + elif conversion_shape == "rgb_to_nv": + if rgb_channels not in (3, 4): + state.skip("RGB/BGR to NV conversion requires uchar3 or uchar4") + return None + if H % 2 or W % 2: + state.skip("NV conversion requires even height and width") + return None + dst_h = (H * 3) // 2 + dst_c = 1 + else: + if rgb_channels not in (3, 4): + state.skip("NV to RGB/BGR conversion requires uchar3 or uchar4") + return None + if H % 2 or W % 2: + state.skip("NV conversion requires even height and width") + return None + src_h = (H * 3) // 2 + src_c = 1 + + dtype_size = get_dtype_size(dtype) + src_bytes = N * src_h * W * src_c * dtype_size + dst_bytes = N * dst_h * W * dst_c * dtype_size + if is_fake_planar: + state.add_global_memory_reads(2 * src_bytes + dst_bytes) + state.add_global_memory_writes(src_bytes + 2 * dst_bytes) + else: + state.add_global_memory_reads(src_bytes) + state.add_global_memory_writes(dst_bytes) + + get_stream = create_stream_cache() + + def tensor_shape(height, channels, tensor_layout): + if tensor_layout == "NCHW": + return (N, channels, height, W) + return (N, height, W, channels) + + if is_fake_planar: + src = create_tensor( + tensor_shape(src_h, src_c, "NCHW"), + dtype, + device_id, + layout="NCHW", + fill_mode="checkerboard", + ) + inter_src = create_tensor( + tensor_shape(src_h, src_c, "NHWC"), + dtype, + device_id, + layout="NHWC", + fill_mode=0, + ) + inter_dst = create_tensor( + tensor_shape(dst_h, dst_c, "NHWC"), + dtype, + device_id, + layout="NHWC", + fill_mode=0, + ) + dst = create_tensor( + tensor_shape(dst_h, dst_c, "NCHW"), + dtype, + device_id, + layout="NCHW", + fill_mode=0, + ) + + def run_fake(launch): + stream = get_stream(launch) + cvcuda.reformat_into(inter_src, src, stream=stream) + cvcuda.advcvtcolor_into(inter_dst, inter_src, code, spec, stream=stream) + cvcuda.reformat_into(dst, inter_dst, stream=stream) + + return run_fake + + tensor_layout = "NCHW" if is_planar else "NHWC" + src = create_tensor( + tensor_shape(src_h, src_c, tensor_layout), + dtype, + device_id, + layout=tensor_layout, + fill_mode="checkerboard", + ) + dst = create_tensor( + tensor_shape(dst_h, dst_c, tensor_layout), + dtype, + device_id, + layout=tensor_layout, + fill_mode=0, + ) + + def run(launch): + cvcuda.advcvtcolor_into(dst, src, code, spec, stream=get_stream(launch)) + + return run + + +if __name__ == "__main__": + run_benchmark("advcvtcolor", advcvtcolor) diff --git a/bench/python/ops/bench_autocontrast.py b/bench/python/ops/bench_autocontrast.py new file mode 100755 index 000000000..42466462c --- /dev/null +++ b/bench/python/ops/bench_autocontrast.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""CV-CUDA Auto Contrast operator benchmark - Python equivalent of BenchAutoContrast.cpp""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + get_input_kind, + parse_shape, + get_dtype, + get_num_channels, + get_dtype_size, + get_format_from_dtype, + create_tensor, + create_image_batch_varshape, + create_stream_cache, + run_benchmark, +) + + +def autocontrast(state): + """Auto Contrast operator benchmark matching C++ BenchAutoContrast.cpp""" + + N, H, W = parse_shape(state.get_string("shape")) + dtype_str = state.get_string("InOutDataType") + dtype = get_dtype(dtype_str) + num_channels = get_num_channels(dtype_str) + input_kind = get_input_kind(state.get_string("inputKind")) + try: + layout = state.get_string("layout") + except (KeyError, RuntimeError): + layout = "NHWC" + device_id = state.get_device() + + if layout not in ("NHWC", "NCHW", "NCHW_FAKE"): + state.skip( + "AutoContrast benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts" + ) + return None + + is_planar = layout == "NCHW" + is_fake_planar = layout == "NCHW_FAKE" + if is_fake_planar and input_kind == "VarShape": + state.skip("Fake-planar (NCHW_FAKE) AutoContrast benchmark is tensor-only") + return None + if is_planar and input_kind == "VarShape" and dtype_str == "uchar4": + state.skip("uchar4 planar ImageBatchVarShape is not supported") + return None + + dtype_size = get_dtype_size(dtype_str) + bytes_ = N * H * W * dtype_size + if is_fake_planar: + state.add_global_memory_reads(3 * bytes_) + state.add_global_memory_writes(3 * bytes_) + else: + state.add_global_memory_reads(bytes_) + state.add_global_memory_writes(bytes_) + + get_stream = create_stream_cache() + + if is_fake_planar: + src = create_tensor( + (N, num_channels, H, W), + dtype, + device_id, + layout="NCHW", + fill_mode="checkerboard", + ) + inter_src = create_tensor( + (N, H, W, num_channels), dtype, device_id, layout="NHWC", fill_mode=0 + ) + inter_dst = create_tensor( + (N, H, W, num_channels), dtype, device_id, layout="NHWC", fill_mode=0 + ) + dst = create_tensor( + (N, num_channels, H, W), dtype, device_id, layout="NCHW", fill_mode=0 + ) + + def run_fake(launch): + stream = get_stream(launch) + cvcuda.reformat_into(inter_src, src, stream=stream) + cvcuda.autocontrast_into(inter_dst, inter_src, stream=stream) + cvcuda.reformat_into(dst, inter_dst, stream=stream) + + return run_fake + + if input_kind == "Tensor": + tshape = (N, num_channels, H, W) if is_planar else (N, H, W, num_channels) + src = create_tensor( + tshape, dtype, device_id, layout=layout, fill_mode="checkerboard" + ) + dst = create_tensor(tshape, dtype, device_id, layout=layout, fill_mode=0) + else: # ImageBatchVarShape + img_format = get_format_from_dtype(dtype_str, num_channels, planar=is_planar) + src_batch = cvcuda.ImageBatchVarShape(N) + dst_batch = cvcuda.ImageBatchVarShape(N) + src = create_image_batch_varshape( + (N, H, W, num_channels), + 0, + img_format, + dtype, + device_id, + fill_mode="checkerboard", + batch=src_batch, + ) + dst = create_image_batch_varshape( + (N, H, W, num_channels), + 0, + img_format, + dtype, + device_id, + fill_mode=0, + batch=dst_batch, + ) + + def run(launch): + cvcuda.autocontrast_into(dst, src, stream=get_stream(launch)) + + return run + + +if __name__ == "__main__": + run_benchmark("autocontrast", autocontrast) diff --git a/bench/python/ops/bench_averageblur.py b/bench/python/ops/bench_averageblur.py new file mode 100755 index 000000000..681d003cc --- /dev/null +++ b/bench/python/ops/bench_averageblur.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""CV-CUDA AverageBlur operator benchmark - Python equivalent of BenchAverageBlur.cpp""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + get_input_kind, + parse_shape, + get_dtype, + get_num_channels, + get_dtype_size, + get_format_from_dtype, + get_border_type, + create_tensor, + create_image_batch_varshape, + create_stream_cache, + run_benchmark, +) + + +def averageblur(state): + """AverageBlur operator benchmark matching C++ BenchAverageBlur.cpp""" + + N, H, W = parse_shape(state.get_string("shape")) + dtype_str = state.get_string("InOutDataType") + dtype = get_dtype(dtype_str) + num_channels = get_num_channels(dtype_str) + kernel_size = parse_shape(state.get_string("kernelSize")) + border = get_border_type(state.get_string("border")) + input_kind = get_input_kind(state.get_string("inputKind")) + try: + layout = state.get_string("layout") + except Exception: + layout = "NHWC" + device_id = state.get_device() + + if layout not in ("NHWC", "NCHW", "NCHW_FAKE"): + state.skip( + "AverageBlur benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts" + ) + return None + is_planar = layout == "NCHW" + is_fake_planar = layout == "NCHW_FAKE" + if is_fake_planar and input_kind == "VarShape": + state.skip("Fake-planar (NCHW_FAKE) AverageBlur benchmark is tensor-only") + return None + + # Report memory (match C++) + dtype_size = get_dtype_size(dtype_str) + state.add_global_memory_reads(N * H * W * dtype_size) + state.add_global_memory_writes(N * H * W * dtype_size) + + get_stream = create_stream_cache() + + def do_averageblur(dst, src, stream): + cvcuda.averageblur_into( + dst, + src, + kernel_size, + kernel_anchor=(-1, -1), + border=border, + stream=stream, + ) + + if is_fake_planar: + src = create_tensor( + (N, num_channels, H, W), + dtype, + device_id, + layout="NCHW", + fill_mode="checkerboard", + ) + inter_src = create_tensor( + (N, H, W, num_channels), + dtype, + device_id, + layout="NHWC", + fill_mode=0, + ) + inter_dst = create_tensor( + (N, H, W, num_channels), + dtype, + device_id, + layout="NHWC", + fill_mode=0, + ) + dst = create_tensor( + (N, num_channels, H, W), + dtype, + device_id, + layout="NCHW", + fill_mode=0, + ) + + def run_fake(launch): + stream = get_stream(launch) + cvcuda.reformat_into(inter_src, src, stream=stream) + do_averageblur(inter_dst, inter_src, stream) + cvcuda.reformat_into(dst, inter_dst, stream=stream) + + return run_fake + + if input_kind == "Tensor": # Tensor mode + tensor_shape = (N, num_channels, H, W) if is_planar else (N, H, W, num_channels) + src = create_tensor( + tensor_shape, + dtype, + device_id, + layout=layout, + fill_mode="checkerboard", + ) + dst = create_tensor( + tensor_shape, + dtype, + device_id, + layout=layout, + fill_mode=0, + ) + + def run(launch): + do_averageblur(dst, src, get_stream(launch)) + + return run + + else: # ImageBatchVarShape mode (inputKind=VarShape) + img_format = get_format_from_dtype(dtype_str, num_channels, planar=is_planar) + src = create_image_batch_varshape( + (N, H, W, num_channels), + 0, + img_format, + dtype, + device_id, + fill_mode="checkerboard", + ) + dst = create_image_batch_varshape( + (N, H, W, num_channels), + 0, + img_format, + dtype, + device_id, + fill_mode=0, + ) + kernel_size_tensor = create_tensor( + (N, 2), + cvcuda.Type.S32, + device_id, + layout="NC", + fill_mode=[kernel_size[0], kernel_size[1]], + ) + kernel_anchor_tensor = create_tensor( + (N, 2), cvcuda.Type.S32, device_id, layout="NC", fill_mode=[-1, -1] + ) + + def run(launch): + cvcuda.averageblur_into( + dst, + src, + kernel_size, + kernel_size_tensor, + kernel_anchor_tensor, + border=border, + stream=get_stream(launch), + ) + + return run + + +if __name__ == "__main__": + run_benchmark("averageblur", averageblur) diff --git a/bench/python/ops/bench_bilateralfilter.py b/bench/python/ops/bench_bilateralfilter.py new file mode 100755 index 000000000..3f2e9a994 --- /dev/null +++ b/bench/python/ops/bench_bilateralfilter.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""CV-CUDA BilateralFilter operator benchmark - Python equivalent of BenchBilateralFilter.cpp""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + get_input_kind, + parse_shape, + get_dtype, + get_num_channels, + get_dtype_size, + get_format_from_dtype, + get_border_type, + create_tensor, + create_image_batch_varshape, + create_stream_cache, + run_benchmark, +) + + +def bilateralfilter(state): + """BilateralFilter operator benchmark matching C++ BenchBilateralFilter.cpp""" + + N, H, W = parse_shape(state.get_string("shape")) + dtype_str = state.get_string("InOutDataType") + dtype = get_dtype(dtype_str) + num_channels = get_num_channels(dtype_str) + border = get_border_type(state.get_string("border")) + input_kind = get_input_kind(state.get_string("inputKind")) + try: + layout = state.get_string("layout") + except KeyError: + layout = "NHWC" + device_id = state.get_device() + + if layout not in ("NHWC", "NCHW", "NCHW_FAKE"): + state.skip( + "BilateralFilter benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts" + ) + return None + is_planar = layout == "NCHW" + is_fake_planar = layout == "NCHW_FAKE" + if is_fake_planar and input_kind == "VarShape": + state.skip("Fake-planar (NCHW_FAKE) BilateralFilter benchmark is tensor-only") + return None + + # Bilateral filter parameters (match C++ lines 32-34) + diameter_scalar = int(state.get_int64("diameter")) # -1 from config + sigma_space_scalar = float(state.get_float64("sigmaSpace")) # 1.2 from config + sigma_color_scalar = -1.0 # hardcoded like C++ + + # Report memory (match C++) + dtype_size = get_dtype_size(dtype_str) + state.add_global_memory_reads(N * H * W * dtype_size) + state.add_global_memory_writes(N * H * W * dtype_size) + + get_stream = create_stream_cache() + + def do_bilateralfilter(dst, src, diameter, sigma_color, sigma_space, stream): + cvcuda.bilateral_filter_into( + dst, + src, + diameter, + sigma_color, + sigma_space, + border=border, + stream=stream, + ) + + if is_fake_planar: + src = create_tensor( + (N, num_channels, H, W), + dtype, + device_id, + layout="NCHW", + fill_mode="lcg", + ) + inter_src = create_tensor( + (N, H, W, num_channels), + dtype, + device_id, + layout="NHWC", + fill_mode=0, + ) + inter_dst = create_tensor( + (N, H, W, num_channels), + dtype, + device_id, + layout="NHWC", + fill_mode=0, + ) + dst = create_tensor( + (N, num_channels, H, W), + dtype, + device_id, + layout="NCHW", + fill_mode=0, + ) + + def run_fake(launch): + stream = get_stream(launch) + cvcuda.reformat_into(inter_src, src, stream=stream) + do_bilateralfilter( + inter_dst, + inter_src, + diameter_scalar, + sigma_color_scalar, + sigma_space_scalar, + stream, + ) + cvcuda.reformat_into(dst, inter_dst, stream=stream) + + return run_fake + + if input_kind == "Tensor": # Tensor mode + diameter = diameter_scalar + sigma_space = sigma_space_scalar + sigma_color = sigma_color_scalar + tensor_shape = (N, num_channels, H, W) if is_planar else (N, H, W, num_channels) + src = create_tensor( + tensor_shape, + dtype, + device_id, + layout=layout, + fill_mode="lcg", + ) + dst = create_tensor( + tensor_shape, + dtype, + device_id, + layout=layout, + fill_mode=0, + ) + else: # ImageBatchVarShape mode + diameter = create_tensor( + (N,), cvcuda.Type.S32, device_id, layout="N", fill_mode=diameter_scalar + ) + sigma_space = create_tensor( + (N,), cvcuda.Type.F32, device_id, layout="N", fill_mode=sigma_space_scalar + ) + sigma_color = create_tensor( + (N,), cvcuda.Type.F32, device_id, layout="N", fill_mode=sigma_color_scalar + ) + img_format = get_format_from_dtype(dtype_str, num_channels, planar=is_planar) + src = create_image_batch_varshape( + (N, H, W, num_channels), + 0, + img_format, + dtype, + device_id, + fill_mode="lcg", + ) + dst = create_image_batch_varshape( + (N, H, W, num_channels), + 0, + img_format, + dtype, + device_id, + fill_mode=0, + ) + + def run(launch): + do_bilateralfilter( + dst, + src, + diameter, + sigma_color, + sigma_space, + get_stream(launch), + ) + + return run + + +if __name__ == "__main__": + run_benchmark("bilateralfilter", bilateralfilter) diff --git a/bench/python/ops/bench_bndbox.py b/bench/python/ops/bench_bndbox.py new file mode 100644 index 000000000..adfa65dc6 --- /dev/null +++ b/bench/python/ops/bench_bndbox.py @@ -0,0 +1,123 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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 sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + get_input_kind, + parse_shape, + get_dtype, + get_dtype_size, + get_num_channels, + create_tensor, + create_stream_cache, + run_benchmark, +) + + +def bndbox(state): + """BndBox operator benchmark matching C++ BenchBndBox.cpp""" + + N, H, W = parse_shape(state.get_string("shape")) + num_boxes = state.get_int64("numBoxes") + dtype_str = state.get_string("InOutDataType") + dtype = get_dtype(dtype_str) + input_kind = get_input_kind(state.get_string("inputKind")) + try: + layout = state.get_string("layout") + except (KeyError, RuntimeError): + layout = "NHWC" + + if input_kind == "VarShape": # ImageBatchVarShape mode + state.skip("ImageBatchVarShape not implemented for this benchmark") + return None + + device_id = state.get_device() + + ch = get_num_channels(dtype_str) + + if dtype_str not in ("uchar3", "uchar4"): + state.skip(f"Unsupported dtype for BndBox: {dtype_str}") + return None + if layout not in ("NHWC", "NCHW", "NCHW_FAKE"): + state.skip("BndBox benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts") + return None + + is_planar = layout == "NCHW" + is_fake_planar = layout == "NCHW_FAKE" + if (is_planar or is_fake_planar) and input_kind != "Tensor": + state.skip("Planar BndBox benchmark is tensor-only") + return None + + image_bytes = N * H * W * get_dtype_size(dtype_str) + sizeof_bndbox = 28 # Matches NVCVBndBoxI: NVCVBoxI + thickness + border/fill colors + box_bytes = N * num_boxes * sizeof_bndbox + if is_planar or is_fake_planar: + state.add_global_memory_reads(3 * image_bytes + box_bytes) + state.add_global_memory_writes(3 * image_bytes) + else: + state.add_global_memory_reads(image_bytes + box_bytes) + state.add_global_memory_writes(image_bytes) + + box = cvcuda.BndBoxI( + box=(43, 21, 12, 34), + thickness=2, + borderColor=(0, 0, 0, 255), + fillColor=(0, 0, 0, 0), + ) + + boxes_batch = [[box] * num_boxes for _ in range(N)] + bboxes = cvcuda.BndBoxesI(boxes_batch) + + get_stream = create_stream_cache() + + if is_fake_planar: + src = create_tensor( + (N, ch, H, W), dtype, device_id, layout="NCHW", fill_mode="checkerboard" + ) + inter_src = create_tensor( + (N, H, W, ch), dtype, device_id, layout="NHWC", fill_mode=0 + ) + inter_dst = create_tensor( + (N, H, W, ch), dtype, device_id, layout="NHWC", fill_mode=0 + ) + dst = create_tensor((N, ch, H, W), dtype, device_id, layout="NCHW", fill_mode=0) + + def run_fake(launch): + stream = get_stream(launch) + cvcuda.reformat_into(inter_src, src, stream=stream) + cvcuda.bndbox_into(inter_dst, inter_src, bboxes, stream=stream) + cvcuda.reformat_into(dst, inter_dst, stream=stream) + + return run_fake + + tensor_shape = (N, ch, H, W) if is_planar else (N, H, W, ch) + src = create_tensor( + tensor_shape, dtype, device_id, layout=layout, fill_mode="checkerboard" + ) + dst = create_tensor(tensor_shape, dtype, device_id, layout=layout, fill_mode=0) + + def run(launch): + cvcuda.bndbox_into(dst, src, bboxes, stream=get_stream(launch)) + + return run + + +if __name__ == "__main__": + run_benchmark("bndbox", bndbox) diff --git a/bench/python/ops/bench_boxblur.py b/bench/python/ops/bench_boxblur.py new file mode 100644 index 000000000..166ba4de1 --- /dev/null +++ b/bench/python/ops/bench_boxblur.py @@ -0,0 +1,137 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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 sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + get_input_kind, + parse_shape, + get_num_channels, + create_tensor, + create_stream_cache, + run_benchmark, +) + + +def boxblur(state): + """BoxBlur operator benchmark matching C++ BenchBoxBlur.cpp""" + + N, H, W = parse_shape(state.get_string("shape")) + num_boxes = state.get_int64("numBoxes") + kernel_size = state.get_int64("kernelSize") + box_width, box_height = parse_shape(state.get_string("boxSize")) + box_pattern = state.get_string("boxPattern") + dtype_str = state.get_string("InOutDataType") + input_kind = get_input_kind(state.get_string("inputKind")) + try: + layout = state.get_string("layout") + except Exception: + layout = "NHWC" + + if layout not in ("NHWC", "NCHW", "NCHW_FAKE"): + state.skip("BoxBlur benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts") + return None + + is_planar = layout == "NCHW" + is_fake_planar = layout == "NCHW_FAKE" + + if input_kind == "VarShape": # ImageBatchVarShape mode + state.skip("ImageBatchVarShape not implemented for this benchmark") + return None + + device_id = state.get_device() + + ch = get_num_channels(dtype_str) + + dtype_size = 1 # uint8 + sizeof_blurbox = 20 # Approximate size of NVCVBlurBoxI struct + state.add_global_memory_reads( + N * H * W * ch * dtype_size + N * num_boxes * sizeof_blurbox + ) + state.add_global_memory_writes(N * H * W * ch * dtype_size) + + box_width = max(3, min(box_width, W)) + box_height = max(3, min(box_height, H)) + + def make_blur_box(box_idx): + x = 43 + y = 21 + if box_pattern == "grid": + grid_cols = 1 + while grid_cols * grid_cols < num_boxes: + grid_cols += 1 + grid_rows = (num_boxes + grid_cols - 1) // grid_cols + col = box_idx % grid_cols + row = box_idx // grid_cols + max_x = max(0, W - box_width) + max_y = max(0, H - box_height) + x = max_x // 2 if grid_cols <= 1 else col * max_x // (grid_cols - 1) + y = max_y // 2 if grid_rows <= 1 else row * max_y // (grid_rows - 1) + elif box_pattern != "fixed": + raise ValueError(f"Unexpected boxPattern = {box_pattern}") + + return cvcuda.BlurBoxI( + box=(x, y, box_width, box_height), kernelSize=kernel_size + ) + + boxes_batch = [[make_blur_box(i) for i in range(num_boxes)] for _ in range(N)] + blur_boxes = cvcuda.BlurBoxesI(boxes_batch) + + get_stream = create_stream_cache() + + if is_fake_planar: + src = create_tensor( + (N, ch, H, W), + "uint8", + device_id, + layout="NCHW", + fill_mode="checkerboard", + ) + inter_src = create_tensor( + (N, H, W, ch), "uint8", device_id, layout="NHWC", fill_mode=0 + ) + inter_dst = create_tensor( + (N, H, W, ch), "uint8", device_id, layout="NHWC", fill_mode=0 + ) + dst = create_tensor( + (N, ch, H, W), "uint8", device_id, layout="NCHW", fill_mode=0 + ) + + def run_fake(launch): + stream = get_stream(launch) + cvcuda.reformat_into(inter_src, src, stream=stream) + cvcuda.boxblur_into(inter_dst, inter_src, blur_boxes, stream=stream) + cvcuda.reformat_into(dst, inter_dst, stream=stream) + + return run_fake + + tensor_shape = (N, ch, H, W) if is_planar else (N, H, W, ch) + src = create_tensor( + tensor_shape, "uint8", device_id, layout=layout, fill_mode="checkerboard" + ) + dst = create_tensor(tensor_shape, "uint8", device_id, layout=layout, fill_mode=0) + + def run(launch): + cvcuda.boxblur_into(dst, src, blur_boxes, stream=get_stream(launch)) + + return run + + +if __name__ == "__main__": + run_benchmark("boxblur", boxblur) diff --git a/bench/python/ops/bench_brightnesscontrast.py b/bench/python/ops/bench_brightnesscontrast.py new file mode 100755 index 000000000..a26ccdf72 --- /dev/null +++ b/bench/python/ops/bench_brightnesscontrast.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""CV-CUDA BrightnessContrast operator benchmark - Python equivalent of BenchBrightnessContrast.cpp""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + get_input_kind, + parse_shape, + get_dtype, + get_num_channels, + get_dtype_size, + get_format_from_dtype, + create_tensor, + create_image_batch_varshape, + create_stream_cache, + run_benchmark, +) + + +def brightnesscontrast(state): + """BrightnessContrast operator benchmark matching C++ BenchBrightnessContrast.cpp""" + + N, H, W = parse_shape(state.get_string("shape")) + dtype_str = state.get_string("InOutDataType") + dtype = get_dtype(dtype_str) + num_channels = get_num_channels(dtype_str) + input_kind = get_input_kind(state.get_string("inputKind")) + try: + layout = state.get_string("layout") + except (KeyError, RuntimeError): + layout = "NHWC" + device_id = state.get_device() + + if layout not in ("NHWC", "NCHW", "NCHW_FAKE"): + state.skip( + "BrightnessContrast benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts" + ) + return None + + is_planar = layout == "NCHW" + is_fake_planar = layout == "NCHW_FAKE" + if is_fake_planar and input_kind == "VarShape": + state.skip( + "Fake-planar (NCHW_FAKE) BrightnessContrast benchmark is tensor-only" + ) + return None + if is_planar and input_kind == "VarShape" and dtype_str == "uchar4": + state.skip("uchar4 planar ImageBatchVarShape is not supported") + return None + + dtype_size = get_dtype_size(dtype_str) + bytes_ = N * H * W * dtype_size + arg_bytes = N * 4 * 4 + if is_fake_planar: + state.add_global_memory_reads(3 * bytes_ + arg_bytes) + state.add_global_memory_writes(3 * bytes_) + else: + state.add_global_memory_reads(bytes_ + arg_bytes) + state.add_global_memory_writes(bytes_) + + brightness = create_tensor( + (N,), cvcuda.Type.F32, device_id, layout="N", fill_mode="lcg" + ) + contrast = create_tensor( + (N,), cvcuda.Type.F32, device_id, layout="N", fill_mode="lcg" + ) + brightness_shift = create_tensor( + (N,), cvcuda.Type.F32, device_id, layout="N", fill_mode="lcg" + ) + contrast_center = create_tensor( + (N,), cvcuda.Type.F32, device_id, layout="N", fill_mode="lcg" + ) + + get_stream = create_stream_cache() + + if is_fake_planar: + src = create_tensor( + (N, num_channels, H, W), + dtype, + device_id, + layout="NCHW", + fill_mode="checkerboard", + ) + inter_src = create_tensor( + (N, H, W, num_channels), dtype, device_id, layout="NHWC", fill_mode=0 + ) + inter_dst = create_tensor( + (N, H, W, num_channels), dtype, device_id, layout="NHWC", fill_mode=0 + ) + dst = create_tensor( + (N, num_channels, H, W), dtype, device_id, layout="NCHW", fill_mode=0 + ) + + def run_fake(launch): + stream = get_stream(launch) + cvcuda.reformat_into(inter_src, src, stream=stream) + cvcuda.brightness_contrast_into( + inter_dst, + inter_src, + brightness, + contrast, + brightness_shift=brightness_shift, + contrast_center=contrast_center, + stream=stream, + ) + cvcuda.reformat_into(dst, inter_dst, stream=stream) + + return run_fake + + if input_kind == "Tensor": # Tensor mode + tshape = (N, num_channels, H, W) if is_planar else (N, H, W, num_channels) + src = create_tensor( + tshape, + dtype, + device_id, + layout=layout, + fill_mode="checkerboard", + ) + dst = create_tensor(tshape, dtype, device_id, layout=layout, fill_mode=0) + else: # ImageBatchVarShape mode + img_format = get_format_from_dtype(dtype_str, num_channels, planar=is_planar) + src = create_image_batch_varshape( + (N, H, W, num_channels), + 0, + img_format, + dtype, + device_id, + fill_mode="checkerboard", + ) + dst = create_image_batch_varshape( + (N, H, W, num_channels), + 0, + img_format, + dtype, + device_id, + fill_mode=0, + ) + + def run(launch): + cvcuda.brightness_contrast_into( + dst, + src, + brightness, + contrast, + brightness_shift=brightness_shift, + contrast_center=contrast_center, + stream=get_stream(launch), + ) + + return run + + +if __name__ == "__main__": + run_benchmark("brightnesscontrast", brightnesscontrast) diff --git a/bench/python/ops/bench_centercrop.py b/bench/python/ops/bench_centercrop.py new file mode 100755 index 000000000..b34b03806 --- /dev/null +++ b/bench/python/ops/bench_centercrop.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""CV-CUDA CenterCrop operator benchmark - Python equivalent of BenchCenterCrop.cpp""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + get_input_kind, + parse_shape, + get_dtype, + get_num_channels, + get_dtype_size, + create_tensor, + create_stream_cache, + run_benchmark, +) + + +def centercrop(state): + """CenterCrop operator benchmark matching C++ BenchCenterCrop.cpp""" + + N, H, W = parse_shape(state.get_string("shape")) + dtype_str = state.get_string("InOutDataType") + dtype = get_dtype(dtype_str) + num_channels = get_num_channels(dtype_str) + crop_type = state.get_string("cropType") + input_kind = get_input_kind(state.get_string("inputKind")) + try: + layout = state.get_string("layout") + except (KeyError, RuntimeError): + layout = "NHWC" + + if layout not in ("NHWC", "NCHW", "NCHW_FAKE"): + state.skip( + "CenterCrop benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts" + ) + return None + + is_planar = layout == "NCHW" + is_fake_planar = layout == "NCHW_FAKE" + + # CenterCrop is tensor-only; the native and fake planar paths are too. + if input_kind == "VarShape": # ImageBatchVarShape mode + state.skip("ImageBatchVarShape not implemented for this benchmark") + return None + + device_id = state.get_device() + + if crop_type == "SAME": + crop_h, crop_w = H, W + elif crop_type == "QUARTER": + crop_h, crop_w = H // 2, W // 2 + else: + state.skip(f"Invalid cropType: {crop_type}") + return None + + dtype_size = get_dtype_size(dtype_str) + full_bytes = N * H * W * dtype_size + crop_bytes = N * crop_h * crop_w * dtype_size + if is_fake_planar: + # reformat(NCHW->NHWC) + crop + reformat(NHWC->NCHW) + state.add_global_memory_reads(full_bytes + 2 * crop_bytes) + state.add_global_memory_writes(full_bytes + 2 * crop_bytes) + else: + state.add_global_memory_reads(crop_bytes) + state.add_global_memory_writes(crop_bytes) + + get_stream = create_stream_cache() + + if is_fake_planar: + # Tensor-only "fake planar": planar->interleaved->crop->interleaved->planar, + # all timed, as the comparison baseline for the native planar (NCHW) path. + src = create_tensor( + (N, num_channels, H, W), + dtype, + device_id, + layout="NCHW", + fill_mode="checkerboard", + ) + inter_src = create_tensor( + (N, H, W, num_channels), dtype, device_id, layout="NHWC", fill_mode=0 + ) + inter_dst = create_tensor( + (N, crop_h, crop_w, num_channels), + dtype, + device_id, + layout="NHWC", + fill_mode=0, + ) + dst = create_tensor( + (N, num_channels, crop_h, crop_w), + dtype, + device_id, + layout="NCHW", + fill_mode=0, + ) + + def run_fake(launch): + stream = get_stream(launch) + cvcuda.reformat_into(inter_src, src, stream=stream) + cvcuda.center_crop_into( + inter_dst, inter_src, (crop_w, crop_h), stream=stream + ) + cvcuda.reformat_into(dst, inter_dst, stream=stream) + + return run_fake + + src_shape = (N, num_channels, H, W) if is_planar else (N, H, W, num_channels) + dst_shape = ( + (N, num_channels, crop_h, crop_w) + if is_planar + else (N, crop_h, crop_w, num_channels) + ) + src = create_tensor( + src_shape, dtype, device_id, layout=layout, fill_mode="checkerboard" + ) + dst = create_tensor(dst_shape, dtype, device_id, layout=layout, fill_mode=0) + + def run(launch): + cvcuda.center_crop_into(dst, src, (crop_w, crop_h), stream=get_stream(launch)) + + return run + + +if __name__ == "__main__": + run_benchmark("centercrop", centercrop) diff --git a/bench/python/ops/bench_channelreorder.py b/bench/python/ops/bench_channelreorder.py new file mode 100644 index 000000000..2e21f9a39 --- /dev/null +++ b/bench/python/ops/bench_channelreorder.py @@ -0,0 +1,158 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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 sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cupy as cp # noqa: E402 +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + get_input_kind, + parse_shape, + get_dtype, + get_num_channels, + get_dtype_size, + get_format_from_dtype, + create_tensor, + create_image_batch_varshape, + create_stream_cache, + run_benchmark, +) + + +def channelreorder(state): + """ChannelReorder operator benchmark matching C++ BenchChannelReorder.cpp""" + + N, H, W = parse_shape(state.get_string("shape")) + dtype_str = state.get_string("InOutDataType") + dtype = get_dtype(dtype_str) + input_kind = get_input_kind(state.get_string("inputKind")) + layout = state.get_string("layout") + order_pattern = state.get_string("orderPattern") + + if layout not in ("NHWC", "NCHW", "NCHW_FAKE"): + state.skip( + "ChannelReorder benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts" + ) + return None + + is_planar = layout == "NCHW" + is_fake_planar = layout == "NCHW_FAKE" + if is_fake_planar and input_kind == "VarShape": + state.skip("Fake-planar (NCHW_FAKE) ChannelReorder benchmark is tensor-only") + return None + + device_id = state.get_device() + + num_channels = get_num_channels(dtype_str) + + dtype_size = get_dtype_size(dtype_str) + bytes_ = N * H * W * dtype_size + state.add_global_memory_reads(3 * bytes_ if is_fake_planar else bytes_) + state.add_global_memory_writes(3 * bytes_ if is_fake_planar else bytes_) + + if order_pattern == "rotate": + host_order = [(c + 1) % num_channels for c in range(num_channels)] + elif order_pattern == "zero_fill": + host_order = [-1 if c == 1 else c for c in range(num_channels)] + else: + state.skip(f"Invalid orderPattern: {order_pattern}") + return None + + get_stream = create_stream_cache() + if input_kind == "Tensor": + if is_fake_planar: + src = create_tensor( + (N, num_channels, H, W), + dtype, + device_id, + layout="NCHW", + fill_mode="checkerboard", + ) + inter_src = create_tensor( + (N, H, W, num_channels), dtype, device_id, layout="NHWC", fill_mode=0 + ) + inter_dst = create_tensor( + (N, H, W, num_channels), dtype, device_id, layout="NHWC", fill_mode=0 + ) + dst = create_tensor( + (N, num_channels, H, W), dtype, device_id, layout="NCHW", fill_mode=0 + ) + + def run_fake(launch): + stream = get_stream(launch) + cvcuda.reformat_into(inter_src, src, stream=stream) + cvcuda.channelreorder_into( + inter_dst, inter_src, host_order, stream=stream + ) + cvcuda.reformat_into(dst, inter_dst, stream=stream) + + return run_fake + + tensor_shape = (N, num_channels, H, W) if is_planar else (N, H, W, num_channels) + src = create_tensor( + tensor_shape, + dtype, + device_id, + layout=layout, + fill_mode="checkerboard", + ) + dst = create_tensor(tensor_shape, dtype, device_id, layout=layout, fill_mode=0) + + def run_tensor(launch): + cvcuda.channelreorder_into(dst, src, host_order, stream=get_stream(launch)) + + return run_tensor + + # Deterministic patterns mirroring the C++ bench exactly. + with cp.cuda.Device(device_id): + n_idx, c_idx = cp.indices((N, 4), dtype=cp.int32) + if order_pattern == "rotate": + order_data = (n_idx + c_idx) % num_channels + elif order_pattern == "zero_fill": + order_data = cp.where(c_idx == 1, -1, c_idx % num_channels).astype(cp.int32) + else: + state.skip(f"Invalid orderPattern: {order_pattern}") + return None + order = cvcuda.as_tensor(order_data, "NC") + img_format = get_format_from_dtype(dtype_str, num_channels, planar=is_planar) + + src = create_image_batch_varshape( + (N, H, W, num_channels), + 0, + img_format, + dtype, + device_id, + fill_mode="checkerboard", + ) + dst = create_image_batch_varshape( + (N, H, W, num_channels), + 0, + img_format, + dtype, + device_id, + fill_mode=0, + ) + + def run(launch): + cvcuda.channelreorder_into(dst, src, order, stream=get_stream(launch)) + + return run + + +if __name__ == "__main__": + run_benchmark("channelreorder", channelreorder) diff --git a/bench/python/ops/bench_clahe.py b/bench/python/ops/bench_clahe.py new file mode 100755 index 000000000..09cb07110 --- /dev/null +++ b/bench/python/ops/bench_clahe.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""CV-CUDA CLAHE operator benchmark - Python equivalent of BenchCLAHE.cpp""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + get_input_kind, + parse_shape, + get_dtype_size, + create_tensor, + create_image_batch_varshape, + create_stream_cache, + get_format_from_dtype, + run_benchmark, +) + + +def clahe(state): + """CLAHE operator benchmark matching C++ BenchCLAHE.cpp""" + + N, H, W = parse_shape(state.get_string("shape")) + dtype_str = state.get_string("InOutDataType") + input_kind = get_input_kind(state.get_string("inputKind")) + try: + layout = state.get_string("layout") + except (KeyError, RuntimeError): + layout = "NHWC" + tiles_x = state.get_int64("tilesX") + tiles_y = state.get_int64("tilesY") + clip10 = state.get_int64("clip10") + device_id = state.get_device() + + clip_limit = clip10 / 10.0 + tile_grid_size = (int(tiles_x), int(tiles_y)) + + if layout not in ("NHWC", "NCHW", "NCHW_FAKE"): + state.skip("CLAHE benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts") + return None + + is_planar = layout == "NCHW" + is_fake_planar = layout == "NCHW_FAKE" + if is_fake_planar and input_kind == "VarShape": + state.skip("Fake-planar (NCHW_FAKE) CLAHE benchmark is tensor-only") + return None + + dtype_size = get_dtype_size(dtype_str) + bytes_ = N * H * W * dtype_size + state.add_global_memory_reads(3 * bytes_ if is_fake_planar else bytes_) + state.add_global_memory_writes(3 * bytes_ if is_fake_planar else bytes_) + + get_stream = create_stream_cache() + + if is_fake_planar: + src = create_tensor( + (N, 1, H, W), + cvcuda.Type.U8, + device_id, + layout="NCHW", + fill_mode="lcg", + ) + inter_src = create_tensor( + (N, H, W, 1), + cvcuda.Type.U8, + device_id, + layout="NHWC", + fill_mode=0, + ) + inter_dst = create_tensor( + (N, H, W, 1), + cvcuda.Type.U8, + device_id, + layout="NHWC", + fill_mode=0, + ) + dst = create_tensor( + (N, 1, H, W), + cvcuda.Type.U8, + device_id, + layout="NCHW", + fill_mode=0, + ) + + def run_fake(launch): + stream = get_stream(launch) + cvcuda.reformat_into(inter_src, src, stream=stream) + cvcuda.clahe_into( + dst=inter_dst, + src=inter_src, + clip_limit=clip_limit, + tile_grid_size=tile_grid_size, + stream=stream, + ) + cvcuda.reformat_into(dst, inter_dst, stream=stream) + + return run_fake + + if input_kind == "Tensor": # Tensor mode + shape = (N, 1, H, W) if is_planar else (N, H, W, 1) + src = create_tensor( + shape, + cvcuda.Type.U8, + device_id, + layout=layout, + fill_mode="lcg", + ) + dst = create_tensor( + shape, + cvcuda.Type.U8, + device_id, + layout=layout, + fill_mode=0, + ) + + def run(launch): + cvcuda.clahe_into( + dst=dst, + src=src, + clip_limit=clip_limit, + tile_grid_size=tile_grid_size, + stream=get_stream(launch), + ) + + else: # ImageBatchVarShape mode + img_format = get_format_from_dtype(dtype_str) + src = create_image_batch_varshape( + (N, H, W, 1), + 0, + img_format, + dtype_str, + device_id, + fill_mode="lcg", + ) + dst = create_image_batch_varshape( + (N, H, W, 1), + 0, + img_format, + dtype_str, + device_id, + fill_mode=0, + ) + + def run(launch): + cvcuda.clahe_into( + dst=dst, + src=src, + clip_limit=clip_limit, + tile_grid_size=tile_grid_size, + stream=get_stream(launch), + ) + + return run + + +if __name__ == "__main__": + run_benchmark("clahe", clahe) diff --git a/bench/python/ops/bench_colortwist.py b/bench/python/ops/bench_colortwist.py new file mode 100755 index 000000000..45c004acb --- /dev/null +++ b/bench/python/ops/bench_colortwist.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""CV-CUDA ColorTwist operator benchmark - Python equivalent of BenchColorTwist.cpp""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + get_input_kind, + parse_shape, + get_dtype, + get_dtype_size, + get_num_channels, + get_format_from_dtype, + create_tensor, + create_image_batch_varshape, + create_stream_cache, + run_benchmark, +) + + +def colortwist(state): + """ColorTwist operator benchmark matching C++ BenchColorTwist.cpp""" + + N, H, W = parse_shape(state.get_string("shape")) + dtype_str = state.get_string("InOutDataType") + dtype = get_dtype(dtype_str) + input_kind = get_input_kind(state.get_string("inputKind")) + twist_mode = state.get_string("twistMode") + try: + layout = state.get_string("layout") + except (KeyError, RuntimeError): + layout = "NHWC" + device_id = state.get_device() + + num_channels = get_num_channels(dtype_str) + + if layout not in ("NHWC", "NCHW", "NCHW_FAKE"): + state.skip( + "ColorTwist benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts" + ) + return None + + is_planar = layout == "NCHW" + is_fake_planar = layout == "NCHW_FAKE" + if is_fake_planar and input_kind == "VarShape": + state.skip("Fake-planar (NCHW_FAKE) ColorTwist benchmark is tensor-only") + return None + if is_planar and input_kind == "VarShape" and dtype_str == "uchar4": + state.skip("RGBA8p varshape is unsupported by the Python image API") + return None + + dtype_size = get_dtype_size(dtype_str) + image_bytes = N * H * W * dtype_size + if is_fake_planar: + state.add_global_memory_reads(3 * image_bytes) + state.add_global_memory_writes(3 * image_bytes) + else: + state.add_global_memory_reads(image_bytes) + state.add_global_memory_writes(image_bytes) + + if twist_mode == "per_sample": + twist = create_tensor( + (N, 3, 4), cvcuda.Type.F32, device_id, layout="NHW", fill_mode="lcg" + ) + elif twist_mode == "global": + twist = create_tensor( + (3, 4), cvcuda.Type.F32, device_id, layout="HW", fill_mode="lcg" + ) + else: + state.skip(f"Invalid twistMode: {twist_mode}") + return None + + get_stream = create_stream_cache() + + if is_fake_planar: + src = create_tensor( + (N, num_channels, H, W), + dtype, + device_id, + layout="NCHW", + fill_mode="checkerboard", + ) + inter_src = create_tensor( + (N, H, W, num_channels), dtype, device_id, layout="NHWC", fill_mode=0 + ) + inter_dst = create_tensor( + (N, H, W, num_channels), dtype, device_id, layout="NHWC", fill_mode=0 + ) + dst = create_tensor( + (N, num_channels, H, W), dtype, device_id, layout="NCHW", fill_mode=0 + ) + + def run_fake(launch): + stream = get_stream(launch) + cvcuda.reformat_into(inter_src, src, stream=stream) + cvcuda.color_twist_into(inter_dst, inter_src, twist, stream=stream) + cvcuda.reformat_into(dst, inter_dst, stream=stream) + + return run_fake + + if input_kind == "Tensor": # Tensor mode + tensor_shape = (N, num_channels, H, W) if is_planar else (N, H, W, num_channels) + src = create_tensor( + tensor_shape, + dtype, + device_id, + layout=layout, + fill_mode="checkerboard", + ) + dst = create_tensor(tensor_shape, dtype, device_id, layout=layout, fill_mode=0) + else: # ImageBatchVarShape mode + img_format = get_format_from_dtype(dtype_str, num_channels, planar=is_planar) + src = create_image_batch_varshape( + (N, H, W, num_channels), + 0, + img_format, + dtype_str, + device_id, + fill_mode="checkerboard", + ) + dst = create_image_batch_varshape( + (N, H, W, num_channels), + 0, + img_format, + dtype_str, + device_id, + fill_mode=0, + ) + + def run(launch): + cvcuda.color_twist_into(dst, src, twist, stream=get_stream(launch)) + + return run + + +if __name__ == "__main__": + run_benchmark("colortwist", colortwist) diff --git a/bench/python/ops/bench_composite.py b/bench/python/ops/bench_composite.py new file mode 100755 index 000000000..1b080f029 --- /dev/null +++ b/bench/python/ops/bench_composite.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""CV-CUDA Composite operator benchmark - Python equivalent of BenchComposite.cpp""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + get_input_kind, + parse_shape, + get_dtype, + get_dtype_size, + get_num_channels, + get_format_from_dtype, + create_tensor, + create_image_batch_varshape, + create_stream_cache, + run_benchmark, +) + + +def composite(state): + """Composite operator benchmark matching C++ BenchComposite.cpp""" + + N, H, W = parse_shape(state.get_string("shape")) + dtype_str = state.get_string("InOutDataType") + dtype = get_dtype(dtype_str) + input_kind = get_input_kind(state.get_string("inputKind")) + out_channels = state.get_int64("outChannels") + try: + layout = state.get_string("layout") + except (KeyError, RuntimeError): + layout = "NHWC" + device_id = state.get_device() + + num_channels = get_num_channels(dtype_str) + + if out_channels not in (3, 4): + state.skip(f"Invalid outChannels: {out_channels}") + return None + if layout not in ("NHWC", "NCHW", "NCHW_FAKE"): + state.skip( + "Composite benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts" + ) + return None + is_planar = layout == "NCHW" + is_fake_planar = layout == "NCHW_FAKE" + if is_fake_planar and input_kind == "VarShape": + state.skip("Fake-planar (NCHW_FAKE) Composite benchmark is tensor-only") + return None + + dtype_size = get_dtype_size(dtype_str) + base_dtype_size = get_dtype_size(dtype) + mask_dtype_size = 1 # uint8 mask + state.add_global_memory_reads(N * H * W * (dtype_size * 2 + mask_dtype_size)) + state.add_global_memory_writes(N * H * W * out_channels * base_dtype_size) + + if is_fake_planar: + fg = create_tensor( + (N, num_channels, H, W), + dtype, + device_id, + layout="NCHW", + fill_mode="checkerboard", + ) + bg = create_tensor( + (N, num_channels, H, W), + dtype, + device_id, + layout="NCHW", + fill_mode="checkerboard", + ) + mask = create_tensor( + (N, 1, H, W), + cvcuda.Type.U8, + device_id, + layout="NCHW", + fill_mode=1, + ) + inter_fg = create_tensor( + (N, H, W, num_channels), + dtype, + device_id, + layout="NHWC", + fill_mode=0, + ) + inter_bg = create_tensor( + (N, H, W, num_channels), + dtype, + device_id, + layout="NHWC", + fill_mode=0, + ) + inter_mask = create_tensor( + (N, H, W, 1), + cvcuda.Type.U8, + device_id, + layout="NHWC", + fill_mode=0, + ) + inter_dst = create_tensor( + (N, H, W, out_channels), + dtype, + device_id, + layout="NHWC", + fill_mode=0, + ) + dst = create_tensor( + (N, out_channels, H, W), + dtype, + device_id, + layout="NCHW", + fill_mode=0, + ) + elif input_kind == "Tensor": # Tensor mode + tensor_shape = (N, num_channels, H, W) if is_planar else (N, H, W, num_channels) + mask_shape = (N, 1, H, W) if is_planar else (N, H, W, 1) + dst_shape = (N, out_channels, H, W) if is_planar else (N, H, W, out_channels) + fg = create_tensor( + tensor_shape, + dtype, + device_id, + layout=layout, + fill_mode="checkerboard", + ) + bg = create_tensor( + tensor_shape, + dtype, + device_id, + layout=layout, + fill_mode="checkerboard", + ) + mask = create_tensor( + mask_shape, + cvcuda.Type.U8, + device_id, + layout=layout, + fill_mode=1, + ) + dst = create_tensor( + dst_shape, + dtype, + device_id, + layout=layout, + fill_mode=0, + ) + else: # ImageBatchVarShape mode + img_format = get_format_from_dtype(dtype_str, num_channels, planar=is_planar) + dst_format = get_format_from_dtype(dtype_str, out_channels, planar=is_planar) + mask_format = cvcuda.Format.U8 + fg = create_image_batch_varshape( + (N, H, W, num_channels), + 0, + img_format, + dtype, + device_id, + fill_mode="checkerboard", + ) + bg = create_image_batch_varshape( + (N, H, W, num_channels), + 0, + img_format, + dtype, + device_id, + fill_mode="checkerboard", + ) + mask = create_image_batch_varshape( + (N, H, W), 0, mask_format, cvcuda.Type.U8, device_id, fill_mode=1 + ) + dst = create_image_batch_varshape( + (N, H, W, out_channels), + 0, + dst_format, + dtype, + device_id, + fill_mode="checkerboard", + ) + + get_stream = create_stream_cache() + + def run(launch): + stream = get_stream(launch) + if is_fake_planar: + cvcuda.reformat_into(inter_fg, fg, stream=stream) + cvcuda.reformat_into(inter_bg, bg, stream=stream) + cvcuda.reformat_into(inter_mask, mask, stream=stream) + cvcuda.composite_into( + inter_dst, inter_fg, inter_bg, inter_mask, stream=stream + ) + cvcuda.reformat_into(dst, inter_dst, stream=stream) + else: + cvcuda.composite_into(dst, fg, bg, mask, stream=stream) + + return run + + +if __name__ == "__main__": + run_benchmark("composite", composite) diff --git a/bench/python/ops/bench_conv2d.py b/bench/python/ops/bench_conv2d.py new file mode 100644 index 000000000..65a929472 --- /dev/null +++ b/bench/python/ops/bench_conv2d.py @@ -0,0 +1,113 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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 sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + get_input_kind, + parse_shape, + get_dtype, + get_dtype_size, + get_num_channels, + get_format_from_dtype, + get_border_type, + create_image_batch_varshape, + create_tensor, + create_stream_cache, + run_benchmark, +) + + +def conv2d(state): + """Conv2D operator benchmark matching C++ BenchConv2D.cpp""" + + N, H, W = parse_shape(state.get_string("shape")) + dtype_str = state.get_string("InOutDataType") + dtype = get_dtype(dtype_str) + num_channels = get_num_channels(dtype_str) + kernel_size_str = state.get_string("kernelSize") + border = get_border_type(state.get_string("border")) + input_kind = get_input_kind(state.get_string("inputKind")) + try: + layout = state.get_string("layout") + except (KeyError, RuntimeError): + layout = "NHWC" + if input_kind == "Tensor": # Tensor mode + state.skip("Tensor not implemented for this benchmark") + return None + + if layout not in ("NHWC", "NCHW"): + state.skip("Conv2D benchmark supports only NHWC and NCHW layouts") + return None + + is_planar = layout == "NCHW" + if is_planar and num_channels == 1: + state.skip("Single-channel Conv2D has no distinct planar image-batch layout") + return None + if is_planar and num_channels == 2: + state.skip("Planar Conv2D benchmark does not support 2-channel layouts") + return None + + device_id = state.get_device() + + kernel_h, kernel_w = map(int, kernel_size_str.split("x")) + + dtype_size = get_dtype_size(dtype_str) + state.add_global_memory_reads(N * H * W * dtype_size) + state.add_global_memory_writes(N * H * W * dtype_size) + + input_format = get_format_from_dtype(dtype_str, num_channels, planar=is_planar) + kernel_format = cvcuda.Format.F32 + image_shape = (N, H, W) if num_channels == 1 else (N, H, W, num_channels) + + src_batch = create_image_batch_varshape( + image_shape, 0, input_format, dtype, device_id, fill_mode="checkerboard" + ) + dst_batch = create_image_batch_varshape( + image_shape, 0, input_format, dtype, device_id, fill_mode=0 + ) + kernel_batch = create_image_batch_varshape( + (N, kernel_h, kernel_w), + 0, + kernel_format, + cvcuda.Type.F32, + device_id, + fill_mode="lcg", + ) + kernel_anchor_cvcuda = create_tensor( + (N, 2), cvcuda.Type.S32, device_id, layout="NC", fill_mode=[-1, -1] + ) + + get_stream = create_stream_cache() + + def run(launch): + cvcuda.conv2d_into( + dst_batch, + src_batch, + kernel_batch, + kernel_anchor_cvcuda, + border, + stream=get_stream(launch), + ) + + return run + + +if __name__ == "__main__": + run_benchmark("conv2d", conv2d) diff --git a/bench/python/ops/bench_convertto.py b/bench/python/ops/bench_convertto.py new file mode 100755 index 000000000..ab32b790a --- /dev/null +++ b/bench/python/ops/bench_convertto.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""CV-CUDA ConvertTo operator benchmark - Python equivalent of BenchConvertTo.cpp""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + get_input_kind, + parse_shape, + get_dtype, + get_dtype_size, + get_num_channels, + create_tensor, + create_stream_cache, + run_benchmark, +) + +SCALE_MODES = { + "affine": (0.123, 0.456), + "identity": (1.0, 0.0), +} + + +def convertto(state): + """ConvertTo operator benchmark matching C++ BenchConvertTo.cpp""" + + N, H, W = parse_shape(state.get_string("shape")) + dtype_str = state.get_string("InOutDataType") + dtype = get_dtype(dtype_str) + num_channels = get_num_channels(dtype_str) + out_dtype_str = state.get_string("outDataType") + out_dtype = get_dtype(out_dtype_str) + scale_mode = state.get_string("scaleMode") + input_kind = get_input_kind(state.get_string("inputKind")) + try: + layout = state.get_string("layout") + except Exception: + layout = "NHWC" + + if layout not in ("NHWC", "NCHW", "NCHW_FAKE"): + state.skip( + "ConvertTo benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts" + ) + return None + + is_planar = layout == "NCHW" + is_fake_planar = layout == "NCHW_FAKE" + + # ConvertTo is tensor-only; the native and fake planar paths are too. + if input_kind == "VarShape": # ImageBatchVarShape mode + state.skip("ImageBatchVarShape not implemented for this benchmark") + return None + if scale_mode not in SCALE_MODES: + state.skip(f"Invalid scaleMode: {scale_mode}") + return None + + scale, offset = SCALE_MODES[scale_mode] + + device_id = state.get_device() + + dtype_size = get_dtype_size(dtype_str) + out_dtype_size = get_dtype_size(out_dtype) * num_channels + src_bytes = N * H * W * dtype_size + dst_bytes = N * H * W * out_dtype_size + if is_fake_planar: + # reformat(NCHW->NHWC) + convert + reformat(NHWC->NCHW) + state.add_global_memory_reads(2 * src_bytes + dst_bytes) + state.add_global_memory_writes(src_bytes + 2 * dst_bytes) + else: + state.add_global_memory_reads(src_bytes) + state.add_global_memory_writes(dst_bytes) + + get_stream = create_stream_cache() + + if is_fake_planar: + # Tensor-only "fake planar": planar->interleaved->convert->interleaved->planar, + # all timed, as the comparison baseline for the native planar (NCHW) path. + src = create_tensor( + (N, num_channels, H, W), + dtype, + device_id, + layout="NCHW", + fill_mode="checkerboard", + ) + inter_src = create_tensor( + (N, H, W, num_channels), dtype, device_id, layout="NHWC", fill_mode=0 + ) + inter_dst = create_tensor( + (N, H, W, num_channels), out_dtype, device_id, layout="NHWC", fill_mode=0 + ) + dst = create_tensor( + (N, num_channels, H, W), out_dtype, device_id, layout="NCHW", fill_mode=0 + ) + + def run_fake(launch): + stream = get_stream(launch) + cvcuda.reformat_into(inter_src, src, stream=stream) + cvcuda.convertto_into( + inter_dst, inter_src, scale=scale, offset=offset, stream=stream + ) + cvcuda.reformat_into(dst, inter_dst, stream=stream) + + return run_fake + + src_shape = (N, num_channels, H, W) if is_planar else (N, H, W, num_channels) + dst_shape = (N, num_channels, H, W) if is_planar else (N, H, W, num_channels) + src = create_tensor( + src_shape, dtype, device_id, layout=layout, fill_mode="checkerboard" + ) + dst = create_tensor(dst_shape, out_dtype, device_id, layout=layout, fill_mode=0) + + def run(launch): + cvcuda.convertto_into( + dst, src, scale=scale, offset=offset, stream=get_stream(launch) + ) + + return run + + +if __name__ == "__main__": + run_benchmark("convertto", convertto) diff --git a/bench/python/ops/bench_copymakeborder.py b/bench/python/ops/bench_copymakeborder.py new file mode 100755 index 000000000..f415059b7 --- /dev/null +++ b/bench/python/ops/bench_copymakeborder.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""CV-CUDA CopyMakeBorder operator benchmark - Python equivalent of BenchCopyMakeBorder.cpp""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + get_input_kind, + parse_shape, + get_dtype, + get_dtype_size, + get_num_channels, + get_format_from_dtype, + get_border_type, + create_tensor, + create_image_batch_varshape, + create_stream_cache, + run_benchmark, +) + + +def copymakeborder(state): + """CopyMakeBorder operator benchmark matching C++ BenchCopyMakeBorder.cpp""" + + N, H, W = parse_shape(state.get_string("shape")) + dtype_str = state.get_string("InOutDataType") + dtype = get_dtype(dtype_str) + num_channels = get_num_channels(dtype_str) + border = get_border_type(state.get_string("border")) + input_kind = get_input_kind(state.get_string("inputKind")) + layout = state.get_string("layout") + device_id = state.get_device() + + if layout not in ("NHWC", "NCHW", "NCHW_FAKE"): + state.skip( + "CopyMakeBorder benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts" + ) + return None + + is_planar = layout == "NCHW" + is_fake_planar = layout == "NCHW_FAKE" + if is_fake_planar and input_kind == "VarShape": + state.skip("Fake-planar (NCHW_FAKE) CopyMakeBorder benchmark is tensor-only") + return None + + top = H // 2 + left = W // 2 + + dtype_size = get_dtype_size(dtype_str) + src_bytes = N * H * W * dtype_size + dst_bytes = N * (top + H) * (left + W) * dtype_size + if is_fake_planar: + state.add_global_memory_reads(2 * src_bytes + dst_bytes) + state.add_global_memory_writes(src_bytes + 2 * dst_bytes) + else: + state.add_global_memory_reads(src_bytes) + state.add_global_memory_writes(dst_bytes) + + border_value = [0.0, 0.0, 0.0, 0.0] + + get_stream = create_stream_cache() + + if is_fake_planar: + src = create_tensor( + (N, num_channels, H, W), + dtype, + device_id, + layout="NCHW", + fill_mode="checkerboard", + ) + inter_src = create_tensor( + (N, H, W, num_channels), + dtype, + device_id, + layout="NHWC", + fill_mode=0, + ) + inter_dst = create_tensor( + (N, top + H, left + W, num_channels), + dtype, + device_id, + layout="NHWC", + fill_mode=0, + ) + dst = create_tensor( + (N, num_channels, top + H, left + W), + dtype, + device_id, + layout="NCHW", + fill_mode=0, + ) + + def run_fake(launch): + stream = get_stream(launch) + cvcuda.reformat_into(inter_src, src, stream=stream) + cvcuda.copymakeborder_into( + inter_dst, + inter_src, + border_mode=border, + border_value=border_value, + top=top, + left=left, + stream=stream, + ) + cvcuda.reformat_into(dst, inter_dst, stream=stream) + + return run_fake + + if input_kind == "Tensor": # Tensor mode + src_shape = (N, num_channels, H, W) if is_planar else (N, H, W, num_channels) + dst_shape = ( + (N, num_channels, top + H, left + W) + if is_planar + else (N, top + H, left + W, num_channels) + ) + src = create_tensor( + src_shape, + dtype, + device_id, + layout=layout, + fill_mode="checkerboard", + ) + dst = create_tensor( + dst_shape, + dtype, + device_id, + layout=layout, + fill_mode=0, + ) + else: # ImageBatchVarShape mode + top_scalar, left_scalar = top, left + top = create_tensor( + (N, 1, 1, 1), + cvcuda.Type.S32, + device_id, + layout="NHWC", + fill_mode=top_scalar, + ) + left = create_tensor( + (N, 1, 1, 1), + cvcuda.Type.S32, + device_id, + layout="NHWC", + fill_mode=left_scalar, + ) + img_format = get_format_from_dtype(dtype_str, num_channels, planar=is_planar) + src = create_image_batch_varshape( + (N, H, W, num_channels), + 0, + img_format, + dtype, + device_id, + fill_mode="checkerboard", + ) + dst = create_image_batch_varshape( + (N, top_scalar + H, left_scalar + W, num_channels), + 0, + img_format, + dtype, + device_id, + fill_mode="checkerboard", + ) + + def run(launch): + cvcuda.copymakeborder_into( + dst, + src, + border_mode=border, + border_value=border_value, + top=top, + left=left, + stream=get_stream(launch), + ) + + return run + + +if __name__ == "__main__": + run_benchmark("copymakeborder", copymakeborder) diff --git a/bench/python/ops/bench_cropflipnormalizereformat.py b/bench/python/ops/bench_cropflipnormalizereformat.py new file mode 100644 index 000000000..b367867b1 --- /dev/null +++ b/bench/python/ops/bench_cropflipnormalizereformat.py @@ -0,0 +1,144 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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 sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + get_input_kind, + parse_shape, + get_dtype, + get_dtype_size, + get_num_channels, + get_format_from_dtype, + get_border_type, + create_image_batch_varshape, + create_tensor, + create_stream_cache, + run_benchmark, +) + + +FLAGS_MODES = { + "normal": 0, + "stddev": cvcuda.NormalizeFlags.SCALE_IS_STDDEV, +} + + +def cropflipnormalizereformat(state): + """CropFlipNormalizeReformat operator benchmark matching C++ BenchCropFlipNormalizeReformat.cpp""" + + N, H, W = parse_shape(state.get_string("shape")) + dtype_str = state.get_string("InOutDataType") + dtype = get_dtype(dtype_str) + num_channels = get_num_channels(dtype_str) + border = get_border_type(state.get_string("border")) + crop_mode = state.get_string("cropMode") + flags_mode = state.get_string("flagsMode") + input_kind = get_input_kind(state.get_string("inputKind")) + src_layout = state.get_string("srcLayout") + dst_layout = state.get_string("layout") + + if input_kind == "Tensor": # Tensor mode + state.skip("Tensor not implemented for this benchmark") + return None + if src_layout not in ("NHWC", "NCHW") or dst_layout not in ("NHWC", "NCHW"): + state.skip( + "CropFlipNormalizeReformat benchmark supports only NHWC and NCHW source/output layouts" + ) + return None + if crop_mode not in ["full", "padded16"]: + state.skip(f"Invalid cropMode: {crop_mode}") + return None + if flags_mode not in FLAGS_MODES: + state.skip(f"Invalid flagsMode: {flags_mode}") + return None + + device_id = state.get_device() + + globalScale = 1.234 + globalShift = 2.345 + epsilon = 12.34 + flags = FLAGS_MODES[flags_mode] + borderValue = 0.0 + + dtype_size = get_dtype_size(dtype_str) + state.add_global_memory_reads(N * H * W * dtype_size + N * 4 + N * 4 + N * 4 * 4) + state.add_global_memory_writes(N * H * W * dtype_size) + + src_planar = src_layout == "NCHW" + dst_planar = dst_layout == "NCHW" + input_format = get_format_from_dtype(dtype_str, num_channels, planar=src_planar) + + src_batch = create_image_batch_varshape( + (N, H, W, num_channels), + 0, + input_format, + dtype, + device_id, + fill_mode="checkerboard", + ) + dst_shape = (N, num_channels, H, W) if dst_planar else (N, H, W, num_channels) + dst = create_tensor(dst_shape, dtype, device_id, layout=dst_layout, fill_mode=0) + + crop_x = -16 if crop_mode == "padded16" else 0 + crop_y = -16 if crop_mode == "padded16" else 0 + rect = create_tensor( + (N, 1, 1, 4), + cvcuda.Type.S32, + device_id, + layout="NHWC", + fill_mode=[crop_x, crop_y, W, H], + ) + flip_code = create_tensor( + (N,), cvcuda.Type.S32, device_id, layout="N", fill_mode=-1 + ) + + # base and scale: deterministic LCG over float [-1, +1], bit-identical to + # the C++ RandomValues() GPU fast path (see BenchCropFlipNormalizeReformat.cpp). + base = create_tensor( + (N, 1, 1, 1), cvcuda.Type.F32, device_id, layout="NHWC", fill_mode="lcg" + ) + scale = create_tensor( + (N, 1, 1, 1), cvcuda.Type.F32, device_id, layout="NHWC", fill_mode="lcg" + ) + + get_stream = create_stream_cache() + + def run(launch): + cvcuda.crop_flip_normalize_reformat_into( + dst, + src_batch, + rect, + flip_code, + base, + scale, + globalscale=globalScale, + globalshift=globalShift, + epsilon=epsilon, + flags=flags, + border=border, + bvalue=borderValue, + stream=get_stream(launch), + ) + + return run + + +if __name__ == "__main__": + run_benchmark("cropflipnormalizereformat", cropflipnormalizereformat) diff --git a/bench/python/ops/bench_customcrop.py b/bench/python/ops/bench_customcrop.py new file mode 100755 index 000000000..069544410 --- /dev/null +++ b/bench/python/ops/bench_customcrop.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""CV-CUDA CustomCrop operator benchmark - Python equivalent of BenchCustomCrop.cpp""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + get_input_kind, + parse_shape, + get_dtype, + get_dtype_size, + get_num_channels, + create_tensor, + create_stream_cache, + run_benchmark, +) + + +def customcrop(state): + """CustomCrop operator benchmark matching C++ BenchCustomCrop.cpp""" + + N, H, W = parse_shape(state.get_string("shape")) + dtype_str = state.get_string("InOutDataType") + dtype = get_dtype(dtype_str) + num_channels = get_num_channels(dtype_str) + crop_mode = state.get_string("cropMode") + input_kind = get_input_kind(state.get_string("inputKind")) + layout = state.get_string("layout") + + if layout not in ("NHWC", "NCHW", "NCHW_FAKE"): + state.skip( + "CustomCrop benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts" + ) + return None + + is_planar = layout == "NCHW" + is_fake_planar = layout == "NCHW_FAKE" + + # CustomCrop is tensor-only; the native and fake planar paths are too. + if input_kind == "VarShape": # ImageBatchVarShape mode + state.skip("ImageBatchVarShape not implemented for this benchmark") + return None + if crop_mode == "full": + crop_x, crop_y, crop_w, crop_h = 0, 0, W, H + elif crop_mode == "center_half": + crop_x, crop_y, crop_w, crop_h = W // 4, H // 4, W // 2, H // 2 + else: + state.skip(f"Invalid cropMode: {crop_mode}") + return None + + device_id = state.get_device() + + crop_rect = cvcuda.RectI(crop_x, crop_y, crop_w, crop_h) + + dtype_size = get_dtype_size(dtype_str) + full_bytes = N * H * W * dtype_size + crop_bytes = N * crop_h * crop_w * dtype_size + if is_fake_planar: + # reformat(NCHW->NHWC) + crop + reformat(NHWC->NCHW) + state.add_global_memory_reads(full_bytes + 2 * crop_bytes) + state.add_global_memory_writes(full_bytes + 2 * crop_bytes) + else: + state.add_global_memory_reads(crop_bytes) + state.add_global_memory_writes(crop_bytes) + + get_stream = create_stream_cache() + + if is_fake_planar: + # Tensor-only "fake planar": planar->interleaved->crop->interleaved->planar, + # all timed, as the comparison baseline for the native planar (NCHW) path. + src = create_tensor( + (N, num_channels, H, W), + dtype, + device_id, + layout="NCHW", + fill_mode="checkerboard", + ) + inter_src = create_tensor( + (N, H, W, num_channels), dtype, device_id, layout="NHWC", fill_mode=0 + ) + inter_dst = create_tensor( + (N, crop_h, crop_w, num_channels), + dtype, + device_id, + layout="NHWC", + fill_mode=0, + ) + dst = create_tensor( + (N, num_channels, crop_h, crop_w), + dtype, + device_id, + layout="NCHW", + fill_mode=0, + ) + + def run_fake(launch): + stream = get_stream(launch) + cvcuda.reformat_into(inter_src, src, stream=stream) + cvcuda.customcrop_into(inter_dst, inter_src, crop_rect, stream=stream) + cvcuda.reformat_into(dst, inter_dst, stream=stream) + + return run_fake + + src_shape = (N, num_channels, H, W) if is_planar else (N, H, W, num_channels) + dst_shape = ( + (N, num_channels, crop_h, crop_w) + if is_planar + else (N, crop_h, crop_w, num_channels) + ) + src = create_tensor( + src_shape, dtype, device_id, layout=layout, fill_mode="checkerboard" + ) + dst = create_tensor(dst_shape, dtype, device_id, layout=layout, fill_mode=0) + + def run(launch): + cvcuda.customcrop_into(dst, src, crop_rect, stream=get_stream(launch)) + + return run + + +if __name__ == "__main__": + run_benchmark("customcrop", customcrop) diff --git a/bench/python/ops/bench_cvtcolor.py b/bench/python/ops/bench_cvtcolor.py new file mode 100755 index 000000000..2cd693210 --- /dev/null +++ b/bench/python/ops/bench_cvtcolor.py @@ -0,0 +1,308 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""CV-CUDA CvtColor operator benchmark - Python equivalent of BenchCvtColor.cpp""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + get_input_kind, + parse_shape, + create_tensor, + create_image_batch_varshape, + create_stream_cache, + run_benchmark, +) + + +def _get_layout(state): + try: + layout = state.get_string("layout") + except (KeyError, RuntimeError): + layout = "NHWC" + if layout not in ("NHWC", "NCHW", "NCHW_FAKE"): + state.skip("CvtColor benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts") + return None + return layout + + +def _planar_format(img_format): + if img_format == cvcuda.Format.RGB8: + return cvcuda.Format.RGB8p + if img_format == cvcuda.Format.RGBA8: + return cvcuda.Format.RGBA8p + raise ValueError("Planar CvtColor var-shape benchmark supports only RGB8p/RGBA8p") + + +def cvtcolor(state): + """CvtColor operator benchmark matching C++ BenchCvtColor.cpp""" + + N, H, W = parse_shape(state.get_string("shape")) + code_str = state.get_string("code") + input_kind = get_input_kind(state.get_string("inputKind")) + layout = _get_layout(state) + if layout is None: + return None + device_id = state.get_device() + + code_map = { + "RGB2BGR": ( + cvcuda.ColorConversion.RGB2BGR, + cvcuda.Format.RGB8, + cvcuda.Format.BGR8, + 3, + 3, + ), + "RGB2RGBA": ( + cvcuda.ColorConversion.RGB2RGBA, + cvcuda.Format.RGB8, + cvcuda.Format.RGBA8, + 3, + 4, + ), + "RGBA2RGB": ( + cvcuda.ColorConversion.RGBA2RGB, + cvcuda.Format.RGBA8, + cvcuda.Format.RGB8, + 4, + 3, + ), + "RGB2GRAY": ( + cvcuda.ColorConversion.RGB2GRAY, + cvcuda.Format.RGB8, + cvcuda.Format.Y8, + 3, + 1, + ), + "GRAY2RGB": ( + cvcuda.ColorConversion.GRAY2RGB, + cvcuda.Format.Y8, + cvcuda.Format.RGB8, + 1, + 3, + ), + "RGB2HSV": ( + cvcuda.ColorConversion.RGB2HSV, + cvcuda.Format.RGB8, + cvcuda.Format.HSV8, + 3, + 3, + ), + "HSV2RGB": ( + cvcuda.ColorConversion.HSV2RGB, + cvcuda.Format.HSV8, + cvcuda.Format.RGB8, + 3, + 3, + ), + "RGB2YUV": ( + cvcuda.ColorConversion.RGB2YUV, + cvcuda.Format.RGB8, + cvcuda.Format.YUV8p, + 3, + 3, + ), + "YUV2RGB": ( + cvcuda.ColorConversion.YUV2RGB, + cvcuda.Format.YUV8p, + cvcuda.Format.RGB8, + 3, + 3, + ), + "RGB2YUV_NV12": ( + cvcuda.ColorConversion.RGB2YUV_NV12, + cvcuda.Format.RGB8, + cvcuda.Format.NV12, + 3, + 1.5, + ), + "YUV2RGB_NV12": ( + cvcuda.ColorConversion.YUV2RGB_NV12, + cvcuda.Format.NV12, + cvcuda.Format.RGB8, + 1.5, + 3, + ), + } + + code, in_format, out_format, in_bpp, out_bpp = code_map[code_str] + + is_planar = layout == "NCHW" + is_fake_planar = layout == "NCHW_FAKE" + if is_fake_planar and input_kind == "VarShape": + state.skip("Fake-planar (NCHW_FAKE) CvtColor benchmark is tensor-only") + return None + if (is_planar or is_fake_planar) and ( + in_bpp != int(in_bpp) or out_bpp != int(out_bpp) + ): + state.skip("Skipping subsampled YUV CvtColor formats for planar benchmarks") + return None + + src_bytes = int(N * H * W * in_bpp) + dst_bytes = int(N * H * W * out_bpp) + if is_fake_planar: + state.add_global_memory_reads(2 * src_bytes + dst_bytes) + state.add_global_memory_writes(src_bytes + 2 * dst_bytes) + else: + state.add_global_memory_reads(src_bytes) + state.add_global_memory_writes(dst_bytes) + + if input_kind == "Tensor": # Tensor mode + if is_fake_planar: + in_ch = int(in_bpp) + out_ch = int(out_bpp) + src = create_tensor( + (N, in_ch, H, W), + cvcuda.Type.U8, + device_id, + layout="NCHW", + fill_mode="checkerboard", + ) + inter_src = create_tensor( + (N, H, W, in_ch), + cvcuda.Type.U8, + device_id, + layout="NHWC", + fill_mode=0, + ) + inter_dst = create_tensor( + (N, H, W, out_ch), + cvcuda.Type.U8, + device_id, + layout="NHWC", + fill_mode=0, + ) + dst = create_tensor( + (N, out_ch, H, W), + cvcuda.Type.U8, + device_id, + layout="NCHW", + fill_mode=0, + ) + elif is_planar: + in_ch = int(in_bpp) + out_ch = int(out_bpp) + src = create_tensor( + (N, in_ch, H, W), + cvcuda.Type.U8, + device_id, + layout="NCHW", + fill_mode="checkerboard", + ) + dst = create_tensor( + (N, out_ch, H, W), cvcuda.Type.U8, device_id, layout="NCHW", fill_mode=0 + ) + elif in_format == cvcuda.Format.NV12: + height420 = (H * 3) // 2 + src = create_tensor( + (N, height420, W, 1), + cvcuda.Type.U8, + device_id, + layout="NHWC", + fill_mode="checkerboard", + ) + else: + in_ch = int(in_bpp) + src = create_tensor( + (N, H, W, in_ch), + cvcuda.Type.U8, + device_id, + layout="NHWC", + fill_mode="checkerboard", + ) + + if not (is_fake_planar or is_planar): + if out_format == cvcuda.Format.NV12: + height420 = (H * 3) // 2 + dst = create_tensor( + (N, height420, W, 1), + cvcuda.Type.U8, + device_id, + layout="NHWC", + fill_mode=0, + ) + else: + out_ch = int(out_bpp) + dst = create_tensor( + (N, H, W, out_ch), + cvcuda.Type.U8, + device_id, + layout="NHWC", + fill_mode=0, + ) + else: # ImageBatchVarShape mode + if ( + in_format == cvcuda.Format.NV12 + or out_format == cvcuda.Format.NV12 + or in_format == cvcuda.Format.YUV8p + or out_format == cvcuda.Format.YUV8p + ): + state.skip( + "Skipping formats that have subsampled planes for the varshape benchmark" + ) + return None + + in_ch = int(in_bpp) + out_ch = int(out_bpp) + if is_planar: + try: + in_format = _planar_format(in_format) + out_format = _planar_format(out_format) + except ValueError as exc: + state.skip(str(exc)) + return None + + src = create_image_batch_varshape( + (N, H, W, in_ch), + 0, + in_format, + cvcuda.Type.U8, + device_id, + fill_mode="checkerboard", + ) + dst = create_image_batch_varshape( + (N, H, W, out_ch), + 0, + out_format, + cvcuda.Type.U8, + device_id, + fill_mode=0, + ) + + get_stream = create_stream_cache() + + if is_fake_planar: + + def run_fake(launch): + stream = get_stream(launch) + cvcuda.reformat_into(inter_src, src, stream=stream) + cvcuda.cvtcolor_into(inter_dst, inter_src, code, stream=stream) + cvcuda.reformat_into(dst, inter_dst, stream=stream) + + return run_fake + + def run(launch): + cvcuda.cvtcolor_into(dst, src, code, stream=get_stream(launch)) + + return run + + +if __name__ == "__main__": + run_benchmark("cvtcolor", cvtcolor) diff --git a/bench/python/ops/bench_erase.py b/bench/python/ops/bench_erase.py new file mode 100755 index 000000000..f5706e4ef --- /dev/null +++ b/bench/python/ops/bench_erase.py @@ -0,0 +1,255 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""CV-CUDA Erase operator benchmark - Python equivalent of BenchErase.cpp""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + get_input_kind, + parse_shape, + get_dtype, + get_dtype_size, + get_num_channels, + get_format_from_dtype, + create_tensor, + create_image_batch_varshape, + create_stream_cache, + run_benchmark, +) + + +def _fill_i32_tensor(tensor, values, device_id): + """Fill vector-typed int32 parameter tensors via their CUDA buffer.""" + + import cupy as cp + + with cp.cuda.Device(device_id): + cuda_buffer = tensor.cuda() + ptr = cuda_buffer.__cuda_array_interface__["data"][0] + data = cp.asarray(values, dtype=cp.int32) + mem = cp.cuda.UnownedMemory(ptr, data.nbytes, cuda_buffer) + view = cp.ndarray( + data.shape, dtype=cp.int32, memptr=cp.cuda.MemoryPointer(mem, 0) + ) + view[...] = data + cp.cuda.get_current_stream().synchronize() + + +def erase(state): + """Erase operator benchmark matching C++ BenchErase.cpp""" + + shape = parse_shape(state.get_string("shape")) + dtype_str = state.get_string("InOutDataType") + dtype = get_dtype(dtype_str) + num_channels = get_num_channels(dtype_str) + random_mode = state.get_string("randomMode") + input_kind = get_input_kind(state.get_string("inputKind")) + num_erase = state.get_int64("numErase") + device_id = state.get_device() + + N, H, W = shape + + region_mode = random_mode == "torchvision" + random = False if region_mode else {"random": True, "constant": False}[random_mode] + seed = 0 + try: + layout = state.get_string("layout") + except Exception: + layout = "NHWC" + + if layout not in ("NHWC", "NCHW", "NCHW_FAKE"): + state.skip("Erase benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts") + return None + + is_planar = layout == "NCHW" + is_fake_planar = layout == "NCHW_FAKE" + if is_fake_planar and input_kind == "VarShape": + state.skip("Fake-planar (NCHW_FAKE) erase benchmark is tensor-only") + return None + if region_mode and input_kind == "VarShape": + state.skip("Torchvision Erase region benchmark is tensor-only") + return None + if not region_mode and is_planar and num_channels == 2: + state.skip("Planar Erase benchmark does not support 2-channel layouts") + return None + if is_planar and input_kind == "VarShape" and num_channels == 1: + state.skip("Single-channel varshape Erase has no distinct planar image layout") + return None + if is_planar and input_kind == "VarShape" and dtype_str == "uchar4": + state.skip("RGBA8p varshape is unsupported by the Python image API") + return None + + dtype_size = get_dtype_size(dtype_str) + image_bytes = N * H * W * dtype_size + region_height = max(H // 4, 1) + region_width = max(W // 4, 1) + param_bytes = ( + num_channels * region_height * region_width * 4 + if region_mode + else num_erase * (8 + 12 + num_channels * 4 + 4) + ) + if is_fake_planar: + state.add_global_memory_reads(3 * image_bytes + param_bytes) + state.add_global_memory_writes(3 * image_bytes) + else: + state.add_global_memory_reads(image_bytes + param_bytes) + state.add_global_memory_writes(image_bytes) + + anchor = cvcuda.Tensor((num_erase,), cvcuda.Type._2S32, "N") + erasing = cvcuda.Tensor((num_erase,), cvcuda.Type._3S32, "N") + erase_mask = (1 << num_channels) - 1 + _fill_i32_tensor(anchor, [[0, 0]] * num_erase, device_id) + _fill_i32_tensor(erasing, [[10, 10, erase_mask]] * num_erase, device_id) + + values = create_tensor( + (num_erase * num_channels,), + cvcuda.Type.F32, + device_id, + layout="N", + fill_mode=1.0, + ) + imgIdx = create_tensor( + (num_erase,), cvcuda.Type.S32, device_id, layout="N", fill_mode=0 + ) + region_values = create_tensor( + (num_channels, region_height, region_width), + cvcuda.Type.F32, + device_id, + layout="CHW", + fill_mode=1.0, + ) + + get_stream = create_stream_cache() + + if is_fake_planar: + src = create_tensor( + (N, num_channels, H, W), + dtype, + device_id, + layout="NCHW", + fill_mode="checkerboard", + ) + inter_src = create_tensor( + (N, H, W, num_channels), dtype, device_id, layout="NHWC", fill_mode=0 + ) + inter_dst = create_tensor( + (N, H, W, num_channels), dtype, device_id, layout="NHWC", fill_mode=0 + ) + dst = create_tensor( + (N, num_channels, H, W), dtype, device_id, layout="NCHW", fill_mode=0 + ) + + def run_fake(launch): + stream = get_stream(launch) + cvcuda.reformat_into(inter_src, src, stream=stream) + if region_mode: + cvcuda.erase_into( + inter_dst, + inter_src, + 0, + 0, + region_height, + region_width, + region_values, + stream=stream, + ) + else: + cvcuda.erase_into( + inter_dst, + inter_src, + anchor, + erasing, + values, + imgIdx, + random=random, + seed=seed, + stream=stream, + ) + cvcuda.reformat_into(dst, inter_dst, stream=stream) + + return run_fake + + if input_kind == "Tensor": # Tensor mode + tensor_shape = (N, num_channels, H, W) if is_planar else (N, H, W, num_channels) + src = create_tensor( + tensor_shape, + dtype, + device_id, + layout=layout, + fill_mode="checkerboard", + ) + dst = create_tensor( + tensor_shape, + dtype, + device_id, + layout=layout, + fill_mode=0, + ) + else: # ImageBatchVarShape mode + img_format = get_format_from_dtype(dtype_str, num_channels, planar=is_planar) + src = create_image_batch_varshape( + (N, H, W, num_channels), + 0, + img_format, + dtype, + device_id, + fill_mode="checkerboard", + ) + dst = create_image_batch_varshape( + (N, H, W, num_channels), + 0, + img_format, + dtype, + device_id, + fill_mode=0, + ) + + def run(launch): + stream = get_stream(launch) + if region_mode: + cvcuda.erase_into( + dst, + src, + 0, + 0, + region_height, + region_width, + region_values, + stream=stream, + ) + else: + cvcuda.erase_into( + dst, + src, + anchor, + erasing, + values, + imgIdx, + random=random, + seed=seed, + stream=stream, + ) + + return run + + +if __name__ == "__main__": + run_benchmark("erase", erase) diff --git a/bench/python/ops/bench_findhomography.py b/bench/python/ops/bench_findhomography.py new file mode 100755 index 000000000..d5ca53297 --- /dev/null +++ b/bench/python/ops/bench_findhomography.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""CV-CUDA FindHomography operator benchmark - Python equivalent of BenchFindHomography.cpp""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + create_stream_cache, + get_input_kind, + parse_shape, + run_benchmark, +) + +try: + import cupy as cp +except ImportError: + import numpy as cp + + +def fill_src_grid_replicated(num_samples, num_points, dtype=cp.float32): + """Create a deterministic 2D point grid replicated across all batches.""" + grid_side = 1 + while grid_side * grid_side < num_points: + grid_side += 1 + + indices = cp.arange(num_points, dtype=cp.int32) + grid_scale = dtype(2.0) / dtype(grid_side - 1) + x = dtype(-1.0) + (indices % grid_side).astype(dtype) * grid_scale + y = dtype(-1.0) + (indices // grid_side).astype(dtype) * grid_scale + single_sample = cp.stack([x, y], axis=-1).reshape(-1) + return cp.tile(single_sample, num_samples) + + +def fill_dst_projective_replicated(src_vec, transform, num_samples, num_points): + """Apply a valid projective transform and replicate the result.""" + sample_size = num_points * 2 + single_sample = src_vec[:sample_size].reshape(num_points, 2) + x = single_sample[:, 0] + y = single_sample[:, 1] + + h00, h01, h02 = transform[0], transform[1], transform[2] + h10, h11, h12 = transform[3], transform[4], transform[5] + h20, h21, h22 = transform[6], transform[7], transform[8] + + # For x,y in [-1,1], this fixed transform keeps w in [0.965,1.035]. + w = h20 * x + h21 * y + h22 + x_transformed = (h00 * x + h01 * y + h02) / w + y_transformed = (h10 * x + h11 * y + h12) / w + + single_dst = cp.stack([x_transformed, y_transformed], axis=-1).reshape(-1) + return cp.tile(single_dst, num_samples) + + +def fill_tensor(tensor, vec, device_id): + """Copy host vector to device tensor""" + with cp.cuda.Device(device_id): + if not isinstance(vec, cp.ndarray): + vec = cp.array(vec) + + tensor_data = tensor.cuda() + cp.cuda.runtime.memcpy( + tensor_data.__cuda_array_interface__["data"][0], + vec.data.ptr, + vec.nbytes, + cp.cuda.runtime.memcpyDeviceToDevice, + ) + + +def findhomography(state): + """FindHomography operator benchmark matching C++ BenchFindHomography.cpp""" + + N, num_points = parse_shape(state.get_string("shape")) + input_kind = get_input_kind(state.get_string("inputKind")) + device_id = state.get_device() + + state.add_global_memory_reads(N * num_points * 4 * 4) + state.add_global_memory_writes(N * 3 * 3 * 4) + + transform = cp.array( + [1.05, 0.08, 0.15, -0.04, 0.97, -0.10, 0.015, -0.020, 1.0], + dtype=cp.float32, + ) + + get_stream = create_stream_cache() + + if input_kind == "Tensor": + src = cvcuda.Tensor((N, num_points), cvcuda.Type._2F32, "NW") + dst = cvcuda.Tensor((N, num_points), cvcuda.Type._2F32, "NW") + models = cvcuda.Tensor((N, 3, 3), cvcuda.Type.F32, "NHW") + + src_vec = fill_src_grid_replicated(N, num_points) + dst_vec = fill_dst_projective_replicated(src_vec, transform, N, num_points) + fill_tensor(src, src_vec, device_id) + fill_tensor(dst, dst_vec, device_id) + + op = cvcuda.get_findhomography_operator(N, num_points) + + def run(launch): + stream = get_stream(launch) + cvcuda.findhomography_into_with_op(models, src, dst, op, stream=stream) + + return run + + # FindHomography's variable-shape API uses TensorBatch rather than + # ImageBatchVarShape. Each point tensor is one sample in the batch. + src_batch = cvcuda.TensorBatch(N) + dst_batch = cvcuda.TensorBatch(N) + models_batch = cvcuda.TensorBatch(N) + + src_vec = fill_src_grid_replicated(1, num_points) + dst_vec = fill_dst_projective_replicated(src_vec, transform, 1, num_points) + for _ in range(N): + src = cvcuda.Tensor((1, num_points), cvcuda.Type._2F32, "NW") + dst = cvcuda.Tensor((1, num_points), cvcuda.Type._2F32, "NW") + model = cvcuda.Tensor((1, 3, 3), cvcuda.Type.F32, "NHW") + fill_tensor(src, src_vec, device_id) + fill_tensor(dst, dst_vec, device_id) + src_batch.pushback(src) + dst_batch.pushback(dst) + models_batch.pushback(model) + + def run(launch): + stream = get_stream(launch) + cvcuda.findhomography_into( + models=models_batch, + srcPts=src_batch, + dstPts=dst_batch, + stream=stream, + ) + + return run + + +if __name__ == "__main__": + run_benchmark("findhomography", findhomography) diff --git a/bench/python/ops/bench_flip.py b/bench/python/ops/bench_flip.py new file mode 100755 index 000000000..8d35b3709 --- /dev/null +++ b/bench/python/ops/bench_flip.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""CV-CUDA Flip operator benchmark - Python equivalent of BenchFlip.cpp""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + get_input_kind, + parse_shape, + get_dtype, + get_dtype_size, + get_num_channels, + get_format_from_dtype, + create_tensor, + create_image_batch_varshape, + create_stream_cache, + run_benchmark, +) + + +def flip(state): + """Flip operator benchmark matching C++ BenchFlip.cpp""" + + N, H, W = parse_shape(state.get_string("shape")) + dtype_str = state.get_string("InOutDataType") + dtype = get_dtype(dtype_str) + num_channels = get_num_channels(dtype_str) + try: + layout = state.get_string("layout") + except Exception: + layout = "NHWC" + flip_type = state.get_string("flipType") + input_kind = get_input_kind(state.get_string("inputKind")) + device_id = state.get_device() + + flip_code_map = {"HORIZONTAL": 0, "VERTICAL": 1, "BOTH": -1} + flip_code = flip_code_map[flip_type] + + if layout not in ("NHWC", "NCHW", "NCHW_FAKE"): + state.skip("Flip benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts") + return None + + is_planar = layout == "NCHW" + is_fake_planar = layout == "NCHW_FAKE" + if is_fake_planar and input_kind == "VarShape": + state.skip("Fake-planar (NCHW_FAKE) flip benchmark is tensor-only") + return None + + # Use the dtype string so vector types (uchar3/float4/...) count their + # channels, matching C++ sizeof(T) in BenchFlip.cpp. Flip preserves size. + dtype_size = get_dtype_size(dtype_str) + bytes_ = N * H * W * dtype_size + if is_fake_planar: + # reformat(NCHW->NHWC) + flip + reformat(NHWC->NCHW) + state.add_global_memory_reads(3 * bytes_) + state.add_global_memory_writes(3 * bytes_) + else: + state.add_global_memory_reads(bytes_) + state.add_global_memory_writes(bytes_) + + get_stream = create_stream_cache() + + if is_fake_planar: + # Tensor-only "fake planar": planar->interleaved->flip->interleaved->planar, + # all timed, as the comparison baseline for the native planar (NCHW) path. + src = create_tensor( + (N, num_channels, H, W), + dtype, + device_id, + layout="NCHW", + fill_mode="checkerboard", + ) + inter_src = create_tensor( + (N, H, W, num_channels), dtype, device_id, layout="NHWC", fill_mode=0 + ) + inter_dst = create_tensor( + (N, H, W, num_channels), dtype, device_id, layout="NHWC", fill_mode=0 + ) + dst = create_tensor( + (N, num_channels, H, W), dtype, device_id, layout="NCHW", fill_mode=0 + ) + + def run_fake(launch): + stream = get_stream(launch) + cvcuda.reformat_into(inter_src, src, stream=stream) + cvcuda.flip_into(inter_dst, inter_src, flipCode=flip_code, stream=stream) + cvcuda.reformat_into(dst, inter_dst, stream=stream) + + return run_fake + + if input_kind == "Tensor": # Tensor mode + shape = (N, num_channels, H, W) if is_planar else (N, H, W, num_channels) + src = create_tensor( + shape, dtype, device_id, layout=layout, fill_mode="checkerboard" + ) + dst = create_tensor(shape, dtype, device_id, layout=layout, fill_mode=0) + else: # ImageBatchVarShape mode + flip_code = create_tensor( + (N,), cvcuda.Type.S32, device_id, layout="N", fill_mode=flip_code + ) + # Select the multi-channel image format from the dtype string so the + # varshape benchmark moves the same bytes as C++ FillImageBatch. + img_format = get_format_from_dtype(dtype_str, num_channels, planar=is_planar) + src = create_image_batch_varshape( + (N, H, W, num_channels), + 0, + img_format, + dtype, + device_id, + fill_mode="checkerboard", + ) + dst = create_image_batch_varshape( + (N, H, W, num_channels), + 0, + img_format, + dtype, + device_id, + fill_mode=0, + ) + + def run(launch): + cvcuda.flip_into(dst, src, flipCode=flip_code, stream=get_stream(launch)) + + return run + + +if __name__ == "__main__": + run_benchmark("flip", flip) diff --git a/bench/python/ops/bench_gammacontrast.py b/bench/python/ops/bench_gammacontrast.py new file mode 100755 index 000000000..b3be71a44 --- /dev/null +++ b/bench/python/ops/bench_gammacontrast.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""CV-CUDA GammaContrast operator benchmark - Python equivalent of BenchGammaContrast.cpp""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + parse_shape, + get_dtype, + get_num_channels, + get_dtype_size, + get_format_from_dtype, + create_image_batch_varshape, + create_tensor, + create_stream_cache, + run_benchmark, +) + + +def gammacontrast(state): + """GammaContrast operator benchmark matching C++ BenchGammaContrast.cpp""" + + N, H, W = parse_shape(state.get_string("shape")) + dtype_str = state.get_string("InOutDataType") + dtype = get_dtype(dtype_str) + input_kind = state.get_string("inputKind") + try: + layout = state.get_string("layout") + except (KeyError, RuntimeError): + layout = "NHWC" + + # ScalarGamma benches the host-scalar gamma/gain overload: same dense-tensor input as + # Tensor, but gamma/gain are kernel launch arguments -- no device gamma tensor is staged. + if input_kind not in ("Tensor", "VarShape", "ScalarGamma"): + state.skip( + "GammaContrast benchmark supports only Tensor, VarShape, and ScalarGamma" + " input kinds" + ) + return None + is_scalar_gamma = input_kind == "ScalarGamma" + + if layout not in ("NHWC", "NCHW", "NCHW_FAKE"): + state.skip( + "GammaContrast benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts" + ) + return None + + is_planar = layout == "NCHW" + is_fake_planar = layout == "NCHW_FAKE" + if is_fake_planar and input_kind != "Tensor": + state.skip("Fake-planar (NCHW_FAKE) GammaContrast benchmark is tensor-only") + return None + if is_planar and input_kind == "VarShape" and dtype_str == "uchar4": + state.skip("RGBA8p varshape is unsupported by the Python image API") + return None + + device_id = state.get_device() + + num_channels = get_num_channels(dtype_str) + dtype_size = get_dtype_size(dtype_str) + + image_bytes = N * H * W * dtype_size + gamma_bytes = 0 if is_scalar_gamma else N * num_channels * 4 + if is_fake_planar: + state.add_global_memory_reads(3 * image_bytes + gamma_bytes) + state.add_global_memory_writes(3 * image_bytes) + else: + state.add_global_memory_reads(image_bytes + gamma_bytes) + state.add_global_memory_writes(image_bytes) + + # ScalarGamma passes the same 0.75 by value (gain 1.0) instead of staging this tensor. + if not is_scalar_gamma: + gamma = create_tensor( + (N * num_channels,), cvcuda.Type.F32, device_id, layout="N", fill_mode=0.75 + ) + + get_stream = create_stream_cache() + + if is_fake_planar: + src = create_tensor( + (N, num_channels, H, W), + dtype, + device_id, + layout="NCHW", + fill_mode="lcg", + ) + inter_src = create_tensor( + (N, H, W, num_channels), dtype, device_id, layout="NHWC", fill_mode=0 + ) + inter_dst = create_tensor( + (N, H, W, num_channels), dtype, device_id, layout="NHWC", fill_mode=0 + ) + dst = create_tensor( + (N, num_channels, H, W), dtype, device_id, layout="NCHW", fill_mode=0 + ) + + def run_fake(launch): + stream = get_stream(launch) + cvcuda.reformat_into(inter_src, src, stream=stream) + cvcuda.gamma_contrast_into(inter_dst, inter_src, gamma, stream=stream) + cvcuda.reformat_into(dst, inter_dst, stream=stream) + + return run_fake + + if input_kind in ("Tensor", "ScalarGamma"): # dense-tensor modes + tensor_shape = (N, num_channels, H, W) if is_planar else (N, H, W, num_channels) + src = create_tensor( + tensor_shape, + dtype, + device_id, + layout=layout, + fill_mode="lcg", + ) + dst = create_tensor(tensor_shape, dtype, device_id, layout=layout, fill_mode=0) + else: # ImageBatchVarShape mode + img_format = get_format_from_dtype(dtype_str, num_channels, planar=is_planar) + src = create_image_batch_varshape( + (N, H, W, num_channels), + 0, + img_format, + dtype_str, + device=device_id, + fill_mode="lcg", + ) + dst = create_image_batch_varshape( + (N, H, W, num_channels), + 0, + img_format, + dtype_str, + device=device_id, + fill_mode=0, + ) + + if is_scalar_gamma: + + def run(launch): + cvcuda.gamma_contrast_into(dst, src, 0.75, 1.0, stream=get_stream(launch)) + + else: + + def run(launch): + cvcuda.gamma_contrast_into(dst, src, gamma, stream=get_stream(launch)) + + return run + + +if __name__ == "__main__": + run_benchmark("gammacontrast", gammacontrast) diff --git a/bench/python/ops/bench_gaussian.py b/bench/python/ops/bench_gaussian.py new file mode 100755 index 000000000..c40de1f58 --- /dev/null +++ b/bench/python/ops/bench_gaussian.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""CV-CUDA Gaussian operator benchmark - Python equivalent of BenchGaussian.cpp""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + get_input_kind, + parse_shape, + get_dtype, + get_dtype_size, + get_num_channels, + get_format_from_dtype, + get_border_type, + create_tensor, + create_image_batch_varshape, + create_stream_cache, + run_benchmark, +) + + +def gaussian(state): + """Gaussian operator benchmark matching C++ BenchGaussian.cpp""" + + N, H, W = parse_shape(state.get_string("shape")) + dtype_str = state.get_string("InOutDataType") + sigma = state.get_float64("sigma") + border = get_border_type(state.get_string("border")) + input_kind = get_input_kind(state.get_string("inputKind")) + try: + layout = state.get_string("layout") + except (KeyError, RuntimeError): + layout = "NHWC" + device_id = state.get_device() + nc = get_num_channels(dtype_str) + + if layout not in ("NHWC", "NCHW", "NCHW_FAKE"): + state.skip("Gaussian benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts") + return None + + is_planar = layout == "NCHW" + is_fake_planar = layout == "NCHW_FAKE" + if is_fake_planar and input_kind == "VarShape": + state.skip("Fake-planar (NCHW_FAKE) Gaussian benchmark is tensor-only") + return None + + mult = 3 if get_dtype(dtype_str) == cvcuda.Type.U8 else 4 + kernel_size = round(sigma * mult * 2 + 1) + if kernel_size % 2 == 0: + kernel_size += 1 + + dtype_size = get_dtype_size(dtype_str) + bytes_ = N * H * W * dtype_size + if is_fake_planar: + state.add_global_memory_reads(3 * bytes_) + state.add_global_memory_writes(3 * bytes_) + else: + state.add_global_memory_reads(bytes_) + state.add_global_memory_writes(bytes_) + + get_stream = create_stream_cache() + + if is_fake_planar: + src = create_tensor( + (N, nc, H, W), dtype_str, device_id, layout="NCHW", fill_mode="checkerboard" + ) + inter_src = create_tensor( + (N, H, W, nc), dtype_str, device_id, layout="NHWC", fill_mode=0 + ) + inter_dst = create_tensor( + (N, H, W, nc), dtype_str, device_id, layout="NHWC", fill_mode=0 + ) + dst = create_tensor( + (N, nc, H, W), dtype_str, device_id, layout="NCHW", fill_mode=0 + ) + + def run_fake(launch): + stream = get_stream(launch) + cvcuda.reformat_into(inter_src, src, stream=stream) + cvcuda.gaussian_into( + inter_dst, + inter_src, + kernel_size=(kernel_size, kernel_size), + sigma=(sigma, sigma), + border=border, + stream=stream, + ) + cvcuda.reformat_into(dst, inter_dst, stream=stream) + + return run_fake + + if input_kind == "Tensor": # Tensor mode + tensor_shape = (N, nc, H, W) if is_planar else (N, H, W, nc) + src = create_tensor( + tensor_shape, dtype_str, device_id, layout=layout, fill_mode="checkerboard" + ) + dst = create_tensor( + tensor_shape, dtype_str, device_id, layout=layout, fill_mode=0 + ) + + def run(launch): + cvcuda.gaussian_into( + dst, + src, + kernel_size=(kernel_size, kernel_size), + sigma=(sigma, sigma), + border=border, + stream=get_stream(launch), + ) + + return run + else: # ImageBatchVarShape mode + if is_planar and get_dtype(dtype_str) == cvcuda.Type.U8 and nc == 4: + state.skip( + "Gaussian RGBA8p planar var-shape is unsupported by the Python image API" + ) + return None + + img_format = get_format_from_dtype(dtype_str, nc, planar=is_planar) + + src = create_image_batch_varshape( + (N, H, W, nc), + 0, + img_format, + dtype_str, + device=device_id, + fill_mode="checkerboard", + ) + dst = create_image_batch_varshape( + (N, H, W, nc), 0, img_format, dtype_str, device=device_id, fill_mode=0 + ) + kernel_size_tensor = create_tensor( + (N, 2), + cvcuda.Type.S32, + device_id, + layout="NC", + fill_mode=(kernel_size, kernel_size), + ) + sigma_tensor = create_tensor( + (N, 2), cvcuda.Type.F64, device_id, layout="NC", fill_mode=(sigma, sigma) + ) + + def run(launch): + cvcuda.gaussian_into( + src=src, + dst=dst, + max_kernel_size=(kernel_size, kernel_size), + kernel_size=kernel_size_tensor, + sigma=sigma_tensor, + border=border, + stream=get_stream(launch), + ) + + return run + + +if __name__ == "__main__": + run_benchmark("gaussian", gaussian) diff --git a/bench/python/ops/bench_gaussiannoise.py b/bench/python/ops/bench_gaussiannoise.py new file mode 100755 index 000000000..cef87c131 --- /dev/null +++ b/bench/python/ops/bench_gaussiannoise.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""CV-CUDA GaussianNoise operator benchmark - Python equivalent of BenchGaussianNoise.cpp""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + get_input_kind, + parse_shape, + get_dtype_size, + get_num_channels, + get_format_from_dtype, + create_tensor, + create_image_batch_varshape, + create_stream_cache, + run_benchmark, +) + + +def gaussiannoise(state): + """GaussianNoise operator benchmark matching C++ BenchGaussianNoise.cpp""" + + N, H, W = parse_shape(state.get_string("shape")) + dtype_str = state.get_string("InOutDataType") + input_kind = get_input_kind(state.get_string("inputKind")) + try: + layout = state.get_string("layout") + except (KeyError, RuntimeError): + layout = "NHWC" + device_id = state.get_device() + + num_channels = get_num_channels(dtype_str) + per_channel = num_channels > 1 + seed = 12345 + + if layout not in ("NHWC", "NCHW", "NCHW_FAKE"): + state.skip( + "GaussianNoise benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts" + ) + return None + + is_planar = layout == "NCHW" + is_fake_planar = layout == "NCHW_FAKE" + if is_fake_planar and input_kind == "VarShape": + state.skip("Fake-planar (NCHW_FAKE) GaussianNoise benchmark is tensor-only") + return None + + dtype_size = get_dtype_size(dtype_str) + bytes_ = N * H * W * dtype_size + state.add_global_memory_reads((3 if is_fake_planar else 1) * bytes_) + state.add_global_memory_writes((3 if is_fake_planar else 1) * bytes_) + + mu = create_tensor((N,), "float32", device_id, layout="N", fill_mode=0.5) + sigma = create_tensor((N,), "float32", device_id, layout="N", fill_mode=0.075) + get_stream = create_stream_cache() + + if is_fake_planar: + src = create_tensor( + (N, num_channels, H, W), + dtype_str, + device_id, + layout="NCHW", + fill_mode="lcg", + ) + inter_src = create_tensor( + (N, H, W, num_channels), dtype_str, device_id, layout="NHWC", fill_mode=0 + ) + inter_dst = create_tensor( + (N, H, W, num_channels), dtype_str, device_id, layout="NHWC", fill_mode=0 + ) + dst = create_tensor( + (N, num_channels, H, W), dtype_str, device_id, layout="NCHW", fill_mode=0 + ) + + def run_fake(launch): + stream = get_stream(launch) + cvcuda.reformat_into(inter_src, src, stream=stream) + cvcuda.gaussiannoise_into( + src=inter_src, + dst=inter_dst, + mu=mu, + sigma=sigma, + per_channel=per_channel, + seed=seed, + stream=stream, + ) + cvcuda.reformat_into(dst, inter_dst, stream=stream) + + return run_fake + + if input_kind == "Tensor": # Tensor mode + input_shape = (N, num_channels, H, W) if is_planar else (N, H, W, num_channels) + src = create_tensor( + input_shape, + dtype_str, + device_id, + layout="NCHW" if is_planar else "NHWC", + fill_mode="lcg", + ) + dst = create_tensor( + input_shape, + dtype_str, + device_id, + layout="NCHW" if is_planar else "NHWC", + fill_mode=0, + ) + else: # ImageBatchVarShape mode + img_format = get_format_from_dtype(dtype_str, num_channels, planar=is_planar) + src = create_image_batch_varshape( + (N, H, W, num_channels), + 0, + img_format, + dtype_str, + device_id, + fill_mode="lcg", + ) + dst = create_image_batch_varshape( + (N, H, W, num_channels), + 0, + img_format, + dtype_str, + device_id, + fill_mode=0, + ) + + def run(launch): + cvcuda.gaussiannoise_into( + src=src, + dst=dst, + mu=mu, + sigma=sigma, + per_channel=per_channel, + seed=seed, + stream=get_stream(launch), + ) + + return run + + +if __name__ == "__main__": + run_benchmark("gaussiannoise", gaussiannoise) diff --git a/bench/python/ops/bench_histogram.py b/bench/python/ops/bench_histogram.py new file mode 100755 index 000000000..0bc481f2e --- /dev/null +++ b/bench/python/ops/bench_histogram.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""CV-CUDA Histogram operator benchmark - Python equivalent of BenchHistogram.cpp""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + get_input_kind, + parse_shape, + get_dtype_size, + create_tensor, + create_stream_cache, + run_benchmark, +) + + +def histogram(state): + """Histogram operator benchmark matching C++ BenchHistogram.cpp""" + + N, H, W = parse_shape(state.get_string("shape")) + dtype_str = state.get_string("InOutDataType") + input_kind = get_input_kind(state.get_string("inputKind")) + mask_mode = state.get_string("maskMode") + try: + layout = state.get_string("layout") + except (KeyError, RuntimeError): + layout = "NHWC" + + if input_kind == "VarShape": # ImageBatchVarShape mode + state.skip("ImageBatchVarShape not implemented for this benchmark") + return None + + if mask_mode not in ("none", "checkerboard"): + raise ValueError(f"Unsupported maskMode: {mask_mode}") + if layout not in ("NHWC", "NCHW", "NCHW_FAKE"): + state.skip( + "Histogram benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts" + ) + return None + + is_planar = layout == "NCHW" + is_fake_planar = layout == "NCHW_FAKE" + if (is_planar or is_fake_planar) and input_kind != "Tensor": + state.skip("Planar Histogram benchmark is tensor-only") + return None + + device_id = state.get_device() + + num_bins = 256 + + image_bytes = N * H * W * get_dtype_size(dtype_str) + mask_bytes = N * H * W + hist_bytes = N * num_bins * 4 + use_mask = mask_mode == "checkerboard" + if is_planar or is_fake_planar: + state.add_global_memory_reads( + 2 * image_bytes + (2 * mask_bytes if use_mask else 0) + ) + state.add_global_memory_writes( + image_bytes + (mask_bytes if use_mask else 0) + hist_bytes + ) + else: + state.add_global_memory_reads(image_bytes + (mask_bytes if use_mask else 0)) + state.add_global_memory_writes(hist_bytes) + + hist = create_tensor( + (N, num_bins, 1), cvcuda.Type.S32, device_id, layout="HWC", fill_mode=0 + ) + get_stream = create_stream_cache() + + if is_fake_planar: + src = create_tensor( + (N, 1, H, W), dtype_str, device_id, layout="NCHW", fill_mode="checkerboard" + ) + inter_src = create_tensor( + (N, H, W, 1), dtype_str, device_id, layout="NHWC", fill_mode=0 + ) + mask = None + inter_mask = None + if use_mask: + mask = create_tensor( + (N, 1, H, W), + cvcuda.Type.U8, + device_id, + layout="NCHW", + fill_mode="checkerboard", + ) + inter_mask = create_tensor( + (N, H, W, 1), cvcuda.Type.U8, device_id, layout="NHWC", fill_mode=0 + ) + + def run_fake(launch): + stream = get_stream(launch) + cvcuda.reformat_into(inter_src, src, stream=stream) + if mask is None: + cvcuda.histogram_into(histogram=hist, src=inter_src, stream=stream) + else: + cvcuda.reformat_into(inter_mask, mask, stream=stream) + cvcuda.histogram_into( + histogram=hist, src=inter_src, mask=inter_mask, stream=stream + ) + + return run_fake + + tensor_shape = (N, 1, H, W) if is_planar else (N, H, W, 1) + src = create_tensor( + tensor_shape, dtype_str, device_id, layout=layout, fill_mode="checkerboard" + ) + mask = None + if use_mask: + mask = create_tensor( + tensor_shape, + cvcuda.Type.U8, + device_id, + layout=layout, + fill_mode="checkerboard", + ) + + def run(launch): + if mask is None: + cvcuda.histogram_into( + histogram=hist, + src=src, + stream=get_stream(launch), + ) + else: + cvcuda.histogram_into( + histogram=hist, + src=src, + mask=mask, + stream=get_stream(launch), + ) + + return run + + +if __name__ == "__main__": + run_benchmark("histogram", histogram) diff --git a/bench/python/ops/bench_histogrameq.py b/bench/python/ops/bench_histogrameq.py new file mode 100755 index 000000000..ceadebab4 --- /dev/null +++ b/bench/python/ops/bench_histogrameq.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""CV-CUDA HistogramEq operator benchmark - Python equivalent of BenchHistogramEq.cpp""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + get_input_kind, + parse_shape, + get_dtype, + get_dtype_size, + create_tensor, + create_image_batch_varshape, + create_stream_cache, + get_num_channels, + get_format_from_dtype, + run_benchmark, +) + + +def histogrameq(state): + """HistogramEq operator benchmark matching C++ BenchHistogramEq.cpp""" + + N, H, W = parse_shape(state.get_string("shape")) + dtype_str = state.get_string("InOutDataType") + dtype = get_dtype(dtype_str) + num_channels = get_num_channels(dtype_str) + layout = state.get_string("layout") + input_kind = get_input_kind(state.get_string("inputKind")) + device_id = state.get_device() + + if layout not in ("NHWC", "NCHW", "NCHW_FAKE"): + state.skip( + "HistogramEq benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts" + ) + return None + + is_planar = layout == "NCHW" + is_fake_planar = layout == "NCHW_FAKE" + if is_fake_planar and input_kind == "VarShape": + state.skip("Fake-planar (NCHW_FAKE) HistogramEq benchmark is tensor-only") + return None + if is_planar and input_kind == "VarShape" and num_channels == 4: + state.skip( + "uchar4 planar var-shape HistogramEq benchmark is unsupported by the image API" + ) + return None + + dtype_size = get_dtype_size(dtype_str) + bytes_ = N * H * W * dtype_size + state.add_global_memory_reads((3 if is_fake_planar else 1) * bytes_) + state.add_global_memory_writes((3 if is_fake_planar else 1) * bytes_) + + get_stream = create_stream_cache() + + if is_fake_planar: + src = create_tensor( + (N, num_channels, H, W), + dtype, + device_id, + layout="NCHW", + fill_mode="lcg", + ) + inter_src = create_tensor( + (N, H, W, num_channels), dtype, device_id, layout="NHWC", fill_mode=0 + ) + inter_dst = create_tensor( + (N, H, W, num_channels), dtype, device_id, layout="NHWC", fill_mode=0 + ) + dst = create_tensor( + (N, num_channels, H, W), dtype, device_id, layout="NCHW", fill_mode=0 + ) + + def run_fake(launch): + stream = get_stream(launch) + cvcuda.reformat_into(inter_src, src, stream=stream) + cvcuda.histogrameq_into(inter_dst, inter_src, stream=stream) + cvcuda.reformat_into(dst, inter_dst, stream=stream) + + return run_fake + + if input_kind == "Tensor": # Tensor mode + shape = (N, num_channels, H, W) if is_planar else (N, H, W, num_channels) + src = create_tensor( + shape, + dtype, + device_id, + layout=layout, + fill_mode="lcg", + ) + dst = create_tensor( + shape, + dtype, + device_id, + layout=layout, + fill_mode=0, + ) + else: # ImageBatchVarShape mode + img_format = get_format_from_dtype(dtype_str, num_channels, planar=is_planar) + src = create_image_batch_varshape( + (N, H, W, num_channels), + 0, + img_format, + dtype_str, + device_id, + fill_mode="lcg", + ) + dst = create_image_batch_varshape( + (N, H, W, num_channels), + 0, + img_format, + dtype_str, + device_id, + fill_mode=0, + ) + + def run(launch): + cvcuda.histogrameq_into( + dst=dst, + src=src, + stream=get_stream(launch), + ) + + return run + + +if __name__ == "__main__": + run_benchmark("histogrameq", histogrameq) diff --git a/bench/python/ops/bench_hqresize.py b/bench/python/ops/bench_hqresize.py new file mode 100755 index 000000000..23f40992f --- /dev/null +++ b/bench/python/ops/bench_hqresize.py @@ -0,0 +1,230 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""CV-CUDA HQResize operator benchmark - Python equivalent of BenchHQResize.cpp""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + parse_shape, + get_dtype, + get_dtype_size, + get_resize_output_shape, + get_format_from_dtype, + get_interpolation_type, + create_tensor, + create_image_batch_varshape, + create_stream_cache, + run_benchmark, +) + + +def hqresize(state): + """HQResize operator benchmark matching C++ BenchHQResize.cpp""" + + N, H, W = parse_shape(state.get_string("shape")) + dtype_str = state.get_string("InOutDataType") + dtype = get_dtype(dtype_str) + resize_type = state.get_string("resizeType") + interp = get_interpolation_type(state.get_string("interpolation")) + antialias = bool(state.get_int64("antialias")) + input_kind = state.get_string("inputKind") + device_id = state.get_device() + + try: + layout = state.get_string("layout") + except Exception: + layout = "NHWC" + if layout not in ("NHWC", "NCHW", "NCHW_FAKE"): + state.skip("HQResize benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts") + return None + if input_kind not in ("Tensor", "VarShape", "TensorBatch"): + state.skip( + "HQResize benchmark supports only Tensor, VarShape, and TensorBatch input kinds" + ) + return None + planar = layout == "NCHW" + is_fake_planar = layout == "NCHW_FAKE" + # Channel count is an explicit axis (default 1): legacy single-channel NHWC profiles omit it, while + # the planar-comparison profiles (NHWC/NCHW/NCHW_FAKE) set numChannels=3 to compare at parity. + try: + channels = int(state.get_int64("numChannels")) + except Exception: + channels = 1 + + # NCHW_FAKE ("fake planar") is a tensor-only comparison path matching BenchHQResize.cpp. + if is_fake_planar and input_kind != "Tensor": + state.skip("Fake-planar (NCHW_FAKE) HQResize benchmark is tensor-only") + return None + + try: + _, dst_h, dst_w = get_resize_output_shape((N, H, W), resize_type) + except ValueError as error: + state.skip(str(error)) + return None + if dst_h >= H and dst_w >= W and (dst_h > H or dst_w > W) and antialias: + state.skip("Antialias is no-op for expanding") + return None + + dtype_size = get_dtype_size(dtype) + + get_stream = create_stream_cache() + + if is_fake_planar: # tensor-only: NCHW -> NHWC -> hqresize -> NHWC -> NCHW + src_bytes = N * H * W * channels * dtype_size + dst_bytes = N * dst_h * dst_w * channels * dtype_size + state.add_global_memory_reads(2 * src_bytes + dst_bytes) + state.add_global_memory_writes(src_bytes + 2 * dst_bytes) + + src = create_tensor( + (N, channels, H, W), + dtype, + device_id, + layout="NCHW", + fill_mode="checkerboard", + ) + inter_src = create_tensor( + (N, H, W, channels), dtype, device_id, layout="NHWC", fill_mode=0 + ) + inter_dst = create_tensor( + (N, dst_h, dst_w, channels), dtype, device_id, layout="NHWC", fill_mode=0 + ) + dst = create_tensor( + (N, channels, dst_h, dst_w), dtype, device_id, layout="NCHW", fill_mode=0 + ) + + def run_fake(launch): + stream = get_stream(launch) + cvcuda.reformat_into(inter_src, src, stream=stream) + cvcuda.hq_resize_into( + inter_dst, + inter_src, + min_interpolation=interp, + mag_interpolation=interp, + antialias=antialias, + stream=stream, + ) + cvcuda.reformat_into(dst, inter_dst, stream=stream) + + return run_fake + + if input_kind == "Tensor": + state.add_global_memory_reads(N * H * W * channels * dtype_size) + state.add_global_memory_writes(N * dst_h * dst_w * channels * dtype_size) + + src_shape = (N, channels, H, W) if planar else (N, H, W, channels) + dst_shape = ( + (N, channels, dst_h, dst_w) if planar else (N, dst_h, dst_w, channels) + ) + src = create_tensor( + src_shape, dtype, device_id, layout=layout, fill_mode="checkerboard" + ) + dst = create_tensor(dst_shape, dtype, device_id, layout=layout, fill_mode=0) + + def run(launch): + cvcuda.hq_resize_into( + dst, + src, + min_interpolation=interp, + mag_interpolation=interp, + antialias=antialias, + stream=get_stream(launch), + ) + + return run + elif input_kind == "VarShape": + if channels != 3: + state.skip( + "HQResize ImageBatchVarShape benchmark currently requires three channels" + ) + return None + + state.add_global_memory_reads(N * H * W * channels * dtype_size) + state.add_global_memory_writes(N * dst_h * dst_w * channels * dtype_size) + + img_format = get_format_from_dtype(dtype_str, channels, planar=planar) + src_batch = create_image_batch_varshape( + (N, H, W, channels), + 0, + img_format, + dtype=dtype, + device=device_id, + fill_mode="checkerboard", + ) + dst_batch = create_image_batch_varshape( + (N, dst_h, dst_w, channels), + 0, + img_format, + dtype=dtype, + device=device_id, + fill_mode=0, + ) + + def run_varshape(launch): + cvcuda.hq_resize_into( + dst_batch, + src_batch, + min_interpolation=interp, + mag_interpolation=interp, + antialias=antialias, + stream=get_stream(launch), + ) + + return run_varshape + else: # TensorBatch mode + state.add_global_memory_reads(N * H * W * channels * dtype_size) + state.add_global_memory_writes(N * dst_h * dst_w * channels * dtype_size) + + src_shape = (channels, H, W) if planar else (H, W, channels) + dst_shape = (channels, dst_h, dst_w) if planar else (dst_h, dst_w, channels) + batch_layout = "CHW" if planar else "HWC" + src_batch = cvcuda.TensorBatch(N) + dst_batch = cvcuda.TensorBatch(N) + for _ in range(N): + src_batch.pushback( + create_tensor( + src_shape, + dtype, + device_id, + layout=batch_layout, + fill_mode="checkerboard", + ) + ) + dst_batch.pushback( + create_tensor( + dst_shape, dtype, device_id, layout=batch_layout, fill_mode=0 + ) + ) + + def run(launch): + cvcuda.hq_resize_into( + dst_batch, + src_batch, + min_interpolation=interp, + mag_interpolation=interp, + antialias=antialias, + stream=get_stream(launch), + ) + + return run + + +if __name__ == "__main__": + run_benchmark("hqresize", hqresize) diff --git a/bench/python/ops/bench_inpaint.py b/bench/python/ops/bench_inpaint.py new file mode 100755 index 000000000..5872caf9b --- /dev/null +++ b/bench/python/ops/bench_inpaint.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""CV-CUDA Inpaint operator benchmark - Python equivalent of BenchInpaint.cpp""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + get_input_kind, + parse_shape, + get_dtype, + get_dtype_size, + get_num_channels, + get_format_from_dtype, + create_tensor, + create_image_batch_varshape, + create_stream_cache, + run_benchmark, +) + +try: + import cupy as cp +except ImportError: + import numpy as cp + + +def inpaint(state): + """Inpaint operator benchmark matching C++ BenchInpaint.cpp""" + + shape = parse_shape(state.get_string("shape")) + dtype_str = state.get_string("InOutDataType") + dtype = get_dtype(dtype_str) + input_kind = get_input_kind(state.get_string("inputKind")) + try: + layout = state.get_string("layout") + except Exception: + layout = "NHWC" + inpaint_radius = state.get_float64("inpaintRadius") + device_id = state.get_device() + + N, H, W = shape + + num_channels = get_num_channels(dtype_str) + + if layout not in ("NHWC", "NCHW", "NCHW_FAKE"): + state.skip("Inpaint benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts") + return None + + is_planar = layout == "NCHW" + is_fake_planar = layout == "NCHW_FAKE" + if is_fake_planar and input_kind == "VarShape": + state.skip("Fake-planar (NCHW_FAKE) Inpaint benchmark is tensor-only") + return None + + dtype_size = get_dtype_size(dtype_str) + image_bytes = N * H * W * dtype_size + mask_bytes = N * H * W + if is_fake_planar: + state.add_global_memory_reads(3 * image_bytes + mask_bytes) + state.add_global_memory_writes(3 * image_bytes) + else: + state.add_global_memory_reads(image_bytes + mask_bytes) + state.add_global_memory_writes(image_bytes) + + get_stream = create_stream_cache() + + if is_fake_planar: + src = create_tensor( + (N, num_channels, H, W), + dtype, + device_id, + layout="NCHW", + fill_mode="lcg", + ) + inter_src = create_tensor( + (N, H, W, num_channels), dtype, device_id, layout="NHWC", fill_mode=0 + ) + inter_dst = create_tensor( + (N, H, W, num_channels), dtype, device_id, layout="NHWC", fill_mode=0 + ) + dst = create_tensor( + (N, num_channels, H, W), dtype, device_id, layout="NCHW", fill_mode=0 + ) + + with cp.cuda.Device(device_id): + mask_data = cp.zeros((N, H, W, 1), dtype=cp.uint8) + mask_data[:, 1::2, :, :] = 1 + mask = cvcuda.as_tensor(mask_data, "NHWC") + + def run_fake(launch): + stream = get_stream(launch) + cvcuda.reformat_into(inter_src, src, stream=stream) + cvcuda.inpaint_into( + inter_dst, inter_src, mask, inpaint_radius, stream=stream + ) + cvcuda.reformat_into(dst, inter_dst, stream=stream) + + return run_fake + + if input_kind == "Tensor": + input_shape = (N, num_channels, H, W) if is_planar else (N, H, W, num_channels) + mask_shape = (N, H, W, 1) + + src = create_tensor( + input_shape, dtype, device_id, layout=layout, fill_mode="lcg" + ) + dst = create_tensor(input_shape, dtype, device_id, layout=layout, fill_mode=0) + + with cp.cuda.Device(device_id): + mask_data = cp.zeros(mask_shape, dtype=cp.uint8) + mask_data[:, 1::2, :, :] = 1 + mask = cvcuda.as_tensor(mask_data, "NHWC") + + def run(launch): + cvcuda.inpaint_into( + dst, src, mask, inpaint_radius, stream=get_stream(launch) + ) + + return run + else: # ImageBatchVarShape mode + if is_planar and dtype_str == "uchar4": + state.skip( + "uchar4 planar var-shape Inpaint benchmark is unsupported by the Python image API" + ) + return None + + img_format = get_format_from_dtype(dtype_str, num_channels, planar=is_planar) + + if num_channels == 1: + img_shape = (N, H, W) + else: + img_shape = (N, H, W, num_channels) + + src = create_image_batch_varshape( + img_shape, 0, img_format, dtype, device_id, fill_mode="lcg" + ) + dst = create_image_batch_varshape( + img_shape, 0, img_format, dtype, device_id, fill_mode=0 + ) + + with cp.cuda.Device(device_id): + mask = cvcuda.ImageBatchVarShape(N) + for i in range(N): + # Uniform image sizes (the var-shape benches run with zero size variation). + img_h, img_w = H, W + + mask_data = cp.zeros((img_h, img_w), dtype=cp.uint8) + mask_data[:, 1::2] = 1 + + img = cvcuda.as_image(mask_data, cvcuda.Format.U8) + mask.pushback(img) + + def run(launch): + cvcuda.inpaint_into( + dst, src, mask, inpaint_radius, stream=get_stream(launch) + ) + + return run + + +if __name__ == "__main__": + run_benchmark("inpaint", inpaint) diff --git a/bench/python/ops/bench_invert.py b/bench/python/ops/bench_invert.py new file mode 100755 index 000000000..15b332456 --- /dev/null +++ b/bench/python/ops/bench_invert.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""CV-CUDA Invert operator benchmark - Python equivalent of BenchInvert.cpp""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + get_input_kind, + parse_shape, + get_dtype, + get_dtype_size, + get_num_channels, + get_format_from_dtype, + create_tensor, + create_image_batch_varshape, + create_stream_cache, + run_benchmark, +) + + +def invert(state): + """Invert operator benchmark matching C++ BenchInvert.cpp""" + + N, H, W = parse_shape(state.get_string("shape")) + dtype_str = state.get_string("InOutDataType") + dtype = get_dtype(dtype_str) + num_channels = get_num_channels(dtype_str) + try: + layout = state.get_string("layout") + except Exception: + layout = "NHWC" + input_kind = get_input_kind(state.get_string("inputKind")) + device_id = state.get_device() + + if layout not in ("NHWC", "NCHW", "NCHW_FAKE"): + state.skip("Invert benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts") + return None + + is_planar = layout == "NCHW" + is_fake_planar = layout == "NCHW_FAKE" + if is_fake_planar and input_kind == "VarShape": + state.skip("Fake-planar (NCHW_FAKE) invert benchmark is tensor-only") + return None + + # Invert preserves size. + dtype_size = get_dtype_size(dtype_str) + bytes_ = N * H * W * dtype_size + if is_fake_planar: + # reformat(NCHW->NHWC) + invert + reformat(NHWC->NCHW) + state.add_global_memory_reads(3 * bytes_) + state.add_global_memory_writes(3 * bytes_) + else: + state.add_global_memory_reads(bytes_) + state.add_global_memory_writes(bytes_) + + get_stream = create_stream_cache() + + if is_fake_planar: + src = create_tensor( + (N, num_channels, H, W), + dtype, + device_id, + layout="NCHW", + fill_mode="checkerboard", + ) + inter_src = create_tensor( + (N, H, W, num_channels), dtype, device_id, layout="NHWC", fill_mode=0 + ) + inter_dst = create_tensor( + (N, H, W, num_channels), dtype, device_id, layout="NHWC", fill_mode=0 + ) + dst = create_tensor( + (N, num_channels, H, W), dtype, device_id, layout="NCHW", fill_mode=0 + ) + + def run_fake(launch): + stream = get_stream(launch) + cvcuda.reformat_into(inter_src, src, stream=stream) + cvcuda.invert_into(inter_dst, inter_src, stream=stream) + cvcuda.reformat_into(dst, inter_dst, stream=stream) + + return run_fake + + if input_kind == "Tensor": + shape = (N, num_channels, H, W) if is_planar else (N, H, W, num_channels) + src = create_tensor( + shape, dtype, device_id, layout=layout, fill_mode="checkerboard" + ) + dst = create_tensor(shape, dtype, device_id, layout=layout, fill_mode=0) + else: # ImageBatchVarShape + img_format = get_format_from_dtype(dtype_str, num_channels, planar=is_planar) + src = create_image_batch_varshape( + (N, H, W, num_channels), + 0, + img_format, + dtype, + device_id, + fill_mode="checkerboard", + ) + dst = create_image_batch_varshape( + (N, H, W, num_channels), 0, img_format, dtype, device_id, fill_mode=0 + ) + + def run(launch): + cvcuda.invert_into(dst, src, stream=get_stream(launch)) + + return run + + +if __name__ == "__main__": + run_benchmark("invert", invert) diff --git a/bench/python/ops/bench_jointbilateralfilter.py b/bench/python/ops/bench_jointbilateralfilter.py new file mode 100755 index 000000000..8075b4e20 --- /dev/null +++ b/bench/python/ops/bench_jointbilateralfilter.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""CV-CUDA JointBilateralFilter operator benchmark - Python equivalent of BenchJointBilateralFilter.cpp""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + get_input_kind, + parse_shape, + get_dtype, + get_num_channels, + get_dtype_size, + get_format_from_dtype, + get_border_type, + create_tensor, + create_image_batch_varshape, + create_stream_cache, + run_benchmark, +) + + +def jointbilateralfilter(state): + """JointBilateralFilter operator benchmark matching C++ BenchJointBilateralFilter.cpp""" + + N, H, W = parse_shape(state.get_string("shape")) + dtype_str = state.get_string("InOutDataType") + dtype = get_dtype(dtype_str) + num_channels = get_num_channels(dtype_str) + border = get_border_type(state.get_string("border")) + input_kind = get_input_kind(state.get_string("inputKind")) + try: + layout = state.get_string("layout") + except (KeyError, RuntimeError): + layout = "NHWC" + device_id = state.get_device() + + if layout not in ("NHWC", "NCHW", "NCHW_FAKE"): + state.skip( + "JointBilateralFilter benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts" + ) + return None + is_planar = layout == "NCHW" + is_fake_planar = layout == "NCHW_FAKE" + if is_fake_planar and input_kind == "VarShape": + state.skip( + "Fake-planar (NCHW_FAKE) JointBilateralFilter benchmark is tensor-only" + ) + return None + + diameter_scalar = int(state.get_int64("diameter")) + sigma_space_scalar = float(state.get_float64("sigmaSpace")) + sigma_color_scalar = -1.0 + + dtype_size = get_dtype_size(dtype_str) + state.add_global_memory_reads(N * H * W * dtype_size) + state.add_global_memory_writes(N * H * W * dtype_size) + + get_stream = create_stream_cache() + + def do_jointbilateralfilter( + dst, src, color, diameter, sigma_color, sigma_space, stream + ): + cvcuda.joint_bilateral_filter_into( + dst, + src, + color, + diameter, + sigma_color, + sigma_space, + border=border, + stream=stream, + ) + + if is_fake_planar: + src = create_tensor( + (N, num_channels, H, W), + dtype, + device_id, + layout="NCHW", + fill_mode="lcg", + ) + inter_src = create_tensor( + (N, H, W, num_channels), + dtype, + device_id, + layout="NHWC", + fill_mode=0, + ) + inter_dst = create_tensor( + (N, H, W, num_channels), dtype, device_id, layout="NHWC", fill_mode=0 + ) + dst = create_tensor( + (N, num_channels, H, W), + dtype, + device_id, + layout="NCHW", + fill_mode=0, + ) + + def run_fake(launch): + stream = get_stream(launch) + cvcuda.reformat_into(inter_src, src, stream=stream) + do_jointbilateralfilter( + inter_dst, + inter_src, + inter_src, + diameter_scalar, + sigma_color_scalar, + sigma_space_scalar, + stream, + ) + cvcuda.reformat_into(dst, inter_dst, stream=stream) + + return run_fake + + if input_kind == "Tensor": + tensor_shape = (N, num_channels, H, W) if is_planar else (N, H, W, num_channels) + src = create_tensor( + tensor_shape, + dtype, + device_id, + layout=layout, + fill_mode="lcg", + ) + color = create_tensor( + tensor_shape, + dtype, + device_id, + layout=layout, + fill_mode="lcg", + ) + dst = create_tensor( + tensor_shape, + dtype, + device_id, + layout=layout, + fill_mode=0, + ) + + def run(launch): + do_jointbilateralfilter( + dst, + src, + color, + diameter_scalar, + sigma_color_scalar, + sigma_space_scalar, + get_stream(launch), + ) + + return run + else: # ImageBatchVarShape mode + diameter = create_tensor( + (N,), cvcuda.Type.S32, device_id, layout="N", fill_mode=diameter_scalar + ) + sigma_space = create_tensor( + (N,), cvcuda.Type.F32, device_id, layout="N", fill_mode=sigma_space_scalar + ) + sigma_color = create_tensor( + (N,), cvcuda.Type.F32, device_id, layout="N", fill_mode=sigma_color_scalar + ) + + img_format = get_format_from_dtype(dtype_str, num_channels, planar=is_planar) + src = create_image_batch_varshape( + (N, H, W, num_channels), + 0, + img_format, + dtype, + device_id, + fill_mode="lcg", + ) + color = src + dst = create_image_batch_varshape( + (N, H, W, num_channels), + 0, + img_format, + dtype, + device_id, + fill_mode=0, + ) + + def run(launch): + do_jointbilateralfilter( + dst, + src, + color, + diameter, + sigma_color, + sigma_space, + get_stream(launch), + ) + + return run + + +if __name__ == "__main__": + run_benchmark("jointbilateralfilter", jointbilateralfilter) diff --git a/bench/python/ops/bench_jpegcompressiondistortion.py b/bench/python/ops/bench_jpegcompressiondistortion.py new file mode 100755 index 000000000..4338f31b3 --- /dev/null +++ b/bench/python/ops/bench_jpegcompressiondistortion.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""CV-CUDA Jpeg Compression Distortion operator benchmark - Python equivalent of +BenchJpegCompressionDistortion.cpp""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + get_input_kind, + parse_shape, + get_dtype, + get_dtype_size, + get_num_channels, + get_format_from_dtype, + create_tensor, + create_image_batch_varshape, + create_stream_cache, + run_benchmark, +) + +# Quality only scales the quantization tables, so it does not affect throughput; use a fixed +# mid-range value. +QUALITY = 50 + + +def jpegcompressiondistortion(state): + """Jpeg Compression Distortion benchmark matching C++ BenchJpegCompressionDistortion.cpp""" + + N, H, W = parse_shape(state.get_string("shape")) + dtype_str = state.get_string("InOutDataType") + dtype = get_dtype(dtype_str) + num_channels = get_num_channels(dtype_str) + try: + layout = state.get_string("layout") + except Exception: + layout = "NHWC" + input_kind = get_input_kind(state.get_string("inputKind")) + device_id = state.get_device() + + if layout not in ("NHWC", "NCHW", "NCHW_FAKE"): + state.skip( + "JpegCompressionDistortion benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts" + ) + return None + + is_planar = layout == "NCHW" + is_fake_planar = layout == "NCHW_FAKE" + if is_fake_planar and input_kind == "VarShape": + state.skip( + "Fake-planar (NCHW_FAKE) jpegcompressiondistortion benchmark is tensor-only" + ) + return None + + # JpegCompressionDistortion preserves size. + dtype_size = get_dtype_size(dtype_str) + bytes_ = N * H * W * dtype_size + if is_fake_planar: + # reformat(NCHW->NHWC) + distort + reformat(NHWC->NCHW) + state.add_global_memory_reads(3 * bytes_) + state.add_global_memory_writes(3 * bytes_) + else: + state.add_global_memory_reads(bytes_) + state.add_global_memory_writes(bytes_) + + get_stream = create_stream_cache() + + if is_fake_planar: + src = create_tensor( + (N, num_channels, H, W), + dtype, + device_id, + layout="NCHW", + fill_mode="checkerboard", + ) + inter_src = create_tensor( + (N, H, W, num_channels), dtype, device_id, layout="NHWC", fill_mode=0 + ) + inter_dst = create_tensor( + (N, H, W, num_channels), dtype, device_id, layout="NHWC", fill_mode=0 + ) + dst = create_tensor( + (N, num_channels, H, W), dtype, device_id, layout="NCHW", fill_mode=0 + ) + + def run_fake(launch): + stream = get_stream(launch) + cvcuda.reformat_into(inter_src, src, stream=stream) + cvcuda.jpeg_compression_distortion_into( + inter_dst, inter_src, QUALITY, stream=stream + ) + cvcuda.reformat_into(dst, inter_dst, stream=stream) + + return run_fake + + if input_kind == "Tensor": + shape = (N, num_channels, H, W) if is_planar else (N, H, W, num_channels) + src = create_tensor( + shape, dtype, device_id, layout=layout, fill_mode="checkerboard" + ) + dst = create_tensor(shape, dtype, device_id, layout=layout, fill_mode=0) + else: # ImageBatchVarShape + img_format = get_format_from_dtype(dtype_str, num_channels, planar=is_planar) + src = create_image_batch_varshape( + (N, H, W, num_channels), + 0, + img_format, + dtype, + device_id, + fill_mode="checkerboard", + ) + dst = create_image_batch_varshape( + (N, H, W, num_channels), 0, img_format, dtype, device_id, fill_mode=0 + ) + + def run(launch): + cvcuda.jpeg_compression_distortion_into( + dst, src, QUALITY, stream=get_stream(launch) + ) + + return run + + +if __name__ == "__main__": + run_benchmark("jpegcompressiondistortion", jpegcompressiondistortion) diff --git a/bench/python/ops/bench_label.py b/bench/python/ops/bench_label.py new file mode 100755 index 000000000..edf8277b5 --- /dev/null +++ b/bench/python/ops/bench_label.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""CV-CUDA Label operator benchmark - Python equivalent of BenchLabel.cpp""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + parse_shape, + get_dtype, + get_dtype_size, + create_tensor, + create_stream_cache, + run_benchmark, +) + + +def label(state): + """Label operator benchmark matching C++ BenchLabel.cpp""" + + N, H, W = parse_shape(state.get_string("shape")) + dtype = get_dtype(state.get_string("InOutDataType")) + runChoice = state.get_string("runChoice") + device_id = state.get_device() + try: + layout = state.get_string("layout") + except (KeyError, RuntimeError): + layout = "NHWC" + + if layout not in ("NHWC", "NCHW", "NCHW_FAKE"): + state.skip("Label benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts") + return None + + is_planar = layout == "NCHW" + is_fake_planar = layout == "NCHW_FAKE" + + if runChoice == "DEFAULT": + runChoice = "" + + dtype_size = get_dtype_size(dtype) + image_bytes = N * H * W * dtype_size + out_bytes = N * H * W * 4 + if is_fake_planar: + state.add_global_memory_reads(2 * image_bytes + out_bytes) + state.add_global_memory_writes(image_bytes + 2 * out_bytes) + else: + state.add_global_memory_reads(image_bytes) + state.add_global_memory_writes(out_bytes) + + tensor_shape = (N, 1, H, W) if is_planar or is_fake_planar else (N, H, W, 1) + tensor_layout = "NCHW" if is_planar or is_fake_planar else "NHWC" + + src = create_tensor( + tensor_shape, dtype, device_id, layout=tensor_layout, fill_mode="lcg" + ) + + dst = create_tensor( + tensor_shape, cvcuda.Type.S32, device_id, layout=tensor_layout, fill_mode=0 + ) + + bgLabel = None + minThresh = None + maxThresh = None + minSize = None + mask = None + + if "BG" in runChoice: + bgLabel = create_tensor((N,), dtype, device_id, layout="N", fill_mode="lcg") + if "MIN" in runChoice: + minThresh = create_tensor((N,), dtype, device_id, layout="N", fill_mode=64) + if "MAX" in runChoice: + maxThresh = create_tensor((N,), dtype, device_id, layout="N", fill_mode=192) + if "ISLAND" in runChoice: + minSize = create_tensor( + (N,), cvcuda.Type.S32, device_id, layout="N", fill_mode=16 + ) + if "MASK" in runChoice: + mask = create_tensor( + tensor_shape, + cvcuda.Type.U8, + device_id, + layout=tensor_layout, + fill_mode="checkerboard", + ) + + countTensor = None + statsTensor = None + + if "COUNT" in runChoice: + countTensor = create_tensor( + (N,), cvcuda.Type.S32, device_id, layout="N", fill_mode=0 + ) + if "STAT" in runChoice: + statsTensor = create_tensor( + (N, 10000, 7), cvcuda.Type.S32, device_id, layout="NMA", fill_mode=0 + ) + + conn = cvcuda.ConnectivityType.CONNECTIVITY_4_2D + alab = cvcuda.LABEL.FAST + mType = cvcuda.LabelMaskType.REMOVE_ISLANDS_OUTSIDE_MASK_ONLY + + get_stream = create_stream_cache() + + if is_fake_planar: + inter_src = create_tensor( + (N, H, W, 1), dtype, device_id, layout="NHWC", fill_mode=0 + ) + inter_dst = create_tensor( + (N, H, W, 1), cvcuda.Type.S32, device_id, layout="NHWC", fill_mode=0 + ) + inter_mask = None + if mask is not None: + inter_mask = create_tensor( + (N, H, W, 1), cvcuda.Type.U8, device_id, layout="NHWC", fill_mode=0 + ) + + def run_fake(launch): + stream = get_stream(launch) + cvcuda.reformat_into(inter_src, src, stream=stream) + if mask is not None: + cvcuda.reformat_into(inter_mask, mask, stream=stream) + cvcuda.label_into( + inter_dst, + countTensor, + statsTensor, + inter_src, + connectivity=conn, + assign_labels=alab, + mask_type=mType, + bg_label=bgLabel, + min_thresh=minThresh, + max_thresh=maxThresh, + min_size=minSize, + mask=inter_mask, + stream=stream, + ) + cvcuda.reformat_into(dst, inter_dst, stream=stream) + + return run_fake + + def run(launch): + cvcuda.label_into( + dst, + countTensor, + statsTensor, + src, + connectivity=conn, + assign_labels=alab, + mask_type=mType, + bg_label=bgLabel, + min_thresh=minThresh, + max_thresh=maxThresh, + min_size=minSize, + mask=mask, + stream=get_stream(launch), + ) + + return run + + +if __name__ == "__main__": + run_benchmark("label", label) diff --git a/bench/python/ops/bench_laplacian.py b/bench/python/ops/bench_laplacian.py new file mode 100755 index 000000000..dd3581dc4 --- /dev/null +++ b/bench/python/ops/bench_laplacian.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""CV-CUDA Laplacian operator benchmark - Python equivalent of BenchLaplacian.cpp""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + get_input_kind, + parse_shape, + get_dtype, + get_num_channels, + get_dtype_size, + get_format_from_dtype, + get_border_type, + create_tensor, + create_image_batch_varshape, + create_stream_cache, + run_benchmark, +) + + +def laplacian(state): + """Laplacian operator benchmark matching C++ BenchLaplacian.cpp""" + + N, H, W = parse_shape(state.get_string("shape")) + dtype_str = state.get_string("InOutDataType") + dtype = get_dtype(dtype_str) + num_channels = get_num_channels(dtype_str) + border = get_border_type(state.get_string("border")) + input_kind = get_input_kind(state.get_string("inputKind")) + device_id = state.get_device() + + ksize_scalar = int(state.get_int64("ksize")) + scale_scalar = float(state.get_float64("scale")) + try: + layout = state.get_string("layout") + except (KeyError, RuntimeError): + layout = "NHWC" + + if layout not in ("NHWC", "NCHW", "NCHW_FAKE"): + state.skip( + "Laplacian benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts" + ) + return None + is_planar = layout == "NCHW" + is_fake_planar = layout == "NCHW_FAKE" + if is_fake_planar and input_kind == "VarShape": + state.skip("Fake-planar (NCHW_FAKE) Laplacian benchmark is tensor-only") + return None + + dtype_size = get_dtype_size(dtype_str) + state.add_global_memory_reads(N * H * W * dtype_size) + state.add_global_memory_writes(N * H * W * dtype_size) + + get_stream = create_stream_cache() + + def do_laplacian(dst, src, stream): + cvcuda.laplacian_into( + dst, + src, + ksize=ksize_scalar, + scale=scale_scalar, + border=border, + stream=stream, + ) + + if is_fake_planar: + src = create_tensor( + (N, num_channels, H, W), + dtype, + device_id, + layout="NCHW", + fill_mode="checkerboard", + ) + inter_src = create_tensor( + (N, H, W, num_channels), + dtype, + device_id, + layout="NHWC", + fill_mode=0, + ) + inter_dst = create_tensor( + (N, H, W, num_channels), + dtype, + device_id, + layout="NHWC", + fill_mode=0, + ) + dst = create_tensor( + (N, num_channels, H, W), + dtype, + device_id, + layout="NCHW", + fill_mode=0, + ) + + def run_fake(launch): + stream = get_stream(launch) + cvcuda.reformat_into(inter_src, src, stream=stream) + do_laplacian(inter_dst, inter_src, stream) + cvcuda.reformat_into(dst, inter_dst, stream=stream) + + return run_fake + + if input_kind == "Tensor": + tensor_shape = (N, num_channels, H, W) if is_planar else (N, H, W, num_channels) + src = create_tensor( + tensor_shape, + dtype, + device_id, + layout=layout, + fill_mode="checkerboard", + ) + dst = create_tensor( + tensor_shape, + dtype, + device_id, + layout=layout, + fill_mode=0, + ) + + def run(launch): + do_laplacian(dst, src, get_stream(launch)) + + return run + else: # ImageBatchVarShape mode + img_format = get_format_from_dtype(dtype_str, num_channels, planar=is_planar) + src = create_image_batch_varshape( + (N, H, W, num_channels), + 0, + img_format, + dtype, + device_id, + fill_mode="checkerboard", + ) + dst = create_image_batch_varshape( + (N, H, W, num_channels), + 0, + img_format, + dtype, + device_id, + fill_mode=0, + ) + + ksize = create_tensor( + (N,), cvcuda.Type.S32, device_id, layout="N", fill_mode=ksize_scalar + ) + scale = create_tensor( + (N,), cvcuda.Type.F32, device_id, layout="N", fill_mode=scale_scalar + ) + + def run(launch): + cvcuda.laplacian_into( + dst, src, ksize, scale, border=border, stream=get_stream(launch) + ) + + return run + + +if __name__ == "__main__": + run_benchmark("laplacian", laplacian) diff --git a/bench/python/ops/bench_medianblur.py b/bench/python/ops/bench_medianblur.py new file mode 100755 index 000000000..2967bcff4 --- /dev/null +++ b/bench/python/ops/bench_medianblur.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""CV-CUDA MedianBlur operator benchmark - Python equivalent of BenchMedianBlur.cpp""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cupy as cp # noqa: E402 +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + get_input_kind, + parse_shape, + get_dtype, + get_num_channels, + get_dtype_size, + get_format_from_dtype, + create_image_batch_varshape, + create_stream_cache, + run_benchmark, +) + + +def medianblur(state): + """MedianBlur operator benchmark matching C++ BenchMedianBlur.cpp""" + + N, H, W = parse_shape(state.get_string("shape")) + dtype_str = state.get_string("InOutDataType") + dtype = get_dtype(dtype_str) + num_channels = get_num_channels(dtype_str) + ksize_str = state.get_string("kernelSize") + input_kind = get_input_kind(state.get_string("inputKind")) + try: + layout = state.get_string("layout") + except (KeyError, RuntimeError): + layout = "NHWC" + device_id = state.get_device() + + ksize = parse_shape(ksize_str) + + if layout not in ("NHWC", "NCHW", "NCHW_FAKE"): + state.skip( + "MedianBlur benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts" + ) + return None + + is_planar = layout == "NCHW" + is_fake_planar = layout == "NCHW_FAKE" + if is_fake_planar and input_kind == "VarShape": + state.skip("Fake-planar (NCHW_FAKE) MedianBlur benchmark is tensor-only") + return None + + dtype_size = get_dtype_size(dtype_str) + bytes_ = N * H * W * dtype_size + if is_fake_planar: + state.add_global_memory_reads(3 * bytes_) + state.add_global_memory_writes(3 * bytes_) + else: + state.add_global_memory_reads(bytes_) + state.add_global_memory_writes(bytes_) + + get_stream = create_stream_cache() + + cupy_dtype_map = { + cvcuda.Type.U8: cp.uint8, + cvcuda.Type.U16: cp.uint16, + cvcuda.Type.F32: cp.float32, + } + cupy_dtype = cupy_dtype_map.get(dtype, cp.float32) + is_float = dtype == cvcuda.Type.F32 + + if is_fake_planar: + with cp.cuda.Device(device_id): + gradient = (255 - cp.arange(W, dtype=cp.int32) % 256).astype(cupy_dtype) + if is_float: + gradient = gradient / 255.0 + src_data = cp.broadcast_to( + gradient.reshape(1, 1, 1, W), (N, num_channels, H, W) + ).copy() + src = cvcuda.as_tensor(src_data, "NCHW") + + inter_src_data = cp.zeros((N, H, W, num_channels), dtype=cupy_dtype) + inter_src = cvcuda.as_tensor(inter_src_data, "NHWC") + inter_dst_data = cp.zeros((N, H, W, num_channels), dtype=cupy_dtype) + inter_dst = cvcuda.as_tensor(inter_dst_data, "NHWC") + + dst_data = cp.zeros((N, num_channels, H, W), dtype=cupy_dtype) + dst = cvcuda.as_tensor(dst_data, "NCHW") + + def run_fake(launch): + stream = get_stream(launch) + cvcuda.reformat_into(inter_src, src, stream=stream) + cvcuda.median_blur_into(inter_dst, inter_src, ksize, stream=stream) + cvcuda.reformat_into(dst, inter_dst, stream=stream) + + return run_fake + + if input_kind == "Tensor": # Tensor mode + with cp.cuda.Device(device_id): + gradient = (255 - cp.arange(W, dtype=cp.int32) % 256).astype(cupy_dtype) + if is_float: + gradient = gradient / 255.0 + if is_planar: + src_data = cp.broadcast_to( + gradient.reshape(1, 1, 1, W), (N, num_channels, H, W) + ).copy() + src = cvcuda.as_tensor(src_data, "NCHW") + + dst_data = cp.zeros((N, num_channels, H, W), dtype=cupy_dtype) + dst = cvcuda.as_tensor(dst_data, "NCHW") + else: + src_data = cp.broadcast_to( + gradient.reshape(1, 1, W, 1), (N, H, W, num_channels) + ).copy() + src = cvcuda.as_tensor(src_data, "NHWC") + + dst_data = cp.zeros((N, H, W, num_channels), dtype=cupy_dtype) + dst = cvcuda.as_tensor(dst_data, "NHWC") + + def run(launch): + cvcuda.median_blur_into(dst, src, ksize, stream=get_stream(launch)) + + return run + + else: # ImageBatchVarShape mode + img_format = get_format_from_dtype(dtype_str, num_channels, planar=is_planar) + src = create_image_batch_varshape( + (N, H, W, num_channels), + 0, + img_format, + dtype, + device_id, + fill_mode="gradient_h", + ) + dst = create_image_batch_varshape( + (N, H, W, num_channels), + 0, + img_format, + dtype, + device_id, + fill_mode=0, + ) + + with cp.cuda.Device(device_id): + ksize_data = cp.tile(cp.array(ksize, dtype=cp.int32), (N, 1)) + ksize_tensor = cvcuda.as_tensor(ksize_data, "NC") + + def run(launch): + cvcuda.median_blur_into(dst, src, ksize_tensor, stream=get_stream(launch)) + + return run + + +if __name__ == "__main__": + run_benchmark("medianblur", medianblur) diff --git a/bench/python/ops/bench_minarearect.py b/bench/python/ops/bench_minarearect.py new file mode 100755 index 000000000..c90cdc834 --- /dev/null +++ b/bench/python/ops/bench_minarearect.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""MinAreaRect operator benchmark matching C++ BenchMinAreaRect.cpp""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cupy as cp # noqa: E402 +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + get_input_kind, + parse_shape, + get_dtype, + get_dtype_size, + create_stream_cache, + run_benchmark, + _lcg_fill, + _LCG_SEED, +) + + +def minarearect(state): + """MinAreaRect operator benchmark matching C++ BenchMinAreaRect.cpp""" + + shape_str = state.get_string("shape") + input_kind = get_input_kind(state.get_string("inputKind")) + dtype_str = state.get_string("InOutDataType") + num_points_axis = state.get_int64("numPoints") + device_id = state.get_device() + + shape = parse_shape(shape_str) + N = shape[0] + max_points = shape[1] + + if num_points_axis < 0 or num_points_axis > max_points: + raise ValueError("numPoints must be 0 or in [1, max_points]") + + dtype = get_dtype(dtype_str) + cupy_dtype_map = { + cvcuda.Type.U16: cp.uint16, + cvcuda.Type.S16: cp.int16, + cvcuda.Type.S32: cp.int32, + } + cupy_dtype = cupy_dtype_map.get(dtype) + if cupy_dtype is None: + raise ValueError(f"Unsupported MinAreaRect dtype: {dtype_str}") + + dtype_size = get_dtype_size(dtype_str) + state.add_global_memory_reads(N * max_points * dtype_size) + state.add_global_memory_writes(N * 8 * 4 + N * 4) + + get_stream = create_stream_cache() + + if input_kind == "Tensor": + with cp.cuda.Device(device_id): + # src: deterministic LCG over the full dtype range, matching the + # C++ LcgValues() GPU fast path. + src_data = cp.empty((N, max_points, 2), dtype=cupy_dtype) + _lcg_fill(src_data, _LCG_SEED) + src = cvcuda.as_tensor(src_data, "NWC") + + dst_data = cp.zeros((N, 8), dtype=cp.float32) + dst = cvcuda.as_tensor(dst_data, "NW") + + # numPoints=0 preserves the original deterministic [10, 100] cycle. + if num_points_axis > 0: + points_data = cp.full((1, N), num_points_axis, dtype=cp.int32) + else: + points_data = ( + cp.arange(N, dtype=cp.int32).reshape(1, N) % 91 + 10 + ).astype(cp.int32) + num_points = cvcuda.as_tensor(points_data, "NW") + + def run(launch): + stream = get_stream(launch) + cvcuda.minarearect_into(dst, src, num_points, N, stream=stream) + + return run + + else: + state.skip("ImageBatchVarShape not implemented for this operator") + return None + + +if __name__ == "__main__": + run_benchmark("minarearect", minarearect) diff --git a/bench/python/ops/bench_minmaxloc.py b/bench/python/ops/bench_minmaxloc.py new file mode 100755 index 000000000..3baf182e4 --- /dev/null +++ b/bench/python/ops/bench_minmaxloc.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""CV-CUDA MinMaxLoc operator benchmark - Python equivalent of BenchMinMaxLoc.cpp""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cupy as cp # noqa: E402 +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + get_input_kind, + parse_shape, + get_dtype, + get_dtype_size, + create_tensor, + create_image_batch_varshape, + create_stream_cache, + run_benchmark, +) + + +def minmaxloc(state): + """MinMaxLoc operator benchmark matching C++ BenchMinMaxLoc.cpp""" + + shape = parse_shape(state.get_string("shape")) + dtype = get_dtype(state.get_string("InOutDataType")) + input_kind = get_input_kind(state.get_string("inputKind")) + max_locs = state.get_int64("maxLocations") + run_choice = state.get_string("runChoice") + try: + layout = state.get_string("layout") + except (KeyError, RuntimeError): + layout = "NHWC" + device_id = state.get_device() + + N, H, W = shape + dtype_size = get_dtype_size(dtype) + + if run_choice not in ("MIN", "MAX", "MIN_MAX"): + raise ValueError("runChoice must be MIN, MAX, or MIN_MAX") + if layout not in ("NHWC", "NCHW", "NCHW_FAKE"): + state.skip( + "MinMaxLoc benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts" + ) + return None + if input_kind != "Tensor" and layout != "NHWC": + state.skip("Planar MinMaxLoc benchmark is tensor-only") + return None + + run_min = run_choice in ("MIN", "MIN_MAX") + run_max = run_choice in ("MAX", "MIN_MAX") + is_planar = layout == "NCHW" + is_fake_planar = layout == "NCHW_FAKE" + + input_bytes = N * H * W * dtype_size + state.add_global_memory_reads((3 if is_fake_planar else 2) * input_bytes) + state.add_global_memory_writes( + (int(run_min) + int(run_max)) * N * (4 + max_locs * 8 + 4) + + (input_bytes if is_fake_planar else 0) + ) + + get_stream = create_stream_cache() + + if dtype in (cvcuda.Type.S8, cvcuda.Type.S16, cvcuda.Type.S32): + val_dtype = cvcuda.Type.S32 + elif dtype in (cvcuda.Type.U8, cvcuda.Type.U16, cvcuda.Type.U32): + val_dtype = cvcuda.Type.U32 + else: + val_dtype = dtype + + val_dtype_map = { + cvcuda.Type.U32: cp.uint32, + cvcuda.Type.S32: cp.int32, + cvcuda.Type.F32: cp.float32, + cvcuda.Type.F64: cp.float64, + } + + with cp.cuda.Device(device_id): + min_val_data = cp.zeros((N, 1), dtype=val_dtype_map[val_dtype]) + min_val = cvcuda.as_tensor(min_val_data, "NC") + + min_loc_data = cp.zeros((N, max_locs, 2), dtype=cp.int32) + min_loc = cvcuda.as_tensor(min_loc_data, "NMC") + + num_min_data = cp.zeros((N, 1), dtype=cp.int32) + num_min = cvcuda.as_tensor(num_min_data, "NC") + + max_val_data = cp.zeros((N, 1), dtype=val_dtype_map[val_dtype]) + max_val = cvcuda.as_tensor(max_val_data, "NC") + + max_loc_data = cp.zeros((N, max_locs, 2), dtype=cp.int32) + max_loc = cvcuda.as_tensor(max_loc_data, "NMC") + + num_max_data = cp.zeros((N, 1), dtype=cp.int32) + num_max = cvcuda.as_tensor(num_max_data, "NC") + + def run_op(stream, src): + if run_choice == "MIN": + cvcuda.min_loc_into(min_val, min_loc, num_min, src, stream=stream) + elif run_choice == "MAX": + cvcuda.max_loc_into(max_val, max_loc, num_max, src, stream=stream) + else: + cvcuda.min_max_loc_into( + min_val, + min_loc, + num_min, + max_val, + max_loc, + num_max, + src, + stream=stream, + ) + + if input_kind == "Tensor": # Tensor mode + tensor_shape = (N, 1, H, W) if (is_planar or is_fake_planar) else (N, H, W, 1) + src = create_tensor( + tensor_shape, + dtype, + device_id, + layout="NCHW" if (is_planar or is_fake_planar) else "NHWC", + fill_mode="lcg", + ) + + if is_fake_planar: + inter_src = create_tensor( + (N, H, W, 1), dtype, device_id, layout="NHWC", fill_mode=0 + ) + + def run_fake(launch): + stream = get_stream(launch) + cvcuda.reformat_into(inter_src, src, stream=stream) + run_op(stream, inter_src) + + return run_fake + + def run(launch): + run_op(get_stream(launch), src) + + return run + + else: # ImageBatchVarShape mode + if dtype == cvcuda.Type.U8: + img_format = cvcuda.Format.U8 + elif dtype == cvcuda.Type.U16: + img_format = cvcuda.Format.U16 + elif dtype == cvcuda.Type.U32: + img_format = cvcuda.Format.U32 + elif dtype == cvcuda.Type.S16: + img_format = cvcuda.Format.S16 + elif dtype == cvcuda.Type.S32: + img_format = cvcuda.Format.S32 + else: + img_format = cvcuda.Format.F32 + + src = create_image_batch_varshape( + (N, H, W), 0, img_format, dtype, device_id, fill_mode="lcg" + ) + + def run(launch): + run_op(get_stream(launch), src) + + return run + + +if __name__ == "__main__": + run_benchmark("minmaxloc", minmaxloc) diff --git a/bench/python/ops/bench_morphology.py b/bench/python/ops/bench_morphology.py new file mode 100755 index 000000000..734d359f6 --- /dev/null +++ b/bench/python/ops/bench_morphology.py @@ -0,0 +1,251 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""CV-CUDA Morphology operator benchmark - Python equivalent of BenchMorphology.cpp""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cupy as cp # noqa: E402 +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + get_input_kind, + parse_shape, + get_dtype, + get_dtype_size, + get_num_channels, + get_format_from_dtype, + get_border_type, + create_tensor, + create_image_batch_varshape, + create_stream_cache, + run_benchmark, +) + + +def morphology(state): + """Morphology operator benchmark matching C++ BenchMorphology.cpp""" + + shape = parse_shape(state.get_string("shape")) + dtype_str = state.get_string("InOutDataType") + dtype = get_dtype(dtype_str) + num_channels = get_num_channels(dtype_str) + morph_type = state.get_string("morphType") + border = get_border_type(state.get_string("border")) + kernel_size_str = state.get_string("kernelSize") + iteration = int(state.get_int64("iteration")) + input_kind = get_input_kind(state.get_string("inputKind")) + try: + layout = state.get_string("layout") + except (KeyError, RuntimeError): + layout = "NHWC" + device_id = state.get_device() + + N, H, W = shape + dtype_size = get_dtype_size(dtype_str) + + kernel_w, kernel_h = map(int, kernel_size_str.lower().split("x")) + mask_size = (kernel_w, kernel_h) + + morph_map = { + "ERODE": cvcuda.MorphologyType.ERODE, + "DILATE": cvcuda.MorphologyType.DILATE, + "OPEN": cvcuda.MorphologyType.OPEN, + "CLOSE": cvcuda.MorphologyType.CLOSE, + } + morph_op = morph_map.get(morph_type, cvcuda.MorphologyType.ERODE) + + needs_workspace = ( + morph_op in (cvcuda.MorphologyType.OPEN, cvcuda.MorphologyType.CLOSE) + or iteration > 1 + ) + + if layout not in ("NHWC", "NCHW", "NCHW_FAKE"): + state.skip( + "Morphology benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts" + ) + return None + + is_planar = layout == "NCHW" + is_fake_planar = layout == "NCHW_FAKE" + if is_fake_planar and input_kind == "VarShape": + state.skip("Fake-planar (NCHW_FAKE) Morphology benchmark is tensor-only") + return None + + if needs_workspace: + bw_iteration = 2 * iteration + else: + bw_iteration = iteration + + bytes_ = N * H * W * dtype_size * bw_iteration + if is_fake_planar: + state.add_global_memory_reads(3 * bytes_) + state.add_global_memory_writes(3 * bytes_) + else: + state.add_global_memory_reads(bytes_) + state.add_global_memory_writes(bytes_) + + get_stream = create_stream_cache() + + anchor = (-1, -1) + + if input_kind == "Tensor": # Tensor mode + if is_fake_planar: + src = create_tensor( + (N, num_channels, H, W), + dtype, + device_id, + layout="NCHW", + fill_mode="checkerboard", + ) + inter_src = create_tensor( + (N, H, W, num_channels), dtype, device_id, layout="NHWC", fill_mode=0 + ) + inter_dst = create_tensor( + (N, H, W, num_channels), dtype, device_id, layout="NHWC", fill_mode=0 + ) + dst = create_tensor( + (N, num_channels, H, W), dtype, device_id, layout="NCHW", fill_mode=0 + ) + + workspace = None + if needs_workspace: + workspace = create_tensor( + (N, H, W, num_channels), + dtype, + device_id, + layout="NHWC", + fill_mode=0, + ) + + def run_fake(launch): + stream = get_stream(launch) + cvcuda.reformat_into(inter_src, src, stream=stream) + cvcuda.morphology_into( + inter_dst, + inter_src, + morph_op, + mask_size, + anchor=anchor, + iteration=iteration, + border=border, + workspace=workspace, + stream=stream, + ) + cvcuda.reformat_into(dst, inter_dst, stream=stream) + + return run_fake + + input_shape = (N, num_channels, H, W) if is_planar else (N, H, W, num_channels) + src = create_tensor( + input_shape, + dtype, + device_id, + layout="NCHW" if is_planar else "NHWC", + fill_mode="checkerboard", + ) + dst = create_tensor( + input_shape, + dtype, + device_id, + layout="NCHW" if is_planar else "NHWC", + fill_mode=0, + ) + + workspace = None + if needs_workspace: + workspace = create_tensor( + input_shape, + dtype, + device_id, + layout="NCHW" if is_planar else "NHWC", + fill_mode=0, + ) + + def run(launch): + cvcuda.morphology_into( + dst, + src, + morph_op, + mask_size, + anchor=anchor, + iteration=iteration, + border=border, + workspace=workspace, + stream=get_stream(launch), + ) + + return run + + else: # ImageBatchVarShape mode + img_format = get_format_from_dtype(dtype_str, num_channels, planar=is_planar) + src = create_image_batch_varshape( + (N, H, W, num_channels), + 0, + img_format, + dtype, + device_id, + fill_mode="checkerboard", + ) + dst = create_image_batch_varshape( + (N, H, W, num_channels), + 0, + img_format, + dtype, + device_id, + fill_mode=0, + ) + + workspace = None + if needs_workspace: + workspace = create_image_batch_varshape( + (N, H, W, num_channels), + 0, + img_format, + dtype, + device_id, + fill_mode=0, + ) + + with cp.cuda.Device(device_id): + mask_data = cp.zeros((N, 2), dtype=cp.int32) + mask_data[:, 0] = kernel_w + mask_data[:, 1] = kernel_h + mask_tensor = cvcuda.as_tensor(mask_data.reshape(N, 2), "NW") + + anchor_data = cp.full((N, 2), -1, dtype=cp.int32) + anchor_tensor = cvcuda.as_tensor(anchor_data.reshape(N, 2), "NW") + + def run(launch): + cvcuda.morphology_into( + dst, + src, + morph_op, + mask_tensor, + anchor_tensor, + iteration=iteration, + border=border, + workspace=workspace, + stream=get_stream(launch), + ) + + return run + + +if __name__ == "__main__": + run_benchmark("morphology", morphology) diff --git a/bench/python/ops/bench_nonmaximumsuppression.py b/bench/python/ops/bench_nonmaximumsuppression.py new file mode 100755 index 000000000..174aeb1bb --- /dev/null +++ b/bench/python/ops/bench_nonmaximumsuppression.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""CV-CUDA NonMaximumSuppression operator benchmark - Python equivalent of BenchNonMaximumSuppression.cpp""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cupy as cp # noqa: E402 +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + parse_shape, + create_tensor, + create_stream_cache, + run_benchmark, + _lcg_fill, + _LCG_SEED, +) + + +def nms(state): + """NonMaximumSuppression operator benchmark matching C++ BenchNonMaximumSuppression.cpp""" + + shape = parse_shape(state.get_string("shape")) + device_id = state.get_device() + score_threshold = state.get_float64("scoreThreshold") + iou_threshold = state.get_float64("iouThreshold") + + N, num_boxes = shape + + state.add_global_memory_reads(N * num_boxes * (8 + 4)) + state.add_global_memory_writes(N * num_boxes * 1 * 2) + + get_stream = create_stream_cache() + + src_bb = cvcuda.Tensor((N, num_boxes), cvcuda.Type._4S16, "NB") + with cp.cuda.Device(device_id): + # Deterministic per-element [10, 50] cycle mirroring the C++ + # BenchNonMaximumSuppression lambda exactly (n=batch, b=box, c=channel + # → 10 + (b*7 + c*11 + n*3) % 41). + n_idx, b_idx, c_idx = cp.indices((N, num_boxes, 4), dtype=cp.int64) + bb_data = (10 + (b_idx * 7 + c_idx * 11 + n_idx * 3) % 41).astype(cp.int16) + cp.copyto( + cp.asarray(src_bb.cuda()).view(cp.int16).reshape(N, num_boxes, 4), bb_data + ) + + with cp.cuda.Device(device_id): + # srcSc: deterministic LCG over float [-1, +1] (bit-identical to C++ + # RandomValues() which routes through the GPU LCG fast path). + sc_data = cp.empty((N, num_boxes), dtype=cp.float32) + _lcg_fill(sc_data, _LCG_SEED) + src_sc = cvcuda.as_tensor(sc_data, "NB") + + dst_mk = create_tensor( + (N, num_boxes), cvcuda.Type.U8, device_id, layout="NB", fill_mode=0 + ) + + def run(launch): + cvcuda.nms_into( + dst_mk, + src_bb, + src_sc, + score_threshold, + iou_threshold, + stream=get_stream(launch), + ) + + return run + + +if __name__ == "__main__": + run_benchmark("nonmaximumsuppression", nms) diff --git a/bench/python/ops/bench_normalize.py b/bench/python/ops/bench_normalize.py new file mode 100755 index 000000000..bca4046ea --- /dev/null +++ b/bench/python/ops/bench_normalize.py @@ -0,0 +1,243 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""CV-CUDA Normalize operator benchmark - Python equivalent of BenchNormalize.cpp""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + get_input_kind, + parse_shape, + get_dtype, + get_dtype_size, + get_num_channels, + get_format_from_dtype, + create_tensor, + create_image_batch_varshape, + create_stream_cache, + run_benchmark, +) + + +def normalize(state): + """Normalize operator benchmark matching C++ BenchNormalize.cpp""" + + shape = parse_shape(state.get_string("shape")) + dtype_str = state.get_string("InOutDataType") + dtype = get_dtype(dtype_str) + num_channels = get_num_channels(dtype_str) + try: + layout = state.get_string("layout") + except Exception: + layout = "NHWC" + input_kind_name = state.get_string("inputKind") + scalar_params = input_kind_name == "TensorScalar" + input_kind = "Tensor" if scalar_params else get_input_kind(input_kind_name) + device_id = state.get_device() + + N, H, W = shape + dtype_size = get_dtype_size(dtype_str) + + if layout not in ("NHWC", "NCHW", "NCHW_FAKE"): + state.skip( + "Normalize benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts" + ) + return None + + is_planar = layout == "NCHW" + is_fake_planar = layout == "NCHW_FAKE" + if is_fake_planar and input_kind == "VarShape": + state.skip("Fake-planar (NCHW_FAKE) normalize benchmark is tensor-only") + return None + + param_samples = 1 if is_planar and input_kind == "VarShape" else N + param_bytes = 0 if scalar_params else param_samples * num_channels * 4 * 2 + bytes_ = N * H * W * dtype_size + if is_fake_planar: + # reformat(NCHW->NHWC) + normalize + reformat(NHWC->NCHW); normalize preserves size. + state.add_global_memory_reads(3 * bytes_ + param_bytes) + state.add_global_memory_writes(3 * bytes_) + else: + state.add_global_memory_reads(bytes_ + param_bytes) + state.add_global_memory_writes(bytes_) + + get_stream = create_stream_cache() + + global_scale = 1.234 + global_shift = 2.345 + epsilon = 12.34 + flags = cvcuda.NormalizeFlags.SCALE_IS_STDDEV + + if scalar_params: + if is_fake_planar: + state.skip( + "Scalar-parameter normalize benchmark supports only native NHWC and NCHW tensors" + ) + return None + + input_shape = ( + (N, num_channels, H, W) if layout == "NCHW" else (N, H, W, num_channels) + ) + src = create_tensor( + input_shape, dtype, device_id, layout=layout, fill_mode="checkerboard" + ) + dst = create_tensor(input_shape, dtype, device_id, layout=layout, fill_mode=0) + base = [0.25, 0.5, 0.75, 1.0][:num_channels] + scale = [1.25, 1.5, 1.75, 2.0][:num_channels] + + def run_scalar(launch): + cvcuda.normalize_into( + dst, + src, + base, + scale, + flags, + globalscale=global_scale, + globalshift=global_shift, + epsilon=epsilon, + stream=get_stream(launch), + ) + + return run_scalar + + # Fake-planar runs the interleaved kernel, so its base/scale are interleaved (NHWC). + base_shape = ( + (param_samples, num_channels, 1, 1) + if is_planar + else (param_samples, 1, 1, num_channels) + ) + param_layout = "NCHW" if is_planar else "NHWC" + + # base, scale: deterministic LCG over float [-1, +1], bit-identical to + # the C++ RandomValues() GPU fast path (see BenchNormalize.cpp). + base = create_tensor( + base_shape, + cvcuda.Type.F32, + device_id, + layout=param_layout, + fill_mode="lcg", + ) + scale = create_tensor( + base_shape, + cvcuda.Type.F32, + device_id, + layout=param_layout, + fill_mode="lcg", + ) + + if is_fake_planar: + # Tensor-only "fake planar": planar->interleaved->normalize->interleaved->planar, + # all timed, as the comparison baseline for the native planar (NCHW) path. + src = create_tensor( + (N, num_channels, H, W), + dtype, + device_id, + layout="NCHW", + fill_mode="checkerboard", + ) + inter_src = create_tensor( + (N, H, W, num_channels), dtype, device_id, layout="NHWC", fill_mode=0 + ) + inter_dst = create_tensor( + (N, H, W, num_channels), dtype, device_id, layout="NHWC", fill_mode=0 + ) + dst = create_tensor( + (N, num_channels, H, W), dtype, device_id, layout="NCHW", fill_mode=0 + ) + + def run_fake(launch): + stream = get_stream(launch) + cvcuda.reformat_into(inter_src, src, stream=stream) + cvcuda.normalize_into( + inter_dst, + inter_src, + base, + scale, + flags, + globalscale=global_scale, + globalshift=global_shift, + epsilon=epsilon, + stream=stream, + ) + cvcuda.reformat_into(dst, inter_dst, stream=stream) + + return run_fake + + if input_kind == "Tensor": # Tensor mode + input_shape = ( + (N, num_channels, H, W) if layout == "NCHW" else (N, H, W, num_channels) + ) + src = create_tensor( + input_shape, dtype, device_id, layout=layout, fill_mode="checkerboard" + ) + dst = create_tensor(input_shape, dtype, device_id, layout=layout, fill_mode=0) + + def run(launch): + cvcuda.normalize_into( + dst, + src, + base, + scale, + flags, + globalscale=global_scale, + globalshift=global_shift, + epsilon=epsilon, + stream=get_stream(launch), + ) + + return run + + else: # ImageBatchVarShape mode + img_format = get_format_from_dtype(dtype_str, num_channels, planar=is_planar) + src = create_image_batch_varshape( + (N, H, W, num_channels), + 0, + img_format, + dtype, + device_id, + fill_mode="checkerboard", + ) + dst = create_image_batch_varshape( + (N, H, W, num_channels), + 0, + img_format, + dtype, + device_id, + fill_mode=0, + ) + + def run(launch): + cvcuda.normalize_into( + dst, + src, + base, + scale, + flags, + globalscale=global_scale, + globalshift=global_shift, + epsilon=epsilon, + stream=get_stream(launch), + ) + + return run + + +if __name__ == "__main__": + run_benchmark("normalize", normalize) diff --git a/bench/python/ops/bench_osd.py b/bench/python/ops/bench_osd.py new file mode 100644 index 000000000..bcdf0a7fd --- /dev/null +++ b/bench/python/ops/bench_osd.py @@ -0,0 +1,161 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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 sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + get_input_kind, + parse_shape, + get_dtype, + get_dtype_size, + get_num_channels, + create_tensor, + create_stream_cache, + run_benchmark, +) + + +def _make_element(element_type, height, width): + if element_type == "POINT": + return cvcuda.Point( + centerPos=(width // 2, height // 2), + radius=min(width, height) // 2, + color=(0, 0, 0, 255), + ) + + if element_type == "RECT": + return cvcuda.BndBoxI( + box=(width // 4, height // 4, width // 2, height // 2), + thickness=3, + borderColor=(255, 255, 0, 255), + fillColor=(0, 128, 255, 0), + ) + + if element_type == "LINE": + return cvcuda.Line( + pos0=(width // 8, height // 8), + pos1=(7 * width // 8, 7 * height // 8), + thickness=4, + color=(255, 0, 0, 255), + ) + + if element_type == "CIRCLE": + return cvcuda.Circle( + centerPos=(width // 2, height // 2), + radius=min(width, height) // 4, + thickness=4, + borderColor=(0, 255, 255, 255), + bgColor=(255, 0, 255, 0), + ) + + raise ValueError(f"Unsupported OSD elementType: {element_type}") + + +def osd(state): + """OSD operator benchmark matching C++ BenchOSD.cpp""" + + shape = parse_shape(state.get_string("shape")) + num_elem = state.get_int64("numElem") + device_id = state.get_device() + dtype_str = state.get_string("InOutDataType") + dtype = get_dtype(dtype_str) + num_channels = get_num_channels(dtype_str) + input_kind = get_input_kind(state.get_string("inputKind")) + element_type = state.get_string("elementType") + try: + layout = state.get_string("layout") + except (KeyError, RuntimeError): + layout = "NHWC" + + N, H, W = shape + + if dtype_str not in ("uchar3", "uchar4"): + state.skip(f"Unsupported dtype for OSD: {dtype_str}") + return None + if layout not in ("NHWC", "NCHW", "NCHW_FAKE"): + state.skip("OSD benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts") + return None + + is_planar = layout == "NCHW" + is_fake_planar = layout == "NCHW_FAKE" + if (is_planar or is_fake_planar) and input_kind != "Tensor": + state.skip("Planar OSD benchmark is tensor-only") + return None + + bytes_ = N * H * W * get_dtype_size(dtype_str) + elem_bytes = num_elem * 4 * 16 + if is_planar or is_fake_planar: + state.add_global_memory_reads(3 * bytes_ + elem_bytes) + state.add_global_memory_writes(3 * bytes_) + else: + state.add_global_memory_reads(bytes_ + elem_bytes) + state.add_global_memory_writes(bytes_) + + elements_per_batch = [] + for _ in range(N): + batch_elements = [] + for _ in range(num_elem): + batch_elements.append(_make_element(element_type, H, W)) + elements_per_batch.append(batch_elements) + + elements = cvcuda.Elements(elements=elements_per_batch) + + get_stream = create_stream_cache() + + if is_fake_planar: + src = create_tensor( + (N, num_channels, H, W), + dtype, + device_id, + layout="NCHW", + fill_mode="checkerboard", + ) + inter_src = create_tensor( + (N, H, W, num_channels), dtype, device_id, layout="NHWC", fill_mode=0 + ) + inter_dst = create_tensor( + (N, H, W, num_channels), dtype, device_id, layout="NHWC", fill_mode=0 + ) + dst = create_tensor( + (N, num_channels, H, W), dtype, device_id, layout="NCHW", fill_mode=0 + ) + + def run_fake(launch): + stream = get_stream(launch) + cvcuda.reformat_into(inter_src, src, stream=stream) + cvcuda.osd_into(inter_dst, inter_src, elements, stream=stream) + cvcuda.reformat_into(dst, inter_dst, stream=stream) + + return run_fake + + tensor_shape = (N, num_channels, H, W) if is_planar else (N, H, W, num_channels) + src = create_tensor( + tensor_shape, dtype, device_id, layout=layout, fill_mode="checkerboard" + ) + dst = create_tensor(tensor_shape, dtype, device_id, layout=layout, fill_mode=0) + + def run(launch): + stream = get_stream(launch) + cvcuda.osd_into(dst, src, elements, stream=stream) + + return run + + +if __name__ == "__main__": + run_benchmark("osd", osd) diff --git a/bench/python/ops/bench_padandstack.py b/bench/python/ops/bench_padandstack.py new file mode 100755 index 000000000..6b99d2332 --- /dev/null +++ b/bench/python/ops/bench_padandstack.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""CV-CUDA PadAndStack operator benchmark - Python equivalent of BenchPadAndStack.cpp""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cupy as cp # noqa: E402 +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + get_input_kind, + get_format_from_dtype, + get_num_channels, + parse_shape, + get_dtype, + get_dtype_size, + create_stream_cache, + create_image_batch_varshape, + run_benchmark, +) + + +def padandstack(state): + """PadAndStack operator benchmark matching C++ BenchPadAndStack.cpp""" + + shape = parse_shape(state.get_string("shape")) + dtype = get_dtype(state.get_string("InOutDataType")) + dtype_str = state.get_string("InOutDataType") + input_kind = get_input_kind(state.get_string("inputKind")) + layout = state.get_string("layout") + border_str = state.get_string("border") + pad = state.get_int64("pad") + if layout not in ("NHWC", "NCHW"): + state.skip("PadAndStack benchmark supports only NHWC and NCHW layouts") + return None + is_planar = layout == "NCHW" + num_channels = get_num_channels(dtype_str) + if is_planar and num_channels == 2: + state.skip("Planar PadAndStack benchmark does not support 2-channel layouts") + return None + if is_planar and num_channels == 1: + state.skip( + "Single-channel PadAndStack has no distinct planar image-batch format" + ) + return None + + N, H, W = shape + num_batches = N + src_height = H + src_width = W + + dst_height = src_height + 2 * pad + dst_width = src_width + 2 * pad + + border_map = { + "CONSTANT": cvcuda.Border.CONSTANT, + "REPLICATE": cvcuda.Border.REPLICATE, + "REFLECT": cvcuda.Border.REFLECT, + "WRAP": cvcuda.Border.WRAP, + "REFLECT101": cvcuda.Border.REFLECT101, + } + border_type = border_map.get(border_str, cvcuda.Border.REFLECT101) + border_value = 0.0 + + dtype_size = get_dtype_size(dtype) + state.add_global_memory_reads( + num_batches * src_height * src_width * num_channels * dtype_size + + num_batches * 4 * 2 + ) + state.add_global_memory_writes( + num_batches * dst_height * dst_width * num_channels * dtype_size + ) + + if input_kind == "Tensor": + state.skip( + "PadAndStack only supports ImageBatchVarShape input (inputKind=VarShape)" + ) + return None + + img_format = get_format_from_dtype(dtype_str, num_channels, planar=is_planar) + + device_id = state.get_device() + src = create_image_batch_varshape( + (num_batches, src_height, src_width, num_channels), + 0, + img_format, + dtype, + device_id, + fill_mode="checkerboard", + ) + + dst_shape = ( + (num_batches, num_channels, dst_height, dst_width) + if is_planar + else (num_batches, dst_height, dst_width, num_channels) + ) + dst = cvcuda.Tensor(dst_shape, dtype, layout) + + top_pad = (dst_height - src_height) // 2 + left_pad = (dst_width - src_width) // 2 + + top_data = cp.full((1, 1, num_batches, 1), top_pad, dtype=cp.int32) + left_data = cp.full((1, 1, num_batches, 1), left_pad, dtype=cp.int32) + + top = cvcuda.as_tensor(top_data, "NHWC") + left = cvcuda.as_tensor(left_data, "NHWC") + + get_stream = create_stream_cache() + + def run(launch): + stream = get_stream(launch) + cvcuda.padandstack_into( + dst, src, top, left, border_type, border_value, stream=stream + ) + + return run + + +if __name__ == "__main__": + run_benchmark("padandstack", padandstack) diff --git a/bench/python/ops/bench_pairwisematcher.py b/bench/python/ops/bench_pairwisematcher.py new file mode 100644 index 000000000..fc2c68a24 --- /dev/null +++ b/bench/python/ops/bench_pairwisematcher.py @@ -0,0 +1,95 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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 sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cupy as cp # noqa: E402 +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + get_dtype, + get_dtype_size, + create_tensor, + create_stream_cache, + run_benchmark, +) + + +def pairwisematcher(state): + """PairwiseMatcher operator benchmark matching C++ BenchPairwiseMatcher.cpp""" + + shape_str = state.get_string("shape") + matches_per_point = state.get_int64("matchesPerPoint") + cross_check = state.get_string("crossCheck") == "T" + read_num_sets = state.get_string("readNumSets") == "T" + write_distances = state.get_string("writeDistances") == "T" + norm_type_str = state.get_string("normType") + dtype = get_dtype(state.get_string("InOutDataType")) + device_id = state.get_device() + + parts = shape_str.split("x") + N = int(parts[0]) + M = int(parts[1]) + D = int(parts[2]) + shape = (N, M, D) + + norm_map = { + "HAMMING": cvcuda.Norm.HAMMING, + "L1": cvcuda.Norm.L1, + "L2": cvcuda.Norm.L2, + } + norm_type = norm_map.get(norm_type_str, cvcuda.Norm.L2) + + dtype_size = get_dtype_size(dtype) + max_matches = M * matches_per_point + state.add_global_memory_reads((3 if cross_check else 2) * N * M * D * dtype_size) + state.add_global_memory_writes(N * (4 + max_matches * (2 * 4 + 4))) + + set1 = create_tensor(shape, dtype, device_id, layout="NMD", fill_mode="lcg") + set2 = create_tensor(shape, dtype, device_id, layout="NMD", fill_mode="lcg") + + num_set1 = None + num_set2 = None + if read_num_sets: + num_set1_data = cp.full((N,), M, dtype=cp.int32) + num_set1 = cvcuda.as_tensor(num_set1_data, "N") + num_set2_data = cp.full((N,), M, dtype=cp.int32) + num_set2 = cvcuda.as_tensor(num_set2_data, "N") + + get_stream = create_stream_cache() + + def run(launch): + stream = get_stream(launch) + cvcuda.match( + set1, + set2, + num_set1=num_set1, + num_set2=num_set2, + num_matches=True, + distances=write_distances, + cross_check=cross_check, + matches_per_point=matches_per_point, + norm_type=norm_type, + algo_choice=cvcuda.Matcher.BRUTE_FORCE, + stream=stream, + ) + + return run + + +if __name__ == "__main__": + run_benchmark("pairwisematcher", pairwisematcher) diff --git a/bench/python/ops/bench_pillowresize.py b/bench/python/ops/bench_pillowresize.py new file mode 100755 index 000000000..388cb2b7a --- /dev/null +++ b/bench/python/ops/bench_pillowresize.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""CV-CUDA PillowResize operator benchmark - Python equivalent of BenchPillowResize.cpp""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + get_input_kind, + parse_shape, + get_dtype, + get_dtype_size, + get_resize_output_shape, + get_num_channels, + get_format_from_dtype, + get_interpolation_type, + create_tensor, + create_image_batch_varshape, + create_stream_cache, + run_benchmark, +) + + +def pillowresize(state): + """PillowResize operator benchmark matching C++ BenchPillowResize.cpp""" + + shape = parse_shape(state.get_string("shape")) + dtype_str = state.get_string("InOutDataType") + dtype = get_dtype(dtype_str) + num_channels = get_num_channels(dtype_str) + resize_type = state.get_string("resizeType") + interp = get_interpolation_type(state.get_string("interpolation")) + input_kind = get_input_kind(state.get_string("inputKind")) + device_id = state.get_device() + + # The layout axis is optional; profiles without it default to interleaved NHWC. + try: + layout = state.get_string("layout") + except Exception: + layout = "NHWC" + + if layout not in ("NHWC", "NCHW", "NCHW_FAKE"): + state.skip( + "PillowResize benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts" + ) + return None + + is_planar = layout == "NCHW" + is_fake_planar = layout == "NCHW_FAKE" + # NCHW_FAKE ("fake planar") is a tensor-only comparison path. + if is_fake_planar and input_kind != "Tensor": + state.skip("Fake-planar (NCHW_FAKE) PillowResize benchmark is tensor-only") + return None + + N, H, W = shape + + try: + _, dst_H, dst_W = get_resize_output_shape(shape, resize_type) + except ValueError as error: + state.skip(str(error)) + return None + + dtype_size = get_dtype_size(dtype_str) + src_bytes = N * H * W * dtype_size + dst_bytes = N * dst_H * dst_W * dtype_size + if is_fake_planar: + # reformat(NCHW->NHWC) + resize + reformat(NHWC->NCHW) + state.add_global_memory_reads(2 * src_bytes + dst_bytes) + state.add_global_memory_writes(src_bytes + 2 * dst_bytes) + else: + state.add_global_memory_reads(src_bytes) + state.add_global_memory_writes(dst_bytes) + + # Workspace/image format carries the channel count (and planar vs interleaved layout); the tensor + # path needs it for getWorkspaceRequirements. Matches BenchPillowResize.cpp. + img_format = get_format_from_dtype(dtype_str, num_channels, planar=is_planar) + + get_stream = create_stream_cache() + + if is_fake_planar: + # Tensor-only "fake planar": planar->interleaved->resize->interleaved->planar, all timed, + # as the comparison baseline for the native planar (NCHW) path. img_format is interleaved + # here (is_planar is False for NCHW_FAKE). + src = create_tensor( + (N, num_channels, H, W), + dtype, + device_id, + layout="NCHW", + fill_mode="checkerboard", + ) + inter_src = create_tensor( + (N, H, W, num_channels), dtype, device_id, layout="NHWC", fill_mode=0 + ) + inter_dst = create_tensor( + (N, dst_H, dst_W, num_channels), + dtype, + device_id, + layout="NHWC", + fill_mode=0, + ) + dst = create_tensor( + (N, num_channels, dst_H, dst_W), + dtype, + device_id, + layout="NCHW", + fill_mode=0, + ) + + def run_fake(launch): + stream = get_stream(launch) + cvcuda.reformat_into(inter_src, src, stream=stream) + cvcuda.pillowresize_into( + inter_dst, inter_src, img_format, interp, stream=stream + ) + cvcuda.reformat_into(dst, inter_dst, stream=stream) + + return run_fake + + # Channels come from the dtype (e.g. uchar3/float3 -> 3); planar (NCHW) puts the channel dimension + # before the spatial dims, interleaved (NHWC) after. + def shaped(h, w): + return (N, num_channels, h, w) if is_planar else (N, h, w, num_channels) + + if input_kind == "Tensor": + src = create_tensor( + shaped(H, W), dtype, device_id, layout=layout, fill_mode="checkerboard" + ) + dst = cvcuda.Tensor(shaped(dst_H, dst_W), dtype, layout) + + def run(launch): + stream = get_stream(launch) + cvcuda.pillowresize_into(dst, src, img_format, interp, stream=stream) + + return run + else: # VarShape + src = create_image_batch_varshape( + (N, H, W, num_channels), + 0, + img_format, + dtype=dtype, + device=device_id, + fill_mode="checkerboard", + ) + dst = create_image_batch_varshape( + (N, dst_H, dst_W, num_channels), + 0, + img_format, + dtype=dtype, + device=device_id, + fill_mode=0, + ) + + def run(launch): + stream = get_stream(launch) + cvcuda.pillowresize_into(dst, src, interp, stream=stream) + + return run + + +if __name__ == "__main__": + run_benchmark("pillowresize", pillowresize) diff --git a/bench/python/ops/bench_posterize.py b/bench/python/ops/bench_posterize.py new file mode 100755 index 000000000..d5bdd107a --- /dev/null +++ b/bench/python/ops/bench_posterize.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""CV-CUDA Posterize operator benchmark - Python equivalent of BenchPosterize.cpp""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + get_input_kind, + parse_shape, + get_dtype, + get_dtype_size, + get_num_channels, + get_format_from_dtype, + create_tensor, + create_image_batch_varshape, + create_stream_cache, + run_benchmark, +) + +# bits is irrelevant to throughput; use a fixed mid-range value. +BITS = 4 + + +def posterize(state): + """Posterize operator benchmark matching C++ BenchPosterize.cpp""" + + N, H, W = parse_shape(state.get_string("shape")) + dtype_str = state.get_string("InOutDataType") + dtype = get_dtype(dtype_str) + num_channels = get_num_channels(dtype_str) + layout = state.get_string("layout") + input_kind = get_input_kind(state.get_string("inputKind")) + device_id = state.get_device() + + if layout not in ("NHWC", "NCHW", "NCHW_FAKE"): + state.skip( + "Posterize benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts" + ) + return None + + is_planar = layout == "NCHW" + is_fake_planar = layout == "NCHW_FAKE" + if is_fake_planar and input_kind == "VarShape": + state.skip("Fake-planar (NCHW_FAKE) posterize benchmark is tensor-only") + return None + + # Posterize preserves size. + dtype_size = get_dtype_size(dtype_str) + bytes_ = N * H * W * dtype_size + if is_fake_planar: + # reformat(NCHW->NHWC) + posterize + reformat(NHWC->NCHW) + state.add_global_memory_reads(3 * bytes_) + state.add_global_memory_writes(3 * bytes_) + else: + state.add_global_memory_reads(bytes_) + state.add_global_memory_writes(bytes_) + + get_stream = create_stream_cache() + + if is_fake_planar: + src = create_tensor( + (N, num_channels, H, W), + dtype, + device_id, + layout="NCHW", + fill_mode="checkerboard", + ) + inter_src = create_tensor( + (N, H, W, num_channels), dtype, device_id, layout="NHWC", fill_mode=0 + ) + inter_dst = create_tensor( + (N, H, W, num_channels), dtype, device_id, layout="NHWC", fill_mode=0 + ) + dst = create_tensor( + (N, num_channels, H, W), dtype, device_id, layout="NCHW", fill_mode=0 + ) + + def run_fake(launch): + stream = get_stream(launch) + cvcuda.reformat_into(inter_src, src, stream=stream) + cvcuda.posterize_into(inter_dst, inter_src, BITS, stream=stream) + cvcuda.reformat_into(dst, inter_dst, stream=stream) + + return run_fake + + if input_kind == "Tensor": + shape = (N, num_channels, H, W) if is_planar else (N, H, W, num_channels) + src = create_tensor( + shape, dtype, device_id, layout=layout, fill_mode="checkerboard" + ) + dst = create_tensor(shape, dtype, device_id, layout=layout, fill_mode=0) + else: # ImageBatchVarShape + img_format = get_format_from_dtype(dtype_str, num_channels, planar=is_planar) + src = create_image_batch_varshape( + (N, H, W, num_channels), + 0, + img_format, + dtype, + device_id, + fill_mode="checkerboard", + ) + dst = create_image_batch_varshape( + (N, H, W, num_channels), 0, img_format, dtype, device_id, fill_mode=0 + ) + + def run(launch): + cvcuda.posterize_into(dst, src, BITS, stream=get_stream(launch)) + + return run + + +if __name__ == "__main__": + run_benchmark("posterize", posterize) diff --git a/bench/python/ops/bench_randomresizedcrop.py b/bench/python/ops/bench_randomresizedcrop.py new file mode 100755 index 000000000..fbc709244 --- /dev/null +++ b/bench/python/ops/bench_randomresizedcrop.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""CV-CUDA RandomResizedCrop operator benchmark - Python equivalent of BenchRandomResizedCrop.cpp""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + get_input_kind, + parse_shape, + get_dtype, + get_dtype_size, + get_num_channels, + get_format_from_dtype, + get_interpolation_type, + create_tensor, + create_image_batch_varshape, + create_stream_cache, + run_benchmark, +) + + +def randomresizedcrop(state): + """RandomResizedCrop operator benchmark matching C++ BenchRandomResizedCrop.cpp""" + + shape = parse_shape(state.get_string("shape")) + dtype_str = state.get_string("InOutDataType") + dtype = get_dtype(dtype_str) + nc = get_num_channels(dtype_str) + interp = get_interpolation_type(state.get_string("interpolation")) + try: + layout = state.get_string("layout") + except (KeyError, RuntimeError): + layout = "NHWC" + resize_type = state.get_string("resizeType") + input_kind = get_input_kind(state.get_string("inputKind")) + device_id = state.get_device() + + N, H, W = shape + + if layout not in ("NHWC", "NCHW", "NCHW_FAKE"): + state.skip( + "RandomResizedCrop benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts" + ) + return None + + is_planar = layout == "NCHW" + is_fake_planar = layout == "NCHW_FAKE" + if is_fake_planar and input_kind == "VarShape": + state.skip("Fake-planar (NCHW_FAKE) RandomResizedCrop benchmark is tensor-only") + return None + + if resize_type == "EXPAND": + dst_H, dst_W = H * 2, W * 2 + elif resize_type == "CONTRACT": + dst_H, dst_W = H // 2, W // 2 + else: + state.skip(f"Invalid resizeType: {resize_type}") + return None + + min_scale = 0.08 + max_scale = 1.0 + min_ratio = 0.5 + max_ratio = 2.0 + seed = 1234 + + dtype_size = get_dtype_size(dtype_str) + src_bytes = N * H * W * dtype_size + dst_bytes = N * dst_H * dst_W * dtype_size + if is_fake_planar: + # reformat(NCHW->NHWC) + random_resized_crop + reformat(NHWC->NCHW) + state.add_global_memory_reads(2 * src_bytes + dst_bytes) + state.add_global_memory_writes(src_bytes + 2 * dst_bytes) + else: + state.add_global_memory_reads(src_bytes) + state.add_global_memory_writes(dst_bytes) + + get_stream = create_stream_cache() + + if is_fake_planar: + src = create_tensor( + (N, nc, H, W), dtype, device_id, layout="NCHW", fill_mode="checkerboard" + ) + inter_src = create_tensor( + (N, H, W, nc), dtype, device_id, layout="NHWC", fill_mode=0 + ) + inter_dst = create_tensor( + (N, dst_H, dst_W, nc), dtype, device_id, layout="NHWC", fill_mode=0 + ) + dst = create_tensor( + (N, nc, dst_H, dst_W), dtype, device_id, layout="NCHW", fill_mode=0 + ) + + def run_fake(launch): + stream = get_stream(launch) + cvcuda.reformat_into(inter_src, src, stream=stream) + cvcuda.random_resized_crop_into( + inter_dst, + inter_src, + min_scale, + max_scale, + min_ratio, + max_ratio, + interp, + seed, + stream=stream, + ) + cvcuda.reformat_into(dst, inter_dst, stream=stream) + + return run_fake + + if input_kind == "Tensor": # Tensor mode + input_shape = (N, nc, H, W) if is_planar else (N, H, W, nc) + dst_shape = (N, nc, dst_H, dst_W) if is_planar else (N, dst_H, dst_W, nc) + src = create_tensor( + input_shape, dtype, device_id, layout=layout, fill_mode="checkerboard" + ) + dst = create_tensor(dst_shape, dtype, device_id, layout=layout, fill_mode=0) + else: # VarShape mode + img_format = get_format_from_dtype(dtype_str, nc, planar=is_planar) + src = create_image_batch_varshape( + (N, H, W, nc), + 0, + img_format, + dtype, + device_id, + fill_mode="checkerboard", + ) + dst = create_image_batch_varshape( + (N, dst_H, dst_W, nc), + 0, + img_format, + dtype, + device_id, + fill_mode=0, + ) + + def run(launch): + stream = get_stream(launch) + cvcuda.random_resized_crop_into( + dst, + src, + min_scale, + max_scale, + min_ratio, + max_ratio, + interp, + seed, + stream=stream, + ) + + return run + + +if __name__ == "__main__": + run_benchmark("randomresizedcrop", randomresizedcrop) diff --git a/bench/python/ops/bench_reformat.py b/bench/python/ops/bench_reformat.py new file mode 100755 index 000000000..26b6a8423 --- /dev/null +++ b/bench/python/ops/bench_reformat.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""CV-CUDA Reformat operator benchmark - Python equivalent of BenchReformat.cpp""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + parse_shape, + get_dtype, + get_dtype_size, + get_input_kind, + get_num_channels, + create_tensor, + create_stream_cache, + run_benchmark, +) + + +def reformat(state): + """Reformat operator benchmark matching C++ BenchReformat.cpp""" + + shape = parse_shape(state.get_string("shape")) + dtype_str = state.get_string("InOutDataType") + dtype = get_dtype(dtype_str) + nc = get_num_channels(dtype_str) + input_kind = get_input_kind(state.get_string("inputKind")) + row_alignment = int(state.get_int64("rowAlignment")) + device_id = state.get_device() + + if row_alignment < 0: + raise ValueError("rowAlignment must be non-negative") + + N, H, W = shape + + dtype_size = get_dtype_size(dtype_str) + state.add_global_memory_reads(N * H * W * dtype_size) + state.add_global_memory_writes(N * H * W * dtype_size) + + if input_kind != "Tensor": + state.skip("ImageBatchVarShape not implemented for this operator") + return None + + # Reformat converts planar NCHW -> interleaved NHWC; use the real channel + # count so both layouts move the same bytes as the C++ benchmark. + if row_alignment > 0: + src = cvcuda.Tensor((N, nc, H, W), dtype, layout="NCHW", rowalign=row_alignment) + dst = cvcuda.Tensor((N, H, W, nc), dtype, layout="NHWC", rowalign=row_alignment) + else: + src = create_tensor( + (N, nc, H, W), + dtype, + device_id, + layout="NCHW", + fill_mode="checkerboard", + ) + dst = create_tensor((N, H, W, nc), dtype, device_id, layout="NHWC", fill_mode=0) + + get_stream = create_stream_cache() + + def run(launch): + stream = get_stream(launch) + cvcuda.reformat_into(dst, src, stream=stream) + + return run + + +if __name__ == "__main__": + run_benchmark("reformat", reformat) diff --git a/bench/python/ops/bench_remap.py b/bench/python/ops/bench_remap.py new file mode 100755 index 000000000..df8e6d5a8 --- /dev/null +++ b/bench/python/ops/bench_remap.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""CV-CUDA Remap operator benchmark - Python equivalent of BenchRemap.cpp""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cupy as cp # noqa: E402 +import numpy as np # noqa: E402 +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + get_input_kind, + parse_shape, + get_dtype, + get_dtype_size, + get_num_channels, + get_format_from_dtype, + create_tensor, + create_image_batch_varshape, + create_stream_cache, + run_benchmark, + _lcg_fill, + _LCG_SEED, +) + + +def remap(state): + """Remap operator benchmark matching C++ BenchRemap.cpp""" + + shape = parse_shape(state.get_string("shape")) + dtype_str = state.get_string("InOutDataType") + dtype = get_dtype(dtype_str) + nc = get_num_channels(dtype_str) + map_type = state.get_string("mapType") + input_kind = get_input_kind(state.get_string("inputKind")) + try: + layout = state.get_string("layout") + except Exception: + layout = "NHWC" + device_id = state.get_device() + + if layout not in ("NHWC", "NCHW", "NCHW_FAKE"): + state.skip("Remap benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts") + return None + is_planar = layout == "NCHW" + is_fake_planar = layout == "NCHW_FAKE" + if is_fake_planar and input_kind == "VarShape": + state.skip("Fake-planar (NCHW_FAKE) remap benchmark is tensor-only") + return None + + N, H, W = shape + + if map_type == "DENSE": + src_interp = cvcuda.Interp.NEAREST + map_interp = cvcuda.Interp.NEAREST + border_type = cvcuda.Border.CONSTANT + map_value_type = cvcuda.Remap.ABSOLUTE_NORMALIZED + map_shape = (N, H, W, 1) + elif map_type == "RELATIVE": + src_interp = cvcuda.Interp.CUBIC + map_interp = cvcuda.Interp.CUBIC + border_type = cvcuda.Border.REFLECT101 + map_value_type = cvcuda.Remap.RELATIVE_NORMALIZED + map_shape = (N, 4, 4, 1) + else: + state.skip(f"Invalid mapType = {map_type}") + return None + + align_corners = True + border_value = np.array([0, 0, 0, 0], dtype=np.float32) + + dtype_size = get_dtype_size(dtype_str) + state.add_global_memory_reads( + N * H * W * dtype_size + map_shape[0] * map_shape[1] * map_shape[2] * 2 * 4 + ) + state.add_global_memory_writes(N * H * W * dtype_size) + + # map: deterministic LCG over float [-1, +1] (bit-identical to C++ + # RandomValues() which routes through the GPU LCG fast path). + with cp.cuda.Device(device_id): + map_data = cp.empty( + (map_shape[0], map_shape[1], map_shape[2], 2), dtype=cp.float32 + ) + _lcg_fill(map_data, _LCG_SEED) + map_tensor = cvcuda.as_tensor(map_data, "NHWC") + + get_stream = create_stream_cache() + + def do_remap(dst, src, stream): + cvcuda.remap_into( + dst, + src, + map_tensor, + src_interp=src_interp, + map_interp=map_interp, + map_type=map_value_type, + align_corners=align_corners, + border=border_type, + border_value=border_value, + stream=stream, + ) + + if is_fake_planar: + # Tensor-only: planar->interleaved->remap->interleaved->planar, all timed. + src = create_tensor( + (N, nc, H, W), dtype, device_id, layout="NCHW", fill_mode="checkerboard" + ) + inter_src = create_tensor( + (N, H, W, nc), dtype, device_id, layout="NHWC", fill_mode=0 + ) + inter_dst = create_tensor( + (N, H, W, nc), dtype, device_id, layout="NHWC", fill_mode=0 + ) + dst = create_tensor((N, nc, H, W), dtype, device_id, layout="NCHW", fill_mode=0) + + def run_fake(launch): + stream = get_stream(launch) + cvcuda.reformat_into(inter_src, src, stream=stream) + do_remap(inter_dst, inter_src, stream) + cvcuda.reformat_into(dst, inter_dst, stream=stream) + + return run_fake + + if input_kind == "Tensor": + src_shape = (N, nc, H, W) if is_planar else (N, H, W, nc) + src = create_tensor( + src_shape, dtype, device_id, layout=layout, fill_mode="checkerboard" + ) + dst = create_tensor(src_shape, dtype, device_id, layout=layout, fill_mode=0) + else: + img_format = get_format_from_dtype(dtype_str, nc, planar=is_planar) + src = create_image_batch_varshape( + (N, H, W, nc), + 0, + img_format, + dtype=dtype, + device=device_id, + fill_mode="checkerboard", + ) + dst = create_image_batch_varshape( + (N, H, W, nc), 0, img_format, dtype=dtype, device=device_id, fill_mode=0 + ) + + def run(launch): + do_remap(dst, src, get_stream(launch)) + + return run + + +if __name__ == "__main__": + run_benchmark("remap", remap) diff --git a/bench/python/ops/bench_resize.py b/bench/python/ops/bench_resize.py new file mode 100755 index 000000000..2aded21af --- /dev/null +++ b/bench/python/ops/bench_resize.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""CV-CUDA Resize operator benchmark - Python equivalent of BenchResize.cpp""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + get_input_kind, + parse_shape, + get_dtype, + get_dtype_size, + get_resize_output_shape, + get_num_channels, + get_interpolation_type, + get_format_from_dtype, + create_tensor, + create_image_batch_varshape, + create_stream_cache, + run_benchmark, +) + + +def resize(state): + """Resize operator benchmark matching C++ BenchResize.cpp""" + + shape = parse_shape(state.get_string("shape")) + dtype_str = state.get_string("InOutDataType") + dtype = get_dtype(dtype_str) + nc = get_num_channels(dtype_str) + try: + layout = state.get_string("layout") + except Exception: + layout = "NHWC" + input_kind = get_input_kind(state.get_string("inputKind")) + resize_type = state.get_string("resizeType") + interp = get_interpolation_type(state.get_string("interpolation")) + device_id = state.get_device() + + N, H, W = shape + + if layout not in ("NHWC", "NCHW", "NCHW_FAKE"): + state.skip("Resize benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts") + return None + + is_planar = layout == "NCHW" + is_fake_planar = layout == "NCHW_FAKE" + if is_fake_planar and input_kind == "VarShape": + state.skip("Fake-planar (NCHW_FAKE) resize benchmark is tensor-only") + return None + + try: + _, dst_H, dst_W = get_resize_output_shape(shape, resize_type) + except ValueError as error: + state.skip(str(error)) + return None + + dtype_size = get_dtype_size(dtype_str) + src_bytes = N * H * W * dtype_size + dst_bytes = N * dst_H * dst_W * dtype_size + if is_fake_planar: + # reformat(NCHW->NHWC) + resize + reformat(NHWC->NCHW) + state.add_global_memory_reads(2 * src_bytes + dst_bytes) + state.add_global_memory_writes(src_bytes + 2 * dst_bytes) + else: + state.add_global_memory_reads(src_bytes) + state.add_global_memory_writes(dst_bytes) + + get_stream = create_stream_cache() + + if is_fake_planar: + # Tensor-only "fake planar": planar->interleaved->resize->interleaved->planar, + # all timed, as the comparison baseline for the native planar (NCHW) path. + src = create_tensor( + (N, nc, H, W), dtype, device_id, layout="NCHW", fill_mode="checkerboard" + ) + inter_src = create_tensor( + (N, H, W, nc), dtype, device_id, layout="NHWC", fill_mode=0 + ) + inter_dst = create_tensor( + (N, dst_H, dst_W, nc), dtype, device_id, layout="NHWC", fill_mode=0 + ) + dst = create_tensor( + (N, nc, dst_H, dst_W), dtype, device_id, layout="NCHW", fill_mode=0 + ) + + def run_fake(launch): + stream = get_stream(launch) + cvcuda.reformat_into(inter_src, src, stream=stream) + cvcuda.resize_into(inter_dst, inter_src, interp, stream=stream) + cvcuda.reformat_into(dst, inter_dst, stream=stream) + + return run_fake + + if input_kind == "Tensor": + src_shape = (N, nc, H, W) if is_planar else (N, H, W, nc) + dst_shape = (N, nc, dst_H, dst_W) if is_planar else (N, dst_H, dst_W, nc) + src = create_tensor( + src_shape, dtype, device_id, layout=layout, fill_mode="checkerboard" + ) + dst = create_tensor(dst_shape, dtype, device_id, layout=layout, fill_mode=0) + else: + img_format = get_format_from_dtype(dtype_str, nc, planar=is_planar) + src = create_image_batch_varshape( + (N, H, W, nc), + 0, + img_format, + dtype=dtype, + device=device_id, + fill_mode="checkerboard", + ) + dst = create_image_batch_varshape( + (N, dst_H, dst_W, nc), + 0, + img_format, + dtype=dtype, + device=device_id, + fill_mode=0, + ) + + def run(launch): + stream = get_stream(launch) + cvcuda.resize_into(dst, src, interp, stream=stream) + + return run + + +if __name__ == "__main__": + run_benchmark("resize", resize) diff --git a/bench/python/ops/bench_resizecropconvertreformat.py b/bench/python/ops/bench_resizecropconvertreformat.py new file mode 100755 index 000000000..37e0d7d1b --- /dev/null +++ b/bench/python/ops/bench_resizecropconvertreformat.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""CV-CUDA ResizeCropConvertReformat operator benchmark. + +Python equivalent of BenchResizeCropConvertReformat.cpp +""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import math # noqa: E402 + +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + get_input_kind, + parse_shape, + get_num_channels, + get_dtype_size, + get_interpolation_type, + get_format_from_dtype, + create_tensor, + create_image_batch_varshape, + create_stream_cache, + run_benchmark, +) + + +def resizecropconvertreformat(state): + """ResizeCropConvertReformat fused operator benchmark matching C++ BenchResizeCropConvertReformat.cpp""" + + shape = parse_shape(state.get_string("shape")) + dtype_str = state.get_string("InOutDataType") + input_kind = get_input_kind(state.get_string("inputKind")) + interp = get_interpolation_type(state.get_string("interpolation")) + layout = state.get_string("layout") + device_id = state.get_device() + + N, H, W = shape + nc = get_num_channels(dtype_str) + is_planar = layout == "NCHW" + is_fake_planar = layout == "NCHW_FAKE" + + if layout not in ("NHWC", "NCHW", "NCHW_FAKE"): + state.skip( + "ResizeCropConvertReformat benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts" + ) + return None + + if is_fake_planar and input_kind == "VarShape": + state.skip( + "Fake-planar (NCHW_FAKE) ResizeCropConvertReformat benchmark is tensor-only" + ) + return None + + if H <= 1 or W <= 1: + state.skip("Height and width must be > 1 for resize/crop operations") + return None + + resize_h, resize_w = H // 2, W // 2 + crop_x, crop_y = 0, 0 + crop_w = max(1, min(512, resize_w - 1)) + crop_h = max(1, min(512, resize_h - 1)) + + dst_shape = ( + (N, nc, crop_h, crop_w) + if is_planar or is_fake_planar + else (N, crop_h, crop_w, nc) + ) + + scale_x = W / resize_w + scale_y = H / resize_h + src_region_w = min(math.ceil((crop_x + crop_w) * scale_x) + 2, W) + src_region_h = min(math.ceil((crop_y + crop_h) * scale_y) + 2, H) + + dtype_size = get_dtype_size(dtype_str) + src_bytes = N * H * W * nc * dtype_size + dst_bytes = N * crop_h * crop_w * nc * dtype_size + state.add_global_memory_reads( + N * src_region_h * src_region_w * nc * dtype_size + + (src_bytes if is_fake_planar else 0) + ) + state.add_global_memory_writes(dst_bytes + (src_bytes if is_fake_planar else 0)) + + get_stream = create_stream_cache() + + if input_kind == "Tensor": + if is_planar or is_fake_planar: + src = create_tensor( + (N, nc, H, W), + dtype_str, + device_id, + layout="NCHW", + fill_mode="checkerboard", + ) + dst = create_tensor( + dst_shape, dtype_str, device_id, layout="NCHW", fill_mode=0 + ) + else: + src = create_tensor( + (N, H, W, nc), + dtype_str, + device_id, + layout="NHWC", + fill_mode="checkerboard", + ) + dst = create_tensor( + dst_shape, dtype_str, device_id, layout="NHWC", fill_mode=0 + ) + + if is_fake_planar: + inter_src = create_tensor( + (N, H, W, nc), dtype_str, device_id, layout="NHWC", fill_mode=0 + ) + + def run_fake(launch): + stream = get_stream(launch) + cvcuda.reformat_into(inter_src, src, stream=stream) + cvcuda.resize_crop_convert_reformat_into( + dst, + inter_src, + (resize_w, resize_h), + interp, + (crop_x, crop_y), + manip=cvcuda.ChannelManip.NO_OP, + stream=stream, + ) + + return run_fake + + def run(launch): + stream = get_stream(launch) + cvcuda.resize_crop_convert_reformat_into( + dst, + src, + (resize_w, resize_h), + interp, + (crop_x, crop_y), + manip=cvcuda.ChannelManip.NO_OP, + stream=stream, + ) + + return run + + else: + img_format = get_format_from_dtype(dtype_str, nc, planar=is_planar) + src = create_image_batch_varshape( + (N, H, W, nc), + 0, + img_format, + dtype=dtype_str, + device=device_id, + fill_mode="checkerboard", + ) + dst = create_tensor(dst_shape, dtype_str, device_id, layout=layout, fill_mode=0) + + def run(launch): + stream = get_stream(launch) + cvcuda.resize_crop_convert_reformat_into( + dst, + src, + (resize_w, resize_h), + interp, + (crop_x, crop_y), + manip=cvcuda.ChannelManip.NO_OP, + stream=stream, + ) + + return run + + +if __name__ == "__main__": + run_benchmark("resizecropconvertreformat", resizecropconvertreformat) diff --git a/bench/python/ops/bench_rotate.py b/bench/python/ops/bench_rotate.py new file mode 100755 index 000000000..9aa0fa1c9 --- /dev/null +++ b/bench/python/ops/bench_rotate.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""CV-CUDA Rotate operator benchmark - Python equivalent of BenchRotate.cpp""" + +import math +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cupy as cp # noqa: E402 +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + get_input_kind, + parse_shape, + get_dtype, + get_dtype_size, + get_num_channels, + get_interpolation_type, + get_format_from_dtype, + create_tensor, + create_image_batch_varshape, + create_stream_cache, + run_benchmark, +) + + +def _rotate_in_bounds_fraction(angle_deg, x_shift, y_shift, w, h): + """Fraction of dst pixels in [0,W]x[0,H] whose inverse rotation lands inside source. + + The rotate kernel skips both read and write outside this region (rotate.cu:61), + so the byte model is scaled by this fraction to track HBM traffic. + """ + th = math.radians(angle_deg) + c, s = math.cos(th), math.sin(th) + + # Kernel mapping is src = R(+θ)·(dst - shift), so the in-bounds dst region is + # the image of [0,W]x[0,H] under dst = R(-θ)·src + shift. + def m(x, y): + return (c * x + s * y + x_shift, -s * x + c * y + y_shift) + + poly = [m(0.0, 0.0), m(w, 0.0), m(w, h), m(0.0, h)] + + def clip(in_pts, axis, bound, keep_above): + if not in_pts: + return [] + out = [] + for i, cur in enumerate(in_pts): + prev = in_pts[i - 1] + ci = (cur[axis] >= bound) if keep_above else (cur[axis] <= bound) + pi = (prev[axis] >= bound) if keep_above else (prev[axis] <= bound) + if ci: + if not pi: + t = (bound - prev[axis]) / (cur[axis] - prev[axis]) + out.append( + ( + prev[0] + t * (cur[0] - prev[0]), + prev[1] + t * (cur[1] - prev[1]), + ) + ) + out.append(cur) + elif pi: + t = (bound - prev[axis]) / (cur[axis] - prev[axis]) + out.append( + (prev[0] + t * (cur[0] - prev[0]), prev[1] + t * (cur[1] - prev[1])) + ) + return out + + poly = clip(poly, 0, 0.0, True) + poly = clip(poly, 0, w, False) + poly = clip(poly, 1, 0.0, True) + poly = clip(poly, 1, h, False) + if len(poly) < 3: + return 0.0 + area = 0.0 + n = len(poly) + for i in range(n): + j = (i + 1) % n + area += poly[i][0] * poly[j][1] - poly[j][0] * poly[i][1] + return abs(area) * 0.5 / (w * h) + + +def rotate(state): + """Rotate operator benchmark matching C++ BenchRotate.cpp""" + + shape = parse_shape(state.get_string("shape")) + dtype_str = state.get_string("InOutDataType") + _ = get_dtype(dtype_str) + try: + layout = state.get_string("layout") + except Exception: + layout = "NHWC" + input_kind = get_input_kind(state.get_string("inputKind")) + interp = get_interpolation_type(state.get_string("interpolation")) + device_id = state.get_device() + + N, H, W = shape + nc = get_num_channels(dtype_str) + + if layout not in ("NHWC", "NCHW", "NCHW_FAKE"): + state.skip("Rotate benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts") + return None + + is_planar = layout == "NCHW" + is_fake_planar = layout == "NCHW_FAKE" + if is_fake_planar and input_kind == "VarShape": + state.skip("Fake-planar (NCHW_FAKE) rotate benchmark is tensor-only") + return None + + # Rotation around image center: keeps the bulk of dst pixels inside source so + # the kernel actually exercises read+interp+write rather than the out-of-bounds + # early-exit guard at rotate.cu:61. + angle_deg = 30.0 + th = math.radians(angle_deg) + cx, cy = W * 0.5, H * 0.5 + x_shift = cx * (1.0 - math.cos(th)) - cy * math.sin(th) + y_shift = cy * (1.0 - math.cos(th)) + cx * math.sin(th) + shift = (x_shift, y_shift) + + in_bounds_frac = _rotate_in_bounds_fraction(angle_deg, x_shift, y_shift, W, H) + bytes_full = N * H * W * get_dtype_size(dtype_str) + # Native rotate only touches the in-bounds region; the two reformats in the fake-planar path move + # the full tensor each way (reformat is full-coverage), so add 2*bytes_full on top of the rotate. + bytes_io = int( + (2 * bytes_full if is_fake_planar else 0) + in_bounds_frac * bytes_full + ) + state.add_global_memory_reads(bytes_io) + state.add_global_memory_writes(bytes_io) + + get_stream = create_stream_cache() + + if is_fake_planar: + # Tensor-only "fake planar": planar->interleaved->rotate->interleaved->planar, + # all timed, as the comparison baseline for the native planar (NCHW) path. + src = create_tensor( + (N, nc, H, W), dtype_str, device_id, layout="NCHW", fill_mode="lcg" + ) + inter_src = create_tensor( + (N, H, W, nc), dtype_str, device_id, layout="NHWC", fill_mode=0 + ) + inter_dst = create_tensor( + (N, H, W, nc), dtype_str, device_id, layout="NHWC", fill_mode=0 + ) + dst = create_tensor( + (N, nc, H, W), dtype_str, device_id, layout="NCHW", fill_mode=0 + ) + + def run_fake(launch): + stream = get_stream(launch) + cvcuda.reformat_into(inter_src, src, stream=stream) + cvcuda.rotate_into( + inter_dst, inter_src, angle_deg, shift, interp, stream=stream + ) + cvcuda.reformat_into(dst, inter_dst, stream=stream) + + return run_fake + + if input_kind == "Tensor": + tshape = (N, nc, H, W) if is_planar else (N, H, W, nc) + src = create_tensor( + tshape, dtype_str, device_id, layout=layout, fill_mode="lcg" + ) + dst = create_tensor(tshape, dtype_str, device_id, layout=layout, fill_mode=0) + + def run(launch): + stream = get_stream(launch) + cvcuda.rotate_into(dst, src, angle_deg, shift, interp, stream=stream) + + return run + + else: + img_format = get_format_from_dtype(dtype_str, nc, planar=is_planar) + src = create_image_batch_varshape( + (N, H, W, nc), + 0, + img_format, + dtype=dtype_str, + device=device_id, + fill_mode="lcg", + ) + dst = create_image_batch_varshape( + (N, H, W, nc), + 0, + img_format, + dtype=dtype_str, + device=device_id, + fill_mode=0, + ) + + with cp.cuda.Device(device_id): + angle_data = cp.full((N,), angle_deg, dtype=cp.float64) + shift_data = cp.zeros((N, 2), dtype=cp.float64) + shift_data[:, 0] = shift[0] + shift_data[:, 1] = shift[1] + angle_tensor = cvcuda.as_tensor(angle_data, "N") + shift_tensor = cvcuda.as_tensor(shift_data, "NW") + + def run(launch): + stream = get_stream(launch) + cvcuda.rotate_into( + dst, src, angle_tensor, shift_tensor, interp, stream=stream + ) + + return run + + +if __name__ == "__main__": + run_benchmark("rotate", rotate) diff --git a/bench/python/ops/bench_sift.py b/bench/python/ops/bench_sift.py new file mode 100755 index 000000000..cd99090fc --- /dev/null +++ b/bench/python/ops/bench_sift.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""CV-CUDA SIFT operator benchmark - Python equivalent of BenchSIFT.cpp""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + get_input_kind, + parse_shape, + create_tensor, + create_stream_cache, + run_benchmark, +) + + +def sift(state): + """SIFT operator benchmark matching C++ BenchSIFT.cpp""" + + shape = parse_shape(state.get_string("shape")) + input_kind = get_input_kind(state.get_string("inputKind")) + layout = state.get_string("layout") + expand_input = state.get_string("expandInput") == "Y" + capacity = state.get_int64("maxCapacity") + num_octave_layers = state.get_int64("numOctaveLayers") + contrast_threshold = state.get_float64("contrastThreshold") + edge_threshold = state.get_float64("edgeThreshold") + init_sigma = state.get_float64("initSigma") + device_id = state.get_device() + + N, H, W = shape + if layout not in ("NHWC", "NCHW"): + raise ValueError(f"Invalid layout = {layout}") + + input_shape = (N, 1, H, W) if layout == "NCHW" else (N, H, W, 1) + + flags = ( + cvcuda.SIFT.USE_EXPANDED_INPUT + if expand_input + else cvcuda.SIFT.USE_ORIGINAL_INPUT + ) + + if expand_input: + max_w, max_h = W * 2, H * 2 + else: + max_w, max_h = W, H + + pyr_size = (num_octave_layers + 3) * N * (max_w * max_h * 2) * 4 + + dtype_size = 1 + state.add_global_memory_reads(N * H * W * dtype_size + 2 * pyr_size) + state.add_global_memory_writes( + 2 * pyr_size + N * 4 + N * capacity * (16 + 12 + 128 * dtype_size) + ) + + if input_kind == "VarShape": + state.skip("ImageBatchVarShape not implemented for this operator") + return None + + src = create_tensor(input_shape, "uint8", device_id, layout=layout, fill_mode="lcg") + + feat_coords = cvcuda.Tensor((N, capacity, 4), cvcuda.Type.F32, rowalign=1) + feat_metadata = cvcuda.Tensor((N, capacity, 3), cvcuda.Type.F32, rowalign=1) + feat_descriptors = cvcuda.Tensor((N, capacity, 128), cvcuda.Type.U8, rowalign=1) + num_features = cvcuda.Tensor((N, 1), cvcuda.Type.S32, rowalign=1) + + get_stream = create_stream_cache() + + def run(launch): + stream = get_stream(launch) + cvcuda.sift_into( + feat_coords, + feat_metadata, + feat_descriptors, + num_features, + src, + num_octave_layers=num_octave_layers, + contrast_threshold=contrast_threshold, + edge_threshold=edge_threshold, + init_sigma=init_sigma, + flags=flags, + stream=stream, + ) + + return run + + +if __name__ == "__main__": + run_benchmark("sift", sift) diff --git a/bench/python/ops/bench_solarize.py b/bench/python/ops/bench_solarize.py new file mode 100755 index 000000000..ba0ed39bc --- /dev/null +++ b/bench/python/ops/bench_solarize.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""CV-CUDA Solarize operator benchmark - Python equivalent of BenchSolarize.cpp""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + get_input_kind, + parse_shape, + get_dtype, + get_dtype_size, + get_num_channels, + get_format_from_dtype, + create_tensor, + create_image_batch_varshape, + create_stream_cache, + run_benchmark, +) + +# Threshold value is irrelevant to throughput; use a fixed mid-range value. +THRESHOLD = 128.0 + + +def solarize(state): + """Solarize operator benchmark matching C++ BenchSolarize.cpp""" + + N, H, W = parse_shape(state.get_string("shape")) + dtype_str = state.get_string("InOutDataType") + dtype = get_dtype(dtype_str) + num_channels = get_num_channels(dtype_str) + try: + layout = state.get_string("layout") + except Exception: + layout = "NHWC" + input_kind = get_input_kind(state.get_string("inputKind")) + device_id = state.get_device() + + if layout not in ("NHWC", "NCHW", "NCHW_FAKE"): + state.skip("Solarize benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts") + return None + + is_planar = layout == "NCHW" + is_fake_planar = layout == "NCHW_FAKE" + if is_fake_planar and input_kind == "VarShape": + state.skip("Fake-planar (NCHW_FAKE) solarize benchmark is tensor-only") + return None + + # Solarize preserves size. + dtype_size = get_dtype_size(dtype_str) + bytes_ = N * H * W * dtype_size + if is_fake_planar: + # reformat(NCHW->NHWC) + solarize + reformat(NHWC->NCHW) + state.add_global_memory_reads(3 * bytes_) + state.add_global_memory_writes(3 * bytes_) + else: + state.add_global_memory_reads(bytes_) + state.add_global_memory_writes(bytes_) + + get_stream = create_stream_cache() + + if is_fake_planar: + src = create_tensor( + (N, num_channels, H, W), + dtype, + device_id, + layout="NCHW", + fill_mode="checkerboard", + ) + inter_src = create_tensor( + (N, H, W, num_channels), dtype, device_id, layout="NHWC", fill_mode=0 + ) + inter_dst = create_tensor( + (N, H, W, num_channels), dtype, device_id, layout="NHWC", fill_mode=0 + ) + dst = create_tensor( + (N, num_channels, H, W), dtype, device_id, layout="NCHW", fill_mode=0 + ) + + def run_fake(launch): + stream = get_stream(launch) + cvcuda.reformat_into(inter_src, src, stream=stream) + cvcuda.solarize_into(inter_dst, inter_src, THRESHOLD, stream=stream) + cvcuda.reformat_into(dst, inter_dst, stream=stream) + + return run_fake + + if input_kind == "Tensor": + shape = (N, num_channels, H, W) if is_planar else (N, H, W, num_channels) + src = create_tensor( + shape, dtype, device_id, layout=layout, fill_mode="checkerboard" + ) + dst = create_tensor(shape, dtype, device_id, layout=layout, fill_mode=0) + else: # ImageBatchVarShape + img_format = get_format_from_dtype(dtype_str, num_channels, planar=is_planar) + src = create_image_batch_varshape( + (N, H, W, num_channels), + 0, + img_format, + dtype, + device_id, + fill_mode="checkerboard", + ) + dst = create_image_batch_varshape( + (N, H, W, num_channels), 0, img_format, dtype, device_id, fill_mode=0 + ) + + def run(launch): + cvcuda.solarize_into(dst, src, THRESHOLD, stream=get_stream(launch)) + + return run + + +if __name__ == "__main__": + run_benchmark("solarize", solarize) diff --git a/bench/python/ops/bench_stack.py b/bench/python/ops/bench_stack.py new file mode 100755 index 000000000..0d24d0aab --- /dev/null +++ b/bench/python/ops/bench_stack.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""CV-CUDA Stack operator benchmark - Python equivalent of BenchStack.cpp""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + parse_shape, + get_dtype, + get_dtype_size, + get_num_channels, + get_format_from_dtype, + get_input_kind, + create_tensor, + create_image_batch_varshape, + create_stream_cache, + run_benchmark, +) + + +def stack(state): + """Stack operator benchmark matching C++ BenchStack.cpp""" + + shape = parse_shape(state.get_string("shape")) + dtype_str = state.get_string("InOutDataType") + dtype = get_dtype(dtype_str) + input_kind = get_input_kind(state.get_string("inputKind")) + try: + layout = state.get_string("layout") + except KeyError: + layout = "NHWC" + device_id = state.get_device() + + N, H, W = shape + ch = get_num_channels(dtype_str) + + if layout not in ("NHWC", "NCHW", "NCHW_FAKE"): + state.skip("Stack benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts") + return None + is_planar = layout == "NCHW" + is_fake_planar = layout == "NCHW_FAKE" + if is_fake_planar and input_kind != "Tensor": + state.skip("Fake-planar (NCHW_FAKE) Stack benchmark is TensorBatch-only") + return None + + dtype_size = get_dtype_size(dtype_str) + bytes_ = N * H * W * dtype_size + state.add_global_memory_reads((3 if is_fake_planar else 1) * bytes_) + state.add_global_memory_writes((3 if is_fake_planar else 1) * bytes_) + + get_stream = create_stream_cache() + + if is_fake_planar: + planar_src_tensors = [] + inter_src_tensors = [] + inter_src = cvcuda.TensorBatch(N) + for _ in range(N): + planar_src = create_tensor( + (ch, H, W), dtype, device_id, layout="CHW", fill_mode="checkerboard" + ) + interleaved_src = create_tensor( + (H, W, ch), dtype, device_id, layout="HWC", fill_mode=0 + ) + planar_src_tensors.append(planar_src) + inter_src_tensors.append(interleaved_src) + inter_src.pushback(interleaved_src) + + inter_dst = create_tensor( + (N, H, W, ch), dtype, device_id, layout="NHWC", fill_mode=0 + ) + dst = create_tensor((N, ch, H, W), dtype, device_id, layout="NCHW", fill_mode=0) + + def run_fake(launch): + stream = get_stream(launch) + for planar_src, interleaved_src in zip( + planar_src_tensors, inter_src_tensors, strict=True + ): + cvcuda.reformat_into(interleaved_src, planar_src, stream=stream) + cvcuda.stack_into(inter_dst, inter_src, stream=stream) + cvcuda.reformat_into(dst, inter_dst, stream=stream) + + return run_fake + + dst_shape = (N, ch, H, W) if is_planar else (N, H, W, ch) + dst = create_tensor(dst_shape, dtype, device_id, layout=layout, fill_mode=0) + + if input_kind == "Tensor": + src = cvcuda.TensorBatch(N) + src_layout = "CHW" if is_planar else "HWC" + src_shape = (ch, H, W) if is_planar else (H, W, ch) + for _ in range(N): + t = create_tensor( + src_shape, dtype, device_id, layout=src_layout, fill_mode="checkerboard" + ) + src.pushback(t) + else: + img_format = get_format_from_dtype(dtype_str, ch, planar=is_planar) + src = create_image_batch_varshape( + (N, H, W, ch), + 0, + img_format, + dtype, + device_id, + fill_mode="checkerboard", + ) + + def run(launch): + stream = get_stream(launch) + cvcuda.stack_into(dst, src, stream=stream) + + return run + + +if __name__ == "__main__": + run_benchmark("stack", stack) diff --git a/bench/python/ops/bench_threshold.py b/bench/python/ops/bench_threshold.py new file mode 100755 index 000000000..70a340e4f --- /dev/null +++ b/bench/python/ops/bench_threshold.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""CV-CUDA Threshold operator benchmark - Python equivalent of BenchThreshold.cpp""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + get_input_kind, + parse_shape, + get_dtype, + get_num_channels, + get_dtype_size, + get_format_from_dtype, + create_tensor, + create_stream_cache, + create_image_batch_varshape, + run_benchmark, +) + + +def threshold(state): + """Threshold operator benchmark matching C++ BenchThreshold.cpp""" + + shape = parse_shape(state.get_string("shape")) + dtype_str = state.get_string("InOutDataType") + dtype = get_dtype(dtype_str) + num_channels = get_num_channels(dtype_str) + input_kind = get_input_kind(state.get_string("inputKind")) + try: + layout = state.get_string("layout") + except Exception: + layout = "NHWC" + device_id = state.get_device() + + N, H, W = shape + + if dtype_str in ("uint8", "U8") and num_channels == 1: + thresh_type = cvcuda.ThresholdType.BINARY | cvcuda.ThresholdType.OTSU + else: + thresh_type = cvcuda.ThresholdType.BINARY + + if layout not in ("NHWC", "NCHW", "NCHW_FAKE"): + state.skip( + "Threshold benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts" + ) + return None + is_planar = layout == "NCHW" + is_fake_planar = layout == "NCHW_FAKE" + if is_fake_planar and input_kind == "VarShape": + state.skip("Fake-planar (NCHW_FAKE) Threshold benchmark is tensor-only") + return None + if is_planar and input_kind == "VarShape" and num_channels == 1: + state.skip( + "Single-channel varshape Threshold has no distinct planar image layout" + ) + return None + + dtype_size = get_dtype_size(dtype_str) + state.add_global_memory_reads(N * H * W * dtype_size) + state.add_global_memory_writes(N * H * W * dtype_size) + + get_stream = create_stream_cache() + + thresh = create_tensor((N,), "float64", device_id, layout="N", fill_mode="lcg") + maxval = create_tensor((N,), "float64", device_id, layout="N", fill_mode="lcg") + + if is_fake_planar: + src = create_tensor( + (N, num_channels, H, W), + dtype, + device_id, + layout="NCHW", + fill_mode="checkerboard", + ) + inter_src = create_tensor( + (N, H, W, num_channels), dtype, device_id, layout="NHWC", fill_mode=0 + ) + inter_dst = create_tensor( + (N, H, W, num_channels), dtype, device_id, layout="NHWC", fill_mode=0 + ) + dst = create_tensor( + (N, num_channels, H, W), dtype, device_id, layout="NCHW", fill_mode=0 + ) + + def run_fake(launch): + stream = get_stream(launch) + cvcuda.reformat_into(inter_src, src, stream=stream) + cvcuda.threshold_into( + inter_dst, inter_src, thresh, maxval, thresh_type, stream=stream + ) + cvcuda.reformat_into(dst, inter_dst, stream=stream) + + return run_fake + + if input_kind == "Tensor": + tensor_shape = (N, num_channels, H, W) if is_planar else (N, H, W, num_channels) + src = create_tensor( + tensor_shape, dtype, device_id, layout=layout, fill_mode="checkerboard" + ) + dst = create_tensor(tensor_shape, dtype, device_id, layout=layout, fill_mode=0) + + def run(launch): + stream = get_stream(launch) + cvcuda.threshold_into(dst, src, thresh, maxval, thresh_type, stream=stream) + + return run + + else: + img_format = get_format_from_dtype(dtype_str, num_channels, planar=is_planar) + + src = create_image_batch_varshape( + (N, H, W, num_channels), + 0, + img_format, + dtype, + device_id, + fill_mode="checkerboard", + ) + dst = create_image_batch_varshape( + (N, H, W, num_channels), 0, img_format, dtype, device_id, fill_mode=0 + ) + + def run(launch): + stream = get_stream(launch) + cvcuda.threshold_into(dst, src, thresh, maxval, thresh_type, stream=stream) + + return run + + +if __name__ == "__main__": + run_benchmark("threshold", threshold) diff --git a/bench/python/ops/bench_warpaffine.py b/bench/python/ops/bench_warpaffine.py new file mode 100755 index 000000000..53932d833 --- /dev/null +++ b/bench/python/ops/bench_warpaffine.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""CV-CUDA WarpAffine operator benchmark - Python equivalent of BenchWarpAffine.cpp""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cupy as cp # noqa: E402 +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + get_input_kind, + parse_shape, + get_dtype, + get_dtype_size, + get_num_channels, + get_format_from_dtype, + get_border_type, + get_interpolation_type, + create_tensor, + create_stream_cache, + create_image_batch_varshape, + run_benchmark, +) + + +def warpaffine(state): + """WarpAffine operator benchmark matching C++ BenchWarpAffine.cpp""" + + shape = parse_shape(state.get_string("shape")) + dtype_str = state.get_string("InOutDataType") + dtype = get_dtype(dtype_str) + try: + layout = state.get_string("layout") + except Exception: + layout = "NHWC" + border = get_border_type(state.get_string("border")) + interp = get_interpolation_type(state.get_string("interpolation")) + inverse_map = state.get_string("inverseMap") == "Y" + input_kind = get_input_kind(state.get_string("inputKind")) + device_id = state.get_device() + + N, H, W = shape + nc = get_num_channels(dtype_str) + + if layout not in ("NHWC", "NCHW", "NCHW_FAKE"): + state.skip( + "WarpAffine benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts" + ) + return None + + is_planar = layout == "NCHW" + is_fake_planar = layout == "NCHW_FAKE" + if is_fake_planar and input_kind == "VarShape": + state.skip("Fake-planar (NCHW_FAKE) WarpAffine benchmark is tensor-only") + return None + + flags = interp + if inverse_map: + flags = flags | cvcuda.Interp.WARP_INVERSE_MAP + + # WarpAffine preserves size; the 6-float matrix is also read each launch. + dtype_size = get_dtype_size(dtype_str) + bytes_ = N * H * W * dtype_size + if is_fake_planar: + state.add_global_memory_reads(3 * bytes_ + 6 * 4) + state.add_global_memory_writes(3 * bytes_) + else: + state.add_global_memory_reads(bytes_ + 6 * 4) + state.add_global_memory_writes(bytes_) + + get_stream = create_stream_cache() + + if is_fake_planar: + # Tensor-only "fake planar": planar->interleaved->warp->interleaved->planar, all timed, as the + # comparison baseline for the native planar (NCHW) path. + src = create_tensor( + (N, nc, H, W), dtype, device_id, layout="NCHW", fill_mode="checkerboard" + ) + inter_src = create_tensor( + (N, H, W, nc), dtype, device_id, layout="NHWC", fill_mode=0 + ) + inter_dst = create_tensor( + (N, H, W, nc), dtype, device_id, layout="NHWC", fill_mode=0 + ) + dst = create_tensor((N, nc, H, W), dtype, device_id, layout="NCHW", fill_mode=0) + + xform = [[2.0, 2.0, 0.0], [3.0, 1.0, 0.0]] + + def run_fake(launch): + stream = get_stream(launch) + cvcuda.reformat_into(inter_src, src, stream=stream) + cvcuda.warp_affine_into( + inter_dst, + inter_src, + xform, + flags=flags, + border_mode=border, + border_value=(0, 0, 0, 0), + stream=stream, + ) + cvcuda.reformat_into(dst, inter_dst, stream=stream) + + return run_fake + + if input_kind == "Tensor": + tshape = (N, nc, H, W) if is_planar else (N, H, W, nc) + src = create_tensor( + tshape, dtype, device_id, layout=layout, fill_mode="checkerboard" + ) + dst = create_tensor(tshape, dtype, device_id, layout=layout, fill_mode=0) + + xform = [[2.0, 2.0, 0.0], [3.0, 1.0, 0.0]] + + def run(launch): + stream = get_stream(launch) + cvcuda.warp_affine_into( + dst, + src, + xform, + flags=flags, + border_mode=border, + border_value=(0, 0, 0, 0), + stream=stream, + ) + + return run + + else: + img_format = get_format_from_dtype(dtype_str, nc, planar=is_planar) + + src = create_image_batch_varshape( + (N, H, W, nc), + 0, + img_format, + dtype, + device_id, + fill_mode="checkerboard", + ) + dst = create_image_batch_varshape( + (N, H, W, nc), 0, img_format, dtype, device_id, fill_mode=0 + ) + + xform_data = cp.array([[2.0, 2.0, 0.0, 3.0, 1.0, 0.0]] * N, dtype=cp.float32) + xform_tensor = cvcuda.as_tensor(xform_data, "NW") + + def run(launch): + stream = get_stream(launch) + cvcuda.warp_affine_into( + dst, + src, + xform_tensor, + flags=flags, + border_mode=border, + border_value=(0, 0, 0, 0), + stream=stream, + ) + + return run + + +if __name__ == "__main__": + run_benchmark("warpaffine", warpaffine) diff --git a/bench/python/ops/bench_warpperspective.py b/bench/python/ops/bench_warpperspective.py new file mode 100755 index 000000000..6441514cf --- /dev/null +++ b/bench/python/ops/bench_warpperspective.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""CV-CUDA WarpPerspective operator benchmark - Python equivalent of BenchWarpPerspective.cpp""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cupy as cp # noqa: E402 +import numpy as np # noqa: E402 +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + get_input_kind, + parse_shape, + get_dtype, + get_dtype_size, + get_num_channels, + get_format_from_dtype, + get_border_type, + get_interpolation_type, + create_tensor, + create_stream_cache, + create_image_batch_varshape, + run_benchmark, +) + + +def warpperspective(state): + """WarpPerspective operator benchmark matching C++ BenchWarpPerspective.cpp""" + + shape = parse_shape(state.get_string("shape")) + dtype_str = state.get_string("InOutDataType") + dtype = get_dtype(dtype_str) + try: + layout = state.get_string("layout") + except KeyError: + layout = "NHWC" + border = get_border_type(state.get_string("border")) + interp = get_interpolation_type(state.get_string("interpolation")) + inverse_map = state.get_string("inverseMap") == "Y" + input_kind = get_input_kind(state.get_string("inputKind")) + device_id = state.get_device() + + N, H, W = shape + nc = get_num_channels(dtype_str) + + if layout not in ("NHWC", "NCHW", "NCHW_FAKE"): + state.skip( + "WarpPerspective benchmark supports only NHWC, NCHW, and NCHW_FAKE layouts" + ) + return None + + is_planar = layout == "NCHW" + is_fake_planar = layout == "NCHW_FAKE" + if is_fake_planar and input_kind == "VarShape": + state.skip("Fake-planar (NCHW_FAKE) WarpPerspective benchmark is tensor-only") + return None + + flags = interp + if inverse_map: + flags = flags | cvcuda.Interp.WARP_INVERSE_MAP + + dtype_size = get_dtype_size(dtype_str) + bytes_ = N * H * W * dtype_size + if is_fake_planar: + state.add_global_memory_reads(3 * bytes_ + 9 * 4) + state.add_global_memory_writes(3 * bytes_) + else: + state.add_global_memory_reads(bytes_ + 9 * 4) + state.add_global_memory_writes(bytes_) + + get_stream = create_stream_cache() + + xform_values = [0.27, 0.16, 0.00, -0.11, 0.61, 0.65, -0.09, 0.06, 1.00] + + if is_fake_planar: + src = create_tensor( + (N, nc, H, W), dtype, device_id, layout="NCHW", fill_mode="checkerboard" + ) + inter_src = create_tensor( + (N, H, W, nc), dtype, device_id, layout="NHWC", fill_mode=0 + ) + inter_dst = create_tensor( + (N, H, W, nc), dtype, device_id, layout="NHWC", fill_mode=0 + ) + dst = create_tensor((N, nc, H, W), dtype, device_id, layout="NCHW", fill_mode=0) + + xform = np.array( + [xform_values[0:3], xform_values[3:6], xform_values[6:9]], + dtype=np.float32, + ) + border_value_np = np.array([0, 0, 0, 0], dtype=np.float32) + + def run_fake(launch): + stream = get_stream(launch) + cvcuda.reformat_into(inter_src, src, stream=stream) + cvcuda.warp_perspective_into( + inter_dst, + inter_src, + xform, + flags=flags, + border_mode=border, + border_value=border_value_np, + stream=stream, + ) + cvcuda.reformat_into(dst, inter_dst, stream=stream) + + return run_fake + + if input_kind == "Tensor": + tshape = (N, nc, H, W) if is_planar else (N, H, W, nc) + src = create_tensor( + tshape, dtype, device_id, layout=layout, fill_mode="checkerboard" + ) + dst = create_tensor(tshape, dtype, device_id, layout=layout, fill_mode=0) + + # Pre-convert the 3x3 xform to a contiguous host numpy array ONCE. + # The C++ Python wrapper (OpWarpPerspective.cpp) takes `xform` as a + # pyarray and indexes it 9 times per call (xform.data(i, j) loop) — + # passing a fresh Python list of lists every iteration forces pybind11 + # to redo the list→ndarray conversion in the per-call hot path, which + # adds 50-100us of host overhead per submit and surfaces as a parity- + # gate trip on uint8 (compute-bound, ~1.8ms kernel where 50us is 3%). + # The varshape branch already pre-builds an NW-layout cvcuda.Tensor + # and passes that — so this branch was the only place still paying + # the per-iteration conversion. + xform = np.array( + [xform_values[0:3], xform_values[3:6], xform_values[6:9]], + dtype=np.float32, + ) + border_value_np = np.array([0, 0, 0, 0], dtype=np.float32) + + def run(launch): + stream = get_stream(launch) + cvcuda.warp_perspective_into( + dst, + src, + xform, + flags=flags, + border_mode=border, + border_value=border_value_np, + stream=stream, + ) + + return run + + else: + img_format = get_format_from_dtype(dtype_str, nc, planar=is_planar) + + src = create_image_batch_varshape( + (N, H, W, nc), + 0, + img_format, + dtype, + device_id, + fill_mode="checkerboard", + ) + dst = create_image_batch_varshape( + (N, H, W, nc), 0, img_format, dtype, device_id, fill_mode=0 + ) + + xform_data = cp.array([xform_values] * N, dtype=cp.float32) + xform_tensor = cvcuda.as_tensor(xform_data, "NW") + + def run(launch): + stream = get_stream(launch) + cvcuda.warp_perspective_into( + dst, + src, + xform_tensor, + flags=flags, + border_mode=border, + border_value=(0, 0, 0, 0), + stream=stream, + ) + + return run + + +if __name__ == "__main__": + run_benchmark("warpperspective", warpperspective) diff --git a/bench/python/python_bench_utils.py b/bench/python/python_bench_utils.py new file mode 100755 index 000000000..7b316e83f --- /dev/null +++ b/bench/python/python_bench_utils.py @@ -0,0 +1,1123 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Utility functions for CV-CUDA Python benchmarks using nvbench. +Python equivalent of C++ CppBenchUtils.hpp +""" + +import sys +from pathlib import Path +from typing import Tuple, Union + +# Workaround for cuda-pathfinder >= 1.4 rejecting 'nvperf_target' and +# 'nvperf_host' as unknown library names. cuda.bench.__init__ tries to +# load these unconditionally via load_nvidia_dynamic_lib(); we patch the +# loader so unrecognised names are silently skipped instead of raising. +try: + import cuda.pathfinder as _pf + + _orig_load = _pf.load_nvidia_dynamic_lib + + def _tolerant_load(libname, *args, **kwargs): + try: + return _orig_load(libname, *args, **kwargs) + except Exception: + pass + + _pf.load_nvidia_dynamic_lib = _tolerant_load +except ImportError: + pass + +import cvcuda + +# Add config directory to path for imports (handles both source and build layouts) +_this_dir = Path(__file__).resolve().parent +_config_paths = [ + str(_this_dir / "config"), # Installed/build: bin/config/ + str(_this_dir.parent / "config"), # Source: bench/config/ +] +for _p in _config_paths: + sys.path.insert(0, _p) + +_support_paths = [str(_this_dir), str(_this_dir.parent)] +for _p in _support_paths: + sys.path.insert(0, _p) + +from _internal.warmup import resolve_warmup_iterations # noqa: E402 + +# Re-export config loading functions so benchmarks can import from python_bench_utils +# (maintains backwards compatibility) +try: + from load_config import ( # noqa: E402, F401 + BenchmarkConfig, + ConfigLoader, + load_bench_config, + load_operator_config, + register_axes_from_config, + generate_axis_args, + get_operator_from_benchmark_name, + get_configs_for_benchmark, + ) +except ImportError as e: + import warnings + + warnings.warn( + f"Failed to import load_config. Searched paths: {_config_paths}. " + f"python_bench_utils.py location: {Path(__file__).resolve()}. Error: {e}" + ) + raise + + +def _disable_cupy_memory_pool() -> None: + """Route all cuPy allocations through ``cudaMalloc`` instead of the cached pool. + + cuPy's default allocator caches freed blocks and reuses them at the same + physical pages. C++ benchmarks allocate via NVCV, which goes straight to + ``cudaMalloc``. Two different allocator policies on the two sides produce + systematically different physical-DRAM layouts for the kernel's working + set. On memory-bandwidth-bound operators (e.g. remap with >100 MB/kernel + streamed reads), DRAM bank-row conflicts depend on physical placement and + drive multi-percent wall-time differences that ncu cannot see — it counts + bytes and SM cycles, not bank-conflict latency. + + Switching cuPy to direct ``cudaMalloc`` removes the systematic placement + bias between the two paths. This is a partial fix: separate processes + still get different physical pages from the kernel-mode driver, so the + parity gate still has a process-level noise floor. The complete fix is a + single-process harness that runs both paths in one CUDA context. + """ + import cupy as cp + + cp.cuda.set_allocator(None) + + +_disable_cupy_memory_pool() + + +# nvcv is now part of cvcuda in 0.16.0 + + +def parse_shape(shape_str: str, delimiter: str = "x") -> Tuple[int, ...]: + """ + Parse shape string into tuple. + Equivalent to C++ GetShape function. + + Args: + shape_str: Shape string like "1x1080x1920" + delimiter: Delimiter character (default: 'x') + + Returns: + Tuple of integers representing the shape + + Examples: + >>> parse_shape("1x1080x1920") + (1, 1080, 1920) + """ + try: + return tuple(int(x) for x in shape_str.split(delimiter)) + except ValueError as e: + raise ValueError(f"Invalid shape string '{shape_str}': {e}") + + +def get_resize_output_shape(shape: Tuple[int, int, int], resize_type: str): + """Resolve legacy relative resize modes or an exact ``TARGET_HxW`` size.""" + N, H, W = shape + if resize_type == "EXPAND": + return N, H * 2, W * 2 + if resize_type == "CONTRACT": + return N, H // 2, W // 2 + if resize_type.startswith("TARGET_"): + target = parse_shape(resize_type.removeprefix("TARGET_")) + if len(target) != 2 or any(dim <= 0 for dim in target): + raise ValueError( + f"Resize target must contain two positive dimensions: {resize_type}" + ) + return N, *target + raise ValueError(f"Invalid resizeType: {resize_type}") + + +def get_dtype(dtype_str: str): + """ + Map dtype string to cvcuda.Type. + Equivalent to C++ GetDataType function. + + Supports both Python-style names (uint8, float32) and C++-style vector types + (uchar, uchar3, short4, etc.) which are mapped to their base types. + + Args: + dtype_str: String like "uint8", "float32", "float", "int32", "uchar3", "short4" + + Returns: + cvcuda.Type value (e.g., cvcuda.Type.U8, cvcuda.Type.F32) + """ + # C++ vector type to base type mapping + # uchar, uchar3, uchar4 -> uint8 + # char, char3, char4 -> int8 + # ushort, ushort3, ushort4 -> uint16 + # short, short3, short4 -> int16 + # uint, uint3, uint4 -> uint32 + # int, int3, int4 -> int32 + # float, float3, float4 -> float32 + # double, double3, double4 -> float64 + + # Strip vector suffix (1-4) if present + import re + + base_type = re.sub(r"[1-4]$", "", dtype_str) + + dtype_map = { + # Standard names + "uint8": cvcuda.Type.U8, + "uint16": cvcuda.Type.U16, + "uint32": cvcuda.Type.U32, + "int8": cvcuda.Type.S8, + "int16": cvcuda.Type.S16, + "int32": cvcuda.Type.S32, + "int": cvcuda.Type.S32, + "float32": cvcuda.Type.F32, + "float": cvcuda.Type.F32, + "float64": cvcuda.Type.F64, + "double": cvcuda.Type.F64, + # C++ type names (base) + "uchar": cvcuda.Type.U8, + "char": cvcuda.Type.S8, + "ushort": cvcuda.Type.U16, + "short": cvcuda.Type.S16, + "uint": cvcuda.Type.U32, + } + + # Try exact match first, then base type + if dtype_str in dtype_map: + return dtype_map[dtype_str] + elif base_type in dtype_map: + return dtype_map[base_type] + else: + raise ValueError( + f"Unsupported dtype: {dtype_str}. " f"Supported: {list(dtype_map.keys())}" + ) + + +def get_input_kind(value: str) -> str: + """Validate and return the ``inputKind`` config axis value. + + Selects which nvcv container a benchmark builds for its batch: ``Tensor`` + (one dense tensor) or ``VarShape`` (ImageBatchVarShape). Mirrors + ``get_interpolation_type``: maps the axis string to the accepted set and + raises on anything unexpected (no silent fallback). + """ + if value not in ("Tensor", "VarShape"): + raise ValueError(f"Unexpected inputKind = {value}") + return value + + +def get_num_channels(dtype_str: str) -> int: + """ + Get number of channels for a dtype string. + + Args: + dtype_str: String like "uint8", "uchar", "uchar3", "uchar4", "float3" + + Returns: + Number of channels (1, 3, or 4) + + Examples: + >>> get_num_channels("uint8") # 1 + >>> get_num_channels("uchar3") # 3 + >>> get_num_channels("uchar4") # 4 + """ + import re + + # Extract channel count from vector types (uchar3, float4, etc.) + # Look for patterns like "uchar3", "float4", etc. (not "uint8", "uint16") + # Match only if preceded by a letter (to avoid matching digits in uint8, uint16, etc.) + match = re.search(r"[a-z](\d)$", dtype_str, re.IGNORECASE) + if match: + channel_count = int(match.group(1)) + # Only return if it's a valid channel count (2, 3 or 4) + if channel_count in [2, 3, 4]: + return channel_count + return 1 + + +def get_dtype_size(dtype) -> int: + """ + Get element size in bytes for a dtype (including vector types). + + Args: + dtype: cvcuda.Type or dtype string (e.g., cvcuda.Type.U8, "uint8", "uchar4") + + Returns: + Total size in bytes (base_size * num_channels for string types) + """ + if isinstance(dtype, str): + base_dtype = get_dtype(dtype) # Gets base type + num_channels = get_num_channels(dtype) + base_size = { + cvcuda.Type.U8: 1, + cvcuda.Type.S8: 1, + cvcuda.Type.U16: 2, + cvcuda.Type.S16: 2, + cvcuda.Type.U32: 4, + cvcuda.Type.S32: 4, + cvcuda.Type.F32: 4, + cvcuda.Type.F64: 8, + }.get(base_dtype, 4) + return base_size * num_channels + else: + # Existing logic for cvcuda.Type + size_map = { + cvcuda.Type.U8: 1, + cvcuda.Type.S8: 1, + cvcuda.Type.U16: 2, + cvcuda.Type.S16: 2, + cvcuda.Type.U32: 4, + cvcuda.Type.S32: 4, + cvcuda.Type.F32: 4, + cvcuda.Type.F64: 8, + } + return size_map.get(dtype, 4) + + +def get_format_from_dtype( + dtype_str: str, num_channels: int = None, planar: bool = False +): + """ + Get cvcuda.Format from dtype string and channel count. + + Args: + dtype_str: Base dtype like "uint8", "float32" + num_channels: Number of channels (if None, inferred from dtype_str) + planar: Return a planar multi-plane format when available. + + Returns: + cvcuda.Format + + Examples: + >>> get_format_from_dtype("uint8", 1) # Format.U8 + >>> get_format_from_dtype("uchar4") # Format.RGBA8 + >>> get_format_from_dtype("float32", 3) # Format.RGBf32 + >>> get_format_from_dtype("uchar3", planar=True) # Format.RGB8p + """ + if num_channels is None: + num_channels = get_num_channels(dtype_str) + + base_dtype = get_dtype(dtype_str) + + # Map (base_type, channels) to Format + format_map = { + (cvcuda.Type.U8, 1): cvcuda.Format.U8, + (cvcuda.Type.U8, 3): cvcuda.Format.RGB8, + (cvcuda.Type.U8, 4): cvcuda.Format.RGBA8, + (cvcuda.Type.F32, 1): cvcuda.Format.F32, + (cvcuda.Type.F32, 2): cvcuda.Format._2F32, + (cvcuda.Type.F32, 3): cvcuda.Format.RGBf32, + (cvcuda.Type.F32, 4): cvcuda.Format.RGBAf32, + (cvcuda.Type.U16, 1): cvcuda.Format.U16, + (cvcuda.Type.S16, 1): cvcuda.Format.S16, + (cvcuda.Type.S16, 2): cvcuda.Format._2S16, + (cvcuda.Type.S32, 1): cvcuda.Format.S32, + (cvcuda.Type.F64, 1): cvcuda.Format.F64, + } + + key = (base_dtype, num_channels) + if planar: + planar_format_map = { + (cvcuda.Type.U8, 3): cvcuda.Format.RGB8p, + (cvcuda.Type.U8, 4): cvcuda.Format.RGBA8p, + (cvcuda.Type.F32, 3): cvcuda.Format.RGBf32p, + (cvcuda.Type.F32, 4): cvcuda.Format.RGBAf32p, + } + if key in planar_format_map: + return planar_format_map[key] + + if key in format_map: + return format_map[key] + raise ValueError( + f"Unsupported {'planar ' if planar else ''}format for " + f"{dtype_str} with {num_channels} channels" + ) + + +def get_stream(launch): + """ + Get/create cached CUDA stream from launch object. + + Caches stream per unique stream address to avoid recreating on each call, + but recreates if the stream address changes (e.g., between benchmark configs). + + Args: + launch: cuda.bench launch object, or None for warmup iterations + + Returns: + cvcuda stream object + + Usage in benchmarks: + from python_bench_utils import get_stream + # In run function: + operator(..., stream=get_stream(launch)) + """ + # During warmup, launch is None - use current CUDA stream + if launch is None: + if not hasattr(get_stream, "warmup_stream"): + get_stream.warmup_stream = cvcuda.Stream.current + return get_stream.warmup_stream + + stream_addr = launch.get_stream().addressof() + if not hasattr(get_stream, "stream") or get_stream.stream_addr != stream_addr: + get_stream.stream = cvcuda.as_stream(stream_addr) + get_stream.stream_addr = stream_addr + return get_stream.stream + + +def create_stream_cache(): + """ + Create a local stream cache for use in benchmark run functions. + + This is faster than get_stream() because it uses a closure-based cache + that avoids the overhead of hasattr() and global attribute lookups on + every iteration. Caches the warmup stream and the measurement stream + separately, since they ARE different streams: warmup runs on + cvcuda.Stream.current (typically the null stream), while nvbench's + state.exec hands launch objects whose get_stream() is the dedicated + measurement stream nvbench records its CUDA events on. + + Returning the warmup-time Stream.current for measurement runs is a + correctness bug: kernels submit to the null stream, but nvbench + measures launch.get_stream(), producing a bogus very-short measured + interval (event start/stop fire back-to-back on an idle stream). + Data-dependent ops (histogram, remap, sift, …) surface this as + Python-faster-than-C++ parity failures. + + Usage in benchmarks: + get_cached_stream = create_stream_cache() + + def run(launch): + operator(..., stream=get_cached_stream(launch)) + + # Warmup (launch will be None) + run_warmup(run, warmup_iterations) + state.exec(run, sync=True) + """ + warmup_stream = [None] + measure_stream = [None, None] # [stream, addr] + + def get_cached_stream(launch): + if launch is None: + # Warmup phase: nvbench hasn't created its measurement stream yet. + # Use the current CUDA stream so warmup ordering is well-defined. + if warmup_stream[0] is None: + warmup_stream[0] = cvcuda.Stream.current + return warmup_stream[0] + # Measurement phase: ALWAYS submit to launch.get_stream() so the + # kernel ends up on the same stream nvbench is timing. + addr = launch.get_stream().addressof() + if measure_stream[1] != addr: + measure_stream[0] = cvcuda.as_stream(addr) + measure_stream[1] = addr + return measure_stream[0] + + return get_cached_stream + + +def run_warmup(run_fn, iterations: int): + """ + Run warmup iterations before benchmark measurement. + + This mirrors the C++ warmup functionality in CppBenchUtils.hpp. + Warmup helps stabilize GPU clocks and ensures caches are warm + before actual measurements begin. + + Args: + run_fn: The benchmark function to run (takes launch as argument). + When called with None as launch, the run function should + use get_stream(None) or create_stream_cache()(None) which + will return the current CUDA stream. + iterations: Number of warmup iterations to execute + + Usage in benchmarks: + get_stream = create_stream_cache() + + def run(launch): + operator(..., stream=get_stream(launch)) + + # Run warmup before measurement (pass None as launch during warmup) + run_warmup(run, config.warmup_iterations) + state.exec(run, sync=True) + """ + iterations = resolve_warmup_iterations(iterations) + if iterations <= 0: + return + + import torch + + for _ in range(iterations): + run_fn(None) # Pass None as launch during warmup (no timing needed) + + # Synchronize to ensure all warmup work is complete + torch.cuda.synchronize() + + +def as_torch_cuda_stream(cuda_stream, device_id: int = 0): + """ + Convert a CUDA stream to a PyTorch CUDA stream. + + Args: + cuda_stream: A CUDA stream object (e.g., from cuda.bench launch.get_stream()) + device_id: The GPU device ID + + Returns: + torch.cuda.Stream: A PyTorch CUDA stream wrapping the given stream + """ + import torch + + return torch.cuda.ExternalStream( + cuda_stream.addressof(), + device=torch.device(f"cuda:{device_id}"), + ) + + +def run_benchmark(operator_name: str, benchmark_func): + """ + Standard entry point for benchmark scripts. + + Handles config loading, axis registration, warmup, and benchmark execution. + The benchmark function should return its run function instead of calling exec. + + Args: + operator_name: The operator name for config lookup (e.g., "resize", "gaussian") + benchmark_func: The benchmark function that returns a run(launch) callable + + Usage: + from python_bench_utils import run_benchmark + + def my_benchmark(state): + # Setup tensors, etc. + ... + def run(launch): + cvcuda.operator_into(dst, src, stream=get_stream(launch)) + return run # Return run function for framework to handle + + if __name__ == "__main__": + run_benchmark("my_operator", my_benchmark) + """ + import gc + import cuda.bench as bench + + gc.disable() + + config, bench_args = load_operator_config(operator_name) + warmup_iterations = config.warmup_iterations + + def wrapped_benchmark(state): + """Wrapper that handles warmup and execution.""" + # Keep setup and teardown state-local, matching the C++ harness. Automatic + # GC stays disabled while timing, so collect cycles explicitly afterward. + cvcuda.clear_cache() + run_fn = None + try: + run_fn = benchmark_func(state) + if run_fn is not None: + run_warmup(run_fn, warmup_iterations) + state.exec(run_fn, sync=True) + finally: + run_fn = None + cvcuda.clear_cache() + gc.collect() + + # Copy function name BEFORE registration (nvbench captures name at register time) + wrapped_benchmark.__name__ = benchmark_func.__name__ + b = bench.register(wrapped_benchmark) + b.add_string_axis("InOutDataType", config.dtypes) + register_axes_from_config(b, config) + bench.run_all_benchmarks(bench_args) + + +def get_interpolation_type(interp_str: str): + """ + Map interpolation string to CV-CUDA interpolation type. + Equivalent to C++ GetInterpolationType function. + + Args: + interp_str: String like "NEAREST", "LINEAR", "CUBIC", "AREA" + + Returns: + CV-CUDA interpolation type (NVCV_INTERP_*) + """ + interp_map = { + "NEAREST": cvcuda.Interp.NEAREST, + "LINEAR": cvcuda.Interp.LINEAR, + "CUBIC": cvcuda.Interp.CUBIC, + "AREA": cvcuda.Interp.AREA, + "LANCZOS": cvcuda.Interp.LANCZOS, + "GAUSSIAN": cvcuda.Interp.GAUSSIAN, + "HAMMING": cvcuda.Interp.HAMMING, + "BOX": cvcuda.Interp.BOX, + } + if interp_str not in interp_map: + raise ValueError( + f"Unsupported interpolation: {interp_str}. " + f"Supported: {list(interp_map.keys())}" + ) + return interp_map[interp_str] + + +def get_border_type(border_str: str): + """ + Map border string to CV-CUDA border type. + Equivalent to C++ GetBorderType function. + + Args: + border_str: String like "CONSTANT", "REPLICATE", "REFLECT", "WRAP", "REFLECT101" + + Returns: + CV-CUDA border type (NVCV_BORDER_*) + """ + border_map = { + "CONSTANT": cvcuda.Border.CONSTANT, + "REPLICATE": cvcuda.Border.REPLICATE, + "REFLECT": cvcuda.Border.REFLECT, + "WRAP": cvcuda.Border.WRAP, + "REFLECT101": cvcuda.Border.REFLECT101, + } + if border_str not in border_map: + raise ValueError( + f"Unsupported border: {border_str}. " + f"Supported: {list(border_map.keys())}" + ) + return border_map[border_str] + + +# --- Shared deterministic LCG fill ---------------------------------------- +# +# Mirrors bench/cpp/BenchFillKernels.cu's randomFillTypedKernel: same hash32 +# (low-32(seed XOR idx) XOR high-32(seed), 3 rounds of Numerical Recipes LCG) +# and same sample_typed mapping (truncate-to-T for ints, -1 + s * 2/4294967295 +# for floats). Both sides use the same seed → byte-identical buffers. +# +# Verified bit-for-bit equivalent to the C++ output across all supported types +# in /tmp/claude/dump_lcg_{cpp,py}.* (one-shot dev tool). + +_LCG_SEED = 0x9E3779B97F4A7C15 # matches CppBenchUtils.hpp FillTensor seed + +# (tag, cupy dtype, C type, is_float, kInv literal at C precision) +_LCG_TYPE_SPECS = ( + ("u8", "uint8", "unsigned char", False, None), + ("u16", "uint16", "unsigned short", False, None), + ("u32", "uint32", "unsigned int", False, None), + ("i8", "int8", "signed char", False, None), + ("i16", "int16", "short", False, None), + ("i32", "int32", "int", False, None), + ("f32", "float32", "float", True, "2.0f / 4294967295.0f"), + ("f64", "float64", "double", True, "2.0 / 4294967295.0"), +) + +_LCG_SRC_INT = r""" +extern "C" __global__ +void lcg_fill_{tag}({T} *data, unsigned long long n_elements, unsigned long long seed) +{{ + unsigned long long idx = (unsigned long long)blockIdx.x * (unsigned long long)blockDim.x + + (unsigned long long)threadIdx.x; + if (idx >= n_elements) return; + unsigned int s = (unsigned int)(seed ^ idx) ^ (unsigned int)(seed >> 32); + s = s * 1664525u + 1013904223u; + s = s * 1664525u + 1013904223u; + s = s * 1664525u + 1013904223u; + data[idx] = ({T})s; +}} +""" + +_LCG_SRC_FLT = r""" +extern "C" __global__ +void lcg_fill_{tag}({T} *data, unsigned long long n_elements, unsigned long long seed) +{{ + unsigned long long idx = (unsigned long long)blockIdx.x * (unsigned long long)blockDim.x + + (unsigned long long)threadIdx.x; + if (idx >= n_elements) return; + unsigned int s = (unsigned int)(seed ^ idx) ^ (unsigned int)(seed >> 32); + s = s * 1664525u + 1013904223u; + s = s * 1664525u + 1013904223u; + s = s * 1664525u + 1013904223u; + const {T} kInv = ({T})({kInv_lit}); + data[idx] = ({T})(-1) + ({T})s * kInv; +}} +""" + +_lcg_kernel_cache = {} + + +def _lcg_kernel_for(dtype): + import cupy as cp + + np_dtype = cp.dtype(dtype) + spec = next(s for s in _LCG_TYPE_SPECS if cp.dtype(s[1]) == np_dtype) + tag, _, T, is_float, kInv_lit = spec + if tag not in _lcg_kernel_cache: + tmpl = _LCG_SRC_FLT if is_float else _LCG_SRC_INT + src = tmpl.format(tag=tag, T=T, kInv_lit=kInv_lit or "") + _lcg_kernel_cache[tag] = cp.RawKernel(src, f"lcg_fill_{tag}") + return _lcg_kernel_cache[tag] + + +def _lcg_fill(arr, seed: int = _LCG_SEED) -> None: + """Fill `arr` (a cupy ndarray of any shape) in place with the bit-deterministic + LCG bytes. Output is byte-identical to BenchFillKernels' launchRandomFillTyped + with the same seed.""" + import numpy as np + + flat = arr.ravel() + kernel = _lcg_kernel_for(arr.dtype) + n = flat.size + threads = 256 + blocks = (n + threads - 1) // threads + kernel((blocks,), (threads,), (flat, np.uint64(n), np.uint64(seed))) + + +# --- Shared deterministic checkerboard fill -------------------------------- + +_CHECKERBOARD_TYPE_SPECS = ( + ("u8", "uint8", "unsigned char", "255"), + ("u16", "uint16", "unsigned short", "65535"), + ("u32", "uint32", "unsigned int", "4294967295u"), + ("i8", "int8", "signed char", "127"), + ("i16", "int16", "short", "32767"), + ("i32", "int32", "int", "2147483647"), + ("f32", "float32", "float", "1.0f"), + ("f64", "float64", "double", "1.0"), +) + +_CHECKERBOARD_SRC = r""" +extern "C" __global__ +void checkerboard_fill_{tag}({T} *data, + unsigned long long n_elements, + unsigned long long d0, + unsigned long long d1, + unsigned long long d2, + unsigned long long d3, + unsigned long long s0, + unsigned long long s1, + unsigned long long s2, + unsigned long long s3, + int rank) +{{ + unsigned long long idx = (unsigned long long)blockIdx.x * (unsigned long long)blockDim.x + + (unsigned long long)threadIdx.x; + if (idx >= n_elements) return; + + unsigned long long tmp = idx; + unsigned long long i0 = 0, i1 = 0, i2 = 0, i3 = 0; + unsigned int parity = 0u; + if (rank == 4) + {{ + i3 = tmp % d3; tmp /= d3; + i2 = tmp % d2; tmp /= d2; + i1 = tmp % d1; tmp /= d1; + i0 = tmp % d0; + }} + else if (rank == 3) + {{ + i2 = tmp % d2; tmp /= d2; + i1 = tmp % d1; tmp /= d1; + i0 = tmp % d0; + }} + else + {{ + i1 = tmp % d1; tmp /= d1; + i0 = tmp % d0; + }} + + parity = (unsigned int)((i0 + i1 + i2 + i3) & 1ull); + data[i0 * s0 + i1 * s1 + i2 * s2 + i3 * s3] + = parity ? ({T})({hi}) : ({T})0; +}} +""" + +_checkerboard_kernel_cache = {} + + +def _checkerboard_kernel_for(dtype): + import cupy as cp + + np_dtype = cp.dtype(dtype) + spec = next(s for s in _CHECKERBOARD_TYPE_SPECS if cp.dtype(s[1]) == np_dtype) + tag, _, T, hi = spec + if tag not in _checkerboard_kernel_cache: + src = _CHECKERBOARD_SRC.format(tag=tag, T=T, hi=hi) + _checkerboard_kernel_cache[tag] = cp.RawKernel(src, f"checkerboard_fill_{tag}") + return _checkerboard_kernel_cache[tag] + + +def _checkerboard_fill(arr) -> None: + """Fill `arr` in place with the per-coordinate checkerboard pattern used by + benchutils::CheckerboardValues, without materializing temporary grids.""" + import numpy as np + + shape = arr.shape + if len(shape) not in (2, 3, 4): + raise ValueError(f"checkerboard not supported for shape {shape}") + + dims = list(shape) + [1] * (4 - len(shape)) + strides = [stride // arr.itemsize for stride in arr.strides] + strides += [0] * (4 - len(strides)) + kernel = _checkerboard_kernel_for(arr.dtype) + n = arr.size + threads = 256 + blocks = (n + threads - 1) // threads + kernel( + (blocks,), + (threads,), + ( + arr, + np.uint64(n), + np.uint64(dims[0]), + np.uint64(dims[1]), + np.uint64(dims[2]), + np.uint64(dims[3]), + np.uint64(strides[0]), + np.uint64(strides[1]), + np.uint64(strides[2]), + np.uint64(strides[3]), + np.int32(len(shape)), + ), + ) + + +def _checkerboard_fill_image(arr) -> None: + """Fill an image with checkerboard parity based on pixel coordinates.""" + if len(arr.shape) == 3 and arr.shape[-1] > 1: + for channel in range(arr.shape[-1]): + _checkerboard_fill(arr[..., channel]) + else: + _checkerboard_fill(arr) + + +def create_tensor( + shape: Tuple[int, ...], + dtype, + device: int = 0, + layout: str = "NHWC", + fill_mode: Union[str, float, int, Tuple, list] = "random", +) -> cvcuda.Tensor: + """ + Create a tensor for benchmarking with specified fill pattern. + Equivalent to C++ FillTensor function. + + Uses CuPy for array creation. + + Args: + shape: Tensor shape tuple (e.g., (1, 1080, 1920, 1)) + dtype: cvcuda.Type or dtype string (e.g., cvcuda.Type.U8, "uint8", "float32") + device: Device ID + layout: Tensor layout ("NHWC", "NCHW", "NC", "N", etc.) + fill_mode: How to fill the tensor: + - "random": Random values (0-255 for uint8, 0-1 for float) + - numeric value: Fill with specific constant + - tuple/list: Broadcast to shape (e.g., [3, 5] for shape (N, 2) creates N rows of [3, 5]) + + Returns: + CV-CUDA tensor with data filled according to fill_mode + """ + import cupy as cp + + # Convert string to cvcuda.Type if needed + if isinstance(dtype, str): + cvcuda_dtype = get_dtype(dtype) + else: + cvcuda_dtype = dtype + + # Map cvcuda.Type to cupy dtype + dtype_map = { + cvcuda.Type.U8: cp.uint8, + cvcuda.Type.U16: cp.uint16, + cvcuda.Type.U32: cp.uint32, + cvcuda.Type.S8: cp.int8, + cvcuda.Type.S16: cp.int16, + cvcuda.Type.S32: cp.int32, + cvcuda.Type.F32: cp.float32, + cvcuda.Type.F64: cp.float64, + } + cupy_dtype = dtype_map.get(cvcuda_dtype, cp.float32) + + # Determine if this is an integer type + is_integer = cvcuda_dtype in [ + cvcuda.Type.U8, + cvcuda.Type.U16, + cvcuda.Type.U32, + cvcuda.Type.S8, + cvcuda.Type.S16, + cvcuda.Type.S32, + ] + + # Set CuPy device + with cp.cuda.Device(device): + # Fill tensor according to fill_mode + if fill_mode == "lcg": + # Deterministic varied fill that's byte-identical to the C++ side's + # launchRandomFillTyped (BenchFillKernels.cu). Use this in place of + # "random" whenever the C++ bench uses RandomValues(): it gives + # the same statistical properties (uniform over full type range for + # ints, [-1, +1] for floats) but produces matching bytes. + array = cp.empty(shape, dtype=cupy_dtype) + _lcg_fill(array, _LCG_SEED) + elif fill_mode == "random": + # Generate random data + if is_integer: + # Integer types: random in valid range + max_val = 256 if cvcuda_dtype == cvcuda.Type.U8 else 65536 + array = cp.random.randint(0, max_val, shape, dtype=cupy_dtype) + else: + # Float types: random in [0, 1] + array = cp.random.random(shape).astype(cupy_dtype) + elif fill_mode == "checkerboard": + # Per-element checkerboard: (sum-of-coords & 1) ? hi : lo. Matches the + # C++ helper benchutils::CheckerboardValues in CppBenchUtils.hpp, + # which uses (x + y + z + w) & 1 across all dims (incl. channel). + # + # Channel must participate in the alternation: data-dependent ops + # like RGB2HSV branch on min(R,G,B)==max(R,G,B). A per-pixel pattern + # (broadcasting across C) makes every pixel either pure-black or + # pure-white, hitting the kernel's uniform-RGB fast path and giving + # Python a ~10% spurious speedup vs the per-element C++ fill. + array = cp.empty(shape, dtype=cupy_dtype) + _checkerboard_fill(array) + elif fill_mode == "gradient_h": + # Match C++ benchmark image patterns for layout-parity runs. + if len(shape) != len(layout): + raise ValueError( + f"gradient_h shape/layout mismatch: shape={shape}, layout={layout}" + ) + try: + width_axis = layout.index("W") + except ValueError as exc: + raise ValueError( + f"gradient_h not supported for layout {layout}" + ) from exc + + W = shape[width_axis] + gradient = (255 - (cp.arange(W, dtype=cp.int32) % 256)).astype(cupy_dtype) + if not is_integer: + gradient = gradient / 255.0 + gradient_shape = [1] * len(shape) + gradient_shape[width_axis] = W + array = cp.broadcast_to( + gradient.reshape(tuple(gradient_shape)), shape + ).copy() + elif isinstance(fill_mode, dict) and "random_int" in fill_mode: + # Random integers in specified range (matching C++ RandomValues(low, high)) + low, high = fill_mode["random_int"] + array = cp.random.randint(low, high + 1, shape, dtype=cupy_dtype) + elif isinstance(fill_mode, (list, tuple)): + # Broadcast tuple/list to shape (e.g., [3, 5] repeated for each row) + # Convert to array and broadcast/tile to match shape + fill_array = cp.array(fill_mode, dtype=cupy_dtype) + # If shape is (N, len(fill_mode)), tile the fill_array N times + if len(shape) == 2 and shape[1] == len(fill_mode): + array = cp.tile(fill_array, (shape[0], 1)) + else: + # General broadcast + array = cp.broadcast_to(fill_array, shape).copy() + else: + # Fill with constant value + array = cp.full(shape, fill_mode, dtype=cupy_dtype) + + return cvcuda.as_tensor(array, layout) + + +def create_image_batch_varshape( + shape: Tuple[int, ...], + size_variation: int, + img_format, + dtype=None, + device: int = 0, + fill_mode: Union[str, float, int] = "random", + batch=None, +): + """ + Create an ImageBatchVarShape with specified fill pattern. + Equivalent to C++ FillImageBatch function. + + Args: + shape: Image batch shape (N, H, W) or (N, H, W, C) + - N: batch size + - H: base height for all images (or fixed height if size_variation=0) + - W: base width for all images (or fixed width if size_variation=0) + - C: number of channels (optional, defaults to 1) + size_variation: Size variation parameter: + - 0: All images same size (base_height x base_width) + - >0: Variable sizes around base, ±size_variation per image + img_format: cvcuda.Format for images (e.g., Format.U8, Format.RGB8) + dtype: cvcuda.Type or dtype string (e.g., cvcuda.Type.U8, "uint8"). If None, inferred from img_format. + device: Device ID + fill_mode: How to fill the images: + - "random": Random values (0-255 for uint8, 0-1 for float) + - numeric value: Fill with specific constant + batch: Optional preallocated cvcuda.ImageBatchVarShape. Use this when + multiple batches must allocate metadata before any image storage. + + Returns: + cvcuda.ImageBatchVarShape with images filled according to fill_mode + + Example: + >>> # Create batch with 32 images, 1080x1920 base, random RGB data + >>> batch = create_image_batch_varshape( + ... (32, 1080, 1920, 3), 0, cvcuda.Format.RGB8, + ... dtype=cvcuda.Type.U8, device=0, fill_mode="random" + ... ) + """ + import cupy as cp + + # Parse shape + if len(shape) == 3: + batch_size, base_height, base_width = shape + num_channels = 1 + elif len(shape) == 4: + batch_size, base_height, base_width, num_channels = shape + else: + raise ValueError(f"shape must be (N, H, W) or (N, H, W, C), got {shape}") + + # Convert string to cvcuda.Type if needed (matches create_tensor pattern) + if dtype is None: + # Check if format suggests uint8 or float + format_name = str(img_format) + if "F32" in format_name or "F64" in format_name: + cvcuda_dtype = cvcuda.Type.F32 + else: + cvcuda_dtype = cvcuda.Type.U8 + elif isinstance(dtype, str): + cvcuda_dtype = get_dtype(dtype) + else: + cvcuda_dtype = dtype + + # Map cvcuda.Type to cupy dtype + dtype_map = { + cvcuda.Type.U8: cp.uint8, + cvcuda.Type.U16: cp.uint16, + cvcuda.Type.U32: cp.uint32, + cvcuda.Type.S8: cp.int8, + cvcuda.Type.S16: cp.int16, + cvcuda.Type.S32: cp.int32, + cvcuda.Type.F32: cp.float32, + cvcuda.Type.F64: cp.float64, + } + cupy_dtype = dtype_map.get(cvcuda_dtype, cp.uint8) + + # Determine if this is an integer type + is_integer = cvcuda_dtype in [ + cvcuda.Type.U8, + cvcuda.Type.U16, + cvcuda.Type.U32, + cvcuda.Type.S8, + cvcuda.Type.S16, + cvcuda.Type.S32, + ] + + # Helper to calculate image size for each index (matches C++ FillImageBatch) + def get_image_size(i): + if size_variation == 0: + return base_height, base_width + else: + img_h = base_height + ( + i * size_variation + if i < batch_size // 2 + else -(i - batch_size // 2) * size_variation + ) + img_w = base_width + ( + i * size_variation + if i < batch_size // 2 + else -(i - batch_size // 2) * size_variation + ) + return max(1, img_h), max(1, img_w) + + if batch is None: + batch = cvcuda.ImageBatchVarShape(batch_size) + elif batch.capacity != batch_size: + raise ValueError( + f"batch capacity ({batch.capacity}) does not match shape ({batch_size})" + ) + elif len(batch) != 0: + raise ValueError(f"preallocated batch must be empty, got {len(batch)} images") + + # Set CuPy device (matches create_tensor pattern) + with cp.cuda.Device(device): + for i in range(batch_size): + img_h, img_w = get_image_size(i) + + def make_cupy_data(img_shape): + # Generate fill data according to fill_mode + if fill_mode == "lcg": + # Per-image deterministic LCG fill, byte-identical to the C++ + # FillImageBatch GPU fast path (BenchFillKernels.cu): seed is + # _LCG_SEED + i to match the per-image offset on the C++ side. + cupy_data = cp.empty(img_shape, dtype=cupy_dtype) + _lcg_fill(cupy_data, _LCG_SEED + i) + elif fill_mode == "random": + if is_integer: + max_val = 256 if cvcuda_dtype == cvcuda.Type.U8 else 65536 + cupy_data = cp.random.randint( + 0, max_val, img_shape, dtype=cupy_dtype + ) + else: + cupy_data = cp.random.random(img_shape).astype(cupy_dtype) + elif fill_mode == "gradient_h": + # Match C++ benchmark image patterns for per-image parity runs. + gradient = (255 - (cp.arange(img_w, dtype=cp.int32) % 256)).astype( + cupy_dtype + ) + if not is_integer: + gradient = gradient / 255.0 # normalize to [0, 1] for float + if len(img_shape) == 2: + cupy_data = cp.broadcast_to( + gradient.reshape(1, img_w), img_shape + ).copy() + else: + cupy_data = cp.broadcast_to( + gradient.reshape(1, img_w, 1), img_shape + ).copy() + elif isinstance(fill_mode, dict) and "random_int" in fill_mode: + # Random integers in specified range (matching C++ RandomValues(low, high)) + low, high = fill_mode["random_int"] + cupy_data = cp.random.randint( + low, high + 1, img_shape, dtype=cupy_dtype + ) + else: + # Fill with constant value + cupy_data = cp.full(img_shape, fill_mode, dtype=cupy_dtype) + + return cupy_data + + def fill_image_data(target): + if fill_mode == "checkerboard": + _checkerboard_fill_image(target) + elif isinstance(fill_mode, (int, float)): + target.fill(fill_mode) + else: + target[...] = make_cupy_data(target.shape) + + num_planes = img_format.planes + if len(shape) == 4 and num_planes > 1 and num_channels != num_planes: + raise ValueError( + f"shape channel count ({num_channels}) does not match " + f"{img_format} plane count ({num_planes})" + ) + if num_planes == 1: + # Allocate via NVCV so the row pitch is padded to NVCV's aligned + # stride (matching the C++ FillImageBatch images). Wrapping a dense + # cupy array instead leaves unaligned rows when W*channels is not + # pitch-aligned, which slows memory-bound kernels on the Python side + # only and produces a spurious parity gap. + img = cvcuda.Image((img_w, img_h), img_format) + view = cp.asarray(img.cuda()) + fill_image_data(view) + else: + # Keep the same per-plane deterministic data as the previous + # as_image([plane...]) path, but use NVCV allocation so planar + # images get the same row pitch and allocator behavior as C++. + img = cvcuda.Image((img_w, img_h), img_format) + view = cp.asarray(img.cuda()) + for plane in range(num_planes): + fill_image_data(view[plane, ...]) + + batch.pushback(img) + + return batch diff --git a/bench/python/requirements.bench.common.template b/bench/python/requirements.bench.common.template new file mode 100644 index 000000000..c753b456c --- /dev/null +++ b/bench/python/requirements.bench.common.template @@ -0,0 +1,43 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +# Template for requirements.bench.common.txt — to regenerate, run from the repo root: +# bash generate_requirements.sh + +# Common Python packages for CV-CUDA nvbench-based benchmarking +# Used by both CUDA 12 and CUDA 13 benchmarks + +# Python benchmark runner dependencies +pandas==${pandas} + +# pynvbench dependency for finding CUDA libraries +cuda-pathfinder==${cuda_pathfinder} + +# NumPy 2 with version constraints based on Python version +numpy==${numpy_v2_py310}; python_version=="3.10" +numpy==${numpy_v2_py311plus}; python_version>="3.11" + +# Benchmarking dependencies +pycuda==${pycuda} +nvtx==${nvtx}; python_version < "3.14" + +# NOTE: CUPTI libraries are provided by the system CUDA Toolkit installation +# (typically /usr/local/cuda/lib64/libcupti.so), not via pip packages. +# Ensure CUDA Toolkit is installed and LD_LIBRARY_PATH includes: +# export LD_LIBRARY_PATH=/usr/local/cuda/lib64:$LD_LIBRARY_PATH +# +# We do NOT install nvidia-cuda-cupti-cu12/cu13 packages to avoid version conflicts +# between CUDA 12 and CUDA 13, and to ensure we use the correct CUPTI version +# that matches the system's installed CUDA Toolkit. diff --git a/bench/python/requirements.bench.cu12.template b/bench/python/requirements.bench.cu12.template new file mode 100644 index 000000000..46df7081c --- /dev/null +++ b/bench/python/requirements.bench.cu12.template @@ -0,0 +1,32 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +# Template for requirements.bench.cu12.txt — to regenerate, run from the repo root: +# bash generate_requirements.sh + +# CUDA 12-specific Python packages for CV-CUDA benchmarking + +# Include common requirements +-r requirements.bench.common.txt + +# PyTorch for CUDA 12.x (${torch_cu12_index} wheel index, compatible with CUDA 12.5+) +--extra-index-url https://download.pytorch.org/whl/${torch_cu12_index} +torch==${torch_cu12} + +cupy-cuda12x==${cupy_cu12} + +# nvbench Python bindings (cuda.bench) are installed system-wide in Docker devel images + +# cvcuda-cu12 is installed separately (built from source or via install_bench_dependencies.sh) diff --git a/bench/python/requirements.bench.cu13.template b/bench/python/requirements.bench.cu13.template new file mode 100644 index 000000000..4343dbd4d --- /dev/null +++ b/bench/python/requirements.bench.cu13.template @@ -0,0 +1,32 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +# Template for requirements.bench.cu13.txt — to regenerate, run from the repo root: +# bash generate_requirements.sh + +# CUDA 13-specific Python packages for CV-CUDA benchmarking + +# Include common requirements +-r requirements.bench.common.txt + +# PyTorch for CUDA 13.x (${torch_cu13_index} wheel index, compatible with CUDA 13.3+) +--extra-index-url https://download.pytorch.org/whl/${torch_cu13_index} +torch==${torch_cu13} + +cupy-cuda13x==${cupy_cu13} + +# nvbench Python bindings (cuda.bench) are installed system-wide in Docker devel images + +# cvcuda-cu13 is installed separately (built from source or via install_bench_dependencies.sh) diff --git a/bench/python/requirements_common.txt b/bench/python/requirements_common.txt deleted file mode 100644 index 9cf8db77e..000000000 --- a/bench/python/requirements_common.txt +++ /dev/null @@ -1,31 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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. - -# Common Python packages for CV-CUDA benchmarking -# Used by both CUDA 12 and CUDA 13 benchmarks - -# NumPy 2 with version constraints based on Python version -numpy==2.0.2; python_version=="3.9" -numpy==2.2.6; python_version>="3.10" and python_version<"3.14" -numpy==2.3.3; python_version=="3.14" - -# Benchmarking dependencies -pycuda==2025.1.2 -nvtx==0.2.13 -pandas==2.3.3 -matplotlib==3.10.7 -av==16.0.0 -pillow==12.0.0 -PyNvVideoCodec==2.0.3; (platform_machine == "x86_64" or platform_machine == "amd64") and python_version>="3.10" and python_version<="3.13" diff --git a/bench/python/run_bench.py b/bench/python/run_bench.py deleted file mode 100644 index 0f2c68628..000000000 --- a/bench/python/run_bench.py +++ /dev/null @@ -1,234 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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: One must import PyCuda driver first, before CVCUDA or VPF otherwise -# things may throw unexpected errors. -import pycuda.driver as cuda # noqa: F401 -import os -import logging -import cvcuda -import torch -from pathlib import Path - -from perf_utils import ( # noqa: E402 - CvCudaPerf, - get_default_arg_parser, - parse_validate_default_args, -) - -from nvcodec_utils import ( # noqa: E402 - ImageBatchDecoder, -) - -from bench_utils import get_benchmark_eligible_ops_info # noqa: E402 - -current_dir = Path(os.path.abspath(__file__)).parents[0] - - -def run_bench( - input_path, - output_dir, - batch_size, - target_img_height, - target_img_width, - device_id, - num_iters, - should_visualize, - ops_filter_list, - cvcuda_perf, -): - """ - Runs the per operator benchmarks. It automatically discovers eligible operators for benchmarking, - sets them up, runs them and saves the runtime numbers. benchmark.py is needed to actually perform any - timing measurements. - """ - logger = logging.getLogger("run_bench") - logger.info("Benchmarking started.") - - # Set up various CUDA stuff. - cuda_device = cuda.Device(device_id) - cuda_ctx = cuda_device.retain_primary_context() - cuda_ctx.push() - # Use the the default stream for cvcuda and torch - # Since we never created a stream current will be the CUDA default stream - cvcuda_stream = cvcuda.Stream().current - torch_stream = torch.cuda.default_stream(device=cuda_device) - - # Create an image batch decoder to supply us the input test data. - decoder = ImageBatchDecoder( - input_path, - batch_size, - device_id, - cuda_ctx, - cvcuda_stream, - cvcuda_perf=cvcuda_perf, - ) - - # Get a list of (class names, class types) of all the ops that can be profiled. - ops_info_list = get_benchmark_eligible_ops_info() - logger.info("Found a total of %d operators for benchmarking." % len(ops_info_list)) - - if ops_filter_list: - # Filter based on user's criteria. - ops_info_list_filtered = [] - for op_class_name, op_class in ops_info_list: - for op_filter_name in ops_filter_list: - if op_class_name.startswith(op_filter_name): - ops_info_list_filtered.append((op_class_name, op_class)) - break - - ops_info_list = ops_info_list_filtered - logger.info( - "Filtered to a total of %d operators for benchmarking." % len(ops_info_list) - ) - - if should_visualize: - logger.warning( - "Visualization is turned ON. Run-times may increase drastically due to disk I/O." - ) - - # Do everything in streams. - with cvcuda_stream, torch.cuda.stream(torch_stream): - - # Start the decoder and get a batch. - # NOTE: Currently, we will grab the first and only batch out of the decoder for - # performance benchmarking. All ops will receive this and only this batch. - decoder.start() - batch = decoder() - batch.data = cvcuda.as_tensor(batch.data.cuda(), "NHWC") - # Read input and create a batch - - for op_class_name, op_class in ops_info_list: - logger.info("Running %s..." % op_class_name) - cvcuda_perf.push_range(op_class_name) - - # Step 1: Initialize the operator... - cvcuda_perf.push_range("init_op") - try: - op_instance = op_class( - device_id=device_id, - input=batch.data, - output_dir=output_dir, - should_visualize=should_visualize, - ) - torch.cuda.current_stream().synchronize() - cvcuda_perf.pop_range() # For init_op - except Exception as e: - logger.error( - "Unable to init the op %s due to error: %s" - % (op_class_name, str(e)) - ) - cvcuda_perf.pop_range(delete_range=True) # Deletes the init_op range - cvcuda_perf.pop_range( - delete_range=True - ) # Deletes the op_name range, too. - continue # Continue to the next operator. - - # Step 2: Run the operator. - # Repeat for as many iterations as we wanted. - cvcuda_perf.push_range("run_op") - for i in range(num_iters): - # Start the iteration. - cvcuda_perf.push_range("iter", batch_idx=i) - - # Run the op - success = op_instance(batch.data) - torch.cuda.current_stream().synchronize() - # Finish - cvcuda_perf.pop_range(total_items=batch_size, delete_range=not success) - - # Get out of the loop if our operator invocation fails. - if not success: - break - - cvcuda_perf.pop_range(delete_range=not success) # For the run_op - # reset the cache limit to not affect other operator benchmarks, in case a benchmark test - # changed it - if hasattr(cvcuda, "set_cache_limit_inbytes"): - total = torch.cuda.mem_get_info()[1] - cvcuda.set_cache_limit_inbytes(total // 2) - - # Step 3: log the parameters used by the operator, initialized during the setup call. - if success: - cvcuda_perf.push_range("op_params") - cvcuda_perf.push_range(str(op_instance.get_params_info())) - cvcuda_perf.pop_range() - cvcuda_perf.pop_range() - - cvcuda_perf.pop_range() # For the op_name - else: - cvcuda_perf.pop_range( - delete_range=True - ) # Deletes the op_name range, too, if run_op failed - - cuda_ctx.pop() - cvcuda_perf.finalize() - logger.info("Finished run_bench.") - - -def main(): - # docs_tag: begin_parse_args - parser = get_default_arg_parser( - "Profiler for all ops of CV-CUDA.", - input_path=os.path.join(current_dir, "assets", "brooklyn.jpg"), - supports_video=False, - batch_size=32, - ) - parser.add_argument( - "-n", - "--num_iters", - default=10, - type=int, - help="The number of iterations to run the benchmarks for.", - ) - parser.add_argument( - "--visualize", - action="store_true", - default=False, - help="Flag specifying whether outputs from the operators should be visualized" - " on written on disk or not.", - ) - parser.add_argument( - "ops", - nargs="*", - help="Optional list of one or more operator names which you want to benchmark. " - "When supplied, the benchmarking will be restricted to only the operators that starts " - "with these names.", - ) - args = parse_validate_default_args(parser) - - logging.basicConfig( - format="[%(name)s:%(lineno)d] %(asctime)s %(levelname)-6s %(message)s", - level=getattr(logging, args.log_level.upper()), - datefmt="%Y-%m-%d %H:%M:%S", - ) - - cvcuda_perf = CvCudaPerf("run_bench", default_args=args) - run_bench( - args.input_path, - args.output_dir, - args.batch_size, - args.target_img_height, - args.target_img_width, - args.device_id, - args.num_iters, - args.visualize, - args.ops, - cvcuda_perf, - ) - - -if __name__ == "__main__": - main() diff --git a/bench/run_bench.py b/bench/run_bench.py old mode 100644 new mode 100755 index b4aa0fd86..6631dd6c3 --- a/bench/run_bench.py +++ b/bench/run_bench.py @@ -1,4 +1,6 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +#!/usr/bin/env python3 + +# SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,305 +15,2467 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""Run selected CV-CUDA C++ and Python benchmarks and aggregate their results. + +Results are written as a combined CSV by default. JSON output can be consumed +by the baseline comparison and update commands. +""" + import os import sys import time +import argparse +import atexit +import json +import difflib import subprocess +import threading import pandas as pd -import argparse -import numpy as np +from pathlib import Path +from typing import List, Optional, Tuple +from abc import ABC, abstractmethod +from dataclasses import dataclass, field + +from _internal.warmup import WARMUP_CAP_ENV, parse_warmup_cap + +from _internal.axes import format_axis_name, format_axis_value +from _internal.baselines import ( + DEFAULT_CONFIG_DIR, + DEFAULT_SKU_MAP_PATH, + BaselineError, + baseline_payload_from_dataframe, + load_config_index, +) +from _internal.quality import ( + BenchmarkQualityCriteria, + DEFAULT_BENCHMARK_QUALITY, + exceeds_limit, +) + +# ============================================================================= +# GPU CLOCK CONTROL — lock SM clocks during the run + log live clocks +# ============================================================================= +# +# Why: across the K8s GPU pool we observed individual nodes whose SM clocks did +# not boost (entire bench wave 40–75% slower than the rest of the wave on the +# *same* SHA + same SKU label). Locking the SM clock to a fixed value before +# the bench starts eliminates frequency drift as a noise source and makes +# build-to-build comparisons deterministic. Locking requires CAP_SYS_ADMIN +# (or root) — we attempt it and fall back gracefully if denied. +# +# Independently, we sample the live SM clock + power + temperature throughout +# the run and write JSONL next to the bench output. Even when locking is +# denied, the log makes it obvious in retrospect why a build's kernel times +# drifted. +# +# Target clock: BENCH_LOCK_SM_CLOCK_MHZ accepts a comma-separated preference +# list (highest-first). We intersect it with the device's `clocks.gr.supported` +# list and pick the highest value present in both. Default 1095 is the +# documented base SM clock for both A100 PCIe 40GB and H100 PCIe; we fall back +# through {1005, 900, 750} for H100 PCIe silicon variants whose supported list +# does not include 1095 (the driver silently clamps `-lgc 1095,1095` to the +# closest supported value below, so we have to check ourselves). +_CLOCK_LOCK_PREFERRED_MHZ = [ + int(x) + for x in os.environ.get("BENCH_LOCK_SM_CLOCK_MHZ", "1095,1005,900,750").split(",") + if x.strip() +] +_CLOCK_SAMPLE_INTERVAL_S = float(os.environ.get("BENCH_CLOCK_SAMPLE_INTERVAL_S", "0.5")) +_CLOCK_LOG_PATH = os.environ.get("BENCH_CLOCK_LOG", "") # set after parse_args -BENCH_PREFIX = "cvcuda_bench_" -BENCH_OUTPUT = "out.csv" -BENCH_COMMAND = "{} {} --csv {}" -BENCH_COLNAME = "Benchmark" -BENCH_RESULTS = "bench_output.csv" -GPU_TIME_COLNAME = "GPU Time (sec)" -GPU_STATS_OUTPUT = "bench_gpu_stats.csv" -BENCH_COLUMNS = {"Benchmark", "BWUtil", "Skipped", GPU_TIME_COLNAME} -BANDWIDTH_COLNAME = "BWUtil" +_clock_lock_acquired = False +_clock_lock_applied_mhz: Optional[int] = None +_clock_sampler_stop = threading.Event() +_clock_sampler_thread: Optional[threading.Thread] = None -if __name__ == "__main__": - parser = argparse.ArgumentParser( - description="Run CV-CUDA benchmarks multiple times and aggregate results." - ) - parser.add_argument( - "-n", - "--num_runs", - type=int, - default=1, - help="Number of times to run each benchmark.", +def _nvidia_smi(*args: str, timeout: float = 5.0) -> Optional[str]: + """Run nvidia-smi with given args, return stdout or None on failure.""" + try: + r = subprocess.run( + ["nvidia-smi", *args], capture_output=True, text=True, timeout=timeout + ) + return r.stdout if r.returncode == 0 else None + except (FileNotFoundError, subprocess.TimeoutExpired): + return None + + +def _query_supported_sm_clocks_mhz() -> Optional[List[int]]: + """Parse `nvidia-smi -q -d SUPPORTED_CLOCKS` to extract the SM (graphics) + clock list the driver will actually accept for `-lgc`. + + Different H100 PCIe silicon variants ship with different supported lists; + `-lgc ,` silently clamps to the nearest value in the list and + reports success either way, so picking a target without consulting this + list can land at an unintended clock. Returns None on parse failure. + """ + out = _nvidia_smi("-q", "-d", "SUPPORTED_CLOCKS", "-i", "0", timeout=5.0) + if not out: + return None + clocks: List[int] = [] + in_graphics = False + for line in out.splitlines(): + s = line.strip() + if s.startswith("Graphics"): + in_graphics = True + try: + val = s.split(":", 1)[1].strip().split()[0] + clocks.append(int(val)) + except (IndexError, ValueError): + pass + elif s.startswith("Memory") or s.startswith("SM"): + in_graphics = False + elif in_graphics and s.endswith("MHz"): + try: + clocks.append(int(s.split()[0])) + except (IndexError, ValueError): + pass + return sorted(set(clocks)) if clocks else None + + +def _query_current_sm_clock_mhz() -> Optional[int]: + """Sample `clocks.current.sm` once. Returns None on failure.""" + out = _nvidia_smi( + "--query-gpu=clocks.current.sm", + "--format=csv,noheader,nounits", + "-i", + "0", + timeout=5.0, ) - parser.add_argument("bench_folder", help="Folder containing benchmark executables.") - parser.add_argument( - "bench_args", - nargs=argparse.REMAINDER, - help="Extra arguments to pass to the benchmark executables.", + if not out: + return None + try: + return int(out.strip().splitlines()[0].split()[0]) + except (IndexError, ValueError): + return None + + +def _pick_lock_target(preferred: List[int]) -> Optional[int]: + """Pick the highest value from `preferred` that's in the device's + supported-clocks list, so `-lgc` lands deterministically. Falls back + to `preferred[0]` if the supported list cannot be queried (e.g. + older driver), accepting the risk of silent clamping. + """ + supported = _query_supported_sm_clocks_mhz() + if supported is None: + return preferred[0] if preferred else None + supported_set = set(supported) + for target in preferred: + if target in supported_set: + return target + return None + + +def _try_lock_sm_clock(preferred: List[int]) -> Tuple[bool, Optional[int]]: + """Attempt to lock the SM clock to a value in `preferred` that the device + actually supports. + + Follows the CI GPU-clock-control recipe: + 1. -acp UNRESTRICTED (allow per-process clock changes) + 2. -pm ENABLED (persistence mode — keeps the driver loaded so + the lock survives idle gaps without snapping + back to default boost/throttle policy) + 3. -lgc , (the actual lock) + + Each step tries `sudo -n` first (CI runners with a sudoers rule) then + unprivileged (CI pods with SYS_ADMIN injected via the mutating + admission webhook). Prep-step failures are tolerated. + + Returns (locked, applied_mhz). When `locked` is True, `applied_mhz` is + the verified `clocks.current.sm` post-lock — which may differ from the + requested target if the driver clamped, so callers should trust this + over the requested value. When `locked` is False, `applied_mhz` is + the freerunning sampled clock (best-effort) for diagnostic logging. + """ + + def _run(args): + for prefix in (["sudo", "-n"], []): + try: + r = subprocess.run( + prefix + ["nvidia-smi"] + list(args), + capture_output=True, + text=True, + timeout=10, + ) + if r.returncode == 0: + return True, prefix + except (FileNotFoundError, subprocess.TimeoutExpired): + continue + return False, None + + # Prep — best effort, ignore failures + _run(["-acp", "UNRESTRICTED", "-i", "0"]) + _run(["-pm", "ENABLED", "-i", "0"]) + + target = _pick_lock_target(preferred) + if target is None: + print( + f"[clock-lock] WARN: no preferred clock {preferred} is in the " + f"device's supported list; skipping lock", + file=sys.stderr, + ) + return False, _query_current_sm_clock_mhz() + + ok, prefix = _run(["-lgc", f"{target},{target}", "-i", "0"]) + if ok: + applied = _query_current_sm_clock_mhz() + mode = "sudo" if prefix == ["sudo", "-n"] else "unprivileged" + if applied == target: + print( + f"[clock-lock] Locked SM clock to {target} MHz ({mode})", + file=sys.stderr, + ) + else: + print( + f"[clock-lock] Locked SM clock to {target} MHz ({mode}); " + f"driver clamped to {applied} MHz", + file=sys.stderr, + ) + return True, applied + + print( + f"[clock-lock] WARN: could not lock SM clock to {target} MHz " + f"(insufficient permissions); benchmarks may show frequency drift", + file=sys.stderr, ) - args = parser.parse_args() + return False, _query_current_sm_clock_mhz() - if args.num_runs <= 0: - print("E Number of runs must be positive.") - sys.exit(1) - bench_args_str = " ".join(args.bench_args) - bench_folder = args.bench_folder +def _try_reset_sm_clock() -> None: + """Restore default clock policy. Safe to call even if lock failed.""" + for prefix in (["sudo", "-n"], []): + try: + r = subprocess.run( + prefix + ["nvidia-smi", "-rgc"], + capture_output=True, + text=True, + timeout=10, + ) + if r.returncode == 0: + return + except (FileNotFoundError, subprocess.TimeoutExpired): + continue + +def _clock_sampler_loop(out_path: str, interval_s: float) -> None: + """Background thread: periodically sample SM clock / power / temp. + + Writes one JSON object per sample. Robust to nvidia-smi transient + failures (skips the sample on parse error). + """ + fields = [ + "wall_time_s", + "sm_clock_mhz", + "mem_clock_mhz", + "gpu_util_pct", + "mem_util_pct", + "power_w", + "power_limit_w", + "gpu_temp_c", + "throttle_reasons", + ] + query_cols = ( + "clocks.current.sm,clocks.current.memory,utilization.gpu,utilization.memory," + "power.draw,power.limit,temperature.gpu,clocks_throttle_reasons.active" + ) + + t0 = time.time() try: - bench_files_all = sorted(os.listdir(bench_folder)) - except FileNotFoundError: - print(f"E Benchmark folder not found: {bench_folder}") - sys.exit(1) + with open(out_path, "w") as f: + while not _clock_sampler_stop.is_set(): + out = _nvidia_smi( + f"--query-gpu={query_cols}", + "--format=csv,noheader,nounits", + "-i", + "0", + timeout=2.0, + ) + if out: + line = out.strip().splitlines()[0] if out.strip() else "" + parts = [p.strip() for p in line.split(",")] + if len(parts) >= 8: + row = dict(zip(fields, [round(time.time() - t0, 2), *parts])) + f.write(json.dumps(row) + "\n") + f.flush() + _clock_sampler_stop.wait(interval_s) + except Exception as e: + print(f"[clock-sampler] thread error: {e}", file=sys.stderr) - bench_files = [ - fn - for fn in bench_files_all - if BENCH_PREFIX in fn and os.path.isfile(os.path.join(bench_folder, fn)) + +def init_clock_control(out_dir: str) -> None: + """Lock SM clock + dump GPU fingerprint + start the live-clock sampler. + Idempotent on cleanup. + """ + global _clock_lock_acquired, _clock_lock_applied_mhz, _clock_sampler_thread + if _clock_lock_acquired or _clock_sampler_thread is not None: + return # already initialized + _clock_lock_acquired, _clock_lock_applied_mhz = _try_lock_sm_clock( + _CLOCK_LOCK_PREFERRED_MHZ + ) + _write_gpu_fingerprint(out_dir) + log_path = _CLOCK_LOG_PATH or os.path.join(out_dir, "clock_log.jsonl") + _clock_sampler_thread = threading.Thread( + target=_clock_sampler_loop, + args=(log_path, _CLOCK_SAMPLE_INTERVAL_S), + daemon=True, + ) + _clock_sampler_thread.start() + print( + f"[clock-sampler] sampling SM clock + power + temp every " + f"{_CLOCK_SAMPLE_INTERVAL_S:.2f}s -> {log_path}", + file=sys.stderr, + ) + atexit.register(shutdown_clock_control) + + +def shutdown_clock_control() -> None: + """Stop sampler thread and reset clock lock. Safe to call multiple times.""" + global _clock_sampler_thread, _clock_lock_acquired + if _clock_sampler_thread is not None: + _clock_sampler_stop.set() + _clock_sampler_thread.join(timeout=3.0) + _clock_sampler_thread = None + if _clock_lock_acquired: + _try_reset_sm_clock() + _clock_lock_acquired = False + + +try: + from tabulate import tabulate +except ImportError: + tabulate = None + + +# ============================================================================= +# SCHEMA DEFINITIONS +# ============================================================================= + +CONFIG_KEY_COLUMN = "config_key" + + +@dataclass +class ColumnDef: + """Definition of a single column in benchmark results.""" + + name: str + category: str # 'config', 'metric', 'derived', 'metadata' + + +# Single source of truth for all column definitions +SCHEMA = [ + # Configuration columns (used for grouping/matching) + ColumnDef("Benchmark", "config"), + ColumnDef("config_key", "config"), + ColumnDef("Language", "config"), + ColumnDef("InOutDataType", "config"), + ColumnDef("shape", "config"), + ColumnDef("outDataType", "config"), + ColumnDef("distanceType", "config"), + ColumnDef("inputKind", "config"), + ColumnDef("batch", "config"), + ColumnDef("antialias", "config"), + ColumnDef("numBoxes", "config"), + ColumnDef("kernelSize", "config"), + ColumnDef("diameter", "config"), + ColumnDef("cropMode", "config"), + ColumnDef("flagsMode", "config"), + ColumnDef("randomMode", "config"), + ColumnDef("maskMode", "config"), + ColumnDef("inpaintRadius", "config"), + ColumnDef("ksize", "config"), + ColumnDef("iteration", "config"), + ColumnDef("numErase", "config"), + ColumnDef("maxLocations", "config"), + ColumnDef("numOctaveLayers", "config"), + ColumnDef("matchesPerPoint", "config"), + ColumnDef("numElem", "config"), + ColumnDef("maxCapacity", "config"), + ColumnDef("blockSize", "config"), + # Metric columns (raw measurements) + ColumnDef("GPU Time (µs)", "metric"), + ColumnDef("GPU Noise (%)", "metric"), + ColumnDef("GPU Noise (µs)", "metric"), + ColumnDef("CPU Time (µs)", "metric"), + ColumnDef("CPU Noise (%)", "metric"), + ColumnDef("CPU Noise (µs)", "metric"), + ColumnDef("GlobalMem BW (bytes/sec)", "metric"), + ColumnDef("BWUtil", "metric"), + # Derived columns (computed from metrics) + ColumnDef("Py overhead (%)", "derived"), + ColumnDef("Py overhead (µs)", "derived"), + ColumnDef("Status", "derived"), + # Metadata columns (informational only) + ColumnDef("Device Name", "metadata"), + ColumnDef("Power Cap (W)", "metadata"), + ColumnDef("Locked SM Clock (MHz)", "metadata"), + ColumnDef("vBIOS Version", "metadata"), + ColumnDef("Samples", "metadata"), + ColumnDef("tier", "metadata"), +] + +# Columns that are NOT config columns (used for grouping/matching) +NON_CONFIG_COLS = frozenset(c.name for c in SCHEMA if c.category != "config") | { + "Device", + "Device Name", + "Skipped", + "Samples", + "Samples.1", + "Batch GPU (sec)", +} + +# Integer config columns that need normalization to Int64 +# (inputKind is a string axis -- Tensor/VarShape -- so it is intentionally absent here) +INT_COLUMNS = [ + "batch", + "antialias", + "numBoxes", + "kernelSize", + "diameter", + "ksize", + "iteration", + "numErase", + "maxLocations", + "numOctaveLayers", + "matchesPerPoint", + "numElem", + "maxCapacity", + "blockSize", +] + +# C++ nvbench type names → Python numpy names +DTYPE_MAP = { + "U8": "uint8", + "U16": "uint16", + "U32": "uint32", + "U64": "uint64", + "I8": "int8", + "I16": "int16", + "I32": "int32", + "I64": "int64", + "F32": "float32", + "F64": "float64", +} + +# Column display order (priority columns first) +PRIORITY_COLUMNS = [ + "Benchmark", + "tier", + "config_key", + "Language", + "GPU Time (µs)", + "GPU Noise (%)", + "GPU Noise (µs)", + "CPU Time (µs)", + "CPU Noise (%)", + "CPU Noise (µs)", + "Py overhead (%)", + "Py overhead (µs)", + "Status", + "GlobalMem BW (bytes/sec)", + "BWUtil", +] + +# Internal nvbench scratch columns we require. +REQUIRED_SCRATCH_COLS = {"Benchmark", "BWUtil", "Skipped", "GPU Time (sec)"} + + +def get_config_cols(df: pd.DataFrame) -> List[str]: + """Get configuration columns present in dataframe (excludes Language).""" + return [c for c in df.columns if c not in NON_CONFIG_COLS and c != "Language"] + + +_POWER_CAP_W_CACHE: Optional[int] = None +_POWER_CAP_W_QUERIED = False + + +def query_locked_sm_clock_mhz() -> Optional[int]: + """Return the SM clock the bench is effectively running at. + + When init_clock_control() has applied a lock, this is the verified + `clocks.current.sm` post-lock (the value the driver actually committed, + which may differ from the requested target on H100 silicon variants + whose supported-clocks list does not include the requested value). + + When no lock is in effect, this samples the live clock once. The JSON + artifact field derived from this is informational on locked runs and noisy + on unlocked runs; baselines should be reseeded only from locked runs. + """ + if _clock_lock_applied_mhz is not None: + return _clock_lock_applied_mhz + return _query_current_sm_clock_mhz() + + +_VBIOS_CACHE: Optional[str] = None +_VBIOS_QUERIED = False + + +def query_vbios_version() -> Optional[str]: + """Query the GPU's vBIOS version via nvidia-smi, cached per process. + + Used as an additional baseline-routing dimension because the silicon-side + behaviour (atomic throughput, DRAM placement quirks, supported-clocks + list) varies across H100 PCIe revisions even when Device Name + Power Cap + + Locked SM Clock all match. We observed 12% spread on histogrameq + between two H100 PCIe 350W pools that locked identically to 1095 MHz. + The vBIOS string (e.g. "96.00.51.00.00") is durable per silicon revision. + """ + global _VBIOS_CACHE, _VBIOS_QUERIED + if _VBIOS_QUERIED: + return _VBIOS_CACHE + _VBIOS_QUERIED = True + out = _nvidia_smi( + "--query-gpu=vbios_version", + "--format=csv,noheader", + "-i", + "0", + timeout=5.0, + ) + if not out: + return None + val = out.strip().splitlines()[0].strip() if out.strip() else None + _VBIOS_CACHE = val or None + return _VBIOS_CACHE + + +def _write_gpu_fingerprint(out_dir: str) -> None: + """Dump a richer hardware fingerprint alongside clock_log.jsonl. + + Captures vBIOS, UUID, product name/brand, the full `clocks.gr.supported` + list, and the K8s `spec.nodeName` (per-build ephemeral name; the + stable host name lives in the node's `kubernetes.io/hostname` + label, which the pod's default ServiceAccount can't read — physical + host identification is done out of band). Every + signal we have for distinguishing silicon variants in a multi-host + K8s pool. Purely diagnostic; routing keys do not consume these + fields. + """ + path = os.path.join(out_dir, "gpu_fingerprint.txt") + queries = [ + ("vbios_version", "--query-gpu=vbios_version"), + ("uuid", "--query-gpu=uuid"), + ("name", "--query-gpu=name"), + ("driver_version", "--query-gpu=driver_version"), + ("memory.total", "--query-gpu=memory.total"), + ("compute_cap", "--query-gpu=compute_cap"), ] + try: + with open(path, "w") as f: + for label, q in queries: + out = _nvidia_smi(q, "--format=csv,noheader", "-i", "0", timeout=5.0) + f.write(f"{label}: {(out or '').strip()}\n") + supported = _query_supported_sm_clocks_mhz() or [] + f.write(f"clocks.gr.supported: {supported}\n") + # Read K8S_NODE_NAME (not NODE_NAME) — the CI agent reserves + # NODE_NAME and overwrites the pod-level Downward API var of + # that name with the agent's own name (= the pod name). + # K8S_NODE_NAME is the Downward API spec.nodeName value the + # K8s scheduler placed the pod on. That is the ephemeral + # agent name, not the underlying physical host; the stable + # host name is in the node's `kubernetes.io/hostname` label, + # readable only via the K8s API (RBAC denied here, so it is + # looked up out of band). + f.write(f"k8s_node_name: {os.environ.get('K8S_NODE_NAME', '')}\n") + except OSError as e: + print(f"[gpu-fingerprint] failed to write {path}: {e}", file=sys.stderr) - if len(bench_files) == 0: - print( - f"E No benchmark executables starting with '{BENCH_PREFIX}' found in {bench_folder}" + +def query_power_cap_w() -> Optional[int]: + """Query the GPU's max power cap (W) via nvidia-smi, cached per process. + + Used to key per-(SKU, power-cap) baselines: the same Device Name can + appear at different TDP configurations (e.g. H100 PCIe at 350W vs 310W), + and they perform meaningfully differently. + """ + global _POWER_CAP_W_CACHE, _POWER_CAP_W_QUERIED + if _POWER_CAP_W_QUERIED: + return _POWER_CAP_W_CACHE + _POWER_CAP_W_QUERIED = True + try: + out = subprocess.check_output( + [ + "nvidia-smi", + "--query-gpu=power.max_limit", + "--format=csv,noheader,nounits", + ], + text=True, + stderr=subprocess.DEVNULL, + timeout=10, ) - sys.exit(1) + first_line = out.strip().splitlines()[0] + _POWER_CAP_W_CACHE = int(round(float(first_line))) + except (subprocess.SubprocessError, ValueError, IndexError, FileNotFoundError): + _POWER_CAP_W_CACHE = None + return _POWER_CAP_W_CACHE - print(f"I Found {len(bench_files)} benchmark executable(s) in {bench_folder}") - print(f"I Will run each benchmark {args.num_runs} time(s)") - if bench_args_str: - print(f"I Passing extra arguments to benchmarks: '{bench_args_str}'") - - l_df_all_runs = [] - all_gpu_times = {} - - for run_num in range(1, args.num_runs + 1): - print(f"\n--- Starting Run {run_num}/{args.num_runs} ---") - run_results_found = False - for filename in bench_files: - filepath = os.path.join(bench_folder, filename) - if not os.access(filepath, os.X_OK): - print(f"W Skipping non-executable file: {filename}") - continue - cmd = BENCH_COMMAND.format(filepath, bench_args_str, BENCH_OUTPUT) +# ============================================================================= +# DISPLAY UTILITIES +# ============================================================================= - print(f'I Running "{cmd}"', end=" ") - sys.stdout.flush() - beg = time.time() - try: - process = subprocess.run( - cmd.split(), - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - check=False, +class Colors: + """ANSI color codes for terminal output.""" + + RESET = "\033[0m" + GREEN = "\033[92m" + RED = "\033[91m" + YELLOW = "\033[93m" + CYAN = "\033[96m" + GRAY = "\033[90m" + BOLD = "\033[1m" + + @staticmethod + def disable(): + """Disable all colors.""" + Colors.RESET = "" + Colors.GREEN = "" + Colors.RED = "" + Colors.YELLOW = "" + Colors.CYAN = "" + Colors.GRAY = "" + Colors.BOLD = "" + + +# Symbols +SUCCESS = "✓" +ERROR = "✗" +WARNING = "⚠" +INFO = "→" + +# Global output control +VERBOSE = False +QUIET = False + + +def log_info(msg: str): + if not QUIET: + print(f"{Colors.GRAY}{INFO}{Colors.RESET} {msg}") + + +def log_success(msg: str): + if not QUIET: + print(f"{Colors.GREEN}{SUCCESS}{Colors.RESET} {msg}") + + +def log_warning(msg: str): + print(f"{Colors.YELLOW}{WARNING}{Colors.RESET} {msg}") + + +def log_error(msg: str): + print(f"{Colors.RED}{ERROR}{Colors.RESET} {msg}") + + +def print_table(rows: List[List[str]], headers: List[str]): + """Print table using tabulate or fallback to simple formatting.""" + if tabulate: + print(tabulate(rows, headers=headers, tablefmt="pipe")) + else: + print("| " + " | ".join(headers) + " |") + print("|" + "|".join(["-" * (len(h) + 2) for h in headers]) + "|") + for row in rows: + print("| " + " | ".join(str(v) for v in row) + " |") + + +# ============================================================================= +# VALIDATION +# ============================================================================= + + +@dataclass +class ValidationError: + """A single validation error.""" + + error_type: str # 'high_noise', 'missing_cpp', 'missing_python', 'perf_diff' + config: str + details: dict = field(default_factory=dict) + + +@dataclass +class ValidationResult: + """Result of all validation checks.""" + + noise_errors: List[ValidationError] = field(default_factory=list) + config_errors: List[ValidationError] = field(default_factory=list) + perf_errors: List[ValidationError] = field(default_factory=list) + + @property + def has_errors(self) -> bool: + return bool(self.noise_errors or self.config_errors or self.perf_errors) + + @property + def total_errors(self) -> int: + return len(self.noise_errors) + len(self.config_errors) + len(self.perf_errors) + + +# ============================================================================= +# RESULTS PROCESSOR +# ============================================================================= + + +class ResultsProcessor: + """Processes benchmark results through a clean transform pipeline.""" + + def __init__( + self, max_noise_pct: float, max_perf_diff_pct: float, max_perf_diff_us: float + ): + self.quality = BenchmarkQualityCriteria( + max_noise_pct=max_noise_pct, + max_perf_diff_pct=max_perf_diff_pct, + max_perf_diff_us=max_perf_diff_us, + ) + self.max_noise_pct = self.quality.max_noise_pct + self.max_perf_diff_pct = self.quality.max_perf_diff_pct + # Absolute Py-overhead floor. The relative gate alone misses anomalies + # on long kernels; those are typically real path divergences (different + # kernel selection, different launch params, or bench harness mismatch). + self.max_perf_diff_us = self.quality.max_perf_diff_us + + def process( + self, dfs: List[pd.DataFrame], compute_parity: bool = True + ) -> pd.DataFrame: + """Run full transform pipeline on list of result dataframes.""" + if not dfs: + return pd.DataFrame() + + df = pd.concat(dfs, ignore_index=True) + df = self._normalize_dtypes(df) + df = self._normalize_int_columns(df) + df = self._compute_noise_absolute(df) + df = self._compute_status_and_parity(df, compute_parity) + df = self._sort_results(df) + df = self._reorder_columns(df) + return df + + def _normalize_dtypes(self, df: pd.DataFrame) -> pd.DataFrame: + """Normalize C++ dtype names to Python names.""" + if "InOutDataType" in df.columns: + df["InOutDataType"] = df["InOutDataType"].replace(DTYPE_MAP) + return df + + def _normalize_int_columns(self, df: pd.DataFrame) -> pd.DataFrame: + """Convert integer columns to nullable Int64 to avoid float/int mismatches.""" + for col in INT_COLUMNS: + if col in df.columns: + df[col] = pd.to_numeric(df[col], errors="coerce").astype("Int64") + return df + + def _compute_noise_absolute(self, df: pd.DataFrame) -> pd.DataFrame: + """Compute absolute noise values in microseconds.""" + if "GPU Noise (%)" in df.columns and "GPU Time (µs)" in df.columns: + df["GPU Noise (µs)"] = ( + df["GPU Time (µs)"] * df["GPU Noise (%)"] / 100 + ).round(2) + if "CPU Noise (%)" in df.columns and "CPU Time (µs)" in df.columns: + df["CPU Noise (µs)"] = ( + df["CPU Time (µs)"] * df["CPU Noise (%)"] / 100 + ).round(2) + return df + + def _compute_status_and_parity( + self, df: pd.DataFrame, compute_parity: bool + ) -> pd.DataFrame: + """Compute status for all rows, and parity metrics for paired configs. + + For unpaired rows (or when compute_parity=False): status based on noise only. + For paired rows: status includes noise and parity checks. + """ + # Initialize status and parity columns + df["Status"] = "PASS" + df["Py overhead (%)"] = pd.NA + df["Py overhead (µs)"] = pd.NA + + # Track which rows have been processed (for paired configs) + processed_indices = set() + + if compute_parity: + config_cols = get_config_cols(df) + if config_cols: + for _, group in df.groupby(config_cols, dropna=False): + cpp_rows = group[group["Language"] == "cpp"] + py_rows = group[group["Language"] == "python"] + + if len(cpp_rows) > 0 and len(py_rows) > 0: + cpp_idx, py_idx = cpp_rows.index[0], py_rows.index[0] + cpp_time = df.at[cpp_idx, "GPU Time (µs)"] + py_time = df.at[py_idx, "GPU Time (µs)"] + cpp_noise = df.at[cpp_idx, "GPU Noise (%)"] + py_noise = df.at[py_idx, "GPU Noise (%)"] + + # Check for missing/invalid timing data + cpp_time_valid = pd.notna(cpp_time) and cpp_time > 0 + py_time_valid = pd.notna(py_time) and py_time > 0 + + if not cpp_time_valid or not py_time_valid: + # Mark as FAIL if timing data is missing + fail_msg = "FAIL (missing timing data:" + if not cpp_time_valid: + fail_msg += " C++" + if not py_time_valid: + fail_msg += " Python" + fail_msg += ")" + df.at[cpp_idx, "Status"] = fail_msg + df.at[py_idx, "Status"] = fail_msg + processed_indices.update([cpp_idx, py_idx]) + elif cpp_time_valid and py_time_valid: + overhead_pct = (py_time / cpp_time - 1.0) * 100 + overhead_us = py_time - cpp_time + + # Set overhead on both rows + for idx in [cpp_idx, py_idx]: + df.at[idx, "Py overhead (%)"] = overhead_pct + df.at[idx, "Py overhead (µs)"] = overhead_us + + # Compute combined status for paired rows + fail_reasons = [] + if self.quality.noise_exceeds_limit(cpp_noise): + fail_reasons.append(f"C++ noise {cpp_noise:.1f}%") + if self.quality.noise_exceeds_limit(py_noise): + fail_reasons.append(f"Py noise {py_noise:.1f}%") + if self.quality.relative_parity_exceeds_limit(overhead_pct): + fail_reasons.append(f"parity {overhead_pct:+.1f}%") + # Absolute parity floor — catches large path + # divergences on long kernels that the relative + # gate would miss. + if self.quality.absolute_parity_exceeds_limit(overhead_us): + fail_reasons.append(f"parity {overhead_us:+.0f}us") + + status = ( + f"FAIL ({', '.join(fail_reasons)})" + if fail_reasons + else "PASS" + ) + df.at[cpp_idx, "Status"] = status + df.at[py_idx, "Status"] = status + processed_indices.update([cpp_idx, py_idx]) + + # Set status for unpaired rows based on noise and missing data + for idx in df.index: + if idx not in processed_indices: + gpu_time = df.at[idx, "GPU Time (µs)"] + noise = df.at[idx, "GPU Noise (%)"] + lang = df.at[idx, "Language"] + + # Check for missing timing data in THIS row + if pd.isna(gpu_time) or gpu_time <= 0: + df.at[idx, "Status"] = f"FAIL (missing {lang} timing)" + # In parity mode, unpaired rows mean the counterpart is missing + elif compute_parity: + other = "C++" if lang == "python" else "Python" + df.at[idx, "Status"] = f"FAIL (missing {other})" + elif pd.notna(noise) and self.quality.noise_exceeds_limit(noise): + df.at[idx, "Status"] = f"FAIL (noise {noise:.1f}%)" + + return df + + def _sort_results(self, df: pd.DataFrame) -> pd.DataFrame: + """Sort to interleave C++ and Python entries for easy comparison.""" + config_cols = get_config_cols(df) + sort_cols = [c for c in config_cols + ["Language"] if c in df.columns] + return df.sort_values(by=sort_cols, ignore_index=True) if sort_cols else df + + def _reorder_columns(self, df: pd.DataFrame) -> pd.DataFrame: + """Reorder columns: priority first, then remaining.""" + all_cols = df.columns.tolist() + ordered = [c for c in PRIORITY_COLUMNS if c in all_cols] + ordered += [c for c in all_cols if c not in ordered] + return df[ordered] + + def finalize_for_output(self, df: pd.DataFrame) -> pd.DataFrame: + """Final cleanup for CSV output and display summaries.""" + # Drop Device index column if present + if "Device" in df.columns: + df = df.drop(columns=["Device"]) + + # Round numeric columns + round_2 = [ + "GPU Time (µs)", + "CPU Time (µs)", + "GPU Noise (%)", + "CPU Noise (%)", + "GPU Noise (µs)", + "CPU Noise (µs)", + "Py overhead (%)", + "Py overhead (µs)", + ] + for col in round_2: + if col in df.columns: + df[col] = pd.to_numeric(df[col], errors="coerce").round(2) + if "BWUtil" in df.columns: + df["BWUtil"] = pd.to_numeric(df["BWUtil"], errors="coerce").round(4) + + return df + + # ------------------------------------------------------------------------- + # Validation methods + # ------------------------------------------------------------------------- + + def validate(self, df: pd.DataFrame, check_parity: bool = True) -> ValidationResult: + """Run all validations and return structured result.""" + result = ValidationResult() + result.noise_errors = self._check_noise(df) + if check_parity: + result.config_errors = self._check_config_parity(df) + result.perf_errors = self._check_perf_parity(df) + return result + + def _build_config_str(self, row: pd.Series) -> str: + """Build human-readable config string from row.""" + parts = [row.get("Benchmark", "unknown")] + for col in ["InOutDataType", "shape", "inputKind"]: + if col in row and pd.notna(row[col]): + parts.append( + f"{format_axis_name(col)}={format_axis_value(col, row[col])}" ) - except FileNotFoundError: - print( - f"\nE Command not found for {filename}. Is it built and in the correct path?" + return ", ".join(parts) + + def _check_noise(self, df: pd.DataFrame) -> List[ValidationError]: + """Check for excessive noise/stddev in benchmark results.""" + errors = [] + for _, row in df.iterrows(): + noise = row.get("GPU Noise (%)", 0.0) + if self.quality.noise_exceeds_limit(noise): + lang = row.get("Language", "unknown") + config = f"{self._build_config_str(row)} ({lang})" + errors.append( + ValidationError( + "high_noise", + config, + {"stddev": noise, "threshold": self.max_noise_pct}, + ) ) - continue - end = time.time() + return errors - print(f"took {end - beg: .3f} sec") + def _check_config_parity(self, df: pd.DataFrame) -> List[ValidationError]: + """Check that each C++ config has a matching Python config.""" + errors = [] + config_cols = get_config_cols(df) + if not config_cols: + return errors - if process.returncode != 0: - print( - f"W Benchmark exited with error (code {process.returncode}): {filename}" - ) - print(f"W Stderr: \n{process.stderr}") - if os.path.exists(BENCH_OUTPUT): - os.remove(BENCH_OUTPUT) + for _, group in df.groupby(config_cols, dropna=False): + cpp_rows = group[group["Language"] == "cpp"] + py_rows = group[group["Language"] == "python"] + row = cpp_rows.iloc[0] if len(cpp_rows) > 0 else py_rows.iloc[0] + config = self._build_config_str(row) + + if len(cpp_rows) == 0: + errors.append(ValidationError("missing_cpp", config)) + elif len(py_rows) == 0: + errors.append(ValidationError("missing_python", config)) + return errors + + def _check_perf_parity(self, df: pd.DataFrame) -> List[ValidationError]: + """Check for performance differences between C++ and Python.""" + errors = [] + config_cols = get_config_cols(df) + if not config_cols: + return errors + + for _, group in df.groupby(config_cols, dropna=False): + cpp_rows = group[group["Language"] == "cpp"] + py_rows = group[group["Language"] == "python"] + if len(cpp_rows) == 0 or len(py_rows) == 0: continue - if not os.path.exists(BENCH_OUTPUT) or os.path.getsize(BENCH_OUTPUT) == 0: - print( - f"W Skipping as benchmark output '{BENCH_OUTPUT}' does not exist " - f"or is empty for {filename}" - ) + cpp_time = cpp_rows.iloc[0].get("GPU Time (µs)", 0) + py_time = py_rows.iloc[0].get("GPU Time (µs)", 0) + if cpp_time == 0: continue - try: - df = pd.read_csv(BENCH_OUTPUT) - except pd.errors.EmptyDataError: + ratio = py_time / cpp_time + diff_pct = abs(ratio - 1.0) * 100 + diff_us = py_time - cpp_time + if self.quality.parity_exceeds_limit(diff_pct, diff_us): + config = self._build_config_str(cpp_rows.iloc[0]) + errors.append( + ValidationError( + "perf_diff", + config, + { + "cpp_time": cpp_time, + "python_time": py_time, + "ratio": ratio, + "diff_pct": diff_pct, + "diff_us": diff_us, + "threshold_pct": self.max_perf_diff_pct, + "threshold_us": self.max_perf_diff_us, + }, + ) + ) + return errors + + def print_errors(self, result: ValidationResult): + """Print validation errors in a clear format.""" + print(f"{Colors.RED}{Colors.BOLD}{'=' * 60}{Colors.RESET}") + print(f"{Colors.RED}{Colors.BOLD} VALIDATION ERRORS{Colors.RESET}") + print(f"{Colors.RED}{Colors.BOLD}{'=' * 60}{Colors.RESET}\n") + + if result.config_errors: + print(f"{Colors.RED}{Colors.BOLD}Configuration Mismatches:{Colors.RESET}") + for err in result.config_errors: + if err.error_type == "missing_cpp": + print(f" - {err.config}: Missing C++ benchmark") + elif err.error_type == "missing_python": + print(f" - {err.config}: Missing Python benchmark") + print() + + if result.noise_errors: + threshold = result.noise_errors[0].details["threshold"] + print( + f"{Colors.RED}{Colors.BOLD}High Noise/StdDev (>{threshold}%):{Colors.RESET}" + ) + for err in result.noise_errors: + print(f" - {err.config}: {err.details['stddev']:.2f}%") + print() + + if result.perf_errors: + d0 = result.perf_errors[0].details + tpct = d0["threshold_pct"] + tus = d0["threshold_us"] + print( + f"{Colors.RED}{Colors.BOLD}Performance Differences " + f"(>{tpct:.1f}% relative or >{tus:.0f}us absolute):{Colors.RESET}" + ) + for err in result.perf_errors: + d = err.details + direction = "faster" if d["ratio"] < 1.0 else "slower" print( - f"W Skipping as benchmark output '{BENCH_OUTPUT}' is empty or invalid CSV for {filename}" + f" - {err.config}: Python {d['ratio']:.3f}x {direction} " + f"({d['diff_pct']:.1f}% / {d['diff_us']:+.0f}us)" ) - os.remove(BENCH_OUTPUT) + print() + + print(f"{Colors.RED}Total errors: {result.total_errors}{Colors.RESET}") + + +# ============================================================================= +# DISPLAY FORMATTERS +# ============================================================================= + + +def print_parity_summary_table(df: pd.DataFrame): + """Print a summary table comparing C++ and Python performance. + + Uses pre-computed columns from ResultsProcessor (Status, Py overhead, etc.). + Shows N/A for missing language data when running single-language mode. + """ + config_cols = get_config_cols(df) + if not config_cols: + return + + def fmt(val): + return "" if pd.isna(val) else str(val) + + def fmt_time(t): + return "N/A" if t is None or pd.isna(t) else f"{t:,.1f}" + + def fmt_noise(noise, time): + if noise is None or time is None or pd.isna(noise) or pd.isna(time): + return "N/A" + return f"±{noise:.2f}% (±{time * noise / 100:.0f}µs)" + + def fmt_overhead(pct, us): + if pct is None or pd.isna(pct): + return "N/A" + return f"{pct:+.1f}% ({us:+.0f}µs)" + + def fmt_bwutil(bw): + if bw is None or pd.isna(bw): + return "N/A" + return f"{bw:.1%}" + + table_rows = [] + for _, group in df.groupby(config_cols, dropna=False): + cpp_rows = group[group["Language"] == "cpp"] + py_rows = group[group["Language"] == "python"] + if len(cpp_rows) == 0 and len(py_rows) == 0: + continue + + ref_row = cpp_rows.iloc[0] if len(cpp_rows) > 0 else py_rows.iloc[0] + cpp_time = cpp_rows.iloc[0].get("GPU Time (µs)") if len(cpp_rows) > 0 else None + py_time = py_rows.iloc[0].get("GPU Time (µs)") if len(py_rows) > 0 else None + cpp_noise = cpp_rows.iloc[0].get("GPU Noise (%)") if len(cpp_rows) > 0 else None + py_noise = py_rows.iloc[0].get("GPU Noise (%)") if len(py_rows) > 0 else None + cpp_bwutil = cpp_rows.iloc[0].get("BWUtil") if len(cpp_rows) > 0 else None + py_bwutil = py_rows.iloc[0].get("BWUtil") if len(py_rows) > 0 else None + + # Use pre-computed values from ResultsProcessor + overhead_pct = ref_row.get("Py overhead (%)") + overhead_us = ref_row.get("Py overhead (µs)") + status = ref_row.get("Status", "PASS") + is_pass = status == "PASS" + + table_rows.append( + [format_axis_value(c, ref_row.get(c)) for c in config_cols] + + [ + fmt_time(cpp_time), + fmt_time(py_time), + fmt_overhead(overhead_pct, 0 if pd.isna(overhead_us) else overhead_us), + fmt_noise(cpp_noise, cpp_time), + fmt_noise(py_noise, py_time), + fmt_bwutil(cpp_bwutil), + fmt_bwutil(py_bwutil), + ( + f"{Colors.GREEN}PASS{Colors.RESET}" + if is_pass + else f"{Colors.RED}FAIL{Colors.RESET}" + ), + ] + ) + + if table_rows: + headers = [format_axis_name(c) for c in config_cols] + [ + "C++ (µs)", + "Py (µs)", + "Py overhead", + "C++ Noise", + "Py Noise", + "C++ BWUtil", + "Py BWUtil", + "Status", + ] + print_table(table_rows, headers) + print() + + +# ============================================================================= +# BENCHMARK RUNNERS +# ============================================================================= + + +def get_output_filename(benchmark_name: str, language: str) -> str: + """Generate a unique internal nvbench scratch filename.""" + clean_name = benchmark_name.replace(".py", "").replace("bench_", "") + return f"out_{clean_name}_{language}.nvbench" + + +@dataclass +class BenchmarkSummary: + """Track benchmark execution statistics.""" + + language: str + total: int = 0 + successful: int = 0 + failed: int = 0 + + +class BenchmarkRunner(ABC): + """Base class for benchmark runners.""" + + def __init__( + self, + bench_prefix: str, + bench_folder: str, + operators: Optional[List[str]], + language: str, + keep_outputs: bool = False, + max_noise_pct: float = DEFAULT_BENCHMARK_QUALITY.max_noise_pct, + config_file: Optional[str] = None, + tiers: Optional[set] = None, + config_keys: Optional[List[str]] = None, + warmup_cap: Optional[int] = None, + ): + self.bench_prefix = bench_prefix + self.bench_folder = bench_folder + self.operators = operators or [] + self.language = language + self.keep_outputs = keep_outputs + self.max_noise_pct = max_noise_pct + self.config_file = config_file + self.tiers = tiers # None = no tier filter + self.config_keys = config_keys + self.warmup_cap = warmup_cap + self.operator_manifest = {} + self.results: List[pd.DataFrame] = [] + self.output_files: List[str] = [] + self.summary = BenchmarkSummary(language) + + def _load_config(self, need_axis_args: bool = False): + """Load benchmark configuration from JSON file. + + Args: + need_axis_args: If True, also load generate_axis_args (C++ only). + """ + try: + sys.path.insert(0, str(Path(__file__).parent / "config")) + from load_config import ( + load_bench_config, + load_bench_manifest, + get_operator_from_benchmark_name, + get_configs_for_benchmark, + ) + + self.get_operator_from_benchmark_name = get_operator_from_benchmark_name + self.get_configs_for_benchmark = get_configs_for_benchmark + self.operator_manifest = load_bench_manifest(self.config_file) + + if need_axis_args: + from load_config import generate_axis_args + + self.generate_axis_args = generate_axis_args + cfg_path = self.config_file or "bench/config/bench_params.json" + log_info(f"Loaded benchmark configuration from {cfg_path}") + + return load_bench_config(self.config_file) + except Exception as e: + log_error(f"Failed to load benchmark config: {e}") + sys.exit(2) + + def discover_benchmarks(self) -> List[tuple]: + """Map configured operators to config keys and benchmark files.""" + if self.config_keys is not None: + return self._discover_selected_config_keys() + + selected_operators = self.operators or list(self.operator_manifest) + pairs = [] + missing_tier = [] + for operator in selected_operators: + keys = self.get_configs_for_benchmark( + operator, self.config, tiers=self.tiers + ) + if keys: + path = self._benchmark_path_for_operator(operator) + pairs.extend((k, path) for k in keys) + elif self.operators: + missing_tier.append(operator) + + if missing_tier: + tiers = ",".join(sorted(self.tiers)) if self.tiers else "all" + log_error( + f"No config entries for operator(s) {missing_tier} in tier(s) {tiers}" + ) + sys.exit(1) + + return pairs + + def _discover_selected_config_keys(self) -> List[tuple]: + """Map exact config keys to benchmark files for this language.""" + pairs = [] + errors = [] + for config_key in self.config_keys: + entry = self.config.get(config_key) + if not isinstance(entry, dict): + errors.append(f"Unknown config key: {config_key}") continue - except Exception as e: - print(f"W Error reading CSV '{BENCH_OUTPUT}' for {filename}: {e}") - os.remove(BENCH_OUTPUT) + benchmark = entry.get("benchmark") + if not benchmark: + errors.append( + f"Config key {config_key!r} is missing required 'benchmark' field" + ) continue - if not BENCH_COLUMNS.issubset(df.columns): - missing_cols = BENCH_COLUMNS - set(df.columns) - print( - f"W Skipping {filename} output: Missing required columns: " - f"{missing_cols}. Found: {list(df.columns)}" + try: + path = self._benchmark_path_for_operator(benchmark) + except KeyError: + errors.append( + f"Config key {config_key!r} targets benchmark {benchmark!r}, " + "which is not listed in bench_params.json" ) - os.remove(BENCH_OUTPUT) continue + pairs.append((config_key, path)) - df_filtered = df[df["Skipped"] == "No"].copy() - - os.remove(BENCH_OUTPUT) - - if len(df_filtered) > 0: - run_results_found = True - df_filtered["run"] = run_num - l_df_all_runs.append(df_filtered) - - # Collect GPU times for statistics - # Switch to iterrows() for reliable column name access - for index, row in df_filtered.iterrows(): - bench_name = getattr(row, BENCH_COLNAME) - try: - # Access directly using the original column name string - gpu_time = float(row[GPU_TIME_COLNAME]) - all_gpu_times.setdefault(bench_name, []).append(gpu_time) - except (KeyError, ValueError, TypeError) as e: - print( - f"W Could not parse GPU time for '{bench_name}' in run " - f"{run_num}. Value: '{row.get(GPU_TIME_COLNAME, 'N/A')}'. Error: {e}" - ) + if errors: + for error in errors: + log_error(error) + sys.exit(2) - else: + return pairs + + def _benchmark_path_for_operator(self, operator: str) -> str: + """Return the configured benchmark file path for this runner language.""" + spec = self.operator_manifest.get(operator) + if spec is None: + raise KeyError(operator) + + benchmark_file = spec.get(self.language) + if not benchmark_file: + log_error( + f"Operator {operator!r} has no {self.language} benchmark in " + "bench_params.json" + ) + sys.exit(1) + + benchmark_path = os.path.join(self.bench_folder, benchmark_file) + if not os.path.isfile(benchmark_path): + log_error( + f"Configured {self.language} benchmark for operator {operator!r} " + f"does not exist: {benchmark_path}" + ) + sys.exit(1) + + return benchmark_path + + @abstractmethod + def build_command( + self, + benchmark_path: str, + extra_args: List[str], + output_file: str, + config_key: Optional[str] = None, + ) -> List[str]: + """Build command to execute benchmark.""" + pass + + def benchmark_env(self) -> Optional[dict]: + """Environment for benchmark subprocesses.""" + env = os.environ.copy() + if self.warmup_cap is None: + env.pop(WARMUP_CAP_ENV, None) + else: + env[WARMUP_CAP_ENV] = str(self.warmup_cap) + return env + + def run_benchmark( + self, + benchmark_path: str, + extra_args: List[str], + config_key: Optional[str] = None, + ) -> tuple: + """Run a single benchmark and return (output_filename, all_pass).""" + benchmark_name = config_key if config_key else os.path.basename(benchmark_path) + output_file = get_output_filename(benchmark_name, self.language) + output_path = os.path.join(self.bench_folder, output_file) + cmd_list = self.build_command( + benchmark_path, extra_args, output_path, config_key=config_key + ) + if VERBOSE: + log_info(f'Running: "{" ".join(cmd_list)}"') + + beg = time.time() + + try: + result = subprocess.run( + cmd_list, + shell=False, + check=False, + capture_output=True, + text=True, + timeout=300, + env=self.benchmark_env(), + ) + elapsed = time.time() - beg + + if result.returncode != 0: + if not QUIET: + print( + f"{Colors.RED}{ERROR}{Colors.RESET} " + f"(exit code {result.returncode}, {elapsed:.2f}s)" + ) + if result.stderr: + print(result.stderr) + if os.path.exists(output_path): + os.remove(output_path) + return None, False + + # Check noise from the internal nvbench scratch output. + all_pass = self._check_noise_from_scratch(output_path) + + return output_file, all_pass + + except subprocess.TimeoutExpired: + if not QUIET: + print(f"{Colors.RED}TIMEOUT{Colors.RESET} ({time.time() - beg:.2f}s)") + return None, False + except Exception as e: + if not QUIET: print( - f"W No valid (non-skipped) results found in output for {filename} in run {run_num}" + f"{Colors.RED}ERROR{Colors.RESET}: {e} ({time.time() - beg:.2f}s)" ) + return None, False - if not run_results_found: - print( - f"W No benchmark results were successfully processed in run {run_num}." + def _check_noise_from_scratch(self, scratch_path: str) -> bool: + """Check if all noise values in internal nvbench output are within threshold.""" + try: + if not os.path.exists(scratch_path): + return False # Missing scratch output is a failure + df = pd.read_csv(scratch_path) + if len(df) == 0: + return False # Empty scratch output is a failure + # Filter out skipped rows + df = df[df.get("Skipped", "No") == "No"] + if len(df) == 0: + return False # All configs skipped is a failure + if "Noise.1" in df.columns: # GPU noise in nvbench scratch output. + return not any( + exceeds_limit(float(noise) * 100.0, self.max_noise_pct) + for noise in df["Noise.1"].dropna() + ) + return True + except Exception: + return False + + def collect_results(self, output_file: str, config_key: str) -> bool: + """Collect results from an internal nvbench scratch output file.""" + filepath = os.path.join(self.bench_folder, output_file) + + if not os.path.exists(filepath) or os.path.getsize(filepath) == 0: + VERBOSE and log_warning( + f"Skipping '{output_file}': does not exist or is empty" + ) + return False + + try: + df = pd.read_csv(filepath) + except Exception as e: + VERBOSE and log_warning(f"Error reading '{output_file}': {e}") + return False + + entry = self.config.get(config_key, {}) if self.config else {} + benchmark_name = entry.get( + "benchmark", self.get_operator_from_benchmark_name(config_key) + ) + if "Benchmark" not in df.columns: + df.insert(0, "Benchmark", benchmark_name) + else: + df["Benchmark"] = benchmark_name + + if not REQUIRED_SCRATCH_COLS.issubset(df.columns): + missing = REQUIRED_SCRATCH_COLS - set(df.columns) + VERBOSE and log_warning(f"Skipping {config_key}: Missing columns {missing}") + return False + + df_filtered = df[df["Skipped"] == "No"].copy() + if len(df_filtered) == 0: + VERBOSE and log_warning(f"No valid results found in {config_key}") + return False + + df_filtered.insert(0, "Language", self.language) + df_filtered.insert(0, CONFIG_KEY_COLUMN, config_key) + # Stamp the config entry's tier so combined-tier outputs (and any + # downstream filtering / per-tier baseline compare) can disambiguate + # rows by tier. + df_filtered["tier"] = entry.get("tier") + df_filtered = self._transform_scratch_columns(df_filtered) + + has_noise_failures = False + if "GPU Noise (%)" in df_filtered.columns: + has_noise_failures = any( + exceeds_limit(float(noise), self.max_noise_pct) + for noise in df_filtered["GPU Noise (%)"].dropna() ) - print("\n--- Aggregating Results ---") + self.results.append(df_filtered) + self.output_files.append(filepath) + return not has_noise_failures - if l_df_all_runs: - df_all = pd.concat(l_df_all_runs, axis=0, ignore_index=True) + def _transform_scratch_columns(self, df: pd.DataFrame) -> pd.DataFrame: + """Transform nvbench scratch columns to standardized format.""" + # Time: seconds → microseconds + for col in ["GPU Time (sec)", "CPU Time (sec)"]: + if col in df.columns: + new_col = col.replace("(sec)", "(µs)") + df[new_col] = df[col] * 1_000_000 + df.drop(columns=[col], inplace=True) - filepath_orig = os.path.join(args.bench_folder, BENCH_RESULTS) + # Noise: fractions → percentages + rename_map = {"Noise": "CPU Noise (%)", "Noise.1": "GPU Noise (%)"} + for old_col, new_col in rename_map.items(): + if old_col in df.columns: + df[new_col] = df[old_col] * 100 + df.drop(columns=[old_col], inplace=True) + + # Stamp the GPU power cap (W) so baselines can split per configuration. + # nvbench's Device Name alone is ambiguous: e.g. H100 PCIe boxes ship at + # both 350W and 310W TDP and perform ~26% apart on identical kernels. + df["Power Cap (W)"] = query_power_cap_w() + + # Stamp the actually-applied SM clock. H100 PCIe pools contain silicon + # variants whose supported-clocks lists differ; even with the same + # `-lgc` request, half the pool can land at a different clock and + # produce a bimodal baseline. Recording the committed value lets + # baselines route per (SKU, Power Cap, Locked SM Clock). + df["Locked SM Clock (MHz)"] = query_locked_sm_clock_mhz() + + # Stamp the vBIOS version. Even within a single (SKU, Power Cap, + # Locked SM Clock) bucket, H100 PCIe silicon revisions perform + # measurably differently on bandwidth/atomics-bound ops (observed + # 12% spread on histogrameq between two pools both locked to + # 1095 MHz). Routing by vBIOS isolates each revision into its own + # baseline. + df["vBIOS Version"] = query_vbios_version() + + # Note: dtype normalization is done in ResultsProcessor._normalize_dtypes() + return df + + +class CppBenchmarkRunner(BenchmarkRunner): + """Runner for C++ benchmarks.""" + + def __init__( + self, + bench_folder: str, + operators: Optional[List[str]], + keep_outputs: bool = False, + max_noise_pct: float = DEFAULT_BENCHMARK_QUALITY.max_noise_pct, + config_file: Optional[str] = None, + tiers: Optional[set] = None, + config_keys: Optional[List[str]] = None, + warmup_cap: Optional[int] = None, + ): + super().__init__( + "bench_", + bench_folder, + operators, + "cpp", + keep_outputs, + max_noise_pct, + config_file=config_file, + tiers=tiers, + config_keys=config_keys, + warmup_cap=warmup_cap, + ) + self.config = self._load_config(need_axis_args=True) + + def build_command( + self, + benchmark_path: str, + extra_args: List[str], + output_file: str, + config_key: Optional[str] = None, + ) -> List[str]: + """Build command for C++ benchmark with --axis arguments from config.""" + if config_key is None: + config_key = self.get_operator_from_benchmark_name( + os.path.basename(benchmark_path) + ) + axis_args = self.generate_axis_args(config_key, self.config) + return [benchmark_path] + axis_args + extra_args + ["--csv", output_file] + + +class PythonBenchmarkRunner(BenchmarkRunner): + """Runner for Python benchmarks.""" + + def __init__( + self, + bench_folder: str, + operators: Optional[List[str]], + keep_outputs: bool = False, + max_noise_pct: float = DEFAULT_BENCHMARK_QUALITY.max_noise_pct, + config_file: Optional[str] = None, + tiers: Optional[set] = None, + config_keys: Optional[List[str]] = None, + warmup_cap: Optional[int] = None, + ): + super().__init__( + "bench_", + bench_folder, + operators, + "python", + keep_outputs, + max_noise_pct, + config_file=config_file, + tiers=tiers, + config_keys=config_keys, + warmup_cap=warmup_cap, + ) + self.config = self._load_config(need_axis_args=False) + self._verify_cuda_available() + + def _python_package_path(self) -> Optional[Path]: + """Return the build-tree Python package path when available. + + Source-tree runs against build-rel/bin should exercise the freshly + built cvcuda extension instead of an older user-site install. Installed + benchmark layouts do not have this sibling directory, so they keep the + ambient Python environment. + """ + candidate = Path(self.bench_folder).resolve().parent / "python3" + if (candidate / "cvcuda" / "__init__.py").exists(): + return candidate + return None + + def benchmark_env(self) -> Optional[dict]: + env = super().benchmark_env() + package_path = self._python_package_path() + if package_path is not None: + existing = env.get("PYTHONPATH") + env["PYTHONPATH"] = ( + str(package_path) + if not existing + else os.pathsep.join([str(package_path), existing]) + ) + return env + + def _verify_cuda_available(self): + """Verify CUDA is accessible from Python before running benchmarks.""" try: - df_all.to_csv(filepath_orig, index=False) - print( - f"I Full results across {args.num_runs} run(s) written to {filepath_orig}" + # Run a quick import check in a subprocess. We add the bench directory + # to sys.path so that python_bench_utils can be imported — it applies + # the cuda.pathfinder patch (for cuda-pathfinder >= 1.4) before importing + # cvcuda, ensuring the patch is active when cuda.bench is loaded. + # Note: we only check cvcuda and cuda.bench (pynvbench), not cuda.cuda + # which requires the cuda-python package. + bench_dir = str(Path(__file__).parent) + check_script = f"""\ +import sys +sys.path.insert(0, {bench_dir!r}) +print(f'Python: {{sys.executable}}') +try: + import python_bench_utils # applies cuda.pathfinder patch; imports cvcuda + import cvcuda + print(f'cvcuda: {{cvcuda.__file__}}') +except ImportError as e: + print(f'cvcuda import error: {{e}}') + sys.exit(1) +try: + import cuda.bench + print(f'cuda.bench (pynvbench): OK') +except ImportError as e: + print(f'cuda.bench import error: {{e}}') + sys.exit(1) +""" + result = subprocess.run( + [sys.executable, "-c", check_script], + capture_output=True, + text=True, + timeout=30, + env=self.benchmark_env(), ) + if result.returncode != 0: + log_warning("Python environment check failed:") + for line in result.stdout.strip().split("\n"): + print(f" {line}") + if result.stderr: + print(f" stderr: {result.stderr.strip()}") + else: + # Always show this info to help debug CI issues + log_info("Python benchmark environment:") + for line in result.stdout.strip().split("\n"): + print(f" {line}") except Exception as e: - print(f"E Failed to write aggregated results to {filepath_orig}: {e}") + log_warning(f"Python environment check failed: {e}") - if BANDWIDTH_COLNAME in df_all.columns: - try: - df_all[BANDWIDTH_COLNAME] = pd.to_numeric( - df_all[BANDWIDTH_COLNAME], errors="coerce" - ) - df_summary = ( - df_all.dropna(subset=[BANDWIDTH_COLNAME]) - .groupby(BENCH_COLNAME)[BANDWIDTH_COLNAME] - .mean() + def build_command( + self, + benchmark_path: str, + extra_args: List[str], + output_file: str, + config_key: Optional[str] = None, + ) -> List[str]: + """Build command for Python benchmark with --config-key for aliasing.""" + cmd = [sys.executable, benchmark_path, "--csv", output_file] + if config_key: + cmd.extend(["--config-key", config_key]) + config_file = self.config_file or str( + Path(__file__).resolve().parent / "config" / "bench_params.json" + ) + cmd.extend(["--config-file", config_file]) + cmd.extend(extra_args) + return cmd + + +# ============================================================================= +# PAIRED BENCHMARK DISCOVERY +# ============================================================================= + + +def discover_operator_pairs( + cpp_runner: Optional[BenchmarkRunner], + python_runner: Optional[BenchmarkRunner], + config_keys: Optional[List[str]] = None, +) -> List[tuple]: + """Discover all operators and their C++/Python benchmark paths. + + Returns list of (config_key, cpp_path_or_None, python_path_or_None). + """ + cpp_map = dict(cpp_runner.discover_benchmarks()) if cpp_runner else {} + python_map = dict(python_runner.discover_benchmarks()) if python_runner else {} + if config_keys is not None: + all_keys = [key for key in config_keys if key in cpp_map or key in python_map] + elif (cpp_runner and cpp_runner.operators) or ( + python_runner and python_runner.operators + ): + all_keys = [] + seen = set() + for key in list(cpp_map) + list(python_map): + if key not in seen: + all_keys.append(key) + seen.add(key) + else: + all_keys = sorted(set(cpp_map.keys()) | set(python_map.keys())) + return [(key, cpp_map.get(key), python_map.get(key)) for key in all_keys] + + +# ============================================================================= +# CLI ENTRY POINT +# ============================================================================= + + +def _format_unknown_operator_error(unknown: List[str], available: List[str]) -> str: + details = [] + for operator_name in unknown: + suggestions = difflib.get_close_matches(operator_name, available, n=3) + suffix = f" (did you mean {', '.join(suggestions)}?)" if suggestions else "" + details.append(f"{operator_name}{suffix}") + return ( + f"--operator: unknown operator(s): {', '.join(details)}. " + "Use --list-operators to see valid names." + ) + + +def parse_args(): + """Parse command line arguments for unified benchmark runner.""" + parser = argparse.ArgumentParser( + description="Run selected CV-CUDA C++ and Python benchmarks and write one result.", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog="""\ +Examples: + run_bench.py --lang python --operator resize + run_bench.py --output bench_output.json + run_bench.py . -- --axis shape=1x1080x1920 + +Output: + CSV by default; use a .json path for the baseline tools. GPU diagnostic + files are written beside the result. + +Exit status: + 0 A result was written and final validation passed or was skipped. + 1 The run could not complete, output failed, or validation failed. + 2 Arguments or benchmark configuration could not be parsed. +""", + ) + parser.add_argument( + "--lang", + choices=["cpp", "python", "both"], + default="both", + help="Language to run: cpp, python, or both (default: both)", + ) + parser.add_argument( + "--operator", + "--operators", + dest="operator", + type=str, + default=None, + help=( + "Run exact operator name(s), comma-separated or quoted " + "space-separated. Names must be listed in " + "bench/config/bench_params.json. Example: --operator resize,gaussian" + ), + ) + parser.add_argument( + "--benchmarks", + type=str, + default=None, + help=( + "Deprecated alias for --operator. Matching is exact against " + "operator names; substring matching is no longer supported." + ), + ) + parser.add_argument( + "--list-operators", + action="store_true", + default=False, + help="List operator names available in the benchmark manifest and exit.", + ) + parser.add_argument( + "--keep-outputs", + action="store_true", + default=False, + help="Keep individual benchmark output CSV files (for debugging)", + ) + parser.add_argument( + "--verbose", + action="store_true", + default=False, + help="Show verbose output including full command lines", + ) + parser.add_argument( + "--quiet", + action="store_true", + default=False, + help="Suppress per-benchmark output, show only summary", + ) + parser.add_argument( + "--no-color", + action="store_true", + default=False, + help="Disable colored output (useful for CI/logging)", + ) + parser.add_argument( + "--max-noise-pct", + type=float, + default=DEFAULT_BENCHMARK_QUALITY.max_noise_pct, + help="Maximum allowed noise/stddev percentage (default: %(default)s)", + ) + parser.add_argument( + "--max-perf-diff-pct", + type=float, + default=DEFAULT_BENCHMARK_QUALITY.max_perf_diff_pct, + help=( + "Maximum allowed performance difference between C++ and Python " + "(default: %(default)s)" + ), + ) + parser.add_argument( + "--max-perf-diff-us", + type=float, + default=DEFAULT_BENCHMARK_QUALITY.max_perf_diff_us, + help=( + "Maximum absolute C++/Python timing difference in microseconds " + "(default: %(default)s)." + ), + ) + parser.add_argument( + "--skip-validation", + action="store_true", + default=False, + help=( + "Do not fail the final run on noise or C++/Python parity checks. " + "Result rows still include validation statuses." + ), + ) + parser.add_argument( + "--max-retries", + type=int, + default=0, + help="Max retries per operator if validation fails (default: 0)", + ) + parser.add_argument( + "--bench-min-time", + type=float, + default=0.25, + help="nvbench --min-time per-config measurement floor in seconds " + "(default: 0.25; nvbench's own default is 0.5). Lower = faster runs; " + "stays well below the --max-noise-pct gate. Pass 0 to use nvbench's default.", + ) + parser.add_argument( + "--bench-max-noise", + type=float, + default=1.0, + help="nvbench --max-noise stopping criterion in percent rel-stddev " + "(default: 1.0; nvbench's own default is 0.5). Higher = faster runs; " + "must stay below the configured --max-noise-pct gate. " + "Pass 0 to use nvbench's default.", + ) + parser.add_argument( + "--warmup-cap", + type=parse_warmup_cap, + default=None, + help=( + "Cap configured warmup iterations for every C++ and Python " + "benchmark. Pass 0 to disable warmup; omit to preserve each " + "operator's configured count." + ), + ) + parser.add_argument( + "--output", + "-o", + type=str, + default=None, + help=( + "Output file path. .csv writes the legacy combined CSV, .json writes " + "the baseline JSON payload. Default: bench_output.csv in bench_folder, " + "or bench_output_.csv when --config-file is set." + ), + ) + parser.add_argument( + "--tier", + type=str, + default="basic", + help="Tiers: basic, advanced, or basic,advanced (default: basic).", + ) + parser.add_argument( + "--config-file", + type=str, + default=None, + help=( + "Path to a bench_params.json config file. Defaults to " + "bench/config/bench_params.json. Power-user escape hatch — most " + "users want --tier or --config-key instead." + ), + ) + parser.add_argument( + "--config-key", + type=str, + default=None, + help=( + "Run exact config key(s), comma-separated. This selection bypasses " + "--tier and --operator." + ), + ) + parser.add_argument( + "bench_folder", + nargs="?", + default=None, + metavar="BENCH_FOLDER", + help=( + "Directory containing benchmark executables and scripts (default: " + "this script's directory). Supply it before forwarded arguments." + ), + ) + parser.add_argument( + "bench_args", + nargs=argparse.REMAINDER, + metavar="BENCH_ARG", + help=( + "Arguments forwarded to each benchmark. Put BENCH_FOLDER and -- " + "before them." + ), + ) + + args = parser.parse_args() + + if args.bench_folder is None: + args.bench_folder = str(Path(__file__).parent) + + if args.operator and args.benchmarks: + log_error("Use only one of --operator or --benchmarks") + sys.exit(2) + + operator_arg = args.operator or args.benchmarks + + # Parse selectors and config metadata; fail fast on typos. + sys.path.insert(0, str(Path(__file__).parent / "config")) + from load_config import ( + load_bench_config, + load_bench_manifest, + parse_config_key_arg, + parse_operator_arg, + parse_tier_arg, + ) + + try: + manifest = load_bench_manifest(args.config_file) + except Exception as e: + log_error(f"Failed to load benchmark manifest: {e}") + sys.exit(2) + + if args.list_operators: + for operator_name in manifest: + print(operator_name) + sys.exit(0) + + args.operators = None + if operator_arg: + try: + args.operators = parse_operator_arg(operator_arg) + except ValueError as e: + option_name = "--benchmarks" if args.benchmarks else "--operator" + log_error(str(e).replace("--operator", option_name)) + sys.exit(2) + + available = list(manifest) + unknown = [ + operator_name + for operator_name in args.operators + if operator_name not in manifest + ] + if unknown: + option_name = "--benchmarks" if args.benchmarks else "--operator" + message = _format_unknown_operator_error(unknown, available).replace( + "--operator", option_name + ) + log_error(message) + sys.exit(2) + + try: + args.tiers = parse_tier_arg(args.tier) + except ValueError as e: + log_error(str(e)) + sys.exit(2) + + args.config_keys = None + if args.config_key: + try: + args.config_keys = parse_config_key_arg(args.config_key) + config = load_bench_config(args.config_file) + except ValueError as e: + log_error(str(e)) + sys.exit(2) + except Exception as e: + log_error(f"Failed to load benchmark config: {e}") + sys.exit(2) + + unknown = [key for key in args.config_keys if key not in config] + if unknown: + log_error( + f"--config-key: unknown key(s) {unknown}; " + "run with a key present in bench/config/bench_params.json" + ) + sys.exit(2) + + missing_benchmark = [ + key + for key in args.config_keys + if not isinstance(config.get(key), dict) or "benchmark" not in config[key] + ] + if missing_benchmark: + log_error( + "--config-key: key(s) missing required benchmark field " + f"{missing_benchmark}" + ) + sys.exit(2) + + # Set default output path if not specified. + # --tier basic -> bench_output.csv (legacy default) + # An explicit --config-file overrides this with stem-based naming, for + # power users running ad-hoc custom configs. + if args.output is None: + if args.config_file: + stem = Path(args.config_file).stem # e.g. "bench_params_smoke" + suffix = stem.replace("bench_params", "").lstrip("_") + if suffix: + args.output = os.path.join( + args.bench_folder, f"bench_output_{suffix}.csv" ) - if not df_summary.empty: - pd.options.display.float_format = "{: .2%}".format - print( - f"\nI Summary results (Mean {BANDWIDTH_COLNAME} across {args.num_runs} run(s)): " - ) - print(df_summary) - pd.options.display.float_format = None - else: - print( - f"W Could not compute mean {BANDWIDTH_COLNAME} summary (no valid numeric data)." - ) - except Exception as e: - print(f"W Could not compute mean {BANDWIDTH_COLNAME} summary: {e}") + else: + args.output = os.path.join(args.bench_folder, "bench_output.csv") else: - print(f"W Cannot compute {BANDWIDTH_COLNAME} summary: Column not present.") + args.output = os.path.join(args.bench_folder, "bench_output.csv") + + # Apply global settings + global VERBOSE, QUIET + VERBOSE = args.verbose + QUIET = args.quiet + if args.no_color: + Colors.disable() + + # Inject nvbench stopping-criterion overrides into extra args, unless the + # user already passed the same flag explicitly. This trims per-config + # measurement time on the all-operator run while keeping noise well below + # the --max-noise-pct gate. + user_extra = list(args.bench_args or []) + nvbench_prefix: List[str] = [] + if ( + args.bench_min_time + and args.bench_min_time > 0 + and "--min-time" not in user_extra + ): + nvbench_prefix += ["--min-time", str(args.bench_min_time)] + if ( + args.bench_max_noise + and args.bench_max_noise > 0 + and "--max-noise" not in user_extra + ): + nvbench_prefix += ["--max-noise", str(args.bench_max_noise)] + args.bench_args = nvbench_prefix + user_extra + + return args + + +def _print_banner(title: str): + """Print a colored section banner.""" + print(f"\n{Colors.CYAN}{'=' * 60}{Colors.RESET}") + print(f"{Colors.CYAN}{title:<60}{Colors.RESET}") + print(f"{Colors.CYAN}{'=' * 60}{Colors.RESET}\n") + + +def _get_gpu_info() -> str: + """Get GPU name using nvidia-smi.""" + try: + result = subprocess.run( + ["nvidia-smi", "--query-gpu=name", "--format=csv,noheader"], + capture_output=True, + text=True, + timeout=5, + ) + if result.returncode == 0: + return result.stdout.strip().split("\n")[0] # First GPU + except Exception: + pass + return "Unknown GPU" + + +def _baseline_config_paths(config_file: Optional[str]) -> Tuple[List[Path], Path]: + sys.path.insert(0, str(Path(__file__).parent / "config")) + from load_config import load_bench_manifest + if config_file is None: + base_dir = DEFAULT_CONFIG_DIR else: - print( - "W No valid benchmark results collected across any run. Skipping summary generation." + config_path = Path(config_file).resolve() + base_dir = config_path if config_path.is_dir() else config_path.parent + + manifest = load_bench_manifest(config_file) + paths: List[Path] = [] + seen = set() + for spec in manifest.values(): + raw_path = Path(spec["config"]) + if raw_path.is_absolute(): + path = raw_path + elif raw_path.exists(): + path = raw_path.resolve() + else: + path = (base_dir / raw_path).resolve() + if path not in seen: + seen.add(path) + paths.append(path) + + sku_map_path = base_dir / "sku_map.json" + if not sku_map_path.is_file(): + sku_map_path = DEFAULT_SKU_MAP_PATH + return paths, sku_map_path + + +def _write_output_json(args, df_combined: pd.DataFrame) -> None: + config_paths, sku_map_path = _baseline_config_paths(args.config_file) + index = load_config_index(paths=config_paths) + payload = baseline_payload_from_dataframe( + df_combined, + index=index, + sku_map_path=sku_map_path, + source=Path(args.output), + ) + out_path = Path(args.output) + out_path.parent.mkdir(parents=True, exist_ok=True) + out_path.write_text(json.dumps(payload, indent=4) + "\n") + log_success(f"Results written to {out_path}") + + +def _write_output_csv(args, df_combined: pd.DataFrame) -> None: + out_path = Path(args.output) + out_path.parent.mkdir(parents=True, exist_ok=True) + ResultsProcessor( + args.max_noise_pct, args.max_perf_diff_pct, args.max_perf_diff_us + ).finalize_for_output(df_combined.copy()).to_csv(out_path, index=False) + log_success(f"Results written to {out_path}") + + +def _write_output(args, df_combined: pd.DataFrame) -> None: + suffix = Path(args.output).suffix.lower() + if suffix == ".json": + _write_output_json(args, df_combined) + else: + _write_output_csv(args, df_combined) + + +def _run_paired_benchmarks(args) -> tuple: + """Run C++/Python benchmark pairs per operator with immediate parity check.""" + # Create runners + cpp_runner = ( + CppBenchmarkRunner( + bench_folder=args.bench_folder, + operators=args.operators, + keep_outputs=args.keep_outputs, + max_noise_pct=args.max_noise_pct, + config_file=args.config_file, + tiers=args.tiers, + config_keys=args.config_keys, + warmup_cap=args.warmup_cap, ) + if args.lang in ["cpp", "both"] + else None + ) - if all_gpu_times: - gpu_stats_results = [] - bench_names_with_incomplete_data = [] + python_runner = ( + PythonBenchmarkRunner( + bench_folder=args.bench_folder, + operators=args.operators, + keep_outputs=args.keep_outputs, + max_noise_pct=args.max_noise_pct, + config_file=args.config_file, + tiers=args.tiers, + config_keys=args.config_keys, + warmup_cap=args.warmup_cap, + ) + if args.lang in ["python", "both"] + else None + ) - for bench_name, times_list in all_gpu_times.items(): - if len(times_list) == args.num_runs: - try: - mean_gpu_time = np.mean(times_list) - std_dev_gpu_time = np.std(times_list) - gpu_stats_results.append( - { - BENCH_COLNAME: bench_name, - f"Mean {GPU_TIME_COLNAME}": mean_gpu_time, - f"Std Dev {GPU_TIME_COLNAME}": std_dev_gpu_time, - "Runs": args.num_runs, - } - ) - except Exception as e: - print(f"W Error calculating stats for '{bench_name}': {e}") + pairs = discover_operator_pairs(cpp_runner, python_runner, args.config_keys) + if not pairs: + log_error("No benchmarks found to run") + sys.exit(1) + + _print_banner("=== Running Benchmarks ===") + log_info(f"GPU: {_get_gpu_info()}") + log_info(f"Found {len(pairs)} benchmarks") + if args.warmup_cap is not None: + log_info(f"Warmup iterations capped at {args.warmup_cap}") + + processor = ResultsProcessor( + args.max_noise_pct, args.max_perf_diff_pct, args.max_perf_diff_us + ) + + def run_one(runner, path, config_key): + """Run a single benchmark and update runner stats. Returns df or None.""" + output_file, noise_pass = runner.run_benchmark( + path, args.bench_args, config_key + ) + runner.summary.total += 1 + if output_file: + results_before = len(runner.results) + collect_pass = runner.collect_results(output_file, config_key) + # Always return df if results were collected, regardless of noise + if len(runner.results) > results_before: + runner.summary.successful += 1 if (noise_pass and collect_pass) else 0 + runner.summary.failed += 0 if (noise_pass and collect_pass) else 1 + return runner.results[-1] + runner.summary.failed += 1 + return None + + # Collect all operator results in memory, then write one JSON artifact. + total_retries = 0 + max_attempts = args.max_retries + 1 # +1 for initial attempt + + def check_validation_passed(df_op: pd.DataFrame) -> bool: + """Check if all rows in processed dataframe passed validation.""" + if df_op is None or len(df_op) == 0: + return False + if "Status" not in df_op.columns: + return True + return not df_op["Status"].str.contains("FAIL").any() + + def get_failure_reasons(df_op: pd.DataFrame) -> List[str]: + """Extract failure reasons from Status column for failed rows.""" + if df_op is None or "Status" not in df_op.columns: + return [] + failed = df_op[df_op["Status"].str.contains("FAIL", na=False)] + reasons = [] + for _, row in failed.drop_duplicates(subset=["Status"]).iterrows(): + status = row["Status"] + # Extract the reason from "FAIL (reason)" format + if "(" in status and ")" in status: + _, _, after = status.partition("(") + reason, _, _ = after.rpartition(")") else: - if len(times_list) > 1: - mean_gpu_time = np.mean(times_list) - std_dev_gpu_time = np.std(times_list) - gpu_stats_results.append( - { - BENCH_COLNAME: bench_name, - f"Mean {GPU_TIME_COLNAME}": mean_gpu_time, - f"Std Dev {GPU_TIME_COLNAME}": std_dev_gpu_time, - "Runs": len(times_list), - } - ) - bench_names_with_incomplete_data.append( - f"{bench_name} ({len(times_list)}/{args.num_runs} runs)" - ) - elif len(times_list) == 1: - mean_gpu_time = times_list[0] - std_dev_gpu_time = 0.0 - gpu_stats_results.append( - { - BENCH_COLNAME: bench_name, - f"Mean {GPU_TIME_COLNAME}": mean_gpu_time, - f"Std Dev {GPU_TIME_COLNAME}": std_dev_gpu_time, - "Runs": 1, - } - ) - bench_names_with_incomplete_data.append( - f"{bench_name} (1/{args.num_runs} runs)" + reason = "unknown" + # Build a descriptive message + config_parts = [] + if "InOutDataType" in row and pd.notna(row["InOutDataType"]): + config_parts.append(str(row["InOutDataType"])) + if "shape" in row and pd.notna(row["shape"]): + config_parts.append(str(row["shape"])) + config_str = ", ".join(config_parts) if config_parts else "" + reasons.append(f"{config_str}: {reason}" if config_str else reason) + return reasons + + # Run each operator pair with retry logic + for idx, (config_key, cpp_path, py_path) in enumerate(pairs, 1): + if not QUIET: + print(f"\n{Colors.BOLD}[{idx}/{len(pairs)}] {config_key}{Colors.RESET}") + + best_df_op = None + validation_passed = False + + for attempt in range(1, max_attempts + 1): + if attempt > 1: + total_retries += 1 + if not QUIET: + print( + f"{Colors.YELLOW} Retry {attempt - 1}/{args.max_retries} " + f"for {config_key}{Colors.RESET}" ) + + op_start = time.time() + + # Save state before running (for potential rollback on retry) + cpp_results_before = len(cpp_runner.results) if cpp_runner else 0 + py_results_before = len(python_runner.results) if python_runner else 0 + + cpp_df = ( + run_one(cpp_runner, cpp_path, config_key) + if cpp_runner and cpp_path + else None + ) + py_df = ( + run_one(python_runner, py_path, config_key) + if python_runner and py_path + else None + ) + op_elapsed = time.time() - op_start + + # Process results for this operator + dfs = [df for df in [cpp_df, py_df] if df is not None] + if dfs: + has_both = cpp_df is not None and py_df is not None + df_op = processor.process(dfs, compute_parity=has_both) + + validation_passed = check_validation_passed(df_op) + + if validation_passed or attempt == max_attempts: + # Keep these results (passed or last attempt) + best_df_op = df_op + break else: - bench_names_with_incomplete_data.append( - f"{bench_name} (0/{args.num_runs} runs)" - ) + # Print failure reasons before retrying + if not QUIET: + reasons = get_failure_reasons(df_op) + for reason in reasons: + print(f"{Colors.RED} ✗ {reason}{Colors.RESET}") + # Rollback: remove results added in this attempt for retry + if cpp_runner and len(cpp_runner.results) > cpp_results_before: + cpp_runner.results.pop() + if python_runner and len(python_runner.results) > py_results_before: + python_runner.results.pop() + else: + # No results collected, can't retry + break - if bench_names_with_incomplete_data: + if best_df_op is not None: + if not QUIET: + print_parity_summary_table(best_df_op) + + if not QUIET: + retry_info = f" (after {attempt - 1} retries)" if attempt > 1 else "" print( - f"\nW Warning: The following benchmarks had incomplete data " - f"across runs (results based on available data): " - f"{', '.join(bench_names_with_incomplete_data)}" + f"{Colors.GRAY} Completed in {op_elapsed:.2f}s{retry_info}{Colors.RESET}" ) - if gpu_stats_results: - gpu_stats_df = pd.DataFrame(gpu_stats_results) - filepath_gpu = os.path.join(args.bench_folder, GPU_STATS_OUTPUT) - try: - gpu_stats_df.to_csv(filepath_gpu, index=False, float_format="%.6f") - print( - f"\nI GPU Time statistics across up to {args.num_runs} run(s) written to {filepath_gpu}" - ) - print(gpu_stats_df.to_string(index=False, float_format="%.6f")) - except Exception as e: - print(f"E Failed to write GPU stats results to {filepath_gpu}: {e}") + if total_retries > 0: + log_info(f"Total retries across all operators: {total_retries}") + return cpp_runner, python_runner + + +def _process_and_validate(args, cpp_runner, python_runner) -> tuple: + """Process results and run validation checks.""" + all_results = [] + if cpp_runner and cpp_runner.results: + all_results.extend(cpp_runner.results) + if python_runner and python_runner.results: + all_results.extend(python_runner.results) + + if not all_results: + log_warning("No benchmark results were successfully processed.") + sys.exit(1) + + compute_parity = args.lang == "both" and cpp_runner and python_runner + processor = ResultsProcessor( + args.max_noise_pct, args.max_perf_diff_pct, args.max_perf_diff_us + ) + df_combined = processor.process(all_results, compute_parity=compute_parity) + + validation_failed = False + if not args.skip_validation: + result = processor.validate(df_combined, check_parity=compute_parity) + if result.has_errors: + validation_failed = True + processor.print_errors(result) else: - print( - "W No benchmarks had sufficient GPU time data to calculate statistics." + log_success( + f"All validations passed (noise <{args.max_noise_pct}%, " + f"perf diff <{args.max_perf_diff_pct}% AND <{args.max_perf_diff_us:.0f}us)" ) - else: - print( - "W No GPU time data collected across any run. Skipping GPU statistics generation." + + return df_combined, validation_failed + + +def _print_final_summary(args, df_combined): + """Print final summary statistics.""" + print(f"\n{Colors.BOLD}--- Summary ---{Colors.RESET}") + num_operators = ( + df_combined["Benchmark"].nunique() if "Benchmark" in df_combined.columns else 0 + ) + config_cols = get_config_cols(df_combined) + num_configs = ( + len(df_combined.groupby(config_cols, dropna=False)) + if config_cols + else len(df_combined) + ) + + failed_configs = 0 + if "Status" in df_combined.columns and config_cols: + for _, group in df_combined.groupby(config_cols, dropna=False): + if group["Status"].str.contains("FAIL").any(): + failed_configs += 1 + + passed = num_configs - failed_configs + print( + f" Operators: {num_operators}, Configurations: {num_configs} " + f"({passed} passed, {failed_configs} failed)" + ) + print(f" Output: {args.output}") + + # Compute and display performance statistics + if "Language" in df_combined.columns and "GPU Noise (%)" in df_combined.columns: + cpp_rows = df_combined[df_combined["Language"] == "cpp"] + py_rows = df_combined[df_combined["Language"] == "python"] + + stats = [] + + # C++ noise statistics + if len(cpp_rows) > 0: + cpp_noise_pct = cpp_rows["GPU Noise (%)"].mean() + cpp_noise_us = ( + cpp_rows["GPU Noise (µs)"].mean() + if "GPU Noise (µs)" in cpp_rows.columns + else None + ) + if cpp_noise_us is not None: + stats.append( + f"C++ noise: ±{cpp_noise_pct:.2f}% (±{cpp_noise_us:.1f}µs)" + ) + else: + stats.append(f"C++ noise: ±{cpp_noise_pct:.2f}%") + + # Python noise statistics + if len(py_rows) > 0: + py_noise_pct = py_rows["GPU Noise (%)"].mean() + py_noise_us = ( + py_rows["GPU Noise (µs)"].mean() + if "GPU Noise (µs)" in py_rows.columns + else None + ) + if py_noise_us is not None: + stats.append(f"Py noise: ±{py_noise_pct:.2f}% (±{py_noise_us:.1f}µs)") + else: + stats.append(f"Py noise: ±{py_noise_pct:.2f}%") + + # Python overhead statistics (only when both languages present) + if "Py overhead (%)" in df_combined.columns: + overhead_pct = df_combined["Py overhead (%)"].dropna() + overhead_us = ( + df_combined["Py overhead (µs)"].dropna() + if "Py overhead (µs)" in df_combined.columns + else None + ) + if len(overhead_pct) > 0: + mean_overhead_pct = overhead_pct.mean() + if overhead_us is not None and len(overhead_us) > 0: + mean_overhead_us = overhead_us.mean() + stats.append( + f"Py overhead: +{mean_overhead_pct:.1f}% (+{mean_overhead_us:.0f}µs)" + ) + else: + stats.append(f"Py overhead: +{mean_overhead_pct:.1f}%") + + if stats: + print(f" {', '.join(stats)}") + + print(f"{Colors.GRAY} View full results: cat {args.output}{Colors.RESET}") + print(f"\n{Colors.BOLD}--- Benchmarking Complete ---{Colors.RESET}") + + +def main(): + args = parse_args() + + # Lock SM clock + start clock sampler before any kernels run. Cleanup + # registered via atexit so we always reset the lock and flush the log. + init_clock_control(out_dir=str(Path(args.output).parent or ".")) + + cpp_runner, python_runner = _run_paired_benchmarks(args) + df_combined, validation_failed = _process_and_validate( + args, cpp_runner, python_runner + ) + + output_failed = False + try: + _write_output(args, df_combined) + except BaselineError as exc: + output_failed = True + log_error(f"Failed to write benchmark output: {exc}") + + # Cleanup internal nvbench scratch files unless requested for debugging. + all_output_files = (cpp_runner.output_files if cpp_runner else []) + ( + python_runner.output_files if python_runner else [] + ) + if not args.keep_outputs: + for f in all_output_files: + try: + os.path.exists(f) and os.remove(f) + except Exception: + pass + all_output_files and log_info( + f"Cleaned up {len(all_output_files)} individual output file(s)" ) - print("\n--- Benchmarking Complete ---") + _print_final_summary(args, df_combined) + if validation_failed: + log_error("Benchmarking completed with validation errors") + sys.exit(1 if validation_failed or output_failed else 0) + + +if __name__ == "__main__": + main() diff --git a/samples/requirements_samples_common.txt b/bench/tests/conftest.py similarity index 60% rename from samples/requirements_samples_common.txt rename to bench/tests/conftest.py index 3b7273628..74bc7854c 100644 --- a/samples/requirements_samples_common.txt +++ b/bench/tests/conftest.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,14 +13,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Common Python packages for CV-CUDA samples -# Used by both CUDA 12 and CUDA 13 samples +# Make the parent bench/ directory importable for benchmark-tool tests. +import sys +from pathlib import Path -# NumPy 2 with version constraints based on Python version -numpy==2.0.2; python_version=="3.9" -numpy==2.2.6; python_version>="3.10" and python_version<"3.14" -numpy==2.3.3; python_version=="3.14" - -# Sample dependencies -onnx==1.19.0 -onnxslim==0.1.70 +BENCH_DIR = Path(__file__).resolve().parent.parent +if str(BENCH_DIR) not in sys.path: + sys.path.insert(0, str(BENCH_DIR)) diff --git a/bench/tests/test_axis_display.py b/bench/tests/test_axis_display.py new file mode 100644 index 000000000..0729750e9 --- /dev/null +++ b/bench/tests/test_axis_display.py @@ -0,0 +1,43 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""Tests for human-readable benchmark axis formatting.""" + +from __future__ import annotations + +from _internal.axes import format_axes, format_axis_name, format_axis_value + + +def test_axis_names_pass_through(): + assert format_axis_name("inputKind") == "inputKind" + assert format_axis_name("shape") == "shape" + + +def test_input_kind_values_are_self_describing(): + # inputKind is a string axis carrying its own labels; nothing to translate. + assert format_axis_value("inputKind", "Tensor") == "Tensor" + assert format_axis_value("inputKind", "VarShape") == "VarShape" + + +def test_format_axes_renders_input_kind(): + axes = ( + ("InOutDataType", "uint8"), + ("shape", "64x1080x1920"), + ("inputKind", "VarShape"), + ) + + assert format_axes(axes) == ( + "InOutDataType=uint8, shape=64x1080x1920, inputKind=VarShape" + ) diff --git a/bench/tests/test_bench_cli_surface.py b/bench/tests/test_bench_cli_surface.py new file mode 100644 index 000000000..b33a0964c --- /dev/null +++ b/bench/tests/test_bench_cli_surface.py @@ -0,0 +1,48 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Smoke tests for the supported benchmark command-line tools.""" + +from __future__ import annotations + +import os +import subprocess +import sys +from pathlib import Path + +import pytest + +BENCH_DIR = Path(__file__).resolve().parent.parent + + +@pytest.mark.parametrize("script", ("update_baseline.py", "validate_baselines.py")) +def test_baseline_maintenance_commands_are_internal(script): + assert not (BENCH_DIR / script).exists() + assert (BENCH_DIR / "_internal" / script).is_file() + + +@pytest.mark.parametrize( + ("script", "signatures"), + [ + ("run_bench.py", ("BENCH_FOLDER",)), + ("compare_wheels.py", ("REFERENCE_WHEEL", "CANDIDATE_WHEEL")), + ("compare_to_baseline.py", ("--current", "JSON")), + ], +) +def test_supported_command_help_is_self_contained(tmp_path, script, signatures): + env = os.environ.copy() + env.pop("PYTHONPATH", None) + + result = subprocess.run( + [sys.executable, str(BENCH_DIR / script), "--help"], + cwd=tmp_path, + env=env, + capture_output=True, + text=True, + check=False, + ) + + assert result.returncode == 0, result.stderr + assert all(signature in result.stdout for signature in signatures) + assert "Examples:" in result.stdout + assert "Exit status:" in result.stdout diff --git a/bench/tests/test_bench_rgb_guidelines.py b/bench/tests/test_bench_rgb_guidelines.py new file mode 100644 index 000000000..c87b8bd73 --- /dev/null +++ b/bench/tests/test_bench_rgb_guidelines.py @@ -0,0 +1,297 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""Static guideline checks for the RGB benchmark-config standardization. + +Each test maps to a rule (R1..R11) from the MR guidelines. These scan the +operator JSON configs and the category manifest only (no GPU / build needed); +timing rules (R7, R9) and parity (R12) are verified by local benchmark runs. +""" + +from __future__ import annotations + +import glob +import json +import math +import os +import re + +_HERE = os.path.dirname(os.path.abspath(__file__)) +_BENCH = os.path.dirname(_HERE) +_OPS_DIR = os.path.join(_BENCH, "config", "operators") +_MANIFEST = os.path.join(_BENCH, "config", "operator_categories.json") + +TWO_GB = 2**31 # TensorWrap32 uses int32 byte offsets + +_BASE_BYTES = { + "uint8": 1, + "int8": 1, + "uchar": 1, + "char": 1, + "uint16": 2, + "int16": 2, + "ushort": 2, + "short": 2, + "uint32": 4, + "int32": 4, + "uint": 4, + "int": 4, + "float32": 4, + "float": 4, + "float64": 8, + "double": 8, +} + + +# A vector dtype is a base name + a single channel digit (uchar3, float3, short2), +# distinct from a scalar whose trailing digits are a bit width (float32, uint16). +_VEC = re.compile(r"[a-z](\d)$", re.IGNORECASE) + + +def _channels(dtype: str) -> int: + m = _VEC.search(dtype) + if m and int(m.group(1)) in (2, 3, 4): + return int(m.group(1)) + return 1 + + +def _base(dtype: str) -> str: + return dtype[:-1] if _channels(dtype) > 1 else dtype + + +def _dtype_bytes(dtype: str) -> int: + return _BASE_BYTES[_base(dtype)] * _channels(dtype) + + +def _is_scalar(dtype: str) -> bool: + return _channels(dtype) == 1 + + +def _is_rgb(dtype: str) -> bool: + return _channels(dtype) == 3 + + +def _is_rgba(dtype: str) -> bool: + return _channels(dtype) == 4 + + +def _load_manifest() -> dict: + return json.load(open(_MANIFEST))["operators"] + + +def _op_files() -> dict: + return { + os.path.basename(f)[:-5]: f for f in glob.glob(os.path.join(_OPS_DIR, "*.json")) + } + + +def _profiles(): + """Yield (op, profile_key, body) for every profile in every operator config.""" + for op, f in sorted(_op_files().items()): + raw = json.load(open(f)) + profiles = raw["configs"] if "configs" in raw else raw + for key, body in profiles.items(): + yield op, key, body + + +def _shapes(body) -> list: + return body.get("string_axes", {}).get("shape", []) + + +def _batch(shape: str): + head = shape.split("x")[0] + return int(head) if head.lstrip("-").isdigit() else None + + +def _is_round(n: int) -> bool: + """Powers of 2, multiples of 4, or small values (<=4).""" + return n <= 4 or (n & (n - 1)) == 0 or n % 4 == 0 + + +def _max_tensor_elems(body, shape: str): + """Largest input/output tensor represented by a resize profile.""" + try: + dims = [int(x) for x in shape.split("x")] + except ValueError: + return None + + input_elems = math.prod(dims) + if len(dims) != 3: + return input_elems + + batch, height, width = dims + resize_types = body.get("string_axes", {}).get("resizeType", []) + output_elems = [input_elems] + for resize_type in resize_types: + if resize_type == "EXPAND": + output_elems.append(batch * (height * 2) * (width * 2)) + elif resize_type.startswith("TARGET_"): + target_height, target_width = ( + int(dim) for dim in resize_type.removeprefix("TARGET_").split("x") + ) + output_elems.append(batch * target_height * target_width) + return max(output_elems) + + +CATS = _load_manifest() +CAT_A = {op for op, m in CATS.items() if m["category"] == "A"} +CAT_B = {op for op, m in CATS.items() if m["category"] == "B"} + + +# --------------------------------------------------------------------------- +# R1 — every operator classified A/B/C; manifest matches the config dir. +# --------------------------------------------------------------------------- +def test_r1_every_operator_classified(): + on_disk = set(_op_files()) + classified = set(CATS) + assert on_disk == classified, ( + f"unclassified ops: {sorted(on_disk - classified)}; " + f"stale manifest entries: {sorted(classified - on_disk)}" + ) + assert all(m["category"] in {"A", "B", "C"} for m in CATS.values()) + + +# --------------------------------------------------------------------------- +# R2 / R5 — Category-A basic tier is RGB-only (uchar3/float3); no scalar, no RGBA. +# --------------------------------------------------------------------------- +def test_r2_cat_a_basic_is_rgb_only(): + bad = [] + for op, key, body in _profiles(): + if ( + op in CAT_A + and not CATS[op].get("rgb_unsupported") + and body.get("tier") == "basic" + ): + non_rgb = [dt for dt in body.get("dtypes", []) if not _is_rgb(dt)] + if non_rgb: + bad.append(f"{key}: {non_rgb}") + assert ( + not bad + ), "Cat-A basic profiles must be RGB-only (uchar3/float3):\n " + "\n ".join(bad) + + +# --------------------------------------------------------------------------- +# R3 — Category-A single-channel profiles live only in the advanced tier. +# --------------------------------------------------------------------------- +def test_r3_cat_a_single_channel_only_in_advanced(): + bad = [] + for op, key, body in _profiles(): + if ( + op in CAT_A + and not CATS[op].get("rgb_unsupported") + and body.get("tier") != "advanced" + ): + scalar = [dt for dt in body.get("dtypes", []) if _is_scalar(dt)] + if scalar: + bad.append(f"{key} (tier={body.get('tier')}): {scalar}") + assert ( + not bad + ), "Cat-A single-channel profiles must be in the advanced tier:\n " + "\n ".join( + bad + ) + + +# --------------------------------------------------------------------------- +# R4 — Category-A ops with basic RGB coverage carry an RGBA mirror in advanced. +# --------------------------------------------------------------------------- +def test_r4_cat_a_has_advanced_rgba_mirror(): + missing = [] + for op in sorted(CAT_A): + if CATS[op].get("rgba_unsupported") or CATS[op].get("rgb_unsupported"): + continue # op's kernel has no 4-channel (or no multi-channel) instantiation + if op not in _op_files(): + continue + raw = json.load(open(_op_files()[op])) + body_by_key = raw["configs"] if "configs" in raw else raw + has_basic_rgb = any( + b.get("tier") == "basic" and any(_is_rgb(d) for d in b.get("dtypes", [])) + for b in body_by_key.values() + ) + has_adv_rgba = any( + b.get("tier") == "advanced" + and any(_is_rgba(d) for d in b.get("dtypes", [])) + for b in body_by_key.values() + ) + if has_basic_rgb and not has_adv_rgba: + missing.append(op) + assert ( + not missing + ), "Cat-A ops with basic RGB but no advanced RGBA mirror: " + ", ".join(missing) + + +# --------------------------------------------------------------------------- +# R6 — batch sizes are round numbers / powers of two (all operators). +# --------------------------------------------------------------------------- +def test_r6_batch_sizes_are_round(): + bad = [] + for op, key, body in _profiles(): + if op not in CAT_A: # only the operators this MR re-tunes + continue + for shape in _shapes(body): + n = _batch(shape) + if n is not None and not _is_round(n): + bad.append(f"{key}: N={n} ({shape})") + assert not bad, ( + "Cat-A batch sizes must be round (pow2 / multiple of 4 / <=4):\n " + + "\n ".join(bad) + ) + + +# --------------------------------------------------------------------------- +# R8 — no tensor exceeds 2 GB (TensorWrap32), input or EXPAND output. +# --------------------------------------------------------------------------- +def test_r8_no_tensor_exceeds_2gb(): + bad = [] + for op, key, body in _profiles(): + channels = max(body.get("int64_axes", {}).get("numChannels", [1])) + for dt in body.get("dtypes", []): + for shape in _shapes(body): + elems = _max_tensor_elems(body, shape) + if elems is None: + continue + nbytes = elems * _dtype_bytes(dt) * channels + if nbytes >= TWO_GB: + bad.append(f"{key}: {dt} {shape} = {nbytes/1e9:.2f} GB") + assert not bad, "Configs exceeding the 2 GB TensorWrap32 limit:\n " + "\n ".join( + bad + ) + + +# --------------------------------------------------------------------------- +# R10 — inputKind is the string container selector everywhere. Operator-specific +# API variants extend it here: TensorBatch, TensorScalar, and ScalarGamma are +# reserved for operators with distinct execution pathways. +# --------------------------------------------------------------------------- +def test_r10_input_kind_is_valid(): + allowed = ( + "Tensor", + "VarShape", + "TensorBatch", + "TensorScalar", + "ScalarGamma", + ) + bad = [] + for op, key, body in _profiles(): + kinds = body.get("string_axes", {}).get("inputKind", []) + invalid = [v for v in kinds if v not in allowed] + if op != "hqresize" and "TensorBatch" in kinds: + invalid.append("TensorBatch (only HQResize supports it)") + if op != "normalize" and "TensorScalar" in kinds: + invalid.append("TensorScalar (only Normalize supports it)") + if op != "gammacontrast" and "ScalarGamma" in kinds: + invalid.append("ScalarGamma (only GammaContrast supports it)") + if invalid: + bad.append(f"{key}: {invalid}") + assert not bad, "inputKind has unsupported values:\n " + "\n ".join(bad) diff --git a/bench/tests/test_benchmark_state_order.py b/bench/tests/test_benchmark_state_order.py new file mode 100644 index 000000000..676e0dcdb --- /dev/null +++ b/bench/tests/test_benchmark_state_order.py @@ -0,0 +1,208 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Regression coverage for cross-language benchmark state ordering.""" + +from __future__ import annotations + +import itertools +import json +import subprocess +from pathlib import Path + +from config.axis_order import order_axis_names +from config.load_config import ConfigLoader, register_axes_from_config + + +BENCH_DIR = Path(__file__).resolve().parent.parent +OPERATORS_DIR = BENCH_DIR / "config/operators" + + +class _AxisRecorder: + def __init__(self): + self.axes = [] + + def add_string_axis(self, name, values): + self.axes.append((name, list(values))) + + def add_int64_axis(self, name, values): + self.axes.append((name, list(values))) + + def add_float64_axis(self, name, values): + self.axes.append((name, list(values))) + + +def _operator_documents(): + for path in sorted(OPERATORS_DIR.glob("*.json")): + document = json.loads(path.read_text()) + yield path, document["benchmark"], document["configs"] + + +def _cpp_registered_axes(tmp_path, documents): + generated_headers = [] + invocations = [] + for path, operator, _ in documents: + header = tmp_path / f"Bench{operator.title()}Config.hpp" + subprocess.run( + [ + "cmake", + f"-DJSON_FILE={path}", + f"-DOPERATOR={operator}", + f"-DOPERATOR_CAMEL={operator.title()}", + f"-DOUTPUT_FILE={header}", + "-P", + str(BENCH_DIR / "cpp/GenerateBenchConfig.cmake"), + ], + check=True, + capture_output=True, + text=True, + ) + generated_headers.append(header.name) + invocations.append( + f' Recorder recorder_{operator}("{operator}");\n' + f" recorder_{operator} BENCH_{operator.upper()}_AXES;" + ) + + fake_include = tmp_path / "include/nvbench" + fake_include.mkdir(parents=True) + (fake_include / "nvbench.cuh").write_text("\n") + source = tmp_path / "record_axes.cpp" + includes = "\n".join(f'#include "{header}"' for header in generated_headers) + source.write_text( + f"""\ +#include +#include +#include +#include + +{includes} + +struct Recorder +{{ + explicit Recorder(const char *operator_name) + : operator_name(operator_name) + {{ + }} + + Recorder &set_type_axes_names(std::initializer_list names) + {{ + for (const char *name : names) + std::cout << operator_name << '\\t' << name << '\\n'; + return *this; + }} + + Recorder &add_string_axis(const char *name, std::initializer_list) + {{ + std::cout << operator_name << '\\t' << name << '\\n'; + return *this; + }} + + Recorder &add_int64_axis(const char *name, std::initializer_list) + {{ + std::cout << operator_name << '\\t' << name << '\\n'; + return *this; + }} + + Recorder &add_float64_axis(const char *name, std::initializer_list) + {{ + std::cout << operator_name << '\\t' << name << '\\n'; + return *this; + }} + + const char *operator_name; +}}; + +int main() +{{ +{chr(10).join(invocations)} +}} +""" + ) + executable = tmp_path / "record_axes" + subprocess.run( + [ + "g++", + "-std=c++17", + f"-I{tmp_path / 'include'}", + f"-I{tmp_path}", + str(source), + "-o", + str(executable), + ], + check=True, + capture_output=True, + text=True, + ) + output = subprocess.run( + [str(executable)], check=True, capture_output=True, text=True + ).stdout + registered = {} + for line in output.splitlines(): + operator, name = line.split("\t") + registered.setdefault(operator, []).append(name) + return registered + + +def _effective_cpp_axes(registered_axes, config): + values_by_name = { + "InOutDataType": config.dtypes, + **config.string_axes, + **config.int64_axes, + **config.float64_axes, + } + return [ + (name, values_by_name[name]) + for name in registered_axes + if name in values_by_name + ] + + +def _state_order(axes): + names = [name for name, _ in axes] + reversed_values = [values for _, values in reversed(axes)] + return [ + dict(zip(names, reversed(values))) + for values in itertools.product(*reversed_values) + ] + + +def test_future_axes_are_automatically_sorted_by_utf8_bytes(): + assert order_axis_names( + ({"zFuture": [], "AFuture": [], "äFuture": [], "aFuture": []},) + ) == [ + "AFuture", + "aFuture", + "zFuture", + "äFuture", + ] + + +def test_all_cartesian_state_orders_match_cpp_and_python(tmp_path): + documents = list(_operator_documents()) + cpp_axes = _cpp_registered_axes(tmp_path, documents) + checked_configs = 0 + + for path, operator, entries in documents: + loader = ConfigLoader(str(path)) + for config_key in entries: + config = loader.get_operator_config(config_key) + python_recorder = _AxisRecorder() + python_recorder.add_string_axis("InOutDataType", config.dtypes) + register_axes_from_config(python_recorder, config) + + assert [name for name, _ in python_recorder.axes] == sorted( + (name for name, _ in python_recorder.axes), + key=lambda name: name.encode("utf-8"), + ) + + cpp_effective_axes = _effective_cpp_axes(cpp_axes[operator], config) + assert [name for name, _ in cpp_effective_axes] == [ + name for name, _ in python_recorder.axes + ], f"{operator}: {config_key}" + + cpp_states = _state_order(cpp_effective_axes) + python_states = _state_order(python_recorder.axes) + assert cpp_states == python_states, f"{operator}: {config_key}" + checked_configs += 1 + + assert checked_configs == sum(len(entries) for _, _, entries in documents) diff --git a/bench/tests/test_channelreorder_benchmark.py b/bench/tests/test_channelreorder_benchmark.py new file mode 100644 index 000000000..b76954536 --- /dev/null +++ b/bench/tests/test_channelreorder_benchmark.py @@ -0,0 +1,71 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Build-free regression coverage for the ChannelReorder benchmark.""" + +import importlib.util +import sys +import types +from pathlib import Path + + +BENCH_DIR = Path(__file__).resolve().parent.parent + + +def _load_channelreorder_benchmark(monkeypatch): + monkeypatch.setitem(sys.modules, "cupy", types.ModuleType("cupy")) + monkeypatch.setitem(sys.modules, "cvcuda", types.ModuleType("cvcuda")) + + fake_utils = types.ModuleType("python_bench_utils") + fake_utils.get_input_kind = lambda _name: "VarShape" + fake_utils.parse_shape = lambda _shape: (2, 3, 4) + fake_utils.get_dtype = lambda _dtype: object() + + def unexpected_benchmark_setup(*_args, **_kwargs): + raise AssertionError("NCHW_FAKE VarShape must skip before benchmark setup") + + fake_utils.get_num_channels = unexpected_benchmark_setup + fake_utils.get_dtype_size = unexpected_benchmark_setup + fake_utils.get_format_from_dtype = unexpected_benchmark_setup + fake_utils.create_tensor = unexpected_benchmark_setup + fake_utils.create_image_batch_varshape = unexpected_benchmark_setup + fake_utils.create_stream_cache = unexpected_benchmark_setup + fake_utils.run_benchmark = lambda *_args, **_kwargs: None + monkeypatch.setitem(sys.modules, "python_bench_utils", fake_utils) + + spec = importlib.util.spec_from_file_location( + "bench_channelreorder_test", + BENCH_DIR / "python" / "ops" / "bench_channelreorder.py", + ) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_fake_planar_varshape_skips_before_benchmark_setup(monkeypatch): + bench_channelreorder = _load_channelreorder_benchmark(monkeypatch) + + class State: + skipped = None + + def get_string(self, name): + return { + "shape": "2x3x4", + "InOutDataType": "U8x3", + "layout": "NCHW_FAKE", + "inputKind": "VarShape", + "orderPattern": "rotate", + }[name] + + def get_device(self): + raise AssertionError("NCHW_FAKE VarShape must skip before device setup") + + def skip(self, reason): + self.skipped = reason + + state = State() + + assert bench_channelreorder.channelreorder(state) is None + assert state.skipped == ( + "Fake-planar (NCHW_FAKE) ChannelReorder benchmark is tensor-only" + ) diff --git a/bench/tests/test_check_bench_sku.py b/bench/tests/test_check_bench_sku.py new file mode 100644 index 000000000..3864db25b --- /dev/null +++ b/bench/tests/test_check_bench_sku.py @@ -0,0 +1,116 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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 subprocess + +import pytest + +from _internal import check_bench_sku as cbs +from _internal.check_bench_sku import ( + EXIT_ELIGIBLE, + EXIT_ERROR, + EXIT_INELIGIBLE, + decide, + predict_locked_clock, +) + +ENTRIES = [ + {"gpu_name": "NVIDIA H100 PCIe", "power_cap_w": 350, "locked_sm_clock_mhz": 1095}, + { + "gpu_name": "NVIDIA A100-PCIE-40GB", + "power_cap_w": 250, + "locked_sm_clock_mhz": 1095, + }, +] + + +@pytest.mark.parametrize( + "name,power,clock,expected", + [ + ("NVIDIA H100 PCIe", 350, 1095, EXIT_ELIGIBLE), # canonical H100 + ("NVIDIA H100 PCIe", 350, 1005, EXIT_INELIGIBLE), # clamp silicon + ("NVIDIA H100 PCIe", 310, 1095, EXIT_INELIGIBLE), # low-TDP variant + ("NVIDIA A100-PCIE-40GB", 250, 1095, EXIT_ELIGIBLE), + ("NVIDIA L40", 300, 1095, EXIT_ELIGIBLE), # not in map -> fail-open + (None, None, None, EXIT_ELIGIBLE), # nvidia-smi unavailable -> fail-open + ], +) +def test_decide(name, power, clock, expected): + code, msg = decide(name, power, clock, ENTRIES) + assert code == expected + assert msg # always explains the decision + + +def test_decide_ineligible_lists_allowed_skus(): + code, msg = decide("NVIDIA H100 PCIe", 310, 1095, ENTRIES) + assert code == EXIT_INELIGIBLE + assert "(350, 1095)" in msg # surfaces the allowed combo for triage + + +@pytest.mark.parametrize( + "supported,expected", + [ + ({750, 900, 1005, 1095}, 1095), # 1095 available -> highest preferred + ({750, 900, 1005}, 1005), # 1095 absent -> clamps to 1005 + ({750, 900}, 900), + (None, 1095), # cannot query -> request preferred[0] (run_bench behavior) + (set(), None), # nothing supported in preferred list + ], +) +def test_predict_locked_clock(supported, expected): + assert predict_locked_clock(supported, preferred=[1095, 1005, 900, 750]) == expected + + +def test_smi_returns_none_when_binary_absent(monkeypatch): + def _raise(*a, **k): + raise FileNotFoundError() + + monkeypatch.setattr(cbs.subprocess, "run", _raise) + assert cbs._smi("--query-gpu=gpu_name") is None + + +def test_smi_raises_on_nonzero_exit(monkeypatch): + class _R: + returncode = 1 + stdout = "" + stderr = "boom" + + monkeypatch.setattr(cbs.subprocess, "run", lambda *a, **k: _R()) + with pytest.raises(cbs.NvidiaSmiError): + cbs._smi("--query-gpu=gpu_name") + + +def test_smi_raises_on_timeout(monkeypatch): + def _timeout(*a, **k): + raise subprocess.TimeoutExpired(cmd="nvidia-smi", timeout=10) + + monkeypatch.setattr(cbs.subprocess, "run", _timeout) + with pytest.raises(cbs.NvidiaSmiError): + cbs._smi("--query-gpu=gpu_name") + + +def test_main_fails_fast_when_nvidia_smi_errors(monkeypatch): + # nvidia-smi present but failing -> EXIT_ERROR (fail fast), not eligible. + def _raise(): + raise cbs.NvidiaSmiError("boom") + + monkeypatch.setattr(cbs, "_gpu_name", _raise) + assert cbs.main() == EXIT_ERROR + + +def test_main_fail_open_when_binary_absent(monkeypatch): + # nvidia-smi absent -> _gpu_name() is None -> fail-open (eligible). + monkeypatch.setattr(cbs, "_gpu_name", lambda: None) + assert cbs.main() == EXIT_ELIGIBLE diff --git a/bench/tests/test_compare_to_baseline.py b/bench/tests/test_compare_to_baseline.py new file mode 100644 index 000000000..ecae9ffd8 --- /dev/null +++ b/bench/tests/test_compare_to_baseline.py @@ -0,0 +1,348 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""Unit tests for JSON-backed compare_to_baseline.py.""" + +from __future__ import annotations + +import json + +import pytest + +from _internal.baselines import ( + BaselineError, + baseline_updates_from_payload, + case_key_from_axis_values, + load_config_index, + load_sku_map, + parse_case_key, + sku_stems, +) +from compare_to_baseline import Thresholds, compare_updates, format_markdown + + +def _write_json(path, payload): + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(payload, indent=4) + "\n") + + +def _write_sku_map(path, *entries): + _write_json( + path, + { + "entries": [ + { + "gpu_name": name, + "power_cap_w": cap, + "locked_sm_clock_mhz": clock, + "stem": stem, + } + for name, cap, clock, stem in entries + ] + }, + ) + + +def _case(config_key="resize_basic", dtype="uint8", shape="1x2x3", kernel=3): + return case_key_from_axis_values( + config_key, + [ + ("InOutDataType", dtype), + ("shape", shape), + ("inputKind", "Tensor"), + ("kernelSize", str(kernel)), + ], + ) + + +def _write_config( + root, + *, + baseline_time=100.0, + include_advanced=True, + include_other_operator=False, + sku="TESTSKU", +): + operators = root / "operators" + basic_case = _case() + configs = { + "resize_basic": { + "tier": "basic", + "dtypes": ["uint8"], + "string_axes": { + "shape": ["1x2x3"], + "inputKind": ["Tensor"], + }, + "int64_axes": { + "kernelSize": [3], + }, + "baselines": { + basic_case: { + sku: { + "n_runs": 2, + "gpu_time_us_cpp": baseline_time, + "gpu_time_us_python": baseline_time + 10.0, + "gpu_noise_us_cpp": 1.0, + "gpu_noise_us_python": 2.0, + "gpu_bwutil_cpp": 0.42, + "gpu_bwutil_python": 0.41, + } + } + }, + } + } + if include_advanced: + advanced_case = _case("resize_advanced", shape="9x9x9") + configs["resize_advanced"] = { + "tier": "advanced", + "dtypes": ["uint8"], + "string_axes": { + "shape": ["9x9x9"], + "inputKind": ["Tensor"], + }, + "int64_axes": { + "kernelSize": [3], + }, + "baselines": { + advanced_case: { + sku: { + "n_runs": 2, + "gpu_time_us_cpp": 900.0, + "gpu_time_us_python": 910.0, + "gpu_noise_us_cpp": 1.0, + "gpu_noise_us_python": 1.0, + "gpu_bwutil_cpp": 0.12, + "gpu_bwutil_python": 0.11, + } + } + }, + } + _write_json(operators / "resize.json", {"benchmark": "resize", "configs": configs}) + if include_other_operator: + other_case = _case("gaussian_basic") + _write_json( + operators / "gaussian.json", + { + "benchmark": "gaussian", + "configs": { + "gaussian_basic": { + "tier": "basic", + "dtypes": ["uint8"], + "string_axes": { + "shape": ["1x2x3"], + "inputKind": ["Tensor"], + }, + "int64_axes": {"kernelSize": [3]}, + "baselines": { + other_case: { + sku: { + "n_runs": 2, + "gpu_time_us_cpp": baseline_time, + "gpu_time_us_python": baseline_time + 10.0, + "gpu_noise_us_cpp": 1.0, + "gpu_noise_us_python": 2.0, + "gpu_bwutil_cpp": 0.42, + "gpu_bwutil_python": 0.41, + } + } + }, + } + }, + }, + ) + return basic_case + + +def _current_payload( + config_key="resize_basic", + sku="TESTSKU", + cpp_us=100.0, + python_us=110.0, + include_cpp=True, + include_python=True, + shape="1x2x3", + kernel=3, +): + metrics = {"n_runs": 1} + if include_cpp: + metrics.update( + { + "gpu_time_us_cpp": cpp_us, + "gpu_noise_us_cpp": 1.0, + "gpu_bwutil_cpp": 0.42, + } + ) + if include_python: + metrics.update( + { + "gpu_time_us_python": python_us, + "gpu_noise_us_python": 1.0, + "gpu_bwutil_python": 0.41, + } + ) + return { + _case(config_key, shape=shape, kernel=kernel): { + sku: metrics, + } + } + + +def _updates(tmp_path, payload, *, sku="TESTSKU", include_other_operator=False): + config_dir = tmp_path / "config" + _write_config( + config_dir, + sku="TESTSKU", + include_other_operator=include_other_operator, + ) + sku_map = config_dir / "sku_map.json" + _write_sku_map( + sku_map, + ("NVIDIA Test GPU", 350, 1095, "TESTSKU"), + ("NVIDIA Test GPU 2", 250, 1095, sku), + ) + index = load_config_index(config_dir / "operators") + updates = baseline_updates_from_payload( + payload, + index=index, + sku_stem_set=sku_stems(sku_map, strict=True), + source=tmp_path / "bench_output.json", + ) + return index, updates, tmp_path / "bench_output.json" + + +def test_case_key_parse_round_trip(): + case_key = _case() + assert parse_case_key(case_key) == ( + "resize_basic", + ( + ("InOutDataType", "uint8"), + ("shape", "1x2x3"), + ("inputKind", "Tensor"), + ("kernelSize", "3"), + ), + ) + + +def test_compare_passes_within_threshold(tmp_path): + index, measurements, _ = _updates( + tmp_path, _current_payload(cpp_us=105.0, python_us=115.0) + ) + result = compare_updates(measurements, index=index, sku="TESTSKU") + assert not result.any_fail + assert result.matched == 2 + + +def test_compare_flags_regression_and_improvement(tmp_path): + index, measurements, _ = _updates( + tmp_path, _current_payload(cpp_us=120.0, python_us=80.0) + ) + result = compare_updates(measurements, index=index, sku="TESTSKU") + assert len(result.regressions) == 1 + assert len(result.improvements) == 1 + + +def test_compare_reports_missing_case_key(tmp_path): + index, measurements, _ = _updates(tmp_path, _current_payload(include_python=False)) + result = compare_updates(measurements, index=index, sku="TESTSKU") + assert len(result.missing_in_current) == 1 + assert result.missing_in_current[0].language == "python" + + +def test_compare_reports_missing_sku(tmp_path): + index, measurements, _ = _updates( + tmp_path, + _current_payload(sku="NEWSKU"), + sku="NEWSKU", + ) + result = compare_updates(measurements, index=index, sku="NEWSKU") + assert len(result.missing_sku) == 2 + + +def test_tier_scoping_ignores_advanced_baselines_for_basic_run(tmp_path): + index, measurements, _ = _updates(tmp_path, _current_payload()) + result = compare_updates(measurements, index=index, sku="TESTSKU") + assert result.missing_in_current == [] + + +def test_operator_scoping_ignores_unselected_operator_baselines(tmp_path): + index, measurements, _ = _updates( + tmp_path, + _current_payload(), + include_other_operator=True, + ) + + unscoped = compare_updates(measurements, index=index, sku="TESTSKU") + scoped = compare_updates( + measurements, + index=index, + sku="TESTSKU", + operators={"resize"}, + ) + + assert len(unscoped.missing_in_current) == 2 + assert not scoped.any_fail + + +def test_json_validation_rejects_unknown_config_key(tmp_path): + with pytest.raises(BaselineError, match="does not exist"): + _updates(tmp_path, _current_payload(config_key="unknown_basic")) + + +def test_json_validation_rejects_out_of_order_case_key(tmp_path): + bad_case = ( + "resize_basic[kernelSize=3][InOutDataType=uint8]" + "[shape=1x2x3][inputKind=Tensor]" + ) + with pytest.raises(BaselineError, match="axes"): + _updates( + tmp_path, + { + bad_case: { + "TESTSKU": { + "n_runs": 1, + "gpu_time_us_cpp": 100.0, + "gpu_noise_us_cpp": 1.0, + "gpu_bwutil_cpp": 0.42, + } + } + }, + ) + + +def test_json_validation_rejects_unknown_sku(tmp_path): + with pytest.raises(BaselineError, match="unknown SKU"): + _updates(tmp_path, _current_payload(sku="UNKNOWN")) + + +def test_load_sku_map_skips_invalid_entries_when_not_strict(tmp_path): + path = tmp_path / "sku_map.json" + path.write_text( + '{"entries": [' + '{"gpu_name": "bad", "power_cap_w": 1},' + '{"gpu_name": "NVIDIA Test GPU", "power_cap_w": 350, ' + '"locked_sm_clock_mhz": 1095, "stem": "TESTSKU"}' + "]}" + ) + assert load_sku_map(path) == {("NVIDIA Test GPU", 350, 1095): "TESTSKU"} + + +def test_markdown_summary_reports_delta_stats(tmp_path): + index, measurements, current = _updates( + tmp_path, _current_payload(cpp_us=120.0, python_us=115.0) + ) + result = compare_updates(measurements, index=index, sku="TESTSKU") + report = format_markdown(result, "TESTSKU", "operators", current, Thresholds()) + assert "all-rows |Delta|:" in report + assert "Regressions (1)" in report diff --git a/bench/tests/test_compare_wheels.py b/bench/tests/test_compare_wheels.py new file mode 100644 index 000000000..30a294414 --- /dev/null +++ b/bench/tests/test_compare_wheels.py @@ -0,0 +1,590 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""CPU-only tests for the two-wheel Python benchmark tool.""" + +from __future__ import annotations + +import json +import os +import shutil +import subprocess +import sys +import zipfile +from argparse import Namespace +from pathlib import Path + +import pandas as pd +import pytest + +import compare_wheels +from _internal.baselines import load_config_index +from _internal.quality import DEFAULT_BENCHMARK_QUALITY + + +def _write_wheel(path: Path, *, name="cvcuda-cu12", version="1.2.3") -> Path: + dist_info = name.replace("-", "_") + f"-{version}.dist-info" + with zipfile.ZipFile(path, "w") as wheel: + wheel.writestr( + f"{dist_info}/METADATA", + f"Metadata-Version: 2.1\nName: {name}\nVersion: {version}\n", + ) + return path + + +def _operator_document(*, dtypes=None): + return { + "benchmark": "resize", + "configs": { + "resize_basic": { + "tier": "basic", + "dtypes": dtypes or ["uint8"], + "string_axes": {"shape": ["1x2x3"], "inputKind": ["Tensor"]}, + }, + "resize_advanced": { + "tier": "advanced", + "dtypes": ["float32"], + "string_axes": {"shape": ["1x4x5"], "inputKind": ["Tensor"]}, + }, + }, + } + + +def _write_config(root: Path, *, dtypes=None): + operators = root / "config" / "operators" + operators.mkdir(parents=True) + (operators / "resize.json").write_text( + json.dumps(_operator_document(dtypes=dtypes)) + ) + return load_config_index(operators) + + +def _row( + *, + config_key="resize_basic", + dtype="uint8", + shape="1x2x3", + time=100.0, + noise=1.0, + status="PASS", + device="NVIDIA Test GPU", + power=350, + clock=1095, +): + return { + "Benchmark": "resize", + "config_key": config_key, + "Language": "python", + "InOutDataType": dtype, + "shape": shape, + "inputKind": "Tensor", + "GPU Time (µs)": time, + "GPU Noise (%)": noise, + "Status": status, + "Device Name": device, + "Power Cap (W)": power, + "Locked SM Clock (MHz)": clock, + "vBIOS Version": "1.0", + } + + +def _write_run(path: Path, rows) -> Path: + pd.DataFrame(rows).to_csv(path, index=False) + return path + + +def _args(**overrides): + values = { + "operator": None, + "config_key": None, + "tier": "basic", + "max_noise_pct": DEFAULT_BENCHMARK_QUALITY.max_noise_pct, + "warmup_cap": None, + "bench_min_time": None, + "bench_max_noise": None, + } + values.update(overrides) + return Namespace(**values) + + +def _expected(index, *config_keys): + return compare_wheels.expected_case_operators(index, set(config_keys)) + + +def _stage_source(root: Path, *, flat: bool) -> Path: + root.mkdir() + for filename in compare_wheels.RUNNER_FILES: + (root / filename).write_text(f"# {filename}\n") + internal = root / "_internal" + internal.mkdir() + for filename in compare_wheels.INTERNAL_FILES: + (internal / filename).write_text(f"# {filename}\n") + config = root / "config" + (config / "operators").mkdir(parents=True) + (config / "bench_params.json").write_text( + json.dumps( + { + "operators": { + "resize": { + "config": "operators/resize.json", + "python": "bench_resize.py", + } + } + } + ) + ) + (config / "operators" / "resize.json").write_text(json.dumps(_operator_document())) + for filename in ("sku_map.json", "load_config.py", "axis_order.py"): + (config / filename).write_text( + "{}\n" if filename.endswith(".json") else "# helper\n" + ) + python_root = root if flat else root / "python" + ops_root = root if flat else root / "python" / "ops" + ops_root.mkdir(parents=True, exist_ok=True) + (python_root / "python_bench_utils.py").write_text("# utils\n") + (ops_root / "bench_resize.py").write_text("# resize\n") + return root + + +def test_read_wheel_metadata_and_reject_non_cvcuda(tmp_path): + metadata = compare_wheels.read_wheel_metadata(_write_wheel(tmp_path / "cvcuda.whl")) + assert (metadata.name, metadata.version, len(metadata.sha256)) == ( + "cvcuda-cu12", + "1.2.3", + 64, + ) + + with pytest.raises(compare_wheels.WheelComparisonError, match="not a CV-CUDA"): + compare_wheels.read_wheel_metadata( + _write_wheel(tmp_path / "other.whl", name="other") + ) + + +@pytest.mark.parametrize("flat", [False, True]) +def test_stage_benchmark_harness_supports_source_and_installed_layout(tmp_path, flat): + source = _stage_source(tmp_path / "source", flat=flat) + destination = tmp_path / "work" / "harness-bin" + + compare_wheels.stage_benchmark_harness(source, destination) + + assert (destination / "run_bench.py").is_file() + for filename in compare_wheels.INTERNAL_FILES: + assert (destination / "_internal" / filename).is_file() + assert (destination / "python_bench_utils.py").is_file() + assert (destination / "bench_resize.py").is_file() + assert (destination / "config" / "operators" / "resize.json").is_file() + + +def test_flattened_build_layout_can_start_clis(tmp_path): + destination = tmp_path / "bin" + compare_wheels.stage_benchmark_harness(compare_wheels.BENCH_DIR, destination) + shutil.copy2(compare_wheels.__file__, destination / "compare_wheels.py") + env = os.environ.copy() + env.pop("PYTHONPATH", None) + + for script, expected in ( + ("run_bench.py", "--lang {cpp,python,both}"), + ("compare_wheels.py", "REFERENCE_WHEEL"), + ): + result = subprocess.run( + [sys.executable, str(destination / script), "--help"], + cwd=tmp_path, + env=env, + capture_output=True, + text=True, + check=False, + ) + + assert result.returncode == 0, result.stderr + assert expected in result.stdout + + +def test_create_wheel_environment_uses_exact_wheel_in_system_site_venv( + tmp_path, monkeypatch +): + wheel = compare_wheels.read_wheel_metadata(_write_wheel(tmp_path / "cvcuda.whl")) + commands = [] + + def fake_run(command, _log_path, **_kwargs): + commands.append(command) + return 0 + + monkeypatch.setattr(compare_wheels, "_run_logged", fake_run) + python = compare_wheels.create_wheel_environment( + Path("/usr/bin/python3"), tmp_path / "env", wheel, tmp_path / "install.log" + ) + + assert python == tmp_path / "env" / "bin" / "python" + assert commands[0][2:] == ["venv", "--system-site-packages", str(tmp_path / "env")] + assert {"--force-reinstall", "--no-deps"} <= set(commands[1]) + assert commands[1][-1] == str(wheel.path) + + +def test_build_runner_command_reuses_runner_defaults(tmp_path): + args = _args(config_key="resize_basic", warmup_cap=3, bench_min_time=0.01) + command = compare_wheels.build_runner_command( + args, Path("/env/python"), tmp_path / "harness", tmp_path / "out.csv" + ) + + assert command[:2] == ["/env/python", str(tmp_path / "harness" / "run_bench.py")] + assert command[-1] == str(tmp_path / "harness") + assert command[command.index("--config-key") + 1] == "resize_basic" + assert "--max-retries" not in command + assert "--skip-validation" not in command + + +def test_compare_flags_regression_but_not_improvement(tmp_path): + index = _write_config(tmp_path) + expected = _expected(index, "resize_basic") + baseline = compare_wheels.load_measurements( + _write_run(tmp_path / "baseline.csv", [_row(time=100)]), index + ) + + regression = compare_wheels.compare_measurements( + baseline, + compare_wheels.load_measurements( + _write_run(tmp_path / "slow.csv", [_row(time=120)]), index + ), + expected_case_operators=expected, + threshold=0.10, + ) + assert regression.regressions.iloc[0]["candidate_delta_pct"] == pytest.approx(20) + assert regression.failed + + improvement = compare_wheels.compare_measurements( + baseline, + compare_wheels.load_measurements( + _write_run(tmp_path / "fast.csv", [_row(time=80)]), index + ), + expected_case_operators=expected, + threshold=0.10, + ) + assert len(improvement.improvements) == 1 + assert improvement.mean_operator_speedup == pytest.approx(1.25) + assert not improvement.failed + + +def test_speedup_is_mean_over_operators_of_per_operator_geomeans(): + table = pd.DataFrame( + [ + { + "Benchmark": "many_cases", + "candidate_speedup": speedup, + "comparison_status": "neutral", + } + for speedup in (16.0, 16.0, 16.0, 16.0) + ] + + [ + { + "Benchmark": "one_case", + "candidate_speedup": 1.0, + "comparison_status": "neutral", + } + ] + ) + comparison = compare_wheels.Comparison( + table=table, + expected_case_operators={}, + missing_baseline_cases=(), + missing_candidate_cases=(), + baseline_fingerprint=None, + candidate_fingerprint=None, + baseline_clocks=(), + candidate_clocks=(), + baseline_row_count=5, + candidate_row_count=5, + threshold=0.10, + ) + + assert comparison.operator_geomean_speedups.to_dict() == { + "many_cases": pytest.approx(16.0), + "one_case": pytest.approx(1.0), + } + assert comparison.mean_operator_speedup == pytest.approx(8.5) + + +def test_compatibility_distinguishes_full_partial_and_zero_overlap(): + table = pd.DataFrame( + [ + ("full", "full-1", "neutral", 1.0), + ("full", "full-2", "improvement", 2.0), + ("partial", "partial-1", "neutral", 1.0), + ("partial", "partial-2", "only_in_candidate", None), + ("absent", "absent-1", "only_in_candidate", None), + ("failed", "failed-1", "excluded", 1.0), + ], + columns=( + "Benchmark", + "case_key", + "comparison_status", + "candidate_speedup", + ), + ) + comparison = compare_wheels.Comparison( + table=table, + expected_case_operators={ + row.case_key: row.Benchmark for row in table.itertuples(index=False) + }, + missing_baseline_cases=("absent-1", "partial-2"), + missing_candidate_cases=(), + baseline_fingerprint=None, + candidate_fingerprint=None, + baseline_clocks=(), + candidate_clocks=(), + baseline_row_count=4, + candidate_row_count=6, + threshold=0.10, + ) + + assert comparison.operator_compatibility == { + "absent": "incompatible_or_not_present", + "failed": "fully_compatible", + "full": "fully_compatible", + "partial": "partially_compatible", + } + items = dict(compare_wheels._compatibility_items(comparison)) + assert items["operators with any incompatible or missing configurations"] == "2/4" + assert items["operators present in both"] == "3/4" + assert items["operators with no shared configurations"] == "1/4" + assert items["operators partially compatible"] == "1/4" + assert items["operators fully compatible"] == "2/4" + assert ( + items["expanded benchmark configurations incompatible or not present"] == "2/6" + ) + assert items["expanded benchmark configurations present in both"] == "4/6" + assert items["present in both but non-PASS"] == "1" + + +def test_clock_drift_is_diagnostic_but_gpu_mismatch_is_rejected(tmp_path): + index = _write_config(tmp_path) + expected = _expected(index, "resize_basic") + baseline = compare_wheels.load_measurements( + _write_run(tmp_path / "baseline.csv", [_row(clock=1095)]), index + ) + drifted = compare_wheels.load_measurements( + _write_run(tmp_path / "drifted.csv", [_row(clock=1110)]), index + ) + comparison = compare_wheels.compare_measurements( + baseline, drifted, expected_case_operators=expected, threshold=0.10 + ) + assert comparison.baseline_clocks != comparison.candidate_clocks + assert not comparison.failed + + different_gpu = compare_wheels.load_measurements( + _write_run(tmp_path / "other.csv", [_row(device="GPU B")]), index + ) + with pytest.raises( + compare_wheels.WheelComparisonError, match="fingerprints differ" + ): + compare_wheels.compare_measurements( + baseline, + different_gpu, + expected_case_operators=expected, + threshold=0.10, + ) + + +def test_comparison_reports_nonpass_asymmetric_and_both_missing_cases(tmp_path): + index = _write_config(tmp_path, dtypes=["uint8", "int16"]) + expected = _expected(index, "resize_basic") + baseline = compare_wheels.load_measurements( + _write_run(tmp_path / "baseline.csv", [_row(status="FAIL (noise)")]), index + ) + candidate = compare_wheels.load_measurements( + _write_run(tmp_path / "candidate.csv", [_row(status="FAIL (noise)")]), index + ) + + comparison = compare_wheels.compare_measurements( + baseline, candidate, expected_case_operators=expected, threshold=0.10 + ) + + assert len(comparison.rows("excluded")) == 1 + assert len(comparison.rows("missing_from_both")) == 1 + assert len(comparison.missing_baseline_cases) == 1 + assert len(comparison.missing_candidate_cases) == 1 + assert comparison.failed + + +def test_load_measurements_rejects_duplicate_cases(tmp_path): + index = _write_config(tmp_path) + path = _write_run(tmp_path / "run.csv", [_row(), _row(time=101)]) + with pytest.raises(compare_wheels.WheelComparisonError, match="duplicate"): + compare_wheels.load_measurements(path, index) + + +def test_selectors_reuse_shared_parsers(tmp_path): + index = _write_config(tmp_path) + assert compare_wheels.selected_config_keys(index, _args()) == {"resize_basic"} + assert compare_wheels.selected_config_keys( + index, _args(tier="basic,advanced", operator="resize") + ) == {"resize_basic", "resize_advanced"} + with pytest.raises(ValueError, match="duplicate"): + compare_wheels.selected_config_keys( + index, _args(config_key="resize_basic,resize_basic") + ) + + +def test_report_and_comparison_csv_include_all_failure_kinds(tmp_path): + index = _write_config(tmp_path, dtypes=["uint8", "int16"]) + baseline = compare_wheels.load_measurements( + _write_run(tmp_path / "baseline.csv", [_row(time=100)]), index + ) + candidate = compare_wheels.load_measurements( + _write_run(tmp_path / "candidate.csv", [_row(time=120)]), index + ) + comparison = compare_wheels.compare_measurements( + baseline, + candidate, + expected_case_operators=_expected(index, "resize_basic"), + threshold=0.10, + ) + baseline_wheel = compare_wheels.read_wheel_metadata( + _write_wheel(tmp_path / "baseline.whl", version="1.0") + ) + candidate_wheel = compare_wheels.read_wheel_metadata( + _write_wheel(tmp_path / "candidate.whl", version="2.0") + ) + + output_csv = tmp_path / "comparison.csv" + comparison.table.to_csv(output_csv, index=False) + report = compare_wheels.format_report( + baseline_wheel, + candidate_wheel, + comparison, + baseline_rc=0, + candidate_rc=0, + output_dir=tmp_path, + ) + + written = pd.read_csv(output_csv) + assert set(written["comparison_status"]) == {"regression", "missing_from_both"} + assert ( + "overall candidate speedup vs baseline: **0.8333x** " + "(mean of per-operator geomeans across matched benchmarks)" in report + ) + assert "valid compatible configurations: 1/2" in report + assert "valid compatible operators: 1/1" in report + assert report.index("valid compatible operators") < report.index( + "valid compatible configurations" + ) + assert "## Compatibility" in report + assert "operators partially compatible: 1/1" in report + assert ( + "expanded benchmark configurations incompatible or not present: 1/2" in report + ) + assert "## Top 5 operator improvements\n\n_none_" in report + assert "| resize | 0.8333x | +20.00% | 1 |" in report + assert "reference `run_bench.py`: **PASS** (exit code 0)" in report + assert "candidate `run_bench.py`: **PASS** (exit code 0)" in report + artifact_lines = ( + f"- artifacts: `{tmp_path}`\n" + " - reference benchmark CSV: `baseline/bench_output.csv`\n" + " - candidate benchmark CSV: `candidate/bench_output.csv`\n" + " - comparison CSV: `comparison.csv`" + ) + assert artifact_lines in report + summary = report.split("## Summary\n\n", 1)[1] + assert summary.startswith("- overall candidate speedup vs baseline:") + assert report.index("## Compatibility") < report.index( + "## Top 5 operator improvements" + ) + assert "## Result" not in report + assert "comparison status: **FAIL**" in report + assert ( + "1 configuration regression over threshold; " + "1 configuration missing from both" in report + ) + + +def _mock_run_dependencies( + monkeypatch, *, baseline_row=True, baseline_rc=0, candidate_rc=0 +): + calls = [] + + def fake_stage(_source, destination): + _write_config(destination) + + def fake_run(_args, _python, _harness, output_csv, _log): + side = output_csv.parent.name + calls.append(side) + if side == "candidate" or baseline_row: + _write_run(output_csv, [_row(time=105 if side == "candidate" else 100)]) + return baseline_rc if side == "baseline" else candidate_rc + + monkeypatch.setattr(compare_wheels, "stage_benchmark_harness", fake_stage) + monkeypatch.setattr( + compare_wheels, + "create_wheel_environment", + lambda _base, env, _wheel, _log: env / "bin" / "python", + ) + monkeypatch.setattr(compare_wheels, "probe_wheel_environment", lambda *_args: None) + monkeypatch.setattr(compare_wheels, "run_benchmark", fake_run) + return calls + + +def _run_args(tmp_path, output_dir): + baseline = _write_wheel(tmp_path / "baseline.whl", version="1.0") + candidate = _write_wheel(tmp_path / "candidate.whl", version="2.0") + return compare_wheels.parse_args( + [ + str(baseline), + str(candidate), + "--config-key", + "resize_basic", + "--output-dir", + str(output_dir), + "--python", + sys.executable, + ] + ) + + +def test_run_passes_writes_artifacts_and_cleans_workdir(tmp_path, monkeypatch): + calls = _mock_run_dependencies(monkeypatch) + output_dir = tmp_path / "result" + + assert compare_wheels.run(_run_args(tmp_path, output_dir)) == 0 + assert calls == ["baseline", "candidate"] + assert (output_dir / "summary.md").is_file() + assert (output_dir / "comparison.csv").is_file() + assert (output_dir / "baseline" / "bench_output.csv").is_file() + assert (output_dir / "candidate" / "bench_output.csv").is_file() + assert not list(output_dir.glob("work-*")) + + +def test_run_reports_missing_csv_after_running_both_sides(tmp_path, monkeypatch): + calls = _mock_run_dependencies(monkeypatch, baseline_row=False, baseline_rc=1) + output_dir = tmp_path / "result" + + assert compare_wheels.run(_run_args(tmp_path, output_dir)) == 1 + assert calls == ["baseline", "candidate"] + summary = (output_dir / "summary.md").read_text() + assert "reference `run_bench.py`: **FAIL** (exit code 1)" in summary + assert "candidate `run_bench.py`: **PASS** (exit code 0)" in summary + assert "comparison status: **FAIL**" in summary + comparison = pd.read_csv(output_dir / "comparison.csv") + assert set(comparison["comparison_status"]) == {"only_in_candidate"} + + +def test_main_returns_two_for_output_path_file(tmp_path, capsys): + output_path = tmp_path / "not-a-directory" + output_path.write_text("occupied") + baseline = _write_wheel(tmp_path / "baseline.whl") + candidate = _write_wheel(tmp_path / "candidate.whl") + + assert ( + compare_wheels.main( + [str(baseline), str(candidate), "--output-dir", str(output_path)] + ) + == 2 + ) + captured = capsys.readouterr() + assert "output path is not a directory" in captured.err + assert "Traceback" not in captured.err + + +def test_parse_args_uses_shared_noise_default(tmp_path): + args = compare_wheels.parse_args( + [str(tmp_path / "baseline.whl"), str(tmp_path / "candidate.whl")] + ) + assert args.max_noise_pct == DEFAULT_BENCHMARK_QUALITY.max_noise_pct diff --git a/bench/tests/test_histogrameq_benchmark_isolation.py b/bench/tests/test_histogrameq_benchmark_isolation.py new file mode 100644 index 000000000..9ae038da5 --- /dev/null +++ b/bench/tests/test_histogrameq_benchmark_isolation.py @@ -0,0 +1,56 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Regression coverage for HistogramEq benchmark process isolation.""" + +from __future__ import annotations + +from collections import Counter +import json +from pathlib import Path + + +BENCH_DIR = Path(__file__).resolve().parent.parent + + +def test_histogrameq_container_states_use_separate_config_keys(): + config_path = BENCH_DIR / "config/operators/histogrameq.json" + configs = json.loads(config_path.read_text())["configs"] + + combined = { + key: entry["string_axes"]["inputKind"] + for key, entry in configs.items() + if len(entry["string_axes"]["inputKind"]) != 1 + } + + assert combined == {} + + entries = list(configs.values()) + + def signature(entry): + axes = tuple( + (name, tuple(values)) + for name, values in entry["string_axes"].items() + if name != "inputKind" + ) + return ( + entry["tier"], + tuple(entry["dtypes"]), + axes, + entry.get("warmup_iterations"), + ) + + tensor_signatures = Counter( + signature(entry) + for entry in entries + if entry["string_axes"]["inputKind"] == ["Tensor"] + ) + varshape_signatures = Counter( + signature(entry) + for entry in entries + if entry["string_axes"]["inputKind"] == ["VarShape"] + ) + + assert tensor_signatures + assert varshape_signatures + assert varshape_signatures - tensor_signatures == {} diff --git a/bench/tests/test_normalize_benchmark.py b/bench/tests/test_normalize_benchmark.py new file mode 100644 index 000000000..eb4d07ebb --- /dev/null +++ b/bench/tests/test_normalize_benchmark.py @@ -0,0 +1,77 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Build-free regression coverage for the Normalize benchmark.""" + +import importlib.util +import sys +import types +from pathlib import Path + + +BENCH_DIR = Path(__file__).resolve().parent.parent + + +def _load_normalize_benchmark(monkeypatch): + fake_cvcuda = types.ModuleType("cvcuda") + fake_cvcuda.NormalizeFlags = types.SimpleNamespace(SCALE_IS_STDDEV=object()) + monkeypatch.setitem(sys.modules, "cvcuda", fake_cvcuda) + + fake_utils = types.ModuleType("python_bench_utils") + fake_utils.get_input_kind = lambda _name: "Tensor" + fake_utils.parse_shape = lambda _shape: (2, 3, 4) + fake_utils.get_dtype = lambda _dtype: object() + fake_utils.get_dtype_size = lambda _dtype: 1 + fake_utils.get_num_channels = lambda _dtype: 3 + fake_utils.get_format_from_dtype = lambda *_args, **_kwargs: object() + + def unexpected_allocation(*_args, **_kwargs): + raise AssertionError("NCHW_FAKE TensorScalar must skip before allocation") + + fake_utils.create_tensor = unexpected_allocation + fake_utils.create_image_batch_varshape = unexpected_allocation + fake_utils.create_stream_cache = lambda: object() + fake_utils.run_benchmark = lambda *_args, **_kwargs: None + monkeypatch.setitem(sys.modules, "python_bench_utils", fake_utils) + + spec = importlib.util.spec_from_file_location( + "bench_normalize_test", + BENCH_DIR / "python" / "ops" / "bench_normalize.py", + ) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_tensor_scalar_fake_planar_skips_like_cpp_benchmark(monkeypatch): + bench_normalize = _load_normalize_benchmark(monkeypatch) + + class State: + skipped = None + + def get_string(self, name): + return { + "shape": "2x3x4", + "InOutDataType": "U8x3", + "layout": "NCHW_FAKE", + "inputKind": "TensorScalar", + }[name] + + def get_device(self): + return 0 + + def skip(self, reason): + self.skipped = reason + + def add_global_memory_reads(self, _bytes): + pass + + def add_global_memory_writes(self, _bytes): + pass + + state = State() + + assert bench_normalize.normalize(state) is None + assert state.skipped == ( + "Scalar-parameter normalize benchmark supports only native NHWC and NCHW tensors" + ) diff --git a/bench/tests/test_python_bench_fill.py b/bench/tests/test_python_bench_fill.py new file mode 100644 index 000000000..687da10ac --- /dev/null +++ b/bench/tests/test_python_bench_fill.py @@ -0,0 +1,136 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Regression coverage for Python benchmark data fills.""" + +import importlib.util +import sys +import types +from contextlib import nullcontext +from pathlib import Path + +import pytest + + +BENCH_DIR = Path(__file__).resolve().parent.parent + + +def _load_python_bench_utils(monkeypatch): + fake_cvcuda = types.ModuleType("cvcuda") + fake_cvcuda.Tensor = object + monkeypatch.setitem(sys.modules, "cvcuda", fake_cvcuda) + + fake_cupy = types.ModuleType("cupy") + fake_cupy.cuda = types.SimpleNamespace(set_allocator=lambda _: None) + monkeypatch.setitem(sys.modules, "cupy", fake_cupy) + + spec = importlib.util.spec_from_file_location( + "python_bench_utils_fill_test", + BENCH_DIR / "python" / "python_bench_utils.py", + ) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_checkerboard_fill_writes_strided_view_without_ravel(monkeypatch): + bench_utils = _load_python_bench_utils(monkeypatch) + launches = [] + + class StridedArray: + shape = (3, 11, 17) + strides = (1056, 96, 4) + itemsize = 4 + size = 3 * 11 * 17 + dtype = "float32" + + def ravel(self): + raise AssertionError("pitched image fill must not materialize a dense copy") + + def kernel(grid, block, args): + launches.append((grid, block, args)) + + monkeypatch.setattr(bench_utils, "_checkerboard_kernel_for", lambda _dtype: kernel) + + array = StridedArray() + bench_utils._checkerboard_fill(array) + + assert len(launches) == 1 + grid, block, args = launches[0] + assert grid == (3,) + assert block == (256,) + assert args[0] is array + assert tuple(int(value) for value in args[6:10]) == (264, 24, 1, 0) + + +def test_checkerboard_image_fill_uses_pixel_parity_for_packed_channels(monkeypatch): + bench_utils = _load_python_bench_utils(monkeypatch) + fills = [] + + class PackedImage: + shape = (11, 17, 3) + + def __getitem__(self, key): + return key + + monkeypatch.setattr(bench_utils, "_checkerboard_fill", fills.append) + + bench_utils._checkerboard_fill_image(PackedImage()) + + assert fills == [ + (Ellipsis, 0), + (Ellipsis, 1), + (Ellipsis, 2), + ] + + +def test_preallocated_varshape_batch_rejects_nonempty_without_mutation(monkeypatch): + bench_utils = _load_python_bench_utils(monkeypatch) + + class FakeBatch: + capacity = 2 + + def __init__(self): + self.images = [object()] + + def __len__(self): + return len(self.images) + + def pushback(self, image): + if len(self.images) == self.capacity: + raise RuntimeError("capacity exceeded") + self.images.append(image) + + type_names = ("U8", "U16", "U32", "S8", "S16", "S32", "F32", "F64") + fake_type = types.SimpleNamespace(**{name: object() for name in type_names}) + bench_utils.cvcuda.Type = fake_type + bench_utils.cvcuda.Image = lambda _size, _format: types.SimpleNamespace( + cuda=lambda: object() + ) + fake_cupy = sys.modules["cupy"] + for name in ( + "uint8", + "uint16", + "uint32", + "int8", + "int16", + "int32", + "float32", + "float64", + ): + setattr(fake_cupy, name, name) + fake_cupy.cuda.Device = lambda _device: nullcontext() + fake_cupy.asarray = lambda _buffer: types.SimpleNamespace(fill=lambda _value: None) + + batch = FakeBatch() + with pytest.raises(ValueError, match="must be empty"): + bench_utils.create_image_batch_varshape( + (2, 1, 1), + 0, + types.SimpleNamespace(planes=1), + dtype=fake_type.U8, + fill_mode=0, + batch=batch, + ) + + assert len(batch) == 1 diff --git a/bench/tests/test_python_bench_state_lifetime.py b/bench/tests/test_python_bench_state_lifetime.py new file mode 100644 index 000000000..6dff22b89 --- /dev/null +++ b/bench/tests/test_python_bench_state_lifetime.py @@ -0,0 +1,112 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Regression coverage for Python benchmark state resource lifetime.""" + +from __future__ import annotations + +import gc +import importlib.util +import sys +import types +import weakref +from pathlib import Path + + +BENCH_DIR = Path(__file__).resolve().parent.parent + + +def _load_python_bench_utils(monkeypatch): + fake_cvcuda = types.ModuleType("cvcuda") + fake_cvcuda.Tensor = object + cache_clears = [] + fake_cvcuda.clear_cache = lambda: cache_clears.append(None) + monkeypatch.setitem(sys.modules, "cvcuda", fake_cvcuda) + + fake_cupy = types.ModuleType("cupy") + fake_cupy.cuda = types.SimpleNamespace(set_allocator=lambda _: None) + monkeypatch.setitem(sys.modules, "cupy", fake_cupy) + + spec = importlib.util.spec_from_file_location( + "python_bench_utils_lifetime_test", + BENCH_DIR / "python" / "python_bench_utils.py", + ) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module, cache_clears + + +def test_run_benchmark_releases_cyclic_resources_before_next_state(monkeypatch): + bench_utils, cache_clears = _load_python_bench_utils(monkeypatch) + + callbacks = [] + fake_bench = types.ModuleType("cuda.bench") + + class Registration: + def add_string_axis(self, _name, _values): + return self + + def register(callback): + callbacks.append(callback) + return Registration() + + class State: + def __init__(self, index): + self.index = index + + def exec(self, callback, sync): + assert sync is True + callback(None) + + def run_all_benchmarks(_args): + for index in range(2): + callbacks[0](State(index)) + + fake_bench.register = register + fake_bench.run_all_benchmarks = run_all_benchmarks + fake_cuda = types.ModuleType("cuda") + fake_cuda.__path__ = [] + fake_cuda.bench = fake_bench + monkeypatch.setitem(sys.modules, "cuda", fake_cuda) + monkeypatch.setitem(sys.modules, "cuda.bench", fake_bench) + + config = types.SimpleNamespace(warmup_iterations=0, dtypes=[]) + monkeypatch.setattr( + bench_utils, "load_operator_config", lambda _operator: (config, []) + ) + monkeypatch.setattr( + bench_utils, "register_axes_from_config", lambda _bench, _config: None + ) + + resources = [] + + class CyclicResource: + def __init__(self): + self.cycle = self + + def benchmark(state): + if state.index: + assert resources[-1]() is None + resource = CyclicResource() + resources.append(weakref.ref(resource)) + + def run(_launch): + return resource + + return run + + was_enabled = gc.isenabled() + gc.collect() + try: + bench_utils.run_benchmark("test", benchmark) + finally: + gc.collect() + if was_enabled: + gc.enable() + else: + gc.disable() + + assert gc.isenabled() is was_enabled + assert len(resources) == 2 + assert all(resource() is None for resource in resources) + assert len(cache_clears) == 4 diff --git a/bench/tests/test_run_bench_config_key.py b/bench/tests/test_run_bench_config_key.py new file mode 100644 index 000000000..6849121b7 --- /dev/null +++ b/bench/tests/test_run_bench_config_key.py @@ -0,0 +1,1227 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""Unit tests for benchmark tier configs and exact --config-key discovery.""" + +from __future__ import annotations + +import json +import os +import sys +from types import SimpleNamespace + +import pandas as pd +import pytest + +import run_bench +from _internal.baselines import ( + BaselineError, + baseline_payload_from_dataframe, + expected_case_keys_for_entry, + fake_planar_pairing_issues, + load_config_index, + parse_case_key, +) +from _internal.quality import DEFAULT_BENCHMARK_QUALITY +from config.load_config import ( + generate_axis_args, + get_configs_for_benchmark, + load_bench_config, + load_bench_manifest, + parse_config_key_arg, + parse_operator_arg, + parse_tier_arg, +) + + +class DummyRunner(run_bench.BenchmarkRunner): + def __init__( + self, + bench_folder, + config, + config_keys, + language, + operators=None, + tiers=None, + ): + super().__init__( + "bench_", + str(bench_folder), + operators=operators, + language=language, + config_keys=config_keys, + tiers=tiers, + ) + self.config = config + self.operator_manifest = { + entry["benchmark"]: { + "config": f"operators/{entry['benchmark']}.json", + "cpp": f"bench_{entry['benchmark']}", + "python": f"bench_{entry['benchmark']}.py", + } + for entry in config.values() + if isinstance(entry, dict) and "benchmark" in entry + } + + from config.load_config import ( # noqa: PLC0415 + get_operator_from_benchmark_name, + get_configs_for_benchmark, + ) + + self.get_operator_from_benchmark_name = get_operator_from_benchmark_name + self.get_configs_for_benchmark = get_configs_for_benchmark + + def build_command(self, benchmark_path, extra_args, output_file, config_key=None): + return [] + + +def _touch(path): + path.write_text("") + + +def _write_json(path, data): + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(data, indent=4) + "\n") + + +def _write_baseline_config_tree(root): + _write_json( + root / "sku_map.json", + { + "entries": [ + { + "gpu_name": "NVIDIA Test GPU", + "power_cap_w": 250, + "locked_sm_clock_mhz": 1095, + "stem": "A100_PCIE_40GB_250W_1095MHz", + } + ] + }, + ) + _write_json( + root / "operators" / "resize.json", + { + "benchmark": "resize", + "configs": { + "resize_contract_area_tensor_uchar3_basic": { + "tier": "basic", + "dtypes": ["uchar3"], + "string_axes": { + "shape": ["32x1080x1920"], + "kernelSize": ["5x5"], + "resizeType": ["CONTRACT"], + "interpolation": ["AREA"], + "layout": ["NHWC"], + "inputKind": ["Tensor"], + }, + } + }, + }, + ) + + +def _baseline_row(language="cpp", gpu_us=525.7525, noise_us=1.2, bwutil=0.42): + return { + "Benchmark": "resize", + "tier": "basic", + "config_key": "resize_contract_area_tensor_uchar3_basic", + "Language": language, + "GPU Time (µs)": gpu_us, + "GPU Noise (µs)": noise_us, + "BWUtil": bwutil, + "Device Name": "NVIDIA Test GPU", + "Power Cap (W)": 250, + "Locked SM Clock (MHz)": 1095, + "InOutDataType": "uchar3", + "shape": "32x1080x1920", + "resizeType": "CONTRACT", + "interpolation": "AREA", + "layout": "NHWC", + "inputKind": "Tensor", + } + + +def test_advanced_tier_is_not_discovered_by_basic_tier(): + config = { + "resize_basic": { + "benchmark": "resize", + "tier": "basic", + }, + "resize_advanced": { + "benchmark": "resize", + "tier": "advanced", + }, + } + + assert get_configs_for_benchmark("resize", config, tiers={"basic"}) == [ + "resize_basic" + ] + assert get_configs_for_benchmark("resize", config, tiers={"advanced"}) == [ + "resize_advanced" + ] + assert get_configs_for_benchmark("resize", config, tiers={"basic", "advanced"}) == [ + "resize_basic", + "resize_advanced", + ] + + +def test_parse_tier_arg_accepts_basic_and_advanced(): + assert parse_tier_arg("basic") == {"basic"} + assert parse_tier_arg("advanced") == {"advanced"} + assert parse_tier_arg("basic, advanced") == {"basic", "advanced"} + + with pytest.raises(ValueError, match="unknown tier"): + parse_tier_arg("ci") + + +def test_parse_config_key_arg_single_and_comma_separated(): + assert parse_config_key_arg("resize_advanced") == ["resize_advanced"] + assert parse_config_key_arg("resize_advanced, gaussian_advanced ") == [ + "resize_advanced", + "gaussian_advanced", + ] + + +def test_parse_config_key_arg_rejects_empty_and_duplicate_values(): + with pytest.raises(ValueError, match="empty"): + parse_config_key_arg(" , ") + + with pytest.raises(ValueError, match="duplicate"): + parse_config_key_arg("resize_advanced,resize_advanced") + + +def test_parse_operator_arg_accepts_ordered_exact_names(): + assert parse_operator_arg("resize") == ["resize"] + assert parse_operator_arg("resize, gaussian flip") == [ + "resize", + "gaussian", + "flip", + ] + + +def test_parse_operator_arg_rejects_empty_and_duplicate_values(): + with pytest.raises(ValueError, match="empty"): + parse_operator_arg(" , ") + + with pytest.raises(ValueError, match="duplicate"): + parse_operator_arg("resize,resize") + + +def test_load_bench_config_expands_split_manifest(tmp_path): + operators = tmp_path / "operators" + operators.mkdir() + _write_json( + operators / "resize.json", + { + "resize_basic": { + "benchmark": "resize", + "tier": "basic", + }, + }, + ) + _write_json( + operators / "gaussian.json", + { + "gaussian_advanced": { + "benchmark": "gaussian", + "tier": "advanced", + }, + }, + ) + _write_json( + tmp_path / "bench_params.json", + {"include": ["operators/resize.json", "operators/gaussian.json"]}, + ) + + assert list(load_bench_config(str(tmp_path / "bench_params.json"))) == [ + "resize_basic", + "gaussian_advanced", + ] + assert list(load_bench_config(str(tmp_path))) == [ + "resize_basic", + "gaussian_advanced", + ] + + +def test_load_bench_config_expands_operator_manifest(tmp_path): + operators = tmp_path / "operators" + operators.mkdir() + _write_json( + operators / "resize.json", + { + "resize_basic": { + "benchmark": "resize", + "tier": "basic", + }, + }, + ) + _write_json( + operators / "gaussian.json", + { + "gaussian_advanced": { + "benchmark": "gaussian", + "tier": "advanced", + }, + }, + ) + _write_json( + tmp_path / "bench_params.json", + { + "operators": { + "resize": { + "config": "operators/resize.json", + "cpp": "bench_resize", + "python": "bench_resize.py", + }, + "gaussian": { + "config": "operators/gaussian.json", + "cpp": "bench_gaussian", + "python": "bench_gaussian.py", + }, + } + }, + ) + + assert list(load_bench_config(str(tmp_path / "bench_params.json"))) == [ + "resize_basic", + "gaussian_advanced", + ] + assert list(load_bench_manifest(str(tmp_path / "bench_params.json"))) == [ + "resize", + "gaussian", + ] + + +def test_load_bench_config_rejects_duplicate_split_keys(tmp_path): + operators = tmp_path / "operators" + operators.mkdir() + entry = { + "resize_basic": { + "benchmark": "resize", + "tier": "basic", + }, + } + _write_json(operators / "resize.json", entry) + _write_json(operators / "duplicate.json", entry) + _write_json( + tmp_path / "bench_params.json", + {"include": ["operators/resize.json", "operators/duplicate.json"]}, + ) + + with pytest.raises(ValueError, match="Duplicate config key"): + load_bench_config(str(tmp_path / "bench_params.json")) + + +def _entry_string_axis_values(entry, axis): + return entry.get("string_axes", {}).get(axis, []) + + +def _entry_row_count(entry): + count = len(entry.get("dtypes", [])) or 1 + for axis_group in ("string_axes", "int64_axes", "float64_axes"): + for values in entry.get(axis_group, {}).values(): + count *= len(values) + return count + + +_RESIZE_ANISOTROPIC_TARGETS = { + "CONTRACT": "TARGET_480x864", + "EXPAND": "TARGET_2160x3840", +} +_RESIZE_ANISOTROPIC_INPUT_SPATIAL_SHAPES = { + "TARGET_480x864": ["1080", "1920"], + "TARGET_2160x3840": ["480", "864"], +} + + +def _resize_coverage_signature(operator, axes): + fields = ["InOutDataType", "interpolation", "layout", "inputKind"] + if operator == "hqresize": + fields.extend(("antialias", "numChannels")) + return tuple(axes[field] for field in fields) + + +def test_generate_axis_args_preserves_axis_order_and_grouping(): + config = { + "synthetic_planar_basic": { + "benchmark": "synthetic", + "tier": "basic", + "dtypes": ["uchar3"], + "string_axes": { + "shape": ["1x2x3"], + "layout": ["NCHW"], + "inputKind": ["Tensor", "VarShape"], + }, + } + } + + assert generate_axis_args("synthetic_planar_basic", config) == [ + "--axis", + "InOutDataType=uchar3", + "--axis", + "shape=1x2x3", + "--axis", + "layout=NCHW", + "--axis", + "inputKind=[Tensor,VarShape]", + ] + + +def test_repository_config_separates_basic_and_advanced_tiers(): + config = load_bench_config() + manifest = load_bench_manifest() + + assert { + entry.get("tier") for entry in config.values() if isinstance(entry, dict) + } == {"basic", "advanced"} + assert manifest["resize"] == { + "config": "operators/resize.json", + "cpp": "bench_resize", + "python": "bench_resize.py", + } + removed_field = "manual" + "_only" + assert [ + key + for key, entry in config.items() + if isinstance(entry, dict) and removed_field in entry + ] == [] + assert [ + key + for key, entry in config.items() + if isinstance(entry, dict) + and entry.get("tier") == "basic" + and "advanced" in key + ] == [] + assert [ + key + for key, entry in config.items() + if isinstance(entry, dict) + and entry.get("tier") == "advanced" + and not key.endswith("_advanced") + ] == [] + + planar_layouts = {"NCHW", "CHW", "NCHW_FAKE", "CHW_FAKE"} + fake_planar_layouts = {"NCHW_FAKE", "CHW_FAKE"} + planar_keys = [] + fake_planar_keys = [] + + for key, entry in config.items(): + if not isinstance(entry, dict): + continue + + string_axes = entry.get("string_axes", {}) + layouts = _entry_string_axis_values(entry, "layout") + + if any(token in key.lower() for token in ("planar", "nchw", "chw")): + assert ( + "layout" in string_axes + ), f"{key} names a planar layout but has no layout axis" + + if any(layout in planar_layouts for layout in layouts): + planar_keys.append(key) + + if any(layout in fake_planar_layouts for layout in layouts): + fake_planar_keys.append(key) + assert entry["tier"] == "advanced" + input_kinds = _entry_string_axis_values(entry, "inputKind") + assert input_kinds == [ + "Tensor" + ], f"{key} fake-planar row is not Tensor-only" + + assert planar_keys + assert fake_planar_keys + + # CvtColor keeps one basic case for every reviewed conversion branch. + assert config["cvtcolor_basic"]["string_axes"]["code"] == [ + "RGB2GRAY", + "RGB2HSV", + "YUV2RGB", + "YUV2RGB_NV12", + ] + assert config["cvtcolor_planar_nchw_1080p_basic"]["string_axes"]["code"] == [ + "RGB2GRAY", + "RGB2HSV", + "YUV2RGB", + ] + assert config["cvtcolor_rgb2rgba_uchar3_basic"]["string_axes"]["inputKind"] == [ + "Tensor", + "VarShape", + ] + + # CLAHE keeps the representative clip limit in basic; the alternate limit + # and fake-planar comparison live in advanced. + assert config["clahe_basic"]["int64_axes"]["clip10"] == [400] + assert config["clahe_clip20_advanced"]["int64_axes"]["clip10"] == [20] + assert config["clahe_clip20_advanced"]["tier"] == "advanced" + assert config["clahe_fake_planar_advanced"]["tier"] == "advanced" + + # Normalize's RGB8 basic matrix also measures the tensor-free scalar-parameter path. + assert config["normalize_rgb_u8_1080p_basic"]["string_axes"]["inputKind"] == [ + "Tensor", + "TensorScalar", + "VarShape", + ] + assert config["normalize_planar_rgb_u8_nchw_1080p_basic"]["string_axes"][ + "inputKind" + ] == ["Tensor", "TensorScalar", "VarShape"] + assert config["normalize_rgb_f32_1080p_basic"]["string_axes"]["inputKind"] == [ + "Tensor" + ] + assert config["normalize_planar_rgb_f32_nchw_1080p_basic"]["string_axes"][ + "inputKind" + ] == ["Tensor"] + assert config["normalize_rgb_f32_1080p_advanced"]["string_axes"]["inputKind"] == [ + "VarShape" + ] + + # GammaContrast float3 is symmetric across native layouts for Tensor; + # VarShape float3 remains advanced-only. + assert config["gammacontrast_float3_lcg_basic"]["string_axes"] == { + "shape": ["32x1080x1920"], + "layout": ["NHWC", "NCHW"], + "inputKind": ["Tensor"], + } + assert config["gammacontrast_planar_nchw_float3_varshape_lcg_advanced"][ + "string_axes" + ]["inputKind"] == ["VarShape"] + + # The real container coverage added for non-standard APIs stays explicit. + assert config["findhomography_basic"]["string_axes"]["inputKind"] == ["Tensor"] + assert config["findhomography_varshape_basic"]["string_axes"]["inputKind"] == [ + "VarShape" + ] + assert config["stack_uchar3_basic"]["string_axes"] == { + "shape": ["32x2160x3840"], + "layout": ["NHWC", "NCHW"], + "inputKind": ["Tensor", "VarShape"], + } + assert config["stack_uchar3_layout_compare_4k_advanced"]["string_axes"] == { + "shape": ["4x2160x3840"], + "layout": ["NHWC", "NCHW", "NCHW_FAKE"], + "inputKind": ["Tensor"], + } + + # These image-layout-bearing benchmarks now expose their truthful layout; + # semantic N-A operators intentionally do not receive a dummy axis. + layout_bearing = { + "bndbox", + "channelreorder", + "composite", + "cropflipnormalizereformat", + "histogram", + "label", + "minmaxloc", + "osd", + "padandstack", + "resizecropconvertreformat", + "sift", + "stack", + } + assert all( + "layout" in entry.get("string_axes", {}) + for entry in config.values() + if entry.get("benchmark") in layout_bearing + ) + layout_na = { + "findhomography", + "minarearect", + "nonmaximumsuppression", + "pairwisematcher", + "reformat", + } + assert all( + "layout" not in entry.get("string_axes", {}) + for entry in config.values() + if entry.get("benchmark") in layout_na + ) + assert config["label_basic"]["string_axes"]["inputKind"] == ["Tensor"] + assert config["pairwisematcher_uint8_basic"]["string_axes"]["inputKind"] == [ + "Tensor" + ] + + # AdaptiveThreshold's ImageBatch path rejects planar formats; its NCHW + # benchmark is therefore truthfully Tensor-only rather than a skipped row. + assert config["adaptivethreshold_planar_nchw_basic"]["string_axes"][ + "inputKind" + ] == ["Tensor"] + + assert "distancemap" not in manifest + + +@pytest.mark.parametrize( + ("operator", "dtype"), + [ + ("resize", "uchar3"), + ("pillowresize", "uchar3"), + ("hqresize", "uint8"), + ], +) +def test_resize_family_has_basic_anisotropic_profiles(operator, dtype): + config = load_bench_config() + profiles = { + "contract_480p_tensor": ("TARGET_480x864", "Tensor", ["1080", "1920"]), + "contract_480p_varshape": ( + "TARGET_480x864", + "VarShape", + ["1080", "1920"], + ), + "expand_4k_tensor": ("TARGET_2160x3840", "Tensor", ["480", "864"]), + "expand_4k_varshape": ( + "TARGET_2160x3840", + "VarShape", + ["480", "864"], + ), + } + + for suffix, (resize_type, input_kind, spatial_shape) in profiles.items(): + entry = config[f"{operator}_anisotropic_{suffix}_rgb8_basic"] + axes = entry["string_axes"] + assert entry["tier"] == "basic" + assert entry["dtypes"] == [dtype] + assert axes["layout"] == ["NHWC"] + assert axes["resizeType"] == [resize_type] + assert axes["inputKind"] == [input_kind] + assert axes["shape"][0].split("x")[1:] == spatial_shape + + +_RESIZE_EXISTING_BASIC_ANISOTROPIC_SIGNATURES = { + "resize": { + ("TARGET_480x864", ("uchar3", "AREA", "NHWC", "Tensor")), + ("TARGET_480x864", ("uchar3", "AREA", "NHWC", "VarShape")), + ("TARGET_2160x3840", ("uchar3", "LINEAR", "NHWC", "Tensor")), + ("TARGET_2160x3840", ("uchar3", "LINEAR", "NHWC", "VarShape")), + }, + "pillowresize": { + ("TARGET_480x864", ("uchar3", "CUBIC", "NHWC", "Tensor")), + ("TARGET_480x864", ("uchar3", "CUBIC", "NHWC", "VarShape")), + ("TARGET_2160x3840", ("uchar3", "CUBIC", "NHWC", "Tensor")), + ("TARGET_2160x3840", ("uchar3", "CUBIC", "NHWC", "VarShape")), + }, + "hqresize": { + ( + "TARGET_480x864", + ("uint8", "CUBIC", "NHWC", "Tensor", "1", "3"), + ), + ( + "TARGET_480x864", + ("uint8", "CUBIC", "NHWC", "VarShape", "1", "3"), + ), + ( + "TARGET_2160x3840", + ("uint8", "CUBIC", "NHWC", "Tensor", "0", "3"), + ), + ( + "TARGET_2160x3840", + ("uint8", "CUBIC", "NHWC", "VarShape", "0", "3"), + ), + }, +} + + +@pytest.mark.parametrize( + "operator,basic_rows,advanced_rows", + [ + ("resize", 22, 209), + ("pillowresize", 22, 247), + ("hqresize", 26, 86), + ], +) +def test_resize_family_anisotropic_coverage_mirrors_isotropic_matrix( + operator, basic_rows, advanced_rows +): + config = { + key: entry + for key, entry in load_bench_config().items() + if entry.get("benchmark") == operator + } + tier_rows = { + tier: sum( + _entry_row_count(entry) + for entry in config.values() + if entry["tier"] == tier + ) + for tier in ("basic", "advanced") + } + assert tier_rows == {"basic": basic_rows, "advanced": advanced_rows} + + isotropic_signatures = set() + anisotropic_cases = {} + for key, entry in config.items(): + for case_key in expected_case_keys_for_entry(key, entry): + axes = dict(parse_case_key(case_key)[1]) + resize_type = axes["resizeType"] + signature = _resize_coverage_signature(operator, axes) + + if resize_type in _RESIZE_ANISOTROPIC_TARGETS: + isotropic_signatures.add( + (_RESIZE_ANISOTROPIC_TARGETS[resize_type], signature) + ) + elif resize_type in _RESIZE_ANISOTROPIC_TARGETS.values(): + assert axes["shape"].split("x")[1:] == ( + _RESIZE_ANISOTROPIC_INPUT_SPATIAL_SHAPES[resize_type] + ) + semantic_case = (resize_type, signature) + assert semantic_case not in anisotropic_cases, ( + f"duplicate {operator} anisotropic semantic case: " + f"{semantic_case} in {anisotropic_cases[semantic_case][0]} and {key}" + ) + anisotropic_cases[semantic_case] = (key, entry["tier"]) + + existing_basic = _RESIZE_EXISTING_BASIC_ANISOTROPIC_SIGNATURES[operator] + assert { + semantic_case + for semantic_case, (_, tier) in anisotropic_cases.items() + if tier == "basic" + } == existing_basic + + # Some pre-existing basic profiles (for example anisotropic EXPAND + # VarShape) intentionally have no isotropic source. They remain in the + # matrix, while every projected case that was added by this extension is + # advanced-only. + assert set(anisotropic_cases) == isotropic_signatures | existing_basic + for semantic_case in isotropic_signatures - existing_basic: + assert anisotropic_cases[semantic_case][1] == "advanced" + + target_configs = { + key: entry + for key, entry in config.items() + if any( + resize_type in _RESIZE_ANISOTROPIC_TARGETS.values() + for resize_type in entry["string_axes"]["resizeType"] + ) + } + assert fake_planar_pairing_issues(target_configs) == () + + layout_batches = {} + for key, entry in target_configs.items(): + for case_key in expected_case_keys_for_entry(key, entry): + _, case_axes = parse_case_key(case_key) + axes = dict(case_axes) + layout = axes["layout"] + if layout not in {"NHWC", "NCHW", "NCHW_FAKE"}: + continue + shape = axes["shape"].split("x") + identity = tuple( + sorted( + (name, value) + for name, value in case_axes + if name not in {"layout", "shape"} + ) + ) + (("spatialShape", "x".join(shape[1:])),) + layout_batches.setdefault(identity, {})[layout] = int(shape[0]) + + for identity, batches in layout_batches.items(): + if "NHWC" in batches and {"NCHW", "NCHW_FAKE"} & batches.keys(): + assert ( + len(set(batches.values())) == 1 + ), f"{operator} layout batch mismatch for {dict(identity)}: {batches}" + + +@pytest.mark.parametrize( + "operator", + [ + "autocontrast", + "bndbox", + "boxblur", + "brightnesscontrast", + "centercrop", + "clahe", + "convertto", + "copymakeborder", + "cvtcolor", + "erase", + "flip", + "gaussiannoise", + "inpaint", + "invert", + ], +) +def test_priority_operator_fake_planar_rows_have_native_twins(operator): + config = { + key: entry + for key, entry in load_bench_config().items() + if entry.get("benchmark") == operator + } + + assert any( + layout in {"NCHW_FAKE", "CHW_FAKE"} + for entry in config.values() + for layout in _entry_string_axis_values(entry, "layout") + ) + assert fake_planar_pairing_issues(config) == () + + +def test_reformat_single_call_calibration_case_keys_are_stable(): + config = load_bench_config() + + expected = { + "reformat_uint8_pitched_fallback_advanced": ( + "reformat_uint8_pitched_fallback_advanced" + "[InOutDataType=uint8][shape=512x480x640]" + "[inputKind=Tensor][rowAlignment=256]" + ), + "reformat_uint8_pitched_large_image_advanced": ( + "reformat_uint8_pitched_large_image_advanced" + "[InOutDataType=uint8][shape=128x1080x1920]" + "[inputKind=Tensor][rowAlignment=256]" + ), + } + + for config_key, case_key in expected.items(): + entry = config[config_key] + assert expected_case_keys_for_entry(config_key, entry) == [case_key] + assert "timed_repetitions" not in entry.get("metadata", {}) + + assert "reformat_uint8_pitched_fullhd_advanced" not in config + + +def test_brightnesscontrast_benchmark_signatures_are_canonical_and_unique(): + config = { + key: entry + for key, entry in load_bench_config().items() + if entry.get("benchmark") == "brightnesscontrast" + } + assert { + "brightnesscontrast_rgb_f32_1080p_advanced", + "brightnesscontrast_planar_rgb_f32_nchw_1080p_advanced", + } <= config.keys() + + signatures = {} + for key, entry in config.items(): + for case in expected_case_keys_for_entry(key, entry): + _, axes = parse_case_key(case) + signature = (entry["tier"], tuple(sorted(axes))) + assert signature not in signatures, ( + f"{case} duplicates canonical benchmark signature from " + f"{signatures.get(signature)}" + ) + signatures[signature] = case + + +def test_parse_args_rejects_unknown_config_key(monkeypatch): + monkeypatch.setattr( + sys, + "argv", + ["run_bench.py", "--config-key", "definitely_not_a_config_key"], + ) + + with pytest.raises(SystemExit) as exc: + run_bench.parse_args() + + assert exc.value.code == 2 + + +def test_parse_args_accepts_exact_operator_names(monkeypatch): + monkeypatch.setattr( + sys, + "argv", + ["run_bench.py", "--operator", "resize,gaussian"], + ) + + args = run_bench.parse_args() + + assert args.operators == ["resize", "gaussian"] + + +def test_parse_args_forwards_arguments_after_explicit_bench_folder(monkeypatch): + monkeypatch.setattr( + sys, + "argv", + ["run_bench.py", ".", "--", "--axis", "shape=1x2x3"], + ) + + args = run_bench.parse_args() + + assert args.bench_folder == "." + assert args.bench_args[-2:] == ["--axis", "shape=1x2x3"] + + +def test_parse_args_defaults_to_csv_output(monkeypatch): + monkeypatch.setattr(sys, "argv", ["run_bench.py"]) + + args = run_bench.parse_args() + + assert args.output.endswith("bench_output.csv") + + +def test_parse_args_uses_shared_quality_defaults(monkeypatch): + monkeypatch.setattr(sys, "argv", ["run_bench.py"]) + + args = run_bench.parse_args() + + assert args.max_noise_pct == DEFAULT_BENCHMARK_QUALITY.max_noise_pct + assert args.max_perf_diff_pct == DEFAULT_BENCHMARK_QUALITY.max_perf_diff_pct + assert args.max_perf_diff_us == DEFAULT_BENCHMARK_QUALITY.max_perf_diff_us + + +def test_parse_args_accepts_warmup_cap(monkeypatch): + monkeypatch.setattr(sys, "argv", ["run_bench.py", "--warmup-cap", "50"]) + + args = run_bench.parse_args() + + assert args.warmup_cap == 50 + + +@pytest.mark.parametrize("value", ["-1", "abc", "1.5"]) +def test_parse_args_rejects_invalid_warmup_cap(monkeypatch, value): + monkeypatch.setattr(sys, "argv", ["run_bench.py", "--warmup-cap", value]) + + with pytest.raises(SystemExit) as exc: + run_bench.parse_args() + + assert exc.value.code == 2 + + +def test_parse_args_uses_csv_output_for_custom_config_file(monkeypatch, tmp_path): + config_dir = tmp_path / "config" + _write_json( + config_dir / "bench_params_smoke.json", + { + "operators": { + "resize": { + "config": "operators/resize.json", + "cpp": "bench_resize", + "python": "bench_resize.py", + } + } + }, + ) + monkeypatch.setattr( + sys, + "argv", + ["run_bench.py", "--config-file", str(config_dir / "bench_params_smoke.json")], + ) + + args = run_bench.parse_args() + + assert args.output.endswith("bench_output_smoke.csv") + + +def test_parse_args_accepts_keep_outputs_and_json_output(monkeypatch, tmp_path): + out = tmp_path / "bench_output.json" + monkeypatch.setattr( + sys, + "argv", + [ + "run_bench.py", + "--operator", + "resize", + "--keep-outputs", + "--output", + str(out), + ], + ) + + args = run_bench.parse_args() + + assert args.output == str(out) + assert args.keep_outputs is True + + +def test_parse_args_rejects_unknown_operator(monkeypatch): + monkeypatch.setattr( + sys, + "argv", + ["run_bench.py", "--operator", "res"], + ) + + with pytest.raises(SystemExit) as exc: + run_bench.parse_args() + + assert exc.value.code == 2 + + +def test_deprecated_benchmarks_arg_uses_exact_operator_names(monkeypatch): + monkeypatch.setattr( + sys, + "argv", + ["run_bench.py", "--benchmarks", "resize"], + ) + + args = run_bench.parse_args() + + assert args.operators == ["resize"] + + +def test_baseline_payload_from_dataframe_emits_raw_baselines_json(tmp_path): + config_dir = tmp_path / "config" + _write_baseline_config_tree(config_dir) + index = load_config_index(config_dir / "operators") + df = pd.DataFrame( + [ + _baseline_row(language="cpp"), + _baseline_row( + language="python", gpu_us=535.7935, noise_us=3.88, bwutil=0.41 + ), + ] + ) + + payload = baseline_payload_from_dataframe( + df, + index=index, + sku_map_path=config_dir / "sku_map.json", + source=tmp_path / "bench_output.json", + ) + + case = ( + "resize_contract_area_tensor_uchar3_basic" + "[InOutDataType=uchar3]" + "[shape=32x1080x1920]" + "[kernelSize=5x5]" + "[resizeType=CONTRACT]" + "[interpolation=AREA]" + "[layout=NHWC]" + "[inputKind=Tensor]" + ) + assert payload == { + case: { + "A100_PCIE_40GB_250W_1095MHz": { + "n_runs": 1, + "gpu_time_us_cpp": 525.7525, + "gpu_time_us_python": 535.7935, + "gpu_noise_us_cpp": 1.2, + "gpu_noise_us_python": 3.88, + "gpu_bwutil_cpp": 0.42, + "gpu_bwutil_python": 0.41, + } + } + } + + +def test_baseline_payload_from_dataframe_allows_single_language(tmp_path): + config_dir = tmp_path / "config" + _write_baseline_config_tree(config_dir) + index = load_config_index(config_dir / "operators") + + payload = baseline_payload_from_dataframe( + pd.DataFrame([_baseline_row(language="cpp")]), + index=index, + sku_map_path=config_dir / "sku_map.json", + source=tmp_path / "bench_output.json", + ) + + metrics = next(iter(next(iter(payload.values())).values())) + assert metrics == { + "n_runs": 1, + "gpu_time_us_cpp": 525.7525, + "gpu_noise_us_cpp": 1.2, + "gpu_bwutil_cpp": 0.42, + } + + +def test_baseline_payload_from_dataframe_rejects_missing_ambiguous_axis(tmp_path): + config_dir = tmp_path / "config" + _write_baseline_config_tree(config_dir) + config_path = config_dir / "operators" / "resize.json" + config = json.loads(config_path.read_text()) + config["configs"]["resize_contract_area_tensor_uchar3_basic"]["string_axes"][ + "kernelSize" + ] = ["3x3", "5x5"] + _write_json(config_path, config) + + index = load_config_index(config_dir / "operators") + + with pytest.raises(BaselineError, match="missing axis column 'kernelSize'"): + baseline_payload_from_dataframe( + pd.DataFrame([_baseline_row(language="cpp")]), + index=index, + sku_map_path=config_dir / "sku_map.json", + source=tmp_path / "bench_output.json", + ) + + +def test_baseline_config_paths_uses_run_manifest(tmp_path): + config_dir = tmp_path / "config" + _write_baseline_config_tree(config_dir) + _write_json( + config_dir / "bench_params.json", + { + "operators": { + "resize": { + "config": "operators/resize.json", + "cpp": "bench_resize", + "python": "bench_resize.py", + } + } + }, + ) + + paths, sku_map_path = run_bench._baseline_config_paths( + str(config_dir / "bench_params.json") + ) + + assert paths == [(config_dir / "operators" / "resize.json").resolve()] + assert sku_map_path == config_dir / "sku_map.json" + + +def test_write_output_json_writes_raw_payload(tmp_path): + config_dir = tmp_path / "config" + _write_baseline_config_tree(config_dir) + _write_json( + config_dir / "bench_params.json", + { + "operators": { + "resize": { + "config": "operators/resize.json", + "cpp": "bench_resize", + "python": "bench_resize.py", + } + } + }, + ) + out = tmp_path / "bench_baselines.json" + args = SimpleNamespace( + config_file=str(config_dir / "bench_params.json"), + output=str(out), + ) + df = pd.DataFrame( + [ + _baseline_row(language="cpp"), + _baseline_row( + language="python", gpu_us=535.7935, noise_us=3.88, bwutil=0.41 + ), + ] + ) + + run_bench._write_output(args, df) + + case = next(iter(json.loads(out.read_text()))) + assert case == ( + "resize_contract_area_tensor_uchar3_basic" + "[InOutDataType=uchar3]" + "[shape=32x1080x1920]" + "[kernelSize=5x5]" + "[resizeType=CONTRACT]" + "[interpolation=AREA]" + "[layout=NHWC]" + "[inputKind=Tensor]" + ) + + +def test_write_output_csv_writes_legacy_combined_csv(tmp_path): + out = tmp_path / "bench_output.csv" + args = SimpleNamespace( + output=str(out), + max_noise_pct=DEFAULT_BENCHMARK_QUALITY.max_noise_pct, + max_perf_diff_pct=DEFAULT_BENCHMARK_QUALITY.max_perf_diff_pct, + max_perf_diff_us=DEFAULT_BENCHMARK_QUALITY.max_perf_diff_us, + ) + df = pd.DataFrame( + [ + { + **_baseline_row(gpu_us=525.75251, noise_us=1.234), + "Device": 0, + "GPU Noise (%)": 0.23456, + "CPU Time (µs)": 42.4242, + "CPU Noise (%)": 0.1234, + "CPU Noise (µs)": 0.5678, + "Py overhead (%)": 1.2345, + "Py overhead (µs)": 2.3456, + } + ] + ) + + run_bench._write_output(args, df) + + written = pd.read_csv(out) + assert "Device" not in written.columns + assert written.loc[0, "config_key"] == "resize_contract_area_tensor_uchar3_basic" + assert written.loc[0, "GPU Time (µs)"] == pytest.approx(525.75) + assert written.loc[0, "BWUtil"] == pytest.approx(0.42) + + +def test_discover_operator_pairs_uses_manifest_operator_names(tmp_path): + for name in ("resize", "gaussian"): + _touch(tmp_path / f"bench_{name}") + _touch(tmp_path / f"bench_{name}.py") + + config = { + "resize_basic": { + "benchmark": "resize", + "tier": "basic", + }, + "gaussian_basic": { + "benchmark": "gaussian", + "tier": "basic", + }, + } + + cpp_runner = DummyRunner( + tmp_path, + config, + config_keys=None, + language="cpp", + operators=["gaussian", "resize"], + tiers={"basic"}, + ) + python_runner = DummyRunner( + tmp_path, + config, + config_keys=None, + language="python", + operators=["gaussian", "resize"], + tiers={"basic"}, + ) + + pairs = run_bench.discover_operator_pairs(cpp_runner, python_runner) + + assert [ + (key, os.path.basename(cpp), os.path.basename(py)) for key, cpp, py in pairs + ] == [ + ("gaussian_basic", "bench_gaussian", "bench_gaussian.py"), + ("resize_basic", "bench_resize", "bench_resize.py"), + ] + + +def test_discover_operator_pairs_with_config_keys_supports_mixed_operators(tmp_path): + for name in ("resize", "gaussian"): + _touch(tmp_path / f"bench_{name}") + _touch(tmp_path / f"bench_{name}.py") + + config = { + "resize_advanced": { + "benchmark": "resize", + "tier": "advanced", + }, + "gaussian_advanced": { + "benchmark": "gaussian", + "tier": "advanced", + }, + } + config_keys = ["resize_advanced", "gaussian_advanced"] + + cpp_runner = DummyRunner( + tmp_path, + config, + config_keys, + language="cpp", + ) + python_runner = DummyRunner( + tmp_path, + config, + config_keys, + language="python", + ) + + pairs = run_bench.discover_operator_pairs( + cpp_runner, python_runner, config_keys=config_keys + ) + + assert [ + (key, os.path.basename(cpp), os.path.basename(py)) for key, cpp, py in pairs + ] == [ + ("resize_advanced", "bench_resize", "bench_resize.py"), + ("gaussian_advanced", "bench_gaussian", "bench_gaussian.py"), + ] diff --git a/bench/tests/test_update_baseline.py b/bench/tests/test_update_baseline.py new file mode 100644 index 000000000..616074809 --- /dev/null +++ b/bench/tests/test_update_baseline.py @@ -0,0 +1,656 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""Unit tests for JSON-backed update_baseline.py.""" + +from __future__ import annotations + +import json +import math +import os +import subprocess +import sys +from pathlib import Path + +import pytest + +from _internal import update_baseline +from _internal.baselines import case_key_from_axis_values +from _internal.quality import DEFAULT_BENCHMARK_QUALITY + + +def test_internal_command_help_is_self_contained(tmp_path): + env = os.environ.copy() + env.pop("PYTHONPATH", None) + + result = subprocess.run( + [sys.executable, update_baseline.__file__, "--help"], + cwd=tmp_path, + env=env, + capture_output=True, + text=True, + check=False, + ) + + assert result.returncode == 0, result.stderr + assert "--from JSON_OR_DIR" in result.stdout + assert "Examples:" in result.stdout + assert "Exit status:" in result.stdout + + +def test_repo_root_after_internal_move(): + assert update_baseline._repo_root() == Path(__file__).resolve().parents[2] + + +def _write_json(path, payload): + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(payload, indent=4) + "\n") + + +def _case(config_key, dtype="uint8", shape="1x2x3"): + return case_key_from_axis_values( + config_key, + [ + ("InOutDataType", dtype), + ("shape", shape), + ("inputKind", "Tensor"), + ], + ) + + +def _config_entry(dtype="uint8", shape="1x2x3", baselines=None): + entry = { + "tier": "basic", + "dtypes": [dtype], + "string_axes": { + "shape": [shape], + "inputKind": ["Tensor"], + }, + } + if baselines is not None: + entry["baselines"] = baselines + return entry + + +def _write_config_tree( + root, *, include_second_sku=False, include_resize_baseline=False +): + entries = [ + { + "gpu_name": "NVIDIA Test GPU", + "power_cap_w": 350, + "locked_sm_clock_mhz": 1095, + "stem": "TESTSKU", + } + ] + if include_second_sku: + entries.append( + { + "gpu_name": "NVIDIA Test GPU 2", + "power_cap_w": 250, + "locked_sm_clock_mhz": 1095, + "stem": "TESTSKU2", + } + ) + _write_json( + root / "sku_map.json", + {"entries": entries}, + ) + resize_baselines = None + if include_resize_baseline: + resize_case = _case("resize_basic") + resize_baselines = { + resize_case: { + "TESTSKU": { + "n_runs": 1, + "gpu_time_us_cpp": 100.0, + "gpu_time_us_python": 110.0, + "gpu_noise_us_cpp": 1.0, + "gpu_noise_us_python": 1.0, + "gpu_bwutil_cpp": 0.42, + "gpu_bwutil_python": 0.41, + } + } + } + + _write_json( + root / "operators" / "resize.json", + { + "benchmark": "resize", + "configs": { + "resize_basic": _config_entry(baselines=resize_baselines), + }, + }, + ) + gaussian_case = _case("gaussian_basic", shape="4x5x6") + _write_json( + root / "operators" / "gaussian.json", + { + "benchmark": "gaussian", + "configs": { + "gaussian_basic": _config_entry( + shape="4x5x6", + baselines={ + gaussian_case: { + "TESTSKU": { + "n_runs": 1, + "gpu_time_us_cpp": 200.0, + "gpu_time_us_python": 210.0, + "gpu_noise_us_cpp": 2.0, + "gpu_noise_us_python": 3.0, + "gpu_bwutil_cpp": 0.22, + "gpu_bwutil_python": 0.21, + } + } + }, + ), + }, + }, + ) + + +def _baseline_payload( + config_key="resize_basic", + sku="TESTSKU", + cpp_us=100.0, + python_us=110.0, + cpp_noise_us=1.0, + python_noise_us=1.0, + cpp_bwutil=0.42, + python_bwutil=0.41, + n_runs=1, + gap_stddev_us=None, + shape="1x2x3", +): + case = _case(config_key, shape=shape) + metrics = { + "n_runs": n_runs, + "gpu_time_us_cpp": cpp_us, + "gpu_time_us_python": python_us, + "gpu_noise_us_cpp": cpp_noise_us, + "gpu_noise_us_python": python_noise_us, + "gpu_bwutil_cpp": cpp_bwutil, + "gpu_bwutil_python": python_bwutil, + } + if gap_stddev_us is not None: + metrics["gpu_gap_stddev_us"] = gap_stddev_us + return { + case: { + sku: metrics, + } + } + + +def _write_run_json(path, *payloads): + merged = {} + for payload in payloads: + for case_key, case_payload in payload.items(): + merged.setdefault(case_key, {}).update(case_payload) + _write_json(path, merged) + + +def _run(config_dir, *args): + return update_baseline.main(["--config-dir", str(config_dir), *args]) + + +def _load_operator(config_dir, name): + return json.loads((config_dir / "operators" / f"{name}.json").read_text()) + + +def test_normalize_operators_handles_repeats_and_commas(): + assert update_baseline._normalize_operators(None) is None + assert update_baseline._normalize_operators(["resize,gaussian", "resize"]) == { + "resize", + "gaussian", + } + + +def test_update_writes_nested_json_baseline(tmp_path): + config_dir = tmp_path / "config" + _write_config_tree(config_dir) + run_json = tmp_path / "run.json" + _write_run_json(run_json, _baseline_payload()) + + assert _run(config_dir, "--from", str(run_json)) == 0 + + raw = _load_operator(config_dir, "resize") + case = _case("resize_basic") + metrics = raw["configs"]["resize_basic"]["baselines"][case]["TESTSKU"] + assert metrics == { + "n_runs": 1, + "gpu_time_us_cpp": 100.0, + "gpu_time_us_python": 110.0, + "gpu_noise_us_cpp": 1.0, + "gpu_noise_us_python": 1.0, + "gpu_bwutil_cpp": 0.42, + "gpu_bwutil_python": 0.41, + } + + +def test_multi_input_averages_repeated_runs(tmp_path): + config_dir = tmp_path / "config" + _write_config_tree(config_dir) + runs = tmp_path / "runs" + runs.mkdir() + _write_run_json( + runs / "r1.json", + _baseline_payload(cpp_us=100.0, python_us=105.0), + ) + _write_run_json( + runs / "r2.json", + _baseline_payload(cpp_us=110.0, python_us=115.0), + ) + + _run(config_dir, "--from", str(runs)) + + case = _case("resize_basic") + metrics = _load_operator(config_dir, "resize")["configs"]["resize_basic"][ + "baselines" + ][case]["TESTSKU"] + assert metrics["n_runs"] == 2 + assert metrics["gpu_time_us_cpp"] == pytest.approx(105.0) + assert metrics["gpu_time_us_python"] == pytest.approx(110.0) + assert metrics["gpu_bwutil_cpp"] == pytest.approx(0.42) + assert metrics["gpu_bwutil_python"] == pytest.approx(0.41) + assert metrics["gpu_gap_stddev_us"] == pytest.approx(0.0) + + +def test_multi_input_computes_paired_artifact_gap_stddev(tmp_path): + config_dir = tmp_path / "config" + _write_config_tree(config_dir) + runs = tmp_path / "runs" + runs.mkdir() + _write_run_json( + runs / "r1.json", + _baseline_payload(cpp_us=100.0, python_us=105.0), + ) + _write_run_json( + runs / "r2.json", + _baseline_payload(cpp_us=200.0, python_us=215.0), + ) + + _run(config_dir, "--from", str(runs)) + + case = _case("resize_basic") + metrics = _load_operator(config_dir, "resize")["configs"]["resize_basic"][ + "baselines" + ][case]["TESTSKU"] + assert metrics["gpu_time_us_cpp"] == pytest.approx(150.0) + assert metrics["gpu_time_us_python"] == pytest.approx(160.0) + assert metrics["gpu_gap_stddev_us"] == pytest.approx(math.sqrt(50.0)) + + +def test_multi_input_pools_preaggregated_paired_gap_stddev(tmp_path): + config_dir = tmp_path / "config" + _write_config_tree(config_dir) + run1 = tmp_path / "run1.json" + run2 = tmp_path / "run2.json" + _write_run_json( + run1, + _baseline_payload( + cpp_us=100.0, + python_us=105.0, + n_runs=2, + gap_stddev_us=math.sqrt(2.0), + ), + ) + _write_run_json( + run2, + _baseline_payload( + cpp_us=200.0, + python_us=210.0, + n_runs=3, + gap_stddev_us=2.0, + ), + ) + + _run(config_dir, "--from", str(run1), "--from", str(run2)) + + case = _case("resize_basic") + metrics = _load_operator(config_dir, "resize")["configs"]["resize_basic"][ + "baselines" + ][case]["TESTSKU"] + assert metrics["n_runs"] == 5 + assert metrics["gpu_time_us_cpp"] == pytest.approx(160.0) + assert metrics["gpu_time_us_python"] == pytest.approx(168.0) + assert metrics["gpu_gap_stddev_us"] == pytest.approx(math.sqrt(10.0)) + + +def test_preaggregated_input_without_gap_dispersion_does_not_invent_it(tmp_path): + config_dir = tmp_path / "config" + _write_config_tree(config_dir) + run_json = tmp_path / "run.json" + _write_run_json(run_json, _baseline_payload(n_runs=2)) + + _run(config_dir, "--from", str(run_json)) + + case = _case("resize_basic") + metrics = _load_operator(config_dir, "resize")["configs"]["resize_basic"][ + "baselines" + ][case]["TESTSKU"] + assert "gpu_gap_stddev_us" not in metrics + + +def test_update_drops_stale_gap_dispersion_when_new_pairing_is_unavailable(tmp_path): + config_dir = tmp_path / "config" + _write_config_tree(config_dir, include_resize_baseline=True) + case = _case("resize_basic") + raw = _load_operator(config_dir, "resize") + existing = raw["configs"]["resize_basic"]["baselines"][case]["TESTSKU"] + existing["n_runs"] = 2 + existing["gpu_gap_stddev_us"] = 3.0 + _write_json(config_dir / "operators" / "resize.json", raw) + run_json = tmp_path / "run.json" + _write_run_json(run_json, _baseline_payload(n_runs=2)) + + _run(config_dir, "--from", str(run_json)) + + metrics = _load_operator(config_dir, "resize")["configs"]["resize_basic"][ + "baselines" + ][case]["TESTSKU"] + assert "gpu_gap_stddev_us" not in metrics + + +def test_operator_filter_preserves_unrelated_operator_json(tmp_path): + config_dir = tmp_path / "config" + _write_config_tree(config_dir) + before = (config_dir / "operators" / "gaussian.json").read_text() + run_json = tmp_path / "run.json" + _write_run_json( + run_json, + _baseline_payload(), + _baseline_payload( + config_key="gaussian_basic", + cpp_us=999.0, + python_us=999.0, + shape="4x5x6", + ), + ) + + _run(config_dir, "--from", str(run_json), "--operator", "resize") + + assert (config_dir / "operators" / "gaussian.json").read_text() == before + + +def test_dry_run_writes_nothing(tmp_path, capsys): + config_dir = tmp_path / "config" + _write_config_tree(config_dir) + before = (config_dir / "operators" / "resize.json").read_text() + run_json = tmp_path / "run.json" + _write_run_json(run_json, _baseline_payload()) + + assert _run(config_dir, "--from", str(run_json), "--dry-run") == 0 + assert (config_dir / "operators" / "resize.json").read_text() == before + assert "dry run" in capsys.readouterr().out + + +def test_update_rejects_same_key_regression_without_writing(tmp_path, capsys): + config_dir = tmp_path / "config" + _write_config_tree(config_dir, include_resize_baseline=True) + before = (config_dir / "operators" / "resize.json").read_text() + run_json = tmp_path / "run.json" + _write_run_json(run_json, _baseline_payload(cpp_us=120.0, python_us=125.0)) + + with pytest.raises(SystemExit) as exc: + _run(config_dir, "--from", str(run_json)) + + assert exc.value.code == 1 + assert (config_dir / "operators" / "resize.json").read_text() == before + captured = capsys.readouterr() + assert "same-key baseline regressions" in captured.err + assert "resize_basic" in captured.err + assert "--allow-regressions" in captured.err + + +def test_dry_run_rejects_same_key_regression_without_writing(tmp_path): + config_dir = tmp_path / "config" + _write_config_tree(config_dir, include_resize_baseline=True) + before = (config_dir / "operators" / "resize.json").read_text() + run_json = tmp_path / "run.json" + _write_run_json(run_json, _baseline_payload(cpp_us=120.0, python_us=125.0)) + + with pytest.raises(SystemExit): + _run(config_dir, "--from", str(run_json), "--dry-run") + + assert (config_dir / "operators" / "resize.json").read_text() == before + + +def test_update_accepts_same_key_change_within_regression_threshold(tmp_path): + config_dir = tmp_path / "config" + _write_config_tree(config_dir, include_resize_baseline=True) + run_json = tmp_path / "run.json" + _write_run_json(run_json, _baseline_payload(cpp_us=105.0, python_us=115.0)) + + assert _run(config_dir, "--from", str(run_json)) == 0 + + case = _case("resize_basic") + metrics = _load_operator(config_dir, "resize")["configs"]["resize_basic"][ + "baselines" + ][case]["TESTSKU"] + assert metrics["gpu_time_us_cpp"] == 105.0 + assert metrics["gpu_time_us_python"] == 115.0 + + +def test_update_allows_same_key_regression_with_explicit_override(tmp_path): + config_dir = tmp_path / "config" + _write_config_tree(config_dir, include_resize_baseline=True) + run_json = tmp_path / "run.json" + _write_run_json(run_json, _baseline_payload(cpp_us=120.0, python_us=125.0)) + + assert _run(config_dir, "--from", str(run_json), "--allow-regressions") == 0 + + case = _case("resize_basic") + metrics = _load_operator(config_dir, "resize")["configs"]["resize_basic"][ + "baselines" + ][case]["TESTSKU"] + assert metrics["gpu_time_us_cpp"] == 120.0 + assert metrics["gpu_time_us_python"] == 125.0 + + +def test_non_json_input_is_rejected_without_writing(tmp_path): + config_dir = tmp_path / "config" + _write_config_tree(config_dir) + before = (config_dir / "operators" / "resize.json").read_text() + text = tmp_path / "bad.txt" + text.write_text("not,supported\n") + + with pytest.raises(SystemExit): + _run(config_dir, "--from", str(text)) + assert (config_dir / "operators" / "resize.json").read_text() == before + + +def test_routes_multiple_skus_in_one_invocation(tmp_path): + config_dir = tmp_path / "config" + _write_config_tree(config_dir, include_second_sku=True) + runs = tmp_path / "runs" + runs.mkdir() + _write_run_json( + runs / "sku1.json", + _baseline_payload(), + ) + _write_run_json( + runs / "sku2.json", + _baseline_payload(sku="TESTSKU2", cpp_us=200.0, python_us=220.0), + ) + + _run(config_dir, "--from", str(runs)) + + case = _case("resize_basic") + case_payload = _load_operator(config_dir, "resize")["configs"]["resize_basic"][ + "baselines" + ][case] + assert set(case_payload) == {"TESTSKU", "TESTSKU2"} + + +def test_update_imports_raw_baseline_json(tmp_path): + config_dir = tmp_path / "config" + _write_config_tree(config_dir) + case = _case("resize_basic") + baseline_json = tmp_path / "run.json" + _write_run_json( + baseline_json, + _baseline_payload(python_noise_us=2.0), + ) + + _run(config_dir, "--from", str(baseline_json)) + + metrics = _load_operator(config_dir, "resize")["configs"]["resize_basic"][ + "baselines" + ][case]["TESTSKU"] + assert metrics == { + "n_runs": 1, + "gpu_time_us_cpp": 100.0, + "gpu_time_us_python": 110.0, + "gpu_noise_us_cpp": 1.0, + "gpu_noise_us_python": 2.0, + "gpu_bwutil_cpp": 0.42, + "gpu_bwutil_python": 0.41, + } + + +def test_update_averages_json_repeated_runs(tmp_path): + config_dir = tmp_path / "config" + _write_config_tree(config_dir) + case = _case("resize_basic") + run1 = tmp_path / "run1.json" + run2 = tmp_path / "run2.json" + _write_run_json(run1, _baseline_payload()) + _write_run_json( + run2, + _baseline_payload( + cpp_us=120.0, + python_us=130.0, + cpp_noise_us=3.0, + python_noise_us=5.0, + cpp_bwutil=0.44, + python_bwutil=0.43, + ), + ) + + _run(config_dir, "--from", str(run1), "--from", str(run2)) + + metrics = _load_operator(config_dir, "resize")["configs"]["resize_basic"][ + "baselines" + ][case]["TESTSKU"] + assert metrics["n_runs"] == 2 + assert metrics["gpu_time_us_cpp"] == pytest.approx(110.0) + assert metrics["gpu_time_us_python"] == pytest.approx(120.0) + assert metrics["gpu_noise_us_cpp"] == pytest.approx(2.0) + assert metrics["gpu_noise_us_python"] == pytest.approx(3.0) + assert metrics["gpu_bwutil_cpp"] == pytest.approx(0.43) + assert metrics["gpu_bwutil_python"] == pytest.approx(0.42) + assert metrics["gpu_gap_stddev_us"] == pytest.approx(0.0) + + +def test_invalid_json_aborts_without_writing(tmp_path): + config_dir = tmp_path / "config" + _write_config_tree(config_dir) + before = (config_dir / "operators" / "resize.json").read_text() + case = _case("resize_basic") + baseline_json = tmp_path / "bad.json" + _write_json( + baseline_json, + { + case: { + "UNKNOWN": { + "n_runs": 1, + "gpu_time_us_cpp": 100.0, + "gpu_noise_us_cpp": 1.0, + "gpu_bwutil_cpp": 0.42, + } + } + }, + ) + + with pytest.raises(SystemExit): + _run(config_dir, "--from", str(baseline_json)) + assert (config_dir / "operators" / "resize.json").read_text() == before + + +def test_quality_violating_json_aborts_without_writing(tmp_path): + config_dir = tmp_path / "config" + _write_config_tree(config_dir) + before = (config_dir / "operators" / "resize.json").read_text() + run_json = tmp_path / "run.json" + _write_run_json( + run_json, + _baseline_payload( + python_us=100.0 + * (1.0 + (DEFAULT_BENCHMARK_QUALITY.max_perf_diff_pct + 1.0) / 100.0) + ), + ) + + with pytest.raises(SystemExit): + _run(config_dir, "--from", str(run_json)) + + assert (config_dir / "operators" / "resize.json").read_text() == before + + +def test_dry_run_validates_quality_without_writing(tmp_path): + config_dir = tmp_path / "config" + _write_config_tree(config_dir) + before = (config_dir / "operators" / "resize.json").read_text() + run_json = tmp_path / "run.json" + _write_run_json( + run_json, + _baseline_payload(cpp_noise_us=DEFAULT_BENCHMARK_QUALITY.max_noise_pct + 1.0), + ) + + with pytest.raises(SystemExit): + _run(config_dir, "--from", str(run_json), "--dry-run") + + assert (config_dir / "operators" / "resize.json").read_text() == before + + +def test_single_language_json_aborts_without_writing(tmp_path): + config_dir = tmp_path / "config" + _write_config_tree(config_dir) + before = (config_dir / "operators" / "resize.json").read_text() + case = _case("resize_basic") + run_json = tmp_path / "run.json" + _write_json( + run_json, + { + case: { + "TESTSKU": { + "n_runs": 1, + "gpu_time_us_cpp": 100.0, + "gpu_noise_us_cpp": 1.0, + "gpu_bwutil_cpp": 0.42, + } + } + }, + ) + + with pytest.raises(SystemExit): + _run(config_dir, "--from", str(run_json)) + + assert (config_dir / "operators" / "resize.json").read_text() == before + + +def test_from_diff_filename_mapping(monkeypatch): + class _Stub: + returncode = 0 + stdout = ( + "src/cvcuda/priv/OpGaussian.cpp\n" + "src/cvcuda/priv/OpGaussianNoise.cu\n" + "docs/index.rst\n" + ) + stderr = "" + + monkeypatch.setattr(update_baseline.subprocess, "run", lambda *a, **k: _Stub()) + assert update_baseline._ops_from_git_diff( + "origin/main", {"gaussian", "gaussiannoise", "resize"} + ) == {"gaussian", "gaussiannoise"} diff --git a/bench/tests/test_validate_baselines.py b/bench/tests/test_validate_baselines.py new file mode 100644 index 000000000..29f856a4a --- /dev/null +++ b/bench/tests/test_validate_baselines.py @@ -0,0 +1,519 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""Unit tests for validate_baselines.py and shared JSON validation.""" + +from __future__ import annotations + +import copy +import json +import os +import subprocess +import sys +from pathlib import Path +from types import SimpleNamespace + +import pytest +from _internal import validate_baselines +from _internal.baselines import ( + case_key_from_axis_values, + load_config_index, + split_operator_payload, + sku_stems, + validate_baselines_in_document, +) +from _internal.quality import DEFAULT_BENCHMARK_QUALITY + + +def test_internal_command_help_is_self_contained(tmp_path): + env = os.environ.copy() + env.pop("PYTHONPATH", None) + + result = subprocess.run( + [sys.executable, validate_baselines.__file__, "--help"], + cwd=tmp_path, + env=env, + capture_output=True, + text=True, + check=False, + ) + + assert result.returncode == 0, result.stderr + assert "OPERATOR_JSON" in result.stdout + assert "Examples:" in result.stdout + assert "Exit status:" in result.stdout + + +def test_repo_root_after_internal_move(): + assert validate_baselines._repo_root() == Path(__file__).resolve().parents[2] + + +def _write_json(path, payload): + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(payload, indent=4) + "\n") + + +def _case(config_key="resize_basic", *, dtype="uint8", shape="1x2x3"): + return case_key_from_axis_values( + config_key, + [ + ("InOutDataType", dtype), + ("shape", shape), + ("inputKind", "Tensor"), + ], + ) + + +def _payload(): + case = _case() + return { + "benchmark": "resize", + "configs": { + "resize_basic": { + "tier": "basic", + "dtypes": ["uint8"], + "string_axes": { + "shape": ["1x2x3"], + "inputKind": ["Tensor"], + }, + "baselines": { + case: { + "TESTSKU": { + "n_runs": 2, + "gpu_time_us_cpp": 100.0, + "gpu_time_us_python": 110.0, + "gpu_noise_us_cpp": 1.0, + "gpu_noise_us_python": 2.0, + "gpu_bwutil_cpp": 0.42, + "gpu_bwutil_python": 0.41, + } + } + }, + } + }, + } + + +def _write_tree(tmp_path, payload): + config_dir = tmp_path / "config" + _write_json(config_dir / "operators" / "resize.json", payload) + _write_json( + config_dir / "sku_map.json", + { + "entries": [ + { + "gpu_name": "NVIDIA Test GPU", + "power_cap_w": 350, + "locked_sm_clock_mhz": 1095, + "stem": "TESTSKU", + } + ] + }, + ) + index = load_config_index(config_dir / "operators") + doc = index.docs_by_path[config_dir / "operators" / "resize.json"] + stems = sku_stems(config_dir / "sku_map.json", strict=True) + return validate_baselines_in_document(doc, index, stems) + + +def _write_config_dir(tmp_path, payload): + config_dir = tmp_path / "config" + _write_json(config_dir / "operators" / "resize.json", payload) + _write_json( + config_dir / "sku_map.json", + { + "entries": [ + { + "gpu_name": "NVIDIA Test GPU", + "power_cap_w": 350, + "locked_sm_clock_mhz": 1095, + "stem": "TESTSKU", + } + ] + }, + ) + return config_dir + + +def _config_dir_with_ref_baseline( + tmp_path, monkeypatch, *, cpp_time_us, python_time_us +): + base_payload = _payload() + current_payload = copy.deepcopy(base_payload) + metrics = current_payload["configs"]["resize_basic"]["baselines"][_case()][ + "TESTSKU" + ] + metrics["gpu_time_us_cpp"] = cpp_time_us + metrics["gpu_time_us_python"] = python_time_us + config_dir = _write_config_dir(tmp_path, current_payload) + base_doc = split_operator_payload( + config_dir / "operators" / "resize.json", base_payload + ) + + monkeypatch.setattr(validate_baselines, "_verify_git_ref", lambda ref: None) + monkeypatch.setattr( + validate_baselines, + "_operator_doc_at_ref", + lambda ref, path: base_doc, + ) + return config_dir + + +def test_valid_operator_json_passes(tmp_path): + assert _write_tree(tmp_path, _payload()) == [] + + +def test_valid_optional_paired_gap_stddev_passes(tmp_path): + payload = _payload() + payload["configs"]["resize_basic"]["baselines"][_case()]["TESTSKU"][ + "gpu_gap_stddev_us" + ] = 3.0 + assert _write_tree(tmp_path, payload) == [] + + +def test_wrong_config_key_in_case_key_fails(tmp_path): + payload = _payload() + case_payload = payload["configs"]["resize_basic"]["baselines"].pop(_case()) + payload["configs"]["resize_basic"]["baselines"][_case("other_basic")] = case_payload + errors = _write_tree(tmp_path, payload) + assert any("does not match nested config" in error for error in errors) + + +def test_out_of_order_axis_fails(tmp_path): + payload = _payload() + case_payload = payload["configs"]["resize_basic"]["baselines"].pop(_case()) + bad_case = "resize_basic[shape=1x2x3][InOutDataType=uint8][inputKind=Tensor]" + payload["configs"]["resize_basic"]["baselines"][bad_case] = case_payload + errors = _write_tree(tmp_path, payload) + assert any("has axes" in error for error in errors) + + +def test_invalid_axis_value_fails(tmp_path): + payload = _payload() + case_payload = payload["configs"]["resize_basic"]["baselines"].pop(_case()) + payload["configs"]["resize_basic"]["baselines"][_case(shape="9x9x9")] = case_payload + errors = _write_tree(tmp_path, payload) + assert any("is not declared" in error for error in errors) + + +def test_missing_declared_case_key_fails(tmp_path): + payload = _payload() + payload["configs"]["resize_basic"]["string_axes"]["inputKind"] = [ + "Tensor", + "VarShape", + ] + + errors = _write_tree(tmp_path, payload) + + assert any( + "missing baseline case" in error and "VarShape" in error for error in errors + ) + + +def test_unknown_sku_fails(tmp_path): + payload = _payload() + payload["configs"]["resize_basic"]["baselines"][_case()]["UNKNOWN"] = payload[ + "configs" + ]["resize_basic"]["baselines"][_case()].pop("TESTSKU") + errors = _write_tree(tmp_path, payload) + assert any("unknown SKU" in error for error in errors) + + +def test_missing_metric_field_fails(tmp_path): + payload = _payload() + del payload["configs"]["resize_basic"]["baselines"][_case()]["TESTSKU"][ + "gpu_time_us_python" + ] + errors = _write_tree(tmp_path, payload) + assert any("missing metric field" in error for error in errors) + + +def test_missing_bwutil_metric_field_fails(tmp_path): + payload = _payload() + del payload["configs"]["resize_basic"]["baselines"][_case()]["TESTSKU"][ + "gpu_bwutil_python" + ] + errors = _write_tree(tmp_path, payload) + assert any("missing metric field" in error for error in errors) + + +def test_invalid_n_runs_fails(tmp_path): + payload = _payload() + payload["configs"]["resize_basic"]["baselines"][_case()]["TESTSKU"]["n_runs"] = 0 + errors = _write_tree(tmp_path, payload) + assert any("n_runs" in error for error in errors) + + +def test_non_numeric_metric_fails(tmp_path): + payload = copy.deepcopy(_payload()) + payload["configs"]["resize_basic"]["baselines"][_case()]["TESTSKU"][ + "gpu_noise_us_cpp" + ] = "nope" + errors = _write_tree(tmp_path, payload) + assert any("must be numeric" in error for error in errors) + + +def test_non_numeric_optional_paired_gap_stddev_fails(tmp_path): + payload = _payload() + payload["configs"]["resize_basic"]["baselines"][_case()]["TESTSKU"][ + "gpu_gap_stddev_us" + ] = "nope" + errors = _write_tree(tmp_path, payload) + assert any("gpu_gap_stddev_us must be numeric" in error for error in errors) + + +def test_optional_paired_gap_stddev_requires_repeated_runs(tmp_path): + payload = _payload() + metrics = payload["configs"]["resize_basic"]["baselines"][_case()]["TESTSKU"] + metrics["n_runs"] = 1 + metrics["gpu_gap_stddev_us"] = 0.0 + errors = _write_tree(tmp_path, payload) + assert any("gpu_gap_stddev_us requires n_runs >= 2" in error for error in errors) + + +def test_noise_above_quality_threshold_fails(tmp_path): + payload = _payload() + metrics = payload["configs"]["resize_basic"]["baselines"][_case()]["TESTSKU"] + metrics["gpu_noise_us_cpp"] = ( + metrics["gpu_time_us_cpp"] + * (DEFAULT_BENCHMARK_QUALITY.max_noise_pct + 1.0) + / 100.0 + ) + + errors = _write_tree(tmp_path, payload) + + assert any("cpp noise" in error and "exceeds" in error for error in errors) + + +def test_relative_parity_above_quality_threshold_fails(tmp_path): + payload = _payload() + metrics = payload["configs"]["resize_basic"]["baselines"][_case()]["TESTSKU"] + metrics["gpu_time_us_python"] = metrics["gpu_time_us_cpp"] * ( + 1.0 + (DEFAULT_BENCHMARK_QUALITY.max_perf_diff_pct + 1.0) / 100.0 + ) + + errors = _write_tree(tmp_path, payload) + + assert any("C++/Python parity" in error and "%" in error for error in errors) + + +def test_absolute_parity_above_quality_threshold_fails(tmp_path): + payload = _payload() + metrics = payload["configs"]["resize_basic"]["baselines"][_case()]["TESTSKU"] + metrics["gpu_time_us_cpp"] = DEFAULT_BENCHMARK_QUALITY.max_perf_diff_us * 100.0 + metrics["gpu_time_us_python"] = ( + metrics["gpu_time_us_cpp"] + DEFAULT_BENCHMARK_QUALITY.max_perf_diff_us + 1.0 + ) + + errors = _write_tree(tmp_path, payload) + + assert any("C++/Python parity" in error and "us" in error for error in errors) + + +def test_main_rejects_same_key_baseline_regression_against_ref( + tmp_path, monkeypatch, capsys +): + config_dir = _config_dir_with_ref_baseline( + tmp_path, + monkeypatch, + cpp_time_us=120.0, + python_time_us=125.0, + ) + + rc = validate_baselines.main( + [ + "--config-dir", + str(config_dir), + "--reject-regressions-from", + "origin/main", + ] + ) + + assert rc == 1 + captured = capsys.readouterr() + assert "same-key baseline regressed +20.00%" in captured.err + assert "origin/main" in captured.err + + +def test_main_allows_reviewed_same_key_baseline_regression_against_ref( + tmp_path, monkeypatch, capsys +): + config_dir = _config_dir_with_ref_baseline( + tmp_path, + monkeypatch, + cpp_time_us=120.0, + python_time_us=125.0, + ) + + assert ( + validate_baselines.main( + [ + "--config-dir", + str(config_dir), + "--reject-regressions-from", + "origin/main", + "--allow-regressions", + ] + ) + == 0 + ) + assert ( + "reviewed same-key baseline regressions are allowed" in capsys.readouterr().err + ) + + +def test_allow_regressions_requires_comparison_ref(tmp_path, capsys): + config_dir = _write_config_dir(tmp_path, _payload()) + + assert ( + validate_baselines.main( + ["--config-dir", str(config_dir), "--allow-regressions"] + ) + == 2 + ) + assert ( + "--allow-regressions requires --reject-regressions-from" + in capsys.readouterr().err + ) + + +def test_allow_regressions_rejects_invalid_comparison_ref( + tmp_path, monkeypatch, capsys +): + config_dir = _write_config_dir(tmp_path, _payload()) + + def reject_ref(ref): + raise validate_baselines.BaselineError( + f"baseline regression ref does not exist: {ref}" + ) + + monkeypatch.setattr(validate_baselines, "_verify_git_ref", reject_ref) + + assert ( + validate_baselines.main( + [ + "--config-dir", + str(config_dir), + "--reject-regressions-from", + "missing-ref", + "--allow-regressions", + ] + ) + == 2 + ) + assert ( + "baseline regression ref does not exist: missing-ref" in capsys.readouterr().err + ) + + +def test_allow_regressions_does_not_bypass_baseline_validation(tmp_path): + payload = _payload() + del payload["configs"]["resize_basic"]["baselines"][_case()]["TESTSKU"][ + "gpu_noise_us_cpp" + ] + config_dir = _write_config_dir(tmp_path, payload) + + assert ( + validate_baselines.main( + [ + "--config-dir", + str(config_dir), + "--reject-regressions-from", + "origin/main", + "--allow-regressions", + ] + ) + == 1 + ) + + +def test_main_accepts_same_key_baseline_change_within_ref_threshold( + tmp_path, monkeypatch +): + config_dir = _config_dir_with_ref_baseline( + tmp_path, + monkeypatch, + cpp_time_us=105.0, + python_time_us=115.0, + ) + + assert ( + validate_baselines.main( + [ + "--config-dir", + str(config_dir), + "--reject-regressions-from", + "origin/main", + ] + ) + == 0 + ) + + +def test_git_ref_commands_trust_only_the_repository_root(monkeypatch): + calls = [] + + def fake_run(command, **kwargs): + calls.append((command, kwargs)) + return SimpleNamespace( + returncode=0, + stdout=json.dumps(_payload()), + stderr="", + ) + + monkeypatch.setattr(validate_baselines.subprocess, "run", fake_run) + + validate_baselines._verify_git_ref("origin/main") + validate_baselines._operator_doc_at_ref( + "origin/main", + validate_baselines._repo_root() + / "bench" + / "config" + / "operators" + / "resize.json", + ) + + safe_directory = f"safe.directory={validate_baselines._repo_root()}" + assert len(calls) == 2 + assert all(command[:3] == ["git", "-c", safe_directory] for command, _ in calls) + assert all(kwargs["cwd"] == validate_baselines._repo_root() for _, kwargs in calls) + assert all( + kwargs["timeout"] == validate_baselines.GIT_COMMAND_TIMEOUT_SECONDS + for _, kwargs in calls + ) + + +@pytest.mark.parametrize("operation", ["verify", "show"]) +def test_git_ref_commands_report_timeouts(monkeypatch, operation): + def fake_run(command, **kwargs): + raise subprocess.TimeoutExpired( + cmd=command, + timeout=kwargs["timeout"], + ) + + monkeypatch.setattr(validate_baselines.subprocess, "run", fake_run) + + with pytest.raises(validate_baselines.BaselineError, match="timed out"): + if operation == "verify": + validate_baselines._verify_git_ref("origin/main") + else: + validate_baselines._operator_doc_at_ref( + "origin/main", + validate_baselines._repo_root() + / "bench" + / "config" + / "operators" + / "resize.json", + ) diff --git a/bench/tests/test_warmup_policy.py b/bench/tests/test_warmup_policy.py new file mode 100644 index 000000000..5c5d098c9 --- /dev/null +++ b/bench/tests/test_warmup_policy.py @@ -0,0 +1,126 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Unit tests for the shared C++/Python benchmark warmup policy.""" + +from __future__ import annotations + +import os +import shlex +import subprocess +from pathlib import Path + +import pytest + +import run_bench +from _internal.warmup import WARMUP_CAP_ENV, resolve_warmup_iterations + + +BENCH_DIR = Path(__file__).resolve().parents[1] + + +@pytest.fixture(scope="session") +def warmup_policy_probe(tmp_path_factory): + source = tmp_path_factory.mktemp("warmup-policy") / "probe.cpp" + binary = source.with_suffix("") + source.write_text( + """ +#include "cpp/WarmupPolicy.hpp" + +#include +#include + +int main(int argc, char **argv) +{ + try + { + std::cout << benchutils::resolve_warmup_iterations(std::stoi(argv[1])) << '\\n'; + return 0; + } + catch (const std::exception &err) + { + std::cerr << err.what() << '\\n'; + return 2; + } +} +""" + ) + compiler = shlex.split(os.environ.get("CXX", "c++")) + subprocess.run( + [*compiler, "-std=c++17", "-I", str(BENCH_DIR), str(source), "-o", str(binary)], + check=True, + capture_output=True, + text=True, + ) + return binary + + +def _run_probe(binary: Path, configured: int, cap: str | None): + env = os.environ.copy() + env.pop(WARMUP_CAP_ENV, None) + if cap is not None: + env[WARMUP_CAP_ENV] = cap + return subprocess.run( + [str(binary), str(configured)], + env=env, + capture_output=True, + text=True, + check=False, + ) + + +@pytest.mark.parametrize( + ("cap", "configured", "expected"), + [ + (None, 200, 200), + ("0", 200, 0), + ("50", 0, 0), + ("50", 20, 20), + ("50", 50, 50), + ("50", 51, 50), + ("50", 200, 50), + ("500", 200, 200), + ], +) +def test_cpp_and_python_resolve_warmups_identically( + warmup_policy_probe, cap, configured, expected +): + env = {} if cap is None else {WARMUP_CAP_ENV: cap} + assert resolve_warmup_iterations(configured, env) == expected + + result = _run_probe(warmup_policy_probe, configured, cap) + assert result.returncode == 0, result.stderr + assert int(result.stdout) == expected + + +@pytest.mark.parametrize("cap", ["", "-1", "abc", "1.5", "50junk", "2147483648"]) +def test_cpp_and_python_reject_the_same_invalid_caps(warmup_policy_probe, cap): + with pytest.raises(ValueError, match=WARMUP_CAP_ENV): + resolve_warmup_iterations(200, {WARMUP_CAP_ENV: cap}) + + result = _run_probe(warmup_policy_probe, 200, cap) + assert result.returncode == 2 + assert result.stdout == "" + assert WARMUP_CAP_ENV in result.stderr + + +class _Runner(run_bench.BenchmarkRunner): + def build_command(self, benchmark_path, extra_args, output_file, config_key=None): + return [] + + +def test_runner_propagates_cap_and_clears_stale_environment(monkeypatch, tmp_path): + monkeypatch.setenv(WARMUP_CAP_ENV, "999") + capped = _Runner("bench_", str(tmp_path), [], "cpp", warmup_cap=50) + uncapped = _Runner("bench_", str(tmp_path), [], "cpp") + + assert capped.benchmark_env()[WARMUP_CAP_ENV] == "50" + assert WARMUP_CAP_ENV not in uncapped.benchmark_env() + + +def test_python_runner_preserves_cap_while_extending_environment(tmp_path): + runner = object.__new__(run_bench.PythonBenchmarkRunner) + runner.bench_folder = str(tmp_path) + runner.warmup_cap = 50 + + assert runner.benchmark_env()[WARMUP_CAP_ENV] == "50" diff --git a/ci/build.sh b/build.sh similarity index 76% rename from ci/build.sh rename to build.sh index 674f418e4..aa6893e04 100755 --- a/ci/build.sh +++ b/build.sh @@ -1,6 +1,6 @@ #!/bin/bash -e -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -31,8 +31,13 @@ SDIR=$(dirname "$(readlink -f "$0")") # Defaults build_type="release" build_dir="" -source_dir="$SDIR/.." -num_jobs=$(nproc) # Automatically determines the number of CPU cores +source_dir="$SDIR" +num_jobs="${CVCUDA_BUILD_JOBS:-$(nproc)}" # Allow callers to cap build parallelism. + +if ! [[ "$num_jobs" =~ ^[1-9][0-9]*$ ]]; then + echo "Invalid CVCUDA_BUILD_JOBS='${CVCUDA_BUILD_JOBS:-}'; falling back to nproc." >&2 + num_jobs="$(nproc)" +fi # Command line parsing if [[ $# -ge 1 ]]; then @@ -67,6 +72,8 @@ case $build_type in debug) cmake_args="$cmake_args -DCMAKE_BUILD_TYPE=Debug" ;; + *) + ;; esac # Configure build toolchain @@ -88,13 +95,17 @@ if which ccache > /dev/null; then fi # Configure CUDA - use default installation -if [ -x "/usr/local/cuda/bin/nvcc" ]; then +if [[ -x "/usr/local/cuda/bin/nvcc" ]]; then cmake_args="$cmake_args -DCMAKE_CUDA_COMPILER=/usr/local/cuda/bin/nvcc" fi -# Create build tree and build -cmake -B "$build_dir" "$source_dir" $cmake_args -cmake --build "$build_dir" -- -j$num_jobs +# Generate requirements files from templates and versions.env +bash "$source_dir/generate_requirements.sh" + +# Create build tree and build. Forward the job budget so the Python wheel +# sub-builds can cap their combined fan-out (see cmake/BuildPython.cmake). +cmake -B "$build_dir" "$source_dir" $cmake_args -DCVCUDA_BUILD_JOBS="${num_jobs}" +cmake --build "$build_dir" -- "-j${num_jobs}" # Show ccache status if which ccache > /dev/null; then diff --git a/cmake/BuildPython.cmake b/cmake/BuildPython.cmake index 8504dec70..39bca55eb 100644 --- a/cmake/BuildPython.cmake +++ b/cmake/BuildPython.cmake @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -37,8 +37,6 @@ list(APPEND PYPROJ_COMMON_ARGS -DCMAKE_MODULE_PATH=${CMAKE_CURRENT_BINARY_DIR}/cmake -DCMAKE_LIBRARY_OUTPUT_DIRECTORY=${CMAKE_LIBRARY_OUTPUT_DIRECTORY} -DNVCV_TYPES_SOURCE_DIR=${NVCV_TYPES_SOURCE_DIR} - -DPYBIND11_SOURCE_DIR=${PYBIND11_SOURCE_DIR} - -DDLPACK_SOURCE_DIR=${DLPACK_SOURCE_DIR} -DWARNINGS_AS_ERRORS=${WARNINGS_AS_ERRORS} -DENABLE_COMPAT_OLD_GLIBC=${ENABLE_COMPAT_OLD_GLIBC} -DCMAKE_C_COMPILER=${CMAKE_C_COMPILER} @@ -55,6 +53,30 @@ if (CMAKE_CROSSCOMPILING) ) endif() +# The Python wheels build as concurrent ExternalProjects that each default to a +# full host-core Ninja, so their combined fan-out (versions x cores) can +# oversubscribe memory and stall or OOM large builds. Split the outer job budget +# (CVCUDA_BUILD_JOBS from build.sh; host cores otherwise) across them. +list(LENGTH PYTHON_VERSIONS _NUM_PY_VERSIONS) +if(DEFINED CVCUDA_BUILD_JOBS AND CVCUDA_BUILD_JOBS GREATER 0) + set(_PY_TOTAL_JOBS ${CVCUDA_BUILD_JOBS}) +else() + include(ProcessorCount) + ProcessorCount(_PY_TOTAL_JOBS) + if(_PY_TOTAL_JOBS EQUAL 0) + set(_PY_TOTAL_JOBS 1) + endif() +endif() +if(_NUM_PY_VERSIONS GREATER 0) + math(EXPR _PY_BUILD_JOBS "${_PY_TOTAL_JOBS} / ${_NUM_PY_VERSIONS}") +else() + set(_PY_BUILD_JOBS ${_PY_TOTAL_JOBS}) +endif() +if(_PY_BUILD_JOBS LESS 1) + set(_PY_BUILD_JOBS 1) +endif() +message(STATUS "Python wheel sub-builds: ${_NUM_PY_VERSIONS} x -j${_PY_BUILD_JOBS} (budget ${_PY_TOTAL_JOBS})") + foreach(VER ${PYTHON_VERSIONS}) set(BASEDIR ${CMAKE_CURRENT_BINARY_DIR}/python${VER}) @@ -66,31 +88,69 @@ foreach(VER ${PYTHON_VERSIONS}) TMP_DIR ${BASEDIR}/tmp STAMP_DIR ${BASEDIR}/stamp BUILD_ALWAYS true + BUILD_COMMAND ${CMAKE_COMMAND} --build --parallel ${_PY_BUILD_JOBS} DEPENDS nvcv_types cvcuda INSTALL_COMMAND "" ) endforeach() -if(CMAKE_BUILD_TYPE STREQUAL "Release") - set(PACKAGE_LIB_DIR ${CMAKE_BINARY_DIR}/python3/lib) +# Enum classes exposed under `cvcuda.`. pybind11_stubgen emits warnings and +# falls back to ugly default-value reprs like `` unless +# it knows where each enum class lives. Keep this list in sync with enums +# registered via py::enum_ under the cvcuda module. +set(CVCUDA_STUBGEN_ENUM_LOCATIONS + --enum-class-locations Border:cvcuda.Border + --enum-class-locations Interp:cvcuda.Interp + --enum-class-locations ThresholdType:cvcuda.ThresholdType + --enum-class-locations AdaptiveThresholdType:cvcuda.AdaptiveThresholdType + --enum-class-locations Remap:cvcuda.Remap + --enum-class-locations ChannelManip:cvcuda.ChannelManip + --enum-class-locations LabelMaskType:cvcuda.LabelMaskType + --enum-class-locations ThreadScope:cvcuda.ThreadScope + --enum-class-locations LABEL:cvcuda.LABEL + --enum-class-locations SIFT:cvcuda.SIFT + --enum-class-locations Matcher:cvcuda.Matcher + --enum-class-locations ConnectivityType:cvcuda.ConnectivityType +) - file(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/python3) - file(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/python3/lib) - file(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/python3/cvcuda) +# pybind11_stubgen must run against a Python that (a) can import the just-built +# cvcuda extension and (b) has pybind11-stubgen installed. Prefer the first +# version in PYTHON_VERSIONS (always paired with a pybind11-stubgen install in +# our Docker images), and fall back to the generic Python3 interpreter CMake +# discovered. Without this, CMake's `find_package(Python3)` may pick a newer +# system Python (e.g. /usr/bin/python3.12) that has no pybind11-stubgen and +# breaks the `wheel` target. +find_package(Python3 COMPONENTS Interpreter QUIET) +if(PYTHON_VERSIONS) + list(GET PYTHON_VERSIONS 0 _STUBGEN_PY_VER) + set(STUBGEN_PYTHON "python${_STUBGEN_PY_VER}") + unset(_STUBGEN_PY_VER) +elseif(Python3_FOUND) + set(STUBGEN_PYTHON ${Python3_EXECUTABLE}) +endif() - # Configure Python packaging files - configure_file("${CMAKE_CURRENT_SOURCE_DIR}/python/setup.py.in" "${CMAKE_BINARY_DIR}/python3/setup.py") - configure_file("${CMAKE_CURRENT_SOURCE_DIR}/python/pyproject.toml.in" "${CMAKE_BINARY_DIR}/python3/pyproject.toml") - configure_file("${CMAKE_CURRENT_SOURCE_DIR}/python/README.md.in" "${CMAKE_BINARY_DIR}/python3/README.md") - configure_file("${CMAKE_CURRENT_SOURCE_DIR}/python/MANIFEST.in" "${CMAKE_BINARY_DIR}/python3/MANIFEST.in") +if(STUBGEN_PYTHON) + add_custom_target(generate_stubs + COMMAND ${CMAKE_COMMAND} -E env + PYTHONPATH=${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/python + ${STUBGEN_PYTHON} -m pybind11_stubgen cvcuda + ${CVCUDA_STUBGEN_ENUM_LOCATIONS} + --output-dir ${CMAKE_BINARY_DIR}/python3 + COMMENT "Generating Python type stubs (dev target)" + VERBATIM + ) +endif() + +if(CMAKE_BUILD_TYPE STREQUAL "Release") + file(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/python3/cvcuda) # Configure __init__.py for each package with the appropriate module name # cvcuda: all types and operators in single module set(PACKAGE_NAME "cvcuda") set(EXTRA_IMPORTS " -# Explicitly export C API capsule (not included in 'import *' since it starts with _) -from ._cvcuda import _C_API # noqa: F401") +# Explicitly export private attributes not included in 'import *'. +from ._cvcuda import _C_API, _test # noqa: F401") configure_file("${CMAKE_CURRENT_SOURCE_DIR}/python/__init__.py.in" "${CMAKE_BINARY_DIR}/python3/cvcuda/__init__.py") # Install __init__.py files for package structure in Debian packages @@ -99,6 +159,20 @@ from ._cvcuda import _C_API # noqa: F401") DESTINATION ${CMAKE_INSTALL_LIBDIR}/python/cvcuda COMPONENT lib) +endif() + +if(CMAKE_BUILD_TYPE STREQUAL "Release" AND BUILD_PYTHON_WHEEL) + set(PACKAGE_LIB_DIR ${CMAKE_BINARY_DIR}/python3/lib) + + file(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/python3/lib) + + # Configure Python packaging files + configure_file("${CMAKE_CURRENT_SOURCE_DIR}/python/setup.py.in" "${CMAKE_BINARY_DIR}/python3/setup.py") + configure_file("${CMAKE_CURRENT_SOURCE_DIR}/python/pyproject.toml.in" "${CMAKE_BINARY_DIR}/python3/pyproject.toml") + configure_file("${CMAKE_CURRENT_SOURCE_DIR}/python/README.md.in" "${CMAKE_BINARY_DIR}/python3/README.md") + configure_file("${CMAKE_CURRENT_SOURCE_DIR}/python/MANIFEST.in" "${CMAKE_BINARY_DIR}/python3/MANIFEST.in") + configure_file("${CMAKE_CURRENT_SOURCE_DIR}/python/py.typed" "${CMAKE_BINARY_DIR}/python3/cvcuda/py.typed" COPYONLY) + add_custom_target(wheel ALL) foreach(VER ${PYTHON_VERSIONS}) @@ -112,6 +186,31 @@ from ._cvcuda import _C_API # noqa: F401") COMMAND sh -c "cp ${CMAKE_BINARY_DIR}/lib/python/_cvcuda*.so ${CMAKE_BINARY_DIR}/python3/cvcuda/" ) + # Ensure numpy is importable by the stubgen Python before pybind11_stubgen + # runs: stubgen imports cvcuda, and _cvcuda.so pulls numpy via pybind11's + # npy_api at module init. The manylinux :v11 builder image ships without + # numpy for cp3{10..14}, so without this the wheel target fails with + # "ModuleNotFoundError: No module named 'numpy'". On images that already + # have numpy (Ubuntu devel, Jetson edge venv) this is a fast no-op. + # TODO: drop this COMMAND once the manylinux builder image is rebuilt with + # numpy preinstalled (see docker/Dockerfile.builder.deps). + add_custom_command( + TARGET wheel + COMMAND sh -c "${STUBGEN_PYTHON} -c 'import numpy' >/dev/null 2>&1 || ${STUBGEN_PYTHON} -m pip install --quiet --disable-pip-version-check -r ${CMAKE_SOURCE_DIR}/tests/requirements.tests.numpy2.txt" + COMMENT "Ensuring numpy is available for pybind11_stubgen" + VERBATIM + ) + + add_custom_command( + TARGET wheel + COMMAND ${CMAKE_COMMAND} -E env PYTHONPATH=${CMAKE_BINARY_DIR}/python3 + ${STUBGEN_PYTHON} -m pybind11_stubgen cvcuda + ${CVCUDA_STUBGEN_ENUM_LOCATIONS} + --output-dir ${CMAKE_BINARY_DIR}/python3 + COMMENT "Generating Python type stubs for cvcuda" + VERBATIM + ) + add_custom_command( TARGET wheel COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/python/build_wheels.sh" "${CMAKE_BINARY_DIR}/python3" diff --git a/cmake/CUDAArchitecturePolicy.cmake b/cmake/CUDAArchitecturePolicy.cmake new file mode 100644 index 000000000..244c8fe8b --- /dev/null +++ b/cmake/CUDAArchitecturePolicy.cmake @@ -0,0 +1,232 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +include_guard(GLOBAL) + +# This must run before CUDA is enabled. Afterwards CMake populates +# CMAKE_CUDA_ARCHITECTURES even when neither the cache nor CUDAARCHS supplied it. +macro(cvcuda_detect_cuda_architecture_source) + if(DEFINED CMAKE_CUDA_ARCHITECTURES) + if(DEFINED CVCUDA_GENERATED_CUDA_ARCHITECTURES + AND "${CMAKE_CUDA_ARCHITECTURES}" STREQUAL "${CVCUDA_GENERATED_CUDA_ARCHITECTURES}") + set(_CVCUDA_CUDA_ARCHITECTURES_SOURCE "GENERATED") + else() + set(_CVCUDA_CUDA_ARCHITECTURES_SOURCE "EXPLICIT_CACHE") + endif() + elseif(NOT "$ENV{CUDAARCHS}" STREQUAL "") + set(_CVCUDA_CUDA_ARCHITECTURES_SOURCE "ENVIRONMENT") + else() + set(_CVCUDA_CUDA_ARCHITECTURES_SOURCE "GENERATED") + endif() +endmacro() + +function(cvcuda_configure_cuda_architecture_policy) + if(NOT DEFINED _CVCUDA_CUDA_ARCHITECTURES_SOURCE) + message(FATAL_ERROR "cvcuda_detect_cuda_architecture_source() must run before enabling CUDA") + endif() + + set(_CVCUDA_CANONICAL_COMPACT_X86_ARCHITECTURES) + if(ARCH_X86_64) + if(CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER_EQUAL "11.8") + list(APPEND _CVCUDA_CANONICAL_COMPACT_X86_ARCHITECTURES + 80-real # Ampere - ga100/Tesla; binary-compatible baseline for SM8.x + 90-real # Hopper - gh100/Tesla + ) + endif() + if(CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER_EQUAL "12.8") + list(APPEND _CVCUDA_CANONICAL_COMPACT_X86_ARCHITECTURES + 100-real # Blackwell B200, B300 + 120-real # RTX Pro 6000, RTX 50** + ) + endif() + list(APPEND _CVCUDA_CANONICAL_COMPACT_X86_ARCHITECTURES 75-real) + endif() + + if(_CVCUDA_CUDA_ARCHITECTURES_SOURCE STREQUAL "GENERATED") + set(_CVCUDA_CUDA_ARCHITECTURES) + + if(ARCH_X86_64) + set(_CVCUDA_CUDA_ARCHITECTURES + "${_CVCUDA_CANONICAL_COMPACT_X86_ARCHITECTURES}") + elseif(ARCH_AARCH64) + if(CVCUDA_AARCH64_JETSON) + # Jetson Orin platforms only. + list(APPEND _CVCUDA_CUDA_ARCHITECTURES + 86-real # IGX Orin + Ampere RTX A6000 + 87-real # Jetson AGX Orin - ga10b,ga10c/Tegra + 89-real # IGX Orin + Ada RTX 6000 + ) + else() + # SBSA server platforms (default). + list(APPEND _CVCUDA_CUDA_ARCHITECTURES + 80-real # Ampere - ga100/Tesla + 86-real # Ampere - ga10x + 89-real # Ada - ad10x + ) + if(CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER_EQUAL "11.8") + list(APPEND _CVCUDA_CUDA_ARCHITECTURES + 90-real # Grace Hopper - gh100/Tesla + ) + endif() + if(CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER_EQUAL "12.8") + list(APPEND _CVCUDA_CUDA_ARCHITECTURES + 100-real # Blackwell GB200, GB300 + ) + endif() + if(CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER_EQUAL "13.0") + list(APPEND _CVCUDA_CUDA_ARCHITECTURES + 110-real # Thor + 121-real # DGX Spark + ) + endif() + endif() + endif() + + if(ARCH_AARCH64 AND NOT CVCUDA_AARCH64_JETSON) + # Native Turing code avoids requiring a newer driver to JIT build-toolkit PTX. + list(APPEND _CVCUDA_CUDA_ARCHITECTURES 75-real) + endif() + + set(CMAKE_CUDA_ARCHITECTURES "${_CVCUDA_CUDA_ARCHITECTURES}" CACHE STRING + "CUDA architectures to build for" FORCE) + set(CVCUDA_GENERATED_CUDA_ARCHITECTURES "${_CVCUDA_CUDA_ARCHITECTURES}" CACHE INTERNAL + "CUDA architectures generated by CV-CUDA" FORCE) + elseif(_CVCUDA_CUDA_ARCHITECTURES_SOURCE STREQUAL "ENVIRONMENT") + # CUDAARCHS is a complete CMake architecture list, not a prefix for our defaults. + set(CMAKE_CUDA_ARCHITECTURES "$ENV{CUDAARCHS}" CACHE STRING + "CUDA architectures to build for" FORCE) + unset(CVCUDA_GENERATED_CUDA_ARCHITECTURES CACHE) + elseif(_CVCUDA_CUDA_ARCHITECTURES_SOURCE STREQUAL "EXPLICIT_CACHE") + # Preserve every explicit cache spelling, including OFF, FALSE, and 0. + unset(CVCUDA_GENERATED_CUDA_ARCHITECTURES CACHE) + else() + message(FATAL_ERROR + "Unknown CUDA architecture source: ${_CVCUDA_CUDA_ARCHITECTURES_SOURCE}") + endif() + + set(CVCUDA_TARGETED_SM8X_CUBINS "AUTO" CACHE STRING + "Build supplemental native SM86/SM89 operator cubins (AUTO, ON, or OFF)") + set_property(CACHE CVCUDA_TARGETED_SM8X_CUBINS PROPERTY STRINGS AUTO ON OFF) + string(TOUPPER "${CVCUDA_TARGETED_SM8X_CUBINS}" _CVCUDA_TARGETED_SM8X_CUBINS) + if(NOT _CVCUDA_TARGETED_SM8X_CUBINS MATCHES "^(AUTO|ON|OFF)$") + message(FATAL_ERROR + "CVCUDA_TARGETED_SM8X_CUBINS must be AUTO, ON, or OFF; got '${CVCUDA_TARGETED_SM8X_CUBINS}'") + endif() + set(CVCUDA_TARGETED_SM8X_CUBINS "${_CVCUDA_TARGETED_SM8X_CUBINS}" CACHE STRING + "Build supplemental native SM86/SM89 operator cubins (AUTO, ON, or OFF)" FORCE) + set_property(CACHE CVCUDA_TARGETED_SM8X_CUBINS PROPERTY STRINGS AUTO ON OFF) + + set(_CVCUDA_EFFECTIVE_ARCHITECTURES ${CMAKE_CUDA_ARCHITECTURES}) + set(_CVCUDA_CANONICAL_ARCHITECTURES ${_CVCUDA_CANONICAL_COMPACT_X86_ARCHITECTURES}) + list(REMOVE_DUPLICATES _CVCUDA_EFFECTIVE_ARCHITECTURES) + list(REMOVE_DUPLICATES _CVCUDA_CANONICAL_ARCHITECTURES) + list(SORT _CVCUDA_EFFECTIVE_ARCHITECTURES) + list(SORT _CVCUDA_CANONICAL_ARCHITECTURES) + if(ARCH_X86_64 + AND "${_CVCUDA_EFFECTIVE_ARCHITECTURES}" STREQUAL "${_CVCUDA_CANONICAL_ARCHITECTURES}") + set(_CVCUDA_USES_CANONICAL_COMPACT_X86_ARCHITECTURES ON) + else() + set(_CVCUDA_USES_CANONICAL_COMPACT_X86_ARCHITECTURES OFF) + endif() + + if(_CVCUDA_TARGETED_SM8X_CUBINS STREQUAL "ON" + OR (_CVCUDA_TARGETED_SM8X_CUBINS STREQUAL "AUTO" + AND _CVCUDA_USES_CANONICAL_COMPACT_X86_ARCHITECTURES)) + set(CVCUDA_TARGETED_SM8X_CUBINS_ACTIVE ON CACHE INTERNAL + "Whether supplemental native SM86/SM89 operator cubins are active" FORCE) + else() + set(CVCUDA_TARGETED_SM8X_CUBINS_ACTIVE OFF CACHE INTERNAL + "Whether supplemental native SM86/SM89 operator cubins are active" FORCE) + endif() + + # set(... CACHE) does not replace a same-named normal variable under CMP0126. + set(CMAKE_CUDA_ARCHITECTURES "${CMAKE_CUDA_ARCHITECTURES}" PARENT_SCOPE) + set(CVCUDA_TARGETED_SM8X_CUBINS_ACTIVE "${CVCUDA_TARGETED_SM8X_CUBINS_ACTIVE}" PARENT_SCOPE) +endfunction() + +# Appends SASS-only native SM86/SM89 code generation to selected CUDA sources. +# Recompiling the owning source with extra -gencode entries keeps its current +# host symbols and kernel implementations intact, while the CUDA runtime picks +# the exact native image on matching devices. SELECTED_SOURCES_VAR names the +# caller's already-filtered source list, so CV_CUDA_SRC_PATERN builds never +# compile supplemental code for an operator they did not select. +function(cvcuda_add_targeted_cuda_architectures_to_sources) + set(_CVCUDA_OPTIONS) + set(_CVCUDA_ONE_VALUE_ARGS SELECTED_SOURCES_VAR) + set(_CVCUDA_MULTI_VALUE_ARGS ARCHITECTURES SOURCES) + cmake_parse_arguments(PARSE_ARGV 0 CVCUDA_TARGETED + "${_CVCUDA_OPTIONS}" "${_CVCUDA_ONE_VALUE_ARGS}" "${_CVCUDA_MULTI_VALUE_ARGS}") + + if(CVCUDA_TARGETED_UNPARSED_ARGUMENTS) + message(FATAL_ERROR + "cvcuda_add_targeted_cuda_architectures_to_sources() got unknown arguments: " + "${CVCUDA_TARGETED_UNPARSED_ARGUMENTS}") + endif() + foreach(_CVCUDA_REQUIRED_ARG IN ITEMS ARCHITECTURES SOURCES SELECTED_SOURCES_VAR) + if(NOT CVCUDA_TARGETED_${_CVCUDA_REQUIRED_ARG}) + message(FATAL_ERROR + "cvcuda_add_targeted_cuda_architectures_to_sources() requires ${_CVCUDA_REQUIRED_ARG}") + endif() + endforeach() + if(NOT DEFINED ${CVCUDA_TARGETED_SELECTED_SOURCES_VAR}) + message(FATAL_ERROR + "cvcuda_add_targeted_cuda_architectures_to_sources() SELECTED_SOURCES_VAR names an undefined variable: " + "${CVCUDA_TARGETED_SELECTED_SOURCES_VAR}") + endif() + + set(_CVCUDA_TARGETED_ARCHITECTURES) + foreach(_CVCUDA_ARCH IN LISTS CVCUDA_TARGETED_ARCHITECTURES) + if(NOT "${_CVCUDA_ARCH}" MATCHES "^(86|89)(-real)?$") + message(FATAL_ERROR + "Targeted CUDA architecture must be 86, 86-real, 89, or 89-real; got '${_CVCUDA_ARCH}'") + endif() + string(REGEX REPLACE "-real$" "" _CVCUDA_ARCH "${_CVCUDA_ARCH}") + list(APPEND _CVCUDA_TARGETED_ARCHITECTURES "${_CVCUDA_ARCH}") + endforeach() + list(REMOVE_DUPLICATES _CVCUDA_TARGETED_ARCHITECTURES) + + # Resolve CMake's special architecture tokens before deciding which native + # images already apply globally. A bare numeric architecture produces both + # SASS and PTX, while a -real entry produces SASS. Only -virtual entries + # still need a supplemental native image. + set(_CVCUDA_RESOLVED_GLOBAL_ARCHITECTURES) + foreach(_CVCUDA_GLOBAL_ARCH IN LISTS CMAKE_CUDA_ARCHITECTURES) + if(_CVCUDA_GLOBAL_ARCH STREQUAL "all") + list(APPEND _CVCUDA_RESOLVED_GLOBAL_ARCHITECTURES ${CMAKE_CUDA_ARCHITECTURES_ALL}) + elseif(_CVCUDA_GLOBAL_ARCH STREQUAL "all-major") + list(APPEND _CVCUDA_RESOLVED_GLOBAL_ARCHITECTURES ${CMAKE_CUDA_ARCHITECTURES_ALL_MAJOR}) + elseif(_CVCUDA_GLOBAL_ARCH STREQUAL "native") + list(APPEND _CVCUDA_RESOLVED_GLOBAL_ARCHITECTURES ${CMAKE_CUDA_ARCHITECTURES_NATIVE}) + else() + list(APPEND _CVCUDA_RESOLVED_GLOBAL_ARCHITECTURES "${_CVCUDA_GLOBAL_ARCH}") + endif() + endforeach() + foreach(_CVCUDA_GLOBAL_ARCH IN LISTS _CVCUDA_RESOLVED_GLOBAL_ARCHITECTURES) + if(_CVCUDA_GLOBAL_ARCH MATCHES "^([0-9]+)(-real)?$") + list(REMOVE_ITEM _CVCUDA_TARGETED_ARCHITECTURES "${CMAKE_MATCH_1}") + endif() + endforeach() + + if(NOT CVCUDA_TARGETED_SM8X_CUBINS_ACTIVE OR NOT _CVCUDA_TARGETED_ARCHITECTURES) + return() + endif() + + foreach(_CVCUDA_SOURCE IN LISTS CVCUDA_TARGETED_SOURCES) + list(FIND ${CVCUDA_TARGETED_SELECTED_SOURCES_VAR} "${_CVCUDA_SOURCE}" _CVCUDA_SOURCE_INDEX) + if(_CVCUDA_SOURCE_INDEX EQUAL -1) + continue() + endif() + + get_property(_CVCUDA_SOURCE_OPTIONS SOURCE "${_CVCUDA_SOURCE}" PROPERTY COMPILE_OPTIONS) + foreach(_CVCUDA_ARCH IN LISTS _CVCUDA_TARGETED_ARCHITECTURES) + set(_CVCUDA_GENCODE_OPTION + "$<$:--generate-code=arch=compute_${_CVCUDA_ARCH},code=sm_${_CVCUDA_ARCH}>") + list(FIND _CVCUDA_SOURCE_OPTIONS "${_CVCUDA_GENCODE_OPTION}" _CVCUDA_OPTION_INDEX) + if(_CVCUDA_OPTION_INDEX EQUAL -1) + set_property(SOURCE "${_CVCUDA_SOURCE}" APPEND PROPERTY + COMPILE_OPTIONS "${_CVCUDA_GENCODE_OPTION}") + list(APPEND _CVCUDA_SOURCE_OPTIONS "${_CVCUDA_GENCODE_OPTION}") + endif() + endforeach() + endforeach() +endfunction() diff --git a/cmake/ConfigBuildTree.cmake b/cmake/ConfigBuildTree.cmake index 7b306ae45..6cfc4a823 100644 --- a/cmake/ConfigBuildTree.cmake +++ b/cmake/ConfigBuildTree.cmake @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -71,15 +71,18 @@ cmake_dependent_option(ENABLE_COMPAT_OLD_GLIBC "Generates binaries that work wit # Needed to get cuda version find_package(CUDAToolkit REQUIRED) -# Are we inside a git repo and it has submodules enabled? -if(EXISTS ${CMAKE_SOURCE_DIR}/.git AND EXISTS ${CMAKE_SOURCE_DIR}/.gitmodules) - if(NOT EXISTS ${CMAKE_SOURCE_DIR}/.git/modules) - message(FATAL_ERROR "git submodules not initialized. Did you forget to run 'git submodule update --init'?") - endif() -endif() if(PLATFORM_IS_LINUX) - set(CVCUDA_SYSTEM_NAME "${CMAKE_SYSTEM_PROCESSOR}-linux") + # Jetson-target builds get their own system token: they carry a different + # GPU arch set (Orin-only, see ConfigCUDA.cmake) and, when built on-device, + # the L4T/Tegra CUDA runtime - yet would otherwise be name-identical to + # SBSA aarch64 artifacts, colliding in the package registry and in CI + # artifact discovery. + if(CVCUDA_AARCH64_JETSON) + set(CVCUDA_SYSTEM_NAME "${CMAKE_SYSTEM_PROCESSOR}-jetson-linux") + else() + set(CVCUDA_SYSTEM_NAME "${CMAKE_SYSTEM_PROCESSOR}-linux") + endif() else() message(FATAL_ERROR "Unsupported platform: ${CMAKE_SYSTEM_NAME}. " "CV-CUDA only supports Linux platform.") diff --git a/cmake/ConfigCPack.cmake b/cmake/ConfigCPack.cmake index bfd1fe845..a3c70c77f 100644 --- a/cmake/ConfigCPack.cmake +++ b/cmake/ConfigCPack.cmake @@ -92,6 +92,10 @@ if(BUILD_TESTS OR BUILD_TESTS_CPP OR BUILD_TESTS_WHEELS OR BUILD_TESTS_PYTHON) include(InstallTests) endif() +if(BUILD_BENCH) + include(InstallBench) +endif() + if(BUILD_PYTHON) include(InstallPython) endif() diff --git a/cmake/ConfigCUDA.cmake b/cmake/ConfigCUDA.cmake index 0ec51d304..bc16a8ce3 100644 --- a/cmake/ConfigCUDA.cmake +++ b/cmake/ConfigCUDA.cmake @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -29,74 +29,22 @@ endif() set(CMAKE_CUDA_STANDARD ${CMAKE_CXX_STANDARD}) -# Compress kernels to generate smaller executables +# Compress kernels to generate smaller executables. NVCC supports compression +# modes starting with CUDA 12.8, so older supported toolkits retain the default. set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} -Xfatbin=--compress-all") +if(CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER_EQUAL "12.8") + set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} --compress-mode=size") +endif() # Enable device lambdas set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} --extended-lambda") -# see https://developer.nvidia.com/cuda-gpus -if(NOT USE_CMAKE_CUDA_ARCHITECTURES) - set(CMAKE_CUDA_ARCHITECTURES "$ENV{CUDAARCHS}") - - if(ARCH_X86_64) - if(CMAKE_CUDA_COMPILER_VERSION VERSION_LESS "13.0") - list(APPEND CMAKE_CUDA_ARCHITECTURES - 70-real # Volta - gv100/Tesla - ) - endif() - if(CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER_EQUAL "11.8") - list(APPEND CMAKE_CUDA_ARCHITECTURES - 75-real # Turing - tu10x/GeForce - 80-real # Ampere - ga100/Tesla - 86-real # Ampere - ga10x/GeForce - 89-real # Ada - ad102/GeForce - 90-real # Hopper - gh100/Tesla - ) - endif() - if(CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER_EQUAL "12.8") - list(APPEND CMAKE_CUDA_ARCHITECTURES - 100-real # Blackwell B200, B300 - 120-real # RTX Pro 6000, RTX 50** - ) - endif() - elseif(ARCH_AARCH64) - list(APPEND CMAKE_CUDA_ARCHITECTURES - 80-real # Ampere - ga100/Tesla - 86-real # Jetson IGX Orin with optional Ampere RTX A6000 - 87-real # Ampere - ga10b,ga10c/Tegra (Jetson AGX Orin) - ) - if(CMAKE_CUDA_COMPILER_VERSION VERSION_LESS "13.0") - list(APPEND CMAKE_CUDA_ARCHITECTURES - 72-real # Volta - gv11b/Tegra (Jetson AGX Xavier) - ) - endif() - if(CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER_EQUAL "11.8") - list(APPEND CMAKE_CUDA_ARCHITECTURES - 89-real # Jetson IGX Orin with optional RTX 6000 Ada - 90-real # Grace Hopper - gh100/Tesla - ) - endif() - if(CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER_EQUAL "12.8") - list(APPEND CMAKE_CUDA_ARCHITECTURES - 100-real # Blackwell GB200, GB300 - ) - endif() - if(CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER_EQUAL "13.0") - list(APPEND CMAKE_CUDA_ARCHITECTURES - 110-real # Thor - 121-real # DGX Spark - ) - endif() - endif() +# Compile multiple GPU architectures in parallel within each nvcc invocation. +# Cap at 4 threads per nvcc process to avoid oversaturating with outer build parallelism. +set(CVCUDA_NVCC_THREADS 4 CACHE STRING "Max threads per nvcc invocation for multi-arch builds") +set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} --threads ${CVCUDA_NVCC_THREADS}") - if(CMAKE_CUDA_COMPILER_VERSION VERSION_LESS "13.0") - # Required compute capability: - # * compute_70: fast fp16 support + PTX for forward compatibility - list(APPEND CMAKE_CUDA_ARCHITECTURES 70-virtual) - endif() +# see https://developer.nvidia.com/cuda-gpus +option(CVCUDA_AARCH64_JETSON "Build for Jetson Orin platforms only (aarch64)" OFF) - # We must set the cache to the correct values, or else cmake will write its default there, - # which is the old architecture supported by nvcc. We don't want that. - set(CMAKE_CUDA_ARCHITECTURES "${CMAKE_CUDA_ARCHITECTURES}" CACHE STRING "CUDA architectures to build for" FORCE) -endif() +cvcuda_configure_cuda_architecture_policy() diff --git a/cmake/ConfigCompiler.cmake b/cmake/ConfigCompiler.cmake index c0984eef1..002e6a2e4 100644 --- a/cmake/ConfigCompiler.cmake +++ b/cmake/ConfigCompiler.cmake @@ -17,8 +17,8 @@ set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO} -O3 -ggdb") set(CMAKE_C_FLAGS_RELWITHDEBINFO "${CMAKE_C_FLAGS_RELWITHDEBINFO} -O3 -ggdb") -# Use old behavior (before CMake 3.20) for Ninja DEPFILES generators -cmake_policy(SET CMP0116 OLD) +# Use new behavior (CMake 3.20+) for Ninja DEPFILES generators +cmake_policy(SET CMP0116 NEW) if(WARNINGS_AS_ERRORS) set(C_WARNING_ERROR_FLAG "-Werror") diff --git a/cmake/ConfigVersion.cmake b/cmake/ConfigVersion.cmake index f0e98a2f4..a8598e0fe 100644 --- a/cmake/ConfigVersion.cmake +++ b/cmake/ConfigVersion.cmake @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,6 +20,28 @@ get_filename_component(config_version_script_path ${CMAKE_CURRENT_LIST_FILE} PAT include(GetGitRevisionDescription) get_git_head_revision(GIT_REFSPEC REPO_COMMIT) +# When building from an exported source tree with no .git — the OSS release +# tarball, or a downloaded release archive — get_git_head_revision() +# returns a "*-NOTFOUND" sentinel instead of a hash, so the embedded +# _COMMIT would be bogus. Fall back to a COMMIT_SHA file written at the +# source-tree root by the release packaging, preserving in-binary commit +# provenance (and the version-commit test) without a repository present. +if(NOT REPO_COMMIT MATCHES "^[0-9a-fA-F]+$" AND EXISTS "${CMAKE_SOURCE_DIR}/COMMIT_SHA") + file(STRINGS "${CMAKE_SOURCE_DIR}/COMMIT_SHA" _repo_commit_lines LIMIT_COUNT 1) + string(STRIP "${_repo_commit_lines}" _repo_commit_file) + # Only ever embed a bare commit hash. Refusing anything else keeps build + # state (tokens, env dumps, build flags) from leaking into the in-binary + # _COMMIT through a malformed or polluted COMMIT_SHA file. + if(_repo_commit_file MATCHES "^[0-9a-fA-F]+$") + set(REPO_COMMIT "${_repo_commit_file}") + message(STATUS "REPO_COMMIT taken from COMMIT_SHA file (no .git present): ${REPO_COMMIT}") + else() + message(WARNING "Ignoring COMMIT_SHA file: contents are not a bare commit hash") + endif() + unset(_repo_commit_lines) + unset(_repo_commit_file) +endif() + set(PROJECT_VERSION "${PROJECT_VERSION}${PROJECT_VERSION_SUFFIX}") function(configure_version target LIBPREFIX incpath VERSION_FULL) diff --git a/cmake/InstallBench.cmake b/cmake/InstallBench.cmake new file mode 100644 index 000000000..d0782b8f9 --- /dev/null +++ b/cmake/InstallBench.cmake @@ -0,0 +1,30 @@ +# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +list(APPEND CPACK_COMPONENTS_ALL bench) + +# Enable bench package when benchmarks are being built +# This file is only included when BUILD_BENCH is ON +set(CPACK_COMPONENT_BENCH_DISABLED false) +set(CPACK_COMPONENT_BENCH_DISPLAY_NAME "Benchmarks") +set(CPACK_COMPONENT_BENCH_DESCRIPTION "NVIDIA CV-CUDA benchmark suite") +set(CPACK_COMPONENT_BENCH_GROUP internal) + +# Depend on current or any future ABI with same major version +set(CPACK_DEBIAN_BENCH_PACKAGE_DEPENDS "${CPACK_DEBIAN_LIB_PACKAGE_NAME} (>= ${NVCV_VERSION_API})") +set(CPACK_DEBIAN_BENCH_PACKAGE_NAME "cvcuda${PROJECT_VERSION_MAJOR}-bench") +set(CVCUDA_BENCH_FILE_NAME "cvcuda-bench-${CVCUDA_VERSION_BUILD}") +set(CPACK_DEBIAN_BENCH_FILE_NAME "${CVCUDA_BENCH_FILE_NAME}.deb") +set(CPACK_ARCHIVE_BENCH_FILE_NAME "${CVCUDA_BENCH_FILE_NAME}") diff --git a/cmake/InstallSamples.cmake b/cmake/InstallSamples.cmake index d185f0504..52d6bb9ab 100644 --- a/cmake/InstallSamples.cmake +++ b/cmake/InstallSamples.cmake @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,7 +15,7 @@ list(APPEND CPACK_COMPONENTS_ALL samples) -set(CPACK_COMPONENT_SAMPLES_DISABLED true) +set(CPACK_COMPONENT_SAMPLES_DISABLED false) set(CPACK_COMPONENT_SAMPLES_DISPLAY_NAME "Samples") set(CPACK_COMPONENT_SAMPLES_DESCRIPTION "NVIDIA CV-CUDA Samples") diff --git a/cmake/InstallTests.cmake b/cmake/InstallTests.cmake index 53eebb47f..ff6d23068 100644 --- a/cmake/InstallTests.cmake +++ b/cmake/InstallTests.cmake @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -27,6 +27,8 @@ if(UNIX) set(CPACK_DEBIAN_TESTS_PACKAGE_DEPENDS "${CPACK_DEBIAN_LIB_PACKAGE_NAME} (>= ${NVCV_VERSION_API})") # External dependencies set(CPACK_DEBIAN_TESTS_PACKAGE_DEPENDS "${CPACK_DEBIAN_TESTS_PACKAGE_DEPENDS},libssl3 | libssl1.1") + # CMake is supplied by the test toolchain and may not be owned by dpkg. + set(CPACK_DEBIAN_TESTS_PACKAGE_DEPENDS "${CPACK_DEBIAN_TESTS_PACKAGE_DEPENDS},python3") set(CPACK_DEBIAN_TESTS_PACKAGE_NAME "cvcuda${PROJECT_VERSION_MAJOR}-tests") diff --git a/cmake/PrintConfig.cmake b/cmake/PrintConfig.cmake index bb2a98e9e..756101ea9 100644 --- a/cmake/PrintConfig.cmake +++ b/cmake/PrintConfig.cmake @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -37,6 +37,7 @@ message(STATUS " BUILD_TESTS_PYTHON : ${BUILD_TESTS_PYTHON}") if(BUILD_PYTHON) message(STATUS " BUILD_PYTHON : ON") + message(STATUS " BUILD_PYTHON_WHEEL : ${BUILD_PYTHON_WHEEL}") message(STATUS " Python versions : ${PYTHON_VERSIONS}") else() message(STATUS " BUILD_PYTHON : OFF") @@ -138,6 +139,7 @@ message(STATUS " C Flags : ${CMAKE_C_FLAGS} ${CMAKE_C_FLAGS_${BUILD_TYPE message(STATUS "") message(STATUS " CUDA Compiler : ${CMAKE_CUDA_COMPILER} (${CMAKE_CUDA_COMPILER_VERSION})") message(STATUS " CUDA Arch : ${CMAKE_CUDA_ARCHITECTURES}") +message(STATUS " Targeted SM8.x cubins : ${CVCUDA_TARGETED_SM8X_CUBINS} (active: ${CVCUDA_TARGETED_SM8X_CUBINS_ACTIVE})") message(STATUS " CUDA flags : ${CMAKE_CUDA_FLAGS} ${CMAKE_CUDA_FLAGS_${BUILD_TYPE}}") message(STATUS " CUDA toolkit target dir : ${CUDAToolkit_TARGET_DIR}") message(STATUS "") diff --git a/cmake/VersionDef.h.in b/cmake/VersionDef.h.in index 535a729d4..99e6a1c96 100644 --- a/cmake/VersionDef.h.in +++ b/cmake/VersionDef.h.in @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -45,7 +45,7 @@ extern "C" { * @returns The numeric version representation. */ #define @LIBPREFIX@_MAKE_VERSION4(major, minor, patch, tweak) \ - ((uint32_t)((major)*1000000 + (minor)*10000 + (patch)*100 + (tweak))) + ((major)*1000000 + (minor)*10000 + (patch)*100 + (tweak)) /** Make a @LIBPREFIX@ version identifier with three components. * diff --git a/docker/Dockerfile.builder.deps b/docker/Dockerfile.builder.deps index 30f5f4d00..1856640ed 100644 --- a/docker/Dockerfile.builder.deps +++ b/docker/Dockerfile.builder.deps @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,7 @@ ## Build CV-CUDA builder dependencies on top of manylinux_2_28 ## CV-CUDA builder images are based on manylinux_2_28, official page https://github.com/pypa/manylinux ######################################################################################### -ARG FROM_IMAGE_NAME=quay.io/pypa/manylinux_2_28_x86_64:latest +ARG FROM_IMAGE_NAME=quay.io/pypa/manylinux_2_28_x86_64:2026.05.27-1 ARG CUDA_IMAGE=scratch ARG BUILDER_EXTRA_DEPS=scratch ARG BUILDER_CUDA_EXTRA_DEPS=scratch @@ -26,49 +26,70 @@ FROM ${BUILDER_EXTRA_DEPS:-scratch} AS extra_deps FROM ${CUDA_IMAGE:-scratch} AS cuda FROM ${FROM_IMAGE_NAME:-quay.io/pypa/manylinux_2_28_x86_64:latest} +ARG PYTHON_VERSIONS="3.10 3.11 3.12 3.13 3.14" ARG CMAKE_VERSION=3.24.3 +ARG CCACHE_VERSION=4.13.6 +ARG CCACHE_SHA256_X86_64=09e0547a0c3b250a76675c33130366f1399f3580842fb360c052520d56214ead +ARG CCACHE_SHA256_AARCH64=bff0e0c19165db8627c85c36b0885b3b180659eda67a028298d26565aad52f56 ARG CC=gcc ARG CXX=g++ # Enable gcc-toolset-10 (AlmaLinux 8 / manylinux_2_28) ENV PATH=/opt/rh/gcc-toolset-10/root/usr/bin:${PATH} \ LD_LIBRARY_PATH=/opt/rh/gcc-toolset-10/root/usr/lib64:/opt/rh/gcc-toolset-10/root/usr/lib:${LD_LIBRARY_PATH:-} \ - MANPATH=/opt/rh/gcc-toolset-10/root/usr/share/man:${MANPATH:-} + MANPATH=/opt/rh/gcc-toolset-10/root/usr/share/man # Detect architecture and install CMake -RUN export CMAKE_VERSION=${CMAKE_VERSION} \ +RUN export CMAKE_VERSION="${CMAKE_VERSION}" \ && ARCH=$(uname -m) \ && echo "Detected architecture: ${ARCH}" \ && echo "Downloading CMake ${CMAKE_VERSION} for ${ARCH}..." \ - && wget https://github.com/Kitware/CMake/releases/download/v${CMAKE_VERSION}/cmake-${CMAKE_VERSION}-linux-${ARCH}.sh \ - -O /tmp/cmake-install.sh \ + && curl --fail --location --proto '=https' --proto-redir '=https' --tlsv1.2 \ + "https://github.com/Kitware/CMake/releases/download/v${CMAKE_VERSION}/cmake-${CMAKE_VERSION}-linux-${ARCH}.sh" \ + --output /tmp/cmake-install.sh \ && echo "Download completed, verifying file..." \ && ls -lh /tmp/cmake-install.sh \ && chmod u+x /tmp/cmake-install.sh \ - && mkdir -p /opt/cmake-${CMAKE_VERSION} \ + && mkdir -p "/opt/cmake-${CMAKE_VERSION}" \ && echo "Running CMake installer..." \ - && sh /tmp/cmake-install.sh --skip-license --prefix=/opt/cmake-${CMAKE_VERSION} \ + && sh /tmp/cmake-install.sh --skip-license --prefix="/opt/cmake-${CMAKE_VERSION}" \ && echo "CMake installation completed" \ && rm -f /tmp/cmake-install.sh \ && rm -f /usr/local/bin/*cmake* \ && rm -f /usr/local/bin/cpack \ && rm -f /usr/local/bin/ctest \ - && for file in /opt/cmake-${CMAKE_VERSION}/bin/*; do \ + && for file in "/opt/cmake-${CMAKE_VERSION}"/bin/*; do \ if [ -f "$file" ]; then \ ln -s "$file" /usr/local/bin/; \ fi; \ - done + done \ + && yum install -y epel-release almalinux-release-devel \ + && yum install -y --enablerepo=powertools,devel \ + git-lfs openssl-devel openssl-static \ + ninja-build dejavu-sans-fonts -# build dependencies -RUN curl -s https://packagecloud.io/install/repositories/github/git-lfs/script.rpm.sh | bash \ - && yum install -y epel-release almalinux-release-devel \ - && yum install -y --enablerepo=powertools,devel \ - git-lfs openssl-devel openssl-static \ - ninja-build ccache dejavu-sans-fonts +# Install ccache from upstream static binaries; distro packages are ccache 3.x, +# which lacks nvcc support (added in ccache 4.4) +RUN ARCH=$(uname -m) \ + && case "$ARCH" in \ + x86_64) CCACHE_SHA256="$CCACHE_SHA256_X86_64" ;; \ + aarch64) CCACHE_SHA256="$CCACHE_SHA256_AARCH64" ;; \ + *) echo "Unsupported architecture: $ARCH" >&2 && exit 1 ;; \ + esac \ + && curl --fail --location --proto '=https' --proto-redir '=https' --tlsv1.2 \ + "https://github.com/ccache/ccache/releases/download/v${CCACHE_VERSION}/ccache-${CCACHE_VERSION}-linux-${ARCH}-musl-static.tar.gz" \ + --output /tmp/ccache.tar.gz \ + && echo "${CCACHE_SHA256} /tmp/ccache.tar.gz" | sha256sum -c - \ + && tar -xzf /tmp/ccache.tar.gz -C /tmp \ + && install -m 755 "/tmp/ccache-${CCACHE_VERSION}-linux-${ARCH}-musl-static/ccache" /usr/local/bin/ccache \ + && rm -rf /tmp/ccache.tar.gz "/tmp/ccache-${CCACHE_VERSION}-linux-${ARCH}-musl-static" \ + && ccache --version # Build and install zlib 1.3.1 from source to replace vulnerable system version (CVE-2018-25032) # Remove any old system zlib artifacts and install new version to /usr/local -RUN curl -L https://github.com/madler/zlib/releases/download/v1.3.1/zlib-1.3.1.tar.gz -o /tmp/zlib-1.3.1.tar.gz \ +RUN curl --fail --location --proto '=https' --proto-redir '=https' --tlsv1.2 \ + https://github.com/madler/zlib/releases/download/v1.3.1/zlib-1.3.1.tar.gz \ + --output /tmp/zlib-1.3.1.tar.gz \ && tar -xzf /tmp/zlib-1.3.1.tar.gz -C /tmp \ && cd /tmp/zlib-1.3.1 \ && ./configure --prefix=/usr/local \ @@ -78,18 +99,38 @@ RUN curl -L https://github.com/madler/zlib/releases/download/v1.3.1/zlib-1.3.1.t && rm -rf /tmp/zlib-1.3.1* \ && ldconfig -# Python -ARG PYVER=3.9 -ARG PYV=39 +# Install dlpack headers and CMake config from source (header-only library) +RUN git clone --depth 1 --branch v1.3 https://github.com/dmlc/dlpack.git /tmp/dlpack \ + && cmake -S /tmp/dlpack -B /tmp/dlpack/build -DBUILD_MOCK=OFF -DBUILD_DOCS=OFF \ + && cmake --install /tmp/dlpack/build --prefix /usr/local \ + && rm -rf /tmp/dlpack + +# Install googletest from source (not available as a recent package on manylinux/AlmaLinux 8) +RUN git clone --depth 1 --branch v1.14.0 https://github.com/google/googletest.git /tmp/googletest \ + && cmake -S /tmp/googletest -B /tmp/googletest/build \ + -DBUILD_GMOCK=ON -DINSTALL_GTEST=ON -DCMAKE_POSITION_INDEPENDENT_CODE=ON \ + && cmake --build /tmp/googletest/build --parallel $(nproc) \ + && cmake --install /tmp/googletest/build --prefix /usr/local \ + && rm -rf /tmp/googletest + +# Python — PYTHON_VERSIONS controls which versions are set up (same pattern as Dockerfile.devel.deps) +# PYVER/PYV select the default Python symlinked to /opt/python/v +# NOTE: Update the ENV PATH/LD_LIBRARY_PATH/LIBRARY_PATH below when changing PYTHON_VERSIONS +ARG PYVER=3.10 +ARG PYV=310 ENV PYVER=${PYVER} PYV=${PYV} PYTHONPATH=/opt/python/v ENV PYBIN=${PYTHONPATH}/bin \ PYLIB=${PYTHONPATH}/lib -ENV PATH=/opt/python/cp39-cp39/bin:/opt/python/cp310-cp310/bin:/opt/python/cp311-cp311/bin:/opt/python/cp312-cp312/bin:/opt/python/cp313-cp313/bin:/opt/python/cp313-cp313t/bin:/opt/python/cp314-cp314/bin:${PYBIN}:${PATH} \ - LD_LIBRARY_PATH=/usr/local/lib:/opt/python/cp39-cp39/lib:/opt/python/cp310-cp310/lib:/opt/python/cp311-cp311/lib:/opt/python/cp312-cp312/lib:/opt/python/cp313-cp313/lib:/opt/python/cp313-cp313t/lib:/opt/python/cp314-cp314/lib:${PYLIB}:${LD_LIBRARY_PATH:-} \ - LIBRARY_PATH=/usr/local/lib:/opt/python/cp39-cp39/lib:/opt/python/cp310-cp310/lib:/opt/python/cp311-cp311/lib:/opt/python/cp312-cp312/lib:/opt/python/cp313-cp313/lib:/opt/python/cp313-cp313t/lib:/opt/python/cp314-cp314/lib:${PYLIB}:${LIBRARY_PATH:-} +# Manylinux Python paths — must list all versions from PYTHON_VERSIONS +# Free-threaded variants (cp313t, cp314t) are intentionally excluded; add them here +# and to PYTHON_VERSIONS if free-threaded support is needed in the future. +ENV PATH=/opt/python/cp310-cp310/bin:/opt/python/cp311-cp311/bin:/opt/python/cp312-cp312/bin:/opt/python/cp313-cp313/bin:/opt/python/cp314-cp314/bin:${PYBIN}:${PATH} \ + LD_LIBRARY_PATH=/usr/local/cuda/lib64:/usr/local/lib:/opt/python/cp310-cp310/lib:/opt/python/cp311-cp311/lib:/opt/python/cp312-cp312/lib:/opt/python/cp313-cp313/lib:/opt/python/cp314-cp314/lib:${PYLIB}:${LD_LIBRARY_PATH:-} \ + LIBRARY_PATH=/usr/local/cuda/lib64:/usr/local/lib:/opt/python/cp310-cp310/lib:/opt/python/cp311-cp311/lib:/opt/python/cp312-cp312/lib:/opt/python/cp313-cp313/lib:/opt/python/cp314-cp314/lib:${PYLIB} +# Symlink default Python version RUN echo "Looking for Python directories in /opt/python:" && \ ls -la /opt/python/ && \ echo "Searching for cp${PYV}* pattern:" && \ @@ -100,7 +141,8 @@ RUN echo "Looking for Python directories in /opt/python:" && \ ln -s "$PYTHON_DIR" /opt/python/v; \ else \ echo "No Python directory found matching cp${PYV}*, trying alternative patterns..." && \ - PYTHON_DIR=$(find /opt/python -name "*python*" \( -type d -o -type l \) | grep -E "cp${PYV}|python${PYV}" | head -1) && \ + PYTHON_DIR=$(find /opt/python -name "*python*" \( -type d -o -type l \) \ + | grep -E "cp${PYV}|python${PYV}" | head -1) && \ if [ -n "$PYTHON_DIR" ]; then \ echo "Found alternative Python directory: $PYTHON_DIR" && \ ln -s "$PYTHON_DIR" /opt/python/v; \ @@ -112,13 +154,93 @@ RUN echo "Looking for Python directories in /opt/python:" && \ fi; \ fi -# install pip packages needed only for the system python -COPY requirements.sys_python.txt /tmp/requirements.sys_python.txt -RUN python -m pip install --break-system-packages --force-reinstall -r /tmp/requirements.sys_python.txt && \ - rm -rf /root/.cache/pip /tmp/requirements.sys_python.txt +# install build/packaging/linting packages for the system python +COPY requirements.build.sys_python.txt /tmp/requirements.build.sys_python.txt +RUN --mount=type=cache,target=/root/.cache/pip \ + python -m pip install --break-system-packages --force-reinstall -r /tmp/requirements.build.sys_python.txt && \ + rm -rf /tmp/requirements.build.sys_python.txt + +# install documentation packages for the system python +COPY requirements.docs.txt /tmp/requirements.docs.txt +RUN --mount=type=cache,target=/root/.cache/pip \ + python -m pip install --break-system-packages --force-reinstall -r /tmp/requirements.docs.txt && \ + rm -rf /tmp/requirements.docs.txt + +# install build dependencies for all specified Python versions (needed for CMake find_package) +COPY requirements.build.all_pythons.txt /tmp/requirements.build.all_pythons.txt +RUN --mount=type=cache,target=/root/.cache/pip \ + set -e; \ + for pyver in $PYTHON_VERSIONS; do \ + cpver="cp$(echo "$pyver" | tr -d '.')"; \ + pydir="/opt/python/${cpver}-${cpver}"; \ + if [ -d "$pydir" ]; then \ + echo "Installing build packages for Python $pyver ($pydir)"; \ + "$pydir/bin/pip" install -r /tmp/requirements.build.all_pythons.txt; \ + else \ + echo "ERROR: No manylinux directory found at $pydir" && exit 1; \ + fi; \ + done && \ + rm -f /tmp/requirements.build.all_pythons.txt + +# install test dependencies for all specified Python versions +COPY requirements.tests.common.txt /tmp/requirements.tests.common.txt +RUN --mount=type=cache,target=/root/.cache/pip \ + set -e; \ + for pyver in $PYTHON_VERSIONS; do \ + cpver="cp$(echo "$pyver" | tr -d '.')"; \ + pydir="/opt/python/${cpver}-${cpver}"; \ + if [ -d "$pydir" ]; then \ + echo "Installing test packages for Python $pyver ($pydir)"; \ + "$pydir/bin/pip" install -r /tmp/requirements.tests.common.txt; \ + else \ + echo "ERROR: No manylinux directory found at $pydir" && exit 1; \ + fi; \ + done && \ + rm -f /tmp/requirements.tests.common.txt + +# install numpy for all specified Python versions +# - Needed at wheel-build time: pybind11_stubgen imports cvcuda, and +# _cvcuda.so pulls numpy via pybind11's npy_api at module init. +# - At end-user runtime the wheel declares numpy as an install_requires +# so pip pulls it transparently; this install is purely build-time. +COPY requirements.tests.numpy2.txt /tmp/requirements.tests.numpy.txt +RUN --mount=type=cache,target=/root/.cache/pip \ + set -e; \ + for pyver in $PYTHON_VERSIONS; do \ + cpver="cp$(echo "$pyver" | tr -d '.')"; \ + pydir="/opt/python/${cpver}-${cpver}"; \ + if [ -d "$pydir" ]; then \ + echo "Installing numpy for Python $pyver ($pydir)"; \ + "$pydir/bin/pip" install -r /tmp/requirements.tests.numpy.txt; \ + else \ + echo "ERROR: No manylinux directory found at $pydir" && exit 1; \ + fi; \ + done && \ + rm -f /tmp/requirements.tests.numpy.txt RUN ldconfig +# Verify critical packages are installed and importable for all Python versions +ARG PYTHON_VERSIONS +RUN set -e; \ + for pyver in $PYTHON_VERSIONS; do \ + cpver="cp$(echo "$pyver" | tr -d '.')"; \ + pydir="/opt/python/${cpver}-${cpver}"; \ + if [ ! -d "$pydir" ]; then \ + echo "VERIFY FAILED: No directory found at $pydir" && exit 1; \ + fi; \ + echo "Verifying Python $pyver ($pydir)..."; \ + "$pydir/bin/python" -c "import pybind11; print(f' pybind11 {pybind11.__version__}')" || \ + { echo "VERIFY FAILED: pybind11 not importable for Python $pyver" && exit 1; }; \ + "$pydir/bin/python" -c "import pybind11_stubgen" || \ + { echo "VERIFY FAILED: pybind11_stubgen not importable for Python $pyver" && exit 1; }; \ + "$pydir/bin/python" -c "import pytest; print(f' pytest {pytest.__version__}')" || \ + { echo "VERIFY FAILED: pytest not importable for Python $pyver" && exit 1; }; \ + "$pydir/bin/python" -c "import numpy; print(f' numpy {numpy.__version__}')" || \ + { echo "VERIFY FAILED: numpy not importable for Python $pyver" && exit 1; }; \ + done; \ + echo "All Python versions verified successfully" + # extra deps COPY --from=extra_deps / / @@ -130,5 +252,23 @@ ENV NVIDIA_DRIVER_CAPABILITIES=video,compute,utility # CUDA COPY --from=cuda /usr/local/cuda /usr/local/cuda +# Install cupy and CUDA Python test dependencies from requirements file +# Uses ${cpver}-${cpver} pattern to match standard builds only (excludes free-threaded cp3XXt) +COPY requirements.tests.cu12.txt requirements.tests.cu13.txt /tmp/ +RUN --mount=type=cache,target=/root/.cache/pip \ + CUDA_MAJOR=$(/usr/local/cuda/bin/nvcc --version | grep -oP 'release \K[0-9]+') && \ + set -e; \ + for pyver in $PYTHON_VERSIONS; do \ + cpver="cp$(echo "$pyver" | tr -d '.')"; \ + pydir="/opt/python/${cpver}-${cpver}"; \ + if [ -d "$pydir" ]; then \ + echo "Installing cupy test deps for Python $pyver ($pydir)"; \ + "$pydir/bin/pip" install -r "/tmp/requirements.tests.cu${CUDA_MAJOR}.txt"; \ + else \ + echo "WARNING: No manylinux directory found at $pydir, skipping"; \ + fi; \ + done && \ + rm -f /tmp/requirements.tests.cu*.txt + # extra deps COPY --from=cuda_extra_deps / / diff --git a/docker/Dockerfile.cuda12.5.0.deps b/docker/Dockerfile.cuda12.5.0.deps deleted file mode 100644 index 737299934..000000000 --- a/docker/Dockerfile.cuda12.5.0.deps +++ /dev/null @@ -1,39 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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. - -ARG TOOLKIT_BASE_IMAGE=ubuntu:22.04 -FROM ${TOOLKIT_BASE_IMAGE:-ubuntu:22.04} AS cuda - -ENV DEBIAN_FRONTEND=noninteractive - -RUN apt update && apt install -y libxml2 curl perl gcc && \ - rm -rf /var/lib/apt/lists/* - -# Detect architecture and download appropriate CUDA installer -RUN arch=$(dpkg --print-architecture) && \ - case "${arch}" in \ - amd64) \ - cuda_arch_suffix="linux";; \ - arm64) \ - cuda_arch_suffix="linux_sbsa";; \ - *) \ - echo >&2 "error: unsupported architecture ('${arch}')"; \ - exit 1;; \ - esac && \ - echo "Downloading CUDA 12.5.0 for ${arch} (suffix: ${cuda_arch_suffix})" && \ - curl -LO "https://developer.download.nvidia.com/compute/cuda/12.5.0/local_installers/cuda_12.5.0_555.42.02_${cuda_arch_suffix}.run" && \ - chmod +x cuda_*.run && \ - ./cuda_*.run --silent --no-opengl-libs --toolkit && \ - rm -f cuda_*.run diff --git a/docker/Dockerfile.cuda12.9.0.deps b/docker/Dockerfile.cuda12.9.0.deps deleted file mode 100644 index 9700e6a06..000000000 --- a/docker/Dockerfile.cuda12.9.0.deps +++ /dev/null @@ -1,39 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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. - -ARG TOOLKIT_BASE_IMAGE=ubuntu:22.04 -FROM ${TOOLKIT_BASE_IMAGE:-ubuntu:22.04} AS cuda - -ENV DEBIAN_FRONTEND=noninteractive - -RUN apt update && apt install -y libxml2 curl perl gcc && \ - rm -rf /var/lib/apt/lists/* - -# Detect architecture and download appropriate CUDA installer -RUN arch=$(dpkg --print-architecture) && \ - case "${arch}" in \ - amd64) \ - cuda_arch_suffix="linux";; \ - arm64) \ - cuda_arch_suffix="linux_sbsa";; \ - *) \ - echo >&2 "error: unsupported architecture ('${arch}')"; \ - exit 1;; \ - esac && \ - echo "Downloading CUDA 12.9.0 for ${arch} (suffix: ${cuda_arch_suffix})" && \ - curl -LO "https://developer.download.nvidia.com/compute/cuda/12.9.0/local_installers/cuda_12.9.0_575.51.03_${cuda_arch_suffix}.run" && \ - chmod +x cuda_*.run && \ - ./cuda_*.run --silent --no-opengl-libs --toolkit && \ - rm -f cuda_*.run diff --git a/docker/Dockerfile.cuda13.0.1.deps b/docker/Dockerfile.cuda13.0.1.deps deleted file mode 100644 index 12d441543..000000000 --- a/docker/Dockerfile.cuda13.0.1.deps +++ /dev/null @@ -1,39 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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. - -ARG TOOLKIT_BASE_IMAGE=ubuntu:22.04 -FROM ${TOOLKIT_BASE_IMAGE:-ubuntu:22.04} AS cuda - -ENV DEBIAN_FRONTEND=noninteractive - -RUN apt update && apt install -y libxml2 curl perl gcc && \ - rm -rf /var/lib/apt/lists/* - -# Detect architecture and download appropriate CUDA installer -RUN arch=$(dpkg --print-architecture) && \ - case "${arch}" in \ - amd64) \ - cuda_arch_suffix="linux";; \ - arm64) \ - cuda_arch_suffix="linux_sbsa";; \ - *) \ - echo >&2 "error: unsupported architecture ('${arch}')"; \ - exit 1;; \ - esac && \ - echo "Downloading CUDA 13.0.1 for ${arch} (suffix: ${cuda_arch_suffix})" && \ - curl -LO "https://developer.download.nvidia.com/compute/cuda/13.0.1/local_installers/cuda_13.0.1_580.82.07_${cuda_arch_suffix}.run" && \ - chmod +x cuda_*.run && \ - ./cuda_*.run --silent --no-opengl-libs --toolkit && \ - rm -f cuda_*.run diff --git a/docker/Dockerfile.devel.deps b/docker/Dockerfile.devel.deps index 8fefae782..8b70634ab 100644 --- a/docker/Dockerfile.devel.deps +++ b/docker/Dockerfile.devel.deps @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,13 +16,17 @@ ARG BASE=nvidia/cuda:12.5.0-devel-ubuntu22.04 FROM ${BASE:-nvidia/cuda:12.5.0-devel-ubuntu22.04} -ARG VER_CUDA=12.9.0 +ARG VER_CUDA=12.5.0 ARG VER_NUMPY_MAJOR=2 -ARG VER_TORCH=2.8.0 -ARG TORCH_ADDITIONAL_INDEX_URL="" +ARG TORCH_CUDA_SUFFIX=cu12 -ARG PYTHON_VERSIONS="3.9 3.10 3.11 3.12 3.13 3.14" -ARG CMAKE_VERSION=3.24.3 +ARG PYTHON_VERSIONS="3.10 3.11 3.12 3.13 3.14" +ARG CMAKE_VERSION=3.31.1 +ARG CCACHE_VERSION=4.13.6 +ARG CCACHE_SHA256_X86_64=09e0547a0c3b250a76675c33130366f1399f3580842fb360c052520d56214ead +ARG CCACHE_SHA256_AARCH64=bff0e0c19165db8627c85c36b0885b3b180659eda67a028298d26565aad52f56 +ARG NVBENCH_COMMIT=92d2e01cd1ec32ecb914888a8b3374923d818165 +ARG RAPIDS_CMAKE_SHA=edc8773f660e15fa2948324df5dc1a1bb7d9d1cc ARG DEBIAN_FRONTEND=noninteractive @@ -33,22 +37,15 @@ ENV PYTHONUNBUFFERED=1 # Install basic packages first (needed for add-apt-repository) RUN apt-get update && \ apt-get install -y --no-install-recommends \ - software-properties-common \ - && rm -rf /var/lib/apt/lists/* - -RUN apt-get update && \ - apt-get install -y --no-install-recommends \ - lsb-release \ ca-certificates \ gnupg \ + lsb-release \ + software-properties-common \ + && add-apt-repository -y ppa:apt-fast/stable \ + && apt-get update \ + && apt-get install -y --no-install-recommends apt-fast \ && rm -rf /var/lib/apt/lists/* -# add apt-fast -RUN add-apt-repository -y ppa:apt-fast/stable && \ - apt-get update && \ - apt-get install -y --no-install-recommends apt-fast && \ - rm -rf /var/lib/apt/lists/* - # Install system packages in smaller chunks to avoid QEMU issues RUN apt-fast update && apt-fast install -y --no-install-recommends \ @@ -59,10 +56,14 @@ RUN apt-fast update && apt-fast install -y --no-install-recommends \ git-lfs \ && rm -rf /var/lib/apt/lists/* -# Install GCC packages - one version per RUN command for better isolation and caching -RUN apt-fast update && \ - apt-fast install -y --no-install-recommends gcc-10 g++-10 \ - && rm -rf /var/lib/apt/lists/* +# Install GCC packages - one version per RUN command for better isolation and caching. +# GCC 10 is not available on Ubuntu 26.04, so keep it only on the Ubuntu 22.04 lane. +RUN UBUNTU_VERSION=$(lsb_release -rs) && \ + if [ "$UBUNTU_VERSION" = "22.04" ]; then \ + apt-fast update && \ + apt-fast install -y --no-install-recommends gcc-10 g++-10 && \ + rm -rf /var/lib/apt/lists/*; \ + fi RUN apt-fast update && \ apt-fast install -y --no-install-recommends gcc-11 g++-11 \ @@ -72,27 +73,29 @@ RUN apt-fast update && \ apt-fast install -y --no-install-recommends gcc-12 g++-12 \ && rm -rf /var/lib/apt/lists/* -# need to add ppa for gcc-13 and gcc-14 -RUN add-apt-repository ppa:ubuntu-toolchain-r/ppa -y && \ - add-apt-repository ppa:ubuntu-toolchain-r/test -y - - -RUN apt-fast update && \ - apt-fast install -y --no-install-recommends gcc-13 g++-13 \ - && rm -rf /var/lib/apt/lists/* - -# Install GCC-14 only for Ubuntu 24.04 +# Install gcc-13 (and gcc-14/gcc-15 on 26.04) +# On Ubuntu 22.04, gcc-13 requires the ubuntu-toolchain-r PPA +# On Ubuntu 26.04, gcc-13 through gcc-15 are available natively. GCC 16 is not +# installed because CUDA 13.3 supports GCC host compilers only through GCC 15. RUN UBUNTU_VERSION=$(lsb_release -rs) && \ - if [ "$UBUNTU_VERSION" = "24.04" ]; then \ + if [ "$UBUNTU_VERSION" = "22.04" ]; then \ + add-apt-repository ppa:ubuntu-toolchain-r/ppa -y && \ + add-apt-repository ppa:ubuntu-toolchain-r/test -y && \ apt-fast update && \ - apt-fast install -y --no-install-recommends gcc-14 g++-14 \ - && rm -rf /var/lib/apt/lists/*; \ + apt-fast install -y --no-install-recommends gcc-13 g++-13 && \ + add-apt-repository --remove ppa:ubuntu-toolchain-r/ppa && \ + add-apt-repository --remove ppa:ubuntu-toolchain-r/test && \ + rm -rf /var/lib/apt/lists/*; \ + elif [ "$UBUNTU_VERSION" = "26.04" ]; then \ + apt-fast update && \ + apt-fast install -y --no-install-recommends \ + gcc-13 g++-13 gcc-14 g++-14 gcc-15 g++-15 && \ + rm -rf /var/lib/apt/lists/*; \ + else \ + echo "Unsupported Ubuntu version: $UBUNTU_VERSION" >&2; \ + exit 1; \ fi -# remove ppa for gcc-13 and gcc-14 -RUN add-apt-repository --remove ppa:ubuntu-toolchain-r/ppa && \ - add-apt-repository --remove ppa:ubuntu-toolchain-r/test - # Install clang-11 only for Ubuntu 22.04 or lower RUN UBUNTU_VERSION=$(lsb_release -rs) && \ UBUNTU_MAJOR=$(echo "$UBUNTU_VERSION" | cut -d. -f1) && \ @@ -102,25 +105,56 @@ RUN UBUNTU_VERSION=$(lsb_release -rs) && \ && rm -rf /var/lib/apt/lists/*; \ fi -# Install development tools -RUN apt-fast update && \ +# Install development tools. Clang 14 is not available on Ubuntu 26.04; Clang 18 +# is the oldest native release there and is supported by CUDA 13.3. +RUN UBUNTU_VERSION=$(lsb_release -rs) && \ + if [ "$UBUNTU_VERSION" = "22.04" ]; then \ + CLANG_VERSION=14; \ + elif [ "$UBUNTU_VERSION" = "26.04" ]; then \ + CLANG_VERSION=18; \ + else \ + echo "Unsupported Ubuntu version: $UBUNTU_VERSION" >&2; \ + exit 1; \ + fi && \ + apt-fast update && \ apt-fast install -y --no-install-recommends \ - clang-14 libclang-14-dev \ + "clang-${CLANG_VERSION}" "libclang-${CLANG_VERSION}-dev" \ ninja-build \ libgtest-dev \ libgmock-dev \ - ccache \ libssl-dev \ doxygen \ graphviz \ fonts-dejavu \ && rm -rf /var/lib/apt/lists/* +# Install ccache from upstream static binaries; distro packages are older 4.x +# releases missing recent nvcc caching fixes +RUN ARCH=$(uname -m) \ + && case "$ARCH" in \ + x86_64) CCACHE_SHA256="$CCACHE_SHA256_X86_64" ;; \ + aarch64) CCACHE_SHA256="$CCACHE_SHA256_AARCH64" ;; \ + *) echo "Unsupported architecture: $ARCH" >&2 && exit 1 ;; \ + esac \ + && curl --fail --location --proto '=https' --proto-redir '=https' --tlsv1.2 \ + "https://github.com/ccache/ccache/releases/download/v${CCACHE_VERSION}/ccache-${CCACHE_VERSION}-linux-${ARCH}-musl-static.tar.gz" \ + --output /tmp/ccache.tar.gz \ + && echo "${CCACHE_SHA256} /tmp/ccache.tar.gz" | sha256sum -c - \ + && tar -xzf /tmp/ccache.tar.gz -C /tmp \ + && install -m 755 "/tmp/ccache-${CCACHE_VERSION}-linux-${ARCH}-musl-static/ccache" /usr/local/bin/ccache \ + && rm -rf /tmp/ccache.tar.gz "/tmp/ccache-${CCACHE_VERSION}-linux-${ARCH}-musl-static" \ + && ccache --version + # Build and install zlib 1.3.1 from source to replace vulnerable system version (CVE-2018-25032) # Remove any old system zlib artifacts and install new version to /usr/local -RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates build-essential \ +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + ca-certificates \ + curl \ && rm -rf /var/lib/apt/lists/* \ - && curl -L https://github.com/madler/zlib/releases/download/v1.3.1/zlib-1.3.1.tar.gz -o /tmp/zlib-1.3.1.tar.gz \ + && curl --fail --location --proto '=https' --proto-redir '=https' --tlsv1.2 \ + https://github.com/madler/zlib/releases/download/v1.3.1/zlib-1.3.1.tar.gz \ + --output /tmp/zlib-1.3.1.tar.gz \ && tar -xzf /tmp/zlib-1.3.1.tar.gz -C /tmp \ && cd /tmp/zlib-1.3.1 \ && ./configure --prefix=/usr/local \ @@ -147,7 +181,8 @@ RUN apt-fast update && \ python3 python3-dev python3-venv \ && rm -rf /var/lib/apt/lists/* -# add deadsnakes ppa for python versions +# Add deadsnakes for the non-default Python versions. It provides Python +# 3.10-3.13 on Ubuntu 26.04; Ubuntu itself provides Python 3.14. RUN add-apt-repository ppa:deadsnakes/ppa -y # Install all specified Python versions with dev and venv packages @@ -156,7 +191,7 @@ RUN set -e; \ for pyver in $PYTHON_VERSIONS; do \ echo "Installing Python $pyver with dev and venv packages" && \ apt-fast install -y --no-install-recommends \ - python$pyver python$pyver-dev python$pyver-venv; \ + "python${pyver}" "python${pyver}-dev" "python${pyver}-venv"; \ done && \ rm -rf /var/lib/apt/lists/* @@ -167,28 +202,44 @@ RUN dpkg_arch=$(dpkg --print-architecture) && \ arm64) cmake_arch="aarch64";; \ *) echo >&2 "error: unsupported architecture ('${dpkg_arch}')" && exit 1;; \ esac && \ - CMAKE_VERSION_SHORT=$(echo ${CMAKE_VERSION} | cut -d. -f1,2) && \ + CMAKE_VERSION_SHORT=$(echo "${CMAKE_VERSION}" | cut -d. -f1,2) && \ echo "Downloading CMake for ${cmake_arch}" && \ - curl --fail -L https://cmake.org/files/v${CMAKE_VERSION_SHORT}/cmake-${CMAKE_VERSION}-linux-${cmake_arch}.tar.gz --output /tmp/cmake-${CMAKE_VERSION}.tar.gz \ - && tar -xzf /tmp/cmake-${CMAKE_VERSION}.tar.gz -C /tmp/ && cd /tmp/cmake-${CMAKE_VERSION}-linux-${cmake_arch}/ \ - && cp -r bin/ share/ doc/ /usr/local/ && rm -rf /tmp/cmake-${CMAKE_VERSION}* - + curl --fail --location --proto '=https' --proto-redir '=https' --tlsv1.2 \ + "https://cmake.org/files/v${CMAKE_VERSION_SHORT}/cmake-${CMAKE_VERSION}-linux-${cmake_arch}.tar.gz" \ + --output "/tmp/cmake-${CMAKE_VERSION}.tar.gz" \ + && tar -xzf "/tmp/cmake-${CMAKE_VERSION}.tar.gz" -C /tmp/ && cd "/tmp/cmake-${CMAKE_VERSION}-linux-${cmake_arch}/" \ + && cp -r bin/ share/ doc/ /usr/local/ && rm -rf "/tmp/cmake-${CMAKE_VERSION}"* + +# Install dlpack headers and CMake config from source (header-only library) +RUN git clone --depth 1 --branch v1.3 https://github.com/dmlc/dlpack.git /tmp/dlpack \ + && cmake -S /tmp/dlpack -B /tmp/dlpack/build -DBUILD_MOCK=OFF -DBUILD_DOCS=OFF \ + && cmake --install /tmp/dlpack/build --prefix /usr/local \ + && rm -rf /tmp/dlpack # Install pip using get-pip.py for all Python versions # This is the simplest solution that works across: -# - Multiple Ubuntu versions (22.04, 24.04) with different restrictions +# - Multiple Ubuntu versions (22.04, 26.04) with different restrictions # - System Python and deadsnakes PPA versions -# - Python versions with and without distutils (3.9-3.14) +# - Python versions with and without distutils (3.10-3.14) # Alternative approaches (apt, ensurepip) fail due to Debian restrictions and distutils removal in Python 3.12+ -# Upgrade system pip first to support --break-system-packages flag -# using --force-reinstall to replace the debian packages (eg wheel) with the pip packages +# Upgrade system pip first to support --break-system-packages. Ignore distro-owned +# packages because Ubuntu 26.04 packages do not always include pip RECORD metadata. RUN curl -sS https://bootstrap.pypa.io/get-pip.py | python3 - --break-system-packages --force-reinstall --ignore-installed -# install pip packages needed only for the system python -COPY requirements.sys_python.txt /tmp/requirements.sys_python.txt -RUN python3 -m pip install --break-system-packages --force-reinstall -r /tmp/requirements.sys_python.txt && \ - rm -rf /root/.cache/pip /tmp/requirements.sys_python.txt +# install build/packaging/linting packages for the system python +COPY requirements.build.sys_python.txt /tmp/requirements.build.sys_python.txt +RUN --mount=type=cache,target=/root/.cache/pip \ + python3 -m pip install --break-system-packages --force-reinstall --ignore-installed \ + -r /tmp/requirements.build.sys_python.txt && \ + rm -rf /tmp/requirements.build.sys_python.txt + +# install documentation packages for the system python +COPY requirements.docs.txt /tmp/requirements.docs.txt +RUN --mount=type=cache,target=/root/.cache/pip \ + python3 -m pip install --break-system-packages --force-reinstall --ignore-installed \ + -r /tmp/requirements.docs.txt && \ + rm -rf /tmp/requirements.docs.txt # Install/Update pip for all python versions in one layer @@ -196,61 +247,174 @@ RUN python3 -m pip install --break-system-packages --force-reinstall -r /tmp/req RUN set -e; \ for pyver in $PYTHON_VERSIONS; do \ echo "Installing pip for Python $pyver" && \ - curl -sS https://bootstrap.pypa.io/get-pip.py | python$pyver - --break-system-packages --force-reinstall --ignore-installed; \ + curl -sS https://bootstrap.pypa.io/get-pip.py | "python${pyver}" - --break-system-packages --force-reinstall --ignore-installed; \ done -# Install python packages (common dependencies, no torch, no numpy) -COPY requirements.no_torch_no_numpy.txt /tmp/requirements.no_torch_no_numpy.txt -RUN set -e; \ +# Install build dependencies (pybind11, needed for CMake find_package) +COPY requirements.build.all_pythons.txt /tmp/requirements.build.all_pythons.txt +RUN --mount=type=cache,target=/root/.cache/pip \ + set -e; \ for pyver in $PYTHON_VERSIONS; do \ - python$pyver -m pip install --break-system-packages --ignore-installed -r /tmp/requirements.no_torch_no_numpy.txt; \ + "python${pyver}" -m pip install --break-system-packages --ignore-installed \ + -r /tmp/requirements.build.all_pythons.txt; \ done -# Install numpy based on VER_NUMPY_MAJOR -COPY requirements.numpy${VER_NUMPY_MAJOR}.txt /tmp/requirements.numpy.txt -RUN set -e; \ +# Install test dependencies (pytest, typing-extensions) +COPY requirements.tests.common.txt /tmp/requirements.tests.common.txt +RUN --mount=type=cache,target=/root/.cache/pip \ + set -e; \ for pyver in $PYTHON_VERSIONS; do \ - python$pyver -m pip install --break-system-packages --ignore-installed -r /tmp/requirements.numpy.txt; \ + "python${pyver}" -m pip install --break-system-packages --ignore-installed \ + -r /tmp/requirements.tests.common.txt; \ done -RUN rm -rf /root/.cache/pip /tmp/requirements.no_torch_no_numpy.txt /tmp/requirements.numpy.txt - - -# Install torch -# breaking down per python version for better caching of layers - -# Install torch for Python 3.9 if present in PYTHON_VERSIONS -RUN if echo "$PYTHON_VERSIONS" | grep -q "3.9"; then \ - python3.9 -m pip install --break-system-packages ${TORCH_ADDITIONAL_INDEX_URL:+--extra-index-url $TORCH_ADDITIONAL_INDEX_URL} torch==$VER_TORCH; \ - fi - -# Install torch for Python 3.10 if present in PYTHON_VERSIONS -RUN if echo "$PYTHON_VERSIONS" | grep -q "3.10"; then \ - python3.10 -m pip install --break-system-packages ${TORCH_ADDITIONAL_INDEX_URL:+--extra-index-url $TORCH_ADDITIONAL_INDEX_URL} torch==$VER_TORCH; \ - fi - -# Install torch for Python 3.11 if present in PYTHON_VERSIONS -RUN if echo "$PYTHON_VERSIONS" | grep -q "3.11"; then \ - python3.11 -m pip install --break-system-packages ${TORCH_ADDITIONAL_INDEX_URL:+--extra-index-url $TORCH_ADDITIONAL_INDEX_URL} torch==$VER_TORCH; \ - fi - -# Install torch for Python 3.12 if present in PYTHON_VERSIONS -RUN if echo "$PYTHON_VERSIONS" | grep -q "3.12"; then \ - python3.12 -m pip install --break-system-packages ${TORCH_ADDITIONAL_INDEX_URL:+--extra-index-url $TORCH_ADDITIONAL_INDEX_URL} torch==$VER_TORCH; \ - fi - -# Install torch for Python 3.13 if present in PYTHON_VERSIONS -RUN if echo "$PYTHON_VERSIONS" | grep -q "3.13"; then \ - python3.13 -m pip install --break-system-packages ${TORCH_ADDITIONAL_INDEX_URL:+--extra-index-url $TORCH_ADDITIONAL_INDEX_URL} torch==$VER_TORCH; \ +# Install numpy based on VER_NUMPY_MAJOR +COPY requirements.tests.numpy${VER_NUMPY_MAJOR}.txt /tmp/requirements.tests.numpy.txt +RUN --mount=type=cache,target=/root/.cache/pip \ + set -e; \ + for pyver in $PYTHON_VERSIONS; do \ + "python${pyver}" -m pip install --break-system-packages --ignore-installed \ + -r /tmp/requirements.tests.numpy.txt; \ + done; \ + rm -rf /tmp/requirements.build.all_pythons.txt /tmp/requirements.tests.common.txt /tmp/requirements.tests.numpy.txt + + +# Install CUDA-version-specific test dependencies (cupy, cuda-python). NumPy 1 +# uses CuPy 13.6 because CuPy 14 requires NumPy 2. +COPY requirements.tests.${TORCH_CUDA_SUFFIX}.txt /tmp/requirements.tests.cuda.txt +COPY requirements.tests.cu12.numpy1.txt /tmp/requirements.tests.cuda.cu12.numpy1.txt + +RUN --mount=type=cache,target=/root/.cache/pip \ + set -e; \ + cuda_requirements=/tmp/requirements.tests.cuda.txt; \ + if [ "$VER_NUMPY_MAJOR" = "1" ]; then \ + if [ "$TORCH_CUDA_SUFFIX" != "cu12" ]; then \ + echo "NumPy 1 test images are supported only with CUDA 12" >&2; \ + exit 1; \ + fi; \ + cuda_requirements=/tmp/requirements.tests.cuda.cu12.numpy1.txt; \ + fi; \ + for pyver in $PYTHON_VERSIONS; do \ + "python${pyver}" -m pip install --break-system-packages -r "$cuda_requirements"; \ + done; \ + rm -f /tmp/requirements.tests.cuda.txt /tmp/requirements.tests.cuda.cu12.numpy1.txt + +# Build and install nvbench C++ library system-wide, then build Python bindings +# for all Python versions in parallel to avoid redundant git clones and reduce +# wall-clock time (especially on SBSA/QEMU where each sequential compile is slow). +# Uses --no-deps to avoid pulling nvidia-cuda-cupti-cu12 (system CUPTI is used). +# Skipped for numpy1 images (same gate as bench deps). +RUN set -e; \ + git clone https://github.com/NVIDIA/nvbench.git /tmp/nvbench && \ + cd /tmp/nvbench && git checkout "$NVBENCH_COMMIT" && \ + cmake -S . -B build -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX=/usr/local \ + -DCMAKE_CUDA_COMPILER=/usr/local/cuda/bin/nvcc \ + -DCMAKE_CUDA_HOST_COMPILER=/usr/bin/c++ \ + -Drapids-cmake-sha="${RAPIDS_CMAKE_SHA}" \ + -DNVBench_ENABLE_CUPTI=OFF \ + -DNVBench_ENABLE_TESTING=OFF \ + -DNVBench_ENABLE_EXAMPLES=OFF && \ + cmake --build build -j"$(nproc)" --target install && \ + if [ "$VER_NUMPY_MAJOR" != "1" ]; then \ + mkdir -p /tmp/nvbench_wheels && \ + for pyver in $PYTHON_VERSIONS; do \ + CMAKE_ARGS="-Drapids-cmake-sha=${RAPIDS_CMAKE_SHA}" \ + CMAKE_BUILD_PARALLEL_LEVEL="$(nproc)" \ + "python${pyver}" -m pip wheel --no-deps \ + /tmp/nvbench/python -w /tmp/nvbench_wheels & \ + done && \ + wait && \ + for pyver in $PYTHON_VERSIONS; do \ + "python${pyver}" -m pip install --break-system-packages --no-deps \ + --no-index --find-links /tmp/nvbench_wheels pynvbench; \ + done && \ + rm -rf /tmp/nvbench_wheels; \ + fi && \ + rm -rf /tmp/nvbench + +# Install benchmark dependencies from bench/python/requirements files (single source of truth). +# Skipped for numpy1 images since benchmarks require numpy 2. +# Torch and numpy are already installed above; pip will report "already satisfied" for those. +COPY requirements.bench.common.txt requirements.bench.cu12.txt requirements.bench.cu13.txt /tmp/ + +# Pre-build pycuda wheels in parallel (pycuda is sdist-only on PyPI — no pre-built wheels). +# Extracting the version from the requirements file keeps this in sync with versions.env. +RUN if [ "$VER_NUMPY_MAJOR" != "1" ]; then \ + set -e; \ + PYCUDA_VER=$(grep "^pycuda==" /tmp/requirements.bench.common.txt | head -1 | cut -d= -f3); \ + mkdir -p /tmp/pycuda_wheels && \ + for pyver in $PYTHON_VERSIONS; do \ + CMAKE_BUILD_PARALLEL_LEVEL="$(nproc)" \ + "python${pyver}" -m pip wheel "pycuda==${PYCUDA_VER}" \ + -w /tmp/pycuda_wheels & \ + done && \ + wait; \ fi -# Install torch for Python 3.14 if present in PYTHON_VERSIONS -RUN if echo "$PYTHON_VERSIONS" | grep -q "3.14"; then \ - python3.14 -m pip install --break-system-packages ${TORCH_ADDITIONAL_INDEX_URL:+--extra-index-url $TORCH_ADDITIONAL_INDEX_URL} torch==$VER_TORCH; \ +RUN --mount=type=cache,target=/root/.cache/pip \ + if [ "$VER_NUMPY_MAJOR" != "1" ]; then \ + set -e; \ + CUDA_MAJOR=$(echo "$VER_CUDA" | cut -d. -f1); \ + for pyver in $PYTHON_VERSIONS; do \ + "python${pyver}" -m pip install --break-system-packages --no-cache-dir \ + --find-links /tmp/pycuda_wheels \ + -r "/tmp/requirements.bench.cu${CUDA_MAJOR}.txt"; \ + done; \ + fi; \ + rm -rf /tmp/requirements.bench.common.txt /tmp/requirements.bench.cu*.txt /tmp/pycuda_wheels + +# Optionally install NVIDIA Nsight profilers (Nsight Compute + Nsight Systems CLI). +# Gated behind INSTALL_NSIGHT_COMPUTE because nsight-compute is large (~1.5-2.5 GB, +# the apt package bundles the GUI + section files); only the benchmark-serving image +# enables it (see docker/build_dockers.sh). Nsight packages are version-pinned to the +# toolkit (cuda-nsight-{compute,systems}--). Some CUDA repositories +# provide a lightweight nsight-systems-cli package, while CUDA 13.3 on Ubuntu 26.04 +# uses the versioned meta package. The CUDA apt repo is configured by the base image. +ARG INSTALL_NSIGHT_COMPUTE=0 +RUN if [ "$INSTALL_NSIGHT_COMPUTE" = "1" ]; then \ + set -e; \ + CUDA_DASH=$(echo "$VER_CUDA" | cut -d. -f1,2 | tr '.' '-'); \ + apt-fast update && \ + if apt-cache show nsight-systems-cli >/dev/null 2>&1; then \ + NSIGHT_SYSTEMS_PACKAGE=nsight-systems-cli; \ + else \ + NSIGHT_SYSTEMS_PACKAGE="cuda-nsight-systems-${CUDA_DASH}"; \ + fi && \ + apt-fast install -y --no-install-recommends \ + "cuda-nsight-compute-${CUDA_DASH}" \ + "$NSIGHT_SYSTEMS_PACKAGE" && \ + rm -rf /var/lib/apt/lists/*; \ + # The packages install under /opt/nvidia/...; make sure ncu/nsys are on PATH. + for tool in ncu nsys; do \ + if ! command -v "$tool" >/dev/null 2>&1; then \ + bin=$(find /opt/nvidia -type f -name "$tool" -perm -u+x 2>/dev/null | head -1); \ + [ -n "$bin" ] && ln -sf "$bin" "/usr/local/bin/$tool"; \ + fi; \ + done; \ + ncu --version && nsys --version; \ fi -# Clean up pip cache -RUN rm -rf /root/.cache/pip +# Verify critical packages are installed and importable for all Python versions +RUN set -e; \ + for pyver in $PYTHON_VERSIONS; do \ + echo "Verifying Python $pyver..."; \ + "python${pyver}" -c "import pybind11; print(f' pybind11 {pybind11.__version__}')" || \ + { echo "VERIFY FAILED: pybind11 not importable for Python $pyver" && exit 1; }; \ + "python${pyver}" -c "import pytest; print(f' pytest {pytest.__version__}')" || \ + { echo "VERIFY FAILED: pytest not importable for Python $pyver" && exit 1; }; \ + "python${pyver}" -c \ + "import numpy; \ + expected='$VER_NUMPY_MAJOR'; \ + actual=numpy.__version__.split('.', 1)[0]; \ + assert actual == expected, f'expected NumPy {expected}.x, got {numpy.__version__}'; \ + print(f' numpy {numpy.__version__}')" || \ + { echo "VERIFY FAILED: expected NumPy $VER_NUMPY_MAJOR for Python $pyver" && exit 1; }; \ + "python${pyver}" -c "import cupy; print(f' cupy {cupy.__version__}')" || \ + { echo "VERIFY FAILED: cupy not importable for Python $pyver" && exit 1; }; \ + done; \ + echo "All Python versions verified successfully" WORKDIR /workspace diff --git a/docker/README.md b/docker/README.md index bd5215eb3..6ba1fbcab 100644 --- a/docker/README.md +++ b/docker/README.md @@ -1,4 +1,4 @@ -[//]: # "SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved." +[//]: # "SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved." [//]: # "SPDX-License-Identifier: Apache-2.0" [//]: # "" [//]: # "Licensed under the Apache License, Version 2.0 (the 'License');" @@ -34,9 +34,10 @@ Combines manylinux base with CUDA toolkit for building CV-CUDA packages. | Image Name | GCC Version | CUDA Version | Base Image | Purpose | |------------|-------------|--------------|----------------|---------| +| `builder_cu12.2.0_gcc10` | 10 | 12.2.0 | `manylinux_2_28` | CUDA 12.2 builds for CV-CUDA packages (multi-arch) | | `builder_cu12.5.0_gcc10` | 10 | 12.5.0 | `manylinux_2_28` | CUDA 12.5 builds for CV-CUDA packages (multi-arch) | -| `builder_cu12.9.0_gcc10` | 10 | 12.9.0 | `manylinux_2_28` | CUDA 12.9 builds for CV-CUDA packages (multi-arch) | | `builder_cu13.0.1_gcc10` | 10 | 13.0.1 | `manylinux_2_28` | CUDA 13.0 builds for CV-CUDA packages (multi-arch) | +| `builder_cu13.3.0_gcc10` | 10 | 13.3.0 | `manylinux_2_28` | CUDA 13.3 builds for CV-CUDA packages (multi-arch) | ### Build Dependencies @@ -76,7 +77,9 @@ Combines manylinux base with CUDA toolkit for building CV-CUDA packages. All builder images include: - **CMake 3.24.3** - Modern build system -- **Python Support** - Multiple Python versions (3.9-3.14) from ManyLinux +- **Python Support** - Multiple Python versions (3.10-3.14) from ManyLinux +- **pybind11** - Installed for all Python versions (Python bindings build dependency) +- **dlpack** - DLPack tensor exchange protocol headers (installed from source) - **Documentation Tools** - Sphinx 7.4.7, sphinx_rtd_theme, breathe - **Development Tools** - patchelf 0.17.2, setuptools, wheel, clang 14.0 - **CUDA Integration** - Full CUDA toolkit from base CUDA images @@ -94,28 +97,53 @@ The build script `build_dockers.sh` creates several development image variants: | Image | Base Image | CUDA | NumPy | PyTorch | Python Versions | |-------|------------|------|-------|---------|-----------------| -| `devel_u22.04_cu12.9.0_num2_torch2.8.0` | nvidia/cuda:12.9.0-devel-ubuntu22.04 | 12.9.0 | 2.x (2.0.2-2.3.3) | 2.8.0 | 3.9-3.13 (multi-arch) | -| `devel_u22.04_cu12.5.0_num1_torch2.8.0` | nvidia/cuda:12.5.0-devel-ubuntu22.04 | 12.5.0 | 1.26.4 | 2.8.0 | 3.9-3.13 (multi-arch) | -| `devel_u24.04_cu13.0.1_num2_torch2.9.0` | nvidia/cuda:13.0.1-devel-ubuntu24.04 | 13.0.1 | 2.x (2.2.6-2.3.3) | 2.9.0 | 3.10-3.14 (multi-arch) | +| `devel_u26.04_cu13.3.0_num2` | nvidia/cuda:13.3.0-devel-ubuntu26.04 | 13.3.0 | 2.x (2.2.6-2.3.3) | 2.11.0 | 3.14 (multi-arch)¹ | +| `devel_u22.04_cu12.5.0_num1` | nvidia/cuda:12.5.0-devel-ubuntu22.04 | 12.5.0 | 1.26.4 | 2.9.1 | 3.10 (multi-arch) | +| `devel_u22.04_py310-314_cu12.5.0_num2` | nvidia/cuda:12.5.0-devel-ubuntu22.04 | 12.5.0 | 2.x (2.2.6-2.3.3) | 2.9.1 | 3.10-3.14 (multi-arch) | +| `devel_u26.04_py310-314_cu13.3.0_num2` | nvidia/cuda:13.3.0-devel-ubuntu26.04 | 13.3.0 | 2.x (2.2.6-2.3.3) | 2.11.0 | 3.10-3.14 (multi-arch)¹ | -*Note: PyTorch 2.8.0 supports Python 3.9-3.13, PyTorch 2.9.0 supports Python 3.10-3.14 +The NumPy 1 image uses CuPy 13.6.0, the newest release compatible with NumPy +1.26. The NumPy 2 images use CuPy 14.0.1. + +*Note: PyTorch 2.8.0 supports Python 3.10-3.13, PyTorch 2.9.0/2.11.0 support Python 3.10-3.14 + +¹ The CUDA 13.3 / Ubuntu 26.04 devel images are built with `INSTALL_NSIGHT_COMPUTE=1`, +which adds the Nsight Compute (`ncu`) and Nsight Systems (`nsys`) CLIs for kernel/timeline +profiling (the `num2` 3.14 image is also the CI `TestBenchmarks` runner). The CUDA 12.5 +devel images leave the arg at its `0` default to stay lean (Nsight Compute alone is +~1.5-2.5 GB). **Key Features:** -- **Python Versions:** 3.9, 3.10, 3.11, 3.12, 3.13, 3.14 -- **Development Tools:** CMake 3.24.3, build-essential, clang-14, ninja-build +- **Python Versions:** 3.10, 3.11, 3.12, 3.13, 3.14 +- **Development Tools:** CMake 3.31.1, build-essential, clang-14 (Ubuntu 22.04), clang-18 (Ubuntu 26.04), ninja-build +- **Compilers:** GCC 10-13 (Ubuntu 22.04), GCC 11-15 (Ubuntu 26.04) +- **Profiling (CUDA 13.3 devel images):** Nsight Compute (`ncu`), Nsight Systems (`nsys`) - **Testing:** Google Test/Mock, pytest - **ML Frameworks:** PyTorch, CuPy (CUDA-specific versions) - **Documentation:** Doxygen, Sphinx ecosystem - **Version Control:** git, git-lfs, pre-commit -### Python Requirements +### Version Management + +All pinned Python package versions (cupy, numpy, torchvision, nvimgcodec, etc.) are defined +in **`versions.env`** at the repository root — the single source of truth. + +To update a version: +1. Edit `versions.env` +2. Run `bash generate_requirements.sh` +3. Commit both files together + +Most requirements files under `tests/`, `bench/`, and `samples/` are **auto-generated** from +`versions.env` and carry an `AUTO-GENERATED` header. Do not edit them directly. + +The following files are **not** auto-generated and are maintained manually: -| File | Purpose | Usage | -|------|---------|-------| -| `requirements.sys_python.txt` | System Python packages, installed only for the system Python version | Documentation and build tools | -| `requirements.no_torch_no_numpy.txt` | Development/test packages, installed for each Python version | Testing, building (excludes Torch and Numpy) | -| `requirements.numpy1.txt` | NumPy 1.x requirements with Python version constraints | NumPy 1.26.4 for Python 3.9-3.12 (selected via VER_NUMPY_MAJOR=1) | -| `requirements.numpy2.txt` | NumPy 2.x requirements with Python version constraints | NumPy 2.x versions for Python 3.9-3.14 (selected via VER_NUMPY_MAJOR=2) | +| File | Purpose | +|------|---------| +| `requirements.build.sys_python.txt` | System Python: build, packaging, and linting tools | +| `docs/requirements.docs.txt` | System Python: Sphinx documentation tools | +| `requirements.build.all_pythons.txt` | All Python versions: pybind11 for CMake find_package | +| `tests/requirements.tests.common.txt` | All Python versions: pytest and typing-extensions | ### Build Script @@ -141,12 +169,12 @@ where $REGISTRY_PREFIX should be set to your remote registry. ### Using Development Images ```bash # Run development container (automatically selects correct architecture) -docker run -it --gpus all devel_u22.04_cu12.5.0_num1_torch2.8.0:v2 +docker run -it --gpus all devel_u22.04_cu12.5.0_num1:v9 # Mount source code for development docker run -it --gpus all \ -v /path/to/cvcuda:/workspace \ - devel_u22.04_cu12.5.0_num1_torch2.8.0:v2 + devel_u22.04_cu12.5.0_num1:v9 ``` ### Using Builder Images for Package Creation @@ -175,7 +203,7 @@ Run build script to create new image versions For builder images, Python versions available come directly from the base ManyLinux. -For development images, update the build arguments to docker buildx in `build_dockers.sh`: `--build-arg "PYTHON_VERSIONS=3.9 3.10 3.11 3.12 3.13 3.14"` +For development images, update the build arguments to docker buildx in `build_dockers.sh`: `--build-arg "PYTHON_VERSIONS=3.10 3.11 3.12 3.13 3.14"` ## Troubleshooting diff --git a/docker/build_dockers.sh b/docker/build_dockers.sh index bcc09ee36..690a7d5b7 100755 --- a/docker/build_dockers.sh +++ b/docker/build_dockers.sh @@ -1,6 +1,6 @@ -#!/bin/bash -ex +#!/bin/bash -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,6 +15,10 @@ # See the License for the specific language governing permissions and # limitations under the License. +set -ex + +REPO_ROOT=$(git -C "$(dirname "$(readlink -f "$0")")" rev-parse --show-toplevel) + # Main script used to build the Docker images for the CV-CUDA project # Usage: ./build_dockers.sh [REGISTRY_PREFIX] [MODE] # REGISTRY_PREFIX: Optional registry prefix for Docker images (e.g., "myregistry.com/") @@ -23,43 +27,47 @@ # - "multiarch": Build multi-architecture images (x86_64 + aarch64) and push to registry (requires REGISTRY_PREFIX) # - "local": Build for native architecture only and load into local Docker -export VERSION=${VERSION:-5} # Update version when changing anything in the Dockerfiles +export VERSION=${VERSION:-14} # Update version when changing anything in the Dockerfiles export TEGRA_VERSION=${TEGRA_VERSION:-1} # Update version when changing anything in the Dockerfile.tegra-aarch64-linux.builder export REGISTRY_PREFIX=${1:-${REGISTRY_PREFIX:-}} export PYVER=${PYVER:-"py310"} export MANYLINUX_IMAGE_TAG="2025.10.10-1" +readonly MODE_MULTIARCH="multiarch" +readonly MODE_LOCAL="local" -# Python versions for Docker images -export PYTHON_VERSIONS_39_TO_312="3.9 3.10 3.11 3.12" # For numpy 1 (no Python 3.13 and 3.14 support) -export PYTHON_VERSIONS_39_TO_313="3.9 3.10 3.11 3.12 3.13" # For Torch 2.8.0 (no Python 3.14 support) -export PYTHON_VERSIONS_310_TO_314="3.10 3.11 3.12 3.13 3.14" # For Torch 2.9.0 (no Python 3.9 support) +# Python versions for the multi-Python devel image +export PYTHON_VERSIONS_310_TO_314="3.10 3.11 3.12 3.13 3.14" # Parse MODE from command line argument, or auto-detect based on REGISTRY_PREFIX MODE="${2:-}" if [[ -z "$MODE" ]]; then if [[ -n "$REGISTRY_PREFIX" ]]; then - MODE="multiarch" + MODE="$MODE_MULTIARCH" else - MODE="local" + MODE="$MODE_LOCAL" fi fi # Validate MODE -if [[ "$MODE" != "multiarch" && "$MODE" != "local" ]]; then - echo "Error: Unsupported mode '$MODE'. Supported values are: multiarch, local" +if [[ "$MODE" != "$MODE_MULTIARCH" && "$MODE" != "$MODE_LOCAL" ]]; then + echo "Error: Unsupported mode '$MODE'. Supported values are: $MODE_MULTIARCH, $MODE_LOCAL" >&2 exit 1 fi # Multiarch mode requires a registry -if [[ "$MODE" == "multiarch" && -z "$REGISTRY_PREFIX" ]]; then - echo "Error: multiarch mode requires REGISTRY_PREFIX to be set" - echo "Usage: $0 multiarch" +if [[ "$MODE" == "$MODE_MULTIARCH" && -z "$REGISTRY_PREFIX" ]]; then + echo "Error: $MODE_MULTIARCH mode requires REGISTRY_PREFIX to be set" >&2 + echo "Usage: $0 $MODE_MULTIARCH" >&2 exit 1 fi echo "Build mode: $MODE" +# Regenerate requirements files from versions.env before building images +echo "Regenerating requirements files from versions.env..." +bash "$REPO_ROOT/generate_requirements.sh" + # Detect native architecture (used for local builds and context directory naming) DETECTED_ARCH=$(uname -m) case "$DETECTED_ARCH" in @@ -72,14 +80,14 @@ case "$DETECTED_ARCH" in NATIVE_PLATFORM="linux/arm64" ;; *) - echo "Error: Unsupported detected architecture '$DETECTED_ARCH'" + echo "Error: Unsupported detected architecture '$DETECTED_ARCH'" >&2 exit 1 ;; esac echo "Native architecture: $NATIVE_ARCH" # Set platform(s) based on mode -if [[ "$MODE" == "multiarch" ]]; then +if [[ "$MODE" == "$MODE_MULTIARCH" ]]; then PLATFORMS="linux/amd64,linux/arm64" echo "Building for platforms: $PLATFORMS" else @@ -96,17 +104,36 @@ mkdir -p "$BUILD_CONTEXT_DIR" # Copy all necessary files to the isolated build context SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cp "$SCRIPT_DIR"/Dockerfile.* "$BUILD_CONTEXT_DIR/" -cp "$SCRIPT_DIR"/requirements.*.txt "$BUILD_CONTEXT_DIR/" 2>/dev/null || true +# Copy build requirements (docker/ is the source of truth for build dependencies) +cp "$SCRIPT_DIR"/requirements.build.sys_python.txt "$BUILD_CONTEXT_DIR/" +cp "$SCRIPT_DIR"/requirements.build.all_pythons.txt "$BUILD_CONTEXT_DIR/" + +# Copy docs requirements (single source of truth for documentation dependencies) +cp "$SCRIPT_DIR"/../docs/requirements.docs.txt "$BUILD_CONTEXT_DIR/" + +# Copy test requirements (single source of truth for test dependencies) +cp "$SCRIPT_DIR"/../tests/requirements.tests.common.txt "$BUILD_CONTEXT_DIR/" +cp "$SCRIPT_DIR"/../tests/requirements.tests.numpy1.txt "$BUILD_CONTEXT_DIR/" +cp "$SCRIPT_DIR"/../tests/requirements.tests.numpy2.txt "$BUILD_CONTEXT_DIR/" +cp "$SCRIPT_DIR"/../tests/requirements.tests.cu12.txt "$BUILD_CONTEXT_DIR/" +cp "$SCRIPT_DIR"/../tests/requirements.tests.cu12.numpy1.txt "$BUILD_CONTEXT_DIR/" +cp "$SCRIPT_DIR"/../tests/requirements.tests.cu13.txt "$BUILD_CONTEXT_DIR/" + +# Copy bench requirements (single source of truth for benchmark dependencies) +cp "$SCRIPT_DIR"/../bench/python/requirements.bench.common.txt "$BUILD_CONTEXT_DIR/" +cp "$SCRIPT_DIR"/../bench/python/requirements.bench.cu12.txt "$BUILD_CONTEXT_DIR/" +cp "$SCRIPT_DIR"/../bench/python/requirements.bench.cu13.txt "$BUILD_CONTEXT_DIR/" # Cleanup function to remove build context on exit cleanup_build_context() { echo "Cleaning up isolated build context: $BUILD_CONTEXT_DIR" rm -rf "$BUILD_CONTEXT_DIR" + return $? } trap cleanup_build_context EXIT # Determine push/load strategy based on mode -if [[ "$MODE" == "multiarch" ]]; then +if [[ "$MODE" == "$MODE_MULTIARCH" ]]; then echo "Multiarch mode: pushing images to registry '$REGISTRY_PREFIX'" PUSH_OR_LOAD="--push" else @@ -119,7 +146,7 @@ fi ATTESTATION_FLAGS="--provenance=false --sbom=false" # Set builder name based on mode -if [[ "$MODE" == "multiarch" ]]; then +if [[ "$MODE" == "$MODE_MULTIARCH" ]]; then BUILDER_NAME="cvcuda_multiarch_builder" else BUILDER_NAME="cvcuda_builder_${NATIVE_ARCH}" @@ -138,9 +165,13 @@ fi docker buildx inspect --bootstrap "$BUILDER_NAME" # Note about QEMU emulation for multiarch builds -if [[ "$MODE" == "multiarch" ]]; then +if [[ "$MODE" == "$MODE_MULTIARCH" ]]; then echo "Note: Multi-arch builds will use QEMU emulation for non-native architectures" echo " This may be slower than native builds" + + # Optimize QEMU performance + export QEMU_CPU=max + echo " QEMU_CPU set to 'max' for better performance" fi ####### BASE IMAGES ####### @@ -151,7 +182,7 @@ docker buildx build \ --builder "$BUILDER_NAME" \ ${REGISTRY_PREFIX:+--cache-from type=registry,ref=${MANYLINUX_GCC10}:v${VERSION}} \ -t ${MANYLINUX_GCC10} -t ${MANYLINUX_GCC10}:v${VERSION} \ - -f Dockerfile.gcc10.deps \ + -f "$BUILD_CONTEXT_DIR/Dockerfile.gcc10.deps" \ --build-arg "MANYLINUX_IMAGE_TAG=${MANYLINUX_IMAGE_TAG}" \ ${REGISTRY_PREFIX:+--cache-to type=inline} \ --platform ${PLATFORMS} \ @@ -159,88 +190,69 @@ docker buildx build \ ${PUSH_OR_LOAD} \ "$BUILD_CONTEXT_DIR" -# base with just the toolkit installed (CUDA 12.5.0) on top of Ubuntu 22.04 -export CUDA_125="${REGISTRY_PREFIX}cu12.5.0" -docker buildx build \ - --builder "$BUILDER_NAME" \ - ${REGISTRY_PREFIX:+--cache-from type=registry,ref=${CUDA_125}:v${VERSION}} \ - -t ${CUDA_125} -t ${CUDA_125}:v${VERSION} \ - -f Dockerfile.cuda12.5.0.deps \ - ${REGISTRY_PREFIX:+--cache-to type=inline} \ - --platform ${PLATFORMS} \ - ${ATTESTATION_FLAGS} \ - ${PUSH_OR_LOAD} \ - "$BUILD_CONTEXT_DIR" - -# base with just the toolkit installed (CUDA 12.9.0) on top of Ubuntu 22.04 -export CUDA_129="${REGISTRY_PREFIX}cu12.9.0" -docker buildx build \ - --builder "$BUILDER_NAME" \ - ${REGISTRY_PREFIX:+--cache-from type=registry,ref=${CUDA_129}:v${VERSION}} \ - -t ${CUDA_129} -t ${CUDA_129}:v${VERSION} \ - -f Dockerfile.cuda12.9.0.deps \ - ${REGISTRY_PREFIX:+--cache-to type=inline} \ - --platform ${PLATFORMS} \ - ${ATTESTATION_FLAGS} \ - ${PUSH_OR_LOAD} \ - "$BUILD_CONTEXT_DIR" +####### BUILDER IMAGES ####### +# Manylinux-based, various GCC versions +# Dockerfile: Dockerfile.builder.deps +# CUDA toolkit is copied from official NVIDIA Docker images (multi-arch, no .run installer needed) -# base with just the toolkit installed (CUDA 13.0.1) on top of Ubuntu 22.04 -export CUDA_1301="${REGISTRY_PREFIX}cu13.0.1" +# GCC 10, CUDA 12.2 +export BUILDER_CUDA_122="${REGISTRY_PREFIX}builder_cu12.2.0_gcc10" docker buildx build \ --builder "$BUILDER_NAME" \ - ${REGISTRY_PREFIX:+--cache-from type=registry,ref=${CUDA_1301}:v${VERSION}} \ - -t ${CUDA_1301} -t ${CUDA_1301}:v${VERSION} \ - -f Dockerfile.cuda13.0.1.deps \ + ${REGISTRY_PREFIX:+--cache-from type=registry,ref=${BUILDER_CUDA_122}:v${VERSION}} \ + -t ${BUILDER_CUDA_122} -t ${BUILDER_CUDA_122}:v${VERSION} \ + -f "$BUILD_CONTEXT_DIR/Dockerfile.builder.deps" \ + --build-arg "FROM_IMAGE_NAME=${MANYLINUX_GCC10}:v${VERSION}" \ + --build-arg "CUDA_IMAGE=nvidia/cuda:12.2.0-devel-ubuntu22.04" \ + --build-arg "PYTHON_VERSIONS=${PYTHON_VERSIONS_310_TO_314}" \ ${REGISTRY_PREFIX:+--cache-to type=inline} \ --platform ${PLATFORMS} \ ${ATTESTATION_FLAGS} \ ${PUSH_OR_LOAD} \ "$BUILD_CONTEXT_DIR" -####### BUILDER IMAGES ####### -# Manylinux-based, various GCC versions -# Dockerfile: Dockerfile.builder.deps - # GCC 10, CUDA 12.5 export BUILDER_CUDA_125="${REGISTRY_PREFIX}builder_cu12.5.0_gcc10" docker buildx build \ --builder "$BUILDER_NAME" \ ${REGISTRY_PREFIX:+--cache-from type=registry,ref=${BUILDER_CUDA_125}:v${VERSION}} \ -t ${BUILDER_CUDA_125} -t ${BUILDER_CUDA_125}:v${VERSION} \ - -f Dockerfile.builder.deps \ + -f "$BUILD_CONTEXT_DIR/Dockerfile.builder.deps" \ --build-arg "FROM_IMAGE_NAME=${MANYLINUX_GCC10}:v${VERSION}" \ - --build-arg "CUDA_IMAGE=${CUDA_125}:v${VERSION}" \ + --build-arg "CUDA_IMAGE=nvidia/cuda:12.5.0-devel-ubuntu22.04" \ + --build-arg "PYTHON_VERSIONS=${PYTHON_VERSIONS_310_TO_314}" \ ${REGISTRY_PREFIX:+--cache-to type=inline} \ --platform ${PLATFORMS} \ ${ATTESTATION_FLAGS} \ ${PUSH_OR_LOAD} \ "$BUILD_CONTEXT_DIR" -# GCC 10, CUDA 12.9 -export BUILDER_CUDA_129="${REGISTRY_PREFIX}builder_cu12.9.0_gcc10" +# GCC 10, CUDA 13.0.1 +export BUILDER_CUDA_1301="${REGISTRY_PREFIX}builder_cu13.0.1_gcc10" docker buildx build \ --builder "$BUILDER_NAME" \ - ${REGISTRY_PREFIX:+--cache-from type=registry,ref=${BUILDER_CUDA_129}:v${VERSION}} \ - -t ${BUILDER_CUDA_129} -t ${BUILDER_CUDA_129}:v${VERSION} \ - -f Dockerfile.builder.deps \ + ${REGISTRY_PREFIX:+--cache-from type=registry,ref=${BUILDER_CUDA_1301}:v${VERSION}} \ + -t ${BUILDER_CUDA_1301} -t ${BUILDER_CUDA_1301}:v${VERSION} \ + -f "$BUILD_CONTEXT_DIR/Dockerfile.builder.deps" \ --build-arg "FROM_IMAGE_NAME=${MANYLINUX_GCC10}:v${VERSION}" \ - --build-arg "CUDA_IMAGE=${CUDA_129}:v${VERSION}" \ + --build-arg "CUDA_IMAGE=nvidia/cuda:13.0.1-devel-ubuntu22.04" \ + --build-arg "PYTHON_VERSIONS=${PYTHON_VERSIONS_310_TO_314}" \ ${REGISTRY_PREFIX:+--cache-to type=inline} \ --platform ${PLATFORMS} \ ${ATTESTATION_FLAGS} \ ${PUSH_OR_LOAD} \ "$BUILD_CONTEXT_DIR" -# GCC 10, CUDA 13.0.1 -export BUILDER_CUDA_1301="${REGISTRY_PREFIX}builder_cu13.0.1_gcc10" +# GCC 10, CUDA 13.3.0 +export BUILDER_CUDA_1330="${REGISTRY_PREFIX}builder_cu13.3.0_gcc10" docker buildx build \ --builder "$BUILDER_NAME" \ - ${REGISTRY_PREFIX:+--cache-from type=registry,ref=${BUILDER_CUDA_1301}:v${VERSION}} \ - -t ${BUILDER_CUDA_1301} -t ${BUILDER_CUDA_1301}:v${VERSION} \ - -f Dockerfile.builder.deps \ + ${REGISTRY_PREFIX:+--cache-from type=registry,ref=${BUILDER_CUDA_1330}:v${VERSION}} \ + -t ${BUILDER_CUDA_1330} -t ${BUILDER_CUDA_1330}:v${VERSION} \ + -f "$BUILD_CONTEXT_DIR/Dockerfile.builder.deps" \ --build-arg "FROM_IMAGE_NAME=${MANYLINUX_GCC10}:v${VERSION}" \ - --build-arg "CUDA_IMAGE=${CUDA_1301}:v${VERSION}" \ + --build-arg "CUDA_IMAGE=nvidia/cuda:13.3.0-devel-ubuntu22.04" \ + --build-arg "PYTHON_VERSIONS=${PYTHON_VERSIONS_310_TO_314}" \ ${REGISTRY_PREFIX:+--cache-to type=inline} \ --platform ${PLATFORMS} \ ${ATTESTATION_FLAGS} \ @@ -250,57 +262,77 @@ docker buildx build \ ####### DEVEL IMAGES ####### # Ubuntu-based, various Python versions -# UBUNTU 22.04, CUDA 12.9, NUMPY 2, TORCH 2.8 -export DEVEL_U22_CU129_NUM2_TORCH28="${REGISTRY_PREFIX}devel_u22.04_cu12.9.0_num2_torch2.8.0" + +# UBUNTU 22.04, CUDA 12.5, NUMPY 1 +# Python 3.13 and 3.14 not supported by numpy 1 +export DEVEL_U22_CU125_NUM1="${REGISTRY_PREFIX}devel_u22.04_cu12.5.0_num1" docker buildx build \ --builder "$BUILDER_NAME" \ - ${REGISTRY_PREFIX:+--cache-from type=registry,ref=${DEVEL_U22_CU129_NUM2_TORCH28}:v${VERSION}} \ - -t ${DEVEL_U22_CU129_NUM2_TORCH28} -t ${DEVEL_U22_CU129_NUM2_TORCH28}:v${VERSION} \ - -f Dockerfile.devel.deps \ - --build-arg "BASE=nvidia/cuda:12.9.0-devel-ubuntu22.04" \ - --build-arg "PYTHON_VERSIONS=${PYTHON_VERSIONS_39_TO_313}" \ - --build-arg "VER_CUDA=12.9.0" \ - --build-arg "VER_NUMPY_MAJOR=2" \ - --build-arg "VER_TORCH=2.8.0" \ - --build-arg "TORCH_ADDITIONAL_INDEX_URL=https://download.pytorch.org/whl/cu129" \ + ${REGISTRY_PREFIX:+--cache-from type=registry,ref=${DEVEL_U22_CU125_NUM1}:v${VERSION}} \ + -t ${DEVEL_U22_CU125_NUM1} -t ${DEVEL_U22_CU125_NUM1}:v${VERSION} \ + -f "$BUILD_CONTEXT_DIR/Dockerfile.devel.deps" \ + --build-arg "BASE=nvidia/cuda:12.5.0-devel-ubuntu22.04" \ + --build-arg "PYTHON_VERSIONS=3.10" \ + --build-arg "VER_CUDA=12.5.0" \ + --build-arg "VER_NUMPY_MAJOR=1" \ + --build-arg "TORCH_CUDA_SUFFIX=cu12" \ ${REGISTRY_PREFIX:+--cache-to type=inline} \ --platform ${PLATFORMS} \ ${ATTESTATION_FLAGS} \ ${PUSH_OR_LOAD} \ "$BUILD_CONTEXT_DIR" -# UBUNTU 24.04, CUDA 13.0.1, NUMPY 2, TORCH 2.9 -export DEVEL_U24_CU1301_NUM2_TORCH29="${REGISTRY_PREFIX}devel_u24.04_cu13.0.1_num2_torch2.9.0" +# UBUNTU 22.04, CUDA 12.5, NUMPY 2, ALL PYTHON VERSIONS (3.10-3.14) +export DEVEL_U22_PY310_314_CU125_NUM2="${REGISTRY_PREFIX}devel_u22.04_py310-314_cu12.5.0_num2" docker buildx build \ --builder "$BUILDER_NAME" \ - ${REGISTRY_PREFIX:+--cache-from type=registry,ref=${DEVEL_U24_CU1301_NUM2_TORCH29}:v${VERSION}} \ - -t ${DEVEL_U24_CU1301_NUM2_TORCH29} -t ${DEVEL_U24_CU1301_NUM2_TORCH29}:v${VERSION} \ - -f Dockerfile.devel.deps \ - --build-arg "BASE=nvidia/cuda:13.0.1-devel-ubuntu24.04" \ + ${REGISTRY_PREFIX:+--cache-from type=registry,ref=${DEVEL_U22_PY310_314_CU125_NUM2}:v${VERSION}} \ + -t ${DEVEL_U22_PY310_314_CU125_NUM2} -t ${DEVEL_U22_PY310_314_CU125_NUM2}:v${VERSION} \ + -f "$BUILD_CONTEXT_DIR/Dockerfile.devel.deps" \ + --build-arg "BASE=nvidia/cuda:12.5.0-devel-ubuntu22.04" \ --build-arg "PYTHON_VERSIONS=${PYTHON_VERSIONS_310_TO_314}" \ - --build-arg "VER_CUDA=13.0.1" \ + --build-arg "VER_CUDA=12.5.0" \ --build-arg "VER_NUMPY_MAJOR=2" \ - --build-arg "VER_TORCH=2.9.0" \ - --build-arg "TORCH_ADDITIONAL_INDEX_URL=https://download.pytorch.org/whl/cu130" \ + --build-arg "TORCH_CUDA_SUFFIX=cu12" \ ${REGISTRY_PREFIX:+--cache-to type=inline} \ --platform ${PLATFORMS} \ ${ATTESTATION_FLAGS} \ ${PUSH_OR_LOAD} \ "$BUILD_CONTEXT_DIR" -# UBUNTU 22.04, CUDA 12.5, NUMPY 1, TORCH 2.8 -# python 3.13 and 3.14 not supported by numpy 1 -export DEVEL_U22_CU125_NUM1_TORCH28="${REGISTRY_PREFIX}devel_u22.04_cu12.5.0_num1_torch2.8.0" + +# UBUNTU 26.04, CUDA 13.3.0, NUMPY 2 +export DEVEL_U26_CU1330_NUM2="${REGISTRY_PREFIX}devel_u26.04_cu13.3.0_num2" docker buildx build \ --builder "$BUILDER_NAME" \ - ${REGISTRY_PREFIX:+--cache-from type=registry,ref=${DEVEL_U22_CU125_NUM1_TORCH28}:v${VERSION}} \ - -t ${DEVEL_U22_CU125_NUM1_TORCH28} -t ${DEVEL_U22_CU125_NUM1_TORCH28}:v${VERSION} \ - -f Dockerfile.devel.deps \ - --build-arg "BASE=nvidia/cuda:12.5.0-devel-ubuntu22.04" \ - --build-arg "PYTHON_VERSIONS=${PYTHON_VERSIONS_39_TO_312}" \ - --build-arg "VER_CUDA=12.5.0" \ - --build-arg "VER_NUMPY_MAJOR=1" \ - --build-arg "VER_TORCH=2.8.0" \ + ${REGISTRY_PREFIX:+--cache-from type=registry,ref=${DEVEL_U26_CU1330_NUM2}:v${VERSION}} \ + -t ${DEVEL_U26_CU1330_NUM2} -t ${DEVEL_U26_CU1330_NUM2}:v${VERSION} \ + -f "$BUILD_CONTEXT_DIR/Dockerfile.devel.deps" \ + --build-arg "BASE=nvidia/cuda:13.3.0-devel-ubuntu26.04" \ + --build-arg "PYTHON_VERSIONS=3.14" \ + --build-arg "VER_CUDA=13.3.0" \ + --build-arg "VER_NUMPY_MAJOR=2" \ + --build-arg "TORCH_CUDA_SUFFIX=cu13" \ + --build-arg "INSTALL_NSIGHT_COMPUTE=1" \ + ${REGISTRY_PREFIX:+--cache-to type=inline} \ + --platform ${PLATFORMS} \ + ${ATTESTATION_FLAGS} \ + ${PUSH_OR_LOAD} \ + "$BUILD_CONTEXT_DIR" + +# UBUNTU 26.04, CUDA 13.3.0, NUMPY 2, ALL PYTHON VERSIONS (3.10-3.14) +export DEVEL_U26_PY310_314_CU1330_NUM2="${REGISTRY_PREFIX}devel_u26.04_py310-314_cu13.3.0_num2" +docker buildx build \ + --builder "$BUILDER_NAME" \ + ${REGISTRY_PREFIX:+--cache-from type=registry,ref=${DEVEL_U26_PY310_314_CU1330_NUM2}:v${VERSION}} \ + -t ${DEVEL_U26_PY310_314_CU1330_NUM2} -t ${DEVEL_U26_PY310_314_CU1330_NUM2}:v${VERSION} \ + -f "$BUILD_CONTEXT_DIR/Dockerfile.devel.deps" \ + --build-arg "BASE=nvidia/cuda:13.3.0-devel-ubuntu26.04" \ + --build-arg "PYTHON_VERSIONS=${PYTHON_VERSIONS_310_TO_314}" \ + --build-arg "VER_CUDA=13.3.0" \ + --build-arg "VER_NUMPY_MAJOR=2" \ + --build-arg "TORCH_CUDA_SUFFIX=cu13" \ + --build-arg "INSTALL_NSIGHT_COMPUTE=1" \ ${REGISTRY_PREFIX:+--cache-to type=inline} \ --platform ${PLATFORMS} \ ${ATTESTATION_FLAGS} \ diff --git a/bench/python/requirements_cu13.txt b/docker/requirements.build.all_pythons.template similarity index 57% rename from bench/python/requirements_cu13.txt rename to docker/requirements.build.all_pythons.template index e8514ccfe..b0d5ce3be 100644 --- a/bench/python/requirements_cu13.txt +++ b/docker/requirements.build.all_pythons.template @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,19 +13,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -# CUDA 13-specific Python packages for CV-CUDA benchmarking +# Template for requirements.build.all_pythons.txt — to regenerate, run from the repo root: +# bash generate_requirements.sh -# Include common requirements --r requirements_common.txt +# Build dependencies installed for each Python version (3.10-3.14) +# - used by both Manylinux builder and Ubuntu devel Docker images +# - numpy, torch, and test dependencies are in separate requirements files -# PyTorch for CUDA 13.0 ---extra-index-url https://download.pytorch.org/whl/cu130 -torch==2.9.0 -torchvision==0.24.0 - -# CUDA 13-specific NVIDIA packages ---extra-index-url https://pypi.nvidia.com/ -nvidia-nvimgcodec-cu13==0.6.1.37 - -# CV-CUDA for CUDA 13 -cvcuda-cu13 +pybind11==${pybind11} +pybind11-stubgen==${pybind11_stubgen} diff --git a/docker/requirements.build.sys_python.template b/docker/requirements.build.sys_python.template new file mode 100644 index 000000000..32032ba5d --- /dev/null +++ b/docker/requirements.build.sys_python.template @@ -0,0 +1,28 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +# Template for requirements.build.sys_python.txt — to regenerate, run from the repo root: +# bash generate_requirements.sh + +# Build, packaging, and linting tools for the system Python only +# Documentation: docs/requirements.docs.txt + +setuptools==${setuptools} +wheel==${wheel} +build==${build} +patchelf==${patchelf} +auditwheel==${auditwheel} +flake8==${flake8} +pybind11-stubgen==${pybind11_stubgen} diff --git a/docker/requirements.no_torch_no_numpy.txt b/docker/requirements.no_torch_no_numpy.txt deleted file mode 100644 index 6ff9a3c51..000000000 --- a/docker/requirements.no_torch_no_numpy.txt +++ /dev/null @@ -1,25 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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. - - -# Python packages for the Docker images, builder and devel -# - requirements are installed across Python versions (3.9-3.14) -# - using the same requirements for Manylinux builders, Ubuntu 22.04 and Ubuntu 24.04 -# - torch is installed separately via per-Python-version RUN commands in Dockerfile -# - numpy is installed separately via requirements.numpy1.txt or requirements.numpy2.txt - -# Common dependencies -pytest==8.3.5 -typing-extensions==4.13.1 diff --git a/docker/requirements.sys_python.txt b/docker/requirements.sys_python.txt deleted file mode 100644 index b3a5a8dcd..000000000 --- a/docker/requirements.sys_python.txt +++ /dev/null @@ -1,51 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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. - -# Python packages for the Docker images, builder and devel -# - requirements are installed across Python versions (3.9-3.14) -# - using the same requirements for Manylinux builders, Ubuntu 22.04 and Ubuntu 24.04 -# - Contains build and documentation dependencies for all Docker images - -# these requirements are to be installed only for the system python (python3 -m pip install) - -# Common dependencies -exhale==0.3.7 -sphinx-rtd-theme==3.0.2 -sphinxcontrib-jquery==4.1 -sphinxcontrib-jsmath==1.0.1 -sphinx-tabs==3.4.7 -recommonmark==0.7.1 -graphviz==0.20.3 -setuptools==80.9.0 -wheel==0.45.0 -build==1.3.0 -typing-extensions==4.13.1 -patchelf==0.17.2.1 -flake8==7.1.0 - -# Dependencies for Python 3.9 only (legacy support) -sphinx==7.4.7; python_version == '3.9' - -# Dependencies for Python 3.9 and greater -sphinxcontrib-applehelp==2.0.0; python_version >= '3.9' -sphinxcontrib-devhelp==2.0.0; python_version >= '3.9' -sphinxcontrib-htmlhelp==2.1.0; python_version >= '3.9' -sphinxcontrib-qthelp==2.0.0; python_version >= '3.9' -sphinxcontrib-serializinghtml==2.0.0; python_version >= '3.9' -breathe==4.36.0; python_version >= '3.9' -auditwheel==6.4.2; python_version >= '3.9' - -# Dependencies specifically for Python > 3.9 -sphinx==8.1.3; python_version > '3.9' diff --git a/docs/CMakeLists.txt b/docs/CMakeLists.txt index b9e8c895e..f755c4937 100644 --- a/docs/CMakeLists.txt +++ b/docs/CMakeLists.txt @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,6 +16,13 @@ # Setup Doxygen input/output find_package(Doxygen) +if(DOXYGEN_DOT_FOUND) + set(DOXYGEN_HAVE_DOT YES) +else() + message(STATUS "Graphviz dot not found — disabling Doxygen graph generation") + set(DOXYGEN_HAVE_DOT NO) +endif() + get_target_property(CORE_SRCPATH nvcv_types SOURCE_DIR) get_target_property(OPS_SRCPATH cvcuda SOURCE_DIR) @@ -69,19 +76,15 @@ add_custom_target(cvcuda_doxygen ALL DEPENDS ${DOXYGEN_INDEX_FILE}) set(SPHINX_SOURCE ${CMAKE_CURRENT_SOURCE_DIR}/sphinx) set(SPHINX_BUILD ${CMAKE_CURRENT_BINARY_DIR}/sphinx) set(SPHINX_INDEX_FILE ${SPHINX_BUILD}/index.html) -set(C_API_RST ${SPHINX_SOURCE}/_c_api) -set(CPP_API_RST ${SPHINX_SOURCE}/_cpp_api) +set(C_CPP_API_RST ${SPHINX_SOURCE}/_c_cpp_api) # Start from clean directory for rst files, otherwise build could be affected due to old files -if(EXISTS ${C_API_RST}) - file(REMOVE_RECURSE ${C_API_RST}) -endif() -if(EXISTS ${CPP_API_RST}) - file(REMOVE_RECURSE ${CPP_API_RST}) +if(EXISTS ${C_CPP_API_RST}) + file(REMOVE_RECURSE ${C_CPP_API_RST}) endif() # Generate rst files for groups from doxygen index.xml -add_custom_target(cvcuda_groups ALL ${PYTHON_EXECUTABLE} ${SPHINX_SOURCE}/generate_groups.py ${C_API_RST} ${CPP_API_RST} ${DOXYGEN_OUTPUT_DIR}/xml +add_custom_target(cvcuda_groups ALL ${PYTHON_EXECUTABLE} ${SPHINX_SOURCE}/generate_groups.py ${C_CPP_API_RST} ${DOXYGEN_OUTPUT_DIR}/xml DEPENDS cvcuda_doxygen) # Create virtual environment and install wheel for documentation @@ -105,7 +108,7 @@ list(GET WHEEL_FILES 0 WHEEL_FILE) message(STATUS \"Installing wheel: \${WHEEL_FILE}\") execute_process( - COMMAND \"\${VENV_PYTHON}\" -m pip install --force-reinstall \"\${WHEEL_FILE}\" + COMMAND \"\${VENV_PYTHON}\" -m pip install --force-reinstall --no-deps \"\${WHEEL_FILE}\" RESULT_VARIABLE INSTALL_RESULT ) @@ -126,7 +129,7 @@ file(TOUCH \"\${MARKER_FILE}\") # this will skip packages installed via the system-site-packages flag # in existing CI setup this will do nothing, on local it may install packages if venv creator # does not properly support system-site-packages flag - COMMAND ${DOC_VENV_PYTHON} -m pip install -r ${CMAKE_SOURCE_DIR}/docker/requirements.sys_python.txt + COMMAND ${DOC_VENV_PYTHON} -m pip install -r ${CMAKE_SOURCE_DIR}/docs/requirements.docs.txt # Find the wheel file dynamically and install it using CMake script COMMAND ${CMAKE_COMMAND} -DWHEEL_DIR=${CMAKE_BINARY_DIR}/python3/repaired_wheels @@ -154,10 +157,16 @@ endif() find_package(CUDAToolkit REQUIRED) get_filename_component(CUDA_LIB_DIR "${CUDAToolkit_LIBRARY_DIR}" ABSOLUTE) +# NOTE: use `-j 1` (serial) rather than `-j auto`. With `-j auto`, each +# worker builds its own C++ domain symbol tree and the main process then +# merges them back, which is O(n^2) in the symbol count. CV-CUDA exposes a +# large C++ API (nvcv::, cvcuda::, enums, macros), and this merge step takes +# longer than the whole serial build would. Single-process is faster here +# and avoids that wart. add_custom_command(OUTPUT ${SPHINX_INDEX_FILE} COMMAND ${CMAKE_COMMAND} -E env "LD_LIBRARY_PATH=${CUDA_LIB_DIR}:$ENV{LD_LIBRARY_PATH}" - ${SPHINX_PYTHON} -m sphinx -j auto -b html + ${SPHINX_PYTHON} -m sphinx -j 1 -b html # Tell Breathe where to find the Doxygen's xml output. Needed to have c/cpp documentation. -Dbreathe_projects.cvcuda=${DOXYGEN_OUTPUT_DIR}/xml ${SPHINX_SOURCE} ${SPHINX_BUILD} diff --git a/docs/Doxyfile.in b/docs/Doxyfile.in index fe31051a7..7220ef15a 100644 --- a/docs/Doxyfile.in +++ b/docs/Doxyfile.in @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -1509,7 +1509,7 @@ FORMULA_TRANSPARENT = YES # The default value is: NO. # This tag requires that the tag GENERATE_HTML is set to YES. -USE_MATHJAX = NO +USE_MATHJAX = YES # When MathJax is enabled you can set the default output format to be used for # the MathJax output. See the MathJax site (see: @@ -1642,7 +1642,7 @@ EXTRA_SEARCH_MAPPINGS = # If the GENERATE_LATEX tag is set to YES, doxygen will generate LaTeX output. # The default value is: YES. -GENERATE_LATEX = YES +GENERATE_LATEX = NO # The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put. If a # relative path is entered the value of OUTPUT_DIRECTORY will be put in front of @@ -2190,7 +2190,7 @@ HIDE_UNDOC_RELATIONS = YES # set to NO # The default value is: YES. -HAVE_DOT = YES +HAVE_DOT = @DOXYGEN_HAVE_DOT@ # The DOT_NUM_THREADS specifies the number of dot invocations doxygen is allowed # to run in parallel. When set to 0 doxygen will base this on the number of diff --git a/ci/build_docs.sh b/docs/build_docs.sh similarity index 75% rename from ci/build_docs.sh rename to docs/build_docs.sh index 78832983c..5e3ef603f 100755 --- a/ci/build_docs.sh +++ b/docs/build_docs.sh @@ -1,6 +1,6 @@ #!/bin/bash -e -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,7 +16,9 @@ # limitations under the License. # Builds documentation based on sphinx and doxygen -# Usage: build_docs.sh [build folder] +# Usage: docs/build_docs.sh [build folder] + +REPO_ROOT=$(dirname "$(dirname "$(readlink -f "$0")")") build_type="release" build_dir="build-rel" @@ -41,7 +43,7 @@ if [ -f "$build_dir/CMakeCache.txt" ]; then # Check if DEFAULT_PYTHON_VER is in EXISTING_PYTHON_VERSIONS if [[ "$EXISTING_PYTHON_VERSIONS" == *"$DEFAULT_PYTHON_VER"* ]]; then # Default version already present, no need to modify PYTHON_VERSIONS - ./ci/build.sh $build_type $build_dir "-DBUILD_DOCS=1 -DBUILD_PYTHON=1 -DDOC_PYTHON_VERSION=$DEFAULT_PYTHON_VER" + "$REPO_ROOT/build.sh" $build_type $build_dir "-DBUILD_DOCS=1 -DBUILD_PYTHON=1 -DDOC_PYTHON_VERSION=$DEFAULT_PYTHON_VER" else # Add DEFAULT_PYTHON_VER to existing list if [ -n "$EXISTING_PYTHON_VERSIONS" ]; then @@ -50,10 +52,10 @@ if [ -f "$build_dir/CMakeCache.txt" ]; then NEW_PYTHON_VERSIONS="$DEFAULT_PYTHON_VER" fi echo "Adding Python $DEFAULT_PYTHON_VER to existing versions: $NEW_PYTHON_VERSIONS" - ./ci/build.sh $build_type $build_dir "-DBUILD_DOCS=1 -DBUILD_PYTHON=1 -DPYTHON_VERSIONS=$NEW_PYTHON_VERSIONS -DDOC_PYTHON_VERSION=$DEFAULT_PYTHON_VER" + "$REPO_ROOT/build.sh" $build_type $build_dir "-DBUILD_DOCS=1 -DBUILD_PYTHON=1 -DPYTHON_VERSIONS=$NEW_PYTHON_VERSIONS -DDOC_PYTHON_VERSION=$DEFAULT_PYTHON_VER" fi else echo "Build directory not configured. Running full configuration for docs..." # Set all flags explicitly for clean docs-only build with single Python version - ./ci/build.sh $build_type $build_dir "-DBUILD_DOCS=1 -DBUILD_TESTS=0 -DBUILD_TESTS_CPP=0 -DBUILD_TESTS_WHEELS=0 -DBUILD_TESTS_PYTHON=0 -DBUILD_PYTHON=1 -DPYTHON_VERSIONS=$DEFAULT_PYTHON_VER -DDOC_PYTHON_VERSION=$DEFAULT_PYTHON_VER" + "$REPO_ROOT/build.sh" $build_type $build_dir "-DBUILD_DOCS=1 -DBUILD_TESTS=0 -DBUILD_TESTS_CPP=0 -DBUILD_TESTS_WHEELS=0 -DBUILD_TESTS_PYTHON=0 -DBUILD_PYTHON=1 -DPYTHON_VERSIONS=$DEFAULT_PYTHON_VER -DDOC_PYTHON_VERSION=$DEFAULT_PYTHON_VER" fi diff --git a/docs/requirements.docs.template b/docs/requirements.docs.template new file mode 100644 index 000000000..de60d3a14 --- /dev/null +++ b/docs/requirements.docs.template @@ -0,0 +1,35 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +# Template for requirements.docs.txt — to regenerate, run from the repo root: +# bash generate_requirements.sh + +# Documentation dependencies (Sphinx ecosystem) +# Installed for the system Python only + +sphinx==${sphinx} +exhale==${exhale} +breathe==${breathe} +sphinx-rtd-theme==${sphinx_rtd_theme} +sphinx-tabs==${sphinx_tabs} +recommonmark==${recommonmark} +graphviz==${graphviz} +sphinxcontrib-jquery==${sphinxcontrib_jquery} +sphinxcontrib-jsmath==${sphinxcontrib_jsmath} +sphinxcontrib-applehelp==${sphinxcontrib_applehelp} +sphinxcontrib-devhelp==${sphinxcontrib_devhelp} +sphinxcontrib-htmlhelp==${sphinxcontrib_htmlhelp} +sphinxcontrib-qthelp==${sphinxcontrib_qthelp} +sphinxcontrib-serializinghtml==${sphinxcontrib_serializinghtml} diff --git a/docs/sphinx/advanced/make_operator.rst b/docs/sphinx/advanced/make_operator.rst index 4cb0ba5a1..8378419b9 100644 --- a/docs/sphinx/advanced/make_operator.rst +++ b/docs/sphinx/advanced/make_operator.rst @@ -1,5 +1,5 @@ .. - # SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,48 +16,369 @@ .. _make_operator: -CV-CUDA Make Operator Tool -=========================== +Adding a New Operator +===================== -The ``mkop.sh`` script is a tool for creating a scaffold for new operators in the CV-CUDA library. It automates several tasks, ensuring consistency and saving time. +This page walks through adding a new operator to CV-CUDA end-to-end: generating +the scaffold, implementing the kernel, writing tests, adding benchmarks, and +updating docs. -Features of ``mkop.sh`` ------------------------- +The ``/make-op`` skill +---------------------- -1. **Operator Stub Creation**: Generates no-op (no-operation) operator templates, which serve as a starting point for implementing new functionalities. +This page is the narrative how-to. For an assisted, gated workflow, use the **make-op skill** +family (defined once under ``.agents/skills/make-op*/SKILL.md``; ``.claude/skills`` symlinks to it +for Claude Code), whose checklist and definition-of-done live in +``.agents/guidance/MAKE_OP_GUIDELINES.md`` and are enforced by the deterministic checker +``tools/make_op.py``. Two modes: -2. **File Customization**: Modifies template files to include the new operator's name, ensuring consistent naming conventions across the codebase. +* **Full end-to-end** — ``/make-op ``: propose the operator's spec (semantics + a cited + reference oracle such as a TorchVision/OpenCV function, plus the support matrix) and **get user + approval**; scaffold; implement; then pass the **done-gate** + (``tools/make_op.py --phase done --run``), a deterministic regression checklist that + reuses ``/review-op`` (all domains) and ``/optimize-op`` preflight and additionally requires an + independent CPU gold reference, **bit-exact** coverage across the declared support matrix, + required equivalent-layout parity, complement negatives, the operator in the release notes, and + tests that actually run and pass. It then hands off to ``/optimize-op`` for performance. +* **Scaffold-only** — ``/make-op-scaffold [--bare]``: produce a wired, building skeleton + and stop, delegating the implementation to a human or another AI. -3. **CMake Integration**: Adds the new operator files to the appropriate CMakeLists, facilitating seamless compilation and integration into the build system. +The steps below are what those skills automate; follow them directly for a manual workflow. -4. **Python Bindings**: Creates Python wrapper stubs for the new operator, allowing it to be used within Python environments. +.. _scaffold: -5. **Test Setup**: Generates test files for both C++ and Python, enabling immediate development of unit tests for the new operator. +Step 1: Generate the Scaffold +------------------------------ -How to Use ``mkop.sh`` +The ``mkop.sh`` script generates no-op stubs for every file a new operator +needs and wires them into the build system. Run it from the repository root: + +.. code-block:: shell + + tools/mkop/mkop.sh + +The first letter of *OperatorName* is capitalized automatically, so ``clahe`` +and ``Clahe`` both produce ``Clahe`` as the canonical name. + +**Generated files** + +.. list-table:: + :widths: 50 50 + :header-rows: 1 + + * - File + - Purpose + * - ``src/cvcuda/include/cvcuda/Op.h`` + - Public C API header + * - ``src/cvcuda/include/cvcuda/Op.hpp`` + - Public C++ header + * - ``src/cvcuda/Op.cpp`` + - C API implementation (dispatches to private impl) + * - ``src/cvcuda/priv/Op.cpp`` + - Private implementation stub + * - ``src/cvcuda/priv/Op.hpp`` + - Private implementation header + * - ``tests/cvcuda/system/TestOp.cpp`` + - C++ system test stub + * - ``python/mod_cvcuda/operators/Op.cpp`` + - Python binding stub (under ``operators/``) + * - ``tests/cvcuda/python/test_op.py`` + - Python test stub + * - ``bench/cpp/ops/Bench.cpp`` + - C++ benchmark stub + * - ``bench/python/ops/bench_.py`` + - Python benchmark stub + * - ``bench/config/operators/.json`` + - Benchmark config stub (tiers, layout axis, declared dtypes) + +The script also updates these existing files: + +* ``src/cvcuda/priv/CMakeLists.txt`` +* ``src/cvcuda/CMakeLists.txt`` +* ``tests/cvcuda/system/CMakeLists.txt`` +* ``python/mod_cvcuda/CMakeLists.txt`` +* ``python/mod_cvcuda/Main.cpp`` +* ``python/mod_cvcuda/operators/Operators.hpp`` +* ``bench/cpp/CMakeLists.txt`` and ``bench/python/CMakeLists.txt`` +* ``bench/config/bench_params.json`` (the benchmark manifest) +* ``docs/sphinx/operator_list.rst`` (the operator table row) +* ``docs/sphinx/modules/python/operators.rst`` (the ``autofunction`` directives) +* the latest release notes (``docs/sphinx/relnotes/vX.Y.Z-*.rst``) — a "New Features" bullet + +.. note:: + + ``mkop.sh`` writes the Python binding directly under ``python/mod_cvcuda/operators/`` and + wires the benchmark, documentation, and release-note stubs listed above. The generated stubs + are intentionally minimal (interleaved ``NHWC`` Tensor only, with ``TODO(make-op)`` markers); + extend them while implementing. The private implementation is generated as ``.cpp`` — rename + it to ``.cu`` (and update ``src/cvcuda/priv/CMakeLists.txt``) only if you write CUDA device + code directly in it; operators that call into legacy kernels keep ``.cpp``. + +.. _implement: + +Step 2: Implement the Operator +------------------------------- + +1. **Rename the private implementation to** ``.cu`` for CUDA kernels: + + .. code-block:: shell + + mv src/cvcuda/priv/Op.cpp src/cvcuda/priv/Op.cu + + Update the filename in ``src/cvcuda/priv/CMakeLists.txt`` to match. + +2. **Write the CUDA kernel** in ``src/cvcuda/priv/Op.cu``. + +3. **Make the operator multi-GPU safe** — see :ref:`multi_gpu` below. + +4. **Document supported layouts and data types** in the Doxygen comment for + each function in ``src/cvcuda/include/cvcuda/Op.h``, using the + standard "Limitations" table format: + + .. code-block:: c + + /* + * Limitations: + * + * Input: + * Data Layout: [kNHWC, kHWC] + * Channels: [1, 3, 4] + * + * Data Type | Allowed + * -------------- | ------------- + * 8bit Unsigned | Yes + * 8bit Signed | No + * 16bit Unsigned | Yes + * 32bit Float | Yes + * ... + */ + + Image operators declare both interleaved (``NHWC``/``HWC``) and planar + (``NCHW``/``CHW``) layouts by default. If image layouts do not apply, add this + operator-local declaration beside the Limitations table instead of maintaining + a central exception list: + + .. code-block:: text + + Planar image layouts: Not applicable + Reason: + + The implementation must enforce these constraints at runtime and return + ``NVCV_ERROR_INVALID_ARGUMENT`` for unsupported combinations. + +5. **Add negative tests** to ``tests/cvcuda/system/TestOp.cpp`` that + verify unsupported formats and dtypes are rejected. Use a parameterized + ``NVCV_TEST_SUITE_P`` value list for operators with many rejected + combinations (see ``TestOpResize.cpp``), or individual ``TEST(..._Negative, + ...)`` blocks for simpler cases (see ``TestOpCLAHE.cpp``). Each case + should assert ``NVCV_ERROR_INVALID_ARGUMENT``. + +6. **Expose the parameters** in the public C and C++ headers + (``Op.h`` and ``Op.hpp``). + +7. **Build and run the tests** to confirm the implementation is correct: + + .. code-block:: shell + + cmake --build build-rel --target cvcuda_test_system + ctest --test-dir build-rel -R TestOp + +.. _multi_gpu: + +Multi-GPU Safety +---------------- + +Any operator that allocates device memory must do so with multi-GPU in mind. +The naive approach — calling ``cudaMalloc`` in the constructor and storing the +pointer as a member — allocates on whatever GPU happens to be current at +construction time. If the operator is later invoked on a different GPU the +kernel will access memory that lives on the wrong device, causing silent +corruption or a CUDA error. + +**The pattern: ``PerDeviceResource``** + +``src/cvcuda/priv/PerDeviceResource.hpp`` provides a template that lazily +creates one instance of ``T`` per CUDA device. The factory runs the first +time ``get()`` is called from a new device, with that device already set as +current. Destruction also sets the correct device before calling the +destructor, so ``cudaFree`` always targets the right GPU. + +Put device allocations in a small helper struct and wrap it: + +.. code-block:: cpp + + // Op.hpp + #include "PerDeviceResource.hpp" + + struct DeviceBuffers + { + void *buf = nullptr; + + DeviceBuffers(/* constructor params */) + { + NVCV_CHECK_THROW(cudaMalloc(&buf, size)); + } + + ~DeviceBuffers() + { + if (buf) NVCV_CHECK_LOG(cudaFree(buf)); + } + }; + + class final : public IOperator + { + // ... + mutable PerDeviceResource<DeviceBuffers> m_deviceBuffers; + }; + +Initialise it in the constructor with a factory lambda that captures the +parameters needed to size the allocation: + +.. code-block:: cpp + + // Op.cu (constructor) + ::(/* params */) + : /* other members */ + , m_deviceBuffers([/* capture params */](int /*deviceId*/) + { return std::make_unique<DeviceBuffers>(/* params */); }) + { + } + +And call ``m_deviceBuffers.get()`` in ``operator()``: + +.. code-block:: cpp + + void ::operator()(cudaStream_t stream, ...) const + { + RunKernel(..., m_deviceBuffers.get().buf, stream); + } + +See ``src/cvcuda/priv/OpCLAHE.hpp`` and ``OpCLAHE.cu`` for a complete +reference implementation. + +.. _testing: + +Step 3: Write Tests +-------------------- + +The scaffold generates stubs for both C++ and Python tests. Fill them in as +described below. See ``TestOpCLAHE.cpp`` and ``test_opclahe.py`` as reference +implementations. + +**C++ correctness tests** (``tests/cvcuda/system/TestOp.cpp``) + +The standard approach is: + +1. Write a plain-C++ reference implementation of the operator (or, + alternatively, test against known reference outputs). +2. Use ``NVCV_TEST_SUITE_P`` to define a parameterized table of inputs + (sizes, batch sizes, dtypes, operator parameters): + + .. code-block:: cpp + + NVCV_TEST_SUITE_P(Op, test::ValueList + { + // width, height, batches, ... + { 64, 64, 1 }, + { 320, 240, 4 }, + }); + +3. In the test body, fill input tensors with random data, run the operator, + copy results back to host, and compare against the reference with a tight + pixel tolerance: + + .. code-block:: cpp + + TEST_P(Op, tensor_correct_output) + { + // ... create tensors, fill with random data ... + cvcuda:: op(/* params */); + EXPECT_NO_THROW(op(stream, in, out, /* params */)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + // compare out vs. reference result + } + +4. Add a separate ``TEST(Op, varshape_correct_output)`` that builds an + ``nvcv::ImageBatchVarShape`` with randomly sized images and runs the same + comparison. + +5. Add ``TEST(Op_Negative, ...)`` cases (covered in step 2 above). + +**Python smoke tests** (``tests/cvcuda/python/test_op.py``) + +Python tests focus on the API surface rather than numerical correctness +(the C++ tests own that). Cover: + +* ``Tensor`` inputs with ``NHWC`` and ``HWC`` layouts using + ``@pytest.mark.parametrize`` over a small set of shapes. +* ``ImageBatchVarShape`` inputs. +* Both the allocating variant (``cvcuda.(src, ...)``) and the + in-place variant (``cvcuda._into(dst, src, ...)``), asserting that + the output has the expected ``shape``, ``layout``, and ``dtype``. +* Negative cases with ``pytest.raises(Exception)`` for unsupported formats + and invalid parameter values. + +.. _benchmarks: + +Step 4: Add Benchmarks ----------------------- -Run the script with the desired operator name. The script assumes it's located in ``cvcuda/tools/mkop``. +Every operator requires matching C++ and Python benchmarks. See +``bench/README.md`` in the repository root for full benchmark documentation. -.. code-block:: shell +**C++ benchmark** — create ``bench/cpp/ops/Bench.cpp`` and register it +in ``bench/cpp/CMakeLists.txt``. - ./mkop.sh [Operator Name] +**Python benchmark** — create ``bench/python/ops/bench_.py`` and +register it in ``bench/python/CMakeLists.txt``. -If the script is run from a different location, provide the path to the CV-CUDA root directory. +**Shared configuration** — add an entry to ``bench/config/bench_params.json``. +This file is the single source of truth for parameter axes in both languages: -.. code-block:: shell +.. code-block:: json + + "": { + "dtypes": ["uint8"], + "string_axes": { + "shape": ["16x1080x1920"] + }, + "int64_axes": { + "varShape": [-1, 0] + } + } + +.. note:: + + Choose batch sizes (the leading dimension of ``shape``) so that each + benchmark configuration runs for roughly **1–2 ms** on an H100 (or + equivalent GPU). Kernels that finish in tens of microseconds have high + relative timing noise; kernels that run for tens of milliseconds make the + full benchmark suite slow. The 1–2 ms range gives nvbench enough signal + to produce stable, low-noise measurements. + +.. _documentation: - ./mkop.sh [Operator Name] [CV-CUDA root] +Step 5: Fill in the Documentation Stubs +--------------------------------------- -**NOTE**: The first letter of the new operator name is captitalized where needed to match the rest of the file structures. +``mkop.sh`` has **already inserted** the documentation entries listed above — +the ``operator_list.rst`` table row, the ``operators.rst`` ``autofunction`` +directives (``cvcuda.`` and ``cvcuda._into``), and the release-note +bullet — each with a ``TODO(make-op)`` placeholder. You do **not** add these +rows by hand; you only replace the placeholder text with real content: -Process Details ---------------- +1. **Operator list** — in ``docs/sphinx/operator_list.rst``, replace the + ``TODO(make-op)`` description on the generated row: -* **Initial Setup**: The script begins by validating the input and setting up necessary variables. It then capitalizes the first letter of the operator name to adhere to naming conventions. + .. code-block:: rst -* **Template Modification**: It processes various template files (``Public.h``, ``PrivateImpl.cpp``, etc.), replacing placeholders with the new operator name. This includes adjusting file headers, namespaces, and function signatures. + * - (:py:func:`cvcuda.`) + - Brief description of what the operator does. -* **CMake and Python Integration**: The script updates ``CMakeLists.txt`` files and Python module files to include the new operator, ensuring it's recognized by the build system and Python interface. +2. **Python API reference** — the ``autofunction`` directives in + ``docs/sphinx/modules/python/operators.rst`` are already in place; confirm + they render once the pybind docstrings (written with the binding in Step 2) are in place. -* **Testing Framework**: Finally, it sets up test files for both C++ and Python, allowing developers to immediately start writing tests for the new operator. +3. **Release notes** — replace the ``TODO(make-op)`` text in the generated + "New Features and Enhancements" bullet of the latest + ``docs/sphinx/relnotes/vX.Y.Z-*.rst``. diff --git a/docs/sphinx/advanced/object_cache.rst b/docs/sphinx/advanced/object_cache.rst index 6e7fefc6b..382a0c6ff 100644 --- a/docs/sphinx/advanced/object_cache.rst +++ b/docs/sphinx/advanced/object_cache.rst @@ -1,5 +1,5 @@ .. - # SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -128,7 +128,7 @@ The cache uses thread-local storage internally. Objects created in one thread ca You can clear the cache for the current thread using :py:func:`cvcuda.clear_cache` with :py:class:`cvcuda.ThreadScope`.LOCAL and query the thread-local cache size with :py:func:`cvcuda.cache_size` with :py:class:`cvcuda.ThreadScope`.LOCAL: -.. literalinclude:: ../../../samples/object_cache/threading.py +.. literalinclude:: ../../../samples/object_cache/threads.py :language: python :start-after: # docs-start: main :end-before: # docs-end: main diff --git a/docs/sphinx/advanced/operator_variants.rst b/docs/sphinx/advanced/operator_variants.rst new file mode 100644 index 000000000..2127d6463 --- /dev/null +++ b/docs/sphinx/advanced/operator_variants.rst @@ -0,0 +1,107 @@ +.. + # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-License-Identifier: Apache-2.0 + # + # Licensed 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. + +.. _operator_variants: + +Allocating vs. Pre-allocated Operator Variants +=============================================== + +Every CV-CUDA Python operator is available in two forms: + +* **Allocating variant** — ``cvcuda.(src, ...)`` allocates and returns a new output tensor. +* **Pre-allocated variant** — ``cvcuda._into(dst, src, ...)`` writes into a caller-supplied output tensor. + +Understanding when to use each form can have a meaningful impact on pipeline throughput. + +How the Allocating Variant Works +--------------------------------- + +When you call the allocating form, CV-CUDA creates the output tensor for you: + +.. code-block:: python + + out = cvcuda.resize(src, out_shape, cvcuda.Interp.LINEAR, stream=stream) + +Internally this calls ``Tensor.Create``, which consults the :ref:`object_cache` for a +previously freed tensor with matching shape, layout, and dtype. If one is found it is +reused; otherwise a new GPU allocation is made. Either way, a cache lookup or +allocation occurs on every call. + +How the Pre-allocated Variant Works +------------------------------------- + +When you call the ``_into`` form, no cache interaction takes place: + +.. code-block:: python + + cvcuda.resize_into(out, src, cvcuda.Interp.LINEAR, stream=stream) + +The operator writes directly into ``out``. The object cache is not consulted, and no +GPU memory is allocated. The return value is the same ``out`` tensor that was passed in. + +When to Use Each Variant +-------------------------- + +**Use the allocating variant** when: + +* You are writing a one-shot script or prototype where throughput is not the priority. +* The output shape changes from call to call (e.g. variable-resolution inputs with + different target sizes), because caching handles the varying shapes for you. + +**Use the pre-allocated (``_into``) variant** when: + +* You have a fixed-shape inference pipeline (the common case for batch pre-processing) + and can allocate output tensors once at startup. +* You are calling an operator in a tight loop and want to eliminate the per-iteration + cache overhead. +* You manage your own buffer pool and need deterministic memory behaviour. + +Example: pre-allocating for a fixed-shape pipeline +--------------------------------------------------- + +.. code-block:: python + + import cvcuda + + BATCH, H, W, C = 8, 224, 224, 3 + OUT_SHAPE = (BATCH, H, W, C) + + # Allocate output tensor once, before the loop. + resize_out = cvcuda.Tensor(OUT_SHAPE, cvcuda.Type.U8, "NHWC") + + stream = cvcuda.Stream() + with stream: + for frame_batch in data_loader: + # No allocation on each iteration — writes directly into resize_out. + cvcuda.resize_into(resize_out, frame_batch, cvcuda.Interp.LINEAR) + # ... further processing ... + +Constraints +----------- + +When using the ``_into`` variant the output tensor you supply must already have the +correct shape, layout, and dtype that the operator would have produced. Passing an +incompatible tensor raises an exception. + +.. note:: + + ``_into`` variants are available for all standard operators. The ``ImageBatchVarShape`` + overloads follow the same pattern: ``cvcuda._into(dst_batch, src_batch, ...)``. + +.. seealso:: + + :ref:`object_cache` — detailed description of the CV-CUDA object cache, including + cache reuse, growth control, and multi-threading considerations. diff --git a/docs/sphinx/conf.py b/docs/sphinx/conf.py index 9e387f115..a3370a5eb 100644 --- a/docs/sphinx/conf.py +++ b/docs/sphinx/conf.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -28,14 +28,16 @@ import os import sys -import sphinx_rtd_theme + +# Ensure local extensions in docs/sphinx/ are importable +sys.path.insert(0, os.path.abspath(os.path.dirname(__file__))) # -- Project information ----------------------------------------------------- project = "CV-CUDA" -copyright = "2022-2025, NVIDIA." +copyright = "2022-2026, NVIDIA." author = "NVIDIA" -version = "Beta" +version = "0.17.0" release = version # cvcuda module imported from virtual environment @@ -80,6 +82,22 @@ # Tell sphinx what the pygments highlight language should be. highlight_language = "cpp" +# CUDA attribute keywords that Sphinx's C++ domain parser doesn't natively +# understand; without this every `__device__` / `__host__` / etc. function +# emitted by Doxygen triggers "Error when parsing function declaration". +cpp_id_attributes = [ + "__device__", + "__host__", + "__global__", + "__forceinline__", + "__noinline__", + "__shared__", + "__constant__", + "__managed__", + "__restrict__", +] +cpp_paren_attributes = ["__declspec"] + autodoc_inherit_docstrings = False # -- Options for HTML output ------------------------------------------------- @@ -88,14 +106,11 @@ # a list of builtin themes. # -html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] - html_theme = "sphinx_rtd_theme" html_logo = os.path.join("content", "nv_logo.png") html_theme_options = { "logo_only": True, - "display_version": True, "prev_next_buttons_location": "bottom", "style_external_links": False, "style_nav_header_background": "#000000", @@ -147,6 +162,10 @@ def setup(app): extensions.append("sphinx.ext.napoleon") extensions.append("sphinx_tabs.tabs") +# Injects C API limitations into Python operator autodoc pages. +# Must be registered after napoleon so the hook receives post-Napoleon RST. +extensions.append("cvcuda_limitations_ext") + # -- Extension configuration ------------------------------------------------- # Set up the default project for breathe extension breathe_default_project = "cvcuda" diff --git a/docs/sphinx/content/cat_adaptivethreshold.jpg b/docs/sphinx/content/cat_adaptivethreshold.jpg new file mode 100644 index 000000000..527a00deb --- /dev/null +++ b/docs/sphinx/content/cat_adaptivethreshold.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8a42bff73c52f440f49795c3bad36f85244dbe84227307e293dfe636620eca76 +size 212026 diff --git a/docs/sphinx/content/cat_advcvtcolor.jpg b/docs/sphinx/content/cat_advcvtcolor.jpg new file mode 100644 index 000000000..6a572e366 --- /dev/null +++ b/docs/sphinx/content/cat_advcvtcolor.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:721350e0af461bda47f505f8733c5157319c01482da144f42516ff2f3dbbfb9d +size 69881 diff --git a/docs/sphinx/content/cat_averageblur.jpg b/docs/sphinx/content/cat_averageblur.jpg new file mode 100644 index 000000000..cd3c2911f --- /dev/null +++ b/docs/sphinx/content/cat_averageblur.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:85fa1aa5bad03541974f141e3c607b1d46f9069ce91ac3e78cbc2401429a523a +size 41455 diff --git a/docs/sphinx/content/cat_bilateral_filter.jpg b/docs/sphinx/content/cat_bilateral_filter.jpg new file mode 100644 index 000000000..6ddd9c301 --- /dev/null +++ b/docs/sphinx/content/cat_bilateral_filter.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:153f3a0e0120abd252cf0bb53422a47942556cce17703f5e7c775fe4979952bc +size 55251 diff --git a/docs/sphinx/content/cat_bndbox.jpg b/docs/sphinx/content/cat_bndbox.jpg new file mode 100644 index 000000000..84f87b767 --- /dev/null +++ b/docs/sphinx/content/cat_bndbox.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ac60ca9f54a2ca9329e302ae3da136ec8d639d43e677b68f1fbb80a483744502 +size 79002 diff --git a/docs/sphinx/content/cat_boxblur.jpg b/docs/sphinx/content/cat_boxblur.jpg new file mode 100644 index 000000000..ed83234c5 --- /dev/null +++ b/docs/sphinx/content/cat_boxblur.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a94be5cd54b619340b806e939760f4261bb9847cbdeb2bbd3cd337435a3f300a +size 45914 diff --git a/docs/sphinx/content/cat_brightness_contrast.jpg b/docs/sphinx/content/cat_brightness_contrast.jpg new file mode 100644 index 000000000..2bb066e8e --- /dev/null +++ b/docs/sphinx/content/cat_brightness_contrast.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1c59fd8b0671decc25420a0ceb99ecdaae4b4530a00ef566d09b626a09652c88 +size 88486 diff --git a/docs/sphinx/content/cat_center_crop.jpg b/docs/sphinx/content/cat_center_crop.jpg new file mode 100644 index 000000000..6ba0ef430 --- /dev/null +++ b/docs/sphinx/content/cat_center_crop.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:64bf24054906f3dc9ea5c7ef242a730fcb88624474e062cc1469f27968049e39 +size 14514 diff --git a/docs/sphinx/content/cat_channelreorder.jpg b/docs/sphinx/content/cat_channelreorder.jpg new file mode 100644 index 000000000..4a89c2df3 --- /dev/null +++ b/docs/sphinx/content/cat_channelreorder.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c2771b68f84c695616e35f135366729c405837d9e0f7456c3c533c1b84ce1db3 +size 69823 diff --git a/docs/sphinx/content/cat_clahe.jpg b/docs/sphinx/content/cat_clahe.jpg new file mode 100644 index 000000000..39913b609 --- /dev/null +++ b/docs/sphinx/content/cat_clahe.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:54815ca3d1cfc4083ae124738f480ae390c8189aab85a65f1c85f55ad59a3988 +size 91907 diff --git a/docs/sphinx/content/cat_color_twist.jpg b/docs/sphinx/content/cat_color_twist.jpg new file mode 100644 index 000000000..5228587d1 --- /dev/null +++ b/docs/sphinx/content/cat_color_twist.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ac6c16d3c27e8a6b494cd9f3af112020fcc1adfed5f006391d24f613e7cec859 +size 73748 diff --git a/docs/sphinx/content/cat_composite.jpg b/docs/sphinx/content/cat_composite.jpg new file mode 100644 index 000000000..c1ae18058 --- /dev/null +++ b/docs/sphinx/content/cat_composite.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:18cde2b06882b8670e286161af586df77bfea6838798a9ec31493bcdee851b7f +size 92025 diff --git a/docs/sphinx/content/cat_conv2d.jpg b/docs/sphinx/content/cat_conv2d.jpg new file mode 100644 index 000000000..ceaee8e2c --- /dev/null +++ b/docs/sphinx/content/cat_conv2d.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:67671042ae3415ae2ed2a489ba579e6d3fc0b95c3603fc35c02ead84201af572 +size 112657 diff --git a/docs/sphinx/content/cat_convertto.jpg b/docs/sphinx/content/cat_convertto.jpg new file mode 100644 index 000000000..25f62713a --- /dev/null +++ b/docs/sphinx/content/cat_convertto.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0dc34c18d179f36892488ea363073fc39729dc7d8fc5cfd1ec0ba9070ed5e1ed +size 69274 diff --git a/docs/sphinx/content/cat_copymakeborder.jpg b/docs/sphinx/content/cat_copymakeborder.jpg new file mode 100644 index 000000000..77a5cf326 --- /dev/null +++ b/docs/sphinx/content/cat_copymakeborder.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:90fe6215e102a170e9a6d87b39b0b703ff9f4bf12d8d58a50f13a7184af67782 +size 81668 diff --git a/docs/sphinx/content/cat_crop_flip_normalize_reformat.jpg b/docs/sphinx/content/cat_crop_flip_normalize_reformat.jpg new file mode 100644 index 000000000..895507c3d --- /dev/null +++ b/docs/sphinx/content/cat_crop_flip_normalize_reformat.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8146cc22d91f8a175f26dc01cdeaaa9ff4776d5d0f2d789721262ef59563e150 +size 46403 diff --git a/docs/sphinx/content/cat_customcrop.jpg b/docs/sphinx/content/cat_customcrop.jpg new file mode 100644 index 000000000..2ab296e8e --- /dev/null +++ b/docs/sphinx/content/cat_customcrop.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:050e636de7f2c3b1be74be6ee54a96aea48d98c320211bde555a84e366dced5f +size 38228 diff --git a/docs/sphinx/content/cat_cvtcolor.jpg b/docs/sphinx/content/cat_cvtcolor.jpg new file mode 100644 index 000000000..4a89c2df3 --- /dev/null +++ b/docs/sphinx/content/cat_cvtcolor.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c2771b68f84c695616e35f135366729c405837d9e0f7456c3c533c1b84ce1db3 +size 69823 diff --git a/docs/sphinx/content/cat_erase.jpg b/docs/sphinx/content/cat_erase.jpg new file mode 100644 index 000000000..4b465c44d --- /dev/null +++ b/docs/sphinx/content/cat_erase.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:85700c65032873464ff0ae9334ceea02efdbdd38da0c3d28adb4ce88534cba6b +size 62216 diff --git a/docs/sphinx/content/cat_flip.jpg b/docs/sphinx/content/cat_flip.jpg new file mode 100644 index 000000000..65d2d46fa --- /dev/null +++ b/docs/sphinx/content/cat_flip.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b7d2c72cbf60ee5f69c5370dd4f06f442f5c6222085440375ab5db8d3966eaed +size 69248 diff --git a/docs/sphinx/content/cat_gamma_contrast.jpg b/docs/sphinx/content/cat_gamma_contrast.jpg new file mode 100644 index 000000000..ae28ee3ab --- /dev/null +++ b/docs/sphinx/content/cat_gamma_contrast.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0c2cd03207ac316a943ef84dd8c45076ad1a6aa330e96da1a0ffda2b85c4681f +size 74825 diff --git a/docs/sphinx/content/cat_gaussiannoise.jpg b/docs/sphinx/content/cat_gaussiannoise.jpg new file mode 100644 index 000000000..c0db15cc3 --- /dev/null +++ b/docs/sphinx/content/cat_gaussiannoise.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:edef06bff7e68d31892b01554e9f595096f882a039f80f553849e01a2d8feecd +size 205091 diff --git a/docs/sphinx/content/cat_histogrameq.jpg b/docs/sphinx/content/cat_histogrameq.jpg new file mode 100644 index 000000000..692372d44 --- /dev/null +++ b/docs/sphinx/content/cat_histogrameq.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c734d1bacc4fdf79b86422b9e74efdda3a5fba62554eaeb2f01c4f76e58a9608 +size 75970 diff --git a/docs/sphinx/content/cat_hq_resize.jpg b/docs/sphinx/content/cat_hq_resize.jpg new file mode 100644 index 000000000..7e1a74b8a --- /dev/null +++ b/docs/sphinx/content/cat_hq_resize.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a422b9a4e021a86391913d9a2663a9f5e79b74a9822353284a1ad2cdbefb9aa7 +size 9554 diff --git a/docs/sphinx/content/cat_inpaint.jpg b/docs/sphinx/content/cat_inpaint.jpg new file mode 100644 index 000000000..3bfbbb363 --- /dev/null +++ b/docs/sphinx/content/cat_inpaint.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:df7d72630540c5f03c8c005c34e0a76c738a08315ab61a51a9f38b5abb42cb99 +size 69269 diff --git a/docs/sphinx/content/cat_inpaint_damaged.jpg b/docs/sphinx/content/cat_inpaint_damaged.jpg new file mode 100644 index 000000000..3ea962d25 --- /dev/null +++ b/docs/sphinx/content/cat_inpaint_damaged.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:257165753207e49f88a9a3a02ee03009d56e4e2bafa4b4ae3f3a906add0c87c3 +size 261929 diff --git a/docs/sphinx/content/cat_joint_bilateral_filter.jpg b/docs/sphinx/content/cat_joint_bilateral_filter.jpg new file mode 100644 index 000000000..e085baff5 --- /dev/null +++ b/docs/sphinx/content/cat_joint_bilateral_filter.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:28d49f46fcd4947c85833069b01c0dd07d7212effabb426e37daf836d53f5419 +size 55225 diff --git a/docs/sphinx/content/cat_labeled.jpg b/docs/sphinx/content/cat_labeled.jpg index becd5c350..401cb9f8b 100644 --- a/docs/sphinx/content/cat_labeled.jpg +++ b/docs/sphinx/content/cat_labeled.jpg @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0875d1875ab467e2ce45750992b8137e6a708f20b70058d3e1aebb7c06153fd3 -size 96671 +oid sha256:2904695b1916c69e8a7a2ac4bc3216bd460eefac3e2778d4d6e98cc3bc647437 +size 95454 diff --git a/docs/sphinx/content/cat_laplacian.jpg b/docs/sphinx/content/cat_laplacian.jpg new file mode 100644 index 000000000..0e863d3b3 --- /dev/null +++ b/docs/sphinx/content/cat_laplacian.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:458c1d3d381fd858bb88e08b870d41ae5d97f4f2449e0eb0a7d7b03395552969 +size 120794 diff --git a/docs/sphinx/content/cat_median_blur.jpg b/docs/sphinx/content/cat_median_blur.jpg new file mode 100644 index 000000000..3f17ad5a0 --- /dev/null +++ b/docs/sphinx/content/cat_median_blur.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3527051f0f882b5cb4f3cc8978e227f41c43fd258af29dc810365ac8f1255227 +size 43587 diff --git a/docs/sphinx/content/cat_morphology.jpg b/docs/sphinx/content/cat_morphology.jpg new file mode 100644 index 000000000..4b188f5dd --- /dev/null +++ b/docs/sphinx/content/cat_morphology.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:983dda8d286b700c3647f03b031b2839a809d96e294a48bfb40d5d645cbff6ea +size 54050 diff --git a/docs/sphinx/content/cat_normalize.jpg b/docs/sphinx/content/cat_normalize.jpg new file mode 100644 index 000000000..0ba49e158 --- /dev/null +++ b/docs/sphinx/content/cat_normalize.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7c50841fb4432897a06185b4106e97c43ba3671c05d3a154010d6ab160ce7122 +size 67769 diff --git a/docs/sphinx/content/cat_osd.jpg b/docs/sphinx/content/cat_osd.jpg new file mode 100644 index 000000000..60c591cc0 --- /dev/null +++ b/docs/sphinx/content/cat_osd.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1aef2e89b66e6bd3cca9bd6829ea175efe33a39b96ebd7dfb94d02198756196e +size 88161 diff --git a/docs/sphinx/content/cat_pillowresize.jpg b/docs/sphinx/content/cat_pillowresize.jpg new file mode 100644 index 000000000..c7d726b67 --- /dev/null +++ b/docs/sphinx/content/cat_pillowresize.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:716b4eb5ea9dc47f71f0bf3b3da5142c6ffc3c8f6a16e3ff4a2ba43de7728cd4 +size 9564 diff --git a/docs/sphinx/content/cat_random_resized_crop.jpg b/docs/sphinx/content/cat_random_resized_crop.jpg new file mode 100644 index 000000000..97deb2551 --- /dev/null +++ b/docs/sphinx/content/cat_random_resized_crop.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3c1c722d90b564f62c081fc7029ce73d1d590331fd4a0c71b8058b7514816c74 +size 10882 diff --git a/docs/sphinx/content/cat_remap.jpg b/docs/sphinx/content/cat_remap.jpg new file mode 100644 index 000000000..218be5f7d --- /dev/null +++ b/docs/sphinx/content/cat_remap.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2409e1bb9a132ca49161f3b4bcf8f0e1ebebb502617c04a0e8f259705c45a76c +size 66087 diff --git a/docs/sphinx/content/cat_resize_crop_convert_reformat.jpg b/docs/sphinx/content/cat_resize_crop_convert_reformat.jpg new file mode 100644 index 000000000..40f483e0d --- /dev/null +++ b/docs/sphinx/content/cat_resize_crop_convert_reformat.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c8120cea4d08917cdad2adfb1d56e1ae3636bf68158ee0db99c4163444517f9c +size 10366 diff --git a/docs/sphinx/content/cat_rotate.jpg b/docs/sphinx/content/cat_rotate.jpg new file mode 100644 index 000000000..cba8d2f92 --- /dev/null +++ b/docs/sphinx/content/cat_rotate.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:af1a4d2d9f2e65441de8a26f9c67f09773d2d9be6eb55df0341ee4024f1839a9 +size 63507 diff --git a/docs/sphinx/content/cat_threshold.jpg b/docs/sphinx/content/cat_threshold.jpg new file mode 100644 index 000000000..b2351ed5d --- /dev/null +++ b/docs/sphinx/content/cat_threshold.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:468657e936ead0fdb57b2d8ccbc5c71202e9174b91742f460d0671bd752c6221 +size 178698 diff --git a/docs/sphinx/content/cat_warp_affine.jpg b/docs/sphinx/content/cat_warp_affine.jpg new file mode 100644 index 000000000..63afe1555 --- /dev/null +++ b/docs/sphinx/content/cat_warp_affine.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:67820a7ea2c18f8dc1de62221b026d13f8611963b62854faaf1f6cd13b0d1e69 +size 63313 diff --git a/docs/sphinx/content/cat_warp_perspective.jpg b/docs/sphinx/content/cat_warp_perspective.jpg new file mode 100644 index 000000000..1c362888a --- /dev/null +++ b/docs/sphinx/content/cat_warp_perspective.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a982a55eb4764e1055f85edaadab302ec8fc58144e75173971c6a61b21766749 +size 53639 diff --git a/docs/sphinx/cvcuda_limitations_ext.py b/docs/sphinx/cvcuda_limitations_ext.py new file mode 100644 index 000000000..1d4d97526 --- /dev/null +++ b/docs/sphinx/cvcuda_limitations_ext.py @@ -0,0 +1,413 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""Sphinx extension for CV-CUDA Python operator docs. + +Two jobs: +1. Inject a "Limitations" table (sourced from the C API Doxygen XML) under + every cvcuda.* operator page. +2. Provide a ``cvcuda-autofunction`` directive that splits pybind11's + ``Overloaded function.`` docstring into one ``.. py:function::`` entry per + overload. Sphinx autodoc does not split this format natively — it renders + the numbered list as prose. +""" + +import importlib +import os +import re +import textwrap +import xml.etree.ElementTree as ET +from typing import Optional + +from docutils import nodes +from docutils.statemachine import StringList +from sphinx.ext.autodoc.directive import AutodocDirective +from sphinx.ext.napoleon import Config as NapoleonConfig, GoogleDocstring +from sphinx.util.docutils import SphinxDirective + +# --------------------------------------------------------------------------- # +# Pybind11 overload splitter # +# --------------------------------------------------------------------------- # + +_OVERLOAD_ENTRY = re.compile(r"^\d+\.\s+(\w+\(.*)") +_NAPOLEON_CONFIG = NapoleonConfig( + napoleon_use_param=True, napoleon_use_rtype=True, napoleon_use_keyword=True +) + + +def _parse_pybind11_overloads( + docstring: str, +) -> Optional[list[tuple[str, list[str]]]]: + """Parse pybind11's ``Overloaded function.`` docstring into (sig, body) pairs.""" + if not docstring or "Overloaded function." not in docstring: + return None + overloads: list[tuple[str, list[str]]] = [] + current_sig: Optional[str] = None + current_body: list[str] = [] + for raw_line in docstring.splitlines(): + m = _OVERLOAD_ENTRY.match(raw_line) + if m: + if current_sig is not None: + overloads.append((current_sig, current_body)) + current_sig = m.group(1) + current_body = [] + elif current_sig is not None: + current_body.append(raw_line) + if current_sig is not None: + overloads.append((current_sig, current_body)) + return overloads if len(overloads) >= 2 else None + + +def _body_to_rst(body_lines: list[str]) -> str: + """Dedent and run Napoleon so Args:/Returns: become :param:/:returns:.""" + text = textwrap.dedent("\n".join(body_lines)).strip() + return str(GoogleDocstring(text, _NAPOLEON_CONFIG)) if text else "" + + +class CvcudaAutofunctionDirective(SphinxDirective): + """Replacement for ``autofunction`` that splits pybind11 overloads. + + * Single-overload functions delegate to the stock ``autofunction``. + * Overloaded functions emit one ``.. py:function::`` per overload. + """ + + required_arguments = 1 + optional_arguments = 0 + final_argument_whitespace = False + has_content = False + option_spec = {} + + def run(self) -> list[nodes.Node]: + fullname = self.arguments[0] + modname, attrname = fullname.rsplit(".", 1) + try: + mod = importlib.import_module(modname) + func = getattr(mod, attrname) + except (ImportError, AttributeError): + return self._autofunction(fullname) + + raw_doc = getattr(func, "__doc__", "") or "" + overloads = _parse_pybind11_overloads(raw_doc) + if not overloads: + return self._autofunction(fullname) + + return self._split_overloads(fullname, overloads) + + def _autofunction(self, fullname: str) -> list[nodes.Node]: + ad = AutodocDirective( + "autofunction", + [fullname], + {}, + StringList([]), + self.lineno, + self.content_offset, + "", + self.state, + self.state_machine, + ) + return ad.run() + + def _split_overloads( + self, fullname: str, overloads: list[tuple[str, list[str]]] + ) -> list[nodes.Node]: + base = _func_base_name(fullname) + rst_lines: list[str] = [] + + for idx, (sig, body) in enumerate(overloads): + rst_body = _body_to_rst(body) + + rst_lines.append(f".. py:function:: {sig}") + rst_lines.append(" :module: cvcuda") + if idx > 0: + # Avoid duplicate anchor targets — first overload owns the + # canonical #cvcuda. anchor. + rst_lines.append(" :noindex:") + rst_lines.append("") + + for line in rst_body.splitlines(): + rst_lines.append(f" {line}") + if rst_body: + rst_lines.append("") + + if base in _cache: + rst_lines.append(" .. rubric:: Limitations") + rst_lines.append("") + for lim_line in _cache[base]: + rst_lines.append(f" {lim_line}") + rst_lines.append("") + + vl = StringList(rst_lines, source=f"") + wrapper = nodes.container() + wrapper.document = self.state.document + self.state.nested_parse(vl, 0, wrapper) + return wrapper.children + + +# --------------------------------------------------------------------------- # +# Mapping: Python function name (bare, no _into) → Doxygen group name suffix # +# The XML files live at {breathe_xml_dir}/group__NVCV__C__ALGORITHM__{suffix}.xml +# --------------------------------------------------------------------------- # +_FUNC_TO_GROUP: dict[str, str] = { + "adaptivethreshold": "ADAPTIVETHRESHOLD", + "advcvtcolor": "__ADV__CVT__COLOR", + "averageblur": "AVERAGEBLUR", + "bilateral_filter": "BILATERAL__FILTER", + "bndbox": "__BND__BOX", + "boxblur": "__BOX__BLUR", + "brightness_contrast": "BRIGHTNESS__CONTRAST", + "center_crop": "CENTER__CROP", + "channelreorder": "CHANNEL__REORDER", + "clahe": "__CLAHE", + "color_twist": "COLOR__TWIST", + "composite": "COMPOSITE", + "conv2d": "CONV2D", + "convertto": "CONVERT__TO", + "copymakeborder": "COPYMAKEBORDER", + "copymakeborderstack": "COPYMAKEBORDER", + "crop_flip_normalize_reformat": "CROP__FLIP__NORMALIZE__REFORMAT", + "customcrop": "CUSTOM__CROP", + "cvtcolor": "CVTCOLOR", + "erase": "ERASE", + "findhomography": "FIND__HOMOGRAPHY", + "flip": "FLIP", + "gamma_contrast": "GAMMA__CONTRAST", + "gaussian": "GAUSSIAN", + "gaussiannoise": "GAUSSIAN__NOISE", + "histogram": "__HISTOGRAM", + "histogrameq": "__HISTOGRAM__EQ", + "hq_resize": "HQ__RESIZE", + "inpaint": "INPAINT", + "joint_bilateral_filter": "JOINT__BILATERAL__FILTER", + "label": "LABEL", + "laplacian": "LAPLACIAN", + "match": "PAIRWISE__MATCHER", + "max_loc": "MINMAXLOC", + "median_blur": "MEDIAN__BLUR", + "minarearect": "__MIN__AREA__RECT", + "min_loc": "MINMAXLOC", + "min_max_loc": "MINMAXLOC", + "morphology": "MORPHOLOGY", + "nms": "NON__MAXIMUM__SUPPRESSION", + "normalize": "NORMALIZE", + "osd": "__O__S__D", + "padandstack": "PADANDSTACK", + "pillowresize": "PILLOW__RESIZE", + "random_resized_crop": "RANDOMRESIZEDCROP", + "reformat": "REFORMAT", + "remap": "REMAP", + "resize": "RESIZE", + "resize_crop_convert_reformat": "__RESIZE__CROP", + "rotate": "ROTATE", + "sift": "SIFT", + "stack": "__STACK", + "threshold": "THRESHOLD", + "warp_affine": "WARP__AFFINE", + "warp_perspective": "WARP__PERSPECTIVE", +} + +# Module-level cache populated at builder-inited time +_cache: dict[str, list[str]] = {} + + +# --------------------------------------------------------------------------- # +# XML → RST conversion helpers # +# --------------------------------------------------------------------------- # + + +def _table_to_rst(table_elem) -> list[str]: + rows = table_elem.findall("row") + if not rows: + return [] + lines = [".. list-table::", " :header-rows: 1", " :widths: auto", ""] + for row in rows: + entries = row.findall("entry") + for j, entry in enumerate(entries): + text = "".join(entry.itertext()).strip() + lines.append(f" * - {text}" if j == 0 else f" - {text}") + lines.append("") + return lines + + +def _verbatim_to_rst(text: str) -> list[str]: + if not text or not text.strip(): + return [] + lines = [".. code-block:: text", ""] + for line in text.splitlines(): + lines.append(f" {line}") + lines.append("") + return lines + + +def _para_to_rst(para) -> list[str]: + """Convert a element to RST lines, stopping before .""" + table = para.find("table") + if table is not None: + rst: list[str] = [] + if para.text and para.text.strip(): + rst.append(para.text.strip()) + rst.append("") + rst.extend(_table_to_rst(table)) + return rst + + lines: list[str] = [] + buf = para.text or "" + + for child in para: + if child.tag == "parameterlist": + break + elif child.tag == "verbatim": + if buf.strip(): + lines.append(buf.strip()) + lines.append("") + lines.extend(_verbatim_to_rst(child.text or "")) + buf = child.tail or "" + elif child.tag == "table": + if buf.strip(): + lines.append(buf.strip()) + lines.append("") + lines.extend(_table_to_rst(child)) + buf = child.tail or "" + elif child.tag == "ref": + buf += (child.text or "") + (child.tail or "") + else: + buf += "".join(child.itertext()) + (child.tail or "") + + if buf.strip(): + lines.append(buf.strip()) + lines.append("") + return lines + + +def _parse_limitations(xml_path: str) -> list[str]: + """Extract Limitations section from a Doxygen group XML file as RST lines.""" + try: + tree = ET.parse(xml_path) + except (FileNotFoundError, ET.ParseError): + return [] + + root = tree.getroot() + + sections: list[tuple[list[str], list[str]]] = [] + for memberdef in root.iter("memberdef"): + desc = memberdef.find("detaileddescription") + if desc is None: + continue + paras = list(desc) + + lim_start: Optional[int] = None + for i, para in enumerate(paras): + if "".join(para.itertext()).strip() == "Limitations:": + lim_start = i + 1 + break + if lim_start is None: + continue + + result: list[str] = [] + for para in paras[lim_start:]: + has_table = para.find("table") is not None + has_verbatim = para.find("verbatim") is not None + has_plist = para.find("parameterlist") is not None + has_text = bool(para.text and para.text.strip()) + if has_plist and not has_table and not has_verbatim and not has_text: + break + result.extend(_para_to_rst(para)) + + if result: + name = (memberdef.findtext("name", default="") or "").strip() + for names, lines in sections: + if lines == result: + if name and name not in names: + names.append(name) + break + else: + sections.append(([name] if name else [], result)) + + if not sections: + return [] + if len(sections) == 1: + return sections[0][1] + + combined: list[str] = [] + for names, lines in sections: + symbols = ", ".join(f"``{name}``" for name in names if name) + if symbols: + combined.extend((f"**Applies to {symbols}:**", "")) + combined.extend(lines) + return combined + + +# --------------------------------------------------------------------------- # +# Sphinx event handlers # +# --------------------------------------------------------------------------- # + + +def _load_cache(app) -> None: + """Populate _cache at builder-inited from the Doxygen XML directory.""" + global _cache + _cache = {} + + breathe_projects = getattr(app.config, "breathe_projects", {}) + xml_dir = breathe_projects.get("cvcuda", "") + if not xml_dir or not os.path.isdir(xml_dir): + return + + for func_name, group_suffix in _FUNC_TO_GROUP.items(): + if func_name in _cache: + continue + xml_path = os.path.join( + xml_dir, f"group__NVCV__C__ALGORITHM__{group_suffix}.xml" + ) + if os.path.isfile(xml_path): + lines = _parse_limitations(xml_path) + if lines: + _cache[func_name] = lines + + +def _func_base_name(full_name: str) -> Optional[str]: + """Return the bare function name from 'cvcuda.resize_into' → 'resize'.""" + if not full_name.startswith("cvcuda."): + return None + base = full_name.removeprefix("cvcuda.") + for suffix in ("_into_with_op", "_into"): + if base.endswith(suffix): + return base.removesuffix(suffix) + return base + + +def _inject(app, what, name, obj, options, lines) -> None: + """Append Limitations to single-overload operators. + + Overloaded operators are handled entirely by CvcudaAutofunctionDirective + (which emits ``.. py:function::`` blocks that don't trigger autodoc + events), so this hook only fires for single-overload functions delegated + to stock ``autofunction``. + """ + if what not in ("function", "method") or not name.startswith("cvcuda."): + return + base = _func_base_name(name) + if base not in _cache: + return + + lines.append("") + lines.append(".. rubric:: Limitations") + lines.append("") + lines.extend(_cache[base]) + + +def setup(app): + app.connect("builder-inited", _load_cache) + app.connect("autodoc-process-docstring", _inject) + app.add_directive("cvcuda-autofunction", CvcudaAutofunctionDirective) + return {"version": "1.0", "parallel_read_safe": True} diff --git a/docs/sphinx/docker_images.rst b/docs/sphinx/docker_images.rst index 3fdc0fe1e..b596f1ae6 100644 --- a/docs/sphinx/docker_images.rst +++ b/docs/sphinx/docker_images.rst @@ -1,5 +1,5 @@ .. - # SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -46,18 +46,22 @@ Manylinux-based images with CUDA toolkit for building CV-CUDA packages compatibl - GCC Version - CUDA Version - Purpose + * - builder_cu12.2.0_gcc10 + - 10 + - 12.2.0 + - CUDA 12.2 builds (multi-arch) * - builder_cu12.5.0_gcc10 - 10 - 12.5.0 - CUDA 12.5 builds (multi-arch) - * - builder_cu12.9.0_gcc10 - - 10 - - 12.9.0 - - CUDA 12.9 builds (multi-arch) * - builder_cu13.0.1_gcc10 - 10 - 13.0.1 - CUDA 13.0 builds (multi-arch) + * - builder_cu13.3.0_gcc10 + - 10 + - 13.3.0 + - CUDA 13.3 builds (multi-arch) **Build Dependencies Hierarchy:** @@ -86,7 +90,7 @@ Manylinux-based images with CUDA toolkit for building CV-CUDA packages compatibl **Builder Image Features:** - CMake 3.24.3 -- Python 3.9-3.14 from ManyLinux +- Python 3.10-3.14 from ManyLinux - Documentation tools (Sphinx 7.4.7/8.1.3, sphinx_rtd_theme, breathe) - Development tools (patchelf 0.17.2, setuptools, wheel, clang 14.0) - Full CUDA toolkit @@ -109,56 +113,105 @@ Base: `NVIDIA CUDA images `_ (nvidia/ - NumPy - PyTorch - Python - * - devel_u22.04_cu12.9.0_num2_torch2.8.0 - - ubuntu22.04 - - 12.9.0 - - 2.x - - 2.8.0 - - 3.9-3.14 - * - devel_u22.04_cu12.5.0_num1_torch2.8.0 - - ubuntu22.04 - - 12.5.0 - - 1.26.4 - - 2.8.0 - - 3.9-3.14 - * - devel_u24.04_cu13.0.1_num2_torch2.9.0 - - ubuntu24.04 - - 13.0.1 + * - devel_u26.04_cu13.3.0_num2 + - ubuntu26.04 + - 13.3.0 + - 2.x + - 2.11.0 + - 3.14 + * - devel_u22.04_cu12.5.0_num1 + - ubuntu22.04 + - 12.5.0 + - 1.26.4 + - 2.9.1 + - 3.10 + * - devel_u22.04_py310-314_cu12.5.0_num2 + - ubuntu22.04 + - 12.5.0 - 2.x - - 2.9.0 + - 2.9.1 - 3.10-3.14 + * - devel_u26.04_py310-314_cu13.3.0_num2 + - ubuntu26.04 + - 13.3.0 + - 2.x + - 2.11.0 + - 3.10-3.14 + +The NumPy 1 image uses CuPy 13.6.0, the newest release compatible with NumPy +1.26. The NumPy 2 images use CuPy 14.0.1. **Key Features:** -- Multiple GCC versions (10, 11, 12, 13, 14 on Ubuntu 24.04) -- Multiple Clang versions (11 on Ubuntu 22.04, 14 on all) +- Multiple GCC versions (10-13 on Ubuntu 22.04, 11-15 on Ubuntu 26.04) +- Multiple Clang versions (11 and 14 on Ubuntu 22.04, 18 on Ubuntu 26.04) - CMake 3.24.3, ninja-build, ccache - Testing frameworks (Google Test/Mock, pytest) - ML frameworks (PyTorch, NumPy with version-specific wheels) - Documentation tools (Doxygen, Sphinx) - Development tools (git, git-lfs, pre-commit, shellcheck) -Python Requirements Files --------------------------- +Version Management +------------------ + +All pinned Python package versions are defined in a single file at the repository root: +``versions.env``. This is the **only place** where versions should be changed. + +After editing ``versions.env``, regenerate all requirements files: + +.. code-block:: shell -CV-CUDA uses multiple requirements files for different purposes: + bash generate_requirements.sh -.. list-table:: Requirements Files +The generator rewrites the following files (do not edit them directly — they are +auto-generated and carry an ``AUTO-GENERATED`` header): + +.. list-table:: Auto-Generated Requirements Files :header-rows: 1 - :widths: 35 65 + :widths: 45 55 * - File - - Purpose - * - requirements.sys_python.txt - - System Python only: documentation (Sphinx, Breathe), wheel building (setuptools, wheel, build, patchelf, auditwheel), linting (flake8) - * - requirements.no_torch_no_numpy.txt - - All Python versions: testing tools (pytest, typing-extensions) - excludes PyTorch and NumPy - * - requirements.numpy1.txt - - NumPy 1.x for Python 3.9-3.12 (1.26.4). Not compatible with Python 3.13+ - * - requirements.numpy2.txt - - NumPy 2.x with version constraints: 2.0.2 (Python 3.9), 2.2.6 (Python 3.10-3.13), 2.3.3 (Python 3.14) - -PyTorch is installed separately per Python version in the Dockerfiles (not via requirements files). + - Contents + * - tests/requirements.tests.cu12.txt + - CuPy and CUDA-Python for CUDA 12.x + * - tests/requirements.tests.cu12.numpy1.txt + - NumPy 1-compatible CuPy and CUDA-Python for CUDA 12.x + * - tests/requirements.tests.cu13.txt + - CuPy and CUDA-Python for CUDA 13.x + * - tests/requirements.tests.numpy1.txt + - NumPy 1.x (Python 3.10-3.12) + * - tests/requirements.tests.numpy2.txt + - NumPy 2.x (Python 3.10-3.14) + * - bench/python/requirements.bench.common.txt + - Common benchmark dependencies + * - bench/python/requirements.bench.cu12.txt + - CUDA 12 benchmark dependencies + * - bench/python/requirements.bench.cu13.txt + - CUDA 13 benchmark dependencies + * - samples/requirements.samples.common.txt + - Common sample dependencies + * - samples/requirements.samples.cu12.txt + - CUDA 12 sample dependencies + * - samples/requirements.samples.cu13.txt + - CUDA 13 sample dependencies + * - samples/requirements.samples.hello_world_cu12.txt + - Minimal CUDA 12 hello-world dependencies + * - samples/requirements.samples.hello_world_cu13.txt + - Minimal CUDA 13 hello-world dependencies + * - docker/requirements.build.sys_python.txt + - System Python only: wheel building and linting tools + * - docker/requirements.build.all_pythons.txt + - All Python versions: pybind11 for CMake find_package + * - tests/requirements.tests.common.txt + - All Python versions: pytest and typing-extensions + * - docs/requirements.docs.txt + - System Python only: Sphinx documentation tools + +The generator runs automatically in ``init_repo.sh`` (on clone) and ``docker/build_dockers.sh`` +(before Docker builds). ``build.sh`` also runs the generator before each build to ensure +requirements files are always up to date. The pre-commit hook (triggered on changes to +``versions.env`` or any ``.template`` file) runs ``--check`` mode and fails if the generated +files are out of sync, forcing you to run ``bash generate_requirements.sh`` before committing. Building the Images ------------------- @@ -194,7 +247,7 @@ Running a development image, mounting source code for development: docker run -it --gpus all \ -v /path/to/cvcuda:/workspace \ - devel_u22.04_cu12.5.0_num1_torch2.8.0:v2 + devel_u22.04_cu12.5.0_num1:v9 Using a builder image for creating manylinux-compatible wheels: @@ -203,12 +256,19 @@ Using a builder image for creating manylinux-compatible wheels: docker run -it --gpus all \ -v /path/to/cvcuda:/workspace \ - builder_cu12.5.0_gcc10:v1 + builder_cu12.5.0_gcc10:v9 Maintenance ----------- +Updating Package Versions +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +1. Edit ``versions.env`` at the repository root +2. Run ``bash generate_requirements.sh`` to regenerate all requirements files +3. Commit both ``versions.env`` and the regenerated files together + Updating Image Versions ^^^^^^^^^^^^^^^^^^^^^^^^ @@ -222,7 +282,8 @@ Adding New CUDA Versions - Use ``dpkg --print-architecture`` to detect amd64 vs arm64 - Download appropriate CUDA installer (linux.run for x86_64, linux_sbsa.run for aarch64) 2. Add corresponding sections in ``build_dockers.sh`` -3. Update development image variants +3. Add the new CUDA version to ``versions.env`` and add any new package variants +4. Update development image variants Adding New Python Versions ^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -232,7 +293,7 @@ Adding New Python Versions .. code-block:: shell - --build-arg "PYTHON_VERSIONS=3.9 3.10 3.11 3.12 3.13 3.14" + --build-arg "PYTHON_VERSIONS=3.10 3.11 3.12 3.13 3.14" Troubleshooting --------------- diff --git a/docs/sphinx/generate_groups.py b/docs/sphinx/generate_groups.py index 8ddadfe40..7e87ae436 100644 --- a/docs/sphinx/generate_groups.py +++ b/docs/sphinx/generate_groups.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,65 +18,25 @@ import xml.etree.ElementTree as ET from pathlib import Path - -if __name__ == "__main__": - c_api_dir = Path(sys.argv[1]) - cpp_api_dir = Path(sys.argv[2]) - xmlRoot = sys.argv[3] - - os.makedirs(c_api_dir, exist_ok=True) - os.makedirs(cpp_api_dir, exist_ok=True) - - for i in os.listdir(xmlRoot): - group_path = os.path.join(xmlRoot, i) - if os.path.isfile(group_path) and "group__" in i: - tree = ET.parse(group_path) - root = tree.getroot() - for compounddef in root.iter("compounddef"): - group_name = compounddef.attrib["id"] - group_label = compounddef.find("compoundname").text - group_title = compounddef.find("title").text - - # Determine output directory based on group name - if "group__NVCV__CPP__" in group_name: - outdir = cpp_api_dir - is_cpp = True - elif "group__NVCV__C__" in group_name: - outdir = c_api_dir - is_cpp = False - else: - # Default to C API for any other groups - outdir = c_api_dir - is_cpp = False - - outfile = outdir / (group_name + ".rst") - output = ":orphan:\n\n" - output += group_title + "\n" - output += "=" * len(group_title) + "\n\n" - - # For C++ API, add a note pointing to the corresponding C API documentation - if is_cpp: - # Generate the corresponding C API group name and file - c_group_name = group_name.replace("__CPP__", "__C__") - # Sphinx :doc: directive expects path without .rst extension - c_group_file = f"../_c_api/{c_group_name}" - - output += ".. note::\n" - output += ( - " The C++ API provides RAII wrappers around the C API.\n" - ) - output += " For detailed documentation including parameters, " - output += "return values, and limitations,\n" - output += " please refer to the " - output += ":doc:`corresponding C API documentation " # noqa: E231 - output += f"<{c_group_file}>`.\n" # noqa: W604, E231 - output += "\n" - - output += f".. doxygengroup:: {group_label}\n" # noqa: E231 - output += " :project: cvcuda\n" # noqa: E231 - - # For C++ API, don't show the namespace members to avoid recursive namespace cvcuda - if is_cpp: - output += " :content-only:\n" - - outfile.write_text(output) +outdir = Path(sys.argv[1]) + +os.makedirs(outdir, exist_ok=True) + +xmlRoot = sys.argv[2] + +for i in os.listdir(xmlRoot): + group_path = os.path.join(xmlRoot, i) + if os.path.isfile(group_path) and "group__" in i: + tree = ET.parse(group_path) + root = tree.getroot() + for compounddef in root.iter("compounddef"): + group_name = compounddef.attrib["id"] + group_label = compounddef.find("compoundname").text + group_title = compounddef.find("title").text + outfile = outdir / (group_name + ".rst") + output = ":orphan:\n\n" + output += group_title + "\n" + output += "=" * len(group_title) + "\n\n" + output += f".. doxygengroup:: {group_label}\n" + output += " :project: cvcuda\n" + outfile.write_text(output) diff --git a/docs/sphinx/getting_started.rst b/docs/sphinx/getting_started.rst index fbe9c91d4..a27da13f8 100644 --- a/docs/sphinx/getting_started.rst +++ b/docs/sphinx/getting_started.rst @@ -1,5 +1,5 @@ .. - # SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -49,7 +49,7 @@ For CUDA 12: python3 -m venv venv_samples source venv_samples/bin/activate - python3 -m pip install -r samples/requirements_hello_world_cu12.txt + python3 -m pip install -r samples/requirements.samples.hello_world_cu12.txt For CUDA 13: @@ -57,7 +57,7 @@ For CUDA 13: python3 -m venv venv_samples source venv_samples/bin/activate - python3 -m pip install -r samples/requirements_hello_world_cu13.txt + python3 -m pip install -r samples/requirements.samples.hello_world_cu13.txt This installs minimal dependencies (CV-CUDA, NumPy, nvImageCodec) needed for the hello_world sample. @@ -73,13 +73,13 @@ Check ``cvcuda/.cache/cat_hw.jpg`` - you just processed an image entirely on GPU .. note:: - The ``requirements_hello_world_cu12.txt`` and ``requirements_hello_world_cu13.txt`` files are minimal (only 4 packages) for quick testing. + The ``requirements.samples.hello_world_cu12.txt`` and ``requirements.samples.hello_world_cu13.txt`` files are minimal (only 4 packages) for quick testing. For other samples (operators, applications, interoperability), use the full installation script: .. code-block:: bash - cd samples - ./install_samples_dependencies.sh + cd samples + ./install_samples_dependencies.sh **What's Next?** Continue below to learn the prerequisites and explore more samples. @@ -101,15 +101,18 @@ See the :doc:`Samples Documentation ` for a guided tour of all availabl Interoperability ---------------- -See the :doc:`Interoperability ` for information on how to use CV-CUDA with other libraries. +CV-CUDA seamlessly interoperates with PyTorch, CuPy, PyCUDA, and other GPU frameworks through zero-copy data exchange. + +See the :ref:`Interoperability section ` in the Samples documentation for detailed information on using CV-CUDA with other libraries. Advanced Topics --------------- Once you're comfortable with the basics, explore advanced features: -* :doc:`Object Cache ` - Learn about CV-CUDA's memory caching system -* :doc:`Make Operator Tool ` - Create custom CV-CUDA operators +* :doc:`Object Cache ` +* :doc:`Operator Variants ` +* :doc:`Make Operator Tool ` Additional Resources -------------------- diff --git a/docs/sphinx/index.rst b/docs/sphinx/index.rst index 75cb5aac3..99a29abeb 100644 --- a/docs/sphinx/index.rst +++ b/docs/sphinx/index.rst @@ -1,5 +1,5 @@ .. - # SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -58,7 +58,7 @@ CV-CUDA Pre- and Post-Processing Operators CV-CUDA offers a comprehensive collection of Computer Vision and Image Processing operators. Click any operator name to view its detailed API documentation. -.. include:: content/cvcuda_op.rst +.. include:: operator_list.rst Where Are the Release Notes? @@ -126,7 +126,6 @@ Copyright Docker Data Types Samples - Interoperability .. toctree:: :caption: Advanced Topics @@ -134,6 +133,7 @@ Copyright :hidden: Object Cache + Operator Variants Make Operator Tool .. toctree:: @@ -142,14 +142,14 @@ Copyright :hidden: Python API - C API - C++ API + C/C++ API .. toctree:: :caption: Release Notes :maxdepth: 1 :hidden: + v0.17.0-beta v0.16.0-beta v0.15.0-beta v0.14.0-beta diff --git a/docs/sphinx/installation.rst b/docs/sphinx/installation.rst index a037ee324..e8c2c7658 100644 --- a/docs/sphinx/installation.rst +++ b/docs/sphinx/installation.rst @@ -1,5 +1,5 @@ .. - # SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,14 +21,6 @@ Installation CV-CUDA can be installed using pre-built packages or built from source. Choose the installation method that best fits your development workflow and requirements. -.. toctree:: - :maxdepth: 2 - :caption: Installation Methods: - - Prerequisites <#prerequisites> - Using Pre-built Packages <#using-pre-built-packages> - Building from Source <#building-from-source> - .. _prerequisites: Prerequisites @@ -38,7 +30,7 @@ Before installing CV-CUDA, ensure your system meets the following requirements: * **Operating System**: Ubuntu >= 22.04 * **CUDA Toolkit**: CUDA >= 12.2 -* **NVIDIA Driver**: r525 or later for CUDA 12.x (r535 required for samples), r580 or later for CUDA 13.x +* **NVIDIA Driver**: r525 or later for CUDA 12.x, r580 or later for CUDA 13.x If you are using WSL2, follow the instructions in the :ref:`WSL2 Setup `. @@ -211,23 +203,27 @@ The following table summarizes all dependencies needed to build and test CV-CUDA **Installation Notes:** -- Any version of the 12.x or 13.x CUDA toolkit should work. CV-CUDA was tested with 12.5, 12.9 and 13.0, these versions are thus recommended. -- For NumPy: Use ``requirements.numpy1.txt`` for Python 3.9-3.12 (NumPy 1.26.4) or ``requirements.numpy2.txt`` for Python 3.9-3.14 (NumPy 2.x with version constraints). +- Install the CUDA Toolkit and a compatible NVIDIA driver by following NVIDIA's official + `CUDA Toolkit Downloads`_ and `CUDA Installation Guide for Linux`_. CUDA package names, + repository setup, and driver requirements vary by distribution and toolkit release. +- CUDA 12.2+ and CUDA 13.x should work. CV-CUDA was tested with 12.5 and 13.3; these versions are recommended. +- For NumPy: Use ``tests/requirements.tests.numpy1.txt`` for Python 3.10-3.12 (NumPy 1.26.4) or ``tests/requirements.tests.numpy2.txt`` for Python 3.10-3.14 (NumPy 2.2.6 on Python 3.10, NumPy 2.4.4 on Python 3.11+). - PyTorch is installed separately and not included in requirements files. If you are using WSL2, you can follow the instructions in the :ref:`WSL2 Setup `. -To install all Debian build and test dependencies manually: +After CUDA is installed, install the remaining Debian build and test dependencies manually. +The following command intentionally excludes CUDA packages: .. code-block:: shell sudo apt install -y \ - g++-11 cmake ninja-build cuda-12-9 \ + g++-11 cmake ninja-build \ python3-dev python3-venv python3-pip \ libgtest-dev libgmock-dev libssl-dev zlib1g-dev \ fonts-dejavu doxygen graphviz -Python dependencies are specified with exact versions in ``docker/requirements.sys_python.txt``. +Python dependencies are specified with exact versions in ``docker/requirements.build.sys_python.txt`` (build/packaging tools) and ``docs/requirements.docs.txt`` (documentation tools). The recommended method to install these Python dependencies is to use a Python virtual environment, with **venv** or **uv**. 1. Using ``venv`` @@ -236,7 +232,7 @@ The recommended method to install these Python dependencies is to use a Python v python3 -m venv env source env/bin/activate - python3 -m pip install -r docker/requirements.sys_python.txt + python3 -m pip install -r docker/requirements.build.sys_python.txt -r docs/requirements.docs.txt 2. Using ``uv``, importantly use the ``--seed`` flag to expose pip inside the virtual environment @@ -244,7 +240,7 @@ The recommended method to install these Python dependencies is to use a Python v uv venv env --seed source env/bin/activate - uv pip install -r docker/requirements.sys_python.txt + uv pip install -r docker/requirements.build.sys_python.txt -r docs/requirements.docs.txt .. note:: All these dependencies are pre-configured in our Docker images. @@ -253,11 +249,11 @@ The recommended method to install these Python dependencies is to use a Python v 3. Build the Project ~~~~~~~~~~~~~~~~~~~~ -The central ``ci/build.sh`` script is used to build the project, Python bindings and wheels, tests and documentation by setting the appropriate CMake arguments. +The central ``build.sh`` script is used to build the project, Python bindings and wheels, tests and documentation by setting the appropriate CMake arguments. .. code-block:: shell - ci/build.sh [release|debug] [output build tree path] [additional cmake args] + build.sh [release|debug] [output build tree path] [additional cmake args] **Build Type:** @@ -276,39 +272,56 @@ The central ``ci/build.sh`` script is used to build the project, Python bindings - ``-DBUILD_TESTS_CPP=1|0``: Enable/disable building C++ tests (see Known Limitations in README for GCC-10 restrictions) - ``-DBUILD_TESTS_WHEELS=1|0``: Enable/disable generation of the wheel testing script - ``-DBUILD_TESTS_PYTHON=1|0``: Enable/disable building Python tests -- ``-DPYTHON_VERSIONS='3.9;3.10;3.11;3.12;3.13;3.14'``: Select Python versions to build bindings and wheels for (default: system Python3 only) +- ``-DBUILD_BENCH=1|0``: Enable/disable benchmark builds (default: OFF). Requires `nvbench `_ to be installed (``cmake --install`` or system package). See ``bench/README.md`` for details. +- ``-DPYTHON_VERSIONS='3.10;3.11;3.12;3.13;3.14'``: Select Python versions to build bindings and wheels for (default: system Python3 only) - ``-DPUBLIC_API_COMPILERS='gcc-10;gcc-11;clang-11;clang-14'``: Select compilers for public API compatibility checks (default: gcc-11, clang-11, clang-14) - ``-DDOC_PYTHON_VERSION='3.11'``: Override Python version for documentation build (default: system Python) - ``-DENABLE_SANITIZER=1|0``: Enable/disable address sanitizer (default: disabled) - ``-DCMAKE_CUDA_COMPILER=/path/to/nvcc``: Override CUDA compiler (default: /usr/local/cuda/bin/nvcc) +- ``-DCMAKE_CUDA_ARCHITECTURES=``: Override the complete CUDA architecture list. On x86_64, + the generated default builds native SM75, SM80, and SM90 code (plus SM100 and SM120 with CUDA + 12.8 or newer). Performance-sensitive SM86 and SM89 kernels are added separately without + compiling every CUDA source for those architectures. +- ``-DCVCUDA_TARGETED_SM8X_CUBINS=AUTO|ON|OFF``: Control the supplemental SM86 and SM89 operator + cubins. ``AUTO`` enables them whenever the effective architecture list matches the compact + x86_64 default, including an explicitly supplied equivalent list. ``ON`` ensures selected + kernels have native images without duplicating globally enabled real targets, and ``OFF`` + disables them (default: ``AUTO``). +- ``-DCVCUDA_AARCH64_JETSON=ON|OFF``: aarch64 only -- build for Jetson Orin platforms only, targeting only Orin-relevant GPU architectures (sm_86, sm_87, sm_89) instead of the full SBSA set; package file names carry an ``aarch64-jetson-linux`` token instead of ``aarch64-linux`` so Jetson and SBSA artifacts stay distinguishable (default: OFF) All boolean options accept both numeric (``0``/``1``) and CMake boolean values (``ON``/``OFF``, ``YES``/``NO``, ``TRUE``/``FALSE``). **Environment Variables:** - ``CC``, ``CXX``: Specify C/C++ compilers (default: auto-detected gcc-11 or newer) +- ``CUDAARCHS``: Set the complete CUDA architecture list when + ``CMAKE_CUDA_ARCHITECTURES`` is not already cached. The value is not combined with CV-CUDA's + generated defaults. **Examples:** .. code-block:: shell # Basic release build with default Python - ci/build.sh + build.sh # Build documentation - ci/build.sh release build-rel -DBUILD_DOCS=1 -DBUILD_PYTHON=1 + build.sh release build-rel -DBUILD_DOCS=1 -DBUILD_PYTHON=1 # Debug build with multiple Python versions - ci/build.sh debug build-debug -DPYTHON_VERSIONS='3.10;3.11;3.12' + build.sh debug build-debug -DPYTHON_VERSIONS='3.10;3.11;3.12' # Release build without tests - ci/build.sh release -DBUILD_TESTS=0 + build.sh release -DBUILD_TESTS=0 # Build with specific compiler - CC=gcc-12 CXX=g++-12 ci/build.sh + CC=gcc-12 CXX=g++-12 build.sh # Build with specific CUDA 13 version - ci/build.sh -DCMAKE_CUDA_COMPILER=/usr/local/cuda-13/bin/nvcc + build.sh -DCMAKE_CUDA_COMPILER=/usr/local/cuda-13/bin/nvcc + + # Build for Jetson Orin platforms (aarch64 only) + build.sh release build-rel "-DCVCUDA_AARCH64_JETSON=ON" 1. Run Tests @@ -330,39 +343,62 @@ Prerequisite: Prerequisites: install system Python dependencies (see step 2). On top of that, the following dependencies are required for running the tests: -- **numpy**: dependencies needed by python bindings tests -- **torch**: dependencies needed by python bindings tests (install separately) -- **typing-extensions**: dependencies needed by python bindings tests +- **numpy**: required by python bindings tests +- **typing-extensions**: required by python bindings tests - **pytest**: to run the tests +- **cupy**: required by the Python test runner and CUDA array tests -Package versions are specified in: +Package versions are defined in ``versions.env`` at the repository root and auto-generated +into the requirements files below. To update a version, edit ``versions.env`` and run +``bash generate_requirements.sh``. -- ``docker/requirements.no_torch_no_numpy.txt`` for pytest and typing-extensions -- ``docker/requirements.numpy1.txt`` for NumPy 1.x (Python 3.9-3.12) -- ``docker/requirements.numpy2.txt`` for NumPy 2.x (Python 3.9-3.14) +- ``tests/requirements.tests.common.txt`` for pytest and typing-extensions (manually maintained) +- ``tests/requirements.tests.numpy1.txt`` for NumPy 1.x (Python 3.10-3.12) — auto-generated +- ``tests/requirements.tests.numpy2.txt`` for NumPy 2.x (Python 3.10-3.14) — auto-generated +- ``tests/requirements.tests.cu12.txt`` for CuPy with CUDA 12.x (compatible with CUDA 12.2+) — auto-generated +- ``tests/requirements.tests.cu12.numpy1.txt`` for the NumPy 1-compatible CuPy pin with CUDA 12.x — auto-generated +- ``tests/requirements.tests.cu13.txt`` for CuPy with CUDA 13.x (compatible with CUDA 13.3+) — auto-generated + +The easiest way to install test dependencies is to use the provided script: + +.. code-block:: shell -Install the dependencies into your virtual environment (setup in step 2) using your method of choice. -The Torch and NumPy version are specified in the commands below. + # For NumPy 2.x + CuPy for CUDA 12.x (CUDA 12.2+) + tests/install_test_dependencies.sh numpy2 cu12 + + # For NumPy 1.x + CuPy for CUDA 12.x (Python 3.10-3.12) + tests/install_test_dependencies.sh numpy1 cu12 + + # For NumPy 2.x + CuPy for CUDA 13.x (13.3+) + tests/install_test_dependencies.sh numpy2 cu13 + +Alternatively, install the dependencies manually into your virtual environment (setup in step 2): 1. Using ``venv`` .. code-block:: shell - # For NumPy 1.x (Python 3.9-3.12) - python3 -m pip install -r docker/requirements.no_torch_no_numpy.txt -r docker/requirements.numpy1.txt torch==2.8.* + # For CUDA 12.2+, NumPy 2.x + python3 -m pip install -r tests/requirements.tests.common.txt -r tests/requirements.tests.numpy2.txt -r tests/requirements.tests.cu12.txt - # OR for NumPy 2.x (Python 3.9-3.14) - python3 -m pip install -r docker/requirements.no_torch_no_numpy.txt -r docker/requirements.numpy2.txt torch==2.8.* + # For CUDA 12.2+, NumPy 1.x + python3 -m pip install -r tests/requirements.tests.common.txt -r tests/requirements.tests.numpy1.txt -r tests/requirements.tests.cu12.numpy1.txt + + # For CUDA 13.x (13.3+), NumPy 2.x + python3 -m pip install -r tests/requirements.tests.common.txt -r tests/requirements.tests.numpy2.txt -r tests/requirements.tests.cu13.txt 2. Using ``uv``: .. code-block:: shell - # For NumPy 1.x (Python 3.9-3.12) - uv pip install -r docker/requirements.no_torch_no_numpy.txt -r docker/requirements.numpy1.txt torch==2.8.* + # For CUDA 12.2+, NumPy 2.x + uv pip install -r tests/requirements.tests.common.txt -r tests/requirements.tests.numpy2.txt -r tests/requirements.tests.cu12.txt + + # For CUDA 12.2+, NumPy 1.x + uv pip install -r tests/requirements.tests.common.txt -r tests/requirements.tests.numpy1.txt -r tests/requirements.tests.cu12.numpy1.txt - # OR for NumPy 2.x (Python 3.9-3.14) - uv pip install -r docker/requirements.no_torch_no_numpy.txt -r docker/requirements.numpy2.txt torch==2.8.* + # For CUDA 13.x (13.3+), NumPy 2.x + uv pip install -r tests/requirements.tests.common.txt -r tests/requirements.tests.numpy2.txt -r tests/requirements.tests.cu13.txt 4.2 Run the Tests ^^^^^^^^^^^^^^^^^ @@ -463,5 +499,7 @@ The built wheels can still be installed using ``pip``. For example, to install t .. _CV-CUDA GitHub Releases: https://github.com/CVCUDA/CV-CUDA/releases +.. _CUDA Toolkit Downloads: https://developer.nvidia.com/cuda-downloads +.. _CUDA Installation Guide for Linux: https://docs.nvidia.com/cuda/cuda-installation-guide-linux/index.html .. _cvcuda-cu12: https://pypi.org/project/cvcuda-cu12/ .. _cvcuda-cu13: https://pypi.org/project/cvcuda-cu13/ diff --git a/docs/sphinx/interop/cupy.rst b/docs/sphinx/interop/cupy.rst deleted file mode 100644 index bde3bef57..000000000 --- a/docs/sphinx/interop/cupy.rst +++ /dev/null @@ -1,58 +0,0 @@ -.. - # SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - # SPDX-License-Identifier: Apache-2.0 - # - # Licensed 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. - -CuPy ----- - -CuPy is a NumPy-compatible GPU array library that provides GPU acceleration for numerical operations. -It's an excellent choice when you want to use NumPy-like operations on the GPU. - -**Key Points:** - -* CuPy arrays are already on GPU, no explicit device transfer needed -* Use :py:func:`cvcuda.as_tensor` to convert CuPy arrays to CV-CUDA -* Use ``cupy.asarray()`` to convert CV-CUDA tensors back to CuPy -* CuPy provides the most NumPy-like interface for GPU arrays - -**Required Imports:** - -.. literalinclude:: ../../../samples/interoperability/cupy_interop.py - :language: python - :start-after: docs_tag: begin_imports - :end-before: docs_tag: end_imports - -**CuPy to CV-CUDA:** - -.. literalinclude:: ../../../samples/interoperability/cupy_interop.py - :language: python - :start-after: docs_tag: begin_cupy_to_cvcuda - :end-before: docs_tag: end_cupy_to_cvcuda - :dedent: 4 - -CuPy arrays are created directly on the GPU and can be immediately converted to CV-CUDA tensors. - -**CV-CUDA to CuPy:** - -.. literalinclude:: ../../../samples/interoperability/cupy_interop.py - :language: python - :start-after: docs_tag: begin_cvcuda_to_cupy - :end-before: docs_tag: end_cvcuda_to_cupy - :dedent: 4 - -The ``cupy.asarray()`` function recognizes the CUDA Array Interface and creates a CuPy array that -views the same GPU memory as the CV-CUDA tensor. - -**Complete Example:** See ``samples/interoperability/cupy_interop.py`` diff --git a/docs/sphinx/interop/pytorch.rst b/docs/sphinx/interop/pytorch.rst deleted file mode 100644 index d9744a5be..000000000 --- a/docs/sphinx/interop/pytorch.rst +++ /dev/null @@ -1,60 +0,0 @@ -.. - # SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - # SPDX-License-Identifier: Apache-2.0 - # - # Licensed 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. - -PyTorch -------- - -PyTorch is one of the most popular deep learning frameworks. -CV-CUDA allows seamless interoperability with PyTorch, allowing you to use CV-CUDA's -optimized computer vision operations within your existing PyTorch workflows. - -**Key Points:** - -* PyTorch tensors must be on GPU (``.cuda()``) to convert to CV-CUDA -* Use :py:func:`cvcuda.as_tensor` to convert PyTorch tensors to CV-CUDA -* Use ``torch.as_tensor()`` to convert CV-CUDA tensors back to PyTorch -* You can use ``.clone()`` when converting from CV-CUDA to avoid shared memory issues, but this will incur a memcpy operation and potential performance degradation. Both torch and cvcuda support zero-copy intepability through their ``as_tensor`` functions. - -**Required Imports:** - -.. literalinclude:: ../../../samples/interoperability/pytorch_interop.py - :language: python - :start-after: docs_tag: begin_imports - :end-before: docs_tag: end_imports - -**PyTorch to CV-CUDA:** - -.. literalinclude:: ../../../samples/interoperability/pytorch_interop.py - :language: python - :start-after: docs_tag: begin_torch_to_cvcuda - :end-before: docs_tag: end_torch_to_cvcuda - :dedent: 4 - -The PyTorch tensor must be moved to GPU using ``.cuda()`` before conversion. The :py:func:`cvcuda.as_tensor` -function creates a CV-CUDA tensor that shares the same GPU memory as the PyTorch tensor using the ``__cuda_array_interface__`` protocol. - -**CV-CUDA to PyTorch:** - -.. literalinclude:: ../../../samples/interoperability/pytorch_interop.py - :language: python - :start-after: docs_tag: begin_cvcuda_to_torch - :end-before: docs_tag: end_cvcuda_to_torch - :dedent: 4 - -The ``.clone()`` call is important to avoid multiple tensors sharing the same GPU buffer, which can -lead to unexpected behavior if one tensor is modified or deallocated. - -**Complete Example:** See ``samples/interoperability/pytorch_interop.py`` diff --git a/docs/sphinx/modules/c_api.rst b/docs/sphinx/modules/c_api.rst deleted file mode 100644 index a2fa2347b..000000000 --- a/docs/sphinx/modules/c_api.rst +++ /dev/null @@ -1,52 +0,0 @@ -.. - # SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - # SPDX-License-Identifier: Apache-2.0 - # - # Licensed 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. - -.. _c_api: - -C API -===== - -The CV-CUDA C API provides a low-level interface to all functionality using C functions and types. - -.. note:: - For C++ developers, see the :ref:`C++ API documentation ` for RAII wrappers and additional type safety. - For Python developers, see the :ref:`Python API documentation ` for equivalent functionality. - -The C API uses functions and types with ``NVCV`` or ``cvcuda`` prefixes (e.g., ``NVCVImage``, ``cvcudaResize``). - -NVCV - Core Types ------------------ - -The NVCV library provides fundamental data types and containers for computer vision applications. - -.. toctree:: - :glob: - :maxdepth: 1 - - ../_c_api/group__NVCV__C__CORE__* - ../_c_api/group__NVCV__C__UTIL__* - ../_c_api/group__NVCV__C__TYPES - -CV-CUDA - Operators -------------------- - -The CV-CUDA library provides high-performance, GPU-accelerated computer vision and image processing operators. - -.. toctree:: - :glob: - :maxdepth: 1 - - ../_c_api/group__NVCV__C__ALGORITHM__* diff --git a/docs/sphinx/modules/cpp_api.rst b/docs/sphinx/modules/c_cpp_api.rst similarity index 59% rename from docs/sphinx/modules/cpp_api.rst rename to docs/sphinx/modules/c_cpp_api.rst index 5f00ef907..e28cb4828 100644 --- a/docs/sphinx/modules/cpp_api.rst +++ b/docs/sphinx/modules/c_cpp_api.rst @@ -14,41 +14,38 @@ # See the License for the specific language governing permissions and # limitations under the License. -.. _cpp_api: +.. _c_cpp_api: -C++ API -======= +C/C++ API +========= -The CV-CUDA C++ API provides RAII wrappers around the C API with additional type safety and convenience. +The CV-CUDA C/C++ API provides both C and C++ interfaces to all functionality. .. note:: - For C developers, see the :ref:`C API documentation ` for the low-level C interface. For Python developers, see the :ref:`Python API documentation ` for equivalent functionality. -The C++ API provides classes in ``nvcv`` and ``cvcuda`` namespaces (e.g., ``nvcv::Image``, ``cvcuda::Resize``). +Both libraries provide complete C and C++ APIs: -NVCV - Core Types ------------------ - -The NVCV library provides fundamental data types and containers for computer vision applications. - -.. toctree:: - :glob: - :maxdepth: 1 +* **C API**: Functions and types with ``NVCV`` or ``cvcuda`` prefixes (e.g., ``NVCVImage``, ``cvcudaResize``) +* **C++ API**: Classes in ``nvcv`` and ``cvcuda`` namespaces (e.g., ``nvcv::Image``, ``cvcuda::Resize``) - ../_cpp_api/group__NVCV__CPP__CORE__* - ../_cpp_api/group__NVCV__CPP__UTIL__* +The C++ API provides RAII wrappers around the C API with additional type safety and convenience. -NVCV - CUDA Tools +NVCV - Core Types ----------------- -The NVCV library provides CUDA utility classes and tools for device-side operations. +The NVCV library provides fundamental data types and containers for computer vision applications. .. toctree:: :glob: :maxdepth: 1 - ../_cpp_api/group__NVCV__CPP__CUDATOOLS__* + ../_c_cpp_api/group__NVCV__C__CORE__* + ../_c_cpp_api/group__NVCV__CPP__CORE__* + ../_c_cpp_api/group__NVCV__C__UTIL__* + ../_c_cpp_api/group__NVCV__CPP__UTIL__* + ../_c_cpp_api/group__NVCV__C__TYPES + ../_c_cpp_api/group__NVCV__CPP__CUDATOOLS__* CV-CUDA - Operators ------------------- @@ -59,4 +56,5 @@ The CV-CUDA library provides high-performance, GPU-accelerated computer vision a :glob: :maxdepth: 1 - ../_cpp_api/group__NVCV__CPP__ALGORITHM__* + ../_c_cpp_api/group__NVCV__C__ALGORITHM__* + ../_c_cpp_api/group__NVCV__CPP__ALGORITHM__* diff --git a/docs/sphinx/modules/c_status.rst b/docs/sphinx/modules/c_status.rst new file mode 100644 index 000000000..e46c0fca3 --- /dev/null +++ b/docs/sphinx/modules/c_status.rst @@ -0,0 +1,25 @@ +.. + # SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-License-Identifier: Apache-2.0 + # + # Licensed 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. + +:orphan: + +Status +====== + +.. toctree:: + :glob: + + ../_c_cpp_api/group__NVCV__C__API_STATUS* diff --git a/docs/sphinx/modules/python/auxiliary_types.rst b/docs/sphinx/modules/python/auxiliary_types.rst index b847d8fa0..96ec21fdf 100644 --- a/docs/sphinx/modules/python/auxiliary_types.rst +++ b/docs/sphinx/modules/python/auxiliary_types.rst @@ -1,5 +1,5 @@ .. - # SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -89,7 +89,7 @@ Auxiliary Types :members: :undoc-members: -.. autoclass:: cvcuda.NormType +.. autoclass:: cvcuda.Norm :members: :undoc-members: @@ -97,7 +97,7 @@ Auxiliary Types :members: :undoc-members: -.. autoclass:: cvcuda.PairwiseMatcherType +.. autoclass:: cvcuda.Matcher :members: :undoc-members: @@ -109,7 +109,7 @@ Auxiliary Types :members: :undoc-members: -.. autoclass:: cvcuda.RemapMapValueType +.. autoclass:: cvcuda.Remap :members: :undoc-members: @@ -121,7 +121,7 @@ Auxiliary Types :members: :undoc-members: -.. autoclass:: cvcuda.SIFTFlagType +.. autoclass:: cvcuda.SIFT :members: :undoc-members: diff --git a/docs/sphinx/modules/python/data_types.rst b/docs/sphinx/modules/python/data_types.rst index 73b63297c..d01514739 100644 --- a/docs/sphinx/modules/python/data_types.rst +++ b/docs/sphinx/modules/python/data_types.rst @@ -1,5 +1,5 @@ .. - # SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -29,10 +29,6 @@ Data Types :members: :undoc-members: -.. autoclass:: cvcuda.ImageBatch - :members: - :undoc-members: - .. autoclass:: cvcuda.TensorBatch :members: :undoc-members: @@ -45,10 +41,6 @@ Data Types :members: :undoc-members: -.. autoclass:: cvcuda.DataType - :members: - :undoc-members: - .. autoclass:: cvcuda.TensorLayout :members: :undoc-members: diff --git a/docs/sphinx/modules/python/index.rst b/docs/sphinx/modules/python/index.rst index 307b6b32f..440bd185e 100644 --- a/docs/sphinx/modules/python/index.rst +++ b/docs/sphinx/modules/python/index.rst @@ -19,16 +19,6 @@ Python API ========== -The CV-CUDA Python API provides a high-level interface to CV-CUDA functionality. - -.. note:: - For C/C++ developers, see the :ref:`C API documentation ` and :ref:`C++ API documentation ` for equivalent functionality. - -The Python API is available through the ``cvcuda`` module and provides: - -* **Zero-copy interoperability** with PyTorch, CuPy, and other Python frameworks -* **Pythonic interfaces** for all operators and data types - .. toctree:: :maxdepth: 1 diff --git a/docs/sphinx/modules/python/operators.rst b/docs/sphinx/modules/python/operators.rst index 243a153bf..2a22dc190 100644 --- a/docs/sphinx/modules/python/operators.rst +++ b/docs/sphinx/modules/python/operators.rst @@ -1,5 +1,5 @@ .. - # SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,112 +17,262 @@ Operators ========= -.. autofunction:: cvcuda.adaptivethreshold +.. cvcuda-autofunction:: cvcuda.adaptivethreshold -.. autofunction:: cvcuda.advcvtcolor +.. cvcuda-autofunction:: cvcuda.adaptivethreshold_into -.. autofunction:: cvcuda.averageblur +.. cvcuda-autofunction:: cvcuda.adjust_hue -.. autofunction:: cvcuda.bilateral_filter +.. cvcuda-autofunction:: cvcuda.adjust_hue_into -.. autofunction:: cvcuda.bndbox +.. cvcuda-autofunction:: cvcuda.adjust_saturation -.. autofunction:: cvcuda.boxblur +.. cvcuda-autofunction:: cvcuda.adjust_saturation_into -.. autofunction:: cvcuda.brightness_contrast +.. cvcuda-autofunction:: cvcuda.adjust_sharpness -.. autofunction:: cvcuda.center_crop +.. cvcuda-autofunction:: cvcuda.adjust_sharpness_into -.. autofunction:: cvcuda.channelreorder +.. cvcuda-autofunction:: cvcuda.adjust_contrast -.. autofunction:: cvcuda.color_twist +.. cvcuda-autofunction:: cvcuda.adjust_contrast_into -.. autofunction:: cvcuda.composite +.. cvcuda-autofunction:: cvcuda.advcvtcolor -.. autofunction:: cvcuda.conv2d +.. cvcuda-autofunction:: cvcuda.advcvtcolor_into -.. autofunction:: cvcuda.convertto +.. cvcuda-autofunction:: cvcuda.autocontrast -.. autofunction:: cvcuda.copymakeborder +.. cvcuda-autofunction:: cvcuda.autocontrast_into -.. autofunction:: cvcuda.copymakeborderstack +.. cvcuda-autofunction:: cvcuda.averageblur -.. autofunction:: cvcuda.crop_flip_normalize_reformat +.. cvcuda-autofunction:: cvcuda.averageblur_into -.. autofunction:: cvcuda.customcrop +.. cvcuda-autofunction:: cvcuda.bilateral_filter -.. autofunction:: cvcuda.cvtcolor +.. cvcuda-autofunction:: cvcuda.bilateral_filter_into -.. autofunction:: cvcuda.distance_map +.. cvcuda-autofunction:: cvcuda.bndbox -.. autofunction:: cvcuda.erase +.. cvcuda-autofunction:: cvcuda.bndbox_into -.. autofunction:: cvcuda.findhomography +.. cvcuda-autofunction:: cvcuda.boxblur -.. autofunction:: cvcuda.flip +.. cvcuda-autofunction:: cvcuda.boxblur_into -.. autofunction:: cvcuda.gamma_contrast +.. cvcuda-autofunction:: cvcuda.brightness_contrast -.. autofunction:: cvcuda.gaussian +.. cvcuda-autofunction:: cvcuda.brightness_contrast_into -.. autofunction:: cvcuda.gaussiannoise +.. cvcuda-autofunction:: cvcuda.center_crop -.. autofunction:: cvcuda.histogram +.. cvcuda-autofunction:: cvcuda.center_crop_into -.. autofunction:: cvcuda.histogrameq +.. cvcuda-autofunction:: cvcuda.channelreorder -.. autofunction:: cvcuda.hq_resize +.. cvcuda-autofunction:: cvcuda.channelreorder_into -.. autofunction:: cvcuda.inpaint +.. cvcuda-autofunction:: cvcuda.clahe -.. autofunction:: cvcuda.joint_bilateral_filter +.. cvcuda-autofunction:: cvcuda.clahe_into -.. autofunction:: cvcuda.label +.. cvcuda-autofunction:: cvcuda.color_twist -.. autofunction:: cvcuda.laplacian +.. cvcuda-autofunction:: cvcuda.color_twist_into -.. autofunction:: cvcuda.max_loc +.. cvcuda-autofunction:: cvcuda.composite -.. autofunction:: cvcuda.median_blur +.. cvcuda-autofunction:: cvcuda.composite_into -.. autofunction:: cvcuda.minarearect +.. cvcuda-autofunction:: cvcuda.conv2d -.. autofunction:: cvcuda.min_loc +.. cvcuda-autofunction:: cvcuda.conv2d_into -.. autofunction:: cvcuda.min_max_loc +.. cvcuda-autofunction:: cvcuda.convertto -.. autofunction:: cvcuda.morphology +.. cvcuda-autofunction:: cvcuda.convertto_into -.. autofunction:: cvcuda.nms +.. cvcuda-autofunction:: cvcuda.copymakeborder -.. autofunction:: cvcuda.normalize +.. cvcuda-autofunction:: cvcuda.copymakeborder_into -.. autofunction:: cvcuda.osd +.. cvcuda-autofunction:: cvcuda.copymakeborderstack -.. autofunction:: cvcuda.padandstack +.. cvcuda-autofunction:: cvcuda.copymakeborderstack_into -.. autofunction:: cvcuda.match +.. cvcuda-autofunction:: cvcuda.crop_flip_normalize_reformat -.. autofunction:: cvcuda.pillowresize +.. cvcuda-autofunction:: cvcuda.crop_flip_normalize_reformat_into -.. autofunction:: cvcuda.random_resized_crop +.. cvcuda-autofunction:: cvcuda.customcrop -.. autofunction:: cvcuda.reformat +.. cvcuda-autofunction:: cvcuda.customcrop_into -.. autofunction:: cvcuda.remap +.. cvcuda-autofunction:: cvcuda.cvtcolor -.. autofunction:: cvcuda.resize +.. cvcuda-autofunction:: cvcuda.cvtcolor_into -.. autofunction:: cvcuda.resize_crop_convert_reformat +.. cvcuda-autofunction:: cvcuda.erase -.. autofunction:: cvcuda.rotate +.. cvcuda-autofunction:: cvcuda.erase_into -.. autofunction:: cvcuda.sift +.. cvcuda-autofunction:: cvcuda.findhomography -.. autofunction:: cvcuda.stack +.. cvcuda-autofunction:: cvcuda.findhomography_into -.. autofunction:: cvcuda.threshold +.. cvcuda-autofunction:: cvcuda.findhomography_into_with_op -.. autofunction:: cvcuda.warp_affine +.. cvcuda-autofunction:: cvcuda.get_findhomography_operator -.. autofunction:: cvcuda.warp_perspective +.. cvcuda-autofunction:: cvcuda.flip + +.. cvcuda-autofunction:: cvcuda.flip_into + +.. cvcuda-autofunction:: cvcuda.gamma_contrast + +.. cvcuda-autofunction:: cvcuda.gamma_contrast_into + +.. cvcuda-autofunction:: cvcuda.gaussian + +.. cvcuda-autofunction:: cvcuda.gaussian_into + +.. cvcuda-autofunction:: cvcuda.gaussiannoise + +.. cvcuda-autofunction:: cvcuda.gaussiannoise_into + +.. cvcuda-autofunction:: cvcuda.histogram + +.. cvcuda-autofunction:: cvcuda.histogram_into + +.. cvcuda-autofunction:: cvcuda.histogrameq + +.. cvcuda-autofunction:: cvcuda.histogrameq_into + +.. cvcuda-autofunction:: cvcuda.hq_resize + +.. cvcuda-autofunction:: cvcuda.hq_resize_into + +.. cvcuda-autofunction:: cvcuda.inpaint + +.. cvcuda-autofunction:: cvcuda.inpaint_into + +.. cvcuda-autofunction:: cvcuda.invert + +.. cvcuda-autofunction:: cvcuda.invert_into + +.. cvcuda-autofunction:: cvcuda.joint_bilateral_filter + +.. cvcuda-autofunction:: cvcuda.joint_bilateral_filter_into + +.. cvcuda-autofunction:: cvcuda.jpeg_compression_distortion + +.. cvcuda-autofunction:: cvcuda.jpeg_compression_distortion_into + +.. cvcuda-autofunction:: cvcuda.label + +.. cvcuda-autofunction:: cvcuda.label_into + +.. cvcuda-autofunction:: cvcuda.laplacian + +.. cvcuda-autofunction:: cvcuda.laplacian_into + +.. cvcuda-autofunction:: cvcuda.match + +.. cvcuda-autofunction:: cvcuda.match_into + +.. cvcuda-autofunction:: cvcuda.max_loc + +.. cvcuda-autofunction:: cvcuda.max_loc_into + +.. cvcuda-autofunction:: cvcuda.median_blur + +.. cvcuda-autofunction:: cvcuda.median_blur_into + +.. cvcuda-autofunction:: cvcuda.minarearect + +.. cvcuda-autofunction:: cvcuda.minarearect_into + +.. cvcuda-autofunction:: cvcuda.min_loc + +.. cvcuda-autofunction:: cvcuda.min_loc_into + +.. cvcuda-autofunction:: cvcuda.min_max_loc + +.. cvcuda-autofunction:: cvcuda.min_max_loc_into + +.. cvcuda-autofunction:: cvcuda.morphology + +.. cvcuda-autofunction:: cvcuda.morphology_into + +.. cvcuda-autofunction:: cvcuda.nms + +.. cvcuda-autofunction:: cvcuda.nms_into + +.. cvcuda-autofunction:: cvcuda.normalize + +.. cvcuda-autofunction:: cvcuda.normalize_into + +.. cvcuda-autofunction:: cvcuda.osd + +.. cvcuda-autofunction:: cvcuda.osd_into + +.. cvcuda-autofunction:: cvcuda.padandstack + +.. cvcuda-autofunction:: cvcuda.padandstack_into + +.. cvcuda-autofunction:: cvcuda.pillowresize + +.. cvcuda-autofunction:: cvcuda.pillowresize_into + +.. cvcuda-autofunction:: cvcuda.posterize + +.. cvcuda-autofunction:: cvcuda.posterize_into + +.. cvcuda-autofunction:: cvcuda.random_resized_crop + +.. cvcuda-autofunction:: cvcuda.random_resized_crop_into + +.. cvcuda-autofunction:: cvcuda.reformat + +.. cvcuda-autofunction:: cvcuda.reformat_into + +.. cvcuda-autofunction:: cvcuda.remap + +.. cvcuda-autofunction:: cvcuda.remap_into + +.. cvcuda-autofunction:: cvcuda.resize + +.. cvcuda-autofunction:: cvcuda.resize_into + +.. cvcuda-autofunction:: cvcuda.resize_crop_convert_reformat + +.. cvcuda-autofunction:: cvcuda.resize_crop_convert_reformat_into + +.. cvcuda-autofunction:: cvcuda.rotate + +.. cvcuda-autofunction:: cvcuda.rotate_into + +.. cvcuda-autofunction:: cvcuda.sift + +.. cvcuda-autofunction:: cvcuda.sift_into + +.. cvcuda-autofunction:: cvcuda.solarize + +.. cvcuda-autofunction:: cvcuda.solarize_into + +.. cvcuda-autofunction:: cvcuda.stack + +.. cvcuda-autofunction:: cvcuda.stack_into + +.. cvcuda-autofunction:: cvcuda.threshold + +.. cvcuda-autofunction:: cvcuda.threshold_into + +.. cvcuda-autofunction:: cvcuda.warp_affine + +.. cvcuda-autofunction:: cvcuda.warp_affine_into + +.. cvcuda-autofunction:: cvcuda.warp_perspective + +.. cvcuda-autofunction:: cvcuda.warp_perspective_into diff --git a/docs/sphinx/content/cvcuda_op.rst b/docs/sphinx/operator_list.rst similarity index 78% rename from docs/sphinx/content/cvcuda_op.rst rename to docs/sphinx/operator_list.rst index b9ba61929..bfc8618af 100644 --- a/docs/sphinx/content/cvcuda_op.rst +++ b/docs/sphinx/operator_list.rst @@ -1,5 +1,5 @@ .. - # SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,8 +22,18 @@ - Definition * - Adaptive Thresholding (:py:func:`cvcuda.adaptivethreshold`) - Chooses threshold based on smaller regions in the neighborhood of each pixel. + * - Adjust Hue (:py:func:`cvcuda.adjust_hue`) + - Rotates the hue of an RGB image in HSV space by a scalar factor (torchvision-compatible). + * - Adjust Saturation (:py:func:`cvcuda.adjust_saturation`) + - Blends an RGB image toward its grayscale by a scalar factor (torchvision-compatible). + * - AdjustSharpness (:py:func:`cvcuda.adjust_sharpness`) + - Adjusts image sharpness by blending each image with a 3x3-smoothed copy of itself over the image interior. + * - AdjustContrast (:py:func:`cvcuda.adjust_contrast`) + - Blends an image toward its grayscale mean by a scalar factor (torchvision-compatible) * - Advanced Color Format Conversions (:py:func:`cvcuda.advcvtcolor`) - Performs color conversion from interleaved RGB/BGR <-> YUV/YVU and semi planar. Supported standards: BT.601. BT.709. BT.2020 + * - AutoContrast (:py:func:`cvcuda.autocontrast`) + - Maximizes per-channel contrast by stretching each channel to the full dynamic range * - AverageBlur (:py:func:`cvcuda.averageblur`) - Reduces image noise using an average filter * - BilateralFilter (:py:func:`cvcuda.bilateral_filter`) @@ -37,7 +47,9 @@ * - CenterCrop (:py:func:`cvcuda.center_crop`) - Crops an image at its center * - ChannelReorder (:py:func:`cvcuda.channelreorder`) - - Shuffles the order of image channels + - Gathers image channels using a host sequence for tensors or a per-image device tensor for variable-shape batches + * - CLAHE (:py:func:`cvcuda.clahe`) + - Enhances local contrast using contrast-limited adaptive histogram equalization on single-channel images * - Color_Twist (:py:func:`cvcuda.color_twist`) - Adjusts the hue saturation brightness and contrast of an image * - Composite (:py:func:`cvcuda.composite`) @@ -46,6 +58,8 @@ - Convolves an image with a provided kernel * - CopyMakeBorder (:py:func:`cvcuda.copymakeborder`) - Creates a border around an image + * - CropFlipNormalizeReformat (:py:func:`cvcuda.crop_flip_normalize_reformat`) + - Crops, flips, normalizes, and reformats a variable-shape image batch * - CustomCrop (:py:func:`cvcuda.customcrop`) - Crops an image with a given region-of-interest * - CvtColor (:py:func:`cvcuda.cvtcolor`) @@ -53,7 +67,7 @@ * - DataTypeConvert (:py:func:`cvcuda.convertto`) - Converts an image's data type with optional scaling * - Erase (:py:func:`cvcuda.erase`) - - Erases image regions + - Erases image regions, including a torchvision-compatible single-region overload * - Flip (:py:func:`cvcuda.flip`) - Flips a 2D image around its axis * - GammaContrast (:py:func:`cvcuda.gamma_contrast`) @@ -70,8 +84,12 @@ - Performs advanced resizing supporting 2D and 3D data, tensors, tensor batches, and varshape image batches (2D only). Supports nearest neighbor, linear, cubic, Gaussian and Lanczos interpolation, with optional antialiasing when down-sampling. * - Inpainting (:py:func:`cvcuda.inpaint`) - Performs inpainting by replacing a pixel by normalized weighted sum of all the known pixels in the neighborhood + * - Invert (:py:func:`cvcuda.invert`) + - Computes the per-element photometric negative (out = max - in) of an image * - Joint Bilateral Filter (:py:func:`cvcuda.joint_bilateral_filter`) - Reduces image noise while preserving strong edges based on a guidance image + * - JpegCompressionDistortion (:py:func:`cvcuda.jpeg_compression_distortion`) + - Simulates JPEG compression artifacts (4:2:0 chroma subsampling and per-8x8-block DCT quantization) on the GPU * - Label (:py:func:`cvcuda.label`) - Labels connected regions in an image using 4-way connectivity for foreground and 8-way for background pixels * - Laplacian (:py:func:`cvcuda.laplacian`) @@ -96,6 +114,8 @@ - Matches features computed separately (e.g. via the SIFT operator) in two images, e.g. using the brute force method * - PillowResize (:py:func:`cvcuda.pillowresize`) - Changes the size and scale of an image using python-pillow algorithm + * - Posterize (:py:func:`cvcuda.posterize`) + - Reduces each channel to its most-significant bits * - RandomResizedCrop (:py:func:`cvcuda.random_resized_crop`) - Crops a random portion of an image and resizes it to a specified size. * - Reformat (:py:func:`cvcuda.reformat`) @@ -110,6 +130,8 @@ - Rotates a 2D array in multiples of 90 degrees * - SIFT (:py:func:`cvcuda.sift`) - Identifies and matches features in images that are invariant to scale rotation and affine distortion. + * - Solarize (:py:func:`cvcuda.solarize`) + - Inverts all pixel values at or above a threshold * - Stack (:py:func:`cvcuda.stack`) - Combines multiple images into a single batch tensor * - Thresholding (:py:func:`cvcuda.threshold`) @@ -118,3 +140,5 @@ - Applies an affine transformation to an image * - WarpPerspective (:py:func:`cvcuda.warp_perspective`) - Applies a perspective transformation to an image + +To add a new operator, see :ref:`make_operator`. diff --git a/docs/sphinx/perf_benchmark.rst b/docs/sphinx/perf_benchmark.rst index 7ee7de6f4..aa8f42b2e 100644 --- a/docs/sphinx/perf_benchmark.rst +++ b/docs/sphinx/perf_benchmark.rst @@ -1,5 +1,5 @@ .. - # SPDX-FileCopyrightText: Copyright (c) 2022 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,6 +14,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +:orphan: + .. _perf_benchmark: Performance Benchmark diff --git a/docs/sphinx/relnotes/relnotes-template.md b/docs/sphinx/relnotes/relnotes-template.md new file mode 100644 index 000000000..08fc576b6 --- /dev/null +++ b/docs/sphinx/relnotes/relnotes-template.md @@ -0,0 +1,44 @@ + + +# vX.Y.Z + + +## Release Highlights + +CV-CUDA vX.Y.Z includes the following changes: + +### New Features and Enhancements + +- + +### Performance Improvements + +- + +### Bug Fixes + +- + +### Compatibility Changes + +- + +## Compatibility and Known Limitations + +For full details, see the main README's compatibility table and known limitations. + +## License + +CV-CUDA is licensed under the Apache 2.0 license. + +## Resources + +1. + +## Acknowledgements + +CV-CUDA originated as a collaborative effort between the NVIDIA and ByteDance +Machine Learning teams. diff --git a/docs/sphinx/relnotes/v0.11.0-beta.rst b/docs/sphinx/relnotes/v0.11.0-beta.rst index 2957e40ee..5d910dcfa 100644 --- a/docs/sphinx/relnotes/v0.11.0-beta.rst +++ b/docs/sphinx/relnotes/v0.11.0-beta.rst @@ -1,65 +1,65 @@ -.. - # SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - # SPDX-License-Identifier: Apache-2.0 - # - # Licensed 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. - -.. _v0.11.0-beta: - -v0.11.0-beta -============ - -Release Highlights ------------------- - -CV-CUDA v0.11.0 includes critical bug fixes alongside the following changes:​ - -* **New Features**:​ - - * Enable NVCV to be built as static library​ - * Improve Python doc generation and structure ​ - -* **Bug Fixes**:​ - - * Update pybind11 2.10.0 to 2.13.1. Fixes rare race conditions with Python garbage collector, adds compatibility with numpy2​ - -Compatibility and Known Limitations ------------------------------------ - -* **Pre-existing limitations**: - - * We note a bug in the YUV(420) color conversion API (NVCV_COLOR_RGB2YUV_I420) which incorrectly computes the U and V plane index​ - - * This persists through this release and we intend to address this bug in CV-CUDA v0.12.0​ - -For the full list, see main README on `CV-CUDA GitHub `_. - -License -------- - -CV-CUDA is licensed under the `Apache 2.0 `_ license. - -Resources ---------- - -1. `CV-CUDA GitHub `_ -2. `CV-CUDA Increasing Throughput and Reducing Costs for AI-Based Computer Vision with CV-CUDA `_ -3. `NVIDIA Announces Microsoft, Tencent, Baidu Adopting CV-CUDA for Computer Vision AI `_ -4. `CV-CUDA helps Tencent Cloud audio and video PaaS platform achieve full-process GPU acceleration for video enhancement AI `_ - -Acknowledgements ----------------- - -CV-CUDA is developed jointly by NVIDIA and the ByteDance Machine Learning team. - -.. [1] These fixes and features add micro-second-level overhead to Python operator calls. Based on the performance analysis of our Python samples, we expect the production- and pipeline-level impact to be negligible. CUDA kernel and C++ call performance is not affected. We aim to investigate and reduce this overhead further in a future release.​ +.. + # SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-License-Identifier: Apache-2.0 + # + # Licensed 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. + +.. _v0.11.0-beta: + +v0.11.0-beta +============ + +Release Highlights +------------------ + +CV-CUDA v0.11.0 includes critical bug fixes alongside the following changes:​ + +* **New Features**:​ + + * Enable NVCV to be built as static library​ + * Improve Python doc generation and structure ​ + +* **Bug Fixes**:​ + + * Update pybind11 2.10.0 to 2.13.1 [1]_. Fixes rare race conditions with Python garbage collector, adds compatibility with numpy2​ + +Compatibility and Known Limitations +----------------------------------- + +* **Pre-existing limitations**: + + * We note a bug in the YUV(420) color conversion API (NVCV_COLOR_RGB2YUV_I420) which incorrectly computes the U and V plane index​ + + * This persists through this release and we intend to address this bug in CV-CUDA v0.12.0​ + +For the full list, see main README on `CV-CUDA GitHub `_. + +License +------- + +CV-CUDA is licensed under the `Apache 2.0 `_ license. + +Resources +--------- + +1. `CV-CUDA GitHub `_ +2. `CV-CUDA Increasing Throughput and Reducing Costs for AI-Based Computer Vision with CV-CUDA `_ +3. `NVIDIA Announces Microsoft, Tencent, Baidu Adopting CV-CUDA for Computer Vision AI `_ +4. `CV-CUDA helps Tencent Cloud audio and video PaaS platform achieve full-process GPU acceleration for video enhancement AI `_ + +Acknowledgements +---------------- + +CV-CUDA is developed jointly by NVIDIA and the ByteDance Machine Learning team. + +.. [1] These fixes and features add micro-second-level overhead to Python operator calls. Based on the performance analysis of our Python samples, we expect the production- and pipeline-level impact to be negligible. CUDA kernel and C++ call performance is not affected. We aim to investigate and reduce this overhead further in a future release.​ diff --git a/docs/sphinx/relnotes/v0.13.0-beta.rst b/docs/sphinx/relnotes/v0.13.0-beta.rst index fdf98df46..84475c11f 100644 --- a/docs/sphinx/relnotes/v0.13.0-beta.rst +++ b/docs/sphinx/relnotes/v0.13.0-beta.rst @@ -1,65 +1,65 @@ -.. - # SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - # SPDX-License-Identifier: Apache-2.0 - # - # Licensed 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. - -.. _v0.13.0-beta: - -v0.13.0-beta -============ - -Release Highlights ------------------- - -CV-CUDA v0.13.0 includes ManyLinux 2014 compliant wheels alongside the following changes:​ - -* **New Features**:​ - - * Added Python wheel generation compliant with ManyLinux 2014 and PyPI standards. - - * The multiple python version wheels are now unified into a single wheel file per CUDA version​. - - * Included scripts to build two ManyLinux 2014 Docker images (CUDA 11, CUDA 12) for build, and four Ubuntu images (20.04 and 22.04 x CUDA 11, CUDA 12) for testing. - - * Python wheels must be built within the ManyLinux 2014 docker images to guarantee ManyLinux2014 compliance. - -* **Bug Fixes**:​ - - * Upgraded pybind11 to version 2.13.6 for improved compatibility and functionality.​ - - * Resolved Python ABI compatibility issues present in previous versions by upgrading pybind11 reported in previous versions.​ - - -Compatibility and Known Limitations ------------------------------------ - -For the full list, see main README on `CV-CUDA GitHub `_. - -License -------- - -CV-CUDA is licensed under the `Apache 2.0 `_ license. - -Resources ---------- - -1. `CV-CUDA GitHub `_ -2. `CV-CUDA Increasing Throughput and Reducing Costs for AI-Based Computer Vision with CV-CUDA `_ -3. `NVIDIA Announces Microsoft, Tencent, Baidu Adopting CV-CUDA for Computer Vision AI `_ -4. `CV-CUDA helps Tencent Cloud audio and video PaaS platform achieve full-process GPU acceleration for video enhancement AI `_ - -Acknowledgements ----------------- - -CV-CUDA is developed jointly by NVIDIA and the ByteDance Machine Learning team. +.. + # SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-License-Identifier: Apache-2.0 + # + # Licensed 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. + +.. _v0.13.0-beta: + +v0.13.0-beta +============ + +Release Highlights +------------------ + +CV-CUDA v0.13.0 includes ManyLinux 2014 compliant wheels alongside the following changes:​ + +* **New Features**:​ + + * Added Python wheel generation compliant with ManyLinux 2014 and PyPI standards. + + * The multiple python version wheels are now unified into a single wheel file per CUDA version​. + + * Included scripts to build two ManyLinux 2014 Docker images (CUDA 11, CUDA 12) for build, and four Ubuntu images (20.04 and 22.04 x CUDA 11, CUDA 12) for testing. + + * Python wheels must be built within the ManyLinux 2014 docker images to guarantee ManyLinux2014 compliance. + +* **Bug Fixes**:​ + + * Upgraded pybind11 to version 2.13.6 for improved compatibility and functionality.​ + + * Resolved Python ABI compatibility issues present in previous versions by upgrading pybind11 reported in previous versions.​ + + +Compatibility and Known Limitations +----------------------------------- + +For the full list, see main README on `CV-CUDA GitHub `_. + +License +------- + +CV-CUDA is licensed under the `Apache 2.0 `_ license. + +Resources +--------- + +1. `CV-CUDA GitHub `_ +2. `CV-CUDA Increasing Throughput and Reducing Costs for AI-Based Computer Vision with CV-CUDA `_ +3. `NVIDIA Announces Microsoft, Tencent, Baidu Adopting CV-CUDA for Computer Vision AI `_ +4. `CV-CUDA helps Tencent Cloud audio and video PaaS platform achieve full-process GPU acceleration for video enhancement AI `_ + +Acknowledgements +---------------- + +CV-CUDA is developed jointly by NVIDIA and the ByteDance Machine Learning team. diff --git a/docs/sphinx/relnotes/v0.16.0-beta.rst b/docs/sphinx/relnotes/v0.16.0-beta.rst index b3a523d9d..8d058d9de 100644 --- a/docs/sphinx/relnotes/v0.16.0-beta.rst +++ b/docs/sphinx/relnotes/v0.16.0-beta.rst @@ -1,5 +1,5 @@ .. - # SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -28,7 +28,7 @@ CV-CUDA v0.16.0 includes the following changes:​ * Added support for Python 3.14​, CUDA 13, GCC-12 to GCC-14 and Blackwell GPU architecture, including Jetson Thor - * Improved documentation, :doc:`samples <../samples>` and :doc:`framework interoperability <../interoperability>` examples + * Improved documentation, :doc:`samples <../samples>` and :doc:`framework interoperability <../samples/interoperability>` examples * Added new multi-architecture (x86_64, aarch64) Docker images for building (ManyLinux-based) and developing CV-CUDA (Ubuntu-based) diff --git a/docs/sphinx/relnotes/v0.17.0-beta.rst b/docs/sphinx/relnotes/v0.17.0-beta.rst new file mode 100644 index 000000000..d7afc0bb6 --- /dev/null +++ b/docs/sphinx/relnotes/v0.17.0-beta.rst @@ -0,0 +1,151 @@ +.. + # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-License-Identifier: Apache-2.0 + # + # Licensed 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. + +.. _v0.17.0-beta: + +v0.17.0-beta +================== + + +Release Highlights +------------------ + +CV-CUDA v0.17.0-beta includes the following changes: + +* **New Features and Enhancements**: + + * Added the ``JpegCompressionDistortion`` operator, a self-contained GPU + simulation of JPEG compression artifacts (full-range JFIF YCbCr, 4:2:0 + chroma subsampling, per-8x8-block DCT quantization; ported from NVIDIA + DALI, no codec dependency) with per-image or per-batch quality. + + * Added the ``AdjustContrast``, ``AdjustHue``, ``AdjustSaturation``, + ``AdjustSharpness``, ``AutoContrast``, ``CLAHE`` (Contrast Limited Adaptive + Histogram Equalization), ``Invert``, ``Posterize``, and ``Solarize`` + operators. + + * Added native planar (``NCHW``/``CHW``) support across the public image + operator catalog, subject to each operator's documented input type, + dimensionality, channel, format, and auxiliary-input limitations. + + * Added tensor input support to ``GammaContrast`` and by-value parameter paths + to ``BrightnessContrast``, ``GammaContrast``, ``GaussianNoise``, and + ``Normalize``, avoiding temporary device parameter tensors and + host-to-device copies. + + * Added ``NVCVRoundMode`` (``cvcuda.Round`` in Python), with nearest and + truncate modes for integral output conversion in ``ConvertTo`` and the + scalar ``GammaContrast`` path. + + * Extended the Python API with ``Stream.wait_stream()``, NumPy ``float16`` + interoperability, single-dimension and empty ``TensorLayout`` values, and + hashable ``TensorLayout`` and ``Type`` objects. + + * Added always-on NVTX ranges to public C and Python operator submissions for + improved Nsight profiling visibility. + + * Expanded Python operator samples and documentation, and revamped the Docker + build and development images. Source builds no longer use Git submodules. + +* **Performance Improvements**: + + * Improved performance across more than 50 operators. 1.8x average speedups (H100). + + * Reduced Python wrapper overhead in ``ResourceGuard`` with batched sync/hold + and cached pybind11 reverse casts. + + * Reduced binary and package sizes with size-optimized CUDA fatbinary + compression when built with CUDA 12.8 or later. + + * Reduced x86_64 package sizes by removing blanket SM86 and SM89 code + generation while retaining performance-sensitive operator cubins and + conservative coverage for inconclusive or post-freeze cases. + +* **Bug Fixes**: + + * Fixed correctness and robustness issues across operators: ``Composite``, + ``Erase``, ``FindHomography``, ``GammaContrast``, ``GaussianNoise``, + ``Histogram``, ``HistogramEq``, ``HQResize``, ``Inpaint``, + ``PillowResize``, ``RandomResizedCrop``, ``ResizeCropConvertReformat``, + ``Rotate``, and ``WarpAffine``/``WarpPerspective``. + + * Fixed cubic interpolation boundary conditions in ``Resize`` and the shared + cubic paths used by ``RandomResizedCrop``, and added 1-channel + (grayscale/Y8) support to ``ResizeCropConvertReformat`` + (`issue #285 `_). + + * Fixed multi-GPU workspace and cache handling, CUDA Array Interface and + DLPack stream synchronization, device restoration during ``Stream`` + destruction, and memory growth in the ``as_image`` cache + (`issue #258 `_). + + * Strengthened input validation and memory safety across operators, returning + clear errors instead of aborting or launching invalid CUDA work. Also fixed + ``ResourceGuard`` error paths that could terminate the Python process. + + * Fixed text rendering backend issues and added dedicated unit coverage for + the STB text backend. + + * Corrected multi-stream Python test synchronization (community contribution + `PR #288 `_ by Daniel Ching). + +* **Compatibility Changes**: + + * Dropped official support for Python 3.9. + + * C++ operator wrapper classes are now move-only; copying an operator wrapper + no longer compiles. + + * The C ``cvcudaConvertToSubmit`` function now requires an explicit + ``NVCVRoundMode`` argument. C++ and Python callers retain nearest rounding + as the default; existing C binaries using this entry point must be rebuilt. + + * Public NVCV allocator callbacks and resource user-pointer APIs now use the + opaque ``NVCVResourceContext``, ``NVCVMemoryBuffer``, and + ``NVCVUserPointer`` types instead of ``void *``. Custom allocators and + direct C/C++ callers may require signature updates or explicit casts. + + * Added ``CVCUDA_AARCH64_JETSON`` for Jetson Orin source builds; default + aarch64 source builds target SBSA. + + +Compatibility and Known Limitations +----------------------------------- + +For full details, see the main README's +`compatibility table `_ and +`known limitations `_. + +License +------- + +CV-CUDA is licensed under the `Apache 2.0 `_ license. + +Resources +--------- + +1. `CV-CUDA GitHub `_ +2. `Optimizing Microsoft Bing Visual Search with NVIDIA Accelerated Libraries `_ +3. `Accelerating AI Pipelines: Boosting Visual Search Efficiency `_ +4. `Optimize Short-Form Video Processing Toward the Speed of Light `_ +5. `CV-CUDA Increasing Throughput and Reducing Costs for AI-Based Computer Vision with CV-CUDA `_ +6. `NVIDIA Announces Microsoft, Tencent, Baidu Adopting CV-CUDA for Computer Vision AI `_ +7. `CV-CUDA helps Tencent Cloud audio and video PaaS platform achieve full-process GPU acceleration for video enhancement AI `_ + +Acknowledgements +---------------- + +CV-CUDA originated as a collaborative effort between NVIDIA and the ByteDance Machine Learning team. diff --git a/docs/sphinx/samples.rst b/docs/sphinx/samples.rst index 73567a50a..5f0cc2b0a 100644 --- a/docs/sphinx/samples.rst +++ b/docs/sphinx/samples.rst @@ -1,5 +1,5 @@ .. - # SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -35,6 +35,9 @@ Sample Categories **Applications** Complete end-to-end pipelines combining preprocessing, inference, and post-processing. +**Interoperability** + Examples demonstrating how CV-CUDA works with other GPU frameworks (PyTorch, CuPy, PyCUDA, etc.) through zero-copy data exchange. + Walkthrough Guide ----------------- @@ -65,8 +68,6 @@ After installation, activate the virtual environment: source venv_samples/bin/activate -For interoperability samples, see :ref:`interoperability_venv_installation`. - **Option 2: Build from Source** Alternatively, you can build CV-CUDA from source and install the remaining dependencies. @@ -113,8 +114,8 @@ See the complete :ref:`Hello World documentation ` for detai .. _running_the_samples: -Running Operator and Application Samples -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Running Samples +^^^^^^^^^^^^^^^ To test all samples at once: @@ -123,7 +124,6 @@ To test all samples at once: ./samples/run_samples.sh This script runs every sample with default parameters. - Next Steps ^^^^^^^^^^ @@ -134,6 +134,8 @@ Now that you've explored the basics: 3. **Read the API Documentation**: Explore the full :ref:`Python API ` 4. **Build Your Pipelines**: Use sample patterns in your applications +.. _sample_index: + Sample Index ------------ @@ -142,9 +144,12 @@ Quick access to all CV-CUDA sample documentation. Applications ^^^^^^^^^^^^ +See :ref:`Applications Overview ` for an overview of the application samples. + .. toctree:: :maxdepth: 1 + samples/applications samples/applications/hello_world samples/applications/classification samples/applications/object_detection @@ -156,11 +161,67 @@ Operators .. toctree:: :maxdepth: 1 - samples/operators/gaussian - samples/operators/resize - samples/operators/reformat - samples/operators/stack - samples/operators/label + adaptivethreshold + advcvtcolor + averageblur + bilateral_filter + bndbox + boxblur + brightness_contrast + center_crop + channelreorder + clahe + color_twist + composite + conv2d + convertto + copymakeborder + crop_flip_normalize_reformat + customcrop + cvtcolor + erase + flip + gamma_contrast + gaussian + gaussiannoise + histogrameq + hq_resize + inpaint + joint_bilateral_filter + label + laplacian + median_blur + morphology + normalize + osd + pillowresize + random_resized_crop + reformat + remap + resize + resize_crop_convert_reformat + rotate + stack + threshold + warp_affine + warp_perspective + +Interoperability +^^^^^^^^^^^^^^^^ + +See :ref:`Interoperability Overview ` for an overview of the interoperability samples. + +.. toctree:: + :maxdepth: 1 + + samples/interoperability + samples/interoperability/pytorch + samples/interoperability/cuda_python + samples/interoperability/numpy + samples/interoperability/nvimgcodec + samples/interoperability/pynvvideocodec + samples/interoperability/cupy + samples/interoperability/pycuda Common Utilities ---------------- @@ -178,8 +239,3 @@ Additional Resources * :ref:`Installation Guide ` - Build and setup instructions * `GitHub Repository `_ - Source code and issue tracker * `Discussions `_ - Ask questions and share use cases - -See Also --------- - -* :ref:`Interoperability ` - Using CV-CUDA with other libraries diff --git a/docs/sphinx/samples/applications/classification.rst b/docs/sphinx/samples/applications/classification.rst index 8c6cf7e83..0d76f0fbf 100644 --- a/docs/sphinx/samples/applications/classification.rst +++ b/docs/sphinx/samples/applications/classification.rst @@ -131,7 +131,7 @@ Preprocessing Pipeline The preprocessing steps: 1. **Setup Normalization Parameters**: ImageNet mean and std deviation -2. **Add Batch Dimension**: Convert HWC → NHWC using :py:func:`cvcuda.stack` +2. **Add Batch Dimension**: Convert HWC → NHWC using :pyfunc:`cvcuda.stack` 3. **Resize**: Scale to target size (default 224×224) 4. **Convert to Float**: Convert uint8 [0,255] → float32 [0.0,1.0] 5. **Normalize**: Apply ImageNet normalization: ``(x - mean) / std`` diff --git a/docs/sphinx/samples/applications/hello_world.rst b/docs/sphinx/samples/applications/hello_world.rst index 74c5bac15..2bd656574 100644 --- a/docs/sphinx/samples/applications/hello_world.rst +++ b/docs/sphinx/samples/applications/hello_world.rst @@ -1,5 +1,5 @@ .. - # SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -141,7 +141,7 @@ Batching Images :end-before: docs_tag: end_batch :dedent: -The :py:func:`cvcuda.stack` operation combines individual HWC tensors into a single NHWC tensor, enabling efficient batched processing. +The :pyfunc:`cvcuda.stack` operation combines individual HWC tensors into a single NHWC tensor, enabling efficient batched processing. Applying Gaussian Blur """"""""""""""""""""""" diff --git a/docs/sphinx/samples/applications/object_detection.rst b/docs/sphinx/samples/applications/object_detection.rst index bbca2eb2c..dfde35071 100644 --- a/docs/sphinx/samples/applications/object_detection.rst +++ b/docs/sphinx/samples/applications/object_detection.rst @@ -138,7 +138,7 @@ Preprocessing Pipeline Preprocessing steps: -1. **Add Batch Dimension**: HWC → NHWC using :py:func:`cvcuda.stack` +1. **Add Batch Dimension**: HWC → NHWC using :pyfunc:`cvcuda.stack` 2. **Resize**: Scale to target model input size (default 224×224) 3. **Normalize**: Convert to float32 [0,1] range 4. **Reformat**: NHWC → NCHW for model input @@ -175,7 +175,7 @@ Postprocessing: 1. **Copy to Host**: Transfer detection results to CPU 2. **Scale Boxes**: Scale from model input size to original image size 3. **Create Bounding Boxes**: Build CV-CUDA bounding box objects -4. **Draw Boxes**: Use :py:func:`cvcuda.bndbox` to draw on GPU +4. **Draw Boxes**: Use :pyfunc:`cvcuda.bndbox` to draw on GPU 5. **Save Result**: Write annotated image Expected Output diff --git a/docs/sphinx/samples/applications/segmentation.rst b/docs/sphinx/samples/applications/segmentation.rst index 0b3330f36..a59535845 100644 --- a/docs/sphinx/samples/applications/segmentation.rst +++ b/docs/sphinx/samples/applications/segmentation.rst @@ -168,7 +168,7 @@ Advanced post-processing: Joint Bilateral Filter ^^^^^^^^^^^^^^^^^^^^^^ -The joint bilateral filter (:py:func:`cvcuda.joint_bilateral_filter`) is key to quality: +The joint bilateral filter (:pyfunc:`cvcuda.joint_bilateral_filter`) is key to quality: * **Purpose**: Smooth mask while respecting image edges * **Joint**: Uses grayscale image to guide filtering diff --git a/docs/sphinx/samples/common.rst b/docs/sphinx/samples/common.rst index 64939b6a1..9d6516803 100644 --- a/docs/sphinx/samples/common.rst +++ b/docs/sphinx/samples/common.rst @@ -356,6 +356,7 @@ The common module requires: * **torch** - PyTorch for model export * **nvimgcodec** - Image I/O * **cuda-python** - CUDA runtime bindings +* **cupy** - GPU array library See Also -------- diff --git a/docs/sphinx/interoperability.rst b/docs/sphinx/samples/interoperability.rst similarity index 80% rename from docs/sphinx/interoperability.rst rename to docs/sphinx/samples/interoperability.rst index 3409c3801..1ebfaa0fc 100644 --- a/docs/sphinx/interoperability.rst +++ b/docs/sphinx/samples/interoperability.rst @@ -14,6 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +.. _sample_interoperability: .. _interoperability: Interoperability @@ -33,23 +34,21 @@ The key to this interoperability is the ``__cuda_array_interface__`` property, w expose via the ``.cuda()`` method. This property provides metadata about the GPU buffer (pointer, shape, dtype, strides) that other frameworks can use to create their own tensor views of the same memory. -.. _interoperability_venv_installation: - Setting Up the Environment --------------------------- +--------------------------- Use the provided installation script to automatically detect your CUDA version and install all required dependencies: .. code-block:: bash cd samples - ./install_interop_dependencies.sh + ./install_samples_dependencies.sh This script will: - Detect your CUDA version (12 or 13) - Create a virtual environment at ``venv_samples`` -- Install all required dependencies for interoperability samples (PyTorch, CuPy, PyCUDA, PyNvVideoCodec, CV-CUDA, etc.) +- Install all required dependencies including CV-CUDA, PyTorch, NumPy, and interoperability packages (CuPy, PyCUDA, PyNvVideoCodec) After installation, activate the virtual environment: @@ -66,15 +65,14 @@ CV-CUDA interoperates with the following frameworks through the CUDA Array Inter .. toctree:: :maxdepth: 1 - :caption: Frameworks - interop/pytorch - interop/cuda_python - interop/numpy - interop/nvimgcodec - interop/pynvvideocodec - interop/cupy - interop/pycuda + interoperability/pytorch + interoperability/cuda_python + interoperability/numpy + interoperability/nvimgcodec + interoperability/pynvvideocodec + interoperability/cupy + interoperability/pycuda Best Practices @@ -90,7 +88,7 @@ Best Practices * CV-CUDA uses HWC (Height × Width × Channels) layout by default for images * PyTorch typically uses CHW (Channels × Height × Width) layout - use ``.permute()`` to convert - * Be explicit about layout when converting with :py:func:`cvcuda.as_tensor` (e.g., ``cvcuda.as_tensor(obj, "HWC")``) + * Be explicit about layout when converting with :pyfunc:`cvcuda.as_tensor` (e.g., ``cvcuda.as_tensor(obj, "HWC")``) 3. **Device Management:** @@ -110,3 +108,11 @@ Best Practices * Check return codes when using CUDA Python directly * Validate tensor shapes and dtypes after conversion * Handle codec errors appropriately in pipelines + +See Also +-------- + +* :ref:`Applications ` - End-to-end deep learning pipelines +* :ref:`Operators ` - Individual CV-CUDA operators +* :ref:`Common Utilities ` - Helper functions +* :ref:`Python API ` - Complete API documentation diff --git a/docs/sphinx/interop/cuda_python.rst b/docs/sphinx/samples/interoperability/cuda_python.rst similarity index 82% rename from docs/sphinx/interop/cuda_python.rst rename to docs/sphinx/samples/interoperability/cuda_python.rst index edeab4d16..2a9996e73 100644 --- a/docs/sphinx/interop/cuda_python.rst +++ b/docs/sphinx/samples/interoperability/cuda_python.rst @@ -1,5 +1,5 @@ .. - # SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -31,7 +31,7 @@ To simplify working with CUDA Python, we provide utility functions in ``cuda_pyt The ``CudaBuffer`` class is a lightweight wrapper that implements the ``__cuda_array_interface__`` protocol, making raw CUDA memory accessible to CV-CUDA and other frameworks: -.. literalinclude:: ../../../samples/interoperability/cuda_python_common.py +.. literalinclude:: ../../../../samples/interoperability/cuda_python_common.py :language: python :start-after: docs_tag: begin_cuda_buffer :end-before: docs_tag: end_cuda_buffer @@ -45,12 +45,12 @@ Key features: **Memory Copy Utilities:** -.. literalinclude:: ../../../samples/interoperability/cuda_python_common.py +.. literalinclude:: ../../../../samples/interoperability/cuda_python_common.py :language: python :start-after: docs_tag: being_cuda_memcpy_h2d :end-before: docs_tag: end_cuda_memcpy_h2d -.. literalinclude:: ../../../samples/interoperability/cuda_python_common.py +.. literalinclude:: ../../../../samples/interoperability/cuda_python_common.py :language: python :start-after: docs_tag: begin_cuda_memcpy_d2h :end-before: docs_tag: end_cuda_memcpy_d2h @@ -67,14 +67,14 @@ This approach uses the custom ``CudaBuffer`` class to manually allocate CUDA mem **Required Imports:** -.. literalinclude:: ../../../samples/interoperability/cuda_python_interop_1.py +.. literalinclude:: ../../../../samples/interoperability/cuda_python_interop_1.py :language: python :start-after: docs_tag: begin_imports :end-before: docs_tag: end_imports **CUDA Python to CV-CUDA:** -.. literalinclude:: ../../../samples/interoperability/cuda_python_interop_1.py +.. literalinclude:: ../../../../samples/interoperability/cuda_python_interop_1.py :language: python :start-after: docs_tag: begin_cuda_python_to_cvcuda :end-before: docs_tag: end_cuda_python_to_cvcuda @@ -82,7 +82,7 @@ This approach uses the custom ``CudaBuffer`` class to manually allocate CUDA mem **CV-CUDA to CUDA Python:** -.. literalinclude:: ../../../samples/interoperability/cuda_python_interop_1.py +.. literalinclude:: ../../../../samples/interoperability/cuda_python_interop_1.py :language: python :start-after: docs_tag: begin_cvcuda_to_cuda_python :end-before: docs_tag: end_cvcuda_to_cuda_python @@ -104,14 +104,14 @@ raw buffer, then copying data into it. This is useful when you want CV-CUDA to m **Required Imports:** -.. literalinclude:: ../../../samples/interoperability/cuda_python_interop_2.py +.. literalinclude:: ../../../../samples/interoperability/cuda_python_interop_2.py :language: python :start-after: docs_tag: begin_imports :end-before: docs_tag: end_imports **CUDA Python to CV-CUDA:** -.. literalinclude:: ../../../samples/interoperability/cuda_python_interop_2.py +.. literalinclude:: ../../../../samples/interoperability/cuda_python_interop_2.py :language: python :start-after: docs_tag: begin_cuda_python_to_cvcuda :end-before: docs_tag: end_cuda_python_to_cvcuda @@ -122,14 +122,6 @@ raw buffer, then copying data into it. This is useful when you want CV-CUDA to m * Uses :py:class:`cvcuda.Tensor` to allocate GPU memory instead of ``CudaBuffer`` * CV-CUDA manages the memory lifecycle -**Conversion to Tensor:** - -.. literalinclude:: ../../../samples/interoperability/cuda_python_interop_2.py - :language: python - :start-after: docs_tag: begin_cvcuda_to_cuda_python - :end-before: docs_tag: end_cvcuda_to_cuda_python - :dedent: 4 - **When to use this approach:** * You want CV-CUDA to manage memory allocation diff --git a/docs/sphinx/samples/interoperability/cupy.rst b/docs/sphinx/samples/interoperability/cupy.rst new file mode 100644 index 000000000..74320e07f --- /dev/null +++ b/docs/sphinx/samples/interoperability/cupy.rst @@ -0,0 +1,106 @@ +.. + # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-License-Identifier: Apache-2.0 + # + # Licensed 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. + +CuPy +---- + +CuPy is a NumPy-compatible GPU array library that provides GPU acceleration for numerical operations. +It's an excellent choice when you want to use NumPy-like operations on the GPU. + +**Key Points:** + +* CuPy arrays are already on GPU, no explicit device transfer needed +* Use :py:func:`cvcuda.as_tensor` to convert CuPy arrays to CV-CUDA +* Use ``cupy.asarray()`` to convert CV-CUDA tensors back to CuPy +* CuPy provides the most NumPy-like interface for GPU arrays + +**Required Imports:** + +.. literalinclude:: ../../../../samples/interoperability/cupy_interop.py + :language: python + :start-after: docs_tag: begin_imports + :end-before: docs_tag: end_imports + +**CuPy to CV-CUDA:** + +.. literalinclude:: ../../../../samples/interoperability/cupy_interop.py + :language: python + :start-after: docs_tag: begin_cupy_to_cvcuda + :end-before: docs_tag: end_cupy_to_cvcuda + :dedent: 4 + +CuPy arrays are created directly on the GPU and can be immediately converted to CV-CUDA tensors. + +**CV-CUDA to CuPy:** + +.. literalinclude:: ../../../../samples/interoperability/cupy_interop.py + :language: python + :start-after: docs_tag: begin_cvcuda_to_cupy + :end-before: docs_tag: end_cvcuda_to_cupy + :dedent: 4 + +The ``cupy.asarray()`` function recognizes the CUDA Array Interface and creates a CuPy array that +views the same GPU memory as the CV-CUDA tensor. + +**Stream synchronization:** + +CV-CUDA streams are non-blocking (``cudaStreamNonBlocking``), so they do not implicitly +synchronize with CuPy's default stream (CUDA stream 0). CV-CUDA inserts the +necessary cross-stream barrier automatically when wrapping an external buffer +via :py:func:`cvcuda.as_tensor`; no manual ``cupy.cuda.Stream.null.synchronize()`` +or equivalent is required: + +.. code-block:: python + + src_cp = cupy.asarray(host_array) # H2D on CuPy's default stream + src_nv = cvcuda.as_tensor(src_cp, "NHWC") # CAI stream captured here + target = cvcuda.Stream() # dedicated non-blocking stream + with target: + out_nv = cvcuda.flip(src_nv, -1, stream=target) # waits for src_cp + target.sync() # done; result is valid + +To opt out (e.g., when you've manually synchronized), export your buffer with +``stream: -1`` in its CAI dict. + +.. note:: + + **Defensive synchronization for CuPy buffers.** CV-CUDA's automatic CAI v3 + honoring uses the producer's ``stream`` field to insert an event-based barrier. + That works only if the producer reports the stream the buffer was actually + written on. CuPy's CAI implementation instead reports the cupy-current stream + at the moment ``__cuda_array_interface__`` is evaluated; if a CuPy buffer was + filled inside a ``with cupy.cuda.Stream(...):`` block but read by CV-CUDA from + outside that block, CuPy reports the consumer's current stream (typically the + legacy default) rather than the producer's stream. An event-based barrier on + the wrong stream wouldn't capture the producer's work, especially when the + producer used a non-blocking stream that doesn't synchronize implicitly with + the legacy default. + + To handle this defensively, CV-CUDA falls back to ``cudaDeviceSynchronize`` + on the first use of any externally-wrapped buffer whose CAI ``stream`` is a + default-stream sentinel (``stream`` ∈ {``0``, ``1``, ``2``}). This is + correct regardless of which stream the producer actually used. The cost is + one host-side device sync per ``cvcuda.as_tensor`` of a non-cvcuda buffer; + subsequent CV-CUDA ops on the same wrapper take the event-based fast path. + CV-CUDA → CV-CUDA chains advertise the actual writer stream as a real + pointer in CAI (not a sentinel), so they keep the event-based fast path + end-to-end. + + This was reproduced on CuPy 13.6.0 and 14.0.1 (cupy-cuda12x) on CUDA 12.5; + ``tests/cvcuda/python/test_cai_input_stream_race.py`` is the regression + guard. + +**Complete Example:** See ``samples/interoperability/cupy_interop.py`` diff --git a/docs/sphinx/interop/numpy.rst b/docs/sphinx/samples/interoperability/numpy.rst similarity index 84% rename from docs/sphinx/interop/numpy.rst rename to docs/sphinx/samples/interoperability/numpy.rst index 80839f714..33f300b7d 100644 --- a/docs/sphinx/interop/numpy.rst +++ b/docs/sphinx/samples/interoperability/numpy.rst @@ -1,5 +1,5 @@ .. - # SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,14 +23,14 @@ CPU, you can transfer them to GPU using any of the GPU-accelerated frameworks me **Required Imports:** -.. literalinclude:: ../../../samples/interoperability/numpy_interop.py +.. literalinclude:: ../../../../samples/interoperability/numpy_interop.py :language: python :start-after: docs_tag: begin_imports :end-before: docs_tag: end_imports **Method 1: Via CUDA Python** -.. literalinclude:: ../../../samples/interoperability/numpy_interop.py +.. literalinclude:: ../../../../samples/interoperability/numpy_interop.py :language: python :start-after: docs_tag: begin_numpy_cuda_python :end-before: docs_tag: end_numpy_cuda_python @@ -40,7 +40,7 @@ This method gives you the most control over memory allocation and transfer. **Method 2: Via PyTorch** -.. literalinclude:: ../../../samples/interoperability/numpy_interop.py +.. literalinclude:: ../../../../samples/interoperability/numpy_interop.py :language: python :start-after: docs_tag: begin_numpy_torch :end-before: docs_tag: end_numpy_torch @@ -51,7 +51,7 @@ the NumPy array (on CPU), then ``.cuda()`` transfers it to GPU. **Method 3: Via CuPy** -.. literalinclude:: ../../../samples/interoperability/numpy_interop.py +.. literalinclude:: ../../../../samples/interoperability/numpy_interop.py :language: python :start-after: docs_tag: begin_numpy_cupy :end-before: docs_tag: end_numpy_cupy @@ -61,7 +61,7 @@ CuPy's ``cp.asarray()`` directly transfers NumPy arrays to GPU with NumPy-compat **Method 4: Via PyCUDA** -.. literalinclude:: ../../../samples/interoperability/numpy_interop.py +.. literalinclude:: ../../../../samples/interoperability/numpy_interop.py :language: python :start-after: docs_tag: begin_numpy_pycuda :end-before: docs_tag: end_numpy_pycuda diff --git a/docs/sphinx/interop/nvimgcodec.rst b/docs/sphinx/samples/interoperability/nvimgcodec.rst similarity index 83% rename from docs/sphinx/interop/nvimgcodec.rst rename to docs/sphinx/samples/interoperability/nvimgcodec.rst index b3f27d1de..19b09581d 100644 --- a/docs/sphinx/interop/nvimgcodec.rst +++ b/docs/sphinx/samples/interoperability/nvimgcodec.rst @@ -1,5 +1,5 @@ .. - # SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -29,14 +29,14 @@ memory. **Required Imports:** -.. literalinclude:: ../../../samples/interoperability/nvimgcodec_interop.py +.. literalinclude:: ../../../../samples/interoperability/nvimgcodec_interop.py :language: python :start-after: docs_tag: begin_imports :end-before: docs_tag: end_imports **Setup NvImgCodec:** -.. literalinclude:: ../../../samples/interoperability/nvimgcodec_interop.py +.. literalinclude:: ../../../../samples/interoperability/nvimgcodec_interop.py :language: python :start-after: docs_tag: begin_init_nvimgcodec :end-before: docs_tag: end_init_nvimgcodec @@ -44,7 +44,7 @@ memory. **NvImgCodec to CV-CUDA:** -.. literalinclude:: ../../../samples/interoperability/nvimgcodec_interop.py +.. literalinclude:: ../../../../samples/interoperability/nvimgcodec_interop.py :language: python :start-after: docs_tag: begin_nvimgcodec_to_cvcuda :end-before: docs_tag: end_nvimgcodec_to_cvcuda @@ -55,7 +55,7 @@ are decoded directly to GPU memory and can be immediately converted to CV-CUDA t **Process with CV-CUDA:** -.. literalinclude:: ../../../samples/interoperability/nvimgcodec_interop.py +.. literalinclude:: ../../../../samples/interoperability/nvimgcodec_interop.py :language: python :start-after: docs_tag: begin_cvcuda_resize :end-before: docs_tag: end_cvcuda_resize @@ -66,7 +66,7 @@ interpolation. **CV-CUDA to NvImgCodec:** -.. literalinclude:: ../../../samples/interoperability/nvimgcodec_interop.py +.. literalinclude:: ../../../../samples/interoperability/nvimgcodec_interop.py :language: python :start-after: docs_tag: begin_cvcuda_to_nvimgcodec :end-before: docs_tag: end_cvcuda_to_nvimgcodec diff --git a/docs/sphinx/interop/pycuda.rst b/docs/sphinx/samples/interoperability/pycuda.rst similarity index 84% rename from docs/sphinx/interop/pycuda.rst rename to docs/sphinx/samples/interoperability/pycuda.rst index 7f12ee23b..825ab1eda 100644 --- a/docs/sphinx/interop/pycuda.rst +++ b/docs/sphinx/samples/interoperability/pycuda.rst @@ -1,5 +1,5 @@ .. - # SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -29,14 +29,14 @@ when you need fine-grained control over GPU memory and kernel execution. **Required Imports:** -.. literalinclude:: ../../../samples/interoperability/pycuda_interop.py +.. literalinclude:: ../../../../samples/interoperability/pycuda_interop.py :language: python :start-after: docs_tag: begin_imports :end-before: docs_tag: end_imports **PyCUDA to CV-CUDA:** -.. literalinclude:: ../../../samples/interoperability/pycuda_interop.py +.. literalinclude:: ../../../../samples/interoperability/pycuda_interop.py :language: python :start-after: docs_tag: begin_pycuda_to_cvcuda :end-before: docs_tag: end_pycuda_to_cvcuda @@ -44,7 +44,7 @@ when you need fine-grained control over GPU memory and kernel execution. **CV-CUDA to PyCUDA:** -.. literalinclude:: ../../../samples/interoperability/pycuda_interop.py +.. literalinclude:: ../../../../samples/interoperability/pycuda_interop.py :language: python :start-after: docs_tag: begin_cvcuda_to_pycuda :end-before: docs_tag: end_cvcuda_to_pycuda diff --git a/docs/sphinx/interop/pynvvideocodec.rst b/docs/sphinx/samples/interoperability/pynvvideocodec.rst similarity index 80% rename from docs/sphinx/interop/pynvvideocodec.rst rename to docs/sphinx/samples/interoperability/pynvvideocodec.rst index f7c6ec3ad..50a579fa6 100644 --- a/docs/sphinx/interop/pynvvideocodec.rst +++ b/docs/sphinx/samples/interoperability/pynvvideocodec.rst @@ -1,5 +1,5 @@ .. - # SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -31,25 +31,24 @@ with CV-CUDA. **Required Imports:** -.. literalinclude:: ../../../samples/interoperability/pynvvideocodec_interop.py +.. literalinclude:: ../../../../samples/interoperability/pynvvideocodec_interop.py :language: python :start-after: docs_tag: begin_imports :end-before: docs_tag: end_imports **Setup PyNvVideoCodec:** -.. literalinclude:: ../../../samples/interoperability/pynvvideocodec_interop.py +.. literalinclude:: ../../../../samples/interoperability/pynvvideocodec_interop.py :language: python :start-after: docs_tag: begin_init_pynvvideocodec :end-before: docs_tag: end_init_pynvvideocodec :dedent: 4 -The decoder is configured to output RGB frames directly to device memory. The encoder is set up to -accept NV12 pixel format frames at the target resolution (640×480). +The decoder is configured to output RGB frames directly to device memory **Read video frames into CV-CUDA tensors and process:** -.. literalinclude:: ../../../samples/interoperability/pynvvideocodec_interop.py +.. literalinclude:: ../../../../samples/interoperability/pynvvideocodec_interop.py :language: python :start-after: docs_tag: begin_read_and_process_video :end-before: docs_tag: end_read_and_process_video @@ -65,12 +64,15 @@ apply operations on the stacked tensor. For clarity, we process each frame indiv **Encode processed frames:** -.. literalinclude:: ../../../samples/interoperability/pynvvideocodec_interop.py +.. literalinclude:: ../../../../samples/interoperability/pynvvideocodec_interop.py :language: python :start-after: docs_tag: begin_encode_frames :end-before: docs_tag: end_encode_frames :dedent: 4 +The encoder is set up to accept NV12 pixel format frames at the target resolution (640×480). +If a hardware video encoder (NVENC) is not available, the creation of the encoder object will raise an exception and the encoding step is skipped. + The processed frames are encoded back to video. The encoder produces compressed bitstreams that are written to the output file. Note that ``EndEncode()`` must be called to flush any remaining frames. diff --git a/docs/sphinx/samples/interoperability/pytorch.rst b/docs/sphinx/samples/interoperability/pytorch.rst new file mode 100644 index 000000000..5364dead5 --- /dev/null +++ b/docs/sphinx/samples/interoperability/pytorch.rst @@ -0,0 +1,102 @@ +.. + # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-License-Identifier: Apache-2.0 + # + # Licensed 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. + +PyTorch +------- + +PyTorch is one of the most popular deep learning frameworks. +CV-CUDA allows seamless interoperability with PyTorch, allowing you to use CV-CUDA's +optimized computer vision operations within your existing PyTorch workflows. + +**Key Points:** + +* PyTorch tensors must be on GPU (``.cuda()``) to convert to CV-CUDA +* Use :py:func:`cvcuda.as_tensor` to convert PyTorch tensors to CV-CUDA +* Use ``torch.as_tensor()`` to convert CV-CUDA tensors back to PyTorch +* You can use ``.clone()`` when converting from CV-CUDA to avoid shared memory issues, but this will incur a memcpy operation and potential performance degradation. Both torch and cvcuda support zero-copy intepability through their ``as_tensor`` functions. + +**Required Imports:** + +.. literalinclude:: ../../../../samples/interoperability/pytorch_interop.py + :language: python + :start-after: docs_tag: begin_imports + :end-before: docs_tag: end_imports + +**PyTorch to CV-CUDA:** + +.. literalinclude:: ../../../../samples/interoperability/pytorch_interop.py + :language: python + :start-after: docs_tag: begin_torch_to_cvcuda + :end-before: docs_tag: end_torch_to_cvcuda + :dedent: 4 + +The PyTorch tensor must be moved to GPU using ``.cuda()`` before conversion. The :py:func:`cvcuda.as_tensor` +function creates a CV-CUDA tensor that shares the same GPU memory as the PyTorch tensor using the ``__cuda_array_interface__`` protocol. + +**CV-CUDA to PyTorch:** + +.. literalinclude:: ../../../../samples/interoperability/pytorch_interop.py + :language: python + :start-after: docs_tag: begin_cvcuda_to_torch + :end-before: docs_tag: end_cvcuda_to_torch + :dedent: 4 + +The ``.clone()`` call is important to avoid multiple tensors sharing the same GPU buffer, which can +lead to unexpected behavior if one tensor is modified or deallocated. + +**Stream synchronization:** + +CV-CUDA streams are non-blocking, so they do not implicitly synchronize with PyTorch's +current stream. CV-CUDA inserts the necessary cross-stream barrier automatically when +wrapping a PyTorch tensor via :py:func:`cvcuda.as_tensor`; no explicit +``torch.cuda.synchronize()`` is required: + +.. code-block:: python + + src = torch.randint(0, 256, (2, 64, 64, 3), dtype=torch.uint8).cuda() + src_nv = cvcuda.as_tensor(src, "NHWC") + target = cvcuda.Stream() + with target: + out_nv = cvcuda.flip(src_nv, -1, stream=target) + target.sync() + +To opt out (e.g., when you've manually synchronized), export your buffer with +``stream: -1`` in its CAI dict. + +.. note:: + + **Defensive synchronization for PyTorch buffers.** PyTorch's + ``__cuda_array_interface__`` is **CAI v2** — it carries no ``stream`` field at + all. Per the CAI v3 spec, the ``stream`` field uses sentinel integers (``0`` + = "no stream associated, consumer must synchronize", ``1`` = legacy default + stream, ``2`` = per-thread default stream, other positive integers = real + stream handles). Because PyTorch advertises v2, CV-CUDA has no producer-stream + information and would race against any work PyTorch had queued on a + non-default (non-blocking) stream. + + To handle this defensively, CV-CUDA falls back to ``cudaDeviceSynchronize`` + on the first use of any externally-wrapped buffer whose producer stream is + unknown or is a default-stream sentinel. PyTorch tensors fall under "unknown" + (CAI v2, no stream field) and so always pay one host-side device sync at + ``cvcuda.as_tensor`` time; subsequent CV-CUDA ops on the same wrapper take the + event-based fast path. CV-CUDA → CV-CUDA chains advertise the actual writer + stream in CAI v3 and stay on the fast path end-to-end. + + This was verified on **PyTorch 2.9.0+cu128**. If a future PyTorch release + adopts CAI v3 and reports a real writer stream, CV-CUDA will pick that up + automatically and skip the defensive sync. + +**Complete Example:** See ``samples/interoperability/pytorch_interop.py`` diff --git a/docs/sphinx/samples/operators.rst b/docs/sphinx/samples/operators.rst index 848c8eec3..67c469e3c 100644 --- a/docs/sphinx/samples/operators.rst +++ b/docs/sphinx/samples/operators.rst @@ -1,5 +1,5 @@ .. - # SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,83 +14,56 @@ # See the License for the specific language governing permissions and # limitations under the License. -.. _sample_operators: - -Operators -========= - -Individual operator samples demonstrating specific CV-CUDA operations. - -Overview --------- - -The operator samples show focused functionality for understanding specific operations: - -* **Gaussian** - Blur and smoothing with configurable kernel and sigma -* **Resize** - Image resizing with various interpolation methods (linear, cubic, area, nearest) -* **Reformat** - Tensor layout conversions (HWC, CHW, NHWC, NCHW) -* **Stack** - Batch creation from multiple tensors for parallel processing -* **Label** - Connected component labeling for region identification - -These samples are perfect for: +:orphan: -* Learning individual operator behavior -* Understanding operator parameters -* Quick experimentation -* Building custom pipelines - -Operator Samples ----------------- +.. _sample_operators: -.. toctree:: - :maxdepth: 1 +Operator Samples Overview +========================= - Gaussian Blur - Resize - Reformat - Stack - Connected Components Labeling +Each operator sample reads an image from disk, runs a single CV-CUDA operator on the GPU, +and writes the result back to disk. -Common Usage Patterns +Resampling & Geometry --------------------- -Single Operator -^^^^^^^^^^^^^^^ +:doc:`operators/center_crop` · :doc:`operators/copymakeborder` · :doc:`operators/customcrop` · +:doc:`operators/flip` · :doc:`operators/hq_resize` · :doc:`operators/pillowresize` · +:doc:`operators/random_resized_crop` · :doc:`operators/remap` · :doc:`operators/resize` · +:doc:`operators/rotate` · :doc:`operators/warp_affine` · :doc:`operators/warp_perspective` -Simple, focused operation: - -.. code-block:: python +Filtering & Blur +---------------- - import cvcuda - from common import read_image, write_image +:doc:`operators/averageblur` · :doc:`operators/bilateral_filter` · :doc:`operators/boxblur` · +:doc:`operators/conv2d` · :doc:`operators/gaussian` · :doc:`operators/joint_bilateral_filter` · +:doc:`operators/laplacian` · :doc:`operators/median_blur` · :doc:`operators/morphology` - image = read_image("input.jpg") - result = cvcuda.gaussian(image, (5, 5), (1.0, 1.0)) - write_image(result, "output.jpg") +Color & Photometric +------------------- -Chaining Operators -^^^^^^^^^^^^^^^^^^ +:doc:`operators/advcvtcolor` · :doc:`operators/brightness_contrast` · +:doc:`operators/channelreorder` · :doc:`operators/clahe` · :doc:`operators/color_twist` · +:doc:`operators/convertto` · :doc:`operators/cvtcolor` · :doc:`operators/gamma_contrast` · +:doc:`operators/histogrameq` · :doc:`operators/normalize` -Combine multiple operations: +Thresholding & Segmentation +--------------------------- -.. code-block:: python +:doc:`operators/adaptivethreshold` · :doc:`operators/label` · :doc:`operators/threshold` - image = read_image("input.jpg") - resized = cvcuda.resize(image, (224, 224, 3)) - blurred = cvcuda.gaussian(resized, (5, 5), (1.0, 1.0)) - write_image(blurred, "output.jpg") +Noise, Restoration & Augmentation +---------------------------------- -Batch Processing -^^^^^^^^^^^^^^^^ +:doc:`operators/erase` · :doc:`operators/gaussiannoise` · :doc:`operators/inpaint` -.. code-block:: python +Compositing & Drawing +--------------------- - batch = cvcuda.stack([read_image(p) for p in paths]) - processed = cvcuda.gaussian(batch, (5, 5), (1.0, 1.0)) +:doc:`operators/bndbox` · :doc:`operators/composite` · :doc:`operators/osd` -See Also --------- +Layout & Preprocessing +----------------------- -* :ref:`Applications ` - End-to-end pipelines -* :ref:`Common Utilities ` - Helper functions -* :ref:`Python API ` - Complete operator API documentation +:doc:`operators/crop_flip_normalize_reformat` · :doc:`operators/reformat` · +:doc:`operators/resize_crop_convert_reformat` · :doc:`operators/stack` diff --git a/docs/sphinx/samples/operators/adaptivethreshold.rst b/docs/sphinx/samples/operators/adaptivethreshold.rst new file mode 100644 index 000000000..fa8662e3c --- /dev/null +++ b/docs/sphinx/samples/operators/adaptivethreshold.rst @@ -0,0 +1,138 @@ +.. + # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-License-Identifier: Apache-2.0 + # + # Licensed 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. + +.. _sample_adaptivethreshold: + +Adaptive Threshold +================== + +Overview +-------- + +The Adaptive Threshold sample demonstrates locally-adaptive binarisation of a grayscale image using +CV-CUDA's GPU-accelerated adaptive threshold operator. Unlike a global threshold, adaptive +thresholding computes a per-pixel threshold from a local neighbourhood, making it robust to +uneven illumination. The sample converts the colour input to grayscale, runs +:py:func:`cvcuda.adaptivethreshold`, then broadcasts the single-channel result back to RGB for +saving as a viewable JPEG. + +Usage +----- + +Basic Usage +^^^^^^^^^^^ + +Apply adaptive thresholding to the default cat image: + +.. code-block:: bash + + python3 adaptivethreshold.py + +Custom Input +^^^^^^^^^^^^ + +Supply your own image and output path: + +.. code-block:: bash + + python3 adaptivethreshold.py -i input.jpg -o cat_adaptivethreshold.jpg + +Command-Line Arguments +---------------------- + +.. list-table:: + :header-rows: 1 + :widths: 20 15 15 50 + + * - Argument + - Short Form + - Default + - Description + * - ``--input`` + - ``-i`` + - tabby_tiger_cat.jpg + - Input image file path + * - ``--output`` + - ``-o`` + - cvcuda/.cache/cat_adaptivethreshold.jpg + - Output image file path + +Implementation +-------------- + +Adaptive Threshold Operation +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. literalinclude:: ../../../../samples/operators/adaptivethreshold.py + :language: python + :start-after: docs_tag: begin_adaptivethreshold + :end-before: docs_tag: end_adaptivethreshold + :dedent: + +Key points: + +1. **Single-channel input**: :py:func:`cvcuda.adaptivethreshold` requires a U8 single-channel (HWC with C=1 or NHWC with C=1) tensor; colour images must be converted to grayscale first. +2. **Adaptive method**: ``GAUSSIAN_C`` uses a Gaussian-weighted neighbourhood average; ``MEAN_C`` uses a plain mean — both then subtract the constant ``c`` to produce the local threshold. +3. **block_size**: Must be an odd integer ≥ 3; larger values consider a wider neighbourhood and produce smoother thresholds. +4. **c constant**: A positive ``c`` makes the threshold stricter (fewer pixels exceed it), producing a sparser binary result; negative values do the opposite. +5. **Viewable output**: The single-channel binary result is replicated to three channels on the host before writing so that standard JPEG viewers can display it correctly. + +Expected Output +^^^^^^^^^^^^^^^ + +The output is a binary (black-and-white) image where pixel intensity reflects whether each +pixel exceeded its local Gaussian-weighted neighbourhood threshold: + +.. list-table:: + :widths: 50 50 + :align: center + + * - .. figure:: ../../content/tabby_tiger_cat.jpg + :width: 100% + + Original Input Image + + - .. figure:: ../../content/cat_adaptivethreshold.jpg + :width: 100% + + Output: Adaptive Threshold (GAUSSIAN_C, block=11, c=2) + +CV-CUDA Operators Used +^^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Operator + - Purpose + * - :py:func:`cvcuda.cvtcolor` + - Convert RGB input to single-channel grayscale + * - :py:func:`cvcuda.adaptivethreshold` + - Apply locally-adaptive binarisation per pixel + +Common Utilities Used +^^^^^^^^^^^^^^^^^^^^^ + +* :ref:`read_image() ` - Load input image as CV-CUDA tensor +* :ref:`write_image() ` - Save thresholded result as JPEG +* ``cuda_memcpy_d2h`` / ``cuda_memcpy_h2d`` - Transfer binary result to host for channel replication, then back to device + +See Also +-------- + +* :ref:`Resize Operator ` - Basic image transformation +* :ref:`Common Utilities ` - Helper functions diff --git a/docs/sphinx/samples/operators/advcvtcolor.rst b/docs/sphinx/samples/operators/advcvtcolor.rst new file mode 100644 index 000000000..2830f3945 --- /dev/null +++ b/docs/sphinx/samples/operators/advcvtcolor.rst @@ -0,0 +1,141 @@ +.. + # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-License-Identifier: Apache-2.0 + # + # Licensed 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. + +.. _sample_advcvtcolor: + +Advanced Color Conversion +========================= + +Overview +-------- + +The Advanced Color Conversion sample demonstrates GPU-accelerated color-space transformation +using CV-CUDA's ``advcvtcolor`` operator. Unlike the basic :ref:`CvtColor Operator `, +``advcvtcolor`` accepts a :pydata:`cvcuda.ColorSpec` argument that selects the standardized +luma/chroma coefficients (BT.601, BT.709, or BT.2020) used during the conversion. This sample +converts an RGB image to YUV (BT.709) and then back to RGB, demonstrating the round-trip workflow +common in video-processing pipelines. + +Usage +----- + +Basic Usage +^^^^^^^^^^^ + +Run with the default tabby cat image: + +.. code-block:: bash + + python3 advcvtcolor.py -i input.jpg + +Custom Input and Output +^^^^^^^^^^^^^^^^^^^^^^^ + +Specify a custom input image and output path: + +.. code-block:: bash + + python3 advcvtcolor.py -i image.jpg -o cat_advcvtcolor.jpg + +Command-Line Arguments +---------------------- + +.. list-table:: + :header-rows: 1 + :widths: 20 15 15 50 + + * - Argument + - Short Form + - Default + - Description + * - ``--input`` + - ``-i`` + - tabby_tiger_cat.jpg + - Input image file path + * - ``--output`` + - ``-o`` + - cvcuda/.cache/cat_advcvtcolor.jpg + - Output image file path + +Implementation +-------------- + +RGB to YUV and Back +^^^^^^^^^^^^^^^^^^^ + +.. literalinclude:: ../../../../samples/operators/advcvtcolor.py + :language: python + :start-after: docs_tag: begin_advcvtcolor + :end-before: docs_tag: end_advcvtcolor + :dedent: + +Key points: + +1. **Color Specification**: The ``spec`` argument selects the luma/chroma coefficients standard + (BT.601 for SD video, BT.709 for HDTV, BT.2020 for UHD/HDR). Mixing specifications between + forward and inverse conversions will produce incorrect colors. +2. **Round-trip fidelity**: Converting RGB → YUV → RGB with the same ``ColorSpec`` closely + reproduces the original image; any visible difference is due to quantization in uint8. +3. **Supported layouts**: Both ``HWC`` (single image) and ``NHWC`` (batch) layouts are accepted + without any reshaping step. +4. **Output shape preserved**: The output tensor always has the same shape and dtype as the input, + so no extra allocation or reshape is needed for 444 (3-channel) conversions. +5. **NV12/NV21 variants**: For semi-planar YUV (NV12/NV21) conversions the input height must be + ``H * 3 / 2`` and channels must be 1; the 444 interleaved path used here keeps the standard + ``(H, W, 3)`` shape. + +Expected Output +^^^^^^^^^^^^^^^ + +After the RGB → YUV → RGB round-trip the image looks nearly identical to the original: + +.. list-table:: + :widths: 50 50 + :align: center + + * - .. figure:: ../../content/tabby_tiger_cat.jpg + :width: 100% + + Original Input Image + + - .. figure:: ../../content/cat_advcvtcolor.jpg + :width: 100% + + Output: RGB → YUV (BT.709) → RGB + +CV-CUDA Operators Used +^^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Operator + - Purpose + * - :py:func:`cvcuda.advcvtcolor` + - Convert between RGB and YUV color spaces with a selectable color specification + +Common Utilities Used +^^^^^^^^^^^^^^^^^^^^^ + +* :ref:`read_image() ` - Load image as CV-CUDA tensor +* :ref:`write_image() ` - Save the color-converted image + +See Also +-------- + +* :ref:`Resize Operator ` - GPU-accelerated image resizing +* :ref:`Common Utilities ` - Helper functions used across samples diff --git a/docs/sphinx/samples/operators/averageblur.rst b/docs/sphinx/samples/operators/averageblur.rst new file mode 100644 index 000000000..b5b28ef63 --- /dev/null +++ b/docs/sphinx/samples/operators/averageblur.rst @@ -0,0 +1,131 @@ +.. + # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-License-Identifier: Apache-2.0 + # + # Licensed 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. + +.. _sample_averageblur: + +Average Blur +============ + +Overview +-------- + +The Average Blur sample demonstrates GPU-accelerated box filtering using CV-CUDA's +``averageblur`` operator. Each output pixel is the arithmetic mean of the pixels +within a rectangular kernel, producing a smoothing (low-pass) effect that reduces +noise and fine detail. + +Usage +----- + +Basic Usage +^^^^^^^^^^^ + +Apply the default 7×7 average blur to an image: + +.. code-block:: bash + + python3 averageblur.py -i input.jpg + +Custom Output Path +^^^^^^^^^^^^^^^^^^ + +Specify a different output file: + +.. code-block:: bash + + python3 averageblur.py -i input.jpg -o cat_averageblur.jpg + +Command-Line Arguments +---------------------- + +.. list-table:: + :header-rows: 1 + :widths: 20 15 15 50 + + * - Argument + - Short Form + - Default + - Description + * - ``--input`` + - ``-i`` + - tabby_tiger_cat.jpg + - Input image file path + * - ``--output`` + - ``-o`` + - cvcuda/.cache/cat_averageblur.jpg + - Output image file path + +Implementation +-------------- + +Average Blur Operator Call +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. literalinclude:: ../../../../samples/operators/averageblur.py + :language: python + :start-after: docs_tag: begin_averageblur + :end-before: docs_tag: end_averageblur + :dedent: + +Key points: + +1. **Kernel size**: ``[7, 7]`` specifies a 7-pixel-wide by 7-pixel-tall averaging window; larger kernels produce stronger blurring. +2. **Kernel anchor**: ``[-1, -1]`` automatically centers the anchor within the kernel, which is standard for symmetric filters. +3. **Border mode**: ``cvcuda.Border.REFLECT101`` mirrors pixels across the border without repeating the edge pixel, preventing visible seams at image boundaries. +4. **Supported dtypes**: U8, U16, S16, S32, and F32 are all supported, making the operator suitable for both display images and intermediate float feature maps. + +Expected Output +^^^^^^^^^^^^^^^ + +The output shows the image with a 7×7 box blur applied: + +.. list-table:: + :widths: 50 50 + :align: center + + * - .. figure:: ../../content/tabby_tiger_cat.jpg + :width: 100% + + Original Input Image + + - .. figure:: ../../content/cat_averageblur.jpg + :width: 100% + + Output: 7×7 Average Blur Applied + +CV-CUDA Operators Used +^^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Operator + - Purpose + * - :py:func:`cvcuda.averageblur` + - Apply a box (average) blur with a rectangular kernel to smooth the image + +Common Utilities Used +^^^^^^^^^^^^^^^^^^^^^ + +* :ref:`read_image() ` - Load image as CV-CUDA tensor +* :ref:`write_image() ` - Save blurred image to disk + +See Also +-------- + +* :ref:`Resize Operator ` - Resize images with GPU acceleration +* :ref:`Common Utilities ` - Shared helper functions diff --git a/docs/sphinx/samples/operators/bilateral_filter.rst b/docs/sphinx/samples/operators/bilateral_filter.rst new file mode 100644 index 000000000..5b59895eb --- /dev/null +++ b/docs/sphinx/samples/operators/bilateral_filter.rst @@ -0,0 +1,139 @@ +.. + # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-License-Identifier: Apache-2.0 + # + # Licensed 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. + +.. _sample_bilateral_filter: + +Bilateral Filter +================ + +Overview +-------- + +The Bilateral Filter sample demonstrates edge-preserving image smoothing using +CV-CUDA's GPU-accelerated bilateral filter operator. Unlike a standard Gaussian +blur, the bilateral filter weighs contributions by both spatial proximity and +color similarity, so it reduces noise in flat regions while leaving edges sharp. + +Usage +----- + +Basic Usage +^^^^^^^^^^^ + +Apply bilateral filter to an image with default parameters: + +.. code-block:: bash + + python3 bilateral_filter.py -i input.jpg + +Custom Parameters +^^^^^^^^^^^^^^^^^ + +Specify a custom output path: + +.. code-block:: bash + + python3 bilateral_filter.py -i input.jpg -o cat_bilateral_filter.jpg + +Command-Line Arguments +---------------------- + +.. list-table:: + :header-rows: 1 + :widths: 20 15 15 50 + + * - Argument + - Short Form + - Default + - Description + * - ``--input`` + - ``-i`` + - tabby_tiger_cat.jpg + - Input image file path + * - ``--output`` + - ``-o`` + - cvcuda/.cache/cat_bilateral_filter.jpg + - Output image file path + +Implementation +-------------- + +Bilateral Filter Application +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. literalinclude:: ../../../../samples/operators/bilateral_filter.py + :language: python + :start-after: docs_tag: begin_bilateral_filter + :end-before: docs_tag: end_bilateral_filter + :dedent: + +Key points: + +1. **Edge Preservation**: Unlike Gaussian blur, bilateral filter preserves sharp + edges by weighting pixel contributions by color similarity (``sigma_color``) + as well as spatial distance (``sigma_space``). +2. **Diameter**: Controls the size of the pixel neighborhood considered for each + output pixel. Larger values produce stronger smoothing but increase runtime. +3. **Sigma Color**: Higher values allow more dissimilar colors to be blended, + reducing edge-preservation strength toward a plain Gaussian blur. +4. **Sigma Space**: Controls spatial falloff; behaves like the radius of a + Gaussian blur and determines how far neighboring pixels contribute. +5. **Border Mode**: ``cvcuda.Border.REFLECT`` mirrors edge pixels outward, + avoiding darkening or artifacts at image boundaries. + +Expected Output +^^^^^^^^^^^^^^^ + +The output retains sharp edges (fur markings, whiskers) while noise and texture +in flat regions is smoothed: + +.. list-table:: + :widths: 50 50 + :align: center + + * - .. figure:: ../../content/tabby_tiger_cat.jpg + :width: 100% + + Original Input Image + + - .. figure:: ../../content/cat_bilateral_filter.jpg + :width: 100% + + Output: Edge-Preserving Bilateral Filter + +CV-CUDA Operators Used +^^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Operator + - Purpose + * - :py:func:`cvcuda.bilateral_filter` + - Apply edge-preserving bilateral smoothing to an image + +Common Utilities Used +^^^^^^^^^^^^^^^^^^^^^ + +* :ref:`read_image() ` - Load image as CV-CUDA tensor +* :ref:`write_image() ` - Save filtered image + +See Also +-------- + +* :ref:`Resize Operator ` - Resize images with GPU acceleration +* :ref:`Common Utilities ` - Helper functions diff --git a/docs/sphinx/samples/operators/bndbox.rst b/docs/sphinx/samples/operators/bndbox.rst new file mode 100644 index 000000000..88cf3eabf --- /dev/null +++ b/docs/sphinx/samples/operators/bndbox.rst @@ -0,0 +1,133 @@ +.. + # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-License-Identifier: Apache-2.0 + # + # Licensed 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. + +.. _sample_bndbox: + +Bounding Boxes +============== + +Overview +-------- + +The Bounding Boxes sample demonstrates GPU-accelerated axis-aligned bounding-box +rendering using CV-CUDA's ``bndbox`` operator. Three colored rectangles are drawn +over a cat image, each with an independent border color and thickness. + +Usage +----- + +Basic Usage +^^^^^^^^^^^ + +Draw boxes on the default cat image: + +.. code-block:: bash + + python3 bndbox.py + +Custom Input +^^^^^^^^^^^^ + +Specify your own image: + +.. code-block:: bash + + python3 bndbox.py -i input.jpg -o cat_bndbox.jpg + +Command-Line Arguments +---------------------- + +.. list-table:: + :header-rows: 1 + :widths: 20 15 15 50 + + * - Argument + - Short Form + - Default + - Description + * - ``--input`` + - ``-i`` + - tabby_tiger_cat.jpg + - Input image file path + * - ``--output`` + - ``-o`` + - cvcuda/.cache/cat_bndbox.jpg + - Output image file path + +Implementation +-------------- + +Bounding Box Rendering +^^^^^^^^^^^^^^^^^^^^^^ + +.. literalinclude:: ../../../../samples/operators/bndbox.py + :language: python + :start-after: docs_tag: begin_bndbox + :end-before: docs_tag: end_bndbox + :dedent: + +Key points: + +1. **Tensor layout**: :py:func:`cvcuda.bndbox` supports ``NHWC``/``HWC`` and + ``NCHW``/``CHW`` tensors. The sample uses ``NHWC``: use ``cvcuda.stack`` to + add the batch dimension to a single ``HWC`` image. +2. **BndBoxesI structure**: One list of ``BndBoxI`` objects per batch image; each box specifies ``(x, y, width, height)`` in pixel coordinates. +3. **Fill alpha 0**: Setting the RGBA fill alpha to 0 draws only the border, leaving interior pixels unchanged. +4. **In-place semantics**: The operator returns a new tensor but operates on a copy; the source tensor is not modified. +5. **Layout restoration**: Reshape the NHWC output back to HWC before passing to ``write_image``. + +Expected Output +^^^^^^^^^^^^^^^ + +The output shows the original cat image with three colored bounding boxes drawn on it: + +.. list-table:: + :widths: 50 50 + :align: center + + * - .. figure:: ../../content/tabby_tiger_cat.jpg + :width: 100% + + Original Input Image + + - .. figure:: ../../content/cat_bndbox.jpg + :width: 100% + + Output: Three colored bounding boxes drawn on the cat + +CV-CUDA Operators Used +^^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Operator + - Purpose + * - :py:func:`cvcuda.bndbox` + - Draw axis-aligned bounding boxes with configurable border color and thickness + +Common Utilities Used +^^^^^^^^^^^^^^^^^^^^^ + +* :ref:`read_image() ` - Load image as CV-CUDA tensor +* :ref:`write_image() ` - Save the annotated output image + +See Also +-------- + +* :ref:`Resize Operator ` - Scale images before annotation +* :ref:`Common Utilities ` - Helper functions diff --git a/docs/sphinx/samples/operators/boxblur.rst b/docs/sphinx/samples/operators/boxblur.rst new file mode 100644 index 000000000..8be7863b1 --- /dev/null +++ b/docs/sphinx/samples/operators/boxblur.rst @@ -0,0 +1,141 @@ +.. + # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-License-Identifier: Apache-2.0 + # + # Licensed 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. + +.. _sample_boxblur: + +Box Blur +======== + +Overview +-------- + +The Box Blur sample demonstrates selective region blurring using CV-CUDA's GPU-accelerated +box blur operator. Rather than blurring the entire image, the operator accepts a list of +``BlurBoxI`` rectangles per image in the batch and applies a mean (box) filter only inside +those regions. Pixels outside the declared boxes are copied through unchanged, making the +operator ideal for privacy redaction, watermark concealment, and artistic effects. + +Usage +----- + +Basic Usage +^^^^^^^^^^^ + +Blur three rectangular regions of the default input image: + +.. code-block:: bash + + python3 boxblur.py -i input.jpg + +Custom Input and Output +^^^^^^^^^^^^^^^^^^^^^^^ + +Specify input and output paths explicitly: + +.. code-block:: bash + + python3 boxblur.py -i image.jpg -o cat_boxblur.jpg + +Command-Line Arguments +---------------------- + +.. list-table:: + :header-rows: 1 + :widths: 20 15 20 45 + + * - Argument + - Short Form + - Default + - Description + * - ``--input`` + - ``-i`` + - tabby_tiger_cat.jpg + - Input image file path + * - ``--output`` + - ``-o`` + - cvcuda/.cache/cat_boxblur.jpg + - Output image file path + +Implementation +-------------- + +Box Blur on Selected Regions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. literalinclude:: ../../../../samples/operators/boxblur.py + :language: python + :start-after: docs_tag: begin_boxblur + :end-before: docs_tag: end_boxblur + :dedent: + +Key points: + +1. **Batch dimension required**: ``cvcuda.boxblur`` expects NHWC input; use + ``cvcuda.stack([hwc_image])`` to add a leading batch dimension before calling the operator. +2. **BlurBoxesI structure**: One inner list of ``BlurBoxI`` objects per image in the batch; + each box is ``(x, y, width, height)`` in pixel coordinates plus a ``kernelSize`` for the + square mean filter. +3. **Selective blurring**: Only the pixels inside each declared rectangle are filtered; all + other pixels are passed through untouched. +4. **Kernel size trade-off**: Larger ``kernelSize`` produces stronger, more noticeable blur + at the cost of slightly more compute; the kernel must be odd and at least 1. +5. **Layout restoration**: After blurring, ``reshape(shape[1:], "HWC")`` strips the batch + dimension so the result can be saved directly with ``write_image``. + +Expected Output +^^^^^^^^^^^^^^^ + +The output image is identical to the input except for three blurred rectangles: a moderate +patch in the upper-left, a strong central blur, and a light blur in the lower-right corner. + +.. list-table:: + :widths: 50 50 + :align: center + + * - .. figure:: ../../content/tabby_tiger_cat.jpg + :width: 100% + + Original Input Image + + - .. figure:: ../../content/cat_boxblur.jpg + :width: 100% + + Output: Selective Box Blur Applied + +CV-CUDA Operators Used +^^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Operator + - Purpose + * - :py:func:`cvcuda.boxblur` + - Apply a mean (box) filter to user-defined rectangular regions within an image + +Common Utilities Used +^^^^^^^^^^^^^^^^^^^^^ + +* :ref:`read_image() ` - Load image as CV-CUDA tensor +* :ref:`write_image() ` - Save the blurred result image +* :ref:`parse_image_args() ` - Parse ``--input`` / ``--output`` CLI arguments + +See Also +-------- + +* :ref:`Resize Operator ` - GPU-accelerated image resize +* :ref:`Common Utilities ` - Helper functions used across samples diff --git a/docs/sphinx/samples/operators/brightness_contrast.rst b/docs/sphinx/samples/operators/brightness_contrast.rst new file mode 100644 index 000000000..14d4dffc4 --- /dev/null +++ b/docs/sphinx/samples/operators/brightness_contrast.rst @@ -0,0 +1,150 @@ +.. + # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-License-Identifier: Apache-2.0 + # + # Licensed 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. + +.. _sample_brightness_contrast: + +Brightness Contrast +=================== + +Overview +-------- + +The Brightness Contrast sample demonstrates GPU-accelerated brightness and contrast adjustment +using CV-CUDA's ``brightness_contrast`` operator. The operator applies a per-image affine +transform to every pixel: + +.. code-block:: text + + output = brightness * (contrast * (input − contrast_center) + contrast_center) + brightness_shift + +where ``brightness``, ``contrast``, ``brightness_shift``, and ``contrast_center`` are +per-image scalar tensors. This formulation separates multiplicative brightness from the +contrast pivot, giving fine-grained control over image appearance. + +Usage +----- + +Basic Usage +^^^^^^^^^^^ + +Apply the default brightening/contrast boost to the bundled test image: + +.. code-block:: bash + + python3 brightness_contrast.py + +Custom Input +^^^^^^^^^^^^ + +Supply your own image: + +.. code-block:: bash + + python3 brightness_contrast.py -i input.jpg -o output.jpg + +Command-Line Arguments +---------------------- + +.. list-table:: + :header-rows: 1 + :widths: 20 15 15 50 + + * - Argument + - Short Form + - Default + - Description + * - ``--input`` + - ``-i`` + - tabby_tiger_cat.jpg + - Input image file path + * - ``--output`` + - ``-o`` + - cvcuda/.cache/cat_brightness_contrast.jpg + - Output image file path + +Implementation +-------------- + +Parameter Setup +^^^^^^^^^^^^^^^ + +.. literalinclude:: ../../../../samples/operators/brightness_contrast.py + :language: python + :start-after: docs_tag: begin_brightness_contrast_setup + :end-before: docs_tag: end_brightness_contrast_setup + :dedent: + +Operator Call +^^^^^^^^^^^^^ + +.. literalinclude:: ../../../../samples/operators/brightness_contrast.py + :language: python + :start-after: docs_tag: begin_brightness_contrast + :end-before: docs_tag: end_brightness_contrast + :dedent: + +Key points: + +1. **Parameter tensors** — each of ``brightness``, ``contrast``, ``brightness_shift``, and + ``contrast_center`` is a 1-D ``"N"``-layout float32 tensor with one element per image. +2. **All parameters are optional** — you may pass any subset; omitted parameters default to + identity values (brightness=1, contrast=1, brightness_shift=0, contrast_center=0). +3. **Dtype preserved** — the output tensor has the same dtype and layout as the input, so no + post-processing conversion is needed for uint8 HWC images. +4. **contrast_center pivot** — setting ``contrast_center`` to 127.0 for uint8 data places the + pivot at mid-gray, which preserves overall luminance while expanding tonal range. + +Expected Output +^^^^^^^^^^^^^^^ + +.. list-table:: + :widths: 50 50 + :align: center + + * - .. figure:: ../../content/tabby_tiger_cat.jpg + :width: 100% + + Original Input Image + + - .. figure:: ../../content/cat_brightness_contrast.jpg + :width: 100% + + Output: Brightness × 1.5, Contrast × 1.4 + +CV-CUDA Operators Used +^^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Operator + - Purpose + * - :py:func:`cvcuda.brightness_contrast` + - Per-image affine pixel transform for brightness and contrast adjustment + +Common Utilities Used +^^^^^^^^^^^^^^^^^^^^^ + +* :ref:`read_image() ` - Load image as CV-CUDA tensor +* :ref:`write_image() ` - Save adjusted image +* ``cuda_memcpy_h2d`` - Upload per-image scalar parameters from host to device + +See Also +-------- + +* :ref:`Resize Operator ` - Resize images with CV-CUDA +* :ref:`Common Utilities ` - Helper functions used by all operator samples diff --git a/docs/sphinx/samples/operators/center_crop.rst b/docs/sphinx/samples/operators/center_crop.rst new file mode 100644 index 000000000..df89955e6 --- /dev/null +++ b/docs/sphinx/samples/operators/center_crop.rst @@ -0,0 +1,146 @@ +.. + # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-License-Identifier: Apache-2.0 + # + # Licensed 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. + +.. _sample_center_crop: + +Center Crop +=========== + +Overview +-------- + +The Center Crop sample demonstrates symmetric center-cropping of an image using +CV-CUDA's GPU-accelerated ``center_crop`` operator. The operator extracts a +rectangular region from the geometric centre of the image without requiring the +caller to compute corner offsets manually. + +Usage +----- + +Basic Usage +^^^^^^^^^^^ + +Crop the default cat image to 224×224 pixels: + +.. code-block:: bash + + python3 center_crop.py -i input.jpg + +Custom Crop Size +^^^^^^^^^^^^^^^^ + +Specify a different crop width and height: + +.. code-block:: bash + + python3 center_crop.py -i input.jpg -o cat_center_crop.jpg --width 320 --height 240 + +Command-Line Arguments +---------------------- + +.. list-table:: + :header-rows: 1 + :widths: 20 15 15 50 + + * - Argument + - Short Form + - Default + - Description + * - ``--input`` + - ``-i`` + - tabby_tiger_cat.jpg + - Input image file path + * - ``--output`` + - ``-o`` + - cvcuda/.cache/cat_center_crop.jpg + - Output image file path + * - ``--width`` + - + - 224 + - Crop width in pixels (clamped to input width) + * - ``--height`` + - + - 224 + - Crop height in pixels (clamped to input height) + +Implementation +-------------- + +Center Crop +^^^^^^^^^^^ + +.. literalinclude:: ../../../../samples/operators/center_crop.py + :language: python + :start-after: docs_tag: begin_center_crop + :end-before: docs_tag: end_center_crop + :dedent: + +Key points: + +1. **No coordinate math**: ``cvcuda.center_crop`` computes the top-left corner + internally, so the caller only needs to supply the desired ``[height, width]``. +2. **crop_size list**: The second argument is a two-element Python list + ``[crop_height, crop_width]`` — not a tuple. +3. **Layout preserved**: The output tensor shares the same layout (HWC/NHWC) and + dtype as the input; no conversion is needed before writing. +4. **Clamping**: The sample clamps the requested crop dimensions to the actual image + size to avoid an out-of-bounds error when the crop is larger than the source. +5. **Single-image usage**: The sample operates on an HWC tensor directly; batched + NHWC usage follows the same ``crop_size`` argument convention. + +Expected Output +^^^^^^^^^^^^^^^ + +The output is the center portion of the input image at the requested dimensions +(default 224×224): + +.. list-table:: + :widths: 50 50 + :align: center + + * - .. figure:: ../../content/tabby_tiger_cat.jpg + :width: 100% + + Original Input Image + + - .. figure:: ../../content/cat_center_crop.jpg + :width: 100% + + Output: Center-cropped to 224×224 + +CV-CUDA Operators Used +^^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Operator + - Purpose + * - :py:func:`cvcuda.center_crop` + - Symmetrically crop a rectangular region from the centre of an image + +Common Utilities Used +^^^^^^^^^^^^^^^^^^^^^ + +* :ref:`read_image() ` - Load image as CV-CUDA tensor +* :ref:`write_image() ` - Save cropped image + +See Also +-------- + +* :ref:`Resize Operator ` - Scale images to arbitrary dimensions +* :ref:`Common Utilities ` - Helper functions used across samples diff --git a/docs/sphinx/samples/operators/channelreorder.rst b/docs/sphinx/samples/operators/channelreorder.rst new file mode 100644 index 000000000..baa1fbdd7 --- /dev/null +++ b/docs/sphinx/samples/operators/channelreorder.rst @@ -0,0 +1,147 @@ +.. + # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-License-Identifier: Apache-2.0 + # + # Licensed 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. + +.. _sample_channelreorder: + +Channel Reorder +=============== + +Overview +-------- + +The Channel Reorder sample demonstrates per-image channel permutation using CV-CUDA's +GPU-accelerated ``channelreorder`` operator. It reads an RGB image, wraps it in an +``ImageBatchVarShape``, specifies a ``[2, 1, 0]`` channel-index order to swap R and B +(producing a BGR image), and writes the result back to disk. + +Usage +----- + +Basic Usage +^^^^^^^^^^^ + +Apply the default RGB → BGR channel swap: + +.. code-block:: bash + + python3 channelreorder.py -i input.jpg + +Custom Input and Output +^^^^^^^^^^^^^^^^^^^^^^^ + +Specify custom input and output paths: + +.. code-block:: bash + + python3 channelreorder.py -i input.jpg -o cat_channelreorder.jpg + +Command-Line Arguments +---------------------- + +.. list-table:: + :header-rows: 1 + :widths: 20 15 15 50 + + * - Argument + - Short Form + - Default + - Description + * - ``--input`` + - ``-i`` + - tabby_tiger_cat.jpg + - Input image file path + * - ``--output`` + - ``-o`` + - cvcuda/.cache/cat_channelreorder.jpg + - Output image file path + +Implementation +-------------- + +Setup: Wrapping the Input Tensor as an ImageBatchVarShape +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. literalinclude:: ../../../../samples/operators/channelreorder.py + :language: python + :start-after: docs_tag: begin_channelreorder_setup + :end-before: docs_tag: end_channelreorder_setup + :dedent: + +Channel Reorder Operator Call +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. literalinclude:: ../../../../samples/operators/channelreorder.py + :language: python + :start-after: docs_tag: begin_channelreorder + :end-before: docs_tag: end_channelreorder + :dedent: + +Key points: + +1. **Two input containers** — fixed-shape ``Tensor`` inputs take a host order sequence, while + ``ImageBatchVarShape`` inputs take a device-resident per-image orders tensor. +2. **VarShape orders layout** — the ``orders`` tensor must have layout ``"NC"`` (num_images × + num_channels), where each row gives the input-channel index for each output channel. +3. **Tensor order sequence** — ``cvcuda.channelreorder(tensor, [2, 1, 0])`` performs the + canonical RGB → BGR swap without allocating a device parameter tensor. +4. **Zero-copy wrapping** — ``cvcuda.as_image`` ties the source buffer lifetime to the + ``Image`` object; no extra device copy is performed. +5. **Result extraction** — iterate over a variable-shape output and call + ``cvcuda.as_tensor(out_image.cuda(), "HWC")`` to obtain a writable HWC tensor. + +Expected Output +^^^^^^^^^^^^^^^ + +The output image has its red and blue channels swapped relative to the input: + +.. list-table:: + :widths: 50 50 + :align: center + + * - .. figure:: ../../content/tabby_tiger_cat.jpg + :width: 100% + + Original Input Image (RGB) + + - .. figure:: ../../content/cat_channelreorder.jpg + :width: 100% + + Output: Channels Reordered to BGR + +CV-CUDA Operators Used +^^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Operator + - Purpose + * - :py:func:`cvcuda.channelreorder` + - Permute image channels according to a per-image index tensor + +Common Utilities Used +^^^^^^^^^^^^^^^^^^^^^ + +* :ref:`read_image() ` - Load image as CV-CUDA tensor +* :ref:`write_image() ` - Save reordered image +* ``cuda_memcpy_h2d`` - Upload the host-side orders array to the GPU + +See Also +-------- + +* :ref:`Resize Operator ` - Basic GPU image resizing +* :ref:`Common Utilities ` - Helper functions diff --git a/docs/sphinx/samples/operators/clahe.rst b/docs/sphinx/samples/operators/clahe.rst new file mode 100644 index 000000000..319055006 --- /dev/null +++ b/docs/sphinx/samples/operators/clahe.rst @@ -0,0 +1,149 @@ +.. + # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-License-Identifier: Apache-2.0 + # + # Licensed 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. + +.. _sample_clahe: + +CLAHE +===== + +Overview +-------- + +The CLAHE sample demonstrates Contrast Limited Adaptive Histogram Equalization using CV-CUDA's +GPU-accelerated ``cvcuda.clahe`` operator. CLAHE improves local contrast by equalizing the +histogram of small contextual tiles independently, then clipping the amplification to a +user-supplied limit to suppress noise amplification. The result is a perceptually clearer image +without the over-saturation that can occur with global histogram equalization. + +Because ``cvcuda.clahe`` requires a single-channel (grayscale) ``U8`` tensor, the sample first +converts the RGB input to grayscale, applies CLAHE, and then replicates the enhanced grayscale +channel across R, G, and B for a viewable output image. + +Usage +----- + +Basic Usage +^^^^^^^^^^^ + +Apply CLAHE to an image (default ``clip_limit=2.0``, ``tile_grid_size=(8, 8)``): + +.. code-block:: bash + + python3 clahe.py -i input.jpg + +Custom Example +^^^^^^^^^^^^^^ + +Specify a custom input and output path: + +.. code-block:: bash + + python3 clahe.py -i input.jpg -o cat_clahe.jpg + +Command-Line Arguments +---------------------- + +.. list-table:: + :header-rows: 1 + :widths: 20 15 15 50 + + * - Argument + - Short Form + - Default + - Description + * - ``--input`` + - ``-i`` + - tabby_tiger_cat.jpg + - Input image file path + * - ``--output`` + - ``-o`` + - cvcuda/.cache/cat_clahe.jpg + - Output image file path + +Implementation +-------------- + +CLAHE Operator +^^^^^^^^^^^^^^ + +.. literalinclude:: ../../../../samples/operators/clahe.py + :language: python + :start-after: docs_tag: begin_clahe + :end-before: docs_tag: end_clahe + :dedent: + +Key points: + +1. **Grayscale requirement**: ``cvcuda.clahe`` only accepts single-channel ``U8`` tensors; RGB + inputs must first be converted with ``cvcuda.cvtcolor(src, cvcuda.ColorConversion.RGB2GRAY)``. +2. **clip_limit**: Values above 1.0 enable contrast limiting; the default of 2.0 provides + moderate enhancement while suppressing noise. Setting it to 0.0 raises an exception. +3. **tile_grid_size**: The tuple ``(cols, rows)`` of contextual tiles; each tile must be at + least 1×1. Larger grids produce more localised adaptation at the cost of extra computation. +4. **Stream support**: An optional ``stream`` keyword enables asynchronous GPU execution; call + ``stream.sync()`` before reading results back to the host. +5. **Batch support**: The operator accepts both ``HWC`` (single image) and ``NHWC`` (batch) + tensors as well as variable-shape image batches (``ImageBatchVarShape``). + +Expected Output +^^^^^^^^^^^^^^^ + +The output shows the grayscale-enhanced image saved as a three-channel (RGB) JPEG for +viewer compatibility: + +.. list-table:: + :widths: 50 50 + :align: center + + * - .. figure:: ../../content/tabby_tiger_cat.jpg + :width: 100% + + Original Input Image + + - .. figure:: ../../content/cat_clahe.jpg + :width: 100% + + Output: CLAHE-enhanced grayscale (replicated to RGB) + +CV-CUDA Operators Used +^^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Operator + - Purpose + * - :py:func:`cvcuda.clahe` + - Contrast Limited Adaptive Histogram Equalization on a grayscale tensor + * - :py:func:`cvcuda.cvtcolor` + - Convert RGB input image to single-channel grayscale before CLAHE + * - :py:func:`cvcuda.stack` + - Stack the HWC input tensor into an NHWC batch for ``cvtcolor`` + +Common Utilities Used +^^^^^^^^^^^^^^^^^^^^^ + +* :ref:`read_image() ` - Load image as CV-CUDA tensor +* :ref:`write_image() ` - Save CLAHE-enhanced image +* ``cuda_memcpy_d2h`` - Download CLAHE result to host for grayscale-to-RGB replication +* ``cuda_memcpy_h2d`` - Upload the replicated RGB array back to a CVCUDA tensor + +See Also +-------- + +* :ref:`Resize Operator ` - Simple spatial transformation example +* :ref:`Common Utilities ` - Helper functions used across samples diff --git a/docs/sphinx/samples/operators/color_twist.rst b/docs/sphinx/samples/operators/color_twist.rst new file mode 100644 index 000000000..267c74c05 --- /dev/null +++ b/docs/sphinx/samples/operators/color_twist.rst @@ -0,0 +1,136 @@ +.. + # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-License-Identifier: Apache-2.0 + # + # Licensed 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. + +.. _sample_color_twist: + +Color Twist +=========== + +Overview +-------- + +The Color Twist sample demonstrates per-channel affine color transformation using CV-CUDA's +GPU-accelerated ``color_twist`` operator. A 3×4 float matrix defines how each output channel is +computed as a linear combination of the input channels plus a bias, enabling operations such as +saturation adjustments, color temperature shifts, sepia toning, and general channel mixing. + +Usage +----- + +Basic Usage +^^^^^^^^^^^ + +Apply the default warm-tint color twist to an image: + +.. code-block:: bash + + python3 color_twist.py -i input.jpg + +Custom Output Path +^^^^^^^^^^^^^^^^^^ + +Specify a custom output path: + +.. code-block:: bash + + python3 color_twist.py -i input.jpg -o cat_color_twist.jpg + +Command-Line Arguments +---------------------- + +.. list-table:: + :header-rows: 1 + :widths: 20 15 15 50 + + * - Argument + - Short Form + - Default + - Description + * - ``--input`` + - ``-i`` + - tabby_tiger_cat.jpg + - Input image file path + * - ``--output`` + - ``-o`` + - cvcuda/.cache/cat_color_twist.jpg + - Output image file path + +Implementation +-------------- + +Color Twist Transform +^^^^^^^^^^^^^^^^^^^^^ + +.. literalinclude:: ../../../../samples/operators/color_twist.py + :language: python + :start-after: docs_tag: begin_color_twist + :end-before: docs_tag: end_color_twist + :dedent: + +Key points: + +1. **Twist matrix layout**: The twist tensor has shape ``(3, 4)`` with ``"HW"`` layout. Row ``i`` + defines the output for channel ``i`` as ``twist[i,0]*R + twist[i,1]*G + twist[i,2]*B + twist[i,3]``. +2. **Offset column**: The fourth column acts as a per-channel bias (brightness shift), allowing + independent control of each channel's black point. +3. **Automatic clipping**: The operator clips results back into the source dtype's representable + range, so no explicit clamping is needed. +4. **Batch support**: Pass an ``NHWC`` tensor or an ``ImageBatchVarShape`` to process a whole batch + in one GPU kernel launch. + +Expected Output +^^^^^^^^^^^^^^^ + +The output shows the image with a warm golden-hour tint (red boosted, blue slightly reduced): + +.. list-table:: + :widths: 50 50 + :align: center + + * - .. figure:: ../../content/tabby_tiger_cat.jpg + :width: 100% + + Original Input Image + + - .. figure:: ../../content/cat_color_twist.jpg + :width: 100% + + Output: Warm Color Twist Applied + +CV-CUDA Operators Used +^^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Operator + - Purpose + * - :py:func:`cvcuda.color_twist` + - Apply a 3×4 per-channel affine color transform to every pixel + +Common Utilities Used +^^^^^^^^^^^^^^^^^^^^^ + +* :ref:`read_image() ` - Load image as CV-CUDA tensor +* :ref:`write_image() ` - Save color-twisted image +* ``cuda_memcpy_h2d`` - Upload the twist matrix from host NumPy array to device tensor + +See Also +-------- + +* :ref:`Resize Operator ` - GPU-accelerated image resizing +* :ref:`Common Utilities ` - Helper functions used by all samples diff --git a/docs/sphinx/samples/operators/composite.rst b/docs/sphinx/samples/operators/composite.rst new file mode 100644 index 000000000..81bceb2b2 --- /dev/null +++ b/docs/sphinx/samples/operators/composite.rst @@ -0,0 +1,137 @@ +.. + # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-License-Identifier: Apache-2.0 + # + # Licensed 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. + +.. _sample_composite: + +Composite +========= + +Overview +-------- + +The Composite sample demonstrates GPU-accelerated image compositing with CV-CUDA. +It blends a foreground image (tabby cat) over a background image (Weimaraner dog) +using a single-channel alpha mask. A filled circle in the centre of the frame +keeps the cat visible while the dog shows through outside the circle, making the +blend immediately obvious in the output image. + +Usage +----- + +Basic Usage +^^^^^^^^^^^ + +Composite the default cat foreground over the Weimaraner background: + +.. code-block:: bash + + python3 composite.py -i input.jpg + +Custom Input +^^^^^^^^^^^^ + +Supply your own foreground image and redirect the output: + +.. code-block:: bash + + python3 composite.py -i image.jpg -o cat_composite.jpg + +Command-Line Arguments +---------------------- + +.. list-table:: + :header-rows: 1 + :widths: 20 15 15 50 + + * - Argument + - Short Form + - Default + - Description + * - ``--input`` + - ``-i`` + - tabby_tiger_cat.jpg + - Foreground input image file path + * - ``--output`` + - ``-o`` + - cvcuda/.cache/cat_composite.jpg + - Output composited image file path + +Implementation +-------------- + +Composite Operation +^^^^^^^^^^^^^^^^^^^ + +.. literalinclude:: ../../../../samples/operators/composite.py + :language: python + :start-after: docs_tag: begin_composite + :end-before: docs_tag: end_composite + :dedent: + +Key points: + +1. **Mask shape**: The foreground mask must be single-channel (``HWC`` with ``C=1``), uint8. +2. **Spatial alignment**: Foreground, background, and mask must share the same ``(H, W)`` dimensions; the background is resized to match the foreground before compositing. +3. **outchannels parameter**: Pass ``3`` for an RGB output tensor or ``4`` for RGBA. +4. **Mask semantics**: Pixel values ``> 0`` select the foreground; ``0`` selects the background — effectively a hard binary blend. +5. **GPU upload**: The mask is constructed on the CPU with NumPy then transferred to the GPU via ``cuda_memcpy_h2d`` before the operator call. + +Expected Output +^^^^^^^^^^^^^^^ + +The composited image shows the cat inside a circular region with the Weimaraner +dog visible outside it: + +.. list-table:: + :widths: 50 50 + :align: center + + * - .. figure:: ../../content/tabby_tiger_cat.jpg + :width: 100% + + Original Input Image (foreground) + + - .. figure:: ../../content/cat_composite.jpg + :width: 100% + + Output: Cat composited over Weimaraner + +CV-CUDA Operators Used +^^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Operator + - Purpose + * - :py:func:`cvcuda.composite` + - Blend foreground and background images using an alpha mask + * - :py:func:`cvcuda.resize` + - Resize the background to match foreground spatial dimensions + +Common Utilities Used +^^^^^^^^^^^^^^^^^^^^^ + +* :ref:`read_image() ` - Load images as CV-CUDA tensors +* :ref:`write_image() ` - Save the composited image +* :ref:`cuda_memcpy_h2d() ` - Upload the NumPy mask to the GPU + +See Also +-------- + +* :ref:`Resize Operator ` - Resize images to target dimensions +* :ref:`Common Utilities ` - Helper functions used across samples diff --git a/docs/sphinx/samples/operators/conv2d.rst b/docs/sphinx/samples/operators/conv2d.rst new file mode 100644 index 000000000..fbc3e3538 --- /dev/null +++ b/docs/sphinx/samples/operators/conv2d.rst @@ -0,0 +1,144 @@ +.. + # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-License-Identifier: Apache-2.0 + # + # Licensed 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. + +.. _sample_conv2d: + +Conv2D +====== + +Overview +-------- + +The Conv2D sample demonstrates GPU-accelerated 2-D convolution using CV-CUDA's +``conv2d`` operator. The sample wraps a single RGB image in an +``ImageBatchVarShape``, constructs a 3×3 sharpening kernel as a float +``ImageBatchVarShape``, and runs the convolution on the GPU. Per-image kernel +anchors are supplied via a small ``Tensor`` of shape ``(N, 2)``. + +Usage +----- + +Basic Usage +^^^^^^^^^^^ + +Apply the default sharpening filter to an image: + +.. code-block:: bash + + python3 conv2d.py -i input.jpg + +Custom Input and Output +^^^^^^^^^^^^^^^^^^^^^^^ + +Specify both input and output paths: + +.. code-block:: bash + + python3 conv2d.py -i image.jpg -o cat_conv2d.jpg + +Command-Line Arguments +---------------------- + +.. list-table:: + :header-rows: 1 + :widths: 20 15 15 50 + + * - Argument + - Short Form + - Default + - Description + * - ``--input`` + - ``-i`` + - tabby_tiger_cat.jpg + - Input image file path + * - ``--output`` + - ``-o`` + - cvcuda/.cache/cat_conv2d.jpg + - Output image file path + +Implementation +-------------- + +Batch and Kernel Setup +^^^^^^^^^^^^^^^^^^^^^^ + +.. literalinclude:: ../../../../samples/operators/conv2d.py + :language: python + :start-after: docs_tag: begin_conv2d_setup + :end-before: docs_tag: end_conv2d_setup + :dedent: + +Conv2D Operator Call +^^^^^^^^^^^^^^^^^^^^ + +.. literalinclude:: ../../../../samples/operators/conv2d.py + :language: python + :start-after: docs_tag: begin_conv2d + :end-before: docs_tag: end_conv2d + :dedent: + +Key points: + +1. **ImageBatchVarShape input**: ``conv2d`` requires the source image(s) wrapped in an ``ImageBatchVarShape``; individual ``Tensor`` objects must be converted via ``cvcuda.as_image`` first. +2. **Float kernel**: The convolution kernel must use ``cvcuda.Format.F32``; integer kernels are not supported. +3. **Kernel anchor**: A ``Tensor`` of shape ``(N, 2)`` with layout ``"NC"`` provides the ``(x, y)`` anchor for each image; ``(-1, -1)`` selects the kernel centre automatically. +4. **Border mode**: ``REFLECT101`` avoids the dark halo at image edges that ``CONSTANT`` (zero) padding produces when sharpening. +5. **Result extraction**: The output ``ImageBatchVarShape`` is iterated to retrieve each result ``Image``, which is then wrapped back into an HWC ``Tensor`` for saving. + +Expected Output +^^^^^^^^^^^^^^^ + +The output image shows the input with edges and fine detail enhanced by the +sharpening kernel: + +.. list-table:: + :widths: 50 50 + :align: center + + * - .. figure:: ../../content/tabby_tiger_cat.jpg + :width: 100% + + Original Input Image + + - .. figure:: ../../content/cat_conv2d.jpg + :width: 100% + + Output: Sharpened with 3×3 kernel + +CV-CUDA Operators Used +^^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Operator + - Purpose + * - :py:func:`cvcuda.conv2d` + - Apply a per-image 2-D convolution kernel over an ``ImageBatchVarShape`` + +Common Utilities Used +^^^^^^^^^^^^^^^^^^^^^ + +* :ref:`read_image() ` - Load image as CV-CUDA tensor +* :ref:`write_image() ` - Save convolved image +* ``cuda_memcpy_h2d`` - Upload NumPy kernel weights and anchor coordinates to the GPU + +See Also +-------- + +* :ref:`Resize Operator ` - Another spatial image operator +* :ref:`Common Utilities ` - Helper functions used by all operator samples diff --git a/docs/sphinx/samples/operators/convertto.rst b/docs/sphinx/samples/operators/convertto.rst new file mode 100644 index 000000000..3023a328e --- /dev/null +++ b/docs/sphinx/samples/operators/convertto.rst @@ -0,0 +1,137 @@ +.. + # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-License-Identifier: Apache-2.0 + # + # Licensed 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. + +.. _sample_convertto: + +Convert To +========== + +Overview +-------- + +The Convert To sample demonstrates dtype conversion using CV-CUDA's GPU-accelerated +``convertto`` operator. It converts a ``uint8`` image to ``float32`` with a scale factor +of ``1/255`` (normalising pixel values to ``[0, 1]``), then converts the result back to +``uint8`` by applying the inverse scale of ``255``. This round-trip is a fundamental +pre/post-processing step for deep-learning inference pipelines. + +Usage +----- + +Basic Usage +^^^^^^^^^^^ + +Convert the default tabby cat image: + +.. code-block:: bash + + python3 convertto.py + +Custom Input/Output +^^^^^^^^^^^^^^^^^^^ + +Specify explicit input and output paths: + +.. code-block:: bash + + python3 convertto.py -i image.jpg -o cat_convertto.jpg + +Command-Line Arguments +---------------------- + +.. list-table:: + :header-rows: 1 + :widths: 20 15 15 50 + + * - Argument + - Short Form + - Default + - Description + * - ``--input`` + - ``-i`` + - tabby_tiger_cat.jpg + - Input image file path + * - ``--output`` + - ``-o`` + - cvcuda/.cache/cat_convertto.jpg + - Output image file path + +Implementation +-------------- + +Convert To Operator +^^^^^^^^^^^^^^^^^^^ + +.. literalinclude:: ../../../../samples/operators/convertto.py + :language: python + :start-after: docs_tag: begin_convertto + :end-before: docs_tag: end_convertto + :dedent: + +Key points: + +1. **dtype parameter**: Pass a ``numpy`` dtype (e.g. ``np.float32``) or a ``cvcuda.Type`` + enum value — both are accepted by ``cvcuda.convertto``. +2. **scale parameter**: Each output pixel is computed as + ``out = src * scale + offset``. Omitting ``scale`` defaults to ``1.0``. +3. **offset parameter**: An optional additive bias applied after scaling; defaults to + ``0.0`` when omitted. +4. **Layout preservation**: The output tensor always has the same layout (HWC, NHWC, + CHW, NCHW) as the input tensor. + +Expected Output +^^^^^^^^^^^^^^^ + +The output image is visually identical to the input because the uint8→float32→uint8 +round-trip is lossless for pixel values in ``[0, 255]``: + +.. list-table:: + :widths: 50 50 + :align: center + + * - .. figure:: ../../content/tabby_tiger_cat.jpg + :width: 100% + + Original Input Image + + - .. figure:: ../../content/cat_convertto.jpg + :width: 100% + + Output: uint8 round-trip via float32 + +CV-CUDA Operators Used +^^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Operator + - Purpose + * - :py:func:`cvcuda.convertto` + - Convert tensor dtype with optional scale and offset + +Common Utilities Used +^^^^^^^^^^^^^^^^^^^^^ + +* :ref:`read_image() ` - Load image as CV-CUDA tensor +* :ref:`write_image() ` - Save converted image + +See Also +-------- + +* :ref:`Resize Operator ` - GPU-accelerated image resizing +* :ref:`Common Utilities ` - Helper functions used by all samples diff --git a/docs/sphinx/samples/operators/copymakeborder.rst b/docs/sphinx/samples/operators/copymakeborder.rst new file mode 100644 index 000000000..25078f10f --- /dev/null +++ b/docs/sphinx/samples/operators/copymakeborder.rst @@ -0,0 +1,138 @@ +.. + # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-License-Identifier: Apache-2.0 + # + # Licensed 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. + +.. _sample_copymakeborder: + +Copy Make Border +================ + +Overview +-------- + +The Copy Make Border sample demonstrates how to pad an image with a border of configurable +width and fill style using CV-CUDA's GPU-accelerated ``copymakeborder`` operator. The +operator supports multiple border modes (``CONSTANT``, ``REPLICATE``, ``REFLECT``, +``REFLECT101``, ``WRAP``) and operates directly on device tensors, making it well-suited +for preprocessing pipelines that need to pad images before inference. + +Usage +----- + +Basic Usage +^^^^^^^^^^^ + +Add an orange border to the default cat image: + +.. code-block:: bash + + python3 copymakeborder.py + +Custom Output Path +^^^^^^^^^^^^^^^^^^ + +Specify a custom output file: + +.. code-block:: bash + + python3 copymakeborder.py -i input.jpg -o cat_copymakeborder.jpg + +Command-Line Arguments +---------------------- + +.. list-table:: + :header-rows: 1 + :widths: 20 15 15 50 + + * - Argument + - Short Form + - Default + - Description + * - ``--input`` + - ``-i`` + - tabby_tiger_cat.jpg + - Input image file path + * - ``--output`` + - ``-o`` + - cvcuda/.cache/cat_copymakeborder.jpg + - Output image file path + +Implementation +-------------- + +Adding a Colored Border +^^^^^^^^^^^^^^^^^^^^^^^ + +.. literalinclude:: ../../../../samples/operators/copymakeborder.py + :language: python + :start-after: docs_tag: begin_copymakeborder + :end-before: docs_tag: end_copymakeborder + :dedent: + +Key points: + +1. **Border mode**: ``cvcuda.Border.CONSTANT`` fills the added region with a fixed color; + other modes (``REPLICATE``, ``REFLECT``, ``REFLECT101``, ``WRAP``) derive fill values + from existing image pixels instead. +2. **Border value**: A three-element list ``[R, G, B]`` supplies the fill color for + ``CONSTANT`` mode; it is silently ignored for the other modes. +3. **Output shape**: The output tensor is automatically sized to + ``(H + top + bottom, W + left + right, C)``; no pre-allocation is required. +4. **Asymmetric padding**: ``top``, ``bottom``, ``left``, and ``right`` are independent + integers, enabling padding that differs on each side — useful for letterboxing. + +Expected Output +^^^^^^^^^^^^^^^ + +The output shows the original image surrounded by a 30-pixel vertical and 60-pixel +horizontal orange border: + +.. list-table:: + :widths: 50 50 + :align: center + + * - .. figure:: ../../content/tabby_tiger_cat.jpg + :width: 100% + + Original Input Image + + - .. figure:: ../../content/cat_copymakeborder.jpg + :width: 100% + + Output: Image with orange border padding + +CV-CUDA Operators Used +^^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Operator + - Purpose + * - :py:func:`cvcuda.copymakeborder` + - Pad an image with a configurable border width and fill mode + +Common Utilities Used +^^^^^^^^^^^^^^^^^^^^^ + +* :ref:`read_image() ` - Load image as CV-CUDA tensor +* :ref:`write_image() ` - Save bordered image + +See Also +-------- + +* :ref:`Resize Operator ` - Resize images to target dimensions +* :ref:`Common Utilities ` - Helper functions used across samples diff --git a/docs/sphinx/samples/operators/crop_flip_normalize_reformat.rst b/docs/sphinx/samples/operators/crop_flip_normalize_reformat.rst new file mode 100644 index 000000000..b0895bc0e --- /dev/null +++ b/docs/sphinx/samples/operators/crop_flip_normalize_reformat.rst @@ -0,0 +1,147 @@ +.. + # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-License-Identifier: Apache-2.0 + # + # Licensed 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. + +.. _sample_crop_flip_normalize_reformat: + +Crop Flip Normalize Reformat +============================ + +Overview +-------- + +The Crop Flip Normalize Reformat sample demonstrates how to combine four common image +preprocessing steps — spatial cropping, horizontal or vertical flipping, per-channel +normalization, and layout reformatting (HWC → NCHW) — into a single GPU kernel call +using CV-CUDA's ``crop_flip_normalize_reformat`` operator. This fused pipeline is +typical in deep-learning inference pipelines where images must be cropped to a region +of interest, augmented with a flip, and then normalized and reformatted before being +fed to a model. + +Usage +----- + +Basic Usage +^^^^^^^^^^^ + +Run the pipeline on the default tabby-cat image: + +.. code-block:: bash + + python3 crop_flip_normalize_reformat.py -i input.jpg + +Custom Input / Output +^^^^^^^^^^^^^^^^^^^^^ + +Specify your own input image and save the result to a custom path: + +.. code-block:: bash + + python3 crop_flip_normalize_reformat.py -i image.jpg -o cat_crop_flip_normalize_reformat.jpg + +Command-Line Arguments +---------------------- + +.. list-table:: + :header-rows: 1 + :widths: 20 15 15 50 + + * - Argument + - Short Form + - Default + - Description + * - ``--input`` + - ``-i`` + - tabby_tiger_cat.jpg + - Input image file path + * - ``--output`` + - ``-o`` + - cvcuda/.cache/cat_crop_flip_normalize_reformat.jpg + - Output image file path + +Implementation +-------------- + +Crop, Flip, Normalize, and Reformat +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. literalinclude:: ../../../../samples/operators/crop_flip_normalize_reformat.py + :language: python + :start-after: docs_tag: begin_crop_flip_normalize_reformat + :end-before: docs_tag: end_crop_flip_normalize_reformat + :dedent: + +Key points: + +1. **Fused kernel**: All four operations (crop, flip, normalize, reformat) run in a + single GPU kernel, avoiding intermediate allocations and memory-bandwidth overhead. +2. **ImageBatchVarShape input**: The operator requires images wrapped in a + :py:class:`cvcuda.ImageBatchVarShape`, which supports batches of varying-size images. +3. **Crop rectangle tensor**: The ``rect`` tensor has shape ``[N, 1, 1, 4]`` with + ``[crop_x, crop_y, crop_width, crop_height]`` stored per image in the last dimension. +4. **SCALE_IS_STDDEV flag**: When :py:data:`cvcuda.NormalizeFlags.SCALE_IS_STDDEV` is + set the ``scale`` argument is interpreted as per-channel standard deviation, matching + the common ``(pixel/255 - mean) / std`` convention used by PyTorch models. +5. **NCHW output layout**: Passing ``out_layout="NCHW"`` reformats the data from the + interleaved HWC format that the camera/decoder produces into the planar CHW format + expected by most deep-learning frameworks — no separate ``reformat`` call is required. + +Expected Output +^^^^^^^^^^^^^^^ + +The output shows the central 80% of the image, flipped horizontally, with pixel values +de-normalized back to uint8 for display: + +.. list-table:: + :widths: 50 50 + :align: center + + * - .. figure:: ../../content/tabby_tiger_cat.jpg + :width: 100% + + Original Input Image + + - .. figure:: ../../content/cat_crop_flip_normalize_reformat.jpg + :width: 100% + + Output: Cropped, Flipped, Normalized (visualized as uint8) + +CV-CUDA Operators Used +^^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Operator + - Purpose + * - :py:func:`cvcuda.crop_flip_normalize_reformat` + - Crop a region of interest, optionally flip, normalize per channel, and reformat + the layout in a single fused GPU kernel + +Common Utilities Used +^^^^^^^^^^^^^^^^^^^^^ + +* :ref:`read_image() ` - Load image as CV-CUDA tensor +* :ref:`write_image() ` - Save the result image +* ``cuda_memcpy_h2d`` - Upload crop-rect, flip-code, and normalization parameter arrays to GPU +* ``cuda_memcpy_d2h`` - Download the float32 result to CPU for inverse-normalization + +See Also +-------- + +* :ref:`Resize Operator ` - Simple spatial resize +* :ref:`Normalize Operator ` - Standalone per-channel normalization +* :ref:`Common Utilities ` - Helper functions diff --git a/docs/sphinx/samples/operators/customcrop.rst b/docs/sphinx/samples/operators/customcrop.rst new file mode 100644 index 000000000..69843fa98 --- /dev/null +++ b/docs/sphinx/samples/operators/customcrop.rst @@ -0,0 +1,142 @@ +.. + # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-License-Identifier: Apache-2.0 + # + # Licensed 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. + +.. _sample_customcrop: + +Custom Crop +=========== + +Overview +-------- + +The Custom Crop sample demonstrates how to extract an off-center rectangular sub-region +from an image using CV-CUDA's GPU-accelerated custom crop operator. +The crop region is expressed as a :py:class:`cvcuda.RectI` (x, y, width, height) in +input-image pixel coordinates, making it straightforward to implement any region-of-interest +extraction pipeline. + +Usage +----- + +Basic Usage +^^^^^^^^^^^ + +Crop the default input image with an automatically computed off-center rectangle: + +.. code-block:: bash + + python3 customcrop.py + +Custom Input and Output +^^^^^^^^^^^^^^^^^^^^^^^ + +Specify a custom input file and output path: + +.. code-block:: bash + + python3 customcrop.py -i input.jpg -o cat_customcrop.jpg + +Command-Line Arguments +---------------------- + +.. list-table:: + :header-rows: 1 + :widths: 20 15 15 50 + + * - Argument + - Short Form + - Default + - Description + * - ``--input`` + - ``-i`` + - tabby_tiger_cat.jpg + - Input image file path + * - ``--output`` + - ``-o`` + - cvcuda/.cache/cat_customcrop.jpg + - Output image file path + +Implementation +-------------- + +Crop Region Setup +^^^^^^^^^^^^^^^^^ + +.. literalinclude:: ../../../../samples/operators/customcrop.py + :language: python + :start-after: docs_tag: begin_customcrop_setup + :end-before: docs_tag: end_customcrop_setup + :dedent: + +Applying the Custom Crop +^^^^^^^^^^^^^^^^^^^^^^^^ + +.. literalinclude:: ../../../../samples/operators/customcrop.py + :language: python + :start-after: docs_tag: begin_customcrop + :end-before: docs_tag: end_customcrop + :dedent: + +Key points: + +1. **RectI coordinates**: ``x`` and ``y`` are the top-left corner of the crop window in the input image; ``width`` and ``height`` define the output dimensions. +2. **Off-center crop**: Choosing ``x = img_w // 5`` and ``y = img_h // 5`` deliberately avoids a centered crop, which is typical for ROI extraction use cases. +3. **Output shape**: The output tensor shape is ``(crop_h, crop_w, channels)`` for an HWC input, matching exactly the rectangle dimensions. +4. **HWC layout preserved**: The operator preserves the input layout (HWC or NHWC), so the result can be passed directly to downstream ops or written with ``write_image``. +5. **Stream support**: An optional ``stream`` keyword argument enables asynchronous execution on a specific CUDA stream. + +Expected Output +^^^^^^^^^^^^^^^ + +The output shows the central 60% of the image, shifted 20% from the top-left corner: + +.. list-table:: + :widths: 50 50 + :align: center + + * - .. figure:: ../../content/tabby_tiger_cat.jpg + :width: 100% + + Original Input Image + + - .. figure:: ../../content/cat_customcrop.jpg + :width: 100% + + Output: Off-center Cropped Region + +CV-CUDA Operators Used +^^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Operator + - Purpose + * - :py:func:`cvcuda.customcrop` + - Extract a rectangular region-of-interest from the input tensor + +Common Utilities Used +^^^^^^^^^^^^^^^^^^^^^ + +* :ref:`read_image() ` - Load image as CV-CUDA tensor +* :ref:`write_image() ` - Save cropped image + +See Also +-------- + +* :ref:`Resize Operator ` - Resize images to target dimensions +* :ref:`Common Utilities ` - Helper functions diff --git a/docs/sphinx/samples/operators/cvtcolor.rst b/docs/sphinx/samples/operators/cvtcolor.rst new file mode 100644 index 000000000..278d54ef6 --- /dev/null +++ b/docs/sphinx/samples/operators/cvtcolor.rst @@ -0,0 +1,140 @@ +.. + # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-License-Identifier: Apache-2.0 + # + # Licensed 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. + +.. _sample_cvtcolor: + +Color Conversion +================ + +Overview +-------- + +The Color Conversion sample demonstrates GPU-accelerated color space conversion using CV-CUDA's +``cvtcolor`` operator. The example reads an input image, converts it from RGB to BGR by swapping +the red and blue channels, and writes the result as a viewable uint8 JPEG. The same operator +supports a wide range of conversions including grayscale, RGBA, HSV, and YUV formats — only the +``code`` argument needs to change. + +Usage +----- + +Basic Usage +^^^^^^^^^^^ + +Convert the default tabby-cat image (RGB to BGR): + +.. code-block:: bash + + python3 cvtcolor.py + +Custom Input and Output +^^^^^^^^^^^^^^^^^^^^^^^ + +Specify input and output paths explicitly: + +.. code-block:: bash + + python3 cvtcolor.py -i input.jpg -o cat_cvtcolor.jpg + +Command-Line Arguments +---------------------- + +.. list-table:: + :header-rows: 1 + :widths: 20 15 15 50 + + * - Argument + - Short Form + - Default + - Description + * - ``--input`` + - ``-i`` + - tabby_tiger_cat.jpg + - Input image file path + * - ``--output`` + - ``-o`` + - cvcuda/.cache/cat_cvtcolor.jpg + - Output image file path + +Implementation +-------------- + +Color Space Conversion +^^^^^^^^^^^^^^^^^^^^^^ + +.. literalinclude:: ../../../../samples/operators/cvtcolor.py + :language: python + :start-after: docs_tag: begin_cvtcolor + :end-before: docs_tag: end_cvtcolor + :dedent: + +Key points: + +1. **Batched input**: ``cvcuda.cvtcolor`` requires an NHWC tensor; a single HWC image is + promoted to a batch of one with ``cvcuda.stack``. +2. **ColorConversion enum**: The desired conversion is selected by passing a + :pydata:`cvcuda.ColorConversion` member as the ``code`` keyword argument. +3. **Symmetric channel counts**: The source and destination channel counts must match the + chosen conversion code (e.g. RGB2BGR keeps 3 channels; BGR2GRAY reduces to 1). +4. **Batch dimension removal**: After conversion the leading batch dimension is dropped with + ``Tensor.reshape`` so the result is a plain HWC tensor that ``write_image`` can encode + directly as JPEG. +5. **Supported dtypes**: The operator accepts ``uint8`` and ``uint16`` inputs; the default + JPEG pipeline uses ``uint8``. + +Expected Output +^^^^^^^^^^^^^^^ + +The output shows the input image with red and blue channels exchanged. Warm-toned areas +(e.g. orange fur) appear cooler and vice versa: + +.. list-table:: + :widths: 50 50 + :align: center + + * - .. figure:: ../../content/tabby_tiger_cat.jpg + :width: 100% + + Original Input Image + + - .. figure:: ../../content/cat_cvtcolor.jpg + :width: 100% + + Output: RGB channels converted to BGR + +CV-CUDA Operators Used +^^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Operator + - Purpose + * - :py:func:`cvcuda.cvtcolor` + - Convert image between color spaces using a GPU-accelerated kernel + +Common Utilities Used +^^^^^^^^^^^^^^^^^^^^^ + +* :ref:`read_image() ` - Load image as CV-CUDA tensor +* :ref:`write_image() ` - Save color-converted image + +See Also +-------- + +* :ref:`Resize Operator ` - Basic single-image operator example +* :ref:`Common Utilities ` - Helper functions used across samples diff --git a/docs/sphinx/samples/operators/erase.rst b/docs/sphinx/samples/operators/erase.rst new file mode 100644 index 000000000..102889a01 --- /dev/null +++ b/docs/sphinx/samples/operators/erase.rst @@ -0,0 +1,152 @@ +.. + # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-License-Identifier: Apache-2.0 + # + # Licensed 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. + +.. _sample_erase: + +Erase +===== + +Overview +-------- + +The Erase sample demonstrates how to fill one or more rectangular regions of an image with +solid colours using CV-CUDA's GPU-accelerated erase operator. Six rectangular regions +(red, green, blue, white, and black solid fills plus one green-channel-only tint) are +stamped onto the image entirely on the GPU — no round-trip to the CPU is needed for the +pixel data. + +Usage +----- + +Basic Usage +^^^^^^^^^^^ + +Erase six rectangular regions into the default tabby-cat image: + +.. code-block:: bash + + python3 erase.py -i input.jpg + +Custom Output Path +^^^^^^^^^^^^^^^^^^ + +Save the result to a specific file: + +.. code-block:: bash + + python3 erase.py -i input.jpg -o erased.jpg + +Command-Line Arguments +---------------------- + +.. list-table:: + :header-rows: 1 + :widths: 20 15 15 50 + + * - Argument + - Short Form + - Default + - Description + * - ``--input`` + - ``-i`` + - tabby_tiger_cat.jpg + - Input image file path + * - ``--output`` + - ``-o`` + - cvcuda/.cache/cat_erase.jpg + - Output image file path + +Implementation +-------------- + +Parameter Tensor Setup +^^^^^^^^^^^^^^^^^^^^^^ + +.. literalinclude:: ../../../../samples/operators/erase.py + :language: python + :start-after: docs_tag: begin_erase_setup + :end-before: docs_tag: end_erase_setup + :dedent: + +Erase Call +^^^^^^^^^^ + +.. literalinclude:: ../../../../samples/operators/erase.py + :language: python + :start-after: docs_tag: begin_erase + :end-before: docs_tag: end_erase + :dedent: + +Key points: + +1. **Parameter tensors**: ``anchor``, ``erasing``, ``values``, and ``imgIdx`` are small + 1-D tensors built from NumPy arrays and uploaded to the GPU with ``cuda_memcpy_h2d``. +2. **Batch dimension**: The operator expects NHWC input, so a single HWC image is wrapped + in a batch of size 1 via ``reshape``. +3. **anchor** holds ``(x, y)`` pixel coordinates of each rectangle's top-left corner + (type ``_2S32`` — a pair of int32 per element). +4. **erasing** holds ``(width, height, flag)`` per rectangle; ``flag`` is a channel + bitmask (bit0=R, bit1=G, bit2=B) selecting which channels are overwritten by the + ``values`` fill. ``flag=7`` (``0b111``) replaces all three channels for a solid fill, + while ``flag=2`` (``0b010``) replaces only the green channel, leaving R and B intact + for a tint. +5. **random mode**: Setting ``random=True`` ignores ``values`` and fills each rectangle + with deterministic pseudo-random noise controlled by ``seed``. + +Expected Output +^^^^^^^^^^^^^^^ + +The output image is identical to the input except for six erased regions — red, green, +blue, white, and black solid rectangles plus one green-channel-only tint: + +.. list-table:: + :widths: 50 50 + :align: center + + * - .. figure:: ../../content/tabby_tiger_cat.jpg + :width: 100% + + Original Input Image + + - .. figure:: ../../content/cat_erase.jpg + :width: 100% + + Output: Six rectangular regions erased from the image + +CV-CUDA Operators Used +^^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Operator + - Purpose + * - :py:func:`cvcuda.erase` + - Fill rectangular regions with solid colours or pseudo-random noise + +Common Utilities Used +^^^^^^^^^^^^^^^^^^^^^ + +* :ref:`read_image() ` - Load image as CV-CUDA tensor +* :ref:`write_image() ` - Save erased image +* ``cuda_memcpy_h2d`` - Upload NumPy parameter arrays to the GPU + +See Also +-------- + +* :ref:`Resize Operator ` - Basic single-operator sample structure +* :ref:`Common Utilities ` - Helper functions diff --git a/docs/sphinx/samples/operators/flip.rst b/docs/sphinx/samples/operators/flip.rst new file mode 100644 index 000000000..cecf20282 --- /dev/null +++ b/docs/sphinx/samples/operators/flip.rst @@ -0,0 +1,129 @@ +.. + # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-License-Identifier: Apache-2.0 + # + # Licensed 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. + +.. _sample_flip: + +Flip +==== + +Overview +-------- + +The Flip sample demonstrates GPU-accelerated image flipping using CV-CUDA's flip operator. +The operator mirrors an image along one or both axes: horizontal (left-right), vertical +(top-bottom), or both simultaneously. + +Usage +----- + +Basic Usage +^^^^^^^^^^^ + +Flip the default input image horizontally (left-right mirror): + +.. code-block:: bash + + python3 flip.py -i input.jpg + +Custom Output Path +^^^^^^^^^^^^^^^^^^ + +Write the flipped result to a custom path: + +.. code-block:: bash + + python3 flip.py -i input.jpg -o cat_flip.jpg + +Command-Line Arguments +---------------------- + +.. list-table:: + :header-rows: 1 + :widths: 20 15 15 50 + + * - Argument + - Short Form + - Default + - Description + * - ``--input`` + - ``-i`` + - tabby_tiger_cat.jpg + - Input image file path + * - ``--output`` + - ``-o`` + - cvcuda/.cache/cat_flip.jpg + - Output image file path + +Implementation +-------------- + +Horizontal Flip +^^^^^^^^^^^^^^^ + +.. literalinclude:: ../../../../samples/operators/flip.py + :language: python + :start-after: docs_tag: begin_flip + :end-before: docs_tag: end_flip + :dedent: + +Key points: + +1. **flipCode=1** mirrors the image left-right (horizontal flip). +2. **flipCode=0** mirrors the image top-to-bottom (vertical flip). +3. **flipCode=-1** mirrors the image along both axes simultaneously. + +Expected Output +^^^^^^^^^^^^^^^ + +The output is a mirror image of the input flipped left-right: + +.. list-table:: + :widths: 50 50 + :align: center + + * - .. figure:: ../../content/tabby_tiger_cat.jpg + :width: 100% + + Original Input Image + + - .. figure:: ../../content/cat_flip.jpg + :width: 100% + + Output: Horizontally Flipped Image + +CV-CUDA Operators Used +^^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Operator + - Purpose + * - :py:func:`cvcuda.flip` + - Mirror an image along one or both spatial axes + +Common Utilities Used +^^^^^^^^^^^^^^^^^^^^^ + +* :ref:`read_image() ` - Load image as CV-CUDA tensor +* :ref:`write_image() ` - Save flipped image + +See Also +-------- + +* :ref:`Resize Operator ` - Resize images with CV-CUDA +* :ref:`Common Utilities ` - Helper functions diff --git a/docs/sphinx/samples/operators/gamma_contrast.rst b/docs/sphinx/samples/operators/gamma_contrast.rst new file mode 100644 index 000000000..2ee3817b7 --- /dev/null +++ b/docs/sphinx/samples/operators/gamma_contrast.rst @@ -0,0 +1,143 @@ +.. + # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-License-Identifier: Apache-2.0 + # + # Licensed 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. + +.. _sample_gamma_contrast: + +Gamma Contrast +============== + +Overview +-------- + +The Gamma Contrast sample demonstrates how to apply per-image gamma correction using CV-CUDA's +GPU-accelerated ``gamma_contrast`` operator. Gamma correction maps each normalised pixel value +``p`` to ``p^gamma``, which is widely used to match display transfer functions (e.g. the sRGB +standard uses gamma ≈ 2.2) or to adjust the perceptual brightness of an image. + +Usage +----- + +Basic Usage +^^^^^^^^^^^ + +Apply standard sRGB gamma correction (gamma = 2.2) to an image: + +.. code-block:: bash + + python3 gamma_contrast.py -i input.jpg + +Custom Output Path +^^^^^^^^^^^^^^^^^^ + +Specify a custom output file: + +.. code-block:: bash + + python3 gamma_contrast.py -i input.jpg -o my_gamma_output.jpg + +Command-Line Arguments +---------------------- + +.. list-table:: + :header-rows: 1 + :widths: 20 15 15 50 + + * - Argument + - Short Form + - Default + - Description + * - ``--input`` + - ``-i`` + - tabby_tiger_cat.jpg + - Input image file path + * - ``--output`` + - ``-o`` + - cvcuda/.cache/cat_gamma_contrast.jpg + - Output image file path + +Implementation +-------------- + +Gamma Contrast Correction +^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. literalinclude:: ../../../../samples/operators/gamma_contrast.py + :language: python + :start-after: docs_tag: begin_gamma_contrast + :end-before: docs_tag: end_gamma_contrast + :dedent: + +Key points: + +1. **ImageBatchVarShape input**: This sample demonstrates the var-shape overload, where a + single-image tensor is wrapped with ``cvcuda.as_image`` and pushed into a + ``cvcuda.ImageBatchVarShape``. ``gamma_contrast`` also accepts plain ``cvcuda.Tensor`` + input/output, with either a per-sample gamma tensor or a host-scalar ``gamma``/``gain``. + The host-scalar overload accepts ``round=cvcuda.Round.NEAREST`` (the default) or + ``round=cvcuda.Round.TRUNCATE`` for integer outputs. +2. **Per-image gamma**: The gamma argument is a 1-D ``float32`` tensor with one value per image + in the batch, enabling different corrections per image in the same call. +3. **Standard gamma 2.2**: A value of 2.2 matches the sRGB display transfer function, darkening + mid-tones to compensate for how monitors render brightness non-linearly. +4. **In-place output extraction**: The output ``ImageBatchVarShape`` contains ``cvcuda.Image`` + objects; ``cvcuda.as_tensor`` converts the first image back to a writable HWC tensor with no + data copy. +5. **uint8 passthrough**: Because the input is already uint8 RGB8, the operator preserves that + dtype and the result can be written directly with ``write_image``. + +Expected Output +^^^^^^^^^^^^^^^ + +The output shows the image with gamma-corrected pixel intensities: + +.. list-table:: + :widths: 50 50 + :align: center + + * - .. figure:: ../../content/tabby_tiger_cat.jpg + :width: 100% + + Original Input Image + + - .. figure:: ../../content/cat_gamma_contrast.jpg + :width: 100% + + Output: Gamma-corrected (gamma = 2.2) + +CV-CUDA Operators Used +^^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Operator + - Purpose + * - :py:func:`cvcuda.gamma_contrast` + - Apply per-image power-law (gamma) contrast correction to an image batch + +Common Utilities Used +^^^^^^^^^^^^^^^^^^^^^ + +* :ref:`read_image() ` - Load image as CV-CUDA tensor +* :ref:`write_image() ` - Save gamma-corrected image +* ``cuda_memcpy_h2d`` - Upload the per-image gamma values to the GPU + +See Also +-------- + +* :ref:`Resize Operator ` - Basic GPU image resizing +* :ref:`Common Utilities ` - Helper functions diff --git a/docs/sphinx/samples/operators/gaussiannoise.rst b/docs/sphinx/samples/operators/gaussiannoise.rst new file mode 100644 index 000000000..d0d16a2e9 --- /dev/null +++ b/docs/sphinx/samples/operators/gaussiannoise.rst @@ -0,0 +1,133 @@ +.. + # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-License-Identifier: Apache-2.0 + # + # Licensed 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. + +.. _sample_gaussiannoise: + +Gaussian Noise +============== + +Overview +-------- + +The Gaussian Noise sample demonstrates how to add per-image Gaussian noise to an +image using CV-CUDA's GPU-accelerated ``gaussiannoise`` operator. The operator +accepts per-image mean (``mu``) and standard deviation (``sigma``) tensors, making +it straightforward to apply different noise levels to images in a batch. + +Usage +----- + +Basic Usage +^^^^^^^^^^^ + +Add Gaussian noise with default settings: + +.. code-block:: bash + + python3 gaussiannoise.py -i input.jpg + +Custom Output Path +^^^^^^^^^^^^^^^^^^ + +Save the noisy image to a specific location: + +.. code-block:: bash + + python3 gaussiannoise.py -i input.jpg -o noisy_cat.jpg + +Command-Line Arguments +---------------------- + +.. list-table:: + :header-rows: 1 + :widths: 20 15 15 50 + + * - Argument + - Short Form + - Default + - Description + * - ``--input`` + - ``-i`` + - tabby_tiger_cat.jpg + - Input image file path + * - ``--output`` + - ``-o`` + - cvcuda/.cache/cat_gaussiannoise.jpg + - Output image file path + +Implementation +-------------- + +Applying Gaussian Noise +^^^^^^^^^^^^^^^^^^^^^^^ + +.. literalinclude:: ../../../../samples/operators/gaussiannoise.py + :language: python + :start-after: docs_tag: begin_gaussiannoise + :end-before: docs_tag: end_gaussiannoise + :dedent: + +Key points: + +1. **Per-image parameters**: ``mu`` and ``sigma`` are rank-1 tensors with layout ``"N"``, one scalar per image in the batch. +2. **per_channel flag**: When ``False`` the same noise sample is applied to every colour channel; set to ``True`` for independent per-channel noise. +3. **Reproducibility**: The ``seed`` parameter pins the PRNG state so results are deterministic across runs. +4. **Data type preservation**: The output tensor keeps the same dtype and layout as the input; no implicit conversion occurs. +5. **Clipping**: For ``U8`` inputs the operator automatically clamps the noisy values to ``[0, 255]``. + +Expected Output +^^^^^^^^^^^^^^^ + +The output shows the image with visible Gaussian noise (``sigma=25``): + +.. list-table:: + :widths: 50 50 + :align: center + + * - .. figure:: ../../content/tabby_tiger_cat.jpg + :width: 100% + + Original Input Image + + - .. figure:: ../../content/cat_gaussiannoise.jpg + :width: 100% + + Output: Image with Gaussian Noise (sigma=25) + +CV-CUDA Operators Used +^^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Operator + - Purpose + * - :py:func:`cvcuda.gaussiannoise` + - Add per-image Gaussian noise with configurable mu, sigma, and per-channel control + +Common Utilities Used +^^^^^^^^^^^^^^^^^^^^^ + +* :ref:`read_image() ` - Load image as CV-CUDA tensor +* :ref:`write_image() ` - Save noisy image +* ``cuda_memcpy_h2d`` - Upload mu/sigma parameter arrays to GPU + +See Also +-------- + +* :ref:`Resize Operator ` - Basic image transformation +* :ref:`Common Utilities ` - Helper functions diff --git a/docs/sphinx/samples/operators/histogrameq.rst b/docs/sphinx/samples/operators/histogrameq.rst new file mode 100644 index 000000000..78500c842 --- /dev/null +++ b/docs/sphinx/samples/operators/histogrameq.rst @@ -0,0 +1,138 @@ +.. + # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-License-Identifier: Apache-2.0 + # + # Licensed 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. + +.. _sample_histogrameq: + +Histogram Equalization +====================== + +Overview +-------- + +The Histogram Equalization sample demonstrates GPU-accelerated contrast enhancement using +CV-CUDA's ``histogrameq`` operator. The operator redistributes pixel intensities so the +cumulative histogram of the output image is approximately uniform, improving global contrast +without any parameter tuning. + +Usage +----- + +Basic Usage +^^^^^^^^^^^ + +Equalize an image with the default input: + +.. code-block:: bash + + python3 histogrameq.py + +Custom Input +^^^^^^^^^^^^ + +Specify a custom input and output path: + +.. code-block:: bash + + python3 histogrameq.py -i input.jpg -o cat_histogrameq.jpg + +Command-Line Arguments +---------------------- + +.. list-table:: + :header-rows: 1 + :widths: 20 15 15 50 + + * - Argument + - Short Form + - Default + - Description + * - ``--input`` + - ``-i`` + - tabby_tiger_cat.jpg + - Input image file path + * - ``--output`` + - ``-o`` + - cvcuda/.cache/cat_histogrameq.jpg + - Output image file path + +Implementation +-------------- + +Histogram Equalization +^^^^^^^^^^^^^^^^^^^^^^ + +.. literalinclude:: ../../../../samples/operators/histogrameq.py + :language: python + :start-after: docs_tag: begin_histogrameq + :end-before: docs_tag: end_histogrameq + :dedent: + +Key points: + +1. **Grayscale conversion**: ``cvcuda.cvtcolor`` with ``RGB2GRAY`` is applied first because histogram equalization is most meaningful on a single luminance channel. +2. **Batched NHWC layout**: The HWC image is wrapped in a batch dimension via ``cvcuda.stack`` so the ``cvtcolor`` operator (which expects NHWC) can be used directly. +3. **dtype keyword**: ``cvcuda.histogrameq`` requires an explicit ``dtype`` argument when operating on a ``Tensor``; for image-batch inputs the argument is optional. +4. **Host-side channel replication**: The equalized single-channel output is downloaded, tiled to three channels on the CPU, and re-uploaded as an HWC tensor so ``write_image`` can encode a standard JPEG. +5. **Zero-copy back-path**: ``cuda_memcpy_h2d`` and ``cuda_memcpy_d2h`` avoid any Python-level buffer copies beyond the mandatory host round-trip needed for channel replication. + +Expected Output +^^^^^^^^^^^^^^^ + +The output shows the original image converted to grayscale with equalized contrast: + +.. list-table:: + :widths: 50 50 + :align: center + + * - .. figure:: ../../content/tabby_tiger_cat.jpg + :width: 100% + + Original Input Image + + - .. figure:: ../../content/cat_histogrameq.jpg + :width: 100% + + Output: Histogram-Equalized Grayscale + +CV-CUDA Operators Used +^^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Operator + - Purpose + * - :py:func:`cvcuda.histogrameq` + - Equalize pixel-intensity histogram to enhance global contrast + * - :py:func:`cvcuda.cvtcolor` + - Convert RGB image to single-channel grayscale before equalization + * - :py:func:`cvcuda.stack` + - Wrap a single HWC tensor into an NHWC batch for ``cvtcolor`` + +Common Utilities Used +^^^^^^^^^^^^^^^^^^^^^ + +* :ref:`read_image() ` - Load image as CV-CUDA tensor +* :ref:`write_image() ` - Save equalized image +* ``cuda_memcpy_d2h`` - Download equalized tensor to NumPy for channel replication +* ``cuda_memcpy_h2d`` - Upload replicated RGB tensor back to GPU for encoding + +See Also +-------- + +* :ref:`Resize Operator ` - Basic image transformation example +* :ref:`Common Utilities ` - Helper functions used across samples diff --git a/docs/sphinx/samples/operators/hq_resize.rst b/docs/sphinx/samples/operators/hq_resize.rst new file mode 100644 index 000000000..d5d497ed1 --- /dev/null +++ b/docs/sphinx/samples/operators/hq_resize.rst @@ -0,0 +1,147 @@ +.. + # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-License-Identifier: Apache-2.0 + # + # Licensed 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. + +.. _sample_hq_resize: + +HQ Resize +========= + +Overview +-------- + +The HQ Resize sample demonstrates high-quality image resizing using CV-CUDA's GPU-accelerated HQ +Resize operator. Unlike the standard Resize operator, HQ Resize accepts separate interpolation +filters for downscaling (``min_interpolation``) and upscaling (``mag_interpolation``), and +optionally applies an antialiasing low-pass filter before downscaling to eliminate moiré patterns +and ringing artifacts. + +Usage +----- + +Basic Usage +^^^^^^^^^^^ + +HQ-resize an image to 224×224 (default): + +.. code-block:: bash + + python3 hq_resize.py -i input.jpg + +Custom Dimensions +^^^^^^^^^^^^^^^^^ + +Specify a target width and height with a custom output path: + +.. code-block:: bash + + python3 hq_resize.py -i input.jpg -o cat_hq_resize.jpg --width 512 --height 512 + +Command-Line Arguments +---------------------- + +.. list-table:: + :header-rows: 1 + :widths: 20 15 15 50 + + * - Argument + - Short Form + - Default + - Description + * - ``--input`` + - ``-i`` + - tabby_tiger_cat.jpg + - Input image file path + * - ``--output`` + - ``-o`` + - cvcuda/.cache/cat_hq_resize.jpg + - Output image file path + * - ``--width`` + - + - 224 + - Target width in pixels + * - ``--height`` + - + - 224 + - Target height in pixels + +Implementation +-------------- + +HQ Resize Operator Call +^^^^^^^^^^^^^^^^^^^^^^^ + +.. literalinclude:: ../../../../samples/operators/hq_resize.py + :language: python + :start-after: docs_tag: begin_hq_resize + :end-before: docs_tag: end_hq_resize + :dedent: + +Key points: + +1. **Dual interpolation filters**: ``min_interpolation`` governs downscaling and + ``mag_interpolation`` governs upscaling, allowing the best filter to be chosen + for each direction independently. +2. **LANCZOS for minification**: The Lanczos filter provides superior sharpness + and suppresses aliasing compared to LINEAR or NEAREST when reducing image size. +3. **Antialiasing flag**: Setting ``antialias=True`` applies a low-pass filter + before downscaling, which further reduces moiré and ringing in the output. +4. **out_size is (H, W)**: The target size is specified as a ``(height, width)`` + tuple — no channel dimension is included; the operator infers it from the input layout. +5. **U8 in/out, no conversion**: The output tensor inherits the data type and layout + (HWC, uint8) of the input, so no additional type conversion is needed before saving. + +Expected Output +^^^^^^^^^^^^^^^ + +The output shows the image resized to the target dimensions (default 224×224): + +.. list-table:: + :widths: 50 50 + :align: center + + * - .. figure:: ../../content/tabby_tiger_cat.jpg + :width: 100% + + Original Input Image + + - .. figure:: ../../content/cat_hq_resize.jpg + :width: 100% + + Output: HQ-Resized to 224×224 + +CV-CUDA Operators Used +^^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Operator + - Purpose + * - :py:func:`cvcuda.hq_resize` + - High-quality resize with separate min/mag interpolation filters and optional antialiasing + +Common Utilities Used +^^^^^^^^^^^^^^^^^^^^^ + +* :ref:`read_image() ` - Load image as CV-CUDA tensor +* :ref:`write_image() ` - Save the resized image + +See Also +-------- + +* :ref:`Resize Operator ` - Standard (lower-overhead) resize operator +* :ref:`Common Utilities ` - Helper functions diff --git a/docs/sphinx/samples/operators/inpaint.rst b/docs/sphinx/samples/operators/inpaint.rst new file mode 100644 index 000000000..8e58dd6e6 --- /dev/null +++ b/docs/sphinx/samples/operators/inpaint.rst @@ -0,0 +1,144 @@ +.. + # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-License-Identifier: Apache-2.0 + # + # Licensed 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. + +.. _sample_inpaint: + +Inpaint +======= + +Overview +-------- + +The Inpaint sample demonstrates image inpainting using CV-CUDA's GPU-accelerated inpaint operator. +Inpainting reconstructs the pixel values inside a user-supplied mask region by propagating colour +information from the surrounding unmasked pixels. The sample simulates salt-and-pepper sensor +noise by randomly zeroing ~15 % of pixels, then uses inpainting to remove the noise and restore +the image. + +Usage +----- + +Basic Usage +^^^^^^^^^^^ + +Inpaint the default tabby-cat image: + +.. code-block:: bash + + python3 inpaint.py -i input.jpg + +Custom Input and Output +^^^^^^^^^^^^^^^^^^^^^^^ + +Specify a custom input image and output path: + +.. code-block:: bash + + python3 inpaint.py -i image.jpg -o cat_inpaint.jpg + +Command-Line Arguments +---------------------- + +.. list-table:: + :header-rows: 1 + :widths: 20 15 15 50 + + * - Argument + - Short Form + - Default + - Description + * - ``--input`` + - ``-i`` + - tabby_tiger_cat.jpg + - Input image file path + * - ``--output`` + - ``-o`` + - cvcuda/.cache/cat_inpaint.jpg + - Output image file path + +Implementation +-------------- + +Inpainting and Outside-Mask Restoration +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. literalinclude:: ../../../../samples/operators/inpaint.py + :language: python + :start-after: docs_tag: begin_inpaint + :end-before: docs_tag: end_inpaint + :dedent: + +Key points: + +1. **Mask format**: The mask must be a single-channel (``NHWC`` with ``C=1``) ``U8`` tensor. + Non-zero pixels mark the region to be reconstructed; zero pixels are left unchanged. +2. **Batched input**: ``cvcuda.inpaint`` requires an ``NHWC`` (batched) source tensor. + A plain ``HWC`` image is reshaped to ``(1, H, W, C)`` before the call. +3. **inpaintRadius**: Controls the neighbourhood radius examined when reconstructing each + masked pixel. Larger values smooth over wider damaged areas at the cost of more + computation. +4. **Outside-mask restoration**: The operator may alter pixels just outside the mask + boundary, so the result is downloaded and the original content is restored everywhere + outside the mask (via ``np.where``) before the final image is uploaded and saved. +5. **Synthetic mask via upload_tensor**: The mask is built as a NumPy array on the CPU and + then uploaded to a pre-allocated GPU tensor with ``upload_tensor``, matching the pattern + used whenever host-side parameter data must be passed as a tensor. + +Expected Output +^^^^^^^^^^^^^^^ + +The output shows the image with salt-and-pepper noise removed by inpainting: + +.. list-table:: + :widths: 50 50 + :align: center + + * - .. figure:: ../../content/cat_inpaint_damaged.jpg + :width: 100% + + Input: Image with simulated salt-and-pepper sensor noise (~15 % pixels zeroed) + + - .. figure:: ../../content/cat_inpaint.jpg + :width: 100% + + Output: Noise removed by inpainting + +CV-CUDA Operators Used +^^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Operator + - Purpose + * - :py:func:`cvcuda.inpaint` + - Reconstruct masked pixel regions using surrounding colour information + +Common Utilities Used +^^^^^^^^^^^^^^^^^^^^^ + +* :ref:`read_image() ` - Load image as CV-CUDA tensor +* :ref:`write_image() ` - Save inpainted image +* ``upload_tensor`` - Upload the CPU-built mask and result arrays to GPU tensors +* ``download_tensor`` - Download tensors to the CPU for damage synthesis and restoration + +See Also +-------- + +* :ref:`Resize Operator ` - Simple single-operator sample +* :ref:`Label Operator ` - Another sample that synthesises inputs and uploads via cuda_memcpy_h2d +* :ref:`Common Utilities ` - Helper functions diff --git a/docs/sphinx/samples/operators/joint_bilateral_filter.rst b/docs/sphinx/samples/operators/joint_bilateral_filter.rst new file mode 100644 index 000000000..3f4dde21c --- /dev/null +++ b/docs/sphinx/samples/operators/joint_bilateral_filter.rst @@ -0,0 +1,143 @@ +.. + # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-License-Identifier: Apache-2.0 + # + # Licensed 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. + +.. _sample_joint_bilateral_filter: + +Joint Bilateral Filter +====================== + +Overview +-------- + +The Joint Bilateral Filter sample demonstrates edge-preserving image smoothing using CV-CUDA's +GPU-accelerated joint bilateral filter operator. Unlike the standard bilateral filter, the joint +(cross) variant uses a separate *guidance* image to steer the range kernel — edges detected in +the guidance image are preserved in the filtered output, making it well-suited for noise +reduction while retaining sharp structural boundaries. + +Usage +----- + +Basic Usage +^^^^^^^^^^^ + +Apply joint bilateral filtering to an image using the default input: + +.. code-block:: bash + + python3 joint_bilateral_filter.py -i input.jpg + +Custom Output +^^^^^^^^^^^^^ + +Specify a custom output path: + +.. code-block:: bash + + python3 joint_bilateral_filter.py -i input.jpg -o cat_joint_bilateral_filter.jpg + +Command-Line Arguments +---------------------- + +.. list-table:: + :header-rows: 1 + :widths: 20 15 15 50 + + * - Argument + - Short Form + - Default + - Description + * - ``--input`` + - ``-i`` + - tabby_tiger_cat.jpg + - Input image file path + * - ``--output`` + - ``-o`` + - cvcuda/.cache/cat_joint_bilateral_filter.jpg + - Output image file path + +Implementation +-------------- + +Joint Bilateral Filter +^^^^^^^^^^^^^^^^^^^^^^ + +.. literalinclude:: ../../../../samples/operators/joint_bilateral_filter.py + :language: python + :start-after: docs_tag: begin_joint_bilateral_filter + :end-before: docs_tag: end_joint_bilateral_filter + :dedent: + +Key points: + +1. **Guidance image**: A grayscale-derived image is used as the guidance signal so that luminance + edges govern which pixels are blended — colour-channel noise is reduced without crossing structural + boundaries. +2. **Channel matching**: ``srcColor`` must have the same spatial size and channel count as ``src``; + the grayscale result is converted back to 3-channel RGB before being passed as guidance. +3. **diameter**: Controls the neighbourhood size; larger values consider farther pixels but increase + cost quadratically. +4. **sigma_color / sigma_space**: Larger values produce stronger smoothing; ``sigma_color`` governs + how different colours can still be blended, ``sigma_space`` governs spatial reach. +5. **Batch dimension**: The HWC tensor read from disk is reshaped to NHWC for ``cvtcolor`` and the + filter call, then reshaped back to HWC before writing the output. + +Expected Output +^^^^^^^^^^^^^^^ + +The output is a smoothed version of the input with fine texture noise reduced while prominent +edges — fur boundaries, whiskers — remain sharp because the luminance guidance image keeps them +intact. + +.. list-table:: + :widths: 50 50 + :align: center + + * - .. figure:: ../../content/tabby_tiger_cat.jpg + :width: 100% + + Original Input Image + + - .. figure:: ../../content/cat_joint_bilateral_filter.jpg + :width: 100% + + Output: Joint Bilateral Filtered + +CV-CUDA Operators Used +^^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Operator + - Purpose + * - :py:func:`cvcuda.joint_bilateral_filter` + - Edge-preserving smoothing guided by a separate reference image + * - :py:func:`cvcuda.cvtcolor` + - Convert RGB to grayscale (and back) to build the guidance tensor + +Common Utilities Used +^^^^^^^^^^^^^^^^^^^^^ + +* :ref:`read_image() ` - Load image as CV-CUDA tensor +* :ref:`write_image() ` - Save filtered image + +See Also +-------- + +* :ref:`Resize Operator ` - Basic spatial transformation +* :ref:`Common Utilities ` - Helper functions diff --git a/docs/sphinx/samples/operators/laplacian.rst b/docs/sphinx/samples/operators/laplacian.rst new file mode 100644 index 000000000..c29da016f --- /dev/null +++ b/docs/sphinx/samples/operators/laplacian.rst @@ -0,0 +1,141 @@ +.. + # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-License-Identifier: Apache-2.0 + # + # Licensed 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. + +.. _sample_laplacian: + +Laplacian +========= + +Overview +-------- + +The Laplacian sample demonstrates second-order edge detection using CV-CUDA's GPU-accelerated +Laplacian operator. The Laplacian highlights regions of rapid intensity change, making it a +classic tool for detecting edges and fine structures in images. After applying the operator the +sample stretches the response histogram to the full uint8 range so the edge map is immediately +viewable as a JPEG. + +Usage +----- + +Basic Usage +^^^^^^^^^^^ + +Apply the Laplacian operator with the default settings (ksize=3, scale=1.0): + +.. code-block:: bash + + python3 laplacian.py -i input.jpg + +Custom Output Path +^^^^^^^^^^^^^^^^^^ + +Specify a custom output file path: + +.. code-block:: bash + + python3 laplacian.py -i input.jpg -o cat_laplacian.jpg + +Command-Line Arguments +---------------------- + +.. list-table:: + :header-rows: 1 + :widths: 20 15 15 50 + + * - Argument + - Short Form + - Default + - Description + * - ``--input`` + - ``-i`` + - tabby_tiger_cat.jpg + - Input image file path + * - ``--output`` + - ``-o`` + - cvcuda/.cache/cat_laplacian.jpg + - Output image file path + +Implementation +-------------- + +Laplacian Edge Detection +^^^^^^^^^^^^^^^^^^^^^^^^ + +.. literalinclude:: ../../../../samples/operators/laplacian.py + :language: python + :start-after: docs_tag: begin_laplacian + :end-before: docs_tag: end_laplacian + :dedent: + +Key points: + +1. **Kernel size**: ``ksize=3`` selects the 3×3 discrete Laplacian aperture. The only other + supported value is ``ksize=1``, which uses a simpler cross-shaped kernel. +2. **Scale factor**: ``scale=1.0`` applies a uniform multiplier to the computed Laplacian + values before the result is saturated back to the input dtype. Increasing the scale + amplifies weaker edges. +3. **Border handling**: ``cvcuda.Border.REPLICATE`` repeats the edge pixels outward, + avoiding the zero-filled boundary artifacts that ``CONSTANT`` mode would introduce. +4. **Dtype preservation**: The operator returns a tensor with the same dtype and layout as + the input (uint8 HWC here), so no format conversion is required. +5. **Histogram stretching**: The raw Laplacian response is typically concentrated in a + narrow value range. A host-side min/max stretch makes the edge map clearly visible in + the saved image without changing the operator's output semantics. + +Expected Output +^^^^^^^^^^^^^^^ + +The output shows the Laplacian edge response of the input image, normalized for visibility: + +.. list-table:: + :widths: 50 50 + :align: center + + * - .. figure:: ../../content/tabby_tiger_cat.jpg + :width: 100% + + Original Input Image + + - .. figure:: ../../content/cat_laplacian.jpg + :width: 100% + + Output: Laplacian Edge Response + +CV-CUDA Operators Used +^^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Operator + - Purpose + * - :py:func:`cvcuda.laplacian` + - Apply second-order Laplacian edge-detection filter to the input image + +Common Utilities Used +^^^^^^^^^^^^^^^^^^^^^ + +* :ref:`read_image() ` - Load image as CV-CUDA tensor +* :ref:`write_image() ` - Save the edge-response image +* ``cuda_memcpy_d2h`` / ``cuda_memcpy_h2d`` - Transfer tensor data to/from host for histogram stretching + +See Also +-------- + +* :ref:`Resize Operator ` - Resize images with GPU acceleration +* :ref:`Common Utilities ` - Helper functions used across samples diff --git a/docs/sphinx/samples/operators/median_blur.rst b/docs/sphinx/samples/operators/median_blur.rst new file mode 100644 index 000000000..920913d35 --- /dev/null +++ b/docs/sphinx/samples/operators/median_blur.rst @@ -0,0 +1,131 @@ +.. + # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-License-Identifier: Apache-2.0 + # + # Licensed 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. + +.. _sample_median_blur: + +Median Blur +=========== + +Overview +-------- + +The Median Blur sample demonstrates GPU-accelerated median filtering using CV-CUDA's +``median_blur`` operator. Median blur replaces each pixel with the median value of its +neighborhood, making it highly effective for removing salt-and-pepper noise while +preserving edges better than a simple averaging blur. + +Usage +----- + +Basic Usage +^^^^^^^^^^^ + +Apply a 7×7 median blur to an image (default): + +.. code-block:: bash + + python3 median_blur.py -i input.jpg + +Custom Output Path +^^^^^^^^^^^^^^^^^^ + +Specify a custom output path: + +.. code-block:: bash + + python3 median_blur.py -i input.jpg -o blurred.jpg + +Command-Line Arguments +---------------------- + +.. list-table:: + :header-rows: 1 + :widths: 20 15 15 50 + + * - Argument + - Short Form + - Default + - Description + * - ``--input`` + - ``-i`` + - tabby_tiger_cat.jpg + - Input image file path + * - ``--output`` + - ``-o`` + - cvcuda/.cache/cat_median_blur.jpg + - Output image file path + +Implementation +-------------- + +Median Blur Operation +^^^^^^^^^^^^^^^^^^^^^ + +.. literalinclude:: ../../../../samples/operators/median_blur.py + :language: python + :start-after: docs_tag: begin_median_blur + :end-before: docs_tag: end_median_blur + :dedent: + +Key points: + +1. **Kernel size**: ``ksize`` is a two-element list ``[kW, kH]`` where both values must be odd positive integers. Larger kernels produce stronger smoothing. +2. **Noise removal**: Median blur is especially effective for removing impulse (salt-and-pepper) noise because the median statistic is robust to outliers. +3. **Edge preservation**: Unlike mean blur, median blur preserves edges well since the median value is always drawn from actual pixel values in the neighborhood. +4. **Supported types**: The operator supports ``uint8``, ``uint16``, and ``float32`` data types in HWC or NHWC layout. + +Expected Output +^^^^^^^^^^^^^^^ + +The output shows the image with impulse noise suppressed and edges intact: + +.. list-table:: + :widths: 50 50 + :align: center + + * - .. figure:: ../../content/tabby_tiger_cat.jpg + :width: 100% + + Original Input Image + + - .. figure:: ../../content/cat_median_blur.jpg + :width: 100% + + Output: Median Blur (7×7 kernel) + +CV-CUDA Operators Used +^^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Operator + - Purpose + * - :py:func:`cvcuda.median_blur` + - Apply median blur filter to remove noise while preserving edges + +Common Utilities Used +^^^^^^^^^^^^^^^^^^^^^ + +* :ref:`read_image() ` - Load image as CV-CUDA tensor +* :ref:`write_image() ` - Save blurred image + +See Also +-------- + +* :ref:`Resize Operator ` - Resize images with interpolation +* :ref:`Common Utilities ` - Helper functions diff --git a/docs/sphinx/samples/operators/morphology.rst b/docs/sphinx/samples/operators/morphology.rst new file mode 100644 index 000000000..3dc5f79f4 --- /dev/null +++ b/docs/sphinx/samples/operators/morphology.rst @@ -0,0 +1,133 @@ +.. + # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-License-Identifier: Apache-2.0 + # + # Licensed 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. + +.. _sample_morphology: + +Morphology +========== + +Overview +-------- + +The Morphology sample demonstrates GPU-accelerated morphological image processing using CV-CUDA. +It applies a dilation followed by an erosion (equivalent to a morphological close operation) to +fill small dark gaps while preserving the main structures of the image. The sample illustrates how to +choose a structuring element size and how to supply a workspace tensor when required. + +Usage +----- + +Basic Usage +^^^^^^^^^^^ + +Apply morphological close (dilate then erode) with a 5×5 kernel to the default cat image: + +.. code-block:: bash + + python3 morphology.py -i input.jpg + +Custom Input +^^^^^^^^^^^^ + +Process a different image and save the result explicitly: + +.. code-block:: bash + + python3 morphology.py -i image.jpg -o cat_morphology.jpg + +Command-Line Arguments +---------------------- + +.. list-table:: + :header-rows: 1 + :widths: 20 15 15 50 + + * - Argument + - Short Form + - Default + - Description + * - ``--input`` + - ``-i`` + - tabby_tiger_cat.jpg + - Input image file path + * - ``--output`` + - ``-o`` + - cvcuda/.cache/cat_morphology.jpg + - Output image file path + +Implementation +-------------- + +Morphological Close (Dilate then Erode) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. literalinclude:: ../../../../samples/operators/morphology.py + :language: python + :start-after: docs_tag: begin_morphology + :end-before: docs_tag: end_morphology + :dedent: + +Key points: + +1. **Batch reshape**: The HWC tensor returned by ``read_image`` is reshaped to NHWC before calling the operator, which accepts both layouts. +2. **Structuring element**: ``mask_size=[5, 5]`` selects a 5×5 rectangular kernel; ``anchor=[-1, -1]`` auto-centres it. +3. **DILATE then ERODE**: Applying dilation followed by erosion is a morphological close, which fills small dark holes and gaps while keeping large bright structures intact. +4. **Workspace tensor**: A workspace tensor of the same shape and dtype as the input is required when passing the result of one morphological call into a second one; it is used internally by the operator as scratch memory. +5. **Output reshape**: The NHWC result is reshaped back to HWC before ``write_image`` to produce a standard single-image output. + +Expected Output +^^^^^^^^^^^^^^^ + +The output shows the image after morphological closing — small dark gaps are filled and bright regions are slightly expanded: + +.. list-table:: + :widths: 50 50 + :align: center + + * - .. figure:: ../../content/tabby_tiger_cat.jpg + :width: 100% + + Original Input Image + + - .. figure:: ../../content/cat_morphology.jpg + :width: 100% + + Output: Morphological Close (5×5 kernel) + +CV-CUDA Operators Used +^^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Operator + - Purpose + * - :py:func:`cvcuda.morphology` + - Apply dilation and erosion with a rectangular structuring element + +Common Utilities Used +^^^^^^^^^^^^^^^^^^^^^ + +* :ref:`read_image() ` - Load image as CV-CUDA tensor +* :ref:`write_image() ` - Save the morphologically processed image +* :ref:`parse_image_args() ` - Parse ``--input`` / ``--output`` CLI arguments + +See Also +-------- + +* :ref:`Resize Operator ` - Basic spatial transform operator +* :ref:`Common Utilities ` - Helper functions used across samples diff --git a/docs/sphinx/samples/operators/normalize.rst b/docs/sphinx/samples/operators/normalize.rst new file mode 100644 index 000000000..7572b406b --- /dev/null +++ b/docs/sphinx/samples/operators/normalize.rst @@ -0,0 +1,150 @@ +.. + # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-License-Identifier: Apache-2.0 + # + # Licensed 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. + +.. _sample_normalize: + +Normalize +========= + +Overview +-------- + +The Normalize sample demonstrates per-channel mean-and-standard-deviation normalization +using CV-CUDA's GPU-accelerated normalize operator. The sample applies the standard +ImageNet statistics (mean ``[123.675, 116.28, 103.53]`` and std ``[58.395, 57.12, 57.375]`` +expressed in [0, 255] space) to an RGB image, producing float32 normalized values. +Because the normalized output is not directly viewable as a JPEG, the sample linearly +rescales the result back to the [0, 255] uint8 range before saving. + +Usage +----- + +Basic Usage +^^^^^^^^^^^ + +Normalize an image using the default ImageNet statistics: + +.. code-block:: bash + + python3 normalize.py -i input.jpg + +Custom Output Path +^^^^^^^^^^^^^^^^^^ + +Specify a custom output file: + +.. code-block:: bash + + python3 normalize.py -i input.jpg -o my_normalized.jpg + +Command-Line Arguments +---------------------- + +.. list-table:: + :header-rows: 1 + :widths: 20 15 15 50 + + * - Argument + - Short Form + - Default + - Description + * - ``--input`` + - ``-i`` + - tabby_tiger_cat.jpg + - Input image file path + * - ``--output`` + - ``-o`` + - cvcuda/.cache/cat_normalize.jpg + - Output image file path + +Implementation +-------------- + +Setup: Mean and Std Tensors +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. literalinclude:: ../../../../samples/operators/normalize.py + :language: python + :start-after: docs_tag: begin_normalize_setup + :end-before: docs_tag: end_normalize_setup + :dedent: + +Normalize Operator Call +^^^^^^^^^^^^^^^^^^^^^^^ + +.. literalinclude:: ../../../../samples/operators/normalize.py + :language: python + :start-after: docs_tag: begin_normalize + :end-before: docs_tag: end_normalize + :dedent: + +Key points: + +1. **base and scale tensors**: Broadcast-shaped ``(1, 1, 3)`` HWC tensors holding per-channel mean + and standard deviation values; the operator broadcasts them across all pixels automatically. +2. **SCALE_IS_STDDEV flag**: Tells the operator that the ``scale`` argument is a standard deviation + rather than a raw scaling factor, so it computes ``out = (src - base) / (scale + epsilon)``. +3. **Float32 input requirement**: Passing a float32 source keeps the output in float32 so the + normalized values retain their signed range; a uint8 source would clamp the result back to uint8. +4. **epsilon**: A small regularizer added to the denominator, preventing division by zero when the + standard deviation is near zero. +5. **Visualization rescaling**: The normalized output typically falls in ``[-2, 2]``. The sample + min-max rescales that range back to ``[0, 255]`` for JPEG encoding. + +Expected Output +^^^^^^^^^^^^^^^ + +The output shows the pixel distribution shifted and scaled by the ImageNet statistics, +then remapped to uint8 for viewing: + +.. list-table:: + :widths: 50 50 + :align: center + + * - .. figure:: ../../content/tabby_tiger_cat.jpg + :width: 100% + + Original Input Image + + - .. figure:: ../../content/cat_normalize.jpg + :width: 100% + + Output: ImageNet-normalized (rescaled to uint8 for display) + +CV-CUDA Operators Used +^^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Operator + - Purpose + * - :py:func:`cvcuda.normalize` + - Apply per-channel mean-std normalization to a tensor + +Common Utilities Used +^^^^^^^^^^^^^^^^^^^^^ + +* :ref:`read_image() ` - Load image as CV-CUDA tensor +* :ref:`write_image() ` - Save normalized image +* ``cuda_memcpy_h2d`` / ``cuda_memcpy_d2h`` - Transfer base/scale parameters and results between host and device + +See Also +-------- + +* :ref:`Resize Operator ` - Resize images with GPU acceleration +* :ref:`Common Utilities ` - Helper functions used in this sample diff --git a/docs/sphinx/samples/operators/osd.rst b/docs/sphinx/samples/operators/osd.rst new file mode 100644 index 000000000..0dedcb3a3 --- /dev/null +++ b/docs/sphinx/samples/operators/osd.rst @@ -0,0 +1,149 @@ +.. + # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-License-Identifier: Apache-2.0 + # + # Licensed 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. + +.. _sample_osd: + +On-Screen Display +================= + +Overview +-------- + +The On-Screen Display (OSD) sample demonstrates GPU-accelerated compositing of +visual annotations—bounding boxes, text labels, lines, circles, arrows, polygons, +and clock overlays—onto an image using CV-CUDA's :py:func:`cvcuda.osd` operator. + +Usage +----- + +Basic Usage +^^^^^^^^^^^ + +Draw the default set of OSD elements onto the sample cat image: + +.. code-block:: bash + + python3 osd.py -i input.jpg + +Custom Input and Output +^^^^^^^^^^^^^^^^^^^^^^^ + +Specify a custom input image and output path: + +.. code-block:: bash + + python3 osd.py -i input.jpg -o annotated.jpg + +Command-Line Arguments +---------------------- + +.. list-table:: + :header-rows: 1 + :widths: 20 15 15 50 + + * - Argument + - Short Form + - Default + - Description + * - ``--input`` + - ``-i`` + - tabby_tiger_cat.jpg + - Input image file path + * - ``--output`` + - ``-o`` + - cvcuda/.cache/cat_osd.jpg + - Output image file path with OSD annotations + +Implementation +-------------- + +OSD Element Setup +^^^^^^^^^^^^^^^^^ + +.. literalinclude:: ../../../../samples/operators/osd.py + :language: python + :start-after: docs_tag: begin_osd_setup + :end-before: docs_tag: end_osd_setup + :dedent: + +OSD Operator Call +^^^^^^^^^^^^^^^^^ + +.. literalinclude:: ../../../../samples/operators/osd.py + :language: python + :start-after: docs_tag: begin_osd + :end-before: docs_tag: end_osd + :dedent: + +Key points: + +1. **Tensor layout**: :py:func:`cvcuda.osd` supports ``NHWC``/``HWC`` and + ``NCHW``/``CHW`` tensors. The sample uses ``NHWC``: a single ``HWC`` image is + reshaped to ``(1, H, W, C)`` before the call and squeezed back afterward. +2. **Elements list-of-lists**: :py:class:`cvcuda.Elements` takes a list with one + inner list per image in the batch. Each inner list may contain any mix of the + supported primitive types. +3. **In-place compositing**: The output tensor shares shape, dtype, and layout + with the input; all primitives are alpha-blended onto it in a single GPU pass. +4. **Coordinate scaling**: Positions and sizes are computed relative to the image + dimensions so the overlay adapts to any input resolution. +5. **Primitive variety**: The sample showcases all major OSD primitives— + :py:class:`cvcuda.BndBoxI`, :py:class:`cvcuda.Label`, :py:class:`cvcuda.Line`, + :py:class:`cvcuda.Circle`, :py:class:`cvcuda.Arrow`, + :py:class:`cvcuda.PolyLine`, and :py:class:`cvcuda.Clock`. + +Expected Output +^^^^^^^^^^^^^^^ + +The output is the input image with all OSD annotations composited on top: + +.. list-table:: + :widths: 50 50 + :align: center + + * - .. figure:: ../../content/tabby_tiger_cat.jpg + :width: 100% + + Original Input Image + + - .. figure:: ../../content/cat_osd.jpg + :width: 100% + + Output: Image with OSD annotations + +CV-CUDA Operators Used +^^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Operator + - Purpose + * - :py:func:`cvcuda.osd` + - Composite bounding boxes, text, lines, shapes, and overlays onto images + +Common Utilities Used +^^^^^^^^^^^^^^^^^^^^^ + +* :ref:`read_image() ` - Load image as CV-CUDA tensor +* :ref:`write_image() ` - Save annotated image + +See Also +-------- + +* :ref:`Resize Operator ` - Resize images before annotation +* :ref:`Common Utilities ` - Helper functions diff --git a/docs/sphinx/samples/operators/pillowresize.rst b/docs/sphinx/samples/operators/pillowresize.rst new file mode 100644 index 000000000..2bd5d7d9f --- /dev/null +++ b/docs/sphinx/samples/operators/pillowresize.rst @@ -0,0 +1,142 @@ +.. + # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-License-Identifier: Apache-2.0 + # + # Licensed 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. + +.. _sample_pillowresize: + +Pillow Resize +============= + +Overview +-------- + +The Pillow Resize sample demonstrates high-quality image resizing using CV-CUDA's +GPU-accelerated Pillow-style resize operator. Unlike a plain bilinear or nearest-neighbour +resize, ``pillowresize`` matches the resampling quality of Python's Pillow library by +supporting filters such as LANCZOS, HAMMING, and BOX that are especially well-suited for +downscaling images. + +Usage +----- + +Basic Usage +^^^^^^^^^^^ + +Resize an image to 224×224 (default) using the LANCZOS filter: + +.. code-block:: bash + + python3 pillowresize.py -i input.jpg + +Custom Dimensions +^^^^^^^^^^^^^^^^^ + +Specify target width and height: + +.. code-block:: bash + + python3 pillowresize.py -i input.jpg -o cat_pillowresize.jpg --width 512 --height 512 + +Command-Line Arguments +---------------------- + +.. list-table:: + :header-rows: 1 + :widths: 20 15 15 50 + + * - Argument + - Short Form + - Default + - Description + * - ``--input`` + - ``-i`` + - tabby_tiger_cat.jpg + - Input image file path + * - ``--output`` + - ``-o`` + - cvcuda/.cache/cat_pillowresize.jpg + - Output image file path + * - ``--width`` + - + - 224 + - Target width in pixels + * - ``--height`` + - + - 224 + - Target height in pixels + +Implementation +-------------- + +Pillow-Quality Resize +^^^^^^^^^^^^^^^^^^^^^ + +.. literalinclude:: ../../../../samples/operators/pillowresize.py + :language: python + :start-after: docs_tag: begin_pillowresize + :end-before: docs_tag: end_pillowresize + :dedent: + +Key points: + +1. **Output Shape**: Must include channel count explicitly, e.g. ``(H, W, C)`` for HWC tensors. +2. **Format Parameter**: Tells the operator how to interpret channel ordering (e.g. ``cvcuda.Format.RGB8``). +3. **LANCZOS Filter**: Produces sharper edges than LINEAR and is the recommended choice for downscaling, matching Pillow's high-quality mode. +4. **uint8 Output**: The operator preserves the input dtype; reading a JPEG returns ``uint8``, so the result is directly viewable without rescaling. +5. **Interp Variants**: ``HAMMING`` and ``BOX`` are also available and offer different quality/speed trade-offs for downscaling. + +Expected Output +^^^^^^^^^^^^^^^ + +The output shows the image resized to the target dimensions (default 224×224) with +Pillow-quality LANCZOS interpolation: + +.. list-table:: + :widths: 50 50 + :align: center + + * - .. figure:: ../../content/tabby_tiger_cat.jpg + :width: 100% + + Original Input Image + + - .. figure:: ../../content/cat_pillowresize.jpg + :width: 100% + + Output: Pillow Resize to 224×224 (LANCZOS) + +CV-CUDA Operators Used +^^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Operator + - Purpose + * - :py:func:`cvcuda.pillowresize` + - Resize images to target dimensions using Pillow-compatible high-quality filters + +Common Utilities Used +^^^^^^^^^^^^^^^^^^^^^ + +* :ref:`read_image() ` - Load image as CV-CUDA tensor +* :ref:`write_image() ` - Save resized image + +See Also +-------- + +* :ref:`Resize Operator ` - Standard GPU resize operator +* :ref:`Common Utilities ` - Helper functions diff --git a/docs/sphinx/samples/operators/random_resized_crop.rst b/docs/sphinx/samples/operators/random_resized_crop.rst new file mode 100644 index 000000000..ed547325f --- /dev/null +++ b/docs/sphinx/samples/operators/random_resized_crop.rst @@ -0,0 +1,150 @@ +.. + # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-License-Identifier: Apache-2.0 + # + # Licensed 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. + +.. _sample_random_resized_crop: + +Random Resized Crop +=================== + +Overview +-------- + +The Random Resized Crop sample demonstrates how to use CV-CUDA's GPU-accelerated +:py:func:`cvcuda.random_resized_crop` operator to randomly select a sub-region of an +image, resize it to a fixed output size, and write the result. This operation is the +core augmentation used in standard ImageNet training pipelines (e.g., torchvision's +``RandomResizedCrop``): a crop whose area is a random fraction of the original image +area and whose aspect ratio is sampled from a configurable range. + +Usage +----- + +Basic Usage +^^^^^^^^^^^ + +Apply random resized crop with default 224×224 output: + +.. code-block:: bash + + python3 random_resized_crop.py -i input.jpg + +Custom Output Size +^^^^^^^^^^^^^^^^^^ + +Specify a different target resolution: + +.. code-block:: bash + + python3 random_resized_crop.py -i input.jpg -o cropped.jpg --width 320 --height 320 + +Command-Line Arguments +---------------------- + +.. list-table:: + :header-rows: 1 + :widths: 20 15 15 50 + + * - Argument + - Short Form + - Default + - Description + * - ``--input`` + - ``-i`` + - tabby_tiger_cat.jpg + - Input image file path + * - ``--output`` + - ``-o`` + - cvcuda/.cache/cat_random_resized_crop.jpg + - Output image file path + * - ``--width`` + - + - 224 + - Target output width in pixels + * - ``--height`` + - + - 224 + - Target output height in pixels + +Implementation +-------------- + +Random Resized Crop +^^^^^^^^^^^^^^^^^^^ + +.. literalinclude:: ../../../../samples/operators/random_resized_crop.py + :language: python + :start-after: docs_tag: begin_random_resized_crop + :end-before: docs_tag: end_random_resized_crop + :dedent: + +Key points: + +1. **Batched NHWC input**: The operator expects an NHWC tensor (batch dimension first). + A single HWC image is reshaped to ``(1, H, W, C)`` before the call and the result is + reshaped back to ``(H, W, C)`` afterwards. +2. **Scale bounds**: ``min_scale`` and ``max_scale`` control what fraction of the + original image area the random crop covers. The defaults ``(0.08, 1.0)`` match + standard ImageNet pre-processing. +3. **Ratio bounds**: ``min_ratio`` and ``max_ratio`` bound the width-to-height ratio of + the crop region before it is scaled to the output size, letting the network see both + tall and wide crops. +4. **Interpolation**: ``cvcuda.Interp.LINEAR`` (bilinear) gives a good quality/speed + trade-off; ``NEAREST`` is faster, ``CUBIC`` provides higher fidelity. +5. **Reproducibility**: The ``seed`` parameter makes the crop deterministic, which is + useful for debugging or ablation experiments. + +Expected Output +^^^^^^^^^^^^^^^ + +The output shows a randomly selected and resized crop of the original image: + +.. list-table:: + :widths: 50 50 + :align: center + + * - .. figure:: ../../content/tabby_tiger_cat.jpg + :width: 100% + + Original Input Image + + - .. figure:: ../../content/cat_random_resized_crop.jpg + :width: 100% + + Output: Random Resized Crop to 224×224 + +CV-CUDA Operators Used +^^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Operator + - Purpose + * - :py:func:`cvcuda.random_resized_crop` + - Randomly crop a sub-region of the image and resize it to the target dimensions + +Common Utilities Used +^^^^^^^^^^^^^^^^^^^^^ + +* :ref:`read_image() ` - Load image as CV-CUDA tensor +* :ref:`write_image() ` - Save the cropped and resized image + +See Also +-------- + +* :ref:`Resize Operator ` - Deterministic resize to fixed dimensions +* :ref:`Common Utilities ` - Helper functions diff --git a/docs/sphinx/samples/operators/reformat.rst b/docs/sphinx/samples/operators/reformat.rst index c236a3e18..8f4c7b526 100644 --- a/docs/sphinx/samples/operators/reformat.rst +++ b/docs/sphinx/samples/operators/reformat.rst @@ -1,5 +1,5 @@ .. - # SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/docs/sphinx/samples/operators/remap.rst b/docs/sphinx/samples/operators/remap.rst new file mode 100644 index 000000000..e66439a96 --- /dev/null +++ b/docs/sphinx/samples/operators/remap.rst @@ -0,0 +1,147 @@ +.. + # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-License-Identifier: Apache-2.0 + # + # Licensed 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. + +.. _sample_remap: + +Remap +===== + +Overview +-------- + +The Remap sample demonstrates GPU-accelerated pixel remapping using CV-CUDA's ``remap`` operator. +A coordinate map is built on the CPU with NumPy — a sinusoidal wave-distortion field — then +uploaded to the GPU and applied to the source image. The result is a ripple-distorted version of +the input that is saved as a viewable JPEG. + +Usage +----- + +Basic Usage +^^^^^^^^^^^ + +Apply the default wave distortion to the built-in test image: + +.. code-block:: bash + + python3 remap.py + +Custom Input +^^^^^^^^^^^^ + +Remap a custom source image and write to a custom output path: + +.. code-block:: bash + + python3 remap.py -i input.jpg -o cat_remap.jpg + +Command-Line Arguments +---------------------- + +.. list-table:: + :header-rows: 1 + :widths: 20 15 15 50 + + * - Argument + - Short Form + - Default + - Description + * - ``--input`` + - ``-i`` + - tabby_tiger_cat.jpg + - Input image file path + * - ``--output`` + - ``-o`` + - cvcuda/.cache/cat_remap.jpg + - Output image file path + +Implementation +-------------- + +Building the Displacement Map +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. literalinclude:: ../../../../samples/operators/remap.py + :language: python + :start-after: docs_tag: begin_remap_setup + :end-before: docs_tag: end_remap_setup + :dedent: + +Applying the Remap Operator +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. literalinclude:: ../../../../samples/operators/remap.py + :language: python + :start-after: docs_tag: begin_remap + :end-before: docs_tag: end_remap + :dedent: + +Key points: + +1. **Map tensor shape**: The coordinate map uses shape ``(H, W, 2)`` with dtype ``F32`` — two + float channels storing ``[src_x, src_y]`` absolute source coordinates per output pixel. +2. **Map type — ABSOLUTE**: ``cvcuda.Remap.ABSOLUTE`` means each map value is an un-normalized + ``(x, y)`` pixel coordinate in the source image, giving full control over the displacement. +3. **Source interpolation**: ``src_interp=LINEAR`` smooths the sampled source values for a + continuous displacement field; ``NEAREST`` is faster when sub-pixel accuracy is not needed. +4. **Border policy**: ``border=REPLICATE`` avoids black edges at the image boundary by repeating + the nearest border pixel, keeping the output perceptually clean. +5. **Host-to-device upload**: The NumPy map array is transferred to a pre-allocated ``cvcuda.Tensor`` + via ``cuda_memcpy_h2d`` — the same pattern used by other samples that synthesize GPU inputs. + +Expected Output +^^^^^^^^^^^^^^^ + +The output shows the image with a sinusoidal wave distortion applied: + +.. list-table:: + :widths: 50 50 + :align: center + + * - .. figure:: ../../content/tabby_tiger_cat.jpg + :width: 100% + + Original Input Image + + - .. figure:: ../../content/cat_remap.jpg + :width: 100% + + Output: Wave-Distorted Image + +CV-CUDA Operators Used +^^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Operator + - Purpose + * - :py:func:`cvcuda.remap` + - Warp an image using an arbitrary (H, W, 2) coordinate map + +Common Utilities Used +^^^^^^^^^^^^^^^^^^^^^ + +* :ref:`read_image() ` - Load image as CV-CUDA tensor +* :ref:`write_image() ` - Save remapped image +* ``cuda_memcpy_h2d`` - Upload the NumPy coordinate map to the GPU tensor + +See Also +-------- + +* :ref:`Resize Operator ` - Simple spatial scaling +* :ref:`Common Utilities ` - Helper functions diff --git a/docs/sphinx/samples/operators/resize.rst b/docs/sphinx/samples/operators/resize.rst index f72cee890..ee2304965 100644 --- a/docs/sphinx/samples/operators/resize.rst +++ b/docs/sphinx/samples/operators/resize.rst @@ -1,5 +1,5 @@ .. - # SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -138,5 +138,5 @@ See Also * :ref:`Hello World Sample ` - Uses resize in pipeline * :ref:`Classification Sample ` - Resizes for model input -* :ref:`Reformat Operator ` - Change tensor layouts +* :ref:`Crop, Flip, Normalize, Reformat ` - Change tensor layouts * :ref:`Common Utilities ` - Helper functions diff --git a/docs/sphinx/samples/operators/resize_crop_convert_reformat.rst b/docs/sphinx/samples/operators/resize_crop_convert_reformat.rst new file mode 100644 index 000000000..3a1e8099c --- /dev/null +++ b/docs/sphinx/samples/operators/resize_crop_convert_reformat.rst @@ -0,0 +1,159 @@ +.. + # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-License-Identifier: Apache-2.0 + # + # Licensed 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. + +.. _sample_resize_crop_convert_reformat: + +Resize Crop Convert Reformat +============================= + +Overview +-------- + +The Resize Crop Convert Reformat sample demonstrates a fused preprocessing pipeline using +CV-CUDA's GPU-accelerated :py:func:`cvcuda.resize_crop_convert_reformat` operator. In a +single kernel the operator: + +* Resizes the input image to a target dimension using linear or nearest-neighbor + interpolation, +* Crops a rectangular region of interest from the resized result, +* Converts the pixel data type (e.g. ``uint8`` → ``float32``), and +* Reformats the memory layout (e.g. ``NHWC`` → ``NCHW``) and optionally reverses + the channel order (BGR ↔ RGB). + +This mirrors the standard ImageNet-style pre-processing pipeline that DL inference +frameworks apply before feeding images to a convolutional network. + +Usage +----- + +Basic Usage +^^^^^^^^^^^ + +Run with the default 224×224 resize / 224×224 crop: + +.. code-block:: bash + + python3 resize_crop_convert_reformat.py -i input.jpg + +Custom Dimensions +^^^^^^^^^^^^^^^^^ + +Specify a different resize and crop target (crop is always the full ``--width`` × +``--height`` window starting at the top-left corner): + +.. code-block:: bash + + python3 resize_crop_convert_reformat.py -i input.jpg -o result.jpg --width 512 --height 512 + +Command-Line Arguments +---------------------- + +.. list-table:: + :header-rows: 1 + :widths: 20 15 15 50 + + * - Argument + - Short Form + - Default + - Description + * - ``--input`` + - ``-i`` + - tabby_tiger_cat.jpg + - Input image file path + * - ``--output`` + - ``-o`` + - cvcuda/.cache/cat_resize_crop_convert_reformat.jpg + - Output image file path + * - ``--width`` + - + - 224 + - Target crop width in pixels (also used as resize width) + * - ``--height`` + - + - 224 + - Target crop height in pixels (also used as resize height) + +Implementation +-------------- + +Fused Pipeline +^^^^^^^^^^^^^^ + +.. literalinclude:: ../../../../samples/operators/resize_crop_convert_reformat.py + :language: python + :start-after: docs_tag: begin_resize_crop_convert_reformat + :end-before: docs_tag: end_resize_crop_convert_reformat + :dedent: + +Key points: + +1. **Single-kernel fusion**: resize, crop, type conversion, layout reformat, and optional + channel reversal all happen in one GPU pass, avoiding intermediate allocations and + memory bandwidth waste. +2. **``layout="NCHW"``**: the output tensor uses channel-first memory order, which is the + format expected by most deep-learning inference runtimes (TensorRT, ONNX Runtime, etc.). +3. **``manip=cvcuda.ChannelManip.REVERSE``**: swaps BGR ↔ RGB in the same pass, which is + needed when the codec reads BGR and the model was trained on RGB (or vice versa). +4. **``scale`` and ``offset``**: optional linear normalisation ``output = pixel / scale + offset`` + applied after type conversion; set ``scale=127.5, offset=-1`` for ``[-1, 1]`` + normalisation used by many classification and detection models. +5. **Host-side reformat for saving**: because the image encoder expects HWC uint8, the + NCHW float32 result is transposed and clipped on the CPU before writing. + +Expected Output +^^^^^^^^^^^^^^^ + +The output shows the input image resized to 224×224, cropped to 224×224, and returned +to a viewable HWC uint8 format for saving: + +.. list-table:: + :widths: 50 50 + :align: center + + * - .. figure:: ../../content/tabby_tiger_cat.jpg + :width: 100% + + Original Input Image + + - .. figure:: ../../content/cat_resize_crop_convert_reformat.jpg + :width: 100% + + Output: Resized, Cropped, and Reformatted + +CV-CUDA Operators Used +^^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Operator + - Purpose + * - :py:func:`cvcuda.resize_crop_convert_reformat` + - Fused resize → crop → type convert → layout reformat in a single GPU kernel + +Common Utilities Used +^^^^^^^^^^^^^^^^^^^^^ + +* :ref:`read_image() ` - Load image as CV-CUDA tensor +* :ref:`write_image() ` - Save result image +* ``cuda_memcpy_d2h`` / ``cuda_memcpy_h2d`` - Transfer tensor data between GPU and CPU for host-side reformat + +See Also +-------- + +* :ref:`Resize Operator ` - Simple image resize +* :ref:`Common Utilities ` - Helper functions diff --git a/docs/sphinx/samples/operators/rotate.rst b/docs/sphinx/samples/operators/rotate.rst new file mode 100644 index 000000000..0c65aa5e1 --- /dev/null +++ b/docs/sphinx/samples/operators/rotate.rst @@ -0,0 +1,135 @@ +.. + # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-License-Identifier: Apache-2.0 + # + # Licensed 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. + +.. _sample_rotate: + +Rotate +====== + +Overview +-------- + +The Rotate sample demonstrates GPU-accelerated image rotation using CV-CUDA's rotate operator. +It reads an input image, computes a centring shift so the rotated content stays visible, and +writes the result as a standard uint8 JPEG. + +Usage +----- + +Basic Usage +^^^^^^^^^^^ + +Rotate the default tabby-cat image by 45 degrees: + +.. code-block:: bash + + python3 rotate.py + +Custom Input +^^^^^^^^^^^^ + +Rotate a specific image and save to a custom output path: + +.. code-block:: bash + + python3 rotate.py -i input.jpg -o cat_rotate.jpg + +Command-Line Arguments +---------------------- + +.. list-table:: + :header-rows: 1 + :widths: 20 15 15 50 + + * - Argument + - Short Form + - Default + - Description + * - ``--input`` + - ``-i`` + - tabby_tiger_cat.jpg + - Input image file path + * - ``--output`` + - ``-o`` + - cvcuda/.cache/cat_rotate.jpg + - Output image file path + +Implementation +-------------- + +Centred Rotation +^^^^^^^^^^^^^^^^ + +.. literalinclude:: ../../../../samples/operators/rotate.py + :language: python + :start-after: docs_tag: begin_rotate + :end-before: docs_tag: end_rotate + :dedent: + +Key points: + +1. **Rotation origin**: ``cvcuda.rotate`` rotates around the top-left corner, so a compensating + translation shift must be provided to keep the image content centred. +2. **Centring shift**: The shift ``(cx - cx*cos - cy*sin, cy - cy*cos + cx*sin)`` is derived from + the standard 2-D rotation-about-centre formula. +3. **Interpolation**: ``cvcuda.Interp.LINEAR`` gives smooth results; ``NEAREST`` is faster and + ``CUBIC`` provides higher quality at the cost of more computation. +4. **Output shape and dtype**: The output tensor has the same spatial dimensions and data type as + the input — no host-side conversion is needed. + +Expected Output +^^^^^^^^^^^^^^^ + +The output shows the image rotated 45 degrees with the content kept centred: + +.. list-table:: + :widths: 50 50 + :align: center + + * - .. figure:: ../../content/tabby_tiger_cat.jpg + :width: 100% + + Original Input Image + + - .. figure:: ../../content/cat_rotate.jpg + :width: 100% + + Output: Rotated 45 degrees (centred) + +CV-CUDA Operators Used +^^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Operator + - Purpose + * - :py:func:`cvcuda.rotate` + - Rotate an image by an arbitrary angle with configurable interpolation + +Common Utilities Used +^^^^^^^^^^^^^^^^^^^^^ + +* :ref:`read_image() ` - Load image as CV-CUDA tensor +* :ref:`write_image() ` - Save rotated image + +See Also +-------- + +* :ref:`Resize Operator ` - Resize images to target dimensions +* :ref:`Warp Affine Operator ` - General affine transformations +* :ref:`Common Utilities ` - Helper functions diff --git a/docs/sphinx/samples/operators/stack.rst b/docs/sphinx/samples/operators/stack.rst index dbdd6c04d..7bbd0b0f2 100644 --- a/docs/sphinx/samples/operators/stack.rst +++ b/docs/sphinx/samples/operators/stack.rst @@ -1,5 +1,5 @@ .. - # SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/docs/sphinx/samples/operators/threshold.rst b/docs/sphinx/samples/operators/threshold.rst new file mode 100644 index 000000000..b1c85957d --- /dev/null +++ b/docs/sphinx/samples/operators/threshold.rst @@ -0,0 +1,140 @@ +.. + # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-License-Identifier: Apache-2.0 + # + # Licensed 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. + +.. _sample_threshold: + +Threshold +========= + +Overview +-------- + +The Threshold sample demonstrates pixel-level intensity thresholding using CV-CUDA's GPU-accelerated +threshold operator. A binary threshold is applied: every pixel whose value exceeds a configurable +threshold is set to a maximum value (255), and all others are set to 0, producing a clean binary mask. + +Usage +----- + +Basic Usage +^^^^^^^^^^^ + +Apply a binary threshold to an image (default threshold = 128): + +.. code-block:: bash + + python3 threshold.py -i input.jpg + +Custom Output Path +^^^^^^^^^^^^^^^^^^ + +Specify a custom output file: + +.. code-block:: bash + + python3 threshold.py -i input.jpg -o thresholded.jpg + +Command-Line Arguments +---------------------- + +.. list-table:: + :header-rows: 1 + :widths: 20 15 15 50 + + * - Argument + - Short Form + - Default + - Description + * - ``--input`` + - ``-i`` + - tabby_tiger_cat.jpg + - Input image file path + * - ``--output`` + - ``-o`` + - cvcuda/.cache/cat_threshold.jpg + - Output image file path + +Implementation +-------------- + +Threshold Operator +^^^^^^^^^^^^^^^^^^ + +.. literalinclude:: ../../../../samples/operators/threshold.py + :language: python + :start-after: docs_tag: begin_threshold + :end-before: docs_tag: end_threshold + :dedent: + +Key points: + +1. **Batch dimension required**: ``cvcuda.threshold`` expects NHWC layout, so a HWC image must be + reshaped with a leading batch dimension before calling the operator. +2. **Per-image parameters**: ``thresh`` and ``maxval`` are GPU tensors of shape ``(N,)`` and dtype + ``F64``, allowing each image in a batch to use a different threshold value. +3. **Upload via cuda_memcpy_h2d**: NumPy arrays holding the scalar parameters are copied to GPU + memory using ``cuda_memcpy_h2d`` before the operator is called. +4. **BINARY type**: ``cvcuda.ThresholdType.BINARY`` sets pixels above the threshold to ``maxval`` + and all others to zero; other types (``BINARY_INV``, ``TRUNC``, ``TOZERO``, ``TOZERO_INV``, + ``OTSU``, ``TRIANGLE``) are also available. +5. **Output shape preserved**: The operator returns a tensor with the same shape, layout, and dtype + as the input, which is reshaped back to HWC before writing. + +Expected Output +^^^^^^^^^^^^^^^ + +The output is a binary image where bright regions (pixel value > 128) appear white and dark regions +appear black: + +.. list-table:: + :widths: 50 50 + :align: center + + * - .. figure:: ../../content/tabby_tiger_cat.jpg + :width: 100% + + Original Input Image + + - .. figure:: ../../content/cat_threshold.jpg + :width: 100% + + Output: Binary Threshold (thresh=128, maxval=255) + +CV-CUDA Operators Used +^^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Operator + - Purpose + * - :py:func:`cvcuda.threshold` + - Apply pixel-intensity thresholding with configurable per-image threshold and maxval + +Common Utilities Used +^^^^^^^^^^^^^^^^^^^^^ + +* :ref:`read_image() ` - Load image as CV-CUDA tensor +* :ref:`write_image() ` - Save thresholded image +* ``cuda_memcpy_h2d`` - Upload per-image threshold and maxval scalars to GPU memory + +See Also +-------- + +* :ref:`Resize Operator ` - Basic GPU image resize +* :ref:`Label Operator ` - Connected-components labeling using threshold as preprocessing +* :ref:`Common Utilities ` - Helper functions diff --git a/docs/sphinx/samples/operators/warp_affine.rst b/docs/sphinx/samples/operators/warp_affine.rst new file mode 100644 index 000000000..637126e62 --- /dev/null +++ b/docs/sphinx/samples/operators/warp_affine.rst @@ -0,0 +1,145 @@ +.. + # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-License-Identifier: Apache-2.0 + # + # Licensed 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. + +.. _sample_warp_affine: + +Warp Affine +=========== + +Overview +-------- + +The Warp Affine sample demonstrates GPU-accelerated affine image transformation using CV-CUDA's +``warp_affine`` operator. The sample builds a 2×3 float32 transformation matrix that rotates the +image 15 degrees counter-clockwise about its centre and shifts it slightly to the right, then +applies it with bilinear interpolation and constant-value border filling. + +Usage +----- + +Basic Usage +^^^^^^^^^^^ + +Apply the default rotation+translation warp to an image: + +.. code-block:: bash + + python3 warp_affine.py -i input.jpg + +Custom Output Path +^^^^^^^^^^^^^^^^^^ + +Write the warped result to a specific location: + +.. code-block:: bash + + python3 warp_affine.py -i input.jpg -o warped.jpg + +Command-Line Arguments +---------------------- + +.. list-table:: + :header-rows: 1 + :widths: 20 15 15 50 + + * - Argument + - Short Form + - Default + - Description + * - ``--input`` + - ``-i`` + - tabby_tiger_cat.jpg + - Input image file path + * - ``--output`` + - ``-o`` + - cvcuda/.cache/cat_warp_affine.jpg + - Output image file path + +Implementation +-------------- + +Affine Matrix Setup +^^^^^^^^^^^^^^^^^^^ + +.. literalinclude:: ../../../../samples/operators/warp_affine.py + :language: python + :start-after: docs_tag: begin_warp_affine_setup + :end-before: docs_tag: end_warp_affine_setup + :dedent: + +Warp Affine Call +^^^^^^^^^^^^^^^^ + +.. literalinclude:: ../../../../samples/operators/warp_affine.py + :language: python + :start-after: docs_tag: begin_warp_affine + :end-before: docs_tag: end_warp_affine + :dedent: + +Key points: + +1. **2×3 Matrix**: ``xform`` is a ``np.float32`` array of shape ``(2, 3)`` that encodes the full + affine map (rotation, scale, shear, translation) in one compact structure. +2. **Centre-relative rotation**: Translating to the image centre before rotating avoids the image + drifting off-canvas; the standard formula embeds the centre correction directly in the + translation column of the matrix. +3. **Interpolation flag**: ``cvcuda.Interp.LINEAR`` gives smooth bilinear interpolation; + ``NEAREST`` is faster but produces aliasing artefacts on smooth gradients. +4. **Border handling**: ``cvcuda.Border.CONSTANT`` with ``border_value=[0]`` fills any pixels that + map outside the source image with black — useful for preserving the original framing. + +Expected Output +^^^^^^^^^^^^^^^ + +The output shows the image rotated 15 degrees counter-clockwise with a small rightward translation: + +.. list-table:: + :widths: 50 50 + :align: center + + * - .. figure:: ../../content/tabby_tiger_cat.jpg + :width: 100% + + Original Input Image + + - .. figure:: ../../content/cat_warp_affine.jpg + :width: 100% + + Output: Rotated 15° and shifted right + +CV-CUDA Operators Used +^^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Operator + - Purpose + * - :py:func:`cvcuda.warp_affine` + - Apply a 2×3 affine transformation matrix to an image with configurable interpolation and border handling + +Common Utilities Used +^^^^^^^^^^^^^^^^^^^^^ + +* :ref:`read_image() ` - Load image as CV-CUDA tensor +* :ref:`write_image() ` - Save warped image + +See Also +-------- + +* :ref:`Resize Operator ` - GPU-accelerated image resize +* :ref:`Common Utilities ` - Helper functions diff --git a/docs/sphinx/samples/operators/warp_perspective.rst b/docs/sphinx/samples/operators/warp_perspective.rst new file mode 100644 index 000000000..6f0c8cbe1 --- /dev/null +++ b/docs/sphinx/samples/operators/warp_perspective.rst @@ -0,0 +1,145 @@ +.. + # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-License-Identifier: Apache-2.0 + # + # Licensed 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. + +.. _sample_warp_perspective: + +Warp Perspective +================ + +Overview +-------- + +The Warp Perspective sample demonstrates GPU-accelerated perspective transform using CV-CUDA's +``warp_perspective`` operator. A 3×3 homography matrix maps every destination pixel back to its +source location, enabling keystone correction, bird's-eye-view synthesis, and other projective +geometry tasks. + +Usage +----- + +Basic Usage +^^^^^^^^^^^ + +Apply a default mild-keystone perspective transform: + +.. code-block:: bash + + python3 warp_perspective.py -i input.jpg + +Custom Output Path +^^^^^^^^^^^^^^^^^^ + +Save the warped result to a specific file: + +.. code-block:: bash + + python3 warp_perspective.py -i input.jpg -o cat_warp_perspective.jpg + +Command-Line Arguments +---------------------- + +.. list-table:: + :header-rows: 1 + :widths: 20 15 15 50 + + * - Argument + - Short Form + - Default + - Description + * - ``--input`` + - ``-i`` + - tabby_tiger_cat.jpg + - Input image file path + * - ``--output`` + - ``-o`` + - cvcuda/.cache/cat_warp_perspective.jpg + - Output image file path + +Implementation +-------------- + +Perspective Matrix Setup +^^^^^^^^^^^^^^^^^^^^^^^^ + +.. literalinclude:: ../../../../samples/operators/warp_perspective.py + :language: python + :start-after: docs_tag: begin_warp_perspective_setup + :end-before: docs_tag: end_warp_perspective_setup + :dedent: + +Warp Perspective Call +^^^^^^^^^^^^^^^^^^^^^ + +.. literalinclude:: ../../../../samples/operators/warp_perspective.py + :language: python + :start-after: docs_tag: begin_warp_perspective + :end-before: docs_tag: end_warp_perspective + :dedent: + +Key points: + +1. **3×3 float32 matrix**: ``warp_perspective`` expects a 3×3 homography matrix, either as a + nested Python list or a float32 NumPy array. The matrix relates homogeneous destination + coordinates to homogeneous source coordinates. +2. **WARP_INVERSE_MAP flag**: When this flag is combined with the interpolation mode the matrix + is interpreted as a destination→source mapping, which is how the standard DLT construction + works. Without the flag the operator inverts the matrix internally. +3. **Border mode**: ``cvcuda.Border.CONSTANT`` fills pixels that map outside the source image + with the ``border_value``; ``REPLICATE`` and ``WRAP`` are also supported. +4. **Batch support**: Pass an ``ImageBatch`` and a ``(N, 9)`` float32 transform tensor to apply + per-image perspective matrices in a single call. + +Expected Output +^^^^^^^^^^^^^^^ + +.. list-table:: + :widths: 50 50 + :align: center + + * - .. figure:: ../../content/tabby_tiger_cat.jpg + :width: 100% + + Original Input Image + + - .. figure:: ../../content/cat_warp_perspective.jpg + :width: 100% + + Output: Perspective-warped image + +CV-CUDA Operators Used +^^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Operator + - Purpose + * - :py:func:`cvcuda.warp_perspective` + - Apply a 3×3 homography perspective transform to an image + +Common Utilities Used +^^^^^^^^^^^^^^^^^^^^^ + +* :ref:`read_image() ` - Load image as CV-CUDA tensor +* :ref:`write_image() ` - Save perspective-warped image +* :ref:`parse_image_args() ` - Parse ``--input`` / ``--output`` CLI arguments + +See Also +-------- + +* :ref:`Resize Operator ` - Simple geometric scaling +* :ref:`Common Utilities ` - Helper functions used across samples diff --git a/docs/sphinx/wsl2.rst b/docs/sphinx/wsl2.rst index 043f368fe..4a4a1f3fe 100644 --- a/docs/sphinx/wsl2.rst +++ b/docs/sphinx/wsl2.rst @@ -1,5 +1,5 @@ .. - # SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + # SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,6 +14,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +:orphan: + .. _wsl2: WSL2 Setup @@ -29,7 +31,7 @@ More information about WSL2 and using CUDA can be found on this page `CUDA Toolk For installation instructions, you can refer to the following links: - `CUDA Toolkit 13.0`_, if you have driver r580 or later -- `CUDA Toolkit 12.8`_, if you have driver r525 or later +- `CUDA Toolkit 12.8`_, if you have driver r550 or later 2. Setup Environment Variables ------------------------------ diff --git a/generate_requirements.sh b/generate_requirements.sh new file mode 100755 index 000000000..0fb3de4b8 --- /dev/null +++ b/generate_requirements.sh @@ -0,0 +1,128 @@ +#!/bin/bash +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +# Generate requirements .txt files from *.template sources and versions.env. +# Requirements outputs are written next to their templates across the tree. +# Each *.template file uses ${var_name} placeholders defined in versions.env. +# +# Usage: +# bash generate_requirements.sh # generate all managed dependency files +# bash generate_requirements.sh --check # verify files are up to date (exits 1 if stale) + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +TEMPLATES=( + docker/requirements.build.all_pythons.template + docker/requirements.build.sys_python.template + tests/requirements.tests.common.template + tests/requirements.tests.cu12.template + tests/requirements.tests.cu12.numpy1.template + tests/requirements.tests.cu13.template + tests/requirements.tests.numpy1.template + tests/requirements.tests.numpy2.template + bench/python/requirements.bench.common.template + bench/python/requirements.bench.cu12.template + bench/python/requirements.bench.cu13.template + samples/requirements.samples.common.template + samples/requirements.samples.cu12.template + samples/requirements.samples.cu13.template + samples/requirements.samples.hello_world_cu12.template + samples/requirements.samples.hello_world_cu13.template + docs/requirements.docs.template +) + +# Render a template to stdout: strip the "Template for …" comment block and +# substitute ${var_name} placeholders with values from versions.env. +_render() { + local tpl="$1" + local tpl_name sed_args=() + tpl_name=$(basename "$tpl") + + while IFS='=' read -r key value; do + # Strip inline comments (# and everything after) and trailing whitespace + value="${value%%#*}" + value="${value%"${value##*[^[:space:]]}"}" + [[ -z "$value" ]] && continue + sed_args+=(-e "s|\${${key}}|${value}|g") + done < <(grep -v '^[[:space:]]*#' "$REPO_ROOT/versions.env" | grep '=') + + printf '# AUTO-GENERATED \xe2\x80\x94 do not edit directly.\n' + printf '# Edit %s or versions.env, then run: bash generate_requirements.sh\n' "$tpl_name" + printf '\n' + + awk ' + /^# Template for / { skip = 1 } + skip && /generate_requirements/ { skip = 2; next } + skip == 2 && /^$/ { skip = 0; next } + skip { next } + { print } + ' "$tpl" | sed "${sed_args[@]}" + return $? +} + + +# ── Main ────────────────────────────────────────────────────────────────────── + +check_mode=0 +[[ "${1:-}" == "--check" ]] && check_mode=1 + +stale=() +skipped=0 +for tpl_rel in "${TEMPLATES[@]}"; do + tpl="$REPO_ROOT/$tpl_rel" + out="${tpl%.template}.txt" + + # Tolerate sparse checkouts: CI test pods that only need a subset of + # the tree (e.g. test-benchmarks pulls just bench/) won't have every + # template on disk. Skip what isn't present in both modes. + if [[ ! -f "$tpl" ]]; then + printf ' skip %s (template not present — partial checkout?)\n' "$tpl_rel" + skipped=$((skipped + 1)) + continue + fi + + if (( check_mode )); then + tmp=$(mktemp) + _render "$tpl" > "$tmp" + if ! cmp -s "$tmp" "$out" 2>/dev/null; then + stale+=("${out#"$REPO_ROOT/"}") + fi + rm -f "$tmp" + else + _render "$tpl" > "$out" + printf ' wrote %s\n' "${out#"$REPO_ROOT/"}" + fi +done + + +if (( check_mode )); then + if (( ${#stale[@]} > 0 )); then + printf '\n%d managed dependency file(s) are out of date or missing:\n' "${#stale[@]}" >&2 + printf ' %s\n' "${stale[@]}" >&2 + printf '\nRun: bash generate_requirements.sh\n' >&2 + exit 1 + else + checked=$(( ${#TEMPLATES[@]} - skipped )) + if (( skipped > 0 )); then + printf 'OK \xe2\x80\x94 %d/%d requirements files up to date (%d skipped — partial checkout).\n' \ + "$checked" "${#TEMPLATES[@]}" "$skipped" + else + printf 'OK \xe2\x80\x94 all %d requirements files are up to date.\n' "${#TEMPLATES[@]}" + fi + fi +fi diff --git a/init_repo.sh b/init_repo.sh index d26cc64f0..82508ed5a 100755 --- a/init_repo.sh +++ b/init_repo.sh @@ -1,6 +1,6 @@ #!/bin/bash -e -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,7 +23,11 @@ distro_ver=$(lsb_release -rs || true) function version_le() { - [[ $(echo -e "$1\n$2" | sort -V | head -n1) = "$1" ]] && echo true + local version="$1" + local limit="$2" + + [[ $(echo -e "$version\n$limit" | sort -V | head -n1) = "$version" ]] && echo true + return $? } skip_precommit=0 @@ -52,7 +56,7 @@ if ! which git-lfs ; then exit 1 fi -cd "$SDIR" +cd "$SDIR" # repo root # We use LFS git lfs install @@ -60,10 +64,11 @@ git lfs install # Fetch all lfs object git lfs fetch && git lfs checkout -# We use submodules -git submodule update --init +# Regenerate requirements files from versions.env (single source of truth) +echo "Regenerating requirements files from versions.env..." +bash generate_requirements.sh -if [ "$skip_precommit" -ne 1 ]; then +if [[ "$skip_precommit" -ne 1 ]]; then # allow-missing-config is useful when checking out an old commit or a branch that don't have pre-config configuration. pre-commit install \ --allow-missing-config \ diff --git a/print_env.sh b/print_env.sh index 50b686645..a1201f11e 100755 --- a/print_env.sh +++ b/print_env.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -35,10 +35,193 @@ cat /etc/*-release uname -a echo +echo "***Docker Image***" +echo "${DOCKER_IMAGE_FULL:-(unset; not running in CI container?)}" +echo + echo "***GPU Information***" nvidia-smi echo +# Always-on driver-stack snapshot + Container Toolkit / CDI injection check. +# +# Background: when nvidia-container-runtime / CDI works correctly, the host's +# libcuda + libnvidia-*.so.* libs are bind-mounted into the container under +# /usr/lib/x86_64-linux-gnu/ (or similar host path). When this injection +# silently fails on a node, ldconfig falls back to the container-bundled +# /usr/local/cuda-*/compat/libcuda.so. forward-compat shim. That shim is +# meant to span across major driver branches (e.g., R535 LTS userspace → +# R595 kernel), not within the same branch — so when it ends up paired with +# a kernel module whose patch level it wasn't built against, every cuda +# runtime call returns "unsupported display driver / cuda driver combination". +# +# Detection: glob for libcuda.so.* outside */compat/ and check whether any +# version matches the kernel NVRM. Empty match = injection failed. We also +# count libnvidia-*.so.* (the toolkit normally bind-mounts ~15+) as a +# secondary fingerprint. +echo "***Driver Stack***" +nvrm_ver="" +if [ -r /proc/driver/nvidia/version ]; then + awk '/NVRM version/{sub(/^[^:]+:[[:space:]]+/,""); print "Kernel NVRM module: " $0; exit}' /proc/driver/nvidia/version + nvrm_ver=$(awk '/NVRM version/{for(i=1;i<=NF;i++) if($i ~ /^[0-9]+\.[0-9]/) {print $i; exit}}' /proc/driver/nvidia/version) +else + echo "Kernel NVRM module: (not readable from container)" +fi +ud=$(nvidia-smi --query-gpu=driver_version --format=csv,noheader 2>/dev/null | head -1) +echo "Userspace driver (smi): ${ud:-unknown}" +libcuda_path=$(ldconfig -p 2>/dev/null | awk '/libcuda\.so\.1 /{print $NF; exit}') +echo "libcuda.so.1 (ldconfig): ${libcuda_path:-not found}" +if [ -n "$libcuda_path" ]; then + libcuda_real=$(readlink -f "$libcuda_path" 2>/dev/null) + [ -n "$libcuda_real" ] && [ "$libcuda_real" != "$libcuda_path" ] && echo "libcuda.so.1 (resolved): $libcuda_real" +fi + +# Container Toolkit / CDI injection probe +host_libcuda_files=$(find /usr/lib /lib /opt -maxdepth 6 -name 'libcuda.so.[0-9]*' \ + -not -path '*/compat/*' 2>/dev/null | sort -u) +compat_libcuda_files=$(find /usr/local -maxdepth 6 -path '*/compat/libcuda.so.[0-9]*' 2>/dev/null | sort -u) +libnv_count=$(find /usr/lib /lib -maxdepth 4 -name 'libnvidia-*.so.[0-9]*' \ + -not -path '*/compat/*' 2>/dev/null | wc -l) + +echo "Host libcuda.so.* visible (toolkit-injected, non-compat):" +if [ -z "$host_libcuda_files" ]; then + echo " (none)" +else + echo "$host_libcuda_files" | sed 's/^/ /' +fi +echo "Compat libcuda.so.* (container-bundled forward-compat shim):" +if [ -z "$compat_libcuda_files" ]; then + echo " (none)" +else + echo "$compat_libcuda_files" | sed 's/^/ /' +fi +echo "libnvidia-*.so.* in /usr/lib (toolkit normally injects 15+): ${libnv_count}" + +# /etc/ld.so.conf.d/ ordering is the actual discriminator: when the host +# libcuda is bind-mounted (which it usually IS, even on broken nodes) but +# /etc/ld.so.conf.d/cuda-compat-*.conf ranks ahead of nvidia*.conf, +# ldconfig caches the compat libcuda first and the cuda runtime loads it +# instead of the host one — yielding the "unsupported display driver / cuda +# driver combination" error even though the host libcuda was perfectly fine. +echo "ld.so.conf.d entries (search-path order — *cuda-compat* before host nvidia.conf is the bug):" +if [ -d /etc/ld.so.conf.d ]; then + ls -1 /etc/ld.so.conf.d 2>/dev/null | sed 's/^/ /' +else + echo " (no /etc/ld.so.conf.d directory)" +fi + +# Verdict: what does ldconfig ACTUALLY pick for libcuda.so.1? +# - host path + version matches kernel NVRM → OK +# - /compat/ path → FAIL (regardless of whether host libcuda +# is also present; on the broken nodes it usually IS, just shadowed) +# - host path but version mismatch → DEGRADED +ld_pick_real="" +if [ -n "$libcuda_path" ]; then + ld_pick_real=$(readlink -f "$libcuda_path" 2>/dev/null || echo "$libcuda_path") +fi +ld_pick_ver="" +if [ -n "$ld_pick_real" ]; then + ld_pick_ver=$(basename "$ld_pick_real" | sed 's/^libcuda\.so\.//') +fi + +# Find the host libcuda matching the kernel (the "expected" pick) and the +# offending ld.so.conf.d entry that pushes compat ahead, for the verdict body. +expected_libcuda="" +if [ -n "$nvrm_ver" ] && [ -n "$host_libcuda_files" ]; then + while IFS= read -r f; do + [ -z "$f" ] && continue + real=$(readlink -f "$f" 2>/dev/null || echo "$f") + fver=$(basename "$real" | sed 's/^libcuda\.so\.//') + if [ "$fver" = "$nvrm_ver" ]; then + expected_libcuda="$real" + break + fi + done <<< "$host_libcuda_files" +fi +offending_conf="" +for _f in /etc/ld.so.conf.d/*compat*; do + [ -e "$_f" ] || break + offending_conf=$(basename "$_f") + break +done + +echo -n "Toolkit injection verdict: " +if [ -z "$libcuda_path" ]; then + echo "UNKNOWN — ldconfig could not locate libcuda.so.1" +elif [[ "$ld_pick_real" == */compat/* ]]; then + if [ -n "$expected_libcuda" ]; then + echo "FAIL — loaded libcuda ${ld_pick_ver} != libcuda ${nvrm_ver} (expected)" + echo " loaded: ${ld_pick_real} (CUDA forward-compat shim)" + echo " expected: ${expected_libcuda} (host driver, matches kernel NVRM ${nvrm_ver})" + echo " cause: /etc/ld.so.conf.d/${offending_conf:-cuda-compat*.conf} ranks above nvidia*.conf," + echo " so ldconfig caches the compat shim instead of the host libcuda." + echo " fix: rm /etc/ld.so.conf.d/${offending_conf:-cuda-compat-*.conf} && ldconfig" + echo " (or rename to 99-compat-*.conf so it ranks below nvidia*.conf)" + else + echo "FAIL — loaded libcuda ${ld_pick_ver} != kernel NVRM ${nvrm_ver:-unknown} (expected)" + echo " loaded: ${ld_pick_real} (CUDA forward-compat shim)" + echo " expected: host libcuda matching kernel NVRM ${nvrm_ver:-unknown} (NOT visible in container)" + echo " cause: nvidia-container-runtime did not bind-mount the host driver." + echo " fix: on the host node, regenerate CDI spec / restart container-runtime." + fi +elif [ -n "$nvrm_ver" ] && [ "$ld_pick_ver" = "$nvrm_ver" ]; then + echo "OK — loaded libcuda ${ld_pick_ver} == kernel NVRM ${nvrm_ver} (host driver in $(dirname "$ld_pick_real"))" +elif [ -z "$nvrm_ver" ]; then + echo "UNKNOWN — kernel NVRM version not readable (loaded libcuda ${ld_pick_ver})" +else + echo "DEGRADED — loaded libcuda ${ld_pick_ver} != kernel NVRM ${nvrm_ver}" + echo " loaded: ${ld_pick_real}" + echo " no host libcuda matching kernel ${nvrm_ver} found in container" +fi +echo + +echo "***CUDA Compute Smoke Test***" +if which nvcc &> /dev/null; then + # Write a minimal CUDA program that tests runtime context creation + CUDA_TEST=$(mktemp /tmp/cuda_smoke_XXXXXX.cu) + cat > "$CUDA_TEST" << 'CUDAEOF' +#include +#include +int main() { + int dev = -1, count = 0, failures = 0; + cudaError_t err; + err = cudaGetDeviceCount(&count); + printf("cudaGetDeviceCount: %s (%d devices)\n", cudaGetErrorString(err), count); + if (err != cudaSuccess || count == 0) failures++; + err = cudaGetDevice(&dev); + printf("cudaGetDevice: %s (device %d)\n", cudaGetErrorString(err), dev); + if (err != cudaSuccess) failures++; + void *ptr = nullptr; + err = cudaMalloc(&ptr, 256); + printf("cudaMalloc(256): %s (ptr=%p)\n", cudaGetErrorString(err), ptr); + if (err != cudaSuccess) failures++; + if (ptr) cudaFree(ptr); + cudaStream_t stream = nullptr; + err = cudaStreamCreate(&stream); + printf("cudaStreamCreate: %s\n", cudaGetErrorString(err)); + if (err != cudaSuccess) failures++; + if (stream) cudaStreamDestroy(stream); + if (failures > 0) { + printf("CUDA SMOKE TEST FAILED: %d of 4 checks failed\n", failures); + return 1; + } + printf("CUDA SMOKE TEST PASSED\n"); + return 0; +} +CUDAEOF + CUDA_BIN=$(mktemp /tmp/cuda_smoke_XXXXXX) + if nvcc -o "$CUDA_BIN" "$CUDA_TEST" -cudart shared -Xlinker -rpath,/usr/local/cuda/lib64 2>/dev/null; then + "$CUDA_BIN" + else + echo "Failed to compile CUDA smoke test" + fi + rm -f "$CUDA_TEST" "$CUDA_BIN" +else + echo "nvcc not found, skipping CUDA smoke test" +fi +echo + + echo "***CPU***" lscpu echo @@ -99,3 +282,154 @@ print_env | while read -r line; do echo " $line" done echo "" + +# When the smoke test fails, emit a tight copy-paste-ready incident ticket. +# Skips fields already printed above (full Driver Stack, smoke output) — the +# ticket only carries the load-bearing info: PROBLEM (loaded != expected), +# pod / build URL / GPU UUID-serial-bus, and the host-side fix command. The +# verbose Driver Stack section above is referenced as EVIDENCE. +print_smoke_diagnostics() { + local reason="$1" + # $2 (smoke stdout) is intentionally unused in the trimmed ticket; the + # full smoke output is already printed above in ***CUDA Compute Smoke Test*** + local ts pod node build_url gpu_line offending loaded_path loaded_ver expected_path nvrm_ver_local + + ts=$(date -u '+%Y-%m-%d %H:%M:%S UTC') + pod="${POD_NAME:-${HOSTNAME:-$(hostname 2>/dev/null || echo unknown)}}" + node="${K8S_NODE_NAME:-unknown}" + build_url="${BUILD_URL:-N/A}" + + # GPU one-liner: name uuid=... serial=... bus=... + gpu_line=$(nvidia-smi --query-gpu=name,uuid,serial,pci.bus_id --format=csv,noheader 2>/dev/null \ + | head -1 | awk -F', ' '{printf "%s uuid=%s serial=%s bus=%s", $1, $2, $3, $4}') + + # Re-derive the loaded vs expected libcuda paths and the offending conf + # file. Best-effort — if anything is missing the field renders as "?". + nvrm_ver_local=$(awk '/NVRM version/{for(i=1;i<=NF;i++) if($i ~ /^[0-9]+\.[0-9]/) {print $i; exit}}' /proc/driver/nvidia/version 2>/dev/null) + loaded_path=$(ldconfig -p 2>/dev/null | awk '/libcuda\.so\.1 /{print $NF; exit}') + [ -n "$loaded_path" ] && loaded_path=$(readlink -f "$loaded_path" 2>/dev/null || echo "$loaded_path") + loaded_ver=$(basename "${loaded_path:-libcuda.so.0}" | sed 's/^libcuda\.so\.//') + expected_path=$(find /usr/lib /lib -maxdepth 4 -name "libcuda.so.${nvrm_ver_local}" -not -path '*/compat/*' 2>/dev/null | head -1) + offending="" + for _f in /etc/ld.so.conf.d/*compat*; do + [ -e "$_f" ] || break + offending=$(basename "$_f") + break + done + + echo + echo "================== INCIDENT TICKET (paste into bug) ==================" + echo "PROBLEM: cvcuda CI smoke test failed." + if [[ "$loaded_path" == */compat/* ]] && [ -n "$expected_path" ]; then + echo " Loaded libcuda ${loaded_ver} != ${nvrm_ver_local} (expected — matches kernel NVRM)." + echo " Cause: /etc/ld.so.conf.d/${offending:-cuda-compat*.conf} shadows nvidia.conf." + else + echo " ${reason}" + echo " Loaded libcuda: ${loaded_path:-?} Kernel NVRM: ${nvrm_ver_local:-?}" + fi + echo + echo "WHEN: ${ts}" + echo "POD: ${pod}" + echo "NODE: ${node}" + echo "BUILD: ${build_url}" + echo "GPU: ${gpu_line:-?}" + echo + if [[ "$loaded_path" == */compat/* ]] && [ -n "$offending" ]; then + echo "FIX: On the broken node:" + echo " rm /etc/ld.so.conf.d/${offending} && ldconfig" + echo " (or rename to 99-${offending#*-} to deprioritize without removing)" + echo " Verify: docker run --rm --gpus all nvidia/cuda:13.3.0-base-ubuntu26.04 \\" + echo " bash -c 'ldconfig -p | grep libcuda.so.1'" + echo " should print /usr/lib/x86_64-linux-gnu/libcuda.so.1 (not /compat/)." + else + echo "FIX: Inspect the Driver Stack section above for the loaded vs expected" + echo " libcuda paths and the ld.so.conf.d listing." + fi + echo + echo "EVIDENCE: ***Driver Stack*** section above — FAIL verdict + full ld.so.conf.d listing." + echo "======================================================================" + echo +} + +# Re-run the CUDA smoke test outside the function to capture exit code +# (the one inside print_env runs in a subshell via pipe, so its exit code is lost) +if which nvcc &> /dev/null; then + CUDA_TEST=$(mktemp /tmp/cuda_check_XXXXXX.cu) + # Same checks as the verbose smoke test above, but emits one labeled line + # per cuda call so the gating-stage stdout is itself ticket-worthy. + cat > "$CUDA_TEST" << 'EOF2' +#include +#include +int main() { + int dev = -1, count = 0, failures = 0; + cudaError_t err; + err = cudaGetDeviceCount(&count); + printf("cudaGetDeviceCount: %s (%d devices)\n", cudaGetErrorString(err), count); + if (err != cudaSuccess || count == 0) failures++; + err = cudaGetDevice(&dev); + printf("cudaGetDevice: %s (device %d)\n", cudaGetErrorString(err), dev); + if (err != cudaSuccess) failures++; + void *ptr = 0; + err = cudaMalloc(&ptr, 256); + printf("cudaMalloc(256): %s (ptr=%p)\n", cudaGetErrorString(err), ptr); + if (err != cudaSuccess) failures++; + if (ptr) cudaFree(ptr); + cudaStream_t stream = 0; + err = cudaStreamCreate(&stream); + printf("cudaStreamCreate: %s\n", cudaGetErrorString(err)); + if (err != cudaSuccess) failures++; + if (stream) cudaStreamDestroy(stream); + return failures > 0 ? 1 : 0; +} +EOF2 + CUDA_BIN=$(mktemp /tmp/cuda_check_XXXXXX) + NVCC_ERR=$(mktemp /tmp/cuda_nvcc_err_XXXXXX) + if ! nvcc -o "$CUDA_BIN" "$CUDA_TEST" -cudart shared -Xlinker -rpath,/usr/local/cuda/lib64 2>"$NVCC_ERR"; then + echo "ERROR: Failed to compile CUDA smoke test ($CUDA_TEST -> $CUDA_BIN)" + nvcc_stderr=$(cat "$NVCC_ERR") + echo "nvcc stderr:" + echo "$nvcc_stderr" + print_smoke_diagnostics "nvcc failed to compile smoke test" "$nvcc_stderr" + rm -f "$CUDA_TEST" "$CUDA_BIN" "$NVCC_ERR" + exit 1 + fi + rm -f "$NVCC_ERR" + smoke_out=$("$CUDA_BIN" 2>&1) + smoke_rc=$? + if [ $smoke_rc -ne 0 ]; then + echo "$smoke_out" + # Last-resort retry: try the libcuda-ordering self-heal script and + # re-run the smoke test once. The script only acts when the host + # libcuda matches the kernel NVRM (i.e. the broken-node ld.so.conf.d + # ordering pattern); legitimate forward-compat use cases on older + # host drivers leave it untouched. If the script applies a fix, it + # exits 0 and we retry; otherwise we go straight to emitting the + # incident ticket. + SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) + FIX_SCRIPT="${SCRIPT_DIR}/ci/fix_libcuda_ldconfig.sh" + if [ -x "$FIX_SCRIPT" ]; then + echo + echo "*** Smoke test failed — attempting self-heal via ${FIX_SCRIPT##*/} ***" + if "$FIX_SCRIPT"; then + echo "*** Self-heal applied, retrying smoke test ***" + smoke_out=$("$CUDA_BIN" 2>&1) + smoke_rc=$? + if [ $smoke_rc -eq 0 ]; then + echo "$smoke_out" + echo "*** Smoke test PASSED after libcuda ldconfig self-heal ***" + rm -f "$CUDA_TEST" "$CUDA_BIN" + return 0 2>/dev/null || exit 0 + fi + echo "$smoke_out" + echo "*** Smoke test still failing after self-heal — giving up ***" + else + echo "*** Self-heal not applicable on this node — giving up ***" + fi + fi + print_smoke_diagnostics "cuda runtime call failed (driver/userspace mismatch likely)" "$smoke_out" + echo "ERROR: CUDA compute smoke test failed on this node. Check GPU/driver compatibility." + rm -f "$CUDA_TEST" "$CUDA_BIN" + exit 1 + fi + rm -f "$CUDA_TEST" "$CUDA_BIN" +fi diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt index 80791f13e..1b907419f 100644 --- a/python/CMakeLists.txt +++ b/python/CMakeLists.txt @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,15 +25,26 @@ set_directory_properties(PROPERTIES COMPILE_OPTIONS "${COMPILE_WARNINGS}") set(CMAKE_POSITION_INDEPENDENT_CODE true) -set(PYBIND11_PYTHON_VERSION ${PYTHON_VERSION}) -add_subdirectory(${PYBIND11_SOURCE_DIR} pybind11) +string(REGEX MATCH "^[0-9]+\\.[0-9]+" CVCUDA_PYTHON_VERSION "${PYTHON_VERSION}") +set(PYBIND11_PYTHON_VERSION ${CVCUDA_PYTHON_VERSION}) +execute_process( + COMMAND python${CVCUDA_PYTHON_VERSION} -m pybind11 --cmakedir + OUTPUT_VARIABLE pybind11_DIR + OUTPUT_STRIP_TRAILING_WHITESPACE + RESULT_VARIABLE pybind11_cmakedir_result +) +if(NOT pybind11_cmakedir_result EQUAL 0) + message(FATAL_ERROR "Failed to find pybind11 cmake dir for python${CVCUDA_PYTHON_VERSION}. " + "Is pybind11 installed? Try: python${CVCUDA_PYTHON_VERSION} -m pip install pybind11") +endif() +find_package(pybind11 REQUIRED CONFIG) find_package(nvcv_types REQUIRED) add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../src/nvcv/util nvcv_util) add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../src/cvcuda/util cvcuda_util) -set(PYTHON_MODULE_NAME python${PYTHON_VERSION}) +set(PYTHON_MODULE_NAME python${CVCUDA_PYTHON_VERSION}) string(REPLACE "." "" PYTHON_MODULE_NAME "${PYTHON_MODULE_NAME}") include(GNUInstallDirs) @@ -55,9 +66,15 @@ function(cvcuda_python_add_module) set_target_properties(${ARG_TARGET} PROPERTIES DEBUG_POSTFIX "" LIBRARY_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}" - # Set RPATH so Python extensions can find C++ libraries in parent directory - BUILD_RPATH "$ORIGIN/.." - INSTALL_RPATH "$ORIGIN/.." + # Set RPATH so Python extensions can find C++ libraries. + # Install layout: _cvcuda*.so → /opt/nvidia/cvcuda/lib/python/cvcuda/_cvcuda*.so + # libcvcuda.so. → /opt/nvidia/cvcuda/lib/libcvcuda.so. + # From the .so's perspective $ORIGIN is .../lib/python/cvcuda/, so libcvcuda + # lives at $ORIGIN/../.. (two dirs up). Without this, tar-installed Python + # modules fail to load libcvcuda.so on systems where the build tree no + # longer exists (anything other than the build pod itself). + BUILD_RPATH "$ORIGIN/../.." + INSTALL_RPATH "$ORIGIN/../.." BUILD_RPATH_USE_ORIGIN ON ) @@ -74,16 +91,25 @@ function(cvcuda_python_add_module) -static-libstdc++ ) - # Suppress false positive GCC-12 warning in pybind11 internals - # This warning appears during LTO, so we need to pass it to both compiler and linker + # Suppress false positive GCC warnings in pybind11 internals. These appear + # during LTO, so they must be passed to both compiler and linker: + # - stringop-overread: GCC-12 misanalysis of pybind11 string handling. + # - odr: pybind11 value-type type_caster specializations (e.g. for + # nvcv::ImageFormat, nvcv::DataType, nvcv::Size2D) use PYBIND11_TYPE_CASTER + # and so do not derive from type_caster_base like the primary template + # does. GCC's LTO ODR check cannot model an explicit specialization as a + # distinct type and reports the layout difference as a violation; each + # specialization is in fact defined once and identically across all TUs. if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") target_compile_options(${ARG_TARGET} PRIVATE -Wno-stringop-overread + -Wno-odr ) target_link_options(${ARG_TARGET} PRIVATE -Wno-stringop-overread + -Wno-odr ) endif() @@ -114,8 +140,7 @@ add_subdirectory(mod_cvcuda) set(SRC_PYTHON_MODULE_PATH ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}/python) set(DST_PYTHON_MODULE_PATH /usr/lib/python3/dist-packages) -# Add __init__.py files to the list before converting to string -# Append to the cache variable first +# Add package files to the list before converting to string. set(PYTHON_MODULE_FILENAME_LIST "${PYTHON_MODULE_FILENAME_LIST};cvcuda/__init__.py" CACHE INTERNAL "") # Now convert to space-separated string for the postinst script string(JOIN " " PYTHON_MODULE_FILENAME_LIST ${PYTHON_MODULE_FILENAME_LIST}) diff --git a/python/MANIFEST.in b/python/MANIFEST.in index 7fdf012be..22ab0d446 100644 --- a/python/MANIFEST.in +++ b/python/MANIFEST.in @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # Include README and license files in source distributions @@ -8,3 +8,7 @@ include pyproject.toml # Include all .so files (binary extensions) recursive-include cvcuda *.so recursive-include cvcuda_cu*.libs * + +# Include type stubs and PEP 561 marker +recursive-include cvcuda *.pyi +include cvcuda/py.typed diff --git a/python/README.md.in b/python/README.md.in index 4e3b1e851..2e7c7102a 100644 --- a/python/README.md.in +++ b/python/README.md.in @@ -1,11 +1,11 @@ -[//]: # "SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved." +[//]: # "SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved." [//]: # "SPDX-License-Identifier: Apache-2.0" # CV-CUDA Python Package [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) [![CUDA](https://img.shields.io/badge/CUDA-${CUDA_VERSION_MAJOR}-green.svg)](https://developer.nvidia.com/cuda-toolkit) -[![Python](https://img.shields.io/badge/python-3.9--3.14-blue)](https://www.python.org/) +[![Python](https://img.shields.io/badge/python-3.10--3.14-blue)](https://www.python.org/) ![Platform](https://img.shields.io/badge/Platform-Linux%20x86__64%20%7C%20Linux%20aarch64%20%7C%20WSL2-blue) CV-CUDA is an open-source library of GPU-accelerated computer vision algorithms designed for speed and scalability. @@ -45,7 +45,7 @@ resized = cvcuda.resize(cvcuda_tensor, (224, 224, 3), cvcuda.Interp.LINEAR) ## Requirements - CUDA Toolkit ${CUDA_VERSION_MAJOR} or later -- Python 3.9 or later +- Python 3.10 or later - NumPy 1.23.5 or later - Linux x86_64 (Ubuntu 20.04, 22.04, or later) diff --git a/python/__init__.py.in b/python/__init__.py.in index 18715d18a..1b310b6fe 100644 --- a/python/__init__.py.in +++ b/python/__init__.py.in @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/python/build_wheels.sh b/python/build_wheels.sh index e24cd707d..213a82b57 100755 --- a/python/build_wheels.sh +++ b/python/build_wheels.sh @@ -1,6 +1,6 @@ #!/bin/bash -e -# SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,7 +15,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -if [ "$#" -ne 1 ]; then +if [[ "$#" -ne 1 ]]; then echo "Usage: build_wheels.sh " exit 1 fi @@ -26,17 +26,18 @@ WHEEL_DIR="${PYTHON_BUILD_DIR}/dist" REPAIRED_WHEEL_DIR="${PYTHON_BUILD_DIR}/repaired_wheels" WHEEL_BUILD_DIR="${PYTHON_BUILD_DIR}/build_wheel" LIB_DIR="${PYTHON_BUILD_DIR}/cvcuda_cu${CUDA_VERSION_MAJOR}.libs" -SUPPORTED_PYTHONS=("39" "310" "311" "312" "313" "314") +SUPPORTED_PYTHONS=("310" "311" "312" "313" "314") # Option to force universal wheel creation even if not all Python versions are built FORCE_UNIVERSAL=${FORCE_UNIVERSAL:-false} detect_platform_tag() { - if [ -n "${AUDITWHEEL_PLAT}" ]; then + if [[ -n "${AUDITWHEEL_PLAT}" ]]; then echo "${AUDITWHEEL_PLAT}" else echo "auto" fi + return 0 } PLATFORM_TAG=$(detect_platform_tag) @@ -53,10 +54,8 @@ mkdir -p "${WHEEL_DIR}" "${REPAIRED_WHEEL_DIR}" "${WHEEL_BUILD_DIR}" "${LIB_DIR} AVAILABLE_PYTHONS=() for py_ver in "${SUPPORTED_PYTHONS[@]}"; do py_exec="python3.${py_ver:1}" - if command -v "${py_exec}" &> /dev/null; then - if compgen -G "${PYTHON_BUILD_DIR}/cvcuda/*.cpython-${py_ver}-*.so" > /dev/null; then - AVAILABLE_PYTHONS+=("cp${py_ver}") - fi + if command -v "${py_exec}" &> /dev/null && compgen -G "${PYTHON_BUILD_DIR}/cvcuda/*.cpython-${py_ver}-*.so" > /dev/null; then + AVAILABLE_PYTHONS+=("cp${py_ver}") fi done PYTHON_EXECUTABLE=python3 @@ -64,8 +63,8 @@ PYTHON_EXECUTABLE=python3 # Print the available Python bindings echo "Available Python Bindings: ${AVAILABLE_PYTHONS[*]}" -if [ "${#AVAILABLE_PYTHONS[@]}" -eq 0 ]; then - echo "Error: No Python bindings detected." +if [[ "${#AVAILABLE_PYTHONS[@]}" -eq 0 ]]; then + echo "Error: No Python bindings detected." >&2 exit 1 fi @@ -73,7 +72,7 @@ fi echo "Copying and patching shared libraries..." for lib in "${LIBRARIES[@]}"; do src_path="${BUILD_DIR}/lib/${lib}" - if [ -f "${src_path}" ]; then + if [[ -f "${src_path}" ]]; then cp "${src_path}" "${LIB_DIR}/" echo "Copied: ${src_path} -> ${LIB_DIR}/" patchelf --force-rpath --set-rpath '$ORIGIN/../cvcuda_cu${CUDA_VERSION_MAJOR}.libs' "${LIB_DIR}/${lib}" @@ -90,7 +89,7 @@ ln -sf "${PYTHON_BUILD_DIR}/MANIFEST.in" "${WHEEL_BUILD_DIR}/" ln -sf "${PYTHON_BUILD_DIR}/cvcuda" "${WHEEL_BUILD_DIR}/" ln -sf "${LIB_DIR}" "${WHEEL_BUILD_DIR}/cvcuda_cu${CUDA_VERSION_MAJOR}.libs" -echo "Printing currently installed python packages from v-env: $VIRTUAL_ENV and dir: `pwd`." +echo "Printing currently installed python packages from v-env: $VIRTUAL_ENV and dir: $(pwd)." ${PYTHON_EXECUTABLE} -m pip list # Build wheel @@ -100,7 +99,7 @@ ${PYTHON_EXECUTABLE} -m build --wheel --outdir="${WHEEL_DIR}" || ${PYTHON_EXECUT # Modify the wheel's Python and ABI tags for detected versions # If all supported Python versions are available or FORCE_UNIVERSAL is set, use py3-none for universal compatibility -if [ "${#AVAILABLE_PYTHONS[@]}" -eq "${#SUPPORTED_PYTHONS[@]}" ] || [ "${FORCE_UNIVERSAL}" = "true" ]; then +if [[ "${#AVAILABLE_PYTHONS[@]}" -eq "${#SUPPORTED_PYTHONS[@]}" ]] || [[ "${FORCE_UNIVERSAL}" == "true" ]]; then echo "Creating universal py3-none wheel..." echo " Available Python versions: ${AVAILABLE_PYTHONS[*]}" echo " Supported Python versions: ${SUPPORTED_PYTHONS[*]}" @@ -108,7 +107,7 @@ if [ "${#AVAILABLE_PYTHONS[@]}" -eq "${#SUPPORTED_PYTHONS[@]}" ] || [ "${FORCE_U abi_tag="none" # Verify that we have bindings for the most common Python versions - if [ "${FORCE_UNIVERSAL}" = "true" ] && [ "${#AVAILABLE_PYTHONS[@]}" -lt 3 ]; then + if [[ "${FORCE_UNIVERSAL}" == "true" ]] && [[ "${#AVAILABLE_PYTHONS[@]}" -lt 3 ]]; then echo "Warning: Creating universal wheel with only ${#AVAILABLE_PYTHONS[@]} Python version(s). This may cause compatibility issues." fi else @@ -118,29 +117,16 @@ else abi_tag="${python_tag}" fi -# Ensuring the tag is propagated to the wheel -for whl in "${WHEEL_DIR}"/*.whl; do - ${PYTHON_EXECUTABLE} -m wheel tags --remove \ - --python-tag "${python_tag}" \ - --abi-tag "${abi_tag}" \ - --platform-tag "${PLATFORM_TAG}" \ - "${whl}" -done -popd > /dev/null - -echo "Repairing wheel for compliance..." - -# check the auditwheel version +# Check the auditwheel version and handle "auto" platform tag auditwheel_version=$(${PYTHON_EXECUTABLE} -m pip list | grep auditwheel | awk '{print $2}') echo "Auditwheel version: ${auditwheel_version}" version_check() { local version1=$1 local version2=$2 - local IFS=. local i - read -ra ver1 <<< "$version1" - read -ra ver2 <<< "$version2" + IFS=. read -ra ver1 <<< "$version1" + IFS=. read -ra ver2 <<< "$version2" for ((i=${#ver1[@]}; i<${#ver2[@]}; i++)); do ver1[i]=0 @@ -163,12 +149,37 @@ version_check() { return 0 } -if ! version_check "${auditwheel_version}" "6.4.0" && [ "${PLATFORM_TAG}" = "auto" ]; then - echo "Auditwheel version ${auditwheel_version} is below requirement (>= 6.4.0) and PLATFORM_TAG is auto, set PLATFORM_TAG to linux_$(uname -m)" - PLATFORM_TAG="linux_$(uname -m)" - export AUDITWHEEL_PLAT="${PLATFORM_TAG}" +# Handle "auto" platform tag based on auditwheel version +if [[ "${PLATFORM_TAG}" == "auto" ]]; then + if ! version_check "${auditwheel_version}" "6.4.0"; then + echo "Auditwheel version ${auditwheel_version} is below requirement (>= 6.4.0) and PLATFORM_TAG is auto, set PLATFORM_TAG to linux_$(uname -m)" + PLATFORM_TAG="linux_$(uname -m)" + else + echo "Auditwheel version ${auditwheel_version} supports auto platform detection - will auto-detect appropriate manylinux/musllinux tag" + # Keep PLATFORM_TAG as "auto" for auditwheel >= 6.4.0 to auto-detect the appropriate platform + fi fi +# Apply Python and ABI tags to the wheel +# Only set platform-tag if it's not "auto" (auditwheel repair will set it) +for whl in "${WHEEL_DIR}"/*.whl; do + if [[ "${PLATFORM_TAG}" == "auto" ]]; then + ${PYTHON_EXECUTABLE} -m wheel tags --remove \ + --python-tag "${python_tag}" \ + --abi-tag "${abi_tag}" \ + "${whl}" + else + ${PYTHON_EXECUTABLE} -m wheel tags --remove \ + --python-tag "${python_tag}" \ + --abi-tag "${abi_tag}" \ + --platform-tag "${PLATFORM_TAG}" \ + "${whl}" + fi +done +popd > /dev/null + +echo "Repairing wheel for compliance..." + for whl in "${WHEEL_DIR}"/*.whl; do echo "Auditing wheel: ${whl}" ${PYTHON_EXECUTABLE} -m auditwheel show "${whl}" diff --git a/python/common/Assert.hpp b/python/common/Assert.hpp index 8acc6f925..8ca455ce9 100644 --- a/python/common/Assert.hpp +++ b/python/common/Assert.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -41,9 +41,19 @@ NVCV_NORETURN void DoAssert(const char *file, int line, const char *cond); # define NVCV_SOURCE_FILE_LINENO __LINE__ # define NVCV_OPTIONAL_STRINGIFY(X) # X #else -# define NVCV_SOURCE_FILE_NAME "" -# define NVCV_SOURCE_FILE_LINENO 0 -# define NVCV_OPTIONAL_STRINGIFY(X) "" +inline constexpr char kHiddenSourceFileName[] = ""; +inline constexpr int kHiddenSourceFileLineNo = 0; +inline constexpr char kHiddenConditionText[] = ""; + +# if !defined(NVCV_SOURCE_FILE_NAME) +# define NVCV_SOURCE_FILE_NAME ::nvcvpy::util::kHiddenSourceFileName +# endif +# if !defined(NVCV_SOURCE_FILE_LINENO) +# define NVCV_SOURCE_FILE_LINENO ::nvcvpy::util::kHiddenSourceFileLineNo +# endif +# if !defined(NVCV_OPTIONAL_STRINGIFY) +# define NVCV_OPTIONAL_STRINGIFY(X) ::nvcvpy::util::kHiddenConditionText +# endif #endif // allows overriding of NVCV_ASSERT definition diff --git a/python/common/CheckError.cpp b/python/common/CheckError.cpp index 24227a3f3..608bb88d1 100644 --- a/python/common/CheckError.cpp +++ b/python/common/CheckError.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,9 +19,20 @@ #include #include +#include namespace nvcvpy::util { +namespace { + +class CudaError : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + +} // namespace + static std::string ToString(cudaError_t err) { std::ostringstream ss; @@ -34,7 +45,7 @@ void CheckThrow(cudaError_t err) if (err != cudaSuccess) { cudaGetLastError(); // consume the error - throw std::runtime_error(ToString(err)); + throw CudaError(ToString(err)); } } diff --git a/python/common/Hash.hpp b/python/common/Hash.hpp index 121e2f631..ecbce1a92 100644 --- a/python/common/Hash.hpp +++ b/python/common/Hash.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -38,7 +38,7 @@ size_t ComputeHash(const T &a) template, int> = 0> size_t ComputeHash(const T &a) { - using Base = typename std::underlying_type::type; + using Base = std::underlying_type_t; return std::hash{}(static_cast(a)); } diff --git a/python/common/PyUtil.cpp b/python/common/PyUtil.cpp index d1e61b4d4..fe1e702cd 100644 --- a/python/common/PyUtil.cpp +++ b/python/common/PyUtil.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,6 +22,7 @@ #include #include +#include namespace nvcvpy::util { @@ -48,6 +49,11 @@ class Cleanup m_instance = nullptr; } + Cleanup(const Cleanup &) = delete; + Cleanup(Cleanup &&) = delete; + Cleanup &operator=(const Cleanup &) = delete; + Cleanup &operator=(Cleanup &&) = delete; + void addHandler(std::function fn) { m_handlers.emplace_back(std::move(fn)); @@ -74,7 +80,7 @@ class Cleanup Cleanup *Cleanup::m_instance = nullptr; } // namespace -void RegisterCleanup(py::module &m, std::function fn) +void RegisterCleanup(py::module &, std::function fn) { static Cleanup cleanup; cleanup.addHandler(std::move(fn)); @@ -82,7 +88,7 @@ void RegisterCleanup(py::module &m, std::function fn) std::string GetFullyQualifiedName(py::handle h) { - py::handle type = h.get_type(); + py::handle type = py::type::of(h); std::ostringstream ss; ss << type.attr("__module__").cast() << '.' << type.attr("__qualname__").cast(); @@ -136,6 +142,16 @@ py::dtype ToDType(const std::string &fmt) namespace cvcudapy { +namespace { + +class PyUtilError : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + +} // namespace + float4 GetFloat4FromPyArray(const pyarray &array) { if (array.ndim() == 0) @@ -144,6 +160,13 @@ float4 GetFloat4FromPyArray(const pyarray &array) } else { + if (array.ndim() != 1 || array.size() > 4) + { + throw PyUtilError(nvcvpy::util::ConcatString( + "borderValue must be a scalar or 1D array with at most 4 elements, current ndim is '", array.ndim(), + "' and size is '", array.size(), "'")); + } + float4 value = nvcv::cuda::SetAll(0.f); for (int i = 0; i < static_cast(array.size()); i++) diff --git a/python/common/PyUtil.hpp b/python/common/PyUtil.hpp index de8348b05..49c21e731 100644 --- a/python/common/PyUtil.hpp +++ b/python/common/PyUtil.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -39,7 +39,7 @@ void DefClassMethod(const char *name, Func &&f, const Extra &...extra) // got from pt::class_<...>::def py::cpp_function cf(py::method_adaptor(std::forward(f)), py::name(name), py::is_method(class_), py::sibling(py::getattr(class_, name, py::none())), extra...); - py::detail::add_class_method(class_, name, std::move(cf)); + py::detail::add_class_method(class_, name, cf); } // Adds a static method to an existing class diff --git a/python/common/String.cpp b/python/common/String.cpp index e4cbf2053..57eb9f04a 100644 --- a/python/common/String.cpp +++ b/python/common/String.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,24 +16,3 @@ */ #include "String.hpp" - -#include -#include - -namespace nvcvpy::util { - -std::string FormatString(const char *fmt, ...) -{ - va_list va; - va_start(va, fmt); - - char buffer[1024]; - vsnprintf(buffer, sizeof(buffer) - 1, fmt, va); - buffer[sizeof(buffer) - 1] = '\0'; // better be safe against truncation - - va_end(va); - - return buffer; -} - -} // namespace nvcvpy::util diff --git a/python/common/String.hpp b/python/common/String.hpp index 9c6572154..003a75ede 100644 --- a/python/common/String.hpp +++ b/python/common/String.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,10 +20,23 @@ #include #include +#include namespace nvcvpy::util { -std::string FormatString(const char *fmt, ...) __attribute__((format(printf, 1, 2))); +template +void AppendToStream(std::ostringstream &ss, Args &&...args) +{ + ((void)(ss << std::forward(args)), ...); +} + +template +std::string ConcatString(Args &&...args) +{ + std::ostringstream ss; + AppendToStream(ss, std::forward(args)...); + return ss.str(); +} // Make it easier to use ostreams to define __repr__ template diff --git a/python/cpack/debian_python_postinst.in b/python/cpack/debian_python_postinst.in index 9a9874862..92e93b42a 100755 --- a/python/cpack/debian_python_postinst.in +++ b/python/cpack/debian_python_postinst.in @@ -27,7 +27,7 @@ case "$1" in # Create parent directory for the symlink if it contains subdirectories dst_dir=$(dirname "$dst") mkdir -p "$dst_dir" - ln -sf "$src" "$dst" + ln -sfn "$src" "$dst" done ;; esac diff --git a/python/mod_cvcuda/AdaptiveThresholdType.cpp b/python/mod_cvcuda/AdaptiveThresholdType.cpp index fe18fd62e..750951f5e 100644 --- a/python/mod_cvcuda/AdaptiveThresholdType.cpp +++ b/python/mod_cvcuda/AdaptiveThresholdType.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,9 +23,11 @@ namespace cvcudapy { void ExportAdaptiveThresholdType(py::module &m) { - py::enum_(m, "AdaptiveThresholdType", py::arithmetic()) - .value("MEAN_C", NVCV_ADAPTIVE_THRESH_MEAN_C) - .value("GAUSSIAN_C", NVCV_ADAPTIVE_THRESH_GAUSSIAN_C); + py::enum_(m, "AdaptiveThresholdType", "Adaptive threshold computation methods.", + py::arithmetic()) + .value("MEAN_C", NVCV_ADAPTIVE_THRESH_MEAN_C, "Threshold is the mean of the neighbourhood area minus C.") + .value("GAUSSIAN_C", NVCV_ADAPTIVE_THRESH_GAUSSIAN_C, + "Threshold is the Gaussian-weighted sum of the neighbourhood area minus C."); } } // namespace cvcudapy diff --git a/python/mod_cvcuda/BorderType.cpp b/python/mod_cvcuda/BorderType.cpp index 7d9096f92..6ab8c096e 100644 --- a/python/mod_cvcuda/BorderType.cpp +++ b/python/mod_cvcuda/BorderType.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,7 +23,7 @@ namespace cvcudapy { void ExportBorderType(py::module &m) { - py::enum_(m, "Border") + py::enum_(m, "Border", "Border padding modes for image operations.") .value("CONSTANT", NVCV_BORDER_CONSTANT, "Constant border") .value("REPLICATE", NVCV_BORDER_REPLICATE, "Replicate border") .value("REFLECT", NVCV_BORDER_REFLECT, "Reflect border") diff --git a/python/mod_cvcuda/CMakeLists.txt b/python/mod_cvcuda/CMakeLists.txt index 5c15aa9ab..1cd551701 100644 --- a/python/mod_cvcuda/CMakeLists.txt +++ b/python/mod_cvcuda/CMakeLists.txt @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,13 +16,18 @@ find_package(cvcuda REQUIRED) find_package(CUDAToolkit REQUIRED) -# Set up dlpack (needed for NVCV types) -set(save_policy_0077 ${CMAKE_POLICY_DEFAULT_CMP0077}) -set(CMAKE_POLICY_DEFAULT_CMP0077 NEW) -set(BUILD_MOCK OFF) -set(BUILD_DOCS OFF) -add_subdirectory(${DLPACK_SOURCE_DIR} dlpack) -set(CMAKE_POLICY_DEFAULT_CMP0077 ${save_policy_0077}) +find_package(dlpack CONFIG REQUIRED) + +# dlpack's CMake project version is stuck at 0.6 regardless of release, +# so check the actual version from header macros. +get_target_property(_dlpack_inc dlpack::dlpack INTERFACE_INCLUDE_DIRECTORIES) +file(STRINGS "${_dlpack_inc}/dlpack/dlpack.h" _dlpack_major REGEX "#define DLPACK_MAJOR_VERSION") +file(STRINGS "${_dlpack_inc}/dlpack/dlpack.h" _dlpack_minor REGEX "#define DLPACK_MINOR_VERSION") +string(REGEX MATCH "[0-9]+" _dlpack_major "${_dlpack_major}") +string(REGEX MATCH "[0-9]+" _dlpack_minor "${_dlpack_minor}") +if(_dlpack_major LESS 1 OR (_dlpack_major EQUAL 1 AND _dlpack_minor LESS 3)) + message(FATAL_ERROR "DLPack >= 1.3 required, found ${_dlpack_major}.${_dlpack_minor}") +endif() # Generate NVCV type definitions set(imgformat_defs ${CMAKE_CURRENT_BINARY_DIR}/NVCVPythonImageFormatDefs.inc) @@ -47,6 +52,15 @@ cvcuda_python_add_module( PACKAGE_DIR cvcuda SOURCES # NVCV core types + operators/OpJpegCompressionDistortion.cpp + operators/OpAdjustHue.cpp + operators/OpAdjustSaturation.cpp + operators/OpAdjustSharpness.cpp + operators/OpAdjustContrast.cpp + operators/OpInvert.cpp + operators/OpSolarize.cpp + operators/OpPosterize.cpp + operators/OpAutoContrast.cpp ${imgformat_defs} ${dtype_defs} nvcv/ImageFormat.cpp @@ -82,6 +96,7 @@ cvcuda_python_add_module( RemapMapValueType.cpp InterpolationType.cpp BorderType.cpp + RoundMode.cpp ColorConversionCode.cpp MorphologyType.cpp ThresholdType.cpp @@ -139,6 +154,7 @@ cvcuda_python_add_module( operators/OpGaussianNoise.cpp operators/OpInpaint.cpp operators/OpFindHomography.cpp + operators/OpCLAHE.cpp ) target_include_directories(cvcuda_module_python diff --git a/python/mod_cvcuda/ChannelManipType.cpp b/python/mod_cvcuda/ChannelManipType.cpp index 623f28289..6c292b482 100644 --- a/python/mod_cvcuda/ChannelManipType.cpp +++ b/python/mod_cvcuda/ChannelManipType.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,7 +23,7 @@ namespace cvcudapy { void ExportChannelManipType(py::module &m) { - py::enum_(m, "ChannelManip") + py::enum_(m, "ChannelManip", "Channel manipulation modes for color conversion.") .value("NO_OP", NVCV_CHANNEL_NO_OP, "No manipulation (i.e., channels are unchanged)") .value("REVERSE", NVCV_CHANNEL_REVERSE, "Reverse channel order (e.g., RGB to BGR, RGBA to ABGR, etc.)"); } diff --git a/python/mod_cvcuda/ColorConversionCode.cpp b/python/mod_cvcuda/ColorConversionCode.cpp index c6e5f0e78..a045adf43 100644 --- a/python/mod_cvcuda/ColorConversionCode.cpp +++ b/python/mod_cvcuda/ColorConversionCode.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,7 +23,8 @@ namespace cvcudapy { void ExportColorConversionCode(py::module &m) { - py::enum_(m, "ColorConversion") + py::enum_(m, "ColorConversion", + "Color space conversion codes (mirrors OpenCV convention).") .value("BGR2BGRA", NVCV_COLOR_BGR2BGRA) .value("RGB2RGBA", NVCV_COLOR_RGB2RGBA) .value("BGRA2BGR", NVCV_COLOR_BGRA2BGR) @@ -221,7 +222,6 @@ void ExportColorConversionCode(py::module &m) .value("BayerGB2RGB_EA", NVCV_COLOR_BayerGB2RGB_EA) .value("BayerRG2RGB_EA", NVCV_COLOR_BayerRG2RGB_EA) .value("BayerGR2RGB_EA", NVCV_COLOR_BayerGR2RGB_EA) - .value("COLORCVT_MAX", NVCV_COLOR_COLORCVT_MAX) .value("RGB2YUV_NV12", NVCV_COLOR_RGB2YUV_NV12) .value("BGR2YUV_NV12", NVCV_COLOR_BGR2YUV_NV12) .value("RGB2YUV_NV21", NVCV_COLOR_RGB2YUV_NV21) @@ -233,8 +233,7 @@ void ExportColorConversionCode(py::module &m) .value("RGBA2YUV_NV21", NVCV_COLOR_RGBA2YUV_NV21) .value("RGBA2YUV420sp", NVCV_COLOR_RGBA2YUV420sp) .value("BGRA2YUV_NV21", NVCV_COLOR_BGRA2YUV_NV21) - .value("BGRA2YUV420sp", NVCV_COLOR_BGRA2YUV420sp) - .value("CVT_MAX", NVCV_COLORCVT_MAX); + .value("BGRA2YUV420sp", NVCV_COLOR_BGRA2YUV420sp); } } // namespace cvcudapy diff --git a/python/mod_cvcuda/ConnectivityType.cpp b/python/mod_cvcuda/ConnectivityType.cpp index 8cb5d41f1..67bdb5ab5 100644 --- a/python/mod_cvcuda/ConnectivityType.cpp +++ b/python/mod_cvcuda/ConnectivityType.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,11 +23,13 @@ namespace cvcudapy { void ExportConnectivityType(py::module &m) { - py::enum_(m, "ConnectivityType", py::arithmetic()) - .value("CONNECTIVITY_4_2D", NVCV_CONNECTIVITY_4_2D) - .value("CONNECTIVITY_6_3D", NVCV_CONNECTIVITY_6_3D) - .value("CONNECTIVITY_8_2D", NVCV_CONNECTIVITY_8_2D) - .value("CONNECTIVITY_26_2D", NVCV_CONNECTIVITY_26_3D) + py::enum_( + m, "ConnectivityType", "Pixel/voxel connectivity types for connected-component labeling.", py::arithmetic()) + .value("CONNECTIVITY_4_2D", NVCV_CONNECTIVITY_4_2D, "4-connected: pixels sharing an edge in 2D.") + .value("CONNECTIVITY_6_3D", NVCV_CONNECTIVITY_6_3D, "6-connected: voxels sharing a face in 3D.") + .value("CONNECTIVITY_8_2D", NVCV_CONNECTIVITY_8_2D, "8-connected: pixels sharing an edge or corner in 2D.") + .value("CONNECTIVITY_26_3D", NVCV_CONNECTIVITY_26_3D, + "26-connected: voxels sharing a face, edge, or corner in 3D.") .export_values(); } diff --git a/python/mod_cvcuda/CvtColorUtil.cpp b/python/mod_cvcuda/CvtColorUtil.cpp index 5f80c08bf..5e179b761 100644 --- a/python/mod_cvcuda/CvtColorUtil.cpp +++ b/python/mod_cvcuda/CvtColorUtil.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,11 +21,20 @@ #include #include +#include #include +#include #include -// All commented mappings below "// {...}" are not implemented in legacy code -// All commented codes below "//NVCV..." do not have clear output format +namespace { + +class CvtColorError : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + +} // namespace const std::unordered_map kOutputFormat = { { NVCV_COLOR_BGR2BGRA, NVCV_IMAGE_FORMAT_BGRA8}, @@ -44,70 +53,14 @@ const std::unordered_map kOutputFormat { NVCV_COLOR_RGB2GRAY, NVCV_IMAGE_FORMAT_Y8_ER}, { NVCV_COLOR_GRAY2BGR, NVCV_IMAGE_FORMAT_BGR8}, { NVCV_COLOR_GRAY2RGB, NVCV_IMAGE_FORMAT_RGB8}, - // { NVCV_COLOR_GRAY2BGRA, NVCV_IMAGE_FORMAT_BGRA8}, - // { NVCV_COLOR_GRAY2RGBA, NVCV_IMAGE_FORMAT_RGBA8}, - // { NVCV_COLOR_BGRA2GRAY, NVCV_IMAGE_FORMAT_Y8_ER}, - // { NVCV_COLOR_RGBA2GRAY, NVCV_IMAGE_FORMAT_Y8_ER}, - //NVCV_COLOR_BGR2BGR565 - //NVCV_COLOR_RGB2BGR565 - // { NVCV_COLOR_BGR5652BGR, NVCV_IMAGE_FORMAT_BGR8}, - // { NVCV_COLOR_BGR5652RGB, NVCV_IMAGE_FORMAT_RGB8}, - //NVCV_COLOR_BGRA2BGR565 - //NVCV_COLOR_RGBA2BGR565 - // { NVCV_COLOR_BGR5652BGRA, NVCV_IMAGE_FORMAT_BGRA8}, - // { NVCV_COLOR_BGR5652RGBA, NVCV_IMAGE_FORMAT_RGBA8}, - //NVCV_COLOR_GRAY2BGR565 - // { NVCV_COLOR_BGR5652GRAY, NVCV_IMAGE_FORMAT_Y8_ER}, - //NVCV_COLOR_BGR2BGR555 - //NVCV_COLOR_RGB2BGR555 - // { NVCV_COLOR_BGR5552BGR, NVCV_IMAGE_FORMAT_BGR8}, - // { NVCV_COLOR_BGR5552RGB, NVCV_IMAGE_FORMAT_RGB8}, - //NVCV_COLOR_BGRA2BGR555 - //NVCV_COLOR_RGBA2BGR555 - // { NVCV_COLOR_BGR5552BGRA, NVCV_IMAGE_FORMAT_BGRA8}, - // { NVCV_COLOR_BGR5552RGBA, NVCV_IMAGE_FORMAT_RGBA8}, - //NVCV_COLOR_GRAY2BGR555 - // { NVCV_COLOR_BGR5552GRAY, NVCV_IMAGE_FORMAT_Y8_ER}, - //NVCV_COLOR_BGR2XYZ - //NVCV_COLOR_RGB2XYZ - // { NVCV_COLOR_XYZ2BGR, NVCV_IMAGE_FORMAT_BGR8}, - // { NVCV_COLOR_XYZ2RGB, NVCV_IMAGE_FORMAT_RGB8}, - //NVCV_COLOR_BGR2YCrCb - //NVCV_COLOR_RGB2YCrCb - // { NVCV_COLOR_YCrCb2BGR, NVCV_IMAGE_FORMAT_BGR8}, - // { NVCV_COLOR_YCrCb2RGB, NVCV_IMAGE_FORMAT_RGB8}, { NVCV_COLOR_BGR2HSV, NVCV_IMAGE_FORMAT_HSV8}, { NVCV_COLOR_RGB2HSV, NVCV_IMAGE_FORMAT_HSV8}, - //NVCV_COLOR_BGR2Lab - //NVCV_COLOR_RGB2Lab - //NVCV_COLOR_BGR2Luv - //NVCV_COLOR_RGB2Luv - //NVCV_COLOR_BGR2HLS - //NVCV_COLOR_RGB2HLS { NVCV_COLOR_HSV2BGR, NVCV_IMAGE_FORMAT_BGR8}, { NVCV_COLOR_HSV2RGB, NVCV_IMAGE_FORMAT_RGB8}, - // { NVCV_COLOR_Lab2BGR, NVCV_IMAGE_FORMAT_BGR8}, - // { NVCV_COLOR_Lab2RGB, NVCV_IMAGE_FORMAT_RGB8}, - // { NVCV_COLOR_Luv2BGR, NVCV_IMAGE_FORMAT_BGR8}, - // { NVCV_COLOR_Luv2RGB, NVCV_IMAGE_FORMAT_RGB8}, - // { NVCV_COLOR_HLS2BGR, NVCV_IMAGE_FORMAT_BGR8}, - // { NVCV_COLOR_HLS2RGB, NVCV_IMAGE_FORMAT_RGB8}, { NVCV_COLOR_BGR2HSV_FULL, NVCV_IMAGE_FORMAT_HSV8}, { NVCV_COLOR_RGB2HSV_FULL, NVCV_IMAGE_FORMAT_HSV8}, - //NVCV_COLOR_BGR2HLS_FULL - //NVCV_COLOR_RGB2HLS_FULL { NVCV_COLOR_HSV2BGR_FULL, NVCV_IMAGE_FORMAT_BGR8}, { NVCV_COLOR_HSV2RGB_FULL, NVCV_IMAGE_FORMAT_RGB8}, - // { NVCV_COLOR_HLS2BGR_FULL, NVCV_IMAGE_FORMAT_BGR8}, - // { NVCV_COLOR_HLS2RGB_FULL, NVCV_IMAGE_FORMAT_RGB8}, - //NVCV_COLOR_LBGR2Lab - //NVCV_COLOR_LRGB2Lab - //NVCV_COLOR_LBGR2Luv - //NVCV_COLOR_LRGB2Luv - // { NVCV_COLOR_Lab2LBGR, NVCV_IMAGE_FORMAT_BGR8}, - // { NVCV_COLOR_Lab2LRGB, NVCV_IMAGE_FORMAT_RGB8}, - // { NVCV_COLOR_Luv2LBGR, NVCV_IMAGE_FORMAT_BGR8}, - // { NVCV_COLOR_Luv2LRGB, NVCV_IMAGE_FORMAT_RGB8}, { NVCV_COLOR_BGR2YUV, NVCV_IMAGE_FORMAT_YUV8}, { NVCV_COLOR_RGB2YUV, NVCV_IMAGE_FORMAT_YUV8}, { NVCV_COLOR_YUV2BGR, NVCV_IMAGE_FORMAT_BGR8}, @@ -183,62 +136,12 @@ const std::unordered_map kOutputFormat {NVCV_COLOR_YUV2GRAY_YVYU, NVCV_IMAGE_FORMAT_Y8_ER}, {NVCV_COLOR_YUV2GRAY_YUYV, NVCV_IMAGE_FORMAT_Y8_ER}, {NVCV_COLOR_YUV2GRAY_YUNV, NVCV_IMAGE_FORMAT_Y8_ER}, - // { NVCV_COLOR_RGBA2mRGBA, NVCV_IMAGE_FORMAT_RGBA8}, - // { NVCV_COLOR_mRGBA2RGBA, NVCV_IMAGE_FORMAT_RGBA8}, - //NVCV_COLOR_RGB2YUV_I420 - //NVCV_COLOR_BGR2YUV_I420 - //NVCV_COLOR_RGB2YUV_IYUV - //NVCV_COLOR_BGR2YUV_IYUV - //NVCV_COLOR_RGBA2YUV_I420 - //NVCV_COLOR_BGRA2YUV_I420 - //NVCV_COLOR_RGBA2YUV_IYUV - //NVCV_COLOR_BGRA2YUV_IYUV - //NVCV_COLOR_RGB2YUV_YV12 - //NVCV_COLOR_BGR2YUV_YV12 - //NVCV_COLOR_RGBA2YUV_YV12 - //NVCV_COLOR_BGRA2YUV_YV12 - // { NVCV_COLOR_BayerBG2BGR, NVCV_IMAGE_FORMAT_BGR8}, - // { NVCV_COLOR_BayerGB2BGR, NVCV_IMAGE_FORMAT_BGR8}, - // { NVCV_COLOR_BayerRG2BGR, NVCV_IMAGE_FORMAT_BGR8}, - // { NVCV_COLOR_BayerGR2BGR, NVCV_IMAGE_FORMAT_BGR8}, - // { NVCV_COLOR_BayerBG2RGB, NVCV_IMAGE_FORMAT_RGB8}, - // { NVCV_COLOR_BayerGB2RGB, NVCV_IMAGE_FORMAT_RGB8}, - // { NVCV_COLOR_BayerRG2RGB, NVCV_IMAGE_FORMAT_RGB8}, - // { NVCV_COLOR_BayerGR2RGB, NVCV_IMAGE_FORMAT_RGB8}, - // { NVCV_COLOR_BayerBG2GRAY, NVCV_IMAGE_FORMAT_Y8_ER}, - // { NVCV_COLOR_BayerGB2GRAY, NVCV_IMAGE_FORMAT_Y8_ER}, - // { NVCV_COLOR_BayerRG2GRAY, NVCV_IMAGE_FORMAT_Y8_ER}, - // { NVCV_COLOR_BayerGR2GRAY, NVCV_IMAGE_FORMAT_Y8_ER}, - // {NVCV_COLOR_BayerBG2BGR_VNG, NVCV_IMAGE_FORMAT_BGR8}, - // {NVCV_COLOR_BayerGB2BGR_VNG, NVCV_IMAGE_FORMAT_BGR8}, - // {NVCV_COLOR_BayerRG2BGR_VNG, NVCV_IMAGE_FORMAT_BGR8}, - // {NVCV_COLOR_BayerGR2BGR_VNG, NVCV_IMAGE_FORMAT_BGR8}, - // {NVCV_COLOR_BayerBG2RGB_VNG, NVCV_IMAGE_FORMAT_RGB8}, - // {NVCV_COLOR_BayerGB2RGB_VNG, NVCV_IMAGE_FORMAT_RGB8}, - // {NVCV_COLOR_BayerRG2RGB_VNG, NVCV_IMAGE_FORMAT_RGB8}, - // {NVCV_COLOR_BayerGR2RGB_VNG, NVCV_IMAGE_FORMAT_RGB8}, - // { NVCV_COLOR_BayerBG2BGR_EA, NVCV_IMAGE_FORMAT_BGR8}, - // { NVCV_COLOR_BayerGB2BGR_EA, NVCV_IMAGE_FORMAT_BGR8}, - // { NVCV_COLOR_BayerRG2BGR_EA, NVCV_IMAGE_FORMAT_BGR8}, - // { NVCV_COLOR_BayerGR2BGR_EA, NVCV_IMAGE_FORMAT_BGR8}, - // { NVCV_COLOR_BayerBG2RGB_EA, NVCV_IMAGE_FORMAT_RGB8}, - // { NVCV_COLOR_BayerGB2RGB_EA, NVCV_IMAGE_FORMAT_RGB8}, - // { NVCV_COLOR_BayerRG2RGB_EA, NVCV_IMAGE_FORMAT_RGB8}, - // { NVCV_COLOR_BayerGR2RGB_EA, NVCV_IMAGE_FORMAT_RGB8}, - //NVCV_COLOR_COLORCVT_MAX { NVCV_COLOR_RGB2YUV_NV12, NVCV_IMAGE_FORMAT_Y8}, { NVCV_COLOR_BGR2YUV_NV12, NVCV_IMAGE_FORMAT_Y8}, { NVCV_COLOR_RGB2YUV_NV21, NVCV_IMAGE_FORMAT_Y8}, { NVCV_COLOR_BGR2YUV_NV21, NVCV_IMAGE_FORMAT_Y8}, - //NVCV_COLOR_RGB2YUV420sp - //NVCV_COLOR_BGR2YUV420sp {NVCV_COLOR_RGBA2YUV_NV12, NVCV_IMAGE_FORMAT_Y8}, {NVCV_COLOR_BGRA2YUV_NV12, NVCV_IMAGE_FORMAT_Y8}, - //NVCV_COLOR_RGBA2YUV_NV21 - //NVCV_COLOR_RGBA2YUV420sp - //NVCV_COLOR_BGRA2YUV_NV21 - //NVCV_COLOR_BGRA2YUV420sp - //NVCV_COLORCVT_MAX = 148, }; nvcv::ImageFormat GetOutputFormat(nvcv::DataType in, NVCVColorConversionCode code) @@ -246,7 +149,7 @@ nvcv::ImageFormat GetOutputFormat(nvcv::DataType in, NVCVColorConversionCode cod auto outFormatIt = kOutputFormat.find(code); if (outFormatIt == kOutputFormat.end()) { - throw std::runtime_error("Invalid color conversion code"); + throw CvtColorError("Invalid color conversion code"); } nvcv::ImageFormat outFormat{outFormatIt->second}; @@ -259,7 +162,7 @@ nvcv::ImageFormat GetOutputFormat(nvcv::DataType in, NVCVColorConversionCode cod inNumBits = (inNumBits == 0) ? numBits : inNumBits; if (numBits != inNumBits) { - throw std::runtime_error("Invalid input format, all channels must have the same bit-depth"); + throw CvtColorError("Invalid input format, all channels must have the same bit-depth"); } } } @@ -301,28 +204,25 @@ nvcv::TensorShape GetOutputTensorShape(const nvcv::TensorShape &inputShape, nvcv { if (inputShape.rank() < 3 || inputShape.rank() > 4) { - throw std::runtime_error("Invalid input tensor shape, only NHWC or HWC are supported"); + throw CvtColorError("Invalid input tensor shape, only NHWC, HWC, NCHW, or CHW are supported"); + } + + std::array outputShape = {}; + auto layout = inputShape.layout(); + int heightIndex = layout.find('H'); + int channelIndex = layout.find('C'); + if (heightIndex < 0 || channelIndex < 0) + { + throw CvtColorError("Invalid input tensor shape, layout must contain H and C axes"); } - int64_t outputShape[4] = {}; - bool heightIndex = inputShape.rank() == 4 ? 1 : 0; for (int i = 0; i < inputShape.rank(); i++) { outputShape[i] = inputShape[i]; } - int channelIndex = inputShape.rank() == 4 ? 3 : 2; outputShape[heightIndex] = GetOutputHeight(outputShape[heightIndex], code); outputShape[channelIndex] = outputFormat.numChannels(); - if (inputShape.rank() == 4) - { - return nvcv::TensorShape({outputShape[0], outputShape[1], outputShape[2], outputShape[3]}, "NHWC"); - } - else - { - assert(inputShape.rank() == 3); - - return nvcv::TensorShape({outputShape[0], outputShape[1], outputShape[2]}, "HWC"); - } + return nvcv::TensorShape(outputShape.data(), inputShape.rank(), layout); } diff --git a/python/mod_cvcuda/CvtColorUtil.hpp b/python/mod_cvcuda/CvtColorUtil.hpp index 17203d1ed..849907215 100644 --- a/python/mod_cvcuda/CvtColorUtil.hpp +++ b/python/mod_cvcuda/CvtColorUtil.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/python/mod_cvcuda/InterpolationType.cpp b/python/mod_cvcuda/InterpolationType.cpp index 35c968aa0..f3b13dd99 100644 --- a/python/mod_cvcuda/InterpolationType.cpp +++ b/python/mod_cvcuda/InterpolationType.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,7 +23,7 @@ namespace cvcudapy { void ExportInterpolationType(py::module &m) { - py::enum_(m, "Interp") + py::enum_(m, "Interp", "Interpolation methods for image resampling.") .value("NEAREST", NVCV_INTERP_NEAREST, "Nearest-neighbor interpolation") .value("LINEAR", NVCV_INTERP_LINEAR, "Linear interpolation") .value("CUBIC", NVCV_INTERP_CUBIC, "Cubic interpolation") diff --git a/python/mod_cvcuda/LabelType.cpp b/python/mod_cvcuda/LabelType.cpp index 04efc42df..8aa86cef9 100644 --- a/python/mod_cvcuda/LabelType.cpp +++ b/python/mod_cvcuda/LabelType.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,9 +23,10 @@ namespace cvcudapy { void ExportLabelType(py::module &m) { - py::enum_(m, "LABEL", py::arithmetic()) - .value("FAST", NVCV_LABEL_FAST) - .value("SEQUENTIAL", NVCV_LABEL_SEQUENTIAL); + py::enum_(m, "LABEL", "Label assignment modes for connected-component output.", py::arithmetic()) + .value("FAST", NVCV_LABEL_FAST, + "Labels assigned using a fast parallel algorithm; values may not be sequential.") + .value("SEQUENTIAL", NVCV_LABEL_SEQUENTIAL, "Labels assigned sequentially starting from 1."); } } // namespace cvcudapy diff --git a/python/mod_cvcuda/Main.cpp b/python/mod_cvcuda/Main.cpp index 8f9c6590b..8e9cce898 100644 --- a/python/mod_cvcuda/Main.cpp +++ b/python/mod_cvcuda/Main.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -46,6 +46,7 @@ #include "OsdElement.hpp" #include "PairwiseMatcherType.hpp" #include "RemapMapValueType.hpp" +#include "RoundMode.hpp" #include "SIFTFlagType.hpp" #include "ThresholdType.hpp" @@ -53,6 +54,7 @@ #include "operators/Operators.hpp" #include +#include #include namespace py = pybind11; @@ -88,20 +90,41 @@ PYBIND11_MODULE(_cvcuda, m) ExportDataType(m); ExportRect(m); ExportThreadScope(m); + ExportTensorLayout(m); // Image::cpu/cuda take a TensorLayout — must be registered before Image::Export. // Core Entities + // Registration order matters: (a) base classes must be registered + // before derived ones (Resource, CacheItem → Container → Tensor/Image/...); + // (b) when a method signature references another pybind11 type, that + // type must be registered first, or pybind11 emits the raw C++ + // typename (e.g. `nvcvpy::priv::Stream`) in stubs and reprs. ExportCAPI(m); - Resource::Export(m); - Cache::Export(m); - Container::Export(m); + Resource::Export(m); // base of Container (no stream methods yet) + Cache::Export(m); // exports CacheItem (base of Container, Stream) + Container::Export(m); // depends on Resource + CacheItem ExternalBuffer::Export(m); // Objects + Image::Export(m); // before Tensor (as_tensor takes Image&) Tensor::Export(m); TensorBatch::Export(m); - Image::Export(m); ImageBatchVarShape::Export(m); Stream::Export(m); + + // Deferred method bindings: add methods that reference Stream now + // that Stream is a known pybind11 type. + Resource::ExportStreamMethods(m); + + py::module_ test = m.def_submodule("_test"); + test.def("resourceguard_destructor_error", + []() + { + nvcvpy::Stream stream = nvcvpy::Stream::Current(); + nvcvpy::ResourceGuard guard(stream); + + PyErr_SetString(PyExc_RuntimeError, "injected ResourceGuard commit failure"); + }); + ExportCAPITestHooks(test); } { @@ -122,11 +145,21 @@ PYBIND11_MODULE(_cvcuda, m) ExportMorphologyType(m); ExportNormType(m); ExportRemapMapValueType(m); + ExportRoundMode(m); ExportSIFTFlagType(m); ExportThresholdType(m); // doctag: Operators // CV-CUDA Operators + ExportOpJpegCompressionDistortion(m); + ExportOpAdjustHue(m); + ExportOpAdjustSaturation(m); + ExportOpAdjustSharpness(m); + ExportOpAdjustContrast(m); + ExportOpInvert(m); + ExportOpSolarize(m); + ExportOpPosterize(m); + ExportOpAutoContrast(m); ExportOpResizeCropConvertReformat(m); ExportOpPairwiseMatcher(m); ExportOpLabel(m); @@ -178,5 +211,6 @@ PYBIND11_MODULE(_cvcuda, m) ExportOpInpaint(m); ExportOpStack(m); ExportOpFindHomography(m); + ExportOpCLAHE(m); } } diff --git a/python/mod_cvcuda/MorphologyType.cpp b/python/mod_cvcuda/MorphologyType.cpp index 7a55f449c..379e41399 100644 --- a/python/mod_cvcuda/MorphologyType.cpp +++ b/python/mod_cvcuda/MorphologyType.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,11 +23,11 @@ namespace cvcudapy { void ExportMorphologyType(py::module &m) { - py::enum_(m, "MorphologyType") - .value("ERODE", NVCV_ERODE) - .value("DILATE", NVCV_DILATE) - .value("OPEN", NVCV_OPEN) - .value("CLOSE", NVCV_CLOSE); + py::enum_(m, "MorphologyType", "Morphological operation types.") + .value("ERODE", NVCV_ERODE, "Replaces each pixel with the minimum over the structuring element.") + .value("DILATE", NVCV_DILATE, "Replaces each pixel with the maximum over the structuring element.") + .value("OPEN", NVCV_OPEN, "Erosion followed by dilation; removes small bright regions.") + .value("CLOSE", NVCV_CLOSE, "Dilation followed by erosion; fills small dark holes."); } } // namespace cvcudapy diff --git a/python/mod_cvcuda/NormType.cpp b/python/mod_cvcuda/NormType.cpp index 0f53f820e..b79dada4e 100644 --- a/python/mod_cvcuda/NormType.cpp +++ b/python/mod_cvcuda/NormType.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,7 +23,7 @@ namespace cvcudapy { void ExportNormType(py::module &m) { - py::enum_(m, "Norm", py::arithmetic()) + py::enum_(m, "Norm", "Vector norm types for distance and similarity computation.", py::arithmetic()) .value("HAMMING", NVCV_NORM_HAMMING, "Hamming distance") .value("L1", NVCV_NORM_L1, "Manhattan distance") .value("L2", NVCV_NORM_L2, "Euclidean distance"); diff --git a/python/mod_cvcuda/NvtxRange.hpp b/python/mod_cvcuda/NvtxRange.hpp new file mode 100644 index 000000000..63c692ea7 --- /dev/null +++ b/python/mod_cvcuda/NvtxRange.hpp @@ -0,0 +1,88 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#ifndef CVCUDA_PYTHON_NVTXRANGE_HPP +#define CVCUDA_PYTHON_NVTXRANGE_HPP + +#include + +namespace cvcudapy { + +// RAII push/pop of an NVTX range on the calling host thread: pushed on +// construction, popped on destruction, so the range spans the enclosing scope. +// Mirrors src/cvcuda/priv/Nvtx.hpp on the C++ core side; the Python module +// cannot include that private core header, so the helper is duplicated here. +class NvtxRange final +{ +public: + explicit NvtxRange(const char *name) noexcept + { + nvtxRangePushA(name); + } + + ~NvtxRange() noexcept + { + nvtxRangePop(); + } + + NvtxRange(const NvtxRange &) = delete; + NvtxRange(NvtxRange &&) = delete; + NvtxRange &operator=(const NvtxRange &) = delete; + NvtxRange &operator=(NvtxRange &&) = delete; +}; + +// Wrap a bound operator function so every Python-side call pushes an NVTX range +// named `name` for the duration of the call, nesting the C-API submit ranges +// underneath it. The returned closure keeps the exact parameter signature of +// `fn`, so pybind11 still introspects the argument types and the argument +// annotations / default values on the m.def() site continue to apply unchanged. +// `name` must have static lifetime (string literals do); only the pointer is +// captured. +template +auto NvtxTrace(const char *name, R (*fn)(Args...)) +{ + return [name, fn](Args... args) -> R + { + NvtxRange range(name); + // The lambda mirrors fn's parameter types (Args, not forwarding + // references) so pybind11 keeps the original signature; static_cast + // applies the same value-category cast std::forward would, and stays + // correct when Args is a reference type. + return fn(static_cast(args)...); + }; +} + +// Member-function overload, for bindings registered with `cls.def(...)` on a +// pybind11 class (e.g. Tensor.cuda / Image.cpu). The closure takes the instance +// as its first parameter, which is exactly how pybind11 binds a free callable as +// an instance method, so the resulting method keeps the original signature. Only +// const methods are wrapped today; add a non-const overload if that changes. +template +auto NvtxTrace(const char *name, R (C::*fn)(Args...) const) +{ + return [name, fn](const C &self, Args... args) -> R + { + NvtxRange range(name); + // See the free-function overload: static_cast reproduces + // std::forward's cast while keeping fn's exact signature for pybind11. + return (self.*fn)(static_cast(args)...); + }; +} + +} // namespace cvcudapy + +#endif // CVCUDA_PYTHON_NVTXRANGE_HPP diff --git a/python/mod_cvcuda/OsdElement.cpp b/python/mod_cvcuda/OsdElement.cpp index a6b602989..888b21f82 100644 --- a/python/mod_cvcuda/OsdElement.cpp +++ b/python/mod_cvcuda/OsdElement.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -24,6 +24,9 @@ #include #include +#include +#include + namespace cvcudapy { #define checkRuntime(call) check_runtime(call, #call, __LINE__, __FILE__) @@ -41,55 +44,93 @@ inline static bool check_runtime(cudaError_t e, const char *call, int line, cons namespace { -static NVCVPointI pytopoint(py::tuple point) +class OsdElementError : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + +NVCVPointI pytopoint(py::tuple point) { - if (point.size() > 2 || point.size() == 0) + if (point.size() > 2 || point.empty()) throw py::value_error("Invalid point size."); NVCVPointI ret; memset(&ret, 0, sizeof(ret)); - int *pr = (int *)&ret; - for (size_t i = 0; i < point.size(); ++i) + if (!point.empty()) + { + ret.x = point[0].cast(); + } + if (point.size() > 1) { - pr[i] = point[i].cast(); + ret.y = point[1].cast(); } return ret; } -static NVCVBoxI pytobox(py::tuple box) +NVCVBoxI pytobox(py::tuple box) { - if (box.size() > 4 || box.size() == 0) + if (box.size() > 4 || box.empty()) throw py::value_error("Invalid box size."); NVCVBoxI ret; memset(&ret, 0, sizeof(ret)); - int *pr = (int *)&ret; - for (size_t i = 0; i < box.size(); ++i) + if (!box.empty()) + { + ret.x = box[0].cast(); + } + if (box.size() > 1) + { + ret.y = box[1].cast(); + } + if (box.size() > 2) + { + ret.width = box[2].cast(); + } + if (box.size() > 3) { - pr[i] = box[i].cast(); + ret.height = box[3].cast(); } return ret; } -static NVCVColorRGBA pytocolor(py::tuple color) +NVCVColorRGBA pytocolor(py::tuple color) { - if (color.size() > 4 || color.size() == 0) + if (color.size() > 4 || color.empty()) throw py::value_error("Invalid color size."); NVCVColorRGBA ret; memset(&ret, 0, sizeof(ret)); ret.a = 255; - unsigned char *pr = (unsigned char *)&ret; + auto *pr = reinterpret_cast(&ret); for (size_t i = 0; i < color.size(); ++i) { - pr[i] = color[i].cast(); + pr[i] = static_cast(color[i].cast()); } return ret; } +// Inverse of pytopoint/pytobox/pytocolor. Used for `def_property_readonly` +// accessors so pybind11-stubgen sees a Python-importable `tuple` instead of +// raw C-API type names like `NVCVPointI`. +py::tuple pointtotuple(const NVCVPointI &p) +{ + return py::make_tuple(p.x, p.y); +} + +py::tuple boxtotuple(const NVCVBoxI &b) +{ + return py::make_tuple(b.x, b.y, b.width, b.height); +} + +py::tuple colortotuple(const NVCVColorRGBA &c) +{ + return py::make_tuple(c.r, c.g, c.b, c.a); +} + } // namespace void ExportBoxBlur(py::module &m) @@ -107,7 +148,9 @@ void ExportBoxBlur(py::module &m) return blurbox; }), "box"_a, "kernelSize"_a) - .def_readonly("box", &NVCVBlurBoxI::box, "Tuple describing a box: x-coordinate, y-coordinate, width, height.") + .def_property_readonly( + "box", [](const NVCVBlurBoxI &self) { return boxtotuple(self.box); }, + "Tuple describing a box: x-coordinate, y-coordinate, width, height.") .def_readonly("kernelSize", &NVCVBlurBoxI::kernelSize, "Kernel sizes of mean filter."); py::class_>(m, "BlurBoxesI") @@ -133,10 +176,16 @@ void ExportOSD(py::module &m) return bndbox; }), "box"_a, "thickness"_a, "borderColor"_a, "fillColor"_a) - .def_readonly("box", &NVCVBndBoxI::box, "Tuple describing a box: x-coordinate, y-coordinate, width, height.") + .def_property_readonly( + "box", [](const NVCVBndBoxI &self) { return boxtotuple(self.box); }, + "Tuple describing a box: x-coordinate, y-coordinate, width, height.") .def_readonly("thickness", &NVCVBndBoxI::thickness, "Border thickness of bounding box.") - .def_readonly("borderColor", &NVCVBndBoxI::borderColor, "Border color of bounding box.") - .def_readonly("fillColor", &NVCVBndBoxI::fillColor, "Filled color of bounding box."); + .def_property_readonly( + "borderColor", [](const NVCVBndBoxI &self) { return colortotuple(self.borderColor); }, + "Border color of bounding box.") + .def_property_readonly( + "fillColor", [](const NVCVBndBoxI &self) { return colortotuple(self.fillColor); }, + "Filled color of bounding box."); py::class_>(m, "BndBoxesI") .def(py::init([](const std::vector> &bndboxes_vec) @@ -160,11 +209,12 @@ void ExportOSD(py::module &m) py::buffer_info hSeg = segArray.request(); if (hSeg.ndim != 2) { - throw std::runtime_error("segArray dims must be 2!"); + throw OsdElementError("segArray dims must be 2!"); } - return NVCVSegment(pytobox(box), thickness, (float *)hSeg.ptr, hSeg.shape[0], hSeg.shape[1], - segThreshold, pytocolor(borderColor), pytocolor(segColor)); + return NVCVSegment(pytobox(box), thickness, (float *)hSeg.ptr, static_cast(hSeg.shape[0]), + static_cast(hSeg.shape[1]), segThreshold, pytocolor(borderColor), + pytocolor(segColor)); }), "box"_a, "thickness"_a, "segArray"_a, "segThreshold"_a, "borderColor"_a, "segColor"_a); @@ -179,9 +229,11 @@ void ExportOSD(py::module &m) return point; }), "centerPos"_a, "radius"_a, "color"_a) - .def_readonly("centerPos", &NVCVPoint::centerPos, "Center point.") + .def_property_readonly( + "centerPos", [](const NVCVPoint &self) { return pointtotuple(self.centerPos); }, "Center point.") .def_readonly("radius", &NVCVPoint::radius, "Point size.") - .def_readonly("color", &NVCVPoint::color, "Point color."); + .def_property_readonly( + "color", [](const NVCVPoint &self) { return colortotuple(self.color); }, "Point color."); py::class_(m, "Line", "Line") .def(py::init( @@ -196,10 +248,13 @@ void ExportOSD(py::module &m) return line; }), "pos0"_a, "pos1"_a, "thickness"_a, "color"_a, py::arg("interpolation") = true) - .def_readonly("pos0", &NVCVLine::pos0, "Start point.") - .def_readonly("pos1", &NVCVLine::pos1, "End point.") + .def_property_readonly( + "pos0", [](const NVCVLine &self) { return pointtotuple(self.pos0); }, "Start point.") + .def_property_readonly( + "pos1", [](const NVCVLine &self) { return pointtotuple(self.pos1); }, "End point.") .def_readonly("thickness", &NVCVLine::thickness, "Line thickness.") - .def_readonly("color", &NVCVLine::color, "Line color.") + .def_property_readonly( + "color", [](const NVCVLine &self) { return colortotuple(self.color); }, "Line color.") .def_readonly("interpolation", &NVCVLine::interpolation, "Default: true."); py::class_(m, "PolyLine") @@ -210,11 +265,12 @@ void ExportOSD(py::module &m) py::buffer_info points_info = points.request(); if (points_info.ndim != 2 || points_info.shape[1] != 2) { - throw std::runtime_error("points dims and shape[1] must be 2!"); + throw OsdElementError("points dims and shape[1] must be 2!"); } - return NVCVPolyLine((int32_t *)points_info.ptr, points_info.shape[0], thickness, isClosed, - pytocolor(borderColor), pytocolor(fillColor), interpolation); + return NVCVPolyLine((int32_t *)points_info.ptr, static_cast(points_info.shape[0]), + thickness, isClosed, pytocolor(borderColor), pytocolor(fillColor), + interpolation); }), "points"_a, "thickness"_a, "isClosed"_a, "borderColor"_a, "fillColor"_a, py::arg("interpolation") = true); @@ -236,13 +292,18 @@ void ExportOSD(py::module &m) }), "centerPos"_a, "width"_a, "height"_a, "yaw"_a, "thickness"_a, "borderColor"_a, "bgColor"_a, py::arg("interpolation") = false) - .def_readonly("centerPos", &NVCVRotatedBox::centerPos, "Center point.") + .def_property_readonly( + "centerPos", [](const NVCVRotatedBox &self) { return pointtotuple(self.centerPos); }, "Center point.") .def_readonly("width", &NVCVRotatedBox::width, "Box width.") .def_readonly("height", &NVCVRotatedBox::height, "Box height.") .def_readonly("yaw", &NVCVRotatedBox::yaw, "Box yaw.") .def_readonly("thickness", &NVCVRotatedBox::thickness, "Box border thickness.") - .def_readonly("borderColor", &NVCVRotatedBox::borderColor, "Circle border color.") - .def_readonly("bgColor", &NVCVRotatedBox::bgColor, "Circle filled color.") + .def_property_readonly( + "borderColor", [](const NVCVRotatedBox &self) { return colortotuple(self.borderColor); }, + "Rotated box border color.") + .def_property_readonly( + "bgColor", [](const NVCVRotatedBox &self) { return colortotuple(self.bgColor); }, + "Rotated box filled color.") .def_readonly("interpolation", &NVCVRotatedBox::interpolation, "Default: false."); py::class_(m, "Circle", "Circle") @@ -258,11 +319,15 @@ void ExportOSD(py::module &m) return circle; }), "centerPos"_a, "radius"_a, "thickness"_a, "borderColor"_a, "bgColor"_a) - .def_readonly("centerPos", &NVCVCircle::centerPos, "Center point.") + .def_property_readonly( + "centerPos", [](const NVCVCircle &self) { return pointtotuple(self.centerPos); }, "Center point.") .def_readonly("radius", &NVCVCircle::radius, "Circle radius.") .def_readonly("thickness", &NVCVCircle::thickness, "Circle thickness.") - .def_readonly("borderColor", &NVCVCircle::borderColor, "Circle border color.") - .def_readonly("bgColor", &NVCVCircle::bgColor, "Circle filled color."); + .def_property_readonly( + "borderColor", [](const NVCVCircle &self) { return colortotuple(self.borderColor); }, + "Circle border color.") + .def_property_readonly( + "bgColor", [](const NVCVCircle &self) { return colortotuple(self.bgColor); }, "Circle filled color."); py::class_(m, "Arrow", "Arrow") .def(py::init( @@ -279,11 +344,14 @@ void ExportOSD(py::module &m) return arrow; }), "pos0"_a, "pos1"_a, "arrowSize"_a, "thickness"_a, "color"_a, py::arg("interpolation") = false) - .def_readonly("pos0", &NVCVArrow::pos0, "Start point.") - .def_readonly("pos1", &NVCVArrow::pos1, "End point.") + .def_property_readonly( + "pos0", [](const NVCVArrow &self) { return pointtotuple(self.pos0); }, "Start point.") + .def_property_readonly( + "pos1", [](const NVCVArrow &self) { return pointtotuple(self.pos1); }, "End point.") .def_readonly("arrowSize", &NVCVArrow::arrowSize, "Arrow size.") .def_readonly("thickness", &NVCVArrow::thickness, "Arrow line thickness.") - .def_readonly("color", &NVCVArrow::color, "Arrow line color.") + .def_property_readonly( + "color", [](const NVCVArrow &self) { return colortotuple(self.color); }, "Arrow line color.") .def_readonly("interpolation", &NVCVArrow::interpolation, "Default: false."); py::enum_(m, "ClockFormat") @@ -309,57 +377,57 @@ void ExportOSD(py::module &m) for (const auto &elements_list : elements_list_vec) { std::vector> curVec; - for (size_t i = 0; i < elements_list.size(); ++i) + for (const auto &item : elements_list) { std::shared_ptr element; - if (pybind11::isinstance(elements_list[i])) + if (pybind11::isinstance(item)) { - auto rect = elements_list[i].cast(); + auto rect = item.cast(); element = std::make_shared(NVCVOSDType::NVCV_OSD_RECT, &rect); } - else if (pybind11::isinstance(elements_list[i])) + else if (pybind11::isinstance(item)) { - auto text = elements_list[i].cast(); + auto text = item.cast(); element = std::make_shared(NVCVOSDType::NVCV_OSD_TEXT, &text); } - else if (pybind11::isinstance(elements_list[i])) + else if (pybind11::isinstance(item)) { - auto segment = elements_list[i].cast(); + auto segment = item.cast(); element = std::make_shared(NVCVOSDType::NVCV_OSD_SEGMENT, &segment); } - else if (pybind11::isinstance(elements_list[i])) + else if (pybind11::isinstance(item)) { - auto point = elements_list[i].cast(); + auto point = item.cast(); element = std::make_shared(NVCVOSDType::NVCV_OSD_POINT, &point); } - else if (pybind11::isinstance(elements_list[i])) + else if (pybind11::isinstance(item)) { - auto line = elements_list[i].cast(); + auto line = item.cast(); element = std::make_shared(NVCVOSDType::NVCV_OSD_LINE, &line); } - else if (pybind11::isinstance(elements_list[i])) + else if (pybind11::isinstance(item)) { - auto pl = elements_list[i].cast(); + auto pl = item.cast(); element = std::make_shared(NVCVOSDType::NVCV_OSD_POLYLINE, &pl); } - else if (pybind11::isinstance(elements_list[i])) + else if (pybind11::isinstance(item)) { - auto rb = elements_list[i].cast(); + auto rb = item.cast(); element = std::make_shared(NVCVOSDType::NVCV_OSD_ROTATED_RECT, &rb); } - else if (pybind11::isinstance(elements_list[i])) + else if (pybind11::isinstance(item)) { - auto circle = elements_list[i].cast(); + auto circle = item.cast(); element = std::make_shared(NVCVOSDType::NVCV_OSD_CIRCLE, &circle); } - else if (pybind11::isinstance(elements_list[i])) + else if (pybind11::isinstance(item)) { - auto arrow = elements_list[i].cast(); + auto arrow = item.cast(); element = std::make_shared(NVCVOSDType::NVCV_OSD_ARROW, &arrow); } - else if (pybind11::isinstance(elements_list[i])) + else if (pybind11::isinstance(item)) { - auto clock = elements_list[i].cast(); + auto clock = item.cast(); element = std::make_shared(NVCVOSDType::NVCV_OSD_CLOCK, &clock); } else diff --git a/python/mod_cvcuda/PairwiseMatcherType.cpp b/python/mod_cvcuda/PairwiseMatcherType.cpp index deb975784..4b7b78f4c 100644 --- a/python/mod_cvcuda/PairwiseMatcherType.cpp +++ b/python/mod_cvcuda/PairwiseMatcherType.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,7 +23,8 @@ namespace cvcudapy { void ExportPairwiseMatcherType(py::module &m) { - py::enum_(m, "Matcher", py::arithmetic()).value("BRUTE_FORCE", NVCV_BRUTE_FORCE); + py::enum_(m, "Matcher", "Pairwise feature matcher algorithm types.", py::arithmetic()) + .value("BRUTE_FORCE", NVCV_BRUTE_FORCE, "Exhaustively compares all descriptor pairs to find the best match."); } } // namespace cvcudapy diff --git a/python/mod_cvcuda/RemapMapValueType.cpp b/python/mod_cvcuda/RemapMapValueType.cpp index 4fd3f9779..be8d62092 100644 --- a/python/mod_cvcuda/RemapMapValueType.cpp +++ b/python/mod_cvcuda/RemapMapValueType.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,10 +23,12 @@ namespace cvcudapy { void ExportRemapMapValueType(py::module &m) { - py::enum_(m, "Remap", py::arithmetic()) - .value("ABSOLUTE", NVCV_REMAP_ABSOLUTE) - .value("ABSOLUTE_NORMALIZED", NVCV_REMAP_ABSOLUTE_NORMALIZED) - .value("RELATIVE_NORMALIZED", NVCV_REMAP_RELATIVE_NORMALIZED); + py::enum_(m, "Remap", "Coordinate map value types for remap operations.", py::arithmetic()) + .value("ABSOLUTE", NVCV_REMAP_ABSOLUTE, "Map values are absolute pixel coordinates.") + .value("ABSOLUTE_NORMALIZED", NVCV_REMAP_ABSOLUTE_NORMALIZED, + "Map values are absolute coordinates normalized to [-1, 1].") + .value("RELATIVE_NORMALIZED", NVCV_REMAP_RELATIVE_NORMALIZED, + "Map values are relative offsets normalized to [-1, 1]."); } } // namespace cvcudapy diff --git a/python/mod_cvcuda/RoundMode.cpp b/python/mod_cvcuda/RoundMode.cpp new file mode 100644 index 000000000..17cd6661c --- /dev/null +++ b/python/mod_cvcuda/RoundMode.cpp @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "RoundMode.hpp" + +#include + +namespace cvcudapy { + +void ExportRoundMode(py::module &m) +{ + py::enum_(m, "Round", "Rounding modes for data-type conversion to integer types.") + .value("NEAREST", NVCV_ROUND_NEAREST, "Round to nearest, ties to even (default)") + .value("TRUNCATE", NVCV_ROUND_TRUNCATE, "Truncate toward zero (drop the fractional part)"); +} + +} // namespace cvcudapy diff --git a/python/mod_cvcuda/RoundMode.hpp b/python/mod_cvcuda/RoundMode.hpp new file mode 100644 index 000000000..08f0d73ea --- /dev/null +++ b/python/mod_cvcuda/RoundMode.hpp @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#ifndef NVCV_PYTHON_ROUND_MODE_HPP +#define NVCV_PYTHON_ROUND_MODE_HPP + +#include + +namespace cvcudapy { +namespace py = ::pybind11; + +void ExportRoundMode(py::module &m); + +} // namespace cvcudapy + +#endif // NVCV_PYTHON_ROUND_MODE_HPP diff --git a/python/mod_cvcuda/SIFTFlagType.cpp b/python/mod_cvcuda/SIFTFlagType.cpp index df707b397..a420d70bd 100644 --- a/python/mod_cvcuda/SIFTFlagType.cpp +++ b/python/mod_cvcuda/SIFTFlagType.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,9 +23,10 @@ namespace cvcudapy { void ExportSIFTFlagType(py::module &m) { - py::enum_(m, "SIFT", py::arithmetic()) - .value("USE_ORIGINAL_INPUT", NVCV_SIFT_USE_ORIGINAL_INPUT) - .value("USE_EXPANDED_INPUT", NVCV_SIFT_USE_EXPANDED_INPUT); + py::enum_(m, "SIFT", "SIFT algorithm configuration flags.", py::arithmetic()) + .value("USE_ORIGINAL_INPUT", NVCV_SIFT_USE_ORIGINAL_INPUT, "Use the original input image without upscaling.") + .value("USE_EXPANDED_INPUT", NVCV_SIFT_USE_EXPANDED_INPUT, + "Upscale the input image 2x before processing for improved keypoint detection."); } } // namespace cvcudapy diff --git a/python/mod_cvcuda/ThresholdType.cpp b/python/mod_cvcuda/ThresholdType.cpp index d867a70d9..93bd558d3 100644 --- a/python/mod_cvcuda/ThresholdType.cpp +++ b/python/mod_cvcuda/ThresholdType.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,7 +23,8 @@ namespace cvcudapy { void ExportThresholdType(py::module &m) { - py::enum_(m, "ThresholdType", py::arithmetic()) + py::enum_(m, "ThresholdType", "Thresholding types for binary image segmentation.", + py::arithmetic()) .value("BINARY", NVCV_THRESH_BINARY, "Value above threshold is set to maxval, otherwise set to 0") .value("BINARY_INV", NVCV_THRESH_BINARY_INV, "Value above threshold is set to 0, otherwise set to maxval") .value("TRUNC", NVCV_THRESH_TRUNC, "Value above threshold is set to threshold, otherwise unchanged") diff --git a/python/mod_cvcuda/WorkspaceCache.cpp b/python/mod_cvcuda/WorkspaceCache.cpp index 989b9d513..0327ed191 100644 --- a/python/mod_cvcuda/WorkspaceCache.cpp +++ b/python/mod_cvcuda/WorkspaceCache.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,8 +17,36 @@ #include "WorkspaceCache.hpp" +#include + +#include +#include +#include +#include + namespace cvcudapy { +namespace { + +template +void ReleaseWorkspaceMem(WorkspaceMemCache &cache, CachedWorkspaceMem &mem, + std::optional releaseStream) noexcept +{ + if (!mem) + return; + + try + { + cache.put(std::move(mem), releaseStream); + } + catch (...) + { + mem.reset(); + } +} + +} // namespace + WorkspaceLease::WorkspaceLease(WorkspaceCache *owner, CachedWorkspaceMem &&host, CachedWorkspaceMem &&pinned, CachedWorkspaceMem &&cuda, @@ -35,14 +63,14 @@ WorkspaceLease::WorkspaceLease(WorkspaceCache *owner, CachedWorkspaceMemm_host.put(std::move(m_host), m_hostReleaseStream); - if (m_pinned) - m_owner->m_pinned.put(std::move(m_pinned), m_pinnedReleaseStream); - if (m_cuda) - m_owner->m_cuda.put(std::move(m_cuda), m_hostReleaseStream); + if (m_owner == nullptr) + return; + + ReleaseWorkspaceMem(m_owner->m_host, m_host, m_hostReleaseStream); + ReleaseWorkspaceMem(m_owner->m_pinned, m_pinned, m_pinnedReleaseStream); + ReleaseWorkspaceMem(m_owner->m_cuda, m_cuda, m_cudaReleaseStream); } WorkspaceCache::WorkspaceCache(nvcv::Allocator allocator) @@ -58,12 +86,13 @@ WorkspaceCache::WorkspaceCache() { } -WorkspaceLease WorkspaceCache::get(cvcuda::WorkspaceRequirements req, std::optional hostAcquireStream, - std::optional hostReleaseStream, - std::optional pinnedAcquireStream, - std::optional pinnedReleaseStream, - std::optional cudaAcquireStream, - std::optional cudaReleaseStream) +WorkspaceLease WorkspaceCache::get(const cvcuda::WorkspaceRequirements &req, + std::optional hostAcquireStream, + std::optional hostReleaseStream, + std::optional pinnedAcquireStream, + std::optional pinnedReleaseStream, + std::optional cudaAcquireStream, + std::optional cudaReleaseStream) { return WorkspaceLease(this, m_host.get(req.hostMem, hostAcquireStream), m_pinned.get(req.pinnedMem, pinnedAcquireStream), m_cuda.get(req.cudaMem, cudaAcquireStream), @@ -72,8 +101,30 @@ WorkspaceLease WorkspaceCache::get(cvcuda::WorkspaceRequirements req, std::optio WorkspaceCache &WorkspaceCache::instance() { - static WorkspaceCache instance; - return instance; + // Per-device singleton: each CUDA device gets its own WorkspaceCache + // so that device memory allocations are always on the correct GPU. + // Uses shared_ptr for heap stability — unordered_map rehashing won't + // invalidate the objects that outstanding references point to. + static std::unordered_map> instances; + static std::shared_mutex instances_mutex; + + int dev = 0; + nvcvpy::util::CheckThrow(cudaGetDevice(&dev)); + + // Shared lock: concurrent readers when the entry already exists. + { + std::shared_lock lock(instances_mutex); + auto it = instances.find(dev); + if (it != instances.end()) + return *it->second; + } + + // Exclusive lock: serializes the one-time insertion of a new entry. + std::unique_lock lock(instances_mutex); + auto [it, _] = instances.try_emplace(dev, nullptr); + if (!it->second) + it->second = std::make_shared(); + return *it->second; } void WorkspaceCache::clear() diff --git a/python/mod_cvcuda/WorkspaceCache.hpp b/python/mod_cvcuda/WorkspaceCache.hpp index 19761e32a..42fd773df 100644 --- a/python/mod_cvcuda/WorkspaceCache.hpp +++ b/python/mod_cvcuda/WorkspaceCache.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -28,6 +28,9 @@ #include #include +#include +#include +#include #include #include @@ -59,12 +62,12 @@ class CachedWorkspaceMem : public cvcuda::WorkspaceMem { } - CachedWorkspaceMem(CachedWorkspaceMem &&mem) + CachedWorkspaceMem(CachedWorkspaceMem &&mem) noexcept { *this = std::move(mem); } - CachedWorkspaceMem &operator=(CachedWorkspaceMem &&mem) + CachedWorkspaceMem &operator=(CachedWorkspaceMem &&mem) noexcept { std::swap(wsMem(), mem.wsMem()); std::swap(m_destructor, mem.m_destructor); @@ -72,16 +75,27 @@ class CachedWorkspaceMem : public cvcuda::WorkspaceMem return *this; } - ~CachedWorkspaceMem() + ~CachedWorkspaceMem() noexcept { reset(); } - void reset() + void reset() noexcept { if (m_destructor) { - m_destructor(*this); + try + { + m_destructor(*this); + } + catch (const std::exception &e) + { + std::fprintf(stderr, "workspace memory cleanup failed: %s\n", e.what()); + } + catch (...) + { + std::fputs("workspace memory cleanup failed\n", stderr); + } m_destructor = {}; } wsMem() = {}; @@ -142,8 +156,7 @@ class WorkspaceMemCache return {}; ++m_outstandingAllocs; - auto opt = m_memCache.get(req.size, req.alignment, stream); - if (opt) + if (auto opt = m_memCache.get(req.size, req.alignment, stream); opt) return std::move(opt).value(); return create(req); @@ -162,26 +175,32 @@ class WorkspaceMemCache } private: - void *allocateMem(size_t size, size_t alignment) const + std::byte *allocateMem(size_t size, size_t alignment) const { + const auto allocSize = static_cast(size); + const auto allocAlignment = static_cast(alignment); + if constexpr (kind == MemoryKind::Host) - return m_alloc.hostMem().alloc(size, alignment); + return reinterpret_cast(m_alloc.hostMem().alloc(allocSize, allocAlignment)); else if constexpr (kind == MemoryKind::Pinned) - return m_alloc.hostPinnedMem().alloc(size, alignment); + return reinterpret_cast(m_alloc.hostPinnedMem().alloc(allocSize, allocAlignment)); else if constexpr (kind == MemoryKind::Cuda) - return m_alloc.cudaMem().alloc(size, alignment); + return reinterpret_cast(m_alloc.cudaMem().alloc(allocSize, allocAlignment)); else return nullptr; // should never happen } - void freeMem(void *mem, size_t size, size_t alignment) const + void freeMem(std::byte *mem, size_t size, size_t alignment) const { + const auto allocSize = static_cast(size); + const auto allocAlignment = static_cast(alignment); + if constexpr (kind == MemoryKind::Host) - return m_alloc.hostMem().free(mem, size, alignment); + return m_alloc.hostMem().free(mem, allocSize, allocAlignment); else if constexpr (kind == MemoryKind::Pinned) - return m_alloc.hostPinnedMem().free(mem, size, alignment); + return m_alloc.hostPinnedMem().free(mem, allocSize, allocAlignment); else if constexpr (kind == MemoryKind::Cuda) - return m_alloc.cudaMem().free(mem, size, alignment); + return m_alloc.cudaMem().free(mem, allocSize, allocAlignment); } auto getMemDeleter() const @@ -189,7 +208,7 @@ class WorkspaceMemCache return [this](cvcuda::WorkspaceMem &mem) { // free the memory - freeMem(mem.data, mem.req.size, mem.req.alignment); + freeMem(static_cast(mem.data), mem.req.size, mem.req.alignment); // return the event to the event cache if (mem.ready) { @@ -203,8 +222,8 @@ class WorkspaceMemCache { WorkspaceMemDestructor_t del = getMemDeleter(); - auto evt = nvcv::util::CudaEvent::Create(); - void *data = allocateMem(req.size, req.alignment); + auto evt = nvcv::util::CudaEvent::Create(); + std::byte *data = allocateMem(req.size, req.alignment); cvcuda::WorkspaceMem wsmem = {req, data, evt.get()}; @@ -229,10 +248,11 @@ class WorkspaceLease public: cvcuda::Workspace get() const { - return {m_host, m_pinned, m_cuda}; + return {static_cast(m_host), static_cast(m_pinned), + static_cast(m_cuda)}; } - ~WorkspaceLease(); + ~WorkspaceLease() noexcept; private: friend class WorkspaceCache; @@ -246,7 +266,9 @@ class WorkspaceLease CachedWorkspaceMem m_pinned; CachedWorkspaceMem m_cuda; - std::optional m_hostReleaseStream, m_pinnedReleaseStream, m_cudaReleaseStream; + std::optional m_hostReleaseStream; + std::optional m_pinnedReleaseStream; + std::optional m_cudaReleaseStream; }; class WorkspaceCache @@ -254,7 +276,7 @@ class WorkspaceCache public: WorkspaceCache(); - WorkspaceCache(nvcv::Allocator allocator); + explicit WorkspaceCache(nvcv::Allocator allocator); /** Gets a workspace with custom stream semantics * @@ -267,7 +289,7 @@ class WorkspaceCache * @param cudaAcquireStream The stream on which device memory will be initialky used * @param cudaReleaseStream The stream on which device memory usage will be completed */ - WorkspaceLease get(cvcuda::WorkspaceRequirements req, std::optional hostAcquireStream, + WorkspaceLease get(const cvcuda::WorkspaceRequirements &req, std::optional hostAcquireStream, std::optional hostReleaseStream, std::optional pinnedAcquireStream, std::optional pinnedReleaseStream, std::optional cudaAcquireStream, std::optional cudaReleaseStream); @@ -281,7 +303,7 @@ class WorkspaceCache * * NOTE: If these semantics are not honored by the user, the code should still be correct, just less efficient. */ - WorkspaceLease get(cvcuda::WorkspaceRequirements req, cudaStream_t stream) + WorkspaceLease get(const cvcuda::WorkspaceRequirements &req, cudaStream_t stream) { return get(req, std::nullopt, std::nullopt, std::nullopt, stream, stream, stream); } diff --git a/python/mod_cvcuda/gen_dtype_list.sh b/python/mod_cvcuda/gen_dtype_list.sh index 95404a25d..a5b0cc03d 100755 --- a/python/mod_cvcuda/gen_dtype_list.sh +++ b/python/mod_cvcuda/gen_dtype_list.sh @@ -1,6 +1,6 @@ #!/bin/bash -e -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,7 +15,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -if [ $# != 1 ]; then +if [[ $# != 1 ]]; then echo "Invalid arguments" echo "Usage: $(basename "$0") " exit 1 diff --git a/python/mod_cvcuda/gen_imgformat_list.sh b/python/mod_cvcuda/gen_imgformat_list.sh index 88c638466..3fc0ed922 100755 --- a/python/mod_cvcuda/gen_imgformat_list.sh +++ b/python/mod_cvcuda/gen_imgformat_list.sh @@ -1,6 +1,6 @@ #!/bin/bash -e -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,7 +15,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -if [ $# != 1 ]; then +if [[ $# != 1 ]]; then echo "Invalid arguments" echo "Usage: $(basename "$0") " exit 1 diff --git a/python/mod_cvcuda/include/nvcv/python/Array.hpp b/python/mod_cvcuda/include/nvcv/python/Array.hpp index 3149243f3..507ea30c3 100644 --- a/python/mod_cvcuda/include/nvcv/python/Array.hpp +++ b/python/mod_cvcuda/include/nvcv/python/Array.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -41,7 +41,7 @@ class Array public: static Array Create(int64_t length, nvcv::DataType dtype) { - PyObject *oarray = capi().Array_Create(length, dtype); + PyObject *oarray = capi().Array_Create(length, static_cast(dtype)); CheckCAPIError(); NVCV_ASSERT(oarray == nullptr); py::object pyarray = py::reinterpret_steal(oarray); @@ -79,7 +79,7 @@ struct type_caster : type_caster_base bool load(handle src, bool) { // Does it have the correct object type? - PyTypeObject *srctype = Py_TYPE(src.ptr()); + const PyTypeObject *srctype = Py_TYPE(src.ptr()); if (strcmp(name.text, srctype->tp_name) == 0) { value = cvpy::Array(reinterpret_borrow(src)); @@ -93,8 +93,7 @@ struct type_caster : type_caster_base static handle cast(cvpy::Array array, return_value_policy /* policy */, handle /*parent */) { - array.inc_ref(); // for some reason this is needed - return array; + return static_cast(array).release(); } }; diff --git a/python/mod_cvcuda/include/nvcv/python/CAPI.hpp b/python/mod_cvcuda/include/nvcv/python/CAPI.hpp index 9a440604e..b3646e251 100644 --- a/python/mod_cvcuda/include/nvcv/python/CAPI.hpp +++ b/python/mod_cvcuda/include/nvcv/python/CAPI.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,6 +25,8 @@ #include #include +#include + namespace pybind11::detail { // to force inclusion of "DataType.hpp" if needed struct type_caster; @@ -36,7 +38,13 @@ class ICacheItem; class IKey; class Container; -struct CAPI +class CAPIError : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + +struct CAPI // NOSONAR: Python extension ABI table must keep one slot per exported callback. { PyObject *(*DataType_ToPython)(NVCVDataType p); NVCVDataType (*DataType_FromPython)(PyObject *obj); @@ -85,15 +93,33 @@ struct CAPI void (*TensorBatch_Clear)(PyObject *tensorBatch); + // Batched sync-and-hold: takes the full resource list (same shape as + // Stream_HoldResources expects — list of (lockmode_str, resource) tuples) + // and runs the per-resource submitSync inside C++, then holdResources, + // in one C-ABI round trip. Avoids N pybind11 boundary crossings for ops + // with many tracked resources (erase, threshold, normalize, ...). Use + // this from ResourceGuard::commit() in lieu of N×Resource_SubmitSync + + // 1×Stream_HoldResources. + void (*Resources_SyncAndHold)(PyObject *stream, PyObject *resourceList); + + // Batched submit-sync only (no hold): inserts producer→consumer wait + // events for every resource in the list against `stream`. This is the + // correct point in time to insert sync barriers — it must run BEFORE + // `op->submit()` queues the consumer's kernel on `stream`, otherwise + // `cudaStreamWaitEvent` is enqueued behind the kernel and provides no + // protection. Pair with `Stream_HoldResources` at scope end (run via + // `ResourceGuard::run()` which handles both halves correctly). + void (*Resources_SubmitSyncOnly)(PyObject *stream, PyObject *resourceList); + // always add new functions at the end, and never change the function prototypes above. }; inline const CAPI &capi() { - static const CAPI *capi = reinterpret_cast(PyCapsule_Import("cvcuda._C_API", 0)); + static const auto *capi = reinterpret_cast(PyCapsule_Import("cvcuda._C_API", 0)); if (capi == nullptr) { - throw std::runtime_error("Can't load cvcuda C API"); + throw CAPIError("Can't load cvcuda C API"); } return *capi; } diff --git a/python/mod_cvcuda/include/nvcv/python/Cache.hpp b/python/mod_cvcuda/include/nvcv/python/Cache.hpp index cc83f8721..63841f507 100644 --- a/python/mod_cvcuda/include/nvcv/python/Cache.hpp +++ b/python/mod_cvcuda/include/nvcv/python/Cache.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,6 +20,8 @@ #include "CAPI.hpp" +#include +#include #include namespace nvcvpy { @@ -29,30 +31,48 @@ namespace py = ::pybind11; class IKey { public: + IKey() + { + util::CheckThrow(cudaGetDevice(&m_deviceId)); + } + virtual ~IKey() = default; + /// Returns the CUDA device ID captured at construction time. + int deviceId() const + { + return m_deviceId; + } + size_t hash() const { size_t h = doGetHash(); // Make hash dependent on concrete object type h ^= typeid(*this).hash_code() << 1; + + // Make hash dependent on device ID so that cache entries + // from different GPUs never collide. + h ^= std::hash{}(m_deviceId) << 2; return h; } bool operator==(const IKey &that) const { - if (typeid(*this) == typeid(that)) + if (typeid(*this) != typeid(that)) { - return doIsCompatible(that); + return false; } - else + if (m_deviceId != that.m_deviceId) { return false; } + return doIsCompatible(that); } private: + int m_deviceId = 0; + virtual size_t doGetHash() const = 0; virtual bool doIsCompatible(const IKey &that) const = 0; }; @@ -77,7 +97,7 @@ class Cache static std::vector> fetch(const IKey &key) { - std::unique_ptr list + std::unique_ptr list // NOSONAR: C API returns an owned null-terminated array. { capi().Cache_Fetch(&key) }; diff --git a/python/mod_cvcuda/include/nvcv/python/Container.hpp b/python/mod_cvcuda/include/nvcv/python/Container.hpp index 08d5ff695..af424d931 100644 --- a/python/mod_cvcuda/include/nvcv/python/Container.hpp +++ b/python/mod_cvcuda/include/nvcv/python/Container.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -34,7 +34,7 @@ class Container , public ICacheItem { public: - Container(py::object o) + explicit Container(py::object o) : Resource(o) { } diff --git a/python/mod_cvcuda/include/nvcv/python/DataType.hpp b/python/mod_cvcuda/include/nvcv/python/DataType.hpp index 6887917b1..eaf595080 100644 --- a/python/mod_cvcuda/include/nvcv/python/DataType.hpp +++ b/python/mod_cvcuda/include/nvcv/python/DataType.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/python/mod_cvcuda/include/nvcv/python/Image.hpp b/python/mod_cvcuda/include/nvcv/python/Image.hpp index 53283c50a..c99bfeeb4 100644 --- a/python/mod_cvcuda/include/nvcv/python/Image.hpp +++ b/python/mod_cvcuda/include/nvcv/python/Image.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -58,7 +58,6 @@ class Image private: friend struct py::detail::type_caster; - NVCVImageHandle m_handle; Image() = default; @@ -83,7 +82,7 @@ struct type_caster : type_caster_base bool load(handle src, bool) { // Does it have the correct object type? - PyTypeObject *srctype = Py_TYPE(src.ptr()); + const PyTypeObject *srctype = Py_TYPE(src.ptr()); if (strcmp(name.text, srctype->tp_name) == 0) { value = cvpy::Image(reinterpret_borrow(src)); @@ -97,8 +96,7 @@ struct type_caster : type_caster_base static handle cast(cvpy::Image tensor, return_value_policy /* policy */, handle /*parent */) { - tensor.inc_ref(); // for some reason this is needed - return tensor; + return static_cast(tensor).release(); } }; diff --git a/python/mod_cvcuda/include/nvcv/python/ImageBatchVarShape.hpp b/python/mod_cvcuda/include/nvcv/python/ImageBatchVarShape.hpp index 1d814496e..5e98ddbe8 100644 --- a/python/mod_cvcuda/include/nvcv/python/ImageBatchVarShape.hpp +++ b/python/mod_cvcuda/include/nvcv/python/ImageBatchVarShape.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -36,6 +36,12 @@ class ImageBatchVarShape , public nvcv::ImageBatchVarShape { public: + ImageBatchVarShape(const ImageBatchVarShape &) = default; + ImageBatchVarShape(ImageBatchVarShape &&) noexcept = default; + + ImageBatchVarShape &operator=(const ImageBatchVarShape &) = default; + ImageBatchVarShape &operator=(ImageBatchVarShape &&) noexcept = default; + static ImageBatchVarShape Create(int capacity) { PyObject *ovarshape = capi().ImageBatchVarShape_Create(capacity); @@ -46,27 +52,21 @@ class ImageBatchVarShape return ImageBatchVarShape(pyvarshape); } - // For manipulating the image list we can't call directly the - // nvcv::ImageBatchVarShape methods, it must go through the python - // bindings because it ends up storing a reference to the added images, to - // keep them alive. We can't do it here, things must be consistent. - // PROBLEM: these functions should be virtual but they aren't. - // We can't modify the image list through nvcv::ImageBatchVarShape - // or else the this image list to keep their alive won't be updated. - // We currently can't avoid this issue. - void pushBack(Image img) + // Python image wrappers must go through CAPI so the backing Python objects + // stay alive for as long as the batch references them. + void pushBackImage(Image img) { capi().ImageBatchVarShape_PushBack(this->ptr(), img.ptr()); CheckCAPIError(); } - void popBack(int cnt) + void popBackImages(int cnt) { capi().ImageBatchVarShape_PopBack(this->ptr(), cnt); CheckCAPIError(); } - void clear() + void clearImages() { capi().ImageBatchVarShape_Clear(this->ptr()); CheckCAPIError(); @@ -105,7 +105,7 @@ struct type_caster : type_caster_basetp_name) == 0) { value = cvpy::ImageBatchVarShape(reinterpret_borrow(src)); @@ -119,8 +119,7 @@ struct type_caster : type_caster_base(tensor).release(); } }; diff --git a/python/mod_cvcuda/include/nvcv/python/ImageFormat.hpp b/python/mod_cvcuda/include/nvcv/python/ImageFormat.hpp index 4c725e22d..39b9d0027 100644 --- a/python/mod_cvcuda/include/nvcv/python/ImageFormat.hpp +++ b/python/mod_cvcuda/include/nvcv/python/ImageFormat.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/python/mod_cvcuda/include/nvcv/python/Resource.hpp b/python/mod_cvcuda/include/nvcv/python/Resource.hpp index c903b7a8e..bc624928c 100644 --- a/python/mod_cvcuda/include/nvcv/python/Resource.hpp +++ b/python/mod_cvcuda/include/nvcv/python/Resource.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -29,11 +29,17 @@ class Resource : public py::object public: using py::object::object; - Resource(py::object o) + explicit Resource(py::object o) : py::object(o) { } + Resource(const Resource &) = default; + Resource(Resource &&) noexcept = default; + + Resource &operator=(const Resource &) = default; + Resource &operator=(Resource &&) noexcept = default; + virtual ~Resource() = default; }; diff --git a/python/mod_cvcuda/include/nvcv/python/ResourceGuard.hpp b/python/mod_cvcuda/include/nvcv/python/ResourceGuard.hpp index 4a68efb85..aab3c55cf 100644 --- a/python/mod_cvcuda/include/nvcv/python/ResourceGuard.hpp +++ b/python/mod_cvcuda/include/nvcv/python/ResourceGuard.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,6 +23,8 @@ #include "Resource.hpp" #include "Stream.hpp" +#include + namespace nvcvpy { namespace py = pybind11; @@ -30,14 +32,19 @@ namespace py = pybind11; class ResourceGuard { public: - ResourceGuard(Stream &stream) - : m_pyStream(stream) + explicit ResourceGuard(Stream &stream) + : m_pyStream(py::reinterpret_borrow(stream.ptr())) { } + ResourceGuard(const ResourceGuard &) = delete; + ResourceGuard(ResourceGuard &&) = delete; + ResourceGuard &operator=(const ResourceGuard &) = delete; + ResourceGuard &operator=(ResourceGuard &&) = delete; + ~ResourceGuard() { - this->commit(); + finishNoThrow(); } ResourceGuard &add(LockMode mode, std::initializer_list> resources) @@ -59,27 +66,215 @@ class ResourceGuard break; } + // Just append; sync is deferred to run() (or commit() in the legacy + // path used by out-of-tree consumers that haven't migrated). for (const std::reference_wrapper &r : resources) { - py::object pyRes = r.get(); - capi().Resource_SubmitSync(pyRes.ptr(), m_pyStream.ptr()); - CheckCAPIError(); + py::object pyRes = py::reinterpret_borrow(r.get().ptr()); m_resourcesPerLockMode.append(std::make_pair(pyLockMode, std::move(pyRes))); } return *this; } + // Run the consumer's kernel-submitting callable with sync barriers + // already in place. This inserts cudaStreamWaitEvent on the consumer + // stream for every previously-add()'d resource BEFORE invoking the + // callable, so the kernel(s) the callable queues are guaranteed to see + // the producer-stream work complete. The hold half (Stream_HoldResources) + // runs immediately after the callable, or during exception cleanup if the + // callable throws after partially submitting work. + // + // Use this in op shims: + // + // ResourceGuard guard(*pstream); + // guard.add(LOCK_MODE_READ, {input}); + // guard.add(LOCK_MODE_WRITE, {output}); + // guard.add(LOCK_MODE_NONE, {*op}); + // guard.run([&]() { op->submit(pstream->cudaHandle(), input, output); }); + // + // Calling run() before op->submit() is what makes sync barriers effective + // — `cudaStreamWaitEvent` only gates commands enqueued AFTER it on the + // same stream, so it must precede the kernel. The previous pattern + // (sync-on-destruction) inserted barriers behind the kernel and did not + // protect it. + template + void run(F &&fn) + { + // The C API callback reports failures only through a pending Python + // error (its implementation traps all C++ exceptions), so an explicit + // check needs no general catch clause here. + capi().Resources_SubmitSyncOnly(m_pyStream.ptr(), m_resourcesPerLockMode.ptr()); + if (PyErr_Occurred()) + { + // The callable never ran, so no consumer work needs a lifetime + // hold. Marking the guard finished stops the destructor from + // retrying the failed synchronization through the legacy path. + m_finished = true; + throw py::error_already_set(); + } + + // Set before invoking the callable: if it throws after (partially) + // submitting work, unwind runs ~ResourceGuard, which installs the + // hold while keeping the callable's exception primary. + m_synced = true; + std::forward(fn)(); + + // Finalize on the normal path so a hold failure reaches Python + // instead of being swallowed by the noexcept destructor. + commit(); + } + void commit() { - capi().Stream_HoldResources(m_pyStream.ptr(), m_resourcesPerLockMode.ptr()); - CheckCAPIError(); + if (m_finished) + { + return; + } + // Terminal before fallible code: makes commit() idempotent and + // prevents a destructor retry during unwind. + m_finished = true; + try + { + if (m_synced) + { + // Sync was performed by run() before the kernel; only the hold + // remains. Skips redundant per-resource submitSync work. + capi().Stream_HoldResources(m_pyStream.ptr(), m_resourcesPerLockMode.ptr()); + } + else + { + // Legacy path for guards that didn't use run() — typically + // out-of-tree code compiled against an older header. This + // still syncs and holds in one call, but the syncs are queued + // AFTER the consumer kernel and do not protect it from + // external-producer races. In-tree cvcuda ops should use + // run(). + capi().Resources_SyncAndHold(m_pyStream.ptr(), m_resourcesPerLockMode.ptr()); + } + CheckCAPIError(); + } + catch (...) + { + // The hold is what keeps resources alive until the submitted + // kernel completes; releasing the references now could free GPU + // memory the kernel still uses. Draining the stream proves every + // resource idle. The legacy combined call cannot distinguish a + // producer-sync failure from a hold failure, so no drain of this + // stream proves safety there — retain the references instead. + if (!m_synced || !drainStreamNoThrow()) + { + quarantineNoThrow(); + } + throw; + } } private: + // Saves any pending Python error at construction and re-instates it at + // scope exit, discarding errors raised in between. Keeps the primary + // exception authoritative across cleanup that may set secondary errors. + class PyErrPreserver + { + public: + PyErrPreserver() noexcept + { + PyErr_Fetch(&m_type, &m_value, &m_traceback); + } + + ~PyErrPreserver() noexcept + { + PyErr_Clear(); + PyErr_Restore(m_type, m_value, m_traceback); + } + + PyErrPreserver(const PyErrPreserver &) = delete; + PyErrPreserver &operator=(const PyErrPreserver &) = delete; + + private: + PyObject *m_type = nullptr; + PyObject *m_value = nullptr; + PyObject *m_traceback = nullptr; + }; + + void finishNoThrow() noexcept + { + if (m_finished) + { + return; + } + + // Preserve an exception already being propagated by the submitted + // callable; cleanup failures are secondary and must not replace it. + PyErrPreserver preserver; + try + { + this->commit(); + } + catch (const std::exception &e) + { + std::fprintf(stderr, "[cvcuda] ~ResourceGuard: commit() threw: %s\n", e.what()); + } + catch (...) + { + std::fprintf(stderr, "[cvcuda] ~ResourceGuard: commit() threw unknown exception\n"); + } + } + + bool drainStreamNoThrow() noexcept + { + PyErrPreserver preserver; + bool drained = false; + try + { + cudaStream_t stream = capi().Stream_GetCudaHandle(m_pyStream.ptr()); + if (!PyErr_Occurred()) + { + cudaError_t status; + { + // A foreign host callback queued on this stream may need + // the GIL in order to finish. + py::gil_scoped_release release; + status = cudaStreamSynchronize(stream); + } + + if (status == cudaSuccess) + { + drained = true; + } + else + { + std::fprintf(stderr, "[cvcuda] ResourceGuard: stream drain failed (%s); quarantining resources\n", + cudaGetErrorName(status)); + cudaGetLastError(); + } + } + } + catch (const std::exception &e) + { + std::fprintf(stderr, "[cvcuda] ResourceGuard: stream drain threw: %s; quarantining resources\n", e.what()); + } + catch (...) + { + std::fprintf(stderr, "[cvcuda] ResourceGuard: stream drain threw; quarantining resources\n"); + } + return drained; + } + + void quarantineNoThrow() noexcept + { + // Intentionally leak one reference to the list and stream. This path + // is only reached when completion cannot be proven; releasing or + // reusing a GPU resource that may still be active would be unsafe. + (void)m_resourcesPerLockMode.release(); + (void)m_pyStream.release(); + } + py::object m_pyStream; py::object m_pyLockMode; py::list m_resourcesPerLockMode; + bool m_synced = false; + bool m_finished = false; }; } // namespace nvcvpy diff --git a/python/mod_cvcuda/include/nvcv/python/Shape.hpp b/python/mod_cvcuda/include/nvcv/python/Shape.hpp index 536c61cad..7c02b5bcf 100644 --- a/python/mod_cvcuda/include/nvcv/python/Shape.hpp +++ b/python/mod_cvcuda/include/nvcv/python/Shape.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -48,26 +48,27 @@ inline nvcv::TensorShape CreateNVCVTensorShape(const Shape &shape, nvcv::TensorL { std::vector dims; dims.reserve(shape.size()); - for (size_t i = 0; i < shape.size(); ++i) + for (const pybind11::handle dim : shape) { - dims.push_back(shape[i].cast()); + dims.push_back(dim.cast()); } - return nvcv::TensorShape(dims.data(), dims.size(), layout); + return nvcv::TensorShape(dims.data(), static_cast(dims.size()), layout); } inline int64_t LengthIf1D(const Shape &shape) { int64_t largest = 1; - for (size_t i = 0; i < shape.size(); ++i) + for (const pybind11::handle dim : shape) { - if (shape[i].cast() > 1) + int64_t length = dim.cast(); + if (length > 1) { if (largest > 1) { throw std::invalid_argument("Non-supported array shape"); } - largest = shape[i].cast(); + largest = length; } } diff --git a/python/mod_cvcuda/include/nvcv/python/Size.hpp b/python/mod_cvcuda/include/nvcv/python/Size.hpp index 7a371e02a..88ff935bd 100644 --- a/python/mod_cvcuda/include/nvcv/python/Size.hpp +++ b/python/mod_cvcuda/include/nvcv/python/Size.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/python/mod_cvcuda/include/nvcv/python/Stream.hpp b/python/mod_cvcuda/include/nvcv/python/Stream.hpp index dbbc7d39a..51d2c4665 100644 --- a/python/mod_cvcuda/include/nvcv/python/Stream.hpp +++ b/python/mod_cvcuda/include/nvcv/python/Stream.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -72,7 +72,7 @@ struct type_caster : type_caster_base bool load(handle src, bool) { // Does it have the correct object type? - PyTypeObject *srctype = Py_TYPE(src.ptr()); + const PyTypeObject *srctype = Py_TYPE(src.ptr()); if (strcmp(name.text, srctype->tp_name) == 0) { value = cvpy::Stream(reinterpret_borrow(src)); @@ -86,8 +86,7 @@ struct type_caster : type_caster_base static handle cast(cvpy::Stream stream, return_value_policy /* policy */, handle /*parent */) { - stream.inc_ref(); // for some reason this is needed - return stream; + return stream.release(); } }; diff --git a/python/mod_cvcuda/include/nvcv/python/Tensor.hpp b/python/mod_cvcuda/include/nvcv/python/Tensor.hpp index 196bcadcc..a8cbcab74 100644 --- a/python/mod_cvcuda/include/nvcv/python/Tensor.hpp +++ b/python/mod_cvcuda/include/nvcv/python/Tensor.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -30,6 +30,7 @@ #include #include +#include namespace nvcvpy { @@ -40,6 +41,12 @@ class Tensor , public nvcv::Tensor { public: + Tensor(const Tensor &) = default; + Tensor(Tensor &&) noexcept = default; + + Tensor &operator=(const Tensor &) = default; + Tensor &operator=(Tensor &&) noexcept = default; + static Tensor Create(const nvcv::TensorShape &tshape, nvcv::DataType dtype, int rowalign = 0) { PyObject *otensor = capi().Tensor_Create(tshape.size(), &tshape[0], static_cast(dtype), @@ -80,6 +87,15 @@ class Tensor } }; +// Forward an optional Python Tensor wrapper to a C++ submit() taking +// const nvcv::Tensor&, substituting a caller-owned null-handle fallback when +// the optional is empty. Centralizes the explicit cast that silences Sonar's +// object-slicing finding at the operator call sites. +inline const nvcv::Tensor &AsNvcvTensor(const std::optional &opt, const nvcv::Tensor &nullTensor) +{ + return opt ? static_cast(*opt) : nullTensor; +} + } // namespace nvcvpy namespace pybind11::detail { @@ -94,7 +110,7 @@ struct type_caster : type_caster_base bool load(handle src, bool) { // Does it have the correct object type? - PyTypeObject *srctype = Py_TYPE(src.ptr()); + const PyTypeObject *srctype = Py_TYPE(src.ptr()); if (strcmp(name.text, srctype->tp_name) == 0) { value = cvpy::Tensor(reinterpret_borrow(src)); @@ -108,8 +124,7 @@ struct type_caster : type_caster_base static handle cast(cvpy::Tensor tensor, return_value_policy /* policy */, handle /*parent */) { - tensor.inc_ref(); // for some reason this is needed - return tensor; + return static_cast(tensor).release(); } }; diff --git a/python/mod_cvcuda/include/nvcv/python/TensorBatch.hpp b/python/mod_cvcuda/include/nvcv/python/TensorBatch.hpp index 466986b43..0cf092668 100644 --- a/python/mod_cvcuda/include/nvcv/python/TensorBatch.hpp +++ b/python/mod_cvcuda/include/nvcv/python/TensorBatch.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -36,6 +36,12 @@ class TensorBatch , public nvcv::TensorBatch { public: + TensorBatch(const TensorBatch &) = default; + TensorBatch(TensorBatch &&) noexcept = default; + + TensorBatch &operator=(const TensorBatch &) = default; + TensorBatch &operator=(TensorBatch &&) noexcept = default; + static TensorBatch Create(int capacity) { PyObject *tensorBatch = capi().TensorBatch_Create(capacity); @@ -46,19 +52,19 @@ class TensorBatch return TensorBatch(pytensorBatch); } - void pushBack(Tensor tensor) + void pushBackTensor(Tensor tensor) { capi().TensorBatch_PushBack(this->ptr(), tensor.ptr()); CheckCAPIError(); } - void popBack(int cnt) + void popBackTensors(int cnt) { capi().TensorBatch_PopBack(this->ptr(), cnt); CheckCAPIError(); } - void clear() + void clearTensors() { capi().TensorBatch_Clear(this->ptr()); CheckCAPIError(); @@ -94,7 +100,7 @@ struct type_caster : type_caster_base bool load(handle src, bool) { // Does it have the correct object type? - PyTypeObject *srctype = Py_TYPE(src.ptr()); + const PyTypeObject *srctype = Py_TYPE(src.ptr()); if (strcmp(name.text, srctype->tp_name) == 0) { value = cvpy::TensorBatch(reinterpret_borrow(src)); @@ -108,8 +114,7 @@ struct type_caster : type_caster_base static handle cast(cvpy::TensorBatch tensor, return_value_policy /* policy */, handle /*parent */) { - tensor.inc_ref(); // for some reason this is needed - return tensor; + return static_cast(tensor).release(); } }; diff --git a/python/mod_cvcuda/nvcv/Array.cpp b/python/mod_cvcuda/nvcv/Array.cpp index 88d1f6de9..f404a5180 100644 --- a/python/mod_cvcuda/nvcv/Array.cpp +++ b/python/mod_cvcuda/nvcv/Array.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -28,8 +28,20 @@ #include #include +#include + namespace nvcvpy::priv { +namespace { + +class ArrayError : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + +} // namespace + std::shared_ptr Array::CreateFromReqs(const nvcv::Array::Requirements &reqs) { std::vector> vcont = Cache::Instance().fetch(Key{reqs}); @@ -37,7 +49,7 @@ std::shared_ptr Array::CreateFromReqs(const nvcv::Array::Requirements &re // None found? if (vcont.empty()) { - std::shared_ptr array(new Array(reqs)); + std::shared_ptr array(new Array(reqs)); // NOSONAR: constructor is private. array->impl().resize(reqs.capacity); Cache::Instance().add(*array); return array; @@ -69,15 +81,15 @@ NVCVArrayData FillNVCVArrayData(const DLTensor &tensor, NVCVArrayBufferType bufT NVCVArrayData arrayData = {}; // dtype ------------ - arrayData.dtype = py::cast(ToDType(ToNVCVDataType(tensor.dtype))); + arrayData.dtype = static_cast(py::cast(ToDType(ToNVCVDataType(tensor.dtype)))); // rank ------------ { - // TODO: Add 0D support + // REVISIT: Add 0D support int rank = tensor.ndim == 0 ? 1 : tensor.ndim; if (rank != 1) { - throw std::invalid_argument(util::FormatString("The tensor rank must be 1 not %d", rank)); + throw std::invalid_argument(util::ConcatString("The tensor rank must be 1 not ", rank)); } } @@ -87,11 +99,11 @@ NVCVArrayData FillNVCVArrayData(const DLTensor &tensor, NVCVArrayBufferType bufT // buffer type ------------ if (IsCudaAccessible(tensor.device.device_type)) { - arrayData.bufferType = NVCV_ARRAY_BUFFER_HOST; + arrayData.bufferType = bufType; } else { - throw std::runtime_error("Only CUDA-accessible arrays are supported for now"); + throw ArrayError("Only CUDA-accessible arrays are supported for now"); } NVCVArrayBufferStrided &dataStrided = arrayData.buffer.strided; @@ -129,7 +141,21 @@ std::shared_ptr Array::Wrap(ExternalBuffer &buffer) // being used. They aren't reusable anyway. Cache::Instance().removeAllNotInUseMatching(key); - auto array = std::shared_ptr(new Array(data, py::cast(buffer.shared_from_this()))); + auto array = std::shared_ptr( // NOSONAR: constructor is private. + new Array(data, py::cast(buffer.shared_from_this()))); + + // Seed the array's Resource state with the producer stream from CAI so + // the first cvcuda op reading this array inserts the proper cross-stream + // wait. + if (!buffer.producerIsSynced() && buffer.producerStream() != nullptr) + { + int device = buffer.producerDevice(); + if (device < 0) + { + util::CheckThrow(cudaGetDevice(&device)); + } + array->seedLastStream(buffer.producerStream(), device); + } // Need to add wrappers to cache so that they don't get destroyed by // the cuda stream when they're last used, and python script isn't @@ -146,7 +172,7 @@ std::shared_ptr Array::ResizeArray(Array &array, int64_t length) auto array_impl = array.impl(); array_impl.resize(length); - auto new_array = std::shared_ptr(new Array(std::move(array_impl))); + auto new_array = std::shared_ptr(new Array(std::move(array_impl))); // NOSONAR: constructor is private. // Need to add wrappers to cache so that they don't get destroyed by // the cuda stream when they're last used, and python script isn't @@ -179,7 +205,6 @@ Array::Array(const nvcv::Array::Requirements &reqs) Array::Array(const nvcv::ArrayData &data, py::object wrappedObject) : m_impl{nvcv::ArrayWrapData(data)} - , m_key{} , m_size_inbytes{doComputeSizeInBytes(nvcv::Array::Requirements())} , m_wrappedObject(wrappedObject) { @@ -187,12 +212,11 @@ Array::Array(const nvcv::ArrayData &data, py::object wrappedObject) Array::Array(nvcv::Array &&array) : m_impl{std::move(array)} - , m_key{} , m_size_inbytes{doComputeSizeInBytes(nvcv::Array::Requirements())} { } -int64_t Array::doComputeSizeInBytes(const nvcv::Array::Requirements &reqs) +int64_t Array::doComputeSizeInBytes(const nvcv::Array::Requirements &reqs) const { int64_t size_inbytes; util::CheckThrow(nvcvMemRequirementsCalcTotalSizeBytes(&(reqs.mem.cudaMem), &size_inbytes)); @@ -206,16 +230,6 @@ int64_t Array::GetSizeInBytes() const return m_size_inbytes; } -std::shared_ptr Array::shared_from_this() -{ - return std::static_pointer_cast(Container::shared_from_this()); -} - -std::shared_ptr Array::shared_from_this() const -{ - return std::static_pointer_cast(Container::shared_from_this()); -} - nvcv::Array &Array::impl() { return m_impl; @@ -252,7 +266,7 @@ Array::Key::Key(const nvcv::Array::Requirements &reqs) } Array::Key::Key(int64_t length, nvcv::DataType dtype) - : m_length(std::move(length)) + : m_length(length) , m_dtype(dtype) , m_wrapper(false) { @@ -273,7 +287,7 @@ size_t Array::Key::doGetHash() const bool Array::Key::doIsCompatible(const IKey &that_) const { - const Key &that = static_cast(that_); + const auto &that = static_cast(that_); // Wrapper key's all compare equal, are they can't be used // and whenever we query the cache for wrappers, we really @@ -297,27 +311,33 @@ auto Array::key() const -> const Key & return m_key; } -static py::object ToPython(const nvcv::ArrayData &arrayData, py::object owner) +static py::object ToPython(const nvcv::ArrayData &arrayData, py::object owner, cudaStream_t exportStream, + bool setExportStream) { py::object out; auto data = arrayData.cast(); if (!data) { - throw std::runtime_error("Only tensors with pitch-linear data can be exported"); + throw ArrayError("Only tensors with pitch-linear data can be exported"); } DLPackTensor dlTensor(*data); - return ExternalBuffer::Create(std::move(dlTensor), owner); + return ExternalBuffer::Create(std::move(dlTensor), owner, exportStream, setExportStream); } py::object Array::cuda() const { nvcv::ArrayData arrayData = m_impl.exportData(); + // Advertise the stream the array's data was last written on via CAI + // `stream` so downstream consumers can sync. + cudaStream_t lastStream = this->getLastStreamHandle(); + bool setStream = lastStream != nullptr; + // Note: we can't cache the returned ExternalBuffer because it is holding // a reference to us. Doing so would lead to mem leaks. - return ToPython(arrayData, py::cast(this->shared_from_this())); + return ToPython(arrayData, py::cast(SharedContainerFrom(*this)), lastStream, setStream); } std::ostream &operator<<(std::ostream &out, const Array &array) diff --git a/python/mod_cvcuda/nvcv/Array.hpp b/python/mod_cvcuda/nvcv/Array.hpp index fbc42ed7c..eac2b832c 100644 --- a/python/mod_cvcuda/nvcv/Array.hpp +++ b/python/mod_cvcuda/nvcv/Array.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -50,9 +50,6 @@ class Array : public Container std::shared_ptr Resize(Shape shape); std::shared_ptr Resize(int64_t length); - std::shared_ptr shared_from_this(); - std::shared_ptr shared_from_this() const; - Shape shape() const; nvcv::DataType dtype() const; int rank() const; @@ -77,22 +74,22 @@ class Array : public Container nvcv::DataType m_dtype; bool m_wrapper; - virtual size_t doGetHash() const override; - virtual bool doIsCompatible(const IKey &that) const override; + size_t doGetHash() const override; + bool doIsCompatible(const IKey &that) const override; }; - virtual const Key &key() const override; + const Key &key() const override; int64_t GetSizeInBytes() const override; py::object cuda() const; private: - Array(const nvcv::Array::Requirements &reqs); + explicit Array(const nvcv::Array::Requirements &reqs); Array(const nvcv::ArrayData &data, py::object wrappedObject); - Array(nvcv::Array &&array); + explicit Array(nvcv::Array &&array); - int64_t doComputeSizeInBytes(const nvcv::Array::Requirements &reqs); + int64_t doComputeSizeInBytes(const nvcv::Array::Requirements &reqs) const; nvcv::Array m_impl; // must come before m_key Key m_key; diff --git a/python/mod_cvcuda/nvcv/CAPI.cpp b/python/mod_cvcuda/nvcv/CAPI.cpp index fdb6be871..f7a93b88e 100644 --- a/python/mod_cvcuda/nvcv/CAPI.cpp +++ b/python/mod_cvcuda/nvcv/CAPI.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -32,10 +32,20 @@ #include #include +#include +#include +#include + namespace nvcvpy::priv { namespace { +class CAPIResourceError : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + // We need to catch any exceptions and set the appropriate PyError prior to crossing any C API boundry #define CATCH_RETURN_DEFAULT(return_value, error_message) \ catch (const std::exception &e) \ @@ -49,6 +59,37 @@ namespace { return return_value; \ } +// --- Test-only failure injection --------------------------------------------- +// +// Deterministic failure toggles for the ResourceGuard lifetime regressions in +// tests/cvcuda/python/test_resourceguard.py, armed from Python through the +// private cvcuda._test submodule (see ExportCAPITestHooks). An armed toggle +// makes the corresponding C API callback fail through its production +// CATCH_RETURN_DEFAULT path, so the error the guard observes is shaped exactly +// like a real failure. Plain bools: both the toggles and every injected +// callback run under the GIL. + +struct TestFailureInjection +{ + bool streamHoldResources = false; + bool resourcesSubmitSyncOnly = false; + bool resourcesSyncAndHold = false; +}; + +TestFailureInjection &testFailureInjection() +{ + static TestFailureInjection injection; + return injection; +} + +void ThrowIfInjected(bool armed, const char *what) +{ + if (armed) + { + throw CAPIResourceError(what); + } +} + template std::shared_ptr ToSharedObj(PyObject *obj) { @@ -61,7 +102,7 @@ T ToObj(PyObject *obj) return py::reinterpret_borrow(obj).cast(); } -extern "C" PyObject *ImplDataType_ToPython(NVCVDataType p) +PyObject *ImplDataType_ToPython(NVCVDataType p) { try { @@ -71,16 +112,16 @@ extern "C" PyObject *ImplDataType_ToPython(NVCVDataType p) CATCH_RETURN_DEFAULT(nullptr, "Casting PyObject from NVCVDataType failed") } -extern "C" NVCVDataType ImplDataType_FromPython(PyObject *obj) +NVCVDataType ImplDataType_FromPython(PyObject *obj) { try { - return ToObj(obj); + return static_cast(ToObj(obj)); } CATCH_RETURN_DEFAULT(0, "Casting nvcv::DataType from PyObject failed") } -extern "C" PyObject *ImplImageFormat_ToPython(NVCVImageFormat p) +PyObject *ImplImageFormat_ToPython(NVCVImageFormat p) { try { @@ -90,31 +131,31 @@ extern "C" PyObject *ImplImageFormat_ToPython(NVCVImageFormat p) CATCH_RETURN_DEFAULT(nullptr, "Casting PyObject from NVCVImageFormat failed") } -extern "C" NVCVImageFormat ImplImageFormat_FromPython(PyObject *obj) +NVCVImageFormat ImplImageFormat_FromPython(PyObject *obj) { try { - return ToObj(obj); + return static_cast(ToObj(obj)); } CATCH_RETURN_DEFAULT(0, "Casting nvcv::ImageFormat from PyObject failed") } -extern "C" NVCVTensorHandle ImplTensor_GetHandle(PyObject *obj) +NVCVTensorHandle ImplTensor_GetHandle(PyObject *obj) { try { return ToSharedObj(obj)->impl().handle(); } - CATCH_RETURN_DEFAULT(0, "Getting Tensor handle from PyObject failed") + CATCH_RETURN_DEFAULT(nullptr, "Getting Tensor handle from PyObject failed") } -extern "C" NVCVArrayHandle ImplArray_GetHandle(PyObject *obj) +NVCVArrayHandle ImplArray_GetHandle(PyObject *obj) { try { return ToSharedObj(obj)->impl().handle(); } - CATCH_RETURN_DEFAULT(0, "Getting Array handle from PyObject failed") + CATCH_RETURN_DEFAULT(nullptr, "Getting Array handle from PyObject failed") } LockMode ToLockMode(PyObject *_mode) @@ -138,11 +179,176 @@ LockMode ToLockMode(PyObject *_mode) } else { - throw std::runtime_error("Lock mode not understood: '" + s + "'"); + throw CAPIResourceError("Lock mode not understood: '" + s + "'"); } } -extern "C" void ImplResource_SubmitSync(PyObject *res, PyObject *stream) +// ----- Resource-pointer cache (weakref-invalidated) ------------------------- +// +// Inside the per-resource hot loop of Resources_SyncAndHold, the dominant +// per-item cost is the pybind11 reverse-cast `ToSharedObj(PyObject*)` +// — a type-registry lookup plus holder extraction, on the order of ~1 us per +// resource. Bench-style workloads call the same op repeatedly with the same +// Tensor / ImageBatch wrappers, so caching by raw PyObject* skips the cast on +// repeats. +// +// The hazard with raw PyObject* keys is identity reuse: when a wrapper is +// destroyed, Python's allocator can recycle its memory address for an +// unrelated new object. A naive cache would then return a stale Resource for +// the new wrapper. We block this by registering a weakref-with-callback on +// each cached PyObject; the callback fires inside the wrapper's tp_dealloc, +// *before* the address can be reused, and erases the cache entry. +// +// All access happens under the GIL (this code is reachable only from C-API +// entrypoints invoked by Python), so no extra synchronization is required. + +// Self-decref'ing PyObject reference holder (for weakref strong refs). +struct PyRef +{ + PyObject *p = nullptr; + + PyRef() = default; + + explicit PyRef(PyObject *x) + : p(x) + { + } + + ~PyRef() + { + if (p) + { + Py_DECREF(p); + } + } + + PyRef(const PyRef &) = delete; + PyRef &operator=(const PyRef &) = delete; + + PyRef(PyRef &&o) noexcept + : p(o.p) + { + o.p = nullptr; + } + + PyRef &operator=(PyRef &&o) noexcept + { + if (this != &o) + { + if (p) + { + Py_DECREF(p); + } + p = o.p; + o.p = nullptr; + } + return *this; + } +}; + +struct ResCacheEntry +{ + std::shared_ptr res; + PyRef weakref; // owns a strong ref; ~PyRef -> Py_DECREF on erase +}; + +std::unordered_map &ResCache() +{ + static std::unordered_map cache; + return cache; +} + +std::unordered_map &WeakrefToObj() +{ + // weakref* -> tracked obj* (borrowed) + static std::unordered_map cache; + return cache; +} + +PyObject *ImplResCache_Invalidate(PyObject * /*self*/, PyObject *weakref) +{ + auto &weakrefToObj = WeakrefToObj(); + if (auto wrIt = weakrefToObj.find(weakref); wrIt != weakrefToObj.end()) + { + // Erasing the cache entry destroys the ResCacheEntry, whose ~PyRef + // releases our strong ref on `weakref`. CPython's weakref machinery + // bumps the weakref refcount before invoking us, so the implied + // decref-to-zero here cannot dangle the object we're inside of. + ResCache().erase(wrIt->second); + weakrefToObj.erase(wrIt); + } + Py_RETURN_NONE; +} + +PyMethodDef &InvalidateMethodDef() +{ + static PyMethodDef methodDef = {"_resCache_invalidate", ImplResCache_Invalidate, METH_O, nullptr}; + return methodDef; +} + +PyObject *GetResCacheInvalidateCallback() +{ + static PyObject *cb = []() + { + // Module-lifetime, never released. PyCFunction_NewEx requires a + // module-or-None reference; nullptr is fine for our purposes. + return PyCFunction_NewEx(&InvalidateMethodDef(), nullptr, nullptr); + }(); + return cb; +} + +// Cache-aware Resource extractor. On miss, performs the pybind11 reverse cast +// and registers a weakref so the entry self-invalidates when the wrapper is +// destroyed. Falls back to plain extraction if weakrefs aren't supported. +std::shared_ptr ExtractResourceCached(PyObject *obj) +{ + auto &resCache = ResCache(); + if (auto it = resCache.find(obj); it != resCache.end()) + { + return it->second.res; + } + auto res = ToSharedObj(obj); + PyObject *cb = GetResCacheInvalidateCallback(); + PyObject *wr_raw = (cb != nullptr) ? PyWeakref_NewRef(obj, cb) : nullptr; + if (wr_raw == nullptr) + { + // Wrapper type doesn't support weakrefs (or callback init failed). + // Skip caching to preserve correctness; pay the cast every call. + PyErr_Clear(); + return res; + } + + // Take RAII ownership of the new weakref reference immediately so any + // throw between here and the successful inserts cleans it up. + PyRef wr_holder{wr_raw}; + auto &weakrefToObj = WeakrefToObj(); + try + { + // Reverse map first so the order matches the natural cleanup path: + // if the second emplace below throws, we erase this entry in the + // catch block. + weakrefToObj.try_emplace(wr_raw, obj); + // Move ownership of the weakref ref into the cache entry. If the + // emplace fails, the temporary ResCacheEntry's destructor (~PyRef) + // decrefs wr_raw — wr_holder is then moved-from and is a no-op at + // its own destruction. + resCache.try_emplace(obj, ResCacheEntry{res, std::move(wr_holder)}); + } + catch (...) + { + // erase() by key is safe even when the entry was never inserted + // (no-op) and even when wr_raw is now a dangling pointer (we don't + // dereference it — the unordered_map only hashes/compares the + // pointer value). + weakrefToObj.erase(wr_raw); + throw; + } + return res; +} + +// ----- end of Resource-pointer cache ---------------------------------------- + +void ImplResource_SubmitSync(PyObject *res, PyObject *stream) { try { @@ -151,24 +357,41 @@ extern "C" void ImplResource_SubmitSync(PyObject *res, PyObject *stream) CATCH_RETURN_DEFAULT(, "Submit sync failed") } -extern "C" void ImplStream_HoldResources(PyObject *stream, PyObject *resourceList) +void ImplStream_HoldResources(PyObject *stream, PyObject *resourceList) { try { + ThrowIfInjected(testFailureInjection().streamHoldResources, "injected Stream_HoldResources failure"); + py::list resList = ToObj(resourceList); LockResources resVector; + const PyObject *lastModePtr = nullptr; + LockMode lastMode = LockMode::LOCK_MODE_NONE; + for (py::handle h : resList) { py::tuple t = h.cast(); if (t.size() != 2) { - throw std::runtime_error("ResourcePerMode tuple must have two elements"); + throw CAPIResourceError("ResourcePerMode tuple must have two elements"); + } + + PyObject *modePtr = t[0].ptr(); + LockMode lockMode; + if (modePtr == lastModePtr) + { + lockMode = lastMode; + } + else + { + lockMode = ToLockMode(modePtr); + lastModePtr = modePtr; + lastMode = lockMode; } - auto lockMode = ToLockMode(t[0].ptr()); - auto res = ToSharedObj(t[1].ptr()); + auto res = ExtractResourceCached(t[1].ptr()); resVector.emplace(lockMode, res); } @@ -178,26 +401,124 @@ extern "C" void ImplStream_HoldResources(PyObject *stream, PyObject *resourceLis CATCH_RETURN_DEFAULT(, "Hold resources failed") } -extern "C" PyObject *ImplStream_GetCurrent() +// Batched submit-sync only (no holdResources). Used by ResourceGuard::run() +// to insert all wait_events on the consumer stream BEFORE the op's kernel is +// queued. The hold half runs at guard destruction via Stream_HoldResources. +// +// Splitting the sync from the hold matters because cudaStreamWaitEvent must +// be enqueued before the kernel it's meant to gate — the previous combined +// Resources_SyncAndHold (called at guard destruction) ran AFTER the kernel +// and was therefore a no-op for the kernel it was supposed to protect. +void ImplResources_SubmitSyncOnly(PyObject *stream, PyObject *resourceList) +{ + try + { + ThrowIfInjected(testFailureInjection().resourcesSubmitSyncOnly, "injected Resources_SubmitSyncOnly failure"); + + py::list resList = ToObj(resourceList); + auto pyStream = ToSharedObj(stream); + Stream &streamRef = *pyStream; + + for (py::handle h : resList) + { + py::tuple t = h.cast(); + if (t.size() != 2) + { + throw CAPIResourceError("ResourcePerMode tuple must have two elements"); + } + + // Per-resource cast goes through the weakref-invalidated cache so + // repeat ops on the same wrapper skip the pybind11 holder lookup. + auto res = ExtractResourceCached(t[1].ptr()); + res->submitSync(streamRef); + } + } + CATCH_RETURN_DEFAULT(, "Submit sync only failed") +} + +// Batched equivalent of "for r in list: Resource_SubmitSync(r, stream)" followed +// by Stream_HoldResources, executed entirely in C++. Saves N pybind11/C-ABI +// boundary crossings on ops that track many resources. Per-resource cast goes +// through ExtractResourceCached which dedups repeat lookups for the same +// PyObject; see the cache section above for the identity-reuse safety story. +// +// NOTE: this combined call is retained for backward compatibility with +// out-of-tree consumers that haven't migrated to the run()-based pattern. +// New code should use Resources_SubmitSyncOnly + Stream_HoldResources via +// ResourceGuard::run(). +void ImplResources_SyncAndHold(PyObject *stream, PyObject *resourceList) { try { - return py::cast(Stream::Current().shared_from_this()).ptr(); + ThrowIfInjected(testFailureInjection().resourcesSyncAndHold, "injected Resources_SyncAndHold failure"); + + py::list resList = ToObj(resourceList); + auto pyStream = ToSharedObj(stream); + Stream &streamRef = *pyStream; + + LockResources resVector; + + // Within a single commit() call, contiguous items typically share the + // same py::str instance for their lock mode (one .add(mode, {...}) + // call -> N items with the same mode). A one-slot last-seen cache + // skips the string parse on those. + const PyObject *lastModePtr = nullptr; + LockMode lastMode = LockMode::LOCK_MODE_NONE; + + for (py::handle h : resList) + { + py::tuple t = h.cast(); + if (t.size() != 2) + { + throw CAPIResourceError("ResourcePerMode tuple must have two elements"); + } + + PyObject *modePtr = t[0].ptr(); + LockMode lockMode; + if (modePtr == lastModePtr) + { + lockMode = lastMode; + } + else + { + lockMode = ToLockMode(modePtr); + lastModePtr = modePtr; + lastMode = lockMode; + } + + auto res = ExtractResourceCached(t[1].ptr()); + + // Per-resource stream-ordering registration. + res->submitSync(streamRef); + + resVector.emplace(lockMode, res); + } + + pyStream->holdResources(std::move(resVector)); + } + CATCH_RETURN_DEFAULT(, "Sync and hold resources failed") +} + +PyObject *ImplStream_GetCurrent() +{ + try + { + return py::cast(Stream::Current().sharedStream()).ptr(); } CATCH_RETURN_DEFAULT(nullptr, "Get current stream failed") } -extern "C" cudaStream_t ImplStream_GetCudaHandle(PyObject *stream) +cudaStream_t ImplStream_GetCudaHandle(PyObject *stream) { try { return ToSharedObj(stream)->handle(); } - CATCH_RETURN_DEFAULT(0, "Get cuda handle failed") + CATCH_RETURN_DEFAULT(nullptr, "Get cuda handle failed") } -extern "C" PyObject *ImplTensor_Create(int32_t ndim, const int64_t *shape, NVCVDataType dtype, NVCVTensorLayout layout, - int32_t rowalign) +PyObject *ImplTensor_Create(int32_t ndim, const int64_t *shape, NVCVDataType dtype, NVCVTensorLayout layout, + int32_t rowalign) { try { @@ -206,15 +527,16 @@ extern "C" PyObject *ImplTensor_Create(int32_t ndim, const int64_t *shape, NVCVD { cxxLayout = nvcv::TensorLayout(layout); } + const nvcv::TensorLayout tensorLayout = cxxLayout.value_or(nvcv::TENSOR_NONE); - std::shared_ptr tensor = Tensor::Create(CreateShape(nvcv::TensorShape(shape, ndim, layout)), - nvcv::DataType{dtype}, std::move(layout), rowalign); + std::shared_ptr tensor = Tensor::Create(CreateShape(nvcv::TensorShape(shape, ndim, tensorLayout)), + nvcv::DataType{dtype}, std::move(cxxLayout), rowalign); return py::cast(std::move(tensor)).release().ptr(); } CATCH_RETURN_DEFAULT(nullptr, "Tensor create failed") } -extern "C" PyObject *ImplArray_Create(int64_t length, NVCVDataType dtype) +PyObject *ImplArray_Create(int64_t length, NVCVDataType dtype) { try { @@ -225,7 +547,7 @@ extern "C" PyObject *ImplArray_Create(int64_t length, NVCVDataType dtype) CATCH_RETURN_DEFAULT(nullptr, "Array create failed") } -extern "C" PyObject *ImplImageBatchVarShape_Create(int32_t capacity) +PyObject *ImplImageBatchVarShape_Create(int32_t capacity) { try { @@ -235,17 +557,17 @@ extern "C" PyObject *ImplImageBatchVarShape_Create(int32_t capacity) CATCH_RETURN_DEFAULT(nullptr, "ImageBatchVarShape create failed") } -extern "C" NVCVImageBatchHandle ImplImageBatchVarShape_GetHandle(PyObject *varshape) +NVCVImageBatchHandle ImplImageBatchVarShape_GetHandle(PyObject *varshape) { try { return ToSharedObj(varshape)->impl().handle(); } - CATCH_RETURN_DEFAULT(0, "ImageBatchVarShape get handle failed") + CATCH_RETURN_DEFAULT(nullptr, "ImageBatchVarShape get handle failed") } -extern "C" PyObject *ImplTensor_CreateForImageBatch(int32_t numImages, int32_t width, int32_t height, - NVCVImageFormat fmt, int32_t rowalign) +PyObject *ImplTensor_CreateForImageBatch(int32_t numImages, int32_t width, int32_t height, NVCVImageFormat fmt, + int32_t rowalign) { try { @@ -256,7 +578,7 @@ extern "C" PyObject *ImplTensor_CreateForImageBatch(int32_t numImages, int32_t w CATCH_RETURN_DEFAULT(nullptr, "Tensor for ImageBatch create failed") } -extern "C" void ImplImageBatchVarShape_PushBack(PyObject *varshape, PyObject *image) +void ImplImageBatchVarShape_PushBack(PyObject *varshape, PyObject *image) { try { @@ -266,7 +588,7 @@ extern "C" void ImplImageBatchVarShape_PushBack(PyObject *varshape, PyObject *im CATCH_RETURN_DEFAULT(, "ImageBatchVarShape push back failed") } -extern "C" void ImplImageBatchVarShape_PopBack(PyObject *varshape, int32_t cnt) +void ImplImageBatchVarShape_PopBack(PyObject *varshape, int32_t cnt) { try { @@ -275,7 +597,7 @@ extern "C" void ImplImageBatchVarShape_PopBack(PyObject *varshape, int32_t cnt) CATCH_RETURN_DEFAULT(, "ImageBatchVarShape pop back failed") } -extern "C" void ImplImageBatchVarShape_Clear(PyObject *varshape) +void ImplImageBatchVarShape_Clear(PyObject *varshape) { try { @@ -284,7 +606,7 @@ extern "C" void ImplImageBatchVarShape_Clear(PyObject *varshape) CATCH_RETURN_DEFAULT(, "ImageBatchVarShape clear failed") } -extern "C" PyObject *ImplTensorBatch_Create(int32_t capacity) +PyObject *ImplTensorBatch_Create(int32_t capacity) { try { @@ -294,16 +616,16 @@ extern "C" PyObject *ImplTensorBatch_Create(int32_t capacity) CATCH_RETURN_DEFAULT(nullptr, "TensorBatch create failed") } -extern "C" NVCVTensorBatchHandle ImplTensorBatch_GetHandle(PyObject *tensorBatch) +NVCVTensorBatchHandle ImplTensorBatch_GetHandle(PyObject *tensorBatch) { try { return ToSharedObj(tensorBatch)->impl().handle(); } - CATCH_RETURN_DEFAULT(0, "TensorBatch get handle failed") + CATCH_RETURN_DEFAULT(nullptr, "TensorBatch get handle failed") } -extern "C" void ImplTensorBatch_PushBack(PyObject *tensorBatch, PyObject *tensor) +void ImplTensorBatch_PushBack(PyObject *tensorBatch, PyObject *tensor) { try { @@ -313,7 +635,7 @@ extern "C" void ImplTensorBatch_PushBack(PyObject *tensorBatch, PyObject *tensor CATCH_RETURN_DEFAULT(, "TensorBatch push back failed") } -extern "C" void ImplTensorBatch_PopBack(PyObject *tensorBatch, uint32_t cnt) +void ImplTensorBatch_PopBack(PyObject *tensorBatch, uint32_t cnt) { try { @@ -322,7 +644,7 @@ extern "C" void ImplTensorBatch_PopBack(PyObject *tensorBatch, uint32_t cnt) CATCH_RETURN_DEFAULT(, "TensorBatch pop back failed") } -extern "C" void ImplTensorBatch_Clear(PyObject *tensorBatch) +void ImplTensorBatch_Clear(PyObject *tensorBatch) { try { @@ -331,7 +653,7 @@ extern "C" void ImplTensorBatch_Clear(PyObject *tensorBatch) CATCH_RETURN_DEFAULT(, "TensorBatch clear failed") } -extern "C" void ImplCache_Add(ICacheItem *extItem) +void ImplCache_Add(ICacheItem *extItem) { try { @@ -341,7 +663,7 @@ extern "C" void ImplCache_Add(ICacheItem *extItem) CATCH_RETURN_DEFAULT(, "Cache add item failed") } -extern "C" ICacheItem **ImplCache_Fetch(const IKey *pkey) +ICacheItem **ImplCache_Fetch(const IKey *pkey) { try { @@ -349,13 +671,13 @@ extern "C" ICacheItem **ImplCache_Fetch(const IKey *pkey) std::vector> vcont = Cache::Instance().fetch(*pkey); - std::unique_ptr out(new ICacheItem *[vcont.size() + 1]); + auto out = std::make_unique(vcont.size() + 1); for (size_t i = 0; i < vcont.size(); ++i) { - ExternalCacheItem *extItem = dynamic_cast(vcont[i].get()); + const auto *extItem = dynamic_cast(vcont[i].get()); NVCV_ASSERT(extItem != nullptr); - out[i] = extItem->obj.get(); + out[i] = extItem->obj(); } out[vcont.size()] = nullptr; // end of list @@ -364,7 +686,7 @@ extern "C" ICacheItem **ImplCache_Fetch(const IKey *pkey) CATCH_RETURN_DEFAULT(nullptr, "Cache add fetch failed") } -extern "C" PyObject *ImplImage_Create(int32_t width, int32_t height, NVCVImageFormat fmt, int32_t rowAlign) +PyObject *ImplImage_Create(int32_t width, int32_t height, NVCVImageFormat fmt, int32_t rowAlign) { try { @@ -374,16 +696,16 @@ extern "C" PyObject *ImplImage_Create(int32_t width, int32_t height, NVCVImageFo CATCH_RETURN_DEFAULT(nullptr, "Image create failed") } -extern "C" NVCVImageHandle ImplImage_GetHandle(PyObject *img) +NVCVImageHandle ImplImage_GetHandle(PyObject *img) { try { return ToSharedObj(img)->impl().handle(); } - CATCH_RETURN_DEFAULT(0, "Image get handle failed") + CATCH_RETURN_DEFAULT(nullptr, "Image get handle failed") } -extern "C" PyObject *ImplContainer_Create(nvcvpy::Container *pcont) +PyObject *ImplContainer_Create(nvcvpy::Container *pcont) { try { @@ -396,7 +718,7 @@ extern "C" PyObject *ImplContainer_Create(nvcvpy::Container *pcont) CATCH_RETURN_DEFAULT(nullptr, "Container create failed") } -extern "C" void ImplCache_RemoveAllNotInUseMatching(const IKey *pkey) +void ImplCache_RemoveAllNotInUseMatching(const IKey *pkey) { try { @@ -443,9 +765,18 @@ void ExportCAPI(py::module &m) .TensorBatch_PushBack = &ImplTensorBatch_PushBack, .TensorBatch_PopBack = &ImplTensorBatch_PopBack, .TensorBatch_Clear = &ImplTensorBatch_Clear, + .Resources_SyncAndHold = &ImplResources_SyncAndHold, + .Resources_SubmitSyncOnly = &ImplResources_SubmitSyncOnly, }; m.add_object("_C_API", py::capsule(&capi, "cvcuda._C_API")); } +void ExportCAPITestHooks(py::module &m) +{ + m.def("fail_hold_resources", [](bool armed) { testFailureInjection().streamHoldResources = armed; }); + m.def("fail_submit_sync_only", [](bool armed) { testFailureInjection().resourcesSubmitSyncOnly = armed; }); + m.def("fail_sync_and_hold", [](bool armed) { testFailureInjection().resourcesSyncAndHold = armed; }); +} + } // namespace nvcvpy::priv diff --git a/python/mod_cvcuda/nvcv/CAPI.hpp b/python/mod_cvcuda/nvcv/CAPI.hpp index be50d8cf2..575a92946 100644 --- a/python/mod_cvcuda/nvcv/CAPI.hpp +++ b/python/mod_cvcuda/nvcv/CAPI.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -26,6 +26,11 @@ namespace py = pybind11; void ExportCAPI(py::module &m); +// Registers the deterministic C API failure-injection toggles used by the +// ResourceGuard lifetime regressions. Bound under the private cvcuda._test +// submodule; not part of the public API. +void ExportCAPITestHooks(py::module &m); + } // namespace nvcvpy::priv #endif // NVCV_PYTHON_PRIV_CAPI_HPP diff --git a/python/mod_cvcuda/nvcv/Cache.cpp b/python/mod_cvcuda/nvcv/Cache.cpp index b9a509f2e..570500cd1 100644 --- a/python/mod_cvcuda/nvcv/Cache.cpp +++ b/python/mod_cvcuda/nvcv/Cache.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,6 +18,7 @@ #include "Cache.hpp" #include "Definitions.hpp" +#include "Stream.hpp" #include "ThreadScope.hpp" #include @@ -25,6 +26,7 @@ #include #include +#include #include #include #include @@ -85,34 +87,37 @@ using Items = std::unordered_multimap, struct Cache::Impl { - Items items; - inline static std::mutex mtx; - inline static int64_t cache_limit_inbytes; - inline static int64_t current_size_inbytes; + Items items; + inline static std::mutex mtx; + inline static std::unordered_map cache_limit_inbytes; + inline static std::unordered_map current_size_inbytes; }; Cache::Cache() - : pimpl(new Impl()) { - std::lock_guard lk(pimpl->mtx); + pimpl = std::make_unique(); + std::lock_guard lk(Impl::mtx); instances.insert(this); } -Cache::~Cache() +Cache::~Cache() noexcept { + std::unique_ptr localPimpl; + + try { - std::lock_guard lk(pimpl->mtx); - instances.erase(this); - // It might not be safe to call destructors here, decrease the size manually - for (const auto &node : pimpl->items) { - pimpl->current_size_inbytes -= node.second->GetSizeInBytes(); + std::lock_guard lk(Impl::mtx); + instances.erase(this); + // It might not be safe to call destructors here, decrease the size manually + for (const auto &[nodeKey, node] : pimpl->items) + { + int dev = nodeKey->deviceId(); + Impl::current_size_inbytes[dev] -= node->GetSizeInBytes(); + } } - } - Impl *pimpl = this->pimpl.release(); - try - { + localPimpl = std::move(this->pimpl); #if PY_MAJOR_VERSION >= 3 && PY_MINOR_VERSION >= 13 if (Py_IsInitialized() && !Py_IsFinalizing()) #else @@ -121,13 +126,25 @@ Cache::~Cache() { // Make sure that the main thread doesn't finalize the interpreter until all objects have been destroyed py::gil_scoped_acquire acq; - delete pimpl; + localPimpl.reset(); + } + else + { + localPimpl.release(); } } - catch (const std::exception &) + catch (...) { // Leak intentionally if the Python runtime is not available anymore. // See https://pybind11.readthedocs.io/en/stable/advanced/misc.html#common-sources-of-global-interpreter-lock-errors + if (localPimpl) + { + localPimpl.release(); + } + if (pimpl) + { + pimpl.release(); + } } } @@ -135,21 +152,33 @@ void Cache::add(CacheItem &item) { Items savedItems; { - std::unique_lock lk(pimpl->mtx); - if (item.GetSizeInBytes() > doGetCacheLimit()) + std::unique_lock lk(Impl::mtx); + int dev = item.key().deviceId(); + + if (item.GetSizeInBytes() > doGetDeviceLimit(dev)) { return; } - if (item.GetSizeInBytes() + doGetCurrentSizeInBytes() > doGetCacheLimit()) + if (item.GetSizeInBytes() + doGetDeviceSize(dev) > doGetDeviceLimit(dev)) { - // we clear the cache: all pimpl->items will be dtor'ed at the end of scope of savedItems and cache size will be reset to 0 - savedItems = std::move(pimpl->items); - pimpl->current_size_inbytes = 0; + // Evict only items belonging to this device. + for (auto it = pimpl->items.begin(); it != pimpl->items.end();) + { + if (it->first->deviceId() == dev) + { + savedItems.insert(pimpl->items.extract(it++)); + } + else + { + ++it; + } + } + Impl::current_size_inbytes[dev] = 0; } pimpl->items.emplace(&item.key(), item.shared_from_this()); - pimpl->current_size_inbytes += item.GetSizeInBytes(); + Impl::current_size_inbytes[dev] += item.GetSizeInBytes(); } } @@ -168,19 +197,20 @@ void Cache::removeAllNotInUseMatching(const IKey &key) std::vector> holdItemsUntilMtxUnlocked; { - std::unique_lock lk(pimpl->mtx); + std::unique_lock lk(Impl::mtx); - auto itrange = pimpl->items.equal_range(&key); + auto [firstItem, lastItem] = pimpl->items.equal_range(&key); - int numItems = std::distance(itrange.first, itrange.second); + auto numItems = std::distance(firstItem, lastItem); - auto it = itrange.first; - for (int i = 0; i < numItems; ++i) + auto it = firstItem; + for (decltype(numItems) i = 0; i < numItems; ++i) { if (!it->second->isInUse()) { holdItemsUntilMtxUnlocked.push_back(it->second); - pimpl->current_size_inbytes -= it->second->GetSizeInBytes(); + int dev = it->first->deviceId(); + Impl::current_size_inbytes[dev] -= it->second->GetSizeInBytes(); pimpl->items.erase(it++); } else @@ -195,13 +225,13 @@ std::vector> Cache::fetch(const IKey &key) const { std::vector> v; - std::unique_lock lk(pimpl->mtx); + std::unique_lock lk(Impl::mtx); - auto itrange = pimpl->items.equal_range(&key); + auto [firstItem, lastItem] = pimpl->items.equal_range(&key); - v.reserve(distance(itrange.first, itrange.second)); + v.reserve(distance(firstItem, lastItem)); - for (auto it = itrange.first; it != itrange.second; ++it) + for (auto it = firstItem; it != lastItem; ++it) { if (!it->second->isInUse()) { @@ -216,7 +246,7 @@ std::vector> Cache::fetch(const IKey &key) const void Cache::dbgPrintCacheForKey(const IKey &key, const std::string &prefix) { std::vector> v; - std::unique_lock lk(pimpl->mtx); + std::unique_lock lk(Impl::mtx); auto itrange = pimpl->items.equal_range(&key); for (auto it = itrange.first; it != itrange.second; ++it) @@ -228,11 +258,11 @@ void Cache::dbgPrintCacheForKey(const IKey &key, const std::string &prefix) std::shared_ptr Cache::fetchOne(const IKey &key) const { - std::unique_lock lk(pimpl->mtx); + std::unique_lock lk(Impl::mtx); - auto itrange = pimpl->items.equal_range(&key); + auto [firstItem, lastItem] = pimpl->items.equal_range(&key); - for (auto it = itrange.first; it != itrange.second; ++it) + for (auto it = firstItem; it != lastItem; ++it) { if (!it->second->isInUse()) { @@ -260,60 +290,78 @@ void Cache::setCacheLimit(int64_t new_cache_limit_inbytes) throw std::invalid_argument("Cache limit must be non-negative."); } - size_t free_mem, total_mem; + int dev = 0; + util::CheckThrow(cudaGetDevice(&dev)); + + size_t free_mem; + size_t total_mem; util::CheckThrow(cudaMemGetInfo(&free_mem, &total_mem)); if (static_cast(total_mem) < new_cache_limit_inbytes) { - // Cache is not device aware, so in a multi-gpu scenario it could be ok to have a cache limit larger - // than the total mem of the current device, but we should notify the user about this. std::cerr << "WARNING: new_cache_limit=" << new_cache_limit_inbytes - << " is more than total available memory on current device: " << total_mem << std::endl; + << " is more than total available memory on device " << dev << ": " << total_mem << std::endl; } Items savedItems; { - std::unique_lock lk(pimpl->mtx); - if (doGetCurrentSizeInBytes() > new_cache_limit_inbytes) + std::unique_lock lk(Impl::mtx); + if (doGetDeviceSize(dev) > new_cache_limit_inbytes) { - // we clear the cache: all pimpl->items will be dtor'ed at the end of scope of savedItems and cache size will be reset to 0 - savedItems = std::move(pimpl->items); - pimpl->current_size_inbytes = 0; + // Evict only items belonging to this device. + for (auto it = pimpl->items.begin(); it != pimpl->items.end();) + { + if (it->first->deviceId() == dev) + { + savedItems.insert(pimpl->items.extract(it++)); + } + else + { + ++it; + } + } + Impl::current_size_inbytes[dev] = 0; } - pimpl->cache_limit_inbytes = new_cache_limit_inbytes; + Impl::cache_limit_inbytes[dev] = new_cache_limit_inbytes; } } int64_t Cache::getCacheLimit() const { - std::unique_lock lk(pimpl->mtx); - return doGetCacheLimit(); + int dev = 0; + util::CheckThrow(cudaGetDevice(&dev)); + std::unique_lock lk(Impl::mtx); + return doGetDeviceLimit(dev); } -int64_t Cache::doGetCacheLimit() const +int64_t Cache::doGetDeviceLimit(int dev) const { - return pimpl->cache_limit_inbytes; + auto it = Impl::cache_limit_inbytes.find(dev); + return it != Impl::cache_limit_inbytes.end() ? it->second : 0; } -int64_t Cache::getCurrentSizeInBytes() +int64_t Cache::getCurrentSizeInBytes() const { - std::unique_lock lk(pimpl->mtx); - return doGetCurrentSizeInBytes(); + int dev = 0; + util::CheckThrow(cudaGetDevice(&dev)); + std::unique_lock lk(Impl::mtx); + return doGetDeviceSize(dev); } -int64_t Cache::doGetCurrentSizeInBytes() const +int64_t Cache::doGetDeviceSize(int dev) const { - return pimpl->current_size_inbytes; + auto it = Impl::current_size_inbytes.find(dev); + return it != Impl::current_size_inbytes.end() ? it->second : 0; } -void Cache::doIterateThroughItems(const std::function &fn) const +std::vector> Cache::doSnapshotItems() const { // To avoid keeping mutex locked for too long, let's first gather all items // into a vector, unlock the mutex, and then iterate through them. std::vector> v; { - std::unique_lock lk(pimpl->mtx); + std::unique_lock lk(Impl::mtx); v.reserve(pimpl->items.size()); for (auto it = pimpl->items.begin(); it != pimpl->items.end(); ++it) @@ -322,10 +370,7 @@ void Cache::doIterateThroughItems(const std::function &fn } } - for (const std::shared_ptr &item : v) - { - fn(*item); - } + return v; } Cache &Cache::Instance() @@ -338,16 +383,16 @@ void Cache::ClearAll() { Items savedItems; { - std::lock_guard lk(Cache::Impl::mtx); + std::lock_guard lk(Cache::Impl::mtx); std::for_each(instances.begin(), instances.end(), - [&](Cache *instance) { savedItems.merge(instance->pimpl->items); }); - Cache::Impl::current_size_inbytes = 0; + [&savedItems](Cache *instance) { savedItems.merge(instance->pimpl->items); }); + Cache::Impl::current_size_inbytes.clear(); } } size_t Cache::TotalSize() { - std::lock_guard lk(Cache::Impl::mtx); + std::lock_guard lk(Cache::Impl::mtx); return std::accumulate(instances.cbegin(), instances.cend(), static_cast(0), [](size_t sum, const Cache *instance) { return sum + instance->size(); }); } @@ -356,15 +401,47 @@ void Cache::Export(py::module &m) { using namespace pybind11::literals; - py::class_>(nullptr, "CacheItem", py::module_local()); - - py::class_>(nullptr, "ExternalCacheItem", - py::module_local()); - - // Initialy set cache limit to half the size of the GPU memory - size_t free_mem, total_mem; - util::CheckThrow(cudaMemGetInfo(&free_mem, &total_mem)); - Cache::Instance().setCacheLimit(total_mem / 2); + py::class_> cacheItem(nullptr, "CacheItem", py::module_local()); + + py::class_> externalCacheItem( + nullptr, "ExternalCacheItem", py::module_local()); + (void)cacheItem; + (void)externalCacheItem; + + // Initialize per-device cache limits to half each GPU's total memory. + // Tolerate hosts with no CUDA device or only a stub libcuda available + // (CPU-only build/CI nodes, CUDA_VISIBLE_DEVICES="", manylinux build + // hosts that resolve libcuda.so.1 to a stub). cudaGetDeviceCount may + // return cudaSuccess with deviceCount=0, cudaErrorNoDevice, or + // cudaErrorStubLibrary. In any of those there is nothing to seed; + // skipping leaves `import cvcuda` working. The cache cannot actually + // be used until a real device is present, so deferring is safe. + { + int deviceCount = 0; + if (cudaError_t err = cudaGetDeviceCount(&deviceCount); err == cudaErrorNoDevice || err == cudaErrorStubLibrary) + { + (void)cudaGetLastError(); // clear sticky error + deviceCount = 0; + } + else + { + util::CheckThrow(err); + } + if (deviceCount > 0) + { + int savedDev = 0; + util::CheckThrow(cudaGetDevice(&savedDev)); + for (int d = 0; d < deviceCount; ++d) + { + util::CheckThrow(cudaSetDevice(d)); + size_t free_mem; + size_t total_mem; + util::CheckThrow(cudaMemGetInfo(&free_mem, &total_mem)); + Impl::cache_limit_inbytes[d] = static_cast(total_mem / 2); + } + util::CheckThrow(cudaSetDevice(savedDev)); + } + } // Make sure cache is cleared up when script ends. util::RegisterCleanup(m, Cache::ClearAll); @@ -373,6 +450,9 @@ void Cache::Export(py::module &m) "clear_cache", [](ThreadScope scope) { + // ResourceGuard releases completed holds through auxiliary-stream callbacks. + // Drain them so clearing the cache also releases their resources. + Stream::SynchronizeAndClearGCBag(); switch (scope) { case ThreadScope::GLOBAL: @@ -414,15 +494,15 @@ void Cache::Export(py::module &m) m.def( "get_cache_limit_inbytes", [] { return Cache::Instance().getCacheLimit(); }, - "Returns the current cache limit [in bytes]"); + "Returns the cache limit [in bytes] for the current CUDA device."); m.def( "set_cache_limit_inbytes", [](int64_t new_cache_limit_inbytes) { Cache::Instance().setCacheLimit(new_cache_limit_inbytes); }, - "Sets the current cache limit [in bytes]"); + "Sets the cache limit [in bytes] for the current CUDA device."); m.def( "current_cache_size_inbytes", [] { return Cache::Instance().getCurrentSizeInBytes(); }, - "Returns the current cache size [in bytes]"); + "Returns the current cache size [in bytes] for the current CUDA device."); py::module_ internal = m.attr(INTERNAL_SUBMODULE_NAME); internal.def("nbytes_in_cache", [](const CacheItem &item) { return item.GetSizeInBytes(); }); diff --git a/python/mod_cvcuda/nvcv/Cache.hpp b/python/mod_cvcuda/nvcv/Cache.hpp index d3dd79e6e..eac9ac441 100644 --- a/python/mod_cvcuda/nvcv/Cache.hpp +++ b/python/mod_cvcuda/nvcv/Cache.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -26,13 +26,14 @@ #include #include +#include #include namespace nvcvpy::priv { namespace py = pybind11; -class PYBIND11_EXPORT CacheItem : public virtual Object +class PYBIND11_EXPORT CacheItem : public virtual Object // NOSONAR: cache resources share one Object base. { public: uint64_t id() const; @@ -56,8 +57,8 @@ class PYBIND11_EXPORT CacheItem : public virtual Object class ExternalCacheItem : public CacheItem { public: - ExternalCacheItem(std::shared_ptr obj_) - : obj(obj_) + explicit ExternalCacheItem(std::shared_ptr obj_) + : m_obj(std::move(obj_)) , m_size_inbytes(doComputeSizeInBytes()) { } @@ -70,22 +71,26 @@ class ExternalCacheItem : public CacheItem return m_size_inbytes; } - std::shared_ptr obj; + nvcvpy::ICacheItem *obj() const + { + return m_obj.get(); + } const IKey &key() const override { - return obj->key(); + return m_obj->key(); } private: - int64_t doComputeSizeInBytes() + int64_t doComputeSizeInBytes() const { // ExternalCacheItems (CacheItems outside of nvcv, eg. operators from cvcuda) will not pollute the // Cache, thus for now we say they've no impact on the Cache return 0; } - int64_t m_size_inbytes = -1; + std::shared_ptr m_obj; + int64_t m_size_inbytes = -1; }; class PYBIND11_EXPORT Cache @@ -113,14 +118,13 @@ class PYBIND11_EXPORT Cache { std::vector> out; - doIterateThroughItems( - [&out](CacheItem &item) + for (const std::shared_ptr &item : doSnapshotItems()) + { + if (auto titem = std::dynamic_pointer_cast(item)) { - if (auto titem = std::dynamic_pointer_cast(item.shared_from_this())) - { - out.emplace_back(std::move(titem)); - } - }); + out.emplace_back(std::move(titem)); + } + } return out; } @@ -129,7 +133,7 @@ class PYBIND11_EXPORT Cache void setCacheLimit(int64_t new_cache_limit); int64_t getCacheLimit() const; - int64_t getCurrentSizeInBytes(); + int64_t getCurrentSizeInBytes() const; private: inline static std::unordered_set instances; @@ -138,11 +142,11 @@ class PYBIND11_EXPORT Cache std::unique_ptr pimpl; Cache(); - ~Cache(); + ~Cache() noexcept; - void doIterateThroughItems(const std::function &fn) const; - int64_t doGetCurrentSizeInBytes() const; - int64_t doGetCacheLimit() const; + std::vector> doSnapshotItems() const; + int64_t doGetDeviceSize(int dev) const; + int64_t doGetDeviceLimit(int dev) const; }; } // namespace nvcvpy::priv diff --git a/python/mod_cvcuda/nvcv/Container.cpp b/python/mod_cvcuda/nvcv/Container.cpp index 69d71623d..5bb124d2f 100644 --- a/python/mod_cvcuda/nvcv/Container.cpp +++ b/python/mod_cvcuda/nvcv/Container.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,16 +19,6 @@ namespace nvcvpy::priv { -std::shared_ptr Container::shared_from_this() -{ - return std::static_pointer_cast(Resource::shared_from_this()); -} - -std::shared_ptr Container::shared_from_this() const -{ - return std::static_pointer_cast(Resource::shared_from_this()); -} - void Container::Export(py::module &m) { py::class_, Resource, CacheItem> cont(m, "Container"); diff --git a/python/mod_cvcuda/nvcv/Container.hpp b/python/mod_cvcuda/nvcv/Container.hpp index bf010f2f4..77fdaeb77 100644 --- a/python/mod_cvcuda/nvcv/Container.hpp +++ b/python/mod_cvcuda/nvcv/Container.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -36,13 +36,32 @@ class Container public: static void Export(py::module &m); - std::shared_ptr shared_from_this(); - std::shared_ptr shared_from_this() const; - protected: Container() = default; }; +inline std::shared_ptr SharedContainerFrom(Container &container) +{ + return std::static_pointer_cast(static_cast(container).shared_from_this()); +} + +inline std::shared_ptr SharedContainerFrom(const Container &container) +{ + return std::static_pointer_cast(static_cast(container).shared_from_this()); +} + +template +std::shared_ptr SharedContainerFrom(T &container) +{ + return std::static_pointer_cast(SharedContainerFrom(static_cast(container))); +} + +template +std::shared_ptr SharedContainerFrom(const T &container) +{ + return std::static_pointer_cast(SharedContainerFrom(static_cast(container))); +} + class ExternalContainer : public Container { public: @@ -63,7 +82,7 @@ class ExternalContainer : public Container private: nvcvpy::Container &m_extCont; - int64_t doComputeSizeInBytes() + int64_t doComputeSizeInBytes() const { // ExternalCacheItems (CacheItems outside of nvcv, eg. operators from cvcuda) will not pollute the // Cache, thus for now we say they've no impact on the Cache diff --git a/python/mod_cvcuda/nvcv/DLPackUtils.cpp b/python/mod_cvcuda/nvcv/DLPackUtils.cpp index f5cbaca1f..a67d8970d 100644 --- a/python/mod_cvcuda/nvcv/DLPackUtils.cpp +++ b/python/mod_cvcuda/nvcv/DLPackUtils.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,11 +19,51 @@ #include "DataType.hpp" +#include #include +#include #include +#include +#include +#include + namespace nvcvpy::priv { +namespace { + +class DLPackError : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + +int GetOwningDevice(const nvcv::Byte *ptr) +{ + cudaPointerAttributes attrs = {}; + if (cudaError_t err = cudaPointerGetAttributes(&attrs, ptr); + err == cudaSuccess && (attrs.type == cudaMemoryTypeDevice || attrs.type == cudaMemoryTypeManaged)) + { + return attrs.device; + } + // cudaPointerGetAttributes leaves a sticky error on failure; clear it + // so the subsequent cudaGetDevice call doesn't pick it up. + cudaGetLastError(); + int dev = 0; + util::CheckThrow(cudaGetDevice(&dev)); + return dev; +} + +void DeleteShapeAndStrides(DLManagedTensor *self) noexcept +{ + std::unique_ptr shape(self->dl_tensor.shape); + std::unique_ptr strides(self->dl_tensor.strides); + self->dl_tensor.shape = nullptr; + self->dl_tensor.strides = nullptr; +} + +} // namespace + DLPackTensor::DLPackTensor() noexcept : m_tensor{} { @@ -52,36 +92,31 @@ DLPackTensor::DLPackTensor(const py::buffer_info &info, const DLDevice &dev) } else { - throw std::runtime_error("Cannot wrap buffer with given format"); + throw DLPackError("Cannot wrap buffer with given format"); } - dlTensor.ndim = info.ndim; + dlTensor.ndim = static_cast(info.ndim); dlTensor.device = dev; dlTensor.byte_offset = 0; - m_tensor.deleter = [](DLManagedTensor *self) - { - delete[] self->dl_tensor.shape; - self->dl_tensor.shape = nullptr; - - delete[] self->dl_tensor.strides; - self->dl_tensor.strides = nullptr; - }; + m_tensor.deleter = DeleteShapeAndStrides; try { - dlTensor.shape = new int64_t[info.ndim]; - std::copy_n(info.shape.begin(), info.shape.size(), dlTensor.shape); + auto shape = std::make_unique(info.ndim); + std::copy_n(info.shape.begin(), info.shape.size(), shape.get()); + dlTensor.shape = shape.release(); - dlTensor.strides = new int64_t[info.ndim]; + auto strides = std::make_unique(info.ndim); for (int i = 0; i < info.ndim; ++i) { if (info.strides[i] % info.itemsize != 0) { - throw std::runtime_error("Stride must be a multiple of the element size in bytes"); + throw DLPackError("Stride must be a multiple of the element size in bytes"); } - dlTensor.strides[i] = info.strides[i] / info.itemsize; + strides[i] = info.strides[i] / info.itemsize; } + dlTensor.strides = strides.release(); } catch (...) { @@ -91,13 +126,9 @@ DLPackTensor::DLPackTensor(const py::buffer_info &info, const DLDevice &dev) } DLPackTensor::DLPackTensor(const nvcv::TensorDataStrided &tensorData) + : m_tensor{} { - m_tensor = {}; - m_tensor.deleter = [](DLManagedTensor *self) - { - delete[] self->dl_tensor.shape; - delete[] self->dl_tensor.strides; - }; + m_tensor.deleter = DeleteShapeAndStrides; try { @@ -106,14 +137,12 @@ DLPackTensor::DLPackTensor(const nvcv::TensorDataStrided &tensorData) // Set up device if (tensorData.IsCompatible()) { - // TODO: detect correct device_type from memory buffer tensor.device.device_type = kDLCUDA; - // TODO: detect correct device_id from memory buffer (if possible) - tensor.device.device_id = 0; + tensor.device.device_id = GetOwningDevice(tensorData.basePtr()); } else { - throw std::runtime_error("Tensor buffer type not supported, must be either CUDA or Host (CPU)"); + throw DLPackError("Tensor buffer type not supported, must be either CUDA or Host (CPU)"); } // Set up ndim @@ -124,24 +153,26 @@ DLPackTensor::DLPackTensor(const nvcv::TensorDataStrided &tensorData) tensor.byte_offset = 0; // Set up shape - tensor.shape = new int64_t[tensor.ndim]; - std::copy_n(tensorData.shape().shape().begin(), tensor.ndim, tensor.shape); + auto shape = std::make_unique(tensor.ndim); + std::copy_n(tensorData.shape().shape().begin(), tensor.ndim, shape.get()); + tensor.shape = shape.release(); // Set up dtype tensor.dtype = ToDLDataType(tensorData.dtype()); // Set up strides - tensor.strides = new int64_t[tensor.ndim]; + auto strides = std::make_unique(tensor.ndim); for (int i = 0; i < tensor.ndim; ++i) { - int64_t stride = tensorData.cdata().buffer.strided.strides[i]; - if (stride % tensorData.dtype().strideBytes() != 0) + if (int64_t stride = tensorData.cdata().buffer.strided.strides[i]; + stride % tensorData.dtype().strideBytes() != 0) { - throw std::runtime_error("Stride must be a multiple of the element size in bytes"); + throw DLPackError("Stride must be a multiple of the element size in bytes"); } - tensor.strides[i] = tensorData.cdata().buffer.strided.strides[i] / tensorData.dtype().strideBytes(); + strides[i] = tensorData.cdata().buffer.strided.strides[i] / tensorData.dtype().strideBytes(); } + tensor.strides = strides.release(); } catch (...) { @@ -151,13 +182,9 @@ DLPackTensor::DLPackTensor(const nvcv::TensorDataStrided &tensorData) } DLPackTensor::DLPackTensor(const nvcv::ArrayData &arrayData) + : m_tensor{} { - m_tensor = {}; - m_tensor.deleter = [](DLManagedTensor *self) - { - delete[] self->dl_tensor.shape; - delete[] self->dl_tensor.strides; - }; + m_tensor.deleter = DeleteShapeAndStrides; try { @@ -166,14 +193,12 @@ DLPackTensor::DLPackTensor(const nvcv::ArrayData &arrayData) // Set up device if (arrayData.IsCompatible()) { - // TODO: detect correct device_type from memory buffer tensor.device.device_type = kDLCUDA; - // TODO: detect correct device_id from memory buffer (if possible) - tensor.device.device_id = 0; + tensor.device.device_id = GetOwningDevice(arrayData.basePtr()); } else { - throw std::runtime_error("Array buffer type not supported, must be either CUDA"); + throw DLPackError("Array buffer type not supported, must be either CUDA"); } // Set up ndim @@ -184,15 +209,17 @@ DLPackTensor::DLPackTensor(const nvcv::ArrayData &arrayData) tensor.byte_offset = 0; // Set up shape - tensor.shape = new int64_t[tensor.ndim]; - tensor.shape[0] = arrayData.capacity(); + auto shape = std::make_unique(tensor.ndim); + shape[0] = arrayData.capacity(); + tensor.shape = shape.release(); // Set up dtype tensor.dtype = ToDLDataType(arrayData.dtype()); // Set up strides - tensor.strides = new int64_t[tensor.ndim]; - tensor.strides[0] = arrayData.stride(); + auto strides = std::make_unique(tensor.ndim); + strides[0] = arrayData.stride(); + tensor.strides = strides.release(); } catch (...) { @@ -286,7 +313,7 @@ nvcv::DataType ToNVCVDataType(const DLDataType &dtype) pp.swizzle = nvcv::Swizzle::S_XYZW; break; default: - throw std::runtime_error("DLPack buffer's data type must have at most 4 lanes"); + throw DLPackError("DLPack buffer's data type must have at most 4 lanes"); } for (int i = 0; i < lanes; ++i) @@ -318,7 +345,7 @@ nvcv::DataType ToNVCVDataType(const DLDataType &dtype) kind = nvcv::DataKind::FLOAT; break; default: - throw std::runtime_error("Data type code not supported, must be Int, UInt, Float, Complex or Bool"); + throw DLPackError("Data type code not supported, must be Int, UInt, Float, Complex or Bool"); } return nvcv::DataType(kind, packing); @@ -327,7 +354,7 @@ nvcv::DataType ToNVCVDataType(const DLDataType &dtype) DLDataType ToDLDataType(const nvcv::DataType &dataType) { DLDataType dt = {}; - dt.lanes = dataType.numChannels(); + dt.lanes = static_cast(dataType.numChannels()); switch (dataType.dataKind()) { @@ -344,7 +371,7 @@ DLDataType ToDLDataType(const nvcv::DataType &dataType) dt.code = kDLComplex; break; default: - throw std::runtime_error("Data kind not supported, must be UNSIGNED, SIGNED, FLOAT or COMPLEX"); + throw DLPackError("Data kind not supported, must be UNSIGNED, SIGNED, FLOAT or COMPLEX"); } std::array bpc = dataType.bitsPerChannel(); @@ -353,11 +380,11 @@ DLDataType ToDLDataType(const nvcv::DataType &dataType) { if (bpc[i] != bpc[0]) { - throw std::runtime_error("All lanes must have the same bit depth"); + throw DLPackError("All lanes must have the same bit depth"); } } - dt.bits = bpc[0]; + dt.bits = static_cast(bpc[0]); return dt; } diff --git a/python/mod_cvcuda/nvcv/DataType.cpp b/python/mod_cvcuda/nvcv/DataType.cpp index 854acc234..69a48c93e 100644 --- a/python/mod_cvcuda/nvcv/DataType.cpp +++ b/python/mod_cvcuda/nvcv/DataType.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -53,6 +53,82 @@ struct IsComplex> : std::true_type { }; +// Marker type for numpy float16 (no native C++ 16-bit float equivalent) +struct Float16Tag +{ +}; + +// Type traits to abstract differences between standard types and Float16Tag +template +struct DTypeTraits +{ + static bool matches(const py::dtype &dtbase) + { + return dtbase.equal(py::dtype::of()); + } + + static py::dtype create() + { + return py::dtype::of(); + } + + static constexpr int itemsize() + { + return sizeof(T); + } + + static int bits_per_component(int itemsize_param) + { + return itemsize_param * 8; + } + + static nvcv::DataKind infer_kind() + { + if (IsComplex::value) + return nvcv::DataKind::COMPLEX; + else if (std::is_floating_point_v) + return nvcv::DataKind::FLOAT; + else if (std::is_signed_v) + return nvcv::DataKind::SIGNED; + else if (std::is_unsigned_v) + return nvcv::DataKind::UNSIGNED; + else + { + NVCV_ASSERT(!"Invalid type"); + } + } +}; + +// Specialization for Float16Tag +template<> +struct DTypeTraits +{ + static bool matches(const py::dtype &dtbase) + { + return dtbase.itemsize() == 2 && dtbase.kind() == 'f'; + } + + static py::dtype create() + { + return py::dtype("e"); + } + + static constexpr int itemsize() + { + return 2; + } + + static int bits_per_component(int /*itemsize_param*/) + { + return 16; + } + + static nvcv::DataKind infer_kind() + { + return nvcv::DataKind::FLOAT; + } +}; + template bool FindDataType(const py::dtype &dt, nvcv::DataType *dtype) { @@ -82,73 +158,54 @@ bool FindDataType(const py::dtype &dt, nvcv::DataType *dtype) } } - int itemsize = dtbase.itemsize(); + auto itemsize = static_cast(dtbase.itemsize()); - if (dtbase.equal(py::dtype::of())) + // Use traits to check if this type matches + if (!DTypeTraits::matches(dtbase)) { - nvcv::DataKind dataKind; - if (IsComplex::value) - { - dataKind = nvcv::DataKind::COMPLEX; - } - else if (std::is_floating_point::value) - { - dataKind = nvcv::DataKind::FLOAT; - } - else if (std::is_signed::value) - { - dataKind = nvcv::DataKind::SIGNED; - } - else if (std::is_unsigned::value) - { - dataKind = nvcv::DataKind::UNSIGNED; - } - else - { - NVCV_ASSERT(!"Invalid type"); - } - - // Infer the packing - nvcv::PackingParams pp = {}; - - pp.byteOrder = nvcv::ByteOrder::MSB; + return false; + } - switch (nchannels) - { - case 1: - pp.swizzle = nvcv::Swizzle::S_X000; - break; - case 2: - pp.swizzle = nvcv::Swizzle::S_XY00; - break; - case 3: - pp.swizzle = nvcv::Swizzle::S_XYZ0; - break; - case 4: - pp.swizzle = nvcv::Swizzle::S_XYZW; - break; - default: - NVCV_ASSERT(!"Invalid number of channels"); - } - for (int i = 0; i < nchannels; ++i) - { - pp.bits[i] = static_cast(itemsize * 8); - } - nvcv::Packing packing = MakePacking(pp); + // get the data kind from the traits + auto dataKind = DTypeTraits::infer_kind(); + nvcv::PackingParams pp = {}; + pp.byteOrder = nvcv::ByteOrder::MSB; - // Finally, infer the data type - NVCV_ASSERT(dtype != nullptr); - *dtype = nvcv::DataType{dataKind, packing}; - return true; + switch (nchannels) + { + case 1: + pp.swizzle = nvcv::Swizzle::S_X000; + break; + case 2: + pp.swizzle = nvcv::Swizzle::S_XY00; + break; + case 3: + pp.swizzle = nvcv::Swizzle::S_XYZ0; + break; + case 4: + pp.swizzle = nvcv::Swizzle::S_XYZW; + break; + default: + NVCV_ASSERT(!"Invalid number of channels"); } - else + + // Use traits to get bits per component + for (int i = 0; i < nchannels; ++i) { - return false; + pp.bits[i] = DTypeTraits::bits_per_component(itemsize); } + + nvcv::Packing packing = MakePacking(pp); + + // Finally, infer the data type + NVCV_ASSERT(dtype != nullptr); + *dtype = nvcv::DataType{dataKind, packing}; + return true; } // clang-format off using SupportedBaseTypes = std::tuple< + Float16Tag, // Explicit marker for numpy float16 std::complex, std::complex, float, double, @@ -178,37 +235,28 @@ std::optional SelectDataType(std::tuple, const py::dtype template bool FindDType(T *, const nvcv::DataType &dtype, py::dtype *dt) { - int nchannels = dtype.numChannels(); - int itemsize = dtype.bitsPerPixel() / 8; + int nchannels = dtype.numChannels(); + int itemsize = dtype.bitsPerPixel() / 8; + nvcv::DataKind dataKind = dtype.dataKind(); - if (sizeof(T) != itemsize / nchannels) + if (DTypeTraits::itemsize() != itemsize / nchannels) { return false; } - nvcv::DataKind dataKind = dtype.dataKind(); - - if ((std::is_floating_point_v && dataKind == nvcv::DataKind::FLOAT) - || (IsComplex::value && dataKind == nvcv::DataKind::COMPLEX) - || (std::is_integral_v && std::is_signed_v && dataKind == nvcv::DataKind::SIGNED) - || (std::is_integral_v && std::is_unsigned_v && dataKind == nvcv::DataKind::UNSIGNED)) + if (DTypeTraits::infer_kind() != dataKind) { - NVCV_ASSERT(dt != nullptr); + return false; + } - *dt = py::dtype::of(); + NVCV_ASSERT(dt != nullptr); + *dt = DTypeTraits::create(); - // data type has multiple components? - if (nchannels > 1) - { - // Create a dtype with multiple components too, with shape argument - *dt = py::dtype(util::FormatString("%d%c", nchannels, dt->char_())); - } - return true; - } - else + if (nchannels > 1) { - return false; + *dt = py::dtype(util::ConcatString(nchannels, dt->char_())); } + return true; } template @@ -245,14 +293,19 @@ static std::string DataTypeToString(nvcv::DataType type) out << "nvcv."; - if (prefix == str) + auto starts_with = [](const char *s, std::string_view p) + { + return std::string_view{s}.rfind(p, 0) == 0; + }; + + if (starts_with(str, prefix)) { out << "Type." << str + prefix.length(); } else { prefix = "DataType"; - if (prefix == str) + if (starts_with(str, prefix)) { out << "Type" << str + prefix.length(); } @@ -284,9 +337,28 @@ void ExportDataType(py::module &m) type.def(py::init<>()); type.def("__repr__", &DataTypeToString); - type.def(py::self == py::self); - type.def(py::self != py::self); - type.def(py::self < py::self); + type.def( + "__eq__", [](const nvcv::DataType &a, const nvcv::DataType &b) { return a == b; }, py::is_operator()); + type.def( + "__ne__", [](const nvcv::DataType &a, const nvcv::DataType &b) { return a != b; }, py::is_operator()); + type.def( + "__lt__", [](const nvcv::DataType &a, const nvcv::DataType &b) { return a < b; }, py::is_operator()); + // Type values surface to Python as numpy.dtype (see the DataType type_caster + // below), so a Type constructed via Type(...) must hash identically to the + // equivalent numpy.dtype to stay consistent with __eq__ across both forms. + // Fall back to the packed value for the no-dtype sentinel so hashing never + // dereferences a null object. + type.def( + "__hash__", + [](const nvcv::DataType &a) + { + if (py::dtype dt = ToDType(a)) + { + return PyObject_Hash(dt.ptr()); + } + return static_cast(std::hash{}(static_cast(a))); + }, + "Return a value-based hash matching the equivalent numpy.dtype, so Type can be used as a dict key."); py::implicitly_convertible(); } diff --git a/python/mod_cvcuda/nvcv/DataType.hpp b/python/mod_cvcuda/nvcv/DataType.hpp index 99eed004f..7b4b3c28f 100644 --- a/python/mod_cvcuda/nvcv/DataType.hpp +++ b/python/mod_cvcuda/nvcv/DataType.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/python/mod_cvcuda/nvcv/ExternalBuffer.cpp b/python/mod_cvcuda/nvcv/ExternalBuffer.cpp index ab4a8c1ee..3723a66bd 100644 --- a/python/mod_cvcuda/nvcv/ExternalBuffer.cpp +++ b/python/mod_cvcuda/nvcv/ExternalBuffer.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,21 +20,41 @@ #include "DataType.hpp" #include +#include #include #include #include +#include +#include #include // for std::multiplies +#include +#include namespace nvcvpy::priv { using namespace py::literals; -static void CheckValidCUDABuffer(const void *ptr) +namespace { + +class ExternalBufferError : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + +} // namespace + +static std::byte *AsBytePointer(std::uintptr_t address) noexcept +{ + return reinterpret_cast(address); +} + +static void CheckValidCUDABuffer(const std::byte *ptr) { if (ptr == nullptr) { - throw std::runtime_error("NULL CUDA buffer not accepted"); + throw ExternalBufferError("NULL CUDA buffer not accepted"); } cudaPointerAttributes attrs = {}; @@ -42,19 +62,399 @@ static void CheckValidCUDABuffer(const void *ptr) cudaGetLastError(); // reset the cuda error (if any) if (err != cudaSuccess || attrs.type == cudaMemoryTypeUnregistered) { - throw std::runtime_error("Buffer is not CUDA-accessible"); + throw ExternalBufferError("Buffer is not CUDA-accessible"); + } +} + +// Query the CUDA device a pointer lives on. Returns -1 if the attributes +// can't be obtained (e.g. null pointer); callers then fall back to the +// current device. +static int GetCudaDeviceForPtr(const std::byte *ptr) +{ + if (ptr == nullptr) + { + return -1; + } + + cudaPointerAttributes attrs = {}; + cudaError_t err = cudaPointerGetAttributes(&attrs, ptr); + cudaGetLastError(); + if (err != cudaSuccess) + { + return -1; + } + return attrs.device; +} + +// Parse the CAI `stream` field per the v3 spec: +// absent / field missing : legacy default stream implied (producer was +// on stream 0) --> handle = cudaStreamLegacy, +// isSynced = false +// None / -1 : producer has synchronized; no wait needed +// --> handle = 0, isSynced = true +// 0 : disallowed by spec but cupy emits this for +// its null stream; treat as legacy default +// --> handle = cudaStreamLegacy, isSynced = false +// 1 : legacy default stream +// --> handle = cudaStreamLegacy +// 2 : per-thread default stream +// --> handle = cudaStreamPerThread +// other positive int : raw stream handle +static void ParseCAIStreamField(const py::dict &iface, cudaStream_t &outHandle, bool &outIsSynced) +{ + outHandle = reinterpret_cast(1); // cudaStreamLegacy + outIsSynced = false; + + if (!iface.contains("stream")) + { + return; // legacy default + } + + py::object sf = iface["stream"]; + if (sf.is_none()) + { + outHandle = nullptr; + outIsSynced = true; + return; + } + + long value = 0; + try + { + value = sf.cast(); + } + catch (py::cast_error &) + { + // Unparseable stream field; conservative default. + return; + } + + if (value == -1) + { + outHandle = nullptr; + outIsSynced = true; + return; + } + if (value == 0 || value == 1) + { + outHandle = reinterpret_cast(1); // cudaStreamLegacy + return; + } + if (value == 2) + { + outHandle = reinterpret_cast(2); // cudaStreamPerThread + return; + } + outHandle = reinterpret_cast(value); +} + +// Parse the DLPack v1 `stream` kwarg to `__dlpack__`: +// None / 0 / -1 : no synchronization requested; producer should +// not insert a cross-stream wait +// --> outConsumerStream = 0, outNeedSync = false +// 1 : legacy default stream +// --> outConsumerStream = cudaStreamLegacy, +// outNeedSync = true +// 2 : per-thread default stream +// --> outConsumerStream = cudaStreamPerThread, +// outNeedSync = true +// other int : consumer's CUDA stream handle +// --> outConsumerStream = handle, outNeedSync = true +// +// See https://dmlc.github.io/dlpack/latest/python_spec.html. Note that +// None/0/-1 all map to "no sync" — the consumer is asserting they'll +// handle synchronization themselves (or the data is already retired). +static void ParseDLPackStreamArg(const py::object &streamArg, cudaStream_t &outConsumerStream, bool &outNeedSync) +{ + outConsumerStream = nullptr; + outNeedSync = false; + + if (streamArg.is_none()) + { + return; // no sync + } + + long value = 0; + try + { + value = streamArg.cast(); + } + catch (py::cast_error &) + { + // Unparseable stream arg; conservative: no sync, consumer is on + // their own. This also covers edge cases like callers passing a + // cupy.cuda.Stream object directly (which pybind11 may not cast). + return; + } + + if (value == 0 || value == -1) + { + return; // no sync + } + if (value == 1) + { + outConsumerStream = reinterpret_cast(1); // cudaStreamLegacy + outNeedSync = true; + return; + } + if (value == 2) + { + outConsumerStream = reinterpret_cast(2); // cudaStreamPerThread + outNeedSync = true; + return; + } + outConsumerStream = reinterpret_cast(value); + outNeedSync = true; +} + +// RAII guard that destroys a `cudaEvent_t` on scope exit. Used below so +// an exception thrown from `cudaEventRecord` or `cudaStreamWaitEvent` +// cannot leak the event that was just created. Declared local to this +// translation unit since no other file here needs it. +namespace { +struct ScopedCudaEvent +{ + cudaEvent_t handle = nullptr; + + ScopedCudaEvent() = default; + ScopedCudaEvent(const ScopedCudaEvent &) = delete; + ScopedCudaEvent &operator=(const ScopedCudaEvent &) = delete; + ScopedCudaEvent(ScopedCudaEvent &&) = delete; + ScopedCudaEvent &operator=(ScopedCudaEvent &&) = delete; + + ~ScopedCudaEvent() + { + if (handle != nullptr) + { + util::CheckLog(cudaEventDestroy(handle)); + } + } +}; + +struct VersionedManagerCtx +{ + DLManagedTensorVersioned tensor; + std::shared_ptr extBuffer; +}; + +struct ManagerCtx +{ + DLManagedTensor tensor; + std::shared_ptr extBuffer; +}; + +void DeleteShapeAndStrides(DLManagedTensor *self) noexcept +{ + std::unique_ptr shape(self->dl_tensor.shape); + std::unique_ptr strides(self->dl_tensor.strides); + self->dl_tensor.shape = nullptr; + self->dl_tensor.strides = nullptr; +} + +DLPackTensor CreateCAITensor() +{ + DLManagedTensor dlManagedTensor = {}; + dlManagedTensor.deleter = DeleteShapeAndStrides; + return DLPackTensor{std::move(dlManagedTensor)}; +} + +void DeleteVersionedManager(DLManagedTensorVersioned *tensor) +{ + std::unique_ptr ctx(static_cast(tensor->manager_ctx)); + (void)ctx; +} + +void DeleteManager(DLManagedTensor *tensor) +{ + std::unique_ptr ctx(static_cast(tensor->manager_ctx)); + (void)ctx; +} + +void ReleaseVersionedCapsule(PyObject *ptr) +{ + if (!PyCapsule_IsValid(ptr, "dltensor_versioned")) + { + return; + } + + auto *vt = static_cast(PyCapsule_GetPointer(ptr, "dltensor_versioned")); + if (vt != nullptr && vt->deleter != nullptr) + { + vt->deleter(vt); + } +} + +void ReleaseLegacyCapsule(PyObject *ptr) +{ + if (!PyCapsule_IsValid(ptr, "dltensor")) + { + return; + } + + auto *dlTensor = static_cast(PyCapsule_GetPointer(ptr, "dltensor")); + if (dlTensor != nullptr && dlTensor->deleter != nullptr) + { + dlTensor->deleter(dlTensor); + } +} + +bool UseVersionedDLPack(py::object maxVersion) +{ + if (maxVersion.is_none()) + { + return false; } + + py::tuple ver = maxVersion.cast(); + return ver.size() >= 1 && ver[0].cast() >= 1; } -static std::string ToFormatString(const DLDataType &dtype) +py::capsule CreateVersionedDLPackCapsule(const DLTensor &dlTensor, std::shared_ptr extBuffer) +{ + auto ctx = std::make_unique(); + + ctx->tensor.version = {DLPACK_MAJOR_VERSION, DLPACK_MINOR_VERSION}; + ctx->tensor.flags = 0; + ctx->tensor.manager_ctx = ctx.get(); + ctx->tensor.deleter = DeleteVersionedManager; + ctx->tensor.dl_tensor = dlTensor; + ctx->extBuffer = std::move(extBuffer); + + py::capsule cap(&ctx->tensor, "dltensor_versioned", ReleaseVersionedCapsule); + ctx.release(); + return cap; +} + +py::capsule CreateLegacyDLPackCapsule(const DLTensor &dlTensor, std::shared_ptr extBuffer) +{ + auto ctx = std::make_unique(); + + ctx->tensor.manager_ctx = ctx.get(); + ctx->tensor.deleter = DeleteManager; + ctx->tensor.dl_tensor = dlTensor; + ctx->extBuffer = std::move(extBuffer); + + py::capsule cap(&ctx->tensor, "dltensor", ReleaseLegacyCapsule); + ctx.release(); + return cap; +} + +// Insert a cross-stream synchronization into the consumer's stream before +// returning a DLPack capsule that exposes writer-stream-pending data. +// +// Records a fresh event on the producer's writer stream and queues a wait +// on the consumer's stream. Per CUDA docs the event may be destroyed +// immediately after `cudaStreamWaitEvent` returns (the wait captures a +// snapshot of the recording), so we destroy it on scope exit via +// `ScopedCudaEvent`. +// +// Semantics: +// * Writer stream unknown (buffer wasn't produced by cvcuda, i.e. +// m_hasExportStream is false or handle is null) — nothing to sync, +// return. +// * Writer == consumer — sync is trivial (stream is sequential), +// return. +// * Writer == opted-out sentinel (nullptr) from `setExportStream( +// (cudaStream_t)-1)` — producer has asserted no sync is needed, +// return. +// * Otherwise — insert `cudaEventRecord`+`cudaStreamWaitEvent`. +void InsertDLPackStreamSync(cudaStream_t writerStream, cudaStream_t consumerStream) +{ + if (writerStream == nullptr || writerStream == consumerStream) + { + return; + } + + ScopedCudaEvent evt; + util::CheckThrow(cudaEventCreateWithFlags(&evt.handle, cudaEventDisableTiming)); + util::CheckThrow(cudaEventRecord(evt.handle, writerStream)); + util::CheckThrow(cudaStreamWaitEvent(consumerStream, evt.handle, 0)); + // evt destroyed by ScopedCudaEvent destructor on return / on any + // exception propagating out of the CheckThrow calls above. +} + +std::string ToFormatString(const DLDataType &dtype) { py::dtype dt = ToDType(ToNVCVDataType(dtype)); return dt.attr("str").cast(); } -py::object ExternalBuffer::Create(DLPackTensor &&dlPackTensor, py::object wrappedObj) +std::unique_ptr ParseCAIShape(const py::tuple &shape) +{ + auto shapeData = std::make_unique(shape.size()); + + for (size_t i = 0; i < shape.size(); ++i) + { + shapeData[i] = shape[i].cast(); + } + + return shapeData; +} + +std::unique_ptr ParseCAIStrides(const py::dict &iface, int ndim, const int64_t *shape, int64_t itemSize) +{ + auto stridesData = std::make_unique(ndim); + + if (iface.contains("strides") && !iface["strides"].is_none()) + { + py::tuple strides = iface["strides"].cast(); + for (int i = 0; i < ndim; ++i) + { + int64_t strideBytes = strides[i].cast(); + if (strideBytes % itemSize != 0) + { + throw ExternalBufferError("Stride must be a multiple of the element size in bytes"); + } + stridesData[i] = strideBytes / itemSize; + } + return stridesData; + } + + if (ndim <= 0) + { + return stridesData; + } + + // If strides isn't defined, according to cuda array interface, we must + // set them up for packed, row-major strides. + stridesData[ndim - 1] = 1; + for (int i = ndim - 1; i > 0; --i) + { + stridesData[i - 1] = stridesData[i] * shape[i]; + } + return stridesData; +} + +bool HasRequiredCAIFields(const py::dict &iface) +{ + return iface.contains("shape") && iface.contains("typestr") && iface.contains("data") && iface.contains("version"); +} + +py::capsule RequestDLPackCapsule(const py::object &object) +{ + try + { + return object.attr("__dlpack__")("stream"_a = 1, "max_version"_a = py::make_tuple(1, 0)).cast(); + } + catch (py::error_already_set &) + { + // Producer doesn't accept max_version (v0 producer) -- retry without it. + PyErr_Clear(); + return object.attr("__dlpack__")("stream"_a = 1).cast(); + } +} + +} // namespace + +py::object ExternalBuffer::Create(DLPackTensor &&dlPackTensor, py::object wrappedObj, cudaStream_t exportStream, + bool setExportStream) { - std::shared_ptr buf(new ExternalBuffer(std::move(dlPackTensor))); + auto buf = std::make_shared(std::move(dlPackTensor)); + + if (setExportStream) + { + buf->setExportStream(exportStream); + } // We must make the returned object keep wrappedObj alive. // Using py::return_value_policy::reference_internal in py::cast doesn't work @@ -75,12 +475,12 @@ ExternalBuffer::ExternalBuffer(DLPackTensor &&dlTensor) { if (!IsCudaAccessible(dlTensor->device.device_type)) { - throw std::runtime_error("Only CUDA memory buffers can be wrapped"); + throw ExternalBufferError("Only CUDA memory buffers can be wrapped"); } if (dlTensor->data != nullptr) { - CheckValidCUDABuffer(dlTensor->data); + CheckValidCUDABuffer(static_cast(dlTensor->data)); } m_dlTensor = std::move(dlTensor); @@ -114,11 +514,6 @@ py::object ExternalBuffer::dtype() const return ToDType(ToNVCVDataType(m_dlTensor->dtype)); } -void *ExternalBuffer::data() const -{ - return m_dlTensor->data; -} - bool ExternalBuffer::load(PyObject *o) { if (!o) @@ -127,148 +522,131 @@ bool ExternalBuffer::load(PyObject *o) } py::object tmp = py::reinterpret_borrow(o); - if (hasattr(tmp, "__cuda_array_interface__")) { - py::dict iface = tmp.attr("__cuda_array_interface__").cast(); + return loadCudaArrayInterface(tmp); + } - if (!iface.contains("shape") || !iface.contains("typestr") || !iface.contains("data") - || !iface.contains("version")) - { - return false; - } + if (hasattr(tmp, "__dlpack__")) + { + return loadDLPack(tmp); + } - int version = iface["version"].cast(); - if (version < 2) - { - return false; - } + return false; +} - DLPackTensor dlTensor; - { - DLManagedTensor dlManagedTensor = {}; - dlManagedTensor.deleter = [](DLManagedTensor *self) - { - delete[] self->dl_tensor.shape; - delete[] self->dl_tensor.strides; - }; - dlTensor = DLPackTensor{std::move(dlManagedTensor)}; - } +bool ExternalBuffer::loadCudaArrayInterface(const py::object &object) +{ + py::dict iface = object.attr("__cuda_array_interface__").cast(); - dlTensor->byte_offset = 0; + if (!HasRequiredCAIFields(iface)) + { + return false; + } - // TODO: infer the device type from the memory buffer - dlTensor->device.device_type = kDLCUDA; - // TODO: infer the device from the memory buffer - dlTensor->device.device_id = 0; + if (int version = iface["version"].cast(); version < 2) + { + return false; + } - // Convert data - py::tuple tdata = iface["data"].cast(); - void *ptr = reinterpret_cast(tdata[0].cast()); - CheckValidCUDABuffer(ptr); - dlTensor->data = ptr; + DLPackTensor dlTensor = CreateCAITensor(); + dlTensor->byte_offset = 0; - // Convert DataType - py::dtype dt = util::ToDType(iface["typestr"].cast()); - if (std::optional dtype = ToNVCVDataType(dt)) - { - dlTensor->dtype = ToDLDataType(*dtype); - } + // REVISIT: infer the device type from the memory buffer + dlTensor->device.device_type = kDLCUDA; + // REVISIT: infer the device from the memory buffer + dlTensor->device.device_id = 0; - // Convert ndim - py::tuple shape = iface["shape"].cast(); - dlTensor->ndim = shape.size(); + py::tuple tdata = iface["data"].cast(); + auto *ptr = AsBytePointer(tdata[0].cast()); + CheckValidCUDABuffer(ptr); + dlTensor->data = ptr; - // Convert shape - dlTensor->shape = new int64_t[dlTensor->ndim]; - for (int i = 0; i < dlTensor->ndim; ++i) - { - dlTensor->shape[i] = shape[i].cast(); - } + py::dtype dt = util::ToDType(iface["typestr"].cast()); + if (std::optional dtype = ToNVCVDataType(dt)) + { + dlTensor->dtype = ToDLDataType(*dtype); + } - // Convert strides - dlTensor->strides = new int64_t[dlTensor->ndim]; - if (iface.contains("strides") && !iface["strides"].is_none()) - { - py::tuple strides = iface["strides"].cast(); - for (int i = 0; i < dlTensor->ndim; ++i) - { - dlTensor->strides[i] = strides[i].cast(); - if (dlTensor->strides[i] % dt.itemsize() != 0) - { - throw std::runtime_error("Stride must be a multiple of the element size in bytes"); - } - dlTensor->strides[i] /= dt.itemsize(); - } - } - else - { - // If strides isn't defined, according to cuda array interface, we must - // set them up for packed, row-major strides. - dlTensor->strides[dlTensor->ndim - 1] = 1; - for (int i = dlTensor->ndim - 1; i > 0; --i) - { - dlTensor->strides[i - 1] = dlTensor->strides[i] * dlTensor->shape[i]; - } - } + py::tuple shape = iface["shape"].cast(); + dlTensor->ndim = static_cast(shape.size()); + if (dlTensor->ndim < 1) + { + return false; + } - if (dlTensor->ndim >= 1) - { - m_wrappedObj = tmp; - m_cacheCudaArrayInterface = std::move(iface); - m_dlTensor = std::move(dlTensor); - return true; - } + auto shapeData = ParseCAIShape(shape); + auto stridesData = ParseCAIStrides(iface, dlTensor->ndim, shapeData.get(), dt.itemsize()); + dlTensor->shape = shapeData.release(); + dlTensor->strides = stridesData.release(); + + // Parse CAI v3 `stream` to record producer-stream info so the first cvcuda + // op that reads this buffer can insert the appropriate cross-stream wait. + ParseCAIStreamField(iface, m_producerStream, m_producerIsSynced); + m_producerDevice = GetCudaDeviceForPtr(static_cast(dlTensor->data)); + if (m_producerDevice >= 0) + { + dlTensor->device.device_id = m_producerDevice; } - else if (hasattr(tmp, "__dlpack__")) + + m_wrappedObj = object; + m_cacheCudaArrayInterface = std::move(iface); + m_dlTensor = std::move(dlTensor); + return true; +} + +bool ExternalBuffer::loadDLPack(const py::object &object) +{ + if (hasattr(object, "__dlpack_device__")) { - // Quickly check if we support the device - if (hasattr(tmp, "__dlpack_device__")) + py::tuple dlpackDevice = object.attr("__dlpack_device__")().cast(); + auto devType = static_cast(dlpackDevice[0].cast()); + if (!IsCudaAccessible(devType)) { - py::tuple dlpackDevice = tmp.attr("__dlpack_device__")().cast(); - auto devType = static_cast(dlpackDevice[0].cast()); - if (!IsCudaAccessible(devType)) - { - throw std::runtime_error("Only CUDA-accessible memory buffers can be wrapped"); - } + throw ExternalBufferError("Only CUDA-accessible memory buffers can be wrapped"); } + } - // PyTorch is divergent on the __dlpack__ method specification. - // By default, they will select a non-synchronization method for exporting - // the capsule. This is to ensure that existing libraries using PyTorch can - // continue to use CUDA graph capture. However, this is non-compliant with DLPack. - // As such, we must explicitly pass the legacy default stream setup to the __dlpack__ method. - // This explicit calling is compliant with the standard and means we do not - // have to worry about diverging behavior (in this case it would be a bug in external libraries). - // // Previous behavior (< 0.15.0): - // // cap = tmp.attr("__dlpack__")(1).cast(); // Legacy stream default - // // Current behavior (0.16.0 internal) - // // cap = tmp.attr("__dlpack__")().cast(); // Allow library to choose default - // This behavior is broken with PyNvVideoCodec which fills in zero when we call it here. - // // Fixed behavior (0.16.0 release) - // Always enforces the legacy stream default on the CV-CUDA side, compliant with DLPack. - // DLPack Python Spec: https://dmlc.github.io/dlpack/latest/python_spec.html - // DLPack 2022.12 Spec: https://data-apis.org/array-api/2022.12/API_specification/generated/array_api.array.__dlpack__.html - // Torch Issue 1: https://github.com/pytorch/pytorch/pull/150217/files - // Torch Issue 2: https://github.com/pytorch/pytorch/pull/163242 - // Discussion Thread on Torch Behavior: https://github.com/data-apis/array-api/issues/974 - py::capsule cap = tmp.attr("__dlpack__")("stream"_a = 1).cast(); - - if (auto *tensor = static_cast(cap.get_pointer())) + py::capsule cap = RequestDLPackCapsule(object); + loadDLPackCapsule(cap); + + // DLPack does not thread a producer-stream field through the capsule the + // way CAI does. We asked for stream=1, so seed that as the producer stream + // for the first cvcuda op that consumes the buffer. + if (m_dlTensor->data != nullptr) + { + m_producerStream = reinterpret_cast(1); + m_producerIsSynced = false; + m_producerDevice = GetCudaDeviceForPtr(static_cast(m_dlTensor->data)); + } + + return true; +} + +void ExternalBuffer::loadDLPackCapsule(py::capsule &cap) +{ + if (PyCapsule_IsValid(cap.ptr(), "dltensor_versioned")) + { + auto *vt = static_cast(PyCapsule_GetPointer(cap.ptr(), "dltensor_versioned")); + if (vt != nullptr) { - m_dlTensor = DLPackTensor{std::move(*tensor)}; - // signal that producer don't have to call tensor's deleter, we - // (consumer will do it instead. - cap.set_name("used_dltensor"); + m_dlTensor = DLPackTensor{vt->dl_tensor}; + m_dlManagedVersioned.reset(vt); + PyCapsule_SetName(cap.ptr(), "used_dltensor_versioned"); + return; } - else + } + else if (PyCapsule_IsValid(cap.ptr(), "dltensor")) + { + if (auto *tensor = static_cast(PyCapsule_GetPointer(cap.ptr(), "dltensor"))) { - m_dlTensor = {}; + m_dlTensor = DLPackTensor{std::move(*tensor)}; + PyCapsule_SetName(cap.ptr(), "used_dltensor"); + return; } - return true; } - return false; + m_dlTensor = {}; } std::optional ExternalBuffer::cudaArrayInterface() const @@ -304,6 +682,21 @@ std::optional ExternalBuffer::cudaArrayInterface() const std::string format = ToFormatString(m_dlTensor->dtype); + // CAI v3 `stream` field — advertise which stream the consumer must + // synchronize with before reading. If an export stream was set + // (typically by Tensor::cuda() from the owning tensor's last-writer + // stream), emit its integer handle. Otherwise, default to `1` (legacy + // default stream), which is the safe conservative value per the CAI + // spec and matches behavior for locally-constructed buffers whose + // writer stream is unknown. + long streamValue = 1; // cudaStreamLegacy + if (m_hasExportStream) + { + // m_exportStream == nullptr means "no sync needed" (-1 per spec), + // typically used when the producer has already synchronized. + streamValue = m_exportStream == nullptr ? -1L : reinterpret_cast(m_exportStream); + } + // clang-format off m_cacheCudaArrayInterface = py::dict { @@ -311,66 +704,54 @@ std::optional ExternalBuffer::cudaArrayInterface() const "strides"_a = strides, "typestr"_a = format, "data"_a = py::make_tuple(reinterpret_cast(m_dlTensor->data), false /* read/write */), - "version"_a = 2 + "stream"_a = streamValue, + "version"_a = 3 }; } return *m_cacheCudaArrayInterface; } -py::capsule ExternalBuffer::dlpack(py::object stream) const +py::capsule ExternalBuffer::dlpack(py::object stream, py::object maxVersion) const { - struct ManagerCtx + // Honor the DLPack v1 `stream` contract: when the consumer passes a + // CUDA stream, synchronize the writer stream into it before returning + // the capsule so any work the consumer queues on that stream waits for + // our pending writes. `m_hasExportStream` is set by Tensor::cuda() / + // Image::cuda() / Array::cuda() when a cvcuda op last wrote the buffer; + // if it's unset the buffer wasn't produced by cvcuda (pass-through + // import), and we have no writer stream to sync. + if (m_hasExportStream) { - DLManagedTensor tensor; - std::shared_ptr extBuffer; - }; - - auto ctx = std::make_unique(); + cudaStream_t consumerStream = nullptr; + bool needSync = false; + ParseDLPackStreamArg(stream, consumerStream, needSync); + if (needSync) + { + // m_exportStream == -1 sentinel means the producer asserted + // the data is already synchronized (CAI v3 `stream: -1` + // semantics); honor that by skipping the wait here too. + cudaStream_t writerStream = m_exportStream; + if (writerStream == reinterpret_cast(-1)) + { + writerStream = nullptr; + } + InsertDLPackStreamSync(writerStream, consumerStream); + } + } - // Set up tensor deleter to delete the ManagerCtx - ctx->tensor.manager_ctx = ctx.get(); - ctx->tensor.deleter = [](DLManagedTensor *tensor) - { - auto *ctx = static_cast(tensor->manager_ctx); - delete ctx; - }; - - // Copy tensor data - ctx->tensor.dl_tensor = *m_dlTensor; - - // Manager context holds a reference to this External Buffer so that - // GC doesn't delete this buffer while the dlpack tensor still refers to it. - ctx->extBuffer = this->shared_from_this(); - - // Creates the python capsule with the DLManagedTensor instance we're returning. - py::capsule cap(&ctx->tensor, "dltensor", [](PyObject *ptr) - { - if(PyCapsule_IsValid(ptr, "dltensor")) - { - // If consumer didn't delete the tensor, - if(auto *dlTensor = static_cast(PyCapsule_GetPointer(ptr, "dltensor"))) - { - // Delete the tensor. - if(dlTensor->deleter != nullptr) - { - dlTensor->deleter(dlTensor); - } - } - } - }); - - // Now that the capsule is created and the manager ctx was transfered to it, - // we can release the unique_ptr. - ctx.release(); + if (UseVersionedDLPack(maxVersion)) + { + return CreateVersionedDLPackCapsule(*m_dlTensor, this->shared_from_this()); + } - return cap; + return CreateLegacyDLPackCapsule(*m_dlTensor, this->shared_from_this()); } py::tuple ExternalBuffer::dlpackDevice() const { return py::make_tuple(py::int_(static_cast(m_dlTensor->device.device_type)), - py::int_(static_cast(m_dlTensor->device.device_id))); + py::int_(m_dlTensor->device.device_id)); } const DLTensor &ExternalBuffer::dlTensor() const @@ -384,7 +765,8 @@ void ExternalBuffer::Export(py::module &m) .def_property_readonly("shape", &ExternalBuffer::shape, "Get the shape of the buffer as an array") .def_property_readonly("strides", &ExternalBuffer::strides, "Get the strides of the buffer") .def_property_readonly("dtype", &ExternalBuffer::dtype, "Get the data type of the buffer") - .def("__dlpack__", &ExternalBuffer::dlpack, "stream"_a=1, "Export the buffer as a DLPack tensor") + .def("__dlpack__", &ExternalBuffer::dlpack, "stream"_a=1, "max_version"_a=py::none(), + "Export the buffer as a DLPack tensor") .def("__dlpack_device__", &ExternalBuffer::dlpackDevice, "Get the device associated with the buffer"); } @@ -395,9 +777,9 @@ namespace pybind11::detail { namespace priv = nvcvpy::priv; // Python -> C++ -bool type_caster::load(handle src, bool implicit_conv) +bool type_caster::load(handle src, bool) { - PyTypeObject *srctype = Py_TYPE(src.ptr()); + const PyTypeObject *srctype = Py_TYPE(src.ptr()); const type_info *cuda_buffer_type = get_type_info(typeid(priv::ExternalBuffer)); // src's type is ExternalBuffer? @@ -413,7 +795,7 @@ bool type_caster::load(handle src, bool implicit_conv) // create a ExternalBuffer out of it. else { - value.reset(new priv::ExternalBuffer); + value = std::make_shared(); return value->load(src.ptr()); } } diff --git a/python/mod_cvcuda/nvcv/ExternalBuffer.hpp b/python/mod_cvcuda/nvcv/ExternalBuffer.hpp index f77195d63..f3ce97322 100644 --- a/python/mod_cvcuda/nvcv/ExternalBuffer.hpp +++ b/python/mod_cvcuda/nvcv/ExternalBuffer.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -37,7 +37,26 @@ class ExternalBuffer final : public std::enable_shared_from_this ExternalBuffer(ExternalBuffer &&that) = delete; - static py::object Create(DLPackTensor &&dlTensor, py::object wrappedObj); + /** + * @brief Create an ExternalBuffer py::object that wraps a DLPack tensor. + * + * @param dlTensor Tensor data to wrap. + * @param wrappedObj Owner Python object kept alive with the buffer. + * @param exportStream Stream handle to advertise via the exported CAI + * `stream` field. Use the special value (void*)0x1 + * to skip populating (defaults to legacy default + * stream), or any valid CUDA stream handle when the + * producer (typically a cvcuda Tensor) wants + * downstream consumers to wait on a specific stream. + * Pass (cudaStream_t)-1 to advertise "no sync + * needed" (CAI `stream: -1`). + * @param setExportStream When true, `exportStream` is used to populate + * the CAI `stream` field (including the -1 + * opt-out). When false, the field defaults to + * `1` (legacy default). + */ + static py::object Create(DLPackTensor &&dlTensor, py::object wrappedObj, cudaStream_t exportStream = nullptr, + bool setExportStream = false); const DLTensor &dlTensor() const; @@ -45,26 +64,110 @@ class ExternalBuffer final : public std::enable_shared_from_this py::tuple strides() const; py::object dtype() const; - void *data() const; - bool load(PyObject *o); -private: + /** + * @brief Producer stream handle advertised via CAI `stream` on wrap. + * + * Returns the CUDA stream on which the producing library (e.g. cupy, + * torch) has outstanding work for this buffer, as reported by + * `__cuda_array_interface__["stream"]` at `load()` time. + * + * Returns 0 if: + * - The producer did not populate a stream field (v2 CAI, or absent), + * in which case the legacy default stream is implied. + * - The producer set `stream: None` or `stream: -1` (no sync required). + * - The buffer was constructed locally (not wrapped from Python). + * + * Valid only after `load()` has populated it; returns 0 otherwise. + */ + cudaStream_t producerStream() const + { + return m_producerStream; + } + + /** + * @brief Whether the wrap-time CAI indicated that no sync is required. + * + * True when the producer advertised `stream: None` or `stream: -1`. + * In that case, `producerStream()` returns 0 and the consumer must + * NOT insert any implicit wait. + */ + bool producerIsSynced() const + { + return m_producerIsSynced; + } + + /** + * @brief CUDA device on which the producer stream lives. + * + * Captured from `cudaPointerGetAttributes` on the buffer's device + * pointer at wrap time. Returns -1 if unknown (e.g. local buffer). + */ + int producerDevice() const + { + return m_producerDevice; + } + + /** + * @brief Set the stream handle to advertise on CAI export (`cudaArrayInterface`). + * + * Typically called by Tensor::cuda() / Image::cuda() with the current + * cvcuda owning stream of the data so downstream consumers (cupy/torch) + * know which stream to synchronize with. + */ + void setExportStream(cudaStream_t handle) + { + m_exportStream = handle; + m_hasExportStream = true; + m_cacheCudaArrayInterface.reset(); + } + explicit ExternalBuffer(DLPackTensor &&dlTensor); + ExternalBuffer() = default; +private: friend py::detail::type_caster; - ExternalBuffer() = default; DLPackTensor m_dlTensor; mutable std::optional m_cacheCudaArrayInterface; py::object m_wrappedObj; + // Producer stream advertised by the wrapped Python object's CAI dict + // (`__cuda_array_interface__["stream"]`). See accessors above for + // semantics of the three fields. + cudaStream_t m_producerStream = nullptr; + bool m_producerIsSynced = false; + int m_producerDevice = -1; + + // Producer stream to advertise back out via CAI export. When unset, the + // exporter emits the conservative "stream: 1" (legacy default stream). + cudaStream_t m_exportStream = nullptr; + bool m_hasExportStream = false; + + // Owns the DLManagedTensorVersioned for v1.0 imports. + // null for v0 imports and locally-created tensors. + struct VersionedDeleter + { + void operator()(DLManagedTensorVersioned *p) const + { + if (p && p->deleter) + p->deleter(p); + } + }; + + std::unique_ptr m_dlManagedVersioned; + // Returns the __cuda_array_interface__ if the buffer is cuda-accessible, // or std::nullopt if it's not. std::optional cudaArrayInterface() const; + bool loadCudaArrayInterface(const py::object &object); + bool loadDLPack(const py::object &object); + void loadDLPackCapsule(py::capsule &cap); + // __dlpack__ implementation - py::capsule dlpack(py::object stream) const; + py::capsule dlpack(py::object stream, py::object maxVersion) const; // __dlpack_device__ implementation py::tuple dlpackDevice() const; @@ -82,15 +185,14 @@ struct type_caster : public type_caster_base; -public: PYBIND11_TYPE_CASTER(std::shared_ptr, const_name("cvcuda.ExternalBuffer")); - operator type *() + explicit operator type *() { return value.get(); } - operator type &() + explicit operator type &() { return *value; } diff --git a/python/mod_cvcuda/nvcv/Image.cpp b/python/mod_cvcuda/nvcv/Image.cpp index c68edda72..9a32854f1 100644 --- a/python/mod_cvcuda/nvcv/Image.cpp +++ b/python/mod_cvcuda/nvcv/Image.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "Image.hpp" +#include "../NvtxRange.hpp" #include "Cache.hpp" #include "CastUtils.hpp" #include "DataType.hpp" @@ -34,6 +35,9 @@ #include #include +#include +#include + namespace nvcvpy::priv { bool Image::Key::doIsCompatible(const IKey &ithat) const @@ -72,158 +76,152 @@ size_t Image::Key::doGetHash() const namespace { +class ImageError : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + struct BufferImageInfo { int numPlanes; nvcv::Size2D size; int numChannels; bool isChannelLast; - int64_t planeStride, rowStride; + int64_t planeStride; + int64_t rowStride; nvcv::DataType dtype; - void *data; + NVCVByte *data; }; -std::vector ExtractBufferImageInfo(const std::vector &tensorList, - const nvcv::ImageFormat &fmt) +struct BufferTensorInfo { - std::vector bufferInfoList; - - int curChannel = 0; + std::array shape; + std::array strides; + nvcv::TensorLayout layout; +}; - // For each buffer, - for (size_t p = 0; p < tensorList.size(); ++p) +nvcv::TensorLayout SelectBufferLayout(const nvcv::ImageFormat &fmt, size_t plane, ssize_t channels) +{ + if (fmt != nvcv::FMT_NONE) { - const DLTensor &tensor = *tensorList[p]; - - int elemStrideBytes = (tensor.dtype.bits * tensor.dtype.lanes + 7) / 8; - - // Extract 4d shape and layout regardless of rank - ssize_t shape[4]; - ssize_t strides[4]; - nvcv::TensorLayout layout; - - switch (tensor.ndim) - { - case 1: - layout = nvcv::TENSOR_NCHW; - - shape[0] = 1; - shape[1] = 1; - shape[2] = 1; - shape[3] = tensor.shape[0]; - - strides[0] = tensor.strides[0] * elemStrideBytes; - strides[1] = strides[0]; - strides[2] = strides[0]; - strides[3] = strides[0]; - break; - - case 2: - layout = nvcv::TENSOR_NCHW; - - shape[0] = 1; - shape[1] = 1; - shape[2] = tensor.shape[0]; - shape[3] = tensor.shape[1]; - - strides[0] = tensor.shape[0] * tensor.strides[0] * elemStrideBytes; - strides[1] = strides[0]; - strides[2] = tensor.strides[0] * elemStrideBytes; - strides[3] = tensor.strides[1] * elemStrideBytes; - break; - - case 3: - case 4: - shape[0] = tensor.ndim == 3 ? 1 : tensor.shape[tensor.ndim - 4]; - shape[1] = tensor.shape[tensor.ndim - 3]; - shape[2] = tensor.shape[tensor.ndim - 2]; - shape[3] = tensor.shape[tensor.ndim - 1]; - - // User has specified a format? - if (fmt != nvcv::FMT_NONE) - { - // Use it to disambiguate - if (fmt.planeNumChannels(p) == shape[3]) - { - layout = nvcv::TENSOR_NHWC; - } - else - { - layout = nvcv::TENSOR_NCHW; - } - } - else - { - // Or else, - if (shape[3] <= 4) // (C<=4) - { - layout = nvcv::TENSOR_NHWC; - } - else - { - layout = nvcv::TENSOR_NCHW; - } - } - - strides[1] = tensor.strides[tensor.ndim - 3] * elemStrideBytes; - strides[2] = tensor.strides[tensor.ndim - 2] * elemStrideBytes; - strides[3] = tensor.strides[tensor.ndim - 1] * elemStrideBytes; + return fmt.planeNumChannels(static_cast(plane)) == channels ? nvcv::TENSOR_NHWC : nvcv::TENSOR_NCHW; + } - if (tensor.ndim == 3) - { - strides[0] = shape[1] * strides[1]; - } - else - { - strides[0] = tensor.strides[tensor.ndim - 4]; - } - break; + return channels <= 4 ? nvcv::TENSOR_NHWC : nvcv::TENSOR_NCHW; +} - default: - throw std::invalid_argument( - util::FormatString("Number of buffer dimensions must be between 1 and 4, not %d", tensor.ndim)); - } +BufferTensorInfo MakeBufferTensorInfo(const DLTensor &tensor, const nvcv::ImageFormat &fmt, size_t plane, + int elemStrideBytes) +{ + BufferTensorInfo info{}; - // Validate strides ----------------------- + switch (tensor.ndim) + { + case 1: + info.layout = nvcv::TENSOR_NCHW; + info.shape = {1, 1, 1, tensor.shape[0]}; + info.strides[0] = tensor.strides[0] * elemStrideBytes; + info.strides[1] = info.strides[0]; + info.strides[2] = info.strides[0]; + info.strides[3] = info.strides[0]; + break; + + case 2: + info.layout = nvcv::TENSOR_NCHW; + info.shape = {1, 1, tensor.shape[0], tensor.shape[1]}; + info.strides[0] = tensor.shape[0] * tensor.strides[0] * elemStrideBytes; + info.strides[1] = info.strides[0]; + info.strides[2] = tensor.strides[0] * elemStrideBytes; + info.strides[3] = tensor.strides[1] * elemStrideBytes; + break; + + case 3: + case 4: + info.shape[0] = tensor.ndim == 3 ? 1 : tensor.shape[tensor.ndim - 4]; + info.shape[1] = tensor.shape[tensor.ndim - 3]; + info.shape[2] = tensor.shape[tensor.ndim - 2]; + info.shape[3] = tensor.shape[tensor.ndim - 1]; + info.layout = SelectBufferLayout(fmt, plane, info.shape[3]); + + info.strides[1] = tensor.strides[tensor.ndim - 3] * elemStrideBytes; + info.strides[2] = tensor.strides[tensor.ndim - 2] * elemStrideBytes; + info.strides[3] = tensor.strides[tensor.ndim - 1] * elemStrideBytes; + info.strides[0] = tensor.ndim == 3 ? info.shape[1] * info.strides[1] : tensor.strides[tensor.ndim - 4]; + break; + + default: + throw std::invalid_argument( + util::ConcatString("Number of buffer dimensions must be between 1 and 4, not ", tensor.ndim)); + } - if (strides[0] <= 0 || strides[1] <= 0 || strides[2] <= 0) - { - throw std::invalid_argument("Buffer strides must be all >= 1"); - } + return info; +} - NVCV_ASSERT(layout.rank() == 4); +void ValidateBufferStrides(const BufferTensorInfo &bufferInfo, int elemStrideBytes, + const nvcv::TensorShapeInfoImagePlanar &infoShape) +{ + const auto &strides = bufferInfo.strides; + if (strides[0] <= 0 || strides[1] <= 0 || strides[2] <= 0) + { + throw std::invalid_argument("Buffer strides must be all >= 1"); + } - auto infoShape = nvcv::TensorShapeInfoImagePlanar::Create(nvcv::TensorShape(shape, 4, layout)); - NVCV_ASSERT(infoShape); + const auto &infoLayout = infoShape.infoLayout(); - const auto *infoLayout = &infoShape->infoLayout(); + if (strides[3] != elemStrideBytes) + { + throw std::invalid_argument( + util::ConcatString("Fastest changing dimension must be packed, i.e., have stride equal to ", + elemStrideBytes, " byte(s), not ", strides[3])); + } - if (strides[3] != elemStrideBytes) - { - throw std::invalid_argument(util::FormatString( - "Fastest changing dimension must be packed, i.e., have stride equal to %d byte(s), not %ld", - elemStrideBytes, strides[2])); - } + ssize_t packedRowStride = static_cast(elemStrideBytes) * infoShape.numCols(); + if (ssize_t rowStride = strides[infoLayout.idxHeight()]; + !infoLayout.isChannelLast() && rowStride != packedRowStride) + { + throw std::invalid_argument(util::ConcatString("Image row must packed, i.e., have stride equal to ", + packedRowStride, " byte(s), not ", rowStride)); + } +} - ssize_t packedRowStride = static_cast(elemStrideBytes) * infoShape->numCols(); - ssize_t rowStride = strides[infoLayout->idxHeight()]; - if (!infoLayout->isChannelLast() && rowStride != packedRowStride) - { - throw std::invalid_argument(util::FormatString( - "Image row must packed, i.e., have stride equal to %ld byte(s), not %ld", packedRowStride, rowStride)); - } +BufferImageInfo MakeBufferImageInfo(const DLTensor &tensor, const nvcv::ImageFormat &fmt, size_t plane) +{ + int elemStrideBytes = (tensor.dtype.bits * tensor.dtype.lanes + 7) / 8; + auto tensorInfo = MakeBufferTensorInfo(tensor, fmt, plane, elemStrideBytes); + + auto infoShape = nvcv::TensorShapeInfoImagePlanar::Create( + nvcv::TensorShape(tensorInfo.shape.data(), tensorInfo.shape.size(), tensorInfo.layout)); + NVCV_ASSERT(infoShape); + + ValidateBufferStrides(tensorInfo, elemStrideBytes, *infoShape); + + const auto &infoLayout = infoShape->infoLayout(); + + BufferImageInfo bufferInfo; + bufferInfo.isChannelLast = infoLayout.isChannelLast(); + bufferInfo.numPlanes + = bufferInfo.isChannelLast ? static_cast(infoShape->numSamples()) : infoShape->numChannels(); + bufferInfo.numChannels = infoShape->numChannels(); + bufferInfo.size = infoShape->size(); + bufferInfo.planeStride + = tensorInfo.strides[bufferInfo.isChannelLast ? infoLayout.idxSample() : infoLayout.idxChannel()]; + bufferInfo.rowStride = tensorInfo.strides[infoLayout.idxHeight()]; + bufferInfo.dtype = ToNVCVDataType(tensor.dtype); + bufferInfo.data = static_cast(tensor.data); + return bufferInfo; +} - bufferInfoList.emplace_back(); +std::vector ExtractBufferImageInfo(const std::vector &tensorList, + const nvcv::ImageFormat &fmt) +{ + std::vector bufferInfoList; + int curChannel = 0; - BufferImageInfo &bufInfo = bufferInfoList.back(); - bufInfo.isChannelLast = infoLayout->isChannelLast(); - bufInfo.numPlanes = bufInfo.isChannelLast ? infoShape->numSamples() : infoShape->numChannels(); - bufInfo.numChannels = infoShape->numChannels(); - bufInfo.size = infoShape->size(); - bufInfo.planeStride = strides[bufInfo.isChannelLast ? infoLayout->idxSample() : infoLayout->idxChannel()]; - bufInfo.rowStride = strides[infoLayout->idxHeight()]; - bufInfo.data = tensor.data; - bufInfo.dtype = ToNVCVDataType(tensor.dtype); + for (size_t p = 0; p < tensorList.size(); ++p) + { + const DLTensor &tensor = *tensorList[p]; + auto bufInfo = MakeBufferImageInfo(tensor, fmt, p); curChannel += bufInfo.numPlanes * bufInfo.numChannels; if (curChannel > 4) @@ -233,6 +231,7 @@ std::vector ExtractBufferImageInfo(const std::vector &planePixTy static_assert(NVCV_PACKING_0 == 0, "Invalid 0 packing value"); NVCV_ASSERT(planePixTypes.size() <= 4); - nvcv::Packing packing[4] = {nvcv::Packing::NONE}; + std::array packing = {nvcv::Packing::NONE}; int numChannels = 0; @@ -300,19 +301,19 @@ nvcv::ImageFormat InferImageFormat(const std::vector &planePixTy nvcv::DataKind dataKind = planePixTypes[0].dataKind(); - int numPlanes = planePixTypes.size(); + auto numPlanes = static_cast(planePixTypes.size()); // Planar or packed? if (numPlanes == 1 || numChannels == numPlanes) { - static const nvcv::ImageFormat baseFormatList[4] + static const std::array baseFormatList = {nvcv::FMT_U8, nvcv::FMT_2F32, nvcv::FMT_RGB8, nvcv::FMT_RGBA8}; // Validate array index to prevent buffer overrun if (numChannels < 1 || numChannels > 4) { throw std::invalid_argument( - util::FormatString("Invalid number of channels %d, must be between 1 and 4", numChannels)); + util::ConcatString("Invalid number of channels ", numChannels, ", must be between 1 and 4")); } nvcv::ImageFormat baseFormat = baseFormatList[numChannels - 1]; @@ -336,7 +337,7 @@ nvcv::ImageFormat InferImageFormat(const std::vector &planePixTy } } // semi-planar, NV12-like? - // TODO: this test is too fragile, must improve + // REVISIT: this test is too fragile, must improve else if (numPlanes == 2 && numChannels == 3) { return nvcv::FMT_NV12_ER.dataKind(dataKind).swizzleAndPacking(nvcv::Swizzle::S_XYZ0, packing[0], packing[1], @@ -387,8 +388,8 @@ void FillNVCVImageBufferStrided(NVCVImageData &imgData, const std::vector(b.data) + b.planeStride * p; + dataStrided.planes[curPlane].rowStride = static_cast(b.rowStride); + dataStrided.planes[curPlane].basePtr = b.data + b.planeStride * p; planeDataTypes.push_back(MakePackedType(b.dtype, b.isChannelLast ? b.numChannels : 1)); } @@ -409,9 +410,8 @@ void FillNVCVImageBufferStrided(NVCVImageData &imgData, const std::vector(finalFormat); nvcv::Size2D imgSize = {dataStrided.planes[0].width, dataStrided.planes[0].height}; @@ -432,10 +432,10 @@ void FillNVCVImageBufferStrided(NVCVImageData &imgData, const std::vector(size), std::get<1>(size), fmt, bufAlign.baseAddr(), bufAlign.rowAddr(), - &reqs); + nvcvImageCalcRequirements(std::get<0>(size), std::get<1>(size), static_cast(fmt), + bufAlign.baseAddr(), bufAlign.rowAddr(), &reqs); - m_impl = nvcv::Image(reqs, nullptr /* allocator */); - m_key = Key{size, fmt}; + m_impl = nvcv::Image(reqs); m_size_inbytes = doComputeSizeInBytes(reqs); } Image::Image(std::vector> bufs, const nvcv::ImageDataStridedCuda &imgData) - : m_key{} // it's a wrap! - , m_size_inbytes{doComputeSizeInBytes(NVCVImageRequirements())} + : m_size_inbytes{doComputeSizeInBytes(NVCVImageRequirements())} { m_wrapData.emplace(); this->setWrapData(std::move(bufs), imgData); } -Image::Image(std::vector bufs, const nvcv::ImageDataStridedHost &hostData, int rowAlign) +Image::Image(const std::vector &, const nvcv::ImageDataStridedHost &hostData, int rowAlign) { // Input buffer is host data. // We'll create a regular image and copy the host data into it. @@ -489,10 +488,10 @@ Image::Image(std::vector bufs, const nvcv::ImageDataStridedHost &hos nvcv::MemAlignment bufAlign = nvcv::MemAlignment{}.rowAddr(rowAlign); NVCVImageRequirements reqs; - nvcvImageCalcRequirements(hostData.size().w, hostData.size().h, hostData.format(), bufAlign.baseAddr(), - bufAlign.rowAddr(), &reqs); + nvcvImageCalcRequirements(hostData.size().w, hostData.size().h, static_cast(hostData.format()), + bufAlign.baseAddr(), bufAlign.rowAddr(), &reqs); - m_impl = nvcv::Image(reqs, nullptr /* allocator */); + m_impl = nvcv::Image(reqs); m_size_inbytes = doComputeSizeInBytes(reqs); auto devData = *m_impl.exportData(); @@ -519,7 +518,7 @@ Image::Image(std::vector bufs, const nvcv::ImageDataStridedHost &hos }; } -int64_t Image::doComputeSizeInBytes(const NVCVImageRequirements &reqs) +int64_t Image::doComputeSizeInBytes(const NVCVImageRequirements &reqs) const { int64_t size_inbytes; util::CheckThrow(nvcvMemRequirementsCalcTotalSizeBytes(&(reqs.mem.cudaMem), &size_inbytes)); @@ -533,16 +532,6 @@ int64_t Image::GetSizeInBytes() const return m_size_inbytes; } -std::shared_ptr Image::shared_from_this() -{ - return std::static_pointer_cast(Container::shared_from_this()); -} - -std::shared_ptr Image::shared_from_this() const -{ - return std::static_pointer_cast(Container::shared_from_this()); -} - std::shared_ptr Image::Create(const Size2D &size, nvcv::ImageFormat fmt, int rowAlign) { std::vector> vcont = Cache::Instance().fetch(Key{size, fmt}); @@ -550,7 +539,7 @@ std::shared_ptr Image::Create(const Size2D &size, nvcv::ImageFormat fmt, // None found? if (vcont.empty()) { - std::shared_ptr img(new Image(size, fmt, rowAlign)); + std::shared_ptr img(new Image(size, fmt, rowAlign)); // NOSONAR: constructor is private. Cache::Instance().add(*img); return img; } @@ -585,6 +574,31 @@ std::shared_ptr Image::WrapExternalBuffer(ExternalBuffer &buffer, nvcv::I return WrapExternalBufferVector({obj}, fmt); } +// Seed the Image's Resource with the producer stream of its wrapping +// ExternalBuffer so the first cvcuda op reading the image inserts the +// necessary cross-stream wait (CAI v3 `stream` field honoring). +// +// For multi-plane buffers we conservatively seed from the first buffer that +// advertises a (non-synced) stream; common-case cupy/torch interop uses a +// single buffer so this captures the full producer-stream contract. +static void SeedImageFromBuffers(Image &img, const std::vector> &bufs) +{ + for (const auto &buf : bufs) + { + if (!buf || buf->producerIsSynced() || buf->producerStream() == nullptr) + { + continue; + } + int device = buf->producerDevice(); + if (device < 0) + { + util::CheckThrow(cudaGetDevice(&device)); + } + img.seedLastStream(buf->producerStream(), device); + return; + } +} + std::vector> Image::WrapExternalBufferMany(std::vector> &buffers, nvcv::ImageFormat fmt) { @@ -597,13 +611,13 @@ std::vector> Image::WrapExternalBufferMany(std::vector> out; out.reserve(buffers.size()); - for (size_t i = 0; i < buffers.size(); ++i) + for (const auto &buffer : buffers) { std::vector> spBuffers; - spBuffers.push_back(buffers[i]); + spBuffers.push_back(buffer); if (!spBuffers.back()) - throw std::runtime_error("Input buffer doesn't provide cuda_array_interface or DLPack interfaces"); + throw ImageError("Input buffer doesn't provide cuda_array_interface or DLPack interfaces"); std::vector bufinfos; bufinfos.emplace_back(spBuffers[0]->dlTensor()); @@ -615,7 +629,8 @@ std::vector> Image::WrapExternalBufferMany(std::vector img(new Image(std::move(spBuffers), imgData)); + std::shared_ptr img(new Image(spBuffers, imgData)); // NOSONAR: constructor is private. + SeedImageFromBuffers(*img, spBuffers); Cache::Instance().add(*img); out.push_back(img); } @@ -623,11 +638,20 @@ std::vector> Image::WrapExternalBufferMany(std::vector img = std::static_pointer_cast(items.back()); items.pop_back(); - img->setWrapData(std::move(spBuffers), imgData); + img->setWrapData(spBuffers, imgData); + SeedImageFromBuffers(*img, spBuffers); out.push_back(img); } } + // Release any over-fetched or pre-existing not-in-use wrappers so their + // ExternalBuffer references (and thus the wrapped GPU buffers) are freed + // promptly. Drop 'items' first so those shared_ptrs no longer count as + // "in use", then run the cleanup. Images in 'out' are still in-use and + // will not be removed. + items.clear(); + Cache::Instance().removeAllNotInUseMatching(key); + return out; } @@ -638,24 +662,25 @@ std::shared_ptr Image::WrapExternalBufferVector(std::vector b { std::shared_ptr buffer = cast_py_object_as(obj); if (!buffer) - throw std::runtime_error("Input buffer doesn't provide cuda_array_interface or DLPack interfaces"); + throw ImageError("Input buffer doesn't provide cuda_array_interface or DLPack interfaces"); spBuffers.push_back(std::move(buffer)); } std::vector bufinfos; - for (size_t i = 0; i < spBuffers.size(); ++i) + for (const auto &buffer : spBuffers) { - bufinfos.emplace_back(spBuffers[i]->dlTensor()); + bufinfos.emplace_back(buffer->dlTensor()); } - nvcv::ImageDataStridedCuda imgData = CreateNVCVImageDataCuda(std::move(bufinfos), fmt); + nvcv::ImageDataStridedCuda imgData = CreateNVCVImageDataCuda(bufinfos, fmt); // This is the key of an image wrapper. // All image wrappers have the same key. Image::Key key; std::shared_ptr item = Cache::Instance().fetchOne(key); + std::shared_ptr img; // None found? if (!item) @@ -663,16 +688,22 @@ std::shared_ptr Image::WrapExternalBufferVector(std::vector b // Need to add wrappers into cache so that they don't get destroyed by // the cuda stream when they're last used, and python script isn't // holding a reference to them. If we don't do it, things might break. - std::shared_ptr img(new Image(std::move(spBuffers), imgData)); + img = std::shared_ptr(new Image(spBuffers, imgData)); // NOSONAR: constructor is private. Cache::Instance().add(*img); - return img; } else { - std::shared_ptr img = std::static_pointer_cast(item); - img->setWrapData(std::move(spBuffers), imgData); - return img; + img = std::static_pointer_cast(item); + img->setWrapData(spBuffers, imgData); } + SeedImageFromBuffers(*img, spBuffers); + + // Release any other not-in-use wrappers so their ExternalBuffer references + // (and thus the wrapped GPU buffers) are freed promptly. The current img + // is in-use and will not be removed. + Cache::Instance().removeAllNotInUseMatching(key); + + return img; } void Image::setWrapData(std::vector> bufs, const nvcv::ImageDataStridedCuda &imgData) @@ -680,11 +711,12 @@ void Image::setWrapData(std::vector> bufs, const NVCV_ASSERT(m_wrapData); NVCV_ASSERT(bufs.size() >= 1); - m_wrapData->devType = bufs[0]->dlTensor().device.device_type; + const DLDeviceType devType = bufs[0]->dlTensor().device.device_type; + py::object newObj; if (bufs.size() == 1) { - m_wrapData->obj = py::cast(bufs[0]); + newObj = py::cast(bufs[0]); } else { @@ -693,16 +725,24 @@ void Image::setWrapData(std::vector> bufs, const if (bufs[i]->dlTensor().device.device_type != bufs[0]->dlTensor().device.device_type || bufs[i]->dlTensor().device.device_id != bufs[0]->dlTensor().device.device_id) { - throw std::runtime_error("All buffers must belong to the same device, but some don't."); + throw ImageError("All buffers must belong to the same device, but some don't."); } } - m_wrapData->obj = py::cast(std::move(bufs)); + newObj = py::cast(std::move(bufs)); } + nvcv::Image newImpl = nvcv::ImageWrapData(imgData); + + // Cache::fetch only returns wrappers after their prior stream work has + // released them, so the old buffer's ordering state is safe to discard. + resetLastStreamForRebind(); + m_wrapData->devType = devType; + m_wrapData->obj = std::move(newObj); + //We recreate the nvcv::Image wrapper (m_impl) because it's cheap. //It's not cheap to create nvcvpy::Image as it might have allocated expensive resources (cudaEvent_t in Resource parent). - m_impl = nvcv::ImageWrapData(imgData); + m_impl = std::move(newImpl); } std::shared_ptr Image::CreateHost(py::buffer buffer, nvcv::ImageFormat fmt, int rowAlign) @@ -710,23 +750,24 @@ std::shared_ptr Image::CreateHost(py::buffer buffer, nvcv::ImageFormat fm return CreateHostVector(std::vector{buffer}, fmt, rowAlign); } -std::shared_ptr Image::CreateHostVector(std::vector buffers, nvcv::ImageFormat fmt, int rowAlign) +std::shared_ptr Image::CreateHostVector(const std::vector &buffers, nvcv::ImageFormat fmt, + int rowAlign) { std::vector dlTensorList; - for (size_t i = 0; i < buffers.size(); ++i) + for (const auto &buffer : buffers) { - dlTensorList.emplace_back(buffers[i].request(), DLDevice{kDLCPU, 0}); + dlTensorList.emplace_back(buffer.request(), DLDevice{kDLCPU, 0}); } - nvcv::ImageDataStridedHost imgData = CreateNVCVImageDataHost(std::move(dlTensorList), fmt); + nvcv::ImageDataStridedHost imgData = CreateNVCVImageDataHost(dlTensorList, fmt); // We take this opportunity to remove all wrappers from cache. // They aren't reusable anyway. Image::Key key; Cache::Instance().removeAllNotInUseMatching(key); - std::shared_ptr img(new Image(std::move(buffers), imgData, rowAlign)); + std::shared_ptr img(new Image(buffers, imgData, rowAlign)); // NOSONAR: constructor is private. Cache::Instance().add(*img); return img; } @@ -761,225 +802,263 @@ std::ostream &operator<<(std::ostream &out, const Image &img) namespace { -std::vector> ToPyBufferInfo(const nvcv::ImageDataStrided &imgData, - std::optional userLayout) +struct InferredBufferInfo { - if (imgData.numPlanes() < 1) + std::vector shape; + std::vector strides; + nvcv::TensorLayout layout; + py::dtype dtype; +}; + +void ValidateExportLayout(const std::optional &userLayout) +{ + if (!userLayout) { - return {}; + return; + } + + if (!nvcv::TensorLayoutInfoImage::Create(*userLayout)) + { + throw ImageError("Layout can't represent the planar images needed"); } +} + +bool PlaneMatchesSingleBuffer(const nvcv::ImageDataStrided &imgData, const nvcv::ImagePlaneStrided &firstPlane, int p) +{ + const nvcv::ImagePlaneStrided &plane = imgData.plane(p); + return plane.width == firstPlane.width && plane.height == firstPlane.height + && plane.rowStride == firstPlane.rowStride && imgData.format().planeDataType(0).numChannels() < 2 + && imgData.format().planeDataType(0) == imgData.format().planeDataType(p); +} + +bool PlaneStrideMatchesSingleBuffer(const nvcv::ImageDataStrided &imgData, int p) +{ + intptr_t goldPlaneStride = imgData.plane(1).basePtr - imgData.plane(0).basePtr; + intptr_t curPlaneStride = imgData.plane(p).basePtr - imgData.plane(p - 1).basePtr; + return curPlaneStride == goldPlaneStride; +} + +bool CanExportAsSingleBuffer(const nvcv::ImageDataStrided &imgData) +{ const nvcv::ImagePlaneStrided &firstPlane = imgData.plane(0); - std::optional infoLayout; - if (userLayout) + for (int p = 1; p < imgData.numPlanes(); ++p) { - if (auto tmp = nvcv::TensorLayoutInfoImage::Create(*userLayout)) + if (!PlaneMatchesSingleBuffer(imgData, firstPlane, p)) { - infoLayout.emplace(std::move(*tmp)); + return false; } - else + + if (p >= 2 && !PlaneStrideMatchesSingleBuffer(imgData, p)) { - throw std::runtime_error("Layout can't represent the planar images needed"); + return false; } } - bool singleBuffer = true; + return true; +} - // Let's check if we can return only one buffer, depending - // on the planes dimensions, pitch and data type. - for (int p = 1; p < imgData.numPlanes(); ++p) +int PlaneNumChannelsForExport(const nvcv::ImageFormat &format, int p) +{ + switch (format.planePacking(p)) { - const nvcv::ImagePlaneStrided &plane = imgData.plane(p); + // These (YUYV, UYVY, ...) need some special treatment. + // Although it's 3 channels in the plane, it's actually two channels per pixel. + case nvcv::Packing::X8_Y8__X8_Z8: + case nvcv::Packing::Y8_X8__Z8_X8: + return 2; + + default: + return format.planeNumChannels(p); + } +} - if (plane.width != firstPlane.width || plane.height != firstPlane.height - || plane.rowStride != firstPlane.rowStride || imgData.format().planeDataType(0).numChannels() >= 2 - || imgData.format().planeDataType(0) != imgData.format().planeDataType(p)) - { - singleBuffer = false; - break; - } +InferredBufferInfo InferSingleBufferInfo(const nvcv::ImageDataStrided &imgData, int p) +{ + const nvcv::ImagePlaneStrided &plane = imgData.plane(p); + int planeBPP = imgData.format().planeDataType(p).strideBytes(); - // check if using the same plane pitch - if (p >= 2) - { - intptr_t goldPlaneStrided = imgData.plane(1).basePtr - imgData.plane(0).basePtr; - intptr_t curPlaneStrided = imgData.plane(p).basePtr - imgData.plane(p - 1).basePtr; - if (curPlaneStrided != goldPlaneStrided) - { - singleBuffer = false; - break; - } - } + if (imgData.format().numChannels() == 1) + { + NVCV_ASSERT(imgData.numPlanes() == 1); + + InferredBufferInfo info; + info.shape = {plane.height, plane.width}; + info.strides = {plane.rowStride, planeBPP}; + info.layout = nvcv::TensorLayout{"HW"}; + info.dtype = py::cast(imgData.format().planeDataType(p)); + return info; } - std::vector> out; + if (imgData.numPlanes() == 1) + { + int planeNumChannels = PlaneNumChannelsForExport(imgData.format(), p); + NVCV_ASSERT(planeNumChannels >= 2); + + InferredBufferInfo info; + info.shape = {plane.height, plane.width, planeNumChannels}; + info.strides = {plane.rowStride, planeBPP, planeBPP / planeNumChannels}; + info.layout = nvcv::TensorLayout{"HWC"}; + info.dtype = py::cast(imgData.format().planeDataType(p).channelType(0)); + return info; + } - // If not using a single buffer, we'll forcibly use one buffer per plane. - int numBuffers = singleBuffer ? 1 : imgData.numPlanes(); + NVCV_ASSERT(PlaneNumChannelsForExport(imgData.format(), p) == 1); - for (int p = 0; p < numBuffers; ++p) + intptr_t planeStride = imgData.plane(1).basePtr - imgData.plane(0).basePtr; + NVCV_ASSERT(planeStride > 0); + + InferredBufferInfo info; + info.shape = {imgData.numPlanes(), plane.height, plane.width}; + info.strides = {planeStride, plane.rowStride, planeBPP}; + info.layout = nvcv::TensorLayout{"CHW"}; + info.dtype = py::cast(imgData.format().planeDataType(p)); + return info; +} + +InferredBufferInfo InferPlaneBufferInfo(const nvcv::ImageDataStrided &imgData, int p) +{ + const nvcv::ImagePlaneStrided &plane = imgData.plane(p); + int planeNumChannels = PlaneNumChannelsForExport(imgData.format(), p); + int planeBPP = imgData.format().planeDataType(p).strideBytes(); + + NVCV_ASSERT(imgData.numPlanes() >= 2); + + InferredBufferInfo info; + info.shape = {plane.height, plane.width, planeNumChannels}; + info.strides = {static_cast(plane.rowStride), static_cast(planeBPP), + static_cast(planeBPP / planeNumChannels)}; + info.layout = nvcv::TensorLayout{"HWC"}; + info.dtype = py::cast(imgData.format().planeDataType(p).channelType(0)); + return info; +} + +InferredBufferInfo InferBufferInfo(const nvcv::ImageDataStrided &imgData, int p, int numBuffers) +{ + if (numBuffers == 1) { - int planeWidth = imgData.plane(p).width; - int planeHeight = imgData.plane(p).height; - int planeNumChannels = imgData.format().planeNumChannels(p); - // bytes per pixel in the plane - int planeBPP = imgData.format().planeDataType(p).strideBytes(); + return InferSingleBufferInfo(imgData, p); + } + + return InferPlaneBufferInfo(imgData, p); +} - switch (imgData.format().planePacking(p)) +void ValidateRequiredLayoutDimensions(const InferredBufferInfo &inferred, const nvcv::TensorLayout &userLayout) +{ + for (int i = 0; i < inferred.layout.rank(); ++i) + { + if (inferred.shape[i] >= 2 && userLayout.find(inferred.layout[i]) < 0) { - // These (YUYV, UYVY, ...) need some special treatment. - // Although it's 3 channels in the plane, it's actually - // two channels per pixel. - case nvcv::Packing::X8_Y8__X8_Z8: - case nvcv::Packing::Y8_X8__Z8_X8: - planeNumChannels = 2; - break; - default: - break; + throw py::value_error(util::ConcatString("Layout need dimension '", inferred.layout[i], "'")); } + } +} - // Infer the layout and shape of this buffer - std::vector inferredShape; - std::vector inferredStrides; - nvcv::TensorLayout inferredLayout; +InferredBufferInfo ApplyUserLayout(const InferredBufferInfo &inferred, const nvcv::TensorLayout &userLayout) +{ + InferredBufferInfo out{{}, {}, userLayout, inferred.dtype}; + int idxLastInferDim = -1; - py::dtype inferredDType; + ValidateRequiredLayoutDimensions(inferred, userLayout); - if (numBuffers == 1) - { - if (imgData.format().numChannels() == 1) - { - NVCV_ASSERT(imgData.numPlanes() == 1); - inferredShape = {planeHeight, planeWidth}; - inferredStrides = {imgData.plane(p).rowStride, planeBPP}; - inferredLayout = nvcv::TensorLayout{"HW"}; - inferredDType = py::cast(imgData.format().planeDataType(p)); - } - else if (imgData.numPlanes() == 1) - { - NVCV_ASSERT(planeNumChannels >= 2); - inferredShape = {planeHeight, planeWidth, planeNumChannels}; - inferredStrides = {imgData.plane(p).rowStride, planeBPP, planeBPP / planeNumChannels}; - inferredLayout = nvcv::TensorLayout{"HWC"}; - inferredDType = py::cast(imgData.format().planeDataType(p).channelType(0)); - } - else - { - NVCV_ASSERT(planeNumChannels == 1); - - intptr_t planeStride = imgData.plane(1).basePtr - imgData.plane(0).basePtr; - NVCV_ASSERT(planeStride > 0); + for (int i = 0; i < userLayout.rank(); ++i) + { + int idxInferDim = inferred.layout.find(userLayout[i]); - inferredShape = {imgData.numPlanes(), planeHeight, planeWidth}; - inferredStrides = {planeStride, imgData.plane(p).rowStride, planeBPP}; - inferredLayout = nvcv::TensorLayout{"CHW"}; - inferredDType = py::cast(imgData.format().planeDataType(p)); - } + if (idxInferDim < 0) + { + out.shape.push_back(1); + // REVISIT: must do better than this + out.strides.push_back(0); + continue; } - else + + // The order of channels must be the same, despite of user layout having + // some other channels in the layout in between the channels in inferredLayout. + if (idxLastInferDim >= idxInferDim) { - NVCV_ASSERT(imgData.numPlanes() >= 2); - NVCV_ASSERT(imgData.numPlanes() == numBuffers); - - inferredShape = {planeHeight, planeWidth, planeNumChannels}; - inferredStrides - = {(int64_t)imgData.plane(p).rowStride, (int64_t)planeBPP, (int64_t)planeBPP / planeNumChannels}; - inferredLayout = nvcv::TensorLayout{"HWC"}; - inferredDType = py::cast(imgData.format().planeDataType(p).channelType(0)); + throw ImageError("Layout not compatible with image to be exported"); } - NVCV_ASSERT((ssize_t)inferredShape.size() == inferredLayout.rank()); - NVCV_ASSERT((ssize_t)inferredStrides.size() == inferredLayout.rank()); + idxLastInferDim = idxInferDim; + out.shape.push_back(inferred.shape[idxInferDim]); + out.strides.push_back(inferred.strides[idxInferDim]); + } - std::vector shape; - std::vector strides; - nvcv::TensorLayout layout; + return out; +} - // Do we have to use the layout user has specified? - if (userLayout) - { - layout = *userLayout; +InferredBufferInfo ResolveBufferInfoLayout(const InferredBufferInfo &inferred, + const std::optional &userLayout) +{ + if (userLayout) + { + return ApplyUserLayout(inferred, *userLayout); + } - // Check if user layout has all required dimensions - for (int i = 0; i < inferredLayout.rank(); ++i) - { - if (inferredShape[i] >= 2 && userLayout->find(inferredLayout[i]) < 0) - { - throw std::runtime_error(util::FormatString("Layout need dimension '%c'", inferredLayout[i])); - } - } + return inferred; +} - int idxLastInferDim = -1; +std::vector> ToPyBufferInfo(const nvcv::ImageDataStrided &imgData, + std::optional userLayout) +{ + if (imgData.numPlanes() < 1) + { + return {}; + } - // Fill up the final shape and strides according to the user layout - for (int i = 0; i < userLayout->rank(); ++i) - { - int idxInferDim = inferredLayout.find((*userLayout)[i]); - - if (idxInferDim < 0) - { - shape.push_back(1); - // TODO: must do better than this - strides.push_back(0); - } - else - { - // The order of channels must be the same, despite of - // user layout having some other channels in the layout - // in between the channels in inferredLayout. - if (idxLastInferDim >= idxInferDim) - { - throw std::runtime_error("Layout not compatible with image to be exported"); - } - idxLastInferDim = idxInferDim; - - shape.push_back(inferredShape[idxInferDim]); - strides.push_back(inferredStrides[idxInferDim]); - } - } - } - else - { - layout = inferredLayout; - shape = inferredShape; - strides = inferredStrides; - } + ValidateExportLayout(userLayout); + + std::vector> out; + + // If not using a single buffer, we'll forcibly use one buffer per plane. + int numBuffers = CanExportAsSingleBuffer(imgData) ? 1 : imgData.numPlanes(); + + for (int p = 0; p < numBuffers; ++p) + { + NVCV_ASSERT(numBuffers == 1 || imgData.numPlanes() == numBuffers); + + InferredBufferInfo inferred = InferBufferInfo(imgData, p, numBuffers); + NVCV_ASSERT(static_cast(inferred.shape.size()) == inferred.layout.rank()); + NVCV_ASSERT(static_cast(inferred.strides.size()) == inferred.layout.rank()); + + InferredBufferInfo resolved = ResolveBufferInfoLayout(inferred, userLayout); // There's no direct way to construct a py::buffer_info from data together with a py::dtype. // To do that, we first construct a py::array (it accepts py::dtype), and use ".request()" // to retrieve the corresponding py::buffer_info. // To avoid spurious data copies in py::array ctor, we create this dummy owner. py::tuple tmpOwner = py::make_tuple(); - py::array tmp(inferredDType, shape, strides, imgData.plane(p).basePtr, tmpOwner); - out.emplace_back(tmp.request(), layout); + py::array tmp(resolved.dtype, resolved.shape, resolved.strides, imgData.plane(p).basePtr, tmpOwner); + out.emplace_back(tmp.request(), resolved.layout); } return out; } std::vector ToPython(const nvcv::ImageData &imgData, std::optional userLayout, - py::object owner) + py::object owner, cudaStream_t exportStream, bool setExportStream) { std::vector out; auto pitchData = imgData.cast(); if (!pitchData) { - throw std::runtime_error("Only images with pitch-linear formats can be exported"); + throw ImageError("Only images with pitch-linear formats can be exported"); } for (const auto &[info, layout] : ToPyBufferInfo(*pitchData, userLayout)) { if (pitchData->cast()) { - // TODO: set correct device_type and device_id + // REVISIT: set correct device_type and device_id out.emplace_back(ExternalBuffer::Create( DLPackTensor{ info, {kDLCUDA, 0} }, - owner)); + owner, exportStream, setExportStream)); } else if (pitchData->cast()) { @@ -988,7 +1067,7 @@ std::vector ToPython(const nvcv::ImageData &imgData, std::optional layout) const { if (!IsCudaAccessible(m_wrapData->devType)) { - throw std::runtime_error("Image data can't be exported, it's not cuda-accessible"); + throw ImageError("Image data can't be exported, it's not cuda-accessible"); } // That's what we'll return, as m_impl is wrapping it. @@ -1015,10 +1094,15 @@ py::object Image::cuda(std::optional layout) const auto imgData = m_impl.exportData(); if (!imgData) { - throw std::runtime_error("Image data can't be exported, it's not cuda-accessible"); + throw ImageError("Image data can't be exported, it's not cuda-accessible"); } - std::vector out = ToPython(*imgData, layout, py::cast(*this)); + // Advertise the stream the image's data was last written on so + // downstream consumers (cupy/torch) can sync via CAI `stream`. + cudaStream_t lastStream = this->getLastStreamHandle(); + bool setStream = lastStream != nullptr; + + std::vector out = ToPython(*imgData, layout, py::cast(*this), lastStream, setStream); if (out.size() == 1) { @@ -1036,7 +1120,7 @@ py::object Image::cpu(std::optional layout) const auto devStrided = m_impl.exportData(); if (!devStrided) { - throw std::runtime_error("Only images with pitch-linear formats can be exported to CPU"); + throw ImageError("Only images with pitch-linear formats can be exported to CPU"); } std::vector> vDevBufInfo = ToPyBufferInfo(*devStrided, layout); @@ -1053,8 +1137,8 @@ py::object Image::cpu(std::optional layout) const py::buffer_info hostBufInfo = hostData.request(); std::vector hostStrides = hostBufInfo.strides; - auto infoShape - = nvcv::TensorShapeInfoImagePlanar::Create(nvcv::TensorShape(shape.data(), shape.size(), bufLayout)); + auto infoShape = nvcv::TensorShapeInfoImagePlanar::Create( + nvcv::TensorShape(shape.data(), static_cast(shape.size()), bufLayout)); NVCV_ASSERT(infoShape); int nplanes = infoShape->numPlanes(); @@ -1064,7 +1148,8 @@ py::object Image::cpu(std::optional layout) const ssize_t colStride = devStrides[infoShape->infoLayout().idxWidth()]; NVCV_ASSERT(colStride == hostStrides[infoShape->infoLayout().idxWidth()]); // both must be packed - ssize_t hostRowStride, devRowStride; + ssize_t hostRowStride; + ssize_t devRowStride; if (infoShape->infoLayout().idxHeight() >= 0) { devRowStride = devStrides[infoShape->infoLayout().idxHeight()]; @@ -1114,18 +1199,21 @@ void Image::Export(py::module &m) .def_static("zeros", &Image::Zeros, "size"_a, "format"_a, "rowalign"_a = 0, "Create an image filled with zeros with a given size, format and optional row align") .def("__repr__", &util::ToString) - .def("cuda", &Image::cuda, "layout"_a = std::nullopt, "The image on the CUDA device") - .def("cpu", &Image::cpu, "layout"_a = std::nullopt, "The image on the CPU") + .def("cuda", ::cvcudapy::NvtxTrace("cvcuda.Image.cuda", &Image::cuda), "layout"_a = std::nullopt, + "The image on the CUDA device") + .def("cpu", ::cvcudapy::NvtxTrace("cvcuda.Image.cpu", &Image::cpu), "layout"_a = std::nullopt, + "The image on the CPU") .def_property_readonly("size", &Image::size, "Read-only property that returns the size of the image") .def_property_readonly("width", &Image::width, "Read-only property that returns the width of the image") .def_property_readonly("height", &Image::height, "Read-only property that returns the height of the image") .def_property_readonly("format", &Image::format, "Read-only property that returns the format of the image"); // Make sure buffer lifetime is tied to image's (keep_alive) - m.def("as_image", &Image::WrapExternalBuffer, "buffer"_a, "format"_a = nvcv::FMT_NONE, py::keep_alive<0, 1>(), - "Wrap an external buffer as an image and tie the buffer lifetime to the image"); - m.def("as_image", &Image::WrapExternalBufferVector, py::arg_v("buffer", std::vector{}), + m.def("as_image", ::cvcudapy::NvtxTrace("cvcuda.as_image", &Image::WrapExternalBuffer), "buffer"_a, "format"_a = nvcv::FMT_NONE, py::keep_alive<0, 1>(), + "Wrap an external buffer as an image and tie the buffer lifetime to the image"); + m.def("as_image", ::cvcudapy::NvtxTrace("cvcuda.as_image", &Image::WrapExternalBufferVector), + py::arg_v("buffer", std::vector{}), "format"_a = nvcv::FMT_NONE, py::keep_alive<0, 1>(), "Wrap a vector of external buffers as an image and tie the buffer lifetime to the image"); } diff --git a/python/mod_cvcuda/nvcv/Image.hpp b/python/mod_cvcuda/nvcv/Image.hpp index d41c36287..24f51f3aa 100644 --- a/python/mod_cvcuda/nvcv/Image.hpp +++ b/python/mod_cvcuda/nvcv/Image.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -44,16 +44,14 @@ class Image final : public Container static std::shared_ptr Zeros(const Size2D &size, nvcv::ImageFormat fmt, int rowAlign); static std::shared_ptr Create(const Size2D &size, nvcv::ImageFormat fmt, int rowAlign); static std::shared_ptr CreateHost(py::buffer buffer, nvcv::ImageFormat fmt, int rowAlign); - static std::shared_ptr CreateHostVector(std::vector buffer, nvcv::ImageFormat fmt, int rowAlign); + static std::shared_ptr CreateHostVector(const std::vector &buffer, nvcv::ImageFormat fmt, + int rowAlign); static std::shared_ptr WrapExternalBuffer(ExternalBuffer &buffer, nvcv::ImageFormat fmt); static std::vector> WrapExternalBufferMany( std::vector> &buffer, nvcv::ImageFormat fmt); static std::shared_ptr WrapExternalBufferVector(std::vector buffer, nvcv::ImageFormat fmt); - std::shared_ptr shared_from_this(); - std::shared_ptr shared_from_this() const; - Size2D size() const; int32_t width() const; int32_t height() const; @@ -93,11 +91,11 @@ class Image final : public Container nvcv::ImageFormat m_format; bool m_isWrapper; - virtual size_t doGetHash() const override; - virtual bool doIsCompatible(const IKey &that) const override; + size_t doGetHash() const override; + bool doIsCompatible(const IKey &that) const override; }; - virtual const Key &key() const override + const Key &key() const override { return m_key; } @@ -108,9 +106,9 @@ class Image final : public Container private: explicit Image(const Size2D &size, nvcv::ImageFormat fmt, int rowAlign); explicit Image(std::vector> buf, const nvcv::ImageDataStridedCuda &imgData); - explicit Image(std::vector buf, const nvcv::ImageDataStridedHost &imgData, int rowalign); + explicit Image(const std::vector &buf, const nvcv::ImageDataStridedHost &imgData, int rowalign); - int64_t doComputeSizeInBytes(const NVCVImageRequirements &reqs); + int64_t doComputeSizeInBytes(const NVCVImageRequirements &reqs) const; void setWrapData(std::vector> buf, const nvcv::ImageDataStridedCuda &imgData); diff --git a/python/mod_cvcuda/nvcv/ImageBatch.cpp b/python/mod_cvcuda/nvcv/ImageBatch.cpp index a4bbf62c9..4fbe0e7c8 100644 --- a/python/mod_cvcuda/nvcv/ImageBatch.cpp +++ b/python/mod_cvcuda/nvcv/ImageBatch.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "ImageBatch.hpp" +#include "../NvtxRange.hpp" #include "CastUtils.hpp" #include "ExternalBuffer.hpp" #include "Image.hpp" @@ -24,8 +25,20 @@ #include #include +#include + namespace nvcvpy::priv { +namespace { + +class ImageBatchError : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + +} // namespace + size_t ImageBatchVarShape::Key::doGetHash() const { using util::ComputeHash; @@ -45,7 +58,7 @@ std::shared_ptr ImageBatchVarShape::Create(int capacity) // None found? if (vcont.empty()) { - std::shared_ptr batch(new ImageBatchVarShape(capacity)); + std::shared_ptr batch(new ImageBatchVarShape(capacity)); // NOSONAR: constructor is private. Cache::Instance().add(*batch); return batch; } @@ -70,12 +83,12 @@ std::shared_ptr ImageBatchVarShape::WrapExternalBufferVector std::shared_ptr buffer = cast_py_object_as(obj); if (!buffer) { - throw std::runtime_error("Input buffer doesn't provide cuda_array_interface or DLPack interfaces"); + throw ImageBatchError("Input buffer doesn't provide cuda_array_interface or DLPack interfaces"); } buflist.push_back(buffer); } - std::shared_ptr batch = Create(buffers.size()); + std::shared_ptr batch = Create(static_cast(buffers.size())); batch->pushBackMany(Image::WrapExternalBufferMany(buflist, fmt)); return batch; @@ -89,7 +102,7 @@ ImageBatchVarShape::ImageBatchVarShape(int capacity) m_list.reserve(capacity); } -int64_t ImageBatchVarShape::doComputeSizeInBytes(const NVCVImageBatchVarShapeRequirements &reqs) +int64_t ImageBatchVarShape::doComputeSizeInBytes(const NVCVImageBatchVarShapeRequirements &reqs) const { int64_t size_inbytes; util::CheckThrow(nvcvMemRequirementsCalcTotalSizeBytes(&(reqs.mem.cudaMem), &size_inbytes)); @@ -104,6 +117,19 @@ int64_t ImageBatchVarShape::GetSizeInBytes() const return m_size_inbytes; } +void ImageBatchVarShape::submitSync(Stream &stream) +{ + const auto synchronizedState = syncState(); + Resource::submitSync(stream); + for (const std::shared_ptr &image : m_list) + { + if (image && !image->submitSyncThrough(stream, synchronizedState)) + { + image->submitSync(stream); + } + } +} + const nvcv::ImageBatchVarShape &ImageBatchVarShape::impl() const { return m_impl; @@ -147,27 +173,28 @@ int32_t ImageBatchVarShape::numImages() const void ImageBatchVarShape::pushBack(Image &img) { m_impl.pushBack(img.impl()); - m_list.push_back(img.shared_from_this()); + m_list.push_back(SharedContainerFrom(img)); } void ImageBatchVarShape::pushBackMany(const std::vector> &imgList) { std::vector handlelist; handlelist.reserve(imgList.size()); - for (size_t i = 0; i < imgList.size(); ++i) + for (const auto &img : imgList) { - if (imgList[i]) + if (img) { - handlelist.push_back(imgList[i]->impl().handle()); + handlelist.push_back(img->impl().handle()); } else { handlelist.push_back(nullptr); } - m_list.push_back(imgList[i]); + m_list.push_back(img); } - nvcv::detail::CheckThrow(nvcvImageBatchVarShapePushImages(m_impl.handle(), handlelist.data(), handlelist.size())); + nvcv::detail::CheckThrow( + nvcvImageBatchVarShapePushImages(m_impl.handle(), handlelist.data(), static_cast(handlelist.size()))); } void ImageBatchVarShape::popBack(int imgCount) @@ -192,16 +219,6 @@ auto ImageBatchVarShape::end() const -> ImageList::const_iterator return m_list.end(); } -std::shared_ptr ImageBatchVarShape::shared_from_this() -{ - return std::static_pointer_cast(Container::shared_from_this()); -} - -std::shared_ptr ImageBatchVarShape::shared_from_this() const -{ - return std::static_pointer_cast(Container::shared_from_this()); -} - void ImageBatchVarShape::Export(py::module &m) { using namespace py::literals; @@ -226,8 +243,8 @@ void ImageBatchVarShape::Export(py::module &m) "Remove one or more images from the end of the ImageBatchVarShape.") .def("clear", &ImageBatchVarShape::clear, "Remove all images from the ImageBatchVarShape."); - m.def("as_images", &ImageBatchVarShape::WrapExternalBufferVector, py::arg_v("buffers", std::vector{}), - "format"_a = nvcv::FMT_NONE, py::keep_alive<0, 1>(), + m.def("as_images", ::cvcudapy::NvtxTrace("cvcuda.as_images", &ImageBatchVarShape::WrapExternalBufferVector), + py::arg_v("buffers", std::vector{}), "format"_a = nvcv::FMT_NONE, py::keep_alive<0, 1>(), "Wrap a vector of external buffers as a batch of images, and tie the buffers lifetime to it"); } diff --git a/python/mod_cvcuda/nvcv/ImageBatch.hpp b/python/mod_cvcuda/nvcv/ImageBatch.hpp index 324ae39b6..f278b0e51 100644 --- a/python/mod_cvcuda/nvcv/ImageBatch.hpp +++ b/python/mod_cvcuda/nvcv/ImageBatch.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -41,9 +41,6 @@ class ImageBatchVarShape : public Container static std::shared_ptr WrapExternalBufferVector(std::vector buffer, nvcv::ImageFormat fmt); - std::shared_ptr shared_from_this(); - std::shared_ptr shared_from_this() const; - const nvcv::ImageBatchVarShape &impl() const; nvcv::ImageBatchVarShape &impl(); @@ -55,6 +52,7 @@ class ImageBatchVarShape : public Container Size2D maxSize() const; int64_t GetSizeInBytes() const override; + void submitSync(Stream &stream) override; void pushBack(Image &img); void pushBackMany(const std::vector> &imgList); @@ -75,11 +73,11 @@ class ImageBatchVarShape : public Container private: int m_capacity; - virtual size_t doGetHash() const override; - virtual bool doIsCompatible(const IKey &that) const override; + size_t doGetHash() const override; + bool doIsCompatible(const IKey &that) const override; }; - virtual const Key &key() const override + const Key &key() const override { return m_key; } @@ -87,7 +85,7 @@ class ImageBatchVarShape : public Container private: explicit ImageBatchVarShape(int capacity); - int64_t doComputeSizeInBytes(const NVCVImageBatchVarShapeRequirements &reqs); + int64_t doComputeSizeInBytes(const NVCVImageBatchVarShapeRequirements &reqs) const; Key m_key; ImageList m_list; diff --git a/python/mod_cvcuda/nvcv/ImageFormat.cpp b/python/mod_cvcuda/nvcv/ImageFormat.cpp index 6fb09a49a..ac40ff31b 100644 --- a/python/mod_cvcuda/nvcv/ImageFormat.cpp +++ b/python/mod_cvcuda/nvcv/ImageFormat.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,6 +20,7 @@ #include #include +#include // So that pybind can export nvcv::ImageFormat as python enum namespace std { @@ -42,24 +43,26 @@ namespace nvcvpy::priv { static std::string ImageFormatToString(nvcv::ImageFormat fmt) { - const char *str = nvcvImageFormatGetName(fmt); + const char *str = nvcvImageFormatGetName(static_cast(fmt)); - std::string_view prefix = "NVCV_IMAGE_FORMAT_"; + auto starts_with = [](const char *s, std::string_view p) + { + return std::string_view{s}.compare(0, p.size(), p) == 0; + }; std::ostringstream out; - out << "nvcv."; - if (prefix == str) + if (constexpr std::string_view nvcvPrefix = "NVCV_IMAGE_FORMAT_"; starts_with(str, nvcvPrefix)) { - out << "Format." << str + prefix.length(); + out << "Format." << str + nvcvPrefix.length(); } else { - prefix = "ImageFormat"; - if (prefix == str) + constexpr std::string_view cppPrefix = "ImageFormat"; + if (starts_with(str, cppPrefix)) { - out << "Format" << str + prefix.length(); + out << "Format" << str + cppPrefix.length(); } else { @@ -81,6 +84,12 @@ void ExportImageFormat(py::module &m) #include "NVCVPythonImageFormatDefs.inc" + // FMT_NONE is a sentinel ("no format"), not part of the generated defs. + // Expose it so signature defaults like `format=FMT_NONE` render as the + // valid Python expression `nvcv.Format.NONE` instead of an unrepresentable + // `` string. + fmt.value("NONE", nvcv::FMT_NONE); + #undef DEF #undef DEF_NUM diff --git a/python/mod_cvcuda/nvcv/Object.cpp b/python/mod_cvcuda/nvcv/Object.cpp index 7dbb66803..68171f30a 100644 --- a/python/mod_cvcuda/nvcv/Object.cpp +++ b/python/mod_cvcuda/nvcv/Object.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,9 +19,6 @@ namespace nvcvpy::priv { -Object::~Object() -{ - // nothing -} +Object::~Object() = default; } // namespace nvcvpy::priv diff --git a/python/mod_cvcuda/nvcv/Resource.cpp b/python/mod_cvcuda/nvcv/Resource.cpp index c5634b925..1c237ee6f 100644 --- a/python/mod_cvcuda/nvcv/Resource.cpp +++ b/python/mod_cvcuda/nvcv/Resource.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,6 +21,7 @@ #include #include +#include namespace nvcvpy::priv { @@ -29,13 +30,23 @@ Resource::Resource() static uint64_t idnext = 0; m_id = idnext++; - - m_event = nullptr; } Resource::~Resource() { - cudaEventDestroy(m_event); + int savedDev = -1; + util::CheckLog(cudaGetDevice(&savedDev)); + + for (const auto &[dev, evt] : m_events) + { + util::CheckLog(cudaSetDevice(dev)); + util::CheckLog(cudaEventDestroy(evt)); + } + + if (savedDev >= 0) + { + util::CheckLog(cudaSetDevice(savedDev)); + } } uint64_t Resource::id() const @@ -45,37 +56,194 @@ uint64_t Resource::id() const cudaEvent_t Resource::event() { - if (m_event == nullptr) + int dev; + util::CheckThrow(cudaGetDevice(&dev)); + + auto it = m_events.find(dev); + if (it == m_events.end()) { - util::CheckThrow(cudaEventCreateWithFlags(&m_event, cudaEventDisableTiming)); + cudaEvent_t evt = nullptr; + util::CheckThrow(cudaEventCreateWithFlags(&evt, cudaEventDisableTiming)); + m_events.emplace(dev, evt); + return evt; } - return m_event; + return it->second; } void Resource::submitSync(Stream &stream) { - std::unique_lock lk(m_mtx); - //Check if we have a last stream, if not set it to the current stream - if (!m_lastStream.has_value()) + std::unique_lock lk(m_mtx); + + // Compute the "previous" stream handle. Either a cvcuda-owned Stream + // (m_lastStream) or a raw handle seeded from an external producer via + // seedLastStream() (m_lastStreamHandle). Both being unset means this is + // the first submission for this resource. + cudaStream_t prevHandle = nullptr; + if (m_lastStream.has_value()) + { + prevHandle = m_lastStream.value()->handle(); + } + else if (m_lastStreamHandle != nullptr) { - m_lastStream.emplace(stream.shared_from_this()); //store a shared pointer to the stream + prevHandle = m_lastStreamHandle; } - // if we are on the same stream we dont need to do anything - // as streams are sequential and we can assume that the last operation on the stream is done - if (m_lastStream.value()->handle() == stream.handle()) + // Fast path: previously bound to the same stream → no sync work, and + // no need to query the current device. Streams are sequential, so the + // last operation on this stream is already ordered before whatever the + // caller is about to enqueue. This path runs once per resource per Python + // op call after the first, so keeping it cheap (mutex + pointer compare, + // no CUDA driver round-trip) is the difference between an op wrapper + // adding ~5µs vs ~30µs of constant overhead per call. cudaGetDevice + // costs a few µs each invocation; with 5-7 resources locked per op, + // skipping it on the fast path saves 25-35µs per gamma_contrast_into / + // erase / brightness_contrast / etc. + if (prevHandle != nullptr && prevHandle == stream.handle()) { return; } - // if we are on a different stream we need to wait for that stream to finish - // write event on the old stream, the new stream will have to wait for it to be done - util::CheckThrow(cudaEventRecord(event(), m_lastStream.value()->handle())); - util::CheckThrow(cudaStreamWaitEvent(stream.handle(), event())); + // Slow path: need the current device for either first-binding state or + // cross-stream/cross-device sync logic. + int curDev; + util::CheckThrow(cudaGetDevice(&curDev)); + + // First submission for this resource — take ownership on the current + // stream and return with no sync work. + if (prevHandle == nullptr) + { + m_lastStream.emplace(stream.sharedStream()); + m_lastDevice = curDev; + return; + } + + // Defensive sync for CAI default-stream sentinels. A `prevHandle` of + // `cudaStreamLegacy` (1) or `cudaStreamPerThread` (2) only reaches us + // through CAI parsing — either a producer that explicitly advertised a + // default-stream sentinel (cupy reports the cupy-current stream at CAI- + // query time, not the actual writer stream) or a producer that didn't + // advertise a stream at all (CAI v2, e.g. PyTorch) and got the + // cudaStreamLegacy fallback. Both populations include producers that + // lie. An event-based barrier on a sentinel does NOT capture work on + // non-blocking streams, so we'd race against unfinished producer + // kernels. Fall back to `cudaDeviceSynchronize` so the wait is correct + // regardless of which stream the producer actually used. + // + // No real cvcuda stream handle is 1 or 2, so cvcuda → cvcuda chains + // (which advertise their actual writer stream as a real pointer) stay + // on the event-based fast path below. This branch only fires once per + // wrapped buffer's first cvcuda use; subsequent ops on the same buffer + // take the event-based path because m_lastStream is set after this. + if (prevHandle == cudaStreamLegacy || prevHandle == cudaStreamPerThread) + { + if (m_lastDevice != curDev) + { + int savedDev = curDev; + util::CheckThrow(cudaSetDevice(m_lastDevice)); + util::CheckThrow(cudaDeviceSynchronize()); + util::CheckThrow(cudaSetDevice(savedDev)); + } + else + { + util::CheckThrow(cudaDeviceSynchronize()); + } + } + // If the resource is moving between devices, CUDA events cannot synchronize + // across device boundaries. Fall back to a full stream synchronize. + else if (m_lastDevice != curDev) + { + // Sync the old stream on its device to ensure all work completes. + int savedDev = curDev; + util::CheckThrow(cudaSetDevice(m_lastDevice)); + util::CheckThrow(cudaStreamSynchronize(prevHandle)); + util::CheckThrow(cudaSetDevice(savedDev)); + } + else + { + // Same device — use the efficient event-based synchronization. + // Write event on the old stream, the new stream will wait for it. + util::CheckThrow(cudaEventRecord(event(), prevHandle)); + util::CheckThrow(cudaStreamWaitEvent(stream.handle(), event())); + } // update the last stream since we changed streams m_lastStream.reset(); - m_lastStream.emplace(stream.shared_from_this()); + m_lastStreamHandle = nullptr; + m_lastStream.emplace(stream.sharedStream()); + m_lastDevice = curDev; +} + +Resource::SyncState Resource::syncState() const +{ + std::unique_lock lk(m_mtx); + + if (m_lastStream.has_value()) + { + return {m_lastStream.value()->handle(), m_lastDevice}; + } + return {m_lastStreamHandle, m_lastDevice}; +} + +bool Resource::submitSyncThrough(Stream &stream, SyncState synchronizedState) +{ + std::unique_lock lk(m_mtx); + + SyncState currentState{m_lastStreamHandle, m_lastDevice}; + if (m_lastStream.has_value()) + { + currentState.stream = m_lastStream.value()->handle(); + } + if (currentState.stream != synchronizedState.stream || currentState.device != synchronizedState.device) + { + return false; + } + if (currentState.stream == stream.handle()) + { + return true; + } + + // The parent already established this stream dependency, so only transfer + // ownership; recording another event per child would duplicate that wait. + m_lastStream.reset(); + m_lastStreamHandle = nullptr; + m_lastStream.emplace(stream.sharedStream()); + m_lastDevice = stream.deviceId(); + return true; +} + +void Resource::seedLastStream(cudaStream_t handle, int device) +{ + std::unique_lock lk(m_mtx); + + // If some stream ownership is already recorded, respect it — either we've + // been seeded before, or a cvcuda op has already claimed this resource. + if (m_lastStream.has_value() || m_lastStreamHandle != nullptr) + { + return; + } + + m_lastStreamHandle = handle; + m_lastDevice = device; +} + +void Resource::resetLastStreamForRebind() +{ + std::unique_lock lk(m_mtx); + + m_lastStream.reset(); + m_lastStreamHandle = nullptr; + m_lastDevice = -1; +} + +cudaStream_t Resource::getLastStreamHandle() const +{ + std::unique_lock lk(m_mtx); + + if (m_lastStream.has_value()) + { + return m_lastStream.value()->handle(); + } + return m_lastStreamHandle; } std::shared_ptr Resource::shared_from_this() @@ -91,8 +259,19 @@ std::shared_ptr Resource::shared_from_this() const void Resource::Export(py::module &m) { py::class_>(m, "Resource", "Resource") - .def_property_readonly("id", &Resource::id, "Unique resource instance identifier") - .def("submitStreamSync", &Resource::submitSync, "Syncs object on new Stream"); + .def_property_readonly("id", &Resource::id, "Unique resource instance identifier"); +} + +void Resource::ExportStreamMethods(py::module &m) +{ + // Re-open the already-registered Resource class and add the stream-using + // method now that Stream is a known pybind11 type. Without this + // deferral, pybind11 would emit the raw C++ typename + // `nvcvpy::priv::Stream` in the signature. + py::object cls = m.attr("Resource"); + cls.attr("submitStreamSync") + = py::cpp_function(&Resource::submitSync, py::name("submitStreamSync"), py::is_method(cls), py::arg("stream"), + "Syncs object on new Stream"); } } // namespace nvcvpy::priv diff --git a/python/mod_cvcuda/nvcv/Resource.hpp b/python/mod_cvcuda/nvcv/Resource.hpp index 20c74805e..e06aed796 100644 --- a/python/mod_cvcuda/nvcv/Resource.hpp +++ b/python/mod_cvcuda/nvcv/Resource.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -27,34 +27,54 @@ #include #include +#include -// fwd declaration from driver_types.h -typedef struct CUevent_st *cudaEvent_t; +// fwd declaration from driver_types.h (cudaStream_t comes via Stream.hpp → cuda_runtime.h) +struct CUevent_st; +using cudaEvent_t = CUevent_st *; namespace nvcvpy::priv { namespace py = pybind11; +class ImageBatchVarShape; + /** * @brief A class representing a CUDA resource. * * This class encapsulates a CUDA resource and provides methods for synchronization * with CUDA streams. */ -class PYBIND11_EXPORT Resource : public virtual Object +class PYBIND11_EXPORT Resource : public virtual Object // NOSONAR: CUDA resources share one Object base. { public: /** * @brief Destructor. */ - ~Resource(); + ~Resource() override; /** * @brief Export the Resource class to Python. * + * Must be called before any class that inherits from Resource (e.g. + * Container). It does NOT bind methods whose signatures reference + * Stream — those are added later by ExportStreamMethods so pybind11 + * emits the proper Python typename in the stubs. + * * @param m The Python module to export the class to. */ static void Export(py::module &m); + /** + * @brief Bind Resource methods that reference Stream. + * + * Must be called AFTER Stream::Export so pybind11 can resolve Stream + * to its Python type in the method signature (otherwise the stubs + * leak the raw C++ typename `nvcvpy::priv::Stream`). + * + * @param m The Python module. + */ + static void ExportStreamMethods(py::module &m); + /** * @brief Get the unique identifier of the resource. * @@ -69,7 +89,32 @@ class PYBIND11_EXPORT Resource : public virtual Object * * @param stream The CUDA stream to synchronize with. */ - void submitSync(Stream &stream); + virtual void submitSync(Stream &stream); + + /** + * @brief Seed the "last stream" tracking with a raw CUDA stream handle. + * + * Used when wrapping an external buffer (e.g., a cupy/torch tensor) that + * advertises its producer stream via `__cuda_array_interface__["stream"]`. + * The next call to `submitSync` will insert the appropriate event-wait + * (or cross-device `cudaStreamSynchronize`) before the cvcuda op runs. + * + * No-op if the resource already has a last-stream set (subsequent ops + * own the sync chain). + * + * @param handle The producer CUDA stream handle. + * @param device The CUDA device the producer stream lives on. + */ + void seedLastStream(cudaStream_t handle, int device); + + /** + * @brief Get the handle of the stream this resource is currently pending on. + * + * Returns 0 if no prior stream has been recorded. Used by the CAI / + * DLPack exporters to populate the outgoing "stream" field so downstream + * consumers know which stream they must synchronize with. + */ + cudaStream_t getLastStreamHandle() const; /** * @brief Get a shared pointer to this resource. @@ -88,13 +133,39 @@ class PYBIND11_EXPORT Resource : public virtual Object protected: Resource(); + /** + * @brief Clear stream ownership before a cached wrapper is rebound to new storage. + * + * The caller must guarantee that the wrapper is no longer in flight on its + * recorded stream, as the cache does before returning a reusable wrapper. + */ + void resetLastStreamForRebind(); + private: + friend class ImageBatchVarShape; + + struct SyncState + { + cudaStream_t stream = nullptr; + int device = -1; + }; + + SyncState syncState() const; + bool submitSyncThrough(Stream &stream, SyncState synchronizedState); + uint64_t m_id; /**< The unique identifier of the resource. */ - cudaEvent_t m_event; /**< The CUDA event used for synchronization. */ + std::unordered_map m_events; /**< Per-device CUDA events for synchronization. */ std::optional> m_lastStream; /**< Cache the last stream used for this resource. */ - std::mutex m_mtx; /**< Lock reads and writes to the resource. */ + /** + * Raw handle of the last stream when it is an external (non-cvcuda) stream + * seeded via `seedLastStream` (e.g., from CAI `stream` field). Mutually + * exclusive with `m_lastStream`: at most one is set. + */ + cudaStream_t m_lastStreamHandle = nullptr; + int m_lastDevice = -1; /**< Device ID of the last stream. */ + mutable std::mutex m_mtx; /**< Lock reads and writes to the resource. */ - cudaEvent_t event(); + cudaEvent_t event(); /**< Returns the CUDA event for the current device. */ }; } // namespace nvcvpy::priv diff --git a/python/mod_cvcuda/nvcv/Stream.cpp b/python/mod_cvcuda/nvcv/Stream.cpp index 2322054f5..18650adf8 100644 --- a/python/mod_cvcuda/nvcv/Stream.cpp +++ b/python/mod_cvcuda/nvcv/Stream.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "Stream.hpp" +#include "../NvtxRange.hpp" #include "Cache.hpp" #include "Definitions.hpp" #include "StreamStack.hpp" @@ -27,13 +28,27 @@ #include #include +#include +#include +#include + namespace nvcvpy::priv { +namespace { + +class StreamError : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + +} // namespace + // Static members initialization -cudaStream_t Stream::m_auxStream = nullptr; -std::atomic Stream::m_instanceCount = 0; -std::mutex Stream::m_auxStreamMutex; -std::mutex Stream::m_gcMutex; +std::unordered_map Stream::m_auxStreams; +std::atomic Stream::m_instanceCount = 0; +std::shared_mutex Stream::m_auxStreamMutex; +std::mutex Stream::m_gcMutex; // Here we define the representation of external cuda streams. // It defines pybind11's type casters from the python object @@ -45,6 +60,7 @@ enum ExternalStreamType VOIDP, INT, TORCH, + CUPY, }; template @@ -64,12 +80,12 @@ class ExternalStream : public IExternalStream m_wrappedObj = std::move(obj); } - virtual cudaStream_t handle() const override + cudaStream_t handle() const override { return m_cudaStream; } - virtual py::object wrappedObject() const override + py::object wrappedObject() const override { return m_wrappedObj; } @@ -94,9 +110,7 @@ struct type_caster> bool load(handle src, bool) { - std::string strType = util::GetFullyQualifiedName(src); - - if (strType != "ctypes.c_void_p") + if (std::string strType = util::GetFullyQualifiedName(src); strType != "ctypes.c_void_p") { return false; } @@ -107,7 +121,7 @@ struct type_caster> void *data = *reinterpret_cast(info.ptr); - value.setCudaStream(reinterpret_cast(data), ::pybind11::cast(std::move(src))); + value.setCudaStream(reinterpret_cast(data), ::pybind11::cast(src)); return true; } }; @@ -121,13 +135,13 @@ struct type_caster> { try { - // TODO: don't know how to test if a python object + // REVISIT: don't know how to test if a python object // is convertible to a type without exceptions. intptr_t data = src.cast(); - value.setCudaStream(reinterpret_cast(data), ::pybind11::cast(std::move(src))); + value.setCudaStream(reinterpret_cast(data), ::pybind11::cast(src)); return true; } - catch (...) + catch (...) // NOSONAR: pybind type casters probe conversions by catching failures. { return false; } @@ -141,22 +155,47 @@ struct type_caster> bool load(handle src, bool) { - std::string strType = util::GetFullyQualifiedName(src); - - if (strType != "torch.cuda.streams.Stream" && strType != "torch.cuda.streams.ExternalStream") + if (std::string strType = util::GetFullyQualifiedName(src); + strType != "torch.cuda.streams.Stream" && strType != "torch.cuda.streams.ExternalStream") { return false; } try { - // TODO: don't know how to test if a python object + // REVISIT: don't know how to test if a python object // is convertible to a type without exceptions. intptr_t data = src.attr("cuda_stream").cast(); - value.setCudaStream(reinterpret_cast(data), ::pybind11::cast(std::move(src))); + value.setCudaStream(reinterpret_cast(data), ::pybind11::cast(src)); return true; } - catch (...) + catch (...) // NOSONAR: pybind type casters probe conversions by catching failures. + { + return false; + } + } +}; + +template<> +struct type_caster> +{ + PYBIND11_TYPE_CASTER(priv::ExternalStream, const_name("cupy.cuda.Stream")); + + bool load(handle src, bool) + { + if (std::string strType = util::GetFullyQualifiedName(src); + strType != "cupy.cuda.stream.Stream" && strType != "cupy.cuda.stream.ExternalStream") + { + return false; + } + + try + { + intptr_t data = src.attr("ptr").cast(); + value.setCudaStream(reinterpret_cast(data), ::pybind11::cast(src)); + return true; + } + catch (...) // NOSONAR: pybind type casters probe conversions by catching failures. { return false; } @@ -181,12 +220,13 @@ bool Stream::Key::doIsCompatible(const IKey &that) const std::shared_ptr Stream::Create() { + ::cvcudapy::NvtxRange nvtxRange("cvcuda.Stream.create"); std::vector> vcont = Cache::Instance().fetch(Stream::Key{}); // None found? if (vcont.empty()) { - std::shared_ptr stream(new Stream()); + std::shared_ptr stream(new Stream()); // NOSONAR: constructor is private. Cache::Instance().add(*stream); return stream; } @@ -206,9 +246,8 @@ Stream::Stream() util::CheckThrow(cudaStreamCreateWithFlags(&m_handle, cudaStreamNonBlocking)); incrementInstanceCount(); GetAuxStream(); - util::CheckThrow(cudaEventCreateWithFlags(&m_event, cudaEventDisableTiming)); } - catch (...) + catch (...) // NOSONAR: constructor cleanup must run for any exception. { destroy(); throw; @@ -216,23 +255,20 @@ Stream::Stream() } Stream::Stream(IExternalStream &extStream) - : m_owns(false) - , m_handle(extStream.handle()) - , m_wrappedObj(std::move(extStream.wrappedObject())) + : m_handle(extStream.handle()) + , m_wrappedObj(extStream.wrappedObject()) { - unsigned int flags; - if (cudaStreamGetFlags(m_handle, &flags) != cudaSuccess) + if (unsigned int flags; cudaStreamGetFlags(m_handle, &flags) != cudaSuccess) { - throw std::runtime_error("Invalid cuda stream"); + throw StreamError("Invalid cuda stream"); } try { incrementInstanceCount(); - GetAuxStream(); // Make sure the singleton aux stream is created - util::CheckThrow(cudaEventCreateWithFlags(&m_event, cudaEventDisableTiming)); + GetAuxStream(); } - catch (...) + catch (...) // NOSONAR: constructor cleanup must run for any exception. { destroy(); throw; @@ -249,23 +285,80 @@ int Stream::decrementInstanceCount() return m_instanceCount.fetch_sub(1, std::memory_order_acq_rel) - 1; } -cudaStream_t &Stream::GetAuxStream() -{ - if (!m_auxStream) +// Returns by value intentionally: returning a reference into m_auxStreams +// would be unsafe because a later insertion can trigger a rehash, invalidating +// the reference after the mutex is released. +cudaStream_t Stream::GetAuxStream() +{ + // Tolerate hosts with no CUDA device. The wrap-stream-0 ctor calls this + // at module init for cache warm-up; any real consumer of the returned + // handle (cudaStreamWaitEvent / cudaStreamAddCallback) requires a device + // and would have failed regardless. Returning nullptr here lets + // `import cvcuda` succeed on CPU-only build/CI hosts. + int dev = 0; + cudaError_t err = cudaGetDevice(&dev); + if (err == cudaErrorNoDevice || err == cudaErrorStubLibrary) { - std::lock_guard lock(m_auxStreamMutex); - if (!m_auxStream) - { - util::CheckThrow(cudaStreamCreateWithFlags(&m_auxStream, cudaStreamNonBlocking)); - } + (void)cudaGetLastError(); // clear sticky error + return nullptr; + } + util::CheckThrow(err); + + // Shared lock: concurrent readers when the entry already exists. + { + std::shared_lock lock(m_auxStreamMutex); + auto it = m_auxStreams.find(dev); + if (it != m_auxStreams.end()) + return it->second; } - return m_auxStream; + + // Exclusive lock: serializes the one-time insertion of a new entry. + std::unique_lock lock(m_auxStreamMutex); + auto [it, inserted] = m_auxStreams.try_emplace(dev, nullptr); + if (inserted) + { + cudaStream_t s = nullptr; + util::CheckThrow(cudaStreamCreateWithFlags(&s, cudaStreamNonBlocking)); + it->second = s; + } + return it->second; } void Stream::SyncAuxStream() { - cudaStream_t auxStream = GetAuxStream(); - util::CheckThrow(cudaStreamSynchronize(auxStream)); + ::cvcudapy::NvtxRange nvtxRange("cvcuda.Stream.syncAuxStream"); + // Sync aux streams on all devices. + std::shared_lock lock(m_auxStreamMutex); + int savedDev = 0; + util::CheckThrow(cudaGetDevice(&savedDev)); + + struct DeviceGuard + { + explicit DeviceGuard(int dev_) + : dev(dev_) + { + } + + DeviceGuard(const DeviceGuard &) = delete; + DeviceGuard(DeviceGuard &&) = delete; + DeviceGuard &operator=(const DeviceGuard &) = delete; + DeviceGuard &operator=(DeviceGuard &&) = delete; + + ~DeviceGuard() + { + util::CheckLog(cudaSetDevice(dev)); + } + + int dev; + }; + + DeviceGuard guard{savedDev}; + + for (const auto &[dev, auxStream] : m_auxStreams) + { + util::CheckThrow(cudaSetDevice(dev)); + util::CheckThrow(cudaStreamSynchronize(auxStream)); + } } Stream::~Stream() @@ -275,32 +368,47 @@ Stream::~Stream() void Stream::destroy() { - if (m_owns) + if (m_owns && m_handle) { - if (m_handle) - { - util::CheckLog(cudaStreamSynchronize(m_handle)); - util::CheckLog(cudaStreamDestroy(m_handle)); - m_handle = nullptr; - } + int savedDev = 0; + util::CheckLog(cudaGetDevice(&savedDev)); + util::CheckLog(cudaSetDevice(m_key.deviceId())); + util::CheckLog(cudaStreamSynchronize(m_handle)); + util::CheckLog(cudaStreamDestroy(m_handle)); + util::CheckLog(cudaSetDevice(savedDev)); + m_handle = nullptr; } { - std::lock_guard lock(m_auxStreamMutex); - if (m_auxStream && decrementInstanceCount() == 0) + std::unique_lock lock(m_auxStreamMutex); + if (decrementInstanceCount() == 0) { - util::CheckThrow(cudaStreamSynchronize(m_auxStream)); - util::CheckThrow(cudaStreamDestroy(m_auxStream)); - m_auxStream = nullptr; + int savedDev = 0; + util::CheckLog(cudaGetDevice(&savedDev)); + for (const auto &[dev, auxStream] : m_auxStreams) + { + util::CheckLog(cudaSetDevice(dev)); + util::CheckLog(cudaStreamSynchronize(auxStream)); + util::CheckLog(cudaStreamDestroy(auxStream)); + } + m_auxStreams.clear(); + util::CheckLog(cudaSetDevice(savedDev)); } } - if (m_event) { - util::CheckThrow(cudaEventDestroy(m_event)); - m_event = nullptr; + std::lock_guard lock(m_eventMutex); + int savedDev = 0; + util::CheckLog(cudaGetDevice(&savedDev)); + for (const auto &[dev, evt] : m_events) + { + util::CheckLog(cudaSetDevice(dev)); + util::CheckLog(cudaEventDestroy(evt)); + } + m_events.clear(); + util::CheckLog(cudaSetDevice(savedDev)); } } -int64_t Stream::doComputeSizeInBytes() +int64_t Stream::doComputeSizeInBytes() const { // We only cache the stream's handles, which are 8 byte on CPU memory, hence 0 bytes gpu memory. return 0; @@ -314,12 +422,12 @@ int64_t Stream::GetSizeInBytes() const return m_size_inbytes; } -std::shared_ptr Stream::shared_from_this() +std::shared_ptr Stream::sharedStream() { return std::dynamic_pointer_cast(Object::shared_from_this()); } -std::shared_ptr Stream::shared_from_this() const +std::shared_ptr Stream::sharedStream() const { return std::dynamic_pointer_cast(Object::shared_from_this()); } @@ -329,6 +437,11 @@ cudaStream_t Stream::handle() const return m_handle; } +int Stream::deviceId() const +{ + return m_key.deviceId(); +} + intptr_t Stream::pyhandle() const { return reinterpret_cast(m_handle); @@ -336,24 +449,60 @@ intptr_t Stream::pyhandle() const void Stream::sync() { + ::cvcudapy::NvtxRange nvtxRange("cvcuda.Stream.sync"); py::gil_scoped_release release; util::CheckThrow(cudaStreamSynchronize(m_handle)); } +void Stream::wait_stream(std::shared_ptr other) +{ + ::cvcudapy::NvtxRange nvtxRange("cvcuda.Stream.wait_stream"); + if (!other) + throw std::invalid_argument("other is null"); + + if (other->handle() == m_handle) + return; + + py::gil_scoped_release release; + cudaEvent_t evt = other->getEvent(); + util::CheckThrow(cudaEventRecord(evt, other->handle())); + util::CheckThrow(cudaStreamWaitEvent(m_handle, evt, 0)); +} + Stream &Stream::Current() { auto defStream = StreamStack::Instance().top(); - NVCV_ASSERT(defStream); + if (!defStream) + { + // Empty stream stack means Stream::Export skipped wrapping the legacy + // default stream — only happens on no-device hosts. Throw a Python- + // visible error rather than NVCV_ASSERT (which would abort()), so + // that introspection tools (pybind11_stubgen, sphinx) can read the + // class without crashing. + throw StreamError("No default cvcuda.Stream available (no CUDA device visible)."); + } return *defStream; } void Stream::activate() { + ::cvcudapy::NvtxRange nvtxRange("cvcuda.Stream.__enter__"); + if (m_owns) + { + int curDev = 0; + util::CheckThrow(cudaGetDevice(&curDev)); + if (m_key.deviceId() != curDev) + { + throw StreamError("Cannot activate cvcuda.Stream (created on device " + std::to_string(m_key.deviceId()) + + ") while current CUDA device is " + std::to_string(curDev)); + } + } StreamStack::Instance().push(*this); } -void Stream::deactivate(py::object exc_type, py::object exc_value, py::object exc_tb) +void Stream::deactivate(py::object, py::object, py::object) const { + ::cvcudapy::NvtxRange nvtxRange("cvcuda.Stream.__exit__"); StreamStack::Instance().pop(); } @@ -368,6 +517,29 @@ struct Stream::HostFunctionClosure LockResources resources; }; +cudaEvent_t Stream::getEvent() +{ + int dev; + util::CheckThrow(cudaGetDevice(&dev)); + + { + std::shared_lock lock(m_eventMutex); + auto it = m_events.find(dev); + if (it != m_events.end()) + return it->second; + } + + std::unique_lock lock(m_eventMutex); + auto [it, inserted] = m_events.try_emplace(dev, nullptr); + if (inserted) + { + cudaEvent_t evt = nullptr; + util::CheckThrow(cudaEventCreateWithFlags(&evt, cudaEventDisableTiming)); + it->second = evt; + } + return it->second; +} + void Stream::holdResources(LockResources usedResources) { if (!usedResources.empty()) @@ -382,12 +554,12 @@ void Stream::holdResources(LockResources usedResources) auto closure = std::make_unique(); - closure->stream = this->shared_from_this(); + closure->stream = this->sharedStream(); closure->resources = std::move(usedResources); - auto fn = [](cudaStream_t stream, cudaError_t error, void *userData) -> void + auto fn = [](cudaStream_t, cudaError_t, auto *userData) { - std::unique_ptr pclosure(reinterpret_cast(userData)); + std::unique_ptr pclosure(static_cast(userData)); NVCV_ASSERT(pclosure != nullptr); AddToGCBag(std::move(pclosure)); }; @@ -412,9 +584,9 @@ void Stream::holdResources(LockResources usedResources) // GPU activity xxxxxxxxxxx xxxxxxxxxxx // CPU activity xxxxxxxx - util::CheckThrow(cudaEventRecord(m_event, m_handle)); // add async record the event in the main stream - util::CheckThrow( - cudaStreamWaitEvent(GetAuxStream(), m_event)); // add async wait for the event in the aux stream + cudaEvent_t evt = getEvent(); + util::CheckThrow(cudaEventRecord(evt, m_handle)); // add async record the event in the main stream + util::CheckThrow(cudaStreamWaitEvent(GetAuxStream(), evt)); // add async wait for the event in the aux stream // cudaStreamAddCallback pushes a task to the given stream, which at some point (asynchonously) calls // the given callback (fn), passing to it the closure we created, among other stream states. @@ -445,6 +617,11 @@ Stream::GCBag &Stream::GetGCBag() // is destroyed. This is due to this function being called the first time // (via AddToGCBag or ClearGCBag) only after the python script (and python // ctx) has already started. + // + // Multi-GPU safety: this is a single process-wide bag holding closures from + // all devices. This is safe because access is serialized by m_gcMutex, and + // each HostFunctionClosure holds a shared_ptr whose destroy() + // properly saves/restores the CUDA device before freeing device resources. static GCBag gcBag; return gcBag; } @@ -483,6 +660,21 @@ void Stream::ClearGCBag() // will be finally destroyed with the mutex unlocked. } +void Stream::SynchronizeAndClearGCBag() +{ + bool hasAuxStreams; + { + std::shared_lock lock(m_auxStreamMutex); + hasAuxStreams = !m_auxStreams.empty(); + } + + if (hasAuxStreams) + { + SyncAuxStream(); + } + ClearGCBag(); +} + std::ostream &operator<<(std::ostream &out, const Stream &stream) { return out << " extStream) { return std::shared_ptr(new Stream(extStream)); }); + m.def("as_stream", + [](ExternalStream extStream) + { + ::cvcudapy::NvtxRange nvtxRange("cvcuda.as_stream"); + return std::make_shared(extStream); + }); } -void Stream::Export(py::module &m) +static void LogCleanupWarning(const std::exception &e) { - py::class_, CacheItem> stream(m, "Stream"); + std::cerr << "Warning CVCUDA cleanup may be incomplete due to: " << e.what() << std::endl; +} + +void Stream::CleanupAtExit(const std::shared_ptr &globalStream) +{ + // No globalStream means no device at module-init time, no streams were ever + // created, nothing to sync. Cleanup is a no-op. + if (!globalStream) + { + return; + } + + try + { + for (std::shared_ptr stream : Cache::Instance().fetchAll()) + { + stream->sync(); + } + globalStream->sync(); + SyncAuxStream(); + + // There should only be 1 stream in the stack, namely the global stream. + if (auto s = StreamStack::Instance().top(); s != globalStream) + { + std::cerr << "Stream stack leak detected" << std::endl; + } + + // Make sure stream stack is empty. + while (auto s = StreamStack::Instance().top()) + { + StreamStack::Instance().pop(); + } + + // Make sure the gc bag is also cleaned up *after* all streams are done, + // when all remaining items that need to be GC'd are in the bag. + ClearGCBag(); + } + catch (const py::error_already_set &e) + { + LogCleanupWarning(e); + } + catch (const StreamError &e) + { + LogCleanupWarning(e); + } + catch (const std::invalid_argument &e) + { + LogCleanupWarning(e); + } + catch (const std::domain_error &e) + { + LogCleanupWarning(e); + } + catch (const std::length_error &e) + { + LogCleanupWarning(e); + } + catch (const std::out_of_range &e) + { + LogCleanupWarning(e); + } + catch (const std::range_error &e) + { + LogCleanupWarning(e); + } + catch (const std::overflow_error &e) + { + LogCleanupWarning(e); + } + catch (const std::underflow_error &e) + { + LogCleanupWarning(e); + } + catch (const std::bad_alloc &e) + { + LogCleanupWarning(e); + } +} - stream - .def_property_readonly_static( - "current", [](py::object) { return Current().shared_from_this(); }, - "Get the current CUDA stream for this thread.") - .def(py::init(&Stream::Create), "Create a new CUDA stream."); +void Stream::Export(py::module &m) +{ + py::class_, CacheItem> stream(m, "Stream", + R"pbdoc( + A CUDA stream that cvcuda operators can be submitted to. + + Streams created via ``cvcuda.Stream()`` are non-blocking + (``cudaStreamNonBlocking``) so they can run concurrently with CUDA's + legacy default stream and with other non-blocking streams. + + Cross-library stream safety is handled automatically via the CUDA + Array Interface (CAI) v3 ``stream`` field: + + * On **input**, ``cvcuda.as_tensor`` / ``cvcuda.as_image`` read + ``__cuda_array_interface__["stream"]`` from the producer (cupy, + torch, jax, etc.) and arrange for the first cvcuda operator to + insert the proper ``cudaStreamWaitEvent`` before reading the data. + No user-side ``synchronize()`` call is required. + * On **output**, tensors returned by ``cvcuda.Tensor.cuda()`` / + ``cvcuda.Image.cuda()`` populate ``__cuda_array_interface__ + ["stream"]`` with the cvcuda stream the data was last written on, + so downstream consumers can sync themselves. + + Advanced usage: + + * **Opt out of the implicit wait**: export your buffer with + ``stream: -1`` in its CAI dict to tell cvcuda you've already + synchronized; the first op then runs without an extra event. + * **Legacy default stream**: if your producer uses v2 CAI (no + ``stream`` field) or advertises ``stream: 1``, cvcuda waits on the + legacy default stream. This is the safe conservative default but + forfeits concurrency with stream-0 work. + * **Use the default stream**: ``cvcuda.Stream.default`` wraps stream + 0 and has the usual legacy-default blocking semantics; use it when + you want implicit sync with everything else on stream 0 and don't + need the concurrency of a dedicated stream. + )pbdoc"); + + stream.def(py::init(&Stream::Create), + "Create a new CUDA stream. Streams created this way are non-blocking; " + "cross-library synchronization is handled automatically via the CAI " + "``stream`` field (see the class docstring)."); py::module_ internal = m.attr(INTERNAL_SUBMODULE_NAME); internal.def("syncAuxStream", &SyncAuxStream); - // Create the global stream object by wrapping cuda stream 0. - // It'll be destroyed when python module is deinitialized. - static priv::ExternalStream cudaDefaultStream((cudaStream_t)0); - auto globalStream = std::make_shared(cudaDefaultStream); - StreamStack::Instance().push(*globalStream); - stream.attr("default") = globalStream; + // Wrap the CUDA legacy default stream as `cvcuda.Stream.default` and seed + // the per-thread stream stack so `cvcuda.Stream.current` has something to + // return. + // + // Skipped on hosts with no CUDA device (CPU-only build/CI nodes, + // CUDA_VISIBLE_DEVICES=""): wrapping stream 0 unavoidably touches the + // CUDA runtime (e.g. the `Stream` ctor's cudaStreamGetFlags + aux-stream + // bookkeeping), which fails without a driver-bound device. We also skip + // registering the `current` static property in that case — there is no + // sensible value, and exposing a getter that throws breaks introspection + // tools (pybind11_stubgen, sphinx-autodoc) that walk class members. + int deviceCount = 0; + if (cudaError_t devCntErr = cudaGetDeviceCount(&deviceCount); + devCntErr == cudaErrorNoDevice || devCntErr == cudaErrorStubLibrary) + { + (void)cudaGetLastError(); + deviceCount = 0; + } + std::shared_ptr globalStream; + if (deviceCount > 0) + { + stream.def_property_readonly_static( + "current", [](py::object) { return Current().sharedStream(); }, + "Get the current CUDA stream for this thread."); + + static priv::ExternalStream cudaDefaultStream(static_cast(nullptr)); + globalStream = std::make_shared(cudaDefaultStream); + StreamStack::Instance().push(*globalStream); + stream.attr("default") = globalStream; + } // Order from most specific to less specific ExportExternalStream(m); + ExportExternalStream(m); ExportExternalStream(m); ExportExternalStream(m); @@ -524,6 +860,11 @@ void Stream::Export(py::module &m) stream.def("__enter__", &Stream::activate, "Activate the CUDA stream as the current stream for this thread.") .def("__exit__", &Stream::deactivate, "Deactivate the CUDA stream as the current stream for this thread.") .def("sync", &Stream::sync, "Wait for all preceding CUDA calls in the current stream to complete.") + .def("wait_stream", &Stream::wait_stream, py::arg("other"), + "Insert a dependency on 'other' into this stream. All subsequent work enqueued on this stream " + "will wait until all work currently enqueued on 'other' has completed. " + "Calling wait_stream(self) is a no-op. " + "Cross-device usage is not supported and will raise a CUDA error.") .def("__int__", &Stream::pyhandle, "Cast the CUDA stream object to an integer handle.") .def("__repr__", &util::ToString, "Return a string representation of the CUDA stream object.") .def_property_readonly("handle", &Stream::pyhandle, "Get the integer handle for the CUDA stream object.") @@ -531,43 +872,7 @@ void Stream::Export(py::module &m) // Make sure all streams we've created are synced when script ends. // Also make cleanup hold the globalStream reference during script execution. - util::RegisterCleanup(m, - [globalStream]() - { - try - { - for (std::shared_ptr stream : Cache::Instance().fetchAll()) - { - stream->sync(); - } - globalStream->sync(); - Stream::SyncAuxStream(); - - // There should only be 1 stream in the stack, namely the - // global stream. - auto s = StreamStack::Instance().top(); - if (s != globalStream) - { - std::cerr << "Stream stack leak detected" << std::endl; - } - - // Make sure stream stack is empty - while (auto s = StreamStack::Instance().top()) - { - StreamStack::Instance().pop(); - } - - // Make sure the gc bag is also cleaned up *after* all streams are done, - // then when know all remaining items that need to be GC'd are in the bag. - Stream::ClearGCBag(); - } - catch (const std::exception &e) - { - //Do nothing here this can happen if someone closes the cuda context prior to exit. - std::cerr << "Warning CVCUDA cleanup may be incomplete due to: " << e.what() - << std::endl; - } - }); + util::RegisterCleanup(m, [globalStream]() { CleanupAtExit(globalStream); }); } } // namespace nvcvpy::priv diff --git a/python/mod_cvcuda/nvcv/Stream.hpp b/python/mod_cvcuda/nvcv/Stream.hpp index 7ebccbdda..c1bb23f30 100644 --- a/python/mod_cvcuda/nvcv/Stream.hpp +++ b/python/mod_cvcuda/nvcv/Stream.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -28,6 +28,7 @@ #include #include #include +#include #include #include @@ -38,6 +39,8 @@ class Resource; class IExternalStream { public: + virtual ~IExternalStream() = default; + virtual cudaStream_t handle() const = 0; virtual py::object wrappedObject() const = 0; }; @@ -53,25 +56,29 @@ class PYBIND11_EXPORT Stream : public CacheItem static std::shared_ptr Create(); - virtual ~Stream(); + ~Stream() override; - std::shared_ptr shared_from_this(); - std::shared_ptr shared_from_this() const; + std::shared_ptr sharedStream(); + std::shared_ptr sharedStream() const; void activate(); - void deactivate(py::object exc_type, py::object exc_value, py::object exc_tb); + void deactivate(py::object exc_type, py::object exc_value, py::object exc_tb) const; void holdResources(LockResources usedResources); + static void SynchronizeAndClearGCBag(); + int64_t GetSizeInBytes() const override; void sync(); + void wait_stream(std::shared_ptr other); cudaStream_t handle() const; + int deviceId() const; // Returns the cuda handle in python intptr_t pyhandle() const; - Stream(IExternalStream &extStream); + explicit Stream(IExternalStream &extStream); friend std::ostream &operator<<(std::ostream &out, const Stream &stream); @@ -79,45 +86,48 @@ class PYBIND11_EXPORT Stream : public CacheItem Stream(Stream &&) = delete; Stream(); - int64_t doComputeSizeInBytes(); + int64_t doComputeSizeInBytes() const; // Singleton access to the auxiliary CUDA stream class Key final : public IKey { private: - virtual size_t doGetHash() const override; - virtual bool doIsCompatible(const IKey &that) const override; + size_t doGetHash() const override; + bool doIsCompatible(const IKey &that) const override; }; - virtual const Key &key() const override + const Key &key() const override { - static Key key; - return key; + return m_key; } - void destroy(); + void destroy(); + cudaEvent_t getEvent(); - bool m_owns = false; - cudaStream_t m_handle = nullptr; - cudaEvent_t m_event = nullptr; - py::object m_wrappedObj; - int64_t m_size_inbytes = -1; + Key m_key; + bool m_owns = false; + cudaStream_t m_handle = nullptr; + std::unordered_map m_events; + std::shared_mutex m_eventMutex; + py::object m_wrappedObj; + int64_t m_size_inbytes = -1; - // TODO: these don't have to be static members, but simply defined + // REVISIT: these don't have to be static members, but simply defined // as local entities in Stream.cpp, thereby minimizing code coupling and // unnecessary rebuilds. - //singleton aux stream and protection. this a a bit overkill + //per-device aux streams and protection. this is a bit overkill //for now as python is single threaded, but it is a good practice - static std::mutex m_auxStreamMutex; - static std::atomic m_instanceCount; - static cudaStream_t m_auxStream; - - static void incrementInstanceCount(); - static int decrementInstanceCount(); - static cudaStream_t &GetAuxStream(); - static void SyncAuxStream(); + static std::shared_mutex m_auxStreamMutex; + static std::atomic m_instanceCount; + static std::unordered_map m_auxStreams; + + static void incrementInstanceCount(); + static int decrementInstanceCount(); + static cudaStream_t GetAuxStream(); + static void SyncAuxStream(); + static void CleanupAtExit(const std::shared_ptr &globalStream); // Adds the object to the garbage-collector's bag to delay its destruction // until it's safe to destroy it. diff --git a/python/mod_cvcuda/nvcv/StreamStack.cpp b/python/mod_cvcuda/nvcv/StreamStack.cpp index 3bb9ee0be..464adc88a 100644 --- a/python/mod_cvcuda/nvcv/StreamStack.cpp +++ b/python/mod_cvcuda/nvcv/StreamStack.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -24,7 +24,7 @@ namespace nvcvpy::priv { void StreamStack::push(Stream &stream) { std::unique_lock lk(m_mtx); - m_stack.push(stream.shared_from_this()); + m_stack.push(stream.sharedStream()); } void StreamStack::pop() diff --git a/python/mod_cvcuda/nvcv/Tensor.cpp b/python/mod_cvcuda/nvcv/Tensor.cpp index 44f99434c..828a2a84c 100644 --- a/python/mod_cvcuda/nvcv/Tensor.cpp +++ b/python/mod_cvcuda/nvcv/Tensor.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "Tensor.hpp" +#include "../NvtxRange.hpp" #include "DataType.hpp" #include "ExternalBuffer.hpp" #include "Image.hpp" @@ -31,6 +32,11 @@ #include #include +#include +#include +#include +#include + namespace nvcv { static size_t ComputeHash(const nvcv::TensorShape &shape) @@ -43,6 +49,16 @@ static size_t ComputeHash(const nvcv::TensorShape &shape) namespace nvcvpy::priv { +namespace { + +class TensorError : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + +} // namespace + std::shared_ptr Tensor::CreateForImageBatch(int numImages, const Size2D &size, nvcv::ImageFormat fmt, int rowalign) { @@ -73,7 +89,7 @@ std::shared_ptr Tensor::CreateFromReqs(const nvcv::Tensor::Requirements // None found? if (vcont.empty()) { - std::shared_ptr tensor(new Tensor(reqs)); + std::shared_ptr tensor(new Tensor(reqs)); // NOSONAR: constructor is private. Cache::Instance().add(*tensor); return tensor; } @@ -94,22 +110,22 @@ NVCVTensorData FillNVCVTensorData(const DLTensor &tensor, std::optional(ToDType(ToNVCVDataType(tensor.dtype))); + tensorData.dtype = static_cast(py::cast(ToDType(ToNVCVDataType(tensor.dtype)))); // layout ------------ if (layout) { - tensorData.layout = *layout; + tensorData.layout = static_cast(*layout); } // rank ------------ { - // TODO: Add 0D support + // REVISIT: Add 0D support int rank = tensor.ndim == 0 ? 1 : tensor.ndim; if (rank < 1 || rank > NVCV_TENSOR_MAX_RANK) { - throw std::invalid_argument(util::FormatString("Number of dimensions must be between 1 and %d, not %d", - NVCV_TENSOR_MAX_RANK, rank)); + throw std::invalid_argument(util::ConcatString("Number of dimensions must be between 1 and ", + NVCV_TENSOR_MAX_RANK, ", not ", rank)); } tensorData.rank = rank; } @@ -120,11 +136,11 @@ NVCVTensorData FillNVCVTensorData(const DLTensor &tensor, std::optional Tensor::Wrap(ExternalBuffer &buffer, std::optional(new Tensor(data, py::cast(buffer.shared_from_this()))); + auto tensor = std::shared_ptr( // NOSONAR: constructor is private. + new Tensor(data, py::cast(buffer.shared_from_this()))); + + // Seed the tensor's Resource state with the producer's CUDA stream so the + // first cvcuda op that reads this tensor inserts the necessary + // cross-stream wait. Skipped when the producer advertised + // `stream: None` / `stream: -1`, which means they've already synchronized. + if (!buffer.producerIsSynced() && buffer.producerStream() != nullptr) + { + int device = buffer.producerDevice(); + if (device < 0) + { + util::CheckThrow(cudaGetDevice(&device)); + } + tensor->seedLastStream(buffer.producerStream(), device); + } // Need to add wrappers to cache so that they don't get destroyed by // the cuda stream when they're last used, and python script isn't @@ -176,7 +207,7 @@ std::shared_ptr Tensor::WrapImage(Image &img) Tensor::Key key; Cache::Instance().removeAllNotInUseMatching(key); - auto tensor = std::shared_ptr(new Tensor(img)); + auto tensor = std::shared_ptr(new Tensor(img)); // NOSONAR: constructor is private. Cache::Instance().add(*tensor); return tensor; @@ -189,8 +220,9 @@ std::shared_ptr Tensor::ReshapeTensor(Tensor &tensor, Shape shape, std:: nvcv::Tensor tensor_impl = tensor.impl(); auto new_tensor_shape = CreateNVCVTensorShape(shape, layout ? *layout : tensor_impl.layout()); - nvcv::Tensor new_tensor_impl = tensor_impl.reshape(std::move(new_tensor_shape)); - auto new_tensor = std::shared_ptr(new Tensor(std::move(new_tensor_impl))); + nvcv::Tensor new_tensor_impl = tensor_impl.reshape(new_tensor_shape); + auto new_tensor + = std::shared_ptr(new Tensor(std::move(new_tensor_impl))); // NOSONAR: constructor is private. // Need to add wrappers to cache so that they don't get destroyed by // the cuda stream when they're last used, and python script isn't @@ -213,7 +245,6 @@ Tensor::Tensor(const nvcv::Tensor::Requirements &reqs) Tensor::Tensor(const nvcv::TensorData &data, py::object wrappedObject) : m_impl{nvcv::TensorWrapData(data)} - , m_key{} , m_size_inbytes{doComputeSizeInBytes(nvcv::Tensor::Requirements())} , m_wrappedObject(wrappedObject) { @@ -221,7 +252,6 @@ Tensor::Tensor(const nvcv::TensorData &data, py::object wrappedObject) Tensor::Tensor(Image &img) : m_impl{nvcv::TensorWrapImage(img.impl())} - , m_key{} , m_size_inbytes{doComputeSizeInBytes(nvcv::Tensor::Requirements())} , m_wrappedObject(py::cast(img)) { @@ -229,12 +259,11 @@ Tensor::Tensor(Image &img) Tensor::Tensor(nvcv::Tensor &&tensor) : m_impl{std::move(tensor)} - , m_key{} , m_size_inbytes{doComputeSizeInBytes(nvcv::Tensor::Requirements())} { } -int64_t Tensor::doComputeSizeInBytes(const nvcv::Tensor::Requirements &reqs) +int64_t Tensor::doComputeSizeInBytes(const nvcv::Tensor::Requirements &reqs) const { int64_t size_inbytes; util::CheckThrow(nvcvMemRequirementsCalcTotalSizeBytes(&(reqs.mem.cudaMem), &size_inbytes)); @@ -249,16 +278,6 @@ int64_t Tensor::GetSizeInBytes() const return m_size_inbytes; } -std::shared_ptr Tensor::shared_from_this() -{ - return std::static_pointer_cast(Container::shared_from_this()); -} - -std::shared_ptr Tensor::shared_from_this() const -{ - return std::static_pointer_cast(Container::shared_from_this()); -} - nvcv::Tensor &Tensor::impl() { return m_impl; @@ -298,12 +317,13 @@ int Tensor::rank() const } Tensor::Key::Key(const nvcv::Tensor::Requirements &reqs) - : Key(nvcv::TensorShape(reqs.shape, reqs.rank, reqs.layout), static_cast(reqs.dtype)) + : Key(nvcv::TensorShape(reqs.shape, reqs.rank, nvcv::TensorLayout{reqs.layout}), + static_cast(reqs.dtype)) { } Tensor::Key::Key(const nvcv::TensorShape &shape, nvcv::DataType dtype) - : m_shape(std::move(shape)) + : m_shape(shape) , m_dtype(dtype) , m_wrapper(false) { @@ -324,7 +344,7 @@ size_t Tensor::Key::doGetHash() const bool Tensor::Key::doIsCompatible(const IKey &that_) const { - const Key &that = static_cast(that_); + const auto &that = static_cast(that_); // Wrapper key's all compare equal, are they can't be used // and whenever we query the cache for wrappers, we really @@ -348,27 +368,36 @@ auto Tensor::key() const -> const Key & return m_key; } -static py::object ToPython(const nvcv::TensorData &tensorData, py::object owner) +static py::object ToPython(const nvcv::TensorData &tensorData, py::object owner, cudaStream_t exportStream, + bool setExportStream) { py::object out; auto stridedData = tensorData.cast(); if (!stridedData) { - throw std::runtime_error("Only tensors with pitch-linear data can be exported"); + throw TensorError("Only tensors with pitch-linear data can be exported"); } DLPackTensor dlTensor(*stridedData); - return ExternalBuffer::Create(std::move(dlTensor), owner); + return ExternalBuffer::Create(std::move(dlTensor), owner, exportStream, setExportStream); } py::object Tensor::cuda() const { nvcv::TensorData tensorData = m_impl.exportData(); + // Advertise the stream the tensor's data was last written on via the + // CAI `stream` field so downstream consumers (cupy/torch) can sync. + // If the tensor has never been used by a cvcuda op, getLastStreamHandle() + // returns 0 -- in which case we fall back to the default "stream: 1" + // (legacy default) by not populating the export stream. + cudaStream_t lastStream = this->getLastStreamHandle(); + bool setStream = lastStream != nullptr; + // Note: we can't cache the returned ExternalBuffer because it is holding // a reference to us. Doing so would lead to mem leaks. - return ToPython(tensorData, py::cast(this->shared_from_this())); + return ToPython(tensorData, py::cast(SharedContainerFrom(*this)), lastStream, setStream); } std::ostream &operator<<(std::ostream &out, const Tensor &tensor) @@ -384,7 +413,7 @@ static std::string TensorLayoutToString(const nvcv::TensorLayout &layout) std::string s = ss.str(); auto p = s.rfind('_'); - if (p != s.npos) + if (p != std::string::npos) { return s.substr(p + 1); } @@ -394,43 +423,132 @@ static std::string TensorLayoutToString(const nvcv::TensorLayout &layout) } } -void Tensor::Export(py::module &m) +static std::string_view TensorLayoutLabels(const nvcv::TensorLayout &layout) { - using namespace py::literals; + return std::string_view(layout.m_layout.data, layout.rank()); +} - py::class_(m, "TensorLayout") - .def(py::init()) -#define NVCV_DETAIL_DEF_TLAYOUT(LAYOUT) .def_readonly_static(#LAYOUT, &nvcv::TENSOR_##LAYOUT) -#include -#undef NVCV_DETAIL_DEF_TLAYOUT - .def(py::self == py::self, "Check if two TensorLayout objects are equal.") - .def(py::self != py::self, "Check if two TensorLayout objects are not equal.") +// Named-layout Python objects, keyed by their labels and shared for the +// process lifetime (never destroyed: py::object statics must not outlive the +// interpreter). Tensor.layout returns these instead of constructing a new +// Python object per access, so repeated reads yield the identical object and +// dict lookups keyed by the class constants hit CPython's identity shortcut. +static std::unordered_map &InternedTensorLayouts() +{ + static auto *interned = new std::unordered_map; // NOSONAR: deliberately immortal + return *interned; +} + +static py::object PyTensorLayout(const std::optional &layout) +{ + if (!layout) + { + return py::none(); + } + auto &interned = InternedTensorLayouts(); + if (auto it = interned.find(std::string(TensorLayoutLabels(*layout))); it != interned.end()) + { + return it->second; + } + return py::cast(*layout); +} + +// Hash over the raw dimension labels so it stays consistent with __eq__ +// (which compares label data): equal layouts always hash equal, whether built +// from a named constant or a string. std::hash is required +// to match std::hash for equal characters. +static Py_hash_t TensorLayoutHashValue(const nvcv::TensorLayout &l) +{ + auto h = static_cast(std::hash{}(TensorLayoutLabels(l))); + return h == -1 ? -2 : h; // CPython reserves -1 for errors +} + +// Installed directly as tp_hash: dict/set operations then skip the Python +// method-dispatch of a def("__hash__"), which costs several times the hash +// itself on this hot path. +static Py_hash_t TensorLayoutTpHash(PyObject *self) +{ + try + { + return TensorLayoutHashValue(py::cast(py::handle(self))); + } + catch (const std::exception &e) // NOSONAR: tp_hash is a C slot; no C++ exception may reach CPython + { + PyErr_SetString(PyExc_TypeError, e.what()); + return -1; + } +} + +void ExportTensorLayout(py::module &m) +{ + auto cls = py::class_(m, "TensorLayout"); + cls.def(py::init()) + .def( + "__eq__", [](const nvcv::TensorLayout &a, const nvcv::TensorLayout &b) { return a == b; }, + py::is_operator(), "Check if two TensorLayout objects are equal.") + .def( + "__ne__", [](const nvcv::TensorLayout &a, const nvcv::TensorLayout &b) { return a != b; }, + py::is_operator(), "Check if two TensorLayout objects are not equal.") + // Kept alongside the tp_hash slot so TensorLayout.__hash__ resolves to + // the same value; without a def, pybind11 nulls __hash__ once __eq__ + // is defined, leaving TensorLayout unhashable. + .def( + "__hash__", [](const nvcv::TensorLayout &l) { return TensorLayoutHashValue(l); }, + "Return a value-based hash so TensorLayout can be used as a dict key or set member.") .def("__repr__", &TensorLayoutToString, "Return the string representation of the TensorLayout object."); + // The class constants are the interned instances themselves (not + // per-access getters), so `TensorLayout.NHWC is tensor.layout` holds. + auto intern = [&cls](const char *name, const nvcv::TensorLayout &layout) + { + py::object obj = py::cast(layout); + InternedTensorLayouts().try_emplace(std::string(TensorLayoutLabels(layout)), obj); + cls.attr(name) = obj; + }; + cls.attr("NONE") = py::cast(nvcv::TENSOR_NONE); +#define NVCV_DETAIL_DEF_TLAYOUT(LAYOUT) intern(#LAYOUT, nvcv::TENSOR_##LAYOUT); +#include // NOSONAR: this include expands TensorLayout constants inside the binding chain. +#undef NVCV_DETAIL_DEF_TLAYOUT + + // Override last: pybind's def("__hash__") above set tp_hash to CPython's + // slot dispatcher; replace it with the direct C implementation. + auto *tp = reinterpret_cast(cls.ptr()); + tp->tp_hash = &TensorLayoutTpHash; + PyType_Modified(tp); + py::implicitly_convertible(); +} + +void Tensor::Export(py::module &m) +{ + using namespace py::literals; py::class_, Container>(m, "Tensor", "Tensor") .def(py::init(&Tensor::CreateForImageBatch), "nimages"_a, "imgsize"_a, "format"_a, "rowalign"_a = 0, "Create a Tensor object for an ImageBatch.") .def(py::init(&Tensor::Create), "shape"_a, "dtype"_a, "layout"_a = std::nullopt, "rowalign"_a = 0, "Create a Tensor object with the given shape, data type and layout.") - .def_property_readonly("layout", &Tensor::layout, "The TensorLayout of the Tensor.") + .def_property_readonly( + "layout", [](const Tensor &self) { return PyTensorLayout(self.layout()); }, + "The TensorLayout of the Tensor.") .def_property_readonly("shape", &Tensor::shape, "The shape of the Tensor.") .def_property_readonly("dtype", &Tensor::dtype, "The data type of the Tensor.") // numpy and others use ndim, let's be consistent with them in python. // It's not a requirement to be consistent between NVCV Python and C/C++. // Each language use whatever is appropriate (and expected) in their environment. .def_property_readonly("ndim", &Tensor::rank, "The number of dimensions of the Tensor.") - .def("cuda", &Tensor::cuda, "Reference to the Tensor on the CUDA device.") + .def("cuda", ::cvcudapy::NvtxTrace("cvcuda.Tensor.cuda", &Tensor::cuda), + "Reference to the Tensor on the CUDA device.") .def("reshape", &Tensor::Reshape, "shape"_a, "layout"_a = std::nullopt, "Produces a tensor pointing to the same data but with a new shape and layout.") .def("__repr__", &util::ToString, "Return the string representation of the Tensor object."); - m.def("as_tensor", &Tensor::Wrap, "buffer"_a, "layout"_a = std::nullopt, + m.def("as_tensor", ::cvcudapy::NvtxTrace("cvcuda.as_tensor", &Tensor::Wrap), "buffer"_a, "layout"_a = std::nullopt, "Wrap an existing buffer into a Tensor object with the given layout."); - m.def("as_tensor", &Tensor::WrapImage, "image"_a, "Wrap an existing image into a Tensor object."); - m.def("reshape", &Tensor::ReshapeTensor, "tensor"_a, "shape"_a, "layout"_a = std::nullopt, - "Produces a tensor pointing to the same data but with a new shape and layout."); + m.def("as_tensor", ::cvcudapy::NvtxTrace("cvcuda.as_tensor", &Tensor::WrapImage), "image"_a, + "Wrap an existing image into a Tensor object."); + m.def("reshape", ::cvcudapy::NvtxTrace("cvcuda.reshape", &Tensor::ReshapeTensor), "tensor"_a, "shape"_a, + "layout"_a = std::nullopt, "Produces a tensor pointing to the same data but with a new shape and layout."); } } // namespace nvcvpy::priv diff --git a/python/mod_cvcuda/nvcv/Tensor.hpp b/python/mod_cvcuda/nvcv/Tensor.hpp index 3751fbe99..1e7463261 100644 --- a/python/mod_cvcuda/nvcv/Tensor.hpp +++ b/python/mod_cvcuda/nvcv/Tensor.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -33,6 +33,11 @@ namespace py = pybind11; class ExternalBuffer; class Image; +// Register nvcv::TensorLayout. Split out of Tensor::Export so it can run +// early — Image::cpu/cuda take a TensorLayout parameter and pybind11 needs +// the type registered before Image::Export binds those methods. +void ExportTensorLayout(py::module &m); + class Tensor : public Container { public: @@ -51,9 +56,6 @@ class Tensor : public Container std::shared_ptr Reshape(Shape shape, std::optional layout); - std::shared_ptr shared_from_this(); - std::shared_ptr shared_from_this() const; - std::optional layout() const; Shape shape() const; nvcv::DataType dtype() const; @@ -78,23 +80,23 @@ class Tensor : public Container nvcv::DataType m_dtype; bool m_wrapper; - virtual size_t doGetHash() const override; - virtual bool doIsCompatible(const IKey &that) const override; + size_t doGetHash() const override; + bool doIsCompatible(const IKey &that) const override; }; - virtual const Key &key() const override; + const Key &key() const override; py::object cuda() const; int64_t GetSizeInBytes() const override; private: - Tensor(const nvcv::Tensor::Requirements &reqs); + explicit Tensor(const nvcv::Tensor::Requirements &reqs); Tensor(const nvcv::TensorData &data, py::object wrappedObject); - Tensor(Image &img); - Tensor(nvcv::Tensor &&tensor); + explicit Tensor(Image &img); + explicit Tensor(nvcv::Tensor &&tensor); - int64_t doComputeSizeInBytes(const nvcv::Tensor::Requirements &reqs); + int64_t doComputeSizeInBytes(const nvcv::Tensor::Requirements &reqs) const; nvcv::Tensor m_impl; // must come before m_key Key m_key; diff --git a/python/mod_cvcuda/nvcv/TensorBatch.cpp b/python/mod_cvcuda/nvcv/TensorBatch.cpp index edeefbb5f..6247a3b73 100644 --- a/python/mod_cvcuda/nvcv/TensorBatch.cpp +++ b/python/mod_cvcuda/nvcv/TensorBatch.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "TensorBatch.hpp" +#include "../NvtxRange.hpp" #include "CastUtils.hpp" #include "DataType.hpp" #include "ExternalBuffer.hpp" @@ -25,8 +26,20 @@ #include #include +#include + namespace nvcvpy::priv { +namespace { + +class TensorBatchError : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + +} // namespace + size_t TensorBatch::Key::doGetHash() const { using util::ComputeHash; @@ -46,7 +59,7 @@ std::shared_ptr TensorBatch::Create(int capacity) // None found? if (vcont.empty()) { - std::shared_ptr batch(new TensorBatch(capacity)); + std::shared_ptr batch(new TensorBatch(capacity)); // NOSONAR: constructor is private. Cache::Instance().add(*batch); return batch; } @@ -69,12 +82,12 @@ std::shared_ptr TensorBatch::WrapExternalBufferVector(std::vector

buffer = cast_py_object_as(obj); if (!buffer) { - throw std::runtime_error("Input buffer doesn't provide cuda_array_interface or DLPack interfaces."); + throw TensorBatchError("Input buffer doesn't provide cuda_array_interface or DLPack interfaces."); } auto tensor = Tensor::Wrap(*buffer, layout); list.push_back(tensor); } - auto batch = Create(buffers.size()); + auto batch = Create(static_cast(buffers.size())); batch->pushBackMany(list); return batch; } @@ -87,7 +100,7 @@ TensorBatch::TensorBatch(int capacity) m_list.reserve(capacity); } -int64_t TensorBatch::doComputeSizeInBytes(const NVCVTensorBatchRequirements &reqs) +int64_t TensorBatch::doComputeSizeInBytes(const NVCVTensorBatchRequirements &reqs) const { int64_t size_inbytes; util::CheckThrow(nvcvMemRequirementsCalcTotalSizeBytes(&(reqs.mem.cudaMem), &size_inbytes)); @@ -157,14 +170,14 @@ std::optional TensorBatch::layout() const void TensorBatch::pushBack(Tensor &tensor) { m_impl.pushBack(tensor.impl()); - m_list.push_back(tensor.shared_from_this()); + m_list.push_back(SharedContainerFrom(tensor)); } void TensorBatch::pushBackMany(std::vector> &tensorList) { std::vector nvcvTensors; nvcvTensors.reserve(tensorList.size()); - for (auto &tensor : tensorList) + for (const auto &tensor : tensorList) { m_list.push_back(tensor); if (tensor) @@ -191,12 +204,12 @@ std::shared_ptr TensorBatch::at(int64_t idx) const { if (idx < 0) { - throw std::runtime_error("Invalid index: " + std::to_string(idx)); + throw TensorBatchError("Invalid index: " + std::to_string(idx)); } else if (idx >= static_cast(m_list.size())) { - throw std::runtime_error("Cannot get tensor at index " + std::to_string(idx) + ". Batch has only " - + std::to_string(m_list.size()) + " elements."); + throw TensorBatchError("Cannot get tensor at index " + std::to_string(idx) + ". Batch has only " + + std::to_string(m_list.size()) + " elements."); } return m_list[idx]; } @@ -205,12 +218,12 @@ void TensorBatch::set_at(int64_t idx, std::shared_ptr tensor) { if (idx < 0) { - throw std::runtime_error("Invalid index: " + std::to_string(idx)); + throw TensorBatchError("Invalid index: " + std::to_string(idx)); } else if (idx >= static_cast(m_list.size())) { - throw std::runtime_error("Cannot set tensor at index " + std::to_string(idx) + ". Batch has only " - + std::to_string(m_list.size()) + " elements."); + throw TensorBatchError("Cannot set tensor at index " + std::to_string(idx) + ". Batch has only " + + std::to_string(m_list.size()) + " elements."); } m_impl.setTensor(static_cast(idx), tensor->impl()); m_list[idx] = tensor; @@ -226,16 +239,6 @@ auto TensorBatch::end() const -> TensorList::const_iterator return m_list.end(); } -std::shared_ptr TensorBatch::shared_from_this() -{ - return std::static_pointer_cast(Container::shared_from_this()); -} - -std::shared_ptr TensorBatch::shared_from_this() const -{ - return std::static_pointer_cast(Container::shared_from_this()); -} - void TensorBatch::Export(py::module &m) { using namespace py::literals; @@ -269,8 +272,8 @@ void TensorBatch::Export(py::module &m) "Remove one or more images from the end of the TensorBatch.") .def("clear", &TensorBatch::clear, "Remove all images from the TensorBatch."); - m.def("as_tensors", &TensorBatch::WrapExternalBufferVector, "buffers"_a = std::vector{}, - "layout"_a = std::nullopt, py::keep_alive<0, 1>(), + m.def("as_tensors", ::cvcudapy::NvtxTrace("cvcuda.as_tensors", &TensorBatch::WrapExternalBufferVector), + "buffers"_a = std::vector{}, "layout"_a = std::nullopt, py::keep_alive<0, 1>(), "Wrap a list of external buffers as a batch of tensors, and tie the buffers lifetime to it"); } diff --git a/python/mod_cvcuda/nvcv/TensorBatch.hpp b/python/mod_cvcuda/nvcv/TensorBatch.hpp index acc7c4d0c..4069fd169 100644 --- a/python/mod_cvcuda/nvcv/TensorBatch.hpp +++ b/python/mod_cvcuda/nvcv/TensorBatch.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -40,9 +40,6 @@ class TensorBatch : public Container static std::shared_ptr WrapExternalBufferVector(std::vector buffers, std::optional layout); - std::shared_ptr shared_from_this(); - std::shared_ptr shared_from_this() const; - const nvcv::TensorBatch &impl() const; nvcv::TensorBatch &impl(); @@ -69,7 +66,7 @@ class TensorBatch : public Container class Key final : public IKey { public: - Key(int capacity) + explicit Key(int capacity) : m_capacity(capacity) { } @@ -77,19 +74,19 @@ class TensorBatch : public Container private: int m_capacity; - virtual size_t doGetHash() const override; - virtual bool doIsCompatible(const IKey &that) const override; + size_t doGetHash() const override; + bool doIsCompatible(const IKey &that) const override; }; - virtual const Key &key() const override + const Key &key() const override { return m_key; } private: - TensorBatch(int capacity); + explicit TensorBatch(int capacity); - int64_t doComputeSizeInBytes(const NVCVTensorBatchRequirements &reqs); + int64_t doComputeSizeInBytes(const NVCVTensorBatchRequirements &reqs) const; Key m_key; nvcv::TensorBatch m_impl; diff --git a/python/mod_cvcuda/operators/OpAdaptiveThreshold.cpp b/python/mod_cvcuda/operators/OpAdaptiveThreshold.cpp index d8d2bc8f9..148a4f4e0 100644 --- a/python/mod_cvcuda/operators/OpAdaptiveThreshold.cpp +++ b/python/mod_cvcuda/operators/OpAdaptiveThreshold.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,6 +16,7 @@ */ #include "Operators.hpp" +#include "VarShapeUtils.hpp" #include #include @@ -45,8 +46,13 @@ Tensor AdaptiveThresholdInto(Tensor &output, Tensor &input, double max_value, NV guard.add(LockMode::LOCK_MODE_WRITE, {output}); guard.add(LockMode::LOCK_MODE_READWRITE, {*adaptiveThreshold}); - adaptiveThreshold->submit(pstream->cudaHandle(), input, output, max_value, adaptive_method, threshold_type, - block_size, c); + guard.run( + [&adaptiveThreshold, &pstream, &c, &input, &output, &max_value, &adaptive_method, &threshold_type, + &block_size]() + { + adaptiveThreshold->submit(pstream->cudaHandle(), input, output, max_value, adaptive_method, threshold_type, + block_size, c); + }); return output; } @@ -76,8 +82,13 @@ ImageBatchVarShape AdaptiveThresholdVarShapeInto(ImageBatchVarShape &output, Ima guard.add(LockMode::LOCK_MODE_WRITE, {output}); guard.add(LockMode::LOCK_MODE_READWRITE, {*adaptiveThreshold}); - adaptiveThreshold->submit(pstream->cudaHandle(), input, output, max_value, adaptive_method, threshold_type, - block_size, c); + guard.run( + [&adaptiveThreshold, &pstream, &c, &input, &output, &max_value, &adaptive_method, &threshold_type, + &block_size]() + { + adaptiveThreshold->submit(pstream->cudaHandle(), input, output, max_value, adaptive_method, threshold_type, + block_size, c); + }); return output; } @@ -87,12 +98,7 @@ ImageBatchVarShape AdaptiveThresholdVarShape(ImageBatchVarShape &input, Tensor & NVCVThresholdType threshold_type, int32_t max_block_size, Tensor &block_size, Tensor &c, std::optional pstream) { - ImageBatchVarShape output = ImageBatchVarShape::Create(input.capacity()); - - for (int i = 0; i < input.numImages(); ++i) - { - output.pushBack(Image::Create(input[i].size(), input[i].format())); - } + ImageBatchVarShape output = CreateSameShapeImageBatch(input); return AdaptiveThresholdVarShapeInto(output, input, max_value, adaptive_method, threshold_type, max_block_size, block_size, c, pstream); @@ -104,20 +110,11 @@ void ExportOpAdaptiveThreshold(py::module &m) { using namespace pybind11::literals; - py::options options; - options.disable_function_signatures(); - - m.def("adaptivethreshold", &AdaptiveThreshold, "src"_a, "max_value"_a, + m.def("adaptivethreshold", NvtxTrace("cvcuda.adaptivethreshold", &AdaptiveThreshold), "src"_a, "max_value"_a, "adaptive_method"_a = NVCV_ADAPTIVE_THRESH_MEAN_C, "threshold_type"_a = NVCV_THRESH_BINARY, "block_size"_a, "c"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - cvcuda.adaptivethreshold(src: cvcuda.Tensor, max_value: float, adaptive_method: cvcuda.AdaptiveThresholdType = cvcuda.AdaptiveThresholdType.MEAN_C, - threshold_type: cvcuda.ThresholdType = cvcuda.ThresholdType.BINARY, block_size: int, c: float, stream: Optional[cvcuda.Stream] = None) -> cvcuda.Tensor - Executes the adaptive threshold operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the AdaptiveThreshold operator - for more details and usage examples. Args: src (cvcuda.Tensor): Input tensor containing one or more images. @@ -131,23 +128,13 @@ void ExportOpAdaptiveThreshold(py::module &m) Returns: cvcuda.Tensor: The output image. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("adaptivethreshold_into", &AdaptiveThresholdInto, "dst"_a, "src"_a, "max_value"_a, - "adaptive_method"_a = NVCV_ADAPTIVE_THRESH_MEAN_C, "threshold_type"_a = NVCV_THRESH_BINARY, "block_size"_a, - "c"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - cvcuda.adaptivethreshold_into(dst: cvcuda.Tensor, src: cvcuda.Tensor, max_value: float, adaptive_method: cvcuda.AdaptiveThresholdType = < - cvcuda.AdaptiveThresholdType.MEAN_C >, threshold_type: cvcuda.ThresholdType = cvcuda.ThresholdType.BINARY, - block_size: int, c: float, stream: Optional[cvcuda.Stream] = None) + m.def("adaptivethreshold_into", NvtxTrace("cvcuda.adaptivethreshold_into", &AdaptiveThresholdInto), "dst"_a, + "src"_a, "max_value"_a, "adaptive_method"_a = NVCV_ADAPTIVE_THRESH_MEAN_C, + "threshold_type"_a = NVCV_THRESH_BINARY, "block_size"_a, "c"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( + Executes the adaptive threshold operation on the given cuda stream. - Executes the adaptive threshold operation on the given cuda stream. - - See also: - Refer to the CV-CUDA C API reference for the AdaptiveThreshold operator - for more details and usage examples. Args: dst (cvcuda.Tensor): Output tensor to store the result of the operation. @@ -160,23 +147,14 @@ void ExportOpAdaptiveThreshold(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.Tensor: The output tensor (same as dst). )pbdoc"); - m.def("adaptivethreshold", &AdaptiveThresholdVarShape, "src"_a, "max_value"_a, - "adaptive_method"_a = NVCV_ADAPTIVE_THRESH_MEAN_C, "threshold_type"_a = NVCV_THRESH_BINARY, + m.def("adaptivethreshold", NvtxTrace("cvcuda.adaptivethreshold", &AdaptiveThresholdVarShape), "src"_a, + "max_value"_a, "adaptive_method"_a = NVCV_ADAPTIVE_THRESH_MEAN_C, "threshold_type"_a = NVCV_THRESH_BINARY, "max_block_size"_a, "block_size"_a, "c"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - cvcuda.adaptivethreshold(src: cvcuda.ImageBatchVarShape, max_value: cvcuda.Tensor, adaptive_method: cvcuda.AdaptiveThresholdType = cvcuda.AdaptiveThresholdType.MEAN_C, threshold_type: cvcuda.ThresholdType = cvcuda.ThresholdType.BINARY, block_size: int, c: float, stream: Optional[cvcuda.Stream] = None) -> cvcuda.ImageBatchVarShape - Executes the adaptive threshold operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the AdaptiveThreshold operator - for more details and usage examples. Args: src (cvcuda.ImageBatchVarShape): Input image batch containing the result of the operation. @@ -191,23 +169,14 @@ void ExportOpAdaptiveThreshold(py::module &m) Returns: cvcuda.ImageBatchVarShape: The output image batch. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("adaptivethreshold_into", &AdaptiveThresholdVarShapeInto, "dst"_a, "src"_a, "max_value"_a, - "adaptive_method"_a = NVCV_ADAPTIVE_THRESH_MEAN_C, "threshold_type"_a = NVCV_THRESH_BINARY, - "max_block_size"_a, "block_size"_a, "c"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - cvcuda.adaptivethreshold_into(dst: cvcuda.ImageBatchVarShape, src: cvcuda.ImageBatchVarShape, max_value: cvcuda.Tensor, - adaptive_method: cvcuda.AdaptiveThresholdType = cvcuda.AdaptiveThresholdType.MEAN_C, threshold_type: cvcuda.ThresholdType = cvcuda.ThresholdType.BINARY, - block_size: int, c: float, stream: Optional[cvcuda.Stream] = None) - + m.def("adaptivethreshold_into", NvtxTrace("cvcuda.adaptivethreshold_into", &AdaptiveThresholdVarShapeInto), "dst"_a, + "src"_a, "max_value"_a, "adaptive_method"_a = NVCV_ADAPTIVE_THRESH_MEAN_C, + "threshold_type"_a = NVCV_THRESH_BINARY, "max_block_size"_a, "block_size"_a, "c"_a, py::kw_only(), + "stream"_a = nullptr, R"pbdoc( Executes the adaptive threshold operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the AdaptiveThreshold operator - for more details and usage examples. Args: dst (cvcuda.ImageBatchVarShape): Output image batch containing the result of the operation. @@ -221,11 +190,7 @@ void ExportOpAdaptiveThreshold(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.ImageBatchVarShape: The output image batch (same as dst). )pbdoc"); } diff --git a/python/mod_cvcuda/operators/OpAdjustContrast.cpp b/python/mod_cvcuda/operators/OpAdjustContrast.cpp new file mode 100644 index 000000000..8235db90b --- /dev/null +++ b/python/mod_cvcuda/operators/OpAdjustContrast.cpp @@ -0,0 +1,148 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "UnaryElementwiseOp.hpp" + +#include +#include + +namespace cvcudapy { + +namespace { +// Thin Python-facing wrappers; the create/guard/submit body lives in UnaryElementwiseOp.hpp, with +// the contrast factor forwarded as the trailing submit() parameter. +Tensor AdjustContrastInto(Tensor &output, Tensor &input, double contrast_factor, std::optional pstream) +{ + return UnaryElementwiseInto(output, input, pstream, contrast_factor); +} + +Tensor AdjustContrast(Tensor &input, double contrast_factor, std::optional pstream) +{ + return UnaryElementwiseTensor(input, pstream, contrast_factor); +} + +ImageBatchVarShape AdjustContrastVarShapeInto(ImageBatchVarShape &output, ImageBatchVarShape &input, + double contrast_factor, std::optional pstream) +{ + return UnaryElementwiseInto(output, input, pstream, contrast_factor); +} + +ImageBatchVarShape AdjustContrastVarShape(ImageBatchVarShape &input, double contrast_factor, + std::optional pstream) +{ + return UnaryElementwiseVarShape(input, pstream, contrast_factor); +} + +} // namespace + +void ExportOpAdjustContrast(py::module &m) +{ + using namespace pybind11::literals; + + m.def("adjust_contrast", NvtxTrace("cvcuda.adjust_contrast", &AdjustContrast), "src"_a, "contrast_factor"_a, + py::kw_only(), "stream"_a = nullptr, + R"pbdoc( + + Executes the AdjustContrast operation on the given cuda stream. + + Blends each image toward its grayscale mean by a scalar factor: + ``out = clamp(contrast_factor * in + (1 - contrast_factor) * mean, 0, bound)``, where ``mean`` + is the per-image grayscale mean (BT.601 luma ``0.2989 R + 0.587 G + 0.114 B``) and ``bound`` + is 1.0 for float32 and 255 for uint8. Mirrors + torchvision.transforms.v2.functional.adjust_contrast. + + See also: + Refer to the CV-CUDA C API reference for the AdjustContrast operator for more details and + usage examples. + + Args: + src (cvcuda.Tensor): Input tensor containing one or more images (1 or 3 channels). + contrast_factor (float): Non-negative contrast multiplier (0 = flat gray, 1 = unchanged). + stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + cvcuda.Tensor: The output tensor (same shape, dtype, and layout as src). + + Caution: + Restrictions to several arguments may apply. Check the C API references of the CV-CUDA + operator. + )pbdoc"); + + m.def("adjust_contrast_into", NvtxTrace("cvcuda.adjust_contrast_into", &AdjustContrastInto), "dst"_a, "src"_a, + "contrast_factor"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( + + Executes the AdjustContrast operation on the given cuda stream. + + See also: + Refer to the CV-CUDA C API reference for the AdjustContrast operator for more details and + usage examples. + + Args: + dst (cvcuda.Tensor): Output tensor to store the result of the operation. + src (cvcuda.Tensor): Input tensor containing one or more images (1 or 3 channels). + contrast_factor (float): Non-negative contrast multiplier (0 = flat gray, 1 = unchanged). + stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + cvcuda.Tensor: The output tensor (same as dst). + + Caution: + Restrictions to several arguments may apply. Check the C API references of the CV-CUDA + operator. + )pbdoc"); + + m.def("adjust_contrast", NvtxTrace("cvcuda.adjust_contrast", &AdjustContrastVarShape), "src"_a, "contrast_factor"_a, + py::kw_only(), "stream"_a = nullptr, R"pbdoc( + + Executes the AdjustContrast operation on the given cuda stream. + + Args: + src (cvcuda.ImageBatchVarShape): Input image batch containing one- or three-component images. + Three-component inputs are interpreted as RGB, with RGB and BGR storage swizzles honored. + contrast_factor (float): Non-negative contrast multiplier (0 = flat gray, 1 = unchanged). + stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + cvcuda.ImageBatchVarShape: The output image batch (same formats and sizes as src). + + Caution: + Restrictions to several arguments may apply. Check the C API references of the CV-CUDA + operator. + )pbdoc"); + + m.def("adjust_contrast_into", NvtxTrace("cvcuda.adjust_contrast_into", &AdjustContrastVarShapeInto), "dst"_a, + "src"_a, "contrast_factor"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( + + Executes the AdjustContrast operation on the given cuda stream. + + Args: + dst (cvcuda.ImageBatchVarShape): Output image batch to store the result of the operation. + src (cvcuda.ImageBatchVarShape): Input image batch containing one- or three-component images. + Three-component inputs are interpreted as RGB, with RGB and BGR storage swizzles honored. + contrast_factor (float): Non-negative contrast multiplier (0 = flat gray, 1 = unchanged). + stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + cvcuda.ImageBatchVarShape: The output image batch (same as dst). + + Caution: + Restrictions to several arguments may apply. Check the C API references of the CV-CUDA + operator. + )pbdoc"); +} + +} // namespace cvcudapy diff --git a/python/mod_cvcuda/operators/OpAdjustHue.cpp b/python/mod_cvcuda/operators/OpAdjustHue.cpp new file mode 100644 index 000000000..523798c38 --- /dev/null +++ b/python/mod_cvcuda/operators/OpAdjustHue.cpp @@ -0,0 +1,143 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "UnaryElementwiseOp.hpp" + +#include +#include + +namespace cvcudapy { + +namespace { +// Thin Python-facing wrappers; the create/guard/submit body lives in UnaryElementwiseOp.hpp, with +// the hue factor forwarded as the trailing submit() parameter. +Tensor AdjustHueInto(Tensor &output, Tensor &input, double hue, std::optional pstream) +{ + return UnaryElementwiseInto(output, input, pstream, hue); +} + +Tensor AdjustHue(Tensor &input, double hue, std::optional pstream) +{ + return UnaryElementwiseTensor(input, pstream, hue); +} + +ImageBatchVarShape AdjustHueVarShapeInto(ImageBatchVarShape &output, ImageBatchVarShape &input, double hue, + std::optional pstream) +{ + return UnaryElementwiseInto(output, input, pstream, hue); +} + +ImageBatchVarShape AdjustHueVarShape(ImageBatchVarShape &input, double hue, std::optional pstream) +{ + return UnaryElementwiseVarShape(input, pstream, hue); +} + +} // namespace + +void ExportOpAdjustHue(py::module &m) +{ + using namespace pybind11::literals; + + m.def("adjust_hue", NvtxTrace("cvcuda.adjust_hue", &AdjustHue), "src"_a, "hue"_a, py::kw_only(), + "stream"_a = nullptr, R"pbdoc( + + Executes the Adjust Hue operation on the given cuda stream. + + Rotates the hue of each RGB image in HSV space: the image is converted to HSV, the hue channel + is shifted by ``hue`` (H normalized to [0, 1)), and converted back to RGB. ``hue`` = 0 leaves + the image unchanged; +/-0.5 is a full 180-degree hue rotation. Single-channel images are + returned unchanged. Mirrors torchvision.transforms.v2.functional.adjust_hue. + + See also: + Refer to the CV-CUDA C API reference for the Adjust Hue operator for more details and + usage examples. + + Args: + src (cvcuda.Tensor): Input tensor containing one or more images. + hue (float): Hue-rotation factor in [-0.5, 0.5], applied to all images. + stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + cvcuda.Tensor: The output tensor (same shape, dtype, and layout as src). + + Caution: + Restrictions to several arguments may apply. Check the C API references of the CV-CUDA + operator. + )pbdoc"); + + m.def("adjust_hue_into", NvtxTrace("cvcuda.adjust_hue_into", &AdjustHueInto), "dst"_a, "src"_a, "hue"_a, + py::kw_only(), "stream"_a = nullptr, R"pbdoc( + + Executes the Adjust Hue operation on the given cuda stream. + + See also: + Refer to the CV-CUDA C API reference for the Adjust Hue operator for more details and + usage examples. + + Args: + dst (cvcuda.Tensor): Output tensor to store the result of the operation. + src (cvcuda.Tensor): Input tensor containing one or more images. + hue (float): Hue-rotation factor in [-0.5, 0.5], applied to all images. + stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + cvcuda.Tensor: The output tensor (same as dst). + + Caution: + Restrictions to several arguments may apply. Check the C API references of the CV-CUDA + operator. + )pbdoc"); + + m.def("adjust_hue", NvtxTrace("cvcuda.adjust_hue", &AdjustHueVarShape), "src"_a, "hue"_a, py::kw_only(), + "stream"_a = nullptr, R"pbdoc( + + Executes the Adjust Hue operation on the given cuda stream. + + Args: + src (cvcuda.ImageBatchVarShape): Input image batch containing one or more images. + hue (float): Hue-rotation factor in [-0.5, 0.5], applied to all images. + stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + cvcuda.ImageBatchVarShape: The output image batch (same formats and sizes as src). + + Caution: + Restrictions to several arguments may apply. Check the C API references of the CV-CUDA + operator. + )pbdoc"); + + m.def("adjust_hue_into", NvtxTrace("cvcuda.adjust_hue_into", &AdjustHueVarShapeInto), "dst"_a, "src"_a, "hue"_a, + py::kw_only(), "stream"_a = nullptr, R"pbdoc( + + Executes the Adjust Hue operation on the given cuda stream. + + Args: + dst (cvcuda.ImageBatchVarShape): Output image batch to store the result of the operation. + src (cvcuda.ImageBatchVarShape): Input image batch containing one or more images. + hue (float): Hue-rotation factor in [-0.5, 0.5], applied to all images. + stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + cvcuda.ImageBatchVarShape: The output image batch (same as dst). + + Caution: + Restrictions to several arguments may apply. Check the C API references of the CV-CUDA + operator. + )pbdoc"); +} + +} // namespace cvcudapy diff --git a/python/mod_cvcuda/operators/OpAdjustSaturation.cpp b/python/mod_cvcuda/operators/OpAdjustSaturation.cpp new file mode 100644 index 000000000..43e98d01b --- /dev/null +++ b/python/mod_cvcuda/operators/OpAdjustSaturation.cpp @@ -0,0 +1,144 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "UnaryElementwiseOp.hpp" + +#include +#include + +namespace cvcudapy { + +namespace { +// Thin Python-facing wrappers; the create/guard/submit body lives in UnaryElementwiseOp.hpp, with +// the saturation factor forwarded as the trailing submit() parameter. +Tensor AdjustSaturationInto(Tensor &output, Tensor &input, double saturation, std::optional pstream) +{ + return UnaryElementwiseInto(output, input, pstream, saturation); +} + +Tensor AdjustSaturation(Tensor &input, double saturation, std::optional pstream) +{ + return UnaryElementwiseTensor(input, pstream, saturation); +} + +ImageBatchVarShape AdjustSaturationVarShapeInto(ImageBatchVarShape &output, ImageBatchVarShape &input, + double saturation, std::optional pstream) +{ + return UnaryElementwiseInto(output, input, pstream, saturation); +} + +ImageBatchVarShape AdjustSaturationVarShape(ImageBatchVarShape &input, double saturation, std::optional pstream) +{ + return UnaryElementwiseVarShape(input, pstream, saturation); +} + +} // namespace + +void ExportOpAdjustSaturation(py::module &m) +{ + using namespace pybind11::literals; + + m.def("adjust_saturation", NvtxTrace("cvcuda.adjust_saturation", &AdjustSaturation), "src"_a, "saturation"_a, + py::kw_only(), "stream"_a = nullptr, R"pbdoc( + + Executes the Adjust Saturation operation on the given cuda stream. + + Blends each RGB image toward its grayscale by a scalar factor: + ``out = saturation * image + (1 - saturation) * grayscale``, where + ``grayscale = 0.2989*R + 0.587*G + 0.114*B``. ``saturation`` = 1 leaves the image unchanged, + 0 yields grayscale, and values > 1 over-saturate. Single-channel images are returned + unchanged. Mirrors torchvision.transforms.v2.functional.adjust_saturation. + + See also: + Refer to the CV-CUDA C API reference for the Adjust Saturation operator for more details + and usage examples. + + Args: + src (cvcuda.Tensor): Input tensor containing one or more images. + saturation (float): Saturation factor applied to all images (must be >= 0). + stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + cvcuda.Tensor: The output tensor (same shape, dtype, and layout as src). + + Caution: + Restrictions to several arguments may apply. Check the C API references of the CV-CUDA + operator. + )pbdoc"); + + m.def("adjust_saturation_into", NvtxTrace("cvcuda.adjust_saturation_into", &AdjustSaturationInto), "dst"_a, "src"_a, + "saturation"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( + + Executes the Adjust Saturation operation on the given cuda stream. + + See also: + Refer to the CV-CUDA C API reference for the Adjust Saturation operator for more details + and usage examples. + + Args: + dst (cvcuda.Tensor): Output tensor to store the result of the operation. + src (cvcuda.Tensor): Input tensor containing one or more images. + saturation (float): Saturation factor applied to all images (must be >= 0). + stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + cvcuda.Tensor: The output tensor (same as dst). + + Caution: + Restrictions to several arguments may apply. Check the C API references of the CV-CUDA + operator. + )pbdoc"); + + m.def("adjust_saturation", NvtxTrace("cvcuda.adjust_saturation", &AdjustSaturationVarShape), "src"_a, + "saturation"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( + + Executes the Adjust Saturation operation on the given cuda stream. + + Args: + src (cvcuda.ImageBatchVarShape): Input image batch containing one or more images. + saturation (float): Saturation factor applied to all images (must be >= 0). + stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + cvcuda.ImageBatchVarShape: The output image batch (same formats and sizes as src). + + Caution: + Restrictions to several arguments may apply. Check the C API references of the CV-CUDA + operator. + )pbdoc"); + + m.def("adjust_saturation_into", NvtxTrace("cvcuda.adjust_saturation_into", &AdjustSaturationVarShapeInto), "dst"_a, + "src"_a, "saturation"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( + + Executes the Adjust Saturation operation on the given cuda stream. + + Args: + dst (cvcuda.ImageBatchVarShape): Output image batch to store the result of the operation. + src (cvcuda.ImageBatchVarShape): Input image batch containing one or more images. + saturation (float): Saturation factor applied to all images (must be >= 0). + stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + cvcuda.ImageBatchVarShape: The output image batch (same as dst). + + Caution: + Restrictions to several arguments may apply. Check the C API references of the CV-CUDA + operator. + )pbdoc"); +} + +} // namespace cvcudapy diff --git a/python/mod_cvcuda/operators/OpAdjustSharpness.cpp b/python/mod_cvcuda/operators/OpAdjustSharpness.cpp new file mode 100644 index 000000000..469780911 --- /dev/null +++ b/python/mod_cvcuda/operators/OpAdjustSharpness.cpp @@ -0,0 +1,145 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "UnaryElementwiseOp.hpp" + +#include +#include + +namespace cvcudapy { + +namespace { +// Thin Python-facing wrappers; the create/guard/submit body lives in UnaryElementwiseOp.hpp, with +// the sharpness factor forwarded as the trailing submit() parameter. +Tensor AdjustSharpnessInto(Tensor &output, Tensor &input, float sharpnessFactor, std::optional pstream) +{ + return UnaryElementwiseInto(output, input, pstream, sharpnessFactor); +} + +Tensor AdjustSharpness(Tensor &input, float sharpnessFactor, std::optional pstream) +{ + return UnaryElementwiseTensor(input, pstream, sharpnessFactor); +} + +ImageBatchVarShape AdjustSharpnessVarShapeInto(ImageBatchVarShape &output, ImageBatchVarShape &input, + float sharpnessFactor, std::optional pstream) +{ + return UnaryElementwiseInto(output, input, pstream, sharpnessFactor); +} + +ImageBatchVarShape AdjustSharpnessVarShape(ImageBatchVarShape &input, float sharpnessFactor, + std::optional pstream) +{ + return UnaryElementwiseVarShape(input, pstream, sharpnessFactor); +} + +} // namespace + +void ExportOpAdjustSharpness(py::module &m) +{ + using namespace pybind11::literals; + + m.def("adjust_sharpness", NvtxTrace("cvcuda.adjust_sharpness", &AdjustSharpness), "src"_a, "sharpness_factor"_a, + py::kw_only(), "stream"_a = nullptr, R"pbdoc( + + Executes the Adjust Sharpness operation on the given cuda stream. + + Blends each image with a 3x3-smoothed copy of itself over the image interior: + ``out = sharpness_factor * in + (1 - sharpness_factor) * blur``. ``sharpness_factor`` of 1.0 + leaves the image unchanged, 0.0 yields the fully-smoothed image, and values above 1.0 + sharpen. The 1-pixel border is copied unchanged, and images with height or width below 3 are + returned unchanged. Mirrors torchvision.transforms.v2.functional.adjust_sharpness. + + See also: + Refer to the CV-CUDA C API reference for the Adjust Sharpness operator for more details + and usage examples. + + Args: + src (cvcuda.Tensor): Input tensor containing one or more images. + sharpness_factor (float): Non-negative blend weight applied to the original image. + stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + cvcuda.Tensor: The output tensor (same shape, dtype, and layout as src). + + Caution: + Restrictions to several arguments may apply. Check the C API references of the CV-CUDA + operator. + )pbdoc"); + + m.def("adjust_sharpness_into", NvtxTrace("cvcuda.adjust_sharpness_into", &AdjustSharpnessInto), "dst"_a, "src"_a, + "sharpness_factor"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( + + Executes the Adjust Sharpness operation on the given cuda stream. + + See also: + Refer to the CV-CUDA C API reference for the Adjust Sharpness operator for more details + and usage examples. + + Args: + dst (cvcuda.Tensor): Output tensor to store the result of the operation. + src (cvcuda.Tensor): Input tensor containing one or more images. + sharpness_factor (float): Non-negative blend weight applied to the original image. + stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + cvcuda.Tensor: The output tensor (same as dst). + + Caution: + Restrictions to several arguments may apply. Check the C API references of the CV-CUDA + operator. + )pbdoc"); + + m.def("adjust_sharpness", NvtxTrace("cvcuda.adjust_sharpness", &AdjustSharpnessVarShape), "src"_a, + "sharpness_factor"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( + + Executes the Adjust Sharpness operation on the given cuda stream. + + Args: + src (cvcuda.ImageBatchVarShape): Input image batch containing one or more images. + sharpness_factor (float): Non-negative blend weight applied to the original image. + stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + cvcuda.ImageBatchVarShape: The output image batch (same formats and sizes as src). + + Caution: + Restrictions to several arguments may apply. Check the C API references of the CV-CUDA + operator. + )pbdoc"); + + m.def("adjust_sharpness_into", NvtxTrace("cvcuda.adjust_sharpness_into", &AdjustSharpnessVarShapeInto), "dst"_a, + "src"_a, "sharpness_factor"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( + + Executes the Adjust Sharpness operation on the given cuda stream. + + Args: + dst (cvcuda.ImageBatchVarShape): Output image batch to store the result of the operation. + src (cvcuda.ImageBatchVarShape): Input image batch containing one or more images. + sharpness_factor (float): Non-negative blend weight applied to the original image. + stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + cvcuda.ImageBatchVarShape: The output image batch (same as dst). + + Caution: + Restrictions to several arguments may apply. Check the C API references of the CV-CUDA + operator. + )pbdoc"); +} + +} // namespace cvcudapy diff --git a/python/mod_cvcuda/operators/OpAdvCvtColor.cpp b/python/mod_cvcuda/operators/OpAdvCvtColor.cpp index a9903b015..ded3cff90 100644 --- a/python/mod_cvcuda/operators/OpAdvCvtColor.cpp +++ b/python/mod_cvcuda/operators/OpAdvCvtColor.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -40,7 +40,8 @@ Tensor AdvCvtColorInto(Tensor &output, Tensor &input, NVCVColorConversionCode co guard.add(LockMode::LOCK_MODE_READ, {input}); guard.add(LockMode::LOCK_MODE_WRITE, {output}); guard.add(LockMode::LOCK_MODE_NONE, {*op}); - op->submit(pstream->cudaHandle(), input, output, code, spec); + guard.run([&op, &pstream, &input, &output, &code, &spec]() + { op->submit(pstream->cudaHandle(), input, output, code, nvcv::ColorSpec{spec}); }); return std::move(output); } @@ -59,14 +60,11 @@ Tensor AdvCvtColor(Tensor &input, NVCVColorConversionCode code, NVCVColorSpec sp void ExportOpAdvCvtColor(py::module &m) { using namespace pybind11::literals; - m.def("advcvtcolor", &AdvCvtColor, "src"_a, "code"_a, "spec"_a, py::kw_only(), "stream"_a = nullptr, + m.def("advcvtcolor", NvtxTrace("cvcuda.advcvtcolor", &AdvCvtColor), "src"_a, "code"_a, "spec"_a, py::kw_only(), + "stream"_a = nullptr, R"pbdoc( - Executes the Adv Cvt Color operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Adv Cvt Color operator - for more details and usage examples. Args: src (cvcuda.Tensor): Input tensor containing one or more images. @@ -77,19 +75,12 @@ void ExportOpAdvCvtColor(py::module &m) Returns: cvcuda.Tensor: The output color converted image. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("advcvtcolor_into", &AdvCvtColorInto, "dst"_a, "src"_a, "code"_a, "spec"_a, py::kw_only(), - "stream"_a = nullptr, R"pbdoc( - + m.def("advcvtcolor_into", NvtxTrace("cvcuda.advcvtcolor_into", &AdvCvtColorInto), "dst"_a, "src"_a, "code"_a, + "spec"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( Executes the Adv Cvt Color operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Adv Cvt Color operator - for more details and usage examples. Args: dst (cvcuda.Tensor): Output tensor to store the result of the operation. @@ -99,11 +90,7 @@ void ExportOpAdvCvtColor(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.Tensor: The output tensor (same as dst). )pbdoc"); } diff --git a/python/mod_cvcuda/operators/OpAutoContrast.cpp b/python/mod_cvcuda/operators/OpAutoContrast.cpp new file mode 100644 index 000000000..104f8d8d3 --- /dev/null +++ b/python/mod_cvcuda/operators/OpAutoContrast.cpp @@ -0,0 +1,184 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "Operators.hpp" + +#include +#include +#include +#include +#include +#include + +namespace cvcudapy { + +namespace { + +inline Tensor tensorLike(Tensor &src) +{ + Shape dstShape = nvcvpy::CreateShape(src.shape()); + return Tensor::Create(dstShape, src.dtype(), src.layout()); +} + +template +Container AutoContrastIntoImpl(Container &output, Container &input, std::optional pstream) +{ + if (!pstream) + { + pstream = Stream::Current(); + } + + auto op = CreateOperator(); + + ResourceGuard guard(*pstream); + guard.add(LockMode::LOCK_MODE_READ, {input}); + guard.add(LockMode::LOCK_MODE_WRITE, {output}); + // The operator allocates a per-(image, channel) min/max scratch buffer accessed by the device, + // so the operator object must outlive the kernels: lock it READWRITE rather than NONE. + guard.add(LockMode::LOCK_MODE_READWRITE, {*op}); + + guard.run([&op, &pstream, &input, &output]() { op->submit(pstream->cudaHandle(), input, output); }); + + return std::move(output); +} + +Tensor AutoContrastInto(Tensor &output, Tensor &input, std::optional pstream) +{ + return AutoContrastIntoImpl(output, input, pstream); +} + +Tensor AutoContrast(Tensor &input, std::optional pstream) +{ + Tensor output = tensorLike(input); + + return AutoContrastInto(output, input, pstream); +} + +ImageBatchVarShape VarShapeAutoContrastInto(ImageBatchVarShape &output, ImageBatchVarShape &input, + std::optional pstream) +{ + return AutoContrastIntoImpl(output, input, pstream); +} + +ImageBatchVarShape VarShapeAutoContrast(ImageBatchVarShape &input, std::optional pstream) +{ + ImageBatchVarShape output = ImageBatchVarShape::Create(input.capacity()); + for (int i = 0; i < input.numImages(); ++i) + { + output.pushBackImage(Image::Create(input[i].size(), input[i].format())); + } + + return VarShapeAutoContrastInto(output, input, pstream); +} + +} // namespace + +void ExportOpAutoContrast(py::module &m) +{ + using namespace pybind11::literals; + + m.def("autocontrast", NvtxTrace("cvcuda.autocontrast", &AutoContrast), "src"_a, py::kw_only(), "stream"_a = nullptr, + R"pbdoc( + + Executes the Auto Contrast operation on the given cuda stream. + + Maximizes (normalizes) image contrast by remapping each channel independently so its spatial + minimum maps to 0 and its spatial maximum maps to the data-type maximum (255 for 8-bit, 65535 + for 16-bit, 1.0 for float). A channel that is flat (all pixels equal) is left unchanged. This + mimics ``torchvision.transforms.v2.functional.autocontrast`` / ``PIL.ImageOps.autocontrast`` + with ``cutoff = 0`` for finite inputs. For floating-point inputs, only finite pixels define + the channel range; NaN and infinity pixels are copied unchanged. + + See also: + Refer to the CV-CUDA C API reference for the Auto Contrast operator + for more details and usage examples. + + Args: + src (cvcuda.Tensor): Input tensor. + stream (Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + cvcuda.Tensor: The output tensor (same shape, layout, and dtype as the input). + + Caution: + Restrictions to several arguments may apply. Check the C + API references of the CV-CUDA operator. + )pbdoc"); + + m.def("autocontrast_into", NvtxTrace("cvcuda.autocontrast_into", &AutoContrastInto), "dst"_a, "src"_a, + py::kw_only(), "stream"_a = nullptr, R"pbdoc( + + Executes the Auto Contrast operation on the given cuda stream. + + See also: + Refer to the CV-CUDA C API reference for the Auto Contrast operator + for more details and usage examples. + + Args: + dst (cvcuda.Tensor): Output tensor (same shape, layout, and dtype as the input). + src (cvcuda.Tensor): Input tensor. + stream (Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + cvcuda.Tensor: The output tensor (same as dst). + + Caution: + Restrictions to several arguments may apply. Check the C + API references of the CV-CUDA operator. + )pbdoc"); + + // VarShape variants + m.def("autocontrast", NvtxTrace("cvcuda.autocontrast", &VarShapeAutoContrast), "src"_a, py::kw_only(), + "stream"_a = nullptr, R"pbdoc( + + Executes the Auto Contrast operation on a batch of variable-shaped images. + + For floating-point inputs, only finite pixels define each channel range; NaN and infinity + pixels are copied unchanged. + + Args: + src (cvcuda.ImageBatchVarShape): Input image batch. + stream (Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + cvcuda.ImageBatchVarShape: The output image batch. + + Caution: + Restrictions to several arguments may apply. Check the C + API references of the CV-CUDA operator. + )pbdoc"); + + m.def("autocontrast_into", NvtxTrace("cvcuda.autocontrast_into", &VarShapeAutoContrastInto), "dst"_a, "src"_a, + py::kw_only(), "stream"_a = nullptr, R"pbdoc( + + Executes the Auto Contrast operation on a batch of variable-shaped images. + + Args: + dst (cvcuda.ImageBatchVarShape): Output image batch. + src (cvcuda.ImageBatchVarShape): Input image batch. + stream (Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + cvcuda.ImageBatchVarShape: The output image batch (same as dst). + + Caution: + Restrictions to several arguments may apply. Check the C + API references of the CV-CUDA operator. + )pbdoc"); +} + +} // namespace cvcudapy diff --git a/python/mod_cvcuda/operators/OpAverageBlur.cpp b/python/mod_cvcuda/operators/OpAverageBlur.cpp index 3fb9a1b83..cfbac9ba1 100644 --- a/python/mod_cvcuda/operators/OpAverageBlur.cpp +++ b/python/mod_cvcuda/operators/OpAverageBlur.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,6 +16,7 @@ */ #include "Operators.hpp" +#include "VarShapeUtils.hpp" #include #include @@ -49,7 +50,8 @@ Tensor AverageBlurInto(Tensor &output, Tensor &input, const std::tuple guard.add(LockMode::LOCK_MODE_WRITE, {output}); guard.add(LockMode::LOCK_MODE_WRITE, {*averageBlur}); - averageBlur->submit(pstream->cudaHandle(), input, output, kernelSizeArg, kernelAnchorArg, border); + guard.run([&averageBlur, &pstream, &input, &output, &kernelSizeArg, &kernelAnchorArg, &border]() + { averageBlur->submit(pstream->cudaHandle(), input, output, kernelSizeArg, kernelAnchorArg, border); }); return output; } @@ -80,7 +82,8 @@ ImageBatchVarShape AverageBlurVarShapeInto(ImageBatchVarShape &output, ImageBatc guard.add(LockMode::LOCK_MODE_WRITE, {output}); guard.add(LockMode::LOCK_MODE_READWRITE, {*averageBlur}); - averageBlur->submit(pstream->cudaHandle(), input, output, kernel_size, kernel_anchor, border); + guard.run([&averageBlur, &pstream, &input, &output, &kernel_size, &kernel_anchor, &border]() + { averageBlur->submit(pstream->cudaHandle(), input, output, kernel_size, kernel_anchor, border); }); return output; } @@ -89,15 +92,7 @@ ImageBatchVarShape AverageBlurVarShape(ImageBatchVarShape &input, const std::tup Tensor &kernel_size, Tensor &kernel_anchor, NVCVBorderType border, std::optional pstream) { - ImageBatchVarShape output = ImageBatchVarShape::Create(input.capacity()); - - for (int i = 0; i < input.numImages(); ++i) - { - nvcv::ImageFormat format = input[i].format(); - nvcv::Size2D size = input[i].size(); - auto image = Image::Create(size, format); - output.pushBack(image); - } + ImageBatchVarShape output = CreateSameShapeImageBatch(input); return AverageBlurVarShapeInto(output, input, max_kernel_size, kernel_size, kernel_anchor, border, pstream); } @@ -107,20 +102,14 @@ ImageBatchVarShape AverageBlurVarShape(ImageBatchVarShape &input, const std::tup void ExportOpAverageBlur(py::module &m) { using namespace pybind11::literals; - py::options options; - options.disable_function_signatures(); const std::tuple def_anchor{-1, -1}; - m.def("averageblur", &AverageBlur, "src"_a, "kernel_size"_a, "kernel_anchor"_a = def_anchor, - "border"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - cvcuda.averageblur(src: cvcuda.Tensor, kernel_size: Tuple[int, int], kernel_anchor: Tuple[int, int], border: cvcuda.Border = cvcuda.Border.CONSTANT, stream: Optional[cvcuda.Stream] = None) -> cvcuda.Tensor + m.def("averageblur", NvtxTrace("cvcuda.averageblur", &AverageBlur), "src"_a, "kernel_size"_a, + "kernel_anchor"_a = def_anchor, "border"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, py::kw_only(), + "stream"_a = nullptr, R"pbdoc( + Executes the AverageBlur operation on the given cuda stream. - Executes the AverageBlur operation on the given cuda stream. - - See also: - Refer to the CV-CUDA C API reference for the AverageBlur operator - for more details and usage examples. Args: src (cvcuda.Tensor): Input tensor containing one or more images. @@ -129,27 +118,17 @@ void ExportOpAverageBlur(py::module &m) border (cvcuda.Border, optional): Border mode to be used when accessing elements outside input image. stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. - See also: - Refer to the CV-CUDA C API reference for the AverageBlur operator - for more details and usage examples. Returns: cvcuda.Tensor: The output tensor. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("averageblur_into", &AverageBlurInto, "dst"_a, "src"_a, "kernel_size"_a, "kernel_anchor"_a = def_anchor, - "border"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - cvcuda.averageblur_into(dst: cvcuda.Tensor, src: cvcuda.Tensor, kernel_size: Tuple[int, int], kernel_anchor: Tuple[int, int], border: cvcuda.Border = cvcuda.Border.CONSTANT, stream: Optional[cvcuda.Stream] = None) - + m.def("averageblur_into", NvtxTrace("cvcuda.averageblur_into", &AverageBlurInto), "dst"_a, "src"_a, "kernel_size"_a, + "kernel_anchor"_a = def_anchor, "border"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, py::kw_only(), + "stream"_a = nullptr, R"pbdoc( Executes the AverageBlur operation on the given cuda stream and writes the result into the 'dst' tensor. - See also: - Refer to the CV-CUDA C API reference for the AverageBlur operator - for more details and usage examples. Args: dst (cvcuda.Tensor): Output tensor to store the result of the operation. @@ -160,17 +139,12 @@ void ExportOpAverageBlur(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.Tensor: The output tensor (same as dst). )pbdoc"); - m.def("averageblur", &AverageBlurVarShape, "src"_a, "max_kernel_size"_a, "kernel_size"_a, "kernel_anchor"_a, - "border"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - cvcuda.averageblur(src: cvcuda.ImageBatchVarShape, kernel_size: Tuple[int, int], kernel_anchor: Tuple[int, int], border: cvcuda.Border = cvcuda.Border.CONSTANT, stream: Optional[cvcuda.Stream] = None) -> cvcuda.ImageBatchVarShape - + m.def("averageblur", NvtxTrace("cvcuda.averageblur", &AverageBlurVarShape), "src"_a, "max_kernel_size"_a, + "kernel_size"_a, "kernel_anchor"_a, "border"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, py::kw_only(), + "stream"_a = nullptr, R"pbdoc( Executes the AverageBlur operation with a variable shape tensors on the given cuda stream. Args: @@ -184,16 +158,12 @@ void ExportOpAverageBlur(py::module &m) Returns: cvcuda.ImageBatchVarShape: The output image batch. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("averageblur_into", &AverageBlurVarShapeInto, "dst"_a, "src"_a, "max_kernel_size"_a, "kernel_size"_a, - "kernel_anchor"_a, "border"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, py::kw_only(), "stream"_a = nullptr, + m.def("averageblur_into", NvtxTrace("cvcuda.averageblur_into", &AverageBlurVarShapeInto), "dst"_a, "src"_a, + "max_kernel_size"_a, "kernel_size"_a, "kernel_anchor"_a, "border"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, + py::kw_only(), "stream"_a = nullptr, R"pbdoc( - cvcuda.averageblur_into(dst: cvcuda.ImageBatchVarShape, src: cvcuda.ImageBatchVarShape, kernel_size: Tuple[int, int], kernel_anchor: Tuple[int, int], border: cvcuda.Border = cvcuda.Border.CONSTANT, stream: Optional[cvcuda.Stream] = None) - Executes the AverageBlur operation with a variable shape tensors on the given cuda stream. Args: @@ -206,11 +176,7 @@ void ExportOpAverageBlur(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.ImageBatchVarShape: The output image batch (same as dst). )pbdoc"); } diff --git a/python/mod_cvcuda/operators/OpBilateralFilter.cpp b/python/mod_cvcuda/operators/OpBilateralFilter.cpp index 12429ed98..7f39b183f 100644 --- a/python/mod_cvcuda/operators/OpBilateralFilter.cpp +++ b/python/mod_cvcuda/operators/OpBilateralFilter.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,6 +16,7 @@ */ #include "Operators.hpp" +#include "VarShapeUtils.hpp" #include #include @@ -46,7 +47,11 @@ Tensor BilateralFilterInto(Tensor &output, Tensor &input, int diameter, float si guard.add(LockMode::LOCK_MODE_WRITE, {output}); guard.add(LockMode::LOCK_MODE_NONE, {*bilateral_filter}); - bilateral_filter->submit(pstream->cudaHandle(), input, output, diameter, sigmaColor, sigmaSpace, borderMode); + guard.run( + [&bilateral_filter, &pstream, &input, &output, &diameter, &sigmaColor, &sigmaSpace, &borderMode]() { + bilateral_filter->submit(pstream->cudaHandle(), input, output, diameter, sigmaColor, sigmaSpace, + borderMode); + }); return output; } @@ -75,7 +80,11 @@ ImageBatchVarShape VarShapeBilateralFilterInto(ImageBatchVarShape &output, Image guard.add(LockMode::LOCK_MODE_WRITE, {output}); guard.add(LockMode::LOCK_MODE_NONE, {*bilateral_filter}); - bilateral_filter->submit(pstream->cudaHandle(), input, output, diameter, sigmaColor, sigmaSpace, borderMode); + guard.run( + [&bilateral_filter, &pstream, &input, &output, &diameter, &sigmaColor, &sigmaSpace, &borderMode]() { + bilateral_filter->submit(pstream->cudaHandle(), input, output, diameter, sigmaColor, sigmaSpace, + borderMode); + }); return output; } @@ -83,15 +92,7 @@ ImageBatchVarShape VarShapeBilateralFilterInto(ImageBatchVarShape &output, Image ImageBatchVarShape VarShapeBilateralFilter(ImageBatchVarShape &input, Tensor &diameter, Tensor &sigmaColor, Tensor &sigmaSpace, NVCVBorderType borderMode, std::optional pstream) { - ImageBatchVarShape output = ImageBatchVarShape::Create(input.capacity()); - - for (int i = 0; i < input.numImages(); ++i) - { - nvcv::ImageFormat format = input[i].format(); - nvcv::Size2D size = input[i].size(); - auto image = Image::Create(size, format); - output.pushBack(image); - } + ImageBatchVarShape output = CreateSameShapeImageBatch(input); return VarShapeBilateralFilterInto(output, input, diameter, sigmaColor, sigmaSpace, borderMode, pstream); } @@ -102,19 +103,11 @@ void ExportOpBilateralFilter(py::module &m) { using namespace pybind11::literals; - py::options options; - options.disable_function_signatures(); - - m.def("bilateral_filter", &BilateralFilter, "src"_a, "diameter"_a, "sigma_color"_a, "sigma_space"_a, - "border"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - - cvcuda.bilateral_filter(src: cvcuda.Tensor, diameter: int, sigma_color: float, sigma_space: float, border:cvcuda.Border = cvcuda.Border.CONSTANT, stream: Optional[cvcuda.Stream] = None) -> cvcuda.Tensor - + m.def("bilateral_filter", NvtxTrace("cvcuda.bilateral_filter", &BilateralFilter), "src"_a, "diameter"_a, + "sigma_color"_a, "sigma_space"_a, "border"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, py::kw_only(), + "stream"_a = nullptr, R"pbdoc( Executes the Bilateral Filter operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Bilateral Filter operator - for more details and usage examples. Args: src (cvcuda.Tensor): Input tensor containing one or more images. @@ -127,22 +120,14 @@ void ExportOpBilateralFilter(py::module &m) Returns: cvcuda.Tensor: The output tensor. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("bilateral_filter_into", &BilateralFilterInto, "dst"_a, "src"_a, "diameter"_a, "sigma_color"_a, - "sigma_space"_a, "border"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, py::kw_only(), "stream"_a = nullptr, + m.def("bilateral_filter_into", NvtxTrace("cvcuda.bilateral_filter_into", &BilateralFilterInto), "dst"_a, "src"_a, + "diameter"_a, "sigma_color"_a, "sigma_space"_a, "border"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, + py::kw_only(), "stream"_a = nullptr, R"pbdoc( - - cvcuda.bilateral_filter_into(dst: cvcuda.Tensor, src: cvcuda.Tensor, diameter: int, sigma_color: float, sigma_space: float, border: cvcuda.Border = cvcuda.Border.CONSTANT, stream: Optional[cvcuda.Stream] = None) - Executes the Bilateral Filter operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Bilateral Filter operator - for more details and usage examples. Args: dst (cvcuda.Tensor): Output tensor to store the result of the operation. @@ -154,23 +139,14 @@ void ExportOpBilateralFilter(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.Tensor: The output tensor (same as dst). )pbdoc"); - m.def("bilateral_filter", &VarShapeBilateralFilter, "src"_a, "diameter"_a, "sigma_color"_a, "sigma_space"_a, - py::kw_only(), "border"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, "stream"_a = nullptr, R"pbdoc( - - cvcuda.bilateral_filter(src: cvcuda.ImageBatchVarShape, diameter: cvcuda.Tensor, sigma_color: cvcuda.Tensor, sigma_space: cvcuda.Tensor, border: cvcuda.Border = cvcuda.Border.CONSTANT, stream: Optional[cvcuda.Stream] = None) -> cvcuda.ImageBatchVarShape - + m.def("bilateral_filter", NvtxTrace("cvcuda.bilateral_filter", &VarShapeBilateralFilter), "src"_a, "diameter"_a, + "sigma_color"_a, "sigma_space"_a, py::kw_only(), "border"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, + "stream"_a = nullptr, R"pbdoc( Executes the Bilateral Filter operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Bilateral Filter operator - for more details and usage examples. Args: src (cvcuda.ImageBatchVarShape): Input tensor containing one or more images. @@ -183,22 +159,14 @@ void ExportOpBilateralFilter(py::module &m) Returns: cvcuda.ImageBatchVarShape: The output image batch. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("bilateral_filter_into", &VarShapeBilateralFilterInto, "dst"_a, "src"_a, "diameter"_a, "sigma_color"_a, - "sigma_space"_a, py::kw_only(), "border"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, "stream"_a = nullptr, + m.def("bilateral_filter_into", NvtxTrace("cvcuda.bilateral_filter_into", &VarShapeBilateralFilterInto), "dst"_a, + "src"_a, "diameter"_a, "sigma_color"_a, "sigma_space"_a, py::kw_only(), + "border"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, "stream"_a = nullptr, R"pbdoc( - - cvcuda.bilateral_filter_into(dst: cvcuda.ImageBatchVarShape, src: cvcuda.ImageBatchVarShape, diameter: cvcuda.Tensor, sigma_color: cvcuda.Tensor, sigma_space: cvcuda.Tensor, border: cvcuda.Border = cvcuda.Border.CONSTANT, stream: Optional[cvcuda.Stream] = None) - Executes the Bilateral Filter operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Bilateral Filter operator - for more details and usage examples. Args: dst (cvcuda.ImageBatchVarShape): Output image batch containing the result of the operation. @@ -210,11 +178,7 @@ void ExportOpBilateralFilter(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.ImageBatchVarShape: The output image batch (same as dst). )pbdoc"); } diff --git a/python/mod_cvcuda/operators/OpBndBox.cpp b/python/mod_cvcuda/operators/OpBndBox.cpp index 40609f63f..b7986236f 100644 --- a/python/mod_cvcuda/operators/OpBndBox.cpp +++ b/python/mod_cvcuda/operators/OpBndBox.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,14 +19,20 @@ #include #include +#include #include #include #include +#include + namespace cvcudapy { namespace { -Tensor BndBoxInto(Tensor &output, Tensor &input, NVCVBndBoxesI bboxes, std::optional pstream) +using BndBoxesImpl = cvcuda::priv::NVCVBndBoxesImpl; + +Tensor BndBoxInto(Tensor &output, Tensor &input, const std::shared_ptr &bboxes, + std::optional pstream) { if (!pstream) { @@ -40,12 +46,14 @@ Tensor BndBoxInto(Tensor &output, Tensor &input, NVCVBndBoxesI bboxes, std::opti guard.add(LockMode::LOCK_MODE_WRITE, {output}); guard.add(LockMode::LOCK_MODE_NONE, {*op}); - op->submit(pstream->cudaHandle(), input, output, bboxes); + auto bboxesHandle = static_cast(static_cast(bboxes.get())); + guard.run([&op, &pstream, &input, &output, bboxesHandle]() + { op->submit(pstream->cudaHandle(), input, output, bboxesHandle); }); return std::move(output); } -Tensor BndBox(Tensor &input, NVCVBndBoxesI bboxes, std::optional pstream) +Tensor BndBox(Tensor &input, const std::shared_ptr &bboxes, std::optional pstream) { Tensor output = Tensor::Create(input.shape(), input.dtype()); @@ -58,13 +66,10 @@ void ExportOpBndBox(py::module &m) { using namespace pybind11::literals; - m.def("bndbox", &BndBox, "src"_a, "bboxes"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - + m.def("bndbox", NvtxTrace("cvcuda.bndbox", &BndBox), "src"_a, "bboxes"_a, py::kw_only(), "stream"_a = nullptr, + R"pbdoc( Executes the BndBox operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the BndBox operator - for more details and usage examples. Args: src (cvcuda.Tensor): Input tensor containing one or more images. @@ -74,18 +79,12 @@ void ExportOpBndBox(py::module &m) Returns: cvcuda.Tensor: The output tensor. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("bndbox_into", &BndBoxInto, "dst"_a, "src"_a, "bboxes"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - + m.def("bndbox_into", NvtxTrace("cvcuda.bndbox_into", &BndBoxInto), "dst"_a, "src"_a, "bboxes"_a, py::kw_only(), + "stream"_a = nullptr, R"pbdoc( Executes the BndBox operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the BndBox operator - for more details and usage examples. Args: dst (cvcuda.Tensor): Output tensor to store the result of the operation. @@ -94,11 +93,7 @@ void ExportOpBndBox(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.Tensor: The output tensor (same as dst). )pbdoc"); } diff --git a/python/mod_cvcuda/operators/OpBoxBlur.cpp b/python/mod_cvcuda/operators/OpBoxBlur.cpp index 6af602b90..cbfdd4e60 100644 --- a/python/mod_cvcuda/operators/OpBoxBlur.cpp +++ b/python/mod_cvcuda/operators/OpBoxBlur.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,14 +19,20 @@ #include #include +#include #include #include #include +#include + namespace cvcudapy { namespace { -Tensor BoxBlurInto(Tensor &output, Tensor &input, NVCVBlurBoxesI bboxes, std::optional pstream) +using BlurBoxesImpl = cvcuda::priv::NVCVBlurBoxesImpl; + +Tensor BoxBlurInto(Tensor &output, Tensor &input, const std::shared_ptr &bboxes, + std::optional pstream) { if (!pstream) { @@ -40,12 +46,14 @@ Tensor BoxBlurInto(Tensor &output, Tensor &input, NVCVBlurBoxesI bboxes, std::op guard.add(LockMode::LOCK_MODE_WRITE, {output}); guard.add(LockMode::LOCK_MODE_NONE, {*op}); - op->submit(pstream->cudaHandle(), input, output, bboxes); + auto bboxesHandle = static_cast(static_cast(bboxes.get())); + guard.run([&op, &pstream, &input, &output, bboxesHandle]() + { op->submit(pstream->cudaHandle(), input, output, bboxesHandle); }); return std::move(output); } -Tensor BoxBlur(Tensor &input, NVCVBlurBoxesI bboxes, std::optional pstream) +Tensor BoxBlur(Tensor &input, const std::shared_ptr &bboxes, std::optional pstream) { Tensor output = Tensor::Create(input.shape(), input.dtype()); return BoxBlurInto(output, input, bboxes, pstream); @@ -57,13 +65,10 @@ void ExportOpBoxBlur(py::module &m) { using namespace pybind11::literals; - m.def("boxblur", &BoxBlur, "src"_a, "bboxes"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - + m.def("boxblur", NvtxTrace("cvcuda.boxblur", &BoxBlur), "src"_a, "bboxes"_a, py::kw_only(), "stream"_a = nullptr, + R"pbdoc( Executes the BoxBlur operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the BoxBlur operator - for more details and usage examples. Args: src (cvcuda.Tensor): Input tensor containing one or more images. @@ -73,18 +78,12 @@ void ExportOpBoxBlur(py::module &m) Returns: cvcuda.Tensor: The output tensor. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("boxblur_into", &BoxBlurInto, "dst"_a, "src"_a, "bboxes"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - + m.def("boxblur_into", NvtxTrace("cvcuda.boxblur_into", &BoxBlurInto), "dst"_a, "src"_a, "bboxes"_a, py::kw_only(), + "stream"_a = nullptr, R"pbdoc( Executes the BoxBlur operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the BoxBlur operator - for more details and usage examples. Args: dst (cvcuda.Tensor): Output tensor to store the result of the operation. @@ -93,11 +92,7 @@ void ExportOpBoxBlur(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.Tensor: The output tensor (same as dst). )pbdoc"); } diff --git a/python/mod_cvcuda/operators/OpBrightnessContrast.cpp b/python/mod_cvcuda/operators/OpBrightnessContrast.cpp index 786ddcc8d..86007d686 100644 --- a/python/mod_cvcuda/operators/OpBrightnessContrast.cpp +++ b/python/mod_cvcuda/operators/OpBrightnessContrast.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,7 +15,7 @@ * limitations under the License. */ -#include "Operators.hpp" +#include "UnaryElementwiseOp.hpp" #include #include @@ -42,15 +42,15 @@ inline ImageBatchVarShape batchLike(ImageBatchVarShape &src) ImageBatchVarShape dst = ImageBatchVarShape::Create(src.capacity()); for (int i = 0; i < src.numImages(); ++i) { - dst.pushBack(Image::Create(src[i].size(), src[i].format())); + dst.pushBackImage(Image::Create(src[i].size(), src[i].format())); } return dst; } template -auto runGuard(Op &op, Src &src, Dst &dst, std::optional &brightness, std::optional &contrast, - std::optional &brightnessShift, std::optional &contrastCenter, - std::optional &pstream, Call &&call) +auto runGuard(Op &op, Src &src, Dst &dst, const std::optional &brightness, + const std::optional &contrast, const std::optional &brightnessShift, + const std::optional &contrastCenter, std::optional &pstream, Call &&call) { if (!pstream) { @@ -59,7 +59,7 @@ auto runGuard(Op &op, Src &src, Dst &dst, std::optional &brightness, std ResourceGuard guard(*pstream); guard.add(LockMode::LOCK_MODE_READ, {src}); - for (auto &arg : {brightness, contrast, brightnessShift, contrastCenter}) + for (const auto &arg : {brightness, contrast, brightnessShift, contrastCenter}) { if (arg) { @@ -69,9 +69,13 @@ auto runGuard(Op &op, Src &src, Dst &dst, std::optional &brightness, std guard.add(LockMode::LOCK_MODE_WRITE, {dst}); guard.add(LockMode::LOCK_MODE_NONE, {*op}); - call(*pstream, brightness ? *brightness : nvcv::Tensor{nullptr}, contrast ? *contrast : nvcv::Tensor{nullptr}, - brightnessShift ? *brightnessShift : nvcv::Tensor{nullptr}, - contrastCenter ? *contrastCenter : nvcv::Tensor{nullptr}); + guard.run( + [&brightness, &contrast, &brightnessShift, &contrastCenter, &call, &pstream]() + { + const nvcv::Tensor nullTensor{nullptr}; + call(*pstream, AsNvcvTensor(brightness, nullTensor), AsNvcvTensor(contrast, nullTensor), + AsNvcvTensor(brightnessShift, nullTensor), AsNvcvTensor(contrastCenter, nullTensor)); + }); } Tensor BrightnessContrastInto(Tensor &dst, Tensor &src, std::optional &brightness, @@ -80,8 +84,8 @@ Tensor BrightnessContrastInto(Tensor &dst, Tensor &src, std::optional &b { auto op = CreateOperator(); runGuard(op, src, dst, brightness, contrast, brightnessShift, contrastCenter, pstream, - [&](Stream &stream, const nvcv::Tensor &brightnessArg, const nvcv::Tensor &contrastArg, - const nvcv::Tensor &brightnessShiftArg, const nvcv::Tensor &contrastCenterArg) { + [&op, &src, &dst](Stream &stream, const nvcv::Tensor &brightnessArg, const nvcv::Tensor &contrastArg, + const nvcv::Tensor &brightnessShiftArg, const nvcv::Tensor &contrastCenterArg) { op->submit(stream.cudaHandle(), src, dst, brightnessArg, contrastArg, brightnessShiftArg, contrastCenterArg); }); @@ -103,8 +107,8 @@ ImageBatchVarShape VarShapeBrightnessContrastInto(ImageBatchVarShape &dst, Image { auto op = CreateOperator(); runGuard(op, src, dst, brightness, contrast, brightnessShift, contrastCenter, pstream, - [&](Stream &stream, const nvcv::Tensor &brightnessArg, const nvcv::Tensor &contrastArg, - const nvcv::Tensor &brightnessShiftArg, const nvcv::Tensor &contrastCenterArg) { + [&op, &src, &dst](Stream &stream, const nvcv::Tensor &brightnessArg, const nvcv::Tensor &contrastArg, + const nvcv::Tensor &brightnessShiftArg, const nvcv::Tensor &contrastCenterArg) { op->submit(stream.cudaHandle(), src, dst, brightnessArg, contrastArg, brightnessShiftArg, contrastCenterArg); }); @@ -119,26 +123,51 @@ ImageBatchVarShape VarShapeBrightnessContrast(ImageBatchVarShape &src, std::opti return VarShapeBrightnessContrastInto(dst, src, brightness, contrast, brightnessShift, contrastCenter, pstream); } +Tensor BrightnessContrastScalarInto(Tensor &dst, Tensor &src, double brightness, double contrast, + double brightnessShift, double contrastCenter, bool clamp, + std::optional pstream) +{ + return UnaryElementwiseInto(dst, src, pstream, brightness, contrast, brightnessShift, + contrastCenter, clamp); +} + +Tensor BrightnessContrastScalar(Tensor &src, double brightness, double contrast, double brightnessShift, + double contrastCenter, bool clamp, std::optional pstream) +{ + return UnaryElementwiseTensor(src, pstream, brightness, contrast, brightnessShift, + contrastCenter, clamp); +} + +ImageBatchVarShape VarShapeBrightnessContrastScalarInto(ImageBatchVarShape &dst, ImageBatchVarShape &src, + double brightness, double contrast, double brightnessShift, + double contrastCenter, bool clamp, + std::optional pstream) +{ + return UnaryElementwiseInto(dst, src, pstream, brightness, contrast, brightnessShift, + contrastCenter, clamp); +} + +ImageBatchVarShape VarShapeBrightnessContrastScalar(ImageBatchVarShape &src, double brightness, double contrast, + double brightnessShift, double contrastCenter, bool clamp, + std::optional pstream) +{ + return UnaryElementwiseVarShape(src, pstream, brightness, contrast, brightnessShift, + contrastCenter, clamp); +} + } // namespace void ExportOpBrightnessContrast(py::module &m) { using namespace pybind11::literals; - py::options options; - options.disable_function_signatures(); - m.def("brightness_contrast", &BrightnessContrast, "src"_a, "brightness"_a = nullptr, "contrast"_a = nullptr, - "brightness_shift"_a = nullptr, "contrast_center"_a = nullptr, py::kw_only(), "stream"_a = nullptr, + m.def("brightness_contrast", NvtxTrace("cvcuda.brightness_contrast", &BrightnessContrast), "src"_a, + "brightness"_a = nullptr, "contrast"_a = nullptr, "brightness_shift"_a = nullptr, + "contrast_center"_a = nullptr, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - - cvcuda.brightness_contrast(src: cvcuda.Tensor, brightness: cvcuda.Tensor, contrast: cvcuda.Tensor, brightness_shift: cvcuda.Tensor, contrast_center: cvcuda.Tensor, stream: Optional[cvcuda.Stream] = None) -> cvcuda.Tensor - Adjusts the brightness and contrast of the images according to the formula: ``out = brightness_shift + brightness * (contrast_center + contrast * (in - contrast_center))``. - See also: - Refer to the CV-CUDA C API reference for the BrightnessContrast operator - for more details and usage examples. Args: src (cvcuda.Tensor): Input tensor. @@ -154,26 +183,20 @@ void ExportOpBrightnessContrast(py::module &m) contrast_center (cvcuda.Tensor, optional): Optional tensor describing contrast center. If specified, it must contain only 1 element. If not specified, the middle of the assumed input type range is used. For floats it is ``0.5``, for unsigned integer - types it is ``2 * (number_of_bits - 1)``, for signed integer types it is - ``2 * (number_of_bits - 2)``. + types it is ``2 ** (number_of_bits - 1)``, for signed integer types it is + ``2 ** (number_of_bits - 2)``. stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: cvcuda.Tensor: The output tensor. )pbdoc"); - m.def("brightness_contrast_into", &BrightnessContrastInto, "dst"_a, "src"_a, "brightness"_a = nullptr, - "contrast"_a = nullptr, "brightness_shift"_a = nullptr, "contrast_center"_a = nullptr, py::kw_only(), - "stream"_a = nullptr, + m.def("brightness_contrast_into", NvtxTrace("cvcuda.brightness_contrast_into", &BrightnessContrastInto), "dst"_a, + "src"_a, "brightness"_a = nullptr, "contrast"_a = nullptr, "brightness_shift"_a = nullptr, + "contrast_center"_a = nullptr, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - - cvcuda.brightness_contrast_into(dst: cvcuda.Tensor, src: cvcuda.Tensor, brightness: cvcuda.Tensor, contrast: cvcuda.Tensor, brightness_shift: cvcuda.Tensor, contrast_center: cvcuda.Tensor, stream: Optional[cvcuda.Stream] = None) - Adjusts the brightness and contrast of the images according to the formula: ``out = brightness_shift + brightness * (contrast_center + contrast * (in - contrast_center))``. - See also: - Refer to the CV-CUDA C API reference for the BrightnessContrast operator - for more details and usage examples. Args: src (cvcuda.Tensor): Input tensor. @@ -190,21 +213,19 @@ void ExportOpBrightnessContrast(py::module &m) contrast_center (cvcuda.Tensor, optional): Optional tensor describing contrast center. If specified, it must contain only 1 element. If not specified, the middle of the assumed input type range is used. For floats it is ``0.5``, for unsigned integer - types it is ``2 * (number_of_bits - 1)``, for signed integer types it is - ``2 * (number_of_bits - 2)``. + types it is ``2 ** (number_of_bits - 1)``, for signed integer types it is + ``2 ** (number_of_bits - 2)``. stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None + cvcuda.Tensor: The output tensor (same as dst). )pbdoc"); // VarShape variants - m.def("brightness_contrast", &VarShapeBrightnessContrast, "src"_a, "brightness"_a = nullptr, "contrast"_a = nullptr, - "brightness_shift"_a = nullptr, "contrast_center"_a = nullptr, py::kw_only(), "stream"_a = nullptr, + m.def("brightness_contrast", NvtxTrace("cvcuda.brightness_contrast", &VarShapeBrightnessContrast), "src"_a, + "brightness"_a = nullptr, "contrast"_a = nullptr, "brightness_shift"_a = nullptr, + "contrast_center"_a = nullptr, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - - cvcuda.brightness_contrast(src: cvcuda.ImageBatchVarShape, brightness: cvcuda.Tensor, contrast: cvcuda.Tensor, brightness_shift: cvcuda.Tensor, contrast_center: cvcuda.Tensor, stream: Optional[cvcuda.Stream] = None) -> cvcuda.ImageBatchVarShape - Adjusts the brightness and contrast of the images according to the formula: ``out = brightness_shift + brightness * (contrast_center + contrast * (in - contrast_center))``. @@ -212,9 +233,6 @@ void ExportOpBrightnessContrast(py::module &m) number of samples in the batch. - See also: - Refer to the CV-CUDA C API reference for the BrightnessContrast operator - for more details and usage examples. Args: src (cvcuda.ImageBatchVarShape): Input tensor. @@ -235,20 +253,17 @@ void ExportOpBrightnessContrast(py::module &m) images. If it contains a single element, the same value is used for all input images. If not specified, the middle of the assumed input type range is used. For floats it is ``0.5``, for unsigned integer types it is - ``2 * (number_of_bits - 1)``, for signed integer types it is - ``2 * (number_of_bits - 2)``. + ``2 ** (number_of_bits - 1)``, for signed integer types it is + ``2 ** (number_of_bits - 2)``. stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: cvcuda.ImageBatchVarShape: The output image batch. )pbdoc"); - m.def("brightness_contrast_into", &VarShapeBrightnessContrastInto, "dst"_a, "src"_a, "brightness"_a = nullptr, - "contrast"_a = nullptr, "brightness_shift"_a = nullptr, "contrast_center"_a = nullptr, py::kw_only(), - "stream"_a = nullptr, + m.def("brightness_contrast_into", NvtxTrace("cvcuda.brightness_contrast_into", &VarShapeBrightnessContrastInto), + "dst"_a, "src"_a, "brightness"_a = nullptr, "contrast"_a = nullptr, "brightness_shift"_a = nullptr, + "contrast_center"_a = nullptr, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - - cvcuda.brightness_contrast_into(dst: cvcuda.ImageBatchVarShape, src: cvcuda.ImageBatchVarShape, brightness: cvcuda.Tensor, contrast: cvcuda.Tensor, brightness_shift: cvcuda.Tensor, contrast_center: cvcuda.Tensor, stream: Optional[cvcuda.Stream] = None) - Adjusts the brightness and contrast of the images according to the formula: ``out = brightness_shift + brightness * (contrast_center + contrast * (in - contrast_center))``. @@ -256,9 +271,6 @@ void ExportOpBrightnessContrast(py::module &m) number of samples in the batch. - See also: - Refer to the CV-CUDA C API reference for the BrightnessContrast operator - for more details and usage examples. Args: src (cvcuda.ImageBatchVarShape): Input image batch containing one or more images. @@ -280,12 +292,90 @@ void ExportOpBrightnessContrast(py::module &m) images. If it contains a single element, the same value is used for all input images. If not specified, the middle of the assumed input type range is used. For floats it is ``0.5``, for unsigned integer types it is - ``2 * (number_of_bits - 1)``, for signed integer types it is - ``2 * (number_of_bits - 2)``. + ``2 ** (number_of_bits - 1)``, for signed integer types it is + ``2 ** (number_of_bits - 2)``. + stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + cvcuda.ImageBatchVarShape: The output image batch (same as dst). + )pbdoc"); + + // By-value variants + m.def("brightness_contrast", NvtxTrace("cvcuda.brightness_contrast", &BrightnessContrastScalar), "src"_a, + "brightness"_a, "contrast"_a, "brightness_shift"_a, "contrast_center"_a, py::kw_only(), "clamp"_a = false, + "stream"_a = nullptr, + R"pbdoc( + Adjusts brightness and contrast using one set of scalar parameters for every input image. + + Args: + src (cvcuda.Tensor): Input tensor. + brightness (float): Brightness multiplier. + contrast (float): Contrast multiplier. + brightness_shift (float): Brightness shift. + contrast_center (float): Contrast center. + clamp (bool, optional): Clamp to the nominal image range: ``[0, 1]`` for floating-point + output and ``[0, max]`` for integer output. Defaults to ``False``. + stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + cvcuda.Tensor: The output tensor. + )pbdoc"); + m.def("brightness_contrast_into", NvtxTrace("cvcuda.brightness_contrast_into", &BrightnessContrastScalarInto), + "dst"_a, "src"_a, "brightness"_a, "contrast"_a, "brightness_shift"_a, "contrast_center"_a, py::kw_only(), + "clamp"_a = false, "stream"_a = nullptr, + R"pbdoc( + Adjusts brightness and contrast into ``dst`` using scalar parameters. + + Args: + dst (cvcuda.Tensor): Output tensor. + src (cvcuda.Tensor): Input tensor. + brightness (float): Brightness multiplier. + contrast (float): Contrast multiplier. + brightness_shift (float): Brightness shift. + contrast_center (float): Contrast center. + clamp (bool, optional): Clamp to the nominal image range. Defaults to ``False``. + stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + cvcuda.Tensor: The output tensor (same as dst). + )pbdoc"); + m.def("brightness_contrast", NvtxTrace("cvcuda.brightness_contrast", &VarShapeBrightnessContrastScalar), "src"_a, + "brightness"_a, "contrast"_a, "brightness_shift"_a, "contrast_center"_a, py::kw_only(), "clamp"_a = false, + "stream"_a = nullptr, + R"pbdoc( + Adjusts brightness and contrast using one set of scalar parameters for every image in a batch. + + Args: + src (cvcuda.ImageBatchVarShape): Input image batch. + brightness (float): Brightness multiplier. + contrast (float): Contrast multiplier. + brightness_shift (float): Brightness shift. + contrast_center (float): Contrast center. + clamp (bool, optional): Clamp to the nominal image range. Defaults to ``False``. + stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + cvcuda.ImageBatchVarShape: The output image batch. + )pbdoc"); + m.def("brightness_contrast_into", + NvtxTrace("cvcuda.brightness_contrast_into", &VarShapeBrightnessContrastScalarInto), "dst"_a, "src"_a, + "brightness"_a, "contrast"_a, "brightness_shift"_a, "contrast_center"_a, py::kw_only(), "clamp"_a = false, + "stream"_a = nullptr, + R"pbdoc( + Adjusts brightness and contrast into ``dst`` using scalar parameters. + + Args: + dst (cvcuda.ImageBatchVarShape): Output image batch. + src (cvcuda.ImageBatchVarShape): Input image batch. + brightness (float): Brightness multiplier. + contrast (float): Contrast multiplier. + brightness_shift (float): Brightness shift. + contrast_center (float): Contrast center. + clamp (bool, optional): Clamp to the nominal image range. Defaults to ``False``. stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None + cvcuda.ImageBatchVarShape: The output image batch (same as dst). )pbdoc"); } diff --git a/python/mod_cvcuda/operators/OpCLAHE.cpp b/python/mod_cvcuda/operators/OpCLAHE.cpp new file mode 100644 index 000000000..39bfac8d1 --- /dev/null +++ b/python/mod_cvcuda/operators/OpCLAHE.cpp @@ -0,0 +1,181 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "Operators.hpp" +#include "VarShapeUtils.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace cvcudapy { + +namespace { + +class CLAHEError : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + +Tensor CLAHEInto(Tensor &output, Tensor &input, float clipLimit, const std::tuple &tileGridSize, + std::optional pstream) +{ + if (!pstream) + { + pstream = Stream::Current(); + } + + // HWC inputs (rank 3) have no N dim, so shape[0] is H — fall back to 1. + int32_t batchSize = (input.shape().size() == 4) ? (int32_t)input.shape()[0] : 1; + auto tx = std::get<0>(tileGridSize); + auto ty = std::get<1>(tileGridSize); + auto op = CreateOperator(batchSize, tx, ty); + + ResourceGuard guard(*pstream); + guard.add(LockMode::LOCK_MODE_READ, {input}); + guard.add(LockMode::LOCK_MODE_WRITE, {output}); + guard.add(LockMode::LOCK_MODE_READWRITE, {*op}); + + guard.run([&op, &pstream, &input, &output, &clipLimit]() + { op->submit(pstream->cudaHandle(), input, output, clipLimit); }); + return output; +} + +Tensor CLAHE(Tensor &input, float clipLimit, const std::tuple &tileGridSize, + std::optional pstream) +{ + Tensor output = Tensor::Create(input.shape(), input.dtype()); + return CLAHEInto(output, input, clipLimit, tileGridSize, pstream); +} + +ImageBatchVarShape CLAHEVarShapeInto(ImageBatchVarShape &output, ImageBatchVarShape &input, float clipLimit, + const std::tuple &tileGridSize, std::optional pstream) +{ + if (!pstream) + { + pstream = Stream::Current(); + } + + auto tx = std::get<0>(tileGridSize); + auto ty = std::get<1>(tileGridSize); + auto op = CreateOperator(input.capacity(), tx, ty); + + ResourceGuard guard(*pstream); + guard.add(LockMode::LOCK_MODE_READ, {input}); + guard.add(LockMode::LOCK_MODE_WRITE, {output}); + guard.add(LockMode::LOCK_MODE_READWRITE, {*op}); + + guard.run([&op, &pstream, &input, &output, &clipLimit]() + { op->submit(pstream->cudaHandle(), input, output, clipLimit); }); + return output; +} + +ImageBatchVarShape CLAHEVarShape(ImageBatchVarShape &input, float clipLimit, + const std::tuple &tileGridSize, std::optional pstream) +{ + auto format = input.uniqueFormat(); + if (!format) + { + throw CLAHEError("All images in input must have the same format."); + } + + ImageBatchVarShape output = CreateSameShapeImageBatch(input, format, input.numImages()); + + return CLAHEVarShapeInto(output, input, clipLimit, tileGridSize, pstream); +} + +} // namespace + +void ExportOpCLAHE(py::module &m) +{ + using namespace pybind11::literals; + + m.def("clahe", NvtxTrace("cvcuda.clahe", &CLAHE), "src"_a, "clip_limit"_a = 40.0, + "tile_grid_size"_a = std::tuple{8, 8}, py::kw_only(), "stream"_a = nullptr, R"pbdoc( + Executes the Contrast Limited Adaptive Histogram Equalization (CLAHE) operation on the given cuda stream. + + + Args: + src (cvcuda.Tensor): Input tensor containing one or more images. + clip_limit (float, optional): The clip limit for the CLAHE operation (default: 40.0). + tile_grid_size (Tuple[int, int], optional): The tile grid size for the CLAHE operation (default: (8, 8)). + stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + cvcuda.Tensor: The output tensor containing the result of the CLAHE operation. + + )pbdoc"); + + m.def("clahe_into", NvtxTrace("cvcuda.clahe_into", &CLAHEInto), "dst"_a, "src"_a, "clip_limit"_a = 40.0, + "tile_grid_size"_a = std::tuple{8, 8}, py::kw_only(), "stream"_a = nullptr, R"pbdoc( + Executes the Contrast Limited Adaptive Histogram Equalization (CLAHE) operation on the given cuda stream. + + + Args: + dst (cvcuda.Tensor): Output tensor containing the result of the operation. + src (cvcuda.Tensor): Input tensor containing one or more images. + clip_limit (float, optional): The clip limit for the CLAHE operation (default: 40.0). + tile_grid_size (Tuple[int, int], optional): The tile grid size for the CLAHE operation (default: (8, 8)). + stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + cvcuda.Tensor: The output tensor ``dst``. + + )pbdoc"); + + m.def("clahe", NvtxTrace("cvcuda.clahe", &CLAHEVarShape), "src"_a, "clip_limit"_a = 40.0, + "tile_grid_size"_a = std::tuple{8, 8}, py::kw_only(), "stream"_a = nullptr, R"pbdoc( + Executes the Contrast Limited Adaptive Histogram Equalization (CLAHE) operation on the given cuda stream. + + + Args: + src (cvcuda.ImageBatchVarShape): Input image batch containing one or more images. + clip_limit (float, optional): The clip limit for the CLAHE operation (default: 40.0). + tile_grid_size (Tuple[int, int], optional): The tile grid size for the CLAHE operation (default: (8, 8)). + stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + cvcuda.ImageBatchVarShape: The output image batch containing the result of the CLAHE operation. + + )pbdoc"); + + m.def("clahe_into", NvtxTrace("cvcuda.clahe_into", &CLAHEVarShapeInto), "dst"_a, "src"_a, "clip_limit"_a = 40.0, + "tile_grid_size"_a = std::tuple{8, 8}, py::kw_only(), "stream"_a = nullptr, R"pbdoc( + Executes the Contrast Limited Adaptive Histogram Equalization (CLAHE) operation on the given cuda stream. + + + Args: + dst (cvcuda.ImageBatchVarShape): Output image batch containing the result of the operation. + src (cvcuda.ImageBatchVarShape): Input image batch containing one or more images. + clip_limit (float, optional): The clip limit for the CLAHE operation (default: 40.0). + tile_grid_size (Tuple[int, int], optional): The tile grid size for the CLAHE operation (default: (8, 8)). + stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + cvcuda.ImageBatchVarShape: The output image batch ``dst``. + + )pbdoc"); +} + +} // namespace cvcudapy diff --git a/python/mod_cvcuda/operators/OpCenterCrop.cpp b/python/mod_cvcuda/operators/OpCenterCrop.cpp index 1e3a8a998..e3cbfa7b9 100644 --- a/python/mod_cvcuda/operators/OpCenterCrop.cpp +++ b/python/mod_cvcuda/operators/OpCenterCrop.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -50,7 +50,8 @@ Tensor CenterCropInto(Tensor &output, Tensor &input, const std::tuple nvcv::Size2D cropSizeArg{std::get<0>(cropSize), std::get<1>(cropSize)}; - center_crop->submit(pstream->cudaHandle(), input, output, cropSizeArg); + guard.run([¢er_crop, &pstream, &input, &output, &cropSizeArg]() + { center_crop->submit(pstream->cudaHandle(), input, output, cropSizeArg); }); return output; } @@ -85,13 +86,10 @@ void ExportOpCenterCrop(py::module &m) { using namespace pybind11::literals; - m.def("center_crop", &CenterCrop, "src"_a, "crop_size"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - + m.def("center_crop", NvtxTrace("cvcuda.center_crop", &CenterCrop), "src"_a, "crop_size"_a, py::kw_only(), + "stream"_a = nullptr, R"pbdoc( Executes the Center Crop operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Center Crop operator - for more details and usage examples. Args: src (cvcuda.Tensor): Input tensor containing one or more images. @@ -101,19 +99,13 @@ void ExportOpCenterCrop(py::module &m) Returns: cvcuda.Tensor: The output tensor. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("center_crop_into", &CenterCropInto, "dst"_a, "src"_a, "crop_size"_a, py::kw_only(), "stream"_a = nullptr, + m.def("center_crop_into", NvtxTrace("cvcuda.center_crop_into", &CenterCropInto), "dst"_a, "src"_a, "crop_size"_a, + py::kw_only(), "stream"_a = nullptr, R"pbdoc( - Executes the Center Crop operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Center Crop operator - for more details and usage examples. Args: dst (cvcuda.Tensor): Output tensor to store the result of the operation. @@ -122,11 +114,7 @@ void ExportOpCenterCrop(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.Tensor: The output tensor (same as dst). )pbdoc"); } diff --git a/python/mod_cvcuda/operators/OpChannelReorder.cpp b/python/mod_cvcuda/operators/OpChannelReorder.cpp index 1643b7309..1cf8932e8 100644 --- a/python/mod_cvcuda/operators/OpChannelReorder.cpp +++ b/python/mod_cvcuda/operators/OpChannelReorder.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,6 +16,7 @@ */ #include "Operators.hpp" +#include "VarShapeUtils.hpp" #include #include @@ -33,6 +34,33 @@ namespace cvcudapy { namespace { +Tensor ChannelReorderTensorInto(Tensor &output, Tensor &input, const std::vector &order, + std::optional pstream) +{ + if (!pstream) + { + pstream = Stream::Current(); + } + + auto channelReorder = CreateOperator(); + ResourceGuard guard(*pstream); + guard.add(LockMode::LOCK_MODE_READ, {input}); + guard.add(LockMode::LOCK_MODE_WRITE, {output}); + guard.add(LockMode::LOCK_MODE_NONE, {*channelReorder}); + guard.run( + [&channelReorder, &pstream, &input, &output, &order] { + channelReorder->submit(pstream->cudaHandle(), input, output, order.data(), + static_cast(order.size())); + }); + return output; +} + +Tensor ChannelReorderTensor(Tensor &input, const std::vector &order, std::optional pstream) +{ + Tensor output = Tensor::Create(input.shape(), input.dtype()); + return ChannelReorderTensorInto(output, input, order, pstream); +} + ImageBatchVarShape ChannelReorderVarShapeInto(ImageBatchVarShape &output, ImageBatchVarShape &input, Tensor &orders, std::optional pstream) { @@ -48,7 +76,8 @@ ImageBatchVarShape ChannelReorderVarShapeInto(ImageBatchVarShape &output, ImageB guard.add(LockMode::LOCK_MODE_WRITE, {output}); guard.add(LockMode::LOCK_MODE_NONE, {*chReorder}); - chReorder->submit(pstream->cudaHandle(), input, output, orders); + guard.run([&chReorder, &pstream, &input, &output, &orders]() + { chReorder->submit(pstream->cudaHandle(), input, output, orders); }); return output; } @@ -56,15 +85,7 @@ ImageBatchVarShape ChannelReorderVarShapeInto(ImageBatchVarShape &output, ImageB ImageBatchVarShape ChannelReorderVarShape(ImageBatchVarShape &input, Tensor &orders, std::optional fmt, std::optional pstream) { - ImageBatchVarShape output = ImageBatchVarShape::Create(input.capacity()); - - for (int i = 0; i < input.numImages(); ++i) - { - nvcv::ImageFormat format = fmt ? *fmt : input[i].format(); - nvcv::Size2D size = input[i].size(); - auto image = Image::Create(size, format); - output.pushBack(image); - } + ImageBatchVarShape output = fmt ? CreateSameShapeImageBatch(input, *fmt) : CreateSameShapeImageBatch(input); return ChannelReorderVarShapeInto(output, input, orders, pstream); } @@ -75,14 +96,41 @@ void ExportOpChannelReorder(py::module &m) { using namespace pybind11::literals; - m.def("channelreorder", &ChannelReorderVarShape, "src"_a, "order"_a, py::kw_only(), "format"_a = nullptr, - "stream"_a = nullptr, R"pbdoc( + m.def("channelreorder", NvtxTrace("cvcuda.channelreorder", &ChannelReorderTensor), "src"_a, "order"_a, + py::kw_only(), "stream"_a = nullptr, R"pbdoc( + Reorders the channels of a tensor using a host sequence. + + Each output channel ``c`` receives input channel ``order[c]``. Negative entries write zero; + repeated non-negative entries are allowed. The output has the same shape, layout, and data + type as the input. + + Args: + src (cvcuda.Tensor): Input tensor in HWC, NHWC, CHW, or NCHW layout. + order (Sequence[int]): One source-channel index per output channel. + stream (cvcuda.Stream, optional): CUDA stream on which to submit the operation. + + Returns: + cvcuda.Tensor: Reordered output tensor. + )pbdoc"); + + m.def("channelreorder_into", NvtxTrace("cvcuda.channelreorder_into", &ChannelReorderTensorInto), "dst"_a, "src"_a, + "order"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( + Reorders tensor channels into a caller-provided output tensor. + Args: + dst (cvcuda.Tensor): Output tensor with metadata identical to ``src``. + src (cvcuda.Tensor): Input tensor. + order (Sequence[int]): One source-channel index per output channel; negatives write zero. + stream (cvcuda.Stream, optional): CUDA stream on which to submit the operation. + + Returns: + cvcuda.Tensor: ``dst``. + )pbdoc"); + + m.def("channelreorder", NvtxTrace("cvcuda.channelreorder", &ChannelReorderVarShape), "src"_a, "order"_a, + py::kw_only(), "format"_a = nullptr, "stream"_a = nullptr, R"pbdoc( Executes the Channel Reorder operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Channel Reorder operator - for more details and usage examples. Args: src (cvcuda.ImageBatchVarShape): Input tensor containing one or more images. @@ -95,19 +143,12 @@ void ExportOpChannelReorder(py::module &m) Returns: cvcuda.ImageBatchVarShape: The output image batch. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("channelreorder_into", &ChannelReorderVarShapeInto, "dst"_a, "src"_a, "orders"_a, py::kw_only(), - "stream"_a = nullptr, R"pbdoc( - + m.def("channelreorder_into", NvtxTrace("cvcuda.channelreorder_into", &ChannelReorderVarShapeInto), "dst"_a, "src"_a, + "orders"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( Executes the Channel Reorder operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Channel Reorder operator - for more details and usage examples. Args: dst (cvcuda.ImageBatchVarShape): Output tensor to store the result of the operation. @@ -119,11 +160,7 @@ void ExportOpChannelReorder(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.ImageBatchVarShape: The output image batch (same as dst). )pbdoc"); } diff --git a/python/mod_cvcuda/operators/OpColorTwist.cpp b/python/mod_cvcuda/operators/OpColorTwist.cpp index 4b1602d34..b7e8d6c47 100644 --- a/python/mod_cvcuda/operators/OpColorTwist.cpp +++ b/python/mod_cvcuda/operators/OpColorTwist.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -42,7 +42,7 @@ inline ImageBatchVarShape batchLike(ImageBatchVarShape &src) ImageBatchVarShape dst = ImageBatchVarShape::Create(src.capacity()); for (int i = 0; i < src.numImages(); ++i) { - dst.pushBack(Image::Create(src[i].size(), src[i].format())); + dst.pushBackImage(Image::Create(src[i].size(), src[i].format())); } return dst; } @@ -60,13 +60,14 @@ auto runGuard(Op &op, Src &src, Dst &dst, const Tensor &twist, std::optional pstream) { auto op = CreateOperator(); - runGuard(op, src, dst, twist, pstream, [&](Stream &stream) { op->submit(stream.cudaHandle(), src, dst, twist); }); + runGuard(op, src, dst, twist, pstream, + [&op, &src, &dst, &twist](const Stream &stream) { op->submit(stream.cudaHandle(), src, dst, twist); }); return dst; } @@ -80,7 +81,8 @@ ImageBatchVarShape VarShapeColorTwistMatrixInto(ImageBatchVarShape &dst, ImageBa std::optional pstream) { auto op = CreateOperator(); - runGuard(op, src, dst, twist, pstream, [&](Stream &stream) { op->submit(stream.cudaHandle(), src, dst, twist); }); + runGuard(op, src, dst, twist, pstream, + [&op, &src, &dst, &twist](const Stream &stream) { op->submit(stream.cudaHandle(), src, dst, twist); }); return dst; } @@ -96,16 +98,11 @@ void ExportOpColorTwist(py::module &m) { using namespace pybind11::literals; - m.def("color_twist", &ColorTwistMatrix, "src"_a, "twist"_a, py::kw_only(), "stream"_a = nullptr, + m.def("color_twist", NvtxTrace("cvcuda.color_twist", &ColorTwistMatrix), "src"_a, "twist"_a, py::kw_only(), + "stream"_a = nullptr, R"pbdoc( - - cvcuda.color_twist(src: cvcuda.Tensor, twist: cvcuda.Tensor, stream: Optional[cvcuda.Stream] = None) -> cvcuda.Tensor - Transforms an image by applying affine transformation to the channels extent. - See Also: - Refer to the CV-CUDA C API reference for the ColorTwist operator for more details and - usage examples. Args: src (cvcuda.Tensor): Tensor corresponding to the input image. It must have @@ -117,16 +114,11 @@ void ExportOpColorTwist(py::module &m) Returns: cvcuda.Tensor: The output tensor. )pbdoc"); - m.def("color_twist_into", &ColorTwistMatrixInto, "dst"_a, "src"_a, "twist"_a, py::kw_only(), "stream"_a = nullptr, + m.def("color_twist_into", NvtxTrace("cvcuda.color_twist_into", &ColorTwistMatrixInto), "dst"_a, "src"_a, "twist"_a, + py::kw_only(), "stream"_a = nullptr, R"pbdoc( - - cvcuda.color_twist_into(dst: cvcuda.Tensor, src: cvcuda.Tensor, twist: cvcuda.Tensor, stream: Optional[cvcuda.Stream] = None) - Transforms an image by applying affine transformation to the channels extent. - See Also: - Refer to the CV-CUDA C API reference for the ColorTwist operator for more details and - usage examples. Args: dst (cvcuda.Tensor): Tensor corresponding to the output image. Must match the shape of @@ -138,20 +130,15 @@ void ExportOpColorTwist(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None + cvcuda.Tensor: The output tensor (same as dst). )pbdoc"); // VarShape variants - m.def("color_twist", &VarShapeColorTwistMatrix, "src"_a, "twist"_a, py::kw_only(), "stream"_a = nullptr, + m.def("color_twist", NvtxTrace("cvcuda.color_twist", &VarShapeColorTwistMatrix), "src"_a, "twist"_a, py::kw_only(), + "stream"_a = nullptr, R"pbdoc( - - cvcuda.color_twist(src: cvcuda.ImageBatchVarShape, twist: cvcuda.Tensor, stream: Optional[cvcuda.Stream] = None) -> cvcuda.ImageBatchVarShape - Transforms a batch of images by applying affine transformation to the channels extent. - See Also: - Refer to the CV-CUDA C API reference for the ColorTwist operator for more details and - usage examples. Args: src (cvcuda.ImageBatchVarShape): Input image batch. Each image must have either 3 or 4 @@ -165,20 +152,14 @@ void ExportOpColorTwist(py::module &m) cvcuda.ImageBatchVarShape: The output image batch. )pbdoc"); - m.def("color_twist_into", &VarShapeColorTwistMatrixInto, "dst"_a, "src"_a, "twist"_a, py::kw_only(), - "stream"_a = nullptr, + m.def("color_twist_into", NvtxTrace("cvcuda.color_twist_into", &VarShapeColorTwistMatrixInto), "dst"_a, "src"_a, + "twist"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - - cvcuda.color_twist_into(dst: cvcuda.ImageBatchVarShape, src: cvcuda.ImageBatchVarShape, twist: cvcuda.Tensor, stream: Optional[cvcuda.Stream] = None) - Transforms a batch of images by applying affine transformation to the channels extent. The twist should be a 2D tensor describing 3x4 affine transformation matrix or a 3D tensor specifying separate transformations for each sample in the input image batch. - See Also: - Refer to the CV-CUDA C API reference for the ColorTwist operator for more details and - usage examples. Args: dst (cvcuda.ImageBatchVarShape): Output image batch. The shapes of the output images @@ -191,7 +172,7 @@ void ExportOpColorTwist(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None + cvcuda.ImageBatchVarShape: The output image batch (same as dst). )pbdoc"); } diff --git a/python/mod_cvcuda/operators/OpComposite.cpp b/python/mod_cvcuda/operators/OpComposite.cpp index b1feeaf43..3bd4b9048 100644 --- a/python/mod_cvcuda/operators/OpComposite.cpp +++ b/python/mod_cvcuda/operators/OpComposite.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -28,6 +28,8 @@ #include #include +#include + namespace cvcudapy { namespace { @@ -46,15 +48,22 @@ Tensor CompositeInto(Tensor &output, Tensor &foreground, Tensor &background, Ten guard.add(LockMode::LOCK_MODE_WRITE, {output}); guard.add(LockMode::LOCK_MODE_NONE, {*composite}); - composite->submit(pstream->cudaHandle(), foreground, background, fgMask, output); + guard.run([&composite, &pstream, &foreground, &background, &fgMask, &output]() + { composite->submit(pstream->cudaHandle(), foreground, background, fgMask, output); }); return output; } Tensor Composite(Tensor &foreground, Tensor &background, Tensor &fgMask, int outChannels, std::optional pstream) { - Shape out_shape = CreateShape(foreground.shape()); - out_shape[out_shape.size() - 1] = outChannels; + Shape out_shape = CreateShape(foreground.shape()); + int channelIdx = foreground.layout().find('C'); + if (channelIdx < 0) + { + throw std::invalid_argument(util::ConcatString("Cannot infer Composite output shape for layout=", + std::string(foreground.layout().m_layout.data))); + } + out_shape[channelIdx] = outChannels; Tensor output = Tensor::Create(out_shape, foreground.dtype(), foreground.layout()); @@ -77,22 +86,50 @@ ImageBatchVarShape CompositeVarShapeInto(ImageBatchVarShape &output, ImageBatchV guard.add(LockMode::LOCK_MODE_WRITE, {output}); guard.add(LockMode::LOCK_MODE_NONE, {*composite}); - composite->submit(pstream->cudaHandle(), foreground, background, fgMask, output); + guard.run([&composite, &pstream, &foreground, &background, &fgMask, &output]() + { composite->submit(pstream->cudaHandle(), foreground, background, fgMask, output); }); return output; } +nvcv::ImageFormat CompositeVarShapeOutputFormat(nvcv::ImageFormat foregroundFormat, int outChannels) +{ + if (foregroundFormat.numChannels() == outChannels) + { + return foregroundFormat; + } + + if (outChannels == 4) + { + if (foregroundFormat == nvcv::FMT_RGB8) + { + return nvcv::FMT_RGBA8; + } + if (foregroundFormat == nvcv::FMT_RGB8p) + { + return nvcv::FMT_RGBA8p; + } + if (foregroundFormat == nvcv::FMT_BGR8) + { + return nvcv::FMT_BGRA8; + } + } + + throw std::invalid_argument( + util::ConcatString("Cannot infer Composite output format for outchannels=", outChannels)); +} + ImageBatchVarShape CompositeVarShape(ImageBatchVarShape &foreground, ImageBatchVarShape &background, - ImageBatchVarShape &fgMask, std::optional pstream) + ImageBatchVarShape &fgMask, int outChannels, std::optional pstream) { - ImageBatchVarShape output = ImageBatchVarShape::Create(foreground.numImages()); + ImageBatchVarShape output = ImageBatchVarShape::Create(foreground.capacity()); - nvcv::ImageFormat format = foreground.uniqueFormat(); + nvcv::ImageFormat format = CompositeVarShapeOutputFormat(foreground.uniqueFormat(), outChannels); for (auto img = foreground.begin(); img != foreground.end(); ++img) { auto newimg = Image::Create(img->size(), format); - output.pushBack(newimg); + output.pushBackImage(newimg); } return CompositeVarShapeInto(output, foreground, background, fgMask, pstream); @@ -103,45 +140,28 @@ ImageBatchVarShape CompositeVarShape(ImageBatchVarShape &foreground, ImageBatchV void ExportOpComposite(py::module &m) { using namespace pybind11::literals; - py::options options; - options.disable_function_signatures(); - - m.def("composite", &Composite, "foreground"_a, "background"_a, "fgmask"_a, "outchannels"_a, py::kw_only(), - "stream"_a = nullptr, R"pbdoc( - - cvcuda.composite(foreground: cvcuda.Tensor, background: cvcuda.Tensor, fgmask: cvcuda.Tensor, outchannels: int, stream: Optional[cvcuda.Stream] = None) -> cvcuda.Tensor + m.def("composite", NvtxTrace("cvcuda.composite", &Composite), "foreground"_a, "background"_a, "fgmask"_a, + "outchannels"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( Executes the Composite operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Composite operator - for more details and usage examples. Args: foreground (cvcuda.Tensor): Input tensor containing one or more foreground images. Each image is BGR (3-channel) 8-bit. background (cvcuda.Tensor): Input tensor containing one or more background images. Each image is BGR (3-channel) 8-bit. fgmask (cvcuda.Tensor): Input foreground mask tensor. Each mask image is grayscale 8-bit - outchannels (int): Specifies 3 channel for RGB and 4 channel for BGRA. + outchannels (int): Specifies 3 channels for RGB/BGR and 4 channels for RGBA/BGRA. stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: cvcuda.Tensor: The output tensor. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("composite_into", &CompositeInto, "dst"_a, "foreground"_a, "background"_a, "fgmask"_a, py::kw_only(), - "stream"_a = nullptr, R"pbdoc( - - cvcuda.composite_into(dst: cvcuda.Tensor, foreground: cvcuda.Tensor, background: cvcuda.Tensor, fgmask: cvcuda.Tensor, outchannels: int, stream: Optional[cvcuda.Stream] = None) - - Executes the Composite operation on the given cuda stream. + m.def("composite_into", NvtxTrace("cvcuda.composite_into", &CompositeInto), "dst"_a, "foreground"_a, "background"_a, + "fgmask"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( + Executes the Composite operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Composite operator - for more details and usage examples. Args: dst (cvcuda.Tensor): Output tensor to store the result of the operation. @@ -151,48 +171,30 @@ void ExportOpComposite(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.Tensor: The output tensor (same as dst). )pbdoc"); - m.def("composite", &CompositeVarShape, "foreground"_a, "background"_a, "fgmask"_a, py::kw_only(), - "stream"_a = nullptr, R"pbdoc( - - cvcuda.composite(foreground: cvcuda.ImageBatchVarShape, background: cvcuda.ImageBatchVarShape, fgmask: cvcuda.ImageBatchVarShape, outchannels: int, stream: Optional[cvcuda.Stream] = None) -> cvcuda.ImageBatchVarShape - - Executes the Composite operation on the given cuda stream. + m.def("composite", NvtxTrace("cvcuda.composite", &CompositeVarShape), "foreground"_a, "background"_a, "fgmask"_a, + "outchannels"_a = 3, py::kw_only(), "stream"_a = nullptr, R"pbdoc( + Executes the Composite operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Composite operator - for more details and usage examples. Args: foreground (cvcuda.ImageBatchVarShape): Input tensor containing one or more foreground images. Each image is BGR (3-channel) 8-bit. background (cvcuda.ImageBatchVarShape): Input tensor containing one or more background images. Each image is BGR (3-channel) 8-bit. fgmask (cvcuda.ImageBatchVarShape): Input foreground mask image batch. Each mask image is grayscale 8-bit. + outchannels (int): Specifies 3 channels for RGB/BGR and 4 channels for RGBA/BGRA. stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: cvcuda.ImageBatchVarShape: The output image batch. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("composite_into", &CompositeVarShapeInto, "dst"_a, "foreground"_a, "background"_a, "fgmask"_a, py::kw_only(), - "stream"_a = nullptr, R"pbdoc( - - cvcuda.composite_into(dst: cvcuda.ImageBatchVarShape, foreground: cvcuda.ImageBatchVarShape, background: cvcuda.ImageBatchVarShape, fgmask: cvcuda.ImageBatchVarShape, outchannels: int, stream: Optional[cvcuda.Stream] = None) - + m.def("composite_into", NvtxTrace("cvcuda.composite_into", &CompositeVarShapeInto), "dst"_a, "foreground"_a, + "background"_a, "fgmask"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( Executes the Composite operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Composite operator - for more details and usage examples. Args: dst (cvcuda.ImageBatchVarShape): Output image batch containing the result of the operation. @@ -202,11 +204,7 @@ void ExportOpComposite(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.ImageBatchVarShape: The output image batch (same as dst). )pbdoc"); } diff --git a/python/mod_cvcuda/operators/OpConv2D.cpp b/python/mod_cvcuda/operators/OpConv2D.cpp index b9dc5df43..953890c4d 100644 --- a/python/mod_cvcuda/operators/OpConv2D.cpp +++ b/python/mod_cvcuda/operators/OpConv2D.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,6 +16,7 @@ */ #include "Operators.hpp" +#include "VarShapeUtils.hpp" #include #include @@ -48,7 +49,8 @@ ImageBatchVarShape Conv2DVarShapeInto(ImageBatchVarShape &output, ImageBatchVarS guard.add(LockMode::LOCK_MODE_WRITE, {output}); guard.add(LockMode::LOCK_MODE_NONE, {*conv2D}); - conv2D->submit(pstream->cudaHandle(), input, output, kernel, kernel_anchor, border); + guard.run([&conv2D, &pstream, &input, &output, &kernel, &kernel_anchor, &border]() + { conv2D->submit(pstream->cudaHandle(), input, output, kernel, kernel_anchor, border); }); return output; } @@ -56,15 +58,7 @@ ImageBatchVarShape Conv2DVarShapeInto(ImageBatchVarShape &output, ImageBatchVarS ImageBatchVarShape Conv2DVarShape(ImageBatchVarShape &input, ImageBatchVarShape &kernel, Tensor &kernel_anchor, NVCVBorderType border, std::optional pstream) { - ImageBatchVarShape output = ImageBatchVarShape::Create(input.capacity()); - - for (int i = 0; i < input.numImages(); ++i) - { - nvcv::ImageFormat format = input[i].format(); - nvcv::Size2D size = input[i].size(); - auto image = Image::Create(size, format); - output.pushBack(image); - } + ImageBatchVarShape output = CreateSameShapeImageBatch(input); return Conv2DVarShapeInto(output, input, kernel, kernel_anchor, border, pstream); } @@ -75,14 +69,10 @@ void ExportOpConv2D(py::module &m) { using namespace pybind11::literals; - m.def("conv2d", &Conv2DVarShape, "src"_a, "kernel"_a, "kernel_anchor"_a, + m.def("conv2d", NvtxTrace("cvcuda.conv2d", &Conv2DVarShape), "src"_a, "kernel"_a, "kernel_anchor"_a, "border"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - Executes the Convolve 2D operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Convolve 2D operator - for more details and usage examples. Args: src (cvcuda.ImageBatchVarShape): Input image batch containing one or more images. @@ -96,19 +86,13 @@ void ExportOpConv2D(py::module &m) Returns: cvcuda.ImageBatchVarShape: The output image batch. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("conv2d_into", &Conv2DVarShapeInto, "dst"_a, "src"_a, "kernel"_a, "kernel_anchor"_a, - "border"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - + m.def("conv2d_into", NvtxTrace("cvcuda.conv2d_into", &Conv2DVarShapeInto), "dst"_a, "src"_a, "kernel"_a, + "kernel_anchor"_a, "border"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, py::kw_only(), "stream"_a = nullptr, + R"pbdoc( Executes the Convolve 2D operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Convolve 2D operator - for more details and usage examples. Args: dst (cvcuda.ImageBatchVarShape): Output image batch to store the result of the operation. @@ -121,11 +105,7 @@ void ExportOpConv2D(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.ImageBatchVarShape: The output image batch (same as dst). )pbdoc"); } diff --git a/python/mod_cvcuda/operators/OpConvertTo.cpp b/python/mod_cvcuda/operators/OpConvertTo.cpp index e63b12e5a..11a688799 100644 --- a/python/mod_cvcuda/operators/OpConvertTo.cpp +++ b/python/mod_cvcuda/operators/OpConvertTo.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,6 +19,7 @@ #include #include +#include #include #include #include @@ -26,7 +27,8 @@ namespace cvcudapy { namespace { -Tensor ConvertToInto(Tensor &output, Tensor &input, float scale, float offset, std::optional pstream) +Tensor ConvertToInto(Tensor &output, Tensor &input, float scale, float offset, NVCVRoundMode round, + std::optional pstream) { if (!pstream) { @@ -40,16 +42,18 @@ Tensor ConvertToInto(Tensor &output, Tensor &input, float scale, float offset, s guard.add(LockMode::LOCK_MODE_WRITE, {output}); guard.add(LockMode::LOCK_MODE_NONE, {*cvt}); - cvt->submit(pstream->cudaHandle(), input, output, scale, offset); + guard.run([&cvt, &pstream, &input, &output, &scale, &offset, &round]() + { cvt->submit(pstream->cudaHandle(), input, output, scale, offset, round); }); return std::move(output); } -Tensor ConvertTo(Tensor &input, nvcv::DataType dtype, float scale, float offset, std::optional pstream) +Tensor ConvertTo(Tensor &input, nvcv::DataType dtype, float scale, float offset, NVCVRoundMode round, + std::optional pstream) { Tensor output = Tensor::Create(input.shape(), dtype); - return ConvertToInto(output, input, scale, offset, pstream); + return ConvertToInto(output, input, scale, offset, round, pstream); } } // namespace @@ -58,51 +62,42 @@ void ExportOpConvertTo(py::module &m) { using namespace pybind11::literals; - m.def("convertto", &ConvertTo, "src"_a, "dtype"_a, "scale"_a = 1, "offset"_a = 0, py::kw_only(), - "stream"_a = nullptr, R"pbdoc( - + m.def("convertto", NvtxTrace("cvcuda.convertto", &ConvertTo), "src"_a, "dtype"_a, "scale"_a = 1, "offset"_a = 0, + py::kw_only(), "round"_a = NVCV_ROUND_NEAREST, "stream"_a = nullptr, R"pbdoc( Executes the Convert To operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Convert To operator - for more details and usage examples. Args: src (cvcuda.Tensor): Input tensor containing one or more images. scale (float, optional): Scalar for output data. offset (float, optional): Offset for the data. + round (cvcuda.Round, optional): Rounding mode used for integer outputs. Defaults to + cvcuda.Round.NEAREST (round to nearest); use cvcuda.Round.TRUNCATE to truncate + toward zero. Has no effect for floating-point outputs. stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: cvcuda.Tensor: The output tensor. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("convertto_into", &ConvertToInto, "dst"_a, "src"_a, "scale"_a = 1, "offset"_a = 0, py::kw_only(), - "stream"_a = nullptr, R"pbdoc( - + m.def("convertto_into", NvtxTrace("cvcuda.convertto_into", &ConvertToInto), "dst"_a, "src"_a, "scale"_a = 1, + "offset"_a = 0, py::kw_only(), "round"_a = NVCV_ROUND_NEAREST, "stream"_a = nullptr, R"pbdoc( Executes the Convert To operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Convert To operator - for more details and usage examples. Args: dst (cvcuda.Tensor): Output tensor to store the result of the operation. src (cvcuda.Tensor): Input tensor containing one or more images. scale (float, optional): Scalar for output data. offset (float, optional): Offset for the data. + round (cvcuda.Round, optional): Rounding mode used for integer outputs. Defaults to + cvcuda.Round.NEAREST (round to nearest); use cvcuda.Round.TRUNCATE to truncate + toward zero. Has no effect for floating-point outputs. stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.Tensor: The output tensor (same as dst). )pbdoc"); } diff --git a/python/mod_cvcuda/operators/OpCopyMakeBorder.cpp b/python/mod_cvcuda/operators/OpCopyMakeBorder.cpp index 4a46f441e..812387a66 100644 --- a/python/mod_cvcuda/operators/OpCopyMakeBorder.cpp +++ b/python/mod_cvcuda/operators/OpCopyMakeBorder.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,34 +23,52 @@ #include #include #include +#include #include #include #include #include #include +#include + namespace cvcudapy { namespace { -Tensor CopyMakeBorderInto(Tensor &output, Tensor &input, NVCVBorderType borderMode, - const std::vector &borderValue, int top, int left, std::optional pstream) + +class CopyMakeBorderError : public std::runtime_error { - if (!pstream) - { - pstream = Stream::Current(); - } +public: + using std::runtime_error::runtime_error; +}; +float4 GetBorderValue(const std::vector &borderValue) +{ size_t bValueDims = borderValue.size(); if (bValueDims > 4) { - throw std::runtime_error( - util::FormatString("Channels of borderValue should <= 4, current is '%lu'", bValueDims)); + throw CopyMakeBorderError( + util::ConcatString("Channels of borderValue should <= 4, current is '", bValueDims, "'")); } + float4 bValue; - for (size_t i = 0; i < 4; i++) + for (int i = 0; i < 4; i++) { - nvcv::cuda::GetElement(bValue, i) = bValueDims > i ? borderValue[i] : 0.f; + const auto valueIdx = static_cast(i); + nvcv::cuda::GetElement(bValue, i) = bValueDims > valueIdx ? borderValue[valueIdx] : 0.f; } + return bValue; +} + +Tensor CopyMakeBorderInto(Tensor &output, Tensor &input, NVCVBorderType borderMode, + const std::vector &borderValue, int top, int left, std::optional pstream) +{ + if (!pstream) + { + pstream = Stream::Current(); + } + + float4 bValue = GetBorderValue(borderValue); auto copyMakeBorder = CreateOperator(); @@ -59,7 +77,8 @@ Tensor CopyMakeBorderInto(Tensor &output, Tensor &input, NVCVBorderType borderMo guard.add(LockMode::LOCK_MODE_WRITE, {output}); guard.add(LockMode::LOCK_MODE_NONE, {*copyMakeBorder}); - copyMakeBorder->submit(pstream->cudaHandle(), input, output, top, left, borderMode, bValue); + guard.run([©MakeBorder, &pstream, &input, &output, &top, &left, &borderMode, &bValue]() + { copyMakeBorder->submit(pstream->cudaHandle(), input, output, top, left, borderMode, bValue); }); return output; } @@ -67,47 +86,31 @@ Tensor CopyMakeBorderInto(Tensor &output, Tensor &input, NVCVBorderType borderMo Tensor CopyMakeBorder(Tensor &input, NVCVBorderType borderMode, const std::vector &borderValue, int top, int bottom, int left, int right, std::optional pstream) { - Shape out_shape = CreateShape(input.shape()); - int cdim = out_shape.size() - 1; - out_shape[cdim - 2] = out_shape[cdim - 2].cast() + top + bottom; - out_shape[cdim - 1] = out_shape[cdim - 1].cast() + left + right; + auto info = nvcv::TensorLayoutInfoImage::Create(input.layout()); + if (!info) + { + throw CopyMakeBorderError("Non-supported tensor layout"); + } + + Shape out_shape = CreateShape(input.shape()); + out_shape[info->idxHeight()] = out_shape[info->idxHeight()].cast() + top + bottom; + out_shape[info->idxWidth()] = out_shape[info->idxWidth()].cast() + left + right; Tensor output = Tensor::Create(out_shape, input.dtype(), input.layout()); return CopyMakeBorderInto(output, input, borderMode, borderValue, top, left, pstream); } +template +Output &VarShapeCopyMakeBorderSubmit(Output &output, ImageBatchVarShape &input, NVCVBorderType borderMode, + const std::vector &borderValue, Tensor &top, Tensor &left, + std::optional pstream); + Tensor VarShapeCopyMakeBorderStackInto(Tensor &output, ImageBatchVarShape &input, NVCVBorderType borderMode, const std::vector &borderValue, Tensor &top, Tensor &left, std::optional pstream) { - if (!pstream) - { - pstream = Stream::Current(); - } - - size_t bValueDims = borderValue.size(); - if (bValueDims > 4) - { - throw std::runtime_error( - util::FormatString("Channels of borderValue should <= 4, current is '%lu'", bValueDims)); - } - float4 bValue; - for (size_t i = 0; i < 4; i++) - { - nvcv::cuda::GetElement(bValue, i) = bValueDims > i ? borderValue[i] : 0.f; - } - - auto copyMakeBorder = CreateOperator(); - - ResourceGuard guard(*pstream); - guard.add(LockMode::LOCK_MODE_READ, {input, top, left}); - guard.add(LockMode::LOCK_MODE_WRITE, {output}); - guard.add(LockMode::LOCK_MODE_NONE, {*copyMakeBorder}); - - copyMakeBorder->submit(pstream->cudaHandle(), input, output, top, left, borderMode, bValue); - - return output; + return VarShapeCopyMakeBorderSubmit(output, input, borderMode, borderValue, top, left, pstream); } Tensor VarShapeCopyMakeBorderStack(ImageBatchVarShape &input, NVCVBorderType borderMode, @@ -117,7 +120,7 @@ Tensor VarShapeCopyMakeBorderStack(ImageBatchVarShape &input, NVCVBorderType bor auto format = input.uniqueFormat(); if (!format) { - throw std::runtime_error("All images in input must have the same format."); + throw CopyMakeBorderError("All images in input must have the same format."); } Tensor output = Tensor::CreateForImageBatch(input.numImages(), {out_width, out_height}, format); @@ -128,23 +131,21 @@ Tensor VarShapeCopyMakeBorderStack(ImageBatchVarShape &input, NVCVBorderType bor ImageBatchVarShape VarShapeCopyMakeBorderInto(ImageBatchVarShape &output, ImageBatchVarShape &input, NVCVBorderType borderMode, const std::vector &borderValue, Tensor &top, Tensor &left, std::optional pstream) +{ + return VarShapeCopyMakeBorderSubmit(output, input, borderMode, borderValue, top, left, pstream); +} + +template +Output &VarShapeCopyMakeBorderSubmit(Output &output, ImageBatchVarShape &input, NVCVBorderType borderMode, + const std::vector &borderValue, Tensor &top, Tensor &left, + std::optional pstream) { if (!pstream) { pstream = Stream::Current(); } - size_t bValueDims = borderValue.size(); - if (bValueDims > 4) - { - throw std::runtime_error( - util::FormatString("Channels of borderValue should <= 4, current is '%lu'", bValueDims)); - } - float4 bValue; - for (size_t i = 0; i < 4; i++) - { - nvcv::cuda::GetElement(bValue, i) = bValueDims > i ? borderValue[i] : 0.f; - } + float4 bValue = GetBorderValue(borderValue); auto copyMakeBorder = CreateOperator(); @@ -153,7 +154,8 @@ ImageBatchVarShape VarShapeCopyMakeBorderInto(ImageBatchVarShape &output, ImageB guard.add(LockMode::LOCK_MODE_WRITE, {output}); guard.add(LockMode::LOCK_MODE_NONE, {*copyMakeBorder}); - copyMakeBorder->submit(pstream->cudaHandle(), input, output, top, left, borderMode, bValue); + guard.run([©MakeBorder, &pstream, &input, &output, &top, &left, &borderMode, &bValue]() + { copyMakeBorder->submit(pstream->cudaHandle(), input, output, top, left, borderMode, bValue); }); return output; } @@ -165,28 +167,28 @@ ImageBatchVarShape VarShapeCopyMakeBorder(ImageBatchVarShape &input, NVCVBorderT { if (int(out_heights.size()) != input.numImages()) { - throw std::runtime_error(util::FormatString("out_heights.size() != input.numImages, %lu != %d", - out_heights.size(), input.numImages())); + throw CopyMakeBorderError(util::ConcatString("out_heights.size() != input.numImages, ", out_heights.size(), + " != ", input.numImages())); } if (int(out_widths.size()) != input.numImages()) { - throw std::runtime_error(util::FormatString("out_widths.size() != input.numImages, %lu != %d", - out_heights.size(), input.numImages())); + throw CopyMakeBorderError( + util::ConcatString("out_widths.size() != input.numImages, ", out_widths.size(), " != ", input.numImages())); } ImageBatchVarShape output = ImageBatchVarShape::Create(input.numImages()); auto format = input.uniqueFormat(); if (!format) { - throw std::runtime_error("All images in input must have the same format."); + throw CopyMakeBorderError("All images in input must have the same format."); } for (int i = 0; i < input.numImages(); ++i) { nvcv::Size2D size = {out_widths[i], out_heights[i]}; auto img = Image::Create(size, format); - output.pushBack(img); + output.pushBackImage(img); } return VarShapeCopyMakeBorderInto(output, input, borderMode, borderValue, top, left, pstream); } @@ -197,20 +199,11 @@ void ExportOpCopyMakeBorder(py::module &m) { using namespace pybind11::literals; - py::options options; - options.disable_function_signatures(); - - m.def("copymakeborder", &CopyMakeBorder, "src"_a, "border_mode"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, - "border_value"_a = std::vector(), py::kw_only(), "top"_a, "bottom"_a, "left"_a, "right"_a, - "stream"_a = nullptr, R"pbdoc( - - cvcuda.copymakeborder(src: cvcuda.Tensor, border_mode: cvcuda.Border = cvcuda.Border.CONSTANT, border_value: List[float], top: int, bottom: int, right: int, stream: Optional[cvcuda.Stream] = None) -> cvcuda.Tensor - - Executes the Copy Make Border operation on the given cuda stream. + m.def("copymakeborder", NvtxTrace("cvcuda.copymakeborder", &CopyMakeBorder), "src"_a, + "border_mode"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, "border_value"_a = std::vector(), + py::kw_only(), "top"_a, "bottom"_a, "left"_a, "right"_a, "stream"_a = nullptr, R"pbdoc( + Executes the Copy Make Border operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Copy Make Border operator - for more details and usage examples. Args: src (cvcuda.Tensor): Input tensor containing one or more images. @@ -226,22 +219,13 @@ void ExportOpCopyMakeBorder(py::module &m) Returns: cvcuda.Tensor: The output tensor. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("copymakeborder_into", &CopyMakeBorderInto, "dst"_a, "src"_a, + m.def("copymakeborder_into", NvtxTrace("cvcuda.copymakeborder_into", &CopyMakeBorderInto), "dst"_a, "src"_a, "border_mode"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, "border_value"_a = std::vector(), py::kw_only(), "top"_a, "left"_a, "stream"_a = nullptr, R"pbdoc( - - cvcuda.copymakeborder_into(dst: cvcuda.Tensor, src: cvcuda.Tensor, border_mode: cvcuda.Border = cvcuda.Border.CONSTANT, border_value: List[float], top: int, bottom: int, right: int, stream: Optional[cvcuda.Stream] = None) - Executes the Copy Make Border operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Copy Make Border operator - for more details and usage examples. Args: dst (cvcuda.Tensor): Output tensor to store the result of the operation. @@ -254,24 +238,14 @@ void ExportOpCopyMakeBorder(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.Tensor: The output tensor (same as dst). )pbdoc"); - m.def("copymakeborderstack", &VarShapeCopyMakeBorderStack, "src"_a, + m.def("copymakeborderstack", NvtxTrace("cvcuda.copymakeborderstack", &VarShapeCopyMakeBorderStack), "src"_a, "border_mode"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, "border_value"_a = std::vector(), py::kw_only(), "top"_a, "left"_a, "out_height"_a, "out_width"_a, "stream"_a = nullptr, R"pbdoc( - - cvcuda.copymakeborderstack(src: cvcuda.ImageBatchVarShape, border_mode: cvcuda.Border = cvcuda.Border.CONSTANT, border_value: List[float], top: int, bottom: int, right: int, stream: Optional[cvcuda.Stream] = None) -> cvcuda.Tensor - Executes the Copy Make Border Stack operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Copy Make Border Stack operator - for more details and usage examples. Args: src (cvcuda.ImageBatchVarShape): Input image batch containing one or more images. @@ -287,22 +261,13 @@ void ExportOpCopyMakeBorder(py::module &m) Returns: cvcuda.Tensor: The output images. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("copymakeborderstack_into", &VarShapeCopyMakeBorderStackInto, "dst"_a, "src"_a, - "border_mode"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, "border_value"_a = std::vector(), - py::kw_only(), "top"_a, "left"_a, "stream"_a = nullptr, R"pbdoc( - - cvcuda.copymakeborderstack_into(dst: cvcuda.Tensor, src: cvcuda.ImageBatchVarShape, border_mode: cvcuda.Border = cvcuda.Border.CONSTANT, border_value: List[float], top: int, bottom: int, right: int, stream: Optional[cvcuda.Stream] = None) - + m.def("copymakeborderstack_into", NvtxTrace("cvcuda.copymakeborderstack_into", &VarShapeCopyMakeBorderStackInto), + "dst"_a, "src"_a, "border_mode"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, + "border_value"_a = std::vector(), py::kw_only(), "top"_a, "left"_a, "stream"_a = nullptr, R"pbdoc( Executes the Copy Make Border Stack operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Copy Make Border Stack operator - for more details and usage examples. Args: dst (cvcuda.Tensor): Output tensor to store the result of the operation. @@ -317,24 +282,14 @@ void ExportOpCopyMakeBorder(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.Tensor: The output tensor (same as dst). )pbdoc"); - m.def("copymakeborder", &VarShapeCopyMakeBorder, "src"_a, "border_mode"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, - "border_value"_a = std::vector(), py::kw_only(), "top"_a, "left"_a, "out_heights"_a, "out_widths"_a, - "stream"_a = nullptr, R"pbdoc( - - cvcuda.copymakeborder(src: cvcuda.ImageBatchVarShape, border_mode: cvcuda.Border = cvcuda.Border.CONSTANT, border_value: List[float], top: int, bottom: int, right: int, stream: Optional[cvcuda.Stream] = None) -> cvcuda.ImageBatchVarShape - + m.def("copymakeborder", NvtxTrace("cvcuda.copymakeborder", &VarShapeCopyMakeBorder), "src"_a, + "border_mode"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, "border_value"_a = std::vector(), + py::kw_only(), "top"_a, "left"_a, "out_heights"_a, "out_widths"_a, "stream"_a = nullptr, R"pbdoc( Executes the Copy Make Border operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Copy Make Border operator - for more details and usage examples. Args: src (cvcuda.ImageBatchVarShape): Input image batch containing one or more images. @@ -350,22 +305,13 @@ void ExportOpCopyMakeBorder(py::module &m) Returns: cvcuda.ImageBatchVarShape: The output image batch. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("copymakeborder_into", &VarShapeCopyMakeBorderInto, "dst"_a, "src"_a, + m.def("copymakeborder_into", NvtxTrace("cvcuda.copymakeborder_into", &VarShapeCopyMakeBorderInto), "dst"_a, "src"_a, "border_mode"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, "border_value"_a = std::vector(), py::kw_only(), "top"_a, "left"_a, "stream"_a = nullptr, R"pbdoc( - - cvcuda.copymakeborder_into(dst: cvcuda.ImageBatchVarShape, src: cvcuda.ImageBatchVarShape, border_mode: cvcuda.Border = cvcuda.Border.CONSTANT, border_value: List[float], top: int, bottom: int, right: int, stream: Optional[cvcuda.Stream] = None) - Executes the Copy Make Border operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Copy Make Border operator - for more details and usage examples. Args: dst (cvcuda.ImageBatchVarShape): Output image batch containing the result of the operation. @@ -380,11 +326,7 @@ void ExportOpCopyMakeBorder(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.ImageBatchVarShape: The output image batch (same as dst). )pbdoc"); } diff --git a/python/mod_cvcuda/operators/OpCropFlipNormalizeReformat.cpp b/python/mod_cvcuda/operators/OpCropFlipNormalizeReformat.cpp index 8298aa177..734913cc2 100644 --- a/python/mod_cvcuda/operators/OpCropFlipNormalizeReformat.cpp +++ b/python/mod_cvcuda/operators/OpCropFlipNormalizeReformat.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -29,24 +29,20 @@ namespace cvcudapy { -namespace { - -} // namespace - namespace { Tensor CropFlipNormalizeReformatInto(Tensor &output, ImageBatchVarShape &input, Tensor &cropRect, Tensor &flipCode, Tensor &base, Tensor &scale, float globalScale, float globalShift, float epsilon, std::optional flags, NVCVBorderType borderMode, float borderValue, std::optional pstream) { - if (!pstream) + if (!pstream.has_value()) { pstream = Stream::Current(); } auto op = CreateOperator(); - if (!flags) + if (!flags.has_value()) { flags = 0; } @@ -55,8 +51,13 @@ Tensor CropFlipNormalizeReformatInto(Tensor &output, ImageBatchVarShape &input, guard.add(LockMode::LOCK_MODE_READ, {input, cropRect, flipCode, base, scale}); guard.add(LockMode::LOCK_MODE_WRITE, {output}); guard.add(LockMode::LOCK_MODE_NONE, {*op}); - op->submit(pstream->cudaHandle(), input, output, cropRect, borderMode, borderValue, flipCode, base, scale, - globalScale, globalShift, epsilon, *flags); + guard.run( + [&op, &pstream, &input, &output, &cropRect, &borderMode, &borderValue, &flipCode, &base, &scale, &globalScale, + &globalShift, &epsilon, &flags]() + { + op->submit(pstream->cudaHandle(), input, output, cropRect, borderMode, borderValue, flipCode, base, scale, + globalScale, globalShift, epsilon, *flags); + }); return output; } @@ -83,16 +84,13 @@ void ExportOpCropFlipNormalizeReformat(py::module &m) float defGlobalShift = 0; float defEpsilon = 0; - m.def("crop_flip_normalize_reformat", &CropFlipNormalizeReformat, "src"_a, "out_shape"_a, "out_dtype"_a, - "out_layout"_a, "rect"_a, "flip_code"_a, "base"_a, "scale"_a, "globalscale"_a = defGlobalScale, - "globalshift"_a = defGlobalShift, "epsilon"_a = defEpsilon, "flags"_a = std::nullopt, - "border"_a = NVCV_BORDER_CONSTANT, "bvalue"_a = 0, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - + m.def("crop_flip_normalize_reformat", NvtxTrace("cvcuda.crop_flip_normalize_reformat", &CropFlipNormalizeReformat), + "src"_a, "out_shape"_a, "out_dtype"_a, "out_layout"_a, "rect"_a, "flip_code"_a, "base"_a, "scale"_a, + "globalscale"_a = defGlobalScale, "globalshift"_a = defGlobalShift, "epsilon"_a = defEpsilon, + "flags"_a = std::nullopt, "border"_a = NVCV_BORDER_CONSTANT, "bvalue"_a = 0, py::kw_only(), + "stream"_a = nullptr, R"pbdoc( Executes the CropFlipNormalizeReformat operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the CropFlipNormalizeReformat operator - for more details and usage examples. Args: src (cvcuda.ImageBatchVarShape): Input image batch containing one or more images. @@ -118,23 +116,17 @@ void ExportOpCropFlipNormalizeReformat(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None + cvcuda.Tensor: The output tensor. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("crop_flip_normalize_reformat_into", &CropFlipNormalizeReformatInto, "dst"_a, "src"_a, "rect"_a, - "flip_code"_a, "base"_a, "scale"_a, "globalscale"_a = defGlobalScale, "globalshift"_a = defGlobalShift, - "epsilon"_a = defEpsilon, "flags"_a = std::nullopt, "border"_a = NVCV_BORDER_CONSTANT, "bvalue"_a = 0, - py::kw_only(), "stream"_a = nullptr, R"pbdoc( - + m.def("crop_flip_normalize_reformat_into", + NvtxTrace("cvcuda.crop_flip_normalize_reformat_into", &CropFlipNormalizeReformatInto), "dst"_a, "src"_a, + "rect"_a, "flip_code"_a, "base"_a, "scale"_a, "globalscale"_a = defGlobalScale, + "globalshift"_a = defGlobalShift, "epsilon"_a = defEpsilon, "flags"_a = std::nullopt, + "border"_a = NVCV_BORDER_CONSTANT, "bvalue"_a = 0, py::kw_only(), "stream"_a = nullptr, R"pbdoc( Executes the CropFlipNormalizeReformat operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the CropFlipNormalizeReformat operator - for more details and usage examples. Args: dst (cvcuda.ImageBatchVarShape): Output image batch containing the result of the operation. @@ -158,11 +150,7 @@ void ExportOpCropFlipNormalizeReformat(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.Tensor: The output tensor (same as dst). )pbdoc"); } diff --git a/python/mod_cvcuda/operators/OpCustomCrop.cpp b/python/mod_cvcuda/operators/OpCustomCrop.cpp index 801b6cca8..74c26ce2d 100644 --- a/python/mod_cvcuda/operators/OpCustomCrop.cpp +++ b/python/mod_cvcuda/operators/OpCustomCrop.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -42,7 +42,8 @@ Tensor CustomCropInto(Tensor &output, Tensor &input, const NVCVRectI &rcCrop, st guard.add(LockMode::LOCK_MODE_WRITE, {output}); guard.add(LockMode::LOCK_MODE_NONE, {*crop}); - crop->submit(pstream->cudaHandle(), input, output, rcCrop); + guard.run([&crop, &pstream, &input, &output, &rcCrop]() + { crop->submit(pstream->cudaHandle(), input, output, rcCrop); }); return std::move(output); } @@ -88,13 +89,10 @@ void ExportOpCustomCrop(py::module &m) { using namespace pybind11::literals; - m.def("customcrop", &CustomCrop, "src"_a, "rect"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - + m.def("customcrop", NvtxTrace("cvcuda.customcrop", &CustomCrop), "src"_a, "rect"_a, py::kw_only(), + "stream"_a = nullptr, R"pbdoc( Executes the Custom Crop operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Custom Crop operator - for more details and usage examples. Args: src (cvcuda.Tensor): Input tensor containing one or more images. @@ -104,18 +102,12 @@ void ExportOpCustomCrop(py::module &m) Returns: cvcuda.Tensor: The output tensor. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("customcrop_into", &CustomCropInto, "dst"_a, "src"_a, "rect"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - + m.def("customcrop_into", NvtxTrace("cvcuda.customcrop_into", &CustomCropInto), "dst"_a, "src"_a, "rect"_a, + py::kw_only(), "stream"_a = nullptr, R"pbdoc( Executes the Custom Crop operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Custom Crop operator - for more details and usage examples. Args: dst (cvcuda.Tensor): Output tensor to store the result of the operation. @@ -124,11 +116,7 @@ void ExportOpCustomCrop(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.Tensor: The output tensor (same as dst). )pbdoc"); } diff --git a/python/mod_cvcuda/operators/OpCvtColor.cpp b/python/mod_cvcuda/operators/OpCvtColor.cpp index 58ba3d506..cde2b826b 100644 --- a/python/mod_cvcuda/operators/OpCvtColor.cpp +++ b/python/mod_cvcuda/operators/OpCvtColor.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "../CvtColorUtil.hpp" #include "Operators.hpp" +#include "VarShapeUtils.hpp" #include #include @@ -30,11 +31,18 @@ #include #include +#include namespace cvcudapy { namespace { +class CvtColorOpError : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + Tensor CvtColorInto(Tensor &output, Tensor &input, NVCVColorConversionCode code, std::optional pstream) { if (!pstream) @@ -45,11 +53,12 @@ Tensor CvtColorInto(Tensor &output, Tensor &input, NVCVColorConversionCode code, auto cvtColor = CreateOperator(); ResourceGuard guard(*pstream); - guard.add(LockMode::LOCK_MODE_READWRITE, {input}); + guard.add(LockMode::LOCK_MODE_READ, {input}); guard.add(LockMode::LOCK_MODE_WRITE, {output}); guard.add(LockMode::LOCK_MODE_NONE, {*cvtColor}); - cvtColor->submit(pstream->cudaHandle(), input, output, code); + guard.run([&cvtColor, &pstream, &input, &output, &code]() + { cvtColor->submit(pstream->cudaHandle(), input, output, code); }); return output; } @@ -74,6 +83,65 @@ Tensor CvtColor(Tensor &input, NVCVColorConversionCode code, std::optional 1 && format.numPlanes() == format.numChannels(); +} + +nvcv::ImageFormat PlanarRGBOutputFormat(nvcv::ImageFormat outputFormat) +{ + if (nvcv::DataType channelType = outputFormat.planeDataType(0).channelType(0); channelType != nvcv::TYPE_U8) + { + throw CvtColorOpError{"Unsupported planar var-shape CvtColor output data type"}; + } + + switch (outputFormat.swizzle()) + { + case nvcv::Swizzle::S_XYZ1: + case nvcv::Swizzle::S_XYZ0: + return nvcv::ImageFormat{NVCV_IMAGE_FORMAT_RGB8p}; + case nvcv::Swizzle::S_ZYX1: + case nvcv::Swizzle::S_ZYX0: + return nvcv::ImageFormat{NVCV_IMAGE_FORMAT_BGR8p}; + case nvcv::Swizzle::S_XYZW: + return nvcv::ImageFormat{NVCV_IMAGE_FORMAT_RGBA8p}; + case nvcv::Swizzle::S_ZYXW: + return nvcv::ImageFormat{NVCV_IMAGE_FORMAT_BGRA8p}; + default: + return outputFormat; + } +} + +nvcv::ImageFormat PreservePlanarVarShapeOutput(nvcv::ImageFormat inputFormat, nvcv::ImageFormat outputFormat) +{ + if (!IsPlanarVarShapeFormat(inputFormat)) + { + return outputFormat; + } + + if (outputFormat.numPlanes() != 1) + { + return outputFormat; + } + + if (outputFormat.colorModel() == nvcv::ColorModel::RGB) + { + return PlanarRGBOutputFormat(outputFormat); + } + + if (outputFormat.colorModel() == nvcv::ColorModel::YCbCr + && outputFormat.chromaSubsampling() == nvcv::ChromaSubsampling::NONE && outputFormat.numChannels() == 3) + { + if (nvcv::DataType channelType = outputFormat.planeDataType(0).channelType(0); channelType == nvcv::TYPE_U8) + { + return nvcv::ImageFormat{NVCV_IMAGE_FORMAT_YUV8p}; + } + throw CvtColorOpError{"Unsupported planar var-shape CvtColor YUV output data type"}; + } + + return outputFormat; +} + ImageBatchVarShape CvtColorVarShapeInto(ImageBatchVarShape &output, ImageBatchVarShape &input, NVCVColorConversionCode code, std::optional pstream) { @@ -85,11 +153,12 @@ ImageBatchVarShape CvtColorVarShapeInto(ImageBatchVarShape &output, ImageBatchVa auto cvtColor = CreateOperator(); ResourceGuard guard(*pstream); - guard.add(LockMode::LOCK_MODE_READWRITE, {input}); - guard.add(LockMode::LOCK_MODE_READWRITE, {output}); + guard.add(LockMode::LOCK_MODE_READ, {input}); + guard.add(LockMode::LOCK_MODE_WRITE, {output}); guard.add(LockMode::LOCK_MODE_NONE, {*cvtColor}); - cvtColor->submit(pstream->cudaHandle(), input, output, code); + guard.run([&cvtColor, &pstream, &input, &output, &code]() + { cvtColor->submit(pstream->cudaHandle(), input, output, code); }); return output; } @@ -98,21 +167,14 @@ ImageBatchVarShape CvtColorVarShape(ImageBatchVarShape &input, NVCVColorConversi std::optional pstream) { auto inFormat = input.uniqueFormat(); - if (!inFormat || inFormat.numPlanes() != 1) + if (!inFormat) { - throw std::runtime_error("All images in input must have the same single-plane format"); + throw CvtColorOpError("All images in input must have the same format"); } auto outFormat = GetOutputFormat(inFormat.planeDataType(0), code); + outFormat = PreservePlanarVarShapeOutput(inFormat, outFormat); - ImageBatchVarShape output = ImageBatchVarShape::Create(input.capacity()); - - for (int i = 0; i < input.numImages(); ++i) - { - nvcv::Size2D size = input[i].size(); - - auto img = Image::Create(size, outFormat); - output.pushBack(img); - } + ImageBatchVarShape output = CreateSameShapeImageBatch(input, outFormat); return CvtColorVarShapeInto(output, input, code, pstream); } @@ -122,18 +184,11 @@ ImageBatchVarShape CvtColorVarShape(ImageBatchVarShape &input, NVCVColorConversi void ExportOpCvtColor(py::module &m) { using namespace pybind11::literals; - py::options options; - options.disable_function_signatures(); - m.def("cvtcolor", &CvtColor, "src"_a, "code"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - - cvcuda.cvtcolor(src: cvcuda.Tensor, code: cvcuda.ColorConversion, stream: Optional[cvcuda.Stream] = None) -> cvcuda.Tensor - - Executes the CVT Color operation on the given cuda stream. + m.def("cvtcolor", NvtxTrace("cvcuda.cvtcolor", &CvtColor), "src"_a, "code"_a, py::kw_only(), "stream"_a = nullptr, + R"pbdoc( + Executes the CVT Color operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the CVT Color operator - for more details and usage examples. Args: src (cvcuda.Tensor): Input tensor containing one or more images. @@ -143,20 +198,12 @@ void ExportOpCvtColor(py::module &m) Returns: cvcuda.Tensor: The output tensor. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("cvtcolor_into", &CvtColorInto, "dst"_a, "src"_a, "code"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( + m.def("cvtcolor_into", NvtxTrace("cvcuda.cvtcolor_into", &CvtColorInto), "dst"_a, "src"_a, "code"_a, py::kw_only(), + "stream"_a = nullptr, R"pbdoc( + Executes the CVT Color operation on the given cuda stream. - cvcuda.cvtcolor_into(dst: cvcuda.Tensor, src: cvcuda.Tensor, code: cvcuda.ColorConversion, stream: Optional[cvcuda.Stream] = None) - - Executes the CVT Color operation on the given cuda stream. - - See also: - Refer to the CV-CUDA C API reference for the CVT Color operator - for more details and usage examples. Args: dst (cvcuda.Tensor): Output tensor to store the result of the operation. @@ -165,22 +212,13 @@ void ExportOpCvtColor(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.Tensor: The output tensor (same as dst). )pbdoc"); - m.def("cvtcolor", &CvtColorVarShape, "src"_a, "code"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( + m.def("cvtcolor", NvtxTrace("cvcuda.cvtcolor", &CvtColorVarShape), "src"_a, "code"_a, py::kw_only(), + "stream"_a = nullptr, R"pbdoc( + Executes the CVT Color operation on the given cuda stream. - cvcuda.cvtcolor(src: cvcuda.ImageBatchVarShape, code: cvcuda.ColorConversion, stream: Optional[cvcuda.Stream] = None) -> cvcuda.ImageBatchVarShape - - Executes the CVT Color operation on the given cuda stream. - - See also: - Refer to the CV-CUDA C API reference for the CVT Color operator - for more details and usage examples. Args: src (cvcuda.ImageBatchVarShape): Input image batch containing one or more images. @@ -190,21 +228,13 @@ void ExportOpCvtColor(py::module &m) Returns: cvcuda.ImageBatchVarShape: The output image batch. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("cvtcolor_into", &CvtColorVarShapeInto, "dst"_a, "src"_a, "code"_a, py::kw_only(), "stream"_a = nullptr, + m.def("cvtcolor_into", NvtxTrace("cvcuda.cvtcolor_into", &CvtColorVarShapeInto), "dst"_a, "src"_a, "code"_a, + py::kw_only(), "stream"_a = nullptr, R"pbdoc( + Executes the CVT Color operation on the given cuda stream. - cvcuda.cvtcolor_into(dst: cvcuda.ImageBatchVarShape, src: cvcuda.ImageBatchVarShape, code: cvcuda.ColorConversion, stream: Optional[cvcuda.Stream] = None) - - Executes the CVT Color operation on the given cuda stream. - - See also: - Refer to the CV-CUDA C API reference for the CVT Color operator - for more details and usage examples. Args: src (cvcuda.ImageBatchVarShape): Input image batch containing one or more images. @@ -213,11 +243,7 @@ void ExportOpCvtColor(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.ImageBatchVarShape: The output image batch (same as dst). )pbdoc"); } diff --git a/python/mod_cvcuda/operators/OpErase.cpp b/python/mod_cvcuda/operators/OpErase.cpp index b9ba657e5..d6bd73dd5 100644 --- a/python/mod_cvcuda/operators/OpErase.cpp +++ b/python/mod_cvcuda/operators/OpErase.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,6 +16,7 @@ */ #include "Operators.hpp" +#include "VarShapeUtils.hpp" #include #include @@ -26,9 +27,18 @@ #include #include +#include + namespace cvcudapy { namespace { + +class EraseError : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + Tensor EraseInto(Tensor &output, Tensor &input, Tensor &anchor, Tensor &erasing, Tensor &values, Tensor &imgIdx, bool random, unsigned int seed, std::optional pstream) { @@ -39,7 +49,7 @@ Tensor EraseInto(Tensor &output, Tensor &input, Tensor &anchor, Tensor &erasing, if (anchor.layout().rank() != 1 || anchor.layout()[0] != 'N') { - throw std::runtime_error("Layout of anchor must be 'N'."); + throw EraseError("Layout of anchor must be 'N'."); } nvcv::TensorShape shape = anchor.shape(); @@ -51,7 +61,8 @@ Tensor EraseInto(Tensor &output, Tensor &input, Tensor &anchor, Tensor &erasing, guard.add(LockMode::LOCK_MODE_WRITE, {output}); guard.add(LockMode::LOCK_MODE_READWRITE, {*erase}); - erase->submit(pstream->cudaHandle(), input, output, anchor, erasing, values, imgIdx, random, seed); + guard.run([&erase, &pstream, &input, &output, &anchor, &erasing, &values, &imgIdx, &random, &seed]() + { erase->submit(pstream->cudaHandle(), input, output, anchor, erasing, values, imgIdx, random, seed); }); return output; } @@ -64,6 +75,42 @@ Tensor Erase(Tensor &input, Tensor &anchor, Tensor &erasing, Tensor &values, Ten return EraseInto(output, input, anchor, erasing, values, imgIdx, random, seed, pstream); } +Tensor EraseRegionInto(Tensor &output, Tensor &input, int64_t i, int64_t j, int64_t h, int64_t w, Tensor &values, + std::optional pstream) +{ + if (!pstream) + { + pstream = Stream::Current(); + } + + auto erase = CreateOperator(0); + + ResourceGuard guard(*pstream); + if (static_cast(input).handle() == static_cast(output).handle()) + { + guard.add(LockMode::LOCK_MODE_READWRITE, {input}); + } + else + { + guard.add(LockMode::LOCK_MODE_READ, {input}); + guard.add(LockMode::LOCK_MODE_WRITE, {output}); + } + guard.add(LockMode::LOCK_MODE_READ, {values}); + guard.add(LockMode::LOCK_MODE_READWRITE, {*erase}); + + guard.run([&erase, &pstream, &input, &output, i, j, h, w, &values] + { erase->submit(pstream->cudaHandle(), input, output, i, j, h, w, values); }); + + return output; +} + +Tensor EraseRegion(Tensor &input, int64_t i, int64_t j, int64_t h, int64_t w, Tensor &values, + std::optional pstream) +{ + Tensor output = Tensor::Create(input.shape(), input.dtype()); + return EraseRegionInto(output, input, i, j, h, w, values, pstream); +} + ImageBatchVarShape EraseVarShapeInto(ImageBatchVarShape &output, ImageBatchVarShape &input, Tensor &anchor, Tensor &erasing, Tensor &values, Tensor &imgIdx, bool random, unsigned int seed, std::optional pstream) @@ -75,7 +122,7 @@ ImageBatchVarShape EraseVarShapeInto(ImageBatchVarShape &output, ImageBatchVarSh if (anchor.layout().rank() != 1 || anchor.layout()[0] != 'N') { - throw std::runtime_error("Layout of anchor must be 'N'."); + throw EraseError("Layout of anchor must be 'N'."); } nvcv::TensorShape shape = anchor.shape(); @@ -87,7 +134,8 @@ ImageBatchVarShape EraseVarShapeInto(ImageBatchVarShape &output, ImageBatchVarSh guard.add(LockMode::LOCK_MODE_WRITE, {output}); guard.add(LockMode::LOCK_MODE_READWRITE, {*erase}); - erase->submit(pstream->cudaHandle(), input, output, anchor, erasing, values, imgIdx, random, seed); + guard.run([&erase, &pstream, &input, &output, &anchor, &erasing, &values, &imgIdx, &random, &seed]() + { erase->submit(pstream->cudaHandle(), input, output, anchor, erasing, values, imgIdx, random, seed); }); return output; } @@ -95,19 +143,13 @@ ImageBatchVarShape EraseVarShapeInto(ImageBatchVarShape &output, ImageBatchVarSh ImageBatchVarShape EraseVarShape(ImageBatchVarShape &input, Tensor &anchor, Tensor &erasing, Tensor &values, Tensor &imgIdx, bool random, unsigned int seed, std::optional pstream) { - ImageBatchVarShape output = ImageBatchVarShape::Create(input.numImages()); - auto format = input.uniqueFormat(); if (!format) { - throw std::runtime_error("All images in input must have the same format."); + throw EraseError("All images in input must have the same format."); } - for (auto img = input.begin(); img != input.end(); ++img) - { - auto newimg = Image::Create(img->size(), format); - output.pushBack(newimg); - } + ImageBatchVarShape output = CreateSameShapeImageBatch(input, format, input.numImages()); return EraseVarShapeInto(output, input, anchor, erasing, values, imgIdx, random, seed, pstream); } @@ -117,19 +159,52 @@ ImageBatchVarShape EraseVarShape(ImageBatchVarShape &input, Tensor &anchor, Tens void ExportOpErase(py::module &m) { using namespace pybind11::literals; - py::options options; - options.disable_function_signatures(); - m.def("erase", &Erase, "src"_a, "anchor"_a, "erasing"_a, "values"_a, "imgIdx"_a, py::kw_only(), "random"_a = false, - "seed"_a = 0, "stream"_a = nullptr, R"pbdoc( + m.def("erase", NvtxTrace("cvcuda.erase", &EraseRegion), "src"_a, "i"_a, "j"_a, "h"_a, "w"_a, "v"_a, py::kw_only(), + "stream"_a = nullptr, R"pbdoc( + Erases one rectangular region with torchvision-compatible slice and broadcast semantics. - cvcuda.erase(src: cvcuda.Tensor, anchor: cvcuda.Tensor, erasing: cvcuda.Tensor, values: cvcuda.Tensor, imgIdx: cvcuda.Tensor, random: int, seed: int, stream: Optional[cvcuda.Stream] = None) -> cvcuda.Tensor + The logical operation is ``out = src.clone(); out[..., i:i+h, j:j+w] = v``. For HWC/NHWC + inputs, ``v`` is still interpreted in logical planar order. The value dtype must match + ``src`` or be float32. + + Args: + src (cvcuda.Tensor): Input image tensor. + i (int): Vertical slice start. + j (int): Horizontal slice start. + h (int): Vertical slice extent. + w (int): Horizontal slice extent. + v (cvcuda.Tensor): Broadcastable value tensor. + stream (cvcuda.Stream, optional): CUDA stream on which to submit the operation. + + Returns: + cvcuda.Tensor: A new erased tensor with the same metadata as ``src``. + )pbdoc"); + + m.def("erase_into", NvtxTrace("cvcuda.erase_into", &EraseRegionInto), "dst"_a, "src"_a, "i"_a, "j"_a, "h"_a, "w"_a, + "v"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( + Writes the torchvision-compatible single-region erase result into ``dst``. + + Passing the same tensor as ``src`` and ``dst`` performs the operation in place. + + Args: + dst (cvcuda.Tensor): Output tensor with the same shape, layout, and dtype as ``src``. + src (cvcuda.Tensor): Input image tensor. + i (int): Vertical slice start. + j (int): Horizontal slice start. + h (int): Vertical slice extent. + w (int): Horizontal slice extent. + v (cvcuda.Tensor): Broadcastable value tensor. + stream (cvcuda.Stream, optional): CUDA stream on which to submit the operation. + + Returns: + cvcuda.Tensor: ``dst``. + )pbdoc"); - Executes the Erase operation on the given cuda stream. + m.def("erase", NvtxTrace("cvcuda.erase", &Erase), "src"_a, "anchor"_a, "erasing"_a, "values"_a, "imgIdx"_a, + py::kw_only(), "random"_a = false, "seed"_a = 0, "stream"_a = nullptr, R"pbdoc( + Executes the Erase operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Erase operator - for more details and usage examples. Args: src (cvcuda.Tensor): Input tensor containing one or more images. @@ -147,21 +222,12 @@ void ExportOpErase(py::module &m) Returns: cvcuda.Tensor: The output tensor. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("erase_into", &EraseInto, "dst"_a, "src"_a, "anchor"_a, "erasing"_a, "values"_a, "imgIdx"_a, py::kw_only(), - "random"_a = false, "seed"_a = 0, "stream"_a = nullptr, R"pbdoc( - - cvcuda.erase_into(dst: cvcuda.Tensor, src: cvcuda.Tensor, anchor: cvcuda.Tensor, erasing: cvcuda.Tensor, values: cvcuda.Tensor, imgIdx: cvcuda.Tensor, random: int, seed: int, stream: Optional[cvcuda.Stream] = None) - + m.def("erase_into", NvtxTrace("cvcuda.erase_into", &EraseInto), "dst"_a, "src"_a, "anchor"_a, "erasing"_a, + "values"_a, "imgIdx"_a, py::kw_only(), "random"_a = false, "seed"_a = 0, "stream"_a = nullptr, R"pbdoc( Executes the Erase operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Erase operator - for more details and usage examples. Args: dst (cvcuda.Tensor): Output tensor to store the result of the operation. @@ -178,23 +244,13 @@ void ExportOpErase(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.Tensor: The output tensor (same as dst). )pbdoc"); - m.def("erase", &EraseVarShape, "src"_a, "anchor"_a, "erasing"_a, "values"_a, "imgIdx"_a, py::kw_only(), - "random"_a = false, "seed"_a = 0, "stream"_a = nullptr, R"pbdoc( - - cvcuda.erase(src: cvcuda.ImageBatchVarShape, anchor: cvcuda.Tensor, erasing: cvcuda.Tensor, values: cvcuda.Tensor, imgIdx: cvcuda.Tensor, random: int, seed: int, stream: Optional[cvcuda.Stream] = None) -> cvcuda.ImageBatchVarShape - + m.def("erase", NvtxTrace("cvcuda.erase", &EraseVarShape), "src"_a, "anchor"_a, "erasing"_a, "values"_a, "imgIdx"_a, + py::kw_only(), "random"_a = false, "seed"_a = 0, "stream"_a = nullptr, R"pbdoc( Executes the Erase operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Erase operator - for more details and usage examples. Args: src (cvcuda.ImageBatchVarShape): Input image batch containing one or more images. @@ -213,21 +269,12 @@ void ExportOpErase(py::module &m) Returns: cvcuda.ImageBatchVarShape: The output image batch. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("erase_into", &EraseVarShapeInto, "dst"_a, "src"_a, "anchor"_a, "erasing"_a, "values"_a, "imgIdx"_a, - py::kw_only(), "random"_a = false, "seed"_a = 0, "stream"_a = nullptr, R"pbdoc( - - cvcuda.erase_into(dst: cvcuda.ImageBatchVarShape, src: cvcuda.ImageBatchVarShape, anchor: cvcuda.Tensor, erasing: cvcuda.Tensor, values: cvcuda.Tensor, imgIdx: cvcuda.Tensor, random: int, seed: int, stream: Optional[cvcuda.Stream] = None) - + m.def("erase_into", NvtxTrace("cvcuda.erase_into", &EraseVarShapeInto), "dst"_a, "src"_a, "anchor"_a, "erasing"_a, + "values"_a, "imgIdx"_a, py::kw_only(), "random"_a = false, "seed"_a = 0, "stream"_a = nullptr, R"pbdoc( Executes the Erase operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Erase operator - for more details and usage examples. Args: src (cvcuda.ImageBatchVarShape): Input image batch containing one or more images. @@ -243,11 +290,7 @@ void ExportOpErase(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.ImageBatchVarShape: The output image batch (same as dst). )pbdoc"); } diff --git a/python/mod_cvcuda/operators/OpFindHomography.cpp b/python/mod_cvcuda/operators/OpFindHomography.cpp index 9c97a50b6..ff8f5ab63 100644 --- a/python/mod_cvcuda/operators/OpFindHomography.cpp +++ b/python/mod_cvcuda/operators/OpFindHomography.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,6 +18,7 @@ #include "../WorkspaceCache.hpp" #include "Operators.hpp" +#include #include #include #include @@ -32,43 +33,45 @@ #include #include -namespace cvcudapy { +#include -namespace { +namespace cvcudapy { -// Specialized class for cvcuda::FindHomography operator with a better cache Key. -// It allows for reusing an existing operator object from cache if its payload size is >= the required size. -// It also allows to fetch the biggest payload object to be reused while removing all others. -// This is more flexible than using the generic PyOperator class and its Key class. -class PyOpFindHomography : public nvcvpy::Container +// Specialized Python wrapper for cvcuda::FindHomography. +// The underlying DeviceState allocates device buffers sized for +// (batchSize, maxNumPoints); a cached op can only be reused when the +// request matches those dimensions exactly, otherwise RunFindHomography +// would issue memsets and kernel launches past the end of the +// allocations. The cache key therefore uses strict equality on both +// ctor arguments (matching the generic PyOperator pattern). +class PyOpFindHomography : public nvcvpy::Container // NOSONAR: operator wrappers share the Python cache hierarchy. { public: - // Define a Key class to be used by the cache to fetch similar items for potential reuse. class Key : public nvcvpy::IKey { public: - // Arguments of the key constructor should match the corresponding cvcuda operator arguments. - Key(int batchSize, int maxNumPoints) {} - - size_t payloadSize() const + Key(int batchSize, int maxNumPoints) + : m_batchSize(batchSize) + , m_maxNumPoints(maxNumPoints) { - return 0; } private: size_t doGetHash() const override { - return 0; + return nvcvpy::util::ComputeHash(m_batchSize, m_maxNumPoints); } - // The comparison of keys is based on the payload size, the one in the cache is "that" key. bool doIsCompatible(const nvcvpy::IKey &that_) const override { - return dynamic_cast(&that_) != nullptr; + const auto &that = static_cast(that_); + return m_batchSize == that.m_batchSize && m_maxNumPoints == that.m_maxNumPoints; } + + int m_batchSize; + int m_maxNumPoints; }; - // Constructor instantiate the cache key and the operator object. PyOpFindHomography(int batchSize, int maxNumPoints) : m_key(batchSize, maxNumPoints) , m_op(batchSize, maxNumPoints) @@ -76,59 +79,33 @@ class PyOpFindHomography : public nvcvpy::Container } inline void submit(cudaStream_t stream, const nvcv::Tensor &srcPts, const nvcv::Tensor &dstPts, - const nvcv::Tensor &models) + const nvcv::Tensor &models) const { m_op(stream, srcPts, dstPts, models); } inline void submit(cudaStream_t stream, const nvcv::TensorBatch &srcPts, const nvcv::TensorBatch &dstPts, - const nvcv::TensorBatch &models) + const nvcv::TensorBatch &models) const { m_op(stream, srcPts, dstPts, models); } - // Required override to get the py object container. py::object container() const override { - return *this; + return py::reinterpret_borrow(this->ptr()); } - // Required override to get the key as the base interface class. const nvcvpy::IKey &key() const override { return m_key; } - // The static fetch function can be used to specialize the fetch of a specific object from the cache. - // It can be used to select the best object among a number of matched cache objects. - // It can also be used to remove other objects that are not needed in the cache anymore. - // Here, it fetches the biggest payload OP among cache items and remove all other OPs from the cache. - // It is ok to remove them since the biggest payload OP can be used to accomodate all of them, - // so they will never be reused and thus are no longer necessary. + // Items returned by Cache::fetch have already passed the strict-equality check above, + // so any one of them can serve the request. static std::shared_ptr fetch(std::vector> &cache) { assert(!cache.empty()); - - std::shared_ptr retItem = cache[0]; - size_t maxPayloadSize = 0; - - for (const auto &item : cache) - { - const Key &key = static_cast(item.get()->key()); - size_t keyPayloadSize = key.payloadSize(); - - if (keyPayloadSize > maxPayloadSize) - { - maxPayloadSize = keyPayloadSize; - retItem = item; - } - } - - cache.clear(); - - nvcvpy::Cache::removeAllNotInUseMatching(retItem.get()->key()); - - return retItem; + return cache[0]; } private: @@ -136,6 +113,8 @@ class PyOpFindHomography : public nvcvpy::Container cvcuda::FindHomography m_op; }; +namespace { + Tensor FindHomographyInto(Tensor &models, Tensor &srcPts, Tensor &dstPts, std::optional pstream) { if (!pstream) @@ -145,8 +124,8 @@ Tensor FindHomographyInto(Tensor &models, Tensor &srcPts, Tensor &dstPts, std::o // Use CreateOperatorEx to use the extended create operator function passing the specialized PyOperator above // as template type, instead of the regular cvcuda::OP class used in the CreateOperator function. - int32_t batchSize = srcPts.shape()[0]; - int32_t numPoints = srcPts.shape()[1]; + auto batchSize = static_cast(srcPts.shape()[0]); + auto numPoints = static_cast(srcPts.shape()[1]); auto findHomography = CreateOperatorEx(batchSize, numPoints); @@ -156,7 +135,8 @@ Tensor FindHomographyInto(Tensor &models, Tensor &srcPts, Tensor &dstPts, std::o guard.add(LockMode::LOCK_MODE_READWRITE, {models}); guard.add(LockMode::LOCK_MODE_READWRITE, {*findHomography}); - findHomography->submit(pstream->cudaHandle(), srcPts, dstPts, models); + guard.run([&findHomography, &pstream, &srcPts, &dstPts, &models]() + { findHomography->submit(pstream->cudaHandle(), srcPts, dstPts, models); }); return models; } @@ -187,7 +167,7 @@ TensorBatch VarShapeFindHomographyInto(TensorBatch &models, TensorBatch &srcPts, for (int i = 0; i < batchSize; i++) { - int numPoints = srcPts[i].shape()[1]; + auto numPoints = static_cast(srcPts[i].shape()[1]); if (numPoints > maxNumPoints) maxNumPoints = numPoints; } @@ -200,7 +180,8 @@ TensorBatch VarShapeFindHomographyInto(TensorBatch &models, TensorBatch &srcPts, guard.add(LockMode::LOCK_MODE_READWRITE, {models}); guard.add(LockMode::LOCK_MODE_READWRITE, {*findHomography}); - findHomography->submit(pstream->cudaHandle(), srcPts, dstPts, models); + guard.run([&findHomography, &pstream, &srcPts, &dstPts, &models]() + { findHomography->submit(pstream->cudaHandle(), srcPts, dstPts, models); }); return models; } @@ -217,30 +198,66 @@ TensorBatch VarShapeFindHomography(TensorBatch &srcPts, TensorBatch &dstPts, std for (int i = 0; i < srcPts.numTensors(); i++) { Tensor outTensor = Tensor::Create(modelsShape, nvcv::TYPE_F32, nvcv::TENSOR_NHW); - models.pushBack(outTensor); + models.pushBackTensor(outTensor); } return VarShapeFindHomographyInto(models, srcPts, dstPts, pstream); } -} // namespace +// Get a reusable FindHomography operator that can be passed to findhomography_into_with_op. +// This allows the caller to hold a persistent reference to prevent cache eviction overhead. +// Returns a PyCapsule containing the shared_ptr to the operator. +py::object GetFindHomographyOperator(int32_t batchSize, int32_t numPoints) +{ + auto op = CreateOperatorEx(batchSize, numPoints); + // Store the shared_ptr on the heap so it can be held by the capsule. + auto opPtr = std::make_unique>(std::move(op)); + return py::capsule(opPtr.release(), "FindHomographyOperator", + [](PyObject *capsule) + { + auto ptr = PyCapsule_GetPointer(capsule, "FindHomographyOperator"); + std::unique_ptr> opPtr( + static_cast *>(ptr)); + (void)opPtr; + }); +} -void ExportOpFindHomography(py::module &m) +// Version of FindHomographyInto that accepts a pre-fetched operator. +// This avoids the cache lookup and ResourceGuard overhead that causes bimodal timing. +Tensor FindHomographyIntoWithOp(Tensor &models, Tensor &srcPts, Tensor &dstPts, py::capsule pyOp, + std::optional pstream) { - using namespace pybind11::literals; + if (!pstream) + { + pstream = Stream::Current(); + } + + // Extract the shared_ptr from the capsule + const auto *opPtr = static_cast *>(pyOp.get_pointer()); + auto findHomography = *opPtr; - py::options options; - options.disable_function_signatures(); + ResourceGuard guard(*pstream); + guard.add(LockMode::LOCK_MODE_READ, {srcPts}); + guard.add(LockMode::LOCK_MODE_READ, {dstPts}); + guard.add(LockMode::LOCK_MODE_READWRITE, {models}); + // NOTE: Do NOT add findHomography to ResourceGuard - the caller holds the reference + + guard.run([&findHomography, &pstream, &srcPts, &dstPts, &models]() + { findHomography->submit(pstream->cudaHandle(), srcPts, dstPts, models); }); + + return models; +} - m.def("findhomography", &FindHomography, "srcPts"_a, "dstPts"_a, "stream"_a = nullptr, R"pbdoc( +} // namespace - cvcuda.findhomography(srcPts: cvcuda.Tensor, dstPts: cvcuda.Tensor, stream: Optional[cvcuda.Stream] = None) -> cvcuda.Tensor +void ExportOpFindHomography(py::module &m) +{ + using namespace pybind11::literals; + m.def("findhomography", NvtxTrace("cvcuda.findhomography", &FindHomography), "srcPts"_a, "dstPts"_a, + "stream"_a = nullptr, R"pbdoc( Estimates the homography matrix between srcPts and dstPts coordinates on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Find Homography operator - for more details and usage examples. Args: srcPts (cvcuda.Tensor): Input source coordinates tensor containing 2D coordinates in the source image. @@ -250,20 +267,12 @@ void ExportOpFindHomography(py::module &m) Returns: cvcuda.Tensor: The model homography matrix tensor. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("findhomography_into", &FindHomographyInto, "models"_a, "srcPts"_a, "dstPts"_a, "stream"_a = nullptr, R"pbdoc( - - cvcuda.findhomography_into(models: cvcuda.Tensor, srcPts: cvcuda.Tensor, dstPts: cvcuda.Tensor, stream: Optional[cvcuda.Stream] = None) - + m.def("findhomography_into", NvtxTrace("cvcuda.findhomography_into", &FindHomographyInto), "models"_a, "srcPts"_a, + "dstPts"_a, "stream"_a = nullptr, R"pbdoc( Executes the Find Homography operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Find Homography operator - for more details and usage examples. Args: models (cvcuda.Tensor): Output model tensor containing 3x3 homography matrices. @@ -274,20 +283,12 @@ void ExportOpFindHomography(py::module &m) Returns: cvcuda.Tensor: The model homography matrix tensor. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("findhomography", &VarShapeFindHomography, "srcPts"_a, "dstPts"_a, "stream"_a = nullptr, R"pbdoc( - - cvcuda.findhomography(srcPts: cvcuda.TensorBatch, dstPts: cvcuda.TensorBatch, stream: Optional[cvcuda.Stream] = None) -> TensorBatch - + m.def("findhomography", NvtxTrace("cvcuda.findhomography", &VarShapeFindHomography), "srcPts"_a, "dstPts"_a, + "stream"_a = nullptr, R"pbdoc( Executes the Find Homography operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Find Homography operator - for more details and usage examples. Args: srcPts (cvcuda.TensorBatch): Input source coordinates tensor containing 2D coordinates in the source image. @@ -297,21 +298,13 @@ void ExportOpFindHomography(py::module &m) Returns: cvcuda.TensorBatch: The model homography matrix tensor batch. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("findhomography_into", &VarShapeFindHomographyInto, "models"_a, "srcPts"_a, "dstPts"_a, "stream"_a = nullptr, + m.def("findhomography_into", NvtxTrace("cvcuda.findhomography_into", &VarShapeFindHomographyInto), "models"_a, + "srcPts"_a, "dstPts"_a, "stream"_a = nullptr, R"pbdoc( - - cvcuda.findhomography(models: cvcuda.TensorBatch, srcPts: cvcuda.TensorBatch, dstPts: cvcuda.TensorBatch, stream: Optional[cvcuda.Stream] = None) - Executes the Find Homography operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Find Homography operator - for more details and usage examples. Args: models (cvcuda.TensorBatch): Output model tensor containing 3x3 homography matrices. @@ -323,9 +316,47 @@ void ExportOpFindHomography(py::module &m) cvcuda.TensorBatch: The model homography matrix tensor batch. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + )pbdoc"); + + m.def("get_findhomography_operator", NvtxTrace("cvcuda.get_findhomography_operator", &GetFindHomographyOperator), + "batch_size"_a, "num_points"_a, R"pbdoc( + Get a reusable FindHomography operator for the given dimensions. + + This allows holding a persistent reference to the operator to avoid + the cache eviction overhead that can cause bimodal timing patterns + when calling findhomography_into repeatedly. + + Args: + batch_size (int): Number of samples in the batch. + num_points (int): Number of points per sample. + + Returns: + object: A FindHomography operator that can be passed to findhomography_into_with_op. + + Example: + >>> op = cvcuda.get_findhomography_operator(1024, 2048) + >>> for _ in range(iterations): + ... cvcuda.findhomography_into_with_op(models, src, dst, op, stream=stream) + )pbdoc"); + + m.def("findhomography_into_with_op", NvtxTrace("cvcuda.findhomography_into_with_op", &FindHomographyIntoWithOp), + "models"_a, "srcPts"_a, "dstPts"_a, "operator"_a, "stream"_a = nullptr, R"pbdoc( + Executes the Find Homography operation using a pre-fetched operator. + + This version accepts an operator obtained from get_findhomography_operator(), + which avoids cache lookup overhead and prevents bimodal timing patterns. + + + Args: + models (cvcuda.Tensor): Output model tensor containing 3x3 homography matrices. + srcPts (cvcuda.Tensor): Input source coordinates tensor containing 2D coordinates in the source image. + dstPts (cvcuda.Tensor): Input destination coordinates tensor containing 2D coordinates in the target image. + operator (object): Pre-fetched operator from get_findhomography_operator(). + stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + cvcuda.Tensor: The model homography matrix tensor. + )pbdoc"); } diff --git a/python/mod_cvcuda/operators/OpFlip.cpp b/python/mod_cvcuda/operators/OpFlip.cpp index 1d7ad614d..2a5312a53 100644 --- a/python/mod_cvcuda/operators/OpFlip.cpp +++ b/python/mod_cvcuda/operators/OpFlip.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,6 +16,7 @@ */ #include "Operators.hpp" +#include "VarShapeUtils.hpp" #include #include @@ -45,7 +46,8 @@ Tensor FlipInto(Tensor &output, Tensor &input, int32_t flipCode, std::optionalsubmit(pstream->cudaHandle(), input, output, flipCode); + guard.run([&Flip, &pstream, &input, &output, &flipCode]() + { Flip->submit(pstream->cudaHandle(), input, output, flipCode); }); return output; } @@ -72,22 +74,15 @@ ImageBatchVarShape FlipVarShapeInto(ImageBatchVarShape &output, ImageBatchVarSha guard.add(LockMode::LOCK_MODE_WRITE, {output}); guard.add(LockMode::LOCK_MODE_NONE, {*flip}); - flip->submit(pstream->cudaHandle(), input, output, flipCode); + guard.run([&flip, &pstream, &input, &output, &flipCode]() + { flip->submit(pstream->cudaHandle(), input, output, flipCode); }); return output; } ImageBatchVarShape FlipVarShape(ImageBatchVarShape &input, Tensor &flipCode, std::optional pstream) { - ImageBatchVarShape output = ImageBatchVarShape::Create(input.capacity()); - - for (int i = 0; i < input.numImages(); ++i) - { - nvcv::ImageFormat format = input[i].format(); - nvcv::Size2D size = input[i].size(); - auto image = Image::Create(size, format); - output.pushBack(image); - } + ImageBatchVarShape output = CreateSameShapeImageBatch(input); return FlipVarShapeInto(output, input, flipCode, pstream); } @@ -97,18 +92,10 @@ ImageBatchVarShape FlipVarShape(ImageBatchVarShape &input, Tensor &flipCode, std void ExportOpFlip(py::module &m) { using namespace pybind11::literals; - py::options options; - options.disable_function_signatures(); - - m.def("flip", &Flip, "src"_a, "flipCode"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - - cvcuda.flip(src: cvcuda.Tensor, flipCode: int, stream: Optional[cvcuda.Stream] = None) -> cvcuda.Tensor + m.def("flip", NvtxTrace("cvcuda.flip", &Flip), "src"_a, "flipCode"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( Executes the Flip operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Flip operator - for more details and usage examples. Args: src (cvcuda.Tensor): Input tensor containing one or more images. @@ -121,20 +108,12 @@ void ExportOpFlip(py::module &m) Returns: cvcuda.Tensor: The output tensor. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("flip_into", &FlipInto, "dst"_a, "src"_a, "flipCode"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - - cvcuda.flip_into(dst: cvcuda.Tensor, src: cvcuda.Tensor, flipCode: int, stream: Optional[cvcuda.Stream] = None) - - Executes the Flip operation on the given cuda stream. + m.def("flip_into", NvtxTrace("cvcuda.flip_into", &FlipInto), "dst"_a, "src"_a, "flipCode"_a, py::kw_only(), + "stream"_a = nullptr, R"pbdoc( + Executes the Flip operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Flip operator - for more details and usage examples. Args: dst (cvcuda.Tensor): Output tensor to store the result of the operation. @@ -146,22 +125,13 @@ void ExportOpFlip(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.Tensor: The output tensor (same as dst). )pbdoc"); - m.def("flip", &FlipVarShape, "src"_a, "flipCode"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - - cvcuda.flip(src: cvcuda.ImageBatchVarShape, flipCode: cvcuda.Tensor, stream: Optional[cvcuda.Stream] = None) -> cvcuda.ImageBatchVarShape - - Executes the Flip operation on the given cuda stream. + m.def("flip", NvtxTrace("cvcuda.flip", &FlipVarShape), "src"_a, "flipCode"_a, py::kw_only(), "stream"_a = nullptr, + R"pbdoc( + Executes the Flip operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Flip operator - for more details and usage examples. Args: src (cvcuda.ImageBatchVarShape): Input image batch containing one or more images. @@ -174,20 +144,12 @@ void ExportOpFlip(py::module &m) Returns: cvcuda.ImageBatchVarShape: The output image batch. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("flip_into", &FlipVarShapeInto, "dst"_a, "src"_a, "flipCode"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - - cvcuda.flip_into(dst:cvcuda.ImageBatchVarShape, src: cvcuda.ImageBatchVarShape, flipCode: cvcuda.Tensor, stream: Optional[cvcuda.Stream] = None) - + m.def("flip_into", NvtxTrace("cvcuda.flip_into", &FlipVarShapeInto), "dst"_a, "src"_a, "flipCode"_a, py::kw_only(), + "stream"_a = nullptr, R"pbdoc( Executes the Flip operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Flip operator - for more details and usage examples. Args: src (cvcuda.ImageBatchVarShape): Input image batch containing one or more images. @@ -199,11 +161,7 @@ void ExportOpFlip(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.ImageBatchVarShape: The output image batch (same as dst). )pbdoc"); } diff --git a/python/mod_cvcuda/operators/OpGammaContrast.cpp b/python/mod_cvcuda/operators/OpGammaContrast.cpp index 7c7aa72e9..0de35e44a 100644 --- a/python/mod_cvcuda/operators/OpGammaContrast.cpp +++ b/python/mod_cvcuda/operators/OpGammaContrast.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,12 +16,14 @@ */ #include "Operators.hpp" +#include "VarShapeUtils.hpp" #include #include #include #include #include +#include #include #include #include @@ -46,24 +48,81 @@ ImageBatchVarShape VarShapeGammaContrastInto(ImageBatchVarShape &output, ImageBa guard.add(LockMode::LOCK_MODE_WRITE, {output}); guard.add(LockMode::LOCK_MODE_READWRITE, {*gamma_contrast}); - gamma_contrast->submit(pstream->cudaHandle(), input, output, gamma); + guard.run([&gamma, &gamma_contrast, &pstream, &input, &output]() + { gamma_contrast->submit(pstream->cudaHandle(), input, output, gamma); }); return output; } ImageBatchVarShape VarShapeGammaContrast(ImageBatchVarShape &input, Tensor &gamma, std::optional pstream) { - ImageBatchVarShape output = ImageBatchVarShape::Create(input.capacity()); + ImageBatchVarShape output = CreateSameShapeImageBatch(input); - for (int i = 0; i < input.numImages(); ++i) + return VarShapeGammaContrastInto(output, input, gamma, pstream); +} + +Tensor TensorGammaContrastInto(Tensor &output, Tensor &input, Tensor &gamma, std::optional pstream) +{ + if (!pstream) { - nvcv::ImageFormat format = input[i].format(); - nvcv::Size2D size = input[i].size(); - auto image = Image::Create(size, format); - output.pushBack(image); + pstream = Stream::Current(); } - return VarShapeGammaContrastInto(output, input, gamma, pstream); + // Number of samples bounds the gamma scratch; 4 is the max supported channel count (the operator + // validates the actual channel count against the input layout). + const int numSamples = (input.shape().size() == 4) ? static_cast(input.shape()[0]) : 1; + + auto gamma_contrast = CreateOperator(numSamples, 4); + + ResourceGuard guard(*pstream); + guard.add(LockMode::LOCK_MODE_READ, {input, gamma}); + guard.add(LockMode::LOCK_MODE_WRITE, {output}); + guard.add(LockMode::LOCK_MODE_READWRITE, {*gamma_contrast}); + + guard.run([&gamma, &gamma_contrast, &pstream, &input, &output]() + { gamma_contrast->submit(pstream->cudaHandle(), input, output, gamma); }); + + return output; +} + +Tensor TensorGammaContrast(Tensor &input, Tensor &gamma, std::optional pstream) +{ + Tensor output = Tensor::Create(input.shape(), input.dtype()); + + return TensorGammaContrastInto(output, input, gamma, pstream); +} + +Tensor TensorGammaContrastScalarInto(Tensor &output, Tensor &input, float gamma, float gain, NVCVRoundMode round, + std::optional pstream) +{ + if (!pstream) + { + pstream = Stream::Current(); + } + + // Scalar gamma/gain are baked into the kernel launch, so the create-time max-batch/max-channel + // capacities (which only bound the gamma-tensor staging of the other overloads) don't constrain + // this path. Use the minimum the C API accepts so one cached operator serves every scalar call + // regardless of batch size. + auto gamma_contrast = CreateOperator(1, 1); + + ResourceGuard guard(*pstream); + guard.add(LockMode::LOCK_MODE_READ, {input}); + guard.add(LockMode::LOCK_MODE_WRITE, {output}); + guard.add(LockMode::LOCK_MODE_READWRITE, {*gamma_contrast}); + + guard.run([&gamma, &gain, &round, &gamma_contrast, &pstream, &input, &output]() + { gamma_contrast->submit(pstream->cudaHandle(), input, output, gamma, gain, round); }); + + return output; +} + +Tensor TensorGammaContrastScalar(Tensor &input, float gamma, float gain, NVCVRoundMode round, + std::optional pstream) +{ + Tensor output = Tensor::Create(input.shape(), input.dtype()); + + return TensorGammaContrastScalarInto(output, input, gamma, gain, round, pstream); } } // namespace @@ -72,13 +131,90 @@ void ExportOpGammaContrast(py::module &m) { using namespace pybind11::literals; - m.def("gamma_contrast", &VarShapeGammaContrast, "src"_a, "gamma"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( + m.def("gamma_contrast", NvtxTrace("cvcuda.gamma_contrast", &TensorGammaContrast), "src"_a, "gamma"_a, py::kw_only(), + "stream"_a = nullptr, R"pbdoc( + Executes the Gamma Contrast operation on the given cuda stream. + + + Args: + src (cvcuda.Tensor): Input tensor (interleaved (N)HWC or planar (N)CHW layout). + gamma (cvcuda.Tensor): 1D Tensor with the gamma value for each sample / sample channel. + stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + cvcuda.Tensor: The output tensor. + + )pbdoc"); + m.def("gamma_contrast_into", NvtxTrace("cvcuda.gamma_contrast_into", &TensorGammaContrastInto), "dst"_a, "src"_a, + "gamma"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( + Executes the Gamma Contrast operation on the given cuda stream. + + + Args: + dst (cvcuda.Tensor): Output tensor to store the result of the operation. + src (cvcuda.Tensor): Input tensor (interleaved (N)HWC or planar (N)CHW layout). + gamma (cvcuda.Tensor): 1D Tensor with the gamma value for each sample / sample channel. + stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + cvcuda.Tensor: The output tensor (same as dst). + )pbdoc"); + + m.def("gamma_contrast", NvtxTrace("cvcuda.gamma_contrast", &TensorGammaContrastScalar), "src"_a, "gamma"_a, + "gain"_a = 1.0, py::kw_only(), "round"_a = NVCV_ROUND_NEAREST, "stream"_a = nullptr, R"pbdoc( + Executes the Gamma Contrast operation with host-scalar gamma and gain on the given cuda stream. + + Applies ``out = gain * in**gamma`` (the torchvision ``adjust_gamma`` formula) with a single + ``gamma``/``gain`` for every sample and channel. The scalars are passed by value into the kernel + launch -- no gamma tensor is allocated and no host-to-device copy is performed. With ``gain == 1.0`` + and ``round == cvcuda.Round.NEAREST``, the result is bit-exact with the device-tensor gamma + overload fed a gamma tensor filled with the same value. ``gamma`` is not range-validated: a + negative value follows powf semantics (NaN for fractional powers of negative inputs). + + Args: + src (cvcuda.Tensor): Input tensor (interleaved (N)HWC or planar (N)CHW layout). + gamma (float): Gamma exponent applied to every sample / channel. + gain (float, optional): Output gain applied to every sample / channel. Defaults to 1.0. + round (cvcuda.Round, optional): Rounding mode used for integer outputs. Defaults to + cvcuda.Round.NEAREST; use cvcuda.Round.TRUNCATE to truncate toward zero. Has no + effect for floating-point outputs. + stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + cvcuda.Tensor: The output tensor. + + )pbdoc"); + + m.def("gamma_contrast_into", NvtxTrace("cvcuda.gamma_contrast_into", &TensorGammaContrastScalarInto), "dst"_a, + "src"_a, "gamma"_a, "gain"_a = 1.0, py::kw_only(), "round"_a = NVCV_ROUND_NEAREST, "stream"_a = nullptr, + R"pbdoc( + Executes the Gamma Contrast operation with host-scalar gamma and gain on the given cuda stream. + + Applies ``out = gain * in**gamma`` (the torchvision ``adjust_gamma`` formula) with a single + ``gamma``/``gain`` for every sample and channel. The scalars are passed by value into the kernel + launch -- no gamma tensor is allocated and no host-to-device copy is performed. ``gamma`` is not + range-validated: a negative value follows powf semantics (NaN for fractional powers of negative + inputs). + + Args: + dst (cvcuda.Tensor): Output tensor to store the result of the operation. + src (cvcuda.Tensor): Input tensor (interleaved (N)HWC or planar (N)CHW layout). + gamma (float): Gamma exponent applied to every sample / channel. + gain (float, optional): Output gain applied to every sample / channel. Defaults to 1.0. + round (cvcuda.Round, optional): Rounding mode used for integer outputs. Defaults to + cvcuda.Round.NEAREST; use cvcuda.Round.TRUNCATE to truncate toward zero. Has no + effect for floating-point outputs. + stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + cvcuda.Tensor: The output tensor (same as dst). + )pbdoc"); + + m.def("gamma_contrast", NvtxTrace("cvcuda.gamma_contrast", &VarShapeGammaContrast), "src"_a, "gamma"_a, + py::kw_only(), "stream"_a = nullptr, R"pbdoc( Executes the Gamma Contrast operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Gamma Contrast operator - for more details and usage examples. Args: src (cvcuda.ImageBatchVarShape): Input tensor containing one or more images. @@ -88,19 +224,12 @@ void ExportOpGammaContrast(py::module &m) Returns: cvcuda.ImageBatchVarShape: The output image batch. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("gamma_contrast_into", &VarShapeGammaContrastInto, "dst"_a, "src"_a, "gamma"_a, py::kw_only(), - "stream"_a = nullptr, R"pbdoc( - + m.def("gamma_contrast_into", NvtxTrace("cvcuda.gamma_contrast_into", &VarShapeGammaContrastInto), "dst"_a, "src"_a, + "gamma"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( Executes the Gamma Contrast operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Gamma Contrast operator - for more details and usage examples. Args: dst (cvcuda.ImageBatchVarShape): Output tensor to store the result of the operation. @@ -109,11 +238,7 @@ void ExportOpGammaContrast(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.ImageBatchVarShape: The output image batch (same as dst). )pbdoc"); } diff --git a/python/mod_cvcuda/operators/OpGaussian.cpp b/python/mod_cvcuda/operators/OpGaussian.cpp index ab81ecf6a..e5bc1a95d 100644 --- a/python/mod_cvcuda/operators/OpGaussian.cpp +++ b/python/mod_cvcuda/operators/OpGaussian.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,6 +16,7 @@ */ #include "Operators.hpp" +#include "VarShapeUtils.hpp" #include #include @@ -50,7 +51,8 @@ Tensor GaussianInto(Tensor &output, Tensor &input, const std::tuple &k guard.add(LockMode::LOCK_MODE_WRITE, {output}); guard.add(LockMode::LOCK_MODE_READWRITE, {*gaussian}); - gaussian->submit(pstream->cudaHandle(), input, output, kernelSizeArg, sigmaArg, border); + guard.run([&gaussian, &pstream, &input, &output, &kernelSizeArg, &sigmaArg, &border]() + { gaussian->submit(pstream->cudaHandle(), input, output, kernelSizeArg, sigmaArg, border); }); return output; } @@ -81,7 +83,8 @@ ImageBatchVarShape VarShapeGaussianInto(ImageBatchVarShape &output, ImageBatchVa guard.add(LockMode::LOCK_MODE_WRITE, {output}); guard.add(LockMode::LOCK_MODE_READWRITE, {*gaussian}); - gaussian->submit(pstream->cudaHandle(), input, output, ksize, sigma, border); + guard.run([&gaussian, &pstream, &input, &output, &ksize, &sigma, &border]() + { gaussian->submit(pstream->cudaHandle(), input, output, ksize, sigma, border); }); return output; } @@ -89,12 +92,7 @@ ImageBatchVarShape VarShapeGaussianInto(ImageBatchVarShape &output, ImageBatchVa ImageBatchVarShape VarShapeGaussian(ImageBatchVarShape &input, const std::tuple &max_kernel_size, Tensor &ksize, Tensor &sigma, NVCVBorderType border, std::optional pstream) { - ImageBatchVarShape output = ImageBatchVarShape::Create(input.capacity()); - - for (int i = 0; i < input.numImages(); ++i) - { - output.pushBack(Image::Create(input[i].size(), input[i].format())); - } + ImageBatchVarShape output = CreateSameShapeImageBatch(input); return VarShapeGaussianInto(output, input, max_kernel_size, ksize, sigma, border, pstream); } @@ -104,18 +102,11 @@ ImageBatchVarShape VarShapeGaussian(ImageBatchVarShape &input, const std::tuple< void ExportOpGaussian(py::module &m) { using namespace pybind11::literals; - py::options options; - options.disable_function_signatures(); - - m.def("gaussian", &Gaussian, "src"_a, "kernel_size"_a, "sigma"_a, "border"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, - py::kw_only(), "stream"_a = nullptr, R"pbdoc( - cvcuda.gaussian(src: cvcuda.Tensor, kernel_size: Tuple[int, int], sigma: Tuple[double, double], border: border_mode: cvcuda.Border, stream: Optional[cvcuda.Stream] = None) -> cvcuda.Tensor + m.def("gaussian", NvtxTrace("cvcuda.gaussian", &Gaussian), "src"_a, "kernel_size"_a, "sigma"_a, + "border"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, py::kw_only(), "stream"_a = nullptr, R"pbdoc( Executes the Gaussian operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Gaussian operator - for more details and usage examples. Args: src (cvcuda.Tensor): Input tensor containing one or more images. @@ -127,20 +118,12 @@ void ExportOpGaussian(py::module &m) Returns: cvcuda.Tensor: The output tensor. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("gaussian_into", &GaussianInto, "dst"_a, "src"_a, "kernel_size"_a, "sigma"_a, - "border"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - - cvcuda.gaussian_into(dst: cvcuda.Tensor, src: Tensor, kernel_size: Tuple[int, int], sigma: Tuple[double, double], border: border_mode: cvcuda.Border, stream: Optional[cvcuda.Stream] = None) + m.def("gaussian_into", NvtxTrace("cvcuda.gaussian_into", &GaussianInto), "dst"_a, "src"_a, "kernel_size"_a, + "sigma"_a, "border"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, py::kw_only(), "stream"_a = nullptr, R"pbdoc( Executes the Gaussian operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Gaussian operator - for more details and usage examples. Args: dst (cvcuda.Tensor): Output tensor to store the result of the operation. @@ -151,23 +134,13 @@ void ExportOpGaussian(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.Tensor: The output tensor (same as dst). )pbdoc"); - m.def("gaussian", &VarShapeGaussian, "src"_a, "max_kernel_size"_a, "kernel_size"_a, "sigma"_a, - "border"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - - cvcuda.gaussian(src: cvcuda.ImageBatchVarShape, kernel_size: cvcuda.Tensor, sigma: cvcuda.Tensor, border: border_mode: cvcuda.Border, stream: Optional[cvcuda.Stream] = None) -> cvcuda.ImageBatchVarShape - + m.def("gaussian", NvtxTrace("cvcuda.gaussian", &VarShapeGaussian), "src"_a, "max_kernel_size"_a, "kernel_size"_a, + "sigma"_a, "border"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, py::kw_only(), "stream"_a = nullptr, R"pbdoc( Executes the Gaussian operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Gaussian operator - for more details and usage examples. Args: src (cvcuda.ImageBatchVarShape): Input image batch containing one or more images. @@ -179,21 +152,13 @@ void ExportOpGaussian(py::module &m) Returns: cvcuda.ImageBatchVarShape: The output image batch. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("gaussian_into", &VarShapeGaussianInto, "dst"_a, "src"_a, "max_kernel_size"_a, "kernel_size"_a, "sigma"_a, - "border"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - - cvcuda.gaussian_into(dst: cvcuda.ImageBatchVarShape, src: cvcuda.ImageBatchVarShape, kernel_size: cvcuda.Tensor, sigma: cvcuda.Tensor, border: border_mode: cvcuda.Border, stream: Optional[cvcuda.Stream] = None) - - Executes the Gaussian operation on the given cuda stream. + m.def("gaussian_into", NvtxTrace("cvcuda.gaussian_into", &VarShapeGaussianInto), "dst"_a, "src"_a, + "max_kernel_size"_a, "kernel_size"_a, "sigma"_a, "border"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, + py::kw_only(), "stream"_a = nullptr, R"pbdoc( + Executes the Gaussian operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Gaussian operator - for more details and usage examples. Args: src (cvcuda.ImageBatchVarShape): Input image batch containing one or more images. @@ -204,11 +169,7 @@ void ExportOpGaussian(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.ImageBatchVarShape: The output image batch (same as dst). )pbdoc"); } diff --git a/python/mod_cvcuda/operators/OpGaussianNoise.cpp b/python/mod_cvcuda/operators/OpGaussianNoise.cpp index 98a798d5d..3c8bbfaea 100644 --- a/python/mod_cvcuda/operators/OpGaussianNoise.cpp +++ b/python/mod_cvcuda/operators/OpGaussianNoise.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,6 +16,7 @@ */ #include "Operators.hpp" +#include "VarShapeUtils.hpp" #include #include @@ -26,9 +27,18 @@ #include #include +#include + namespace cvcudapy { namespace { + +class GaussianNoiseError : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + Tensor GaussianNoiseInto(Tensor &output, Tensor &input, Tensor &mu, Tensor &sigma, bool per_channel, unsigned long long seed, std::optional pstream) { @@ -37,15 +47,17 @@ Tensor GaussianNoiseInto(Tensor &output, Tensor &input, Tensor &mu, Tensor &sigm pstream = Stream::Current(); } - nvcv::TensorShape shape = input.shape(); - auto gaussiannoise = CreateOperator((int)shape[0]); + // HWC inputs (rank 3) have no N dim, so shape[0] is H — fall back to 1. + int batchSize = (input.shape().size() == 4) ? (int)input.shape()[0] : 1; + auto gaussiannoise = CreateOperator(batchSize); ResourceGuard guard(*pstream); guard.add(LockMode::LOCK_MODE_READ, {input, mu, sigma}); guard.add(LockMode::LOCK_MODE_WRITE, {output}); guard.add(LockMode::LOCK_MODE_READWRITE, {*gaussiannoise}); - gaussiannoise->submit(pstream->cudaHandle(), input, output, mu, sigma, per_channel, seed); + guard.run([&gaussiannoise, &pstream, &input, &output, &mu, &sigma, &per_channel, &seed]() + { gaussiannoise->submit(pstream->cudaHandle(), input, output, mu, sigma, per_channel, seed); }); return output; } @@ -58,6 +70,40 @@ Tensor GaussianNoise(Tensor &input, Tensor &mu, Tensor &sigma, bool per_channel, return GaussianNoiseInto(output, input, mu, sigma, per_channel, seed, pstream); } +Tensor GaussianNoiseScalarInto(Tensor &output, Tensor &input, float mu, float sigma, bool per_channel, + std::optional seed, bool clip, std::optional pstream) +{ + if (!pstream) + { + pstream = Stream::Current(); + } + + int batchSize = input.shape().size() == 4 ? static_cast(input.shape()[0]) : 1; + auto gaussiannoise = CreateOperator(batchSize); + auto seedValue = seed.value_or(0); + bool reseed = seed.has_value(); + + ResourceGuard guard(*pstream); + guard.add(LockMode::LOCK_MODE_READ, {input}); + guard.add(LockMode::LOCK_MODE_WRITE, {output}); + guard.add(LockMode::LOCK_MODE_READWRITE, {*gaussiannoise}); + + guard.run( + [gaussiannoise, pstream, &input, &output, mu, sigma, per_channel, seedValue, reseed, clip]() { + gaussiannoise->submit(pstream->cudaHandle(), input, output, mu, sigma, per_channel, seedValue, reseed, + clip); + }); + + return output; +} + +Tensor GaussianNoiseScalar(Tensor &input, float mu, float sigma, bool per_channel, + std::optional seed, bool clip, std::optional pstream) +{ + Tensor output = Tensor::Create(input.shape(), input.dtype()); + return GaussianNoiseScalarInto(output, input, mu, sigma, per_channel, seed, clip, pstream); +} + ImageBatchVarShape GaussianNoiseVarShapeInto(ImageBatchVarShape &output, ImageBatchVarShape &input, Tensor &mu, Tensor &sigma, bool per_channel, unsigned long long seed, std::optional pstream) @@ -74,7 +120,8 @@ ImageBatchVarShape GaussianNoiseVarShapeInto(ImageBatchVarShape &output, ImageBa guard.add(LockMode::LOCK_MODE_WRITE, {output}); guard.add(LockMode::LOCK_MODE_READWRITE, {*gaussiannoise}); - gaussiannoise->submit(pstream->cudaHandle(), input, output, mu, sigma, per_channel, seed); + guard.run([&gaussiannoise, &pstream, &input, &output, &mu, &sigma, &per_channel, &seed]() + { gaussiannoise->submit(pstream->cudaHandle(), input, output, mu, sigma, per_channel, seed); }); return output; } @@ -82,19 +129,13 @@ ImageBatchVarShape GaussianNoiseVarShapeInto(ImageBatchVarShape &output, ImageBa ImageBatchVarShape GaussianNoiseVarShape(ImageBatchVarShape &input, Tensor &mu, Tensor &sigma, bool per_channel, unsigned long long seed, std::optional pstream) { - ImageBatchVarShape output = ImageBatchVarShape::Create(input.numImages()); - auto format = input.uniqueFormat(); if (!format) { - throw std::runtime_error("All images in input must have the same format."); + throw GaussianNoiseError("All images in input must have the same format."); } - for (auto img = input.begin(); img != input.end(); ++img) - { - auto newimg = Image::Create(img->size(), format); - output.pushBack(newimg); - } + ImageBatchVarShape output = CreateSameShapeImageBatch(input, format, input.numImages()); return GaussianNoiseVarShapeInto(output, input, mu, sigma, per_channel, seed, pstream); } @@ -105,19 +146,10 @@ void ExportOpGaussianNoise(py::module &m) { using namespace pybind11::literals; - py::options options; - options.disable_function_signatures(); - - m.def("gaussiannoise", &GaussianNoise, "src"_a, "mu"_a, "sigma"_a, "per_channel"_a, "seed"_a, py::kw_only(), - "stream"_a = nullptr, R"pbdoc( - - cvcuda.gaussiannoise(src: cvcuda.Tensor, mu: cvcuda.Tensor, sigma: cvcuda.Tensor, per_channel: bool, seed: int, stream: Optional[cvcuda.Stream] = None) -> cvcuda.Tensor - + m.def("gaussiannoise", NvtxTrace("cvcuda.gaussiannoise", &GaussianNoise), "src"_a, "mu"_a, "sigma"_a, + "per_channel"_a, "seed"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( Executes the GaussianNoise operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the GaussianNoise operator - for more details and usage examples. Args: src (cvcuda.Tensor): Input image batch containing one or more images. @@ -130,21 +162,12 @@ void ExportOpGaussianNoise(py::module &m) Returns: cvcuda.Tensor: The output image batch. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("gaussiannoise_into", &GaussianNoiseInto, "dst"_a, "src"_a, "mu"_a, "sigma"_a, "per_channel"_a, "seed"_a, - py::kw_only(), "stream"_a = nullptr, R"pbdoc( - - cvcuda.gaussiannoise_into(dst: cvcuda.Tensor, src: cvcuda.Tensor, mu: cvcuda.Tensor, sigma: cvcuda.Tensor, per_channel: bool, seed: int, stream: Optional[cvcuda.Stream] = None) - + m.def("gaussiannoise_into", NvtxTrace("cvcuda.gaussiannoise_into", &GaussianNoiseInto), "dst"_a, "src"_a, "mu"_a, + "sigma"_a, "per_channel"_a, "seed"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( Executes the GaussianNoise operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the GaussianNoise operator - for more details and usage examples. Args: dst (cvcuda.Tensor): Output image batch containing the result of the operation. @@ -156,23 +179,54 @@ void ExportOpGaussianNoise(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None + cvcuda.Tensor: The output tensor (same as dst). + )pbdoc"); + + m.def("gaussiannoise", NvtxTrace("cvcuda.gaussiannoise", &GaussianNoiseScalar), "src"_a, "mu"_a, "sigma"_a, + "per_channel"_a, py::kw_only(), "seed"_a = std::nullopt, "clip"_a = true, "stream"_a = nullptr, R"pbdoc( + Adds Gaussian noise using scalar mean and standard-deviation values. + + This overload passes mu and sigma by value, so it does not allocate or upload parameter tensors. When seed is + None, a cached CV-CUDA random-number stream advances between calls. Supplying a seed forcibly reseeds the call, + so repeating the same explicit seed reproduces the same result. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + Args: + src (cvcuda.Tensor): Input image tensor. + mu (float): Gaussian mean in input-value units. + sigma (float): Non-negative Gaussian standard deviation in input-value units. + per_channel (bool): Whether to generate independent noise for every channel. + seed (int, optional): Non-negative 64-bit seed. None advances the cached random-number stream. + clip (bool, optional): Clamp uint8 to [0, 255] and float32 to [0, 1]. With False, float32 remains unbounded + and uint8 wraps modulo 256. + stream (cvcuda.Stream, optional): CUDA stream on which to perform the operation. + + Returns: + cvcuda.Tensor: The output image tensor. )pbdoc"); - m.def("gaussiannoise", &GaussianNoiseVarShape, "src"_a, "mu"_a, "sigma"_a, "per_channel"_a, "seed"_a, py::kw_only(), + m.def("gaussiannoise_into", NvtxTrace("cvcuda.gaussiannoise_into", &GaussianNoiseScalarInto), "dst"_a, "src"_a, + "mu"_a, "sigma"_a, "per_channel"_a, py::kw_only(), "seed"_a = std::nullopt, "clip"_a = true, "stream"_a = nullptr, R"pbdoc( + Adds Gaussian noise using scalar mean and standard-deviation values into a supplied tensor. - cvcuda.gaussiannoise(src: cvcuda.ImageBatchVarShape, mu: cvcuda.Tensor, sigma: cvcuda.Tensor, per_channel: bool, seed: int, stream: Optional[cvcuda.Stream] = None) -> cvcuda.ImageBatchVarShape + Args: + dst (cvcuda.Tensor): Output tensor. + src (cvcuda.Tensor): Input image tensor. + mu (float): Gaussian mean in input-value units. + sigma (float): Non-negative Gaussian standard deviation in input-value units. + per_channel (bool): Whether to generate independent noise for every channel. + seed (int, optional): Non-negative 64-bit seed. None advances the cached random-number stream. + clip (bool, optional): Whether to clamp output to the image dtype's expected range. + stream (cvcuda.Stream, optional): CUDA stream on which to perform the operation. + Returns: + cvcuda.Tensor: The output tensor (same as dst). + )pbdoc"); + + m.def("gaussiannoise", NvtxTrace("cvcuda.gaussiannoise", &GaussianNoiseVarShape), "src"_a, "mu"_a, "sigma"_a, + "per_channel"_a, "seed"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( Executes the GaussianNoise operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the GaussianNoise operator - for more details and usage examples. Args: src (ImageBatchVarShape): Input image batch containing one or more images. @@ -185,21 +239,12 @@ void ExportOpGaussianNoise(py::module &m) Returns: cvcuda.ImageBatchVarShape: The output image batch. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("gaussiannoise_into", &GaussianNoiseVarShapeInto, "dst"_a, "src"_a, "mu"_a, "sigma"_a, "per_channel"_a, - "seed"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - - cvcuda.gaussiannoise_into(dst: cvcuda.ImageBatchVarShape, src: cvcuda.ImageBatchVarShape, mu: cvcuda.Tensor, sigma: cvcuda.Tensor, per_channel: bool, seed: int, stream: Optional[cvcuda.Stream] = None) - - Executes the GaussianNoise operation on the given cuda stream. + m.def("gaussiannoise_into", NvtxTrace("cvcuda.gaussiannoise_into", &GaussianNoiseVarShapeInto), "dst"_a, "src"_a, + "mu"_a, "sigma"_a, "per_channel"_a, "seed"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( + Executes the GaussianNoise operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the GaussianNoise operator - for more details and usage examples. Args: dst (ImageBatchVarShape): Output image batch containing the result of the operation. @@ -211,11 +256,7 @@ void ExportOpGaussianNoise(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.ImageBatchVarShape: The output image batch (same as dst). )pbdoc"); } diff --git a/python/mod_cvcuda/operators/OpHQResize.cpp b/python/mod_cvcuda/operators/OpHQResize.cpp index c7a0599d8..5f70bfb9d 100644 --- a/python/mod_cvcuda/operators/OpHQResize.cpp +++ b/python/mod_cvcuda/operators/OpHQResize.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -29,6 +29,13 @@ #include #include +#include +#include +#include +#include +#include +#include + namespace cvcudapy { namespace { @@ -36,6 +43,12 @@ namespace { using Roi = pybind11::tuple; using Rois = std::vector; +class HQResizeError : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + inline void GetMinMagInterpolation(NVCVInterpolationType &minInterpolationArg, NVCVInterpolationType &magInterpolationArg, const std::optional &interpolation, @@ -67,19 +80,18 @@ inline void GetMinMagInterpolation(NVCVInterpolationType &m inline void ParseRoi(HQResizeRoiF &parsedRoi, const Roi &roi, int ndim) { assert(ndim == 2 || ndim == 3); - auto roiSize = roi.size(); - if (roiSize != static_cast(2 * ndim)) + if (auto roiSize = roi.size(); roiSize != static_cast(2 * ndim)) { if (ndim == 2) { - throw std::runtime_error( + throw HQResizeError( "Got wrong number of ROI components. For image resize, 4 integers are expected: " "low_height, low_width, high_height, high_width describing the bounding box for " "the input."); } else { - throw std::runtime_error( + throw HQResizeError( "Got wrong number of ROI components. For volumetric data, 6 integers are expected: " "low_depth, low_height, low_width, high_depth, high_height, high_width " "describing the bounding box for the input."); @@ -126,8 +138,8 @@ class RoiHelper HQResizeRoisF NonOwningHandle() { - int32_t size = m_rois.size(); - HQResizeRoiF *data = size == 0 ? nullptr : m_rois.data(); + auto size = static_cast(m_rois.size()); + const HQResizeRoiF *data = size == 0 ? nullptr : m_rois.data(); return {size, m_ndim, data}; } @@ -141,108 +153,259 @@ inline HQResizeTensorShapeI TensorShape(const nvcv::TensorLayout &layout, const { assert(resizeNDim == 2 || resizeNDim == 3); - char shapeArgLayout[4] = "DHW"; - HQResizeTensorShapeI tensorShape; + constexpr std::array shapeArgLayout = {'D', 'H', 'W'}; + HQResizeTensorShapeI tensorShape{}; for (int d = 0; d < resizeNDim; d++) { int axis = layout.find(shapeArgLayout[d + 3 - resizeNDim]); if (axis < 0) { - throw std::runtime_error( + throw HQResizeError( "The layout of an input tensor to the resize operator must contain HW extents in the layout (for " "images) or DHW extents (for 3D resampling). Some extents are missing in the input tensor."); } - tensorShape.extent[d] = shape[axis]; + tensorShape.extent[d] = static_cast(shape[axis]); } int channelAxis = layout.find('C'); - tensorShape.numChannels = channelAxis < 0 ? 1 : shape[channelAxis]; + tensorShape.numChannels = channelAxis < 0 ? 1 : static_cast(shape[channelAxis]); tensorShape.ndim = resizeNDim; return tensorShape; } +inline bool IsPlanarTensorBatchLayout(const nvcv::TensorLayout &layout) +{ + return layout == nvcv::TENSOR_NCHW || layout == nvcv::TENSOR_CHW; +} + +template +bool VisitExpandedTensorBatchShapes(const TensorBatch &batch, int resizeNDim, const nvcv::TensorLayout &layout, + Visitor &&visitor) +{ + const bool planar = IsPlanarTensorBatchLayout(layout); + for (int i = 0; i < batch.numTensors(); i++) + { + HQResizeTensorShapeI shape = TensorShape(layout, batch[i].shape(), resizeNDim); + const int reps = planar ? shape.numChannels : 1; + if (planar) + { + shape.numChannels = 1; + } + + for (int r = 0; r < reps; r++) + { + if (!visitor(shape)) + { + return false; + } + } + } + return true; +} + class BatchShapesHelper { public: - BatchShapesHelper(const nvcv::ImageBatchVarShape &batch) + explicit BatchShapesHelper(const nvcv::ImageBatchVarShape &batch) + : m_ndim(2) { - int32_t numSamples = batch.numImages(); - m_shapes.resize(numSamples); - m_ndim = 2; - m_numChannels = batch.uniqueFormat().numChannels(); - for (int i = 0; i < numSamples; i++) + // Planar (multi-plane, e.g. RGB8p) batches are processed plane-by-plane: the operator expands + // to numImages*channels single-channel samples, so the workspace must be sized the same way. + const auto fmt = batch.uniqueFormat(); + const int32_t planes = fmt ? fmt.numPlanes() : 1; + const int32_t fmtChannels = fmt ? fmt.numChannels() : 1; + const bool planar = planes > 1; + m_numChannels = planar ? 1 : fmtChannels; + const int32_t reps = planar ? planes : 1; + + int32_t numImages = batch.numImages(); + m_shapes.reserve(static_cast(numImages) * reps); + for (int i = 0; i < numImages; i++) { const auto &imgShape = batch[i].size(); - auto &shape = m_shapes[i]; - shape.extent[0] = imgShape.h; - shape.extent[1] = imgShape.w; + for (int r = 0; r < reps; r++) + { + HQResizeTensorShapeI shape{}; + shape.extent[0] = imgShape.h; + shape.extent[1] = imgShape.w; + shape.ndim = 2; + shape.numChannels = m_numChannels; + m_shapes.push_back(shape); + } } } - BatchShapesHelper(const TensorBatch &batch) + explicit BatchShapesHelper(const TensorBatch &batch) + : m_ndim(batch.layout().find('D') >= 0 ? 3 : 2) + , m_numChannels(-1) + , m_layout(batch.layout()) + , m_dtype(batch.dtype()) + , m_numTensors(batch.numTensors()) { - int32_t numSamples = batch.numTensors(); - auto layout = batch.layout(); - bool hasDepth = layout.find('D') >= 0; - m_ndim = hasDepth ? 3 : 2; - m_numChannels = -1; - m_shapes.resize(numSamples); - for (int i = 0; i < numSamples; i++) + const bool planar = IsPlanarTensorBatchLayout(m_layout); + if (planar) { - const auto &tensor = batch[i]; - m_shapes[i] = TensorShape(layout, tensor.shape(), m_ndim); - if (i == 0) - { - m_numChannels = m_shapes[i].numChannels; - } - else if (m_numChannels != m_shapes[i].numChannels) + // Each channel plane becomes a single-channel sample; matches the operator's expansion. + m_numChannels = 1; + } + m_shapes.reserve(m_numTensors); + + auto appendShape = [this, planar](const HQResizeTensorShapeI &shape) + { + if (!planar) { - m_numChannels = -1; + if (m_shapes.empty()) + { + m_numChannels = shape.numChannels; + } + else if (m_numChannels != shape.numChannels) + { + m_numChannels = -1; + } } - } + m_shapes.push_back(shape); + return true; + }; + VisitExpandedTensorBatchShapes(batch, m_ndim, m_layout, appendShape); } HQResizeTensorShapesI NonOwningHandle() { - int32_t size = m_shapes.size(); + auto size = static_cast(m_shapes.size()); return {size ? m_shapes.data() : nullptr, size, m_ndim, m_numChannels}; } + bool Matches(const TensorBatch &batch) const + { + if (batch.numTensors() != m_numTensors || batch.layout() != m_layout || batch.dtype() != m_dtype) + { + return false; + } + + size_t shapeIndex = 0; + auto compareShape = [this, &shapeIndex](const HQResizeTensorShapeI &shape) + { + if (shapeIndex >= m_shapes.size() || !SameShape(m_shapes[shapeIndex], shape)) + { + return false; + } + shapeIndex++; + return true; + }; + return VisitExpandedTensorBatchShapes(batch, m_ndim, m_layout, compareShape) && shapeIndex == m_shapes.size(); + } + private: + static bool SameShape(const HQResizeTensorShapeI &lhs, const HQResizeTensorShapeI &rhs) + { + if (lhs.ndim != rhs.ndim || lhs.numChannels != rhs.numChannels) + { + return false; + } + for (int d = 0; d < lhs.ndim; d++) + { + if (lhs.extent[d] != rhs.extent[d]) + { + return false; + } + } + return true; + } + int32_t m_ndim; int32_t m_numChannels; std::vector m_shapes; + nvcv::TensorLayout m_layout; + nvcv::DataType m_dtype; + int32_t m_numTensors{-1}; +}; + +class TensorBatchRequirementsCache +{ +public: + TensorBatchRequirementsCache(BatchShapesHelper inShapes, BatchShapesHelper outShapes, + NVCVInterpolationType minInterpolation, NVCVInterpolationType magInterpolation, + bool antialias, HQResizeRoisF rois, const cvcuda::WorkspaceRequirements &requirements) + : m_inShapes(std::move(inShapes)) + , m_outShapes(std::move(outShapes)) + , m_minInterpolation(minInterpolation) + , m_magInterpolation(magInterpolation) + , m_antialias(antialias) + , m_roiNDim(rois.ndim) + , m_requirements(requirements) + { + if (rois.size > 0) + { + m_rois.assign(rois.roi, rois.roi + rois.size); + } + } + + bool Matches(const TensorBatch &in, const TensorBatch &out, NVCVInterpolationType minInterpolation, + NVCVInterpolationType magInterpolation, bool antialias, HQResizeRoisF rois) const + { + if (minInterpolation != m_minInterpolation || magInterpolation != m_magInterpolation || antialias != m_antialias + || rois.ndim != m_roiNDim || rois.size != static_cast(m_rois.size()) || !m_inShapes.Matches(in) + || !m_outShapes.Matches(out)) + { + return false; + } + + for (int i = 0; i < rois.size; i++) + { + for (int d = 0; d < rois.ndim; d++) + { + if (m_rois[i].lo[d] != rois.roi[i].lo[d] || m_rois[i].hi[d] != rois.roi[i].hi[d]) + { + return false; + } + } + } + return true; + } + + const cvcuda::WorkspaceRequirements &requirements() const + { + return m_requirements; + } + +private: + BatchShapesHelper m_inShapes; + BatchShapesHelper m_outShapes; + NVCVInterpolationType m_minInterpolation; + NVCVInterpolationType m_magInterpolation; + bool m_antialias; + int32_t m_roiNDim; + std::vector m_rois; + cvcuda::WorkspaceRequirements m_requirements; }; inline Shape ResizedTensorShape(const nvcv::TensorLayout &srcLayout, const nvcv::TensorShape &srcShape, const Shape &outShape) { - int resizeNDim = outShape.size(); + auto resizeNDim = static_cast(outShape.size()); if (resizeNDim != 2 && resizeNDim != 3) { - throw std::runtime_error( + throw HQResizeError( "The `out_shape` must be a tuple of 2 or 3 integers (for 2D or 3D resampling respectively)."); } - bool hasDepth = srcLayout.find('D') >= 0; - int expectedNDim = hasDepth ? 3 : 2; - - if (expectedNDim != resizeNDim) + bool hasDepth = srcLayout.find('D') >= 0; + if (int expectedNDim = hasDepth ? 3 : 2; expectedNDim != resizeNDim) { if (hasDepth) { - throw std::runtime_error( + throw HQResizeError( "The input tensor contains depth extent (`D`) in the layout. For 3D resize, please specify the resized " "shape for 3 extents: depth, height, and width. Got 2 extents."); } else { - throw std::runtime_error( + throw HQResizeError( "Expected the resized shape to consists of 2 integers: for resized height and width. Got 3 integers."); } } - char shapeArgLayout[4] = "DHW"; - int shapeArg[3]; + constexpr std::array shapeArgLayout = {'D', 'H', 'W'}; + std::array shapeArg = {}; for (int d = 0; d < resizeNDim; d++) { shapeArg[d] = outShape[d].cast(); @@ -260,7 +423,7 @@ inline Shape ResizedTensorShape(const nvcv::TensorLayout &srcLayout, const nvcv: int axis = srcLayout.find(shapeArgLayout[d + 3 - resizeNDim]); if (axis < 0) { - throw std::runtime_error( + throw HQResizeError( "The layout of an input tensor to the resize operator must contain HW extents in the layout (for " "images) or DHW extents (for 3D resampling). Some extents are missing in the input tensor."); } @@ -269,7 +432,7 @@ inline Shape ResizedTensorShape(const nvcv::TensorLayout &srcLayout, const nvcv: return resizedShape; } -class PyOpHQResize : public nvcvpy::Container +class PyOpHQResize : public nvcvpy::Container // NOSONAR: operator wrappers share the Python cache hierarchy. { public: // Define a Key class to be used by the cache to fetch similar items for potential reuse. @@ -277,7 +440,7 @@ class PyOpHQResize : public nvcvpy::Container { public: // the filters are generated by the operator constructor for a given device - Key(int deviceId) + explicit Key(int deviceId) : m_deviceId{deviceId} { } @@ -290,25 +453,24 @@ class PyOpHQResize : public nvcvpy::Container bool doIsCompatible(const nvcvpy::IKey &that_) const override { - const Key *thatKey = dynamic_cast(&that_); + const auto *thatKey = dynamic_cast(&that_); return thatKey != nullptr && thatKey->m_deviceId == m_deviceId; } int m_deviceId; }; - PyOpHQResize(int deviceId) + explicit PyOpHQResize(int deviceId) : m_key(deviceId) - , m_op() { } void submit(cudaStream_t stream, const Tensor &in, const Tensor &out, const NVCVInterpolationType minInterpolation, - const NVCVInterpolationType magInterpolation, bool antialias, const HQResizeRoiF *roi) + const NVCVInterpolationType magInterpolation, bool antialias, const HQResizeRoiF *roi) const { if (in.layout() != out.layout()) { - throw std::runtime_error("Input and output tensors must have the same layout"); + throw HQResizeError("Input and output tensors must have the same layout"); } int resizeNDim = in.layout().find('D') >= 0 ? 3 : 2; @@ -316,18 +478,17 @@ class PyOpHQResize : public nvcvpy::Container auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(in.exportData()); if (!inAccess) { - throw std::runtime_error("Incompatible input tensor layout"); + throw HQResizeError("Incompatible input tensor layout"); } - auto outAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(out.exportData()); - if (!outAccess) + if (auto outAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(out.exportData()); !outAccess) { - throw std::runtime_error("Incompatible input tensor layout"); + throw HQResizeError("Incompatible input tensor layout"); } - int numSamples = inAccess->numSamples(); - HQResizeTensorShapeI inShape = TensorShape(in.layout(), in.shape(), resizeNDim); - HQResizeTensorShapeI outShape = TensorShape(out.layout(), out.shape(), resizeNDim); + auto numSamples = static_cast(inAccess->numSamples()); + auto inShape = TensorShape(in.layout(), in.shape(), resizeNDim); + auto outShape = TensorShape(out.layout(), out.shape(), resizeNDim); auto req = m_op.getWorkspaceRequirements(numSamples, inShape, outShape, minInterpolation, magInterpolation, antialias, roi); @@ -337,38 +498,69 @@ class PyOpHQResize : public nvcvpy::Container void submit(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, const NVCVInterpolationType minInterpolation, const NVCVInterpolationType magInterpolation, - bool antialias, const HQResizeRoisF rois) + bool antialias, const HQResizeRoisF rois) const { BatchShapesHelper inShapes(in); BatchShapesHelper outShapes(out); - auto req - = m_op.getWorkspaceRequirements(in.numImages(), inShapes.NonOwningHandle(), outShapes.NonOwningHandle(), - minInterpolation, magInterpolation, antialias, rois); - auto ws = WorkspaceCache::instance().get(req, stream); + // numSamples must be the (possibly plane-expanded) shape count, not the image count, so the + // workspace metadata matches the operator's internal planar expansion. + auto inHandle = inShapes.NonOwningHandle(); + auto outHandle = outShapes.NonOwningHandle(); + auto req = m_op.getWorkspaceRequirements(inHandle.size, inHandle, outHandle, minInterpolation, magInterpolation, + antialias, rois); + auto ws = WorkspaceCache::instance().get(req, stream); m_op(stream, ws.get(), in, out, minInterpolation, magInterpolation, antialias, rois); } void submit(cudaStream_t stream, const TensorBatch &in, const TensorBatch &out, const NVCVInterpolationType minInterpolation, const NVCVInterpolationType magInterpolation, - bool antialias, const HQResizeRoisF rois) + bool antialias, const HQResizeRoisF rois) const { if (in.layout() != out.layout()) { - throw std::runtime_error("Input and output batches must have the same layout"); + throw HQResizeError("Input and output batches must have the same layout"); } - BatchShapesHelper inShapes(in); - BatchShapesHelper outShapes(out); - auto req - = m_op.getWorkspaceRequirements(in.numTensors(), inShapes.NonOwningHandle(), outShapes.NonOwningHandle(), - minInterpolation, magInterpolation, antialias, rois); - auto ws = WorkspaceCache::instance().get(req, stream); + + std::optional req; + { + std::scoped_lock lock(m_tensorBatchRequirementsMutex); + if (m_tensorBatchRequirements + && m_tensorBatchRequirements->Matches(in, out, minInterpolation, magInterpolation, antialias, rois)) + { + req = m_tensorBatchRequirements->requirements(); + } + } + + if (!req) + { + BatchShapesHelper inShapes(in); + BatchShapesHelper outShapes(out); + // numSamples must be the (possibly plane-expanded) shape count, not the tensor count. + auto inHandle = inShapes.NonOwningHandle(); + auto outHandle = outShapes.NonOwningHandle(); + req = m_op.getWorkspaceRequirements(inHandle.size, inHandle, outHandle, minInterpolation, magInterpolation, + antialias, rois); + + // Keep retained metadata bounded even if a caller submits an unusually large batch. + constexpr int32_t kMaxCachedExpandedSamples = 4096; + if (inHandle.size <= kMaxCachedExpandedSamples && outHandle.size <= kMaxCachedExpandedSamples + && rois.size <= kMaxCachedExpandedSamples) + { + TensorBatchRequirementsCache entry(std::move(inShapes), std::move(outShapes), minInterpolation, + magInterpolation, antialias, rois, *req); + std::scoped_lock lock(m_tensorBatchRequirementsMutex); + m_tensorBatchRequirements = std::move(entry); + } + } + + auto ws = WorkspaceCache::instance().get(*req, stream); m_op(stream, ws.get(), in, out, minInterpolation, magInterpolation, antialias, rois); } // Required override to get the py object container. py::object container() const override { - return *this; + return py::reinterpret_borrow(this->ptr()); } // Required override to get the key as the base interface class. @@ -384,8 +576,10 @@ class PyOpHQResize : public nvcvpy::Container } private: - Key m_key; - cvcuda::HQResize m_op; + Key m_key; + cvcuda::HQResize m_op; + mutable std::mutex m_tensorBatchRequirementsMutex; + mutable std::optional m_tensorBatchRequirements; }; template @@ -396,7 +590,7 @@ auto RunGuard(Op &op, Src &src, Dst &dst, Stream &stream, Call &&call) guard.add(LockMode::LOCK_MODE_WRITE, {dst}); guard.add(LockMode::LOCK_MODE_NONE, {*op}); - call(); + guard.run(std::forward(call)); } auto CreatePyOpHQResize() @@ -419,11 +613,12 @@ Tensor TensorHQResizeInto(Tensor &dst, Tensor &src, std::optional antialia RoiHelper parsedRoi(maybeRoi, resizeNDim); const HQResizeRoiF *roi = parsedRoi.NonOwningHandle().roi; - NVCVInterpolationType minInterpolationArg, magInterpolationArg; + NVCVInterpolationType minInterpolationArg; + NVCVInterpolationType magInterpolationArg; GetMinMagInterpolation(minInterpolationArg, magInterpolationArg, interpolation, minInterpolation, magInterpolation); RunGuard(op, src, dst, stream, - [&]() + [&op, &stream, &src, &dst, &minInterpolationArg, &magInterpolationArg, &antialias, &roi]() { op->submit(stream.cudaHandle(), src, dst, minInterpolationArg, magInterpolationArg, antialias.value_or(false), roi); @@ -452,11 +647,12 @@ ImageBatchVarShape VarShapeHQResizeInto(ImageBatchVarShape &dst, const ImageBatc auto op = CreatePyOpHQResize(); RoiHelper parsedRoi(roi, 2); - NVCVInterpolationType minInterpolationArg, magInterpolationArg; + NVCVInterpolationType minInterpolationArg; + NVCVInterpolationType magInterpolationArg; GetMinMagInterpolation(minInterpolationArg, magInterpolationArg, interpolation, minInterpolation, magInterpolation); RunGuard(op, src, dst, stream, - [&]() + [&op, &stream, &src, &dst, &minInterpolationArg, &magInterpolationArg, &antialias, &parsedRoi]() { op->submit(stream.cudaHandle(), src, dst, minInterpolationArg, magInterpolationArg, antialias.value_or(false), parsedRoi.NonOwningHandle()); @@ -471,21 +667,21 @@ ImageBatchVarShape VarShapeHQResize(ImageBatchVarShape &src, const std::vector magInterpolation, std::optional pstream) { - ImageBatchVarShape out = ImageBatchVarShape::Create(src.capacity()); + auto out = ImageBatchVarShape::Create(src.capacity()); - int32_t numOutSizes = outShape.size(); + auto numOutSizes = static_cast(outShape.size()); if (numOutSizes != src.numImages() && numOutSizes != 1) { - throw std::runtime_error( + throw HQResizeError( "The list of output shapes `out_size` must either contain a single shape to be used for all output images " "or its length must match the number of input samples."); } for (int i = 0; i < src.numImages(); ++i) { - auto size = outShape[numOutSizes == 1 ? 0 : i]; - auto image = Image::Create({std::get<1>(size), std::get<0>(size)}, src[i].format()); - out.pushBack(image); + auto [size0, size1] = outShape[numOutSizes == 1 ? 0 : i]; + auto image = Image::Create({size1, size0}, src[i].format()); + out.pushBackImage(image); } return VarShapeHQResizeInto(out, src, antialias, roi, interpolation, minInterpolation, magInterpolation, pstream); @@ -504,11 +700,12 @@ TensorBatch TensorBatchHQResizeInto(TensorBatch &dst, const TensorBatch &src, st int resizeNDim = hasDepth ? 3 : 2; RoiHelper parsedRoi(roi, resizeNDim); - NVCVInterpolationType minInterpolationArg, magInterpolationArg; + NVCVInterpolationType minInterpolationArg; + NVCVInterpolationType magInterpolationArg; GetMinMagInterpolation(minInterpolationArg, magInterpolationArg, interpolation, minInterpolation, magInterpolation); RunGuard(op, src, dst, stream, - [&]() + [&op, &stream, &src, &dst, &minInterpolationArg, &magInterpolationArg, &antialias, &parsedRoi]() { op->submit(stream.cudaHandle(), src, dst, minInterpolationArg, magInterpolationArg, antialias.value_or(false), parsedRoi.NonOwningHandle()); @@ -521,12 +718,12 @@ TensorBatch TensorBatchHQResize(TensorBatch &src, const std::vector &outS std::optional minInterpolation, std::optional magInterpolation, std::optional pstream) { - TensorBatch out = TensorBatch::Create(src.numTensors()); + auto out = TensorBatch::Create(src.numTensors()); - int32_t numOutSizes = outShape.size(); + auto numOutSizes = static_cast(outShape.size()); if (numOutSizes != src.numTensors() && numOutSizes != 1) { - throw std::runtime_error( + throw HQResizeError( "The list of output shapes `out_size` must either contain a single shape to be used for all output tensors " "or its length must match the number of input tensors."); } @@ -537,7 +734,7 @@ TensorBatch TensorBatchHQResize(TensorBatch &src, const std::vector &outS const auto &inSample = src[i]; auto resizedShape = ResizedTensorShape(inSample.layout(), inSample.shape(), sampleShape); Tensor dst = Tensor::Create(resizedShape, src.dtype(), src.layout()); - out.pushBack(dst); + out.pushBackTensor(dst); } return TensorBatchHQResizeInto(out, src, antialias, roi, interpolation, minInterpolation, magInterpolation, @@ -550,28 +747,25 @@ void ExportOpHQResize(py::module &m) { using namespace pybind11::literals; - m.def("hq_resize", &TensorHQResize, "src"_a, "out_size"_a, py::kw_only(), "antialias"_a = false, "roi"_a = nullptr, - "interpolation"_a = nullptr, "min_interpolation"_a = nullptr, "mag_interpolation"_a = nullptr, - "stream"_a = nullptr, R"pbdoc( + m.def("hq_resize", NvtxTrace("cvcuda.hq_resize", &TensorHQResize), "src"_a, "out_size"_a, py::kw_only(), + "antialias"_a = false, "roi"_a = nullptr, "interpolation"_a = nullptr, "min_interpolation"_a = nullptr, + "mag_interpolation"_a = nullptr, "stream"_a = nullptr, R"pbdoc( Executes the HQ Resize operation on the given cuda stream. The operator supports resampling for 2D (images) and 3D volumetric samples. - See also: - Refer to the CV-CUDA C API reference for the HQ Resize operator - for more details and usage examples. Args: src (cvcuda.Tensor): Input tensor containing one or more images. - The tensor layout must match: (N)(D)HW(C). + The tensor layout must match: (N)(D)HW(C). out_size (tuple): Tuple of 2 or 3 ints describing the output shape in (D)HW layout. antialias (bool): If set to true, an antialiasing is enabled for scaling down. roi (Tuple): Optional bounding box describing the input's region of interest. - For 2D resampling it should be (lowH, lowW, highH, highW), - for 3D: (lowD, lowH, lowW, highD, highH, highW). - If, for some axis, the low bound is bigger than the high bound, - the image is flipped across the axis. + For 2D resampling it should be (lowH, lowW, highH, highW), + for 3D: (lowD, lowH, lowW, highD, highH, highW). + If, for some axis, the low bound is bigger than the high bound, + the image is flipped across the axis. interpolation (cvcuda.Interp): Interpolation type used. Used both for scaling down and up, - cannot be specified together with (min_interpolation or mag_interpolation). + cannot be specified together with (min_interpolation or mag_interpolation). min_interpolation (cvcuda.Interp): Interpolation type used for scaling down. mag_interpolation (cvcuda.Interp): Interpolation type used for scaling up. stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. @@ -579,31 +773,25 @@ void ExportOpHQResize(py::module &m) Returns: cvcuda.Tensor: The output tensor. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("hq_resize", &VarShapeHQResize, "src"_a, "out_size"_a, py::kw_only(), "antialias"_a = false, - "roi"_a = nullptr, "interpolation"_a = nullptr, "min_interpolation"_a = nullptr, + m.def("hq_resize", NvtxTrace("cvcuda.hq_resize", &VarShapeHQResize), "src"_a, "out_size"_a, py::kw_only(), + "antialias"_a = false, "roi"_a = nullptr, "interpolation"_a = nullptr, "min_interpolation"_a = nullptr, "mag_interpolation"_a = nullptr, "stream"_a = nullptr, R"pbdoc( Executes the HQ Resize operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the HQ Resize operator - for more details and usage examples. Args: src (cvcuda.ImageBatchVarShape): Input batch of images. out_size (tuple): Tuple of 2 ints describing the output shape in HW layout. antialias (bool): If set to true, an antialiasing is enabled for scaling down. roi (List[Tuple[int]]): Optional bounding boxes describing the input's region of interest. - It should be a list of tuples. The list length must match the number - of input tensors or be 1 (so that the same ROI is used for all samples). - Each tuple must be of the form (lowH, lowW, highH, highW). - If, for some axis, the low bound is bigger than the high bound, - the image is flipped across the axis. + It should be a list of tuples. The list length must match the number + of input tensors or be 1 (so that the same ROI is used for all samples). + Each tuple must be of the form (lowH, lowW, highH, highW). + If, for some axis, the low bound is bigger than the high bound, + the image is flipped across the axis. interpolation (cvcuda.Interp): Interpolation type used. Used both for scaling down and up, - cannot be specified together with (min_interpolation or mag_interpolation). + cannot be specified together with (min_interpolation or mag_interpolation). min_interpolation (cvcuda.Interp): Interpolation type used for scaling down. mag_interpolation (cvcuda.Interp): Interpolation type used for scaling up. stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. @@ -611,34 +799,28 @@ void ExportOpHQResize(py::module &m) Returns: cvcuda.ImageBatchVarShape: The batch of resized images. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("hq_resize", &TensorBatchHQResize, "src"_a, "out_size"_a, py::kw_only(), "antialias"_a = false, - "roi"_a = nullptr, "interpolation"_a = nullptr, "min_interpolation"_a = nullptr, + m.def("hq_resize", NvtxTrace("cvcuda.hq_resize", &TensorBatchHQResize), "src"_a, "out_size"_a, py::kw_only(), + "antialias"_a = false, "roi"_a = nullptr, "interpolation"_a = nullptr, "min_interpolation"_a = nullptr, "mag_interpolation"_a = nullptr, "stream"_a = nullptr, R"pbdoc( Executes the HQ Resize operation on the given cuda stream. The operator supports resampling for 2D (images) and 3D volumetric samples. - See also: - Refer to the CV-CUDA C API reference for the HQ Resize operator - for more details and usage examples. Args: src (cvcuda.TensorBatch): Input batch containing one or more tensors of (D)HW(C) layout. out_size (tuple): Tuple of 2 or 3 ints describing the output shape in (D)HW layout. antialias (bool): If set to true, an antialiasing is enabled for scaling down. roi (List[Tuple[int]]): Optional bounding boxes describing the input's region of interest. - It should be a list of tuples. The list length must match the number - of input tensors or be 1 (so that the same ROI is used for all samples). - Each tuple must be of the form: - * for 2D resampling: (lowH, lowW, highH, highW), - * for 3D: (lowD, lowH, lowW, highD, highH, highW). - If, for some axis, the low bound is bigger than the high bound, - the tensor is flipped across the axis. + It should be a list of tuples. The list length must match the number + of input tensors or be 1 (so that the same ROI is used for all samples). + Each tuple must be of the form: + * for 2D resampling: (lowH, lowW, highH, highW), + * for 3D: (lowD, lowH, lowW, highD, highH, highW). + If, for some axis, the low bound is bigger than the high bound, + the tensor is flipped across the axis. interpolation (cvcuda.Interp): Interpolation type used. Used both for scaling down and up, - cannot be specified together with (min_interpolation or mag_interpolation). + cannot be specified together with (min_interpolation or mag_interpolation). min_interpolation (cvcuda.Interp): Interpolation type used for scaling down. mag_interpolation (cvcuda.Interp): Interpolation type used for scaling up. stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. @@ -646,34 +828,28 @@ void ExportOpHQResize(py::module &m) Returns: cvcuda.TensorBatch: The batch of resized tensors. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("hq_resize_into", &TensorHQResizeInto, "dst"_a, "src"_a, py::kw_only(), "antialias"_a = false, - "roi"_a = nullptr, "interpolation"_a = nullptr, "min_interpolation"_a = nullptr, + m.def("hq_resize_into", NvtxTrace("cvcuda.hq_resize_into", &TensorHQResizeInto), "dst"_a, "src"_a, py::kw_only(), + "antialias"_a = false, "roi"_a = nullptr, "interpolation"_a = nullptr, "min_interpolation"_a = nullptr, "mag_interpolation"_a = nullptr, "stream"_a = nullptr, R"pbdoc( Executes the HQ Resize operation on the given cuda stream. The operator supports resampling for 2D (images) and 3D volumetric samples. - See also: - Refer to the CV-CUDA C API reference for the HQ Resize operator - for more details and usage examples. Args: dst (cvcuda.Tensor): Output tensor. It's layout must match the src tensor. - The size of D, H, and W extents may be different. The dst - type must match the src's type or be float32. + The size of D, H, and W extents may be different. The dst + type must match the src's type or be float32. src (cvcuda.Tensor): Input tensor containing one or more images. - The tensor layout must match: (N)(D)HW(C). + The tensor layout must match: (N)(D)HW(C). antialias (bool): If set to true, an antialiasing is enabled for scaling down. roi (Tuple[int]): Optional bounding box describing the input's region of interest. - For 2D resampling it should be (lowH, lowW, highH, highW), - for 3D: (lowD, lowH, lowW, highD, highH, highW). - If, for some axis, the low bound is bigger than the high bound, - the image is flipped across the axis. + For 2D resampling it should be (lowH, lowW, highH, highW), + for 3D: (lowD, lowH, lowW, highD, highH, highW). + If, for some axis, the low bound is bigger than the high bound, + the image is flipped across the axis. interpolation (cvcuda.Interp): Interpolation type used. Used both for scaling down and up, - cannot be specified together with (min_interpolation or mag_interpolation). + cannot be specified together with (min_interpolation or mag_interpolation). min_interpolation (cvcuda.Interp): Interpolation type used for scaling down. mag_interpolation (cvcuda.Interp): Interpolation type used for scaling up. stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. @@ -681,33 +857,27 @@ void ExportOpHQResize(py::module &m) Returns: cvcuda.Tensor: The output tensor. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("hq_resize_into", &VarShapeHQResizeInto, "dst"_a, "src"_a, py::kw_only(), "antialias"_a = false, - "roi"_a = nullptr, "interpolation"_a = nullptr, "min_interpolation"_a = nullptr, + m.def("hq_resize_into", NvtxTrace("cvcuda.hq_resize_into", &VarShapeHQResizeInto), "dst"_a, "src"_a, py::kw_only(), + "antialias"_a = false, "roi"_a = nullptr, "interpolation"_a = nullptr, "min_interpolation"_a = nullptr, "mag_interpolation"_a = nullptr, "stream"_a = nullptr, R"pbdoc( Executes the HQ Resize operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the HQ Resize operator - for more details and usage examples. Args: dst (cvcuda.ImageBatchVarShape): Output batch. The layout must match the input batch. - The size of D, H, and W extents may be different. The dst - type must match the src's type or be float32. + The size of D, H, and W extents may be different. The dst + type must match the src's type or be float32. src (cvcuda.ImageBatchVarShape): Input batch of images. antialias (bool): If set to true, an antialiasing is enabled for scaling down. roi (List[Tuple[int]]): Optional bounding boxes describing the input's region of interest. - It should be a list of tuples. The list length must match the number - of input tensors or be 1 (so that the same ROI is used for all samples). - Each tuple must be of the form (lowH, lowW, highH, highW). - If, for some axis, the low bound is bigger than the high bound, - the image is flipped across the axis. + It should be a list of tuples. The list length must match the number + of input tensors or be 1 (so that the same ROI is used for all samples). + Each tuple must be of the form (lowH, lowW, highH, highW). + If, for some axis, the low bound is bigger than the high bound, + the image is flipped across the axis. interpolation (cvcuda.Interp): Interpolation type used. Used both for scaling down and up, - cannot be specified together with (min_interpolation or mag_interpolation). + cannot be specified together with (min_interpolation or mag_interpolation). min_interpolation (cvcuda.Interp): Interpolation type used for scaling down. mag_interpolation (cvcuda.Interp): Interpolation type used for scaling up. stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. @@ -715,36 +885,30 @@ void ExportOpHQResize(py::module &m) Returns: cvcuda.ImageBatchVarShape: The batch of resized images. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("hq_resize_into", &TensorBatchHQResizeInto, "dst"_a, "src"_a, py::kw_only(), "antialias"_a = false, - "roi"_a = nullptr, "interpolation"_a = nullptr, "min_interpolation"_a = nullptr, - "mag_interpolation"_a = nullptr, "stream"_a = nullptr, R"pbdoc( + m.def("hq_resize_into", NvtxTrace("cvcuda.hq_resize_into", &TensorBatchHQResizeInto), "dst"_a, "src"_a, + py::kw_only(), "antialias"_a = false, "roi"_a = nullptr, "interpolation"_a = nullptr, + "min_interpolation"_a = nullptr, "mag_interpolation"_a = nullptr, "stream"_a = nullptr, R"pbdoc( Executes the HQ Resize operation on the given cuda stream. The operator supports resampling for 2D (images) and 3D volumetric samples. - See also: - Refer to the CV-CUDA C API reference for the HQ Resize operator - for more details and usage examples. Args: dst (cvcuda.TensorBatch): Output batch. The layout must match the input batch. - The size of D, H, and W extents may be different. The dst - type must match the src's type or be float32. + The size of D, H, and W extents may be different. The dst + type must match the src's type or be float32. src (cvcuda.TensorBatch): Input batch containing one or more tensors of (D)HW(C) layout. antialias (bool): If set to true, an antialiasing is enabled for scaling down. roi (List[Tuple[int]]): Optional bounding boxes describing the input's region of interest. - It should be a list of tuples. The list length must match the number - of input tensors or be 1 (so that the same ROI is used for all samples). - Each tuple must be of the form: - * for 2D resampling: (lowH, lowW, highH, highW), - * for 3D: (lowD, lowH, lowW, highD, highH, highW). - If, for some axis, the low bound is bigger than the high bound, - the tensor is flipped across the axis. + It should be a list of tuples. The list length must match the number + of input tensors or be 1 (so that the same ROI is used for all samples). + Each tuple must be of the form: + * for 2D resampling: (lowH, lowW, highH, highW), + * for 3D: (lowD, lowH, lowW, highD, highH, highW). + If, for some axis, the low bound is bigger than the high bound, + the tensor is flipped across the axis. interpolation (cvcuda.Interp): Interpolation type used. Used both for scaling down and up, - cannot be specified together with (min_interpolation or mag_interpolation). + cannot be specified together with (min_interpolation or mag_interpolation). min_interpolation (cvcuda.Interp): Interpolation type used for scaling down. mag_interpolation (cvcuda.Interp): Interpolation type used for scaling up. stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. @@ -752,9 +916,6 @@ void ExportOpHQResize(py::module &m) Returns: cvcuda.TensorBatch: The batch of resized tensors. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); } diff --git a/python/mod_cvcuda/operators/OpHistogram.cpp b/python/mod_cvcuda/operators/OpHistogram.cpp index 82f6c2509..52232f3fe 100644 --- a/python/mod_cvcuda/operators/OpHistogram.cpp +++ b/python/mod_cvcuda/operators/OpHistogram.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,6 +23,8 @@ #include #include +#include + namespace cvcudapy { namespace { @@ -34,12 +36,9 @@ Tensor HistogramInto(Tensor &histogram, Tensor &input, std::optional mas pstream = Stream::Current(); } - if (mask) + if (mask && mask->shape() != input.shape()) { - if (mask->shape() != input.shape()) - { - throw std::invalid_argument("Mask must have the same shape as input"); - } + throw std::invalid_argument("Mask must have the same shape as input"); } auto op = CreateOperator(); @@ -52,18 +51,21 @@ Tensor HistogramInto(Tensor &histogram, Tensor &input, std::optional mas if (mask) { guard.add(LockMode::LOCK_MODE_READ, {*mask}); - op->submit(pstream->cudaHandle(), input, *mask, histogram); + guard.run([&op, &pstream, &input, &mask, &histogram]() + { op->submit(pstream->cudaHandle(), input, nvcv::OptionalTensorConstRef{*mask}, histogram); }); } else { - op->submit(pstream->cudaHandle(), input, nvcv::NullOpt, histogram); + guard.run( + [&op, &pstream, &input, &histogram]() + { op->submit(pstream->cudaHandle(), input, nvcv::OptionalTensorConstRef{nvcv::NullOpt}, histogram); }); } return std::move(histogram); } -Tensor Histogram(Tensor &input, std::optional mask, std::optional pstream) +Tensor Histogram(Tensor &input, const std::optional &mask, std::optional pstream) { - ssize_t shape[3]; + std::array shape; // check for non batched tensors if (input.shape().size() == 3) { @@ -79,10 +81,10 @@ Tensor Histogram(Tensor &input, std::optional mask, std::optional #include @@ -24,24 +25,34 @@ #include #include +#include + namespace cvcudapy { namespace { + +class HistogramEqError : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + Tensor HistogramEqInto(Tensor &output, Tensor &input, std::optional pstream) { if (!pstream) { pstream = Stream::Current(); } - nvcv::TensorShape shape = input.shape(); - auto op = CreateOperator((uint32_t)shape[0]); + // HWC inputs (rank 3) have no N dim, so shape[0] is H — fall back to 1. + uint32_t batchSize = (input.shape().size() == 4) ? (uint32_t)input.shape()[0] : 1u; + auto op = CreateOperator(batchSize); ResourceGuard guard(*pstream); guard.add(LockMode::LOCK_MODE_READ, {input}); guard.add(LockMode::LOCK_MODE_WRITE, {output}); guard.add(LockMode::LOCK_MODE_READWRITE, {*op}); - op->submit(pstream->cudaHandle(), input, output); + guard.run([&op, &pstream, &input, &output]() { op->submit(pstream->cudaHandle(), input, output); }); return std::move(output); } @@ -68,26 +79,20 @@ ImageBatchVarShape HistogramEqVarShapeInto(ImageBatchVarShape &output, ImageBatc guard.add(LockMode::LOCK_MODE_WRITE, {output}); guard.add(LockMode::LOCK_MODE_READWRITE, {*op}); - op->submit(pstream->cudaHandle(), input, output); + guard.run([&op, &pstream, &input, &output]() { op->submit(pstream->cudaHandle(), input, output); }); return output; } ImageBatchVarShape HistogramEqVarShape(ImageBatchVarShape &input, std::optional pstream) { - ImageBatchVarShape output = ImageBatchVarShape::Create(input.numImages()); - auto format = input.uniqueFormat(); if (!format) { - throw std::runtime_error("All images in input must have the same format."); + throw HistogramEqError("All images in input must have the same format."); } - for (auto img = input.begin(); img != input.end(); ++img) - { - auto newimg = Image::Create(img->size(), format); - output.pushBack(newimg); - } + ImageBatchVarShape output = CreateSameShapeImageBatch(input, format, input.numImages()); return HistogramEqVarShapeInto(output, input, pstream); } @@ -97,18 +102,11 @@ ImageBatchVarShape HistogramEqVarShape(ImageBatchVarShape &input, std::optional< void ExportOpHistogramEq(py::module &m) { using namespace pybind11::literals; - py::options options; - options.disable_function_signatures(); - - m.def("histogrameq", &HistogramEq, "src"_a, "dtype"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - - cvcuda.histogrameq(src: cvcuda.Tensor, dtype: numpy.dtype, stream: Optional[cvcuda.Stream] = None) -> cvcuda.Tensor + m.def("histogrameq", NvtxTrace("cvcuda.histogrameq", &HistogramEq), "src"_a, "dtype"_a, py::kw_only(), + "stream"_a = nullptr, R"pbdoc( Executes the histogram equalization operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Histogram Eq operator - for more details and usage examples. Args: src (cvcuda.Tensor): Input image batch containing one or more images. @@ -118,20 +116,12 @@ void ExportOpHistogramEq(py::module &m) Returns: cvcuda.Tensor: The output image batch. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("histogrameq_into", &HistogramEqInto, "dst"_a, "src"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - - cvcuda.histogrameq_into(dst: cvcuda.Tensor, src: cvcuda.Tensor, stream: Optional[cvcuda.Stream] = None) - + m.def("histogrameq_into", NvtxTrace("cvcuda.histogrameq_into", &HistogramEqInto), "dst"_a, "src"_a, py::kw_only(), + "stream"_a = nullptr, R"pbdoc( Executes the histogram equalization operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Histogram Eq operator - for more details and usage examples. Args: dst (cvcuda.Tensor): Output image batch containing the result of the operation. @@ -139,22 +129,13 @@ void ExportOpHistogramEq(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.Tensor: The output tensor (same as dst). )pbdoc"); - m.def("histogrameq", &HistogramEqVarShape, "src"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - - cvcuda.histogrameq(src: cvcuda.ImageBatchVarShape, stream: Optional[cvcuda.Stream] = None) -> cvcuda.ImageBatchVarShape - - Executes the histogram equalization operation on the given cuda stream. + m.def("histogrameq", NvtxTrace("cvcuda.histogrameq", &HistogramEqVarShape), "src"_a, py::kw_only(), + "stream"_a = nullptr, R"pbdoc( + Executes the histogram equalization operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the HistogramEq operator - for more details and usage examples. Args: src (cvcuda.ImageBatchVarShape): Input image batch containing one or more images. @@ -163,20 +144,12 @@ void ExportOpHistogramEq(py::module &m) Returns: cvcuda.ImageBatchVarShape: The output image batch. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("histogrameq_into", &HistogramEqVarShapeInto, "dst"_a, "src"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - - cvcuda.histogrameq_into(dst: cvcuda.ImageBatchVarShape, src: cvcuda.ImageBatchVarShape, stream: Optional[cvcuda.Stream] = None) - + m.def("histogrameq_into", NvtxTrace("cvcuda.histogrameq_into", &HistogramEqVarShapeInto), "dst"_a, "src"_a, + py::kw_only(), "stream"_a = nullptr, R"pbdoc( Executes the histogram equalization operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the HistogramEq operator - for more details and usage examples. Args: dst (cvcuda.ImageBatchVarShape): Output image batch containing the result of the operation. @@ -184,11 +157,7 @@ void ExportOpHistogramEq(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.ImageBatchVarShape: The output image batch (same as dst). )pbdoc"); } diff --git a/python/mod_cvcuda/operators/OpInpaint.cpp b/python/mod_cvcuda/operators/OpInpaint.cpp index 75a8eaa7c..d3305a00e 100644 --- a/python/mod_cvcuda/operators/OpInpaint.cpp +++ b/python/mod_cvcuda/operators/OpInpaint.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,21 +16,31 @@ */ #include "Operators.hpp" +#include "VarShapeUtils.hpp" #include #include #include +#include #include #include #include #include #include +#include + namespace cvcudapy { namespace { -class PyOpInpaint : public nvcvpy::Container +class InpaintError : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + +class PyOpInpaint : public nvcvpy::Container // NOSONAR: operator wrappers share the Python cache hierarchy. { public: class Key : public nvcvpy::IKey @@ -55,7 +65,7 @@ class PyOpInpaint : public nvcvpy::Container bool doIsCompatible(const nvcvpy::IKey &that_) const override { - const Key &that = static_cast(that_); + const auto &that = static_cast(that_); return this->payloadSize() <= that.payloadSize(); } @@ -77,7 +87,7 @@ class PyOpInpaint : public nvcvpy::Container py::object container() const override { - return *this; + return py::reinterpret_borrow(this->ptr()); } const nvcvpy::IKey &key() const override @@ -89,13 +99,14 @@ class PyOpInpaint : public nvcvpy::Container { assert(!cache.empty()); + // Find the operator with the largest workspace (can handle any smaller request) std::shared_ptr retItem = cache[0]; size_t maxPayloadSize = 0; for (const auto &item : cache) { - const Key &key = static_cast(item.get()->key()); - size_t keyPayloadSize = key.payloadSize(); + const auto &key = static_cast(item.get()->key()); + auto keyPayloadSize = key.payloadSize(); if (keyPayloadSize > maxPayloadSize) { @@ -104,9 +115,8 @@ class PyOpInpaint : public nvcvpy::Container } } - cache.clear(); - - nvcvpy::Cache::removeAllNotInUseMatching(retItem.get()->key()); + // Note: Removed removeAllNotInUseMatching() call to reduce per-call overhead. + // The cache will naturally evict unused operators when memory pressure occurs. return retItem; } @@ -123,16 +133,27 @@ Tensor InpaintInto(Tensor &output, Tensor &input, Tensor &masks, double inpaintR pstream = Stream::Current(); } - nvcv::TensorShape shape = input.shape(); - nvcv::Size2D maxShape{(int)shape[2], (int)shape[1]}; - auto inpaint = CreateOperatorEx((int)shape[0], maxShape); + auto info = nvcv::TensorLayoutInfoImage::Create(input.layout()); + if (!info) + { + throw InpaintError("Non-supported tensor layout"); + } + + auto shape = input.shape(); + auto batchSize = info->idxSample() >= 0 ? (int)shape[info->idxSample()] : 1; + auto h = (int)shape[info->idxHeight()]; + auto w = (int)shape[info->idxWidth()]; + nvcv::Size2D maxShape{w, h}; + + auto inpaint = CreateOperator(batchSize, maxShape); ResourceGuard guard(*pstream); guard.add(LockMode::LOCK_MODE_READ, {input, masks}); guard.add(LockMode::LOCK_MODE_WRITE, {output}); guard.add(LockMode::LOCK_MODE_READWRITE, {*inpaint}); - inpaint->submit(pstream->cudaHandle(), input, masks, output, inpaintRadius); + guard.run([&inpaint, &pstream, &input, &masks, &output, &inpaintRadius]() + { inpaint->submit(pstream->cudaHandle(), input, masks, output, inpaintRadius); }); return output; } @@ -152,14 +173,17 @@ ImageBatchVarShape InpaintVarShapeInto(ImageBatchVarShape &output, ImageBatchVar pstream = Stream::Current(); } nvcv::Size2D maxShape = input.maxSize(); - auto inpaint = CreateOperatorEx(input.numImages(), maxShape); + + // Use simple CreateOperator (like Flip/Gaussian) + auto inpaint = CreateOperator(input.numImages(), maxShape); ResourceGuard guard(*pstream); guard.add(LockMode::LOCK_MODE_READ, {input, masks}); guard.add(LockMode::LOCK_MODE_WRITE, {output}); guard.add(LockMode::LOCK_MODE_READWRITE, {*inpaint}); - inpaint->submit(pstream->cudaHandle(), input, masks, output, inpaintRadius); + guard.run([&inpaint, &pstream, &input, &masks, &output, &inpaintRadius]() + { inpaint->submit(pstream->cudaHandle(), input, masks, output, inpaintRadius); }); return output; } @@ -167,19 +191,13 @@ ImageBatchVarShape InpaintVarShapeInto(ImageBatchVarShape &output, ImageBatchVar ImageBatchVarShape InpaintVarShape(ImageBatchVarShape &input, ImageBatchVarShape &masks, double inpaintRadius, std::optional pstream) { - ImageBatchVarShape output = ImageBatchVarShape::Create(input.numImages()); - auto format = input.uniqueFormat(); if (!format) { - throw std::runtime_error("All images in input must have the same format."); + throw InpaintError("All images in input must have the same format."); } - for (auto img = input.begin(); img != input.end(); ++img) - { - auto newimg = Image::Create(img->size(), format); - output.pushBack(newimg); - } + ImageBatchVarShape output = CreateSameShapeImageBatch(input, format, input.numImages()); return InpaintVarShapeInto(output, input, masks, inpaintRadius, pstream); } @@ -189,19 +207,12 @@ ImageBatchVarShape InpaintVarShape(ImageBatchVarShape &input, ImageBatchVarShape void ExportOpInpaint(py::module &m) { using namespace pybind11::literals; - py::options options; - options.disable_function_signatures(); - m.def("inpaint", &Inpaint, "src"_a, "masks"_a, "inpaintRadius"_a, py::kw_only(), "stream"_a = nullptr, + m.def("inpaint", NvtxTrace("cvcuda.inpaint", &Inpaint), "src"_a, "masks"_a, "inpaintRadius"_a, py::kw_only(), + "stream"_a = nullptr, R"pbdoc( - - cvcuda.inpaint(src: cvcuda.Tensor, masks: Tensor, inpaintRadius: float, stream: Optional[cvcuda.Stream] = None) -> cvcuda.Tensor - Executes the Inpaint operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Inpaint operator - for more details and usage examples. Args: src (cvcuda.Tensor): Input tensor containing one or more images. @@ -212,21 +223,12 @@ void ExportOpInpaint(py::module &m) Returns: cvcuda.Tensor: The output tensor. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("inpaint_into", &InpaintInto, "dst"_a, "src"_a, "masks"_a, "inpaintRadius"_a, py::kw_only(), - "stream"_a = nullptr, R"pbdoc( - - cvcuda.inpaint_into(dst: cvcuda.Tensor, src: cvcuda.Tensor, masks: Tensor, inpaintRadius: float, stream: Optional[cvcuda.Stream] = None) - - Executes the Inpaint operation on the given cuda stream. + m.def("inpaint_into", NvtxTrace("cvcuda.inpaint_into", &InpaintInto), "dst"_a, "src"_a, "masks"_a, + "inpaintRadius"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( + Executes the Inpaint operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Inpaint operator - for more details and usage examples. Args: dst (cvcuda.Tensor): Output tensor to store the result of the operation. @@ -236,23 +238,14 @@ void ExportOpInpaint(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.Tensor: The output tensor (same as dst). )pbdoc"); - m.def("inpaint", &InpaintVarShape, "src"_a, "masks"_a, "inpaintRadius"_a, py::kw_only(), "stream"_a = nullptr, + m.def("inpaint", NvtxTrace("cvcuda.inpaint", &InpaintVarShape), "src"_a, "masks"_a, "inpaintRadius"_a, + py::kw_only(), "stream"_a = nullptr, R"pbdoc( + Executes the Inpaint operation on the given cuda stream. - cvcuda.inpaint(src: cvcuda.ImageBatchVarShape, masks:ImageBatchVarShape, inpaintRadius: float, stream: Optional[cvcuda.Stream] = None) -> cvcuda.ImageBatchVarShape - - Executes the Inpaint operation on the given cuda stream. - - See also: - Refer to the CV-CUDA C API reference for the Inpaint operator - for more details and usage examples. Args: src (cvcuda.ImageBatchVarShape): Input image batch containing one or more images. @@ -263,22 +256,12 @@ void ExportOpInpaint(py::module &m) Returns: cvcuda.ImageBatchVarShape: The output image batch. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("inpaint_into", &InpaintVarShapeInto, "dst"_a, "src"_a, "masks"_a, "inpaintRadius"_a, py::kw_only(), - "stream"_a = nullptr, R"pbdoc( - - - cvcuda.inpaint_into(dst: cvcuda.ImageBatchVarShape, src: cvcuda.ImageBatchVarShape, masks:ImageBatchVarShape, inpaintRadius: float, stream: Optional[cvcuda.Stream] = None) - - Executes the Inpaint operation on the given cuda stream. + m.def("inpaint_into", NvtxTrace("cvcuda.inpaint_into", &InpaintVarShapeInto), "dst"_a, "src"_a, "masks"_a, + "inpaintRadius"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( + Executes the Inpaint operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Inpaint operator - for more details and usage examples. Args: dst (cvcuda.ImageBatchVarShape): Output image batch to store the result of the operation. @@ -288,11 +271,7 @@ void ExportOpInpaint(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.ImageBatchVarShape: The output image batch (same as dst). )pbdoc"); } diff --git a/python/mod_cvcuda/operators/OpInvert.cpp b/python/mod_cvcuda/operators/OpInvert.cpp new file mode 100644 index 000000000..960ad3c4e --- /dev/null +++ b/python/mod_cvcuda/operators/OpInvert.cpp @@ -0,0 +1,136 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "UnaryElementwiseOp.hpp" + +#include +#include + +namespace cvcudapy { + +namespace { +// Thin Python-facing wrappers; the create/guard/submit body lives in UnaryElementwiseOp.hpp. +// Invert takes no extra parameters, so nothing is forwarded past the stream. +Tensor InvertInto(Tensor &output, Tensor &input, std::optional pstream) +{ + return UnaryElementwiseInto(output, input, pstream); +} + +Tensor Invert(Tensor &input, std::optional pstream) +{ + return UnaryElementwiseTensor(input, pstream); +} + +ImageBatchVarShape InvertVarShapeInto(ImageBatchVarShape &output, ImageBatchVarShape &input, + std::optional pstream) +{ + return UnaryElementwiseInto(output, input, pstream); +} + +ImageBatchVarShape InvertVarShape(ImageBatchVarShape &input, std::optional pstream) +{ + return UnaryElementwiseVarShape(input, pstream); +} + +} // namespace + +void ExportOpInvert(py::module &m) +{ + using namespace pybind11::literals; + + m.def("invert", NvtxTrace("cvcuda.invert", &Invert), "src"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( + + Executes the Invert operation on the given cuda stream. + + Computes the per-element photometric negative ``out = bound - in``, where ``bound`` is the + maximum value of the data type (255 for uint8, 65535 for uint16, 1.0 for float32). Mirrors + torchvision.transforms.v2.functional.invert / OpenCV cv::bitwise_not (unsigned). + + See also: + Refer to the CV-CUDA C API reference for the Invert operator for more details and usage + examples. + + Args: + src (cvcuda.Tensor): Input tensor containing one or more images. + stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + cvcuda.Tensor: The output tensor (same shape, dtype, and layout as src). + + Caution: + Restrictions to several arguments may apply. Check the C API references of the CV-CUDA + operator. + )pbdoc"); + + m.def("invert_into", NvtxTrace("cvcuda.invert_into", &InvertInto), "dst"_a, "src"_a, py::kw_only(), + "stream"_a = nullptr, R"pbdoc( + + Executes the Invert operation on the given cuda stream. + + See also: + Refer to the CV-CUDA C API reference for the Invert operator for more details and usage + examples. + + Args: + dst (cvcuda.Tensor): Output tensor to store the result of the operation. + src (cvcuda.Tensor): Input tensor containing one or more images. + stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + cvcuda.Tensor: The output tensor (same as dst). + + Caution: + Restrictions to several arguments may apply. Check the C API references of the CV-CUDA + operator. + )pbdoc"); + + m.def("invert", NvtxTrace("cvcuda.invert", &InvertVarShape), "src"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( + + Executes the Invert operation on the given cuda stream. + + Args: + src (cvcuda.ImageBatchVarShape): Input image batch containing one or more images. + stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + cvcuda.ImageBatchVarShape: The output image batch (same formats and sizes as src). + + Caution: + Restrictions to several arguments may apply. Check the C API references of the CV-CUDA + operator. + )pbdoc"); + + m.def("invert_into", NvtxTrace("cvcuda.invert_into", &InvertVarShapeInto), "dst"_a, "src"_a, py::kw_only(), + "stream"_a = nullptr, R"pbdoc( + + Executes the Invert operation on the given cuda stream. + + Args: + dst (cvcuda.ImageBatchVarShape): Output image batch to store the result of the operation. + src (cvcuda.ImageBatchVarShape): Input image batch containing one or more images. + stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + cvcuda.ImageBatchVarShape: The output image batch (same as dst). + + Caution: + Restrictions to several arguments may apply. Check the C API references of the CV-CUDA + operator. + )pbdoc"); +} + +} // namespace cvcudapy diff --git a/python/mod_cvcuda/operators/OpJointBilateralFilter.cpp b/python/mod_cvcuda/operators/OpJointBilateralFilter.cpp index dd7430542..9f227d993 100644 --- a/python/mod_cvcuda/operators/OpJointBilateralFilter.cpp +++ b/python/mod_cvcuda/operators/OpJointBilateralFilter.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,6 +16,7 @@ */ #include "Operators.hpp" +#include "VarShapeUtils.hpp" #include #include @@ -46,8 +47,13 @@ Tensor JointBilateralFilterInto(Tensor &output, Tensor &input, Tensor &inputColo guard.add(LockMode::LOCK_MODE_WRITE, {output}); guard.add(LockMode::LOCK_MODE_NONE, {*joint_bilateral_filter}); - joint_bilateral_filter->submit(pstream->cudaHandle(), input, inputColor, output, diameter, sigmaColor, sigmaSpace, - borderMode); + guard.run( + [&joint_bilateral_filter, &pstream, &input, &inputColor, &output, &diameter, &sigmaColor, &sigmaSpace, + &borderMode]() + { + joint_bilateral_filter->submit(pstream->cudaHandle(), input, inputColor, output, diameter, sigmaColor, + sigmaSpace, borderMode); + }); return output; } @@ -77,8 +83,13 @@ ImageBatchVarShape VarShapeJointBilateralFilterInto(ImageBatchVarShape &output, guard.add(LockMode::LOCK_MODE_WRITE, {output}); guard.add(LockMode::LOCK_MODE_NONE, {*joint_bilateral_filter}); - joint_bilateral_filter->submit(pstream->cudaHandle(), input, inputColor, output, diameter, sigmaColor, sigmaSpace, - borderMode); + guard.run( + [&joint_bilateral_filter, &pstream, &input, &inputColor, &output, &diameter, &sigmaColor, &sigmaSpace, + &borderMode]() + { + joint_bilateral_filter->submit(pstream->cudaHandle(), input, inputColor, output, diameter, sigmaColor, + sigmaSpace, borderMode); + }); return output; } @@ -87,15 +98,7 @@ ImageBatchVarShape VarShapeJointBilateralFilter(ImageBatchVarShape &input, Image Tensor &diameter, Tensor &sigmaColor, Tensor &sigmaSpace, NVCVBorderType borderMode, std::optional pstream) { - ImageBatchVarShape output = ImageBatchVarShape::Create(input.capacity()); - - for (int i = 0; i < input.numImages(); ++i) - { - nvcv::ImageFormat format = input[i].format(); - nvcv::Size2D size = input[i].size(); - auto image = Image::Create(size, format); - output.pushBack(image); - } + ImageBatchVarShape output = CreateSameShapeImageBatch(input); return VarShapeJointBilateralFilterInto(output, input, inputColor, diameter, sigmaColor, sigmaSpace, borderMode, pstream); @@ -106,20 +109,13 @@ ImageBatchVarShape VarShapeJointBilateralFilter(ImageBatchVarShape &input, Image void ExportOpJointBilateralFilter(py::module &m) { using namespace pybind11::literals; - py::options options; - options.disable_function_signatures(); - m.def("joint_bilateral_filter", &JointBilateralFilter, "src"_a, "srcColor"_a, "diameter"_a, "sigma_color"_a, - "sigma_space"_a, "border"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, py::kw_only(), "stream"_a = nullptr, + m.def("joint_bilateral_filter", NvtxTrace("cvcuda.joint_bilateral_filter", &JointBilateralFilter), "src"_a, + "srcColor"_a, "diameter"_a, "sigma_color"_a, "sigma_space"_a, + "border"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - - cvcuda.joint_bilateral_filter(src: cvcuda.Tensor, srcColor: Tensor, diameter: int, sigma_color: float, sigma_space: float, border: cvcuda.Border = cvcuda.Border.CONSTANT, stream: Optional[cvcuda.Stream] = None) -> cvcuda.Tensor - Executes the Joint Bilateral Filter operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Joint Bilateral Filter operator - for more details and usage examples. Args: src (cvcuda.Tensor): Input tensor containing one or more images. @@ -133,22 +129,13 @@ void ExportOpJointBilateralFilter(py::module &m) Returns: cvcuda.Tensor: The output tensor. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("joint_bilateral_filter_into", &JointBilateralFilterInto, "dst"_a, "src"_a, "srcColor"_a, "diameter"_a, - "sigma_color"_a, "sigma_space"_a, "border"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, py::kw_only(), - "stream"_a = nullptr, R"pbdoc( - - cvcuda.joint_bilateral_filter_into(dst: cvcuda.Tensor, src: cvcuda.Tensor, srcColor: Tensor, diameter: int, sigma_color: float, sigma_space: float, border: cvcuda.Border = cvcuda.Border.CONSTANT, stream: Optional[cvcuda.Stream] = None) - + m.def("joint_bilateral_filter_into", NvtxTrace("cvcuda.joint_bilateral_filter_into", &JointBilateralFilterInto), + "dst"_a, "src"_a, "srcColor"_a, "diameter"_a, "sigma_color"_a, "sigma_space"_a, + "border"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, py::kw_only(), "stream"_a = nullptr, R"pbdoc( Executes the Joint Bilateral Filter operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Joint Bilateral Filter operator - for more details and usage examples. Args: dst (cvcuda.Tensor): Output tensor to store the result of the operation. @@ -161,24 +148,15 @@ void ExportOpJointBilateralFilter(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.Tensor: The output tensor (same as dst). )pbdoc"); - m.def("joint_bilateral_filter", &VarShapeJointBilateralFilter, "src"_a, "srcColor"_a, "diameter"_a, "sigma_color"_a, - "sigma_space"_a, py::kw_only(), "border"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, "stream"_a = nullptr, + m.def("joint_bilateral_filter", NvtxTrace("cvcuda.joint_bilateral_filter", &VarShapeJointBilateralFilter), "src"_a, + "srcColor"_a, "diameter"_a, "sigma_color"_a, "sigma_space"_a, py::kw_only(), + "border"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, "stream"_a = nullptr, R"pbdoc( - - cvcuda.joint_bilateral_filter(src: cvcuda.ImageBatchVarShape, srcColor:ImageBatchVarShape,*, diameter: int, sigma_color: float, sigma_space: float, border: cvcuda.Border = cvcuda.Border.CONSTANT, stream: Optional[cvcuda.Stream] = None) -> cvcuda.ImageBatchVarShape - Executes the Joint Bilateral operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Joint Bilateral operator - for more details and usage examples. Args: src (cvcuda.ImageBatchVarShape): Input image batch containing one or more images. @@ -192,22 +170,14 @@ void ExportOpJointBilateralFilter(py::module &m) Returns: cvcuda.ImageBatchVarShape: The output image batch. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("joint_bilateral_filter_into", &VarShapeJointBilateralFilterInto, "dst"_a, "src"_a, "srcColor"_a, - "diameter"_a, "sigma_color"_a, "sigma_space"_a, py::kw_only(), + m.def("joint_bilateral_filter_into", + NvtxTrace("cvcuda.joint_bilateral_filter_into", &VarShapeJointBilateralFilterInto), "dst"_a, "src"_a, + "srcColor"_a, "diameter"_a, "sigma_color"_a, "sigma_space"_a, py::kw_only(), "border"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, "stream"_a = nullptr, R"pbdoc( + Executes the Joint Bilateral operation on the given cuda stream. - cvcuda.joint_bilateral_filter_into(dst: cvcuda.ImageBatchVarShape, src: cvcuda.ImageBatchVarShape, srcColor:ImageBatchVarShape,*, diameter: int, sigma_color: float, sigma_space: float, border: border (cvcuda.Border, optional): Border mode for input tensor. = , stream: Optional[cvcuda.Stream] = None) - - Executes the Joint Bilateral operation on the given cuda stream. - - See also: - Refer to the CV-CUDA C API reference for the Joint Bilateral operator - for more details and usage examples. Args: src (cvcuda.ImageBatchVarShape): Input image batch containing one or more images. @@ -219,11 +189,7 @@ void ExportOpJointBilateralFilter(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.ImageBatchVarShape: The output image batch (same as dst). )pbdoc"); } diff --git a/python/mod_cvcuda/operators/OpJpegCompressionDistortion.cpp b/python/mod_cvcuda/operators/OpJpegCompressionDistortion.cpp new file mode 100644 index 000000000..35a3edcdd --- /dev/null +++ b/python/mod_cvcuda/operators/OpJpegCompressionDistortion.cpp @@ -0,0 +1,320 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "UnaryElementwiseOp.hpp" + +#include +#include +#include +#include +#include +#include + +namespace cvcudapy { + +namespace { + +// Scalar-quality wrappers share the generic unary-elementwise plumbing; the quality tensor +// variants are hand-rolled because the parameter tensor needs its own read lock. + +Tensor JpegCompressionDistortionScalarInto(Tensor &output, Tensor &input, int quality, std::optional pstream) +{ + return UnaryElementwiseInto(output, input, pstream, quality); +} + +Tensor JpegCompressionDistortionScalar(Tensor &input, int quality, std::optional pstream) +{ + return UnaryElementwiseTensor(input, pstream, quality); +} + +ImageBatchVarShape JpegCompressionDistortionVarShapeScalarInto(ImageBatchVarShape &output, ImageBatchVarShape &input, + int quality, std::optional pstream) +{ + return UnaryElementwiseInto(output, input, pstream, quality); +} + +ImageBatchVarShape JpegCompressionDistortionVarShapeScalar(ImageBatchVarShape &input, int quality, + std::optional pstream) +{ + return UnaryElementwiseVarShape(input, pstream, quality); +} + +Tensor JpegCompressionDistortionInto(Tensor &output, Tensor &input, Tensor &quality, std::optional pstream) +{ + if (!pstream) + { + pstream = Stream::Current(); + } + + auto op = CreateOperator(); + + ResourceGuard guard(*pstream); + guard.add(LockMode::LOCK_MODE_READ, {input, quality}); + guard.add(LockMode::LOCK_MODE_WRITE, {output}); + guard.add(LockMode::LOCK_MODE_NONE, {*op}); + + guard.run([&op, &pstream, &input, &output, &quality]() + { op->submit(pstream->cudaHandle(), input, output, quality); }); + + return std::move(output); +} + +Tensor JpegCompressionDistortion(Tensor &input, Tensor &quality, std::optional pstream) +{ + Tensor output = Tensor::Create(input.shape(), input.dtype()); + + return JpegCompressionDistortionInto(output, input, quality, pstream); +} + +ImageBatchVarShape JpegCompressionDistortionVarShapeInto(ImageBatchVarShape &output, ImageBatchVarShape &input, + Tensor &quality, std::optional pstream) +{ + if (!pstream) + { + pstream = Stream::Current(); + } + + auto op = CreateOperator(); + + ResourceGuard guard(*pstream); + guard.add(LockMode::LOCK_MODE_READ, {input, quality}); + guard.add(LockMode::LOCK_MODE_WRITE, {output}); + guard.add(LockMode::LOCK_MODE_NONE, {*op}); + + guard.run([&op, &pstream, &input, &output, &quality]() + { op->submit(pstream->cudaHandle(), input, output, quality); }); + + return std::move(output); +} + +ImageBatchVarShape JpegCompressionDistortionVarShape(ImageBatchVarShape &input, Tensor &quality, + std::optional pstream) +{ + ImageBatchVarShape output = CreateSameShapeImageBatch(input); + + return JpegCompressionDistortionVarShapeInto(output, input, quality, pstream); +} + +} // namespace + +void ExportOpJpegCompressionDistortion(py::module &m) +{ + using namespace pybind11::literals; + + m.def("jpeg_compression_distortion", NvtxTrace("cvcuda.jpeg_compression_distortion", &JpegCompressionDistortion), + "src"_a, "quality"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( + + Executes the JpegCompressionDistortion operation on the given cuda stream. + + Simulates the artifacts of a JPEG compression/decompression round trip (full-range JFIF + YCbCr conversion, 4:2:0 chroma subsampling, per-8x8-block DCT and Annex-K quantization with + the libjpeg quality scaling). Entropy coding is not simulated, so results approximate — but + do not bit-match — a real JPEG codec. 1-channel images take a luma-only path. + + See also: + Refer to the CV-CUDA C API reference for the JpegCompressionDistortion operator + for more details and usage examples. + + Args: + src (nvcv.Tensor): Input tensor containing one or more uint8 images with (N)HWC or + (N)CHW layout and 1 or 3 channels. + quality (nvcv.Tensor): Per-image JPEG quality, from 1 (strongest distortion) to 100 + (weakest); rank-1 int32 tensor with one value per image. Values are clamped to + [1, 100] on the device. + stream (nvcv.cuda.Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + nvcv.Tensor: The output tensor, with the same shape, layout and data type as the input. + + Caution: + Restrictions to several arguments may apply. Check the C + API references of the CV-CUDA operator. + )pbdoc"); + + m.def("jpeg_compression_distortion", + NvtxTrace("cvcuda.jpeg_compression_distortion", &JpegCompressionDistortionScalar), "src"_a, "quality"_a, + py::kw_only(), "stream"_a = nullptr, R"pbdoc( + + Executes the JpegCompressionDistortion operation on the given cuda stream. + + Overload applying one JPEG quality to the whole batch. + + Args: + src (nvcv.Tensor): Input tensor containing one or more uint8 images with (N)HWC or + (N)CHW layout and 1 or 3 channels. + quality (int): JPEG quality applied to all images, from 1 (strongest distortion) to + 100 (weakest). Must be in [1, 100]. + stream (nvcv.cuda.Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + nvcv.Tensor: The output tensor, with the same shape, layout and data type as the input. + + Caution: + Restrictions to several arguments may apply. Check the C + API references of the CV-CUDA operator. + )pbdoc"); + + m.def("jpeg_compression_distortion_into", + NvtxTrace("cvcuda.jpeg_compression_distortion_into", &JpegCompressionDistortionInto), "dst"_a, "src"_a, + "quality"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( + + Executes the JpegCompressionDistortion operation on the given cuda stream, writing into a + caller-provided output tensor. + + Args: + dst (nvcv.Tensor): Output tensor; must match the input's shape, layout and data type. + src (nvcv.Tensor): Input tensor containing one or more uint8 images with (N)HWC or + (N)CHW layout and 1 or 3 channels. + quality (nvcv.Tensor): Per-image JPEG quality, from 1 (strongest distortion) to 100 + (weakest); rank-1 int32 tensor with one value per image. Values are clamped to + [1, 100] on the device. + stream (nvcv.cuda.Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + nvcv.Tensor: The output tensor. + + Caution: + Restrictions to several arguments may apply. Check the C + API references of the CV-CUDA operator. + )pbdoc"); + + m.def("jpeg_compression_distortion_into", + NvtxTrace("cvcuda.jpeg_compression_distortion_into", &JpegCompressionDistortionScalarInto), "dst"_a, "src"_a, + "quality"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( + + Executes the JpegCompressionDistortion operation on the given cuda stream, writing into a + caller-provided output tensor. + + Overload applying one JPEG quality to the whole batch. + + Args: + dst (nvcv.Tensor): Output tensor; must match the input's shape, layout and data type. + src (nvcv.Tensor): Input tensor containing one or more uint8 images with (N)HWC or + (N)CHW layout and 1 or 3 channels. + quality (int): JPEG quality applied to all images, from 1 (strongest distortion) to + 100 (weakest). Must be in [1, 100]. + stream (nvcv.cuda.Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + nvcv.Tensor: The output tensor. + + Caution: + Restrictions to several arguments may apply. Check the C + API references of the CV-CUDA operator. + )pbdoc"); + + m.def("jpeg_compression_distortion", + NvtxTrace("cvcuda.jpeg_compression_distortion", &JpegCompressionDistortionVarShape), "src"_a, "quality"_a, + py::kw_only(), "stream"_a = nullptr, R"pbdoc( + + Executes the JpegCompressionDistortion operation on the given cuda stream. + + Variable-shape overload: all images in the batch must share one uint8 image format with 1 + or 3 (RGB) channels. + + Args: + src (nvcv.ImageBatchVarShape): Input image batch of uint8 images with 1 or 3 (RGB) + channels. + quality (nvcv.Tensor): Per-image JPEG quality, from 1 (strongest distortion) to 100 + (weakest); rank-1 int32 tensor with one value per image. Values are clamped to + [1, 100] on the device. + stream (nvcv.cuda.Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + nvcv.ImageBatchVarShape: The output image batch, matching the input formats and sizes. + + Caution: + Restrictions to several arguments may apply. Check the C + API references of the CV-CUDA operator. + )pbdoc"); + + m.def("jpeg_compression_distortion", + NvtxTrace("cvcuda.jpeg_compression_distortion", &JpegCompressionDistortionVarShapeScalar), "src"_a, + "quality"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( + + Executes the JpegCompressionDistortion operation on the given cuda stream. + + Variable-shape overload applying one JPEG quality to the whole batch. + + Args: + src (nvcv.ImageBatchVarShape): Input image batch of uint8 images with 1 or 3 (RGB) + channels. + quality (int): JPEG quality applied to all images, from 1 (strongest distortion) to + 100 (weakest). Must be in [1, 100]. + stream (nvcv.cuda.Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + nvcv.ImageBatchVarShape: The output image batch, matching the input formats and sizes. + + Caution: + Restrictions to several arguments may apply. Check the C + API references of the CV-CUDA operator. + )pbdoc"); + + m.def("jpeg_compression_distortion_into", + NvtxTrace("cvcuda.jpeg_compression_distortion_into", &JpegCompressionDistortionVarShapeInto), "dst"_a, + "src"_a, "quality"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( + + Executes the JpegCompressionDistortion operation on the given cuda stream, writing into a + caller-provided output image batch. + + Args: + dst (nvcv.ImageBatchVarShape): Output image batch; must match the input formats and + sizes. + src (nvcv.ImageBatchVarShape): Input image batch of uint8 images with 1 or 3 (RGB) + channels. + quality (nvcv.Tensor): Per-image JPEG quality, from 1 (strongest distortion) to 100 + (weakest); rank-1 int32 tensor with one value per image. Values are clamped to + [1, 100] on the device. + stream (nvcv.cuda.Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + nvcv.ImageBatchVarShape: The output image batch. + + Caution: + Restrictions to several arguments may apply. Check the C + API references of the CV-CUDA operator. + )pbdoc"); + + m.def("jpeg_compression_distortion_into", + NvtxTrace("cvcuda.jpeg_compression_distortion_into", &JpegCompressionDistortionVarShapeScalarInto), "dst"_a, + "src"_a, "quality"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( + + Executes the JpegCompressionDistortion operation on the given cuda stream, writing into a + caller-provided output image batch. + + Variable-shape overload applying one JPEG quality to the whole batch. + + Args: + dst (nvcv.ImageBatchVarShape): Output image batch; must match the input formats and + sizes. + src (nvcv.ImageBatchVarShape): Input image batch of uint8 images with 1 or 3 (RGB) + channels. + quality (int): JPEG quality applied to all images, from 1 (strongest distortion) to + 100 (weakest). Must be in [1, 100]. + stream (nvcv.cuda.Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + nvcv.ImageBatchVarShape: The output image batch. + + Caution: + Restrictions to several arguments may apply. Check the C + API references of the CV-CUDA operator. + )pbdoc"); +} + +} // namespace cvcudapy diff --git a/python/mod_cvcuda/operators/OpLabel.cpp b/python/mod_cvcuda/operators/OpLabel.cpp index 7d47b254d..aceec1f31 100644 --- a/python/mod_cvcuda/operators/OpLabel.cpp +++ b/python/mod_cvcuda/operators/OpLabel.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -34,8 +34,9 @@ namespace { TupleTensor3 LabelInto(Tensor &output, std::optional count, std::optional stats, Tensor &input, NVCVConnectivityType connectivity, NVCVLabelType assignLabels, NVCVLabelMaskType maskType, - std::optional bgLabel, std::optional minThresh, std::optional maxThresh, - std::optional minSize, std::optional mask, std::optional pstream) + const std::optional &bgLabel, const std::optional &minThresh, + const std::optional &maxThresh, const std::optional &minSize, + const std::optional &mask, std::optional pstream) { if (!pstream) { @@ -78,19 +79,26 @@ TupleTensor3 LabelInto(Tensor &output, std::optional count, std::optiona guard.add(LockMode::LOCK_MODE_READ, {*mask}); } - op->submit(pstream->cudaHandle(), input, output, (bgLabel ? *bgLabel : nvcv::Tensor{nullptr}), - (minThresh ? *minThresh : nvcv::Tensor{nullptr}), (maxThresh ? *maxThresh : nvcv::Tensor{nullptr}), - (minSize ? *minSize : nvcv::Tensor{nullptr}), (count ? *count : nvcv::Tensor{nullptr}), - (stats ? *stats : nvcv::Tensor{nullptr}), (mask ? *mask : nvcv::Tensor{nullptr}), connectivity, - assignLabels, maskType); + guard.run( + [&bgLabel, &minThresh, &maxThresh, &minSize, &count, &stats, &mask, &op, &pstream, &input, &output, + &connectivity, &assignLabels, &maskType]() + { + const nvcv::Tensor nullTensor{nullptr}; + op->submit(pstream->cudaHandle(), input, output, AsNvcvTensor(bgLabel, nullTensor), + AsNvcvTensor(minThresh, nullTensor), AsNvcvTensor(maxThresh, nullTensor), + AsNvcvTensor(minSize, nullTensor), AsNvcvTensor(count, nullTensor), + AsNvcvTensor(stats, nullTensor), AsNvcvTensor(mask, nullTensor), connectivity, assignLabels, + maskType); + }); return TupleTensor3(std::move(output), count, stats); } TupleTensor3 Label(Tensor &input, NVCVConnectivityType connectivity, NVCVLabelType assignLabels, - NVCVLabelMaskType maskType, bool count, bool stats, int maxLabels, std::optional bgLabel, - std::optional minThresh, std::optional maxThresh, std::optional minSize, - std::optional mask, std::optional pstream) + NVCVLabelMaskType maskType, bool count, bool stats, int maxLabels, + const std::optional &bgLabel, const std::optional &minThresh, + const std::optional &maxThresh, const std::optional &minSize, + const std::optional &mask, std::optional pstream) { constexpr nvcv::DataType outType = nvcv::TYPE_S32; @@ -104,10 +112,11 @@ TupleTensor3 Label(Tensor &input, NVCVConnectivityType connectivity, NVCVLabelTy { throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input must be a valid image-based tensor"); } - int numSamples = inAccess->numSamples(); + auto numSamples = static_cast(inAccess->numSamples()); Tensor output = Tensor::Create(input.shape(), outType); - std::optional countTensor, statsTensor; + std::optional countTensor; + std::optional statsTensor; if (count) { @@ -147,15 +156,13 @@ void ExportOpLabel(py::module &m) .value("REMOVE_ISLANDS_OUTSIDE_MASK_ONLY", NVCV_REMOVE_ISLANDS_OUTSIDE_MASK_ONLY) .export_values(); - m.def("label", &Label, "src"_a, "connectivity"_a = NVCV_CONNECTIVITY_4_2D, "assign_labels"_a = NVCV_LABEL_FAST, - "mask_type"_a = NVCV_REMOVE_ISLANDS_OUTSIDE_MASK_ONLY, py::kw_only(), "count"_a = false, "stats"_a = false, - "max_labels"_a = 10000, "bg_label"_a = nullptr, "min_thresh"_a = nullptr, "max_thresh"_a = nullptr, - "min_size"_a = nullptr, "mask"_a = nullptr, "stream"_a = nullptr, R"pbdoc( - + m.def("label", NvtxTrace("cvcuda.label", &Label), "src"_a, "connectivity"_a = NVCV_CONNECTIVITY_4_2D, + "assign_labels"_a = NVCV_LABEL_FAST, "mask_type"_a = NVCV_REMOVE_ISLANDS_OUTSIDE_MASK_ONLY, py::kw_only(), + "count"_a = false, "stats"_a = false, "max_labels"_a = 10000, "bg_label"_a = nullptr, + "min_thresh"_a = nullptr, "max_thresh"_a = nullptr, "min_size"_a = nullptr, "mask"_a = nullptr, + "stream"_a = nullptr, R"pbdoc( Executes the Label operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Label operator for more details and usage examples. Args: src (cvcuda.Tensor): Input tensor to label connected-component regions. @@ -184,20 +191,15 @@ void ExportOpLabel(py::module &m) Tuple[cvcuda.Tensor, cvcuda.Tensor, cvcuda.Tensor]: A tuple with output labels, count of regions and their statistics. The count or stats tensors may be None if theirs arguments are False. - Caution: - Restrictions to several arguments may apply. Check the C API references of the CV-CUDA operator. )pbdoc"); - m.def("label_into", &LabelInto, "dst"_a, "count"_a = nullptr, "stats"_a = nullptr, "src"_a, - "connectivity"_a = NVCV_CONNECTIVITY_4_2D, "assign_labels"_a = NVCV_LABEL_FAST, + m.def("label_into", NvtxTrace("cvcuda.label_into", &LabelInto), "dst"_a, "count"_a = nullptr, "stats"_a = nullptr, + "src"_a, "connectivity"_a = NVCV_CONNECTIVITY_4_2D, "assign_labels"_a = NVCV_LABEL_FAST, "mask_type"_a = NVCV_REMOVE_ISLANDS_OUTSIDE_MASK_ONLY, py::kw_only(), "bg_label"_a = nullptr, "min_thresh"_a = nullptr, "max_thresh"_a = nullptr, "min_size"_a = nullptr, "mask"_a = nullptr, "stream"_a = nullptr, R"pbdoc( - Executes the Label operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Label operator for more details and usage examples. Args: dst (cvcuda.Tensor): Output tensor with labels. @@ -226,8 +228,6 @@ void ExportOpLabel(py::module &m) Tuple[cvcuda.Tensor, cvcuda.Tensor, cvcuda.Tensor]: A tuple with output labels, count of regions and their statistics. The count or stats tensors may be None if theirs arguments are None. - Caution: - Restrictions to several arguments may apply. Check the C API references of the CV-CUDA operator. )pbdoc"); } diff --git a/python/mod_cvcuda/operators/OpLaplacian.cpp b/python/mod_cvcuda/operators/OpLaplacian.cpp index 350902aca..edd1fea70 100644 --- a/python/mod_cvcuda/operators/OpLaplacian.cpp +++ b/python/mod_cvcuda/operators/OpLaplacian.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,6 +16,7 @@ */ #include "Operators.hpp" +#include "VarShapeUtils.hpp" #include #include @@ -47,7 +48,8 @@ Tensor LaplacianInto(Tensor &output, Tensor &input, const int &ksize, const floa guard.add(LockMode::LOCK_MODE_WRITE, {output}); guard.add(LockMode::LOCK_MODE_NONE, {*laplacian}); - laplacian->submit(pstream->cudaHandle(), input, output, ksize, scale, border); + guard.run([&laplacian, &pstream, &input, &output, &ksize, &scale, &border]() + { laplacian->submit(pstream->cudaHandle(), input, output, ksize, scale, border); }); return output; } @@ -75,7 +77,8 @@ ImageBatchVarShape LaplacianVarShapeInto(ImageBatchVarShape &output, ImageBatchV guard.add(LockMode::LOCK_MODE_WRITE, {output}); guard.add(LockMode::LOCK_MODE_NONE, {*laplacian}); - laplacian->submit(pstream->cudaHandle(), input, output, ksize, scale, border); + guard.run([&laplacian, &pstream, &input, &output, &ksize, &scale, &border]() + { laplacian->submit(pstream->cudaHandle(), input, output, ksize, scale, border); }); return output; } @@ -83,15 +86,7 @@ ImageBatchVarShape LaplacianVarShapeInto(ImageBatchVarShape &output, ImageBatchV ImageBatchVarShape LaplacianVarShape(ImageBatchVarShape &input, Tensor &ksize, Tensor &scale, NVCVBorderType border, std::optional pstream) { - ImageBatchVarShape output = ImageBatchVarShape::Create(input.numImages()); - - for (int i = 0; i < input.numImages(); ++i) - { - nvcv::ImageFormat format = input[i].format(); - nvcv::Size2D size = input[i].size(); - auto image = Image::Create(size, format); - output.pushBack(image); - } + ImageBatchVarShape output = CreateSameShapeImageBatch(input, input.numImages()); return LaplacianVarShapeInto(output, input, ksize, scale, border, pstream); } @@ -101,19 +96,11 @@ ImageBatchVarShape LaplacianVarShape(ImageBatchVarShape &input, Tensor &ksize, T void ExportOpLaplacian(py::module &m) { using namespace pybind11::literals; - py::options options; - options.disable_function_signatures(); - m.def("laplacian", &Laplacian, "src"_a, "ksize"_a, "scale"_a = 1.f, + m.def("laplacian", NvtxTrace("cvcuda.laplacian", &Laplacian), "src"_a, "ksize"_a, "scale"_a = 1.f, "border"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - - cvcuda.laplacian(src: cvcuda.Tensor, ksize: int, scale: float, stream: Optional[cvcuda.Stream] = None) -> cvcuda.Tensor - Executes the Laplacian operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Laplacian operator - for more details and usage examples. Args: src (cvcuda.Tensor): Input tensor containing one or more images. @@ -124,21 +111,13 @@ void ExportOpLaplacian(py::module &m) Returns: cvcuda.Tensor: The output tensor. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("laplacian_into", &LaplacianInto, "dst"_a, "src"_a, "ksize"_a, "scale"_a = 1.f, - "border"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - - cvcuda.laplacian_into(dst: cvcuda.Tensor, src: cvcuda.Tensor, ksize: int, scale: float, stream: Optional[cvcuda.Stream] = None) - + m.def("laplacian_into", NvtxTrace("cvcuda.laplacian_into", &LaplacianInto), "dst"_a, "src"_a, "ksize"_a, + "scale"_a = 1.f, "border"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, py::kw_only(), "stream"_a = nullptr, + R"pbdoc( Executes the Laplacian operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Laplacian operator - for more details and usage examples. Args: dst (cvcuda.Tensor): Output tensor to store the result of the operation. @@ -148,23 +127,13 @@ void ExportOpLaplacian(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.Tensor: The output tensor (same as dst). )pbdoc"); - m.def("laplacian", &LaplacianVarShape, "src"_a, "ksize"_a, "scale"_a, + m.def("laplacian", NvtxTrace("cvcuda.laplacian", &LaplacianVarShape), "src"_a, "ksize"_a, "scale"_a, "border"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - - cvcuda.laplacian(src: cvcuda.ImageBatchVarShape, ksize: cvcuda.Tensor, scale: cvcuda.Tensor, stream: Optional[cvcuda.Stream] = None) -> cvcuda.ImageBatchVarShape - Executes the Laplacian operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Laplacian operator - for more details and usage examples. Args: src (cvcuda.ImageBatchVarShape): Input image batch containing one or more images. @@ -175,21 +144,12 @@ void ExportOpLaplacian(py::module &m) Returns: cvcuda.ImageBatchVarShape: The output image batch. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("laplacian_into", &LaplacianVarShapeInto, "dst"_a, "src"_a, "ksize"_a, "scale"_a, - "border"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - - cvcuda.laplacian_into(dst: cvcuda.ImageBatchVarShape, src: cvcuda.ImageBatchVarShape, ksize: cvcuda.Tensor, scale: cvcuda.Tensor, stream: Optional[cvcuda.Stream] = None) - + m.def("laplacian_into", NvtxTrace("cvcuda.laplacian_into", &LaplacianVarShapeInto), "dst"_a, "src"_a, "ksize"_a, + "scale"_a, "border"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, py::kw_only(), "stream"_a = nullptr, R"pbdoc( Executes the Laplacian operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Laplacian operator - for more details and usage examples. Args: src (cvcuda.ImageBatchVarShape): Input image batch containing one or more images. @@ -199,11 +159,7 @@ void ExportOpLaplacian(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.ImageBatchVarShape: The output image batch (same as dst). )pbdoc"); } diff --git a/python/mod_cvcuda/operators/OpMedianBlur.cpp b/python/mod_cvcuda/operators/OpMedianBlur.cpp index 798009593..4c07c5f9e 100644 --- a/python/mod_cvcuda/operators/OpMedianBlur.cpp +++ b/python/mod_cvcuda/operators/OpMedianBlur.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,6 +16,7 @@ */ #include "Operators.hpp" +#include "VarShapeUtils.hpp" #include #include @@ -48,7 +49,8 @@ Tensor MedianBlurInto(Tensor &output, Tensor &input, const std::tuple nvcv::Size2D ksizeArg{std::get<0>(ksize), std::get<1>(ksize)}; - median_blur->submit(pstream->cudaHandle(), input, output, ksizeArg); + guard.run([&median_blur, &pstream, &input, &output, &ksizeArg]() + { median_blur->submit(pstream->cudaHandle(), input, output, ksizeArg); }); return output; } @@ -75,22 +77,15 @@ ImageBatchVarShape VarShapeMedianBlurInto(ImageBatchVarShape &output, ImageBatch guard.add(LockMode::LOCK_MODE_WRITE, {output}); guard.add(LockMode::LOCK_MODE_READWRITE, {*median_blur}); - median_blur->submit(pstream->cudaHandle(), input, output, ksize); + guard.run([&median_blur, &pstream, &input, &output, &ksize]() + { median_blur->submit(pstream->cudaHandle(), input, output, ksize); }); return output; } ImageBatchVarShape VarShapeMedianBlur(ImageBatchVarShape &input, Tensor &ksize, std::optional pstream) { - ImageBatchVarShape output = ImageBatchVarShape::Create(input.capacity()); - - for (int i = 0; i < input.numImages(); ++i) - { - nvcv::ImageFormat format = input[i].format(); - nvcv::Size2D size = input[i].size(); - auto image = Image::Create(size, format); - output.pushBack(image); - } + ImageBatchVarShape output = CreateSameShapeImageBatch(input); return VarShapeMedianBlurInto(output, input, ksize, pstream); } @@ -100,18 +95,11 @@ ImageBatchVarShape VarShapeMedianBlur(ImageBatchVarShape &input, Tensor &ksize, void ExportOpMedianBlur(py::module &m) { using namespace pybind11::literals; - py::options options; - options.disable_function_signatures(); - - m.def("median_blur", &MedianBlur, "src"_a, "ksize"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - - cvcuda.median_blur(src: cvcuda.Tensor, ksize: Tuple[int, int], stream: Optional[cvcuda.Stream] = None) -> cvcuda.Tensor + m.def("median_blur", NvtxTrace("cvcuda.median_blur", &MedianBlur), "src"_a, "ksize"_a, py::kw_only(), + "stream"_a = nullptr, R"pbdoc( Executes the Median Blur operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Median Blur operator - for more details and usage examples. Args: src (cvcuda.Tensor): Input tensor containing one or more images. @@ -121,21 +109,13 @@ void ExportOpMedianBlur(py::module &m) Returns: cvcuda.Tensor: The output tensor. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("median_blur_into", &MedianBlurInto, "dst"_a, "src"_a, "ksize"_a, py::kw_only(), "stream"_a = nullptr, + m.def("median_blur_into", NvtxTrace("cvcuda.median_blur_into", &MedianBlurInto), "dst"_a, "src"_a, "ksize"_a, + py::kw_only(), "stream"_a = nullptr, R"pbdoc( - - cvcuda.median_blur_into(dst: cvcuda.Tensor, src: cvcuda.Tensor, ksize: Tuple[int, int], stream: Optional[cvcuda.Stream] = None) - Executes the Median Blur operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Median Blur operator - for more details and usage examples. Args: dst (cvcuda.Tensor): Output tensor to store the result of the operation. @@ -144,22 +124,13 @@ void ExportOpMedianBlur(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.Tensor: The output tensor (same as dst). )pbdoc"); - m.def("median_blur", &VarShapeMedianBlur, "src"_a, "ksize"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - - cvcuda.median_blur(src: cvcuda.ImageBatchVarShape, ksize: Tuple[int, int], stream: Optional[cvcuda.Stream] = None) -> cvcuda.ImageBatchVarShape - + m.def("median_blur", NvtxTrace("cvcuda.median_blur", &VarShapeMedianBlur), "src"_a, "ksize"_a, py::kw_only(), + "stream"_a = nullptr, R"pbdoc( Executes the Median Blur operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Median Blur operator - for more details and usage examples. Args: src (cvcuda.ImageBatchVarShape): Input image batch containing one or more images. @@ -169,21 +140,13 @@ void ExportOpMedianBlur(py::module &m) Returns: cvcuda.ImageBatchVarShape: The output image batch. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("median_blur_into", &VarShapeMedianBlurInto, "dst"_a, "src"_a, "ksize"_a, py::kw_only(), "stream"_a = nullptr, + m.def("median_blur_into", NvtxTrace("cvcuda.median_blur_into", &VarShapeMedianBlurInto), "dst"_a, "src"_a, + "ksize"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( + Executes the Median Blur operation on the given cuda stream. - cvcuda.median_blur_into(dst: cvcuda.ImageBatchVarShape, src: cvcuda.ImageBatchVarShape, ksize: Tuple[int, int], stream: Optional[cvcuda.Stream] = None) - - Executes the Median Blur operation on the given cuda stream. - - See also: - Refer to the CV-CUDA C API reference for the Median Blur operator - for more details and usage examples. Args: src (cvcuda.ImageBatchVarShape): Input image batch containing one or more images. @@ -192,11 +155,7 @@ void ExportOpMedianBlur(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.ImageBatchVarShape: The output image batch (same as dst). )pbdoc"); } diff --git a/python/mod_cvcuda/operators/OpMinAreaRect.cpp b/python/mod_cvcuda/operators/OpMinAreaRect.cpp index b6c921f6d..182c506da 100644 --- a/python/mod_cvcuda/operators/OpMinAreaRect.cpp +++ b/python/mod_cvcuda/operators/OpMinAreaRect.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,9 +23,18 @@ #include #include +#include + namespace cvcudapy { namespace { + +class MinAreaRectError : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + Tensor MinAreaRectInto(Tensor &output, Tensor &input, Tensor &numPointsInContour, const int totalContours, std::optional pstream) { @@ -41,7 +50,8 @@ Tensor MinAreaRectInto(Tensor &output, Tensor &input, Tensor &numPointsInContour guard.add(LockMode::LOCK_MODE_WRITE, {output}); guard.add(LockMode::LOCK_MODE_READWRITE, {*minAreaRect}); - minAreaRect->submit(pstream->cudaHandle(), input, output, numPointsInContour, totalContours); + guard.run([&minAreaRect, &pstream, &input, &output, &numPointsInContour, &totalContours]() + { minAreaRect->submit(pstream->cudaHandle(), input, output, numPointsInContour, totalContours); }); return std::move(output); } @@ -52,11 +62,11 @@ Tensor MinAreaRect(Tensor &input, Tensor &numPointsInContour, const int totalCon const auto &numPointsInContourShape = numPointsInContour.shape(); if ((srcShape.rank() - 1) != numPointsInContourShape.rank()) { - throw std::runtime_error("Input src rank must 1 greater than numPointsInContourShape tensors rank"); + throw MinAreaRectError("Input src rank must 1 greater than numPointsInContourShape tensors rank"); } if (srcShape.shape()[0] != numPointsInContourShape.shape()[1]) { - throw std::runtime_error("Input src and numPointsInContourShape must have same batch size"); + throw MinAreaRectError("Input src and numPointsInContourShape must have same batch size"); } Shape dstShape(2); @@ -74,14 +84,10 @@ void ExportOpMinAreaRect(py::module &m) { using namespace pybind11::literals; - m.def("minarearect", &MinAreaRect, "src"_a, "numPointsInContour"_a, "totalContours"_a, py::kw_only(), - "stream"_a = nullptr, R"pbdoc( - + m.def("minarearect", NvtxTrace("cvcuda.minarearect", &MinAreaRect), "src"_a, "numPointsInContour"_a, + "totalContours"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( Executes the Min Area Rect operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Min Area Rect operator - for more details and usage examples. Args: src (cvcuda.Tensor): Input tensor containing one or more contours.src[i,j,k] is the set of contours @@ -95,19 +101,12 @@ void ExportOpMinAreaRect(py::module &m) cvcuda.Tensor: The output tensor of rotated bounding boxes.The output will give 4 points' cooridinate(x,y) of each contour's minimum rotated bounding boxes - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("minarearect_into", &MinAreaRectInto, "dst"_a, "src"_a, "numPointsInContour"_a, "totalContours"_a, - py::kw_only(), "stream"_a = nullptr, R"pbdoc( - + m.def("minarearect_into", NvtxTrace("cvcuda.minarearect_into", &MinAreaRectInto), "dst"_a, "src"_a, + "numPointsInContour"_a, "totalContours"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( Executes the Min Area Rect operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Min Area Rect operator - for more details and usage examples. Args: dst (cvcuda.Tensor): Output tensor will give 4 points' cooridinate(x,y) @@ -121,11 +120,7 @@ void ExportOpMinAreaRect(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.Tensor: The output tensor (same as dst). )pbdoc"); } diff --git a/python/mod_cvcuda/operators/OpMinMaxLoc.cpp b/python/mod_cvcuda/operators/OpMinMaxLoc.cpp index 45c989f71..525811257 100644 --- a/python/mod_cvcuda/operators/OpMinMaxLoc.cpp +++ b/python/mod_cvcuda/operators/OpMinMaxLoc.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,36 +25,45 @@ #include #include +#include +#include #include +#include namespace cvcudapy { namespace { +class MinMaxLocError : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + using TupleTensor3 = std::tuple; using TupleTensor6 = std::tuple; // Auxiliary function to get the value data type (for minVal or maxVal) for the given input data type nvcv::DataType GetValDataType(nvcv::DataType inDataType) { - switch (inDataType) + switch (static_cast(inDataType)) { - case nvcv::TYPE_S8: - case nvcv::TYPE_S16: - case nvcv::TYPE_S32: + case NVCV_DATA_TYPE_S8: + case NVCV_DATA_TYPE_S16: + case NVCV_DATA_TYPE_S32: return nvcv::TYPE_S32; - case nvcv::TYPE_U8: - case nvcv::TYPE_U16: - case nvcv::TYPE_U32: + case NVCV_DATA_TYPE_U8: + case NVCV_DATA_TYPE_U16: + case NVCV_DATA_TYPE_U32: return nvcv::TYPE_U32; - case nvcv::TYPE_F32: - case nvcv::TYPE_F64: + case NVCV_DATA_TYPE_F32: + case NVCV_DATA_TYPE_F64: return inDataType; default: - throw std::runtime_error("Input data type not supported"); + throw MinMaxLocError("Input data type not supported"); } return nvcv::DataType(); } @@ -81,7 +90,12 @@ TupleTensor3 MinLocInto(Tensor &minVal, Tensor &minLoc, Tensor &numMin, InputCon guard.add(LockMode::LOCK_MODE_WRITE, {minVal, minLoc, numMin}); guard.add(LockMode::LOCK_MODE_NONE, {*op}); - op->submit(pstream->cudaHandle(), input, minVal, minLoc, numMin, nullptr, nullptr, nullptr); + guard.run( + [&op, &pstream, &input, &minVal, &minLoc, &numMin]() + { + op->submit(pstream->cudaHandle(), input, minVal, minLoc, numMin, nvcv::Tensor{nullptr}, + nvcv::Tensor{nullptr}, nvcv::Tensor{nullptr}); + }); return TupleTensor3(std::move(minVal), std::move(minLoc), std::move(numMin)); } @@ -114,7 +128,12 @@ TupleTensor3 MaxLocInto(Tensor &maxVal, Tensor &maxLoc, Tensor &numMax, InputCon guard.add(LockMode::LOCK_MODE_WRITE, {maxVal, maxLoc, numMax}); guard.add(LockMode::LOCK_MODE_NONE, {*op}); - op->submit(pstream->cudaHandle(), input, nullptr, nullptr, nullptr, maxVal, maxLoc, numMax); + guard.run( + [&op, &pstream, &input, &maxVal, &maxLoc, &numMax]() + { + op->submit(pstream->cudaHandle(), input, nvcv::Tensor{nullptr}, nvcv::Tensor{nullptr}, + nvcv::Tensor{nullptr}, maxVal, maxLoc, numMax); + }); return TupleTensor3(std::move(maxVal), std::move(maxLoc), std::move(numMax)); } @@ -147,7 +166,8 @@ TupleTensor6 MinMaxLocInto(Tensor &minVal, Tensor &minLoc, Tensor &numMin, Tenso guard.add(LockMode::LOCK_MODE_WRITE, {minVal, minLoc, numMin, maxVal, maxLoc, numMax}); guard.add(LockMode::LOCK_MODE_NONE, {*op}); - op->submit(pstream->cudaHandle(), input, minVal, minLoc, numMin, maxVal, maxLoc, numMax); + guard.run([&op, &pstream, &input, &minVal, &minLoc, &numMin, &maxVal, &maxLoc, &numMax]() + { op->submit(pstream->cudaHandle(), input, minVal, minLoc, numMin, maxVal, maxLoc, numMax); }); return TupleTensor6(std::move(minVal), std::move(minLoc), std::move(numMin), std::move(maxVal), std::move(maxLoc), std::move(numMax)); @@ -188,7 +208,7 @@ TupleTensor3 MinLocTensor(Tensor &input, int maxLocs, std::optional pstr maxLocs = maxLocs == 0 ? GetDefaultMaxLocs(inAccess->numCols(), inAccess->numRows()) : maxLocs; - return MinLoc(input, input.dtype(), inAccess->numSamples(), maxLocs, pstream); + return MinLoc(input, input.dtype(), static_cast(inAccess->numSamples()), maxLocs, pstream); } TupleTensor3 MinLocVarShape(ImageBatchVarShape &input, int maxLocs, std::optional pstream) @@ -221,7 +241,7 @@ TupleTensor3 MaxLocTensor(Tensor &input, int maxLocs, std::optional pstr maxLocs = maxLocs == 0 ? GetDefaultMaxLocs(inAccess->numCols(), inAccess->numRows()) : maxLocs; - return MaxLoc(input, input.dtype(), inAccess->numSamples(), maxLocs, pstream); + return MaxLoc(input, input.dtype(), static_cast(inAccess->numSamples()), maxLocs, pstream); } TupleTensor3 MaxLocVarShape(ImageBatchVarShape &input, int maxLocs, std::optional pstream) @@ -257,7 +277,7 @@ TupleTensor6 MinMaxLocTensor(Tensor &input, int maxLocs, std::optional p maxLocs = maxLocs == 0 ? GetDefaultMaxLocs(inAccess->numCols(), inAccess->numRows()) : maxLocs; - return MinMaxLoc(input, input.dtype(), inAccess->numSamples(), maxLocs, pstream); + return MinMaxLoc(input, input.dtype(), static_cast(inAccess->numSamples()), maxLocs, pstream); } TupleTensor6 MinMaxLocVarShape(ImageBatchVarShape &input, int maxLocs, std::optional pstream) @@ -267,89 +287,79 @@ TupleTensor6 MinMaxLocVarShape(ImageBatchVarShape &input, int maxLocs, std::opti return MinMaxLoc(input, input.uniqueFormat().planeDataType(0), input.numImages(), maxLocs, pstream); } -// Function to get the docstring for an entry function +// Function to get the docstring for an entry function. +// +// Assembles a Google-style docstring with 4-space continuation indent so that +// Napoleon + docutils render cleanly (no block-quote or unexpected-indent +// warnings). Parameters follow the original convention: +// strInto — "" for allocating variants, "into" for *_into variants +// strTensor — "tensor" or "batch" +// strMinMax — "minimum", "maximum", or "minimum/maximum" -inline std::string GetDocString(const std::string &strInto, const std::string &strTensor, const std::string &strMinMax) +inline std::string GetDocString(std::string_view strInto, std::string_view strTensor, std::string_view strMinMax) { - std::string strSrc; - if (strTensor.find("tensor") != std::string::npos) + const bool isInto = strInto.find("into") != std::string_view::npos; + const bool isBatch = strTensor.find("batch") != std::string_view::npos; + const bool hasMin = strMinMax.find("min") != std::string_view::npos; + const bool hasMax = strMinMax.find("max") != std::string_view::npos; + + const std::string srcType = isBatch ? "cvcuda.ImageBatchVarShape" : "cvcuda.Tensor"; + const std::string srcDesc = isBatch ? "Input image batch to get minimum/maximum values/locations." + : "Input tensor to get minimum/maximum values/locations."; + const std::string kindDesc = isBatch ? "image batch" : "tensor"; + + std::ostringstream out; + out << "\n" + << " Finds " << strMinMax << " values and locations on the input " << kindDesc << ".\n" + << "\n" + << "\n" + << " Args:\n"; + + if (isInto) { - strSrc = std::string(R"pbdoc( - src (cvcuda.Tensor): Input tensor to get minimum/maximum values/locations.)pbdoc"); - } - else if (strTensor.find("batch") != std::string::npos) - { - strSrc = std::string(R"pbdoc( - src (cvcuda.ImageBatchVarShape): Input image batch to get minimum/maximum values/locations.)pbdoc"); - } - - std::string strArgs; - if (strInto.find("into") != std::string::npos) - { - if (strMinMax.find("min") != std::string::npos) + if (hasMin) { - strArgs += std::string(R"pbdoc( - min_val (cvcuda.Tensor): Output tensor with minimum value. - min_loc (cvcuda.Tensor): Output tensor with minimum locations. - num_min (cvcuda.Tensor): Output tensor with number of minimum locations found.)pbdoc"); + out << " min_val (cvcuda.Tensor): Output tensor with minimum value.\n" + << " min_loc (cvcuda.Tensor): Output tensor with minimum locations.\n" + << " num_min (cvcuda.Tensor): Output tensor with number of minimum locations found.\n"; } - if (strMinMax.find("max") != std::string::npos) + if (hasMax) { - strArgs += std::string(R"pbdoc( - max_val (cvcuda.Tensor): Output tensor with maximum value. - max_loc (cvcuda.Tensor): Output tensor with maximum locations. - num_max (cvcuda.Tensor): Output tensor with number of maximum locations found.)pbdoc"); + out << " max_val (cvcuda.Tensor): Output tensor with maximum value.\n" + << " max_loc (cvcuda.Tensor): Output tensor with maximum locations.\n" + << " num_max (cvcuda.Tensor): Output tensor with number of maximum locations found.\n"; } - strArgs += strSrc; + out << " src (" << srcType << "): " << srcDesc << "\n"; } else { - strArgs += strSrc; - strArgs += std::string(R"pbdoc( - max_locations (Number, optional): Number of maximum locations to find, default is 1% of total - pixels at a minimum of 1.)pbdoc"); + out << " src (" << srcType << "): " << srcDesc << "\n" + << " max_locations (Number, optional): Number of maximum locations to find,\n" + << " default is 1% of total pixels at a minimum of 1.\n"; } + out << " stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation.\n" + << "\n" + << " Returns:\n"; - std::string strReturns; - if (strMinMax.find("minimum/maximum") != std::string::npos) + if (strMinMax == "minimum/maximum") { - strReturns = std::string(R"pbdoc( - Tuple[cvcuda.Tensor, cvcuda.Tensor, cvcuda.Tensor, cvcuda.Tensor, cvcuda.Tensor, cvcuda.Tensor]: A tuple with minimum value, locations and number - of minima, and also maximum value, locations and number of maxima.)pbdoc"); + out << " Tuple[cvcuda.Tensor, cvcuda.Tensor, cvcuda.Tensor," + << " cvcuda.Tensor, cvcuda.Tensor, cvcuda.Tensor]: A tuple with minimum\n" + << " value, locations and number of minima, and also maximum value,\n" + << " locations and number of maxima.\n"; } - else if (strMinMax.find("min") != std::string::npos) + else if (hasMin) { - strReturns = std::string(R"pbdoc( - Tuple[cvcuda.Tensor, cvcuda.Tensor, cvcuda.Tensor]: A tuple with minimum value, locations and number - of minima.)pbdoc"); + out << " Tuple[cvcuda.Tensor, cvcuda.Tensor, cvcuda.Tensor]: A tuple with\n" + << " minimum value, locations and number of minima.\n"; } - else if (strMinMax.find("max") != std::string::npos) + else if (hasMax) { - strReturns = std::string(R"pbdoc( - Tuple[cvcuda.Tensor, cvcuda.Tensor, cvcuda.Tensor]: A tuple with maximum value, locations and number - of maxima.)pbdoc"); + out << " Tuple[cvcuda.Tensor, cvcuda.Tensor, cvcuda.Tensor]: A tuple with\n" + << " maximum value, locations and number of maxima.\n"; } - - return std::string(R"pbdoc( - - Finds )pbdoc") - + strMinMax + std::string(R"pbdoc( on the input )pbdoc") + strTensor + std::string(R"pbdoc(. - - See also: - Refer to the CV-CUDA C API reference for the MinMaxLoc operator - for more details and usage examples. - - Args:)pbdoc") - + strArgs + std::string(R"pbdoc( - stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. - - Returns:)pbdoc") - + strReturns + std::string(R"pbdoc( - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. - )pbdoc"); + out << " "; + return out.str(); } } // namespace @@ -358,42 +368,42 @@ void ExportOpMinMaxLoc(py::module &m) { using namespace pybind11::literals; - m.def("min_loc", &MinLocTensor, "src"_a, "max_locations"_a = 0, py::kw_only(), "stream"_a = nullptr, - GetDocString("", "tensor", "minimum").c_str()); + m.def("min_loc", NvtxTrace("cvcuda.min_loc", &MinLocTensor), "src"_a, "max_locations"_a = 0, py::kw_only(), + "stream"_a = nullptr, GetDocString("", "tensor", "minimum").c_str()); - m.def("min_loc", &MinLocVarShape, "src"_a, "max_locations"_a = 0, py::kw_only(), "stream"_a = nullptr, - GetDocString("", "batch", "minimum").c_str()); + m.def("min_loc", NvtxTrace("cvcuda.min_loc", &MinLocVarShape), "src"_a, "max_locations"_a = 0, py::kw_only(), + "stream"_a = nullptr, GetDocString("", "batch", "minimum").c_str()); - m.def("max_loc", &MaxLocTensor, "src"_a, "max_locations"_a = 0, py::kw_only(), "stream"_a = nullptr, - GetDocString("", "tensor", "maximum").c_str()); + m.def("max_loc", NvtxTrace("cvcuda.max_loc", &MaxLocTensor), "src"_a, "max_locations"_a = 0, py::kw_only(), + "stream"_a = nullptr, GetDocString("", "tensor", "maximum").c_str()); - m.def("max_loc", &MaxLocVarShape, "src"_a, "max_locations"_a = 0, py::kw_only(), "stream"_a = nullptr, - GetDocString("", "batch", "maximum").c_str()); + m.def("max_loc", NvtxTrace("cvcuda.max_loc", &MaxLocVarShape), "src"_a, "max_locations"_a = 0, py::kw_only(), + "stream"_a = nullptr, GetDocString("", "batch", "maximum").c_str()); - m.def("min_max_loc", &MinMaxLocTensor, "src"_a, "max_locations"_a = 0, py::kw_only(), "stream"_a = nullptr, - GetDocString("", "tensor", "minimum/maximum").c_str()); + m.def("min_max_loc", NvtxTrace("cvcuda.min_max_loc", &MinMaxLocTensor), "src"_a, "max_locations"_a = 0, + py::kw_only(), "stream"_a = nullptr, GetDocString("", "tensor", "minimum/maximum").c_str()); - m.def("min_max_loc", &MinMaxLocVarShape, "src"_a, "max_locations"_a = 0, py::kw_only(), "stream"_a = nullptr, - GetDocString("", "batch", "minimum/maximum").c_str()); + m.def("min_max_loc", NvtxTrace("cvcuda.min_max_loc", &MinMaxLocVarShape), "src"_a, "max_locations"_a = 0, + py::kw_only(), "stream"_a = nullptr, GetDocString("", "batch", "minimum/maximum").c_str()); - m.def("min_loc_into", &MinLocTensorInto, "min_val"_a, "min_loc"_a, "num_min"_a, "src"_a, py::kw_only(), - "stream"_a = nullptr, GetDocString("into", "tensor", "minimum").c_str()); + m.def("min_loc_into", NvtxTrace("cvcuda.min_loc_into", &MinLocTensorInto), "min_val"_a, "min_loc"_a, "num_min"_a, + "src"_a, py::kw_only(), "stream"_a = nullptr, GetDocString("into", "tensor", "minimum").c_str()); - m.def("min_loc_into", &MinLocVarShapeInto, "min_val"_a, "min_loc"_a, "num_min"_a, "src"_a, py::kw_only(), - "stream"_a = nullptr, GetDocString("into", "batch", "minimum").c_str()); + m.def("min_loc_into", NvtxTrace("cvcuda.min_loc_into", &MinLocVarShapeInto), "min_val"_a, "min_loc"_a, "num_min"_a, + "src"_a, py::kw_only(), "stream"_a = nullptr, GetDocString("into", "batch", "minimum").c_str()); - m.def("max_loc_into", &MaxLocTensorInto, "max_val"_a, "max_loc"_a, "num_max"_a, "src"_a, py::kw_only(), - "stream"_a = nullptr, GetDocString("into", "tensor", "maximum").c_str()); + m.def("max_loc_into", NvtxTrace("cvcuda.max_loc_into", &MaxLocTensorInto), "max_val"_a, "max_loc"_a, "num_max"_a, + "src"_a, py::kw_only(), "stream"_a = nullptr, GetDocString("into", "tensor", "maximum").c_str()); - m.def("max_loc_into", &MaxLocVarShapeInto, "max_val"_a, "max_loc"_a, "num_max"_a, "src"_a, py::kw_only(), - "stream"_a = nullptr, GetDocString("into", "batch", "maximum").c_str()); + m.def("max_loc_into", NvtxTrace("cvcuda.max_loc_into", &MaxLocVarShapeInto), "max_val"_a, "max_loc"_a, "num_max"_a, + "src"_a, py::kw_only(), "stream"_a = nullptr, GetDocString("into", "batch", "maximum").c_str()); - m.def("min_max_loc_into", &MinMaxLocTensorInto, "min_val"_a, "min_loc"_a, "num_min"_a, "max_val"_a, "max_loc"_a, - "num_max"_a, "src"_a, py::kw_only(), "stream"_a = nullptr, + m.def("min_max_loc_into", NvtxTrace("cvcuda.min_max_loc_into", &MinMaxLocTensorInto), "min_val"_a, "min_loc"_a, + "num_min"_a, "max_val"_a, "max_loc"_a, "num_max"_a, "src"_a, py::kw_only(), "stream"_a = nullptr, GetDocString("into", "tensor", "minimum/maximum").c_str()); - m.def("min_max_loc_into", &MinMaxLocVarShapeInto, "min_val"_a, "min_loc"_a, "num_min"_a, "max_val"_a, "max_loc"_a, - "num_max"_a, "src"_a, py::kw_only(), "stream"_a = nullptr, + m.def("min_max_loc_into", NvtxTrace("cvcuda.min_max_loc_into", &MinMaxLocVarShapeInto), "min_val"_a, "min_loc"_a, + "num_min"_a, "max_val"_a, "max_loc"_a, "num_max"_a, "src"_a, py::kw_only(), "stream"_a = nullptr, GetDocString("into", "batch", "minimum/maximum").c_str()); } diff --git a/python/mod_cvcuda/operators/OpMorphology.cpp b/python/mod_cvcuda/operators/OpMorphology.cpp index 156c966e5..b8fceb53f 100644 --- a/python/mod_cvcuda/operators/OpMorphology.cpp +++ b/python/mod_cvcuda/operators/OpMorphology.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,6 +16,7 @@ */ #include "Operators.hpp" +#include "VarShapeUtils.hpp" #include #include @@ -56,20 +57,29 @@ Tensor MorphologyInto(Tensor &output, Tensor &input, NVCVMorphologyType morph_ty if (workspace) { guard.add(LockMode::LOCK_MODE_READ, {*workspace}); - morphology->submit(pstream->cudaHandle(), input, output, *workspace, morph_type, maskSizeArg, anchorArg, - iteration, border); + guard.run( + [&morphology, &pstream, &input, &output, &workspace, &morph_type, &maskSizeArg, &anchorArg, &iteration, + &border]() + { + morphology->submit(pstream->cudaHandle(), input, output, nvcv::OptionalTensorConstRef{*workspace}, + morph_type, maskSizeArg, anchorArg, iteration, border); + }); } else { - morphology->submit(pstream->cudaHandle(), input, output, nvcv::NullOpt, morph_type, maskSizeArg, anchorArg, - iteration, border); + guard.run( + [&morphology, &pstream, &input, &output, &morph_type, &maskSizeArg, &anchorArg, &iteration, &border]() + { + morphology->submit(pstream->cudaHandle(), input, output, nvcv::OptionalTensorConstRef{nvcv::NullOpt}, + morph_type, maskSizeArg, anchorArg, iteration, border); + }); } return output; } Tensor Morphology(Tensor &input, NVCVMorphologyType morph_type, const std::tuple &maskSize, - const std::tuple &anchor, std::optional workspace, int32_t iteration, + const std::tuple &anchor, const std::optional &workspace, int32_t iteration, NVCVBorderType border, std::optional pstream) { Tensor output = Tensor::Create(input.shape(), input.dtype()); @@ -90,39 +100,42 @@ ImageBatchVarShape MorphologyVarShapeInto(ImageBatchVarShape &output, ImageBatch auto morphology = CreateOperator(); ResourceGuard guard(*pstream); - guard.add(LockMode::LOCK_MODE_READ, {input}); - guard.add(LockMode::LOCK_MODE_READWRITE, {output, masks, anchors}); - guard.add(LockMode::LOCK_MODE_READWRITE, {*morphology}); + guard.add(LockMode::LOCK_MODE_READ, {input, masks, anchors}); + guard.add(LockMode::LOCK_MODE_WRITE, {output}); + guard.add(LockMode::LOCK_MODE_NONE, {*morphology}); if (workspace) { guard.add(LockMode::LOCK_MODE_READ, {*workspace}); - morphology->submit(pstream->cudaHandle(), input, output, *workspace, morph_type, masks, anchors, iteration, - borderMode); + guard.run( + [&morphology, &pstream, &input, &output, &workspace, &morph_type, &masks, &anchors, &iteration, + &borderMode]() + { + morphology->submit(pstream->cudaHandle(), input, output, + nvcv::OptionalImageBatchVarShapeConstRef{*workspace}, morph_type, masks, anchors, + iteration, borderMode); + }); } else { - morphology->submit(pstream->cudaHandle(), input, output, nvcv::NullOpt, morph_type, masks, anchors, iteration, - borderMode); + guard.run( + [&morphology, &pstream, &input, &output, &morph_type, &masks, &anchors, &iteration, &borderMode]() + { + morphology->submit(pstream->cudaHandle(), input, output, + nvcv::OptionalImageBatchVarShapeConstRef{nvcv::NullOpt}, morph_type, masks, anchors, + iteration, borderMode); + }); } return output; } ImageBatchVarShape MorphologyVarShape(ImageBatchVarShape &input, NVCVMorphologyType morph_type, Tensor &masks, - Tensor &anchors, std::optional workspace, + Tensor &anchors, const std::optional &workspace, const int32_t iteration, const NVCVBorderType borderMode, std::optional pstream) { - ImageBatchVarShape output = ImageBatchVarShape::Create(input.capacity()); - - for (int i = 0; i < input.numImages(); ++i) - { - nvcv::ImageFormat format = input[i].format(); - nvcv::Size2D size = input[i].size(); - auto image = Image::Create(size, format); - output.pushBack(image); - } + ImageBatchVarShape output = CreateSameShapeImageBatch(input); return MorphologyVarShapeInto(output, input, morph_type, masks, anchors, workspace, iteration, borderMode, pstream); } @@ -133,20 +146,11 @@ void ExportOpMorphology(py::module &m) { using namespace pybind11::literals; - py::options options; - options.disable_function_signatures(); - - m.def("morphology", &Morphology, "src"_a, "morphologyType"_a, "maskSize"_a, "anchor"_a, py::kw_only(), - "workspace"_a = nullptr, "iteration"_a = 1, "border"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, - "stream"_a = nullptr, R"pbdoc( - - cvcuda.morphology(src: cvcuda.Tensor, morphologyType: cvcuda.MorphologyType, maskSize: Tuple[int, int], anchor: Tuple[int, int], workspace: cvcuda.Tensor, iteration: int, border: cvcuda.Border = cvcuda.Border.CONSTANT, stream: Optional[cvcuda.Stream] = None) -> cvcuda.Tensor - + m.def("morphology", NvtxTrace("cvcuda.morphology", &Morphology), "src"_a, "morphologyType"_a, "maskSize"_a, + "anchor"_a, py::kw_only(), "workspace"_a = nullptr, "iteration"_a = 1, + "border"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, "stream"_a = nullptr, R"pbdoc( Executes the Morphology operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Morphology operator - for more details and usage examples. Args: src (cvcuda.Tensor): Input tensor containing one or more images. @@ -162,23 +166,14 @@ void ExportOpMorphology(py::module &m) Returns: cvcuda.Tensor: The output tensor. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("morphology_into", &MorphologyInto, "dst"_a, "src"_a, "morphologyType"_a, "maskSize"_a, "anchor"_a, - py::kw_only(), "workspace"_a = nullptr, "iteration"_a = 1, "border"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, - "stream"_a = nullptr, + m.def("morphology_into", NvtxTrace("cvcuda.morphology_into", &MorphologyInto), "dst"_a, "src"_a, "morphologyType"_a, + "maskSize"_a, "anchor"_a, py::kw_only(), "workspace"_a = nullptr, "iteration"_a = 1, + "border"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, "stream"_a = nullptr, R"pbdoc( - - cvcuda.morphology_into(dst: cvcuda.Tensor, src: cvcuda.Tensor, morphologyType: cvcuda.MorphologyType, maskSize: Tuple[int, int], anchor: Tuple[int, int], workspace: cvcuda.Tensor, iteration: int, border: cvcuda.Border = cvcuda.Border.CONSTANT, stream: Optional[cvcuda.Stream] = None) - Executes the Morphology operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Morphology operator - for more details and usage examples. Args: dst (cvcuda.Tensor): Output tensor to store the result of the operation. @@ -193,24 +188,14 @@ void ExportOpMorphology(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.Tensor: The output tensor (same as dst). )pbdoc"); - m.def("morphology", &MorphologyVarShape, "src"_a, "morphologyType"_a, "masks"_a, "anchors"_a, py::kw_only(), - "workspace"_a = nullptr, "iteration"_a = 1, "border"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, - "stream"_a = nullptr, R"pbdoc( - - cvcuda.morphology(src: cvcuda.ImageBatchVarShape, morphologyType: cvcuda.MorphologyType, maskSize: cvcuda.Tensor, anchor: cvcuda.Tensor, workspace: cvcuda.ImageBatchVarShape, iteration: int, border: cvcuda.Border = cvcuda.Border.CONSTANT, stream: Optional[cvcuda.Stream] = None) -> cvcuda.ImageBatchVarShape - + m.def("morphology", NvtxTrace("cvcuda.morphology", &MorphologyVarShape), "src"_a, "morphologyType"_a, "masks"_a, + "anchors"_a, py::kw_only(), "workspace"_a = nullptr, "iteration"_a = 1, + "border"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, "stream"_a = nullptr, R"pbdoc( Executes the Morphology operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Morphology operator - for more details and usage examples. Args: src (cvcuda.ImageBatchVarShape): Input image batch containing one or more images. @@ -226,23 +211,14 @@ void ExportOpMorphology(py::module &m) Returns: cvcuda.ImageBatchVarShape: The output image batch. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("morphology_into", &MorphologyVarShapeInto, "dst"_a, "src"_a, "morphologyType"_a, "masks"_a, "anchors"_a, - py::kw_only(), "workspace"_a = nullptr, "iteration"_a = 1, "border"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, - "stream"_a = nullptr, + m.def("morphology_into", NvtxTrace("cvcuda.morphology_into", &MorphologyVarShapeInto), "dst"_a, "src"_a, + "morphologyType"_a, "masks"_a, "anchors"_a, py::kw_only(), "workspace"_a = nullptr, "iteration"_a = 1, + "border"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, "stream"_a = nullptr, R"pbdoc( - - cvcuda.morphology_into(dst: cvcuda.ImageBatchVarShape, src: cvcuda.ImageBatchVarShape, morphologyType: cvcuda.MorphologyType, maskSize: cvcuda.Tensor, anchor: cvcuda.Tensor, workspace: cvcuda.ImageBatchVarShape, iteration: int, border: cvcuda.Border = cvcuda.Border.CONSTANT, stream: Optional[cvcuda.Stream] = None) - Executes the Morphology operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Morphology operator - for more details and usage examples. Args: src (cvcuda.ImageBatchVarShape): Input image batch containing one or more images. @@ -257,11 +233,7 @@ void ExportOpMorphology(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.ImageBatchVarShape: The output image batch (same as dst). )pbdoc"); } } // namespace cvcudapy diff --git a/python/mod_cvcuda/operators/OpNonMaximumSuppression.cpp b/python/mod_cvcuda/operators/OpNonMaximumSuppression.cpp index 63e594272..99a3f467d 100644 --- a/python/mod_cvcuda/operators/OpNonMaximumSuppression.cpp +++ b/python/mod_cvcuda/operators/OpNonMaximumSuppression.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -46,7 +46,8 @@ Tensor NonMaximumSuppressionInto(Tensor &dst, Tensor &src, Tensor &scores, float guard.add(LockMode::LOCK_MODE_WRITE, {dst}); guard.add(LockMode::LOCK_MODE_NONE, {*op}); - op->submit(pstream->cudaHandle(), src, dst, scores, scoreThreshold, iouThreshold); + guard.run([&op, &pstream, &src, &dst, &scores, &scoreThreshold, &iouThreshold]() + { op->submit(pstream->cudaHandle(), src, dst, scores, scoreThreshold, iouThreshold); }); return std::move(dst); } @@ -70,18 +71,14 @@ void ExportOpNonMaximumSuppression(py::module &m) { using namespace pybind11::literals; - m.def("nms", &NonMaximumSuppression, "src"_a, "scores"_a, + m.def("nms", NvtxTrace("cvcuda.nms", &NonMaximumSuppression), "src"_a, "scores"_a, "score_threshold"_a = std::numeric_limits::epsilon(), "iou_threshold"_a = 1.0, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - Executes NMS. The Non-Maximum Suppression (NMS) operation reads a set of input bounding boxes (bboxes) proposals and their scores and writes an output boolean mask with suppressed bboxes as zeros and selected bboxes as ones. - See also: - Refer to the CV-CUDA C API reference for the Non-Maximum Suppression operator - for more details and usage examples. Args: src (cvcuda.Tensor): src[i, j] is the set of input bounding box proposals @@ -100,22 +97,15 @@ void ExportOpNonMaximumSuppression(py::module &m) Returns: cvcuda.Tensor: The output tensor of selected bounding boxes. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("nms_into", &NonMaximumSuppressionInto, "dst"_a, "src"_a, "scores"_a, + m.def("nms_into", NvtxTrace("cvcuda.nms_into", &NonMaximumSuppressionInto), "dst"_a, "src"_a, "scores"_a, "score_threshold"_a = std::numeric_limits::epsilon(), "iou_threshold"_a = 1.0, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - Executes NMS. The Non-Maximum Suppression (NMS) operation reads a set of input bounding boxes (bboxes) proposals and their scores and writes an output boolean mask with suppressed bboxes as zeros and selected bboxes as ones. - See also: - Refer to the CV-CUDA C API reference for the Non-Maximum Suppression operator - for more details and usage examples. Args: dst (cvcuda.Tensor): dst[i, j] is the output boolean mask marking selected @@ -136,11 +126,7 @@ void ExportOpNonMaximumSuppression(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.Tensor: The output tensor (same as dst). )pbdoc"); } diff --git a/python/mod_cvcuda/operators/OpNormalize.cpp b/python/mod_cvcuda/operators/OpNormalize.cpp index a5e9c4d23..fc4a9a3c0 100644 --- a/python/mod_cvcuda/operators/OpNormalize.cpp +++ b/python/mod_cvcuda/operators/OpNormalize.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,8 +16,10 @@ */ #include "Operators.hpp" +#include "VarShapeUtils.hpp" #include +#include #include #include #include @@ -26,6 +28,8 @@ #include #include +#include + namespace cvcudapy { namespace { @@ -41,12 +45,12 @@ namespace { Tensor NormalizeInto(Tensor &output, Tensor &input, Tensor &base, Tensor &scale, std::optional flags, float globalScale, float globalShift, float epsilon, std::optional pstream) { - if (!pstream) + if (!pstream.has_value()) { pstream = Stream::Current(); } - if (!flags) + if (!flags.has_value()) { flags = 0; } @@ -58,7 +62,11 @@ Tensor NormalizeInto(Tensor &output, Tensor &input, Tensor &base, Tensor &scale, guard.add(LockMode::LOCK_MODE_WRITE, {output}); guard.add(LockMode::LOCK_MODE_NONE, {*normalize}); - normalize->submit(pstream->cudaHandle(), input, base, scale, output, globalScale, globalShift, epsilon, *flags); + guard.run( + [&normalize, &pstream, &input, &base, &scale, &output, &globalScale, &globalShift, &epsilon, &flags]() { + normalize->submit(pstream->cudaHandle(), input, base, scale, output, globalScale, globalShift, epsilon, + *flags); + }); return std::move(output); } @@ -71,16 +79,83 @@ Tensor Normalize(Tensor &input, Tensor &base, Tensor &scale, std::optional &values, const char *name, float4 &out, int32_t &count) +{ + const size_t n = values.size(); + if (n < 1 || n > 4) + { + throw std::invalid_argument( + util::ConcatString(name, " must have 1 to 4 values (1 = broadcast, or the channel count), got ", n)); + } + out.x = n > 0 ? values[0] : 0.f; + out.y = n > 1 ? values[1] : 0.f; + out.z = n > 2 ? values[2] : 0.f; + out.w = n > 3 ? values[3] : 0.f; + count = static_cast(n); +} + +Tensor NormalizeScalarInto(Tensor &output, Tensor &input, const std::vector &base, + const std::vector &scale, std::optional flags, float globalScale, + float globalShift, float epsilon, std::optional pstream) +{ + if (!pstream.has_value()) + { + pstream = Stream::Current(); + } + + if (!flags.has_value()) + { + flags = 0; + } + + float4 base4; + float4 scale4; + int32_t baseCount; + int32_t scaleCount; + ToFloat4AndCount(base, "base", base4, baseCount); + ToFloat4AndCount(scale, "scale", scale4, scaleCount); + + auto normalize = CreateOperator(); + + // base/scale are host values passed by value into the kernel launch, so only input/output are + // device resources that need guarding. + ResourceGuard guard(*pstream); + guard.add(LockMode::LOCK_MODE_READ, {input}); + guard.add(LockMode::LOCK_MODE_WRITE, {output}); + guard.add(LockMode::LOCK_MODE_NONE, {*normalize}); + + guard.run( + [&normalize, &pstream, &input, base4, scale4, baseCount, scaleCount, &output, &globalScale, &globalShift, + &epsilon, &flags]() + { + normalize->submit(pstream->cudaHandle(), input, base4, scale4, baseCount, scaleCount, output, globalScale, + globalShift, epsilon, *flags); + }); + + return std::move(output); +} + +Tensor NormalizeScalar(Tensor &input, const std::vector &base, const std::vector &scale, + std::optional flags, float globalScale, float globalShift, float epsilon, + std::optional pstream) +{ + Tensor output = Tensor::Create(input.shape(), input.dtype()); + + return NormalizeScalarInto(output, input, base, scale, flags, globalScale, globalShift, epsilon, pstream); +} + ImageBatchVarShape VarShapeNormalizeInto(ImageBatchVarShape &output, ImageBatchVarShape &input, Tensor &base, Tensor &scale, std::optional flags, float globalScale, float globalShift, float epsilon, std::optional pstream) { - if (!pstream) + if (!pstream.has_value()) { pstream = Stream::Current(); } - if (!flags) + if (!flags.has_value()) { flags = 0; } @@ -92,7 +167,11 @@ ImageBatchVarShape VarShapeNormalizeInto(ImageBatchVarShape &output, ImageBatchV guard.add(LockMode::LOCK_MODE_WRITE, {output}); guard.add(LockMode::LOCK_MODE_NONE, {*normalize}); - normalize->submit(pstream->cudaHandle(), input, base, scale, output, globalScale, globalShift, epsilon, *flags); + guard.run( + [&normalize, &pstream, &input, &base, &scale, &output, &globalScale, &globalShift, &epsilon, &flags]() { + normalize->submit(pstream->cudaHandle(), input, base, scale, output, globalScale, globalShift, epsilon, + *flags); + }); return output; } @@ -101,12 +180,7 @@ ImageBatchVarShape VarShapeNormalize(ImageBatchVarShape &input, Tensor &base, Te std::optional flags, float globalScale, float globalShift, float epsilon, std::optional pstream) { - ImageBatchVarShape output = ImageBatchVarShape::Create(input.capacity()); - - for (int i = 0; i < input.numImages(); ++i) - { - output.pushBack(Image::Create(input[i].size(), input[i].format())); - } + ImageBatchVarShape output = CreateSameShapeImageBatch(input); return VarShapeNormalizeInto(output, input, base, scale, flags, globalScale, globalShift, epsilon, pstream); } @@ -117,26 +191,17 @@ void ExportOpNormalize(py::module &m) { using namespace pybind11::literals; - py::options options; - options.disable_function_signatures(); - py::enum_(m, "NormalizeFlags").value("SCALE_IS_STDDEV", OpFlags::SCALE_IS_STDDEV); float defGlobalScale = 1; float defGlobalShift = 0; float defEpsilon = 0; - m.def("normalize", &Normalize, "src"_a, "base"_a, "scale"_a, "flags"_a = std::nullopt, py::kw_only(), - "globalscale"_a = defGlobalScale, "globalshift"_a = defGlobalShift, "epsilon"_a = defEpsilon, - "stream"_a = nullptr, R"pbdoc( - - cvcuda.normalize(src: cvcuda.Tensor, base: cvcuda.Tensor, scale: cvcuda.Tensor, flags: int, globalscale: float, globalshift: float, epsilon: float, stream: Optional[cvcuda.Stream] = None) -> cvcuda.Tensor - + m.def("normalize", NvtxTrace("cvcuda.normalize", &Normalize), "src"_a, "base"_a, "scale"_a, + "flags"_a = std::nullopt, py::kw_only(), "globalscale"_a = defGlobalScale, "globalshift"_a = defGlobalShift, + "epsilon"_a = defEpsilon, "stream"_a = nullptr, R"pbdoc( Executes the Normalize operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Normalize operator - for more details and usage examples. Args: src (cvcuda.Tensor): Input tensor containing one or more images. @@ -153,22 +218,13 @@ void ExportOpNormalize(py::module &m) Returns: cvcuda.Tensor: The output tensor. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("normalize_into", &NormalizeInto, "dst"_a, "src"_a, "base"_a, "scale"_a, "flags"_a = std::nullopt, - py::kw_only(), "globalscale"_a = defGlobalScale, "globalshift"_a = defGlobalShift, "epsilon"_a = defEpsilon, - "stream"_a = nullptr, R"pbdoc( - - cvcuda.normalize_into(dst: cvcuda.Tensor, src: cvcuda.Tensor, base: cvcuda.Tensor, scale: cvcuda.Tensor, flags: int, globalscale: float, globalshift: float, epsilon: float, stream: Optional[cvcuda.Stream] = None) - + m.def("normalize_into", NvtxTrace("cvcuda.normalize_into", &NormalizeInto), "dst"_a, "src"_a, "base"_a, "scale"_a, + "flags"_a = std::nullopt, py::kw_only(), "globalscale"_a = defGlobalScale, "globalshift"_a = defGlobalShift, + "epsilon"_a = defEpsilon, "stream"_a = nullptr, R"pbdoc( Executes the Normalize operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Normalize operator - for more details and usage examples. Args: dst (cvcuda.Tensor): Output tensor to store the result of the operation. @@ -184,24 +240,14 @@ void ExportOpNormalize(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.Tensor: The output tensor (same as dst). )pbdoc"); - m.def("normalize", &VarShapeNormalize, "src"_a, "base"_a, "scale"_a, "flags"_a = std::nullopt, py::kw_only(), - "globalscale"_a = defGlobalScale, "globalshift"_a = defGlobalShift, "epsilon"_a = defEpsilon, - "stream"_a = nullptr, R"pbdoc( - - cvcuda.normalize(src: cvcuda.ImageBatchVarShape, base: cvcuda.Tensor, scale: cvcuda.Tensor, flags: int, globalscale: float, globalshift: float, epsilon: float, stream: Optional[cvcuda.Stream] = None) -> cvcuda.ImageBatchVarShape - - Executes the Normalize operation on the given cuda stream. + m.def("normalize", NvtxTrace("cvcuda.normalize", &VarShapeNormalize), "src"_a, "base"_a, "scale"_a, + "flags"_a = std::nullopt, py::kw_only(), "globalscale"_a = defGlobalScale, "globalshift"_a = defGlobalShift, + "epsilon"_a = defEpsilon, "stream"_a = nullptr, R"pbdoc( + Executes the Normalize operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Normalize operator - for more details and usage examples. Args: src (cvcuda.ImageBatchVarShape): Input image batch containing one or more images. @@ -218,22 +264,13 @@ void ExportOpNormalize(py::module &m) Returns: cvcuda.ImageBatchVarShape: The output image batch. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("normalize_into", &VarShapeNormalizeInto, "dst"_a, "src"_a, "base"_a, "scale"_a, "flags"_a = std::nullopt, - py::kw_only(), "globalscale"_a = defGlobalScale, "globalshift"_a = defGlobalShift, "epsilon"_a = defEpsilon, - "stream"_a = nullptr, R"pbdoc( - - cvcuda.normalize_into(dst: cvcuda.ImageBatchVarShape, src: cvcuda.ImageBatchVarShape, base: cvcuda.Tensor, scale: cvcuda.Tensor, flags: int, globalscale: float, globalshift: float, epsilon: float, stream: Optional[cvcuda.Stream] = None) - + m.def("normalize_into", NvtxTrace("cvcuda.normalize_into", &VarShapeNormalizeInto), "dst"_a, "src"_a, "base"_a, + "scale"_a, "flags"_a = std::nullopt, py::kw_only(), "globalscale"_a = defGlobalScale, + "globalshift"_a = defGlobalShift, "epsilon"_a = defEpsilon, "stream"_a = nullptr, R"pbdoc( Executes the Normalize operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Normalize operator - for more details and usage examples. Args: dst (cvcuda.ImageBatchVarShape): Output image batch containing the result of the operation. @@ -249,11 +286,58 @@ void ExportOpNormalize(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None + cvcuda.ImageBatchVarShape: The output image batch (same as dst). + )pbdoc"); + + m.def("normalize", NvtxTrace("cvcuda.normalize", &NormalizeScalar), "src"_a, "base"_a, "scale"_a, + "flags"_a = std::nullopt, py::kw_only(), "globalscale"_a = defGlobalScale, "globalshift"_a = defGlobalShift, + "epsilon"_a = defEpsilon, "stream"_a = nullptr, R"pbdoc( + Executes the Normalize operation on the given cuda stream. + + base and scale are given by value as Python lists/tuples of floats (not tensors), so no + parameter tensor is allocated or uploaded; interleaved (NHWC/HWC) and planar (NCHW/CHW) input + are both supported. + + Args: + src (cvcuda.Tensor): Tensor of input images. + base (List[float]): One broadcast base or one base per channel. + scale (List[float]): One broadcast scale or one scale per channel. + flags (int, optional): Set cvcuda.NormalizeFlags.SCALE_IS_STDDEV when scale represents standard deviation; + otherwise use 0. + globalscale (float, optional): Scale applied in addition to the per-channel scale. + globalshift (float, optional): Bias applied in addition to the per-channel base. + epsilon (float, optional): Variance regularizer used with cvcuda.NormalizeFlags.SCALE_IS_STDDEV. + stream (cvcuda.Stream, optional): CUDA stream used to run the operation. + + Returns: + cvcuda.Tensor: The output tensor. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + )pbdoc"); + + m.def("normalize_into", NvtxTrace("cvcuda.normalize_into", &NormalizeScalarInto), "dst"_a, "src"_a, "base"_a, + "scale"_a, "flags"_a = std::nullopt, py::kw_only(), "globalscale"_a = defGlobalScale, + "globalshift"_a = defGlobalShift, "epsilon"_a = defEpsilon, "stream"_a = nullptr, R"pbdoc( + Executes the Normalize operation on the given cuda stream. + + base and scale are given by value as Python lists/tuples of floats (not tensors), so no + parameter tensor is allocated or uploaded; interleaved (NHWC/HWC) and planar (NCHW/CHW) input + are both supported. + + Args: + dst (cvcuda.Tensor): Output tensor to store the result of the operation. + src (cvcuda.Tensor): Input tensor containing one or more images. + base (List[float]): Base values for normalization: length 1 (broadcast) or the channel count. + scale (List[float]): Scale values for normalization: length 1 (broadcast) or the channel count. + flags (int, optional): Algorithm flags, use cvcuda.NormalizeFlags.SCALE_IS_STDDEV if scale passed as argument + is standard deviation instead or 0 if it is scaling. + globalscale (float, optional): Additional scale value to be used in addition to scale. + globalshift (float, optional): Additional bias value to be used in addition to base. + epsilon (float, optional): Epsilon to use when cvcuda.NormalizeFlags.SCALE_IS_STDDEV flag is set as a regularizing + term to be added to variance. + stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + cvcuda.Tensor: The output tensor (same as dst). )pbdoc"); } diff --git a/python/mod_cvcuda/operators/OpOSD.cpp b/python/mod_cvcuda/operators/OpOSD.cpp index 26c28d166..5b7c62261 100644 --- a/python/mod_cvcuda/operators/OpOSD.cpp +++ b/python/mod_cvcuda/operators/OpOSD.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,14 +19,20 @@ #include #include +#include #include #include #include +#include + namespace cvcudapy { namespace { -Tensor OSDInto(Tensor &output, Tensor &input, NVCVElements elements, std::optional pstream) +using ElementsImpl = cvcuda::priv::NVCVElementsImpl; + +Tensor OSDInto(Tensor &output, Tensor &input, const std::shared_ptr &elements, + std::optional pstream) { if (!pstream) { @@ -40,12 +46,14 @@ Tensor OSDInto(Tensor &output, Tensor &input, NVCVElements elements, std::option guard.add(LockMode::LOCK_MODE_WRITE, {output}); guard.add(LockMode::LOCK_MODE_NONE, {*op}); - op->submit(pstream->cudaHandle(), input, output, elements); + auto elementsHandle = static_cast(static_cast(elements.get())); + guard.run([&op, &pstream, &input, &output, elementsHandle]() + { op->submit(pstream->cudaHandle(), input, output, elementsHandle); }); return std::move(output); } -Tensor OSD(Tensor &input, NVCVElements elements, std::optional pstream) +Tensor OSD(Tensor &input, const std::shared_ptr &elements, std::optional pstream) { Tensor output = Tensor::Create(input.shape(), input.dtype()); @@ -58,13 +66,9 @@ void ExportOpOSD(py::module &m) { using namespace pybind11::literals; - m.def("osd", &OSD, "src"_a, "elements"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - + m.def("osd", NvtxTrace("cvcuda.osd", &OSD), "src"_a, "elements"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( Executes the OSD operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the OSD operator - for more details and usage examples. Args: src (cvcuda.Tensor): Input tensor containing one or more images. @@ -74,18 +78,12 @@ void ExportOpOSD(py::module &m) Returns: cvcuda.Tensor: The output tensor. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("osd_into", &OSDInto, "dst"_a, "src"_a, "elements"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - + m.def("osd_into", NvtxTrace("cvcuda.osd_into", &OSDInto), "dst"_a, "src"_a, "elements"_a, py::kw_only(), + "stream"_a = nullptr, R"pbdoc( Executes the OSD operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the OSD operator - for more details and usage examples. Args: dst (cvcuda.Tensor): Output tensor to store the result of the operation. @@ -94,11 +92,7 @@ void ExportOpOSD(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.Tensor: The output tensor (same as dst). )pbdoc"); } diff --git a/python/mod_cvcuda/operators/OpPadAndStack.cpp b/python/mod_cvcuda/operators/OpPadAndStack.cpp index 8a22cb200..53e8262d4 100644 --- a/python/mod_cvcuda/operators/OpPadAndStack.cpp +++ b/python/mod_cvcuda/operators/OpPadAndStack.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -24,9 +24,18 @@ #include #include +#include + namespace cvcudapy { namespace { + +class PadAndStackError : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + Tensor PadAndStackInto(Tensor &output, ImageBatchVarShape &input, Tensor &top, Tensor &left, NVCVBorderType border, float borderValue, std::optional pstream) { @@ -42,7 +51,8 @@ Tensor PadAndStackInto(Tensor &output, ImageBatchVarShape &input, Tensor &top, T guard.add(LockMode::LOCK_MODE_WRITE, {output}); guard.add(LockMode::LOCK_MODE_NONE, {*padstack}); - padstack->submit(pstream->cudaHandle(), input, output, top, left, border, borderValue); + guard.run([&padstack, &pstream, &input, &output, &top, &left, &border, &borderValue]() + { padstack->submit(pstream->cudaHandle(), input, output, top, left, border, borderValue); }); return std::move(output); } @@ -53,7 +63,7 @@ Tensor PadAndStack(ImageBatchVarShape &input, Tensor &top, Tensor &left, NVCVBor nvcv::ImageFormat fmt = input.uniqueFormat(); if (fmt == nvcv::FMT_NONE) { - throw std::runtime_error("All images in the input must have the same format"); + throw PadAndStackError("All images in the input must have the same format"); } Tensor output = Tensor::CreateForImageBatch(input.numImages(), input.maxSize(), fmt); @@ -67,14 +77,10 @@ void ExportOpPadAndStack(py::module &m) { using namespace pybind11::literals; - m.def("padandstack", &PadAndStack, "src"_a, "top"_a, "left"_a, "border"_a = NVCV_BORDER_CONSTANT, "bvalue"_a = 0, - py::kw_only(), "stream"_a = nullptr, R"pbdoc( - + m.def("padandstack", NvtxTrace("cvcuda.padandstack", &PadAndStack), "src"_a, "top"_a, "left"_a, + "border"_a = NVCV_BORDER_CONSTANT, "bvalue"_a = 0, py::kw_only(), "stream"_a = nullptr, R"pbdoc( Executes the Pad and Stack operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Pad and Stack operator - for more details and usage examples. Args: src (cvcuda.ImageBatchVarShape): input image batch containing one or more images. @@ -87,19 +93,12 @@ void ExportOpPadAndStack(py::module &m) Returns: cvcuda.Tensor: The output tensor. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("padandstack_into", &PadAndStackInto, "dst"_a, "src"_a, "top"_a, "left"_a, "border"_a = NVCV_BORDER_CONSTANT, - "bvalue"_a = 0, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - + m.def("padandstack_into", NvtxTrace("cvcuda.padandstack_into", &PadAndStackInto), "dst"_a, "src"_a, "top"_a, + "left"_a, "border"_a = NVCV_BORDER_CONSTANT, "bvalue"_a = 0, py::kw_only(), "stream"_a = nullptr, R"pbdoc( Executes the Pad and Stack operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Pad and Stack operator - for more details and usage examples. Args: dst (cvcuda.Tensor): Output tensor to store the result of the operation. @@ -111,11 +110,7 @@ void ExportOpPadAndStack(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.Tensor: The output tensor (same as dst). )pbdoc"); } diff --git a/python/mod_cvcuda/operators/OpPairwiseMatcher.cpp b/python/mod_cvcuda/operators/OpPairwiseMatcher.cpp index 65dcbd037..4a4d9698d 100644 --- a/python/mod_cvcuda/operators/OpPairwiseMatcher.cpp +++ b/python/mod_cvcuda/operators/OpPairwiseMatcher.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -32,8 +32,8 @@ using TupleTensor3 = std::tuple, std::optional numMatches, std::optional distances, - Tensor &set1, Tensor &set2, std::optional numSet1, - std::optional numSet2, bool crossCheck, int matchesPerPoint, + Tensor &set1, Tensor &set2, const std::optional &numSet1, + const std::optional &numSet2, bool crossCheck, int matchesPerPoint, std::optional normType, NVCVPairwiseMatcherType algoChoice, std::optional pstream) { @@ -71,18 +71,23 @@ TupleTensor3 PairwiseMatcherInto(Tensor &matches, std::optional numMatch guard.add(LockMode::LOCK_MODE_WRITE, {*distances}); } - op->submit(pstream->cudaHandle(), set1, set2, (numSet1 ? *numSet1 : nvcv::Tensor{nullptr}), - (numSet2 ? *numSet2 : nvcv::Tensor{nullptr}), matches, - (numMatches ? *numMatches : nvcv::Tensor{nullptr}), (distances ? *distances : nvcv::Tensor{nullptr}), - crossCheck, matchesPerPoint, *normType); + guard.run( + [&numSet1, &numSet2, &numMatches, &distances, &op, &pstream, &set1, &set2, &matches, &crossCheck, + &matchesPerPoint, &normType]() + { + const nvcv::Tensor nullTensor{nullptr}; + op->submit(pstream->cudaHandle(), set1, set2, AsNvcvTensor(numSet1, nullTensor), + AsNvcvTensor(numSet2, nullTensor), matches, AsNvcvTensor(numMatches, nullTensor), + AsNvcvTensor(distances, nullTensor), crossCheck, matchesPerPoint, *normType); + }); return TupleTensor3(std::move(matches), numMatches, distances); } -TupleTensor3 PairwiseMatcher(Tensor &set1, Tensor &set2, std::optional numSet1, std::optional numSet2, - std::optional numMatches, bool distances, bool crossCheck, int matchesPerPoint, - std::optional normType, NVCVPairwiseMatcherType algoChoice, - std::optional pstream) +TupleTensor3 PairwiseMatcher(Tensor &set1, Tensor &set2, const std::optional &numSet1, + const std::optional &numSet2, std::optional numMatches, bool distances, + bool crossCheck, int matchesPerPoint, std::optional normType, + NVCVPairwiseMatcherType algoChoice, std::optional pstream) { nvcv::TensorShape set1Shape = set1.shape(); nvcv::TensorShape set2Shape = set2.shape(); @@ -95,7 +100,7 @@ TupleTensor3 PairwiseMatcher(Tensor &set1, Tensor &set2, std::optional n int64_t numSamples = set1Shape[0]; int64_t maxMatches = std::max(set1Shape[1], set2Shape[1]) * matchesPerPoint; - if (!numMatches) + if (!numMatches.has_value()) { numMatches = crossCheck; } @@ -104,7 +109,8 @@ TupleTensor3 PairwiseMatcher(Tensor &set1, Tensor &set2, std::optional n Tensor matches = Tensor::Create({{numSamples, maxMatches, 2}, "NMA"}, nvcv::TYPE_S32); - std::optional numMatchesTensor, distancesTensor; + std::optional numMatchesTensor; + std::optional distancesTensor; if (*numMatches) { @@ -127,14 +133,12 @@ void ExportOpPairwiseMatcher(py::module &m) { using namespace pybind11::literals; - m.def("match", &PairwiseMatcher, "set1"_a, "set2"_a, "num_set1"_a = nullptr, "num_set2"_a = nullptr, - "num_matches"_a = nullptr, "distances"_a = false, "cross_check"_a = false, "matches_per_point"_a = 1, - "norm_type"_a = nullptr, "algo_choice"_a = NVCV_BRUTE_FORCE, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - + m.def("match", NvtxTrace("cvcuda.match", &PairwiseMatcher), "set1"_a, "set2"_a, "num_set1"_a = nullptr, + "num_set2"_a = nullptr, "num_matches"_a = nullptr, "distances"_a = false, "cross_check"_a = false, + "matches_per_point"_a = 1, "norm_type"_a = nullptr, "algo_choice"_a = NVCV_BRUTE_FORCE, py::kw_only(), + "stream"_a = nullptr, R"pbdoc( Executes the Pairwise matcher operation on the given CUDA stream. - See also: - Refer to the CV-CUDA C API reference for this operator for more details and usage examples. Args: set1 (cvcuda.Tensor): Input tensor with 1st set of points. @@ -158,19 +162,15 @@ void ExportOpPairwiseMatcher(py::module &m) The number of matches tensor may be None if its argument is False. The distances tensor may be None if its argument is False. - Caution: - Restrictions to several arguments may apply. Check the C API references of the CV-CUDA operator. )pbdoc"); - m.def("match_into", &PairwiseMatcherInto, "matches"_a, "num_matches"_a = nullptr, "distances"_a = nullptr, "set1"_a, - "set2"_a, "num_set1"_a = nullptr, "num_set2"_a = nullptr, "cross_check"_a = false, "matches_per_point"_a = 1, - "norm_type"_a = nullptr, "algo_choice"_a = NVCV_BRUTE_FORCE, py::kw_only(), "stream"_a = nullptr, + m.def("match_into", NvtxTrace("cvcuda.match_into", &PairwiseMatcherInto), "matches"_a, "num_matches"_a = nullptr, + "distances"_a = nullptr, "set1"_a, "set2"_a, "num_set1"_a = nullptr, "num_set2"_a = nullptr, + "cross_check"_a = false, "matches_per_point"_a = 1, "norm_type"_a = nullptr, + "algo_choice"_a = NVCV_BRUTE_FORCE, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - Executes the Pairwise matcher operation on the given CUDA stream. - See also: - Refer to the CV-CUDA C API reference for this operator for more details and usage examples. Args: matches (cvcuda.Tensor): Output tensor with matches. @@ -194,8 +194,6 @@ void ExportOpPairwiseMatcher(py::module &m) The number of matches tensor may be None if its argument is None. The distances tensor may be None if its argument is None. - Caution: - Restrictions to several arguments may apply. Check the C API references of the CV-CUDA operator. )pbdoc"); } diff --git a/python/mod_cvcuda/operators/OpPillowResize.cpp b/python/mod_cvcuda/operators/OpPillowResize.cpp index e01f0f8d7..ca73d13c9 100644 --- a/python/mod_cvcuda/operators/OpPillowResize.cpp +++ b/python/mod_cvcuda/operators/OpPillowResize.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "../WorkspaceCache.hpp" #include "Operators.hpp" +#include "VarShapeUtils.hpp" #include #include @@ -32,15 +33,23 @@ #include #include +#include + namespace cvcudapy { namespace { +class PillowResizeError : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + // Specialized class for cvcuda::PillowResize operator with a better cache Key. // It allows for reusing an existing operator object from cache if its payload size is >= the required size. // It also allows to fetch the biggest payload object to be reused while removing all others. // This is more flexible than using the generic PyOperator class and its Key class. -class PyOpPillowResize : public nvcvpy::Container +class PyOpPillowResize : public nvcvpy::Container // NOSONAR: operator wrappers share the Python cache hierarchy. { public: // Define a Key class to be used by the cache to fetch similar items for potential reuse. @@ -48,7 +57,7 @@ class PyOpPillowResize : public nvcvpy::Container { public: // Arguments of the key constructor should match the corresponding cvcuda operator arguments. - Key() {} + Key() = default; size_t payloadSize() const { @@ -69,49 +78,45 @@ class PyOpPillowResize : public nvcvpy::Container }; // Constructor instantiate the cache key and the operator object. - PyOpPillowResize() - : m_key() - , m_op() - { - } + PyOpPillowResize() = default; inline void submit(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, nvcv::ImageFormat format, - NVCVInterpolationType interpolation) + NVCVInterpolationType interpolation) const { int batch_size = getBatchSize(in); nvcv::Size2D in_size = imageSize(in); nvcv::Size2D out_size = imageSize(out); - auto req = m_op.getWorkspaceRequirements(batch_size, out_size, in_size, format); + auto req = m_op.getWorkspaceRequirements(batch_size, in_size, out_size, format); auto ws = WorkspaceCache::instance().get(req, stream); m_op(stream, ws.get(), in, out, interpolation); } - inline int getBatchSize(const nvcv::Tensor &tensor) + static int getBatchSize(const nvcv::Tensor &tensor) { auto access = nvcv::TensorDataAccessStridedImagePlanar::Create(tensor.exportData()); if (!access) - throw std::runtime_error("Incompatible tensor layout"); + throw PillowResizeError("Incompatible tensor layout"); - return access->numSamples(); + return static_cast(access->numSamples()); } static nvcv::Size2D imageSize(const nvcv::Tensor &tensor) { auto access = nvcv::TensorDataAccessStridedImagePlanar::Create(tensor.exportData()); if (!access) - throw std::runtime_error("Incompatible tensor layout"); + throw PillowResizeError("Incompatible tensor layout"); return access->size(); } inline void submit(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, - const NVCVInterpolationType interpolation) + const NVCVInterpolationType interpolation) const { assert(in.numImages() == out.numImages()); auto in_sizes = imageSizes(in); auto out_sizes = imageSizes(out); - int N = in_sizes.size(); + auto N = static_cast(in_sizes.size()); auto req = m_op.getWorkspaceRequirements(N, in_sizes.data(), out_sizes.data(), in.uniqueFormat()); auto ws = WorkspaceCache::instance().get(req, stream); m_op(stream, ws.get(), in, out, interpolation); @@ -129,7 +134,7 @@ class PyOpPillowResize : public nvcvpy::Container // Required override to get the py object container. py::object container() const override { - return *this; + return py::reinterpret_borrow(this->ptr()); } // Required override to get the key as the base interface class. @@ -140,21 +145,19 @@ class PyOpPillowResize : public nvcvpy::Container // The static fetch function can be used to specialize the fetch of a specific object from the cache. // It can be used to select the best object among a number of matched cache objects. - // It can also be used to remove other objects that are not needed in the cache anymore. - // Here, it fetches the biggest payload OP among cache items and remove all other OPs from the cache. - // It is ok to remove them since the biggest payload OP can be used to accomodate all of them, - // so they will never be reused and thus are no longer necessary. + // Here, it fetches the biggest payload OP among cache items (can handle any smaller request). static std::shared_ptr fetch(std::vector> &cache) { assert(!cache.empty()); + // Find the operator with the largest workspace (can handle any smaller request) std::shared_ptr retItem = cache[0]; size_t maxPayloadSize = 0; for (const auto &item : cache) { - const Key &key = static_cast(item.get()->key()); - size_t keyPayloadSize = key.payloadSize(); + const auto &key = static_cast(item.get()->key()); + auto keyPayloadSize = key.payloadSize(); if (keyPayloadSize > maxPayloadSize) { @@ -163,9 +166,9 @@ class PyOpPillowResize : public nvcvpy::Container } } - cache.clear(); - - nvcvpy::Cache::removeAllNotInUseMatching(retItem.get()->key()); + // Note: Removed cache.clear() and removeAllNotInUseMatching() calls to reduce per-call overhead. + // The cache will naturally evict unused operators when memory pressure occurs. + // This fix matches the pattern used in OpInpaint.cpp, OpSIFT.cpp, and OpFindHomography.cpp. return retItem; } @@ -182,11 +185,11 @@ Tensor PillowResizeInto(Tensor &output, Tensor &input, nvcv::ImageFormat format, { pstream = Stream::Current(); } - auto in_access = nvcv::TensorDataAccessStridedImagePlanar::Create(input.exportData()); - auto out_access = nvcv::TensorDataAccessStridedImagePlanar::Create(output.exportData()); - if (!in_access || !out_access) + auto in_access = nvcv::TensorDataAccessStridedImagePlanar::Create(input.exportData()); + if (auto out_access = nvcv::TensorDataAccessStridedImagePlanar::Create(output.exportData()); + !in_access || !out_access) { - throw std::runtime_error("Incompatible input/output tensor layout"); + throw PillowResizeError("Incompatible input/output tensor layout"); } // Use CreateOperatorEx to use the extended create operator function passing the specialized PyOperator above @@ -198,7 +201,8 @@ Tensor PillowResizeInto(Tensor &output, Tensor &input, nvcv::ImageFormat format, guard.add(LockMode::LOCK_MODE_WRITE, {output}); guard.add(LockMode::LOCK_MODE_READWRITE, {*pillowResize}); - pillowResize->submit(pstream->cudaHandle(), input, output, format, interp); + guard.run([&pillowResize, &pstream, &input, &output, &format, &interp]() + { pillowResize->submit(pstream->cudaHandle(), input, output, format, interp); }); return output; } @@ -227,7 +231,8 @@ ImageBatchVarShape VarShapePillowResizeInto(ImageBatchVarShape &output, ImageBat guard.add(LockMode::LOCK_MODE_WRITE, {output}); guard.add(LockMode::LOCK_MODE_READWRITE, {*pillowResize}); - pillowResize->submit(pstream->cudaHandle(), input, output, interpolation); + guard.run([&pillowResize, &pstream, &input, &output, &interpolation]() + { pillowResize->submit(pstream->cudaHandle(), input, output, interpolation); }); return output; } @@ -235,20 +240,12 @@ ImageBatchVarShape VarShapePillowResizeInto(ImageBatchVarShape &output, ImageBat ImageBatchVarShape VarShapePillowResize(ImageBatchVarShape &input, const std::vector> &outSizes, NVCVInterpolationType interpolation, std::optional pstream) { - ImageBatchVarShape output = ImageBatchVarShape::Create(input.capacity()); - if (static_cast(outSizes.size()) != input.numImages()) { - throw std::runtime_error("Invalid outSizes passed"); + throw PillowResizeError("Invalid outSizes passed"); } - for (int i = 0; i < input.numImages(); ++i) - { - nvcv::ImageFormat format = input[i].format(); - auto size = outSizes[i]; - auto image = Image::Create({std::get<0>(size), std::get<1>(size)}, format); - output.pushBack(image); - } + ImageBatchVarShape output = CreateSizedImageBatch(input, outSizes); return VarShapePillowResizeInto(output, input, interpolation, pstream); } @@ -259,19 +256,10 @@ void ExportOpPillowResize(py::module &m) { using namespace pybind11::literals; - py::options options; - options.disable_function_signatures(); - - m.def("pillowresize", &PillowResize, "src"_a, "shape"_a, "format"_a, "interp"_a = NVCV_INTERP_LINEAR, py::kw_only(), - "stream"_a = nullptr, R"pbdoc( - - cvcuda.pillowresize(src: cvcuda.Tensor, shape:Shape, format:ImageFormat, interp: Interp = cvcuda.Interp.LINEAR, stream: Optional[cvcuda.Stream] = None) -> cvcuda.Tensor - + m.def("pillowresize", NvtxTrace("cvcuda.pillowresize", &PillowResize), "src"_a, "shape"_a, "format"_a, + "interp"_a = NVCV_INTERP_LINEAR, py::kw_only(), "stream"_a = nullptr, R"pbdoc( Executes the Pillow Resize operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Pillow Resize operator - for more details and usage examples. Args: src (cvcuda.Tensor): Input tensor containing one or more images. @@ -283,21 +271,12 @@ void ExportOpPillowResize(py::module &m) Returns: cvcuda.Tensor: The output tensor. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("pillowresize_into", &PillowResizeInto, "dst"_a, "src"_a, "format"_a, "interp"_a = NVCV_INTERP_LINEAR, - py::kw_only(), "stream"_a = nullptr, R"pbdoc( - - cvcuda.pillowresize_into(dst: cvcuda.Tensor, src: cvcuda.Tensor, shape: Tuple[int], format: cvcuda.Format, interp: Interp = cvcuda.Interp.LINEAR, stream: Optional[cvcuda.Stream] = None) - + m.def("pillowresize_into", NvtxTrace("cvcuda.pillowresize_into", &PillowResizeInto), "dst"_a, "src"_a, "format"_a, + "interp"_a = NVCV_INTERP_LINEAR, py::kw_only(), "stream"_a = nullptr, R"pbdoc( Executes the Pillow Resize operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Pillow Resize operator - for more details and usage examples. Args: dst (cvcuda.Tensor): Output tensor to store the result of the operation. @@ -308,23 +287,13 @@ void ExportOpPillowResize(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.Tensor: The output tensor (same as dst). )pbdoc"); - m.def("pillowresize", &VarShapePillowResize, "src"_a, "sizes"_a, "interp"_a = NVCV_INTERP_LINEAR, py::kw_only(), - "stream"_a = nullptr, R"pbdoc( - - cvcuda.pillowresize(src: cvcuda.ImageBatchVarShape, shape: Tuple[int], format: cvcuda.Format, interp: Interp = cvcuda.Interp.LINEAR, stream: Optional[cvcuda.Stream] = None) ->ImageBatchVarShape - + m.def("pillowresize", NvtxTrace("cvcuda.pillowresize", &VarShapePillowResize), "src"_a, "sizes"_a, + "interp"_a = NVCV_INTERP_LINEAR, py::kw_only(), "stream"_a = nullptr, R"pbdoc( Executes the Pillow Resize operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Pillow Resize operator - for more details and usage examples. Args: src (cvcuda.ImageBatchVarShape): Input image batch containing one or more images. @@ -335,21 +304,12 @@ void ExportOpPillowResize(py::module &m) Returns: cvcuda.ImageBatchVarShape: The output image batch. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("pillowresize_into", &VarShapePillowResizeInto, "dst"_a, "src"_a, "interp"_a = NVCV_INTERP_LINEAR, - py::kw_only(), "stream"_a = nullptr, R"pbdoc( - - cvcuda.pillowresize(dst: cvcuda.ImageBatchVarShape, src: cvcuda.ImageBatchVarShape, shape: Tuple[int], format: cvcuda.Format, interp: cvcuda.Interp = cvcuda.Interp.LINEAR, stream: Optional[cvcuda.Stream] = None) - + m.def("pillowresize_into", NvtxTrace("cvcuda.pillowresize_into", &VarShapePillowResizeInto), "dst"_a, "src"_a, + "interp"_a = NVCV_INTERP_LINEAR, py::kw_only(), "stream"_a = nullptr, R"pbdoc( Executes the Pillow Resize operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Pillow Resize operator - for more details and usage examples. Args: src (cvcuda.ImageBatchVarShape): Input image batch containing one or more images. @@ -358,11 +318,7 @@ void ExportOpPillowResize(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.ImageBatchVarShape: The output image batch (same as dst). )pbdoc"); } diff --git a/python/mod_cvcuda/operators/OpPosterize.cpp b/python/mod_cvcuda/operators/OpPosterize.cpp new file mode 100644 index 000000000..bd2afb5fd --- /dev/null +++ b/python/mod_cvcuda/operators/OpPosterize.cpp @@ -0,0 +1,145 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "UnaryElementwiseOp.hpp" + +#include +#include + +namespace cvcudapy { + +namespace { +// Thin Python-facing wrappers; the create/guard/submit body lives in UnaryElementwiseOp.hpp, with +// the bit count forwarded as the trailing submit() parameter. +Tensor PosterizeInto(Tensor &output, Tensor &input, int32_t bits, std::optional pstream) +{ + return UnaryElementwiseInto(output, input, pstream, bits); +} + +Tensor Posterize(Tensor &input, int32_t bits, std::optional pstream) +{ + return UnaryElementwiseTensor(input, pstream, bits); +} + +ImageBatchVarShape PosterizeVarShapeInto(ImageBatchVarShape &output, ImageBatchVarShape &input, int32_t bits, + std::optional pstream) +{ + return UnaryElementwiseInto(output, input, pstream, bits); +} + +ImageBatchVarShape PosterizeVarShape(ImageBatchVarShape &input, int32_t bits, std::optional pstream) +{ + return UnaryElementwiseVarShape(input, pstream, bits); +} + +} // namespace + +void ExportOpPosterize(py::module &m) +{ + using namespace pybind11::literals; + + m.def("posterize", NvtxTrace("cvcuda.posterize", &Posterize), "src"_a, "bits"_a, py::kw_only(), + "stream"_a = nullptr, R"pbdoc( + + Executes the Posterize operation on the given cuda stream. + + Keeps the ``bits`` most-significant bits of every channel value and zeros the rest: + ``out = in & ~((1 << (W - bits)) - 1)``, where ``W`` is the data type bit width (8 for uint8, + 16 for uint16). Mirrors torchvision.transforms.v2.functional.posterize / PIL ImageOps.posterize. + + See also: + Refer to the CV-CUDA C API reference for the Posterize operator for more details and usage + examples. + + Args: + src (cvcuda.Tensor): Input tensor containing one or more images. + bits (int): Number of most-significant bits to keep per channel; must be in [0, W] where W + is the data type bit width (8 for uint8, 16 for uint16). bits == W leaves the image + unchanged; bits == 0 zeros the image. + stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + cvcuda.Tensor: The output tensor (same shape, dtype, and layout as src). + + Caution: + Restrictions to several arguments may apply. Check the C API references of the CV-CUDA + operator. + )pbdoc"); + + m.def("posterize_into", NvtxTrace("cvcuda.posterize_into", &PosterizeInto), "dst"_a, "src"_a, "bits"_a, + py::kw_only(), "stream"_a = nullptr, R"pbdoc( + + Executes the Posterize operation on the given cuda stream. + + See also: + Refer to the CV-CUDA C API reference for the Posterize operator for more details and usage + examples. + + Args: + dst (cvcuda.Tensor): Output tensor to store the result of the operation. + src (cvcuda.Tensor): Input tensor containing one or more images. + bits (int): Number of most-significant bits to keep per channel (see posterize). + stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + cvcuda.Tensor: The output tensor (same as dst). + + Caution: + Restrictions to several arguments may apply. Check the C API references of the CV-CUDA + operator. + )pbdoc"); + + m.def("posterize", NvtxTrace("cvcuda.posterize", &PosterizeVarShape), "src"_a, "bits"_a, py::kw_only(), + "stream"_a = nullptr, R"pbdoc( + + Executes the Posterize operation on the given cuda stream. + + Args: + src (cvcuda.ImageBatchVarShape): Input image batch containing one or more images. + bits (int): Number of most-significant bits to keep per channel (see posterize). + stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + cvcuda.ImageBatchVarShape: The output image batch (same formats and sizes as src). + + Caution: + Restrictions to several arguments may apply. Check the C API references of the CV-CUDA + operator. + )pbdoc"); + + m.def("posterize_into", NvtxTrace("cvcuda.posterize_into", &PosterizeVarShapeInto), "dst"_a, "src"_a, "bits"_a, + py::kw_only(), "stream"_a = nullptr, + R"pbdoc( + + Executes the Posterize operation on the given cuda stream. + + Args: + dst (cvcuda.ImageBatchVarShape): Output image batch to store the result of the operation. + src (cvcuda.ImageBatchVarShape): Input image batch containing one or more images. + bits (int): Number of most-significant bits to keep per channel (see posterize). + stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + cvcuda.ImageBatchVarShape: The output image batch (same as dst). + + Caution: + Restrictions to several arguments may apply. Check the C API references of the CV-CUDA + operator. + )pbdoc"); +} + +} // namespace cvcudapy diff --git a/python/mod_cvcuda/operators/OpRandomResizedCrop.cpp b/python/mod_cvcuda/operators/OpRandomResizedCrop.cpp index a5ce3803f..729184355 100644 --- a/python/mod_cvcuda/operators/OpRandomResizedCrop.cpp +++ b/python/mod_cvcuda/operators/OpRandomResizedCrop.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,6 +16,7 @@ */ #include "Operators.hpp" +#include "VarShapeUtils.hpp" #include #include @@ -26,9 +27,18 @@ #include #include +#include + namespace cvcudapy { namespace { + +class RandomResizedCropError : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + Tensor RandomResizedCropInto(Tensor &output, Tensor &input, double min_scale, double max_scale, double min_ratio, double max_ratio, NVCVInterpolationType interp, uint32_t seed, std::optional pstream) @@ -38,7 +48,8 @@ Tensor RandomResizedCropInto(Tensor &output, Tensor &input, double min_scale, do pstream = Stream::Current(); } - int32_t batchSize = static_cast(input.shape()[0]); + // HWC inputs (rank 3) have no N dim, so shape[0] is H — fall back to 1. + int32_t batchSize = (input.shape().size() == 4) ? static_cast(input.shape()[0]) : 1; auto randomResizedCrop = CreateOperator(min_scale, max_scale, min_ratio, max_ratio, batchSize, seed); @@ -47,7 +58,8 @@ Tensor RandomResizedCropInto(Tensor &output, Tensor &input, double min_scale, do guard.add(LockMode::LOCK_MODE_WRITE, {output}); guard.add(LockMode::LOCK_MODE_READWRITE, {*randomResizedCrop}); - randomResizedCrop->submit(pstream->cudaHandle(), input, output, interp); + guard.run([&randomResizedCrop, &pstream, &input, &output, &interp]() + { randomResizedCrop->submit(pstream->cudaHandle(), input, output, interp); }); return std::move(output); } @@ -78,7 +90,8 @@ ImageBatchVarShape RandomResizedCropVarShapeInto(ImageBatchVarShape &output, Ima guard.add(LockMode::LOCK_MODE_WRITE, {output}); guard.add(LockMode::LOCK_MODE_READWRITE, {*randomResizedCrop}); - randomResizedCrop->submit(pstream->cudaHandle(), input, output, interp); + guard.run([&randomResizedCrop, &pstream, &input, &output, &interp]() + { randomResizedCrop->submit(pstream->cudaHandle(), input, output, interp); }); return output; } @@ -90,18 +103,11 @@ ImageBatchVarShape RandomResizedCropVarShape(ImageBatchVarShape { if (input.numImages() != (int)out_size.size()) { - throw std::runtime_error("Number of input images must be equal to the number of elements in output size list "); + throw RandomResizedCropError( + "Number of input images must be equal to the number of elements in output size list "); } - ImageBatchVarShape output = ImageBatchVarShape::Create(input.capacity()); - - for (int i = 0; i < input.numImages(); ++i) - { - nvcv::ImageFormat format = input[i].format(); - auto size = out_size[i]; - auto image = Image::Create({std::get<0>(size), std::get<1>(size)}, format); - output.pushBack(image); - } + ImageBatchVarShape output = CreateSizedImageBatch(input, out_size); return RandomResizedCropVarShapeInto(output, input, min_scale, max_scale, min_ratio, max_ratio, interp, seed, pstream); @@ -113,21 +119,12 @@ void ExportOpRandomResizedCrop(py::module &m) { using namespace pybind11::literals; - py::options options; - options.disable_function_signatures(); - - m.def("random_resized_crop", &RandomResizedCrop, "src"_a, "shape"_a, "min_scale"_a = 0.08, "max_scale"_a = 1.0, - "min_ratio"_a = 0.75, "max_ratio"_a = 1.3333333333333333, "interp"_a = NVCV_INTERP_LINEAR, "seed"_a = 0, - py::kw_only(), "stream"_a = nullptr, + m.def("random_resized_crop", NvtxTrace("cvcuda.random_resized_crop", &RandomResizedCrop), "src"_a, "shape"_a, + "min_scale"_a = 0.08, "max_scale"_a = 1.0, "min_ratio"_a = 0.75, "max_ratio"_a = 1.3333333333333333, + "interp"_a = NVCV_INTERP_LINEAR, "seed"_a = 0, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - - cvcuda.random_resized_crop(src: cvcuda.Tensor, shape: Tuple, min_scale: double, max_scale: double, min_ratio: double, max_ratio: double, interp: Interp = cvcuda.Interp.LINEAR, seed: int, stream: Optional[cvcuda.Stream] = None) -> cvcuda.Tensor - Executes the RandomResizedCrop operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the RandomResizedCrop operator - for more details and usage examples. Args: src (cvcuda.Tensor): Input tensor containing one or more images. @@ -143,22 +140,13 @@ void ExportOpRandomResizedCrop(py::module &m) Returns: cvcuda.Tensor: The output tensor. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("random_resized_crop_into", &RandomResizedCropInto, "dst"_a, "src"_a, "min_scale"_a = 0.08, - "max_scale"_a = 1.0, "min_ratio"_a = 0.75, "max_ratio"_a = 1.3333333333333333, + m.def("random_resized_crop_into", NvtxTrace("cvcuda.random_resized_crop_into", &RandomResizedCropInto), "dst"_a, + "src"_a, "min_scale"_a = 0.08, "max_scale"_a = 1.0, "min_ratio"_a = 0.75, "max_ratio"_a = 1.3333333333333333, "interp"_a = NVCV_INTERP_LINEAR, "seed"_a = 0, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - - cvcuda.random_resized_crop_into(dst: cvcuda.Tensor, src: cvcuda.Tensor, shape: Tuple, min_scale: double, max_scale: double, min_ratio: double, max_ratio: double, interp: Interp = cvcuda.Interp.LINEAR, seed: int, stream: Optional[cvcuda.Stream] = None) - Executes the RandomResizedCrop operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the RandomResizedCrop operator - for more details and usage examples. Args: dst (cvcuda.Tensor): Output tensor to store the result of the operation. @@ -172,24 +160,15 @@ void ExportOpRandomResizedCrop(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.Tensor: The output tensor (same as dst). )pbdoc"); - m.def("random_resized_crop", &RandomResizedCropVarShape, "src"_a, "sizes"_a, "min_scale"_a = 0.08, - "max_scale"_a = 1.0, "min_ratio"_a = 0.75, "max_ratio"_a = 1.3333333333333333, - "interp"_a = NVCV_INTERP_LINEAR, "seed"_a = 0, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - - cvcuda.random_resized_crop(src: cvcuda.ImageBatchVarShape, shape: Tuple, min_scale: double, max_scale: double, min_ratio: double, max_ratio: double, interp: Interp = cvcuda.Interp.LINEAR, seed: int, stream: Optional[cvcuda.Stream] = None) -> cvcuda.ImageBatchVarShape - + m.def("random_resized_crop", NvtxTrace("cvcuda.random_resized_crop", &RandomResizedCropVarShape), "src"_a, + "sizes"_a, "min_scale"_a = 0.08, "max_scale"_a = 1.0, "min_ratio"_a = 0.75, + "max_ratio"_a = 1.3333333333333333, "interp"_a = NVCV_INTERP_LINEAR, "seed"_a = 0, py::kw_only(), + "stream"_a = nullptr, R"pbdoc( Executes the RandomResizedCrop operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the RandomResizedCrop operator - for more details and usage examples. Args: src (cvcuda.ImageBatchVarShape): Input image batch containing one or more images. @@ -205,22 +184,14 @@ void ExportOpRandomResizedCrop(py::module &m) Returns: cvcuda.Tensor: The output tensor. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("random_resized_crop_into", &RandomResizedCropVarShapeInto, "dst"_a, "src"_a, "min_scale"_a = 0.08, - "max_scale"_a = 1.0, "min_ratio"_a = 0.75, "max_ratio"_a = 1.3333333333333333, - "interp"_a = NVCV_INTERP_LINEAR, "seed"_a = 0, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - - cvcuda.random_resized_crop_into(dst: cvcuda.ImageBatchVarShape, src: cvcuda.ImageBatchVarShape, shape: Tuple, min_scale: double, max_scale: double, min_ratio: double, max_ratio: double, interp: Interp = cvcuda.Interp.LINEAR, seed: int, stream: Optional[cvcuda.Stream] = None) - + m.def("random_resized_crop_into", NvtxTrace("cvcuda.random_resized_crop_into", &RandomResizedCropVarShapeInto), + "dst"_a, "src"_a, "min_scale"_a = 0.08, "max_scale"_a = 1.0, "min_ratio"_a = 0.75, + "max_ratio"_a = 1.3333333333333333, "interp"_a = NVCV_INTERP_LINEAR, "seed"_a = 0, py::kw_only(), + "stream"_a = nullptr, R"pbdoc( Executes the RandomResizedCrop operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the RandomResizedCrop operator - for more details and usage examples. Args: src (cvcuda.ImageBatchVarShape): Input image batch containing one or more images. @@ -234,11 +205,7 @@ void ExportOpRandomResizedCrop(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.ImageBatchVarShape: The output image batch (same as dst). )pbdoc"); } diff --git a/python/mod_cvcuda/operators/OpReformat.cpp b/python/mod_cvcuda/operators/OpReformat.cpp index be83a6a3e..8addc59f6 100644 --- a/python/mod_cvcuda/operators/OpReformat.cpp +++ b/python/mod_cvcuda/operators/OpReformat.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -40,7 +40,7 @@ Tensor ReformatInto(Tensor &output, Tensor &input, std::optional pstream guard.add(LockMode::LOCK_MODE_WRITE, {output}); guard.add(LockMode::LOCK_MODE_NONE, {*reformat}); - reformat->submit(pstream->cudaHandle(), input, output); + guard.run([&reformat, &pstream, &input, &output]() { reformat->submit(pstream->cudaHandle(), input, output); }); return std::move(output); } @@ -60,13 +60,10 @@ void ExportOpReformat(py::module &m) { using namespace pybind11::literals; - m.def("reformat", &Reformat, "src"_a, "layout"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - + m.def("reformat", NvtxTrace("cvcuda.reformat", &Reformat), "src"_a, "layout"_a, py::kw_only(), "stream"_a = nullptr, + R"pbdoc( Executes the Reformat operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Reformat operator - for more details and usage examples. Args: src (cvcuda.Tensor): Input tensor containing one or more images. @@ -76,18 +73,12 @@ void ExportOpReformat(py::module &m) Returns: cvcuda.Tensor: The output tensor. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("reformat_into", &ReformatInto, "dst"_a, "src"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - + m.def("reformat_into", NvtxTrace("cvcuda.reformat_into", &ReformatInto), "dst"_a, "src"_a, py::kw_only(), + "stream"_a = nullptr, R"pbdoc( Executes the Reformat operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Reformat operator - for more details and usage examples. Args: dst (cvcuda.Tensor): Output tensor to store the result of the operation. @@ -95,11 +86,7 @@ void ExportOpReformat(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.Tensor: The output tensor (same as dst). )pbdoc"); } diff --git a/python/mod_cvcuda/operators/OpRemap.cpp b/python/mod_cvcuda/operators/OpRemap.cpp index 2f1199f90..17af27852 100644 --- a/python/mod_cvcuda/operators/OpRemap.cpp +++ b/python/mod_cvcuda/operators/OpRemap.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -26,10 +26,18 @@ #include #include +#include + namespace cvcudapy { namespace { +class RemapError : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + // Tensor into ----------------------------------------------------------------- Tensor RemapInto(Tensor &dst, Tensor &src, Tensor &map, NVCVInterpolationType srcInterp, @@ -50,8 +58,12 @@ Tensor RemapInto(Tensor &dst, Tensor &src, Tensor &map, NVCVInterpolationType sr guard.add(LockMode::LOCK_MODE_WRITE, {dst}); guard.add(LockMode::LOCK_MODE_NONE, {*op}); - op->submit(pstream->cudaHandle(), src, dst, map, srcInterp, mapInterp, mapValueType, alignCorners, borderMode, - bValue); + guard.run( + [&op, &pstream, &src, &dst, &map, &srcInterp, &mapInterp, &mapValueType, &alignCorners, &borderMode, &bValue]() + { + op->submit(pstream->cudaHandle(), src, dst, map, srcInterp, mapInterp, mapValueType, alignCorners, + borderMode, bValue); + }); return std::move(dst); } @@ -65,7 +77,7 @@ Tensor Remap(Tensor &src, Tensor &map, NVCVInterpolationType srcInterp, NVCVInte if (srcShape.rank() != mapShape.rank()) { - throw std::runtime_error("Input src and map tensors must have the same rank"); + throw RemapError("Input src and map tensors must have the same rank"); } Shape dstShape = nvcvpy::CreateShape(srcShape); @@ -82,9 +94,21 @@ Tensor Remap(Tensor &src, Tensor &map, NVCVInterpolationType srcInterp, NVCVInte dstShape[1] = mapShape[1]; dstShape[2] = mapShape[2]; } + else if (src.layout() == nvcv::TENSOR_CHW) + { + // Planar (CHW): spatial dims follow the channel dimension; map is rank-3 (HWC). + dstShape[1] = mapShape[0]; + dstShape[2] = mapShape[1]; + } + else if (src.layout() == nvcv::TENSOR_NCHW) + { + // Planar (NCHW): spatial dims are the last two; map is rank-4 (NHWC). + dstShape[2] = mapShape[1]; + dstShape[3] = mapShape[2]; + } else { - throw std::runtime_error("Input src tensor must have either HWC or NHWC layout"); + throw RemapError("Input src tensor must have HWC, NHWC, CHW, or NCHW layout"); } } @@ -114,8 +138,12 @@ ImageBatchVarShape VarShapeRemapInto(ImageBatchVarShape &dst, ImageBatchVarShape guard.add(LockMode::LOCK_MODE_WRITE, {dst}); guard.add(LockMode::LOCK_MODE_NONE, {*op}); - op->submit(pstream->cudaHandle(), src, dst, map, srcInterp, mapInterp, mapValueType, alignCorners, borderMode, - bValue); + guard.run( + [&op, &pstream, &src, &dst, &map, &srcInterp, &mapInterp, &mapValueType, &alignCorners, &borderMode, &bValue]() + { + op->submit(pstream->cudaHandle(), src, dst, map, srcInterp, mapInterp, mapValueType, alignCorners, + borderMode, bValue); + }); return std::move(dst); } @@ -133,7 +161,7 @@ ImageBatchVarShape VarShapeRemap(ImageBatchVarShape &src, Tensor &map, NVCVInter auto mapAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(map.exportData()); if (!mapAccess) { - throw std::runtime_error("Incompatible map tensor layout"); + throw RemapError("Incompatible map tensor layout"); } mapSize.w = mapAccess->numCols(); @@ -144,11 +172,11 @@ ImageBatchVarShape VarShapeRemap(ImageBatchVarShape &src, Tensor &map, NVCVInter { if (mapValueType == NVCV_REMAP_ABSOLUTE || mapValueType == NVCV_REMAP_ABSOLUTE_NORMALIZED) { - dst.pushBack(Image::Create(mapSize, src[i].format())); + dst.pushBackImage(Image::Create(mapSize, src[i].format())); } else { - dst.pushBack(Image::Create(src[i].size(), src[i].format())); + dst.pushBackImage(Image::Create(src[i].size(), src[i].format())); } } @@ -162,17 +190,11 @@ void ExportOpRemap(py::module &m) { using namespace pybind11::literals; - m.def("remap", &Remap, "src"_a, "map"_a, "src_interp"_a = NVCV_INTERP_NEAREST, "map_interp"_a = NVCV_INTERP_NEAREST, - "map_type"_a = NVCV_REMAP_ABSOLUTE, "align_corners"_a = false, "border"_a = NVCV_BORDER_CONSTANT, - "border_value"_a = pyarray{}, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - - cvcuda.remap(src: cvcuda.Tensor, map: cvcuda.Tensor, src_interp: cvcuda.Interp = cvcuda.Interp.NEAREST, map_interp: cvcuda.Interp = cvcuda.Interp.NEAREST, map_type: cvcuda.Remap = cvcuda.Remap.ABSOLUTE, align_corners: bool = False, border: cvcuda.Border = cvcuda.Border.CONSTANT, border_value: numpy.ndarray = np.ndarray((0,)), stream: Optional[cvcuda.Stream] = None) -> cvcuda.Tensor - + m.def("remap", NvtxTrace("cvcuda.remap", &Remap), "src"_a, "map"_a, "src_interp"_a = NVCV_INTERP_NEAREST, + "map_interp"_a = NVCV_INTERP_NEAREST, "map_type"_a = NVCV_REMAP_ABSOLUTE, "align_corners"_a = false, + "border"_a = NVCV_BORDER_CONSTANT, "border_value"_a = pyarray{}, py::kw_only(), "stream"_a = nullptr, R"pbdoc( Executes the Warp Perspective operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Remap operator for more details and usage - examples. Args: src (cvcuda.Tensor): Input tensor. @@ -203,21 +225,13 @@ void ExportOpRemap(py::module &m) Returns: cvcuda.Tensor: The output tensor. - Caution: - Restrictions to several arguments may apply. Check the C API references of the CV-CUDA - operator. )pbdoc"); - m.def("remap_into", &RemapInto, "dst"_a, "src"_a, "map"_a, "src_interp"_a = NVCV_INTERP_NEAREST, - "map_interp"_a = NVCV_INTERP_NEAREST, "map_type"_a = NVCV_REMAP_ABSOLUTE, "align_corners"_a = false, - "border"_a = NVCV_BORDER_CONSTANT, "border_value"_a = pyarray{}, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - - cvcuda.remap_into(dst: cvcuda.Tensor, src: cvcuda.Tensor, map: cvcuda.Tensor, src_interp: cvcuda.Interp = cvcuda.Interp.NEAREST, map_interp: cvcuda.Interp = cvcuda.Interp.NEAREST, map_type: cvcuda.Remap = cvcuda.Remap.ABSOLUTE, align_corners: bool = False, border: cvcuda.Border = cvcuda.Border.CONSTANT, border_value: numpy.ndarray = np.ndarray((0,)), stream: Optional[cvcuda.Stream] = None) - + m.def("remap_into", NvtxTrace("cvcuda.remap_into", &RemapInto), "dst"_a, "src"_a, "map"_a, + "src_interp"_a = NVCV_INTERP_NEAREST, "map_interp"_a = NVCV_INTERP_NEAREST, + "map_type"_a = NVCV_REMAP_ABSOLUTE, "align_corners"_a = false, "border"_a = NVCV_BORDER_CONSTANT, + "border_value"_a = pyarray{}, py::kw_only(), "stream"_a = nullptr, R"pbdoc( Executes the Warp Perspective operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Remap operator for more details and usage - examples. Args: dst (cvcuda.Tensor): Output tensor. @@ -247,23 +261,13 @@ void ExportOpRemap(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C API references of the CV-CUDA - operator. + cvcuda.Tensor: The output tensor (same as dst). )pbdoc"); - m.def("remap", &VarShapeRemap, "src"_a, "map"_a, "src_interp"_a = NVCV_INTERP_NEAREST, + m.def("remap", NvtxTrace("cvcuda.remap", &VarShapeRemap), "src"_a, "map"_a, "src_interp"_a = NVCV_INTERP_NEAREST, "map_interp"_a = NVCV_INTERP_NEAREST, "map_type"_a = NVCV_REMAP_ABSOLUTE, "align_corners"_a = false, "border"_a = NVCV_BORDER_CONSTANT, "border_value"_a = pyarray{}, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - - cvcuda.remap(src: cvcuda.ImageBatchVarShape, map: cvcuda.Tensor, src_interp: cvcuda.Interp = cvcuda.Interp.NEAREST, map_interp: cvcuda.Interp = cvcuda.Interp.NEAREST, map_type: cvcuda.Remap = cvcuda.Remap.ABSOLUTE, align_corners: bool = False, border: cvcuda.Border = cvcuda.Border.CONSTANT, border_value: numpy.ndarray = np.ndarray((0,)), stream: Optional[cvcuda.Stream] = None) -> cvcuda.ImageBatchVarShape - Executes the Warp Perspective operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Remap operator for more details and usage - examples. Args: src (cvcuda.ImageBatchVarShape): Input image batch. @@ -294,21 +298,13 @@ void ExportOpRemap(py::module &m) Returns: cvcuda.ImageBatchVarShape: The output image batch. - Caution: - Restrictions to several arguments may apply. Check the C API references of the CV-CUDA - operator. )pbdoc"); - m.def("remap_into", &VarShapeRemapInto, "dst"_a, "src"_a, "map"_a, "src_interp"_a = NVCV_INTERP_NEAREST, - "map_interp"_a = NVCV_INTERP_NEAREST, "map_type"_a = NVCV_REMAP_ABSOLUTE, "align_corners"_a = false, - "border"_a = NVCV_BORDER_CONSTANT, "border_value"_a = pyarray{}, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - - cvcuda.remap_into(dst: cvcuda.ImageBatchVarShape, src: cvcuda.ImageBatchVarShape, map: cvcuda.Tensor, src_interp: cvcuda.Interp = cvcuda.Interp.NEAREST, map_interp: cvcuda.Interp = cvcuda.Interp.NEAREST, map_type: cvcuda.Remap = cvcuda.Remap.ABSOLUTE, align_corners: bool = False, border: cvcuda.Border = cvcuda.Border.CONSTANT, border_value: numpy.ndarray = np.ndarray((0,)), stream: Optional[cvcuda.Stream] = None) - + m.def("remap_into", NvtxTrace("cvcuda.remap_into", &VarShapeRemapInto), "dst"_a, "src"_a, "map"_a, + "src_interp"_a = NVCV_INTERP_NEAREST, "map_interp"_a = NVCV_INTERP_NEAREST, + "map_type"_a = NVCV_REMAP_ABSOLUTE, "align_corners"_a = false, "border"_a = NVCV_BORDER_CONSTANT, + "border_value"_a = pyarray{}, py::kw_only(), "stream"_a = nullptr, R"pbdoc( Executes the Warp Perspective operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Remap operator for more details and usage - examples. Args: dst (cvcuda.ImageBatchVarShape): Output image batch. @@ -338,11 +334,7 @@ void ExportOpRemap(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C API references of the CV-CUDA - operator. + cvcuda.ImageBatchVarShape: The output image batch (same as dst). )pbdoc"); } diff --git a/python/mod_cvcuda/operators/OpResize.cpp b/python/mod_cvcuda/operators/OpResize.cpp index cc9c1c6bf..f44f529b4 100644 --- a/python/mod_cvcuda/operators/OpResize.cpp +++ b/python/mod_cvcuda/operators/OpResize.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,6 +16,7 @@ */ #include "Operators.hpp" +#include "VarShapeUtils.hpp" #include #include @@ -26,9 +27,18 @@ #include #include +#include + namespace cvcudapy { namespace { + +class ResizeError : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + Tensor ResizeInto(Tensor &output, Tensor &input, NVCVInterpolationType interp, std::optional pstream) { if (!pstream) @@ -43,7 +53,8 @@ Tensor ResizeInto(Tensor &output, Tensor &input, NVCVInterpolationType interp, s guard.add(LockMode::LOCK_MODE_WRITE, {output}); guard.add(LockMode::LOCK_MODE_NONE, {*resize}); - resize->submit(pstream->cudaHandle(), input, output, interp); + guard.run([&resize, &pstream, &input, &output, &interp]() + { resize->submit(pstream->cudaHandle(), input, output, interp); }); return std::move(output); } @@ -70,7 +81,8 @@ ImageBatchVarShape ResizeVarShapeInto(ImageBatchVarShape &output, ImageBatchVarS guard.add(LockMode::LOCK_MODE_WRITE, {output}); guard.add(LockMode::LOCK_MODE_NONE, {*resize}); - resize->submit(pstream->cudaHandle(), input, output, interp); + guard.run([&resize, &pstream, &input, &output, &interp]() + { resize->submit(pstream->cudaHandle(), input, output, interp); }); return output; } @@ -80,18 +92,10 @@ ImageBatchVarShape ResizeVarShape(ImageBatchVarShape &input, const std::vector(size), std::get<1>(size)}, format); - output.pushBack(image); - } + ImageBatchVarShape output = CreateSizedImageBatch(input, out_size); return ResizeVarShapeInto(output, input, interp, pstream); } @@ -102,19 +106,11 @@ void ExportOpResize(py::module &m) { using namespace pybind11::literals; - py::options options; - options.disable_function_signatures(); - - m.def("resize", &Resize, "src"_a, "shape"_a, "interp"_a = NVCV_INTERP_LINEAR, py::kw_only(), "stream"_a = nullptr, + m.def("resize", NvtxTrace("cvcuda.resize", &Resize), "src"_a, "shape"_a, "interp"_a = NVCV_INTERP_LINEAR, + py::kw_only(), "stream"_a = nullptr, R"pbdoc( - - cvcuda.resize(src: cvcuda.Tensor, shape: Tuple[int], interp: cvcuda.Interp = cvcuda.Interp.LINEAR, stream: Optional[cvcuda.Stream] = None) -> cvcuda.Tensor - Executes the Resize operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Resize operator - for more details and usage examples. Args: src (cvcuda.Tensor): Input tensor containing one or more images. @@ -125,21 +121,12 @@ void ExportOpResize(py::module &m) Returns: cvcuda.Tensor: The output tensor. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("resize_into", &ResizeInto, "dst"_a, "src"_a, "interp"_a = NVCV_INTERP_LINEAR, py::kw_only(), - "stream"_a = nullptr, R"pbdoc( - - cvcuda.resize_into(dst: cvcuda.Tensor, src: cvcuda.Tensor, interp: cvcuda.Interp = cvcuda.Interp.LINEAR, stream: Optional[cvcuda.Stream] = None) - + m.def("resize_into", NvtxTrace("cvcuda.resize_into", &ResizeInto), "dst"_a, "src"_a, + "interp"_a = NVCV_INTERP_LINEAR, py::kw_only(), "stream"_a = nullptr, R"pbdoc( Executes the Resize operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Resize operator - for more details and usage examples. Args: dst (cvcuda.Tensor): Output tensor to store the result of the operation. @@ -148,23 +135,13 @@ void ExportOpResize(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.Tensor: The output tensor (same as dst). )pbdoc"); - m.def("resize", &ResizeVarShape, "src"_a, "sizes"_a, "interp"_a = NVCV_INTERP_LINEAR, py::kw_only(), - "stream"_a = nullptr, R"pbdoc( - - cvcuda.resize(src: cvcuda.ImageBatchVarShape, interp: cvcuda.Interp = cvcuda.Interp.LINEAR, stream: Optional[cvcuda.Stream] = None) -> cvcuda.ImageBatchVarShape - + m.def("resize", NvtxTrace("cvcuda.resize", &ResizeVarShape), "src"_a, "sizes"_a, "interp"_a = NVCV_INTERP_LINEAR, + py::kw_only(), "stream"_a = nullptr, R"pbdoc( Executes the Resize operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Resize operator - for more details and usage examples. Args: src (cvcuda.ImageBatchVarShape): Input image batch containing one or more images. @@ -175,21 +152,12 @@ void ExportOpResize(py::module &m) Returns: cvcuda.ImageBatchVarShape: The output image batch. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("resize_into", &ResizeVarShapeInto, "dst"_a, "src"_a, "interp"_a = NVCV_INTERP_LINEAR, py::kw_only(), - "stream"_a = nullptr, R"pbdoc( - - cvcuda.resize_into(dst: cvcuda.ImageBatchVarShape, src: cvcuda.ImageBatchVarShape, interp: cvcuda.Interp = cvcuda.Interp.LINEAR, stream: Optional[cvcuda.Stream] = None) - + m.def("resize_into", NvtxTrace("cvcuda.resize_into", &ResizeVarShapeInto), "dst"_a, "src"_a, + "interp"_a = NVCV_INTERP_LINEAR, py::kw_only(), "stream"_a = nullptr, R"pbdoc( Executes the Resize operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Resize operator - for more details and usage examples. Args: src (cvcuda.ImageBatchVarShape): Input image batch containing one or more images. @@ -198,11 +166,7 @@ void ExportOpResize(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.ImageBatchVarShape: The output image batch (same as dst). )pbdoc"); } diff --git a/python/mod_cvcuda/operators/OpResizeCropConvertReformat.cpp b/python/mod_cvcuda/operators/OpResizeCropConvertReformat.cpp index 8d94742cc..071145e17 100644 --- a/python/mod_cvcuda/operators/OpResizeCropConvertReformat.cpp +++ b/python/mod_cvcuda/operators/OpResizeCropConvertReformat.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -50,7 +50,9 @@ Tensor ResizeCropConvertReformatInto(Tensor &dst, Tensor &src, const std::tuple< nvcv::Size2D size_wh{std::get<0>(resizeDim), std::get<1>(resizeDim)}; int2 crop_xy{std::get<0>(cropPos), std::get<1>(cropPos)}; - resize->submit(pstream->cudaHandle(), src, dst, size_wh, interp, crop_xy, manip, scale, offset, srcCast); + guard.run( + [&resize, &pstream, &src, &dst, &size_wh, &interp, &crop_xy, &manip, &scale, &offset, &srcCast]() + { resize->submit(pstream->cudaHandle(), src, dst, size_wh, interp, crop_xy, manip, scale, offset, srcCast); }); return std::move(dst); } @@ -62,10 +64,11 @@ Tensor ResizeCropConvertReformat(Tensor &src, const std::tuple resizeD { nvcv::TensorLayout srcLayout = src.layout(); - if (srcLayout != NVCV_TENSOR_HWC && srcLayout != NVCV_TENSOR_NHWC) + if (srcLayout != NVCV_TENSOR_HWC && srcLayout != NVCV_TENSOR_NHWC && srcLayout != NVCV_TENSOR_CHW + && srcLayout != NVCV_TENSOR_NCHW) { throw nvcv::Exception(nvcv::Status::ERROR_INVALID_IMAGE_FORMAT, - "Input tensor must have layout 'HWC' or 'NHWC'."); + "Input tensor must have layout 'HWC', 'NHWC', 'CHW', or 'NCHW'."); } nvcv::TensorLayout dstLayout = (layout && *layout ? nvcv::TensorLayout(layout) : nvcv::TensorLayout("")); @@ -87,18 +90,18 @@ Tensor ResizeCropConvertReformat(Tensor &src, const std::tuple resizeD "Output tensor must have layout 'HWC', 'NHWC', 'CHW', or 'NCHW'."); } - nvcv::TensorShape srcShape = Permute(src.shape(), NVCV_TENSOR_NHWC); + nvcv::TensorShape srcShape = Permute(src.shape(), nvcv::TensorLayout{NVCV_TENSOR_NHWC}); nvcv::TensorShape::ShapeType shape = srcShape.shape(); shape[2] = cropRect.width; shape[1] = cropRect.height; - nvcv::TensorShape dstShape = Permute(nvcv::TensorShape(shape, NVCV_TENSOR_NHWC), dstLayout); + nvcv::TensorShape dstShape = Permute(nvcv::TensorShape(shape, nvcv::TensorLayout{NVCV_TENSOR_NHWC}), dstLayout); Tensor dst = Tensor::Create(dstShape, dataType); - const std::tuple cropPos = std::make_tuple((int)cropRect.x, (int)cropRect.y); + const std::tuple cropPos = std::make_tuple(cropRect.x, cropRect.y); return ResizeCropConvertReformatInto(dst, src, resizeDim, interp, cropPos, manip, scale, offset, srcCast, pstream); } @@ -123,7 +126,9 @@ Tensor ResizeCropConvertReformatVarShapeInto(Tensor &dst, ImageBatchVarShape &sr nvcv::Size2D size_wh(std::get<0>(resizeDim), std::get<1>(resizeDim)); int2 crop_xy{std::get<0>(cropPos), std::get<1>(cropPos)}; - resize->submit(pstream->cudaHandle(), src, dst, size_wh, interp, crop_xy, manip, scale, offset, srcCast); + guard.run( + [&resize, &pstream, &src, &dst, &size_wh, &interp, &crop_xy, &manip, &scale, &offset, &srcCast]() + { resize->submit(pstream->cudaHandle(), src, dst, size_wh, interp, crop_xy, manip, scale, offset, srcCast); }); return std::move(dst); } @@ -144,41 +149,39 @@ Tensor ResizeCropConvertReformatVarShape(ImageBatchVarShape &src, const std::tup int channels = srcFrmt.numChannels(); int images = src.numImages(); - if (channels != 3) + if (channels != 1 && channels != 3) { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input must have 3 channels."); + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input must have 1 or 3 channels."); } - if (srcFrmt != nvcv::FMT_RGB8 && srcFrmt != nvcv::FMT_BGR8) + if (srcFrmt != nvcv::FMT_RGB8 && srcFrmt != nvcv::FMT_BGR8 && srcFrmt != nvcv::FMT_RGB8p + && srcFrmt != nvcv::FMT_BGR8p && srcFrmt != nvcv::FMT_Y8 && srcFrmt != nvcv::FMT_U8) { throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, - "Input must have three interleaved, 8-bit channels in RGB or BGR format."); + "Input must have 8-bit channels in RGB, BGR, Y8, or U8 format."); } nvcv::TensorShape shape; if (dstLayout.rank() == 0) { - if (srcFrmt == nvcv::FMT_RGB8 || srcFrmt == nvcv::FMT_BGR8) - { - shape = nvcv::TensorShape{ - {images, cropRect.height, cropRect.width, channels}, - NVCV_TENSOR_NHWC - }; - } + shape = nvcv::TensorShape{ + { images, cropRect.height, cropRect.width, channels}, + nvcv::TensorLayout{NVCV_TENSOR_NHWC } + }; } else { if (dstLayout == NVCV_TENSOR_NHWC || dstLayout == NVCV_TENSOR_HWC || dstLayout == NVCV_TENSOR_NHW || dstLayout == NVCV_TENSOR_HW) shape = nvcv::TensorShape{ - {images, cropRect.height, cropRect.width, channels}, - NVCV_TENSOR_NHWC + { images, cropRect.height, cropRect.width, channels}, + nvcv::TensorLayout{NVCV_TENSOR_NHWC } }; else if (dstLayout == NVCV_TENSOR_NCHW || dstLayout == NVCV_TENSOR_CHW) shape = nvcv::TensorShape{ - {images, channels, cropRect.height, cropRect.width}, - NVCV_TENSOR_NCHW + { images, channels, cropRect.height, cropRect.width}, + nvcv::TensorLayout{NVCV_TENSOR_NCHW } }; else { @@ -189,7 +192,7 @@ Tensor ResizeCropConvertReformatVarShape(ImageBatchVarShape &src, const std::tup Tensor dst = Tensor::Create(shape, dataType); - const std::tuple cropPos = std::make_tuple((int)cropRect.x, (int)cropRect.y); + const std::tuple cropPos = std::make_tuple(cropRect.x, cropRect.y); return ResizeCropConvertReformatVarShapeInto(dst, src, resizeDim, interp, cropPos, manip, scale, offset, srcCast, pstream); @@ -201,39 +204,20 @@ void ExportOpResizeCropConvertReformat(py::module &m) { using namespace pybind11::literals; - py::options options; - options.disable_function_signatures(); - - m.def("resize_crop_convert_reformat", &ResizeCropConvertReformat, "src"_a, "resize_dim"_a, "interp"_a, - "crop_rect"_a, py::kw_only(), "layout"_a = "", "data_type"_a = NVCV_DATA_TYPE_NONE, - "manip"_a = NVCV_CHANNEL_NO_OP, "scale"_a = 1.0, "offset"_a = 0.0, "srcCast"_a = true, "stream"_a = nullptr, + m.def("resize_crop_convert_reformat", NvtxTrace("cvcuda.resize_crop_convert_reformat", &ResizeCropConvertReformat), + "src"_a, "resize_dim"_a, "interp"_a, "crop_rect"_a, py::kw_only(), "layout"_a = "", + "data_type"_a = NVCV_DATA_TYPE_NONE, "manip"_a = NVCV_CHANNEL_NO_OP, "scale"_a = 1.0, "offset"_a = 0.0, + "srcCast"_a = true, "stream"_a = nullptr, R"pbdoc( - - cvcuda.resize_crop_convert_reformat(src: cvcuda.Tensor, - resize_dim: tuple[int,int], - interp: cvcuda.Interp, - crop_rect: cvcuda.RectI, - *, - layout: str = "", - data_type: cvcuda.Type = 0, - manip: cvcuda.ChannelManip = cvcuda.ChannelManip.NO_OP, - scale: float = 1.0, - offset: float = 0.0, - srcCast: bool = True, - stream: Optional[cvcuda.Stream] = None) -> cvcuda.Tensor - Executes the ResizeCropConvertReformat operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the ResizeCropConvertReformat operator - for more details and usage examples. Args: src (cvcuda.Tensor): Input tensor containing one or more images. resize_dim (tuple[int,int]): Dimensions, width & height, of resized tensor (prior to cropping). interp (cvcuda.Interp): Interpolation type used for resizing. Currently, only cvcuda.Interp.NEAREST and cvcuda.Interp.LINEAR are available. - crop_rect (cvcuda.RectI): Crop rectangle, (top, left, width, height), specifying the top-left corner and + crop_rect (cvcuda.RectI): Crop rectangle, (left, top, width, height), specifying the top-left corner and width & height dimensions of the region to crop from the resized images. layout(string, optional): String specifying output tensor layout (e.g., 'NHWC' or 'CHW'). Empty string (default) indicates output tensor layout copies input. @@ -255,32 +239,14 @@ void ExportOpResizeCropConvertReformat(py::module &m) Returns: cvcuda.Tensor: The output tensor. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("resize_crop_convert_reformat_into", &ResizeCropConvertReformatInto, "dst"_a, "src"_a, "resize_dim"_a, - "interp"_a, "cropPos"_a, py::kw_only(), "manip"_a = NVCV_CHANNEL_NO_OP, "scale"_a = 1.0, "offset"_a = 0.0, - "srcCast"_a = true, "stream"_a = nullptr, R"pbdoc( - - cvcuda.resize_crop_convert_reformat_into(dst: cvcuda.Tensor, - src: cvcuda.Tensor, - resize_dim: tuple[int,int], - interp: cvcuda.Interp, - cropPos: tuple[int,int], - *, - manip: cvcuda.ChannelManip = cvcuda.ChannelManip.NO_OP, - scale: float = 1.0, - offset: float = 0.0, - srcCast: bool = True, - stream: Optional[cvcuda.Stream] = None) - + m.def("resize_crop_convert_reformat_into", + NvtxTrace("cvcuda.resize_crop_convert_reformat_into", &ResizeCropConvertReformatInto), "dst"_a, "src"_a, + "resize_dim"_a, "interp"_a, "cropPos"_a, py::kw_only(), "manip"_a = NVCV_CHANNEL_NO_OP, "scale"_a = 1.0, + "offset"_a = 0.0, "srcCast"_a = true, "stream"_a = nullptr, R"pbdoc( Executes the ResizeCropConvertReformat operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the ResizeCropConvertReformat operator - for more details and usage examples. Args: dst (cvcuda.Tensor): Output tensor to store the result of the operation. Output tensor also specifies the @@ -290,7 +256,7 @@ void ExportOpResizeCropConvertReformat(py::module &m) resize_dim (tuple[int,int]): Dimensions, width & height, of resized tensor (prior to cropping). interp (cvcuda.Interp): Interpolation type used for resizing. Currently, only cvcuda.Interp.NEAREST and cvcuda.Interp.LINEAR are available. - cropPos (tuple[int,int]): Crop position, (top, left), specifying the top-left corner of the region to crop + cropPos (tuple[int,int]): Crop position, (x, y), specifying the top-left corner of the region to crop from the resized images. The crop region's width and height is specified by the output tensor's width & height. manip(cvcuda.ChannelManip, optional): Channel manipulation (e.g., shuffle RGB to BGR). NO_OP (default) @@ -307,36 +273,16 @@ void ExportOpResizeCropConvertReformat(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.Tensor: The output tensor (same as dst). )pbdoc"); - m.def("resize_crop_convert_reformat", &ResizeCropConvertReformatVarShape, "src"_a, "resize_dim"_a, "interp"_a, - "crop_rect"_a, py::kw_only(), "layout"_a = "", "data_type"_a = NVCV_DATA_TYPE_NONE, + m.def("resize_crop_convert_reformat", + NvtxTrace("cvcuda.resize_crop_convert_reformat", &ResizeCropConvertReformatVarShape), "src"_a, "resize_dim"_a, + "interp"_a, "crop_rect"_a, py::kw_only(), "layout"_a = "", "data_type"_a = NVCV_DATA_TYPE_NONE, "manip"_a = NVCV_CHANNEL_NO_OP, "scale"_a = 1.0, "offset"_a = 0.0, "srcCast"_a = true, "stream"_a = nullptr, R"pbdoc( - - cvcuda.resizeCropConvertReformat(src: cvcuda.ImageBatchVarShape, - resize_dim: tuple[int,int], - interp: cvcuda.Interp, - crop_rect: cvcuda.RectI, - *, - layout: str = "", - data_type: cvcuda.Type = 0, - manip: cvcuda.ChannelManip = cvcuda.ChannelManip.NO_OP, - scale: float = 1.0, - offset: float = 0.0, - srcCast: bool = True, - stream: Optional[cvcuda.Stream] = None) -> cvcuda.Tensor - Executes the ResizeCropConvertReformat operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the ResizeCropConvertReformat operator - for more details and usage examples. Args: src (cvcuda.ImageBatchVarShape): Input image batch containing one or more images of varying sizes, but all images @@ -344,7 +290,7 @@ void ExportOpResizeCropConvertReformat(py::module &m) resize_dim (tuple[int,int]): Dimensions, width & height, of resized tensor (prior to cropping). interp (cvcuda.Interp): Interpolation type used for resizing. Currently, only cvcuda.Interp.NEAREST and cvcuda.Interp.LINEAR are available. - crop_rect (cvcuda.RectI): Crop rectangle, (top, left, width, height), specifying the top-left corner and + crop_rect (cvcuda.RectI): Crop rectangle, (left, top, width, height), specifying the top-left corner and width & height dimensions of the region to crop from the resized images. layout(string, optional): String specifying output tensor layout (e.g., 'NHWC' or 'CHW'). Empty string (default) indicates output tensor layout copies input. @@ -366,32 +312,14 @@ void ExportOpResizeCropConvertReformat(py::module &m) Returns: cvcuda.Tensor: The output tensor. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("resize_crop_convert_reformat_into", &ResizeCropConvertReformatVarShapeInto, "dst"_a, "src"_a, "resize_dim"_a, - "interp"_a, "cropPos"_a, py::kw_only(), "manip"_a = NVCV_CHANNEL_NO_OP, "scale"_a = 1.0, "offset"_a = 0.0, - "srcCast"_a = true, "stream"_a = nullptr, R"pbdoc( - - cvcuda.resize_crop_convert_reformat_into(dst: cvcuda.Tensor, - src: cvcuda.ImageBatchVarShape, - resize_dim: tuple[int,int], - interp: cvcuda.Interp, - cropPos: tuple[int,int], - *, - manip: cvcuda.ChannelManip = cvcuda.ChannelManip.NO_OP, - scale: float = 1.0, - offset: float = 0.0, - srcCast: bool = True, - stream: Optional[cvcuda.Stream] = None) - + m.def("resize_crop_convert_reformat_into", + NvtxTrace("cvcuda.resize_crop_convert_reformat_into", &ResizeCropConvertReformatVarShapeInto), "dst"_a, + "src"_a, "resize_dim"_a, "interp"_a, "cropPos"_a, py::kw_only(), "manip"_a = NVCV_CHANNEL_NO_OP, + "scale"_a = 1.0, "offset"_a = 0.0, "srcCast"_a = true, "stream"_a = nullptr, R"pbdoc( Executes the ResizeCropConvertReformat operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the ResizeCropConvertReformat operator - for more details and usage examples. Args: dst (cvcuda.Tensor): Output tensor to store the result of the operation. Output tensor also specifies the @@ -402,7 +330,7 @@ void ExportOpResizeCropConvertReformat(py::module &m) resize_dim (tuple[int,int]): Dimensions, width & height, of resized tensor (prior to cropping). interp (cvcuda.Interp): Interpolation type used for resizing. Currently, only cvcuda.Interp.NEAREST and cvcuda.Interp.LINEAR are available. - cropPos (tuple[int,int]): Crop position, (top, left), specifying the top-left corner of the region to + cropPos (tuple[int,int]): Crop position, (x, y), specifying the top-left corner of the region to crop from the resized images. The crop region's width and height is specified by the output tensor's width & height. manip(cvcuda.ChannelManip, optional): Channel manipulation (e.g., shuffle RGB to BGR). NO_OP (default) @@ -419,11 +347,7 @@ void ExportOpResizeCropConvertReformat(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.Tensor: The output tensor (same as dst). )pbdoc"); } diff --git a/python/mod_cvcuda/operators/OpRotate.cpp b/python/mod_cvcuda/operators/OpRotate.cpp index 369a1f583..5ca4b0d0d 100644 --- a/python/mod_cvcuda/operators/OpRotate.cpp +++ b/python/mod_cvcuda/operators/OpRotate.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,6 +16,7 @@ */ #include "Operators.hpp" +#include "VarShapeUtils.hpp" #include #include @@ -48,7 +49,8 @@ Tensor RotateInto(Tensor &output, Tensor &input, double angleDeg, const std::tup double2 shiftArg{std::get<0>(shift), std::get<1>(shift)}; - rotate->submit(pstream->cudaHandle(), input, output, angleDeg, shiftArg, interpolation); + guard.run([&rotate, &pstream, &input, &output, &angleDeg, &shiftArg, &interpolation]() + { rotate->submit(pstream->cudaHandle(), input, output, angleDeg, shiftArg, interpolation); }); return output; } @@ -76,7 +78,8 @@ ImageBatchVarShape VarShapeRotateInto(ImageBatchVarShape &output, ImageBatchVarS guard.add(LockMode::LOCK_MODE_WRITE, {output}); guard.add(LockMode::LOCK_MODE_READWRITE, {*rotate}); - rotate->submit(pstream->cudaHandle(), input, output, angleDeg, shift, interpolation); + guard.run([&rotate, &pstream, &input, &output, &angleDeg, &shift, &interpolation]() + { rotate->submit(pstream->cudaHandle(), input, output, angleDeg, shift, interpolation); }); return output; } @@ -84,15 +87,7 @@ ImageBatchVarShape VarShapeRotateInto(ImageBatchVarShape &output, ImageBatchVarS ImageBatchVarShape VarShapeRotate(ImageBatchVarShape &input, Tensor &angleDeg, Tensor &shift, NVCVInterpolationType interpolation, std::optional pstream) { - ImageBatchVarShape output = ImageBatchVarShape::Create(input.capacity()); - - for (int i = 0; i < input.numImages(); ++i) - { - nvcv::ImageFormat format = input[i].format(); - nvcv::Size2D size = input[i].size(); - auto image = Image::Create(size, format); - output.pushBack(image); - } + ImageBatchVarShape output = CreateSameShapeImageBatch(input); return VarShapeRotateInto(output, input, angleDeg, shift, interpolation, pstream); } @@ -103,19 +98,11 @@ void ExportOpRotate(py::module &m) { using namespace pybind11::literals; - py::options options; - options.disable_function_signatures(); - - m.def("rotate", &Rotate, "src"_a, "angle_deg"_a, "shift"_a, "interpolation"_a, py::kw_only(), "stream"_a = nullptr, + m.def("rotate", NvtxTrace("cvcuda.rotate", &Rotate), "src"_a, "angle_deg"_a, "shift"_a, "interpolation"_a, + py::kw_only(), "stream"_a = nullptr, R"pbdoc( - - cvcuda.rotate(src: cvcuda.Tensor, angle_deg: float, shift: Tuple[float, float], interpolation: cvcuda.Interp, stream: Optional[cvcuda.Stream] = None) -> cvcuda.Tensor - Executes the Rotate operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Rotate operator - for more details and usage examples. Args: src (cvcuda.Tensor): Input tensor containing one or more images. @@ -127,21 +114,12 @@ void ExportOpRotate(py::module &m) Returns: cvcuda.Tensor: The output tensor. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("rotate_into", &RotateInto, "dst"_a, "src"_a, "angle_deg"_a, "shift"_a, "interpolation"_a, py::kw_only(), - "stream"_a = nullptr, R"pbdoc( - - cvcuda.rotate_into(dst: cvcuda.Tensor, src: cvcuda.Tensor, angle_deg: float, shift: Tuple[float, float], interpolation: cvcuda.Interp, stream: Optional[cvcuda.Stream] = None) - + m.def("rotate_into", NvtxTrace("cvcuda.rotate_into", &RotateInto), "dst"_a, "src"_a, "angle_deg"_a, "shift"_a, + "interpolation"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( Executes the Rotate operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Rotate operator - for more details and usage examples. Args: dst (cvcuda.Tensor): Output tensor to store the result of the operation. @@ -152,66 +130,41 @@ void ExportOpRotate(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.Tensor: The output tensor (same as dst). )pbdoc"); - m.def("rotate", &VarShapeRotate, "src"_a, "angle_deg"_a, "shift"_a, "interpolation"_a, py::kw_only(), - "stream"_a = nullptr, R"pbdoc( - - cvcuda.rotate(src: cvcuda.ImageBatchVarShape, angle_deg: float, shift: Tuple[float, float], interpolation: cvcuda.Interp, stream: Optional[cvcuda.Stream] = None) -> cvcuda.ImageBatchVarShape - - - Executes the Rotate operation on the given cuda stream. + m.def("rotate", NvtxTrace("cvcuda.rotate", &VarShapeRotate), "src"_a, "angle_deg"_a, "shift"_a, "interpolation"_a, + py::kw_only(), "stream"_a = nullptr, R"pbdoc( + Executes the Rotate operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Rotate operator - for more details and usage examples. Args: src (cvcuda.ImageBatchVarShape): Input image batch containing one or more images. - dst (cvcuda.ImageBatchVarShape): Output image batch containing the result of the operation. - angle_deg (float): Angle used for rotation in degrees for each image. - shift (Tuple[float, float]): Value of shift in {x, y} directions to move the center at the same coord after rotation for each image. + angle_deg (cvcuda.Tensor): Rotation angle in degrees, specified per image. + shift (cvcuda.Tensor): Shift in {x, y} directions, specified per image. interpolation (cvcuda.Interp): Interpolation type used for transform. stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: cvcuda.ImageBatchVarShape: The output image batch. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("rotate_into", &VarShapeRotateInto, "dst"_a, "src"_a, "angle_deg"_a, "shift"_a, "interpolation"_a, - py::kw_only(), "stream"_a = nullptr, R"pbdoc( - - cvcuda.rotate_into(dst: cvcuda.ImageBatchVarShape, src: cvcuda.ImageBatchVarShape, angle_deg: cvcuda.Tensor, shift: cvcuda.Tensor, interpolation: cvcuda.Interp, stream: Optional[cvcuda.Stream] = None) - + m.def("rotate_into", NvtxTrace("cvcuda.rotate_into", &VarShapeRotateInto), "dst"_a, "src"_a, "angle_deg"_a, + "shift"_a, "interpolation"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( Executes the Rotate operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Rotate operator - for more details and usage examples. Args: src (cvcuda.ImageBatchVarShape): Input image batch containing one or more images. dst (cvcuda.ImageBatchVarShape): Output image batch containing the result of the operation. - angle_deg (cvcuda.Tensor): Angle used for rotation in degrees for each image. - shift (cvcuda.Tensor): Value of shift in {x, y} directions to move the center at the same coord after rotation for each image. + angle_deg (cvcuda.Tensor): Rotation angle in degrees, specified per image. + shift (cvcuda.Tensor): Shift in {x, y} directions, specified per image. interpolation (cvcuda.Interp): Interpolation type used for transform. stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.ImageBatchVarShape: The output image batch (same as dst). )pbdoc"); } diff --git a/python/mod_cvcuda/operators/OpSIFT.cpp b/python/mod_cvcuda/operators/OpSIFT.cpp index e22d9582d..011c7815e 100644 --- a/python/mod_cvcuda/operators/OpSIFT.cpp +++ b/python/mod_cvcuda/operators/OpSIFT.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -24,6 +24,7 @@ #include #include +#include #include #include #include @@ -34,7 +35,7 @@ namespace { using TupleTensor4 = std::tuple; -class PyOpSIFT : public nvcvpy::Container +class PyOpSIFT : public nvcvpy::Container // NOSONAR: operator wrappers share the Python cache hierarchy. { public: class Key : public nvcvpy::IKey @@ -65,7 +66,7 @@ class PyOpSIFT : public nvcvpy::Container bool doIsCompatible(const nvcvpy::IKey &that_) const override { - const Key &that = static_cast(that_); + const auto &that = static_cast(that_); return that.canBeUsedWith(m_maxShape, m_maxOctaveLayers); } @@ -87,7 +88,7 @@ class PyOpSIFT : public nvcvpy::Container py::object container() const override { - return *this; + return py::reinterpret_borrow(this->ptr()); } const nvcvpy::IKey &key() const override @@ -97,13 +98,15 @@ class PyOpSIFT : public nvcvpy::Container static std::shared_ptr fetch(std::vector> &cache) { - std::shared_ptr retItem = cache[0]; + assert(!cache.empty()); - long long int maxPayloadSize = 0; + // Find the operator with the largest workspace (can handle any smaller request) + std::shared_ptr retItem = cache[0]; + long long int maxPayloadSize = 0; for (const auto &item : cache) { - const Key &key = static_cast(item.get()->key()); + const auto &key = static_cast(item.get()->key()); long long int keyPayloadSize = key.payloadSize(); @@ -114,9 +117,9 @@ class PyOpSIFT : public nvcvpy::Container } } - cache.clear(); - - nvcvpy::Cache::removeAllNotInUseMatching(retItem.get()->key()); + // Note: Removed cache.clear() and removeAllNotInUseMatching() calls to reduce per-call overhead. + // The cache will naturally evict unused operators when memory pressure occurs. + // This fix matches the pattern used in OpInpaint.cpp. return retItem; } @@ -152,7 +155,7 @@ TupleTensor4 SIFTInto(Tensor &featCoords, Tensor &featMetadata, Tensor &featDesc } auto inAccess = tensorAccess(in); - int3 inShape{(int)inAccess->numCols(), (int)inAccess->numRows(), (int)inAccess->numSamples()}; + int3 inShape{inAccess->numCols(), inAccess->numRows(), static_cast(inAccess->numSamples())}; if (flags == NVCV_SIFT_USE_EXPANDED_INPUT) { inShape.x *= 2; @@ -166,8 +169,13 @@ TupleTensor4 SIFTInto(Tensor &featCoords, Tensor &featMetadata, Tensor &featDesc guard.add(LockMode::LOCK_MODE_WRITE, {featCoords, featMetadata, featDescriptors, numFeatures}); guard.add(LockMode::LOCK_MODE_READWRITE, {*op}); - op->submit(pstream->cudaHandle(), in, featCoords, featMetadata, featDescriptors, numFeatures, numOctaveLayers, - contrastThreshold, edgeThreshold, initSigma, flags); + guard.run( + [&op, &pstream, &in, &featCoords, &featMetadata, &featDescriptors, &numFeatures, &numOctaveLayers, + &contrastThreshold, &edgeThreshold, &initSigma, &flags]() + { + op->submit(pstream->cudaHandle(), in, featCoords, featMetadata, featDescriptors, numFeatures, + numOctaveLayers, contrastThreshold, edgeThreshold, initSigma, flags); + }); return TupleTensor4(std::move(featCoords), std::move(featMetadata), std::move(featDescriptors), std::move(numFeatures)); @@ -183,7 +191,7 @@ TupleTensor4 SIFT(Tensor &in, int maxFeatures, int numOctaveLayers, float contra float initSigma, NVCVSIFTFlagType flags, std::optional pstream) { auto inAccess = tensorAccess(in); - int numSamples = inAccess->numSamples(); + auto numSamples = static_cast(inAccess->numSamples()); maxFeatures = maxFeatures == 0 ? GetDefaultMaxFeatures(inAccess->numCols(), inAccess->numRows()) : maxFeatures; @@ -191,10 +199,10 @@ TupleTensor4 SIFT(Tensor &in, int maxFeatures, int numOctaveLayers, float contra // clang-format off - Tensor featCoords = Tensor::Create({{numSamples, maxFeatures, 4}, "NMC"}, nvcv::TYPE_F32, 1); - Tensor featMetadata = Tensor::Create({{numSamples, maxFeatures, 3}, "NMC"}, nvcv::TYPE_F32, 1); - Tensor featDescriptors = Tensor::Create({{numSamples, maxFeatures, 128}, "NMD"}, nvcv::TYPE_U8, 1); - Tensor numFeatures = Tensor::Create({{numSamples, 1}, "NC"}, nvcv::TYPE_S32, 1); + auto featCoords = Tensor::Create({{numSamples, maxFeatures, 4}, "NMC"}, nvcv::TYPE_F32, 1); + auto featMetadata = Tensor::Create({{numSamples, maxFeatures, 3}, "NMC"}, nvcv::TYPE_F32, 1); + auto featDescriptors = Tensor::Create({{numSamples, maxFeatures, 128}, "NMD"}, nvcv::TYPE_U8, 1); + auto numFeatures = Tensor::Create({{numSamples, 1}, "NC"}, nvcv::TYPE_S32, 1); // clang-format on @@ -208,15 +216,11 @@ void ExportOpSIFT(py::module &m) { using namespace pybind11::literals; - m.def("sift", &SIFT, "src"_a, "max_features"_a = 0, "num_octave_layers"_a = 3, "contrast_threshold"_a = 0.03f, - "edge_threshold"_a = 10.f, "init_sigma"_a = 1.6f, "flags"_a = NVCV_SIFT_USE_EXPANDED_INPUT, py::kw_only(), - "stream"_a = nullptr, R"pbdoc( - + m.def("sift", NvtxTrace("cvcuda.sift", &SIFT), "src"_a, "max_features"_a = 0, "num_octave_layers"_a = 3, + "contrast_threshold"_a = 0.03f, "edge_threshold"_a = 10.f, "init_sigma"_a = 1.6f, + "flags"_a = NVCV_SIFT_USE_EXPANDED_INPUT, py::kw_only(), "stream"_a = nullptr, R"pbdoc( Executes the SIFT operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the SIFT operator - for more details and usage examples. Args: src (cvcuda.Tensor): Input tensor to extract features and compute descriptors from. @@ -234,20 +238,14 @@ void ExportOpSIFT(py::module &m) Tuple[cvcuda.Tensor, cvcuda.Tensor, cvcuda.Tensor, cvcuda.Tensor]: A tuple with feature coordinates, metadata, descriptors and number of features. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("sift_into", &SIFTInto, "feat_coords"_a, "feat_metadata"_a, "feat_descriptors"_a, "num_features"_a, "src"_a, - "num_octave_layers"_a = 3, "contrast_threshold"_a = 0.03f, "edge_threshold"_a = 10.f, "init_sigma"_a = 1.6f, - "flags"_a = NVCV_SIFT_USE_EXPANDED_INPUT, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - + m.def("sift_into", NvtxTrace("cvcuda.sift_into", &SIFTInto), "feat_coords"_a, "feat_metadata"_a, + "feat_descriptors"_a, "num_features"_a, "src"_a, "num_octave_layers"_a = 3, "contrast_threshold"_a = 0.03f, + "edge_threshold"_a = 10.f, "init_sigma"_a = 1.6f, "flags"_a = NVCV_SIFT_USE_EXPANDED_INPUT, py::kw_only(), + "stream"_a = nullptr, R"pbdoc( Executes the SIFT operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the SIFT operator - for more details and usage examples. Args: feat_coords (cvcuda.Tensor): Output tensor with feature coordinates. @@ -267,9 +265,6 @@ void ExportOpSIFT(py::module &m) Tuple[cvcuda.Tensor, cvcuda.Tensor, cvcuda.Tensor, cvcuda.Tensor]: A tuple with feature coordinates, metadata, descriptors and number of features. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); } diff --git a/python/mod_cvcuda/operators/OpSolarize.cpp b/python/mod_cvcuda/operators/OpSolarize.cpp new file mode 100644 index 000000000..ebbb243b4 --- /dev/null +++ b/python/mod_cvcuda/operators/OpSolarize.cpp @@ -0,0 +1,143 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "UnaryElementwiseOp.hpp" + +#include +#include + +namespace cvcudapy { + +namespace { +// Thin Python-facing wrappers; the create/guard/submit body lives in UnaryElementwiseOp.hpp, with +// the threshold forwarded as the trailing submit() parameter. +Tensor SolarizeInto(Tensor &output, Tensor &input, double threshold, std::optional pstream) +{ + return UnaryElementwiseInto(output, input, pstream, threshold); +} + +Tensor Solarize(Tensor &input, double threshold, std::optional pstream) +{ + return UnaryElementwiseTensor(input, pstream, threshold); +} + +ImageBatchVarShape SolarizeVarShapeInto(ImageBatchVarShape &output, ImageBatchVarShape &input, double threshold, + std::optional pstream) +{ + return UnaryElementwiseInto(output, input, pstream, threshold); +} + +ImageBatchVarShape SolarizeVarShape(ImageBatchVarShape &input, double threshold, std::optional pstream) +{ + return UnaryElementwiseVarShape(input, pstream, threshold); +} + +} // namespace + +void ExportOpSolarize(py::module &m) +{ + using namespace pybind11::literals; + + m.def("solarize", NvtxTrace("cvcuda.solarize", &Solarize), "src"_a, "threshold"_a, py::kw_only(), + "stream"_a = nullptr, R"pbdoc( + + Executes the Solarize operation on the given cuda stream. + + Inverts every pixel at or above ``threshold``: ``out = (in >= threshold) ? (bound - in) : in``, + where ``bound`` is the data type maximum (255 for uint8, 65535 for uint16, 1.0 for float32). + Mirrors torchvision.transforms.v2.functional.solarize. + + See also: + Refer to the CV-CUDA C API reference for the Solarize operator for more details and usage + examples. + + Args: + src (cvcuda.Tensor): Input tensor containing one or more images. + threshold (float): Pixels with value >= threshold are inverted (in the pixel value domain). + stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + cvcuda.Tensor: The output tensor (same shape, dtype, and layout as src). + + Caution: + Restrictions to several arguments may apply. Check the C API references of the CV-CUDA + operator. + )pbdoc"); + + m.def("solarize_into", NvtxTrace("cvcuda.solarize_into", &SolarizeInto), "dst"_a, "src"_a, "threshold"_a, + py::kw_only(), "stream"_a = nullptr, R"pbdoc( + + Executes the Solarize operation on the given cuda stream. + + See also: + Refer to the CV-CUDA C API reference for the Solarize operator for more details and usage + examples. + + Args: + dst (cvcuda.Tensor): Output tensor to store the result of the operation. + src (cvcuda.Tensor): Input tensor containing one or more images. + threshold (float): Pixels with value >= threshold are inverted (in the pixel value domain). + stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + cvcuda.Tensor: The output tensor (same as dst). + + Caution: + Restrictions to several arguments may apply. Check the C API references of the CV-CUDA + operator. + )pbdoc"); + + m.def("solarize", NvtxTrace("cvcuda.solarize", &SolarizeVarShape), "src"_a, "threshold"_a, py::kw_only(), + "stream"_a = nullptr, R"pbdoc( + + Executes the Solarize operation on the given cuda stream. + + Args: + src (cvcuda.ImageBatchVarShape): Input image batch containing one or more images. + threshold (float): Pixels with value >= threshold are inverted (in the pixel value domain). + stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + cvcuda.ImageBatchVarShape: The output image batch (same formats and sizes as src). + + Caution: + Restrictions to several arguments may apply. Check the C API references of the CV-CUDA + operator. + )pbdoc"); + + m.def("solarize_into", NvtxTrace("cvcuda.solarize_into", &SolarizeVarShapeInto), "dst"_a, "src"_a, "threshold"_a, + py::kw_only(), "stream"_a = nullptr, + R"pbdoc( + + Executes the Solarize operation on the given cuda stream. + + Args: + dst (cvcuda.ImageBatchVarShape): Output image batch to store the result of the operation. + src (cvcuda.ImageBatchVarShape): Input image batch containing one or more images. + threshold (float): Pixels with value >= threshold are inverted (in the pixel value domain). + stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + cvcuda.ImageBatchVarShape: The output image batch (same as dst). + + Caution: + Restrictions to several arguments may apply. Check the C API references of the CV-CUDA + operator. + )pbdoc"); +} + +} // namespace cvcudapy diff --git a/python/mod_cvcuda/operators/OpStack.cpp b/python/mod_cvcuda/operators/OpStack.cpp index b0e9921db..3c631c74d 100644 --- a/python/mod_cvcuda/operators/OpStack.cpp +++ b/python/mod_cvcuda/operators/OpStack.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,30 +19,41 @@ #include #include +#include +#include #include #include #include #include +#include +#include + namespace cvcudapy { namespace { -void checkTensorList(std::vector &tensorList, int64_t (&outputShape)[4], nvcv::TensorLayout &layout, +class StackError : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + +void checkTensorList(std::vector &tensorList, std::array &outputShape, nvcv::TensorLayout &layout, nvcv::DataType &dtype) { int32_t totalTensors = 0; - if (tensorList.size() == 0) + if (tensorList.empty()) { - throw std::runtime_error("Invalid input tensor list"); + throw StackError("Invalid input tensor list"); } - for (auto &tensor : tensorList) + for (const auto &tensor : tensorList) { if (tensor.shape().rank() < 3 || tensor.shape().rank() > 4) { - throw std::runtime_error("Invalid input tensor shape"); + throw StackError("Invalid input tensor shape"); } if (tensor.shape().rank() == 4) { @@ -68,8 +79,8 @@ void checkTensorList(std::vector &tensorList, int64_t (&outputShape)[4], dtype = tensorList[0].dtype(); } -Tensor StackIntoInternal(Tensor &output, std::vector &tensorList, std::optional pstream, - int32_t numberOfTensors) +void StackIntoInternal(Tensor &output, std::vector &tensorList, std::optional pstream, + int32_t numberOfTensors) { if (!pstream) { @@ -78,9 +89,9 @@ Tensor StackIntoInternal(Tensor &output, std::vector &tensorList, std::o nvcvpy::TensorBatch inTensorBatch = nvcvpy::TensorBatch::Create(numberOfTensors); - for (auto &tensor : tensorList) + for (const auto &tensor : tensorList) { - inTensorBatch.pushBack(tensor); + inTensorBatch.pushBackTensor(tensor); } auto op = CreateOperator(); @@ -89,33 +100,32 @@ Tensor StackIntoInternal(Tensor &output, std::vector &tensorList, std::o guard.add(LockMode::LOCK_MODE_READ, {inTensorBatch}); guard.add(LockMode::LOCK_MODE_WRITE, {output}); guard.add(LockMode::LOCK_MODE_NONE, {*op}); - op->submit(pstream->cudaHandle(), inTensorBatch, output); - return std::move(output); + guard.run([&op, &pstream, &inTensorBatch, &output]() { op->submit(pstream->cudaHandle(), inTensorBatch, output); }); } Tensor StackInto(Tensor &output, std::vector &tensorList, std::optional pstream) { - int64_t outputShape[4] = {}; // NCHW/NHWC - nvcv::TensorLayout layout = nvcv::TENSOR_CHW; - nvcv::DataType dtype; + std::array outputShape = {}; // NCHW/NHWC + nvcv::TensorLayout layout = nvcv::TENSOR_CHW; + nvcv::DataType dtype; checkTensorList(tensorList, outputShape, layout, dtype); if (output.shape().layout() != nvcv::TENSOR_NCHW && output.shape().layout() != nvcv::TENSOR_NHWC) - throw std::runtime_error("Invalid output tensor shape"); + throw StackError("Invalid output tensor shape"); if (output.shape()[0] != outputShape[0]) - throw std::runtime_error("Invalid output tensor shape"); + throw StackError("Invalid output tensor shape"); - StackIntoInternal(output, tensorList, pstream, outputShape[0]); + StackIntoInternal(output, tensorList, pstream, static_cast(outputShape[0])); return std::move(output); } Tensor Stack(std::vector &tensorList, std::optional pstream) { - int64_t outputShape[4] = {}; // NCHW/NHWC - nvcv::TensorLayout layout = nvcv::TENSOR_CHW; - nvcv::DataType dtype; + std::array outputShape = {}; // NCHW/NHWC + nvcv::TensorLayout layout = nvcv::TENSOR_CHW; + nvcv::DataType dtype; checkTensorList(tensorList, outputShape, layout, dtype); //create new output tensor @@ -125,7 +135,109 @@ Tensor Stack(std::vector &tensorList, std::optional pstream) layout }, dtype); - return StackIntoInternal(output, tensorList, pstream, outputShape[0]); + StackIntoInternal(output, tensorList, pstream, static_cast(outputShape[0])); + return output; +} + +// TensorBatch direct input functions +Tensor StackTensorBatchInto(Tensor &output, nvcvpy::TensorBatch &inTensorBatch, std::optional pstream) +{ + if (!pstream) + { + pstream = Stream::Current(); + } + + auto op = CreateOperator(); + + ResourceGuard guard(*pstream); + guard.add(LockMode::LOCK_MODE_READ, {inTensorBatch}); + guard.add(LockMode::LOCK_MODE_WRITE, {output}); + guard.add(LockMode::LOCK_MODE_NONE, {*op}); + guard.run([&op, &pstream, &inTensorBatch, &output]() { op->submit(pstream->cudaHandle(), inTensorBatch, output); }); + return std::move(output); +} + +Tensor StackTensorBatch(nvcvpy::TensorBatch &inTensorBatch, std::optional pstream) +{ + if (inTensorBatch.numTensors() == 0) + { + throw StackError("Invalid input tensor batch: empty batch"); + } + + // Get info from the first tensor to determine output shape and layout + int64_t totalTensors = 0; + std::array outputShape = {}; + nvcv::TensorLayout layout = nvcv::TENSOR_NHWC; + nvcv::DataType dtype; + + for (int32_t i = 0; i < inTensorBatch.numTensors(); ++i) + { + nvcv::Tensor tensor = inTensorBatch[i]; + if (tensor.rank() < 3 || tensor.rank() > 4) + { + throw StackError("Invalid input tensor shape"); + } + if (tensor.rank() == 4) + { + totalTensors += tensor.shape()[0]; + outputShape[1] = tensor.shape()[1]; + outputShape[2] = tensor.shape()[2]; + outputShape[3] = tensor.shape()[3]; + } + else + { + totalTensors++; + outputShape[1] = tensor.shape()[0]; + outputShape[2] = tensor.shape()[1]; + outputShape[3] = tensor.shape()[2]; + } + if (tensor.layout() == nvcv::TENSOR_CHW || tensor.layout() == nvcv::TENSOR_NCHW) + layout = nvcv::TENSOR_NCHW; + else + layout = nvcv::TENSOR_NHWC; + if (i == 0) + dtype = tensor.dtype(); + } + outputShape[0] = totalTensors; + + Tensor output = Tensor::Create( + { + {outputShape[0], outputShape[1], outputShape[2], outputShape[3]}, + layout + }, + dtype); + return StackTensorBatchInto(output, inTensorBatch, pstream); +} + +// ImageBatchVarShape input functions +Tensor StackVarShapeInto(Tensor &output, ImageBatchVarShape &input, std::optional pstream) +{ + if (!pstream) + { + pstream = Stream::Current(); + } + + auto op = CreateOperator(); + + ResourceGuard guard(*pstream); + guard.add(LockMode::LOCK_MODE_READ, {input}); + guard.add(LockMode::LOCK_MODE_WRITE, {output}); + guard.add(LockMode::LOCK_MODE_NONE, {*op}); + guard.run([&op, &pstream, &input, &output]() { op->submit(pstream->cudaHandle(), input, output); }); + return std::move(output); +} + +Tensor StackVarShape(ImageBatchVarShape &input, std::optional pstream) +{ + nvcv::ImageFormat fmt = input.uniqueFormat(); + if (fmt == nvcv::FMT_NONE) + { + throw StackError("All images in the input must have the same format"); + } + + Tensor output = Tensor::CreateForImageBatch(input.numImages(), input.maxSize(), fmt); + + return StackVarShapeInto(output, input, pstream); } } // namespace @@ -134,13 +246,66 @@ void ExportOpStack(py::module &m) { using namespace pybind11::literals; - m.def("stack", &Stack, "src"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( + // ImageBatchVarShape overloads (register first - most specific type) + m.def("stack", NvtxTrace("cvcuda.stack", &StackVarShape), "src"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( + Executes the Stack operation on the given cuda stream. This takes an ImageBatchVarShape and combines images into a N(HWC/CHW) tensor. + + + Args: + src (cvcuda.ImageBatchVarShape): Input image batch containing one or more images. All images must have the same format and dimensions. + stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + cvcuda.Tensor: The output tensor containing the stacked input images. + + )pbdoc"); + + m.def("stack_into", NvtxTrace("cvcuda.stack_into", &StackVarShapeInto), "dst"_a, "src"_a, py::kw_only(), + "stream"_a = nullptr, R"pbdoc( + Executes the Stack operation on the given cuda stream. This takes an ImageBatchVarShape and combines images into a N(HWC/CHW) tensor. + + Args: + dst (cvcuda.Tensor): Output N(CHW/HWC) tensor to store the result of the operation. + src (cvcuda.ImageBatchVarShape): Input image batch containing one or more images. All images must have the same format and dimensions. + stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + cvcuda.Tensor: The output tensor (same as dst). + )pbdoc"); + + // TensorBatch overloads + m.def("stack", NvtxTrace("cvcuda.stack", &StackTensorBatch), "src"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( + Executes the Stack operation on the given cuda stream. This takes a TensorBatch and combines tensors into a N(HWC/CHW) tensor. + + + Args: + src (cvcuda.TensorBatch): Input tensor batch containing one or more tensors. All tensors must be N(HWC/CHW) or HWC/CHW and have the same data type and shape. + stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + cvcuda.Tensor: The output tensor containing the stacked input tensors. + + )pbdoc"); + + m.def("stack_into", NvtxTrace("cvcuda.stack_into", &StackTensorBatchInto), "dst"_a, "src"_a, py::kw_only(), + "stream"_a = nullptr, R"pbdoc( + Executes the Stack operation on the given cuda stream. This takes a TensorBatch and combines tensors into a N(HWC/CHW) tensor. + + + Args: + dst (cvcuda.Tensor): Output N(CHW/HWC) tensor to store the result of the operation. + src (cvcuda.TensorBatch): Input tensor batch containing one or more tensors. All tensors must be N(HWC/CHW) or HWC/CHW and have the same data type and shape. + stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. + + Returns: + cvcuda.Tensor: The output tensor (same as dst). + )pbdoc"); + + // List[Tensor] overloads (register last - most general type) + m.def("stack", NvtxTrace("cvcuda.stack", &Stack), "src"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( Executes the Stack operation on the given cuda stream. This takes input tensors and combines them into a N(HWC/CHW) tensor. - See also: - Refer to the CV-CUDA C API reference for the Stack operator - for more details and usage examples. Args: src (List[cvcuda.Tensor]): Input tensors containing one or more samples each images all tensors must be N(HWC/CHW) or HWC/CHW and have the same data type and shape. @@ -149,18 +314,12 @@ void ExportOpStack(py::module &m) Returns: cvcuda.Tensor: The output tensor containing the stacked input tensors. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("stack_into", &StackInto, "dst"_a, "src"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( - + m.def("stack_into", NvtxTrace("cvcuda.stack_into", &StackInto), "dst"_a, "src"_a, py::kw_only(), + "stream"_a = nullptr, R"pbdoc( Executes the Stack operation on the given cuda stream. This takes input tensors and combines them into a N(HWC/CHW) tensor. - See also: - Refer to the CV-CUDA C API reference for the Stack operator - for more details and usage examples. Args: dst (cvcuda.Tensor): Output N(CHW/HWC) tensor to store the result of the operation. @@ -168,11 +327,7 @@ void ExportOpStack(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.Tensor: The output tensor (same as dst). )pbdoc"); } diff --git a/python/mod_cvcuda/operators/OpThreshold.cpp b/python/mod_cvcuda/operators/OpThreshold.cpp index 330efda0f..337e812b0 100644 --- a/python/mod_cvcuda/operators/OpThreshold.cpp +++ b/python/mod_cvcuda/operators/OpThreshold.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,6 +16,7 @@ */ #include "Operators.hpp" +#include "VarShapeUtils.hpp" #include #include @@ -26,9 +27,18 @@ #include #include +#include + namespace cvcudapy { namespace { + +class ThresholdError : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + Tensor ThresholdInto(Tensor &output, Tensor &input, Tensor &thresh, Tensor &maxval, uint32_t type, std::optional pstream) { @@ -37,15 +47,17 @@ Tensor ThresholdInto(Tensor &output, Tensor &input, Tensor &thresh, Tensor &maxv pstream = Stream::Current(); } - nvcv::TensorShape shape = input.shape(); - auto threshold = CreateOperator(type, (int)shape[0]); + // HWC inputs (rank 3) have no N dim, so shape[0] is H — fall back to 1. + int batchSize = (input.shape().size() == 4) ? (int)input.shape()[0] : 1; + auto threshold = CreateOperator(type, batchSize); ResourceGuard guard(*pstream); guard.add(LockMode::LOCK_MODE_READ, {input, thresh, maxval}); guard.add(LockMode::LOCK_MODE_WRITE, {output}); guard.add(LockMode::LOCK_MODE_READWRITE, {*threshold}); - threshold->submit(pstream->cudaHandle(), input, output, thresh, maxval); + guard.run([&thresh, &threshold, &pstream, &input, &output, &maxval]() + { threshold->submit(pstream->cudaHandle(), input, output, thresh, maxval); }); return output; } @@ -72,7 +84,8 @@ ImageBatchVarShape ThresholdVarShapeInto(ImageBatchVarShape &output, ImageBatchV guard.add(LockMode::LOCK_MODE_WRITE, {output}); guard.add(LockMode::LOCK_MODE_READWRITE, {*threshold}); - threshold->submit(pstream->cudaHandle(), input, output, thresh, maxval); + guard.run([&thresh, &threshold, &pstream, &input, &output, &maxval]() + { threshold->submit(pstream->cudaHandle(), input, output, thresh, maxval); }); return output; } @@ -80,19 +93,13 @@ ImageBatchVarShape ThresholdVarShapeInto(ImageBatchVarShape &output, ImageBatchV ImageBatchVarShape ThresholdVarShape(ImageBatchVarShape &input, Tensor &thresh, Tensor &maxval, uint32_t type, std::optional pstream) { - ImageBatchVarShape output = ImageBatchVarShape::Create(input.numImages()); - auto format = input.uniqueFormat(); if (!format) { - throw std::runtime_error("All images in input must have the same format."); + throw ThresholdError("All images in input must have the same format."); } - for (auto img = input.begin(); img != input.end(); ++img) - { - auto newimg = Image::Create(img->size(), format); - output.pushBack(newimg); - } + ImageBatchVarShape output = CreateSameShapeImageBatch(input, format, input.numImages()); return ThresholdVarShapeInto(output, input, thresh, maxval, type, pstream); } @@ -103,19 +110,11 @@ void ExportOpThreshold(py::module &m) { using namespace pybind11::literals; - py::options options; - options.disable_function_signatures(); - - m.def("threshold", &Threshold, "src"_a, "thresh"_a, "maxval"_a, "type"_a, py::kw_only(), "stream"_a = nullptr, + m.def("threshold", NvtxTrace("cvcuda.threshold", &Threshold), "src"_a, "thresh"_a, "maxval"_a, "type"_a, + py::kw_only(), "stream"_a = nullptr, R"pbdoc( - - cvcuda.threshold(src: cvcuda.Tensor, thresh: cvcuda.Tensor, maxval: cvcuda.Tensor, type:ThresholdType, stream: Optional[cvcuda.Stream] = None) -> cvcuda.Tensor - Executes the Threshold operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Threshold operator - for more details and usage examples. Args: src (cvcuda.Tensor): Input tensor containing one or more images. @@ -129,21 +128,12 @@ void ExportOpThreshold(py::module &m) Returns: cvcuda.Tensor: The output tensor. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("threshold_into", &ThresholdInto, "dst"_a, "src"_a, "thresh"_a, "maxval"_a, "type"_a, py::kw_only(), - "stream"_a = nullptr, R"pbdoc( - - cvcuda.threshold_into(dst: cvcuda.Tensor, src: cvcuda.Tensor, thresh: cvcuda.Tensor, maxval: cvcuda.Tensor, type:ThresholdType, stream: Optional[cvcuda.Stream] = None) - + m.def("threshold_into", NvtxTrace("cvcuda.threshold_into", &ThresholdInto), "dst"_a, "src"_a, "thresh"_a, + "maxval"_a, "type"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( Executes the Threshold operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Threshold operator - for more details and usage examples. Args: dst (cvcuda.Tensor): Output tensor to store the result of the operation. @@ -156,23 +146,13 @@ void ExportOpThreshold(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.Tensor: The output tensor (same as dst). )pbdoc"); - m.def("threshold", &ThresholdVarShape, "src"_a, "thresh"_a, "maxval"_a, "type"_a, py::kw_only(), - "stream"_a = nullptr, R"pbdoc( - - cvcuda.threshold(src: cvcuda.ImageBatchVarShape, thresh: cvcuda.Tensor, maxval: cvcuda.Tensor, type:ThresholdType, stream: Optional[cvcuda.Stream] = None) -> cvcuda.ImageBatchVarShape - + m.def("threshold", NvtxTrace("cvcuda.threshold", &ThresholdVarShape), "src"_a, "thresh"_a, "maxval"_a, "type"_a, + py::kw_only(), "stream"_a = nullptr, R"pbdoc( Executes the Threshold operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Threshold operator - for more details and usage examples. Args: src (cvcuda.ImageBatchVarShape): Input image batch containing one or more images. @@ -186,21 +166,12 @@ void ExportOpThreshold(py::module &m) Returns: cvcuda.ImageBatchVarShape: The output image batch. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("threshold_into", &ThresholdVarShapeInto, "dst"_a, "src"_a, "thresh"_a, "maxval"_a, "type"_a, py::kw_only(), - "stream"_a = nullptr, R"pbdoc( - - cvcuda.threshold_into(dst: cvcuda.ImageBatchVarShape, src: cvcuda.ImageBatchVarShape, thresh: cvcuda.Tensor, maxval: cvcuda.Tensor, type:ThresholdType, stream: Optional[cvcuda.Stream] = None) - + m.def("threshold_into", NvtxTrace("cvcuda.threshold_into", &ThresholdVarShapeInto), "dst"_a, "src"_a, "thresh"_a, + "maxval"_a, "type"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( Executes the Threshold operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Threshold operator - for more details and usage examples. Args: dst (cvcuda.ImageBatchVarShape): Output image batch containing the result of the operation. @@ -213,11 +184,7 @@ void ExportOpThreshold(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.ImageBatchVarShape: The output image batch (same as dst). )pbdoc"); } diff --git a/python/mod_cvcuda/operators/OpWarpAffine.cpp b/python/mod_cvcuda/operators/OpWarpAffine.cpp index 63f0804d8..11e3f9d7b 100644 --- a/python/mod_cvcuda/operators/OpWarpAffine.cpp +++ b/python/mod_cvcuda/operators/OpWarpAffine.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,6 +16,7 @@ */ #include "Operators.hpp" +#include "VarShapeUtils.hpp" #include #include @@ -29,10 +30,18 @@ #include #include +#include + namespace cvcudapy { namespace { +class WarpAffineError : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + Tensor WarpAffineInto(Tensor &output, Tensor &input, const pyarray &xform, const int32_t flags, const NVCVBorderType borderMode, const pyarray &borderValue, std::optional pstream) { @@ -43,13 +52,11 @@ Tensor WarpAffineInto(Tensor &output, Tensor &input, const pyarray &xform, const float4 bValue = GetFloat4FromPyArray(borderValue); - size_t xformDims = xform.ndim(); - if (!(xformDims == 2 && xform.shape(0) == 2 && xform.shape(1) == 3)) + if (size_t xformDims = xform.ndim(); !(xformDims == 2 && xform.shape(0) == 2 && xform.shape(1) == 3)) { - throw std::runtime_error( - util::FormatString("Details of transformation matrix: nDim == 2, shape == (2, 3) but current is " - "'%lu', ('%lu', '%lu') respectively", - xformDims, xform.shape(0), xform.shape(1))); + throw WarpAffineError( + util::ConcatString("Details of transformation matrix: nDim == 2, shape == (2, 3) but current is '", + xformDims, "', ('", xform.shape(0), "', '", xform.shape(1), "') respectively")); } NVCVAffineTransform xformOutput; @@ -68,7 +75,8 @@ Tensor WarpAffineInto(Tensor &output, Tensor &input, const pyarray &xform, const guard.add(LockMode::LOCK_MODE_WRITE, {output}); guard.add(LockMode::LOCK_MODE_NONE, {*warpAffine}); - warpAffine->submit(pstream->cudaHandle(), input, output, xformOutput, flags, borderMode, bValue); + guard.run([&warpAffine, &pstream, &input, &output, &xformOutput, &flags, &borderMode, &bValue]() + { warpAffine->submit(pstream->cudaHandle(), input, output, xformOutput, flags, borderMode, bValue); }); return output; } @@ -91,17 +99,17 @@ ImageBatchVarShape WarpAffineVarShapeInto(ImageBatchVarShape &output, ImageBatch } size_t bValueSize = borderValue.size(); - size_t bValueDims = borderValue.ndim(); - if (bValueSize > 4 || bValueDims != 1) + if (size_t bValueDims = borderValue.ndim(); bValueSize > 4 || bValueDims != 1) { - throw std::runtime_error(util::FormatString( - "Channels of borderValue should <= 4 and dimension should be 2, current is '%lu', '%lu' respectively", - bValueSize, bValueDims)); + throw py::value_error( + util::ConcatString("Channels of borderValue should <= 4 and dimension should be 2, current is '", + bValueSize, "', '", bValueDims, "' respectively")); } float4 bValue; - for (size_t i = 0; i < 4; i++) + for (int i = 0; i < 4; i++) { - nvcv::cuda::GetElement(bValue, i) = bValueSize > i ? *borderValue.data(i) : 0.f; + const auto valueIdx = static_cast(i); + nvcv::cuda::GetElement(bValue, i) = bValueSize > valueIdx ? *borderValue.data(valueIdx) : 0.f; } auto warpAffine = CreateOperator(input.capacity()); @@ -109,9 +117,10 @@ ImageBatchVarShape WarpAffineVarShapeInto(ImageBatchVarShape &output, ImageBatch ResourceGuard guard(*pstream); guard.add(LockMode::LOCK_MODE_READ, {input, xform}); guard.add(LockMode::LOCK_MODE_WRITE, {output}); - guard.add(LockMode::LOCK_MODE_READWRITE, {*warpAffine}); + guard.add(LockMode::LOCK_MODE_NONE, {*warpAffine}); // operator is const, no internal state modified - warpAffine->submit(pstream->cudaHandle(), input, output, xform, flags, borderMode, bValue); + guard.run([&warpAffine, &pstream, &input, &output, &xform, &flags, &borderMode, &bValue]() + { warpAffine->submit(pstream->cudaHandle(), input, output, xform, flags, borderMode, bValue); }); return output; } @@ -120,15 +129,7 @@ ImageBatchVarShape WarpAffineVarShape(ImageBatchVarShape &input, Tensor &xform, const NVCVBorderType borderMode, const pyarray &borderValue, std::optional pstream) { - ImageBatchVarShape output = ImageBatchVarShape::Create(input.capacity()); - - for (int i = 0; i < input.numImages(); ++i) - { - nvcv::ImageFormat format = input[i].format(); - nvcv::Size2D size = input[i].size(); - auto image = Image::Create(size, format); - output.pushBack(image); - } + ImageBatchVarShape output = CreateSameShapeImageBatch(input); return WarpAffineVarShapeInto(output, input, xform, flags, borderMode, borderValue, pstream); } @@ -139,19 +140,10 @@ void ExportOpWarpAffine(py::module &m) { using namespace pybind11::literals; - py::options options; - options.disable_function_signatures(); - - m.def("warp_affine", &WarpAffine, "src"_a, "xform"_a, "flags"_a, py::kw_only(), + m.def("warp_affine", NvtxTrace("cvcuda.warp_affine", &WarpAffine), "src"_a, "xform"_a, "flags"_a, py::kw_only(), "border_mode"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, "border_value"_a = 0, "stream"_a = nullptr, R"pbdoc( - - cvcuda.warp_affine(src: cvcuda.Tensor, xform: cvcuda.Tensor, flags: cvcuda.Tensor, border_mode: cvcuda.Border = cvcuda.Border.CONSTANT, border_value: numpy.ndarray, stream: Optional[cvcuda.Stream] = None) -> cvcuda.Tensor - Executes the Warp Affine operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Warp Affine operator - for more details and usage examples. Args: src (cvcuda.Tensor): Input tensor containing one or more images. @@ -167,21 +159,13 @@ void ExportOpWarpAffine(py::module &m) Returns: cvcuda.Tensor: The output tensor. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("warp_affine_into", &WarpAffineInto, "dst"_a, "src"_a, "xform"_a, "flags"_a, py::kw_only(), - "border_mode"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, "border_value"_a = 0, "stream"_a = nullptr, R"pbdoc( - - cvcuda.warp_affine_into(dst: cvcuda.Tensor, src: cvcuda.Tensor, xform: cvcuda.Tensor, flags: cvcuda.Tensor, border_mode: cvcuda.Border = cvcuda.Border.CONSTANT, border_value: numpy.ndarray, stream: Optional[cvcuda.Stream] = None) - + m.def("warp_affine_into", NvtxTrace("cvcuda.warp_affine_into", &WarpAffineInto), "dst"_a, "src"_a, "xform"_a, + "flags"_a, py::kw_only(), "border_mode"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, "border_value"_a = 0, + "stream"_a = nullptr, R"pbdoc( Executes the Warp Affine operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Warp Affine operator - for more details and usage examples. Args: dst (cvcuda.Tensor): Output tensor to store the result of the operation. @@ -196,23 +180,14 @@ void ExportOpWarpAffine(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.Tensor: The output tensor (same as dst). )pbdoc"); - m.def("warp_affine", &WarpAffineVarShape, "src"_a, "xform"_a, "flags"_a, py::kw_only(), - "border_mode"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, "border_value"_a = 0, "stream"_a = nullptr, R"pbdoc( - - cvcuda.warp_affine(src: cvcuda.ImageBatchVarShape, xform: cvcuda.Tensor, flags: cvcuda.Tensor, border_mode: cvcuda.Border = cvcuda.Border.CONSTANT, border_value: numpy.ndarray, stream: Optional[cvcuda.Stream] = None) -> cvcuda.ImageBatchVarShape - + m.def("warp_affine", NvtxTrace("cvcuda.warp_affine", &WarpAffineVarShape), "src"_a, "xform"_a, "flags"_a, + py::kw_only(), "border_mode"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, "border_value"_a = 0, + "stream"_a = nullptr, R"pbdoc( Executes the Warp Affine operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Warp Affine operator - for more details and usage examples. Args: src (cvcuda.ImageBatchVarShape): Input image batch containing one or more images. @@ -228,21 +203,13 @@ void ExportOpWarpAffine(py::module &m) Returns: cvcuda.ImageBatchVarShape: The output image batch. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("warp_affine_into", &WarpAffineVarShapeInto, "dst"_a, "src"_a, "xform"_a, "flags"_a, py::kw_only(), - "border_mode"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, "border_value"_a = 0, "stream"_a = nullptr, R"pbdoc( - - cvcuda.warp_affine_into(dst: cvcuda.ImageBatchVarShape, src: cvcuda.ImageBatchVarShape, xform: cvcuda.Tensor, flags: cvcuda.Tensor, border_mode: cvcuda.Border = cvcuda.Border.CONSTANT, border_value: numpy.ndarray, stream: Optional[cvcuda.Stream] = None) - + m.def("warp_affine_into", NvtxTrace("cvcuda.warp_affine_into", &WarpAffineVarShapeInto), "dst"_a, "src"_a, + "xform"_a, "flags"_a, py::kw_only(), "border_mode"_a = NVCVBorderType::NVCV_BORDER_CONSTANT, + "border_value"_a = 0, "stream"_a = nullptr, R"pbdoc( Executes the Warp Affine operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Warp Affine operator - for more details and usage examples. Args: dst (cvcuda.ImageBatchVarShape): Output image batch containing the result of the operation. @@ -257,11 +224,7 @@ void ExportOpWarpAffine(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.ImageBatchVarShape: The output image batch (same as dst). )pbdoc"); } diff --git a/python/mod_cvcuda/operators/OpWarpPerspective.cpp b/python/mod_cvcuda/operators/OpWarpPerspective.cpp index 9691c67e6..b0aaa983e 100644 --- a/python/mod_cvcuda/operators/OpWarpPerspective.cpp +++ b/python/mod_cvcuda/operators/OpWarpPerspective.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,6 +16,7 @@ */ #include "Operators.hpp" +#include "VarShapeUtils.hpp" #include #include @@ -28,10 +29,18 @@ #include #include +#include + namespace cvcudapy { namespace { +class WarpPerspectiveError : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + Tensor WarpPerspectiveInto(Tensor &output, Tensor &input, const pyarray &xform, const int32_t flags, const NVCVBorderType borderMode, const pyarray &borderValue, std::optional pstream) { @@ -42,13 +51,11 @@ Tensor WarpPerspectiveInto(Tensor &output, Tensor &input, const pyarray &xform, float4 bValue = GetFloat4FromPyArray(borderValue); - size_t xformDims = xform.ndim(); - if (!(xformDims == 2 && xform.shape(0) == 3 && xform.shape(1) == 3)) + if (size_t xformDims = xform.ndim(); !(xformDims == 2 && xform.shape(0) == 3 && xform.shape(1) == 3)) { - throw std::runtime_error( - util::FormatString("Details of transformation matrix: nDim == 2, shape == (3, 3) but current is " - "'%lu', ('%lu', '%lu') respectively", - xformDims, xform.shape(0), xform.shape(1))); + throw WarpPerspectiveError( + util::ConcatString("Details of transformation matrix: nDim == 2, shape == (3, 3) but current is '", + xformDims, "', ('", xform.shape(0), "', '", xform.shape(1), "') respectively")); } NVCVPerspectiveTransform xformOutput; @@ -67,7 +74,9 @@ Tensor WarpPerspectiveInto(Tensor &output, Tensor &input, const pyarray &xform, guard.add(LockMode::LOCK_MODE_WRITE, {output}); guard.add(LockMode::LOCK_MODE_NONE, {*warpPerspective}); - warpPerspective->submit(pstream->cudaHandle(), input, output, xformOutput, flags, borderMode, bValue); + guard.run( + [&warpPerspective, &pstream, &input, &output, &xformOutput, &flags, &borderMode, &bValue]() + { warpPerspective->submit(pstream->cudaHandle(), input, output, xformOutput, flags, borderMode, bValue); }); return output; } @@ -90,17 +99,17 @@ ImageBatchVarShape WarpPerspectiveVarShapeInto(ImageBatchVarShape &output, Image } size_t bValueSize = borderValue.size(); - size_t bValueDims = borderValue.ndim(); - if (bValueSize > 4 || bValueDims != 1) + if (size_t bValueDims = borderValue.ndim(); bValueSize > 4 || bValueDims != 1) { - throw std::runtime_error(util::FormatString( - "Channels of borderValue should <= 4 and dimension should be 2, current is '%lu', '%lu' respectively", - bValueSize, bValueDims)); + throw py::value_error( + util::ConcatString("Channels of borderValue should <= 4 and dimension should be 2, current is '", + bValueSize, "', '", bValueDims, "' respectively")); } float4 bValue; - for (size_t i = 0; i < 4; i++) + for (int i = 0; i < 4; i++) { - nvcv::cuda::GetElement(bValue, i) = bValueSize > i ? *borderValue.data(i) : 0.f; + const auto valueIdx = static_cast(i); + nvcv::cuda::GetElement(bValue, i) = bValueSize > valueIdx ? *borderValue.data(valueIdx) : 0.f; } auto warpPerspective = CreateOperator(input.capacity()); @@ -110,7 +119,8 @@ ImageBatchVarShape WarpPerspectiveVarShapeInto(ImageBatchVarShape &output, Image guard.add(LockMode::LOCK_MODE_WRITE, {output}); guard.add(LockMode::LOCK_MODE_READWRITE, {*warpPerspective}); - warpPerspective->submit(pstream->cudaHandle(), input, output, xform, flags, borderMode, bValue); + guard.run([&warpPerspective, &pstream, &input, &output, &xform, &flags, &borderMode, &bValue]() + { warpPerspective->submit(pstream->cudaHandle(), input, output, xform, flags, borderMode, bValue); }); return output; } @@ -119,15 +129,7 @@ ImageBatchVarShape WarpPerspectiveVarShape(ImageBatchVarShape &input, Tensor &xf const NVCVBorderType borderMode, const pyarray &borderValue, std::optional pstream) { - ImageBatchVarShape output = ImageBatchVarShape::Create(input.capacity()); - - for (int i = 0; i < input.numImages(); ++i) - { - nvcv::ImageFormat format = input[i].format(); - nvcv::Size2D size = input[i].size(); - auto image = Image::Create(size, format); - output.pushBack(image); - } + ImageBatchVarShape output = CreateSameShapeImageBatch(input); return WarpPerspectiveVarShapeInto(output, input, xform, flags, borderMode, borderValue, pstream); } @@ -138,19 +140,10 @@ void ExportOpWarpPerspective(py::module &m) { using namespace pybind11::literals; - py::options options; - options.disable_function_signatures(); - - m.def("warp_perspective", &WarpPerspective, "src"_a, "xform"_a, "flags"_a, py::kw_only(), "border_mode"_a, - "border_value"_a, "stream"_a = nullptr, R"pbdoc( - - cvcuda.warp_perspective(src: cvcuda.Tensor, xform: cvcuda.Tensor, flags: int, border_mode: cvcuda.Border, border_value: numpy.ndarray, stream: Optional[cvcuda.Stream] = None) -> cvcuda.Tensor - + m.def("warp_perspective", NvtxTrace("cvcuda.warp_perspective", &WarpPerspective), "src"_a, "xform"_a, "flags"_a, + py::kw_only(), "border_mode"_a, "border_value"_a, "stream"_a = nullptr, R"pbdoc( Executes the Warp Perspective operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Warp Perspective operator - for more details and usage examples. Args: src (cvcuda.Tensor): Input tensor containing one or more images. @@ -166,21 +159,12 @@ void ExportOpWarpPerspective(py::module &m) Returns: cvcuda.Tensor: The output tensor. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("warp_perspective_into", &WarpPerspectiveInto, "dst"_a, "src"_a, "xform"_a, "flags"_a, py::kw_only(), - "border_mode"_a, "border_value"_a, "stream"_a = nullptr, R"pbdoc( - - cvcuda.warp_perspective_into(dst: cvcuda.Tensor, src: cvcuda.Tensor, xform: cvcuda.Tensor, flags: int, border_mode: cvcuda.Border, border_value: numpy.ndarray, stream: Optional[cvcuda.Stream] = None) - + m.def("warp_perspective_into", NvtxTrace("cvcuda.warp_perspective_into", &WarpPerspectiveInto), "dst"_a, "src"_a, + "xform"_a, "flags"_a, py::kw_only(), "border_mode"_a, "border_value"_a, "stream"_a = nullptr, R"pbdoc( Executes the Warp Perspective operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Warp Perspective operator - for more details and usage examples. Args: dst (cvcuda.Tensor): Output tensor to store the result of the operation. @@ -195,23 +179,13 @@ void ExportOpWarpPerspective(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.Tensor: The output tensor (same as dst). )pbdoc"); - m.def("warp_perspective", &WarpPerspectiveVarShape, "src"_a, "xform"_a, "flags"_a, py::kw_only(), "border_mode"_a, - "border_value"_a, "stream"_a = nullptr, R"pbdoc( - - cvcuda.warp_perspective(src: cvcuda.ImageBatchVarShape, xform: cvcuda.Tensor, flags: int, border_mode: cvcuda.Border, border_value: numpy.ndarray, stream: Optional[cvcuda.Stream] = None) -> cvcuda.ImageBatchVarShape - + m.def("warp_perspective", NvtxTrace("cvcuda.warp_perspective", &WarpPerspectiveVarShape), "src"_a, "xform"_a, + "flags"_a, py::kw_only(), "border_mode"_a, "border_value"_a, "stream"_a = nullptr, R"pbdoc( Executes the Warp Perspective operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Warp Perspective operator - for more details and usage examples. Args: src (cvcuda.ImageBatchVarShape): Input image batch containing one or more images. @@ -227,21 +201,13 @@ void ExportOpWarpPerspective(py::module &m) Returns: cvcuda.ImageBatchVarShape: The output image batch. - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. )pbdoc"); - m.def("warp_perspective_into", &WarpPerspectiveVarShapeInto, "dst"_a, "src"_a, "xform"_a, "flags"_a, py::kw_only(), - "border_mode"_a, "border_value"_a, "stream"_a = nullptr, R"pbdoc( - - cvcuda.warp_perspective_into(dst: cvcuda.ImageBatchVarShape, src: cvcuda.ImageBatchVarShape, xform: cvcuda.Tensor, flags: int, border_mode: cvcuda.Border, border_value: numpy.ndarray, stream: Optional[cvcuda.Stream] = None) - + m.def("warp_perspective_into", NvtxTrace("cvcuda.warp_perspective_into", &WarpPerspectiveVarShapeInto), "dst"_a, + "src"_a, "xform"_a, "flags"_a, py::kw_only(), "border_mode"_a, "border_value"_a, "stream"_a = nullptr, + R"pbdoc( Executes the Warp Perspective operation on the given cuda stream. - See also: - Refer to the CV-CUDA C API reference for the Warp Perspective operator - for more details and usage examples. Args: src (cvcuda.ImageBatchVarShape): Input image batch containing one or more images. @@ -256,11 +222,7 @@ void ExportOpWarpPerspective(py::module &m) stream (cvcuda.Stream, optional): CUDA Stream on which to perform the operation. Returns: - None - - Caution: - Restrictions to several arguments may apply. Check the C - API references of the CV-CUDA operator. + cvcuda.ImageBatchVarShape: The output image batch (same as dst). )pbdoc"); } diff --git a/python/mod_cvcuda/operators/Operators.hpp b/python/mod_cvcuda/operators/Operators.hpp index a8f04e8b2..c9885f301 100644 --- a/python/mod_cvcuda/operators/Operators.hpp +++ b/python/mod_cvcuda/operators/Operators.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,6 +15,8 @@ * limitations under the License. */ +#include "../NvtxRange.hpp" + #include #include #include @@ -27,14 +29,14 @@ #include #include -#include +#include -namespace nvcvpy::util { -} +#include namespace cvcudapy { using nvcvpy::Array; +using nvcvpy::AsNvcvTensor; using nvcvpy::CreateNVCVTensorShape; using nvcvpy::CreateShape; using nvcvpy::Image; @@ -100,6 +102,16 @@ void ExportOpPairwiseMatcher(py::module &m); void ExportOpStack(py::module &m); void ExportOpFindHomography(py::module &m); void ExportOpResizeCropConvertReformat(py::module &m); +void ExportOpCLAHE(py::module &m); +void ExportOpInvert(py::module &m); +void ExportOpSolarize(py::module &m); +void ExportOpPosterize(py::module &m); +void ExportOpAutoContrast(py::module &m); +void ExportOpAdjustHue(py::module &m); +void ExportOpAdjustSaturation(py::module &m); +void ExportOpAdjustSharpness(py::module &m); +void ExportOpAdjustContrast(py::module &m); +void ExportOpJpegCompressionDistortion(py::module &m); // Helper class that serves as generic python-side operator class. // OP: native operator class @@ -108,7 +120,8 @@ template class PyOperator; template -class PyOperator : public nvcvpy::Container +class PyOperator // NOSONAR: generic operator wrappers share the cache hierarchy. + : public nvcvpy::Container { public: // This defines a generic cache key class for any cvcuda::OP operator. @@ -118,7 +131,7 @@ class PyOperator : public nvcvpy::Container class Key : public nvcvpy::IKey { public: - Key(const CTOR_ARGS &...args) + explicit Key(const CTOR_ARGS &...args) : m_args{args...} { } @@ -131,16 +144,16 @@ class PyOperator : public nvcvpy::Container bool doIsCompatible(const nvcvpy::IKey &that_) const override { - const Key &that = static_cast(that_); + const auto &that = static_cast(that_); return m_args == that.m_args; } std::tuple...> m_args; }; - PyOperator(CTOR_ARGS &&...args) + explicit PyOperator(CTOR_ARGS &&...args) : m_key(args...) - , m_op{std::forward(args)...} + , m_op{static_cast(args)...} { } @@ -152,7 +165,7 @@ class PyOperator : public nvcvpy::Container py::object container() const override { - return *this; + return py::reinterpret_borrow(this->ptr()); } const nvcvpy::IKey &key() const override @@ -195,7 +208,7 @@ std::shared_ptr CreateOperatorEx(CTOR_ARGS &&...args) if (vcont.empty()) { // Creates a new one - auto op = std::shared_ptr(new PyOP(std::forward(args)...)); + auto op = std::make_shared(std::forward(args)...); // Adds to the resource cache nvcvpy::Cache::add(*op); diff --git a/python/mod_cvcuda/operators/UnaryElementwiseOp.hpp b/python/mod_cvcuda/operators/UnaryElementwiseOp.hpp new file mode 100644 index 000000000..357cb58d7 --- /dev/null +++ b/python/mod_cvcuda/operators/UnaryElementwiseOp.hpp @@ -0,0 +1,85 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#ifndef CVCUDA_PY_UNARY_ELEMENTWISE_OP_HPP +#define CVCUDA_PY_UNARY_ELEMENTWISE_OP_HPP + +// Shared Python-binding plumbing for unary element-wise operators (Invert, Solarize, Posterize, …). +// Every such operator's binding has the same body — create the operator, take a ResourceGuard over +// the input/output, and submit on the stream — differing only by the operator type and any extra +// scalar parameters (e.g. Solarize's threshold, Posterize's bits). make-op generates each binding +// from one template, so that body is centralized here and each Op.cpp keeps only the thin, +// Python-facing wrappers (with op-specific argument names) plus the m.def docstrings. The extra +// scalar parameters are forwarded through the variadic `Args...` to the operator's submit(). + +#include "Operators.hpp" +#include "VarShapeUtils.hpp" + +#include +#include +#include +#include + +#include + +namespace cvcudapy { + +// Run a unary element-wise operator into a caller-provided output (Tensor or ImageBatchVarShape). +// `Args...` are forwarded verbatim as the trailing submit() parameters (none for Invert, the +// threshold for Solarize, the bit count for Posterize, …). +template +InOut UnaryElementwiseInto(InOut &output, InOut &input, std::optional pstream, Args... args) +{ + if (!pstream) + { + pstream = Stream::Current(); + } + + auto op = CreateOperator(); + + ResourceGuard guard(*pstream); + guard.add(LockMode::LOCK_MODE_READ, {input}); + guard.add(LockMode::LOCK_MODE_WRITE, {output}); + guard.add(LockMode::LOCK_MODE_NONE, {*op}); + + guard.run([&op, &pstream, &input, &output, &args...]() + { op->submit(pstream->cudaHandle(), input, output, args...); }); + + return output; +} + +// Tensor convenience overload: allocate an output matching the input's shape/dtype/layout. +template +Tensor UnaryElementwiseTensor(Tensor &input, std::optional pstream, Args... args) +{ + Tensor output = Tensor::Create(input.shape(), input.dtype()); + + return UnaryElementwiseInto(output, input, pstream, args...); +} + +// ImageBatchVarShape convenience overload: allocate an output matching the input batch. +template +ImageBatchVarShape UnaryElementwiseVarShape(ImageBatchVarShape &input, std::optional pstream, Args... args) +{ + ImageBatchVarShape output = CreateSameShapeImageBatch(input); + + return UnaryElementwiseInto(output, input, pstream, args...); +} + +} // namespace cvcudapy + +#endif // CVCUDA_PY_UNARY_ELEMENTWISE_OP_HPP diff --git a/python/mod_cvcuda/operators/VarShapeUtils.hpp b/python/mod_cvcuda/operators/VarShapeUtils.hpp new file mode 100644 index 000000000..b29ded6e5 --- /dev/null +++ b/python/mod_cvcuda/operators/VarShapeUtils.hpp @@ -0,0 +1,87 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#ifndef CVCUDAPY_OPERATORS_VARSHAPEUTILS_HPP +#define CVCUDAPY_OPERATORS_VARSHAPEUTILS_HPP + +#include +#include + +#include +#include + +namespace cvcudapy { + +inline nvcvpy::ImageBatchVarShape CreateSameShapeImageBatch(const nvcvpy::ImageBatchVarShape &input, + nvcv::ImageFormat format, int capacity) +{ + nvcvpy::ImageBatchVarShape output = nvcvpy::ImageBatchVarShape::Create(capacity); + + for (int i = 0; i < input.numImages(); ++i) + { + output.pushBackImage(nvcvpy::Image::Create(input[i].size(), format)); + } + + return output; +} + +inline nvcvpy::ImageBatchVarShape CreateSameShapeImageBatch(const nvcvpy::ImageBatchVarShape &input, + nvcv::ImageFormat format) +{ + return CreateSameShapeImageBatch(input, format, input.capacity()); +} + +inline nvcvpy::ImageBatchVarShape CreateSameShapeImageBatch(const nvcvpy::ImageBatchVarShape &input, int capacity) +{ + nvcvpy::ImageBatchVarShape output = nvcvpy::ImageBatchVarShape::Create(capacity); + + for (int i = 0; i < input.numImages(); ++i) + { + output.pushBackImage(nvcvpy::Image::Create(input[i].size(), input[i].format())); + } + + return output; +} + +inline nvcvpy::ImageBatchVarShape CreateSameShapeImageBatch(const nvcvpy::ImageBatchVarShape &input) +{ + return CreateSameShapeImageBatch(input, input.capacity()); +} + +inline nvcvpy::ImageBatchVarShape CreateSizedImageBatch(const nvcvpy::ImageBatchVarShape &input, + const std::vector> &sizes, int capacity) +{ + nvcvpy::ImageBatchVarShape output = nvcvpy::ImageBatchVarShape::Create(capacity); + + for (int i = 0; i < input.numImages(); ++i) + { + auto [size0, size1] = sizes[i]; + output.pushBackImage(nvcvpy::Image::Create({size0, size1}, input[i].format())); + } + + return output; +} + +inline nvcvpy::ImageBatchVarShape CreateSizedImageBatch(const nvcvpy::ImageBatchVarShape &input, + const std::vector> &sizes) +{ + return CreateSizedImageBatch(input, sizes, input.capacity()); +} + +} // namespace cvcudapy + +#endif // CVCUDAPY_OPERATORS_VARSHAPEUTILS_HPP diff --git a/python/py.typed b/python/py.typed new file mode 100644 index 000000000..52a7a9daf --- /dev/null +++ b/python/py.typed @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 diff --git a/python/pyproject.toml.in b/python/pyproject.toml.in index 330be99fd..53de92cbf 100644 --- a/python/pyproject.toml.in +++ b/python/pyproject.toml.in @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 [build-system] @@ -28,7 +28,6 @@ classifiers = [ "Operating System :: POSIX :: Linux", "Programming Language :: C++", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -40,7 +39,7 @@ classifiers = [ "Topic :: Scientific/Engineering :: Image Processing", "Topic :: Software Development :: Libraries", ] -requires-python = ">=3.9, <3.15" +requires-python = ">=3.10, <3.15" dependencies = [ "numpy>=1.23.5", ] @@ -57,6 +56,7 @@ dev = [ "pytest>=7.0", "black>=22.0", "flake8>=4.0", + "pybind11-stubgen>=2.5", ] [tool.setuptools] @@ -70,12 +70,12 @@ include = ["cvcuda*"] where = ["."] [tool.setuptools.package-data] -cvcuda = ["*.so"] +cvcuda = ["*.so", "*.pyi", "py.typed"] "cvcuda_cu${CUDA_VERSION_MAJOR}.libs" = ["*.*"] [tool.black] line-length = 100 -target-version = ["py39", "py310", "py311", "py312", "py313", "py314"] +target-version = ["py310", "py311", "py312", "py313", "py314"] include = '\.pyi?$' [tool.pytest.ini_options] diff --git a/python/setup.py.in b/python/setup.py.in index f62970a24..d626d7ae5 100644 --- a/python/setup.py.in +++ b/python/setup.py.in @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the 'License'); @@ -53,11 +53,19 @@ setup( license='Apache-2.0', packages=find_packages(include=["cvcuda"]), package_data={ - 'cvcuda': ['*.so'], + # Keep in sync with pyproject.toml.in [tool.setuptools.package-data] + # and MANIFEST.in: ship the pybind11-stubgen .pyi stubs and the + # PEP 561 py.typed marker so this legacy bdist_wheel fallback path + # produces wheels with the same inline typing as `python -m build`. + 'cvcuda': [ + '*.so', + '*.pyi', + 'py.typed', + ], 'cvcuda_cu${CUDA_VERSION_MAJOR}.libs': ['*.*'], }, include_package_data=True, - python_requires='>=3.9, <3.15', + python_requires='>=3.10, <3.15', install_requires=['numpy>=1.23.5'], cmdclass={'build_ext': NoBuildExtension}, ext_modules=[Extension(name='_unused', sources=[])], diff --git a/samples/CMakeLists.txt b/samples/CMakeLists.txt new file mode 100644 index 000000000..9744068fc --- /dev/null +++ b/samples/CMakeLists.txt @@ -0,0 +1,80 @@ +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +project(cvcuda_samples) + +# Install main sample script +install(PROGRAMS ${CMAKE_CURRENT_SOURCE_DIR}/common.py + DESTINATION ${CMAKE_INSTALL_BINDIR} + COMPONENT samples) + +# Install runner scripts +install(PROGRAMS + ${CMAKE_CURRENT_SOURCE_DIR}/run_samples.sh + ${CMAKE_CURRENT_SOURCE_DIR}/install_samples_dependencies.sh + DESTINATION ${CMAKE_INSTALL_BINDIR} + COMPONENT samples) + +# Install sample applications +install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/applications + DESTINATION ${CMAKE_INSTALL_BINDIR} + COMPONENT samples + FILES_MATCHING PATTERN "*.py") + +# Install operator examples +install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/operators + DESTINATION ${CMAKE_INSTALL_BINDIR} + COMPONENT samples + FILES_MATCHING PATTERN "*.py") + +# Install datatype examples +install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/datatypes + DESTINATION ${CMAKE_INSTALL_BINDIR} + COMPONENT samples + FILES_MATCHING PATTERN "*.py") + +# Install interoperability examples +install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/interoperability + DESTINATION ${CMAKE_INSTALL_BINDIR} + COMPONENT samples + FILES_MATCHING PATTERN "*.py") + +# Install sample assets +install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/assets + DESTINATION ${CMAKE_INSTALL_BINDIR} + COMPONENT samples) + +# Install object cache support +install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/object_cache + DESTINATION ${CMAKE_INSTALL_BINDIR} + COMPONENT samples + FILES_MATCHING PATTERN "*.py") + +# Install requirements files +install(FILES + ${CMAKE_CURRENT_SOURCE_DIR}/requirements.samples.hello_world_cu12.txt + ${CMAKE_CURRENT_SOURCE_DIR}/requirements.samples.hello_world_cu13.txt + ${CMAKE_CURRENT_SOURCE_DIR}/requirements.samples.common.txt + ${CMAKE_CURRENT_SOURCE_DIR}/requirements.samples.cu12.txt + ${CMAKE_CURRENT_SOURCE_DIR}/requirements.samples.cu13.txt + DESTINATION ${CMAKE_INSTALL_BINDIR} + COMPONENT samples) + +# Install documentation +install(FILES + ${CMAKE_CURRENT_SOURCE_DIR}/README.md + ${CMAKE_CURRENT_SOURCE_DIR}/NOTICE.md + DESTINATION ${CMAKE_INSTALL_BINDIR} + COMPONENT samples) diff --git a/samples/README.md b/samples/README.md index 24a14a257..00b40a88c 100644 --- a/samples/README.md +++ b/samples/README.md @@ -1,5 +1,5 @@ -[//]: # "SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved." +[//]: # "SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved." [//]: # "SPDX-License-Identifier: Apache-2.0" [//]: # "" [//]: # "Licensed under the Apache License, Version 2.0 (the 'License');" @@ -27,7 +27,7 @@ For quick testing with just the `hello_world.py` sample: ```shell python3 -m venv venv_samples source venv_samples/bin/activate -python3 -m pip install -r requirements_hello_world_cu12.txt +python3 -m pip install -r requirements.samples.hello_world_cu12.txt python3 applications/hello_world.py ``` @@ -35,7 +35,7 @@ python3 applications/hello_world.py ```shell python3 -m venv venv_samples source venv_samples/bin/activate -python3 -m pip install -r requirements_hello_world_cu13.txt +python3 -m pip install -r requirements.samples.hello_world_cu13.txt python3 applications/hello_world.py ``` @@ -53,18 +53,10 @@ cd samples This script will: - Detect your CUDA version (12 or 13) - Create a virtual environment at `venv_samples` -- Install all required dependencies including CV-CUDA, PyTorch, NumPy, and sample-specific packages from self-contained requirements files +- Install all required dependencies including CV-CUDA, PyTorch, NumPy, and sample-specific packages (including interoperability dependencies like CuPy, PyCUDA, PyNvVideoCodec) from self-contained requirements files **Note:** Full samples require Python 3.10-3.13 on x86_64/amd64 platforms -For interoperability samples only, use: - -```shell -./install_interop_dependencies.sh -``` - -This installs a lighter set of dependencies specifically for interoperability samples (PyTorch, CuPy, PyCUDA, PyNvVideoCodec, CV-CUDA). - After installation, activate the virtual environment: ```shell @@ -84,8 +76,7 @@ python3 interoperability/pytorch_interop.py Or run all samples at once: ```shell -./run_samples.sh # Operators and applications -./run_interop.sh # Interoperability samples +./run_samples.sh # All sample categories (operators, applications, interoperability, etc.) ``` ## Documentation @@ -95,7 +86,7 @@ For detailed documentation, tutorials, and API reference: - **[CV-CUDA Samples Documentation](https://cvcuda.github.io/CV-CUDA/samples.html)** - Complete samples guide - [Installation Instructions](https://cvcuda.github.io/CV-CUDA/samples.html#samples-venv-installation) - Virtual environment setup - [Hello World Tutorial](https://cvcuda.github.io/CV-CUDA/samples.html#cv-cuda-hello-world) - Getting started - - [Running the Samples](https://cvcuda.github.io/CV-CUDA/samples.html#running-operator-and-application-samples) - Execution guide + - [Running the Samples](https://cvcuda.github.io/CV-CUDA/samples.html#running-the-samples) - Execution guide - [Sample Index](https://cvcuda.github.io/CV-CUDA/samples.html#sample-index) - Browse all samples - **[Installation Guide](https://cvcuda.github.io/CV-CUDA/installation.html)** - CV-CUDA installation options @@ -103,4 +94,4 @@ For detailed documentation, tutorials, and API reference: - [Building from Source](https://cvcuda.github.io/CV-CUDA/installation.html#building-from-source) - Custom builds - [Prerequisites](https://cvcuda.github.io/CV-CUDA/installation.html#prerequisites) - System requirements -- **[Interoperability Guide](https://cvcuda.github.io/CV-CUDA/interoperability.html)** - Using CV-CUDA with other frameworks +- **[Interoperability Guide](https://cvcuda.github.io/CV-CUDA/samples/interoperability.html)** - Using CV-CUDA with other frameworks diff --git a/samples/applications/hello_world.py b/samples/applications/hello_world.py index 3a00c52e9..5617bb5ce 100644 --- a/samples/applications/hello_world.py +++ b/samples/applications/hello_world.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -184,7 +184,7 @@ def main() -> None: # 1. Load the images into CV-CUDA decoder = nvimgcodec.Decoder() images: list[nvimgcodec.Image] = [ - decoder.decode(str(i_path)) for i_path in input_paths + decoder.read(str(i_path)) for i_path in input_paths ] tensors: list[cvcuda.Tensor] = [ cvcuda.as_tensor(image, "HWC") for image in images diff --git a/samples/common.py b/samples/common.py index 757205025..681feff86 100644 --- a/samples/common.py +++ b/samples/common.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -145,6 +145,148 @@ def cuda_memcpy_d2h( ) +def _tensor_copy_geometry(tensor: cvcuda.Tensor) -> tuple[int, int, int, int]: + """ + Internal: Derive a 2D pitched-copy geometry from a strided device tensor. + + CVCUDA pads each image row to a hardware alignment boundary, so the row + stride is generally larger than the packed row width. A flat copy of + ``shape``-many packed bytes would therefore shear the image. This helper + returns the parameters for a ``cudaMemcpy2D`` that honours the pitch. + + It works directly from the ``__cuda_array_interface__`` strides, so it + handles any layout with a single padded stride level (HWC, NHWC, NCHW, CHW, + 1-D, ...): it finds the contiguous inner block (the "row") and treats every + outer dimension as additional rows at the row pitch. + + Args: + tensor: CVCUDA tensor. + + Returns: + (device_ptr, dpitch_bytes, width_bytes, num_rows) + """ + iface = tensor.cuda().__cuda_array_interface__ + shape = tuple(tensor.shape) + itemsize = tensor.dtype.itemsize + strides = iface.get("strides") + + nbytes = itemsize + for dim in shape: + nbytes *= dim + + # No strides reported => fully contiguous => a single packed row. + if strides is None or len(shape) == 0: + return iface["data"][0], nbytes, nbytes, 1 + + # Walk from the innermost dimension outward while it stays packed; the run + # of packed dimensions forms one contiguous "row". The first dimension that + # breaks contiguity carries the (padded) row pitch. + expected = itemsize + row_bytes = itemsize + pitch_axis = -1 + for axis in range(len(shape) - 1, -1, -1): + if strides[axis] == expected: + row_bytes *= shape[axis] + expected *= shape[axis] + else: + pitch_axis = axis + break + + if pitch_axis < 0: + # Fully contiguous: copy as one row. + return iface["data"][0], nbytes, nbytes, 1 + + width_bytes = row_bytes + dpitch = strides[pitch_axis] + num_rows = 1 + for dim in shape[: pitch_axis + 1]: + num_rows *= dim + + return iface["data"][0], dpitch, width_bytes, num_rows + + +def download_tensor(tensor: cvcuda.Tensor) -> np.ndarray: + """ + Copy an image-like device tensor to a contiguous host numpy array. + + Honours the device row pitch (CVCUDA pads rows to an alignment boundary), + unlike :func:`cuda_memcpy_d2h` which assumes a packed layout and would shear + a padded image. + + Args: + tensor: CVCUDA tensor (any layout with a single padded stride level). + + Returns: + Contiguous host array with the tensor's shape and dtype. + """ + host = np.empty(tuple(tensor.shape), dtype=np.dtype(tensor.dtype)) + device_ptr, dpitch, width_bytes, num_rows = _tensor_copy_geometry(tensor) + (err,) = cudart.cudaMemcpy2D( + host.ctypes.data, + width_bytes, # host is packed: spitch == width + device_ptr, + dpitch, + width_bytes, + num_rows, + cudart.cudaMemcpyKind.cudaMemcpyDeviceToHost, + ) + if err != cudart.cudaError_t.cudaSuccess: + raise RuntimeError(f"cudaMemcpy2D (D2H) failed: {err}") + return host + + +def upload_tensor(host_array: np.ndarray, tensor: cvcuda.Tensor) -> None: + """ + Copy a contiguous host numpy array into an image-like device tensor. + + Honours the device row pitch, so it is safe to write into a freshly + allocated (and therefore possibly padded) tensor, unlike + :func:`cuda_memcpy_h2d` which assumes a packed layout. + + Args: + host_array: Contiguous host array matching the tensor's shape/dtype. + tensor: CVCUDA tensor (any layout with a single padded stride level). + """ + if not host_array.flags.c_contiguous: + raise ValueError("Host array must be contiguous") + + # The copy is a raw byte memcpy, so element sizes must agree; a mismatch + # would silently shear the data. We deliberately do NOT compare full + # shapes: padded device tensors and reshaped/squeezed host views legitimately + # have differing shape tuples, and correctness only depends on the byte count. + if host_array.dtype.itemsize != tensor.dtype.itemsize: + raise ValueError( + f"Host dtype {host_array.dtype} (itemsize {host_array.dtype.itemsize}) " + f"is incompatible with tensor dtype {tensor.dtype} " + f"(itemsize {tensor.dtype.itemsize})" + ) + + device_ptr, dpitch, width_bytes, num_rows = _tensor_copy_geometry(tensor) + + # Guard against a host buffer that is too small for the geometry-derived + # copy: cudaMemcpy2D reads width_bytes * num_rows bytes from the (packed) + # host pointer, so a smaller buffer would read past its end. + expected_nbytes = width_bytes * num_rows + if host_array.nbytes < expected_nbytes: + raise ValueError( + f"Host buffer is too small for the device tensor: host has " + f"{host_array.nbytes} bytes but the copy needs {expected_nbytes} " + f"({width_bytes} width bytes x {num_rows} rows)" + ) + + (err,) = cudart.cudaMemcpy2D( + device_ptr, + dpitch, + host_array.ctypes.data, + width_bytes, # host is packed: spitch == width + width_bytes, + num_rows, + cudart.cudaMemcpyKind.cudaMemcpyHostToDevice, + ) + if err != cudart.cudaError_t.cudaSuccess: + raise RuntimeError(f"cudaMemcpy2D (H2D) failed: {err}") + + def zero_copy_split(batch_tensor: cvcuda.Tensor) -> list[cvcuda.Tensor]: """ Split a batch tensor into a list of individual tensors using zero-copy. @@ -216,14 +358,19 @@ def read_image( """ Read an image from a file and return a CVCUDA tensor. + nvImageCodec decodes to interleaved RGB by default, so the returned tensor + is an RGB8 HWC tensor. Downstream operators that need an explicit format + (e.g. ``cvcuda.pillowresize`` with ``cvcuda.Format.RGB8``) rely on this + contract. + Args: file: Path to the image file. Returns: - CVCUDA tensor. + RGB8 HWC CVCUDA tensor. """ decoder = nvimgcodec.Decoder() - nvc_img = decoder.decode(str(file)) + nvc_img = decoder.read(str(file)) return cvcuda.as_tensor(nvc_img, "HWC") @@ -286,7 +433,7 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: args=(torch.randn(1, *input_shape),), f=output, export_params=True, - opset_version=13, + opset_version=18, do_constant_folding=True, input_names=["input"], output_names=["output"], @@ -334,7 +481,7 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: args=(torch.randn(1, *input_shape),), f=output, export_params=True, - opset_version=13, + opset_version=18, do_constant_folding=True, input_names=["input"], output_names=["output"], @@ -451,7 +598,7 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: args=(dummy_input,), f=temp_output, export_params=True, - opset_version=13, + opset_version=18, do_constant_folding=True, input_names=["images"], output_names=["raw_output"], @@ -588,8 +735,7 @@ def engine_from_onnx( trt.init_libnvinfer_plugins(None, "") trt_logger = trt.Logger(trt.Logger.WARNING) builder = trt.Builder(trt_logger) - network_flags = 1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH) - network = builder.create_network(network_flags) + network = builder.create_network() parser = trt.OnnxParser(network, trt_logger) with onnx.open("rb") as model: diff --git a/samples/install_interop_dependencies.sh b/samples/install_interop_dependencies.sh deleted file mode 100755 index bc0ed2fc6..000000000 --- a/samples/install_interop_dependencies.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/bin/bash -e - -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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. - -# This script installs all the dependencies required to run CV-CUDA interoperability samples. -# It detects the CUDA version and installs appropriate packages. - -# Check CUDA version -if command -v nvcc >/dev/null 2>&1; then - CUDA_VERSION=$(nvcc --version | grep "release" | awk -F'release ' '{print $2}' | cut -d',' -f1) - CUDA_MAJOR_VERSION=$(echo "$CUDA_VERSION" | cut -d. -f1) - - if [ "$CUDA_MAJOR_VERSION" -eq 12 ] || [ "$CUDA_MAJOR_VERSION" -eq 13 ]; then - echo "CUDA $CUDA_MAJOR_VERSION is installed." - else - echo "Unknown/Unsupported CUDA version." - exit 1 - fi -else - echo "Error: CUDA is not installed." - exit 1 -fi - -set -e # Exit script if any command fails - -echo "Installing Python dependencies for CV-CUDA interoperability samples..." - -# Get script directory -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" - -# Create virtual environment if it doesn't exist -if [ ! -d "$SCRIPT_DIR/venv_samples" ]; then - echo "Creating virtual environment..." - python3 -m venv "$SCRIPT_DIR/venv_samples" -fi - -# Activate virtual environment -echo "Activating virtual environment..." -source "$SCRIPT_DIR/venv_samples/bin/activate" - -# Upgrade pip -echo "Upgrading pip..." -python3 -m pip install --upgrade pip - -# Install interoperability dependencies for the detected CUDA version -echo "Installing interoperability dependencies for CUDA $CUDA_MAJOR_VERSION..." -python3 -m pip install -r "$SCRIPT_DIR/requirements_interop_cu${CUDA_MAJOR_VERSION}.txt" - -echo "" -echo "Interoperability dependencies installation complete!" -echo "" -echo "To activate the virtual environment, run:" -echo " source $SCRIPT_DIR/venv_samples/bin/activate" -echo "" -echo "Then you can run interoperability samples:" -echo " python3 interoperability/pytorch_interop.py" -echo " python3 interoperability/cupy_interop.py" -echo "" diff --git a/samples/install_samples_dependencies.sh b/samples/install_samples_dependencies.sh index da539921c..2aa2e6f67 100755 --- a/samples/install_samples_dependencies.sh +++ b/samples/install_samples_dependencies.sh @@ -1,6 +1,6 @@ #!/bin/bash -e -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,14 +20,9 @@ # Check CUDA version. Begin by checking if nvcc command exists. if command -v nvcc >/dev/null 2>&1; then - # Get CUDA version from nvcc output CUDA_VERSION=$(nvcc --version | grep "release" | awk '{print $5}' | sed 's/,//') - - # Extract major version number CUDA_MAJOR_VERSION=$(echo "$CUDA_VERSION" | cut -d. -f1) - - # Check major version to determine CUDA version - if [ "$CUDA_MAJOR_VERSION" -eq 12 ] || [ "$CUDA_MAJOR_VERSION" -eq 13 ]; then + if [[ "$CUDA_MAJOR_VERSION" -eq 12 ]] || [[ "$CUDA_MAJOR_VERSION" -eq 13 ]]; then echo "CUDA $CUDA_MAJOR_VERSION is installed." else echo "Unknown/Unsupported CUDA version." @@ -38,30 +33,44 @@ else exit 1 fi -set -e # Exit script if any command fails - echo "Installing Python dependencies for CV-CUDA samples..." -# Get script directory SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" # Create virtual environment if it doesn't exist -if [ ! -d "$SCRIPT_DIR/venv_samples" ]; then +if [[ ! -d "$SCRIPT_DIR/venv_samples" ]]; then echo "Creating virtual environment..." - python3 -m venv "$SCRIPT_DIR/venv_samples" + python3 -m venv --system-site-packages "$SCRIPT_DIR/venv_samples" fi -# Activate virtual environment -echo "Activating virtual environment..." source "$SCRIPT_DIR/venv_samples/bin/activate" - -# Upgrade pip -echo "Upgrading pip..." python3 -m pip install --upgrade pip -# Install sample dependencies for the detected CUDA version -echo "Installing sample dependencies for CUDA $CUDA_MAJOR_VERSION..." -python3 -m pip install -r "$SCRIPT_DIR/requirements_samples_cu${CUDA_MAJOR_VERSION}.txt" +# Check that the requirements file has been generated. +REQ_FILE="$SCRIPT_DIR/requirements.samples.cu${CUDA_MAJOR_VERSION}.txt" +if [[ ! -f "$REQ_FILE" ]]; then + echo "Error: $REQ_FILE not found." >&2 + echo "Generate it first by running from the repository root:" >&2 + echo " bash generate_requirements.sh" >&2 + exit 1 +fi + +# Install sample dependencies for the detected CUDA version. +# requirements.samples.cu{12,13}.txt includes torch, torchvision, and all other deps. +# If CV-CUDA is already installed (e.g. from a wheel), filter it out to avoid reinstalling. +FILTERED_REQ=$(mktemp "$SCRIPT_DIR/requirements_filtered.XXXXXX.txt") +trap 'rm -f "$FILTERED_REQ"' EXIT +cp "$REQ_FILE" "$FILTERED_REQ" + +if python3 -m pip list 2>/dev/null | grep -q "cvcuda-cu${CUDA_MAJOR_VERSION}"; then + echo "CV-CUDA is already installed, filtering it from requirements..." + grep -v "^cvcuda-cu" "$FILTERED_REQ" > "$FILTERED_REQ.tmp" + mv "$FILTERED_REQ.tmp" "$FILTERED_REQ" +fi + +python3 -m pip install -r "$FILTERED_REQ" +rm "$FILTERED_REQ" +trap - EXIT echo "" echo "Python dependencies installation complete!" @@ -72,4 +81,5 @@ echo "" echo "Then you can run samples from the samples directory:" echo " python3 operators/label.py" echo " python3 applications/classification.py" +echo " python3 interoperability/pytorch_interop.py" echo "" diff --git a/samples/interoperability/pynvvideocodec_interop.py b/samples/interoperability/pynvvideocodec_interop.py index 8f933354f..f4c62d717 100644 --- a/samples/interoperability/pynvvideocodec_interop.py +++ b/samples/interoperability/pynvvideocodec_interop.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,20 +22,36 @@ from __future__ import annotations from pathlib import Path +import sys import cvcuda -import PyNvVideoCodec as nvc +try: + import PyNvVideoCodec as nvc +except ModuleNotFoundError as error: + if error.name != "PyNvVideoCodec" or sys.version_info < (3, 13): + raise + nvc = None # docs_tag: end_imports def main(): - """PyNvVideoCodec <-> CV-CUDA interoperability example.""" + """PyNvVideoCodec <-> CV-CUDA interoperability example. + + This sample demonstrates video decoding, CV-CUDA processing, + and video encoding (if hardware video encoder (NVENC) is available) + """ + if nvc is None: + python_version = f"{sys.version_info[0]}.{sys.version_info[1]}" + print( + "Skipping PyNvVideoCodec interoperability sample: " + f"PyNvVideoCodec is unavailable for Python {python_version}." + ) + return - # 1. Define the demuxer, decoder, encoder and video path # docs_tag: begin_init_pynvvideocodec - # setup paths + # Setup paths video_path = ( Path(__file__).parent.parent / "assets" @@ -48,7 +64,7 @@ def main(): output_path = output_dir / "pexels-chiel-slotman-640x480.h264" device_id = 0 - # create decoder and encoder + # Create decoder decoder = nvc.SimpleDecoder( str(video_path), gpu_id=device_id, @@ -58,16 +74,10 @@ def main(): max_height=1080, use_device_memory=True, ) - encoder = nvc.CreateEncoder( - width=640, - height=480, - fmt="NV12", - codec="h264", - usecpuinputbuffer=False, - ) + # docs_tag: end_init_pynvvideocodec - # 2. Read video and store frames in CV-CUDA tensors + # Read video and store frames in CV-CUDA tensors # Resize and cvtcolor on the frames as we read the images # PyNvVideoCodec will re-use the same buffers, so we cannot # use zero-copy as_tensor and maintain the original data (without copying) @@ -92,14 +102,26 @@ def main(): frame_idx += 1 # docs_tag: end_read_and_process_video - # 3. Encode frames # docs_tag: begin_encode_frames + # Encode frames (only if encoder hardware is available) + try: + encoder = nvc.CreateEncoder( + width=640, + height=480, + fmt="NV12", + codec="h264", + usecpuinputbuffer=False, + ) + except Exception as e: + print(f"Encoder not available - will skip encoding step: {e}") + return with output_path.open("wb") as f: for frame in processed_frames: bitstream = encoder.Encode(frame) f.write(bytearray(bitstream)) bitstream = encoder.EndEncode() f.write(bytearray(bitstream)) + print(f"Successfully encoded video to {output_path}") # docs_tag: end_encode_frames diff --git a/samples/object_cache/threads.py b/samples/object_cache/threads.py index 7e037a45b..b5155e0d9 100644 --- a/samples/object_cache/threads.py +++ b/samples/object_cache/threads.py @@ -17,7 +17,7 @@ # docs-start: main import threading - +import time import cvcuda import numpy as np @@ -39,6 +39,31 @@ def main() -> None: thread.start() thread.join() + # WORKAROUND: Race condition between Python thread.join() and C++ thread-local destructors + # + # Root Cause: + # Python's thread.join() returns when the Python thread function completes, but + # C++ thread-local objects (like the Cache instance) are destroyed asynchronously + # by the C++ runtime AFTER Python considers the thread "joined". Without this sleep, + # the main() function exits immediately after join(), triggering the main thread's + # Cache destructor while the worker thread's Cache destructor is still running. + # Both destructors then concurrently attempt to clean up CUDA resources (acquire GIL, + # destroy events, deallocate memory), causing race conditions and segfaults. + # + # The sleep keeps the main thread alive longer, ensuring temporal separation between + # the worker's destructor completing and the main thread's destructor starting. + # + # Why This Is Not An Issue In Normal Usage: + # - Real applications use long-lived thread pools that outlive individual operations + # - Main threads typically continue running after worker threads complete (serving + # requests, processing more data, etc.) providing natural timing separation + # - This test has the worst-case scenario: main thread exits immediately after + # worker joins, with no other work keeping it alive + # + # A proper fix would require C++ synchronization primitives (condition variables) to + # explicitly wait for C++ thread-local destructors, but adds complexity for a rare edge case. + time.sleep(0.1) # Keep main thread alive while worker's C++ destructor completes + # docs-end: main diff --git a/samples/operators/adaptivethreshold.py b/samples/operators/adaptivethreshold.py new file mode 100644 index 000000000..028a8bbd0 --- /dev/null +++ b/samples/operators/adaptivethreshold.py @@ -0,0 +1,81 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. +"""Simple Adaptive Threshold example with CVCUDA.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +import cvcuda + +common_dir = Path(__file__).parent.parent +sys.path.append(str(common_dir)) + +from common import ( # noqa: E402 + parse_image_args, + read_image, + write_image, +) # noqa: E402 + + +def main() -> None: + """Apply adaptive threshold to an image with CVCUDA.""" + # docs_tag: begin_main + args: argparse.Namespace = parse_image_args("cat_adaptivethreshold.jpg") + # docs_tag: begin_read_image + input_image: cvcuda.Tensor = read_image(args.input) + # docs_tag: end_read_image + + # docs_tag: begin_adaptivethreshold_setup + # adaptivethreshold requires a single-channel (grayscale) U8 input. + # Stack the HWC image into a batch (NHWC) so cvtcolor can operate on it, + # then convert RGB to grayscale. + nhwc_image: cvcuda.Tensor = cvcuda.stack([input_image]) + gray_nhwc: cvcuda.Tensor = cvcuda.cvtcolor( + nhwc_image, cvcuda.ColorConversion.RGB2GRAY + ) + # docs_tag: end_adaptivethreshold_setup + + # docs_tag: begin_adaptivethreshold + # Apply GAUSSIAN_C adaptive threshold: for each pixel, the threshold is the + # Gaussian-weighted average of the block_size x block_size neighbourhood minus c. + # max_value=255 means foreground pixels are set to full white. + # block_size must be an odd integer >= 3; c is subtracted from the local mean. + thresholded: cvcuda.Tensor = cvcuda.adaptivethreshold( + src=gray_nhwc, + max_value=255.0, + adaptive_method=cvcuda.AdaptiveThresholdType.GAUSSIAN_C, + threshold_type=cvcuda.ThresholdType.BINARY, + block_size=11, + c=2.0, + ) + # docs_tag: end_adaptivethreshold + + # The result is NHWC with a single channel. Replicate the single channel + # to RGB on-device with cvtcolor, then reshape to HWC so write_image (which + # expects 3-channel uint8 HWC) can save it. Staying on-device avoids any + # host round-trip and the row-pitch handling it would require. + rgb_nhwc: cvcuda.Tensor = cvcuda.cvtcolor( + thresholded, cvcuda.ColorConversion.GRAY2RGB + ) + h, w = rgb_nhwc.shape[1], rgb_nhwc.shape[2] + write_image(rgb_nhwc.reshape((h, w, 3), "HWC"), args.output) + # docs_tag: end_main + + +if __name__ == "__main__": + main() diff --git a/samples/operators/advcvtcolor.py b/samples/operators/advcvtcolor.py new file mode 100644 index 000000000..0bb2fe72a --- /dev/null +++ b/samples/operators/advcvtcolor.py @@ -0,0 +1,64 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. +"""Simple Advanced Color Conversion example with CVCUDA.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +import cvcuda + +common_dir = Path(__file__).parent.parent +sys.path.append(str(common_dir)) + +from common import parse_image_args, read_image, write_image # noqa: E402 + + +def main() -> None: + """Convert an image from RGB to YUV and back to RGB using advanced color conversion.""" + # docs_tag: begin_main + args: argparse.Namespace = parse_image_args("cat_advcvtcolor.jpg") + # docs_tag: begin_read_image + input_image: cvcuda.Tensor = read_image(args.input) + # docs_tag: end_read_image + + # docs_tag: begin_advcvtcolor + # 1. Convert the RGB image to YUV using the BT.709 color specification. + # BT.709 is the standard for HDTV content and is a common choice for + # high-quality color-space transformations. + yuv_image: cvcuda.Tensor = cvcuda.advcvtcolor( + input_image, + cvcuda.ColorConversion.RGB2YUV, + cvcuda.ColorSpec.BT709, + ) + + # 2. Convert the YUV image back to RGB so the result is a viewable image. + # Using the same color specification (BT.709) ensures a round-trip that + # closely reproduces the original colors. + output_image: cvcuda.Tensor = cvcuda.advcvtcolor( + yuv_image, + cvcuda.ColorConversion.YUV2RGB, + cvcuda.ColorSpec.BT709, + ) + + write_image(output_image, args.output) + # docs_tag: end_advcvtcolor + # docs_tag: end_main + + +if __name__ == "__main__": + main() diff --git a/samples/operators/averageblur.py b/samples/operators/averageblur.py new file mode 100644 index 000000000..9a36e1d22 --- /dev/null +++ b/samples/operators/averageblur.py @@ -0,0 +1,55 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. +"""Simple Average Blur example with CVCUDA.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +import cvcuda + +common_dir = Path(__file__).parent.parent +sys.path.append(str(common_dir)) + +from common import parse_image_args, read_image, write_image # noqa: E402 + + +def main() -> None: + """Apply average blur to an image with CVCUDA.""" + # docs_tag: begin_main + args: argparse.Namespace = parse_image_args("cat_averageblur.jpg") + # docs_tag: begin_read_image + input_image: cvcuda.Tensor = read_image(args.input) + # docs_tag: end_read_image + + # docs_tag: begin_averageblur + # Apply a 7x7 average (box) blur to smooth the image. + # kernel_anchor=[-1, -1] places the anchor at the kernel center, which is + # the conventional choice for symmetric filters. + output_image: cvcuda.Tensor = cvcuda.averageblur( + input_image, + kernel_size=[7, 7], + kernel_anchor=[-1, -1], + border=cvcuda.Border.REFLECT101, + ) + write_image(output_image, args.output) + # docs_tag: end_averageblur + # docs_tag: end_main + + +if __name__ == "__main__": + main() diff --git a/samples/operators/bilateral_filter.py b/samples/operators/bilateral_filter.py new file mode 100644 index 000000000..66516a8e0 --- /dev/null +++ b/samples/operators/bilateral_filter.py @@ -0,0 +1,59 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. +"""Simple Bilateral Filter example with CVCUDA.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +import cvcuda + +common_dir = Path(__file__).parent.parent +sys.path.append(str(common_dir)) + +from common import parse_image_args, read_image, write_image # noqa: E402 + + +def main() -> None: + """Apply bilateral filter to an image with CVCUDA.""" + # docs_tag: begin_main + args: argparse.Namespace = parse_image_args("cat_bilateral_filter.jpg") + # docs_tag: begin_read_image + input_image: cvcuda.Tensor = read_image(args.input) + # docs_tag: end_read_image + + # docs_tag: begin_bilateral_filter + # Apply bilateral filter: preserves edges while smoothing flat regions. + # - diameter: pixel neighborhood size (larger = stronger smoothing but slower) + # - sigma_color: color space standard deviation; larger values allow more + # dissimilar colors to be blended together, weakening edge preservation + # - sigma_space: spatial standard deviation; larger values mean pixels + # farther away influence each other, similar to a Gaussian blur radius + output_image: cvcuda.Tensor = cvcuda.bilateral_filter( + input_image, + diameter=9, + sigma_color=75, + sigma_space=75, + border=cvcuda.Border.REFLECT, + ) + write_image(output_image, args.output) + # docs_tag: end_bilateral_filter + # docs_tag: end_main + + +if __name__ == "__main__": + main() diff --git a/samples/operators/bndbox.py b/samples/operators/bndbox.py new file mode 100644 index 000000000..87fd06e86 --- /dev/null +++ b/samples/operators/bndbox.py @@ -0,0 +1,90 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. +"""Simple Bounding Boxes example with CVCUDA.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +import cvcuda + +common_dir = Path(__file__).parent.parent +sys.path.append(str(common_dir)) + +from common import parse_image_args, read_image, write_image # noqa: E402 + + +def main() -> None: + """Draw colored bounding boxes on an image with CVCUDA.""" + # docs_tag: begin_main + args: argparse.Namespace = parse_image_args("cat_bndbox.jpg") + # docs_tag: begin_read_image + input_image: cvcuda.Tensor = read_image(args.input) + # docs_tag: end_read_image + + # docs_tag: begin_bndbox_setup + # bndbox requires a batched NHWC tensor, so wrap the single HWC image in a batch. + # cvcuda.stack promotes [HWC, ...] -> NHWC without copying pixel data. + nhwc_image: cvcuda.Tensor = cvcuda.stack([input_image]) + + # Describe three axis-aligned boxes to highlight features on the cat image: + # Red box – face region (top-centre) + # Green box – body torso + # Blue box – tail / lower body + # Each BndBoxI takes (x, y, width, height), border thickness, border colour + # (RGB), and fill colour (RGBA). A fill alpha of 0 leaves the interior + # pixels untouched, so only the border is drawn. + bboxes = cvcuda.BndBoxesI( + boxes=[ + [ + cvcuda.BndBoxI( + box=(260, 60, 200, 190), + thickness=4, + borderColor=(255, 80, 0), + fillColor=(255, 80, 0, 0), + ), + cvcuda.BndBoxI( + box=(180, 280, 360, 260), + thickness=4, + borderColor=(0, 220, 60), + fillColor=(0, 220, 60, 0), + ), + cvcuda.BndBoxI( + box=(420, 500, 220, 180), + thickness=4, + borderColor=(30, 120, 255), + fillColor=(30, 120, 255, 0), + ), + ], + ] + ) + # docs_tag: end_bndbox_setup + + # docs_tag: begin_bndbox + # Draw the bounding boxes in-place on a copy of the source image. + # The output tensor has the same shape, dtype, and layout as the input. + out_nhwc: cvcuda.Tensor = cvcuda.bndbox(nhwc_image, bboxes) + + # Squeeze the batch dimension back to HWC so write_image can encode it. + out_hwc: cvcuda.Tensor = out_nhwc.reshape(out_nhwc.shape[1:], "HWC") + write_image(out_hwc, args.output) + # docs_tag: end_bndbox + # docs_tag: end_main + + +if __name__ == "__main__": + main() diff --git a/samples/operators/boxblur.py b/samples/operators/boxblur.py new file mode 100644 index 000000000..91b058079 --- /dev/null +++ b/samples/operators/boxblur.py @@ -0,0 +1,77 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. +"""Simple Box Blur example with CVCUDA.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +import cvcuda + +common_dir = Path(__file__).parent.parent +sys.path.append(str(common_dir)) + +from common import parse_image_args, read_image, write_image # noqa: E402 + + +def main() -> None: + """Box-blur rectangular regions of an image with CVCUDA.""" + # docs_tag: begin_main + args: argparse.Namespace = parse_image_args("cat_boxblur.jpg") + # docs_tag: begin_read_image + input_image: cvcuda.Tensor = read_image(args.input) + # docs_tag: end_read_image + + # docs_tag: begin_boxblur_setup + # boxblur operates on NHWC batches, so wrap the HWC image in a batch dimension. + # cvcuda.stack adds a leading N=1 dimension and returns an NHWC tensor. + nhwc_image: cvcuda.Tensor = cvcuda.stack([input_image]) + h, w = nhwc_image.shape[1], nhwc_image.shape[2] + + # Each BlurBoxI specifies (x, y, width, height) in pixel coordinates and the + # square box-filter kernel size. Larger kernelSize → stronger blur effect. + # Three overlapping boxes of increasing size cover distinct regions of the cat. + bboxes = cvcuda.BlurBoxesI( + boxes=[ + [ + # Upper-left patch — moderate blur + cvcuda.BlurBoxI(box=(w // 8, h // 8, w // 5, h // 5), kernelSize=21), + # Centre of the image — strong blur (e.g. face anonymisation) + cvcuda.BlurBoxI(box=(w // 4, h // 4, w // 2, h // 2), kernelSize=45), + # Lower-right corner — light blur + cvcuda.BlurBoxI( + box=(w * 3 // 4, h * 3 // 4, w // 6, h // 6), kernelSize=11 + ), + ] + ] + ) + # docs_tag: end_boxblur_setup + + # docs_tag: begin_boxblur + # Apply box (mean) blur to the specified rectangles. + # Pixels outside the declared boxes are copied through unchanged. + blurred_nhwc: cvcuda.Tensor = cvcuda.boxblur(src=nhwc_image, bboxes=bboxes) + + # Remove the batch dimension to get back an HWC tensor for writing. + blurred_hwc: cvcuda.Tensor = blurred_nhwc.reshape(blurred_nhwc.shape[1:], "HWC") + write_image(blurred_hwc, args.output) + # docs_tag: end_boxblur + # docs_tag: end_main + + +if __name__ == "__main__": + main() diff --git a/samples/operators/brightness_contrast.py b/samples/operators/brightness_contrast.py new file mode 100644 index 000000000..4c94dcd0f --- /dev/null +++ b/samples/operators/brightness_contrast.py @@ -0,0 +1,86 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. +"""Simple Brightness Contrast example with CVCUDA.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +import cvcuda +import numpy as np + +common_dir = Path(__file__).parent.parent +sys.path.append(str(common_dir)) + +from common import ( # noqa: E402 + parse_image_args, + read_image, + write_image, + cuda_memcpy_h2d, +) + + +def main() -> None: + """Apply brightness and contrast adjustment to an image with CVCUDA.""" + # docs_tag: begin_main + args: argparse.Namespace = parse_image_args("cat_brightness_contrast.jpg") + # docs_tag: begin_read_image + input_image: cvcuda.Tensor = read_image(args.input) + # docs_tag: end_read_image + + # docs_tag: begin_brightness_contrast_setup + # The brightness_contrast operator expects per-image scalar parameters + # supplied as 1-D tensors with layout "N" (one element per image in the batch). + # brightness: multiplicative gain applied to the pixel values (>1 brightens). + # contrast: multiplier around contrast_center (<1 compresses, >1 expands). + # brightness_shift: additive offset added after brightness scaling. + # contrast_center: the pivot value around which contrast is computed (default 0.0). + brightness_host = np.array([1.5], dtype=np.float32) + contrast_host = np.array([1.4], dtype=np.float32) + brightness_shift_host = np.array([20.0], dtype=np.float32) + contrast_center_host = np.array([127.0], dtype=np.float32) + + brightness = cvcuda.Tensor((1,), dtype=cvcuda.Type.F32, layout="N") + contrast = cvcuda.Tensor((1,), dtype=cvcuda.Type.F32, layout="N") + brightness_shift = cvcuda.Tensor((1,), dtype=cvcuda.Type.F32, layout="N") + contrast_center = cvcuda.Tensor((1,), dtype=cvcuda.Type.F32, layout="N") + + cuda_memcpy_h2d(brightness_host, brightness.cuda()) + cuda_memcpy_h2d(contrast_host, contrast.cuda()) + cuda_memcpy_h2d(brightness_shift_host, brightness_shift.cuda()) + cuda_memcpy_h2d(contrast_center_host, contrast_center.cuda()) + # docs_tag: end_brightness_contrast_setup + + # docs_tag: begin_brightness_contrast + # Apply brightness and contrast adjustment. + # The operator processes the HWC tensor directly; no batch dimension is required. + # The result has the same shape, layout, and dtype as the input (uint8 HWC), + # so it can be written directly to disk as a viewable JPEG. + output_image: cvcuda.Tensor = cvcuda.brightness_contrast( + src=input_image, + brightness=brightness, + contrast=contrast, + brightness_shift=brightness_shift, + contrast_center=contrast_center, + ) + write_image(output_image, args.output) + # docs_tag: end_brightness_contrast + # docs_tag: end_main + + +if __name__ == "__main__": + main() diff --git a/samples/operators/center_crop.py b/samples/operators/center_crop.py new file mode 100644 index 000000000..f168fc55b --- /dev/null +++ b/samples/operators/center_crop.py @@ -0,0 +1,58 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. +"""Simple Center Crop example with CVCUDA.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +import cvcuda + +common_dir = Path(__file__).parent.parent +sys.path.append(str(common_dir)) + +from common import parse_image_args, read_image, write_image # noqa: E402 + + +def main() -> None: + """Center-crop an image with CVCUDA.""" + # docs_tag: begin_main + args: argparse.Namespace = parse_image_args("cat_center_crop.jpg") + # docs_tag: begin_read_image + input_image: cvcuda.Tensor = read_image(args.input) + # docs_tag: end_read_image + + # docs_tag: begin_center_crop_setup + # Derive a square crop size that fits within the image dimensions. + # The operator requires [crop_height, crop_width] as a Python list. + h, w = input_image.shape[0], input_image.shape[1] + crop_h = min(args.height, h) + crop_w = min(args.width, w) + crop_size = [crop_h, crop_w] + # docs_tag: end_center_crop_setup + + # docs_tag: begin_center_crop + # cvcuda.center_crop symmetrically extracts a rectangular region from the + # centre of the image, so no manual coordinate arithmetic is required. + output_image: cvcuda.Tensor = cvcuda.center_crop(input_image, crop_size) + write_image(output_image, args.output) + # docs_tag: end_center_crop + # docs_tag: end_main + + +if __name__ == "__main__": + main() diff --git a/samples/operators/channelreorder.py b/samples/operators/channelreorder.py new file mode 100644 index 000000000..115af7cf7 --- /dev/null +++ b/samples/operators/channelreorder.py @@ -0,0 +1,81 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. +"""Simple Channel Reorder example with CVCUDA.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +import cvcuda +import numpy as np + +common_dir = Path(__file__).parent.parent +sys.path.append(str(common_dir)) + +from common import ( # noqa: E402 + parse_image_args, + read_image, + write_image, + cuda_memcpy_h2d, +) + + +def main() -> None: + """Reorder channels of an image with CVCUDA.""" + # docs_tag: begin_main + args: argparse.Namespace = parse_image_args("cat_channelreorder.jpg") + # docs_tag: begin_read_image + input_image: cvcuda.Tensor = read_image(args.input) + # docs_tag: end_read_image + + # docs_tag: begin_channelreorder_setup + # channelreorder operates on ImageBatchVarShape rather than plain tensors, + # so we wrap the HWC tensor as a cvcuda.Image and add it to a batch. + # cvcuda.as_image ties the buffer lifetime to the Image object. + cv_image: cvcuda.Image = cvcuda.as_image( + input_image.cuda(), format=cvcuda.Format.RGB8 + ) + + batch = cvcuda.ImageBatchVarShape(1) + batch.pushback(cv_image) + + # Build the orders tensor with layout "NC" (num_images × num_channels). + # Each row lists, for every output channel, which input channel index to read. + # [2, 1, 0] maps R→B, G→G, B→R — a standard RGB-to-BGR channel swap. + order_data = np.array([[2, 1, 0]], dtype=np.int32) + orders: cvcuda.Tensor = cvcuda.Tensor((1, 3), dtype=np.int32, layout="NC") + cuda_memcpy_h2d(order_data, orders.cuda()) + # docs_tag: end_channelreorder_setup + + # docs_tag: begin_channelreorder + # Run the channel-reorder operator. The output is an ImageBatchVarShape + # of the same format (RGB8) — channels are permuted in-place on the GPU. + out_batch: cvcuda.ImageBatchVarShape = cvcuda.channelreorder(batch, orders) + + # Extract the single result image from the batch and convert back to a + # cvcuda.Tensor in HWC layout so write_image can encode it. + for out_image in out_batch: + out_tensor: cvcuda.Tensor = cvcuda.as_tensor(out_image.cuda(), "HWC") + break + # docs_tag: end_channelreorder + + write_image(out_tensor, args.output) + # docs_tag: end_main + + +if __name__ == "__main__": + main() diff --git a/samples/operators/clahe.py b/samples/operators/clahe.py new file mode 100644 index 000000000..c7fb5a6f5 --- /dev/null +++ b/samples/operators/clahe.py @@ -0,0 +1,79 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. +"""Simple CLAHE example with CVCUDA.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +import cvcuda + +common_dir = Path(__file__).parent.parent +sys.path.append(str(common_dir)) + +from common import ( # noqa: E402 + parse_image_args, + read_image, + write_image, +) + + +def main() -> None: + """Apply CLAHE contrast enhancement to an image with CVCUDA.""" + # docs_tag: begin_main + args: argparse.Namespace = parse_image_args("cat_clahe.jpg") + # docs_tag: begin_read_image + input_image: cvcuda.Tensor = read_image(args.input) + # docs_tag: end_read_image + + # docs_tag: begin_clahe_setup + # CLAHE requires a single-channel (grayscale) U8 tensor. + # Convert the RGB input to grayscale using cvtcolor, producing HWC with C=1. + nhwc_image: cvcuda.Tensor = cvcuda.stack([input_image]) + gray_nhwc: cvcuda.Tensor = cvcuda.cvtcolor( + nhwc_image, cvcuda.ColorConversion.RGB2GRAY + ) + # docs_tag: end_clahe_setup + + # docs_tag: begin_clahe + # Apply Contrast Limited Adaptive Histogram Equalization (CLAHE). + # clip_limit caps the contrast amplification per tile to suppress noise; + # tile_grid_size divides the image into a grid of contextual regions. + stream = cvcuda.Stream() + clahe_output: cvcuda.Tensor = cvcuda.clahe( + gray_nhwc, + clip_limit=2.0, + tile_grid_size=(8, 8), + stream=stream, + ) + stream.sync() + # docs_tag: end_clahe + + # CLAHE output is NHWC with shape (1, H, W, 1). + # Replicate the single channel to RGB on-device with cvtcolor, then reshape + # to HWC so write_image can encode it. Staying on-device avoids a host + # round-trip and its row-pitch handling. + rgb_nhwc: cvcuda.Tensor = cvcuda.cvtcolor( + clahe_output, cvcuda.ColorConversion.GRAY2RGB + ) + h, w = rgb_nhwc.shape[1], rgb_nhwc.shape[2] + write_image(rgb_nhwc.reshape((h, w, 3), "HWC"), args.output) + # docs_tag: end_main + + +if __name__ == "__main__": + main() diff --git a/samples/operators/color_twist.py b/samples/operators/color_twist.py new file mode 100644 index 000000000..6c0484358 --- /dev/null +++ b/samples/operators/color_twist.py @@ -0,0 +1,83 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. +"""Simple Color Twist example with CVCUDA.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +import cvcuda +import numpy as np + +common_dir = Path(__file__).parent.parent +sys.path.append(str(common_dir)) + +from common import ( # noqa: E402 + parse_image_args, + read_image, + write_image, + cuda_memcpy_h2d, +) # noqa: E402 + + +def main() -> None: + """Apply a color twist (per-channel linear transform) to an image with CVCUDA.""" + # docs_tag: begin_main + args: argparse.Namespace = parse_image_args("cat_color_twist.jpg") + # docs_tag: begin_read_image + input_image: cvcuda.Tensor = read_image(args.input) + # docs_tag: end_read_image + + # docs_tag: begin_color_twist_setup + # The color_twist operator expects a twist matrix of shape (3, 4) with dtype F32, + # interpreted as "HW" layout. Each row i defines the output for channel i: + # out[i] = twist[i,0]*R + twist[i,1]*G + twist[i,2]*B + twist[i,3] + # (the +offset column allows brightness shifts per channel). + # + # Below we build a "warm-boost" matrix that: + # - scales the red channel slightly up (row 0) + # - leaves the green channel unchanged (row 1) + # - scales the blue channel slightly down (row 2) + # This gives the image a warm, golden-hour tint. + twist_np = np.array( + [ + [1.2, 0.0, 0.0, 10.0], # R' = 1.2*R + 10 + [0.0, 1.0, 0.0, 0.0], # G' = G + [0.0, 0.0, 0.8, -10.0], # B' = 0.8*B - 10 + ], + dtype=np.float32, + ) + + # Allocate a (3, 4) F32 tensor on device with layout "HW" and upload the matrix. + twist_tensor = cvcuda.Tensor((3, 4), cvcuda.Type.F32, "HW") + cuda_memcpy_h2d(twist_np, twist_tensor.cuda()) + # docs_tag: end_color_twist_setup + + # docs_tag: begin_color_twist + # color_twist applies the 3×4 affine-per-channel matrix to every pixel. + # The operator clips the result back into the source dtype range automatically. + output_image: cvcuda.Tensor = cvcuda.color_twist( + src=input_image, + twist=twist_tensor, + ) + write_image(output_image, args.output) + # docs_tag: end_color_twist + # docs_tag: end_main + + +if __name__ == "__main__": + main() diff --git a/samples/operators/composite.py b/samples/operators/composite.py new file mode 100644 index 000000000..04b2cac18 --- /dev/null +++ b/samples/operators/composite.py @@ -0,0 +1,91 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. +"""Simple Composite example with CVCUDA.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +import cvcuda +import numpy as np + +common_dir = Path(__file__).parent.parent +sys.path.append(str(common_dir)) + +from common import ( # noqa: E402 + parse_image_args, + read_image, + write_image, + upload_tensor, +) + + +def main() -> None: + """Composite two images with a mask using CVCUDA.""" + # docs_tag: begin_main + args: argparse.Namespace = parse_image_args("cat_composite.jpg") + # docs_tag: begin_read_image + # Foreground: the cat image (primary subject) + fg_image: cvcuda.Tensor = read_image(args.input) + # Background: the Weimaraner dog image, resized to match the foreground + bg_raw: cvcuda.Tensor = read_image(common_dir / "assets/images/Weimaraner.jpg") + # docs_tag: end_read_image + + # docs_tag: begin_composite_setup + h, w, c = fg_image.shape # HWC layout from read_image + + # Resize the background to match the foreground's spatial dimensions so + # cvcuda.composite can pair them element-wise on the GPU. + bg_image: cvcuda.Tensor = cvcuda.resize(bg_raw, (h, w, c)) + + # Build the foreground mask on the CPU using NumPy, then upload it. + # The mask is single-channel uint8: 255 = keep foreground, 0 = show background. + # A circular region in the centre of the frame exposes the cat; the rest + # shows the Weimaraner, giving a clear visual demonstration of blending. + mask_np = np.zeros((h, w, 1), dtype=np.uint8) + cy, cx = h // 2, w // 2 + radius = min(h, w) // 3 + # Vectorised distance computation avoids a slow Python loop. + ys, xs = np.ogrid[:h, :w] + inside_circle = (xs - cx) ** 2 + (ys - cy) ** 2 <= radius**2 + mask_np[inside_circle, 0] = 255 + + # Allocate a GPU tensor for the mask and copy the host data across. + # upload_tensor honours the mask tensor's row pitch (a single-channel mask + # is padded to an alignment boundary); a packed copy would shear the mask. + mask_tensor: cvcuda.Tensor = cvcuda.Tensor((h, w, 1), dtype=np.uint8, layout="HWC") + upload_tensor(mask_np, mask_tensor) + # docs_tag: end_composite_setup + + # docs_tag: begin_composite + # cvcuda.composite blends foreground and background using the mask: + # output[px] = fg[px] if mask[px] > 0 else bg[px]. + # outchannels=3 requests an RGB output tensor. + out_image: cvcuda.Tensor = cvcuda.composite( + fg_image, # foreground (cat) + bg_image, # background (Weimaraner, resized) + mask_tensor, # single-channel alpha mask + 3, # outchannels: 3 = RGB, 4 = RGBA + ) + # docs_tag: end_composite + + write_image(out_image, args.output) + # docs_tag: end_main + + +if __name__ == "__main__": + main() diff --git a/samples/operators/conv2d.py b/samples/operators/conv2d.py new file mode 100644 index 000000000..1b79ffe9c --- /dev/null +++ b/samples/operators/conv2d.py @@ -0,0 +1,109 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. +"""Simple Conv2D example with CVCUDA.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +import cvcuda +import numpy as np + +common_dir = Path(__file__).parent.parent +sys.path.append(str(common_dir)) + +from common import ( # noqa: E402 + parse_image_args, + read_image, + write_image, + upload_tensor, + download_tensor, +) + + +def main() -> None: + """Apply a sharpening convolution to an image with CVCUDA.""" + # docs_tag: begin_main + args: argparse.Namespace = parse_image_args("cat_conv2d.jpg") + # docs_tag: begin_read_image + input_image: cvcuda.Tensor = read_image(args.input) + # docs_tag: end_read_image + + # docs_tag: begin_conv2d_setup + h, w, c = input_image.shape[0], input_image.shape[1], input_image.shape[2] + + # conv2d operates on ImageBatchVarShape. Applying a sharpening kernel + # directly on uint8 would overflow (center weight 5 * 200 > 255), so we + # first promote to float32, convolve, then clamp back to uint8. + float_nhwc: cvcuda.Tensor = cvcuda.convertto( + input_image.reshape((1, h, w, c), "NHWC"), dtype=np.float32 + ) + float_hwc: cvcuda.Tensor = float_nhwc.reshape((h, w, c), "HWC") + + src_image: cvcuda.Image = cvcuda.as_image(float_hwc.cuda(), cvcuda.Format.RGBf32) + src_batch: cvcuda.ImageBatchVarShape = cvcuda.ImageBatchVarShape(1) + src_batch.pushback(src_image) + + # 3x3 sharpening kernel (single-channel float — applied to each colour + # channel independently). + sharpen_np: np.ndarray = np.array( + [[0, -1, 0], [-1, 5, -1], [0, -1, 0]], dtype=np.float32 + ).reshape(3, 3, 1) + kernel_tensor: cvcuda.Tensor = cvcuda.Tensor( + (3, 3, 1), dtype=np.float32, layout="HWC" + ) + # upload_tensor honours the kernel tensor's row pitch (CVCUDA pads each row + # to an alignment boundary, so the 3-float rows are not packed contiguously). + # A packed copy here would scramble the kernel and blacken the output. + upload_tensor(sharpen_np, kernel_tensor) + kernel_image: cvcuda.Image = cvcuda.as_image( + kernel_tensor.cuda(), cvcuda.Format.F32 + ) + kernel_batch: cvcuda.ImageBatchVarShape = cvcuda.ImageBatchVarShape(1) + kernel_batch.pushback(kernel_image) + + # Anchor (-1, -1) lets the operator place the kernel centre automatically. + anchor_np: np.ndarray = np.array([[-1, -1]], dtype=np.int32) + anchor_tensor: cvcuda.Tensor = cvcuda.Tensor((1, 2), dtype=np.int32, layout="NC") + upload_tensor(anchor_np, anchor_tensor) + # docs_tag: end_conv2d_setup + + # docs_tag: begin_conv2d + # Convolve on float data; REFLECT101 border prevents dark edge halos. + out_batch: cvcuda.ImageBatchVarShape = cvcuda.conv2d( + src_batch, + kernel_batch, + anchor_tensor, + cvcuda.Border.REFLECT101, + ) + + # Download float result, clamp to [0, 255], and write as uint8 JPEG. + out_float_hwc: cvcuda.Tensor = cvcuda.as_tensor(next(iter(out_batch))) + out_np: np.ndarray = download_tensor(out_float_hwc) + out_clamped: np.ndarray = np.ascontiguousarray( + np.clip(out_np, 0, 255).astype(np.uint8) + ) + # Write through the original (unpadded) input tensor instead of a freshly + # allocated one; upload_tensor would also handle a padded row pitch. + upload_tensor(out_clamped, input_image) + write_image(input_image, args.output) + # docs_tag: end_conv2d + # docs_tag: end_main + + +if __name__ == "__main__": + main() diff --git a/samples/operators/convertto.py b/samples/operators/convertto.py new file mode 100644 index 000000000..e9dd0ecff --- /dev/null +++ b/samples/operators/convertto.py @@ -0,0 +1,65 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. +"""Simple Convert To example with CVCUDA.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +import cvcuda +import numpy as np + +common_dir = Path(__file__).parent.parent +sys.path.append(str(common_dir)) + +from common import parse_image_args, read_image, write_image # noqa: E402 + + +def main() -> None: + """Convert an image dtype with scaling using CVCUDA.""" + # docs_tag: begin_main + args: argparse.Namespace = parse_image_args("cat_convertto.jpg") + # docs_tag: begin_read_image + input_image: cvcuda.Tensor = read_image(args.input) + # docs_tag: end_read_image + + # docs_tag: begin_convertto + # 1. Convert the uint8 image to float32, applying a scale factor. + # scale=1/255.0 maps [0, 255] -> [0.0, 1.0] — a standard normalization step + # used before feeding images into neural networks. + float_image: cvcuda.Tensor = cvcuda.convertto( + src=input_image, + dtype=np.float32, + scale=1.0 / 255.0, + ) + + # 2. Convert back to uint8 by reversing the scale (multiply by 255). + # This round-trip demonstrates that the conversion is lossless for + # images with pixel values in the valid uint8 range. + output_image: cvcuda.Tensor = cvcuda.convertto( + src=float_image, + dtype=np.uint8, + scale=255.0, + ) + # docs_tag: end_convertto + + write_image(output_image, args.output) + # docs_tag: end_main + + +if __name__ == "__main__": + main() diff --git a/samples/operators/copymakeborder.py b/samples/operators/copymakeborder.py new file mode 100644 index 000000000..e72250a8e --- /dev/null +++ b/samples/operators/copymakeborder.py @@ -0,0 +1,60 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. +"""Simple Copy Make Border example with CVCUDA.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +import cvcuda + +common_dir = Path(__file__).parent.parent +sys.path.append(str(common_dir)) + +from common import parse_image_args, read_image, write_image # noqa: E402 + + +def main() -> None: + """Add a colored border to an image with CVCUDA copymakeborder.""" + # docs_tag: begin_main + args: argparse.Namespace = parse_image_args("cat_copymakeborder.jpg") + # docs_tag: begin_read_image + input_image: cvcuda.Tensor = read_image(args.input) + # docs_tag: end_read_image + + # docs_tag: begin_copymakeborder + # Add a visible border around the image using CONSTANT mode so the border + # is filled with a solid color rather than replicated/reflected pixels. + # The border widths (top=30, bottom=30, left=60, right=60) make the + # added region clearly visible in the output. + output_image: cvcuda.Tensor = cvcuda.copymakeborder( + src=input_image, + top=30, + bottom=30, + left=60, + right=60, + border_mode=cvcuda.Border.CONSTANT, + # Bright orange border (R=255, G=140, B=0) makes the padding conspicuous. + border_value=[255, 140, 0], + ) + write_image(output_image, args.output) + # docs_tag: end_copymakeborder + # docs_tag: end_main + + +if __name__ == "__main__": + main() diff --git a/samples/operators/crop_flip_normalize_reformat.py b/samples/operators/crop_flip_normalize_reformat.py new file mode 100644 index 000000000..e6141f888 --- /dev/null +++ b/samples/operators/crop_flip_normalize_reformat.py @@ -0,0 +1,136 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. +"""Simple Crop Flip Normalize Reformat example with CVCUDA.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +import cvcuda +import numpy as np + +common_dir = Path(__file__).parent.parent +sys.path.append(str(common_dir)) + +from common import ( # noqa: E402 + parse_image_args, + read_image, + write_image, + download_tensor, + upload_tensor, +) + + +def main() -> None: + """Crop, flip, normalize, and reformat an image with CVCUDA.""" + # docs_tag: begin_main + args: argparse.Namespace = parse_image_args("cat_crop_flip_normalize_reformat.jpg") + # docs_tag: begin_read_image + input_image: cvcuda.Tensor = read_image(args.input) + # docs_tag: end_read_image + + # docs_tag: begin_crop_flip_normalize_reformat_setup + # crop_flip_normalize_reformat operates on an ImageBatchVarShape, so + # wrap the single HWC tensor into a one-image batch. + height, width, channels = input_image.shape + cvcuda_image = cvcuda.as_image(input_image.cuda(), cvcuda.Format.RGB8) + batch = cvcuda.ImageBatchVarShape(1) + batch.pushback([cvcuda_image]) + + # Define a crop rectangle [crop_x, crop_y, crop_width, crop_height] per image. + # We crop the central 80% of the image so the result still looks good. + crop_x = int(width * 0.1) + crop_y = int(height * 0.1) + crop_w = int(width * 0.8) + crop_h = int(height * 0.8) + crop_data = np.array([[[[crop_x, crop_y, crop_w, crop_h]]]], dtype=np.int32) + crop_rect_host = np.ascontiguousarray(crop_data) + + # Allocate crop rect tensor on GPU (shape: [N, 1, 1, 4], layout NHWC) + crop_tensor = cvcuda.Tensor((1, 1, 1, 4), np.int32, "NHWC") + upload_tensor(crop_rect_host, crop_tensor) + + # flip_code per image: 1 = flip around y-axis (horizontal flip) + flip_data = np.array([[1]], dtype=np.int32) + flip_host = np.ascontiguousarray(flip_data) + flip_tensor = cvcuda.Tensor((1, 1), np.int32, "NC") + upload_tensor(flip_host, flip_tensor) + + # Normalization parameters: base (mean) and scale (std-dev) per channel. + # Using ImageNet-style mean and std for demonstration. + base_data = np.array([[[[0.485, 0.456, 0.406]]]], dtype=np.float32) + scale_data = np.array([[[[0.229, 0.224, 0.225]]]], dtype=np.float32) + base_host = np.ascontiguousarray(base_data) + scale_host = np.ascontiguousarray(scale_data) + + base_tensor = cvcuda.Tensor((1, 1, 1, 3), np.float32, "NHWC") + scale_tensor = cvcuda.Tensor((1, 1, 1, 3), np.float32, "NHWC") + upload_tensor(base_host, base_tensor) + upload_tensor(scale_host, scale_tensor) + + # The output will be in NCHW layout (planar), float32, at the cropped size. + out_shape = (1, channels, crop_h, crop_w) + # docs_tag: end_crop_flip_normalize_reformat_setup + + # docs_tag: begin_crop_flip_normalize_reformat + # Run the combined crop + horizontal-flip + normalize + reformat pipeline in one + # GPU kernel. SCALE_IS_STDDEV tells the operator to treat 'scale' as std-dev + # so it divides (pixel/255 - mean) / std to produce a zero-centred float tensor. + normalized_nchw: cvcuda.Tensor = cvcuda.crop_flip_normalize_reformat( + batch, + out_shape=out_shape, + out_dtype=np.float32, + out_layout="NCHW", + rect=crop_tensor, + flip_code=flip_tensor, + base=base_tensor, + scale=scale_tensor, + globalscale=1.0 / 255.0, + globalshift=0.0, + epsilon=1e-8, + flags=cvcuda.NormalizeFlags.SCALE_IS_STDDEV, + border=cvcuda.Border.REPLICATE, + bvalue=0.0, + ) + # docs_tag: end_crop_flip_normalize_reformat + + # Reverse the normalization on the host so the result is a viewable uint8 image. + # normalized pixel = (raw/255 - mean) / std => raw = clip((normalized*std + mean)*255, 0, 255) + # download_tensor honours the NCHW row pitch (W rows are padded). + host_nchw = download_tensor(normalized_nchw) + + # Reorder NCHW -> HWC for display (squeeze the batch dim) + host_chw = host_nchw[0] # (C, H, W) + host_hwc = np.transpose(host_chw, (1, 2, 0)) # (H, W, C) + + mean = np.array([0.485, 0.456, 0.406], dtype=np.float32) + std = np.array([0.229, 0.224, 0.225], dtype=np.float32) + host_uint8 = np.clip((host_hwc * std + mean) * 255.0, 0, 255).astype(np.uint8) + + # Upload the recovered uint8 image back to GPU for write_image. + # upload_tensor honours the tensor's row pitch, so no contiguous-reshape + # trick is needed. + hwc_shape = host_uint8.shape # (H, W, C) + out_tensor = cvcuda.Tensor(hwc_shape, dtype=np.uint8, layout="HWC") + upload_tensor(np.ascontiguousarray(host_uint8), out_tensor) + + write_image(out_tensor, args.output) + # docs_tag: end_main + + +if __name__ == "__main__": + main() diff --git a/samples/operators/customcrop.py b/samples/operators/customcrop.py new file mode 100644 index 000000000..387a088d7 --- /dev/null +++ b/samples/operators/customcrop.py @@ -0,0 +1,65 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. +"""Simple Custom Crop example with CVCUDA.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +import cvcuda + +common_dir = Path(__file__).parent.parent +sys.path.append(str(common_dir)) + +from common import parse_image_args, read_image, write_image # noqa: E402 + + +def main() -> None: + """Crop an image to an off-center rectangle with CVCUDA.""" + # docs_tag: begin_main + args: argparse.Namespace = parse_image_args("cat_customcrop.jpg") + # docs_tag: begin_read_image + input_image: cvcuda.Tensor = read_image(args.input) + # docs_tag: end_read_image + + # docs_tag: begin_customcrop_setup + # Determine an off-center crop rectangle. + # cvcuda.customcrop takes (x, y, width, height) in pixel coordinates, + # where (x, y) is the top-left corner of the crop region. + # We choose a region that is 60% of each dimension, starting at 20% offset + # so the crop is visually off-center and still fits within the image. + img_h, img_w = input_image.shape[0], input_image.shape[1] + crop_x = img_w // 5 # 20% from the left edge + crop_y = img_h // 5 # 20% from the top edge + crop_w = max(1, (img_w * 3) // 5) # 60% of the image width (at least 1px) + crop_h = max(1, (img_h * 3) // 5) # 60% of the image height (at least 1px) + + # RectI specifies the crop region in the input image coordinate space + rect = cvcuda.RectI(x=crop_x, y=crop_y, width=crop_w, height=crop_h) + # docs_tag: end_customcrop_setup + + # docs_tag: begin_customcrop + # customcrop extracts a rectangular sub-region from the input tensor. + # The output shape will be (crop_h, crop_w, channels) for an HWC input. + output_image: cvcuda.Tensor = cvcuda.customcrop(input_image, rect) + write_image(output_image, args.output) + # docs_tag: end_customcrop + # docs_tag: end_main + + +if __name__ == "__main__": + main() diff --git a/samples/operators/cvtcolor.py b/samples/operators/cvtcolor.py new file mode 100644 index 000000000..90d03d7fa --- /dev/null +++ b/samples/operators/cvtcolor.py @@ -0,0 +1,59 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. +"""Simple Color Conversion example with CVCUDA.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +import cvcuda + +common_dir = Path(__file__).parent.parent +sys.path.append(str(common_dir)) + +from common import parse_image_args, read_image, write_image # noqa: E402 + + +def main() -> None: + """Convert image color space with CVCUDA.""" + # docs_tag: begin_main + args: argparse.Namespace = parse_image_args("cat_cvtcolor.jpg") + # docs_tag: begin_read_image + input_image: cvcuda.Tensor = read_image(args.input) + # docs_tag: end_read_image + + # docs_tag: begin_cvtcolor + # cvtcolor requires a batched (NHWC) tensor, so wrap the HWC image in a batch + # dimension using cvcuda.stack before passing it to the operator. + nhwc_image: cvcuda.Tensor = cvcuda.stack([input_image]) + + # Swap the R and B channels (RGB2BGR) — the output is a visually distinct + # but still fully viewable 3-channel uint8 image, making the conversion easy + # to verify by eye (warm tones shift to cool and vice versa). + converted: cvcuda.Tensor = cvcuda.cvtcolor( + nhwc_image, code=cvcuda.ColorConversion.RGB2BGR + ) + + # Drop the batch dimension back to HWC so write_image can encode the result. + output_image: cvcuda.Tensor = converted.reshape(converted.shape[1:], "HWC") + write_image(output_image, args.output) + # docs_tag: end_cvtcolor + # docs_tag: end_main + + +if __name__ == "__main__": + main() diff --git a/samples/operators/erase.py b/samples/operators/erase.py new file mode 100644 index 000000000..cbfd6f86a --- /dev/null +++ b/samples/operators/erase.py @@ -0,0 +1,146 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. +"""Simple Erase example with CVCUDA.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +import cvcuda +import numpy as np + +common_dir = Path(__file__).parent.parent +sys.path.append(str(common_dir)) + +from common import ( # noqa: E402 + parse_image_args, + read_image, + write_image, + cuda_memcpy_h2d, +) # noqa: E402 + + +def main() -> None: + """Erase rectangular regions from an image with CVCUDA.""" + # docs_tag: begin_main + args: argparse.Namespace = parse_image_args("cat_erase.jpg") + # docs_tag: begin_read_image + input_image: cvcuda.Tensor = read_image(args.input) + # docs_tag: end_read_image + + # docs_tag: begin_erase_setup + # The erase operator works on a batched NHWC tensor, so wrap the HWC image + # in a batch dimension of size 1. + height, width, channels = input_image.shape + nhwc_image: cvcuda.Tensor = input_image.reshape( + (1, height, width, channels), "NHWC" + ) + + # Number of rectangular regions to erase. + num_areas = 6 + + # anchor: (x, y) pixel coordinates of the top-left corner of each rectangle. + # Shape (num_areas,) with element type _2S32 (pair of int32). + anchor_host = np.array( + [[50, 80], [200, 150], [350, 300], [480, 80], [480, 260], [200, 400]], + dtype=np.int32, + ) + anchor = cvcuda.Tensor((num_areas,), cvcuda.Type._2S32, "N") + cuda_memcpy_h2d(anchor_host, anchor.cuda()) + + # erasing: (width, height, flag) for each rectangle. + # flag is a channel bitmask: bit0=R, bit1=G, bit2=B. + # 7 (0b111) erases all three channels (solid fill). + # 2 (0b010) erases only the green channel, leaving R and B intact — tint. + erasing_host = np.array( + [ + [120, 80, 7], + [160, 100, 7], + [100, 120, 7], + [120, 100, 7], + [120, 100, 7], + [160, 100, 2], + ], + dtype=np.int32, + ) + erasing = cvcuda.Tensor((num_areas,), cvcuda.Type._3S32, "N") + cuda_memcpy_h2d(erasing_host, erasing.cuda()) + + # values: 4 float32 fill values per area (R,G,B,A), laid out flat. + # The operator always reserves 4 slots regardless of channel count. + values_host = np.array( + [ + 255.0, + 0.0, + 0.0, + 0.0, # area 0: red (solid, flag=7) + 0.0, + 255.0, + 0.0, + 0.0, # area 1: green (solid, flag=7) + 0.0, + 0.0, + 255.0, + 0.0, # area 2: blue (solid, flag=7) + 255.0, + 255.0, + 255.0, + 0.0, # area 3: white (solid, flag=7) + 0.0, + 0.0, + 0.0, + 0.0, # area 4: black (solid, flag=7) + 0.0, + 255.0, + 0.0, + 0.0, # area 5: green tint (G channel only, flag=2) + ], + dtype=np.float32, + ) + values = cvcuda.Tensor((num_areas * 4,), cvcuda.Type.F32, "N") + cuda_memcpy_h2d(values_host, values.cuda()) + + # imgIdx: which image in the batch each rectangle belongs to. + # All areas are applied to image index 0. + imgIdx_host = np.array([0, 0, 0, 0, 0, 0], dtype=np.int32) + imgIdx = cvcuda.Tensor((num_areas,), cvcuda.Type.S32, "N") + cuda_memcpy_h2d(imgIdx_host, imgIdx.cuda()) + # docs_tag: end_erase_setup + + # docs_tag: begin_erase + # Erase the defined rectangles from the image. + # random=False uses the per-area fill values supplied above. + # random=True would ignore values and fill with pseudo-random noise seeded by seed. + out_nhwc: cvcuda.Tensor = cvcuda.erase( + src=nhwc_image, + anchor=anchor, + erasing=erasing, + values=values, + imgIdx=imgIdx, + random=False, + seed=0, + ) + + # Drop the batch dimension before saving; write_image expects HWC layout. + out_hwc: cvcuda.Tensor = out_nhwc.reshape((height, width, channels), "HWC") + write_image(out_hwc, args.output) + # docs_tag: end_erase + # docs_tag: end_main + + +if __name__ == "__main__": + main() diff --git a/samples/operators/flip.py b/samples/operators/flip.py new file mode 100644 index 000000000..2ceba852a --- /dev/null +++ b/samples/operators/flip.py @@ -0,0 +1,49 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. +"""Simple Flip example with CVCUDA.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +import cvcuda + +common_dir = Path(__file__).parent.parent +sys.path.append(str(common_dir)) + +from common import parse_image_args, read_image, write_image # noqa: E402 + + +def main() -> None: + """Flip an image horizontally with CVCUDA.""" + # docs_tag: begin_main + args: argparse.Namespace = parse_image_args("cat_flip.jpg") + # docs_tag: begin_read_image + input_image: cvcuda.Tensor = read_image(args.input) + # docs_tag: end_read_image + + # docs_tag: begin_flip + # Flip the image along the horizontal axis (left-right mirror). + # flipCode=1 flips left-right; 0 flips top-bottom; -1 flips both axes. + output_image: cvcuda.Tensor = cvcuda.flip(input_image, flipCode=1) + write_image(output_image, args.output) + # docs_tag: end_flip + # docs_tag: end_main + + +if __name__ == "__main__": + main() diff --git a/samples/operators/gamma_contrast.py b/samples/operators/gamma_contrast.py new file mode 100644 index 000000000..36383781c --- /dev/null +++ b/samples/operators/gamma_contrast.py @@ -0,0 +1,75 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. +"""Simple Gamma Contrast example with CVCUDA.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +import cvcuda +import numpy as np + +common_dir = Path(__file__).parent.parent +sys.path.append(str(common_dir)) + +from common import ( # noqa: E402 + parse_image_args, + read_image, + write_image, + cuda_memcpy_h2d, +) + + +def main() -> None: + """Apply gamma contrast correction to an image with CVCUDA.""" + # docs_tag: begin_main + args: argparse.Namespace = parse_image_args("cat_gamma_contrast.jpg") + # docs_tag: begin_read_image + input_image: cvcuda.Tensor = read_image(args.input) + # docs_tag: end_read_image + + # docs_tag: begin_gamma_contrast_setup + # This sample uses the ImageBatchVarShape overload (gamma_contrast also accepts a + # plain Tensor with a gamma tensor or a host-scalar gamma/gain). + # Wrap the single HWC tensor as a cvcuda.Image and push it into a batch. + img_cvcuda: cvcuda.Image = cvcuda.as_image(input_image.cuda()) + batch = cvcuda.ImageBatchVarShape(1) + batch.pushback(img_cvcuda) + + # Build a 1-D float32 gamma tensor, one value per image in the batch. + # A gamma of 2.2 matches the standard sRGB display transfer function — + # values < 1 brighten the image, values > 1 darken/increase contrast. + gamma_np = np.array([2.2], dtype=np.float32) + gamma_tensor = cvcuda.Tensor((1,), dtype=np.float32, layout="N") + cuda_memcpy_h2d(gamma_np, gamma_tensor.cuda()) + # docs_tag: end_gamma_contrast_setup + + # docs_tag: begin_gamma_contrast + # Apply gamma contrast: each pixel p is mapped to p^gamma (normalised to [0,1]). + # The operator accepts ImageBatchVarShape inputs so it supports variable-size batches. + out_batch: cvcuda.ImageBatchVarShape = cvcuda.gamma_contrast(batch, gamma_tensor) + + # Extract the single output image and convert back to an HWC Tensor for writing. + out_img: cvcuda.Image = list(out_batch)[0] + output_image: cvcuda.Tensor = cvcuda.as_tensor(out_img.cuda(), "HWC") + write_image(output_image, args.output) + # docs_tag: end_gamma_contrast + # docs_tag: end_main + + +if __name__ == "__main__": + main() diff --git a/samples/operators/gaussiannoise.py b/samples/operators/gaussiannoise.py new file mode 100644 index 000000000..04cbd048b --- /dev/null +++ b/samples/operators/gaussiannoise.py @@ -0,0 +1,81 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. +"""Simple Gaussian Noise example with CVCUDA.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +import cvcuda +import numpy as np + +common_dir = Path(__file__).parent.parent +sys.path.append(str(common_dir)) + +from common import ( # noqa: E402 + parse_image_args, + read_image, + write_image, + cuda_memcpy_h2d, +) + + +def main() -> None: + """Add Gaussian noise to an image with CVCUDA.""" + # docs_tag: begin_main + args: argparse.Namespace = parse_image_args("cat_gaussiannoise.jpg") + # docs_tag: begin_read_image + input_image: cvcuda.Tensor = read_image(args.input) + # docs_tag: end_read_image + + # docs_tag: begin_gaussiannoise_setup + # Wrap the HWC single image in a batch dimension (N=1) so the + # gaussiannoise operator can receive per-image mu/sigma tensors. + nhwc_image: cvcuda.Tensor = input_image.reshape((1, *input_image.shape), "NHWC") + + # mu and sigma are per-image scalars with layout "N". + # mu=0 means zero-mean (no brightness shift), sigma controls noise strength. + sigma_value = 25.0 # visible but not destructive for an 8-bit image + mu_host = np.array([0.0], dtype=np.float32) + sigma_host = np.array([sigma_value], dtype=np.float32) + + mu_tensor = cvcuda.Tensor((1,), cvcuda.Type.F32, "N") + sigma_tensor = cvcuda.Tensor((1,), cvcuda.Type.F32, "N") + cuda_memcpy_h2d(mu_host, mu_tensor.cuda()) + cuda_memcpy_h2d(sigma_host, sigma_tensor.cuda()) + # docs_tag: end_gaussiannoise_setup + + # docs_tag: begin_gaussiannoise + # Apply Gaussian noise. per_channel=False applies the same noise sample + # to every channel; seed fixes the random state for reproducibility. + noisy_batch: cvcuda.Tensor = cvcuda.gaussiannoise( + src=nhwc_image, + mu=mu_tensor, + sigma=sigma_tensor, + per_channel=False, + seed=42, + ) + # docs_tag: end_gaussiannoise + + # The output is NHWC uint8. Reshape to HWC by dropping the batch dimension + # and write directly — no unnecessary host round-trip needed. + write_image(noisy_batch.reshape(input_image.shape, "HWC"), args.output) + # docs_tag: end_main + + +if __name__ == "__main__": + main() diff --git a/samples/operators/histogrameq.py b/samples/operators/histogrameq.py new file mode 100644 index 000000000..26102726f --- /dev/null +++ b/samples/operators/histogrameq.py @@ -0,0 +1,73 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. +"""Simple Histogram Equalization example with CVCUDA.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +import cvcuda + +common_dir = Path(__file__).parent.parent +sys.path.append(str(common_dir)) + +from common import ( # noqa: E402 + parse_image_args, + read_image, + write_image, +) # noqa: E402 + + +def main() -> None: + """Apply histogram equalization to an image with CVCUDA.""" + # docs_tag: begin_main + args: argparse.Namespace = parse_image_args("cat_histogrameq.jpg") + # docs_tag: begin_read_image + input_image: cvcuda.Tensor = read_image(args.input) + # docs_tag: end_read_image + + # docs_tag: begin_histogrameq_setup + # Histogram equalization in CVCUDA works on single-channel (grayscale) or + # multi-channel tensors in HWC/NHWC layout with U8 dtype. + # We batch the HWC image into NHWC so we can use cvtcolor for RGB->GRAY conversion. + nhwc_image: cvcuda.Tensor = cvcuda.stack([input_image]) + gray_image: cvcuda.Tensor = cvcuda.cvtcolor( + nhwc_image, cvcuda.ColorConversion.RGB2GRAY + ) + # docs_tag: end_histogrameq_setup + + # docs_tag: begin_histogrameq + # Apply histogram equalization: redistributes pixel intensities so that the + # cumulative histogram of the output is approximately uniform, improving contrast. + # The dtype keyword specifies the output element type (must be U8 for uint8 input). + equalized: cvcuda.Tensor = cvcuda.histogrameq(src=gray_image, dtype=cvcuda.Type.U8) + # docs_tag: end_histogrameq + + # The equalized output is NHWC with shape (1, H, W, 1). + # Replicate the single channel across R, G, B on-device with cvtcolor, then + # reshape to HWC so write_image can encode a viewable grayscale JPEG. + # Staying on-device avoids a host round-trip and its row-pitch handling. + rgb_nhwc: cvcuda.Tensor = cvcuda.cvtcolor( + equalized, cvcuda.ColorConversion.GRAY2RGB + ) + h, w = rgb_nhwc.shape[1], rgb_nhwc.shape[2] + write_image(rgb_nhwc.reshape((h, w, 3), "HWC"), args.output) + # docs_tag: end_main + + +if __name__ == "__main__": + main() diff --git a/samples/operators/hq_resize.py b/samples/operators/hq_resize.py new file mode 100644 index 000000000..6d0a9e99d --- /dev/null +++ b/samples/operators/hq_resize.py @@ -0,0 +1,58 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. +"""Simple HQ Resize example with CVCUDA.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +import cvcuda + +common_dir = Path(__file__).parent.parent +sys.path.append(str(common_dir)) + +from common import parse_image_args, read_image, write_image # noqa: E402 + + +def main() -> None: + """High-quality resize an image with CVCUDA.""" + # docs_tag: begin_main + args: argparse.Namespace = parse_image_args("cat_hq_resize.jpg") + # docs_tag: begin_read_image + input_image: cvcuda.Tensor = read_image(args.input) + # docs_tag: end_read_image + + # docs_tag: begin_hq_resize + # HQ Resize uses separate interpolation filters for downscaling (min) and + # upscaling (mag), which produces sharper results than standard resize. + # LANCZOS for minification avoids moiré patterns; LINEAR for magnification + # is fast and smooth. antialias=True applies a low-pass filter before + # downscaling to further suppress aliasing. + output_image: cvcuda.Tensor = cvcuda.hq_resize( + input_image, + (args.height, args.width), + min_interpolation=cvcuda.Interp.LANCZOS, + mag_interpolation=cvcuda.Interp.LINEAR, + antialias=True, + ) + write_image(output_image, args.output) + # docs_tag: end_hq_resize + # docs_tag: end_main + + +if __name__ == "__main__": + main() diff --git a/samples/operators/inpaint.py b/samples/operators/inpaint.py new file mode 100644 index 000000000..c045ed0dd --- /dev/null +++ b/samples/operators/inpaint.py @@ -0,0 +1,108 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. +"""Simple Inpaint example with CVCUDA.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +import cvcuda +import numpy as np + +common_dir = Path(__file__).parent.parent +sys.path.append(str(common_dir)) + +from common import ( # noqa: E402 + parse_image_args, + read_image, + write_image, + upload_tensor, + download_tensor, +) + + +def main() -> None: + """Inpaint a masked region of an image with CVCUDA.""" + # docs_tag: begin_main + args: argparse.Namespace = parse_image_args("cat_inpaint.jpg") + # docs_tag: begin_read_image + input_image: cvcuda.Tensor = read_image(args.input) + # docs_tag: end_read_image + + height, width, _ = input_image.shape + + # docs_tag: begin_inpaint_setup + # Reshape to NHWC and download so we can synthesise the damage on the CPU. + # We keep a clean copy of the original to restore non-masked pixels later. + nhwc_image: cvcuda.Tensor = input_image.reshape((1, height, width, 3), "NHWC") + orig_np = download_tensor(input_image) # (H, W, 3) + damaged_np = orig_np.copy()[np.newaxis] # (1,H,W,3) + + # Simulate salt-and-pepper sensor noise by randomly zeroing ~15% of pixels. + # Each masked pixel is surrounded by unmasked neighbours so the inpaint + # operator fills every corrupted pixel cleanly from its immediate context. + rng = np.random.default_rng(42) + noise_mask = rng.random((height, width)) < 0.15 # bool (H, W) + mask_np = np.zeros((1, height, width, 1), dtype=np.uint8) + mask_np[0, :, :, 0] = noise_mask.astype(np.uint8) * 255 + damaged_np[0, noise_mask, :] = 0 + + # Save the noisy image for the before/after comparison in the docs. + damaged_output = args.output.parent / ( + args.output.stem + "_damaged" + args.output.suffix + ) + upload_tensor(np.ascontiguousarray(damaged_np), nhwc_image) + write_image(nhwc_image.reshape((height, width, 3), "HWC"), damaged_output) + + # Re-upload damaged (write_image may have altered the tensor content) + upload_tensor(np.ascontiguousarray(damaged_np), nhwc_image) + + mask_tensor: cvcuda.Tensor = cvcuda.Tensor( + (1, height, width, 1), cvcuda.Type.U8, "NHWC" + ) + upload_tensor(mask_np, mask_tensor) + # docs_tag: end_inpaint_setup + + # docs_tag: begin_inpaint + # Inpaint the noisy image — the operator reconstructs each corrupted pixel + # from its neighbourhood. inpaintRadius controls the neighbourhood size. + inpaint_radius: float = 15.0 + output_nhwc: cvcuda.Tensor = cvcuda.inpaint( + src=nhwc_image, + masks=mask_tensor, + inpaintRadius=inpaint_radius, + ) + + # The operator may modify pixels just outside the mask boundary. + # Restore the original content everywhere outside the mask so only the + # noisy pixels differ from the input. + out_np = download_tensor(output_nhwc) # (1, H, W, 3) + mask_bool = mask_np.astype(bool) + final_np = np.where(mask_bool, out_np, orig_np[np.newaxis]).astype(np.uint8) + + # Upload final result back into nhwc_image (preserves tensor format for + # write_image) then reshape to HWC for output. + upload_tensor(np.ascontiguousarray(final_np), nhwc_image) + # docs_tag: end_inpaint + + output_hwc: cvcuda.Tensor = nhwc_image.reshape((height, width, 3), "HWC") + write_image(output_hwc, args.output) + # docs_tag: end_main + + +if __name__ == "__main__": + main() diff --git a/samples/operators/joint_bilateral_filter.py b/samples/operators/joint_bilateral_filter.py new file mode 100644 index 000000000..f5c5d8f75 --- /dev/null +++ b/samples/operators/joint_bilateral_filter.py @@ -0,0 +1,77 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. +"""Simple Joint Bilateral Filter example with CVCUDA.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +import cvcuda + +common_dir = Path(__file__).parent.parent +sys.path.append(str(common_dir)) + +from common import parse_image_args, read_image, write_image # noqa: E402 + + +def main() -> None: + """Apply joint bilateral filter to an image with CVCUDA.""" + # docs_tag: begin_main + args: argparse.Namespace = parse_image_args("cat_joint_bilateral_filter.jpg") + # docs_tag: begin_read_image + input_image: cvcuda.Tensor = read_image(args.input) + # docs_tag: end_read_image + + # docs_tag: begin_joint_bilateral_filter_setup + # The joint bilateral filter smooths `src` guided by `srcColor`. + # Using a grayscale-converted version of the image as the guidance signal + # keeps edges defined by luminance sharp while smoothing color noise. + # We batch the HWC tensor to NHWC so cvtcolor (which requires a batch dim) works. + nhwc_image: cvcuda.Tensor = input_image.reshape((1, *input_image.shape), "NHWC") + guidance_image: cvcuda.Tensor = cvcuda.cvtcolor( + nhwc_image, cvcuda.ColorConversion.RGB2GRAY + ) + # cvtcolor produces a 1-channel NHWC tensor; replicate to 3 channels so it + # matches the source tensor's channel count, which joint_bilateral_filter requires. + guidance_3ch: cvcuda.Tensor = cvcuda.cvtcolor( + guidance_image, cvcuda.ColorConversion.GRAY2RGB + ) + # docs_tag: end_joint_bilateral_filter_setup + + # docs_tag: begin_joint_bilateral_filter + # diameter=9 – neighbourhood pixel diameter (must be odd and positive) + # sigma_color – range kernel width; larger values blend more dissimilar colours + # sigma_space – spatial kernel width; larger values mean farther pixels contribute + filtered_nhwc: cvcuda.Tensor = cvcuda.joint_bilateral_filter( + nhwc_image, + guidance_3ch, + diameter=9, + sigma_color=75.0, + sigma_space=75.0, + border=cvcuda.Border.REFLECT, + ) + # Drop the batch dimension added for processing back to HWC before saving. + filtered_image: cvcuda.Tensor = filtered_nhwc.reshape( + filtered_nhwc.shape[1:], "HWC" + ) + write_image(filtered_image, args.output) + # docs_tag: end_joint_bilateral_filter + # docs_tag: end_main + + +if __name__ == "__main__": + main() diff --git a/samples/operators/label.py b/samples/operators/label.py index 2bee1cdf3..1e437d023 100644 --- a/samples/operators/label.py +++ b/samples/operators/label.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -31,8 +31,8 @@ parse_image_args, read_image, write_image, - cuda_memcpy_d2h, - cuda_memcpy_h2d, + download_tensor, + upload_tensor, ) @@ -46,8 +46,7 @@ def color_labels_nhwc(labels): cvcuda.Tensor: RGB image, with each label having a unique color """ # Download labels from GPU - host_labels = np.zeros(labels.shape, dtype=np.int32) - cuda_memcpy_d2h(labels.cuda(), host_labels) + host_labels = download_tensor(labels) # Create RGB output on CPU (contiguous) rgb_shape = (host_labels.shape[0], host_labels.shape[1], host_labels.shape[2], 3) @@ -62,10 +61,10 @@ def color_labels_nhwc(labels): mask = host_labels[n] == label a_rgb[n][mask[:, :, 0]] = rgb_label_color - # Force a contiguous tensor allocation and reshape to NHWC - contiguous_tensor = cvcuda.Tensor((np.prod(rgb_shape),), dtype=np.uint8) - nhwc_tensor = contiguous_tensor.reshape(rgb_shape, layout="NHWC") - cuda_memcpy_h2d(a_rgb, nhwc_tensor.cuda()) + # Allocate an NHWC tensor and upload the colorized labels. upload_tensor + # honours the tensor's row pitch, so no contiguous-reshape trick is needed. + nhwc_tensor = cvcuda.Tensor(rgb_shape, dtype=np.uint8, layout="NHWC") + upload_tensor(a_rgb, nhwc_tensor) # Return the NHWC tensor return nhwc_tensor @@ -90,11 +89,11 @@ def main() -> None: # 2. Compute threshold tp_host = np.array([128], dtype=np.float64) tp = cvcuda.Tensor((1,), dtype=np.float64, layout="N") - cuda_memcpy_h2d(tp_host, tp.cuda()) + upload_tensor(tp_host, tp) mp_host = np.array([255], dtype=np.float64) mp = cvcuda.Tensor((1,), dtype=np.float64, layout="N") - cuda_memcpy_h2d(mp_host, mp.cuda()) + upload_tensor(mp_host, mp) threshold_image: cvcuda.Tensor = cvcuda.threshold( histogram_image, tp, mp, cvcuda.ThresholdType.BINARY diff --git a/samples/operators/laplacian.py b/samples/operators/laplacian.py new file mode 100644 index 000000000..0af11ebf6 --- /dev/null +++ b/samples/operators/laplacian.py @@ -0,0 +1,94 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. +"""Simple Laplacian example with CVCUDA.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +import cvcuda +import numpy as np + +common_dir = Path(__file__).parent.parent +sys.path.append(str(common_dir)) + +from common import ( # noqa: E402 + parse_image_args, + read_image, + write_image, + download_tensor, + upload_tensor, +) + + +def normalize_to_uint8(tensor: cvcuda.Tensor) -> cvcuda.Tensor: + """Stretch the value range of a uint8 tensor to full [0, 255] for visibility. + + The Laplacian response is often concentrated in a narrow range; + normalization ensures the output is a useful visual image. + """ + host = download_tensor(tensor) + + lo = float(host.min()) + hi = float(host.max()) + if hi > lo: + # Scale to [0, 255] using float32 arithmetic to avoid overflow + stretched = ((host.astype(np.float32) - lo) / (hi - lo) * 255.0).astype( + np.uint8 + ) + else: + # Uniform image — return as-is + stretched = host + + # Allocate a device tensor and upload the result (upload_tensor honours the + # tensor's row pitch, so a padded allocation is handled correctly). + out_tensor = cvcuda.Tensor(tensor.shape, np.uint8, tensor.layout) + upload_tensor(np.ascontiguousarray(stretched), out_tensor) + return out_tensor + + +def main() -> None: + """Apply the Laplacian edge-detection operator to an image with CVCUDA.""" + # docs_tag: begin_main + args: argparse.Namespace = parse_image_args("cat_laplacian.jpg") + # docs_tag: begin_read_image + input_image: cvcuda.Tensor = read_image(args.input) + # docs_tag: end_read_image + + # docs_tag: begin_laplacian + # Apply the Laplacian operator. + # ksize=3 selects the 3×3 discrete Laplacian aperture; scale=1.0 leaves + # the computed values unchanged before the output is saturated back to uint8. + # REPLICATE border avoids zero-valued artifacts at the image boundary. + output_image: cvcuda.Tensor = cvcuda.laplacian( + input_image, + ksize=3, + scale=1.0, + border=cvcuda.Border.REPLICATE, + ) + + # The raw Laplacian response occupies only a small fraction of [0, 255]. + # Stretch the histogram so the edges are clearly visible in the saved JPEG. + output_image = normalize_to_uint8(output_image) + + write_image(output_image, args.output) + # docs_tag: end_laplacian + # docs_tag: end_main + + +if __name__ == "__main__": + main() diff --git a/samples/operators/median_blur.py b/samples/operators/median_blur.py new file mode 100644 index 000000000..918c59cfa --- /dev/null +++ b/samples/operators/median_blur.py @@ -0,0 +1,51 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. +"""Simple Median Blur example with CVCUDA.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +import cvcuda + +common_dir = Path(__file__).parent.parent +sys.path.append(str(common_dir)) + +from common import parse_image_args, read_image, write_image # noqa: E402 + + +def main() -> None: + """Apply median blur to an image with CVCUDA.""" + # docs_tag: begin_main + args: argparse.Namespace = parse_image_args("cat_median_blur.jpg") + # docs_tag: begin_read_image + input_image: cvcuda.Tensor = read_image(args.input) + # docs_tag: end_read_image + + # docs_tag: begin_median_blur + # Apply median blur with a 7x7 kernel. + # ksize must be a list of two odd positive integers [kW, kH]. + # Larger kernels produce stronger smoothing and better noise removal + # at the cost of more detail loss. + output_image: cvcuda.Tensor = cvcuda.median_blur(input_image, [7, 7]) + write_image(output_image, args.output) + # docs_tag: end_median_blur + # docs_tag: end_main + + +if __name__ == "__main__": + main() diff --git a/samples/operators/morphology.py b/samples/operators/morphology.py new file mode 100644 index 000000000..5b5118ee8 --- /dev/null +++ b/samples/operators/morphology.py @@ -0,0 +1,86 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. +"""Simple Morphology example with CVCUDA.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +import cvcuda + +common_dir = Path(__file__).parent.parent +sys.path.append(str(common_dir)) + +from common import parse_image_args, read_image, write_image # noqa: E402 + + +def main() -> None: + """Apply morphological erosion and dilation to an image with CVCUDA.""" + # docs_tag: begin_main + args: argparse.Namespace = parse_image_args("cat_morphology.jpg") + # docs_tag: begin_read_image + input_image: cvcuda.Tensor = read_image(args.input) + # docs_tag: end_read_image + + # docs_tag: begin_morphology + # Wrap the HWC single image in a batch dimension (NHWC) so the morphology + # operator can process it. The operator accepts both HWC and NHWC layouts. + nhwc_image: cvcuda.Tensor = input_image.reshape((1, *input_image.shape), "NHWC") + + # A 5x5 rectangular structuring element is large enough to show a visible + # effect on a natural image without destroying structure. + mask_size = [5, 5] + # anchor=[-1, -1] centres the structuring element automatically. + anchor = [-1, -1] + + # DILATE expands bright regions — edges become thicker and fine dark lines + # are reduced. A workspace tensor is not required for a single iteration. + dilated: cvcuda.Tensor = cvcuda.morphology( + nhwc_image, + cvcuda.MorphologyType.DILATE, + mask_size, + anchor, + iteration=1, + border=cvcuda.Border.REPLICATE, + ) + + # ERODE is the dual of dilation — it shrinks bright regions and removes + # small bright specks. Running erode after dilate is a CLOSE operation, + # which suppresses small dark artifacts/holes while preserving larger + # structures. + workspace: cvcuda.Tensor = cvcuda.Tensor( + nhwc_image.shape, nhwc_image.dtype, nhwc_image.layout + ) + closed: cvcuda.Tensor = cvcuda.morphology( + dilated, + cvcuda.MorphologyType.ERODE, + mask_size, + anchor, + iteration=1, + border=cvcuda.Border.REPLICATE, + workspace=workspace, + ) + + # Remove the batch dimension before writing; write_image expects HWC. + result: cvcuda.Tensor = closed.reshape(closed.shape[1:], "HWC") + write_image(result, args.output) + # docs_tag: end_morphology + # docs_tag: end_main + + +if __name__ == "__main__": + main() diff --git a/samples/operators/normalize.py b/samples/operators/normalize.py new file mode 100644 index 000000000..941e4b20b --- /dev/null +++ b/samples/operators/normalize.py @@ -0,0 +1,112 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. +"""Simple Normalize example with CVCUDA.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +import cvcuda +import numpy as np + +common_dir = Path(__file__).parent.parent +sys.path.append(str(common_dir)) + +from common import ( # noqa: E402 + parse_image_args, + read_image, + write_image, + download_tensor, + upload_tensor, +) + + +def main() -> None: + """Normalize an image with CVCUDA using ImageNet mean and standard deviation.""" + # docs_tag: begin_main + args: argparse.Namespace = parse_image_args("cat_normalize.jpg") + # docs_tag: begin_read_image + input_image: cvcuda.Tensor = read_image(args.input) + # docs_tag: end_read_image + + # docs_tag: begin_normalize_setup + # The normalize operator expects base (mean) and scale (std) tensors with + # the same layout as the source. We use per-channel ImageNet statistics + # expressed as pixel values in [0, 255] so no manual pre-scaling is needed. + # Shape (1, 1, 3) broadcasts over height and width for an HWC image. + imagenet_mean = np.array( + [[[123.675, 116.28, 103.53]]], dtype=np.float32 + ) # R, G, B means × 255 + imagenet_std = np.array( + [[[58.395, 57.12, 57.375]]], dtype=np.float32 + ) # R, G, B stds × 255 + + base_tensor = cvcuda.Tensor(imagenet_mean.shape, np.float32, "HWC") + scale_tensor = cvcuda.Tensor(imagenet_std.shape, np.float32, "HWC") + upload_tensor(imagenet_mean, base_tensor) + upload_tensor(imagenet_std, scale_tensor) + + # Convert the uint8 HWC input to float32 so that the operator emits float32 + # output; a uint8 source would produce a clamped uint8 result that is + # unsuitable for visualising the normalized values. + uint8_host = download_tensor(input_image) + float32_host = uint8_host.astype(np.float32) + + float32_tensor = cvcuda.Tensor(float32_host.shape, np.float32, "HWC") + upload_tensor(float32_host, float32_tensor) + # docs_tag: end_normalize_setup + + # docs_tag: begin_normalize + # Apply mean-std normalization: out = (src - base) / scale. + # SCALE_IS_STDDEV tells the operator to treat the scale tensor as a standard + # deviation and apply out = (src - base) / (scale + epsilon) accordingly. + # globalscale and globalshift are multiplicative/additive post-processing + # factors applied after the per-pixel formula; both are 1.0/0.0 (identity) + # here because we only want the standard ImageNet normalization. + normalized: cvcuda.Tensor = cvcuda.normalize( + src=float32_tensor, + base=base_tensor, + scale=scale_tensor, + flags=cvcuda.NormalizeFlags.SCALE_IS_STDDEV, + globalscale=1.0, + globalshift=0.0, + epsilon=1e-5, + ) + # docs_tag: end_normalize + + # docs_tag: begin_normalize_postprocess + # The normalized output is in roughly [-2, 2]. To produce a viewable image + # we map that range linearly back to [0, 255] uint8 on the host, then upload + # the result to a new GPU tensor so write_image can save it. + norm_host = download_tensor(normalized) + + # Rescale: shift by ~2 (min of typical range) and compress to [0, 255] + norm_min = norm_host.min() + norm_max = norm_host.max() + scale_range = norm_max - norm_min if norm_max != norm_min else 1.0 + vis_host = ( + ((norm_host - norm_min) / scale_range * 255.0).clip(0, 255).astype(np.uint8) + ) + + upload_tensor(np.ascontiguousarray(vis_host), input_image) + write_image(input_image, args.output) + # docs_tag: end_normalize_postprocess + # docs_tag: end_main + + +if __name__ == "__main__": + main() diff --git a/samples/operators/osd.py b/samples/operators/osd.py new file mode 100644 index 000000000..6d5930bf4 --- /dev/null +++ b/samples/operators/osd.py @@ -0,0 +1,137 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. +"""Simple On-Screen Display example with CVCUDA.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +import cvcuda +import numpy as np + +common_dir = Path(__file__).parent.parent +sys.path.append(str(common_dir)) + +from common import parse_image_args, read_image, write_image # noqa: E402 + + +def main() -> None: + """Draw OSD elements onto an image with CVCUDA.""" + # docs_tag: begin_main + args: argparse.Namespace = parse_image_args("cat_osd.jpg") + # docs_tag: begin_read_image + input_image: cvcuda.Tensor = read_image(args.input) + # docs_tag: end_read_image + + # docs_tag: begin_osd_setup + # The OSD operator requires an NHWC batch tensor and a list-of-lists of + # elements — one inner list per image in the batch. We promote the single + # HWC image to a batch of 1 (N=1). + h, w, c = input_image.shape + nhwc_image: cvcuda.Tensor = input_image.reshape((1, h, w, c), "NHWC") + + # Build a representative set of OSD primitives. All coordinates are in + # pixels; colours are RGBA tuples. We scale positions relative to the + # image dimensions so the overlay looks reasonable on any input size. + box_x, box_y = w // 8, h // 8 + box_w, box_h = w // 4, h // 4 + + elements = cvcuda.Elements( + elements=[ + [ + # Filled bounding box drawn around the top-left region + cvcuda.BndBoxI( + box=(box_x, box_y, box_w, box_h), + thickness=3, + borderColor=(255, 255, 0), + fillColor=(0, 128, 255, 64), + ), + # Text label placed near the top-left of the image + cvcuda.Label( + utf8Text="CV-CUDA OSD", + fontSize=24, + tlPos=(box_x, box_y - 30 if box_y >= 30 else box_y + box_h + 5), + fontColor=(255, 255, 255), + bgColor=(0, 0, 0, 180), + ), + # Diagonal line across the image + cvcuda.Line( + pos0=(0, 0), + pos1=(w - 1, h - 1), + thickness=2, + color=(0, 255, 0), + ), + # Circle at the image centre + cvcuda.Circle( + centerPos=(w // 2, h // 2), + radius=min(w, h) // 8, + thickness=2, + borderColor=(255, 128, 0), + bgColor=(255, 128, 0, 48), + ), + # Arrow pointing inward from the right edge + cvcuda.Arrow( + pos0=(w - 1, h // 2), + pos1=(w * 3 // 4, h // 2), + arrowSize=12, + thickness=2, + color=(255, 0, 128), + ), + # Closed polygon (diamond shape) at the image centre + cvcuda.PolyLine( + points=np.array( + [ + [w // 2, h // 4], + [w * 3 // 4, h // 2], + [w // 2, h * 3 // 4], + [w // 4, h // 2], + ], + dtype=np.int32, + ), + thickness=2, + isClosed=True, + borderColor=(0, 255, 255), + fillColor=(0, 255, 255, 32), + ), + # Timestamp overlay in the bottom-left corner + cvcuda.Clock( + clockFormat=cvcuda.ClockFormat.YYMMDD_HHMMSS, + time=0, + fontSize=14, + tlPos=(10, h - 30 if h > 40 else 5), + fontColor=(255, 255, 0), + bgColor=(0, 0, 0, 160), + ), + ] + ] + ) + # docs_tag: end_osd_setup + + # docs_tag: begin_osd + # Run the OSD operator; it composites every element onto the image in-place + # (the output tensor has the same shape and dtype as the input). + output_nhwc: cvcuda.Tensor = cvcuda.osd(nhwc_image, elements) + + # Squeeze the batch dimension back to HWC so write_image accepts it. + output_image: cvcuda.Tensor = output_nhwc.reshape((h, w, c), "HWC") + write_image(output_image, args.output) + # docs_tag: end_osd + # docs_tag: end_main + + +if __name__ == "__main__": + main() diff --git a/samples/operators/pillowresize.py b/samples/operators/pillowresize.py new file mode 100644 index 000000000..9659fb6bb --- /dev/null +++ b/samples/operators/pillowresize.py @@ -0,0 +1,55 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. +"""Simple Pillow Resize example with CVCUDA.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +import cvcuda + +common_dir = Path(__file__).parent.parent +sys.path.append(str(common_dir)) + +from common import parse_image_args, read_image, write_image # noqa: E402 + + +def main() -> None: + """Pillow-quality resize an image with CVCUDA.""" + # docs_tag: begin_main + args: argparse.Namespace = parse_image_args("cat_pillowresize.jpg") + # docs_tag: begin_read_image + input_image: cvcuda.Tensor = read_image(args.input) + # docs_tag: end_read_image + + # docs_tag: begin_pillowresize + # Pillow-style resize uses high-quality downsampling filters (e.g. LANCZOS) + # that match PIL/Pillow output more closely than a plain bilinear resize. + # The output shape must be (H, W, C) for HWC tensors. + output_image: cvcuda.Tensor = cvcuda.pillowresize( + input_image, + (args.height, args.width, input_image.shape[2]), + cvcuda.Format.RGB8, + cvcuda.Interp.LANCZOS, + ) + write_image(output_image, args.output) + # docs_tag: end_pillowresize + # docs_tag: end_main + + +if __name__ == "__main__": + main() diff --git a/samples/operators/random_resized_crop.py b/samples/operators/random_resized_crop.py new file mode 100644 index 000000000..93273f1c4 --- /dev/null +++ b/samples/operators/random_resized_crop.py @@ -0,0 +1,82 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. +"""Simple Random Resized Crop example with CVCUDA.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +import cvcuda + +common_dir = Path(__file__).parent.parent +sys.path.append(str(common_dir)) + +from common import parse_image_args, read_image, write_image # noqa: E402 + + +def main() -> None: + """Apply random resized crop to an image with CVCUDA.""" + # docs_tag: begin_main + args: argparse.Namespace = parse_image_args("cat_random_resized_crop.jpg") + # docs_tag: begin_read_image + input_image: cvcuda.Tensor = read_image(args.input) + # docs_tag: end_read_image + + # docs_tag: begin_random_resized_crop_setup + # The operator works on batched (NHWC) tensors, so wrap the single HWC image + # in a batch dimension of size 1. The output shape must also carry the batch dim. + batched_input: cvcuda.Tensor = input_image.reshape((1, *input_image.shape), "NHWC") + _, _, _, c = batched_input.shape + output_h, output_w = args.height, args.width + output_shape = (1, output_h, output_w, c) + + # Scale bounds control what fraction of the original image area the crop covers. + # A min_scale of 0.08 and max_scale of 1.0 matches the standard torchvision + # RandomResizedCrop defaults used in ImageNet training pipelines. + min_scale: float = 0.08 + max_scale: float = 1.0 + + # Ratio bounds set the aspect-ratio range (width/height) for the crop window + # before it is resized to the target output dimensions. + min_ratio: float = 0.75 + max_ratio: float = 1.3333333 + seed: int = 42 + # docs_tag: end_random_resized_crop_setup + + # docs_tag: begin_random_resized_crop + # Apply a random crop of a random area/aspect-ratio sub-region, then resize it + # to the requested output size — all in a single GPU kernel launch. + output_image: cvcuda.Tensor = cvcuda.random_resized_crop( + batched_input, + output_shape, + min_scale, + max_scale, + min_ratio, + max_ratio, + cvcuda.Interp.LINEAR, + seed, + ) + + # Remove the batch dimension before writing so write_image receives an HWC tensor. + hwc_output: cvcuda.Tensor = output_image.reshape((output_h, output_w, c), "HWC") + write_image(hwc_output, args.output) + # docs_tag: end_random_resized_crop + # docs_tag: end_main + + +if __name__ == "__main__": + main() diff --git a/samples/operators/reformat.py b/samples/operators/reformat.py index 84acc1c0d..fcf9083b9 100644 --- a/samples/operators/reformat.py +++ b/samples/operators/reformat.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/samples/operators/remap.py b/samples/operators/remap.py new file mode 100644 index 000000000..65425c5a1 --- /dev/null +++ b/samples/operators/remap.py @@ -0,0 +1,96 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. +"""Simple Remap example with CVCUDA.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +import cvcuda +import numpy as np + +common_dir = Path(__file__).parent.parent +sys.path.append(str(common_dir)) + +from common import ( # noqa: E402 + parse_image_args, + read_image, + write_image, + upload_tensor, +) + + +def main() -> None: + """Remap (wave-distort) an image with CVCUDA.""" + # docs_tag: begin_main + args: argparse.Namespace = parse_image_args("cat_remap.jpg") + # docs_tag: begin_read_image + input_image: cvcuda.Tensor = read_image(args.input) + # docs_tag: end_read_image + + # docs_tag: begin_remap_setup + # Build a sinusoidal wave-distortion map in absolute coordinates. + # The map tensor must have shape (H, W, 1) with dtype _2F32 (two float32 + # values packed per element: [src_x, src_y]) or shape (H, W, 2) with dtype F32. + # We use shape (H, W, 2) / dtype F32 here so each pixel stores [src_x, src_y]. + height, width, _ = input_image.shape + + # Create grid of output pixel coordinates + ys = np.arange(height, dtype=np.float32) + xs = np.arange(width, dtype=np.float32) + grid_x, grid_y = np.meshgrid(xs, ys) # both (H, W) + + # Apply a sinusoidal horizontal and vertical wave displacement + amplitude = height * 0.04 # ~4 % of image height + freq_x = 2.0 * np.pi / width * 3 # 3 cycles across width + freq_y = 2.0 * np.pi / height * 3 # 3 cycles across height + + # Each output pixel at (y, x) samples the source at a displaced position, + # creating a ripple effect that is visually distinctive without clipping content. + src_x = grid_x + amplitude * np.sin(freq_y * grid_y) + src_y = grid_y + amplitude * np.sin(freq_x * grid_x) + + # Stack into (H, W, 2) array — channel 0 = src_x, channel 1 = src_y + map_np = np.stack([src_x, src_y], axis=2).astype(np.float32) + map_np = np.ascontiguousarray(map_np) + + # Allocate a GPU tensor for the map and upload it from the host. + # Layout "HWC" matches the (H, W, 2) shape; the operator sees 2 channels of F32. + map_tensor = cvcuda.Tensor(map_np.shape, cvcuda.Type.F32, "HWC") + upload_tensor(map_np, map_tensor) + # docs_tag: end_remap_setup + + # docs_tag: begin_remap + # Apply the remap operator. + # map_type=ABSOLUTE means map values are absolute source-pixel coordinates. + # src_interp=LINEAR gives smooth results on continuous displacement fields. + # border=REPLICATE pads the edges with the nearest border pixel instead of black. + output_image: cvcuda.Tensor = cvcuda.remap( + src=input_image, + map=map_tensor, + src_interp=cvcuda.Interp.LINEAR, + map_interp=cvcuda.Interp.NEAREST, + map_type=cvcuda.Remap.ABSOLUTE, + border=cvcuda.Border.REPLICATE, + ) + write_image(output_image, args.output) + # docs_tag: end_remap + # docs_tag: end_main + + +if __name__ == "__main__": + main() diff --git a/samples/operators/resize_crop_convert_reformat.py b/samples/operators/resize_crop_convert_reformat.py new file mode 100644 index 000000000..82727926f --- /dev/null +++ b/samples/operators/resize_crop_convert_reformat.py @@ -0,0 +1,118 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. +"""Simple Resize Crop Convert Reformat example with CVCUDA.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +import cvcuda +import numpy as np + +common_dir = Path(__file__).parent.parent +sys.path.append(str(common_dir)) + +from common import ( # noqa: E402 + parse_image_args, + read_image, + write_image, + download_tensor, + upload_tensor, +) + + +def nchw_f32_to_hwc_u8(tensor: cvcuda.Tensor) -> cvcuda.Tensor: + """Convert an NCHW float32 tensor back to HWC uint8 for saving. + + The batch dimension is removed (first image only) and the channel-last + memory layout expected by nvimgcodec is restored. + + Args: + tensor: NCHW or CHW float32 tensor from resize_crop_convert_reformat. + + Returns: + HWC uint8 cvcuda.Tensor suitable for write_image. + """ + # Download to host so we can reorder axes and clip. download_tensor honours + # the NCHW row pitch (the W rows are padded to an alignment boundary). + host = download_tensor(tensor) + + # Drop the batch dimension when present (NCHW -> CHW) + if host.ndim == 4: + host = host[0] # take first image in the batch + + # CHW -> HWC and scale back to [0, 255] + hwc = np.transpose(host, (1, 2, 0)) + hwc = np.clip(hwc, 0.0, 255.0).astype(np.uint8) + hwc = np.ascontiguousarray(hwc) + + h, w, c = hwc.shape + out_tensor = cvcuda.Tensor((h, w, c), cvcuda.Type.U8, "HWC") + upload_tensor(hwc, out_tensor) + return out_tensor + + +def main() -> None: + """Resize, crop, convert, and reformat an image with CVCUDA.""" + # docs_tag: begin_main + args: argparse.Namespace = parse_image_args("cat_resize_crop_convert_reformat.jpg") + # docs_tag: begin_read_image + input_image: cvcuda.Tensor = read_image(args.input) + # docs_tag: end_read_image + + # docs_tag: begin_resize_crop_convert_reformat_setup + # Wrap the single HWC image in a batch dimension so we can use the NHWC + # path, which also demonstrates the typical DL pipeline usage pattern. + nhwc_image: cvcuda.Tensor = input_image.reshape( + (1, *input_image.shape), layout="NHWC" + ) + + # Target resize and crop dimensions. We resize to the requested + # height x width (224x224 by default), then crop a region of the same size + # from the top-left corner — a typical pre-processing pipeline. + resize_dim = (args.height, args.width) # (H, W) after resize + crop_w = min(args.width, resize_dim[1]) + crop_h = min(args.height, resize_dim[0]) + crop_rect = cvcuda.RectI(0, 0, crop_w, crop_h) + # docs_tag: end_resize_crop_convert_reformat_setup + + # docs_tag: begin_resize_crop_convert_reformat + # Fused pipeline: resize → crop → convert to float32 → reformat to NCHW. + # ChannelManip.REVERSE swaps BGR↔RGB in one pass, which is common when + # feeding models trained with a different channel ordering than the codec. + output: cvcuda.Tensor = cvcuda.resize_crop_convert_reformat( + nhwc_image, + resize_dim, + cvcuda.Interp.LINEAR, + crop_rect, + layout="NCHW", + data_type=cvcuda.Type.F32, + manip=cvcuda.ChannelManip.REVERSE, + scale=1.0, + offset=0.0, + ) + # docs_tag: end_resize_crop_convert_reformat + + # The output is NCHW float32; convert back to HWC uint8 so that the + # image encoder can save it as a standard JPEG. + viewable: cvcuda.Tensor = nchw_f32_to_hwc_u8(output) + write_image(viewable, args.output) + # docs_tag: end_main + + +if __name__ == "__main__": + main() diff --git a/samples/operators/rotate.py b/samples/operators/rotate.py new file mode 100644 index 000000000..393a15ec4 --- /dev/null +++ b/samples/operators/rotate.py @@ -0,0 +1,73 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. +"""Simple Rotate example with CVCUDA.""" + +from __future__ import annotations + +import argparse +import math +import sys +from pathlib import Path + +import cvcuda + +common_dir = Path(__file__).parent.parent +sys.path.append(str(common_dir)) + +from common import parse_image_args, read_image, write_image # noqa: E402 + + +def main() -> None: + """Rotate an image with CVCUDA.""" + # docs_tag: begin_main + args: argparse.Namespace = parse_image_args("cat_rotate.jpg") + # docs_tag: begin_read_image + input_image: cvcuda.Tensor = read_image(args.input) + # docs_tag: end_read_image + + # docs_tag: begin_rotate_setup + # cvcuda.rotate performs the inverse mapping: + # src = R(angle) * (dst - shift) + # where R is a counter-clockwise rotation matrix (screen coords, y-down). + # To keep the image centre fixed we solve for the shift such that + # dst=(cx,cy) maps back to src=(cx,cy), giving: + # shift = (cx*(1-cos) - cy*sin, cy*(1-cos) + cx*sin) + h, w = input_image.shape[0], input_image.shape[1] + angle_deg = 45.0 + angle_rad = math.radians(angle_deg) + cos_a = math.cos(angle_rad) + sin_a = math.sin(angle_rad) + cx, cy = w / 2.0, h / 2.0 + shift_x = cx * (1 - cos_a) - cy * sin_a + shift_y = cy * (1 - cos_a) + cx * sin_a + # docs_tag: end_rotate_setup + + # docs_tag: begin_rotate + # Rotate the image by 45 degrees with bilinear interpolation. + # The shift [shift_x, shift_y] re-centres the content after rotation so the + # subject remains visible rather than drifting off-canvas. + output_image: cvcuda.Tensor = cvcuda.rotate( + input_image, + angle_deg, + [shift_x, shift_y], + cvcuda.Interp.LINEAR, + ) + write_image(output_image, args.output) + # docs_tag: end_rotate + # docs_tag: end_main + + +if __name__ == "__main__": + main() diff --git a/samples/operators/stack.py b/samples/operators/stack.py index 45490c193..5d2f5800e 100644 --- a/samples/operators/stack.py +++ b/samples/operators/stack.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/samples/operators/threshold.py b/samples/operators/threshold.py new file mode 100644 index 000000000..e8cf88af2 --- /dev/null +++ b/samples/operators/threshold.py @@ -0,0 +1,79 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. +"""Simple Threshold example with CVCUDA.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +import cvcuda +import numpy as np + +common_dir = Path(__file__).parent.parent +sys.path.append(str(common_dir)) + +from common import ( # noqa: E402 + parse_image_args, + read_image, + write_image, + cuda_memcpy_h2d, +) + + +def main() -> None: + """Apply binary threshold to an image with CVCUDA.""" + # docs_tag: begin_main + args: argparse.Namespace = parse_image_args("cat_threshold.jpg") + # docs_tag: begin_read_image + input_image: cvcuda.Tensor = read_image(args.input) + # docs_tag: end_read_image + + # docs_tag: begin_threshold_setup + # threshold() requires a batch dimension (NHWC), so wrap the HWC image. + # One thresh/maxval scalar is needed per image in the batch. + nhwc_image: cvcuda.Tensor = input_image.reshape((1, *input_image.shape), "NHWC") + batch_size = nhwc_image.shape[0] + + # Allocate per-image threshold and maxval tensors on the GPU (dtype F64, layout "N"). + thresh_host = np.array([128.0] * batch_size, dtype=np.float64) + thresh_tensor = cvcuda.Tensor((batch_size,), dtype=np.float64, layout="N") + cuda_memcpy_h2d(thresh_host, thresh_tensor.cuda()) + + maxval_host = np.array([255.0] * batch_size, dtype=np.float64) + maxval_tensor = cvcuda.Tensor((batch_size,), dtype=np.float64, layout="N") + cuda_memcpy_h2d(maxval_host, maxval_tensor.cuda()) + # docs_tag: end_threshold_setup + + # docs_tag: begin_threshold + # Apply BINARY threshold: pixels > thresh become maxval, others become 0. + # The operator returns an NHWC tensor of the same shape and dtype as the input. + thresholded: cvcuda.Tensor = cvcuda.threshold( + src=nhwc_image, + thresh=thresh_tensor, + maxval=maxval_tensor, + type=cvcuda.ThresholdType.BINARY, + ) + # docs_tag: end_threshold + + # Strip the batch dimension back to HWC so write_image accepts it. + out_hwc: cvcuda.Tensor = thresholded.reshape(thresholded.shape[1:], "HWC") + write_image(out_hwc, args.output) + # docs_tag: end_main + + +if __name__ == "__main__": + main() diff --git a/samples/operators/warp_affine.py b/samples/operators/warp_affine.py new file mode 100644 index 000000000..8e37260ce --- /dev/null +++ b/samples/operators/warp_affine.py @@ -0,0 +1,79 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. +"""Simple Warp Affine example with CVCUDA.""" + +from __future__ import annotations + +import argparse +import math +import sys +from pathlib import Path + +import cvcuda +import numpy as np + +common_dir = Path(__file__).parent.parent +sys.path.append(str(common_dir)) + +from common import parse_image_args, read_image, write_image # noqa: E402 + + +def main() -> None: + """Apply a rotation+translation affine warp to an image with CVCUDA.""" + # docs_tag: begin_main + args: argparse.Namespace = parse_image_args("cat_warp_affine.jpg") + # docs_tag: begin_read_image + input_image: cvcuda.Tensor = read_image(args.input) + # docs_tag: end_read_image + + # docs_tag: begin_warp_affine_setup + # Build a 2x3 float32 affine matrix that rotates the image 15 degrees + # counter-clockwise about the image centre and shifts it slightly right. + # OpenCV convention: the matrix maps *destination* pixel coordinates to + # *source* pixel coordinates (inverse warp), so we use a rotation of -angle. + h, w = input_image.shape[0], input_image.shape[1] + angle_deg = 15.0 + angle_rad = math.radians(angle_deg) + cos_a = math.cos(angle_rad) + sin_a = math.sin(angle_rad) + cx, cy = w / 2.0, h / 2.0 + # Rotation about the image centre + a small horizontal translation + tx = 20.0 # pixels to shift right + xform = np.array( + [ + [cos_a, sin_a, (1 - cos_a) * cx - sin_a * cy + tx], + [-sin_a, cos_a, sin_a * cx + (1 - cos_a) * cy], + ], + dtype=np.float32, + ) + # docs_tag: end_warp_affine_setup + + # docs_tag: begin_warp_affine + # Apply the affine transformation with bilinear interpolation. + # Pixels that fall outside the source image are filled with black (CONSTANT, value=0). + output_image: cvcuda.Tensor = cvcuda.warp_affine( + input_image, + xform, + cvcuda.Interp.LINEAR, + border_mode=cvcuda.Border.CONSTANT, + border_value=[0], + ) + write_image(output_image, args.output) + # docs_tag: end_warp_affine + # docs_tag: end_main + + +if __name__ == "__main__": + main() diff --git a/samples/operators/warp_perspective.py b/samples/operators/warp_perspective.py new file mode 100644 index 000000000..5f3d1c568 --- /dev/null +++ b/samples/operators/warp_perspective.py @@ -0,0 +1,97 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. +"""Simple Warp Perspective example with CVCUDA.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +import cvcuda +import numpy as np + +common_dir = Path(__file__).parent.parent +sys.path.append(str(common_dir)) + +from common import parse_image_args, read_image, write_image # noqa: E402 + + +def main() -> None: + """Apply a perspective warp transform to an image with CVCUDA.""" + # docs_tag: begin_main + args: argparse.Namespace = parse_image_args("cat_warp_perspective.jpg") + # docs_tag: begin_read_image + input_image: cvcuda.Tensor = read_image(args.input) + # docs_tag: end_read_image + + h, w = input_image.shape[0], input_image.shape[1] + + # docs_tag: begin_warp_perspective_setup + # Build a perspective matrix that applies a mild keystone / tilt effect. + # The matrix maps destination pixel (x, y) to source pixel via homogeneous + # coordinates: [x_src, y_src, w] = M @ [x_dst, y_dst, 1]. + # We nudge the top-right and bottom-left corners inward so the image + # appears to recede into the distance without leaving empty regions. + src_pts = np.array( + [[0, 0], [w, 0], [w, h], [0, h]], + dtype=np.float32, + ) + dst_pts = np.array( + [ + [w * 0.1, h * 0.05], + [w * 0.9, h * 0.1], + [w * 0.85, h * 0.95], + [w * 0.15, h * 0.9], + ], + dtype=np.float32, + ) + + # Use OpenCV-compatible 3x3 float32 perspective matrix expected by cvcuda.warp_perspective. + # We compute it manually via the 4-point DLT (Direct Linear Transform). + def _get_perspective_transform(src: np.ndarray, dst: np.ndarray) -> np.ndarray: + """Compute 3x3 perspective matrix from 4 point correspondences (DLT).""" + A = [] + for (sx, sy), (dx, dy) in zip(src, dst, strict=True): + A.append([-sx, -sy, -1, 0, 0, 0, dx * sx, dx * sy, dx]) + A.append([0, 0, 0, -sx, -sy, -1, dy * sx, dy * sy, dy]) + A_mat = np.array(A, dtype=np.float64) + _, _, Vt = np.linalg.svd(A_mat) + H = Vt[-1].reshape(3, 3) + return (H / H[2, 2]).astype(np.float32) + + xform = _get_perspective_transform(dst_pts, src_pts) + # docs_tag: end_warp_perspective_setup + + # docs_tag: begin_warp_perspective + # Apply the perspective warp. We pass xform as a plain Python list-of-lists + # (or a numpy array); cvcuda converts it internally to float32. + # WARP_INVERSE_MAP tells the kernel that xform maps destination → source, + # which is the convention used by the matrix we computed above. + output_image: cvcuda.Tensor = cvcuda.warp_perspective( + input_image, + xform.tolist(), + cvcuda.Interp.LINEAR | cvcuda.Interp.WARP_INVERSE_MAP, + border_mode=cvcuda.Border.CONSTANT, + border_value=[0], + ) + # docs_tag: end_warp_perspective + + write_image(output_image, args.output) + # docs_tag: end_main + + +if __name__ == "__main__": + main() diff --git a/samples/requirements_interop_common.txt b/samples/requirements.samples.common.template similarity index 52% rename from samples/requirements_interop_common.txt rename to samples/requirements.samples.common.template index 4eb266013..e0074f81d 100644 --- a/samples/requirements_interop_common.txt +++ b/samples/requirements.samples.common.template @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,15 +13,21 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Common Python packages for CV-CUDA interoperability samples -# Used by both CUDA 12 and CUDA 13 interop samples -# Note: This is lighter than full samples requirements +# Template for requirements.samples.common.txt — to regenerate, run from the repo root: +# bash generate_requirements.sh + +# Common Python packages for CV-CUDA samples +# Used by both CUDA 12 and CUDA 13 samples # NumPy 2 with version constraints based on Python version -numpy==2.0.2; python_version=="3.9" -numpy==2.2.6; python_version>="3.10" and python_version<"3.14" -numpy==2.3.3; python_version=="3.14" +numpy==${numpy_v2_py310}; python_version=="3.10" +numpy==${numpy_v2_py311plus}; python_version>="3.11" + +# Sample dependencies +onnx==${onnx} +onnxscript==${onnxscript} +onnxslim==${onnxslim} # Interop dependencies -pycuda==2025.1.2 -PyNvVideoCodec==2.0.3; (platform_machine == "x86_64" or platform_machine == "amd64") and python_version>="3.10" and python_version<="3.13" +pycuda==${pycuda} +PyNvVideoCodec==${pynvvideocodec}; (platform_machine == "x86_64" or platform_machine == "amd64") and python_version>="3.10" and python_version<="3.12" diff --git a/samples/requirements_samples_cu12.txt b/samples/requirements.samples.cu12.template similarity index 53% rename from samples/requirements_samples_cu12.txt rename to samples/requirements.samples.cu12.template index 3e7c942c1..ab3cc8dfd 100644 --- a/samples/requirements_samples_cu12.txt +++ b/samples/requirements.samples.cu12.template @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,24 +13,29 @@ # See the License for the specific language governing permissions and # limitations under the License. +# Template for requirements.samples.cu12.txt — to regenerate, run from the repo root: +# bash generate_requirements.sh + # CUDA 12-specific Python packages for CV-CUDA samples # Include common requirements --r requirements_samples_common.txt +-r requirements.samples.common.txt -# PyTorch and torchvision for CUDA 12.1 ---extra-index-url https://download.pytorch.org/whl/cu121 -torch==2.8.0 -torchvision==0.23.0 +# PyTorch + torchvision (paired versions) for CUDA 12.x (${torch_cu12_index} wheel index, compatible with CUDA 12.5+) +--extra-index-url https://download.pytorch.org/whl/${torch_cu12_index} +torch==${torch_cu12} +torchvision==${torchvision_cu12} # CUDA 12-specific packages --extra-index-url https://pypi.nvidia.com/ -cuda-python==12.9.4 -nvidia-nvimgcodec-cu12==0.6.1.37 -nvidia-nvjpeg-cu12==12.4.0.76 +cuda-python==${cuda_python_cu12} +nvidia-nvimgcodec-cu12==${nvimgcodec} +nvidia-nvjpeg-cu12==${nvjpeg_cu12} +cupy-cuda12x==${cupy_cu12} # TensorRT for CUDA 12 (skip on aarch64 - handled in install script) -tensorrt-cu12==10.13.3.9; platform_machine != "aarch64" +# Using .post1 version which has fixed dependencies (uses cuda-toolkit instead of deprecated packages) +tensorrt-cu12==${tensorrt_cu12}; platform_machine != "aarch64" # CV-CUDA for CUDA 12 cvcuda-cu12 diff --git a/samples/requirements_samples_cu13.txt b/samples/requirements.samples.cu13.template similarity index 55% rename from samples/requirements_samples_cu13.txt rename to samples/requirements.samples.cu13.template index 2c9db5fa4..cc4dcfb28 100644 --- a/samples/requirements_samples_cu13.txt +++ b/samples/requirements.samples.cu13.template @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,23 +13,29 @@ # See the License for the specific language governing permissions and # limitations under the License. +# Template for requirements.samples.cu13.txt — to regenerate, run from the repo root: +# bash generate_requirements.sh + # CUDA 13-specific Python packages for CV-CUDA samples # Include common requirements --r requirements_samples_common.txt +-r requirements.samples.common.txt -# PyTorch and torchvision for CUDA 13.0 ---extra-index-url https://download.pytorch.org/whl/cu130 -torch==2.9.0 -torchvision==0.24.0 +# PyTorch + torchvision (paired versions) for CUDA 13.x (${torch_cu13_index} wheel index, compatible with CUDA 13.3+) +--extra-index-url https://download.pytorch.org/whl/${torch_cu13_index} +torch==${torch_cu13} +torchvision==${torchvision_cu13} # CUDA 13-specific packages --extra-index-url https://pypi.nvidia.com/ -cuda-python==13.0.3 -nvidia-nvimgcodec-cu13==0.6.1.37 +cuda-python==${cuda_python_cu13} +nvidia-nvimgcodec-cu13==${nvimgcodec} +nvidia-nvjpeg==${nvjpeg_cu13} +cupy-cuda13x==${cupy_cu13} # TensorRT for CUDA 13 (skip on aarch64 - handled in install script) -tensorrt-cu13==10.13.3.9; platform_machine != "aarch64" +# TensorRT 11.1 is the first release with Python 3.14 bindings. +tensorrt-cu13==${tensorrt_cu13}; platform_machine != "aarch64" # CV-CUDA for CUDA 13 cvcuda-cu13 diff --git a/samples/requirements_hello_world_cu12.txt b/samples/requirements.samples.hello_world_cu12.template similarity index 71% rename from samples/requirements_hello_world_cu12.txt rename to samples/requirements.samples.hello_world_cu12.template index e3eae57ba..e2338e5f9 100644 --- a/samples/requirements_hello_world_cu12.txt +++ b/samples/requirements.samples.hello_world_cu12.template @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,14 +13,17 @@ # See the License for the specific language governing permissions and # limitations under the License. +# Template for requirements.samples.hello_world_cu12.txt — to regenerate, run from the repo root: +# bash generate_requirements.sh + # Minimal requirements for the hello_world.py sample only (CUDA 12) # This is a lightweight option for quick testing and getting started with CV-CUDA # # For running other samples (operators, applications, interoperability), # please use the installation script: ./install_dependencies.sh -cuda-python==12.9.4 +cuda-python==${cuda_python_cu12} cvcuda-cu12 -nvidia-nvimgcodec-cu12==0.6.1.37 -numpy==2.0.2 ; python_version < '3.10' -numpy==2.2.6 ; python_version >= '3.10' +nvidia-nvimgcodec-cu12==${nvimgcodec} +numpy==${numpy_v2_py310}; python_version=="3.10" +numpy==${numpy_v2_py311plus}; python_version>="3.11" diff --git a/samples/requirements_hello_world_cu13.txt b/samples/requirements.samples.hello_world_cu13.template similarity index 71% rename from samples/requirements_hello_world_cu13.txt rename to samples/requirements.samples.hello_world_cu13.template index 1314d2956..898acc257 100644 --- a/samples/requirements_hello_world_cu13.txt +++ b/samples/requirements.samples.hello_world_cu13.template @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,14 +13,17 @@ # See the License for the specific language governing permissions and # limitations under the License. +# Template for requirements.samples.hello_world_cu13.txt — to regenerate, run from the repo root: +# bash generate_requirements.sh + # Minimal requirements for the hello_world.py sample only (CUDA 13) # This is a lightweight option for quick testing and getting started with CV-CUDA # # For running other samples (operators, applications, interoperability), # please use the installation script: ./install_dependencies.sh -cuda-python==13.0.3 +cuda-python==${cuda_python_cu13} cvcuda-cu13 -nvidia-nvimgcodec-cu13==0.6.1.37 -numpy==2.0.2 ; python_version < '3.10' -numpy==2.2.6 ; python_version >= '3.10' +nvidia-nvimgcodec-cu13==${nvimgcodec} +numpy==${numpy_v2_py310}; python_version=="3.10" +numpy==${numpy_v2_py311plus}; python_version>="3.11" diff --git a/samples/requirements_interop_cu12.txt b/samples/requirements_interop_cu12.txt deleted file mode 100644 index 04153927c..000000000 --- a/samples/requirements_interop_cu12.txt +++ /dev/null @@ -1,32 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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. - -# CUDA 12-specific Python packages for CV-CUDA interoperability samples - -# Include common interop requirements --r requirements_interop_common.txt - -# PyTorch for CUDA 12.1 (only torch, interop samples don't need torchvision) ---extra-index-url https://download.pytorch.org/whl/cu121 -torch==2.8.0 - -# CUDA 12-specific packages ---extra-index-url https://pypi.nvidia.com/ -cuda-python==12.9.4 -nvidia-nvimgcodec-cu12==0.6.1.37 -cupy-cuda12x==13.6.0 - -# CV-CUDA for CUDA 12 -cvcuda-cu12 diff --git a/samples/requirements_interop_cu13.txt b/samples/requirements_interop_cu13.txt deleted file mode 100644 index 3dd2c5d03..000000000 --- a/samples/requirements_interop_cu13.txt +++ /dev/null @@ -1,32 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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. - -# CUDA 13-specific Python packages for CV-CUDA interoperability samples - -# Include common interop requirements --r requirements_interop_common.txt - -# PyTorch for CUDA 13.0 (only torch, interop samples don't need torchvision) ---extra-index-url https://download.pytorch.org/whl/cu130 -torch==2.9.0 - -# CUDA 13-specific packages ---extra-index-url https://pypi.nvidia.com/ -cuda-python==13.0.3 -nvidia-nvimgcodec-cu13==0.6.1.37 -cupy-cuda13x==13.6.0 - -# CV-CUDA for CUDA 13 -cvcuda-cu13 diff --git a/samples/run_samples.sh b/samples/run_samples.sh index a089f2753..630e266c6 100755 --- a/samples/run_samples.sh +++ b/samples/run_samples.sh @@ -1,6 +1,6 @@ #!/bin/bash -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,27 +17,55 @@ # Get the directory where this script is located SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SEPARATOR_LINE="======================================================================" # Function to run samples and report status run_samples() { local category="$1" local directory="$2" local failed_tests=() + local temp_output for script in "$SCRIPT_DIR"/"$directory"/*.py; do echo -n "Running $category: $script ... " - if [ -f "$script" ]; then - if python3 "$script" > /dev/null 2>&1; then - echo "PASSED" + if [[ -f "$script" ]]; then + # Create a temporary file to capture output + temp_output=$(mktemp) + + if python3 "$script" > "$temp_output" 2>&1; then + # Check if output contains warnings or skip messages + if grep -qiE "(warning|skipping)" "$temp_output"; then + echo "PASSED (with warnings)" + echo "$SEPARATOR_LINE" + echo "OUTPUT from $(basename "$script"):" + echo "$SEPARATOR_LINE" + cat "$temp_output" + echo "$SEPARATOR_LINE" + echo "" + else + echo "PASSED" + fi + rm -f "$temp_output" else - echo "FAILED" + exit_code=$? + echo "FAILED (exit code: $exit_code)" failed_tests+=("$(basename "$script")") + + # Display the error output for debugging + echo "$SEPARATOR_LINE" + echo "ERROR OUTPUT from $(basename "$script"):" >&2 + echo "$SEPARATOR_LINE" + cat "$temp_output" + echo "$SEPARATOR_LINE" + echo "" + + rm -f "$temp_output" fi fi done # Print summary if there were failures - if [ ${#failed_tests[@]} -gt 0 ]; then + if [[ ${#failed_tests[@]} -gt 0 ]]; then echo "" echo "FAILED ${#failed_tests[@]} TEST(S): ${failed_tests[*]}" return 1 @@ -50,22 +78,27 @@ run_samples() { if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then # Run samples for each category run_samples "Data Types" "datatypes" - if [ $? -ne 0 ]; then + if [[ $? -ne 0 ]]; then exit 1 fi run_samples "Object Cache" "object_cache" - if [ $? -ne 0 ]; then + if [[ $? -ne 0 ]]; then exit 1 fi run_samples "Operator" "operators" - if [ $? -ne 0 ]; then + if [[ $? -ne 0 ]]; then exit 1 fi run_samples "Application" "applications" - if [ $? -ne 0 ]; then + if [[ $? -ne 0 ]]; then + exit 1 + fi + + run_samples "Interoperability" "interoperability" + if [[ $? -ne 0 ]]; then exit 1 fi fi diff --git a/src/cvcuda/CMakeLists.txt b/src/cvcuda/CMakeLists.txt index 64c6c3d93..63ac0f905 100644 --- a/src/cvcuda/CMakeLists.txt +++ b/src/cvcuda/CMakeLists.txt @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,9 +18,19 @@ add_subdirectory(util) # cvcuda private implementation add_subdirectory(priv) -set(CV_CUDA_LIB_FILES Operator.cpp) +set(CV_CUDA_LIB_FILES Operator.cpp Types.cpp) set(CV_CUDA_OP_FILES + OpJpegCompressionDistortion.cpp + OpAdjustHue.cpp + OpAdjustSaturation.cpp + OpAdjustSharpness.cpp + OpAdjustContrast.cpp + OpInvert.cpp + OpSolarize.cpp + OpPosterize.cpp + OpAutoContrast.cpp + OpCLAHE.cpp OpOSD.cpp OpHistogramEq.cpp OpAdvCvtColor.cpp @@ -106,6 +116,7 @@ target_link_libraries(cvcuda nvcv_util nvcv_util_symver cvcuda_priv + cvcuda_nvtx_config ) target_include_directories(cvcuda diff --git a/src/cvcuda/OpAdaptiveThreshold.cpp b/src/cvcuda/OpAdaptiveThreshold.cpp index 6a96bf2b1..6b43448f7 100644 --- a/src/cvcuda/OpAdaptiveThreshold.cpp +++ b/src/cvcuda/OpAdaptiveThreshold.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "priv/OpAdaptiveThreshold.hpp" +#include "priv/Nvtx.hpp" #include "priv/SymbolVersioning.hpp" #include @@ -30,7 +31,7 @@ CVCUDA_DEFINE_API(0, 3, NVCVStatus, cvcudaAdaptiveThresholdCreate, (NVCVOperatorHandle * handle, int32_t maxBlockSize, int32_t maxVarShapeBatchSize)) { return nvcv::ProtectCall( - [&] + [&handle, &maxBlockSize, &maxVarShapeBatchSize] { if (handle == nullptr) { @@ -38,8 +39,7 @@ CVCUDA_DEFINE_API(0, 3, NVCVStatus, cvcudaAdaptiveThresholdCreate, "Pointer to NVCVOperator handle must not be NULL"); } - *handle - = reinterpret_cast(new priv::AdaptiveThreshold(maxBlockSize, maxVarShapeBatchSize)); + *handle = priv::CreateOperatorHandle(maxBlockSize, maxVarShapeBatchSize); }); } @@ -48,12 +48,14 @@ CVCUDA_DEFINE_API(0, 3, NVCVStatus, cvcudaAdaptiveThresholdSubmit, double maxValue, NVCVAdaptiveThresholdType adaptiveMethod, NVCVThresholdType thresholdType, int32_t blockSize, double c)) { + CVCUDA_NVTX_RANGE("cvcudaAdaptiveThresholdSubmit"); return nvcv::ProtectCall( - [&] + [&out, &in, &handle, &stream, &maxValue, &adaptiveMethod, &thresholdType, &blockSize, &c] { - nvcv::TensorWrapHandle output(out), input(in); - priv::ToDynamicRef(handle)(stream, input, output, maxValue, adaptiveMethod, - thresholdType, blockSize, c); + nvcv::TensorWrapHandle output(out); + nvcv::TensorWrapHandle input(in); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), maxValue, + adaptiveMethod, thresholdType, blockSize, c); }); } @@ -62,12 +64,17 @@ CVCUDA_DEFINE_API(0, 3, NVCVStatus, cvcudaAdaptiveThresholdVarShapeSubmit, NVCVTensorHandle maxValue, NVCVAdaptiveThresholdType adaptiveMethod, NVCVThresholdType thresholdType, NVCVTensorHandle blockSize, NVCVTensorHandle c)) { + CVCUDA_NVTX_RANGE("cvcudaAdaptiveThresholdVarShapeSubmit"); return nvcv::ProtectCall( - [&] + [&out, &in, &maxValue, &blockSize, &c, &handle, &stream, &adaptiveMethod, &thresholdType] { - nvcv::ImageBatchVarShapeWrapHandle output(out), input(in); - nvcv::TensorWrapHandle maxvalueVec(maxValue), blocksizeVec(blockSize), cVec(c); - priv::ToDynamicRef(handle)(stream, input, output, maxvalueVec, adaptiveMethod, - thresholdType, blocksizeVec, cVec); + nvcv::ImageBatchVarShapeWrapHandle output(out); + nvcv::ImageBatchVarShapeWrapHandle input(in); + nvcv::TensorWrapHandle maxvalueVec(maxValue); + nvcv::TensorWrapHandle blocksizeVec(blockSize); + nvcv::TensorWrapHandle cVec(c); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), + maxvalueVec.resource(), adaptiveMethod, thresholdType, + blocksizeVec.resource(), cVec.resource()); }); } diff --git a/src/cvcuda/OpAdjustContrast.cpp b/src/cvcuda/OpAdjustContrast.cpp new file mode 100644 index 000000000..6b08b4079 --- /dev/null +++ b/src/cvcuda/OpAdjustContrast.cpp @@ -0,0 +1,74 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "priv/OpAdjustContrast.hpp" + +#include "priv/Nvtx.hpp" +#include "priv/SymbolVersioning.hpp" + +#include +#include +#include + +namespace priv = cvcuda::priv; + +CVCUDA_DEFINE_API(0, 0, NVCVStatus, cvcudaAdjustContrastCreate, (NVCVOperatorHandle * handle)) +{ + return nvcv::ProtectCall( + [handle] + { + if (handle == nullptr) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Pointer to NVCVOperator handle must not be NULL"); + } + + // Ownership of the operator is transferred to the C handle and released by + // cvcudaOperatorDestroy; this matches every other operator's Create entry point. + *handle = reinterpret_cast(new priv::AdjustContrast()); // NOSONAR + }); +} + +CVCUDA_DEFINE_API(0, 0, NVCVStatus, cvcudaAdjustContrastSubmit, + (NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle in, NVCVTensorHandle out, + double contrastFactor)) +{ + CVCUDA_NVTX_RANGE("cvcudaAdjustContrastSubmit"); + return nvcv::ProtectCall( + [handle, stream, in, out, contrastFactor] + { + nvcv::TensorWrapHandle input(in); + nvcv::TensorWrapHandle output(out); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), + contrastFactor); + }); +} + +CVCUDA_DEFINE_API(0, 0, NVCVStatus, cvcudaAdjustContrastVarShapeSubmit, + (NVCVOperatorHandle handle, cudaStream_t stream, NVCVImageBatchHandle in, NVCVImageBatchHandle out, + double contrastFactor)) +{ + CVCUDA_NVTX_RANGE("cvcudaAdjustContrastVarShapeSubmit"); + return nvcv::ProtectCall( + [handle, stream, in, out, contrastFactor] + { + nvcv::ImageBatchVarShapeWrapHandle input(in); + nvcv::ImageBatchVarShapeWrapHandle output(out); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), + contrastFactor); + }); +} diff --git a/src/cvcuda/OpAdjustHue.cpp b/src/cvcuda/OpAdjustHue.cpp new file mode 100644 index 000000000..2117cae18 --- /dev/null +++ b/src/cvcuda/OpAdjustHue.cpp @@ -0,0 +1,72 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "priv/OpAdjustHue.hpp" + +#include "priv/Nvtx.hpp" +#include "priv/SymbolVersioning.hpp" + +#include +#include +#include + +namespace priv = cvcuda::priv; + +CVCUDA_DEFINE_API(0, 0, NVCVStatus, cvcudaAdjustHueCreate, (NVCVOperatorHandle * handle)) +{ + return nvcv::ProtectCall( + [handle] + { + if (handle == nullptr) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Pointer to NVCVOperator handle must not be NULL"); + } + + // Ownership of the operator is transferred to the C handle and released by + // cvcudaOperatorDestroy; this matches every other operator's Create entry point. + *handle = reinterpret_cast(new priv::AdjustHue()); // NOSONAR + }); +} + +CVCUDA_DEFINE_API(0, 0, NVCVStatus, cvcudaAdjustHueSubmit, + (NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle in, NVCVTensorHandle out, + double hue)) +{ + CVCUDA_NVTX_RANGE("cvcudaAdjustHueSubmit"); + return nvcv::ProtectCall( + [handle, stream, in, out, hue] + { + nvcv::TensorWrapHandle input(in); + nvcv::TensorWrapHandle output(out); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), hue); + }); +} + +CVCUDA_DEFINE_API(0, 0, NVCVStatus, cvcudaAdjustHueVarShapeSubmit, + (NVCVOperatorHandle handle, cudaStream_t stream, NVCVImageBatchHandle in, NVCVImageBatchHandle out, + double hue)) +{ + CVCUDA_NVTX_RANGE("cvcudaAdjustHueVarShapeSubmit"); + return nvcv::ProtectCall( + [handle, stream, in, out, hue] + { + nvcv::ImageBatchVarShapeWrapHandle input(in); + nvcv::ImageBatchVarShapeWrapHandle output(out); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), hue); + }); +} diff --git a/src/cvcuda/OpAdjustSaturation.cpp b/src/cvcuda/OpAdjustSaturation.cpp new file mode 100644 index 000000000..23e9a5f11 --- /dev/null +++ b/src/cvcuda/OpAdjustSaturation.cpp @@ -0,0 +1,72 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "priv/OpAdjustSaturation.hpp" + +#include "priv/Nvtx.hpp" +#include "priv/SymbolVersioning.hpp" + +#include +#include +#include + +namespace priv = cvcuda::priv; + +CVCUDA_DEFINE_API(0, 0, NVCVStatus, cvcudaAdjustSaturationCreate, (NVCVOperatorHandle * handle)) +{ + return nvcv::ProtectCall( + [handle] + { + if (handle == nullptr) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Pointer to NVCVOperator handle must not be NULL"); + } + + // Ownership of the operator is transferred to the C handle and released by + // cvcudaOperatorDestroy; this matches every other operator's Create entry point. + *handle = reinterpret_cast(new priv::AdjustSaturation()); // NOSONAR + }); +} + +CVCUDA_DEFINE_API(0, 0, NVCVStatus, cvcudaAdjustSaturationSubmit, + (NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle in, NVCVTensorHandle out, + double saturation)) +{ + CVCUDA_NVTX_RANGE("cvcudaAdjustSaturationSubmit"); + return nvcv::ProtectCall( + [handle, stream, in, out, saturation] + { + nvcv::TensorWrapHandle input(in); + nvcv::TensorWrapHandle output(out); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), saturation); + }); +} + +CVCUDA_DEFINE_API(0, 0, NVCVStatus, cvcudaAdjustSaturationVarShapeSubmit, + (NVCVOperatorHandle handle, cudaStream_t stream, NVCVImageBatchHandle in, NVCVImageBatchHandle out, + double saturation)) +{ + CVCUDA_NVTX_RANGE("cvcudaAdjustSaturationVarShapeSubmit"); + return nvcv::ProtectCall( + [handle, stream, in, out, saturation] + { + nvcv::ImageBatchVarShapeWrapHandle input(in); + nvcv::ImageBatchVarShapeWrapHandle output(out); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), saturation); + }); +} diff --git a/src/cvcuda/OpAdjustSharpness.cpp b/src/cvcuda/OpAdjustSharpness.cpp new file mode 100644 index 000000000..a58dcf4e3 --- /dev/null +++ b/src/cvcuda/OpAdjustSharpness.cpp @@ -0,0 +1,74 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "priv/OpAdjustSharpness.hpp" + +#include "priv/Nvtx.hpp" +#include "priv/SymbolVersioning.hpp" + +#include +#include +#include + +namespace priv = cvcuda::priv; + +CVCUDA_DEFINE_API(0, 0, NVCVStatus, cvcudaAdjustSharpnessCreate, (NVCVOperatorHandle * handle)) +{ + return nvcv::ProtectCall( + [handle] + { + if (handle == nullptr) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Pointer to NVCVOperator handle must not be NULL"); + } + + // Ownership of the operator is transferred to the C handle and released by + // cvcudaOperatorDestroy; this matches every other operator's Create entry point. + *handle = reinterpret_cast(new priv::AdjustSharpness()); // NOSONAR + }); +} + +CVCUDA_DEFINE_API(0, 0, NVCVStatus, cvcudaAdjustSharpnessSubmit, + (NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle in, NVCVTensorHandle out, + float sharpnessFactor)) +{ + CVCUDA_NVTX_RANGE("cvcudaAdjustSharpnessSubmit"); + return nvcv::ProtectCall( + [handle, stream, in, out, sharpnessFactor] + { + nvcv::TensorWrapHandle input(in); + nvcv::TensorWrapHandle output(out); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), + sharpnessFactor); + }); +} + +CVCUDA_DEFINE_API(0, 0, NVCVStatus, cvcudaAdjustSharpnessVarShapeSubmit, + (NVCVOperatorHandle handle, cudaStream_t stream, NVCVImageBatchHandle in, NVCVImageBatchHandle out, + float sharpnessFactor)) +{ + CVCUDA_NVTX_RANGE("cvcudaAdjustSharpnessVarShapeSubmit"); + return nvcv::ProtectCall( + [handle, stream, in, out, sharpnessFactor] + { + nvcv::ImageBatchVarShapeWrapHandle input(in); + nvcv::ImageBatchVarShapeWrapHandle output(out); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), + sharpnessFactor); + }); +} diff --git a/src/cvcuda/OpAdvCvtColor.cpp b/src/cvcuda/OpAdvCvtColor.cpp index 161f0d739..e907930a9 100644 --- a/src/cvcuda/OpAdvCvtColor.cpp +++ b/src/cvcuda/OpAdvCvtColor.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "priv/OpAdvCvtColor.hpp" +#include "priv/Nvtx.hpp" #include "priv/SymbolVersioning.hpp" #include @@ -28,7 +29,7 @@ namespace priv = cvcuda::priv; CVCUDA_DEFINE_API(0, 4, NVCVStatus, cvcudaAdvCvtColorCreate, (NVCVOperatorHandle * handle)) { return nvcv::ProtectCall( - [&] + [&handle] { if (handle == nullptr) { @@ -36,7 +37,7 @@ CVCUDA_DEFINE_API(0, 4, NVCVStatus, cvcudaAdvCvtColorCreate, (NVCVOperatorHandle "Pointer to NVCVOperator handle must not be NULL"); } - *handle = reinterpret_cast(new priv::AdvCvtColor()); + *handle = priv::CreateOperatorHandle(); }); } @@ -44,10 +45,13 @@ CVCUDA_DEFINE_API(0, 4, NVCVStatus, cvcudaAdvCvtColorSubmit, (NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle in, NVCVTensorHandle out, NVCVColorConversionCode code, NVCVColorSpec spec)) { + CVCUDA_NVTX_RANGE("cvcudaAdvCvtColorSubmit"); return nvcv::ProtectCall( - [&] + [&in, &out, &handle, &stream, &code, &spec] { - nvcv::TensorWrapHandle input(in), output(out); - priv::ToDynamicRef(handle)(stream, input, output, code, spec); + nvcv::TensorWrapHandle input(in); + nvcv::TensorWrapHandle output(out); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), code, + nvcv::ColorSpec{spec}); }); } diff --git a/src/cvcuda/OpAutoContrast.cpp b/src/cvcuda/OpAutoContrast.cpp new file mode 100644 index 000000000..ebef16f71 --- /dev/null +++ b/src/cvcuda/OpAutoContrast.cpp @@ -0,0 +1,70 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "priv/OpAutoContrast.hpp" + +#include "priv/Nvtx.hpp" +#include "priv/SymbolVersioning.hpp" + +#include +#include +#include + +namespace priv = cvcuda::priv; + +CVCUDA_DEFINE_API(0, 0, NVCVStatus, cvcudaAutoContrastCreate, (NVCVOperatorHandle * handle)) +{ + return nvcv::ProtectCall( + [handle] + { + if (handle == nullptr) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Pointer to NVCVOperator handle must not be NULL"); + } + + // Ownership of the operator is transferred to the C handle and released by + // cvcudaOperatorDestroy; this matches every other operator's Create entry point. + *handle = reinterpret_cast(new priv::AutoContrast()); // NOSONAR + }); +} + +CVCUDA_DEFINE_API(0, 0, NVCVStatus, cvcudaAutoContrastSubmit, + (NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle in, NVCVTensorHandle out)) +{ + CVCUDA_NVTX_RANGE("cvcudaAutoContrastSubmit"); + return nvcv::ProtectCall( + [handle, stream, in, out] + { + nvcv::TensorWrapHandle input(in); + nvcv::TensorWrapHandle output(out); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource()); + }); +} + +CVCUDA_DEFINE_API(0, 0, NVCVStatus, cvcudaAutoContrastVarShapeSubmit, + (NVCVOperatorHandle handle, cudaStream_t stream, NVCVImageBatchHandle in, NVCVImageBatchHandle out)) +{ + CVCUDA_NVTX_RANGE("cvcudaAutoContrastVarShapeSubmit"); + return nvcv::ProtectCall( + [handle, stream, in, out] + { + nvcv::ImageBatchVarShapeWrapHandle input(in); + nvcv::ImageBatchVarShapeWrapHandle output(out); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource()); + }); +} diff --git a/src/cvcuda/OpAverageBlur.cpp b/src/cvcuda/OpAverageBlur.cpp index e4ec7fbb7..7e321ee15 100644 --- a/src/cvcuda/OpAverageBlur.cpp +++ b/src/cvcuda/OpAverageBlur.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "priv/OpAverageBlur.hpp" +#include "priv/Nvtx.hpp" #include "priv/SymbolVersioning.hpp" #include @@ -31,7 +32,7 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaAverageBlurCreate, int32_t maxVarShapeBatchSize)) { return nvcv::ProtectCall( - [&] + [&handle, &maxKernelWidth, &maxKernelHeight, &maxVarShapeBatchSize] { if (handle == nullptr) { @@ -39,8 +40,8 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaAverageBlurCreate, "Pointer to NVCVOperator handle must not be NULL"); } - *handle = reinterpret_cast( - new priv::AverageBlur(nvcv::Size2D{maxKernelWidth, maxKernelHeight}, maxVarShapeBatchSize)); + *handle = priv::CreateOperatorHandle(nvcv::Size2D{maxKernelWidth, maxKernelHeight}, + maxVarShapeBatchSize); }); } @@ -49,11 +50,13 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaAverageBlurSubmit, int32_t kernelWidth, int32_t kernelHeight, int32_t kernelAnchorX, int32_t kernelAnchorY, NVCVBorderType borderMode)) { + CVCUDA_NVTX_RANGE("cvcudaAverageBlurSubmit"); return nvcv::ProtectCall( - [&] + [&out, &in, &handle, &stream, &kernelWidth, &kernelHeight, &kernelAnchorX, &kernelAnchorY, &borderMode] { - nvcv::TensorWrapHandle output(out), input(in); - priv::ToDynamicRef(handle)(stream, input, output, + nvcv::TensorWrapHandle output(out); + nvcv::TensorWrapHandle input(in); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), nvcv::Size2D{kernelWidth, kernelHeight}, int2{kernelAnchorX, kernelAnchorY}, borderMode); }); @@ -63,12 +66,16 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaAverageBlurVarShapeSubmit, (NVCVOperatorHandle handle, cudaStream_t stream, NVCVImageBatchHandle in, NVCVImageBatchHandle out, NVCVTensorHandle kernelSize, NVCVTensorHandle kernelAnchor, NVCVBorderType borderMode)) { + CVCUDA_NVTX_RANGE("cvcudaAverageBlurVarShapeSubmit"); return nvcv::ProtectCall( - [&] + [&in, &out, &kernelSize, &kernelAnchor, &handle, &stream, &borderMode] { - nvcv::ImageBatchVarShapeWrapHandle inWrap(in), outWrap(out); - nvcv::TensorWrapHandle kernelSizeWrap(kernelSize), kernelAnchorWrap(kernelAnchor); - priv::ToDynamicRef(handle)(stream, inWrap, outWrap, kernelSizeWrap, kernelAnchorWrap, + nvcv::ImageBatchVarShapeWrapHandle inWrap(in); + nvcv::ImageBatchVarShapeWrapHandle outWrap(out); + nvcv::TensorWrapHandle kernelSizeWrap(kernelSize); + nvcv::TensorWrapHandle kernelAnchorWrap(kernelAnchor); + priv::ToDynamicRef(handle)(stream, inWrap.resource(), outWrap.resource(), + kernelSizeWrap.resource(), kernelAnchorWrap.resource(), borderMode); }); } diff --git a/src/cvcuda/OpBilateralFilter.cpp b/src/cvcuda/OpBilateralFilter.cpp index 8759f7787..3575c097e 100644 --- a/src/cvcuda/OpBilateralFilter.cpp +++ b/src/cvcuda/OpBilateralFilter.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "priv/OpBilateralFilter.hpp" +#include "priv/Nvtx.hpp" #include "priv/SymbolVersioning.hpp" #include @@ -29,7 +30,7 @@ namespace priv = cvcuda::priv; CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaBilateralFilterCreate, (NVCVOperatorHandle * handle)) { return nvcv::ProtectCall( - [&] + [&handle] { if (handle == nullptr) { @@ -37,7 +38,7 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaBilateralFilterCreate, (NVCVOperatorHa "Pointer to NVCVOperator handle must not be NULL"); } - *handle = reinterpret_cast(new priv::BilateralFilter()); + *handle = priv::CreateOperatorHandle(); }); } @@ -45,12 +46,14 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaBilateralFilterSubmit, (NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle in, NVCVTensorHandle out, int diameter, float sigmaColor, float sigmaSpace, NVCVBorderType borderMode)) { + CVCUDA_NVTX_RANGE("cvcudaBilateralFilterSubmit"); return nvcv::ProtectCall( - [&] + [&in, &out, &handle, &stream, &diameter, &sigmaColor, &sigmaSpace, &borderMode] { - nvcv::TensorWrapHandle input(in), output(out); - priv::ToDynamicRef(handle)(stream, input, output, diameter, sigmaColor, sigmaSpace, - borderMode); + nvcv::TensorWrapHandle input(in); + nvcv::TensorWrapHandle output(out); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), diameter, + sigmaColor, sigmaSpace, borderMode); }); } @@ -59,12 +62,17 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaBilateralFilterVarShapeSubmit, NVCVTensorHandle diameter, NVCVTensorHandle sigmaColor, NVCVTensorHandle sigmaSpace, NVCVBorderType borderMode)) { + CVCUDA_NVTX_RANGE("cvcudaBilateralFilterVarShapeSubmit"); return nvcv::ProtectCall( - [&] + [&in, &out, &diameter, &sigmaColor, &sigmaSpace, &handle, &stream, &borderMode] { - nvcv::ImageBatchVarShapeWrapHandle input(in), output(out); - nvcv::TensorWrapHandle diameterData(diameter), sigmaColorData(sigmaColor), sigmaSpaceData(sigmaSpace); - priv::ToDynamicRef(handle)(stream, input, output, diameterData, sigmaColorData, - sigmaSpaceData, borderMode); + nvcv::ImageBatchVarShapeWrapHandle input(in); + nvcv::ImageBatchVarShapeWrapHandle output(out); + nvcv::TensorWrapHandle diameterData(diameter); + nvcv::TensorWrapHandle sigmaColorData(sigmaColor); + nvcv::TensorWrapHandle sigmaSpaceData(sigmaSpace); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), + diameterData.resource(), sigmaColorData.resource(), + sigmaSpaceData.resource(), borderMode); }); } diff --git a/src/cvcuda/OpBndBox.cpp b/src/cvcuda/OpBndBox.cpp index 8783b2f27..f9c6d30c4 100644 --- a/src/cvcuda/OpBndBox.cpp +++ b/src/cvcuda/OpBndBox.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "priv/OpBndBox.hpp" +#include "priv/Nvtx.hpp" #include "priv/SymbolVersioning.hpp" #include @@ -28,7 +29,7 @@ namespace priv = cvcuda::priv; CVCUDA_DEFINE_API(0, 3, NVCVStatus, cvcudaBndBoxCreate, (NVCVOperatorHandle * handle)) { return nvcv::ProtectCall( - [&] + [&handle] { if (handle == nullptr) { @@ -36,7 +37,7 @@ CVCUDA_DEFINE_API(0, 3, NVCVStatus, cvcudaBndBoxCreate, (NVCVOperatorHandle * ha "Pointer to NVCVOperator handle must not be NULL"); } - *handle = reinterpret_cast(new priv::BndBox()); + *handle = priv::CreateOperatorHandle(); }); } @@ -44,10 +45,12 @@ CVCUDA_DEFINE_API(0, 3, NVCVStatus, cvcudaBndBoxSubmit, (NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle in, NVCVTensorHandle out, const NVCVBndBoxesI bboxes)) { + CVCUDA_NVTX_RANGE("cvcudaBndBoxSubmit"); return nvcv::ProtectCall( - [&] + [&in, &out, &handle, &stream, &bboxes] { - nvcv::TensorWrapHandle input(in), output(out); - priv::ToDynamicRef(handle)(stream, input, output, bboxes); + nvcv::TensorWrapHandle input(in); + nvcv::TensorWrapHandle output(out); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), bboxes); }); } diff --git a/src/cvcuda/OpBoxBlur.cpp b/src/cvcuda/OpBoxBlur.cpp index ed0e0346b..7e1e4bd11 100644 --- a/src/cvcuda/OpBoxBlur.cpp +++ b/src/cvcuda/OpBoxBlur.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "priv/OpBoxBlur.hpp" +#include "priv/Nvtx.hpp" #include "priv/SymbolVersioning.hpp" #include @@ -28,7 +29,7 @@ namespace priv = cvcuda::priv; CVCUDA_DEFINE_API(0, 3, NVCVStatus, cvcudaBoxBlurCreate, (NVCVOperatorHandle * handle)) { return nvcv::ProtectCall( - [&] + [&handle] { if (handle == nullptr) { @@ -36,7 +37,7 @@ CVCUDA_DEFINE_API(0, 3, NVCVStatus, cvcudaBoxBlurCreate, (NVCVOperatorHandle * h "Pointer to NVCVOperator handle must not be NULL"); } - *handle = reinterpret_cast(new priv::BoxBlur()); + *handle = priv::CreateOperatorHandle(); }); } @@ -44,10 +45,12 @@ CVCUDA_DEFINE_API(0, 3, NVCVStatus, cvcudaBoxBlurSubmit, (NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle in, NVCVTensorHandle out, const NVCVBlurBoxesI bboxes)) { + CVCUDA_NVTX_RANGE("cvcudaBoxBlurSubmit"); return nvcv::ProtectCall( - [&] + [&in, &out, &handle, &stream, &bboxes] { - nvcv::TensorWrapHandle input(in), output(out); - priv::ToDynamicRef(handle)(stream, input, output, bboxes); + nvcv::TensorWrapHandle input(in); + nvcv::TensorWrapHandle output(out); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), bboxes); }); } diff --git a/src/cvcuda/OpBrightnessContrast.cpp b/src/cvcuda/OpBrightnessContrast.cpp index fcfbcdf4b..c4f0f7106 100644 --- a/src/cvcuda/OpBrightnessContrast.cpp +++ b/src/cvcuda/OpBrightnessContrast.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "priv/OpBrightnessContrast.hpp" +#include "priv/Nvtx.hpp" #include "priv/SymbolVersioning.hpp" #include @@ -26,10 +27,37 @@ namespace priv = cvcuda::priv; +template +void SubmitBrightnessContrast(NVCVOperatorHandle handle, cudaStream_t stream, InHandle in, OutHandle out, + NVCVTensorHandle brightness, NVCVTensorHandle contrast, NVCVTensorHandle brightnessShift, + NVCVTensorHandle contrastCenter) +{ + InWrap _in(in); + OutWrap _out(out); + nvcv::TensorWrapHandle _brightness(brightness); + nvcv::TensorWrapHandle _contrast(contrast); + nvcv::TensorWrapHandle _brightnessShift(brightnessShift); + nvcv::TensorWrapHandle _contrastCenter(contrastCenter); + priv::ToDynamicRef(handle)(stream, _in.resource(), _out.resource(), + _brightness.resource(), _contrast.resource(), + _brightnessShift.resource(), _contrastCenter.resource()); +} + +template +void SubmitBrightnessContrastScalar(NVCVOperatorHandle handle, cudaStream_t stream, InHandle in, OutHandle out, + double brightness, double contrast, double brightnessShift, double contrastCenter, + bool clamp) +{ + InWrap _in(in); + OutWrap _out(out); + priv::ToDynamicRef(handle)(stream, _in.resource(), _out.resource(), brightness, contrast, + brightnessShift, contrastCenter, clamp); +} + CVCUDA_DEFINE_API(0, 3, NVCVStatus, cvcudaBrightnessContrastCreate, (NVCVOperatorHandle * handle)) { return nvcv::ProtectCall( - [&] + [&handle] { if (handle == nullptr) { @@ -37,7 +65,7 @@ CVCUDA_DEFINE_API(0, 3, NVCVStatus, cvcudaBrightnessContrastCreate, (NVCVOperato "Pointer to NVCVOperator handle must not be NULL"); } - *handle = reinterpret_cast(new priv::BrightnessContrast()); + *handle = priv::CreateOperatorHandle(); }); } @@ -46,14 +74,12 @@ CVCUDA_DEFINE_API(0, 3, NVCVStatus, cvcudaBrightnessContrastSubmit, NVCVTensorHandle brightness, NVCVTensorHandle contrast, NVCVTensorHandle brightnessShift, NVCVTensorHandle contrastCenter)) { + CVCUDA_NVTX_RANGE("cvcudaBrightnessContrastSubmit"); return nvcv::ProtectCall( - [&] + [&in, &out, &brightness, &contrast, &brightnessShift, &contrastCenter, &handle, &stream] { - nvcv::TensorWrapHandle _in(in), _out(out); - nvcv::TensorWrapHandle _brightness(brightness), _contrast(contrast); - nvcv::TensorWrapHandle _brightnessShift(brightnessShift), _contrastCenter(contrastCenter); - priv::ToDynamicRef(handle)(stream, _in, _out, _brightness, _contrast, - _brightnessShift, _contrastCenter); + SubmitBrightnessContrast( + handle, stream, in, out, brightness, contrast, brightnessShift, contrastCenter); }); } @@ -62,13 +88,37 @@ CVCUDA_DEFINE_API(0, 3, NVCVStatus, cvcudaBrightnessContrastVarShapeSubmit, NVCVTensorHandle brightness, NVCVTensorHandle contrast, NVCVTensorHandle brightnessShift, NVCVTensorHandle contrastCenter)) { + CVCUDA_NVTX_RANGE("cvcudaBrightnessContrastVarShapeSubmit"); + return nvcv::ProtectCall( + [&in, &out, &brightness, &contrast, &brightnessShift, &contrastCenter, &handle, &stream] + { + SubmitBrightnessContrast( + handle, stream, in, out, brightness, contrast, brightnessShift, contrastCenter); + }); +} + +CVCUDA_DEFINE_API(0, 17, NVCVStatus, cvcudaBrightnessContrastScalarSubmit, + (NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle in, NVCVTensorHandle out, + double brightness, double contrast, double brightnessShift, double contrastCenter, bool clamp)) +{ + CVCUDA_NVTX_RANGE("cvcudaBrightnessContrastScalarSubmit"); + return nvcv::ProtectCall( + [&handle, &stream, &in, &out, &brightness, &contrast, &brightnessShift, &contrastCenter, &clamp] + { + SubmitBrightnessContrastScalar( + handle, stream, in, out, brightness, contrast, brightnessShift, contrastCenter, clamp); + }); +} + +CVCUDA_DEFINE_API(0, 17, NVCVStatus, cvcudaBrightnessContrastVarShapeScalarSubmit, + (NVCVOperatorHandle handle, cudaStream_t stream, NVCVImageBatchHandle in, NVCVImageBatchHandle out, + double brightness, double contrast, double brightnessShift, double contrastCenter, bool clamp)) +{ + CVCUDA_NVTX_RANGE("cvcudaBrightnessContrastVarShapeScalarSubmit"); return nvcv::ProtectCall( - [&] + [&handle, &stream, &in, &out, &brightness, &contrast, &brightnessShift, &contrastCenter, &clamp] { - nvcv::ImageBatchVarShapeWrapHandle _in(in), _out(out); - nvcv::TensorWrapHandle _brightness(brightness), _contrast(contrast); - nvcv::TensorWrapHandle _brightnessShift(brightnessShift), _contrastCenter(contrastCenter); - priv::ToDynamicRef(handle)(stream, _in, _out, _brightness, _contrast, - _brightnessShift, _contrastCenter); + SubmitBrightnessContrastScalar( + handle, stream, in, out, brightness, contrast, brightnessShift, contrastCenter, clamp); }); } diff --git a/src/cvcuda/OpCLAHE.cpp b/src/cvcuda/OpCLAHE.cpp new file mode 100644 index 000000000..9dafafbc5 --- /dev/null +++ b/src/cvcuda/OpCLAHE.cpp @@ -0,0 +1,73 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "priv/OpCLAHE.hpp" + +#include "priv/Nvtx.hpp" +#include "priv/SymbolVersioning.hpp" + +#include +#include +#include +#include +#include + +namespace priv = cvcuda::priv; + +CVCUDA_DEFINE_API(0, 8, NVCVStatus, cvcudaCLAHECreate, + (NVCVOperatorHandle * handle, int32_t maxBatchSize, int32_t tilesX, int32_t tilesY)) +{ + return nvcv::ProtectCall( + [&handle, &maxBatchSize, &tilesX, &tilesY] + { + if (handle == nullptr) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Pointer to NVCVOperator handle must not be NULL"); + } + + *handle = priv::CreateOperatorHandle(maxBatchSize, tilesX, tilesY); + }); +} + +CVCUDA_DEFINE_API(0, 8, NVCVStatus, cvcudaCLAHESubmit, + (NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle in, NVCVTensorHandle out, + float clipLimit)) +{ + CVCUDA_NVTX_RANGE("cvcudaCLAHESubmit"); + return nvcv::ProtectCall( + [&handle, &stream, &in, &out, &clipLimit] + { + nvcv::TensorWrapHandle input(in); + nvcv::TensorWrapHandle output(out); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), clipLimit); + }); +} + +CVCUDA_DEFINE_API(0, 8, NVCVStatus, cvcudaCLAHEVarShapeSubmit, + (NVCVOperatorHandle handle, cudaStream_t stream, NVCVImageBatchHandle in, NVCVImageBatchHandle out, + float clipLimit)) +{ + CVCUDA_NVTX_RANGE("cvcudaCLAHEVarShapeSubmit"); + return nvcv::ProtectCall( + [&handle, &stream, &in, &out, &clipLimit] + { + nvcv::ImageBatchVarShapeWrapHandle input(in); + nvcv::ImageBatchVarShapeWrapHandle output(out); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), clipLimit); + }); +} diff --git a/src/cvcuda/OpCenterCrop.cpp b/src/cvcuda/OpCenterCrop.cpp index ae5970aa8..03256d2fe 100644 --- a/src/cvcuda/OpCenterCrop.cpp +++ b/src/cvcuda/OpCenterCrop.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "priv/OpCenterCrop.hpp" +#include "priv/Nvtx.hpp" #include "priv/SymbolVersioning.hpp" #include @@ -28,7 +29,7 @@ namespace priv = cvcuda::priv; CVCUDA_DEFINE_API(0, 0, NVCVStatus, cvcudaCenterCropCreate, (NVCVOperatorHandle * handle)) { return nvcv::ProtectCall( - [&] + [&handle] { if (handle == nullptr) { @@ -36,7 +37,7 @@ CVCUDA_DEFINE_API(0, 0, NVCVStatus, cvcudaCenterCropCreate, (NVCVOperatorHandle "Pointer to NVCVOperator handle must not be NULL"); } - *handle = reinterpret_cast(new priv::CenterCrop()); + *handle = priv::CreateOperatorHandle(); }); } @@ -44,10 +45,13 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaCenterCropSubmit, (NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle in, NVCVTensorHandle out, int32_t cropWidth, int32_t cropHeight)) { + CVCUDA_NVTX_RANGE("cvcudaCenterCropSubmit"); return nvcv::ProtectCall( - [&] + [&in, &out, &handle, &stream, &cropWidth, &cropHeight] { - nvcv::TensorWrapHandle input(in), output(out); - priv::ToDynamicRef(handle)(stream, input, output, {cropWidth, cropHeight}); + nvcv::TensorWrapHandle input(in); + nvcv::TensorWrapHandle output(out); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), + {cropWidth, cropHeight}); }); } diff --git a/src/cvcuda/OpChannelReorder.cpp b/src/cvcuda/OpChannelReorder.cpp index c1076b405..286c26fab 100644 --- a/src/cvcuda/OpChannelReorder.cpp +++ b/src/cvcuda/OpChannelReorder.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "priv/OpChannelReorder.hpp" +#include "priv/Nvtx.hpp" #include "priv/SymbolVersioning.hpp" #include @@ -29,7 +30,7 @@ namespace priv = cvcuda::priv; CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaChannelReorderCreate, (NVCVOperatorHandle * handle)) { return nvcv::ProtectCall( - [&] + [&handle] { if (handle == nullptr) { @@ -37,7 +38,26 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaChannelReorderCreate, (NVCVOperatorHan "Pointer to NVCVOperator handle must not be NULL"); } - *handle = reinterpret_cast(new priv::ChannelReorder()); + *handle = priv::CreateOperatorHandle(); + }); +} + +CVCUDA_DEFINE_API(0, 17, NVCVStatus, cvcudaChannelReorderSubmit, + (NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle in, NVCVTensorHandle out, + const int32_t *order, int32_t orderLength)) +{ + CVCUDA_NVTX_RANGE("cvcudaChannelReorderSubmit"); + return nvcv::ProtectCall( + [handle, stream, in, out, order, orderLength] + { + if (in == out) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input and output tensors must not alias"); + } + nvcv::TensorWrapHandle input(in); + nvcv::TensorWrapHandle output(out); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), order, + orderLength); }); } @@ -45,12 +65,15 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaChannelReorderVarShapeSubmit, (NVCVOperatorHandle handle, cudaStream_t stream, NVCVImageBatchHandle in, NVCVImageBatchHandle out, NVCVTensorHandle orders_in)) { + CVCUDA_NVTX_RANGE("cvcudaChannelReorderVarShapeSubmit"); return nvcv::ProtectCall( - [&] + [&out, &in, &orders_in, &handle, &stream] { - nvcv::ImageBatchVarShapeWrapHandle output(out), input(in); + nvcv::ImageBatchVarShapeWrapHandle output(out); + nvcv::ImageBatchVarShapeWrapHandle input(in); nvcv::TensorWrapHandle orders(orders_in); - priv::ToDynamicRef(handle)(stream, input, output, orders); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), + orders.resource()); }); } diff --git a/src/cvcuda/OpColorTwist.cpp b/src/cvcuda/OpColorTwist.cpp index 7e9e7d45e..177c19d53 100644 --- a/src/cvcuda/OpColorTwist.cpp +++ b/src/cvcuda/OpColorTwist.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "priv/OpColorTwist.hpp" +#include "priv/Nvtx.hpp" #include "priv/SymbolVersioning.hpp" #include @@ -29,7 +30,7 @@ namespace priv = cvcuda::priv; CVCUDA_DEFINE_API(0, 3, NVCVStatus, cvcudaColorTwistCreate, (NVCVOperatorHandle * handle)) { return nvcv::ProtectCall( - [&] + [&handle] { if (handle == nullptr) { @@ -37,7 +38,7 @@ CVCUDA_DEFINE_API(0, 3, NVCVStatus, cvcudaColorTwistCreate, (NVCVOperatorHandle "Pointer to NVCVOperator handle must not be NULL"); } - *handle = reinterpret_cast(new priv::ColorTwist()); + *handle = priv::CreateOperatorHandle(); }); } @@ -45,11 +46,14 @@ CVCUDA_DEFINE_API(0, 3, NVCVStatus, cvcudaColorTwistSubmit, (NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle in, NVCVTensorHandle out, NVCVTensorHandle twist)) { + CVCUDA_NVTX_RANGE("cvcudaColorTwistSubmit"); return nvcv::ProtectCall( - [&] + [&in, &out, &twist, &handle, &stream] { - nvcv::TensorWrapHandle _in(in), _out(out), _twist(twist); - priv::ToDynamicRef(handle)(stream, _in, _out, _twist); + nvcv::TensorWrapHandle _in(in); + nvcv::TensorWrapHandle _out(out); + nvcv::TensorWrapHandle _twist(twist); + priv::ToDynamicRef(handle)(stream, _in.resource(), _out.resource(), _twist.resource()); }); } @@ -57,11 +61,13 @@ CVCUDA_DEFINE_API(0, 3, NVCVStatus, cvcudaColorTwistVarShapeSubmit, (NVCVOperatorHandle handle, cudaStream_t stream, NVCVImageBatchHandle in, NVCVImageBatchHandle out, NVCVTensorHandle twist)) { + CVCUDA_NVTX_RANGE("cvcudaColorTwistVarShapeSubmit"); return nvcv::ProtectCall( - [&] + [&in, &out, &twist, &handle, &stream] { - nvcv::ImageBatchVarShapeWrapHandle _in(in), _out(out); + nvcv::ImageBatchVarShapeWrapHandle _in(in); + nvcv::ImageBatchVarShapeWrapHandle _out(out); nvcv::TensorWrapHandle _twist(twist); - priv::ToDynamicRef(handle)(stream, _in, _out, _twist); + priv::ToDynamicRef(handle)(stream, _in.resource(), _out.resource(), _twist.resource()); }); } diff --git a/src/cvcuda/OpComposite.cpp b/src/cvcuda/OpComposite.cpp index 2434a84ae..867b00f71 100644 --- a/src/cvcuda/OpComposite.cpp +++ b/src/cvcuda/OpComposite.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "priv/OpComposite.hpp" +#include "priv/Nvtx.hpp" #include "priv/SymbolVersioning.hpp" #include @@ -29,7 +30,7 @@ namespace priv = cvcuda::priv; CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaCompositeCreate, (NVCVOperatorHandle * handle)) { return nvcv::ProtectCall( - [&] + [&handle] { if (handle == nullptr) { @@ -37,7 +38,7 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaCompositeCreate, (NVCVOperatorHandle * "Pointer to NVCVOperator handle must not be NULL"); } - *handle = reinterpret_cast(new priv::Composite()); + *handle = priv::CreateOperatorHandle(); }); } @@ -45,11 +46,16 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaCompositeSubmit, (NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle fg, NVCVTensorHandle bg, NVCVTensorHandle fgMask, NVCVTensorHandle out)) { + CVCUDA_NVTX_RANGE("cvcudaCompositeSubmit"); return nvcv::ProtectCall( - [&] + [&fg, &bg, &fgMask, &out, &handle, &stream] { - nvcv::TensorWrapHandle foreground(fg), background(bg), mask(fgMask), output(out); - priv::ToDynamicRef(handle)(stream, foreground, background, mask, output); + nvcv::TensorWrapHandle foreground(fg); + nvcv::TensorWrapHandle background(bg); + nvcv::TensorWrapHandle mask(fgMask); + nvcv::TensorWrapHandle output(out); + priv::ToDynamicRef(handle)(stream, foreground.resource(), background.resource(), + mask.resource(), output.resource()); }); } @@ -57,10 +63,15 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaCompositeVarShapeSubmit, (NVCVOperatorHandle handle, cudaStream_t stream, NVCVImageBatchHandle fg, NVCVImageBatchHandle bg, NVCVImageBatchHandle fgMask, NVCVImageBatchHandle out)) { + CVCUDA_NVTX_RANGE("cvcudaCompositeVarShapeSubmit"); return nvcv::ProtectCall( - [&] + [&fg, &bg, &fgMask, &out, &handle, &stream] { - nvcv::ImageBatchVarShapeWrapHandle foreground(fg), background(bg), mask(fgMask), output(out); - priv::ToDynamicRef(handle)(stream, foreground, background, mask, output); + nvcv::ImageBatchVarShapeWrapHandle foreground(fg); + nvcv::ImageBatchVarShapeWrapHandle background(bg); + nvcv::ImageBatchVarShapeWrapHandle mask(fgMask); + nvcv::ImageBatchVarShapeWrapHandle output(out); + priv::ToDynamicRef(handle)(stream, foreground.resource(), background.resource(), + mask.resource(), output.resource()); }); } diff --git a/src/cvcuda/OpConv2D.cpp b/src/cvcuda/OpConv2D.cpp index e40f9e3b0..de0a7548d 100644 --- a/src/cvcuda/OpConv2D.cpp +++ b/src/cvcuda/OpConv2D.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "priv/OpConv2D.hpp" +#include "priv/Nvtx.hpp" #include "priv/SymbolVersioning.hpp" #include @@ -30,7 +31,7 @@ namespace priv = cvcuda::priv; CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaConv2DCreate, (NVCVOperatorHandle * handle)) { return nvcv::ProtectCall( - [&] + [&handle] { if (handle == nullptr) { @@ -38,7 +39,7 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaConv2DCreate, (NVCVOperatorHandle * ha "Pointer to NVCVOperator handle must not be NULL"); } - *handle = reinterpret_cast(new priv::Conv2D()); + *handle = priv::CreateOperatorHandle(); }); } @@ -46,11 +47,15 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaConv2DVarShapeSubmit, (NVCVOperatorHandle handle, cudaStream_t stream, NVCVImageBatchHandle in, NVCVImageBatchHandle out, NVCVImageBatchHandle kernel, NVCVTensorHandle kernelAnchor, NVCVBorderType borderMode)) { + CVCUDA_NVTX_RANGE("cvcudaConv2DVarShapeSubmit"); return nvcv::ProtectCall( - [&] + [&in, &out, &kernel, &kernelAnchor, &handle, &stream, &borderMode] { - nvcv::ImageBatchVarShapeWrapHandle inWrap(in), outWrap(out), kernelWrap(kernel); + nvcv::ImageBatchVarShapeWrapHandle inWrap(in); + nvcv::ImageBatchVarShapeWrapHandle outWrap(out); + nvcv::ImageBatchVarShapeWrapHandle kernelWrap(kernel); nvcv::TensorWrapHandle kernelAnchorWrap(kernelAnchor); - priv::ToDynamicRef(handle)(stream, inWrap, outWrap, kernelWrap, kernelAnchorWrap, borderMode); + priv::ToDynamicRef(handle)(stream, inWrap.resource(), outWrap.resource(), + kernelWrap.resource(), kernelAnchorWrap.resource(), borderMode); }); } diff --git a/src/cvcuda/OpConvertTo.cpp b/src/cvcuda/OpConvertTo.cpp index 346acc941..854d2c7b4 100644 --- a/src/cvcuda/OpConvertTo.cpp +++ b/src/cvcuda/OpConvertTo.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "priv/OpConvertTo.hpp" +#include "priv/Nvtx.hpp" #include "priv/SymbolVersioning.hpp" #include @@ -28,7 +29,7 @@ namespace priv = cvcuda::priv; CVCUDA_DEFINE_API(0, 0, NVCVStatus, cvcudaConvertToCreate, (NVCVOperatorHandle * handle)) { return nvcv::ProtectCall( - [&] + [&handle] { if (handle == nullptr) { @@ -36,18 +37,21 @@ CVCUDA_DEFINE_API(0, 0, NVCVStatus, cvcudaConvertToCreate, (NVCVOperatorHandle * "Pointer to NVCVOperator handle must not be NULL"); } - *handle = reinterpret_cast(new priv::ConvertTo()); + *handle = priv::CreateOperatorHandle(); }); } -CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaConvertToSubmit, +CVCUDA_DEFINE_API(0, 17, NVCVStatus, cvcudaConvertToSubmit, (NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle in, NVCVTensorHandle out, - const double alpha, const double beta)) + const double alpha, const double beta, NVCVRoundMode roundMode)) { + CVCUDA_NVTX_RANGE("cvcudaConvertToSubmit"); return nvcv::ProtectCall( - [&] + [&in, &out, &handle, &stream, &alpha, &beta, &roundMode] { - nvcv::TensorWrapHandle input(in), output(out); - priv::ToDynamicRef(handle)(stream, input, output, alpha, beta); + nvcv::TensorWrapHandle input(in); + nvcv::TensorWrapHandle output(out); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), alpha, beta, + roundMode); }); } diff --git a/src/cvcuda/OpCopyMakeBorder.cpp b/src/cvcuda/OpCopyMakeBorder.cpp index 15bad1f52..137e79863 100644 --- a/src/cvcuda/OpCopyMakeBorder.cpp +++ b/src/cvcuda/OpCopyMakeBorder.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "priv/OpCopyMakeBorder.hpp" +#include "priv/Nvtx.hpp" #include "priv/SymbolVersioning.hpp" #include @@ -29,7 +30,7 @@ namespace priv = cvcuda::priv; CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaCopyMakeBorderCreate, (NVCVOperatorHandle * handle)) { return nvcv::ProtectCall( - [&] + [&handle] { if (handle == nullptr) { @@ -37,7 +38,7 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaCopyMakeBorderCreate, (NVCVOperatorHan "Pointer to NVCVOperator handle must not be NULL"); } - *handle = reinterpret_cast(new priv::CopyMakeBorder()); + *handle = priv::CreateOperatorHandle(); }); } @@ -45,11 +46,14 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaCopyMakeBorderSubmit, (NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle in, NVCVTensorHandle out, int32_t top, int32_t left, NVCVBorderType borderMode, const float4 borderValue)) { + CVCUDA_NVTX_RANGE("cvcudaCopyMakeBorderSubmit"); return nvcv::ProtectCall( - [&] + [&out, &in, &handle, &stream, &top, &left, &borderMode, &borderValue] { - nvcv::TensorWrapHandle output(out), input(in); - priv::ToDynamicRef(handle)(stream, input, output, top, left, borderMode, borderValue); + nvcv::TensorWrapHandle output(out); + nvcv::TensorWrapHandle input(in); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), top, left, + borderMode, borderValue); }); } @@ -57,12 +61,16 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaCopyMakeBorderVarShapeSubmit, (NVCVOperatorHandle handle, cudaStream_t stream, NVCVImageBatchHandle in, NVCVImageBatchHandle out, NVCVTensorHandle top, NVCVTensorHandle left, NVCVBorderType borderMode, const float4 borderValue)) { + CVCUDA_NVTX_RANGE("cvcudaCopyMakeBorderVarShapeSubmit"); return nvcv::ProtectCall( - [&] + [&out, &in, &top, &left, &handle, &stream, &borderMode, &borderValue] { - nvcv::ImageBatchWrapHandle output(out), input(in); - nvcv::TensorWrapHandle topVec(top), leftVec(left); - priv::ToDynamicRef(handle)(stream, input, output, topVec, leftVec, borderMode, + nvcv::ImageBatchWrapHandle output(out); + nvcv::ImageBatchWrapHandle input(in); + nvcv::TensorWrapHandle topVec(top); + nvcv::TensorWrapHandle leftVec(left); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), + topVec.resource(), leftVec.resource(), borderMode, borderValue); }); } @@ -71,12 +79,16 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaCopyMakeBorderVarShapeStackSubmit, (NVCVOperatorHandle handle, cudaStream_t stream, NVCVImageBatchHandle in, NVCVTensorHandle out, NVCVTensorHandle top, NVCVTensorHandle left, NVCVBorderType borderMode, const float4 borderValue)) { + CVCUDA_NVTX_RANGE("cvcudaCopyMakeBorderVarShapeStackSubmit"); return nvcv::ProtectCall( - [&] + [&in, &out, &top, &left, &handle, &stream, &borderMode, &borderValue] { nvcv::ImageBatchWrapHandle input(in); - nvcv::TensorWrapHandle output(out), topVec(top), leftVec(left); - priv::ToDynamicRef(handle)(stream, input, output, topVec, leftVec, borderMode, + nvcv::TensorWrapHandle output(out); + nvcv::TensorWrapHandle topVec(top); + nvcv::TensorWrapHandle leftVec(left); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), + topVec.resource(), leftVec.resource(), borderMode, borderValue); }); } diff --git a/src/cvcuda/OpCropFlipNormalizeReformat.cpp b/src/cvcuda/OpCropFlipNormalizeReformat.cpp index 5d7cbc5d3..16c46a0fe 100644 --- a/src/cvcuda/OpCropFlipNormalizeReformat.cpp +++ b/src/cvcuda/OpCropFlipNormalizeReformat.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "priv/OpCropFlipNormalizeReformat.hpp" +#include "priv/Nvtx.hpp" #include "priv/SymbolVersioning.hpp" #include @@ -29,7 +30,7 @@ namespace priv = cvcuda::priv; CVCUDA_DEFINE_API(0, 3, NVCVStatus, cvcudaCropFlipNormalizeReformatCreate, (NVCVOperatorHandle * handle)) { return nvcv::ProtectCall( - [&] + [&handle] { if (handle == nullptr) { @@ -37,7 +38,7 @@ CVCUDA_DEFINE_API(0, 3, NVCVStatus, cvcudaCropFlipNormalizeReformatCreate, (NVCV "Pointer to NVCVOperator handle must not be NULL"); } - *handle = reinterpret_cast(new priv::CropFlipNormalizeReformat()); + *handle = priv::CreateOperatorHandle(); }); } @@ -47,14 +48,20 @@ CVCUDA_DEFINE_API(0, 3, NVCVStatus, cvcudaCropFlipNormalizeReformatSubmit, NVCVTensorHandle base, NVCVTensorHandle scale, float global_scale, float shift, float epsilon, uint32_t flags)) { + CVCUDA_NVTX_RANGE("cvcudaCropFlipNormalizeReformatSubmit"); return nvcv::ProtectCall( - [&] + [&base, &scale, &flipCode, &cropRect, &out, &in, &handle, &stream, &borderMode, &borderValue, &global_scale, + &shift, &epsilon, &flags] { - nvcv::TensorWrapHandle baseWrap(base), scaleWrap(scale), flipCodeWrap(flipCode), cropRectWrap(cropRect); - nvcv::TensorWrapHandle outWrap(out); + nvcv::TensorWrapHandle baseWrap(base); + nvcv::TensorWrapHandle scaleWrap(scale); + nvcv::TensorWrapHandle flipCodeWrap(flipCode); + nvcv::TensorWrapHandle cropRectWrap(cropRect); + nvcv::TensorWrapHandle outWrap(out); nvcv::ImageBatchVarShapeWrapHandle inWrap(in); - priv::ToDynamicRef(handle)(stream, inWrap, outWrap, cropRectWrap, - borderMode, borderValue, flipCodeWrap, baseWrap, - scaleWrap, global_scale, shift, epsilon, flags); + priv::ToDynamicRef(handle)( + stream, inWrap.resource(), outWrap.resource(), cropRectWrap.resource(), borderMode, borderValue, + flipCodeWrap.resource(), baseWrap.resource(), scaleWrap.resource(), global_scale, shift, epsilon, + flags); }); } diff --git a/src/cvcuda/OpCustomCrop.cpp b/src/cvcuda/OpCustomCrop.cpp index 1ea2ea428..3e7dde7e6 100644 --- a/src/cvcuda/OpCustomCrop.cpp +++ b/src/cvcuda/OpCustomCrop.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "priv/OpCustomCrop.hpp" +#include "priv/Nvtx.hpp" #include "priv/SymbolVersioning.hpp" #include @@ -28,7 +29,7 @@ namespace priv = cvcuda::priv; CVCUDA_DEFINE_API(0, 0, NVCVStatus, cvcudaCustomCropCreate, (NVCVOperatorHandle * handle)) { return nvcv::ProtectCall( - [&] + [&handle] { if (handle == nullptr) { @@ -36,7 +37,7 @@ CVCUDA_DEFINE_API(0, 0, NVCVStatus, cvcudaCustomCropCreate, (NVCVOperatorHandle "Pointer to NVCVOperator handle must not be NULL"); } - *handle = reinterpret_cast(new priv::CustomCrop()); + *handle = priv::CreateOperatorHandle(); }); } @@ -44,10 +45,12 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaCustomCropSubmit, (NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle in, NVCVTensorHandle out, const NVCVRectI cropRect)) { + CVCUDA_NVTX_RANGE("cvcudaCustomCropSubmit"); return nvcv::ProtectCall( - [&] + [&in, &out, &handle, &stream, &cropRect] { - nvcv::TensorWrapHandle input(in), output(out); - priv::ToDynamicRef(handle)(stream, input, output, cropRect); + nvcv::TensorWrapHandle input(in); + nvcv::TensorWrapHandle output(out); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), cropRect); }); } diff --git a/src/cvcuda/OpCvtColor.cpp b/src/cvcuda/OpCvtColor.cpp index ce57d4583..7464ba571 100644 --- a/src/cvcuda/OpCvtColor.cpp +++ b/src/cvcuda/OpCvtColor.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "priv/OpCvtColor.hpp" +#include "priv/Nvtx.hpp" #include "priv/SymbolVersioning.hpp" #include @@ -29,7 +30,7 @@ namespace priv = cvcuda::priv; CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaCvtColorCreate, (NVCVOperatorHandle * handle)) { return nvcv::ProtectCall( - [&] + [&handle] { if (handle == nullptr) { @@ -37,7 +38,7 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaCvtColorCreate, (NVCVOperatorHandle * "Pointer to NVCVOperator handle must not be NULL"); } - *handle = reinterpret_cast(new priv::CvtColor()); + *handle = priv::CreateOperatorHandle(); }); } @@ -45,11 +46,13 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaCvtColorSubmit, (NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle in, NVCVTensorHandle out, NVCVColorConversionCode code)) { + CVCUDA_NVTX_RANGE("cvcudaCvtColorSubmit"); return nvcv::ProtectCall( - [&] + [&out, &in, &handle, &stream, &code] { - nvcv::TensorWrapHandle output(out), input(in); - priv::ToDynamicRef(handle)(stream, input, output, code); + nvcv::TensorWrapHandle output(out); + nvcv::TensorWrapHandle input(in); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), code); }); } @@ -57,10 +60,12 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaCvtColorVarShapeSubmit, (NVCVOperatorHandle handle, cudaStream_t stream, NVCVImageBatchHandle in, NVCVImageBatchHandle out, NVCVColorConversionCode code)) { + CVCUDA_NVTX_RANGE("cvcudaCvtColorVarShapeSubmit"); return nvcv::ProtectCall( - [&] + [&in, &out, &handle, &stream, &code] { - nvcv::ImageBatchVarShapeWrapHandle inWrap(in), outWrap(out); - priv::ToDynamicRef(handle)(stream, inWrap, outWrap, code); + nvcv::ImageBatchVarShapeWrapHandle inWrap(in); + nvcv::ImageBatchVarShapeWrapHandle outWrap(out); + priv::ToDynamicRef(handle)(stream, inWrap.resource(), outWrap.resource(), code); }); } diff --git a/src/cvcuda/OpErase.cpp b/src/cvcuda/OpErase.cpp index 51c11b0e3..a86b52007 100644 --- a/src/cvcuda/OpErase.cpp +++ b/src/cvcuda/OpErase.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "priv/OpErase.hpp" +#include "priv/Nvtx.hpp" #include "priv/SymbolVersioning.hpp" #include @@ -26,10 +27,26 @@ namespace priv = cvcuda::priv; +template +void SubmitErase(NVCVOperatorHandle handle, cudaStream_t stream, InHandle in, OutHandle out, NVCVTensorHandle anchor, + NVCVTensorHandle erasing, NVCVTensorHandle values, NVCVTensorHandle imgIdx, int8_t random, + uint32_t seed) +{ + InWrap input(in); + OutWrap output(out); + nvcv::TensorWrapHandle anchorwrap(anchor); + nvcv::TensorWrapHandle erasingwrap(erasing); + nvcv::TensorWrapHandle valueswrap(values); + nvcv::TensorWrapHandle imgIdxwrap(imgIdx); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), anchorwrap.resource(), + erasingwrap.resource(), valueswrap.resource(), imgIdxwrap.resource(), + random, seed); +} + CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaEraseCreate, (NVCVOperatorHandle * handle, int32_t max_num_erasing_area)) { return nvcv::ProtectCall( - [&] + [&handle, &max_num_erasing_area] { if (handle == nullptr) { @@ -37,7 +54,7 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaEraseCreate, (NVCVOperatorHandle * han "Pointer to NVCVOperator handle must not be NULL"); } - *handle = reinterpret_cast(new priv::Erase(max_num_erasing_area)); + *handle = priv::CreateOperatorHandle(max_num_erasing_area); }); } @@ -46,13 +63,12 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaEraseSubmit, NVCVTensorHandle anchor, NVCVTensorHandle erasing, NVCVTensorHandle values, NVCVTensorHandle imgIdx, int8_t random, uint32_t seed)) { + CVCUDA_NVTX_RANGE("cvcudaEraseSubmit"); return nvcv::ProtectCall( - [&] + [&in, &out, &anchor, &erasing, &values, &imgIdx, &handle, &stream, &random, &seed] { - nvcv::TensorWrapHandle input(in), output(out), anchorwrap(anchor), erasingwrap(erasing), valueswrap(values), - imgIdxwrap(imgIdx); - priv::ToDynamicRef(handle)(stream, input, output, anchorwrap, erasingwrap, valueswrap, - imgIdxwrap, random, seed); + SubmitErase(handle, stream, in, out, anchor, erasing, + values, imgIdx, random, seed); }); } @@ -61,12 +77,27 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaEraseVarShapeSubmit, NVCVTensorHandle anchor, NVCVTensorHandle erasing, NVCVTensorHandle values, NVCVTensorHandle imgIdx, int8_t random, uint32_t seed)) { + CVCUDA_NVTX_RANGE("cvcudaEraseVarShapeSubmit"); + return nvcv::ProtectCall( + [&in, &out, &anchor, &erasing, &values, &imgIdx, &handle, &stream, &random, &seed] + { + SubmitErase( + handle, stream, in, out, anchor, erasing, values, imgIdx, random, seed); + }); +} + +CVCUDA_DEFINE_API(0, 17, NVCVStatus, cvcudaEraseRegionSubmit, + (NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle in, NVCVTensorHandle out, int64_t i, + int64_t j, int64_t h, int64_t w, NVCVTensorHandle values)) +{ + CVCUDA_NVTX_RANGE("cvcudaEraseRegionSubmit"); return nvcv::ProtectCall( - [&] + [handle, stream, in, out, i, j, h, w, values] { - nvcv::ImageBatchVarShapeWrapHandle input(in), output(out); - nvcv::TensorWrapHandle anchorwrap(anchor), erasingwrap(erasing), valueswrap(values), imgIdxwrap(imgIdx); - priv::ToDynamicRef(handle)(stream, input, output, anchorwrap, erasingwrap, valueswrap, - imgIdxwrap, random, seed); + nvcv::TensorWrapHandle input(in); + nvcv::TensorWrapHandle output(out); + nvcv::TensorWrapHandle value(values); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), i, j, h, w, + value.resource()); }); } diff --git a/src/cvcuda/OpFindHomography.cpp b/src/cvcuda/OpFindHomography.cpp index c3ded42ca..285cff384 100644 --- a/src/cvcuda/OpFindHomography.cpp +++ b/src/cvcuda/OpFindHomography.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "priv/OpFindHomography.hpp" +#include "priv/Nvtx.hpp" #include "priv/SymbolVersioning.hpp" #include @@ -29,7 +30,7 @@ CVCUDA_DEFINE_API(0, 5, NVCVStatus, cvcudaFindHomographyCreate, (NVCVOperatorHandle * handle, int batchSize, int numPoints)) { return nvcv::ProtectCall( - [&] + [&handle, &batchSize, &numPoints] { if (handle == nullptr) { @@ -37,7 +38,7 @@ CVCUDA_DEFINE_API(0, 5, NVCVStatus, cvcudaFindHomographyCreate, "Pointer to NVCVOperator handle must not be NULL"); } - *handle = reinterpret_cast(new priv::FindHomography(batchSize, numPoints)); + *handle = priv::CreateOperatorHandle(batchSize, numPoints); }); } @@ -45,11 +46,15 @@ CVCUDA_DEFINE_API(0, 5, NVCVStatus, cvcudaFindHomographySubmit, (NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle srcPts, NVCVTensorHandle dstPts, NVCVTensorHandle models)) { + CVCUDA_NVTX_RANGE("cvcudaFindHomographySubmit"); return nvcv::ProtectCall( - [&] + [&srcPts, &dstPts, &models, &handle, &stream] { - nvcv::TensorWrapHandle _srcPts(srcPts), _dstPts(dstPts), _models(models); - priv::ToDynamicRef(handle)(stream, _srcPts, _dstPts, _models); + nvcv::TensorWrapHandle _srcPts(srcPts); + nvcv::TensorWrapHandle _dstPts(dstPts); + nvcv::TensorWrapHandle _models(models); + priv::ToDynamicRef(handle)(stream, _srcPts.resource(), _dstPts.resource(), + _models.resource()); }); } @@ -57,11 +62,14 @@ CVCUDA_DEFINE_API(0, 5, NVCVStatus, cvcudaFindHomographyVarShapeSubmit, (NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorBatchHandle srcPts, NVCVTensorBatchHandle dstPts, NVCVTensorBatchHandle models)) { + CVCUDA_NVTX_RANGE("cvcudaFindHomographyVarShapeSubmit"); return nvcv::ProtectCall( - [&] + [&srcPts, &dstPts, &models, &handle, &stream] { - nvcv::TensorBatchWrapHandle _srcPts(srcPts), _dstPts(dstPts); + nvcv::TensorBatchWrapHandle _srcPts(srcPts); + nvcv::TensorBatchWrapHandle _dstPts(dstPts); nvcv::TensorBatchWrapHandle _models(models); - priv::ToDynamicRef(handle)(stream, _srcPts, _dstPts, _models); + priv::ToDynamicRef(handle)(stream, _srcPts.resource(), _dstPts.resource(), + _models.resource()); }); } diff --git a/src/cvcuda/OpFlip.cpp b/src/cvcuda/OpFlip.cpp index 9278673f1..2a37e2400 100644 --- a/src/cvcuda/OpFlip.cpp +++ b/src/cvcuda/OpFlip.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "priv/OpFlip.hpp" +#include "priv/Nvtx.hpp" #include "priv/SymbolVersioning.hpp" #include @@ -29,7 +30,7 @@ namespace priv = cvcuda::priv; CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaFlipCreate, (NVCVOperatorHandle * handle, int32_t maxVarShapeBatchSize)) { return nvcv::ProtectCall( - [&] + [&handle, &maxVarShapeBatchSize] { if (handle == nullptr) { @@ -37,7 +38,7 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaFlipCreate, (NVCVOperatorHandle * hand "Pointer to NVCVOperator handle must not be NULL"); } - *handle = reinterpret_cast(new priv::Flip(maxVarShapeBatchSize)); + *handle = priv::CreateOperatorHandle(maxVarShapeBatchSize); }); } @@ -45,11 +46,13 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaFlipSubmit, (NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle in, NVCVTensorHandle out, int32_t flipCode)) { + CVCUDA_NVTX_RANGE("cvcudaFlipSubmit"); return nvcv::ProtectCall( - [&] + [&out, &in, &handle, &stream, &flipCode] { - nvcv::TensorWrapHandle output(out), input(in); - priv::ToDynamicRef(handle)(stream, input, output, flipCode); + nvcv::TensorWrapHandle output(out); + nvcv::TensorWrapHandle input(in); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), flipCode); }); } @@ -57,11 +60,13 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaFlipVarShapeSubmit, (NVCVOperatorHandle handle, cudaStream_t stream, NVCVImageBatchHandle in, NVCVImageBatchHandle out, NVCVTensorHandle flipCode)) { + CVCUDA_NVTX_RANGE("cvcudaFlipVarShapeSubmit"); return nvcv::ProtectCall( - [&] + [&out, &in, &flipCode, &handle, &stream] { - nvcv::ImageBatchVarShapeWrapHandle output(out), input(in); + nvcv::ImageBatchVarShapeWrapHandle output(out); + nvcv::ImageBatchVarShapeWrapHandle input(in); nvcv::TensorWrapHandle flip_code(flipCode); - priv::ToDynamicRef(handle)(stream, input, output, flip_code); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), flip_code.resource()); }); } diff --git a/src/cvcuda/OpGammaContrast.cpp b/src/cvcuda/OpGammaContrast.cpp index 0a3175181..ba0b316bb 100644 --- a/src/cvcuda/OpGammaContrast.cpp +++ b/src/cvcuda/OpGammaContrast.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "priv/OpGammaContrast.hpp" +#include "priv/Nvtx.hpp" #include "priv/SymbolVersioning.hpp" #include @@ -31,7 +32,7 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaGammaContrastCreate, const int32_t maxVarShapeChannelCount)) { return nvcv::ProtectCall( - [&] + [&handle, &maxVarShapeBatchSize, &maxVarShapeChannelCount] { if (handle == nullptr) { @@ -39,8 +40,19 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaGammaContrastCreate, "Pointer to NVCVOperator handle must not be NULL"); } - *handle = reinterpret_cast( - new priv::GammaContrast(maxVarShapeBatchSize, maxVarShapeChannelCount)); + if (maxVarShapeBatchSize <= 0) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Maximum var-shape batch size must be positive"); + } + + if (maxVarShapeChannelCount <= 0) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Maximum var-shape channel count must be positive"); + } + + *handle = priv::CreateOperatorHandle(maxVarShapeBatchSize, maxVarShapeChannelCount); }); } @@ -48,11 +60,45 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaGammaContrastVarShapeSubmit, (NVCVOperatorHandle handle, cudaStream_t stream, NVCVImageBatchHandle in, NVCVImageBatchHandle out, NVCVTensorHandle gamma)) { + CVCUDA_NVTX_RANGE("cvcudaGammaContrastVarShapeSubmit"); return nvcv::ProtectCall( - [&] + [&in, &out, &gamma, &handle, &stream] { - nvcv::ImageBatchVarShapeWrapHandle inWrap(in), outWrap(out); + nvcv::ImageBatchVarShapeWrapHandle inWrap(in); + nvcv::ImageBatchVarShapeWrapHandle outWrap(out); nvcv::TensorWrapHandle gammaWrap(gamma); - priv::ToDynamicRef(handle)(stream, inWrap, outWrap, gammaWrap); + priv::ToDynamicRef(handle)(stream, inWrap.resource(), outWrap.resource(), + gammaWrap.resource()); + }); +} + +CVCUDA_DEFINE_API(0, 17, NVCVStatus, cvcudaGammaContrastSubmit, + (NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle in, NVCVTensorHandle out, + NVCVTensorHandle gamma)) +{ + CVCUDA_NVTX_RANGE("cvcudaGammaContrastSubmit"); + return nvcv::ProtectCall( + [&in, &out, &gamma, &handle, &stream] + { + nvcv::TensorWrapHandle inWrap(in); + nvcv::TensorWrapHandle outWrap(out); + nvcv::TensorWrapHandle gammaWrap(gamma); + priv::ToDynamicRef(handle)(stream, inWrap.resource(), outWrap.resource(), + gammaWrap.resource()); + }); +} + +CVCUDA_DEFINE_API(0, 17, NVCVStatus, cvcudaGammaContrastScalarSubmit, + (NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle in, NVCVTensorHandle out, + float gamma, float gain, NVCVRoundMode roundMode)) +{ + CVCUDA_NVTX_RANGE("cvcudaGammaContrastScalarSubmit"); + return nvcv::ProtectCall( + [&in, &out, &gamma, &gain, &roundMode, &handle, &stream] + { + nvcv::TensorWrapHandle inWrap(in); + nvcv::TensorWrapHandle outWrap(out); + priv::ToDynamicRef(handle)(stream, inWrap.resource(), outWrap.resource(), gamma, gain, + roundMode); }); } diff --git a/src/cvcuda/OpGaussian.cpp b/src/cvcuda/OpGaussian.cpp index e618caebe..a3216854a 100644 --- a/src/cvcuda/OpGaussian.cpp +++ b/src/cvcuda/OpGaussian.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "priv/OpGaussian.hpp" +#include "priv/Nvtx.hpp" #include "priv/SymbolVersioning.hpp" #include @@ -31,7 +32,7 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaGaussianCreate, int32_t maxVarShapeBatchSize)) { return nvcv::ProtectCall( - [&] + [&handle, &maxKernelWidth, &maxKernelHeight, &maxVarShapeBatchSize] { if (handle == nullptr) { @@ -39,8 +40,8 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaGaussianCreate, "Pointer to NVCVOperator handle must not be NULL"); } - *handle = reinterpret_cast( - new priv::Gaussian(nvcv::Size2D{maxKernelWidth, maxKernelHeight}, maxVarShapeBatchSize)); + *handle = priv::CreateOperatorHandle(nvcv::Size2D{maxKernelWidth, maxKernelHeight}, + maxVarShapeBatchSize); }); } @@ -48,12 +49,15 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaGaussianSubmit, (NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle in, NVCVTensorHandle out, int32_t kernelWidth, int32_t kernelHeight, double sigmaX, double sigmaY, NVCVBorderType borderMode)) { + CVCUDA_NVTX_RANGE("cvcudaGaussianSubmit"); return nvcv::ProtectCall( - [&] + [&out, &in, &handle, &stream, &kernelWidth, &kernelHeight, &sigmaX, &sigmaY, &borderMode] { - nvcv::TensorWrapHandle output(out), input(in); - priv::ToDynamicRef(handle)(stream, input, output, nvcv::Size2D{kernelWidth, kernelHeight}, - double2{sigmaX, sigmaY}, borderMode); + nvcv::TensorWrapHandle output(out); + nvcv::TensorWrapHandle input(in); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), + nvcv::Size2D{kernelWidth, kernelHeight}, double2{sigmaX, sigmaY}, + borderMode); }); } @@ -61,11 +65,15 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaGaussianVarShapeSubmit, (NVCVOperatorHandle handle, cudaStream_t stream, NVCVImageBatchHandle in, NVCVImageBatchHandle out, NVCVTensorHandle kernelSize, NVCVTensorHandle sigma, NVCVBorderType borderMode)) { + CVCUDA_NVTX_RANGE("cvcudaGaussianVarShapeSubmit"); return nvcv::ProtectCall( - [&] + [&in, &out, &kernelSize, &sigma, &handle, &stream, &borderMode] { - nvcv::ImageBatchVarShapeWrapHandle inWrap(in), outWrap(out); - nvcv::TensorWrapHandle kernelSizeWrap(kernelSize), sigmaWrap(sigma); - priv::ToDynamicRef(handle)(stream, inWrap, outWrap, kernelSizeWrap, sigmaWrap, borderMode); + nvcv::ImageBatchVarShapeWrapHandle inWrap(in); + nvcv::ImageBatchVarShapeWrapHandle outWrap(out); + nvcv::TensorWrapHandle kernelSizeWrap(kernelSize); + nvcv::TensorWrapHandle sigmaWrap(sigma); + priv::ToDynamicRef(handle)(stream, inWrap.resource(), outWrap.resource(), + kernelSizeWrap.resource(), sigmaWrap.resource(), borderMode); }); } diff --git a/src/cvcuda/OpGaussianNoise.cpp b/src/cvcuda/OpGaussianNoise.cpp index 490e5edec..3ab0e18f6 100644 --- a/src/cvcuda/OpGaussianNoise.cpp +++ b/src/cvcuda/OpGaussianNoise.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "priv/OpGaussianNoise.hpp" +#include "priv/Nvtx.hpp" #include "priv/SymbolVersioning.hpp" #include @@ -29,7 +30,7 @@ namespace priv = cvcuda::priv; CVCUDA_DEFINE_API(0, 4, NVCVStatus, cvcudaGaussianNoiseCreate, (NVCVOperatorHandle * handle, int maxBatchSize)) { return nvcv::ProtectCall( - [&] + [&handle, &maxBatchSize] { if (handle == nullptr) { @@ -37,7 +38,7 @@ CVCUDA_DEFINE_API(0, 4, NVCVStatus, cvcudaGaussianNoiseCreate, (NVCVOperatorHand "Pointer to NVCVOperator handle must not be NULL"); } - *handle = reinterpret_cast(new priv::GaussianNoise(maxBatchSize)); + *handle = priv::CreateOperatorHandle(maxBatchSize); }); } @@ -45,25 +46,50 @@ CVCUDA_DEFINE_API(0, 4, NVCVStatus, cvcudaGaussianNoiseSubmit, (NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle in, NVCVTensorHandle out, NVCVTensorHandle mu, NVCVTensorHandle sigma, int8_t per_channel, unsigned long long seed)) { + CVCUDA_NVTX_RANGE("cvcudaGaussianNoiseSubmit"); return nvcv::ProtectCall( - [&] + [&in, &out, &mu, &sigma, &handle, &stream, &per_channel, &seed] { - nvcv::TensorWrapHandle input(in), output(out), muwrap(mu), sigmawrap(sigma); - priv::ToDynamicRef(handle)(stream, input, output, muwrap, sigmawrap, + nvcv::TensorWrapHandle input(in); + nvcv::TensorWrapHandle output(out); + nvcv::TensorWrapHandle muwrap(mu); + nvcv::TensorWrapHandle sigmawrap(sigma); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), + muwrap.resource(), sigmawrap.resource(), static_cast(per_channel), seed); }); } +CVCUDA_DEFINE_API(0, 17, NVCVStatus, cvcudaGaussianNoiseScalarSubmit, + (NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle in, NVCVTensorHandle out, float mu, + float sigma, int8_t per_channel, unsigned long long seed, int8_t reseed, int8_t clip)) +{ + CVCUDA_NVTX_RANGE("cvcudaGaussianNoiseScalarSubmit"); + return nvcv::ProtectCall( + [&in, &out, &mu, &sigma, &handle, &stream, &per_channel, &seed, &reseed, &clip] + { + nvcv::TensorWrapHandle input(in); + nvcv::TensorWrapHandle output(out); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), mu, sigma, + static_cast(per_channel), seed, + static_cast(reseed), static_cast(clip)); + }); +} + CVCUDA_DEFINE_API(0, 4, NVCVStatus, cvcudaGaussianNoiseVarShapeSubmit, (NVCVOperatorHandle handle, cudaStream_t stream, NVCVImageBatchHandle in, NVCVImageBatchHandle out, NVCVTensorHandle mu, NVCVTensorHandle sigma, int8_t per_channel, unsigned long long seed)) { + CVCUDA_NVTX_RANGE("cvcudaGaussianNoiseVarShapeSubmit"); return nvcv::ProtectCall( - [&] + [&in, &out, &mu, &sigma, &handle, &stream, &per_channel, &seed] { - nvcv::ImageBatchVarShapeWrapHandle input(in), output(out); - nvcv::TensorWrapHandle muwrap(mu), sigmawrap(sigma); - priv::ToDynamicRef(handle)(stream, input, output, muwrap, sigmawrap, + nvcv::ImageBatchVarShapeWrapHandle input(in); + nvcv::ImageBatchVarShapeWrapHandle output(out); + nvcv::TensorWrapHandle muwrap(mu); + nvcv::TensorWrapHandle sigmawrap(sigma); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), + muwrap.resource(), sigmawrap.resource(), static_cast(per_channel), seed); }); } diff --git a/src/cvcuda/OpHQResize.cpp b/src/cvcuda/OpHQResize.cpp index 00f283f07..bd5d5635e 100644 --- a/src/cvcuda/OpHQResize.cpp +++ b/src/cvcuda/OpHQResize.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "cvcuda/OpHQResize.h" +#include "priv/Nvtx.hpp" #include "priv/OpHQResize.hpp" #include "priv/SymbolVersioning.hpp" @@ -25,12 +26,43 @@ #include #include +#include + namespace priv = cvcuda::priv; +namespace { + +bool roiIsFinite(const HQResizeRoiF &roi) +{ + for (int i = 0; i < NVCV_HQ_RESIZE_MAX_RESIZED_NDIM; i++) + { + if (!std::isfinite(roi.lo[i]) || !std::isfinite(roi.hi[i])) + return false; + } + return true; +} + +bool roisAreFinite(const HQResizeRoisF &rois) +{ + if (rois.roi == nullptr) + return true; + for (int i = 0; i < rois.size; i++) + { + for (int j = 0; j < rois.ndim; j++) + { + if (!std::isfinite(rois.roi[i].lo[j]) || !std::isfinite(rois.roi[i].hi[j])) + return false; + } + } + return true; +} + +} // namespace + CVCUDA_DEFINE_API(0, 6, NVCVStatus, cvcudaHQResizeCreate, (NVCVOperatorHandle * handle)) { return nvcv::ProtectCall( - [&] + [&handle] { if (handle == nullptr) { @@ -38,7 +70,7 @@ CVCUDA_DEFINE_API(0, 6, NVCVStatus, cvcudaHQResizeCreate, (NVCVOperatorHandle * "Pointer to NVCVOperator handle must not be NULL"); } - *handle = reinterpret_cast(new priv::HQResize()); + *handle = priv::CreateOperatorHandle(); }); } @@ -48,12 +80,20 @@ CVCUDA_DEFINE_API(0, 6, NVCVStatus, cvcudaHQResizeTensorGetWorkspaceRequirements const NVCVInterpolationType magInterpolation, bool antialias, const HQResizeRoiF *roi, NVCVWorkspaceRequirements *reqOut)) { - if (!reqOut) - return NVCV_ERROR_INVALID_ARGUMENT; - return nvcv::ProtectCall( - [&] + [&reqOut, &roi, &handle, &batchSize, &inputShape, &outputShape, &minInterpolation, &magInterpolation, + &antialias] { + if (reqOut == nullptr) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Pointer to output workspace requirements must not be NULL"); + } + if (roi != nullptr && !roiIsFinite(*roi)) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "ROI coordinates must be finite"); + } + *reqOut = priv::ToDynamicRef(handle).getWorkspaceRequirements( batchSize, inputShape, outputShape, minInterpolation, magInterpolation, antialias, roi); }); @@ -65,12 +105,20 @@ CVCUDA_DEFINE_API(0, 6, NVCVStatus, cvcudaHQResizeTensorBatchGetWorkspaceRequire const NVCVInterpolationType magInterpolation, bool antialias, const HQResizeRoisF roi, NVCVWorkspaceRequirements *reqOut)) { - if (!reqOut) - return NVCV_ERROR_INVALID_ARGUMENT; - return nvcv::ProtectCall( - [&] + [&reqOut, &roi, &handle, &batchSize, &inputShapes, &outputShapes, &minInterpolation, &magInterpolation, + &antialias] { + if (reqOut == nullptr) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Pointer to output workspace requirements must not be NULL"); + } + if (!roisAreFinite(roi)) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "ROI coordinates must be finite"); + } + *reqOut = priv::ToDynamicRef(handle).getWorkspaceRequirements( batchSize, inputShapes, outputShapes, minInterpolation, magInterpolation, antialias, roi); }); @@ -80,11 +128,17 @@ CVCUDA_DEFINE_API(0, 6, NVCVStatus, cvcudaHQResizeGetMaxWorkspaceRequirements, (NVCVOperatorHandle handle, int maxBatchSize, const HQResizeTensorShapeI maxShape, NVCVWorkspaceRequirements *reqOut)) { - if (!reqOut) - return NVCV_ERROR_INVALID_ARGUMENT; - return nvcv::ProtectCall( - [&] { *reqOut = priv::ToDynamicRef(handle).getWorkspaceRequirements(maxBatchSize, maxShape); }); + [&reqOut, &handle, &maxBatchSize, &maxShape] + { + if (reqOut == nullptr) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Pointer to output workspace requirements must not be NULL"); + } + + *reqOut = priv::ToDynamicRef(handle).getWorkspaceRequirements(maxBatchSize, maxShape); + }); } CVCUDA_DEFINE_API(0, 6, NVCVStatus, cvcudaHQResizeSubmit, @@ -92,15 +146,24 @@ CVCUDA_DEFINE_API(0, 6, NVCVStatus, cvcudaHQResizeSubmit, NVCVTensorHandle out, const NVCVInterpolationType minInterpolation, const NVCVInterpolationType magInterpolation, bool antialias, const HQResizeRoiF *roi)) { - if (!ws) - return NVCV_ERROR_INVALID_ARGUMENT; - + CVCUDA_NVTX_RANGE("cvcudaHQResizeSubmit"); return nvcv::ProtectCall( - [&] + [&ws, &roi, &in, &out, &handle, &stream, &minInterpolation, &magInterpolation, &antialias] { - nvcv::TensorWrapHandle _in(in), _out(out); - priv::ToDynamicRef(handle)(stream, *ws, _in, _out, minInterpolation, magInterpolation, - antialias, roi); + if (ws == nullptr) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Pointer to workspace must not be NULL"); + } + if (roi != nullptr && !roiIsFinite(*roi)) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "ROI coordinates must be finite"); + } + + nvcv::TensorWrapHandle _in(in); + + nvcv::TensorWrapHandle _out(out); + priv::ToDynamicRef(handle)(stream, *ws, _in.resource(), _out.resource(), minInterpolation, + magInterpolation, antialias, roi); }); } @@ -109,15 +172,24 @@ CVCUDA_DEFINE_API(0, 6, NVCVStatus, cvcudaHQResizeImageBatchSubmit, NVCVImageBatchHandle out, const NVCVInterpolationType minInterpolation, const NVCVInterpolationType magInterpolation, bool antialias, const HQResizeRoisF roi)) { - if (!ws) - return NVCV_ERROR_INVALID_ARGUMENT; - + CVCUDA_NVTX_RANGE("cvcudaHQResizeImageBatchSubmit"); return nvcv::ProtectCall( - [&] + [&ws, &roi, &in, &out, &handle, &stream, &minInterpolation, &magInterpolation, &antialias] { - nvcv::ImageBatchVarShapeWrapHandle _in(in), _out(out); - priv::ToDynamicRef(handle)(stream, *ws, _in, _out, minInterpolation, magInterpolation, - antialias, roi); + if (ws == nullptr) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Pointer to workspace must not be NULL"); + } + if (!roisAreFinite(roi)) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "ROI coordinates must be finite"); + } + + nvcv::ImageBatchVarShapeWrapHandle _in(in); + + nvcv::ImageBatchVarShapeWrapHandle _out(out); + priv::ToDynamicRef(handle)(stream, *ws, _in.resource(), _out.resource(), minInterpolation, + magInterpolation, antialias, roi); }); } @@ -126,14 +198,23 @@ CVCUDA_DEFINE_API(0, 6, NVCVStatus, cvcudaHQResizeTensorBatchSubmit, NVCVTensorBatchHandle out, const NVCVInterpolationType minInterpolation, const NVCVInterpolationType magInterpolation, bool antialias, const HQResizeRoisF roi)) { - if (!ws) - return NVCV_ERROR_INVALID_ARGUMENT; - + CVCUDA_NVTX_RANGE("cvcudaHQResizeTensorBatchSubmit"); return nvcv::ProtectCall( - [&] + [&ws, &roi, &in, &out, &handle, &stream, &minInterpolation, &magInterpolation, &antialias] { - nvcv::TensorBatchWrapHandle _in(in), _out(out); - priv::ToDynamicRef(handle)(stream, *ws, _in, _out, minInterpolation, magInterpolation, - antialias, roi); + if (ws == nullptr) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Pointer to workspace must not be NULL"); + } + if (!roisAreFinite(roi)) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "ROI coordinates must be finite"); + } + + nvcv::TensorBatchWrapHandle _in(in); + + nvcv::TensorBatchWrapHandle _out(out); + priv::ToDynamicRef(handle)(stream, *ws, _in.resource(), _out.resource(), minInterpolation, + magInterpolation, antialias, roi); }); } diff --git a/src/cvcuda/OpHistogram.cpp b/src/cvcuda/OpHistogram.cpp index 9cbc8877a..457b95be3 100644 --- a/src/cvcuda/OpHistogram.cpp +++ b/src/cvcuda/OpHistogram.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "priv/OpHistogram.hpp" +#include "priv/Nvtx.hpp" #include "priv/SymbolVersioning.hpp" #include @@ -28,7 +29,7 @@ namespace priv = cvcuda::priv; CVCUDA_DEFINE_API(0, 4, NVCVStatus, cvcudaHistogramCreate, (NVCVOperatorHandle * handle)) { return nvcv::ProtectCall( - [&] + [&handle] { if (handle == nullptr) { @@ -36,7 +37,7 @@ CVCUDA_DEFINE_API(0, 4, NVCVStatus, cvcudaHistogramCreate, (NVCVOperatorHandle * "Pointer to NVCVOperator handle must not be NULL"); } - *handle = reinterpret_cast(new priv::Histogram()); + *handle = priv::CreateOperatorHandle(); }); } @@ -44,10 +45,13 @@ CVCUDA_DEFINE_API(0, 4, NVCVStatus, cvcudaHistogramSubmit, (NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle in, NVCVTensorHandle mask, NVCVTensorHandle histogram)) { + CVCUDA_NVTX_RANGE("cvcudaHistogramSubmit"); return nvcv::ProtectCall( - [&] + [&in, &histogram, &handle, &stream, &mask] { - nvcv::TensorWrapHandle input(in), output(histogram); - priv::ToDynamicRef(handle)(stream, input, NVCV_TENSOR_HANDLE_TO_OPTIONAL(mask), output); + nvcv::TensorWrapHandle input(in); + nvcv::TensorWrapHandle output(histogram); + priv::ToDynamicRef(handle)(stream, input.resource(), NVCV_TENSOR_HANDLE_TO_OPTIONAL(mask), + output.resource()); }); } diff --git a/src/cvcuda/OpHistogramEq.cpp b/src/cvcuda/OpHistogramEq.cpp index 3e24990c7..b15d83dcb 100644 --- a/src/cvcuda/OpHistogramEq.cpp +++ b/src/cvcuda/OpHistogramEq.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "priv/OpHistogramEq.hpp" +#include "priv/Nvtx.hpp" #include "priv/SymbolVersioning.hpp" #include @@ -28,7 +29,7 @@ namespace priv = cvcuda::priv; CVCUDA_DEFINE_API(0, 4, NVCVStatus, cvcudaHistogramEqCreate, (NVCVOperatorHandle * handle, int maxBatchSize)) { return nvcv::ProtectCall( - [&] + [&handle, &maxBatchSize] { if (handle == nullptr) { @@ -36,28 +37,32 @@ CVCUDA_DEFINE_API(0, 4, NVCVStatus, cvcudaHistogramEqCreate, (NVCVOperatorHandle "Pointer to NVCVOperator handle must not be NULL"); } - *handle = reinterpret_cast(new priv::HistogramEq(maxBatchSize)); + *handle = priv::CreateOperatorHandle(maxBatchSize); }); } CVCUDA_DEFINE_API(0, 4, NVCVStatus, cvcudaHistogramEqSubmit, (NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle in, NVCVTensorHandle out)) { + CVCUDA_NVTX_RANGE("cvcudaHistogramEqSubmit"); return nvcv::ProtectCall( - [&] + [&in, &out, &handle, &stream] { - nvcv::TensorWrapHandle input(in), output(out); - priv::ToDynamicRef(handle)(stream, input, output); + nvcv::TensorWrapHandle input(in); + nvcv::TensorWrapHandle output(out); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource()); }); } CVCUDA_DEFINE_API(0, 4, NVCVStatus, cvcudaHistogramEqVarShapeSubmit, (NVCVOperatorHandle handle, cudaStream_t stream, NVCVImageBatchHandle in, NVCVImageBatchHandle out)) { + CVCUDA_NVTX_RANGE("cvcudaHistogramEqVarShapeSubmit"); return nvcv::ProtectCall( - [&] + [&in, &out, &handle, &stream] { - nvcv::ImageBatchVarShapeWrapHandle input(in), output(out); - priv::ToDynamicRef(handle)(stream, input, output); + nvcv::ImageBatchVarShapeWrapHandle input(in); + nvcv::ImageBatchVarShapeWrapHandle output(out); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource()); }); } diff --git a/src/cvcuda/OpInpaint.cpp b/src/cvcuda/OpInpaint.cpp index 74cb87445..ec53a8a0e 100644 --- a/src/cvcuda/OpInpaint.cpp +++ b/src/cvcuda/OpInpaint.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "priv/OpInpaint.hpp" +#include "priv/Nvtx.hpp" #include "priv/SymbolVersioning.hpp" #include @@ -30,16 +31,23 @@ CVCUDA_DEFINE_API(0, 4, NVCVStatus, cvcudaInpaintCreate, (NVCVOperatorHandle * handle, int32_t maxBatchSize, int32_t maxHeight, int32_t maxWidth)) { return nvcv::ProtectCall( - [&] + [&handle, &maxBatchSize, &maxWidth, &maxHeight] { if (handle == nullptr) { throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Pointer to NVCVOperator handle must not be NULL"); } + if (maxBatchSize <= 0) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "maxBatchSize must be > 0"); + } + if (maxHeight <= 0 || maxWidth <= 0) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "maxHeight and maxWidth must be > 0"); + } - *handle = reinterpret_cast( - new priv::Inpaint(maxBatchSize, nvcv::Size2D{maxWidth, maxHeight})); + *handle = priv::CreateOperatorHandle(maxBatchSize, nvcv::Size2D{maxWidth, maxHeight}); }); } @@ -47,11 +55,15 @@ CVCUDA_DEFINE_API(0, 4, NVCVStatus, cvcudaInpaintSubmit, (NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle in, NVCVTensorHandle masks, NVCVTensorHandle out, double inpaintRadius)) { + CVCUDA_NVTX_RANGE("cvcudaInpaintSubmit"); return nvcv::ProtectCall( - [&] + [&in, &out, &masks, &handle, &stream, &inpaintRadius] { - nvcv::TensorWrapHandle input(in), output(out), maskswrap(masks); - priv::ToDynamicRef(handle)(stream, input, maskswrap, output, inpaintRadius); + nvcv::TensorWrapHandle input(in); + nvcv::TensorWrapHandle output(out); + nvcv::TensorWrapHandle maskswrap(masks); + priv::ToDynamicRef(handle)(stream, input.resource(), maskswrap.resource(), output.resource(), + inpaintRadius); }); } @@ -59,10 +71,14 @@ CVCUDA_DEFINE_API(0, 4, NVCVStatus, cvcudaInpaintVarShapeSubmit, (NVCVOperatorHandle handle, cudaStream_t stream, NVCVImageBatchHandle in, NVCVImageBatchHandle masks, NVCVImageBatchHandle out, double inpaintRadius)) { + CVCUDA_NVTX_RANGE("cvcudaInpaintVarShapeSubmit"); return nvcv::ProtectCall( - [&] + [&in, &out, &masks, &handle, &stream, &inpaintRadius] { - nvcv::ImageBatchVarShapeWrapHandle input(in), output(out), maskswrap(masks); - priv::ToDynamicRef(handle)(stream, input, maskswrap, output, inpaintRadius); + nvcv::ImageBatchVarShapeWrapHandle input(in); + nvcv::ImageBatchVarShapeWrapHandle output(out); + nvcv::ImageBatchVarShapeWrapHandle maskswrap(masks); + priv::ToDynamicRef(handle)(stream, input.resource(), maskswrap.resource(), output.resource(), + inpaintRadius); }); } diff --git a/src/cvcuda/OpInvert.cpp b/src/cvcuda/OpInvert.cpp new file mode 100644 index 000000000..459225d0d --- /dev/null +++ b/src/cvcuda/OpInvert.cpp @@ -0,0 +1,70 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "priv/OpInvert.hpp" + +#include "priv/Nvtx.hpp" +#include "priv/SymbolVersioning.hpp" + +#include +#include +#include + +namespace priv = cvcuda::priv; + +CVCUDA_DEFINE_API(0, 0, NVCVStatus, cvcudaInvertCreate, (NVCVOperatorHandle * handle)) +{ + return nvcv::ProtectCall( + [handle] + { + if (handle == nullptr) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Pointer to NVCVOperator handle must not be NULL"); + } + + // Ownership of the operator is transferred to the C handle and released by + // cvcudaOperatorDestroy; this matches every other operator's Create entry point. + *handle = reinterpret_cast(new priv::Invert()); // NOSONAR + }); +} + +CVCUDA_DEFINE_API(0, 0, NVCVStatus, cvcudaInvertSubmit, + (NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle in, NVCVTensorHandle out)) +{ + CVCUDA_NVTX_RANGE("cvcudaInvertSubmit"); + return nvcv::ProtectCall( + [handle, stream, in, out] + { + nvcv::TensorWrapHandle input(in); + nvcv::TensorWrapHandle output(out); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource()); + }); +} + +CVCUDA_DEFINE_API(0, 0, NVCVStatus, cvcudaInvertVarShapeSubmit, + (NVCVOperatorHandle handle, cudaStream_t stream, NVCVImageBatchHandle in, NVCVImageBatchHandle out)) +{ + CVCUDA_NVTX_RANGE("cvcudaInvertVarShapeSubmit"); + return nvcv::ProtectCall( + [handle, stream, in, out] + { + nvcv::ImageBatchVarShapeWrapHandle input(in); + nvcv::ImageBatchVarShapeWrapHandle output(out); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource()); + }); +} diff --git a/src/cvcuda/OpJointBilateralFilter.cpp b/src/cvcuda/OpJointBilateralFilter.cpp index 00b609e4e..2632fa45c 100644 --- a/src/cvcuda/OpJointBilateralFilter.cpp +++ b/src/cvcuda/OpJointBilateralFilter.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "priv/OpJointBilateralFilter.hpp" +#include "priv/Nvtx.hpp" #include "priv/SymbolVersioning.hpp" #include @@ -29,7 +30,7 @@ namespace priv = cvcuda::priv; CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaJointBilateralFilterCreate, (NVCVOperatorHandle * handle)) { return nvcv::ProtectCall( - [&] + [&handle] { if (handle == nullptr) { @@ -37,7 +38,7 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaJointBilateralFilterCreate, (NVCVOpera "Pointer to NVCVOperator handle must not be NULL"); } - *handle = reinterpret_cast(new priv::JointBilateralFilter()); + *handle = priv::CreateOperatorHandle(); }); } @@ -45,12 +46,16 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaJointBilateralFilterSubmit, (NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle in, NVCVTensorHandle inColor, NVCVTensorHandle out, int diameter, float sigmaColor, float sigmaSpace, NVCVBorderType borderMode)) { + CVCUDA_NVTX_RANGE("cvcudaJointBilateralFilterSubmit"); return nvcv::ProtectCall( - [&] + [&in, &inColor, &out, &handle, &stream, &diameter, &sigmaColor, &sigmaSpace, &borderMode] { - nvcv::TensorWrapHandle input(in), inputColor(inColor), output(out); - priv::ToDynamicRef(handle)(stream, input, inputColor, output, diameter, - sigmaColor, sigmaSpace, borderMode); + nvcv::TensorWrapHandle input(in); + nvcv::TensorWrapHandle inputColor(inColor); + nvcv::TensorWrapHandle output(out); + priv::ToDynamicRef(handle)(stream, input.resource(), inputColor.resource(), + output.resource(), diameter, sigmaColor, sigmaSpace, + borderMode); }); } @@ -59,12 +64,18 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaJointBilateralFilterVarShapeSubmit, NVCVImageBatchHandle inColor, NVCVImageBatchHandle out, NVCVTensorHandle diameter, NVCVTensorHandle sigmaColor, NVCVTensorHandle sigmaSpace, NVCVBorderType borderMode)) { + CVCUDA_NVTX_RANGE("cvcudaJointBilateralFilterVarShapeSubmit"); return nvcv::ProtectCall( - [&] + [&in, &inColor, &out, &diameter, &sigmaColor, &sigmaSpace, &handle, &stream, &borderMode] { - nvcv::ImageBatchVarShapeWrapHandle input(in), inputColor(inColor), output(out); - nvcv::TensorWrapHandle diameterData(diameter), sigmaColorData(sigmaColor), sigmaSpaceData(sigmaSpace); - priv::ToDynamicRef(handle)(stream, input, inputColor, output, diameterData, - sigmaColorData, sigmaSpaceData, borderMode); + nvcv::ImageBatchVarShapeWrapHandle input(in); + nvcv::ImageBatchVarShapeWrapHandle inputColor(inColor); + nvcv::ImageBatchVarShapeWrapHandle output(out); + nvcv::TensorWrapHandle diameterData(diameter); + nvcv::TensorWrapHandle sigmaColorData(sigmaColor); + nvcv::TensorWrapHandle sigmaSpaceData(sigmaSpace); + priv::ToDynamicRef(handle)( + stream, input.resource(), inputColor.resource(), output.resource(), diameterData.resource(), + sigmaColorData.resource(), sigmaSpaceData.resource(), borderMode); }); } diff --git a/src/cvcuda/OpJpegCompressionDistortion.cpp b/src/cvcuda/OpJpegCompressionDistortion.cpp new file mode 100644 index 000000000..37698a994 --- /dev/null +++ b/src/cvcuda/OpJpegCompressionDistortion.cpp @@ -0,0 +1,106 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "priv/OpJpegCompressionDistortion.hpp" + +#include "priv/Nvtx.hpp" +#include "priv/SymbolVersioning.hpp" + +#include +#include +#include + +namespace priv = cvcuda::priv; + +CVCUDA_DEFINE_API(0, 0, NVCVStatus, cvcudaJpegCompressionDistortionCreate, (NVCVOperatorHandle * handle)) +{ + return nvcv::ProtectCall( + [handle] + { + if (handle == nullptr) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Pointer to NVCVOperator handle must not be NULL"); + } + + // Ownership of the operator is transferred to the C handle and released by + // cvcudaOperatorDestroy; this matches every other operator's Create entry point. + *handle = reinterpret_cast(new priv::JpegCompressionDistortion()); // NOSONAR + }); +} + +CVCUDA_DEFINE_API(0, 0, NVCVStatus, cvcudaJpegCompressionDistortionSubmit, + (NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle in, NVCVTensorHandle out, + NVCVTensorHandle quality)) +{ + CVCUDA_NVTX_RANGE("cvcudaJpegCompressionDistortionSubmit"); + return nvcv::ProtectCall( + [handle, stream, in, out, quality] + { + nvcv::TensorWrapHandle input(in); + nvcv::TensorWrapHandle output(out); + nvcv::TensorWrapHandle qualityTensor(quality); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), + qualityTensor.resource()); + }); +} + +CVCUDA_DEFINE_API(0, 0, NVCVStatus, cvcudaJpegCompressionDistortionScalarSubmit, + (NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle in, NVCVTensorHandle out, + int32_t quality)) +{ + CVCUDA_NVTX_RANGE("cvcudaJpegCompressionDistortionScalarSubmit"); + return nvcv::ProtectCall( + [handle, stream, in, out, quality] + { + nvcv::TensorWrapHandle input(in); + nvcv::TensorWrapHandle output(out); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), + quality); + }); +} + +CVCUDA_DEFINE_API(0, 0, NVCVStatus, cvcudaJpegCompressionDistortionVarShapeSubmit, + (NVCVOperatorHandle handle, cudaStream_t stream, NVCVImageBatchHandle in, NVCVImageBatchHandle out, + NVCVTensorHandle quality)) +{ + CVCUDA_NVTX_RANGE("cvcudaJpegCompressionDistortionVarShapeSubmit"); + return nvcv::ProtectCall( + [handle, stream, in, out, quality] + { + nvcv::ImageBatchVarShapeWrapHandle input(in); + nvcv::ImageBatchVarShapeWrapHandle output(out); + nvcv::TensorWrapHandle qualityTensor(quality); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), + qualityTensor.resource()); + }); +} + +CVCUDA_DEFINE_API(0, 0, NVCVStatus, cvcudaJpegCompressionDistortionVarShapeScalarSubmit, + (NVCVOperatorHandle handle, cudaStream_t stream, NVCVImageBatchHandle in, NVCVImageBatchHandle out, + int32_t quality)) +{ + CVCUDA_NVTX_RANGE("cvcudaJpegCompressionDistortionVarShapeScalarSubmit"); + return nvcv::ProtectCall( + [handle, stream, in, out, quality] + { + nvcv::ImageBatchVarShapeWrapHandle input(in); + nvcv::ImageBatchVarShapeWrapHandle output(out); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), + quality); + }); +} diff --git a/src/cvcuda/OpLabel.cpp b/src/cvcuda/OpLabel.cpp index d2a529f42..a70ee76e8 100644 --- a/src/cvcuda/OpLabel.cpp +++ b/src/cvcuda/OpLabel.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "priv/OpLabel.hpp" +#include "priv/Nvtx.hpp" #include "priv/SymbolVersioning.hpp" #include @@ -26,7 +27,7 @@ CVCUDA_DEFINE_API(0, 5, NVCVStatus, cvcudaLabelCreate, (NVCVOperatorHandle * handle)) { return nvcv::ProtectCall( - [&] + [&handle] { if (handle == nullptr) { @@ -34,7 +35,7 @@ CVCUDA_DEFINE_API(0, 5, NVCVStatus, cvcudaLabelCreate, (NVCVOperatorHandle * han "Pointer to NVCVOperator handle must not be NULL"); } - *handle = reinterpret_cast(new cvcuda::priv::Label()); + *handle = cvcuda::priv::CreateOperatorHandle(); }); } @@ -44,13 +45,16 @@ CVCUDA_DEFINE_API(0, 7, NVCVStatus, cvcudaLabelSubmit, NVCVTensorHandle minSize, NVCVTensorHandle count, NVCVTensorHandle stats, NVCVTensorHandle mask, NVCVConnectivityType connectivity, NVCVLabelType assignLabels, NVCVLabelMaskType maskType)) { + CVCUDA_NVTX_RANGE("cvcudaLabelSubmit"); return nvcv::ProtectCall( - [&] + [&handle, &stream, &in, &out, &bgLabel, &minThresh, &maxThresh, &minSize, &count, &stats, &mask, &connectivity, + &assignLabels, &maskType] { cvcuda::priv::ToDynamicRef(handle)( - stream, nvcv::TensorWrapHandle{in}, nvcv::TensorWrapHandle{out}, nvcv::TensorWrapHandle{bgLabel}, - nvcv::TensorWrapHandle{minThresh}, nvcv::TensorWrapHandle{maxThresh}, nvcv::TensorWrapHandle{minSize}, - nvcv::TensorWrapHandle{count}, nvcv::TensorWrapHandle{stats}, nvcv::TensorWrapHandle{mask}, - connectivity, assignLabels, maskType); + stream, nvcv::TensorWrapHandle{in}.resource(), nvcv::TensorWrapHandle{out}.resource(), + nvcv::TensorWrapHandle{bgLabel}.resource(), nvcv::TensorWrapHandle{minThresh}.resource(), + nvcv::TensorWrapHandle{maxThresh}.resource(), nvcv::TensorWrapHandle{minSize}.resource(), + nvcv::TensorWrapHandle{count}.resource(), nvcv::TensorWrapHandle{stats}.resource(), + nvcv::TensorWrapHandle{mask}.resource(), connectivity, assignLabels, maskType); }); } diff --git a/src/cvcuda/OpLaplacian.cpp b/src/cvcuda/OpLaplacian.cpp index 69895da09..ffddb1d18 100644 --- a/src/cvcuda/OpLaplacian.cpp +++ b/src/cvcuda/OpLaplacian.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "priv/OpLaplacian.hpp" +#include "priv/Nvtx.hpp" #include "priv/SymbolVersioning.hpp" #include @@ -29,7 +30,7 @@ namespace priv = cvcuda::priv; CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaLaplacianCreate, (NVCVOperatorHandle * handle)) { return nvcv::ProtectCall( - [&] + [&handle] { if (handle == nullptr) { @@ -37,7 +38,7 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaLaplacianCreate, (NVCVOperatorHandle * "Pointer to NVCVOperator handle must not be NULL"); } - *handle = reinterpret_cast(new priv::Laplacian()); + *handle = priv::CreateOperatorHandle(); }); } @@ -45,11 +46,14 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaLaplacianSubmit, (NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle in, NVCVTensorHandle out, int32_t ksize, float scale, NVCVBorderType borderMode)) { + CVCUDA_NVTX_RANGE("cvcudaLaplacianSubmit"); return nvcv::ProtectCall( - [&] + [&out, &in, &handle, &stream, &ksize, &scale, &borderMode] { - nvcv::TensorWrapHandle output(out), input(in); - priv::ToDynamicRef(handle)(stream, input, output, ksize, scale, borderMode); + nvcv::TensorWrapHandle output(out); + nvcv::TensorWrapHandle input(in); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), ksize, scale, + borderMode); }); } @@ -57,11 +61,15 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaLaplacianVarShapeSubmit, (NVCVOperatorHandle handle, cudaStream_t stream, NVCVImageBatchHandle in, NVCVImageBatchHandle out, NVCVTensorHandle ksize, NVCVTensorHandle scale, NVCVBorderType borderMode)) { + CVCUDA_NVTX_RANGE("cvcudaLaplacianVarShapeSubmit"); return nvcv::ProtectCall( - [&] + [&in, &out, &ksize, &scale, &handle, &stream, &borderMode] { - nvcv::ImageBatchVarShapeWrapHandle inWrap(in), outWrap(out); - nvcv::TensorWrapHandle ksizeWrap(ksize), scaleWrap(scale); - priv::ToDynamicRef(handle)(stream, inWrap, outWrap, ksizeWrap, scaleWrap, borderMode); + nvcv::ImageBatchVarShapeWrapHandle inWrap(in); + nvcv::ImageBatchVarShapeWrapHandle outWrap(out); + nvcv::TensorWrapHandle ksizeWrap(ksize); + nvcv::TensorWrapHandle scaleWrap(scale); + priv::ToDynamicRef(handle)(stream, inWrap.resource(), outWrap.resource(), + ksizeWrap.resource(), scaleWrap.resource(), borderMode); }); } diff --git a/src/cvcuda/OpMedianBlur.cpp b/src/cvcuda/OpMedianBlur.cpp index 728541c6a..a4273d50b 100644 --- a/src/cvcuda/OpMedianBlur.cpp +++ b/src/cvcuda/OpMedianBlur.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "priv/OpMedianBlur.hpp" +#include "priv/Nvtx.hpp" #include "priv/SymbolVersioning.hpp" #include @@ -30,7 +31,7 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaMedianBlurCreate, (NVCVOperatorHandle * handle, const int32_t maxVarShapeBatchSize)) { return nvcv::ProtectCall( - [&] + [&handle, &maxVarShapeBatchSize] { if (handle == nullptr) { @@ -38,7 +39,7 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaMedianBlurCreate, "Pointer to NVCVOperator handle must not be NULL"); } - *handle = reinterpret_cast(new priv::MedianBlur(maxVarShapeBatchSize)); + *handle = priv::CreateOperatorHandle(maxVarShapeBatchSize); }); } @@ -46,11 +47,13 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaMedianBlurSubmit, (NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle in, NVCVTensorHandle out, const int32_t kernelWidth, const int32_t kernelHeight)) { + CVCUDA_NVTX_RANGE("cvcudaMedianBlurSubmit"); return nvcv::ProtectCall( - [&] + [&in, &out, &handle, &stream, &kernelWidth, &kernelHeight] { - nvcv::TensorWrapHandle input(in), output(out); - priv::ToDynamicRef(handle)(stream, input, output, + nvcv::TensorWrapHandle input(in); + nvcv::TensorWrapHandle output(out); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), nvcv::Size2D{kernelWidth, kernelHeight}); }); } @@ -59,11 +62,14 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaMedianBlurVarShapeSubmit, (NVCVOperatorHandle handle, cudaStream_t stream, NVCVImageBatchHandle in, NVCVImageBatchHandle out, NVCVTensorHandle ksize)) { + CVCUDA_NVTX_RANGE("cvcudaMedianBlurVarShapeSubmit"); return nvcv::ProtectCall( - [&] + [&in, &out, &ksize, &handle, &stream] { - nvcv::ImageBatchVarShapeWrapHandle input(in), output(out); + nvcv::ImageBatchVarShapeWrapHandle input(in); + nvcv::ImageBatchVarShapeWrapHandle output(out); nvcv::TensorWrapHandle ksizeWrap(ksize); - priv::ToDynamicRef(handle)(stream, input, output, ksizeWrap); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), + ksizeWrap.resource()); }); } diff --git a/src/cvcuda/OpMinAreaRect.cpp b/src/cvcuda/OpMinAreaRect.cpp index e274dfca7..671d956c0 100644 --- a/src/cvcuda/OpMinAreaRect.cpp +++ b/src/cvcuda/OpMinAreaRect.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "priv/OpMinAreaRect.hpp" +#include "priv/Nvtx.hpp" #include "priv/SymbolVersioning.hpp" #include @@ -28,7 +29,7 @@ namespace priv = cvcuda::priv; CVCUDA_DEFINE_API(0, 4, NVCVStatus, cvcudaMinAreaRectCreate, (NVCVOperatorHandle * handle, int maxContourNum)) { return nvcv::ProtectCall( - [&] + [&handle, &maxContourNum] { if (handle == nullptr) { @@ -36,7 +37,7 @@ CVCUDA_DEFINE_API(0, 4, NVCVStatus, cvcudaMinAreaRectCreate, (NVCVOperatorHandle "Pointer to NVCVOperator handle must not be NULL"); } - *handle = reinterpret_cast(new priv::MinAreaRect(maxContourNum)); + *handle = priv::CreateOperatorHandle(maxContourNum); }); } @@ -44,10 +45,14 @@ CVCUDA_DEFINE_API(0, 4, NVCVStatus, cvcudaMinAreaRectSubmit, (NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle in, NVCVTensorHandle out, NVCVTensorHandle numPointsInContour, const int totalContours)) { + CVCUDA_NVTX_RANGE("cvcudaMinAreaRectSubmit"); return nvcv::ProtectCall( - [&] + [&in, &out, &numPointsInContour, &handle, &stream, &totalContours] { - nvcv::TensorWrapHandle input(in), output(out), _numPointsInContour(numPointsInContour); - priv::ToDynamicRef(handle)(stream, input, output, _numPointsInContour, totalContours); + nvcv::TensorWrapHandle input(in); + nvcv::TensorWrapHandle output(out); + nvcv::TensorWrapHandle _numPointsInContour(numPointsInContour); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), + _numPointsInContour.resource(), totalContours); }); } diff --git a/src/cvcuda/OpMinMaxLoc.cpp b/src/cvcuda/OpMinMaxLoc.cpp index 449d7d08d..80dab0294 100644 --- a/src/cvcuda/OpMinMaxLoc.cpp +++ b/src/cvcuda/OpMinMaxLoc.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "priv/OpMinMaxLoc.hpp" +#include "priv/Nvtx.hpp" #include "priv/SymbolVersioning.hpp" #include @@ -28,7 +29,7 @@ namespace priv = cvcuda::priv; CVCUDA_DEFINE_API(0, 4, NVCVStatus, cvcudaMinMaxLocCreate, (NVCVOperatorHandle * handle)) { return nvcv::ProtectCall( - [&] + [&handle] { if (handle == nullptr) { @@ -36,7 +37,7 @@ CVCUDA_DEFINE_API(0, 4, NVCVStatus, cvcudaMinMaxLocCreate, (NVCVOperatorHandle * "Pointer to NVCVOperator handle must not be NULL"); } - *handle = reinterpret_cast(new priv::MinMaxLoc()); + *handle = priv::CreateOperatorHandle(); }); } @@ -45,15 +46,17 @@ CVCUDA_DEFINE_API(0, 4, NVCVStatus, cvcudaMinMaxLocSubmit, NVCVTensorHandle minLoc, NVCVTensorHandle numMin, NVCVTensorHandle maxVal, NVCVTensorHandle maxLoc, NVCVTensorHandle numMax)) { + CVCUDA_NVTX_RANGE("cvcudaMinMaxLocSubmit"); return nvcv::ProtectCall( - [&] + [&in, &handle, &stream, &minVal, &minLoc, &numMin, &maxVal, &maxLoc, &numMax] { nvcv::TensorWrapHandle input(in); - priv::ToDynamicRef(handle)(stream, input, nvcv::TensorWrapHandle{minVal}, - nvcv::TensorWrapHandle{minLoc}, nvcv::TensorWrapHandle{numMin}, - nvcv::TensorWrapHandle{maxVal}, nvcv::TensorWrapHandle{maxLoc}, - nvcv::TensorWrapHandle{numMax}); + priv::ToDynamicRef(handle)( + stream, input.resource(), nvcv::TensorWrapHandle{minVal}.resource(), + nvcv::TensorWrapHandle{minLoc}.resource(), nvcv::TensorWrapHandle{numMin}.resource(), + nvcv::TensorWrapHandle{maxVal}.resource(), nvcv::TensorWrapHandle{maxLoc}.resource(), + nvcv::TensorWrapHandle{numMax}.resource()); }); } @@ -62,14 +65,16 @@ CVCUDA_DEFINE_API(0, 4, NVCVStatus, cvcudaMinMaxLocVarShapeSubmit, NVCVTensorHandle minLoc, NVCVTensorHandle numMin, NVCVTensorHandle maxVal, NVCVTensorHandle maxLoc, NVCVTensorHandle numMax)) { + CVCUDA_NVTX_RANGE("cvcudaMinMaxLocVarShapeSubmit"); return nvcv::ProtectCall( - [&] + [&in, &handle, &stream, &minVal, &minLoc, &numMin, &maxVal, &maxLoc, &numMax] { nvcv::ImageBatchVarShapeWrapHandle input(in); - priv::ToDynamicRef(handle)(stream, input, nvcv::TensorWrapHandle{minVal}, - nvcv::TensorWrapHandle{minLoc}, nvcv::TensorWrapHandle{numMin}, - nvcv::TensorWrapHandle{maxVal}, nvcv::TensorWrapHandle{maxLoc}, - nvcv::TensorWrapHandle{numMax}); + priv::ToDynamicRef(handle)( + stream, input.resource(), nvcv::TensorWrapHandle{minVal}.resource(), + nvcv::TensorWrapHandle{minLoc}.resource(), nvcv::TensorWrapHandle{numMin}.resource(), + nvcv::TensorWrapHandle{maxVal}.resource(), nvcv::TensorWrapHandle{maxLoc}.resource(), + nvcv::TensorWrapHandle{numMax}.resource()); }); } diff --git a/src/cvcuda/OpMorphology.cpp b/src/cvcuda/OpMorphology.cpp index f3fb6e5d6..694b1c42a 100644 --- a/src/cvcuda/OpMorphology.cpp +++ b/src/cvcuda/OpMorphology.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "priv/OpMorphology.hpp" +#include "priv/Nvtx.hpp" #include "priv/SymbolVersioning.hpp" #include @@ -29,7 +30,7 @@ namespace priv = cvcuda::priv; CVCUDA_DEFINE_API(0, 4, NVCVStatus, cvcudaMorphologyCreate, (NVCVOperatorHandle * handle)) { return nvcv::ProtectCall( - [&] + [&handle] { if (handle == nullptr) { @@ -37,7 +38,7 @@ CVCUDA_DEFINE_API(0, 4, NVCVStatus, cvcudaMorphologyCreate, (NVCVOperatorHandle "Pointer to NVCVOperator handle must not be NULL"); } - *handle = reinterpret_cast(new priv::Morphology()); + *handle = priv::CreateOperatorHandle(); }); } @@ -46,13 +47,16 @@ CVCUDA_DEFINE_API(0, 4, NVCVStatus, cvcudaMorphologySubmit, NVCVTensorHandle workspace, NVCVMorphologyType morphType, int32_t maskWidth, int32_t maskHeight, int32_t anchorX, int32_t anchorY, int32_t iteration, const NVCVBorderType borderMode)) { + CVCUDA_NVTX_RANGE("cvcudaMorphologySubmit"); return nvcv::ProtectCall( - [&] + [&in, &out, &maskWidth, &maskHeight, &anchorX, &anchorY, &handle, &stream, &workspace, &morphType, &iteration, + &borderMode] { - nvcv::TensorWrapHandle input(in), output(out); + nvcv::TensorWrapHandle input(in); + nvcv::TensorWrapHandle output(out); nvcv::Size2D maskSize = {maskWidth, maskHeight}; int2 anchor = {anchorX, anchorY}; - priv::ToDynamicRef(handle)(stream, input, output, + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), NVCV_TENSOR_HANDLE_TO_OPTIONAL(workspace), morphType, maskSize, anchor, iteration, borderMode); }); @@ -63,13 +67,16 @@ CVCUDA_DEFINE_API(0, 4, NVCVStatus, cvcudaMorphologyVarShapeSubmit, NVCVImageBatchHandle workspace, NVCVMorphologyType morphType, NVCVTensorHandle masks, NVCVTensorHandle anchors, int32_t iteration, const NVCVBorderType borderMode)) { + CVCUDA_NVTX_RANGE("cvcudaMorphologyVarShapeSubmit"); return nvcv::ProtectCall( - [&] + [&in, &out, &masks, &anchors, &handle, &stream, &workspace, &morphType, &iteration, &borderMode] { - nvcv::ImageBatchVarShapeWrapHandle input(in), output(out); - nvcv::TensorWrapHandle masksWrap(masks), anchorsWrap(anchors); - priv::ToDynamicRef(handle)(stream, input, output, - NVCV_IMAGE_BATCH_VAR_SHAPE_HANDLE_TO_OPTIONAL(workspace), - morphType, masksWrap, anchorsWrap, iteration, borderMode); + nvcv::ImageBatchVarShapeWrapHandle input(in); + nvcv::ImageBatchVarShapeWrapHandle output(out); + nvcv::TensorWrapHandle masksWrap(masks); + nvcv::TensorWrapHandle anchorsWrap(anchors); + priv::ToDynamicRef(handle)( + stream, input.resource(), output.resource(), NVCV_IMAGE_BATCH_VAR_SHAPE_HANDLE_TO_OPTIONAL(workspace), + morphType, masksWrap.resource(), anchorsWrap.resource(), iteration, borderMode); }); } diff --git a/src/cvcuda/OpNonMaximumSuppression.cpp b/src/cvcuda/OpNonMaximumSuppression.cpp index c94793efc..38457cb24 100644 --- a/src/cvcuda/OpNonMaximumSuppression.cpp +++ b/src/cvcuda/OpNonMaximumSuppression.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "priv/OpNonMaximumSuppression.hpp" +#include "priv/Nvtx.hpp" #include "priv/SymbolVersioning.hpp" #include @@ -28,7 +29,7 @@ namespace priv = cvcuda::priv; CVCUDA_DEFINE_API(0, 3, NVCVStatus, cvcudaNonMaximumSuppressionCreate, (NVCVOperatorHandle * handle)) { return nvcv::ProtectCall( - [&] + [&handle] { if (handle == nullptr) { @@ -36,7 +37,7 @@ CVCUDA_DEFINE_API(0, 3, NVCVStatus, cvcudaNonMaximumSuppressionCreate, (NVCVOper "Pointer to NVCVOperator handle must not be NULL"); } - *handle = reinterpret_cast(new priv::NonMaximumSuppression()); + *handle = priv::CreateOperatorHandle(); }); } @@ -44,11 +45,14 @@ CVCUDA_DEFINE_API(0, 3, NVCVStatus, cvcudaNonMaximumSuppressionSubmit, (NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle in, NVCVTensorHandle out, NVCVTensorHandle scores, float scoreThreshold, float iouThreshold)) { + CVCUDA_NVTX_RANGE("cvcudaNonMaximumSuppressionSubmit"); return nvcv::ProtectCall( - [&] + [&in, &out, &scores, &handle, &stream, &scoreThreshold, &iouThreshold] { - nvcv::TensorWrapHandle _in(in), _out(out), _scores(scores); - priv::ToDynamicRef(handle)(stream, _in, _out, _scores, scoreThreshold, - iouThreshold); + nvcv::TensorWrapHandle _in(in); + nvcv::TensorWrapHandle _out(out); + nvcv::TensorWrapHandle _scores(scores); + priv::ToDynamicRef(handle)(stream, _in.resource(), _out.resource(), + _scores.resource(), scoreThreshold, iouThreshold); }); } diff --git a/src/cvcuda/OpNormalize.cpp b/src/cvcuda/OpNormalize.cpp index 1a45efa43..00ce2e193 100644 --- a/src/cvcuda/OpNormalize.cpp +++ b/src/cvcuda/OpNormalize.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "priv/OpNormalize.hpp" +#include "priv/Nvtx.hpp" #include "priv/SymbolVersioning.hpp" #include @@ -29,7 +30,7 @@ namespace priv = cvcuda::priv; CVCUDA_DEFINE_API(0, 0, NVCVStatus, cvcudaNormalizeCreate, (NVCVOperatorHandle * handle)) { return nvcv::ProtectCall( - [&] + [&handle] { if (handle == nullptr) { @@ -37,7 +38,7 @@ CVCUDA_DEFINE_API(0, 0, NVCVStatus, cvcudaNormalizeCreate, (NVCVOperatorHandle * "Pointer to NVCVOperator handle must not be NULL"); } - *handle = reinterpret_cast(new priv::Normalize()); + *handle = priv::CreateOperatorHandle(); }); } @@ -46,12 +47,35 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaNormalizeSubmit, NVCVTensorHandle scale, NVCVTensorHandle out, float global_scale, float shift, float epsilon, uint32_t flags)) { + CVCUDA_NVTX_RANGE("cvcudaNormalizeSubmit"); return nvcv::ProtectCall( - [&] + [&in, &base, &scale, &out, &handle, &stream, &global_scale, &shift, &epsilon, &flags] { - nvcv::TensorWrapHandle inWrap(in), baseWrap(base), scaleWrap(scale), outWrap(out); - priv::ToDynamicRef(handle)(stream, inWrap, baseWrap, scaleWrap, outWrap, global_scale, - shift, epsilon, flags); + nvcv::TensorWrapHandle inWrap(in); + nvcv::TensorWrapHandle baseWrap(base); + nvcv::TensorWrapHandle scaleWrap(scale); + nvcv::TensorWrapHandle outWrap(out); + priv::ToDynamicRef(handle)(stream, inWrap.resource(), baseWrap.resource(), + scaleWrap.resource(), outWrap.resource(), global_scale, shift, + epsilon, flags); + }); +} + +CVCUDA_DEFINE_API(0, 17, NVCVStatus, cvcudaNormalizeScalarSubmit, + (NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle in, float4 base, float4 scale, + int32_t baseChannels, int32_t scaleChannels, NVCVTensorHandle out, float global_scale, float shift, + float epsilon, uint32_t flags)) +{ + CVCUDA_NVTX_RANGE("cvcudaNormalizeScalarSubmit"); + return nvcv::ProtectCall( + [&in, &base, &scale, &baseChannels, &scaleChannels, &out, &handle, &stream, &global_scale, &shift, &epsilon, + &flags] + { + nvcv::TensorWrapHandle inWrap(in); + nvcv::TensorWrapHandle outWrap(out); + priv::ToDynamicRef(handle)(stream, inWrap.resource(), base, scale, baseChannels, + scaleChannels, outWrap.resource(), global_scale, shift, epsilon, + flags); }); } @@ -60,12 +84,16 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaNormalizeVarShapeSubmit, NVCVTensorHandle scale, NVCVImageBatchHandle out, float global_scale, float shift, float epsilon, uint32_t flags)) { + CVCUDA_NVTX_RANGE("cvcudaNormalizeVarShapeSubmit"); return nvcv::ProtectCall( - [&] + [&base, &scale, &in, &out, &handle, &stream, &global_scale, &shift, &epsilon, &flags] { - nvcv::TensorWrapHandle baseWrap(base), scaleWrap(scale); - nvcv::ImageBatchVarShapeWrapHandle inWrap(in), outWrap(out); - priv::ToDynamicRef(handle)(stream, inWrap, baseWrap, scaleWrap, outWrap, global_scale, - shift, epsilon, flags); + nvcv::TensorWrapHandle baseWrap(base); + nvcv::TensorWrapHandle scaleWrap(scale); + nvcv::ImageBatchVarShapeWrapHandle inWrap(in); + nvcv::ImageBatchVarShapeWrapHandle outWrap(out); + priv::ToDynamicRef(handle)(stream, inWrap.resource(), baseWrap.resource(), + scaleWrap.resource(), outWrap.resource(), global_scale, shift, + epsilon, flags); }); } diff --git a/src/cvcuda/OpOSD.cpp b/src/cvcuda/OpOSD.cpp index b779bd33a..456144d3e 100644 --- a/src/cvcuda/OpOSD.cpp +++ b/src/cvcuda/OpOSD.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "priv/OpOSD.hpp" +#include "priv/Nvtx.hpp" #include "priv/SymbolVersioning.hpp" #include @@ -28,7 +29,7 @@ namespace priv = cvcuda::priv; CVCUDA_DEFINE_API(0, 3, NVCVStatus, cvcudaOSDCreate, (NVCVOperatorHandle * handle)) { return nvcv::ProtectCall( - [&] + [&handle] { if (handle == nullptr) { @@ -36,7 +37,7 @@ CVCUDA_DEFINE_API(0, 3, NVCVStatus, cvcudaOSDCreate, (NVCVOperatorHandle * handl "Pointer to NVCVOperator handle must not be NULL"); } - *handle = reinterpret_cast(new priv::OSD()); + *handle = priv::CreateOperatorHandle(); }); } @@ -45,10 +46,12 @@ CVCUDA_DEFINE_API(0, 3, NVCVStatus, cvcudaOSDSubmit, const NVCVElements elements)) { + CVCUDA_NVTX_RANGE("cvcudaOSDSubmit"); return nvcv::ProtectCall( - [&] + [&in, &out, &handle, &stream, &elements] { - nvcv::TensorWrapHandle input(in), output(out); - priv::ToDynamicRef(handle)(stream, input, output, elements); + nvcv::TensorWrapHandle input(in); + nvcv::TensorWrapHandle output(out); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), elements); }); } diff --git a/src/cvcuda/OpPadAndStack.cpp b/src/cvcuda/OpPadAndStack.cpp index bcd9ecd75..77afe0524 100644 --- a/src/cvcuda/OpPadAndStack.cpp +++ b/src/cvcuda/OpPadAndStack.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "priv/OpPadAndStack.hpp" +#include "priv/Nvtx.hpp" #include "priv/SymbolVersioning.hpp" #include @@ -29,7 +30,7 @@ namespace priv = cvcuda::priv; CVCUDA_DEFINE_API(0, 0, NVCVStatus, cvcudaPadAndStackCreate, (NVCVOperatorHandle * handle)) { return nvcv::ProtectCall( - [&] + [&handle] { if (handle == nullptr) { @@ -37,7 +38,7 @@ CVCUDA_DEFINE_API(0, 0, NVCVStatus, cvcudaPadAndStackCreate, (NVCVOperatorHandle "Pointer to NVCVOperator handle must not be NULL"); } - *handle = reinterpret_cast(new priv::PadAndStack()); + *handle = priv::CreateOperatorHandle(); }); } @@ -45,12 +46,16 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaPadAndStackSubmit, (NVCVOperatorHandle handle, cudaStream_t stream, NVCVImageBatchHandle in, NVCVTensorHandle out, NVCVTensorHandle top, NVCVTensorHandle left, NVCVBorderType borderMode, float borderValue)) { + CVCUDA_NVTX_RANGE("cvcudaPadAndStackSubmit"); return nvcv::ProtectCall( - [&] + [&in, &out, &top, &left, &handle, &stream, &borderMode, &borderValue] { nvcv::ImageBatchVarShapeWrapHandle input(in); - nvcv::TensorWrapHandle output(out), topWrap(top), leftWrap(left); - priv::ToDynamicRef(handle)(stream, input, output, topWrap, leftWrap, borderMode, + nvcv::TensorWrapHandle output(out); + nvcv::TensorWrapHandle topWrap(top); + nvcv::TensorWrapHandle leftWrap(left); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), + topWrap.resource(), leftWrap.resource(), borderMode, borderValue); }); } diff --git a/src/cvcuda/OpPairwiseMatcher.cpp b/src/cvcuda/OpPairwiseMatcher.cpp index 53434d0cc..cc7345405 100644 --- a/src/cvcuda/OpPairwiseMatcher.cpp +++ b/src/cvcuda/OpPairwiseMatcher.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "priv/OpPairwiseMatcher.hpp" +#include "priv/Nvtx.hpp" #include "priv/SymbolVersioning.hpp" #include @@ -29,7 +30,7 @@ CVCUDA_DEFINE_API(0, 5, NVCVStatus, cvcudaPairwiseMatcherCreate, (NVCVOperatorHandle * handle, NVCVPairwiseMatcherType algoChoice)) { return nvcv::ProtectCall( - [&] + [&handle, &algoChoice] { if (handle == nullptr) { @@ -37,7 +38,7 @@ CVCUDA_DEFINE_API(0, 5, NVCVStatus, cvcudaPairwiseMatcherCreate, "Pointer to NVCVOperator handle must not be NULL"); } - *handle = reinterpret_cast(new cvcuda::priv::PairwiseMatcher(algoChoice)); + *handle = priv::CreateOperatorHandle(algoChoice); }); } @@ -47,12 +48,15 @@ CVCUDA_DEFINE_API(0, 5, NVCVStatus, cvcudaPairwiseMatcherSubmit, NVCVTensorHandle numMatches, NVCVTensorHandle distances, bool crossCheck, int matchesPerPoint, NVCVNormType normType)) { + CVCUDA_NVTX_RANGE("cvcudaPairwiseMatcherSubmit"); return nvcv::ProtectCall( - [&] + [&handle, &stream, &set1, &set2, &numSet1, &numSet2, &matches, &numMatches, &distances, &crossCheck, + &matchesPerPoint, &normType] { cvcuda::priv::ToDynamicRef(handle)( - stream, nvcv::TensorWrapHandle{set1}, nvcv::TensorWrapHandle{set2}, nvcv::TensorWrapHandle{numSet1}, - nvcv::TensorWrapHandle{numSet2}, nvcv::TensorWrapHandle{matches}, nvcv::TensorWrapHandle{numMatches}, - nvcv::TensorWrapHandle{distances}, crossCheck, matchesPerPoint, normType); + stream, nvcv::TensorWrapHandle{set1}.resource(), nvcv::TensorWrapHandle{set2}.resource(), + nvcv::TensorWrapHandle{numSet1}.resource(), nvcv::TensorWrapHandle{numSet2}.resource(), + nvcv::TensorWrapHandle{matches}.resource(), nvcv::TensorWrapHandle{numMatches}.resource(), + nvcv::TensorWrapHandle{distances}.resource(), crossCheck, matchesPerPoint, normType); }); } diff --git a/src/cvcuda/OpPillowResize.cpp b/src/cvcuda/OpPillowResize.cpp index b9f5fadab..16cd95de4 100644 --- a/src/cvcuda/OpPillowResize.cpp +++ b/src/cvcuda/OpPillowResize.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "cvcuda/OpPillowResize.h" +#include "priv/Nvtx.hpp" #include "priv/OpPillowResize.hpp" #include "priv/SymbolVersioning.hpp" @@ -27,17 +28,36 @@ namespace priv = cvcuda::priv; +namespace { + +bool isPillowResizeInterpolationValid(NVCVInterpolationType interpolation) +{ + switch (interpolation) + { + case NVCV_INTERP_LINEAR: + case NVCV_INTERP_CUBIC: + case NVCV_INTERP_LANCZOS: + case NVCV_INTERP_BOX: + case NVCV_INTERP_HAMMING: + return true; + default: + return false; + } +} + +} // namespace + CVCUDA_DEFINE_API(0, 3, NVCVStatus, cvcudaPillowResizeCreate, (NVCVOperatorHandle * handle)) { return nvcv::ProtectCall( - [&] + [&handle] { if (handle == nullptr) { throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Pointer to NVCVOperator handle must not be NULL"); } - *handle = reinterpret_cast(new priv::PillowResize()); + *handle = priv::CreateOperatorHandle(); }); } @@ -49,12 +69,12 @@ CVCUDA_DEFINE_API(0, 3, NVCVStatus, cvcudaPillowResizeGetWorkspaceRequirements, return NVCV_ERROR_INVALID_ARGUMENT; return nvcv::ProtectCall( - [&] + [&maxInWidth, &maxInHeight, &maxOutWidth, &maxOutHeight, &reqOut, &handle, &maxBatchSize, &fmt] { NVCVSize2D maxInSize = {maxInWidth, maxInHeight}; NVCVSize2D maxOutSize = {maxOutWidth, maxOutHeight}; - *reqOut = priv::ToDynamicRef(handle).getWorkspaceRequirements(maxBatchSize, maxInSize, - maxOutSize, fmt); + *reqOut = priv::ToDynamicRef(handle).getWorkspaceRequirements( + maxBatchSize, nvcv::Size2D{maxInSize}, nvcv::Size2D{maxOutSize}, fmt); }); } @@ -62,11 +82,11 @@ CVCUDA_DEFINE_API(0, 3, NVCVStatus, cvcudaPillowResizeVarShapeGetWorkspaceRequir (NVCVOperatorHandle handle, int batchSize, const NVCVSize2D *inputSizes, const NVCVSize2D *outputSizes, NVCVImageFormat fmt, NVCVWorkspaceRequirements *reqOut)) { - if (!reqOut) + if (!inputSizes || !outputSizes || !reqOut) return NVCV_ERROR_INVALID_ARGUMENT; return nvcv::ProtectCall( - [&] + [&reqOut, &handle, &batchSize, &inputSizes, &outputSizes, &fmt] { *reqOut = priv::ToDynamicRef(handle).getWorkspaceRequirements( batchSize, static_cast(inputSizes), @@ -78,11 +98,20 @@ CVCUDA_DEFINE_API(0, 3, NVCVStatus, cvcudaPillowResizeSubmit, (NVCVOperatorHandle handle, cudaStream_t stream, const NVCVWorkspace *ws, NVCVTensorHandle in, NVCVTensorHandle out, const NVCVInterpolationType interpolation)) { + CVCUDA_NVTX_RANGE("cvcudaPillowResizeSubmit"); + if (!isPillowResizeInterpolationValid(interpolation)) + return NVCV_ERROR_INVALID_ARGUMENT; + + if (!ws) + return NVCV_ERROR_INVALID_ARGUMENT; + return nvcv::ProtectCall( - [&] + [&in, &out, &handle, &stream, &ws, &interpolation] { - nvcv::TensorWrapHandle input(in), output(out); - priv::ToDynamicRef(handle)(stream, *ws, input, output, interpolation); + nvcv::TensorWrapHandle input(in); + nvcv::TensorWrapHandle output(out); + priv::ToDynamicRef(handle)(stream, *ws, input.resource(), output.resource(), + interpolation); }); } @@ -90,10 +119,19 @@ CVCUDA_DEFINE_API(0, 3, NVCVStatus, cvcudaPillowResizeVarShapeSubmit, (NVCVOperatorHandle handle, cudaStream_t stream, const NVCVWorkspace *ws, NVCVImageBatchHandle in, NVCVImageBatchHandle out, const NVCVInterpolationType interpolation)) { + CVCUDA_NVTX_RANGE("cvcudaPillowResizeVarShapeSubmit"); + if (!isPillowResizeInterpolationValid(interpolation)) + return NVCV_ERROR_INVALID_ARGUMENT; + + if (!ws) + return NVCV_ERROR_INVALID_ARGUMENT; + return nvcv::ProtectCall( - [&] + [&in, &out, &handle, &stream, &ws, &interpolation] { - nvcv::ImageBatchVarShapeWrapHandle input(in), output(out); - priv::ToDynamicRef(handle)(stream, *ws, input, output, interpolation); + nvcv::ImageBatchVarShapeWrapHandle input(in); + nvcv::ImageBatchVarShapeWrapHandle output(out); + priv::ToDynamicRef(handle)(stream, *ws, input.resource(), output.resource(), + interpolation); }); } diff --git a/src/cvcuda/OpPosterize.cpp b/src/cvcuda/OpPosterize.cpp new file mode 100644 index 000000000..7236f94ed --- /dev/null +++ b/src/cvcuda/OpPosterize.cpp @@ -0,0 +1,72 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "priv/OpPosterize.hpp" + +#include "priv/Nvtx.hpp" +#include "priv/SymbolVersioning.hpp" + +#include +#include +#include + +namespace priv = cvcuda::priv; + +CVCUDA_DEFINE_API(0, 0, NVCVStatus, cvcudaPosterizeCreate, (NVCVOperatorHandle * handle)) +{ + return nvcv::ProtectCall( + [handle] + { + if (handle == nullptr) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Pointer to NVCVOperator handle must not be NULL"); + } + + // Ownership of the operator is transferred to the C handle and released by + // cvcudaOperatorDestroy; this matches every other operator's Create entry point. + *handle = reinterpret_cast(new priv::Posterize()); // NOSONAR + }); +} + +CVCUDA_DEFINE_API(0, 0, NVCVStatus, cvcudaPosterizeSubmit, + (NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle in, NVCVTensorHandle out, + int32_t bits)) +{ + CVCUDA_NVTX_RANGE("cvcudaPosterizeSubmit"); + return nvcv::ProtectCall( + [handle, stream, in, out, bits] + { + nvcv::TensorWrapHandle input(in); + nvcv::TensorWrapHandle output(out); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), bits); + }); +} + +CVCUDA_DEFINE_API(0, 0, NVCVStatus, cvcudaPosterizeVarShapeSubmit, + (NVCVOperatorHandle handle, cudaStream_t stream, NVCVImageBatchHandle in, NVCVImageBatchHandle out, + int32_t bits)) +{ + CVCUDA_NVTX_RANGE("cvcudaPosterizeVarShapeSubmit"); + return nvcv::ProtectCall( + [handle, stream, in, out, bits] + { + nvcv::ImageBatchVarShapeWrapHandle input(in); + nvcv::ImageBatchVarShapeWrapHandle output(out); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), bits); + }); +} diff --git a/src/cvcuda/OpRandomResizedCrop.cpp b/src/cvcuda/OpRandomResizedCrop.cpp index ac9c25581..ca5673ec4 100644 --- a/src/cvcuda/OpRandomResizedCrop.cpp +++ b/src/cvcuda/OpRandomResizedCrop.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "priv/OpRandomResizedCrop.hpp" +#include "priv/Nvtx.hpp" #include "priv/SymbolVersioning.hpp" #include @@ -31,7 +32,7 @@ CVCUDA_DEFINE_API(0, 4, NVCVStatus, cvcudaRandomResizedCropCreate, int32_t maxBatchSize, uint32_t seed)) { return nvcv::ProtectCall( - [&] + [&handle, &minScale, &maxScale, &minRatio, &maxRatio, &maxBatchSize, &seed] { if (handle == nullptr) { @@ -39,8 +40,8 @@ CVCUDA_DEFINE_API(0, 4, NVCVStatus, cvcudaRandomResizedCropCreate, "Pointer to NVCVOperator handle must not be NULL"); } - *handle = reinterpret_cast( - new priv::RandomResizedCrop(minScale, maxScale, minRatio, maxRatio, maxBatchSize, seed)); + *handle = priv::CreateOperatorHandle(minScale, maxScale, minRatio, maxRatio, + maxBatchSize, seed); }); } @@ -48,11 +49,14 @@ CVCUDA_DEFINE_API(0, 4, NVCVStatus, cvcudaRandomResizedCropSubmit, (NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle in, NVCVTensorHandle out, const NVCVInterpolationType interpolation)) { + CVCUDA_NVTX_RANGE("cvcudaRandomResizedCropSubmit"); return nvcv::ProtectCall( - [&] + [&in, &out, &handle, &stream, &interpolation] { - nvcv::TensorWrapHandle input(in), output(out); - priv::ToDynamicRef(handle)(stream, input, output, interpolation); + nvcv::TensorWrapHandle input(in); + nvcv::TensorWrapHandle output(out); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), + interpolation); }); } @@ -60,10 +64,13 @@ CVCUDA_DEFINE_API(0, 4, NVCVStatus, cvcudaRandomResizedCropVarShapeSubmit, (NVCVOperatorHandle handle, cudaStream_t stream, NVCVImageBatchHandle in, NVCVImageBatchHandle out, const NVCVInterpolationType interpolation)) { + CVCUDA_NVTX_RANGE("cvcudaRandomResizedCropVarShapeSubmit"); return nvcv::ProtectCall( - [&] + [&in, &out, &handle, &stream, &interpolation] { - nvcv::ImageBatchVarShapeWrapHandle input(in), output(out); - priv::ToDynamicRef(handle)(stream, input, output, interpolation); + nvcv::ImageBatchVarShapeWrapHandle input(in); + nvcv::ImageBatchVarShapeWrapHandle output(out); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), + interpolation); }); } diff --git a/src/cvcuda/OpReformat.cpp b/src/cvcuda/OpReformat.cpp index be5b6b7af..988d6a48d 100644 --- a/src/cvcuda/OpReformat.cpp +++ b/src/cvcuda/OpReformat.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "priv/OpReformat.hpp" +#include "priv/Nvtx.hpp" #include "priv/SymbolVersioning.hpp" #include @@ -28,7 +29,7 @@ namespace priv = cvcuda::priv; CVCUDA_DEFINE_API(0, 0, NVCVStatus, cvcudaReformatCreate, (NVCVOperatorHandle * handle)) { return nvcv::ProtectCall( - [&] + [&handle] { if (handle == nullptr) { @@ -36,17 +37,19 @@ CVCUDA_DEFINE_API(0, 0, NVCVStatus, cvcudaReformatCreate, (NVCVOperatorHandle * "Pointer to NVCVOperator handle must not be NULL"); } - *handle = reinterpret_cast(new priv::Reformat()); + *handle = priv::CreateOperatorHandle(); }); } CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaReformatSubmit, (NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle in, NVCVTensorHandle out)) { + CVCUDA_NVTX_RANGE("cvcudaReformatSubmit"); return nvcv::ProtectCall( - [&] + [&in, &out, &handle, &stream] { - nvcv::TensorWrapHandle input(in), output(out); - priv::ToDynamicRef(handle)(stream, input, output); + nvcv::TensorWrapHandle input(in); + nvcv::TensorWrapHandle output(out); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource()); }); } diff --git a/src/cvcuda/OpRemap.cpp b/src/cvcuda/OpRemap.cpp index 9681c55b7..b17afe733 100644 --- a/src/cvcuda/OpRemap.cpp +++ b/src/cvcuda/OpRemap.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "priv/OpRemap.hpp" +#include "priv/Nvtx.hpp" #include "priv/SymbolVersioning.hpp" #include @@ -29,7 +30,7 @@ namespace priv = cvcuda::priv; CVCUDA_DEFINE_API(0, 3, NVCVStatus, cvcudaRemapCreate, (NVCVOperatorHandle * handle)) { return nvcv::ProtectCall( - [&] + [&handle] { if (handle == nullptr) { @@ -37,7 +38,7 @@ CVCUDA_DEFINE_API(0, 3, NVCVStatus, cvcudaRemapCreate, (NVCVOperatorHandle * han "Pointer to NVCVOperator handle must not be NULL"); } - *handle = reinterpret_cast(new priv::Remap()); + *handle = priv::CreateOperatorHandle(); }); } @@ -46,12 +47,16 @@ CVCUDA_DEFINE_API(0, 3, NVCVStatus, cvcudaRemapSubmit, NVCVTensorHandle map, NVCVInterpolationType inInterp, NVCVInterpolationType mapInterp, NVCVRemapMapValueType mapValueType, int8_t alignCorners, NVCVBorderType border, float4 borderValue)) { + CVCUDA_NVTX_RANGE("cvcudaRemapSubmit"); return nvcv::ProtectCall( - [&] + [&in, &out, &map, &handle, &stream, &inInterp, &mapInterp, &mapValueType, &alignCorners, &border, &borderValue] { - nvcv::TensorWrapHandle _in(in), _out(out), _map(map); - priv::ToDynamicRef(handle)(stream, _in, _out, _map, inInterp, mapInterp, mapValueType, - static_cast(alignCorners), border, borderValue); + nvcv::TensorWrapHandle _in(in); + nvcv::TensorWrapHandle _out(out); + nvcv::TensorWrapHandle _map(map); + priv::ToDynamicRef(handle)(stream, _in.resource(), _out.resource(), _map.resource(), inInterp, + mapInterp, mapValueType, static_cast(alignCorners), border, + borderValue); }); } @@ -60,12 +65,15 @@ CVCUDA_DEFINE_API(0, 3, NVCVStatus, cvcudaRemapVarShapeSubmit, NVCVTensorHandle map, NVCVInterpolationType inInterp, NVCVInterpolationType mapInterp, NVCVRemapMapValueType mapValueType, int8_t alignCorners, NVCVBorderType border, float4 borderValue)) { + CVCUDA_NVTX_RANGE("cvcudaRemapVarShapeSubmit"); return nvcv::ProtectCall( - [&] + [&in, &out, &map, &handle, &stream, &inInterp, &mapInterp, &mapValueType, &alignCorners, &border, &borderValue] { - nvcv::ImageBatchVarShapeWrapHandle _in(in), _out(out); + nvcv::ImageBatchVarShapeWrapHandle _in(in); + nvcv::ImageBatchVarShapeWrapHandle _out(out); nvcv::TensorWrapHandle _map(map); - priv::ToDynamicRef(handle)(stream, _in, _out, _map, inInterp, mapInterp, mapValueType, - static_cast(alignCorners), border, borderValue); + priv::ToDynamicRef(handle)(stream, _in.resource(), _out.resource(), _map.resource(), inInterp, + mapInterp, mapValueType, static_cast(alignCorners), border, + borderValue); }); } diff --git a/src/cvcuda/OpResize.cpp b/src/cvcuda/OpResize.cpp index 01eeec561..937672674 100644 --- a/src/cvcuda/OpResize.cpp +++ b/src/cvcuda/OpResize.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "priv/OpResize.hpp" +#include "priv/Nvtx.hpp" #include "priv/SymbolVersioning.hpp" #include @@ -29,7 +30,7 @@ namespace priv = cvcuda::priv; CVCUDA_DEFINE_API(0, 0, NVCVStatus, cvcudaResizeCreate, (NVCVOperatorHandle * handle)) { return nvcv::ProtectCall( - [&] + [&handle] { if (handle == nullptr) { @@ -37,7 +38,7 @@ CVCUDA_DEFINE_API(0, 0, NVCVStatus, cvcudaResizeCreate, (NVCVOperatorHandle * ha "Pointer to NVCVOperator handle must not be NULL"); } - *handle = reinterpret_cast(new priv::Resize()); + *handle = priv::CreateOperatorHandle(); }); } @@ -45,11 +46,13 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaResizeSubmit, (NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle in, NVCVTensorHandle out, const NVCVInterpolationType interpolation)) { + CVCUDA_NVTX_RANGE("cvcudaResizeSubmit"); return nvcv::ProtectCall( - [&] + [&in, &out, &handle, &stream, &interpolation] { - nvcv::TensorWrapHandle input(in), output(out); - priv::ToDynamicRef(handle)(stream, input, output, interpolation); + nvcv::TensorWrapHandle input(in); + nvcv::TensorWrapHandle output(out); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), interpolation); }); } @@ -57,10 +60,12 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaResizeVarShapeSubmit, (NVCVOperatorHandle handle, cudaStream_t stream, NVCVImageBatchHandle in, NVCVImageBatchHandle out, const NVCVInterpolationType interpolation)) { + CVCUDA_NVTX_RANGE("cvcudaResizeVarShapeSubmit"); return nvcv::ProtectCall( - [&] + [&in, &out, &handle, &stream, &interpolation] { - nvcv::ImageBatchVarShapeWrapHandle input(in), output(out); - priv::ToDynamicRef(handle)(stream, input, output, interpolation); + nvcv::ImageBatchVarShapeWrapHandle input(in); + nvcv::ImageBatchVarShapeWrapHandle output(out); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), interpolation); }); } diff --git a/src/cvcuda/OpResizeCropConvertReformat.cpp b/src/cvcuda/OpResizeCropConvertReformat.cpp index 97277f6d2..43978781d 100644 --- a/src/cvcuda/OpResizeCropConvertReformat.cpp +++ b/src/cvcuda/OpResizeCropConvertReformat.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "priv/OpResizeCropConvertReformat.hpp" +#include "priv/Nvtx.hpp" #include "priv/SymbolVersioning.hpp" #include @@ -29,7 +30,7 @@ namespace priv = cvcuda::priv; CVCUDA_DEFINE_API(0, 8, NVCVStatus, cvcudaResizeCropConvertReformatCreate, (NVCVOperatorHandle * handle)) { return nvcv::ProtectCall( - [&] + [&handle] { if (handle == nullptr) { @@ -37,7 +38,7 @@ CVCUDA_DEFINE_API(0, 8, NVCVStatus, cvcudaResizeCropConvertReformatCreate, (NVCV "Pointer to NVCVOperator handle must not be NULL"); } - *handle = reinterpret_cast(new priv::ResizeCropConvertReformat()); + *handle = priv::CreateOperatorHandle(); }); } @@ -46,12 +47,15 @@ CVCUDA_DEFINE_API(0, 10, NVCVStatus, cvcudaResizeCropConvertReformatSubmit, const NVCVSize2D resizeDim, const NVCVInterpolationType interpolation, const int2 cropPos, const NVCVChannelManip manip, const float scale, const float offset, const bool srcCast)) { + CVCUDA_NVTX_RANGE("cvcudaResizeCropConvertReformatSubmit"); return nvcv::ProtectCall( - [&] + [&in, &out, &handle, &stream, &resizeDim, &interpolation, &cropPos, &manip, &scale, &offset, &srcCast] { - nvcv::TensorWrapHandle input(in), output(out); - priv::ToDynamicRef(handle)(stream, input, output, resizeDim, interpolation, - cropPos, manip, scale, offset, srcCast); + nvcv::TensorWrapHandle input(in); + nvcv::TensorWrapHandle output(out); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), + resizeDim, interpolation, cropPos, manip, scale, + offset, srcCast); }); } @@ -60,12 +64,14 @@ CVCUDA_DEFINE_API(0, 10, NVCVStatus, cvcudaResizeCropConvertReformatVarShapeSubm const NVCVSize2D resizeDim, const NVCVInterpolationType interpolation, const int2 cropPos, const NVCVChannelManip manip, const float scale, const float offset, const bool srcCast)) { + CVCUDA_NVTX_RANGE("cvcudaResizeCropConvertReformatVarShapeSubmit"); return nvcv::ProtectCall( - [&] + [&in, &out, &handle, &stream, &resizeDim, &interpolation, &cropPos, &manip, &scale, &offset, &srcCast] { nvcv::ImageBatchVarShapeWrapHandle input(in); nvcv::TensorWrapHandle output(out); - priv::ToDynamicRef(handle)(stream, input, output, resizeDim, interpolation, - cropPos, manip, scale, offset, srcCast); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), + resizeDim, interpolation, cropPos, manip, scale, + offset, srcCast); }); } diff --git a/src/cvcuda/OpRotate.cpp b/src/cvcuda/OpRotate.cpp index db642a5c8..ac10c762b 100644 --- a/src/cvcuda/OpRotate.cpp +++ b/src/cvcuda/OpRotate.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "priv/OpRotate.hpp" +#include "priv/Nvtx.hpp" #include "priv/SymbolVersioning.hpp" #include @@ -30,7 +31,7 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaRotateCreate, (NVCVOperatorHandle * handle, const int32_t maxVarShapeBatchSize)) { return nvcv::ProtectCall( - [&] + [&handle, &maxVarShapeBatchSize] { if (handle == nullptr) { @@ -38,7 +39,7 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaRotateCreate, "Pointer to NVCVOperator handle must not be NULL"); } - *handle = reinterpret_cast(new priv::Rotate(maxVarShapeBatchSize)); + *handle = priv::CreateOperatorHandle(maxVarShapeBatchSize); }); } @@ -46,11 +47,14 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaRotateSubmit, (NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle in, NVCVTensorHandle out, const double angleDeg, const double2 shift, const NVCVInterpolationType interpolation)) { + CVCUDA_NVTX_RANGE("cvcudaRotateSubmit"); return nvcv::ProtectCall( - [&] + [&in, &out, &handle, &stream, &angleDeg, &shift, &interpolation] { - nvcv::TensorWrapHandle input(in), output(out); - priv::ToDynamicRef(handle)(stream, input, output, angleDeg, shift, interpolation); + nvcv::TensorWrapHandle input(in); + nvcv::TensorWrapHandle output(out); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), angleDeg, shift, + interpolation); }); } @@ -58,11 +62,15 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaRotateVarShapeSubmit, (NVCVOperatorHandle handle, cudaStream_t stream, NVCVImageBatchHandle in, NVCVImageBatchHandle out, NVCVTensorHandle angleDeg, NVCVTensorHandle shift, const NVCVInterpolationType interpolation)) { + CVCUDA_NVTX_RANGE("cvcudaRotateVarShapeSubmit"); return nvcv::ProtectCall( - [&] + [&in, &out, &angleDeg, &shift, &handle, &stream, &interpolation] { - nvcv::ImageBatchVarShapeWrapHandle input(in), output(out); - nvcv::TensorWrapHandle angleDegWrap(angleDeg), shiftWrap(shift); - priv::ToDynamicRef(handle)(stream, input, output, angleDegWrap, shiftWrap, interpolation); + nvcv::ImageBatchVarShapeWrapHandle input(in); + nvcv::ImageBatchVarShapeWrapHandle output(out); + nvcv::TensorWrapHandle angleDegWrap(angleDeg); + nvcv::TensorWrapHandle shiftWrap(shift); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), + angleDegWrap.resource(), shiftWrap.resource(), interpolation); }); } diff --git a/src/cvcuda/OpSIFT.cpp b/src/cvcuda/OpSIFT.cpp index 29e6605b3..b97aa9a98 100644 --- a/src/cvcuda/OpSIFT.cpp +++ b/src/cvcuda/OpSIFT.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "priv/OpSIFT.hpp" +#include "priv/Nvtx.hpp" #include "priv/SymbolVersioning.hpp" #include @@ -28,7 +29,7 @@ namespace priv = cvcuda::priv; CVCUDA_DEFINE_API(0, 4, NVCVStatus, cvcudaSIFTCreate, (NVCVOperatorHandle * handle, int3 maxShape, int maxOctaveLayers)) { return nvcv::ProtectCall( - [&] + [&handle, &maxShape, &maxOctaveLayers] { if (handle == nullptr) { @@ -36,7 +37,7 @@ CVCUDA_DEFINE_API(0, 4, NVCVStatus, cvcudaSIFTCreate, (NVCVOperatorHandle * hand "Pointer to NVCVOperator handle must not be NULL"); } - *handle = reinterpret_cast(new priv::SIFT(maxShape, maxOctaveLayers)); + *handle = priv::CreateOperatorHandle(maxShape, maxOctaveLayers); }); } @@ -46,13 +47,18 @@ CVCUDA_DEFINE_API(0, 4, NVCVStatus, cvcudaSIFTSubmit, int numOctaveLayers, float contrastThreshold, float edgeThreshold, float initSigma, NVCVSIFTFlagType flags)) { + CVCUDA_NVTX_RANGE("cvcudaSIFTSubmit"); return nvcv::ProtectCall( - [&] + [&in, &featCoords, &featMetadata, &featDescriptors, &numFeatures, &handle, &stream, &numOctaveLayers, + &contrastThreshold, &edgeThreshold, &initSigma, &flags] { - nvcv::TensorWrapHandle _in(in), _featCoords(featCoords), _featMetadata(featMetadata), - _featDescriptors(featDescriptors), _numFeatures(numFeatures); - priv::ToDynamicRef(handle)(stream, _in, _featCoords, _featMetadata, _featDescriptors, - _numFeatures, numOctaveLayers, contrastThreshold, edgeThreshold, - initSigma, flags); + nvcv::TensorWrapHandle _in(in); + nvcv::TensorWrapHandle _featCoords(featCoords); + nvcv::TensorWrapHandle _featMetadata(featMetadata); + nvcv::TensorWrapHandle _featDescriptors(featDescriptors); + nvcv::TensorWrapHandle _numFeatures(numFeatures); + priv::ToDynamicRef(handle)( + stream, _in.resource(), _featCoords.resource(), _featMetadata.resource(), _featDescriptors.resource(), + _numFeatures.resource(), numOctaveLayers, contrastThreshold, edgeThreshold, initSigma, flags); }); } diff --git a/src/cvcuda/OpSolarize.cpp b/src/cvcuda/OpSolarize.cpp new file mode 100644 index 000000000..7402c229f --- /dev/null +++ b/src/cvcuda/OpSolarize.cpp @@ -0,0 +1,72 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "priv/OpSolarize.hpp" + +#include "priv/Nvtx.hpp" +#include "priv/SymbolVersioning.hpp" + +#include +#include +#include + +namespace priv = cvcuda::priv; + +CVCUDA_DEFINE_API(0, 0, NVCVStatus, cvcudaSolarizeCreate, (NVCVOperatorHandle * handle)) +{ + return nvcv::ProtectCall( + [handle] + { + if (handle == nullptr) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Pointer to NVCVOperator handle must not be NULL"); + } + + // Ownership of the operator is transferred to the C handle and released by + // cvcudaOperatorDestroy; this matches every other operator's Create entry point. + *handle = reinterpret_cast(new priv::Solarize()); // NOSONAR + }); +} + +CVCUDA_DEFINE_API(0, 0, NVCVStatus, cvcudaSolarizeSubmit, + (NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle in, NVCVTensorHandle out, + double threshold)) +{ + CVCUDA_NVTX_RANGE("cvcudaSolarizeSubmit"); + return nvcv::ProtectCall( + [handle, stream, in, out, threshold] + { + nvcv::TensorWrapHandle input(in); + nvcv::TensorWrapHandle output(out); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), threshold); + }); +} + +CVCUDA_DEFINE_API(0, 0, NVCVStatus, cvcudaSolarizeVarShapeSubmit, + (NVCVOperatorHandle handle, cudaStream_t stream, NVCVImageBatchHandle in, NVCVImageBatchHandle out, + double threshold)) +{ + CVCUDA_NVTX_RANGE("cvcudaSolarizeVarShapeSubmit"); + return nvcv::ProtectCall( + [handle, stream, in, out, threshold] + { + nvcv::ImageBatchVarShapeWrapHandle input(in); + nvcv::ImageBatchVarShapeWrapHandle output(out); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), threshold); + }); +} diff --git a/src/cvcuda/OpStack.cpp b/src/cvcuda/OpStack.cpp index 8ebf4dd99..f505c4158 100644 --- a/src/cvcuda/OpStack.cpp +++ b/src/cvcuda/OpStack.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,9 +17,11 @@ #include "priv/OpStack.hpp" +#include "priv/Nvtx.hpp" #include "priv/SymbolVersioning.hpp" #include +#include #include #include @@ -28,7 +30,7 @@ namespace priv = cvcuda::priv; CVCUDA_DEFINE_API(0, 5, NVCVStatus, cvcudaStackCreate, (NVCVOperatorHandle * handle)) { return nvcv::ProtectCall( - [&] + [&handle] { if (handle == nullptr) { @@ -36,18 +38,32 @@ CVCUDA_DEFINE_API(0, 5, NVCVStatus, cvcudaStackCreate, (NVCVOperatorHandle * han "Pointer to NVCVOperator handle must not be NULL"); } - *handle = reinterpret_cast(new priv::Stack()); + *handle = priv::CreateOperatorHandle(); }); } CVCUDA_DEFINE_API(0, 5, NVCVStatus, cvcudaStackSubmit, (NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorBatchHandle in, NVCVTensorHandle out)) { + CVCUDA_NVTX_RANGE("cvcudaStackSubmit"); return nvcv::ProtectCall( - [&] + [&out, &in, &handle, &stream] { nvcv::TensorWrapHandle output(out); nvcv::TensorBatchWrapHandle input(in); - priv::ToDynamicRef(handle)(stream, input, output); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource()); + }); +} + +CVCUDA_DEFINE_API(0, 5, NVCVStatus, cvcudaStackVarShapeSubmit, + (NVCVOperatorHandle handle, cudaStream_t stream, NVCVImageBatchHandle in, NVCVTensorHandle out)) +{ + CVCUDA_NVTX_RANGE("cvcudaStackVarShapeSubmit"); + return nvcv::ProtectCall( + [&out, &in, &handle, &stream] + { + nvcv::TensorWrapHandle output(out); + nvcv::ImageBatchVarShapeWrapHandle input(in); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource()); }); } diff --git a/src/cvcuda/OpThreshold.cpp b/src/cvcuda/OpThreshold.cpp index 02fdce76a..b85a89d41 100644 --- a/src/cvcuda/OpThreshold.cpp +++ b/src/cvcuda/OpThreshold.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "priv/OpThreshold.hpp" +#include "priv/Nvtx.hpp" #include "priv/SymbolVersioning.hpp" #include @@ -30,7 +31,7 @@ CVCUDA_DEFINE_API(0, 3, NVCVStatus, cvcudaThresholdCreate, (NVCVOperatorHandle * handle, uint32_t type, int32_t maxBatchSize)) { return nvcv::ProtectCall( - [&] + [&handle, &type, &maxBatchSize] { if (handle == nullptr) { @@ -38,7 +39,7 @@ CVCUDA_DEFINE_API(0, 3, NVCVStatus, cvcudaThresholdCreate, "Pointer to NVCVOperator handle must not be NULL"); } - *handle = reinterpret_cast(new priv::Threshold(type, maxBatchSize)); + *handle = priv::CreateOperatorHandle(type, maxBatchSize); }); } @@ -46,11 +47,16 @@ CVCUDA_DEFINE_API(0, 3, NVCVStatus, cvcudaThresholdSubmit, (NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle in, NVCVTensorHandle out, NVCVTensorHandle thresh, NVCVTensorHandle maxval)) { + CVCUDA_NVTX_RANGE("cvcudaThresholdSubmit"); return nvcv::ProtectCall( - [&] + [&in, &out, &thresh, &maxval, &handle, &stream] { - nvcv::TensorWrapHandle input(in), output(out), threshwrap(thresh), maxvalwrap(maxval); - priv::ToDynamicRef(handle)(stream, input, output, threshwrap, maxvalwrap); + nvcv::TensorWrapHandle input(in); + nvcv::TensorWrapHandle output(out); + nvcv::TensorWrapHandle threshwrap(thresh); + nvcv::TensorWrapHandle maxvalwrap(maxval); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), + threshwrap.resource(), maxvalwrap.resource()); }); } @@ -58,11 +64,15 @@ CVCUDA_DEFINE_API(0, 3, NVCVStatus, cvcudaThresholdVarShapeSubmit, (NVCVOperatorHandle handle, cudaStream_t stream, NVCVImageBatchHandle in, NVCVImageBatchHandle out, NVCVTensorHandle thresh, NVCVTensorHandle maxval)) { + CVCUDA_NVTX_RANGE("cvcudaThresholdVarShapeSubmit"); return nvcv::ProtectCall( - [&] + [&in, &out, &thresh, &maxval, &handle, &stream] { - nvcv::ImageBatchVarShapeWrapHandle input(in), output(out); - nvcv::TensorWrapHandle threshwrap(thresh), maxvalwrap(maxval); - priv::ToDynamicRef(handle)(stream, input, output, threshwrap, maxvalwrap); + nvcv::ImageBatchVarShapeWrapHandle input(in); + nvcv::ImageBatchVarShapeWrapHandle output(out); + nvcv::TensorWrapHandle threshwrap(thresh); + nvcv::TensorWrapHandle maxvalwrap(maxval); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), + threshwrap.resource(), maxvalwrap.resource()); }); } diff --git a/src/cvcuda/OpWarpAffine.cpp b/src/cvcuda/OpWarpAffine.cpp index f8e10d91d..9951361dc 100644 --- a/src/cvcuda/OpWarpAffine.cpp +++ b/src/cvcuda/OpWarpAffine.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "priv/OpWarpAffine.hpp" +#include "priv/Nvtx.hpp" #include "priv/SymbolVersioning.hpp" #include @@ -30,7 +31,7 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaWarpAffineCreate, (NVCVOperatorHandle * handle, const int32_t maxVarShapeBatchSize)) { return nvcv::ProtectCall( - [&] + [&handle, &maxVarShapeBatchSize] { if (handle == nullptr) { @@ -38,7 +39,7 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaWarpAffineCreate, "Pointer to NVCVOperator handle must not be NULL"); } - *handle = reinterpret_cast(new priv::WarpAffine(maxVarShapeBatchSize)); + *handle = priv::CreateOperatorHandle(maxVarShapeBatchSize); }); } @@ -47,11 +48,14 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaWarpAffineSubmit, const NVCVAffineTransform xform, const int32_t flags, const NVCVBorderType borderMode, const float4 borderValue)) { + CVCUDA_NVTX_RANGE("cvcudaWarpAffineSubmit"); return nvcv::ProtectCall( - [&] + [&in, &out, &handle, &stream, &xform, &flags, &borderMode, &borderValue] { - nvcv::TensorWrapHandle input(in), output(out); - priv::ToDynamicRef(handle)(stream, input, output, xform, flags, borderMode, borderValue); + nvcv::TensorWrapHandle input(in); + nvcv::TensorWrapHandle output(out); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), xform, flags, + borderMode, borderValue); }); } @@ -60,12 +64,14 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaWarpAffineVarShapeSubmit, NVCVTensorHandle transMatrix, const int32_t flags, const NVCVBorderType borderMode, const float4 borderValue)) { + CVCUDA_NVTX_RANGE("cvcudaWarpAffineVarShapeSubmit"); return nvcv::ProtectCall( - [&] + [&in, &out, &transMatrix, &handle, &stream, &flags, &borderMode, &borderValue] { - nvcv::ImageBatchVarShapeWrapHandle input(in), output(out); + nvcv::ImageBatchVarShapeWrapHandle input(in); + nvcv::ImageBatchVarShapeWrapHandle output(out); nvcv::TensorWrapHandle transMatrixWrap(transMatrix); - priv::ToDynamicRef(handle)(stream, input, output, transMatrixWrap, flags, borderMode, - borderValue); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), + transMatrixWrap.resource(), flags, borderMode, borderValue); }); } diff --git a/src/cvcuda/OpWarpPerspective.cpp b/src/cvcuda/OpWarpPerspective.cpp index 7de743d53..68d3a6208 100644 --- a/src/cvcuda/OpWarpPerspective.cpp +++ b/src/cvcuda/OpWarpPerspective.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "priv/OpWarpPerspective.hpp" +#include "priv/Nvtx.hpp" #include "priv/SymbolVersioning.hpp" #include @@ -30,7 +31,7 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaWarpPerspectiveCreate, (NVCVOperatorHandle * handle, const int maxVarShapeBatchSize)) { return nvcv::ProtectCall( - [&] + [&handle, &maxVarShapeBatchSize] { if (handle == nullptr) { @@ -38,7 +39,7 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaWarpPerspectiveCreate, "Pointer to NVCVOperator handle must not be NULL"); } - *handle = reinterpret_cast(new priv::WarpPerspective(maxVarShapeBatchSize)); + *handle = priv::CreateOperatorHandle(maxVarShapeBatchSize); }); } @@ -47,12 +48,14 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaWarpPerspectiveSubmit, const NVCVPerspectiveTransform transMatrix, const int flags, const NVCVBorderType borderMode, const float4 borderValue)) { + CVCUDA_NVTX_RANGE("cvcudaWarpPerspectiveSubmit"); return nvcv::ProtectCall( - [&] + [&in, &out, &handle, &stream, &transMatrix, &flags, &borderMode, &borderValue] { - nvcv::TensorWrapHandle input(in), output(out); - priv::ToDynamicRef(handle)(stream, input, output, transMatrix, flags, borderMode, - borderValue); + nvcv::TensorWrapHandle input(in); + nvcv::TensorWrapHandle output(out); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), transMatrix, + flags, borderMode, borderValue); }); } @@ -61,12 +64,15 @@ CVCUDA_DEFINE_API(0, 2, NVCVStatus, cvcudaWarpPerspectiveVarShapeSubmit, NVCVTensorHandle transMatrix, const int flags, const NVCVBorderType borderMode, const float4 borderValue)) { + CVCUDA_NVTX_RANGE("cvcudaWarpPerspectiveVarShapeSubmit"); return nvcv::ProtectCall( - [&] + [&in, &out, &transMatrix, &handle, &stream, &flags, &borderMode, &borderValue] { - nvcv::ImageBatchVarShapeWrapHandle input(in), output(out); + nvcv::ImageBatchVarShapeWrapHandle input(in); + nvcv::ImageBatchVarShapeWrapHandle output(out); nvcv::TensorWrapHandle transMatrixWrap(transMatrix); - priv::ToDynamicRef(handle)(stream, input, output, transMatrixWrap, flags, borderMode, + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource(), + transMatrixWrap.resource(), flags, borderMode, borderValue); }); } diff --git a/src/cvcuda/Operator.cpp b/src/cvcuda/Operator.cpp index 08949b698..2e78b855c 100644 --- a/src/cvcuda/Operator.cpp +++ b/src/cvcuda/Operator.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,5 +25,5 @@ namespace priv = cvcuda::priv; CVCUDA_DEFINE_API(0, 3, void, nvcvOperatorDestroy, (NVCVOperatorHandle handle)) { - nvcv::ProtectCall([&] { delete priv::ToOperatorPtr(handle); }); + nvcv::ProtectCall([&handle] { priv::DestroyOperatorHandle(handle); }); } diff --git a/src/cvcuda/Types.cpp b/src/cvcuda/Types.cpp new file mode 100644 index 000000000..2be39dc8a --- /dev/null +++ b/src/cvcuda/Types.cpp @@ -0,0 +1,264 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "priv/Types.hpp" + +#include "priv/SymbolVersioning.hpp" + +#include +#include +#include + +#include +#include + +namespace priv = cvcuda::priv; + +namespace { + +bool isPODElementType(NVCVOSDType type) +{ + switch (type) + { + case NVCV_OSD_RECT: + case NVCV_OSD_POINT: + case NVCV_OSD_LINE: + case NVCV_OSD_ROTATED_RECT: + case NVCV_OSD_CIRCLE: + case NVCV_OSD_ARROW: + return true; + default: + return false; + } +} + +} // namespace + +CVCUDA_DEFINE_API(0, 16, NVCVStatus, nvcvBndBoxesIConstruct, + (NVCVBndBoxesI * handle, const NVCVBndBoxI *boxes, const int32_t *numBoxesPerBatch, + int32_t batchSize)) +{ + return nvcv::ProtectCall( + [&handle, &boxes, &numBoxesPerBatch, &batchSize] + { + if (handle == nullptr) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Output handle pointer must not be NULL"); + } + if (batchSize < 0) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "batchSize must be >= 0"); + } + if (batchSize > 0 && numBoxesPerBatch == nullptr) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "numBoxesPerBatch must not be NULL when batchSize > 0"); + } + + std::vector> vec(batchSize); + const NVCVBndBoxI *cursor = boxes; + for (int32_t b = 0; b < batchSize; ++b) + { + int32_t n = numBoxesPerBatch[b]; + if (n < 0) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Per-batch box counts must be >= 0"); + } + if (n > 0) + { + if (cursor == nullptr) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "boxes must not be NULL when any batch has boxes"); + } + vec[b].assign(cursor, cursor + n); + cursor += n; + } + } + + auto impl = std::make_unique(vec); + *handle = static_cast(static_cast(impl.release())); + }); +} + +CVCUDA_DEFINE_API(0, 16, NVCVStatus, nvcvBndBoxesIDestroy, (NVCVBndBoxesI handle)) +{ + return nvcv::ProtectCall( + [&handle] + { + std::unique_ptr impl( + static_cast(static_cast(handle))); + (void)impl; + }); +} + +CVCUDA_DEFINE_API(0, 16, NVCVStatus, nvcvBlurBoxesIConstruct, + (NVCVBlurBoxesI * handle, const NVCVBlurBoxI *boxes, const int32_t *numBoxesPerBatch, + int32_t batchSize)) +{ + return nvcv::ProtectCall( + [&handle, &boxes, &numBoxesPerBatch, &batchSize] + { + if (handle == nullptr) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Output handle pointer must not be NULL"); + } + if (batchSize < 0) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "batchSize must be >= 0"); + } + if (batchSize > 0 && numBoxesPerBatch == nullptr) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "numBoxesPerBatch must not be NULL when batchSize > 0"); + } + + std::vector> vec(batchSize); + const NVCVBlurBoxI *cursor = boxes; + for (int32_t b = 0; b < batchSize; ++b) + { + int32_t n = numBoxesPerBatch[b]; + if (n < 0) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Per-batch box counts must be >= 0"); + } + if (n > 0) + { + if (cursor == nullptr) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "boxes must not be NULL when any batch has boxes"); + } + vec[b].assign(cursor, cursor + n); + cursor += n; + } + } + + auto impl = std::make_unique(vec); + *handle = static_cast(static_cast(impl.release())); + }); +} + +CVCUDA_DEFINE_API(0, 16, NVCVStatus, nvcvBlurBoxesIDestroy, (NVCVBlurBoxesI handle)) +{ + return nvcv::ProtectCall( + [&handle] + { + std::unique_ptr impl( + static_cast(static_cast(handle))); + (void)impl; + }); +} + +CVCUDA_DEFINE_API(0, 16, NVCVStatus, nvcvElementsConstruct, + (NVCVElements * handle, const NVCVOSDType *types, const void *const *payloads, + const int32_t *numElementsPerBatch, int32_t batchSize)) +{ + return nvcv::ProtectCall( + [&handle, &types, &payloads, &numElementsPerBatch, &batchSize] + { + if (handle == nullptr) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Output handle pointer must not be NULL"); + } + if (batchSize < 0) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "batchSize must be >= 0"); + } + if (batchSize > 0 && numElementsPerBatch == nullptr) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "numElementsPerBatch must not be NULL when batchSize > 0"); + } + + std::vector>> vec(batchSize); + int32_t flatIdx = 0; + for (int32_t b = 0; b < batchSize; ++b) + { + int32_t n = numElementsPerBatch[b]; + if (n < 0) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Per-batch element counts must be >= 0"); + } + if (n > 0 && (types == nullptr || payloads == nullptr)) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "types and payloads must not be NULL when any batch has elements"); + } + vec[b].reserve(n); + for (int32_t i = 0; i < n; ++i, ++flatIdx) + { + NVCVOSDType t = types[flatIdx]; + if (!isPODElementType(t)) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Only POD OSD element types are supported by this API"); + } + if (payloads[flatIdx] == nullptr) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Element payload must not be NULL"); + } + const void *payload = payloads[flatIdx]; + switch (t) + { + case NVCV_OSD_RECT: + vec[b].push_back( + std::make_shared(t, static_cast(payload))); + break; + case NVCV_OSD_POINT: + vec[b].push_back( + std::make_shared(t, static_cast(payload))); + break; + case NVCV_OSD_LINE: + vec[b].push_back( + std::make_shared(t, static_cast(payload))); + break; + case NVCV_OSD_ROTATED_RECT: + vec[b].push_back( + std::make_shared(t, static_cast(payload))); + break; + case NVCV_OSD_CIRCLE: + vec[b].push_back( + std::make_shared(t, static_cast(payload))); + break; + case NVCV_OSD_ARROW: + vec[b].push_back( + std::make_shared(t, static_cast(payload))); + break; + default: + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Only POD OSD element types are supported by this API"); + } + } + } + + auto impl = std::make_unique(vec); + *handle = static_cast(static_cast(impl.release())); + }); +} + +CVCUDA_DEFINE_API(0, 16, NVCVStatus, nvcvElementsDestroy, (NVCVElements handle)) +{ + return nvcv::ProtectCall( + [&handle] + { + std::unique_ptr impl( + static_cast(static_cast(handle))); + (void)impl; + }); +} diff --git a/src/cvcuda/include/cvcuda/IOperator.hpp b/src/cvcuda/include/cvcuda/IOperator.hpp index 10ecd9ff9..2ec2218f2 100644 --- a/src/cvcuda/include/cvcuda/IOperator.hpp +++ b/src/cvcuda/include/cvcuda/IOperator.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -28,14 +28,72 @@ namespace cvcuda { -class IOperator +namespace detail { + +// Move-only RAII wrapper around an NVCVOperatorHandle. Owning operator +// wrappers hold one of these so they don't need to write any rule-of-five +// boilerplate of their own — copy is deleted, move transfers the handle, +// destruction calls nvcvOperatorDestroy exactly once. +class OperatorHandle { public: - virtual ~IOperator() = default; + OperatorHandle() noexcept = default; - virtual NVCVOperatorHandle handle() const noexcept = 0; + explicit OperatorHandle(NVCVOperatorHandle h) noexcept + : m_handle{h} + { + } + + ~OperatorHandle() + { + nvcvOperatorDestroy(m_handle); + } + + OperatorHandle(const OperatorHandle &) = delete; + OperatorHandle &operator=(const OperatorHandle &) = delete; + + OperatorHandle(OperatorHandle &&that) noexcept + : m_handle{that.m_handle} + { + that.m_handle = nullptr; + } + + OperatorHandle &operator=(OperatorHandle &&that) noexcept + { + if (this != &that) + { + nvcvOperatorDestroy(m_handle); + m_handle = that.m_handle; + that.m_handle = nullptr; + } + return *this; + } + + NVCVOperatorHandle get() const noexcept + { + return m_handle; + } private: + NVCVOperatorHandle m_handle = nullptr; +}; + +} // namespace detail + +class IOperator +{ +public: + IOperator() = default; + virtual ~IOperator() = default; + IOperator(const IOperator &) = delete; + IOperator &operator=(const IOperator &) = delete; + // Defaulted move is correct only because IOperator carries no state; if + // data members are added here, revisit move semantics so subclasses don't + // silently inherit a wrong default. + IOperator(IOperator &&) noexcept = default; + IOperator &operator=(IOperator &&) noexcept = default; + + virtual NVCVOperatorHandle handle() const noexcept = 0; }; } // namespace cvcuda diff --git a/src/cvcuda/include/cvcuda/OpAdaptiveThreshold.h b/src/cvcuda/include/cvcuda/OpAdaptiveThreshold.h index 70bea5792..cb80713a8 100644 --- a/src/cvcuda/include/cvcuda/OpAdaptiveThreshold.h +++ b/src/cvcuda/include/cvcuda/OpAdaptiveThreshold.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -61,7 +61,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaAdaptiveThresholdCreate(NVCVOperatorHandle *handl * Limitations: * * Input: - * Data Layout: [kNHWC, kHWC] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] * Channels: [1] * * Data Type | Allowed @@ -72,11 +72,12 @@ CVCUDA_PUBLIC NVCVStatus cvcudaAdaptiveThresholdCreate(NVCVOperatorHandle *handl * 16bit Signed | No * 32bit Unsigned | No * 32bit Signed | No + * 16bit Float | No * 32bit Float | No * 64bit Float | No * * Output: - * Data Layout: [kNHWC, kHWC] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] * Channels: [1] * * Data Type | Allowed @@ -87,6 +88,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaAdaptiveThresholdCreate(NVCVOperatorHandle *handl * 16bit Signed | No * 32bit Unsigned | No * 32bit Signed | No + * 16bit Float | No * 32bit Float | No * 64bit Float | No * @@ -139,4 +141,6 @@ CVCUDA_PUBLIC NVCVStatus cvcudaAdaptiveThresholdVarShapeSubmit(NVCVOperatorHandl } #endif +/** @} */ + #endif /* CVCUDA_ADAPTIVETHRESHOLD_H */ diff --git a/src/cvcuda/include/cvcuda/OpAdaptiveThreshold.hpp b/src/cvcuda/include/cvcuda/OpAdaptiveThreshold.hpp index 7a2f16ef7..5c29f531f 100644 --- a/src/cvcuda/include/cvcuda/OpAdaptiveThreshold.hpp +++ b/src/cvcuda/include/cvcuda/OpAdaptiveThreshold.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -35,6 +35,8 @@ #include #include +#include + namespace cvcuda { class AdaptiveThreshold final : public IOperator @@ -42,56 +44,52 @@ class AdaptiveThreshold final : public IOperator public: explicit AdaptiveThreshold(int32_t maxBlockSize, int32_t maxVarShapeBatchSize); - ~AdaptiveThreshold(); - void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, double maxValue, NVCVAdaptiveThresholdType adaptiveMethod, NVCVThresholdType thresholdType, int32_t blockSize, - double c); + double c) const; void operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, const nvcv::Tensor &maxValue, NVCVAdaptiveThresholdType adaptiveMethod, - NVCVThresholdType thresholdType, const nvcv::Tensor &blockSize, const nvcv::Tensor &c); + NVCVThresholdType thresholdType, const nvcv::Tensor &blockSize, const nvcv::Tensor &c) const; - virtual NVCVOperatorHandle handle() const noexcept override; + NVCVOperatorHandle handle() const noexcept override; private: - NVCVOperatorHandle m_handle; + detail::OperatorHandle m_handle; }; inline AdaptiveThreshold::AdaptiveThreshold(int32_t maxBlockSize, int32_t maxVarShapeBatchSize) { - nvcv::detail::CheckThrow(cvcudaAdaptiveThresholdCreate(&m_handle, maxBlockSize, maxVarShapeBatchSize)); - assert(m_handle); -} - -inline AdaptiveThreshold::~AdaptiveThreshold() -{ - nvcvOperatorDestroy(m_handle); - m_handle = nullptr; + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaAdaptiveThresholdCreate(&h, maxBlockSize, maxVarShapeBatchSize)); + assert(h); + m_handle = detail::OperatorHandle{h}; } inline void AdaptiveThreshold::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, double maxValue, NVCVAdaptiveThresholdType adaptiveMethod, - NVCVThresholdType thresholdType, int32_t blockSize, double c) + NVCVThresholdType thresholdType, int32_t blockSize, double c) const { - nvcv::detail::CheckThrow(cvcudaAdaptiveThresholdSubmit(m_handle, stream, in.handle(), out.handle(), maxValue, + nvcv::detail::CheckThrow(cvcudaAdaptiveThresholdSubmit(m_handle.get(), stream, in.handle(), out.handle(), maxValue, adaptiveMethod, thresholdType, blockSize, c)); } inline void AdaptiveThreshold::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, const nvcv::Tensor &maxValue, NVCVAdaptiveThresholdType adaptiveMethod, NVCVThresholdType thresholdType, - const nvcv::Tensor &blockSize, const nvcv::Tensor &c) + const nvcv::Tensor &blockSize, const nvcv::Tensor &c) const { - nvcv::detail::CheckThrow(cvcudaAdaptiveThresholdVarShapeSubmit(m_handle, stream, in.handle(), out.handle(), + nvcv::detail::CheckThrow(cvcudaAdaptiveThresholdVarShapeSubmit(m_handle.get(), stream, in.handle(), out.handle(), maxValue.handle(), adaptiveMethod, thresholdType, blockSize.handle(), c.handle())); } inline NVCVOperatorHandle AdaptiveThreshold::handle() const noexcept { - return m_handle; + return m_handle.get(); } } // namespace cvcuda +/** @} */ + #endif // CVCUDA_ADAPTIVETHRESHOLD_HPP diff --git a/src/cvcuda/include/cvcuda/OpAdjustContrast.h b/src/cvcuda/include/cvcuda/OpAdjustContrast.h new file mode 100644 index 000000000..6f1aef352 --- /dev/null +++ b/src/cvcuda/include/cvcuda/OpAdjustContrast.h @@ -0,0 +1,157 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +/** + * @file OpAdjustContrast.h + * + * @brief Blends each image toward its grayscale mean by a scalar factor (torchvision-compatible + * adjust_contrast): out = clamp(contrastFactor * in + (1 - contrastFactor) * mean, 0, bound). + * @defgroup NVCV_C_ALGORITHM_ADJUST_CONTRAST Adjust Contrast + * @{ + */ + +#ifndef CVCUDA_ADJUST_CONTRAST_H +#define CVCUDA_ADJUST_CONTRAST_H + +#include "Operator.h" +#include "detail/Export.h" + +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + +/** Constructs an instance of the AdjustContrast operator. + * + * @param [out] handle Where the image instance handle will be written to. + * + Must not be NULL. + * + * @retval #NVCV_ERROR_INVALID_ARGUMENT Handle is null. + * @retval #NVCV_ERROR_OUT_OF_MEMORY Not enough memory to create the operator. + * @retval #NVCV_SUCCESS Operation executed successfully. + */ +CVCUDA_PUBLIC NVCVStatus cvcudaAdjustContrastCreate(NVCVOperatorHandle *handle); + +/** Executes the AdjustContrast operation on the given cuda stream. This operation does not + * wait for completion. + * + * Blends each image toward its grayscale mean by a single scalar factor, matching + * torchvision.transforms.v2.functional.adjust_contrast: + * + * out = clamp(contrastFactor * in + (1 - contrastFactor) * mean, 0, bound) + * + * where `bound` is 1.0 for floating-point images and the dtype maximum for integer images, and + * `mean` is the per-image mean of its grayscale conversion. Grayscale uses the BT.601 luma weights + * `0.2989 R + 0.587 G + 0.114 B` (floored for integer 3-channel input, matching torchvision); a + * single-channel image is its own grayscale. A factor of 0 produces a flat gray image, 1 leaves + * the image unchanged, and values > 1 increase contrast. Integer results round-to-nearest, so they + * may differ from torchvision (which truncates) by at most 1 LSB. + * Three-channel tensors are interpreted in RGB component order. Three-channel image batches use + * their RGB format semantics; formats without a color model are interpreted as RGB. The format + * swizzle is honored, including BGR storage order. + * + * Reference: torchvision.transforms.v2.functional.adjust_contrast + * (out = clamp(factor * in + (1 - factor) * grayscale_mean, 0, bound)). + * + * Limitations: + * + * Input: + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 3] + * + * Data Type | Allowed + * -------------- | ------------- + * 8bit Unsigned | Yes + * 8bit Signed | No + * 16bit Unsigned | No + * 16bit Signed | No + * 32bit Unsigned | No + * 32bit Signed | No + * 16bit Float | No + * 32bit Float | Yes + * 64bit Float | No + * + * Output: + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 3] + * + * Data Type | Allowed + * -------------- | ------------- + * 8bit Unsigned | Yes + * 8bit Signed | No + * 16bit Unsigned | No + * 16bit Signed | No + * 32bit Unsigned | No + * 32bit Signed | No + * 16bit Float | No + * 32bit Float | Yes + * 64bit Float | No + * + * Input/Output dependency + * Property | Input == Output + * -------------- | ------------- + * Data Layout | Yes + * Data Type | Yes + * Channels | Yes + * Width | Yes + * Height | Yes + * Samples | Yes + * + * @param [in] handle Handle to the operator. + * + Must not be NULL. + * @param [in] stream Handle to a valid CUDA stream. + * + * @param [in] in Input tensor to get values from. + * + Must not be NULL. + * + * @param [out] out Output tensor to set values to. + * + Must not be NULL. + * + * @param [in] contrastFactor Non-negative scalar contrast multiplier. + * + * @retval #NVCV_ERROR_INVALID_ARGUMENT Some parameter is outside valid range. + * @retval #NVCV_ERROR_INTERNAL Internal error in the operator, invalid types passed in. + * @retval #NVCV_SUCCESS Operation executed successfully. + */ +CVCUDA_PUBLIC NVCVStatus cvcudaAdjustContrastSubmit(NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle in, + NVCVTensorHandle out, double contrastFactor); + +/** + * Executes the AdjustContrast operation on a batch of images. + * + * Apart from input and output image batches, all parameters are the same as \ref cvcudaAdjustContrastSubmit. + * + * @param[in] in Input image batch. + * + * @param[out] out Output image batch. + */ +CVCUDA_PUBLIC NVCVStatus cvcudaAdjustContrastVarShapeSubmit(NVCVOperatorHandle handle, cudaStream_t stream, + NVCVImageBatchHandle in, NVCVImageBatchHandle out, + double contrastFactor); + +#ifdef __cplusplus +} +#endif + +/** @} */ + +#endif /* CVCUDA_ADJUST_CONTRAST_H */ diff --git a/src/cvcuda/include/cvcuda/OpAdjustContrast.hpp b/src/cvcuda/include/cvcuda/OpAdjustContrast.hpp new file mode 100644 index 000000000..150450357 --- /dev/null +++ b/src/cvcuda/include/cvcuda/OpAdjustContrast.hpp @@ -0,0 +1,87 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +/** + * @file OpAdjustContrast.hpp + * + * @brief Defines the public C++ Class for the AdjustContrast operation. + * @defgroup NVCV_CPP_ALGORITHM_ADJUST_CONTRAST Adjust Contrast + * @{ + */ + +#ifndef CVCUDA__ADJUST_CONTRAST_HPP +#define CVCUDA__ADJUST_CONTRAST_HPP + +#include "IOperator.hpp" +#include "OpAdjustContrast.h" + +#include +#include +#include + +#include + +namespace cvcuda { + +class AdjustContrast final : public IOperator +{ +public: + explicit AdjustContrast(); + + void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, double contrastFactor) const; + + void operator()(cudaStream_t stream, const nvcv::ImageBatch &in, const nvcv::ImageBatch &out, + double contrastFactor) const; + + NVCVOperatorHandle handle() const noexcept override; + +private: + detail::OperatorHandle m_handle; +}; + +inline AdjustContrast::AdjustContrast() +{ + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaAdjustContrastCreate(&h)); + assert(h); + m_handle = detail::OperatorHandle{h}; +} + +inline void AdjustContrast::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, + double contrastFactor) const +{ + nvcv::detail::CheckThrow( + cvcudaAdjustContrastSubmit(m_handle.get(), stream, in.handle(), out.handle(), contrastFactor)); +} + +inline void AdjustContrast::operator()(cudaStream_t stream, const nvcv::ImageBatch &in, const nvcv::ImageBatch &out, + double contrastFactor) const +{ + nvcv::detail::CheckThrow( + cvcudaAdjustContrastVarShapeSubmit(m_handle.get(), stream, in.handle(), out.handle(), contrastFactor)); +} + +inline NVCVOperatorHandle AdjustContrast::handle() const noexcept +{ + return m_handle.get(); +} + +} // namespace cvcuda + +/** @} */ + +#endif // CVCUDA__ADJUST_CONTRAST_HPP diff --git a/src/cvcuda/include/cvcuda/OpAdjustHue.h b/src/cvcuda/include/cvcuda/OpAdjustHue.h new file mode 100644 index 000000000..5af6fabc9 --- /dev/null +++ b/src/cvcuda/include/cvcuda/OpAdjustHue.h @@ -0,0 +1,152 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +/** + * @file OpAdjustHue.h + * + * @brief Rotates the hue of an image in HSV space. + * + * AdjustHue converts each RGB pixel to HSV, shifts the hue channel by a scalar @p hue factor + * (H is normalized to [0, 1)), and converts back to RGB: @f$ H' = (H + hue) \bmod 1 @f$. @p hue = 0 + * leaves the image unchanged; +/-0.5 is a full 180-degree hue rotation in either direction. + * Single-channel images are returned unchanged. + * + * The pixel is processed in single precision: 8-bit unsigned input is scaled to [0, 1] before the + * HSV round-trip and scaled back (truncated toward zero) afterwards. Float input is used directly; + * input in [0, 1] remains in [0, 1], while out-of-range value-channel data is preserved as in torchvision. + * + * Reference: mimics torchvision.transforms.v2.functional.adjust_hue + * (RGB -> HSV, H = (H + hue_factor) % 1, HSV -> RGB). + * + * @defgroup NVCV_C_ALGORITHM__ADJUST_HUE Adjust Hue + * @{ + */ + +#ifndef CVCUDA__ADJUST_HUE_H +#define CVCUDA__ADJUST_HUE_H + +#include "Operator.h" +#include "detail/Export.h" + +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + +/** Constructs an instance of the AdjustHue operator. + * + * @param [out] handle Where the operator instance handle will be written to. + * + Must not be NULL. + * + * @retval #NVCV_ERROR_INVALID_ARGUMENT Handle is null. + * @retval #NVCV_ERROR_OUT_OF_MEMORY Not enough memory to create the operator. + * @retval #NVCV_SUCCESS Operation executed successfully. + */ +CVCUDA_PUBLIC NVCVStatus cvcudaAdjustHueCreate(NVCVOperatorHandle *handle); + +/** Executes the AdjustHue operation on the given cuda stream. This operation does not + * wait for completion. + * + * Limitations: + * + * Input: + * Data Layout: [NVCV_TENSOR_NHWC, NVCV_TENSOR_HWC, NVCV_TENSOR_NCHW, NVCV_TENSOR_CHW] + * Channels: [1, 3] + * + * Data Type | Allowed + * -------------- | ------------- + * 8bit Unsigned | Yes + * 8bit Signed | No + * 16bit Unsigned | No + * 16bit Signed | No + * 32bit Unsigned | No + * 32bit Signed | No + * 16bit Float | No + * 32bit Float | Yes + * 64bit Float | No + * + * Output: + * Data Layout: [NVCV_TENSOR_NHWC, NVCV_TENSOR_HWC, NVCV_TENSOR_NCHW, NVCV_TENSOR_CHW] + * Channels: [1, 3] + * + * Data Type | Allowed + * -------------- | ------------- + * 8bit Unsigned | Yes + * 8bit Signed | No + * 16bit Unsigned | No + * 16bit Signed | No + * 32bit Unsigned | No + * 32bit Signed | No + * 16bit Float | No + * 32bit Float | Yes + * 64bit Float | No + * + * Input/Output dependency + * + * Property | Input == Output + * -------------- | ------------- + * Data Layout | Yes + * Data Type | Yes + * Number | Yes + * Channels | Yes + * Width | Yes + * Height | Yes + * + * @param [in] handle Handle to the operator. + * + Must not be NULL. + * @param [in] stream Handle to a valid CUDA stream. + * + * @param [in] in input tensor. + * + * @param [out] out output tensor. + * + * @param [in] hue Hue-rotation factor applied to all images. Must be in [-0.5, 0.5]; 0 leaves the + * image unchanged, +/-0.5 is a full 180-degree rotation. + * + * @retval #NVCV_ERROR_INVALID_ARGUMENT Some parameter is outside valid range. + * @retval #NVCV_ERROR_INTERNAL Internal error in the operator, invalid types passed in. + * @retval #NVCV_SUCCESS Operation executed successfully. + */ +CVCUDA_PUBLIC NVCVStatus cvcudaAdjustHueSubmit(NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle in, + NVCVTensorHandle out, double hue); + +/** Executes the AdjustHue operation on a batch of variable-shaped images on the given cuda stream. + * Same limitations as cvcudaAdjustHueSubmit. + * + * @param [in] handle Handle to the operator. Must not be NULL. + * @param [in] stream Handle to a valid CUDA stream. + * @param [in] in input image batch. + * @param [out] out output image batch. + * @param [in] hue Hue-rotation factor applied to all images (see cvcudaAdjustHueSubmit). + * + * @retval #NVCV_ERROR_INVALID_ARGUMENT Some parameter is outside valid range. + * @retval #NVCV_ERROR_INTERNAL Internal error in the operator, invalid types passed in. + * @retval #NVCV_SUCCESS Operation executed successfully. + */ +CVCUDA_PUBLIC NVCVStatus cvcudaAdjustHueVarShapeSubmit(NVCVOperatorHandle handle, cudaStream_t stream, + NVCVImageBatchHandle in, NVCVImageBatchHandle out, double hue); + +#ifdef __cplusplus +} +#endif + +#endif /* CVCUDA__ADJUST_HUE_H */ diff --git a/src/cvcuda/include/cvcuda/OpAdjustHue.hpp b/src/cvcuda/include/cvcuda/OpAdjustHue.hpp new file mode 100644 index 000000000..f6e7619f4 --- /dev/null +++ b/src/cvcuda/include/cvcuda/OpAdjustHue.hpp @@ -0,0 +1,82 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +/** + * @file OpAdjustHue.hpp + * + * @brief Defines the public C++ Class for the AdjustHue operation. + * @defgroup NVCV_CPP_ALGORITHM__ADJUST_HUE Adjust Hue + * @{ + */ + +#ifndef CVCUDA__ADJUST_HUE_HPP +#define CVCUDA__ADJUST_HUE_HPP + +#include "IOperator.hpp" +#include "OpAdjustHue.h" + +#include +#include +#include +#include +#include + +namespace cvcuda { + +class AdjustHue final : public IOperator +{ +public: + explicit AdjustHue(); + + void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, double hue) const; + + void operator()(cudaStream_t stream, const nvcv::ImageBatch &in, const nvcv::ImageBatch &out, double hue) const; + + NVCVOperatorHandle handle() const noexcept override; + +private: + detail::OperatorHandle m_handle; +}; + +inline AdjustHue::AdjustHue() +{ + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaAdjustHueCreate(&h)); + assert(h); + m_handle = detail::OperatorHandle{h}; +} + +inline void AdjustHue::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, + double hue) const +{ + nvcv::detail::CheckThrow(cvcudaAdjustHueSubmit(m_handle.get(), stream, in.handle(), out.handle(), hue)); +} + +inline void AdjustHue::operator()(cudaStream_t stream, const nvcv::ImageBatch &in, const nvcv::ImageBatch &out, + double hue) const +{ + nvcv::detail::CheckThrow(cvcudaAdjustHueVarShapeSubmit(m_handle.get(), stream, in.handle(), out.handle(), hue)); +} + +inline NVCVOperatorHandle AdjustHue::handle() const noexcept +{ + return m_handle.get(); +} + +} // namespace cvcuda + +#endif // CVCUDA__ADJUST_HUE_HPP diff --git a/src/cvcuda/include/cvcuda/OpAdjustSaturation.h b/src/cvcuda/include/cvcuda/OpAdjustSaturation.h new file mode 100644 index 000000000..8e81dc55c --- /dev/null +++ b/src/cvcuda/include/cvcuda/OpAdjustSaturation.h @@ -0,0 +1,156 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +/** + * @file OpAdjustSaturation.h + * + * @brief Adjusts the color saturation of an image by blending it toward its grayscale. + * + * AdjustSaturation blends each RGB pixel toward its luminance (grayscale) by a scalar + * @p saturation factor: @f$ out_c = saturation \cdot in_c + (1 - saturation) \cdot gray @f$, where + * @f$ gray = 0.2989 \cdot R + 0.587 \cdot G + 0.114 \cdot B @f$ (computed in single precision, and + * floored for integer dtypes before blending). @p saturation = 1 leaves the image unchanged, + * 0 produces the grayscale image, and values > 1 over-saturate. For integer dtypes the blended + * value is clamped to the dtype range and truncated toward zero (matching torchvision's cast); for + * float the value is clamped to [0, 1]. Single-channel images are returned unchanged. + * + * The luminance coefficients (0.2989 / 0.587 / 0.114) follow torchvision and intentionally differ + * from cvcuda.cvtcolor's RGB2GRAY (0.299 / 0.587 / 0.114); the two operators answer to different + * references. + * + * Reference: mimics torchvision.transforms.v2.functional.adjust_saturation + * (out = blend(image, rgb_to_grayscale(image), saturation_factor)). + * + * @defgroup NVCV_C_ALGORITHM__ADJUST_SATURATION Adjust Saturation + * @{ + */ + +#ifndef CVCUDA__ADJUST_SATURATION_H +#define CVCUDA__ADJUST_SATURATION_H + +#include "Operator.h" +#include "detail/Export.h" + +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + +/** Constructs an instance of the AdjustSaturation operator. + * + * @param [out] handle Where the operator instance handle will be written to. + * + Must not be NULL. + * + * @retval #NVCV_ERROR_INVALID_ARGUMENT Handle is null. + * @retval #NVCV_ERROR_OUT_OF_MEMORY Not enough memory to create the operator. + * @retval #NVCV_SUCCESS Operation executed successfully. + */ +CVCUDA_PUBLIC NVCVStatus cvcudaAdjustSaturationCreate(NVCVOperatorHandle *handle); + +/** Executes the AdjustSaturation operation on the given cuda stream. This operation does not + * wait for completion. + * + * Limitations: + * + * Input: + * Data Layout: [NVCV_TENSOR_NHWC, NVCV_TENSOR_HWC, NVCV_TENSOR_NCHW, NVCV_TENSOR_CHW] + * Channels: [1, 3] + * + * Data Type | Allowed + * -------------- | ------------- + * 8bit Unsigned | Yes + * 8bit Signed | No + * 16bit Unsigned | No + * 16bit Signed | No + * 32bit Unsigned | No + * 32bit Signed | No + * 16bit Float | No + * 32bit Float | Yes + * 64bit Float | No + * + * Output: + * Data Layout: [NVCV_TENSOR_NHWC, NVCV_TENSOR_HWC, NVCV_TENSOR_NCHW, NVCV_TENSOR_CHW] + * Channels: [1, 3] + * + * Data Type | Allowed + * -------------- | ------------- + * 8bit Unsigned | Yes + * 8bit Signed | No + * 16bit Unsigned | No + * 16bit Signed | No + * 32bit Unsigned | No + * 32bit Signed | No + * 16bit Float | No + * 32bit Float | Yes + * 64bit Float | No + * + * Input/Output dependency + * + * Property | Input == Output + * -------------- | ------------- + * Data Layout | Yes + * Data Type | Yes + * Number | Yes + * Channels | Yes + * Width | Yes + * Height | Yes + * + * @param [in] handle Handle to the operator. + * + Must not be NULL. + * @param [in] stream Handle to a valid CUDA stream. + * + * @param [in] in input tensor. + * + * @param [out] out output tensor. + * + * @param [in] saturation Saturation factor applied to all images. Must be >= 0; 1 leaves the image + * unchanged, 0 yields grayscale, values > 1 over-saturate. + * + * @retval #NVCV_ERROR_INVALID_ARGUMENT Some parameter is outside valid range. + * @retval #NVCV_ERROR_INTERNAL Internal error in the operator, invalid types passed in. + * @retval #NVCV_SUCCESS Operation executed successfully. + */ +CVCUDA_PUBLIC NVCVStatus cvcudaAdjustSaturationSubmit(NVCVOperatorHandle handle, cudaStream_t stream, + NVCVTensorHandle in, NVCVTensorHandle out, double saturation); + +/** Executes the AdjustSaturation operation on a batch of variable-shaped images on the given cuda + * stream. Same limitations as cvcudaAdjustSaturationSubmit. + * + * @param [in] handle Handle to the operator. Must not be NULL. + * @param [in] stream Handle to a valid CUDA stream. + * @param [in] in input image batch. + * @param [out] out output image batch. + * @param [in] saturation Saturation factor applied to all images (see cvcudaAdjustSaturationSubmit). + * + * @retval #NVCV_ERROR_INVALID_ARGUMENT Some parameter is outside valid range. + * @retval #NVCV_ERROR_INTERNAL Internal error in the operator, invalid types passed in. + * @retval #NVCV_SUCCESS Operation executed successfully. + */ +CVCUDA_PUBLIC NVCVStatus cvcudaAdjustSaturationVarShapeSubmit(NVCVOperatorHandle handle, cudaStream_t stream, + NVCVImageBatchHandle in, NVCVImageBatchHandle out, + double saturation); + +#ifdef __cplusplus +} +#endif + +#endif /* CVCUDA__ADJUST_SATURATION_H */ diff --git a/src/cvcuda/include/cvcuda/OpAdjustSaturation.hpp b/src/cvcuda/include/cvcuda/OpAdjustSaturation.hpp new file mode 100644 index 000000000..05240d45f --- /dev/null +++ b/src/cvcuda/include/cvcuda/OpAdjustSaturation.hpp @@ -0,0 +1,85 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +/** + * @file OpAdjustSaturation.hpp + * + * @brief Defines the public C++ Class for the AdjustSaturation operation. + * @defgroup NVCV_CPP_ALGORITHM__ADJUST_SATURATION Adjust Saturation + * @{ + */ + +#ifndef CVCUDA__ADJUST_SATURATION_HPP +#define CVCUDA__ADJUST_SATURATION_HPP + +#include "IOperator.hpp" +#include "OpAdjustSaturation.h" + +#include +#include +#include +#include +#include + +namespace cvcuda { + +class AdjustSaturation final : public IOperator +{ +public: + explicit AdjustSaturation(); + + void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, double saturation) const; + + void operator()(cudaStream_t stream, const nvcv::ImageBatch &in, const nvcv::ImageBatch &out, + double saturation) const; + + NVCVOperatorHandle handle() const noexcept override; + +private: + detail::OperatorHandle m_handle; +}; + +inline AdjustSaturation::AdjustSaturation() +{ + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaAdjustSaturationCreate(&h)); + assert(h); + m_handle = detail::OperatorHandle{h}; +} + +inline void AdjustSaturation::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, + double saturation) const +{ + nvcv::detail::CheckThrow( + cvcudaAdjustSaturationSubmit(m_handle.get(), stream, in.handle(), out.handle(), saturation)); +} + +inline void AdjustSaturation::operator()(cudaStream_t stream, const nvcv::ImageBatch &in, const nvcv::ImageBatch &out, + double saturation) const +{ + nvcv::detail::CheckThrow( + cvcudaAdjustSaturationVarShapeSubmit(m_handle.get(), stream, in.handle(), out.handle(), saturation)); +} + +inline NVCVOperatorHandle AdjustSaturation::handle() const noexcept +{ + return m_handle.get(); +} + +} // namespace cvcuda + +#endif // CVCUDA__ADJUST_SATURATION_HPP diff --git a/src/cvcuda/include/cvcuda/OpAdjustSharpness.h b/src/cvcuda/include/cvcuda/OpAdjustSharpness.h new file mode 100644 index 000000000..4de20bacc --- /dev/null +++ b/src/cvcuda/include/cvcuda/OpAdjustSharpness.h @@ -0,0 +1,161 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +/** + * @file OpAdjustSharpness.h + * + * @brief Adjusts image sharpness by blending each image with a 3x3-smoothed copy of itself. + * + * AdjustSharpness enhances or reduces local contrast by blending the input with a "degenerate" + * (smoothed) version of itself. The smoothed image is produced, per channel independently, by a + * depthwise 3x3 convolution with the normalized smoothing kernel @f$ \frac{1}{13} + * \begin{bmatrix} 1 & 1 & 1 \\ 1 & 5 & 1 \\ 1 & 1 & 1 \end{bmatrix} @f$. The output is the blend + * @f$ out = factor \cdot in + (1 - factor) \cdot blur @f$, where @p factor is + * @p sharpnessFactor: 1.0 leaves the image unchanged, 0.0 yields the fully-smoothed image, and + * values greater than 1.0 sharpen. For integer types the smoothed value is rounded to nearest + * (ties to even) before blending, the blend is clamped to @f$ [0, bound] @f$ (255 for 8-bit + * unsigned, 65535 for 16-bit unsigned) and truncated toward zero on the final cast; for float it + * is clamped to @f$ [0, 1] @f$. + * + * The convolution only rewrites the image interior: the 1-pixel border is copied through + * unchanged (there is no border extension). Consequently, when the image height or width is less + * than 3 there is no interior and the entire image is copied unchanged. + * + * Reference: mimics torchvision.transforms.v2.functional.adjust_sharpness + * (out = blend(image, blur(image), sharpness_factor), interior-only, border unchanged). + * + * @defgroup NVCV_C_ALGORITHM__ADJUST_SHARPNESS Adjust Sharpness + * @{ + */ + +#ifndef CVCUDA__ADJUST_SHARPNESS_H +#define CVCUDA__ADJUST_SHARPNESS_H + +#include "Operator.h" +#include "detail/Export.h" + +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + +/** Constructs an instance of the AdjustSharpness operator. + * + * @param [out] handle Where the operator instance handle will be written to. + * + Must not be NULL. + * + * @retval #NVCV_ERROR_INVALID_ARGUMENT Handle is null. + * @retval #NVCV_ERROR_OUT_OF_MEMORY Not enough memory to create the operator. + * @retval #NVCV_SUCCESS Operation executed successfully. + */ +CVCUDA_PUBLIC NVCVStatus cvcudaAdjustSharpnessCreate(NVCVOperatorHandle *handle); + +/** Executes the AdjustSharpness operation on the given cuda stream. This operation does not + * wait for completion. + * + * Limitations: + * + * Input: + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 3, 4] + * + * Data Type | Allowed + * -------------- | ------------- + * 8bit Unsigned | Yes + * 8bit Signed | No + * 16bit Unsigned | Yes + * 16bit Signed | No + * 32bit Unsigned | No + * 32bit Signed | No + * 32bit Float | Yes + * 64bit Float | No + * + * Output: + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 3, 4] + * + * Data Type | Allowed + * -------------- | ------------- + * 8bit Unsigned | Yes + * 8bit Signed | No + * 16bit Unsigned | Yes + * 16bit Signed | No + * 32bit Unsigned | No + * 32bit Signed | No + * 32bit Float | Yes + * 64bit Float | No + * + * Input/Output dependency + * + * Property | Input == Output + * -------------- | ------------- + * Data Layout | Yes + * Data Type | Yes + * Number | Yes + * Channels | Yes + * Width | Yes + * Height | Yes + * + * @param [in] handle Handle to the operator. + * + Must not be NULL. + * @param [in] stream Handle to a valid CUDA stream. + * + * @param [in] in input tensor. + * + * @param [out] out output tensor. + * + * @param [in] sharpnessFactor Blend weight applied to the original image; the smoothed image + * receives weight (1 - sharpnessFactor). 1.0 leaves the image + * unchanged, 0.0 yields the fully-smoothed image, values above 1.0 + * sharpen. Must be non-negative. Only the image interior is blended; + * the 1-pixel border is copied unchanged, and images with height or + * width below 3 are copied unchanged in full. + * + * @retval #NVCV_ERROR_INVALID_ARGUMENT Some parameter is outside valid range. + * @retval #NVCV_ERROR_INTERNAL Internal error in the operator, invalid types passed in. + * @retval #NVCV_SUCCESS Operation executed successfully. + */ +CVCUDA_PUBLIC NVCVStatus cvcudaAdjustSharpnessSubmit(NVCVOperatorHandle handle, cudaStream_t stream, + NVCVTensorHandle in, NVCVTensorHandle out, float sharpnessFactor); + +/** Executes the AdjustSharpness operation on a batch of variable-shaped images on the given cuda + * stream. Same limitations as cvcudaAdjustSharpnessSubmit. + * + * @param [in] handle Handle to the operator. Must not be NULL. + * @param [in] stream Handle to a valid CUDA stream. + * @param [in] in input image batch. + * @param [out] out output image batch. + * @param [in] sharpnessFactor Blend weight (see cvcudaAdjustSharpnessSubmit). + * + * @retval #NVCV_ERROR_INVALID_ARGUMENT Some parameter is outside valid range. + * @retval #NVCV_ERROR_INTERNAL Internal error in the operator, invalid types passed in. + * @retval #NVCV_SUCCESS Operation executed successfully. + */ +CVCUDA_PUBLIC NVCVStatus cvcudaAdjustSharpnessVarShapeSubmit(NVCVOperatorHandle handle, cudaStream_t stream, + NVCVImageBatchHandle in, NVCVImageBatchHandle out, + float sharpnessFactor); + +#ifdef __cplusplus +} +#endif + +#endif /* CVCUDA__ADJUST_SHARPNESS_H */ diff --git a/src/cvcuda/include/cvcuda/OpAdjustSharpness.hpp b/src/cvcuda/include/cvcuda/OpAdjustSharpness.hpp new file mode 100644 index 000000000..ee5427d73 --- /dev/null +++ b/src/cvcuda/include/cvcuda/OpAdjustSharpness.hpp @@ -0,0 +1,85 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +/** + * @file OpAdjustSharpness.hpp + * + * @brief Defines the public C++ Class for the AdjustSharpness operation. + * @defgroup NVCV_CPP_ALGORITHM__ADJUST_SHARPNESS AdjustSharpness + * @{ + */ + +#ifndef CVCUDA__ADJUST_SHARPNESS_HPP +#define CVCUDA__ADJUST_SHARPNESS_HPP + +#include "IOperator.hpp" +#include "OpAdjustSharpness.h" + +#include +#include +#include +#include +#include + +namespace cvcuda { + +class AdjustSharpness final : public IOperator +{ +public: + explicit AdjustSharpness(); + + void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, float sharpnessFactor) const; + + void operator()(cudaStream_t stream, const nvcv::ImageBatch &in, const nvcv::ImageBatch &out, + float sharpnessFactor) const; + + NVCVOperatorHandle handle() const noexcept override; + +private: + detail::OperatorHandle m_handle; +}; + +inline AdjustSharpness::AdjustSharpness() +{ + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaAdjustSharpnessCreate(&h)); + assert(h); + m_handle = detail::OperatorHandle{h}; +} + +inline void AdjustSharpness::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, + float sharpnessFactor) const +{ + nvcv::detail::CheckThrow( + cvcudaAdjustSharpnessSubmit(m_handle.get(), stream, in.handle(), out.handle(), sharpnessFactor)); +} + +inline void AdjustSharpness::operator()(cudaStream_t stream, const nvcv::ImageBatch &in, const nvcv::ImageBatch &out, + float sharpnessFactor) const +{ + nvcv::detail::CheckThrow( + cvcudaAdjustSharpnessVarShapeSubmit(m_handle.get(), stream, in.handle(), out.handle(), sharpnessFactor)); +} + +inline NVCVOperatorHandle AdjustSharpness::handle() const noexcept +{ + return m_handle.get(); +} + +} // namespace cvcuda + +#endif // CVCUDA__ADJUST_SHARPNESS_HPP diff --git a/src/cvcuda/include/cvcuda/OpAdvCvtColor.h b/src/cvcuda/include/cvcuda/OpAdvCvtColor.h index 5e6683523..50768faa5 100644 --- a/src/cvcuda/include/cvcuda/OpAdvCvtColor.h +++ b/src/cvcuda/include/cvcuda/OpAdvCvtColor.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -56,8 +56,9 @@ CVCUDA_PUBLIC NVCVStatus cvcudaAdvCvtColorCreate(NVCVOperatorHandle *handle); * Limitations: * * Input: - * Data Layout: [kNHWC, kHWC] - * Channels: [1, 3] kNWHC/KHWC semi planar 420 tensors are allowed + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 3, 4] kNHWC/kHWC/kNCHW/kCHW semi planar 420 tensors are allowed + * (2-channel tensors are not supported) * * Data Type | Allowed * -------------- | ------------- @@ -67,12 +68,14 @@ CVCUDA_PUBLIC NVCVStatus cvcudaAdvCvtColorCreate(NVCVOperatorHandle *handle); * 16bit Signed | No * 32bit Unsigned | No * 32bit Signed | No + * 16bit Float | No * 32bit Float | No * 64bit Float | No * * Output: - * Data Layout: [kNHWC, kHWC] - * Channels: [1, 3] kNWHC/KHWC semi planar 420 tensors are allowed + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 3, 4] kNHWC/kHWC/kNCHW/kCHW semi planar 420 tensors are allowed + * (2-channel tensors are not supported) * * Data Type | Allowed * -------------- | ------------- @@ -82,6 +85,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaAdvCvtColorCreate(NVCVOperatorHandle *handle); * 16bit Signed | No * 32bit Unsigned | No * 32bit Signed | No + * 16bit Float | No * 32bit Float | No * 64bit Float | No * @@ -89,7 +93,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaAdvCvtColorCreate(NVCVOperatorHandle *handle); * * Property | Input == Output * -------------- | ------------- - * Data Layout | No + * Data Layout | Yes * Data Type | Yes * Number | Yes * Channels | Yes (No for semi planar 420 tensors conversion) @@ -107,14 +111,19 @@ CVCUDA_PUBLIC NVCVStatus cvcudaAdvCvtColorCreate(NVCVOperatorHandle *handle); * @param [in] code color conversion code see \p NVCVColorConversionCode group. * The following conversion codes are available for this operator: * - * Interleaved Y,U,V <-> R,G,B tensors are (n)HWC with C = 3 for YUV/RGB components + * Packed 4:4:4 Y,U,V <-> R,G,B tensors use kNHWC/kHWC with C = 3. + * Planar 4:4:4 tensors use kNCHW/kCHW with C = 3, one color + * component per channel plane. The output keeps the input layout. * NVCV_COLOR_YUV2BGR * NVCV_COLOR_YUV2RGB * NVCV_COLOR_BGR2YUV * NVCV_COLOR_RGB2YUV * - * Semi planar Y,U,V <-> R,G,B tensors are (n)HWC with C = 3 for RGB and C = 1. - * For YUV NV12/21 tensors H = (pixel height) * 3/2, w = (pixel width) and bottom 1/3 of the tensor contains interlaced VU data. + * Semi planar Y,U,V <-> R,G,B tensors use kNHWC/kHWC with C = 3 or + * C = 4 for RGB/BGR(A), or kNCHW/kCHW with C = 3 or C = 4 for planar + * RGB/BGR(A). NV12/21 tensors are single-channel tensors in the same + * layout family. For NV12/21, H = (pixel height) * 3/2, W = pixel + * width, and the bottom 1/3 of the tensor contains interleaved UV/VU data. * NVCV_COLOR_YUV2RGB_NV12 * NVCV_COLOR_YUV2BGR_NV12 * NVCV_COLOR_YUV2RGB_NV21 @@ -143,4 +152,6 @@ CVCUDA_PUBLIC NVCVStatus cvcudaAdvCvtColorSubmit(NVCVOperatorHandle handle, cuda } #endif +/** @} */ + #endif /* CVCUDA__ADV_CVT_COLOR_H */ diff --git a/src/cvcuda/include/cvcuda/OpAdvCvtColor.hpp b/src/cvcuda/include/cvcuda/OpAdvCvtColor.hpp index 26de68443..8a80f7653 100644 --- a/src/cvcuda/include/cvcuda/OpAdvCvtColor.hpp +++ b/src/cvcuda/include/cvcuda/OpAdvCvtColor.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -34,6 +34,8 @@ #include #include +#include + namespace cvcuda { class AdvCvtColor final : public IOperator @@ -41,40 +43,37 @@ class AdvCvtColor final : public IOperator public: explicit AdvCvtColor(); - ~AdvCvtColor(); - void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, NVCVColorConversionCode code, - nvcv::ColorSpec spec); + nvcv::ColorSpec spec) const; - virtual NVCVOperatorHandle handle() const noexcept override; + NVCVOperatorHandle handle() const noexcept override; private: - NVCVOperatorHandle m_handle; + detail::OperatorHandle m_handle; }; inline AdvCvtColor::AdvCvtColor() { - nvcv::detail::CheckThrow(cvcudaAdvCvtColorCreate(&m_handle)); - assert(m_handle); -} - -inline AdvCvtColor::~AdvCvtColor() -{ - nvcvOperatorDestroy(m_handle); - m_handle = nullptr; + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaAdvCvtColorCreate(&h)); + assert(h); + m_handle = detail::OperatorHandle{h}; } inline void AdvCvtColor::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, - NVCVColorConversionCode code, nvcv::ColorSpec spec) + NVCVColorConversionCode code, nvcv::ColorSpec spec) const { - nvcv::detail::CheckThrow(cvcudaAdvCvtColorSubmit(m_handle, stream, in.handle(), out.handle(), code, spec)); + nvcv::detail::CheckThrow(cvcudaAdvCvtColorSubmit(m_handle.get(), stream, in.handle(), out.handle(), code, + static_cast(spec))); } inline NVCVOperatorHandle AdvCvtColor::handle() const noexcept { - return m_handle; + return m_handle.get(); } } // namespace cvcuda +/** @} */ + #endif // CVCUDA__ADV_CVT_COLOR_HPP diff --git a/src/cvcuda/include/cvcuda/OpAutoContrast.h b/src/cvcuda/include/cvcuda/OpAutoContrast.h new file mode 100644 index 000000000..bbe3dffe3 --- /dev/null +++ b/src/cvcuda/include/cvcuda/OpAutoContrast.h @@ -0,0 +1,166 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +/** + * @file OpAutoContrast.h + * + * @brief Maximizes per-channel contrast by stretching each channel to the full dynamic range. + * + * AutoContrast remaps every channel independently so its spatial minimum maps to 0 and its spatial + * maximum maps to the data-type maximum (255 for 8-bit, 65535 for 16-bit, 1.0 for float): + * @f$ out = \mathrm{clamp}\big((in - lo) \cdot bound / (hi - lo)\big) @f$, where @p lo and @p hi + * are the per-(image, channel) minimum and maximum over the spatial extent and @p bound is the + * data-type maximum. Integer results are truncated toward zero after clamping. A channel that is + * flat (@f$ hi == lo @f$) is left unchanged. For floating-point inputs, only finite pixels + * contribute to @p lo and @p hi; NaN and infinity pixels are copied unchanged. A channel with no + * finite pixels is therefore left unchanged. + * + * Reference: mimics torchvision.transforms.v2.functional.autocontrast / PIL ImageOps.autocontrast + * (cutoff = 0) for finite inputs. The explicit non-finite handling above provides deterministic + * floating-point behavior where those references do not define a useful contrast transform. The + * remap is exact, so the result is bit-exact with an independent reference. + * + * @defgroup NVCV_C_ALGORITHM__AUTO_CONTRAST Auto Contrast + * @{ + */ + +#ifndef CVCUDA__AUTO_CONTRAST_H +#define CVCUDA__AUTO_CONTRAST_H + +#include "Operator.h" +#include "detail/Export.h" + +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + +/** Constructs an instance of the AutoContrast operator. + * + * @param [out] handle Where the operator instance handle will be written to. + * + Must not be NULL. + * + * @retval #NVCV_ERROR_INVALID_ARGUMENT Handle is null. + * @retval #NVCV_ERROR_OUT_OF_MEMORY Not enough memory to create the operator. + * @retval #NVCV_SUCCESS Operation executed successfully. + */ +CVCUDA_PUBLIC NVCVStatus cvcudaAutoContrastCreate(NVCVOperatorHandle *handle); + +/** Executes the AutoContrast operation on the given cuda stream. This operation does not + * wait for completion. + * + * Limitations: + * + * CUDA stream capture is not supported. + * + * Input: + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Number: At most 65535 samples + * Channels: [1, 3, 4] + * Element Type: Scalar; channels are represented by the C dimension + * Pixel Strides: Width and channel dimensions must be packed; rows and samples may be padded + * Addressing: Dynamic byte strides and the maximum byte offset must fit signed 32-bit + * Width: At most 2147483647 pixels + * Height: At most 262140 pixels + * + * Data Type | Allowed + * -------------- | ------------- + * 8bit Unsigned | Yes + * 8bit Signed | No + * 16bit Unsigned | Yes + * 16bit Signed | No + * 32bit Unsigned | No + * 32bit Signed | No + * 32bit Float | Yes + * 64bit Float | No + * + * Output: + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 3, 4] + * Addressing: Dynamic byte strides and the maximum byte offset must fit signed 32-bit + * + * Data Type | Allowed + * -------------- | ------------- + * 8bit Unsigned | Yes + * 8bit Signed | No + * 16bit Unsigned | Yes + * 16bit Signed | No + * 32bit Unsigned | No + * 32bit Signed | No + * 32bit Float | Yes + * 64bit Float | No + * + * Input/Output dependency + * + * Property | Input == Output + * -------------- | ------------- + * Data Layout | Yes + * Data Type | Yes + * Number | Yes + * Channels | Yes + * Width | Yes + * Height | Yes + * + * @param [in] handle Handle to the operator. + * + Must not be NULL. + * @param [in] stream Handle to a valid CUDA stream. + * + * @param [in] in input tensor. + * + * @param [out] out output tensor. + * + * @retval #NVCV_ERROR_INVALID_ARGUMENT Some parameter is outside valid range. + * @retval #NVCV_ERROR_INVALID_OPERATION The CUDA stream is being captured. + * @retval #NVCV_ERROR_OVERFLOW A dynamic stride or maximum byte offset exceeds signed 32-bit addressing. + * @retval #NVCV_ERROR_INTERNAL Internal error in the operator, invalid types passed in. + * @retval #NVCV_SUCCESS Operation executed successfully. + */ +CVCUDA_PUBLIC NVCVStatus cvcudaAutoContrastSubmit(NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle in, + NVCVTensorHandle out); + +/** Executes the AutoContrast operation on a batch of variable-shaped images on the given cuda stream. + * Same data type, channel, and maximum-height (262140 pixels) limitations as cvcudaAutoContrastSubmit. + * A batch may contain at most 65535 images. + * Images must be either single-plane packed or full-resolution planar with one scalar plane per channel. + * Chroma-subsampled, semi-planar, macro-pixel, and extra-channel formats are not supported. + * Each image plane's maximum byte offset must fit signed 32-bit addressing. + * CUDA stream capture is not supported. + * + * @param [in] handle Handle to the operator. Must not be NULL. + * @param [in] stream Handle to a valid CUDA stream. + * @param [in] in input image batch. + * @param [out] out output image batch. + * + * @retval #NVCV_ERROR_INVALID_ARGUMENT Some parameter is outside valid range. + * @retval #NVCV_ERROR_INVALID_OPERATION The CUDA stream is being captured. + * @retval #NVCV_ERROR_OVERFLOW An image-plane maximum byte offset exceeds signed 32-bit addressing. + * @retval #NVCV_ERROR_INTERNAL Internal error in the operator, invalid types passed in. + * @retval #NVCV_SUCCESS Operation executed successfully. + */ +CVCUDA_PUBLIC NVCVStatus cvcudaAutoContrastVarShapeSubmit(NVCVOperatorHandle handle, cudaStream_t stream, + NVCVImageBatchHandle in, NVCVImageBatchHandle out); + +#ifdef __cplusplus +} +#endif + +#endif /* CVCUDA__AUTO_CONTRAST_H */ diff --git a/src/cvcuda/include/cvcuda/OpAutoContrast.hpp b/src/cvcuda/include/cvcuda/OpAutoContrast.hpp new file mode 100644 index 000000000..4c945e228 --- /dev/null +++ b/src/cvcuda/include/cvcuda/OpAutoContrast.hpp @@ -0,0 +1,81 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +/** + * @file OpAutoContrast.hpp + * + * @brief Defines the public C++ Class for the AutoContrast operation. + * @defgroup NVCV_CPP_ALGORITHM__AUTO_CONTRAST AutoContrast + * @{ + */ + +#ifndef CVCUDA__AUTO_CONTRAST_HPP +#define CVCUDA__AUTO_CONTRAST_HPP + +#include "IOperator.hpp" +#include "OpAutoContrast.h" + +#include +#include +#include +#include +#include + +namespace cvcuda { + +class AutoContrast final : public IOperator +{ +public: + explicit AutoContrast(); + + void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out) const; + + void operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out) const; + + NVCVOperatorHandle handle() const noexcept override; + +private: + detail::OperatorHandle m_handle; +}; + +inline AutoContrast::AutoContrast() +{ + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaAutoContrastCreate(&h)); + assert(h); + m_handle = detail::OperatorHandle{h}; +} + +inline void AutoContrast::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out) const +{ + nvcv::detail::CheckThrow(cvcudaAutoContrastSubmit(m_handle.get(), stream, in.handle(), out.handle())); +} + +inline void AutoContrast::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, + const nvcv::ImageBatchVarShape &out) const +{ + nvcv::detail::CheckThrow(cvcudaAutoContrastVarShapeSubmit(m_handle.get(), stream, in.handle(), out.handle())); +} + +inline NVCVOperatorHandle AutoContrast::handle() const noexcept +{ + return m_handle.get(); +} + +} // namespace cvcuda + +#endif // CVCUDA__AUTO_CONTRAST_HPP diff --git a/src/cvcuda/include/cvcuda/OpAverageBlur.h b/src/cvcuda/include/cvcuda/OpAverageBlur.h index ee6d5b971..cc2913a08 100644 --- a/src/cvcuda/include/cvcuda/OpAverageBlur.h +++ b/src/cvcuda/include/cvcuda/OpAverageBlur.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -63,8 +63,10 @@ CVCUDA_PUBLIC NVCVStatus cvcudaAverageBlurCreate(NVCVOperatorHandle *handle, int * Limitations: * * Input: - * Data Layout: [kNHWC, kHWC] - * Channels: [1, 3, 4] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 3, 4] (planar kNCHW/kCHW: [1, 3, 4]) + * Planar tensors must be tightly packed with N*C <= 65535; planar var-shape batches require + * numImages <= 65535. * * Data Type | Allowed * -------------- | ------------- @@ -74,12 +76,15 @@ CVCUDA_PUBLIC NVCVStatus cvcudaAverageBlurCreate(NVCVOperatorHandle *handle, int * 16bit Signed | Yes * 32bit Unsigned | No * 32bit Signed | Yes + * 16bit Float | No * 32bit Float | Yes * 64bit Float | No * * Output: - * Data Layout: [kNHWC, kHWC] - * Channels: [1, 3, 4] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 3, 4] (planar kNCHW/kCHW: [1, 3, 4]) + * Planar tensors must be tightly packed with N*C <= 65535; planar var-shape batches require + * numImages <= 65535. * * Data Type | Allowed * -------------- | ------------- @@ -89,6 +94,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaAverageBlurCreate(NVCVOperatorHandle *handle, int * 16bit Signed | Yes * 32bit Unsigned | No * 32bit Signed | Yes + * 16bit Float | No * 32bit Float | Yes * 64bit Float | No * @@ -152,4 +158,6 @@ CVCUDA_PUBLIC NVCVStatus cvcudaAverageBlurVarShapeSubmit(NVCVOperatorHandle hand } #endif +/** @} */ + #endif /* CVCUDA_AVERAGEBLUR_H */ diff --git a/src/cvcuda/include/cvcuda/OpAverageBlur.hpp b/src/cvcuda/include/cvcuda/OpAverageBlur.hpp index a44874614..e2525e07a 100644 --- a/src/cvcuda/include/cvcuda/OpAverageBlur.hpp +++ b/src/cvcuda/include/cvcuda/OpAverageBlur.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -36,6 +36,8 @@ #include #include +#include + namespace cvcuda { class AverageBlur final : public IOperator @@ -43,53 +45,48 @@ class AverageBlur final : public IOperator public: explicit AverageBlur(nvcv::Size2D maxKernelSize, int32_t maxVarShapeBatchSize); - ~AverageBlur(); - void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, nvcv::Size2D kernelSize, - int2 kernelAnchor, NVCVBorderType borderMode); + int2 kernelAnchor, NVCVBorderType borderMode) const; void operator()(cudaStream_t stream, const nvcv::ImageBatch &in, const nvcv::ImageBatch &out, - const nvcv::Tensor &kernelSize, const nvcv::Tensor &kernelAnchor, NVCVBorderType borderMode); + const nvcv::Tensor &kernelSize, const nvcv::Tensor &kernelAnchor, NVCVBorderType borderMode) const; - virtual NVCVOperatorHandle handle() const noexcept override; + NVCVOperatorHandle handle() const noexcept override; private: - NVCVOperatorHandle m_handle; + detail::OperatorHandle m_handle; }; inline AverageBlur::AverageBlur(nvcv::Size2D maxKernelSize, int32_t maxVarShapeBatchSize) { - nvcv::detail::CheckThrow( - cvcudaAverageBlurCreate(&m_handle, maxKernelSize.w, maxKernelSize.h, maxVarShapeBatchSize)); - assert(m_handle); -} - -inline AverageBlur::~AverageBlur() -{ - nvcvOperatorDestroy(m_handle); - m_handle = nullptr; + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaAverageBlurCreate(&h, maxKernelSize.w, maxKernelSize.h, maxVarShapeBatchSize)); + assert(h); + m_handle = detail::OperatorHandle{h}; } inline void AverageBlur::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, - nvcv::Size2D kernelSize, int2 kernelAnchor, NVCVBorderType borderMode) + nvcv::Size2D kernelSize, int2 kernelAnchor, NVCVBorderType borderMode) const { - nvcv::detail::CheckThrow(cvcudaAverageBlurSubmit(m_handle, stream, in.handle(), out.handle(), kernelSize.w, + nvcv::detail::CheckThrow(cvcudaAverageBlurSubmit(m_handle.get(), stream, in.handle(), out.handle(), kernelSize.w, kernelSize.h, kernelAnchor.x, kernelAnchor.y, borderMode)); } inline void AverageBlur::operator()(cudaStream_t stream, const nvcv::ImageBatch &in, const nvcv::ImageBatch &out, const nvcv::Tensor &kernelSize, const nvcv::Tensor &kernelAnchor, - NVCVBorderType borderMode) + NVCVBorderType borderMode) const { - nvcv::detail::CheckThrow(cvcudaAverageBlurVarShapeSubmit(m_handle, stream, in.handle(), out.handle(), + nvcv::detail::CheckThrow(cvcudaAverageBlurVarShapeSubmit(m_handle.get(), stream, in.handle(), out.handle(), kernelSize.handle(), kernelAnchor.handle(), borderMode)); } inline NVCVOperatorHandle AverageBlur::handle() const noexcept { - return m_handle; + return m_handle.get(); } } // namespace cvcuda +/** @} */ + #endif // CVCUDA_AVERAGEBLUR_HPP diff --git a/src/cvcuda/include/cvcuda/OpBilateralFilter.h b/src/cvcuda/include/cvcuda/OpBilateralFilter.h index 734471984..620cdb77a 100644 --- a/src/cvcuda/include/cvcuda/OpBilateralFilter.h +++ b/src/cvcuda/include/cvcuda/OpBilateralFilter.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -60,34 +60,36 @@ CVCUDA_PUBLIC NVCVStatus cvcudaBilateralFilterCreate(NVCVOperatorHandle *handle) * Destination must be same format and size as source * * Input: - * Data Layout: [kNHWC, kHWC] - * Channels: [1, 3, 4] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 2, 3, 4] (planar kNCHW/kCHW: [1, 3, 4]) * * Data Type | Allowed * -------------- | ------------- * 8bit Unsigned | Yes - * 8bit Signed | Yes + * 8bit Signed | No * 16bit Unsigned | Yes * 16bit Signed | Yes - * 32bit Unsigned | Yes + * 32bit Unsigned | No * 32bit Signed | Yes + * 16bit Float | No * 32bit Float | Yes - * 64bit Float | Yes + * 64bit Float | No * * Output: - * Data Layout: [kNHWC, kHWC] - * Channels: [1, 3, 4] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 2, 3, 4] (planar kNCHW/kCHW: [1, 3, 4]) * * Data Type | Allowed * -------------- | ------------- * 8bit Unsigned | Yes - * 8bit Signed | Yes + * 8bit Signed | No * 16bit Unsigned | Yes * 16bit Signed | Yes - * 32bit Unsigned | Yes + * 32bit Unsigned | No * 32bit Signed | Yes + * 16bit Float | No * 32bit Float | Yes - * 64bit Float | Yes + * 64bit Float | No * * Input/Output dependency * @@ -134,4 +136,6 @@ CVCUDA_PUBLIC NVCVStatus cvcudaBilateralFilterVarShapeSubmit(NVCVOperatorHandle } #endif +/** @} */ + #endif /* CVCUDA_BILATERAL_FILTER_H */ diff --git a/src/cvcuda/include/cvcuda/OpBilateralFilter.hpp b/src/cvcuda/include/cvcuda/OpBilateralFilter.hpp index 89fd17958..f4133d886 100644 --- a/src/cvcuda/include/cvcuda/OpBilateralFilter.hpp +++ b/src/cvcuda/include/cvcuda/OpBilateralFilter.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -36,6 +36,8 @@ #include #include +#include + namespace cvcuda { class BilateralFilter final : public IOperator @@ -43,54 +45,51 @@ class BilateralFilter final : public IOperator public: explicit BilateralFilter(); - ~BilateralFilter(); - void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, int diameter, - float sigmaColor, float sigmaSpace, NVCVBorderType borderMode); + float sigmaColor, float sigmaSpace, NVCVBorderType borderMode) const; void operator()(cudaStream_t stream, const nvcv::ImageBatch &in, const nvcv::ImageBatch &out, const nvcv::Tensor &diameterData, const nvcv::Tensor &sigmaColorData, - const nvcv::Tensor &sigmaSpace, NVCVBorderType borderMode); + const nvcv::Tensor &sigmaSpace, NVCVBorderType borderMode) const; - virtual NVCVOperatorHandle handle() const noexcept override; + NVCVOperatorHandle handle() const noexcept override; private: - NVCVOperatorHandle m_handle; + detail::OperatorHandle m_handle; }; inline BilateralFilter::BilateralFilter() { - nvcv::detail::CheckThrow(cvcudaBilateralFilterCreate(&m_handle)); - assert(m_handle); -} - -inline BilateralFilter::~BilateralFilter() -{ - nvcvOperatorDestroy(m_handle); - m_handle = nullptr; + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaBilateralFilterCreate(&h)); + assert(h); + m_handle = detail::OperatorHandle{h}; } inline void BilateralFilter::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, - int diameter, float sigmaColor, float sigmaSpace, NVCVBorderType borderMode) + int diameter, float sigmaColor, float sigmaSpace, + NVCVBorderType borderMode) const { - nvcv::detail::CheckThrow(cvcudaBilateralFilterSubmit(m_handle, stream, in.handle(), out.handle(), diameter, + nvcv::detail::CheckThrow(cvcudaBilateralFilterSubmit(m_handle.get(), stream, in.handle(), out.handle(), diameter, sigmaColor, sigmaSpace, borderMode)); } inline void BilateralFilter::operator()(cudaStream_t stream, const nvcv::ImageBatch &in, const nvcv::ImageBatch &out, const nvcv::Tensor &diameterData, const nvcv::Tensor &sigmaColorData, - const nvcv::Tensor &sigmaSpaceData, NVCVBorderType borderMode) + const nvcv::Tensor &sigmaSpaceData, NVCVBorderType borderMode) const { - nvcv::detail::CheckThrow(cvcudaBilateralFilterVarShapeSubmit(m_handle, stream, in.handle(), out.handle(), + nvcv::detail::CheckThrow(cvcudaBilateralFilterVarShapeSubmit(m_handle.get(), stream, in.handle(), out.handle(), diameterData.handle(), sigmaColorData.handle(), sigmaSpaceData.handle(), borderMode)); } inline NVCVOperatorHandle BilateralFilter::handle() const noexcept { - return m_handle; + return m_handle.get(); } } // namespace cvcuda +/** @} */ + #endif // CVCUDA_BILATERAL_FILTER_HPP diff --git a/src/cvcuda/include/cvcuda/OpBndBox.h b/src/cvcuda/include/cvcuda/OpBndBox.h index e6aaadaac..4ca5b4433 100644 --- a/src/cvcuda/include/cvcuda/OpBndBox.h +++ b/src/cvcuda/include/cvcuda/OpBndBox.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -56,7 +56,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaBndBoxCreate(NVCVOperatorHandle *handle); * Limitations: * * Input: - * Data Layout: [kNHWC, kHWC] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] * Channels: [3, 4] * * Data Type | Allowed @@ -67,11 +67,12 @@ CVCUDA_PUBLIC NVCVStatus cvcudaBndBoxCreate(NVCVOperatorHandle *handle); * 16bit Signed | No * 32bit Unsigned | No * 32bit Signed | No + * 16bit Float | No * 32bit Float | No * 64bit Float | No * * Output: - * Data Layout: [kNHWC, kHWC] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] * Channels: [3, 4] * * Data Type | Allowed @@ -82,6 +83,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaBndBoxCreate(NVCVOperatorHandle *handle); * 16bit Signed | No * 32bit Unsigned | No * 32bit Signed | No + * 16bit Float | No * 32bit Float | No * 64bit Float | No * @@ -117,4 +119,6 @@ CVCUDA_PUBLIC NVCVStatus cvcudaBndBoxSubmit(NVCVOperatorHandle handle, cudaStrea } #endif +/** @} */ + #endif /* CVCUDA__BND_BOX_H */ diff --git a/src/cvcuda/include/cvcuda/OpBndBox.hpp b/src/cvcuda/include/cvcuda/OpBndBox.hpp index 6a77580bf..0f09d3dab 100644 --- a/src/cvcuda/include/cvcuda/OpBndBox.hpp +++ b/src/cvcuda/include/cvcuda/OpBndBox.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -34,6 +34,8 @@ #include #include +#include + namespace cvcuda { class BndBox final : public IOperator @@ -41,39 +43,36 @@ class BndBox final : public IOperator public: explicit BndBox(); - ~BndBox(); - - void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, const NVCVBndBoxesI bboxes); + void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, + const NVCVBndBoxesI bboxes) const; - virtual NVCVOperatorHandle handle() const noexcept override; + NVCVOperatorHandle handle() const noexcept override; private: - NVCVOperatorHandle m_handle; + detail::OperatorHandle m_handle; }; inline BndBox::BndBox() { - nvcv::detail::CheckThrow(cvcudaBndBoxCreate(&m_handle)); - assert(m_handle); -} - -inline BndBox::~BndBox() -{ - nvcvOperatorDestroy(m_handle); - m_handle = nullptr; + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaBndBoxCreate(&h)); + assert(h); + m_handle = detail::OperatorHandle{h}; } inline void BndBox::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, - const NVCVBndBoxesI bboxes) + const NVCVBndBoxesI bboxes) const { - nvcv::detail::CheckThrow(cvcudaBndBoxSubmit(m_handle, stream, in.handle(), out.handle(), bboxes)); + nvcv::detail::CheckThrow(cvcudaBndBoxSubmit(m_handle.get(), stream, in.handle(), out.handle(), bboxes)); } inline NVCVOperatorHandle BndBox::handle() const noexcept { - return m_handle; + return m_handle.get(); } } // namespace cvcuda +/** @} */ + #endif // CVCUDA__BND_BOX_HPP diff --git a/src/cvcuda/include/cvcuda/OpBoxBlur.h b/src/cvcuda/include/cvcuda/OpBoxBlur.h index 566ecd526..119fbec24 100644 --- a/src/cvcuda/include/cvcuda/OpBoxBlur.h +++ b/src/cvcuda/include/cvcuda/OpBoxBlur.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -39,9 +39,9 @@ extern "C" { #endif -/** Constructs and an instance of the BoxBlur operator. +/** Constructs an instance of the BoxBlur operator. * - * @param [out] handle Where the image instance handle will be written to. + * @param [out] handle Where the operator instance handle will be written to. * + Must not be NULL. * * @retval #NVCV_ERROR_INVALID_ARGUMENT Handle is null. @@ -56,8 +56,8 @@ CVCUDA_PUBLIC NVCVStatus cvcudaBoxBlurCreate(NVCVOperatorHandle *handle); * Limitations: * * Input: - * Data Layout: [kNHWC, kHWC] - * Channels: [3, 4] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [3, 4] (planar kNCHW/kCHW: [3, 4]) * * Data Type | Allowed * -------------- | ------------- @@ -67,12 +67,13 @@ CVCUDA_PUBLIC NVCVStatus cvcudaBoxBlurCreate(NVCVOperatorHandle *handle); * 16bit Signed | No * 32bit Unsigned | No * 32bit Signed | No + * 16bit Float | No * 32bit Float | No * 64bit Float | No * * Output: - * Data Layout: [kNHWC, kHWC] - * Channels: [3, 4] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [3, 4] (planar kNCHW/kCHW: [3, 4]) * * Data Type | Allowed * -------------- | ------------- @@ -82,6 +83,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaBoxBlurCreate(NVCVOperatorHandle *handle); * 16bit Signed | No * 32bit Unsigned | No * 32bit Signed | No + * 16bit Float | No * 32bit Float | No * 64bit Float | No * @@ -96,15 +98,15 @@ CVCUDA_PUBLIC NVCVStatus cvcudaBoxBlurCreate(NVCVOperatorHandle *handle); * Width | Yes * Height | Yes * - * @param [in] handle Handle to the operator. + * @param [in] handle Handle to the operator instance created by #cvcudaBoxBlurCreate. * + Must not be NULL. - * @param [in] stream Handle to a valid CUDA stream. + * @param [in] stream CUDA stream on which the operation is enqueued; execution may continue asynchronously. * - * @param [in] in intput tensor. + * @param [in] in Input tensor that supplies source pixels. It remains unchanged when @p out is a distinct tensor. * - * @param [out] out output tensor. + * @param [out] out Output tensor. Its shape, layout, and data type must match the input; it may alias the input. * - * @param [in] bboxes blur boxes in reference to the input tensor. + * @param [in] bboxes Per-sample blur boxes in input-tensor coordinates; its batch count must match the input batch. * * @retval #NVCV_ERROR_INVALID_ARGUMENT Some parameter is outside valid range. * @retval #NVCV_ERROR_INTERNAL Internal error in the operator, invalid types passed in. @@ -117,4 +119,6 @@ CVCUDA_PUBLIC NVCVStatus cvcudaBoxBlurSubmit(NVCVOperatorHandle handle, cudaStre } #endif +/** @} */ + #endif /* CVCUDA__BOX_BLUR_H */ diff --git a/src/cvcuda/include/cvcuda/OpBoxBlur.hpp b/src/cvcuda/include/cvcuda/OpBoxBlur.hpp index 66ce5a40d..295cd5730 100644 --- a/src/cvcuda/include/cvcuda/OpBoxBlur.hpp +++ b/src/cvcuda/include/cvcuda/OpBoxBlur.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -34,6 +34,8 @@ #include #include +#include + namespace cvcuda { class BoxBlur final : public IOperator @@ -41,39 +43,36 @@ class BoxBlur final : public IOperator public: explicit BoxBlur(); - ~BoxBlur(); - - void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, const NVCVBlurBoxesI bboxes); + void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, + const NVCVBlurBoxesI bboxes) const; - virtual NVCVOperatorHandle handle() const noexcept override; + NVCVOperatorHandle handle() const noexcept override; private: - NVCVOperatorHandle m_handle; + detail::OperatorHandle m_handle; }; inline BoxBlur::BoxBlur() { - nvcv::detail::CheckThrow(cvcudaBoxBlurCreate(&m_handle)); - assert(m_handle); -} - -inline BoxBlur::~BoxBlur() -{ - nvcvOperatorDestroy(m_handle); - m_handle = nullptr; + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaBoxBlurCreate(&h)); + assert(h); + m_handle = detail::OperatorHandle{h}; } inline void BoxBlur::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, - const NVCVBlurBoxesI bboxes) + const NVCVBlurBoxesI bboxes) const { - nvcv::detail::CheckThrow(cvcudaBoxBlurSubmit(m_handle, stream, in.handle(), out.handle(), bboxes)); + nvcv::detail::CheckThrow(cvcudaBoxBlurSubmit(m_handle.get(), stream, in.handle(), out.handle(), bboxes)); } inline NVCVOperatorHandle BoxBlur::handle() const noexcept { - return m_handle; + return m_handle.get(); } } // namespace cvcuda +/** @} */ + #endif // CVCUDA__BOX_BLUR_HPP diff --git a/src/cvcuda/include/cvcuda/OpBrightnessContrast.h b/src/cvcuda/include/cvcuda/OpBrightnessContrast.h index aa591117d..9675e4db8 100644 --- a/src/cvcuda/include/cvcuda/OpBrightnessContrast.h +++ b/src/cvcuda/include/cvcuda/OpBrightnessContrast.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -64,6 +64,8 @@ CVCUDA_PUBLIC NVCVStatus cvcudaBrightnessContrastCreate(NVCVOperatorHandle *hand * Limitations: * * Input: + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 2, 3, 4] * * Data Type | Allowed * -------------- | ------------- @@ -73,10 +75,13 @@ CVCUDA_PUBLIC NVCVStatus cvcudaBrightnessContrastCreate(NVCVOperatorHandle *hand * 16bit Signed | Yes * 32bit Unsigned | No * 32bit Signed | Yes + * 16bit Float | No * 32bit Float | Yes * 64bit Float | No * * Output: + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 2, 3, 4] * * Data Type | Allowed * -------------- | ------------- @@ -86,6 +91,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaBrightnessContrastCreate(NVCVOperatorHandle *hand * 16bit Signed | Yes * 32bit Unsigned | No * 32bit Signed | Yes + * 16bit Float | No * 32bit Float | Yes * 64bit Float | No * @@ -162,8 +168,43 @@ CVCUDA_PUBLIC NVCVStatus cvcudaBrightnessContrastVarShapeSubmit(NVCVOperatorHand NVCVTensorHandle brightnessShift, NVCVTensorHandle contrastCenter); +/** Executes BrightnessContrast with one set of parameters supplied by value for every input image. + * + * The affine formula and input/output limitations are the same as \ref cvcudaBrightnessContrastSubmit. Unlike the + * tensor-parameter entry point, this path requires no parameter tensors or host-to-device copies. + * + * @param [in] handle Handle to the operator. + * @param [in] stream Handle to a valid CUDA stream. + * @param [in] in Input tensor. + * @param [out] out Output tensor. + * @param [in] brightness Brightness multiplier. + * @param [in] contrast Contrast multiplier. + * @param [in] brightnessShift Brightness shift. + * @param [in] contrastCenter Contrast center. + * @param [in] clamp If true, clamp to the nominal image range before conversion: `[0, 1]` for floating-point output + * and `[0, max]` for integer output. If false, preserve the existing BrightnessContrast behavior. + */ +CVCUDA_PUBLIC NVCVStatus cvcudaBrightnessContrastScalarSubmit(NVCVOperatorHandle handle, cudaStream_t stream, + NVCVTensorHandle in, NVCVTensorHandle out, + double brightness, double contrast, + double brightnessShift, double contrastCenter, + bool clamp); + +/** Executes the by-value BrightnessContrast path on a variable-shape image batch. + * + * Apart from input and output image batches, all parameters are the same as + * \ref cvcudaBrightnessContrastScalarSubmit. + */ +CVCUDA_PUBLIC NVCVStatus cvcudaBrightnessContrastVarShapeScalarSubmit(NVCVOperatorHandle handle, cudaStream_t stream, + NVCVImageBatchHandle in, NVCVImageBatchHandle out, + double brightness, double contrast, + double brightnessShift, double contrastCenter, + bool clamp); + #ifdef __cplusplus } #endif +/** @} */ + #endif /* CVCUDA_BRIGHTNESS_CONTRAST_H */ diff --git a/src/cvcuda/include/cvcuda/OpBrightnessContrast.hpp b/src/cvcuda/include/cvcuda/OpBrightnessContrast.hpp index 805aa54dc..cd9220322 100644 --- a/src/cvcuda/include/cvcuda/OpBrightnessContrast.hpp +++ b/src/cvcuda/include/cvcuda/OpBrightnessContrast.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -34,6 +34,8 @@ #include #include +#include + namespace cvcuda { class BrightnessContrast final : public IOperator @@ -41,56 +43,79 @@ class BrightnessContrast final : public IOperator public: explicit BrightnessContrast(); - ~BrightnessContrast(); - void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, const nvcv::Tensor &brightness, const nvcv::Tensor &contrast, const nvcv::Tensor &brightnessShift, - const nvcv::Tensor &contrastCenter); + const nvcv::Tensor &contrastCenter) const; void operator()(cudaStream_t stream, const nvcv::ImageBatch &in, const nvcv::ImageBatch &out, const nvcv::Tensor &brightness, const nvcv::Tensor &contrast, const nvcv::Tensor &brightnessShift, - const nvcv::Tensor &contrastCenter); + const nvcv::Tensor &contrastCenter) const; + + void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, double brightness, + double contrast, double brightnessShift, double contrastCenter, bool clamp = false) const; + + void operator()(cudaStream_t stream, const nvcv::ImageBatch &in, const nvcv::ImageBatch &out, double brightness, + double contrast, double brightnessShift, double contrastCenter, bool clamp = false) const; - virtual NVCVOperatorHandle handle() const noexcept override; + NVCVOperatorHandle handle() const noexcept override; private: - NVCVOperatorHandle m_handle; + detail::OperatorHandle m_handle; }; inline BrightnessContrast::BrightnessContrast() { - nvcv::detail::CheckThrow(cvcudaBrightnessContrastCreate(&m_handle)); - assert(m_handle); -} - -inline BrightnessContrast::~BrightnessContrast() -{ - nvcvOperatorDestroy(m_handle); + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaBrightnessContrastCreate(&h)); + assert(h); + m_handle = detail::OperatorHandle{h}; } inline void BrightnessContrast::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, const nvcv::Tensor &brightness, const nvcv::Tensor &contrast, - const nvcv::Tensor &brightnessShift, const nvcv::Tensor &contrastCenter) + const nvcv::Tensor &brightnessShift, + const nvcv::Tensor &contrastCenter) const { - nvcv::detail::CheckThrow(cvcudaBrightnessContrastSubmit(m_handle, stream, in.handle(), out.handle(), + nvcv::detail::CheckThrow(cvcudaBrightnessContrastSubmit(m_handle.get(), stream, in.handle(), out.handle(), brightness.handle(), contrast.handle(), brightnessShift.handle(), contrastCenter.handle())); } inline void BrightnessContrast::operator()(cudaStream_t stream, const nvcv::ImageBatch &in, const nvcv::ImageBatch &out, const nvcv::Tensor &brightness, const nvcv::Tensor &contrast, - const nvcv::Tensor &brightnessShift, const nvcv::Tensor &contrastCenter) + const nvcv::Tensor &brightnessShift, + const nvcv::Tensor &contrastCenter) const { - nvcv::detail::CheckThrow(cvcudaBrightnessContrastVarShapeSubmit(m_handle, stream, in.handle(), out.handle(), + nvcv::detail::CheckThrow(cvcudaBrightnessContrastVarShapeSubmit(m_handle.get(), stream, in.handle(), out.handle(), brightness.handle(), contrast.handle(), brightnessShift.handle(), contrastCenter.handle())); } +inline void BrightnessContrast::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, + double brightness, double contrast, double brightnessShift, + double contrastCenter, bool clamp) const +{ + nvcv::detail::CheckThrow(cvcudaBrightnessContrastScalarSubmit(m_handle.get(), stream, in.handle(), out.handle(), + brightness, contrast, brightnessShift, contrastCenter, + clamp)); +} + +inline void BrightnessContrast::operator()(cudaStream_t stream, const nvcv::ImageBatch &in, const nvcv::ImageBatch &out, + double brightness, double contrast, double brightnessShift, + double contrastCenter, bool clamp) const +{ + nvcv::detail::CheckThrow(cvcudaBrightnessContrastVarShapeScalarSubmit(m_handle.get(), stream, in.handle(), + out.handle(), brightness, contrast, + brightnessShift, contrastCenter, clamp)); +} + inline NVCVOperatorHandle BrightnessContrast::handle() const noexcept { - return m_handle; + return m_handle.get(); } } // namespace cvcuda +/** @} */ + #endif // CVCUDA__BRIGHTNESS_CONTRAST_HPP diff --git a/src/cvcuda/include/cvcuda/OpCLAHE.h b/src/cvcuda/include/cvcuda/OpCLAHE.h new file mode 100644 index 000000000..f6230a7bc --- /dev/null +++ b/src/cvcuda/include/cvcuda/OpCLAHE.h @@ -0,0 +1,143 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +/** + * @file OpCLAHE.h + * + * @brief Defines types and functions to handle CLAHE operation. + * @defgroup NVCV_C_ALGORITHM__CLAHE CLAHE + * @{ + */ + +#ifndef CVCUDA__CLAHE_H +#define CVCUDA__CLAHE_H + +#include "Operator.h" +#include "detail/Export.h" + +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + +/** Constructs an instance of the CLAHE operator. + * + * @param [out] handle Where the operator handle will be written to. + * + Must not be NULL. + * @param [in] maxBatchSize The maximum batch size this operator will process. + * + Must be >= 1. + * @param [in] tilesX Number of tiles along width. + * + Must be >= 1. + * @param [in] tilesY Number of tiles along height. + * + Must be >= 1. + * + * @retval #NVCV_ERROR_INVALID_ARGUMENT Handle is null or maxBatchSize is invalid. + * @retval #NVCV_ERROR_OUT_OF_MEMORY Not enough memory to create the operator. + * @retval #NVCV_SUCCESS Operation executed successfully. + */ +CVCUDA_PUBLIC NVCVStatus cvcudaCLAHECreate(NVCVOperatorHandle *handle, int32_t maxBatchSize, int32_t tilesX, + int32_t tilesY); + +/** Executes CLAHE on a tensor input/output on the given cuda stream. + * + * Limitations: + * + * Input: + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1] + * + * Data Type | Allowed + * -------------- | ------------- + * 8bit Unsigned | Yes + * 8bit Signed | No + * 16bit Unsigned | No + * 16bit Signed | No + * 32bit Unsigned | No + * 32bit Signed | No + * 32bit Float | No + * 64bit Float | No + * + * Output: + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1] + * + * Data Type | Allowed + * -------------- | ------------- + * 8bit Unsigned | Yes + * 8bit Signed | No + * 16bit Unsigned | No + * 16bit Signed | No + * 32bit Unsigned | No + * 32bit Signed | No + * 32bit Float | No + * 64bit Float | No + * + * Input/Output dependency + * + * Property | Input == Output + * -------------- | ------------- + * Data Layout | Yes + * Data Type | Yes + * Number | Yes + * Channels | Yes + * Width | Yes + * Height | Yes + * + * @param [in] handle Handle to the operator. + * + Must not be NULL. + * @param [in] stream Handle to a valid CUDA stream. + * @param [in] in Input tensor. + * @param [out] out Output tensor. + * @param [in] clipLimit CLAHE clip limit value. Must be > 0. + * + * @retval #NVCV_ERROR_INVALID_ARGUMENT Some parameter is outside valid range. + * @retval #NVCV_ERROR_INTERNAL Internal error in the operator. + * @retval #NVCV_SUCCESS Operation executed successfully. + */ +CVCUDA_PUBLIC NVCVStatus cvcudaCLAHESubmit(NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle in, + NVCVTensorHandle out, float clipLimit); + +/** Executes CLAHE on a varshape image batch on the given cuda stream. + * + * Input and output images must be single-channel U8 and have matching per-image + * shape and format. + * + * @param [in] handle Handle to the operator. + * @param [in] stream Handle to a valid CUDA stream. + * @param [in] in Input image batch. + * @param [out] out Output image batch. + * @param [in] clipLimit CLAHE clip limit value. Must be > 0. + * + * @retval #NVCV_ERROR_INVALID_ARGUMENT Some parameter is outside valid range. + * @retval #NVCV_ERROR_INTERNAL Internal error in the operator. + * @retval #NVCV_SUCCESS Operation executed successfully. + */ +CVCUDA_PUBLIC NVCVStatus cvcudaCLAHEVarShapeSubmit(NVCVOperatorHandle handle, cudaStream_t stream, + NVCVImageBatchHandle in, NVCVImageBatchHandle out, float clipLimit); + +#ifdef __cplusplus +} +#endif + +/** @} */ + +#endif /* CVCUDA__CLAHE_H */ diff --git a/src/cvcuda/include/cvcuda/OpCLAHE.hpp b/src/cvcuda/include/cvcuda/OpCLAHE.hpp new file mode 100644 index 000000000..cc8a06800 --- /dev/null +++ b/src/cvcuda/include/cvcuda/OpCLAHE.hpp @@ -0,0 +1,85 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +/** + * @file OpCLAHE.hpp + * + * @brief Defines the public C++ Class for CLAHE operation. + * @defgroup NVCV_CPP_ALGORITHM__CLAHE CLAHE + * @{ + */ + +#ifndef CVCUDA__CLAHE_HPP +#define CVCUDA__CLAHE_HPP + +#include "IOperator.hpp" +#include "OpCLAHE.h" + +#include +#include +#include + +#include + +namespace cvcuda { + +class CLAHE final : public IOperator +{ +public: + explicit CLAHE(int32_t maxBatchSize, int32_t tilesX = 8, int32_t tilesY = 8); + + void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, float clipLimit = 40.f) const; + + void operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, + float clipLimit = 40.f) const; + + NVCVOperatorHandle handle() const noexcept override; + +private: + detail::OperatorHandle m_handle; +}; + +inline CLAHE::CLAHE(int32_t maxBatchSize, int32_t tilesX, int32_t tilesY) +{ + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaCLAHECreate(&h, maxBatchSize, tilesX, tilesY)); + assert(h); + m_handle = detail::OperatorHandle{h}; +} + +inline void CLAHE::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, + float clipLimit) const +{ + nvcv::detail::CheckThrow(cvcudaCLAHESubmit(m_handle.get(), stream, in.handle(), out.handle(), clipLimit)); +} + +inline void CLAHE::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, + const nvcv::ImageBatchVarShape &out, float clipLimit) const +{ + nvcv::detail::CheckThrow(cvcudaCLAHEVarShapeSubmit(m_handle.get(), stream, in.handle(), out.handle(), clipLimit)); +} + +inline NVCVOperatorHandle CLAHE::handle() const noexcept +{ + return m_handle.get(); +} + +} // namespace cvcuda + +/** @} */ + +#endif // CVCUDA__CLAHE_HPP diff --git a/src/cvcuda/include/cvcuda/OpCenterCrop.h b/src/cvcuda/include/cvcuda/OpCenterCrop.h index 33d74d976..5b11122b9 100644 --- a/src/cvcuda/include/cvcuda/OpCenterCrop.h +++ b/src/cvcuda/include/cvcuda/OpCenterCrop.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -58,8 +58,8 @@ CVCUDA_PUBLIC NVCVStatus cvcudaCenterCropCreate(NVCVOperatorHandle *handle); * ROI must be smaller than output tensor. * * Input: - * Data Layout: [kNHWC, kHWC] - * Channels: [1, 3, 4] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 2, 3, 4] (planar kNCHW/kCHW: [1, 3, 4]) * * Data Type | Allowed * -------------- | ------------- @@ -67,14 +67,15 @@ CVCUDA_PUBLIC NVCVStatus cvcudaCenterCropCreate(NVCVOperatorHandle *handle); * 8bit Signed | Yes * 16bit Unsigned | Yes * 16bit Signed | Yes - * 32bit Unsigned | Yes + * 32bit Unsigned | No * 32bit Signed | Yes + * 16bit Float | Yes * 32bit Float | Yes * 64bit Float | Yes * * Output: - * Data Layout: [kNHWC, kHWC] - * Channels: [1, 3, 4] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 2, 3, 4] (planar kNCHW/kCHW: [1, 3, 4]) * * Data Type | Allowed * -------------- | ------------- @@ -82,8 +83,9 @@ CVCUDA_PUBLIC NVCVStatus cvcudaCenterCropCreate(NVCVOperatorHandle *handle); * 8bit Signed | Yes * 16bit Unsigned | Yes * 16bit Signed | Yes - * 32bit Unsigned | Yes + * 32bit Unsigned | No * 32bit Signed | Yes + * 16bit Float | Yes * 32bit Float | Yes * 64bit Float | Yes * @@ -121,4 +123,6 @@ CVCUDA_PUBLIC NVCVStatus cvcudaCenterCropSubmit(NVCVOperatorHandle handle, cudaS } #endif +/** @} */ + #endif /* CVCUDA_CENTER_CROP_H */ diff --git a/src/cvcuda/include/cvcuda/OpCenterCrop.hpp b/src/cvcuda/include/cvcuda/OpCenterCrop.hpp index 14b2f60d8..d7da87232 100644 --- a/src/cvcuda/include/cvcuda/OpCenterCrop.hpp +++ b/src/cvcuda/include/cvcuda/OpCenterCrop.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -35,6 +35,8 @@ #include #include +#include + namespace cvcuda { class CenterCrop final : public IOperator @@ -42,40 +44,37 @@ class CenterCrop final : public IOperator public: explicit CenterCrop(); - ~CenterCrop(); - - void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, const nvcv::Size2D &cropSize); + void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, + const nvcv::Size2D &cropSize) const; - virtual NVCVOperatorHandle handle() const noexcept override; + NVCVOperatorHandle handle() const noexcept override; private: - NVCVOperatorHandle m_handle; + detail::OperatorHandle m_handle; }; inline CenterCrop::CenterCrop() { - nvcv::detail::CheckThrow(cvcudaCenterCropCreate(&m_handle)); - assert(m_handle); -} - -inline CenterCrop::~CenterCrop() -{ - nvcvOperatorDestroy(m_handle); - m_handle = nullptr; + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaCenterCropCreate(&h)); + assert(h); + m_handle = detail::OperatorHandle{h}; } inline void CenterCrop::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, - const nvcv::Size2D &cropSize) + const nvcv::Size2D &cropSize) const { nvcv::detail::CheckThrow( - cvcudaCenterCropSubmit(m_handle, stream, in.handle(), out.handle(), cropSize.w, cropSize.h)); + cvcudaCenterCropSubmit(m_handle.get(), stream, in.handle(), out.handle(), cropSize.w, cropSize.h)); } inline NVCVOperatorHandle CenterCrop::handle() const noexcept { - return m_handle; + return m_handle.get(); } } // namespace cvcuda +/** @} */ + #endif // CVCUDA_CENTER_CROP_HPP diff --git a/src/cvcuda/include/cvcuda/OpChannelReorder.h b/src/cvcuda/include/cvcuda/OpChannelReorder.h index c2ab21d2e..3d6475e40 100644 --- a/src/cvcuda/include/cvcuda/OpChannelReorder.h +++ b/src/cvcuda/include/cvcuda/OpChannelReorder.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -38,10 +38,8 @@ extern "C" { #endif -/** Constructs and an instance of the channel reorder operator. - * The operator is used to cha - * reformats the input from kNHWC format to kNCHW format or from kNCHW format to - * kNHWC format. +/** Constructs an instance of the channel reorder operator. + * The operator copies input channels to output channels according to an order tensor. * * @param [out] handle Where the image instance handle will be written to. * + Must not be NULL. @@ -52,13 +50,102 @@ extern "C" */ CVCUDA_PUBLIC NVCVStatus cvcudaChannelReorderCreate(NVCVOperatorHandle *handle); +/** Executes channel reorder on a tensor on the given CUDA stream. This operation does not + * wait for completion. + * + * For every output channel @p c, the operator copies input channel @p order[c]. A negative + * order entry writes zero to the corresponding output channel. Repeated non-negative entries + * are allowed. + * + * Reference: the non-negative gather mapping matches + * torchvision.transforms.v2.functional.permute_channels. Native ChannelReorder preserves its + * established negative-entry zero-fill behavior; callers that need Python negative indexing + * must normalize those indices before submission. + * + * Limitations: + * + * Input/Output: + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 2, 3, 4] (planar kNCHW/kCHW: [1, 3, 4]) + * + * Data Type | Allowed + * -------------- | ------------- + * 8bit Unsigned | Yes + * 8bit Signed | No + * 16bit Unsigned | Yes + * 16bit Signed | Yes + * 32bit Unsigned | No + * 32bit Signed | Yes + * 16bit Float | No + * 32bit Float | Yes + * 64bit Float | No + * + * Input and output must have identical shape, layout, data type, and channel count. + * + * @param [in] handle Handle to the operator. Must not be NULL. + * @param [in] stream Handle to a valid CUDA stream. + * @param [in] in Input tensor. + * @param [out] out Output tensor. + * @param [in] order Host pointer to @p orderLength signed 32-bit channel indices. The values are + * copied synchronously during this call and may be released after it returns. + * @param [in] orderLength Number of entries in @p order; must equal the tensor channel count. + * + * @retval #NVCV_ERROR_INVALID_ARGUMENT Some parameter is outside the valid range. + * @retval #NVCV_ERROR_INTERNAL Internal error in the operator. + * @retval #NVCV_SUCCESS Operation submitted successfully. + */ +CVCUDA_PUBLIC NVCVStatus cvcudaChannelReorderSubmit(NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle in, + NVCVTensorHandle out, const int32_t *order, int32_t orderLength); + /** Executes the reformat operation on the given cuda stream. This operation does not * wait for completion. * * Limitations: * - * * Input and output image formats must all have the same number of channels and - * have only one plane, although the channels can be swizzled (i.e. RGBA8, BGRA8, etc). + * Input: + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 2, 3, 4] (planar kNCHW/kCHW: [1, 3, 4]) + * + * Data Type | Allowed + * -------------- | ------------- + * 8bit Unsigned | Yes + * 8bit Signed | No + * 16bit Unsigned | Yes + * 16bit Signed | Yes + * 32bit Unsigned | No + * 32bit Signed | Yes + * 16bit Float | No + * 32bit Float | Yes + * 64bit Float | No + * + * Output: + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 2, 3, 4] (planar kNCHW/kCHW: [1, 3, 4]) + * + * Data Type | Allowed + * -------------- | ------------- + * 8bit Unsigned | Yes + * 8bit Signed | No + * 16bit Unsigned | Yes + * 16bit Signed | Yes + * 32bit Unsigned | No + * 32bit Signed | Yes + * 16bit Float | No + * 32bit Float | Yes + * 64bit Float | No + * + * Input/Output dependency + * + * Property | Input == Output + * -------------- | ------------- + * Data Layout | Layout family + * Data Type | Yes + * Number | Yes + * Channels | No + * + * * Input and output image formats must have the same layout family: interleaved + * formats have one plane, and planar formats have one plane per channel. + * Channels can be swizzled (i.e. RGBA8, BGRA8, RGBA8p, BGRA8p, etc). * * * The number of samples in the input and output ImageBatch must be the same * @@ -87,7 +174,8 @@ CVCUDA_PUBLIC NVCVStatus cvcudaChannelReorderCreate(NVCVOperatorHandle *handle); * The corresponding pixel in the output will be [214,0,63]. * * + Must not be NULL. - * + The order value must be >= 0 and < the number of channels in input image. + * + A non-negative order value must be less than the number of channels in the input image. + * Negative values write zero to the corresponding output channel. * + Tensor dimensions must be NxC, where N is the number of images in the input varshape, * and C is the number of channels in the output images. * @@ -103,4 +191,6 @@ CVCUDA_PUBLIC NVCVStatus cvcudaChannelReorderVarShapeSubmit(NVCVOperatorHandle h } #endif +/** @} */ + #endif /* CVCUDA_CHANNEL_REORDER_H */ diff --git a/src/cvcuda/include/cvcuda/OpChannelReorder.hpp b/src/cvcuda/include/cvcuda/OpChannelReorder.hpp index 14e88a991..ed6149779 100644 --- a/src/cvcuda/include/cvcuda/OpChannelReorder.hpp +++ b/src/cvcuda/include/cvcuda/OpChannelReorder.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -34,6 +34,8 @@ #include #include +#include + namespace cvcuda { class ChannelReorder final : public IOperator @@ -41,41 +43,47 @@ class ChannelReorder final : public IOperator public: explicit ChannelReorder(); - ~ChannelReorder(); + void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, const int32_t *order, + int32_t orderLength) const; void operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, - const nvcv::Tensor &orders); + const nvcv::Tensor &orders) const; - virtual NVCVOperatorHandle handle() const noexcept override; + NVCVOperatorHandle handle() const noexcept override; private: - NVCVOperatorHandle m_handle; + detail::OperatorHandle m_handle; }; inline ChannelReorder::ChannelReorder() { - nvcv::detail::CheckThrow(cvcudaChannelReorderCreate(&m_handle)); - assert(m_handle); + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaChannelReorderCreate(&h)); + assert(h); + m_handle = detail::OperatorHandle{h}; } -inline ChannelReorder::~ChannelReorder() +inline void ChannelReorder::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, + const int32_t *order, int32_t orderLength) const { - nvcvOperatorDestroy(m_handle); - m_handle = nullptr; + nvcv::detail::CheckThrow( + cvcudaChannelReorderSubmit(m_handle.get(), stream, in.handle(), out.handle(), order, orderLength)); } inline void ChannelReorder::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, - const nvcv::ImageBatchVarShape &out, const nvcv::Tensor &orders) + const nvcv::ImageBatchVarShape &out, const nvcv::Tensor &orders) const { nvcv::detail::CheckThrow( - cvcudaChannelReorderVarShapeSubmit(m_handle, stream, in.handle(), out.handle(), orders.handle())); + cvcudaChannelReorderVarShapeSubmit(m_handle.get(), stream, in.handle(), out.handle(), orders.handle())); } inline NVCVOperatorHandle ChannelReorder::handle() const noexcept { - return m_handle; + return m_handle.get(); } } // namespace cvcuda +/** @} */ + #endif // CVCUDA_CHANNEL_REORDER_HPP diff --git a/src/cvcuda/include/cvcuda/OpColorTwist.h b/src/cvcuda/include/cvcuda/OpColorTwist.h index a06bb414e..1f736acb5 100644 --- a/src/cvcuda/include/cvcuda/OpColorTwist.h +++ b/src/cvcuda/include/cvcuda/OpColorTwist.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -67,7 +67,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaColorTwistCreate(NVCVOperatorHandle *handle); * Limitations: * * Input: - * Data Layout: [NVCV_TENSOR_NHWC, NVCV_TENSOR_HWC] + * Data Layout: [NVCV_TENSOR_NHWC, NVCV_TENSOR_HWC, NVCV_TENSOR_NCHW, NVCV_TENSOR_CHW] * Channels: [3, 4] * * Data Type | Allowed @@ -78,11 +78,12 @@ CVCUDA_PUBLIC NVCVStatus cvcudaColorTwistCreate(NVCVOperatorHandle *handle); * 16bit Signed | Yes * 32bit Unsigned | Yes * 32bit Signed | Yes + * 16bit Float | No * 32bit Float | Yes * 64bit Float | No * * Output: - * Data Layout: [NVCV_TENSOR_NHWC, NVCV_TENSOR_HWC] + * Data Layout: [NVCV_TENSOR_NHWC, NVCV_TENSOR_HWC, NVCV_TENSOR_NCHW, NVCV_TENSOR_CHW] * Channels: [3, 4] * * Data Type | Allowed @@ -93,6 +94,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaColorTwistCreate(NVCVOperatorHandle *handle); * 16bit Signed | Yes * 32bit Unsigned | Yes * 32bit Signed | Yes + * 16bit Float | No * 32bit Float | Yes * 64bit Float | No * @@ -167,4 +169,6 @@ CVCUDA_PUBLIC NVCVStatus cvcudaColorTwistVarShapeSubmit(NVCVOperatorHandle handl } #endif +/** @} */ + #endif /* CVCUDA_COLOR_TWIST_H */ diff --git a/src/cvcuda/include/cvcuda/OpColorTwist.hpp b/src/cvcuda/include/cvcuda/OpColorTwist.hpp index 748d7f70b..ccd5af1a7 100644 --- a/src/cvcuda/include/cvcuda/OpColorTwist.hpp +++ b/src/cvcuda/include/cvcuda/OpColorTwist.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -34,6 +34,8 @@ #include #include +#include + namespace cvcuda { class ColorTwist final : public IOperator @@ -41,48 +43,46 @@ class ColorTwist final : public IOperator public: explicit ColorTwist(); - ~ColorTwist(); - - void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, const nvcv::Tensor &twist); + void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, + const nvcv::Tensor &twist) const; void operator()(cudaStream_t stream, const nvcv::ImageBatch &in, const nvcv::ImageBatch &out, - const nvcv::Tensor &twist); + const nvcv::Tensor &twist) const; - virtual NVCVOperatorHandle handle() const noexcept override; + NVCVOperatorHandle handle() const noexcept override; private: - NVCVOperatorHandle m_handle; + detail::OperatorHandle m_handle; }; inline ColorTwist::ColorTwist() { - nvcv::detail::CheckThrow(cvcudaColorTwistCreate(&m_handle)); - assert(m_handle); -} - -inline ColorTwist::~ColorTwist() -{ - nvcvOperatorDestroy(m_handle); + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaColorTwistCreate(&h)); + assert(h); + m_handle = detail::OperatorHandle{h}; } inline void ColorTwist::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, - const nvcv::Tensor &twist) + const nvcv::Tensor &twist) const { - nvcv::detail::CheckThrow(cvcudaColorTwistSubmit(m_handle, stream, in.handle(), out.handle(), twist.handle())); + nvcv::detail::CheckThrow(cvcudaColorTwistSubmit(m_handle.get(), stream, in.handle(), out.handle(), twist.handle())); } inline void ColorTwist::operator()(cudaStream_t stream, const nvcv::ImageBatch &in, const nvcv::ImageBatch &out, - const nvcv::Tensor &twist) + const nvcv::Tensor &twist) const { nvcv::detail::CheckThrow( - cvcudaColorTwistVarShapeSubmit(m_handle, stream, in.handle(), out.handle(), twist.handle())); + cvcudaColorTwistVarShapeSubmit(m_handle.get(), stream, in.handle(), out.handle(), twist.handle())); } inline NVCVOperatorHandle ColorTwist::handle() const noexcept { - return m_handle; + return m_handle.get(); } } // namespace cvcuda +/** @} */ + #endif // CVCUDA__COLOR_TWIST_HPP diff --git a/src/cvcuda/include/cvcuda/OpComposite.h b/src/cvcuda/include/cvcuda/OpComposite.h index f4d817e90..338f07cca 100644 --- a/src/cvcuda/include/cvcuda/OpComposite.h +++ b/src/cvcuda/include/cvcuda/OpComposite.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -57,8 +57,9 @@ CVCUDA_PUBLIC NVCVStatus cvcudaCompositeCreate(NVCVOperatorHandle *handle); * Limitations: * * Input: - * Data Layout: [kNHWC, kHWC] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] * Channels: [3] + * Note: Foreground and background are 3-channel images; fgMask is a single-channel 8-bit mask. * * Data Type | Allowed * -------------- | ------------- @@ -68,11 +69,12 @@ CVCUDA_PUBLIC NVCVStatus cvcudaCompositeCreate(NVCVOperatorHandle *handle); * 16bit Signed | No * 32bit Unsigned | No * 32bit Signed | No + * 16bit Float | No * 32bit Float | No * 64bit Float | No * * Output: - * Data Layout: [kNHWC, kHWC] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] * Channels: [3, 4] * * Data Type | Allowed @@ -83,6 +85,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaCompositeCreate(NVCVOperatorHandle *handle); * 16bit Signed | No * 32bit Unsigned | No * 32bit Signed | No + * 16bit Float | No * 32bit Float | No * 64bit Float | No * diff --git a/src/cvcuda/include/cvcuda/OpComposite.hpp b/src/cvcuda/include/cvcuda/OpComposite.hpp index 65ac3b4d7..23eae17f0 100644 --- a/src/cvcuda/include/cvcuda/OpComposite.hpp +++ b/src/cvcuda/include/cvcuda/OpComposite.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -35,6 +35,8 @@ #include #include +#include + namespace cvcuda { class Composite final : public IOperator @@ -42,53 +44,49 @@ class Composite final : public IOperator public: explicit Composite(); - ~Composite(); - void operator()(cudaStream_t stream, const nvcv::Tensor &foreground, const nvcv::Tensor &background, - const nvcv::Tensor &fgMask, const nvcv::Tensor &output); + const nvcv::Tensor &fgMask, const nvcv::Tensor &output) const; void operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &foreground, const nvcv::ImageBatchVarShape &background, const nvcv::ImageBatchVarShape &fgMask, - const nvcv::ImageBatchVarShape &output); + const nvcv::ImageBatchVarShape &output) const; - virtual NVCVOperatorHandle handle() const noexcept override; + NVCVOperatorHandle handle() const noexcept override; private: - NVCVOperatorHandle m_handle; + detail::OperatorHandle m_handle; }; inline Composite::Composite() { - nvcv::detail::CheckThrow(cvcudaCompositeCreate(&m_handle)); - assert(m_handle); -} - -inline Composite::~Composite() -{ - nvcvOperatorDestroy(m_handle); - m_handle = nullptr; + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaCompositeCreate(&h)); + assert(h); + m_handle = detail::OperatorHandle{h}; } inline void Composite::operator()(cudaStream_t stream, const nvcv::Tensor &foreground, const nvcv::Tensor &background, - const nvcv::Tensor &fgMask, const nvcv::Tensor &output) + const nvcv::Tensor &fgMask, const nvcv::Tensor &output) const { - nvcv::detail::CheckThrow(cvcudaCompositeSubmit(m_handle, stream, foreground.handle(), background.handle(), + nvcv::detail::CheckThrow(cvcudaCompositeSubmit(m_handle.get(), stream, foreground.handle(), background.handle(), fgMask.handle(), output.handle())); } inline void Composite::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &foreground, const nvcv::ImageBatchVarShape &background, const nvcv::ImageBatchVarShape &fgMask, - const nvcv::ImageBatchVarShape &output) + const nvcv::ImageBatchVarShape &output) const { - nvcv::detail::CheckThrow(cvcudaCompositeVarShapeSubmit(m_handle, stream, foreground.handle(), background.handle(), - fgMask.handle(), output.handle())); + nvcv::detail::CheckThrow(cvcudaCompositeVarShapeSubmit(m_handle.get(), stream, foreground.handle(), + background.handle(), fgMask.handle(), output.handle())); } inline NVCVOperatorHandle Composite::handle() const noexcept { - return m_handle; + return m_handle.get(); } } // namespace cvcuda +/** @} */ + #endif // CVCUDA_COMPOSITE_HPP diff --git a/src/cvcuda/include/cvcuda/OpConv2D.h b/src/cvcuda/include/cvcuda/OpConv2D.h index 3122406f7..2e8c0470f 100644 --- a/src/cvcuda/include/cvcuda/OpConv2D.h +++ b/src/cvcuda/include/cvcuda/OpConv2D.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -56,8 +56,8 @@ CVCUDA_PUBLIC NVCVStatus cvcudaConv2DCreate(NVCVOperatorHandle *handle); * Limitations: * * Input: - * Data Layout: [kNHWC, kHWC] - * Channels: [1, 3, 4] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 3, 4] (planar kNCHW/kCHW: [1, 3, 4]) * * Data Type | Allowed * -------------- | ------------- @@ -67,12 +67,13 @@ CVCUDA_PUBLIC NVCVStatus cvcudaConv2DCreate(NVCVOperatorHandle *handle); * 16bit Signed | Yes * 32bit Unsigned | No * 32bit Signed | Yes + * 16bit Float | No * 32bit Float | Yes * 64bit Float | No * * Output: - * Data Layout: [kNHWC, kHWC] - * Channels: [1, 3, 4] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 3, 4] (planar kNCHW/kCHW: [1, 3, 4]) * * Data Type | Allowed * -------------- | ------------- @@ -82,6 +83,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaConv2DCreate(NVCVOperatorHandle *handle); * 16bit Signed | Yes * 32bit Unsigned | No * 32bit Signed | Yes + * 16bit Float | No * 32bit Float | Yes * 64bit Float | No * @@ -128,4 +130,6 @@ CVCUDA_PUBLIC NVCVStatus cvcudaConv2DVarShapeSubmit(NVCVOperatorHandle handle, c } #endif +/** @} */ + #endif /* CVCUDA_CONV2D_H */ diff --git a/src/cvcuda/include/cvcuda/OpConv2D.hpp b/src/cvcuda/include/cvcuda/OpConv2D.hpp index 23b05fb7b..72a774014 100644 --- a/src/cvcuda/include/cvcuda/OpConv2D.hpp +++ b/src/cvcuda/include/cvcuda/OpConv2D.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -34,6 +34,8 @@ #include #include +#include + namespace cvcuda { class Conv2D final : public IOperator @@ -41,42 +43,38 @@ class Conv2D final : public IOperator public: explicit Conv2D(); - ~Conv2D(); - void operator()(cudaStream_t stream, const nvcv::ImageBatch &in, const nvcv::ImageBatch &out, - const nvcv::ImageBatch &kernel, const nvcv::Tensor &kernelAnchor, NVCVBorderType borderMode); + const nvcv::ImageBatch &kernel, const nvcv::Tensor &kernelAnchor, NVCVBorderType borderMode) const; - virtual NVCVOperatorHandle handle() const noexcept override; + NVCVOperatorHandle handle() const noexcept override; private: - NVCVOperatorHandle m_handle; + detail::OperatorHandle m_handle; }; inline Conv2D::Conv2D() { - nvcv::detail::CheckThrow(cvcudaConv2DCreate(&m_handle)); - assert(m_handle); -} - -inline Conv2D::~Conv2D() -{ - nvcvOperatorDestroy(m_handle); - m_handle = nullptr; + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaConv2DCreate(&h)); + assert(h); + m_handle = detail::OperatorHandle{h}; } inline void Conv2D::operator()(cudaStream_t stream, const nvcv::ImageBatch &in, const nvcv::ImageBatch &out, const nvcv::ImageBatch &kernel, const nvcv::Tensor &kernelAnchor, - NVCVBorderType borderMode) + NVCVBorderType borderMode) const { - nvcv::detail::CheckThrow(cvcudaConv2DVarShapeSubmit(m_handle, stream, in.handle(), out.handle(), kernel.handle(), - kernelAnchor.handle(), borderMode)); + nvcv::detail::CheckThrow(cvcudaConv2DVarShapeSubmit(m_handle.get(), stream, in.handle(), out.handle(), + kernel.handle(), kernelAnchor.handle(), borderMode)); } inline NVCVOperatorHandle Conv2D::handle() const noexcept { - return m_handle; + return m_handle.get(); } } // namespace cvcuda +/** @} */ + #endif // CVCUDA_CONV2D_HPP diff --git a/src/cvcuda/include/cvcuda/OpConvertTo.h b/src/cvcuda/include/cvcuda/OpConvertTo.h index eaa7d6bc8..60e267a6b 100644 --- a/src/cvcuda/include/cvcuda/OpConvertTo.h +++ b/src/cvcuda/include/cvcuda/OpConvertTo.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -30,6 +30,7 @@ #include "detail/Export.h" #include +#include #include #include @@ -54,10 +55,15 @@ CVCUDA_PUBLIC NVCVStatus cvcudaConvertToCreate(NVCVOperatorHandle *handle); * * outputs(x,y) = saturate_cast(α * inputs(x, y) + β) * + * When the output type is integral, @p roundMode selects how the floating-point result is rounded + * before the saturating cast: NVCV_ROUND_NEAREST (round to nearest, ties to even — the default and + * historical behavior) or NVCV_ROUND_TRUNCATE (truncate toward zero). The mode has no effect when + * the output type is floating-point. + * * Limitations: * * Input: - * Data Layout: [kNHWC, kHWC] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] * Channels: [1-4] * * Data Type | Allowed @@ -68,11 +74,12 @@ CVCUDA_PUBLIC NVCVStatus cvcudaConvertToCreate(NVCVOperatorHandle *handle); * 16bit Signed | Yes * 32bit Unsigned | No * 32bit Signed | Yes + * 16bit Float | No * 32bit Float | Yes * 64bit Float | Yes * * Output: - * Data Layout: [kNHWC, kHWC] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] * Channels: [1-4] * * Data Type | Allowed @@ -83,6 +90,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaConvertToCreate(NVCVOperatorHandle *handle); * 16bit Signed | Yes * 32bit Unsigned | No * 32bit Signed | Yes + * 16bit Float | No * 32bit Float | Yes * 64bit Float | Yes * @@ -109,15 +117,21 @@ CVCUDA_PUBLIC NVCVStatus cvcudaConvertToCreate(NVCVOperatorHandle *handle); * * @param [in] beta Offset for the data. * + * @param [in] roundMode Rounding mode used for float-to-integer outputs, cf. \ref NVCVRoundMode. + * Use NVCV_ROUND_NEAREST to preserve the historical behavior. + * * @retval #NVCV_ERROR_INVALID_ARGUMENT Some parameter is outside valid range. * @retval #NVCV_ERROR_INTERNAL Internal error in the operator, invalid types passed in. * @retval #NVCV_SUCCESS Operation executed successfully. */ CVCUDA_PUBLIC NVCVStatus cvcudaConvertToSubmit(NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle in, - NVCVTensorHandle out, const double alpha, const double beta); + NVCVTensorHandle out, const double alpha, const double beta, + NVCVRoundMode roundMode); #ifdef __cplusplus } #endif +/** @} */ + #endif /* CVCUDA_CONVERT_TO_H */ diff --git a/src/cvcuda/include/cvcuda/OpConvertTo.hpp b/src/cvcuda/include/cvcuda/OpConvertTo.hpp index 40fe7ff7d..552e8b235 100644 --- a/src/cvcuda/include/cvcuda/OpConvertTo.hpp +++ b/src/cvcuda/include/cvcuda/OpConvertTo.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -34,6 +34,8 @@ #include #include +#include + namespace cvcuda { class ConvertTo final : public IOperator @@ -41,40 +43,37 @@ class ConvertTo final : public IOperator public: explicit ConvertTo(); - ~ConvertTo(); - void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, const double alpha, - const double beta); + const double beta, NVCVRoundMode roundMode = NVCV_ROUND_NEAREST) const; - virtual NVCVOperatorHandle handle() const noexcept override; + NVCVOperatorHandle handle() const noexcept override; private: - NVCVOperatorHandle m_handle; + detail::OperatorHandle m_handle; }; inline ConvertTo::ConvertTo() { - nvcv::detail::CheckThrow(cvcudaConvertToCreate(&m_handle)); - assert(m_handle); -} - -inline ConvertTo::~ConvertTo() -{ - nvcvOperatorDestroy(m_handle); - m_handle = nullptr; + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaConvertToCreate(&h)); + assert(h); + m_handle = detail::OperatorHandle{h}; } inline void ConvertTo::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, - const double alpha, const double beta) + const double alpha, const double beta, NVCVRoundMode roundMode) const { - nvcv::detail::CheckThrow(cvcudaConvertToSubmit(m_handle, stream, in.handle(), out.handle(), alpha, beta)); + nvcv::detail::CheckThrow( + cvcudaConvertToSubmit(m_handle.get(), stream, in.handle(), out.handle(), alpha, beta, roundMode)); } inline NVCVOperatorHandle ConvertTo::handle() const noexcept { - return m_handle; + return m_handle.get(); } } // namespace cvcuda +/** @} */ + #endif // CVCUDA_CONVERT_TO_HPP diff --git a/src/cvcuda/include/cvcuda/OpCopyMakeBorder.h b/src/cvcuda/include/cvcuda/OpCopyMakeBorder.h index 251b9c2e0..021d6e7d4 100644 --- a/src/cvcuda/include/cvcuda/OpCopyMakeBorder.h +++ b/src/cvcuda/include/cvcuda/OpCopyMakeBorder.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -58,8 +58,8 @@ CVCUDA_PUBLIC NVCVStatus cvcudaCopyMakeBorderCreate(NVCVOperatorHandle *handle); * Limitations: * * Input: - * Data Layout: [kNHWC, kHWC] - * Channels: [1, 2, 3, 4] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 2, 3, 4] (planar kNCHW/kCHW: [1, 3, 4]) * * Data Type | Allowed * -------------- | ------------- @@ -69,14 +69,15 @@ CVCUDA_PUBLIC NVCVStatus cvcudaCopyMakeBorderCreate(NVCVOperatorHandle *handle); * 16bit Signed | Yes * 32bit Unsigned | No * 32bit Signed | No + * 16bit Float | No * 32bit Float | Yes * 64bit Float | No * - * Note: 2 channels can only support 8bit Unsigned data type. + * Note: 2 channels can only support 8bit Unsigned data type and interleaved layout. * * Output: - * Data Layout: [kNHWC, kHWC] - * Channels: [1, 2, 3, 4] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 2, 3, 4] (planar kNCHW/kCHW: [1, 3, 4]) * * Data Type | Allowed * -------------- | ------------- @@ -86,6 +87,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaCopyMakeBorderCreate(NVCVOperatorHandle *handle); * 16bit Signed | Yes * 32bit Unsigned | No * 32bit Signed | No + * 16bit Float | No * 32bit Float | Yes * 64bit Float | No * @@ -143,4 +145,6 @@ CVCUDA_PUBLIC NVCVStatus cvcudaCopyMakeBorderVarShapeStackSubmit(NVCVOperatorHan } #endif +/** @} */ + #endif /* CVCUDA_COPYMAKEBORADER_H */ diff --git a/src/cvcuda/include/cvcuda/OpCopyMakeBorder.hpp b/src/cvcuda/include/cvcuda/OpCopyMakeBorder.hpp index 17c8f37ac..50cc894ae 100644 --- a/src/cvcuda/include/cvcuda/OpCopyMakeBorder.hpp +++ b/src/cvcuda/include/cvcuda/OpCopyMakeBorder.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -35,6 +35,8 @@ #include #include +#include + namespace cvcuda { class CopyMakeBorder final : public IOperator @@ -42,63 +44,61 @@ class CopyMakeBorder final : public IOperator public: explicit CopyMakeBorder(); - ~CopyMakeBorder(); - void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, int32_t top, int32_t left, - NVCVBorderType borderMode, const float4 borderValue); + NVCVBorderType borderMode, const float4 borderValue) const; void operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, const nvcv::Tensor &top, const nvcv::Tensor &left, NVCVBorderType borderMode, - const float4 borderValue); + const float4 borderValue) const; void operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::Tensor &out, const nvcv::Tensor &top, const nvcv::Tensor &left, NVCVBorderType borderMode, - const float4 borderValue); + const float4 borderValue) const; - virtual NVCVOperatorHandle handle() const noexcept override; + NVCVOperatorHandle handle() const noexcept override; private: - NVCVOperatorHandle m_handle; + detail::OperatorHandle m_handle; }; inline CopyMakeBorder::CopyMakeBorder() { - nvcv::detail::CheckThrow(cvcudaCopyMakeBorderCreate(&m_handle)); - assert(m_handle); -} - -inline CopyMakeBorder::~CopyMakeBorder() -{ - nvcvOperatorDestroy(m_handle); - m_handle = nullptr; + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaCopyMakeBorderCreate(&h)); + assert(h); + m_handle = detail::OperatorHandle{h}; } inline void CopyMakeBorder::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, - int32_t top, int32_t left, NVCVBorderType borderMode, const float4 borderValue) + int32_t top, int32_t left, NVCVBorderType borderMode, + const float4 borderValue) const { - nvcv::detail::CheckThrow( - cvcudaCopyMakeBorderSubmit(m_handle, stream, in.handle(), out.handle(), top, left, borderMode, borderValue)); + nvcv::detail::CheckThrow(cvcudaCopyMakeBorderSubmit(m_handle.get(), stream, in.handle(), out.handle(), top, left, + borderMode, borderValue)); } inline void CopyMakeBorder::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, const nvcv::Tensor &top, - const nvcv::Tensor &left, NVCVBorderType borderMode, const float4 borderValue) + const nvcv::Tensor &left, NVCVBorderType borderMode, + const float4 borderValue) const { - nvcv::detail::CheckThrow(cvcudaCopyMakeBorderVarShapeSubmit(m_handle, stream, in.handle(), out.handle(), + nvcv::detail::CheckThrow(cvcudaCopyMakeBorderVarShapeSubmit(m_handle.get(), stream, in.handle(), out.handle(), top.handle(), left.handle(), borderMode, borderValue)); } inline void CopyMakeBorder::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::Tensor &out, const nvcv::Tensor &top, const nvcv::Tensor &left, NVCVBorderType borderMode, - const float4 borderValue) + const float4 borderValue) const { nvcv::detail::CheckThrow(cvcudaCopyMakeBorderVarShapeStackSubmit( - m_handle, stream, in.handle(), out.handle(), top.handle(), left.handle(), borderMode, borderValue)); + m_handle.get(), stream, in.handle(), out.handle(), top.handle(), left.handle(), borderMode, borderValue)); } inline NVCVOperatorHandle CopyMakeBorder::handle() const noexcept { - return m_handle; + return m_handle.get(); } } // namespace cvcuda +/** @} */ + #endif // CVCUDA_COPYMAKEBORDER_HPP diff --git a/src/cvcuda/include/cvcuda/OpCropFlipNormalizeReformat.h b/src/cvcuda/include/cvcuda/OpCropFlipNormalizeReformat.h index 2978acb72..28750a1b5 100644 --- a/src/cvcuda/include/cvcuda/OpCropFlipNormalizeReformat.h +++ b/src/cvcuda/include/cvcuda/OpCropFlipNormalizeReformat.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -101,6 +101,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaCropFlipNormalizeReformatCreate(NVCVOperatorHandl * 16bit Signed | Yes * 32bit Unsigned | Yes * 32bit Signed | Yes + * 16bit Float | No * 32bit Float | Yes * 64bit Float | No * @@ -116,6 +117,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaCropFlipNormalizeReformatCreate(NVCVOperatorHandl * 16bit Signed | Yes * 32bit Unsigned | Yes * 32bit Signed | Yes + * 16bit Float | No * 32bit Float | Yes * 64bit Float | No * @@ -177,4 +179,6 @@ CVCUDA_PUBLIC NVCVStatus cvcudaCropFlipNormalizeReformatSubmit( } #endif +/** @} */ + #endif /* CVCUDA_CROP_FLIP_NORMALIZE_REFORMAT_H */ diff --git a/src/cvcuda/include/cvcuda/OpCropFlipNormalizeReformat.hpp b/src/cvcuda/include/cvcuda/OpCropFlipNormalizeReformat.hpp index 25f9ca746..5877216b2 100644 --- a/src/cvcuda/include/cvcuda/OpCropFlipNormalizeReformat.hpp +++ b/src/cvcuda/include/cvcuda/OpCropFlipNormalizeReformat.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -35,6 +35,8 @@ #include #include +#include + namespace cvcuda { class CropFlipNormalizeReformat final : public IOperator @@ -42,29 +44,23 @@ class CropFlipNormalizeReformat final : public IOperator public: explicit CropFlipNormalizeReformat(); - ~CropFlipNormalizeReformat(); - void operator()(cudaStream_t stream, const nvcv::ImageBatch &in, const nvcv::Tensor &out, const nvcv::Tensor &cropRect, NVCVBorderType borderMode, float borderValue, const nvcv::Tensor &flipCode, const nvcv::Tensor &base, const nvcv::Tensor &scale, - float global_scale, float shift, float epsilon, uint32_t flags = 0); + float global_scale, float shift, float epsilon, uint32_t flags = 0) const; - virtual NVCVOperatorHandle handle() const noexcept override; + NVCVOperatorHandle handle() const noexcept override; private: - NVCVOperatorHandle m_handle; + detail::OperatorHandle m_handle; }; inline CropFlipNormalizeReformat::CropFlipNormalizeReformat() { - nvcv::detail::CheckThrow(cvcudaCropFlipNormalizeReformatCreate(&m_handle)); - assert(m_handle); -} - -inline CropFlipNormalizeReformat::~CropFlipNormalizeReformat() -{ - nvcvOperatorDestroy(m_handle); - m_handle = nullptr; + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaCropFlipNormalizeReformatCreate(&h)); + assert(h); + m_handle = detail::OperatorHandle{h}; } inline void CropFlipNormalizeReformat::operator()(cudaStream_t stream, const nvcv::ImageBatch &in, @@ -72,18 +68,20 @@ inline void CropFlipNormalizeReformat::operator()(cudaStream_t stream, const nvc NVCVBorderType borderMode, float borderValue, const nvcv::Tensor &flipCode, const nvcv::Tensor &base, const nvcv::Tensor &scale, float global_scale, float shift, - float epsilon, uint32_t flags) + float epsilon, uint32_t flags) const { nvcv::detail::CheckThrow(cvcudaCropFlipNormalizeReformatSubmit( - m_handle, stream, in.handle(), out.handle(), cropRect.handle(), borderMode, borderValue, flipCode.handle(), - base.handle(), scale.handle(), global_scale, shift, epsilon, flags)); + m_handle.get(), stream, in.handle(), out.handle(), cropRect.handle(), borderMode, borderValue, + flipCode.handle(), base.handle(), scale.handle(), global_scale, shift, epsilon, flags)); } inline NVCVOperatorHandle CropFlipNormalizeReformat::handle() const noexcept { - return m_handle; + return m_handle.get(); } } // namespace cvcuda +/** @} */ + #endif // CVCUDA_CROP_FLIP_NORMALIZE_REFORMAT_HPP diff --git a/src/cvcuda/include/cvcuda/OpCustomCrop.h b/src/cvcuda/include/cvcuda/OpCustomCrop.h index c3a986cb7..e286cce16 100644 --- a/src/cvcuda/include/cvcuda/OpCustomCrop.h +++ b/src/cvcuda/include/cvcuda/OpCustomCrop.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -58,8 +58,8 @@ CVCUDA_PUBLIC NVCVStatus cvcudaCustomCropCreate(NVCVOperatorHandle *handle); * ROI must be smaller than output tensor. * * Input: - * Data Layout: [kNHWC, kHWC] - * Channels: [1, 3, 4] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 2, 3, 4] (planar kNCHW/kCHW: [1, 3, 4]) * * Data Type | Allowed * -------------- | ------------- @@ -67,14 +67,15 @@ CVCUDA_PUBLIC NVCVStatus cvcudaCustomCropCreate(NVCVOperatorHandle *handle); * 8bit Signed | Yes * 16bit Unsigned | Yes * 16bit Signed | Yes - * 32bit Unsigned | Yes + * 32bit Unsigned | No * 32bit Signed | Yes + * 16bit Float | Yes * 32bit Float | Yes * 64bit Float | Yes * * Output: - * Data Layout: [kNHWC, kHWC] - * Channels: [1, 3, 4] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 2, 3, 4] (planar kNCHW/kCHW: [1, 3, 4]) * * Data Type | Allowed * -------------- | ------------- @@ -82,8 +83,9 @@ CVCUDA_PUBLIC NVCVStatus cvcudaCustomCropCreate(NVCVOperatorHandle *handle); * 8bit Signed | Yes * 16bit Unsigned | Yes * 16bit Signed | Yes - * 32bit Unsigned | Yes + * 32bit Unsigned | No * 32bit Signed | Yes + * 16bit Float | Yes * 32bit Float | Yes * 64bit Float | Yes * @@ -119,4 +121,6 @@ CVCUDA_PUBLIC NVCVStatus cvcudaCustomCropSubmit(NVCVOperatorHandle handle, cudaS } #endif +/** @} */ + #endif /* CVCUDA_CUSTOM_CROP_H */ diff --git a/src/cvcuda/include/cvcuda/OpCustomCrop.hpp b/src/cvcuda/include/cvcuda/OpCustomCrop.hpp index 60eb7d10e..7b25c3755 100644 --- a/src/cvcuda/include/cvcuda/OpCustomCrop.hpp +++ b/src/cvcuda/include/cvcuda/OpCustomCrop.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -34,6 +34,8 @@ #include #include +#include + namespace cvcuda { class CustomCrop final : public IOperator @@ -41,39 +43,36 @@ class CustomCrop final : public IOperator public: explicit CustomCrop(); - ~CustomCrop(); - - void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, const NVCVRectI cropRect); + void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, + const NVCVRectI cropRect) const; - virtual NVCVOperatorHandle handle() const noexcept override; + NVCVOperatorHandle handle() const noexcept override; private: - NVCVOperatorHandle m_handle; + detail::OperatorHandle m_handle; }; inline CustomCrop::CustomCrop() { - nvcv::detail::CheckThrow(cvcudaCustomCropCreate(&m_handle)); - assert(m_handle); -} - -inline CustomCrop::~CustomCrop() -{ - nvcvOperatorDestroy(m_handle); - m_handle = nullptr; + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaCustomCropCreate(&h)); + assert(h); + m_handle = detail::OperatorHandle{h}; } inline void CustomCrop::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, - const NVCVRectI cropRect) + const NVCVRectI cropRect) const { - nvcv::detail::CheckThrow(cvcudaCustomCropSubmit(m_handle, stream, in.handle(), out.handle(), cropRect)); + nvcv::detail::CheckThrow(cvcudaCustomCropSubmit(m_handle.get(), stream, in.handle(), out.handle(), cropRect)); } inline NVCVOperatorHandle CustomCrop::handle() const noexcept { - return m_handle; + return m_handle.get(); } } // namespace cvcuda +/** @} */ + #endif // CVCUDA_CUSTOM_CROP_HPP diff --git a/src/cvcuda/include/cvcuda/OpCvtColor.h b/src/cvcuda/include/cvcuda/OpCvtColor.h index f14954ae9..79b523c3b 100644 --- a/src/cvcuda/include/cvcuda/OpCvtColor.h +++ b/src/cvcuda/include/cvcuda/OpCvtColor.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -52,6 +52,38 @@ extern "C" CVCUDA_PUBLIC NVCVStatus cvcudaCvtColorCreate(NVCVOperatorHandle *handle); /** Executes the CvtColor (convert color) operation on the given cuda stream. This operation does not wait for completion. + * + * Limitations: + * + * Input: + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 3, 4] for kNCHW/kCHW planar tensors; var-shape planar images require RGB8p or RGBA8p + * compatible conversion codes. + * Data Type: [U8, S8, U16, S16, S32, F16, F32, F64] depending on conversion code. + * + * Output: + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 3, 4] for kNCHW/kCHW planar tensors; var-shape planar images require RGB8p or RGBA8p + * compatible conversion codes. + * Data Type: Same base type as input, depending on conversion code. + * + * Notes: + * Planar kNCHW/kCHW support excludes subsampled YUV420 and packed YUV422 conversion codes. + * + * Supported backends: + * + * Backend | Supported + * ------------- | --------- + * CUDA | Yes + * CPU | No + * + * Performance characteristics: + * + * Input | Supported + * ------------- | --------- + * Data Layout | Yes + * Data Type | Yes + * Channels | Yes * * @param [in] handle Handle to the operator. * + Must not be NULL. @@ -78,4 +110,6 @@ CVCUDA_PUBLIC NVCVStatus cvcudaCvtColorVarShapeSubmit(NVCVOperatorHandle handle, } #endif +/** @} */ + #endif /* CVCUDA_CVTCOLOR_H */ diff --git a/src/cvcuda/include/cvcuda/OpCvtColor.hpp b/src/cvcuda/include/cvcuda/OpCvtColor.hpp index a3dbb7519..0542ebc9b 100644 --- a/src/cvcuda/include/cvcuda/OpCvtColor.hpp +++ b/src/cvcuda/include/cvcuda/OpCvtColor.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -36,6 +36,8 @@ #include #include +#include + namespace cvcuda { class CvtColor final : public IOperator @@ -43,48 +45,45 @@ class CvtColor final : public IOperator public: explicit CvtColor(); - ~CvtColor(); - - void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, NVCVColorConversionCode code); + void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, + NVCVColorConversionCode code) const; void operator()(cudaStream_t stream, const nvcv::ImageBatch &in, const nvcv::ImageBatch &out, - NVCVColorConversionCode code); + NVCVColorConversionCode code) const; - virtual NVCVOperatorHandle handle() const noexcept override; + NVCVOperatorHandle handle() const noexcept override; private: - NVCVOperatorHandle m_handle; + detail::OperatorHandle m_handle; }; inline CvtColor::CvtColor() { - nvcv::detail::CheckThrow(cvcudaCvtColorCreate(&m_handle)); - assert(m_handle); -} - -inline CvtColor::~CvtColor() -{ - nvcvOperatorDestroy(m_handle); - m_handle = nullptr; + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaCvtColorCreate(&h)); + assert(h); + m_handle = detail::OperatorHandle{h}; } inline void CvtColor::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, - NVCVColorConversionCode code) + NVCVColorConversionCode code) const { - nvcv::detail::CheckThrow(cvcudaCvtColorSubmit(m_handle, stream, in.handle(), out.handle(), code)); + nvcv::detail::CheckThrow(cvcudaCvtColorSubmit(m_handle.get(), stream, in.handle(), out.handle(), code)); } inline void CvtColor::operator()(cudaStream_t stream, const nvcv::ImageBatch &in, const nvcv::ImageBatch &out, - NVCVColorConversionCode code) + NVCVColorConversionCode code) const { - nvcv::detail::CheckThrow(cvcudaCvtColorVarShapeSubmit(m_handle, stream, in.handle(), out.handle(), code)); + nvcv::detail::CheckThrow(cvcudaCvtColorVarShapeSubmit(m_handle.get(), stream, in.handle(), out.handle(), code)); } inline NVCVOperatorHandle CvtColor::handle() const noexcept { - return m_handle; + return m_handle.get(); } } // namespace cvcuda +/** @} */ + #endif // CVCUDA_CVTCOLOR_HPP diff --git a/src/cvcuda/include/cvcuda/OpErase.h b/src/cvcuda/include/cvcuda/OpErase.h index 0bc4c053b..3719613fe 100644 --- a/src/cvcuda/include/cvcuda/OpErase.h +++ b/src/cvcuda/include/cvcuda/OpErase.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -60,8 +60,8 @@ CVCUDA_PUBLIC NVCVStatus cvcudaEraseCreate(NVCVOperatorHandle *handle, int32_t m * Limitations: * * Input: - * Data Layout: [kNHWC, kHWC] - * Channels: [1, 2, 3, 4] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 2, 3, 4] (planar kNCHW/kCHW: [1, 3, 4]) * * Data Type | Allowed * -------------- | ------------- @@ -71,12 +71,13 @@ CVCUDA_PUBLIC NVCVStatus cvcudaEraseCreate(NVCVOperatorHandle *handle, int32_t m * 16bit Signed | Yes * 32bit Unsigned | No * 32bit Signed | Yes + * 16bit Float | No * 32bit Float | Yes * 64bit Float | No * * Output: - * Data Layout: [kNHWC, kHWC] - * Channels: [1, 2, 3, 4] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 2, 3, 4] (planar kNCHW/kCHW: [1, 3, 4]) * * Data Type | Allowed * -------------- | ------------- @@ -86,6 +87,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaEraseCreate(NVCVOperatorHandle *handle, int32_t m * 16bit Signed | Yes * 32bit Unsigned | No * 32bit Signed | Yes + * 16bit Float | No * 32bit Float | Yes * 64bit Float | No * @@ -100,6 +102,11 @@ CVCUDA_PUBLIC NVCVStatus cvcudaEraseCreate(NVCVOperatorHandle *handle, int32_t m * Width | Yes * Height | Yes * + * In-place execution is selected by passing the same handle for `in` and `out`. + * There is no separate in-place parameter. When `in` and `out` are different handles, + * the operation copies `in` to `out` before applying the erase areas. Passing different + * handles that partially alias the same storage is not a supported aliasing mode. + * * anchor Tensor * * Must be 'N' (dim = 1) with N = number of erasing area. @@ -146,8 +153,6 @@ CVCUDA_PUBLIC NVCVStatus cvcudaEraseCreate(NVCVOperatorHandle *handle, int32_t m * * @param [in] seed random seed for random filling erase area. * - * @param [in] inplace for perform inplace op. - * * @retval #NVCV_ERROR_INVALID_ARGUMENT Some parameter is outside valid range. * @retval #NVCV_ERROR_INTERNAL Internal error in the operator, invalid types passed in. * @retval #NVCV_SUCCESS Operation executed successfully. @@ -164,6 +169,68 @@ CVCUDA_PUBLIC NVCVStatus cvcudaEraseVarShapeSubmit(NVCVOperatorHandle handle, cu NVCVTensorHandle values, NVCVTensorHandle imgIdx, int8_t random, uint32_t seed); +/** Executes a torchvision-compatible single-region erase on the given CUDA stream. + * This operation does not wait for completion. + * + * The operation is equivalent to the following assignment on the logical planar view of the input: + * + * out = in.clone() + * out[..., i:i+h, j:j+w] = values + * + * Passing the same tensor handle for @p in and @p out performs the assignment in place. Negative, + * empty, and out-of-bounds regions follow Python's step-one slice rules. For interleaved layouts, + * @p values is still broadcast against the logical planar shape `(N, C, h, w)` or `(C, h, w)`. + * + * Reference: torchvision.transforms.v2.functional.erase. + * + * This overload is independent of the `max_num_erasing_area` capacity supplied to + * #cvcudaEraseCreate. + * + * Limitations: + * + * Input / Output: + * Container: Tensor only + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 2, 3, 4] + * + * Data Type | Allowed + * -------------- | ------------- + * 8bit Unsigned | Yes + * 8bit Signed | Yes + * 16bit Unsigned | Yes + * 16bit Signed | Yes + * 32bit Unsigned | Yes + * 32bit Signed | Yes + * 64bit Unsigned | Yes + * 64bit Signed | Yes + * 16bit Float | Yes + * 32bit Float | Yes + * 64bit Float | Yes + * + * Values: + * Rank must not exceed four. Dimensions are right-aligned against the logical `(N, C, H, W)` + * shape, with `N = 1` for unbatched inputs, and each must equal the corresponding + * selected-region dimension or be one for broadcasting. The dtype must match the input dtype + * or be 32-bit float. Float-to-integer conversion matches CUDA PyTorch assignment semantics, + * including for non-finite and out-of-range values. + * + * @param [in] handle Handle to the operator. Must not be NULL. + * @param [in] stream Handle to a valid CUDA stream. + * @param [in] in Input image tensor. + * @param [out] out Output image tensor. Shape, layout, and dtype must match @p in. + * @param [in] i Vertical slice start. + * @param [in] j Horizontal slice start. + * @param [in] h Vertical slice extent used to form the exclusive stop `i + h`. + * @param [in] w Horizontal slice extent used to form the exclusive stop `j + w`. + * @param [in] values Value tensor to broadcast into the selected region. + * + * @retval #NVCV_ERROR_INVALID_ARGUMENT A tensor or parameter violates the contract above. + * @retval #NVCV_SUCCESS Operation submitted successfully. + */ +CVCUDA_PUBLIC NVCVStatus cvcudaEraseRegionSubmit(NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle in, + NVCVTensorHandle out, int64_t i, int64_t j, int64_t h, int64_t w, + NVCVTensorHandle values); + /** @} */ #ifdef __cplusplus diff --git a/src/cvcuda/include/cvcuda/OpErase.hpp b/src/cvcuda/include/cvcuda/OpErase.hpp index 2147ef092..a00eb528e 100644 --- a/src/cvcuda/include/cvcuda/OpErase.hpp +++ b/src/cvcuda/include/cvcuda/OpErase.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -35,6 +35,8 @@ #include #include +#include + namespace cvcuda { class Erase final : public IOperator @@ -42,57 +44,67 @@ class Erase final : public IOperator public: explicit Erase(int32_t max_num_erasing_area); - ~Erase(); - void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, const nvcv::Tensor &anchor, const nvcv::Tensor &erasing, const nvcv::Tensor &values, const nvcv::Tensor &imgIdx, bool random, - uint32_t seed); + uint32_t seed) const; void operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, const nvcv::Tensor &anchor, const nvcv::Tensor &erasing, const nvcv::Tensor &values, - const nvcv::Tensor &imgIdx, bool random, uint32_t seed); + const nvcv::Tensor &imgIdx, bool random, uint32_t seed) const; + + /** Executes the torchvision-compatible single-region overload. + * + * @see cvcudaEraseRegionSubmit + */ + void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, int64_t i, int64_t j, + int64_t h, int64_t w, const nvcv::Tensor &values) const; - virtual NVCVOperatorHandle handle() const noexcept override; + NVCVOperatorHandle handle() const noexcept override; private: - NVCVOperatorHandle m_handle; + detail::OperatorHandle m_handle; }; inline Erase::Erase(int32_t max_num_erasing_area) { - nvcv::detail::CheckThrow(cvcudaEraseCreate(&m_handle, max_num_erasing_area)); - assert(m_handle); -} - -inline Erase::~Erase() -{ - nvcvOperatorDestroy(m_handle); - m_handle = nullptr; + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaEraseCreate(&h, max_num_erasing_area)); + assert(h); + m_handle = detail::OperatorHandle{h}; } inline void Erase::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, const nvcv::Tensor &anchor, const nvcv::Tensor &erasing, const nvcv::Tensor &values, - const nvcv::Tensor &imgIdx, bool random, uint32_t seed) + const nvcv::Tensor &imgIdx, bool random, uint32_t seed) const { - nvcv::detail::CheckThrow(cvcudaEraseSubmit(m_handle, stream, in.handle(), out.handle(), anchor.handle(), + nvcv::detail::CheckThrow(cvcudaEraseSubmit(m_handle.get(), stream, in.handle(), out.handle(), anchor.handle(), erasing.handle(), values.handle(), imgIdx.handle(), random, seed)); } inline void Erase::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, const nvcv::Tensor &anchor, const nvcv::Tensor &erasing, const nvcv::Tensor &values, const nvcv::Tensor &imgIdx, - bool random, uint32_t seed) + bool random, uint32_t seed) const { - nvcv::detail::CheckThrow(cvcudaEraseVarShapeSubmit(m_handle, stream, in.handle(), out.handle(), anchor.handle(), - erasing.handle(), values.handle(), imgIdx.handle(), random, - seed)); + nvcv::detail::CheckThrow(cvcudaEraseVarShapeSubmit(m_handle.get(), stream, in.handle(), out.handle(), + anchor.handle(), erasing.handle(), values.handle(), + imgIdx.handle(), random, seed)); +} + +inline void Erase::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, int64_t i, + int64_t j, int64_t h, int64_t w, const nvcv::Tensor &values) const +{ + nvcv::detail::CheckThrow( + cvcudaEraseRegionSubmit(m_handle.get(), stream, in.handle(), out.handle(), i, j, h, w, values.handle())); } inline NVCVOperatorHandle Erase::handle() const noexcept { - return m_handle; + return m_handle.get(); } } // namespace cvcuda +/** @} */ + #endif // CVCUDA_ERASE_HPP diff --git a/src/cvcuda/include/cvcuda/OpFindHomography.h b/src/cvcuda/include/cvcuda/OpFindHomography.h index d40fb875b..642d12806 100644 --- a/src/cvcuda/include/cvcuda/OpFindHomography.h +++ b/src/cvcuda/include/cvcuda/OpFindHomography.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -58,9 +58,12 @@ CVCUDA_PUBLIC NVCVStatus cvcudaFindHomographyCreate(NVCVOperatorHandle *handle, * * Limitations: * - * Input: - * Data Layout: [NW] - * Channel count: [1] + * Planar image layouts: Not applicable + * Reason: Inputs are point-coordinate tensors and outputs are transformation matrices, not images. + * + * Input (srcPts, dstPts): + * Data Layout: [NW] with dtype 2F32, or [NWC] with dtype F32 and C=2 + * Channels: [2] (packed as 2F32 or explicit dimension) * * Data Type | Allowed * -------------- | ------------- @@ -70,12 +73,13 @@ CVCUDA_PUBLIC NVCVStatus cvcudaFindHomographyCreate(NVCVOperatorHandle *handle, * 16bit Signed | No * 32bit Unsigned | No * 32bit Signed | No - * 32bit Float | Yes + * 16bit Float | No + * 32bit Float | Yes (F32 or 2F32) * 64bit Float | No * - * Output: + * Output (models): * Data Layout: [NHW] - * Channel count: [1] + * Shape: [N, 3, 3] * * Data Type | Allowed * -------------- | ------------- @@ -85,6 +89,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaFindHomographyCreate(NVCVOperatorHandle *handle, * 16bit Signed | No * 32bit Unsigned | No * 32bit Signed | No + * 16bit Float | No * 32bit Float | Yes * 64bit Float | No * @@ -93,7 +98,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaFindHomographyCreate(NVCVOperatorHandle *handle, * Property | Input == Output * -------------- | ------------- * Data Layout | No - * Data Type | Yes + * Data Type | Yes (base type F32) * Batches (N) | Yes * Channels | No * @@ -108,18 +113,18 @@ CVCUDA_PUBLIC NVCVStatus cvcudaFindHomographyCreate(NVCVOperatorHandle *handle, * + Must have data type 2F32 or F32 * + Must have rank 2 or 3 * - * * @param [in] dstPts Input tensor, dstPts[i, j] is the set of coordinates for the destination image where i ranges + * @param [in] dstPts Input tensor, dstPts[i, j] is the set of coordinates for the destination image where i ranges * from 0 to batch-1, j ranges from 4 to number of coordinates per image, and the data type being * float2 for (x=x, y=y) * + Number of coordinates must be >= 4 * + Must have data type 2F32 or F32 * + Must have rank 2 or 3 * - * @param [out] out Output tensor, models[i, j, k] is the output model tensor which maps the src points to dst points - * in image i, where i ranges from 0 to batch-1, j ranges from 0 to 2 and k ranges from 0 to 2, and - * the data type being F32. - * + Must have data type F32 - * + Must have rank 3 + * @param [out] models Output tensor, models[i, j, k] is the output model tensor which maps the src points to dst + * points in image i, where i ranges from 0 to batch-1, j ranges from 0 to 2 and k ranges from + * 0 to 2, and the data type being F32. + * + Must have data type F32 + * + Must have rank 3 * * @retval #NVCV_ERROR_INVALID_ARGUMENT Some parameter is outside valid range. * @retval #NVCV_ERROR_INTERNAL Internal error in the operator, invalid types passed in. @@ -131,13 +136,13 @@ CVCUDA_PUBLIC NVCVStatus cvcudaFindHomographySubmit(NVCVOperatorHandle handle, c NVCVTensorHandle models); /** - * Executes the FindHomography operation on a batch of images. + * Executes the FindHomography operation on tensor batches. * - * Apart from input and output image batches, all parameters are the same as \ref cvcudaFindHomographySubmit. + * Apart from using tensor batches, all parameters are the same as \ref cvcudaFindHomographySubmit. * - * @param[in] srcPts batch of coordinates in the source image. - * @param[out] dstPts batch of coordinates in the destination image. - * @param [in] models model tensor batch. + * @param[in] srcPts Input tensor batch of coordinates in the source image. + * @param[in] dstPts Input tensor batch of coordinates in the destination image. + * @param[out] models Output model tensor batch. * */ CVCUDA_PUBLIC NVCVStatus cvcudaFindHomographyVarShapeSubmit(NVCVOperatorHandle handle, cudaStream_t stream, @@ -148,4 +153,6 @@ CVCUDA_PUBLIC NVCVStatus cvcudaFindHomographyVarShapeSubmit(NVCVOperatorHandle h } #endif +/** @} */ + #endif /* CVCUDA__FIND_HOMOGRAPHY_H */ diff --git a/src/cvcuda/include/cvcuda/OpFindHomography.hpp b/src/cvcuda/include/cvcuda/OpFindHomography.hpp index 7e7c807d0..3c5f3f281 100644 --- a/src/cvcuda/include/cvcuda/OpFindHomography.hpp +++ b/src/cvcuda/include/cvcuda/OpFindHomography.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -32,6 +32,8 @@ #include #include +#include + namespace cvcuda { class FindHomography final : public IOperator @@ -39,46 +41,43 @@ class FindHomography final : public IOperator public: explicit FindHomography(int batchSize, int numPoints); - ~FindHomography(); - - void operator()(cudaStream_t stream, const nvcv::Tensor &src, const nvcv::Tensor &dst, const nvcv::Tensor &models); + void operator()(cudaStream_t stream, const nvcv::Tensor &src, const nvcv::Tensor &dst, + const nvcv::Tensor &models) const; void operator()(cudaStream_t stream, const nvcv::TensorBatch &src, const nvcv::TensorBatch &dst, - const nvcv::TensorBatch &models); + const nvcv::TensorBatch &models) const; - virtual NVCVOperatorHandle handle() const noexcept override; + NVCVOperatorHandle handle() const noexcept override; private: - NVCVOperatorHandle m_handle; + detail::OperatorHandle m_handle; }; inline FindHomography::FindHomography(int batchSize, int numPoints) { - nvcv::detail::CheckThrow(cvcudaFindHomographyCreate(&m_handle, batchSize, numPoints)); - assert(m_handle); -} - -inline FindHomography::~FindHomography() -{ - nvcvOperatorDestroy(m_handle); + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaFindHomographyCreate(&h, batchSize, numPoints)); + assert(h); + m_handle = detail::OperatorHandle{h}; } inline void FindHomography::operator()(cudaStream_t stream, const nvcv::Tensor &src, const nvcv::Tensor &dst, - const nvcv::Tensor &models) + const nvcv::Tensor &models) const { - nvcv::detail::CheckThrow(cvcudaFindHomographySubmit(m_handle, stream, src.handle(), dst.handle(), models.handle())); + nvcv::detail::CheckThrow( + cvcudaFindHomographySubmit(m_handle.get(), stream, src.handle(), dst.handle(), models.handle())); } inline void FindHomography::operator()(cudaStream_t stream, const nvcv::TensorBatch &src, const nvcv::TensorBatch &dst, - const nvcv::TensorBatch &models) + const nvcv::TensorBatch &models) const { nvcv::detail::CheckThrow( - cvcudaFindHomographyVarShapeSubmit(m_handle, stream, src.handle(), dst.handle(), models.handle())); + cvcudaFindHomographyVarShapeSubmit(m_handle.get(), stream, src.handle(), dst.handle(), models.handle())); } inline NVCVOperatorHandle FindHomography::handle() const noexcept { - return m_handle; + return m_handle.get(); } } // namespace cvcuda diff --git a/src/cvcuda/include/cvcuda/OpFlip.h b/src/cvcuda/include/cvcuda/OpFlip.h index 2db50dcbd..ce9d0b65e 100644 --- a/src/cvcuda/include/cvcuda/OpFlip.h +++ b/src/cvcuda/include/cvcuda/OpFlip.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -58,7 +58,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaFlipCreate(NVCVOperatorHandle *handle, int32_t ma * Limitations: * * Input: - * Data Layout: [kNHWC, kHWC] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] * Channels: [1, 3, 4] * * Data Type | Allowed @@ -69,11 +69,12 @@ CVCUDA_PUBLIC NVCVStatus cvcudaFlipCreate(NVCVOperatorHandle *handle, int32_t ma * 16bit Signed | No * 32bit Unsigned | No * 32bit Signed | Yes + * 16bit Float | No * 32bit Float | Yes * 64bit Float | No * * Output: - * Data Layout: [kNHWC, kHWC] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] * Channels: [1, 3, 4] * * Data Type | Allowed @@ -84,6 +85,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaFlipCreate(NVCVOperatorHandle *handle, int32_t ma * 16bit Signed | No * 32bit Unsigned | No * 32bit Signed | Yes + * 16bit Float | No * 32bit Float | Yes * 64bit Float | No * @@ -120,7 +122,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaFlipSubmit(NVCVOperatorHandle handle, cudaStream_ * Limitations: * * Input: - * Data Layout: [kNHWC, kHWC] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] * Channels: [1, 3, 4] * * Data Type | Allowed @@ -131,11 +133,12 @@ CVCUDA_PUBLIC NVCVStatus cvcudaFlipSubmit(NVCVOperatorHandle handle, cudaStream_ * 16bit Signed | Yes * 32bit Unsigned | No * 32bit Signed | Yes + * 16bit Float | No * 32bit Float | Yes * 64bit Float | No * * Output: - * Data Layout: [kNHWC, kHWC] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] * Channels: [1, 3, 4] * * Data Type | Allowed @@ -146,6 +149,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaFlipSubmit(NVCVOperatorHandle handle, cudaStream_ * 16bit Signed | Yes * 32bit Unsigned | No * 32bit Signed | Yes + * 16bit Float | No * 32bit Float | Yes * 64bit Float | No * @@ -182,4 +186,6 @@ CVCUDA_PUBLIC NVCVStatus cvcudaFlipVarShapeSubmit(NVCVOperatorHandle handle, cud } #endif /* __cplusplus */ +/** @} */ + #endif /* CVCUDA_FLIP_H */ diff --git a/src/cvcuda/include/cvcuda/OpFlip.hpp b/src/cvcuda/include/cvcuda/OpFlip.hpp index 0d04c0d2c..621947cf5 100644 --- a/src/cvcuda/include/cvcuda/OpFlip.hpp +++ b/src/cvcuda/include/cvcuda/OpFlip.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -36,52 +36,53 @@ #include #include +#include + namespace cvcuda { class Flip final : public IOperator { public: explicit Flip(int32_t maxVarShapeBatchSize = 0); - ~Flip(); - void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, int32_t flipCode); + void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, int32_t flipCode) const; void operator()(cudaStream_t stream, const nvcv::ImageBatch &in, const nvcv::ImageBatch &out, - const nvcv::Tensor &flipCode); + const nvcv::Tensor &flipCode) const; - virtual NVCVOperatorHandle handle() const noexcept override; + NVCVOperatorHandle handle() const noexcept override; private: - NVCVOperatorHandle m_handle; + detail::OperatorHandle m_handle; }; inline Flip::Flip(int32_t maxVarShapeBatchSize) { - nvcv::detail::CheckThrow(cvcudaFlipCreate(&m_handle, maxVarShapeBatchSize)); - assert(m_handle); + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaFlipCreate(&h, maxVarShapeBatchSize)); + assert(h); + m_handle = detail::OperatorHandle{h}; } -inline Flip::~Flip() +inline void Flip::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, + int32_t flipCode) const { - nvcvOperatorDestroy(m_handle); - m_handle = nullptr; -} - -inline void Flip::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, int32_t flipCode) -{ - nvcv::detail::CheckThrow(cvcudaFlipSubmit(m_handle, stream, in.handle(), out.handle(), flipCode)); + nvcv::detail::CheckThrow(cvcudaFlipSubmit(m_handle.get(), stream, in.handle(), out.handle(), flipCode)); } inline void Flip::operator()(cudaStream_t stream, const nvcv::ImageBatch &in, const nvcv::ImageBatch &out, - const nvcv::Tensor &flipCode) + const nvcv::Tensor &flipCode) const { - nvcv::detail::CheckThrow(cvcudaFlipVarShapeSubmit(m_handle, stream, in.handle(), out.handle(), flipCode.handle())); + nvcv::detail::CheckThrow( + cvcudaFlipVarShapeSubmit(m_handle.get(), stream, in.handle(), out.handle(), flipCode.handle())); } inline NVCVOperatorHandle Flip::handle() const noexcept { - return m_handle; + return m_handle.get(); } } // namespace cvcuda +/** @} */ + #endif // CVCUDA_FLIP_HPP diff --git a/src/cvcuda/include/cvcuda/OpGammaContrast.h b/src/cvcuda/include/cvcuda/OpGammaContrast.h index b52adcbe2..9cdf49c6c 100644 --- a/src/cvcuda/include/cvcuda/OpGammaContrast.h +++ b/src/cvcuda/include/cvcuda/OpGammaContrast.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -31,6 +31,7 @@ #include "detail/Export.h" #include +#include #include #include @@ -43,12 +44,12 @@ extern "C" * * @param [out] handle Where the operator instance handle will be written to. * + Must not be NULL. - * @param [in] maxVarShapeBatchSize is the maximum batch size for the operator. + * @param [in] maxVarShapeBatchSize is the positive maximum batch size for the operator. * - * @param [in] maxVarShapeChannelCount is the maximum channel count for the operator. + * @param [in] maxVarShapeChannelCount is the positive maximum channel count for the operator. * * - * @retval #NVCV_ERROR_INVALID_ARGUMENT Handle is null. + * @retval #NVCV_ERROR_INVALID_ARGUMENT Handle is null or an input limit is not positive. * @retval #NVCV_ERROR_OUT_OF_MEMORY Not enough memory to create the operator. * @retval #NVCV_SUCCESS Operation executed successfully. */ @@ -59,9 +60,9 @@ CVCUDA_PUBLIC NVCVStatus cvcudaGammaContrastCreate(NVCVOperatorHandle *handle, c * * Limitations: * - * Input: - * Data Layout: [kNHWC, kHWC] - * Channels: [1, 3, 4] + * Input image batch: + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 2, 3, 4] (planar layouts kNCHW/kCHW exclude 2 channels) * * Data Type | Allowed * -------------- | ------------- @@ -71,12 +72,13 @@ CVCUDA_PUBLIC NVCVStatus cvcudaGammaContrastCreate(NVCVOperatorHandle *handle, c * 16bit Signed | Yes * 32bit Unsigned | No * 32bit Signed | Yes + * 16bit Float | No * 32bit Float | Yes * 64bit Float | No * - * Output: - * Data Layout: [kNHWC, kHWC] - * Channels: [1, 3, 4] + * Output image batch: + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 2, 3, 4] (planar layouts kNCHW/kCHW exclude 2 channels) * * Data Type | Allowed * -------------- | ------------- @@ -86,6 +88,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaGammaContrastCreate(NVCVOperatorHandle *handle, c * 16bit Signed | Yes * 32bit Unsigned | No * 32bit Signed | Yes + * 16bit Float | No * 32bit Float | Yes * 64bit Float | No * @@ -104,11 +107,11 @@ CVCUDA_PUBLIC NVCVStatus cvcudaGammaContrastCreate(NVCVOperatorHandle *handle, c * + Must not be NULL. * @param [in] stream Handle to a valid CUDA stream. * - * @param [in] in Input tensor. + * @param [in] in Input image batch. * - * @param [out] out Output tensor. + * @param [out] out Output image batch. * - * @param [in] gamma 1D Tensor with the the gamma value for each image / image channel. + * @param [in] gamma 1D tensor with the gamma value for each image / image channel. * * * @@ -120,8 +123,59 @@ CVCUDA_PUBLIC NVCVStatus cvcudaGammaContrastVarShapeSubmit(NVCVOperatorHandle ha NVCVImageBatchHandle in, NVCVImageBatchHandle out, NVCVTensorHandle gamma); +/** Executes the GammaContrast operation on a tensor input/output (interleaved (N)HWC or planar + * (N)CHW layout). This operation does not wait for completion. + * + * @param [in] handle Handle to the operator. Must not be NULL. + * @param [in] stream Handle to a valid CUDA stream. + * @param [in] in Input tensor. + * @param [out] out Output tensor. + * @param [in] gamma 1D tensor with the gamma value for each sample / sample channel. + * + * @retval #NVCV_ERROR_INVALID_ARGUMENT Some parameter is outside valid range. + * @retval #NVCV_ERROR_INTERNAL Internal error in the operator, invalid types passed in. + * @retval #NVCV_SUCCESS Operation executed successfully. + */ +CVCUDA_PUBLIC NVCVStatus cvcudaGammaContrastSubmit(NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle in, + NVCVTensorHandle out, NVCVTensorHandle gamma); + +/** Executes the GammaContrast operation on a tensor input/output using host-scalar gamma and gain + * (interleaved (N)HWC or planar (N)CHW layout). This operation does not wait for completion. + * + * Unlike #cvcudaGammaContrastSubmit, gamma and gain are plain host floats passed by value into the + * kernel launch, so no gamma tensor is allocated and no host->device copy is performed. The same + * gamma/gain is applied to every sample and channel, computing out = gain * in**gamma (the + * torchvision adjust_gamma formula). With gain == 1.0f and roundMode == #NVCV_ROUND_NEAREST, the + * result is bit-exact with #cvcudaGammaContrastSubmit fed a gamma tensor filled with the same value. + * Data layout, channel, and data-type support match #cvcudaGammaContrastSubmit. + * + * Because no gamma scratch is used, the max-batch/max-channel capacities given to + * #cvcudaGammaContrastCreate do not constrain this function (they only bound the gamma-tensor + * staging used by the tensor and var-shape submits). gamma and gain are not range-validated; + * a negative gamma follows powf semantics (NaN for fractional exponents of negative inputs). + * + * @param [in] handle Handle to the operator. Must not be NULL. + * @param [in] stream Handle to a valid CUDA stream. + * @param [in] in Input tensor. + * @param [out] out Output tensor. + * @param [in] gamma Host-scalar gamma exponent applied to every sample/channel. + * @param [in] gain Host-scalar output gain applied to every sample/channel. + * @param [in] roundMode Rounding mode used for integer outputs, cf. \ref NVCVRoundMode. + * Use #NVCV_ROUND_NEAREST to preserve the tensor-gamma behavior or + * #NVCV_ROUND_TRUNCATE to truncate toward zero. Floating-point outputs are unaffected. + * + * @retval #NVCV_ERROR_INVALID_ARGUMENT Some parameter is outside valid range. + * @retval #NVCV_ERROR_INTERNAL Internal error in the operator, invalid types passed in. + * @retval #NVCV_SUCCESS Operation executed successfully. + */ +CVCUDA_PUBLIC NVCVStatus cvcudaGammaContrastScalarSubmit(NVCVOperatorHandle handle, cudaStream_t stream, + NVCVTensorHandle in, NVCVTensorHandle out, float gamma, + float gain, NVCVRoundMode roundMode); + #ifdef __cplusplus } #endif +/** @} */ + #endif /* CVCUDA_GAMMA_CONTRAST_H */ diff --git a/src/cvcuda/include/cvcuda/OpGammaContrast.hpp b/src/cvcuda/include/cvcuda/OpGammaContrast.hpp index c3bccbe7e..1cda7441c 100644 --- a/src/cvcuda/include/cvcuda/OpGammaContrast.hpp +++ b/src/cvcuda/include/cvcuda/OpGammaContrast.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -34,6 +34,8 @@ #include #include +#include + namespace cvcuda { class GammaContrast final : public IOperator @@ -41,40 +43,57 @@ class GammaContrast final : public IOperator public: explicit GammaContrast(const int32_t maxVarShapeBatchSize, const int32_t maxVarShapeChannelCount); - ~GammaContrast(); + void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, + const nvcv::Tensor &gamma) const; + + void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, float gamma, float gain, + NVCVRoundMode roundMode = NVCV_ROUND_NEAREST) const; void operator()(cudaStream_t stream, const nvcv::ImageBatch &in, const nvcv::ImageBatch &out, - const nvcv::Tensor &gamma); + const nvcv::Tensor &gamma) const; - virtual NVCVOperatorHandle handle() const noexcept override; + NVCVOperatorHandle handle() const noexcept override; private: - NVCVOperatorHandle m_handle; + detail::OperatorHandle m_handle; }; inline GammaContrast::GammaContrast(const int32_t maxVarShapeBatchSize, const int32_t maxVarShapeChannelCount) { - nvcv::detail::CheckThrow(cvcudaGammaContrastCreate(&m_handle, maxVarShapeBatchSize, maxVarShapeChannelCount)); - assert(m_handle); + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaGammaContrastCreate(&h, maxVarShapeBatchSize, maxVarShapeChannelCount)); + assert(h); + m_handle = detail::OperatorHandle{h}; } -inline GammaContrast::~GammaContrast() +inline void GammaContrast::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, + const nvcv::Tensor &gamma) const { - nvcvOperatorDestroy(m_handle); - m_handle = nullptr; + nvcv::detail::CheckThrow( + cvcudaGammaContrastSubmit(m_handle.get(), stream, in.handle(), out.handle(), gamma.handle())); +} + +inline void GammaContrast::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, float gamma, + float gain, NVCVRoundMode roundMode) const +{ + nvcv::detail::CheckThrow( + cvcudaGammaContrastScalarSubmit(m_handle.get(), stream, in.handle(), out.handle(), gamma, gain, roundMode)); } inline void GammaContrast::operator()(cudaStream_t stream, const nvcv::ImageBatch &in, const nvcv::ImageBatch &out, - const nvcv::Tensor &gamma) + const nvcv::Tensor &gamma) const { nvcv::detail::CheckThrow( - cvcudaGammaContrastVarShapeSubmit(m_handle, stream, in.handle(), out.handle(), gamma.handle())); + cvcudaGammaContrastVarShapeSubmit(m_handle.get(), stream, in.handle(), out.handle(), gamma.handle())); } inline NVCVOperatorHandle GammaContrast::handle() const noexcept { - return m_handle; + return m_handle.get(); } } // namespace cvcuda + +/** @} */ + #endif // CVCUDA_GAMMA_CONTRAST_HPP diff --git a/src/cvcuda/include/cvcuda/OpGaussian.h b/src/cvcuda/include/cvcuda/OpGaussian.h index 2cd20b9b2..5ad1d2a21 100644 --- a/src/cvcuda/include/cvcuda/OpGaussian.h +++ b/src/cvcuda/include/cvcuda/OpGaussian.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -64,7 +64,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaGaussianCreate(NVCVOperatorHandle *handle, int32_ * Limitations: * * Input: - * Data Layout: [kNHWC, kHWC] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] * Channels: [1, 3, 4] * * Data Type | Allowed @@ -75,11 +75,12 @@ CVCUDA_PUBLIC NVCVStatus cvcudaGaussianCreate(NVCVOperatorHandle *handle, int32_ * 16bit Signed | Yes * 32bit Unsigned | No * 32bit Signed | Yes + * 16bit Float | No * 32bit Float | Yes * 64bit Float | No * * Output: - * Data Layout: [kNHWC, kHWC] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] * Channels: [1, 3, 4] * * Data Type | Allowed @@ -90,6 +91,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaGaussianCreate(NVCVOperatorHandle *handle, int32_ * 16bit Signed | Yes * 32bit Unsigned | No * 32bit Signed | Yes + * 16bit Float | No * 32bit Float | Yes * 64bit Float | No * @@ -150,4 +152,6 @@ CVCUDA_PUBLIC NVCVStatus cvcudaGaussianVarShapeSubmit(NVCVOperatorHandle handle, } #endif +/** @} */ + #endif /* CVCUDA_GAUSSIAN_H */ diff --git a/src/cvcuda/include/cvcuda/OpGaussian.hpp b/src/cvcuda/include/cvcuda/OpGaussian.hpp index 575c3fc49..1c6b45f0b 100644 --- a/src/cvcuda/include/cvcuda/OpGaussian.hpp +++ b/src/cvcuda/include/cvcuda/OpGaussian.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -36,6 +36,8 @@ #include #include +#include + namespace cvcuda { class Gaussian final : public IOperator @@ -43,51 +45,48 @@ class Gaussian final : public IOperator public: explicit Gaussian(nvcv::Size2D maxKernelSize, int32_t maxVarShapeBatchSize); - ~Gaussian(); - void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, nvcv::Size2D kernelSize, - double2 sigma, NVCVBorderType borderMode); + double2 sigma, NVCVBorderType borderMode) const; void operator()(cudaStream_t stream, const nvcv::ImageBatch &in, const nvcv::ImageBatch &out, - const nvcv::Tensor &kernelSize, const nvcv::Tensor &sigma, NVCVBorderType borderMode); + const nvcv::Tensor &kernelSize, const nvcv::Tensor &sigma, NVCVBorderType borderMode) const; - virtual NVCVOperatorHandle handle() const noexcept override; + NVCVOperatorHandle handle() const noexcept override; private: - NVCVOperatorHandle m_handle; + detail::OperatorHandle m_handle; }; inline Gaussian::Gaussian(nvcv::Size2D maxKernelSize, int32_t maxVarShapeBatchSize) { - nvcv::detail::CheckThrow(cvcudaGaussianCreate(&m_handle, maxKernelSize.w, maxKernelSize.h, maxVarShapeBatchSize)); - assert(m_handle); -} - -inline Gaussian::~Gaussian() -{ - nvcvOperatorDestroy(m_handle); - m_handle = nullptr; + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaGaussianCreate(&h, maxKernelSize.w, maxKernelSize.h, maxVarShapeBatchSize)); + assert(h); + m_handle = detail::OperatorHandle{h}; } inline void Gaussian::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, - nvcv::Size2D kernelSize, double2 sigma, NVCVBorderType borderMode) + nvcv::Size2D kernelSize, double2 sigma, NVCVBorderType borderMode) const { - nvcv::detail::CheckThrow(cvcudaGaussianSubmit(m_handle, stream, in.handle(), out.handle(), kernelSize.w, + nvcv::detail::CheckThrow(cvcudaGaussianSubmit(m_handle.get(), stream, in.handle(), out.handle(), kernelSize.w, kernelSize.h, sigma.x, sigma.y, borderMode)); } inline void Gaussian::operator()(cudaStream_t stream, const nvcv::ImageBatch &in, const nvcv::ImageBatch &out, - const nvcv::Tensor &kernelSize, const nvcv::Tensor &sigma, NVCVBorderType borderMode) + const nvcv::Tensor &kernelSize, const nvcv::Tensor &sigma, + NVCVBorderType borderMode) const { - nvcv::detail::CheckThrow(cvcudaGaussianVarShapeSubmit(m_handle, stream, in.handle(), out.handle(), + nvcv::detail::CheckThrow(cvcudaGaussianVarShapeSubmit(m_handle.get(), stream, in.handle(), out.handle(), kernelSize.handle(), sigma.handle(), borderMode)); } inline NVCVOperatorHandle Gaussian::handle() const noexcept { - return m_handle; + return m_handle.get(); } } // namespace cvcuda +/** @} */ + #endif // CVCUDA_GAUSSIAN_HPP diff --git a/src/cvcuda/include/cvcuda/OpGaussianNoise.h b/src/cvcuda/include/cvcuda/OpGaussianNoise.h index 557f62fb3..d1b645ffe 100644 --- a/src/cvcuda/include/cvcuda/OpGaussianNoise.h +++ b/src/cvcuda/include/cvcuda/OpGaussianNoise.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -60,8 +60,8 @@ CVCUDA_PUBLIC NVCVStatus cvcudaGaussianNoiseCreate(NVCVOperatorHandle *handle, i * Limitations: * * Input: - * Data Layout: [kNHWC, kHWC] - * Channels: [1, 2, 3, 4] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 2, 3, 4] (planar kNCHW/kCHW: [1, 3, 4]) * * Data Type | Allowed * -------------- | ------------- @@ -71,12 +71,13 @@ CVCUDA_PUBLIC NVCVStatus cvcudaGaussianNoiseCreate(NVCVOperatorHandle *handle, i * 16bit Signed | Yes * 32bit Unsigned | No * 32bit Signed | Yes + * 16bit Float | No * 32bit Float | Yes * 64bit Float | No * * Output: - * Data Layout: [kNHWC, kHWC] - * Channels: [1, 2, 3, 4] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 2, 3, 4] (planar kNCHW/kCHW: [1, 3, 4]) * * Data Type | Allowed * -------------- | ------------- @@ -86,6 +87,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaGaussianNoiseCreate(NVCVOperatorHandle *handle, i * 16bit Signed | Yes * 32bit Unsigned | No * 32bit Signed | Yes + * 16bit Float | No * 32bit Float | Yes * 64bit Float | No * @@ -139,6 +141,49 @@ CVCUDA_PUBLIC NVCVStatus cvcudaGaussianNoiseSubmit(NVCVOperatorHandle handle, cu NVCVTensorHandle out, NVCVTensorHandle mu, NVCVTensorHandle sigma, int8_t per_channel, unsigned long long seed); +/** + * Executes GaussianNoise on a tensor with scalar noise parameters. + * + * This parameter-tensor-free variant passes \p mu and \p sigma by value, avoiding device parameter tensors and their + * host-to-device uploads. Its uint8 conversion and optional clipping match + * `torchvision.transforms.v2.functional.gaussian_noise`; float32 output is optionally clamped to [0, 1]. Random samples + * are generated by CV-CUDA's cuRAND stream and are not sample-identical to Torch. + * + * Reference: torchvision.transforms.v2.functional.gaussian_noise + * + * Limitations: + * + * Input and output: + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 2, 3, 4] (planar kNCHW/kCHW: [1, 3, 4]) + * Data Type: [TYPE_U8, TYPE_F32] + * + * Input and output must have identical shape, layout, and data type. \p mu and \p sigma use input-value units. For + * uint8 torchvision-compatible normalized parameters, callers multiply both values by 255 before submission. + * + * @param [in] handle Handle to the operator. Must not be NULL. + * @param [in] stream CUDA stream on which to perform the operation. + * @param [in] in Input image tensor. + * @param [out] out Output image tensor. + * @param [in] mu Mean of the Gaussian distribution in input-value units. + * @param [in] sigma Non-negative standard deviation in input-value units. + * @param [in] per_channel If non-zero, generate independent noise for every channel; otherwise share noise across + * channels at each pixel. + * @param [in] seed Seed for CV-CUDA's random-number stream. + * @param [in] reseed If non-zero, reinitialize the random-number stream from \p seed before this submission. Otherwise + * the stream is initialized only on first use and advances between calls. + * @param [in] clip If non-zero, clamp output to [0, 255] for uint8 or [0, 1] for float32. If zero, float32 remains + * unbounded and uint8 wraps modulo 256 after addition. + * + * @retval #NVCV_ERROR_INVALID_ARGUMENT Some parameter is outside its valid range. + * @retval #NVCV_ERROR_INTERNAL An internal error occurred. + * @retval #NVCV_SUCCESS Operation was submitted successfully. + */ +CVCUDA_PUBLIC NVCVStatus cvcudaGaussianNoiseScalarSubmit(NVCVOperatorHandle handle, cudaStream_t stream, + NVCVTensorHandle in, NVCVTensorHandle out, float mu, + float sigma, int8_t per_channel, unsigned long long seed, + int8_t reseed, int8_t clip); + CVCUDA_PUBLIC NVCVStatus cvcudaGaussianNoiseVarShapeSubmit(NVCVOperatorHandle handle, cudaStream_t stream, NVCVImageBatchHandle in, NVCVImageBatchHandle out, NVCVTensorHandle mu, NVCVTensorHandle sigma, diff --git a/src/cvcuda/include/cvcuda/OpGaussianNoise.hpp b/src/cvcuda/include/cvcuda/OpGaussianNoise.hpp index a6a0891ff..abb08f7bb 100644 --- a/src/cvcuda/include/cvcuda/OpGaussianNoise.hpp +++ b/src/cvcuda/include/cvcuda/OpGaussianNoise.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -35,6 +35,8 @@ #include #include +#include + namespace cvcuda { class GaussianNoise final : public IOperator @@ -42,53 +44,62 @@ class GaussianNoise final : public IOperator public: explicit GaussianNoise(int maxBatchSize); - ~GaussianNoise(); - void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, const nvcv::Tensor &mu, - const nvcv::Tensor &sigma, bool per_channel, unsigned long long seed); + const nvcv::Tensor &sigma, bool per_channel, unsigned long long seed) const; + + void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, float mu, float sigma, + bool per_channel, unsigned long long seed, bool reseed, bool clip) const; void operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, - const nvcv::Tensor &mu, const nvcv::Tensor &sigma, bool per_channel, unsigned long long seed); + const nvcv::Tensor &mu, const nvcv::Tensor &sigma, bool per_channel, unsigned long long seed) const; - virtual NVCVOperatorHandle handle() const noexcept override; + NVCVOperatorHandle handle() const noexcept override; private: - NVCVOperatorHandle m_handle; + detail::OperatorHandle m_handle; }; inline GaussianNoise::GaussianNoise(int maxBatchSize) { - nvcv::detail::CheckThrow(cvcudaGaussianNoiseCreate(&m_handle, maxBatchSize)); - assert(m_handle); -} - -inline GaussianNoise::~GaussianNoise() -{ - nvcvOperatorDestroy(m_handle); - m_handle = nullptr; + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaGaussianNoiseCreate(&h, maxBatchSize)); + assert(h); + m_handle = detail::OperatorHandle{h}; } inline void GaussianNoise::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, const nvcv::Tensor &mu, const nvcv::Tensor &sigma, bool per_channel, - unsigned long long seed) + unsigned long long seed) const { - nvcv::detail::CheckThrow(cvcudaGaussianNoiseSubmit(m_handle, stream, in.handle(), out.handle(), mu.handle(), + nvcv::detail::CheckThrow(cvcudaGaussianNoiseSubmit(m_handle.get(), stream, in.handle(), out.handle(), mu.handle(), sigma.handle(), static_cast(per_channel), seed)); } +inline void GaussianNoise::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, float mu, + float sigma, bool per_channel, unsigned long long seed, bool reseed, + bool clip) const +{ + nvcv::detail::CheckThrow(cvcudaGaussianNoiseScalarSubmit(m_handle.get(), stream, in.handle(), out.handle(), mu, + sigma, static_cast(per_channel), seed, + static_cast(reseed), static_cast(clip))); +} + inline void GaussianNoise::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, const nvcv::Tensor &mu, - const nvcv::Tensor &sigma, bool per_channel, unsigned long long seed) + const nvcv::Tensor &sigma, bool per_channel, unsigned long long seed) const { - nvcv::detail::CheckThrow(cvcudaGaussianNoiseVarShapeSubmit(m_handle, stream, in.handle(), out.handle(), mu.handle(), - sigma.handle(), static_cast(per_channel), seed)); + nvcv::detail::CheckThrow(cvcudaGaussianNoiseVarShapeSubmit(m_handle.get(), stream, in.handle(), out.handle(), + mu.handle(), sigma.handle(), + static_cast(per_channel), seed)); } inline NVCVOperatorHandle GaussianNoise::handle() const noexcept { - return m_handle; + return m_handle.get(); } } // namespace cvcuda +/** @} */ + #endif // CVCUDA_GAUSSIAN_NOISE_HPP diff --git a/src/cvcuda/include/cvcuda/OpHQResize.h b/src/cvcuda/include/cvcuda/OpHQResize.h index d6715e138..39f931bfe 100644 --- a/src/cvcuda/include/cvcuda/OpHQResize.h +++ b/src/cvcuda/include/cvcuda/OpHQResize.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -53,10 +53,10 @@ typedef struct typedef struct { - HQResizeTensorShapeI *shape; - int32_t size; // the number of valid elements in the `shape` array - int32_t ndim; // the number of spatial extents in each `shapes` element - int32_t numChannels; // the number of innermost channels, -1 if they differ between samples + const HQResizeTensorShapeI *shape; + int32_t size; // the number of valid elements in the `shape` array + int32_t ndim; // the number of spatial extents in each `shapes` element + int32_t numChannels; // the number of innermost channels, -1 if they differ between samples } HQResizeTensorShapesI; typedef struct @@ -67,9 +67,9 @@ typedef struct typedef struct { - int32_t size; // the number of valid elements in the `roi` array - int32_t ndim; // the number of valid extents in each `roi` element - HQResizeRoiF *roi; + int32_t size; // the number of valid elements in the `roi` array + int32_t ndim; // the number of valid extents in each `roi` element + const HQResizeRoiF *roi; } HQResizeRoisF; /** Constructs an instance of the HQResize operator. @@ -190,7 +190,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaHQResizeGetMaxWorkspaceRequirements(NVCVOperatorH * Limitations: * * Input, Output: - * Data Layout: NVCV_TENSOR_[N][D]HW[C] + * Data Layout: NVCV_TENSOR_[N][D]HW[C] (interleaved); NVCV_TENSOR_[N]CHW (planar, 2D only) * * Number of channels: Positive integer * @@ -202,6 +202,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaHQResizeGetMaxWorkspaceRequirements(NVCVOperatorH * 16bit Signed | Yes * 32bit Unsigned | No * 32bit Signed | No + * 16bit Float | No * 32bit Float | Yes * 64bit Float | No * @@ -260,7 +261,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaHQResizeSubmit(NVCVOperatorHandle handle, cudaStr * Limitations: * * Input, Output: - * Data Layout: NVCV_TENSOR_HWC + * Data Layout: NVCV_TENSOR_HWC (interleaved); planar multi-plane formats (e.g. RGB8p) are also supported * * Number of channels: [1, 2, 3, 4] * @@ -272,6 +273,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaHQResizeSubmit(NVCVOperatorHandle handle, cudaStr * 16bit Signed | Yes * 32bit Unsigned | No * 32bit Signed | No + * 16bit Float | No * 32bit Float | Yes * 64bit Float | No * @@ -332,7 +334,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaHQResizeImageBatchSubmit(NVCVOperatorHandle handl * Limitations: * * Input, Output: - * Data Layout: NVCV_TENSOR_[D]HW[C] + * Data Layout: NVCV_TENSOR_[D]HW[C] (interleaved); NVCV_TENSOR_CHW (planar, 2D only) * * Number of channels: Positive integer * @@ -344,6 +346,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaHQResizeImageBatchSubmit(NVCVOperatorHandle handl * 16bit Signed | Yes * 32bit Unsigned | No * 32bit Signed | No + * 16bit Float | No * 32bit Float | Yes * 64bit Float | No * @@ -403,4 +406,6 @@ CVCUDA_PUBLIC NVCVStatus cvcudaHQResizeTensorBatchSubmit(NVCVOperatorHandle hand } #endif +/** @} */ + #endif /* CVCUDA_HQ_RESIZE_H */ diff --git a/src/cvcuda/include/cvcuda/OpHQResize.hpp b/src/cvcuda/include/cvcuda/OpHQResize.hpp index 8e929bc5f..22eeda95d 100644 --- a/src/cvcuda/include/cvcuda/OpHQResize.hpp +++ b/src/cvcuda/include/cvcuda/OpHQResize.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -37,6 +37,8 @@ #include #include +#include + namespace cvcuda { class HQResize final : public IOperator @@ -44,60 +46,55 @@ class HQResize final : public IOperator public: explicit HQResize(); - ~HQResize(); - WorkspaceRequirements getWorkspaceRequirements(int batchSize, const HQResizeTensorShapeI inputShape, const HQResizeTensorShapeI outputShape, const NVCVInterpolationType minInterpolation, const NVCVInterpolationType magInterpolation, bool antialias, - const HQResizeRoiF *roi = nullptr); + const HQResizeRoiF *roi = nullptr) const; WorkspaceRequirements getWorkspaceRequirements(int batchSize, HQResizeTensorShapesI inputShapes, const HQResizeTensorShapesI outputShapes, const NVCVInterpolationType minInterpolation, const NVCVInterpolationType magInterpolation, bool antialias, - const HQResizeRoisF roi = {}); + const HQResizeRoisF roi = {}) const; - WorkspaceRequirements getWorkspaceRequirements(int maxBatchSize, const HQResizeTensorShapeI maxShape); + WorkspaceRequirements getWorkspaceRequirements(int maxBatchSize, const HQResizeTensorShapeI maxShape) const; void operator()(cudaStream_t stream, const Workspace &ws, const nvcv::Tensor &in, const nvcv::Tensor &out, const NVCVInterpolationType minInterpolation, const NVCVInterpolationType magInterpolation, - bool antialias = false, const HQResizeRoiF *roi = nullptr); + bool antialias = false, const HQResizeRoiF *roi = nullptr) const; void operator()(cudaStream_t stream, const Workspace &ws, const nvcv::ImageBatch &in, const nvcv::ImageBatch &out, const NVCVInterpolationType minInterpolation, const NVCVInterpolationType magInterpolation, - bool antialias = false, const HQResizeRoisF roi = {}); + bool antialias = false, const HQResizeRoisF roi = {}) const; void operator()(cudaStream_t stream, const Workspace &ws, const nvcv::TensorBatch &in, const nvcv::TensorBatch &out, const NVCVInterpolationType minInterpolation, const NVCVInterpolationType magInterpolation, - bool antialias = false, const HQResizeRoisF roi = {}); + bool antialias = false, const HQResizeRoisF roi = {}) const; - virtual NVCVOperatorHandle handle() const noexcept override; + NVCVOperatorHandle handle() const noexcept override; private: - NVCVOperatorHandle m_handle; + detail::OperatorHandle m_handle; }; inline HQResize::HQResize() { - nvcv::detail::CheckThrow(cvcudaHQResizeCreate(&m_handle)); - assert(m_handle); -} - -inline HQResize::~HQResize() -{ - nvcvOperatorDestroy(m_handle); + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaHQResizeCreate(&h)); + assert(h); + m_handle = detail::OperatorHandle{h}; } inline WorkspaceRequirements HQResize::getWorkspaceRequirements(int batchSize, const HQResizeTensorShapeI inputShape, const HQResizeTensorShapeI outputShape, const NVCVInterpolationType minInterpolation, const NVCVInterpolationType magInterpolation, - bool antialias, const HQResizeRoiF *roi) + bool antialias, const HQResizeRoiF *roi) const { WorkspaceRequirements req{}; nvcv::detail::CheckThrow(cvcudaHQResizeTensorGetWorkspaceRequirements( - m_handle, batchSize, inputShape, outputShape, minInterpolation, magInterpolation, antialias, roi, &req)); + m_handle.get(), batchSize, inputShape, outputShape, minInterpolation, magInterpolation, antialias, roi, &req)); return req; } @@ -105,50 +102,57 @@ inline WorkspaceRequirements HQResize::getWorkspaceRequirements(int batchSize, c const HQResizeTensorShapesI outputShapes, const NVCVInterpolationType minInterpolation, const NVCVInterpolationType magInterpolation, - bool antialias, const HQResizeRoisF roi) + bool antialias, const HQResizeRoisF roi) const { WorkspaceRequirements req{}; - nvcv::detail::CheckThrow(cvcudaHQResizeTensorBatchGetWorkspaceRequirements( - m_handle, batchSize, inputShapes, outputShapes, minInterpolation, magInterpolation, antialias, roi, &req)); + nvcv::detail::CheckThrow(cvcudaHQResizeTensorBatchGetWorkspaceRequirements(m_handle.get(), batchSize, inputShapes, + outputShapes, minInterpolation, + magInterpolation, antialias, roi, &req)); return req; } -inline WorkspaceRequirements HQResize::getWorkspaceRequirements(int maxBatchSize, const HQResizeTensorShapeI maxShape) +inline WorkspaceRequirements HQResize::getWorkspaceRequirements(int maxBatchSize, + const HQResizeTensorShapeI maxShape) const { WorkspaceRequirements req{}; - nvcv::detail::CheckThrow(cvcudaHQResizeGetMaxWorkspaceRequirements(m_handle, maxBatchSize, maxShape, &req)); + nvcv::detail::CheckThrow(cvcudaHQResizeGetMaxWorkspaceRequirements(m_handle.get(), maxBatchSize, maxShape, &req)); return req; } inline void HQResize::operator()(cudaStream_t stream, const Workspace &ws, const nvcv::Tensor &in, const nvcv::Tensor &out, const NVCVInterpolationType minInterpolation, - const NVCVInterpolationType magInterpolation, bool antialias, const HQResizeRoiF *roi) + const NVCVInterpolationType magInterpolation, bool antialias, + const HQResizeRoiF *roi) const { - nvcv::detail::CheckThrow(cvcudaHQResizeSubmit(m_handle, stream, &ws, in.handle(), out.handle(), minInterpolation, - magInterpolation, antialias, roi)); + nvcv::detail::CheckThrow(cvcudaHQResizeSubmit(m_handle.get(), stream, &ws, in.handle(), out.handle(), + minInterpolation, magInterpolation, antialias, roi)); } inline void HQResize::operator()(cudaStream_t stream, const Workspace &ws, const nvcv::ImageBatch &in, const nvcv::ImageBatch &out, const NVCVInterpolationType minInterpolation, - const NVCVInterpolationType magInterpolation, bool antialias, const HQResizeRoisF roi) + const NVCVInterpolationType magInterpolation, bool antialias, + const HQResizeRoisF roi) const { - nvcv::detail::CheckThrow(cvcudaHQResizeImageBatchSubmit(m_handle, stream, &ws, in.handle(), out.handle(), + nvcv::detail::CheckThrow(cvcudaHQResizeImageBatchSubmit(m_handle.get(), stream, &ws, in.handle(), out.handle(), minInterpolation, magInterpolation, antialias, roi)); } inline void HQResize::operator()(cudaStream_t stream, const Workspace &ws, const nvcv::TensorBatch &in, const nvcv::TensorBatch &out, const NVCVInterpolationType minInterpolation, - const NVCVInterpolationType magInterpolation, bool antialias, const HQResizeRoisF roi) + const NVCVInterpolationType magInterpolation, bool antialias, + const HQResizeRoisF roi) const { - nvcv::detail::CheckThrow(cvcudaHQResizeTensorBatchSubmit(m_handle, stream, &ws, in.handle(), out.handle(), + nvcv::detail::CheckThrow(cvcudaHQResizeTensorBatchSubmit(m_handle.get(), stream, &ws, in.handle(), out.handle(), minInterpolation, magInterpolation, antialias, roi)); } inline NVCVOperatorHandle HQResize::handle() const noexcept { - return m_handle; + return m_handle.get(); } } // namespace cvcuda +/** @} */ + #endif // CVCUDA_HQ_RESIZE_HPP diff --git a/src/cvcuda/include/cvcuda/OpHistogram.h b/src/cvcuda/include/cvcuda/OpHistogram.h index bb5e77fff..dbc76c10d 100644 --- a/src/cvcuda/include/cvcuda/OpHistogram.h +++ b/src/cvcuda/include/cvcuda/OpHistogram.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -55,7 +55,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaHistogramCreate(NVCVOperatorHandle *handle); * Limitations: * * Input: - * Data Layout: [kNHWC, kHWC] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] * Channels: [1] * * Data Type | Allowed @@ -66,11 +66,12 @@ CVCUDA_PUBLIC NVCVStatus cvcudaHistogramCreate(NVCVOperatorHandle *handle); * 16bit Signed | No * 32bit Unsigned | No * 32bit Signed | No + * 16bit Float | No * 32bit Float | No * 64bit Float | No * * Output: - * Data Layout: [kHWC] + * Data Layout: [kNHWC, kHWC] * Channels: [1] * * Data Type | Allowed @@ -79,8 +80,9 @@ CVCUDA_PUBLIC NVCVStatus cvcudaHistogramCreate(NVCVOperatorHandle *handle); * 8bit Signed | No * 16bit Unsigned | No * 16bit Signed | No - * 32bit Unsigned | Yes + * 32bit Unsigned | No * 32bit Signed | Yes + * 16bit Float | No * 32bit Float | No * 64bit Float | No * @@ -103,7 +105,8 @@ CVCUDA_PUBLIC NVCVStatus cvcudaHistogramCreate(NVCVOperatorHandle *handle); * * @param [out] histogram output histogram, with width of 256 and a height = N on input tensor (1 if HWC tensor). * - * @param [in] mask mask tensor, with shape the same as input tensor any value != 0 will be counted in the histogram. + * @param [in] mask Mask tensor with the same shape and layout as the input tensor; any non-zero element is counted + * in the histogram. * * @retval #NVCV_ERROR_INVALID_ARGUMENT Some parameter is outside valid range. * @retval #NVCV_ERROR_INTERNAL Internal error in the operator, invalid types passed in. @@ -116,4 +119,6 @@ CVCUDA_PUBLIC NVCVStatus cvcudaHistogramSubmit(NVCVOperatorHandle handle, cudaSt } #endif +/** @} */ + #endif /* CVCUDA__HISTOGRAM_H */ diff --git a/src/cvcuda/include/cvcuda/OpHistogram.hpp b/src/cvcuda/include/cvcuda/OpHistogram.hpp index fee5923f3..5fe834a37 100644 --- a/src/cvcuda/include/cvcuda/OpHistogram.hpp +++ b/src/cvcuda/include/cvcuda/OpHistogram.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -34,6 +34,8 @@ #include #include +#include + namespace cvcuda { class Histogram final : public IOperator @@ -41,41 +43,37 @@ class Histogram final : public IOperator public: explicit Histogram(); - ~Histogram(); - void operator()(cudaStream_t stream, const nvcv::Tensor &in, nvcv::OptionalTensorConstRef mask, - const nvcv::Tensor &histogram); + const nvcv::Tensor &histogram) const; - virtual NVCVOperatorHandle handle() const noexcept override; + NVCVOperatorHandle handle() const noexcept override; private: - NVCVOperatorHandle m_handle; + detail::OperatorHandle m_handle; }; inline Histogram::Histogram() { - nvcv::detail::CheckThrow(cvcudaHistogramCreate(&m_handle)); - assert(m_handle); -} - -inline Histogram::~Histogram() -{ - nvcvOperatorDestroy(m_handle); - m_handle = nullptr; + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaHistogramCreate(&h)); + assert(h); + m_handle = detail::OperatorHandle{h}; } inline void Histogram::operator()(cudaStream_t stream, const nvcv::Tensor &in, nvcv::OptionalTensorConstRef mask, - const nvcv::Tensor &histogram) + const nvcv::Tensor &histogram) const { nvcv::detail::CheckThrow( - cvcudaHistogramSubmit(m_handle, stream, in.handle(), NVCV_OPTIONAL_TO_HANDLE(mask), histogram.handle())); + cvcudaHistogramSubmit(m_handle.get(), stream, in.handle(), NVCV_OPTIONAL_TO_HANDLE(mask), histogram.handle())); } inline NVCVOperatorHandle Histogram::handle() const noexcept { - return m_handle; + return m_handle.get(); } } // namespace cvcuda +/** @} */ + #endif // CVCUDA__HISTOGRAM_HPP diff --git a/src/cvcuda/include/cvcuda/OpHistogramEq.h b/src/cvcuda/include/cvcuda/OpHistogramEq.h index 5377c6c30..4acd01915 100644 --- a/src/cvcuda/include/cvcuda/OpHistogramEq.h +++ b/src/cvcuda/include/cvcuda/OpHistogramEq.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -56,8 +56,8 @@ CVCUDA_PUBLIC NVCVStatus cvcudaHistogramEqCreate(NVCVOperatorHandle *handle, uin * Limitations: * * Input: - * Data Layout: [kNHWC, kHWC] - * Channels: [1, 2, 3, 4] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 2, 3, 4] (kNCHW/kCHW support [1, 3, 4]) * * Data Type | Allowed * -------------- | ------------- @@ -67,12 +67,13 @@ CVCUDA_PUBLIC NVCVStatus cvcudaHistogramEqCreate(NVCVOperatorHandle *handle, uin * 16bit Signed | No * 32bit Unsigned | No * 32bit Signed | No + * 16bit Float | No * 32bit Float | No * 64bit Float | No * * Output: - * Data Layout: [kNHWC, kHWC] - * Channels: [1, 2, 3, 4] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 2, 3, 4] (kNCHW/kCHW support [1, 3, 4]) * * Data Type | Allowed * -------------- | ------------- @@ -82,6 +83,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaHistogramEqCreate(NVCVOperatorHandle *handle, uin * 16bit Signed | No * 32bit Unsigned | No * 32bit Signed | No + * 16bit Float | No * 32bit Float | No * 64bit Float | No * @@ -111,7 +113,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaHistogramEqCreate(NVCVOperatorHandle *handle, uin CVCUDA_PUBLIC NVCVStatus cvcudaHistogramEqSubmit(NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle in, NVCVTensorHandle out); /** - * Executes the Gaussian operation on a batch of images. + * Executes the HistogramEq operation on a batch of images. * * @param[in] in Input image batch. * @param[out] out Output image batch. @@ -124,4 +126,6 @@ CVCUDA_PUBLIC NVCVStatus cvcudaHistogramEqVarShapeSubmit(NVCVOperatorHandle hand } #endif +/** @} */ + #endif /* CVCUDA__HISTOGRAM_EQ_H */ diff --git a/src/cvcuda/include/cvcuda/OpHistogramEq.hpp b/src/cvcuda/include/cvcuda/OpHistogramEq.hpp index 7b3aac7b6..7c521e9d6 100644 --- a/src/cvcuda/include/cvcuda/OpHistogramEq.hpp +++ b/src/cvcuda/include/cvcuda/OpHistogramEq.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -35,6 +35,8 @@ #include #include +#include + namespace cvcuda { class HistogramEq final : public IOperator @@ -42,45 +44,41 @@ class HistogramEq final : public IOperator public: explicit HistogramEq(uint32_t maxBatchSize); - ~HistogramEq(); - - void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out); - void operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out); + void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out) const; + void operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out) const; - virtual NVCVOperatorHandle handle() const noexcept override; + NVCVOperatorHandle handle() const noexcept override; private: - NVCVOperatorHandle m_handle; + detail::OperatorHandle m_handle; }; inline HistogramEq::HistogramEq(uint32_t maxBatchSize) { - nvcv::detail::CheckThrow(cvcudaHistogramEqCreate(&m_handle, maxBatchSize)); - assert(m_handle); -} - -inline HistogramEq::~HistogramEq() -{ - nvcvOperatorDestroy(m_handle); - m_handle = nullptr; + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaHistogramEqCreate(&h, maxBatchSize)); + assert(h); + m_handle = detail::OperatorHandle{h}; } -inline void HistogramEq::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out) +inline void HistogramEq::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out) const { - nvcv::detail::CheckThrow(cvcudaHistogramEqSubmit(m_handle, stream, in.handle(), out.handle())); + nvcv::detail::CheckThrow(cvcudaHistogramEqSubmit(m_handle.get(), stream, in.handle(), out.handle())); } inline void HistogramEq::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, - const nvcv::ImageBatchVarShape &out) + const nvcv::ImageBatchVarShape &out) const { - nvcv::detail::CheckThrow(cvcudaHistogramEqVarShapeSubmit(m_handle, stream, in.handle(), out.handle())); + nvcv::detail::CheckThrow(cvcudaHistogramEqVarShapeSubmit(m_handle.get(), stream, in.handle(), out.handle())); } inline NVCVOperatorHandle HistogramEq::handle() const noexcept { - return m_handle; + return m_handle.get(); } } // namespace cvcuda +/** @} */ + #endif // CVCUDA__HISTOGRAM_EQ_HPP diff --git a/src/cvcuda/include/cvcuda/OpInpaint.h b/src/cvcuda/include/cvcuda/OpInpaint.h index b29f3d34a..1a78969ce 100644 --- a/src/cvcuda/include/cvcuda/OpInpaint.h +++ b/src/cvcuda/include/cvcuda/OpInpaint.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -65,8 +65,8 @@ CVCUDA_PUBLIC NVCVStatus cvcudaInpaintCreate(NVCVOperatorHandle *handle, int32_t * Limitations: * * Input: - * Data Layout: [kNHWC, kHWC] - * Channels: [1, 2, 3, 4] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 2, 3, 4] (planar kNCHW/kCHW: [1, 3, 4]) * * Data Type | Allowed * -------------- | ------------- @@ -76,6 +76,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaInpaintCreate(NVCVOperatorHandle *handle, int32_t * 16bit Signed | No * 32bit Unsigned | No * 32bit Signed | Yes + * 16bit Float | No * 32bit Float | Yes * 64bit Float | No * @@ -91,12 +92,13 @@ CVCUDA_PUBLIC NVCVStatus cvcudaInpaintCreate(NVCVOperatorHandle *handle, int32_t * 16bit Signed | No * 32bit Unsigned | No * 32bit Signed | No + * 16bit Float | No * 32bit Float | No * 64bit Float | No * * Output: - * Data Layout: [kNHWC, kHWC] - * Channels: [1, 2, 3, 4] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 2, 3, 4] (planar kNCHW/kCHW: [1, 3, 4]) * * Data Type | Allowed * -------------- | ------------- @@ -107,6 +109,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaInpaintCreate(NVCVOperatorHandle *handle, int32_t * 32bit Unsigned | No * 32bit Signed | Yes * 32bit Float | Yes + * 16bit Float | No * 64bit Float | No * * Input/Output dependency diff --git a/src/cvcuda/include/cvcuda/OpInpaint.hpp b/src/cvcuda/include/cvcuda/OpInpaint.hpp index c2c6ace2f..9b3a941b9 100644 --- a/src/cvcuda/include/cvcuda/OpInpaint.hpp +++ b/src/cvcuda/include/cvcuda/OpInpaint.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -35,6 +35,8 @@ #include #include +#include + namespace cvcuda { class Inpaint final : public IOperator @@ -42,52 +44,48 @@ class Inpaint final : public IOperator public: explicit Inpaint(int32_t maxBatchSize, nvcv::Size2D maxShape); - ~Inpaint(); - void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &masks, const nvcv::Tensor &out, - double inpaintRadius); + double inpaintRadius) const; void operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &masks, - const nvcv::ImageBatchVarShape &out, double inpaintRadius); + const nvcv::ImageBatchVarShape &out, double inpaintRadius) const; - virtual NVCVOperatorHandle handle() const noexcept override; + NVCVOperatorHandle handle() const noexcept override; private: - NVCVOperatorHandle m_handle; + detail::OperatorHandle m_handle; }; inline Inpaint::Inpaint(int32_t maxBatchSize, nvcv::Size2D maxShape) { - nvcv::detail::CheckThrow(cvcudaInpaintCreate(&m_handle, maxBatchSize, maxShape.h, maxShape.w)); - assert(m_handle); -} - -inline Inpaint::~Inpaint() -{ - nvcvOperatorDestroy(m_handle); - m_handle = nullptr; + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaInpaintCreate(&h, maxBatchSize, maxShape.h, maxShape.w)); + assert(h); + m_handle = detail::OperatorHandle{h}; } inline void Inpaint::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &masks, - const nvcv::Tensor &out, double inpaintRadius) + const nvcv::Tensor &out, double inpaintRadius) const { nvcv::detail::CheckThrow( - cvcudaInpaintSubmit(m_handle, stream, in.handle(), masks.handle(), out.handle(), inpaintRadius)); + cvcudaInpaintSubmit(m_handle.get(), stream, in.handle(), masks.handle(), out.handle(), inpaintRadius)); } inline void Inpaint::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &masks, const nvcv::ImageBatchVarShape &out, - double inpaintRadius) + double inpaintRadius) const { nvcv::detail::CheckThrow( - cvcudaInpaintVarShapeSubmit(m_handle, stream, in.handle(), masks.handle(), out.handle(), inpaintRadius)); + cvcudaInpaintVarShapeSubmit(m_handle.get(), stream, in.handle(), masks.handle(), out.handle(), inpaintRadius)); } inline NVCVOperatorHandle Inpaint::handle() const noexcept { - return m_handle; + return m_handle.get(); } } // namespace cvcuda +/** @} */ + #endif // CVCUDA_INPAINT_HPP diff --git a/src/cvcuda/include/cvcuda/OpInvert.h b/src/cvcuda/include/cvcuda/OpInvert.h new file mode 100644 index 000000000..3383ab99b --- /dev/null +++ b/src/cvcuda/include/cvcuda/OpInvert.h @@ -0,0 +1,142 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +/** + * @file OpInvert.h + * + * @brief Inverts an image — the photometric negative @f$ out = bound - in @f$, per element. + * + * Invert computes the per-element photometric negative @f$ out = bound - in @f$, where @p bound + * is the maximum representable value of the data type (255 for 8-bit unsigned, 65535 for 16-bit + * unsigned, and 1.0 for 32-bit float). The operation is element-wise and channel-independent. + * + * Reference: mimics torchvision.transforms.v2.functional.invert (equivalently OpenCV + * cv::bitwise_not for the unsigned-integer cases). The mapping is exactly invertible, so the + * result is bit-exact with no rounding. + * + * @defgroup NVCV_C_ALGORITHM__INVERT Invert + * @{ + */ + +#ifndef CVCUDA__INVERT_H +#define CVCUDA__INVERT_H + +#include "Operator.h" +#include "detail/Export.h" + +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + +/** Constructs an instance of the Invert operator. + * + * @param [out] handle Where the image instance handle will be written to. + * + Must not be NULL. + * + * @retval #NVCV_ERROR_INVALID_ARGUMENT Handle is null. + * @retval #NVCV_ERROR_OUT_OF_MEMORY Not enough memory to create the operator. + * @retval #NVCV_SUCCESS Operation executed successfully. + */ +CVCUDA_PUBLIC NVCVStatus cvcudaInvertCreate(NVCVOperatorHandle *handle); + +/** Executes the Invert operation on the given cuda stream. This operation does not + * wait for completion. + * + * Limitations: + * + * Input: + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 3, 4] + * + * Data Type | Allowed + * -------------- | ------------- + * 8bit Unsigned | Yes + * 8bit Signed | No + * 16bit Unsigned | Yes + * 16bit Signed | No + * 32bit Unsigned | No + * 32bit Signed | No + * 32bit Float | Yes + * 64bit Float | No + * + * Output: + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 3, 4] + * + * Data Type | Allowed + * -------------- | ------------- + * 8bit Unsigned | Yes + * 8bit Signed | No + * 16bit Unsigned | Yes + * 16bit Signed | No + * 32bit Unsigned | No + * 32bit Signed | No + * 32bit Float | Yes + * 64bit Float | No + * + * Input/Output dependency + * + * Property | Input == Output + * -------------- | ------------- + * Data Layout | Yes + * Data Type | Yes + * Number | Yes + * Channels | Yes + * Width | Yes + * Height | Yes + * + * @param [in] handle Handle to the operator. + * + Must not be NULL. + * @param [in] stream Handle to a valid CUDA stream. + * + * @param [in] in input tensor. + * + * @param [out] out output tensor. + * + * @retval #NVCV_ERROR_INVALID_ARGUMENT Some parameter is outside valid range. + * @retval #NVCV_ERROR_INTERNAL Internal error in the operator, invalid types passed in. + * @retval #NVCV_SUCCESS Operation executed successfully. + */ +CVCUDA_PUBLIC NVCVStatus cvcudaInvertSubmit(NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle in, + NVCVTensorHandle out); + +/** Executes the Invert operation on a batch of variable-shaped images on the given cuda stream. + * Same limitations as cvcudaInvertSubmit. + * + * @param [in] handle Handle to the operator. Must not be NULL. + * @param [in] stream Handle to a valid CUDA stream. + * @param [in] in input image batch. + * @param [out] out output image batch. + * + * @retval #NVCV_ERROR_INVALID_ARGUMENT Some parameter is outside valid range. + * @retval #NVCV_ERROR_INTERNAL Internal error in the operator, invalid types passed in. + * @retval #NVCV_SUCCESS Operation executed successfully. + */ +CVCUDA_PUBLIC NVCVStatus cvcudaInvertVarShapeSubmit(NVCVOperatorHandle handle, cudaStream_t stream, + NVCVImageBatchHandle in, NVCVImageBatchHandle out); + +#ifdef __cplusplus +} +#endif + +#endif /* CVCUDA__INVERT_H */ diff --git a/src/cvcuda/include/cvcuda/OpInvert.hpp b/src/cvcuda/include/cvcuda/OpInvert.hpp new file mode 100644 index 000000000..d80fb1af2 --- /dev/null +++ b/src/cvcuda/include/cvcuda/OpInvert.hpp @@ -0,0 +1,80 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +/** + * @file OpInvert.hpp + * + * @brief Defines the public C++ Class for the Invert operation. + * @defgroup NVCV_CPP_ALGORITHM__INVERT Invert + * @{ + */ + +#ifndef CVCUDA__INVERT_HPP +#define CVCUDA__INVERT_HPP + +#include "IOperator.hpp" +#include "OpInvert.h" + +#include +#include +#include +#include +#include + +namespace cvcuda { + +class Invert final : public IOperator +{ +public: + explicit Invert(); + + void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out) const; + + void operator()(cudaStream_t stream, const nvcv::ImageBatch &in, const nvcv::ImageBatch &out) const; + + NVCVOperatorHandle handle() const noexcept override; + +private: + detail::OperatorHandle m_handle; +}; + +inline Invert::Invert() +{ + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaInvertCreate(&h)); + assert(h); + m_handle = detail::OperatorHandle{h}; +} + +inline void Invert::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out) const +{ + nvcv::detail::CheckThrow(cvcudaInvertSubmit(m_handle.get(), stream, in.handle(), out.handle())); +} + +inline void Invert::operator()(cudaStream_t stream, const nvcv::ImageBatch &in, const nvcv::ImageBatch &out) const +{ + nvcv::detail::CheckThrow(cvcudaInvertVarShapeSubmit(m_handle.get(), stream, in.handle(), out.handle())); +} + +inline NVCVOperatorHandle Invert::handle() const noexcept +{ + return m_handle.get(); +} + +} // namespace cvcuda + +#endif // CVCUDA__INVERT_HPP diff --git a/src/cvcuda/include/cvcuda/OpJointBilateralFilter.h b/src/cvcuda/include/cvcuda/OpJointBilateralFilter.h index f3665b822..00e2c2e33 100644 --- a/src/cvcuda/include/cvcuda/OpJointBilateralFilter.h +++ b/src/cvcuda/include/cvcuda/OpJointBilateralFilter.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -60,49 +60,52 @@ CVCUDA_PUBLIC NVCVStatus cvcudaJointBilateralFilterCreate(NVCVOperatorHandle *ha * Destination must be same format and size as source * * Input: - * Data Layout: [kNHWC, kHWC] - * Channels: [1, 3, 4] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 2, 3, 4] (planar kNCHW/kCHW: [1, 3, 4]) * * Data Type | Allowed * -------------- | ------------- * 8bit Unsigned | Yes - * 8bit Signed | Yes + * 8bit Signed | No * 16bit Unsigned | Yes * 16bit Signed | Yes - * 32bit Unsigned | Yes + * 32bit Unsigned | No * 32bit Signed | Yes + * 16bit Float | No * 32bit Float | Yes - * 64bit Float | Yes + * 64bit Float | No * * InputColor: - * Data Layout: [kNHWC, kHWC] - * Channels: [1, 3, 4] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 2, 3, 4] (planar kNCHW/kCHW: [1, 3, 4]) * * Data Type | Allowed * -------------- | ------------- * 8bit Unsigned | Yes - * 8bit Signed | Yes + * 8bit Signed | No * 16bit Unsigned | Yes * 16bit Signed | Yes - * 32bit Unsigned | Yes + * 32bit Unsigned | No * 32bit Signed | Yes + * 16bit Float | No * 32bit Float | Yes - * 64bit Float | Yes + * 64bit Float | No * * Output: - * Data Layout: [kNHWC, kHWC] - * Channels: [1, 3, 4] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 2, 3, 4] (planar kNCHW/kCHW: [1, 3, 4]) * * Data Type | Allowed * -------------- | ------------- * 8bit Unsigned | Yes - * 8bit Signed | Yes + * 8bit Signed | No * 16bit Unsigned | Yes * 16bit Signed | Yes - * 32bit Unsigned | Yes + * 32bit Unsigned | No * 32bit Signed | Yes + * 16bit Float | No * 32bit Float | Yes - * 64bit Float | Yes + * 64bit Float | No * * Input/Output dependency * @@ -150,4 +153,6 @@ CVCUDA_PUBLIC NVCVStatus cvcudaJointBilateralFilterVarShapeSubmit( } #endif +/** @} */ + #endif /* CVCUDA_JOINT_BILATERAL_FILTER_H */ diff --git a/src/cvcuda/include/cvcuda/OpJointBilateralFilter.hpp b/src/cvcuda/include/cvcuda/OpJointBilateralFilter.hpp index 93236480b..129c20777 100644 --- a/src/cvcuda/include/cvcuda/OpJointBilateralFilter.hpp +++ b/src/cvcuda/include/cvcuda/OpJointBilateralFilter.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -36,6 +36,8 @@ #include #include +#include + namespace cvcuda { class JointBilateralFilter final : public IOperator @@ -43,56 +45,53 @@ class JointBilateralFilter final : public IOperator public: explicit JointBilateralFilter(); - ~JointBilateralFilter(); - void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &inColor, const nvcv::Tensor &out, - int diameter, float sigmaColor, float sigmaSpace, NVCVBorderType borderMode); + int diameter, float sigmaColor, float sigmaSpace, NVCVBorderType borderMode) const; void operator()(cudaStream_t stream, const nvcv::ImageBatch &in, const nvcv::ImageBatch &inColor, const nvcv::ImageBatch &out, const nvcv::Tensor &diameterData, const nvcv::Tensor &sigmaColorData, - const nvcv::Tensor &sigmaSpace, NVCVBorderType borderMode); + const nvcv::Tensor &sigmaSpace, NVCVBorderType borderMode) const; - virtual NVCVOperatorHandle handle() const noexcept override; + NVCVOperatorHandle handle() const noexcept override; private: - NVCVOperatorHandle m_handle; + detail::OperatorHandle m_handle; }; inline JointBilateralFilter::JointBilateralFilter() { - nvcv::detail::CheckThrow(cvcudaJointBilateralFilterCreate(&m_handle)); - assert(m_handle); -} - -inline JointBilateralFilter::~JointBilateralFilter() -{ - nvcvOperatorDestroy(m_handle); - m_handle = nullptr; + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaJointBilateralFilterCreate(&h)); + assert(h); + m_handle = detail::OperatorHandle{h}; } inline void JointBilateralFilter::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &inColor, const nvcv::Tensor &out, int diameter, float sigmaColor, float sigmaSpace, - NVCVBorderType borderMode) + NVCVBorderType borderMode) const { - nvcv::detail::CheckThrow(cvcudaJointBilateralFilterSubmit( - m_handle, stream, in.handle(), inColor.handle(), out.handle(), diameter, sigmaColor, sigmaSpace, borderMode)); + nvcv::detail::CheckThrow(cvcudaJointBilateralFilterSubmit(m_handle.get(), stream, in.handle(), inColor.handle(), + out.handle(), diameter, sigmaColor, sigmaSpace, + borderMode)); } inline void JointBilateralFilter::operator()(cudaStream_t stream, const nvcv::ImageBatch &in, const nvcv::ImageBatch &inColor, const nvcv::ImageBatch &out, const nvcv::Tensor &diameterData, const nvcv::Tensor &sigmaColorData, - const nvcv::Tensor &sigmaSpaceData, NVCVBorderType borderMode) + const nvcv::Tensor &sigmaSpaceData, NVCVBorderType borderMode) const { nvcv::detail::CheckThrow(cvcudaJointBilateralFilterVarShapeSubmit( - m_handle, stream, in.handle(), inColor.handle(), out.handle(), diameterData.handle(), sigmaColorData.handle(), - sigmaSpaceData.handle(), borderMode)); + m_handle.get(), stream, in.handle(), inColor.handle(), out.handle(), diameterData.handle(), + sigmaColorData.handle(), sigmaSpaceData.handle(), borderMode)); } inline NVCVOperatorHandle JointBilateralFilter::handle() const noexcept { - return m_handle; + return m_handle.get(); } } // namespace cvcuda +/** @} */ + #endif // CVCUDA_JOINT_BILATERAL_FILTER_HPP diff --git a/src/cvcuda/include/cvcuda/OpJpegCompressionDistortion.h b/src/cvcuda/include/cvcuda/OpJpegCompressionDistortion.h new file mode 100644 index 000000000..6cb7d2891 --- /dev/null +++ b/src/cvcuda/include/cvcuda/OpJpegCompressionDistortion.h @@ -0,0 +1,224 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +/** + * @file OpJpegCompressionDistortion.h + * + * @brief C API for JpegCompressionDistortion: simulates JPEG compression artifacts (full-range + * JFIF YCbCr, 4:2:0 chroma subsampling, per-8x8-block DCT quantization) without a codec. + * @defgroup NVCV_C_ALGORITHM__JPEG_COMPRESSION_DISTORTION Jpeg Compression Distortion + * @{ + */ + +#ifndef CVCUDA__JPEG_COMPRESSION_DISTORTION_H +#define CVCUDA__JPEG_COMPRESSION_DISTORTION_H + +#include "Operator.h" +#include "detail/Export.h" + +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + +/** Constructs an instance of the JpegCompressionDistortion operator. + * + * @param [out] handle Where the operator instance handle will be written to. + * + Must not be NULL. + * + * @retval #NVCV_ERROR_INVALID_ARGUMENT Handle is null. + * @retval #NVCV_ERROR_OUT_OF_MEMORY Not enough memory to create the operator. + * @retval #NVCV_SUCCESS Operation executed successfully. + */ +CVCUDA_PUBLIC NVCVStatus cvcudaJpegCompressionDistortionCreate(NVCVOperatorHandle *handle); + +/** Executes the JpegCompressionDistortion operation on the given cuda stream. This operation does not + * wait for completion. + * + * JpegCompressionDistortion simulates the artifacts of a JPEG compression/decompression round + * trip: 3-channel RGB images are converted to full-range JFIF YCbCr, chroma is 4:2:0 subsampled + * (2x2 box average on RGB, nearest-neighbor upsampling on reconstruction), and every 8x8 block of + * each plane goes through a DCT, quantization with the JPEG Annex-K tables scaled by the libjpeg + * quality mapping, dequantization and inverse DCT. 1-channel images are treated as a bare luma + * plane (DCT/quantization only, no color conversion or chroma path). Entropy coding is not + * simulated, so results approximate — but do not bit-match — a real JPEG codec round trip. + * + * Reference: mimics `torchvision.transforms.v2.functional.jpeg` (approximately; torchvision runs + * a real libjpeg round trip on the CPU). Algorithm ported from NVIDIA DALI's + * `JpegCompressionDistortion` GPU kernel (dali/kernels/imgproc/jpeg, Apache-2.0). + * + * Limitations: + * + * Input: + * Data Layout: [NVCV_TENSOR_NHWC, NVCV_TENSOR_HWC, NVCV_TENSOR_NCHW, NVCV_TENSOR_CHW] + * Channels: [1, 3] + * + * Data Type | Allowed + * -------------- | ------------- + * 8bit Unsigned | Yes + * 8bit Signed | No + * 16bit Unsigned | No + * 16bit Signed | No + * 32bit Unsigned | No + * 32bit Signed | No + * 16bit Float | No + * 32bit Float | No + * 64bit Float | No + * + * Output: + * Data Layout: [NVCV_TENSOR_NHWC, NVCV_TENSOR_HWC, NVCV_TENSOR_NCHW, NVCV_TENSOR_CHW] + * Channels: [1, 3] + * + * Data Type | Allowed + * -------------- | ------------- + * 8bit Unsigned | Yes + * 8bit Signed | No + * 16bit Unsigned | No + * 16bit Signed | No + * 32bit Unsigned | No + * 32bit Signed | No + * 16bit Float | No + * 32bit Float | No + * 64bit Float | No + * + * Input/Output dependency + * + * Property | Input == Output + * -------------- | ------------- + * Data Layout | Yes + * Data Type | Yes + * Number | Yes + * Channels | Yes + * Width | Yes + * Height | Yes + * + * quality Tensor + * + * Must be rank-1 ('N') and packed, with one value per image (length == batch size). + * Data Type must be TYPE_S32. + * Values are clamped to [1, 100] on the device (matching NVIDIA DALI); they are not + * validated on the host. + * + * @param [in] handle Handle to the operator. + * + Must not be NULL. + * @param [in] stream Handle to a valid CUDA stream. + * + * @param [in] in Input tensor. + * + * @param [out] out Output tensor. + * + * @param [in] quality Per-image JPEG quality tensor, from 1 (strongest distortion) to 100 + * (weakest). See the quality Tensor requirements above. + * + * @retval #NVCV_ERROR_INVALID_ARGUMENT Some parameter is outside valid range. + * @retval #NVCV_ERROR_INTERNAL Internal error in the operator, invalid types passed in. + * @retval #NVCV_SUCCESS Operation executed successfully. + */ +CVCUDA_PUBLIC NVCVStatus cvcudaJpegCompressionDistortionSubmit(NVCVOperatorHandle handle, cudaStream_t stream, + NVCVTensorHandle in, NVCVTensorHandle out, + NVCVTensorHandle quality); + +/** Executes the JpegCompressionDistortion operation with a single quality for the whole batch. + * + * This parameter-tensor-free variant passes \p quality by value, avoiding a device parameter + * tensor. Semantics and Limitations are identical to #cvcudaJpegCompressionDistortionSubmit, + * except that the scalar quality is validated on the host: values outside [1, 100] are rejected + * with #NVCV_ERROR_INVALID_ARGUMENT (matching torchvision's argument validation) instead of being + * clamped. + * + * @param [in] handle Handle to the operator. + * + Must not be NULL. + * @param [in] stream Handle to a valid CUDA stream. + * + * @param [in] in Input tensor. + * + * @param [out] out Output tensor. + * + * @param [in] quality JPEG quality applied to all images, from 1 (strongest distortion) to 100 + * (weakest). Must be in [1, 100]. + * + * @retval #NVCV_ERROR_INVALID_ARGUMENT Some parameter is outside valid range. + * @retval #NVCV_ERROR_INTERNAL Internal error in the operator, invalid types passed in. + * @retval #NVCV_SUCCESS Operation executed successfully. + */ +CVCUDA_PUBLIC NVCVStatus cvcudaJpegCompressionDistortionScalarSubmit(NVCVOperatorHandle handle, cudaStream_t stream, + NVCVTensorHandle in, NVCVTensorHandle out, + int32_t quality); + +/** Executes the JpegCompressionDistortion operation on a variable-shape image batch. + * + * Semantics, data-type and channel constraints match #cvcudaJpegCompressionDistortionSubmit. + * All images in a batch must share one image format, which must be RGB(8) for 3 channels (packed + * or planar) or U8/Y8 for 1 channel, without chroma subsampling or extra channels. + * + * @param [in] handle Handle to the operator. + * + Must not be NULL. + * @param [in] stream Handle to a valid CUDA stream. + * + * @param [in] in Input image batch. + * + * @param [out] out Output image batch. + * + * @param [in] quality Per-image JPEG quality tensor, from 1 (strongest distortion) to 100 + * (weakest). Must be rank-1, packed, TYPE_S32, with one value per image; + * values are clamped to [1, 100] on the device. + * + * @retval #NVCV_ERROR_INVALID_ARGUMENT Some parameter is outside valid range. + * @retval #NVCV_ERROR_INTERNAL Internal error in the operator, invalid types passed in. + * @retval #NVCV_SUCCESS Operation executed successfully. + */ +CVCUDA_PUBLIC NVCVStatus cvcudaJpegCompressionDistortionVarShapeSubmit(NVCVOperatorHandle handle, cudaStream_t stream, + NVCVImageBatchHandle in, + NVCVImageBatchHandle out, + NVCVTensorHandle quality); + +/** Executes the JpegCompressionDistortion operation on a variable-shape image batch with a single + * quality for the whole batch. + * + * Semantics match #cvcudaJpegCompressionDistortionVarShapeSubmit; the scalar quality is validated + * on the host like #cvcudaJpegCompressionDistortionScalarSubmit. + * + * @param [in] handle Handle to the operator. + * + Must not be NULL. + * @param [in] stream Handle to a valid CUDA stream. + * + * @param [in] in Input image batch. + * + * @param [out] out Output image batch. + * + * @param [in] quality JPEG quality applied to all images, from 1 (strongest distortion) to 100 + * (weakest). Must be in [1, 100]. + * + * @retval #NVCV_ERROR_INVALID_ARGUMENT Some parameter is outside valid range. + * @retval #NVCV_ERROR_INTERNAL Internal error in the operator, invalid types passed in. + * @retval #NVCV_SUCCESS Operation executed successfully. + */ +CVCUDA_PUBLIC NVCVStatus cvcudaJpegCompressionDistortionVarShapeScalarSubmit(NVCVOperatorHandle handle, + cudaStream_t stream, + NVCVImageBatchHandle in, + NVCVImageBatchHandle out, int32_t quality); + +#ifdef __cplusplus +} +#endif + +#endif /* CVCUDA__JPEG_COMPRESSION_DISTORTION_H */ diff --git a/src/cvcuda/include/cvcuda/OpJpegCompressionDistortion.hpp b/src/cvcuda/include/cvcuda/OpJpegCompressionDistortion.hpp new file mode 100644 index 000000000..b8a975ec8 --- /dev/null +++ b/src/cvcuda/include/cvcuda/OpJpegCompressionDistortion.hpp @@ -0,0 +1,106 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +/** + * @file OpJpegCompressionDistortion.hpp + * + * @brief Defines the public C++ Class for the JpegCompressionDistortion operation. + * @defgroup NVCV_CPP_ALGORITHM__JPEG_COMPRESSION_DISTORTION JpegCompressionDistortion + * @{ + */ + +#ifndef CVCUDA__JPEG_COMPRESSION_DISTORTION_HPP +#define CVCUDA__JPEG_COMPRESSION_DISTORTION_HPP + +#include "IOperator.hpp" +#include "OpJpegCompressionDistortion.h" + +#include +#include +#include +#include +#include + +namespace cvcuda { + +class JpegCompressionDistortion final : public IOperator +{ +public: + explicit JpegCompressionDistortion(); + + void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, + const nvcv::Tensor &quality) const; + + void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, int32_t quality) const; + + void operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, + const nvcv::Tensor &quality) const; + + void operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, + int32_t quality) const; + + NVCVOperatorHandle handle() const noexcept override; + +private: + detail::OperatorHandle m_handle; +}; + +inline JpegCompressionDistortion::JpegCompressionDistortion() +{ + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaJpegCompressionDistortionCreate(&h)); + assert(h); + m_handle = detail::OperatorHandle{h}; +} + +inline void JpegCompressionDistortion::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, + const nvcv::Tensor &quality) const +{ + nvcv::detail::CheckThrow( + cvcudaJpegCompressionDistortionSubmit(m_handle.get(), stream, in.handle(), out.handle(), quality.handle())); +} + +inline void JpegCompressionDistortion::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, + int32_t quality) const +{ + nvcv::detail::CheckThrow( + cvcudaJpegCompressionDistortionScalarSubmit(m_handle.get(), stream, in.handle(), out.handle(), quality)); +} + +inline void JpegCompressionDistortion::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, + const nvcv::ImageBatchVarShape &out, + const nvcv::Tensor &quality) const +{ + nvcv::detail::CheckThrow(cvcudaJpegCompressionDistortionVarShapeSubmit(m_handle.get(), stream, in.handle(), + out.handle(), quality.handle())); +} + +inline void JpegCompressionDistortion::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, + const nvcv::ImageBatchVarShape &out, int32_t quality) const +{ + nvcv::detail::CheckThrow(cvcudaJpegCompressionDistortionVarShapeScalarSubmit(m_handle.get(), stream, in.handle(), + out.handle(), quality)); +} + +inline NVCVOperatorHandle JpegCompressionDistortion::handle() const noexcept +{ + return m_handle.get(); +} + +} // namespace cvcuda + +#endif // CVCUDA__JPEG_COMPRESSION_DISTORTION_HPP diff --git a/src/cvcuda/include/cvcuda/OpLabel.h b/src/cvcuda/include/cvcuda/OpLabel.h index 12e3e5027..2a4ad175e 100644 --- a/src/cvcuda/include/cvcuda/OpLabel.h +++ b/src/cvcuda/include/cvcuda/OpLabel.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -76,7 +76,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaLabelCreate(NVCVOperatorHandle *handle); * Limitations: * * Input: - * Data Layout: [HWC], [NHWC], [DHWC], [NDHWC] + * Data Layout: [kHWC, kNHWC, kCHW, kNCHW, kDHWC, kNDHWC] and C-less HW/NHW/DHW/NDHW * Channels: [1] * * Data Type | Allowed @@ -87,11 +87,12 @@ CVCUDA_PUBLIC NVCVStatus cvcudaLabelCreate(NVCVOperatorHandle *handle); * 16bit Signed | Yes * 32bit Unsigned | Yes * 32bit Signed | Yes + * 16bit Float | No * 32bit Float | No * 64bit Float | No * * Output: - * Data Layout: [HWC], [NHWC], [DHWC], [NDHWC] + * Data Layout: [kHWC, kNHWC, kCHW, kNCHW, kDHWC, kNDHWC] and C-less HW/NHW/DHW/NDHW * Channels: [1] * * Data Type | Allowed @@ -102,6 +103,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaLabelCreate(NVCVOperatorHandle *handle); * 16bit Signed | No * 32bit Unsigned | Yes * 32bit Signed | Yes + * 16bit Float | No * 32bit Float | No * 64bit Float | No * @@ -122,14 +124,14 @@ CVCUDA_PUBLIC NVCVStatus cvcudaLabelCreate(NVCVOperatorHandle *handle); * + Must not be NULL. * @param [in] stream Handle to a valid CUDA stream. * - * @param [in] in Input tensor. The expected layout is [HWC] or [NHWC] for 2D labeling or [DHWC] or [NDHWC] for + * @param [in] in Input tensor. The expected layout is [HWC], [NHWC], [CHW] or [NCHW] for 2D labeling or [DHWC] or [NDHWC] for * 3D labeling, with either explicit C dimension or missing C with channels embedded in the data type. * The N dimension is the number of samples, i.e. either 2D images with height H and width W or * 3D volumes with depth D and height H and width W, inside the tensor. This operator labels * regions, i.e. connected components, of each input image or volume read from the \ref in tensor. * + Check above limitations table to the input tensor data layout, number of channels and data type. * - * @param [out] out Output tensor. The expected layout is [HWC] or [NHWC] for 2D labeling or [DHWC] or [NDHWC] for + * @param [out] out Output tensor. The expected layout is [HWC], [NHWC], [CHW] or [NCHW] for 2D labeling or [DHWC] or [NDHWC] for * 3D labeling, with either explicit C dimension or missing C with channels embedded in the data type. * The N dimension is the number of samples, i.e. either 2D images with height H and width W or * 3D volumes with depth D and height H and width W, inside the tensor. This operator labels @@ -220,7 +222,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaLabelCreate(NVCVOperatorHandle *handle); * the sequential labels are up to the maximum capacity M * + If not NULL, the \ref count tensor must not be NULL as well. * - * @param [in] mask Mask tensor. The expected layout is [HWC] or [NHWC] for 2D masking or [DHWC] or [NDHWC] for 3D + * @param [in] mask Mask tensor. The expected layout is [HWC], [NHWC], [CHW] or [NCHW] for 2D masking or [DHWC] or [NDHWC] for 3D * masking, with either explicit C dimension or missing C with channels embedded in the data type. * The N dimension is the number of samples, if missing it is considered to be N=1, in case N=1 * and \ref in and \ref out tensors have N>1 the same mask is to be applied to all images (2D) or @@ -237,7 +239,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaLabelCreate(NVCVOperatorHandle *handle); * * @param [in] connectivity Specify connectivity of elements for the operator, see \ref NVCVConnectivityType. * + It must conform with \ref in and \ref out tensors, i.e. 3D labeling requires [DHWC] - * or [NDHWC] tensor layouts and 2D labeling requires [HWC] or [NHWC], where the C + * or [NDHWC] tensor layouts and 2D labeling requires [HWC], [NHWC], [CHW] or [NCHW], where the C * channel may be missing as embedded in data type. * * @param [in] assignLabels Specify how labels are assigned by the operator, see \ref NVCVLabelType. Use @@ -263,4 +265,6 @@ CVCUDA_PUBLIC NVCVStatus cvcudaLabelSubmit(NVCVOperatorHandle handle, cudaStream } #endif +/** @} */ + #endif /* CVCUDA_LABEL_H */ diff --git a/src/cvcuda/include/cvcuda/OpLabel.hpp b/src/cvcuda/include/cvcuda/OpLabel.hpp index f6b9948b0..fe3547b6c 100644 --- a/src/cvcuda/include/cvcuda/OpLabel.hpp +++ b/src/cvcuda/include/cvcuda/OpLabel.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -34,6 +34,8 @@ #include #include +#include + namespace cvcuda { class Label final : public IOperator @@ -41,29 +43,23 @@ class Label final : public IOperator public: explicit Label(); - ~Label(); - void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, const nvcv::Tensor &bgLabel, const nvcv::Tensor &minThresh, const nvcv::Tensor &maxThresh, const nvcv::Tensor &minSize, const nvcv::Tensor &count, const nvcv::Tensor &stats, const nvcv::Tensor &mask, NVCVConnectivityType connectivity, NVCVLabelType assignLabels, NVCVLabelMaskType maskType) const; - virtual NVCVOperatorHandle handle() const noexcept override; + NVCVOperatorHandle handle() const noexcept override; private: - NVCVOperatorHandle m_handle; + detail::OperatorHandle m_handle; }; inline Label::Label() { - nvcv::detail::CheckThrow(cvcudaLabelCreate(&m_handle)); - assert(m_handle); -} - -inline Label::~Label() -{ - nvcvOperatorDestroy(m_handle); - m_handle = nullptr; + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaLabelCreate(&h)); + assert(h); + m_handle = detail::OperatorHandle{h}; } inline void Label::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, @@ -72,16 +68,18 @@ inline void Label::operator()(cudaStream_t stream, const nvcv::Tensor &in, const const nvcv::Tensor &mask, NVCVConnectivityType connectivity, NVCVLabelType assignLabels, NVCVLabelMaskType maskType) const { - nvcv::detail::CheckThrow(cvcudaLabelSubmit(m_handle, stream, in.handle(), out.handle(), bgLabel.handle(), + nvcv::detail::CheckThrow(cvcudaLabelSubmit(m_handle.get(), stream, in.handle(), out.handle(), bgLabel.handle(), minThresh.handle(), maxThresh.handle(), minSize.handle(), count.handle(), stats.handle(), mask.handle(), connectivity, assignLabels, maskType)); } inline NVCVOperatorHandle Label::handle() const noexcept { - return m_handle; + return m_handle.get(); } } // namespace cvcuda +/** @} */ + #endif // CVCUDA_LABEL_HPP diff --git a/src/cvcuda/include/cvcuda/OpLaplacian.h b/src/cvcuda/include/cvcuda/OpLaplacian.h index 719356be3..ac73ba141 100644 --- a/src/cvcuda/include/cvcuda/OpLaplacian.h +++ b/src/cvcuda/include/cvcuda/OpLaplacian.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -57,8 +57,9 @@ CVCUDA_PUBLIC NVCVStatus cvcudaLaplacianCreate(NVCVOperatorHandle *handle); * Limitations: * * Input: - * Data Layout: [kNHWC, kHWC] - * Channels: [1, 3, 4] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 3, 4] (planar kNCHW/kCHW: [1, 3, 4]) + * Planar tensors must be tightly packed with N*C <= 65535. * * Data Type | Allowed * -------------- | ------------- @@ -68,12 +69,14 @@ CVCUDA_PUBLIC NVCVStatus cvcudaLaplacianCreate(NVCVOperatorHandle *handle); * 16bit Signed | No * 32bit Unsigned | No * 32bit Signed | No + * 16bit Float | No * 32bit Float | Yes * 64bit Float | No * * Output: - * Data Layout: [kNHWC, kHWC] - * Channels: [1, 3, 4] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 3, 4] (planar kNCHW/kCHW: [1, 3, 4]) + * Planar tensors must be tightly packed with N*C <= 65535. * * Data Type | Allowed * -------------- | ------------- @@ -83,6 +86,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaLaplacianCreate(NVCVOperatorHandle *handle); * 16bit Signed | No * 32bit Unsigned | No * 32bit Signed | No + * 16bit Float | No * 32bit Float | Yes * 64bit Float | No * @@ -139,4 +143,6 @@ CVCUDA_PUBLIC NVCVStatus cvcudaLaplacianVarShapeSubmit(NVCVOperatorHandle handle } #endif +/** @} */ + #endif /* CVCUDA_LAPLACIAN_H */ diff --git a/src/cvcuda/include/cvcuda/OpLaplacian.hpp b/src/cvcuda/include/cvcuda/OpLaplacian.hpp index e00056ab6..0e250167f 100644 --- a/src/cvcuda/include/cvcuda/OpLaplacian.hpp +++ b/src/cvcuda/include/cvcuda/OpLaplacian.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -35,6 +35,8 @@ #include #include +#include + namespace cvcuda { class Laplacian final : public IOperator @@ -42,51 +44,47 @@ class Laplacian final : public IOperator public: explicit Laplacian(); - ~Laplacian(); - void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, int32_t ksize, float scale, - NVCVBorderType borderMode); + NVCVBorderType borderMode) const; void operator()(cudaStream_t stream, const nvcv::ImageBatch &in, const nvcv::ImageBatch &out, - const nvcv::Tensor &ksize, const nvcv::Tensor &scale, NVCVBorderType borderMode); + const nvcv::Tensor &ksize, const nvcv::Tensor &scale, NVCVBorderType borderMode) const; - virtual NVCVOperatorHandle handle() const noexcept override; + NVCVOperatorHandle handle() const noexcept override; private: - NVCVOperatorHandle m_handle; + detail::OperatorHandle m_handle; }; inline Laplacian::Laplacian() { - nvcv::detail::CheckThrow(cvcudaLaplacianCreate(&m_handle)); - assert(m_handle); -} - -inline Laplacian::~Laplacian() -{ - nvcvOperatorDestroy(m_handle); - m_handle = nullptr; + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaLaplacianCreate(&h)); + assert(h); + m_handle = detail::OperatorHandle{h}; } inline void Laplacian::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, int32_t ksize, - float scale, NVCVBorderType borderMode) + float scale, NVCVBorderType borderMode) const { nvcv::detail::CheckThrow( - cvcudaLaplacianSubmit(m_handle, stream, in.handle(), out.handle(), ksize, scale, borderMode)); + cvcudaLaplacianSubmit(m_handle.get(), stream, in.handle(), out.handle(), ksize, scale, borderMode)); } inline void Laplacian::operator()(cudaStream_t stream, const nvcv::ImageBatch &in, const nvcv::ImageBatch &out, - const nvcv::Tensor &ksize, const nvcv::Tensor &scale, NVCVBorderType borderMode) + const nvcv::Tensor &ksize, const nvcv::Tensor &scale, NVCVBorderType borderMode) const { - nvcv::detail::CheckThrow(cvcudaLaplacianVarShapeSubmit(m_handle, stream, in.handle(), out.handle(), ksize.handle(), - scale.handle(), borderMode)); + nvcv::detail::CheckThrow(cvcudaLaplacianVarShapeSubmit(m_handle.get(), stream, in.handle(), out.handle(), + ksize.handle(), scale.handle(), borderMode)); } inline NVCVOperatorHandle Laplacian::handle() const noexcept { - return m_handle; + return m_handle.get(); } } // namespace cvcuda +/** @} */ + #endif // CVCUDA_LAPLACIAN_HPP diff --git a/src/cvcuda/include/cvcuda/OpMedianBlur.h b/src/cvcuda/include/cvcuda/OpMedianBlur.h index 8cb9a3e80..b9b217318 100644 --- a/src/cvcuda/include/cvcuda/OpMedianBlur.h +++ b/src/cvcuda/include/cvcuda/OpMedianBlur.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -59,8 +59,8 @@ CVCUDA_PUBLIC NVCVStatus cvcudaMedianBlurCreate(NVCVOperatorHandle *handle, cons * Limitations: * * Input: - * Data Layout: [kNHWC, kHWC] - * Channels: [1, 3, 4] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 3, 4] (planar kNCHW/kCHW: [1, 3, 4]) * * Data Type | Allowed * -------------- | ------------- @@ -70,12 +70,13 @@ CVCUDA_PUBLIC NVCVStatus cvcudaMedianBlurCreate(NVCVOperatorHandle *handle, cons * 16bit Signed | No * 32bit Unsigned | No * 32bit Signed | No + * 16bit Float | No * 32bit Float | Yes * 64bit Float | No * * Output: - * Data Layout: [kNHWC, kHWC] - * Channels: [1, 3, 4] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 3, 4] (planar kNCHW/kCHW: [1, 3, 4]) * * Data Type | Allowed * -------------- | ------------- @@ -85,6 +86,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaMedianBlurCreate(NVCVOperatorHandle *handle, cons * 16bit Signed | No * 32bit Unsigned | No * 32bit Signed | No + * 16bit Float | No * 32bit Float | Yes * 64bit Float | No * diff --git a/src/cvcuda/include/cvcuda/OpMedianBlur.hpp b/src/cvcuda/include/cvcuda/OpMedianBlur.hpp index 34ec8bca7..dd43042f9 100644 --- a/src/cvcuda/include/cvcuda/OpMedianBlur.hpp +++ b/src/cvcuda/include/cvcuda/OpMedianBlur.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -35,6 +35,8 @@ #include #include +#include + namespace cvcuda { class MedianBlur final : public IOperator @@ -42,49 +44,47 @@ class MedianBlur final : public IOperator public: explicit MedianBlur(const int maxVarShapeBatchSize); - ~MedianBlur(); - - void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, const nvcv::Size2D ksize); + void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, + const nvcv::Size2D ksize) const; void operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, - const nvcv::Tensor &ksize); + const nvcv::Tensor &ksize) const; - virtual NVCVOperatorHandle handle() const noexcept override; + NVCVOperatorHandle handle() const noexcept override; private: - NVCVOperatorHandle m_handle; + detail::OperatorHandle m_handle; }; inline MedianBlur::MedianBlur(const int maxVarShapeBatchSize) { - nvcv::detail::CheckThrow(cvcudaMedianBlurCreate(&m_handle, maxVarShapeBatchSize)); - assert(m_handle); -} - -inline MedianBlur::~MedianBlur() -{ - nvcvOperatorDestroy(m_handle); - m_handle = nullptr; + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaMedianBlurCreate(&h, maxVarShapeBatchSize)); + assert(h); + m_handle = detail::OperatorHandle{h}; } inline void MedianBlur::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, - const nvcv::Size2D ksize) + const nvcv::Size2D ksize) const { - nvcv::detail::CheckThrow(cvcudaMedianBlurSubmit(m_handle, stream, in.handle(), out.handle(), ksize.w, ksize.h)); + nvcv::detail::CheckThrow( + cvcudaMedianBlurSubmit(m_handle.get(), stream, in.handle(), out.handle(), ksize.w, ksize.h)); } inline void MedianBlur::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, - const nvcv::ImageBatchVarShape &out, const nvcv::Tensor &ksize) + const nvcv::ImageBatchVarShape &out, const nvcv::Tensor &ksize) const { nvcv::detail::CheckThrow( - cvcudaMedianBlurVarShapeSubmit(m_handle, stream, in.handle(), out.handle(), ksize.handle())); + cvcudaMedianBlurVarShapeSubmit(m_handle.get(), stream, in.handle(), out.handle(), ksize.handle())); } inline NVCVOperatorHandle MedianBlur::handle() const noexcept { - return m_handle; + return m_handle.get(); } } // namespace cvcuda +/** @} */ + #endif // CVCUDA_MEDIAN_BLUR_HPP diff --git a/src/cvcuda/include/cvcuda/OpMinAreaRect.h b/src/cvcuda/include/cvcuda/OpMinAreaRect.h index 61bba8a22..c9dae70ee 100644 --- a/src/cvcuda/include/cvcuda/OpMinAreaRect.h +++ b/src/cvcuda/include/cvcuda/OpMinAreaRect.h @@ -1,6 +1,6 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -57,6 +57,9 @@ CVCUDA_PUBLIC NVCVStatus cvcudaMinAreaRectCreate(NVCVOperatorHandle *handle, int * * Limitations: * + * Planar image layouts: Not applicable + * Reason: Inputs are contour-coordinate tensors and outputs are rectangle parameters, not images. + * * Input: * Data Layout: [NWC] * Channels: [2] @@ -69,6 +72,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaMinAreaRectCreate(NVCVOperatorHandle *handle, int * 16bit Signed | Yes * 32bit Unsigned | No * 32bit Signed | Yes + * 16bit Float | No * 32bit Float | No * 64bit Float | No * @@ -84,6 +88,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaMinAreaRectCreate(NVCVOperatorHandle *handle, int * 16bit Signed | No * 32bit Unsigned | No * 32bit Signed | No + * 16bit Float | No * 32bit Float | Yes * 64bit Float | No * @@ -122,4 +127,6 @@ CVCUDA_PUBLIC NVCVStatus cvcudaMinAreaRectSubmit(NVCVOperatorHandle handle, cuda } #endif +/** @} */ + #endif /* CVCUDA__MIN_AREA_RECT_H */ diff --git a/src/cvcuda/include/cvcuda/OpMinAreaRect.hpp b/src/cvcuda/include/cvcuda/OpMinAreaRect.hpp index ac842a185..706063cd0 100644 --- a/src/cvcuda/include/cvcuda/OpMinAreaRect.hpp +++ b/src/cvcuda/include/cvcuda/OpMinAreaRect.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -34,6 +34,8 @@ #include #include +#include + namespace cvcuda { class MinAreaRect final : public IOperator @@ -41,41 +43,37 @@ class MinAreaRect final : public IOperator public: explicit MinAreaRect(int maxContourNum); - ~MinAreaRect(); - void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, - const nvcv::Tensor &numPointsInContour, int totalContours); + const nvcv::Tensor &numPointsInContour, int totalContours) const; - virtual NVCVOperatorHandle handle() const noexcept override; + NVCVOperatorHandle handle() const noexcept override; private: - NVCVOperatorHandle m_handle; + detail::OperatorHandle m_handle; }; inline MinAreaRect::MinAreaRect(int maxContourNum) { - nvcv::detail::CheckThrow(cvcudaMinAreaRectCreate(&m_handle, maxContourNum)); - assert(m_handle); -} - -inline MinAreaRect::~MinAreaRect() -{ - nvcvOperatorDestroy(m_handle); - m_handle = nullptr; + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaMinAreaRectCreate(&h, maxContourNum)); + assert(h); + m_handle = detail::OperatorHandle{h}; } inline void MinAreaRect::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, - const nvcv::Tensor &numPointsInContour, const int totalContours) + const nvcv::Tensor &numPointsInContour, const int totalContours) const { - nvcv::detail::CheckThrow(cvcudaMinAreaRectSubmit(m_handle, stream, in.handle(), out.handle(), + nvcv::detail::CheckThrow(cvcudaMinAreaRectSubmit(m_handle.get(), stream, in.handle(), out.handle(), numPointsInContour.handle(), totalContours)); } inline NVCVOperatorHandle MinAreaRect::handle() const noexcept { - return m_handle; + return m_handle.get(); } } // namespace cvcuda +/** @} */ + #endif // CVCUDA__MIN_AREA_RECT_HPP diff --git a/src/cvcuda/include/cvcuda/OpMinMaxLoc.h b/src/cvcuda/include/cvcuda/OpMinMaxLoc.h index 4a308cf9f..88142ad25 100644 --- a/src/cvcuda/include/cvcuda/OpMinMaxLoc.h +++ b/src/cvcuda/include/cvcuda/OpMinMaxLoc.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -58,7 +58,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaMinMaxLocCreate(NVCVOperatorHandle *handle); * Limitations: * * Input: - * Data Layout: [HWC, NHWC, CHW, NCHW] + * Data Layout: [HW, NHW, HWC, NHWC, CHW, NCHW] * Channels: [1] * * Data Type | Allowed @@ -69,6 +69,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaMinMaxLocCreate(NVCVOperatorHandle *handle); * 16bit Signed | Yes * 32bit Unsigned | Yes * 32bit Signed | Yes + * 16bit Float | No * 32bit Float | Yes * 64bit Float | Yes * @@ -76,8 +77,9 @@ CVCUDA_PUBLIC NVCVStatus cvcudaMinMaxLocCreate(NVCVOperatorHandle *handle); * + Must not be NULL. * @param [in] stream Handle to a valid CUDA stream. * - * @param [in] in Input tensor. The expected layout is [HWC] or [NHWC] or [CHW] or [NCHW], where N is the number - * of samples, i.e. images with height H and width W and channels C, inside the tensor. + * @param [in] in Input tensor. The expected layout is [HW] or [NHW] or [HWC] or [NHWC] or [CHW] or [NCHW], + * where N is the number of samples, i.e. images with height H and width W and channels C, + * inside the tensor. * * @param [out] minVal Output tensor to store minimum values found in the input tensor. The expected layout is [N] * or [NC], meaning rank-1 or rank-2 tensor with first dimension as number of samples N, and a @@ -161,4 +163,6 @@ CVCUDA_PUBLIC NVCVStatus cvcudaMinMaxLocVarShapeSubmit(NVCVOperatorHandle handle } #endif +/** @} */ + #endif /* CVCUDA_MINMAXLOC_H */ diff --git a/src/cvcuda/include/cvcuda/OpMinMaxLoc.hpp b/src/cvcuda/include/cvcuda/OpMinMaxLoc.hpp index 3e3416893..627cd50b2 100644 --- a/src/cvcuda/include/cvcuda/OpMinMaxLoc.hpp +++ b/src/cvcuda/include/cvcuda/OpMinMaxLoc.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -36,6 +36,8 @@ #include #include +#include + namespace cvcuda { class MinMaxLoc final : public IOperator @@ -43,8 +45,6 @@ class MinMaxLoc final : public IOperator public: explicit MinMaxLoc(); - ~MinMaxLoc(); - void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &minVal, const nvcv::Tensor &minLoc, const nvcv::Tensor &numMin, const nvcv::Tensor &maxVal, const nvcv::Tensor &maxLoc, const nvcv::Tensor &numMax) const; @@ -53,46 +53,45 @@ class MinMaxLoc final : public IOperator const nvcv::Tensor &minLoc, const nvcv::Tensor &numMin, const nvcv::Tensor &maxVal, const nvcv::Tensor &maxLoc, const nvcv::Tensor &numMax) const; - virtual NVCVOperatorHandle handle() const noexcept override; + NVCVOperatorHandle handle() const noexcept override; private: - NVCVOperatorHandle m_handle; + detail::OperatorHandle m_handle; }; inline MinMaxLoc::MinMaxLoc() { - nvcv::detail::CheckThrow(cvcudaMinMaxLocCreate(&m_handle)); - assert(m_handle); -} - -inline MinMaxLoc::~MinMaxLoc() -{ - nvcvOperatorDestroy(m_handle); - m_handle = nullptr; + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaMinMaxLocCreate(&h)); + assert(h); + m_handle = detail::OperatorHandle{h}; } inline void MinMaxLoc::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &minVal, const nvcv::Tensor &minLoc, const nvcv::Tensor &numMin, const nvcv::Tensor &maxVal, const nvcv::Tensor &maxLoc, const nvcv::Tensor &numMax) const { - nvcv::detail::CheckThrow(cvcudaMinMaxLocSubmit(m_handle, stream, in.handle(), minVal.handle(), minLoc.handle(), - numMin.handle(), maxVal.handle(), maxLoc.handle(), numMax.handle())); + nvcv::detail::CheckThrow(cvcudaMinMaxLocSubmit(m_handle.get(), stream, in.handle(), minVal.handle(), + minLoc.handle(), numMin.handle(), maxVal.handle(), maxLoc.handle(), + numMax.handle())); } inline void MinMaxLoc::operator()(cudaStream_t stream, const nvcv::ImageBatch &in, const nvcv::Tensor &minVal, const nvcv::Tensor &minLoc, const nvcv::Tensor &numMin, const nvcv::Tensor &maxVal, const nvcv::Tensor &maxLoc, const nvcv::Tensor &numMax) const { - nvcv::detail::CheckThrow(cvcudaMinMaxLocVarShapeSubmit(m_handle, stream, in.handle(), minVal.handle(), + nvcv::detail::CheckThrow(cvcudaMinMaxLocVarShapeSubmit(m_handle.get(), stream, in.handle(), minVal.handle(), minLoc.handle(), numMin.handle(), maxVal.handle(), maxLoc.handle(), numMax.handle())); } inline NVCVOperatorHandle MinMaxLoc::handle() const noexcept { - return m_handle; + return m_handle.get(); } } // namespace cvcuda +/** @} */ + #endif // CVCUDA_MINMAXLOC_HPP diff --git a/src/cvcuda/include/cvcuda/OpMorphology.h b/src/cvcuda/include/cvcuda/OpMorphology.h index 3ab9bd85d..f620a1b9f 100644 --- a/src/cvcuda/include/cvcuda/OpMorphology.h +++ b/src/cvcuda/include/cvcuda/OpMorphology.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -58,8 +58,8 @@ CVCUDA_PUBLIC NVCVStatus cvcudaMorphologyCreate(NVCVOperatorHandle *handle); * Limitations: * * Input: - * Data Layout: [kNHWC, kHWC] - * Channels: [1, 3, 4] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 3, 4] (planar kNCHW/kCHW: [1, 3, 4]) * * Data Type | Allowed * -------------- | ------------- @@ -69,12 +69,13 @@ CVCUDA_PUBLIC NVCVStatus cvcudaMorphologyCreate(NVCVOperatorHandle *handle); * 16bit Signed | No * 32bit Unsigned | No * 32bit Signed | No + * 16bit Float | No * 32bit Float | Yes * 64bit Float | No * * Output: - * Data Layout: [kNHWC, kHWC] - * Channels: [1, 3, 4] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 3, 4] (planar kNCHW/kCHW: [1, 3, 4]) * * Data Type | Allowed * -------------- | ------------- @@ -84,6 +85,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaMorphologyCreate(NVCVOperatorHandle *handle); * 16bit Signed | No * 32bit Unsigned | No * 32bit Signed | No + * 16bit Float | No * 32bit Float | Yes * 64bit Float | No * @@ -139,8 +141,8 @@ CVCUDA_PUBLIC NVCVStatus cvcudaMorphologySubmit(NVCVOperatorHandle handle, cudaS * Limitations: * * Input: - * Data Layout: [kNHWC, kHWC] - * Channels: [1, 3, 4] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 3, 4] (planar kNCHW/kCHW: [1, 3, 4]) * * Data Type | Allowed * -------------- | ------------- @@ -150,12 +152,13 @@ CVCUDA_PUBLIC NVCVStatus cvcudaMorphologySubmit(NVCVOperatorHandle handle, cudaS * 16bit Signed | No * 32bit Unsigned | No * 32bit Signed | No + * 16bit Float | No * 32bit Float | Yes * 64bit Float | No * * Output: - * Data Layout: [kNHWC, kHWC] - * Channels: [1, 3, 4] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 3, 4] (planar kNCHW/kCHW: [1, 3, 4]) * * Data Type | Allowed * -------------- | ------------- @@ -165,6 +168,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaMorphologySubmit(NVCVOperatorHandle handle, cudaS * 16bit Signed | No * 32bit Unsigned | No * 32bit Signed | No + * 16bit Float | No * 32bit Float | Yes * 64bit Float | No * @@ -218,4 +222,6 @@ CVCUDA_PUBLIC NVCVStatus cvcudaMorphologyVarShapeSubmit(NVCVOperatorHandle handl } #endif +/** @} */ + #endif /* CVCUDA_MORPHOLOGY */ diff --git a/src/cvcuda/include/cvcuda/OpMorphology.hpp b/src/cvcuda/include/cvcuda/OpMorphology.hpp index 36d124dcd..29ed1b7b8 100644 --- a/src/cvcuda/include/cvcuda/OpMorphology.hpp +++ b/src/cvcuda/include/cvcuda/OpMorphology.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -36,6 +36,8 @@ #include #include +#include + namespace cvcuda { class Morphology final : public IOperator @@ -43,41 +45,35 @@ class Morphology final : public IOperator public: explicit Morphology(); - ~Morphology(); - void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, nvcv::OptionalTensorConstRef workspace, NVCVMorphologyType morphType, const nvcv::Size2D &maskSize, - const int2 &anchor, int32_t iteration, const NVCVBorderType borderMode); + const int2 &anchor, int32_t iteration, const NVCVBorderType borderMode) const; void operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, const nvcv::OptionalImageBatchVarShapeConstRef workspace, NVCVMorphologyType morphType, const nvcv::Tensor &masks, const nvcv::Tensor &anchors, int32_t iteration, - const NVCVBorderType borderMode); + const NVCVBorderType borderMode) const; - virtual NVCVOperatorHandle handle() const noexcept override; + NVCVOperatorHandle handle() const noexcept override; private: - NVCVOperatorHandle m_handle; + detail::OperatorHandle m_handle; }; inline Morphology::Morphology() { - nvcv::detail::CheckThrow(cvcudaMorphologyCreate(&m_handle)); - assert(m_handle); -} - -inline Morphology::~Morphology() -{ - nvcvOperatorDestroy(m_handle); - m_handle = nullptr; + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaMorphologyCreate(&h)); + assert(h); + m_handle = detail::OperatorHandle{h}; } inline void Morphology::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, nvcv::OptionalTensorConstRef workspace, NVCVMorphologyType morphType, const nvcv::Size2D &maskSize, const int2 &anchor, int32_t iteration, - const NVCVBorderType borderMode) + const NVCVBorderType borderMode) const { - nvcv::detail::CheckThrow(cvcudaMorphologySubmit(m_handle, stream, in.handle(), out.handle(), + nvcv::detail::CheckThrow(cvcudaMorphologySubmit(m_handle.get(), stream, in.handle(), out.handle(), NVCV_OPTIONAL_TO_HANDLE(workspace), morphType, maskSize.w, maskSize.h, anchor.x, anchor.y, iteration, borderMode)); } @@ -86,18 +82,20 @@ inline void Morphology::operator()(cudaStream_t stream, const nvcv::ImageBatchVa const nvcv::ImageBatchVarShape &out, const nvcv::OptionalImageBatchVarShapeConstRef workspace, NVCVMorphologyType morphType, const nvcv::Tensor &masks, const nvcv::Tensor &anchors, - int32_t iteration, const NVCVBorderType borderMode) + int32_t iteration, const NVCVBorderType borderMode) const { - nvcv::detail::CheckThrow(cvcudaMorphologyVarShapeSubmit(m_handle, stream, in.handle(), out.handle(), + nvcv::detail::CheckThrow(cvcudaMorphologyVarShapeSubmit(m_handle.get(), stream, in.handle(), out.handle(), NVCV_OPTIONAL_TO_HANDLE(workspace), morphType, masks.handle(), anchors.handle(), iteration, borderMode)); } inline NVCVOperatorHandle Morphology::handle() const noexcept { - return m_handle; + return m_handle.get(); } } // namespace cvcuda +/** @} */ + #endif // CVCUDA_MORPHOLOGY_HPP diff --git a/src/cvcuda/include/cvcuda/OpNonMaximumSuppression.h b/src/cvcuda/include/cvcuda/OpNonMaximumSuppression.h index b842e729f..fb655e037 100644 --- a/src/cvcuda/include/cvcuda/OpNonMaximumSuppression.h +++ b/src/cvcuda/include/cvcuda/OpNonMaximumSuppression.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -61,6 +61,9 @@ CVCUDA_PUBLIC NVCVStatus cvcudaNonMaximumSuppressionCreate(NVCVOperatorHandle *h * * Limitations: * + * Planar image layouts: Not applicable + * Reason: Inputs are bounding-box and score tensors and outputs are selection masks, not images. + * * Input: * Data Layout: [NW] * Channel count: [4] @@ -73,6 +76,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaNonMaximumSuppressionCreate(NVCVOperatorHandle *h * 16bit Signed | Yes * 32bit Unsigned | No * 32bit Signed | No + * 16bit Float | No * 32bit Float | No * 64bit Float | No * @@ -88,6 +92,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaNonMaximumSuppressionCreate(NVCVOperatorHandle *h * 16bit Signed | No * 32bit Unsigned | No * 32bit Signed | No + * 16bit Float | No * 32bit Float | No * 64bit Float | No * @@ -141,4 +146,6 @@ CVCUDA_PUBLIC NVCVStatus cvcudaNonMaximumSuppressionSubmit(NVCVOperatorHandle ha } #endif +/** @} */ + #endif /* CVCUDA__NON_MAXIMUM_SUPPRESSION_H */ diff --git a/src/cvcuda/include/cvcuda/OpNonMaximumSuppression.hpp b/src/cvcuda/include/cvcuda/OpNonMaximumSuppression.hpp index 0519d8ed7..93720895e 100644 --- a/src/cvcuda/include/cvcuda/OpNonMaximumSuppression.hpp +++ b/src/cvcuda/include/cvcuda/OpNonMaximumSuppression.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -31,6 +31,8 @@ #include #include +#include + namespace cvcuda { class NonMaximumSuppression final : public IOperator @@ -38,38 +40,34 @@ class NonMaximumSuppression final : public IOperator public: explicit NonMaximumSuppression(); - ~NonMaximumSuppression(); - void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, const nvcv::Tensor &scores, - float scoreThreshold, float iouThreshold); + float scoreThreshold, float iouThreshold) const; - virtual NVCVOperatorHandle handle() const noexcept override; + NVCVOperatorHandle handle() const noexcept override; private: - NVCVOperatorHandle m_handle; + detail::OperatorHandle m_handle; }; inline NonMaximumSuppression::NonMaximumSuppression() { - nvcv::detail::CheckThrow(cvcudaNonMaximumSuppressionCreate(&m_handle)); - assert(m_handle); -} - -inline NonMaximumSuppression::~NonMaximumSuppression() -{ - nvcvOperatorDestroy(m_handle); + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaNonMaximumSuppressionCreate(&h)); + assert(h); + m_handle = detail::OperatorHandle{h}; } inline void NonMaximumSuppression::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, - const nvcv::Tensor &scores, float scoreThreshold, float iouThreshold) + const nvcv::Tensor &scores, float scoreThreshold, + float iouThreshold) const { - nvcv::detail::CheckThrow(cvcudaNonMaximumSuppressionSubmit(m_handle, stream, in.handle(), out.handle(), + nvcv::detail::CheckThrow(cvcudaNonMaximumSuppressionSubmit(m_handle.get(), stream, in.handle(), out.handle(), scores.handle(), scoreThreshold, iouThreshold)); } inline NVCVOperatorHandle NonMaximumSuppression::handle() const noexcept { - return m_handle; + return m_handle.get(); } } // namespace cvcuda diff --git a/src/cvcuda/include/cvcuda/OpNormalize.h b/src/cvcuda/include/cvcuda/OpNormalize.h index d20eed11e..e907e8843 100644 --- a/src/cvcuda/include/cvcuda/OpNormalize.h +++ b/src/cvcuda/include/cvcuda/OpNormalize.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -82,7 +82,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaNormalizeCreate(NVCVOperatorHandle *handle); * Limitations: * * Input: - * Data Layout: [kNHWC, kHWC] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] * Channels: [1, 3, 4] * * Data Type | Allowed @@ -93,11 +93,12 @@ CVCUDA_PUBLIC NVCVStatus cvcudaNormalizeCreate(NVCVOperatorHandle *handle); * 16bit Signed | Yes * 32bit Unsigned | No * 32bit Signed | Yes + * 16bit Float | No * 32bit Float | Yes * 64bit Float | No * * Output: - * Data Layout: [kNHWC, kHWC] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] * Channels: [1, 3, 4] * * Data Type | Allowed @@ -108,6 +109,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaNormalizeCreate(NVCVOperatorHandle *handle); * 16bit Signed | Yes * 32bit Unsigned | No * 32bit Signed | Yes + * 16bit Float | No * 32bit Float | Yes * 64bit Float | No * @@ -125,9 +127,18 @@ CVCUDA_PUBLIC NVCVStatus cvcudaNormalizeCreate(NVCVOperatorHandle *handle); * Scale/Base Tensor: * * Scale and Base may be a tensor the same shape as the input/output tensors, or it can be a scalar each dimension. + * Dimensions with extent 1 are broadcast over the corresponding input axis. * - * For varshape variant, scale and base may represent either a scalar with shape [1,1,1,1], - * or a tensor with shape [1,1,1,C], where C is the number of channels in the input format. + * For varshape variant with interleaved input, scale and base may represent either a scalar with shape [1,1,1,1], + * or a tensor with shape [1,1,1,C], where C is the number of channels in the input format. Varshape scale and + * base are always broadcast over the image batch. + * + * For planar (kNCHW / kCHW) tensor input, scale and base must use a planar layout that matches the input + * for non-scalar parameters (e.g. [1,C,1,1] for per-channel parameters). Tensor input supports the same + * per-axis broadcasting rule above. For planar varshape input, scale and base must be batch and spatial + * broadcasted, so use [1,1,1,1] for scalar parameters or [1,C,1,1] for per-channel parameters. A truly + * scalar parameter tensor (numChannels == 1) is also accepted in either layout because both encode the same + * bytes. * * @param [in] handle Handle to the operator. * + Must not be NULL. @@ -166,6 +177,127 @@ CVCUDA_PUBLIC NVCVStatus cvcudaNormalizeVarShapeSubmit(NVCVOperatorHandle handle float global_scale, float shift, float epsilon, uint32_t flags); /** @} */ +/** + * Executes the normalize operation on the given cuda stream, taking base and scale by value instead of + * as tensors. This operation does not wait for completion. + * + * This is a "tensor-free" variant of #cvcudaNormalizeSubmit: the base (typically: mean or min) and + * scale (typically reciprocal of standard deviation or 1/(max-min)) values are passed by value in + * \p base and \p scale (each a `float4` holding up to four channels) instead of being read from device + * tensors, so the caller does not allocate or upload any parameter tensor (avoiding a host-to-device + * copy). The normalization follows the same formula: + * ``` + * out[data_idx] = (in[data_idx] - base[channel]) * scale[channel] * global_scale + shift + * ``` + * The two additional constants, `global_scale` and `shift`, can be used to adjust the result to the + * dynamic range and resolution of the output type. + * + * The `scale` parameter may also be interpreted as standard deviation - in that case, its reciprocal is + * used and optionally, a regularizing term is added to the variance. + * ``` + * m = 1 / sqrt(square(stddev[channel]) + epsilon) + * out[data_idx] = (in[data_idx] - mean[channel]) * m * global_scale + shift + * ``` + * + * `baseChannels` and `scaleChannels` give how many leading lanes of the corresponding `float4` are + * meaningful: + * ``` + * channel_idx = count == 1 ? 0 : channel + * ``` + * A count of 1 broadcasts a single value to every channel; a count equal to the input channel count + * supplies one value per channel. Any other count is rejected. Results are bit-identical to + * #cvcudaNormalizeSubmit for the same values. + * + * Limitations: + * + * Input: + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 3, 4] + * + * Data Type | Allowed + * -------------- | ------------- + * 8bit Unsigned | Yes + * 8bit Signed | Yes + * 16bit Unsigned | Yes + * 16bit Signed | Yes + * 32bit Unsigned | No + * 32bit Signed | Yes + * 16bit Float | No + * 32bit Float | Yes + * 64bit Float | No + * + * Output: + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 3, 4] + * + * Data Type | Allowed + * -------------- | ------------- + * 8bit Unsigned | Yes + * 8bit Signed | Yes + * 16bit Unsigned | Yes + * 16bit Signed | Yes + * 32bit Unsigned | No + * 32bit Signed | Yes + * 16bit Float | No + * 32bit Float | Yes + * 64bit Float | No + * + * Input/Output dependency + * + * Property | Input == Output + * -------------- | ------------- + * Data Layout | Yes + * Data Type | Yes + * Number | Yes + * Channels | Yes + * Width | Yes + * Height | Yes + * + * Base/Scale: + * + * Base and scale are supplied by value (up to four channels packed in a float4). Each may represent + * a scalar broadcast over every channel (baseChannels / scaleChannels == 1) or a per-channel value + * (count equal to the number of channels in the input). Interleaved (kNHWC / kHWC) and planar + * (kNCHW / kCHW) tensor input are both supported. Unlike #cvcudaNormalizeSubmit, per-axis spatial + * (width/height) parameters are not available; use #cvcudaNormalizeSubmit with parameter tensors + * for spatial parameters. CUDA launch limits require N <= 65535 for interleaved input and + * N*C <= 65535 for planar input. + * + * @param [in] handle Handle to the operator. + * + Must not be NULL. + * @param [in] stream Handle to a valid CUDA stream. + * + * @param [in] in input tensor. + * + * @param [in] base Base values, passed by value (up to four channels in a float4). + * + * @param [in] scale Scale values, passed by value (up to four channels in a float4). + * + * @param [in] baseChannels Number of meaningful \p base lanes: 1 (broadcast) or the input channel count. + * + * @param [in] scaleChannels Number of meaningful \p scale lanes: 1 (broadcast) or the input channel count. + * + * @param [out] out Output tensor. + * + * @param [in] global_scale Additional scale value to be used in addition to scale. + * + * @param [in] shift Additional bias value to be used in addition to base. + * + * @param [in] epsilon Epsilon to use when \p CVCUDA_NORMALIZE_SCALE_IS_STDDEV flag is set as a regularizing term to be + * added to variance. + * + * @param [in] flags Algorithm flags, use \p CVCUDA_NORMALIZE_SCALE_IS_STDDEV if scale passed as argument + * is standard deviation instead or 0 if it is scaling. + * + * @retval #NVCV_ERROR_INVALID_ARGUMENT Some parameter is outside valid range. + * @retval #NVCV_ERROR_INTERNAL Internal error in the operator, invalid types passed in. + * @retval #NVCV_SUCCESS Operation executed successfully. + */ +CVCUDA_PUBLIC NVCVStatus cvcudaNormalizeScalarSubmit(NVCVOperatorHandle handle, cudaStream_t stream, + NVCVTensorHandle in, float4 base, float4 scale, + int32_t baseChannels, int32_t scaleChannels, NVCVTensorHandle out, + float global_scale, float shift, float epsilon, uint32_t flags); + #ifdef __cplusplus } #endif diff --git a/src/cvcuda/include/cvcuda/OpNormalize.hpp b/src/cvcuda/include/cvcuda/OpNormalize.hpp index 5592a516a..68b4d3870 100644 --- a/src/cvcuda/include/cvcuda/OpNormalize.hpp +++ b/src/cvcuda/include/cvcuda/OpNormalize.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -35,6 +35,8 @@ #include #include +#include + namespace cvcuda { class Normalize final : public IOperator @@ -42,54 +44,64 @@ class Normalize final : public IOperator public: explicit Normalize(); - ~Normalize(); - void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &base, const nvcv::Tensor &scale, - const nvcv::Tensor &out, float global_scale, float shift, float epsilon, uint32_t flags = 0); + const nvcv::Tensor &out, float global_scale, float shift, float epsilon, uint32_t flags = 0) const; + + void operator()(cudaStream_t stream, const nvcv::Tensor &in, const float4 base, const float4 scale, + int32_t baseChannels, int32_t scaleChannels, const nvcv::Tensor &out, float global_scale, + float shift, float epsilon, uint32_t flags = 0) const; void operator()(cudaStream_t stream, const nvcv::ImageBatch &in, const nvcv::Tensor &base, const nvcv::Tensor &scale, const nvcv::ImageBatch &out, float global_scale, float shift, - float epsilon, uint32_t flags = 0); + float epsilon, uint32_t flags = 0) const; - virtual NVCVOperatorHandle handle() const noexcept override; + NVCVOperatorHandle handle() const noexcept override; private: - NVCVOperatorHandle m_handle; + detail::OperatorHandle m_handle; }; inline Normalize::Normalize() { - nvcv::detail::CheckThrow(cvcudaNormalizeCreate(&m_handle)); - assert(m_handle); -} - -inline Normalize::~Normalize() -{ - nvcvOperatorDestroy(m_handle); - m_handle = nullptr; + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaNormalizeCreate(&h)); + assert(h); + m_handle = detail::OperatorHandle{h}; } inline void Normalize::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &base, const nvcv::Tensor &scale, const nvcv::Tensor &out, float global_scale, float shift, - float epsilon, uint32_t flags) + float epsilon, uint32_t flags) const { - nvcv::detail::CheckThrow(cvcudaNormalizeSubmit(m_handle, stream, in.handle(), base.handle(), scale.handle(), + nvcv::detail::CheckThrow(cvcudaNormalizeSubmit(m_handle.get(), stream, in.handle(), base.handle(), scale.handle(), out.handle(), global_scale, shift, epsilon, flags)); } +inline void Normalize::operator()(cudaStream_t stream, const nvcv::Tensor &in, const float4 base, const float4 scale, + int32_t baseChannels, int32_t scaleChannels, const nvcv::Tensor &out, + float global_scale, float shift, float epsilon, uint32_t flags) const +{ + nvcv::detail::CheckThrow(cvcudaNormalizeScalarSubmit(m_handle.get(), stream, in.handle(), base, scale, baseChannels, + scaleChannels, out.handle(), global_scale, shift, epsilon, + flags)); +} + inline void Normalize::operator()(cudaStream_t stream, const nvcv::ImageBatch &in, const nvcv::Tensor &base, const nvcv::Tensor &scale, const nvcv::ImageBatch &out, float global_scale, - float shift, float epsilon, uint32_t flags) + float shift, float epsilon, uint32_t flags) const { - nvcv::detail::CheckThrow(cvcudaNormalizeVarShapeSubmit(m_handle, stream, in.handle(), base.handle(), scale.handle(), - out.handle(), global_scale, shift, epsilon, flags)); + nvcv::detail::CheckThrow(cvcudaNormalizeVarShapeSubmit(m_handle.get(), stream, in.handle(), base.handle(), + scale.handle(), out.handle(), global_scale, shift, epsilon, + flags)); } inline NVCVOperatorHandle Normalize::handle() const noexcept { - return m_handle; + return m_handle.get(); } } // namespace cvcuda +/** @} */ + #endif // CVCUDA_NORMALIZE_HPP diff --git a/src/cvcuda/include/cvcuda/OpOSD.h b/src/cvcuda/include/cvcuda/OpOSD.h index 60abae54c..555e9e8e4 100644 --- a/src/cvcuda/include/cvcuda/OpOSD.h +++ b/src/cvcuda/include/cvcuda/OpOSD.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -56,32 +56,34 @@ CVCUDA_PUBLIC NVCVStatus cvcudaOSDCreate(NVCVOperatorHandle *handle); * Limitations: * * Input: - * Data Layout: [kNHWC, kHWC] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] * Channels: [3, 4] * * Data Type | Allowed * -------------- | ------------- * 8bit Unsigned | Yes - * 8bit Signed | Yes + * 8bit Signed | No * 16bit Unsigned | No * 16bit Signed | No * 32bit Unsigned | No * 32bit Signed | No + * 16bit Float | No * 32bit Float | No * 64bit Float | No * * Output: - * Data Layout: [kNHWC, kHWC] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] * Channels: [3, 4] * * Data Type | Allowed * -------------- | ------------- * 8bit Unsigned | Yes - * 8bit Signed | Yes + * 8bit Signed | No * 16bit Unsigned | No * 16bit Signed | No * 32bit Unsigned | No * 32bit Signed | No + * 16bit Float | No * 32bit Float | No * 64bit Float | No * @@ -117,4 +119,6 @@ CVCUDA_PUBLIC NVCVStatus cvcudaOSDSubmit(NVCVOperatorHandle handle, cudaStream_t } #endif +/** @} */ + #endif /* CVCUDA__O_S_D_H */ diff --git a/src/cvcuda/include/cvcuda/OpOSD.hpp b/src/cvcuda/include/cvcuda/OpOSD.hpp index 9a549c38f..b9acd078e 100644 --- a/src/cvcuda/include/cvcuda/OpOSD.hpp +++ b/src/cvcuda/include/cvcuda/OpOSD.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -34,6 +34,8 @@ #include #include +#include + namespace cvcuda { class OSD final : public IOperator @@ -41,39 +43,36 @@ class OSD final : public IOperator public: explicit OSD(); - ~OSD(); - - void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, const NVCVElements elements); + void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, + const NVCVElements elements) const; - virtual NVCVOperatorHandle handle() const noexcept override; + NVCVOperatorHandle handle() const noexcept override; private: - NVCVOperatorHandle m_handle; + detail::OperatorHandle m_handle; }; inline OSD::OSD() { - nvcv::detail::CheckThrow(cvcudaOSDCreate(&m_handle)); - assert(m_handle); -} - -inline OSD::~OSD() -{ - nvcvOperatorDestroy(m_handle); - m_handle = nullptr; + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaOSDCreate(&h)); + assert(h); + m_handle = detail::OperatorHandle{h}; } inline void OSD::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, - const NVCVElements elements) + const NVCVElements elements) const { - nvcv::detail::CheckThrow(cvcudaOSDSubmit(m_handle, stream, in.handle(), out.handle(), elements)); + nvcv::detail::CheckThrow(cvcudaOSDSubmit(m_handle.get(), stream, in.handle(), out.handle(), elements)); } inline NVCVOperatorHandle OSD::handle() const noexcept { - return m_handle; + return m_handle.get(); } } // namespace cvcuda +/** @} */ + #endif // CVCUDA__O_S_D_HPP diff --git a/src/cvcuda/include/cvcuda/OpPadAndStack.h b/src/cvcuda/include/cvcuda/OpPadAndStack.h index 506a867ad..e1b8c0b17 100644 --- a/src/cvcuda/include/cvcuda/OpPadAndStack.h +++ b/src/cvcuda/include/cvcuda/OpPadAndStack.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -57,8 +57,8 @@ CVCUDA_PUBLIC NVCVStatus cvcudaPadAndStackCreate(NVCVOperatorHandle *handle); * Limitations: * * Input: - * Data Layout: [kNHWC, kHWC] - * Channels: [1, 3, 4] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 3, 4] for all types, [2] for 8bit Unsigned interleaved only * * Data Type | Allowed * -------------- | ------------- @@ -68,12 +68,13 @@ CVCUDA_PUBLIC NVCVStatus cvcudaPadAndStackCreate(NVCVOperatorHandle *handle); * 16bit Signed | Yes * 32bit Unsigned | No * 32bit Signed | Yes + * 16bit Float | No * 32bit Float | Yes * 64bit Float | No * * Output: - * Data Layout: [kNHWC, kHWC] - * Channels: [1, 3, 4] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 3, 4] for all types, [2] for 8bit Unsigned interleaved only * * Data Type | Allowed * -------------- | ------------- @@ -83,6 +84,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaPadAndStackCreate(NVCVOperatorHandle *handle); * 16bit Signed | Yes * 32bit Unsigned | No * 32bit Signed | Yes + * 16bit Float | No * 32bit Float | Yes * 64bit Float | No * @@ -134,4 +136,6 @@ CVCUDA_PUBLIC NVCVStatus cvcudaPadAndStackSubmit(NVCVOperatorHandle handle, cuda } #endif +/** @} */ + #endif /* CVCUDA_PADANDSTACK_H */ diff --git a/src/cvcuda/include/cvcuda/OpPadAndStack.hpp b/src/cvcuda/include/cvcuda/OpPadAndStack.hpp index 50750b5a3..491510dbb 100644 --- a/src/cvcuda/include/cvcuda/OpPadAndStack.hpp +++ b/src/cvcuda/include/cvcuda/OpPadAndStack.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -35,6 +35,8 @@ #include #include +#include + namespace cvcuda { class PadAndStack final : public IOperator @@ -42,42 +44,39 @@ class PadAndStack final : public IOperator public: explicit PadAndStack(); - ~PadAndStack(); - void operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::Tensor &out, - const nvcv::Tensor &top, const nvcv::Tensor &left, NVCVBorderType borderMode, float borderValue); + const nvcv::Tensor &top, const nvcv::Tensor &left, NVCVBorderType borderMode, + float borderValue) const; - virtual NVCVOperatorHandle handle() const noexcept override; + NVCVOperatorHandle handle() const noexcept override; private: - NVCVOperatorHandle m_handle; + detail::OperatorHandle m_handle; }; inline PadAndStack::PadAndStack() { - nvcv::detail::CheckThrow(cvcudaPadAndStackCreate(&m_handle)); - assert(m_handle); -} - -inline PadAndStack::~PadAndStack() -{ - nvcvOperatorDestroy(m_handle); - m_handle = nullptr; + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaPadAndStackCreate(&h)); + assert(h); + m_handle = detail::OperatorHandle{h}; } inline void PadAndStack::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::Tensor &out, const nvcv::Tensor &top, const nvcv::Tensor &left, NVCVBorderType borderMode, - float borderValue) + float borderValue) const { - nvcv::detail::CheckThrow(cvcudaPadAndStackSubmit(m_handle, stream, in.handle(), out.handle(), top.handle(), + nvcv::detail::CheckThrow(cvcudaPadAndStackSubmit(m_handle.get(), stream, in.handle(), out.handle(), top.handle(), left.handle(), borderMode, borderValue)); } inline NVCVOperatorHandle PadAndStack::handle() const noexcept { - return m_handle; + return m_handle.get(); } } // namespace cvcuda +/** @} */ + #endif // CVCUDA_PADANDSTACK_HPP diff --git a/src/cvcuda/include/cvcuda/OpPairwiseMatcher.h b/src/cvcuda/include/cvcuda/OpPairwiseMatcher.h index 02705857f..511e68fcb 100644 --- a/src/cvcuda/include/cvcuda/OpPairwiseMatcher.h +++ b/src/cvcuda/include/cvcuda/OpPairwiseMatcher.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -64,6 +64,35 @@ CVCUDA_PUBLIC NVCVStatus cvcudaPairwiseMatcherCreate(NVCVOperatorHandle *handle, * found by the operator, that is the total number may be greater than this limitation and the order of * matches returned might differ in different runs. * + * Limitations: + * + * Planar image layouts: Not applicable + * Reason: Inputs are descriptor-set tensors and outputs are matches and distances, not images. + * + * Input set1, set2: + * Data Layout: [NMD] (rank-3 tensor) + * Channels: D (depth dimension, e.g., 128 for SIFT descriptors) + * + * Data Type | Allowed + * -------------- | ------------- + * 8bit Unsigned | Yes + * 8bit Signed | No + * 16bit Unsigned | No + * 16bit Signed | No + * 32bit Unsigned | Yes + * 32bit Signed | No + * 16bit Float | No + * 32bit Float | Yes + * 64bit Float | No + * + * Output matches: + * Data Layout: [NMA] (rank-3 tensor, A=2) + * Data Type: 32bit Signed + * + * Output distances: + * Data Layout: [NM] or [NMC] (rank-2 or rank-3 tensor) + * Data Type: 32bit Float + * * @param [in] handle Handle to the operator. * + Must not be NULL. * @@ -170,4 +199,6 @@ CVCUDA_PUBLIC NVCVStatus cvcudaPairwiseMatcherSubmit(NVCVOperatorHandle handle, } #endif +/** @} */ + #endif /* CVCUDA_PAIRWISE_MATCHER_H */ diff --git a/src/cvcuda/include/cvcuda/OpPairwiseMatcher.hpp b/src/cvcuda/include/cvcuda/OpPairwiseMatcher.hpp index 13178ac75..54c6b069a 100644 --- a/src/cvcuda/include/cvcuda/OpPairwiseMatcher.hpp +++ b/src/cvcuda/include/cvcuda/OpPairwiseMatcher.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -33,6 +33,8 @@ #include #include +#include + namespace cvcuda { class PairwiseMatcher final : public IOperator @@ -40,47 +42,43 @@ class PairwiseMatcher final : public IOperator public: explicit PairwiseMatcher(NVCVPairwiseMatcherType algoChoice); - ~PairwiseMatcher(); - void operator()(cudaStream_t stream, const nvcv::Tensor &set1, const nvcv::Tensor &set2, const nvcv::Tensor &numSet1, const nvcv::Tensor &numSet2, const nvcv::Tensor &matches, const nvcv::Tensor &numMatches, const nvcv::Tensor &distances, bool crossCheck, int matchesPerPoint, - NVCVNormType normType); + NVCVNormType normType) const; - virtual NVCVOperatorHandle handle() const noexcept override; + NVCVOperatorHandle handle() const noexcept override; private: - NVCVOperatorHandle m_handle; + detail::OperatorHandle m_handle; }; inline PairwiseMatcher::PairwiseMatcher(NVCVPairwiseMatcherType algoChoice) { - nvcv::detail::CheckThrow(cvcudaPairwiseMatcherCreate(&m_handle, algoChoice)); - assert(m_handle); -} - -inline PairwiseMatcher::~PairwiseMatcher() -{ - nvcvOperatorDestroy(m_handle); - m_handle = nullptr; + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaPairwiseMatcherCreate(&h, algoChoice)); + assert(h); + m_handle = detail::OperatorHandle{h}; } inline void PairwiseMatcher::operator()(cudaStream_t stream, const nvcv::Tensor &set1, const nvcv::Tensor &set2, const nvcv::Tensor &numSet1, const nvcv::Tensor &numSet2, const nvcv::Tensor &matches, const nvcv::Tensor &numMatches, const nvcv::Tensor &distances, bool crossCheck, int matchesPerPoint, - NVCVNormType normType) + NVCVNormType normType) const { nvcv::detail::CheckThrow(cvcudaPairwiseMatcherSubmit( - m_handle, stream, set1.handle(), set2.handle(), numSet1.handle(), numSet2.handle(), matches.handle(), + m_handle.get(), stream, set1.handle(), set2.handle(), numSet1.handle(), numSet2.handle(), matches.handle(), numMatches.handle(), distances.handle(), crossCheck, matchesPerPoint, normType)); } inline NVCVOperatorHandle PairwiseMatcher::handle() const noexcept { - return m_handle; + return m_handle.get(); } } // namespace cvcuda +/** @} */ + #endif // CVCUDA_PAIRWISE_MATCHER_HPP diff --git a/src/cvcuda/include/cvcuda/OpPillowResize.h b/src/cvcuda/include/cvcuda/OpPillowResize.h index 73bdb3a28..1087901b3 100644 --- a/src/cvcuda/include/cvcuda/OpPillowResize.h +++ b/src/cvcuda/include/cvcuda/OpPillowResize.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -78,10 +78,15 @@ CVCUDA_PUBLIC NVCVStatus cvcudaPillowResizeGetWorkspaceRequirements(NVCVOperator * @param [in] handle Where the image instance handle will be written to. * + Must not be NULL. * @param [in] batchSize The number of images - * @param [in] inputSizes The sizes of the input images - * @param [in] outputSizes The sizes of the output images + * @param [in] inputSizesWH The sizes of the input images. + * + Must not be NULL. + * + Must contain at least @p batchSize elements. + * @param [in] outputSizesWH The sizes of the output images. + * + Must not be NULL. + * + Must contain at least @p batchSize elements. * @param [in] fmt Image format * @param [out] reqOut Requirements for the operator's workspace + * + Must not be NULL. * * @retval #NVCV_ERROR_INVALID_ARGUMENT Handle is null or one of the arguments is out of range. * @retval #NVCV_SUCCESS Operation executed successfully. @@ -98,32 +103,36 @@ CVCUDA_PUBLIC NVCVStatus cvcudaPillowResizeVarShapeGetWorkspaceRequirements(NVCV * Limitations: * * Input: - * Data Layout: [kNHWC, kHWC] - * Channels: [1, 3, 4] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 2, 3, 4] for interleaved (kNHWC, kHWC); [1, 3, 4] for planar (kNCHW, kCHW) + * (2-channel planar is not supported) * * Data Type | Allowed * -------------- | ------------- * 8bit Unsigned | Yes - * 8bit Signed | No + * 8bit Signed | Yes * 16bit Unsigned | Yes * 16bit Signed | Yes * 32bit Unsigned | No - * 32bit Signed | No + * 32bit Signed | Yes + * 16bit Float | No * 32bit Float | Yes * 64bit Float | No * * Output: - * Data Layout: [kNHWC, kHWC] - * Channels: [1, 3, 4] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 2, 3, 4] for interleaved (kNHWC, kHWC); [1, 3, 4] for planar (kNCHW, kCHW) + * (2-channel planar is not supported) * * Data Type | Allowed * -------------- | ------------- * 8bit Unsigned | Yes - * 8bit Signed | No + * 8bit Signed | Yes * 16bit Unsigned | Yes * 16bit Signed | Yes * 32bit Unsigned | No - * 32bit Signed | No + * 32bit Signed | Yes + * 16bit Float | No * 32bit Float | Yes * 64bit Float | No * diff --git a/src/cvcuda/include/cvcuda/OpPillowResize.hpp b/src/cvcuda/include/cvcuda/OpPillowResize.hpp index 6d6475032..c4af4fca8 100644 --- a/src/cvcuda/include/cvcuda/OpPillowResize.hpp +++ b/src/cvcuda/include/cvcuda/OpPillowResize.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -37,6 +37,8 @@ #include #include +#include + namespace cvcuda { class PillowResize final : public IOperator @@ -44,75 +46,74 @@ class PillowResize final : public IOperator public: PillowResize(); - ~PillowResize(); - WorkspaceRequirements getWorkspaceRequirements(int batchSize, const nvcv::Size2D *in_sizes, - const nvcv::Size2D *out_sizes, nvcv::ImageFormat fmt); + const nvcv::Size2D *out_sizes, nvcv::ImageFormat fmt) const; WorkspaceRequirements getWorkspaceRequirements(int maxBatchSize, nvcv::Size2D maxInSize, nvcv::Size2D maxOutSize, - nvcv::ImageFormat fmt); + nvcv::ImageFormat fmt) const; void operator()(cudaStream_t stream, const Workspace &ws, const nvcv::Tensor &in, const nvcv::Tensor &out, - const NVCVInterpolationType interpolation); + const NVCVInterpolationType interpolation) const; void operator()(cudaStream_t stream, const Workspace &ws, const nvcv::ImageBatchVarShape &in, - const nvcv::ImageBatchVarShape &out, const NVCVInterpolationType interpolation); + const nvcv::ImageBatchVarShape &out, const NVCVInterpolationType interpolation) const; - virtual NVCVOperatorHandle handle() const noexcept override; + NVCVOperatorHandle handle() const noexcept override; private: - NVCVOperatorHandle m_handle; + detail::OperatorHandle m_handle; }; inline PillowResize::PillowResize() { - nvcv::detail::CheckThrow(cvcudaPillowResizeCreate(&m_handle)); - assert(m_handle); -} - -inline PillowResize::~PillowResize() -{ - nvcvOperatorDestroy(m_handle); - m_handle = nullptr; + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaPillowResizeCreate(&h)); + assert(h); + m_handle = detail::OperatorHandle{h}; } inline WorkspaceRequirements PillowResize::getWorkspaceRequirements(int batchSize, const nvcv::Size2D *in_sizes, const nvcv::Size2D *out_sizes, - nvcv::ImageFormat fmt) + nvcv::ImageFormat fmt) const { WorkspaceRequirements req{}; - nvcv::detail::CheckThrow(cvcudaPillowResizeVarShapeGetWorkspaceRequirements(m_handle, batchSize, in_sizes, + nvcv::detail::CheckThrow(cvcudaPillowResizeVarShapeGetWorkspaceRequirements(m_handle.get(), batchSize, in_sizes, out_sizes, fmt.cvalue(), &req)); return req; } inline WorkspaceRequirements PillowResize::getWorkspaceRequirements(int maxBatchSize, nvcv::Size2D maxInSize, - nvcv::Size2D maxOutSize, nvcv::ImageFormat fmt) + nvcv::Size2D maxOutSize, + nvcv::ImageFormat fmt) const { WorkspaceRequirements req{}; nvcv::detail::CheckThrow(cvcudaPillowResizeGetWorkspaceRequirements( - m_handle, maxBatchSize, maxInSize.w, maxInSize.h, maxOutSize.w, maxOutSize.h, fmt.cvalue(), &req)); + m_handle.get(), maxBatchSize, maxInSize.w, maxInSize.h, maxOutSize.w, maxOutSize.h, fmt.cvalue(), &req)); return req; } inline void PillowResize::operator()(cudaStream_t stream, const Workspace &ws, const nvcv::Tensor &in, - const nvcv::Tensor &out, const NVCVInterpolationType interpolation) + const nvcv::Tensor &out, const NVCVInterpolationType interpolation) const { - nvcv::detail::CheckThrow(cvcudaPillowResizeSubmit(m_handle, stream, &ws, in.handle(), out.handle(), interpolation)); + nvcv::detail::CheckThrow( + cvcudaPillowResizeSubmit(m_handle.get(), stream, &ws, in.handle(), out.handle(), interpolation)); } inline void PillowResize::operator()(cudaStream_t stream, const Workspace &ws, const nvcv::ImageBatchVarShape &in, - const nvcv::ImageBatchVarShape &out, const NVCVInterpolationType interpolation) + const nvcv::ImageBatchVarShape &out, + const NVCVInterpolationType interpolation) const { nvcv::detail::CheckThrow( - cvcudaPillowResizeVarShapeSubmit(m_handle, stream, &ws, in.handle(), out.handle(), interpolation)); + cvcudaPillowResizeVarShapeSubmit(m_handle.get(), stream, &ws, in.handle(), out.handle(), interpolation)); } inline NVCVOperatorHandle PillowResize::handle() const noexcept { - return m_handle; + return m_handle.get(); } } // namespace cvcuda +/** @} */ + #endif // CVCUDA_PILLOW_RESIZE_HPP diff --git a/src/cvcuda/include/cvcuda/OpPosterize.h b/src/cvcuda/include/cvcuda/OpPosterize.h new file mode 100644 index 000000000..034febe00 --- /dev/null +++ b/src/cvcuda/include/cvcuda/OpPosterize.h @@ -0,0 +1,147 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +/** + * @file OpPosterize.h + * + * @brief Posterizes an image — reduces each channel to its top @p bits. + * + * Posterize keeps the @p bits most-significant bits of every channel value and zeros the rest, + * computing @f$ out = in \& \sim((1 \ll (W - bits)) - 1) @f$ per element, where @p W is the data + * type bit width (8 for 8-bit unsigned, 16 for 16-bit unsigned). It is a pure bitwise mask, applied + * channel-independently. @p bits must be in @f$ [0, W] @f$ for the input data type. + * + * Reference: mimics torchvision.transforms.v2.functional.posterize / PIL ImageOps.posterize (8-bit: + * keep the top @p bits), generalized to 16-bit. The mask is exact, so the result is bit-exact. + * + * @defgroup NVCV_C_ALGORITHM__POSTERIZE Posterize + * @{ + */ + +#ifndef CVCUDA__POSTERIZE_H +#define CVCUDA__POSTERIZE_H + +#include "Operator.h" +#include "detail/Export.h" + +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + +/** Constructs an instance of the Posterize operator. + * + * @param [out] handle Where the operator instance handle will be written to. + * + Must not be NULL. + * + * @retval #NVCV_ERROR_INVALID_ARGUMENT Handle is null. + * @retval #NVCV_ERROR_OUT_OF_MEMORY Not enough memory to create the operator. + * @retval #NVCV_SUCCESS Operation executed successfully. + */ +CVCUDA_PUBLIC NVCVStatus cvcudaPosterizeCreate(NVCVOperatorHandle *handle); + +/** Executes the Posterize operation on the given cuda stream. This operation does not + * wait for completion. + * + * Limitations: + * + * Input: + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 3, 4] + * + * Data Type | Allowed + * -------------- | ------------- + * 8bit Unsigned | Yes + * 8bit Signed | No + * 16bit Unsigned | Yes + * 16bit Signed | No + * 32bit Unsigned | No + * 32bit Signed | No + * 32bit Float | No + * 64bit Float | No + * + * Output: + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 3, 4] + * + * Data Type | Allowed + * -------------- | ------------- + * 8bit Unsigned | Yes + * 8bit Signed | No + * 16bit Unsigned | Yes + * 16bit Signed | No + * 32bit Unsigned | No + * 32bit Signed | No + * 32bit Float | No + * 64bit Float | No + * + * Input/Output dependency + * + * Property | Input == Output + * -------------- | ------------- + * Data Layout | Yes + * Data Type | Yes + * Number | Yes + * Channels | Yes + * Width | Yes + * Height | Yes + * + * @param [in] handle Handle to the operator. + * + Must not be NULL. + * @param [in] stream Handle to a valid CUDA stream. + * + * @param [in] in input tensor. + * + * @param [out] out output tensor. + * + * @param [in] bits Number of most-significant bits to keep per channel; must be in [0, W] where W + * is the data type bit width (8 for 8-bit, 16 for 16-bit). bits == W is a no-op; + * bits == 0 zeros the image. Applied to all images. + * + * @retval #NVCV_ERROR_INVALID_ARGUMENT Some parameter is outside valid range. + * @retval #NVCV_ERROR_INTERNAL Internal error in the operator, invalid types passed in. + * @retval #NVCV_SUCCESS Operation executed successfully. + */ +CVCUDA_PUBLIC NVCVStatus cvcudaPosterizeSubmit(NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle in, + NVCVTensorHandle out, int32_t bits); + +/** Executes the Posterize operation on a batch of variable-shaped images on the given cuda stream. + * Same limitations as cvcudaPosterizeSubmit. + * + * @param [in] handle Handle to the operator. Must not be NULL. + * @param [in] stream Handle to a valid CUDA stream. + * @param [in] in input image batch. + * @param [out] out output image batch. + * @param [in] bits Number of most-significant bits to keep per channel (see cvcudaPosterizeSubmit). + * + * @retval #NVCV_ERROR_INVALID_ARGUMENT Some parameter is outside valid range. + * @retval #NVCV_ERROR_INTERNAL Internal error in the operator, invalid types passed in. + * @retval #NVCV_SUCCESS Operation executed successfully. + */ +CVCUDA_PUBLIC NVCVStatus cvcudaPosterizeVarShapeSubmit(NVCVOperatorHandle handle, cudaStream_t stream, + NVCVImageBatchHandle in, NVCVImageBatchHandle out, int32_t bits); + +#ifdef __cplusplus +} +#endif + +#endif /* CVCUDA__POSTERIZE_H */ diff --git a/src/cvcuda/include/cvcuda/OpPosterize.hpp b/src/cvcuda/include/cvcuda/OpPosterize.hpp new file mode 100644 index 000000000..36584e9bd --- /dev/null +++ b/src/cvcuda/include/cvcuda/OpPosterize.hpp @@ -0,0 +1,83 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +/** + * @file OpPosterize.hpp + * + * @brief Defines the public C++ Class for the Posterize operation. + * @defgroup NVCV_CPP_ALGORITHM__POSTERIZE Posterize + * @{ + */ + +#ifndef CVCUDA__POSTERIZE_HPP +#define CVCUDA__POSTERIZE_HPP + +#include "IOperator.hpp" +#include "OpPosterize.h" + +#include +#include +#include +#include +#include + +namespace cvcuda { + +class Posterize final : public IOperator +{ +public: + explicit Posterize(); + + void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, int32_t bits) const; + + void operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, + int32_t bits) const; + + NVCVOperatorHandle handle() const noexcept override; + +private: + detail::OperatorHandle m_handle; +}; + +inline Posterize::Posterize() +{ + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaPosterizeCreate(&h)); + assert(h); + m_handle = detail::OperatorHandle{h}; +} + +inline void Posterize::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, + int32_t bits) const +{ + nvcv::detail::CheckThrow(cvcudaPosterizeSubmit(m_handle.get(), stream, in.handle(), out.handle(), bits)); +} + +inline void Posterize::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, + const nvcv::ImageBatchVarShape &out, int32_t bits) const +{ + nvcv::detail::CheckThrow(cvcudaPosterizeVarShapeSubmit(m_handle.get(), stream, in.handle(), out.handle(), bits)); +} + +inline NVCVOperatorHandle Posterize::handle() const noexcept +{ + return m_handle.get(); +} + +} // namespace cvcuda + +#endif // CVCUDA__POSTERIZE_HPP diff --git a/src/cvcuda/include/cvcuda/OpRandomResizedCrop.h b/src/cvcuda/include/cvcuda/OpRandomResizedCrop.h index fe59ce49d..40c9b4b59 100644 --- a/src/cvcuda/include/cvcuda/OpRandomResizedCrop.h +++ b/src/cvcuda/include/cvcuda/OpRandomResizedCrop.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -72,7 +72,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaRandomResizedCropCreate(NVCVOperatorHandle *handl * Limitations: * * Input: - * Data Layout: [kNHWC, kHWC] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] * Channels: [1, 3, 4] * * Data Type | Allowed @@ -83,11 +83,12 @@ CVCUDA_PUBLIC NVCVStatus cvcudaRandomResizedCropCreate(NVCVOperatorHandle *handl * 16bit Signed | Yes * 32bit Unsigned | No * 32bit Signed | No + * 16bit Float | No * 32bit Float | Yes * 64bit Float | No * * Output: - * Data Layout: [kNHWC, kHWC] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] * Channels: [1, 3, 4] * * Data Type | Allowed @@ -98,6 +99,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaRandomResizedCropCreate(NVCVOperatorHandle *handl * 16bit Signed | Yes * 32bit Unsigned | No * 32bit Signed | No + * 16bit Float | No * 32bit Float | Yes * 64bit Float | No * diff --git a/src/cvcuda/include/cvcuda/OpRandomResizedCrop.hpp b/src/cvcuda/include/cvcuda/OpRandomResizedCrop.hpp index d328542d4..fa071bd64 100644 --- a/src/cvcuda/include/cvcuda/OpRandomResizedCrop.hpp +++ b/src/cvcuda/include/cvcuda/OpRandomResizedCrop.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -35,6 +35,8 @@ #include #include +#include + namespace cvcuda { class RandomResizedCrop final : public IOperator @@ -43,52 +45,49 @@ class RandomResizedCrop final : public IOperator explicit RandomResizedCrop(double minScale, double maxScale, double minRatio, double maxRatio, int32_t maxBatchSize, uint32_t seed); - ~RandomResizedCrop(); - void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, - const NVCVInterpolationType interpolation); + const NVCVInterpolationType interpolation) const; void operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, - const NVCVInterpolationType interpolation); + const NVCVInterpolationType interpolation) const; - virtual NVCVOperatorHandle handle() const noexcept override; + NVCVOperatorHandle handle() const noexcept override; private: - NVCVOperatorHandle m_handle; + detail::OperatorHandle m_handle; }; inline RandomResizedCrop::RandomResizedCrop(double minScale, double maxScale, double minRatio, double maxRatio, int32_t maxBatchSize, uint32_t seed) { + NVCVOperatorHandle h = nullptr; nvcv::detail::CheckThrow( - cvcudaRandomResizedCropCreate(&m_handle, minScale, maxScale, minRatio, maxRatio, maxBatchSize, seed)); - assert(m_handle); -} - -inline RandomResizedCrop::~RandomResizedCrop() -{ - nvcvOperatorDestroy(m_handle); - m_handle = nullptr; + cvcudaRandomResizedCropCreate(&h, minScale, maxScale, minRatio, maxRatio, maxBatchSize, seed)); + assert(h); + m_handle = detail::OperatorHandle{h}; } inline void RandomResizedCrop::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, - const NVCVInterpolationType interpolation) + const NVCVInterpolationType interpolation) const { - nvcv::detail::CheckThrow(cvcudaRandomResizedCropSubmit(m_handle, stream, in.handle(), out.handle(), interpolation)); + nvcv::detail::CheckThrow( + cvcudaRandomResizedCropSubmit(m_handle.get(), stream, in.handle(), out.handle(), interpolation)); } inline void RandomResizedCrop::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, - const NVCVInterpolationType interpolation) + const NVCVInterpolationType interpolation) const { nvcv::detail::CheckThrow( - cvcudaRandomResizedCropVarShapeSubmit(m_handle, stream, in.handle(), out.handle(), interpolation)); + cvcudaRandomResizedCropVarShapeSubmit(m_handle.get(), stream, in.handle(), out.handle(), interpolation)); } inline NVCVOperatorHandle RandomResizedCrop::handle() const noexcept { - return m_handle; + return m_handle.get(); } } // namespace cvcuda +/** @} */ + #endif // CVCUDA_RANDOMRESIZEDCROP_HPP diff --git a/src/cvcuda/include/cvcuda/OpReformat.h b/src/cvcuda/include/cvcuda/OpReformat.h index ec2e40f32..fd9e6fa36 100644 --- a/src/cvcuda/include/cvcuda/OpReformat.h +++ b/src/cvcuda/include/cvcuda/OpReformat.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -57,8 +57,8 @@ CVCUDA_PUBLIC NVCVStatus cvcudaReformatCreate(NVCVOperatorHandle *handle); * Limitations: * * Input: - * Data Layout: [kNHWC, kHWC, kNCHW, KCHW] - * Channels: [1, 3, 4] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 2, 3, 4] * * Data Type | Allowed * -------------- | ------------- @@ -68,12 +68,13 @@ CVCUDA_PUBLIC NVCVStatus cvcudaReformatCreate(NVCVOperatorHandle *handle); * 16bit Signed | Yes * 32bit Unsigned | No * 32bit Signed | Yes + * 16bit Float | No * 32bit Float | Yes * 64bit Float | Yes * * Output: - * Data Layout: [kNHWC, kHWC, kNCHW, KCHW] - * Channels: [1, 3, 4] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 2, 3, 4] * * Data Type | Allowed * -------------- | ------------- @@ -83,6 +84,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaReformatCreate(NVCVOperatorHandle *handle); * 16bit Signed | Yes * 32bit Unsigned | No * 32bit Signed | Yes + * 16bit Float | No * 32bit Float | Yes * 64bit Float | Yes * @@ -116,4 +118,6 @@ CVCUDA_PUBLIC NVCVStatus cvcudaReformatSubmit(NVCVOperatorHandle handle, cudaStr } #endif +/** @} */ + #endif /* CVCUDA_REFORMAT_H */ diff --git a/src/cvcuda/include/cvcuda/OpReformat.hpp b/src/cvcuda/include/cvcuda/OpReformat.hpp index 30590f911..1e4c5d157 100644 --- a/src/cvcuda/include/cvcuda/OpReformat.hpp +++ b/src/cvcuda/include/cvcuda/OpReformat.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -34,6 +34,8 @@ #include #include +#include + namespace cvcuda { class Reformat final : public IOperator @@ -41,38 +43,34 @@ class Reformat final : public IOperator public: explicit Reformat(); - ~Reformat(); - - void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out); + void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out) const; - virtual NVCVOperatorHandle handle() const noexcept override; + NVCVOperatorHandle handle() const noexcept override; private: - NVCVOperatorHandle m_handle; + detail::OperatorHandle m_handle; }; inline Reformat::Reformat() { - nvcv::detail::CheckThrow(cvcudaReformatCreate(&m_handle)); - assert(m_handle); -} - -inline Reformat::~Reformat() -{ - nvcvOperatorDestroy(m_handle); - m_handle = nullptr; + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaReformatCreate(&h)); + assert(h); + m_handle = detail::OperatorHandle{h}; } -inline void Reformat::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out) +inline void Reformat::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out) const { - nvcv::detail::CheckThrow(cvcudaReformatSubmit(m_handle, stream, in.handle(), out.handle())); + nvcv::detail::CheckThrow(cvcudaReformatSubmit(m_handle.get(), stream, in.handle(), out.handle())); } inline NVCVOperatorHandle Reformat::handle() const noexcept { - return m_handle; + return m_handle.get(); } } // namespace cvcuda +/** @} */ + #endif // CVCUDA_REFORMAT_HPP diff --git a/src/cvcuda/include/cvcuda/OpRemap.h b/src/cvcuda/include/cvcuda/OpRemap.h index df3afd250..33d7ccda2 100644 --- a/src/cvcuda/include/cvcuda/OpRemap.h +++ b/src/cvcuda/include/cvcuda/OpRemap.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -65,7 +65,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaRemapCreate(NVCVOperatorHandle *handle); * Limitations: * * Input: - * Data Layout: [NVCV_TENSOR_NHWC, NVCV_TENSOR_HWC] + * Data Layout: [NVCV_TENSOR_NHWC, NVCV_TENSOR_HWC, NVCV_TENSOR_NCHW, NVCV_TENSOR_CHW] * Channels: [1, 3, 4] * * Data Type | Allowed @@ -76,11 +76,12 @@ CVCUDA_PUBLIC NVCVStatus cvcudaRemapCreate(NVCVOperatorHandle *handle); * 16bit Signed | No * 32bit Unsigned | No * 32bit Signed | No + * 16bit Float | No * 32bit Float | Yes (only if Channel=1) * 64bit Float | No * * Output: - * Data Layout: [NVCV_TENSOR_NHWC, NVCV_TENSOR_HWC] + * Data Layout: [NVCV_TENSOR_NHWC, NVCV_TENSOR_HWC, NVCV_TENSOR_NCHW, NVCV_TENSOR_CHW] * Channels: [1, 3, 4] * * Data Type | Allowed @@ -91,6 +92,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaRemapCreate(NVCVOperatorHandle *handle); * 16bit Signed | No * 32bit Unsigned | No * 32bit Signed | No + * 16bit Float | No * 32bit Float | Yes (only if Channel=1) * 64bit Float | No * @@ -106,6 +108,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaRemapCreate(NVCVOperatorHandle *handle); * 16bit Signed | No * 32bit Unsigned | No * 32bit Signed | No + * 16bit Float | No * 32bit Float | Yes * 64bit Float | No * @@ -212,4 +215,6 @@ CVCUDA_PUBLIC NVCVStatus cvcudaRemapVarShapeSubmit(NVCVOperatorHandle handle, cu } #endif +/** @} */ + #endif /* CVCUDA_REMAP_H */ diff --git a/src/cvcuda/include/cvcuda/OpRemap.hpp b/src/cvcuda/include/cvcuda/OpRemap.hpp index 68cc328af..7fa2bef2c 100644 --- a/src/cvcuda/include/cvcuda/OpRemap.hpp +++ b/src/cvcuda/include/cvcuda/OpRemap.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -35,6 +35,8 @@ #include #include +#include + namespace cvcuda { class Remap final : public IOperator @@ -42,58 +44,56 @@ class Remap final : public IOperator public: explicit Remap(); - ~Remap(); - void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, const nvcv::Tensor &map, NVCVInterpolationType inInterp, NVCVInterpolationType mapInterp, NVCVRemapMapValueType mapValueType, - bool alignCorners, NVCVBorderType border, float4 borderValue); + bool alignCorners, NVCVBorderType border, float4 borderValue) const; void operator()(cudaStream_t stream, const nvcv::ImageBatch &in, const nvcv::ImageBatch &out, const nvcv::Tensor &map, NVCVInterpolationType inInterp, NVCVInterpolationType mapInterp, - NVCVRemapMapValueType mapValueType, bool alignCorners, NVCVBorderType border, float4 borderValue); + NVCVRemapMapValueType mapValueType, bool alignCorners, NVCVBorderType border, + float4 borderValue) const; - virtual NVCVOperatorHandle handle() const noexcept override; + NVCVOperatorHandle handle() const noexcept override; private: - NVCVOperatorHandle m_handle; + detail::OperatorHandle m_handle; }; inline Remap::Remap() { - nvcv::detail::CheckThrow(cvcudaRemapCreate(&m_handle)); - assert(m_handle); -} - -inline Remap::~Remap() -{ - nvcvOperatorDestroy(m_handle); + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaRemapCreate(&h)); + assert(h); + m_handle = detail::OperatorHandle{h}; } inline void Remap::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, const nvcv::Tensor &map, NVCVInterpolationType inInterp, NVCVInterpolationType mapInterp, NVCVRemapMapValueType mapValueType, bool alignCorners, NVCVBorderType border, - float4 borderValue) + float4 borderValue) const { - nvcv::detail::CheckThrow(cvcudaRemapSubmit(m_handle, stream, in.handle(), out.handle(), map.handle(), inInterp, - mapInterp, mapValueType, static_cast(alignCorners), border, - borderValue)); + nvcv::detail::CheckThrow(cvcudaRemapSubmit(m_handle.get(), stream, in.handle(), out.handle(), map.handle(), + inInterp, mapInterp, mapValueType, static_cast(alignCorners), + border, borderValue)); } inline void Remap::operator()(cudaStream_t stream, const nvcv::ImageBatch &in, const nvcv::ImageBatch &out, const nvcv::Tensor &map, NVCVInterpolationType inInterp, NVCVInterpolationType mapInterp, NVCVRemapMapValueType mapValueType, bool alignCorners, NVCVBorderType border, - float4 borderValue) + float4 borderValue) const { - nvcv::detail::CheckThrow(cvcudaRemapVarShapeSubmit(m_handle, stream, in.handle(), out.handle(), map.handle(), + nvcv::detail::CheckThrow(cvcudaRemapVarShapeSubmit(m_handle.get(), stream, in.handle(), out.handle(), map.handle(), inInterp, mapInterp, mapValueType, static_cast(alignCorners), border, borderValue)); } inline NVCVOperatorHandle Remap::handle() const noexcept { - return m_handle; + return m_handle.get(); } } // namespace cvcuda +/** @} */ + #endif // CVCUDA__REMAP_HPP diff --git a/src/cvcuda/include/cvcuda/OpResize.h b/src/cvcuda/include/cvcuda/OpResize.h index d50517062..5d6bcb900 100644 --- a/src/cvcuda/include/cvcuda/OpResize.h +++ b/src/cvcuda/include/cvcuda/OpResize.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -57,7 +57,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaResizeCreate(NVCVOperatorHandle *handle); * Limitations: * * Input: - * Data Layout: [kNHWC, kHWC] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] * Channels: [1, 3, 4] * * Data Type | Allowed @@ -68,11 +68,12 @@ CVCUDA_PUBLIC NVCVStatus cvcudaResizeCreate(NVCVOperatorHandle *handle); * 16bit Signed | Yes * 32bit Unsigned | No * 32bit Signed | No + * 16bit Float | No * 32bit Float | Yes * 64bit Float | No * * Output: - * Data Layout: [kNHWC, kHWC] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] * Channels: [1, 3, 4] * * Data Type | Allowed @@ -83,6 +84,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaResizeCreate(NVCVOperatorHandle *handle); * 16bit Signed | Yes * 32bit Unsigned | No * 32bit Signed | No + * 16bit Float | No * 32bit Float | Yes * 64bit Float | No * diff --git a/src/cvcuda/include/cvcuda/OpResize.hpp b/src/cvcuda/include/cvcuda/OpResize.hpp index 328e4a8ac..ccdd8c92d 100644 --- a/src/cvcuda/include/cvcuda/OpResize.hpp +++ b/src/cvcuda/include/cvcuda/OpResize.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -35,6 +35,8 @@ #include #include +#include + namespace cvcuda { class Resize final : public IOperator @@ -42,48 +44,45 @@ class Resize final : public IOperator public: explicit Resize(); - ~Resize(); - void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, - const NVCVInterpolationType interpolation); + const NVCVInterpolationType interpolation) const; void operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, - const NVCVInterpolationType interpolation); + const NVCVInterpolationType interpolation) const; - virtual NVCVOperatorHandle handle() const noexcept override; + NVCVOperatorHandle handle() const noexcept override; private: - NVCVOperatorHandle m_handle; + detail::OperatorHandle m_handle; }; inline Resize::Resize() { - nvcv::detail::CheckThrow(cvcudaResizeCreate(&m_handle)); - assert(m_handle); -} - -inline Resize::~Resize() -{ - nvcvOperatorDestroy(m_handle); - m_handle = nullptr; + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaResizeCreate(&h)); + assert(h); + m_handle = detail::OperatorHandle{h}; } inline void Resize::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, - const NVCVInterpolationType interpolation) + const NVCVInterpolationType interpolation) const { - nvcv::detail::CheckThrow(cvcudaResizeSubmit(m_handle, stream, in.handle(), out.handle(), interpolation)); + nvcv::detail::CheckThrow(cvcudaResizeSubmit(m_handle.get(), stream, in.handle(), out.handle(), interpolation)); } inline void Resize::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, - const nvcv::ImageBatchVarShape &out, const NVCVInterpolationType interpolation) + const nvcv::ImageBatchVarShape &out, const NVCVInterpolationType interpolation) const { - nvcv::detail::CheckThrow(cvcudaResizeVarShapeSubmit(m_handle, stream, in.handle(), out.handle(), interpolation)); + nvcv::detail::CheckThrow( + cvcudaResizeVarShapeSubmit(m_handle.get(), stream, in.handle(), out.handle(), interpolation)); } inline NVCVOperatorHandle Resize::handle() const noexcept { - return m_handle; + return m_handle.get(); } } // namespace cvcuda +/** @} */ + #endif // CVCUDA_RESIZE_HPP diff --git a/src/cvcuda/include/cvcuda/OpResizeCropConvertReformat.h b/src/cvcuda/include/cvcuda/OpResizeCropConvertReformat.h index cd70ff631..da0085b38 100644 --- a/src/cvcuda/include/cvcuda/OpResizeCropConvertReformat.h +++ b/src/cvcuda/include/cvcuda/OpResizeCropConvertReformat.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -103,7 +103,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaResizeCropConvertReformatCreate(NVCVOperatorHandl * Limitations: * * Input: - * + Data Layout: [NVCV_TENSOR_HWC, NVCV_TENSOR_NHWC] + * + Data Layout: [NVCV_TENSOR_HWC, NVCV_TENSOR_NHWC, NVCV_TENSOR_CHW, NVCV_TENSOR_NCHW] * + Channels: [1, 3] * * Data Type | Allowed @@ -114,12 +114,12 @@ CVCUDA_PUBLIC NVCVStatus cvcudaResizeCropConvertReformatCreate(NVCVOperatorHandl * 16bit Signed | No * 32bit Unsigned | No * 32bit Signed | No + * 16bit Float | No * 32bit Float | No * 64bit Float | No * * Output: - * + Data Layout: [NVCV_TENSOR_NHWC, NVCV_TENSOR_HWC, - * NVCV_TENSOR_NCHW, NVCV_TENSOR_CHW] + * + Data Layout: [NVCV_TENSOR_NHWC, NVCV_TENSOR_HWC, NVCV_TENSOR_NCHW, NVCV_TENSOR_CHW] * + Channels: [1, 3] * * Data Type | Allowed @@ -130,6 +130,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaResizeCropConvertReformatCreate(NVCVOperatorHandl * 16bit Signed | No * 32bit Unsigned | No * 32bit Signed | No + * 16bit Float | No * 32bit Float | Yes * 64bit Float | No * diff --git a/src/cvcuda/include/cvcuda/OpResizeCropConvertReformat.hpp b/src/cvcuda/include/cvcuda/OpResizeCropConvertReformat.hpp index cbb13ee78..019ec8ebd 100644 --- a/src/cvcuda/include/cvcuda/OpResizeCropConvertReformat.hpp +++ b/src/cvcuda/include/cvcuda/OpResizeCropConvertReformat.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -35,6 +35,8 @@ #include #include +#include + namespace cvcuda { class ResizeCropConvertReformat final : public IOperator @@ -42,60 +44,58 @@ class ResizeCropConvertReformat final : public IOperator public: explicit ResizeCropConvertReformat(); - ~ResizeCropConvertReformat(); - void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, const NVCVSize2D resizeDim, const NVCVInterpolationType interpolation, const int2 cropPos, const NVCVChannelManip manip = NVCV_CHANNEL_NO_OP, const float scale = 1, const float offset = 0, - const bool srcCast = true); + const bool srcCast = true) const; void operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::Tensor &out, const NVCVSize2D resizeDim, const NVCVInterpolationType interpolation, const int2 cropPos, const NVCVChannelManip manip = NVCV_CHANNEL_NO_OP, const float scale = 1, const float offset = 0, - const bool srcCast = true); + const bool srcCast = true) const; - virtual NVCVOperatorHandle handle() const noexcept override; + NVCVOperatorHandle handle() const noexcept override; private: - NVCVOperatorHandle m_handle; + detail::OperatorHandle m_handle; }; inline ResizeCropConvertReformat::ResizeCropConvertReformat() { - nvcv::detail::CheckThrow(cvcudaResizeCropConvertReformatCreate(&m_handle)); - assert(m_handle); -} - -inline ResizeCropConvertReformat::~ResizeCropConvertReformat() -{ - nvcvOperatorDestroy(m_handle); - m_handle = nullptr; + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaResizeCropConvertReformatCreate(&h)); + assert(h); + m_handle = detail::OperatorHandle{h}; } inline void ResizeCropConvertReformat::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, const NVCVSize2D resizeDim, const NVCVInterpolationType interpolation, const int2 cropPos, const NVCVChannelManip manip, const float scale, - const float offset, const bool srcCast) + const float offset, const bool srcCast) const { - nvcv::detail::CheckThrow(cvcudaResizeCropConvertReformatSubmit( - m_handle, stream, in.handle(), out.handle(), resizeDim, interpolation, cropPos, manip, scale, offset, srcCast)); + nvcv::detail::CheckThrow(cvcudaResizeCropConvertReformatSubmit(m_handle.get(), stream, in.handle(), out.handle(), + resizeDim, interpolation, cropPos, manip, scale, + offset, srcCast)); } inline void ResizeCropConvertReformat::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::Tensor &out, const NVCVSize2D resizeDim, const NVCVInterpolationType interpolation, const int2 cropPos, const NVCVChannelManip manip, const float scale, const float offset, - const bool srcCast) + const bool srcCast) const { - nvcv::detail::CheckThrow(cvcudaResizeCropConvertReformatVarShapeSubmit( - m_handle, stream, in.handle(), out.handle(), resizeDim, interpolation, cropPos, manip, scale, offset, srcCast)); + nvcv::detail::CheckThrow(cvcudaResizeCropConvertReformatVarShapeSubmit(m_handle.get(), stream, in.handle(), + out.handle(), resizeDim, interpolation, + cropPos, manip, scale, offset, srcCast)); } inline NVCVOperatorHandle ResizeCropConvertReformat::handle() const noexcept { - return m_handle; + return m_handle.get(); } } // namespace cvcuda +/** @} */ + #endif // CVCUDA__RESIZE_CROP_HPP diff --git a/src/cvcuda/include/cvcuda/OpRotate.h b/src/cvcuda/include/cvcuda/OpRotate.h index a8f6d6694..e7d9d80d7 100644 --- a/src/cvcuda/include/cvcuda/OpRotate.h +++ b/src/cvcuda/include/cvcuda/OpRotate.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -59,7 +59,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaRotateCreate(NVCVOperatorHandle *handle, const in * Limitations: * * Input: - * Data Layout: [kNHWC, kHWC] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] * Channels: [1, 3, 4] * * Data Type | Allowed @@ -70,11 +70,12 @@ CVCUDA_PUBLIC NVCVStatus cvcudaRotateCreate(NVCVOperatorHandle *handle, const in * 16bit Signed | Yes * 32bit Unsigned | No * 32bit Signed | No + * 16bit Float | No * 32bit Float | Yes * 64bit Float | No * * Output: - * Data Layout: [kNHWC, kHWC] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] * Channels: [1, 3, 4] * * Data Type | Allowed @@ -85,6 +86,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaRotateCreate(NVCVOperatorHandle *handle, const in * 16bit Signed | Yes * 32bit Unsigned | No * 32bit Signed | No + * 16bit Float | No * 32bit Float | Yes * 64bit Float | No * diff --git a/src/cvcuda/include/cvcuda/OpRotate.hpp b/src/cvcuda/include/cvcuda/OpRotate.hpp index 4e36418b6..1734ee4cd 100644 --- a/src/cvcuda/include/cvcuda/OpRotate.hpp +++ b/src/cvcuda/include/cvcuda/OpRotate.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -35,6 +35,8 @@ #include #include +#include + namespace cvcuda { class Rotate final : public IOperator @@ -42,52 +44,50 @@ class Rotate final : public IOperator public: explicit Rotate(const int maxVarShapeBatchSize); - ~Rotate(); - void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, const double angleDeg, - const double2 shift, const NVCVInterpolationType interpolation); + const double2 shift, const NVCVInterpolationType interpolation) const; void operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, - const nvcv::Tensor &angleDeg, const nvcv::Tensor &shift, const NVCVInterpolationType interpolation); + const nvcv::Tensor &angleDeg, const nvcv::Tensor &shift, + const NVCVInterpolationType interpolation) const; - virtual NVCVOperatorHandle handle() const noexcept override; + NVCVOperatorHandle handle() const noexcept override; private: - NVCVOperatorHandle m_handle; + detail::OperatorHandle m_handle; }; inline Rotate::Rotate(const int maxVarShapeBatchSize) { - nvcv::detail::CheckThrow(cvcudaRotateCreate(&m_handle, maxVarShapeBatchSize)); - assert(m_handle); -} - -inline Rotate::~Rotate() -{ - nvcvOperatorDestroy(m_handle); - m_handle = nullptr; + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaRotateCreate(&h, maxVarShapeBatchSize)); + assert(h); + m_handle = detail::OperatorHandle{h}; } inline void Rotate::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, - const double angleDeg, const double2 shift, const NVCVInterpolationType interpolation) + const double angleDeg, const double2 shift, + const NVCVInterpolationType interpolation) const { nvcv::detail::CheckThrow( - cvcudaRotateSubmit(m_handle, stream, in.handle(), out.handle(), angleDeg, shift, interpolation)); + cvcudaRotateSubmit(m_handle.get(), stream, in.handle(), out.handle(), angleDeg, shift, interpolation)); } inline void Rotate::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, const nvcv::Tensor &angleDeg, - const nvcv::Tensor &shift, const NVCVInterpolationType interpolation) + const nvcv::Tensor &shift, const NVCVInterpolationType interpolation) const { - nvcv::detail::CheckThrow(cvcudaRotateVarShapeSubmit(m_handle, stream, in.handle(), out.handle(), angleDeg.handle(), - shift.handle(), interpolation)); + nvcv::detail::CheckThrow(cvcudaRotateVarShapeSubmit(m_handle.get(), stream, in.handle(), out.handle(), + angleDeg.handle(), shift.handle(), interpolation)); } inline NVCVOperatorHandle Rotate::handle() const noexcept { - return m_handle; + return m_handle.get(); } } // namespace cvcuda +/** @} */ + #endif // CVCUDA_ROTATE_HPP diff --git a/src/cvcuda/include/cvcuda/OpSIFT.h b/src/cvcuda/include/cvcuda/OpSIFT.h index b2ba49348..d64559519 100644 --- a/src/cvcuda/include/cvcuda/OpSIFT.h +++ b/src/cvcuda/include/cvcuda/OpSIFT.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -72,7 +72,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaSIFTCreate(NVCVOperatorHandle *handle, int3 maxSh * Limitations: * * Input: - * Data Layout: [HWC, NHWC] + * Data Layout: [HWC, NHWC, CHW, NCHW] * Channels: [1] * * Data Type | Allowed @@ -83,6 +83,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaSIFTCreate(NVCVOperatorHandle *handle, int3 maxSh * 16bit Signed | No * 32bit Unsigned | No * 32bit Signed | No + * 16bit Float | No * 32bit Float | No * 64bit Float | No * @@ -91,9 +92,9 @@ CVCUDA_PUBLIC NVCVStatus cvcudaSIFTCreate(NVCVOperatorHandle *handle, int3 maxSh * * @param [in] stream Handle to a valid CUDA stream. * - * @param [in] in Input tensor. The expected layout is [HWC] or [NHWC], where N is the number of samples, - * i.e. images with height H and width W and channels C, inside the tensor. This operator extracts - * features and computes descriptors of each input image in the \ref in tensor. + * @param [in] in Input tensor. The expected layout is [HWC], [NHWC], [CHW] or [NCHW], where N is the number of + * samples, i.e. images with height H and width W and channels C, inside the tensor. This operator + * extracts features and computes descriptors of each input image in the \ref in tensor. * + Check above limitations table to the input tensor data layout, number of channels and data type. * * @param [out] featCoords Output tensor with features coordinates. The expected layout is [NM] or [NMC] meaning a @@ -190,4 +191,6 @@ CVCUDA_PUBLIC NVCVStatus cvcudaSIFTSubmit(NVCVOperatorHandle handle, cudaStream_ } #endif +/** @} */ + #endif /* CVCUDA_SIFT_H */ diff --git a/src/cvcuda/include/cvcuda/OpSIFT.hpp b/src/cvcuda/include/cvcuda/OpSIFT.hpp index 2f50ea8c7..1c821e599 100644 --- a/src/cvcuda/include/cvcuda/OpSIFT.hpp +++ b/src/cvcuda/include/cvcuda/OpSIFT.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -34,6 +34,8 @@ #include #include +#include + namespace cvcuda { class SIFT final : public IOperator @@ -41,46 +43,42 @@ class SIFT final : public IOperator public: explicit SIFT(int3 maxShape, int maxOctaveLayers); - ~SIFT(); - void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &featCoords, const nvcv::Tensor &featMetadata, const nvcv::Tensor &featDescriptors, const nvcv::Tensor &numFeatures, int numOctaveLayers, float contrastThreshold, float edgeThreshold, - float initSigma, NVCVSIFTFlagType flags); + float initSigma, NVCVSIFTFlagType flags) const; - virtual NVCVOperatorHandle handle() const noexcept override; + NVCVOperatorHandle handle() const noexcept override; private: - NVCVOperatorHandle m_handle; + detail::OperatorHandle m_handle; }; inline SIFT::SIFT(int3 maxShape, int maxOctaveLayers) { - nvcv::detail::CheckThrow(cvcudaSIFTCreate(&m_handle, maxShape, maxOctaveLayers)); - assert(m_handle); -} - -inline SIFT::~SIFT() -{ - nvcvOperatorDestroy(m_handle); - m_handle = nullptr; + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaSIFTCreate(&h, maxShape, maxOctaveLayers)); + assert(h); + m_handle = detail::OperatorHandle{h}; } inline void SIFT::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &featCoords, const nvcv::Tensor &featMetadata, const nvcv::Tensor &featDescriptors, const nvcv::Tensor &numFeatures, int numOctaveLayers, float contrastThreshold, - float edgeThreshold, float initSigma, NVCVSIFTFlagType flags) + float edgeThreshold, float initSigma, NVCVSIFTFlagType flags) const { - nvcv::detail::CheckThrow(cvcudaSIFTSubmit(m_handle, stream, in.handle(), featCoords.handle(), featMetadata.handle(), - featDescriptors.handle(), numFeatures.handle(), numOctaveLayers, - contrastThreshold, edgeThreshold, initSigma, flags)); + nvcv::detail::CheckThrow(cvcudaSIFTSubmit(m_handle.get(), stream, in.handle(), featCoords.handle(), + featMetadata.handle(), featDescriptors.handle(), numFeatures.handle(), + numOctaveLayers, contrastThreshold, edgeThreshold, initSigma, flags)); } inline NVCVOperatorHandle SIFT::handle() const noexcept { - return m_handle; + return m_handle.get(); } } // namespace cvcuda +/** @} */ + #endif // CVCUDA_SIFT_HPP diff --git a/src/cvcuda/include/cvcuda/OpSolarize.h b/src/cvcuda/include/cvcuda/OpSolarize.h new file mode 100644 index 000000000..c9f981d64 --- /dev/null +++ b/src/cvcuda/include/cvcuda/OpSolarize.h @@ -0,0 +1,150 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +/** + * @file OpSolarize.h + * + * @brief Solarizes an image — inverts every pixel at or above a threshold, per element. + * + * Solarize computes, element-wise, @f$ out = (in \ge threshold) ? (bound - in) : in @f$, where + * @p bound is the maximum representable value of the data type (255 for 8-bit unsigned, 65535 for + * 16-bit unsigned, and 1.0 for 32-bit float). The above-threshold branch is the photometric + * negative; the below-threshold branch passes the pixel through unchanged. The operation is + * channel-independent. + * + * Reference: mimics torchvision.transforms.v2.functional.solarize + * (out = where(x >= threshold, invert(x), x)). The mapping is exactly representable, so the result + * is bit-exact with no rounding. + * + * @defgroup NVCV_C_ALGORITHM__SOLARIZE Solarize + * @{ + */ + +#ifndef CVCUDA__SOLARIZE_H +#define CVCUDA__SOLARIZE_H + +#include "Operator.h" +#include "detail/Export.h" + +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + +/** Constructs an instance of the Solarize operator. + * + * @param [out] handle Where the operator instance handle will be written to. + * + Must not be NULL. + * + * @retval #NVCV_ERROR_INVALID_ARGUMENT Handle is null. + * @retval #NVCV_ERROR_OUT_OF_MEMORY Not enough memory to create the operator. + * @retval #NVCV_SUCCESS Operation executed successfully. + */ +CVCUDA_PUBLIC NVCVStatus cvcudaSolarizeCreate(NVCVOperatorHandle *handle); + +/** Executes the Solarize operation on the given cuda stream. This operation does not + * wait for completion. + * + * Limitations: + * + * Input: + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 3, 4] + * + * Data Type | Allowed + * -------------- | ------------- + * 8bit Unsigned | Yes + * 8bit Signed | No + * 16bit Unsigned | Yes + * 16bit Signed | No + * 32bit Unsigned | No + * 32bit Signed | No + * 32bit Float | Yes + * 64bit Float | No + * + * Output: + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] + * Channels: [1, 3, 4] + * + * Data Type | Allowed + * -------------- | ------------- + * 8bit Unsigned | Yes + * 8bit Signed | No + * 16bit Unsigned | Yes + * 16bit Signed | No + * 32bit Unsigned | No + * 32bit Signed | No + * 32bit Float | Yes + * 64bit Float | No + * + * Input/Output dependency + * + * Property | Input == Output + * -------------- | ------------- + * Data Layout | Yes + * Data Type | Yes + * Number | Yes + * Channels | Yes + * Width | Yes + * Height | Yes + * + * @param [in] handle Handle to the operator. + * + Must not be NULL. + * @param [in] stream Handle to a valid CUDA stream. + * + * @param [in] in input tensor. + * + * @param [out] out output tensor. + * + * @param [in] threshold Pixels whose value is greater than or equal to this threshold are inverted; + * the threshold is expressed in the pixel value domain (e.g. 0..255 for 8-bit + * unsigned, 0..1 for normalized float) and applied to all images. + * + * @retval #NVCV_ERROR_INVALID_ARGUMENT Some parameter is outside valid range. + * @retval #NVCV_ERROR_INTERNAL Internal error in the operator, invalid types passed in. + * @retval #NVCV_SUCCESS Operation executed successfully. + */ +CVCUDA_PUBLIC NVCVStatus cvcudaSolarizeSubmit(NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle in, + NVCVTensorHandle out, double threshold); + +/** Executes the Solarize operation on a batch of variable-shaped images on the given cuda stream. + * Same limitations as cvcudaSolarizeSubmit. + * + * @param [in] handle Handle to the operator. Must not be NULL. + * @param [in] stream Handle to a valid CUDA stream. + * @param [in] in input image batch. + * @param [out] out output image batch. + * @param [in] threshold Inversion threshold in the pixel value domain (see cvcudaSolarizeSubmit). + * + * @retval #NVCV_ERROR_INVALID_ARGUMENT Some parameter is outside valid range. + * @retval #NVCV_ERROR_INTERNAL Internal error in the operator, invalid types passed in. + * @retval #NVCV_SUCCESS Operation executed successfully. + */ +CVCUDA_PUBLIC NVCVStatus cvcudaSolarizeVarShapeSubmit(NVCVOperatorHandle handle, cudaStream_t stream, + NVCVImageBatchHandle in, NVCVImageBatchHandle out, + double threshold); + +#ifdef __cplusplus +} +#endif + +#endif /* CVCUDA__SOLARIZE_H */ diff --git a/src/cvcuda/include/cvcuda/OpSolarize.hpp b/src/cvcuda/include/cvcuda/OpSolarize.hpp new file mode 100644 index 000000000..650796286 --- /dev/null +++ b/src/cvcuda/include/cvcuda/OpSolarize.hpp @@ -0,0 +1,84 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +/** + * @file OpSolarize.hpp + * + * @brief Defines the public C++ Class for the Solarize operation. + * @defgroup NVCV_CPP_ALGORITHM__SOLARIZE Solarize + * @{ + */ + +#ifndef CVCUDA__SOLARIZE_HPP +#define CVCUDA__SOLARIZE_HPP + +#include "IOperator.hpp" +#include "OpSolarize.h" + +#include +#include +#include +#include +#include + +namespace cvcuda { + +class Solarize final : public IOperator +{ +public: + explicit Solarize(); + + void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, double threshold) const; + + void operator()(cudaStream_t stream, const nvcv::ImageBatch &in, const nvcv::ImageBatch &out, + double threshold) const; + + NVCVOperatorHandle handle() const noexcept override; + +private: + detail::OperatorHandle m_handle; +}; + +inline Solarize::Solarize() +{ + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaSolarizeCreate(&h)); + assert(h); + m_handle = detail::OperatorHandle{h}; +} + +inline void Solarize::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, + double threshold) const +{ + nvcv::detail::CheckThrow(cvcudaSolarizeSubmit(m_handle.get(), stream, in.handle(), out.handle(), threshold)); +} + +inline void Solarize::operator()(cudaStream_t stream, const nvcv::ImageBatch &in, const nvcv::ImageBatch &out, + double threshold) const +{ + nvcv::detail::CheckThrow( + cvcudaSolarizeVarShapeSubmit(m_handle.get(), stream, in.handle(), out.handle(), threshold)); +} + +inline NVCVOperatorHandle Solarize::handle() const noexcept +{ + return m_handle.get(); +} + +} // namespace cvcuda + +#endif // CVCUDA__SOLARIZE_HPP diff --git a/src/cvcuda/include/cvcuda/OpStack.h b/src/cvcuda/include/cvcuda/OpStack.h index 58e9bff17..f21747665 100644 --- a/src/cvcuda/include/cvcuda/OpStack.h +++ b/src/cvcuda/include/cvcuda/OpStack.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -30,6 +30,7 @@ #include "detail/Export.h" #include +#include #include #include @@ -59,7 +60,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaStackCreate(NVCVOperatorHandle *handle); * Limitations: * * Input: - * Data Layout: [NHWC, NCHW, CHW, HWC] + * Data Layout: [kNHWC, kNCHW, kCHW, kHWC] * Channels: [1,2,3,4] * * Data Type | Allowed @@ -70,11 +71,12 @@ CVCUDA_PUBLIC NVCVStatus cvcudaStackCreate(NVCVOperatorHandle *handle); * 16bit Signed | Yes * 32bit Unsigned | Yes * 32bit Signed | Yes + * 16bit Float | Yes * 32bit Float | Yes * 64bit Float | Yes * * Output: - * Data Layout: [NHWC, NCHW] + * Data Layout: [kNHWC, kNCHW] * Channels: [1,2,3,4] * * Data Type | Allowed @@ -85,6 +87,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaStackCreate(NVCVOperatorHandle *handle); * 16bit Signed | Yes * 32bit Unsigned | Yes * 32bit Signed | Yes + * 16bit Float | Yes * 32bit Float | Yes * 64bit Float | Yes * @@ -105,7 +108,8 @@ CVCUDA_PUBLIC NVCVStatus cvcudaStackCreate(NVCVOperatorHandle *handle); * * @param [in] in input tensors batch. * - * @param [out] out output tensor NHWC/CHW where N is equal to the number of all input tensors. + * @param [out] out output tensor in NHWC or NCHW layout, where N is equal to + * the total number of samples across all input tensors. * * @retval #NVCV_ERROR_INVALID_ARGUMENT Some parameter is outside valid range. * @retval #NVCV_ERROR_INTERNAL Internal error in the operator, invalid types passed in. @@ -114,8 +118,31 @@ CVCUDA_PUBLIC NVCVStatus cvcudaStackCreate(NVCVOperatorHandle *handle); CVCUDA_PUBLIC NVCVStatus cvcudaStackSubmit(NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorBatchHandle in, NVCVTensorHandle out); +/** + * Executes the Stack operation on the given cuda stream using a variable shape image batch as input. + * This operation does not wait for completion. The stack operation copies source images into an output tensor. + * The output tensor is a concatenation of the source images. All of the source images must have the same + * format (data type and number of channels) and dimensions (width and height). + * + * @param [in] handle Handle to the operator. + * + Must not be NULL. + * @param [in] stream Handle to a valid CUDA stream. + * + * @param [in] in input varshape image batch. + * + * @param [out] out output tensor NHWC/NCHW where N is equal to the number of input images. + * + * @retval #NVCV_ERROR_INVALID_ARGUMENT Some parameter is outside valid range. + * @retval #NVCV_ERROR_INTERNAL Internal error in the operator, invalid types passed in. + * @retval #NVCV_SUCCESS Operation executed successfully. + */ +CVCUDA_PUBLIC NVCVStatus cvcudaStackVarShapeSubmit(NVCVOperatorHandle handle, cudaStream_t stream, + NVCVImageBatchHandle in, NVCVTensorHandle out); + #ifdef __cplusplus } #endif +/** @} */ + #endif /* CVCUDA__STACK_H */ diff --git a/src/cvcuda/include/cvcuda/OpStack.hpp b/src/cvcuda/include/cvcuda/OpStack.hpp index 8f85a7363..db1a3d187 100644 --- a/src/cvcuda/include/cvcuda/OpStack.hpp +++ b/src/cvcuda/include/cvcuda/OpStack.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -30,11 +30,14 @@ #include "OpStack.h" #include +#include #include #include #include #include +#include + namespace cvcuda { class Stack final : public IOperator @@ -42,38 +45,40 @@ class Stack final : public IOperator public: explicit Stack(); - ~Stack(); - - void operator()(cudaStream_t stream, const nvcv::TensorBatch &in, const nvcv::Tensor &out); + void operator()(cudaStream_t stream, const nvcv::TensorBatch &in, const nvcv::Tensor &out) const; + void operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::Tensor &out) const; - virtual NVCVOperatorHandle handle() const noexcept override; + NVCVOperatorHandle handle() const noexcept override; private: - NVCVOperatorHandle m_handle; + detail::OperatorHandle m_handle; }; inline Stack::Stack() { - nvcv::detail::CheckThrow(cvcudaStackCreate(&m_handle)); - assert(m_handle); + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaStackCreate(&h)); + assert(h); + m_handle = detail::OperatorHandle{h}; } -inline Stack::~Stack() +inline void Stack::operator()(cudaStream_t stream, const nvcv::TensorBatch &in, const nvcv::Tensor &out) const { - nvcvOperatorDestroy(m_handle); - m_handle = nullptr; + nvcv::detail::CheckThrow(cvcudaStackSubmit(m_handle.get(), stream, in.handle(), out.handle())); } -inline void Stack::operator()(cudaStream_t stream, const nvcv::TensorBatch &in, const nvcv::Tensor &out) +inline void Stack::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::Tensor &out) const { - nvcv::detail::CheckThrow(cvcudaStackSubmit(m_handle, stream, in.handle(), out.handle())); + nvcv::detail::CheckThrow(cvcudaStackVarShapeSubmit(m_handle.get(), stream, in.handle(), out.handle())); } inline NVCVOperatorHandle Stack::handle() const noexcept { - return m_handle; + return m_handle.get(); } } // namespace cvcuda +/** @} */ + #endif // CVCUDA__STACK_HPP diff --git a/src/cvcuda/include/cvcuda/OpThreshold.h b/src/cvcuda/include/cvcuda/OpThreshold.h index 94cf75018..402350c83 100644 --- a/src/cvcuda/include/cvcuda/OpThreshold.h +++ b/src/cvcuda/include/cvcuda/OpThreshold.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -68,9 +68,10 @@ CVCUDA_PUBLIC NVCVStatus cvcudaThresholdCreate(NVCVOperatorHandle *handle, uint3 * Limitations: * * Input: - * Data Layout: [kNHWC, kHWC, kNCHW, KCHW] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] * Channels: [1, 2, 3, 4] for the thresholding type NVCV_THRESH_BINARY, NVCV_THRESH_BINARY_INV, * NVCV_THRESH_TRUNC, NVCV_THRESH_TOZERO and NVCV_THRESH_TOZERO_INV. + * [1, 3, 4] for planar kNCHW/kCHW layouts. * [1] for the thresholding type NVCV_THRESH_OTSU and NVCV_THRESH_TRIANGLE. * * Data Type | Allowed @@ -82,6 +83,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaThresholdCreate(NVCVOperatorHandle *handle, uint3 * 16bit Signed | Yes * 32bit Unsigned | No * 32bit Signed | No + * 16bit Float | No * 32bit Float | Yes * 64bit Float | Yes * -------------- | ------------- for the thresholding type NVCV_THRESH_OTSU and NVCV_THRESH_TRIANGLE. @@ -91,14 +93,16 @@ CVCUDA_PUBLIC NVCVStatus cvcudaThresholdCreate(NVCVOperatorHandle *handle, uint3 * 16bit Signed | No * 32bit Unsigned | No * 32bit Signed | No + * 16bit Float | No * 32bit Float | No * 64bit Float | No * * * Output: - * Data Layout: [kNHWC, kHWC, kNCHW, KCHW] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] * Channels: [1, 2, 3, 4] for the thresholding type NVCV_THRESH_BINARY, NVCV_THRESH_BINARY_INV, * NVCV_THRESH_TRUNC, NVCV_THRESH_TOZERO and NVCV_THRESH_TOZERO_INV. + * [1, 3, 4] for planar kNCHW/kCHW layouts. * [1] for the thresholding type NVCV_THRESH_OTSU and NVCV_THRESH_TRIANGLE. * * @@ -111,6 +115,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaThresholdCreate(NVCVOperatorHandle *handle, uint3 * 16bit Signed | Yes * 32bit Unsigned | No * 32bit Signed | No + * 16bit Float | No * 32bit Float | Yes * 64bit Float | Yes * -------------- | ------------- for the thresholding type NVCV_THRESH_OTSU and NVCV_THRESH_TRIANGLE. @@ -120,6 +125,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaThresholdCreate(NVCVOperatorHandle *handle, uint3 * 16bit Signed | No * 32bit Unsigned | No * 32bit Signed | No + * 16bit Float | No * 32bit Float | No * 64bit Float | No * diff --git a/src/cvcuda/include/cvcuda/OpThreshold.hpp b/src/cvcuda/include/cvcuda/OpThreshold.hpp index aaae501af..1e67a8a59 100644 --- a/src/cvcuda/include/cvcuda/OpThreshold.hpp +++ b/src/cvcuda/include/cvcuda/OpThreshold.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -35,6 +35,8 @@ #include #include +#include + namespace cvcuda { class Threshold final : public IOperator @@ -42,52 +44,48 @@ class Threshold final : public IOperator public: explicit Threshold(uint32_t type, int32_t maxBatchSize); - ~Threshold(); - void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, const nvcv::Tensor &thresh, - const nvcv::Tensor &maxval); + const nvcv::Tensor &maxval) const; void operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, - const nvcv::Tensor &thresh, const nvcv::Tensor &maxval); + const nvcv::Tensor &thresh, const nvcv::Tensor &maxval) const; - virtual NVCVOperatorHandle handle() const noexcept override; + NVCVOperatorHandle handle() const noexcept override; private: - NVCVOperatorHandle m_handle; + detail::OperatorHandle m_handle; }; inline Threshold::Threshold(uint32_t type, int32_t maxBatchSize) { - nvcv::detail::CheckThrow(cvcudaThresholdCreate(&m_handle, type, maxBatchSize)); - assert(m_handle); -} - -inline Threshold::~Threshold() -{ - nvcvOperatorDestroy(m_handle); - m_handle = nullptr; + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaThresholdCreate(&h, type, maxBatchSize)); + assert(h); + m_handle = detail::OperatorHandle{h}; } inline void Threshold::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, - const nvcv::Tensor &thresh, const nvcv::Tensor &maxval) + const nvcv::Tensor &thresh, const nvcv::Tensor &maxval) const { nvcv::detail::CheckThrow( - cvcudaThresholdSubmit(m_handle, stream, in.handle(), out.handle(), thresh.handle(), maxval.handle())); + cvcudaThresholdSubmit(m_handle.get(), stream, in.handle(), out.handle(), thresh.handle(), maxval.handle())); } inline void Threshold::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, const nvcv::Tensor &thresh, - const nvcv::Tensor &maxval) + const nvcv::Tensor &maxval) const { - nvcv::detail::CheckThrow( - cvcudaThresholdVarShapeSubmit(m_handle, stream, in.handle(), out.handle(), thresh.handle(), maxval.handle())); + nvcv::detail::CheckThrow(cvcudaThresholdVarShapeSubmit(m_handle.get(), stream, in.handle(), out.handle(), + thresh.handle(), maxval.handle())); } inline NVCVOperatorHandle Threshold::handle() const noexcept { - return m_handle; + return m_handle.get(); } } // namespace cvcuda +/** @} */ + #endif // CVCUDA_THRESHOLD_HPP diff --git a/src/cvcuda/include/cvcuda/OpWarpAffine.h b/src/cvcuda/include/cvcuda/OpWarpAffine.h index 001650780..fb84c14d8 100644 --- a/src/cvcuda/include/cvcuda/OpWarpAffine.h +++ b/src/cvcuda/include/cvcuda/OpWarpAffine.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -36,7 +36,7 @@ #include #include -typedef float NVCVAffineTransform[6]; +typedef float NVCVAffineTransform[6]; // NOSONAR: C ABI exposes affine transforms as fixed-size arrays. #ifdef __cplusplus extern "C" @@ -66,7 +66,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaWarpAffineCreate(NVCVOperatorHandle *handle, cons * Limitations: * * Input: - * Data Layout: [kNHWC, kHWC] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] * Channels: [1,3,4] * * Data Type | Allowed @@ -77,11 +77,12 @@ CVCUDA_PUBLIC NVCVStatus cvcudaWarpAffineCreate(NVCVOperatorHandle *handle, cons * 16bit Signed | Yes * 32bit Unsigned | No * 32bit Signed | No + * 16bit Float | No * 32bit Float | Yes * 64bit Float | No * * Output: - * Data Layout: [kNHWC, kHWC] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] * Channels: [1,3,4] * * Data Type | Allowed @@ -92,6 +93,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaWarpAffineCreate(NVCVOperatorHandle *handle, cons * 16bit Signed | Yes * 32bit Unsigned | No * 32bit Signed | No + * 16bit Float | No * 32bit Float | Yes * 64bit Float | No * @@ -141,4 +143,6 @@ CVCUDA_PUBLIC NVCVStatus cvcudaWarpAffineVarShapeSubmit(NVCVOperatorHandle handl } #endif +/** @} */ + #endif /* CVCUDA_WARP_AFFINE_H */ diff --git a/src/cvcuda/include/cvcuda/OpWarpAffine.hpp b/src/cvcuda/include/cvcuda/OpWarpAffine.hpp index c42026646..b29e8f657 100644 --- a/src/cvcuda/include/cvcuda/OpWarpAffine.hpp +++ b/src/cvcuda/include/cvcuda/OpWarpAffine.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -36,6 +36,8 @@ #include #include +#include + namespace cvcuda { class WarpAffine final : public IOperator @@ -43,55 +45,51 @@ class WarpAffine final : public IOperator public: explicit WarpAffine(const int32_t maxVarShapeBatchSize); - ~WarpAffine(); - void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, const NVCVAffineTransform xform, const int32_t flags, const NVCVBorderType borderMode, - const float4 borderValue); + const float4 borderValue) const; void operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, const nvcv::Tensor &transMatrix, const int32_t flags, const NVCVBorderType borderMode, - const float4 borderValue); + const float4 borderValue) const; - virtual NVCVOperatorHandle handle() const noexcept override; + NVCVOperatorHandle handle() const noexcept override; private: - NVCVOperatorHandle m_handle; + detail::OperatorHandle m_handle; }; inline WarpAffine::WarpAffine(const int32_t maxVarShapeBatchSize) { - nvcv::detail::CheckThrow(cvcudaWarpAffineCreate(&m_handle, maxVarShapeBatchSize)); - assert(m_handle); -} - -inline WarpAffine::~WarpAffine() -{ - nvcvOperatorDestroy(m_handle); - m_handle = nullptr; + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaWarpAffineCreate(&h, maxVarShapeBatchSize)); + assert(h); + m_handle = detail::OperatorHandle{h}; } inline void WarpAffine::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, const NVCVAffineTransform xform, const int32_t flags, - const NVCVBorderType borderMode, const float4 borderValue) + const NVCVBorderType borderMode, const float4 borderValue) const { - nvcv::detail::CheckThrow( - cvcudaWarpAffineSubmit(m_handle, stream, in.handle(), out.handle(), xform, flags, borderMode, borderValue)); + nvcv::detail::CheckThrow(cvcudaWarpAffineSubmit(m_handle.get(), stream, in.handle(), out.handle(), xform, flags, + borderMode, borderValue)); } inline void WarpAffine::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, const nvcv::Tensor &transMatrix, - const int32_t flags, const NVCVBorderType borderMode, const float4 borderValue) + const int32_t flags, const NVCVBorderType borderMode, const float4 borderValue) const { - nvcv::detail::CheckThrow(cvcudaWarpAffineVarShapeSubmit(m_handle, stream, in.handle(), out.handle(), + nvcv::detail::CheckThrow(cvcudaWarpAffineVarShapeSubmit(m_handle.get(), stream, in.handle(), out.handle(), transMatrix.handle(), flags, borderMode, borderValue)); } inline NVCVOperatorHandle WarpAffine::handle() const noexcept { - return m_handle; + return m_handle.get(); } } // namespace cvcuda +/** @} */ + #endif // CVCUDA_WARP_AFFINE_HPP diff --git a/src/cvcuda/include/cvcuda/OpWarpPerspective.h b/src/cvcuda/include/cvcuda/OpWarpPerspective.h index 207629fc9..4a207f768 100644 --- a/src/cvcuda/include/cvcuda/OpWarpPerspective.h +++ b/src/cvcuda/include/cvcuda/OpWarpPerspective.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -37,7 +37,7 @@ #include // @brief storage for perspective transform matrix (row major) -typedef float NVCVPerspectiveTransform[9]; +typedef float NVCVPerspectiveTransform[9]; // NOSONAR: C ABI exposes perspective transforms as fixed-size arrays. #ifdef __cplusplus extern "C" @@ -65,7 +65,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaWarpPerspectiveCreate(NVCVOperatorHandle *handle, * Limitations: * * Input: - * Data Layout: [kNHWC, kHWC] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] * Channels: [1,3,4] * * Data Type | Allowed @@ -76,11 +76,12 @@ CVCUDA_PUBLIC NVCVStatus cvcudaWarpPerspectiveCreate(NVCVOperatorHandle *handle, * 16bit Signed | Yes * 32bit Unsigned | No * 32bit Signed | No + * 16bit Float | No * 32bit Float | Yes * 64bit Float | No * * Output: - * Data Layout: [kNHWC, kHWC] + * Data Layout: [kNHWC, kHWC, kNCHW, kCHW] * Channels: [1,3,4] * * Data Type | Allowed @@ -91,6 +92,7 @@ CVCUDA_PUBLIC NVCVStatus cvcudaWarpPerspectiveCreate(NVCVOperatorHandle *handle, * 16bit Signed | Yes * 32bit Unsigned | No * 32bit Signed | No + * 16bit Float | No * 32bit Float | Yes * 64bit Float | No * @@ -141,4 +143,6 @@ CVCUDA_PUBLIC NVCVStatus cvcudaWarpPerspectiveVarShapeSubmit(NVCVOperatorHandle } #endif +/** @} */ + #endif /* CVCUDA_WARP_PERSPECTIVE_H */ diff --git a/src/cvcuda/include/cvcuda/OpWarpPerspective.hpp b/src/cvcuda/include/cvcuda/OpWarpPerspective.hpp index cb91f3d1f..c4d19a613 100644 --- a/src/cvcuda/include/cvcuda/OpWarpPerspective.hpp +++ b/src/cvcuda/include/cvcuda/OpWarpPerspective.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -36,6 +36,8 @@ #include #include +#include + namespace cvcuda { class WarpPerspective final : public IOperator @@ -43,55 +45,52 @@ class WarpPerspective final : public IOperator public: explicit WarpPerspective(const int32_t maxVarShapeBatchSize); - ~WarpPerspective(); - void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, const NVCVPerspectiveTransform transMatrix, const int32_t flags, const NVCVBorderType borderMode, - const float4 borderValue); + const float4 borderValue) const; void operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, const nvcv::Tensor &transMatrix, const int32_t flags, const NVCVBorderType borderMode, - const float4 borderValue); + const float4 borderValue) const; - virtual NVCVOperatorHandle handle() const noexcept override; + NVCVOperatorHandle handle() const noexcept override; private: - NVCVOperatorHandle m_handle; + detail::OperatorHandle m_handle; }; inline WarpPerspective::WarpPerspective(const int32_t maxVarShapeBatchSize) { - nvcv::detail::CheckThrow(cvcudaWarpPerspectiveCreate(&m_handle, maxVarShapeBatchSize)); - assert(m_handle); -} - -inline WarpPerspective::~WarpPerspective() -{ - nvcvOperatorDestroy(m_handle); - m_handle = nullptr; + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcudaWarpPerspectiveCreate(&h, maxVarShapeBatchSize)); + assert(h); + m_handle = detail::OperatorHandle{h}; } inline void WarpPerspective::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, const NVCVPerspectiveTransform transMatrix, const int32_t flags, - const NVCVBorderType borderMode, const float4 borderValue) + const NVCVBorderType borderMode, const float4 borderValue) const { - nvcv::detail::CheckThrow(cvcudaWarpPerspectiveSubmit(m_handle, stream, in.handle(), out.handle(), transMatrix, + nvcv::detail::CheckThrow(cvcudaWarpPerspectiveSubmit(m_handle.get(), stream, in.handle(), out.handle(), transMatrix, flags, borderMode, borderValue)); } inline void WarpPerspective::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, const nvcv::Tensor &transMatrix, - const int32_t flags, const NVCVBorderType borderMode, const float4 borderValue) + const int32_t flags, const NVCVBorderType borderMode, + const float4 borderValue) const { - nvcv::detail::CheckThrow(cvcudaWarpPerspectiveVarShapeSubmit(m_handle, stream, in.handle(), out.handle(), + nvcv::detail::CheckThrow(cvcudaWarpPerspectiveVarShapeSubmit(m_handle.get(), stream, in.handle(), out.handle(), transMatrix.handle(), flags, borderMode, borderValue)); } inline NVCVOperatorHandle WarpPerspective::handle() const noexcept { - return m_handle; + return m_handle.get(); } } // namespace cvcuda +/** @} */ + #endif // CVCUDA_WARP_PERSPECTIVE_HPP diff --git a/src/cvcuda/include/cvcuda/Types.h b/src/cvcuda/include/cvcuda/Types.h index 360905a7e..3be5440e8 100644 --- a/src/cvcuda/include/cvcuda/Types.h +++ b/src/cvcuda/include/cvcuda/Types.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -28,6 +28,7 @@ #include "detail/Export.h" +#include #include #ifdef __cplusplus @@ -262,7 +263,6 @@ typedef enum NVCV_COLOR_YUV2GRAY_UYVY = 123, NVCV_COLOR_YUV2GRAY_YUY2 = 124, - //CV_YUV2GRAY_VYUY = CV_YUV2GRAY_UYVY, NVCV_COLOR_YUV2GRAY_Y422 = NVCV_COLOR_YUV2GRAY_UYVY, NVCV_COLOR_YUV2GRAY_UYNV = NVCV_COLOR_YUV2GRAY_UYVY, NVCV_COLOR_YUV2GRAY_YVYU = NVCV_COLOR_YUV2GRAY_YUY2, @@ -471,7 +471,7 @@ typedef struct NVCVColorRGBA fillColor; // Filled color of bounding box. } NVCVBndBoxI; -typedef void *NVCVBndBoxesI; +typedef struct NVCVBndBoxesIRec *NVCVBndBoxesI; typedef struct { @@ -479,7 +479,7 @@ typedef struct int32_t kernelSize; // Kernel sizes of mean filter, refer to cv::blur(). } NVCVBlurBoxI; -typedef void *NVCVBlurBoxesI; +typedef struct NVCVBlurBoxesIRec *NVCVBlurBoxesI; typedef struct { @@ -536,10 +536,62 @@ typedef enum HHMMSS = 3 } NVCVClockFormat; -typedef void *NVCVElements; +typedef struct NVCVElementsRec *NVCVElements; + +/** Construct an NVCVBndBoxesI from a flat array of bounding boxes. + * + * The handle owns its copy of the data; the input arrays may be freed after + * the call returns. Free the handle with nvcvBndBoxesIDestroy. + * + * @param [out] handle Where the new handle is written. Must not be NULL. + * @param [in] boxes Flat array of bounding boxes, length == sum(numBoxesPerBatch[]). + * @param [in] numBoxesPerBatch Per-batch box counts, length == batchSize. + * @param [in] batchSize Number of batches. Must be >= 0. + * + * @retval #NVCV_ERROR_INVALID_ARGUMENT One of the pointers is NULL or batchSize < 0. + * @retval #NVCV_ERROR_OUT_OF_MEMORY Allocation failed. + * @retval #NVCV_SUCCESS Operation executed successfully. + */ +CVCUDA_PUBLIC NVCVStatus nvcvBndBoxesIConstruct(NVCVBndBoxesI *handle, const NVCVBndBoxI *boxes, + const int32_t *numBoxesPerBatch, int32_t batchSize); + +/** Destroy a handle previously returned by nvcvBndBoxesIConstruct. Passing NULL is a no-op. */ +CVCUDA_PUBLIC NVCVStatus nvcvBndBoxesIDestroy(NVCVBndBoxesI handle); + +/** Construct an NVCVBlurBoxesI from a flat array of blur boxes. See nvcvBndBoxesIConstruct. */ +CVCUDA_PUBLIC NVCVStatus nvcvBlurBoxesIConstruct(NVCVBlurBoxesI *handle, const NVCVBlurBoxI *boxes, + const int32_t *numBoxesPerBatch, int32_t batchSize); + +/** Destroy a handle previously returned by nvcvBlurBoxesIConstruct. Passing NULL is a no-op. */ +CVCUDA_PUBLIC NVCVStatus nvcvBlurBoxesIDestroy(NVCVBlurBoxesI handle); + +/** Construct an NVCVElements from parallel flat arrays of OSD types and payload pointers. + * + * Element i in the flat arrays is described by types[i] and payloads[i]. Elements are + * grouped into batches according to numElementsPerBatch (length batchSize). + * The handle owns its copy of the payloads; input arrays may be freed after the call. + * Free the handle with nvcvElementsDestroy. + * + * Only POD OSD element types are supported here: NVCV_OSD_RECT, NVCV_OSD_POINT, + * NVCV_OSD_LINE, NVCV_OSD_ROTATED_RECT, NVCV_OSD_CIRCLE, NVCV_OSD_ARROW. + * + * @param [out] handle Where the new handle is written. Must not be NULL. + * @param [in] types Flat array of OSD types, length == sum(numElementsPerBatch[]). + * @param [in] payloads Flat array of payload pointers, parallel to types[]. + * @param [in] numElementsPerBatch Per-batch element counts, length == batchSize. + * @param [in] batchSize Number of batches. Must be >= 0. + */ +CVCUDA_PUBLIC NVCVStatus nvcvElementsConstruct(NVCVElements *handle, const NVCVOSDType *types, + const void *const *payloads, const int32_t *numElementsPerBatch, + int32_t batchSize); + +/** Destroy a handle previously returned by nvcvElementsConstruct. Passing NULL is a no-op. */ +CVCUDA_PUBLIC NVCVStatus nvcvElementsDestroy(NVCVElements handle); #ifdef __cplusplus } #endif +/** @} */ + #endif /* CVCUDAERATORS_TYPES_H */ diff --git a/src/cvcuda/include/cvcuda/Workspace.hpp b/src/cvcuda/include/cvcuda/Workspace.hpp index 6e576e26d..be559694b 100644 --- a/src/cvcuda/include/cvcuda/Workspace.hpp +++ b/src/cvcuda/include/cvcuda/Workspace.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,6 +25,7 @@ #include #include +#include #include namespace cvcuda { @@ -34,6 +35,12 @@ using WorkspaceMem = NVCVWorkspaceMem; using WorkspaceRequirements = NVCVWorkspaceRequirements; using WorkspaceMemRequirements = NVCVWorkspaceMemRequirements; +class WorkspaceError : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + /** Computes memory requirements that can cover both input requirements. * * The resulting memory requriements will have alignment and size that is not smaller than that of either @@ -71,6 +78,27 @@ inline void AlignUp(WorkspaceRequirements &ws) ws.cudaMem.size = nvcv::detail::AlignUp(ws.cudaMem.size, ws.cudaMem.alignment); } +inline void SynchronizeWorkspaceMem(const WorkspaceMem &mem) +{ + if (mem.ready && cudaEventSynchronize(mem.ready) != cudaSuccess) + { + throw WorkspaceError("cudaEventSynchronize failed"); + } +} + +template +inline void FreeWorkspaceMem(WorkspaceMem &mem, Allocator alloc) +{ + if (!mem.data) + { + return; + } + + SynchronizeWorkspaceMem(mem); + alloc.free(mem.data, static_cast(mem.req.size), static_cast(mem.req.alignment)); + mem.data = nullptr; +} + /** A helper class that manages the lifetime of resources stored in a Workspace structure. * * This class works in a way similar to unique_ptr with a custom deleter. @@ -85,7 +113,7 @@ class UniqueWorkspace UniqueWorkspace(const UniqueWorkspace &) = delete; - UniqueWorkspace(UniqueWorkspace &&ws) + UniqueWorkspace(UniqueWorkspace &&ws) noexcept { swap(ws); } @@ -95,28 +123,39 @@ class UniqueWorkspace UniqueWorkspace &operator=(UniqueWorkspace &&ws) noexcept { swap(ws); - ws.reset(); + ws.resetNoThrow(); return *this; } - UniqueWorkspace(Workspace workspace, Deleter del = {}) + explicit UniqueWorkspace(const Workspace &workspace, Deleter del = {}) : m_impl(workspace) , m_del(std::move(del)) { } - UniqueWorkspace(WorkspaceMem host, WorkspaceMem pinned, WorkspaceMem cuda, Deleter del = {}) + UniqueWorkspace(const WorkspaceMem &host, const WorkspaceMem &pinned, const WorkspaceMem &cuda, Deleter del = {}) : m_impl{host, pinned, cuda} , m_del(std::move(del)) { } - ~UniqueWorkspace() + ~UniqueWorkspace() noexcept { - reset(); + resetNoThrow(); } void reset() noexcept + { + resetNoThrow(); + } + + const Workspace &get() const + { + return m_impl; + } + +private: + void resetImpl() { if (m_del) { @@ -126,13 +165,20 @@ class UniqueWorkspace } } - const Workspace &get() const + void resetNoThrow() noexcept { - return m_impl; + try + { + resetImpl(); + } + catch (...) + { + m_del = {}; + m_impl = {}; + } } -private: - void swap(UniqueWorkspace &ws) + void swap(UniqueWorkspace &ws) noexcept { std::swap(m_impl, ws.m_impl); std::swap(m_del, ws.m_del); @@ -148,7 +194,7 @@ class UniqueWorkspace * may degrade performance due to excessive allocations and deallocations. * For code used in tight loops, some workspace reuse scheme and/or resource pools are recommended. */ -inline UniqueWorkspace AllocateWorkspace(WorkspaceRequirements req, nvcv::Allocator alloc = {}) +inline UniqueWorkspace AllocateWorkspace(const WorkspaceRequirements &req, nvcv::Allocator alloc = {}) { if (!alloc) { @@ -157,31 +203,10 @@ inline UniqueWorkspace AllocateWorkspace(WorkspaceRequirements req, nvcv::Alloca } auto del = [alloc](NVCVWorkspace &ws) { - // TODO(michalz): Add proper CUDA error handling in public API - if (ws.hostMem.data) - { - if (ws.hostMem.ready) - if (cudaEventSynchronize(ws.hostMem.ready) != cudaSuccess) - throw std::runtime_error("cudaEventSynchronize failed"); - alloc.hostMem().free(ws.hostMem.data, ws.hostMem.req.size, ws.hostMem.req.alignment); - ws.hostMem.data = nullptr; - } - if (ws.pinnedMem.data) - { - if (ws.pinnedMem.ready) - if (cudaEventSynchronize(ws.pinnedMem.ready) != cudaSuccess) - throw std::runtime_error("cudaEventSynchronize failed"); - alloc.hostPinnedMem().free(ws.pinnedMem.data, ws.pinnedMem.req.size, ws.pinnedMem.req.alignment); - ws.pinnedMem.data = nullptr; - } - if (ws.cudaMem.data) - { - if (ws.cudaMem.ready) - if (cudaEventSynchronize(ws.cudaMem.ready) != cudaSuccess) - throw std::runtime_error("cudaEventSynchronize failed"); - alloc.cudaMem().free(ws.cudaMem.data, ws.cudaMem.req.size, ws.cudaMem.req.alignment); - ws.cudaMem.data = nullptr; - } + // REVISIT(michalz): Add proper CUDA error handling in public API + FreeWorkspaceMem(ws.hostMem, alloc.hostMem()); + FreeWorkspaceMem(ws.pinnedMem, alloc.hostPinnedMem()); + FreeWorkspaceMem(ws.cudaMem, alloc.cudaMem()); }; NVCVWorkspace ws = {}; try @@ -191,11 +216,14 @@ inline UniqueWorkspace AllocateWorkspace(WorkspaceRequirements req, nvcv::Alloca ws.cudaMem.req = req.cudaMem; if (req.hostMem.size) - ws.hostMem.data = alloc.hostMem().alloc(req.hostMem.size, req.hostMem.alignment); + ws.hostMem.data = alloc.hostMem().alloc(static_cast(req.hostMem.size), + static_cast(req.hostMem.alignment)); if (req.pinnedMem.size) - ws.pinnedMem.data = alloc.hostPinnedMem().alloc(req.pinnedMem.size, req.pinnedMem.alignment); + ws.pinnedMem.data = alloc.hostPinnedMem().alloc(static_cast(req.pinnedMem.size), + static_cast(req.pinnedMem.alignment)); if (req.cudaMem.size) - ws.cudaMem.data = alloc.cudaMem().alloc(req.cudaMem.size, req.cudaMem.alignment); + ws.cudaMem.data = alloc.cudaMem().alloc(static_cast(req.cudaMem.size), + static_cast(req.cudaMem.alignment)); return UniqueWorkspace(ws, std::move(del)); } catch (...) diff --git a/src/cvcuda/include/cvcuda/cuda_tools/ArrayWrap.hpp b/src/cvcuda/include/cvcuda/cuda_tools/ArrayWrap.hpp index 2a6103d8c..e2c6a2a99 100644 --- a/src/cvcuda/include/cvcuda/cuda_tools/ArrayWrap.hpp +++ b/src/cvcuda/include/cvcuda/cuda_tools/ArrayWrap.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -55,7 +55,7 @@ class ArrayWrap assert(length > 0 && stride >= static_cast(sizeof(value_type))); } - __host__ ArrayWrap(const ArrayData &data) + explicit __host__ ArrayWrap(const ArrayData &data) : m_data{reinterpret_cast(data.basePtr())} , m_length{static_cast(data.capacity())} , m_stride{static_cast(data.stride())} @@ -82,7 +82,7 @@ class ArrayWrap return doGetPtr(c); } - inline __host__ __device__ operator pointer() const + explicit inline __host__ __device__ operator pointer() const { return reinterpret_cast(m_data); } @@ -100,7 +100,7 @@ class ArrayWrap private: std::byte *m_data{nullptr}; size_type m_length{0}; - size_type m_stride; + size_type m_stride{0}; }; } // namespace nvcv::cuda diff --git a/src/cvcuda/include/cvcuda/cuda_tools/Atomics.hpp b/src/cvcuda/include/cvcuda/cuda_tools/Atomics.hpp index 311639c72..cb30ba2a5 100644 --- a/src/cvcuda/include/cvcuda/cuda_tools/Atomics.hpp +++ b/src/cvcuda/include/cvcuda/cuda_tools/Atomics.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -44,13 +44,16 @@ namespace nvcv::cuda { * @param[in] val Second value to be used. * @param[in] op Operation to be used. */ -template>> +template __device__ void AtomicOp(T *address, T val, OP op) { + static_assert(std::is_floating_point_v, "AtomicOp requires a floating-point type"); + using UT = typename std::conditional_t; - UT *intAddress = reinterpret_cast(address); - UT assumed, old = *intAddress; + auto *intAddress = reinterpret_cast(address); + UT assumed; + UT old = *intAddress; do { diff --git a/src/cvcuda/include/cvcuda/cuda_tools/BorderVarShapeWrap.hpp b/src/cvcuda/include/cvcuda/cuda_tools/BorderVarShapeWrap.hpp index d7942c83f..d254ff2f2 100644 --- a/src/cvcuda/include/cvcuda/cuda_tools/BorderVarShapeWrap.hpp +++ b/src/cvcuda/include/cvcuda/cuda_tools/BorderVarShapeWrap.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -44,7 +44,7 @@ class BorderIWImpl static constexpr int kNumDimensions = ImageBatchWrapper::kNumDimensions; static constexpr NVCVBorderType kBorderType = B; - static constexpr bool kActiveDimensions[] = {false, false, true, true}; + static constexpr bool kActiveDimensions[] = {false, false, true, true}; // NOSONAR: device metadata. static constexpr int kNumActiveDimensions = 2; BorderIWImpl() = default; diff --git a/src/cvcuda/include/cvcuda/cuda_tools/BorderWrap.hpp b/src/cvcuda/include/cvcuda/cuda_tools/BorderWrap.hpp index ce2a02820..6ef5f03ef 100644 --- a/src/cvcuda/include/cvcuda/cuda_tools/BorderWrap.hpp +++ b/src/cvcuda/include/cvcuda/cuda_tools/BorderWrap.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -32,6 +32,8 @@ #include // for TensorDataStridedCuda, etc. #include // for TensorDataAccessStridedImagePlanar, etc. +#include + namespace nvcv::cuda { /** @@ -51,7 +53,7 @@ namespace nvcv::cuda { * @return True if given coordinate is outside given size. */ template -constexpr inline bool __host__ __device__ IsOutside(T c, T s) +constexpr bool __host__ __device__ IsOutside(T c, T s) { if constexpr (Active) { @@ -60,6 +62,53 @@ constexpr inline bool __host__ __device__ IsOutside(T c, T s) return false; } +namespace detail { + +template +constexpr T __host__ __device__ ReplicateBorderIndex(T c, T s) +{ + if (c < 0) + { + return 0; + } + + if (c >= s) + { + return s - 1; + } + + return c; +} + +template +constexpr T __host__ __device__ WrapBorderIndex(T c, T s) +{ + c = c % s; + return (c < 0) ? c + s : c; +} + +template +constexpr T __host__ __device__ ReflectBorderIndex(T c, T s) +{ + T s2 = s * 2; + c = WrapBorderIndex(c, s2); + return s - 1 - (abs(2 * c + 1 - s2) >> 1); +} + +template +constexpr T __host__ __device__ Reflect101BorderIndex(T c, T s) +{ + if (s == 1) + { + return 0; + } + + c = WrapBorderIndex(c, 2 * s - 2); + return s - 1 - abs(s - 1 - c); +} + +} // namespace detail + /** * Function to get a border-aware index considering the range defined by given size. * @@ -73,7 +122,7 @@ constexpr inline bool __host__ __device__ IsOutside(T c, T s) * @param[in] s Size that defines the valid range [0, s). */ template -constexpr inline T __host__ __device__ GetIndexWithBorder(T c, T s) +constexpr T __host__ __device__ GetIndexWithBorder(T c, T s) { static_assert(B != NVCV_BORDER_CONSTANT, "GetIndexWithBorder cannot be used with NVCV_BORDER_CONSTANT"); @@ -83,41 +132,19 @@ constexpr inline T __host__ __device__ GetIndexWithBorder(T c, T s) if constexpr (B == NVCV_BORDER_REPLICATE) { - c = (c < 0) ? 0 : (c >= s ? s - 1 : c); + c = detail::ReplicateBorderIndex(c, s); } else if constexpr (B == NVCV_BORDER_WRAP) { - c = c % s; - if (c < 0) - { - c += s; - } + c = detail::WrapBorderIndex(c, s); } else if constexpr (B == NVCV_BORDER_REFLECT) { - T s2 = s * 2; - c = c % s2; - if (c < 0) - { - c += s2; - } - c = s - 1 - (abs(2 * c + 1 - s2) >> 1); + c = detail::ReflectBorderIndex(c, s); } else if constexpr (B == NVCV_BORDER_REFLECT101) { - if (s == 1) - { - c = 0; - } - else - { - c = c % (2 * s - 2); - if (c < 0) - { - c += 2 * s - 2; - } - c = s - 1 - abs(s - 1 - c); - } + c = detail::Reflect101BorderIndex(c, s); } assert(c >= 0 && c < s); @@ -143,15 +170,14 @@ class BorderWrapImpl static_assert(kNumDimensions == sizeof...(ActiveDimensions)); - static constexpr bool kActiveDimensions[] = {ActiveDimensions...}; + static constexpr bool kActiveDimensions[] = {ActiveDimensions...}; // NOSONAR: device metadata. static constexpr int kNumActiveDimensions = ((ActiveDimensions ? 1 : 0) + ...); struct ActiveMap { - int from[kNumDimensions]; + int from[kNumDimensions] = {}; // NOSONAR: constexpr device lookup table. constexpr ActiveMap() - : from() { int j = 0; for (int i = 0; i < kNumDimensions; ++i) @@ -171,7 +197,7 @@ class BorderWrapImpl template explicit __host__ __device__ BorderWrapImpl(TensorWrapper tensorWrap, Args... tensorShape) : m_tensorWrap(tensorWrap) - , m_tensorShape{std::forward(tensorShape)...} + , m_tensorShape{static_cast(tensorShape)...} { if constexpr (sizeof...(Args) == 0) { @@ -226,9 +252,9 @@ class BorderWrapImpl return ValueType{}; } -protected: +private: const TensorWrapper m_tensorWrap = {}; - StrideType m_tensorShape[kNumActiveDimensions] = {0}; + StrideType m_tensorShape[kNumActiveDimensions] = {0}; // NOSONAR: device storage exposed as pointer. }; } // namespace detail @@ -367,15 +393,15 @@ class BorderWrap : public detail::BorderWrapImpl template inline __host__ __device__ ValueType *ptr(Args... c) const { - return doGetPtr(std::index_sequence_for{}, std::forward(c)...); + return doGetPtr(std::index_sequence_for{}, c...); } private: template inline __host__ __device__ ValueType *doGetPtr(std::index_sequence, Args... c) const { - return Base::m_tensorWrap.ptr(GetIndexWithBorder( - static_cast(c), Base::m_tensorShape[kMap.from[Is]])...); + return Base::tensorWrap().ptr(GetIndexWithBorder( + static_cast(c), Base::tensorShape()[kMap.from[Is]])...); } }; @@ -509,18 +535,18 @@ class BorderWrap template inline __host__ __device__ ValueType *ptr(Args... c) const { - return doGetPtr(std::index_sequence_for{}, std::forward(c)...); + return doGetPtr(std::index_sequence_for{}, c...); } private: template inline __host__ __device__ ValueType *doGetPtr(std::index_sequence, Args... c) const { - if ((IsOutside(static_cast(c), Base::m_tensorShape[kMap.from[Is]]) || ...)) + if ((IsOutside(static_cast(c), Base::tensorShape()[kMap.from[Is]]) || ...)) { return nullptr; } - return Base::m_tensorWrap.ptr(c...); + return Base::tensorWrap().ptr(c...); } const ValueType m_borderValue = SetAll(0); diff --git a/src/cvcuda/include/cvcuda/cuda_tools/DropCast.hpp b/src/cvcuda/include/cvcuda/cuda_tools/DropCast.hpp index d2f7aa379..842490938 100644 --- a/src/cvcuda/include/cvcuda/cuda_tools/DropCast.hpp +++ b/src/cvcuda/include/cvcuda/cuda_tools/DropCast.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -24,7 +24,7 @@ #ifndef NVCV_CUDA_DROP_CAST_HPP #define NVCV_CUDA_DROP_CAST_HPP -#include "TypeTraits.hpp" // for Require, etc. +#include "TypeTraits.hpp" namespace nvcv::cuda { @@ -52,9 +52,11 @@ namespace nvcv::cuda { * * @return The compound value with N components dropping the last, extra components. */ -template>> +template __host__ __device__ auto DropCast(T v) { + static_assert(HasEnoughComponents, "DropCast requires a compound type with at least N components"); + using RT = MakeType, N>; if constexpr (std::is_same_v) { diff --git a/src/cvcuda/include/cvcuda/cuda_tools/FullTensorWrap.hpp b/src/cvcuda/include/cvcuda/cuda_tools/FullTensorWrap.hpp index 13e19690d..1e212ba93 100644 --- a/src/cvcuda/include/cvcuda/cuda_tools/FullTensorWrap.hpp +++ b/src/cvcuda/include/cvcuda/cuda_tools/FullTensorWrap.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -28,6 +28,7 @@ #include // for TensorDataStridedCuda, etc. +#include #include namespace nvcv::cuda { @@ -99,18 +100,17 @@ class FullTensorWrap * * @param[in] tensor Tensor reference to the tensor that will be wrapped. */ - __host__ FullTensorWrap(const TensorDataStridedCuda &tensor) + explicit __host__ FullTensorWrap(const TensorDataStridedCuda &tensor) + : m_data(reinterpret_cast(tensor.basePtr())) { - m_data = reinterpret_cast(tensor.basePtr()); - #pragma unroll for (int i = 0; i < kNumDimensions; ++i) { assert(tensor.stride(i) <= TypeTraits::max); assert(tensor.shape(i) <= TypeTraits::max); - m_strides[i] = tensor.stride(i); - m_shapes[i] = tensor.shape(i); + m_strides[i] = static_cast(tensor.stride(i)); + m_shapes[i] = static_cast(tensor.shape(i)); } } @@ -184,12 +184,12 @@ class FullTensorWrap protected: template - inline const __host__ __device__ T *doGetPtr(Args... c) const + inline __host__ __device__ int doGetOffset(Args... c) const { static_assert(std::conjunction_v...>); static_assert(sizeof...(Args) <= kNumDimensions); - int coords[] = {std::forward(c)...}; + int coords[] = {static_cast(c)...}; // NOSONAR: CUDA code indexes parameter-pack values. // Computing offset first potentially postpones or avoids 64-bit math during addressing int offset = 0; @@ -199,13 +199,19 @@ class FullTensorWrap offset += coords[i] * m_strides[i]; } - return reinterpret_cast(m_data + offset); + return offset; + } + + template + inline const __host__ __device__ T *doGetPtr(Args... c) const + { + return reinterpret_cast(m_data + doGetOffset(c...)); } private: const std::byte *m_data = nullptr; - int m_strides[kNumDimensions] = {}; - int m_shapes[kNumDimensions] = {}; + int m_strides[kNumDimensions] = {}; // NOSONAR: device storage exposed as pointer. + int m_shapes[kNumDimensions] = {}; // NOSONAR: device storage exposed as pointer. }; /** @@ -241,6 +247,7 @@ class FullTensorWrap : public FullTensorWrap template explicit __host__ __device__ FullTensorWrap(DataType *data, const int (&strides)[N], const int (&shapes)[N]) : Base(data, strides, shapes) + , m_data(reinterpret_cast(data)) { } @@ -249,8 +256,9 @@ class FullTensorWrap : public FullTensorWrap * * @param[in] tensor Tensor reference to the tensor that will be wrapped. */ - __host__ FullTensorWrap(const TensorDataStridedCuda &tensor) + explicit __host__ FullTensorWrap(const TensorDataStridedCuda &tensor) : Base(tensor) + , m_data(reinterpret_cast(tensor.basePtr())) { } @@ -312,8 +320,11 @@ class FullTensorWrap : public FullTensorWrap template inline __host__ __device__ T *doGetPtr(Args... c) const { - return const_cast(Base::doGetPtr(c...)); + return reinterpret_cast(m_data + Base::doGetOffset(c...)); } + +private: + std::byte *m_data = nullptr; }; /**@}*/ diff --git a/src/cvcuda/include/cvcuda/cuda_tools/ImageBatchVarShapeWrap.hpp b/src/cvcuda/include/cvcuda/cuda_tools/ImageBatchVarShapeWrap.hpp index bbd23775f..57311546c 100644 --- a/src/cvcuda/include/cvcuda/cuda_tools/ImageBatchVarShapeWrap.hpp +++ b/src/cvcuda/include/cvcuda/cuda_tools/ImageBatchVarShapeWrap.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -28,6 +28,8 @@ #include // for ImageBatchVarShapeDataStridedCuda, etc. +#include + namespace nvcv::cuda { /** @@ -80,7 +82,7 @@ class ImageBatchVarShapeWrap * * @param[in] images Reference to the list of images that will be wrapped. */ - __host__ ImageBatchVarShapeWrap(const ImageBatchVarShapeDataStridedCuda &images) + explicit __host__ ImageBatchVarShapeWrap(const ImageBatchVarShapeDataStridedCuda &images) : m_imageList(images.imageList()) { } @@ -93,7 +95,7 @@ class ImageBatchVarShapeWrap * * @return The plane of the given image sample in batch. */ - inline const __host__ __device__ NVCVImagePlaneStrided plane(int s, int p = 0) const + inline __host__ __device__ NVCVImagePlaneStrided plane(int s, int p = 0) const { return m_imageList[s].planes[p]; } @@ -241,7 +243,7 @@ class ImageBatchVarShapeWrap : public ImageBatchVarShapeWrap * * @param[in] images Reference to the list of images that will be wrapped. */ - __host__ ImageBatchVarShapeWrap(const ImageBatchVarShapeDataStridedCuda &images) + explicit __host__ ImageBatchVarShapeWrap(const ImageBatchVarShapeDataStridedCuda &images) : Base(images) { } @@ -337,7 +339,7 @@ class ImageBatchVarShapeWrap : public ImageBatchVarShapeWrap * @tparam T Type (it can be const) of each element inside this wrapper. */ template -class ImageBatchVarShapeWrapNHWC : ImageBatchVarShapeWrap +class ImageBatchVarShapeWrapNHWC : private ImageBatchVarShapeWrap { using Base = ImageBatchVarShapeWrap; diff --git a/src/cvcuda/include/cvcuda/cuda_tools/InterpolationVarShapeWrap.hpp b/src/cvcuda/include/cvcuda/cuda_tools/InterpolationVarShapeWrap.hpp index e44c5e76c..1b2afab8a 100644 --- a/src/cvcuda/include/cvcuda/cuda_tools/InterpolationVarShapeWrap.hpp +++ b/src/cvcuda/include/cvcuda/cuda_tools/InterpolationVarShapeWrap.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -317,9 +317,12 @@ class InterpolationVarShapeWrap std::is_same_v, float> && (NumElements == 3 || NumElements == 4)>> inline __host__ __device__ ValueType operator[](DimType c) const { - const int x1 = GetIndexForInterpolation(c.x); + // IndexType must match the storage width (`int`): letting + // GetIndexForInterpolation return the default int64 would bypass its + // overflow-prevention clamp and re-expose x1 + 1 to int32 overflow. + const int x1 = GetIndexForInterpolation(c.x); const int x2 = x1 + 1; - const int y1 = GetIndexForInterpolation(c.y); + const int y1 = GetIndexForInterpolation(c.y); const int y2 = y1 + 1; auto out = SetAll>>(0); @@ -421,15 +424,16 @@ class InterpolationVarShapeWrap std::is_same_v, float> && (NumElements == 3 || NumElements == 4)>> inline __host__ __device__ ValueType operator[](DimType c) const { - const int ix = GetIndexForInterpolation(c.x); - const int iy = GetIndexForInterpolation(c.y); + // Explicit int IndexType — see note at LINEAR above. + const int ix = GetIndexForInterpolation(c.x); + const int iy = GetIndexForInterpolation(c.y); using FT = ConvertBaseTypeTo>; auto sum = SetAll(0); - float wx[4]; + float wx[4]; // NOSONAR: CUDA cubic coefficients are indexed in the unrolled loop. GetCubicCoeffs(c.x - ix, wx[0], wx[1], wx[2], wx[3]); - float wy[4]; + float wy[4]; // NOSONAR: CUDA cubic coefficients are indexed in the unrolled loop. GetCubicCoeffs(c.y - iy, wy[0], wy[1], wy[2], wy[3]); #pragma unroll @@ -551,108 +555,200 @@ class InterpolationVarShapeWrap class = Require< std::is_same_v, float> && (NumElements == 3 || NumElements == 4)>> inline __host__ __device__ ValueType operator[](DimType c) const + { + const AreaBounds bounds = MakeAreaBounds(c); + + auto out = SetAll>>(0); + + if (m_isIntegerArea) + { + AddIntegerArea(c, bounds, out); + } + else + { + AddFractionalArea(c, bounds, out); + } + + return SaturateCast(out); + } + +private: + struct AreaBounds + { + float fsx1; + float fsy1; + float fsx2; + float fsy2; + int xmin; + int xmax; + int ymin; + int ymax; + }; + + template + inline __host__ __device__ AreaBounds MakeAreaBounds(DimType c) const { const float fsx1 = c.x * m_scaleX; const float fsy1 = c.y * m_scaleY; const float fsx2 = fsx1 + m_scaleX; const float fsy2 = fsy1 + m_scaleY; - const int xmin = GetIndexForInterpolation(fsx1); - const int xmax = GetIndexForInterpolation(fsx2); - const int ymin = GetIndexForInterpolation(fsy1); - const int ymax = GetIndexForInterpolation(fsy2); - auto out = SetAll>>(0); + // Explicit int IndexType — see note at LINEAR above. + return {fsx1, + fsy1, + fsx2, + fsy2, + GetIndexForInterpolation(fsx1), + GetIndexForInterpolation(fsx2), + GetIndexForInterpolation(fsy1), + GetIndexForInterpolation(fsy2)}; + } - if (m_isIntegerArea) - { - const float scale = 1.f / (m_scaleX * m_scaleY); + template + inline __host__ __device__ void AddIntegerArea(DimType c, const AreaBounds &bounds, AccumType &out) const + { + const float scale = 1.f / (m_scaleX * m_scaleY); - for (int cy = ymin; cy < ymax; ++cy) + for (int cy = bounds.ymin; cy < bounds.ymax; ++cy) + { + for (int cx = bounds.xmin; cx < bounds.xmax; ++cx) { - for (int cx = xmin; cx < xmax; ++cx) - { - out += Base::doGetValue(c, cx, cy) * scale; - } + out += Base::doGetValue(c, cx, cy) * scale; } } - else + } + + template + inline __host__ __device__ void AddFractionalArea(DimType c, const AreaBounds &bounds, AccumType &out) const + { + const float scale = FractionalAreaScale(c, bounds); + + AddInteriorRows(c, bounds, scale, out); + + if (bounds.ymin > bounds.fsy1) { - int w, h; + AddTopEdgeRow(c, bounds, scale, out); + } - if constexpr (NumElements == 3) - w = Base::m_borderWrap.imageBatchWrap().width(static_cast(c.z)); - else - w = Base::m_borderWrap.imageBatchWrap().width(static_cast(c.w), static_cast(c.z)); + if (bounds.ymax < bounds.fsy2) + { + AddBottomEdgeRow(c, bounds, scale, out); + } + } - if constexpr (NumElements == 3) - h = Base::m_borderWrap.imageBatchWrap().height(static_cast(c.z)); - else - h = Base::m_borderWrap.imageBatchWrap().height(static_cast(c.w), static_cast(c.z)); + template + inline __host__ __device__ float FractionalAreaScale(DimType c, const AreaBounds &bounds) const + { + const int w = ImageWidth(c); + const int h = ImageHeight(c); - const float scale = 1.f / (min(m_scaleX, w - fsx1) * min(m_scaleY, h - fsy1)); + return 1.f / (min(m_scaleX, w - bounds.fsx1) * min(m_scaleY, h - bounds.fsy1)); + } - for (int cy = ymin; cy < ymax; ++cy) - { - for (int cx = xmin; cx < xmax; ++cx) - { - out += Base::doGetValue(c, cx, cy) * scale; - } - - if (xmin > fsx1) - { - out += Base::doGetValue(c, (xmin - 1), cy) * ((xmin - fsx1) * scale); - } - - if (xmax < fsx2) - { - out += Base::doGetValue(c, xmax, cy) * ((fsx2 - xmax) * scale); - } - } + template + inline __host__ __device__ int ImageWidth(DimType c) const + { + if constexpr (NumElements == 3) + { + return Base::m_borderWrap.imageBatchWrap().width(static_cast(c.z)); + } + else + { + return Base::m_borderWrap.imageBatchWrap().width(static_cast(c.w), static_cast(c.z)); + } + } - if (ymin > fsy1) - { - for (int cx = xmin; cx < xmax; ++cx) - { - out += Base::doGetValue(c, cx, (ymin - 1)) * ((ymin - fsy1) * scale); - } - - if (xmin > fsx1) - { - out += Base::doGetValue(c, (xmin - 1), (ymin - 1)) * ((ymin - fsy1) * (xmin - fsx1) * scale); - } - - if (xmax < fsx2) - { - out += Base::doGetValue(c, xmax, (ymin - 1)) * ((ymin - fsy1) * (fsx2 - xmax) * scale); - } - } + template + inline __host__ __device__ int ImageHeight(DimType c) const + { + if constexpr (NumElements == 3) + { + return Base::m_borderWrap.imageBatchWrap().height(static_cast(c.z)); + } + else + { + return Base::m_borderWrap.imageBatchWrap().height(static_cast(c.w), static_cast(c.z)); + } + } - if (ymax < fsy2) + template + inline __host__ __device__ void AddInteriorRows(DimType c, const AreaBounds &bounds, float scale, + AccumType &out) const + { + for (int cy = bounds.ymin; cy < bounds.ymax; ++cy) + { + for (int cx = bounds.xmin; cx < bounds.xmax; ++cx) { - for (int cx = xmin; cx < xmax; ++cx) - { - out += Base::doGetValue(c, cx, ymax) * ((fsy2 - ymax) * scale); - } - - if (xmax < fsx2) - { - out += Base::doGetValue(c, xmax, ymax) * ((fsy2 - ymax) * (fsx2 - xmax) * scale); - } - - if (xmin > fsx1) - { - out += Base::doGetValue(c, (xmin - 1), ymax) * ((fsy2 - ymax) * (xmin - fsx1) * scale); - } + out += Base::doGetValue(c, cx, cy) * scale; } + + AddHorizontalEdgeCells(c, bounds, cy, scale, out); } + } - return SaturateCast(out); + template + inline __host__ __device__ void AddHorizontalEdgeCells(DimType c, const AreaBounds &bounds, int cy, float scale, + AccumType &out) const + { + if (bounds.xmin > bounds.fsx1) + { + out += Base::doGetValue(c, (bounds.xmin - 1), cy) * ((bounds.xmin - bounds.fsx1) * scale); + } + + if (bounds.xmax < bounds.fsx2) + { + out += Base::doGetValue(c, bounds.xmax, cy) * ((bounds.fsx2 - bounds.xmax) * scale); + } + } + + template + inline __host__ __device__ void AddTopEdgeRow(DimType c, const AreaBounds &bounds, float scale, + AccumType &out) const + { + for (int cx = bounds.xmin; cx < bounds.xmax; ++cx) + { + out += Base::doGetValue(c, cx, (bounds.ymin - 1)) * ((bounds.ymin - bounds.fsy1) * scale); + } + + if (bounds.xmin > bounds.fsx1) + { + out += Base::doGetValue(c, (bounds.xmin - 1), (bounds.ymin - 1)) + * ((bounds.ymin - bounds.fsy1) * (bounds.xmin - bounds.fsx1) * scale); + } + + if (bounds.xmax < bounds.fsx2) + { + out += Base::doGetValue(c, bounds.xmax, (bounds.ymin - 1)) + * ((bounds.ymin - bounds.fsy1) * (bounds.fsx2 - bounds.xmax) * scale); + } + } + + template + inline __host__ __device__ void AddBottomEdgeRow(DimType c, const AreaBounds &bounds, float scale, + AccumType &out) const + { + for (int cx = bounds.xmin; cx < bounds.xmax; ++cx) + { + out += Base::doGetValue(c, cx, bounds.ymax) * ((bounds.fsy2 - bounds.ymax) * scale); + } + + if (bounds.xmax < bounds.fsx2) + { + out += Base::doGetValue(c, bounds.xmax, bounds.ymax) + * ((bounds.fsy2 - bounds.ymax) * (bounds.fsx2 - bounds.xmax) * scale); + } + + if (bounds.xmin > bounds.fsx1) + { + out += Base::doGetValue(c, (bounds.xmin - 1), bounds.ymax) + * ((bounds.fsy2 - bounds.ymax) * (bounds.xmin - bounds.fsx1) * scale); + } } -private: inline __host__ __device__ bool isIntegerArea(float scaleX, float scaleY) const { - return cuda::round(scaleX) == scaleX && cuda::round(scaleY) == scaleY; + return static_cast(cuda::round(scaleX)) == scaleX + && static_cast(cuda::round(scaleY)) == scaleY; } const float m_scaleX = {}; diff --git a/src/cvcuda/include/cvcuda/cuda_tools/InterpolationWrap.hpp b/src/cvcuda/include/cvcuda/cuda_tools/InterpolationWrap.hpp index bb8236d5e..4c8c8af8c 100644 --- a/src/cvcuda/include/cvcuda/cuda_tools/InterpolationWrap.hpp +++ b/src/cvcuda/include/cvcuda/cuda_tools/InterpolationWrap.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -41,22 +41,48 @@ namespace nvcv::cuda { /** * Function to get an integer index from a float coordinate for interpolation purpose. * + * @note When \p IndexType is an integer type, the input coordinate is first clamped to + * `[TypeTraits::min / 4, TypeTraits::max / 4]` to guarantee that + * common downstream arithmetic performed by callers stays within \p IndexType's range: + * `x + 1`, `ix + cx` (CUBIC, `cx` in `[-1, 2]`), `xmin - 1`, loop `++cx`, as well as + * \ref GetIndexWithBorder's `2*c + 1 - s2` (REFLECT) and `c % (2*s - 2)` (REFLECT101). + * Without this clamp, an ill-conditioned transform producing `|c| >= IndexType::max` + * would let `__float2{int,ll}_rd` saturate at `IndexType::max`; the subsequent `x + 1` + * signed-overflows, and the resulting undefined behaviour lets the compiler elide the + * bounds check in \ref GetIndexWithBorder for REPLICATE borders, leading to a + * `cudaErrorIllegalAddress` when the wild index is dereferenced by \ref TensorWrap::ptr + * (CVCUDA issue #249). For any realistic pixel offset the clamp is a no-op; only + * pathological coordinates (orders of magnitude outside any real tensor) are clipped, + * in which case the returned index is well-defined but not mathematically exact. + * * @tparam I Interpolation type, one of \ref NVCVInterpolationType. * @tparam Position Interpolation position, 1 for the first index and 2 for the second index. - * @tparam IndexType Type of the returned value + * @tparam IndexType Type of the returned value. Defaults to `int64_t`; callers storing + * the result in a narrower integer type (e.g. `int32_t`) should pass + * that type here so the overflow-prevention clamp targets the final + * storage width. * * @param[in] c Coordinate in floating-point to convert to index in integer. * * @return Index in integer suitable for interpolation computation. */ template -constexpr inline IndexType __host__ __device__ GetIndexForInterpolation(float c) +constexpr IndexType __host__ __device__ GetIndexForInterpolation(float c) { static_assert( I == NVCV_INTERP_NEAREST || I == NVCV_INTERP_LINEAR || I == NVCV_INTERP_CUBIC || I == NVCV_INTERP_AREA, "GetIndexForInterpolation accepts only NVCV_INTERP_{NEAREST, LINEAR, CUBIC, AREA}"); static_assert(Position == 1 || Position == 2, "GetIndexForInterpolation accepts only position 1 or 2"); + // Overflow-prevention clamp for downstream integer arithmetic; see the @note above + // for the rationale. + if constexpr (std::is_integral_v) + { + constexpr auto kSafeMax = static_cast(TypeTraits::max / 4); + constexpr auto kSafeMin = static_cast(TypeTraits::min / 4); + c = cuda::max(cuda::min(c, kSafeMax), kSafeMin); + } + if constexpr (I == NVCV_INTERP_NEAREST) { return cuda::round(c); @@ -123,10 +149,9 @@ class InterpolationWrapImpl struct ActiveCoordMap { - int id[kNumDimensions]; + int id[kNumDimensions] = {}; // NOSONAR: constexpr device lookup table. constexpr ActiveCoordMap() - : id() { int dimCoord = 0; for (int dim = kNumDimensions - 1, idCoord = 0; dim >= 0; dim--, idCoord++) @@ -192,8 +217,8 @@ class InterpolationWrapImpl inline const __host__ __device__ ValueType &doGetValue(DimType c, StrideType x, StrideType y) const { cuda::ConvertBaseTypeTo ic; - GetElement(ic) = x; - GetElement(ic) = y; + GetElement(ic) = static_cast(x); + GetElement(ic) = static_cast(y); if constexpr (NumElements >= 3) GetElement(ic) = static_cast(GetElement(c)); if constexpr (NumElements == 4) @@ -437,10 +462,10 @@ class InterpolationWrap : public detail::InterpolationWr auto out = SetAll>>(0); - out += Base::doGetValue(c, x1, y1) * (x2 - x) * (y2 - y); - out += Base::doGetValue(c, x2, y1) * (x - x1) * (y2 - y); - out += Base::doGetValue(c, x1, y2) * (x2 - x) * (y - y1); - out += Base::doGetValue(c, x2, y2) * (x - x1) * (y - y1); + out += Base::doGetValue(c, x1, y1) * (static_cast(x2) - x) * (static_cast(y2) - y); + out += Base::doGetValue(c, x2, y1) * (x - static_cast(x1)) * (static_cast(y2) - y); + out += Base::doGetValue(c, x1, y2) * (static_cast(x2) - x) * (y - static_cast(y1)); + out += Base::doGetValue(c, x2, y2) * (x - static_cast(x1)) * (y - static_cast(y1)); return SaturateCast(out); } @@ -539,10 +564,10 @@ class InterpolationWrap : public detail::InterpolationWra const StrideType ix = GetIndexForInterpolation(x); const StrideType iy = GetIndexForInterpolation(y); - float wx[4]; - GetCubicCoeffs(x - ix, wx[0], wx[1], wx[2], wx[3]); - float wy[4]; - GetCubicCoeffs(y - iy, wy[0], wy[1], wy[2], wy[3]); + float wx[4]; // NOSONAR: CUDA cubic coefficients are indexed in the unrolled loop. + GetCubicCoeffs(x - static_cast(ix), wx[0], wx[1], wx[2], wx[3]); + float wy[4]; // NOSONAR: CUDA cubic coefficients are indexed in the unrolled loop. + GetCubicCoeffs(y - static_cast(iy), wy[0], wy[1], wy[2], wy[3]); using FT = ConvertBaseTypeTo>; auto sum = SetAll(0); @@ -667,100 +692,201 @@ class InterpolationWrap : public detail::InterpolationWrap float> && 2 <= NumElements && NumElements <= kNumDimensions>> inline __host__ __device__ ValueType operator[](DimType c) const { - const float fsx1 = GetElement(c) * m_scaleX; - const float fsy1 = GetElement(c) * m_scaleY; - const float fsx2 = fsx1 + m_scaleX; - const float fsy2 = fsy1 + m_scaleY; - const StrideType xmin = GetIndexForInterpolation(fsx1); - const StrideType xmax = GetIndexForInterpolation(fsx2); - const StrideType ymin = GetIndexForInterpolation(fsy1); - const StrideType ymax = GetIndexForInterpolation(fsy2); + return interpolateWithBounds(c, computeAreaBounds(c)); + } + + // Area box geometry for an output coordinate. It depends only on the active spatial coordinate + // (x, y), not on any inactive channel/sample/batch dimension, so a caller resizing several planes + // at the same (x, y) can compute it once and reuse it across planes -- avoiding the per-plane + // recomputation that one operator[] call per plane would incur. + struct AreaBounds + { + float fsx1; + float fsy1; + float fsx2; + float fsy2; + StrideType xmin; + StrideType xmax; + StrideType ymin; + StrideType ymax; + }; + // Compute the reusable area box geometry for coordinate \p c (only its spatial elements matter). + template< + typename DimType, + class = Require, + float> && 2 <= NumElements && NumElements <= kNumDimensions>> + inline __host__ __device__ AreaBounds computeAreaBounds(DimType c) const + { + return MakeAreaBounds(c); + } + + // Interpolate at \p c using bounds from computeAreaBounds() for the SAME (x, y); \p c selects the + // plane/sample. operator[](c) is exactly interpolateWithBounds(c, computeAreaBounds(c)). + template< + typename DimType, + class = Require, + float> && 2 <= NumElements && NumElements <= kNumDimensions>> + inline __host__ __device__ ValueType interpolateWithBounds(DimType c, const AreaBounds &bounds) const + { auto out = SetAll>>(0); if (m_isIntegerArea) { - const float scale = 1.f / (m_scaleX * m_scaleY); + AddIntegerArea(c, bounds, out); + } + else + { + AddFractionalArea(c, bounds, out); + } + + return SaturateCast(out); + } - for (StrideType cy = ymin; cy < ymax; ++cy) +private: + template + inline __host__ __device__ AreaBounds MakeAreaBounds(DimType c) const + { + const float fsx1 = GetElement(c) * m_scaleX; + const float fsy1 = GetElement(c) * m_scaleY; + const float fsx2 = fsx1 + m_scaleX; + const float fsy2 = fsy1 + m_scaleY; + + return {fsx1, + fsy1, + fsx2, + fsy2, + GetIndexForInterpolation(fsx1), + GetIndexForInterpolation(fsx2), + GetIndexForInterpolation(fsy1), + GetIndexForInterpolation(fsy2)}; + } + + inline static __host__ __device__ float ToFloat(StrideType value) + { + return static_cast(value); + } + + template + inline __host__ __device__ void AddIntegerArea(DimType c, const AreaBounds &bounds, AccumType &out) const + { + const float scale = 1.f / (m_scaleX * m_scaleY); + + for (StrideType cy = bounds.ymin; cy < bounds.ymax; ++cy) + { + for (StrideType cx = bounds.xmin; cx < bounds.xmax; ++cx) { - for (StrideType cx = xmin; cx < xmax; ++cx) - { - out += Base::doGetValue(c, cx, cy) * scale; - } + out += Base::doGetValue(c, cx, cy) * scale; } } - else + } + + template + inline __host__ __device__ void AddFractionalArea(DimType c, const AreaBounds &bounds, AccumType &out) const + { + const float scale = FractionalAreaScale(bounds); + + AddInteriorRows(c, bounds, scale, out); + + if (ToFloat(bounds.ymin) > bounds.fsy1) { - // There are 2 active dimensions (0, 1) and the coordinates are inverted (y, x) - // so y corresponds to dimension 0 and x corresponds to dimension 1 - const StrideType w = Base::m_borderWrap.tensorShape()[1]; - const StrideType h = Base::m_borderWrap.tensorShape()[0]; + AddTopEdgeRow(c, bounds, scale, out); + } - const float scale = 1.f / (min(m_scaleX, w - fsx1) * min(m_scaleY, h - fsy1)); + if (ToFloat(bounds.ymax) < bounds.fsy2) + { + AddBottomEdgeRow(c, bounds, scale, out); + } + } - for (StrideType cy = ymin; cy < ymax; ++cy) - { - for (StrideType cx = xmin; cx < xmax; ++cx) - { - out += Base::doGetValue(c, cx, cy) * scale; - } + inline __host__ __device__ float FractionalAreaScale(const AreaBounds &bounds) const + { + // There are 2 active dimensions (0, 1) and the coordinates are inverted (y, x) + // so y corresponds to dimension 0 and x corresponds to dimension 1. + const float w = ToFloat(Base::m_borderWrap.tensorShape()[1]); + const float h = ToFloat(Base::m_borderWrap.tensorShape()[0]); - if (xmin > fsx1) - { - out += Base::doGetValue(c, (xmin - 1), cy) * ((xmin - fsx1) * scale); - } + return 1.f / (min(m_scaleX, w - bounds.fsx1) * min(m_scaleY, h - bounds.fsy1)); + } - if (xmax < fsx2) - { - out += Base::doGetValue(c, xmax, cy) * ((fsx2 - xmax) * scale); - } + template + inline __host__ __device__ void AddInteriorRows(DimType c, const AreaBounds &bounds, float scale, + AccumType &out) const + { + for (StrideType cy = bounds.ymin; cy < bounds.ymax; ++cy) + { + for (StrideType cx = bounds.xmin; cx < bounds.xmax; ++cx) + { + out += Base::doGetValue(c, cx, cy) * scale; } - if (ymin > fsy1) - { - for (StrideType cx = xmin; cx < xmax; ++cx) - { - out += Base::doGetValue(c, cx, (ymin - 1)) * ((ymin - fsy1) * scale); - } + AddHorizontalEdgeCells(c, bounds, cy, scale, out); + } + } - if (xmin > fsx1) - { - out += Base::doGetValue(c, (xmin - 1), (ymin - 1)) * ((ymin - fsy1) * (xmin - fsx1) * scale); - } + template + inline __host__ __device__ void AddHorizontalEdgeCells(DimType c, const AreaBounds &bounds, StrideType cy, + float scale, AccumType &out) const + { + if (ToFloat(bounds.xmin) > bounds.fsx1) + { + out += Base::doGetValue(c, (bounds.xmin - 1), cy) * ((ToFloat(bounds.xmin) - bounds.fsx1) * scale); + } - if (xmax < fsx2) - { - out += Base::doGetValue(c, xmax, (ymin - 1)) * ((ymin - fsy1) * (fsx2 - xmax) * scale); - } - } + if (ToFloat(bounds.xmax) < bounds.fsx2) + { + out += Base::doGetValue(c, bounds.xmax, cy) * ((bounds.fsx2 - ToFloat(bounds.xmax)) * scale); + } + } - if (ymax < fsy2) - { - for (StrideType cx = xmin; cx < xmax; ++cx) - { - out += Base::doGetValue(c, cx, ymax) * ((fsy2 - ymax) * scale); - } + template + inline __host__ __device__ void AddTopEdgeRow(DimType c, const AreaBounds &bounds, float scale, + AccumType &out) const + { + for (StrideType cx = bounds.xmin; cx < bounds.xmax; ++cx) + { + out += Base::doGetValue(c, cx, (bounds.ymin - 1)) * ((ToFloat(bounds.ymin) - bounds.fsy1) * scale); + } - if (xmax < fsx2) - { - out += Base::doGetValue(c, xmax, ymax) * ((fsy2 - ymax) * (fsx2 - xmax) * scale); - } + if (ToFloat(bounds.xmin) > bounds.fsx1) + { + out += Base::doGetValue(c, (bounds.xmin - 1), (bounds.ymin - 1)) + * ((ToFloat(bounds.ymin) - bounds.fsy1) * (ToFloat(bounds.xmin) - bounds.fsx1) * scale); + } - if (xmin > fsx1) - { - out += Base::doGetValue(c, (xmin - 1), ymax) * ((fsy2 - ymax) * (xmin - fsx1) * scale); - } - } + if (ToFloat(bounds.xmax) < bounds.fsx2) + { + out += Base::doGetValue(c, bounds.xmax, (bounds.ymin - 1)) + * ((ToFloat(bounds.ymin) - bounds.fsy1) * (bounds.fsx2 - ToFloat(bounds.xmax)) * scale); } + } - return SaturateCast(out); + template + inline __host__ __device__ void AddBottomEdgeRow(DimType c, const AreaBounds &bounds, float scale, + AccumType &out) const + { + for (StrideType cx = bounds.xmin; cx < bounds.xmax; ++cx) + { + out += Base::doGetValue(c, cx, bounds.ymax) * ((bounds.fsy2 - ToFloat(bounds.ymax)) * scale); + } + + if (ToFloat(bounds.xmax) < bounds.fsx2) + { + out += Base::doGetValue(c, bounds.xmax, bounds.ymax) + * ((bounds.fsy2 - ToFloat(bounds.ymax)) * (bounds.fsx2 - ToFloat(bounds.xmax)) * scale); + } + + if (ToFloat(bounds.xmin) > bounds.fsx1) + { + out += Base::doGetValue(c, (bounds.xmin - 1), bounds.ymax) + * ((bounds.fsy2 - ToFloat(bounds.ymax)) * (ToFloat(bounds.xmin) - bounds.fsx1) * scale); + } } -private: inline __host__ __device__ bool isIntegerArea(float scaleX, float scaleY) const { - return cuda::round(scaleX) == scaleX && cuda::round(scaleY) == scaleY; + return static_cast(cuda::round(scaleX)) == scaleX + && static_cast(cuda::round(scaleY)) == scaleY; } const float m_scaleX = {}; diff --git a/src/cvcuda/include/cvcuda/cuda_tools/MathOps.hpp b/src/cvcuda/include/cvcuda/cuda_tools/MathOps.hpp index 4506cf101..60bda0092 100644 --- a/src/cvcuda/include/cvcuda/cuda_tools/MathOps.hpp +++ b/src/cvcuda/include/cvcuda/cuda_tools/MathOps.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -176,16 +176,16 @@ template >= 1) - if (a.x != b.x) + if (a.x != b.x) // NOSONAR: comparison intentionally follows CUDA vector component types. return false; if constexpr (nvcv::cuda::NumElements >= 2) - if (a.y != b.y) + if (a.y != b.y) // NOSONAR: comparison intentionally follows CUDA vector component types. return false; if constexpr (nvcv::cuda::NumElements >= 3) - if (a.z != b.z) + if (a.z != b.z) // NOSONAR: comparison intentionally follows CUDA vector component types. return false; if constexpr (nvcv::cuda::NumElements == 4) - if (a.w != b.w) + if (a.w != b.w) // NOSONAR: comparison intentionally follows CUDA vector component types. return false; return true; } diff --git a/src/cvcuda/include/cvcuda/cuda_tools/RangeCast.hpp b/src/cvcuda/include/cvcuda/cuda_tools/RangeCast.hpp index 64e5b0e7d..47e980cc9 100644 --- a/src/cvcuda/include/cvcuda/cuda_tools/RangeCast.hpp +++ b/src/cvcuda/include/cvcuda/cuda_tools/RangeCast.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -24,7 +24,7 @@ * @brief Defines range cast functionality. */ -#include "TypeTraits.hpp" // for Require, etc. +#include "TypeTraits.hpp" #include "detail/RangeCastImpl.hpp" // for RangeCastImpl, etc. namespace nvcv::cuda { @@ -65,9 +65,11 @@ namespace nvcv::cuda { * * @return The value with all elements scaled. */ -template && !IsCompound>> +template __host__ __device__ auto RangeCast(U u) { + static_assert(HasTypeTraits && !IsCompound, "RangeCast requires a regular C target type"); + using RT = ConvertBaseTypeTo; if constexpr (std::is_same_v) { diff --git a/src/cvcuda/include/cvcuda/cuda_tools/SaturateCast.hpp b/src/cvcuda/include/cvcuda/cuda_tools/SaturateCast.hpp index 41840eb4e..395d76f04 100644 --- a/src/cvcuda/include/cvcuda/cuda_tools/SaturateCast.hpp +++ b/src/cvcuda/include/cvcuda/cuda_tools/SaturateCast.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -24,7 +24,7 @@ #ifndef NVCV_CUDA_SATURATE_CAST_HPP #define NVCV_CUDA_SATURATE_CAST_HPP -#include "TypeTraits.hpp" // for Require, etc. +#include "TypeTraits.hpp" #include "detail/SaturateCastImpl.hpp" // for SaturateCastImpl, etc. namespace nvcv::cuda { @@ -56,10 +56,12 @@ namespace nvcv::cuda { * * @return The value with all elements clamped and potentially rounded. */ -template == NumComponents) || (NumComponents == 0 && HasTypeTraits)>> +template __host__ __device__ auto SaturateCast(U u) { + static_assert((NumComponents == NumComponents) || (NumComponents == 0 && HasTypeTraits), + "SaturateCast requires matching component counts or a regular C target type"); + using BU = BaseType; using BT = BaseType; using RT = ConvertBaseTypeTo; diff --git a/src/cvcuda/include/cvcuda/cuda_tools/StaticCast.hpp b/src/cvcuda/include/cvcuda/cuda_tools/StaticCast.hpp index 5f81d14a4..bf97474c3 100644 --- a/src/cvcuda/include/cvcuda/cuda_tools/StaticCast.hpp +++ b/src/cvcuda/include/cvcuda/cuda_tools/StaticCast.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -24,7 +24,7 @@ #ifndef NVCV_CUDA_STATIC_CAST_HPP #define NVCV_CUDA_STATIC_CAST_HPP -#include "TypeTraits.hpp" // for Require, etc. +#include "TypeTraits.hpp" namespace nvcv::cuda { @@ -53,9 +53,11 @@ namespace nvcv::cuda { * * @return The compound value with all components static casted to type \p T. */ -template && !IsCompound>> +template __host__ __device__ auto StaticCast(U u) { + static_assert(HasTypeTraits && !IsCompound, "StaticCast requires a regular C target type"); + using RT = ConvertBaseTypeTo; if constexpr (std::is_same_v) { diff --git a/src/cvcuda/include/cvcuda/cuda_tools/TensorBatchWrap.hpp b/src/cvcuda/include/cvcuda/cuda_tools/TensorBatchWrap.hpp index 5076823e3..681bd535a 100644 --- a/src/cvcuda/include/cvcuda/cuda_tools/TensorBatchWrap.hpp +++ b/src/cvcuda/include/cvcuda/cuda_tools/TensorBatchWrap.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -29,6 +29,7 @@ #include +#include #include namespace nvcv::cuda { @@ -74,7 +75,7 @@ namespace nvcv::cuda { * @tparam T Type (it can be const) of each element inside the tensor wrapper. * @tparam Strides Each compile-time (use -1 for run-time) pitch in bytes from first to last dimension. */ -template +template // NOSONAR: StrideT is part of this public template API. class TensorBatchWrapT; template @@ -99,7 +100,7 @@ class TensorBatchWrapT * * @param[in] data Tensor batch data to wrap. */ - __host__ TensorBatchWrapT(const TensorBatchDataStridedCuda &data) + explicit __host__ TensorBatchWrapT(const TensorBatchDataStridedCuda &data) : TensorBatchWrapT(data.cdata()) { } @@ -109,7 +110,7 @@ class TensorBatchWrapT * * @param[in] data Tensor batch data to wrap. */ - __host__ __device__ TensorBatchWrapT(const NVCVTensorBatchData &data) + explicit __host__ __device__ TensorBatchWrapT(const NVCVTensorBatchData &data) : m_numTensors(data.numTensors) , m_tensors(data.buffer.strided.tensors) { @@ -170,7 +171,7 @@ class TensorBatchWrapT * @tparam Strides static strides * @param t index of the tensor */ - inline const __host__ __device__ auto tensor(int t) const + inline __host__ __device__ auto tensor(int t) const { return TensorWrapT(doGetPtr(t), strides(t)); } @@ -215,13 +216,13 @@ class TensorBatchWrapT constexpr int kArgSize = sizeof...(Args); constexpr int kVarSize = kArgSize < kVariableStrides ? kArgSize : kVariableStrides; constexpr int kDimSize = kArgSize < kNumDimensions ? kArgSize : kNumDimensions; - constexpr StrideT kStride[] = {std::forward(Strides)...}; + constexpr StrideT kStride[] = {Strides...}; // NOSONAR: CUDA code indexes template-pack values. // Computing offset first potentially postpones or avoids 64-bit math during addressing StrideType offset = 0; if constexpr (kArgSize > 0) { - StrideType coords[] = {std::forward(c)...}; + StrideType coords[] = {static_cast(c)...}; // NOSONAR: CUDA code indexes pack values. const int64_t *strides = m_tensors[t].stride; #pragma unroll @@ -236,10 +237,11 @@ class TensorBatchWrapT } } - NVCVByte *dataPtr = m_tensors[t].data; + NVCVByte *dataPtr = m_tensors[t].data; // NOSONAR: doGetPtr preserves mutable tensor element access. return reinterpret_cast(dataPtr + offset); } +private: int32_t m_numTensors; NVCVTensorBatchElementStridedRec *m_tensors; }; @@ -250,7 +252,7 @@ class TensorBatchWrapT * @tparam T Type (non-const) of each element inside the tensor batch wrapper. * @tparam Strides Each compile-time (use -1 for run-time) pitch in bytes from first to last dimension. */ -template +template // NOSONAR: StrideT is part of this public template API. class TensorBatchWrapT : public TensorBatchWrapT { using Base = TensorBatchWrapT; @@ -259,7 +261,6 @@ class TensorBatchWrapT : public TensorBatchWrapT using ValueType = T; using Base::doGetPtr; using Base::kNumDimensions; - using Base::m_tensors; using Base::strides; using typename Base::StrideType; @@ -268,7 +269,7 @@ class TensorBatchWrapT : public TensorBatchWrapT * * @param[in] data Tensor batch data to wrap. */ - __host__ TensorBatchWrapT(const TensorBatchDataStridedCuda &data) + explicit __host__ TensorBatchWrapT(const TensorBatchDataStridedCuda &data) : Base(data) { } @@ -278,7 +279,7 @@ class TensorBatchWrapT : public TensorBatchWrapT * * @param[in] data Tensor batch data to wrap. */ - __host__ __device__ TensorBatchWrapT(NVCVTensorBatchData &data) + explicit __host__ __device__ TensorBatchWrapT(NVCVTensorBatchData &data) : Base(data) { } diff --git a/src/cvcuda/include/cvcuda/cuda_tools/TensorWrap.hpp b/src/cvcuda/include/cvcuda/cuda_tools/TensorWrap.hpp index 6e768f9f9..d4ca27f15 100644 --- a/src/cvcuda/include/cvcuda/cuda_tools/TensorWrap.hpp +++ b/src/cvcuda/include/cvcuda/cuda_tools/TensorWrap.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -79,7 +79,7 @@ namespace nvcv::cuda { * @tparam T Type (it can be const) of each element inside the tensor wrapper. * @tparam Strides Each compile-time (use -1 for run-time) pitch in bytes from first to last dimension. */ -template +template // NOSONAR: StrideT is part of this public template API. class TensorWrapT; template @@ -93,7 +93,7 @@ class TensorWrapT using StrideType = StrideT; static constexpr int kNumDimensions = sizeof...(Strides); - static constexpr int kVariableStrides = ((Strides == -1) + ...); + static constexpr int kVariableStrides = ((Strides == -1 ? 1 : 0) + ...); static constexpr int kConstantStrides = kNumDimensions - kVariableStrides; TensorWrapT() = default; @@ -107,7 +107,7 @@ class TensorWrapT template explicit __host__ __device__ TensorWrapT(const DataType *data, Args... strides) : m_data(reinterpret_cast(data)) - , m_strides{std::forward(strides)...} + , m_strides{static_cast(strides)...} { static_assert((IsIndexType && ...)); static_assert(sizeof...(Args) == kVariableStrides); @@ -135,12 +135,11 @@ class TensorWrapT * * @param[in] image Image reference to the image that will be wrapped. */ - __host__ TensorWrapT(const ImageDataStridedCuda &image) + explicit __host__ TensorWrapT(const ImageDataStridedCuda &image) + : m_data(reinterpret_cast(image.plane(0).basePtr)) { static_assert(kVariableStrides == 1 && kNumDimensions == 2); - m_data = reinterpret_cast(image.plane(0).basePtr); - m_strides[0] = image.plane(0).rowStride; } @@ -149,14 +148,13 @@ class TensorWrapT * * @param[in] tensor Tensor reference to the tensor that will be wrapped. */ - __host__ TensorWrapT(const TensorDataStridedCuda &tensor) + explicit __host__ TensorWrapT(const TensorDataStridedCuda &tensor) + : m_data(reinterpret_cast(tensor.basePtr())) { - constexpr StrideT kStride[] = {std::forward(Strides)...}; + constexpr StrideT kStride[] = {Strides...}; // NOSONAR: CUDA code indexes template-pack values. assert(tensor.rank() >= kNumDimensions); - m_data = reinterpret_cast(tensor.basePtr()); - #pragma unroll for (int i = 0; i < kNumDimensions; ++i) { @@ -233,7 +231,7 @@ class TensorWrapT protected: template - inline const __host__ __device__ T *doGetPtr(Args... c) const + inline __host__ __device__ StrideT doGetOffset(Args... c) const { static_assert((IsIndexType && ...)); static_assert(sizeof...(Args) <= kNumDimensions); @@ -241,9 +239,9 @@ class TensorWrapT constexpr int kArgSize = sizeof...(Args); constexpr int kVarSize = kArgSize < kVariableStrides ? kArgSize : kVariableStrides; constexpr int kDimSize = kArgSize < kNumDimensions ? kArgSize : kNumDimensions; - constexpr StrideT kStride[] = {std::forward(Strides)...}; + constexpr StrideT kStride[] = {Strides...}; // NOSONAR: CUDA code indexes template-pack values. - StrideType coords[] = {std::forward(c)...}; + StrideType coords[] = {static_cast(c)...}; // NOSONAR: CUDA code indexes pack values. // Computing offset first potentially postpones or avoids 64-bit math during addressing StrideT offset = 0; @@ -258,12 +256,18 @@ class TensorWrapT offset += coords[i] * kStride[i]; } - return reinterpret_cast(m_data + offset); + return offset; + } + + template + inline const __host__ __device__ T *doGetPtr(Args... c) const + { + return reinterpret_cast(m_data + doGetOffset(c...)); } private: const std::byte *m_data = nullptr; - StrideT m_strides[kVariableStrides] = {}; + StrideT m_strides[kVariableStrides] = {}; // NOSONAR: device storage exposed as pointer. }; /** @@ -272,7 +276,7 @@ class TensorWrapT * @tparam T Type (non-const) of each element inside the tensor wrapper. * @tparam Strides Each compile-time (use -1 for run-time) pitch in bytes from first to last dimension. */ -template +template // NOSONAR: StrideT is part of this public template API. class TensorWrapT : public TensorWrapT { using Base = TensorWrapT; @@ -296,6 +300,7 @@ class TensorWrapT : public TensorWrapT template explicit __host__ __device__ TensorWrapT(DataType *data, Args... strides) : Base(data, strides...) + , m_data(reinterpret_cast(data)) { } @@ -309,6 +314,7 @@ class TensorWrapT : public TensorWrapT template explicit __host__ __device__ TensorWrapT(DataType *data, StrideType *strides) : Base(data, strides) + , m_data(reinterpret_cast(data)) { } @@ -317,8 +323,9 @@ class TensorWrapT : public TensorWrapT * * @param[in] image Image reference to the image that will be wrapped. */ - __host__ TensorWrapT(const ImageDataStridedCuda &image) + explicit __host__ TensorWrapT(const ImageDataStridedCuda &image) : Base(image) + , m_data(reinterpret_cast(image.plane(0).basePtr)) { } @@ -327,8 +334,9 @@ class TensorWrapT : public TensorWrapT * * @param[in] tensor Tensor reference to the tensor that will be wrapped. */ - __host__ TensorWrapT(const TensorDataStridedCuda &tensor) + explicit __host__ TensorWrapT(const TensorDataStridedCuda &tensor) : Base(tensor) + , m_data(reinterpret_cast(tensor.basePtr())) { } @@ -384,9 +392,11 @@ class TensorWrapT : public TensorWrapT template inline __host__ __device__ T *doGetPtr(Args... c) const { - // The const_cast here is the *only* place where it is used to remove the base pointer constness - return const_cast(Base::doGetPtr(c...)); + return reinterpret_cast(m_data + Base::doGetOffset(c...)); } + +private: + std::byte *m_data = nullptr; }; template diff --git a/src/cvcuda/include/cvcuda/cuda_tools/TypeTraits.hpp b/src/cvcuda/include/cvcuda/cuda_tools/TypeTraits.hpp index 7cf4b57bd..97815c88c 100644 --- a/src/cvcuda/include/cvcuda/cuda_tools/TypeTraits.hpp +++ b/src/cvcuda/include/cvcuda/cuda_tools/TypeTraits.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -57,21 +57,50 @@ namespace nvcv::cuda { */ using detail::TypeTraits; +template +struct Requirement; + +template +struct Requirement +{ + using type = T; +}; + // Metatype to serve as a requirement for a template object to meet the given boolean expression. -template -using Require = std::enable_if_t; +template +using Require = typename Requirement::type; // Metavariable to check if one or more types have type traits. template constexpr bool HasTypeTraits = (detail::HasTypeTraits_t::value && ...); // Metavariable to check if a type is a CUDA compound type. -template>> -constexpr bool IsCompound = TypeTraits::components >= 1; +template +constexpr bool IsCompound = [] +{ + if constexpr (HasTypeTraits) + { + return TypeTraits::components >= 1; + } + else + { + return false; + } +}(); // Metavariable to check if a CUDA compound type T has N or more components. -template>> -constexpr bool HasEnoughComponents = N <= TypeTraits::components; +template +constexpr bool HasEnoughComponents = [] +{ + if constexpr (HasTypeTraits) + { + return N <= TypeTraits::components; + } + else + { + return false; + } +}(); template constexpr bool IsStrideType = std::is_same_v || std::is_same_v; @@ -91,7 +120,7 @@ constexpr bool IsIndexType = std::is_integral_v && (TypeTraits::max <= Typ * * @tparam T Type to get the base type from. */ -template>> +template using BaseType = typename TypeTraits::base_type; /** @@ -106,7 +135,7 @@ using BaseType = typename TypeTraits::base_type; * * @tparam T Type to get the number of components from. */ -template>> +template constexpr int NumComponents = TypeTraits::components; /** @@ -122,12 +151,22 @@ constexpr int NumComponents = TypeTraits::components; * * @tparam T Type to get the number of elements from. */ -template>> +template constexpr int NumElements = TypeTraits::elements; // Metavariable to get the lowest value from a regular C or CUDA compound type T. -template>> -constexpr BaseType Lowest = std::is_floating_point_v> ? -TypeTraits::max : TypeTraits::min; +template +constexpr BaseType Lowest = [] +{ + if constexpr (std::is_floating_point_v>) + { + return -TypeTraits::max; + } + else + { + return TypeTraits::min; + } +}(); /** * Metatype to make a type from a base type and number of components. @@ -144,7 +183,7 @@ constexpr BaseType Lowest = std::is_floating_point_v> ? -TypeTrai * @tparam T Base type to make the type from. * @tparam C Number of components to make the type. */ -template>> +template using MakeType = detail::MakeType_t; /** @@ -160,7 +199,7 @@ using MakeType = detail::MakeType_t; * @tparam BT Base type to use in the conversion. * @tparam T Target type to convert its base type. */ -template>> +template using ConvertBaseTypeTo = detail::ConvertBaseTypeTo_t; /** @@ -187,9 +226,11 @@ using ConvertBaseTypeTo = detail::ConvertBaseTypeTo_t; * @return The reference of the value's element. */ template, BaseType, T>>, - class = Require>> + class = void> __host__ __device__ RT &GetElement(T &v, int eidx) { + static_assert(HasTypeTraits, "GetElement requires a type with CUDA type traits"); + if constexpr (IsCompound) { assert(eidx < NumElements); @@ -202,10 +243,11 @@ __host__ __device__ RT &GetElement(T &v, int eidx) } template, BaseType, T>>, - class = Require>> + typename RT = detail::CopyConstness_t, BaseType, T>>, class = void> __host__ __device__ RT &GetElement(T &v) { + static_assert(HasTypeTraits, "GetElement requires a type with CUDA type traits"); + if constexpr (IsCompound) { static_assert(EIDX < NumElements); @@ -245,9 +287,11 @@ __host__ __device__ RT &GetElement(T &v) * * @return The object of type T with all elements set to \p x. */ -template>> +template __host__ __device__ T SetAll(BaseType x) { + static_assert(HasTypeTraits, "SetAll requires a type with CUDA type traits"); + T out{}; GetElement<0>(out) = x; @@ -261,9 +305,11 @@ __host__ __device__ T SetAll(BaseType x) return out; } -template, class = Require>> +template> __host__ __device__ RT SetAll(BT x) { + static_assert(HasTypeTraits, "SetAll requires a base type with CUDA type traits"); + return SetAll(x); } @@ -281,9 +327,11 @@ __host__ __device__ RT SetAll(BT x) * * @return String with the name of the type. */ -template>> +template __host__ const char *GetTypeName() { + static_assert(HasTypeTraits, "GetTypeName requires a type with CUDA type traits"); + return TypeTraits::name; } diff --git a/src/cvcuda/include/cvcuda/cuda_tools/detail/MathWrappersImpl.hpp b/src/cvcuda/include/cvcuda/cuda_tools/detail/MathWrappersImpl.hpp index e64481777..fa8ab7337 100644 --- a/src/cvcuda/include/cvcuda/cuda_tools/detail/MathWrappersImpl.hpp +++ b/src/cvcuda/include/cvcuda/cuda_tools/detail/MathWrappersImpl.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,6 +21,7 @@ // Internal implementation of math wrapppers functionalities. // Not to be used directly. +#include #include // for FE_TONEAREST, etc. #include // for std::round, etc. @@ -404,12 +405,9 @@ template inline __host__ U RoundEvenImpl(U u) { U rounded = std::round(u); - if (std::abs(rounded - u) == U(0.5)) + if (std::abs(rounded - u) == U(0.5) && (static_cast(rounded) & 1) != 0) { - if (static_cast(rounded) & 1) - { - rounded -= std::copysign(U(1.0), u); - } + rounded -= std::copysign(U(1.0), u); } return rounded; } @@ -424,19 +422,19 @@ inline __host__ __device__ T RoundImpl(U u) // round is to nearest; floor is downward; ceil is upward; and trunc is towards zero. if constexpr (RM == FE_TONEAREST) { - return RoundEvenImpl(u); + return static_cast(RoundEvenImpl(u)); } else if constexpr (RM == FE_DOWNWARD) { - return std::floor(u); + return static_cast(std::floor(u)); } else if constexpr (RM == FE_UPWARD) { - return std::ceil(u); + return static_cast(std::ceil(u)); } else if constexpr (RM == FE_TOWARDZERO) { - return std::trunc(u); + return static_cast(std::trunc(u)); } #endif } @@ -467,7 +465,7 @@ inline __host__ __device__ U PowImpl(U x, S y) #ifdef __CUDA_ARCH__ return DevicePowImpl(x, y); #else - return std::pow(x, y); + return static_cast(std::pow(x, y)); #endif } @@ -477,7 +475,7 @@ inline __host__ __device__ U ExpImpl(U u) #ifdef __CUDA_ARCH__ return DeviceExpImpl(u); #else - return std::exp(u); + return static_cast(std::exp(u)); #endif } @@ -487,7 +485,7 @@ inline __host__ __device__ U SqrtImpl(U u) #ifdef __CUDA_ARCH__ return DeviceSqrtImpl(u); #else - return std::sqrt(u); + return static_cast(std::sqrt(u)); #endif } @@ -503,7 +501,7 @@ inline __host__ __device__ U AbsImpl(U u) #ifdef __CUDA_ARCH__ return DeviceAbsImpl(u); #else - return std::abs(u); + return static_cast(std::abs(u)); #endif } } @@ -511,7 +509,15 @@ inline __host__ __device__ U AbsImpl(U u) template inline __host__ __device__ U ClampImpl(U u, S lo, S hi) { - return u <= lo ? lo : (u >= hi ? hi : u); + if (u <= lo) + { + return lo; + } + if (u >= hi) + { + return hi; + } + return u; } } // namespace nvcv::cuda::detail diff --git a/src/cvcuda/include/cvcuda/cuda_tools/detail/RangeCastImpl.hpp b/src/cvcuda/include/cvcuda/cuda_tools/detail/RangeCastImpl.hpp index de51cfd78..370bed2c0 100644 --- a/src/cvcuda/include/cvcuda/cuda_tools/detail/RangeCastImpl.hpp +++ b/src/cvcuda/include/cvcuda/cuda_tools/detail/RangeCastImpl.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -27,41 +27,103 @@ namespace nvcv::cuda::detail { +template +inline __host__ __device__ T RangeCastFloatingNarrow(U u) +{ + return BoundedCast(u, -TypeTraits::max, TypeTraits::max); +} + +template +inline __host__ __device__ T RangeCastFloatingToSignedIntegral(U u) +{ + if (u >= U{1}) + { + return TypeTraits::max; + } + else if (u <= U{-1}) + { + return -TypeTraits::max; + } + else + { + return RoundImpl(static_cast(TypeTraits::max) * u); + } +} + +template +inline __host__ __device__ T RangeCastSignedIntegralToFloating(U u) +{ + constexpr T invmax = T{1} / static_cast(TypeTraits::max); + + T out = static_cast(u) * invmax; + return out < T{-1} ? T{-1} : out; +} + +template +inline __host__ __device__ T RangeCastFloatingToUnsignedIntegral(U u) +{ + if (u >= U{1}) + { + return TypeTraits::max; + } + else if (u <= U{0}) + { + return T{0}; + } + else + { + return RoundImpl(static_cast(TypeTraits::max) * u); + } +} + +template +inline __host__ __device__ T RangeCastUnsignedIntegralToFloating(U u) +{ + constexpr T invmax = T{1} / static_cast(TypeTraits::max); + return static_cast(u) * invmax; +} + template inline __host__ __device__ T RangeCastImpl(U u) { - if constexpr (std::is_floating_point_v && std::is_floating_point_v && sizeof(U) > sizeof(T)) + constexpr bool kFloatingToFloatingNarrow + = std::is_floating_point_v && std::is_floating_point_v && sizeof(U) > sizeof(T); + constexpr bool kFloatingToSignedIntegral + = std::is_floating_point_v && std::is_integral_v && std::is_signed_v; + constexpr bool kSignedIntegralToFloating + = std::is_integral_v && std::is_signed_v && std::is_floating_point_v; + constexpr bool kFloatingToUnsignedIntegral + = std::is_floating_point_v && std::is_integral_v && std::is_unsigned_v; + constexpr bool kUnsignedIntegralToFloating + = std::is_integral_v && std::is_unsigned_v && std::is_floating_point_v; + constexpr bool kIntegralToIntegral = std::is_integral_v && std::is_integral_v; + + if constexpr (kFloatingToFloatingNarrow) { // any-float -> any-float, big -> small - return u <= -TypeTraits::max ? -TypeTraits::max - : (u >= TypeTraits::max ? TypeTraits::max : static_cast(u)); + return RangeCastFloatingNarrow(u); } - else if constexpr (std::is_floating_point_v && std::is_integral_v && std::is_signed_v) + else if constexpr (kFloatingToSignedIntegral) { // any-float -> any-integral-signed - return u >= U{1} ? TypeTraits::max - : (u <= U{-1} ? -TypeTraits::max : RoundImpl(TypeTraits::max * u)); + return RangeCastFloatingToSignedIntegral(u); } - else if constexpr (std::is_integral_v && std::is_signed_v && std::is_floating_point_v) + else if constexpr (kSignedIntegralToFloating) { // any-integral-signed -> any-float - constexpr T invmax = T{1} / TypeTraits::max; - - T out = static_cast(u) * invmax; - return out < T{-1} ? T{-1} : out; + return RangeCastSignedIntegralToFloating(u); } - else if constexpr (std::is_floating_point_v && std::is_integral_v && std::is_unsigned_v) + else if constexpr (kFloatingToUnsignedIntegral) { // any-float -> any-integral-unsigned - return u >= U{1} ? TypeTraits::max : (u <= U{0} ? T{0} : RoundImpl(TypeTraits::max * u)); + return RangeCastFloatingToUnsignedIntegral(u); } - else if constexpr (std::is_integral_v && std::is_unsigned_v && std::is_floating_point_v) + else if constexpr (kUnsignedIntegralToFloating) { // any-integral-unsigned -> any-float - constexpr T invmax = T{1} / TypeTraits::max; - return static_cast(u) * invmax; + return RangeCastUnsignedIntegralToFloating(u); } - else if constexpr (std::is_integral_v && std::is_integral_v) + else if constexpr (kIntegralToIntegral) { // any-integral -> any-integral, range cast reduces to saturate cast return BaseSaturateCastImpl(u); diff --git a/src/cvcuda/include/cvcuda/cuda_tools/detail/SaturateCastImpl.hpp b/src/cvcuda/include/cvcuda/cuda_tools/detail/SaturateCastImpl.hpp index b1bcdbbc3..e9fc893e6 100644 --- a/src/cvcuda/include/cvcuda/cuda_tools/detail/SaturateCastImpl.hpp +++ b/src/cvcuda/include/cvcuda/cuda_tools/detail/SaturateCastImpl.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -28,54 +28,104 @@ namespace nvcv::cuda::detail { // The base saturate cast implementation can be used by host- or device-side calls +template +inline __host__ __device__ T BoundedCast(U u, Min minValue, Max maxValue) +{ + if (u <= minValue) + { + return static_cast(minValue); + } + else if (u >= maxValue) + { + return static_cast(maxValue); + } + else + { + return static_cast(u); + } +} + +template +inline __host__ __device__ T SaturateFloatingToIntegral(U u) +{ + constexpr auto minT = static_cast(TypeTraits::min); + constexpr auto maxT = static_cast(TypeTraits::max); + + U out = ClampImpl(RoundImpl(u), minT, maxT); + + return static_cast(out); +} + +template +inline __host__ __device__ T SaturateSignedSmallToUnsigned(U u) +{ + return u <= 0 ? 0 : static_cast(u); +} + +template +inline __host__ __device__ T SaturateIntegralNarrowSameSign(U u) +{ + return BoundedCast(u, TypeTraits::min, TypeTraits::max); +} + +template +inline __host__ __device__ T SaturateUnsignedToSigned(U u) +{ + return u >= TypeTraits::max ? TypeTraits::max : static_cast(u); +} + +template +inline __host__ __device__ T SaturateSignedBigToUnsigned(U u) +{ + return BoundedCast(u, static_cast(TypeTraits::min), static_cast(TypeTraits::max)); +} + template inline __host__ __device__ T BaseSaturateCastImpl(U u) { - constexpr bool SmallToBig = sizeof(U) <= sizeof(T); - constexpr bool BigToSmall = sizeof(U) > sizeof(T); + constexpr bool SmallToBig = sizeof(U) <= sizeof(T); + constexpr bool BigToSmall = sizeof(U) > sizeof(T); + constexpr bool kFloatingToIntegral = std::is_floating_point_v && std::is_integral_v; + constexpr bool kSignedSmallToUnsigned + = std::is_integral_v && std::is_signed_v && std::is_integral_v && std::is_unsigned_v && SmallToBig; + constexpr bool kNarrowSameSign + = std::is_integral_v< + U> && std::is_integral_v && ((std::is_signed_v && std::is_signed_v) || (std::is_unsigned_v && std::is_unsigned_v)) + && BigToSmall; + constexpr bool kUnsignedToSigned + = std::is_integral_v && std::is_unsigned_v && std::is_integral_v && std::is_signed_v; + constexpr bool kSignedBigToUnsigned + = std::is_integral_v && std::is_signed_v && std::is_integral_v && std::is_unsigned_v && BigToSmall; // To silence spurious warnings with gcc-11.1 (-Wunused-but-set-variable) (void)SmallToBig; (void)BigToSmall; - if constexpr (std::is_floating_point_v && std::is_integral_v) + if constexpr (kFloatingToIntegral) { // any-float -> any-integral - constexpr U minT = static_cast(TypeTraits::min); - constexpr U maxT = static_cast(TypeTraits::max); - - U out = ClampImpl(RoundImpl(u), minT, maxT); - - return static_cast(out); + return SaturateFloatingToIntegral(u); } - else if constexpr (std::is_integral_v< - U> && std::is_signed_v && std::is_integral_v && std::is_unsigned_v && SmallToBig) + else if constexpr (kSignedSmallToUnsigned) { // any-integral-signed -> any-integral-unsigned, small -> big and equal - return u <= 0 ? 0 : static_cast(u); + return SaturateSignedSmallToUnsigned(u); } - else if constexpr ( - std::is_integral_v< - U> && std::is_integral_v && ((std::is_signed_v && std::is_signed_v) || (std::is_unsigned_v && std::is_unsigned_v)) - && BigToSmall) + else if constexpr (kNarrowSameSign) { // any-integral-signed -> any-integral-signed, big -> small // any-integral-unsigned -> any-integral-unsigned, big -> small - return u <= TypeTraits::min ? TypeTraits::min - : (u >= TypeTraits::max ? TypeTraits::max : static_cast(u)); + return SaturateIntegralNarrowSameSign(u); } - else if constexpr (std::is_integral_v && std::is_unsigned_v && std::is_integral_v && std::is_signed_v) + else if constexpr (kUnsignedToSigned) { // any-integral-unsigned -> any-integral-signed - return u >= TypeTraits::max ? TypeTraits::max : static_cast(u); + return SaturateUnsignedToSigned(u); } - else if constexpr (std::is_integral_v< - U> && std::is_signed_v && std::is_integral_v && std::is_unsigned_v && BigToSmall) + else if constexpr (kSignedBigToUnsigned) { // any-integral-signed -> any-integral-unsigned, big -> small - return u <= static_cast(TypeTraits::min) - ? TypeTraits::min - : (u >= static_cast(TypeTraits::max) ? TypeTraits::max : static_cast(u)); + return SaturateSignedBigToUnsigned(u); } else { @@ -83,7 +133,7 @@ inline __host__ __device__ T BaseSaturateCastImpl(U u) // any-integral-signed -> any-integral-signed, small -> big and equal // any-integral-unsigned -> any-integral-unsigned, small -> big and equal // any -> any-float - return u; + return static_cast(u); } } diff --git a/src/cvcuda/include/cvcuda/cuda_tools/math/LinAlg.hpp b/src/cvcuda/include/cvcuda/cuda_tools/math/LinAlg.hpp index c413a4685..4f932f8c2 100644 --- a/src/cvcuda/include/cvcuda/cuda_tools/math/LinAlg.hpp +++ b/src/cvcuda/include/cvcuda/cuda_tools/math/LinAlg.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -134,7 +134,7 @@ class Vector * * @return Pointer to the first element of this vector. */ - constexpr __host__ __device__ operator const T *() const + explicit constexpr __host__ __device__ operator const T *() const { return &m_data[0]; } @@ -144,7 +144,7 @@ class Vector * * @return Pointer to the first element of this vector. */ - constexpr __host__ __device__ operator T *() + explicit constexpr __host__ __device__ operator T *() { return &m_data[0]; } @@ -221,9 +221,9 @@ class Vector // On-purpose public data to allow POD-class direct initialization. #ifdef __CUDA_ARCH__ - T m_data[N]; + T m_data[N]; // NOSONAR: public POD storage enables aggregate initialization in device code. #else - T m_data[N] = {}; + T m_data[N] = {}; // NOSONAR: public POD storage enables aggregate initialization. #endif }; @@ -458,7 +458,7 @@ class Matrix } // On-purpose public data to allow POD-class direct initialization. - Vector m_data[M]; + Vector m_data[M]; // NOSONAR: public POD storage enables aggregate initialization. }; namespace detail { @@ -779,10 +779,10 @@ template constexpr __host__ __device__ Matrix &operator*=(Matrix &lhs, T rhs) { #pragma unroll - for (int i = 0; i < lhs.rows(); ++i) + for (int i = 0; i < lhs.rows(); ++i) // NOSONAR: CUDA unroll loop. { #pragma unroll - for (int j = 0; j < lhs.cols(); ++j) + for (int j = 0; j < lhs.cols(); ++j) // NOSONAR: CUDA unroll loop. { lhs[i][j] *= rhs; } @@ -884,10 +884,10 @@ constexpr __host__ __device__ Matrix operator*(const Matrix &m Matrix r; #pragma unroll - for (int i = 0; i < r.rows(); ++i) + for (int i = 0; i < r.rows(); ++i) // NOSONAR: CUDA unroll loop. { #pragma unroll - for (int j = 0; j < r.cols(); ++j) + for (int j = 0; j < r.cols(); ++j) // NOSONAR: CUDA unroll loop. { r[i][j] = m[i][0] * v[j]; } @@ -1425,7 +1425,7 @@ constexpr __host__ Matrix rev(const Matrix &b, const Matrix -constexpr __host__ __device__ T det(const Matrix &m) +constexpr __host__ __device__ T det(const Matrix &) { return T{1}; } @@ -1477,7 +1477,7 @@ constexpr __host__ __device__ bool lu_inplace(Matrix &m, Vector #pragma unroll for (int j = 0; j < N; ++j) { - big = cuda::max(big, cuda::abs(m[i][j])); + big = cuda::max(big, static_cast(cuda::abs(m[i][j]))); } if (big == 0) @@ -1485,7 +1485,7 @@ constexpr __host__ __device__ bool lu_inplace(Matrix &m, Vector return false; } - v[i] = 1.0 / big; + v[i] = F{1} / big; } #pragma unroll @@ -1497,7 +1497,7 @@ constexpr __host__ __device__ bool lu_inplace(Matrix &m, Vector #pragma unroll for (int i = k; i < N; ++i) { - F aux = v[i] * cuda::abs(m[i][k]); + F aux = v[i] * static_cast(cuda::abs(m[i][k])); if (aux > big) { @@ -1523,7 +1523,8 @@ constexpr __host__ __device__ bool lu_inplace(Matrix &m, Vector #pragma unroll for (int i = k + 1; i < N; ++i) { - T aux = m[i][k] /= m[k][k]; + m[i][k] /= m[k][k]; + T aux = m[i][k]; #pragma unroll for (int j = k + 1; j < N; ++j) @@ -1609,7 +1610,7 @@ constexpr __host__ __device__ void inv_inplace(Matrix &m, const T &d) } template -constexpr __host__ __device__ Matrix inv(const Matrix &m, const T &d) +constexpr __host__ __device__ Matrix inv(const Matrix &, const T &d) { Matrix A; inv_inplace(A, d); diff --git a/src/cvcuda/priv/AdjustColorCommon.cuh b/src/cvcuda/priv/AdjustColorCommon.cuh new file mode 100644 index 000000000..53f33f1db --- /dev/null +++ b/src/cvcuda/priv/AdjustColorCommon.cuh @@ -0,0 +1,108 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +/** + * @file AdjustColorCommon.cuh + * + * @brief Device helpers for torchvision-compatible image-range clamping and AdjustContrast. + */ + +#ifndef CVCUDA_PRIV_ADJUST_COLOR_COMMON_CUH +#define CVCUDA_PRIV_ADJUST_COLOR_COMMON_CUH + +#include +#include +#include + +#include + +namespace cvcuda::priv::adjust { + +namespace cuda = nvcv::cuda; + +// torchvision's per-dtype clamp bound (``_max_value``): 1.0 for floating-point images and the +// dtype maximum for integer images. AdjustContrast uses this bound to match torchvision's +// image-domain ``_blend`` behavior. +template +inline __host__ __device__ float Bound() +{ + if constexpr (std::is_floating_point_v) + { + return 1.0f; + } + else + { + return static_cast(cuda::TypeTraits::max); + } +} + +// torchvision BT.601 luma weights used by adjust_contrast's grayscale conversion. These differ +// from cvtcolor's RGB2GRAY red weight (0.299) so the mean matches torchvision exactly. +inline constexpr float kLumaR = 0.2989f; +inline constexpr float kLumaG = 0.587f; +inline constexpr float kLumaB = 0.114f; + +// torchvision ``_blend`` for one channel component, evaluated in the float domain then clamped to +// [0, bound] and saturate-cast back to the base type: +// out = SaturateCast(clamp(ratio * in + (1 - ratio) * other, 0, bound)) +// adjust_contrast passes the per-image grayscale mean as ``other``. Integer results round-to-nearest +// via SaturateCast (torchvision truncates, so integer outputs may differ by <=1 LSB); float results +// are the bit-exact clamped affine. +template +inline __device__ BT Blend(BT v, float ratio, float other, float bound) +{ + float pixel = ratio * static_cast(v) + (1.0f - ratio) * other; + pixel = pixel < 0.0f ? 0.0f : (pixel > bound ? bound : pixel); + return cuda::SaturateCast(pixel); +} + +// torchvision adjust_contrast grayscale value from explicit R, G, B base components (used by the +// planar reduction path where the channels live in separate planes): +// gray = 0.2989 R + 0.587 G + 0.114 B; floored for integer inputs, matching torchvision's +// ``_rgb_to_grayscale_image(..., preserve_dtype=False)`` followed by ``floor_()``. +template +inline __device__ float GrayFromRGB(BT r, BT g, BT b) +{ + float gray = kLumaR * static_cast(r) + kLumaG * static_cast(g) + kLumaB * static_cast(b); + if constexpr (std::is_integral_v) + { + gray = floorf(gray); + } + return gray; +} + +// torchvision adjust_contrast grayscale value for one interleaved pixel, in the float domain: +// 1 channel -> the channel value cast to float. +// 3 channels -> GrayFromRGB of the three interleaved components. +template +inline __device__ float GrayValue(T pixel) +{ + using BT = cuda::BaseType; + static constexpr int numChannels = cuda::NumElements; + if constexpr (numChannels == 1) + { + return static_cast(cuda::GetElement(pixel, 0)); + } + else + { + return GrayFromRGB(cuda::GetElement(pixel, 0), cuda::GetElement(pixel, 1), cuda::GetElement(pixel, 2)); + } +} + +} // namespace cvcuda::priv::adjust + +#endif // CVCUDA_PRIV_ADJUST_COLOR_COMMON_CUH diff --git a/src/cvcuda/priv/BrightnessContrastPolicy.hpp b/src/cvcuda/priv/BrightnessContrastPolicy.hpp new file mode 100644 index 000000000..a9c441bd8 --- /dev/null +++ b/src/cvcuda/priv/BrightnessContrastPolicy.hpp @@ -0,0 +1,48 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#ifndef CVCUDA_PRIV_BRIGHTNESS_CONTRAST_POLICY_HPP +#define CVCUDA_PRIV_BRIGHTNESS_CONTRAST_POLICY_HPP + +namespace cvcuda::priv { + +enum class BrightnessContrastTensorKernelPolicy +{ + kScalar, + kVectorized +}; + +constexpr BrightnessContrastTensorKernelPolicy BrightnessContrastTensorKernelPolicyForSM(int sm, bool isUnsignedByte, + int numElements) +{ + return sm == 89 && isUnsignedByte && numElements == 3 ? BrightnessContrastTensorKernelPolicy::kScalar + : BrightnessContrastTensorKernelPolicy::kVectorized; +} + +static_assert(BrightnessContrastTensorKernelPolicyForSM(89, true, 3) == BrightnessContrastTensorKernelPolicy::kScalar); +static_assert(BrightnessContrastTensorKernelPolicyForSM(90, true, 3) + == BrightnessContrastTensorKernelPolicy::kVectorized); +static_assert(BrightnessContrastTensorKernelPolicyForSM(89, false, 3) + == BrightnessContrastTensorKernelPolicy::kVectorized); +static_assert(BrightnessContrastTensorKernelPolicyForSM(89, true, 1) + == BrightnessContrastTensorKernelPolicy::kVectorized); +static_assert(BrightnessContrastTensorKernelPolicyForSM(89, true, 4) + == BrightnessContrastTensorKernelPolicy::kVectorized); + +} // namespace cvcuda::priv + +#endif // CVCUDA_PRIV_BRIGHTNESS_CONTRAST_POLICY_HPP diff --git a/src/cvcuda/priv/CMakeLists.txt b/src/cvcuda/priv/CMakeLists.txt index deac31c27..84ab86bd7 100644 --- a/src/cvcuda/priv/CMakeLists.txt +++ b/src/cvcuda/priv/CMakeLists.txt @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,6 +18,16 @@ add_subdirectory(legacy) set(CV_CUDA_PRIV_FILES IOperator.cpp) set(CV_CUDA_PRIV_OP_FILES + OpJpegCompressionDistortion.cu + OpAdjustHue.cu + OpAdjustSaturation.cu + OpAdjustSharpness.cu + OpAdjustContrast.cu + OpInvert.cu + OpSolarize.cu + OpPosterize.cu + OpAutoContrast.cu + OpCLAHE.cu OpOSD.cpp OpHistogramEq.cpp OpAdvCvtColor.cu @@ -32,6 +42,8 @@ set(CV_CUDA_PRIV_OP_FILES OpColorTwist.cu OpCropFlipNormalizeReformat.cu OpHQResize.cu + OpHQResize2D.cu + OpHQResize3D.cu OpNonMaximumSuppression.cu OpReformat.cpp OpResize.cpp @@ -46,6 +58,7 @@ set(CV_CUDA_PRIV_OP_FILES OpLaplacian.cpp OpGaussian.cpp OpErase.cpp + OpEraseRegion.cu OpAverageBlur.cpp OpConv2D.cpp OpMedianBlur.cpp @@ -56,7 +69,7 @@ set(CV_CUDA_PRIV_OP_FILES OpWarpAffine.cpp OpWarpPerspective.cpp OpComposite.cpp - OpChannelReorder.cpp + OpChannelReorder.cu OpFlip.cpp OpGammaContrast.cpp OpPillowResize.cpp @@ -68,6 +81,7 @@ set(CV_CUDA_PRIV_OP_FILES OpLabel.cu OpPairwiseMatcher.cu OpStack.cpp + OpStackKernels.cu OpFindHomography.cu OpResizeCropConvertReformat.cu ) @@ -91,6 +105,36 @@ else() list(APPEND CV_CUDA_PRIV_FILES ${CV_CUDA_PRIV_OP_FILES}) endif() +cvcuda_add_targeted_cuda_architectures_to_sources( + ARCHITECTURES 86 89 + SOURCES + OpAdjustContrast.cu + OpAdvCvtColor.cu + OpChannelReorder.cu + OpEraseRegion.cu + OpHQResize2D.cu + OpInvert.cu + OpJpegCompressionDistortion.cu + OpMinMaxLoc.cu + OpResize.cu + SELECTED_SOURCES_VAR CV_CUDA_PRIV_FILES +) + +cvcuda_add_targeted_cuda_architectures_to_sources( + ARCHITECTURES 86 + SOURCES + OpResizeCropConvertReformat.cu + SELECTED_SOURCES_VAR CV_CUDA_PRIV_FILES +) + +cvcuda_add_targeted_cuda_architectures_to_sources( + ARCHITECTURES 89 + SOURCES + OpBrightnessContrast.cu + OpPairwiseMatcher.cu + SELECTED_SOURCES_VAR CV_CUDA_PRIV_FILES +) + add_library(cvcuda_priv STATIC ${CV_CUDA_PRIV_FILES} ) @@ -108,3 +152,5 @@ target_link_libraries(cvcuda_priv CUDA::cublasLt_static -lrt ) + +target_link_libraries(cvcuda_priv PRIVATE cvcuda_nvtx_config) diff --git a/src/cvcuda/priv/CudaDeviceUtils.hpp b/src/cvcuda/priv/CudaDeviceUtils.hpp new file mode 100644 index 000000000..36485db15 --- /dev/null +++ b/src/cvcuda/priv/CudaDeviceUtils.hpp @@ -0,0 +1,61 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#ifndef CVCUDA_PRIV_CUDA_DEVICE_UTILS_HPP +#define CVCUDA_PRIV_CUDA_DEVICE_UTILS_HPP + +#include + +namespace cvcuda::priv { + +// Returns the current device's SM compute-capability encoding (major * 10 + minor), +// not its number of streaming multiprocessors. +inline cudaError_t GetCurrentDeviceSM(int &sm) noexcept +{ + sm = 0; + + int device = 0; + cudaError_t status = cudaGetDevice(&device); + if (status != cudaSuccess) + return status; + + static thread_local int cachedDevice = -1; + static thread_local int cachedSM = 0; + if (cachedDevice != device) + { + int major = 0; + int minor = 0; + + status = cudaDeviceGetAttribute(&major, cudaDevAttrComputeCapabilityMajor, device); + if (status != cudaSuccess) + return status; + + status = cudaDeviceGetAttribute(&minor, cudaDevAttrComputeCapabilityMinor, device); + if (status != cudaSuccess) + return status; + + cachedDevice = device; + cachedSM = major * 10 + minor; + } + + sm = cachedSM; + return cudaSuccess; +} + +} // namespace cvcuda::priv + +#endif // CVCUDA_PRIV_CUDA_DEVICE_UTILS_HPP diff --git a/src/cvcuda/priv/IOperator.cpp b/src/cvcuda/priv/IOperator.cpp index 3839cb927..2e2705077 100644 --- a/src/cvcuda/priv/IOperator.cpp +++ b/src/cvcuda/priv/IOperator.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * NVIDIA CORPORATION, its affiliates and licensors retain all intellectual @@ -16,10 +16,10 @@ namespace cvcuda::priv { -IOperator *ToOperatorPtr(void *handle) +IOperator *ToOperatorPtr(NVCVOperatorHandle handle) { // First cast to the operator interface, this must always succeed. - if (IOperator *op = reinterpret_cast(handle)) + if (auto *op = reinterpret_cast(handle)) { // If major version are the same, if (op->version().major() == CURRENT_VERSION.major()) diff --git a/src/cvcuda/priv/IOperator.hpp b/src/cvcuda/priv/IOperator.hpp index 4e8fb2e0a..1cc9d08a6 100644 --- a/src/cvcuda/priv/IOperator.hpp +++ b/src/cvcuda/priv/IOperator.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * NVIDIA CORPORATION, its affiliates and licensors retain all intellectual @@ -24,6 +24,9 @@ #include #include +#include +#include + namespace cvcuda::priv { class IOperator @@ -34,18 +37,31 @@ class IOperator virtual ~IOperator() = default; - HandleType handle() const + HandleType handle() { - return reinterpret_cast(const_cast(static_cast(this))); + return reinterpret_cast(this); } - Version version() + Version version() const { return CURRENT_VERSION; } }; -IOperator *ToOperatorPtr(void *handle); +IOperator *ToOperatorPtr(NVCVOperatorHandle handle); + +template +inline NVCVOperatorHandle CreateOperatorHandle(Args &&...args) +{ + auto op = std::make_unique(std::forward(args)...); + return reinterpret_cast(op.release()); +} + +inline void DestroyOperatorHandle(NVCVOperatorHandle handle) +{ + std::unique_ptr op(ToOperatorPtr(handle)); + (void)op; +} template inline T *ToDynamicPtr(NVCVOperatorHandle h) diff --git a/src/cvcuda/priv/InvertPolicy.hpp b/src/cvcuda/priv/InvertPolicy.hpp new file mode 100644 index 000000000..ee16bed7e --- /dev/null +++ b/src/cvcuda/priv/InvertPolicy.hpp @@ -0,0 +1,37 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#ifndef CVCUDA_PRIV_INVERT_POLICY_HPP +#define CVCUDA_PRIV_INVERT_POLICY_HPP + +#include + +namespace cvcuda::priv { + +constexpr bool UsePackedU8C3VarShapeKernelForDevice(int sm, std::string_view deviceName) +{ + return sm != 89 || deviceName != "NVIDIA L4"; +} + +static_assert(!UsePackedU8C3VarShapeKernelForDevice(89, "NVIDIA L4")); +static_assert(UsePackedU8C3VarShapeKernelForDevice(89, "NVIDIA L40")); +static_assert(UsePackedU8C3VarShapeKernelForDevice(80, "NVIDIA A100-PCIE-40GB")); +static_assert(UsePackedU8C3VarShapeKernelForDevice(90, "NVIDIA H100 PCIe")); + +} // namespace cvcuda::priv + +#endif // CVCUDA_PRIV_INVERT_POLICY_HPP diff --git a/src/cvcuda/priv/JpegDistortionMath.hpp b/src/cvcuda/priv/JpegDistortionMath.hpp new file mode 100644 index 000000000..f9b2c25a4 --- /dev/null +++ b/src/cvcuda/priv/JpegDistortionMath.hpp @@ -0,0 +1,295 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +/** + * @file JpegDistortionMath.hpp + * + * @brief Shared host/device math for the JpegCompressionDistortion operator. + * + * Ported from NVIDIA DALI (Apache-2.0, Copyright (c) 2021-2022 NVIDIA CORPORATION & AFFILIATES): + * dali/kernels/imgproc/jpeg/dct_8x8_gpu.cuh (fixed-rotation 8-point DCT, itself derived from the + * NVIDIA CUDA dct8x8 sample), dali/kernels/imgproc/jpeg/jpeg_distortion_gpu_kernel.h (Annex-K + * quantization tables and libjpeg quality scaling) and + * dali/kernels/imgproc/color_manipulation/color_space_conversion_impl.h (full-range JFIF YCbCr). + * + * Every multiply-add is written as an explicit fmaf() in a fixed order, and every remaining + * operation (add/sub, lone multiply, correctly-rounded reciprocal, roundf, round-to-nearest-even + * saturating cast) is correctly rounded and identical on host and device. This makes the pipeline + * immune to compiler contraction (nvcc -fmad, gcc -ffp-contract), so an independent CPU evaluation + * of the same canonical operation order reproduces the GPU results bit-exactly. This canonical + * order — not bit-parity with DALI's own (contraction-dependent) GPU output — is the operator's + * defined semantics. + */ + +#ifndef CVCUDA_PRIV_JPEG_DISTORTION_MATH_HPP +#define CVCUDA_PRIV_JPEG_DISTORTION_MATH_HPP + +#include +#include + +#include +#include + +namespace cvcuda::priv::jpeg { + +// Fixed-rotation DCT constants (sqrt(2)*cos(k*pi/16)) and the 1/sqrt(8) per-pass normalization. +constexpr float kDctA = 1.387039845322148f; // sqrt(2) * cos( pi / 16) +constexpr float kDctB = 1.306562964876377f; // sqrt(2) * cos( pi / 8) +constexpr float kDctC = 1.175875602419359f; // sqrt(2) * cos(3 * pi / 16) +constexpr float kDctD = 0.785694958387102f; // sqrt(2) * cos(5 * pi / 16) +constexpr float kDctE = 0.541196100146197f; // sqrt(2) * cos(3 * pi / 8) +constexpr float kDctF = 0.275899379282943f; // sqrt(2) * cos(7 * pi / 16) +constexpr float kDctNorm = 0.3535533905932737f; // 1 / sqrt(8) + +// Base quantization tables suggested in Annex K of the JPEG standard (row-major 8x8). The kernels +// index them with runtime thread indices, so under CUDA compilation they must be device-resident +// (__constant__); a plain constexpr host array is not addressable from device code. Host +// compilation sees constexpr arrays. Note: __constant__ definitions have external linkage, so this +// header must stay included by a single CUDA translation unit (OpJpegCompressionDistortion.cu). +#ifdef __CUDACC__ +# define CVCUDA_JPEG_TABLE_STORAGE __constant__ const +#else +# define CVCUDA_JPEG_TABLE_STORAGE constexpr +#endif + +CVCUDA_JPEG_TABLE_STORAGE uint8_t kLumaQuantBase[64] = { + 16, 11, 10, 16, 24, 40, 51, 61, // + 12, 12, 14, 19, 26, 58, 60, 55, // + 14, 13, 16, 24, 40, 57, 69, 56, // + 14, 17, 22, 29, 51, 87, 80, 62, // + 18, 22, 37, 56, 68, 109, 103, 77, // + 24, 35, 55, 64, 81, 104, 113, 92, // + 49, 64, 78, 87, 103, 121, 120, 101, // + 72, 92, 95, 98, 112, 100, 103, 99, // +}; + +CVCUDA_JPEG_TABLE_STORAGE uint8_t kChromaQuantBase[64] = { + 17, 18, 24, 47, 99, 99, 99, 99, // + 18, 21, 26, 66, 99, 99, 99, 99, // + 24, 26, 56, 99, 99, 99, 99, 99, // + 47, 66, 99, 99, 99, 99, 99, 99, // + 99, 99, 99, 99, 99, 99, 99, 99, // + 99, 99, 99, 99, 99, 99, 99, 99, // + 99, 99, 99, 99, 99, 99, 99, 99, // + 99, 99, 99, 99, 99, 99, 99, 99, // +}; + +// Round-to-nearest-even saturating cast to uint8 (device: cvt.rni.sat.u8.f32). +inline __host__ __device__ uint8_t SatCastU8(float v) +{ + return nvcv::cuda::SaturateCast(v); +} + +// Correctly-rounded reciprocal: __frcp_rn and IEEE round-to-nearest 1/x are the same value. +inline __host__ __device__ float Rcp(float v) +{ +#ifdef __CUDA_ARCH__ + return __frcp_rn(v); +#else + return 1.0f / v; +#endif +} + +// Canonical fused dot products: each product-sum is a single-rounded fmaf chain evaluated from the +// first coefficient outward, with the last term a lone (single-rounded) multiply. +inline __host__ __device__ float Dot2(float c0, float v0, float c1, float v1) +{ + return fmaf(c0, v0, c1 * v1); +} + +inline __host__ __device__ float Dot3(float c0, float v0, float c1, float v1, float c2, float v2) +{ + return fmaf(c0, v0, Dot2(c1, v1, c2, v2)); +} + +inline __host__ __device__ float Dot4(float c0, float v0, float c1, float v1, float c2, float v2, float c3, float v3) +{ + return fmaf(c0, v0, Dot3(c1, v1, c2, v2, c3, v3)); +} + +inline __host__ __device__ float Dot3Bias(float c0, float v0, float c1, float v1, float c2, float v2, float bias) +{ + return fmaf(c0, v0, fmaf(c1, v1, fmaf(c2, v2, bias))); +} + +// libjpeg quality scaling (jpeg_quality_scaling pre-divided by 100); quality is clamped to [1, 100]. +inline __host__ __device__ float QuantScale(int quality) +{ + const int q = quality < 1 ? 1 : (quality > 100 ? 100 : quality); + return q < 50 ? 50.0f / static_cast(q) : 2.0f - static_cast(2 * q) / 100.0f; +} + +// Scaled table entry, rounded half away from zero and clamped so every quantization step is in +// [1, 255]. DALI builds these tables on the host, where its saturating cast rounds with +// std::roundf; libjpeg's integer mapping agrees. Round-half-even here would diverge at q=75, +// whose exact 0.5 scale puts every odd base entry on a tie. +inline __host__ __device__ float QuantTableEntry(float scale, uint8_t base) +{ + const float entry = roundf(scale * static_cast(base)); + return entry < 1.0f ? 1.0f : (entry > 255.0f ? 255.0f : entry); +} + +// Quantization round trip: Q * round(value / Q) with the division as a reciprocal-multiply. +inline __host__ __device__ float Quantize(float value, float q) +{ + return q * roundf(value * Rcp(q)); +} + +// Forward 8-point DCT over 8 strided elements (rows: Stride=1, columns: Stride=9 with the padded +// shared-memory layout). Two passes (rows then columns) give the orthonormal 2-D DCT. +template +inline __host__ __device__ void FwdDct8(float *data) +{ + float x0 = data[0 * Stride]; + float x1 = data[1 * Stride]; + float x2 = data[2 * Stride]; + float x3 = data[3 * Stride]; + float x4 = data[4 * Stride]; + float x5 = data[5 * Stride]; + float x6 = data[6 * Stride]; + float x7 = data[7 * Stride]; + + const float tmp0 = x0 + x7; + const float tmp1 = x1 + x6; + const float tmp2 = x2 + x5; + const float tmp3 = x3 + x4; + + const float tmp4 = x0 - x7; + const float tmp5 = x6 - x1; + const float tmp6 = x2 - x5; + const float tmp7 = x4 - x3; + + const float tmp8 = tmp0 + tmp3; + const float tmp9 = tmp0 - tmp3; + const float tmp10 = tmp1 + tmp2; + const float tmp11 = tmp1 - tmp2; + + x0 = kDctNorm * (tmp8 + tmp10); + x2 = kDctNorm * Dot2(kDctB, tmp9, kDctE, tmp11); + x4 = kDctNorm * (tmp8 - tmp10); + x6 = kDctNorm * Dot2(kDctE, tmp9, -kDctB, tmp11); + + x1 = kDctNorm * Dot4(kDctA, tmp4, -kDctC, tmp5, kDctD, tmp6, -kDctF, tmp7); + x3 = kDctNorm * Dot4(kDctC, tmp4, kDctF, tmp5, -kDctA, tmp6, kDctD, tmp7); + x5 = kDctNorm * Dot4(kDctD, tmp4, kDctA, tmp5, kDctF, tmp6, -kDctC, tmp7); + x7 = kDctNorm * Dot4(kDctF, tmp4, kDctD, tmp5, kDctC, tmp6, kDctA, tmp7); + + data[0 * Stride] = x0; + data[1 * Stride] = x1; + data[2 * Stride] = x2; + data[3 * Stride] = x3; + data[4 * Stride] = x4; + data[5 * Stride] = x5; + data[6 * Stride] = x6; + data[7 * Stride] = x7; +} + +// Inverse 8-point DCT over 8 strided elements (columns first: Stride=9, then rows: Stride=1). +template +inline __host__ __device__ void InvDct8(float *data) +{ + float x0 = data[0 * Stride]; + float x1 = data[1 * Stride]; + float x2 = data[2 * Stride]; + float x3 = data[3 * Stride]; + float x4 = data[4 * Stride]; + float x5 = data[5 * Stride]; + float x6 = data[6 * Stride]; + float x7 = data[7 * Stride]; + + const float tmp0 = x0 + x4; + const float tmp1 = Dot2(kDctB, x2, kDctE, x6); + + const float tmp2 = tmp0 + tmp1; + const float tmp3 = tmp0 - tmp1; + const float tmp4 = Dot4(kDctF, x7, kDctA, x1, kDctC, x3, kDctD, x5); + const float tmp5 = Dot4(kDctA, x7, -kDctF, x1, kDctD, x3, -kDctC, x5); + + const float tmp6 = x0 - x4; + const float tmp7 = Dot2(kDctE, x2, -kDctB, x6); + + const float tmp8 = tmp6 + tmp7; + const float tmp9 = tmp6 - tmp7; + const float tmp10 = Dot4(kDctC, x1, -kDctD, x7, -kDctF, x3, -kDctA, x5); + const float tmp11 = Dot4(kDctD, x1, kDctC, x7, -kDctA, x3, kDctF, x5); + + x0 = kDctNorm * (tmp2 + tmp4); + x7 = kDctNorm * (tmp2 - tmp4); + x4 = kDctNorm * (tmp3 + tmp5); + x3 = kDctNorm * (tmp3 - tmp5); + + x1 = kDctNorm * (tmp8 + tmp10); + x5 = kDctNorm * (tmp9 - tmp11); + x2 = kDctNorm * (tmp9 + tmp11); + x6 = kDctNorm * (tmp8 - tmp10); + + data[0 * Stride] = x0; + data[1 * Stride] = x1; + data[2 * Stride] = x2; + data[3 * Stride] = x3; + data[4 * Stride] = x4; + data[5 * Stride] = x5; + data[6 * Stride] = x6; + data[7 * Stride] = x7; +} + +// Full-range JFIF color conversions (uint8 in/out, chroma biased by +128). + +inline __host__ __device__ uint8_t RgbToY(uchar3 rgb) +{ + return SatCastU8( + Dot3(0.299f, static_cast(rgb.x), 0.587f, static_cast(rgb.y), 0.114f, static_cast(rgb.z))); +} + +inline __host__ __device__ uint8_t RgbToCb(uchar3 rgb) +{ + return SatCastU8(Dot3Bias(-0.16873589f, static_cast(rgb.x), -0.33126411f, static_cast(rgb.y), 0.5f, + static_cast(rgb.z), 128.0f)); +} + +inline __host__ __device__ uint8_t RgbToCr(uchar3 rgb) +{ + return SatCastU8(Dot3Bias(0.5f, static_cast(rgb.x), -0.41868759f, static_cast(rgb.y), -0.08131241f, + static_cast(rgb.z), 128.0f)); +} + +inline __host__ __device__ uchar3 YCbCrToRgb(uint8_t y, uint8_t cb, uint8_t cr) +{ + const float ys = static_cast(y); + const float tb = static_cast(cb) - 128.0f; + const float tr = static_cast(cr) - 128.0f; + uchar3 rgb; + rgb.x = SatCastU8(fmaf(1.402f, tr, ys)); + rgb.y = SatCastU8(fmaf(-0.714136285f, tr, fmaf(-0.344136285f, tb, ys))); + rgb.z = SatCastU8(fmaf(1.772f, tb, ys)); + return rgb; +} + +// 2x2 box average of RGB pixels: the integer channel sum is exact and the *0.25f scaling is a +// power of two, so the round-to-nearest-even cast is the only rounding step. +inline __host__ __device__ uchar3 Avg4(uchar3 p00, uchar3 p01, uchar3 p10, uchar3 p11) +{ + uchar3 avg; + avg.x = SatCastU8(static_cast(p00.x + p01.x + p10.x + p11.x) * 0.25f); + avg.y = SatCastU8(static_cast(p00.y + p01.y + p10.y + p11.y) * 0.25f); + avg.z = SatCastU8(static_cast(p00.z + p01.z + p10.z + p11.z) * 0.25f); + return avg; +} + +} // namespace cvcuda::priv::jpeg + +#endif // CVCUDA_PRIV_JPEG_DISTORTION_MATH_HPP diff --git a/src/cvcuda/priv/Nvtx.hpp b/src/cvcuda/priv/Nvtx.hpp new file mode 100644 index 000000000..56a2bbc04 --- /dev/null +++ b/src/cvcuda/priv/Nvtx.hpp @@ -0,0 +1,51 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#ifndef CVCUDA_PRIV_NVTX_HPP +#define CVCUDA_PRIV_NVTX_HPP + +#include + +namespace cvcuda::priv::nvtx { + +class Range final +{ +public: + explicit Range(const char *name) noexcept + { + nvtxRangePushA(name); + } + + ~Range() noexcept + { + nvtxRangePop(); + } + + Range(const Range &) = delete; + Range(Range &&) = delete; + Range &operator=(const Range &) = delete; + Range &operator=(Range &&) = delete; +}; + +} // namespace cvcuda::priv::nvtx + +#define CVCUDA_NVTX_DETAIL_JOIN(a, b) a##b +#define CVCUDA_NVTX_DETAIL_MAKE_NAME(a, b) CVCUDA_NVTX_DETAIL_JOIN(a, b) +#define CVCUDA_NVTX_RANGE(name) \ + ::cvcuda::priv::nvtx::Range CVCUDA_NVTX_DETAIL_MAKE_NAME(cvcudaNvtxRange_, __COUNTER__)(name) + +#endif // CVCUDA_PRIV_NVTX_HPP diff --git a/src/cvcuda/priv/OpAdaptiveThreshold.cpp b/src/cvcuda/priv/OpAdaptiveThreshold.cpp index d512aecb7..2ca864b70 100644 --- a/src/cvcuda/priv/OpAdaptiveThreshold.cpp +++ b/src/cvcuda/priv/OpAdaptiveThreshold.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,8 @@ #include "OpAdaptiveThreshold.hpp" +#include "Nvtx.hpp" +#include "legacy/AdaptiveThresholdPolicy.hpp" #include "legacy/CvCudaLegacy.h" #include "legacy/CvCudaLegacyHelpers.hpp" @@ -27,18 +29,51 @@ namespace cvcuda::priv { namespace legacy = nvcv::legacy::cuda_op; +namespace { + +legacy::AdaptiveThresholdKernelPolicy AdaptiveThresholdKernelPolicyForDevice(int deviceId) +{ + int major = 0; + int minor = 0; + NVCV_CHECK_THROW(cudaDeviceGetAttribute(&major, cudaDevAttrComputeCapabilityMajor, deviceId)); + NVCV_CHECK_THROW(cudaDeviceGetAttribute(&minor, cudaDevAttrComputeCapabilityMinor, deviceId)); + + return legacy::AdaptiveThresholdKernelPolicyForSM(major * 10 + minor); +} + +} // namespace + AdaptiveThreshold::AdaptiveThreshold(int32_t maxBlockSize, int32_t maxVarShapeBatchSize) + // Legacy operators are single-device by design. PerDeviceResource creates + // one instance per CUDA device for transparent multi-GPU support. + : m_legacyOp( + [maxBlockSize](int deviceId) + { + legacy::DataShape maxIn; + legacy::DataShape maxOut; + return std::make_unique(maxIn, maxOut, maxBlockSize, + AdaptiveThresholdKernelPolicyForDevice(deviceId)); + }) + , m_legacyOpVarShape( + [maxBlockSize, maxVarShapeBatchSize](int deviceId) + { + legacy::DataShape maxIn; + legacy::DataShape maxOut; + return std::make_unique( + maxIn, maxOut, maxBlockSize, maxVarShapeBatchSize, AdaptiveThresholdKernelPolicyForDevice(deviceId)); + }) { - legacy::DataShape maxIn, maxOut; //maxIn/maxOut not used by op. - m_legacyOp = std::make_unique(maxIn, maxOut, maxBlockSize); - m_legacyOpVarShape - = std::make_unique(maxIn, maxOut, maxBlockSize, maxVarShapeBatchSize); + if (maxBlockSize <= 0) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "maxBlockSize must be > 0"); + } } void AdaptiveThreshold::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, const double maxValue, const NVCVAdaptiveThresholdType adaptiveMethod, const NVCVThresholdType thresholdType, const int32_t blockSize, const double c) const { + CVCUDA_NVTX_RANGE("cvcuda::AdaptiveThreshold::operator()[Tensor]"); auto inData = in.exportData(); if (inData == nullptr) { @@ -53,8 +88,20 @@ void AdaptiveThreshold::operator()(cudaStream_t stream, const nvcv::Tensor &in, "Output must be cuda-accessible, pitch-linear tensor"); } + if (inData->dtype() != nvcv::TYPE_U8) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input data type must be U8. Unsupported data type."); + } + + if (outData->dtype() != nvcv::TYPE_U8) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Output data type must be U8. Unsupported data type."); + } + NVCV_CHECK_THROW( - m_legacyOp->infer(*inData, *outData, maxValue, adaptiveMethod, thresholdType, blockSize, c, stream)); + m_legacyOp.get().infer(*inData, *outData, maxValue, adaptiveMethod, thresholdType, blockSize, c, stream)); } void AdaptiveThreshold::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, @@ -63,6 +110,7 @@ void AdaptiveThreshold::operator()(cudaStream_t stream, const nvcv::ImageBatchVa const NVCVThresholdType thresholdType, const nvcv::Tensor &blockSize, const nvcv::Tensor &c) const { + CVCUDA_NVTX_RANGE("cvcuda::AdaptiveThreshold::operator()[ImageBatchVarShape]"); auto inData = in.exportData(stream); if (inData == nullptr) { @@ -75,6 +123,18 @@ void AdaptiveThreshold::operator()(cudaStream_t stream, const nvcv::ImageBatchVa throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Output must be varshape image batch"); } + if (!inData->uniqueFormat() || inData->uniqueFormat().planeDataType(0) != nvcv::TYPE_U8) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input data type must be U8. Unsupported data type."); + } + + if (!outData->uniqueFormat() || outData->uniqueFormat().planeDataType(0) != nvcv::TYPE_U8) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Output data type must be U8. Unsupported data type."); + } + auto maxvalueData = maxValue.exportData(); if (maxvalueData == nullptr) { @@ -95,8 +155,8 @@ void AdaptiveThreshold::operator()(cudaStream_t stream, const nvcv::ImageBatchVa throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "C must be cuda-accessible, pitch-linear tensor"); } - NVCV_CHECK_THROW(m_legacyOpVarShape->infer(*inData, *outData, *maxvalueData, adaptiveMethod, thresholdType, - *blocksizeData, *cData, stream)); + NVCV_CHECK_THROW(m_legacyOpVarShape.get().infer(*inData, *outData, *maxvalueData, adaptiveMethod, thresholdType, + *blocksizeData, *cData, stream)); } } // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpAdaptiveThreshold.hpp b/src/cvcuda/priv/OpAdaptiveThreshold.hpp index 18c3a0681..48598dcde 100644 --- a/src/cvcuda/priv/OpAdaptiveThreshold.hpp +++ b/src/cvcuda/priv/OpAdaptiveThreshold.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,13 +25,12 @@ #define CVCUDA_PRIV_ADAPTIVETHRESHOLD_HPP #include "IOperator.hpp" +#include "PerDeviceResource.hpp" #include "legacy/CvCudaLegacy.h" #include #include -#include - namespace cvcuda::priv { class AdaptiveThreshold final : public IOperator @@ -47,8 +46,8 @@ class AdaptiveThreshold final : public IOperator const NVCVThresholdType thresholdType, const nvcv::Tensor &blockSize, const nvcv::Tensor &c) const; private: - std::unique_ptr m_legacyOp; - std::unique_ptr m_legacyOpVarShape; + mutable PerDeviceResource m_legacyOp; + mutable PerDeviceResource m_legacyOpVarShape; }; } // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpAdjustContrast.cu b/src/cvcuda/priv/OpAdjustContrast.cu new file mode 100644 index 000000000..47c73bb2d --- /dev/null +++ b/src/cvcuda/priv/OpAdjustContrast.cu @@ -0,0 +1,1358 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "Nvtx.hpp" +#include "OpAdjustContrast.hpp" + +#include "AdjustColorCommon.cuh" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace cuda = nvcv::cuda; +namespace util = nvcv::util; +namespace adjust = cvcuda::priv::adjust; + +namespace { + +inline void ValidateFactor(double factor) +{ + if (factor < 0.0) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Contrast factor must be non-negative"); + } +} + +constexpr int kBlockX = 32; +constexpr int kBlockY = 4; +constexpr int kApplyXSteps = 4; +constexpr int kMeanBlock = 256; // fixed legacy reduction width; must remain a power of two +constexpr int kF32MeanThreads = 64; +constexpr int64_t kF32MinParallelPixels = 8192; +constexpr int kMeanValuesPerThread = 32; +constexpr size_t kMaxMeanWorkspaceBytes = 8 * 1024 * 1024; +static_assert(kMeanBlock % kF32MeanThreads == 0); +static_assert(sizeof(float) == sizeof(uint32_t)); + +// Deterministic shared-memory tree reduction of a per-thread partial into means[blockIdx.z] = sum / +// count. One block per image (grid = {1, 1, numImages}); the fixed reduction order keeps results +// run-to-run deterministic (no atomics). +template +inline __device__ void WriteMean(float local, int64_t count, float *means) +{ + __shared__ float smem[BLOCK]; + const int t = threadIdx.x; + smem[t] = local; + __syncthreads(); +#pragma unroll + for (int s = BLOCK / 2; s > 0; s >>= 1) + { + if (t < s) + { + smem[t] += smem[t + s]; + } + __syncthreads(); + } + if (t == 0) + { + means[blockIdx.z] = smem[0] / static_cast(count); + } +} + +// Accumulate the grayscale value of one pixel position (x, y) of image z into `local`, dispatching +// on interleaved vs planar layout. Planar reads the channels from separate planes (numPlanes is 1 +// or 3); interleaved reads the vector pixel directly. +template +inline __device__ float PixelGray(const SrcWrapper &src, int x, int y, int z, int numPlanes, int3 rgbIndices) +{ + if constexpr (!IsPlanar) + { + const auto pixel = src[int3{x, y, z}]; + if constexpr (cuda::NumElements> == 1) + { + return static_cast(cuda::GetElement(pixel, 0)); + } + else + { + return adjust::GrayFromRGB(cuda::GetElement(pixel, rgbIndices.x), cuda::GetElement(pixel, rgbIndices.y), + cuda::GetElement(pixel, rgbIndices.z)); + } + } + else + { + // Planar wrappers carry a 1-element vector element type (uchar1 / float1); extract the base + // component before combining channels from separate planes. + if (numPlanes == 1) + { + return static_cast(cuda::GetElement(src[int4{x, y, 0, z}], 0)); + } + return adjust::GrayFromRGB(cuda::GetElement(src[int4{x, y, rgbIndices.x, z}], 0), + cuda::GetElement(src[int4{x, y, rgbIndices.y, z}], 0), + cuda::GetElement(src[int4{x, y, rgbIndices.z, z}], 0)); + } +} + +// Grayscale-mean reduction, tensor variant (uniform per-image size). +template +__global__ void ContrastMeanTensor(SrcWrapper src, int2 size, int numPlanes, int3 rgbIndices, float *means) +{ + using BT = cuda::BaseType>; + const int z = blockIdx.z; + const int64_t n = static_cast(size.x) * size.y; + + int64_t idx = threadIdx.x; + int x = static_cast(idx % size.x); + int y = static_cast(idx / size.x); + const int xStep = blockDim.x % size.x; + const int yStep = blockDim.x / size.x; + + float local = 0.0f; + for (; idx < n; idx += blockDim.x) + { + local += PixelGray(src, x, y, z, numPlanes, rgbIndices); + + x += xStep; + y += yStep; + if (x >= size.x) + { + x -= size.x; + ++y; + } + } + WriteMean(local, n, means); +} + +// Grayscale-mean reduction, var-shape variant (per-image size from the wrapper). +template +__global__ void ContrastMeanVarShape(SrcWrapper src, int numPlanes, int3 rgbIndices, float *means) +{ + using BT = cuda::BaseType>; + const int z = blockIdx.z; + const int w = src.width(z); + const int h = src.height(z); + const int64_t n = static_cast(w) * h; + + int64_t idx = threadIdx.x; + int x = static_cast(idx % w); + int y = static_cast(idx / w); + const int xStep = blockDim.x % w; + const int yStep = blockDim.x / w; + + float local = 0.0f; + for (; idx < n; idx += blockDim.x) + { + local += PixelGray(src, x, y, z, numPlanes, rgbIndices); + + x += xStep; + y += yStep; + if (x >= w) + { + x -= w; + ++y; + } + } + WriteMean(local, n, means); +} + +// Split the fixed F32 reduction lanes across two-warp CTAs without changing their arithmetic. +// Lane t still visits t, t + 256, ... in order; the final kernel below replays the original tree. +template +__global__ void ContrastMeanF32Tensor(SrcWrapper src, int2 size, int numPlanes, int3 rgbIndices, float *laneSums) +{ + using BT = cuda::BaseType>; + static_assert(std::is_same_v); + + const int z = blockIdx.z; + const int lane = blockIdx.x * blockDim.x + threadIdx.x; + const int64_t n = static_cast(size.x) * size.y; + + int64_t idx = lane; + int x = static_cast(idx % size.x); + int y = static_cast(idx / size.x); + const int xStep = BLOCK % size.x; + const int yStep = BLOCK / size.x; + + float local = 0.0f; + for (; idx < n; idx += BLOCK) + { + local += PixelGray(src, x, y, z, numPlanes, rgbIndices); + + x += xStep; + y += yStep; + if (x >= size.x) + { + x -= size.x; + ++y; + } + } + laneSums[static_cast(z) * BLOCK + lane] = local; +} + +template +__global__ void ContrastMeanF32VarShape(SrcWrapper src, int numPlanes, int3 rgbIndices, float *laneSums) +{ + using BT = cuda::BaseType>; + static_assert(std::is_same_v); + + const int z = blockIdx.z; + const int lane = blockIdx.x * blockDim.x + threadIdx.x; + const int w = src.width(z); + const int h = src.height(z); + const int64_t n = static_cast(w) * h; + +#if __CUDA_ARCH__ == 900 + // Direct planar addressing keeps independent RGB loads in flight on sm_90; its wrapper + // subscript codegen serializes each triplet. Other architectures retain their faster wrapper path. + NVCVImagePlaneStrided redPlane{}; + NVCVImagePlaneStrided greenPlane{}; + NVCVImagePlaneStrided bluePlane{}; + if constexpr (IsPlanar) + { + redPlane = src.plane(z, rgbIndices.x); + greenPlane = src.plane(z, rgbIndices.y); + bluePlane = src.plane(z, rgbIndices.z); + } +#endif + + int64_t idx = lane; + int x = static_cast(idx % w); + int y = static_cast(idx / w); + const int xStep = BLOCK % w; + const int yStep = BLOCK / w; + + float local = 0.0f; + for (; idx < n; idx += BLOCK) + { +#if __CUDA_ARCH__ == 900 + if constexpr (IsPlanar) + { + const int redOffset = y * redPlane.rowStride + x * sizeof(BT); + const int greenOffset = y * greenPlane.rowStride + x * sizeof(BT); + const int blueOffset = y * bluePlane.rowStride + x * sizeof(BT); + const BT red = *reinterpret_cast(redPlane.basePtr + redOffset); + const BT green = *reinterpret_cast(greenPlane.basePtr + greenOffset); + const BT blue = *reinterpret_cast(bluePlane.basePtr + blueOffset); + local += adjust::GrayFromRGB(red, green, blue); + } + else + { + local += PixelGray(src, x, y, z, numPlanes, rgbIndices); + } +#else + local += PixelGray(src, x, y, z, numPlanes, rgbIndices); +#endif + + x += xStep; + y += yStep; + if (x >= w) + { + x -= w; + ++y; + } + } + laneSums[static_cast(z) * BLOCK + lane] = local; +} + +template +__global__ void FinalizeMeanF32Tensor(const float *laneSums, int64_t count, float *means) +{ + const int z = blockIdx.z; + const float local = laneSums[static_cast(z) * BLOCK + threadIdx.x]; + WriteMean(local, count, means); +} + +template +__global__ void FinalizeMeanF32VarShape(SrcWrapper src, const float *laneSums, float *means) +{ + const int z = blockIdx.z; + const float local = laneSums[static_cast(z) * BLOCK + threadIdx.x]; + const int64_t count = static_cast(src.width(z)) * src.height(z); + WriteMean(local, count, means); +} + +// Parallel U8 first pass. A thread always owns the same lane as in ContrastMean*, and each logical +// chunk begins on a multiple of BLOCK. Consequently, the final pass can reconstruct the exact 256 +// per-lane values used by the original reduction tree. +template +__global__ void ContrastMeanU8Tensor(SrcWrapper src, int2 size, int numPlanes, int3 rgbIndices, uint32_t *partials) +{ + using BT = cuda::BaseType>; + static_assert(std::is_same_v); + + constexpr int64_t chunkPixels = static_cast(BLOCK) * kMeanValuesPerThread; + const int z = blockIdx.z; + const int64_t n = static_cast(size.x) * size.y; + const int xStep = BLOCK % size.x; + const int yStep = BLOCK / size.x; + uint32_t local = 0; + + for (int64_t base = static_cast(blockIdx.x) * chunkPixels + threadIdx.x; base < n; + base += static_cast(gridDim.x) * chunkPixels) + { + int64_t idx = base; + int x = static_cast(idx % size.x); + int y = static_cast(idx / size.x); +#pragma unroll + for (int i = 0; i < kMeanValuesPerThread; ++i) + { + if (idx < n) + { + local += static_cast(PixelGray(src, x, y, z, numPlanes, rgbIndices)); + } + idx += BLOCK; + x += xStep; + y += yStep; + if (x >= size.x) + { + x -= size.x; + ++y; + } + } + } + + const size_t partial = (static_cast(z) * gridDim.x + blockIdx.x) * BLOCK + threadIdx.x; + partials[partial] = local; +} + +template +__global__ void ContrastMeanU8VarShape(SrcWrapper src, int numPlanes, int3 rgbIndices, uint32_t *partials) +{ + using BT = cuda::BaseType>; + static_assert(std::is_same_v); + + constexpr int64_t chunkPixels = static_cast(BLOCK) * kMeanValuesPerThread; + const int z = blockIdx.z; + const int w = src.width(z); + const int h = src.height(z); + const int64_t n = static_cast(w) * h; + const int xStep = BLOCK % w; + const int yStep = BLOCK / w; + uint32_t local = 0; + + for (int64_t base = static_cast(blockIdx.x) * chunkPixels + threadIdx.x; base < n; + base += static_cast(gridDim.x) * chunkPixels) + { + int64_t idx = base; + int x = static_cast(idx % w); + int y = static_cast(idx / w); +#pragma unroll + for (int i = 0; i < kMeanValuesPerThread; ++i) + { + if (idx < n) + { + local += static_cast(PixelGray(src, x, y, z, numPlanes, rgbIndices)); + } + idx += BLOCK; + x += xStep; + y += yStep; + if (x >= w) + { + x -= w; + ++y; + } + } + } + + const size_t partial = (static_cast(z) * gridDim.x + blockIdx.x) * BLOCK + threadIdx.x; + partials[partial] = local; +} + +template +__global__ void FinalizeMeanU8Tensor(const uint32_t *partials, int numPartials, int64_t count, float *means) +{ + const int z = blockIdx.z; + uint32_t local{}; + for (int p = 0; p < numPartials; ++p) + { + const size_t offset = (static_cast(z) * numPartials + p) * BLOCK + threadIdx.x; + local += partials[offset]; + } + WriteMean(static_cast(local), count, means); +} + +template +__global__ void FinalizeMeanU8VarShape(SrcWrapper src, const uint32_t *partials, int numPartials, float *means) +{ + const int z = blockIdx.z; + uint32_t local{}; + for (int p = 0; p < numPartials; ++p) + { + const size_t offset = (static_cast(z) * numPartials + p) * BLOCK + threadIdx.x; + local += partials[offset]; + } + const int64_t count = static_cast(src.width(z)) * src.height(z); + WriteMean(static_cast(local), count, means); +} + +// out = clamp(factor * in + (1 - factor) * mean, 0, bound) per channel component. +template +inline __device__ T AdjustContrastPixel(T pixel, float factor, float mean) +{ + using BT = cuda::BaseType; + static constexpr int numChannels = cuda::NumElements; + const float bound = adjust::Bound(); + + T out{}; +#pragma unroll + for (int c = 0; c < numChannels; ++c) + { + cuda::GetElement(out, c) = adjust::Blend(cuda::GetElement(pixel, c), factor, mean, bound); + } + return out; +} + +template +__global__ void ApplyTensor(SrcWrapper src, DstWrapper dst, int2 size, int numPlanes, float factor, const float *means) +{ + using SrcT = std::remove_const_t; + using DstT = typename DstWrapper::ValueType; + static constexpr int numChannels = cuda::NumElements; + static_assert(numChannels == cuda::NumElements); + static_assert(!IsPlanar || numChannels == 1); + + const int x0 = blockIdx.x * blockDim.x * kApplyXSteps + threadIdx.x; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const int z = blockIdx.z; + if (y >= size.y) + { + return; + } + + const float mean = means[z]; + if constexpr (!IsPlanar) + { +#pragma unroll + for (int i = 0; i < kApplyXSteps; ++i) + { + const int x = x0 + i * blockDim.x; + if (x < size.x) + { + const int3 coord{x, y, z}; + dst[coord] = AdjustContrastPixel(src[coord], factor, mean); + } + } + } + else + { + for (int p = 0; p < numPlanes; ++p) + { +#pragma unroll + for (int i = 0; i < kApplyXSteps; ++i) + { + const int x = x0 + i * blockDim.x; + if (x < size.x) + { + const int4 coord{x, y, p, z}; + dst[coord] = AdjustContrastPixel(src[coord], factor, mean); + } + } + } + } +} + +template +__global__ void ApplyVarShape(SrcWrapper src, DstWrapper dst, int numPlanes, float factor, const float *means) +{ + using SrcT = std::remove_const_t; + using DstT = typename DstWrapper::ValueType; + static constexpr int numChannels = cuda::NumElements; + static_assert(numChannels == cuda::NumElements); + static_assert(!IsPlanar || numChannels == 1); + + const int z = blockIdx.z; + const int width = dst.width(z); + const int height = dst.height(z); + const int x0 = blockIdx.x * blockDim.x * kApplyXSteps + threadIdx.x; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + if (y >= height) + { + return; + } + + const float mean = means[z]; + if constexpr (!IsPlanar) + { +#pragma unroll + for (int i = 0; i < kApplyXSteps; ++i) + { + const int x = x0 + i * blockDim.x; + if (x < width) + { + const int3 coord{x, y, z}; + dst[coord] = AdjustContrastPixel(src[coord], factor, mean); + } + } + } + else + { + for (int p = 0; p < numPlanes; ++p) + { +#pragma unroll + for (int i = 0; i < kApplyXSteps; ++i) + { + const int x = x0 + i * blockDim.x; + if (x < width) + { + const int4 coord{x, y, p, z}; + dst[coord] = AdjustContrastPixel(src[coord], factor, mean); + } + } + } + } +} + +struct ReductionGrid +{ + dim3 launch; + bool parallel; +}; + +struct ReductionWorkspace +{ + uint32_t *partials; + float *means; + size_t numPartials; +}; + +inline size_t CheckedWorkspaceMul(size_t a, size_t b) +{ + if (a != 0 && b > std::numeric_limits::max() / a) + { + throw nvcv::Exception(nvcv::Status::ERROR_OVERFLOW, "AdjustContrast reduction workspace size overflows size_t"); + } + return a * b; +} + +inline size_t CheckedWorkspaceAdd(size_t a, size_t b) +{ + if (b > std::numeric_limits::max() - a) + { + throw nvcv::Exception(nvcv::Status::ERROR_OVERFLOW, "AdjustContrast reduction workspace size overflows size_t"); + } + return a + b; +} + +inline ReductionGrid GetReductionGrid(int64_t pixels, int numSamples, bool isU8) +{ + const dim3 fallback{1, 1, static_cast(numSamples)}; + constexpr int64_t chunkPixels = static_cast(kMeanBlock) * kMeanValuesPerThread; + + const size_t meansBytes = CheckedWorkspaceMul(static_cast(numSamples), sizeof(float)); + + if (!isU8) + { + const size_t partialBytes = CheckedWorkspaceMul( + CheckedWorkspaceMul(static_cast(numSamples), static_cast(kMeanBlock)), sizeof(float)); + if (pixels <= kF32MinParallelPixels || CheckedWorkspaceAdd(partialBytes, meansBytes) > kMaxMeanWorkspaceBytes) + { + return {fallback, false}; + } + return {{{kMeanBlock / kF32MeanThreads, 1, static_cast(numSamples)}}, true}; + } + + // The final pass casts each complete legacy lane sum to float once. Keep that cast exact so the + // unchanged 256-lane float tree produces the same bits as the original one-CTA reduction. + const int64_t maxTermsPerLane = (static_cast(1) << 24) / 255; + if (pixels <= chunkPixels || util::DivUp(pixels, static_cast(kMeanBlock)) > maxTermsPerLane) + { + return {fallback, false}; + } + + const size_t onePartialBytes = CheckedWorkspaceMul( + CheckedWorkspaceMul(static_cast(numSamples), static_cast(kMeanBlock)), sizeof(uint32_t)); + if (meansBytes >= kMaxMeanWorkspaceBytes) + { + return {fallback, false}; + } + + const size_t maxPartials = (kMaxMeanWorkspaceBytes - meansBytes) / onePartialBytes; + const size_t logicalPartials = static_cast(util::DivUp(pixels, static_cast(chunkPixels))); + const size_t numPartials = std::min(logicalPartials, maxPartials); + // One partial exposes no more CTAs than the fallback and adds a finalization launch. + if (numPartials <= 1) + { + return {fallback, false}; + } + + return {{{static_cast(numPartials), 1, static_cast(numSamples)}}, true}; +} + +inline dim3 GetApplyGrid(int width, int height, int numSamples) +{ + const int64_t gridX = util::DivUp(static_cast(width), static_cast(kBlockX) * kApplyXSteps); + const int64_t gridY = util::DivUp(static_cast(height), static_cast(kBlockY)); + if (gridX > cuda::TypeTraits::max || gridY > 65535) + { + throw nvcv::Exception(nvcv::Status::ERROR_OVERFLOW, "AdjustContrast apply grid exceeds the CUDA launch limits"); + } + return {static_cast(gridX), static_cast(gridY), static_cast(numSamples)}; +} + +template +inline ReductionWorkspace GetReductionWorkspace(Workspace &ws, cudaStream_t stream, int numSamples, ReductionGrid grid, + bool isU8) +{ + const size_t numPartials = grid.parallel ? grid.launch.x : 0; + const size_t partialsPerSample + = grid.parallel ? (isU8 ? CheckedWorkspaceMul(numPartials, kMeanBlock) : kMeanBlock) : 0; + const size_t partialCount = CheckedWorkspaceMul(static_cast(numSamples), partialsPerSample); + const size_t partialBytes = CheckedWorkspaceMul(partialCount, sizeof(uint32_t)); + const size_t meansBytes = CheckedWorkspaceMul(static_cast(numSamples), sizeof(float)); + auto *buf = static_cast(ws.acquire(CheckedWorkspaceAdd(partialBytes, meansBytes), stream)); + + return {reinterpret_cast(buf), reinterpret_cast(buf + partialBytes), numPartials}; +} + +template +class WorkspaceReleaseGuard +{ +public: + WorkspaceReleaseGuard(Workspace &workspace, cudaStream_t stream) + : m_workspace(workspace) + , m_stream(stream) + { + } + + ~WorkspaceReleaseGuard() + { + if (m_active) + { + m_workspace.releaseNoThrow(m_stream); + } + } + + void finish() + { + m_workspace.release(m_stream); + m_active = false; + } + +private: + Workspace &m_workspace; + cudaStream_t m_stream; + bool m_active = true; +}; + +// cudaMallocAsync/freeAsync become graph allocation nodes during stream capture. Keep that existing +// behavior for captured submissions; ordinary submissions use the retained AutoContrast-style +// workspace below. +class CapturedWorkspace +{ +public: + void *acquire(size_t sizeBytes, cudaStream_t stream) + { + NVCV_CHECK_THROW(cudaMallocAsync(&m_data, sizeBytes, stream)); + return m_data; + } + + void release(cudaStream_t stream) + { + NVCV_CHECK_THROW(cudaFreeAsync(m_data, stream)); + m_data = nullptr; + } + + void releaseNoThrow(cudaStream_t stream) noexcept + { + if (m_data != nullptr) + { + NVCV_CHECK_LOG(cudaFreeAsync(m_data, stream)); + m_data = nullptr; + } + } + +private: + void *m_data = nullptr; +}; + +template +inline void RunTensor(cudaStream_t stream, Workspace &ws, const nvcv::TensorDataStridedCuda &srcData, + const nvcv::TensorDataStridedCuda &dstData, float factor, int numPlanes, int3 rgbIndices) +{ + auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(srcData); + auto dstAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(dstData); + int2 size = cuda::StaticCast(long2{srcAccess->numCols(), srcAccess->numRows()}); + const int numSamples = srcAccess->numSamples(); + + if (numSamples > 65535) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Batch size exceeds the CUDA grid.z limit of 65535"); + } + + int64_t inMaxStride = srcAccess->sampleStride() * srcAccess->numSamples(); + int64_t outMaxStride = dstAccess->sampleStride() * dstAccess->numSamples(); + if (std::max(inMaxStride, outMaxStride) > cuda::TypeTraits::max) + { + throw nvcv::Exception(nvcv::Status::ERROR_OVERFLOW, "Input or output size exceeds %d. Tensor is too large.", + cuda::TypeTraits::max); + } + using StrideType = int32_t; + + using BT = cuda::BaseType; + constexpr bool isU8 = std::is_same_v; + const int64_t pixels = static_cast(size.x) * size.y; + auto reduceGrid = GetReductionGrid(pixels, numSamples, isU8); + auto reduction = GetReductionWorkspace(ws, stream, numSamples, reduceGrid, isU8); + WorkspaceReleaseGuard releaseGuard(ws, stream); + + dim3 block(kBlockX, kBlockY, 1); + dim3 meanGrid(1, 1, numSamples); + dim3 applyGrid = GetApplyGrid(size.x, size.y, numSamples); + + if constexpr (!IsPlanar) + { + auto src = cuda::CreateTensorWrapNHW(srcData); + auto dst = cuda::CreateTensorWrapNHW(dstData); + if constexpr (isU8) + { + if (reduceGrid.parallel) + { + ContrastMeanU8Tensor + <<>>(src, size, 1, rgbIndices, reduction.partials); + FinalizeMeanU8Tensor<<>>( + reduction.partials, reduction.numPartials, pixels, reduction.means); + } + else + { + ContrastMeanTensor + <<>>(src, size, 1, rgbIndices, reduction.means); + } + } + else + { + if (reduceGrid.parallel) + { + auto *laneSums = reinterpret_cast(reduction.partials); + ContrastMeanF32Tensor + <<>>(src, size, 1, rgbIndices, laneSums); + FinalizeMeanF32Tensor + <<>>(laneSums, pixels, reduction.means); + } + else + { + ContrastMeanTensor + <<>>(src, size, 1, rgbIndices, reduction.means); + } + } + ApplyTensor<<>>(src, dst, size, 1, factor, reduction.means); + } + else + { + auto src = cuda::Tensor4DWrap( + srcData.basePtr(), static_cast(srcAccess->sampleStride()), static_cast(srcAccess->planeStride()), + static_cast(srcAccess->rowStride())); + auto dst = cuda::Tensor4DWrap( + dstData.basePtr(), static_cast(dstAccess->sampleStride()), static_cast(dstAccess->planeStride()), + static_cast(dstAccess->rowStride())); + if constexpr (isU8) + { + if (reduceGrid.parallel) + { + ContrastMeanU8Tensor<<>>( + src, size, numPlanes, rgbIndices, reduction.partials); + FinalizeMeanU8Tensor<<>>( + reduction.partials, reduction.numPartials, pixels, reduction.means); + } + else + { + ContrastMeanTensor + <<>>(src, size, numPlanes, rgbIndices, reduction.means); + } + } + else + { + if (reduceGrid.parallel) + { + auto *laneSums = reinterpret_cast(reduction.partials); + ContrastMeanF32Tensor + <<>>(src, size, numPlanes, rgbIndices, laneSums); + FinalizeMeanF32Tensor + <<>>(laneSums, pixels, reduction.means); + } + else + { + ContrastMeanTensor + <<>>(src, size, numPlanes, rgbIndices, reduction.means); + } + } + ApplyTensor<<>>(src, dst, size, numPlanes, factor, reduction.means); + } + NVCV_CHECK_THROW(cudaGetLastError()); + releaseGuard.finish(); +} + +template +inline void RunVarShapeBatch(cudaStream_t stream, Workspace &ws, const nvcv::ImageBatchVarShapeDataStridedCuda &srcData, + const nvcv::ImageBatchVarShapeDataStridedCuda &dstData, float factor, int numPlanes, + int3 rgbIndices, int64_t maxPixels) +{ + const int numImages = dstData.numImages(); + if (numImages > 65535) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Batch size exceeds the CUDA grid.z limit of 65535"); + } + + using BT = cuda::BaseType; + constexpr bool isU8 = std::is_same_v; + int3 maxSize{dstData.maxSize().w, dstData.maxSize().h, numImages}; + auto reduceGrid = GetReductionGrid(maxPixels, numImages, isU8); + auto reduction = GetReductionWorkspace(ws, stream, numImages, reduceGrid, isU8); + WorkspaceReleaseGuard releaseGuard(ws, stream); + + dim3 block(kBlockX, kBlockY, 1); + dim3 meanGrid(1, 1, numImages); + dim3 applyGrid = GetApplyGrid(maxSize.x, maxSize.y, numImages); + + cuda::ImageBatchVarShapeWrap src(srcData); + cuda::ImageBatchVarShapeWrap dst(dstData); + + if constexpr (isU8) + { + if (reduceGrid.parallel) + { + ContrastMeanU8VarShape + <<>>(src, numPlanes, rgbIndices, reduction.partials); + FinalizeMeanU8VarShape + <<>>(src, reduction.partials, reduction.numPartials, reduction.means); + } + else + { + ContrastMeanVarShape + <<>>(src, numPlanes, rgbIndices, reduction.means); + } + } + else + { + if (reduceGrid.parallel) + { + auto *laneSums = reinterpret_cast(reduction.partials); + ContrastMeanF32VarShape + <<>>(src, numPlanes, rgbIndices, laneSums); + FinalizeMeanF32VarShape<<>>(src, laneSums, reduction.means); + } + else + { + ContrastMeanVarShape + <<>>(src, numPlanes, rgbIndices, reduction.means); + } + } + ApplyVarShape<<>>(src, dst, numPlanes, factor, reduction.means); + NVCV_CHECK_THROW(cudaGetLastError()); + releaseGuard.finish(); +} + +// Dispatch over base data type (u8 / f32) and channel count (1 / 3) ---------------------- + +template +inline void RunTypeSwitch(nvcv::DataType dType, const Cb &cb) +{ + using uchar = unsigned char; + +#define NVCV_ADJUST_CONTRAST_RUN_TYPED(DYN_BASE_TYPE, STATIC_BASE_TYPE) \ + ((dType == nvcv::TYPE_3##DYN_BASE_TYPE) || (dType == nvcv::TYPE_##DYN_BASE_TYPE)) cb(STATIC_BASE_TYPE{}); + + // clang-format off + if NVCV_ADJUST_CONTRAST_RUN_TYPED(U8, uchar) + else if NVCV_ADJUST_CONTRAST_RUN_TYPED(F32, float) + else + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Invalid data type: AdjustContrast supports 8-bit unsigned and 32-bit float"); + } + // clang-format on + +#undef NVCV_ADJUST_CONTRAST_RUN_TYPED +} + +template +inline void RunChannelSwitch(int numChannels, int numPlanes, nvcv::DataType dType, const Cb &cb) +{ + RunTypeSwitch(dType, + [&numChannels, &numPlanes, &cb](auto dummyVal) + { + using ValBase = decltype(dummyVal); + // clang-format off + if (numChannels == 1) + { + using Val = cuda::MakeType; + if (numPlanes == 1) + { + cb(Val{}, std::integral_constant{}); + } + else if (numPlanes == 3) + { + cb(Val{}, std::integral_constant{}); + } + else + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Invalid number of channels: AdjustContrast supports 1 or 3 channels"); + } + } + else if (numChannels == 3) + { + cb(cuda::MakeType{}, std::integral_constant{}); + } + else + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Invalid number of channels: AdjustContrast supports 1 or 3 channels"); + } + // clang-format on + }); +} + +// Validation ------------------------------------------------------------------------------ + +inline bool ValidateSrcDstTensors(int &numInterleavedChannels, int &numPlanes, nvcv::DataType &dtype, + const nvcv::Optional &srcData, + const nvcv::Optional &dstData) +{ + if (!srcData) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input must be cuda-accessible, pitch-linear tensor"); + } + if (!dstData) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Output must be cuda-accessible, pitch-linear tensor"); + } + if (srcData->layout() != dstData->layout()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input and output must have the same layout"); + } + if (!(srcData->layout() == nvcv::TENSOR_HWC || srcData->layout() == nvcv::TENSOR_NHWC + || srcData->layout() == nvcv::TENSOR_CHW || srcData->layout() == nvcv::TENSOR_NCHW)) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input must have (N)HWC or (N)CHW layout"); + } + if (srcData->dtype() != dstData->dtype()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input and output must have the same data type"); + } + if (srcData->dtype().numChannels() != 1) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Tensor data type must be scalar; use the C dimension for image channels"); + } + + auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcData); + auto dstAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*dstData); + NVCV_ASSERT(srcAccess && dstAccess); + + if (srcAccess->numSamples() != dstAccess->numSamples()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of samples"); + } + + int numChannels = srcAccess->numChannels(); + if (numChannels != dstAccess->numChannels()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of channels"); + } + + numPlanes = srcAccess->numPlanes(); + if (numPlanes != dstAccess->numPlanes()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of planes"); + } + if (numChannels != 1 && numChannels != 3) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Invalid number of channels: AdjustContrast supports 1 or 3 channels"); + } + if (numPlanes != 1 && numPlanes != 3) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Invalid number of planes: AdjustContrast supports 1 or 3 planes"); + } + + if (srcAccess->numCols() != dstAccess->numCols() || srcAccess->numRows() != dstAccess->numRows()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input and output must have matching width and height"); + } + + const int64_t elementStride = srcData->dtype().strideBytes(); + const int64_t expectedColStride = elementStride * (srcAccess->infoLayout().isChannelLast() ? numChannels : 1); + if (srcAccess->colStride() != expectedColStride || dstAccess->colStride() != expectedColStride + || (srcAccess->infoLayout().isChannelLast() + && (srcAccess->chStride() != elementStride || dstAccess->chStride() != elementStride))) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Tensor pixels must have packed width and channel strides"); + } + + dtype = srcData->dtype(); + numInterleavedChannels = srcAccess->infoLayout().isChannelLast() ? numChannels : 1; + return srcAccess->numSamples() == 0 || srcAccess->numCols() == 0 || srcAccess->numRows() == 0; +} + +inline auto ValidateSrcDstVarBatch(int &numInterleavedChannels, int &numPlanes, int3 &rgbIndices, nvcv::DataType &dtype, + int64_t &maxPixels, cudaStream_t stream, const nvcv::ImageBatchVarShape &src, + const nvcv::ImageBatchVarShape &dst) +{ + using maybeVarShape = nvcv::Optional; + std::tuple srcDstData{ + src.exportData(stream), + dst.exportData(stream)}; + auto &[srcData, dstData] = srcDstData; + + if (!srcData) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input must be cuda-accessible, varshape pitch-linear image batch"); + } + if (!dstData) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Output must be cuda-accessible, varshape pitch-linear image batch"); + } + + int numSamples = srcData->numImages(); + if (numSamples != dstData->numImages()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of samples"); + } + if (numSamples == 0) + { + return srcDstData; + } + + const auto &srcFormat = srcData->uniqueFormat(); + const auto &dstFormat = dstData->uniqueFormat(); + if (!srcFormat || !dstFormat) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "All images in a batch must have the same format"); + } + if (srcFormat != dstFormat) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input and output must have the same format"); + } + + const int numChannels = srcFormat.numChannels(); + if (numChannels != 1 && numChannels != 3) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Invalid number of channels: AdjustContrast supports 1 or 3 channels"); + } + + nvcv::ExtraChannelInfo extraChannels{}; + srcFormat.extraChannelInfo(&extraChannels); + if (extraChannels.numChannels != 0) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Image formats with extra channels are not supported"); + } + if (srcFormat.chromaSubsampling() != nvcv::ChromaSubsampling::CSS_444) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Chroma-subsampled image formats are not supported"); + } + + numPlanes = srcFormat.numPlanes(); + + dtype = srcFormat.planeDataType(0); + if (numPlanes == 1) + { + if (dtype.numChannels() != numChannels || srcFormat.planePixelStrideBytes(0) != dtype.strideBytes()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Single-plane images must have one packed element per pixel"); + } + } + else if (numPlanes != numChannels || dtype.numChannels() != 1) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Planar images must have one scalar, full-resolution plane per channel"); + } + for (int i = 1; i < numPlanes; ++i) + { + if (dtype != srcFormat.planeDataType(i) || srcFormat.planeDataType(i).numChannels() != 1) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "All planes in the input image must have the same data type"); + } + } + + numInterleavedChannels = dtype.numChannels(); + + rgbIndices = int3{0, 1, 2}; + if (numChannels == 3) + { + if (srcFormat.colorModel() != nvcv::ColorModel::RGB && srcFormat.colorModel() != nvcv::ColorModel::UNDEFINED) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Three-channel images must use the RGB color model"); + } + + const auto swizzleChannels = nvcv::GetChannels(srcFormat.swizzle()); + auto channelIndex = [](nvcv::Channel channel) + { + const int index = static_cast(channel) - static_cast(nvcv::Channel::X); + if (index < 0 || index >= 3) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Three-channel images must use an RGB channel permutation"); + } + return index; + }; + rgbIndices = int3{channelIndex(swizzleChannels[0]), channelIndex(swizzleChannels[1]), + channelIndex(swizzleChannels[2])}; + if (rgbIndices.x == rgbIndices.y || rgbIndices.x == rgbIndices.z || rgbIndices.y == rgbIndices.z) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Three-channel images must use an RGB channel permutation"); + } + } + + for (int i = 0; i < numSamples; i++) + { + const nvcv::Size2D size = src[i].size(); + if (size != dst[i].size()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input and output must have matching width and height"); + } + if (size.w <= 0 || size.h <= 0) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Image width and height must be greater than zero"); + } + maxPixels = std::max(maxPixels, static_cast(size.w) * size.h); + const nvcv::Size2D plane0Size = srcFormat.planeSize(size, 0); + for (int p = 1; p < numPlanes; ++p) + { + if (srcFormat.planeSize(size, p) != plane0Size) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "All image planes must have matching width and height"); + } + } + } + + return srcDstData; +} + +inline bool IsStreamCapturing(cudaStream_t stream) +{ + cudaStreamCaptureStatus captureStatus; + NVCV_CHECK_THROW(cudaStreamIsCapturing(stream, &captureStatus)); + return captureStatus != cudaStreamCaptureStatusNone; +} + +template +inline void RunWithWorkspace(cudaStream_t stream, bool isCapturing, + cvcuda::priv::PerDeviceResource &resources, + const Callback &cb) +{ + if (isCapturing) + { + CapturedWorkspace workspace; + cb(workspace); + } + else + { + auto &workspace = resources.get(); + auto lock = workspace.lock(); + cb(workspace); + } +} + +} // anonymous namespace + +namespace cvcuda::priv { + +// --------------------------------- Workspace ------------------------------- + +AdjustContrastWorkspace::AdjustContrastWorkspace() +{ + NVCV_CHECK_THROW(cudaEventCreateWithFlags(&m_ready, cudaEventDisableTiming)); +} + +AdjustContrastWorkspace::~AdjustContrastWorkspace() +{ + if (m_pending) + { + NVCV_CHECK_LOG(cudaEventSynchronize(m_ready)); + } + if (m_data != nullptr) + { + NVCV_CHECK_LOG(cudaFree(m_data)); + } + if (m_ready != nullptr) + { + NVCV_CHECK_LOG(cudaEventDestroy(m_ready)); + } +} + +[[nodiscard]] std::unique_lock AdjustContrastWorkspace::lock() +{ + return std::unique_lock{m_mutex}; +} + +void *AdjustContrastWorkspace::acquire(size_t sizeBytes, cudaStream_t stream) +{ + if (m_pending) + { + if (sizeBytes > m_capacity) + { + NVCV_CHECK_THROW(cudaEventSynchronize(m_ready)); + m_pending = false; + } + else + { + NVCV_CHECK_THROW(cudaStreamWaitEvent(stream, m_ready)); + } + } + if (sizeBytes > m_capacity) + { + if (m_data != nullptr) + { + NVCV_CHECK_THROW(cudaFree(m_data)); + m_data = nullptr; + m_capacity = 0; + } + if (cudaMalloc(&m_data, sizeBytes) != cudaSuccess) + { + throw nvcv::Exception(nvcv::Status::ERROR_OUT_OF_MEMORY, + "AdjustContrast: failed to allocate reduction workspace"); + } + m_capacity = sizeBytes; + } + return m_data; +} + +void AdjustContrastWorkspace::release(cudaStream_t stream) +{ + NVCV_CHECK_THROW(cudaEventRecord(m_ready, stream)); + m_pending = true; +} + +void AdjustContrastWorkspace::releaseNoThrow(cudaStream_t stream) noexcept +{ + cudaError_t err = cudaEventRecord(m_ready, stream); + if (err == cudaSuccess) + { + m_pending = true; + } + NVCV_CHECK_LOG(err); +} + +// --------------------------------- Operator -------------------------------- + +AdjustContrast::AdjustContrast() + : m_workspace([](int) { return std::make_unique(); }) +{ +} + +// Tensor input variant +void AdjustContrast::operator()(cudaStream_t stream, const nvcv::Tensor &src, const nvcv::Tensor &dst, + double contrastFactor) const +{ + CVCUDA_NVTX_RANGE("cvcuda::AdjustContrast::operator()[Tensor]"); + ValidateFactor(contrastFactor); + int numInterleavedChannels; + int numPlanes; + nvcv::DataType dtype; + auto srcData = src.exportData(); + auto dstData = dst.exportData(); + const bool isEmpty = ValidateSrcDstTensors(numInterleavedChannels, numPlanes, dtype, srcData, dstData); + + if (isEmpty) + { + RunChannelSwitch(numInterleavedChannels, numPlanes, dtype, [](auto, auto) {}); + return; + } + + const float factor = static_cast(contrastFactor); + const int3 rgbIndices{0, 1, 2}; + const bool isCapturing = IsStreamCapturing(stream); + + RunChannelSwitch( + numInterleavedChannels, numPlanes, dtype, + [this, &stream, &srcData, &dstData, factor, numPlanes, rgbIndices, isCapturing](auto dummyVal, auto isPlanar) + { + using ValueT = decltype(dummyVal); + using IsPlanar = decltype(isPlanar); + RunWithWorkspace(stream, isCapturing, m_workspace, + [&](auto &workspace) { + RunTensor(stream, workspace, *srcData, *dstData, factor, + numPlanes, rgbIndices); + }); + }); +} + +// VarShape input variant +void AdjustContrast::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &src, + const nvcv::ImageBatchVarShape &dst, double contrastFactor) const +{ + CVCUDA_NVTX_RANGE("cvcuda::AdjustContrast::operator()[ImageBatchVarShape]"); + ValidateFactor(contrastFactor); + int numInterleavedChannels; + int numPlanes; + int3 rgbIndices; + nvcv::DataType dtype; + int64_t maxPixels{}; + auto srcDstData + = ValidateSrcDstVarBatch(numInterleavedChannels, numPlanes, rgbIndices, dtype, maxPixels, stream, src, dst); + if (src.numImages() == 0) + { + return; + } + + const float factor = static_cast(contrastFactor); + const bool isCapturing = IsStreamCapturing(stream); + + RunChannelSwitch(numInterleavedChannels, numPlanes, dtype, + [this, &stream, &srcDstData, factor, numPlanes, rgbIndices, maxPixels, isCapturing](auto dummyVal, + auto isPlanar) + { + using ValueT = decltype(dummyVal); + using IsPlanar = decltype(isPlanar); + RunWithWorkspace(stream, isCapturing, m_workspace, + [&](auto &workspace) + { + RunVarShapeBatch( + stream, workspace, *std::get<0>(srcDstData), *std::get<1>(srcDstData), + factor, numPlanes, rgbIndices, maxPixels); + }); + }); +} + +} // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpAdjustContrast.hpp b/src/cvcuda/priv/OpAdjustContrast.hpp new file mode 100644 index 000000000..5477685f6 --- /dev/null +++ b/src/cvcuda/priv/OpAdjustContrast.hpp @@ -0,0 +1,80 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +/** + * @file OpAdjustContrast.hpp + * + * @brief Defines the private C++ Class for the AdjustContrast operation. + */ + +#ifndef CVCUDA_PRIV__ADJUST_CONTRAST_HPP +#define CVCUDA_PRIV__ADJUST_CONTRAST_HPP + +#include "IOperator.hpp" +#include "PerDeviceResource.hpp" + +#include +#include +#include + +#include +#include + +namespace cvcuda::priv { + +// Bounded reduction workspace, retained per device and grown on demand. Host submissions are +// serialized while the completion event orders asynchronous reuse across CUDA streams. +class AdjustContrastWorkspace +{ +public: + AdjustContrastWorkspace(); + ~AdjustContrastWorkspace(); + + AdjustContrastWorkspace(const AdjustContrastWorkspace &) = delete; + AdjustContrastWorkspace &operator=(const AdjustContrastWorkspace &) = delete; + + [[nodiscard]] std::unique_lock lock(); + + void *acquire(size_t sizeBytes, cudaStream_t stream); + void release(cudaStream_t stream); + void releaseNoThrow(cudaStream_t stream) noexcept; + +private: + std::mutex m_mutex; + void *m_data = nullptr; + size_t m_capacity = 0; // bytes + cudaEvent_t m_ready = nullptr; + bool m_pending = false; +}; + +class AdjustContrast final : public IOperator +{ +public: + explicit AdjustContrast(); + + void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, double contrastFactor) const; + + void operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, + double contrastFactor) const; + +private: + mutable PerDeviceResource m_workspace; +}; + +} // namespace cvcuda::priv + +#endif // CVCUDA_PRIV__ADJUST_CONTRAST_HPP diff --git a/src/cvcuda/priv/OpAdjustHue.cu b/src/cvcuda/priv/OpAdjustHue.cu new file mode 100644 index 000000000..3e981bf52 --- /dev/null +++ b/src/cvcuda/priv/OpAdjustHue.cu @@ -0,0 +1,656 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "OpAdjustHue.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace cuda = nvcv::cuda; +namespace util = nvcv::util; + +namespace { + +// torchvision's float->uint8 scale factor (255 + 1 - eps): x in [0,1] maps to [0, 255.999], truncated. +constexpr float kU8Scale = 255.0f + 1.0f - 1e-3f; + +static bool IsPlanarLayout(nvcv::TensorLayout layout) +{ + return layout == nvcv::TENSOR_NCHW || layout == nvcv::TENSOR_CHW; +} + +// Core: rotate the hue of a single normalized RGB pixel (channels in [0, 1]) by `hueShift`, using +// torchvision's RGB<->HSV operation order. Shared by the interleaved and planar kernels. +inline __host__ __device__ void HsvHueRotate(float r, float g, float b, float hueShift, float &outR, float &outG, + float &outB) +{ + const float maxc = fmaxf(fmaxf(r, g), b); + const float minc = fminf(fminf(r, g), b); + const float v = maxc; + const float cr = maxc - minc; + const bool eqc = (maxc == minc); + + const float s = cr / (eqc ? 1.0f : maxc); + const float crd = eqc ? 1.0f : cr; + const float rc = (maxc - r) / crd; + const float gc = (maxc - g) / crd; + const float bc = (maxc - b) / crd; + + // Hue sector (mutually-exclusive branches == torchvision's masked hr+hg+hb sum). + float hh; + if (maxc == r) + { + hh = bc - gc; + } + else if (maxc == g) + { + hh = 2.0f + rc - bc; + } + else + { + hh = 4.0f + gc - rc; + } + + float h = hh / 6.0f + 1.0f; + h = h - floorf(h); // torch fmod(.,1) — arg is positive here, so == frac + + h = h + hueShift; + h = h - floorf(h); // torch.remainder(.,1): result in [0,1) + + // HSV -> RGB + const float h6 = h * 6.0f; + const float ii = floorf(h6); + const float f = h6 - ii; + const int i = static_cast(ii) % 6; + + const float sxf = s * f; + const float oneMinusS = 1.0f - s; + const float p = fminf(fmaxf(oneMinusS * v, 0.0f), 1.0f); + const float q = fminf(fmaxf((1.0f - sxf) * v, 0.0f), 1.0f); + const float t = fminf(fmaxf((sxf + oneMinusS) * v, 0.0f), 1.0f); + + switch (i) + { + case 0: + outR = v, outG = t, outB = p; + break; + case 1: + outR = q, outG = v, outB = p; + break; + case 2: + outR = p, outG = v, outB = t; + break; + case 3: + outR = p, outG = q, outB = v; + break; + case 4: + outR = t, outG = p, outB = v; + break; + default: // 5 + outR = v, outG = p, outB = q; + break; + } +} + +// uint8 input is scaled to [0,1] before the round-trip; float input is used directly (matching +// torchvision's to_dtype(scale=True), which leaves float32 unchanged). +template +inline __host__ __device__ float NormalizeIn(BT v) +{ + if constexpr (std::is_floating_point_v) + { + return static_cast(v); + } + else + { + return static_cast(v) * (1.0f / 255.0f); + } +} + +// Store the rotated channel: uint8 scales back and truncates toward zero (torchvision's cast); float +// is written as-is so value-channel outputs outside [0,1] retain torchvision's behavior. +template +inline __host__ __device__ BT StoreOut(float x) +{ + if constexpr (std::is_floating_point_v) + { + return static_cast(x); + } + else + { + return cuda::SaturateCast(cuda::round(x * kU8Scale)); + } +} + +// Core per-pixel hue rotation, shared by the interleaved and planar paths. 1-channel pixels are +// returned unchanged (torchvision returns the image as-is). +template +inline __host__ __device__ T AdjustHuePixel(T pixel, float hueShift) +{ + using BT = cuda::BaseType; + constexpr int numChans = cuda::NumElements; + + T out{}; + if constexpr (numChans == 1) + { + out = pixel; + } + else + { + static_assert(numChans == 3, "AdjustHue supports 1- or 3-channel pixels"); + float outR; + float outG; + float outB; + HsvHueRotate(NormalizeIn(cuda::GetElement(pixel, 0)), NormalizeIn(cuda::GetElement(pixel, 1)), + NormalizeIn(cuda::GetElement(pixel, 2)), hueShift, outR, outG, outB); + cuda::GetElement(out, 0) = StoreOut(outR); + cuda::GetElement(out, 1) = StoreOut(outG); + cuda::GetElement(out, 2) = StoreOut(outB); + } + return out; +} + +// Interleaved ((N)HWC) kernels ---------------------------------------------------------- + +template +__global__ void AdjustHueInterleaved(SrcWrapper src, DstWrapper dst, int2 size, float hueShift) +{ + int3 coord = cuda::StaticCast(blockIdx * blockDim + threadIdx); + if (coord.x >= size.x || coord.y >= size.y) + { + return; + } + dst[coord] = AdjustHuePixel(src[coord], hueShift); +} + +template +__global__ void AdjustHueInterleavedVarShape(SrcWrapper src, DstWrapper dst, float hueShift) +{ + const int z = blockIdx.z; + const int2 size{dst.width(z), dst.height(z)}; + int3 coord = cuda::StaticCast(blockIdx * blockDim + threadIdx); + if (coord.x >= size.x || coord.y >= size.y) + { + return; + } + dst[coord] = AdjustHuePixel(src[coord], hueShift); +} + +// Planar ((N)CHW) kernels --------------------------------------------------------------- + +template +inline __device__ void DoAdjustHuePlanar(SrcWrapper src, DstWrapper dst, int2 size, int z, float hueShift) +{ + int3 coord = cuda::StaticCast(blockIdx * blockDim + threadIdx); + if (coord.x >= size.x || coord.y >= size.y) + { + return; + } + if constexpr (numChannels == 1) + { + *dst.ptr(z, 0, coord.y, coord.x) = *src.ptr(z, 0, coord.y, coord.x); + } + else + { + using Vec = cuda::MakeType; + Vec pixel; + cuda::GetElement(pixel, 0) = *src.ptr(z, 0, coord.y, coord.x); + cuda::GetElement(pixel, 1) = *src.ptr(z, 1, coord.y, coord.x); + cuda::GetElement(pixel, 2) = *src.ptr(z, 2, coord.y, coord.x); + + const Vec out = AdjustHuePixel(pixel, hueShift); + + *dst.ptr(z, 0, coord.y, coord.x) = cuda::GetElement(out, 0); + *dst.ptr(z, 1, coord.y, coord.x) = cuda::GetElement(out, 1); + *dst.ptr(z, 2, coord.y, coord.x) = cuda::GetElement(out, 2); + } +} + +template +__global__ void AdjustHuePlanar(SrcWrapper src, DstWrapper dst, int2 size, float hueShift) +{ + DoAdjustHuePlanar(src, dst, size, blockIdx.z, hueShift); +} + +template +__global__ void AdjustHuePlanarVarShape(SrcWrapper src, DstWrapper dst, float hueShift) +{ + const int z = blockIdx.z; + const int2 size{dst.width(z), dst.height(z)}; + DoAdjustHuePlanar(src, dst, size, z, hueShift); +} + +// Launchers ----------------------------------------------------------------------------- + +constexpr int kGridZLimit = 65535; + +inline void CheckBatchLimit(int numSamples) +{ + if (numSamples > kGridZLimit) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Batch size exceeds the CUDA grid.z limit of 65535"); + } +} + +template +inline void RunTensor(cudaStream_t stream, const nvcv::TensorDataStridedCuda &srcData, + const nvcv::TensorDataStridedCuda &dstData, bool isPlanar, float hueShift) +{ + using BT = cuda::BaseType; + + auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(srcData); + auto dstAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(dstData); + NVCV_ASSERT(srcAccess && dstAccess); + + const int2 size = cuda::StaticCast(long2{srcAccess->numCols(), srcAccess->numRows()}); + CheckBatchLimit(static_cast(srcAccess->numSamples())); + + const int64_t inMaxStride = srcAccess->sampleStride() * srcAccess->numSamples(); + const int64_t outMaxStride = dstAccess->sampleStride() * dstAccess->numSamples(); + if (std::max(inMaxStride, outMaxStride) > cuda::TypeTraits::max) + { + throw nvcv::Exception(nvcv::Status::ERROR_OVERFLOW, "Input or output size exceeds %d. Tensor is too large.", + cuda::TypeTraits::max); + } + + const dim3 block(32, 4, 1); + const dim3 grid(util::DivUp(size.x, block.x), util::DivUp(size.y, block.y), + static_cast(srcAccess->numSamples())); + + if (isPlanar) + { + auto src = cuda::CreateTensorWrapNCHW(srcData); + auto dst = cuda::CreateTensorWrapNCHW(dstData); + AdjustHuePlanar<<>>(src, dst, size, hueShift); + } + else + { + auto src = cuda::CreateTensorWrapNHW(srcData); + auto dst = cuda::CreateTensorWrapNHW(dstData); + AdjustHueInterleaved<<>>(src, dst, size, hueShift); + } + NVCV_CHECK_THROW(cudaGetLastError()); +} + +template +inline void RunVarShape(cudaStream_t stream, const nvcv::ImageBatchVarShapeDataStridedCuda &srcData, + const nvcv::ImageBatchVarShapeDataStridedCuda &dstData, bool isPlanar, float hueShift) +{ + using BT = cuda::BaseType; + + CheckBatchLimit(dstData.numImages()); + + const int3 dstMaxSize{dstData.maxSize().w, dstData.maxSize().h, dstData.numImages()}; + const dim3 block(32, 4, 1); + const dim3 grid(util::DivUp(dstMaxSize.x, block.x), util::DivUp(dstMaxSize.y, block.y), + static_cast(dstMaxSize.z)); + + if (isPlanar) + { + cuda::ImageBatchVarShapeWrap src(srcData); + cuda::ImageBatchVarShapeWrap dst(dstData); + AdjustHuePlanarVarShape<<>>(src, dst, hueShift); + } + else + { + cuda::ImageBatchVarShapeWrap src(srcData); + cuda::ImageBatchVarShapeWrap dst(dstData); + AdjustHueInterleavedVarShape<<>>(src, dst, hueShift); + } + NVCV_CHECK_THROW(cudaGetLastError()); +} + +// Dispatch over base dtype (u8 / f32) and channel count (1 / 3) ------------------------- + +template +inline void RunChannelSwitch(int numChannels, nvcv::DataType dtype, const Cb &cb) +{ + const bool isU8 = (dtype == nvcv::TYPE_U8 || dtype == nvcv::TYPE_3U8); + const bool isF32 = (dtype == nvcv::TYPE_F32 || dtype == nvcv::TYPE_3F32); + + if (!isU8 && !isF32) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Invalid data type: AdjustHue supports 8-bit unsigned and 32-bit float"); + } + + if (numChannels == 1) + { + if (isU8) + { + cb(uchar1{}, std::integral_constant{}); + } + else + { + cb(float1{}, std::integral_constant{}); + } + } + else if (numChannels == 3) + { + if (isU8) + { + cb(uchar3{}, std::integral_constant{}); + } + else + { + cb(float3{}, std::integral_constant{}); + } + } + else + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Invalid number of channels: AdjustHue supports 1 or 3 channels"); + } +} + +// Validation ---------------------------------------------------------------------------- + +inline void ValidateHue(double hue) +{ + if (!(hue >= -0.5 && hue <= 0.5)) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "The hue factor must be in the range [-0.5, 0.5]"); + } +} + +inline bool ValidateSrcDstTensors(bool &isEmpty, int &numChannels, nvcv::DataType &dtype, + const nvcv::Optional &srcData, + const nvcv::Optional &dstData) +{ + if (!srcData) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input must be cuda-accessible, pitch-linear tensor"); + } + if (!dstData) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Output must be cuda-accessible, pitch-linear tensor"); + } + if (srcData->layout() != dstData->layout()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input and output must have the same layout"); + } + + const bool isPlanar = IsPlanarLayout(srcData->layout()); + if (srcData->layout() != nvcv::TENSOR_HWC && srcData->layout() != nvcv::TENSOR_NHWC && !isPlanar) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input must have (N)HWC or (N)CHW layout"); + } + if (srcData->dtype() != dstData->dtype()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input and output must have the same data type"); + } + if (srcData->dtype().numChannels() != 1) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Tensor data type must be scalar; use the C dimension for image channels"); + } + + auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcData); + auto dstAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*dstData); + if (!srcAccess || !dstAccess) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input/output must be accessible as strided images"); + } + if (srcAccess->numSamples() != dstAccess->numSamples()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of samples"); + } + + numChannels = srcAccess->numChannels(); + if (numChannels != dstAccess->numChannels()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of channels"); + } + if (numChannels != 1 && numChannels != 3) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input must have 1 or 3 channels"); + } + if (srcAccess->numCols() != dstAccess->numCols() || srcAccess->numRows() != dstAccess->numRows()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input and output must have matching width and height"); + } + + isEmpty = srcAccess->numSamples() == 0 || srcAccess->numRows() == 0 || srcAccess->numCols() == 0; + dtype = srcData->dtype(); + return isPlanar; +} + +inline void ValidateImagePlanes(const nvcv::Image &image, const nvcv::ImageFormat &format) +{ + auto data = image.exportData(); + if (!data || data->numPlanes() != format.numPlanes()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Image plane descriptors must match the image format"); + } + + constexpr int64_t limit = cuda::TypeTraits::max; + for (int p = 0; p < data->numPlanes(); ++p) + { + const nvcv::ImagePlaneStrided &plane = data->plane(p); + const nvcv::Size2D expected = format.planeSize(image.size(), p); + if (plane.width != expected.w || plane.height != expected.h) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Image plane descriptors must match the image format and size"); + } + if (expected != image.size()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "All image planes must have matching width and height"); + } + + const int64_t pixelStride = format.planePixelStrideBytes(p); + if (plane.rowStride < 0 + || (static_cast(plane.height - 1) * plane.rowStride + + static_cast(plane.width - 1) * pixelStride + > limit)) + { + throw nvcv::Exception(nvcv::Status::ERROR_OVERFLOW, + "Input or output image-plane maximum byte offset exceeds %d", + static_cast(limit)); + } + } +} + +inline bool ValidateSrcDstVarBatch(bool &isEmpty, int &numChannels, nvcv::DataType &dtype, + const nvcv::ImageBatchVarShape &src, const nvcv::ImageBatchVarShape &dst, + const nvcv::Optional &srcData, + const nvcv::Optional &dstData) +{ + if (!srcData) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input must be cuda-accessible, varshape pitch-linear image batch"); + } + if (!dstData) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Output must be cuda-accessible, varshape pitch-linear image batch"); + } + if (srcData->numImages() != dstData->numImages()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of samples"); + } + isEmpty = srcData->numImages() == 0; + if (isEmpty) + { + return false; + } + + const auto &srcFormat = srcData->uniqueFormat(); + const auto &dstFormat = dstData->uniqueFormat(); + if (!srcFormat || !dstFormat) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "All images in a batch must have the same format"); + } + if (srcFormat != dstFormat) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input and output must have the same format"); + } + + numChannels = srcFormat.numChannels(); + if (numChannels != 1 && numChannels != 3) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "The input must have 1 or 3 channels"); + } + if (numChannels == 3 + && (srcFormat.colorModel() != nvcv::ColorModel::RGB + || (srcFormat.swizzle() != nvcv::Swizzle::S_XYZ0 && srcFormat.swizzle() != nvcv::Swizzle::S_XYZ1))) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Three-channel input must use RGB channel order"); + } + + nvcv::ExtraChannelInfo extraChannels{}; + srcFormat.extraChannelInfo(&extraChannels); + if (extraChannels.numChannels != 0) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Image formats with extra channels are not supported"); + } + if (srcFormat.chromaSubsampling() != nvcv::ChromaSubsampling::CSS_444) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Chroma-subsampled image formats are not supported"); + } + + const int numPlanes = srcFormat.numPlanes(); + dtype = srcFormat.planeDataType(0); + if (numPlanes == 1) + { + if (dtype.numChannels() != numChannels || srcFormat.planePixelStrideBytes(0) != dtype.strideBytes()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Single-plane images must have one packed element per pixel"); + } + } + else if (numPlanes != numChannels || dtype.numChannels() != 1 + || srcFormat.planePixelStrideBytes(0) != dtype.strideBytes()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Planar images must have one scalar, full-resolution plane per channel"); + } + for (int p = 1; p < numPlanes; ++p) + { + if (srcFormat.planeDataType(p) != dtype || srcFormat.planeDataType(p).numChannels() != 1 + || srcFormat.planePixelStrideBytes(p) != dtype.strideBytes()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "All image planes must have the same packed scalar data type"); + } + } + + for (int i = 0; i < src.numImages(); ++i) + { + const nvcv::Size2D srcSize = src[i].size(); + const nvcv::Size2D dstSize = dst[i].size(); + if (srcSize != dstSize) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input and output image %d sizes must match: input is %dx%d, output is %dx%d", i, + srcSize.w, srcSize.h, dstSize.w, dstSize.h); + } + ValidateImagePlanes(src[i], srcFormat); + ValidateImagePlanes(dst[i], dstFormat); + } + + return numPlanes > 1; +} + +} // anonymous namespace + +namespace cvcuda::priv { + +AdjustHue::AdjustHue() {} + +// Tensor input variant +void AdjustHue::operator()(cudaStream_t stream, const nvcv::Tensor &src, const nvcv::Tensor &dst, double hue) const +{ + ValidateHue(hue); + + bool isEmpty; + int numChannels; + nvcv::DataType dtype; + auto srcData = src.exportData(); + auto dstData = dst.exportData(); + const bool isPlanar = ValidateSrcDstTensors(isEmpty, numChannels, dtype, srcData, dstData); + if (isEmpty) + { + return; + } + + const float hueShift = static_cast(hue); + + RunChannelSwitch(numChannels, dtype, + [&](auto dummy, auto channelsIC) + { + using T = decltype(dummy); + constexpr int numChannels = decltype(channelsIC)::value; + RunTensor(stream, *srcData, *dstData, isPlanar, hueShift); + }); +} + +// VarShape input variant +void AdjustHue::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &src, + const nvcv::ImageBatchVarShape &dst, double hue) const +{ + ValidateHue(hue); + + bool isEmpty; + int numChannels; + nvcv::DataType dtype; + auto srcData = src.exportData(stream); + auto dstData = dst.exportData(stream); + const bool isPlanar = ValidateSrcDstVarBatch(isEmpty, numChannels, dtype, src, dst, srcData, dstData); + if (isEmpty) + { + return; + } + + const float hueShift = static_cast(hue); + + RunChannelSwitch(numChannels, dtype, + [&](auto dummy, auto channelsIC) + { + using T = decltype(dummy); + constexpr int numChannels = decltype(channelsIC)::value; + RunVarShape(stream, *srcData, *dstData, isPlanar, hueShift); + }); +} + +} // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpAdjustHue.hpp b/src/cvcuda/priv/OpAdjustHue.hpp new file mode 100644 index 000000000..9ba2129df --- /dev/null +++ b/src/cvcuda/priv/OpAdjustHue.hpp @@ -0,0 +1,48 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +/** + * @file OpAdjustHue.hpp + * + * @brief Defines the private C++ Class for the AdjustHue operation. + */ + +#ifndef CVCUDA_PRIV__ADJUST_HUE_HPP +#define CVCUDA_PRIV__ADJUST_HUE_HPP + +#include "IOperator.hpp" + +#include +#include +#include + +namespace cvcuda::priv { + +class AdjustHue final : public IOperator +{ +public: + explicit AdjustHue(); + + void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, double hue) const; + + void operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, + double hue) const; +}; + +} // namespace cvcuda::priv + +#endif // CVCUDA_PRIV__ADJUST_HUE_HPP diff --git a/src/cvcuda/priv/OpAdjustSaturation.cu b/src/cvcuda/priv/OpAdjustSaturation.cu new file mode 100644 index 000000000..d8be3fcc4 --- /dev/null +++ b/src/cvcuda/priv/OpAdjustSaturation.cu @@ -0,0 +1,583 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "OpAdjustSaturation.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace cuda = nvcv::cuda; +namespace util = nvcv::util; + +namespace { + +// torchvision luminance coefficients. Intentionally NOT cvtcolor's 0.299 (see OpAdjustSaturation.h). +constexpr float kR2Y = 0.2989f; +constexpr float kG2Y = 0.587f; +constexpr float kB2Y = 0.114f; + +static bool IsPlanarLayout(nvcv::TensorLayout layout) +{ + return layout == nvcv::TENSOR_NCHW || layout == nvcv::TENSOR_CHW; +} + +// Clamp + store to the output element type. Integers truncate toward zero (matching torchvision's +// `.to(uint8)`): round drops the fractional part and SaturateCast clamps into the dtype range, +// the same sequence cvcuda's ConvertTo uses for NVCV_ROUND_TRUNCATE (priv/legacy/convert_to.cu). +// Floats clamp to [0, 1] with no rounding. +template +inline __host__ __device__ BT StoreSaturation(float v) +{ + if constexpr (std::is_floating_point_v) + { + return static_cast(fminf(fmaxf(v, 0.0f), 1.0f)); + } + else + { + return cuda::SaturateCast(cuda::round(v)); + } +} + +// Core per-pixel saturation blend, shared by the interleaved and planar paths (and mirrored by the +// CPU gold). 1-channel pixels are returned unchanged (torchvision returns the image as-is); a +// 3-channel pixel is blended toward its luminance by `ratio` with weight `oneMinus = 1 - ratio`. +template +inline __host__ __device__ T AdjustSaturationPixel(T pixel, float ratio, float oneMinus) +{ + using BT = cuda::BaseType; + constexpr int numChans = cuda::NumElements; + + T out{}; + if constexpr (numChans == 1) + { + out = pixel; + } + else + { + static_assert(numChans == 3, "AdjustSaturation supports 1- or 3-channel pixels"); + const float r = static_cast(cuda::GetElement(pixel, 0)); + const float g = static_cast(cuda::GetElement(pixel, 1)); + const float b = static_cast(cuda::GetElement(pixel, 2)); + + float gray = (kR2Y * r + kG2Y * g) + kB2Y * b; + if constexpr (!std::is_floating_point_v) + { + gray = floorf(gray); // torchvision floors the grayscale for integer dtypes before the blend + } + + cuda::GetElement(out, 0) = StoreSaturation(ratio * r + oneMinus * gray); + cuda::GetElement(out, 1) = StoreSaturation(ratio * g + oneMinus * gray); + cuda::GetElement(out, 2) = StoreSaturation(ratio * b + oneMinus * gray); + } + return out; +} + +// Interleaved ((N)HWC) kernels ---------------------------------------------------------- + +template +__global__ void AdjustSaturationInterleaved(SrcWrapper src, DstWrapper dst, int2 size, float ratio, float oneMinus) +{ + int3 coord = cuda::StaticCast(blockIdx * blockDim + threadIdx); + if (coord.x >= size.x || coord.y >= size.y) + { + return; + } + dst[coord] = AdjustSaturationPixel(src[coord], ratio, oneMinus); +} + +template +__global__ void AdjustSaturationInterleavedVarShape(SrcWrapper src, DstWrapper dst, float ratio, float oneMinus) +{ + const int z = blockIdx.z; + const int2 size{dst.width(z), dst.height(z)}; + int3 coord = cuda::StaticCast(blockIdx * blockDim + threadIdx); + if (coord.x >= size.x || coord.y >= size.y) + { + return; + } + dst[coord] = AdjustSaturationPixel(src[coord], ratio, oneMinus); +} + +// Planar ((N)CHW) kernels --------------------------------------------------------------- + +template +inline __device__ void DoAdjustSaturationPlanar(SrcWrapper src, DstWrapper dst, int2 size, int z, float ratio, + float oneMinus) +{ + int3 coord = cuda::StaticCast(blockIdx * blockDim + threadIdx); + if (coord.x >= size.x || coord.y >= size.y) + { + return; + } + if constexpr (numChannels == 1) + { + *dst.ptr(z, 0, coord.y, coord.x) = *src.ptr(z, 0, coord.y, coord.x); + } + else + { + using Vec = cuda::MakeType; + Vec pixel; + cuda::GetElement(pixel, 0) = *src.ptr(z, 0, coord.y, coord.x); + cuda::GetElement(pixel, 1) = *src.ptr(z, 1, coord.y, coord.x); + cuda::GetElement(pixel, 2) = *src.ptr(z, 2, coord.y, coord.x); + + const Vec out = AdjustSaturationPixel(pixel, ratio, oneMinus); + + *dst.ptr(z, 0, coord.y, coord.x) = cuda::GetElement(out, 0); + *dst.ptr(z, 1, coord.y, coord.x) = cuda::GetElement(out, 1); + *dst.ptr(z, 2, coord.y, coord.x) = cuda::GetElement(out, 2); + } +} + +template +__global__ void AdjustSaturationPlanar(SrcWrapper src, DstWrapper dst, int2 size, float ratio, float oneMinus) +{ + DoAdjustSaturationPlanar(src, dst, size, blockIdx.z, ratio, oneMinus); +} + +template +__global__ void AdjustSaturationPlanarVarShape(SrcWrapper src, DstWrapper dst, float ratio, float oneMinus) +{ + const int z = blockIdx.z; + const int2 size{dst.width(z), dst.height(z)}; + DoAdjustSaturationPlanar(src, dst, size, z, ratio, oneMinus); +} + +// Launchers ----------------------------------------------------------------------------- + +constexpr int kGridZLimit = 65535; + +inline void CheckBatchLimit(int numSamples) +{ + if (numSamples > kGridZLimit) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Batch size exceeds the CUDA grid.z limit of 65535"); + } +} + +template +inline void RunTensor(cudaStream_t stream, const nvcv::TensorDataStridedCuda &srcData, + const nvcv::TensorDataStridedCuda &dstData, bool isPlanar, float ratio, float oneMinus) +{ + using BT = cuda::BaseType; + + auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(srcData); + auto dstAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(dstData); + NVCV_ASSERT(srcAccess && dstAccess); + + const int2 size = cuda::StaticCast(long2{srcAccess->numCols(), srcAccess->numRows()}); + CheckBatchLimit(static_cast(srcAccess->numSamples())); + + const int64_t inMaxStride = srcAccess->sampleStride() * srcAccess->numSamples(); + const int64_t outMaxStride = dstAccess->sampleStride() * dstAccess->numSamples(); + if (std::max(inMaxStride, outMaxStride) > cuda::TypeTraits::max) + { + throw nvcv::Exception(nvcv::Status::ERROR_OVERFLOW, "Input or output size exceeds %d. Tensor is too large.", + cuda::TypeTraits::max); + } + + const dim3 block(32, 4, 1); + const dim3 grid(util::DivUp(size.x, block.x), util::DivUp(size.y, block.y), + static_cast(srcAccess->numSamples())); + + if (isPlanar) + { + auto src = cuda::CreateTensorWrapNCHW(srcData); + auto dst = cuda::CreateTensorWrapNCHW(dstData); + AdjustSaturationPlanar<<>>(src, dst, size, ratio, oneMinus); + } + else + { + auto src = cuda::CreateTensorWrapNHW(srcData); + auto dst = cuda::CreateTensorWrapNHW(dstData); + AdjustSaturationInterleaved<<>>(src, dst, size, ratio, oneMinus); + } + NVCV_CHECK_THROW(cudaGetLastError()); +} + +template +inline void RunVarShape(cudaStream_t stream, const nvcv::ImageBatchVarShapeDataStridedCuda &srcData, + const nvcv::ImageBatchVarShapeDataStridedCuda &dstData, bool isPlanar, float ratio, + float oneMinus) +{ + using BT = cuda::BaseType; + + CheckBatchLimit(dstData.numImages()); + + const int3 dstMaxSize{dstData.maxSize().w, dstData.maxSize().h, dstData.numImages()}; + const dim3 block(32, 4, 1); + const dim3 grid(util::DivUp(dstMaxSize.x, block.x), util::DivUp(dstMaxSize.y, block.y), + static_cast(dstMaxSize.z)); + + if (isPlanar) + { + cuda::ImageBatchVarShapeWrap src(srcData); + cuda::ImageBatchVarShapeWrap dst(dstData); + AdjustSaturationPlanarVarShape<<>>(src, dst, ratio, oneMinus); + } + else + { + cuda::ImageBatchVarShapeWrap src(srcData); + cuda::ImageBatchVarShapeWrap dst(dstData); + AdjustSaturationInterleavedVarShape<<>>(src, dst, ratio, oneMinus); + } + NVCV_CHECK_THROW(cudaGetLastError()); +} + +// Dispatch over base dtype (u8 / f32) and channel count (1 / 3) ------------------------- + +template +inline void RunChannelSwitch(int numChannels, nvcv::DataType dtype, const Cb &cb) +{ + const bool isU8 = (dtype == nvcv::TYPE_U8 || dtype == nvcv::TYPE_3U8); + const bool isF32 = (dtype == nvcv::TYPE_F32 || dtype == nvcv::TYPE_3F32); + + if (!isU8 && !isF32) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Invalid data type: AdjustSaturation supports 8-bit unsigned and 32-bit float"); + } + + if (numChannels == 1) + { + if (isU8) + { + cb(uchar1{}, std::integral_constant{}); + } + else + { + cb(float1{}, std::integral_constant{}); + } + } + else if (numChannels == 3) + { + if (isU8) + { + cb(uchar3{}, std::integral_constant{}); + } + else + { + cb(float3{}, std::integral_constant{}); + } + } + else + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Invalid number of channels: AdjustSaturation supports 1 or 3 channels"); + } +} + +// Validation ---------------------------------------------------------------------------- + +inline void ValidateSaturation(double saturation) +{ + if (!(saturation >= 0.0)) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "The saturation factor must be >= 0 (and not NaN)"); + } +} + +inline bool ValidateSrcDstTensors(bool &isEmpty, int &numChannels, nvcv::DataType &dtype, + const nvcv::Optional &srcData, + const nvcv::Optional &dstData) +{ + if (!srcData) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input must be cuda-accessible, pitch-linear tensor"); + } + if (!dstData) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Output must be cuda-accessible, pitch-linear tensor"); + } + if (srcData->layout() != dstData->layout()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input and output must have the same layout"); + } + + const bool isPlanar = IsPlanarLayout(srcData->layout()); + if (srcData->layout() != nvcv::TENSOR_HWC && srcData->layout() != nvcv::TENSOR_NHWC && !isPlanar) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input must have (N)HWC or (N)CHW layout"); + } + if (srcData->dtype() != dstData->dtype()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input and output must have the same data type"); + } + if (srcData->dtype().numChannels() != 1) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Tensor data type must be scalar; use the C dimension for image channels"); + } + + auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcData); + auto dstAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*dstData); + if (!srcAccess || !dstAccess) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input/output must be accessible as strided images"); + } + if (srcAccess->numSamples() != dstAccess->numSamples()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of samples"); + } + + numChannels = srcAccess->numChannels(); + if (numChannels != dstAccess->numChannels()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of channels"); + } + if (numChannels != 1 && numChannels != 3) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input must have 1 or 3 channels"); + } + if (srcAccess->numCols() != dstAccess->numCols() || srcAccess->numRows() != dstAccess->numRows()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input and output must have matching width and height"); + } + + isEmpty = srcAccess->numSamples() == 0 || srcAccess->numRows() == 0 || srcAccess->numCols() == 0; + dtype = srcData->dtype(); + return isPlanar; +} + +inline void ValidateImagePlanes(const nvcv::Image &image, const nvcv::ImageFormat &format) +{ + auto data = image.exportData(); + if (!data || data->numPlanes() != format.numPlanes()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Image plane descriptors must match the image format"); + } + + constexpr int64_t limit = cuda::TypeTraits::max; + for (int p = 0; p < data->numPlanes(); ++p) + { + const nvcv::ImagePlaneStrided &plane = data->plane(p); + const nvcv::Size2D expected = format.planeSize(image.size(), p); + if (plane.width != expected.w || plane.height != expected.h) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Image plane descriptors must match the image format and size"); + } + if (expected != image.size()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "All image planes must have matching width and height"); + } + + const int64_t pixelStride = format.planePixelStrideBytes(p); + if (plane.rowStride < 0 + || (static_cast(plane.height - 1) * plane.rowStride + + static_cast(plane.width - 1) * pixelStride + > limit)) + { + throw nvcv::Exception(nvcv::Status::ERROR_OVERFLOW, + "Input or output image-plane maximum byte offset exceeds %d", + static_cast(limit)); + } + } +} + +inline bool ValidateSrcDstVarBatch(bool &isEmpty, int &numChannels, nvcv::DataType &dtype, + const nvcv::ImageBatchVarShape &src, const nvcv::ImageBatchVarShape &dst, + const nvcv::Optional &srcData, + const nvcv::Optional &dstData) +{ + if (!srcData) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input must be cuda-accessible, varshape pitch-linear image batch"); + } + if (!dstData) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Output must be cuda-accessible, varshape pitch-linear image batch"); + } + if (srcData->numImages() != dstData->numImages()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of samples"); + } + isEmpty = srcData->numImages() == 0; + if (isEmpty) + { + return false; + } + + const auto &srcFormat = srcData->uniqueFormat(); + const auto &dstFormat = dstData->uniqueFormat(); + if (!srcFormat || !dstFormat) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "All images in a batch must have the same format"); + } + if (srcFormat != dstFormat) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input and output must have the same format"); + } + + numChannels = srcFormat.numChannels(); + if (numChannels != 1 && numChannels != 3) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "The input must have 1 or 3 channels"); + } + if (numChannels == 3 + && (srcFormat.colorModel() != nvcv::ColorModel::RGB + || (srcFormat.swizzle() != nvcv::Swizzle::S_XYZ0 && srcFormat.swizzle() != nvcv::Swizzle::S_XYZ1))) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Three-channel input must use RGB channel order"); + } + + nvcv::ExtraChannelInfo extraChannels{}; + srcFormat.extraChannelInfo(&extraChannels); + if (extraChannels.numChannels != 0) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Image formats with extra channels are not supported"); + } + if (srcFormat.chromaSubsampling() != nvcv::ChromaSubsampling::CSS_444) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Chroma-subsampled image formats are not supported"); + } + + const int numPlanes = srcFormat.numPlanes(); + dtype = srcFormat.planeDataType(0); + if (numPlanes == 1) + { + if (dtype.numChannels() != numChannels || srcFormat.planePixelStrideBytes(0) != dtype.strideBytes()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Single-plane images must have one packed element per pixel"); + } + } + else if (numPlanes != numChannels || dtype.numChannels() != 1 + || srcFormat.planePixelStrideBytes(0) != dtype.strideBytes()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Planar images must have one scalar, full-resolution plane per channel"); + } + for (int p = 1; p < numPlanes; ++p) + { + if (srcFormat.planeDataType(p) != dtype || srcFormat.planeDataType(p).numChannels() != 1 + || srcFormat.planePixelStrideBytes(p) != dtype.strideBytes()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "All image planes must have the same packed scalar data type"); + } + } + + for (int i = 0; i < src.numImages(); ++i) + { + const nvcv::Size2D srcSize = src[i].size(); + const nvcv::Size2D dstSize = dst[i].size(); + if (srcSize != dstSize) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input and output image %d sizes must match: input is %dx%d, output is %dx%d", i, + srcSize.w, srcSize.h, dstSize.w, dstSize.h); + } + ValidateImagePlanes(src[i], srcFormat); + ValidateImagePlanes(dst[i], dstFormat); + } + + return numPlanes > 1; +} + +} // anonymous namespace + +namespace cvcuda::priv { + +AdjustSaturation::AdjustSaturation() {} + +// Tensor input variant +void AdjustSaturation::operator()(cudaStream_t stream, const nvcv::Tensor &src, const nvcv::Tensor &dst, + double saturation) const +{ + ValidateSaturation(saturation); + + bool isEmpty; + int numChannels; + nvcv::DataType dtype; + auto srcData = src.exportData(); + auto dstData = dst.exportData(); + const bool isPlanar = ValidateSrcDstTensors(isEmpty, numChannels, dtype, srcData, dstData); + if (isEmpty) + { + return; + } + + const float ratio = static_cast(saturation); + const float oneMinus = static_cast(1.0 - saturation); + + RunChannelSwitch(numChannels, dtype, + [&](auto dummy, auto channelsIC) + { + using T = decltype(dummy); + constexpr int numChannels = decltype(channelsIC)::value; + RunTensor(stream, *srcData, *dstData, isPlanar, ratio, oneMinus); + }); +} + +// VarShape input variant +void AdjustSaturation::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &src, + const nvcv::ImageBatchVarShape &dst, double saturation) const +{ + ValidateSaturation(saturation); + + bool isEmpty; + int numChannels; + nvcv::DataType dtype; + auto srcData = src.exportData(stream); + auto dstData = dst.exportData(stream); + const bool isPlanar = ValidateSrcDstVarBatch(isEmpty, numChannels, dtype, src, dst, srcData, dstData); + if (isEmpty) + { + return; + } + + const float ratio = static_cast(saturation); + const float oneMinus = static_cast(1.0 - saturation); + + RunChannelSwitch(numChannels, dtype, + [&](auto dummy, auto channelsIC) + { + using T = decltype(dummy); + constexpr int numChannels = decltype(channelsIC)::value; + RunVarShape(stream, *srcData, *dstData, isPlanar, ratio, oneMinus); + }); +} + +} // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpAdjustSaturation.hpp b/src/cvcuda/priv/OpAdjustSaturation.hpp new file mode 100644 index 000000000..a8368d002 --- /dev/null +++ b/src/cvcuda/priv/OpAdjustSaturation.hpp @@ -0,0 +1,48 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +/** + * @file OpAdjustSaturation.hpp + * + * @brief Defines the private C++ Class for the AdjustSaturation operation. + */ + +#ifndef CVCUDA_PRIV__ADJUST_SATURATION_HPP +#define CVCUDA_PRIV__ADJUST_SATURATION_HPP + +#include "IOperator.hpp" + +#include +#include +#include + +namespace cvcuda::priv { + +class AdjustSaturation final : public IOperator +{ +public: + explicit AdjustSaturation(); + + void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, double saturation) const; + + void operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, + double saturation) const; +}; + +} // namespace cvcuda::priv + +#endif // CVCUDA_PRIV__ADJUST_SATURATION_HPP diff --git a/src/cvcuda/priv/OpAdjustSharpness.cu b/src/cvcuda/priv/OpAdjustSharpness.cu new file mode 100644 index 000000000..e6fc09655 --- /dev/null +++ b/src/cvcuda/priv/OpAdjustSharpness.cu @@ -0,0 +1,520 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "OpAdjustSharpness.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace cuda = nvcv::cuda; +namespace util = nvcv::util; + +namespace { + +// Blend clamp bound per base type: dtype max for unsigned integers, 1.0 for float (mirrors the +// torchvision `_max_value`). +template +inline __device__ float SharpnessBound() +{ + if constexpr (std::is_floating_point_v) + { + return 1.0f; + } + else + { + return static_cast(cuda::TypeTraits::max); + } +} + +// Per-channel interior compute, shared bit-exactly with the CPU gold reference (see +// TestOpAdjustSharpness.cpp). Every multiply-add is an explicit `fmaf` and the accumulation order is +// fixed, so the correctly-rounded IEEE-754 result is identical on host and device regardless of the +// compiler's FMA-contraction setting. `c11` is the center (original) pixel; the eight edge taps +// carry weight 1/13 and the center 5/13. Integer types round the smoothed value to nearest (ties to +// even) before blending, then the clamped blend is truncated toward zero on the final cast — both +// matching torchvision's `blurred.round_()` + `.to(dtype)`. +template +inline __device__ BT AdjustSharpnessScalar(float c00, float c01, float c02, float c10, float c11, float c12, float c20, + float c21, float c22, float oneMinusFactor, float bound) +{ + constexpr float kEdge = 1.0f / 13.0f; + constexpr float kCenter = 5.0f / 13.0f; + + float blur = c11 * kCenter; + blur = fmaf(c00, kEdge, blur); + blur = fmaf(c01, kEdge, blur); + blur = fmaf(c02, kEdge, blur); + blur = fmaf(c10, kEdge, blur); + blur = fmaf(c12, kEdge, blur); + blur = fmaf(c20, kEdge, blur); + blur = fmaf(c21, kEdge, blur); + blur = fmaf(c22, kEdge, blur); + + if constexpr (!std::is_floating_point_v) + { + blur = rintf(blur); // round-to-nearest-even, matching torch.round on the smoothed image + } + + // out = in + (1 - factor) * (blur - in) = factor*in + (1-factor)*blur + const float out = fmaf(oneMinusFactor, blur - c11, c11); + const float clamped = fminf(fmaxf(out, 0.0f), bound); + return static_cast(clamped); // truncates toward zero for integer BT; identity for float +} + +template +inline __device__ std::conditional_t CoordFor(int col, int row, int plane, int sample) +{ + if constexpr (!IsPlanar) + { + return int3{col, row, sample}; + } + else + { + return int4{col, row, plane, sample}; + } +} + +// Interior pixels are blended; the 1-pixel border is copied unchanged (there is no border +// extension). When a dimension is < 3 every pixel is a border pixel, so the whole image is copied — +// this reproduces torchvision returning the input for images with height or width <= 2. +template +inline __device__ void DoAdjustSharpness(SrcWrapper src, DstWrapper dst, int2 size, int plane, float oneMinusFactor) +{ + using SrcT = std::remove_const_t; + using DstT = typename DstWrapper::ValueType; + using BT = cuda::BaseType; + static constexpr int numChannels = cuda::NumElements; + static_assert(numChannels == cuda::NumElements); + static_assert(!IsPlanar || numChannels == 1); + + const int col = blockIdx.x * blockDim.x + threadIdx.x; + const int row = blockIdx.y * blockDim.y + threadIdx.y; + const int sample = blockIdx.z; + if (col >= size.x || row >= size.y) + { + return; + } + + if (col == 0 || row == 0 || col == size.x - 1 || row == size.y - 1) + { + dst[CoordFor(col, row, plane, sample)] = src[CoordFor(col, row, plane, sample)]; + return; + } + + const float bound = SharpnessBound(); + + const SrcT n00 = src[CoordFor(col - 1, row - 1, plane, sample)]; + const SrcT n01 = src[CoordFor(col, row - 1, plane, sample)]; + const SrcT n02 = src[CoordFor(col + 1, row - 1, plane, sample)]; + const SrcT n10 = src[CoordFor(col - 1, row, plane, sample)]; + const SrcT n11 = src[CoordFor(col, row, plane, sample)]; + const SrcT n12 = src[CoordFor(col + 1, row, plane, sample)]; + const SrcT n20 = src[CoordFor(col - 1, row + 1, plane, sample)]; + const SrcT n21 = src[CoordFor(col, row + 1, plane, sample)]; + const SrcT n22 = src[CoordFor(col + 1, row + 1, plane, sample)]; + + DstT out{}; +#pragma unroll + for (int ch = 0; ch < numChannels; ++ch) + { + cuda::GetElement(out, ch) = AdjustSharpnessScalar( + static_cast(cuda::GetElement(n00, ch)), static_cast(cuda::GetElement(n01, ch)), + static_cast(cuda::GetElement(n02, ch)), static_cast(cuda::GetElement(n10, ch)), + static_cast(cuda::GetElement(n11, ch)), static_cast(cuda::GetElement(n12, ch)), + static_cast(cuda::GetElement(n20, ch)), static_cast(cuda::GetElement(n21, ch)), + static_cast(cuda::GetElement(n22, ch)), oneMinusFactor, bound); + } + dst[CoordFor(col, row, plane, sample)] = out; +} + +// Tensor variant +template +__global__ void AdjustSharpness(SrcWrapper src, DstWrapper dst, int2 size, int numPlanes, float oneMinusFactor) +{ + assert(isPlanar || numPlanes == 1); + if constexpr (!isPlanar) + { + DoAdjustSharpness(src, dst, size, 0, oneMinusFactor); + } + else + { + for (int p = 0; p < numPlanes; p++) + { + DoAdjustSharpness(src, dst, size, p, oneMinusFactor); + } + } +} + +// VarShape variant +template +__global__ void AdjustSharpness(SrcWrapper src, DstWrapper dst, int numPlanes, float oneMinusFactor) +{ + assert(isPlanar || numPlanes == 1); + const int z = blockIdx.z; + int2 size{dst.width(z), dst.height(z)}; + + if constexpr (!isPlanar) + { + DoAdjustSharpness(src, dst, size, 0, oneMinusFactor); + } + else + { + for (int p = 0; p < numPlanes; p++) + { + DoAdjustSharpness(src, dst, size, p, oneMinusFactor); + } + } +} + +// Run AdjustSharpness kernel ------------------------------------------------------------- + +template +inline void RunAdjustSharpness(cudaStream_t stream, const SrcData &srcData, const DstData &dstData, float factor) +{ + const float oneMinusFactor = 1.0f - factor; + dim3 block(32, 4, 1); + if constexpr (std::is_same_v) + { + auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(srcData); + auto dstAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(dstData); + int2 size = cuda::StaticCast(long2{srcAccess->numCols(), srcAccess->numRows()}); + + // Each sample maps to one grid.z block (planar channels are looped inside the kernel); + // CUDA caps grid.z at 65535. + if (srcAccess->numSamples() > 65535) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Batch size exceeds the CUDA grid.z limit of 65535"); + } + dim3 grid(util::DivUp(size.x, block.x), util::DivUp(size.y, block.y), srcAccess->numSamples()); + + int64_t inMaxStride = srcAccess->sampleStride() * srcAccess->numSamples(); + int64_t outMaxStride = dstAccess->sampleStride() * dstAccess->numSamples(); + if (std::max(inMaxStride, outMaxStride) > cuda::TypeTraits::max) + { + throw nvcv::Exception(nvcv::Status::ERROR_OVERFLOW, "Input or output size exceeds %d. Tensor is too large.", + cuda::TypeTraits::max); + } + using StrideType = int32_t; + + if constexpr (!isPlanar) + { + auto src = cuda::CreateTensorWrapNHW(srcData); + auto dst = cuda::CreateTensorWrapNHW(dstData); + AdjustSharpness<<>>(src, dst, size, 1, oneMinusFactor); + } + else + { + const int numPlanes = srcAccess->numPlanes(); + auto src = cuda::Tensor4DWrap( + srcData.basePtr(), static_cast(srcAccess->sampleStride()), + static_cast(srcAccess->planeStride()), static_cast(srcAccess->rowStride())); + auto dst = cuda::Tensor4DWrap( + dstData.basePtr(), static_cast(dstAccess->sampleStride()), + static_cast(dstAccess->planeStride()), static_cast(dstAccess->rowStride())); + AdjustSharpness<<>>(src, dst, size, numPlanes, oneMinusFactor); + } + NVCV_CHECK_THROW(cudaGetLastError()); + } + else + { + static_assert(std::is_same_v); + // One grid.z block per image; CUDA caps grid.z at 65535. + if (dstData.numImages() > 65535) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Batch size exceeds the CUDA grid.z limit of 65535"); + } + int3 dstMaxSize{dstData.maxSize().w, dstData.maxSize().h, dstData.numImages()}; + dim3 grid(util::DivUp(dstMaxSize.x, block.x), util::DivUp(dstMaxSize.y, block.y), dstMaxSize.z); + + const int numPlanes = dstData.uniqueFormat().numPlanes(); + + cuda::ImageBatchVarShapeWrap src(srcData); + cuda::ImageBatchVarShapeWrap dst(dstData); + AdjustSharpness<<>>(src, dst, numPlanes, oneMinusFactor); + NVCV_CHECK_THROW(cudaGetLastError()); + } +} + +// Dispatch over base data type (u8 / u16 / f32) and channel count (1 / 3 / 4) ------------- + +template +inline void RunTypeSwitch(nvcv::DataType dType, const Cb &cb) +{ + using uchar = unsigned char; + using ushort = unsigned short; + +#define NVCV_ADJUST_SHARPNESS_RUN_TYPED(DYN_BASE_TYPE, STATIC_BASE_TYPE) \ + ((dType == nvcv::TYPE_4##DYN_BASE_TYPE) || (dType == nvcv::TYPE_3##DYN_BASE_TYPE) \ + || (dType == nvcv::TYPE_2##DYN_BASE_TYPE) || (dType == nvcv::TYPE_##DYN_BASE_TYPE)) cb(STATIC_BASE_TYPE{}); + + // clang-format off + if NVCV_ADJUST_SHARPNESS_RUN_TYPED(U8, uchar) + else if NVCV_ADJUST_SHARPNESS_RUN_TYPED(U16, ushort) + else if NVCV_ADJUST_SHARPNESS_RUN_TYPED(F32, float) + else + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Invalid data type: AdjustSharpness supports 8-bit unsigned, 16-bit unsigned and 32-bit float"); + } + // clang-format on + +#undef NVCV_ADJUST_SHARPNESS_RUN_TYPED +} + +template +inline void RunChannelSwitch(int numChannels, int numPlanes, nvcv::DataType dType, const Cb &cb) +{ + RunTypeSwitch(dType, + [&numChannels, &numPlanes, &cb](auto dummyVal) + { + using ValBase = decltype(dummyVal); + // clang-format off + if (numChannels == 1) + { + using Val = cuda::MakeType; + if (numPlanes == 1) + { + cb(Val{}, std::integral_constant{}); + } + else + { + cb(Val{}, std::integral_constant{}); + } + } + else if (numChannels == 3) + { + cb(cuda::MakeType{}, std::integral_constant{}); + } + else if (numChannels == 4) + { + cb(cuda::MakeType{}, std::integral_constant{}); + } + else + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Invalid number of channels: AdjustSharpness supports 1, 3 or 4 channels"); + } + // clang-format on + }); +} + +// Validation ------------------------------------------------------------------------------ + +inline void ValidateSrcDstTensors(int &numInterleavedChannels, int &numPlanes, nvcv::DataType &dtype, + const nvcv::Optional &srcData, + const nvcv::Optional &dstData) +{ + if (!srcData) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input must be cuda-accessible, pitch-linear tensor"); + } + if (!dstData) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Output must be cuda-accessible, pitch-linear tensor"); + } + if (srcData->layout() != dstData->layout()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input and output must have the same layout"); + } + if (!(srcData->layout() == nvcv::TENSOR_HWC || srcData->layout() == nvcv::TENSOR_NHWC + || srcData->layout() == nvcv::TENSOR_CHW || srcData->layout() == nvcv::TENSOR_NCHW)) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input must have (N)HWC or (N)CHW layout"); + } + if (srcData->dtype() != dstData->dtype()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input and output must have the same data type"); + } + + auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcData); + auto dstAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*dstData); + NVCV_ASSERT(srcAccess && dstAccess); + + if (srcAccess->numSamples() != dstAccess->numSamples()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of samples"); + } + + int numChannels = srcAccess->numChannels(); + if (numChannels != dstAccess->numChannels()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of channels"); + } + + numPlanes = srcAccess->numPlanes(); + if (numPlanes != dstAccess->numPlanes()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of planes"); + } + if (numPlanes > 1 && numChannels == 2) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "2-channel planar images are not supported"); + } + + if (srcAccess->numCols() != dstAccess->numCols() || srcAccess->numRows() != dstAccess->numRows()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input and output must have matching width and height"); + } + + dtype = srcData->dtype(); + numInterleavedChannels = srcAccess->infoLayout().isChannelLast() ? numChannels : 1; +} + +inline auto ValidateSrcDstVarBatch(int &numInterleavedChannels, int &numPlanes, nvcv::DataType &dtype, + cudaStream_t stream, const nvcv::ImageBatchVarShape &src, + const nvcv::ImageBatchVarShape &dst) +{ + using maybeVarShape = nvcv::Optional; + std::tuple srcDstData{ + src.exportData(stream), + dst.exportData(stream)}; + auto &[srcData, dstData] = srcDstData; + + if (!srcData) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input must be cuda-accessible, varshape pitch-linear image batch"); + } + if (!dstData) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Output must be cuda-accessible, varshape pitch-linear image batch"); + } + + int numSamples = srcData->numImages(); + if (numSamples != dstData->numImages()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of samples"); + } + + const auto &srcFormat = srcData->uniqueFormat(); + const auto &dstFormat = dstData->uniqueFormat(); + if (!srcFormat || !dstFormat) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "All images in a batch must have the same format"); + } + if (srcFormat != dstFormat) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input and output must have the same format"); + } + + int numChannels = srcFormat.numChannels(); + numPlanes = srcFormat.numPlanes(); + if (numPlanes > 1 && numChannels == 2) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "2-channel planar images are not supported"); + } + + dtype = srcFormat.planeDataType(0); + for (int i = 1; i < numPlanes; ++i) + { + if (dtype != srcFormat.planeDataType(i)) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "All planes in the input image must have the same data type"); + } + } + + numInterleavedChannels = dtype.numChannels(); + + for (int i = 0; i < numSamples; i++) + { + if (src[i].size() != dst[i].size()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input and output must have matching width and height"); + } + } + + return srcDstData; +} + +inline void ValidateSharpnessFactor(float sharpnessFactor) +{ + if (sharpnessFactor < 0.0f) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Sharpness factor must be non-negative"); + } +} + +} // anonymous namespace + +namespace cvcuda::priv { + +AdjustSharpness::AdjustSharpness() {} + +// Tensor input variant +void AdjustSharpness::operator()(cudaStream_t stream, const nvcv::Tensor &src, const nvcv::Tensor &dst, + float sharpnessFactor) const +{ + ValidateSharpnessFactor(sharpnessFactor); + + int numInterleavedChannels; + int numPlanes; + nvcv::DataType dtype; + auto srcData = src.exportData(); + auto dstData = dst.exportData(); + ValidateSrcDstTensors(numInterleavedChannels, numPlanes, dtype, srcData, dstData); + + RunChannelSwitch(numInterleavedChannels, numPlanes, dtype, + [&stream, &srcData, &dstData, sharpnessFactor](auto dummyVal, auto isPlanar) + { + using ValueT = decltype(dummyVal); + using IsPlanar = decltype(isPlanar); + RunAdjustSharpness(stream, *srcData, *dstData, sharpnessFactor); + }); +} + +// VarShape input variant +void AdjustSharpness::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &src, + const nvcv::ImageBatchVarShape &dst, float sharpnessFactor) const +{ + ValidateSharpnessFactor(sharpnessFactor); + + int numInterleavedChannels; + int numPlanes; + nvcv::DataType dtype; + auto srcDstData = ValidateSrcDstVarBatch(numInterleavedChannels, numPlanes, dtype, stream, src, dst); + + RunChannelSwitch(numInterleavedChannels, numPlanes, dtype, + [&stream, &srcDstData, sharpnessFactor](auto dummyVal, auto isPlanar) + { + using ValueT = decltype(dummyVal); + using IsPlanar = decltype(isPlanar); + auto &[srcData, dstData] = srcDstData; + RunAdjustSharpness(stream, *srcData, *dstData, sharpnessFactor); + }); +} + +} // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpAdjustSharpness.hpp b/src/cvcuda/priv/OpAdjustSharpness.hpp new file mode 100644 index 000000000..5d61e33d1 --- /dev/null +++ b/src/cvcuda/priv/OpAdjustSharpness.hpp @@ -0,0 +1,48 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +/** + * @file OpAdjustSharpness.hpp + * + * @brief Defines the private C++ Class for the AdjustSharpness operation. + */ + +#ifndef CVCUDA_PRIV__ADJUST_SHARPNESS_HPP +#define CVCUDA_PRIV__ADJUST_SHARPNESS_HPP + +#include "IOperator.hpp" + +#include +#include +#include + +namespace cvcuda::priv { + +class AdjustSharpness final : public IOperator +{ +public: + explicit AdjustSharpness(); + + void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, float sharpnessFactor) const; + + void operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, + float sharpnessFactor) const; +}; + +} // namespace cvcuda::priv + +#endif // CVCUDA_PRIV__ADJUST_SHARPNESS_HPP diff --git a/src/cvcuda/priv/OpAdvCvtColor.cu b/src/cvcuda/priv/OpAdvCvtColor.cu index 645cbee27..f8b1c49ec 100644 --- a/src/cvcuda/priv/OpAdvCvtColor.cu +++ b/src/cvcuda/priv/OpAdvCvtColor.cu @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,6 +15,8 @@ * limitations under the License. */ +#include "CudaDeviceUtils.hpp" +#include "Nvtx.hpp" #include "OpAdvCvtColor.hpp" #include "legacy/CvCudaLegacy.h" #include "legacy/CvCudaLegacyHelpers.hpp" @@ -30,6 +32,18 @@ #define BLOCK 32 +constexpr int kPlanar444RowsPerThread = 4; +constexpr int kPlanar444SM89RowsPerThread = 1; + +constexpr int Planar444RowsPerThreadForSM(int sm) +{ + return sm == 89 ? kPlanar444SM89RowsPerThread : kPlanar444RowsPerThread; +} + +static_assert(Planar444RowsPerThreadForSM(89) == 1); +static_assert(Planar444RowsPerThreadForSM(86) == 4); +static_assert(Planar444RowsPerThreadForSM(90) == 4); + namespace legacy = nvcv::legacy::cuda_op; namespace cuda = nvcv::cuda; @@ -169,6 +183,53 @@ __global__ void yuv_to_bgr_char_nhwc(SrcWrapper src, DstWrapper dst, int2 dstSiz *dst.ptr(batch_idx, dst_y, dst_x, bidx ^ 2) = cuda::SaturateCast(r); } +template +__global__ void yuv_to_bgr_char_nchw(SrcWrapper src, DstWrapper dst, int2 dstSize, int bidx, + const YUV2RGBConstants cooef) +{ + int dst_x = blockIdx.x * blockDim.x + threadIdx.x; + int dst_y0 = blockIdx.y * blockDim.y * RowsPerThread + threadIdx.y; + + if (dst_x >= dstSize.x || dst_y0 >= dstSize.y) + return; + const int batch_idx = get_batch_idx(); + + T Y[RowsPerThread]; + T Cb[RowsPerThread]; + T Cr[RowsPerThread]; + +#pragma unroll + for (int i = 0; i < RowsPerThread; ++i) + { + int dst_y = dst_y0 + i * blockDim.y; + if (dst_y < dstSize.y) + { + Y[i] = *src.ptr(batch_idx, 0, dst_y, dst_x); + Cb[i] = *src.ptr(batch_idx, 1, dst_y, dst_x); + Cr[i] = *src.ptr(batch_idx, 2, dst_y, dst_x); + } + } + + int C0 = cooef.V2R, C1 = cooef.V2G, C2 = cooef.U2G, C3 = cooef.U2B; + int delta = ((T)(cuda::TypeTraits::max / 2 + 1)); + +#pragma unroll + for (int i = 0; i < RowsPerThread; ++i) + { + int dst_y = dst_y0 + i * blockDim.y; + if (dst_y < dstSize.y) + { + int b = Y[i] + CV_DESCALE((Cb[i] - delta) * C3, yuv_shift); + int g = Y[i] + CV_DESCALE((Cb[i] - delta) * C2 + (Cr[i] - delta) * C1, yuv_shift); + int r = Y[i] + CV_DESCALE((Cr[i] - delta) * C0, yuv_shift); + + *dst.ptr(batch_idx, bidx, dst_y, dst_x) = cuda::SaturateCast(b); + *dst.ptr(batch_idx, 1, dst_y, dst_x) = cuda::SaturateCast(g); + *dst.ptr(batch_idx, bidx ^ 2, dst_y, dst_x) = cuda::SaturateCast(r); + } + } +} + template __global__ void bgr_to_yuv_char_nhwc(SrcWrapper src, DstWrapper dst, int2 dstSize, int bidx, const RGB2YUVConstants cooef) @@ -193,6 +254,52 @@ __global__ void bgr_to_yuv_char_nhwc(SrcWrapper src, DstWrapper dst, int2 dstSiz *dst.ptr(batch_idx, dst_y, dst_x, 2) = cuda::SaturateCast(V); } +template +__global__ void bgr_to_yuv_char_nchw(SrcWrapper src, DstWrapper dst, int2 dstSize, int bidx, + const RGB2YUVConstants cooef) +{ + int dst_x = blockIdx.x * blockDim.x + threadIdx.x; + int dst_y0 = blockIdx.y * blockDim.y * RowsPerThread + threadIdx.y; + if (dst_x >= dstSize.x || dst_y0 >= dstSize.y) + return; + const int batch_idx = get_batch_idx(); + + int B[RowsPerThread]; + int G[RowsPerThread]; + int R[RowsPerThread]; + +#pragma unroll + for (int i = 0; i < RowsPerThread; ++i) + { + int dst_y = dst_y0 + i * blockDim.y; + if (dst_y < dstSize.y) + { + B[i] = *src.ptr(batch_idx, bidx, dst_y, dst_x); + G[i] = *src.ptr(batch_idx, 1, dst_y, dst_x); + R[i] = *src.ptr(batch_idx, bidx ^ 2, dst_y, dst_x); + } + } + + int C0 = cooef.R2Y, C1 = cooef.G2Y, C2 = cooef.B2Y, C3 = cooef.R2V, C4 = cooef.B2U; + int delta = ((T)(cuda::TypeTraits::max / 2 + 1)) * (1 << yuv_shift); + +#pragma unroll + for (int i = 0; i < RowsPerThread; ++i) + { + int dst_y = dst_y0 + i * blockDim.y; + if (dst_y < dstSize.y) + { + int Y = CV_DESCALE(R[i] * C0 + G[i] * C1 + B[i] * C2, yuv_shift); + int V = CV_DESCALE((R[i] - Y) * C3 + delta, yuv_shift); //Cr + int U = CV_DESCALE((B[i] - Y) * C4 + delta, yuv_shift); //Cb + + *dst.ptr(batch_idx, 0, dst_y, dst_x) = cuda::SaturateCast(Y); + *dst.ptr(batch_idx, 1, dst_y, dst_x) = cuda::SaturateCast(U); + *dst.ptr(batch_idx, 2, dst_y, dst_x) = cuda::SaturateCast(V); + } + } +} + template __device__ __forceinline__ void yuv_to_bgr_kernel(const T &Y, const T &U, const T &V, T &r, T &g, T &b, const YUV2RGBConstants cooef) @@ -219,9 +326,69 @@ __device__ __forceinline__ void bgr_to_yuv420_kernel(const T &r, const T &g, con u = CV_DESCALE((b - y) * C4, yuv_shift); //Cb } +__device__ __forceinline__ void store_rgb_pair(uint8_t *dst, const uchar3 &pixel0, const uchar3 &pixel1) +{ + if ((reinterpret_cast(dst) & (alignof(uchar4) - 1)) == 0) + { + *reinterpret_cast(dst) = make_uchar4(pixel0.x, pixel0.y, pixel0.z, pixel1.x); + *reinterpret_cast(dst + sizeof(uchar4)) = make_uchar2(pixel1.y, pixel1.z); + } + else + { + *reinterpret_cast(dst) = make_uchar2(pixel0.x, pixel0.y); + *reinterpret_cast(dst + sizeof(uchar2)) = make_uchar4(pixel0.z, pixel1.x, pixel1.y, pixel1.z); + } +} + template __global__ void yuv420sp_to_bgr_char_nhwc(SrcWrapper src, DstWrapper dst, int2 dstSize, int dcn, int bidx, int uidx, - const YUV2RGBConstants cooef) + const YUV2RGBConstants cooef, bool packedOutput) +{ + int dst_x = 2 * (blockIdx.x * blockDim.x + threadIdx.x); + int dst_y = 2 * (blockIdx.y * blockDim.y + threadIdx.y); + if (dst_x >= dstSize.x || dst_y >= dstSize.y) + return; + const int batch_idx = get_batch_idx(); + + // The four luma pixels in this tile share one subsampled chroma pair. + T U = *src.ptr(batch_idx, dstSize.y + dst_y / 2, dst_x + uidx); + T V = *src.ptr(batch_idx, dstSize.y + dst_y / 2, dst_x + 1 - uidx); + +#pragma unroll + for (int dy = 0; dy < 2; ++dy) + { + uchar3 pixels[2]; +#pragma unroll + for (int dx = 0; dx < 2; ++dx) + { + T Y = *src.ptr(batch_idx, dst_y + dy, dst_x + dx, 0); + uint8_t r{0}, g{0}, b{0}; + yuv_to_bgr_kernel(Y, U, V, r, g, b, cooef); + + pixels[dx] = bidx == 0 ? make_uchar3(b, g, r) : make_uchar3(r, g, b); + } + + uint8_t *dstPtr = dst.ptr(batch_idx, dst_y + dy, dst_x, 0); + if (dcn == 3 && packedOutput && (reinterpret_cast(dstPtr) & (alignof(uchar2) - 1)) == 0) + { + store_rgb_pair(dstPtr, pixels[0], pixels[1]); + } + else + { +#pragma unroll + for (int dx = 0; dx < 2; ++dx) + { + *dst.ptr(batch_idx, dst_y + dy, dst_x + dx, 0) = pixels[dx].x; + *dst.ptr(batch_idx, dst_y + dy, dst_x + dx, 1) = pixels[dx].y; + *dst.ptr(batch_idx, dst_y + dy, dst_x + dx, 2) = pixels[dx].z; + } + } + } +} + +template +__global__ void yuv420sp_to_bgra_char_nhwc(SrcWrapper src, DstWrapper dst, int2 dstSize, int bidx, int uidx, + const YUV2RGBConstants cooef) { int dst_x = blockIdx.x * blockDim.x + threadIdx.x; int dst_y = blockIdx.y * blockDim.y + threadIdx.y; @@ -234,15 +401,47 @@ __global__ void yuv420sp_to_bgr_char_nhwc(SrcWrapper src, DstWrapper dst, int2 d T U = *src.ptr(batch_idx, dstSize.y + dst_y / 2, uv_x + uidx); T V = *src.ptr(batch_idx, dstSize.y + dst_y / 2, uv_x + 1 - uidx); - uint8_t r{0}, g{0}, b{0}, a{0xff}; + uint8_t r{0}, g{0}, b{0}; yuv_to_bgr_kernel(Y, U, V, r, g, b, cooef); *dst.ptr(batch_idx, dst_y, dst_x, bidx) = b; *dst.ptr(batch_idx, dst_y, dst_x, 1) = g; *dst.ptr(batch_idx, dst_y, dst_x, bidx ^ 2) = r; - if (dcn == 4) + *dst.ptr(batch_idx, dst_y, dst_x, 3) = 0xff; +} + +template +__global__ void yuv420sp_to_bgr_char_nchw(SrcWrapper src, DstWrapper dst, int2 dstSize, int dcn, int bidx, int uidx, + const YUV2RGBConstants cooef) +{ + int dst_x = 2 * (blockIdx.x * blockDim.x + threadIdx.x); + int dst_y = 2 * (blockIdx.y * blockDim.y + threadIdx.y); + if (dst_x >= dstSize.x || dst_y >= dstSize.y) + return; + const int batch_idx = get_batch_idx(); + + // The four luma pixels in this tile share one subsampled chroma pair. + T U = *src.ptr(batch_idx, 0, dstSize.y + dst_y / 2, dst_x + uidx); + T V = *src.ptr(batch_idx, 0, dstSize.y + dst_y / 2, dst_x + 1 - uidx); + +#pragma unroll + for (int dy = 0; dy < 2; ++dy) { - *dst.ptr(batch_idx, dst_y, dst_x, 3) = a; +#pragma unroll + for (int dx = 0; dx < 2; ++dx) + { + T Y = *src.ptr(batch_idx, 0, dst_y + dy, dst_x + dx); + uint8_t r{0}, g{0}, b{0}; + yuv_to_bgr_kernel(Y, U, V, r, g, b, cooef); + + *dst.ptr(batch_idx, bidx, dst_y + dy, dst_x + dx) = b; + *dst.ptr(batch_idx, 1, dst_y + dy, dst_x + dx) = g; + *dst.ptr(batch_idx, bidx ^ 2, dst_y + dy, dst_x + dx) = r; + if (dcn == 4) + { + *dst.ptr(batch_idx, 3, dst_y + dy, dst_x + dx) = 0xff; + } + } } } @@ -250,41 +449,29 @@ template= srcSize.x || src_y >= srcSize.y) return; const int batch_idx = get_batch_idx(); - int uv_x = (src_x % 2 == 0) ? src_x : (src_x - 1); + // A single thread owns the tile so the four luma values and averaged chroma pair reuse the same pixel loads. uint8_t b0 = static_cast(*src.ptr(batch_idx, src_y, src_x, bidx)); uint8_t g0 = static_cast(*src.ptr(batch_idx, src_y, src_x, 1)); uint8_t r0 = static_cast(*src.ptr(batch_idx, src_y, src_x, bidx ^ 2)); - - // compute Y for every pixel - int Y0{0}, U0{0}, V0{0}; + int Y0{0}, U0{0}, V0{0}; bgr_to_yuv420_kernel(r0, g0, b0, Y0, U0, V0, cooef); - // Write the Y plane - *dst.ptr(batch_idx, src_y, src_x, 0) = cuda::SaturateCast(Y0); - - // compute U and V for every 2x2 block - if (src_x >= srcSize.x - 1 || src_y >= srcSize.y - 1) - return; //bail out since we need 2x2 block to compute U and V - - if (src_x % 2 || src_y % 2) - return; //bail out since we need 2x2 block to compute U and V skip all odd pixels for u and v - - uint8_t b1 = static_cast(*src.ptr(batch_idx, src_y + 0, src_x + 1, bidx)); - uint8_t g1 = static_cast(*src.ptr(batch_idx, src_y + 0, src_x + 1, 1)); - uint8_t r1 = static_cast(*src.ptr(batch_idx, src_y + 0, src_x + 1, bidx ^ 2)); + uint8_t b1 = static_cast(*src.ptr(batch_idx, src_y, src_x + 1, bidx)); + uint8_t g1 = static_cast(*src.ptr(batch_idx, src_y, src_x + 1, 1)); + uint8_t r1 = static_cast(*src.ptr(batch_idx, src_y, src_x + 1, bidx ^ 2)); int Y1{0}, U1{0}, V1{0}; bgr_to_yuv420_kernel(r1, g1, b1, Y1, U1, V1, cooef); - uint8_t b2 = static_cast(*src.ptr(batch_idx, src_y + 1, src_x + 0, bidx)); - uint8_t g2 = static_cast(*src.ptr(batch_idx, src_y + 1, src_x + 0, 1)); - uint8_t r2 = static_cast(*src.ptr(batch_idx, src_y + 1, src_x + 0, bidx ^ 2)); + uint8_t b2 = static_cast(*src.ptr(batch_idx, src_y + 1, src_x, bidx)); + uint8_t g2 = static_cast(*src.ptr(batch_idx, src_y + 1, src_x, 1)); + uint8_t r2 = static_cast(*src.ptr(batch_idx, src_y + 1, src_x, bidx ^ 2)); int Y2{0}, U2{0}, V2{0}; bgr_to_yuv420_kernel(r2, g2, b2, Y2, U2, V2, cooef); @@ -296,10 +483,66 @@ __global__ void bgr_to_yuv420sp_char_nhwc(SrcWrapper src, DstWrapper dst, int2 s int Y3{0}, U3{0}, V3{0}; bgr_to_yuv420_kernel(r3, g3, b3, Y3, U3, V3, cooef); + *dst.ptr(batch_idx, src_y, src_x, 0) = cuda::SaturateCast(Y0); + *dst.ptr(batch_idx, src_y, src_x + 1, 0) = cuda::SaturateCast(Y1); + *dst.ptr(batch_idx, src_y + 1, src_x, 0) = cuda::SaturateCast(Y2); + *dst.ptr(batch_idx, src_y + 1, src_x + 1, 0) = cuda::SaturateCast(Y3); + int delta = ((T)(cuda::TypeTraits::max / 2 + 1)); // non scaled delta in this kernel - *dst.ptr(batch_idx, srcSize.y + src_y / 2, uv_x + uidx) = cuda::SaturateCast((U0 + U1 + U2 + U3) / 4 + delta); - *dst.ptr(batch_idx, srcSize.y + src_y / 2, uv_x + (1 - uidx)) + *dst.ptr(batch_idx, srcSize.y + src_y / 2, src_x + uidx) = cuda::SaturateCast((U0 + U1 + U2 + U3) / 4 + delta); + *dst.ptr(batch_idx, srcSize.y + src_y / 2, src_x + (1 - uidx)) + = cuda::SaturateCast((V0 + V1 + V2 + V3) / 4 + delta); +} + +template +__global__ void bgr_to_yuv420sp_char_nchw(SrcWrapper src, DstWrapper dst, int2 srcSize, int scn, int bidx, int uidx, + const RGB2YUVConstants cooef) +{ + int src_x = 2 * (blockIdx.x * blockDim.x + threadIdx.x); + int src_y = 2 * (blockIdx.y * blockDim.y + threadIdx.y); + if (src_x >= srcSize.x || src_y >= srcSize.y) + return; + const int batch_idx = get_batch_idx(); + + // A single thread owns the tile so the four luma values and averaged chroma pair reuse the same pixel loads. + uint8_t b0 = static_cast(*src.ptr(batch_idx, bidx, src_y, src_x)); + uint8_t g0 = static_cast(*src.ptr(batch_idx, 1, src_y, src_x)); + uint8_t r0 = static_cast(*src.ptr(batch_idx, bidx ^ 2, src_y, src_x)); + int Y0{0}, U0{0}, V0{0}; + bgr_to_yuv420_kernel(r0, g0, b0, Y0, U0, V0, cooef); + + uint8_t b1 = static_cast(*src.ptr(batch_idx, bidx, src_y, src_x + 1)); + uint8_t g1 = static_cast(*src.ptr(batch_idx, 1, src_y, src_x + 1)); + uint8_t r1 = static_cast(*src.ptr(batch_idx, bidx ^ 2, src_y, src_x + 1)); + + int Y1{0}, U1{0}, V1{0}; + bgr_to_yuv420_kernel(r1, g1, b1, Y1, U1, V1, cooef); + + uint8_t b2 = static_cast(*src.ptr(batch_idx, bidx, src_y + 1, src_x)); + uint8_t g2 = static_cast(*src.ptr(batch_idx, 1, src_y + 1, src_x)); + uint8_t r2 = static_cast(*src.ptr(batch_idx, bidx ^ 2, src_y + 1, src_x)); + + int Y2{0}, U2{0}, V2{0}; + bgr_to_yuv420_kernel(r2, g2, b2, Y2, U2, V2, cooef); + + uint8_t b3 = static_cast(*src.ptr(batch_idx, bidx, src_y + 1, src_x + 1)); + uint8_t g3 = static_cast(*src.ptr(batch_idx, 1, src_y + 1, src_x + 1)); + uint8_t r3 = static_cast(*src.ptr(batch_idx, bidx ^ 2, src_y + 1, src_x + 1)); + + int Y3{0}, U3{0}, V3{0}; + bgr_to_yuv420_kernel(r3, g3, b3, Y3, U3, V3, cooef); + + *dst.ptr(batch_idx, 0, src_y, src_x) = cuda::SaturateCast(Y0); + *dst.ptr(batch_idx, 0, src_y, src_x + 1) = cuda::SaturateCast(Y1); + *dst.ptr(batch_idx, 0, src_y + 1, src_x) = cuda::SaturateCast(Y2); + *dst.ptr(batch_idx, 0, src_y + 1, src_x + 1) = cuda::SaturateCast(Y3); + + int delta = ((T)(cuda::TypeTraits::max / 2 + 1)); // non scaled delta in this kernel + + *dst.ptr(batch_idx, 0, srcSize.y + src_y / 2, src_x + uidx) + = cuda::SaturateCast((U0 + U1 + U2 + U3) / 4 + delta); + *dst.ptr(batch_idx, 0, srcSize.y + src_y / 2, src_x + (1 - uidx)) = cuda::SaturateCast((V0 + V1 + V2 + V3) / 4 + delta); } @@ -356,7 +599,7 @@ static bool isSupportedConversionCode(NVCVColorConversionCode code) static bool isSupportedColorSpec(nvcv::ColorSpec spec) { //may need to be extended to check for conversion type - switch (spec) + switch (static_cast(spec)) { case NVCV_COLOR_SPEC_BT601: case NVCV_COLOR_SPEC_BT709: @@ -402,11 +645,24 @@ static bool areCorrectSizes(const NVCVColorConversionCode code, const nvcv::Tens return true; } +static bool isInterleavedTensorLayout(nvcv::TensorLayout layout) +{ + return layout == nvcv::TENSOR_NHWC || layout == nvcv::TENSOR_HWC; +} + +static bool isPlanarTensorLayout(nvcv::TensorLayout layout) +{ + return layout == nvcv::TENSOR_NCHW || layout == nvcv::TENSOR_CHW; +} + static bool checkInputOutputTensors(NVCVColorConversionCode code, const nvcv::TensorDataStridedCuda &in, const nvcv::TensorDataStridedCuda &out) { - if ((in.layout() == nvcv::TENSOR_NHWC || in.layout() == nvcv::TENSOR_HWC) - && (out.layout() == nvcv::TENSOR_HWC || out.layout() == nvcv::TENSOR_NHWC)) + if (isInterleavedTensorLayout(in.layout()) && isInterleavedTensorLayout(out.layout())) + { + return true; + } + if (isPlanarTensorLayout(in.layout()) && isPlanarTensorLayout(out.layout())) { return true; } @@ -415,7 +671,7 @@ static bool checkInputOutputTensors(NVCVColorConversionCode code, const nvcv::Te static const RGB2YUVConstants &getRGB2YUVCooef(nvcv::ColorSpec spec) { - switch (spec) + switch (static_cast(spec)) { case NVCV_COLOR_SPEC_BT601: return rgb2yuv_601; @@ -430,7 +686,7 @@ static const RGB2YUVConstants &getRGB2YUVCooef(nvcv::ColorSpec spec) static const YUV2RGBConstants &getYUV2RGBCooef(nvcv::ColorSpec spec) { - switch (spec) + switch (static_cast(spec)) { case NVCV_COLOR_SPEC_BT601: return yuv2rgb_601; @@ -450,6 +706,7 @@ AdvCvtColor::AdvCvtColor() {} void AdvCvtColor::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, NVCVColorConversionCode code, nvcv::ColorSpec spec) const { + CVCUDA_NVTX_RANGE("cvcuda::AdvCvtColor::operator()[Tensor]"); auto inData = in.exportData(); if (inData == nullptr) { @@ -464,6 +721,18 @@ void AdvCvtColor::operator()(cudaStream_t stream, const nvcv::Tensor &in, const "Output must be cuda-accessible, pitch-linear tensor"); } + if (inData->dtype() != nvcv::TYPE_U8) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input data type must be U8. Unsupported data type."); + } + + if (outData->dtype() != nvcv::TYPE_U8) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Output data type must be U8. Unsupported data type."); + } + //check compatibility if (isSupportedConversionCode(code) == false) { @@ -540,11 +809,34 @@ void AdvCvtColor::Yuv2Bgr(cudaStream_t stream, const nvcv::TensorDataStridedCuda const YUV2RGBConstants &cooef = getYUV2RGBCooef(spec); auto outMaxStride = outAccess->sampleStride() * outAccess->numSamples(); auto inMaxStride = inAccess->sampleStride() * inAccess->numSamples(); + bool isPlanar = isPlanarTensorLayout(in.layout()); if (std::max(outMaxStride, inMaxStride) <= cuda::TypeTraits::max) { - auto srcWrap = cuda::CreateTensorWrapNHWC(in); - auto dstWrap = cuda::CreateTensorWrapNHWC(out); - yuv_to_bgr_char_nhwc<<>>(srcWrap, dstWrap, dstSize, bidx, cooef); + if (isPlanar) + { + auto srcWrap = cuda::CreateTensorWrapNCHW(in); + auto dstWrap = cuda::CreateTensorWrapNCHW(out); + int sm; + NVCV_CHECK_THROW(GetCurrentDeviceSM(sm)); + int rowsPerThread = Planar444RowsPerThreadForSM(sm); + dim3 planarGridSize(gridSize.x, legacy::divUp(inputShape.H, blockSize.y * rowsPerThread), gridSize.z); + if (rowsPerThread == kPlanar444SM89RowsPerThread) + { + yuv_to_bgr_char_nchw + <<>>(srcWrap, dstWrap, dstSize, bidx, cooef); + } + else + { + yuv_to_bgr_char_nchw + <<>>(srcWrap, dstWrap, dstSize, bidx, cooef); + } + } + else + { + auto srcWrap = cuda::CreateTensorWrapNHWC(in); + auto dstWrap = cuda::CreateTensorWrapNHWC(out); + yuv_to_bgr_char_nhwc<<>>(srcWrap, dstWrap, dstSize, bidx, cooef); + } } else { @@ -587,11 +879,34 @@ void AdvCvtColor::Bgr2Yuv(cudaStream_t stream, const nvcv::TensorDataStridedCuda const RGB2YUVConstants &cooef = getRGB2YUVCooef(spec); auto outMaxStride = outAccess->sampleStride() * outAccess->numSamples(); auto inMaxStride = inAccess->sampleStride() * inAccess->numSamples(); + bool isPlanar = isPlanarTensorLayout(in.layout()); if (std::max(outMaxStride, inMaxStride) <= cuda::TypeTraits::max) { - auto srcWrap = cuda::CreateTensorWrapNHWC(in); - auto dstWrap = cuda::CreateTensorWrapNHWC(out); - bgr_to_yuv_char_nhwc<<>>(srcWrap, dstWrap, dstSize, bidx, cooef); + if (isPlanar) + { + auto srcWrap = cuda::CreateTensorWrapNCHW(in); + auto dstWrap = cuda::CreateTensorWrapNCHW(out); + int sm; + NVCV_CHECK_THROW(GetCurrentDeviceSM(sm)); + int rowsPerThread = Planar444RowsPerThreadForSM(sm); + dim3 planarGridSize(gridSize.x, legacy::divUp(inputShape.H, blockSize.y * rowsPerThread), gridSize.z); + if (rowsPerThread == kPlanar444SM89RowsPerThread) + { + bgr_to_yuv_char_nchw + <<>>(srcWrap, dstWrap, dstSize, bidx, cooef); + } + else + { + bgr_to_yuv_char_nchw + <<>>(srcWrap, dstWrap, dstSize, bidx, cooef); + } + } + else + { + auto srcWrap = cuda::CreateTensorWrapNHWC(in); + auto dstWrap = cuda::CreateTensorWrapNHWC(out); + bgr_to_yuv_char_nhwc<<>>(srcWrap, dstWrap, dstSize, bidx, cooef); + } } else { @@ -647,8 +962,9 @@ void AdvCvtColor::NvYuv2Bgr(cudaStream_t stream, const nvcv::TensorDataStridedCu throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "invalid output shape given input"); } - dim3 blockSize(BLOCK, BLOCK / 1, 1); - dim3 gridSize(legacy::divUp(rgb_width, blockSize.x), legacy::divUp(rgb_height, blockSize.y), inputShape.N); + dim3 tiledBlockSize(BLOCK, BLOCK / 4, 1); + dim3 tiledGridSize(legacy::divUp(rgb_width, tiledBlockSize.x * 2), legacy::divUp(rgb_height, tiledBlockSize.y * 2), + inputShape.N); int2 dstSize{outputShape.W, outputShape.H}; int dcn = outputShape.C; @@ -659,12 +975,35 @@ void AdvCvtColor::NvYuv2Bgr(cudaStream_t stream, const nvcv::TensorDataStridedCu const YUV2RGBConstants &cooef = getYUV2RGBCooef(spec); auto outMaxStride = outAccess->sampleStride() * outAccess->numSamples(); auto inMaxStride = inAccess->sampleStride() * inAccess->numSamples(); + bool isPlanar = isPlanarTensorLayout(in.layout()); if (std::max(outMaxStride, inMaxStride) <= cuda::TypeTraits::max) { - auto srcWrap = cuda::CreateTensorWrapNHWC(in); - auto dstWrap = cuda::CreateTensorWrapNHWC(out); - yuv420sp_to_bgr_char_nhwc<<>>(srcWrap, dstWrap, dstSize, dcn, bidx, uidx, - cooef); + if (isPlanar) + { + auto srcWrap = cuda::CreateTensorWrapNCHW(in); + auto dstWrap = cuda::CreateTensorWrapNCHW(out); + yuv420sp_to_bgr_char_nchw<<>>(srcWrap, dstWrap, dstSize, dcn, + bidx, uidx, cooef); + } + else + { + auto srcWrap = cuda::CreateTensorWrapNHWC(in); + auto dstWrap = cuda::CreateTensorWrapNHWC(out); + if (dcn == 4) + { + dim3 blockSize(BLOCK, BLOCK / 4, 1); + dim3 gridSize(legacy::divUp(rgb_width, blockSize.x), legacy::divUp(rgb_height, blockSize.y), + inputShape.N); + yuv420sp_to_bgra_char_nhwc<<>>(srcWrap, dstWrap, dstSize, bidx, + uidx, cooef); + } + else + { + const bool packedOutput = outAccess->chStride() == 1 && outAccess->colStride() == dcn; + yuv420sp_to_bgr_char_nhwc<<>>( + srcWrap, dstWrap, dstSize, dcn, bidx, uidx, cooef, packedOutput); + } + } } else { @@ -717,8 +1056,9 @@ void AdvCvtColor::Bgr2NvYuv(cudaStream_t stream, const nvcv::TensorDataStridedCu } int2 srcSize{inputShape.W, inputShape.H}; - dim3 blockSize(BLOCK, BLOCK / 1, 1); - dim3 gridSize(legacy::divUp(inputShape.W, blockSize.x), legacy::divUp(inputShape.H, blockSize.y), inputShape.N); + dim3 blockSize(BLOCK, BLOCK / 4, 1); + dim3 gridSize(legacy::divUp(inputShape.W, blockSize.x * 2), legacy::divUp(inputShape.H, blockSize.y * 2), + inputShape.N); switch (inDataType) { @@ -727,12 +1067,23 @@ void AdvCvtColor::Bgr2NvYuv(cudaStream_t stream, const nvcv::TensorDataStridedCu const RGB2YUVConstants &cooef = getRGB2YUVCooef(spec); auto outMaxStride = outAccess->sampleStride() * outAccess->numSamples(); auto inMaxStride = inAccess->sampleStride() * inAccess->numSamples(); + bool isPlanar = isPlanarTensorLayout(in.layout()); if (std::max(outMaxStride, inMaxStride) <= cuda::TypeTraits::max) { - auto srcWrap = cuda::CreateTensorWrapNHWC(in); - auto dstWrap = cuda::CreateTensorWrapNHWC(out); - bgr_to_yuv420sp_char_nhwc<<>>(srcWrap, dstWrap, srcSize, inputShape.C, bidx, - uidx, cooef); + if (isPlanar) + { + auto srcWrap = cuda::CreateTensorWrapNCHW(in); + auto dstWrap = cuda::CreateTensorWrapNCHW(out); + bgr_to_yuv420sp_char_nchw<<>>(srcWrap, dstWrap, srcSize, inputShape.C, + bidx, uidx, cooef); + } + else + { + auto srcWrap = cuda::CreateTensorWrapNHWC(in); + auto dstWrap = cuda::CreateTensorWrapNHWC(out); + bgr_to_yuv420sp_char_nhwc<<>>(srcWrap, dstWrap, srcSize, inputShape.C, + bidx, uidx, cooef); + } } else { diff --git a/src/cvcuda/priv/OpAutoContrast.cu b/src/cvcuda/priv/OpAutoContrast.cu new file mode 100644 index 000000000..de290fb78 --- /dev/null +++ b/src/cvcuda/priv/OpAutoContrast.cu @@ -0,0 +1,1505 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "Nvtx.hpp" +#include "OpAutoContrast.hpp" +#include "PerDeviceResource.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace cuda = nvcv::cuda; + +namespace { + +constexpr int WARP_WIDTH = 32; + +// Reduce each warp with shuffles, write one per-warp pair to shared memory, and +// have the first warp fold those pairs. nThreads must be a multiple of 32 (the +// reduction launches use 128 or 256 threads). partialStride is the number of +// CTA partials for one (sample, channel). +template +inline __device__ void ReduceBlockToPartials(float *sm, int nThreads, int tid, int nChan, const float (&vMin)[MAXC], + const float (&vMax)[MAXC], float *loBase, float *hiBase, int partialStride) +{ + const int lane = tid & (WARP_WIDTH - 1); + const int warp = tid / WARP_WIDTH; + const int numWarps = nThreads / WARP_WIDTH; + float *sMin = sm; + float *sMax = sm + nChan * numWarps; +#pragma unroll + for (int c = 0; c < MAXC; ++c) + { + if (c < nChan) + { + float warpMin = vMin[c]; + float warpMax = vMax[c]; +#pragma unroll + for (int offset = WARP_WIDTH / 2; offset > 0; offset >>= 1) + { + warpMin = fminf(warpMin, __shfl_down_sync(0xffffffffu, warpMin, offset)); + warpMax = fmaxf(warpMax, __shfl_down_sync(0xffffffffu, warpMax, offset)); + } + if (lane == 0) + { + sMin[c * numWarps + warp] = warpMin; + sMax[c * numWarps + warp] = warpMax; + } + } + } + __syncthreads(); + + if (warp == 0) + { +#pragma unroll + for (int c = 0; c < MAXC; ++c) + { + if (c < nChan) + { + float blockMin = lane < numWarps ? sMin[c * numWarps + lane] : CUDART_INF_F; + float blockMax = lane < numWarps ? sMax[c * numWarps + lane] : -CUDART_INF_F; +#pragma unroll + for (int offset = WARP_WIDTH / 2; offset > 0; offset >>= 1) + { + blockMin = fminf(blockMin, __shfl_down_sync(0xffffffffu, blockMin, offset)); + blockMax = fmaxf(blockMax, __shfl_down_sync(0xffffffffu, blockMax, offset)); + } + if (lane == 0) + { + const int partialOffset = c * partialStride; + loBase[partialOffset] = blockMin; + hiBase[partialOffset] = blockMax; + } + } + } + } + __syncthreads(); // required when a caller reuses the reduction shared memory +} + +// The VarShape planar-F32 kernel carries enough wrapper state that the shuffle +// fold crosses a register-allocation boundary on SM80/SM90. Retaining the +// shared-memory tree for that specialization preserves full CTA residency. +template +inline __device__ void ReduceBlockToPartialsShared(float *sm, int nThreads, int tid, int nChan, + const float (&vMin)[MAXC], const float (&vMax)[MAXC], float *loBase, + float *hiBase, int partialStride) +{ + float *sMin = sm; + float *sMax = sm + nChan * nThreads; +#pragma unroll + for (int c = 0; c < MAXC; ++c) + { + if (c < nChan) + { + sMin[c * nThreads + tid] = vMin[c]; + sMax[c * nThreads + tid] = vMax[c]; + } + } + __syncthreads(); + + for (int stride = nThreads >> 1; stride > 0; stride >>= 1) + { + if (tid < stride) + { + for (int c = 0; c < nChan; ++c) + { + sMin[c * nThreads + tid] = fminf(sMin[c * nThreads + tid], sMin[c * nThreads + tid + stride]); + sMax[c * nThreads + tid] = fmaxf(sMax[c * nThreads + tid], sMax[c * nThreads + tid + stride]); + } + } + __syncthreads(); + } + + if (tid == 0) + { + for (int c = 0; c < nChan; ++c) + { + const int partialOffset = c * partialStride; + loBase[partialOffset] = sMin[c * nThreads]; + hiBase[partialOffset] = sMax[c * nThreads]; + } + } + __syncthreads(); // required when a caller reuses the reduction shared memory +} + +// One block deterministically folds all CTA partials for one (sample, channel). +// The grid has exactly numSamples*numChannels blocks, so there is no global +// fan-in, atomic retry, or output aliasing in this pass. +__global__ void FinalizeMinMax(const float *partialLo, const float *partialHi, float *lo, float *hi, size_t numPartials) +{ + extern __shared__ float sm[]; + const int tid = threadIdx.x; + const size_t base = static_cast(blockIdx.x) * numPartials; + float vMin[1]{CUDART_INF_F}; + float vMax[1]{-CUDART_INF_F}; + + for (size_t i = static_cast(tid); i < numPartials; i += blockDim.x) + { + vMin[0] = fminf(vMin[0], partialLo[base + i]); + vMax[0] = fmaxf(vMax[0], partialHi[base + i]); + } + ReduceBlockToPartials<1>(sm, blockDim.x, tid, 1, vMin, vMax, &lo[blockIdx.x], &hi[blockIdx.x], 1); +} + +static __device__ __noinline__ float RemapWideFloat(float in, float lo, float hi) +{ + const double wideLo = static_cast(lo); + return static_cast((static_cast(in) - wideLo) / (static_cast(hi) - wideLo)); +} + +// AutoContrast remap, shared by every layout/container. A flat channel passes +// through unchanged. Integer results are clamped and truncated to match Pillow +// and torchvision; floating-point results retain the scaled value. +template +inline __device__ BT RemapPixel(BT in, float lo, float hi, float bound) +{ + if constexpr (std::is_floating_point_v) + { + if (!isfinite(in)) + { + return in; + } + } + if (hi == lo) + { + return in; + } + float range = hi - lo; + float val; + if (isfinite(range)) + { + val = (static_cast(in) - lo) * bound / range; + } + else + { + if constexpr (std::is_floating_point_v) + { + val = RemapWideFloat(static_cast(in), lo, hi); + } + else + { + // Unreachable for supported integer types: their extrema and ranges + // are exactly representable in float. + const float halfLo = lo * 0.5f; + val = (static_cast(in) * 0.5f - halfLo) * bound / (hi * 0.5f - halfLo); + } + } + val = fminf(fmaxf(val, 0.f), bound); + return static_cast(val); +} + +// ------------------------------- Reduce kernels ---------------------------- + +constexpr int REDUCE_X_STEPS = 8; +constexpr int BLOCK_X = 32; +constexpr int BLOCK_Y = 4; +constexpr int FINAL_THREADS = 256; +constexpr int MAX_GRID_Y = 65535; +constexpr int MAX_HEIGHT = BLOCK_Y * MAX_GRID_Y; +constexpr size_t MAX_PARTIALS_PER_SAMPLE = 4096; +constexpr size_t MAX_REDUCTION_WORKSPACE_BYTES = 6 * 1024 * 1024; +constexpr size_t MAX_REDUCTION_WORKSPACE_ONE_SIDE = MAX_REDUCTION_WORKSPACE_BYTES / (2 * sizeof(float)); + +inline int ComputeGridX(int width, int xSteps, int numPlanes = 1) +{ + const int64_t grid = cvcuda::priv::detail::AutoContrastGridX(width, BLOCK_X, xSteps, numPlanes); + if (grid > cuda::TypeTraits::max) + { + throw nvcv::Exception(nvcv::Status::ERROR_OVERFLOW, "CUDA grid width exceeds %d", + cuda::TypeTraits::max); + } + return static_cast(grid); +} + +template +inline __device__ void AccumulateExtrema(float value, float &lo, float &hi) +{ + if constexpr (std::is_floating_point_v) + { + if (!isfinite(value)) + { + return; + } + } + lo = fminf(lo, value); + hi = fmaxf(hi, value); +} + +// Exact-grid overload. Its launch grid is the logical image-tile grid, so this +// kernel deliberately retains the comparator's direct blockIdx mapping and +// argument list. In particular, no bounded-grid iterator state reaches this +// specialization. +template +__global__ void ReduceTensor(SrcWrapper src, float *partialLo, float *partialHi, int2 size, int numChannels, + int numPlanes) +{ + extern __shared__ float sm[]; + const int x0 = blockIdx.x * blockDim.x * REDUCE_X_STEPS + threadIdx.x; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const int s = blockIdx.z; + const int tid = threadIdx.y * blockDim.x + threadIdx.x; + const int nThreads = blockDim.x * blockDim.y; + const int numPartials = gridDim.x * gridDim.y; + const int partial = blockIdx.y * gridDim.x + blockIdx.x; + const int base = s * numChannels * numPartials + partial; + + if constexpr (!IsPlanar) + { + constexpr int C = cuda::NumElements; + float vMin[C], vMax[C]; +#pragma unroll + for (int c = 0; c < C; ++c) + { + vMin[c] = CUDART_INF_F; + vMax[c] = -CUDART_INF_F; + } +#pragma unroll + for (int i = 0; i < REDUCE_X_STEPS; ++i) + { + const int x = x0 + i * blockDim.x; + if (x < size.x && y < size.y) + { + T pix = src[int3{x, y, s}]; +#pragma unroll + for (int c = 0; c < C; ++c) + { + float v = static_cast(cuda::GetElement(pix, c)); + AccumulateExtrema>(v, vMin[c], vMax[c]); + } + } + } + ReduceBlockToPartials(sm, nThreads, tid, C, vMin, vMax, &partialLo[base], &partialHi[base], numPartials); + } + else + { + for (int p = 0; p < numPlanes; ++p) + { + float vMin[1], vMax[1]; + vMin[0] = CUDART_INF_F; + vMax[0] = -CUDART_INF_F; +#pragma unroll + for (int i = 0; i < REDUCE_X_STEPS; ++i) + { + const int x = x0 + i * blockDim.x; + if (x < size.x && y < size.y) + { + float v = static_cast(src[int4{x, y, p, s}]); + AccumulateExtrema>(v, vMin[0], vMax[0]); + } + } + const int planeBase = base + p * numPartials; + ReduceBlockToPartials<1>(sm, nThreads, tid, 1, vMin, vMax, &partialLo[planeBase], &partialHi[planeBase], + numPartials); + } + } +} + +// Bounded-grid overload. A physical CTA deterministically owns every logical +// tile congruent to blockIdx in both grid dimensions. +template +__global__ void ReduceTensor(SrcWrapper src, float *partialLo, float *partialHi, int2 size, int numChannels, + int numPlanes, int logicalGridX, int logicalGridY) +{ + extern __shared__ float sm[]; + const int s = blockIdx.z; + const int tid = threadIdx.y * blockDim.x + threadIdx.x; + const int nThreads = blockDim.x * blockDim.y; + const int numPartials = gridDim.x * gridDim.y; + const int partial = blockIdx.y * gridDim.x + blockIdx.x; + const int base = s * numChannels * numPartials + partial; + + if constexpr (!IsPlanar) + { + constexpr int C = cuda::NumElements; + float vMin[C], vMax[C]; +#pragma unroll + for (int c = 0; c < C; ++c) + { + vMin[c] = CUDART_INF_F; + vMax[c] = -CUDART_INF_F; + } + for (int tileY = blockIdx.y; tileY < logicalGridY; tileY += gridDim.y) + { + const int y = tileY * blockDim.y + threadIdx.y; + for (int tileX = blockIdx.x; tileX < logicalGridX; tileX += gridDim.x) + { + const int x0 = tileX * blockDim.x * REDUCE_X_STEPS + threadIdx.x; +#pragma unroll + for (int i = 0; i < REDUCE_X_STEPS; ++i) + { + const int x = x0 + i * blockDim.x; + if (x < size.x && y < size.y) + { + T pix = src[int3{x, y, s}]; +#pragma unroll + for (int c = 0; c < C; ++c) + { + float v = static_cast(cuda::GetElement(pix, c)); + AccumulateExtrema>(v, vMin[c], vMax[c]); + } + } + } + } + } + ReduceBlockToPartials(sm, nThreads, tid, C, vMin, vMax, &partialLo[base], &partialHi[base], numPartials); + } + else + { + for (int p = 0; p < numPlanes; ++p) + { + float vMin[1], vMax[1]; + vMin[0] = CUDART_INF_F; + vMax[0] = -CUDART_INF_F; + for (int tileY = blockIdx.y; tileY < logicalGridY; tileY += gridDim.y) + { + const int y = tileY * blockDim.y + threadIdx.y; + for (int tileX = blockIdx.x; tileX < logicalGridX; tileX += gridDim.x) + { + const int x0 = tileX * blockDim.x * REDUCE_X_STEPS + threadIdx.x; +#pragma unroll + for (int i = 0; i < REDUCE_X_STEPS; ++i) + { + const int x = x0 + i * blockDim.x; + if (x < size.x && y < size.y) + { + float v = static_cast(src[int4{x, y, p, s}]); + AccumulateExtrema>(v, vMin[0], vMax[0]); + } + } + } + } + const int planeBase = base + p * numPartials; + ReduceBlockToPartials<1>(sm, nThreads, tid, 1, vMin, vMax, &partialLo[planeBase], &partialHi[planeBase], + numPartials); + } + } +} + +// Exact-grid overload matching the comparator's direct blockIdx mapping and +// argument list. +template +__global__ void ReduceVarShape(SrcWrapper src, float *partialLo, float *partialHi, int numChannels, int numPlanes) +{ + extern __shared__ float sm[]; + const int x0 = blockIdx.x * blockDim.x * REDUCE_X_STEPS + threadIdx.x; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const int s = blockIdx.z; + const int tid = threadIdx.y * blockDim.x + threadIdx.x; + const int nThreads = blockDim.x * blockDim.y; + const int width = src.width(s); + const int height = src.height(s); + const int numPartials = gridDim.x * gridDim.y; + const int partial = blockIdx.y * gridDim.x + blockIdx.x; + const int base = s * numChannels * numPartials + partial; + + if constexpr (!IsPlanar) + { + constexpr int C = cuda::NumElements; + float vMin[C], vMax[C]; +#pragma unroll + for (int c = 0; c < C; ++c) + { + vMin[c] = CUDART_INF_F; + vMax[c] = -CUDART_INF_F; + } +#pragma unroll + for (int i = 0; i < REDUCE_X_STEPS; ++i) + { + const int x = x0 + i * blockDim.x; + if (x < width && y < height) + { + T pix = src[int3{x, y, s}]; +#pragma unroll + for (int c = 0; c < C; ++c) + { + float v = static_cast(cuda::GetElement(pix, c)); + AccumulateExtrema>(v, vMin[c], vMax[c]); + } + } + } + ReduceBlockToPartials(sm, nThreads, tid, C, vMin, vMax, &partialLo[base], &partialHi[base], numPartials); + } + else + { + for (int p = 0; p < numPlanes; ++p) + { + float vMin[1], vMax[1]; + vMin[0] = CUDART_INF_F; + vMax[0] = -CUDART_INF_F; +#pragma unroll + for (int i = 0; i < REDUCE_X_STEPS; ++i) + { + const int x = x0 + i * blockDim.x; + if (x < width && y < height) + { + float v = static_cast(src[int4{x, y, p, s}]); + AccumulateExtrema>(v, vMin[0], vMax[0]); + } + } + const int planeBase = base + p * numPartials; + if constexpr (std::is_same_v, float>) + { + ReduceBlockToPartialsShared<1>(sm, nThreads, tid, 1, vMin, vMax, &partialLo[planeBase], + &partialHi[planeBase], numPartials); + } + else + { + ReduceBlockToPartials<1>(sm, nThreads, tid, 1, vMin, vMax, &partialLo[planeBase], &partialHi[planeBase], + numPartials); + } + } + } +} + +// Bounded-grid overload using deterministic 2-D grid-stride ownership. +template +__global__ void ReduceVarShape(SrcWrapper src, float *partialLo, float *partialHi, int numChannels, int numPlanes, + int logicalGridX, int logicalGridY) +{ + extern __shared__ float sm[]; + const int s = blockIdx.z; + const int tid = threadIdx.y * blockDim.x + threadIdx.x; + const int nThreads = blockDim.x * blockDim.y; + const int width = src.width(s); + const int height = src.height(s); + const int numPartials = gridDim.x * gridDim.y; + const int partial = blockIdx.y * gridDim.x + blockIdx.x; + const int base = s * numChannels * numPartials + partial; + + if constexpr (!IsPlanar) + { + constexpr int C = cuda::NumElements; + float vMin[C], vMax[C]; +#pragma unroll + for (int c = 0; c < C; ++c) + { + vMin[c] = CUDART_INF_F; + vMax[c] = -CUDART_INF_F; + } + for (int tileY = blockIdx.y; tileY < logicalGridY; tileY += gridDim.y) + { + const int y = tileY * blockDim.y + threadIdx.y; + for (int tileX = blockIdx.x; tileX < logicalGridX; tileX += gridDim.x) + { + const int x0 = tileX * blockDim.x * REDUCE_X_STEPS + threadIdx.x; +#pragma unroll + for (int i = 0; i < REDUCE_X_STEPS; ++i) + { + const int x = x0 + i * blockDim.x; + if (x < width && y < height) + { + T pix = src[int3{x, y, s}]; +#pragma unroll + for (int c = 0; c < C; ++c) + { + float v = static_cast(cuda::GetElement(pix, c)); + AccumulateExtrema>(v, vMin[c], vMax[c]); + } + } + } + } + } + ReduceBlockToPartials(sm, nThreads, tid, C, vMin, vMax, &partialLo[base], &partialHi[base], numPartials); + } + else + { + for (int p = 0; p < numPlanes; ++p) + { + float vMin[1], vMax[1]; + vMin[0] = CUDART_INF_F; + vMax[0] = -CUDART_INF_F; + for (int tileY = blockIdx.y; tileY < logicalGridY; tileY += gridDim.y) + { + const int y = tileY * blockDim.y + threadIdx.y; + for (int tileX = blockIdx.x; tileX < logicalGridX; tileX += gridDim.x) + { + const int x0 = tileX * blockDim.x * REDUCE_X_STEPS + threadIdx.x; +#pragma unroll + for (int i = 0; i < REDUCE_X_STEPS; ++i) + { + const int x = x0 + i * blockDim.x; + if (x < width && y < height) + { + float v = static_cast(src[int4{x, y, p, s}]); + AccumulateExtrema>(v, vMin[0], vMax[0]); + } + } + } + } + const int planeBase = base + p * numPartials; + if constexpr (std::is_same_v, float>) + { + ReduceBlockToPartialsShared<1>(sm, nThreads, tid, 1, vMin, vMax, &partialLo[planeBase], + &partialHi[planeBase], numPartials); + } + else + { + ReduceBlockToPartials<1>(sm, nThreads, tid, 1, vMin, vMax, &partialLo[planeBase], &partialHi[planeBase], + numPartials); + } + } + } +} + +// ------------------------------- Apply kernels ----------------------------- + +constexpr int APPLY_X_STEPS = 4; + +template +__global__ void ApplyTensor(SrcWrapper src, DstWrapper dst, const float *lo, const float *hi, int2 size, + int numChannels, int numPlanes, float bound) +{ + const int x0 = blockIdx.x * blockDim.x * APPLY_X_STEPS + threadIdx.x; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const int s = blockIdx.z; + const int sampleOffset = s * numChannels; + if (x0 >= size.x || y >= size.y) + { + return; + } + + if constexpr (!IsPlanar) + { + using BT = cuda::BaseType; + constexpr int C = cuda::NumElements; + float minValue[C], maxValue[C]; +#pragma unroll + for (int c = 0; c < C; ++c) + { + minValue[c] = lo[sampleOffset + c]; + maxValue[c] = hi[sampleOffset + c]; + } +#pragma unroll + for (int i = 0; i < APPLY_X_STEPS; ++i) + { + const int x = x0 + i * blockDim.x; + if (x < size.x) + { + T pix = src[int3{x, y, s}]; + T out{}; +#pragma unroll + for (int c = 0; c < C; ++c) + { + cuda::GetElement(out, c) + = RemapPixel(cuda::GetElement(pix, c), minValue[c], maxValue[c], bound); + } + dst[int3{x, y, s}] = out; + } + } + } + else + { + for (int p = 0; p < numPlanes; ++p) + { + const float minValue = lo[sampleOffset + p]; + const float maxValue = hi[sampleOffset + p]; +#pragma unroll + for (int i = 0; i < APPLY_X_STEPS; ++i) + { + const int x = x0 + i * blockDim.x; + if (x < size.x) + { + T in = src[int4{x, y, p, s}]; + dst[int4{x, y, p, s}] = RemapPixel(in, minValue, maxValue, bound); + } + } + } + } +} + +template +__global__ void ApplyVarShape(SrcWrapper src, DstWrapper dst, const float *lo, const float *hi, int numChannels, + int numPlanes, float bound) +{ + const int x0 = blockIdx.x * blockDim.x * APPLY_X_STEPS + threadIdx.x; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const int s = blockIdx.z; + const int width = dst.width(s); + const int height = dst.height(s); + const int sampleOffset = s * numChannels; + if (x0 >= width || y >= height) + { + return; + } + + if constexpr (!IsPlanar) + { + using BT = cuda::BaseType; + constexpr int C = cuda::NumElements; + float minValue[C], maxValue[C]; +#pragma unroll + for (int c = 0; c < C; ++c) + { + minValue[c] = lo[sampleOffset + c]; + maxValue[c] = hi[sampleOffset + c]; + } +#pragma unroll + for (int i = 0; i < APPLY_X_STEPS; ++i) + { + const int x = x0 + i * blockDim.x; + if (x < width) + { + T pix = src[int3{x, y, s}]; + T out{}; +#pragma unroll + for (int c = 0; c < C; ++c) + { + cuda::GetElement(out, c) + = RemapPixel(cuda::GetElement(pix, c), minValue[c], maxValue[c], bound); + } + dst[int3{x, y, s}] = out; + } + } + } + else + { + for (int p = 0; p < numPlanes; ++p) + { + const float minValue = lo[sampleOffset + p]; + const float maxValue = hi[sampleOffset + p]; +#pragma unroll + for (int i = 0; i < APPLY_X_STEPS; ++i) + { + const int x = x0 + i * blockDim.x; + if (x < width) + { + T in = src[int4{x, y, p, s}]; + dst[int4{x, y, p, s}] = RemapPixel(in, minValue, maxValue, bound); + } + } + } + } +} + +// ------------------------------- Launchers --------------------------------- + +template +inline float DtypeBound() +{ + return std::is_floating_point_v ? 1.0f : static_cast(cuda::TypeTraits::max); +} + +struct ReductionWorkspace +{ + float *partialLo; + float *partialHi; + float *lo; + float *hi; + size_t numPartials; + size_t numExtrema; +}; + +class WorkspaceReleaseGuard +{ +public: + WorkspaceReleaseGuard(cvcuda::priv::AutoContrastWorkspace &workspace, cudaStream_t stream) + : m_workspace(workspace) + , m_stream(stream) + { + } + + ~WorkspaceReleaseGuard() + { + if (m_active) + { + m_workspace.releaseNoThrow(m_stream); + } + } + + void finish() + { + m_workspace.release(m_stream); + m_active = false; + } + +private: + cvcuda::priv::AutoContrastWorkspace &m_workspace; + cudaStream_t m_stream; + bool m_active = true; +}; + +struct ReductionGrid +{ + dim3 launch; + int logicalX; + int logicalY; + + bool isExact() const + { + return launch.x == static_cast(logicalX) && launch.y == static_cast(logicalY); + } +}; + +inline size_t CheckedWorkspaceMul(size_t a, size_t b) +{ + if (a != 0 && b > std::numeric_limits::max() / a) + { + throw nvcv::Exception(nvcv::Status::ERROR_OVERFLOW, "AutoContrast reduction workspace size overflows size_t"); + } + return a * b; +} + +inline size_t CheckedWorkspaceAdd(size_t a, size_t b) +{ + if (b > std::numeric_limits::max() - a) + { + throw nvcv::Exception(nvcv::Status::ERROR_OVERFLOW, "AutoContrast reduction workspace size overflows size_t"); + } + return a + b; +} + +inline ReductionGrid GetReductionGrid(int width, int height, int numSamples, int numChannels) +{ + const int logicalX = ComputeGridX(width, REDUCE_X_STEPS); + const int logicalY = cvcuda::priv::detail::AutoContrastDivUp(height, BLOCK_Y); + const size_t logicalPartials = CheckedWorkspaceMul(logicalX, logicalY); + const size_t numExtrema = CheckedWorkspaceMul(static_cast(numSamples), numChannels); + + // The workspace owns two equal sides: lo partials plus final lo, then the + // corresponding hi values. Bound both the persistent allocation and the + // amount of final-pass work, while every logical tile is still visited by + // a physical CTA through the grid-stride loop in Reduce*. + if (numExtrema == 0 || numExtrema > MAX_REDUCTION_WORKSPACE_ONE_SIDE / 2) + { + throw nvcv::Exception(nvcv::Status::ERROR_OVERFLOW, + "AutoContrast extrema count exceeds the bounded reduction workspace"); + } + const size_t maxPartialsByWorkspace = MAX_REDUCTION_WORKSPACE_ONE_SIDE / numExtrema - 1; + const size_t partialLimit = std::min(logicalPartials, std::min(MAX_PARTIALS_PER_SAMPLE, maxPartialsByWorkspace)); + const size_t launchX = std::min(static_cast(logicalX), partialLimit); + const size_t launchY = std::min(static_cast(logicalY), partialLimit / launchX); + + NVCV_ASSERT(launchX > 0 && launchY > 0); + return { + {static_cast(launchX), static_cast(launchY), static_cast(numSamples)}, + logicalX, + logicalY + }; +} + +inline ReductionWorkspace GetReductionWorkspace(cvcuda::priv::AutoContrastWorkspace &ws, cudaStream_t stream, + int numSamples, int numChannels, dim3 reduceGrid) +{ + const size_t numPartials = CheckedWorkspaceMul(reduceGrid.x, reduceGrid.y); + const size_t numExtrema = CheckedWorkspaceMul(static_cast(numSamples), numChannels); + const size_t partialCount = CheckedWorkspaceMul(numExtrema, numPartials); + const size_t pairedCount = CheckedWorkspaceAdd(partialCount, numExtrema); + float *buf = ws.acquire(pairedCount, stream); + + float *partialLo = buf; + float *partialHi = partialLo + partialCount; + float *lo = partialHi + partialCount; + float *hi = lo + numExtrema; + return {partialLo, partialHi, lo, hi, numPartials, numExtrema}; +} + +template +inline bool TensorAddressingFitsInt32(const Access &access) +{ + constexpr int64_t limit = cuda::TypeTraits::max; + int64_t maxOffset{}; + auto accumulate = [&maxOffset](int64_t stride, int64_t extent) + { + if (stride < 0 || stride > limit) + { + return false; + } + const int64_t index = extent > 0 ? extent - 1 : 0; + if (index > 0 && stride > (limit - maxOffset) / index) + { + return false; + } + maxOffset += stride * index; + return true; + }; + + return accumulate(access.sampleStride(), access.numSamples()) + && (!IsPlanar || accumulate(access.planeStride(), access.numPlanes())) + && accumulate(access.rowStride(), access.numRows()) && accumulate(access.colStride(), access.numCols()); +} + +template +void RunTensor(cudaStream_t stream, cvcuda::priv::AutoContrastWorkspace &ws, const nvcv::TensorDataStridedCuda &srcData, + const nvcv::TensorDataStridedCuda &dstData, int numSamples, int numChannels, int numPlanes) +{ + using BT = cuda::BaseType; + const float bound = DtypeBound(); + + auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(srcData); + auto dstAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(dstData); + int2 size = cuda::StaticCast(long2{srcAccess->numCols(), srcAccess->numRows()}); + + if (!TensorAddressingFitsInt32(*srcAccess) || !TensorAddressingFitsInt32(*dstAccess)) + { + throw nvcv::Exception(nvcv::Status::ERROR_OVERFLOW, + "Input or output dynamic stride or maximum byte offset exceeds %d", + cuda::TypeTraits::max); + } + using StrideType = int32_t; + + dim3 block(BLOCK_X, BLOCK_Y, 1); + ReductionGrid reduceGrid = GetReductionGrid(size.x, size.y, numSamples, numChannels); + dim3 applyGrid(ComputeGridX(size.x, APPLY_X_STEPS), cvcuda::priv::detail::AutoContrastDivUp(size.y, BLOCK_Y), + numSamples); + const int nThreads = block.x * block.y; + const int numWarps = nThreads / WARP_WIDTH; + const size_t reduceChan = IsPlanar ? 1 : cuda::NumElements; + const size_t smem = 2 * reduceChan * numWarps * sizeof(float); + const size_t finalSmem = 2 * (FINAL_THREADS / WARP_WIDTH) * sizeof(float); + auto reduction = GetReductionWorkspace(ws, stream, numSamples, numChannels, reduceGrid.launch); + WorkspaceReleaseGuard releaseGuard(ws, stream); + + if constexpr (!IsPlanar) + { + auto src = cuda::CreateTensorWrapNHW(srcData); + auto dst = cuda::CreateTensorWrapNHW(dstData); + if (reduceGrid.isExact()) + { + ReduceTensor<<>>( + src, reduction.partialLo, reduction.partialHi, size, numChannels, 1); + } + else + { + ReduceTensor + <<>>(src, reduction.partialLo, reduction.partialHi, size, + numChannels, 1, reduceGrid.logicalX, reduceGrid.logicalY); + } + FinalizeMinMax<<(reduction.numExtrema), FINAL_THREADS, finalSmem, stream>>>( + reduction.partialLo, reduction.partialHi, reduction.lo, reduction.hi, reduction.numPartials); + ApplyTensor + <<>>(src, dst, reduction.lo, reduction.hi, size, numChannels, 1, bound); + } + else + { + auto src = cuda::Tensor4DWrap( + srcData.basePtr(), static_cast(srcAccess->sampleStride()), + static_cast(srcAccess->planeStride()), static_cast(srcAccess->rowStride())); + auto dst = cuda::Tensor4DWrap( + dstData.basePtr(), static_cast(dstAccess->sampleStride()), + static_cast(dstAccess->planeStride()), static_cast(dstAccess->rowStride())); + if (reduceGrid.isExact()) + { + ReduceTensor<<>>( + src, reduction.partialLo, reduction.partialHi, size, numChannels, numPlanes); + } + else + { + ReduceTensor<<>>( + src, reduction.partialLo, reduction.partialHi, size, numChannels, numPlanes, reduceGrid.logicalX, + reduceGrid.logicalY); + } + FinalizeMinMax<<(reduction.numExtrema), FINAL_THREADS, finalSmem, stream>>>( + reduction.partialLo, reduction.partialHi, reduction.lo, reduction.hi, reduction.numPartials); + ApplyTensor<<>>(src, dst, reduction.lo, reduction.hi, size, numChannels, + numPlanes, bound); + } + NVCV_CHECK_THROW(cudaGetLastError()); + releaseGuard.finish(); +} + +template +void RunVarShapeBatch(cudaStream_t stream, cvcuda::priv::AutoContrastWorkspace &ws, + const nvcv::ImageBatchVarShapeDataStridedCuda &srcData, + const nvcv::ImageBatchVarShapeDataStridedCuda &dstData, int numSamples, int numChannels, + int numPlanes) +{ + using BT = cuda::BaseType; + const float bound = DtypeBound(); + + int3 maxSize{dstData.maxSize().w, dstData.maxSize().h, numSamples}; + dim3 block(BLOCK_X, BLOCK_Y, 1); + ReductionGrid reduceGrid = GetReductionGrid(maxSize.x, maxSize.y, numSamples, numChannels); + dim3 applyGrid(ComputeGridX(maxSize.x, APPLY_X_STEPS), cvcuda::priv::detail::AutoContrastDivUp(maxSize.y, BLOCK_Y), + numSamples); + const int nThreads = block.x * block.y; + const int numWarps = nThreads / WARP_WIDTH; + constexpr bool useSharedTree = IsPlanar && std::is_same_v; + const size_t reduceChan = IsPlanar ? 1 : cuda::NumElements; + const size_t reduceSlots = useSharedTree ? nThreads : numWarps; + const size_t smem = 2 * reduceChan * reduceSlots * sizeof(float); + const size_t finalSmem = 2 * (FINAL_THREADS / WARP_WIDTH) * sizeof(float); + auto reduction = GetReductionWorkspace(ws, stream, numSamples, numChannels, reduceGrid.launch); + WorkspaceReleaseGuard releaseGuard(ws, stream); + + cuda::ImageBatchVarShapeWrap src(srcData); + cuda::ImageBatchVarShapeWrap dst(dstData); + + if (reduceGrid.isExact()) + { + ReduceVarShape<<>>( + src, reduction.partialLo, reduction.partialHi, numChannels, numPlanes); + } + else + { + ReduceVarShape + <<>>(src, reduction.partialLo, reduction.partialHi, numChannels, + numPlanes, reduceGrid.logicalX, reduceGrid.logicalY); + } + FinalizeMinMax<<(reduction.numExtrema), FINAL_THREADS, finalSmem, stream>>>( + reduction.partialLo, reduction.partialHi, reduction.lo, reduction.hi, reduction.numPartials); + ApplyVarShape + <<>>(src, dst, reduction.lo, reduction.hi, numChannels, numPlanes, bound); + NVCV_CHECK_THROW(cudaGetLastError()); + releaseGuard.finish(); +} + +// Pick the supported base type (u8 / u16 / f32 only) from a tensor/format dtype. +template +inline void DispatchBaseType(nvcv::DataType dtype, const Cb &cb) +{ + using uchar = unsigned char; + using ushort = unsigned short; + +#define CVCUDA_AC_BASE(DYN, STAT) \ + ((dtype == nvcv::TYPE_4##DYN) || (dtype == nvcv::TYPE_3##DYN) || (dtype == nvcv::TYPE_2##DYN) \ + || (dtype == nvcv::TYPE_##DYN)) cb(STAT{}); + + // clang-format off + if CVCUDA_AC_BASE(U8, uchar) + else if CVCUDA_AC_BASE(U16, ushort) + else if CVCUDA_AC_BASE(F32, float) + else + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Invalid input/output data type (only U8, U16, F32 are supported)"); + } + // clang-format on + +#undef CVCUDA_AC_BASE +} + +// Build the vectorized value type from (base type, channels, planar-ness) and dispatch the callback. +template +inline void DispatchType(nvcv::DataType dtype, int numInterleavedChannels, int numPlanes, const Cb &cb) +{ + DispatchBaseType(dtype, + [&](auto dummyBase) + { + using BaseT = decltype(dummyBase); + if (numInterleavedChannels == 1) + { + if (numPlanes == 1) + { + cb(BaseT{}, std::integral_constant{}); + } + else + { + cb(BaseT{}, std::integral_constant{}); + } + } + else if (numInterleavedChannels == 3) + { + cb(cuda::MakeType{}, std::integral_constant{}); + } + else if (numInterleavedChannels == 4) + { + cb(cuda::MakeType{}, std::integral_constant{}); + } + else + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Invalid number of channels (only 1, 3, 4 are supported)"); + } + }); +} + +// ------------------------------- Validation -------------------------------- + +inline bool ValidateSrcDstTensors(int &numSamples, int &numInterleavedChannels, int &numPlanes, nvcv::DataType &dtype, + const nvcv::Optional &srcData, + const nvcv::Optional &dstData) +{ + if (!srcData) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input must be cuda-accessible, pitch-linear tensor"); + } + if (!dstData) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Output must be cuda-accessible, pitch-linear tensor"); + } + if (srcData->layout() != dstData->layout()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input and output must have the same layout"); + } + if (!(srcData->layout() == nvcv::TENSOR_HWC || srcData->layout() == nvcv::TENSOR_NHWC + || srcData->layout() == nvcv::TENSOR_CHW || srcData->layout() == nvcv::TENSOR_NCHW)) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input must have (N)HWC or (N)CHW layout"); + } + if (srcData->dtype() != dstData->dtype()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input and output must have the same data type"); + } + if (srcData->dtype().numChannels() != 1) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Tensor data type must be scalar; use the C dimension for image channels"); + } + if (srcData->shape() != dstData->shape()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input and output must have matching tensor shapes"); + } + + const auto &shape = srcData->shape(); + const auto &layout = srcData->layout(); + auto extent = [&shape, &layout](char label, int64_t implicit) + { + const int index = layout.find(label); + return index >= 0 ? shape[index] : implicit; + }; + const int64_t rawSamples = extent('N', 1); + const int64_t rawRows = extent('H', 1); + const int64_t rawCols = extent('W', 1); + const int64_t rawChannels = extent('C', 1); + if (rawSamples > 65535) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Batch size %ld exceeds the maximum supported (65535)", rawSamples); + } + if (rawRows > MAX_HEIGHT) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Image height %ld exceeds the maximum supported (%d)", rawRows, MAX_HEIGHT); + } + if (rawCols > cuda::TypeTraits::max) + { + throw nvcv::Exception(nvcv::Status::ERROR_OVERFLOW, "Image width %ld exceeds signed 32-bit kernel addressing", + rawCols); + } + if (rawChannels != 1 && rawChannels != 3 && rawChannels != 4) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Invalid channel number %ld (only 1, 3, 4)", + rawChannels); + } + + auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcData); + auto dstAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*dstData); + NVCV_ASSERT(srcAccess && dstAccess); + + numSamples = srcAccess->numSamples(); + if (numSamples != dstAccess->numSamples()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of samples"); + } + // Each sample is mapped to grid.z in the reduce/apply kernels, so the batch size must fit + // the CUDA grid.z limit. + if (numSamples > 65535) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Batch size %d exceeds the maximum supported (65535)", numSamples); + } + + int numChannels = srcAccess->numChannels(); + if (numChannels != dstAccess->numChannels()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of channels"); + } + if (numChannels == 2) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "2-channel images are not supported"); + } + if (numChannels != 1 && numChannels != 3 && numChannels != 4) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Invalid channel number %d (only 1, 3, 4)", + numChannels); + } + + numPlanes = srcAccess->numPlanes(); + if (numPlanes != dstAccess->numPlanes()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of planes"); + } + + if (srcAccess->numCols() != dstAccess->numCols() || srcAccess->numRows() != dstAccess->numRows()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input and output must have matching width and height"); + } + if (srcAccess->numRows() > MAX_HEIGHT) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Image height %ld exceeds the maximum supported (%d)", srcAccess->numRows(), MAX_HEIGHT); + } + + const int64_t elementStride = srcData->dtype().strideBytes(); + const int64_t expectedColStride = elementStride * (srcAccess->infoLayout().isChannelLast() ? numChannels : 1); + if (srcAccess->colStride() != expectedColStride || dstAccess->colStride() != expectedColStride + || (srcAccess->infoLayout().isChannelLast() + && (srcAccess->chStride() != elementStride || dstAccess->chStride() != elementStride))) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Tensor pixels must have packed width and channel strides"); + } + + dtype = srcData->dtype(); + numInterleavedChannels = srcAccess->infoLayout().isChannelLast() ? numChannels : 1; + return numSamples == 0 || srcAccess->numCols() == 0 || srcAccess->numRows() == 0; +} + +inline void ValidateImagePlanes(const nvcv::Image &image, const nvcv::ImageFormat &format) +{ + auto data = image.exportData(); + if (!data || data->numPlanes() != format.numPlanes()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Image plane descriptors must match the image format"); + } + + constexpr int64_t limit = cuda::TypeTraits::max; + for (int p = 0; p < data->numPlanes(); ++p) + { + const nvcv::ImagePlaneStrided &plane = data->plane(p); + const nvcv::Size2D expected = format.planeSize(image.size(), p); + const int64_t pixelStride = format.planePixelStrideBytes(p); + if (plane.width != expected.w || plane.height != expected.h) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Image plane descriptors must match the image format and size"); + } + if (plane.rowStride < 0 + || (static_cast(plane.height - 1) * plane.rowStride + + static_cast(plane.width - 1) * pixelStride + > limit)) + { + throw nvcv::Exception(nvcv::Status::ERROR_OVERFLOW, + "Input or output image-plane maximum byte offset exceeds %d", + cuda::TypeTraits::max); + } + } +} + +inline auto ValidateSrcDstVarShape(int &numSamples, int &numInterleavedChannels, int &numPlanes, nvcv::DataType &dtype, + cudaStream_t stream, const nvcv::ImageBatchVarShape &src, + const nvcv::ImageBatchVarShape &dst) +{ + using MaybeVarShape = nvcv::Optional; + std::tuple srcDstData{ + src.exportData(stream), + dst.exportData(stream)}; + auto &[srcData, dstData] = srcDstData; + + if (!srcData) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input must be cuda-accessible, varshape pitch-linear image batch"); + } + if (!dstData) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Output must be cuda-accessible, varshape pitch-linear image batch"); + } + + numSamples = srcData->numImages(); + if (numSamples != dstData->numImages()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of samples"); + } + if (numSamples > 65535) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Batch size %d exceeds the maximum supported (65535)", numSamples); + } + if (numSamples == 0) + { + return srcDstData; + } + + const auto &srcFormat = srcData->uniqueFormat(); + const auto &dstFormat = dstData->uniqueFormat(); + if (!srcFormat || !dstFormat) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "All images in a batch must have the same format"); + } + if (srcFormat != dstFormat) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input and output must have the same format"); + } + + int numChannels = srcFormat.numChannels(); + if (numChannels == 2) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "2-channel images are not supported"); + } + if (numChannels != 1 && numChannels != 3 && numChannels != 4) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Invalid channel number %d (only 1, 3, 4)", + numChannels); + } + + nvcv::ExtraChannelInfo extraChannels{}; + srcFormat.extraChannelInfo(&extraChannels); + if (extraChannels.numChannels != 0) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Image formats with extra channels are not supported"); + } + if (srcFormat.chromaSubsampling() != nvcv::ChromaSubsampling::CSS_444) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Chroma-subsampled image formats are not supported"); + } + + numPlanes = srcFormat.numPlanes(); + dtype = srcFormat.planeDataType(0); + if (numPlanes == 1) + { + if (dtype.numChannels() != numChannels || srcFormat.planePixelStrideBytes(0) != dtype.strideBytes()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Single-plane images must have one packed element per pixel"); + } + } + else if (numPlanes != numChannels || dtype.numChannels() != 1) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Planar images must have one scalar, full-resolution plane per channel"); + } + for (int p = 1; p < numPlanes; ++p) + { + if (dtype != srcFormat.planeDataType(p) || srcFormat.planeDataType(p).numChannels() != 1) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "All image planes must have the same scalar data type"); + } + } + + for (int i = 0; i < numSamples; ++i) + { + const nvcv::Size2D size = src[i].size(); + if (size != dst[i].size()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input and output must have matching width and height"); + } + if (size.w <= 0 || size.h <= 0) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Image width and height must be greater than zero"); + } + if (size.h > MAX_HEIGHT) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Image height %d exceeds the maximum supported (%d)", size.h, MAX_HEIGHT); + } + ValidateImagePlanes(src[i], srcFormat); + ValidateImagePlanes(dst[i], dstFormat); + const nvcv::Size2D plane0Size = srcFormat.planeSize(size, 0); + for (int p = 1; p < numPlanes; ++p) + { + if (srcFormat.planeSize(size, p) != plane0Size) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "All image planes must have matching width and height"); + } + } + } + + numInterleavedChannels = dtype.numChannels(); + return srcDstData; +} + +inline void RejectStreamCapture(cudaStream_t stream) +{ + // Captured kernels would retain the shared workspace pointer but replay outside the host-side + // submission lock. Reject before validation can enqueue work, rather than silently aliasing it. + cudaStreamCaptureStatus captureStatus; + NVCV_CHECK_THROW(cudaStreamIsCapturing(stream, &captureStatus)); + if (captureStatus != cudaStreamCaptureStatusNone) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_OPERATION, + "AutoContrast does not support CUDA stream capture"); + } +} + +} // anonymous namespace + +namespace cvcuda::priv { + +// --------------------------------- Workspace ------------------------------- + +AutoContrastWorkspace::AutoContrastWorkspace() +{ + NVCV_CHECK_THROW(cudaEventCreateWithFlags(&m_ready, cudaEventDisableTiming)); +} + +AutoContrastWorkspace::~AutoContrastWorkspace() +{ + if (m_pending) + { + NVCV_CHECK_LOG(cudaEventSynchronize(m_ready)); + } + if (m_data != nullptr) + { + NVCV_CHECK_LOG(cudaFree(m_data)); + } + if (m_ready != nullptr) + { + NVCV_CHECK_LOG(cudaEventDestroy(m_ready)); + } +} + +[[nodiscard]] std::unique_lock AutoContrastWorkspace::lock() +{ + return std::unique_lock{m_mutex}; +} + +float *AutoContrastWorkspace::acquire(size_t count, cudaStream_t stream) +{ + if (count > std::numeric_limits::max() / 2) + { + throw nvcv::Exception(nvcv::Status::ERROR_OVERFLOW, "AutoContrast reduction workspace size overflows size_t"); + } + const size_t need = 2 * count; + if (need > std::numeric_limits::max() / sizeof(float)) + { + throw nvcv::Exception(nvcv::Status::ERROR_OVERFLOW, "AutoContrast reduction workspace size overflows size_t"); + } + + if (m_pending) + { + if (need > m_capacity) + { + NVCV_CHECK_THROW(cudaEventSynchronize(m_ready)); + m_pending = false; + } + else + { + NVCV_CHECK_THROW(cudaStreamWaitEvent(stream, m_ready)); + } + } + if (need > m_capacity) + { + if (m_data != nullptr) + { + NVCV_CHECK_THROW(cudaFree(m_data)); + m_data = nullptr; + m_capacity = 0; + } + void *ptr = nullptr; + if (cudaMalloc(&ptr, need * sizeof(float)) != cudaSuccess) + { + throw nvcv::Exception(nvcv::Status::ERROR_OUT_OF_MEMORY, "AutoContrast: failed to allocate workspace"); + } + m_data = static_cast(ptr); + m_capacity = need; + } + return m_data; +} + +void AutoContrastWorkspace::release(cudaStream_t stream) +{ + NVCV_CHECK_THROW(cudaEventRecord(m_ready, stream)); + m_pending = true; +} + +void AutoContrastWorkspace::releaseNoThrow(cudaStream_t stream) noexcept +{ + cudaError_t err = cudaEventRecord(m_ready, stream); + if (err == cudaSuccess) + { + m_pending = true; + } + NVCV_CHECK_LOG(err); +} + +// --------------------------------- Operator -------------------------------- + +AutoContrast::AutoContrast() + : m_workspace([](int) { return std::make_unique(); }) +{ +} + +void AutoContrast::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out) const +{ + CVCUDA_NVTX_RANGE("cvcuda::AutoContrast::operator()[Tensor]"); + RejectStreamCapture(stream); + + int numSamples; + int numInterleavedChannels; + int numPlanes; + nvcv::DataType dtype; + auto srcData = in.exportData(); + auto dstData = out.exportData(); + const bool isEmpty = ValidateSrcDstTensors(numSamples, numInterleavedChannels, numPlanes, dtype, srcData, dstData); + + const int numChannels = numInterleavedChannels > 1 ? numInterleavedChannels : numPlanes; + + DispatchType(dtype, numInterleavedChannels, numPlanes, + [&](auto dummyVal, auto isPlanar) + { + using T = decltype(dummyVal); + constexpr bool Plnr = decltype(isPlanar)::value; + if (!isEmpty) + { + auto &workspace = m_workspace.get(); + auto lock = workspace.lock(); + RunTensor(stream, workspace, *srcData, *dstData, numSamples, numChannels, numPlanes); + } + }); +} + +void AutoContrast::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, + const nvcv::ImageBatchVarShape &out) const +{ + CVCUDA_NVTX_RANGE("cvcuda::AutoContrast::operator()[ImageBatchVarShape]"); + RejectStreamCapture(stream); + + int numSamples; + int numInterleavedChannels; + int numPlanes; + nvcv::DataType dtype; + auto srcDstData = ValidateSrcDstVarShape(numSamples, numInterleavedChannels, numPlanes, dtype, stream, in, out); + if (numSamples == 0) + { + return; + } + + const int numChannels = numInterleavedChannels > 1 ? numInterleavedChannels : numPlanes; + + DispatchType(dtype, numInterleavedChannels, numPlanes, + [&](auto dummyVal, auto isPlanar) + { + using T = decltype(dummyVal); + constexpr bool Plnr = decltype(isPlanar)::value; + auto &[srcData, dstData] = srcDstData; + auto &workspace = m_workspace.get(); + auto lock = workspace.lock(); + RunVarShapeBatch(stream, workspace, *srcData, *dstData, numSamples, numChannels, + numPlanes); + }); +} + +} // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpAutoContrast.hpp b/src/cvcuda/priv/OpAutoContrast.hpp new file mode 100644 index 000000000..d3cf1ea42 --- /dev/null +++ b/src/cvcuda/priv/OpAutoContrast.hpp @@ -0,0 +1,96 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +/** + * @file OpAutoContrast.hpp + * + * @brief Defines the private C++ Class for the AutoContrast operation. + */ + +#ifndef CVCUDA_PRIV__AUTO_CONTRAST_HPP +#define CVCUDA_PRIV__AUTO_CONTRAST_HPP + +#include "IOperator.hpp" +#include "PerDeviceResource.hpp" + +#include +#include +#include + +#include +#include +#include + +namespace cvcuda::priv { + +namespace detail { + +constexpr int64_t AutoContrastDivUp(int64_t value, int64_t divisor) +{ + return value / divisor + (value % divisor != 0 ? 1 : 0); +} + +constexpr int64_t AutoContrastGridX(int64_t width, int blockWidth, int xSteps, int numPlanes = 1) +{ + return AutoContrastDivUp(width, static_cast(blockWidth) * xSteps) * numPlanes; +} + +} // namespace detail + +// Bounded per-CTA partial and final per-(sample, channel) extrema workspace, grown on demand. +// Host submissions are serialized while the event orders asynchronous buffer reuse across +// streams. PerDeviceResource gives each GPU an independent workspace and ordering state. +class AutoContrastWorkspace +{ +public: + AutoContrastWorkspace(); + ~AutoContrastWorkspace(); + + AutoContrastWorkspace(const AutoContrastWorkspace &) = delete; + AutoContrastWorkspace &operator=(const AutoContrastWorkspace &) = delete; + + [[nodiscard]] std::unique_lock lock(); + + // Returns 2*count floats for paired low/high partial and final extrema storage. + float *acquire(size_t count, cudaStream_t stream); + void release(cudaStream_t stream); + void releaseNoThrow(cudaStream_t stream) noexcept; + +private: + std::mutex m_mutex; + float *m_data = nullptr; + size_t m_capacity = 0; // capacity in floats + cudaEvent_t m_ready = nullptr; + bool m_pending = false; +}; + +class AutoContrast final : public IOperator +{ +public: + explicit AutoContrast(); + + void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out) const; + + void operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out) const; + +private: + mutable PerDeviceResource m_workspace; +}; + +} // namespace cvcuda::priv + +#endif // CVCUDA_PRIV__AUTO_CONTRAST_HPP diff --git a/src/cvcuda/priv/OpAverageBlur.cpp b/src/cvcuda/priv/OpAverageBlur.cpp index 78ee4a704..e2bf0b643 100644 --- a/src/cvcuda/priv/OpAverageBlur.cpp +++ b/src/cvcuda/priv/OpAverageBlur.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "OpAverageBlur.hpp" +#include "Nvtx.hpp" #include "legacy/CvCudaLegacy.h" #include "legacy/CvCudaLegacyHelpers.hpp" @@ -28,15 +29,29 @@ namespace cvcuda::priv { namespace legacy = nvcv::legacy::cuda_op; AverageBlur::AverageBlur(nvcv::Size2D maxKernelSize, int maxBatchSize) + // Legacy operators are single-device by design. PerDeviceResource creates + // one instance per CUDA device for transparent multi-GPU support. + : m_legacyOp( + [maxKernelSize](int) + { + legacy::DataShape maxIn; + legacy::DataShape maxOut; + return std::make_unique(maxIn, maxOut, maxKernelSize); + }) + , m_legacyOpVarShape( + [maxKernelSize, maxBatchSize](int) + { + legacy::DataShape maxIn; + legacy::DataShape maxOut; + return std::make_unique(maxIn, maxOut, maxKernelSize, maxBatchSize); + }) { - legacy::DataShape maxIn, maxOut; //maxIn/maxOut not used by op. - m_legacyOp = std::make_unique(maxIn, maxOut, maxKernelSize); - m_legacyOpVarShape = std::make_unique(maxIn, maxOut, maxKernelSize, maxBatchSize); } void AverageBlur::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, nvcv::Size2D kernelSize, int2 kernelAnchor, NVCVBorderType borderMode) const { + CVCUDA_NVTX_RANGE("cvcuda::AverageBlur::operator()[Tensor]"); auto inData = in.exportData(); if (inData == nullptr) { @@ -51,13 +66,14 @@ void AverageBlur::operator()(cudaStream_t stream, const nvcv::Tensor &in, const "Output must be cuda-accessible, pitch-linear tensor"); } - NVCV_CHECK_THROW(m_legacyOp->infer(*inData, *outData, kernelSize, kernelAnchor, borderMode, stream)); + NVCV_CHECK_THROW(m_legacyOp.get().infer(*inData, *outData, kernelSize, kernelAnchor, borderMode, stream)); } void AverageBlur::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, const nvcv::Tensor &kernelSize, const nvcv::Tensor &kernelAnchor, NVCVBorderType borderMode) const { + CVCUDA_NVTX_RANGE("cvcuda::AverageBlur::operator()[ImageBatchVarShape]"); auto inData = in.exportData(stream); if (inData == nullptr) { @@ -87,7 +103,7 @@ void AverageBlur::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape } NVCV_CHECK_THROW( - m_legacyOpVarShape->infer(*inData, *outData, *kernelSizeData, *kernelAnchorData, borderMode, stream)); + m_legacyOpVarShape.get().infer(*inData, *outData, *kernelSizeData, *kernelAnchorData, borderMode, stream)); } } // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpAverageBlur.hpp b/src/cvcuda/priv/OpAverageBlur.hpp index d3a2ea406..3a8a6fefd 100644 --- a/src/cvcuda/priv/OpAverageBlur.hpp +++ b/src/cvcuda/priv/OpAverageBlur.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,13 +25,12 @@ #define CVCUDA_PRIV_AVERAGEBLUR_HPP #include "IOperator.hpp" +#include "PerDeviceResource.hpp" #include "legacy/CvCudaLegacy.h" #include #include -#include - namespace cvcuda::priv { class AverageBlur final : public IOperator @@ -46,8 +45,8 @@ class AverageBlur final : public IOperator const nvcv::Tensor &kernelSize, const nvcv::Tensor &kernelAnchor, NVCVBorderType borderMode) const; private: - std::unique_ptr m_legacyOp; - std::unique_ptr m_legacyOpVarShape; + mutable PerDeviceResource m_legacyOp; + mutable PerDeviceResource m_legacyOpVarShape; }; } // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpBilateralFilter.cpp b/src/cvcuda/priv/OpBilateralFilter.cpp index a9d51c843..1c6645307 100644 --- a/src/cvcuda/priv/OpBilateralFilter.cpp +++ b/src/cvcuda/priv/OpBilateralFilter.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "OpBilateralFilter.hpp" +#include "Nvtx.hpp" #include "legacy/CvCudaLegacy.h" #include "legacy/CvCudaLegacyHelpers.hpp" @@ -29,7 +30,8 @@ namespace legacy = nvcv::legacy::cuda_op; BilateralFilter::BilateralFilter() { - legacy::DataShape maxIn, maxOut; + legacy::DataShape maxIn; + legacy::DataShape maxOut; //maxIn/maxOut not used by op. m_legacyOp = std::make_unique(maxIn, maxOut); m_legacyOpVarShape = std::make_unique(maxIn, maxOut); @@ -38,6 +40,7 @@ BilateralFilter::BilateralFilter() void BilateralFilter::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, int diameter, float sigmaColor, float sigmaSpace, NVCVBorderType borderMode) const { + CVCUDA_NVTX_RANGE("cvcuda::BilateralFilter::operator()[Tensor]"); auto inData = in.exportData(); if (inData == nullptr) { @@ -60,6 +63,7 @@ void BilateralFilter::operator()(cudaStream_t stream, const nvcv::ImageBatchVarS const nvcv::Tensor &sigmaColor, const nvcv::Tensor &sigmaSpace, NVCVBorderType borderMode) const { + CVCUDA_NVTX_RANGE("cvcuda::BilateralFilter::operator()[ImageBatchVarShape]"); auto inData = in.exportData(stream); if (inData == nullptr) { diff --git a/src/cvcuda/priv/OpBndBox.cpp b/src/cvcuda/priv/OpBndBox.cpp index 2a6eb5c7f..c024bf6ea 100644 --- a/src/cvcuda/priv/OpBndBox.cpp +++ b/src/cvcuda/priv/OpBndBox.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,25 +17,221 @@ #include "OpBndBox.hpp" +#include "Nvtx.hpp" +#include "OpReformat.hpp" +#include "Types.hpp" #include "legacy/CvCudaLegacy.h" #include "legacy/CvCudaLegacyHelpers.hpp" +#include #include +#include #include +#include +#include + namespace cvcuda::priv { -namespace legacy = nvcv::legacy::cuda_op; +namespace legacy = nvcv::legacy::cuda_op; +namespace helpers = nvcv::legacy::helpers; + +BndBoxPlanarBridgeWorkspace::BndBoxPlanarBridgeWorkspace() +{ + NVCV_CHECK_THROW(cudaEventCreateWithFlags(&ready, cudaEventDisableTiming)); +} + +BndBoxPlanarBridgeWorkspace::~BndBoxPlanarBridgeWorkspace() +{ + if (ready != nullptr) + { + if (busy) + { + NVCV_CHECK_LOG(cudaEventSynchronize(ready)); + } + NVCV_CHECK_LOG(cudaEventDestroy(ready)); + } +} + +void BndBoxPlanarBridgeWorkspace::ensure(const nvcv::TensorShape &shape, nvcv::DataType dtype, cudaStream_t stream) +{ + const bool reallocate = !interleaved || interleaved.shape() != shape || interleaved.dtype() != dtype; + + if (busy) + { + if (reallocate) + { + NVCV_CHECK_THROW(cudaEventSynchronize(ready)); + } + else + { + NVCV_CHECK_THROW(cudaStreamWaitEvent(stream, ready)); + } + } + + if (reallocate) + { + interleaved = nvcv::Tensor(shape, dtype); + } +} + +void BndBoxPlanarBridgeWorkspace::record(cudaStream_t stream) +{ + NVCV_CHECK_THROW(cudaEventRecord(ready, stream)); + busy = true; +} + +namespace { + +bool IsPacked8BitImageBatch(const nvcv::TensorDataAccessStridedImagePlanar &access) +{ + return access.chStride() == 1 && access.colStride() == access.numChannels(); +} + +bool CanCopyThenDrawInplace(const nvcv::TensorDataStridedCuda &inData, const nvcv::TensorDataStridedCuda &outData, + const nvcv::TensorDataAccessStridedImagePlanar &inAccess, + const nvcv::TensorDataAccessStridedImagePlanar &outAccess, NVCVBndBoxesI bboxes) +{ + if (bboxes == nullptr || inData.basePtr() == outData.basePtr()) + { + return false; + } + + { + const auto inputFormat = helpers::GetLegacyDataFormat(inData.layout()); + const auto outputFormat = helpers::GetLegacyDataFormat(outData.layout()); + if (!(inputFormat == legacy::kNHWC || inputFormat == legacy::kHWC) + || !(outputFormat == legacy::kNHWC || outputFormat == legacy::kHWC)) + { + return false; + } + } + + if (inData.dtype() != outData.dtype() || helpers::GetLegacyDataType(inData.dtype()) != legacy::kCV_8U) + { + return false; + } + + if (inAccess.numSamples() != outAccess.numSamples() || inAccess.numRows() != outAccess.numRows() + || inAccess.numCols() != outAccess.numCols() || inAccess.numChannels() != outAccess.numChannels()) + { + return false; + } + + int channels = inAccess.numChannels(); + // The legacy renderer updates 2x2 packed U8 RGB(A) quads; other channel counts, odd extents, + // or non-packed pixels must keep the general out-of-place path. + if (channels < 3 || channels > 4 || (inAccess.numRows() & 1) != 0 || (inAccess.numCols() & 1) != 0 + || !IsPacked8BitImageBatch(inAccess) || !IsPacked8BitImageBatch(outAccess)) + { + return false; + } + + if (const auto rowBytes = static_cast(inAccess.numCols() * channels); + rowBytes <= 0 || inAccess.rowStride() < rowBytes || outAccess.rowStride() < rowBytes) + { + return false; + } + + if (inAccess.numSamples() > 1 + && (inAccess.sampleStride() < inAccess.rowStride() * inAccess.numRows() + || outAccess.sampleStride() < outAccess.rowStride() * outAccess.numRows())) + { + return false; + } + + auto bboxesImpl = reinterpret_cast(bboxes); + if (bboxesImpl->batch() != inAccess.numSamples()) + { + return false; + } + + int64_t totalBoxes = 0; + for (int32_t i = 0; i < bboxesImpl->batch(); ++i) + { + totalBoxes += bboxesImpl->numBoxesAt(i); + } + + return totalBoxes > 0; +} + +void CopyTensorImageBatch(const nvcv::TensorDataAccessStridedImagePlanar &inAccess, + const nvcv::TensorDataAccessStridedImagePlanar &outAccess, cudaStream_t stream) +{ + const auto rowBytes = static_cast(inAccess.numCols() * inAccess.numChannels()); + + if (inAccess.sampleStride() == inAccess.rowStride() * inAccess.numRows() + && outAccess.sampleStride() == outAccess.rowStride() * outAccess.numRows()) + { + NVCV_CHECK_THROW(cudaMemcpy2DAsync( + outAccess.sampleData(0), static_cast(outAccess.rowStride()), inAccess.sampleData(0), + static_cast(inAccess.rowStride()), rowBytes, + static_cast(inAccess.numRows() * inAccess.numSamples()), cudaMemcpyDeviceToDevice, stream)); + return; + } + + for (int32_t i = 0; i < inAccess.numSamples(); ++i) + { + NVCV_CHECK_THROW(cudaMemcpy2DAsync(outAccess.sampleData(i), static_cast(outAccess.rowStride()), + inAccess.sampleData(i), static_cast(inAccess.rowStride()), + rowBytes, static_cast(inAccess.numRows()), + cudaMemcpyDeviceToDevice, stream)); + } +} + +bool IsPlanar(nvcv::TensorLayout layout) +{ + return layout == nvcv::TENSOR_NCHW || layout == nvcv::TENSOR_CHW; +} + +bool IsInterleaved(nvcv::TensorLayout layout) +{ + return layout == nvcv::TENSOR_NHWC || layout == nvcv::TENSOR_HWC; +} + +nvcv::TensorShape MakeInterleavedTensorShape(const nvcv::TensorDataStridedCuda &data, + const nvcv::TensorDataAccessStridedImagePlanar &access) +{ + if (data.layout() == nvcv::TENSOR_NCHW) + { + return nvcv::TensorShape{ + {access.numSamples(), access.numRows(), access.numCols(), access.numChannels()}, + "NHWC" + }; + } + if (data.layout() == nvcv::TENSOR_CHW) + { + return nvcv::TensorShape{ + {access.numRows(), access.numCols(), access.numChannels()}, + "HWC" + }; + } + + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input/output must have NCHW or CHW layout for planar BndBox adaptation"); +} + +} // namespace + +std::unique_ptr BndBox::CreatePlanarBridgeWorkspace(int) +{ + return std::make_unique(); +} BndBox::BndBox() { - legacy::DataShape maxIn, maxOut; //maxIn/maxOut not used by op. - m_legacyOp = std::make_unique(maxIn, maxOut); + legacy::DataShape maxIn; + legacy::DataShape maxOut; //maxIn/maxOut not used by op. + m_legacyOp = std::make_unique(maxIn, maxOut); + m_reformatOp = std::make_unique(); } +BndBox::~BndBox() = default; + void BndBox::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, const NVCVBndBoxesI &bboxes) const { + CVCUDA_NVTX_RANGE("cvcuda::BndBox::operator()[Tensor]"); auto inData = in.exportData(); if (inData == nullptr) { @@ -50,6 +246,74 @@ void BndBox::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv: "Output must be cuda-accessible, pitch-linear tensor"); } + const bool inPlanar = IsPlanar(inData->layout()); + if (const bool outPlanar = IsPlanar(outData->layout()); inPlanar || outPlanar) + { + if (!inPlanar || !outPlanar || inData->layout() != outData->layout()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input and output layouts must both be NCHW or both be CHW for planar BndBox"); + } + + auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*inData); + if (!inAccess) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input must be an image-like planar tensor"); + } + + auto outAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*outData); + if (!outAccess) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Output must be an image-like planar tensor"); + } + + if (inData->dtype() != outData->dtype() || inAccess->numSamples() != outAccess->numSamples() + || inAccess->numRows() != outAccess->numRows() || inAccess->numCols() != outAccess->numCols() + || inAccess->numChannels() != outAccess->numChannels()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input and output planar BndBox tensors must have matching shape and data type"); + } + + nvcv::TensorShape interleavedShape = MakeInterleavedTensorShape(*inData, *inAccess); + auto &workspace = m_planarWorkspace.get(); + std::lock_guard lock(workspace.mutex); + + workspace.ensure(interleavedShape, inData->dtype(), stream); + + (*m_reformatOp)(stream, in, workspace.interleaved); + + auto interleavedData = workspace.interleaved.exportData(); + if (interleavedData == nullptr) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Internal BndBox reformat tensors must be cuda-accessible"); + } + + NVCV_CHECK_THROW(m_legacyOp->inferBox(*interleavedData, *interleavedData, bboxes, stream)); + + (*m_reformatOp)(stream, workspace.interleaved, out); + workspace.record(stream); + return; + } + + if (!IsInterleaved(inData->layout()) || !IsInterleaved(outData->layout())) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input and output layouts must be NHWC/HWC or matching NCHW/CHW"); + } + + { + auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*inData); + auto outAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*outData); + if (inAccess && outAccess && CanCopyThenDrawInplace(*inData, *outData, *inAccess, *outAccess, bboxes)) + { + CopyTensorImageBatch(*inAccess, *outAccess, stream); + NVCV_CHECK_THROW(m_legacyOp->inferBox(*outData, *outData, bboxes, stream)); + return; + } + } + NVCV_CHECK_THROW(m_legacyOp->inferBox(*inData, *outData, bboxes, stream)); } diff --git a/src/cvcuda/priv/OpBndBox.hpp b/src/cvcuda/priv/OpBndBox.hpp index 00364f596..b6eff96d9 100644 --- a/src/cvcuda/priv/OpBndBox.hpp +++ b/src/cvcuda/priv/OpBndBox.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,24 +25,48 @@ #define CVCUDA_PRIV__BND_BOX_HPP #include "IOperator.hpp" +#include "PerDeviceResource.hpp" #include "legacy/CvCudaLegacy.h" +#include #include #include +#include namespace cvcuda::priv { +class Reformat; + +struct BndBoxPlanarBridgeWorkspace +{ + std::mutex mutex; + nvcv::Tensor interleaved; + cudaEvent_t ready = nullptr; + bool busy = false; + + BndBoxPlanarBridgeWorkspace(); + ~BndBoxPlanarBridgeWorkspace(); + + void ensure(const nvcv::TensorShape &shape, nvcv::DataType dtype, cudaStream_t stream); + void record(cudaStream_t stream); +}; + class BndBox final : public IOperator { public: explicit BndBox(); + ~BndBox() override; void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, const NVCVBndBoxesI &bboxes) const; private: - std::unique_ptr m_legacyOp; + static std::unique_ptr CreatePlanarBridgeWorkspace(int deviceId); + + std::unique_ptr m_legacyOp; + std::unique_ptr m_reformatOp; + mutable PerDeviceResource m_planarWorkspace{CreatePlanarBridgeWorkspace}; }; } // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpBoxBlur.cpp b/src/cvcuda/priv/OpBoxBlur.cpp index b503ca5fc..74c5089db 100644 --- a/src/cvcuda/priv/OpBoxBlur.cpp +++ b/src/cvcuda/priv/OpBoxBlur.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,25 +17,243 @@ #include "OpBoxBlur.hpp" +#include "Nvtx.hpp" #include "legacy/CvCudaLegacy.h" #include "legacy/CvCudaLegacyHelpers.hpp" +#include #include +#include #include +#include +#include +#include +#include + namespace cvcuda::priv { -namespace legacy = nvcv::legacy::cuda_op; +namespace legacy = nvcv::legacy::cuda_op; +namespace helpers = nvcv::legacy::helpers; + +namespace { + +enum class DenseCopyLayout +{ + INTERLEAVED, + PLANAR +}; + +std::optional GetDenseCopyLayout(legacy::DataFormat inputFormat, legacy::DataFormat outputFormat) +{ + if (inputFormat == legacy::kNHWC && outputFormat == legacy::kNHWC) + { + return DenseCopyLayout::INTERLEAVED; + } + if (inputFormat == legacy::kNCHW && outputFormat == legacy::kNCHW) + { + return DenseCopyLayout::PLANAR; + } + return std::nullopt; +} + +bool HaveSameImageShape(const nvcv::TensorDataAccessStridedImagePlanar &inAccess, + const nvcv::TensorDataAccessStridedImagePlanar &outAccess) +{ + return inAccess.numSamples() == outAccess.numSamples() && inAccess.numRows() == outAccess.numRows() + && inAccess.numCols() == outAccess.numCols() && inAccess.numChannels() == outAccess.numChannels(); +} + +bool HaveSupportedStrides(DenseCopyLayout layout, const nvcv::TensorDataAccessStridedImagePlanar &inAccess, + const nvcv::TensorDataAccessStridedImagePlanar &outAccess, int64_t rows, int64_t channels, + int64_t rowBytes) +{ + if (layout == DenseCopyLayout::INTERLEAVED) + { + return inAccess.chStride() == 1 && outAccess.chStride() == 1 && inAccess.colStride() == channels + && outAccess.colStride() == channels; + } + + return inAccess.colStride() == 1 && outAccess.colStride() == 1 && inAccess.chStride() == rows * inAccess.rowStride() + && outAccess.chStride() == rows * outAccess.rowStride() && inAccess.rowStride() >= rowBytes + && outAccess.rowStride() >= rowBytes; +} + +int64_t RowsPerSample(int64_t sampleStride, int64_t rowStride, int64_t copyRows, int64_t samples) +{ + if (sampleStride == 0) + { + return samples == 1 ? copyRows : 0; + } + if (rowStride <= 0 || sampleStride % rowStride != 0) + { + return 0; + } + return sampleStride / rowStride; +} + +bool BuffersOverlap(const nvcv::Byte *src, const nvcv::Byte *dst, + const nvcv::TensorDataAccessStridedImagePlanar &inAccess, + const nvcv::TensorDataAccessStridedImagePlanar &outAccess, int64_t samples, int64_t copyRows, + int64_t rowBytes) +{ + const auto srcStart = reinterpret_cast(src); + const auto dstStart = reinterpret_cast(dst); + const auto srcSpan = static_cast((samples - 1) * inAccess.sampleStride() + + (copyRows - 1) * inAccess.rowStride() + rowBytes); + const auto dstSpan = static_cast((samples - 1) * outAccess.sampleStride() + + (copyRows - 1) * outAccess.rowStride() + rowBytes); + return srcStart < dstStart + dstSpan && dstStart < srcStart + srcSpan; +} + +std::optional DenseCopyByteCount(int64_t copyRows, int64_t rowBytes, int64_t samples) +{ + if (copyRows > std::numeric_limits::max() / rowBytes) + { + return std::nullopt; + } + + const int64_t sampleBytes = copyRows * rowBytes; + if (samples > std::numeric_limits::max() / sampleBytes) + { + return std::nullopt; + } + return sampleBytes * samples; +} + +bool CanUseLinearCopy(DenseCopyLayout layout, const nvcv::TensorDataAccessStridedImagePlanar &inAccess, + const nvcv::TensorDataAccessStridedImagePlanar &outAccess, int64_t samples, int64_t rowBytes, + int64_t sampleBytes) +{ + return layout == DenseCopyLayout::INTERLEAVED && inAccess.rowStride() == rowBytes + && outAccess.rowStride() == rowBytes + && (samples == 1 || (inAccess.sampleStride() == sampleBytes && outAccess.sampleStride() == sampleBytes)); +} + +bool TryDenseCopy(cudaStream_t stream, const nvcv::TensorDataStridedCuda &inData, + const nvcv::TensorDataStridedCuda &outData, NVCVBlurBoxesI bboxes) +{ + if (bboxes == nullptr || inData.dtype() != outData.dtype()) + { + return false; + } + + legacy::DataFormat inputFormat; + legacy::DataFormat outputFormat; + legacy::DataType dataType; + try + { + inputFormat = helpers::GetLegacyDataFormat(inData.layout()); + outputFormat = helpers::GetLegacyDataFormat(outData.layout()); + dataType = helpers::GetLegacyDataType(inData.dtype()); + } + catch (const nvcv::Exception &) + { + return false; + } + + const auto copyLayout = GetDenseCopyLayout(inputFormat, outputFormat); + if (dataType != legacy::kCV_8U || !copyLayout) + { + return false; + } + + auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(inData); + auto outAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(outData); + if (!inAccess || !outAccess || !HaveSameImageShape(*inAccess, *outAccess)) + { + return false; + } + + if (const auto *blurBoxes = reinterpret_cast(bboxes); + blurBoxes->batch() != inAccess->numSamples()) + { + return false; + } + + const int64_t samples = inAccess->numSamples(); + const int64_t rows = inAccess->numRows(); + const int64_t cols = inAccess->numCols(); + const int64_t channels = inAccess->numChannels(); + if (channels != 3 && channels != 4) + { + return false; + } + + const bool isInterleaved = *copyLayout == DenseCopyLayout::INTERLEAVED; + const int64_t rowBytes = isInterleaved ? cols * channels : cols; + const int64_t copyRows = isInterleaved ? rows : rows * channels; + + if (samples <= 0 || rows <= 0 || cols <= 0 || rowBytes <= 0 || copyRows <= 0 || inAccess->rowStride() < rowBytes + || outAccess->rowStride() < rowBytes) + { + return false; + } + + if (!HaveSupportedStrides(*copyLayout, *inAccess, *outAccess, rows, channels, rowBytes)) + { + return false; + } + + const int64_t srcRowsPerSample = RowsPerSample(inAccess->sampleStride(), inAccess->rowStride(), copyRows, samples); + const int64_t dstRowsPerSample + = RowsPerSample(outAccess->sampleStride(), outAccess->rowStride(), copyRows, samples); + if (srcRowsPerSample < copyRows || dstRowsPerSample < copyRows) + { + return false; + } + + auto *src = inAccess->sampleData(0); + auto *dst = outAccess->sampleData(0); + if (src == dst) + { + return false; + } + + if (BuffersOverlap(src, dst, *inAccess, *outAccess, samples, copyRows, rowBytes)) + { + return false; + } + + const auto byteCount = DenseCopyByteCount(copyRows, rowBytes, samples); + if (!byteCount.has_value()) + { + return false; + } + + if (const int64_t denseSampleBytes = *byteCount / samples; + CanUseLinearCopy(*copyLayout, *inAccess, *outAccess, samples, rowBytes, denseSampleBytes)) + { + NVCV_CHECK_THROW(cudaMemcpyAsync(dst, src, static_cast(*byteCount), cudaMemcpyDeviceToDevice, stream)); + return true; + } + + cudaMemcpy3DParms copyParams{}; + copyParams.srcPtr = make_cudaPitchedPtr(src, static_cast(inAccess->rowStride()), + static_cast(rowBytes), static_cast(srcRowsPerSample)); + copyParams.dstPtr = make_cudaPitchedPtr(dst, static_cast(outAccess->rowStride()), + static_cast(rowBytes), static_cast(dstRowsPerSample)); + copyParams.extent + = make_cudaExtent(static_cast(rowBytes), static_cast(copyRows), static_cast(samples)); + copyParams.kind = cudaMemcpyDeviceToDevice; + + NVCV_CHECK_THROW(cudaMemcpy3DAsync(©Params, stream)); + return true; +} + +} // namespace BoxBlur::BoxBlur() { - legacy::DataShape maxIn, maxOut; //maxIn/maxOut not used by op. + legacy::DataShape maxIn; + legacy::DataShape maxOut; //maxIn/maxOut not used by op. m_legacyOp = std::make_unique(maxIn, maxOut); } void BoxBlur::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, const NVCVBlurBoxesI &bboxes) const { + CVCUDA_NVTX_RANGE("cvcuda::BoxBlur::operator()[Tensor]"); auto inData = in.exportData(); if (inData == nullptr) { @@ -50,7 +268,15 @@ void BoxBlur::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv "Output must be cuda-accessible, pitch-linear tensor"); } - NVCV_CHECK_THROW(m_legacyOp->infer(*inData, *outData, bboxes, stream)); + if (TryDenseCopy(stream, *inData, *outData, bboxes)) + { + // Keep the immutable input as the blur source so concurrent boxes cannot observe each other's output writes. + NVCV_CHECK_THROW(m_legacyOp->infer(*inData, *outData, bboxes, stream, true)); + } + else + { + NVCV_CHECK_THROW(m_legacyOp->infer(*inData, *outData, bboxes, stream)); + } } } // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpBrightnessContrast.cu b/src/cvcuda/priv/OpBrightnessContrast.cu index 6313b45a5..746fa4d95 100644 --- a/src/cvcuda/priv/OpBrightnessContrast.cu +++ b/src/cvcuda/priv/OpBrightnessContrast.cu @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,6 +15,9 @@ * limitations under the License. */ +#include "BrightnessContrastPolicy.hpp" +#include "CudaDeviceUtils.hpp" +#include "Nvtx.hpp" #include "OpBrightnessContrast.hpp" #include @@ -23,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -58,10 +62,11 @@ using ArgWrapper = cuda::Tensor1DWrap; template struct SampleArgs { - BT brightness; - BT contrast; - BT brightnessShift; - BT contrastCenter; + BT brightness; + BT contrast; + BT brightnessShift; + BT contrastCenter; + bool clamp; }; template @@ -72,12 +77,21 @@ struct BatchArgsWrap const ArgWrapper contrast; const ArgWrapper brightnessShift; const ArgWrapper contrastCenter; + BT scalarBrightness; + BT scalarContrast; + BT scalarBrightnessShift; + BT scalarContrastCenter; + bool clamp; }; template -inline __device__ BT GetArg(const ArgWrapper &tensorArg, int argLen, int sampleIdx, BT defaultVal) +inline __device__ BT GetArg(const ArgWrapper &tensorArg, int argLen, int sampleIdx, BT defaultVal, BT scalarVal) { - if (argLen == 0) + if (argLen < 0) + { + return scalarVal; + } + else if (argLen == 0) { return defaultVal; } @@ -94,10 +108,33 @@ inline __device__ BT GetArg(const ArgWrapper &tensorArg, int argLen, int sam template inline __device__ SampleArgs GetBrightnessContrastArg(const BatchArgsWrap &args, int sampleIdx) { - return {GetArg(args.brightness, args.brightnessLen, sampleIdx, BT{1}), - GetArg(args.contrast, args.contrastLen, sampleIdx, BT{1}), - GetArg(args.brightnessShift, args.brightnessShiftLen, sampleIdx, BT{0}), - GetArg(args.contrastCenter, args.contrastCenterLen, sampleIdx, HalfRange())}; + return {GetArg(args.brightness, args.brightnessLen, sampleIdx, BT{1}, args.scalarBrightness), + GetArg(args.contrast, args.contrastLen, sampleIdx, BT{1}, args.scalarContrast), + GetArg(args.brightnessShift, args.brightnessShiftLen, sampleIdx, BT{0}, args.scalarBrightnessShift), + GetArg(args.contrastCenter, args.contrastCenterLen, sampleIdx, HalfRange(), + args.scalarContrastCenter), + args.clamp}; +} + +template +inline __device__ T ClampToImageRange(T value, bool clamp) +{ + if (!clamp) + { + return value; + } + + using BT = cuda::BaseType; + using DstBT = cuda::BaseType; + static constexpr int numElements = cuda::NumElements; + const BT bound = std::is_floating_point_v ? BT{1} : static_cast(cuda::TypeTraits::max); +#pragma unroll + for (int c = 0; c < numElements; ++c) + { + auto &v = cuda::GetElement(value, c); + v = v < BT{0} ? BT{0} : (v > bound ? bound : v); + } + return value; } template @@ -138,9 +175,127 @@ inline __device__ void DoBrightnessContrast(SrcWrapper src, DstWrapper dst, cons auto coord = GetCoordForLayout(nhwCoord, p); auto pixel = cuda::StaticCast(src[coord]); pixel = arg.brightnessShift + arg.brightness * (arg.contrastCenter + arg.contrast * (pixel - arg.contrastCenter)); + pixel = ClampToImageRange(pixel, arg.clamp); dst[coord] = cuda::SaturateCast(pixel); } +// Per-pixel affine, factored out so the scalar and vectorized paths share byte-identical math. +template +inline __device__ DstT ApplyBrightnessContrast(SrcT v, const SampleArgs &arg) +{ + using IntermediateT = decltype(std::declval() * std::declval()); + using BI = cuda::BaseType; + auto pixel = cuda::StaticCast(v); + pixel = arg.brightnessShift + arg.brightness * (arg.contrastCenter + arg.contrast * (pixel - arg.contrastCenter)); + pixel = ClampToImageRange(pixel, arg.clamp); + return cuda::SaturateCast(pixel); +} + +// Vector pack type for T: uint3 (12B) for 3-element T, else uint4 (16B). BC_NIX = pixels/thread. +template +using BC_DPT = std::conditional_t == 3, uint3, uint4>; +template +constexpr int BC_NIX = sizeof(BC_DPT) / sizeof(T); +template +using BC_BATCH_DPT = std::conditional_t, unsigned char> && cuda::NumElements == 1, + uint2, BC_DPT>; +template +constexpr uintptr_t BC_PACK_MSK = (sizeof(PackT) == sizeof(uint3) ? sizeof(uint) : sizeof(PackT)) - 1; +template +constexpr uintptr_t BC_MSK = BC_PACK_MSK>; + +// Vectorized interleaved (NHWC) brightness/contrast. Keep this path separate from planar batching so +// that the existing one-plane kernel does not pay for runtime plane selection. +template +__global__ void BrightnessContrastVec(SrcWrapper src, DstWrapper dst, BatchArgsWrap batchArgs, int2 size) +{ + using SrcT = std::remove_const_t; + using DstT = typename DstWrapper::ValueType; + using SrcBT = cuda::BaseType; + static constexpr int N = BC_NIX; + + const int z = blockIdx.z; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + if (y >= size.y) + return; + const int x0 = (blockIdx.x * blockDim.x + threadIdx.x) * N; + if (x0 >= size.x) + return; + + const auto arg = GetBrightnessContrastArg(batchArgs, z); + const SrcT *sp = &src[int3{x0, y, z}]; + DstT *dp = &dst[int3{x0, y, z}]; + + if (x0 + N - 1 < size.x && (reinterpret_cast(sp) & BC_MSK) == 0 + && (reinterpret_cast(dp) & BC_MSK) == 0) + { + alignas(BC_DPT) SrcT in[N]; + *reinterpret_cast *>(in) = *reinterpret_cast *>(sp); + alignas(BC_DPT) DstT out[N]; +#pragma unroll + for (int i = 0; i < N; ++i) out[i] = ApplyBrightnessContrast(in[i], arg); + *reinterpret_cast *>(dp) = *reinterpret_cast *>(out); + } + else + { +#pragma unroll + for (int i = 0; i < N; ++i) + if (x0 + i < size.x) + dp[i] = ApplyBrightnessContrast(sp[i], arg); + } +} + +// Batch adjacent planar or var-shape elements while reusing wrapper lookups and per-image arguments. +template +__global__ void BrightnessContrastBatchVec(SrcWrapper src, DstWrapper dst, BatchArgsWrap batchArgs, + int2 tensorSize, int numPlanes) +{ + using SrcT = std::remove_const_t; + using DstT = typename DstWrapper::ValueType; + using SrcBT = cuda::BaseType; + static constexpr int N = sizeof(PackT) / sizeof(SrcT); + + const int z = blockIdx.z; + int2 size{tensorSize}; + if constexpr (IsVarShape) + { + size = {dst.width(z), dst.height(z)}; + } + const int y = blockIdx.y * blockDim.y + threadIdx.y; + if (y >= size.y) + return; + const int x0 = (blockIdx.x * blockDim.x + threadIdx.x) * N; + if (x0 >= size.x) + return; + + const auto arg = GetBrightnessContrastArg(batchArgs, z); + assert(IsPlanar || numPlanes == 1); + for (int p = 0; p < numPlanes; ++p) + { + auto coord = GetCoordForLayout(int3{x0, y, z}, p); + const SrcT *sp = &src[coord]; + DstT *dp = &dst[coord]; + + if (x0 + N - 1 < size.x && (reinterpret_cast(sp) & BC_PACK_MSK) == 0 + && (reinterpret_cast(dp) & BC_PACK_MSK) == 0) + { + alignas(PackT) SrcT in[N]; + *reinterpret_cast(in) = *reinterpret_cast(sp); + alignas(PackT) DstT out[N]; +#pragma unroll + for (int i = 0; i < N; ++i) out[i] = ApplyBrightnessContrast(in[i], arg); + *reinterpret_cast(dp) = *reinterpret_cast(out); + } + else + { +#pragma unroll + for (int i = 0; i < N; ++i) + if (x0 + i < size.x) + dp[i] = ApplyBrightnessContrast(sp[i], arg); + } + } +} + // BrightnessContrast kernel -------------------------------------------------------------- // Tensor variant @@ -195,6 +350,12 @@ template &batchArgs) { + using SrcBT = cuda::BaseType; + static constexpr int kNix = BC_NIX; + static constexpr int kBatchNix = sizeof(BC_BATCH_DPT) / sizeof(SrcValueT); + static constexpr int kNumElements = cuda::NumElements; + static constexpr bool kUseBatchedPath + = kNix > 1 && std::is_same_v && (kNumElements == 1 || kNumElements == 3); dim3 block(32, 4, 1); if constexpr (std::is_same_v) { @@ -216,7 +377,30 @@ inline void RunBrightnessContrast(cudaStream_t stream, const SrcData &srcData, c { auto src = cuda::CreateTensorWrapNHW(srcData); auto dst = cuda::CreateTensorWrapNHW(dstData); - BrightnessContrast<<>>(src, dst, batchArgs, size, 1); + if constexpr (std::is_same_v) + { + // Vectorized interleaved path (BC_NIX pixels/thread). Same-type only so the load/store + // pack widths match; mixed in/out types keep the scalar kernel. + int sm = 0; + NVCV_CHECK_THROW(cvcuda::priv::GetCurrentDeviceSM(sm)); + constexpr bool kIsUnsignedByte = std::is_same_v; + auto policy + = cvcuda::priv::BrightnessContrastTensorKernelPolicyForSM(sm, kIsUnsignedByte, kNumElements); + if (policy == cvcuda::priv::BrightnessContrastTensorKernelPolicy::kScalar) + { + BrightnessContrast<<>>(src, dst, batchArgs, size, 1); + } + else + { + dim3 gridV(util::DivUp(size.x, block.x * kNix), util::DivUp(size.y, block.y), + srcAccess->numSamples()); + BrightnessContrastVec<<>>(src, dst, batchArgs, size); + } + } + else + { + BrightnessContrast<<>>(src, dst, batchArgs, size, 1); + } } else { @@ -227,7 +411,17 @@ inline void RunBrightnessContrast(cudaStream_t stream, const SrcData &srcData, c dstData.basePtr(), static_cast(dstAccess->sampleStride()), static_cast(dstAccess->planeStride()), static_cast(dstAccess->rowStride())); int numPlanes = srcAccess->numPlanes(); - BrightnessContrast<<>>(src, dst, batchArgs, size, numPlanes); + if constexpr (std::is_same_v && kUseBatchedPath) + { + dim3 gridV(util::DivUp(size.x, block.x * kBatchNix), util::DivUp(size.y, block.y), + srcAccess->numSamples()); + BrightnessContrastBatchVec> + <<>>(src, dst, batchArgs, size, numPlanes); + } + else + { + BrightnessContrast<<>>(src, dst, batchArgs, size, numPlanes); + } } NVCV_CHECK_THROW(cudaGetLastError()); } @@ -241,7 +435,17 @@ inline void RunBrightnessContrast(cudaStream_t stream, const SrcData &srcData, c cuda::ImageBatchVarShapeWrap dst(dstData); int numPlanes = dstData.uniqueFormat().numPlanes(); - BrightnessContrast<<>>(src, dst, batchArgs, numPlanes); + if constexpr (std::is_same_v && kUseBatchedPath) + { + dim3 gridV(util::DivUp(dstMaxSize.x, block.x * kBatchNix), util::DivUp(dstMaxSize.y, block.y), + dstMaxSize.z); + BrightnessContrastBatchVec> + <<>>(src, dst, batchArgs, int2{}, numPlanes); + } + else + { + BrightnessContrast<<>>(src, dst, batchArgs, numPlanes); + } NVCV_CHECK_THROW(cudaGetLastError()); } @@ -285,12 +489,12 @@ inline void RunTypeSwitch(int numChannels, int numPlanes, nvcv::DataType srcType RunTypeSwitch( srcType, - [&](auto dummySrcVal) + [&dstType, &argType, &numChannels, &numPlanes, &cb](auto dummySrcVal) { using SrcValBase = decltype(dummySrcVal); RunTypeSwitch( dstType, - [&](auto dummyDstVal) + [&argType, &numChannels, &numPlanes, &cb](auto dummyDstVal) { using DstValBase = decltype(dummyDstVal); using ArgT = GetArgType; @@ -360,6 +564,18 @@ inline void ValidateSrcDstTensors(int &numSamples, int &numInterleavedChannels, "Output must be cuda-accessible, pitch-linear tensor"); } + // Check layout before creating TensorDataAccessStridedImagePlanar, as Create() will fail for unsupported layouts + if (srcData->layout() != dstData->layout()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input and output must have the same layout"); + } + + if (!(srcData->layout() == nvcv::TENSOR_HWC || srcData->layout() == nvcv::TENSOR_NHWC + || srcData->layout() == nvcv::TENSOR_CHW || srcData->layout() == nvcv::TENSOR_NCHW)) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input must have (N)HWC or (N)CHW layout"); + } + auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcData); auto dstAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*dstData); NVCV_ASSERT(srcAccess && dstAccess); @@ -375,12 +591,20 @@ inline void ValidateSrcDstTensors(int &numSamples, int &numInterleavedChannels, { throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of channels"); } + if (numChannels > 4) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Invalid channel number %d", numChannels); + } numPlanes = srcAccess->numPlanes(); if (numPlanes != dstAccess->numPlanes()) { throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of planes"); } + if (numPlanes > 1 && numChannels == 2) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "2-channel planar images are not supported"); + } if (srcAccess->numCols() != dstAccess->numCols() || srcAccess->numRows() != dstAccess->numRows()) { @@ -388,11 +612,6 @@ inline void ValidateSrcDstTensors(int &numSamples, int &numInterleavedChannels, "Input and output must have matching width and height"); } - if (srcData->layout() != dstData->layout()) - { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input and output must have the same layout"); - } - srcDtype = srcData->dtype(); dstDtype = dstData->dtype(); numInterleavedChannels = srcAccess->infoLayout().isChannelLast() ? numChannels : 1; @@ -434,7 +653,31 @@ inline BatchArgsWrap GetBatchArgsWrap(nvcv::Optional{} : ArgWrapper(*brightnessData), contrastLen == 0 ? ArgWrapper{} : ArgWrapper(*contrastData), brightnessShiftLen == 0 ? ArgWrapper{} : ArgWrapper(*brightnessShiftData), - constrastCenterLen == 0 ? ArgWrapper{} : ArgWrapper(*contrastCenterData)}; + constrastCenterLen == 0 ? ArgWrapper{} : ArgWrapper(*contrastCenterData), + ArgT{}, + ArgT{}, + ArgT{}, + ArgT{}, + false}; +} + +template +inline BatchArgsWrap GetScalarBatchArgsWrap(double brightness, double contrast, double brightnessShift, + double contrastCenter, bool clamp) +{ + return {-1, + -1, + -1, + -1, + ArgWrapper{}, + ArgWrapper{}, + ArgWrapper{}, + ArgWrapper{}, + static_cast(brightness), + static_cast(contrast), + static_cast(brightnessShift), + static_cast(contrastCenter), + clamp}; } inline auto validateSrcDstVarBatch(int &numSamples, int &numInterleavedChannels, int &numPlanes, @@ -478,12 +721,20 @@ inline auto validateSrcDstVarBatch(int &numSamples, int &numInterleavedChannels, { throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of channels"); } + if (numChannels > 4) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Invalid channel number %d", numChannels); + } numPlanes = srcFormat.numPlanes(); if (numPlanes != dstFormat.numPlanes()) { throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of planes"); } + if (numPlanes > 1 && numChannels == 2) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "2-channel planar images are not supported"); + } srcDtype = srcFormat.planeDataType(0); for (int i = 1; i < numPlanes; ++i) @@ -538,8 +789,9 @@ inline void ValidateTensorArgs(nvcv::DataType &argDType, nvcv::Optional &argData, - const nvcv::Tensor &argTensor) + auto validateArgData + = [&argDType, &numSamples](const std::string &argName, nvcv::Optional &argData, + const nvcv::Tensor &argTensor) { if (argTensor) { @@ -605,6 +857,7 @@ void BrightnessContrast::operator()(cudaStream_t stream, const nvcv::Tensor &src const nvcv::Tensor &brightness, const nvcv::Tensor &contrast, const nvcv::Tensor &brightnessShift, const nvcv::Tensor &contrastCenter) const { + CVCUDA_NVTX_RANGE("cvcuda::BrightnessContrast::operator()[Tensor]"); int numSamples; int numInterleavedChannels; int numPlanes; @@ -621,7 +874,8 @@ void BrightnessContrast::operator()(cudaStream_t stream, const nvcv::Tensor &src ValidateTensorArgs(argDType, brightnessData, contrastData, brightnessShiftData, contrastCenterData, numSamples, brightness, contrast, brightnessShift, contrastCenter); RunTypeSwitch(numInterleavedChannels, numPlanes, srcDtype, dstDtype, argDType, - [&](auto dummySrcVal, auto dummyDstVal, auto dummyArg, auto isPlanar) + [&brightnessData, &contrastData, &brightnessShiftData, &contrastCenterData, &stream, &srcData, + &dstData](auto dummySrcVal, auto dummyDstVal, auto dummyArg, auto isPlanar) { using InT = decltype(dummySrcVal); using OutT = decltype(dummyDstVal); @@ -639,6 +893,7 @@ void BrightnessContrast::operator()(cudaStream_t stream, const nvcv::ImageBatchV const nvcv::Tensor &contrast, const nvcv::Tensor &brightnessShift, const nvcv::Tensor &contrastCenter) const { + CVCUDA_NVTX_RANGE("cvcuda::BrightnessContrast::operator()[ImageBatchVarShape]"); int numSamples; int numInterleavedChannels; int numPlanes; @@ -654,7 +909,8 @@ void BrightnessContrast::operator()(cudaStream_t stream, const nvcv::ImageBatchV ValidateTensorArgs(argDType, brightnessData, contrastData, brightnessShiftData, contrastCenterData, numSamples, brightness, contrast, brightnessShift, contrastCenter); RunTypeSwitch(numInterleavedChannels, numPlanes, srcDtype, dstDtype, argDType, - [&](auto dummySrcVal, auto dummyDstVal, auto dummyArg, auto isPlanar) + [&brightnessData, &contrastData, &brightnessShiftData, &contrastCenterData, &stream, &srcDstData]( + auto dummySrcVal, auto dummyDstVal, auto dummyArg, auto isPlanar) { using InT = decltype(dummySrcVal); using OutT = decltype(dummyDstVal); @@ -667,4 +923,58 @@ void BrightnessContrast::operator()(cudaStream_t stream, const nvcv::ImageBatchV }); } +void BrightnessContrast::operator()(cudaStream_t stream, const nvcv::Tensor &src, const nvcv::Tensor &dst, + double brightness, double contrast, double brightnessShift, double contrastCenter, + bool clamp) const +{ + CVCUDA_NVTX_RANGE("cvcuda::BrightnessContrast::operator()[Tensor,Scalar]"); + int numSamples; + int numInterleavedChannels; + int numPlanes; + nvcv::DataType srcDtype; + nvcv::DataType dstDtype; + auto srcData = src.exportData(); + auto dstData = dst.exportData(); + ValidateSrcDstTensors(numSamples, numInterleavedChannels, numPlanes, srcDtype, dstDtype, srcData, dstData); + RunTypeSwitch(numInterleavedChannels, numPlanes, srcDtype, dstDtype, nvcv::DataType{}, + [&, brightness, contrast, brightnessShift, contrastCenter, clamp](auto dummySrcVal, auto dummyDstVal, + auto dummyArg, auto isPlanar) + { + using InT = decltype(dummySrcVal); + using OutT = decltype(dummyDstVal); + using ArgT = decltype(dummyArg); + using IsPlanar = decltype(isPlanar); + auto args + = GetScalarBatchArgsWrap(brightness, contrast, brightnessShift, contrastCenter, clamp); + RunBrightnessContrast(stream, *srcData, *dstData, args); + }); +} + +void BrightnessContrast::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &src, + const nvcv::ImageBatchVarShape &dst, double brightness, double contrast, + double brightnessShift, double contrastCenter, bool clamp) const +{ + CVCUDA_NVTX_RANGE("cvcuda::BrightnessContrast::operator()[ImageBatchVarShape,Scalar]"); + int numSamples; + int numInterleavedChannels; + int numPlanes; + nvcv::DataType srcDtype; + nvcv::DataType dstDtype; + auto srcDstData + = validateSrcDstVarBatch(numSamples, numInterleavedChannels, numPlanes, srcDtype, dstDtype, stream, src, dst); + RunTypeSwitch(numInterleavedChannels, numPlanes, srcDtype, dstDtype, nvcv::DataType{}, + [&, brightness, contrast, brightnessShift, contrastCenter, clamp](auto dummySrcVal, auto dummyDstVal, + auto dummyArg, auto isPlanar) + { + using InT = decltype(dummySrcVal); + using OutT = decltype(dummyDstVal); + using ArgT = decltype(dummyArg); + using IsPlanar = decltype(isPlanar); + auto args + = GetScalarBatchArgsWrap(brightness, contrast, brightnessShift, contrastCenter, clamp); + auto &[srcData, dstData] = srcDstData; + RunBrightnessContrast(stream, *srcData, *dstData, args); + }); +} + } // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpBrightnessContrast.hpp b/src/cvcuda/priv/OpBrightnessContrast.hpp index cd911e654..7876e742c 100644 --- a/src/cvcuda/priv/OpBrightnessContrast.hpp +++ b/src/cvcuda/priv/OpBrightnessContrast.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -43,6 +43,13 @@ class BrightnessContrast final : public IOperator void operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &src, const nvcv::ImageBatchVarShape &dst, const nvcv::Tensor &brightness, const nvcv::Tensor &contrast, const nvcv::Tensor &brightnessShift, const nvcv::Tensor &contrastCenter) const; + + void operator()(cudaStream_t stream, const nvcv::Tensor &src, const nvcv::Tensor &dst, double brightness, + double contrast, double brightnessShift, double contrastCenter, bool clamp) const; + + void operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &src, const nvcv::ImageBatchVarShape &dst, + double brightness, double contrast, double brightnessShift, double contrastCenter, + bool clamp) const; }; } // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpCLAHE.cu b/src/cvcuda/priv/OpCLAHE.cu new file mode 100644 index 000000000..d6f33acb0 --- /dev/null +++ b/src/cvcuda/priv/OpCLAHE.cu @@ -0,0 +1,668 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "Nvtx.hpp" +#include "OpCLAHE.hpp" + +#include "legacy/CvCudaUtils.cuh" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +namespace cvcuda::priv { + +namespace { + +constexpr int kHistBins = 256; + +template +struct IsBorderVarShapeWrap : std::false_type +{ +}; + +template +struct IsBorderVarShapeWrap> : std::true_type +{ +}; + +template +struct IsImageBatchVarShapeWrap : std::false_type +{ +}; + +template +struct IsImageBatchVarShapeWrap> : std::true_type +{ +}; + +template +__global__ void CLAHEBuildLUTKernel(SrcWrapper src, unsigned char *luts, float clipLimit, int32_t tilesX, + int32_t tilesY) +{ + const int32_t batch = blockIdx.z; + const int32_t tx = blockIdx.x; + const int32_t ty = blockIdx.y; + const int32_t tid = threadIdx.x; + + using BlockReduce = cub::BlockReduce; + using BlockScan = cub::BlockScan; + + __shared__ unsigned int shHist[kHistBins]; + __shared__ typename BlockReduce::TempStorage shReduce; + __shared__ typename BlockScan::TempStorage shScan; + __shared__ unsigned int shExcess; + + shHist[tid] = 0; + __syncthreads(); + + int32_t width; + int32_t height; + if constexpr (IsBorderVarShapeWrap::value) + { + width = src.imageBatchWrap().width(batch, 0); + height = src.imageBatchWrap().height(batch, 0); + } + else + { + width = src.tensorShape()[1]; + height = src.tensorShape()[0]; + } + const int32_t padW = (tilesX - (width % tilesX)) % tilesX; + const int32_t padH = (tilesY - (height % tilesY)) % tilesY; + const int32_t extW = width + padW; + const int32_t extH = height + padH; + const int32_t tileW = extW / tilesX; + const int32_t tileH = extH / tilesY; + + const int32_t x0 = tx * tileW; + const int32_t y0 = ty * tileH; + const int32_t tileArea = tileW * tileH; + + for (int32_t idx = tid; idx < tileArea; idx += blockDim.x) + { + const int32_t localY = idx / tileW; + const int32_t localX = idx - localY * tileW; + const unsigned char v = *(src.ptr(batch, y0 + localY, x0 + localX)); + atomicAdd(&shHist[v], 1U); + } + __syncthreads(); + + const unsigned int clipCount = max((unsigned int)(clipLimit * (float)tileArea / (float)kHistBins), 1U); + unsigned int localExcess = 0; + if (shHist[tid] > clipCount) + { + localExcess = shHist[tid] - clipCount; + shHist[tid] = clipCount; + } + const unsigned int blockExcess = BlockReduce(shReduce).Sum(localExcess); + if (tid == 0) + { + shExcess = blockExcess; + } + __syncthreads(); + + const unsigned int redist = shExcess / (unsigned int)kHistBins; + const unsigned int rem = shExcess % (unsigned int)kHistBins; + shHist[tid] += redist; + if (rem > 0U) + { + const unsigned int residualStep = max((unsigned int)(kHistBins / rem), 1U); + if (((unsigned int)tid % residualStep) == 0U && ((unsigned int)tid / residualStep) < rem) + { + shHist[tid] += 1U; + } + } + __syncthreads(); + + unsigned int cdf = 0; + BlockScan(shScan).InclusiveSum(shHist[tid], cdf); + unsigned int lutV = (cdf * 255U + (unsigned int)(tileArea / 2)) / (unsigned int)max(tileArea, 1); + lutV = min(lutV, 255U); + const int32_t tileIdx = (batch * tilesY + ty) * tilesX + tx; + luts[tileIdx * kHistBins + tid] = (unsigned char)lutV; +} + +template +__global__ void CLAHEApplyKernel(SrcWrapper src, DstWrapper dst, const unsigned char *luts, int32_t tilesX, + int32_t tilesY) +{ + const int32_t batch = blockIdx.z; + int32_t width; + int32_t height; + if constexpr (IsImageBatchVarShapeWrap::value) + { + width = src.width(batch); + height = src.height(batch); + } + else + { + width = src.tensorShape()[1]; + height = src.tensorShape()[0]; + } + + const int32_t x = blockIdx.x * blockDim.x + threadIdx.x; + const int32_t y = blockIdx.y * blockDim.y + threadIdx.y; + if (x >= width || y >= height) + { + return; + } + + const int32_t padW = (tilesX - (width % tilesX)) % tilesX; + const int32_t padH = (tilesY - (height % tilesY)) % tilesY; + const int32_t extW = width + padW; + const int32_t extH = height + padH; + const int32_t tileW = extW / tilesX; + const int32_t tileH = extH / tilesY; + + float gx = (float)x / (float)tileW - 0.5f; + float gy = (float)y / (float)tileH - 0.5f; + gx = fminf(fmaxf(gx, 0.0f), (float)(tilesX - 1)); + gy = fminf(fmaxf(gy, 0.0f), (float)(tilesY - 1)); + + const int32_t tx0 = (int32_t)floorf(gx); + const int32_t ty0 = (int32_t)floorf(gy); + const float fx = gx - (float)tx0; + const float fy = gy - (float)ty0; + const int32_t tx1 = min(tx0 + 1, tilesX - 1); + const int32_t ty1 = min(ty0 + 1, tilesY - 1); + const int32_t v = *(src.ptr(batch, y, x)); + + const int32_t base = batch * tilesY * tilesX * kHistBins; + const int32_t idx00 = base + ((ty0 * tilesX + tx0) * kHistBins + v); + const int32_t idx10 = base + ((ty0 * tilesX + tx1) * kHistBins + v); + const int32_t idx01 = base + ((ty1 * tilesX + tx0) * kHistBins + v); + const int32_t idx11 = base + ((ty1 * tilesX + tx1) * kHistBins + v); + + const float w00 = (1.0f - fx) * (1.0f - fy); + const float w10 = fx * (1.0f - fy); + const float w01 = (1.0f - fx) * fy; + const float w11 = fx * fy; + + const float outV + = w00 * (float)luts[idx00] + w10 * (float)luts[idx10] + w01 * (float)luts[idx01] + w11 * (float)luts[idx11]; + *(dst.ptr(batch, y, x)) = (unsigned char)(outV + 0.5f); +} + +// Keep Tensor kernels separate so the original VarShape kernel definitions and code generation stay intact. +template +__global__ void CLAHETensorBuildLUTKernel(SrcWrapper src, unsigned char *luts, float clipLimit, int32_t tilesX, + int32_t tilesY, int32_t width, int32_t height) +{ + const int32_t batch = blockIdx.z; + const int32_t tx = blockIdx.x; + const int32_t ty = blockIdx.y; + const int32_t tid = threadIdx.x; + + using BlockReduce = cub::BlockReduce; + using BlockScan = cub::BlockScan; + + __shared__ unsigned int shHist[kHistBins]; + __shared__ typename BlockReduce::TempStorage shReduce; + __shared__ typename BlockScan::TempStorage shScan; + __shared__ unsigned int shExcess; + + shHist[tid] = 0; + __syncthreads(); + + const int32_t padW = (tilesX - (width % tilesX)) % tilesX; + const int32_t padH = (tilesY - (height % tilesY)) % tilesY; + const int32_t extW = width + padW; + const int32_t extH = height + padH; + const int32_t tileW = extW / tilesX; + const int32_t tileH = extH / tilesY; + + const int32_t x0 = tx * tileW; + const int32_t y0 = ty * tileH; + const int32_t tileArea = tileW * tileH; + + for (int32_t idx = tid; idx < tileArea; idx += blockDim.x) + { + const int32_t localY = idx / tileW; + const int32_t localX = idx - localY * tileW; + const unsigned char v = *(src.ptr(batch, y0 + localY, x0 + localX)); + atomicAdd(&shHist[v], 1U); + } + __syncthreads(); + + const unsigned int clipCount = max((unsigned int)(clipLimit * (float)tileArea / (float)kHistBins), 1U); + unsigned int localExcess = 0; + if (shHist[tid] > clipCount) + { + localExcess = shHist[tid] - clipCount; + shHist[tid] = clipCount; + } + const unsigned int blockExcess = BlockReduce(shReduce).Sum(localExcess); + if (tid == 0) + { + shExcess = blockExcess; + } + __syncthreads(); + + const unsigned int redist = shExcess / (unsigned int)kHistBins; + const unsigned int rem = shExcess % (unsigned int)kHistBins; + shHist[tid] += redist; + if (rem > 0U) + { + const unsigned int residualStep = max((unsigned int)(kHistBins / rem), 1U); + if (((unsigned int)tid % residualStep) == 0U && ((unsigned int)tid / residualStep) < rem) + { + shHist[tid] += 1U; + } + } + __syncthreads(); + + unsigned int cdf = 0; + BlockScan(shScan).InclusiveSum(shHist[tid], cdf); + unsigned int lutV = (cdf * 255U + (unsigned int)(tileArea / 2)) / (unsigned int)max(tileArea, 1); + lutV = min(lutV, 255U); + const int32_t tileIdx = (batch * tilesY + ty) * tilesX + tx; + luts[tileIdx * kHistBins + tid] = (unsigned char)lutV; +} + +template +struct IsTensorWrap : std::false_type +{ +}; + +template +struct IsTensorWrap> : std::true_type +{ +}; + +template +__global__ void CLAHETensorBuildLUTDirectKernel(SrcWrapper src, unsigned char *luts, float clipLimit, int32_t tilesX, + int32_t tilesY, int32_t width, int32_t height) +{ + static_assert(IsTensorWrap::value); + + const int32_t batch = blockIdx.z; + const int32_t tx = blockIdx.x; + const int32_t ty = blockIdx.y; + const int32_t tid = threadIdx.x; + + using BlockReduce = cub::BlockReduce; + using BlockScan = cub::BlockScan; + + __shared__ unsigned int shHist[kHistBins]; + __shared__ typename BlockReduce::TempStorage shReduce; + __shared__ typename BlockScan::TempStorage shScan; + __shared__ unsigned int shExcess; + + shHist[tid] = 0; + __syncthreads(); + + const int32_t padW = (tilesX - (width % tilesX)) % tilesX; + const int32_t padH = (tilesY - (height % tilesY)) % tilesY; + const int32_t extW = width + padW; + const int32_t extH = height + padH; + const int32_t tileW = extW / tilesX; + const int32_t tileH = extH / tilesY; + + const int32_t x0 = tx * tileW; + const int32_t y0 = ty * tileH; + const int32_t tileArea = tileW * tileH; + + // Keep the 256-thread CUB block while mapping each warp onto one tile row for coalesced reads. + constexpr int32_t kWarpWidth = 32; + constexpr int32_t kWarpCount = kHistBins / kWarpWidth; + const int32_t virtualX = tid % kWarpWidth; + const int32_t virtualY = tid / kWarpWidth; + + if (virtualX < tileW && virtualY < tileH) + { + const int64_t rowStride = src.strides()[1]; + const unsigned char *row = src.ptr(batch, y0 + virtualY, x0); + for (int32_t localY = virtualY; localY < tileH; localY += kWarpCount) + { + const int32_t scalarPrefix = min((int32_t)((-reinterpret_cast(row)) & 3U), tileW); + const int32_t vectorEnd = scalarPrefix + ((tileW - scalarPrefix) & ~3); + + if (virtualX < scalarPrefix) + { + atomicAdd(&shHist[row[virtualX]], 1U); + } + // Keep one packed word live per lane; NVCC's default unrolling increases register pressure. +#pragma unroll 1 + for (int32_t localX = scalarPrefix + 4 * virtualX; localX < vectorEnd; localX += 4 * kWarpWidth) + { + uint32_t pixels = *reinterpret_cast(row + localX); + atomicAdd(&shHist[pixels & 0xFFU], 1U); + pixels >>= 8; + atomicAdd(&shHist[pixels & 0xFFU], 1U); + pixels >>= 8; + atomicAdd(&shHist[pixels & 0xFFU], 1U); + pixels >>= 8; + atomicAdd(&shHist[pixels], 1U); + } + if (virtualX < tileW - vectorEnd) + { + atomicAdd(&shHist[row[vectorEnd + virtualX]], 1U); + } + if (localY + kWarpCount < tileH) + { + row += kWarpCount * rowStride; + } + } + } + __syncthreads(); + + const unsigned int clipCount = max((unsigned int)(clipLimit * (float)tileArea / (float)kHistBins), 1U); + unsigned int localExcess = 0; + if (shHist[tid] > clipCount) + { + localExcess = shHist[tid] - clipCount; + shHist[tid] = clipCount; + } + const unsigned int blockExcess = BlockReduce(shReduce).Sum(localExcess); + if (tid == 0) + { + shExcess = blockExcess; + } + __syncthreads(); + + const unsigned int redist = shExcess / (unsigned int)kHistBins; + const unsigned int rem = shExcess % (unsigned int)kHistBins; + shHist[tid] += redist; + if (rem > 0U) + { + const unsigned int residualStep = max((unsigned int)(kHistBins / rem), 1U); + if (((unsigned int)tid % residualStep) == 0U && ((unsigned int)tid / residualStep) < rem) + { + shHist[tid] += 1U; + } + } + __syncthreads(); + + unsigned int cdf = 0; + BlockScan(shScan).InclusiveSum(shHist[tid], cdf); + unsigned int lutV = (cdf * 255U + (unsigned int)(tileArea / 2)) / (unsigned int)max(tileArea, 1); + lutV = min(lutV, 255U); + const int32_t tileIdx = (batch * tilesY + ty) * tilesX + tx; + luts[tileIdx * kHistBins + tid] = (unsigned char)lutV; +} + +template +__global__ void CLAHETensorApplyKernel(SrcWrapper src, DstWrapper dst, const unsigned char *luts, int32_t tilesX, + int32_t tilesY, int32_t width, int32_t height, float invTileW, float invTileH) +{ + const int32_t batch = blockIdx.z; + const int32_t x = blockIdx.x * blockDim.x + threadIdx.x; + const int32_t y = blockIdx.y * blockDim.y + threadIdx.y; + if (x >= width || y >= height) + { + return; + } + + float gx = __fmul_rn((float)x, invTileW) - 0.5f; + float gy = __fmul_rn((float)y, invTileH) - 0.5f; + gx = fminf(fmaxf(gx, 0.0f), (float)(tilesX - 1)); + gy = fminf(fmaxf(gy, 0.0f), (float)(tilesY - 1)); + + const int32_t tx0 = (int32_t)floorf(gx); + const int32_t ty0 = (int32_t)floorf(gy); + const float fx = gx - (float)tx0; + const float fy = gy - (float)ty0; + const int32_t tx1 = min(tx0 + 1, tilesX - 1); + const int32_t ty1 = min(ty0 + 1, tilesY - 1); + const int32_t v = *(src.ptr(batch, y, x)); + + const int32_t base = batch * tilesY * tilesX * kHistBins; + const int32_t idx00 = base + ((ty0 * tilesX + tx0) * kHistBins + v); + const int32_t idx10 = base + ((ty0 * tilesX + tx1) * kHistBins + v); + const int32_t idx01 = base + ((ty1 * tilesX + tx0) * kHistBins + v); + const int32_t idx11 = base + ((ty1 * tilesX + tx1) * kHistBins + v); + + const float w00 = (1.0f - fx) * (1.0f - fy); + const float w10 = fx * (1.0f - fy); + const float w01 = (1.0f - fx) * fy; + const float w11 = fx * fy; + + const float outV + = w00 * (float)luts[idx00] + w10 * (float)luts[idx10] + w01 * (float)luts[idx01] + w11 * (float)luts[idx11]; + *(dst.ptr(batch, y, x)) = (unsigned char)(outV + 0.5f); +} + +static void ValidateTileGrid(int32_t tilesX, int32_t tilesY) +{ + if (tilesX < 1 || tilesY < 1) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "tilesX and tilesY must be >= 1"); + } +} + +static void ValidateClipLimit(float clipLimit) +{ + if (!(clipLimit > 0.0)) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "clipLimit must be > 0"); + } +} + +static void ValidateGridZ(int64_t z, const char *what) +{ + if (z > 65535) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "%s must be <= 65535", what); + } +} + +static void RunCLAHETensor(const nvcv::TensorDataStridedCuda &inData, const nvcv::TensorDataStridedCuda &outData, + float clipLimit, int32_t tilesX, int32_t tilesY, unsigned char *dLUTs, cudaStream_t stream) +{ + auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(inData); + auto outAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(outData); + if (!inAccess || !outAccess) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input and output must be pitch-linear image tensors"); + } + + if (inData.layout() != outData.layout() || inData.dtype() != outData.dtype() + || inAccess->numSamples() != outAccess->numSamples() || inAccess->numRows() != outAccess->numRows() + || inAccess->numCols() != outAccess->numCols() || inAccess->numChannels() != outAccess->numChannels()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input and output tensor must match"); + } + + const bool isPlanar = inData.layout() == nvcv::TENSOR_NCHW || inData.layout() == nvcv::TENSOR_CHW; + const bool isInterleaved = inData.layout() == nvcv::TENSOR_NHWC || inData.layout() == nvcv::TENSOR_HWC; + if (!isInterleaved && !isPlanar) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input and output must have (N)HWC or (N)CHW layout"); + } + + if (inAccess->numChannels() != 1) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "CLAHE supports only single-channel tensors"); + } + + if (inData.dtype() != nvcv::TYPE_U8) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "CLAHE supports only U8 tensors"); + } + + auto src = nvcv::cuda::CreateTensorWrapNHW(inData); + auto srcBorder = nvcv::cuda::CreateBorderWrapNHW(inData); + auto dst = nvcv::cuda::CreateTensorWrapNHW(outData); + + const int32_t batch = inAccess->numSamples(); + const int32_t width = inAccess->numCols(); + const int32_t height = inAccess->numRows(); + ValidateGridZ(batch, "Tensor batch"); + + const int32_t padW = (tilesX - (width % tilesX)) % tilesX; + const int32_t padH = (tilesY - (height % tilesY)) % tilesY; + const int32_t tileW = (width + padW) / tilesX; + const int32_t tileH = (height + padH) / tilesY; + const float invTileW = 1.0f / (float)tileW; + const float invTileH = 1.0f / (float)tileH; + + const dim3 lutGrid(tilesX, tilesY, batch); + const dim3 lutBlock(kHistBins, 1, 1); + if (width % tilesX == 0 && height % tilesY == 0) + { + CLAHETensorBuildLUTDirectKernel<<>>(src, dLUTs, clipLimit, tilesX, tilesY, width, + height); + } + else + { + CLAHETensorBuildLUTKernel<<>>(srcBorder, dLUTs, clipLimit, tilesX, tilesY, width, + height); + } + checkKernelErrors(); + + const dim3 applyBlock(32, 8, 1); + const dim3 applyGrid((width + applyBlock.x - 1) / applyBlock.x, (height + applyBlock.y - 1) / applyBlock.y, batch); + CLAHETensorApplyKernel<<>>(src, dst, dLUTs, tilesX, tilesY, width, height, + invTileW, invTileH); + checkKernelErrors(); +} + +static void ValidateVarShapeFormat(const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out) +{ + if (in.numImages() != out.numImages()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input and output must have same number of images"); + } + + for (int i = 0; i < in.numImages(); ++i) + { + if (in[i].size() != out[i].size() || in[i].format() != out[i].format()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input and output varshape images must match in size and format"); + } + + const auto fmt = in[i].format(); + if (fmt.planeNumChannels(0) != 1 || fmt.planeDataType(0) != nvcv::TYPE_U8) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "CLAHE varshape supports only single-channel U8"); + } + } +} + +static void RunCLAHEVarShape(const nvcv::ImageBatchVarShapeDataStridedCuda &inData, + const nvcv::ImageBatchVarShapeDataStridedCuda &outData, float clipLimit, int32_t tilesX, + int32_t tilesY, unsigned char *dLUTs, cudaStream_t stream) +{ + ValidateGridZ(inData.numImages(), "Varshape batch"); + + nvcv::cuda::BorderVarShapeWrap srcBorder(inData); + nvcv::cuda::ImageBatchVarShapeWrap src(inData); + nvcv::cuda::ImageBatchVarShapeWrap dst(outData); + + const dim3 lutGrid(tilesX, tilesY, inData.numImages()); + const dim3 lutBlock(kHistBins, 1, 1); + CLAHEBuildLUTKernel<<>>(srcBorder, dLUTs, clipLimit, tilesX, tilesY); + checkKernelErrors(); + + const dim3 applyBlock(16, 16, 1); + const dim3 applyGrid((inData.maxSize().w + applyBlock.x - 1) / applyBlock.x, + (inData.maxSize().h + applyBlock.y - 1) / applyBlock.y, inData.numImages()); + CLAHEApplyKernel<<>>(src, dst, dLUTs, tilesX, tilesY); + checkKernelErrors(); +} + +} // namespace + +CLAHE::CLAHE(int32_t maxBatchSize, int32_t tilesX, int32_t tilesY) + : m_maxBatchSize(maxBatchSize) + , m_tilesX(tilesX) + , m_tilesY(tilesY) + , m_deviceBuffers([maxBatchSize, tilesX, tilesY](int) + { return std::make_unique(maxBatchSize, tilesX, tilesY); }) +{ + if (m_maxBatchSize < 1) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "maxBatchSize must be >= 1"); + } + ValidateTileGrid(m_tilesX, m_tilesY); +} + +void CLAHE::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, float clipLimit) const +{ + CVCUDA_NVTX_RANGE("cvcuda::CLAHE::operator()[Tensor]"); + ValidateClipLimit(clipLimit); + + auto inData = in.exportData(); + if (!inData) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input must be cuda-accessible, pitch-linear tensor"); + } + auto outData = out.exportData(); + if (!outData) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Output must be cuda-accessible, pitch-linear tensor"); + } + + auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*inData); + if (!inAccess) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input must be image-compatible tensor"); + } + if (inAccess->numSamples() > m_maxBatchSize) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input batch exceeds maxBatchSize"); + } + + RunCLAHETensor(*inData, *outData, clipLimit, m_tilesX, m_tilesY, m_deviceBuffers.get().luts, stream); +} + +void CLAHE::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, + float clipLimit) const +{ + CVCUDA_NVTX_RANGE("cvcuda::CLAHE::operator()[ImageBatchVarShape]"); + ValidateClipLimit(clipLimit); + + if (in.numImages() > m_maxBatchSize) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input batch exceeds maxBatchSize"); + } + + ValidateVarShapeFormat(in, out); + + auto inData = in.exportData(stream); + if (!inData) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input must be varshape image batch"); + } + auto outData = out.exportData(stream); + if (!outData) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Output must be varshape image batch"); + } + + RunCLAHEVarShape(*inData, *outData, clipLimit, m_tilesX, m_tilesY, m_deviceBuffers.get().luts, stream); +} + +} // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpCLAHE.hpp b/src/cvcuda/priv/OpCLAHE.hpp new file mode 100644 index 000000000..aaa85d07c --- /dev/null +++ b/src/cvcuda/priv/OpCLAHE.hpp @@ -0,0 +1,83 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +/** + * @file OpCLAHE.hpp + * + * @brief Defines the private C++ Class for CLAHE operation. + */ + +#ifndef CVCUDA_PRIV__CLAHE_HPP +#define CVCUDA_PRIV__CLAHE_HPP + +#include "IOperator.hpp" +#include "PerDeviceResource.hpp" + +#include +#include +#include +#include + +namespace cvcuda::priv { + +// Per-device GPU buffers for CLAHE. One instance is created per CUDA device +// via PerDeviceResource, ensuring each GPU has its own LUT allocation. +struct CLAHEDeviceBuffers +{ + unsigned char *luts = nullptr; + + CLAHEDeviceBuffers(int32_t maxBatchSize, int32_t tilesX, int32_t tilesY) + { + const size_t lutSize = static_cast(maxBatchSize) * static_cast(tilesX) + * static_cast(tilesY) * 256 * sizeof(unsigned char); + NVCV_CHECK_THROW(cudaMalloc(&luts, lutSize)); + } + + ~CLAHEDeviceBuffers() + { + if (luts != nullptr) + { + NVCV_CHECK_LOG(cudaFree(luts)); + } + } + + CLAHEDeviceBuffers(const CLAHEDeviceBuffers &) = delete; + CLAHEDeviceBuffers &operator=(const CLAHEDeviceBuffers &) = delete; + CLAHEDeviceBuffers(CLAHEDeviceBuffers &&) = delete; + CLAHEDeviceBuffers &operator=(CLAHEDeviceBuffers &&) = delete; +}; + +class CLAHE final : public IOperator +{ +public: + CLAHE(int32_t maxBatchSize, int32_t tilesX, int32_t tilesY); + + void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, float clipLimit) const; + + void operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, + float clipLimit) const; + +private: + int32_t m_maxBatchSize; + int32_t m_tilesX; + int32_t m_tilesY; + mutable PerDeviceResource m_deviceBuffers; +}; + +} // namespace cvcuda::priv + +#endif // CVCUDA_PRIV__CLAHE_HPP diff --git a/src/cvcuda/priv/OpCenterCrop.cpp b/src/cvcuda/priv/OpCenterCrop.cpp index f35c36a5e..8db7ca0f3 100644 --- a/src/cvcuda/priv/OpCenterCrop.cpp +++ b/src/cvcuda/priv/OpCenterCrop.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,19 +17,102 @@ #include "OpCenterCrop.hpp" +#include "Nvtx.hpp" +#include "PlanarTensorView.hpp" #include "legacy/CvCudaLegacy.h" #include "legacy/CvCudaLegacyHelpers.hpp" +#include #include +#include #include +#include + namespace cvcuda::priv { namespace legacy = nvcv::legacy::cuda_op; +namespace { + +constexpr int kBitsPerByte = 8; + +bool TryCopyDenseByteNHWCCenterCrop(cudaStream_t stream, const nvcv::TensorDataStridedCuda &inData, + const nvcv::TensorDataStridedCuda &outData, const nvcv::Size2D &cropSize) +{ + if (inData.rank() != 4 || outData.rank() != 4 || inData.layout() != nvcv::TENSOR_NHWC + || outData.layout() != nvcv::TENSOR_NHWC || inData.dtype() != outData.dtype()) + { + return false; + } + + const auto bitsPerChannel = inData.dtype().bitsPerChannel(); + // Reference CI showed that cudaMemcpy3DAsync regresses the A100 float32 rows by 22% and the + // H100 byte-wide three-channel rows by 8%, while local SM86 benchmarking showed the byte-wide + // four-channel row regressing by 8%. Keep every interleaved non-scalar case on the legacy + // kernel. Native-planar U8 is flattened to scalar planes before reaching this check. + if (bitsPerChannel[0] != kBitsPerByte) + { + return false; + } + const int64_t bytesPerChannel = bitsPerChannel[0] / kBitsPerByte; + + const int64_t inBatch = inData.shape(0); + const int64_t inRows = inData.shape(1); + const int64_t inCols = inData.shape(2); + const int64_t inCh = inData.shape(3); + + if (inCh != 1) + { + return false; + } + + const int64_t outBatch = outData.shape(0); + const int64_t outRows = outData.shape(1); + const int64_t outCols = outData.shape(2); + + if (const int64_t outCh = outData.shape(3); cropSize.h <= 0 || cropSize.w <= 0 || inBatch != outBatch + || inCh != outCh || inCh < 1 || outRows != cropSize.h + || outCols != cropSize.w || cropSize.h > inRows || cropSize.w > inCols) + { + return false; + } + + const int64_t pixelBytes = inCh * bytesPerChannel; + if (inData.stride(3) != bytesPerChannel || outData.stride(3) != bytesPerChannel || inData.stride(2) != pixelBytes + || outData.stride(2) != pixelBytes || inData.stride(1) < inCols * pixelBytes + || outData.stride(1) < outCols * pixelBytes || inData.stride(0) != inData.stride(1) * inRows + || outData.stride(0) != outData.stride(1) * outRows) + { + return false; + } + + const int64_t top = (inRows - cropSize.h) / 2; + const int64_t left = (inCols - cropSize.w) / 2; + + cudaMemcpy3DParms params{}; + params.srcPtr + = make_cudaPitchedPtr(inData.basePtr(), static_cast(inData.stride(1)), + static_cast(inCols * pixelBytes), static_cast(inRows)); + params.dstPtr + = make_cudaPitchedPtr(outData.basePtr(), static_cast(outData.stride(1)), + static_cast(outCols * pixelBytes), static_cast(outRows)); + params.srcPos = make_cudaPos(static_cast(left * pixelBytes), static_cast(top), 0); + params.dstPos = make_cudaPos(0, 0, 0); + params.extent = make_cudaExtent(static_cast(cropSize.w * pixelBytes), + static_cast(cropSize.h), static_cast(inBatch)); + params.kind = cudaMemcpyDeviceToDevice; + + NVCV_CHECK_THROW(cudaMemcpy3DAsync(¶ms, stream)); + return true; +} + +} // namespace + CenterCrop::CenterCrop() { - legacy::DataShape maxIn, maxOut; + legacy::DataShape maxIn; + legacy::DataShape maxOut; //maxIn/maxOut not used by op. m_legacyOp = std::make_unique(maxIn, maxOut); } @@ -37,6 +120,7 @@ CenterCrop::CenterCrop() void CenterCrop::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, const nvcv::Size2D &cropSize) const { + CVCUDA_NVTX_RANGE("cvcuda::CenterCrop::operator()[Tensor]"); auto inData = in.exportData(); if (inData == nullptr) { @@ -51,6 +135,26 @@ void CenterCrop::operator()(cudaStream_t stream, const nvcv::Tensor &in, const n "Output must be cuda-accessible, pitch-linear tensor"); } + if (TryCopyDenseByteNHWCCenterCrop(stream, *inData, *outData, cropSize)) + { + return; + } + + // Center-cropping copies each channel plane independently and identically, so a planar (NCHW/CHW) + // image is just N*C single-channel planes: flatten them into the sample dimension and reuse the + // dense-copy path or legacy interleaved single-channel kernel unchanged, producing bit-exact + // planar output. + if (auto planarViews = PlanarSingleChannelViews(*inData, *outData)) + { + if (TryCopyDenseByteNHWCCenterCrop(stream, planarViews->first, planarViews->second, cropSize)) + { + return; + } + + NVCV_CHECK_THROW(m_legacyOp->infer(planarViews->first, planarViews->second, cropSize.h, cropSize.w, stream)); + return; + } + NVCV_CHECK_THROW(m_legacyOp->infer(*inData, *outData, cropSize.h, cropSize.w, stream)); } diff --git a/src/cvcuda/priv/OpChannelReorder.cpp b/src/cvcuda/priv/OpChannelReorder.cpp deleted file mode 100644 index b689e83cf..000000000 --- a/src/cvcuda/priv/OpChannelReorder.cpp +++ /dev/null @@ -1,63 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed 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. - */ - -#include "OpChannelReorder.hpp" - -#include "legacy/CvCudaLegacy.h" -#include "legacy/CvCudaLegacyHelpers.hpp" - -#include -#include - -namespace cvcuda::priv { - -namespace legacy = nvcv::legacy::cuda_op; - -ChannelReorder::ChannelReorder() -{ - legacy::DataShape maxIn, maxOut; //maxIn/maxOut not used by op. - m_legacyOpVarShape = std::make_unique(maxIn, maxOut); -} - -void ChannelReorder::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, - const nvcv::ImageBatchVarShape &out, const nvcv::Tensor &orders) const -{ - auto inData = in.exportData(stream); - if (inData == nullptr) - { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, - "Input must be cuda-accessible, varshape pitch-linear image batch"); - } - - auto outData = out.exportData(stream); - if (outData == nullptr) - { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, - "Output must be cuda-accessible, varshape pitch-linear image batch"); - } - - auto ordersData = orders.exportData(); - if (!ordersData) - { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, - "Input channel order tensor must be cuda-accessible, pitch-linear tensor"); - } - - NVCV_CHECK_THROW(m_legacyOpVarShape->infer(*inData, *outData, *ordersData, stream)); -} - -} // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpChannelReorder.cu b/src/cvcuda/priv/OpChannelReorder.cu new file mode 100644 index 000000000..9f0e1d4a6 --- /dev/null +++ b/src/cvcuda/priv/OpChannelReorder.cu @@ -0,0 +1,279 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "Nvtx.hpp" +#include "OpChannelReorder.hpp" +#include "legacy/CvCudaLegacy.h" +#include "legacy/CvCudaLegacyHelpers.hpp" + +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace { + +struct TensorByteView +{ + unsigned char *base; + int64_t sampleStride; + int64_t rowStride; + int64_t colStride; + int64_t channelStride; +}; + +inline __device__ int GetOrder(int4 order, int channel) +{ + switch (channel) + { + case 0: + return order.x; + case 1: + return order.y; + case 2: + return order.z; + default: + return order.w; + } +} + +template +__global__ void ChannelReorderTensorKernel(TensorByteView src, TensorByteView dst, int64_t totalPixels, int rows, + int cols, int channels, int4 order) +{ + const int64_t thread = static_cast(blockIdx.x) * blockDim.x + threadIdx.x; + const int64_t step = static_cast(blockDim.x) * gridDim.x; + for (int64_t pixel = thread; pixel < totalPixels; pixel += step) + { + const int x = static_cast(pixel % cols); + const int64_t q = pixel / cols; + const int y = static_cast(q % rows); + const int64_t n = q / rows; + + const int64_t srcPixelOffset = n * src.sampleStride + y * src.rowStride + x * src.colStride; + const int64_t dstPixelOffset = n * dst.sampleStride + y * dst.rowStride + x * dst.colStride; + +#pragma unroll + for (int c = 0; c < 4; ++c) + { + if (c >= channels) + { + break; + } + const int srcChannel = GetOrder(order, c); + T *dstValue + = reinterpret_cast(dst.base + dstPixelOffset + static_cast(c) * dst.channelStride); + if (srcChannel < 0) + { + *dstValue = T{}; + } + else + { + const T *srcValue = reinterpret_cast(src.base + srcPixelOffset + + static_cast(srcChannel) * src.channelStride); + *dstValue = *srcValue; + } + } + } +} + +inline int4 PackOrder(const int32_t *order, int32_t orderLength, int channels) +{ + if (order == nullptr) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Order pointer must not be NULL"); + } + if (orderLength != channels) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Order length must match the input channel count"); + } + + int values[4]{-1, -1, -1, -1}; + for (int i = 0; i < channels; ++i) + { + if (order[i] >= channels) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Non-negative order entries must be smaller than the channel count"); + } + values[i] = order[i]; + } + return {values[0], values[1], values[2], values[3]}; +} + +inline int ElementSize(nvcv::DataType dtype) +{ + if (dtype == nvcv::TYPE_U8) + { + return 1; + } + if (dtype == nvcv::TYPE_U16 || dtype == nvcv::TYPE_S16) + { + return 2; + } + if (dtype == nvcv::TYPE_S32 || dtype == nvcv::TYPE_F32) + { + return 4; + } + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "ChannelReorder supports U8, U16, S16, S32, and F32 tensors"); +} + +template +inline void LaunchChannelReorderTensor(cudaStream_t stream, const nvcv::TensorDataStridedCuda &srcData, + const nvcv::TensorDataStridedCuda &dstData, + const nvcv::TensorDataAccessStridedImagePlanar &srcAccess, + const nvcv::TensorDataAccessStridedImagePlanar &dstAccess, int channels, + int4 order) +{ + const int64_t samples = srcAccess.numSamples(); + const int64_t rows = srcAccess.numRows(); + const int64_t cols = srcAccess.numCols(); + if (samples == 0 || rows == 0 || cols == 0) + { + return; + } + if (samples > std::numeric_limits::max() / rows + || samples * rows > std::numeric_limits::max() / cols) + { + throw nvcv::Exception(nvcv::Status::ERROR_OVERFLOW, "Tensor element count overflows int64"); + } + const int64_t totalPixels = samples * rows * cols; + constexpr int threads = 256; + const int64_t neededBlocks = (totalPixels + threads - 1) / threads; + const int blocks = static_cast(std::min(neededBlocks, 65535)); + + TensorByteView src{reinterpret_cast(srcData.basePtr()), srcAccess.sampleStride(), + srcAccess.rowStride(), srcAccess.colStride(), srcAccess.chStride()}; + TensorByteView dst{reinterpret_cast(dstData.basePtr()), dstAccess.sampleStride(), + dstAccess.rowStride(), dstAccess.colStride(), dstAccess.chStride()}; + ChannelReorderTensorKernel<<>>(src, dst, totalPixels, static_cast(rows), + static_cast(cols), channels, order); + NVCV_CHECK_THROW(cudaGetLastError()); +} + +} // namespace + +namespace cvcuda::priv { + +namespace legacy = nvcv::legacy::cuda_op; + +ChannelReorder::ChannelReorder() +{ + legacy::DataShape maxIn; + legacy::DataShape maxOut; //maxIn/maxOut not used by op. + m_legacyOpVarShape = std::make_unique(maxIn, maxOut); +} + +void ChannelReorder::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, + const int32_t *order, int32_t orderLength) const +{ + CVCUDA_NVTX_RANGE("cvcuda::ChannelReorder::operator()[Tensor]"); + auto inData = in.exportData(); + auto outData = out.exportData(); + if (!inData || !outData) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input and output must be CUDA-accessible pitch-linear tensors"); + } + if (inData->layout() != outData->layout() + || !(inData->layout() == nvcv::TENSOR_HWC || inData->layout() == nvcv::TENSOR_NHWC + || inData->layout() == nvcv::TENSOR_CHW || inData->layout() == nvcv::TENSOR_NCHW)) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input and output must have the same (N)HWC or (N)CHW layout"); + } + if (inData->rank() != outData->rank()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input and output ranks must match"); + } + for (int i = 0; i < inData->rank(); ++i) + { + if (inData->shape(i) != outData->shape(i)) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input and output shapes must match"); + } + } + if (inData->dtype() != outData->dtype() || inData->dtype().numChannels() != 1) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input and output must have the same scalar data type"); + } + + auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*inData); + auto outAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*outData); + if (!inAccess || !outAccess) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input and output must support strided planar image access"); + } + const int channels = inAccess->numChannels(); + if (channels < 1 || channels > 4 || (inAccess->numPlanes() > 1 && channels == 2)) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "ChannelReorder supports 1-4 interleaved channels and 1, 3, or 4 planar channels"); + } + const int4 packedOrder = PackOrder(order, orderLength, channels); + const int elementSize = ElementSize(inData->dtype()); + if (elementSize == 1) + { + LaunchChannelReorderTensor(stream, *inData, *outData, *inAccess, *outAccess, channels, packedOrder); + } + else if (elementSize == 2) + { + LaunchChannelReorderTensor(stream, *inData, *outData, *inAccess, *outAccess, channels, packedOrder); + } + else + { + LaunchChannelReorderTensor(stream, *inData, *outData, *inAccess, *outAccess, channels, packedOrder); + } +} + +void ChannelReorder::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, + const nvcv::ImageBatchVarShape &out, const nvcv::Tensor &orders) const +{ + CVCUDA_NVTX_RANGE("cvcuda::ChannelReorder::operator()[ImageBatchVarShape]"); + auto inData = in.exportData(stream); + if (inData == nullptr) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input must be cuda-accessible, varshape pitch-linear image batch"); + } + + auto outData = out.exportData(stream); + if (outData == nullptr) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Output must be cuda-accessible, varshape pitch-linear image batch"); + } + + auto ordersData = orders.exportData(); + if (!ordersData) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input channel order tensor must be cuda-accessible, pitch-linear tensor"); + } + + NVCV_CHECK_THROW(m_legacyOpVarShape->infer(*inData, *outData, *ordersData, stream)); +} + +} // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpChannelReorder.hpp b/src/cvcuda/priv/OpChannelReorder.hpp index b42fd1910..9fd58b78d 100644 --- a/src/cvcuda/priv/OpChannelReorder.hpp +++ b/src/cvcuda/priv/OpChannelReorder.hpp @@ -30,6 +30,7 @@ #include #include +#include #include namespace cvcuda::priv { @@ -39,6 +40,9 @@ class ChannelReorder final : public IOperator public: explicit ChannelReorder(); + void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, const int32_t *order, + int32_t orderLength) const; + void operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, const nvcv::Tensor &orders) const; diff --git a/src/cvcuda/priv/OpColorTwist.cu b/src/cvcuda/priv/OpColorTwist.cu index 33acbe5dd..4fe86cb1a 100644 --- a/src/cvcuda/priv/OpColorTwist.cu +++ b/src/cvcuda/priv/OpColorTwist.cu @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,6 +15,7 @@ * limitations under the License. */ +#include "Nvtx.hpp" #include "OpColorTwist.hpp" #include @@ -34,6 +35,9 @@ #include #include +#include +#include + namespace cuda = nvcv::cuda; namespace util = nvcv::util; @@ -44,6 +48,19 @@ using Vec = cuda::math::Vector; template using Mat = cuda::math::Matrix; +static bool IsPlanar(nvcv::TensorLayout layout) +{ + return layout == nvcv::TENSOR_NCHW || layout == nvcv::TENSOR_CHW; +} + +constexpr int kPlanarU8NIX = 4; +constexpr uintptr_t kPlanarU8Mask = sizeof(uchar4) - 1; + +inline __device__ bool IsAlignedForPlanarU8x4(const void *ptr) +{ + return (reinterpret_cast(ptr) & kPlanarU8Mask) == 0; +} + // Load explicit affine transform matrix from a tensor template inline auto __device__ GetAffineTransform(const TwistWrap &twist) @@ -120,6 +137,144 @@ inline void __device__ DoAffineTransform(SrcWrapper src, DstWrapper dst, const i dst[coord] = out_pixel; } +template +inline void __device__ DoAffineTransformPlanar(SrcWrapper src, DstWrapper dst, const int2 size, int numChannels, + const Mat transform) +{ + using SrcT = typename SrcWrapper::ValueType; + using DstT = typename DstWrapper::ValueType; + using T = std::remove_const_t; + static_assert(std::is_same_v>); + static_assert(N == 3); + + int3 coord = cuda::StaticCast(blockIdx * blockDim + threadIdx); + if (coord.x >= size.x || coord.y >= size.y) + { + return; + } + + Vec in_vec; +#pragma unroll + for (int i = 0; i < N; i++) + { + in_vec[i] = *src.ptr(coord.z, i, coord.y, coord.x); + } + in_vec[N] = 1; + Vec out_vec = transform * in_vec; + +#pragma unroll + for (int i = 0; i < N; i++) + { + *dst.ptr(coord.z, i, coord.y, coord.x) = cuda::SaturateCast(out_vec[i]); + } + if (numChannels == 4) + { + *dst.ptr(coord.z, 3, coord.y, coord.x) = *src.ptr(coord.z, 3, coord.y, coord.x); + } +} + +template +inline void __device__ ApplyColorTwistPlanarU8(uint8_t r, uint8_t g, uint8_t b, const Mat transform, + uint8_t &outR, uint8_t &outG, uint8_t &outB) +{ + Vec inVec; + inVec[0] = r; + inVec[1] = g; + inVec[2] = b; + inVec[3] = 1; + Vec outVec = transform * inVec; + outR = cuda::SaturateCast(outVec[0]); + outG = cuda::SaturateCast(outVec[1]); + outB = cuda::SaturateCast(outVec[2]); +} + +template +inline void __device__ DoAffineTransformPlanarU8x4(SrcWrapper src, DstWrapper dst, const int2 size, int numChannels, + const Mat transform) +{ + using SrcT = std::remove_const_t; + using DstT = typename DstWrapper::ValueType; + static_assert(std::is_same_v); + static_assert(std::is_same_v); + + int3 coord = cuda::StaticCast(blockIdx * blockDim + threadIdx); + coord.x *= kPlanarU8NIX; + if (coord.x >= size.x || coord.y >= size.y) + { + return; + } + + const uint8_t *srcR = src.ptr(coord.z, 0, coord.y, coord.x); + const uint8_t *srcG = src.ptr(coord.z, 1, coord.y, coord.x); + const uint8_t *srcB = src.ptr(coord.z, 2, coord.y, coord.x); + uint8_t *dstR = dst.ptr(coord.z, 0, coord.y, coord.x); + uint8_t *dstG = dst.ptr(coord.z, 1, coord.y, coord.x); + uint8_t *dstB = dst.ptr(coord.z, 2, coord.y, coord.x); + + bool canVectorize = coord.x + kPlanarU8NIX - 1 < size.x && IsAlignedForPlanarU8x4(srcR) + && IsAlignedForPlanarU8x4(srcG) && IsAlignedForPlanarU8x4(srcB) && IsAlignedForPlanarU8x4(dstR) + && IsAlignedForPlanarU8x4(dstG) && IsAlignedForPlanarU8x4(dstB); + + if (numChannels == 4) + { + canVectorize = canVectorize && IsAlignedForPlanarU8x4(src.ptr(coord.z, 3, coord.y, coord.x)) + && IsAlignedForPlanarU8x4(dst.ptr(coord.z, 3, coord.y, coord.x)); + } + + if (canVectorize) + { + const uchar4 rIn = *reinterpret_cast(srcR); + const uchar4 gIn = *reinterpret_cast(srcG); + const uchar4 bIn = *reinterpret_cast(srcB); + uchar4 rOut{}; + uchar4 gOut{}; + uchar4 bOut{}; +#pragma unroll + for (int i = 0; i < kPlanarU8NIX; i++) + { + uint8_t r; + uint8_t g; + uint8_t b; + ApplyColorTwistPlanarU8(cuda::GetElement(rIn, i), cuda::GetElement(gIn, i), cuda::GetElement(bIn, i), + transform, r, g, b); + cuda::GetElement(rOut, i) = r; + cuda::GetElement(gOut, i) = g; + cuda::GetElement(bOut, i) = b; + } + *reinterpret_cast(dstR) = rOut; + *reinterpret_cast(dstG) = gOut; + *reinterpret_cast(dstB) = bOut; + if (numChannels == 4) + { + *reinterpret_cast(dst.ptr(coord.z, 3, coord.y, coord.x)) + = *reinterpret_cast(src.ptr(coord.z, 3, coord.y, coord.x)); + } + } + else + { +#pragma unroll + for (int i = 0; i < kPlanarU8NIX; i++) + { + const int x = coord.x + i; + if (x < size.x) + { + uint8_t r; + uint8_t g; + uint8_t b; + ApplyColorTwistPlanarU8(*src.ptr(coord.z, 0, coord.y, x), *src.ptr(coord.z, 1, coord.y, x), + *src.ptr(coord.z, 2, coord.y, x), transform, r, g, b); + *dst.ptr(coord.z, 0, coord.y, x) = r; + *dst.ptr(coord.z, 1, coord.y, x) = g; + *dst.ptr(coord.z, 2, coord.y, x) = b; + if (numChannels == 4) + { + *dst.ptr(coord.z, 3, coord.y, x) = *src.ptr(coord.z, 3, coord.y, x); + } + } + } + } +} + // Load affine transform ---------------------------------------------------------- template @@ -140,6 +295,34 @@ inline __device__ void DoColorTwist(SrcWrapper src, DstWrapper dst, const int2 s DoAffineTransform(src, dst, size, transform); } +template +inline __device__ void DoColorTwistPlanar(SrcWrapper src, DstWrapper dst, const int2 size, int numChannels, + const cuda::Tensor1DWrap param) +{ + static_assert(cuda::NumElements == 4); + auto transform = GetAffineTransform(param); + DoAffineTransformPlanar(src, dst, size, numChannels, transform); +} + +template +inline __device__ void DoColorTwistPlanar(SrcWrapper src, DstWrapper dst, const int2 size, int numChannels, + const cuda::Tensor2DWrap param) +{ + static_assert(cuda::NumElements == 4); + auto transform = GetAffineTransform(param); + DoAffineTransformPlanar(src, dst, size, numChannels, transform); +} + +template +inline __device__ void DoColorTwistPlanarU8x4(SrcWrapper src, DstWrapper dst, const int2 size, int numChannels, + const ColorTwistParam param) +{ + using ValueType = std::remove_const_t; + static_assert(cuda::NumElements == 4); + auto transform = GetAffineTransform(param); + DoAffineTransformPlanarU8x4(src, dst, size, numChannels, transform); +} + // ColorTwist kernel -------------------------------------------------------------- // Tensor variant @@ -149,6 +332,20 @@ __global__ void ColorTwist(SrcWrapper src, DstWrapper dst, int2 size, const Colo DoColorTwist(src, dst, size, param); } +template +__global__ void ColorTwistPlanarTensor(SrcWrapper src, DstWrapper dst, int2 size, int numChannels, + const ColorTwistParam param) +{ + DoColorTwistPlanar(src, dst, size, numChannels, param); +} + +template +__global__ void ColorTwistPlanarTensorU8x4(SrcWrapper src, DstWrapper dst, int2 size, int numChannels, + const ColorTwistParam param) +{ + DoColorTwistPlanarU8x4(src, dst, size, numChannels, param); +} + // VarBatch variant template __global__ void ColorTwist(SrcWrapper src, DstWrapper dst, const ColorTwistParam param) @@ -159,6 +356,25 @@ __global__ void ColorTwist(SrcWrapper src, DstWrapper dst, const ColorTwistParam DoColorTwist(src, dst, size, param); } +template +__global__ void ColorTwistPlanarVarShape(SrcWrapper src, DstWrapper dst, int numChannels, const ColorTwistParam param) +{ + int z = blockIdx.z; + int2 size{dst.width(z), dst.height(z)}; + + DoColorTwistPlanar(src, dst, size, numChannels, param); +} + +template +__global__ void ColorTwistPlanarVarShapeU8x4(SrcWrapper src, DstWrapper dst, int numChannels, + const ColorTwistParam param) +{ + int z = blockIdx.z; + int2 size{dst.width(z), dst.height(z)}; + + DoColorTwistPlanarU8x4(src, dst, size, numChannels, param); +} + // Run ColorTwist kernel ---------------------------------------------------------- template @@ -204,19 +420,129 @@ inline void RunColorTwist(cudaStream_t stream, const SrcData &srcData, const Dst } } +template +inline void RunColorTwistPlanar(cudaStream_t stream, const nvcv::TensorDataStridedCuda &srcData, + const nvcv::TensorDataStridedCuda &dstData, const ColorTwistParam ¶m) +{ + using BT = cuda::BaseType; + + dim3 block(32, 4, 1); + + auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(srcData); + NVCV_ASSERT(inAccess); + auto outAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(dstData); + NVCV_ASSERT(outAccess); + int2 size = cuda::StaticCast(long2{inAccess->numCols(), inAccess->numRows()}); + + if (inAccess->numSamples() > 65535) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Planar ColorTwist launch exceeds CUDA grid.z limit: N=%d", + static_cast(inAccess->numSamples())); + } + + dim3 grid(util::DivUp(size.x, block.x), util::DivUp(size.y, block.y), inAccess->numSamples()); + + int64_t inMaxStride = inAccess->sampleStride() * inAccess->numSamples(); + int64_t outMaxStride = outAccess->sampleStride() * outAccess->numSamples(); + if (std::max(inMaxStride, outMaxStride) <= cuda::TypeTraits::max) + { + auto src = cuda::CreateTensorWrapNCHW(srcData); + auto dst = cuda::CreateTensorWrapNCHW(dstData); + if constexpr (std::is_same_v) + { + dim3 gridU8(util::DivUp(size.x, block.x * kPlanarU8NIX), util::DivUp(size.y, block.y), + inAccess->numSamples()); + ColorTwistPlanarTensorU8x4<<>>(src, dst, size, inAccess->numChannels(), param); + } + else + { + ColorTwistPlanarTensor<<>>(src, dst, size, inAccess->numChannels(), param); + } + } + else + { + throw nvcv::Exception(nvcv::Status::ERROR_OVERFLOW, "Input or output size exceeds %d. Tensor is too large.", + cuda::TypeTraits::max); + } + NVCV_CHECK_THROW(cudaGetLastError()); +} + +template +inline void RunColorTwistPlanar(cudaStream_t stream, const nvcv::ImageBatchVarShapeDataStridedCuda &srcData, + const nvcv::ImageBatchVarShapeDataStridedCuda &dstData, int numChannels, + const ColorTwistParam ¶m) +{ + using BT = cuda::BaseType; + + if (dstData.numImages() > 65535) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Planar ColorTwist launch exceeds CUDA grid.z limit: N=%d", dstData.numImages()); + } + + dim3 block(32, 4, 1); + int3 dstMaxSize{dstData.maxSize().w, dstData.maxSize().h, dstData.numImages()}; + dim3 grid(util::DivUp(dstMaxSize.x, block.x), util::DivUp(dstMaxSize.y, block.y), dstMaxSize.z); + + cuda::ImageBatchVarShapeWrap src(srcData); + cuda::ImageBatchVarShapeWrap dst(dstData); + + if constexpr (std::is_same_v) + { + dim3 gridU8(util::DivUp(dstMaxSize.x, block.x * kPlanarU8NIX), util::DivUp(dstMaxSize.y, block.y), + dstMaxSize.z); + ColorTwistPlanarVarShapeU8x4<<>>(src, dst, numChannels, param); + } + else + { + ColorTwistPlanarVarShape<<>>(src, dst, numChannels, param); + } + NVCV_CHECK_THROW(cudaGetLastError()); +} + template inline void RunColorTwist(cudaStream_t stream, const SrcData &srcData, const DstData &dstData, - const nvcv::TensorDataStridedCuda &twistData, bool hasPerSampleTwist) + const nvcv::TensorDataStridedCuda &twistData, bool hasPerSampleTwist, bool isPlanar, + int numChannels) { if (!hasPerSampleTwist) { auto twist = cuda::Tensor1DWrap(twistData); - RunColorTwist(stream, srcData, dstData, twist); + if (isPlanar) + { + if constexpr (std::is_same_v) + { + RunColorTwistPlanar(stream, srcData, dstData, twist); + } + else + { + RunColorTwistPlanar(stream, srcData, dstData, numChannels, twist); + } + } + else + { + RunColorTwist(stream, srcData, dstData, twist); + } } else { auto twist = cuda::Tensor2DWrap(twistData); - RunColorTwist(stream, srcData, dstData, twist); + if (isPlanar) + { + if constexpr (std::is_same_v) + { + RunColorTwistPlanar(stream, srcData, dstData, twist); + } + else + { + RunColorTwistPlanar(stream, srcData, dstData, numChannels, twist); + } + } + else + { + RunColorTwist(stream, srcData, dstData, twist); + } } } @@ -256,7 +582,7 @@ inline void RunSrcTypeSwitch(int numChannels, nvcv::DataType srcType, nvcv::Data // Argument validation helpers ---------------------------------------------------- -inline auto validateSrcDstTensors(int &numSamples, int &numChannels, nvcv::DataType &srcDstDtype, +inline bool validateSrcDstTensors(int &numSamples, int &numChannels, nvcv::DataType &srcDstDtype, const nvcv::Optional &srcData, const nvcv::Optional &dstData) { @@ -272,49 +598,54 @@ inline auto validateSrcDstTensors(int &numSamples, int &numChannels, nvcv::DataT "Output must be cuda-accessible, pitch-linear tensor"); } - auto srcAccess = nvcv::TensorDataAccessStridedImage::Create(*srcData); - auto dstAccess = nvcv::TensorDataAccessStridedImage::Create(*dstData); - NVCV_ASSERT(srcAccess && dstAccess); + srcDstDtype = srcData->dtype(); - numSamples = srcAccess->numSamples(); - if (numSamples != dstAccess->numSamples()) + if (srcDstDtype != dstData->dtype()) { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of samples"); + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input and output data type are different, but must be the same."); } - numChannels = srcAccess->numChannels(); - if (numChannels != dstAccess->numChannels()) + if (srcData->layout() != dstData->layout()) { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of channels"); + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input and output must have the same layout"); } - if (numChannels != 3 && numChannels != 4) + const bool isPlanar = IsPlanar(srcData->layout()); + if (srcData->layout() != nvcv::TENSOR_HWC && srcData->layout() != nvcv::TENSOR_NHWC && !isPlanar) { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input must have 3 or 4 channels"); + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input must have (N)HWC or (N)CHW layout"); } - srcDstDtype = srcData->dtype(); - - if (srcDstDtype != dstData->dtype()) + auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcData); + auto dstAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*dstData); + if (!srcAccess || !dstAccess) { throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, - "Input and output data type are different, but must be the same."); + "Input/output must be accessible as strided images"); } - if (srcData->layout() != dstData->layout()) + numSamples = srcAccess->numSamples(); + if (numSamples != dstAccess->numSamples()) { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input and output must have the same layout"); + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of samples"); } - if (srcData->layout() != nvcv::TENSOR_HWC && srcData->layout() != nvcv::TENSOR_NHWC) + numChannels = srcAccess->numChannels(); + if (numChannels != dstAccess->numChannels()) { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Output must have (N)HWC layout"); + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of channels"); } - return srcAccess; + if (numChannels != 3 && numChannels != 4) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input must have 3 or 4 channels"); + } + + return isPlanar; } -inline void validateSrcDstVarBatch(int &numSamples, int &numChannels, nvcv::DataType &srcDstDtype, +inline bool validateSrcDstVarBatch(int &numSamples, int &numChannels, nvcv::DataType &srcDstDtype, const nvcv::Optional &srcData, const nvcv::Optional &dstData) { @@ -337,9 +668,10 @@ inline void validateSrcDstVarBatch(int &numSamples, int &numChannels, nvcv::Data } const auto &srcFormat = srcData->uniqueFormat(); - if (srcFormat.numPlanes() > 1) + const auto &dstFormat = dstData->uniqueFormat(); + if (!srcFormat || !dstFormat) { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Image batches must have (N)HWC layout"); + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "All images in a batch must have the same format"); } srcDstDtype = srcFormat.planeDataType(0); @@ -350,11 +682,13 @@ inline void validateSrcDstVarBatch(int &numSamples, int &numChannels, nvcv::Data throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "The input must have 3 or 4 channels"); } - if (srcFormat != dstData->uniqueFormat()) + if (srcFormat != dstFormat) { throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input and output data type are different, but must be the same."); } + + return srcFormat.numPlanes() > 1; } inline void validateTwistTensor(bool &hasPerSampleTwist, nvcv::DataType &twistDtype, int numImages, @@ -421,12 +755,13 @@ ColorTwist::ColorTwist() {} void ColorTwist::operator()(cudaStream_t stream, const nvcv::Tensor &src, const nvcv::Tensor &dst, const nvcv::Tensor &twist) const { + CVCUDA_NVTX_RANGE("cvcuda::ColorTwist::operator()[Tensor]"); int numSamples; int numChannels; nvcv::DataType srcDstDtype; - auto srcData = src.exportData(); - auto dstData = dst.exportData(); - validateSrcDstTensors(numSamples, numChannels, srcDstDtype, srcData, dstData); + auto srcData = src.exportData(); + auto dstData = dst.exportData(); + bool isPlanar = validateSrcDstTensors(numSamples, numChannels, srcDstDtype, srcData, dstData); bool hasPerSampleTwist; nvcv::DataType twistDtype; @@ -434,11 +769,13 @@ void ColorTwist::operator()(cudaStream_t stream, const nvcv::Tensor &src, const validateTwistTensor(hasPerSampleTwist, twistDtype, numSamples, twistData); RunSrcTypeSwitch(numChannels, srcDstDtype, twistDtype, - [&](auto srcDummy, auto twistDummy) + [&stream, &srcData, &dstData, &twistData, &hasPerSampleTwist, isPlanar, numChannels]( + auto srcDummy, auto twistDummy) { using SrcDstT = decltype(srcDummy); using TwistT = decltype(twistDummy); - RunColorTwist(stream, *srcData, *dstData, *twistData, hasPerSampleTwist); + RunColorTwist(stream, *srcData, *dstData, *twistData, hasPerSampleTwist, + isPlanar, numChannels); }); } @@ -446,12 +783,13 @@ void ColorTwist::operator()(cudaStream_t stream, const nvcv::Tensor &src, const void ColorTwist::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &src, const nvcv::ImageBatchVarShape &dst, const nvcv::Tensor &twist) const { + CVCUDA_NVTX_RANGE("cvcuda::ColorTwist::operator()[ImageBatchVarShape]"); int numSamples; int numChannels; nvcv::DataType srcDstDtype; - auto srcData = src.exportData(stream); - auto dstData = dst.exportData(stream); - validateSrcDstVarBatch(numSamples, numChannels, srcDstDtype, srcData, dstData); + auto srcData = src.exportData(stream); + auto dstData = dst.exportData(stream); + bool isPlanar = validateSrcDstVarBatch(numSamples, numChannels, srcDstDtype, srcData, dstData); bool hasPerSampleTwist; nvcv::DataType twistDtype; @@ -459,11 +797,13 @@ void ColorTwist::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape validateTwistTensor(hasPerSampleTwist, twistDtype, numSamples, twistData); RunSrcTypeSwitch(numChannels, srcDstDtype, twistDtype, - [&](auto srcDummy, auto twistDummy) + [&stream, &srcData, &dstData, &twistData, &hasPerSampleTwist, isPlanar, numChannels]( + auto srcDummy, auto twistDummy) { using SrcDstT = decltype(srcDummy); using TwistT = decltype(twistDummy); - RunColorTwist(stream, *srcData, *dstData, *twistData, hasPerSampleTwist); + RunColorTwist(stream, *srcData, *dstData, *twistData, hasPerSampleTwist, + isPlanar, numChannels); }); } diff --git a/src/cvcuda/priv/OpComposite.cpp b/src/cvcuda/priv/OpComposite.cpp index bce123058..8ce382fe2 100644 --- a/src/cvcuda/priv/OpComposite.cpp +++ b/src/cvcuda/priv/OpComposite.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "OpComposite.hpp" +#include "Nvtx.hpp" #include "legacy/CvCudaLegacy.h" #include "legacy/CvCudaLegacyHelpers.hpp" @@ -29,7 +30,8 @@ namespace legacy = nvcv::legacy::cuda_op; Composite::Composite() { - legacy::DataShape maxIn, maxOut; + legacy::DataShape maxIn; + legacy::DataShape maxOut; //maxIn/maxOut not used by op. m_legacyOp = std::make_unique(maxIn, maxOut); m_legacyOpVarShape = std::make_unique(maxIn, maxOut); @@ -38,6 +40,7 @@ Composite::Composite() void Composite::operator()(cudaStream_t stream, const nvcv::Tensor &foreground, const nvcv::Tensor &background, const nvcv::Tensor &fgMask, const nvcv::Tensor &output) const { + CVCUDA_NVTX_RANGE("cvcuda::Composite::operator()[Tensor]"); auto foregroundData = foreground.exportData(); if (foregroundData == nullptr) { @@ -73,6 +76,32 @@ void Composite::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape & const nvcv::ImageBatchVarShape &background, const nvcv::ImageBatchVarShape &fgMask, const nvcv::ImageBatchVarShape &output) const { + CVCUDA_NVTX_RANGE("cvcuda::Composite::operator()[ImageBatchVarShape]"); + const int numImages = foreground.numImages(); + if (!((numImages == background.numImages()) && (numImages == fgMask.numImages()) + && (numImages == output.numImages()))) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input foreground, background, fgMask and output must have same number of images"); + } + + for (int i = 0; i < numImages; ++i) + { + const nvcv::Size2D foregroundSize = foreground[i].size(); + const nvcv::Size2D backgroundSize = background[i].size(); + const nvcv::Size2D fgMaskSize = fgMask[i].size(); + const nvcv::Size2D outputSize = output[i].size(); + + if (!((foregroundSize.w == backgroundSize.w) && (foregroundSize.w == fgMaskSize.w) + && (foregroundSize.w == outputSize.w) && (foregroundSize.h == backgroundSize.h) + && (foregroundSize.h == fgMaskSize.h) && (foregroundSize.h == outputSize.h))) + { + throw nvcv::Exception( + nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input foreground, background, fgMask and output images must have same width and height"); + } + } + auto foregroundData = foreground.exportData(stream); if (foregroundData == nullptr) { diff --git a/src/cvcuda/priv/OpConv2D.cpp b/src/cvcuda/priv/OpConv2D.cpp index a9fc049e7..9b207c10b 100644 --- a/src/cvcuda/priv/OpConv2D.cpp +++ b/src/cvcuda/priv/OpConv2D.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "OpConv2D.hpp" +#include "Nvtx.hpp" #include "legacy/CvCudaLegacy.h" #include "legacy/CvCudaLegacyHelpers.hpp" @@ -29,7 +30,8 @@ namespace legacy = nvcv::legacy::cuda_op; Conv2D::Conv2D() { - legacy::DataShape maxIn, maxOut; //maxIn/maxOut not used by op. + legacy::DataShape maxIn; + legacy::DataShape maxOut; //maxIn/maxOut not used by op. m_legacyOpVarShape = std::make_unique(maxIn, maxOut); } @@ -37,6 +39,7 @@ void Conv2D::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &kernel, const nvcv::Tensor &kernelAnchor, NVCVBorderType borderMode) const { + CVCUDA_NVTX_RANGE("cvcuda::Conv2D::operator()[ImageBatchVarShape]"); auto inData = in.exportData(stream); if (inData == nullptr) { diff --git a/src/cvcuda/priv/OpConvertTo.cpp b/src/cvcuda/priv/OpConvertTo.cpp index f2c67c89a..5522e8f22 100644 --- a/src/cvcuda/priv/OpConvertTo.cpp +++ b/src/cvcuda/priv/OpConvertTo.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "OpConvertTo.hpp" +#include "Nvtx.hpp" #include "legacy/CvCudaLegacy.h" #include "legacy/CvCudaLegacyHelpers.hpp" @@ -29,14 +30,16 @@ namespace legacy = nvcv::legacy::cuda_op; ConvertTo::ConvertTo() { - legacy::DataShape maxIn, maxOut; + legacy::DataShape maxIn; + legacy::DataShape maxOut; //maxIn/maxOut not used by op. m_legacyOp = std::make_unique(maxIn, maxOut); } void ConvertTo::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, const double alpha, - const double beta) const + const double beta, NVCVRoundMode roundMode) const { + CVCUDA_NVTX_RANGE("cvcuda::ConvertTo::operator()[Tensor]"); auto inData = in.exportData(); if (inData == nullptr) { @@ -51,7 +54,7 @@ void ConvertTo::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nv "Output must be cuda-accessible, pitch-linear tensor"); } - NVCV_CHECK_THROW(m_legacyOp->infer(*inData, *outData, alpha, beta, stream)); + NVCV_CHECK_THROW(m_legacyOp->infer(*inData, *outData, alpha, beta, roundMode, stream)); } } // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpConvertTo.hpp b/src/cvcuda/priv/OpConvertTo.hpp index 800c06f52..8470a363c 100644 --- a/src/cvcuda/priv/OpConvertTo.hpp +++ b/src/cvcuda/priv/OpConvertTo.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -27,6 +27,7 @@ #include "IOperator.hpp" #include "legacy/CvCudaLegacy.h" +#include #include #include @@ -39,7 +40,7 @@ class ConvertTo final : public IOperator explicit ConvertTo(); void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, const double alpha, - const double beta) const; + const double beta, NVCVRoundMode roundMode) const; private: std::unique_ptr m_legacyOp; diff --git a/src/cvcuda/priv/OpCopyMakeBorder.cpp b/src/cvcuda/priv/OpCopyMakeBorder.cpp index f5fc122c7..cd6518454 100644 --- a/src/cvcuda/priv/OpCopyMakeBorder.cpp +++ b/src/cvcuda/priv/OpCopyMakeBorder.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "OpCopyMakeBorder.hpp" +#include "Nvtx.hpp" #include "legacy/CvCudaLegacy.h" #include "legacy/CvCudaLegacyHelpers.hpp" @@ -29,7 +30,8 @@ namespace legacy = nvcv::legacy::cuda_op; CopyMakeBorder::CopyMakeBorder() { - legacy::DataShape maxIn, maxOut; + legacy::DataShape maxIn; + legacy::DataShape maxOut; //maxIn/maxOut not used by op. m_legacyOp = std::make_unique(maxIn, maxOut); m_legacyOpVarShape = std::make_unique(maxIn, maxOut); @@ -38,6 +40,7 @@ CopyMakeBorder::CopyMakeBorder() void CopyMakeBorder::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, const int top, const int left, const NVCVBorderType borderMode, const float4 borderValue) const { + CVCUDA_NVTX_RANGE("cvcuda::CopyMakeBorder::operator()[Tensor]"); auto inData = in.exportData(); if (inData == nullptr) { @@ -59,6 +62,7 @@ void CopyMakeBorder::operator()(cudaStream_t stream, const nvcv::ImageBatch &in, const nvcv::Tensor &top, const nvcv::Tensor &left, const NVCVBorderType borderMode, const float4 borderValue) const { + CVCUDA_NVTX_RANGE("cvcuda::CopyMakeBorder::operator()[ImageBatch->Tensor]"); auto inData = in.exportData(stream); if (inData == nullptr) { @@ -93,6 +97,7 @@ void CopyMakeBorder::operator()(cudaStream_t stream, const nvcv::ImageBatch &in, const nvcv::Tensor &top, const nvcv::Tensor &left, const NVCVBorderType borderMode, const float4 borderValue) const { + CVCUDA_NVTX_RANGE("cvcuda::CopyMakeBorder::operator()[ImageBatch]"); auto inData = in.exportData(stream); if (inData == nullptr) { diff --git a/src/cvcuda/priv/OpCropFlipNormalizeReformat.cu b/src/cvcuda/priv/OpCropFlipNormalizeReformat.cu index cfae84add..9050a9ad9 100644 --- a/src/cvcuda/priv/OpCropFlipNormalizeReformat.cu +++ b/src/cvcuda/priv/OpCropFlipNormalizeReformat.cu @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,6 +15,7 @@ * limitations under the License. */ +#include "Nvtx.hpp" #include "OpCropFlipNormalizeReformat.hpp" #include @@ -32,6 +33,7 @@ #include #include +#include namespace cuda = nvcv::cuda; @@ -134,55 +136,66 @@ __device__ void set_data(DstWrapper dstWrap, int2 dst_idx, int batchidx, int ch, } } -template +template __global__ void slice_flip_normalize(SrcWrapper srcWrap, DstWrapper dstWrap, FlipWrapper flipCodeWrap, TensorWrapper baseWrap, TensorWrapper scaleWrap, CropRectWrap cropRect, float global_scale, float global_shift, float epsilon, uint32_t flags, int input_channels, int base_ch, int scale_ch, int3 out_size, bool dst_planar) { - int3 dstCoord = cuda::StaticCast(blockIdx * blockDim + threadIdx); + const int batch_idx = blockIdx.z; + const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int dst_x0 = (blockIdx.x * blockDim.x + threadIdx.x) * NIX; - int4 *crop_ptr = reinterpret_cast(cropRect.ptr(dstCoord.z, 0, 0, 0)); + int4 *crop_ptr = reinterpret_cast(cropRect.ptr(batch_idx, 0, 0, 0)); int crop_val[4] = {(*crop_ptr).x, (*crop_ptr).y, (*crop_ptr).z, (*crop_ptr).w}; // crop_x, crop_y, crop_w, crop_h stored in (x,y,z,w) - if (dstCoord.x >= out_size.x || dstCoord.y >= out_size.y) + if (dst_x0 >= out_size.x || dst_y >= out_size.y) { return; } - // out of crop width and height - if (dstCoord.x >= crop_val[2] || dstCoord.y >= crop_val[3]) - { - set_data(dstWrap, (int2){dstCoord.x, dstCoord.y}, blockIdx.z, input_channels, 0, dst_planar); - return; - } - int flip_code = flipCodeWrap[dstCoord.z]; - int2 srcCoord; + const int flip_code = flipCodeWrap[batch_idx]; - if (flip_code == 1) - { // horizental - srcCoord.x = crop_val[2] - 1 - dstCoord.x + crop_val[0]; - srcCoord.y = dstCoord.y + crop_val[1]; - } - else if (flip_code == 0) - { // vertical - srcCoord.x = dstCoord.x + crop_val[0]; - srcCoord.y = crop_val[3] - 1 - dstCoord.y + crop_val[1]; - } - else if (flip_code == -1) - { // horizental + vertical - srcCoord.x = crop_val[2] - 1 - dstCoord.x + crop_val[0]; - srcCoord.y = crop_val[3] - 1 - dstCoord.y + crop_val[1]; - } - else - { // no flip - srcCoord.x = dstCoord.x + crop_val[0]; - srcCoord.y = dstCoord.y + crop_val[1]; - } +#pragma unroll + for (int i = 0; i < NIX; ++i) + { // horizontal + const int dst_x = dst_x0 + i; + if (dst_x >= out_size.x) + break; + + // out of crop width and height + if (dst_x >= crop_val[2] || dst_y >= crop_val[3]) + { + set_data(dstWrap, (int2){dst_x, dst_y}, batch_idx, input_channels, 0, dst_planar); + continue; + } + + int2 srcCoord; + if (flip_code == 1) + { // horizontal + srcCoord.x = crop_val[2] - 1 - dst_x + crop_val[0]; + srcCoord.y = dst_y + crop_val[1]; + } + else if (flip_code == 0) + { // vertical + srcCoord.x = dst_x + crop_val[0]; + srcCoord.y = crop_val[3] - 1 - dst_y + crop_val[1]; + } + else if (flip_code == -1) + { // horizental + vertical + srcCoord.x = crop_val[2] - 1 - dst_x + crop_val[0]; + srcCoord.y = crop_val[3] - 1 - dst_y + crop_val[1]; + } + else + { // no flip + srcCoord.x = dst_x + crop_val[0]; + srcCoord.y = dst_y + crop_val[1]; + } - transfer_data(srcWrap, dstWrap, srcCoord, {dstCoord.x, dstCoord.y}, dstCoord.z, input_channels, baseWrap, scaleWrap, - global_scale, global_shift, epsilon, flags, base_ch, scale_ch, dst_planar); + transfer_data(srcWrap, dstWrap, srcCoord, {dst_x, dst_y}, batch_idx, input_channels, baseWrap, scaleWrap, + global_scale, global_shift, epsilon, flags, base_ch, scale_ch, dst_planar); + } } template @@ -213,42 +226,62 @@ void RunCropFlipNormalizeReformatS(cudaStream_t stream, const nvcv::ImageBatchVa cuda::Tensor1DWrap flipCodeWrap(flipCodeData); auto baseWrap = cuda::CreateTensorWrapNHWC(baseData); auto scaleWrap = cuda::CreateTensorWrapNHWC(scaleData); + dim3 grid4((grid.x + 3) / 4, grid.y, grid.z); + + const bool useScalarWidePath = channel > 1 && (sizeof(T_Src) > 1 || sizeof(T_Dst) > 1); + constexpr bool supportsDataType + = std::is_same_v && (std::is_same_v || std::is_same_v); + constexpr bool supportsBorder = B == NVCV_BORDER_CONSTANT || B == NVCV_BORDER_REFLECT101; + constexpr bool supportsBatchedPath = supportsDataType && supportsBorder; + + auto launch = [&](auto &srcBorderWrap, auto &dstWrap) + { + if constexpr (supportsBatchedPath) + { + if (useScalarWidePath) + slice_flip_normalize<1><<>>( + srcBorderWrap, dstWrap, flipCodeWrap, baseWrap, scaleWrap, cropRectWrap, global_scale, shift, + epsilon, flags, channel, base_channels, scale_channels, out_size, dst_planar); + else + slice_flip_normalize<4><<>>( + srcBorderWrap, dstWrap, flipCodeWrap, baseWrap, scaleWrap, cropRectWrap, global_scale, shift, + epsilon, flags, channel, base_channels, scale_channels, out_size, dst_planar); + } + else + { + slice_flip_normalize<1><<>>( + srcBorderWrap, dstWrap, flipCodeWrap, baseWrap, scaleWrap, cropRectWrap, global_scale, shift, epsilon, + flags, channel, base_channels, scale_channels, out_size, dst_planar); + } + }; if (src_planar && dst_planar) { cuda::ImageBatchVarShapeWrap srcWrap(srcData); // planar cuda::BorderVarShapeWrap srcBorderWrap(srcWrap, static_cast(borderValue)); cuda::Tensor4DWrap dstWrap(dstData); // planar - slice_flip_normalize<<>>(srcBorderWrap, dstWrap, flipCodeWrap, baseWrap, scaleWrap, - cropRectWrap, global_scale, shift, epsilon, flags, channel, - base_channels, scale_channels, out_size, dst_planar); + launch(srcBorderWrap, dstWrap); } else if (src_planar) { cuda::ImageBatchVarShapeWrap srcWrap(srcData); // planar cuda::BorderVarShapeWrap srcBorderWrap(srcWrap, static_cast(borderValue)); auto dstWrap = cuda::CreateTensorWrapNHWC(dstData); // interleaved - slice_flip_normalize<<>>(srcBorderWrap, dstWrap, flipCodeWrap, baseWrap, scaleWrap, - cropRectWrap, global_scale, shift, epsilon, flags, channel, - base_channels, scale_channels, out_size, dst_planar); + launch(srcBorderWrap, dstWrap); } else if (dst_planar) { cuda::ImageBatchVarShapeWrapNHWC srcWrap(srcData, channel); // interleaved cuda::BorderVarShapeWrapNHWC srcBorderWrap(srcWrap, static_cast(borderValue)); cuda::Tensor4DWrap dstWrap(dstData); // planar - slice_flip_normalize<<>>(srcBorderWrap, dstWrap, flipCodeWrap, baseWrap, scaleWrap, - cropRectWrap, global_scale, shift, epsilon, flags, channel, - base_channels, scale_channels, out_size, dst_planar); + launch(srcBorderWrap, dstWrap); } else { cuda::ImageBatchVarShapeWrapNHWC srcWrap(srcData, channel); // interleaved cuda::BorderVarShapeWrapNHWC srcBorderWrap(srcWrap, static_cast(borderValue)); auto dstWrap = cuda::CreateTensorWrapNHWC(dstData); // interleaved - slice_flip_normalize<<>>(srcBorderWrap, dstWrap, flipCodeWrap, baseWrap, scaleWrap, - cropRectWrap, global_scale, shift, epsilon, flags, channel, - base_channels, scale_channels, out_size, dst_planar); + launch(srcBorderWrap, dstWrap); } } @@ -266,9 +299,13 @@ void RunCropFlipNormalizeReformat(cudaStream_t stream, const nvcv::ImageBatchVar NVCV_ASSERT(outAccess); const int3 out_size = {outAccess->numCols(), outAccess->numRows(), num_channels}; + const bool wideInterleavedToPlanar = channel > 1 && (sizeof(T_Src) > 1 || sizeof(T_Dst) > 1) + && srcData.uniqueFormat().numPlanes() == 1 + && dstData.layout() == nvcv::TENSOR_NCHW; + nvcv::Size2D maxSize = {outAccess->numCols(), outAccess->numRows()}; int32_t batchSize = srcData.numImages(); - dim3 block(32, 32, 1); + dim3 block(32, wideInterleavedToPlanar ? 16 : 8, 1); dim3 grid(std::ceil(maxSize.w / static_cast(block.x)), std::ceil(maxSize.h / static_cast(block.y)), batchSize); @@ -385,6 +422,7 @@ void CropFlipNormalizeReformat::operator()(cudaStream_t stream, const nvcv::Imag const nvcv::Tensor &scale, float global_scale, float shift, float epsilon, uint32_t flags) const { + CVCUDA_NVTX_RANGE("cvcuda::CropFlipNormalizeReformat::operator()[ImageBatchVarShape]"); auto inData = in.exportData(stream); if (inData == nullptr) { diff --git a/src/cvcuda/priv/OpCropFlipNormalizeReformat.hpp b/src/cvcuda/priv/OpCropFlipNormalizeReformat.hpp index 968dad790..68f9dabcc 100644 --- a/src/cvcuda/priv/OpCropFlipNormalizeReformat.hpp +++ b/src/cvcuda/priv/OpCropFlipNormalizeReformat.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -44,8 +44,6 @@ class CropFlipNormalizeReformat final : public IOperator const nvcv::Tensor &cropRect, const NVCVBorderType borderMode, const float borderValue, const nvcv::Tensor &flipCode, const nvcv::Tensor &base, const nvcv::Tensor &scale, float global_scale, float shift, float epsilon, uint32_t flags) const; - -private: }; } // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpCustomCrop.cpp b/src/cvcuda/priv/OpCustomCrop.cpp index f761a10e5..3f38c956f 100644 --- a/src/cvcuda/priv/OpCustomCrop.cpp +++ b/src/cvcuda/priv/OpCustomCrop.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,10 +17,13 @@ #include "OpCustomCrop.hpp" +#include "Nvtx.hpp" +#include "PlanarTensorView.hpp" #include "legacy/CvCudaLegacy.h" #include "legacy/CvCudaLegacyHelpers.hpp" #include +#include #include namespace cvcuda::priv { @@ -29,7 +32,8 @@ namespace legacy = nvcv::legacy::cuda_op; CustomCrop::CustomCrop() { - legacy::DataShape maxIn, maxOut; + legacy::DataShape maxIn; + legacy::DataShape maxOut; //maxIn/maxOut not used by op. m_legacyOp = std::make_unique(maxIn, maxOut); } @@ -37,6 +41,7 @@ CustomCrop::CustomCrop() void CustomCrop::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, const NVCVRectI &cropRect) const { + CVCUDA_NVTX_RANGE("cvcuda::CustomCrop::operator()[Tensor]"); auto inData = in.exportData(); if (inData == nullptr) { @@ -51,6 +56,15 @@ void CustomCrop::operator()(cudaStream_t stream, const nvcv::Tensor &in, const n "Output must be cuda-accessible, pitch-linear tensor"); } + // Cropping copies each channel plane independently and identically, so a planar (NCHW/CHW) image + // is just N*C single-channel planes: flatten them into the sample dimension and reuse the + // interleaved single-channel copy kernel unchanged, producing bit-exact planar output. + if (auto planarViews = PlanarSingleChannelViews(*inData, *outData)) + { + NVCV_CHECK_THROW(m_legacyOp->infer(planarViews->first, planarViews->second, cropRect, stream)); + return; + } + NVCV_CHECK_THROW(m_legacyOp->infer(*inData, *outData, cropRect, stream)); } diff --git a/src/cvcuda/priv/OpCvtColor.cpp b/src/cvcuda/priv/OpCvtColor.cpp index 7b12e1bb0..ebb299b08 100644 --- a/src/cvcuda/priv/OpCvtColor.cpp +++ b/src/cvcuda/priv/OpCvtColor.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "OpCvtColor.hpp" +#include "Nvtx.hpp" #include "legacy/CvCudaLegacy.h" #include "legacy/CvCudaLegacyHelpers.hpp" @@ -29,7 +30,8 @@ namespace legacy = nvcv::legacy::cuda_op; CvtColor::CvtColor() { - legacy::DataShape maxIn, maxOut; //maxIn/maxOut not used by op. + legacy::DataShape maxIn; + legacy::DataShape maxOut; //maxIn/maxOut not used by op. m_legacyOp = std::make_unique(maxIn, maxOut); m_legacyOpVarShape = std::make_unique(maxIn, maxOut); } @@ -37,6 +39,7 @@ CvtColor::CvtColor() void CvtColor::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, NVCVColorConversionCode code) const { + CVCUDA_NVTX_RANGE("cvcuda::CvtColor::operator()[Tensor]"); auto inData = in.exportData(); if (inData == nullptr) { @@ -57,6 +60,7 @@ void CvtColor::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvc void CvtColor::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, NVCVColorConversionCode code) const { + CVCUDA_NVTX_RANGE("cvcuda::CvtColor::operator()[ImageBatchVarShape]"); auto inData = in.exportData(stream); if (inData == nullptr) { diff --git a/src/cvcuda/priv/OpErase.cpp b/src/cvcuda/priv/OpErase.cpp index e778f569b..b86beda72 100644 --- a/src/cvcuda/priv/OpErase.cpp +++ b/src/cvcuda/priv/OpErase.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,8 +17,10 @@ #include "OpErase.hpp" +#include "Nvtx.hpp" #include "legacy/CvCudaLegacy.h" #include "legacy/CvCudaLegacyHelpers.hpp" +#include "legacy/EraseCopyPolicy.hpp" #include #include @@ -27,18 +29,49 @@ namespace cvcuda::priv { namespace legacy = nvcv::legacy::cuda_op; +namespace { + +bool UseBulkEraseCopyOnDevice(int deviceId) +{ + cudaDeviceProp properties{}; + NVCV_CHECK_THROW(cudaGetDeviceProperties(&properties, deviceId)); + + const int sm = properties.major * 10 + properties.minor; + return legacy::UseBulkEraseCopyForDevice(sm, properties.name); +} + +} // namespace + Erase::Erase(int num_erasing_area) + // Legacy operators are single-device by design. PerDeviceResource creates + // one instance per CUDA device for transparent multi-GPU support. + : m_legacyOp( + [num_erasing_area](int deviceId) + { + legacy::DataShape maxIn; + legacy::DataShape maxOut; + return std::make_unique(maxIn, maxOut, num_erasing_area, UseBulkEraseCopyOnDevice(deviceId)); + }) + , m_legacyOpVarShape( + [num_erasing_area](int deviceId) + { + legacy::DataShape maxIn; + legacy::DataShape maxOut; + return std::make_unique(maxIn, maxOut, num_erasing_area, + UseBulkEraseCopyOnDevice(deviceId)); + }) { - legacy::DataShape maxIn, maxOut; - // maxIn/maxOut not used by op. - m_legacyOp = std::make_unique(maxIn, maxOut, num_erasing_area); - m_legacyOpVarShape = std::make_unique(maxIn, maxOut, num_erasing_area); + if (num_erasing_area < 0) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "num_erasing_area must be >= 0"); + } } void Erase::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, const nvcv::Tensor &anchor, const nvcv::Tensor &erasing, const nvcv::Tensor &values, const nvcv::Tensor &imgIdx, bool random, unsigned int seed) const { + CVCUDA_NVTX_RANGE("cvcuda::Erase::operator()[Tensor]"); auto inData = in.exportData(); if (inData == nullptr) { @@ -82,14 +115,15 @@ void Erase::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv:: } bool inplace = (in.handle() == out.handle()); - NVCV_CHECK_THROW(m_legacyOp->infer(*inData, *outData, *anchorData, *erasingData, *valuesData, *imgIdxData, random, - seed, inplace, stream)); + NVCV_CHECK_THROW(m_legacyOp.get().infer(*inData, *outData, *anchorData, *erasingData, *valuesData, *imgIdxData, + random, seed, inplace, stream)); } void Erase::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, const nvcv::Tensor &anchor, const nvcv::Tensor &erasing, const nvcv::Tensor &values, const nvcv::Tensor &imgIdx, bool random, unsigned int seed) const { + CVCUDA_NVTX_RANGE("cvcuda::Erase::operator()[ImageBatchVarShape]"); auto anchorData = anchor.exportData(); if (anchorData == nullptr) { @@ -119,8 +153,8 @@ void Erase::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, } bool inplace = (in.handle() == out.handle()); - NVCV_CHECK_THROW(m_legacyOpVarShape->infer(in, out, *anchorData, *erasingData, *valuesData, *imgIdxData, random, - seed, inplace, stream)); + NVCV_CHECK_THROW(m_legacyOpVarShape.get().infer(in, out, *anchorData, *erasingData, *valuesData, *imgIdxData, + random, seed, inplace, stream)); } } // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpErase.hpp b/src/cvcuda/priv/OpErase.hpp index 787e10eca..cb3bace57 100644 --- a/src/cvcuda/priv/OpErase.hpp +++ b/src/cvcuda/priv/OpErase.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,13 +25,12 @@ #define CVCUDA_PRIV_ERASE_HPP #include "IOperator.hpp" +#include "PerDeviceResource.hpp" #include "legacy/CvCudaLegacy.h" #include #include -#include - namespace cvcuda::priv { class Erase final : public IOperator @@ -47,9 +46,12 @@ class Erase final : public IOperator const nvcv::Tensor &anchor, const nvcv::Tensor &erasing, const nvcv::Tensor &values, const nvcv::Tensor &imgIdx, bool random, unsigned int seed) const; + void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, int64_t i, int64_t j, + int64_t h, int64_t w, const nvcv::Tensor &values) const; + private: - std::unique_ptr m_legacyOp; - std::unique_ptr m_legacyOpVarShape; + mutable PerDeviceResource m_legacyOp; + mutable PerDeviceResource m_legacyOpVarShape; }; } // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpEraseRegion.cu b/src/cvcuda/priv/OpEraseRegion.cu new file mode 100644 index 000000000..6186d2c13 --- /dev/null +++ b/src/cvcuda/priv/OpEraseRegion.cu @@ -0,0 +1,534 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "Nvtx.hpp" +#include "OpErase.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace cvcuda::priv { + +namespace { + +struct ImageTensorDesc +{ + const unsigned char *src; + unsigned char *dst; + int64_t shape[4]; + int64_t srcStride[4]; + int64_t dstStride[4]; +}; + +struct ValueTensorDesc +{ + const unsigned char *base; + int64_t stride[4]; +}; + +struct Region +{ + int64_t top; + int64_t left; + int64_t height; + int64_t width; +}; + +template +__device__ __forceinline__ DstT ConvertValue(ValueT value) +{ + return static_cast(value); +} + +template<> +__device__ __forceinline__ __half ConvertValue<__half, float>(float value) +{ + return __float2half_rn(value); +} + +__device__ __forceinline__ int32_t TorchFloatToS32(float value) +{ + if (isnan(value)) + { + return 0; + } + if (value >= 0x1p31f) + { + return 0x7fffffff; + } + if (value <= -0x1p31f) + { + return -0x7fffffff - 1; + } + return static_cast(value); +} + +__device__ __forceinline__ uint32_t TorchFloatToU32(float value) +{ + if (isnan(value) || value <= 0.0f) + { + return 0; + } + if (value >= 0x1p32f) + { + return 0xffffffffU; + } + return static_cast(value); +} + +__device__ __forceinline__ int64_t TorchFloatToS64(float value) +{ + if (isnan(value) || value <= -0x1p63f) + { + return -0x7fffffffffffffffLL - 1; + } + if (value >= 0x1p63f) + { + return 0x7fffffffffffffffLL; + } + return static_cast(value); +} + +__device__ __forceinline__ uint64_t TorchFloatToU64(float value) +{ + if (isnan(value)) + { + return uint64_t{1} << 63; + } + if (value <= 0.0f) + { + return 0; + } + if (value >= 0x1p64f) + { + return 0xffffffffffffffffULL; + } + return static_cast(value); +} + +template<> +__device__ __forceinline__ uint8_t ConvertValue(float value) +{ + return static_cast(TorchFloatToS64(value)); +} + +template<> +__device__ __forceinline__ int8_t ConvertValue(float value) +{ + return static_cast(TorchFloatToS32(value)); +} + +template<> +__device__ __forceinline__ uint16_t ConvertValue(float value) +{ + return static_cast(TorchFloatToU32(value)); +} + +template<> +__device__ __forceinline__ int16_t ConvertValue(float value) +{ + return static_cast(TorchFloatToS32(value)); +} + +template<> +__device__ __forceinline__ uint32_t ConvertValue(float value) +{ + return TorchFloatToU32(value); +} + +template<> +__device__ __forceinline__ int32_t ConvertValue(float value) +{ + return TorchFloatToS32(value); +} + +template<> +__device__ __forceinline__ uint64_t ConvertValue(float value) +{ + return TorchFloatToU64(value); +} + +template<> +__device__ __forceinline__ int64_t ConvertValue(float value) +{ + return TorchFloatToS64(value); +} + +template +__device__ __forceinline__ T LoadAt(const unsigned char *base, int64_t offset) +{ + return *reinterpret_cast(base + offset); +} + +template +__device__ __forceinline__ void StoreAt(unsigned char *base, int64_t offset, T value) +{ + *reinterpret_cast(base + offset) = value; +} + +__device__ __forceinline__ int64_t ImageOffset(const int64_t *stride, int64_t n, int64_t c, int64_t y, int64_t x) +{ + return n * stride[0] + c * stride[1] + y * stride[2] + x * stride[3]; +} + +__device__ __forceinline__ int64_t ValueOffset(const ValueTensorDesc &value, int64_t n, int64_t c, int64_t y, int64_t x) +{ + return n * value.stride[0] + c * value.stride[1] + y * value.stride[2] + x * value.stride[3]; +} + +template +__global__ void EraseRegionOutOfPlace(ImageTensorDesc image, ValueTensorDesc value, Region region, int64_t total) +{ + for (int64_t index = static_cast(blockIdx.x) * blockDim.x + threadIdx.x; index < total; + index += static_cast(gridDim.x) * blockDim.x) + { + int64_t remaining = index; + const int64_t x = remaining % image.shape[3]; + remaining /= image.shape[3]; + const int64_t y = remaining % image.shape[2]; + remaining /= image.shape[2]; + const int64_t c = remaining % image.shape[1]; + const int64_t n = remaining / image.shape[1]; + + const int64_t dstOffset = ImageOffset(image.dstStride, n, c, y, x); + if (y >= region.top && y < region.top + region.height && x >= region.left && x < region.left + region.width) + { + const int64_t valueOffset = ValueOffset(value, n, c, y - region.top, x - region.left); + StoreAt(image.dst, dstOffset, ConvertValue(LoadAt(value.base, valueOffset))); + } + else + { + const int64_t srcOffset = ImageOffset(image.srcStride, n, c, y, x); + StoreAt(image.dst, dstOffset, LoadAt(image.src, srcOffset)); + } + } +} + +template +__global__ void EraseRegionInPlace(ImageTensorDesc image, ValueTensorDesc value, Region region, int64_t total) +{ + for (int64_t index = static_cast(blockIdx.x) * blockDim.x + threadIdx.x; index < total; + index += static_cast(gridDim.x) * blockDim.x) + { + int64_t remaining = index; + const int64_t x = remaining % region.width; + remaining /= region.width; + const int64_t y = remaining % region.height; + remaining /= region.height; + const int64_t c = remaining % image.shape[1]; + const int64_t n = remaining / image.shape[1]; + + const int64_t dstOffset = ImageOffset(image.dstStride, n, c, y + region.top, x + region.left); + const int64_t valueOffset = ValueOffset(value, n, c, y, x); + StoreAt(image.dst, dstOffset, ConvertValue(LoadAt(value.base, valueOffset))); + } +} + +static int64_t SaturatingAdd(int64_t a, int64_t b) +{ + if (b > 0 && a > std::numeric_limits::max() - b) + { + return std::numeric_limits::max(); + } + if (b < 0 && a < std::numeric_limits::min() - b) + { + return std::numeric_limits::min(); + } + return a + b; +} + +static int64_t NormalizeSliceIndex(int64_t index, int64_t size) +{ + if (index < 0) + { + if (index < -size) + { + return 0; + } + return index + size; + } + return std::min(index, size); +} + +static Region NormalizeRegion(int64_t i, int64_t j, int64_t h, int64_t w, int64_t imageHeight, int64_t imageWidth) +{ + const int64_t top = NormalizeSliceIndex(i, imageHeight); + const int64_t bottom = NormalizeSliceIndex(SaturatingAdd(i, h), imageHeight); + const int64_t left = NormalizeSliceIndex(j, imageWidth); + const int64_t right = NormalizeSliceIndex(SaturatingAdd(j, w), imageWidth); + + return Region{top, left, std::max(bottom - top, 0), std::max(right - left, 0)}; +} + +static bool IsSupportedScalarType(nvcv::DataType dtype) +{ + return dtype == nvcv::TYPE_U8 || dtype == nvcv::TYPE_S8 || dtype == nvcv::TYPE_U16 || dtype == nvcv::TYPE_S16 + || dtype == nvcv::TYPE_U32 || dtype == nvcv::TYPE_S32 || dtype == nvcv::TYPE_U64 || dtype == nvcv::TYPE_S64 + || dtype == nvcv::TYPE_F16 || dtype == nvcv::TYPE_F32 || dtype == nvcv::TYPE_F64; +} + +static ImageTensorDesc MakeImageDesc(const nvcv::TensorDataStridedCuda &input, + const nvcv::TensorDataStridedCuda &output) +{ + ImageTensorDesc desc{}; + desc.src = reinterpret_cast(input.basePtr()); + desc.dst = reinterpret_cast(output.basePtr()); + + const nvcv::TensorLayout layout = input.layout(); + if (layout == nvcv::TENSOR_NHWC) + { + desc.shape[0] = input.shape(0); + desc.shape[1] = input.shape(3); + desc.shape[2] = input.shape(1); + desc.shape[3] = input.shape(2); + desc.srcStride[0] = input.stride(0); + desc.srcStride[1] = input.stride(3); + desc.srcStride[2] = input.stride(1); + desc.srcStride[3] = input.stride(2); + desc.dstStride[0] = output.stride(0); + desc.dstStride[1] = output.stride(3); + desc.dstStride[2] = output.stride(1); + desc.dstStride[3] = output.stride(2); + } + else if (layout == nvcv::TENSOR_HWC) + { + desc.shape[0] = 1; + desc.shape[1] = input.shape(2); + desc.shape[2] = input.shape(0); + desc.shape[3] = input.shape(1); + desc.srcStride[0] = 0; + desc.srcStride[1] = input.stride(2); + desc.srcStride[2] = input.stride(0); + desc.srcStride[3] = input.stride(1); + desc.dstStride[0] = 0; + desc.dstStride[1] = output.stride(2); + desc.dstStride[2] = output.stride(0); + desc.dstStride[3] = output.stride(1); + } + else if (layout == nvcv::TENSOR_NCHW) + { + desc.shape[0] = input.shape(0); + desc.shape[1] = input.shape(1); + desc.shape[2] = input.shape(2); + desc.shape[3] = input.shape(3); + for (int k = 0; k < 4; ++k) + { + desc.srcStride[k] = input.stride(k); + desc.dstStride[k] = output.stride(k); + } + } + else if (layout == nvcv::TENSOR_CHW) + { + desc.shape[0] = 1; + desc.shape[1] = input.shape(0); + desc.shape[2] = input.shape(1); + desc.shape[3] = input.shape(2); + desc.srcStride[0] = 0; + desc.srcStride[1] = input.stride(0); + desc.srcStride[2] = input.stride(1); + desc.srcStride[3] = input.stride(2); + desc.dstStride[0] = 0; + desc.dstStride[1] = output.stride(0); + desc.dstStride[2] = output.stride(1); + desc.dstStride[3] = output.stride(2); + } + else + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Erase region supports only NHWC, HWC, NCHW, and CHW layouts"); + } + + return desc; +} + +static ValueTensorDesc MakeValueDesc(const nvcv::TensorDataStridedCuda &values, const ImageTensorDesc &image, + const Region ®ion) +{ + constexpr int targetRank = 4; + if (values.rank() > targetRank) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Erase region value rank exceeds the logical image rank"); + } + + const int64_t targetShape[4] = {image.shape[0], image.shape[1], region.height, region.width}; + ValueTensorDesc desc{}; + desc.base = reinterpret_cast(values.basePtr()); + + const int rank = values.rank(); + const int leading = targetRank - rank; + for (int valueDim = 0; valueDim < rank; ++valueDim) + { + const int targetDim = leading + valueDim; + const int logicalDim = targetDim; + const int64_t extent = values.shape(valueDim); + if (extent != 1 && extent != targetShape[logicalDim]) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Erase region value is not broadcastable to the selected image region"); + } + desc.stride[logicalDim] = extent == 1 ? 0 : values.stride(valueDim); + } + return desc; +} + +static int64_t CheckedProduct(int64_t a, int64_t b, int64_t c, int64_t d) +{ + int64_t result = 1; + for (int64_t value : {a, b, c, d}) + { + if (value == 0) + { + return 0; + } + if (value < 0 || result > std::numeric_limits::max() / value) + { + throw nvcv::Exception(nvcv::Status::ERROR_OVERFLOW, "Erase region tensor size overflows int64_t"); + } + result *= value; + } + return result; +} + +template +void LaunchEraseRegion(cudaStream_t stream, const ImageTensorDesc &image, const ValueTensorDesc &value, + const Region ®ion, bool inplace) +{ + constexpr int kBlockSize = 256; + const int64_t total = inplace ? CheckedProduct(image.shape[0], image.shape[1], region.height, region.width) + : CheckedProduct(image.shape[0], image.shape[1], image.shape[2], image.shape[3]); + if (total == 0) + { + return; + } + + const int64_t neededBlocks = (total + kBlockSize - 1) / kBlockSize; + const int blocks = static_cast(std::min(neededBlocks, 65535)); + if (inplace) + { + EraseRegionInPlace<<>>(image, value, region, total); + } + else + { + EraseRegionOutOfPlace<<>>(image, value, region, total); + } + NVCV_CHECK_THROW(cudaGetLastError()); +} + +template +void DispatchValueType(cudaStream_t stream, const ImageTensorDesc &image, const ValueTensorDesc &value, + const Region ®ion, bool inplace, bool floatValue) +{ + if (floatValue) + { + LaunchEraseRegion(stream, image, value, region, inplace); + } + else + { + LaunchEraseRegion(stream, image, value, region, inplace); + } +} + +static void DispatchImageType(cudaStream_t stream, nvcv::DataType dtype, const ImageTensorDesc &image, + const ValueTensorDesc &value, const Region ®ion, bool inplace, bool floatValue) +{ + if (dtype == nvcv::TYPE_U8) + DispatchValueType(stream, image, value, region, inplace, floatValue); + else if (dtype == nvcv::TYPE_S8) + DispatchValueType(stream, image, value, region, inplace, floatValue); + else if (dtype == nvcv::TYPE_U16) + DispatchValueType(stream, image, value, region, inplace, floatValue); + else if (dtype == nvcv::TYPE_S16) + DispatchValueType(stream, image, value, region, inplace, floatValue); + else if (dtype == nvcv::TYPE_U32) + DispatchValueType(stream, image, value, region, inplace, floatValue); + else if (dtype == nvcv::TYPE_S32) + DispatchValueType(stream, image, value, region, inplace, floatValue); + else if (dtype == nvcv::TYPE_U64) + DispatchValueType(stream, image, value, region, inplace, floatValue); + else if (dtype == nvcv::TYPE_S64) + DispatchValueType(stream, image, value, region, inplace, floatValue); + else if (dtype == nvcv::TYPE_F16) + DispatchValueType<__half>(stream, image, value, region, inplace, floatValue); + else if (dtype == nvcv::TYPE_F32) + DispatchValueType(stream, image, value, region, inplace, floatValue); + else if (dtype == nvcv::TYPE_F64) + DispatchValueType(stream, image, value, region, inplace, floatValue); + else + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Unsupported Erase region dtype"); +} + +} // namespace + +void Erase::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, int64_t i, int64_t j, + int64_t h, int64_t w, const nvcv::Tensor &values) const +{ + CVCUDA_NVTX_RANGE("cvcuda::Erase::operator()[Region]"); + + auto input = in.exportData(); + auto output = out.exportData(); + auto value = values.exportData(); + if (input == nullptr || output == nullptr || value == nullptr) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Erase region inputs must be CUDA-accessible pitch-linear tensors"); + } + if (input->rank() != output->rank() || input->layout() != output->layout() || input->dtype() != output->dtype()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Erase region input and output rank, layout, and dtype must match"); + } + for (int dim = 0; dim < input->rank(); ++dim) + { + if (input->shape(dim) != output->shape(dim)) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Erase region input and output shapes must match"); + } + } + if (!IsSupportedScalarType(input->dtype())) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Erase region supports only real scalar CV-CUDA dtypes"); + } + if (value->dtype() != input->dtype() && value->dtype() != nvcv::TYPE_F32) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Erase region values must match the image dtype or use float32"); + } + + ImageTensorDesc image = MakeImageDesc(*input, *output); + if (image.shape[1] < 1 || image.shape[1] > 4) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Erase region supports image channel counts from 1 through 4"); + } + + Region region = NormalizeRegion(i, j, h, w, image.shape[2], image.shape[3]); + ValueTensorDesc valueDesc = MakeValueDesc(*value, image, region); + const bool inplace = in.handle() == out.handle(); + DispatchImageType(stream, input->dtype(), image, valueDesc, region, inplace, value->dtype() == nvcv::TYPE_F32); +} + +} // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpFindHomography.cu b/src/cvcuda/priv/OpFindHomography.cu index abd413dbc..12754da99 100644 --- a/src/cvcuda/priv/OpFindHomography.cu +++ b/src/cvcuda/priv/OpFindHomography.cu @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,7 +15,9 @@ * limitations under the License. */ +#include "Nvtx.hpp" #include "OpFindHomography.hpp" +#include "SafeSize.hpp" #include #include @@ -632,11 +634,13 @@ __device__ void compute_qr8x8(matrix8x8 &sA, matrix8x8 &sQ) { if (tid < N) { - double theta = atan(-(double)sA[i][j] / (double)sA[pivot_row][j]); - double ctheta = cos(theta); - double stheta = sin(theta); - float sthetaf = (float)stheta; - float cthetaf = (float)ctheta; + const double pivot = sA[pivot_row][j]; + const double value = sA[i][j]; + const double norm = hypot(pivot, value); + const double ctheta = fabs(pivot) / norm; + const double stheta = -value * copysign(1.0, pivot) / norm; + const float sthetaf = (float)stheta; + const float cthetaf = (float)ctheta; temp[0] = ctheta * sA[pivot_row][tid] - stheta * sA[i][tid]; temp[1] = stheta * sA[pivot_row][tid] + ctheta * sA[i][tid]; @@ -803,8 +807,14 @@ __device__ int compute_model_estimate(float2 cM, float2 cm, float2 sM, float2 sm if (sm.x < FLT_EPSILON || sm.y < FLT_EPSILON || sM.x < FLT_EPSILON || sM.y < FLT_EPSILON) { + // Centroid + abs-shift both ~0 in at least one axis means the input + // points are all coincident or the kernel saw zero data (e.g. dst + // tensor never populated — see TestOpFindHomography test fix). Either + // way we cannot estimate a homography. Write NaN so the failure + // propagates to the caller's output rather than emitting a "valid"- + // looking [0 0 0; 0 0 0; 0 0 1] zero matrix. if (tid < 8) - x[tid] = 0; + x[tid] = nanf(""); __syncwarp(); return 1; } @@ -1287,9 +1297,17 @@ template __global__ void compute_LtL(SrcDstWrapper src, SrcDstWrapper dst, float *LtL, Func ltl_op, int maxNumPoints, int batchSize) { + // cuSolver consumes CUBLAS_FILL_MODE_LOWER in column-major order. LtL is populated through a + // row-major linear index, so the 45 physical locations it reads are the row-major upper triangle + // (j <= k). The bit mask marks the first packed-triangle entry for each j; popcount maps one of + // the 45 launched blocks to its (j, k) pair without a lookup table or runtime storage. + constexpr unsigned long long kColumnStarts = (1ULL << 0) | (1ULL << 9) | (1ULL << 17) | (1ULL << 24) | (1ULL << 30) + | (1ULL << 35) | (1ULL << 39) | (1ULL << 42) | (1ULL << 44); + + int pair = blockIdx.y; + int j = __popcll(kColumnStarts & ((1ULL << (pair + 1)) - 1)) - 1; + int k = j + pair - j * (19 - j) / 2; int batch = blockIdx.z; - int j = blockIdx.y / 9; // LtL row index - int k = blockIdx.y % 9; // LtL col index __shared__ cuda::math::Vector warpSums; if (batch < batchSize) { @@ -1358,7 +1376,7 @@ void FindHomographyWrapper(SrcDstWrapper srcWrap, SrcDstWrapper dstWrap, ModelTy printKernelfloat2<<<1, 1, 0, stream>>>(dstShiftSum + check_batch, 1, 0); #endif - grid.y = 81; + grid.y = 45; grid.z = batchSize; LtLOp ltl_op(srcMean, dstMean, srcShiftSum, dstShiftSum); compute_LtL<<>>(srcWrap, dstWrap, LtL, ltl_op, numPoints, batchSize); @@ -1402,7 +1420,10 @@ void FindHomographyWrapper(SrcDstWrapper srcWrap, SrcDstWrapper dstWrap, ModelTy } #endif - block.x = 256; + // One block owns one model. Match its warp count to the residual/Jacobian work so small point + // sets do not reserve idle warps that could otherwise keep more independent batches resident. + const int refinementWork = 2 * numPoints; + block.x = refinementWork <= 32 ? 32 : refinementWork <= 64 ? 64 : refinementWork <= 128 ? 128 : 256; grid.x = 1; grid.y = batchSize; grid.z = 1; @@ -1476,48 +1497,125 @@ namespace cvcuda::priv { // Constructor ----------------------------------------------------------------- -FindHomography::FindHomography(int batchSize, int maxNumPoints) +static void CheckCuSolver(cusolverStatus_t err, const char *msg) +{ + if (err != CUSOLVER_STATUS_SUCCESS) + { + throw nvcv::Exception(nvcv::Status::ERROR_INTERNAL, "CUSOLVER error (%d): %s", static_cast(err), msg); + } +} + +FindHomography::DeviceState::DeviceState(int batchSize, int maxNumPoints) +{ + try + { + const size_t batch = CheckedPositiveToSize(batchSize, "batchSize"); + const size_t points = CheckedPositiveToSize(maxNumPoints, "maxNumPoints"); + + const size_t meanBytes + = CheckedMulMany({sizeof(float2), batch}, "FindHomography mean allocation size overflow"); + NVCV_CHECK_THROW(cudaMalloc(reinterpret_cast(&bufferOffset.srcMean), meanBytes)); + NVCV_CHECK_THROW(cudaMalloc(reinterpret_cast(&bufferOffset.dstMean), meanBytes)); + NVCV_CHECK_THROW(cudaMalloc(reinterpret_cast(&bufferOffset.srcShiftSum), meanBytes)); + NVCV_CHECK_THROW(cudaMalloc(reinterpret_cast(&bufferOffset.dstShiftSum), meanBytes)); + NVCV_CHECK_THROW( + cudaMalloc(reinterpret_cast(&bufferOffset.LtL), + CheckedMulMany({81U, sizeof(float), batch}, "FindHomography LtL allocation size overflow"))); + NVCV_CHECK_THROW( + cudaMalloc(reinterpret_cast(&bufferOffset.W), + CheckedMulMany({9U, sizeof(float), batch}, "FindHomography W allocation size overflow"))); + NVCV_CHECK_THROW(cudaMalloc( + reinterpret_cast(&bufferOffset.r), + CheckedMulMany({2U, points, sizeof(float), batch}, "FindHomography residual allocation size overflow"))); + NVCV_CHECK_THROW(cudaMalloc(reinterpret_cast(&bufferOffset.J), + CheckedMulMany({2U, points, 8U, sizeof(float), batch}, + "FindHomography Jacobian allocation size overflow"))); + NVCV_CHECK_THROW(cudaMalloc( + reinterpret_cast(&bufferOffset.calc_buffer), + CheckedMulMany({points, sizeof(float), batch}, "FindHomography calc buffer allocation size overflow"))); + + CheckCuSolver(cusolverDnCreate(&cusolverData.cusolverH), "Failed to create cusolver handle"); + CheckCuSolver(cusolverDnCreateSyevjInfo(&cusolverData.syevj_params), "Failed to create syevj params"); + CheckCuSolver(cusolverDnXsyevjSetTolerance(cusolverData.syevj_params, 1e-7), + "Failed to set tolerance for syevj"); + CheckCuSolver(cusolverDnXsyevjSetMaxSweeps(cusolverData.syevj_params, 15), + "Failed to set max sweeps for syevj"); + CheckCuSolver(cusolverDnXsyevjSetSortEig(cusolverData.syevj_params, 1), + "Failed to set sorting of eigen values in syevj"); + CheckCuSolver(cusolverDnSsyevjBatched_bufferSize(cusolverData.cusolverH, CUSOLVER_EIG_MODE_VECTOR, + CUBLAS_FILL_MODE_LOWER, 9, nullptr, 9, nullptr, + &cusolverData.lwork, cusolverData.syevj_params, batchSize), + "Failed to calculate buffer size for syevj"); + + NVCV_CHECK_THROW(cudaMalloc( + reinterpret_cast(&cusolverData.cusolverBuffer), + CheckedMulMany({CheckedNonNegativeToSize(cusolverData.lwork, "cusolverData.lwork"), sizeof(float)}, + "FindHomography cuSolver buffer allocation size overflow"))); + NVCV_CHECK_THROW( + cudaMalloc(reinterpret_cast(&cusolverData.cusolverInfo), + CheckedMulMany({batch, sizeof(int)}, "FindHomography cuSolver info allocation size overflow"))); + } + catch (...) + { + cleanup(); + throw; + } +} + +FindHomography::DeviceState::~DeviceState() { - cudaMalloc(reinterpret_cast(&(bufferOffset.srcMean)), sizeof(float2) * batchSize); - cudaMalloc(reinterpret_cast(&(bufferOffset.dstMean)), sizeof(float2) * batchSize); - cudaMalloc(reinterpret_cast(&(bufferOffset.srcShiftSum)), sizeof(float2) * batchSize); - cudaMalloc(reinterpret_cast(&(bufferOffset.dstShiftSum)), sizeof(float2) * batchSize); - cudaMalloc(reinterpret_cast(&(bufferOffset.LtL)), 81 * sizeof(float) * batchSize); - cudaMalloc(reinterpret_cast(&(bufferOffset.W)), 9 * sizeof(float) * batchSize); - cudaMalloc(reinterpret_cast(&(bufferOffset.r)), 2 * maxNumPoints * sizeof(float) * batchSize); - cudaMalloc(reinterpret_cast(&(bufferOffset.J)), 2 * maxNumPoints * 8 * sizeof(float) * batchSize); - cudaMalloc(reinterpret_cast(&(bufferOffset.calc_buffer)), maxNumPoints * sizeof(float) * batchSize); - CUSOLVER_CHECK_ERROR(cusolverDnCreate(&(cusolverData.cusolverH)), "Failed to create cusolver handle"); - CUSOLVER_CHECK_ERROR(cusolverDnCreateSyevjInfo(&(cusolverData.syevj_params)), "Failed to create syevj params"); - CUSOLVER_CHECK_ERROR(cusolverDnXsyevjSetTolerance(cusolverData.syevj_params, 1e-7), - "Failed to set tolerance for syevj"); - CUSOLVER_CHECK_ERROR(cusolverDnXsyevjSetMaxSweeps(cusolverData.syevj_params, 15), - "Failed to set max sweeps for syevj"); - CUSOLVER_CHECK_ERROR(cusolverDnXsyevjSetSortEig(cusolverData.syevj_params, 1), - "Failed to set sorting of eigen values in syevj"); - CUSOLVER_CHECK_ERROR( - cusolverDnSsyevjBatched_bufferSize(cusolverData.cusolverH, CUSOLVER_EIG_MODE_VECTOR, CUBLAS_FILL_MODE_LOWER, 9, - NULL, 9, NULL, &(cusolverData.lwork), cusolverData.syevj_params, batchSize), - "Failed to calculate buffer size for syevj"); - cudaMalloc(reinterpret_cast(&(cusolverData.cusolverBuffer)), cusolverData.lwork * sizeof(float)); - cudaMalloc(reinterpret_cast(&(cusolverData.cusolverInfo)), batchSize * sizeof(int)); + cleanup(); } -FindHomography::~FindHomography() +void FindHomography::DeviceState::cleanup() noexcept { - cudaFree(bufferOffset.srcMean); - cudaFree(bufferOffset.dstMean); - cudaFree(bufferOffset.srcShiftSum); - cudaFree(bufferOffset.dstShiftSum); - cudaFree(bufferOffset.LtL); - cudaFree(bufferOffset.W); - cudaFree(bufferOffset.r); - cudaFree(bufferOffset.J); - cudaFree(bufferOffset.calc_buffer); - cusolverDnDestroySyevjInfo(cusolverData.syevj_params); - cusolverDnDestroy(cusolverData.cusolverH); - cudaFree(cusolverData.cusolverBuffer); - cudaFree(cusolverData.cusolverInfo); + (void)cudaFree(bufferOffset.srcMean); + bufferOffset.srcMean = nullptr; + (void)cudaFree(bufferOffset.dstMean); + bufferOffset.dstMean = nullptr; + (void)cudaFree(bufferOffset.srcShiftSum); + bufferOffset.srcShiftSum = nullptr; + (void)cudaFree(bufferOffset.dstShiftSum); + bufferOffset.dstShiftSum = nullptr; + (void)cudaFree(bufferOffset.LtL); + bufferOffset.LtL = nullptr; + (void)cudaFree(bufferOffset.W); + bufferOffset.W = nullptr; + (void)cudaFree(bufferOffset.r); + bufferOffset.r = nullptr; + (void)cudaFree(bufferOffset.J); + bufferOffset.J = nullptr; + (void)cudaFree(bufferOffset.calc_buffer); + bufferOffset.calc_buffer = nullptr; + + if (cusolverData.syevj_params != nullptr) + { + (void)cusolverDnDestroySyevjInfo(cusolverData.syevj_params); + cusolverData.syevj_params = nullptr; + } + if (cusolverData.cusolverH != nullptr) + { + (void)cusolverDnDestroy(cusolverData.cusolverH); + cusolverData.cusolverH = nullptr; + } + (void)cudaFree(cusolverData.cusolverBuffer); + cusolverData.cusolverBuffer = nullptr; + (void)cudaFree(cusolverData.cusolverInfo); + cusolverData.cusolverInfo = nullptr; + cusolverData.lwork = 0; +} + +FindHomography::FindHomography(int batchSize, int maxNumPoints) + : m_state([batchSize, maxNumPoints](int) { return std::make_unique(batchSize, maxNumPoints); }) +{ + if (batchSize <= 0) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "batchSize must be positive"); + } + if (maxNumPoints < 4) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "maxNumPoints must be at least 4"); + } } // Operator -------------------------------------------------------------------- @@ -1526,6 +1624,7 @@ FindHomography::~FindHomography() void FindHomography::operator()(cudaStream_t stream, const nvcv::Tensor &srcPoints, const nvcv::Tensor &dstPoints, const nvcv::Tensor &models) const { + CVCUDA_NVTX_RANGE("cvcuda::FindHomography::operator()[Tensor]"); auto srcData = srcPoints.exportData(); if (!srcData) { @@ -1547,18 +1646,21 @@ void FindHomography::operator()(cudaStream_t stream, const nvcv::Tensor &srcPoin "Input must be cuda-accessible, pitch-linear tensor"); } - RunFindHomography(*srcData, *dstData, *modelData, &bufferOffset, &cusolverData, stream); + auto &state = m_state.get(); + RunFindHomography(*srcData, *dstData, *modelData, &state.bufferOffset, &state.cusolverData, stream); } void FindHomography::operator()(cudaStream_t stream, const nvcv::TensorBatch &srcPoints, const nvcv::TensorBatch &dstPoints, const nvcv::TensorBatch &models) const { + CVCUDA_NVTX_RANGE("cvcuda::FindHomography::operator()[TensorBatch]"); if (!(srcPoints.numTensors() == dstPoints.numTensors() && srcPoints.numTensors() == models.numTensors())) { throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "source, destination and model tensors must have same batch size"); } + auto &state = m_state.get(); for (int b = 0; b < srcPoints.numTensors(); b++) { auto srcData = srcPoints[b].exportData(); @@ -1582,7 +1684,7 @@ void FindHomography::operator()(cudaStream_t stream, const nvcv::TensorBatch &sr "model must be cuda-accessible, pitch-linear tensor"); } - RunFindHomography(*srcData, *dstData, *modelData, &bufferOffset, &cusolverData, stream); + RunFindHomography(*srcData, *dstData, *modelData, &state.bufferOffset, &state.cusolverData, stream); } } diff --git a/src/cvcuda/priv/OpFindHomography.hpp b/src/cvcuda/priv/OpFindHomography.hpp index c18d3ef48..fa0a4f247 100644 --- a/src/cvcuda/priv/OpFindHomography.hpp +++ b/src/cvcuda/priv/OpFindHomography.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -24,6 +24,7 @@ #ifndef CVCUDA_PRIV__FIND_HOMOGRAPHY_HPP #define CVCUDA_PRIV__FIND_HOMOGRAPHY_HPP #include "IOperator.hpp" +#include "PerDeviceResource.hpp" #include #include @@ -34,7 +35,7 @@ #include #include -typedef struct +struct BufferOffsets { float2 *srcMean; float2 *dstMean; @@ -45,16 +46,16 @@ typedef struct float *r; float *J; float *calc_buffer; -} BufferOffsets; +}; -typedef struct +struct cuSolver { int *cusolverInfo; float *cusolverBuffer; cusolverDnHandle_t cusolverH; syevjInfo_t syevj_params; int lwork; -} cuSolver; +}; namespace cvcuda::priv { @@ -62,15 +63,28 @@ class FindHomography final : public IOperator { public: explicit FindHomography(int batchSize, int numPoints); - ~FindHomography(); void operator()(cudaStream_t stream, const nvcv::Tensor &src, const nvcv::Tensor &dst, const nvcv::Tensor &models) const; void operator()(cudaStream_t stream, const nvcv::TensorBatch &src, const nvcv::TensorBatch &dst, const nvcv::TensorBatch &models) const; private: - BufferOffsets bufferOffset; - cuSolver cusolverData; + struct DeviceState + { + BufferOffsets bufferOffset{}; + cuSolver cusolverData{}; + + DeviceState(int batchSize, int maxNumPoints); + ~DeviceState(); + + DeviceState(const DeviceState &) = delete; + DeviceState &operator=(const DeviceState &) = delete; + + private: + void cleanup() noexcept; + }; + + mutable PerDeviceResource m_state; }; } // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpFlip.cpp b/src/cvcuda/priv/OpFlip.cpp index 7428ba11d..72a0077ef 100644 --- a/src/cvcuda/priv/OpFlip.cpp +++ b/src/cvcuda/priv/OpFlip.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "OpFlip.hpp" +#include "Nvtx.hpp" #include "legacy/CvCudaLegacy.h" #include "legacy/CvCudaLegacyHelpers.hpp" @@ -27,15 +28,17 @@ namespace cvcuda::priv { namespace legacy = nvcv::legacy::cuda_op; -Flip::Flip(int32_t maxBatchSize) +Flip::Flip(int32_t) { - legacy::DataShape maxIn, maxOut; //maxIn/maxOut not used by op. + legacy::DataShape maxIn; + legacy::DataShape maxOut; //maxIn/maxOut not used by op. m_legacyOp = std::make_unique(maxIn, maxOut); m_legacyOpVarShape = std::make_unique(maxIn, maxOut); } void Flip::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, int32_t flipCode) const { + CVCUDA_NVTX_RANGE("cvcuda::Flip::operator()[Tensor]"); auto input = in.exportData(); if (input == nullptr) { @@ -56,6 +59,7 @@ void Flip::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::T void Flip::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, const nvcv::Tensor &flipCode) const { + CVCUDA_NVTX_RANGE("cvcuda::Flip::operator()[ImageBatchVarShape]"); auto input = in.exportData(stream); if (input == nullptr) { diff --git a/src/cvcuda/priv/OpGammaContrast.cpp b/src/cvcuda/priv/OpGammaContrast.cpp index c63f8b355..58f53edb1 100644 --- a/src/cvcuda/priv/OpGammaContrast.cpp +++ b/src/cvcuda/priv/OpGammaContrast.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "OpGammaContrast.hpp" +#include "Nvtx.hpp" #include "legacy/CvCudaLegacy.h" #include "legacy/CvCudaLegacyHelpers.hpp" @@ -28,13 +29,84 @@ namespace cvcuda::priv { namespace legacy = nvcv::legacy::cuda_op; GammaContrast::GammaContrast(const int32_t maxVarShapeBatchSize, const int32_t maxVarShapeChannelCount) + // Legacy operators are single-device by design. PerDeviceResource creates + // one instance per CUDA device for transparent multi-GPU support. The same max + // batch/channel limits bound both the tensor and var-shape paths' gamma scratch. + : m_legacyOp([maxVarShapeBatchSize, maxVarShapeChannelCount](int) + { return std::make_unique(maxVarShapeBatchSize, maxVarShapeChannelCount); }) + , m_legacyOpVarShape( + [maxVarShapeBatchSize, maxVarShapeChannelCount](int) + { return std::make_unique(maxVarShapeBatchSize, maxVarShapeChannelCount); }) { - m_legacyOpVarShape = std::make_unique(maxVarShapeBatchSize, maxVarShapeChannelCount); +} + +void GammaContrast::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, + const nvcv::Tensor &gamma) const +{ + auto inData = in.exportData(); + if (inData == nullptr) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input must be device-acessible, pitch-linear tensor"); + } + + auto outData = out.exportData(); + if (outData == nullptr) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Output must be device-acessible, pitch-linear tensor"); + } + + auto gammaData = gamma.exportData(); + if (gammaData == nullptr) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Gamma must be device-acessible, pitch-linear tensor"); + } + + NVCV_CHECK_THROW(m_legacyOp.get().infer(*inData, *outData, *gammaData, stream)); +} + +void GammaContrast::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, float gamma, + float gain, NVCVRoundMode roundMode) const +{ + auto inData = in.exportData(); + if (inData == nullptr) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input must be device-accessible, pitch-linear tensor"); + } + + auto outData = out.exportData(); + if (outData == nullptr) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Output must be device-accessible, pitch-linear tensor"); + } + + NVCV_CHECK_THROW(m_legacyOp.get().infer(*inData, *outData, gamma, gain, roundMode, stream)); } void GammaContrast::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, const nvcv::Tensor &gamma) const { + CVCUDA_NVTX_RANGE("cvcuda::GammaContrast::operator()[ImageBatchVarShape]"); + + if (in.numImages() != out.numImages()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input and output must have the same number of images"); + } + + for (int i = 0; i < in.numImages(); ++i) + { + if (in[i].size() != out[i].size()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input and output images must have matching width and height"); + } + } + auto inData = in.exportData(stream); if (inData == nullptr) { @@ -56,7 +128,7 @@ void GammaContrast::operator()(cudaStream_t stream, const nvcv::ImageBatchVarSha "Gamma must be device-acessible, pitch-linear tensor"); } - NVCV_CHECK_THROW(m_legacyOpVarShape->infer(*inData, *outData, *gammaData, stream)); + NVCV_CHECK_THROW(m_legacyOpVarShape.get().infer(*inData, *outData, *gammaData, stream)); } } // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpGammaContrast.hpp b/src/cvcuda/priv/OpGammaContrast.hpp index 4f45cc575..257ca532d 100644 --- a/src/cvcuda/priv/OpGammaContrast.hpp +++ b/src/cvcuda/priv/OpGammaContrast.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,13 +25,12 @@ #define CVCUDA_PRIV_GAMMA_CONTRAST_HPP #include "IOperator.hpp" +#include "PerDeviceResource.hpp" #include "legacy/CvCudaLegacy.h" #include #include -#include - // Use the public nvcv API namespace cvcuda::priv { @@ -40,11 +39,18 @@ class GammaContrast final : public IOperator public: explicit GammaContrast(const int32_t maxVarShapeBatchSize, const int32_t maxVarShapeChannelCount); + void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, + const nvcv::Tensor &gamma) const; + + void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, float gamma, float gain, + NVCVRoundMode roundMode) const; + void operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, const nvcv::Tensor &gamma) const; private: - std::unique_ptr m_legacyOpVarShape; + mutable PerDeviceResource m_legacyOp; + mutable PerDeviceResource m_legacyOpVarShape; }; } // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpGaussian.cpp b/src/cvcuda/priv/OpGaussian.cpp index 643d2e168..724767be8 100644 --- a/src/cvcuda/priv/OpGaussian.cpp +++ b/src/cvcuda/priv/OpGaussian.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "OpGaussian.hpp" +#include "Nvtx.hpp" #include "legacy/CvCudaLegacy.h" #include "legacy/CvCudaLegacyHelpers.hpp" @@ -28,15 +29,29 @@ namespace cvcuda::priv { namespace legacy = nvcv::legacy::cuda_op; Gaussian::Gaussian(nvcv::Size2D maxKernelSize, int maxBatchSize) + // Legacy operators are single-device by design. PerDeviceResource creates + // one instance per CUDA device for transparent multi-GPU support. + : m_legacyOp( + [maxKernelSize](int) + { + legacy::DataShape maxIn; + legacy::DataShape maxOut; + return std::make_unique(maxIn, maxOut, maxKernelSize); + }) + , m_legacyOpVarShape( + [maxKernelSize, maxBatchSize](int) + { + legacy::DataShape maxIn; + legacy::DataShape maxOut; + return std::make_unique(maxIn, maxOut, maxKernelSize, maxBatchSize); + }) { - legacy::DataShape maxIn, maxOut; //maxIn/maxOut not used by op. - m_legacyOp = std::make_unique(maxIn, maxOut, maxKernelSize); - m_legacyOpVarShape = std::make_unique(maxIn, maxOut, maxKernelSize, maxBatchSize); } void Gaussian::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, nvcv::Size2D kernelSize, double2 sigma, NVCVBorderType borderMode) const { + CVCUDA_NVTX_RANGE("cvcuda::Gaussian::operator()[Tensor]"); auto inData = in.exportData(); if (inData == nullptr) { @@ -51,12 +66,13 @@ void Gaussian::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvc "Output must be cuda-accessible, pitch-linear tensor"); } - NVCV_CHECK_THROW(m_legacyOp->infer(*inData, *outData, kernelSize, sigma, borderMode, stream)); + NVCV_CHECK_THROW(m_legacyOp.get().infer(*inData, *outData, kernelSize, sigma, borderMode, stream)); } void Gaussian::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, const nvcv::Tensor &kernelSize, const nvcv::Tensor &sigma, NVCVBorderType borderMode) const { + CVCUDA_NVTX_RANGE("cvcuda::Gaussian::operator()[ImageBatchVarShape]"); auto inData = in.exportData(stream); if (inData == nullptr) { @@ -85,7 +101,8 @@ void Gaussian::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &i "Kernel sigma must be cuda-accessible, pitch-linear tensor"); } - NVCV_CHECK_THROW(m_legacyOpVarShape->infer(*inData, *outData, *kernelSizeData, *sigmaData, borderMode, stream)); + NVCV_CHECK_THROW( + m_legacyOpVarShape.get().infer(*inData, *outData, *kernelSizeData, *sigmaData, borderMode, stream)); } } // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpGaussian.hpp b/src/cvcuda/priv/OpGaussian.hpp index b61c20c03..fa58ac67c 100644 --- a/src/cvcuda/priv/OpGaussian.hpp +++ b/src/cvcuda/priv/OpGaussian.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,13 +25,12 @@ #define CVCUDA_PRIV_GAUSSIAN_HPP #include "IOperator.hpp" +#include "PerDeviceResource.hpp" #include "legacy/CvCudaLegacy.h" #include #include -#include - namespace cvcuda::priv { class Gaussian final : public IOperator @@ -46,8 +45,8 @@ class Gaussian final : public IOperator const nvcv::Tensor &kernelSize, const nvcv::Tensor &sigma, NVCVBorderType borderMode) const; private: - std::unique_ptr m_legacyOp; - std::unique_ptr m_legacyOpVarShape; + mutable PerDeviceResource m_legacyOp; + mutable PerDeviceResource m_legacyOpVarShape; }; } // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpGaussianNoise.cpp b/src/cvcuda/priv/OpGaussianNoise.cpp index 0899ec2e3..e8c531132 100644 --- a/src/cvcuda/priv/OpGaussianNoise.cpp +++ b/src/cvcuda/priv/OpGaussianNoise.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "OpGaussianNoise.hpp" +#include "Nvtx.hpp" #include "legacy/CvCudaLegacy.h" #include "legacy/CvCudaLegacyHelpers.hpp" @@ -28,17 +29,34 @@ namespace cvcuda::priv { namespace legacy = nvcv::legacy::cuda_op; GaussianNoise::GaussianNoise(int maxBatchSize) + // Legacy operators are single-device by design. PerDeviceResource creates + // one instance per CUDA device for transparent multi-GPU support. + : m_legacyOp( + [maxBatchSize](int) + { + legacy::DataShape maxIn; + legacy::DataShape maxOut; + return std::make_unique(maxIn, maxOut, maxBatchSize); + }) + , m_legacyOpVarShape( + [maxBatchSize](int) + { + legacy::DataShape maxIn; + legacy::DataShape maxOut; + return std::make_unique(maxIn, maxOut, maxBatchSize); + }) { - legacy::DataShape maxIn, maxOut; - // maxIn/maxOut not used by op. - m_legacyOp = std::make_unique(maxIn, maxOut, maxBatchSize); - m_legacyOpVarShape = std::make_unique(maxIn, maxOut, maxBatchSize); + if (maxBatchSize < 0) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "maxBatchSize must be >= 0"); + } } void GaussianNoise::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, const nvcv::Tensor &mu, const nvcv::Tensor &sigma, bool per_channel, unsigned long long seed) const { + CVCUDA_NVTX_RANGE("cvcuda::GaussianNoise::operator()[Tensor]"); auto inData = in.exportData(); if (inData == nullptr) { @@ -66,13 +84,35 @@ void GaussianNoise::operator()(cudaStream_t stream, const nvcv::Tensor &in, cons "sigma must be cuda-accessible, pitch-linear tensor"); } - NVCV_CHECK_THROW(m_legacyOp->infer(*inData, *outData, *muData, *sigmaData, per_channel, seed, stream)); + NVCV_CHECK_THROW(m_legacyOp.get().infer(*inData, *outData, *muData, *sigmaData, per_channel, seed, stream)); +} + +void GaussianNoise::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, float mu, + float sigma, bool per_channel, unsigned long long seed, bool reseed, bool clip) const +{ + CVCUDA_NVTX_RANGE("cvcuda::GaussianNoise::operator()[Tensor scalar]"); + auto inData = in.exportData(); + if (inData == nullptr) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input must be cuda-accessible, pitch-linear tensor"); + } + + auto outData = out.exportData(); + if (outData == nullptr) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Output must be cuda-accessible, pitch-linear tensor"); + } + + NVCV_CHECK_THROW(m_legacyOp.get().infer(*inData, *outData, mu, sigma, per_channel, seed, reseed, clip, stream)); } void GaussianNoise::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, const nvcv::Tensor &mu, const nvcv::Tensor &sigma, bool per_channel, unsigned long long seed) const { + CVCUDA_NVTX_RANGE("cvcuda::GaussianNoise::operator()[ImageBatchVarShape]"); auto inData = in.exportData(stream); if (inData == nullptr) { @@ -98,7 +138,7 @@ void GaussianNoise::operator()(cudaStream_t stream, const nvcv::ImageBatchVarSha "sigma must be cuda-accessible, pitch-linear tensor"); } - NVCV_CHECK_THROW(m_legacyOpVarShape->infer(*inData, *outData, *muData, *sigmaData, per_channel, seed, stream)); + NVCV_CHECK_THROW(m_legacyOpVarShape.get().infer(*inData, *outData, *muData, *sigmaData, per_channel, seed, stream)); } } // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpGaussianNoise.hpp b/src/cvcuda/priv/OpGaussianNoise.hpp index 92d802be9..bdab081c6 100644 --- a/src/cvcuda/priv/OpGaussianNoise.hpp +++ b/src/cvcuda/priv/OpGaussianNoise.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,13 +25,12 @@ #define CVCUDA_PRIV_GAUSSIAN_NOISE_HPP #include "IOperator.hpp" +#include "PerDeviceResource.hpp" #include "legacy/CvCudaLegacy.h" #include #include -#include - namespace cvcuda::priv { class GaussianNoise final : public IOperator @@ -42,12 +41,15 @@ class GaussianNoise final : public IOperator void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, const nvcv::Tensor &mu, const nvcv::Tensor &sigma, bool per_channel, unsigned long long seed) const; + void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, float mu, float sigma, + bool per_channel, unsigned long long seed, bool reseed, bool clip) const; + void operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, const nvcv::Tensor &mu, const nvcv::Tensor &sigma, bool per_channel, unsigned long long seed) const; private: - std::unique_ptr m_legacyOp; - std::unique_ptr m_legacyOpVarShape; + mutable PerDeviceResource m_legacyOp; + mutable PerDeviceResource m_legacyOpVarShape; }; } // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpHQResize.cu b/src/cvcuda/priv/OpHQResize.cu index dfdc8bb03..da7dbbfdb 100644 --- a/src/cvcuda/priv/OpHQResize.cu +++ b/src/cvcuda/priv/OpHQResize.cu @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,2679 +15,94 @@ * limitations under the License. */ +#include "Nvtx.hpp" #include "OpHQResize.hpp" -#include "WorkspaceUtil.hpp" -#include "cvcuda/Workspace.hpp" +#include "OpHQResizeDispatch.hpp" -#include "OpHQResizeBatchWrap.cuh" -#include "OpHQResizeFilter.cuh" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -namespace { - -namespace cuda = nvcv::cuda; -namespace filter = cvcuda::priv::hq_resize::filter; -namespace batch_wrapper = cvcuda::priv::hq_resize::batch_wrapper; - -template -using Vec = typename cuda::MakeType; - -template -using VecI = Vec; - -template -using VecF = Vec; - -namespace utils { - -template>> -inline std::enable_if_t>, int64_t> Volume(const T &v) -{ - int64_t vol = 1; - for (int i = 0; i < cuda::NumComponents; i++) - { - vol *= cuda::GetElement(v, i); - } - return vol; -} - -template>> -auto DivCeil(const T &a, const T &b) -{ - return (a + b - 1) / b; -} -} // namespace utils - -namespace resampling { - -template -struct SampleDesc -{ - static constexpr int kSpatialNDim = _kSpatialNDim; - - // input, output and the intermediate buffers - static constexpr int kNumBuffers = kSpatialNDim + 1; - - // shapes[0] - input shape, consecutive intermediate results shapes, - // shapes[kSpatialNDim] - output shape - VecI shapes[kNumBuffers]; - - // the number of channels in the sample, common for input, - // intermediate and output sample - int channels; - - // describes which axis to processes in a given resampling pass, e.g. - // if processingOrder.x = 2, then in the first pass the z axis - // will be resampled - VecI processingOrder; - - // resampling origin and scale in pass order, i.e. - // origin.x and scale.x describe origin and scale for resampling - // in the first pass - VecF origin, scale; - - // what type of filter to use (NN, Linear, Support based) - // in pass order (i.e. filterKind[0] refers to filter used in the first pass) - filter::FilterTypeKind filterKind[kSpatialNDim]; - - // filter description (support, coefficients etc.) - // in pass order (i.e. filter[0] refers to filter used in the first pass) - filter::ResamplingFilter filter[kSpatialNDim]; - - // spatial offset in the input sample based on the input ROI - // and filter support - VecI inRoiOffset; - - // describes the logical block shape, i.e. a size of a slice - // that a single gpu block will process in a given pass - VecI blockShape[kSpatialNDim]; -}; - -/** - * @brief Helper structure to indicate the static number of channels - * dynamic number of channels that may differ between samples. - */ -template -struct NumChannels -{ - constexpr int __forceinline__ __device__ operator()() const - { - return kStaticChannels; - } - - static constexpr bool kHasStaticChannels = true; - static constexpr int kStaticChannels = _kStaticChannels; -}; - -template<> -struct NumChannels<-1> -{ - int __forceinline__ __device__ operator()() const - { - return dynamicChannels; - } - - static constexpr bool kHasStaticChannels = false; - static constexpr int kStaticChannels = -1; - int dynamicChannels; -}; - -template -__forceinline__ __device__ void WithChannels(const int dynamicChannels, Cb &&cb) -{ - if constexpr (kNumStaticChannels == -1) - { - cb(NumChannels<-1>{dynamicChannels}); - } - else if constexpr (kNumStaticChannels != -1) - { - static_assert(kNumStaticChannels > 0); - cb(NumChannels{}); - } -} - -/** - * @brief Each threadblock will cover `lanes * volume(blockDim)` - * elements of the output sample. More lanes result in: - * 1. smaller grid launched (possibly reducing parallelism for small images), - * 2. better resuing of the filter's coefficients - * (they are computed once for all lanes). - * - * @return int - the number of lanes for a single threadblock - * to cover in the output image - */ -inline int GetResizeBlockLanesEnv() -{ - char *env = getenv("CVCUDA_HQ_RESIZE_BLOCK_LANES"); - if (env) - { - int lanes = atoi(env); - if (lanes < 1) - { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, - "The CVCUDA_HQ_RESIZE_BLOCK_LANES must be a positive integer"); - } - return lanes; - } - else - { - return 8; - } -} - -inline int GetResizeBlockLanes() -{ - static int lanes = GetResizeBlockLanesEnv(); - return lanes; -} - -template -struct GridHelperDevice -{ -}; - -/** - * @brief Maps cuda blockIdx to sample and bounds of the sample region - * to be processed be the threadblock for 2D resampling - */ -template<> -struct GridHelperDevice<2> -{ - GridHelperDevice(VecI<2> numBlocks) - : m_numBlocksX{numBlocks.x} - { - } - - int __forceinline__ __device__ CurrentSample() const - { - return blockIdx.y; - } - - void __forceinline__ __device__ CurrentBlock(VecI<2> &lo, VecI<2> &hi, const VecI<2> blockShape) const - - { - VecI<2> currentBlock; - { - int block = blockIdx.x; - currentBlock.x = block % m_numBlocksX; - currentBlock.y = block / m_numBlocksX; - } - lo = blockShape * currentBlock; - hi = lo + blockShape; - } - -private: - int m_numBlocksX; -}; - -/** - * @brief Maps cuda blockIdx to sample and bounds of the sample region - * to be processed be the threadblock for 3D resampling - */ -template<> -struct GridHelperDevice<3> -{ - GridHelperDevice(VecI<3> numBlocks) - : m_numBlocksX{numBlocks.x} - , m_numBlocksY{numBlocks.y} - { - } - - int __forceinline__ __device__ CurrentSample() const - { - return blockIdx.y; - } - - void __forceinline__ __device__ CurrentBlock(VecI<3> &lo, VecI<3> &hi, const VecI<3> blockShape) const - - { - VecI<3> currentBlock; - { - int block = blockIdx.x; - currentBlock.x = block % m_numBlocksX; - block = block / m_numBlocksX; - currentBlock.y = block % m_numBlocksY; - currentBlock.z = block / m_numBlocksY; - } - lo = blockShape * currentBlock; - hi = lo + blockShape; - } - -private: - int m_numBlocksX, m_numBlocksY; -}; - -/** - * @brief Maps the logical blocks and the number of samples into cuda grid and back. - */ -template -struct GridHelper -{ - GridHelper(VecI numBlocks, int numSamples) - : m_numBlocks{numBlocks} - , m_numSamples{numSamples} - { - } - - template - std::enable_if_t GetKernelGrid() const - { - static_assert(kSpatialNDim == 2); - return dim3(m_numBlocks.x * m_numBlocks.y, m_numSamples, 1); - } - - template - std::enable_if_t GetKernelGrid() const - { - static_assert(kSpatialNDim == 3); - return dim3(m_numBlocks.x * m_numBlocks.y * m_numBlocks.z, m_numSamples, 1); - } - - GridHelperDevice GetDeviceGridHelper() - { - return {m_numBlocks}; - } - -private: - VecI m_numBlocks; - int m_numSamples; -}; - -// The namespace contains implementation of different resampling -// methods in device code. -namespace interpolate { - -template -auto __forceinline__ __device__ GetWrapPtr(const Wrap wrap, const VecI<2> yx, const Idxs... idxs) -{ - return wrap.ptr(yx.y, yx.x, idxs...); -} - -template -auto __forceinline__ __device__ GetWrapPtr(const Wrap wrap, const VecI<3> zyx, const Idxs... idxs) -{ - return wrap.ptr(zyx.z, zyx.y, zyx.x, idxs...); -} - -template -std::enable_if_t __forceinline__ __device__ - LoadPixelLdg(const Wrap wrap, const NumChannelsT numChannels, const Idxs... idxs) -{ - using T = std::remove_const_t; - using BT = cuda::BaseType; - constexpr int kStaticChannels = NumChannelsT::kStaticChannels; - static_assert(kStaticChannels == cuda::NumElements); - - constexpr bool kSupportsLdg = kStaticChannels == 2 || kStaticChannels == 4; - - if constexpr (kSupportsLdg) - { - return __ldg(GetWrapPtr(wrap, idxs...)); - } - else if constexpr (!kSupportsLdg) - { - const BT *basePtr = reinterpret_cast(GetWrapPtr(wrap, idxs...)); - T value; -#pragma unroll - for (int c = 0; c < kStaticChannels; c++) - { - cuda::GetElement(value, c) = __ldg(basePtr + c); - } - return value; - } -} - -template -std::enable_if_t __forceinline__ __device__ - LoadPixelLdg(const Wrap wrap, const NumChannelsT numChannels, const Idxs... idxs) -{ - static_assert(!cuda::IsCompound); - return __ldg(GetWrapPtr(wrap, idxs...)); -} - -namespace nn { - -template -void __forceinline__ __device__ ForAllPixels(const VecI<2> lo, const VecI<2> hi, ProcessPixel &&processPixel) -{ - for (int y = lo.y + threadIdx.y; y < hi.y; y += blockDim.y) - { - for (int x = lo.x + threadIdx.x; x < hi.x; x += blockDim.x) - { - processPixel(VecI<2>{x, y}); - } - } -} - -template -void __forceinline__ __device__ ForAllPixels(const VecI<3> lo, const VecI<3> hi, ProcessPixel &&processPixel) -{ - for (int z = lo.z + threadIdx.z; z < hi.z; z += blockDim.z) - { - for (int y = lo.y + threadIdx.y; y < hi.y; y += blockDim.y) - { - for (int x = lo.x + threadIdx.x; x < hi.x; x += blockDim.x) - { - processPixel(VecI<3>{x, y, z}); - } - } - } -} - -/** - * @brief Nearest neighbor resampling - * - * @param outWrap - the wrapper for accessing output data - * @param inWrap - the wrapper for accessing input data - * @param lo - inclusive lower bound output coordinates of the block processed by the threadblock - * @param hi - exclusive upper bound output coordinates of the block processed by the threadblock - * @param origin - source coordinates corresponding to output's (0, 0) - * @param scale - step, in source coordinates, for one pixel in output coordinates - * @param inShape - shape of the input (x, y) order - * @param numChannels - the NumChannels specialization describing the number of interleaved - * channels in the input and output sample. - */ -template -void __forceinline__ __device__ Resample(const PassOutWrap outWrap, const PassInWrap inWrap, - const VecI lo, const VecI hi, - VecF origin, const VecF scale, - const VecI inShape, const NumChannelsT numChannels) -{ - using OutT = typename PassOutWrap::ValueType; - using InT = typename PassInWrap::ValueType; - // spatial extents and optional channels extent - constexpr int kNDim = kSpatialNDim + !NumChannelsT::kHasStaticChannels; - - static_assert(!NumChannelsT::kHasStaticChannels || NumChannelsT::kStaticChannels == cuda::NumElements); - static_assert(cuda::NumElements == cuda::NumElements); - static_assert(PassOutWrap::kNumDimensions == kNDim); - static_assert(PassInWrap::kNumDimensions == kNDim); - - origin += 0.5f * scale; - ForAllPixels(lo, hi, - [=](const VecI outIdxs) - { - VecI inIdxs = cuda::round(outIdxs * scale + origin); - inIdxs = cuda::clamp(inIdxs, cuda::SetAll>(0), inShape - 1); - - if constexpr (NumChannelsT::kHasStaticChannels) - { - const InT in = LoadPixelLdg(inWrap, numChannels, inIdxs); - OutT &out = *GetWrapPtr(outWrap, outIdxs); - out = cuda::SaturateCast(in); - } - else if constexpr (!NumChannelsT::kHasStaticChannels) - { - for (int c = 0; c < numChannels(); c++) - { - const InT in = LoadPixelLdg(inWrap, numChannels, inIdxs, c); - OutT &out = *GetWrapPtr(outWrap, outIdxs, c); - out = cuda::SaturateCast(in); - } - } - }); -} - -} // namespace nn - -namespace linear { - -template -void __forceinline__ __device__ Linear(const PassOutWrap outWrap, const PassInWrap inWrap, - const NumChannelsT numChannels, const VecI inIdx0, - const VecI inIdx1, const float q, const VecI outIdx) -{ - using OutT = typename PassOutWrap::ValueType; - using InT = std::remove_const_t; - // spatial extents and optional channels extent - constexpr int kNDim = kSpatialNDim + !NumChannelsT::kHasStaticChannels; - - static_assert(!NumChannelsT::kHasStaticChannels || NumChannelsT::kStaticChannels == cuda::NumElements); - static_assert(cuda::NumElements == cuda::NumElements); - static_assert(PassOutWrap::kNumDimensions == kNDim); - static_assert(PassInWrap::kNumDimensions == kNDim); - - if constexpr (NumChannelsT::kHasStaticChannels) - { - using FloatT = cuda::ConvertBaseTypeTo; - const FloatT a = cuda::StaticCast(LoadPixelLdg(inWrap, numChannels, inIdx0)); - const FloatT b = cuda::StaticCast(LoadPixelLdg(inWrap, numChannels, inIdx1)); - FloatT tmp = b - a; -#pragma unroll - for (int c = 0; c < NumChannelsT::kStaticChannels; c++) - { - cuda::GetElement(tmp, c) = fmaf(cuda::GetElement(tmp, c), q, cuda::GetElement(a, c)); - } - OutT &out = *GetWrapPtr(outWrap, outIdx); - out = cuda::SaturateCast(tmp); - } - else if constexpr (!NumChannelsT::kHasStaticChannels) - { - for (int c = 0; c < numChannels(); c++) - { - const float a = LoadPixelLdg(inWrap, numChannels, inIdx0, c); - const float b = LoadPixelLdg(inWrap, numChannels, inIdx1, c); - const float tmp = fmaf(b - a, q, a); - OutT &out = *GetWrapPtr(outWrap, outIdx, c); - out = cuda::SaturateCast(tmp); - } - } -} - -template -void __forceinline__ __device__ ForAllPixelsHorz(const VecI<2> lo, const VecI<2> hi, ProcessPixel &&processPixel) -{ - for (int x = lo.x + threadIdx.x; x < hi.x; x += blockDim.x) - { - for (int y = threadIdx.y + lo.y; y < hi.y; y += blockDim.y) - { - processPixel(VecI<2>{x, y}); - } - } -} - -template -void __forceinline__ __device__ ForAllPixelsHorz(const VecI<3> lo, const VecI<3> hi, ProcessPixel &&processPixel) -{ - for (int x = lo.x + threadIdx.x; x < hi.x; x += blockDim.x) - { - for (int z = threadIdx.z + lo.z; z < hi.z; z += blockDim.z) - { - for (int y = threadIdx.y + lo.y; y < hi.y; y += blockDim.y) - { - processPixel(VecI<3>{x, y, z}); - } - } - } -} - -template -void __forceinline__ __device__ ForAllPixelsVert(const VecI<2> lo, const VecI<2> hi, ProcessPixel &&processPixel) -{ - for (int y = threadIdx.y + lo.y; y < hi.y; y += blockDim.y) - { - for (int x = lo.x + threadIdx.x; x < hi.x; x += blockDim.x) - { - processPixel(VecI<2>{x, y}); - } - } -} - -template -void __forceinline__ __device__ ForAllPixelsVert(const VecI<3> lo, const VecI<3> hi, ProcessPixel &&processPixel) -{ - for (int z = threadIdx.z + lo.z; z < hi.z; z += blockDim.z) - { - for (int y = threadIdx.y + lo.y; y < hi.y; y += blockDim.y) - { - for (int x = lo.x + threadIdx.x; x < hi.x; x += blockDim.x) - { - processPixel(VecI<3>{x, y, z}); - } - } - } -} - -/** - * @brief Implements horizontal resampling - * - * @param outWrap - the wrapper for accessing output data - * @param inWrap - the wrapper for accessing input data - * @param lo - inclusive lower bound output coordinates of the block processed by the threadblock - * @param hi - exclusive upper bound output coordinates of the block processed by the threadblock - * @param srcX0 - X coordinate in the source image corresponding to output 0 - * @param scale - step, in source X, for one pixel in output X (may be negative) - * @param inShape - shape of the input (x, y[, z]) order - * @param numChannels - the NumChannels specialization describing the number of interleaved - * channels in the input and output sample. - * - * The input region of interest is defined in terms of origin/scale, which are relative to - * output (0, 0). - * The lo/hi parameters are not output RoI - they merely indicate the output slice processed - * by current block. - */ -template -void __forceinline__ __device__ ResampleHorz(const PassOutWrap outWrap, const PassInWrap inWrap, - const VecI lo, const VecI hi, float srcX0, - const float scale, const VecI inShape, - const NumChannelsT numChannels) -{ - srcX0 += 0.5f * scale - 0.5f; - ForAllPixelsHorz(lo, hi, - [=](const VecI outIdx) - { - const float sx0f = outIdx.x * scale + srcX0; - const int sx0i = cuda::round(sx0f); - const float q = sx0f - sx0i; - const int sx0 = cuda::clamp(sx0i, 0, inShape.x - 1); - const int sx1 = cuda::clamp(sx0i + 1, 0, inShape.x - 1); - - VecI inIdx0 = outIdx; - VecI inIdx1 = outIdx; - inIdx0.x = sx0; - inIdx1.x = sx1; - - Linear(outWrap, inWrap, numChannels, inIdx0, inIdx1, q, outIdx); - }); -} - -/** - * @brief Implements vertical resampling - * - * @param outWrap - the wrapper for accessing output data - * @param inWrap - the wrapper for accessing input data - * @param lo - inclusive lower bound output coordinates of the block processed by the threadblock - * @param hi - exclusive upper bound output coordinates of the block processed by the threadblock - * @param srcY0 - Y coordinate in the source image corresponding to output 0 - * @param scale - step, in source Y, for one pixel in output Y (may be negative) - * @param inShape - shape of the input (x, y[, z]) order - * @param numChannels - the NumChannels specialization describing the number of interleaved - * channels in the input and output sample. - */ -template -void __forceinline__ __device__ ResampleVert(const PassOutWrap outWrap, const PassInWrap inWrap, - const VecI lo, const VecI hi, float srcY0, - const float scale, const VecI inShape, - const NumChannelsT numChannels) -{ - srcY0 += 0.5f * scale - 0.5f; - ForAllPixelsVert(lo, hi, - [=](const VecI outIdx) - { - const float sy0f = outIdx.y * scale + srcY0; - const int sy0i = cuda::round(sy0f); - const float q = sy0f - sy0i; - const int sy0 = cuda::clamp(sy0i, 0, inShape.y - 1); - const int sy1 = cuda::clamp(sy0i + 1, 0, inShape.y - 1); - - VecI inIdx0 = outIdx; - VecI inIdx1 = outIdx; - inIdx0.y = sy0; - inIdx1.y = sy1; - - Linear(outWrap, inWrap, numChannels, inIdx0, inIdx1, q, outIdx); - }); -} - -/** - * @brief Implements depthwise resampling - * - * @param outWrap - the wrapper for accessing output data - * @param inWrap - the wrapper for accessing input data - * @param lo - inclusive lower bound output coordinates of the block processed by the threadblock - * @param hi - exclusive upper bound output coordinates of the block processed by the threadblock - * @param srcZ0 - Z coordinate in the source image corresponding to output's 0 - * @param scale - step, in source Z, for one pixel in output Z (may be negative) - * @param inShape - shape of the input (x, y[, z]) order - * @param numChannels - the NumChannels specialization describing the number of interleaved - * channels in the input and output sample. - */ -template -void __forceinline__ __device__ ResampleDepth(const PassOutWrap outWrap, const PassInWrap inWrap, const VecI<3> lo, - const VecI<3> hi, float srcZ0, const float scale, const VecI<3> inShape, - const NumChannelsT numChannels) -{ - srcZ0 += 0.5f * scale - 0.5f; - // threadIdx.y is used to traverse Z axis - for (int z = lo.z + threadIdx.y; z < hi.z; z += blockDim.y) - { - const float sz0f = z * scale + srcZ0; - const int sz0i = cuda::round(sz0f); - const float q = sz0f - sz0i; - const int sz0 = cuda::clamp(sz0i, 0, inShape.z - 1); - const int sz1 = cuda::clamp(sz0i + 1, 0, inShape.z - 1); - - for (int y = lo.y + threadIdx.z; y < hi.y; y += blockDim.z) - { - for (int x = lo.x + threadIdx.x; x < hi.x; x += blockDim.x) - { - VecI<3> inIdx0{x, y, sz0}; - VecI<3> inIdx1{x, y, sz1}; - VecI<3> outIdx{x, y, z}; - Linear<3>(outWrap, inWrap, numChannels, inIdx0, inIdx1, q, outIdx); - } - } - } -} - -} // namespace linear - -namespace filter_support { - -constexpr int kMaxGPUFilterSupport = 8192; - -bool __forceinline__ __host__ __device__ CanComputeCoefPerThread(const int support, const int resamplingAxisBlockSize) -{ - return support * resamplingAxisBlockSize <= kMaxGPUFilterSupport; -} - -inline int RequiredSharedMemoryElements(const int support, const int resamplingAxisBlockSize) -{ - if (CanComputeCoefPerThread(support, resamplingAxisBlockSize)) - { - return support * resamplingAxisBlockSize; - } - else - { - return support; - } -} - -template -void __forceinline__ __device__ ForAllOrthogonalToHorz(const VecI<2> lo, const VecI<2> hi, ProcessPixel &&processPixel) -{ - for (int y = threadIdx.y + lo.y; y < hi.y; y += blockDim.y) - { - processPixel(VecI<2>{0, y}); - } -} - -template -void __forceinline__ __device__ ForAllOrthogonalToHorz(const VecI<3> lo, const VecI<3> hi, ProcessPixel &&processPixel) -{ - for (int z = threadIdx.z + lo.z; z < hi.z; z += blockDim.z) - { - for (int y = threadIdx.y + lo.y; y < hi.y; y += blockDim.y) - { - processPixel(VecI<3>{0, y, z}); - } - } -} - -template -void __forceinline__ __device__ ForAllOrthogonalToVert(const VecI<2> lo, const VecI<2> hi, ProcessPixel &&processPixel) -{ - for (int x = threadIdx.x + lo.x; x < hi.x; x += blockDim.x) - { - processPixel(VecI<2>{x, 0}); - } -} - -template -void __forceinline__ __device__ ForAllOrthogonalToVert(const VecI<3> lo, const VecI<3> hi, ProcessPixel &&processPixel) -{ - for (int z = threadIdx.z + lo.z; z < hi.z; z += blockDim.z) - { - for (int x = threadIdx.x + lo.x; x < hi.x; x += blockDim.x) - { - processPixel(VecI<3>{x, 0, z}); - } - } -} - -/** - * @brief Implements horizontal resampling - * - * @param outWrap - the wrapper for accessing output data - * @param inWrap - the wrapper for accessing input data - * @param lo - inclusive lower bound output coordinates of the block processed by the threadblock - * @param hi - exclusive upper bound output coordinates of the block processed by the threadblock - * @param srcX0 - X coordinate in the source image corresponding to output's 0 - * @param scale - step, in source X, for one pixel in output X (may be negative) - * @param support - size of the resampling kernel, in source pixels - * @param numChannels - the NumChannels specialization describing the number of interleaved - * channels in the input and output sample. - * - * The function fills the output in block-sized vertical spans. - * Block horizontal size is warp-aligned. - * Filter coefficients are pre-calculated for each vertical span to avoid - * recalculating them for each row, and stored in a shared memory block. - * - * The function follows different code paths for static and dynamic number of channels. - * For the dynamic, the innermost loop goes over filter taps, which eliminates the need - * for thread-local memory to store intermediate sums. This allows processing arbitrary - * number of channels. - * For static number of channels, the run-time parameter `channels` is ignored and - * there's also a local temporary storage for a tap sum for each channel. This is faster, - * but requires extra registers for the intermediate sums. - */ -template -void __forceinline__ __device__ ResampleHorz(const PassOutWrap outWrap, const PassInWrap inWrap, - const VecI lo, const VecI hi, float srcX0, - const float scale, const VecI inShape, - const filter::ResamplingFilter filter, const NumChannelsT numChannels) -{ - extern __shared__ float coeffs[]; - - using OutT = typename PassOutWrap::ValueType; - using InT = std::remove_const_t; - // spatial extents and optional channels extent - constexpr int kNDim = kSpatialNDim + !NumChannelsT::kHasStaticChannels; - - static_assert(!NumChannelsT::kHasStaticChannels || NumChannelsT::kStaticChannels == cuda::NumElements); - static_assert(cuda::NumElements == cuda::NumElements); - static_assert(PassOutWrap::kNumDimensions == kNDim); - static_assert(PassInWrap::kNumDimensions == kNDim); - - const int support = filter.support(); - const float filterStep = filter.scale; - // If the support is small enough (for blockDim.x = 32 and kMaxGPUFilterSupport = 8192, it's 256), - // we can fit `support` x `blockDim.x` elements into shm, so that for each output_x mapped to input_x, - // we take into account the exact error that comes from rounding the input_x from float to integer. - // For larger supports, we just compute `support` elements common for all threads. - const bool hugeSupport = !CanComputeCoefPerThread(support, blockDim.x); - const int coeffBase = hugeSupport ? 0 : threadIdx.x; - const int coeffStride = hugeSupport ? 1 : blockDim.x; - - srcX0 += 0.5f * scale - 0.5f - filter.anchor; - - for (int j = lo.x; j < hi.x; j += blockDim.x) - { - const int x = j + threadIdx.x; - const float sx0f = x * scale + srcX0; - const int sx0 = hugeSupport ? cuda::round(sx0f) - : cuda::round(sx0f); - const float f = (sx0 - sx0f) * filterStep; - __syncthreads(); - if (hugeSupport) - { - for (int k = threadIdx.x + blockDim.x * threadIdx.y; k < support; k += blockDim.x * blockDim.y) - { - float flt = filter(f + k * filterStep); - coeffs[k] = flt; - } - } - else - { - for (int k = threadIdx.y; k < support; k += blockDim.y) - { - float flt = filter(f + k * filterStep); - coeffs[coeffBase + coeffStride * k] = flt; - } - } - __syncthreads(); - - if (x >= hi.x) - continue; - - float norm = 0; - for (int k = 0; k < support; k++) - { - norm += coeffs[coeffBase + coeffStride * k]; - } - norm = 1.0f / norm; - - ForAllOrthogonalToHorz( - lo, hi, - [=](VecI outIdx) - { - VecI inIdx = outIdx; - outIdx.x = x; - - if constexpr (NumChannelsT::kHasStaticChannels) - { - using FloatT = cuda::ConvertBaseTypeTo; - FloatT tmp{}; - - for (int k = 0, coeffIdx = coeffBase; k < support; k++, coeffIdx += coeffStride) - { - inIdx.x = cuda::clamp(sx0 + k, 0, inShape.x - 1); - const float flt = coeffs[coeffIdx]; - const InT px = LoadPixelLdg(inWrap, numChannels, inIdx); -#pragma unroll - for (int c = 0; c < NumChannelsT::kStaticChannels; c++) - { - cuda::GetElement(tmp, c) = fmaf(cuda::GetElement(px, c), flt, cuda::GetElement(tmp, c)); - } - } - - OutT &out = *GetWrapPtr(outWrap, outIdx); - out = cuda::SaturateCast(tmp * norm); - } - else if constexpr (!NumChannelsT::kHasStaticChannels) - { - for (int c = 0; c < numChannels(); c++) - { - float tmp = 0; - - for (int k = 0, coeffIdx = coeffBase; k < support; k++, coeffIdx += coeffStride) - { - inIdx.x = cuda::clamp(sx0 + k, 0, inShape.x - 1); - const float flt = coeffs[coeffIdx]; - const InT px = LoadPixelLdg(inWrap, numChannels, inIdx, c); - tmp = fmaf(px, flt, tmp); - } - - OutT &out = *GetWrapPtr(outWrap, outIdx, c); - out = cuda::SaturateCast(tmp * norm); - } - } - }); - } -} - -/** - * @brief Implements vertical resampling - * - * @param outWrap - the wrapper for accessing output data - * @param inWrap - the wrapper for accessing input data - * @param lo - inclusive lower bound output coordinates of the block processed by the threadblock - * @param hi - exclusive upper bound output coordinates of the block processed by the threadblock - * @param srcY0 - Y coordinate in the source image corresponding to output's 0 - * @param scale - step, in source Y, for one pixel in output Y (may be negative) - * @param support - size of the resampling kernel, in source pixels - * @param numChannels - the NumChannels specialization describing the number of interleaved - * channels in the input and output sample. - * - * The function fills the output in block-sized horizontal spans. - * Filter coefficients are pre-calculated for each horizontal span to avoid - * recalculating them for each column, and stored in a shared memory block. - */ -template -void __forceinline__ __device__ ResampleVert(const PassOutWrap outWrap, const PassInWrap inWrap, - const VecI lo, const VecI hi, float srcY0, - const float scale, const VecI inShape, - const filter::ResamplingFilter filter, const NumChannelsT numChannels) -{ - extern __shared__ float coeffs[]; - - using OutT = typename PassOutWrap::ValueType; - using InT = std::remove_const_t; - // spatial extents and optional channels extent - constexpr int kNDim = kSpatialNDim + !NumChannelsT::kHasStaticChannels; - - static_assert(!NumChannelsT::kHasStaticChannels || NumChannelsT::kStaticChannels == cuda::NumElements); - static_assert(cuda::NumElements == cuda::NumElements); - static_assert(PassOutWrap::kNumDimensions == kNDim); - static_assert(PassInWrap::kNumDimensions == kNDim); - - const int support = filter.support(); - const float filterStep = filter.scale; - // If the support is small enough, we can fit `blockDim.y` x `support` elements into shm, so that - // for each output_y mapped to input_y, we take into account the exact error that comes from - // rounding the input_y from float to integer. For larger supports, we just compute `support` - // elements common for all threads. - const bool hugeSupport = !CanComputeCoefPerThread(support, blockDim.y); - const int coeffBase = hugeSupport ? 0 : support * threadIdx.y; - - srcY0 += 0.5f * scale - 0.5f - filter.anchor; - - for (int i = lo.y; i < hi.y; i += blockDim.y) - { - const int y = i + threadIdx.y; - const float sy0f = y * scale + srcY0; - const int sy0 = hugeSupport ? cuda::round(sy0f) - : cuda::round(sy0f); - float f = (sy0 - sy0f) * filterStep; - __syncthreads(); - // fills `support` - if (hugeSupport) - { - for (int k = threadIdx.x + blockDim.x * threadIdx.y; k < support; k += blockDim.x * blockDim.y) - { - float flt = filter(f + k * filterStep); - coeffs[k] = flt; - } - } - else - { - for (int k = threadIdx.x; k < support; k += blockDim.x) - { - float flt = filter(f + k * filterStep); - coeffs[coeffBase + k] = flt; - } - } - __syncthreads(); - - if (y >= hi.y) - continue; - - float norm = 0; - for (int k = 0; k < support; k++) - { - norm += coeffs[coeffBase + k]; - } - norm = 1.0f / norm; - - ForAllOrthogonalToVert(lo, hi, - [=](VecI outIdx) - { - VecI inIdx = outIdx; - outIdx.y = y; - - if constexpr (NumChannelsT::kHasStaticChannels) - { - using FloatT = cuda::ConvertBaseTypeTo; - FloatT tmp{}; - - for (int k = 0; k < support; k++) - { - inIdx.y = cuda::clamp(sy0 + k, 0, inShape.y - 1); - const float flt = coeffs[coeffBase + k]; - const InT px = LoadPixelLdg(inWrap, numChannels, inIdx); -#pragma unroll - for (int c = 0; c < NumChannelsT::kStaticChannels; c++) - { - cuda::GetElement(tmp, c) - = fmaf(cuda::GetElement(px, c), flt, cuda::GetElement(tmp, c)); - } - } - - OutT &out = *GetWrapPtr(outWrap, outIdx); - out = cuda::SaturateCast(tmp * norm); - } - else if constexpr (!NumChannelsT::kHasStaticChannels) - { - for (int c = 0; c < numChannels(); c++) - { - float tmp = 0; - - for (int k = 0; k < support; k++) - { - inIdx.y = cuda::clamp(sy0 + k, 0, inShape.y - 1); - const float flt = coeffs[coeffBase + k]; - const InT px = LoadPixelLdg(inWrap, numChannels, inIdx, c); - tmp = fmaf(px, flt, tmp); - } - - OutT &out = *GetWrapPtr(outWrap, outIdx, c); - out = cuda::SaturateCast(tmp * norm); - } - } - }); - } -} - -/** - * @brief Implements depth resampling - * - * @param outWrap - the wrapper for accessing output data - * @param inWrap - the wrapper for accessing input data - * @param lo - inclusive lower bound output coordinates of the block processed by the threadblock - * @param hi - exclusive upper bound output coordinates of the block processed by the threadblock - * @param srcZ0 - Y coordinate in the source image corresponding to output's 0 - * @param scale - step, in source Y, for one pixel in output Y (may be negative) - * @param support - size of the resampling kernel, in source pixels - * @param numChannels - the NumChannels specialization describing the number of interleaved - * channels in the input and output sample. - * - * The function fills the output in block-sized horizontal spans. - * Filter coefficients are pre-calculated for each horizontal span to avoid - * recalculating them for each column, and stored in a shared memory block. - */ -template -void __forceinline__ __device__ ResampleDepth(const PassOutWrap outWrap, const PassInWrap inWrap, const VecI<3> lo, - const VecI<3> hi, float srcZ0, const float scale, const VecI<3> inShape, - const filter::ResamplingFilter filter, const NumChannelsT numChannels) -{ - extern __shared__ float coeffs[]; - - using OutT = typename PassOutWrap::ValueType; - using InT = std::remove_const_t; - // spatial extents and optional channels extent - constexpr int kNDim = 3 + !NumChannelsT::kHasStaticChannels; - - static_assert(!NumChannelsT::kHasStaticChannels || NumChannelsT::kStaticChannels == cuda::NumElements); - static_assert(cuda::NumElements == cuda::NumElements); - static_assert(PassOutWrap::kNumDimensions == kNDim); - static_assert(PassInWrap::kNumDimensions == kNDim); - - const int support = filter.support(); - const float filterStep = filter.scale; - // If the support is small enough, we can fit `blockDim.y` x `support` elements into shm, - // so that for each output_z mapped to input_z, we take into account the exact error that - // comes from rounding the input_z from float to integer. For larger supports, we just - // compute `support` elements common for all threads. - const bool hugeSupport = !CanComputeCoefPerThread(support, blockDim.y); - const int coeffBase = hugeSupport ? 0 : support * threadIdx.y; - - srcZ0 += 0.5f * scale - 0.5f - filter.anchor; - - for (int i = lo.z; i < hi.z; i += blockDim.y) - { - // threadIdx.y is used to traverse Z axis - const int z = i + threadIdx.y; - const float sz0f = z * scale + srcZ0; - const int sz0 = hugeSupport ? cuda::round(sz0f) - : cuda::round(sz0f); - float f = (sz0 - sz0f) * filterStep; - __syncthreads(); - if (hugeSupport) - { - for (int k = threadIdx.x + blockDim.x * threadIdx.y; k < support; k += blockDim.x * blockDim.y) - { - float flt = filter(f + k * filterStep); - coeffs[k] = flt; - } - } - else - { - for (int k = threadIdx.x; k < support; k += blockDim.x) - { - float flt = filter(f + k * filterStep); - coeffs[coeffBase + k] = flt; - } - } - __syncthreads(); - - if (z >= hi.z) - continue; - - float norm = 0; - for (int k = 0; k < support; k++) - { - norm += coeffs[coeffBase + k]; - } - norm = 1.0f / norm; - - for (int y = threadIdx.z + lo.y; y < hi.y; y += blockDim.z) - { - for (int x = threadIdx.x + lo.x; x < hi.x; x += blockDim.x) - { - const VecI<3> outIdx{x, y, z}; - VecI<3> inIdx = outIdx; - - if constexpr (NumChannelsT::kHasStaticChannels) - { - using FloatT = cuda::ConvertBaseTypeTo; - FloatT tmp{}; - - for (int k = 0; k < support; k++) - { - inIdx.z = cuda::clamp(sz0 + k, 0, inShape.z - 1); - const float flt = coeffs[coeffBase + k]; - const InT px = LoadPixelLdg(inWrap, numChannels, inIdx); -#pragma unroll - for (int c = 0; c < NumChannelsT::kStaticChannels; c++) - { - cuda::GetElement(tmp, c) = fmaf(cuda::GetElement(px, c), flt, cuda::GetElement(tmp, c)); - } - } - - OutT &out = *GetWrapPtr(outWrap, outIdx); - out = cuda::SaturateCast(tmp * norm); - } - else if constexpr (!NumChannelsT::kHasStaticChannels) - { - for (int c = 0; c < numChannels(); c++) - { - float tmp = 0; - - for (int k = 0; k < support; k++) - { - inIdx.z = cuda::clamp(sz0 + k, 0, inShape.z - 1); - const float flt = coeffs[coeffBase + k]; - const InT px = LoadPixelLdg(inWrap, numChannels, inIdx, c); - tmp = fmaf(px, flt, tmp); - } - - OutT &out = *GetWrapPtr(outWrap, outIdx, c); - out = cuda::SaturateCast(tmp * norm); - } - } - } - } - } -} -} // namespace filter_support - -template -void __forceinline__ __device__ RunNN(const PassOutWrap outWrap, const PassInWrap inWrap, const VecI lo, - const VecI hi, int axis, const VecI inShape, - const float origin, const float scale, const NumChannelsT numChannels) -{ - auto originV = cuda::SetAll>(0.f); - auto scaleV = cuda::SetAll>(1.f); - cuda::GetElement(originV, axis) = origin; - cuda::GetElement(scaleV, axis) = scale; - nn::Resample(outWrap, inWrap, lo, hi, originV, scaleV, inShape, numChannels); -} - -template -void __forceinline__ __device__ RunLinear(const PassOutWrap outWrap, const PassInWrap inWrap, - const VecI lo, const VecI hi, int axis, - const VecI inShape, const float origin, const float scale, - const NumChannelsT numChannels) -{ - if (axis == 0) - { - linear::ResampleHorz(outWrap, inWrap, lo, hi, origin, scale, inShape, numChannels); - } - else if (axis == 1) - { - linear::ResampleVert(outWrap, inWrap, lo, hi, origin, scale, inShape, numChannels); - } - else if (axis == 2) - { - if constexpr (kSpatialNDim == 3) - { - linear::ResampleDepth(outWrap, inWrap, lo, hi, origin, scale, inShape, numChannels); - } - } -} - -template -void __forceinline__ __device__ RunFilter(const PassOutWrap outWrap, const PassInWrap inWrap, - const VecI lo, const VecI hi, int axis, - const VecI inShape, const float origin, const float scale, - const filter::ResamplingFilter filter, const NumChannelsT numChannels) -{ - if (axis == 0) - { - filter_support::ResampleHorz(outWrap, inWrap, lo, hi, origin, scale, inShape, filter, - numChannels); - } - else if (axis == 1) - { - filter_support::ResampleVert(outWrap, inWrap, lo, hi, origin, scale, inShape, filter, - numChannels); - } - else if (axis == 2) - { - if constexpr (kSpatialNDim == 3) - { - filter_support::ResampleDepth(outWrap, inWrap, lo, hi, origin, scale, inShape, filter, numChannels); - } - } -} -} // namespace interpolate - -template -void __forceinline__ __device__ RunResamplingPass(const SampleDesc sampleDesc, const PassOutWrap outWrap, - const PassInWrap inWrap, const VecI lo, - const VecI hi, const NumChannelsT numChannels) -{ - VecI inShape = sampleDesc.shapes[kWhichPass]; - int axis = cuda::GetElement(sampleDesc.processingOrder, kWhichPass); // vec-order: 0 = X, 1 = Y, 2 = Z - const float origin = cuda::GetElement(sampleDesc.origin, kWhichPass); - const float scale = cuda::GetElement(sampleDesc.scale, kWhichPass); - - switch (sampleDesc.filterKind[kWhichPass]) - { - case filter::FilterTypeKind::Nearest: - interpolate::RunNN(outWrap, inWrap, lo, hi, axis, inShape, origin, scale, numChannels); - break; - case filter::FilterTypeKind::Linear: - interpolate::RunLinear(outWrap, inWrap, lo, hi, axis, inShape, origin, scale, numChannels); - break; - default: - interpolate::RunFilter(outWrap, inWrap, lo, hi, axis, inShape, origin, scale, - sampleDesc.filter[kWhichPass], numChannels); - break; - } -} - -// Tensor variant (unfirom batch) -template -__global__ void SeparableResamplingKernel(const SampleDesc sampleDesc, const PassOutWrap outWrap, - const PassInWrap inWrap, const GridHelperDevice gridHelper) - -{ - constexpr bool kHasDynamicChannels = kNumStaticChannels == -1; - static_assert(PassInWrap::kNumDimensions == 1 + kSpatialNDim + kHasDynamicChannels); - static_assert(PassOutWrap::kNumDimensions == 1 + kSpatialNDim + kHasDynamicChannels); - // Get sample idx and the region of the output image that - // the current threadblock has to process - int sampleIdx = gridHelper.CurrentSample(); - VecI lo, hi; - gridHelper.CurrentBlock(lo, hi, sampleDesc.blockShape[kWhichPass]); - hi = cuda::min(hi, sampleDesc.shapes[kWhichPass + 1]); - - const auto outSampleView = batch_wrapper::tensor::GetSampleView(outWrap, sampleIdx); - const auto inSampleView = batch_wrapper::tensor::GetSampleView(inWrap, sampleIdx); - WithChannels( - sampleDesc.channels, [=](const NumChannels numChannels) - { RunResamplingPass(sampleDesc, outSampleView, inSampleView, lo, hi, numChannels); }); -} - -// Batch variant (ImageBatchVarShape, TensorBatch) -template -__global__ void SeparableResamplingKernel(const SampleDesc *__restrict__ samples, - const PassOutWrap outWrap, const PassInWrap inWrap, - const GridHelperDevice gridHelper) -{ - constexpr bool kHasDynamicChannels = kNumStaticChannels == -1; - static_assert(PassInWrap::kNumDimensions == 1 + kSpatialNDim + kHasDynamicChannels); - static_assert(PassOutWrap::kNumDimensions == 1 + kSpatialNDim + kHasDynamicChannels); - // Get sample idx and the region of the output image that - // the current threadblock has to process - const int sampleIdx = gridHelper.CurrentSample(); - const auto sampleDesc = samples[sampleIdx]; - const VecI outShape = sampleDesc.shapes[kWhichPass + 1]; - VecI lo, hi; - gridHelper.CurrentBlock(lo, hi, sampleDesc.blockShape[kWhichPass]); - - // exit early for smaller samples - if (lo.x >= outShape.x || lo.y >= outShape.y) - { - return; - } - if constexpr (kSpatialNDim == 3) - { - if (lo.z >= outShape.z) - { - return; - } - } - hi = cuda::min(hi, outShape); - - const auto outSampleView = outWrap.GetSampleView(sampleIdx); - WithChannels( - sampleDesc.channels, - [=](const NumChannels numChannels) - { - if constexpr (kWhichPass == 0) - { - const auto inSampleView = inWrap.GetSampleView(sampleIdx, sampleDesc.inRoiOffset); - RunResamplingPass(sampleDesc, outSampleView, inSampleView, lo, hi, numChannels); - } - else if constexpr (kWhichPass != 0) - { - const auto inSampleView = inWrap.GetSampleView(sampleIdx); - RunResamplingPass(sampleDesc, outSampleView, inSampleView, lo, hi, numChannels); - } - }); -} - -} // namespace resampling - -namespace validate { -inline auto srcDst(const nvcv::Tensor &src, const nvcv::Tensor &dst) -{ - auto srcData = src.exportData(); - auto dstData = dst.exportData(); - - if (!srcData) - { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input must be cuda-accessible tensor"); - } - - if (!dstData) - { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Output must be cuda-accessible tensor"); - } - - using maybeTensorAccess = nvcv::Optional; - std::tuple ret; - - auto &[srcAccess, dstAccess, numSamples, numChannels, srcDtype, dstDtype] = ret; - - srcDtype = srcData->dtype(); - dstDtype = dstData->dtype(); - - srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcData); - dstAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*dstData); - NVCV_ASSERT(srcAccess && dstAccess); - - numSamples = srcAccess->numSamples(); - if (numSamples != dstAccess->numSamples()) - { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of samples"); - } - - if (srcDtype.numChannels() > 1 || dstDtype.numChannels() > 1) - { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, - "The tensor channels should be explicit part of the shape, not tensor type"); - } - - numChannels = srcAccess->numChannels(); - if (numChannels != dstAccess->numChannels()) - { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of channels"); - } - - if (numChannels <= 0) - { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Number of channels must be positive"); - } - - auto numPlanes = srcAccess->numPlanes(); - if (numPlanes != dstAccess->numPlanes()) - { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of planes"); - } - - if (numPlanes > 1) - { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Plannar images are not supported"); - } - - if (srcData->layout() != dstData->layout()) - { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input and output must have the same layout"); - } - - return ret; -} - -inline void srcDst(int &numSamples, int &uniqueNumChannels, nvcv::DataType &srcDtype, nvcv::DataType &dstDtype, - const nvcv::ImageBatchVarShape &src, const nvcv::ImageBatchVarShape &dst) -{ - numSamples = src.numImages(); - if (numSamples != dst.numImages()) - { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of samples"); - } - - const auto &srcFormat = src.uniqueFormat(); - const auto &dstFormat = dst.uniqueFormat(); - - if (!srcFormat || !dstFormat) - { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, - "All images in a batch must have the same format (including number of channels)"); - } - - auto numPlanes = srcFormat.numPlanes(); - if (numPlanes != dstFormat.numPlanes()) - { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of planes"); - } - - if (numPlanes > 1) - { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Plannar images are not supported"); - } - - srcDtype = srcFormat.planeDataType(0); - dstDtype = dstFormat.planeDataType(0); - - uniqueNumChannels = srcFormat.numChannels(); - if (uniqueNumChannels != dstFormat.numChannels()) - { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of channels"); - } -} - -inline void srcDst(int &numSamples, int &uniqueNumChannels, nvcv::DataType &srcDtype, nvcv::DataType &dstDtype, - const nvcv::TensorBatch &src, const nvcv::TensorBatch &dst) -{ - numSamples = src.numTensors(); - if (numSamples != dst.numTensors()) - { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of samples"); - } - - uniqueNumChannels = -1; - srcDtype = src.dtype(); - dstDtype = dst.dtype(); - - if (srcDtype.numChannels() > 1 || dstDtype.numChannels() > 1) - { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, - "The tensor channels should be explicit part of the shape, not tensor type"); - } - - if (src.layout() != dst.layout()) - { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output layouts"); - } - - if (src.layout() != nvcv::TENSOR_HW && src.layout() != nvcv::TENSOR_HWC && src.layout() != nvcv::TENSOR_DHW - && src.layout() != nvcv::TENSOR_DHWC) - { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "The tensor batch must contain [D]HW[C] samples"); - } -} - -inline void inOutNumberOfChannels(const HQResizeTensorShapeI &inShape, const HQResizeTensorShapeI &outShape) -{ - if (inShape.numChannels != outShape.numChannels) - { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, - "Incompatible input/output number of channels in one of the samples"); - } - if (inShape.numChannels <= 0) - { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "The number of channels must be positive"); - } -} - -inline void sameInOutNdim(const HQResizeTensorShapeI &inShape, const HQResizeTensorShapeI &outShape) -{ - if (inShape.ndim != outShape.ndim) - { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, - "Incompatible input/output number extents to resize"); - } -} - -inline void inOutShapes(int numSamples, const HQResizeTensorShapesI &inShapes, const HQResizeTensorShapesI &outShapes) -{ - if (inShapes.ndim != outShapes.ndim) - { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, - "The dimensionality of input and output shapes does not match"); - } - - if (numSamples != inShapes.size || numSamples != outShapes.size) - { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of samples"); - } - - if (inShapes.ndim != outShapes.ndim) - { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of extents"); - } - - if (inShapes.numChannels != outShapes.numChannels) - { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of channels"); - } - - if (inShapes.numChannels < 0) - { - for (int sampleIdx = 0; sampleIdx < numSamples; sampleIdx++) - { - inOutNumberOfChannels(inShapes.shape[sampleIdx], outShapes.shape[sampleIdx]); - } - } - else if (inShapes.numChannels == 0) - { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "The number of channels cannot be 0"); - } -} - -inline void roiBatch(int numSamples, int ndim, const HQResizeRoisF &rois) -{ - auto numRois = rois.size; - if (numRois != 0 && numRois != 1 && numRois != numSamples) - { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, - "The resize ROI list, if specified, must contain a single element to be used across all " - "samples in a batch or its length must match the batch size."); - } - if (numRois != 0) - { - if (rois.ndim != ndim) - { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, - "The number of ROI extents does not match the numebr of extents in the input"); - } - } -} -} // namespace validate - -namespace shape { - -template -struct Roi -{ - Vec Size() const - { - return hi - lo; - } - - Vec lo, hi; -}; - -inline HQResizeRoiF *SampleRoi(const HQResizeRoisF &rois, int sampleIdx) -{ - if (rois.size == 0) - { - return nullptr; - } - else if (rois.size == 1) - { - return rois.roi; - } - else - { - return rois.roi + sampleIdx; - } -} - -template -inline VecI TensorShape(const HQResizeTensorShapeI &shape) -{ - VecI shapeVec; - for (int d = 0; d < kSpatialNDim; d++) - { - cuda::GetElement(shapeVec, d) = shape.extent[kSpatialNDim - d - 1]; - } - return shapeVec; -} - -template -inline VecI SampleShape(const HQResizeTensorShapesI &shapes, int sampleIdx) -{ - return TensorShape(shapes.shape[shapes.size == 1 ? 0 : sampleIdx]); -} - -template -inline VecI TensorShape(const nvcv::Tensor &tensor) -{ - static_assert(kSpatialNDim == 2 || kSpatialNDim == 3); - const auto &shape = tensor.shape(); - const auto &layout = tensor.layout(); - char shapeArgLayout[4] = "WHD"; - VecI tensorShape; - for (int d = 0; d < kSpatialNDim; d++) - { - int axis = layout.find(shapeArgLayout[d]); - if (axis < 0) - { - throw std::runtime_error( - "The layout of an input tensor to the resize operator must contain HW extents in the layout (for " - "images) or DHW extents (for 3D resampling). Some extents are missing in the input tensor."); - } - cuda::GetElement(tensorShape, d) = shape[axis]; - } - return tensorShape; -} - -template -inline VecI SampleShape(const nvcv::ImageBatchVarShape &batch, int sampleIdx) -{ - static_assert(kSpatialNDim == 2); - VecI sampleShape; - const nvcv::Image &image = batch[sampleIdx]; - const auto &imageSize = image.size(); - sampleShape.x = imageSize.w; - sampleShape.y = imageSize.h; - return sampleShape; -} - -template -inline VecI SampleShape(const nvcv::TensorBatch &batch, int sampleIdx) -{ - return TensorShape(batch[sampleIdx]); -} - -inline int TensorNumChannels(const nvcv::Tensor &tensor) -{ - const auto &shape = tensor.shape(); - const auto &layout = tensor.layout(); - int channelAxis = layout.find('C'); - if (channelAxis < 0) - { - return 1; - } - return shape[channelAxis]; -} - -inline int64_t TensorByteSize(const nvcv::Tensor &tensor) -{ - auto data = tensor.exportData().cast(); - assert(data); - return data->stride(0) * data->shape(0); -} - -inline int64_t ImageByteSize(const nvcv::Image &image) -{ - auto data = image.exportData(); - assert(data); - auto plane = data->plane(0); // only single-plane images are supported - return plane.rowStride * plane.height; -} - -inline int SampleNumChannels(const nvcv::TensorBatch &src, const nvcv::TensorBatch &dst, int sampleIdx) -{ - const auto &srcSample = src[sampleIdx]; - const auto &dstSample = dst[sampleIdx]; - int numChannels = TensorNumChannels(srcSample); - if (numChannels != TensorNumChannels(dstSample)) - { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of channels"); - } - if (numChannels <= 0) - { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Number of channels must be positive"); - } - return numChannels; -} -} // namespace shape - -/** - * @brief Calculates optimum processing order based on input/output sizes and filter support. - * - * The sizes of intermediate storage and time taken to compute the intermediate images - * may depend on the order - i.e. if downscaling only one axis, it's beneficial to resample that - * axis first, so that intermediate image is smaller. - */ -template -class ProcessingOrderCalculator - -{ -public: - static constexpr float size_bias = 3; - - ProcessingOrderCalculator(const VecI inSize, const VecI outSize, const VecI filterSupport) - : m_inSize(inSize) - , m_outSize(outSize) - , m_filterSupport(filterSupport) - { - } - - VecI operator()() - { - for (int i = 0; i < ndim; i++) cuda::GetElement(m_bestOrder, i) = i; - m_axisVisited = {}; - m_currSize = m_inSize; - m_minCost = 1e+30f; - Run(0); - return m_bestOrder; - } - -private: - // recursively check every possible order in DFS fashion - void Run(int pass, float totalCost = 0) - { - if (totalCost >= m_minCost) - return; // this branch of recursion will not yield a better result - abandon it - - if (pass == ndim) - { - m_minCost = totalCost; - m_bestOrder = m_currOrder; - } - else - { - for (int a = 0; a < ndim; a++) - { - if (cuda::GetElement(m_axisVisited, a)) - continue; - cuda::GetElement(m_axisVisited, a) = true; - cuda::GetElement(m_currOrder, pass) = a; - auto prevSize = cuda::GetElement(m_currSize, a); - cuda::GetElement(m_currSize, a) = cuda::GetElement(m_outSize, a); - - float passCost = PassCost(pass, a); - Run(pass + 1, totalCost + passCost); - - cuda::GetElement(m_currSize, a) = prevSize; - cuda::GetElement(m_axisVisited, a) = false; - } - } - } - - float PassCost(int pass, int axis) - { - // y-axis is likely to be the cheapest - float axisCost = axis == 0 ? 1.4f : axis > 1 ? 1.2f : 1.0f; - auto vol = utils::Volume(m_currSize); - float baseComputeCost = cuda::GetElement(m_filterSupport, axis) * vol; - return axisCost * baseComputeCost + vol * size_bias; - } - - const VecI m_inSize, m_outSize, m_filterSupport; - float m_minCost; - VecI m_currSize, m_bestOrder, m_currOrder, m_axisVisited; -}; - -template -inline void RunTypedSwitch(nvcv::DataType srcDtype, nvcv::DataType dstDtype, int numChannels, const Cb &cb) -{ - using uchar = unsigned char; - -#define NVCV_RUN_DYNAMIC_CHANNELS_HQ_RESIZE(SRC_TYPE_NAME, DST_TYPE_NAME, SRC_VEC, DST_VEC) \ - ((srcDtype == nvcv::TYPE_##SRC_TYPE_NAME) && (dstDtype == nvcv::TYPE_##DST_TYPE_NAME)) \ - cb(SRC_VEC{}, IntermediateBaseT{}, DST_VEC{}, std::integral_constant{}) - -#define NVCV_RUN_SINGLE_CHANNEL_HQ_RESIZE(NUM_STATIC_CHANNELS, SRC_TYPE_NAME, DST_TYPE_NAME, SRC_VEC, DST_VEC) \ - ((numChannels == NUM_STATIC_CHANNELS) && (srcDtype == nvcv::TYPE_##SRC_TYPE_NAME) \ - && (dstDtype == nvcv::TYPE_##DST_TYPE_NAME)) \ - cb(SRC_VEC{}, IntermediateBaseT{}, DST_VEC{}, std::integral_constant{}) - -#define NVCV_RUN_MULTI_STATIC_CHANNEL_HQ_RESIZE(NUM_STATIC_CHANNELS, SRC_TYPE_NAME, DST_TYPE_NAME, SRC_VEC, DST_VEC) \ - ((numChannels == NUM_STATIC_CHANNELS) \ - && (srcDtype == nvcv::TYPE_##SRC_TYPE_NAME || srcDtype == nvcv::TYPE_##NUM_STATIC_CHANNELS##SRC_TYPE_NAME) \ - && (dstDtype == nvcv::TYPE_##DST_TYPE_NAME || dstDtype == nvcv::TYPE_##NUM_STATIC_CHANNELS##DST_TYPE_NAME)) \ - cb(SRC_VEC##NUM_STATIC_CHANNELS{}, Vec{}, \ - DST_VEC##NUM_STATIC_CHANNELS{}, std::integral_constant{}) - - // clang-format off - if NVCV_RUN_SINGLE_CHANNEL_HQ_RESIZE(1, U8, U8, uchar, uchar); - else if NVCV_RUN_MULTI_STATIC_CHANNEL_HQ_RESIZE(2, U8, U8, uchar, uchar); - else if NVCV_RUN_MULTI_STATIC_CHANNEL_HQ_RESIZE(3, U8, U8, uchar, uchar); - else if NVCV_RUN_MULTI_STATIC_CHANNEL_HQ_RESIZE(4, U8, U8, uchar, uchar); - else if NVCV_RUN_DYNAMIC_CHANNELS_HQ_RESIZE(U8, U8, uchar, uchar); - - else if NVCV_RUN_SINGLE_CHANNEL_HQ_RESIZE(1, U8, F32, uchar, float); - else if NVCV_RUN_MULTI_STATIC_CHANNEL_HQ_RESIZE(2, U8, F32, uchar, float); - else if NVCV_RUN_MULTI_STATIC_CHANNEL_HQ_RESIZE(3, U8, F32, uchar, float); - else if NVCV_RUN_MULTI_STATIC_CHANNEL_HQ_RESIZE(4, U8, F32, uchar, float); - else if NVCV_RUN_DYNAMIC_CHANNELS_HQ_RESIZE(U8, F32, uchar, float); - - else if NVCV_RUN_SINGLE_CHANNEL_HQ_RESIZE(1, S16, S16, short, short); - else if NVCV_RUN_MULTI_STATIC_CHANNEL_HQ_RESIZE(2, S16, S16, short, short); - else if NVCV_RUN_MULTI_STATIC_CHANNEL_HQ_RESIZE(3, S16, S16, short, short); - else if NVCV_RUN_MULTI_STATIC_CHANNEL_HQ_RESIZE(4, S16, S16, short, short); - else if NVCV_RUN_DYNAMIC_CHANNELS_HQ_RESIZE(S16, S16, short, short); - - else if NVCV_RUN_SINGLE_CHANNEL_HQ_RESIZE(1, S16, F32, short, float); - else if NVCV_RUN_MULTI_STATIC_CHANNEL_HQ_RESIZE(2, S16, F32, short, float); - else if NVCV_RUN_MULTI_STATIC_CHANNEL_HQ_RESIZE(3, S16, F32, short, float); - else if NVCV_RUN_MULTI_STATIC_CHANNEL_HQ_RESIZE(4, S16, F32, short, float); - else if NVCV_RUN_DYNAMIC_CHANNELS_HQ_RESIZE(S16, F32, short, float); - - else if NVCV_RUN_SINGLE_CHANNEL_HQ_RESIZE(1, U16, U16, ushort, ushort); - else if NVCV_RUN_MULTI_STATIC_CHANNEL_HQ_RESIZE(2, U16, U16, ushort, ushort); - else if NVCV_RUN_MULTI_STATIC_CHANNEL_HQ_RESIZE(3, U16, U16, ushort, ushort); - else if NVCV_RUN_MULTI_STATIC_CHANNEL_HQ_RESIZE(4, U16, U16, ushort, ushort); - else if NVCV_RUN_DYNAMIC_CHANNELS_HQ_RESIZE(U16, U16, ushort, ushort); - - else if NVCV_RUN_SINGLE_CHANNEL_HQ_RESIZE(1, U16, F32, ushort, float); - else if NVCV_RUN_MULTI_STATIC_CHANNEL_HQ_RESIZE(2, U16, F32, ushort, float); - else if NVCV_RUN_MULTI_STATIC_CHANNEL_HQ_RESIZE(3, U16, F32, ushort, float); - else if NVCV_RUN_MULTI_STATIC_CHANNEL_HQ_RESIZE(4, U16, F32, ushort, float); - else if NVCV_RUN_DYNAMIC_CHANNELS_HQ_RESIZE(U16, F32, ushort, float); - - else if NVCV_RUN_SINGLE_CHANNEL_HQ_RESIZE(1, F32, F32, float, float); - else if NVCV_RUN_MULTI_STATIC_CHANNEL_HQ_RESIZE(2, F32, F32, float, float); - else if NVCV_RUN_MULTI_STATIC_CHANNEL_HQ_RESIZE(3, F32, F32, float, float); - else if NVCV_RUN_MULTI_STATIC_CHANNEL_HQ_RESIZE(4, F32, F32, float, float); - else if NVCV_RUN_DYNAMIC_CHANNELS_HQ_RESIZE(F32, F32, float, float); - else - { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, - "Unsupported input/output types. The resize operator supports the " - "following types: uint8, int16, uint16, and float32. " - "The output type must be same as the input type or float."); - } -// clang-format on -#undef NVCV_RUN_DYNAMIC_CHANNELS_HQ_RESIZE -#undef NVCV_RUN_SINGLE_CHANNEL_HQ_RESIZE -#undef NVCV_RUN_MULTI_STATIC_CHANNEL_HQ_RESIZE -} - -template -class HQResizeRun -{ -public: - static_assert(_kSpatialNDim == 2 || _kSpatialNDim == 3, - "Currently, the resampling operator supports only 2 or 3 spatial dimensions"); - - HQResizeRun(const filter::ResamplingFiltersFactory &filtersFactory) - : m_filtersFactory{filtersFactory} - { - } - - using SampleDescT = resampling::SampleDesc<_kSpatialNDim>; - static_assert(std::is_trivially_copyable_v); - using DynamicBatchWrapMeta = batch_wrapper::dynamic::DynamicBatchWrapMeta; - - static constexpr VecI<3> kBlockDim = {32, 8, 1}; - static constexpr int kSpatialNDim = _kSpatialNDim; - // the number of buffers for intermediate results - static constexpr int kNumTmpBuffers = kSpatialNDim - 1; - // use alignment suitable for maximal supported number of static channels - static constexpr int kIntermediateAlignment = alignof(Vec); - - // Computes workspace requierements for calling the operator with tensor (uniform batch) input/output - cvcuda::WorkspaceRequirements getWorkspaceRequirements(int numSamples, const HQResizeTensorShapeI inputShape, - const HQResizeTensorShapeI outputShape, - const NVCVInterpolationType minInterpolation, - const NVCVInterpolationType magInterpolation, - const bool antialias, const HQResizeRoiF *roi) const - { - validate::inOutNumberOfChannels(inputShape, outputShape); - validate::sameInOutNdim(inputShape, outputShape); - - SampleDescT sampleDesc; - VecI srcShape = shape::TensorShape(inputShape); - VecI dstShape = shape::TensorShape(outputShape); - int numChannels = inputShape.numChannels; - auto [minFilter, magFilter] = filter::GetFilterModes(minInterpolation, magInterpolation, antialias); - SetupSampleDesc(sampleDesc, srcShape, dstShape, numChannels, roi, minFilter, magFilter); - - cvcuda::WorkspaceEstimator est; - for (int t = 0; t < kNumTmpBuffers; t++) - { - // the vectorized alignment may or may not be needed, depending on the number of channels - est.addCuda(GetPassOutputVolume(sampleDesc, t) * numSamples, kIntermediateAlignment); - } - - cvcuda::WorkspaceRequirements req{}; - req.hostMem = est.hostMem.req; - req.pinnedMem = est.pinnedMem.req; - req.cudaMem = est.cudaMem.req; - - // The allocator requries the total size of the allocation to be aligned - cvcuda::AlignUp(req); - return req; - } - - // Computes workspace requirements for calling the operator with TensorBatch/ImageBatchVarShape input/output - cvcuda::WorkspaceRequirements getWorkspaceRequirements(int numSamples, const HQResizeTensorShapesI inputShapes, - const HQResizeTensorShapesI outputShapes, - const NVCVInterpolationType minInterpolation, - const NVCVInterpolationType magInterpolation, - const bool antialias, const HQResizeRoisF rois) const - { - validate::roiBatch(numSamples, kSpatialNDim, rois); - validate::inOutShapes(numSamples, inputShapes, outputShapes); - auto [minFilter, magFilter] = filter::GetFilterModes(minInterpolation, magInterpolation, antialias); - - size_t intermediateSizes[kNumTmpBuffers]{}; - for (int sampleIdx = 0; sampleIdx < numSamples; sampleIdx++) - { - const VecI srcShape = shape::SampleShape(inputShapes, sampleIdx); - const VecI dstShape = shape::SampleShape(outputShapes, sampleIdx); - const HQResizeRoiF *sampleRoi = shape::SampleRoi(rois, sampleIdx); - int numChannels - = inputShapes.numChannels < 0 ? inputShapes.shape[sampleIdx].numChannels : inputShapes.numChannels; - - SampleDescT sampleDesc; - SetupSampleDesc(sampleDesc, srcShape, dstShape, numChannels, sampleRoi, minFilter, magFilter); - for (int t = 0; t < kNumTmpBuffers; t++) - { - intermediateSizes[t] += GetPassOutputVolume(sampleDesc, t); - } - } - - cvcuda::WorkspaceEstimator est; - est.addPinned(numSamples); - est.addCuda(numSamples); - - // reserve space for pointers and strides for intermediate wrappers - for (int t = 0; t < kNumTmpBuffers; t++) - { - batch_wrapper::dynamic::AddDynamicBatchWrapMeta(est, numSamples); - } - for (int t = 0; t < kNumTmpBuffers; t++) - { - // the vectorized alignment may or may not be needed, depending on the number of channels - est.addCuda(intermediateSizes[t], kIntermediateAlignment); - } - - cvcuda::WorkspaceRequirements req{}; - req.hostMem = est.hostMem.req; - req.pinnedMem = est.pinnedMem.req; - req.cudaMem = est.cudaMem.req; - // The allocator requries the total size of the allocation to be aligned - cvcuda::AlignUp(req); - - return req; - } - - // Computes upper bound for workspace requirements, i.e. the workspace that meets the computed requirements - // can be passed to the call with any type of input/output as long as there are no more than maxBatchSize - // samples that do not exceed the maxShape (in the input nor in the output). - cvcuda::WorkspaceRequirements getWorkspaceRequirements(int maxNumSamples, const HQResizeTensorShapeI maxShape) const - { - validate::inOutNumberOfChannels(maxShape, maxShape); - - cvcuda::WorkspaceEstimator est; - est.addPinned(maxNumSamples); - est.addCuda(maxNumSamples); - - // reserve space for pointers and strides for intermediate wrappers - for (int t = 0; t < kNumTmpBuffers; t++) - { - batch_wrapper::dynamic::AddDynamicBatchWrapMeta(est, maxNumSamples); - } - VecI shape = shape::TensorShape(maxShape); - for (int t = 0; t < kNumTmpBuffers; t++) - { - size_t numElements = utils::Volume(shape) * maxNumSamples * maxShape.numChannels; - est.addCuda(numElements, kIntermediateAlignment); - } - - cvcuda::WorkspaceRequirements req{}; - req.hostMem = est.hostMem.req; - req.pinnedMem = est.pinnedMem.req; - req.cudaMem = est.cudaMem.req; - // The allocator requries the total size of the allocation to be aligned - cvcuda::AlignUp(req); - - return req; - } - - void operator()(cudaStream_t stream, const cvcuda::Workspace &ws, const nvcv::Tensor &src, const nvcv::Tensor &dst, - const NVCVInterpolationType minInterpolation, const NVCVInterpolationType magInterpolation, - const bool antialias, const HQResizeRoiF *roi) const - { - auto tensorAccess = validate::srcDst(src, dst); - auto &[srcAccess, dstAccess, numSamples, numChannels, srcDtype, dstDtype] = tensorAccess; - - SampleDescT sampleDesc; - VecI srcShape = shape::TensorShape(src); - VecI dstShape = shape::TensorShape(dst); - const auto [minFilter, magFilter] = filter::GetFilterModes(minInterpolation, magInterpolation, antialias); - SetupSampleDesc(sampleDesc, srcShape, dstShape, numChannels, roi, minFilter, magFilter); - - cvcuda::WorkspaceAllocator allocator(ws); - if (ws.cudaMem.ready != nullptr) - { - NVCV_CHECK_THROW(cudaStreamWaitEvent(stream, ws.cudaMem.ready)); - } - IntermediateBaseT *intermediate[kNumTmpBuffers]; - // Get intermediate buffers - for (int t = 0; t < kNumTmpBuffers; t++) - { - intermediate[t] = allocator.getCuda(GetPassOutputVolume(sampleDesc, t) * numSamples, - kIntermediateAlignment); - } - - auto inMaxStride = shape::TensorByteSize(src); - auto outMaxStride = shape::TensorByteSize(dst); - bool wideStride = std::max(inMaxStride, outMaxStride) > cuda::TypeTraits::max; - - RunTypedSwitch( - srcDtype, dstDtype, numChannels, - [&](auto dummySrcVal, auto intermediateVal, auto dummyDstVal, auto numChannelsVal) - { - using InT = decltype(dummySrcVal); - using IntermediateT = decltype(intermediateVal); - using OutT = decltype(dummyDstVal); - constexpr int numStaticChannels = decltype(numChannelsVal)::value; - static_assert(numStaticChannels == -1 || numStaticChannels == cuda::NumElements); - static_assert(cuda::NumElements == cuda::NumElements); - static_assert(cuda::NumElements == cuda::NumElements); - - auto &[srcAccess, dstAccess, numSamples, numChannels, srcDtype, dstDtype] = tensorAccess; - if (wideStride) - { - RunPasses( - sampleDesc, *dstAccess, *srcAccess, intermediate, numSamples, ws, stream); - } - else - { - RunPasses( - sampleDesc, *dstAccess, *srcAccess, intermediate, numSamples, ws, stream); - } - }); - } - - template - void operator()(cudaStream_t stream, const cvcuda::Workspace &ws, const BatchContainer &src, - const BatchContainer &dst, const NVCVInterpolationType minInterpolation, - const NVCVInterpolationType magInterpolation, const bool antialias, const HQResizeRoisF rois) const - { - int numSamples; - int uniqueNumChannels; // numChannels for ImageBatchVarShape, -1 for TensorBatch - nvcv::DataType srcDtype, dstDtype; - validate::srcDst(numSamples, uniqueNumChannels, srcDtype, dstDtype, src, dst); - validate::roiBatch(numSamples, kSpatialNDim, rois); - - const auto [minFilter, magFilter] = filter::GetFilterModes(minInterpolation, magInterpolation, antialias); - cvcuda::WorkspaceAllocator allocator(ws); - if (ws.pinnedMem.ready != nullptr) - { - NVCV_CHECK_THROW(cudaEventSynchronize(ws.pinnedMem.ready)); - } - if (ws.cudaMem.ready != nullptr) - { - NVCV_CHECK_THROW(cudaStreamWaitEvent(stream, ws.cudaMem.ready)); - } - SampleDescT *sampleDescsCpu = allocator.getPinned(numSamples); - SampleDescT *sampleDescsGpu = allocator.getCuda(numSamples); - size_t intermediateSizes[kNumTmpBuffers]{}; - - int64_t inMaxStride = 0; - int64_t outMaxStride = 0; - for (int sampleIdx = 0; sampleIdx < numSamples; sampleIdx++) - { - const VecI srcShape = shape::SampleShape(src, sampleIdx); - const VecI dstShape = shape::SampleShape(dst, sampleIdx); - const HQResizeRoiF *sampleRoi = shape::SampleRoi(rois, sampleIdx); - int numChannels; - if constexpr (std::is_same_v) - { - numChannels = uniqueNumChannels; - inMaxStride = std::max(inMaxStride, shape::ImageByteSize(src[sampleIdx])); - outMaxStride = std::max(outMaxStride, shape::ImageByteSize(dst[sampleIdx])); - } - else - { - static_assert(std::is_same_v); - numChannels = shape::SampleNumChannels(src, dst, sampleIdx); - inMaxStride = std::max(inMaxStride, shape::TensorByteSize(src[sampleIdx])); - outMaxStride = std::max(outMaxStride, shape::TensorByteSize(dst[sampleIdx])); - } - SampleDescT &sampleDesc = sampleDescsCpu[sampleIdx]; - SetupSampleDesc(sampleDesc, srcShape, dstShape, numChannels, sampleRoi, minFilter, magFilter); - for (int t = 0; t < kNumTmpBuffers; t++) - { - intermediateSizes[t] += GetPassOutputVolume(sampleDesc, t); - } - } - bool wideStride = std::max(inMaxStride, outMaxStride) > cuda::TypeTraits::max; - - NVCV_CHECK_THROW(cudaMemcpyAsync(sampleDescsGpu, sampleDescsCpu, numSamples * sizeof(SampleDescT), - cudaMemcpyHostToDevice, stream)); - - // allocate space for pointers and strides for intermediate wrappers - DynamicBatchWrapMeta intermediateMeta[kNumTmpBuffers]; - IntermediateBaseT *intermediate[kNumTmpBuffers]; - for (int t = 0; t < kNumTmpBuffers; t++) - { - intermediateMeta[t] - = batch_wrapper::dynamic::AllocateDynamicBatchWrapMeta(allocator, numSamples, wideStride); - } - // allocate space for intermediate data - for (int t = 0; t < kNumTmpBuffers; t++) - { - intermediate[t] = allocator.getCuda(intermediateSizes[t], kIntermediateAlignment); - } - - Run(sampleDescsCpu, sampleDescsGpu, src, dst, intermediate, intermediateMeta, numSamples, srcDtype, dstDtype, - uniqueNumChannels, wideStride, ws, stream); - } - -private: - void Run(const SampleDescT *sampleDescsCpu, const SampleDescT *sampleDescsGpu, const nvcv::ImageBatchVarShape &src, - const nvcv::ImageBatchVarShape &dst, IntermediateBaseT *intermediate[kNumTmpBuffers], - const DynamicBatchWrapMeta intermediateMeta[kNumTmpBuffers], int numSamples, const nvcv::DataType srcDtype, - const nvcv::DataType dstDtype, int uniqueNumChannels, bool wideStride, const cvcuda::Workspace &ws, - cudaStream_t stream) const - { - static_assert(kSpatialNDim == 2, "ImageBatchVarShape does not support 3D spatial resampling"); +#include +#include +#include +#include - auto srcData = src.exportData(stream); - auto dstData = dst.exportData(stream); - if (!srcData) - { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, - "Input must be cuda-accessible, varshape pitch-linear image batch"); - } +#include +#include - if (!dstData) - { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, - "Output must be cuda-accessible, varshape pitch-linear image batch"); - } +namespace cvcuda::priv { +namespace hq_resize { - RunTypedSwitch( - srcDtype, dstDtype, uniqueNumChannels, - [&](auto dummySrcVal, auto intermediateVal, auto dummyDstVal, auto numChannelsVal) - { - using InT = decltype(dummySrcVal); - using IntermediateT = decltype(intermediateVal); - using OutT = decltype(dummyDstVal); - constexpr int numStaticChannels = decltype(numChannelsVal)::value; - if constexpr (numStaticChannels == -1) - { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, - "Unsupported number of channels for ImageBatchVarShape input."); - } - else if constexpr (numStaticChannels != -1) - { - static_assert(numStaticChannels == cuda::NumElements); - static_assert(cuda::NumElements == cuda::NumElements); - static_assert(cuda::NumElements == cuda::NumElements); - if (wideStride) - { - RunPasses( - sampleDescsCpu, sampleDescsGpu, *dstData, *srcData, intermediate, intermediateMeta, - numSamples, ws, stream); - } - else - { - RunPasses( - sampleDescsCpu, sampleDescsGpu, *dstData, *srcData, intermediate, intermediateMeta, - numSamples, ws, stream); - } - } - }); - } +namespace { - void Run(const SampleDescT *sampleDescsCpu, const SampleDescT *sampleDescsGpu, const nvcv::TensorBatch &src, - const nvcv::TensorBatch &dst, IntermediateBaseT *intermediate[kNumTmpBuffers], - const DynamicBatchWrapMeta intermediateMeta[kNumTmpBuffers], int numSamples, const nvcv::DataType srcDtype, - const nvcv::DataType dstDtype, int uniqueNumChannels, bool wideStride, const cvcuda::Workspace &ws, - cudaStream_t stream) const +// Expand a planar (NCHW/CHW) tensor batch into a batch of single-channel (H,W,1) HWC views, one per +// (sample, channel) plane. HQResize resizes channels independently, so resizing each plane as an +// independent single-channel image reproduces the interleaved result bit-for-bit. The views alias +// the original device memory (no copy); the returned batch must outlive the operator call. The +// caller sizes the workspace from the matching expanded (sum-of-channels single-channel) shapes. +static nvcv::TensorBatch ExpandPlanarTensorBatch(const nvcv::TensorBatch &batch) +{ + std::vector views; + for (int i = 0; i < batch.numTensors(); ++i) { - // Other cointainer allow exporting data with const qualifiers - const auto srcData - = const_cast(src).exportData(stream).cast(); - const auto dstData - = const_cast(dst).exportData(stream).cast(); - - if (!srcData) + nvcv::Tensor tensor = batch[i]; + auto data = tensor.exportData(); + if (!data) { throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, - "Input must be cuda-accessible, varshape pitch-linear image batch"); + "Planar HQResize requires cuda-accessible " + "tensors"); } - - if (!dstData) + auto access = nvcv::TensorDataAccessStridedImagePlanar::Create(*data); + if (!access) { throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, - "Output must be cuda-accessible, varshape pitch-linear image batch"); - } - - uniqueNumChannels = -1; - for (int sampleIdx = 0; sampleIdx < numSamples; sampleIdx++) - { - if (sampleIdx == 0) - { - uniqueNumChannels = sampleDescsCpu[sampleIdx].channels; - } - else if (uniqueNumChannels != sampleDescsCpu[sampleIdx].channels) - { - uniqueNumChannels = -1; - break; - } - } - - RunTypedSwitch( - srcDtype, dstDtype, uniqueNumChannels, - [&](auto dummySrcVal, auto intermediateVal, auto dummyDstVal, auto numChannelsVal) - { - using InT = decltype(dummySrcVal); - using IntermediateT = decltype(intermediateVal); - using OutT = decltype(dummyDstVal); - constexpr int numStaticChannels = decltype(numChannelsVal)::value; - static_assert(numStaticChannels == -1 || numStaticChannels == cuda::NumElements); - static_assert(cuda::NumElements == cuda::NumElements); - static_assert(cuda::NumElements == cuda::NumElements); - - if (wideStride) - { - RunPasses( - sampleDescsCpu, sampleDescsGpu, *dstData, *srcData, intermediate, intermediateMeta, numSamples, - ws, stream); - } - else - { - RunPasses( - sampleDescsCpu, sampleDescsGpu, *dstData, *srcData, intermediate, intermediateMeta, numSamples, - ws, stream); - } - }); - } - - template - std::enable_if_t RunPasses(const SampleDescT &sampleDesc, - const nvcv::TensorDataAccessStridedImagePlanar &dstAccess, - const nvcv::TensorDataAccessStridedImagePlanar &srcAccess, - IntermediateBaseT *intermediate[kNumTmpBuffers], int numSamples, - const cvcuda::Workspace &ws, cudaStream_t stream) const - { - static_assert(kSpatialNDim == 2); - constexpr bool kHasDynamicChannels = kNumStaticChannels == -1; - // sample extent, spatial extents, optional dynamic channel extent - constexpr int kWrapNDim = 1 + kSpatialNDim + kHasDynamicChannels; - using OutWrap = cuda::TensorNDWrap; - using InWrap = cuda::TensorNDWrap; - using InterWrap = cuda::TensorNDWrap; - static_assert(std::is_trivially_copyable_v); - static_assert(std::is_trivially_copyable_v); - static_assert(std::is_trivially_copyable_v); - const OutWrap outWrap - = batch_wrapper::tensor::WrapTensor(dstAccess); - const InWrap inWrap = batch_wrapper::tensor::WrapTensor( - srcAccess, sampleDesc.inRoiOffset); - const InterWrap interWrap = batch_wrapper::tensor::CreateDenseWrap( - intermediate[0], sampleDesc.channels, sampleDesc.shapes[1]); - RunPass(sampleDesc, interWrap, inWrap, numSamples, stream); - RunPass(sampleDesc, outWrap, interWrap, numSamples, stream); - if (ws.cudaMem.ready != nullptr) - { - NVCV_CHECK_THROW(cudaEventRecord(ws.cudaMem.ready, stream)); - } - } - - template - std::enable_if_t RunPasses(const SampleDescT &sampleDesc, - const nvcv::TensorDataAccessStridedImagePlanar &dstAccess, - const nvcv::TensorDataAccessStridedImagePlanar &srcAccess, - IntermediateBaseT *intermediate[kNumTmpBuffers], int numSamples, - const cvcuda::Workspace &ws, cudaStream_t stream) const - { - static_assert(kSpatialNDim == 3); - constexpr bool kHasDynamicChannels = kNumStaticChannels == -1; - // sample extent, spatial extents, optional dynamic channel extent - constexpr int kWrapNDim = 1 + kSpatialNDim + kHasDynamicChannels; - using OutWrap = cuda::TensorNDWrap; - using InWrap = cuda::TensorNDWrap; - using InterWrap = cuda::TensorNDWrap; - static_assert(std::is_trivially_copyable_v); - static_assert(std::is_trivially_copyable_v); - static_assert(std::is_trivially_copyable_v); - const OutWrap outWrap - = batch_wrapper::tensor::WrapTensor(dstAccess); - const InWrap inWrap = batch_wrapper::tensor::WrapTensor( - srcAccess, sampleDesc.inRoiOffset); - const InterWrap interWrap0 - = batch_wrapper::tensor::CreateDenseWrap( - intermediate[0], sampleDesc.channels, sampleDesc.shapes[1]); - const InterWrap interWrap1 - = batch_wrapper::tensor::CreateDenseWrap( - intermediate[1], sampleDesc.channels, sampleDesc.shapes[2]); - RunPass(sampleDesc, interWrap0, inWrap, numSamples, stream); - RunPass(sampleDesc, interWrap1, interWrap0, numSamples, stream); - RunPass(sampleDesc, outWrap, interWrap1, numSamples, stream); - if (ws.cudaMem.ready != nullptr) - { - NVCV_CHECK_THROW(cudaEventRecord(ws.cudaMem.ready, stream)); - } - } - - template - std::enable_if_t RunPasses(const SampleDescT *sampleDescsCpu, const SampleDescT *sampleDescsGpu, - const BatchDataStridedCuda &dstData, const BatchDataStridedCuda &srcData, - IntermediateBaseT *intermediate[kNumTmpBuffers], - const DynamicBatchWrapMeta intermediateMeta[kNumTmpBuffers], int numSamples, - const cvcuda::Workspace &ws, cudaStream_t stream) const - { - static_assert(kSpatialNDim == 2); - constexpr bool kHasDynamicChannels = kNumStaticChannels == -1; - // sample extent, spatial extents, optional dynamic channel extent - constexpr int kWrapNDim = 1 + kSpatialNDim + kHasDynamicChannels; - using BatchWrapOutT - = std::conditional_t, - batch_wrapper::ImageBatchVarShapeWrapAdapter, - batch_wrapper::TensorBatchWrapAdapter>; - using BatchWrapInT - = std::conditional_t, - batch_wrapper::ImageBatchVarShapeWrapAdapter, - batch_wrapper::TensorBatchWrapAdapter>; - using DynamicBatchWrap = batch_wrapper::dynamic::DynamicBatchWrap; - static_assert(std::is_trivially_copyable_v); - static_assert(std::is_trivially_copyable_v); - static_assert(std::is_trivially_copyable_v); - const BatchWrapOutT outWrap(dstData); - const BatchWrapInT inWrap(srcData); - const DynamicBatchWrap intermediateWrap - = batch_wrapper::dynamic::CreateDynamicBatchWrap( - 0, intermediate[0], intermediateMeta[0], sampleDescsCpu, numSamples, stream); - if (ws.pinnedMem.ready != nullptr) - { - NVCV_CHECK_THROW(cudaEventRecord(ws.pinnedMem.ready, stream)); - } - RunPass(sampleDescsCpu, sampleDescsGpu, intermediateWrap, inWrap, numSamples, stream); - RunPass(sampleDescsCpu, sampleDescsGpu, outWrap, intermediateWrap, numSamples, stream); - if (ws.cudaMem.ready != nullptr) - { - NVCV_CHECK_THROW(cudaEventRecord(ws.cudaMem.ready, stream)); - } - } - - template - std::enable_if_t RunPasses(const SampleDescT *sampleDescsCpu, const SampleDescT *sampleDescsGpu, - const nvcv::TensorBatchDataStridedCuda &dstData, - const nvcv::TensorBatchDataStridedCuda &srcData, - IntermediateBaseT *intermediate[kNumTmpBuffers], - const DynamicBatchWrapMeta intermediateMeta[kNumTmpBuffers], int numSamples, - const cvcuda::Workspace &ws, cudaStream_t stream) const - { - static_assert(kSpatialNDim == 3); - constexpr bool kHasDynamicChannels = kNumStaticChannels == -1; - // sample extent, spatial extents, optional dynamic channel extent - constexpr int kWrapNDim = 1 + kSpatialNDim + kHasDynamicChannels; - using TensorBatchWrapOutT = batch_wrapper::TensorBatchWrapAdapter; - using TensorBatchWrapInT = batch_wrapper::TensorBatchWrapAdapter; - using DynamicBatchWrap = batch_wrapper::dynamic::DynamicBatchWrap; - static_assert(std::is_trivially_copyable_v); - static_assert(std::is_trivially_copyable_v); - static_assert(std::is_trivially_copyable_v); - const TensorBatchWrapOutT outWrap(dstData); - const TensorBatchWrapInT inWrap(srcData); - const DynamicBatchWrap intermediateWrap0 - = batch_wrapper::dynamic::CreateDynamicBatchWrap( - 0, intermediate[0], intermediateMeta[0], sampleDescsCpu, numSamples, stream); - const DynamicBatchWrap intermediateWrap1 - = batch_wrapper::dynamic::CreateDynamicBatchWrap( - 1, intermediate[1], intermediateMeta[1], sampleDescsCpu, numSamples, stream); - if (ws.pinnedMem.ready != nullptr) - { - NVCV_CHECK_THROW(cudaEventRecord(ws.pinnedMem.ready, stream)); + "Planar HQResize requires planar-accessible " + "tensors"); } - RunPass(sampleDescsCpu, sampleDescsGpu, intermediateWrap0, inWrap, numSamples, stream); - RunPass(sampleDescsCpu, sampleDescsGpu, intermediateWrap1, intermediateWrap0, numSamples, - stream); - RunPass(sampleDescsCpu, sampleDescsGpu, outWrap, intermediateWrap1, numSamples, stream); - if (ws.cudaMem.ready != nullptr) - { - NVCV_CHECK_THROW(cudaEventRecord(ws.cudaMem.ready, stream)); - } - } - - template - void RunPass(const SampleDescT &sampleDesc, const PassOutWrap &outWrap, const PassInWrap &inWrap, int numSamples, - cudaStream_t stream) const - { - using GridHelperT = resampling::GridHelper; - - VecI numBlocks; - { - VecI outputShape = sampleDesc.shapes[kWhichPass + 1]; - VecI blockShape = sampleDesc.blockShape[kWhichPass]; - numBlocks = utils::DivCeil(outputShape, blockShape); - if (utils::Volume(numBlocks) == 0) - { - return; - } - } - - GridHelperT gridHelper{numBlocks, numSamples}; - dim3 block(kBlockDim.x, kBlockDim.y, kBlockDim.z); - dim3 grid = gridHelper.GetKernelGrid(); - const auto devGridHelper = gridHelper.GetDeviceGridHelper(); - - int sharedMemSize = RequiredSharedMemorySize(sampleDesc, kWhichPass); - resampling::SeparableResamplingKernel - <<>>(sampleDesc, outWrap, inWrap, devGridHelper); - NVCV_CHECK_THROW(cudaGetLastError()); - } - - template - void RunPass(const SampleDescT *sampleDescsCpu, const SampleDescT *sampleDescsGpu, const PassOutWrap &outWrap, - const PassInWrap &inWrap, int numSamples, cudaStream_t stream) const - { - using GridHelperT = resampling::GridHelper; - - int maxSharedMemSize = 0; - VecI maxNumBlocks{}; - for (int sampleIdx = 0; sampleIdx < numSamples; sampleIdx++) - { - const SampleDescT &sampleDesc = sampleDescsCpu[sampleIdx]; - int sharedMemSize = RequiredSharedMemorySize(sampleDesc, kWhichPass); - maxSharedMemSize = std::max(maxSharedMemSize, sharedMemSize); - - VecI outputShape = sampleDesc.shapes[kWhichPass + 1]; - VecI blockShape = sampleDesc.blockShape[kWhichPass]; - VecI numBlocks = utils::DivCeil(outputShape, blockShape); - maxNumBlocks = cuda::max(maxNumBlocks, numBlocks); - } - - if (utils::Volume(maxNumBlocks) == 0) - { - return; - } - - GridHelperT gridHelper{maxNumBlocks, numSamples}; - dim3 block(kBlockDim.x, kBlockDim.y, kBlockDim.z); - dim3 grid = gridHelper.GetKernelGrid(); - const auto devGridHelper = gridHelper.GetDeviceGridHelper(); - - resampling::SeparableResamplingKernel - <<>>(sampleDescsGpu, outWrap, inWrap, devGridHelper); - NVCV_CHECK_THROW(cudaGetLastError()); - } - - int RequiredSharedMemorySize(const SampleDescT &sampleDesc, int whichPass) const - { - using resampling::interpolate::filter_support::RequiredSharedMemoryElements; - if (sampleDesc.filterKind[whichPass] != filter::FilterTypeKind::ShmFilter) - { - return 0; - } - int support = sampleDesc.filter[whichPass].support(); - int axis = cuda::GetElement(sampleDesc.processingOrder, whichPass); - // for depth resampling y is used as well - int resamplingAxisBlockSize = axis == 0 ? kBlockDim.x : kBlockDim.y; - return sizeof(IntermediateBaseT) * RequiredSharedMemoryElements(support, resamplingAxisBlockSize); - } - - void SetupSampleDesc(SampleDescT &sampleDesc, const VecI &srcShape, - const VecI &dstShape, int numChannels, const HQResizeRoiF *roi, - const filter::FilterMode &minFilter, const filter::FilterMode &magFilter) const - { - SetupSampleDescFilterShapeScale(sampleDesc, srcShape, dstShape, numChannels, minFilter, magFilter, roi); - SetupBlockLayout(sampleDesc); - } - - void SetupSampleDescFilterShapeScale(SampleDescT &sampleDesc, const VecI &inShape, - const VecI &outShape, int numChannels, - const filter::FilterMode &minFilter, const filter::FilterMode &magFilter, - const HQResizeRoiF *roi) const - { - // get user provided roi - const shape::Roi parsedRoi = ParseROI(roi, inShape); - // setup filter based on user provided filter types and the input/output size - filter::FilterTypeKind filterKinds[kSpatialNDim]; - filter::ResamplingFilter filters[kSpatialNDim]; - SetupFilters(filterKinds, filters, parsedRoi.Size(), outShape, minFilter, magFilter); - // get the ROI that is normalized (so that roiLo <= roiHi), adjusted for filter's "halo", - // and clampped to input shape - const shape::Roi adjustedRoi = AdjustRoiForFilter(parsedRoi, inShape, filters); - VecI adjustedRoiSize = adjustedRoi.Size(); - // the processing order is permutation that maps pass number to axis resampled during given pass - sampleDesc.processingOrder = SetupProcessingOrder(adjustedRoiSize, outShape, filters); - // now, use filters, roi and processingOrder to populate sample descriptor - sampleDesc.channels = numChannels; - sampleDesc.shapes[0] = inShape; - // set output shapes, scaling, roi, and relevant filters for each pass - // according to the best processingOrder of axes - { - VecI intermediateShape = adjustedRoiSize; - for (int pass = 0; pass < kSpatialNDim; pass++) - { - const int axis = cuda::GetElement(sampleDesc.processingOrder, pass); - const int axisOutShape = cuda::GetElement(outShape, axis); - const float roiStart = cuda::GetElement(parsedRoi.lo, axis); - const float roiEnd = cuda::GetElement(parsedRoi.hi, axis); - - cuda::GetElement(intermediateShape, axis) = axisOutShape; - sampleDesc.filterKind[pass] = filterKinds[axis]; - sampleDesc.filter[pass] = filters[axis]; - sampleDesc.shapes[pass + 1] = intermediateShape; - - cuda::GetElement(sampleDesc.origin, pass) = roiStart; - cuda::GetElement(sampleDesc.scale, pass) = (roiEnd - roiStart) / axisOutShape; - - // "Clamp" the axes processed in later passes to the input ROI - if (pass == 0) - { - // the first processed axis roi is handled simply with the `origin` - cuda::GetElement(sampleDesc.inRoiOffset, axis) = 0; - } - else - { - // for the axes not resampled in the first pass, we can just use offset when accesing data - // (adjustedRoi.lo) and pretend the input shape is the adjustedRoi.Size() - cuda::GetElement(sampleDesc.shapes[0], axis) = cuda::GetElement(adjustedRoiSize, axis); - cuda::GetElement(sampleDesc.inRoiOffset, axis) = cuda::GetElement(adjustedRoi.lo, axis); - cuda::GetElement(sampleDesc.origin, pass) - -= cuda::GetElement(adjustedRoi.lo, axis); // parsedRoi.lo - adjustedRoi.lo - } - } - } - } - - /** - * @brief If user specified the roi, it's returned with reversed dims oreder ((d)hw -> wh(d)), - * otherwise the input shape is used to create whole-plane roi. - * Note, that in the first case, some lo and hi may be flipped (i.e. lo[d] > hi[d]). - */ - shape::Roi ParseROI(const HQResizeRoiF *roi, VecI inShape) const - { - shape::Roi retRoi; - for (int dim = 0; dim < kSpatialNDim; dim++) - { - int axis = kSpatialNDim - 1 - dim; - auto axisSize = cuda::GetElement(inShape, axis); - float roiStart, roiEnd; - if (roi != nullptr) - { - roiStart = roi->lo[dim]; - roiEnd = roi->hi[dim]; - } - else - { - roiStart = 0; - roiEnd = axisSize; - } - cuda::GetElement(retRoi.lo, axis) = roiStart; - cuda::GetElement(retRoi.hi, axis) = roiEnd; - } - return retRoi; - } - - void SetupFilters(filter::FilterTypeKind filterKind[kSpatialNDim], filter::ResamplingFilter filters[kSpatialNDim], - VecF roiShape, const VecI &outShape, - const filter::FilterMode &minFilter, const filter::FilterMode &magFilter) const - { - using resampling::interpolate::filter_support::kMaxGPUFilterSupport; - static_assert(kSpatialNDim == 2 || kSpatialNDim == 3, - "Currently, the resampling operator supports only 2 or 3 spatial dimensions"); - - for (int axis = 0; axis < kSpatialNDim; axis++) - { - float inSize = std::abs(cuda::GetElement(roiShape, axis)); - float outSize = cuda::GetElement(outShape, axis); - const auto filterMode = outSize < inSize ? minFilter : magFilter; - filterKind[axis] = filter::GetFilterTypeKind(filterMode.filterType); - auto &filter = filters[axis]; - filter = filter::GetResamplingFilter(m_filtersFactory, filterMode, inSize, outSize); - - // for very small outputs, the required support may be too big for avialable shm - if (filter.support() > kMaxGPUFilterSupport) - { - filter.rescale(kMaxGPUFilterSupport); - } - } - } - - /** - * @brief Computes normalized ROI (i.e. so that roiLo <= roiHow), which is adjusted for filter's halo, - * converted to int and clamped to the input shape - */ - shape::Roi AdjustRoiForFilter(const shape::Roi &roi, - const VecI &inShape, - const filter::ResamplingFilter filters[kSpatialNDim]) const - { - shape::Roi ajustedRoi; - for (int axis = 0; axis < kSpatialNDim; axis++) - { - const float &axisLo = cuda::GetElement(roi.lo, axis); - const float &axisHi = cuda::GetElement(roi.hi, axis); - const auto &filter = filters[axis]; - int support = filter.numCoeffs ? filter.support() : 1; - float adjustedAxisLo, adjustedAxisHi; - if (axisLo <= axisHi) - { - adjustedAxisLo = axisLo - filter.anchor; - adjustedAxisHi = axisHi - filter.anchor + support; - } - else - { // flipped - adjustedAxisLo = axisHi - filter.anchor; - adjustedAxisHi = axisLo - filter.anchor + support; - } - const int axisSize = cuda::GetElement(inShape, axis); - cuda::GetElement(ajustedRoi.lo, axis) - = std::max(0, std::min(axisSize, std::floor(adjustedAxisLo))); - cuda::GetElement(ajustedRoi.hi, axis) - = std::max(0, std::min(axisSize, std::ceil(adjustedAxisHi))); - } - return ajustedRoi; - } - - VecI SetupProcessingOrder(const VecI &inRoiSize, const VecI &outSize, - const filter::ResamplingFilter filters[kSpatialNDim]) const - { - VecI filterSupport; - for (int i = 0; i < kSpatialNDim; i++) - { - int support = filters[i].support(); - // NN filter has support -1, so we need the max() below - cuda::GetElement(filterSupport, i) = std::max(1, support); - } - - return ProcessingOrderCalculator(inRoiSize, outSize, filterSupport)(); - } - - int64_t GetPassOutputVolume(SampleDescT sampleDesc, int pass) const - { - return utils::Volume(sampleDesc.shapes[pass + 1]) * sampleDesc.channels; - } - - /** - * @brief Calculates block layout for a 2D sample - * - */ - template - std::enable_if_t SetupBlockLayout(SampleDescT &sampleDesc) const - { - static_assert(kSpatialNDim == 2); - int lanes = resampling::GetResizeBlockLanes(); - for (int pass = 0; pass < kSpatialNDim; pass++) - { - int resamplingAxis = cuda::GetElement(sampleDesc.processingOrder, pass); - // The threadblock is (kBlockDim.x, kBlockDim.y) for all passes. - // In horizontal pass (resamplingAxis == 0), a single block will - // process output slice of (kBlockDim.x, lanes * kBlockDim.y). - // In vertical pass (resamplingAxis == 1), each block will handle - // output slice of (kBlockDim.x * lanes, kBlockDim.y). - VecI<2> blockShape{kBlockDim.x, kBlockDim.y}; - cuda::GetElement(blockShape, 1 - resamplingAxis) *= lanes; - auto outputShape = sampleDesc.shapes[pass + 1]; - sampleDesc.blockShape[pass] = cuda::clamp(blockShape, VecI<2>{1, 1}, outputShape); - } - } - /** - * @brief Calculates block layout for a 3D sample - */ - template - std::enable_if_t SetupBlockLayout(SampleDescT &sampleDesc) const - { - static_assert(kSpatialNDim == 3); - int lanes = resampling::GetResizeBlockLanes(); - for (int pass = 0; pass < kSpatialNDim; pass++) + const int numSamples = access->numSamples(); + const int numChannels = access->numChannels(); + const int64_t H = access->numRows(), W = access->numCols(); + for (int s = 0; s < numSamples; ++s) { - auto outputShape = sampleDesc.shapes[pass + 1]; - int resamplingAxis = cuda::GetElement(sampleDesc.processingOrder, pass); - if (resamplingAxis < 2) - { - VecI<3> blockShape{kBlockDim.x, kBlockDim.y, kBlockDim.z * lanes}; - sampleDesc.blockShape[pass] = cuda::clamp(blockShape, VecI<3>{1, 1, 1}, outputShape); - } - else + for (int c = 0; c < numChannels; ++c) { - assert(resamplingAxis == 2); - VecI<3> blockShape{kBlockDim.x, kBlockDim.z * lanes, kBlockDim.y}; - sampleDesc.blockShape[pass] = cuda::clamp(blockShape, VecI<3>{1, 1, 1}, outputShape); + nvcv::TensorDataStridedCuda::Buffer buf; + buf.basePtr = reinterpret_cast(access->sampleData(s) + c * access->chStride()); + buf.strides[0] = access->rowStride(); // H + buf.strides[1] = access->colStride(); // W + buf.strides[2] = access->colStride(); // C == 1 + nvcv::TensorDataStridedCuda viewData{ + nvcv::TensorShape{{H, W, 1}, "HWC"}, + data->dtype(), buf + }; + views.push_back(nvcv::TensorWrapData(viewData)); } } } + nvcv::TensorBatch out(static_cast(views.size())); + out.pushBack(views.begin(), views.end()); + return out; +} - const filter::ResamplingFiltersFactory &m_filtersFactory; -}; } // namespace -namespace cvcuda::priv { -namespace hq_resize { - -// Implements the IHQResizeImpl interface and keeps the filters fatory with initilized -// supports. The actual implementation is in a stateless HQResizeRun that is parametrized -// with the number of resampled dimensions. class HQResizeImpl final : public IHQResizeImpl { public: + HQResizeImpl() + : m_impl2d(makeImpl2D()) + , m_impl3d(makeImpl3D()) + { + } + cvcuda::WorkspaceRequirements getWorkspaceRequirements(int numSamples, const HQResizeTensorShapeI inputShape, const HQResizeTensorShapeI outputShape, const NVCVInterpolationType minInterpolation, const NVCVInterpolationType magInterpolation, bool antialias, const HQResizeRoiF *roi) const override { - if (inputShape.ndim == 2) - { - HQResizeRun<2> resize(m_filtersFactory); - return resize.getWorkspaceRequirements(numSamples, inputShape, outputShape, minInterpolation, - magInterpolation, antialias, roi); - } - else if (inputShape.ndim == 3) - { - HQResizeRun<3> resize(m_filtersFactory); - return resize.getWorkspaceRequirements(numSamples, inputShape, outputShape, minInterpolation, - magInterpolation, antialias, roi); - } - else - { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, - "Only 2D or 3D resize is supported. Got unexpected number of extents to resize."); - } + return implForNDim(inputShape.ndim) + .getWorkspaceRequirements(numSamples, inputShape, outputShape, minInterpolation, magInterpolation, + antialias, roi); } cvcuda::WorkspaceRequirements getWorkspaceRequirements(int numSamples, const HQResizeTensorShapesI inputShapes, @@ -2696,87 +111,67 @@ public: const NVCVInterpolationType magInterpolation, bool antialias, const HQResizeRoisF rois) const override { - if (inputShapes.ndim == 2) - { - HQResizeRun<2> resize(m_filtersFactory); - return resize.getWorkspaceRequirements(numSamples, inputShapes, outputShapes, minInterpolation, - magInterpolation, antialias, rois); - } - else if (inputShapes.ndim == 3) - { - HQResizeRun<3> resize(m_filtersFactory); - return resize.getWorkspaceRequirements(numSamples, inputShapes, outputShapes, minInterpolation, - magInterpolation, antialias, rois); - } - else - { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, - "Only 2D or 3D resize is supported. Got unexpected number of extents to resize."); - } + return implForNDim(inputShapes.ndim) + .getWorkspaceRequirements(numSamples, inputShapes, outputShapes, minInterpolation, magInterpolation, + antialias, rois); } cvcuda::WorkspaceRequirements getWorkspaceRequirements(int maxBatchSize, const HQResizeTensorShapeI maxShape) const override { - if (maxShape.ndim == 2) - { - HQResizeRun<2> resize(m_filtersFactory); - return resize.getWorkspaceRequirements(maxBatchSize, maxShape); - } - else if (maxShape.ndim == 3) - { - HQResizeRun<3> resize(m_filtersFactory); - return resize.getWorkspaceRequirements(maxBatchSize, maxShape); - } - else - { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, - "Only 2D or 3D resize is supported. Got unexpected number of extents to resize."); - } + return implForNDim(maxShape.ndim).getWorkspaceRequirements(maxBatchSize, maxShape); } void operator()(cudaStream_t stream, const cvcuda::Workspace &ws, const nvcv::Tensor &src, const nvcv::Tensor &dst, const NVCVInterpolationType minInterpolation, const NVCVInterpolationType magInterpolation, bool antialias, const HQResizeRoiF *roi) override { - if (src.layout().find('D') < 0) - { - HQResizeRun<2> resize(m_filtersFactory); - resize(stream, ws, src, dst, minInterpolation, magInterpolation, antialias, roi); - } - else - { - HQResizeRun<3> resize(m_filtersFactory); - resize(stream, ws, src, dst, minInterpolation, magInterpolation, antialias, roi); - } + CVCUDA_NVTX_RANGE("cvcuda::hq_resize::HQResizeImpl::operator()[Tensor]"); + bool is3d = src.layout().find('D') >= 0; + implForNDim(is3d ? 3 : 2)(stream, ws, src, dst, minInterpolation, magInterpolation, antialias, roi); } void operator()(cudaStream_t stream, const cvcuda::Workspace &ws, const nvcv::ImageBatchVarShape &src, const nvcv::ImageBatchVarShape &dst, const NVCVInterpolationType minInterpolation, const NVCVInterpolationType magInterpolation, bool antialias, const HQResizeRoisF rois) override { - HQResizeRun<2> resize(m_filtersFactory); - resize(stream, ws, src, dst, minInterpolation, magInterpolation, antialias, rois); + CVCUDA_NVTX_RANGE("cvcuda::hq_resize::HQResizeImpl::operator()[ImageBatchVarShape]"); + (*m_impl2d)(stream, ws, src, dst, minInterpolation, magInterpolation, antialias, rois); } void operator()(cudaStream_t stream, const cvcuda::Workspace &ws, const nvcv::TensorBatch &src, const nvcv::TensorBatch &dst, const NVCVInterpolationType minInterpolation, const NVCVInterpolationType magInterpolation, bool antialias, const HQResizeRoisF rois) override { - if (src.layout().find('D') < 0) + CVCUDA_NVTX_RANGE("cvcuda::hq_resize::HQResizeImpl::operator()[TensorBatch]"); + // Planar (channel-first 2D, NCHW/CHW) batches are expanded into single-channel (H,W,1) plane + // views and run through the regular 2D path; the views alias the originals so the result is + // bit-identical to the interleaved path. Views are kept alive for the duration of the call. + const nvcv::TensorLayout layout = src.layout(); + if (layout == nvcv::TENSOR_NCHW || layout == nvcv::TENSOR_CHW) { - HQResizeRun<2> resize(m_filtersFactory); - resize(stream, ws, src, dst, minInterpolation, magInterpolation, antialias, rois); - } - else - { - HQResizeRun<3> resize(m_filtersFactory); - resize(stream, ws, src, dst, minInterpolation, magInterpolation, antialias, rois); + nvcv::TensorBatch planarSrc = ExpandPlanarTensorBatch(src); + nvcv::TensorBatch planarDst = ExpandPlanarTensorBatch(dst); + (*m_impl2d)(stream, ws, planarSrc, planarDst, minInterpolation, magInterpolation, antialias, rois); + return; } + bool is3d = layout.find('D') >= 0; + implForNDim(is3d ? 3 : 2)(stream, ws, src, dst, minInterpolation, magInterpolation, antialias, rois); } private: - filter::ResamplingFiltersFactory m_filtersFactory; + IHQResizeImpl &implForNDim(int ndim) const + { + if (ndim == 2) + return *m_impl2d; + if (ndim == 3) + return *m_impl3d; + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Only 2D or 3D resize is supported. Got unexpected number of extents to resize."); + } + + std::unique_ptr m_impl2d; + std::unique_ptr m_impl3d; }; } // namespace hq_resize @@ -2784,14 +179,12 @@ private: // Constructor ----------------------------------------------------------------- HQResize::HQResize() - { m_impl = std::make_unique(); } // Operator -------------------------------------------------------------------- -// Workspace esitmation for Tensor input cvcuda::WorkspaceRequirements HQResize::getWorkspaceRequirements(int batchSize, const HQResizeTensorShapeI inputShape, const HQResizeTensorShapeI outputShape, const NVCVInterpolationType minInterpolation, @@ -2802,7 +195,6 @@ cvcuda::WorkspaceRequirements HQResize::getWorkspaceRequirements(int batchSize, antialias, roi); } -// Workspace esitmation for ImageBatch and TensorBatch input cvcuda::WorkspaceRequirements HQResize::getWorkspaceRequirements(int batchSize, const HQResizeTensorShapesI inputShapes, const HQResizeTensorShapesI outputShapes, const NVCVInterpolationType minInterpolation, @@ -2819,29 +211,29 @@ cvcuda::WorkspaceRequirements HQResize::getWorkspaceRequirements(int return m_impl->getWorkspaceRequirements(maxBatchSize, maxShape); } -// Tensor variant void HQResize::operator()(cudaStream_t stream, const cvcuda::Workspace &ws, const nvcv::Tensor &src, const nvcv::Tensor &dst, const NVCVInterpolationType minInterpolation, const NVCVInterpolationType magInterpolation, bool antialias, const HQResizeRoiF *roi) const { + CVCUDA_NVTX_RANGE("cvcuda::HQResize::operator()[Tensor]"); assert(m_impl); m_impl->operator()(stream, ws, src, dst, minInterpolation, magInterpolation, antialias, roi); } -// ImageBatchVarShape variant void HQResize::operator()(cudaStream_t stream, const cvcuda::Workspace &ws, const nvcv::ImageBatchVarShape &src, const nvcv::ImageBatchVarShape &dst, const NVCVInterpolationType minInterpolation, const NVCVInterpolationType magInterpolation, bool antialias, const HQResizeRoisF rois) const { + CVCUDA_NVTX_RANGE("cvcuda::HQResize::operator()[ImageBatchVarShape]"); assert(m_impl); m_impl->operator()(stream, ws, src, dst, minInterpolation, magInterpolation, antialias, rois); } -// TensorBatch variant void HQResize::operator()(cudaStream_t stream, const cvcuda::Workspace &ws, const nvcv::TensorBatch &src, const nvcv::TensorBatch &dst, const NVCVInterpolationType minInterpolation, const NVCVInterpolationType magInterpolation, bool antialias, const HQResizeRoisF rois) const { + CVCUDA_NVTX_RANGE("cvcuda::HQResize::operator()[TensorBatch]"); assert(m_impl); m_impl->operator()(stream, ws, src, dst, minInterpolation, magInterpolation, antialias, rois); } diff --git a/src/cvcuda/priv/OpHQResize2D.cu b/src/cvcuda/priv/OpHQResize2D.cu new file mode 100644 index 000000000..f32bc91af --- /dev/null +++ b/src/cvcuda/priv/OpHQResize2D.cu @@ -0,0 +1,95 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "Nvtx.hpp" +#include "OpHQResizeDispatch.hpp" + +#include "OpHQResizeKernel.cuh" + +namespace cvcuda::priv::hq_resize { + +using kernel::HQResizeRun; + +class HQResizeImpl2D final : public IHQResizeImpl +{ +public: + cvcuda::WorkspaceRequirements getWorkspaceRequirements(int numSamples, const HQResizeTensorShapeI inputShape, + const HQResizeTensorShapeI outputShape, + const NVCVInterpolationType minInterpolation, + const NVCVInterpolationType magInterpolation, bool antialias, + const HQResizeRoiF *roi) const override + { + HQResizeRun<2> resize(m_filtersFactory); + return resize.getWorkspaceRequirements(numSamples, inputShape, outputShape, minInterpolation, magInterpolation, + antialias, roi); + } + + cvcuda::WorkspaceRequirements getWorkspaceRequirements(int numSamples, const HQResizeTensorShapesI inputShapes, + const HQResizeTensorShapesI outputShapes, + const NVCVInterpolationType minInterpolation, + const NVCVInterpolationType magInterpolation, bool antialias, + const HQResizeRoisF rois) const override + { + HQResizeRun<2> resize(m_filtersFactory); + return resize.getWorkspaceRequirements(numSamples, inputShapes, outputShapes, minInterpolation, + magInterpolation, antialias, rois); + } + + cvcuda::WorkspaceRequirements getWorkspaceRequirements(int maxBatchSize, + const HQResizeTensorShapeI maxShape) const override + { + HQResizeRun<2> resize(m_filtersFactory); + return resize.getWorkspaceRequirements(maxBatchSize, maxShape); + } + + void operator()(cudaStream_t stream, const cvcuda::Workspace &ws, const nvcv::Tensor &src, const nvcv::Tensor &dst, + const NVCVInterpolationType minInterpolation, const NVCVInterpolationType magInterpolation, + bool antialias, const HQResizeRoiF *roi) override + { + CVCUDA_NVTX_RANGE("cvcuda::hq_resize::HQResizeImpl2D::operator()[Tensor]"); + HQResizeRun<2> resize(m_filtersFactory); + resize(stream, ws, src, dst, minInterpolation, magInterpolation, antialias, roi); + } + + void operator()(cudaStream_t stream, const cvcuda::Workspace &ws, const nvcv::ImageBatchVarShape &src, + const nvcv::ImageBatchVarShape &dst, const NVCVInterpolationType minInterpolation, + const NVCVInterpolationType magInterpolation, bool antialias, const HQResizeRoisF rois) override + { + CVCUDA_NVTX_RANGE("cvcuda::hq_resize::HQResizeImpl2D::operator()[ImageBatchVarShape]"); + HQResizeRun<2> resize(m_filtersFactory); + resize(stream, ws, src, dst, minInterpolation, magInterpolation, antialias, rois); + } + + void operator()(cudaStream_t stream, const cvcuda::Workspace &ws, const nvcv::TensorBatch &src, + const nvcv::TensorBatch &dst, const NVCVInterpolationType minInterpolation, + const NVCVInterpolationType magInterpolation, bool antialias, const HQResizeRoisF rois) override + { + CVCUDA_NVTX_RANGE("cvcuda::hq_resize::HQResizeImpl2D::operator()[TensorBatch]"); + HQResizeRun<2> resize(m_filtersFactory); + resize(stream, ws, src, dst, minInterpolation, magInterpolation, antialias, rois); + } + +private: + filter::ResamplingFiltersFactory m_filtersFactory; +}; + +std::unique_ptr makeImpl2D() +{ + return std::make_unique(); +} + +} // namespace cvcuda::priv::hq_resize diff --git a/src/cvcuda/priv/OpHQResize3D.cu b/src/cvcuda/priv/OpHQResize3D.cu new file mode 100644 index 000000000..0255c0088 --- /dev/null +++ b/src/cvcuda/priv/OpHQResize3D.cu @@ -0,0 +1,97 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "Nvtx.hpp" +#include "OpHQResizeDispatch.hpp" + +#include "OpHQResizeKernel.cuh" + +#include + +namespace cvcuda::priv::hq_resize { + +using kernel::HQResizeRun; + +class HQResizeImpl3D final : public IHQResizeImpl +{ +public: + cvcuda::WorkspaceRequirements getWorkspaceRequirements(int numSamples, const HQResizeTensorShapeI inputShape, + const HQResizeTensorShapeI outputShape, + const NVCVInterpolationType minInterpolation, + const NVCVInterpolationType magInterpolation, bool antialias, + const HQResizeRoiF *roi) const override + { + HQResizeRun<3> resize(m_filtersFactory); + return resize.getWorkspaceRequirements(numSamples, inputShape, outputShape, minInterpolation, magInterpolation, + antialias, roi); + } + + cvcuda::WorkspaceRequirements getWorkspaceRequirements(int numSamples, const HQResizeTensorShapesI inputShapes, + const HQResizeTensorShapesI outputShapes, + const NVCVInterpolationType minInterpolation, + const NVCVInterpolationType magInterpolation, bool antialias, + const HQResizeRoisF rois) const override + { + HQResizeRun<3> resize(m_filtersFactory); + return resize.getWorkspaceRequirements(numSamples, inputShapes, outputShapes, minInterpolation, + magInterpolation, antialias, rois); + } + + cvcuda::WorkspaceRequirements getWorkspaceRequirements(int maxBatchSize, + const HQResizeTensorShapeI maxShape) const override + { + HQResizeRun<3> resize(m_filtersFactory); + return resize.getWorkspaceRequirements(maxBatchSize, maxShape); + } + + void operator()(cudaStream_t stream, const cvcuda::Workspace &ws, const nvcv::Tensor &src, const nvcv::Tensor &dst, + const NVCVInterpolationType minInterpolation, const NVCVInterpolationType magInterpolation, + bool antialias, const HQResizeRoiF *roi) override + { + CVCUDA_NVTX_RANGE("cvcuda::hq_resize::HQResizeImpl3D::operator()[Tensor]"); + HQResizeRun<3> resize(m_filtersFactory); + resize(stream, ws, src, dst, minInterpolation, magInterpolation, antialias, roi); + } + + void operator()(cudaStream_t, const cvcuda::Workspace &, const nvcv::ImageBatchVarShape &, + const nvcv::ImageBatchVarShape &, const NVCVInterpolationType, const NVCVInterpolationType, bool, + const HQResizeRoisF) override + { + CVCUDA_NVTX_RANGE("cvcuda::hq_resize::HQResizeImpl3D::operator()[ImageBatchVarShape]"); + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "ImageBatchVarShape is only supported for 2D resize."); + } + + void operator()(cudaStream_t stream, const cvcuda::Workspace &ws, const nvcv::TensorBatch &src, + const nvcv::TensorBatch &dst, const NVCVInterpolationType minInterpolation, + const NVCVInterpolationType magInterpolation, bool antialias, const HQResizeRoisF rois) override + { + CVCUDA_NVTX_RANGE("cvcuda::hq_resize::HQResizeImpl3D::operator()[TensorBatch]"); + HQResizeRun<3> resize(m_filtersFactory); + resize(stream, ws, src, dst, minInterpolation, magInterpolation, antialias, rois); + } + +private: + filter::ResamplingFiltersFactory m_filtersFactory; +}; + +std::unique_ptr makeImpl3D() +{ + return std::make_unique(); +} + +} // namespace cvcuda::priv::hq_resize diff --git a/src/cvcuda/priv/OpHQResizeBatchWrap.cuh b/src/cvcuda/priv/OpHQResizeBatchWrap.cuh index 50c92314e..5436becff 100644 --- a/src/cvcuda/priv/OpHQResizeBatchWrap.cuh +++ b/src/cvcuda/priv/OpHQResizeBatchWrap.cuh @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,6 +20,8 @@ #include "WorkspaceUtil.hpp" #include "cvcuda/Workspace.hpp" +#include "OpHQResizePlanar.cuh" + #include #include #include @@ -274,8 +276,8 @@ struct DynamicBatchWrap { static_assert(1 <= kVariableStrides && kVariableStrides <= 3); - auto sample = m_samples[sampleIdx]; - const unsigned char *basePtr = sample.basePtr; + auto sample = m_samples[sampleIdx]; + auto *basePtr = sample.basePtr; if constexpr (kVariableStrides == 1) { @@ -374,23 +376,31 @@ struct ImageBatchVarShapeWrapAdapter using TensorWrapT = cuda::TensorNDWrap; static_assert(kVariableStrides == TensorWrapT::kVariableStrides); - ImageBatchVarShapeWrapAdapter(const nvcv::ImageBatchVarShapeDataStridedCuda &batchData) + // channels == 1 (the default) is the interleaved path: sampleIdx maps to image sampleIdx, + // plane 0 -- identical to the previous behavior. For planar (multi-plane) batches the kernel + // runs over numImages*channels expanded samples and each expanded index decodes to its + // {image, plane}, so every channel plane is processed as an independent single-channel image. + ImageBatchVarShapeWrapAdapter(const nvcv::ImageBatchVarShapeDataStridedCuda &batchData, int channels = 1) : m_batch{cuda::ImageBatchVarShapeWrap{batchData}} + , m_channels{channels} { } inline __device__ TensorWrapT GetSampleView(const int sampleIdx, const VecI<2> roi) const { - return TensorWrapT{m_batch.ptr(sampleIdx, 0, roi.y, roi.x), m_batch.rowStride(sampleIdx)}; + const int2 ip = planar::DecodePlane(sampleIdx, m_channels); // {image, plane} + return TensorWrapT{m_batch.ptr(ip.x, ip.y, roi.y, roi.x), m_batch.rowStride(ip.x, ip.y)}; } inline __device__ TensorWrapT GetSampleView(const int sampleIdx) const { - return TensorWrapT{m_batch.ptr(sampleIdx, 0, 0, 0), m_batch.rowStride(sampleIdx)}; + const int2 ip = planar::DecodePlane(sampleIdx, m_channels); // {image, plane} + return TensorWrapT{m_batch.ptr(ip.x, ip.y, 0, 0), m_batch.rowStride(ip.x, ip.y)}; } private: cuda::ImageBatchVarShapeWrap m_batch; + int m_channels; }; template @@ -406,7 +416,10 @@ struct TensorBatchWrapAdapter static_assert(kVariableStrides == TensorWrapT::kVariableStrides); static_assert(kVariableStrides == TensorBatchWrapT::kVariableStrides); - TensorBatchWrapAdapter(const nvcv::TensorBatchDataStridedCuda &batchData) + // The trailing argument exists only to share RunPasses' adapter construction with the + // ImageBatchVarShape adapter (which uses it for plane decode). TensorBatch planar is not yet + // supported, so it is always 1 here and ignored. + TensorBatchWrapAdapter(const nvcv::TensorBatchDataStridedCuda &batchData, int = 1) : m_batch{TensorBatchWrapT{batchData}} { } diff --git a/src/cvcuda/priv/OpHQResizeDispatch.hpp b/src/cvcuda/priv/OpHQResizeDispatch.hpp new file mode 100644 index 000000000..5db92ca88 --- /dev/null +++ b/src/cvcuda/priv/OpHQResizeDispatch.hpp @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#ifndef CVCUDA_PRIV_HQ_RESIZE_DISPATCH_HPP +#define CVCUDA_PRIV_HQ_RESIZE_DISPATCH_HPP + +#include "OpHQResize.hpp" + +#include + +namespace cvcuda::priv::hq_resize { + +std::unique_ptr makeImpl2D(); +std::unique_ptr makeImpl3D(); + +} // namespace cvcuda::priv::hq_resize + +#endif // CVCUDA_PRIV_HQ_RESIZE_DISPATCH_HPP diff --git a/src/cvcuda/priv/OpHQResizeFilter.cuh b/src/cvcuda/priv/OpHQResizeFilter.cuh index 7c7b1dcdb..a73219a9b 100644 --- a/src/cvcuda/priv/OpHQResizeFilter.cuh +++ b/src/cvcuda/priv/OpHQResizeFilter.cuh @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,8 @@ #ifndef CVCUDA_PRIV_HQ_RESIZE_FILTER_CUH #define CVCUDA_PRIV_HQ_RESIZE_FILTER_CUH +#include "PerDeviceResource.hpp" + #include #include // for NVCVInterpolationType, etc. #include @@ -203,7 +205,7 @@ inline void InitTriangularFilter(ResamplingFilter filter) inline void InitGaussianFilter(ResamplingFilter filter) { InitFilter(filter, - [&](int i) + [&filter](int i) { float x = 4 * (i - (filter.numCoeffs - 1) * 0.5f) / (filter.numCoeffs - 1); return expf(-x * x); @@ -213,7 +215,7 @@ inline void InitGaussianFilter(ResamplingFilter filter) inline void InitLanczosFilter(ResamplingFilter filter, float a) { InitFilter(filter, - [&](int i) + [&filter, &a](int i) { float x = 2 * a * (i - (filter.numCoeffs - 1) * 0.5f) / (filter.numCoeffs - 1); return LanczosWindow(x, a); @@ -224,7 +226,7 @@ inline void InitLanczosFilter(ResamplingFilter filter, float a) inline void InitCubicFilter(ResamplingFilter filter) { InitFilter(filter, - [&](int i) + [&filter](int i) { float x = 4 * (i - (filter.numCoeffs - 1) * 0.5f) / (filter.numCoeffs - 1); return CubicWindow(x); @@ -255,106 +257,101 @@ public: static constexpr int kTotalSize = kTriangularSize + kGaussianSize + kCubicSize + kLanczosSize; ResamplingFiltersFactory() - : m_deviceId{[]() - { - int deviceId; - NVCV_CHECK_THROW(cudaGetDevice(&deviceId)); - return deviceId; - }()} + : m_state([](int deviceId) { return std::make_unique(deviceId); }) + { + } + int GetDeviceComputeCapability() const { - // Pinned memory is needed for proper synchronization of the synchronous copy - std::unique_ptr> filterDataPinned; - { - float *ptr = nullptr; - NVCV_CHECK_THROW(cudaMallocHost(&ptr, kTotalSize * sizeof(float))); - filterDataPinned = {ptr, [](void *ptr) - { - NVCV_CHECK_THROW(cudaFreeHost(ptr)); - }}; - } - { - float *ptr = nullptr; - NVCV_CHECK_THROW(cudaMalloc(&ptr, kTotalSize * sizeof(float))); - m_filterDataGpu = {ptr, [](void *ptr) - { - NVCV_CHECK_THROW(cudaFree(ptr)); - }}; - } - auto addFilter = [&](FilterIdx filterIdx, int size) - { - float *base = filterIdx == 0 ? filterDataPinned.get() - : m_filters[filterIdx - 1].coeffs + m_filters[filterIdx - 1].numCoeffs; - m_filters[filterIdx] = {base, size, 1, (size - 1) * 0.5f}; - }; - addFilter(Idx_Triangular, kTriangularSize); - InitTriangularFilter(m_filters[Idx_Triangular]); - addFilter(Idx_Gaussian, kGaussianSize); - InitGaussianFilter(m_filters[Idx_Gaussian]); - addFilter(Idx_Lanczos3, kLanczosSize); - InitLanczosFilter(m_filters[Idx_Lanczos3], kLanczosA); - addFilter(Idx_Cubic, kCubicSize); - InitCubicFilter(m_filters[Idx_Cubic]); - - // According to cuda-driver-api: For transfers from pinned host memory to device memory, - // the cudaMemcpy is synchronous with respect to the host. - NVCV_CHECK_THROW(cudaMemcpy(m_filterDataGpu.get(), filterDataPinned.get(), kTotalSize * sizeof(float), - cudaMemcpyHostToDevice)); - // Set the pointers to the corresponding offsets in m_filterDataGpu - ptrdiff_t diff = m_filterDataGpu.get() - filterDataPinned.get(); - for (auto &f : m_filters) - { - f.coeffs += diff; - } + return m_state.get().computeCapability; } - ResamplingFilter CreateCubic(float radius = 2.0f) const noexcept + ResamplingFilter CreateCubic(float radius = 2.0f) const { - validateDeviceId(); - auto flt = m_filters[Idx_Cubic]; + auto flt = m_state.get().filters[Idx_Cubic]; flt.rescale(2.0f * std::max(2.0f, radius)); return flt; } - ResamplingFilter CreateGaussian(float sigma) const noexcept + ResamplingFilter CreateGaussian(float sigma) const { - validateDeviceId(); - auto flt = m_filters[Idx_Gaussian]; + auto flt = m_state.get().filters[Idx_Gaussian]; flt.rescale(std::max(1.0f, static_cast(4 * M_SQRT2) * sigma)); return flt; } - ResamplingFilter CreateLanczos3(float radius = 3.0f) const noexcept + ResamplingFilter CreateLanczos3(float radius = 3.0f) const { - validateDeviceId(); - auto flt = m_filters[Idx_Lanczos3]; + auto flt = m_state.get().filters[Idx_Lanczos3]; flt.rescale(2.0f * std::max(3.0f, radius)); return flt; } - ResamplingFilter CreateTriangular(float radius) const noexcept + ResamplingFilter CreateTriangular(float radius) const { - validateDeviceId(); - auto flt = m_filters[Idx_Triangular]; + auto flt = m_state.get().filters[Idx_Triangular]; flt.rescale(std::max(1.0f, 2 * radius)); return flt; } private: - void validateDeviceId() const + struct DeviceFilterState { - int deviceId; - NVCV_CHECK_THROW(cudaGetDevice(&deviceId)); - if (deviceId != m_deviceId) + std::unique_ptr> filterDataGpu; + ResamplingFilter filters[kNumFilters]; + int computeCapability{}; + + explicit DeviceFilterState(int deviceId) { - throw nvcv::Exception(nvcv::Status::ERROR_DEVICE, - "The HQ resize operator was initialized and called with different current device."); + int major{}; + int minor{}; + NVCV_CHECK_THROW(cudaDeviceGetAttribute(&major, cudaDevAttrComputeCapabilityMajor, deviceId)); + NVCV_CHECK_THROW(cudaDeviceGetAttribute(&minor, cudaDevAttrComputeCapabilityMinor, deviceId)); + computeCapability = major * 10 + minor; + + std::unique_ptr> filterDataPinned; + { + float *ptr = nullptr; + NVCV_CHECK_THROW(cudaMallocHost(&ptr, kTotalSize * sizeof(float))); + filterDataPinned = {ptr, [](void *ptr) + { + NVCV_CHECK_THROW(cudaFreeHost(ptr)); + }}; + } + { + float *ptr = nullptr; + NVCV_CHECK_THROW(cudaMalloc(&ptr, kTotalSize * sizeof(float))); + filterDataGpu = {ptr, [](void *ptr) + { + NVCV_CHECK_THROW(cudaFree(ptr)); + }}; + } + auto addFilter = [this, &filterDataPinned](FilterIdx filterIdx, int size) + { + float *base = filterIdx == 0 ? filterDataPinned.get() + : filters[filterIdx - 1].coeffs + filters[filterIdx - 1].numCoeffs; + filters[filterIdx] = {base, size, 1, (size - 1) * 0.5f}; + }; + addFilter(Idx_Triangular, kTriangularSize); + InitTriangularFilter(filters[Idx_Triangular]); + addFilter(Idx_Gaussian, kGaussianSize); + InitGaussianFilter(filters[Idx_Gaussian]); + addFilter(Idx_Lanczos3, kLanczosSize); + InitLanczosFilter(filters[Idx_Lanczos3], kLanczosA); + addFilter(Idx_Cubic, kCubicSize); + InitCubicFilter(filters[Idx_Cubic]); + + NVCV_CHECK_THROW(cudaMemcpy(filterDataGpu.get(), filterDataPinned.get(), kTotalSize * sizeof(float), + cudaMemcpyHostToDevice)); + ptrdiff_t diff = filterDataGpu.get() - filterDataPinned.get(); + for (auto &f : filters) + { + f.coeffs += diff; + } } - } + }; - int m_deviceId; - std::unique_ptr> m_filterDataGpu; - ResamplingFilter m_filters[kNumFilters]; + mutable cvcuda::priv::PerDeviceResource m_state; }; inline ResamplingFilter GetResamplingFilter(const ResamplingFiltersFactory &filtersFactory, diff --git a/src/cvcuda/priv/OpHQResizeKernel.cuh b/src/cvcuda/priv/OpHQResizeKernel.cuh new file mode 100644 index 000000000..e868cf8ad --- /dev/null +++ b/src/cvcuda/priv/OpHQResizeKernel.cuh @@ -0,0 +1,4604 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ +#ifndef CVCUDA_PRIV_HQ_RESIZE_KERNEL_CUH +#define CVCUDA_PRIV_HQ_RESIZE_KERNEL_CUH + +#include "OpHQResize.hpp" +#include "OpHQResizePolicy.hpp" +#include "WorkspaceUtil.hpp" +#include "cvcuda/Workspace.hpp" + +#include "OpHQResizeBatchWrap.cuh" +#include "OpHQResizeFilter.cuh" +#include "OpHQResizePlanar.cuh" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace cvcuda::priv::hq_resize::kernel { + +namespace cuda = nvcv::cuda; +namespace filter = cvcuda::priv::hq_resize::filter; +namespace batch_wrapper = cvcuda::priv::hq_resize::batch_wrapper; + +template +using Vec = typename cuda::MakeType; + +template +using VecI = Vec; + +template +using VecF = Vec; + +class TensorShapeError : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + +namespace utils { + +template>> +inline std::enable_if_t>, int64_t> Volume(const T &v) +{ + int64_t vol = 1; + for (int i = 0; i < cuda::NumComponents; i++) + { + vol *= cuda::GetElement(v, i); + } + return vol; +} + +template>> +auto DivCeil(const T &a, const T &b) +{ + return (a + b - 1) / b; +} +} // namespace utils + +/** + * @brief Base (per-channel) type of a wrap's value type, as every dispatch predicate + * and lane policy needs it. + */ +template +using WrapBaseT = cuda::BaseType>; + +namespace resampling { + +template +struct SampleDesc +{ + static constexpr int kSpatialNDim = _kSpatialNDim; + + // input, output and the intermediate buffers + static constexpr int kNumBuffers = kSpatialNDim + 1; + + // shapes[0] - input shape, consecutive intermediate results shapes, + // shapes[kSpatialNDim] - output shape + VecI shapes[kNumBuffers]; + + // the number of channels in the sample, common for input, + // intermediate and output sample + int channels; + + // describes which axis to processes in a given resampling pass, e.g. + // if processingOrder.x = 2, then in the first pass the z axis + // will be resampled + VecI processingOrder; + + // resampling origin and scale in pass order, i.e. + // origin.x and scale.x describe origin and scale for resampling + // in the first pass + VecF origin, scale; + + // what type of filter to use (NN, Linear, Support based) + // in pass order (i.e. filterKind[0] refers to filter used in the first pass) + filter::FilterTypeKind filterKind[kSpatialNDim]; + + // filter description (support, coefficients etc.) + // in pass order (i.e. filter[0] refers to filter used in the first pass) + filter::ResamplingFilter filter[kSpatialNDim]; + + // spatial offset in the input sample based on the input ROI + // and filter support + VecI inRoiOffset; + + // describes the logical block shape, i.e. a size of a slice + // that a single gpu block will process in a given pass + VecI blockShape[kSpatialNDim]; +}; + +/** + * @brief Helper structure to indicate the static number of channels + * dynamic number of channels that may differ between samples. + */ +template +struct NumChannels +{ + constexpr int __forceinline__ __device__ operator()() const + { + return kStaticChannels; + } + + static constexpr bool kHasStaticChannels = true; + static constexpr int kStaticChannels = _kStaticChannels; +}; + +template<> +struct NumChannels<-1> +{ + int __forceinline__ __device__ operator()() const + { + return dynamicChannels; + } + + static constexpr bool kHasStaticChannels = false; + static constexpr int kStaticChannels = -1; + int dynamicChannels; +}; + +template +__forceinline__ __device__ void WithChannels(const int dynamicChannels, Cb &&cb) +{ + if constexpr (kNumStaticChannels == -1) + { + cb(NumChannels<-1>{dynamicChannels}); + } + else if constexpr (kNumStaticChannels != -1) + { + static_assert(kNumStaticChannels > 0); + cb(NumChannels{}); + } +} + +/** + * @brief Each threadblock will cover `lanes * volume(blockDim)` + * elements of the output sample. More lanes result in: + * 1. smaller grid launched (possibly reducing parallelism for small images), + * 2. better resuing of the filter's coefficients + * (they are computed once for all lanes). + * + * @return int - the number of lanes for a single threadblock + * to cover in the output image + */ +inline int GetResizeBlockLanesEnv() +{ + char *env = getenv("CVCUDA_HQ_RESIZE_BLOCK_LANES"); + if (env) + { + int lanes = atoi(env); + if (lanes < 1) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "The CVCUDA_HQ_RESIZE_BLOCK_LANES must be a positive integer"); + } + return lanes; + } + else + { + return 8; + } +} + +inline int GetResizeBlockLanes() +{ + static int lanes = GetResizeBlockLanesEnv(); + return lanes; +} + +inline constexpr int GetF32CubicMagnificationBlockLanes(int computeCapability) +{ + // The wider tile wins on the measured SM80/SM86/SM90 devices, but regresses SM89. + // Keep the current behavior everywhere else to avoid changing unmeasured architectures. + return computeCapability == 89 ? 8 : 16; +} + +static_assert(GetF32CubicMagnificationBlockLanes(80) == 16); +static_assert(GetF32CubicMagnificationBlockLanes(86) == 16); +static_assert(GetF32CubicMagnificationBlockLanes(89) == 8); +static_assert(GetF32CubicMagnificationBlockLanes(90) == 16); +static_assert(GetF32CubicMagnificationBlockLanes(100) == 16); + +template +struct GridHelperDevice +{ +}; + +/** + * @brief Maps cuda blockIdx to sample and bounds of the sample region + * to be processed be the threadblock for 2D resampling + */ +template<> +struct GridHelperDevice<2> +{ + GridHelperDevice(VecI<2> numBlocks) + : m_numBlocksX{numBlocks.x} + { + } + + int __forceinline__ __device__ CurrentSample() const + { + return blockIdx.y; + } + + void __forceinline__ __device__ CurrentBlock(VecI<2> &lo, VecI<2> &hi, const VecI<2> blockShape) const + + { + VecI<2> currentBlock; + { + int block = blockIdx.x; + currentBlock.x = block % m_numBlocksX; + currentBlock.y = block / m_numBlocksX; + } + lo = blockShape * currentBlock; + hi = lo + blockShape; + } + +private: + int m_numBlocksX; +}; + +/** + * @brief Maps cuda blockIdx to sample and bounds of the sample region + * to be processed be the threadblock for 3D resampling + */ +template<> +struct GridHelperDevice<3> +{ + GridHelperDevice(VecI<3> numBlocks) + : m_numBlocksX{numBlocks.x} + , m_numBlocksY{numBlocks.y} + { + } + + int __forceinline__ __device__ CurrentSample() const + { + return blockIdx.y; + } + + void __forceinline__ __device__ CurrentBlock(VecI<3> &lo, VecI<3> &hi, const VecI<3> blockShape) const + + { + VecI<3> currentBlock; + { + int block = blockIdx.x; + currentBlock.x = block % m_numBlocksX; + block = block / m_numBlocksX; + currentBlock.y = block % m_numBlocksY; + currentBlock.z = block / m_numBlocksY; + } + lo = blockShape * currentBlock; + hi = lo + blockShape; + } + +private: + int m_numBlocksX, m_numBlocksY; +}; + +/** + * @brief Maps the logical blocks and the number of samples into cuda grid and back. + */ +template +struct GridHelper +{ + GridHelper(VecI numBlocks, int numSamples) + : m_numBlocks{numBlocks} + , m_numSamples{numSamples} + { + } + + template + std::enable_if_t GetKernelGrid() const + { + static_assert(kSpatialNDim == 2); + return dim3(m_numBlocks.x * m_numBlocks.y, m_numSamples, 1); + } + + template + std::enable_if_t GetKernelGrid() const + { + static_assert(kSpatialNDim == 3); + return dim3(m_numBlocks.x * m_numBlocks.y * m_numBlocks.z, m_numSamples, 1); + } + + GridHelperDevice GetDeviceGridHelper() + { + return {m_numBlocks}; + } + +private: + VecI m_numBlocks; + int m_numSamples; +}; + +// The namespace contains implementation of different resampling +// methods in device code. +/** + * @brief acc[c] += src[c] * w for every channel, with the fmaf sequence every fused + * kernel must share so their rounding matches the separable passes. + */ +template +void __forceinline__ __device__ FmaPerChannel(FloatT &acc, const SrcT &src, const float w) +{ + static_assert(cuda::NumElements == cuda::NumElements); +#pragma unroll + for (int c = 0; c < cuda::NumElements; c++) + { + cuda::GetElement(acc, c) = fmaf(cuda::GetElement(src, c), w, cuda::GetElement(acc, c)); + } +} + +template +void __forceinline__ __device__ ScalePerChannel(FloatT &v, const float s) +{ +#pragma unroll + for (int c = 0; c < cuda::NumElements; c++) + { + cuda::GetElement(v, c) *= s; + } +} + +namespace interpolate { + +template +auto __forceinline__ __device__ GetWrapPtr(const Wrap wrap, const VecI<2> yx, const Idxs... idxs) +{ + return wrap.ptr(yx.y, yx.x, idxs...); +} + +template +auto __forceinline__ __device__ GetWrapPtr(const Wrap wrap, const VecI<3> zyx, const Idxs... idxs) +{ + return wrap.ptr(zyx.z, zyx.y, zyx.x, idxs...); +} + +template +std::enable_if_t __forceinline__ __device__ + LoadPixelLdg(const Wrap wrap, const NumChannelsT numChannels, const Idxs... idxs) +{ + using T = std::remove_const_t; + using BT = cuda::BaseType; + constexpr int kStaticChannels = NumChannelsT::kStaticChannels; + static_assert(kStaticChannels == cuda::NumElements); + + constexpr bool kSupportsLdg = kStaticChannels == 2 || kStaticChannels == 4; + + if constexpr (kSupportsLdg) + { + return __ldg(GetWrapPtr(wrap, idxs...)); + } + else if constexpr (!kSupportsLdg) + { + const BT *basePtr = reinterpret_cast(GetWrapPtr(wrap, idxs...)); + T value; +#pragma unroll + for (int c = 0; c < kStaticChannels; c++) + { + cuda::GetElement(value, c) = __ldg(basePtr + c); + } + return value; + } +} + +template +std::enable_if_t __forceinline__ __device__ + LoadPixelLdg(const Wrap wrap, const NumChannelsT numChannels, const Idxs... idxs) +{ + static_assert(!cuda::IsCompound); + return __ldg(GetWrapPtr(wrap, idxs...)); +} + +constexpr int kVertXVec = 4; +constexpr int kVertXVecMaxSupport = 16; +constexpr int kVertXVecMaxScale = 4; + +namespace nn { + +template +void __forceinline__ __device__ ForAllPixels(const VecI<2> lo, const VecI<2> hi, ProcessPixel &&processPixel) +{ + for (int y = lo.y + threadIdx.y; y < hi.y; y += blockDim.y) + { + for (int x = lo.x + threadIdx.x; x < hi.x; x += blockDim.x) + { + processPixel(VecI<2>{x, y}); + } + } +} + +template +void __forceinline__ __device__ ForAllPixels(const VecI<3> lo, const VecI<3> hi, ProcessPixel &&processPixel) +{ + for (int z = lo.z + threadIdx.z; z < hi.z; z += blockDim.z) + { + for (int y = lo.y + threadIdx.y; y < hi.y; y += blockDim.y) + { + for (int x = lo.x + threadIdx.x; x < hi.x; x += blockDim.x) + { + processPixel(VecI<3>{x, y, z}); + } + } + } +} + +/** + * @brief Nearest neighbor resampling + * + * @param outWrap - the wrapper for accessing output data + * @param inWrap - the wrapper for accessing input data + * @param lo - inclusive lower bound output coordinates of the block processed by the threadblock + * @param hi - exclusive upper bound output coordinates of the block processed by the threadblock + * @param origin - source coordinates corresponding to output's (0, 0) + * @param scale - step, in source coordinates, for one pixel in output coordinates + * @param inShape - shape of the input (x, y) order + * @param numChannels - the NumChannels specialization describing the number of interleaved + * channels in the input and output sample. + */ +template +void __forceinline__ __device__ Resample(const PassOutWrap outWrap, const PassInWrap inWrap, + const VecI lo, const VecI hi, + VecF origin, const VecF scale, + const VecI inShape, const NumChannelsT numChannels) +{ + using OutT = typename PassOutWrap::ValueType; + using InT = typename PassInWrap::ValueType; + // spatial extents and optional channels extent + constexpr int kNDim = kSpatialNDim + !NumChannelsT::kHasStaticChannels; + + static_assert(!NumChannelsT::kHasStaticChannels || NumChannelsT::kStaticChannels == cuda::NumElements); + static_assert(cuda::NumElements == cuda::NumElements); + static_assert(PassOutWrap::kNumDimensions == kNDim); + static_assert(PassInWrap::kNumDimensions == kNDim); + + origin += 0.5f * scale; + ForAllPixels(lo, hi, + [=](const VecI outIdxs) + { + VecI inIdxs = cuda::round(outIdxs * scale + origin); + inIdxs = cuda::clamp(inIdxs, cuda::SetAll>(0), inShape - 1); + + if constexpr (NumChannelsT::kHasStaticChannels) + { + const InT in = LoadPixelLdg(inWrap, numChannels, inIdxs); + OutT &out = *GetWrapPtr(outWrap, outIdxs); + out = cuda::SaturateCast(in); + } + else if constexpr (!NumChannelsT::kHasStaticChannels) + { + for (int c = 0; c < numChannels(); c++) + { + const InT in = LoadPixelLdg(inWrap, numChannels, inIdxs, c); + OutT &out = *GetWrapPtr(outWrap, outIdxs, c); + out = cuda::SaturateCast(in); + } + } + }); +} + +} // namespace nn + +namespace linear { + +template +void __forceinline__ __device__ Linear(const PassOutWrap outWrap, const PassInWrap inWrap, + const NumChannelsT numChannels, const VecI inIdx0, + const VecI inIdx1, const float q, const VecI outIdx) +{ + using OutT = typename PassOutWrap::ValueType; + using InT = std::remove_const_t; + // spatial extents and optional channels extent + constexpr int kNDim = kSpatialNDim + !NumChannelsT::kHasStaticChannels; + + static_assert(!NumChannelsT::kHasStaticChannels || NumChannelsT::kStaticChannels == cuda::NumElements); + static_assert(cuda::NumElements == cuda::NumElements); + static_assert(PassOutWrap::kNumDimensions == kNDim); + static_assert(PassInWrap::kNumDimensions == kNDim); + + if constexpr (NumChannelsT::kHasStaticChannels) + { + using FloatT = cuda::ConvertBaseTypeTo; + const FloatT a = cuda::StaticCast(LoadPixelLdg(inWrap, numChannels, inIdx0)); + const FloatT b = cuda::StaticCast(LoadPixelLdg(inWrap, numChannels, inIdx1)); + FloatT tmp = b - a; +#pragma unroll + for (int c = 0; c < NumChannelsT::kStaticChannels; c++) + { + cuda::GetElement(tmp, c) = fmaf(cuda::GetElement(tmp, c), q, cuda::GetElement(a, c)); + } + OutT &out = *GetWrapPtr(outWrap, outIdx); + out = cuda::SaturateCast(tmp); + } + else if constexpr (!NumChannelsT::kHasStaticChannels) + { + for (int c = 0; c < numChannels(); c++) + { + const float a = LoadPixelLdg(inWrap, numChannels, inIdx0, c); + const float b = LoadPixelLdg(inWrap, numChannels, inIdx1, c); + const float tmp = fmaf(b - a, q, a); + OutT &out = *GetWrapPtr(outWrap, outIdx, c); + out = cuda::SaturateCast(tmp); + } + } +} + +template +void __forceinline__ __device__ ForAllPixelsHorz(const VecI<2> lo, const VecI<2> hi, ProcessPixel &&processPixel) +{ + for (int x = lo.x + threadIdx.x; x < hi.x; x += blockDim.x) + { + for (int y = threadIdx.y + lo.y; y < hi.y; y += blockDim.y) + { + processPixel(VecI<2>{x, y}); + } + } +} + +template +void __forceinline__ __device__ ForAllPixelsHorz(const VecI<3> lo, const VecI<3> hi, ProcessPixel &&processPixel) +{ + for (int x = lo.x + threadIdx.x; x < hi.x; x += blockDim.x) + { + for (int z = threadIdx.z + lo.z; z < hi.z; z += blockDim.z) + { + for (int y = threadIdx.y + lo.y; y < hi.y; y += blockDim.y) + { + processPixel(VecI<3>{x, y, z}); + } + } + } +} + +template +void __forceinline__ __device__ ForAllPixelsVert(const VecI<2> lo, const VecI<2> hi, ProcessPixel &&processPixel) +{ + for (int y = threadIdx.y + lo.y; y < hi.y; y += blockDim.y) + { + for (int x = lo.x + threadIdx.x; x < hi.x; x += blockDim.x) + { + processPixel(VecI<2>{x, y}); + } + } +} + +template +void __forceinline__ __device__ ForAllPixelsVert(const VecI<3> lo, const VecI<3> hi, ProcessPixel &&processPixel) +{ + for (int z = threadIdx.z + lo.z; z < hi.z; z += blockDim.z) + { + for (int y = threadIdx.y + lo.y; y < hi.y; y += blockDim.y) + { + for (int x = lo.x + threadIdx.x; x < hi.x; x += blockDim.x) + { + processPixel(VecI<3>{x, y, z}); + } + } + } +} + +/** + * @brief Implements horizontal resampling + * + * @param outWrap - the wrapper for accessing output data + * @param inWrap - the wrapper for accessing input data + * @param lo - inclusive lower bound output coordinates of the block processed by the threadblock + * @param hi - exclusive upper bound output coordinates of the block processed by the threadblock + * @param srcX0 - X coordinate in the source image corresponding to output 0 + * @param scale - step, in source X, for one pixel in output X (may be negative) + * @param inShape - shape of the input (x, y[, z]) order + * @param numChannels - the NumChannels specialization describing the number of interleaved + * channels in the input and output sample. + * + * The input region of interest is defined in terms of origin/scale, which are relative to + * output (0, 0). + * The lo/hi parameters are not output RoI - they merely indicate the output slice processed + * by current block. + */ +template +void __forceinline__ __device__ ResampleHorz(const PassOutWrap outWrap, const PassInWrap inWrap, + const VecI lo, const VecI hi, float srcX0, + const float scale, const VecI inShape, + const NumChannelsT numChannels) +{ + srcX0 += 0.5f * scale - 0.5f; + ForAllPixelsHorz(lo, hi, + [=](const VecI outIdx) + { + const float sx0f = outIdx.x * scale + srcX0; + const int sx0i = cuda::round(sx0f); + const float q = sx0f - sx0i; + const int sx0 = cuda::clamp(sx0i, 0, inShape.x - 1); + const int sx1 = cuda::clamp(sx0i + 1, 0, inShape.x - 1); + + VecI inIdx0 = outIdx; + VecI inIdx1 = outIdx; + inIdx0.x = sx0; + inIdx1.x = sx1; + + Linear(outWrap, inWrap, numChannels, inIdx0, inIdx1, q, outIdx); + }); +} + +/** + * @brief Implements vertical resampling + * + * @param outWrap - the wrapper for accessing output data + * @param inWrap - the wrapper for accessing input data + * @param lo - inclusive lower bound output coordinates of the block processed by the threadblock + * @param hi - exclusive upper bound output coordinates of the block processed by the threadblock + * @param srcY0 - Y coordinate in the source image corresponding to output 0 + * @param scale - step, in source Y, for one pixel in output Y (may be negative) + * @param inShape - shape of the input (x, y[, z]) order + * @param numChannels - the NumChannels specialization describing the number of interleaved + * channels in the input and output sample. + */ +template +void __forceinline__ __device__ ResampleVert(const PassOutWrap outWrap, const PassInWrap inWrap, + const VecI lo, const VecI hi, float srcY0, + const float scale, const VecI inShape, + const NumChannelsT numChannels) +{ + using OutT = typename PassOutWrap::ValueType; + using InT = std::remove_const_t; + using InBT = cuda::BaseType; + using OutBT = cuda::BaseType; + constexpr bool kUseVertXVec = kEnableVertXVec && kSpatialNDim == 2 && NumChannelsT::kHasStaticChannels + && NumChannelsT::kStaticChannels == 1 + && std::is_same_v && std::is_same_v; + + srcY0 += 0.5f * scale - 0.5f; + + if constexpr (kUseVertXVec) + { + if (scale >= 1.0f && scale <= kVertXVecMaxScale) + { + using InVec = Vec; + using OutVec = Vec; + using AccumVec = Vec; + + for (int y = threadIdx.y + lo.y; y < hi.y; y += blockDim.y) + { + const float sy0f = y * scale + srcY0; + const int sy0i = cuda::round(sy0f); + const float q = sy0f - sy0i; + const int sy0 = cuda::clamp(sy0i, 0, inShape.y - 1); + const int sy1 = cuda::clamp(sy0i + 1, 0, inShape.y - 1); + + for (int x0 = lo.x + static_cast(threadIdx.x) * kVertXVec; x0 < hi.x; + x0 += static_cast(blockDim.x) * kVertXVec) + { + const bool full = x0 + kVertXVec <= hi.x; + const bool aligned = full + && ((reinterpret_cast(GetWrapPtr(outWrap, VecI<2>{x0, y})) + & (alignof(OutVec) - 1)) + == 0) + && ((reinterpret_cast(GetWrapPtr(inWrap, VecI<2>{x0, sy0})) + & (alignof(InVec) - 1)) + == 0) + && ((reinterpret_cast(GetWrapPtr(inWrap, VecI<2>{x0, sy1})) + & (alignof(InVec) - 1)) + == 0); + + if (aligned) + { + const InVec px0 = *reinterpret_cast(GetWrapPtr(inWrap, VecI<2>{x0, sy0})); + const InVec px1 = *reinterpret_cast(GetWrapPtr(inWrap, VecI<2>{x0, sy1})); + AccumVec a = cuda::StaticCast(px0); + AccumVec tmp = cuda::StaticCast(px1) - a; +#pragma unroll + for (int lane = 0; lane < kVertXVec; lane++) + { + cuda::GetElement(tmp, lane) + = fmaf(cuda::GetElement(tmp, lane), q, cuda::GetElement(a, lane)); + } + *reinterpret_cast(GetWrapPtr(outWrap, VecI<2>{x0, y})) + = cuda::SaturateCast(tmp); + } + else + { + for (int lane = 0; lane < kVertXVec && x0 + lane < hi.x; lane++) + { + const int x = x0 + lane; + const InT a = LoadPixelLdg(inWrap, numChannels, VecI<2>{x, sy0}); + const InT b = LoadPixelLdg(inWrap, numChannels, VecI<2>{x, sy1}); + float tmp = b - a; + tmp = fmaf(tmp, q, static_cast(a)); + + OutT &out = *GetWrapPtr(outWrap, VecI<2>{x, y}); + out = cuda::SaturateCast(tmp); + } + } + } + } + return; + } + } + + ForAllPixelsVert(lo, hi, + [=](const VecI outIdx) + { + const float sy0f = outIdx.y * scale + srcY0; + const int sy0i = cuda::round(sy0f); + const float q = sy0f - sy0i; + const int sy0 = cuda::clamp(sy0i, 0, inShape.y - 1); + const int sy1 = cuda::clamp(sy0i + 1, 0, inShape.y - 1); + + VecI inIdx0 = outIdx; + VecI inIdx1 = outIdx; + inIdx0.y = sy0; + inIdx1.y = sy1; + + Linear(outWrap, inWrap, numChannels, inIdx0, inIdx1, q, outIdx); + }); +} + +/** + * @brief Implements depthwise resampling + * + * @param outWrap - the wrapper for accessing output data + * @param inWrap - the wrapper for accessing input data + * @param lo - inclusive lower bound output coordinates of the block processed by the threadblock + * @param hi - exclusive upper bound output coordinates of the block processed by the threadblock + * @param srcZ0 - Z coordinate in the source image corresponding to output's 0 + * @param scale - step, in source Z, for one pixel in output Z (may be negative) + * @param inShape - shape of the input (x, y[, z]) order + * @param numChannels - the NumChannels specialization describing the number of interleaved + * channels in the input and output sample. + */ +template +void __forceinline__ __device__ ResampleDepth(const PassOutWrap outWrap, const PassInWrap inWrap, const VecI<3> lo, + const VecI<3> hi, float srcZ0, const float scale, const VecI<3> inShape, + const NumChannelsT numChannels) +{ + srcZ0 += 0.5f * scale - 0.5f; + // threadIdx.y is used to traverse Z axis + for (int z = lo.z + threadIdx.y; z < hi.z; z += blockDim.y) + { + const float sz0f = z * scale + srcZ0; + const int sz0i = cuda::round(sz0f); + const float q = sz0f - sz0i; + const int sz0 = cuda::clamp(sz0i, 0, inShape.z - 1); + const int sz1 = cuda::clamp(sz0i + 1, 0, inShape.z - 1); + + for (int y = lo.y + threadIdx.z; y < hi.y; y += blockDim.z) + { + for (int x = lo.x + threadIdx.x; x < hi.x; x += blockDim.x) + { + VecI<3> inIdx0{x, y, sz0}; + VecI<3> inIdx1{x, y, sz1}; + VecI<3> outIdx{x, y, z}; + Linear<3>(outWrap, inWrap, numChannels, inIdx0, inIdx1, q, outIdx); + } + } + } +} + +} // namespace linear + +namespace filter_support { + +constexpr int kMaxGPUFilterSupport = 8192; + +bool __forceinline__ __host__ __device__ CanComputeCoefPerThread(const int support, const int resamplingAxisBlockSize) +{ + return support * resamplingAxisBlockSize <= kMaxGPUFilterSupport; +} + +inline int RequiredSharedMemoryElements(const int support, const int resamplingAxisBlockSize) +{ + if (CanComputeCoefPerThread(support, resamplingAxisBlockSize)) + { + return support * resamplingAxisBlockSize; + } + else + { + return support; + } +} + +template +void __forceinline__ __device__ ForAllOrthogonalToHorz(const VecI<2> lo, const VecI<2> hi, ProcessPixel &&processPixel) +{ + for (int y = threadIdx.y + lo.y; y < hi.y; y += blockDim.y) + { + processPixel(VecI<2>{0, y}); + } +} + +template +void __forceinline__ __device__ ForAllOrthogonalToHorz(const VecI<3> lo, const VecI<3> hi, ProcessPixel &&processPixel) +{ + for (int z = threadIdx.z + lo.z; z < hi.z; z += blockDim.z) + { + for (int y = threadIdx.y + lo.y; y < hi.y; y += blockDim.y) + { + processPixel(VecI<3>{0, y, z}); + } + } +} + +template +void __forceinline__ __device__ ForAllOrthogonalToVert(const VecI<2> lo, const VecI<2> hi, ProcessPixel &&processPixel) +{ + for (int x = threadIdx.x + lo.x; x < hi.x; x += blockDim.x) + { + processPixel(VecI<2>{x, 0}); + } +} + +template +void __forceinline__ __device__ ForAllOrthogonalToVert(const VecI<3> lo, const VecI<3> hi, ProcessPixel &&processPixel) +{ + for (int z = threadIdx.z + lo.z; z < hi.z; z += blockDim.z) + { + for (int x = threadIdx.x + lo.x; x < hi.x; x += blockDim.x) + { + processPixel(VecI<3>{x, 0, z}); + } + } +} + +/** + * @brief Implements horizontal resampling + * + * @param outWrap - the wrapper for accessing output data + * @param inWrap - the wrapper for accessing input data + * @param lo - inclusive lower bound output coordinates of the block processed by the threadblock + * @param hi - exclusive upper bound output coordinates of the block processed by the threadblock + * @param srcX0 - X coordinate in the source image corresponding to output's 0 + * @param scale - step, in source X, for one pixel in output X (may be negative) + * @param support - size of the resampling kernel, in source pixels + * @param numChannels - the NumChannels specialization describing the number of interleaved + * channels in the input and output sample. + * + * The function fills the output in block-sized vertical spans. + * Block horizontal size is warp-aligned. + * Filter coefficients are pre-calculated for each vertical span to avoid + * recalculating them for each row, and stored in a shared memory block. + * + * The function follows different code paths for static and dynamic number of channels. + * For the dynamic, the innermost loop goes over filter taps, which eliminates the need + * for thread-local memory to store intermediate sums. This allows processing arbitrary + * number of channels. + * For static number of channels, the run-time parameter `channels` is ignored and + * there's also a local temporary storage for a tap sum for each channel. This is faster, + * but requires extra registers for the intermediate sums. + */ +template +void __forceinline__ __device__ ResampleHorz(const PassOutWrap outWrap, const PassInWrap inWrap, + const VecI lo, const VecI hi, float srcX0, + const float scale, const VecI inShape, + const filter::ResamplingFilter filter, const NumChannelsT numChannels) +{ + extern __shared__ float coeffs[]; + + using OutT = typename PassOutWrap::ValueType; + using InT = std::remove_const_t; + // spatial extents and optional channels extent + constexpr int kNDim = kSpatialNDim + !NumChannelsT::kHasStaticChannels; + + static_assert(!NumChannelsT::kHasStaticChannels || NumChannelsT::kStaticChannels == cuda::NumElements); + static_assert(cuda::NumElements == cuda::NumElements); + static_assert(PassOutWrap::kNumDimensions == kNDim); + static_assert(PassInWrap::kNumDimensions == kNDim); + + const int support = filter.support(); + const float filterStep = filter.scale; + // If the support is small enough (for blockDim.x = 32 and kMaxGPUFilterSupport = 8192, it's 256), + // we can fit `support` x `blockDim.x` elements into shm, so that for each output_x mapped to input_x, + // we take into account the exact error that comes from rounding the input_x from float to integer. + // For larger supports, we just compute `support` elements common for all threads. + const bool hugeSupport = !CanComputeCoefPerThread(support, blockDim.x); + const int coeffBase = hugeSupport ? 0 : threadIdx.x; + const int coeffStride = hugeSupport ? 1 : blockDim.x; + + srcX0 += 0.5f * scale - 0.5f - filter.anchor; + + for (int j = lo.x; j < hi.x; j += blockDim.x) + { + const int x = j + threadIdx.x; + const float sx0f = x * scale + srcX0; + const int sx0 = hugeSupport ? cuda::round(sx0f) + : cuda::round(sx0f); + const float f = (sx0 - sx0f) * filterStep; + __syncthreads(); + if (hugeSupport) + { + for (int k = threadIdx.x + blockDim.x * threadIdx.y; k < support; k += blockDim.x * blockDim.y) + { + float flt = filter(f + k * filterStep); + coeffs[k] = flt; + } + } + else + { + for (int k = threadIdx.y; k < support; k += blockDim.y) + { + float flt = filter(f + k * filterStep); + coeffs[coeffBase + coeffStride * k] = flt; + } + } + __syncthreads(); + + if (x >= hi.x) + continue; + + float norm = 0; + for (int k = 0; k < support; k++) + { + norm += coeffs[coeffBase + coeffStride * k]; + } + norm = 1.0f / norm; + + ForAllOrthogonalToHorz(lo, hi, + [=](VecI outIdx) + { + VecI inIdx = outIdx; + outIdx.x = x; + + if constexpr (NumChannelsT::kHasStaticChannels) + { + using FloatT = cuda::ConvertBaseTypeTo; + FloatT tmp{}; + + for (int k = 0, coeffIdx = coeffBase; k < support; k++, coeffIdx += coeffStride) + { + inIdx.x = cuda::clamp(sx0 + k, 0, inShape.x - 1); + const float flt = coeffs[coeffIdx]; + const InT px = LoadPixelLdg(inWrap, numChannels, inIdx); + FmaPerChannel(tmp, px, flt); + } + + OutT &out = *GetWrapPtr(outWrap, outIdx); + out = cuda::SaturateCast(tmp * norm); + } + else if constexpr (!NumChannelsT::kHasStaticChannels) + { + for (int c = 0; c < numChannels(); c++) + { + float tmp = 0; + + for (int k = 0, coeffIdx = coeffBase; k < support; + k++, coeffIdx += coeffStride) + { + inIdx.x = cuda::clamp(sx0 + k, 0, inShape.x - 1); + const float flt = coeffs[coeffIdx]; + const InT px = LoadPixelLdg(inWrap, numChannels, inIdx, c); + tmp = fmaf(px, flt, tmp); + } + + OutT &out = *GetWrapPtr(outWrap, outIdx, c); + out = cuda::SaturateCast(tmp * norm); + } + } + }); + } +} + +/** + * @brief Implements vertical resampling + * + * @param outWrap - the wrapper for accessing output data + * @param inWrap - the wrapper for accessing input data + * @param lo - inclusive lower bound output coordinates of the block processed by the threadblock + * @param hi - exclusive upper bound output coordinates of the block processed by the threadblock + * @param srcY0 - Y coordinate in the source image corresponding to output's 0 + * @param scale - step, in source Y, for one pixel in output Y (may be negative) + * @param support - size of the resampling kernel, in source pixels + * @param numChannels - the NumChannels specialization describing the number of interleaved + * channels in the input and output sample. + * + * The function fills the output in block-sized horizontal spans. + * Filter coefficients are pre-calculated for each horizontal span to avoid + * recalculating them for each column, and stored in a shared memory block. + */ +template +void __forceinline__ __device__ ResampleVert(const PassOutWrap outWrap, const PassInWrap inWrap, + const VecI lo, const VecI hi, float srcY0, + const float scale, const VecI inShape, + const filter::ResamplingFilter filter, const NumChannelsT numChannels) +{ + extern __shared__ float coeffs[]; + + using OutT = typename PassOutWrap::ValueType; + using InT = std::remove_const_t; + using InBT = cuda::BaseType; + using OutBT = cuda::BaseType; + // spatial extents and optional channels extent + constexpr int kNDim = kSpatialNDim + !NumChannelsT::kHasStaticChannels; + constexpr bool kUseVertXVec = kEnableVertXVec && kSpatialNDim == 2 && NumChannelsT::kHasStaticChannels + && NumChannelsT::kStaticChannels == 1 + && std::is_same_v && std::is_same_v; + + static_assert(!NumChannelsT::kHasStaticChannels || NumChannelsT::kStaticChannels == cuda::NumElements); + static_assert(cuda::NumElements == cuda::NumElements); + static_assert(PassOutWrap::kNumDimensions == kNDim); + static_assert(PassInWrap::kNumDimensions == kNDim); + + const int support = filter.support(); + const float filterStep = filter.scale; + // If the support is small enough, we can fit `blockDim.y` x `support` elements into shm, so that + // for each output_y mapped to input_y, we take into account the exact error that comes from + // rounding the input_y from float to integer. For larger supports, we just compute `support` + // elements common for all threads. + const bool hugeSupport = !CanComputeCoefPerThread(support, blockDim.y); + const int coeffBase = hugeSupport ? 0 : support * threadIdx.y; + + srcY0 += 0.5f * scale - 0.5f - filter.anchor; + + for (int i = lo.y; i < hi.y; i += blockDim.y) + { + const int y = i + threadIdx.y; + const float sy0f = y * scale + srcY0; + const int sy0 = hugeSupport ? cuda::round(sy0f) + : cuda::round(sy0f); + float f = (sy0 - sy0f) * filterStep; + __syncthreads(); + // fills `support` + if (hugeSupport) + { + for (int k = threadIdx.x + blockDim.x * threadIdx.y; k < support; k += blockDim.x * blockDim.y) + { + float flt = filter(f + k * filterStep); + coeffs[k] = flt; + } + } + else + { + for (int k = threadIdx.x; k < support; k += blockDim.x) + { + float flt = filter(f + k * filterStep); + coeffs[coeffBase + k] = flt; + } + } + __syncthreads(); + + if (y >= hi.y) + continue; + + float norm = 0; + for (int k = 0; k < support; k++) + { + norm += coeffs[coeffBase + k]; + } + norm = 1.0f / norm; + + if constexpr (kUseVertXVec) + { + const bool canUseXVec + = !hugeSupport && scale >= 1.0f && scale <= kVertXVecMaxScale && support <= kVertXVecMaxSupport; + if (canUseXVec) + { + using InVec = Vec; + using OutVec = Vec; + using AccumVec = Vec; + + for (int x0 = lo.x + static_cast(threadIdx.x) * kVertXVec; x0 < hi.x; + x0 += static_cast(blockDim.x) * kVertXVec) + { + const bool full = x0 + kVertXVec <= hi.x; + bool aligned = full + && ((reinterpret_cast(GetWrapPtr(outWrap, VecI<2>{x0, y})) + & (alignof(OutVec) - 1)) + == 0); + if (aligned) + { + for (int k = 0; k < support; k++) + { + const int inY = cuda::clamp(sy0 + k, 0, inShape.y - 1); + aligned = aligned + && ((reinterpret_cast(GetWrapPtr(inWrap, VecI<2>{x0, inY})) + & (alignof(InVec) - 1)) + == 0); + } + } + + if (aligned) + { + AccumVec tmp = cuda::SetAll(0.f); + for (int k = 0; k < support; k++) + { + const int inY = cuda::clamp(sy0 + k, 0, inShape.y - 1); + const float flt = coeffs[coeffBase + k]; + const InVec px = *reinterpret_cast(GetWrapPtr(inWrap, VecI<2>{x0, inY})); + tmp = tmp + flt * cuda::StaticCast(px); + } + + *reinterpret_cast(GetWrapPtr(outWrap, VecI<2>{x0, y})) + = cuda::SaturateCast(tmp * norm); + } + else + { + for (int lane = 0; lane < kVertXVec && x0 + lane < hi.x; lane++) + { + const int x = x0 + lane; + float tmp{}; + for (int k = 0; k < support; k++) + { + const int inY = cuda::clamp(sy0 + k, 0, inShape.y - 1); + const float flt = coeffs[coeffBase + k]; + const InT px = LoadPixelLdg(inWrap, numChannels, VecI<2>{x, inY}); + tmp = fmaf(px, flt, tmp); + } + + OutT &out = *GetWrapPtr(outWrap, VecI<2>{x, y}); + out = cuda::SaturateCast(tmp * norm); + } + } + } + continue; + } + } + + ForAllOrthogonalToVert(lo, hi, + [=](VecI outIdx) + { + VecI inIdx = outIdx; + outIdx.y = y; + + if constexpr (NumChannelsT::kHasStaticChannels) + { + using FloatT = cuda::ConvertBaseTypeTo; + FloatT tmp{}; + + for (int k = 0; k < support; k++) + { + inIdx.y = cuda::clamp(sy0 + k, 0, inShape.y - 1); + const float flt = coeffs[coeffBase + k]; + const InT px = LoadPixelLdg(inWrap, numChannels, inIdx); + FmaPerChannel(tmp, px, flt); + } + + OutT &out = *GetWrapPtr(outWrap, outIdx); + out = cuda::SaturateCast(tmp * norm); + } + else if constexpr (!NumChannelsT::kHasStaticChannels) + { + for (int c = 0; c < numChannels(); c++) + { + float tmp = 0; + + for (int k = 0; k < support; k++) + { + inIdx.y = cuda::clamp(sy0 + k, 0, inShape.y - 1); + const float flt = coeffs[coeffBase + k]; + const InT px = LoadPixelLdg(inWrap, numChannels, inIdx, c); + tmp = fmaf(px, flt, tmp); + } + + OutT &out = *GetWrapPtr(outWrap, outIdx, c); + out = cuda::SaturateCast(tmp * norm); + } + } + }); + } +} + +/** + * @brief Implements depth resampling + * + * @param outWrap - the wrapper for accessing output data + * @param inWrap - the wrapper for accessing input data + * @param lo - inclusive lower bound output coordinates of the block processed by the threadblock + * @param hi - exclusive upper bound output coordinates of the block processed by the threadblock + * @param srcZ0 - Y coordinate in the source image corresponding to output's 0 + * @param scale - step, in source Y, for one pixel in output Y (may be negative) + * @param support - size of the resampling kernel, in source pixels + * @param numChannels - the NumChannels specialization describing the number of interleaved + * channels in the input and output sample. + * + * The function fills the output in block-sized horizontal spans. + * Filter coefficients are pre-calculated for each horizontal span to avoid + * recalculating them for each column, and stored in a shared memory block. + */ +template +void __forceinline__ __device__ ResampleDepth(const PassOutWrap outWrap, const PassInWrap inWrap, const VecI<3> lo, + const VecI<3> hi, float srcZ0, const float scale, const VecI<3> inShape, + const filter::ResamplingFilter filter, const NumChannelsT numChannels) +{ + extern __shared__ float coeffs[]; + + using OutT = typename PassOutWrap::ValueType; + using InT = std::remove_const_t; + // spatial extents and optional channels extent + constexpr int kNDim = 3 + !NumChannelsT::kHasStaticChannels; + + static_assert(!NumChannelsT::kHasStaticChannels || NumChannelsT::kStaticChannels == cuda::NumElements); + static_assert(cuda::NumElements == cuda::NumElements); + static_assert(PassOutWrap::kNumDimensions == kNDim); + static_assert(PassInWrap::kNumDimensions == kNDim); + + const int support = filter.support(); + const float filterStep = filter.scale; + // If the support is small enough, we can fit `blockDim.y` x `support` elements into shm, + // so that for each output_z mapped to input_z, we take into account the exact error that + // comes from rounding the input_z from float to integer. For larger supports, we just + // compute `support` elements common for all threads. + const bool hugeSupport = !CanComputeCoefPerThread(support, blockDim.y); + const int coeffBase = hugeSupport ? 0 : support * threadIdx.y; + + srcZ0 += 0.5f * scale - 0.5f - filter.anchor; + + for (int i = lo.z; i < hi.z; i += blockDim.y) + { + // threadIdx.y is used to traverse Z axis + const int z = i + threadIdx.y; + const float sz0f = z * scale + srcZ0; + const int sz0 = hugeSupport ? cuda::round(sz0f) + : cuda::round(sz0f); + float f = (sz0 - sz0f) * filterStep; + __syncthreads(); + if (hugeSupport) + { + for (int k = threadIdx.x + blockDim.x * threadIdx.y; k < support; k += blockDim.x * blockDim.y) + { + float flt = filter(f + k * filterStep); + coeffs[k] = flt; + } + } + else + { + for (int k = threadIdx.x; k < support; k += blockDim.x) + { + float flt = filter(f + k * filterStep); + coeffs[coeffBase + k] = flt; + } + } + __syncthreads(); + + if (z >= hi.z) + continue; + + float norm = 0; + for (int k = 0; k < support; k++) + { + norm += coeffs[coeffBase + k]; + } + norm = 1.0f / norm; + + for (int y = threadIdx.z + lo.y; y < hi.y; y += blockDim.z) + { + for (int x = threadIdx.x + lo.x; x < hi.x; x += blockDim.x) + { + const VecI<3> outIdx{x, y, z}; + VecI<3> inIdx = outIdx; + + if constexpr (NumChannelsT::kHasStaticChannels) + { + using FloatT = cuda::ConvertBaseTypeTo; + FloatT tmp{}; + + for (int k = 0; k < support; k++) + { + inIdx.z = cuda::clamp(sz0 + k, 0, inShape.z - 1); + const float flt = coeffs[coeffBase + k]; + const InT px = LoadPixelLdg(inWrap, numChannels, inIdx); + FmaPerChannel(tmp, px, flt); + } + + OutT &out = *GetWrapPtr(outWrap, outIdx); + out = cuda::SaturateCast(tmp * norm); + } + else if constexpr (!NumChannelsT::kHasStaticChannels) + { + for (int c = 0; c < numChannels(); c++) + { + float tmp = 0; + + for (int k = 0; k < support; k++) + { + inIdx.z = cuda::clamp(sz0 + k, 0, inShape.z - 1); + const float flt = coeffs[coeffBase + k]; + const InT px = LoadPixelLdg(inWrap, numChannels, inIdx, c); + tmp = fmaf(px, flt, tmp); + } + + OutT &out = *GetWrapPtr(outWrap, outIdx, c); + out = cuda::SaturateCast(tmp * norm); + } + } + } + } + } +} +} // namespace filter_support + +template +void __forceinline__ __device__ RunNN(const PassOutWrap outWrap, const PassInWrap inWrap, const VecI lo, + const VecI hi, int axis, const VecI inShape, + const float origin, const float scale, const NumChannelsT numChannels) +{ + auto originV = cuda::SetAll>(0.f); + auto scaleV = cuda::SetAll>(1.f); + cuda::GetElement(originV, axis) = origin; + cuda::GetElement(scaleV, axis) = scale; + nn::Resample(outWrap, inWrap, lo, hi, originV, scaleV, inShape, numChannels); +} + +template +void __forceinline__ __device__ RunLinear(const PassOutWrap outWrap, const PassInWrap inWrap, + const VecI lo, const VecI hi, int axis, + const VecI inShape, const float origin, const float scale, + const NumChannelsT numChannels) +{ + if (axis == 0) + { + linear::ResampleHorz(outWrap, inWrap, lo, hi, origin, scale, inShape, numChannels); + } + else if (axis == 1) + { + linear::ResampleVert( + outWrap, inWrap, lo, hi, origin, scale, inShape, numChannels); + } + else if (axis == 2) + { + if constexpr (kSpatialNDim == 3) + { + linear::ResampleDepth(outWrap, inWrap, lo, hi, origin, scale, inShape, numChannels); + } + } +} + +template +void __forceinline__ __device__ RunFilter(const PassOutWrap outWrap, const PassInWrap inWrap, + const VecI lo, const VecI hi, int axis, + const VecI inShape, const float origin, const float scale, + const filter::ResamplingFilter filter, const NumChannelsT numChannels) +{ + if (axis == 0) + { + filter_support::ResampleHorz(outWrap, inWrap, lo, hi, origin, scale, inShape, filter, + numChannels); + } + else if (axis == 1) + { + filter_support::ResampleVert( + outWrap, inWrap, lo, hi, origin, scale, inShape, filter, numChannels); + } + else if (axis == 2) + { + if constexpr (kSpatialNDim == 3) + { + filter_support::ResampleDepth(outWrap, inWrap, lo, hi, origin, scale, inShape, filter, numChannels); + } + } +} +} // namespace interpolate + +template +void __forceinline__ __device__ RunResamplingPass(const SampleDesc sampleDesc, const PassOutWrap outWrap, + const PassInWrap inWrap, const VecI lo, + const VecI hi, const NumChannelsT numChannels) +{ + VecI inShape = sampleDesc.shapes[kWhichPass]; + int axis = cuda::GetElement(sampleDesc.processingOrder, kWhichPass); // vec-order: 0 = X, 1 = Y, 2 = Z + const float origin = cuda::GetElement(sampleDesc.origin, kWhichPass); + const float scale = cuda::GetElement(sampleDesc.scale, kWhichPass); + + switch (sampleDesc.filterKind[kWhichPass]) + { + case filter::FilterTypeKind::Nearest: + interpolate::RunNN(outWrap, inWrap, lo, hi, axis, inShape, origin, scale, numChannels); + break; + case filter::FilterTypeKind::Linear: + interpolate::RunLinear(outWrap, inWrap, lo, hi, axis, inShape, origin, scale, + numChannels); + break; + default: + interpolate::RunFilter(outWrap, inWrap, lo, hi, axis, inShape, origin, scale, + sampleDesc.filter[kWhichPass], numChannels); + break; + } +} + +// Tensor variant (unfirom batch) +template +__global__ void SeparableResamplingKernel(const SampleDesc sampleDesc, const PassOutWrap outWrap, + const PassInWrap inWrap, const GridHelperDevice gridHelper) + +{ + constexpr bool kHasDynamicChannels = kNumStaticChannels == -1; + static_assert(PassInWrap::kNumDimensions == 1 + kSpatialNDim + kHasDynamicChannels); + static_assert(PassOutWrap::kNumDimensions == 1 + kSpatialNDim + kHasDynamicChannels); + // Get sample idx and the region of the output image that + // the current threadblock has to process + int sampleIdx = gridHelper.CurrentSample(); + VecI lo, hi; + gridHelper.CurrentBlock(lo, hi, sampleDesc.blockShape[kWhichPass]); + hi = cuda::min(hi, sampleDesc.shapes[kWhichPass + 1]); + + const auto outSampleView = batch_wrapper::tensor::GetSampleView(outWrap, sampleIdx); + const auto inSampleView = batch_wrapper::tensor::GetSampleView(inWrap, sampleIdx); + WithChannels(sampleDesc.channels, + [=](const NumChannels numChannels) { + RunResamplingPass( + sampleDesc, outSampleView, inSampleView, lo, hi, numChannels); + }); +} + +// Batch variant (ImageBatchVarShape, TensorBatch) +template +__global__ void SeparableResamplingKernel(const SampleDesc *__restrict__ samples, + const PassOutWrap outWrap, const PassInWrap inWrap, + const GridHelperDevice gridHelper) +{ + constexpr bool kHasDynamicChannels = kNumStaticChannels == -1; + static_assert(PassInWrap::kNumDimensions == 1 + kSpatialNDim + kHasDynamicChannels); + static_assert(PassOutWrap::kNumDimensions == 1 + kSpatialNDim + kHasDynamicChannels); + // Get sample idx and the region of the output image that + // the current threadblock has to process + const int sampleIdx = gridHelper.CurrentSample(); + const auto sampleDesc = samples[sampleIdx]; + const VecI outShape = sampleDesc.shapes[kWhichPass + 1]; + VecI lo, hi; + gridHelper.CurrentBlock(lo, hi, sampleDesc.blockShape[kWhichPass]); + + // exit early for smaller samples + if (lo.x >= outShape.x || lo.y >= outShape.y) + { + return; + } + if constexpr (kSpatialNDim == 3) + { + if (lo.z >= outShape.z) + { + return; + } + } + hi = cuda::min(hi, outShape); + + const auto outSampleView = outWrap.GetSampleView(sampleIdx); + WithChannels(sampleDesc.channels, + [=](const NumChannels numChannels) + { + if constexpr (kWhichPass == 0) + { + const auto inSampleView + = inWrap.GetSampleView(sampleIdx, sampleDesc.inRoiOffset); + RunResamplingPass( + sampleDesc, outSampleView, inSampleView, lo, hi, numChannels); + } + else if constexpr (kWhichPass != 0) + { + const auto inSampleView = inWrap.GetSampleView(sampleIdx); + RunResamplingPass( + sampleDesc, outSampleView, inSampleView, lo, hi, numChannels); + } + }); +} + +inline __host__ __device__ void DirectLinearOriginScale(const SampleDesc<2> &sampleDesc, VecF<2> &origin, + VecF<2> &scale) +{ + for (int pass = 0; pass < 2; pass++) + { + const int axis = cuda::GetElement(sampleDesc.processingOrder, pass); + cuda::GetElement(origin, axis) = cuda::GetElement(sampleDesc.origin, pass); + cuda::GetElement(scale, axis) = cuda::GetElement(sampleDesc.scale, pass); + } +} + +/** + * @brief Stores kN consecutive single-channel outputs as kVec-wide vectors when the + * destination is vector-aligned, or element-wise otherwise (user tensors may have row + * strides that are not a multiple of the vector size). All kN elements must be + * in-bounds. + */ +template +void __forceinline__ __device__ StoreRowVectorized(OutT *ptr, const OutT (&vals)[kN]) +{ + static_assert(cuda::NumElements == 1); + static_assert(kN % kVec == 0); + using OutVec = Vec, kVec>; + if ((reinterpret_cast(ptr) & (alignof(OutVec) - 1)) == 0) + { +#pragma unroll + for (int v = 0; v < kN / kVec; v++) + { + OutVec vec; +#pragma unroll + for (int e = 0; e < kVec; e++) + { + cuda::GetElement(vec, e) = vals[kVec * v + e]; + } + reinterpret_cast(ptr)[v] = vec; + } + } + else + { +#pragma unroll + for (int e = 0; e < kN; e++) + { + ptr[e] = vals[e]; + } + } +} + +/** + * @brief Number of consecutive output columns each DirectLinear2DKernel thread produces. + * + * The u8 kernel is issue-bound on per-pixel coordinate math and single-byte stores, + * so each thread produces several output pixels and merges them into one vector store. + */ +template +inline constexpr int kDirectLinear2DLanes + = kNumStaticChannels == 1 && std::is_same_v &&std::is_same_v ? 4 : 1; + +inline __device__ float DirectLinearLerp(float a, float b, float q) +{ + return fmaf(b - a, q, a); +} + +template +FloatT __forceinline__ __device__ DirectLinearPixel(const InSampleView inSampleView, const NumChannelsT numChannels, + const int x, const int y, const VecF<2> origin, const VecF<2> scale, + const int2 inShape) +{ + constexpr int kNumStaticChannels = NumChannelsT::kStaticChannels; + + const float sx0f = static_cast(x) * scale.x + origin.x + 0.5f * scale.x - 0.5f; + const float sy0f = static_cast(y) * scale.y + origin.y + 0.5f * scale.y - 0.5f; + int sx0 = cuda::round(sx0f); + int sy0 = cuda::round(sy0f); + const float qx = sx0f - static_cast(sx0); + const float qy = sy0f - static_cast(sy0); + const int sx1 = cuda::clamp(sx0 + 1, 0, inShape.x - 1); + const int sy1 = cuda::clamp(sy0 + 1, 0, inShape.y - 1); + sx0 = cuda::clamp(sx0, 0, inShape.x - 1); + sy0 = cuda::clamp(sy0, 0, inShape.y - 1); + + FloatT row0 = cuda::StaticCast(interpolate::LoadPixelLdg(inSampleView, numChannels, VecI<2>{sx0, sy0})); + FloatT row1 = cuda::StaticCast(interpolate::LoadPixelLdg(inSampleView, numChannels, VecI<2>{sx0, sy1})); + const FloatT row0b + = cuda::StaticCast(interpolate::LoadPixelLdg(inSampleView, numChannels, VecI<2>{sx1, sy0})); + const FloatT row1b + = cuda::StaticCast(interpolate::LoadPixelLdg(inSampleView, numChannels, VecI<2>{sx1, sy1})); +#pragma unroll + for (int c = 0; c < kNumStaticChannels; c++) + { + cuda::GetElement(row0, c) = DirectLinearLerp(cuda::GetElement(row0, c), cuda::GetElement(row0b, c), qx); + cuda::GetElement(row1, c) = DirectLinearLerp(cuda::GetElement(row1, c), cuda::GetElement(row1b, c), qx); + cuda::GetElement(row0, c) = DirectLinearLerp(cuda::GetElement(row0, c), cuda::GetElement(row1, c), qy); + } + return row0; +} + +template +__global__ void DirectLinear2DKernel(const SampleDesc<2> sampleDesc, const PassOutWrap outWrap, const PassInWrap inWrap) +{ + static_assert(kNumStaticChannels > 0); + static_assert(PassInWrap::kNumDimensions == 3); + static_assert(PassOutWrap::kNumDimensions == 3); + + using OutT = typename PassOutWrap::ValueType; + using InT = std::remove_const_t; + using OutBT = cuda::BaseType; + using InBT = cuda::BaseType; + using FloatT = cuda::ConvertBaseTypeTo; + + static_assert(kNumStaticChannels == cuda::NumElements); + static_assert(cuda::NumElements == cuda::NumElements); + + constexpr int kLanes = kDirectLinear2DLanes; + + const int sampleIdx = blockIdx.z; + const int x0 = (blockIdx.x * blockDim.x + threadIdx.x) * kLanes; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const auto outShape = sampleDesc.shapes[2]; + if (x0 >= outShape.x || y >= outShape.y) + { + return; + } + + VecF<2> origin{}; + VecF<2> scale{}; + DirectLinearOriginScale(sampleDesc, origin, scale); + + const int2 inShape{sampleDesc.shapes[0].x, sampleDesc.shapes[0].y}; + + const auto inSampleView = batch_wrapper::tensor::GetSampleView(inWrap, sampleIdx); + const auto outSampleView = batch_wrapper::tensor::GetSampleView(outWrap, sampleIdx); + const auto numChannels = NumChannels{}; + + if constexpr (kLanes == 1) + { + const FloatT px = DirectLinearPixel(inSampleView, numChannels, x0, y, origin, scale, inShape); + + OutT &out = *interpolate::GetWrapPtr(outSampleView, VecI<2>{x0, y}); + out = cuda::SaturateCast(px); + } + else if constexpr (kLanes > 1) + { + OutT *outPtr = interpolate::GetWrapPtr(outSampleView, VecI<2>{x0, y}); + if (x0 + kLanes <= outShape.x) + { + OutT vals[kLanes]; +#pragma unroll + for (int l = 0; l < kLanes; l++) + { + const FloatT px + = DirectLinearPixel(inSampleView, numChannels, x0 + l, y, origin, scale, inShape); + vals[l] = cuda::SaturateCast(px); + } + StoreRowVectorized(outPtr, vals); + } + else + { + for (int l = 0; l < kLanes && x0 + l < outShape.x; l++) + { + const FloatT px + = DirectLinearPixel(inSampleView, numChannels, x0 + l, y, origin, scale, inShape); + outPtr[l] = cuda::SaturateCast(px); + } + } + } +} + +/** + * @brief Exact per-axis filter support required by the fused direct-filter kernels + * (DirectFilter2D/Batch2D, Contract2x, 2x2). + * + * In practice this admits non-antialiased cubic plus the antialiased contractions + * whose scaled support rounds to 4; smaller supports (non-antialiased gaussian or + * triangular) and larger ones (stronger antialiased contractions, lanczos) keep the + * separable path, whose shared-memory coefficient reuse pays off there. + */ +constexpr int kMaxDirectFilterSupport = 4; + +/** + * @brief Per-axis resampling window of a fused support-based pass: the first source + * index, the filter coefficients, and the reciprocal of their sum. Initialization + * replicates the separable kernel's per-output coefficient computation. + */ +template +struct DirectFilterAxis +{ + int s; + float coeffs[kSupport]; + float norm; + + void __forceinline__ __device__ Init(const int coord, const float scale, const float originAdj, + const filter::ResamplingFilter flt) + { + const float sf = coord * scale + originAdj; + s = cuda::round(sf); + const float f = (s - sf) * flt.scale; + float total = 0; +#pragma unroll + for (int k = 0; k < kSupport; k++) + { + coeffs[k] = flt(f + k * flt.scale); + total += coeffs[k]; + } + norm = 1.0f / total; + } +}; + +/** + * @brief Origin adjustment shared by every output coordinate of a fused pass axis. + */ +float __forceinline__ __device__ DirectFilterOriginAdj(const float origin, const float scale, + const filter::ResamplingFilter flt) +{ + return origin + 0.5f * scale - 0.5f - flt.anchor; +} + +/** + * @brief Evaluates both fused resampling passes for one output pixel, with clamped + * source indexing. axis0 is the axis resampled by pass0 (0 = X, 1 = Y). + */ +template +FloatT __forceinline__ __device__ DirectFilterSample(const InSampleView inSampleView, const NumChannelsT numChannels, + const DirectFilterAxis &a0, + const DirectFilterAxis &a1, const int axis0, + const int inSize0, const int inSize1) +{ + constexpr int kNumStaticChannels = NumChannelsT::kStaticChannels; + using InT = typename InSampleView::ValueType; + using InBT = cuda::BaseType>; + + // u8 with X resampled by pass1 (the contraction pass order): each pass0 tap row's + // kSupport * C window bytes are contiguous, so they load as aligned 32-bit words, + // byte-aligned with funnel shifts so all register indexing stays compile-time. The + // per-k1 fmaf chains accumulate in the same order as the generic loop below, so + // the results are bit-identical. Needs one word of slack before the row end and, + // because the first word is aligned down, up to three bytes of slack before the + // window start so an unaligned sample base is never read below its first byte. + if constexpr (std::is_same_v) + { + constexpr int kWinBytes = kSupport * kNumStaticChannels; + constexpr int kWinWords = (kWinBytes + 3) / 4 + 1; + constexpr int kMinS1 = (3 + kNumStaticChannels - 1) / kNumStaticChannels; + const int maxS1 = inSize1 - (kWinWords * 4 + kNumStaticChannels - 1) / kNumStaticChannels; + if (axis0 == 1 && a1.s >= kMinS1 && a1.s <= maxS1) + { + FloatT passSum[kSupport]{}; +#pragma unroll + for (int k0 = 0; k0 < kSupport; k0++) + { + const int row = cuda::clamp(a0.s + k0, 0, inSize0 - 1); + const auto *pix = reinterpret_cast( + interpolate::GetWrapPtr(inSampleView, VecI<2>{a1.s, row})); + const auto addr = reinterpret_cast(pix); + const unsigned shift = static_cast(addr & 3u) * 8u; + const unsigned *word = reinterpret_cast(addr & ~std::uintptr_t{3}); + unsigned w[kWinWords]; +#pragma unroll + for (int i = 0; i < kWinWords; i++) + { + w[i] = __ldg(word + i); + } + unsigned v[kWinWords - 1]; +#pragma unroll + for (int i = 0; i < kWinWords - 1; i++) + { + v[i] = __funnelshift_r(w[i], w[i + 1], shift); + } + const float flt = a0.coeffs[k0]; +#pragma unroll + for (int k1 = 0; k1 < kSupport; k1++) + { +#pragma unroll + for (int c = 0; c < kNumStaticChannels; c++) + { + const int j = k1 * kNumStaticChannels + c; + const float b = static_cast((v[j / 4] >> ((j % 4) * 8)) & 0xFFu); + cuda::GetElement(passSum[k1], c) = fmaf(b, flt, cuda::GetElement(passSum[k1], c)); + } + } + } + FloatT acc{}; +#pragma unroll + for (int k1 = 0; k1 < kSupport; k1++) + { + ScalePerChannel(passSum[k1], a0.norm); + FmaPerChannel(acc, passSum[k1], a1.coeffs[k1]); + } + ScalePerChannel(acc, a1.norm); + return acc; + } + } + + FloatT acc{}; +#pragma unroll + for (int k1 = 0; k1 < kSupport; k1++) + { + const int idx1 = cuda::clamp(a1.s + k1, 0, inSize1 - 1); + FloatT passSum{}; +#pragma unroll + for (int k0 = 0; k0 < kSupport; k0++) + { + const int idx0 = cuda::clamp(a0.s + k0, 0, inSize0 - 1); + const VecI<2> inIdx = axis0 == 0 ? VecI<2>{idx0, idx1} : VecI<2>{idx1, idx0}; + const InT px = interpolate::LoadPixelLdg(inSampleView, numChannels, inIdx); + FmaPerChannel(passSum, px, a0.coeffs[k0]); + } + ScalePerChannel(passSum, a0.norm); + FmaPerChannel(acc, passSum, a1.coeffs[k1]); + } + ScalePerChannel(acc, a1.norm); + return acc; +} + +/** + * @brief Evaluates one fused-contraction output pixel from its sample descriptor and + * stores it: the shared body of the uniform-tensor and batch direct-filter kernels. + */ +template +void __forceinline__ __device__ DirectFilterOutput(const SampleDesc<2> &sampleDesc, const OutSampleView outSampleView, + const InSampleView inSampleView, const int x, const int y) +{ + using OutT = typename OutSampleView::ValueType; + using InT = std::remove_const_t; + using FloatT = cuda::ConvertBaseTypeTo; + + // pass0 resamples axis0 (0 = X, 1 = Y), pass1 the other axis + const int axis0 = sampleDesc.processingOrder.x; + const int coord0 = axis0 == 0 ? x : y; + const int coord1 = axis0 == 0 ? y : x; + const int2 inShape{sampleDesc.shapes[0].x, sampleDesc.shapes[0].y}; + const int inSize0 = axis0 == 0 ? inShape.x : inShape.y; + const int inSize1 = axis0 == 0 ? inShape.y : inShape.x; + + const filter::ResamplingFilter flt0 = sampleDesc.filter[0]; + const filter::ResamplingFilter flt1 = sampleDesc.filter[1]; + + DirectFilterAxis a0, a1; + a0.Init(coord0, sampleDesc.scale.x, DirectFilterOriginAdj(sampleDesc.origin.x, sampleDesc.scale.x, flt0), flt0); + a1.Init(coord1, sampleDesc.scale.y, DirectFilterOriginAdj(sampleDesc.origin.y, sampleDesc.scale.y, flt1), flt1); + + const auto numChannels = NumChannels{}; + + const FloatT acc = DirectFilterSample(inSampleView, numChannels, a0, a1, axis0, inSize0, inSize1); + + OutT &out = *interpolate::GetWrapPtr(outSampleView, VecI<2>{x, y}); + out = cuda::SaturateCast(acc); +} + +/** + * @brief Fused two-pass support-based resampling: one thread per output pixel. + * + * Both resampling passes of the separable kernel are evaluated in registers, in the + * same pass order with the same coefficient, normalization, and FMA sequences, so the + * result matches the separable path while the float intermediate never reaches memory. + * Only worthwhile for contractions, where the per-output coefficient recomputation is + * amortized by the reduced output count. + */ +template +__global__ void DirectFilter2DKernel(const SampleDesc<2> sampleDesc, const PassOutWrap outWrap, const PassInWrap inWrap) +{ + static_assert(kNumStaticChannels > 0); + static_assert(kSupport <= kMaxDirectFilterSupport); + static_assert(PassInWrap::kNumDimensions == 3); + static_assert(PassOutWrap::kNumDimensions == 3); + + using OutT = typename PassOutWrap::ValueType; + using InT = std::remove_const_t; + + static_assert(kNumStaticChannels == cuda::NumElements); + static_assert(cuda::NumElements == cuda::NumElements); + + const int sampleIdx = blockIdx.z; + const int x = blockIdx.x * blockDim.x + threadIdx.x; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const auto outShape = sampleDesc.shapes[2]; + if (x >= outShape.x || y >= outShape.y) + { + return; + } + + const auto inSampleView = batch_wrapper::tensor::GetSampleView(inWrap, sampleIdx); + const auto outSampleView = batch_wrapper::tensor::GetSampleView(outWrap, sampleIdx); + DirectFilterOutput(sampleDesc, outSampleView, inSampleView, x, y); +} + +/** + * @brief Number of consecutive output columns each DirectFilterContract2xKernel thread + * produces; lanes share the column-sum window and merge into vectorized stores. + */ +template +inline constexpr int kDirectFilterContract2xLanes = std::is_same_v ? 4 : 2; + +/** + * @brief Fused two-pass support-based exact-2x contraction: one thread per kLanes + * consecutive output columns. + * + * With scale exactly 2 on both axes the coefficient phase is the same for every output + * coordinate, so each thread evaluates one coefficient set per axis and reuses it for + * all its outputs, and adjacent lanes share the per-column pass0 sums. The separable + * passes' coefficient, normalization, and FMA sequences are replicated in pass order + * (Y first, as dispatched), so the result matches the separable path while the float + * intermediate never reaches memory. + */ +template +void __forceinline__ __device__ DirectFilterContract2xThread(const SampleDesc<2> &sampleDesc, + const OutSampleView outSampleView, + const InSampleView inSampleView, const int x0, const int y) +{ + using OutT = typename OutSampleView::ValueType; + using InT = std::remove_const_t; + using OutBT = cuda::BaseType; + using InBT = cuda::BaseType; + using FloatT = cuda::ConvertBaseTypeTo; + + constexpr int kLanes = kDirectFilterContract2xLanes; + + const auto outShape = sampleDesc.shapes[2]; + const int inW = sampleDesc.shapes[0].x; + const int inH = sampleDesc.shapes[0].y; + + // pass0 resamples Y (dispatch requires processingOrder == {1, 0}) + const filter::ResamplingFilter fltY = sampleDesc.filter[0]; + const filter::ResamplingFilter fltX = sampleDesc.filter[1]; + + DirectFilterAxis ay, ax; + ay.Init(y, sampleDesc.scale.x, DirectFilterOriginAdj(sampleDesc.origin.x, sampleDesc.scale.x, fltY), fltY); + ax.Init(x0, sampleDesc.scale.y, DirectFilterOriginAdj(sampleDesc.origin.y, sampleDesc.scale.y, fltX), fltX); + + const auto numChannels = NumChannels{}; + + // The source column index advances by exactly 2 per output column. + constexpr int kCols = 2 * kLanes + kSupport - 2; + const bool interior + = ax.s >= 0 && ax.s + kCols - 1 < inW && ay.s >= 0 && ay.s + kSupport - 1 < inH && x0 + kLanes <= outShape.x; + + if (interior) + { + FloatT colSum[kCols]{}; +#pragma unroll + for (int j = 0; j < kSupport; j++) + { + const int row = ay.s + j; +#pragma unroll + for (int i = 0; i < kCols; i++) + { + const InT px = interpolate::LoadPixelLdg(inSampleView, numChannels, VecI<2>{ax.s + i, row}); + FmaPerChannel(colSum[i], px, ay.coeffs[j]); + } + } +#pragma unroll + for (int i = 0; i < kCols; i++) + { + ScalePerChannel(colSum[i], ay.norm); + } + + OutT vals[kLanes]; +#pragma unroll + for (int l = 0; l < kLanes; l++) + { + FloatT acc{}; +#pragma unroll + for (int k = 0; k < kSupport; k++) + { + FmaPerChannel(acc, colSum[2 * l + k], ax.coeffs[k]); + } + ScalePerChannel(acc, ax.norm); + vals[l] = cuda::SaturateCast(acc); + } + + OutT *outPtr = interpolate::GetWrapPtr(outSampleView, VecI<2>{x0, y}); + StoreRowVectorized(outPtr, vals); + } + else + { + for (int l = 0; l < kLanes && x0 + l < outShape.x; l++) + { + DirectFilterOutput(sampleDesc, outSampleView, inSampleView, x0 + l, y); + } + } +} + +template +__global__ void DirectFilterContract2xKernel(const SampleDesc<2> sampleDesc, const PassOutWrap outWrap, + const PassInWrap inWrap) +{ + static_assert(kNumStaticChannels > 0); + static_assert(kSupport <= kMaxDirectFilterSupport); + static_assert(PassInWrap::kNumDimensions == 3); + static_assert(PassOutWrap::kNumDimensions == 3); + + constexpr int kLanes = kDirectFilterContract2xLanes>; + + const int x0 = (blockIdx.x * blockDim.x + threadIdx.x) * kLanes; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const auto outShape = sampleDesc.shapes[2]; + if (x0 >= outShape.x || y >= outShape.y) + { + return; + } + + const int sampleIdx = blockIdx.z; + const auto inSampleView = batch_wrapper::tensor::GetSampleView(inWrap, sampleIdx); + const auto outSampleView = batch_wrapper::tensor::GetSampleView(outWrap, sampleIdx); + DirectFilterContract2xThread(sampleDesc, outSampleView, inSampleView, x0, y); +} + +/** + * @brief Batch (ImageBatchVarShape / TensorBatch) variant of DirectFilterContract2xKernel. + */ +template +__global__ void DirectFilterContract2xBatchKernel(const SampleDesc<2> *__restrict__ samples, const PassOutWrap outWrap, + const PassInWrap inWrap) +{ + static_assert(kNumStaticChannels == 1); + static_assert(kSupport <= kMaxDirectFilterSupport); + + constexpr int kLanes = kDirectFilterContract2xLanes>; + + const int sampleIdx = blockIdx.z; + const auto sampleDesc = samples[sampleIdx]; + const int x0 = (blockIdx.x * blockDim.x + threadIdx.x) * kLanes; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const auto outShape = sampleDesc.shapes[2]; + if (x0 >= outShape.x || y >= outShape.y) + { + return; + } + + const auto inSampleView = inWrap.GetSampleView(sampleIdx); + const auto outSampleView = outWrap.GetSampleView(sampleIdx); + DirectFilterContract2xThread(sampleDesc, outSampleView, inSampleView, x0, y); +} + +/** + * @brief Maximum per-axis phase count of the fused magnification kernel; bounds the + * shared-memory coefficient tables. + */ +constexpr int kMaxDirectFilterPhases = 64; + +/** + * @brief Cooperatively fills the per-phase coefficient/offset tables consumed by + * the + * phased kernels. Layout: bx*(kSupport+1) floats for X, by*(kSupport+1) + * floats for Y, then bx + by ints of source starts. + */ +template +void __forceinline__ __device__ DirectFilterPhaseTableInitShm(const SampleDesc<2> &sampleDesc, const VecI<2> phaseCount, + float *shm) +{ + constexpr int kRow = kSupport + 1; + const int bx = phaseCount.x; + const int by = phaseCount.y; + float *cx = shm; + float *cy = cx + bx * kRow; + int *sbx = reinterpret_cast(cy + by * kRow); + int *sby = sbx + bx; + + const filter::ResamplingFilter fltX = sampleDesc.filter[0]; + const filter::ResamplingFilter fltY = sampleDesc.filter[1]; + const float adjX = DirectFilterOriginAdj(sampleDesc.origin.x, sampleDesc.scale.x, fltX); + const float adjY = DirectFilterOriginAdj(sampleDesc.origin.y, sampleDesc.scale.y, fltY); + + const int tid = threadIdx.y * blockDim.x + threadIdx.x; + const int blockSize = blockDim.x * blockDim.y; + for (int p = tid; p < bx + by; p += blockSize) + { + DirectFilterAxis a; + if (p < bx) + { + a.Init(p, sampleDesc.scale.x, adjX, fltX); + } + else + { + a.Init(p - bx, sampleDesc.scale.y, adjY, fltY); + } + float *row = (p < bx ? cx + p * kRow : cy + (p - bx) * kRow); +#pragma unroll + for (int k = 0; k < kSupport; k++) + { + row[k] = a.coeffs[k]; + } + row[kSupport] = a.norm; + (p < bx ? sbx[p] : sby[p - bx]) = a.s; + } + __syncthreads(); +} + +/** + * @brief Fused contraction with per-phase coefficient tables: when both pass axes have + * small rational phase counts, the block builds the same shared tables as the + * magnification kernel (pass-ordered) and each thread reconstructs its two axis + * windows by phase lookup instead of evaluating the filter table per output pixel. + * The reconstructed windows feed the unmodified DirectFilterSample, so results are + * bit-identical to the per-pixel kernel. + */ +/** + * @brief Phase-lookup evaluation of one fused-contraction output pixel: reconstructs + * both pass windows from the shared tables and feeds the unmodified + * DirectFilterSample. Shared by the uniform-tensor and batch phased kernels. + */ +template +void __forceinline__ __device__ DirectFilterPhasedOutput(const SampleDesc<2> &sampleDesc, const VecI<2> phaseCount, + const VecI<2> phaseStep, const float *shm, + const OutSampleView outSampleView, + const InSampleView inSampleView, const int x, const int y) +{ + using OutT = typename OutSampleView::ValueType; + using InT = std::remove_const_t; + using FloatT = cuda::ConvertBaseTypeTo; + + constexpr int kRow = kSupport + 1; + const int b0 = phaseCount.x; + const int b1 = phaseCount.y; + const float *c0 = shm; + const float *c1 = c0 + b0 * kRow; + const int *sb0 = reinterpret_cast(c1 + b1 * kRow); + const int *sb1 = sb0 + b0; + + // pass0 resamples axis0 (0 = X, 1 = Y); tables and coords are pass-ordered + const int axis0 = sampleDesc.processingOrder.x; + const int coord0 = axis0 == 0 ? x : y; + const int coord1 = axis0 == 0 ? y : x; + const int2 inShape{sampleDesc.shapes[0].x, sampleDesc.shapes[0].y}; + const int inSize0 = axis0 == 0 ? inShape.x : inShape.y; + const int inSize1 = axis0 == 0 ? inShape.y : inShape.x; + + DirectFilterAxis a0, a1; + { + const int p0 = coord0 % b0; + const int p1 = coord1 % b1; + const float *row0 = c0 + p0 * kRow; + const float *row1 = c1 + p1 * kRow; +#pragma unroll + for (int k = 0; k < kSupport; k++) + { + a0.coeffs[k] = row0[k]; + a1.coeffs[k] = row1[k]; + } + a0.norm = row0[kSupport]; + a1.norm = row1[kSupport]; + a0.s = sb0[p0] + (coord0 / b0) * phaseStep.x; + a1.s = sb1[p1] + (coord1 / b1) * phaseStep.y; + } + + const auto numChannels = NumChannels{}; + + const FloatT acc = DirectFilterSample(inSampleView, numChannels, a0, a1, axis0, inSize0, inSize1); + + OutT &out = *interpolate::GetWrapPtr(outSampleView, VecI<2>{x, y}); + out = cuda::SaturateCast(acc); +} + +/** + * @brief Fused contraction with per-phase coefficient tables: when both pass axes have + * small rational phase counts, the block builds the same shared tables as the + * magnification kernel (pass-ordered) and each thread reconstructs its two axis + * windows by phase lookup instead of evaluating the filter table per output pixel. + * Results are bit-identical to the per-pixel kernel. + */ +template +__global__ void DirectFilterPhased2DKernel(const SampleDesc<2> sampleDesc, const VecI<2> phaseCount, + const VecI<2> phaseStep, const PassOutWrap outWrap, const PassInWrap inWrap) +{ + static_assert(kNumStaticChannels > 0); + static_assert(kSupport <= kMaxDirectFilterSupport); + static_assert(PassInWrap::kNumDimensions == 3); + static_assert(PassOutWrap::kNumDimensions == 3); + + extern __shared__ float shm[]; + DirectFilterPhaseTableInitShm(sampleDesc, phaseCount, shm); + + const int sampleIdx = blockIdx.z; + const int x = blockIdx.x * blockDim.x + threadIdx.x; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const auto outShape = sampleDesc.shapes[2]; + if (x >= outShape.x || y >= outShape.y) + { + return; + } + + const auto inSampleView = batch_wrapper::tensor::GetSampleView(inWrap, sampleIdx); + const auto outSampleView = batch_wrapper::tensor::GetSampleView(outWrap, sampleIdx); + DirectFilterPhasedOutput(sampleDesc, phaseCount, phaseStep, shm, outSampleView, + inSampleView, x, y); +} + +/** + * @brief Batch (ImageBatchVarShape / TensorBatch) variant of DirectFilterPhased2DKernel; + * dispatch requires every sample to share one shape, so one phase table serves all. + */ +template +__global__ void DirectFilterPhasedBatch2DKernel(const SampleDesc<2> *__restrict__ samples, const VecI<2> phaseCount, + const VecI<2> phaseStep, const PassOutWrap outWrap, + const PassInWrap inWrap) +{ + static_assert(kNumStaticChannels > 0); + static_assert(kSupport <= kMaxDirectFilterSupport); + + extern __shared__ float shm[]; + const int sampleIdx = blockIdx.z; + const auto sampleDesc = samples[sampleIdx]; + DirectFilterPhaseTableInitShm(sampleDesc, phaseCount, shm); + + const int x = blockIdx.x * blockDim.x + threadIdx.x; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const auto outShape = sampleDesc.shapes[2]; + if (x >= outShape.x || y >= outShape.y) + { + return; + } + + const auto inSampleView = inWrap.GetSampleView(sampleIdx); + const auto outSampleView = outWrap.GetSampleView(sampleIdx); + DirectFilterPhasedOutput(sampleDesc, phaseCount, phaseStep, shm, outSampleView, + inSampleView, x, y); +} + +/** + * @brief Batch (ImageBatchVarShape / TensorBatch) variant of DirectFilter2DKernel: + * per-sample descriptors, grid sized for the largest sample with per-block early exit. + */ +template +__global__ void DirectFilterBatch2DKernel(const SampleDesc<2> *__restrict__ samples, const PassOutWrap outWrap, + const PassInWrap inWrap) +{ + static_assert(kNumStaticChannels > 0); + static_assert(kSupport <= kMaxDirectFilterSupport); + + const int sampleIdx = blockIdx.z; + const auto sampleDesc = samples[sampleIdx]; + const int x = blockIdx.x * blockDim.x + threadIdx.x; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const auto outShape = sampleDesc.shapes[2]; + if (x >= outShape.x || y >= outShape.y) + { + return; + } + + const auto inSampleView = inWrap.GetSampleView(sampleIdx); + const auto outSampleView = outWrap.GetSampleView(sampleIdx); + DirectFilterOutput(sampleDesc, outSampleView, inSampleView, x, y); +} + +/** + * @brief Number of source columns each DirectFilter2x2Kernel thread expands. + * + * The single-channel kernels are load-transaction-bound on the (kSupport+1)^2 source + * neighborhood, so several source columns per thread let adjacent lanes share the + * column loads and the outputs merge into vectorized stores. + */ +template +inline constexpr int kDirectFilter2x2Lanes = std::is_same_v ? 4 : 2; + +/** + * @brief Fused two-pass support-based 2x magnification: one thread per kLanes source + * columns, producing the corresponding 2*kLanes x 2 output block. + * + * With scale exactly 0.5 on both axes, only two resampling phases exist per axis + * (even/odd output parity), so each thread computes the four coefficient sets once and + * reuses them for all its outputs. The coefficient, normalization, and FMA sequences + * replicate the separable passes in pass order (X first, as dispatched), so the result + * matches the separable path while the float intermediate never reaches memory. + */ +template +__global__ void DirectFilter2x2Kernel(const SampleDesc<2> sampleDesc, const PassOutWrap outWrap, + const PassInWrap inWrap) +{ + static_assert(kNumStaticChannels == 1); + static_assert(kSupport <= kMaxDirectFilterSupport); + static_assert(PassInWrap::kNumDimensions == 3); + static_assert(PassOutWrap::kNumDimensions == 3); + + using OutT = typename PassOutWrap::ValueType; + using InT = std::remove_const_t; + using OutBT = cuda::BaseType; + using InBT = cuda::BaseType; + using FloatT = cuda::ConvertBaseTypeTo; + + static_assert(kNumStaticChannels == cuda::NumElements); + static_assert(cuda::NumElements == cuda::NumElements); + + constexpr int kLanes = kDirectFilter2x2Lanes; + + const int tX0 = (blockIdx.x * blockDim.x + threadIdx.x) * kLanes; + const int tY = blockIdx.y * blockDim.y + threadIdx.y; + const int inW = sampleDesc.shapes[0].x; + const int inH = sampleDesc.shapes[0].y; + if (tX0 >= inW || tY >= inH) + { + return; + } + + const int sampleIdx = blockIdx.z; + + const filter::ResamplingFilter fltX = sampleDesc.filter[0]; + const filter::ResamplingFilter fltY = sampleDesc.filter[1]; + const float adjX = DirectFilterOriginAdj(sampleDesc.origin.x, sampleDesc.scale.x, fltX); + const float adjY = DirectFilterOriginAdj(sampleDesc.origin.y, sampleDesc.scale.y, fltY); + + // Per-parity windows at this thread's first lane; lane l shifts the source index + // by exactly l (the source position advances by 1 per output-parity pair). + DirectFilterAxis ax[2], ay[2]; +#pragma unroll + for (int p = 0; p < 2; p++) + { + ax[p].Init(2 * tX0 + p, sampleDesc.scale.x, adjX, fltX); + ay[p].Init(2 * tY + p, sampleDesc.scale.y, adjY, fltY); + } + + const auto inSampleView = batch_wrapper::tensor::GetSampleView(inWrap, sampleIdx); + const auto outSampleView = batch_wrapper::tensor::GetSampleView(outWrap, sampleIdx); + const auto numChannels = NumChannels{}; + + // The support-4 table filters anchor at support/2, so the odd output phase starts + // exactly one source pixel after the even one; anything else takes the edge path. + const bool interior = ax[1].s - ax[0].s == 1 && ay[1].s - ay[0].s == 1 && ax[0].s >= 0 + && ax[0].s + kLanes + kMaxDirectFilterSupport - 1 < inW && ay[0].s >= 0 + && ay[1].s + kSupport - 1 < inH && tX0 + kLanes <= inW; + + if (interior) + { + constexpr int kCols = kLanes + kMaxDirectFilterSupport; + constexpr int kOut = 2 * kLanes; + static_assert(kOut % 4 == 0); + + FloatT acc[2][kOut]{}; + // kSupport + 1 rows: the even output row uses rows [0, kSupport), the odd one + // [1, kSupport + 1). All register-array indices below are compile-time. +#pragma unroll + for (int j = 0; j < kSupport + 1; j++) + { + const int row = ay[0].s + j; + FloatT cols[kCols]; +#pragma unroll + for (int i = 0; i < kCols; i++) + { + cols[i] = cuda::StaticCast( + interpolate::LoadPixelLdg(inSampleView, numChannels, VecI<2>{ax[0].s + i, row})); + } + FloatT xv[2][kLanes]; +#pragma unroll + for (int p = 0; p < 2; p++) + { +#pragma unroll + for (int l = 0; l < kLanes; l++) + { + FloatT passSum{}; +#pragma unroll + for (int k = 0; k < kSupport; k++) + { + FmaPerChannel(passSum, cols[p + l + k], ax[p].coeffs[k]); + } + ScalePerChannel(passSum, ax[p].norm); + xv[p][l] = passSum; + } + } +#pragma unroll + for (int q = 0; q < 2; q++) + { + const int kq = j - q; + if (kq < 0 || kq >= kSupport) + { + continue; + } +#pragma unroll + for (int p = 0; p < 2; p++) + { +#pragma unroll + for (int l = 0; l < kLanes; l++) + { + FmaPerChannel(acc[q][2 * l + p], xv[p][l], ay[q].coeffs[kq]); + } + } + } + } + +#pragma unroll + for (int q = 0; q < 2; q++) + { + OutT vals[kOut]; +#pragma unroll + for (int e = 0; e < kOut; e++) + { + ScalePerChannel(acc[q][e], ay[q].norm); + vals[e] = cuda::SaturateCast(acc[q][e]); + } + OutT *rowPtr = interpolate::GetWrapPtr(outSampleView, VecI<2>{2 * tX0, 2 * tY + q}); + StoreRowVectorized<4>(rowPtr, vals); + } + } + else + { + for (int l = 0; l < kLanes && tX0 + l < inW; l++) + { + DirectFilterAxis axl[2]; +#pragma unroll + for (int p = 0; p < 2; p++) + { + axl[p].Init(2 * (tX0 + l) + p, sampleDesc.scale.x, adjX, fltX); + } +#pragma unroll + for (int q = 0; q < 2; q++) + { +#pragma unroll + for (int p = 0; p < 2; p++) + { + const FloatT acc + = DirectFilterSample(inSampleView, numChannels, axl[p], ay[q], 0, inW, inH); + OutT &out = *interpolate::GetWrapPtr(outSampleView, VecI<2>{2 * (tX0 + l) + p, 2 * tY + q}); + out = cuda::SaturateCast(acc); + } + } + } + } +} + +/** + * @brief Number of source columns each DirectLinear2x2Kernel thread expands. + * + * The u8 kernel is L1-transaction-bound on the 3x3 single-byte neighborhood loads, + * so widening a thread to several source columns lets adjacent lanes share the + * column loads and the 2x2 outputs merge into vectorized stores. + */ +template +inline constexpr int kDirectLinear2x2Lanes + = std::is_same_v &&std::is_same_v ? 4 : 1; + +template +void __forceinline__ __device__ DirectLinear2x2Pixel(const OutSampleView outSampleView, const InSampleView inSampleView, + const NumChannelsT numChannels, const int srcX, const int y0, + const int y1, const int y2, const int inW, const int outY) +{ + using OutT = typename OutSampleView::ValueType; + + const int x0 = cuda::max(srcX - 1, 0); + const int x1 = srcX; + const int x2 = cuda::min(srcX + 1, inW - 1); + + const float p00 = interpolate::LoadPixelLdg(inSampleView, numChannels, VecI<2>{x0, y0}); + const float p10 = interpolate::LoadPixelLdg(inSampleView, numChannels, VecI<2>{x1, y0}); + const float p20 = interpolate::LoadPixelLdg(inSampleView, numChannels, VecI<2>{x2, y0}); + const float p01 = interpolate::LoadPixelLdg(inSampleView, numChannels, VecI<2>{x0, y1}); + const float p11 = interpolate::LoadPixelLdg(inSampleView, numChannels, VecI<2>{x1, y1}); + const float p21 = interpolate::LoadPixelLdg(inSampleView, numChannels, VecI<2>{x2, y1}); + const float p02 = interpolate::LoadPixelLdg(inSampleView, numChannels, VecI<2>{x0, y2}); + const float p12 = interpolate::LoadPixelLdg(inSampleView, numChannels, VecI<2>{x1, y2}); + const float p22 = interpolate::LoadPixelLdg(inSampleView, numChannels, VecI<2>{x2, y2}); + + const int outX = srcX * 2; + + const float evenTop = DirectLinearLerp(p00, p10, 0.75f); + const float evenMid = DirectLinearLerp(p01, p11, 0.75f); + const float evenBot = DirectLinearLerp(p02, p12, 0.75f); + const float oddTop = DirectLinearLerp(p10, p20, 0.25f); + const float oddMid = DirectLinearLerp(p11, p21, 0.25f); + const float oddBot = DirectLinearLerp(p12, p22, 0.25f); + + *interpolate::GetWrapPtr(outSampleView, VecI<2>{outX, outY}) + = cuda::SaturateCast(DirectLinearLerp(evenTop, evenMid, 0.75f)); + *interpolate::GetWrapPtr(outSampleView, VecI<2>{outX + 1, outY}) + = cuda::SaturateCast(DirectLinearLerp(oddTop, oddMid, 0.75f)); + *interpolate::GetWrapPtr(outSampleView, VecI<2>{outX, outY + 1}) + = cuda::SaturateCast(DirectLinearLerp(evenMid, evenBot, 0.25f)); + *interpolate::GetWrapPtr(outSampleView, VecI<2>{outX + 1, outY + 1}) + = cuda::SaturateCast(DirectLinearLerp(oddMid, oddBot, 0.25f)); +} + +template +__global__ void DirectLinear2x2Kernel(const SampleDesc<2> sampleDesc, const PassOutWrap outWrap, + const PassInWrap inWrap) +{ + static_assert(kNumStaticChannels == 1); + static_assert(PassInWrap::kNumDimensions == 3); + static_assert(PassOutWrap::kNumDimensions == 3); + + using OutT = typename PassOutWrap::ValueType; + using InT = std::remove_const_t; + using OutBT = cuda::BaseType; + using InBT = cuda::BaseType; + + static_assert(cuda::NumElements == 1); + static_assert(cuda::NumElements == 1); + + constexpr int kLanes = kDirectLinear2x2Lanes; + + const int srcX0 = (blockIdx.x * blockDim.x + threadIdx.x) * kLanes; + const int srcY = blockIdx.y * blockDim.y + threadIdx.y; + const int sampleIdx = blockIdx.z; + const int inW = sampleDesc.shapes[0].x; + const int inH = sampleDesc.shapes[0].y; + if (srcX0 >= inW || srcY >= inH) + { + return; + } + + const int y0 = cuda::max(srcY - 1, 0); + const int y1 = srcY; + const int y2 = cuda::min(srcY + 1, inH - 1); + + const auto inSampleView = batch_wrapper::tensor::GetSampleView(inWrap, sampleIdx); + const auto outSampleView = batch_wrapper::tensor::GetSampleView(outWrap, sampleIdx); + const auto numChannels = NumChannels{}; + + if constexpr (kLanes == 1) + { + DirectLinear2x2Pixel(outSampleView, inSampleView, numChannels, srcX0, y0, y1, y2, inW, srcY * 2); + } + else if constexpr (kLanes > 1) + { + const int outY = srcY * 2; + // The vector body needs the whole [srcX0-1, srcX0+kLanes] column window unclamped. + if (srcX0 - 1 >= 0 && srcX0 + kLanes <= inW - 1) + { + float cols0[kLanes + 2], cols1[kLanes + 2], cols2[kLanes + 2]; +#pragma unroll + for (int i = 0; i < kLanes + 2; i++) + { + const int x = srcX0 - 1 + i; + cols0[i] = interpolate::LoadPixelLdg(inSampleView, numChannels, VecI<2>{x, y0}); + cols1[i] = interpolate::LoadPixelLdg(inSampleView, numChannels, VecI<2>{x, y1}); + cols2[i] = interpolate::LoadPixelLdg(inSampleView, numChannels, VecI<2>{x, y2}); + } + + OutBT top[2 * kLanes], bot[2 * kLanes]; +#pragma unroll + for (int l = 0; l < kLanes; l++) + { + const float evenTop = DirectLinearLerp(cols0[l], cols0[l + 1], 0.75f); + const float evenMid = DirectLinearLerp(cols1[l], cols1[l + 1], 0.75f); + const float evenBot = DirectLinearLerp(cols2[l], cols2[l + 1], 0.75f); + const float oddTop = DirectLinearLerp(cols0[l + 1], cols0[l + 2], 0.25f); + const float oddMid = DirectLinearLerp(cols1[l + 1], cols1[l + 2], 0.25f); + const float oddBot = DirectLinearLerp(cols2[l + 1], cols2[l + 2], 0.25f); + + top[2 * l] = cuda::SaturateCast(DirectLinearLerp(evenTop, evenMid, 0.75f)); + top[2 * l + 1] = cuda::SaturateCast(DirectLinearLerp(oddTop, oddMid, 0.75f)); + bot[2 * l] = cuda::SaturateCast(DirectLinearLerp(evenMid, evenBot, 0.25f)); + bot[2 * l + 1] = cuda::SaturateCast(DirectLinearLerp(oddMid, oddBot, 0.25f)); + } + + static_assert(2 * kLanes % 4 == 0); + // All 2*kLanes outputs are in-bounds (outW == 2*inW); alignment alone picks + // each row's store path. + StoreRowVectorized<4>(interpolate::GetWrapPtr(outSampleView, VecI<2>{srcX0 * 2, outY}), top); + StoreRowVectorized<4>(interpolate::GetWrapPtr(outSampleView, VecI<2>{srcX0 * 2, outY + 1}), bot); + } + else + { + for (int l = 0; l < kLanes && srcX0 + l < inW; l++) + { + DirectLinear2x2Pixel(outSampleView, inSampleView, numChannels, srcX0 + l, y0, y1, y2, inW, outY); + } + } + } +} + +} // namespace resampling + +namespace validate { +inline auto srcDst(const nvcv::Tensor &src, const nvcv::Tensor &dst) +{ + auto srcData = src.exportData(); + auto dstData = dst.exportData(); + + if (!srcData) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input must be cuda-accessible tensor"); + } + + if (!dstData) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Output must be cuda-accessible tensor"); + } + + using maybeTensorAccess = nvcv::Optional; + std::tuple ret; + + auto &[srcAccess, dstAccess, numSamples, numChannels, srcDtype, dstDtype] = ret; + + srcDtype = srcData->dtype(); + dstDtype = dstData->dtype(); + + srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcData); + dstAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*dstData); + NVCV_ASSERT(srcAccess && dstAccess); + + numSamples = srcAccess->numSamples(); + if (numSamples != dstAccess->numSamples()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of samples"); + } + + if (srcDtype.numChannels() > 1 || dstDtype.numChannels() > 1) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "The tensor channels should be explicit part of the shape, not tensor type"); + } + + numChannels = srcAccess->numChannels(); + if (numChannels != dstAccess->numChannels()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of channels"); + } + + if (numChannels <= 0) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Number of channels must be positive"); + } + + auto numPlanes = srcAccess->numPlanes(); + if (numPlanes != dstAccess->numPlanes()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of planes"); + } + + // Channel-first 2D tensors (NCHW/CHW) report numPlanes == numChannels; the operator views each + // channel plane as a single-channel image. Other multi-plane layouts (e.g. 3D channel-first) are + // not supported. + const bool isPlanar2D = srcData->layout() == nvcv::TENSOR_NCHW || srcData->layout() == nvcv::TENSOR_CHW; + if (numPlanes > 1 && !isPlanar2D) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Plannar images are not supported"); + } + + if (srcData->layout() != dstData->layout()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input and output must have the same layout"); + } + + return ret; +} + +inline void srcDst(int &numSamples, int &uniqueNumChannels, nvcv::DataType &srcDtype, nvcv::DataType &dstDtype, + const nvcv::ImageBatchVarShape &src, const nvcv::ImageBatchVarShape &dst) +{ + numSamples = src.numImages(); + if (numSamples != dst.numImages()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of samples"); + } + + const auto &srcFormat = src.uniqueFormat(); + const auto &dstFormat = dst.uniqueFormat(); + + if (!srcFormat || !dstFormat) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "All images in a batch must have the same format (including number of channels)"); + } + + auto numPlanes = srcFormat.numPlanes(); + if (numPlanes != dstFormat.numPlanes()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of planes"); + } + + // Multi-plane (planar, e.g. RGB8p) images are supported: each plane is processed as an + // independent single-channel image. 2-plane planar has no defined format and is rejected. + if (numPlanes == 2) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "2-plane planar images are not supported"); + } + + srcDtype = srcFormat.planeDataType(0); + dstDtype = dstFormat.planeDataType(0); + + uniqueNumChannels = srcFormat.numChannels(); + if (uniqueNumChannels != dstFormat.numChannels()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of channels"); + } +} + +inline void srcDst(int &numSamples, int &uniqueNumChannels, nvcv::DataType &srcDtype, nvcv::DataType &dstDtype, + const nvcv::TensorBatch &src, const nvcv::TensorBatch &dst) +{ + numSamples = src.numTensors(); + if (numSamples != dst.numTensors()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of samples"); + } + + uniqueNumChannels = -1; + srcDtype = src.dtype(); + dstDtype = dst.dtype(); + + if (srcDtype.numChannels() > 1 || dstDtype.numChannels() > 1) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "The tensor channels should be explicit part of the shape, not tensor type"); + } + + if (src.layout() != dst.layout()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output layouts"); + } + + if (src.layout() != nvcv::TENSOR_HW && src.layout() != nvcv::TENSOR_HWC && src.layout() != nvcv::TENSOR_DHW + && src.layout() != nvcv::TENSOR_DHWC) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "The tensor batch must contain [D]HW[C] samples"); + } +} + +inline void inOutNumberOfChannels(const HQResizeTensorShapeI &inShape, const HQResizeTensorShapeI &outShape) +{ + if (inShape.numChannels != outShape.numChannels) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Incompatible input/output number of channels in one of the samples"); + } + if (inShape.numChannels <= 0) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "The number of channels must be positive"); + } +} + +inline void sameInOutNdim(const HQResizeTensorShapeI &inShape, const HQResizeTensorShapeI &outShape) +{ + if (inShape.ndim != outShape.ndim) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Incompatible input/output number extents to resize"); + } +} + +inline void inOutShapes(int numSamples, const HQResizeTensorShapesI &inShapes, const HQResizeTensorShapesI &outShapes) +{ + if (inShapes.ndim != outShapes.ndim) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "The dimensionality of input and output shapes does not match"); + } + + if (numSamples != inShapes.size || numSamples != outShapes.size) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of samples"); + } + + if (inShapes.ndim != outShapes.ndim) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of extents"); + } + + if (inShapes.numChannels != outShapes.numChannels) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of channels"); + } + + if (inShapes.numChannels < 0) + { + for (int sampleIdx = 0; sampleIdx < numSamples; sampleIdx++) + { + inOutNumberOfChannels(inShapes.shape[sampleIdx], outShapes.shape[sampleIdx]); + } + } + else if (inShapes.numChannels == 0) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "The number of channels cannot be 0"); + } +} + +inline void roiBatch(int numSamples, int ndim, const HQResizeRoisF &rois) +{ + auto numRois = rois.size; + if (numRois != 0 && numRois != 1 && numRois != numSamples) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "The resize ROI list, if specified, must contain a single element to be used across all " + "samples in a batch or its length must match the batch size."); + } + if (numRois != 0) + { + if (rois.ndim != ndim) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "The number of ROI extents does not match the numebr of extents in the input"); + } + } +} +} // namespace validate + +namespace shape { + +template +struct Roi +{ + Vec Size() const + { + return hi - lo; + } + + Vec lo, hi; +}; + +inline const HQResizeRoiF *SampleRoi(const HQResizeRoisF &rois, int sampleIdx) +{ + if (rois.size == 0) + { + return nullptr; + } + else if (rois.size == 1) + { + return rois.roi; + } + else + { + return rois.roi + sampleIdx; + } +} + +template +inline VecI TensorShape(const HQResizeTensorShapeI &shape) +{ + VecI shapeVec; + for (int d = 0; d < kSpatialNDim; d++) + { + cuda::GetElement(shapeVec, d) = shape.extent[kSpatialNDim - d - 1]; + } + return shapeVec; +} + +template +inline VecI SampleShape(const HQResizeTensorShapesI &shapes, int sampleIdx) +{ + return TensorShape(shapes.shape[shapes.size == 1 ? 0 : sampleIdx]); +} + +template +inline VecI TensorShape(const nvcv::Tensor &tensor) +{ + static_assert(kSpatialNDim == 2 || kSpatialNDim == 3); + const auto &shape = tensor.shape(); + const auto &layout = tensor.layout(); + char shapeArgLayout[4] = "WHD"; + VecI tensorShape; + for (int d = 0; d < kSpatialNDim; d++) + { + int axis = layout.find(shapeArgLayout[d]); + if (axis < 0) + { + throw TensorShapeError( + "The layout of an input tensor to the resize operator must contain HW extents in the layout (for " + "images) or DHW extents (for 3D resampling). Some extents are missing in the input tensor."); + } + cuda::GetElement(tensorShape, d) = shape[axis]; + } + return tensorShape; +} + +template +inline VecI SampleShape(const nvcv::ImageBatchVarShape &batch, int sampleIdx) +{ + static_assert(kSpatialNDim == 2); + VecI sampleShape; + const nvcv::Image &image = batch[sampleIdx]; + const auto &imageSize = image.size(); + sampleShape.x = imageSize.w; + sampleShape.y = imageSize.h; + return sampleShape; +} + +template +inline VecI SampleShape(const nvcv::TensorBatch &batch, int sampleIdx) +{ + return TensorShape(batch[sampleIdx]); +} + +inline int TensorNumChannels(const nvcv::Tensor &tensor) +{ + const auto &shape = tensor.shape(); + const auto &layout = tensor.layout(); + int channelAxis = layout.find('C'); + if (channelAxis < 0) + { + return 1; + } + return shape[channelAxis]; +} + +inline int64_t TensorByteSize(const nvcv::Tensor &tensor) +{ + auto data = tensor.exportData().cast(); + assert(data); + return data->stride(0) * data->shape(0); +} + +inline int64_t ImageByteSize(const nvcv::Image &image) +{ + auto data = image.exportData(); + assert(data); + auto plane = data->plane(0); // only single-plane images are supported + return plane.rowStride * plane.height; +} + +inline int SampleNumChannels(const nvcv::TensorBatch &src, const nvcv::TensorBatch &dst, int sampleIdx) +{ + const auto &srcSample = src[sampleIdx]; + const auto &dstSample = dst[sampleIdx]; + int numChannels = TensorNumChannels(srcSample); + if (numChannels != TensorNumChannels(dstSample)) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of channels"); + } + if (numChannels <= 0) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Number of channels must be positive"); + } + return numChannels; +} +} // namespace shape + +/** + * @brief Calculates optimum processing order based on input/output sizes and filter support. + * + * The sizes of intermediate storage and time taken to compute the intermediate images + * may depend on the order - i.e. if downscaling only one axis, it's beneficial to resample that + * axis first, so that intermediate image is smaller. + */ +template +class ProcessingOrderCalculator + +{ +public: + static constexpr float size_bias = 3; + + ProcessingOrderCalculator(const VecI inSize, const VecI outSize, const VecI filterSupport) + : m_inSize(inSize) + , m_outSize(outSize) + , m_filterSupport(filterSupport) + { + } + + VecI operator()() + { + for (int i = 0; i < ndim; i++) cuda::GetElement(m_bestOrder, i) = i; + m_axisVisited = {}; + m_currSize = m_inSize; + m_minCost = 1e+30f; + Run(0); + return m_bestOrder; + } + +private: + // recursively check every possible order in DFS fashion + void Run(int pass, float totalCost = 0) + { + if (totalCost >= m_minCost) + return; // this branch of recursion will not yield a better result - abandon it + + if (pass == ndim) + { + m_minCost = totalCost; + m_bestOrder = m_currOrder; + } + else + { + for (int a = 0; a < ndim; a++) + { + if (cuda::GetElement(m_axisVisited, a)) + continue; + cuda::GetElement(m_axisVisited, a) = true; + cuda::GetElement(m_currOrder, pass) = a; + auto prevSize = cuda::GetElement(m_currSize, a); + cuda::GetElement(m_currSize, a) = cuda::GetElement(m_outSize, a); + + float passCost = PassCost(pass, a); + Run(pass + 1, totalCost + passCost); + + cuda::GetElement(m_currSize, a) = prevSize; + cuda::GetElement(m_axisVisited, a) = false; + } + } + } + + float PassCost(int pass, int axis) + { + // y-axis is likely to be the cheapest + float axisCost = axis == 0 ? 1.4f : axis > 1 ? 1.2f : 1.0f; + auto vol = utils::Volume(m_currSize); + float baseComputeCost = cuda::GetElement(m_filterSupport, axis) * vol; + return axisCost * baseComputeCost + vol * size_bias; + } + + const VecI m_inSize, m_outSize, m_filterSupport; + float m_minCost; + VecI m_currSize, m_bestOrder, m_currOrder, m_axisVisited; +}; + +template +inline void RunTypedSwitch(nvcv::DataType srcDtype, nvcv::DataType dstDtype, int numChannels, const Cb &cb) +{ + using uchar = unsigned char; + +#define NVCV_RUN_DYNAMIC_CHANNELS_HQ_RESIZE(SRC_TYPE_NAME, DST_TYPE_NAME, SRC_VEC, DST_VEC) \ + ((srcDtype == nvcv::TYPE_##SRC_TYPE_NAME) && (dstDtype == nvcv::TYPE_##DST_TYPE_NAME)) \ + cb(SRC_VEC{}, IntermediateBaseT{}, DST_VEC{}, std::integral_constant{}) + +#define NVCV_RUN_SINGLE_CHANNEL_HQ_RESIZE(NUM_STATIC_CHANNELS, SRC_TYPE_NAME, DST_TYPE_NAME, SRC_VEC, DST_VEC) \ + ((numChannels == NUM_STATIC_CHANNELS) && (srcDtype == nvcv::TYPE_##SRC_TYPE_NAME) \ + && (dstDtype == nvcv::TYPE_##DST_TYPE_NAME)) \ + cb(SRC_VEC{}, IntermediateBaseT{}, DST_VEC{}, std::integral_constant{}) + +#define NVCV_RUN_MULTI_STATIC_CHANNEL_HQ_RESIZE(NUM_STATIC_CHANNELS, SRC_TYPE_NAME, DST_TYPE_NAME, SRC_VEC, DST_VEC) \ + ((numChannels == NUM_STATIC_CHANNELS) \ + && (srcDtype == nvcv::TYPE_##SRC_TYPE_NAME || srcDtype == nvcv::TYPE_##NUM_STATIC_CHANNELS##SRC_TYPE_NAME) \ + && (dstDtype == nvcv::TYPE_##DST_TYPE_NAME || dstDtype == nvcv::TYPE_##NUM_STATIC_CHANNELS##DST_TYPE_NAME)) \ + cb(SRC_VEC##NUM_STATIC_CHANNELS{}, Vec{}, \ + DST_VEC##NUM_STATIC_CHANNELS{}, std::integral_constant{}) + + // clang-format off + if NVCV_RUN_SINGLE_CHANNEL_HQ_RESIZE(1, U8, U8, uchar, uchar); + else if NVCV_RUN_MULTI_STATIC_CHANNEL_HQ_RESIZE(2, U8, U8, uchar, uchar); + else if NVCV_RUN_MULTI_STATIC_CHANNEL_HQ_RESIZE(3, U8, U8, uchar, uchar); + else if NVCV_RUN_MULTI_STATIC_CHANNEL_HQ_RESIZE(4, U8, U8, uchar, uchar); + else if NVCV_RUN_DYNAMIC_CHANNELS_HQ_RESIZE(U8, U8, uchar, uchar); + + else if NVCV_RUN_SINGLE_CHANNEL_HQ_RESIZE(1, U8, F32, uchar, float); + else if NVCV_RUN_MULTI_STATIC_CHANNEL_HQ_RESIZE(2, U8, F32, uchar, float); + else if NVCV_RUN_MULTI_STATIC_CHANNEL_HQ_RESIZE(3, U8, F32, uchar, float); + else if NVCV_RUN_MULTI_STATIC_CHANNEL_HQ_RESIZE(4, U8, F32, uchar, float); + else if NVCV_RUN_DYNAMIC_CHANNELS_HQ_RESIZE(U8, F32, uchar, float); + + else if NVCV_RUN_SINGLE_CHANNEL_HQ_RESIZE(1, S16, S16, short, short); + else if NVCV_RUN_MULTI_STATIC_CHANNEL_HQ_RESIZE(2, S16, S16, short, short); + else if NVCV_RUN_MULTI_STATIC_CHANNEL_HQ_RESIZE(3, S16, S16, short, short); + else if NVCV_RUN_MULTI_STATIC_CHANNEL_HQ_RESIZE(4, S16, S16, short, short); + else if NVCV_RUN_DYNAMIC_CHANNELS_HQ_RESIZE(S16, S16, short, short); + + else if NVCV_RUN_SINGLE_CHANNEL_HQ_RESIZE(1, S16, F32, short, float); + else if NVCV_RUN_MULTI_STATIC_CHANNEL_HQ_RESIZE(2, S16, F32, short, float); + else if NVCV_RUN_MULTI_STATIC_CHANNEL_HQ_RESIZE(3, S16, F32, short, float); + else if NVCV_RUN_MULTI_STATIC_CHANNEL_HQ_RESIZE(4, S16, F32, short, float); + else if NVCV_RUN_DYNAMIC_CHANNELS_HQ_RESIZE(S16, F32, short, float); + + else if NVCV_RUN_SINGLE_CHANNEL_HQ_RESIZE(1, U16, U16, ushort, ushort); + else if NVCV_RUN_MULTI_STATIC_CHANNEL_HQ_RESIZE(2, U16, U16, ushort, ushort); + else if NVCV_RUN_MULTI_STATIC_CHANNEL_HQ_RESIZE(3, U16, U16, ushort, ushort); + else if NVCV_RUN_MULTI_STATIC_CHANNEL_HQ_RESIZE(4, U16, U16, ushort, ushort); + else if NVCV_RUN_DYNAMIC_CHANNELS_HQ_RESIZE(U16, U16, ushort, ushort); + + else if NVCV_RUN_SINGLE_CHANNEL_HQ_RESIZE(1, U16, F32, ushort, float); + else if NVCV_RUN_MULTI_STATIC_CHANNEL_HQ_RESIZE(2, U16, F32, ushort, float); + else if NVCV_RUN_MULTI_STATIC_CHANNEL_HQ_RESIZE(3, U16, F32, ushort, float); + else if NVCV_RUN_MULTI_STATIC_CHANNEL_HQ_RESIZE(4, U16, F32, ushort, float); + else if NVCV_RUN_DYNAMIC_CHANNELS_HQ_RESIZE(U16, F32, ushort, float); + + else if NVCV_RUN_SINGLE_CHANNEL_HQ_RESIZE(1, F32, F32, float, float); + else if NVCV_RUN_MULTI_STATIC_CHANNEL_HQ_RESIZE(2, F32, F32, float, float); + else if NVCV_RUN_MULTI_STATIC_CHANNEL_HQ_RESIZE(3, F32, F32, float, float); + else if NVCV_RUN_MULTI_STATIC_CHANNEL_HQ_RESIZE(4, F32, F32, float, float); + else if NVCV_RUN_DYNAMIC_CHANNELS_HQ_RESIZE(F32, F32, float, float); + else + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Unsupported input/output types. The resize operator supports the " + "following types: uint8, int16, uint16, and float32. " + "The output type must be same as the input type or float."); + } +// clang-format on +#undef NVCV_RUN_DYNAMIC_CHANNELS_HQ_RESIZE +#undef NVCV_RUN_SINGLE_CHANNEL_HQ_RESIZE +#undef NVCV_RUN_MULTI_STATIC_CHANNEL_HQ_RESIZE +} + +template +class HQResizeRun +{ +public: + static_assert(_kSpatialNDim == 2 || _kSpatialNDim == 3, + "Currently, the resampling operator supports only 2 or 3 spatial dimensions"); + + HQResizeRun(const filter::ResamplingFiltersFactory &filtersFactory) + : m_filtersFactory{filtersFactory} + { + } + + using SampleDescT = resampling::SampleDesc<_kSpatialNDim>; + static_assert(std::is_trivially_copyable_v); + using DynamicBatchWrapMeta = batch_wrapper::dynamic::DynamicBatchWrapMeta; + + static constexpr VecI<3> kBlockDim = {32, 8, 1}; + static constexpr int kSpatialNDim = _kSpatialNDim; + // the number of buffers for intermediate results + static constexpr int kNumTmpBuffers = kSpatialNDim - 1; + // use alignment suitable for maximal supported number of static channels + static constexpr int kIntermediateAlignment = alignof(Vec); + + // Computes workspace requierements for calling the operator with tensor (uniform batch) input/output + cvcuda::WorkspaceRequirements getWorkspaceRequirements(int numSamples, const HQResizeTensorShapeI inputShape, + const HQResizeTensorShapeI outputShape, + const NVCVInterpolationType minInterpolation, + const NVCVInterpolationType magInterpolation, + const bool antialias, const HQResizeRoiF *roi) const + { + validate::inOutNumberOfChannels(inputShape, outputShape); + validate::sameInOutNdim(inputShape, outputShape); + + SampleDescT sampleDesc; + VecI srcShape = shape::TensorShape(inputShape); + VecI dstShape = shape::TensorShape(outputShape); + int numChannels = inputShape.numChannels; + auto [minFilter, magFilter] = filter::GetFilterModes(minInterpolation, magInterpolation, antialias); + SetupSampleDesc(sampleDesc, srcShape, dstShape, numChannels, roi, minFilter, magFilter); + + cvcuda::WorkspaceEstimator est; + for (int t = 0; t < kNumTmpBuffers; t++) + { + // the vectorized alignment may or may not be needed, depending on the number of channels + est.addCuda(GetPassOutputVolume(sampleDesc, t) * numSamples, kIntermediateAlignment); + } + + cvcuda::WorkspaceRequirements req{}; + req.hostMem = est.hostMem.req; + req.pinnedMem = est.pinnedMem.req; + req.cudaMem = est.cudaMem.req; + + // The allocator requries the total size of the allocation to be aligned + cvcuda::AlignUp(req); + return req; + } + + // Computes workspace requirements for calling the operator with TensorBatch/ImageBatchVarShape input/output + cvcuda::WorkspaceRequirements getWorkspaceRequirements(int numSamples, const HQResizeTensorShapesI inputShapes, + const HQResizeTensorShapesI outputShapes, + const NVCVInterpolationType minInterpolation, + const NVCVInterpolationType magInterpolation, + const bool antialias, const HQResizeRoisF rois) const + { + validate::roiBatch(numSamples, kSpatialNDim, rois); + validate::inOutShapes(numSamples, inputShapes, outputShapes); + auto [minFilter, magFilter] = filter::GetFilterModes(minInterpolation, magInterpolation, antialias); + + size_t intermediateSizes[kNumTmpBuffers]{}; + for (int sampleIdx = 0; sampleIdx < numSamples; sampleIdx++) + { + const VecI srcShape = shape::SampleShape(inputShapes, sampleIdx); + const VecI dstShape = shape::SampleShape(outputShapes, sampleIdx); + const HQResizeRoiF *sampleRoi = shape::SampleRoi(rois, sampleIdx); + int numChannels + = inputShapes.numChannels < 0 ? inputShapes.shape[sampleIdx].numChannels : inputShapes.numChannels; + + SampleDescT sampleDesc; + SetupSampleDesc(sampleDesc, srcShape, dstShape, numChannels, sampleRoi, minFilter, magFilter); + for (int t = 0; t < kNumTmpBuffers; t++) + { + intermediateSizes[t] += GetPassOutputVolume(sampleDesc, t); + } + } + + cvcuda::WorkspaceEstimator est; + est.addPinned(numSamples); + est.addCuda(numSamples); + + // reserve space for pointers and strides for intermediate wrappers + for (int t = 0; t < kNumTmpBuffers; t++) + { + batch_wrapper::dynamic::AddDynamicBatchWrapMeta(est, numSamples); + } + for (int t = 0; t < kNumTmpBuffers; t++) + { + // the vectorized alignment may or may not be needed, depending on the number of channels + est.addCuda(intermediateSizes[t], kIntermediateAlignment); + } + + cvcuda::WorkspaceRequirements req{}; + req.hostMem = est.hostMem.req; + req.pinnedMem = est.pinnedMem.req; + req.cudaMem = est.cudaMem.req; + // The allocator requries the total size of the allocation to be aligned + cvcuda::AlignUp(req); + + return req; + } + + // Computes upper bound for workspace requirements, i.e. the workspace that meets the computed requirements + // can be passed to the call with any type of input/output as long as there are no more than maxBatchSize + // samples that do not exceed the maxShape (in the input nor in the output). + cvcuda::WorkspaceRequirements getWorkspaceRequirements(int maxNumSamples, const HQResizeTensorShapeI maxShape) const + { + validate::inOutNumberOfChannels(maxShape, maxShape); + + cvcuda::WorkspaceEstimator est; + est.addPinned(maxNumSamples); + est.addCuda(maxNumSamples); + + // reserve space for pointers and strides for intermediate wrappers + for (int t = 0; t < kNumTmpBuffers; t++) + { + batch_wrapper::dynamic::AddDynamicBatchWrapMeta(est, maxNumSamples); + } + VecI shape = shape::TensorShape(maxShape); + for (int t = 0; t < kNumTmpBuffers; t++) + { + size_t numElements = utils::Volume(shape) * maxNumSamples * maxShape.numChannels; + est.addCuda(numElements, kIntermediateAlignment); + } + + cvcuda::WorkspaceRequirements req{}; + req.hostMem = est.hostMem.req; + req.pinnedMem = est.pinnedMem.req; + req.cudaMem = est.cudaMem.req; + // The allocator requries the total size of the allocation to be aligned + cvcuda::AlignUp(req); + + return req; + } + + void operator()(cudaStream_t stream, const cvcuda::Workspace &ws, const nvcv::Tensor &src, const nvcv::Tensor &dst, + const NVCVInterpolationType minInterpolation, const NVCVInterpolationType magInterpolation, + const bool antialias, const HQResizeRoiF *roi) const + { + auto tensorAccess = validate::srcDst(src, dst); + auto &[srcAccess, dstAccess, numSamples, numChannels, srcDtype, dstDtype] = tensorAccess; + + // Planar (NCHW/CHW) input: view each (sample, channel) plane as a single-channel image and + // run the interleaved single-channel path over N*C samples. HQResize is channel-independent, + // so the result is bit-identical to the interleaved path and no new kernels are needed. + // Workspace invariance: the caller sized the workspace from the public shape, and per-pass + // volume Volume(pass)*C*N == Volume(pass)*1*(N*C), so the expanded run consumes exactly the + // allocated buffer. The views must outlive RunPasses, hence the scope-level Optionals. + nvcv::Optional srcPlanarView, dstPlanarView; + const bool isPlanar = (src.layout() == nvcv::TENSOR_NCHW || src.layout() == nvcv::TENSOR_CHW); + if (isPlanar) + { + auto srcData = src.exportData(); + auto dstData = dst.exportData(); + srcPlanarView = planar::PlanarAsSingleChannelView(*srcData, *srcAccess); + dstPlanarView = planar::PlanarAsSingleChannelView(*dstData, *dstAccess); + srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcPlanarView); + dstAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*dstPlanarView); + const int64_t expandedSamples = static_cast(numSamples) * numChannels; + if (expandedSamples > 65535) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Planar HQResize launch exceeds the CUDA grid limit: N*C must not exceed 65535"); + } + numSamples = static_cast(expandedSamples); + numChannels = 1; + } + + SampleDescT sampleDesc; + VecI srcShape = shape::TensorShape(src); + VecI dstShape = shape::TensorShape(dst); + const auto [minFilter, magFilter] = filter::GetFilterModes(minInterpolation, magInterpolation, antialias); + SetupSampleDesc(sampleDesc, srcShape, dstShape, numChannels, roi, minFilter, magFilter); + + cvcuda::WorkspaceAllocator allocator(ws); + if (ws.cudaMem.ready != nullptr) + { + NVCV_CHECK_THROW(cudaStreamWaitEvent(stream, ws.cudaMem.ready)); + } + IntermediateBaseT *intermediate[kNumTmpBuffers]; + // Get intermediate buffers + for (int t = 0; t < kNumTmpBuffers; t++) + { + intermediate[t] = allocator.getCuda(GetPassOutputVolume(sampleDesc, t) * numSamples, + kIntermediateAlignment); + } + + auto inMaxStride = shape::TensorByteSize(src); + auto outMaxStride = shape::TensorByteSize(dst); + bool wideStride = std::max(inMaxStride, outMaxStride) > cuda::TypeTraits::max; + + RunTypedSwitch( + srcDtype, dstDtype, numChannels, + [this, &tensorAccess, &wideStride, &sampleDesc, &intermediate, &ws, &stream]( + auto dummySrcVal, auto intermediateVal, auto dummyDstVal, auto numChannelsVal) + { + using InT = decltype(dummySrcVal); + using IntermediateT = decltype(intermediateVal); + using OutT = decltype(dummyDstVal); + constexpr int numStaticChannels = decltype(numChannelsVal)::value; + static_assert(numStaticChannels == -1 || numStaticChannels == cuda::NumElements); + static_assert(cuda::NumElements == cuda::NumElements); + static_assert(cuda::NumElements == cuda::NumElements); + + auto &[srcAccess, dstAccess, numSamples, numChannels, srcDtype, dstDtype] = tensorAccess; + if (wideStride) + { + RunPasses( + sampleDesc, *dstAccess, *srcAccess, intermediate, numSamples, ws, stream); + } + else + { + RunPasses( + sampleDesc, *dstAccess, *srcAccess, intermediate, numSamples, ws, stream); + } + }); + } + + template + void operator()(cudaStream_t stream, const cvcuda::Workspace &ws, const BatchContainer &src, + const BatchContainer &dst, const NVCVInterpolationType minInterpolation, + const NVCVInterpolationType magInterpolation, const bool antialias, const HQResizeRoisF rois) const + { + int numSamples; + int uniqueNumChannels; // numChannels for ImageBatchVarShape, -1 for TensorBatch + nvcv::DataType srcDtype, dstDtype; + validate::srcDst(numSamples, uniqueNumChannels, srcDtype, dstDtype, src, dst); + validate::roiBatch(numSamples, kSpatialNDim, rois); + + // Planar (multi-plane, e.g. RGB8p) var-shape input: process each of the numImages*channels + // planes as an independent single-channel image. The kernel runs over the expanded sample + // count with channels=1 and the var-shape adapter decodes each expanded index to its + // {image, plane}. The caller must size the workspace for the expanded sample count -- the + // bindings/tests build the workspace shapes as numImages*channels single-channel samples, so + // the per-sample metadata (SampleDesc, wrap meta) and intermediate volume match. + int planarChannels = 1; + if constexpr (std::is_same_v) + { + const auto &fmt = src.uniqueFormat(); + if (fmt && fmt.numPlanes() > 1) + { + planarChannels = fmt.numPlanes(); + } + } + const bool isPlanar = planarChannels > 1; + const int kernelChannels = isPlanar ? 1 : uniqueNumChannels; + const int64_t numKernelSamples64 = static_cast(numSamples) * planarChannels; + if (isPlanar && numKernelSamples64 > 65535) + { + throw nvcv::Exception( + nvcv::Status::ERROR_INVALID_ARGUMENT, + "Planar HQResize launch exceeds the CUDA grid limit: numImages*channels must not exceed 65535"); + } + const int numKernelSamples = static_cast(numKernelSamples64); + + const auto [minFilter, magFilter] = filter::GetFilterModes(minInterpolation, magInterpolation, antialias); + cvcuda::WorkspaceAllocator allocator(ws); + if (ws.pinnedMem.ready != nullptr) + { + NVCV_CHECK_THROW(cudaEventSynchronize(ws.pinnedMem.ready)); + } + if (ws.cudaMem.ready != nullptr) + { + NVCV_CHECK_THROW(cudaStreamWaitEvent(stream, ws.cudaMem.ready)); + } + SampleDescT *sampleDescsCpu = allocator.getPinned(numKernelSamples); + SampleDescT *sampleDescsGpu = allocator.getCuda(numKernelSamples); + size_t intermediateSizes[kNumTmpBuffers]{}; + + int64_t inMaxStride = 0; + int64_t outMaxStride = 0; + for (int sampleIdx = 0; sampleIdx < numKernelSamples; sampleIdx++) + { + // For planar, every channel plane of an image shares the image's spatial shape/ROI. + const int imageIdx = isPlanar ? sampleIdx / planarChannels : sampleIdx; + const VecI srcShape = shape::SampleShape(src, imageIdx); + const VecI dstShape = shape::SampleShape(dst, imageIdx); + const HQResizeRoiF *sampleRoi = shape::SampleRoi(rois, imageIdx); + int numChannels; + if constexpr (std::is_same_v) + { + numChannels = kernelChannels; + inMaxStride = std::max(inMaxStride, shape::ImageByteSize(src[imageIdx])); + outMaxStride = std::max(outMaxStride, shape::ImageByteSize(dst[imageIdx])); + } + else + { + static_assert(std::is_same_v); + numChannels = shape::SampleNumChannels(src, dst, imageIdx); + inMaxStride = std::max(inMaxStride, shape::TensorByteSize(src[imageIdx])); + outMaxStride = std::max(outMaxStride, shape::TensorByteSize(dst[imageIdx])); + } + SampleDescT &sampleDesc = sampleDescsCpu[sampleIdx]; + SetupSampleDesc(sampleDesc, srcShape, dstShape, numChannels, sampleRoi, minFilter, magFilter); + for (int t = 0; t < kNumTmpBuffers; t++) + { + intermediateSizes[t] += GetPassOutputVolume(sampleDesc, t); + } + } + bool wideStride = std::max(inMaxStride, outMaxStride) > cuda::TypeTraits::max; + + NVCV_CHECK_THROW(cudaMemcpyAsync(sampleDescsGpu, sampleDescsCpu, numKernelSamples * sizeof(SampleDescT), + cudaMemcpyHostToDevice, stream)); + + // allocate space for pointers and strides for intermediate wrappers + DynamicBatchWrapMeta intermediateMeta[kNumTmpBuffers]; + IntermediateBaseT *intermediate[kNumTmpBuffers]; + for (int t = 0; t < kNumTmpBuffers; t++) + { + intermediateMeta[t] + = batch_wrapper::dynamic::AllocateDynamicBatchWrapMeta(allocator, numKernelSamples, wideStride); + } + // allocate space for intermediate data + for (int t = 0; t < kNumTmpBuffers; t++) + { + intermediate[t] = allocator.getCuda(intermediateSizes[t], kIntermediateAlignment); + } + + Run(sampleDescsCpu, sampleDescsGpu, src, dst, intermediate, intermediateMeta, numKernelSamples, srcDtype, + dstDtype, kernelChannels, wideStride, ws, stream, planarChannels); + } + +private: + void Run(const SampleDescT *sampleDescsCpu, const SampleDescT *sampleDescsGpu, const nvcv::ImageBatchVarShape &src, + const nvcv::ImageBatchVarShape &dst, IntermediateBaseT *intermediate[kNumTmpBuffers], + const DynamicBatchWrapMeta intermediateMeta[kNumTmpBuffers], int numSamples, const nvcv::DataType srcDtype, + const nvcv::DataType dstDtype, int uniqueNumChannels, bool wideStride, const cvcuda::Workspace &ws, + cudaStream_t stream, int planarChannels = 1) const + { + static_assert(kSpatialNDim == 2, "ImageBatchVarShape does not support 3D spatial resampling"); + + auto srcData = src.exportData(stream); + auto dstData = dst.exportData(stream); + if (!srcData) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input must be cuda-accessible, varshape pitch-linear image batch"); + } + + if (!dstData) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Output must be cuda-accessible, varshape pitch-linear image batch"); + } + + RunTypedSwitch( + srcDtype, dstDtype, uniqueNumChannels, + [this, &wideStride, &sampleDescsCpu, &sampleDescsGpu, &dstData, &srcData, &intermediate, &intermediateMeta, + &numSamples, &ws, &stream, + &planarChannels](auto dummySrcVal, auto intermediateVal, auto dummyDstVal, auto numChannelsVal) + { + using InT = decltype(dummySrcVal); + using IntermediateT = decltype(intermediateVal); + using OutT = decltype(dummyDstVal); + constexpr int numStaticChannels = decltype(numChannelsVal)::value; + if constexpr (numStaticChannels == -1) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Unsupported number of channels for ImageBatchVarShape input."); + } + else if constexpr (numStaticChannels != -1) + { + static_assert(numStaticChannels == cuda::NumElements); + static_assert(cuda::NumElements == cuda::NumElements); + static_assert(cuda::NumElements == cuda::NumElements); + if (wideStride) + { + RunPasses( + sampleDescsCpu, sampleDescsGpu, *dstData, *srcData, intermediate, intermediateMeta, + numSamples, ws, stream, planarChannels); + } + else + { + RunPasses( + sampleDescsCpu, sampleDescsGpu, *dstData, *srcData, intermediate, intermediateMeta, + numSamples, ws, stream, planarChannels); + } + } + }); + } + + void Run(const SampleDescT *sampleDescsCpu, const SampleDescT *sampleDescsGpu, const nvcv::TensorBatch &src, + const nvcv::TensorBatch &dst, IntermediateBaseT *intermediate[kNumTmpBuffers], + const DynamicBatchWrapMeta intermediateMeta[kNumTmpBuffers], int numSamples, const nvcv::DataType srcDtype, + const nvcv::DataType dstDtype, int uniqueNumChannels, bool wideStride, const cvcuda::Workspace &ws, + cudaStream_t stream, int planarChannels = 1) const + { + // Planar TensorBatch is not yet wired (the var-shape adapter handles plane decode; the + // tensor-batch adapter does not), so the operator only ever passes planarChannels == 1 here. + NVCV_ASSERT(planarChannels == 1); + + // Other cointainer allow exporting data with const qualifiers + const auto srcData + = const_cast(src).exportData(stream).cast(); + const auto dstData + = const_cast(dst).exportData(stream).cast(); + + if (!srcData) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input must be cuda-accessible, varshape pitch-linear image batch"); + } + + if (!dstData) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Output must be cuda-accessible, varshape pitch-linear image batch"); + } + + uniqueNumChannels = -1; + for (int sampleIdx = 0; sampleIdx < numSamples; sampleIdx++) + { + if (sampleIdx == 0) + { + uniqueNumChannels = sampleDescsCpu[sampleIdx].channels; + } + else if (uniqueNumChannels != sampleDescsCpu[sampleIdx].channels) + { + uniqueNumChannels = -1; + break; + } + } + + RunTypedSwitch( + srcDtype, dstDtype, uniqueNumChannels, + [this, &wideStride, &sampleDescsCpu, &sampleDescsGpu, &dstData, &srcData, &intermediate, &intermediateMeta, + &numSamples, &ws, &stream](auto dummySrcVal, auto intermediateVal, auto dummyDstVal, auto numChannelsVal) + { + using InT = decltype(dummySrcVal); + using IntermediateT = decltype(intermediateVal); + using OutT = decltype(dummyDstVal); + constexpr int numStaticChannels = decltype(numChannelsVal)::value; + static_assert(numStaticChannels == -1 || numStaticChannels == cuda::NumElements); + static_assert(cuda::NumElements == cuda::NumElements); + static_assert(cuda::NumElements == cuda::NumElements); + + if (wideStride) + { + RunPasses( + sampleDescsCpu, sampleDescsGpu, *dstData, *srcData, intermediate, intermediateMeta, numSamples, + ws, stream); + } + else + { + RunPasses( + sampleDescsCpu, sampleDescsGpu, *dstData, *srcData, intermediate, intermediateMeta, numSamples, + ws, stream); + } + }); + } + + static void RecordReady(cudaEvent_t event, cudaStream_t stream) + { + if (event != nullptr) + { + NVCV_CHECK_THROW(cudaEventRecord(event, stream)); + } + } + + template + std::enable_if_t RunPasses(const SampleDescT &sampleDesc, + const nvcv::TensorDataAccessStridedImagePlanar &dstAccess, + const nvcv::TensorDataAccessStridedImagePlanar &srcAccess, + IntermediateBaseT *intermediate[kNumTmpBuffers], int numSamples, + const cvcuda::Workspace &ws, cudaStream_t stream) const + { + static_assert(kSpatialNDim == 2); + constexpr bool kHasDynamicChannels = kNumStaticChannels == -1; + // sample extent, spatial extents, optional dynamic channel extent + constexpr int kWrapNDim = 1 + kSpatialNDim + kHasDynamicChannels; + using OutWrap = cuda::TensorNDWrap; + using InWrap = cuda::TensorNDWrap; + using InterWrap = cuda::TensorNDWrap; + static_assert(std::is_trivially_copyable_v); + static_assert(std::is_trivially_copyable_v); + static_assert(std::is_trivially_copyable_v); + const OutWrap outWrap + = batch_wrapper::tensor::WrapTensor(dstAccess); + const InWrap inWrap = batch_wrapper::tensor::WrapTensor( + srcAccess, sampleDesc.inRoiOffset); + const bool useDirect + = ShouldUseDirectTensorPath(sampleDesc, std::is_same_v, float>, kNumStaticChannels); + if (numSamples > 0 && useDirect + && (TryRunDirectLinear(sampleDesc, outWrap, inWrap, + numSamples, stream) + || TryRunDirectLinear(sampleDesc, outWrap, inWrap, + numSamples, stream) + || TryRunDirectFilter2x2(sampleDesc, outWrap, inWrap, numSamples, stream) + || TryRunDirectFilterContract2x(sampleDesc, outWrap, inWrap, numSamples, stream) + || TryRunDirectFilter(sampleDesc, outWrap, inWrap, numSamples, stream))) + { + RecordReady(ws.cudaMem.ready, stream); + return; + } + const InterWrap interWrap = batch_wrapper::tensor::CreateDenseWrap( + intermediate[0], sampleDesc.channels, sampleDesc.shapes[1]); + + SampleDescT runDesc = sampleDesc; + SetupTensorBlockLayout(runDesc); + RunPass(runDesc, interWrap, inWrap, numSamples, stream); + RunPass(runDesc, outWrap, interWrap, numSamples, stream); + RecordReady(ws.cudaMem.ready, stream); + } + + template + std::enable_if_t RunPasses(const SampleDescT &sampleDesc, + const nvcv::TensorDataAccessStridedImagePlanar &dstAccess, + const nvcv::TensorDataAccessStridedImagePlanar &srcAccess, + IntermediateBaseT *intermediate[kNumTmpBuffers], int numSamples, + const cvcuda::Workspace &ws, cudaStream_t stream) const + { + static_assert(kSpatialNDim == 3); + constexpr bool kHasDynamicChannels = kNumStaticChannels == -1; + // sample extent, spatial extents, optional dynamic channel extent + constexpr int kWrapNDim = 1 + kSpatialNDim + kHasDynamicChannels; + using OutWrap = cuda::TensorNDWrap; + using InWrap = cuda::TensorNDWrap; + using InterWrap = cuda::TensorNDWrap; + static_assert(std::is_trivially_copyable_v); + static_assert(std::is_trivially_copyable_v); + static_assert(std::is_trivially_copyable_v); + const OutWrap outWrap + = batch_wrapper::tensor::WrapTensor(dstAccess); + const InWrap inWrap = batch_wrapper::tensor::WrapTensor( + srcAccess, sampleDesc.inRoiOffset); + const InterWrap interWrap0 + = batch_wrapper::tensor::CreateDenseWrap( + intermediate[0], sampleDesc.channels, sampleDesc.shapes[1]); + const InterWrap interWrap1 + = batch_wrapper::tensor::CreateDenseWrap( + intermediate[1], sampleDesc.channels, sampleDesc.shapes[2]); + RunPass(sampleDesc, interWrap0, inWrap, numSamples, stream); + RunPass(sampleDesc, interWrap1, interWrap0, numSamples, stream); + RunPass(sampleDesc, outWrap, interWrap1, numSamples, stream); + RecordReady(ws.cudaMem.ready, stream); + } + + template + std::enable_if_t RunPasses(const SampleDescT *sampleDescsCpu, const SampleDescT *sampleDescsGpu, + const BatchDataStridedCuda &dstData, const BatchDataStridedCuda &srcData, + IntermediateBaseT *intermediate[kNumTmpBuffers], + const DynamicBatchWrapMeta intermediateMeta[kNumTmpBuffers], int numSamples, + const cvcuda::Workspace &ws, cudaStream_t stream, + int planarChannels = 1) const + { + static_assert(kSpatialNDim == 2); + constexpr bool kHasDynamicChannels = kNumStaticChannels == -1; + // sample extent, spatial extents, optional dynamic channel extent + constexpr int kWrapNDim = 1 + kSpatialNDim + kHasDynamicChannels; + using BatchWrapOutT + = std::conditional_t, + batch_wrapper::ImageBatchVarShapeWrapAdapter, + batch_wrapper::TensorBatchWrapAdapter>; + using BatchWrapInT + = std::conditional_t, + batch_wrapper::ImageBatchVarShapeWrapAdapter, + batch_wrapper::TensorBatchWrapAdapter>; + using DynamicBatchWrap = batch_wrapper::dynamic::DynamicBatchWrap; + static_assert(std::is_trivially_copyable_v); + static_assert(std::is_trivially_copyable_v); + static_assert(std::is_trivially_copyable_v); + // planarChannels > 1 only for the ImageBatchVarShape planar path; the adapter then decodes + // each expanded sample index to its {image, plane}. The TensorBatch adapter ignores it. + const BatchWrapOutT outWrap(dstData, planarChannels); + const BatchWrapInT inWrap(srcData, planarChannels); + if (numSamples > 0 + && TryRunDirectFilterBatch(sampleDescsCpu, sampleDescsGpu, outWrap, inWrap, numSamples, + stream)) + { + RecordReady(ws.pinnedMem.ready, stream); + RecordReady(ws.cudaMem.ready, stream); + return; + } + const DynamicBatchWrap intermediateWrap + = batch_wrapper::dynamic::CreateDynamicBatchWrap( + 0, intermediate[0], intermediateMeta[0], sampleDescsCpu, numSamples, stream); + RecordReady(ws.pinnedMem.ready, stream); + RunPass(sampleDescsCpu, sampleDescsGpu, intermediateWrap, inWrap, numSamples, stream); + RunPass(sampleDescsCpu, sampleDescsGpu, outWrap, intermediateWrap, numSamples, stream); + RecordReady(ws.cudaMem.ready, stream); + } + + template + std::enable_if_t RunPasses(const SampleDescT *sampleDescsCpu, const SampleDescT *sampleDescsGpu, + const nvcv::TensorBatchDataStridedCuda &dstData, + const nvcv::TensorBatchDataStridedCuda &srcData, + IntermediateBaseT *intermediate[kNumTmpBuffers], + const DynamicBatchWrapMeta intermediateMeta[kNumTmpBuffers], int numSamples, + const cvcuda::Workspace &ws, cudaStream_t stream) const + { + static_assert(kSpatialNDim == 3); + constexpr bool kHasDynamicChannels = kNumStaticChannels == -1; + // sample extent, spatial extents, optional dynamic channel extent + constexpr int kWrapNDim = 1 + kSpatialNDim + kHasDynamicChannels; + using TensorBatchWrapOutT = batch_wrapper::TensorBatchWrapAdapter; + using TensorBatchWrapInT = batch_wrapper::TensorBatchWrapAdapter; + using DynamicBatchWrap = batch_wrapper::dynamic::DynamicBatchWrap; + static_assert(std::is_trivially_copyable_v); + static_assert(std::is_trivially_copyable_v); + static_assert(std::is_trivially_copyable_v); + const TensorBatchWrapOutT outWrap(dstData); + const TensorBatchWrapInT inWrap(srcData); + const DynamicBatchWrap intermediateWrap0 + = batch_wrapper::dynamic::CreateDynamicBatchWrap( + 0, intermediate[0], intermediateMeta[0], sampleDescsCpu, numSamples, stream); + const DynamicBatchWrap intermediateWrap1 + = batch_wrapper::dynamic::CreateDynamicBatchWrap( + 1, intermediate[1], intermediateMeta[1], sampleDescsCpu, numSamples, stream); + RecordReady(ws.pinnedMem.ready, stream); + RunPass(sampleDescsCpu, sampleDescsGpu, intermediateWrap0, inWrap, numSamples, stream); + RunPass(sampleDescsCpu, sampleDescsGpu, intermediateWrap1, intermediateWrap0, numSamples, + stream); + RunPass(sampleDescsCpu, sampleDescsGpu, outWrap, intermediateWrap1, numSamples, stream); + RecordReady(ws.cudaMem.ready, stream); + } + + enum class DirectLinearKind + { + kScale2x2, + kGeneric + }; + + template + bool TryRunDirectLinear(const SampleDescT &sampleDesc, const PassOutWrap &outWrap, const PassInWrap &inWrap, + int numSamples, cudaStream_t stream) const + { + if constexpr (IsDirectLinearSupported() + && !(kKind == DirectLinearKind::kScale2x2 && kNumStaticChannels != 1)) + { + if (!IsDirectLinearNoRoi(sampleDesc)) + { + return false; + } + + // 3-channel u8 fuses only contractions: the magnification case loses on + // the reference SKUs, where the separable passes are bandwidth-fed. + if constexpr (kNumStaticChannels == 3 && std::is_same_v, unsigned char>) + { + if (HasMagnifyingAxis(sampleDesc)) + { + return false; + } + } + + const VecI<2> outShape = sampleDesc.shapes[2]; + if (outShape.x <= 0 || outShape.y <= 0) + { + return false; + } + + dim3 block(kBlockDim.x, kBlockDim.y, 1); + if constexpr (kKind == DirectLinearKind::kScale2x2) + { + VecF<2> origin{}; + VecF<2> scale{}; + resampling::DirectLinearOriginScale(sampleDesc, origin, scale); + + const VecI<2> inShape = sampleDesc.shapes[0]; + if (origin.x != 0.f || origin.y != 0.f || scale.x != 0.5f || scale.y != 0.5f + || outShape.x != 2 * inShape.x || outShape.y != 2 * inShape.y) + { + return false; + } + + constexpr int kLanes = resampling::kDirectLinear2x2Lanes, WrapBaseT>; + dim3 grid(utils::DivCeil(inShape.x, static_cast(block.x) * kLanes), + utils::DivCeil(inShape.y, static_cast(block.y)), numSamples); + resampling::DirectLinear2x2Kernel + <<>>(sampleDesc, outWrap, inWrap); + } + else + { + constexpr int kLanes = resampling::kDirectLinear2DLanes, + WrapBaseT>; + dim3 grid(utils::DivCeil(outShape.x, static_cast(block.x) * kLanes), + utils::DivCeil(outShape.y, static_cast(block.y)), numSamples); + resampling::DirectLinear2DKernel + <<>>(sampleDesc, outWrap, inWrap); + } + NVCV_CHECK_THROW(cudaGetLastError()); + return true; + } + return false; + } + + template + static constexpr bool IsDirectLinearSupported() + { + using OutBT = WrapBaseT; + using InBT = WrapBaseT; + return kSpatialNDim == 2 && (kNumStaticChannels == 1 || kNumStaticChannels == 3) + && ((std::is_same_v && std::is_same_v) + || (std::is_same_v && std::is_same_v)); + } + + static bool IsDirectLinearNoRoi(const SampleDescT &sampleDesc) + { + return sampleDesc.filterKind[0] == filter::FilterTypeKind::Linear + && sampleDesc.filterKind[1] == filter::FilterTypeKind::Linear && sampleDesc.inRoiOffset.x == 0 + && sampleDesc.inRoiOffset.y == 0; + } + + template + static constexpr bool IsDirectFilterSupported() + { + // Same type/channel set as the direct-linear kernels, except single-channel + // u8: the separable vectorized vertical pass keeps the reference-SKU edge over + // every fused u8 C1 variant, including the phased word-load kernel. + return IsDirectLinearSupported() + && !(kNumStaticChannels == 1 && std::is_same_v, unsigned char>); + } + + template + static constexpr bool IsDirectFilter2x2Supported() + { + // Single-channel only (its lane structure amortizes the byte loads). 3-channel + // stays separable: u8 C3 is L1-bound on the per-quad byte loads, and f32 C3 + // loses on the bandwidth-rich reference SKUs. + return kNumStaticChannels == 1 && IsDirectLinearSupported(); + } + + /** + * @brief Derives pass-ordered phase counts/steps for any rational resampling of a + * full-plane sample (source extent / gcd per pass axis), or returns false when a + * period exceeds the shared-table cap. + */ + static bool ComputePassPhases(const SampleDescT &sampleDesc, VecI<2> &phaseCount, VecI<2> &phaseStep) + { + if constexpr (kSpatialNDim != 2) + { + return false; + } + else + { + const VecI<2> inShape = sampleDesc.shapes[0]; + const VecI<2> outShape = sampleDesc.shapes[2]; + if (inShape.x <= 0 || inShape.y <= 0 || outShape.x <= 0 || outShape.y <= 0) + { + return false; + } + const int axis0 = sampleDesc.processingOrder.x; + const int in0 = axis0 == 0 ? inShape.x : inShape.y; + const int in1 = axis0 == 0 ? inShape.y : inShape.x; + const int out0 = axis0 == 0 ? outShape.x : outShape.y; + const int out1 = axis0 == 0 ? outShape.y : outShape.x; + // The integer-derived phases describe exactly the full-plane mapping; any + // ROI that shifts the origin or changes the scale must stay per-pixel. + if (sampleDesc.origin.x != 0.f || sampleDesc.origin.y != 0.f + || sampleDesc.scale.x != static_cast(in0) / static_cast(out0) + || sampleDesc.scale.y != static_cast(in1) / static_cast(out1)) + { + return false; + } + const int g0 = std::gcd(in0, out0); + const int g1 = std::gcd(in1, out1); + phaseCount = {out0 / g0, out1 / g1}; + phaseStep = {in0 / g0, in1 / g1}; + return phaseCount.x <= resampling::kMaxDirectFilterPhases + && phaseCount.y <= resampling::kMaxDirectFilterPhases; + } + } + + static int PhaseTableShmSize(const VecI<2> &phaseCount, const int support = resampling::kMaxDirectFilterSupport) + { + const int phases = phaseCount.x + phaseCount.y; + return phases * (support + 1) * sizeof(float) + phases * sizeof(int); + } + + template + bool TryRunDirectFilter2x2(const SampleDescT &sampleDesc, const PassOutWrap &outWrap, const PassInWrap &inWrap, + int numSamples, cudaStream_t stream) const + { + if constexpr (IsDirectFilter2x2Supported()) + { + if (!IsDirectFilterEligible(sampleDesc)) + { + return false; + } + // Exact-2x magnification with X resampled first: two phases per axis. + const VecI<2> inShape = sampleDesc.shapes[0]; + const VecI<2> outShape = sampleDesc.shapes[2]; + if (sampleDesc.processingOrder.x != 0 || sampleDesc.origin.x != 0.f || sampleDesc.origin.y != 0.f + || sampleDesc.scale.x != 0.5f || sampleDesc.scale.y != 0.5f || outShape.x != 2 * inShape.x + || outShape.y != 2 * inShape.y) + { + return false; + } + + constexpr int kLanes = resampling::kDirectFilter2x2Lanes, WrapBaseT>; + dim3 block(kBlockDim.x, kBlockDim.y, 1); + dim3 grid(utils::DivCeil(inShape.x, static_cast(block.x) * kLanes), + utils::DivCeil(inShape.y, static_cast(block.y)), numSamples); + resampling::DirectFilter2x2Kernel + <<>>(sampleDesc, outWrap, inWrap); + NVCV_CHECK_THROW(cudaGetLastError()); + return true; + } + return false; + } + + // Common gate of every fused support-based kernel: no ROI offset and support-4 + // table filters on both axes. + static bool IsDirectFilterEligible(const SampleDescT &sampleDesc) + { + return sampleDesc.inRoiOffset.x == 0 && sampleDesc.inRoiOffset.y == 0 + && sampleDesc.filterKind[0] == filter::FilterTypeKind::ShmFilter + && sampleDesc.filterKind[1] == filter::FilterTypeKind::ShmFilter + && sampleDesc.filter[0].support() == resampling::kMaxDirectFilterSupport + && sampleDesc.filter[1].support() == resampling::kMaxDirectFilterSupport; + } + + static bool IsCubicFilter(const SampleDescT &sampleDesc) + { + return IsDirectFilterEligible(sampleDesc) + && sampleDesc.filter[0].numCoeffs == filter::ResamplingFiltersFactory::kCubicSize + && sampleDesc.filter[1].numCoeffs == filter::ResamplingFiltersFactory::kCubicSize; + } + + bool ShouldUseDirectTensorPath(const SampleDescT &sampleDesc, bool isFloat, int numChannels) const + { + DirectTensorPathDesc pathDesc{}; + pathDesc.linear = IsDirectLinearNoRoi(sampleDesc); + pathDesc.cubic = IsCubicFilter(sampleDesc); + pathDesc.xFirst = sampleDesc.processingOrder.x == 0; + if (pathDesc.linear) + { + VecF<2> origin{}; + VecF<2> scale{}; + resampling::DirectLinearOriginScale(sampleDesc, origin, scale); + pathDesc.originX = origin.x; + pathDesc.originY = origin.y; + pathDesc.scaleX = scale.x; + pathDesc.scaleY = scale.y; + } + else + { + pathDesc.originX = sampleDesc.origin.x; + pathDesc.originY = sampleDesc.origin.y; + pathDesc.scaleX = sampleDesc.scale.x; + pathDesc.scaleY = sampleDesc.scale.y; + } + pathDesc.inWidth = sampleDesc.shapes[0].x; + pathDesc.inHeight = sampleDesc.shapes[0].y; + pathDesc.outWidth = sampleDesc.shapes[2].x; + pathDesc.outHeight = sampleDesc.shapes[2].y; + + return ShouldUseDirectTensorPathForSM(m_filtersFactory.GetDeviceComputeCapability(), pathDesc, isFloat, + numChannels); + } + + static bool HasMagnifyingAxis(const SampleDescT &sampleDesc) + { + return std::abs(sampleDesc.scale.x) < 1.f || std::abs(sampleDesc.scale.y) < 1.f; + } + + // Minimum per-sample output pixels for the u8 uniform-tensor Contract2x kernel + // (4k-class planes; smaller planes stay on the separable VertXVec path, which + // wins there on the bandwidth-rich reference SKUs). + static constexpr int64_t kDirectFilterContract2xMinU8OutPixels = 2000000; + + static bool IsDirectFilterContract2x(const SampleDescT &sampleDesc) + { + // Exact-2x contraction with Y resampled first: one coefficient phase per axis. + return IsDirectFilterEligible(sampleDesc) && sampleDesc.processingOrder.x == 1 && sampleDesc.scale.x == 2.0f + && sampleDesc.scale.y == 2.0f; + } + + template + static constexpr bool IsDirectFilterContract2xSupported() + { + // Single-channel only: multi-channel already amortizes the coefficient + // evaluations over channels in the generic fused kernel, and the phase-shared + // kernel measures slightly worse there. + return kSpatialNDim == 2 && kNumStaticChannels == 1 + && IsDirectLinearSupported(); + } + + template + bool TryRunDirectFilterContract2x(const SampleDescT &sampleDesc, const PassOutWrap &outWrap, + const PassInWrap &inWrap, int numSamples, cudaStream_t stream) const + { + if constexpr (IsDirectFilterContract2xSupported()) + { + if (!IsDirectFilterContract2x(sampleDesc)) + { + return false; + } + const VecI<2> outShape = sampleDesc.shapes[2]; + if (outShape.x <= 0 || outShape.y <= 0) + { + return false; + } + + using InBT = WrapBaseT; + // The issue-bound u8 kernel only beats the separable VertXVec path on + // large planes: 4k-class outputs win on every measured SKU, smaller ones + // lose on the reference SKUs. f32 wins at every measured size. + if constexpr (std::is_same_v) + { + if (static_cast(outShape.x) * outShape.y < kDirectFilterContract2xMinU8OutPixels) + { + return false; + } + } + constexpr int kLanes = resampling::kDirectFilterContract2xLanes; + dim3 block(kBlockDim.x, kBlockDim.y, 1); + dim3 grid(utils::DivCeil(outShape.x, static_cast(block.x) * kLanes), + utils::DivCeil(outShape.y, static_cast(block.y)), numSamples); + resampling::DirectFilterContract2xKernel + <<>>(sampleDesc, outWrap, inWrap); + NVCV_CHECK_THROW(cudaGetLastError()); + return true; + } + return false; + } + + template + bool TryRunDirectFilterBatch(const SampleDescT *sampleDescsCpu, const SampleDescT *sampleDescsGpu, + const BatchOutWrap &outWrap, const BatchInWrap &inWrap, int numSamples, + cudaStream_t stream) const + { + // The generic fused batch kernel is f32-only: the u8 batch rows lose on the + // reference SKUs. The Contract2x batch variant keeps u8, which wins on every + // measured SKU against the heavier batch separable baseline. + constexpr bool kGenericOk = IsDirectFilterSupported() + && std::is_same_v, float>; + constexpr bool kContract2xOk + = IsDirectFilterContract2xSupported(); + constexpr bool kPhasedOk = IsDirectFilterSupported(); + if constexpr (kSpatialNDim != 2 || !(kGenericOk || kContract2xOk || kPhasedOk)) + { + return false; + } + else + { + if (numSamples <= 0) + { + return false; + } + // The sample descriptors live in pinned staging memory, where host reads are + // uncached and slow enough to gap the GPU between iterations: reject on the + // first sample alone where possible, and stage the descriptors locally before + // the remaining per-sample checks. + const SampleDescT desc0 = sampleDescsCpu[0]; + if (!IsDirectFilterEligible(desc0) || HasMagnifyingAxis(desc0)) + { + return false; + } + VecI<2> phaseCount, phaseStep; + const bool phased0 + = ComputePassPhases(desc0, phaseCount, phaseStep) + && (std::is_same_v, + float> || (kNumStaticChannels == 3 && (phaseCount.x > 1 || phaseCount.y > 1))); + if constexpr (!kGenericOk) + { + if (!(kContract2xOk && IsDirectFilterContract2x(desc0)) && !(kPhasedOk && phased0)) + { + return false; + } + } + const std::vector stagedDescs(sampleDescsCpu, sampleDescsCpu + numSamples); + const SampleDescT *descs = stagedDescs.data(); + + // All samples must qualify; a mixed batch falls back to the separable passes. + bool allContract2x = true; + bool uniformShape = true; + VecI<2> maxOutShape{}; + for (int i = 0; i < numSamples; i++) + { + const SampleDescT &d = descs[i]; + if (!IsDirectFilterEligible(d) || HasMagnifyingAxis(d)) + { + return false; + } + allContract2x = allContract2x && IsDirectFilterContract2x(d); + uniformShape = uniformShape && d.shapes[0].x == descs[0].shapes[0].x + && d.shapes[0].y == descs[0].shapes[0].y && d.shapes[2].x == descs[0].shapes[2].x + && d.shapes[2].y == descs[0].shapes[2].y; + maxOutShape = cuda::max(maxOutShape, d.shapes[2]); + } + if (maxOutShape.x <= 0 || maxOutShape.y <= 0) + { + return false; + } + + dim3 block(kBlockDim.x, kBlockDim.y, 1); + if constexpr (kContract2xOk) + { + if (allContract2x) + { + constexpr int kLanes = resampling::kDirectFilterContract2xLanes>; + dim3 grid(utils::DivCeil(maxOutShape.x, static_cast(block.x) * kLanes), + utils::DivCeil(maxOutShape.y, static_cast(block.y)), numSamples); + resampling::DirectFilterContract2xBatchKernel + <<>>(sampleDescsGpu, outWrap, inWrap); + NVCV_CHECK_THROW(cudaGetLastError()); + return true; + } + } + if constexpr (kPhasedOk) + { + // Uniform-shape phased contraction: one shared coefficient table serves + // every sample. u8 fuses only 3-channel samples at non-integer scales; + // single-channel u8 (planar planes) keeps the separable path per the + // reference-SKU evidence. + bool allPhased = uniformShape && phased0; + for (int i = 1; allPhased && i < numSamples; i++) + { + VecI<2> samplePhaseCount, samplePhaseStep; + allPhased = ComputePassPhases(descs[i], samplePhaseCount, samplePhaseStep) + && samplePhaseCount.x == phaseCount.x && samplePhaseCount.y == phaseCount.y + && samplePhaseStep.x == phaseStep.x && samplePhaseStep.y == phaseStep.y; + } + if (allPhased) + { + dim3 grid(utils::DivCeil(maxOutShape.x, static_cast(block.x)), + utils::DivCeil(maxOutShape.y, static_cast(block.y)), numSamples); + resampling::DirectFilterPhasedBatch2DKernel + <<>>(sampleDescsGpu, phaseCount, phaseStep, + outWrap, inWrap); + NVCV_CHECK_THROW(cudaGetLastError()); + return true; + } + } + if constexpr (kGenericOk) + { + dim3 grid(utils::DivCeil(maxOutShape.x, static_cast(block.x)), + utils::DivCeil(maxOutShape.y, static_cast(block.y)), numSamples); + resampling::DirectFilterBatch2DKernel + <<>>(sampleDescsGpu, outWrap, inWrap); + NVCV_CHECK_THROW(cudaGetLastError()); + return true; + } + return false; + } + } + + template + bool TryRunDirectFilter(const SampleDescT &sampleDesc, const PassOutWrap &outWrap, const PassInWrap &inWrap, + int numSamples, cudaStream_t stream) const + { + if constexpr (IsDirectFilterSupported()) + { + // Contraction only: for magnifications the per-output-pixel coefficient + // recomputation outweighs the intermediate-traffic savings. + if (HasMagnifyingAxis(sampleDesc)) + { + return false; + } + const VecI<2> outShape = sampleDesc.shapes[2]; + if (outShape.x <= 0 || outShape.y <= 0) + { + return false; + } + + dim3 block(kBlockDim.x, kBlockDim.y, 1); + dim3 grid(utils::DivCeil(outShape.x, static_cast(block.x)), + utils::DivCeil(outShape.y, static_cast(block.y)), numSamples); + VecI<2> phaseCount, phaseStep; + const bool phased = ComputePassPhases(sampleDesc, phaseCount, phaseStep); + if (IsDirectFilterEligible(sampleDesc)) + { + // Support-4 filters: cubic and the isotropic-2x antialiased linear. The + // phased variant replaces the per-output coefficient evaluation with + // shared-table lookups whenever the scale is rational with small + // periods (identical math either way). + if (phased) + { + resampling::DirectFilterPhased2DKernel + <<>>(sampleDesc, phaseCount, phaseStep, + outWrap, inWrap); + } + else + { + resampling::DirectFilter2DKernel + <<>>(sampleDesc, outWrap, inWrap); + } + NVCV_CHECK_THROW(cudaGetLastError()); + return true; + } + return false; + } + return false; + } + + template + static constexpr bool IsVertXVecSupported() + { + using OutT = typename PassOutWrap::ValueType; + using InT = std::remove_const_t; + using OutBT = cuda::BaseType; + using InBT = cuda::BaseType; + return kSpatialNDim == 2 && kNumStaticChannels == 1 + && std::is_same_v && std::is_same_v; + } + + template + void RunPass(const SampleDescT &sampleDesc, const PassOutWrap &outWrap, const PassInWrap &inWrap, int numSamples, + cudaStream_t stream) const + { + using GridHelperT = resampling::GridHelper; + + VecI numBlocks; + { + VecI outputShape = sampleDesc.shapes[kWhichPass + 1]; + VecI blockShape = sampleDesc.blockShape[kWhichPass]; + if (utils::Volume(blockShape) == 0) + { + return; + } + numBlocks = utils::DivCeil(outputShape, blockShape); + if (utils::Volume(numBlocks) == 0) + { + return; + } + } + + GridHelperT gridHelper{numBlocks, numSamples}; + dim3 block(kBlockDim.x, kBlockDim.y, kBlockDim.z); + dim3 grid = gridHelper.GetKernelGrid(); + const auto devGridHelper = gridHelper.GetDeviceGridHelper(); + + int sharedMemSize = RequiredSharedMemorySize(sampleDesc, kWhichPass); + // Keep the vectorized vertical path in its own instantiation so other tensor passes + // retain their original register footprint. + if constexpr (IsVertXVecSupported()) + { + if (ShouldEnableVertXVec(sampleDesc)) + { + resampling::SeparableResamplingKernel + <<>>(sampleDesc, outWrap, inWrap, devGridHelper); + } + else + { + resampling::SeparableResamplingKernel + <<>>(sampleDesc, outWrap, inWrap, devGridHelper); + } + } + else + { + resampling::SeparableResamplingKernel + <<>>(sampleDesc, outWrap, inWrap, devGridHelper); + } + NVCV_CHECK_THROW(cudaGetLastError()); + } + + template + void RunPass(const SampleDescT *sampleDescsCpu, const SampleDescT *sampleDescsGpu, const PassOutWrap &outWrap, + const PassInWrap &inWrap, int numSamples, cudaStream_t stream) const + { + using GridHelperT = resampling::GridHelper; + + int maxSharedMemSize = 0; + VecI maxNumBlocks{}; + for (int sampleIdx = 0; sampleIdx < numSamples; sampleIdx++) + { + const SampleDescT &sampleDesc = sampleDescsCpu[sampleIdx]; + int sharedMemSize = RequiredSharedMemorySize(sampleDesc, kWhichPass); + maxSharedMemSize = std::max(maxSharedMemSize, sharedMemSize); + + VecI outputShape = sampleDesc.shapes[kWhichPass + 1]; + VecI blockShape = sampleDesc.blockShape[kWhichPass]; + if (utils::Volume(blockShape) == 0) + continue; + VecI numBlocks = utils::DivCeil(outputShape, blockShape); + maxNumBlocks = cuda::max(maxNumBlocks, numBlocks); + } + + if (utils::Volume(maxNumBlocks) == 0) + { + return; + } + + GridHelperT gridHelper{maxNumBlocks, numSamples}; + dim3 block(kBlockDim.x, kBlockDim.y, kBlockDim.z); + dim3 grid = gridHelper.GetKernelGrid(); + const auto devGridHelper = gridHelper.GetDeviceGridHelper(); + + resampling::SeparableResamplingKernel + <<>>(sampleDescsGpu, outWrap, inWrap, devGridHelper); + NVCV_CHECK_THROW(cudaGetLastError()); + } + + template + bool ShouldEnableVertXVec(const SampleDescT &sampleDesc) const + { + if constexpr (IsVertXVecSupported()) + { + using resampling::interpolate::kVertXVecMaxScale; + using resampling::interpolate::kVertXVecMaxSupport; + using resampling::interpolate::filter_support::CanComputeCoefPerThread; + int axis = cuda::GetElement(sampleDesc.processingOrder, kWhichPass); + if (axis == 1 && sampleDesc.filterKind[kWhichPass] == filter::FilterTypeKind::Linear) + { + float scale = std::abs(cuda::GetElement(sampleDesc.scale, kWhichPass)); + return scale >= 1.0f && scale <= kVertXVecMaxScale; + } + else if (axis == 1 && sampleDesc.filterKind[kWhichPass] == filter::FilterTypeKind::ShmFilter) + { + int support = sampleDesc.filter[kWhichPass].support(); + float scale = std::abs(cuda::GetElement(sampleDesc.scale, kWhichPass)); + return scale >= 1.0f && scale <= kVertXVecMaxScale && support <= kVertXVecMaxSupport + && CanComputeCoefPerThread(support, kBlockDim.y); + } + } + return false; + } + + int RequiredSharedMemorySize(const SampleDescT &sampleDesc, int whichPass) const + { + using resampling::interpolate::filter_support::RequiredSharedMemoryElements; + if (sampleDesc.filterKind[whichPass] != filter::FilterTypeKind::ShmFilter) + { + return 0; + } + int support = sampleDesc.filter[whichPass].support(); + int axis = cuda::GetElement(sampleDesc.processingOrder, whichPass); + // for depth resampling y is used as well + int resamplingAxisBlockSize = axis == 0 ? kBlockDim.x : kBlockDim.y; + return sizeof(IntermediateBaseT) * RequiredSharedMemoryElements(support, resamplingAxisBlockSize); + } + + void SetupSampleDesc(SampleDescT &sampleDesc, const VecI &srcShape, + const VecI &dstShape, int numChannels, const HQResizeRoiF *roi, + const filter::FilterMode &minFilter, const filter::FilterMode &magFilter) const + { + SetupSampleDescFilterShapeScale(sampleDesc, srcShape, dstShape, numChannels, minFilter, magFilter, roi); + SetupBlockLayout(sampleDesc); + } + + void SetupSampleDescFilterShapeScale(SampleDescT &sampleDesc, const VecI &inShape, + const VecI &outShape, int numChannels, + const filter::FilterMode &minFilter, const filter::FilterMode &magFilter, + const HQResizeRoiF *roi) const + { + // get user provided roi + const shape::Roi parsedRoi = ParseROI(roi, inShape); + // setup filter based on user provided filter types and the input/output size + filter::FilterTypeKind filterKinds[kSpatialNDim]; + filter::ResamplingFilter filters[kSpatialNDim]; + SetupFilters(filterKinds, filters, parsedRoi.Size(), outShape, minFilter, magFilter); + // get the ROI that is normalized (so that roiLo <= roiHi), adjusted for filter's "halo", + // and clampped to input shape + const shape::Roi adjustedRoi = AdjustRoiForFilter(parsedRoi, inShape, filters); + VecI adjustedRoiSize = adjustedRoi.Size(); + // the processing order is permutation that maps pass number to axis resampled during given pass + sampleDesc.processingOrder = SetupProcessingOrder(adjustedRoiSize, outShape, filters); + // now, use filters, roi and processingOrder to populate sample descriptor + sampleDesc.channels = numChannels; + sampleDesc.shapes[0] = inShape; + // set output shapes, scaling, roi, and relevant filters for each pass + // according to the best processingOrder of axes + { + VecI intermediateShape = adjustedRoiSize; + for (int pass = 0; pass < kSpatialNDim; pass++) + { + const int axis = cuda::GetElement(sampleDesc.processingOrder, pass); + const int axisOutShape = cuda::GetElement(outShape, axis); + const float roiStart = cuda::GetElement(parsedRoi.lo, axis); + const float roiEnd = cuda::GetElement(parsedRoi.hi, axis); + + cuda::GetElement(intermediateShape, axis) = axisOutShape; + sampleDesc.filterKind[pass] = filterKinds[axis]; + sampleDesc.filter[pass] = filters[axis]; + sampleDesc.shapes[pass + 1] = intermediateShape; + + cuda::GetElement(sampleDesc.origin, pass) = roiStart; + cuda::GetElement(sampleDesc.scale, pass) = (roiEnd - roiStart) / axisOutShape; + + // "Clamp" the axes processed in later passes to the input ROI + if (pass == 0) + { + // the first processed axis roi is handled simply with the `origin` + cuda::GetElement(sampleDesc.inRoiOffset, axis) = 0; + } + else + { + // for the axes not resampled in the first pass, we can just use offset when accesing data + // (adjustedRoi.lo) and pretend the input shape is the adjustedRoi.Size() + cuda::GetElement(sampleDesc.shapes[0], axis) = cuda::GetElement(adjustedRoiSize, axis); + cuda::GetElement(sampleDesc.inRoiOffset, axis) = cuda::GetElement(adjustedRoi.lo, axis); + cuda::GetElement(sampleDesc.origin, pass) + -= cuda::GetElement(adjustedRoi.lo, axis); // parsedRoi.lo - adjustedRoi.lo + } + } + } + } + + /** + * @brief If user specified the roi, it's returned with reversed dims oreder ((d)hw -> wh(d)), + * otherwise the input shape is used to create whole-plane roi. + * Note, that in the first case, some lo and hi may be flipped (i.e. lo[d] > hi[d]). + */ + shape::Roi ParseROI(const HQResizeRoiF *roi, VecI inShape) const + { + shape::Roi retRoi; + for (int dim = 0; dim < kSpatialNDim; dim++) + { + int axis = kSpatialNDim - 1 - dim; + auto axisSize = cuda::GetElement(inShape, axis); + float roiStart, roiEnd; + if (roi != nullptr) + { + roiStart = roi->lo[dim]; + roiEnd = roi->hi[dim]; + } + else + { + roiStart = 0; + roiEnd = axisSize; + } + cuda::GetElement(retRoi.lo, axis) = roiStart; + cuda::GetElement(retRoi.hi, axis) = roiEnd; + } + return retRoi; + } + + void SetupFilters(filter::FilterTypeKind filterKind[kSpatialNDim], filter::ResamplingFilter filters[kSpatialNDim], + VecF roiShape, const VecI &outShape, + const filter::FilterMode &minFilter, const filter::FilterMode &magFilter) const + { + using resampling::interpolate::filter_support::kMaxGPUFilterSupport; + static_assert(kSpatialNDim == 2 || kSpatialNDim == 3, + "Currently, the resampling operator supports only 2 or 3 spatial dimensions"); + + for (int axis = 0; axis < kSpatialNDim; axis++) + { + float inSize = std::abs(cuda::GetElement(roiShape, axis)); + float outSize = cuda::GetElement(outShape, axis); + const auto filterMode = outSize < inSize ? minFilter : magFilter; + filterKind[axis] = filter::GetFilterTypeKind(filterMode.filterType); + auto &filter = filters[axis]; + filter = filter::GetResamplingFilter(m_filtersFactory, filterMode, inSize, outSize); + + // for very small outputs, the required support may be too big for avialable shm + if (filter.support() > kMaxGPUFilterSupport) + { + filter.rescale(kMaxGPUFilterSupport); + } + } + } + + /** + * @brief Computes normalized ROI (i.e. so that roiLo <= roiHow), which is adjusted for filter's halo, + * converted to int and clamped to the input shape + */ + shape::Roi AdjustRoiForFilter(const shape::Roi &roi, + const VecI &inShape, + const filter::ResamplingFilter filters[kSpatialNDim]) const + { + shape::Roi ajustedRoi; + for (int axis = 0; axis < kSpatialNDim; axis++) + { + const float &axisLo = cuda::GetElement(roi.lo, axis); + const float &axisHi = cuda::GetElement(roi.hi, axis); + const auto &filter = filters[axis]; + int support = filter.numCoeffs ? filter.support() : 1; + float adjustedAxisLo, adjustedAxisHi; + if (axisLo <= axisHi) + { + adjustedAxisLo = axisLo - filter.anchor; + adjustedAxisHi = axisHi - filter.anchor + support; + } + else + { // flipped + adjustedAxisLo = axisHi - filter.anchor; + adjustedAxisHi = axisLo - filter.anchor + support; + } + const int axisSize = cuda::GetElement(inShape, axis); + cuda::GetElement(ajustedRoi.lo, axis) + = std::max(0, std::min(axisSize, std::floor(adjustedAxisLo))); + cuda::GetElement(ajustedRoi.hi, axis) + = std::max(0, std::min(axisSize, std::ceil(adjustedAxisHi))); + } + return ajustedRoi; + } + + VecI SetupProcessingOrder(const VecI &inRoiSize, const VecI &outSize, + const filter::ResamplingFilter filters[kSpatialNDim]) const + { + VecI filterSupport; + for (int i = 0; i < kSpatialNDim; i++) + { + int support = filters[i].support(); + // NN filter has support -1, so we need the max() below + cuda::GetElement(filterSupport, i) = std::max(1, support); + } + + return ProcessingOrderCalculator(inRoiSize, outSize, filterSupport)(); + } + + int64_t GetPassOutputVolume(SampleDescT sampleDesc, int pass) const + { + return utils::Volume(sampleDesc.shapes[pass + 1]) * sampleDesc.channels; + } + + /** + * @brief Calculates block layout for a 2D sample + * + */ + template + std::enable_if_t SetupBlockLayout(SampleDescT &sampleDesc) const + { + static_assert(kSpatialNDim == 2); + int lanes = resampling::GetResizeBlockLanes(); + for (int pass = 0; pass < kSpatialNDim; pass++) + { + SetupBlockLayoutPass(sampleDesc, pass, lanes); + } + } + + template + std::enable_if_t SetupBlockLayoutPass(SampleDescT &sampleDesc, int pass, int lanes) const + { + static_assert(kSpatialNDim == 2); + int resamplingAxis = cuda::GetElement(sampleDesc.processingOrder, pass); + // The threadblock is (kBlockDim.x, kBlockDim.y) for all passes. + // In horizontal pass (resamplingAxis == 0), a single block will + // process output slice of (kBlockDim.x, lanes * kBlockDim.y). + // In vertical pass (resamplingAxis == 1), each block will handle + // output slice of (kBlockDim.x * lanes, kBlockDim.y). + VecI<2> blockShape{kBlockDim.x, kBlockDim.y}; + cuda::GetElement(blockShape, 1 - resamplingAxis) *= lanes; + auto outputShape = sampleDesc.shapes[pass + 1]; + sampleDesc.blockShape[pass] = cuda::clamp(blockShape, VecI<2>{1, 1}, outputShape); + } + + template + std::enable_if_t SetupTensorBlockLayout(SampleDescT &sampleDesc) const + { + static_assert(kSpatialNDim == 2); + using InBT = cuda::BaseType; + using OutBT = cuda::BaseType; + constexpr bool kSingleChannelU8 + = kNumStaticChannels == 1 && std::is_same_v && std::is_same_v; + constexpr bool kSingleChannelF32 + = kNumStaticChannels == 1 && std::is_same_v && std::is_same_v; + if constexpr (kSingleChannelU8 || kSingleChannelF32) + { + if (resampling::GetResizeBlockLanes() == 8) + { + bool isCubicSupport4 = true; + for (int pass = 0; pass < kSpatialNDim; pass++) + { + const auto &flt = sampleDesc.filter[pass]; + const bool passIsCubicSupport4 = sampleDesc.filterKind[pass] == filter::FilterTypeKind::ShmFilter + && flt.numCoeffs == filter::ResamplingFiltersFactory::kCubicSize + && flt.support() == 4; + isCubicSupport4 = isCubicSupport4 && passIsCubicSupport4; + } + + if (isCubicSupport4) + { + bool isCubicMagnification = true; + for (int pass = 0; pass < kSpatialNDim; pass++) + { + const float scale = std::abs(cuda::GetElement(sampleDesc.scale, pass)); + isCubicMagnification = isCubicMagnification && scale < 1.0f; + } + + int lanes = 8; + if constexpr (kSingleChannelU8) + { + lanes = isCubicMagnification ? 64 : 8; + } + else if constexpr (kSingleChannelF32) + { + lanes = isCubicMagnification ? resampling::GetF32CubicMagnificationBlockLanes( + m_filtersFactory.GetDeviceComputeCapability()) + : 8; + } + + if (lanes != 8) + { + for (int pass = 0; pass < kSpatialNDim; pass++) + { + SetupBlockLayoutPass(sampleDesc, pass, lanes); + } + } + } + } + } + } + + /** + * @brief Calculates block layout for a 3D sample + */ + template + std::enable_if_t SetupBlockLayout(SampleDescT &sampleDesc) const + { + static_assert(kSpatialNDim == 3); + int lanes = resampling::GetResizeBlockLanes(); + for (int pass = 0; pass < kSpatialNDim; pass++) + { + auto outputShape = sampleDesc.shapes[pass + 1]; + int resamplingAxis = cuda::GetElement(sampleDesc.processingOrder, pass); + if (resamplingAxis < 2) + { + VecI<3> blockShape{kBlockDim.x, kBlockDim.y, kBlockDim.z * lanes}; + sampleDesc.blockShape[pass] = cuda::clamp(blockShape, VecI<3>{1, 1, 1}, outputShape); + } + else + { + assert(resamplingAxis == 2); + VecI<3> blockShape{kBlockDim.x, kBlockDim.z * lanes, kBlockDim.y}; + sampleDesc.blockShape[pass] = cuda::clamp(blockShape, VecI<3>{1, 1, 1}, outputShape); + } + } + } + + const filter::ResamplingFiltersFactory &m_filtersFactory; +}; +} // namespace cvcuda::priv::hq_resize::kernel + +#endif // CVCUDA_PRIV_HQ_RESIZE_KERNEL_CUH diff --git a/src/cvcuda/priv/OpHQResizePlanar.cuh b/src/cvcuda/priv/OpHQResizePlanar.cuh new file mode 100644 index 000000000..a8e429e7d --- /dev/null +++ b/src/cvcuda/priv/OpHQResizePlanar.cuh @@ -0,0 +1,72 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#ifndef CVCUDA_PRIV_OP_HQ_RESIZE_PLANAR_CUH +#define CVCUDA_PRIV_OP_HQ_RESIZE_PLANAR_CUH + +#include +#include +#include + +// Shared helpers for planar (NCHW/CHW) HQResize support. HQResize resizes spatial axes only and +// treats channels independently, so a C-plane planar sample is equivalent to C single-channel +// samples. The tensor/tensor-batch paths reinterpret each plane as a single-channel image +// (PlanarAsSingleChannelView); the var-shape path decodes the flattened plane index in-adapter +// (DecodePlane). Both feed the existing single-channel kernels, so the result is bit-identical to +// the interleaved path and no new kernels are needed. +namespace cvcuda::priv::hq_resize::planar { + +// Map an expanded sample index `nc` (running over images * channels, plane-minor) to its +// {image, plane} pair. Used by the var-shape adapter when it decodes grid.y / CurrentSample(). +__host__ __device__ __forceinline__ int2 DecodePlane(int nc, int channels) +{ + return int2{nc / channels, nc % channels}; +} + +// Build an (N*C, H, W, 1) NHWC view of a packed planar (NCHW/CHW) tensor so the interleaved +// single-channel resize path can process each channel plane as an independent image. Requires the +// channel planes to be tightly packed for batched tensors (sampleStride == numChannels * chStride); +// a single sample may legitimately have a larger sampleStride due to allocation alignment. +inline nvcv::TensorDataStridedCuda PlanarAsSingleChannelView(const nvcv::TensorDataStridedCuda &data, + const nvcv::TensorDataAccessStridedImagePlanar &access) +{ + const int64_t numSamples = access.numSamples(); + const int64_t numChannels = access.numChannels(); + const int64_t numRows = access.numRows(); + const int64_t numCols = access.numCols(); + + if (numSamples > 1 && access.sampleStride() != numChannels * access.chStride()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Planar HQResize of a batched tensor requires tightly packed channel planes"); + } + + nvcv::TensorDataStridedCuda::Buffer buf; + buf.basePtr = reinterpret_cast(data.basePtr()); + buf.strides[0] = access.chStride(); // N*C flattened planes + buf.strides[1] = access.rowStride(); // H + buf.strides[2] = access.colStride(); // W + buf.strides[3] = access.colStride(); // C == 1 + return nvcv::TensorDataStridedCuda{ + nvcv::TensorShape{{numSamples * numChannels, numRows, numCols, 1}, "NHWC"}, + data.dtype(), buf + }; +} + +} // namespace cvcuda::priv::hq_resize::planar + +#endif // CVCUDA_PRIV_OP_HQ_RESIZE_PLANAR_CUH diff --git a/src/cvcuda/priv/OpHQResizePolicy.hpp b/src/cvcuda/priv/OpHQResizePolicy.hpp new file mode 100644 index 000000000..0d202ff98 --- /dev/null +++ b/src/cvcuda/priv/OpHQResizePolicy.hpp @@ -0,0 +1,117 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#ifndef CVCUDA_PRIV_OP_HQ_RESIZE_POLICY_HPP +#define CVCUDA_PRIV_OP_HQ_RESIZE_POLICY_HPP + +namespace cvcuda::priv::hq_resize { + +enum class DirectTensorPath +{ + Other, + LinearExpand2x, + CubicContract2x, + CubicExpand2x, +}; + +struct DirectTensorPathDesc +{ + bool linear; + bool cubic; + bool xFirst; + float originX; + float originY; + float scaleX; + float scaleY; + int inWidth; + int inHeight; + int outWidth; + int outHeight; +}; + +constexpr DirectTensorPath ClassifyDirectTensorPath(const DirectTensorPathDesc &desc) +{ + const bool zeroOrigin = desc.originX == 0.f && desc.originY == 0.f; + const bool expand2x = desc.scaleX == 0.5f && desc.scaleY == 0.5f && desc.outWidth == 2 * desc.inWidth + && desc.outHeight == 2 * desc.inHeight; + const bool contract2x = desc.scaleX == 2.f && desc.scaleY == 2.f && desc.inWidth == 2 * desc.outWidth + && desc.inHeight == 2 * desc.outHeight; + + if (desc.linear && zeroOrigin && expand2x) + { + return DirectTensorPath::LinearExpand2x; + } + if (desc.cubic && !desc.xFirst && zeroOrigin && contract2x) + { + return DirectTensorPath::CubicContract2x; + } + if (desc.cubic && desc.xFirst && zeroOrigin && expand2x) + { + return DirectTensorPath::CubicExpand2x; + } + return DirectTensorPath::Other; +} + +constexpr bool UseDirectTensorPathForSM(int sm, DirectTensorPath path, bool isFloat, int numChannels) +{ + if (sm == 75) + { + return path != DirectTensorPath::CubicContract2x || !isFloat || (numChannels != 1 && numChannels != 3); + } + + if (sm != 100 && sm != 103) + { + return true; + } + + switch (path) + { + case DirectTensorPath::Other: + return true; + case DirectTensorPath::LinearExpand2x: + return !(isFloat && numChannels == 3); + case DirectTensorPath::CubicContract2x: + return numChannels != 3; + case DirectTensorPath::CubicExpand2x: + return sm != 100 || !(isFloat && numChannels == 1); + } + return true; +} + +constexpr bool ShouldUseDirectTensorPathForSM(int sm, const DirectTensorPathDesc &desc, bool isFloat, int numChannels) +{ + return UseDirectTensorPathForSM(sm, ClassifyDirectTensorPath(desc), isFloat, numChannels); +} + +static_assert(!UseDirectTensorPathForSM(100, DirectTensorPath::LinearExpand2x, true, 3)); +static_assert(!UseDirectTensorPathForSM(100, DirectTensorPath::CubicContract2x, true, 3)); +static_assert(!UseDirectTensorPathForSM(100, DirectTensorPath::CubicContract2x, false, 3)); +static_assert(!UseDirectTensorPathForSM(100, DirectTensorPath::CubicExpand2x, true, 1)); +static_assert(!UseDirectTensorPathForSM(103, DirectTensorPath::LinearExpand2x, true, 3)); +static_assert(!UseDirectTensorPathForSM(103, DirectTensorPath::CubicContract2x, true, 3)); +static_assert(!UseDirectTensorPathForSM(103, DirectTensorPath::CubicContract2x, false, 3)); +static_assert(UseDirectTensorPathForSM(103, DirectTensorPath::CubicExpand2x, true, 1)); +static_assert(!UseDirectTensorPathForSM(75, DirectTensorPath::CubicContract2x, true, 1)); +static_assert(!UseDirectTensorPathForSM(75, DirectTensorPath::CubicContract2x, true, 3)); +static_assert(UseDirectTensorPathForSM(75, DirectTensorPath::CubicContract2x, false, 1)); +static_assert(UseDirectTensorPathForSM(75, DirectTensorPath::CubicContract2x, true, 4)); +static_assert(UseDirectTensorPathForSM(75, DirectTensorPath::LinearExpand2x, true, 3)); +static_assert(UseDirectTensorPathForSM(80, DirectTensorPath::CubicContract2x, true, 1)); + +} // namespace cvcuda::priv::hq_resize + +#endif // CVCUDA_PRIV_OP_HQ_RESIZE_POLICY_HPP diff --git a/src/cvcuda/priv/OpHistogram.cpp b/src/cvcuda/priv/OpHistogram.cpp index d601c122b..02e1626a6 100644 --- a/src/cvcuda/priv/OpHistogram.cpp +++ b/src/cvcuda/priv/OpHistogram.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,24 +17,249 @@ #include "OpHistogram.hpp" +#include "Nvtx.hpp" +#include "OpReformat.hpp" #include "legacy/CvCudaLegacy.h" #include "legacy/CvCudaLegacyHelpers.hpp" +#include #include +#include #include +#include + namespace cvcuda::priv { namespace legacy = nvcv::legacy::cuda_op; +HistogramPlanarBridgeWorkspace::HistogramPlanarBridgeWorkspace() +{ + NVCV_CHECK_THROW(cudaEventCreateWithFlags(&ready, cudaEventDisableTiming)); +} + +HistogramPlanarBridgeWorkspace::~HistogramPlanarBridgeWorkspace() +{ + if (ready != nullptr) + { + if (busy) + { + NVCV_CHECK_LOG(cudaEventSynchronize(ready)); + } + NVCV_CHECK_LOG(cudaEventDestroy(ready)); + } +} + +void HistogramPlanarBridgeWorkspace::ensure(const nvcv::TensorShape &inShape, nvcv::DataType inDtype, + const nvcv::TensorShape *maskShape, nvcv::DataType maskDtype, + cudaStream_t stream) +{ + const bool reallocateIn = !interleavedIn || interleavedIn.shape() != inShape || interleavedIn.dtype() != inDtype; + const bool reallocateMask + = maskShape != nullptr + && (!interleavedMask || interleavedMask.shape() != *maskShape || interleavedMask.dtype() != maskDtype); + + if (busy) + { + if (reallocateIn || reallocateMask) + { + NVCV_CHECK_THROW(cudaEventSynchronize(ready)); + busy = false; + } + else + { + NVCV_CHECK_THROW(cudaStreamWaitEvent(stream, ready)); + } + } + + if (reallocateIn) + { + interleavedIn = nvcv::Tensor(inShape, inDtype); + } + if (reallocateMask) + { + interleavedMask = nvcv::Tensor(*maskShape, maskDtype); + } +} + +void HistogramPlanarBridgeWorkspace::record(cudaStream_t stream) +{ + NVCV_CHECK_THROW(cudaEventRecord(ready, stream)); + busy = true; +} + +namespace { + +bool IsHistogramPlanarLayout(nvcv::TensorLayout layout) +{ + return layout == nvcv::TENSOR_NCHW || layout == nvcv::TENSOR_CHW; +} + +bool IsHistogramInterleavedLayout(nvcv::TensorLayout layout) +{ + return layout == nvcv::TENSOR_NHWC || layout == nvcv::TENSOR_HWC; +} + +nvcv::TensorShape MakeHistogramLegacyInputShape(const nvcv::TensorDataStridedCuda &data, + const nvcv::TensorDataAccessStridedImagePlanar &access) +{ + std::array shape{}; + const char *layout = nullptr; + int32_t rank = 0; + + const bool hasBatch = data.layout() == nvcv::TENSOR_NCHW; + if (hasBatch) + { + shape[rank] = access.numSamples(); + ++rank; + } + + if (hasBatch || data.layout() == nvcv::TENSOR_CHW) + { + shape[rank] = access.numRows(); + ++rank; + shape[rank] = access.numCols(); + ++rank; + shape[rank] = access.numChannels(); + ++rank; + layout = hasBatch ? "NHWC" : "HWC"; + return nvcv::TensorShape(shape.data(), rank, layout); + } + + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input/mask must have NCHW or CHW layout for planar Histogram adaptation"); +} + +void InferPlanarHistogramNoMask(legacy::Histogram &legacyOp, cudaStream_t stream, + const nvcv::TensorDataStridedCuda &interleavedInData, + const nvcv::TensorDataStridedCuda &outHistogram) +{ + NVCV_CHECK_THROW( + legacyOp.infer(interleavedInData, nvcv::OptionalTensorConstRef{nvcv::NullOpt}, outHistogram, stream)); +} + +void InferPlanarHistogramWithMask(legacy::Histogram &legacyOp, Reformat &reformatOp, + HistogramPlanarBridgeWorkspace &workspace, cudaStream_t stream, + const nvcv::Tensor &mask, const nvcv::TensorDataStridedCuda &inData, + const nvcv::TensorDataStridedCuda &interleavedInData, + const nvcv::TensorDataStridedCuda &outHistogram) +{ + auto maskData = mask.exportData(); + if (maskData == nullptr) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Mask must be cuda-accessible, pitch-linear tensor"); + } + if (!IsHistogramPlanarLayout(maskData->layout()) || maskData->layout() != inData.layout()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Mask layout must match planar Histogram input layout"); + } + + if (auto maskAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*maskData); !maskAccess) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Mask must be an image-like planar tensor"); + } + + reformatOp(stream, mask, workspace.interleavedMask); + + NVCV_CHECK_THROW(legacyOp.infer(interleavedInData, nvcv::OptionalTensorConstRef{workspace.interleavedMask}, + outHistogram, stream)); +} + +void InferPlanarHistogram(legacy::Histogram &legacyOp, Reformat &reformatOp, HistogramPlanarBridgeWorkspace &workspace, + cudaStream_t stream, const nvcv::Tensor &in, nvcv::OptionalTensorConstRef mask, + const nvcv::TensorDataStridedCuda &inData, const nvcv::TensorDataStridedCuda &outHistogram) +{ + auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(inData); + if (!inAccess) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input must be an image-like planar tensor"); + } + + nvcv::TensorShape interleavedInShape = MakeHistogramLegacyInputShape(inData, *inAccess); + + nvcv::TensorShape maskShape; + nvcv::DataType maskDtype = nvcv::TYPE_U8; + if (mask) + { + auto maskData = mask->get().exportData(); + if (maskData == nullptr) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Mask must be cuda-accessible, pitch-linear tensor"); + } + auto maskAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*maskData); + if (!maskAccess) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Mask must be an image-like planar tensor"); + } + maskShape = MakeHistogramLegacyInputShape(*maskData, *maskAccess); + maskDtype = maskData->dtype(); + } + + std::lock_guard lock(workspace.mutex); + workspace.ensure(interleavedInShape, inData.dtype(), mask ? &maskShape : nullptr, maskDtype, stream); + + reformatOp(stream, in, workspace.interleavedIn); + + auto interleavedInData = workspace.interleavedIn.exportData(); + if (interleavedInData == nullptr) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Internal Histogram input tensor must be cuda-accessible"); + } + + if (!mask) + { + InferPlanarHistogramNoMask(legacyOp, stream, *interleavedInData, outHistogram); + workspace.record(stream); + return; + } + + InferPlanarHistogramWithMask(legacyOp, reformatOp, workspace, stream, mask->get(), inData, *interleavedInData, + outHistogram); + workspace.record(stream); +} + +void ValidateInterleavedMask(nvcv::OptionalTensorConstRef mask, nvcv::TensorLayout inputLayout) +{ + if (!mask) + { + return; + } + + auto maskData = mask->get().exportData(); + if (maskData == nullptr) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Mask must be cuda-accessible, pitch-linear tensor"); + } + if (!IsHistogramInterleavedLayout(maskData->layout()) || maskData->layout() != inputLayout) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Mask layout must match Histogram input layout"); + } +} + +} // namespace + +std::unique_ptr Histogram::CreatePlanarBridgeWorkspace(int) +{ + return std::make_unique(); +} + Histogram::Histogram() { - m_legacyOp = std::make_unique(); + m_legacyOp = std::make_unique(); + m_reformatOp = std::make_unique(); } +Histogram::~Histogram() = default; + void Histogram::operator()(cudaStream_t stream, const nvcv::Tensor &in, nvcv::OptionalTensorConstRef mask, const nvcv::Tensor &histogram) const { + CVCUDA_NVTX_RANGE("cvcuda::Histogram::operator()[Tensor]"); auto inData = in.exportData(); if (inData == nullptr) { @@ -49,6 +274,21 @@ void Histogram::operator()(cudaStream_t stream, const nvcv::Tensor &in, nvcv::Op "Output must be cuda-accessible, pitch-linear tensor"); } + if (const bool inPlanar = IsHistogramPlanarLayout(inData->layout()); inPlanar) + { + InferPlanarHistogram(*m_legacyOp, *m_reformatOp, m_planarWorkspace.get(), stream, in, mask, *inData, + *outHistogram); + return; + } + + if (!IsHistogramInterleavedLayout(inData->layout())) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input layout must be NHWC/HWC or single-channel NCHW/CHW"); + } + + ValidateInterleavedMask(mask, inData->layout()); + NVCV_CHECK_THROW(m_legacyOp->infer(*inData, mask, *outHistogram, stream)); } diff --git a/src/cvcuda/priv/OpHistogram.hpp b/src/cvcuda/priv/OpHistogram.hpp index 3f29de540..8fa7a66d8 100644 --- a/src/cvcuda/priv/OpHistogram.hpp +++ b/src/cvcuda/priv/OpHistogram.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,24 +25,50 @@ #define CVCUDA_PRIV__HISTOGRAM_HPP #include "IOperator.hpp" +#include "PerDeviceResource.hpp" #include "legacy/CvCudaLegacy.h" +#include #include #include +#include namespace cvcuda::priv { +class Reformat; + +struct HistogramPlanarBridgeWorkspace +{ + std::mutex mutex; + nvcv::Tensor interleavedIn; + nvcv::Tensor interleavedMask; + cudaEvent_t ready = nullptr; + bool busy = false; + + HistogramPlanarBridgeWorkspace(); + ~HistogramPlanarBridgeWorkspace(); + + void ensure(const nvcv::TensorShape &inShape, nvcv::DataType inDtype, const nvcv::TensorShape *maskShape, + nvcv::DataType maskDtype, cudaStream_t stream); + void record(cudaStream_t stream); +}; + class Histogram final : public IOperator { public: explicit Histogram(); + ~Histogram() override; void operator()(cudaStream_t stream, const nvcv::Tensor &in, nvcv::OptionalTensorConstRef mask, const nvcv::Tensor &histogram) const; private: - std::unique_ptr m_legacyOp; + static std::unique_ptr CreatePlanarBridgeWorkspace(int); + + std::unique_ptr m_legacyOp; + std::unique_ptr m_reformatOp; + mutable PerDeviceResource m_planarWorkspace{CreatePlanarBridgeWorkspace}; }; } // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpHistogramEq.cpp b/src/cvcuda/priv/OpHistogramEq.cpp index 1ccf1bb83..f51262ad1 100644 --- a/src/cvcuda/priv/OpHistogramEq.cpp +++ b/src/cvcuda/priv/OpHistogramEq.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,10 +17,12 @@ #include "OpHistogramEq.hpp" +#include "Nvtx.hpp" #include "legacy/CvCudaLegacy.h" #include "legacy/CvCudaLegacyHelpers.hpp" #include +#include #include namespace cvcuda::priv { @@ -28,18 +30,21 @@ namespace cvcuda::priv { namespace legacy = nvcv::legacy::cuda_op; HistogramEq::HistogramEq(uint32_t maxBatchSize) + // Legacy operators are single-device by design. PerDeviceResource creates + // one instance per CUDA device for transparent multi-GPU support. + : m_maxBatchSize(maxBatchSize) + , m_legacyOp([maxBatchSize](int) { return std::make_unique(maxBatchSize); }) + , m_legacyOpVarShape([maxBatchSize](int) { return std::make_unique(maxBatchSize); }) { if (maxBatchSize == 0) { throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "HistogramEq: maxBatchSize must be >= 1"); } - - m_legacyOp = std::make_unique(maxBatchSize); - m_legacyOpVarShape = std::make_unique(maxBatchSize); } void HistogramEq::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out) const { + CVCUDA_NVTX_RANGE("cvcuda::HistogramEq::operator()[Tensor]"); auto inData = in.exportData(); if (inData == nullptr) { @@ -54,12 +59,29 @@ void HistogramEq::operator()(cudaStream_t stream, const nvcv::Tensor &in, const "Output must be cuda-accessible, pitch-linear tensor"); } - NVCV_CHECK_THROW(m_legacyOp->infer(*inData, *outData, stream)); + auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*inData); + if (!inAccess) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input must be image-compatible tensor"); + } + + if (inAccess->numSamples() > m_maxBatchSize) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input batch exceeds maxBatchSize"); + } + + NVCV_CHECK_THROW(m_legacyOp.get().infer(*inData, *outData, stream)); } void HistogramEq::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out) const { + CVCUDA_NVTX_RANGE("cvcuda::HistogramEq::operator()[ImageBatchVarShape]"); + if (in.numImages() > m_maxBatchSize) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input batch exceeds maxBatchSize"); + } + auto inData = in.exportData(stream); if (inData == nullptr) { @@ -72,7 +94,7 @@ void HistogramEq::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Output must be varshape image batch"); } - NVCV_CHECK_THROW(m_legacyOpVarShape->infer(*inData, *outData, stream)); + NVCV_CHECK_THROW(m_legacyOpVarShape.get().infer(*inData, *outData, stream)); } } // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpHistogramEq.hpp b/src/cvcuda/priv/OpHistogramEq.hpp index 046216293..747663c18 100644 --- a/src/cvcuda/priv/OpHistogramEq.hpp +++ b/src/cvcuda/priv/OpHistogramEq.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,12 +25,11 @@ #define CVCUDA_PRIV__HISTOGRAM_EQ_HPP #include "IOperator.hpp" +#include "PerDeviceResource.hpp" #include "legacy/CvCudaLegacy.h" #include -#include - namespace cvcuda::priv { class HistogramEq final : public IOperator @@ -43,8 +42,9 @@ class HistogramEq final : public IOperator void operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out) const; private: - std::unique_ptr m_legacyOp; - std::unique_ptr m_legacyOpVarShape; + int32_t m_maxBatchSize; + mutable PerDeviceResource m_legacyOp; + mutable PerDeviceResource m_legacyOpVarShape; }; } // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpInpaint.cpp b/src/cvcuda/priv/OpInpaint.cpp index a9e1105e5..131111fd7 100644 --- a/src/cvcuda/priv/OpInpaint.cpp +++ b/src/cvcuda/priv/OpInpaint.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,10 +17,12 @@ #include "OpInpaint.hpp" +#include "Nvtx.hpp" #include "legacy/CvCudaLegacy.h" #include "legacy/CvCudaLegacyHelpers.hpp" #include +#include #include namespace cvcuda::priv { @@ -28,16 +30,31 @@ namespace cvcuda::priv { namespace legacy = nvcv::legacy::cuda_op; Inpaint::Inpaint(int maxBatchSize, nvcv::Size2D maxShape) + // Legacy operators are single-device by design. PerDeviceResource creates + // one instance per CUDA device for transparent multi-GPU support. + : m_maxBatchSize(maxBatchSize) + , m_maxShape(maxShape) + , m_legacyOp( + [maxBatchSize, maxShape](int) + { + legacy::DataShape maxIn; + legacy::DataShape maxOut; + return std::make_unique(maxIn, maxOut, maxBatchSize, maxShape); + }) + , m_legacyOpVarShape( + [maxBatchSize, maxShape](int) + { + legacy::DataShape maxIn; + legacy::DataShape maxOut; + return std::make_unique(maxIn, maxOut, maxBatchSize, maxShape); + }) { - legacy::DataShape maxIn, maxOut; - // maxIn/maxOut not used by op. - m_legacyOp = std::make_unique(maxIn, maxOut, maxBatchSize, maxShape); - m_legacyOpVarShape = std::make_unique(maxIn, maxOut, maxBatchSize, maxShape); } void Inpaint::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &masks, const nvcv::Tensor &out, double inpaintRadius) const { + CVCUDA_NVTX_RANGE("cvcuda::Inpaint::operator()[Tensor]"); auto inData = in.exportData(); if (inData == nullptr) { @@ -59,19 +76,49 @@ void Inpaint::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv "Output must be cuda-accessible, pitch-linear tensor"); } - NVCV_CHECK_THROW(m_legacyOp->infer(*inData, *masksData, *outData, inpaintRadius, stream)); + auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*inData); + if (!inAccess) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input must be image-compatible tensor"); + } + + if (inAccess->numSamples() > m_maxBatchSize) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input batch exceeds maxBatchSize"); + } + if (inAccess->numCols() > m_maxShape.w || inAccess->numRows() > m_maxShape.h) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input shape exceeds maxShape"); + } + + NVCV_CHECK_THROW(m_legacyOp.get().infer(*inData, *masksData, *outData, inpaintRadius, stream)); } void Inpaint::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &masks, const nvcv::ImageBatchVarShape &out, double inpaintRadius) const { + CVCUDA_NVTX_RANGE("cvcuda::Inpaint::operator()[ImageBatchVarShape]"); + if (in.numImages() > m_maxBatchSize) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input batch exceeds maxBatchSize"); + } + if (in.numImages() != masks.numImages() || in.numImages() != out.numImages()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input, masks, and output must have the same number of images"); + } + if (nvcv::Size2D inMaxSize = in.maxSize(); inMaxSize.w > m_maxShape.w || inMaxSize.h > m_maxShape.h) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input shape exceeds maxShape"); + } + auto masksData = masks.exportData(stream); if (masksData == nullptr) { throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Masks must be varshape image batch"); } - NVCV_CHECK_THROW(m_legacyOpVarShape->infer(in, *masksData, out, inpaintRadius, stream)); + NVCV_CHECK_THROW(m_legacyOpVarShape.get().infer(in, *masksData, out, inpaintRadius, stream)); } } // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpInpaint.hpp b/src/cvcuda/priv/OpInpaint.hpp index d758afaa8..f1bcb7e17 100644 --- a/src/cvcuda/priv/OpInpaint.hpp +++ b/src/cvcuda/priv/OpInpaint.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,13 +25,12 @@ #define CVCUDA_PRIV_INPAINT_HPP #include "IOperator.hpp" +#include "PerDeviceResource.hpp" #include "legacy/CvCudaLegacy.h" #include #include -#include - namespace cvcuda::priv { class Inpaint final : public IOperator @@ -45,8 +44,10 @@ class Inpaint final : public IOperator const nvcv::ImageBatchVarShape &out, double inpaintRadius) const; private: - std::unique_ptr m_legacyOp; - std::unique_ptr m_legacyOpVarShape; + int32_t m_maxBatchSize; + nvcv::Size2D m_maxShape; + mutable PerDeviceResource m_legacyOp; + mutable PerDeviceResource m_legacyOpVarShape; }; } // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpInvert.cu b/src/cvcuda/priv/OpInvert.cu new file mode 100644 index 000000000..93e5179ce --- /dev/null +++ b/src/cvcuda/priv/OpInvert.cu @@ -0,0 +1,951 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "InvertPolicy.hpp" +#include "Nvtx.hpp" +#include "OpInvert.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace cuda = nvcv::cuda; +namespace util = nvcv::util; + +namespace { + +inline bool UsePackedU8C3VarShapeKernelOnCurrentDevice() +{ + int device = 0; + NVCV_CHECK_THROW(cudaGetDevice(&device)); + + static thread_local int cachedDevice = -1; + static thread_local bool cachedUsePacked = true; + if (cachedDevice != device) + { + cudaDeviceProp properties{}; + NVCV_CHECK_THROW(cudaGetDeviceProperties(&properties, device)); + + const int sm = properties.major * 10 + properties.minor; + cachedUsePacked = cvcuda::priv::UsePackedU8C3VarShapeKernelForDevice(sm, properties.name); + cachedDevice = device; + } + + return cachedUsePacked; +} + +// Photometric-negative bound per base type: dtype max for unsigned integers, 1.0 for float. +// Matches torchvision.transforms.v2.functional.invert / OpenCV cv::bitwise_not (unsigned). +template +inline __host__ __device__ BT InvertBound() +{ + if constexpr (std::is_floating_point_v) + { + return BT(1); + } + else + { + return cuda::TypeTraits::max; + } +} + +template +inline __device__ std::conditional_t GetCoordForLayout(int3 nhwCoord, int p) +{ + if constexpr (!IsPlanar) + { + return nhwCoord; + } + else + { + return {nhwCoord.x, nhwCoord.y, p, nhwCoord.z}; + } +} + +template +inline __device__ void DoInvert(SrcWrapper src, DstWrapper dst, const int2 size, const int p) +{ + using SrcT = std::remove_const_t; + using DstT = typename DstWrapper::ValueType; + using BT = cuda::BaseType; + static constexpr int numChannels = cuda::NumElements; + static_assert(numChannels == cuda::NumElements); + // if planar then no interleaved channels + static_assert(!IsPlanar || numChannels == 1); + + int3 nhwCoord = cuda::StaticCast(blockIdx * blockDim + threadIdx); + if (nhwCoord.x >= size.x || nhwCoord.y >= size.y) + { + return; + } + auto coord = GetCoordForLayout(nhwCoord, p); + + // out = bound - in, per element. The subtraction is exact for the supported types, so the + // result is bit-exact with the equivalent interleaved computation (Golden rule / COV-BITEXACT). + dst[coord] = cuda::SaturateCast(InvertBound() - src[coord]); +} + +// Invert kernel -------------------------------------------------------------------------- + +// Tensor variant +template +__global__ void Invert(SrcWrapper src, DstWrapper dst, int2 size, int numPlanes) +{ + assert(isPlanar || numPlanes == 1); + if constexpr (!isPlanar) + { + DoInvert(src, dst, size, 0); + } + else + { + for (int p = 0; p < numPlanes; p++) + { + DoInvert(src, dst, size, p); + } + } +} + +// VarShape variant +template +__global__ void Invert(SrcWrapper src, DstWrapper dst, int numPlanes) +{ + assert(isPlanar || numPlanes == 1); + int z = blockIdx.z; + int2 size{dst.width(z), dst.height(z)}; + + if constexpr (!isPlanar) + { + DoInvert(src, dst, size, 0); + } + else + { + for (int p = 0; p < numPlanes; p++) + { + DoInvert(src, dst, size, p); + } + } +} + +// Vectorized planar kernel ----------------------------------------------------------------- +// +// The scalar planar kernel above moves one element per thread per plane, so the 1-byte planar paths +// are memory-latency bound (NCU: ~82% long-scoreboard stalls, ~58-70% BWUtil) — each warp has only a +// single outstanding load. This kernel maps each (sample, plane) to grid.z (no per-plane loop) and +// has each thread own NGROUP independent 4-column groups, issuing all NGROUP wide vector loads +// (uchar4 / float4) before any compute so many memory requests are outstanding at once. Output is +// bit-identical to DoInvert per element (same SaturateCast(bound - in)). Modeled on +// legacy/normalize_planar.cuh. Caller guards sizeof(Vec4)-aligned strides and falls back to the +// scalar kernel otherwise; a width not a multiple of 4 is handled by the per-thread scalar tail. +template +struct InvertVec4Type; + +template +struct InvertVec4Type +{ + using type = uchar4; +}; + +template +struct InvertVec4Type +{ + using type = ushort4; +}; + +template +struct InvertVec4Type +{ + using type = float4; +}; + +template +__global__ void InvertPlanarVec4Kernel(cuda::Tensor4DWrap src, cuda::Tensor4DWrap dst, + int4 inout_size) +{ + const int g0 = blockIdx.x * blockDim.x * NGROUP + threadIdx.x; + const int src_y = blockIdx.y * blockDim.y + threadIdx.y; + const int nc = blockIdx.z; + const int batch = nc / inout_size.y; + const int channel = nc % inout_size.y; + const int width = inout_size.w; + + if (g0 * 4 >= width || src_y >= inout_size.z) + { + return; + } + + using Vec4 = typename InvertVec4Type::type; + const BT bound = InvertBound(); + + // Loads first: issue all NGROUP wide loads before any compute (raises outstanding requests). + int cx[NGROUP]; + bool full[NGROUP]; + Vec4 in4[NGROUP]; +#pragma unroll + for (int i = 0; i < NGROUP; ++i) + { + cx[i] = (g0 + i * blockDim.x) * 4; + full[i] = cx[i] + 4 <= width; + if (full[i]) + { + in4[i] = *reinterpret_cast(src.ptr(batch, channel, src_y, cx[i])); + } + } + +#pragma unroll + for (int i = 0; i < NGROUP; ++i) + { + if (full[i]) + { + Vec4 out4; + out4.x = cuda::SaturateCast(bound - in4[i].x); + out4.y = cuda::SaturateCast(bound - in4[i].y); + out4.z = cuda::SaturateCast(bound - in4[i].z); + out4.w = cuda::SaturateCast(bound - in4[i].w); + *reinterpret_cast(dst.ptr(batch, channel, src_y, cx[i])) = out4; + } + else if (cx[i] < width) + { + for (int x = cx[i]; x < width; ++x) + { + *dst.ptr(batch, channel, src_y, x) = cuda::SaturateCast(bound - *src.ptr(batch, channel, src_y, x)); + } + } + } +} + +// Keep the existing planar kernel's cross-group instruction-level parallelism. Collapsing this path +// into the register-bounded U16 kernel below regresses the reference A100/H100 planar RGB8 workload. +template +__global__ void InvertPlanarVarShapeVec4Kernel(cuda::ImageBatchVarShapeWrap src, + cuda::ImageBatchVarShapeWrap dst, int numChannels) +{ + const int g0 = blockIdx.x * blockDim.x * NGROUP + threadIdx.x; + const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int nc = blockIdx.z; + const int batch = nc / numChannels; + const int channel = nc % numChannels; + const int width = dst.width(batch, channel); + + if (g0 * 4 >= width || dst_y >= dst.height(batch, channel)) + { + return; + } + + using Vec4 = uchar4; // 1-byte planes only (caller guards sizeof(BT) == 1) + const BT bound = InvertBound(); + + int cx[NGROUP]; + bool full[NGROUP]; + Vec4 in4[NGROUP]; +#pragma unroll + for (int i = 0; i < NGROUP; ++i) + { + cx[i] = (g0 + i * blockDim.x) * 4; + full[i] = cx[i] + 4 <= width; + if (full[i]) + { + in4[i] = *reinterpret_cast(src.ptr(batch, channel, dst_y, cx[i])); + } + } + +#pragma unroll + for (int i = 0; i < NGROUP; ++i) + { + if (full[i]) + { + Vec4 out4; + out4.x = cuda::SaturateCast(bound - in4[i].x); + out4.y = cuda::SaturateCast(bound - in4[i].y); + out4.z = cuda::SaturateCast(bound - in4[i].z); + out4.w = cuda::SaturateCast(bound - in4[i].w); + *reinterpret_cast(dst.ptr(batch, channel, dst_y, cx[i])) = out4; + } + else if (cx[i] < width) + { + for (int x = cx[i]; x < width; ++x) + { + *dst.ptr(batch, channel, dst_y, x) = cuda::SaturateCast(bound - *src.ptr(batch, channel, dst_y, x)); + } + } + } +} + +// Vectorized U16 var-shape kernel: resolve the row pointers once per thread so each group does not +// repeat the image-list lookup. Rows without Vec4 alignment and partial groups use the scalar fallback. +template +__global__ void InvertU16VarShapeVec4Kernel(cuda::ImageBatchVarShapeWrap src, + cuda::ImageBatchVarShapeWrap dst, int numChannels) +{ + const int g0 = blockIdx.x * blockDim.x * NGROUP + threadIdx.x; + const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int nc = blockIdx.z; + const int batch = nc / numChannels; + const int channel = nc % numChannels; + const int width = dst.width(batch, channel); + + if (g0 * 4 >= width || dst_y >= dst.height(batch, channel)) + { + return; + } + + using Vec4 = typename InvertVec4Type::type; + const BT bound = InvertBound(); + const BT *srow = src.ptr(batch, channel, dst_y, 0); + BT *drow = dst.ptr(batch, channel, dst_y, 0); + const bool wide = reinterpret_cast(srow) % alignof(Vec4) == 0 + && reinterpret_cast(drow) % alignof(Vec4) == 0; + + // Keep one vector live at a time. Unrolling all groups raises the active register footprint + // above the optimization campaign's automatic memory allowance without improving coalescing. +#pragma unroll 1 + for (int i = 0; i < NGROUP; ++i) + { + const int cx = (g0 + i * blockDim.x) * 4; + if (wide && cx + 4 <= width) + { + const Vec4 in4 = *reinterpret_cast(srow + cx); + Vec4 out4; + out4.x = cuda::SaturateCast(bound - in4.x); + out4.y = cuda::SaturateCast(bound - in4.y); + out4.z = cuda::SaturateCast(bound - in4.z); + out4.w = cuda::SaturateCast(bound - in4.w); + *reinterpret_cast(drow + cx) = out4; + } + else if (cx < width) + { + for (int x = cx; x < min(cx + 4, width); ++x) + { + drow[x] = cuda::SaturateCast(bound - srow[x]); + } + } + } +} + +// Four RGB8 pixels occupy exactly three aligned 32-bit words. Packing at that boundary avoids the +// scalar uchar3 path's excessive sectors while preserving byte-exact 255-v arithmetic. +template +__global__ void InvertU8C3VarShapePackedKernel(cuda::ImageBatchVarShapeWrap src, + cuda::ImageBatchVarShapeWrap dst) +{ + const int g0 = blockIdx.x * blockDim.x * NGROUP + threadIdx.x; + const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch = blockIdx.z; + const int width = dst.width(batch); + + if (g0 * 4 >= width || dst_y >= dst.height(batch)) + { + return; + } + + const uchar3 *srow = src.ptr(batch, dst_y, 0); + uchar3 *drow = dst.ptr(batch, dst_y, 0); + const bool wide = reinterpret_cast(srow) % alignof(uint32_t) == 0 + && reinterpret_cast(drow) % alignof(uint32_t) == 0; + + // Bound live packed words to one pixel group so register residency stays proportional to one + // transaction rather than the compile-time batching factor. +#pragma unroll 1 + for (int i = 0; i < NGROUP; ++i) + { + const int cx = (g0 + i * blockDim.x) * 4; + if (wide && cx + 4 <= width) + { + const uint32_t *packed = reinterpret_cast(srow + cx); + uint32_t *out = reinterpret_cast(drow + cx); + out[0] = ~packed[0]; + out[1] = ~packed[1]; + out[2] = ~packed[2]; + } + else if (cx < width) + { + for (int x = cx; x < min(cx + 4, width); ++x) + { + const uchar3 value = srow[x]; + drow[x] = uchar3{static_cast(255 - value.x), static_cast(255 - value.y), + static_cast(255 - value.z)}; + } + } + } +} + +// Dense flat kernel ------------------------------------------------------------------------- +// +// A fully dense U8 tensor (packed channels/columns/rows/planes/samples in both src and dst) is one +// contiguous byte range, so layout and channel count stop mattering: the scalar interleaved +// kernels' narrow 3-4 byte per-thread accesses (measured 65% BWUtil on uchar3 NHWC Tensor at +// locked clocks) collapse to flat 8-byte words of bitwise NOT, since 255 - v == ~v per unsigned +// lane — bit-identical to DoInvert. One word per thread won a measured geometry sweep on the +// uchar3 NHWC Tensor row (uint4 x4/thread 86.8% BWUtil, uint4 x1 87.5%, uint2 x1 87.7%, +// uint32 x1 84.9%): maximizing resident threads beats per-thread batching for pure streaming. +// The sub-word tail is finished by global thread 0. U16 and F32 dense tensors stay on the per-element kernels: +// they were measured at the 87.8-88.6% BWUtil ridge already, and this flat shape benched 1-2% +// slower there (beyond noise), so the dense path is gated to 1-byte base types. + +__global__ void InvertDenseNotKernel(const uint2 *__restrict__ src, uint2 *__restrict__ dst, int64_t numWords, + const unsigned char *srcTail, unsigned char *dstTail, int tailBytes) +{ + const int64_t i = static_cast(blockIdx.x) * blockDim.x + threadIdx.x; + if (i < numWords) + { + const uint2 v = src[i]; + dst[i] = uint2{~v.x, ~v.y}; + } + if (i == 0) + { + for (int t = 0; t < tailBytes; ++t) + { + dstTail[t] = static_cast(~srcTail[t]); + } + } +} + +// Returns the tensor's contiguous byte count when every axis is packed (the whole tensor is one +// byte block), or -1 when it is padded. Channel/column packing is implied by comparing rowStride +// against the packed row size computed from ValueT (interleaved) or its base type (planar +// single-channel rows). Rank-3 HWC/CHW tensors report sampleStride() == 0; their single sample is +// dense whenever rows/planes are packed. +template +inline int64_t DenseTensorBytes(const nvcv::TensorDataAccessStridedImagePlanar &access) +{ + using BT = cuda::BaseType; + const int64_t packedRowBytes = access.numCols() * (isPlanar ? sizeof(BT) : sizeof(ValueT)); + if (access.rowStride() != packedRowBytes) + { + return -1; + } + const int64_t planeBytes = access.numRows() * packedRowBytes; + int64_t sampleBytes; + if constexpr (isPlanar) + { + if (access.planeStride() != planeBytes) + { + return -1; + } + sampleBytes = access.numPlanes() * planeBytes; + } + else + { + sampleBytes = planeBytes; + } + if (access.sampleStride() != sampleBytes && !(access.sampleStride() == 0 && access.numSamples() == 1)) + { + return -1; + } + return access.numSamples() * sampleBytes; +} + +template +inline bool TryRunInvertDense(cudaStream_t stream, const nvcv::TensorDataStridedCuda &srcData, + const nvcv::TensorDataStridedCuda &dstData, + const nvcv::TensorDataAccessStridedImagePlanar &srcAccess, + const nvcv::TensorDataAccessStridedImagePlanar &dstAccess) +{ + using BT = cuda::BaseType; + + // Measured dtype gate: only 1-byte base types beat their per-element kernels here (see the + // dense-kernel comment above). + if constexpr (sizeof(BT) != 1) + { + return false; + } + else + { + constexpr int kWordBytes = sizeof(uint2); + const int64_t totalBytes = DenseTensorBytes(srcAccess); + if (reinterpret_cast(srcData.basePtr()) % kWordBytes != 0 + || reinterpret_cast(dstData.basePtr()) % kWordBytes != 0 || totalBytes < 0 + || DenseTensorBytes(dstAccess) != totalBytes) + { + return false; + } + const int64_t numWords = totalBytes / kWordBytes; + const int tailBytes = static_cast(totalBytes - numWords * kWordBytes); + + constexpr int kBlock = 512; + const int64_t threads = std::max(numWords, 1); + dim3 grid(static_cast(util::DivUp(threads, static_cast(kBlock)))); + + const auto *src = reinterpret_cast(srcData.basePtr()); + auto *dst = reinterpret_cast(dstData.basePtr()); + InvertDenseNotKernel<<>>( + src, dst, numWords, reinterpret_cast(srcData.basePtr()) + numWords * kWordBytes, + reinterpret_cast(dstData.basePtr()) + numWords * kWordBytes, tailBytes); + NVCV_CHECK_THROW(cudaGetLastError()); + return true; + } +} + +// Run Invert kernel ---------------------------------------------------------------------- + +template +inline void RunInvert(cudaStream_t stream, const SrcData &srcData, const DstData &dstData) +{ + dim3 block(32, 4, 1); + if constexpr (std::is_same_v) + { + auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(srcData); + auto dstAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(dstData); + int2 size = cuda::StaticCast(long2{srcAccess->numCols(), srcAccess->numRows()}); + + // Each sample maps to one grid.z block (planar channels are looped inside the kernel, so + // grid.z is the sample count, not N*C); CUDA caps grid.z at 65535. + if (srcAccess->numSamples() > 65535) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Batch size exceeds the CUDA grid.z limit of 65535"); + } + dim3 grid(util::DivUp(size.x, block.x), util::DivUp(size.y, block.y), srcAccess->numSamples()); + + int64_t inMaxStride = srcAccess->sampleStride() * srcAccess->numSamples(); + int64_t outMaxStride = dstAccess->sampleStride() * dstAccess->numSamples(); + if (std::max(inMaxStride, outMaxStride) > cuda::TypeTraits::max) + { + throw nvcv::Exception(nvcv::Status::ERROR_OVERFLOW, "Input or output size exceeds %d. Tensor is too large.", + cuda::TypeTraits::max); + } + using StrideType = int32_t; + + // Fully dense src+dst collapse to one contiguous byte range; the flat 16-byte-word kernels + // beat every per-element path (see the dense-kernel block above). Falls through to the + // layout-specific kernels for padded or externally-wrapped strides. + if (TryRunInvertDense(stream, srcData, dstData, *srcAccess, *dstAccess)) + { + return; + } + + if constexpr (!isPlanar) + { + using BT = cuda::BaseType; + bool launchedVec = false; + + // 1-channel interleaved (e.g. U16) is byte-identical to a single plane and moves only + // sizeof(BT) bytes/thread, so it is latency-bound (NCU: ~18 cyc long-scoreboard, ~77% BW) + // like the planar paths. Reuse the vectorized planar kernel (C == 1). The multi-channel + // interleaved cases (uchar3/uchar4/float3/float4) already move 3-4 bytes/thread and sit at + // the bandwidth ridge, so they keep the scalar kernel untouched. + if constexpr (cuda::NumElements == 1) + { + using Vec4 = typename InvertVec4Type::type; + constexpr int NGROUP = 4; + const int64_t sStride = srcAccess->sampleStride(), rStride = srcAccess->rowStride(); + const int64_t dsStride = dstAccess->sampleStride(), drStride = dstAccess->rowStride(); + const bool aligned = reinterpret_cast(srcData.basePtr()) % sizeof(Vec4) == 0 + && reinterpret_cast(dstData.basePtr()) % sizeof(Vec4) == 0 + && sStride % sizeof(Vec4) == 0 && rStride % sizeof(Vec4) == 0 + && dsStride % sizeof(Vec4) == 0 && drStride % sizeof(Vec4) == 0; + if (aligned) + { + // C == 1, so planeStride is never indexed (channel is always 0); pass sampleStride. + auto srcV = cuda::Tensor4DWrap(srcData.basePtr(), static_cast(sStride), + static_cast(sStride), + static_cast(rStride)); + auto dstV + = cuda::Tensor4DWrap(dstData.basePtr(), static_cast(dsStride), + static_cast(dsStride), static_cast(drStride)); + dim3 vgrid(util::DivUp(util::DivUp(size.x, 4), static_cast(block.x) * NGROUP), + util::DivUp(size.y, block.y), srcAccess->numSamples()); + InvertPlanarVec4Kernel<<>>( + srcV, dstV, int4{static_cast(srcAccess->numSamples()), 1, size.y, size.x}); + launchedVec = true; + } + } + + if (!launchedVec) + { + auto src = cuda::CreateTensorWrapNHW(srcData); + auto dst = cuda::CreateTensorWrapNHW(dstData); + Invert<<>>(src, dst, size, 1); + } + } + else + { + using BT = cuda::BaseType; + const int numPlanes = srcAccess->numPlanes(); + const int numSamples = static_cast(srcAccess->numSamples()); + bool launchedVec = false; + + // Vectorized planar path for 1-byte / 4-byte planes: map each (sample, plane) to grid.z and + // move 4 columns/thread with a wide load (batched NGROUP loads -> high memory-level + // parallelism), eliminating the scalar planar kernel's long-scoreboard stalls. Requires + // sizeof(Vec4)-aligned base + strides; falls back to the scalar kernel otherwise (the + // kernel's per-thread tail handles a width that is not a multiple of 4). + if constexpr (sizeof(BT) == 1 || sizeof(BT) == 4) + { + using Vec4 = typename InvertVec4Type::type; + constexpr int NGROUP = sizeof(BT) == 1 ? 4 : 2; + const int64_t planes = static_cast(numSamples) * numPlanes; + const int64_t sStride = srcAccess->sampleStride(), pStride = srcAccess->planeStride(), + rStride = srcAccess->rowStride(); + const int64_t dsStride = dstAccess->sampleStride(), dpStride = dstAccess->planeStride(), + drStride = dstAccess->rowStride(); + const bool aligned + = planes <= 65535 && reinterpret_cast(srcData.basePtr()) % sizeof(Vec4) == 0 + && reinterpret_cast(dstData.basePtr()) % sizeof(Vec4) == 0 && sStride % sizeof(Vec4) == 0 + && pStride % sizeof(Vec4) == 0 && rStride % sizeof(Vec4) == 0 && dsStride % sizeof(Vec4) == 0 + && dpStride % sizeof(Vec4) == 0 && drStride % sizeof(Vec4) == 0; + if (aligned) + { + auto srcV = cuda::Tensor4DWrap(srcData.basePtr(), static_cast(sStride), + static_cast(pStride), + static_cast(rStride)); + auto dstV + = cuda::Tensor4DWrap(dstData.basePtr(), static_cast(dsStride), + static_cast(dpStride), static_cast(drStride)); + dim3 vgrid(util::DivUp(util::DivUp(size.x, 4), static_cast(block.x) * NGROUP), + util::DivUp(size.y, block.y), static_cast(planes)); + InvertPlanarVec4Kernel + <<>>(srcV, dstV, int4{numSamples, numPlanes, size.y, size.x}); + launchedVec = true; + } + } + + if (!launchedVec) + { + auto src = cuda::Tensor4DWrap( + srcData.basePtr(), static_cast(srcAccess->sampleStride()), + static_cast(srcAccess->planeStride()), static_cast(srcAccess->rowStride())); + auto dst = cuda::Tensor4DWrap( + dstData.basePtr(), static_cast(dstAccess->sampleStride()), + static_cast(dstAccess->planeStride()), static_cast(dstAccess->rowStride())); + Invert<<>>(src, dst, size, numPlanes); + } + } + NVCV_CHECK_THROW(cudaGetLastError()); + } + else + { + static_assert(std::is_same_v); + // One grid.z block per image; CUDA caps grid.z at 65535. + if (dstData.numImages() > 65535) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Batch size exceeds the CUDA grid.z limit of 65535"); + } + int3 dstMaxSize{dstData.maxSize().w, dstData.maxSize().h, dstData.numImages()}; + dim3 grid(util::DivUp(dstMaxSize.x, block.x), util::DivUp(dstMaxSize.y, block.y), dstMaxSize.z); + + const int numPlanes = dstData.uniqueFormat().numPlanes(); + + if constexpr (!isPlanar && std::is_same_v) + { + if (UsePackedU8C3VarShapeKernelOnCurrentDevice()) + { + constexpr int NGROUP = 4; + dim3 vgrid(util::DivUp(util::DivUp(dstMaxSize.x, 4), static_cast(block.x) * NGROUP), + util::DivUp(dstMaxSize.y, block.y), dstMaxSize.z); + cuda::ImageBatchVarShapeWrap srcV(srcData); + cuda::ImageBatchVarShapeWrap dstV(dstData); + InvertU8C3VarShapePackedKernel<<>>(srcV, dstV); + NVCV_CHECK_THROW(cudaGetLastError()); + return; + } + } + + // Preserve the established 1-byte planar kernel; it needs cross-group ILP to stay at ridge. + using BT = cuda::BaseType; + if constexpr (isPlanar && sizeof(BT) == 1) + { + constexpr int NGROUP = 4; + const int64_t planes = static_cast(dstData.numImages()) * numPlanes; + if (planes <= 65535) + { + cuda::ImageBatchVarShapeWrap srcV(srcData); + cuda::ImageBatchVarShapeWrap dstV(dstData); + dim3 vgrid(util::DivUp(util::DivUp(dstMaxSize.x, 4), static_cast(block.x) * NGROUP), + util::DivUp(dstMaxSize.y, block.y), static_cast(planes)); + InvertPlanarVarShapeVec4Kernel<<>>(srcV, dstV, numPlanes); + NVCV_CHECK_THROW(cudaGetLastError()); + return; + } + } + + // The U16 interleaved path keeps one vector group live at a time to cap register residency. + if constexpr (!isPlanar && cuda::NumElements == 1 && sizeof(BT) == 2) + { + constexpr int NGROUP = 4; + cuda::ImageBatchVarShapeWrap srcV(srcData); + cuda::ImageBatchVarShapeWrap dstV(dstData); + dim3 vgrid(util::DivUp(util::DivUp(dstMaxSize.x, 4), static_cast(block.x) * NGROUP), + util::DivUp(dstMaxSize.y, block.y), dstMaxSize.z); + InvertU16VarShapeVec4Kernel<<>>(srcV, dstV, 1); + NVCV_CHECK_THROW(cudaGetLastError()); + return; + } + + cuda::ImageBatchVarShapeWrap src(srcData); + cuda::ImageBatchVarShapeWrap dst(dstData); + Invert<<>>(src, dst, numPlanes); + NVCV_CHECK_THROW(cudaGetLastError()); + } +} + +// Dispatch over base data type (u8 / u16 / f32) and channel count (1 / 3 / 4) ------------- + +template +inline void RunTypeSwitch(nvcv::DataType dType, const Cb &cb) +{ + using uchar = unsigned char; + using ushort = unsigned short; + +#define NVCV_INVERT_RUN_TYPED(DYN_BASE_TYPE, STATIC_BASE_TYPE) \ + ((dType == nvcv::TYPE_4##DYN_BASE_TYPE) || (dType == nvcv::TYPE_3##DYN_BASE_TYPE) \ + || (dType == nvcv::TYPE_2##DYN_BASE_TYPE) || (dType == nvcv::TYPE_##DYN_BASE_TYPE)) cb(STATIC_BASE_TYPE{}); + + // clang-format off + if NVCV_INVERT_RUN_TYPED(U8, uchar) + else if NVCV_INVERT_RUN_TYPED(U16, ushort) + else if NVCV_INVERT_RUN_TYPED(F32, float) + else + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Invalid data type: Invert supports 8-bit unsigned, 16-bit unsigned and 32-bit float"); + } + // clang-format on + +#undef NVCV_INVERT_RUN_TYPED +} + +template +inline void RunChannelSwitch(int numChannels, int numPlanes, nvcv::DataType dType, const Cb &cb) +{ + RunTypeSwitch(dType, + [&numChannels, &numPlanes, &cb](auto dummyVal) + { + using ValBase = decltype(dummyVal); + // clang-format off + if (numChannels == 1) + { + using Val = cuda::MakeType; + if (numPlanes == 1) + { + cb(Val{}, std::integral_constant{}); + } + else + { + cb(Val{}, std::integral_constant{}); + } + } + else if (numChannels == 3) + { + cb(cuda::MakeType{}, std::integral_constant{}); + } + else if (numChannels == 4) + { + cb(cuda::MakeType{}, std::integral_constant{}); + } + else + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Invalid number of channels: Invert supports 1, 3 or 4 channels"); + } + // clang-format on + }); +} + +// Validation ------------------------------------------------------------------------------ + +inline void ValidateSrcDstTensors(int &numInterleavedChannels, int &numPlanes, nvcv::DataType &dtype, + const nvcv::Optional &srcData, + const nvcv::Optional &dstData) +{ + if (!srcData) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input must be cuda-accessible, pitch-linear tensor"); + } + if (!dstData) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Output must be cuda-accessible, pitch-linear tensor"); + } + if (srcData->layout() != dstData->layout()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input and output must have the same layout"); + } + if (!(srcData->layout() == nvcv::TENSOR_HWC || srcData->layout() == nvcv::TENSOR_NHWC + || srcData->layout() == nvcv::TENSOR_CHW || srcData->layout() == nvcv::TENSOR_NCHW)) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input must have (N)HWC or (N)CHW layout"); + } + if (srcData->dtype() != dstData->dtype()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input and output must have the same data type"); + } + + auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcData); + auto dstAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*dstData); + NVCV_ASSERT(srcAccess && dstAccess); + + if (srcAccess->numSamples() != dstAccess->numSamples()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of samples"); + } + + int numChannels = srcAccess->numChannels(); + if (numChannels != dstAccess->numChannels()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of channels"); + } + + numPlanes = srcAccess->numPlanes(); + if (numPlanes != dstAccess->numPlanes()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of planes"); + } + if (numPlanes > 1 && numChannels == 2) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "2-channel planar images are not supported"); + } + + if (srcAccess->numCols() != dstAccess->numCols() || srcAccess->numRows() != dstAccess->numRows()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input and output must have matching width and height"); + } + + dtype = srcData->dtype(); + numInterleavedChannels = srcAccess->infoLayout().isChannelLast() ? numChannels : 1; +} + +inline auto ValidateSrcDstVarBatch(int &numInterleavedChannels, int &numPlanes, nvcv::DataType &dtype, + cudaStream_t stream, const nvcv::ImageBatchVarShape &src, + const nvcv::ImageBatchVarShape &dst) +{ + using maybeVarShape = nvcv::Optional; + std::tuple srcDstData{ + src.exportData(stream), + dst.exportData(stream)}; + auto &[srcData, dstData] = srcDstData; + + if (!srcData) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input must be cuda-accessible, varshape pitch-linear image batch"); + } + if (!dstData) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Output must be cuda-accessible, varshape pitch-linear image batch"); + } + + int numSamples = srcData->numImages(); + if (numSamples != dstData->numImages()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of samples"); + } + + const auto &srcFormat = srcData->uniqueFormat(); + const auto &dstFormat = dstData->uniqueFormat(); + if (!srcFormat || !dstFormat) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "All images in a batch must have the same format"); + } + if (srcFormat != dstFormat) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input and output must have the same format"); + } + + int numChannels = srcFormat.numChannels(); + numPlanes = srcFormat.numPlanes(); + if (numPlanes > 1 && numChannels == 2) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "2-channel planar images are not supported"); + } + + dtype = srcFormat.planeDataType(0); + for (int i = 1; i < numPlanes; ++i) + { + if (dtype != srcFormat.planeDataType(i)) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "All planes in the input image must have the same data type"); + } + } + + numInterleavedChannels = dtype.numChannels(); + + for (int i = 0; i < numSamples; i++) + { + if (src[i].size() != dst[i].size()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input and output must have matching width and height"); + } + } + + return srcDstData; +} + +} // anonymous namespace + +namespace cvcuda::priv { + +Invert::Invert() {} + +// Tensor input variant +void Invert::operator()(cudaStream_t stream, const nvcv::Tensor &src, const nvcv::Tensor &dst) const +{ + CVCUDA_NVTX_RANGE("cvcuda::Invert::operator()[Tensor]"); + int numInterleavedChannels; + int numPlanes; + nvcv::DataType dtype; + auto srcData = src.exportData(); + auto dstData = dst.exportData(); + ValidateSrcDstTensors(numInterleavedChannels, numPlanes, dtype, srcData, dstData); + + RunChannelSwitch(numInterleavedChannels, numPlanes, dtype, + [&stream, &srcData, &dstData](auto dummyVal, auto isPlanar) + { + using ValueT = decltype(dummyVal); + using IsPlanar = decltype(isPlanar); + RunInvert(stream, *srcData, *dstData); + }); +} + +// VarShape input variant +void Invert::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &src, + const nvcv::ImageBatchVarShape &dst) const +{ + CVCUDA_NVTX_RANGE("cvcuda::Invert::operator()[ImageBatchVarShape]"); + int numInterleavedChannels; + int numPlanes; + nvcv::DataType dtype; + auto srcDstData = ValidateSrcDstVarBatch(numInterleavedChannels, numPlanes, dtype, stream, src, dst); + + RunChannelSwitch(numInterleavedChannels, numPlanes, dtype, + [&stream, &srcDstData](auto dummyVal, auto isPlanar) + { + using ValueT = decltype(dummyVal); + using IsPlanar = decltype(isPlanar); + auto &[srcData, dstData] = srcDstData; + RunInvert(stream, *srcData, *dstData); + }); +} + +} // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpInvert.hpp b/src/cvcuda/priv/OpInvert.hpp new file mode 100644 index 000000000..e0d81c4cf --- /dev/null +++ b/src/cvcuda/priv/OpInvert.hpp @@ -0,0 +1,47 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +/** + * @file OpInvert.hpp + * + * @brief Defines the private C++ Class for the Invert operation. + */ + +#ifndef CVCUDA_PRIV__INVERT_HPP +#define CVCUDA_PRIV__INVERT_HPP + +#include "IOperator.hpp" + +#include +#include +#include + +namespace cvcuda::priv { + +class Invert final : public IOperator +{ +public: + explicit Invert(); + + void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out) const; + + void operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out) const; +}; + +} // namespace cvcuda::priv + +#endif // CVCUDA_PRIV__INVERT_HPP diff --git a/src/cvcuda/priv/OpJointBilateralFilter.cpp b/src/cvcuda/priv/OpJointBilateralFilter.cpp index 6e841cc38..62f178e7b 100644 --- a/src/cvcuda/priv/OpJointBilateralFilter.cpp +++ b/src/cvcuda/priv/OpJointBilateralFilter.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "OpJointBilateralFilter.hpp" +#include "Nvtx.hpp" #include "legacy/CvCudaLegacy.h" #include "legacy/CvCudaLegacyHelpers.hpp" @@ -29,7 +30,8 @@ namespace legacy = nvcv::legacy::cuda_op; JointBilateralFilter::JointBilateralFilter() { - legacy::DataShape maxIn, maxOut; + legacy::DataShape maxIn; + legacy::DataShape maxOut; //maxIn/maxOut not used by op. m_legacyOp = std::make_unique(maxIn, maxOut); m_legacyOpVarShape = std::make_unique(maxIn, maxOut); @@ -39,6 +41,7 @@ void JointBilateralFilter::operator()(cudaStream_t stream, const nvcv::Tensor &i const nvcv::Tensor &out, int diameter, float sigmaColor, float sigmaSpace, NVCVBorderType borderMode) const { + CVCUDA_NVTX_RANGE("cvcuda::JointBilateralFilter::operator()[Tensor]"); auto inData = in.exportData(); if (inData == nullptr) { @@ -69,6 +72,7 @@ void JointBilateralFilter::operator()(cudaStream_t stream, const nvcv::ImageBatc const nvcv::Tensor &diameter, const nvcv::Tensor &sigmaColor, const nvcv::Tensor &sigmaSpace, NVCVBorderType borderMode) const { + CVCUDA_NVTX_RANGE("cvcuda::JointBilateralFilter::operator()[ImageBatchVarShape]"); auto inData = in.exportData(stream); if (inData == nullptr) { diff --git a/src/cvcuda/priv/OpJpegCompressionDistortion.cu b/src/cvcuda/priv/OpJpegCompressionDistortion.cu new file mode 100644 index 000000000..9799b2998 --- /dev/null +++ b/src/cvcuda/priv/OpJpegCompressionDistortion.cu @@ -0,0 +1,874 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +// Kernel structure ported from NVIDIA DALI's jpeg_distortion_gpu_impl.cuh (Apache-2.0, +// Copyright (c) 2021-2022 NVIDIA CORPORATION & AFFILIATES), fixed to 4:2:0 chroma subsampling, +// with quantization tables built on device from a per-image or per-batch quality value and DALI's +// BlockSetup tiling replaced by a chroma-plane grid with one 32x16-chroma region per CUDA block. + +#include "JpegDistortionMath.hpp" +#include "OpJpegCompressionDistortion.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace cuda = nvcv::cuda; +namespace util = nvcv::util; +namespace jpeg = cvcuda::priv::jpeg; + +namespace { + +// One 32x16-thread block covers a 32x16 chroma region = 64x32 luma pixels in 4:2:0. +constexpr int kBlockWidth = 32; +constexpr int kBlockHeight = 16; + +// Per-batch or per-image quality: a packed rank-1 S32 tensor when present, a scalar otherwise. +struct QualityParam +{ + cuda::Tensor1DWrap perSample; + int32_t scalar; + bool hasTensor; + + inline __device__ int32_t at(int z) const + { + return hasTensor ? *perSample.ptr(z) : scalar; + } +}; + +// Pixel accessors: coordinates are pre-clamped by the caller; z is the sample/image index. +// The VarShape accessors also expose per-image dimensions. + +template +struct InterleavedColorIO +{ + SrcWrapper src; + DstWrapper dst; + + inline __device__ uchar3 load(int z, int y, int x) const + { + return src[int3{x, y, z}]; + } + + inline __device__ void store(int z, int y, int x, uchar3 v) const + { + dst[int3{x, y, z}] = v; + } +}; + +template +struct PlanarColorIO +{ + SrcWrapper src; + DstWrapper dst; + + inline __device__ uchar3 load(int z, int y, int x) const + { + return uchar3{*src.ptr(z, 0, y, x), *src.ptr(z, 1, y, x), *src.ptr(z, 2, y, x)}; + } + + inline __device__ void store(int z, int y, int x, uchar3 v) const + { + *dst.ptr(z, 0, y, x) = v.x; + *dst.ptr(z, 1, y, x) = v.y; + *dst.ptr(z, 2, y, x) = v.z; + } +}; + +template +struct InterleavedGrayIO +{ + SrcWrapper src; + DstWrapper dst; + + inline __device__ uint8_t load(int z, int y, int x) const + { + return src[int3{x, y, z}].x; + } + + inline __device__ void store(int z, int y, int x, uint8_t v) const + { + dst[int3{x, y, z}] = uchar1{v}; + } +}; + +template +struct PlanarGrayIO +{ + SrcWrapper src; + DstWrapper dst; + + inline __device__ uint8_t load(int z, int y, int x) const + { + return *src.ptr(z, 0, y, x); + } + + inline __device__ void store(int z, int y, int x, uint8_t v) const + { + *dst.ptr(z, 0, y, x) = v; + } +}; + +// Color (4:2:0) block pipeline ----------------------------------------------------------- +// +// Each thread owns one chroma pixel and its 2x2 luma quad. Loads are clamped to the image (edge +// replication, DALI's NN sampler + BorderClamp), so the whole region's 8x8 blocks are populated +// even past the image edge; stores are bounds-guarded per pixel. + +template +inline __device__ void JpegDistortionColorBlock(const IO &io, int2 size, int z, QualityParam quality) +{ + constexpr int kChromaPerRow = kBlockWidth / 8; // 4 chroma blocks across + constexpr int kChromaRows = kBlockHeight / 8; // 2 chroma blocks down + constexpr int kChromaBlocks = kChromaPerRow * kChromaRows; + constexpr int kLumaPerRow = 2 * kChromaPerRow; + constexpr int kLumaBlocks = 4 * kChromaBlocks; + constexpr int kTotalBlocks = 2 * kChromaBlocks + kLumaBlocks; // cb + cr + luma = 48 + + // The 9-wide padding avoids shared-memory bank conflicts between row and column DCT passes. + __shared__ float sBlocks[kTotalBlocks][8][9]; + __shared__ float sQuant[2][64]; // [0] luma, [1] chroma + + const int chromaW = (size.x + 1) >> 1; + const int chromaH = (size.y + 1) >> 1; + const int chromaStartX = static_cast(blockIdx.x) * kBlockWidth; + const int chromaStartY = static_cast(blockIdx.y) * kBlockHeight; + // The grid covers the batch's maximum extent; a block fully outside this image's chroma plane + // has nothing to do. The condition is uniform across the block, before any __syncthreads(). + if (chromaStartX >= chromaW || chromaStartY >= chromaH) + { + return; + } + + const int tx = static_cast(threadIdx.x); + const int ty = static_cast(threadIdx.y); + const int tid = ty * kBlockWidth + tx; + + // Scaled quantization tables for this sample (device-side equivalent of DALI's host build). + const float scale = jpeg::QuantScale(quality.at(z)); + if (tid < 128) + { + const uint8_t base = tid < 64 ? jpeg::kLumaQuantBase[tid] : jpeg::kChromaQuantBase[tid - 64]; + sQuant[tid >> 6][tid & 63] = jpeg::QuantTableEntry(scale, base); + } + + // Clamped loads of the 2x2 luma quad; chroma from the RGB average (average first, convert after). + const int lxg = (chromaStartX + tx) << 1; + const int lyg = (chromaStartY + ty) << 1; + const int xmax = size.x - 1; + const int ymax = size.y - 1; + + const uchar3 p00 = io.load(z, ::min(lyg, ymax), ::min(lxg, xmax)); + const uchar3 p01 = io.load(z, ::min(lyg, ymax), ::min(lxg + 1, xmax)); + const uchar3 p10 = io.load(z, ::min(lyg + 1, ymax), ::min(lxg, xmax)); + const uchar3 p11 = io.load(z, ::min(lyg + 1, ymax), ::min(lxg + 1, xmax)); + + const uchar3 avg = jpeg::Avg4(p00, p01, p10, p11); + + const int cBlk = (ty >> 3) * kChromaPerRow + (tx >> 3); + const int cy = ty & 7; + const int cx = tx & 7; + const int lx2 = tx << 1; + const int ly2 = ty << 1; + const int lBlk = (ly2 >> 3) * kLumaPerRow + (lx2 >> 3); + const int ly = ly2 & 7; + const int lx = lx2 & 7; + + float(*cb)[8][9] = &sBlocks[cBlk]; + float(*cr)[8][9] = &sBlocks[kChromaBlocks + cBlk]; + float(*luma)[8][9] = &sBlocks[2 * kChromaBlocks + lBlk]; + + // Level shift to [-128, 127] before the DCT. + (*cb)[cy][cx] = static_cast(jpeg::RgbToCb(avg)) - 128.0f; + (*cr)[cy][cx] = static_cast(jpeg::RgbToCr(avg)) - 128.0f; + (*luma)[ly][lx] = static_cast(jpeg::RgbToY(p00)) - 128.0f; + (*luma)[ly][lx + 1] = static_cast(jpeg::RgbToY(p01)) - 128.0f; + (*luma)[ly + 1][lx] = static_cast(jpeg::RgbToY(p10)) - 128.0f; + (*luma)[ly + 1][lx + 1] = static_cast(jpeg::RgbToY(p11)) - 128.0f; + + __syncthreads(); + + constexpr int kSlices = kTotalBlocks * 8; + constexpr int kNThreads = kBlockWidth * kBlockHeight; + + for (int s = tid; s < kSlices; s += kNThreads) + { + jpeg::FwdDct8<1>(&sBlocks[s >> 3][s & 7][0]); + } + __syncthreads(); + for (int s = tid; s < kSlices; s += kNThreads) + { + jpeg::FwdDct8<9>(&sBlocks[s >> 3][0][s & 7]); + } + __syncthreads(); + + const float chromaQ = sQuant[1][(cy << 3) + cx]; + (*cb)[cy][cx] = jpeg::Quantize((*cb)[cy][cx], chromaQ); + (*cr)[cy][cx] = jpeg::Quantize((*cr)[cy][cx], chromaQ); + + (*luma)[ly][lx] = jpeg::Quantize((*luma)[ly][lx], sQuant[0][(ly << 3) + lx]); + (*luma)[ly][lx + 1] = jpeg::Quantize((*luma)[ly][lx + 1], sQuant[0][(ly << 3) + lx + 1]); + (*luma)[ly + 1][lx] = jpeg::Quantize((*luma)[ly + 1][lx], sQuant[0][((ly + 1) << 3) + lx]); + (*luma)[ly + 1][lx + 1] = jpeg::Quantize((*luma)[ly + 1][lx + 1], sQuant[0][((ly + 1) << 3) + lx + 1]); + __syncthreads(); + + for (int s = tid; s < kSlices; s += kNThreads) + { + jpeg::InvDct8<9>(&sBlocks[s >> 3][0][s & 7]); + } + __syncthreads(); + for (int s = tid; s < kSlices; s += kNThreads) + { + jpeg::InvDct8<1>(&sBlocks[s >> 3][s & 7][0]); + } + __syncthreads(); + + // Level shift back, replicate the reconstructed chroma over the quad, store in-bounds pixels. + const uint8_t ocb = jpeg::SatCastU8((*cb)[cy][cx] + 128.0f); + const uint8_t ocr = jpeg::SatCastU8((*cr)[cy][cx] + 128.0f); + + if (lyg <= ymax && lxg <= xmax) + { + io.store(z, lyg, lxg, jpeg::YCbCrToRgb(jpeg::SatCastU8((*luma)[ly][lx] + 128.0f), ocb, ocr)); + } + if (lyg <= ymax && lxg + 1 <= xmax) + { + io.store(z, lyg, lxg + 1, jpeg::YCbCrToRgb(jpeg::SatCastU8((*luma)[ly][lx + 1] + 128.0f), ocb, ocr)); + } + if (lyg + 1 <= ymax && lxg <= xmax) + { + io.store(z, lyg + 1, lxg, jpeg::YCbCrToRgb(jpeg::SatCastU8((*luma)[ly + 1][lx] + 128.0f), ocb, ocr)); + } + if (lyg + 1 <= ymax && lxg + 1 <= xmax) + { + io.store(z, lyg + 1, lxg + 1, jpeg::YCbCrToRgb(jpeg::SatCastU8((*luma)[ly + 1][lx + 1] + 128.0f), ocb, ocr)); + } +} + +// Grayscale block pipeline --------------------------------------------------------------- +// +// The single channel is the luma plane: level shift, DCT, luma-table quantization, inverse DCT. +// No color conversion and no chroma path. One thread per pixel over a 32x16 luma region. + +template +inline __device__ void JpegDistortionGrayBlock(const IO &io, int2 size, int z, QualityParam quality) +{ + constexpr int kBlocksPerRow = kBlockWidth / 8; // 4 + constexpr int kBlockCount = kBlocksPerRow * (kBlockHeight / 8); // 8 + + __shared__ float sBlocks[kBlockCount][8][9]; + __shared__ float sQuant[64]; + + const int startX = static_cast(blockIdx.x) * kBlockWidth; + const int startY = static_cast(blockIdx.y) * kBlockHeight; + if (startX >= size.x || startY >= size.y) + { + return; + } + + const int tx = static_cast(threadIdx.x); + const int ty = static_cast(threadIdx.y); + const int tid = ty * kBlockWidth + tx; + + const float scale = jpeg::QuantScale(quality.at(z)); + if (tid < 64) + { + sQuant[tid] = jpeg::QuantTableEntry(scale, jpeg::kLumaQuantBase[tid]); + } + + const int px = startX + tx; + const int py = startY + ty; + + const uint8_t v = io.load(z, ::min(py, size.y - 1), ::min(px, size.x - 1)); + + const int blk = ((ty >> 3) * kBlocksPerRow) + (tx >> 3); + const int by = ty & 7; + const int bx = tx & 7; + + sBlocks[blk][by][bx] = static_cast(v) - 128.0f; + __syncthreads(); + + constexpr int kSlices = kBlockCount * 8; + constexpr int kNThreads = kBlockWidth * kBlockHeight; + + for (int s = tid; s < kSlices; s += kNThreads) + { + jpeg::FwdDct8<1>(&sBlocks[s >> 3][s & 7][0]); + } + __syncthreads(); + for (int s = tid; s < kSlices; s += kNThreads) + { + jpeg::FwdDct8<9>(&sBlocks[s >> 3][0][s & 7]); + } + __syncthreads(); + + sBlocks[blk][by][bx] = jpeg::Quantize(sBlocks[blk][by][bx], sQuant[(by << 3) + bx]); + __syncthreads(); + + for (int s = tid; s < kSlices; s += kNThreads) + { + jpeg::InvDct8<9>(&sBlocks[s >> 3][0][s & 7]); + } + __syncthreads(); + for (int s = tid; s < kSlices; s += kNThreads) + { + jpeg::InvDct8<1>(&sBlocks[s >> 3][s & 7][0]); + } + __syncthreads(); + + if (px < size.x && py < size.y) + { + io.store(z, py, px, jpeg::SatCastU8(sBlocks[blk][by][bx] + 128.0f)); + } +} + +// Kernel shells --------------------------------------------------------------------------- + +template +__global__ void JpegDistortionColorKernel(IO io, int2 size, QualityParam quality) +{ + JpegDistortionColorBlock(io, size, static_cast(blockIdx.z), quality); +} + +template +__global__ void JpegDistortionColorVarShapeKernel(IO io, QualityParam quality) +{ + const int z = static_cast(blockIdx.z); + const int2 size{io.dst.width(z), io.dst.height(z)}; + JpegDistortionColorBlock(io, size, z, quality); +} + +template +__global__ void JpegDistortionGrayKernel(IO io, int2 size, QualityParam quality) +{ + JpegDistortionGrayBlock(io, size, static_cast(blockIdx.z), quality); +} + +template +__global__ void JpegDistortionGrayVarShapeKernel(IO io, QualityParam quality) +{ + const int z = static_cast(blockIdx.z); + const int2 size{io.dst.width(z), io.dst.height(z)}; + JpegDistortionGrayBlock(io, size, z, quality); +} + +// Launchers ------------------------------------------------------------------------------- + +constexpr int kGridYZLimit = 65535; + +inline void CheckBatchLimit(int numSamples) +{ + if (numSamples > kGridYZLimit) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Batch size exceeds the CUDA grid.z limit of 65535"); + } +} + +// The color grid tiles the chroma plane (half resolution); the grayscale grid tiles the luma plane. +inline dim3 MakeGrid(int width, int height, int numSamples, bool isColor) +{ + const int gridW = isColor ? (width + 1) >> 1 : width; + const int gridH = isColor ? (height + 1) >> 1 : height; + const int gridY = util::DivUp(gridH, kBlockHeight); + if (gridY > kGridYZLimit) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Image height exceeds the CUDA grid.y limit of 65535"); + } + return dim3(util::DivUp(gridW, kBlockWidth), gridY, static_cast(numSamples)); +} + +inline void RunTensor(cudaStream_t stream, const nvcv::TensorDataStridedCuda &srcData, + const nvcv::TensorDataStridedCuda &dstData, bool isPlanar, int numChannels, + const QualityParam &quality) +{ + auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(srcData); + auto dstAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(dstData); + NVCV_ASSERT(srcAccess && dstAccess); + + const int2 size = cuda::StaticCast(long2{srcAccess->numCols(), srcAccess->numRows()}); + CheckBatchLimit(static_cast(srcAccess->numSamples())); + + const int64_t inMaxStride = srcAccess->sampleStride() * srcAccess->numSamples(); + const int64_t outMaxStride = dstAccess->sampleStride() * dstAccess->numSamples(); + if (std::max(inMaxStride, outMaxStride) > cuda::TypeTraits::max) + { + throw nvcv::Exception(nvcv::Status::ERROR_OVERFLOW, "Input or output size exceeds %d. Tensor is too large.", + cuda::TypeTraits::max); + } + + const dim3 block(kBlockWidth, kBlockHeight, 1); + const dim3 grid = MakeGrid(size.x, size.y, static_cast(srcAccess->numSamples()), numChannels == 3); + + if (numChannels == 3) + { + if (isPlanar) + { + auto src = cuda::CreateTensorWrapNCHW(srcData); + auto dst = cuda::CreateTensorWrapNCHW(dstData); + PlanarColorIO io{src, dst}; + JpegDistortionColorKernel<<>>(io, size, quality); + } + else + { + auto src = cuda::CreateTensorWrapNHW(srcData); + auto dst = cuda::CreateTensorWrapNHW(dstData); + InterleavedColorIO io{src, dst}; + JpegDistortionColorKernel<<>>(io, size, quality); + } + } + else + { + if (isPlanar) + { + auto src = cuda::CreateTensorWrapNCHW(srcData); + auto dst = cuda::CreateTensorWrapNCHW(dstData); + PlanarGrayIO io{src, dst}; + JpegDistortionGrayKernel<<>>(io, size, quality); + } + else + { + auto src = cuda::CreateTensorWrapNHW(srcData); + auto dst = cuda::CreateTensorWrapNHW(dstData); + InterleavedGrayIO io{src, dst}; + JpegDistortionGrayKernel<<>>(io, size, quality); + } + } + NVCV_CHECK_THROW(cudaGetLastError()); +} + +inline void RunVarShape(cudaStream_t stream, const nvcv::ImageBatchVarShapeDataStridedCuda &srcData, + const nvcv::ImageBatchVarShapeDataStridedCuda &dstData, bool isPlanar, int numChannels, + const QualityParam &quality) +{ + CheckBatchLimit(dstData.numImages()); + + const int3 maxSize{dstData.maxSize().w, dstData.maxSize().h, dstData.numImages()}; + const dim3 block(kBlockWidth, kBlockHeight, 1); + const dim3 grid = MakeGrid(maxSize.x, maxSize.y, maxSize.z, numChannels == 3); + + if (numChannels == 3) + { + if (isPlanar) + { + cuda::ImageBatchVarShapeWrap src(srcData); + cuda::ImageBatchVarShapeWrap dst(dstData); + PlanarColorIO io{src, dst}; + JpegDistortionColorVarShapeKernel<<>>(io, quality); + } + else + { + cuda::ImageBatchVarShapeWrap src(srcData); + cuda::ImageBatchVarShapeWrap dst(dstData); + InterleavedColorIO io{src, dst}; + JpegDistortionColorVarShapeKernel<<>>(io, quality); + } + } + else + { + if (isPlanar) + { + cuda::ImageBatchVarShapeWrap src(srcData); + cuda::ImageBatchVarShapeWrap dst(dstData); + PlanarGrayIO io{src, dst}; + JpegDistortionGrayVarShapeKernel<<>>(io, quality); + } + else + { + cuda::ImageBatchVarShapeWrap src(srcData); + cuda::ImageBatchVarShapeWrap dst(dstData); + InterleavedGrayIO io{src, dst}; + JpegDistortionGrayVarShapeKernel<<>>(io, quality); + } + } + NVCV_CHECK_THROW(cudaGetLastError()); +} + +// Validation ------------------------------------------------------------------------------ + +inline bool IsPlanarLayout(nvcv::TensorLayout layout) +{ + return layout == nvcv::TENSOR_NCHW || layout == nvcv::TENSOR_CHW; +} + +inline void ValidateScalarQuality(int quality) +{ + if (quality < 1 || quality > 100) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "The quality must be in [1, 100]"); + } +} + +// Per-image quality tensor: packed rank-1 S32 with exactly one value per image. Values are not +// validated on host (the data lives on device); the kernel clamps them to [1, 100]. +inline QualityParam ValidateQualityTensor(const nvcv::Tensor &quality, int numSamples) +{ + auto qualityData = quality.exportData(); + if (!qualityData) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Quality must be a cuda-accessible, pitch-linear tensor"); + } + if (qualityData->rank() != 1) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Quality tensor must be rank-1"); + } + if (qualityData->dtype() != nvcv::TYPE_S32) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Quality tensor data type must be S32"); + } + if (qualityData->shape(0) != numSamples) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Quality tensor must have one value per image: expected %d, got %d", numSamples, + static_cast(qualityData->shape(0))); + } + if (qualityData->stride(0) != sizeof(int32_t)) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Quality tensor must be packed"); + } + + QualityParam param{}; + param.perSample = cuda::Tensor1DWrap(reinterpret_cast(qualityData->basePtr())); + param.scalar = 0; + param.hasTensor = true; + return param; +} + +inline QualityParam MakeScalarQuality(int quality) +{ + ValidateScalarQuality(quality); + + QualityParam param{}; + param.scalar = quality; + param.hasTensor = false; + return param; +} + +inline bool ValidateSrcDstTensors(bool &isEmpty, int &numChannels, int &numSamples, + const nvcv::Optional &srcData, + const nvcv::Optional &dstData) +{ + if (!srcData) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input must be cuda-accessible, pitch-linear tensor"); + } + if (!dstData) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Output must be cuda-accessible, pitch-linear tensor"); + } + if (srcData->layout() != dstData->layout()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input and output must have the same layout"); + } + + const bool isPlanar = IsPlanarLayout(srcData->layout()); + if (srcData->layout() != nvcv::TENSOR_HWC && srcData->layout() != nvcv::TENSOR_NHWC && !isPlanar) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input must have (N)HWC or (N)CHW layout"); + } + if (srcData->dtype() != dstData->dtype()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input and output must have the same data type"); + } + if (srcData->dtype().numChannels() != 1) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Tensor data type must be scalar; use the C dimension for image channels"); + } + if (srcData->dtype() != nvcv::TYPE_U8) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Invalid data type: JpegCompressionDistortion supports 8-bit unsigned only"); + } + + auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcData); + auto dstAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*dstData); + if (!srcAccess || !dstAccess) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input/output must be accessible as strided images"); + } + if (srcAccess->numSamples() != dstAccess->numSamples()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of samples"); + } + + numChannels = srcAccess->numChannels(); + if (numChannels != dstAccess->numChannels()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of channels"); + } + if (numChannels != 1 && numChannels != 3) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input must have 1 or 3 channels"); + } + if (srcAccess->numCols() != dstAccess->numCols() || srcAccess->numRows() != dstAccess->numRows()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input and output must have matching width and height"); + } + + numSamples = static_cast(srcAccess->numSamples()); + isEmpty = numSamples == 0 || srcAccess->numRows() == 0 || srcAccess->numCols() == 0; + return isPlanar; +} + +inline void ValidateImagePlanes(const nvcv::Image &image, const nvcv::ImageFormat &format) +{ + auto data = image.exportData(); + if (!data || data->numPlanes() != format.numPlanes()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Image plane descriptors must match the image format"); + } + + constexpr int64_t limit = cuda::TypeTraits::max; + for (int p = 0; p < data->numPlanes(); ++p) + { + const nvcv::ImagePlaneStrided &plane = data->plane(p); + const nvcv::Size2D expected = format.planeSize(image.size(), p); + if (plane.width != expected.w || plane.height != expected.h) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Image plane descriptors must match the image format and size"); + } + if (expected != image.size()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "All image planes must have matching width and height"); + } + + const int64_t pixelStride = format.planePixelStrideBytes(p); + if (plane.rowStride < 0 + || (static_cast(plane.height - 1) * plane.rowStride + + static_cast(plane.width - 1) * pixelStride + > limit)) + { + throw nvcv::Exception(nvcv::Status::ERROR_OVERFLOW, + "Input or output image-plane maximum byte offset exceeds %d", + static_cast(limit)); + } + } +} + +inline bool ValidateSrcDstVarBatch(bool &isEmpty, int &numChannels, const nvcv::ImageBatchVarShape &src, + const nvcv::ImageBatchVarShape &dst, + const nvcv::Optional &srcData, + const nvcv::Optional &dstData) +{ + if (!srcData) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input must be cuda-accessible, varshape pitch-linear image batch"); + } + if (!dstData) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Output must be cuda-accessible, varshape pitch-linear image batch"); + } + if (srcData->numImages() != dstData->numImages()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of samples"); + } + isEmpty = srcData->numImages() == 0; + if (isEmpty) + { + return false; + } + + const auto &srcFormat = srcData->uniqueFormat(); + const auto &dstFormat = dstData->uniqueFormat(); + if (!srcFormat || !dstFormat) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "All images in a batch must have the same format"); + } + if (srcFormat != dstFormat) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input and output must have the same format"); + } + + numChannels = srcFormat.numChannels(); + if (numChannels != 1 && numChannels != 3) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "The input must have 1 or 3 channels"); + } + if (numChannels == 3 + && (srcFormat.colorModel() != nvcv::ColorModel::RGB + || (srcFormat.swizzle() != nvcv::Swizzle::S_XYZ0 && srcFormat.swizzle() != nvcv::Swizzle::S_XYZ1))) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Three-channel input must use RGB channel order"); + } + + nvcv::ExtraChannelInfo extraChannels{}; + srcFormat.extraChannelInfo(&extraChannels); + if (extraChannels.numChannels != 0) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Image formats with extra channels are not supported"); + } + if (srcFormat.chromaSubsampling() != nvcv::ChromaSubsampling::CSS_444) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Chroma-subsampled image formats are not supported"); + } + + const int numPlanes = srcFormat.numPlanes(); + nvcv::DataType dtype = srcFormat.planeDataType(0); + if (dtype != nvcv::TYPE_U8 && dtype != nvcv::TYPE_3U8) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Invalid data type: JpegCompressionDistortion supports 8-bit unsigned only"); + } + if (numPlanes == 1) + { + if (dtype.numChannels() != numChannels || srcFormat.planePixelStrideBytes(0) != dtype.strideBytes()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Single-plane images must have one packed element per pixel"); + } + } + else if (numPlanes != numChannels || dtype.numChannels() != 1 + || srcFormat.planePixelStrideBytes(0) != dtype.strideBytes()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Planar images must have one scalar, full-resolution plane per channel"); + } + for (int p = 1; p < numPlanes; ++p) + { + if (srcFormat.planeDataType(p) != dtype || srcFormat.planeDataType(p).numChannels() != 1 + || srcFormat.planePixelStrideBytes(p) != dtype.strideBytes()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "All image planes must have the same packed scalar data type"); + } + } + + for (int i = 0; i < src.numImages(); ++i) + { + const nvcv::Size2D srcSize = src[i].size(); + const nvcv::Size2D dstSize = dst[i].size(); + if (srcSize != dstSize) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input and output image %d sizes must match: input is %dx%d, output is %dx%d", i, + srcSize.w, srcSize.h, dstSize.w, dstSize.h); + } + ValidateImagePlanes(src[i], srcFormat); + ValidateImagePlanes(dst[i], dstFormat); + } + + return numPlanes > 1; +} + +} // anonymous namespace + +namespace cvcuda::priv { + +JpegCompressionDistortion::JpegCompressionDistortion() {} + +void JpegCompressionDistortion::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, + const nvcv::Tensor &quality) const +{ + bool isEmpty; + int numChannels; + int numSamples; + auto srcData = in.exportData(); + auto dstData = out.exportData(); + const bool isPlanar = ValidateSrcDstTensors(isEmpty, numChannels, numSamples, srcData, dstData); + + const QualityParam param = ValidateQualityTensor(quality, numSamples); + if (isEmpty) + { + return; + } + + RunTensor(stream, *srcData, *dstData, isPlanar, numChannels, param); +} + +void JpegCompressionDistortion::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, + int quality) const +{ + const QualityParam param = MakeScalarQuality(quality); + + bool isEmpty; + int numChannels; + int numSamples; + auto srcData = in.exportData(); + auto dstData = out.exportData(); + const bool isPlanar = ValidateSrcDstTensors(isEmpty, numChannels, numSamples, srcData, dstData); + if (isEmpty) + { + return; + } + + RunTensor(stream, *srcData, *dstData, isPlanar, numChannels, param); +} + +void JpegCompressionDistortion::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, + const nvcv::ImageBatchVarShape &out, const nvcv::Tensor &quality) const +{ + bool isEmpty; + int numChannels; + auto srcData = in.exportData(stream); + auto dstData = out.exportData(stream); + const bool isPlanar = ValidateSrcDstVarBatch(isEmpty, numChannels, in, out, srcData, dstData); + if (isEmpty) + { + return; + } + + const QualityParam param = ValidateQualityTensor(quality, srcData->numImages()); + + RunVarShape(stream, *srcData, *dstData, isPlanar, numChannels, param); +} + +void JpegCompressionDistortion::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, + const nvcv::ImageBatchVarShape &out, int quality) const +{ + const QualityParam param = MakeScalarQuality(quality); + + bool isEmpty; + int numChannels; + auto srcData = in.exportData(stream); + auto dstData = out.exportData(stream); + const bool isPlanar = ValidateSrcDstVarBatch(isEmpty, numChannels, in, out, srcData, dstData); + if (isEmpty) + { + return; + } + + RunVarShape(stream, *srcData, *dstData, isPlanar, numChannels, param); +} + +} // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpJpegCompressionDistortion.hpp b/src/cvcuda/priv/OpJpegCompressionDistortion.hpp new file mode 100644 index 000000000..8bd484ba4 --- /dev/null +++ b/src/cvcuda/priv/OpJpegCompressionDistortion.hpp @@ -0,0 +1,54 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +/** + * @file OpJpegCompressionDistortion.hpp + * + * @brief Defines the private C++ Class for the JpegCompressionDistortion operation. + */ + +#ifndef CVCUDA_PRIV__JPEG_COMPRESSION_DISTORTION_HPP +#define CVCUDA_PRIV__JPEG_COMPRESSION_DISTORTION_HPP + +#include "IOperator.hpp" + +#include +#include +#include + +namespace cvcuda::priv { + +class JpegCompressionDistortion final : public IOperator +{ +public: + explicit JpegCompressionDistortion(); + + void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, + const nvcv::Tensor &quality) const; + + void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, int quality) const; + + void operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, + const nvcv::Tensor &quality) const; + + void operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, + int quality) const; +}; + +} // namespace cvcuda::priv + +#endif // CVCUDA_PRIV__JPEG_COMPRESSION_DISTORTION_HPP diff --git a/src/cvcuda/priv/OpLabel.cu b/src/cvcuda/priv/OpLabel.cu index 726550512..915afc2fb 100644 --- a/src/cvcuda/priv/OpLabel.cu +++ b/src/cvcuda/priv/OpLabel.cu @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -46,6 +46,8 @@ */ #include "Assert.h" +#include "CudaDeviceUtils.hpp" +#include "Nvtx.hpp" #include "OpLabel.hpp" #include @@ -62,6 +64,7 @@ #include #include +#include namespace cuda = nvcv::cuda; namespace util = nvcv::util; @@ -72,6 +75,18 @@ constexpr int REGION_NOT_MARKED = 0; constexpr int REGION_REMOVED = 1; constexpr int REGION_INSIDE_MASK = 2; +constexpr int kLabelDefault2DBlockHeight = 4; +constexpr int kLabelU32Tall2DBlockHeight = 16; + +constexpr int LabelU32BlockHeightForSM(int sm) +{ + return sm == 75 ? kLabelDefault2DBlockHeight : kLabelU32Tall2DBlockHeight; +} + +static_assert(LabelU32BlockHeightForSM(75) == kLabelDefault2DBlockHeight); +static_assert(LabelU32BlockHeightForSM(80) == kLabelU32Tall2DBlockHeight); +static_assert(LabelU32BlockHeightForSM(90) == kLabelU32Tall2DBlockHeight); + template using ArgWrap = cuda::Tensor1DWrap; @@ -384,9 +399,9 @@ __global__ void ReplaceBgLabels2D(DstWrap dst, SrcWrap src, ArgWrap bgLabel, } } -template -__global__ void CountLabels2D(ArgWrap

count, StatsWrap stats, DstWrap dst, ArgWrap bgLabel, int2 size, - int maxCapacity) +template +__global__ void CountLabels2D(ArgWrap
count, StatsWrap stats, DstWrap dst, SrcWrap src, ArgWrap bgLabel, + ArgWrap minThresh, ArgWrap maxThresh, int2 size, int maxCapacity) { int3 gc; gc.x = blockIdx.x * blockDim.x + threadIdx.x; @@ -398,14 +413,43 @@ __global__ void CountLabels2D(ArgWrap
count, StatsWrap stats, DstWrap dst, A return; } - bool hasBgLabel = (bgLabel.ptr(0) != nullptr); + bool hasBgLabel = bgLabel.ptr(0) != nullptr; ST backgroundLabel = hasBgLabel ? bgLabel[gc.z] : 0; + DT label = dst[gc]; - DT label = dst[gc]; - - if (hasBgLabel && label == (DT)backgroundLabel) + if (hasBgLabel) { - return; // do not count background labels + bool hasMinThresh = minThresh.ptr(0) != nullptr; + bool hasMaxThresh = maxThresh.ptr(0) != nullptr; + ST minThreshold = hasMinThresh ? minThresh[gc.z] : 0; + ST maxThreshold = hasMaxThresh ? maxThresh[gc.z] : 0; + ST pyx = src[gc]; + + if (hasMinThresh && hasMaxThresh) + { + pyx = pyx < minThreshold || pyx > maxThreshold ? 0 : 1; + } + else if (hasMinThresh) + { + pyx = pyx < minThreshold ? 0 : 1; + } + else if (hasMaxThresh) + { + pyx = pyx > maxThreshold ? 0 : 1; + } + + if (pyx == backgroundLabel) + { + dst[gc] = backgroundLabel; + return; + } + + DT endLabel = dst.strides()[0] / sizeof(DT); + if (label == (DT)backgroundLabel) + { + label = endLabel; + dst[gc] = endLabel; + } } DT posLabel = gc.y * dst.strides()[1] / sizeof(DT) + gc.x; @@ -1414,7 +1458,7 @@ __global__ void Relabel3D(StatsWrap stats, DstWrap dst, ArgWrap bgLabel, Arg // Run functions --------------------------------------------------------------- -template +template inline void RunLabelForType(cudaStream_t stream, const nvcv::TensorDataStridedCuda &srcData, const nvcv::TensorDataStridedCuda &dstData, const int4 &shapeWHDN, const nvcv::Tensor &bgLabel, const nvcv::Tensor &minThresh, const nvcv::Tensor &maxThresh, @@ -1422,6 +1466,7 @@ inline void RunLabelForType(cudaStream_t stream, const nvcv::TensorDataStridedCu const nvcv::Tensor &mask, int numDim, bool relabel) { constexpr int BW = 32, BH = 4, BD = 2; // block width, height and depth + static_assert(BH2D == kLabelDefault2DBlockHeight || BH2D == kLabelU32Tall2DBlockHeight); int4 idsNDHW{srcData.layout().find('N'), srcData.layout().find('D'), srcData.layout().find('H'), srcData.layout().find('W')}; @@ -1533,10 +1578,10 @@ inline void RunLabelForType(cudaStream_t stream, const nvcv::TensorDataStridedCu srcStridesNH.x = idsNDHW.x == -1 ? srcStridesNH.y * shapeWHDN.y : (int)srcData.stride(idsNDHW.x); dstStridesNH.x = idsNDHW.x == -1 ? dstStridesNH.y * shapeWHDN.y : (int)dstData.stride(idsNDHW.x); - dim3 larThreads(BW, BH, 1); - dim3 labBlocks(util::DivUp(sizeWH.x, BW), util::DivUp(sizeWH.y, BH), shapeWHDN.w); - dim3 redBlocksX(util::DivUp(sizeWH.y, BW), util::DivUp((int)labBlocks.x, BH), shapeWHDN.w); - dim3 redBlocksY(util::DivUp(sizeWH.x, BW), util::DivUp((int)labBlocks.y, BH), shapeWHDN.w); + dim3 larThreads(BW, BH2D, 1); + dim3 labBlocks(util::DivUp(sizeWH.x, BW), util::DivUp(sizeWH.y, BH2D), shapeWHDN.w); + dim3 redBlocksX(util::DivUp(sizeWH.y, BW), util::DivUp((int)labBlocks.x, BH2D), shapeWHDN.w); + dim3 redBlocksY(util::DivUp(sizeWH.x, BW), util::DivUp((int)labBlocks.y, BH2D), shapeWHDN.w); cuda::Tensor3DWrap srcWrap(srcData.basePtr(), srcStridesNH.x, srcStridesNH.y); cuda::Tensor3DWrap dstWrap(dstData.basePtr(), dstStridesNH.x, dstStridesNH.y); @@ -1550,7 +1595,7 @@ inline void RunLabelForType(cudaStream_t stream, const nvcv::TensorDataStridedCu mskWrap = cuda::Tensor3DWrap(mskData->basePtr(), mskStridesNH.x, mskStridesNH.y); } - BlockLabel2D + BlockLabel2D <<>>(dstWrap, srcWrap, minThreshWrap, maxThreshWrap, sizeWH); YLabelReduction2D<<>>(dstWrap, srcWrap, minThreshWrap, maxThreshWrap, @@ -1561,15 +1606,15 @@ inline void RunLabelForType(cudaStream_t stream, const nvcv::TensorDataStridedCu ResolveLabels2D<<>>(dstWrap, sizeWH); - if (bgLabel) + if (bgLabel && !count) { ReplaceBgLabels2D<<>>(dstWrap, srcWrap, bgLabelWrap, minThreshWrap, maxThreshWrap, sizeWH); } if (count) { - CountLabels2D<<>>(countWrap, statsWrap, dstWrap, bgLabelWrap, sizeWH, - maxCapacity); + CountLabels2D<<>>(countWrap, statsWrap, dstWrap, srcWrap, bgLabelWrap, + minThreshWrap, maxThreshWrap, sizeWH, maxCapacity); if (stats) { @@ -1662,21 +1707,37 @@ inline void RunLabel(cudaStream_t stream, const nvcv::TensorDataStridedCuda &src const nvcv::Tensor &minSize, const nvcv::Tensor &count, const nvcv::Tensor &stats, const nvcv::Tensor &mask, int numDim, bool relabel) { - switch (srcDataType) + switch (static_cast(srcDataType)) { #define CVCUDA_LABEL_CASE(DT, T) \ - case nvcv::TYPE_##DT: \ + case static_cast(nvcv::TYPE_##DT): \ RunLabelForType(stream, srcData, dstData, srcShape, bgLabel, minThresh, maxThresh, minSize, count, stats, \ mask, numDim, relabel); \ break CVCUDA_LABEL_CASE(U8, uint8_t); CVCUDA_LABEL_CASE(U16, uint16_t); - CVCUDA_LABEL_CASE(U32, uint32_t); CVCUDA_LABEL_CASE(S8, int8_t); CVCUDA_LABEL_CASE(S16, int16_t); CVCUDA_LABEL_CASE(S32, int32_t); + case static_cast(nvcv::TYPE_U32): + if (numDim == 2) + { + int sm = 0; + NVCV_CHECK_THROW(cvcuda::priv::GetCurrentDeviceSM(sm)); + if (LabelU32BlockHeightForSM(sm) == kLabelDefault2DBlockHeight) + { + RunLabelForType(stream, srcData, dstData, srcShape, bgLabel, + minThresh, maxThresh, minSize, count, stats, mask, + numDim, relabel); + break; + } + } + RunLabelForType(stream, srcData, dstData, srcShape, bgLabel, minThresh, + maxThresh, minSize, count, stats, mask, numDim, relabel); + break; + #undef CVCUDA_LABEL_CASE default: @@ -1700,12 +1761,15 @@ void Label::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv:: const nvcv::Tensor &mask, NVCVConnectivityType connectivity, NVCVLabelType assignLabels, NVCVLabelMaskType maskType) const { - if (!(in.shape().layout() == nvcv::TENSOR_HW || in.shape().layout() == nvcv::TENSOR_HWC - || in.shape().layout() == nvcv::TENSOR_NHW || in.shape().layout() == nvcv::TENSOR_NHWC - || in.shape().layout() == nvcv::TENSOR_DHW || in.shape().layout() == nvcv::TENSOR_DHWC - || in.shape().layout() == nvcv::TENSOR_NDHW || in.shape().layout() == nvcv::TENSOR_NDHWC)) + CVCUDA_NVTX_RANGE("cvcuda::Label::operator()[Tensor]"); + const auto layout = in.shape().layout(); + const bool isPlanar = layout == nvcv::TENSOR_CHW || layout == nvcv::TENSOR_NCHW; + if (!(layout == nvcv::TENSOR_HW || layout == nvcv::TENSOR_HWC || isPlanar || layout == nvcv::TENSOR_NHW + || layout == nvcv::TENSOR_NHWC || layout == nvcv::TENSOR_DHW || layout == nvcv::TENSOR_DHWC + || layout == nvcv::TENSOR_NDHW || layout == nvcv::TENSOR_NDHWC)) { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input tensor must have [N][D]HW[C] layout"); + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input tensor must have [N][D]HW[C] or [N]CHW layout"); } // We expect input and output shape to be the same as TensorShape contains TensorLayout diff --git a/src/cvcuda/priv/OpLaplacian.cpp b/src/cvcuda/priv/OpLaplacian.cpp index 0b6c027a9..fab423234 100644 --- a/src/cvcuda/priv/OpLaplacian.cpp +++ b/src/cvcuda/priv/OpLaplacian.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "OpLaplacian.hpp" +#include "Nvtx.hpp" #include "legacy/CvCudaLegacy.h" #include "legacy/CvCudaLegacyHelpers.hpp" @@ -27,16 +28,26 @@ namespace cvcuda::priv { namespace legacy = nvcv::legacy::cuda_op; -Laplacian::Laplacian() +std::unique_ptr Laplacian::CreateLegacyOp(int) { - legacy::DataShape maxIn, maxOut; //maxIn/maxOut not used by op. - m_legacyOp = std::make_unique(maxIn, maxOut); - m_legacyOpVarShape = std::make_unique(maxIn, maxOut); + legacy::DataShape maxIn; + legacy::DataShape maxOut; + return std::make_unique(maxIn, maxOut); } +std::unique_ptr Laplacian::CreateLegacyOpVarShape(int) +{ + legacy::DataShape maxIn; + legacy::DataShape maxOut; + return std::make_unique(maxIn, maxOut); +} + +Laplacian::Laplacian() = default; + void Laplacian::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, const int ksize, const float scale, const NVCVBorderType borderMode) const { + CVCUDA_NVTX_RANGE("cvcuda::Laplacian::operator()[Tensor]"); auto inData = in.exportData(); if (inData == nullptr) { @@ -51,12 +62,13 @@ void Laplacian::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nv "Output must be cuda-accessible, pitch-linear tensor"); } - NVCV_CHECK_THROW(m_legacyOp->infer(*inData, *outData, ksize, scale, borderMode, stream)); + NVCV_CHECK_THROW(m_legacyOp.get().infer(*inData, *outData, ksize, scale, borderMode, stream)); } void Laplacian::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, const nvcv::Tensor &ksize, const nvcv::Tensor &scale, NVCVBorderType borderMode) const { + CVCUDA_NVTX_RANGE("cvcuda::Laplacian::operator()[ImageBatchVarShape]"); auto inData = in.exportData(stream); if (inData == nullptr) { @@ -85,7 +97,20 @@ void Laplacian::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape & "Kernel scale must be cuda-accessible, pitch-linear tensor"); } - NVCV_CHECK_THROW(m_legacyOpVarShape->infer(*inData, *outData, *ksizeData, *scaleData, borderMode, stream)); + const int numImages = in.numImages(); + if (ksizeData->rank() != 1 || ksizeData->shape(0) != numImages) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Kernel aperture size must be a 1D tensor with one value per input image"); + } + + if (scaleData->rank() != 1 || scaleData->shape(0) != numImages) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Kernel scale must be a 1D tensor with one value per input image"); + } + + NVCV_CHECK_THROW(m_legacyOpVarShape.get().infer(*inData, *outData, *ksizeData, *scaleData, borderMode, stream)); } } // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpLaplacian.hpp b/src/cvcuda/priv/OpLaplacian.hpp index 9e2f92ab9..142ee073f 100644 --- a/src/cvcuda/priv/OpLaplacian.hpp +++ b/src/cvcuda/priv/OpLaplacian.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,6 +25,7 @@ #define CVCUDA_PRIV_LAPLACIAN_HPP #include "IOperator.hpp" +#include "PerDeviceResource.hpp" #include "legacy/CvCudaLegacy.h" #include @@ -46,8 +47,11 @@ class Laplacian final : public IOperator const nvcv::Tensor &ksize, const nvcv::Tensor &scale, NVCVBorderType borderMode) const; private: - std::unique_ptr m_legacyOp; - std::unique_ptr m_legacyOpVarShape; + static std::unique_ptr CreateLegacyOp(int deviceId); + static std::unique_ptr CreateLegacyOpVarShape(int deviceId); + + mutable PerDeviceResource m_legacyOp{CreateLegacyOp}; + mutable PerDeviceResource m_legacyOpVarShape{CreateLegacyOpVarShape}; }; } // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpMedianBlur.cpp b/src/cvcuda/priv/OpMedianBlur.cpp index efc77a044..9d1e8b696 100644 --- a/src/cvcuda/priv/OpMedianBlur.cpp +++ b/src/cvcuda/priv/OpMedianBlur.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "OpMedianBlur.hpp" +#include "Nvtx.hpp" #include "legacy/CvCudaLegacy.h" #include "legacy/CvCudaLegacyHelpers.hpp" @@ -27,17 +28,25 @@ namespace cvcuda::priv { namespace legacy = nvcv::legacy::cuda_op; +std::unique_ptr MedianBlur::CreateLegacyOp(int) +{ + legacy::DataShape maxIn; + legacy::DataShape maxOut; + return std::make_unique(maxIn, maxOut); +} + MedianBlur::MedianBlur(const int maxVarShapeBatchSize) + // Legacy operators are single-device by design. PerDeviceResource creates + // one instance per CUDA device for transparent multi-GPU support. + : m_legacyOpVarShape([maxVarShapeBatchSize](int) + { return std::make_unique(maxVarShapeBatchSize); }) { - legacy::DataShape maxIn, maxOut; - // maxIn/maxOut not used by op. - m_legacyOp = std::make_unique(maxIn, maxOut); - m_legacyOpVarShape = std::make_unique(maxVarShapeBatchSize); } void MedianBlur::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, const nvcv::Size2D ksize) const { + CVCUDA_NVTX_RANGE("cvcuda::MedianBlur::operator()[Tensor]"); auto inData = in.exportData(); if (inData == nullptr) { @@ -52,12 +61,13 @@ void MedianBlur::operator()(cudaStream_t stream, const nvcv::Tensor &in, const n "Output must be cuda-accessible, pitch-linear tensor"); } - NVCV_CHECK_THROW(m_legacyOp->infer(*inData, *outData, ksize, stream)); + NVCV_CHECK_THROW(m_legacyOp.get().infer(*inData, *outData, ksize, stream)); } void MedianBlur::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, const nvcv::Tensor &ksize) const { + CVCUDA_NVTX_RANGE("cvcuda::MedianBlur::operator()[ImageBatchVarShape]"); auto inData = in.exportData(stream); if (inData == nullptr) { @@ -76,7 +86,7 @@ void MedianBlur::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "ksize must be a tensor"); } - NVCV_CHECK_THROW(m_legacyOpVarShape->infer(*inData, *outData, *ksizeData, stream)); + NVCV_CHECK_THROW(m_legacyOpVarShape.get().infer(*inData, *outData, *ksizeData, stream)); } } // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpMedianBlur.hpp b/src/cvcuda/priv/OpMedianBlur.hpp index d518dd0a8..efad0c96e 100644 --- a/src/cvcuda/priv/OpMedianBlur.hpp +++ b/src/cvcuda/priv/OpMedianBlur.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,6 +25,7 @@ #define CVCUDA_PRIV_MEDIAN_BLUR_HPP #include "IOperator.hpp" +#include "PerDeviceResource.hpp" #include "legacy/CvCudaLegacy.h" #include @@ -46,8 +47,10 @@ class MedianBlur final : public IOperator const nvcv::Tensor &ksize) const; private: - std::unique_ptr m_legacyOp; - std::unique_ptr m_legacyOpVarShape; + static std::unique_ptr CreateLegacyOp(int deviceId); + + mutable PerDeviceResource m_legacyOp{CreateLegacyOp}; + mutable PerDeviceResource m_legacyOpVarShape; }; } // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpMinAreaRect.cpp b/src/cvcuda/priv/OpMinAreaRect.cpp index 7cfe54fef..763369c43 100644 --- a/src/cvcuda/priv/OpMinAreaRect.cpp +++ b/src/cvcuda/priv/OpMinAreaRect.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "OpMinAreaRect.hpp" +#include "Nvtx.hpp" #include "legacy/CvCudaLegacy.h" #include "legacy/CvCudaLegacyHelpers.hpp" @@ -28,15 +29,22 @@ namespace cvcuda::priv { namespace legacy = nvcv::legacy::cuda_op; MinAreaRect::MinAreaRect(int maxContourNum) + // Legacy operators are single-device by design. PerDeviceResource creates + // one instance per CUDA device for transparent multi-GPU support. + : m_legacyOp( + [maxContourNum](int) + { + legacy::DataShape maxIn; + legacy::DataShape maxOut; + return std::make_unique(maxIn, maxOut, maxContourNum); + }) { - // init - legacy::DataShape maxIn, maxOut; - m_legacyOp = std::make_unique(maxIn, maxOut, maxContourNum); } void MinAreaRect::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, const nvcv::Tensor &numPointsInContour, const int totalContours) const { + CVCUDA_NVTX_RANGE("cvcuda::MinAreaRect::operator()[Tensor]"); auto inData = in.exportData(); if (inData == nullptr) { @@ -71,10 +79,19 @@ void MinAreaRect::operator()(cudaStream_t stream, const nvcv::Tensor &in, const throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Output must have NW layout"); } + // channel check + auto inShape = inData->shape(); + if (auto channels = static_cast(inShape[inShape.rank() - 1]); channels != 2) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input must have 2 channels (x, y coordinates per point)"); + } + // dtype check if (inData->dtype() != nvcv::TYPE_U16 && inData->dtype() != nvcv::TYPE_S16 && inData->dtype() != nvcv::TYPE_S32) { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input must have TYPE_U16 data type"); + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input must have TYPE_U16, TYPE_S16, or TYPE_S32 data type"); } if (outData->dtype() != nvcv::TYPE_F32) { @@ -87,7 +104,7 @@ void MinAreaRect::operator()(cudaStream_t stream, const nvcv::Tensor &in, const } // add calls to kernel here - NVCV_CHECK_THROW(m_legacyOp->infer(*inData, *outData, *numPointsInContourData, totalContours, stream)); + NVCV_CHECK_THROW(m_legacyOp.get().infer(*inData, *outData, *numPointsInContourData, totalContours, stream)); } } // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpMinAreaRect.hpp b/src/cvcuda/priv/OpMinAreaRect.hpp index 0a57f4366..111fcfc1f 100644 --- a/src/cvcuda/priv/OpMinAreaRect.hpp +++ b/src/cvcuda/priv/OpMinAreaRect.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,12 +25,11 @@ #define CVCUDA_PRIV__MIN_AREA_RECT_HPP #include "IOperator.hpp" +#include "PerDeviceResource.hpp" #include "legacy/CvCudaLegacy.h" #include -#include - namespace cvcuda::priv { class MinAreaRect final : public IOperator @@ -42,7 +41,7 @@ class MinAreaRect final : public IOperator const nvcv::Tensor &numPointsInContour, const int totalContours) const; private: - std::unique_ptr m_legacyOp; + mutable PerDeviceResource m_legacyOp; }; } // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpMinMaxLoc.cu b/src/cvcuda/priv/OpMinMaxLoc.cu index 85bc71cd2..3b37e6473 100644 --- a/src/cvcuda/priv/OpMinMaxLoc.cu +++ b/src/cvcuda/priv/OpMinMaxLoc.cu @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,6 +15,7 @@ * limitations under the License. */ +#include "Nvtx.hpp" #include "OpMinMaxLoc.hpp" #include @@ -29,7 +30,8 @@ #include #include -#include +#include +#include #include @@ -104,21 +106,95 @@ __device__ inline auto get(OutWrapper out) } } -// OpMin used when finding only minimum value +// Ordered-int encoding so native integer atomicMin/Max can be used on floating-point +// min/max values. IEEE-754 floats are mapped to unsigned integers that preserve their +// ordering (flip the sign bit for positives, flip all bits for negatives), so an integer +// atomicMin/atomicMax yields the correct float result. This avoids the atomicCAS spin-loop +// that emulates float atomics (cuda::AtomicMin/Max), whose contended latency stalls badly +// on architectures without native float atomics. The slot holds the encoded integer during +// the reduction and is decoded back to the float value before locations are collected. -template -struct OpMin +template +struct OrderedInt; + +template<> +struct OrderedInt +{ + using type = unsigned int; + + inline static __device__ type encode(float f) + { + type u = __float_as_uint(f); + return u ^ (static_cast(-static_cast(u >> 31)) | 0x80000000u); + } + + static __device__ inline float decode(type u) + { + type mask = ((u >> 31) - 1u) | 0x80000000u; + return __uint_as_float(u ^ mask); + } +}; + +template<> +struct OrderedInt +{ + using type = unsigned long long; + + inline static __device__ type encode(double f) + { + type u = static_cast(__double_as_longlong(f)); + return u ^ (static_cast(-static_cast(u >> 63)) | 0x8000000000000000ULL); + } + + static __device__ inline double decode(type u) + { + type mask = ((u >> 63) - 1ULL) | 0x8000000000000000ULL; + return __longlong_as_double(static_cast(u ^ mask)); + } +}; + +template +struct OpSingleExtremaBase { using OutType = OutputType; using BaseOutType = cuda::BaseType; - static constexpr OutType init = {cuda::TypeTraits::max}; - template __device__ inline static void initFill(OutWrapper out, int z) { - get<0>(out)[z] = init; + if constexpr (std::is_floating_point_v) + { + using OI = OrderedInt; + *reinterpret_cast(&get<0>(out)[z].x) = OI::encode(Derived::init.x); + } + else + { + get<0>(out)[z] = Derived::init; + } + } + + template + __device__ inline static void finalize(OutWrapper out, int z) + { + if constexpr (std::is_floating_point_v) + { + using OI = OrderedInt; + auto *p = reinterpret_cast(&get<0>(out)[z].x); + get<0>(out)[z].x = OI::decode(*p); + } } +}; + +// OpMin used when finding only minimum value + +template +struct OpMin : OpSingleExtremaBase> +{ + using Base = OpSingleExtremaBase>; + using typename Base::BaseOutType; + using typename Base::OutType; + + static constexpr OutType init = {cuda::TypeTraits::max}; template __device__ inline static void op(OutType &a, U b) @@ -129,26 +205,29 @@ struct OpMin template __device__ inline static void opAtomic(OutWrapper out, int z, OutType b) { - cuda::AtomicMin(get<0>(out)[z].x, b.x); + if constexpr (std::is_floating_point_v) + { + using OI = OrderedInt; + atomicMin(reinterpret_cast(&get<0>(out)[z].x), OI::encode(b.x)); + } + else + { + cuda::AtomicMin(get<0>(out)[z].x, b.x); + } } }; // OpMax used when finding only maximum value template -struct OpMax +struct OpMax : OpSingleExtremaBase> { - using OutType = OutputType; - using BaseOutType = cuda::BaseType; + using Base = OpSingleExtremaBase>; + using typename Base::BaseOutType; + using typename Base::OutType; static constexpr OutType init = {cuda::Lowest}; - template - __device__ inline static void initFill(OutWrapper out, int z) - { - get<0>(out)[z] = init; - } - template __device__ inline static void op(OutType &a, U b) { @@ -158,7 +237,15 @@ struct OpMax template __device__ inline static void opAtomic(OutWrapper out, int z, OutType b) { - cuda::AtomicMax(get<0>(out)[z].x, b.x); + if constexpr (std::is_floating_point_v) + { + using OI = OrderedInt; + atomicMax(reinterpret_cast(&get<0>(out)[z].x), OI::encode(b.x)); + } + else + { + cuda::AtomicMax(get<0>(out)[z].x, b.x); + } } }; @@ -175,8 +262,17 @@ struct OpMinMax template __device__ inline static void initFill(OutWrapper out, int z) { - get<0>(out)[z] = {init.x}; - get<1>(out)[z] = {init.y}; + if constexpr (std::is_floating_point_v) + { + using OI = OrderedInt; + *reinterpret_cast(&get<0>(out)[z].x) = OI::encode(init.x); + *reinterpret_cast(&get<1>(out)[z].x) = OI::encode(init.y); + } + else + { + get<0>(out)[z] = {init.x}; + get<1>(out)[z] = {init.y}; + } } template @@ -199,8 +295,30 @@ struct OpMinMax template __device__ inline static void opAtomic(OutWrapper out, int z, OutType b) { - cuda::AtomicMin(get<0>(out)[z].x, b.x); - cuda::AtomicMax(get<1>(out)[z].x, b.y); + if constexpr (std::is_floating_point_v) + { + using OI = OrderedInt; + atomicMin(reinterpret_cast(&get<0>(out)[z].x), OI::encode(b.x)); + atomicMax(reinterpret_cast(&get<1>(out)[z].x), OI::encode(b.y)); + } + else + { + cuda::AtomicMin(get<0>(out)[z].x, b.x); + cuda::AtomicMax(get<1>(out)[z].x, b.y); + } + } + + template + __device__ inline static void finalize(OutWrapper out, int z) + { + if constexpr (std::is_floating_point_v) + { + using OI = OrderedInt; + auto *pMin = reinterpret_cast(&get<0>(out)[z].x); + auto *pMax = reinterpret_cast(&get<1>(out)[z].x); + get<0>(out)[z].x = OI::decode(*pMin); + get<1>(out)[z].x = OI::decode(*pMax); + } } }; @@ -332,6 +450,16 @@ __global__ void InitMinMax(OutWrapper out) OP::initFill(out, static_cast(blockIdx.z)); } +// Decodes the ordered-int min/max values back to their floating-point representation after +// FindMinMax (no-op for integer types). Must run before CollectMinMax, which reads the +// min/max value as a float to locate matching pixels. + +template +__global__ void FinalizeMinMax(OutWrapper out) +{ + OP::finalize(out, static_cast(blockIdx.z)); +} + template __global__ __launch_bounds__(BW *BH) void FindMinMax(InWrapper in, int2 size, OutWrapper out) { @@ -595,6 +723,11 @@ inline void RunMinMaxLocForType(cudaStream_t stream, const DataStridedCuda &inDa FindMinMax, BW, BH, TW, TH><<>>(inWrap, inSize, outWrap); + if constexpr (std::is_floating_point_v>>) + { + FinalizeMinMax><<>>(outWrap); + } + CollectMinMax<<>>(inWrap, inSize, outWrap, op); } else if (minValData) @@ -616,6 +749,11 @@ inline void RunMinMaxLocForType(cudaStream_t stream, const DataStridedCuda &inDa FindMinMax, BW, BH, TW, TH><<>>(inWrap, inSize, outWrap); + if constexpr (std::is_floating_point_v>>) + { + FinalizeMinMax><<>>(outWrap); + } + CollectMinMax<<>>(inWrap, inSize, outWrap, op); } else if (maxValData) @@ -637,6 +775,11 @@ inline void RunMinMaxLocForType(cudaStream_t stream, const DataStridedCuda &inDa FindMinMax, BW, BH, TW, TH><<>>(inWrap, inSize, outWrap); + if constexpr (std::is_floating_point_v>>) + { + FinalizeMinMax><<>>(outWrap); + } + CollectMinMax<<>>(inWrap, inSize, outWrap, op); } } @@ -649,10 +792,10 @@ inline void RunMinMaxLocDataOut(cudaStream_t stream, const DataStridedCuda &inDa OptionalTensorDataRef numMinData, OptionalTensorDataRef maxValData, OptionalTensorDataRef maxLocData, OptionalTensorDataRef numMaxData) { - switch (inDataType) + switch (static_cast(inDataType)) { #define NVCV_CASE_MINMAXLOC(DT, T) \ - case nvcv::TYPE_##DT: \ + case static_cast(nvcv::TYPE_##DT): \ RunMinMaxLocForType(stream, inData, minValData, minLocData, numMinData, maxValData, maxLocData, \ numMaxData); \ break @@ -678,18 +821,18 @@ inline void RunMinMaxLocDataOut(cudaStream_t stream, const DataStridedCuda &inDa inline bool DataTypeMatches(nvcv::DataType inDataType, nvcv::DataType valDataType) { bool match = false; - switch (valDataType) + switch (static_cast(valDataType)) { - case nvcv::TYPE_S32: + case static_cast(nvcv::TYPE_S32): match = inDataType == nvcv::TYPE_S32 || inDataType == nvcv::TYPE_S16 || inDataType == nvcv::TYPE_S8; break; - case nvcv::TYPE_U32: + case static_cast(nvcv::TYPE_U32): match = inDataType == nvcv::TYPE_U32 || inDataType == nvcv::TYPE_U16 || inDataType == nvcv::TYPE_U8; break; - case nvcv::TYPE_F32: - case nvcv::TYPE_F64: + case static_cast(nvcv::TYPE_F32): + case static_cast(nvcv::TYPE_F64): match = inDataType == valDataType; break; @@ -769,8 +912,8 @@ inline void RunMinMaxLocDataIn(cudaStream_t stream, const DataStridedCuda &inDat if (!DataTypeMatches(inDataType, minValData->dtype())) { std::ostringstream oss; - oss << "for minVal=" << nvcvDataTypeGetName(minValData->dtype()) - << " for input=" << nvcvDataTypeGetName(inDataType) + oss << "for minVal=" << nvcvDataTypeGetName(static_cast(minValData->dtype())) + << " for input=" << nvcvDataTypeGetName(static_cast(inDataType)) << "; output minVal data type must be S32/U32/F32/F64: for input " << "data type S8/S16 use S32; for U8/U16 use U32; for all other data types use same as input tensor"; throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Wrong data types: %s", oss.str().c_str()); @@ -811,12 +954,13 @@ inline void RunMinMaxLocDataIn(cudaStream_t stream, const DataStridedCuda &inDat throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Output minLoc must have rank 2 or 3 and 2xS32 or 2S32 data type, " "not rank %d and data type %s", - minLocData->rank(), nvcvDataTypeGetName(minLocData->dtype())); + minLocData->rank(), + nvcvDataTypeGetName(static_cast(minLocData->dtype()))); } if (numMinData->dtype() != nvcv::TYPE_S32) { throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Output numMin must have S32 data type, not %s", - nvcvDataTypeGetName(numMinData->dtype())); + nvcvDataTypeGetName(static_cast(numMinData->dtype()))); } } @@ -848,8 +992,8 @@ inline void RunMinMaxLocDataIn(cudaStream_t stream, const DataStridedCuda &inDat if (!DataTypeMatches(inDataType, maxValData->dtype())) { std::ostringstream oss; - oss << "for maxVal=" << nvcvDataTypeGetName(maxValData->dtype()) - << " for input=" << nvcvDataTypeGetName(inDataType) + oss << "for maxVal=" << nvcvDataTypeGetName(static_cast(maxValData->dtype())) + << " for input=" << nvcvDataTypeGetName(static_cast(inDataType)) << "; output maxVal data type must be S32/U32/F32/F64: for input " << "data type S8/S16 use S32; for U8/U16 use U32; for all other data types use same as input tensor"; throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Wrong data types: %s", oss.str().c_str()); @@ -890,12 +1034,13 @@ inline void RunMinMaxLocDataIn(cudaStream_t stream, const DataStridedCuda &inDat throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Output maxLoc must have rank 2 or 3 and 2xS32 or 2S32 data type, " "not rank %d and data type %s", - maxLocData->rank(), nvcvDataTypeGetName(maxLocData->dtype())); + maxLocData->rank(), + nvcvDataTypeGetName(static_cast(maxLocData->dtype()))); } if (numMaxData->dtype() != nvcv::TYPE_S32) { throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Output numMax must have S32 data type, not %s", - nvcvDataTypeGetName(numMaxData->dtype())); + nvcvDataTypeGetName(static_cast(numMaxData->dtype()))); } } @@ -916,6 +1061,15 @@ void MinMaxLoc::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nv const nvcv::Tensor &minLoc, const nvcv::Tensor &numMin, const nvcv::Tensor &maxVal, const nvcv::Tensor &maxLoc, const nvcv::Tensor &numMax) const { + CVCUDA_NVTX_RANGE("cvcuda::MinMaxLoc::operator()[Tensor]"); + const nvcv::TensorLayout inLayout = in.layout(); + if (!(inLayout == nvcv::TENSOR_HWC || inLayout == nvcv::TENSOR_NHWC || inLayout == nvcv::TENSOR_CHW + || inLayout == nvcv::TENSOR_NCHW || inLayout == nvcv::TENSOR_HW || inLayout == nvcv::TENSOR_NHW)) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input tensor layout must be HW, NHW, HWC, NHWC, CHW, or NCHW"); + } + auto inData = in.exportData(); if (!inData) { @@ -936,6 +1090,7 @@ void MinMaxLoc::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape & const nvcv::Tensor &minLoc, const nvcv::Tensor &numMin, const nvcv::Tensor &maxVal, const nvcv::Tensor &maxLoc, const nvcv::Tensor &numMax) const { + CVCUDA_NVTX_RANGE("cvcuda::MinMaxLoc::operator()[ImageBatchVarShape]"); auto inData = in.exportData(stream); if (!inData) { diff --git a/src/cvcuda/priv/OpMorphology.cpp b/src/cvcuda/priv/OpMorphology.cpp index 09f139069..37facb9a5 100644 --- a/src/cvcuda/priv/OpMorphology.cpp +++ b/src/cvcuda/priv/OpMorphology.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,16 +17,159 @@ #include "OpMorphology.hpp" +#include "Nvtx.hpp" #include "legacy/CvCudaLegacy.h" #include "legacy/CvCudaLegacyHelpers.hpp" #include #include +#include + namespace cvcuda::priv { namespace legacy = nvcv::legacy::cuda_op; +namespace { + +std::pair SplitMorphologyPasses(NVCVMorphologyType morphType) +{ + if (morphType == NVCVMorphologyType::NVCV_OPEN) + { + return {NVCVMorphologyType::NVCV_ERODE, NVCVMorphologyType::NVCV_DILATE}; + } + + return {NVCVMorphologyType::NVCV_DILATE, NVCVMorphologyType::NVCV_ERODE}; +} + +void RunTensorDilateErode(legacy::Morphology &legacyOp, cudaStream_t stream, const nvcv::TensorDataStridedCuda &inData, + const nvcv::TensorDataStridedCuda &outData, nvcv::OptionalTensorConstRef workspace, + NVCVMorphologyType morphType, nvcv::Size2D maskSize, int2 anchor, int32_t iteration, + NVCVBorderType borderMode) +{ + if (workspace == nullptr && iteration > 1) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Workspace must be provided for iterations > 1"); + } + + if (workspace == nullptr) + { + NVCV_CHECK_THROW( + legacyOp.infer(inData, outData, morphType, maskSize, anchor, iteration == 0, borderMode, stream)); + return; + } + + auto workspaceData = workspace->get().exportData(); + NVCV_ASSERT(workspaceData); + + const nvcv::TensorDataStridedCuda *iterIn = &inData; + const nvcv::TensorDataStridedCuda *iterOut = (iteration % 2 == 1) ? &outData : &(*workspaceData); + NVCV_CHECK_THROW(legacyOp.infer(*iterIn, *iterOut, morphType, maskSize, anchor, false, borderMode, stream)); + + std::swap(iterIn, iterOut); + iterOut = (iteration % 2 == 0) ? &outData : &(*workspaceData); + + for (int i = 1; i < iteration; ++i) + { + NVCV_CHECK_THROW(legacyOp.infer(*iterIn, *iterOut, morphType, maskSize, anchor, false, borderMode, stream)); + std::swap(iterIn, iterOut); + } +} + +void RunTensorOpenClose(legacy::Morphology &legacyOp, cudaStream_t stream, const nvcv::TensorDataStridedCuda &inData, + const nvcv::TensorDataStridedCuda &outData, nvcv::OptionalTensorConstRef workspace, + NVCVMorphologyType morphType, nvcv::Size2D maskSize, int2 anchor, int32_t iteration, + NVCVBorderType borderMode) +{ + if (workspace == nullptr) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Workspace must be provided for NVCV_CLOSE or NVCV_OPEN"); + } + + auto [first, second] = SplitMorphologyPasses(morphType); + + auto workspaceData = workspace->get().exportData(); + NVCV_ASSERT(workspaceData); + + NVCV_CHECK_THROW( + legacyOp.infer(inData, *workspaceData, first, maskSize, anchor, iteration == 0, borderMode, stream)); + NVCV_CHECK_THROW( + legacyOp.infer(*workspaceData, outData, second, maskSize, anchor, iteration == 0, borderMode, stream)); + + for (int i = 1; i < iteration; ++i) + { + NVCV_CHECK_THROW(legacyOp.infer(outData, *workspaceData, first, maskSize, anchor, false, borderMode, stream)); + NVCV_CHECK_THROW(legacyOp.infer(*workspaceData, outData, second, maskSize, anchor, false, borderMode, stream)); + } +} + +void RunVarShapeDilateErode(legacy::MorphologyVarShape &legacyOp, cudaStream_t stream, + const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, + nvcv::OptionalImageBatchVarShapeConstRef workspace, NVCVMorphologyType morphType, + const nvcv::TensorDataStridedCuda &masksData, + const nvcv::TensorDataStridedCuda &anchorsData, int32_t iteration, + NVCVBorderType borderMode) +{ + if (workspace == nullptr && iteration > 1) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Workspace must be provided for iterations > 1"); + } + + if (workspace == nullptr) + { + NVCV_CHECK_THROW(legacyOp.infer(in, out, morphType, masksData, anchorsData, iteration == 0, borderMode, + iteration > 1, stream)); + return; + } + + NVCV_ASSERT(workspace); + + const nvcv::ImageBatchVarShape *iterIn = ∈ + const nvcv::ImageBatchVarShape *iterOut = (iteration % 2 == 1) ? &out : &workspace->get(); + + NVCV_CHECK_THROW(legacyOp.infer(*iterIn, *iterOut, morphType, masksData, anchorsData, iteration == 0, borderMode, + iteration > 1, stream)); + + std::swap(iterIn, iterOut); + iterOut = (iteration % 2 == 0) ? &out : &workspace->get(); + + for (int i = 1; i < iteration; ++i) + { + NVCV_CHECK_THROW(legacyOp.infer(*iterIn, *iterOut, morphType, masksData, anchorsData, iteration == 0, + borderMode, iteration > 1, stream)); + std::swap(iterIn, iterOut); + } +} + +void RunVarShapeOpenClose(legacy::MorphologyVarShape &legacyOp, cudaStream_t stream, const nvcv::ImageBatchVarShape &in, + const nvcv::ImageBatchVarShape &out, nvcv::OptionalImageBatchVarShapeConstRef workspace, + NVCVMorphologyType morphType, const nvcv::TensorDataStridedCuda &masksData, + const nvcv::TensorDataStridedCuda &anchorsData, int32_t iteration, NVCVBorderType borderMode) +{ + if (workspace == nullptr) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Workspace must be provided for NVCV_CLOSE"); + } + + auto [first, second] = SplitMorphologyPasses(morphType); + + NVCV_CHECK_THROW( + legacyOp.infer(in, *workspace, first, masksData, anchorsData, iteration == 0, borderMode, false, stream)); + NVCV_CHECK_THROW( + legacyOp.infer(*workspace, out, second, masksData, anchorsData, iteration == 0, borderMode, false, stream)); + + for (int i = 1; i < iteration; ++i) + { + NVCV_CHECK_THROW( + legacyOp.infer(out, *workspace, first, masksData, anchorsData, iteration == 0, borderMode, false, stream)); + NVCV_CHECK_THROW( + legacyOp.infer(*workspace, out, second, masksData, anchorsData, iteration == 0, borderMode, false, stream)); + } +} + +} // namespace + Morphology::Morphology() { m_legacyOp = std::make_unique(); @@ -38,6 +181,7 @@ void Morphology::operator()(cudaStream_t stream, const nvcv::Tensor &in, const n nvcv::Size2D mask_size, int2 anchor, int32_t iteration, const NVCVBorderType borderMode) const { + CVCUDA_NVTX_RANGE("cvcuda::Morphology::operator()[Tensor]"); auto inData = in.exportData(); if (inData == nullptr) { @@ -60,76 +204,14 @@ void Morphology::operator()(cudaStream_t stream, const nvcv::Tensor &in, const n { case NVCVMorphologyType::NVCV_DILATE: case NVCVMorphologyType::NVCV_ERODE: - { - if (workspace == nullptr && iteration > 1) - { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, - "Workspace must be provided for iterations > 1"); - } - - if (workspace == nullptr) - { - NVCV_CHECK_THROW(m_legacyOp->infer(*inData, *outData, morph_type, mask_size, anchor, - iteration == 0 ? true : false, borderMode, stream)); - } - else - { - // With a workspace, we can do in-place operation depending on iteration parity - // we want to avoid copying data back and forth so we pick workspace or output tensor - // as the output of the first iteration, then alternate between them in such a way that - // the output will be in the output tensor after the last iteration. - auto workspaceData = workspace->get().exportData(); - NVCV_ASSERT(workspaceData); - - // pick for parity of iteration - nvcv::TensorDataStridedCuda *in = &(*inData); - nvcv::TensorDataStridedCuda *out = (iteration % 2 == 1) ? &(*outData) : &(*workspaceData); - NVCV_CHECK_THROW(m_legacyOp->infer(*in, *out, morph_type, mask_size, anchor, false, borderMode, stream)); - - std::swap(in, out); - out = (iteration % 2 == 0) ? &(*outData) : &(*workspaceData); - - for (int i = 1; i < iteration; ++i) - { - NVCV_CHECK_THROW( - m_legacyOp->infer(*in, *out, morph_type, mask_size, anchor, false, borderMode, stream)); - std::swap(in, out); - } - } - } - break; + RunTensorDilateErode(*m_legacyOp, stream, *inData, *outData, workspace, morph_type, mask_size, anchor, + iteration, borderMode); + break; case NVCVMorphologyType::NVCV_OPEN: case NVCVMorphologyType::NVCV_CLOSE: - { - if (workspace == nullptr) - { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, - "Workspace must be provided for NVCV_CLOSE or NVCV_OPEN"); - } - - // For open/close operations we must have a workspace, as it will be the ouput of the first operation. - // We then alternate between the workspace and the output tensor as the output of the first operation. - NVCVMorphologyType first = (morph_type == NVCVMorphologyType::NVCV_OPEN ? NVCVMorphologyType::NVCV_ERODE - : NVCVMorphologyType::NVCV_DILATE); - NVCVMorphologyType second = (morph_type == NVCVMorphologyType::NVCV_OPEN ? NVCVMorphologyType::NVCV_DILATE - : NVCVMorphologyType::NVCV_ERODE); - - auto workspaceData = workspace->get().exportData(); - NVCV_ASSERT(workspaceData); - - NVCV_CHECK_THROW(m_legacyOp->infer(*inData, *workspaceData, first, mask_size, anchor, - iteration == 0 ? true : false, borderMode, stream)); - NVCV_CHECK_THROW(m_legacyOp->infer(*workspaceData, *outData, second, mask_size, anchor, - iteration == 0 ? true : false, borderMode, stream)); - for (int i = 1; i < iteration; ++i) - { - NVCV_CHECK_THROW( - m_legacyOp->infer(*outData, *workspaceData, first, mask_size, anchor, false, borderMode, stream)); - NVCV_CHECK_THROW( - m_legacyOp->infer(*workspaceData, *outData, second, mask_size, anchor, false, borderMode, stream)); - } + RunTensorOpenClose(*m_legacyOp, stream, *inData, *outData, workspace, morph_type, mask_size, anchor, iteration, + borderMode); break; - } default: throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Wrong morph_type"); } @@ -140,6 +222,7 @@ void Morphology::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape NVCVMorphologyType morph_type, const nvcv::Tensor &masks, const nvcv::Tensor &anchors, int32_t iteration, NVCVBorderType borderMode) const { + CVCUDA_NVTX_RANGE("cvcuda::Morphology::operator()[ImageBatchVarShape]"); auto masksData = masks.exportData(); if (masksData == nullptr) { @@ -160,68 +243,15 @@ void Morphology::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape { case NVCVMorphologyType::NVCV_DILATE: case NVCVMorphologyType::NVCV_ERODE: - { - if (workspace == nullptr && iteration > 1) - { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, - "Workspace must be provided for iterations > 1"); - } - if (workspace == nullptr) - { - NVCV_CHECK_THROW(m_legacyOpVarShape->infer(in, out, morph_type, *masksData, *anchorsData, - iteration == 0 ? true : false, borderMode, stream)); - } - else - { - NVCV_ASSERT(workspace); - // With a workspace, we can do in-place operation depending on iteration parity - // we want to avoid copying data back and forth so we pick workspace or output tensor - // as the output of the first iteration, then alternate between them in such a way that - // the output will be in the output tensor after the last iteration. - const nvcv::ImageBatchVarShape *pIn = ∈ - const nvcv::ImageBatchVarShape *pOut = (iteration % 2 == 1) ? &out : &workspace->get(); - - NVCV_CHECK_THROW(m_legacyOpVarShape->infer(*pIn, *pOut, morph_type, *masksData, *anchorsData, - iteration == 0 ? true : false, borderMode, stream)); - std::swap(pIn, pOut); - pOut = (iteration % 2 == 0) ? &out : &workspace->get(); - for (int i = 1; i < iteration; ++i) - { - NVCV_CHECK_THROW(m_legacyOpVarShape->infer(*pIn, *pOut, morph_type, *masksData, *anchorsData, - iteration == 0 ? true : false, borderMode, stream)); - std::swap(pIn, pOut); - } - } - } - break; + RunVarShapeDilateErode(*m_legacyOpVarShape, stream, in, out, workspace, morph_type, *masksData, *anchorsData, + iteration, borderMode); + break; case NVCVMorphologyType::NVCV_CLOSE: case NVCVMorphologyType::NVCV_OPEN: - { - if (workspace == nullptr) - { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Workspace must be provided for NVCV_CLOSE"); - } - NVCVMorphologyType first = (morph_type == NVCVMorphologyType::NVCV_OPEN ? NVCVMorphologyType::NVCV_ERODE - : NVCVMorphologyType::NVCV_DILATE); - NVCVMorphologyType second = (morph_type == NVCVMorphologyType::NVCV_OPEN ? NVCVMorphologyType::NVCV_DILATE - : NVCVMorphologyType::NVCV_ERODE); - - // For open/close operations we must have a workspace, as it will be the ouput of the first operation. - // We then alternate between the workspace and the output tensor as the output of the first operation. - NVCV_CHECK_THROW(m_legacyOpVarShape->infer(in, *workspace, first, *masksData, *anchorsData, - iteration == 0 ? true : false, borderMode, stream)); - NVCV_CHECK_THROW(m_legacyOpVarShape->infer(*workspace, out, second, *masksData, *anchorsData, - iteration == 0 ? true : false, borderMode, stream)); - for (int i = 1; i < iteration; ++i) - { - NVCV_CHECK_THROW(m_legacyOpVarShape->infer(out, *workspace, first, *masksData, *anchorsData, - iteration == 0 ? true : false, borderMode, stream)); - NVCV_CHECK_THROW(m_legacyOpVarShape->infer(*workspace, out, second, *masksData, *anchorsData, - iteration == 0 ? true : false, borderMode, stream)); - } + RunVarShapeOpenClose(*m_legacyOpVarShape, stream, in, out, workspace, morph_type, *masksData, *anchorsData, + iteration, borderMode); break; - } default: throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Wrong morph_type"); } diff --git a/src/cvcuda/priv/OpMorphology.hpp b/src/cvcuda/priv/OpMorphology.hpp index 551473140..3c991ef66 100644 --- a/src/cvcuda/priv/OpMorphology.hpp +++ b/src/cvcuda/priv/OpMorphology.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -48,7 +48,7 @@ class Morphology final : public IOperator const nvcv::Tensor &masks, const nvcv::Tensor &anchors, int32_t iteration, NVCVBorderType borderMode) const; -protected: +private: std::unique_ptr m_legacyOp; std::unique_ptr m_legacyOpVarShape; }; diff --git a/src/cvcuda/priv/OpNonMaximumSuppression.cu b/src/cvcuda/priv/OpNonMaximumSuppression.cu index 7d1ae495e..5470802a4 100644 --- a/src/cvcuda/priv/OpNonMaximumSuppression.cu +++ b/src/cvcuda/priv/OpNonMaximumSuppression.cu @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,6 +15,7 @@ * limitations under the License. **/ +#include "Nvtx.hpp" #include "OpNonMaximumSuppression.hpp" #include @@ -86,32 +87,31 @@ __global__ void NonMaximumSuppression(cuda::Tensor2DWrap i return; } - const T srcX = inBBoxes[coordX]; - bool discard = false; + const T srcX = inBBoxes[coordX]; +#pragma unroll 2 for (int bboxY = 0; bboxY < numBBoxes; ++bboxY) { - if (bboxX == bboxY) + const int2 coordY{bboxY, batchIdx}; + const float scoreY = inScores[coordY]; + if (scoreY < scoreX) { continue; } - const int2 coordY{bboxY, batchIdx}; - const T srcY = inBBoxes[coordY]; + const T srcY = inBBoxes[coordY]; if (ComputeIoU(srcX, srcY) > iouThreshold) { - const float scoreY = inScores[coordY]; - if (scoreX < scoreY || (scoreX == scoreY && ComputeArea(srcX) < ComputeArea(srcY))) { - discard = true; - break; + dst = 0; + return; } } } - dst = discard ? 0 : 1; + dst = 1; } inline __host__ void RunNonMaximumSuppresion(const nvcv::TensorDataStridedCuda &in, @@ -126,7 +126,7 @@ inline __host__ void RunNonMaximumSuppresion(const nvcv::TensorDataStridedCuda & int numSamples = in.shape(0); int numBBoxes = in.shape(1); - dim3 block(256, 1, 1); + dim3 block(128, 1, 1); dim3 grid((numBBoxes + block.x - 1) / block.x, 1, numSamples); NonMaximumSuppression<<>>(inWrap, outWrap, scoresWrap, numBBoxes, scThresh, iouThresh); @@ -145,6 +145,7 @@ NonMaximumSuppression::NonMaximumSuppression() {} void NonMaximumSuppression::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, const nvcv::Tensor &scores, float scoreThreshold, float iouThreshold) const { + CVCUDA_NVTX_RANGE("cvcuda::NonMaximumSuppression::operator()[Tensor]"); auto inData = in.exportData(); if (!inData) { diff --git a/src/cvcuda/priv/OpNormalize.cpp b/src/cvcuda/priv/OpNormalize.cpp index 5a9e5c211..e99884b20 100644 --- a/src/cvcuda/priv/OpNormalize.cpp +++ b/src/cvcuda/priv/OpNormalize.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "OpNormalize.hpp" +#include "Nvtx.hpp" #include "legacy/CvCudaLegacy.h" #include "legacy/CvCudaLegacyHelpers.hpp" @@ -29,7 +30,8 @@ namespace legacy = nvcv::legacy::cuda_op; Normalize::Normalize() { - legacy::DataShape maxIn, maxOut; + legacy::DataShape maxIn; + legacy::DataShape maxOut; //maxIn/maxOut not used by op. m_legacyOp = std::make_unique(maxIn, maxOut); m_legacyOpVarShape = std::make_unique(maxIn, maxOut); @@ -39,6 +41,7 @@ void Normalize::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nv const nvcv::Tensor &scale, const nvcv::Tensor &out, const float global_scale, const float shift, const float epsilon, const uint32_t flags) const { + CVCUDA_NVTX_RANGE("cvcuda::Normalize::operator()[Tensor]"); auto inData = in.exportData(); if (inData == nullptr) { @@ -71,10 +74,34 @@ void Normalize::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nv m_legacyOp->infer(*inData, *baseData, *scaleData, *outData, global_scale, shift, epsilon, flags, stream)); } +void Normalize::operator()(cudaStream_t stream, const nvcv::Tensor &in, const float4 base, const float4 scale, + const int baseCount, const int scaleCount, const nvcv::Tensor &out, const float global_scale, + const float shift, const float epsilon, const uint32_t flags) const +{ + CVCUDA_NVTX_RANGE("cvcuda::Normalize::operator()[Tensor scalar]"); + auto inData = in.exportData(); + if (inData == nullptr) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input must be cuda-accessible, pitch-linear tensor"); + } + + auto outData = out.exportData(); + if (outData == nullptr) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Output must be cuda-accessible, pitch-linear tensor"); + } + + NVCV_CHECK_THROW(m_legacyOp->infer(*inData, base, scale, baseCount, scaleCount, *outData, global_scale, shift, + epsilon, flags, stream)); +} + void Normalize::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::Tensor &base, const nvcv::Tensor &scale, const nvcv::ImageBatchVarShape &out, const float global_scale, const float shift, const float epsilon, const uint32_t flags) const { + CVCUDA_NVTX_RANGE("cvcuda::Normalize::operator()[ImageBatchVarShape]"); auto inData = in.exportData(stream); if (inData == nullptr) { diff --git a/src/cvcuda/priv/OpNormalize.hpp b/src/cvcuda/priv/OpNormalize.hpp index e9f165cd8..ea6767ddc 100644 --- a/src/cvcuda/priv/OpNormalize.hpp +++ b/src/cvcuda/priv/OpNormalize.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -42,6 +42,10 @@ class Normalize final : public IOperator void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &base, const nvcv::Tensor &scale, const nvcv::Tensor &out, float global_scale, float shift, float epsilon, uint32_t flags) const; + void operator()(cudaStream_t stream, const nvcv::Tensor &in, const float4 base, const float4 scale, int baseCount, + int scaleCount, const nvcv::Tensor &out, float global_scale, float shift, float epsilon, + uint32_t flags) const; + void operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::Tensor &base, const nvcv::Tensor &scale, const nvcv::ImageBatchVarShape &out, float global_scale, float shift, float epsilon, uint32_t flags) const; diff --git a/src/cvcuda/priv/OpOSD.cpp b/src/cvcuda/priv/OpOSD.cpp index 46f212de0..3ba009265 100644 --- a/src/cvcuda/priv/OpOSD.cpp +++ b/src/cvcuda/priv/OpOSD.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,25 +17,183 @@ #include "OpOSD.hpp" +#include "Nvtx.hpp" +#include "OpReformat.hpp" #include "legacy/CvCudaLegacy.h" #include "legacy/CvCudaLegacyHelpers.hpp" +#include #include +#include #include +#include +#include + namespace cvcuda::priv { namespace legacy = nvcv::legacy::cuda_op; +namespace { + +bool IsOSDPlanarLayout(nvcv::TensorLayout layout) +{ + return layout == nvcv::TENSOR_NCHW || layout == nvcv::TENSOR_CHW; +} + +bool IsOSDInterleavedLayout(nvcv::TensorLayout layout) +{ + return layout == nvcv::TENSOR_NHWC || layout == nvcv::TENSOR_HWC; +} + +nvcv::TensorShape InterleavedShapeForOSD(const nvcv::TensorDataStridedCuda &data, const char *tensorName) +{ + auto access = nvcv::TensorDataAccessStridedImagePlanar::Create(data); + if (!access) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "%s must be an image-like planar tensor", + tensorName); + } + + if (data.layout() == nvcv::TENSOR_NCHW) + { + return nvcv::TensorShape{ + {access->numSamples(), access->numRows(), access->numCols(), access->numChannels()}, + "NHWC" + }; + } + if (data.layout() == nvcv::TENSOR_CHW) + { + return nvcv::TensorShape{ + {access->numRows(), access->numCols(), access->numChannels()}, + "HWC" + }; + } + + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input/output must have NCHW or CHW layout for planar OSD adaptation"); +} + +} // namespace + +OSDPlanarBridgeWorkspace::OSDPlanarBridgeWorkspace() +{ + NVCV_CHECK_THROW(cudaEventCreateWithFlags(&m_ready, cudaEventDisableTiming)); +} + +OSDPlanarBridgeWorkspace::~OSDPlanarBridgeWorkspace() +{ + if (m_ready == nullptr) + { + return; + } + if (m_pending) + { + NVCV_CHECK_LOG(cudaEventSynchronize(m_ready)); + } + NVCV_CHECK_LOG(cudaEventDestroy(m_ready)); +} + +std::unique_lock OSDPlanarBridgeWorkspace::lock() +{ + return std::unique_lock{m_mutex}; +} + +OSDPlanarBridgeTensors OSDPlanarBridgeWorkspace::prepare(cudaStream_t stream, const nvcv::Tensor &input, + const nvcv::TensorDataStridedCuda &inputData, + const nvcv::TensorDataStridedCuda &outputData, + Reformat &reformat) +{ + const std::array targetShapes{ + InterleavedShapeForOSD(inputData, "Input"), + InterleavedShapeForOSD(outputData, "Output"), + }; + + synchronizeIfNeeded(stream, targetShapes, inputData.dtype()); + for (int which : {kInputIndex, kOutputIndex}) + { + resizeBuffer(which, targetShapes[which], inputData.dtype()); + } + + reformat(stream, input, m_interleaved[kInputIndex]); + + auto inputBridge = m_interleaved[kInputIndex].exportData(); + auto outputBridge = m_interleaved[kOutputIndex].exportData(); + if (!inputBridge || !outputBridge) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Internal OSD reformat tensors must be cuda-accessible"); + } + + return {*inputBridge, *outputBridge}; +} + +void OSDPlanarBridgeWorkspace::finish(cudaStream_t stream, const nvcv::Tensor &output, Reformat &reformat) +{ + reformat(stream, m_interleaved[kOutputIndex], output); + markPending(stream); +} + +void OSDPlanarBridgeWorkspace::markPending(cudaStream_t stream) +{ + NVCV_CHECK_THROW(cudaEventRecord(m_ready, stream)); + m_pending = true; +} + +void OSDPlanarBridgeWorkspace::synchronizeIfNeeded( + cudaStream_t stream, const std::array &targetShapes, nvcv::DataType dtype) +{ + if (!m_pending) + { + return; + } + + bool shapeOrTypeChanged = false; + for (int which : {kInputIndex, kOutputIndex}) + { + const nvcv::Tensor &tensor = m_interleaved[which]; + shapeOrTypeChanged |= !tensor || tensor.shape() != targetShapes[which] || tensor.dtype() != dtype; + } + + if (shapeOrTypeChanged) + { + NVCV_CHECK_THROW(cudaEventSynchronize(m_ready)); + m_pending = false; + } + else + { + NVCV_CHECK_THROW(cudaStreamWaitEvent(stream, m_ready)); + } +} + +void OSDPlanarBridgeWorkspace::resizeBuffer(int which, const nvcv::TensorShape &shape, nvcv::DataType dtype) +{ + nvcv::Tensor &tensor = m_interleaved[which]; + if (!tensor || tensor.shape() != shape || tensor.dtype() != dtype) + { + tensor = nvcv::Tensor(shape, dtype); + } +} + +std::unique_ptr OSD::CreatePlanarBridgeWorkspace(int) +{ + return std::make_unique(); +} + OSD::OSD() { - legacy::DataShape maxIn, maxOut; //maxIn/maxOut not used by op. - m_legacyOp = std::make_unique(maxIn, maxOut); + legacy::DataShape maxIn; + legacy::DataShape maxOut; //maxIn/maxOut not used by op. + m_legacyOp = std::make_unique(maxIn, maxOut); + m_reformatOp = std::make_unique(); } +OSD::~OSD() = default; + void OSD::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, const NVCVElements &elements) const { + CVCUDA_NVTX_RANGE("cvcuda::OSD::operator()[Tensor]"); auto inData = in.exportData(); if (inData == nullptr) { @@ -50,6 +208,40 @@ void OSD::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Te "Output must be cuda-accessible, pitch-linear tensor"); } + const bool inPlanar = IsOSDPlanarLayout(inData->layout()); + + if (const bool outPlanar = IsOSDPlanarLayout(outData->layout()); inPlanar || outPlanar) + { + if (!inPlanar || !outPlanar || inData->layout() != outData->layout()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input and output layouts must both be NCHW or both be CHW for planar OSD"); + } + + auto &workspace = m_planarWorkspace.get(); + auto lock = workspace.lock(); + OSDPlanarBridgeTensors bridged = workspace.prepare(stream, in, *inData, *outData, *m_reformatOp); + + try + { + NVCV_CHECK_THROW(m_legacyOp->infer(bridged.input, bridged.output, elements, stream)); + workspace.finish(stream, out, *m_reformatOp); + } + catch (...) + { + workspace.markPending(stream); + throw; + } + return; + } + + if (!IsOSDInterleavedLayout(inData->layout()) || !IsOSDInterleavedLayout(outData->layout()) + || inData->layout() != outData->layout()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input and output must both be interleaved and have identical layouts"); + } + NVCV_CHECK_THROW(m_legacyOp->infer(*inData, *outData, elements, stream)); } diff --git a/src/cvcuda/priv/OpOSD.hpp b/src/cvcuda/priv/OpOSD.hpp index d27a6d4ac..dbb339890 100644 --- a/src/cvcuda/priv/OpOSD.hpp +++ b/src/cvcuda/priv/OpOSD.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,24 +25,73 @@ #define CVCUDA_PRIV__O_S_D_HPP #include "IOperator.hpp" +#include "PerDeviceResource.hpp" #include "legacy/CvCudaLegacy.h" +#include #include +#include +#include #include +#include namespace cvcuda::priv { +class Reformat; + +struct OSDPlanarBridgeTensors +{ + nvcv::TensorDataStridedCuda input; + nvcv::TensorDataStridedCuda output; +}; + +class OSDPlanarBridgeWorkspace +{ +public: + OSDPlanarBridgeWorkspace(); + ~OSDPlanarBridgeWorkspace(); + + std::unique_lock lock(); + + OSDPlanarBridgeTensors prepare(cudaStream_t stream, const nvcv::Tensor &input, + const nvcv::TensorDataStridedCuda &inputData, + const nvcv::TensorDataStridedCuda &outputData, Reformat &reformat); + + void finish(cudaStream_t stream, const nvcv::Tensor &output, Reformat &reformat); + void markPending(cudaStream_t stream); + +private: + static constexpr int kInputIndex = 0; + static constexpr int kOutputIndex = 1; + static constexpr int kBridgeTensorCount = 2; + + void synchronizeIfNeeded(cudaStream_t stream, const std::array &targetShapes, + nvcv::DataType dtype); + + void resizeBuffer(int which, const nvcv::TensorShape &shape, nvcv::DataType dtype); + + std::mutex m_mutex; + std::array m_interleaved; + cudaEvent_t m_ready = nullptr; + bool m_pending = false; +}; + class OSD final : public IOperator { public: explicit OSD(); + ~OSD() override; void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, const NVCVElements &elements) const; private: - std::unique_ptr m_legacyOp; + static std::unique_ptr CreatePlanarBridgeWorkspace(int deviceId); + + std::unique_ptr m_legacyOp; + std::unique_ptr m_reformatOp; + mutable PerDeviceResource m_planarWorkspace{CreatePlanarBridgeWorkspace}; }; } // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpPadAndStack.cpp b/src/cvcuda/priv/OpPadAndStack.cpp index 0abf0ae89..61820cc58 100644 --- a/src/cvcuda/priv/OpPadAndStack.cpp +++ b/src/cvcuda/priv/OpPadAndStack.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "OpPadAndStack.hpp" +#include "Nvtx.hpp" #include "legacy/CvCudaLegacy.h" #include "legacy/CvCudaLegacyHelpers.hpp" @@ -29,7 +30,8 @@ namespace legacy = nvcv::legacy::cuda_op; PadAndStack::PadAndStack() { - legacy::DataShape maxIn, maxOut; + legacy::DataShape maxIn; + legacy::DataShape maxOut; //maxIn/maxOut not used by op. m_legacyOp = std::make_unique(maxIn, maxOut); } @@ -38,6 +40,7 @@ void PadAndStack::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape const nvcv::Tensor &top, const nvcv::Tensor &left, const NVCVBorderType borderMode, const float borderValue) const { + CVCUDA_NVTX_RANGE("cvcuda::PadAndStack::operator()[ImageBatchVarShape]"); auto inData = in.exportData(stream); if (inData == nullptr) { diff --git a/src/cvcuda/priv/OpPairwiseMatcher.cu b/src/cvcuda/priv/OpPairwiseMatcher.cu index 9e6af5f6a..52f23e379 100644 --- a/src/cvcuda/priv/OpPairwiseMatcher.cu +++ b/src/cvcuda/priv/OpPairwiseMatcher.cu @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,6 +16,7 @@ */ #include "Assert.h" +#include "Nvtx.hpp" #include "OpPairwiseMatcher.hpp" #include @@ -25,7 +26,8 @@ #include #include -#include +#include +#include #include @@ -60,7 +62,8 @@ template class PointT { public: - static constexpr int kMaxSize = 0; // maximum size in bytes of a single point stored by this class + static constexpr int kMaxSize = 0; // maximum size in bytes of a single point stored by this class + static constexpr bool kPackedHammingWords = false; __device__ PointT() = default; @@ -89,9 +92,10 @@ class PointT static_assert(NB > 0, "Maximum number of bytes capacity in PointT class must be positive"); public: - static constexpr int kMaxSize = NB; // maximum size in bytes of a single point stored by this class - static constexpr int kNumElem = NB / sizeof(RT); // number of elements in array serving as a cache - static constexpr int kMaxDims = NB / sizeof(T); // maximum number of dimensions a single point may have + static constexpr int kMaxSize = NB; // maximum size in bytes of a single point stored by this class + static constexpr int kNumElem = NB / sizeof(RT); // number of elements in array serving as a cache + static constexpr int kMaxDims = NB / sizeof(T); // maximum number of dimensions a single point may have + static constexpr bool kPackedHammingWords = false; __device__ PointT() = default; @@ -113,6 +117,89 @@ private: RT data[kNumElem]; }; +// Keep each word as a separately named value so the compiler can cache an exact 32-byte descriptor in registers while +// computing Hamming distance a word at a time. Indexing a uint32_t array directly makes nvcc spill every cached descriptor. +class PackedU8Point32 +{ +public: + static constexpr int kNumElem = 8; + static constexpr bool kPackedHammingWords = true; + + __device__ PackedU8Point32() = default; + + inline __device__ void load(cuda::Tensor3DWrap set, int sampleIdx, int setIdx, int) + { + const RT *src = reinterpret_cast(set.ptr(sampleIdx, setIdx)); + d0 = src[0]; + d1 = src[1]; + d2 = src[2]; + d3 = src[3]; + d4 = src[4]; + d5 = src[5]; + d6 = src[6]; + d7 = src[7]; + } + + inline __device__ uint8_t operator[](int i) const + { + RT value; + switch (i / 4) + { + case 0: + value = d0; + break; + case 1: + value = d1; + break; + case 2: + value = d2; + break; + case 3: + value = d3; + break; + case 4: + value = d4; + break; + case 5: + value = d5; + break; + case 6: + value = d6; + break; + default: + value = d7; + } + return value >> ((i % 4) * 8); + } + + template + inline __device__ RT word() const + { + static_assert(I < kNumElem); + if constexpr (I == 0) + return d0; + else if constexpr (I == 1) + return d1; + else if constexpr (I == 2) + return d2; + else if constexpr (I == 3) + return d3; + else if constexpr (I == 4) + return d4; + else if constexpr (I == 5) + return d5; + else if constexpr (I == 6) + return d6; + else + return d7; + } + +private: + RT d0, d1, d2, d3, d4, d5, d6, d7; +}; + +static_assert(sizeof(PackedU8Point32) == 32); + // Is compatible checks if a {numDim}-dimensional point fits in the corresponding Point T class (above) template inline __host__ bool isCompatible(int numDim) @@ -172,6 +259,19 @@ inline __device__ void ComputeDistance(float &distance, const T &e1, const T &e2 } } +template +inline __device__ void ComputeHammingWords(float &distance, const Point &p1, const Point &p2, int numWords) +{ + if constexpr (I < Point::kNumElem) + { + if (I < numWords) + { + distance += __popc(p1.template word() ^ p2.template word()); + } + ComputeHammingWords(distance, p1, p2, numWords); + } +} + // Sort pairs of (distance, index) one per thread from a fixed point p1 to all points p2 in set2 with numDim // dimensions, each point is an array with numDim elements of source type ST, each set is an array of points, and // the tensor is an array of sets where the sampleIdx selects the current set within it with set2Size points @@ -191,7 +291,19 @@ inline __device__ void SortKeyValue(float &sortedDist, int &sortedIdx, const Poi curDist = 0.f; - if constexpr (Point::kMaxSize > 0) + if constexpr (NORM == NVCV_NORM_HAMMING && Point::kPackedHammingWords) + { + constexpr int kElemPerWord = sizeof(RT) / sizeof(uint8_t); + int numWords = numDim / kElemPerWord; + + ComputeHammingWords(curDist, p1, p2, numWords); + + for (int i = numWords * kElemPerWord; i < numDim; ++i) + { + ComputeDistance(curDist, p1[i], p2[i]); + } + } + else if constexpr (Point::kMaxSize > 0) { #pragma unroll for (int i = 0; i < Point::kMaxDims && i < numDim; ++i) @@ -272,7 +384,7 @@ inline __device__ void WriteMatch(int matchIdx, int set1Idx, int set2Idx, int sa // Brute-force matcher finds closest pairs of n-dimensional points in set1 and set2, comparing all against all, it // is instantiated by: an upper limit of each point size in bytes; type; and source type -template +template> __global__ void BruteForceMatcher(cuda::Tensor3DWrap set1, cuda::Tensor3DWrap set2, cuda::Tensor1DWrap numSet1, cuda::Tensor1DWrap numSet2, cuda::Tensor3DWrap matches, cuda::Tensor1DWrap numMatches, @@ -302,7 +414,7 @@ __global__ void BruteForceMatcher(cuda::Tensor3DWrap set1, cuda::Tensor3DWra set2Size = set2Size > set2Capacity ? set2Capacity : set2Size; } - PointT p; + Point p; p.load(set1, sampleIdx, set1Idx, numDim); @@ -372,8 +484,10 @@ inline void RunBruteForceMatcherForNorm(cudaStream_t stream, const nvcv::Tensor const nvcv::Tensor &matches, const nvcv::Tensor &numMatches, const nvcv::Tensor &distances, bool crossCheck, int matchesPerPoint) { - cuda::Tensor3DWrap w_set1, w_set2; // tensor wraps of set1 and set2 and other tensors - cuda::Tensor1DWrap w_numSet1, w_numSet2; + cuda::Tensor3DWrap w_set1; // tensor wraps of set1 and set2 and other tensors + cuda::Tensor3DWrap w_set2; + cuda::Tensor1DWrap w_numSet1; + cuda::Tensor1DWrap w_numSet2; cuda::Tensor3DWrap w_matches; cuda::Tensor1DWrap w_numMatches; cuda::Tensor2DWrap w_distances; @@ -451,6 +565,16 @@ inline void RunBruteForceMatcherForNorm(cudaStream_t stream, const nvcv::Tensor { if (isCompatible(numDim)) { + if constexpr (NORM == NVCV_NORM_HAMMING && std::is_same_v) + { + if (numDim == 32) + { + BruteForceMatcher<32, NORM, const SrcT, PackedU8Point32><<>>( + w_set1, w_set2, w_numSet1, w_numSet2, w_matches, w_numMatches, w_distances, set1Capacity, + set2Capacity, outCapacity, numDim, crossCheck, matchesPerPoint); + return; + } + } CVCUDA_BFM_RUN(32); } else if (isCompatible(numDim)) @@ -506,10 +630,10 @@ inline void RunBruteForceMatcher(cudaStream_t stream, const nvcv::Tensor &set1, const nvcv::Tensor &numMatches, const nvcv::Tensor &distances, bool crossCheck, int matchesPerPoint, NVCVNormType normType) { - switch (set1.dtype()) + switch (static_cast(set1.dtype())) { #define CVCUDA_BFM_CASE(DT, T) \ - case nvcv::TYPE_##DT: \ + case static_cast(nvcv::TYPE_##DT): \ RunBruteForceMatcherForType(stream, set1, set2, numSet1, numSet2, matches, numMatches, distances, \ crossCheck, matchesPerPoint, normType); \ break @@ -548,6 +672,7 @@ void PairwiseMatcher::operator()(cudaStream_t stream, const nvcv::Tensor &set1, const nvcv::Tensor &numMatches, const nvcv::Tensor &distances, bool crossCheck, int matchesPerPoint, NVCVNormType normType) { + CVCUDA_NVTX_RANGE("cvcuda::PairwiseMatcher::operator()[Tensor]"); // Check each input and output tensor and their properties are conforming to what is expected if (!set1 || !set2 || !matches) diff --git a/src/cvcuda/priv/OpPillowResize.cpp b/src/cvcuda/priv/OpPillowResize.cpp index 0625eb74f..99b407d4c 100644 --- a/src/cvcuda/priv/OpPillowResize.cpp +++ b/src/cvcuda/priv/OpPillowResize.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,13 +16,17 @@ */ #include "OpPillowResize.hpp" +#include "Nvtx.hpp" #include "legacy/CvCudaLegacy.h" #include "legacy/CvCudaLegacyHelpers.hpp" #include #include +#include #include +#include + namespace cvcuda::priv { namespace leg = nvcv::legacy; @@ -37,7 +41,8 @@ PillowResize::PillowResize() WorkspaceRequirements PillowResize::getWorkspaceRequirements(int batchSize, const nvcv::Size2D *in_sizes, const nvcv::Size2D *out_sizes, NVCVImageFormat fmt) { - nvcv::Size2D maxInSize{0, 0}, maxOutSize{0, 0}; + nvcv::Size2D maxInSize{0, 0}; + nvcv::Size2D maxOutSize{0, 0}; for (int i = 0; i < batchSize; i++) { maxInSize = nvcv::MaxSize(in_sizes[i], maxInSize); @@ -49,13 +54,13 @@ WorkspaceRequirements PillowResize::getWorkspaceRequirements(int batchSize, cons WorkspaceRequirements PillowResize::getWorkspaceRequirements(int maxBatchSize, nvcv::Size2D maxInSize, nvcv::Size2D maxOutSize, NVCVImageFormat fmt) { - int32_t bpc[4]; - nvcvImageFormatGetBitsPerChannel(fmt, bpc); + std::array bpc; + nvcvImageFormatGetBitsPerChannel(fmt, bpc.data()); int32_t maxChannel = 0; nvcvImageFormatGetNumChannels(fmt, &maxChannel); NVCVDataKind dataKind; nvcvImageFormatGetDataKind(fmt, &dataKind); - nvcv::DataKind dkind = static_cast(dataKind); + auto dkind = static_cast(dataKind); leg::cuda_op::DataType dataType = leg::helpers::GetLegacyDataType(bpc[0], dkind); leg::cuda_op::DataShape maxIn(maxBatchSize, maxChannel, maxInSize.h, maxInSize.w); leg::cuda_op::DataShape maxOut(maxBatchSize, maxChannel, maxOutSize.h, maxOutSize.w); @@ -68,6 +73,7 @@ WorkspaceRequirements PillowResize::getWorkspaceRequirements(int maxBatchSize, n void PillowResize::operator()(cudaStream_t stream, const Workspace &ws, const nvcv::Tensor &in, const nvcv::Tensor &out, const NVCVInterpolationType interpolation) const { + CVCUDA_NVTX_RANGE("cvcuda::PillowResize::operator()[Tensor]"); auto inData = in.exportData(); if (inData == nullptr) { @@ -82,12 +88,47 @@ void PillowResize::operator()(cudaStream_t stream, const Workspace &ws, const nv "Output must be device-acessible, pitch-linear tensor"); } + if (const nvcv::TensorLayout layout = inData->layout(); layout == nvcv::TENSOR_NCHW || layout == nvcv::TENSOR_CHW) + { + if (outData->layout() != layout) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input and output tensors must have the same layout"); + } + + auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*inData); + auto outAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*outData); + if (!inAccess || !outAccess) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible tensor layout"); + } + + if (inAccess->numChannels() != outAccess->numChannels()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input and output tensors must have the same number of channels"); + } + // 2-channel planar is unsupported (matches Normalize and the var-shape planar path); 1, 3 and + // 4 channel planes are valid. See .agents/guidance/PLANAR_GUIDELINES.md. + if (inAccess->numChannels() < 1 || inAccess->numChannels() > 4 || inAccess->numChannels() == 2) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Invalid number of channels"); + } + + // The legacy op resizes planar (NCHW/CHW) tensors natively: one grid z-slice per sample, with + // the C channel planes looped inside the kernel so the per-output-pixel filter setup is shared + // across channels. Plane strides are honored, so there is no tight-packing requirement. + NVCV_CHECK_THROW(m_legacyOp->infer(*inData, *outData, interpolation, stream, ws)); + return; + } + NVCV_CHECK_THROW(m_legacyOp->infer(*inData, *outData, interpolation, stream, ws)); } void PillowResize::operator()(cudaStream_t stream, const Workspace &ws, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, const NVCVInterpolationType interpolation) const { + CVCUDA_NVTX_RANGE("cvcuda::PillowResize::operator()[ImageBatchVarShape]"); NVCV_CHECK_THROW(m_legacyOpVarShape->infer(in, out, interpolation, stream, ws)); } diff --git a/src/cvcuda/priv/OpPosterize.cu b/src/cvcuda/priv/OpPosterize.cu new file mode 100644 index 000000000..52d575947 --- /dev/null +++ b/src/cvcuda/priv/OpPosterize.cu @@ -0,0 +1,683 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "Nvtx.hpp" +#include "OpPosterize.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace cuda = nvcv::cuda; +namespace util = nvcv::util; + +namespace { + +// out = in & mask, per channel component (mask keeps the top `bits` bits of each value). +template +inline __device__ T PosterizeElem(T pixel, BT mask) +{ + static constexpr int numChannels = cuda::NumElements; + T out{}; +#pragma unroll + for (int c = 0; c < numChannels; ++c) + { + cuda::GetElement(out, c) = static_cast(cuda::GetElement(pixel, c) & mask); + } + return out; +} + +template +inline __device__ std::conditional_t GetCoordForLayout(int3 nhwCoord, int p) +{ + if constexpr (!IsPlanar) + { + return nhwCoord; + } + else + { + return {nhwCoord.x, nhwCoord.y, p, nhwCoord.z}; + } +} + +template +inline __device__ void DoPosterize(SrcWrapper src, DstWrapper dst, const int2 size, const int p, BT mask) +{ + using SrcT = std::remove_const_t; + using DstT = typename DstWrapper::ValueType; + static constexpr int numChannels = cuda::NumElements; + static_assert(numChannels == cuda::NumElements); + static_assert(!IsPlanar || numChannels == 1); + + int3 nhwCoord = cuda::StaticCast(blockIdx * blockDim + threadIdx); + if (nhwCoord.x >= size.x || nhwCoord.y >= size.y) + { + return; + } + auto coord = GetCoordForLayout(nhwCoord, p); + dst[coord] = PosterizeElem(src[coord], mask); +} + +// Posterize kernel ----------------------------------------------------------------------- + +// Tensor variant +template +__global__ void Posterize(SrcWrapper src, DstWrapper dst, int2 size, int numPlanes, BT mask) +{ + assert(isPlanar || numPlanes == 1); + if constexpr (!isPlanar) + { + DoPosterize(src, dst, size, 0, mask); + } + else + { + for (int p = 0; p < numPlanes; p++) + { + DoPosterize(src, dst, size, p, mask); + } + } +} + +// VarShape variant +template +__global__ void Posterize(SrcWrapper src, DstWrapper dst, int numPlanes, BT mask) +{ + assert(isPlanar || numPlanes == 1); + int z = blockIdx.z; + int2 size{dst.width(z), dst.height(z)}; + + if constexpr (!isPlanar) + { + DoPosterize(src, dst, size, 0, mask); + } + else + { + for (int p = 0; p < numPlanes; p++) + { + DoPosterize(src, dst, size, p, mask); + } + } +} + +// Compute the posterize mask for a base type given bits-to-keep. Uses a wide intermediate so the +// shift never hits the type-width UB (bits == 0 -> mask 0; bits == W -> all ones). +template +inline BT PosterizeMask(int bits) +{ + constexpr int W = static_cast(sizeof(BT) * 8); + uint32_t m; + if (bits <= 0) + { + m = 0u; + } + else if (bits >= W) + { + m = ~0u; + } + else + { + m = ~((1u << (W - bits)) - 1u); + } + return static_cast(m); +} + +// Vectorized planar / 1-channel kernels -------------------------------------------------- +// +// The scalar kernels above move one element/thread/plane, leaving the planar and 1-channel-interleaved +// paths memory-latency bound (long-scoreboard stalls, low BWUtil) -- each warp has a single outstanding +// load. These map each (sample, plane) to grid.z and have each thread issue NGROUP wide vector loads +// (uchar4 / ushort4) before compute, raising memory-level parallelism. Pure bitwise (in & mask), so no +// extra compute; per element bit-identical to PosterizeElem. Modeled on legacy/normalize_planar.cuh; +// caller guards sizeof(Vec4)-aligned base+strides with a scalar fallback; per-thread tail handles width%4. +template +struct PosterizeVec4Type; + +template +struct PosterizeVec4Type +{ + using type = uchar4; +}; + +template +struct PosterizeVec4Type +{ + using type = ushort4; +}; + +template +__global__ void PosterizePlanarVec4Kernel(cuda::Tensor4DWrap src, + cuda::Tensor4DWrap dst, int4 inout_size, BT mask) +{ + const int g0 = blockIdx.x * blockDim.x * NGROUP + threadIdx.x; + const int src_y = blockIdx.y * blockDim.y + threadIdx.y; + const int nc = blockIdx.z; + const int batch = nc / inout_size.y; + const int channel = nc % inout_size.y; + const int width = inout_size.w; + + if (g0 * 4 >= width || src_y >= inout_size.z) + { + return; + } + + using Vec4 = typename PosterizeVec4Type::type; + + int cx[NGROUP]; + bool full[NGROUP]; + Vec4 in4[NGROUP]; +#pragma unroll + for (int i = 0; i < NGROUP; ++i) + { + cx[i] = (g0 + i * blockDim.x) * 4; + full[i] = cx[i] + 4 <= width; + if (full[i]) + { + in4[i] = *reinterpret_cast(src.ptr(batch, channel, src_y, cx[i])); + } + } + +#pragma unroll + for (int i = 0; i < NGROUP; ++i) + { + if (full[i]) + { + Vec4 out4; + out4.x = static_cast(in4[i].x & mask); + out4.y = static_cast(in4[i].y & mask); + out4.z = static_cast(in4[i].z & mask); + out4.w = static_cast(in4[i].w & mask); + *reinterpret_cast(dst.ptr(batch, channel, src_y, cx[i])) = out4; + } + else if (cx[i] < width) + { + for (int x = cx[i]; x < width; ++x) + { + *dst.ptr(batch, channel, src_y, x) = static_cast(*src.ptr(batch, channel, src_y, x) & mask); + } + } + } +} + +template +__global__ void PosterizePlanarVarShapeVec4Kernel(cuda::ImageBatchVarShapeWrap src, + cuda::ImageBatchVarShapeWrap dst, int num_channels, BT mask) +{ + const int g0 = blockIdx.x * blockDim.x * NGROUP + threadIdx.x; + const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int nc = blockIdx.z; + const int batch = nc / num_channels; + const int channel = nc % num_channels; + const int width = dst.width(batch, channel); + + if (g0 * 4 >= width || dst_y >= dst.height(batch, channel)) + { + return; + } + + using Vec4 = uchar4; // 1-byte planes only (caller guards sizeof(BT) == 1) + + int cx[NGROUP]; + bool full[NGROUP]; + Vec4 in4[NGROUP]; +#pragma unroll + for (int i = 0; i < NGROUP; ++i) + { + cx[i] = (g0 + i * blockDim.x) * 4; + full[i] = cx[i] + 4 <= width; + if (full[i]) + { + in4[i] = *reinterpret_cast(src.ptr(batch, channel, dst_y, cx[i])); + } + } + +#pragma unroll + for (int i = 0; i < NGROUP; ++i) + { + if (full[i]) + { + Vec4 out4; + out4.x = static_cast(in4[i].x & mask); + out4.y = static_cast(in4[i].y & mask); + out4.z = static_cast(in4[i].z & mask); + out4.w = static_cast(in4[i].w & mask); + *reinterpret_cast(dst.ptr(batch, channel, dst_y, cx[i])) = out4; + } + else if (cx[i] < width) + { + for (int x = cx[i]; x < width; ++x) + { + *dst.ptr(batch, channel, dst_y, x) = static_cast(*src.ptr(batch, channel, dst_y, x) & mask); + } + } + } +} + +// Run Posterize kernel ------------------------------------------------------------------- + +template +inline void RunPosterize(cudaStream_t stream, const SrcData &srcData, const DstData &dstData, int bits) +{ + using BT = cuda::BaseType; + constexpr int W = static_cast(sizeof(BT) * 8); + if (bits < 0 || bits > W) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Posterize 'bits' must be in [0, %d] for this data type", W); + } + const BT mask = PosterizeMask(bits); + + dim3 block(32, 4, 1); + if constexpr (std::is_same_v) + { + auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(srcData); + auto dstAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(dstData); + int2 size = cuda::StaticCast(long2{srcAccess->numCols(), srcAccess->numRows()}); + + // Each sample maps to one grid.z block (planar channels looped inside); grid.z cap 65535. + if (srcAccess->numSamples() > 65535) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Batch size exceeds the CUDA grid.z limit of 65535"); + } + dim3 grid(util::DivUp(size.x, block.x), util::DivUp(size.y, block.y), srcAccess->numSamples()); + + int64_t inMaxStride = srcAccess->sampleStride() * srcAccess->numSamples(); + int64_t outMaxStride = dstAccess->sampleStride() * dstAccess->numSamples(); + if (std::max(inMaxStride, outMaxStride) > cuda::TypeTraits::max) + { + throw nvcv::Exception(nvcv::Status::ERROR_OVERFLOW, "Input or output size exceeds %d. Tensor is too large.", + cuda::TypeTraits::max); + } + using StrideType = int32_t; + + if constexpr (!isPlanar) + { + bool launchedVec = false; + // 1-channel interleaved (U16) is byte-identical to a single plane and latency-bound; route + // it through the vectorized planar kernel (C == 1). Multi-channel interleaved (uchar3/ + // uchar4) already moves 3-4 B/thread at the bandwidth ridge -> keep the scalar kernel. + if constexpr (cuda::NumElements == 1) + { + using Vec4 = typename PosterizeVec4Type::type; + constexpr int NGROUP = 4; + const int64_t sStride = srcAccess->sampleStride(), rStride = srcAccess->rowStride(); + const int64_t dsStride = dstAccess->sampleStride(), drStride = dstAccess->rowStride(); + const bool aligned = reinterpret_cast(srcData.basePtr()) % sizeof(Vec4) == 0 + && reinterpret_cast(dstData.basePtr()) % sizeof(Vec4) == 0 + && sStride % sizeof(Vec4) == 0 && rStride % sizeof(Vec4) == 0 + && dsStride % sizeof(Vec4) == 0 && drStride % sizeof(Vec4) == 0; + if (aligned) + { + auto srcV = cuda::Tensor4DWrap(srcData.basePtr(), static_cast(sStride), + static_cast(sStride), + static_cast(rStride)); + auto dstV + = cuda::Tensor4DWrap(dstData.basePtr(), static_cast(dsStride), + static_cast(dsStride), static_cast(drStride)); + dim3 vgrid(util::DivUp(util::DivUp(size.x, 4), static_cast(block.x) * NGROUP), + util::DivUp(size.y, block.y), srcAccess->numSamples()); + PosterizePlanarVec4Kernel<<>>( + srcV, dstV, int4{static_cast(srcAccess->numSamples()), 1, size.y, size.x}, mask); + launchedVec = true; + } + } + if (!launchedVec) + { + auto src = cuda::CreateTensorWrapNHW(srcData); + auto dst = cuda::CreateTensorWrapNHW(dstData); + Posterize<<>>(src, dst, size, 1, mask); + } + } + else + { + const int numPlanes = srcAccess->numPlanes(); + const int numSamples = static_cast(srcAccess->numSamples()); + bool launchedVec = false; + if constexpr (sizeof(BT) == 1 || sizeof(BT) == 2) + { + using Vec4 = typename PosterizeVec4Type::type; + constexpr int NGROUP = 4; + const int64_t planes = static_cast(numSamples) * numPlanes; + const int64_t sStride = srcAccess->sampleStride(), pStride = srcAccess->planeStride(), + rStride = srcAccess->rowStride(); + const int64_t dsStride = dstAccess->sampleStride(), dpStride = dstAccess->planeStride(), + drStride = dstAccess->rowStride(); + const bool aligned + = planes <= 65535 && reinterpret_cast(srcData.basePtr()) % sizeof(Vec4) == 0 + && reinterpret_cast(dstData.basePtr()) % sizeof(Vec4) == 0 && sStride % sizeof(Vec4) == 0 + && pStride % sizeof(Vec4) == 0 && rStride % sizeof(Vec4) == 0 && dsStride % sizeof(Vec4) == 0 + && dpStride % sizeof(Vec4) == 0 && drStride % sizeof(Vec4) == 0; + if (aligned) + { + auto srcV = cuda::Tensor4DWrap(srcData.basePtr(), static_cast(sStride), + static_cast(pStride), + static_cast(rStride)); + auto dstV + = cuda::Tensor4DWrap(dstData.basePtr(), static_cast(dsStride), + static_cast(dpStride), static_cast(drStride)); + dim3 vgrid(util::DivUp(util::DivUp(size.x, 4), static_cast(block.x) * NGROUP), + util::DivUp(size.y, block.y), static_cast(planes)); + PosterizePlanarVec4Kernel + <<>>(srcV, dstV, int4{numSamples, numPlanes, size.y, size.x}, mask); + launchedVec = true; + } + } + if (!launchedVec) + { + auto src = cuda::Tensor4DWrap( + srcData.basePtr(), static_cast(srcAccess->sampleStride()), + static_cast(srcAccess->planeStride()), static_cast(srcAccess->rowStride())); + auto dst = cuda::Tensor4DWrap( + dstData.basePtr(), static_cast(dstAccess->sampleStride()), + static_cast(dstAccess->planeStride()), static_cast(dstAccess->rowStride())); + Posterize<<>>(src, dst, size, numPlanes, mask); + } + } + NVCV_CHECK_THROW(cudaGetLastError()); + } + else + { + static_assert(std::is_same_v); + if (dstData.numImages() > 65535) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Batch size exceeds the CUDA grid.z limit of 65535"); + } + int3 dstMaxSize{dstData.maxSize().w, dstData.maxSize().h, dstData.numImages()}; + dim3 grid(util::DivUp(dstMaxSize.x, block.x), util::DivUp(dstMaxSize.y, block.y), dstMaxSize.z); + + const int numPlanes = dstData.uniqueFormat().numPlanes(); + + // Vectorized planar var-shape path for 1-byte planes (uchar4; NVCV row pitch is >= 4-byte + // aligned). u16 var-shape stays scalar (ushort4 8-byte alignment not guaranteed on var-shape). + if constexpr (isPlanar && sizeof(BT) == 1) + { + constexpr int NGROUP = 4; + const int64_t planes = static_cast(dstData.numImages()) * numPlanes; + if (planes <= 65535) + { + cuda::ImageBatchVarShapeWrap srcV(srcData); + cuda::ImageBatchVarShapeWrap dstV(dstData); + dim3 vgrid(util::DivUp(util::DivUp(dstMaxSize.x, 4), static_cast(block.x) * NGROUP), + util::DivUp(dstMaxSize.y, block.y), static_cast(planes)); + PosterizePlanarVarShapeVec4Kernel<<>>(srcV, dstV, numPlanes, mask); + NVCV_CHECK_THROW(cudaGetLastError()); + return; + } + } + + cuda::ImageBatchVarShapeWrap src(srcData); + cuda::ImageBatchVarShapeWrap dst(dstData); + Posterize<<>>(src, dst, numPlanes, mask); + NVCV_CHECK_THROW(cudaGetLastError()); + } +} + +// Dispatch over base data type (u8 / u16 only — integer) and channel count (1 / 3 / 4) --- + +template +inline void RunTypeSwitch(nvcv::DataType dType, const Cb &cb) +{ + using uchar = unsigned char; + using ushort = unsigned short; + +#define NVCV_POSTERIZE_RUN_TYPED(DYN_BASE_TYPE, STATIC_BASE_TYPE) \ + ((dType == nvcv::TYPE_4##DYN_BASE_TYPE) || (dType == nvcv::TYPE_3##DYN_BASE_TYPE) \ + || (dType == nvcv::TYPE_2##DYN_BASE_TYPE) || (dType == nvcv::TYPE_##DYN_BASE_TYPE)) cb(STATIC_BASE_TYPE{}); + + // clang-format off + if NVCV_POSTERIZE_RUN_TYPED(U8, uchar) + else if NVCV_POSTERIZE_RUN_TYPED(U16, ushort) + else + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Invalid data type: Posterize supports 8-bit and 16-bit unsigned integers only"); + } + // clang-format on + +#undef NVCV_POSTERIZE_RUN_TYPED +} + +template +inline void RunChannelSwitch(int numChannels, int numPlanes, nvcv::DataType dType, const Cb &cb) +{ + RunTypeSwitch(dType, + [&numChannels, &numPlanes, &cb](auto dummyVal) + { + using ValBase = decltype(dummyVal); + // clang-format off + if (numChannels == 1) + { + using Val = cuda::MakeType; + if (numPlanes == 1) + { + cb(Val{}, std::integral_constant{}); + } + else + { + cb(Val{}, std::integral_constant{}); + } + } + else if (numChannels == 3) + { + cb(cuda::MakeType{}, std::integral_constant{}); + } + else if (numChannels == 4) + { + cb(cuda::MakeType{}, std::integral_constant{}); + } + else + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Invalid number of channels: Posterize supports 1, 3 or 4 channels"); + } + // clang-format on + }); +} + +// Validation ------------------------------------------------------------------------------ + +inline void ValidateSrcDstTensors(int &numInterleavedChannels, int &numPlanes, nvcv::DataType &dtype, + const nvcv::Optional &srcData, + const nvcv::Optional &dstData) +{ + if (!srcData) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input must be cuda-accessible, pitch-linear tensor"); + } + if (!dstData) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Output must be cuda-accessible, pitch-linear tensor"); + } + if (srcData->layout() != dstData->layout()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input and output must have the same layout"); + } + if (!(srcData->layout() == nvcv::TENSOR_HWC || srcData->layout() == nvcv::TENSOR_NHWC + || srcData->layout() == nvcv::TENSOR_CHW || srcData->layout() == nvcv::TENSOR_NCHW)) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input must have (N)HWC or (N)CHW layout"); + } + if (srcData->dtype() != dstData->dtype()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input and output must have the same data type"); + } + + auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcData); + auto dstAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*dstData); + NVCV_ASSERT(srcAccess && dstAccess); + + if (srcAccess->numSamples() != dstAccess->numSamples()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of samples"); + } + + int numChannels = srcAccess->numChannels(); + if (numChannels != dstAccess->numChannels()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of channels"); + } + + numPlanes = srcAccess->numPlanes(); + if (numPlanes != dstAccess->numPlanes()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of planes"); + } + if (numPlanes > 1 && numChannels == 2) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "2-channel planar images are not supported"); + } + + if (srcAccess->numCols() != dstAccess->numCols() || srcAccess->numRows() != dstAccess->numRows()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input and output must have matching width and height"); + } + + dtype = srcData->dtype(); + numInterleavedChannels = srcAccess->infoLayout().isChannelLast() ? numChannels : 1; +} + +inline auto ValidateSrcDstVarBatch(int &numInterleavedChannels, int &numPlanes, nvcv::DataType &dtype, + cudaStream_t stream, const nvcv::ImageBatchVarShape &src, + const nvcv::ImageBatchVarShape &dst) +{ + using maybeVarShape = nvcv::Optional; + std::tuple srcDstData{ + src.exportData(stream), + dst.exportData(stream)}; + auto &[srcData, dstData] = srcDstData; + + if (!srcData) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input must be cuda-accessible, varshape pitch-linear image batch"); + } + if (!dstData) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Output must be cuda-accessible, varshape pitch-linear image batch"); + } + + int numSamples = srcData->numImages(); + if (numSamples != dstData->numImages()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of samples"); + } + + const auto &srcFormat = srcData->uniqueFormat(); + const auto &dstFormat = dstData->uniqueFormat(); + if (!srcFormat || !dstFormat) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "All images in a batch must have the same format"); + } + if (srcFormat != dstFormat) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input and output must have the same format"); + } + + int numChannels = srcFormat.numChannels(); + numPlanes = srcFormat.numPlanes(); + if (numPlanes > 1 && numChannels == 2) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "2-channel planar images are not supported"); + } + + dtype = srcFormat.planeDataType(0); + for (int i = 1; i < numPlanes; ++i) + { + if (dtype != srcFormat.planeDataType(i)) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "All planes in the input image must have the same data type"); + } + } + + numInterleavedChannels = dtype.numChannels(); + + for (int i = 0; i < numSamples; i++) + { + if (src[i].size() != dst[i].size()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input and output must have matching width and height"); + } + } + + return srcDstData; +} + +} // anonymous namespace + +namespace cvcuda::priv { + +Posterize::Posterize() {} + +// Tensor input variant +void Posterize::operator()(cudaStream_t stream, const nvcv::Tensor &src, const nvcv::Tensor &dst, int32_t bits) const +{ + CVCUDA_NVTX_RANGE("cvcuda::Posterize::operator()[Tensor]"); + int numInterleavedChannels; + int numPlanes; + nvcv::DataType dtype; + auto srcData = src.exportData(); + auto dstData = dst.exportData(); + ValidateSrcDstTensors(numInterleavedChannels, numPlanes, dtype, srcData, dstData); + + RunChannelSwitch(numInterleavedChannels, numPlanes, dtype, + [&stream, &srcData, &dstData, bits](auto dummyVal, auto isPlanar) + { + using ValueT = decltype(dummyVal); + using IsPlanar = decltype(isPlanar); + RunPosterize(stream, *srcData, *dstData, bits); + }); +} + +// VarShape input variant +void Posterize::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &src, + const nvcv::ImageBatchVarShape &dst, int32_t bits) const +{ + CVCUDA_NVTX_RANGE("cvcuda::Posterize::operator()[ImageBatchVarShape]"); + int numInterleavedChannels; + int numPlanes; + nvcv::DataType dtype; + auto srcDstData = ValidateSrcDstVarBatch(numInterleavedChannels, numPlanes, dtype, stream, src, dst); + + RunChannelSwitch(numInterleavedChannels, numPlanes, dtype, + [&stream, &srcDstData, bits](auto dummyVal, auto isPlanar) + { + using ValueT = decltype(dummyVal); + using IsPlanar = decltype(isPlanar); + auto &[srcData, dstData] = srcDstData; + RunPosterize(stream, *srcData, *dstData, bits); + }); +} + +} // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpPosterize.hpp b/src/cvcuda/priv/OpPosterize.hpp new file mode 100644 index 000000000..b14759abf --- /dev/null +++ b/src/cvcuda/priv/OpPosterize.hpp @@ -0,0 +1,48 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +/** + * @file OpPosterize.hpp + * + * @brief Defines the private C++ Class for the Posterize operation. + */ + +#ifndef CVCUDA_PRIV__POSTERIZE_HPP +#define CVCUDA_PRIV__POSTERIZE_HPP + +#include "IOperator.hpp" + +#include +#include +#include + +namespace cvcuda::priv { + +class Posterize final : public IOperator +{ +public: + explicit Posterize(); + + void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, int32_t bits) const; + + void operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, + int32_t bits) const; +}; + +} // namespace cvcuda::priv + +#endif // CVCUDA_PRIV__POSTERIZE_HPP diff --git a/src/cvcuda/priv/OpRandomResizedCrop.cpp b/src/cvcuda/priv/OpRandomResizedCrop.cpp index fb1cb41a7..522e906f1 100644 --- a/src/cvcuda/priv/OpRandomResizedCrop.cpp +++ b/src/cvcuda/priv/OpRandomResizedCrop.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "OpRandomResizedCrop.hpp" +#include "Nvtx.hpp" #include "legacy/CvCudaLegacy.h" #include "legacy/CvCudaLegacyHelpers.hpp" @@ -29,18 +30,35 @@ namespace legacy = nvcv::legacy::cuda_op; RandomResizedCrop::RandomResizedCrop(double minScale, double maxScale, double minRatio, double maxRatio, int32_t maxBatchSize, uint32_t seed) + // Legacy operators are single-device by design. PerDeviceResource creates + // one instance per CUDA device for transparent multi-GPU support. + : m_legacyOp( + [minScale, maxScale, minRatio, maxRatio, maxBatchSize, seed](int) + { + legacy::DataShape maxIn; + legacy::DataShape maxOut; + return std::make_unique(maxIn, maxOut, minScale, maxScale, minRatio, maxRatio, + maxBatchSize, seed); + }) + , m_legacyOpVarShape( + [minScale, maxScale, minRatio, maxRatio, maxBatchSize, seed](int) + { + legacy::DataShape maxIn; + legacy::DataShape maxOut; + return std::make_unique(maxIn, maxOut, minScale, maxScale, minRatio, + maxRatio, maxBatchSize, seed); + }) { - legacy::DataShape maxIn, maxOut; - // maxIn/maxOut not used by op. - m_legacyOp = std::make_unique(maxIn, maxOut, minScale, maxScale, minRatio, maxRatio, - maxBatchSize, seed); - m_legacyOpVarShape = std::make_unique(maxIn, maxOut, minScale, maxScale, - minRatio, maxRatio, maxBatchSize, seed); + if (minScale > maxScale || minRatio > maxRatio) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "scale and ratio must satisfy min <= max"); + } } void RandomResizedCrop::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, const NVCVInterpolationType interpolation) const { + CVCUDA_NVTX_RANGE("cvcuda::RandomResizedCrop::operator()[Tensor]"); auto inData = in.exportData(); if (inData == nullptr) { @@ -55,13 +73,14 @@ void RandomResizedCrop::operator()(cudaStream_t stream, const nvcv::Tensor &in, "Output must be cuda-accessible, pitch-linear tensor"); } - NVCV_CHECK_THROW(m_legacyOp->infer(*inData, *outData, interpolation, stream)); + NVCV_CHECK_THROW(m_legacyOp.get().infer(*inData, *outData, interpolation, stream)); } void RandomResizedCrop::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, const NVCVInterpolationType interpolation) const { - NVCV_CHECK_THROW(m_legacyOpVarShape->infer(in, out, interpolation, stream)); + CVCUDA_NVTX_RANGE("cvcuda::RandomResizedCrop::operator()[ImageBatchVarShape]"); + NVCV_CHECK_THROW(m_legacyOpVarShape.get().infer(in, out, interpolation, stream)); } } // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpRandomResizedCrop.hpp b/src/cvcuda/priv/OpRandomResizedCrop.hpp index a89b23d4b..5a538c479 100644 --- a/src/cvcuda/priv/OpRandomResizedCrop.hpp +++ b/src/cvcuda/priv/OpRandomResizedCrop.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,13 +25,12 @@ #define CVCUDA_PRIV_RANDOMRESIZEDCROP_HPP #include "IOperator.hpp" +#include "PerDeviceResource.hpp" #include "legacy/CvCudaLegacy.h" #include #include -#include - namespace cvcuda::priv { class RandomResizedCrop final : public IOperator @@ -47,8 +46,8 @@ class RandomResizedCrop final : public IOperator const NVCVInterpolationType interpolation) const; private: - std::unique_ptr m_legacyOp; - std::unique_ptr m_legacyOpVarShape; + mutable PerDeviceResource m_legacyOp; + mutable PerDeviceResource m_legacyOpVarShape; }; } // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpReformat.cpp b/src/cvcuda/priv/OpReformat.cpp index 74b38b55a..7e3710a27 100644 --- a/src/cvcuda/priv/OpReformat.cpp +++ b/src/cvcuda/priv/OpReformat.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "OpReformat.hpp" +#include "Nvtx.hpp" #include "legacy/CvCudaLegacy.h" #include "legacy/CvCudaLegacyHelpers.hpp" @@ -29,13 +30,15 @@ namespace legacy = nvcv::legacy::cuda_op; Reformat::Reformat() { - legacy::DataShape maxIn, maxOut; + legacy::DataShape maxIn; + legacy::DataShape maxOut; //maxIn/maxOut not used by op. m_legacyOp = std::make_unique(maxIn, maxOut); } void Reformat::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out) const { + CVCUDA_NVTX_RANGE("cvcuda::Reformat::operator()[Tensor]"); auto inData = in.exportData(); if (inData == nullptr) { diff --git a/src/cvcuda/priv/OpRemap.cu b/src/cvcuda/priv/OpRemap.cu index a88183446..9985a56ee 100644 --- a/src/cvcuda/priv/OpRemap.cu +++ b/src/cvcuda/priv/OpRemap.cu @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,7 +15,9 @@ * limitations under the License. */ +#include "Nvtx.hpp" #include "OpRemap.hpp" +#include "PlanarTensorView.hpp" #include #include @@ -87,52 +89,142 @@ inline NVCVRemapParams __host__ __device__ GetRemapParams(const int2 &srcSize, c // Do remap kernel ------------------------------------------------------------- -template -inline void __device__ DoRemap(SrcWrapper src, DstWrapper dst, MapWrapper map, const int2 &dstSize, - const int &mapNumSamples, const NVCVRemapParams ¶ms) +// Compute the (x, y) source-sampling coordinate for an output pixel (x, y) of image mapSample. +// The map is accessed at the destination coordinate (offset and scaled by map scale) and +// interpolated; the source is then accessed at the destination coordinate scaled by source scale, +// plus the map value (relative distance or absolute position, normalized or not) times the value +// scale, offset by the source offset. Shared by the interleaved and planar paths so the math has a +// single definition. +template +inline float2 __device__ ComputeRemapSrcCoord(int x, int y, const MapWrapper &map, int mapSample, + const NVCVRemapParams ¶ms) { - int3 dstCoord = cuda::StaticCast(blockIdx * blockDim + threadIdx); + float3 mapCoord{(x + params.dstOffset) * params.mapScale.x, (y + params.dstOffset) * params.mapScale.y, + static_cast(mapSample)}; + + float2 mapValue = map[mapCoord]; + + return float2{x * params.srcScale.x + mapValue.x * params.valScale.x + params.srcOffset.x, + y * params.srcScale.y + mapValue.y * params.valScale.y + params.srcOffset.y}; +} + +// Number of output pixels each thread processes along x for the latency-bound +// (NEAREST/LINEAR) interpolation path. The per-pixel work is a serial map-read -> +// coordinate -> source-gather dependency chain; issuing several independent chains +// per thread overlaps their latency. Threads stay x-adjacent within each strided +// step, so writes remain coalesced. The CUBIC path is compute-bound at full +// occupancy, where the extra per-thread state only hurts, so it uses NIX == 1. +constexpr int kRemapNIX = 4; - if (dstCoord.x >= dstSize.x || dstCoord.y >= dstSize.y) +template +inline void __device__ DoRemap(SrcWrapper src, DstWrapper dst, MapWrapper map, const int2 &dstSize, + const int &mapNumSamples, const NVCVRemapParams ¶ms, int channels = 1) +{ + // For planar (NCHW/CHW) tensors the C channel planes are flattened into grid-z as N*C + // single-channel samples, so the per-(x,y) map sample is the original image index + // z / channels. For interleaved data channels == 1, so this is just z. + if constexpr (NIX == 1) { - return; - } + // Compute-bound (CUBIC) path: one pixel per thread, unchanged from the + // original kernel so the compute-bound case takes no x-unroll overhead. + int3 dstCoord = cuda::StaticCast(blockIdx * blockDim + threadIdx); - float3 mapCoord{0.f, 0.f, (mapNumSamples == 1 ? 0.f : static_cast(dstCoord.z))}; + if (dstCoord.x >= dstSize.x || dstCoord.y >= dstSize.y) + { + return; + } - // The map is accessed at destination coordinate, with destination offset and scaled by map scale. The - // resulting map coordinate is interpolated in the map, given by the map interpolation type. + int mapSample = (mapNumSamples == 1) ? 0 : dstCoord.z / channels; + float2 sc = ComputeRemapSrcCoord(dstCoord.x, dstCoord.y, map, mapSample, params); - mapCoord.x = (dstCoord.x + params.dstOffset) * params.mapScale.x; - mapCoord.y = (dstCoord.y + params.dstOffset) * params.mapScale.y; + dst[dstCoord] = src[float3{sc.x, sc.y, static_cast(dstCoord.z)}]; + } + else + { + // Latency-bound (NEAREST/LINEAR) path: NIX independent chains per thread. + int baseX = (blockIdx.x * blockDim.x) * NIX + threadIdx.x; + int y = blockIdx.y * blockDim.y + threadIdx.y; + int z = blockIdx.z * blockDim.z + threadIdx.z; - float2 mapValue = map[mapCoord]; + if (y >= dstSize.y) + { + return; + } - float3 srcCoord{0.f, 0.f, static_cast(dstCoord.z)}; + int mapSample = (mapNumSamples == 1) ? 0 : z / channels; - // The source is accessed at destination coordinate scaled by source scale, plus the map value that is either a - // relative distance from destination or an absolute position at source (either normalized or not), multiplied - // by value scale and offset by source offset. The source interpolation type only affects the source scaling - // and offset values. +#pragma unroll + for (int i = 0; i < NIX; ++i) + { + int x = baseX + i * blockDim.x; + if (x >= dstSize.x) + { + continue; + } - srcCoord.x = dstCoord.x * params.srcScale.x + mapValue.x * params.valScale.x + params.srcOffset.x; - srcCoord.y = dstCoord.y * params.srcScale.y + mapValue.y * params.valScale.y + params.srcOffset.y; + float2 sc = ComputeRemapSrcCoord(x, y, map, mapSample, params); - dst[dstCoord] = src[srcCoord]; + dst[int3{x, y, z}] = src[float3{sc.x, sc.y, static_cast(z)}]; + } + } } // Remap with tensors kernel --------------------------------------------------- -template +template __global__ void Remap(SrcWrapper src, DstWrapper dst, MapWrapper map, int2 dstSize, int mapNumSamples, - NVCVRemapParams params) + NVCVRemapParams params, int channels = 1) +{ + DoRemap(src, dst, map, dstSize, mapNumSamples, params, channels); +} + +// Remap with planar (NCHW) tensor kernel -------------------------------------- +// +// grid-z runs over images (not images*channels). The per-pixel source coordinate +// (the map lookup + interpolation) is computed once and reused for every channel +// plane, so the map traffic and coordinate math are amortized across channels +// instead of repeated per plane. The (N,C,H,W) tensor is addressed through the +// flattened (N*C,H,W) single-channel view, so plane p of image i is sample +// i*channels + p. Output is identical to the per-plane path (the coordinate does +// not depend on the plane). +template +__global__ void RemapPlanarTensor(SrcWrapper src, DstWrapper dst, MapWrapper map, int2 dstSize, int mapNumSamples, + NVCVRemapParams params, int channels) { - DoRemap(src, dst, map, dstSize, mapNumSamples, params); + int baseX = (blockIdx.x * blockDim.x) * NIX + threadIdx.x; + int y = blockIdx.y * blockDim.y + threadIdx.y; + + if (y >= dstSize.y) + { + return; + } + + int image = blockIdx.z; + int mapSample = (mapNumSamples == 1) ? 0 : image; + +#pragma unroll + for (int i = 0; i < NIX; ++i) + { + int x = baseX + i * blockDim.x; + if (x >= dstSize.x) + { + continue; + } + + float2 sc = ComputeRemapSrcCoord(x, y, map, mapSample, params); + + for (int plane = 0; plane < channels; ++plane) + { + int z = image * channels + plane; + + dst[int3{x, y, z}] = src[float3{sc.x, sc.y, static_cast(z)}]; + } + } } // Remap with varshape kernel -------------------------------------------------- -template +template __global__ void Remap(SrcWrapper src, DstWrapper dst, MapWrapper map, int2 mapSize, int mapNumSamples, bool alignCorners, NVCVRemapMapValueType mapValueType) { @@ -143,7 +235,55 @@ __global__ void Remap(SrcWrapper src, DstWrapper dst, MapWrapper map, int2 mapSi NVCVRemapParams params = GetRemapParams(srcSize, dstSize, mapSize, alignCorners, mapValueType); - DoRemap(src, dst, map, dstSize, mapNumSamples, params); + DoRemap(src, dst, map, dstSize, mapNumSamples, params); +} + +// Remap with planar (CHW) varshape kernel ------------------------------------- +// +// Each planar image has `channels` single-channel planes; grid-z runs over numImages. +// The per-pixel source coordinate (map lookup + interpolation) is computed once per +// (image, x, y) and reused for every plane, amortizing the map traffic and coordinate +// math across channels. Each plane is sampled via the 4D (sample, plane, y, x) +// coordinate the var-shape wraps accept. Remap treats channels independently, so this +// matches the interleaved result plane-for-plane. +template +__global__ void RemapPlanarVarShape(SrcWrapper src, DstWrapper dst, MapWrapper map, int2 mapSize, int mapNumSamples, + bool alignCorners, NVCVRemapMapValueType mapValueType, int channels) +{ + int image = blockIdx.z; + + int baseX = (blockIdx.x * blockDim.x) * NIX + threadIdx.x; + int y = blockIdx.y * blockDim.y + threadIdx.y; + + int2 dstSize{dst.width(image), dst.height(image)}; + if (y >= dstSize.y) + { + return; + } + + int2 srcSize{src.borderWrap().imageBatchWrap().width(image), src.borderWrap().imageBatchWrap().height(image)}; + + NVCVRemapParams params = GetRemapParams(srcSize, dstSize, mapSize, alignCorners, mapValueType); + + int mapSample = (mapNumSamples == 1) ? 0 : image; + +#pragma unroll + for (int i = 0; i < NIX; ++i) + { + int x = baseX + i * blockDim.x; + if (x >= dstSize.x) + { + continue; + } + + float2 sc = ComputeRemapSrcCoord(x, y, map, mapSample, params); + + for (int plane = 0; plane < channels; ++plane) + { + *dst.ptr(image, plane, y, x) + = src[float4{sc.x, sc.y, static_cast(plane), static_cast(image)}]; + } + } } // Host run remap functions ---------------------------------------------------- @@ -151,9 +291,15 @@ __global__ void Remap(SrcWrapper src, DstWrapper dst, MapWrapper map, int2 mapSi template void RunRemap(cudaStream_t stream, const DataStridedCuda &srcData, const DataStridedCuda &dstData, const MapWrapper &mapWrap, NVCVRemapMapValueType mapValueType, bool alignCorners, const T &borderValue, - int2 mapSize, int mapNumSamples) + int2 mapSize, int mapNumSamples, int channels = 1) { dim3 block(32, 4, 1); + + // CUBIC interpolation is compute-bound at full occupancy; the extra per-thread + // x-unrolling only adds register/loop overhead there, so disable it (NIX == 1). + // NEAREST/LINEAR are latency-bound and benefit from the overlap. + constexpr int NIX = (SI == NVCV_INTERP_CUBIC) ? 1 : kRemapNIX; + if constexpr (std::is_same_v) { auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(srcData); @@ -164,7 +310,7 @@ void RunRemap(cudaStream_t stream, const DataStridedCuda &srcData, const DataStr NVCVRemapParams params = GetRemapParams(srcSize, dstSize, mapSize, alignCorners, mapValueType); - dim3 grid(util::DivUp(dstSize.x, block.x), util::DivUp(dstSize.y, block.y), dstAccess->numSamples()); + dim3 grid(util::DivUp(dstSize.x, block.x * NIX), util::DivUp(dstSize.y, block.y), dstAccess->numSamples()); int64_t srcMaxStride = srcAccess->sampleStride() * srcAccess->numSamples(); int64_t dstMaxStride = dstAccess->sampleStride() * dstAccess->numSamples(); @@ -173,7 +319,17 @@ void RunRemap(cudaStream_t stream, const DataStridedCuda &srcData, const DataStr auto src = cuda::CreateInterpolationWrapNHW(srcData, borderValue); auto dst = cuda::CreateTensorWrapNHW(dstData); - Remap<<>>(src, dst, mapWrap, dstSize, mapNumSamples, params); + if (channels > 1) + { + dim3 planarGrid(util::DivUp(dstSize.x, block.x * NIX), util::DivUp(dstSize.y, block.y), + dstAccess->numSamples() / channels); + RemapPlanarTensor + <<>>(src, dst, mapWrap, dstSize, mapNumSamples, params, channels); + } + else + { + Remap<<>>(src, dst, mapWrap, dstSize, mapNumSamples, params, channels); + } } else { @@ -187,19 +343,33 @@ void RunRemap(cudaStream_t stream, const DataStridedCuda &srcData, const DataStr int3 dstMaxSize{dstData.maxSize().w, dstData.maxSize().h, dstData.numImages()}; - dim3 grid(util::DivUp(dstMaxSize.x, block.x), util::DivUp(dstMaxSize.y, block.y), dstMaxSize.z); - cuda::InterpolationVarShapeWrap src(srcData, borderValue); cuda::ImageBatchVarShapeWrap dst(dstData); - Remap<<>>(src, dst, mapWrap, mapSize, mapNumSamples, alignCorners, mapValueType); + // A valid multi-plane format has at most four channels in total, so a four-channel first plane + // cannot have another plane. Avoid instantiating that unreachable planar uchar4 kernel family. + if constexpr (cuda::NumElements < 4) + { + if (channels > 1) + { + // Planar (CHW) var-shape: grid-z spans numImages; each thread amortizes the source + // coordinate across all channel planes via 4D var-shape coordinates. + dim3 grid(util::DivUp(dstMaxSize.x, block.x * NIX), util::DivUp(dstMaxSize.y, block.y), dstMaxSize.z); + RemapPlanarVarShape<<>>(src, dst, mapWrap, mapSize, mapNumSamples, + alignCorners, mapValueType, channels); + return; + } + } + + dim3 grid(util::DivUp(dstMaxSize.x, block.x * NIX), util::DivUp(dstMaxSize.y, block.y), dstMaxSize.z); + Remap<<>>(src, dst, mapWrap, mapSize, mapNumSamples, alignCorners, mapValueType); } } template void RunRemap(cudaStream_t stream, const DataStridedCuda &srcData, const DataStridedCuda &dstData, const nvcv::TensorDataStridedCuda &mapData, NVCVRemapMapValueType mapValueType, bool alignCorners, - const T &borderValue) + const T &borderValue, int channels = 1) { auto mapAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(mapData); int2 mapSize = cuda::StaticCast(long2{mapAccess->numCols(), mapAccess->numRows()}); @@ -209,7 +379,7 @@ void RunRemap(cudaStream_t stream, const DataStridedCuda &srcData, const DataStr { auto map = cuda::CreateInterpolationWrapNHW(mapData); RunRemap(stream, srcData, dstData, map, mapValueType, alignCorners, borderValue, mapSize, - mapNumSamples); + mapNumSamples, channels); } else { @@ -221,12 +391,12 @@ void RunRemap(cudaStream_t stream, const DataStridedCuda &srcData, const DataStr template void RunRemap(cudaStream_t stream, const DataStridedCuda &srcData, const DataStridedCuda &dstData, const nvcv::TensorDataStridedCuda &mapData, NVCVInterpolationType srcInterp, - NVCVRemapMapValueType mapValueType, bool alignCorners, const T &borderValue) + NVCVRemapMapValueType mapValueType, bool alignCorners, const T &borderValue, int channels = 1) { #define NVCV_RUN_REMAP(INTERP_TYPE) \ case NVCV_INTERP_##INTERP_TYPE: \ RunRemap(stream, srcData, dstData, mapData, mapValueType, alignCorners, \ - borderValue); \ + borderValue, channels); \ break switch (srcInterp) @@ -245,12 +415,12 @@ template void RunRemap(cudaStream_t stream, const DataStridedCuda &srcData, const DataStridedCuda &dstData, const nvcv::TensorDataStridedCuda &mapData, NVCVInterpolationType srcInterp, NVCVInterpolationType mapInterp, NVCVRemapMapValueType mapValueType, bool alignCorners, - const T &borderValue) + const T &borderValue, int channels = 1) { #define NVCV_RUN_REMAP(INTERP_TYPE) \ case NVCV_INTERP_##INTERP_TYPE: \ RunRemap(stream, srcData, dstData, mapData, srcInterp, mapValueType, \ - alignCorners, borderValue); \ + alignCorners, borderValue, channels); \ break switch (mapInterp) @@ -269,14 +439,14 @@ template void RunRemap(cudaStream_t stream, const DataStridedCuda &srcData, const DataStridedCuda &dstData, const nvcv::TensorDataStridedCuda &mapData, NVCVInterpolationType srcInterp, NVCVInterpolationType mapInterp, NVCVRemapMapValueType mapValueType, bool alignCorners, - NVCVBorderType border, const float4 &borderValue) + NVCVBorderType border, const float4 &borderValue, int channels = 1) { const T bvalue = cuda::DropCast>(cuda::StaticCast>(borderValue)); #define NVCV_RUN_REMAP(BORDER_TYPE) \ case NVCV_BORDER_##BORDER_TYPE: \ RunRemap(stream, srcData, dstData, mapData, srcInterp, mapInterp, mapValueType, \ - alignCorners, bvalue); \ + alignCorners, bvalue, channels); \ break switch (border) @@ -297,11 +467,16 @@ template inline void RunRemap(cudaStream_t stream, const DataStridedCuda &srcData, const DataStridedCuda &dstData, const nvcv::TensorDataStridedCuda &mapData, NVCVInterpolationType srcInterp, NVCVInterpolationType mapInterp, NVCVRemapMapValueType mapValueType, bool alignCorners, - NVCVBorderType border, const float4 &borderValue, nvcv::DataType dataType, int numChannels = 1) + NVCVBorderType border, const float4 &borderValue, nvcv::DataType dataType, int numChannels = 1, + int mapSampleChannels = 1) { // When this function is called with tensors, the data type may contain the channels baked in or the number of // channels is in the tensor shape; when it is called with varshape, the data type always contain the channels // as each image only stores size, there is no shape information with number of channels baked in. + // + // numChannels selects the element type T (e.g. uchar3 vs single-channel uchar1 for a flattened planar view). + // mapSampleChannels is the planar channel count used to map a flattened grid-z plane back to its image index + // for the per-sample map lookup (1 for interleaved/var-shape, C for the planar single-channel tensor view). // clang-format off @@ -309,7 +484,7 @@ inline void RunRemap(cudaStream_t stream, const DataStridedCuda &srcData, const ((dataType == nvcv::TYPE_##BT && numChannels == cuda::NumElements) || \ (dataType == nvcv::TYPE_##DT && numChannels == 1)) \ RunRemap(stream, srcData, dstData, mapData, srcInterp, mapInterp, mapValueType, \ - alignCorners, border, borderValue) + alignCorners, border, borderValue, mapSampleChannels) if NVCV_RUN_REMAP(U8, U8, uchar1); else if NVCV_RUN_REMAP(U8, 3U8, uchar3); @@ -325,6 +500,29 @@ inline void RunRemap(cudaStream_t stream, const DataStridedCuda &srcData, const // clang-format on } +// The planar path flattens the C channel planes into single-channel samples that all share one +// border scalar (borderValue.x). The interleaved path applies borderValue componentwise, so for +// NVCV_BORDER_CONSTANT the two only agree when the first `channels` border components are equal. +// Reject the non-uniform case rather than silently producing a different result for out-of-bounds +// samples (other border modes ignore borderValue, so they are always fine). +inline void RequireUniformConstantBorderForPlanar(NVCVBorderType border, const float4 &borderValue, int channels) +{ + if (border != NVCV_BORDER_CONSTANT) + { + return; + } + const float *bv = &borderValue.x; + for (int c = 1; c < channels; ++c) + { + if (bv[c] != bv[0]) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Planar remap with NVCV_BORDER_CONSTANT requires a uniform border value across " + "channels"); + } + } +} + } // anonymous namespace namespace cvcuda::priv { @@ -340,6 +538,7 @@ void Remap::operator()(cudaStream_t stream, const nvcv::Tensor &src, const nvcv: NVCVRemapMapValueType mapValueType, bool alignCorners, NVCVBorderType border, float4 borderValue) const { + CVCUDA_NVTX_RANGE("cvcuda::Remap::operator()[Tensor]"); auto srcData = src.exportData(); if (!srcData) { @@ -392,9 +591,40 @@ void Remap::operator()(cudaStream_t stream, const nvcv::Tensor &src, const nvcv: throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input and output data type are different"); } - if (srcData->layout() != nvcv::TENSOR_HWC && srcData->layout() != nvcv::TENSOR_NHWC) + const bool srcPlanar = (srcData->layout() == nvcv::TENSOR_NCHW || srcData->layout() == nvcv::TENSOR_CHW); + const bool dstPlanar = (dstData->layout() == nvcv::TENSOR_NCHW || dstData->layout() == nvcv::TENSOR_CHW); + const bool srcInterleaved = (srcData->layout() == nvcv::TENSOR_HWC || srcData->layout() == nvcv::TENSOR_NHWC); + + if (!(srcInterleaved || srcPlanar)) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input must have (N)HWC or (N)CHW layout"); + } + if (srcPlanar != dstPlanar) { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input must have (N)HWC layout"); + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input and output must share the same layout family ((N)HWC or (N)CHW)"); + } + + if (srcPlanar) + { + // Remap treats channels independently: flatten (N,C,H,W) into N*C single-channel samples and + // reuse the interleaved single-channel kernel, looking the per-sample map up at plane/channels + // (see PlanarTensorView.hpp / DoRemap). The flattened plane count is the kernel grid-z, which + // is capped at CUDA's 65535 limit. + const int channels = srcAccess->numChannels(); + const int64_t planarBatch = static_cast(channels) * srcAccess->numSamples(); + if (planarBatch > 65535) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Planar remap requires numSamples * numChannels <= 65535 (CUDA grid-z limit)"); + } + RequireUniformConstantBorderForPlanar(border, borderValue, channels); + + auto srcView = PlanarAsSingleChannelView(*srcData, *srcAccess); + auto dstView = PlanarAsSingleChannelView(*dstData, *dstAccess); + RunRemap(stream, srcView, dstView, *mapData, srcInterp, mapInterp, mapValueType, alignCorners, border, + borderValue, dstData->dtype(), /*numChannels=*/1, /*mapSampleChannels=*/channels); + return; } RunRemap(stream, *srcData, *dstData, *mapData, srcInterp, mapInterp, mapValueType, alignCorners, border, @@ -408,6 +638,7 @@ void Remap::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &src, NVCVRemapMapValueType mapValueType, bool alignCorners, NVCVBorderType border, float4 borderValue) const { + CVCUDA_NVTX_RANGE("cvcuda::Remap::operator()[ImageBatchVarShape]"); auto srcData = src.exportData(stream); if (!srcData) { @@ -453,9 +684,24 @@ void Remap::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &src, throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input and output formats are different"); } - if (srcData->uniqueFormat().numPlanes() > 1) + const int numPlanes = srcData->uniqueFormat().numPlanes(); + + if (numPlanes > 1) { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Image batches must have (N)HWC layout"); + // Planar (CHW) var-shape: each image stores numPlanes single-channel planes; remap each + // plane independently with the single-channel element type. grid-z spans + // numImages*numPlanes, capped at CUDA's 65535 limit. + if (static_cast(srcData->numImages()) * numPlanes > 65535) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Planar remap requires numImages * numChannels <= 65535 (CUDA grid-z limit)"); + } + RequireUniformConstantBorderForPlanar(border, borderValue, numPlanes); + + RunRemap(stream, *srcData, *dstData, *mapData, srcInterp, mapInterp, mapValueType, alignCorners, border, + borderValue, srcData->uniqueFormat().planeDataType(0), /*numChannels=*/1, + /*mapSampleChannels=*/numPlanes); + return; } RunRemap(stream, *srcData, *dstData, *mapData, srcInterp, mapInterp, mapValueType, alignCorners, border, diff --git a/src/cvcuda/priv/OpResize.cpp b/src/cvcuda/priv/OpResize.cpp index 4abc21e7e..ac169ac7a 100644 --- a/src/cvcuda/priv/OpResize.cpp +++ b/src/cvcuda/priv/OpResize.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,19 +17,24 @@ #include "OpResize.hpp" +#include "Nvtx.hpp" #include "legacy/CvCudaLegacy.h" #include "legacy/CvCudaLegacyHelpers.hpp" #include #include +#include +#include + namespace cvcuda::priv { namespace legacy = nvcv::legacy::cuda_op; Resize::Resize() { - legacy::DataShape maxIn, maxOut; // maxIn/maxOut not used by op. + legacy::DataShape maxIn; + legacy::DataShape maxOut; // maxIn/maxOut not used by op. m_legacyOpVarShape = std::make_unique(maxIn, maxOut); } @@ -37,6 +42,7 @@ Resize::Resize() void Resize::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, const NVCVInterpolationType interpolation) const { + CVCUDA_NVTX_RANGE("cvcuda::Resize::operator()[Tensor]"); auto inData = in.exportData(); if (inData == nullptr) { @@ -57,6 +63,20 @@ void Resize::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv: void Resize::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, const NVCVInterpolationType interpolation) const { + CVCUDA_NVTX_RANGE("cvcuda::Resize::operator()[ImageBatchVarShape]"); + if (interpolation == NVCV_INTERP_LINEAR) + { + for (const auto &img : in) + { + auto sz = img.size(); + if (sz.w < 2 || sz.h < 2) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Linear interpolation requires source dimensions of at least 2x2"); + } + } + } + auto inData = in.exportData(stream); if (inData == nullptr) { @@ -69,7 +89,41 @@ void Resize::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Output must be varshape image batch"); } - NVCV_CHECK_THROW(m_legacyOpVarShape->infer(*inData, *outData, interpolation, stream)); + // The uniform-scale fast paths need per-image sizes, which are only host-accessible through + // the batch handles (the exported imageList is device memory). The fractional zoom-out check + // replicates the kernel's float scale/is_area_fast computation so dispatch and per-image + // branch agree exactly. + const bool sameCount = in.numImages() == out.numImages() && in.numImages() > 0; + bool allExpand2x = sameCount; + bool allContract2x = sameCount; + bool allFracZoomOut = sameCount; + for (int32_t i = 0; i < in.numImages(); ++i) + { + if (!(allExpand2x || allContract2x || allFracZoomOut)) + { + break; + } + auto srcSize = in[i].size(); + auto dstSize = out[i].size(); + allExpand2x = allExpand2x && dstSize.w == 2 * srcSize.w && dstSize.h == 2 * srcSize.h; + allContract2x = allContract2x && srcSize.w == 2 * dstSize.w && srcSize.h == 2 * dstSize.h; + + const float scaleX = static_cast(srcSize.w) / static_cast(dstSize.w); + const float scaleY = static_cast(srcSize.h) / static_cast(dstSize.h); + const bool isAreaFast = std::abs(scaleX - static_cast(static_cast(scaleX))) < DBL_EPSILON + && std::abs(scaleY - static_cast(static_cast(scaleY))) < DBL_EPSILON; + allFracZoomOut = allFracZoomOut && scaleX >= 1.f && scaleY >= 1.f && scaleX < 3.f && !isAreaFast; + } + + legacy::ResizeVarShapeScale batchScale = legacy::ResizeVarShapeScale::kGeneric; + if (allExpand2x) + batchScale = legacy::ResizeVarShapeScale::kExpand2x; + else if (allContract2x) + batchScale = legacy::ResizeVarShapeScale::kContract2x; + else if (allFracZoomOut) + batchScale = legacy::ResizeVarShapeScale::kFractionalZoomOut; + + NVCV_CHECK_THROW(m_legacyOpVarShape->infer(*inData, *outData, interpolation, stream, batchScale)); } } // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpResize.cu b/src/cvcuda/priv/OpResize.cu index 448ff958f..32d1a165b 100644 --- a/src/cvcuda/priv/OpResize.cu +++ b/src/cvcuda/priv/OpResize.cu @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,7 +15,9 @@ * limitations under the License. */ +#include "CudaDeviceUtils.hpp" #include "OpResize.hpp" +#include "PlanarTensorView.hpp" #include #include @@ -29,6 +31,7 @@ #include #include #include +#include #include namespace { @@ -165,22 +168,158 @@ __global__ void NearestResize(SrcWrapper src, DstWrapper dst, int2 srcSize, int2 // Linear ---------------------------------------------------------------------- -template -inline __device__ void LinearReadPack(SrcWrapper src, T (&srcPack)[4], int3 iSrcCoord) +// The cached INTERSECT pack holds the taps as work-type floats: float(byte) conversion is exact, +// so converting once at read time (instead of inside every blend that reuses the pack) is +// value-identical while removing the per-output conversions the 4x-class upscales were +// instruction-bound on. For float element types the conversion is a no-op. Scalar byte elements +// keep the raw byte pack: the float cache regressed the NIX=16 U8 upscale rows +24% on A100 +// (+5% H100) against reference baselines while multi-channel bytes gained. +template +using LinearFT = std::conditional_t == 1 && sizeof(cuda::BaseType) == 1, T, + cuda::ConvertBaseTypeTo>; + +template +__device__ __forceinline__ LinearFT LinearToPack(T v) +{ + return cuda::StaticCast>>(v); +} + +template +inline __device__ void LinearReadPack(SrcWrapper src, FT (&srcPack)[4], int3 iSrcCoord) +{ + srcPack[0] = LinearToPack(src[int3{iSrcCoord.x, iSrcCoord.y, iSrcCoord.z}]); + srcPack[1] = LinearToPack(src[int3{iSrcCoord.x + 1, iSrcCoord.y, iSrcCoord.z}]); + srcPack[2] = LinearToPack(src[int3{iSrcCoord.x, iSrcCoord.y + 1, iSrcCoord.z}]); + srcPack[3] = LinearToPack(src[int3{iSrcCoord.x + 1, iSrcCoord.y + 1, iSrcCoord.z}]); +} + +template +__device__ __forceinline__ void LinearReadPack(const T *srcRow0, const T *srcRow1, LinearFT (&srcPack)[4], + int srcCoordX) +{ + srcPack[0] = LinearToPack(srcRow0[srcCoordX]); + srcPack[1] = LinearToPack(srcRow0[srcCoordX + 1]); + srcPack[2] = LinearToPack(srcRow1[srcCoordX]); + srcPack[3] = LinearToPack(srcRow1[srcCoordX + 1]); +} + +template +__device__ __forceinline__ void LinearReadPackMaybe(SrcWrapper src, const T *srcRow0, const T *srcRow1, + LinearFT (&srcPack)[4], int3 iSrcCoord) +{ + if constexpr (USE_ROW_PTR) + { + LinearReadPack(srcRow0, srcRow1, srcPack, iSrcCoord.x); + } + else + { + LinearReadPack(src, srcPack, iSrcCoord); + } +} + +template +__device__ __forceinline__ T LinearReadMaybe(SrcWrapper src, const T *srcRow, int3 iSrcCoord) +{ + if constexpr (USE_ROW_PTR) + { + return srcRow[iSrcCoord.x]; + } + else + { + return src[iSrcCoord]; + } +} + +template +__device__ __forceinline__ T LinearBlend(T p00, T p10, T p01, T p11, float2 w) +{ + return cuda::SaturateCast(p00 * ((1.f - w.x) * (1.f - w.y)) + p10 * (w.x * (1.f - w.y)) + + p01 * ((1.f - w.x) * w.y) + p11 * (w.x * w.y)); +} + +// Blend from the float-cached INTERSECT pack: the expression tree matches LinearBlend exactly and +// float(byte) conversion is exact, so the result is bit-identical to blending the raw taps. +template +__device__ __forceinline__ T LinearBlendPack(const LinearFT (&p)[4], float2 w) +{ + return cuda::SaturateCast(p[0] * ((1.f - w.x) * (1.f - w.y)) + p[1] * (w.x * (1.f - w.y)) + + p[2] * ((1.f - w.x) * w.y) + p[3] * (w.x * w.y)); +} + +template +__device__ __forceinline__ void LinearReadNextPackMaybe(SrcWrapper src, const T *srcRow0, const T *srcRow1, + LinearFT (&srcPack)[4], int3 iSrcCoord) { - srcPack[0] = src[int3{iSrcCoord.x, iSrcCoord.y, iSrcCoord.z}]; - srcPack[1] = src[int3{iSrcCoord.x + 1, iSrcCoord.y, iSrcCoord.z}]; - srcPack[2] = src[int3{iSrcCoord.x, iSrcCoord.y + 1, iSrcCoord.z}]; - srcPack[3] = src[int3{iSrcCoord.x + 1, iSrcCoord.y + 1, iSrcCoord.z}]; + srcPack[0] = srcPack[1]; + srcPack[2] = srcPack[3]; + srcPack[1] + = LinearToPack(LinearReadMaybe(src, srcRow0, int3{iSrcCoord.x + 1, iSrcCoord.y, iSrcCoord.z})); + srcPack[3] + = LinearToPack(LinearReadMaybe(src, srcRow1, int3{iSrcCoord.x + 1, iSrcCoord.y + 1, iSrcCoord.z})); +} + +template +__device__ __forceinline__ void LinearUpdatePackMaybe(SrcWrapper src, const T *srcRow0, const T *srcRow1, + LinearFT (&srcPack)[4], int3 iSrcCoord, int iPrevCoordX, int x) +{ + if (x == 0) + { + LinearReadPackMaybe(src, srcRow0, srcRow1, srcPack, iSrcCoord); + } + else if (iSrcCoord.x != iPrevCoordX) + { + if (iSrcCoord.x == (iPrevCoordX + 1)) + { + LinearReadNextPackMaybe(src, srcRow0, srcRow1, srcPack, iSrcCoord); + } + else + { + LinearReadPackMaybe(src, srcRow0, srcRow1, srcPack, iSrcCoord); + } + } +} + +template +__device__ __forceinline__ T LinearSampleMaybe(SrcWrapper src, const T *srcRow0, const T *srcRow1, + LinearFT (&srcPack)[4], int &iPrevCoordX, int3 iSrcCoord, int x, + float2 w) +{ + if constexpr (INTERSECT) + { + LinearUpdatePackMaybe(src, srcRow0, srcRow1, srcPack, iSrcCoord, iPrevCoordX, x); + iPrevCoordX = iSrcCoord.x; + return LinearBlendPack(srcPack, w); + } + else + { + return LinearBlend( + LinearReadMaybe(src, srcRow0, int3{iSrcCoord.x, iSrcCoord.y, iSrcCoord.z}), + LinearReadMaybe(src, srcRow0, int3{iSrcCoord.x + 1, iSrcCoord.y, iSrcCoord.z}), + LinearReadMaybe(src, srcRow1, int3{iSrcCoord.x, iSrcCoord.y + 1, iSrcCoord.z}), + LinearReadMaybe(src, srcRow1, int3{iSrcCoord.x + 1, iSrcCoord.y + 1, iSrcCoord.z}), w); + } } template inline __device__ void LinearInterpolatePack(T *dstRow, SrcWrapper src, int3 iSrcCoord, float srcCoordX, int srcSizeX, int dstCoordX, int dstSizeX, float scaleRatioX, float2 w) { - float sx; - int iPrevCoordX; - T srcPack[4]; + float sx; + int iPrevCoordX; + LinearFT srcPack[4]; + + // Row pointers reduce index math for byte-vector LINEAR; float vectors keep wrapper loads + // to preserve bit-exact parity with the flattened scalar planar path. + constexpr int NUM_ELEMENTS = cuda::NumElements; + constexpr bool USE_ROW_PTR = NUM_ELEMENTS > 1 && sizeof(cuda::BaseType) == 1; + + const T *srcRow0 = nullptr; + const T *srcRow1 = nullptr; + if constexpr (USE_ROW_PTR) + { + srcRow0 = src.ptr(iSrcCoord.z, iSrcCoord.y); + srcRow1 = src.ptr(iSrcCoord.z, iSrcCoord.y + 1); + } if (dstCoordX + NIX - 1 < dstSizeX) { @@ -195,43 +334,8 @@ inline __device__ void LinearInterpolatePack(T *dstRow, SrcWrapper src, int3 iSr iSrcCoord.x = cuda::max(0, cuda::min(iSrcCoord.x, srcSizeX - 2)); - if constexpr (INTERSECT) - { - if (x == 0) - { - LinearReadPack(src, srcPack, iSrcCoord); - } - else - { - if (iSrcCoord.x != iPrevCoordX) - { - if (iSrcCoord.x == (iPrevCoordX + 1)) - { - srcPack[0] = srcPack[1]; - srcPack[2] = srcPack[3]; - srcPack[1] = src[int3{iSrcCoord.x + 1, iSrcCoord.y, iSrcCoord.z}]; - srcPack[3] = src[int3{iSrcCoord.x + 1, iSrcCoord.y + 1, iSrcCoord.z}]; - } - else - { - LinearReadPack(src, srcPack, iSrcCoord); - } - } - } - dstPack[x] - = cuda::SaturateCast(srcPack[0] * ((1.f - w.x) * (1.f - w.y)) + srcPack[1] * (w.x * (1.f - w.y)) - + srcPack[2] * ((1.f - w.x) * w.y) + srcPack[3] * (w.x * w.y)); - - iPrevCoordX = iSrcCoord.x; - } - else - { - dstPack[x] = cuda::SaturateCast( - src[int3{iSrcCoord.x, iSrcCoord.y, iSrcCoord.z}] * ((1.f - w.x) * (1.f - w.y)) - + src[int3{iSrcCoord.x + 1, iSrcCoord.y, iSrcCoord.z}] * (w.x * (1.f - w.y)) - + src[int3{iSrcCoord.x, iSrcCoord.y + 1, iSrcCoord.z}] * ((1.f - w.x) * w.y) - + src[int3{iSrcCoord.x + 1, iSrcCoord.y + 1, iSrcCoord.z}] * (w.x * w.y)); - } + dstPack[x] = LinearSampleMaybe(src, srcRow0, srcRow1, srcPack, iPrevCoordX, + iSrcCoord, x, w); } if (CheckRowAlign(dstRow)) // Branch is the same for all threads in warp. @@ -258,43 +362,8 @@ inline __device__ void LinearInterpolatePack(T *dstRow, SrcWrapper src, int3 iSr iSrcCoord.x = cuda::max(0, cuda::min(iSrcCoord.x, srcSizeX - 2)); - if constexpr (INTERSECT) - { - if (x == 0) - { - LinearReadPack(src, srcPack, iSrcCoord); - } - else - { - if (iSrcCoord.x != iPrevCoordX) - { - if (iSrcCoord.x == (iPrevCoordX + 1)) - { - srcPack[0] = srcPack[1]; - srcPack[2] = srcPack[3]; - srcPack[1] = src[int3{iSrcCoord.x + 1, iSrcCoord.y, iSrcCoord.z}]; - srcPack[3] = src[int3{iSrcCoord.x + 1, iSrcCoord.y + 1, iSrcCoord.z}]; - } - else - { - LinearReadPack(src, srcPack, iSrcCoord); - } - } - } - dstRow[dstCoordX + x] = cuda::SaturateCast( - srcPack[0] * ((1.f - w.x) * (1.f - w.y)) + srcPack[1] * (w.x * (1.f - w.y)) - + srcPack[2] * ((1.f - w.x) * w.y) + srcPack[3] * (w.x * w.y)); - - iPrevCoordX = iSrcCoord.x; - } - else - { - dstRow[dstCoordX + x] = cuda::SaturateCast( - src[int3{iSrcCoord.x, iSrcCoord.y, iSrcCoord.z}] * ((1.f - w.x) * (1.f - w.y)) - + src[int3{iSrcCoord.x + 1, iSrcCoord.y, iSrcCoord.z}] * (w.x * (1.f - w.y)) - + src[int3{iSrcCoord.x, iSrcCoord.y + 1, iSrcCoord.z}] * ((1.f - w.x) * w.y) - + src[int3{iSrcCoord.x + 1, iSrcCoord.y + 1, iSrcCoord.z}] * (w.x * w.y)); - } + dstRow[dstCoordX + x] = LinearSampleMaybe(src, srcRow0, srcRow1, srcPack, + iPrevCoordX, iSrcCoord, x, w); } } } @@ -329,6 +398,83 @@ __global__ void LinearResize(SrcWrapper src, DstWrapper dst, int2 srcSize, int2 } } +// Exact-2x bilinear upscale for 1-byte base types, mirroring CubicResizeExpand2x: at dst == 2*src +// the x/y weights are exactly 0.25f/0.75f (the same fx/fy the general kernel derives), so each +// thread owns NIX output columns x 2 output rows, stages the shared clamped 3-row x (NIX/2+2)-col +// source window in registers, and writes two vector packs. Border outputs replicate the general +// kernel's weight override (w = 0 when iSrc < 0, w = 1 when iSrc > size-2): the overridden weight +// zeroes the tap whose unclamped-window value differs from the clamped-coordinate load, so the +// blend -- evaluated with LinearBlend's exact expression -- stays bit-exact. +template +__global__ void LinearResizeExpand2x(SrcWrapper src, DstWrapper dst, int2 srcSize, int2 dstSize) +{ + using T = typename DstWrapper::ValueType; + + constexpr int KC = NIX / 2; // source columns owned per thread + constexpr int WC = KC + 2; // staged window columns (taps k0-1 .. k0+KC) + + const int k0 = (blockIdx.x * blockDim.x + threadIdx.x) * KC; // first owned source column + const int j = blockIdx.y * blockDim.y + threadIdx.y; // owned source row + const int z = blockIdx.z; + const int dstX0 = k0 * 2; + const int dstY0 = j * 2; + + if (dstX0 >= dstSize.x || dstY0 >= dstSize.y) + return; + + // Clamped source window shared by all 2*NIX outputs: win[r][c] = src[clamp(k0-1+c), clamp(j-1+r)]. + T win[3][WC]; +#pragma unroll + for (int r = 0; r < 3; ++r) + { + const T *srcRow = src.ptr(z, cuda::clamp(j - 1 + r, 0, srcSize.y - 1)); +#pragma unroll + for (int c = 0; c < WC; ++c) + { + win[r][c] = srcRow[cuda::clamp(k0 - 1 + c, 0, srcSize.x - 1)]; + } + } + + // Even output row 2j: iSrcY = j-1, fy = 0.75 (taps window rows 0,1); odd row 2j+1: iSrcY = j, + // fy = 0.25 (taps rows 1,2) -- with the general kernel's border overrides. + const float wyE = (j - 1 < 0) ? 0.f : ((j - 1 > srcSize.y - 2) ? 1.f : 0.75f); + const float wyO = (j > srcSize.y - 2) ? 1.f : 0.25f; + + T out0[NIX], out1[NIX]; +#pragma unroll + for (int i = 0; i < NIX; ++i) + { + const bool odd = (i & 1) != 0; + const int k = k0 + i / 2; + const int iSrcX = odd ? k : k - 1; + const float wx = (iSrcX < 0) ? 0.f : ((iSrcX > srcSize.x - 2) ? 1.f : (odd ? 0.25f : 0.75f)); + const int cb = (i / 2) + (odd ? 1 : 0); // window column of the first x tap + + out0[i] = LinearBlend(win[0][cb], win[0][cb + 1], win[1][cb], win[1][cb + 1], float2{wx, wyE}); + out1[i] = LinearBlend(win[1][cb], win[1][cb + 1], win[2][cb], win[2][cb + 1], float2{wx, wyO}); + } + + const int validCount = cuda::min(NIX, dstSize.x - dstX0); + // dstSize.y = 2*srcSize.y is even, so the odd output row always exists. +#pragma unroll + for (int p = 0; p < 2; ++p) + { + T *dstRow = dst.ptr(z, dstY0 + p); + const T(&outp)[NIX] = p == 0 ? out0 : out1; + + if (validCount == NIX && CheckRowAlign(dstRow)) // uniform across the warp + WritePack(dstRow[dstX0], outp); + else + { + T *dstPtr = dstRow + dstX0; +#pragma unroll + for (int c = 0; c < NIX; ++c) + if (c < validCount) + dstPtr[c] = outp[c]; + } + } +} + // Cubic ----------------------------------------------------------------------- inline __device__ void GetCubicCoeffs(float delta, float &w0, float &w1, float &w2, float &w3) @@ -341,67 +487,866 @@ inline __device__ void GetCubicCoeffs(float delta, float &w0, float &w1, float & w3 = 1.f - w0 - w1 - w2; } +// Each thread owns COLS output columns of a single destination row. The cubic y-axis pipeline +// (source coordinate, fractional offset, the 4 cubic coefficients, and the 4 clamped tap rows) +// depends only on the row, so it is computed once and reused across all COLS columns -- removing +// the redundant per-pixel y-coefficient and y-clamp ALU work that dominated the issue-bound +// single-pixel-per-thread kernel. The columns are grid-strided by blockDim.x so each store step +// stays coalesced. The per-pixel arithmetic is byte-identical to the scalar kernel (the hoisted +// wy/sy values are the same), so the result is bit-exact. +// Shared-memory-tiled bicubic for wide-float (float3/float4) EXPAND (upscale). +// +// Reference-SKU ncu shows the per-output 4x4 gather is L1/TEX-throughput bound (~77-87% on A100/ +// H100) with DRAM far below (~28-61%): the 12-/16-byte unaligned float3/float4 taps thrash L1 while +// the data is being re-read, not streamed. For an upscale, a thread block's output tile maps to a +// SMALL source tile (output_extent * scale + cubic support), so we stage that tile once in shared +// memory -- with replicate-border clamp baked in at load time -- and serve all 16 taps/pixel from +// smem instead of L1. tileW/tileH are sized on the host from the scale ratio (generous upper bound). +// +// Bit-exact with CubicResize: each smem slot holds src[clamp(tileOrigin + slot)], so indexing a tap +// by its UNCLAMPED position minus the tile origin yields tile[tap - origin] == src[clamp(tap)] -- +// identical values, weights and accumulation order. EXPAND-only (CONTRACT would need a huge tile) +// and gated to float3/float4 (the at-ridge wide-element path); every other CUBIC case keeps +// CubicResize / CubicResizePlanar. template -__global__ void CubicResize(SrcWrapper src, DstWrapper dst, int2 srcSize, int2 dstSize, float2 scaleRatio) +__global__ void CubicResizeSharedExpand(SrcWrapper src, DstWrapper dst, int2 srcSize, int2 dstSize, float2 scaleRatio, + int tileW, int tileH) { using T = typename DstWrapper::ValueType; using FT = nvcv::cuda::ConvertBaseTypeTo; - int3 dstCoord; - dstCoord.z = blockIdx.z; - dstCoord.y = blockIdx.y * blockDim.y + threadIdx.y; - dstCoord.x = blockIdx.x * blockDim.x + threadIdx.x; + extern __shared__ __align__(16) unsigned char smemRaw[]; + T *tile = reinterpret_cast(smemRaw); + + const int z = blockIdx.z; + const int ox0 = blockIdx.x * blockDim.x; // block output-tile origin + const int oy0 = blockIdx.y * blockDim.y; - if (dstCoord.y < dstSize.y && dstCoord.x < dstSize.x) + // Source tile origin = first cubic tap (iSrc-1) of the block's first output pixel. + const int tx0 = (int)floorf(((float)ox0 + .5f) * scaleRatio.x - .5f) - 1; + const int ty0 = (int)floorf(((float)oy0 + .5f) * scaleRatio.y - .5f) - 1; + + // Cooperative, border-clamped load: tile[j*tileW + i] = src[clamp(tx0+i), clamp(ty0+j), z]. + const int nThreads = blockDim.x * blockDim.y; + for (int idx = threadIdx.y * blockDim.x + threadIdx.x; idx < tileW * tileH; idx += nThreads) { - float2 srcCoord = (cuda::DropCast<2>(dstCoord) + .5f) * scaleRatio - .5f; - int3 iSrcCoord{(int)floor(srcCoord.x), (int)floor(srcCoord.y), dstCoord.z}; + const int i = idx % tileW; + const int j = idx / tileW; + const int sx = cuda::clamp(tx0 + i, 0, srcSize.x - 1); + const int sy = cuda::clamp(ty0 + j, 0, srcSize.y - 1); + tile[idx] = src[int3{sx, sy, z}]; + } + __syncthreads(); - float fx = srcCoord.x - iSrcCoord.x; - float fy = srcCoord.y - iSrcCoord.y; + const int dstX = ox0 + threadIdx.x; + const int dstY = oy0 + threadIdx.y; + if (dstX >= dstSize.x || dstY >= dstSize.y) + return; + + const float srcX = ((float)dstX + .5f) * scaleRatio.x - .5f; + const float srcY = ((float)dstY + .5f) * scaleRatio.y - .5f; + const int iSrcX = (int)floorf(srcX); + const int iSrcY = (int)floorf(srcY); - fx = (iSrcCoord.x < 1 || iSrcCoord.x >= srcSize.x - 3) ? 0 : fx; + float wx[4], wy[4]; + GetCubicCoeffs(srcX - iSrcX, wx[0], wx[1], wx[2], wx[3]); + GetCubicCoeffs(srcY - iSrcY, wy[0], wy[1], wy[2], wy[3]); + + FT sum = FT{}; +#pragma unroll + for (int cy = 0; cy < 4; cy++) + { + // tile rows/cols hold the clamped source, so index by the unclamped tap minus the tile + // origin -- tile[(tap)-origin] == src[clamp(tap)], matching CubicResize exactly. + const int jj = (iSrcY + cy - 1) - ty0; +#pragma unroll + for (int cx = 0; cx < 4; cx++) + { + const int ii = (iSrcX + cx - 1) - tx0; + sum += tile[jj * tileW + ii] * (wx[cx] * wy[cy]); + } + } + dst[int3{dstX, dstY, z}] = cuda::SaturateCast(sum); +} - iSrcCoord.y = cuda::max(1, cuda::min(iSrcCoord.y, srcSize.y - 3)); - iSrcCoord.x = cuda::max(1, cuda::min(iSrcCoord.x, srcSize.x - 3)); +// General cubic upscale for byte and scalar elements with NIX consecutive output columns per +// thread: for any expand scale the source column index advances by at most one between adjacent +// outputs, so the 4x4 tap window is kept as work-type floats (float(byte) conversion is exact) and +// shifted by a single converted column load when it advances -- the grid-strided general kernel +// re-gathers and re-converts all 16 taps per output. The y coefficients/rows stay hoisted as in +// CubicResize; x coefficients remain per-output (fractional phases). Tap selection, weight values, +// and the cy-outer/cx-inner accumulation order match CubicResize, so the result is bit-exact. The +// exact-2x cases peel off to their phase-fused kernels before this dispatch. +template +__global__ void CubicResizeUpscale(SrcWrapper src, DstWrapper dst, int2 srcSize, int2 dstSize, float2 scaleRatio) +{ + using T = typename DstWrapper::ValueType; + using FT = nvcv::cuda::ConvertBaseTypeTo; + + const int z = blockIdx.z; + const int dstY = blockIdx.y * blockDim.y + threadIdx.y; + const int x0 = (blockIdx.x * blockDim.x + threadIdx.x) * NIX; + + if (dstY >= dstSize.y || x0 >= dstSize.x) + return; + + // Row-invariant y terms, identical to CubicResize. + const float srcY = ((float)dstY + .5f) * scaleRatio.y - .5f; + const int iSrcY = (int)floor(srcY); + const float fy = srcY - iSrcY; + + float wy[4]; + GetCubicCoeffs(fy, wy[0], wy[1], wy[2], wy[3]); + + const T *rows[4]; +#pragma unroll + for (int k = 0; k < 4; ++k) rows[k] = src.ptr(z, cuda::clamp(iSrcY + k - 1, 0, srcSize.y - 1)); + + // Float-cached 4x4 tap window of the current source column set [prevSx-1, prevSx+2]. + FT win[4][4]; + int prevSx = -srcSize.x - 4; // never matches or adjoins a valid first column + + T out[NIX]; +#pragma unroll + for (int i = 0; i < NIX; ++i) + { + const int dstX = x0 + i; + const float srcX = ((float)dstX + .5f) * scaleRatio.x - .5f; + const int iSrcX = (int)floor(srcX); + const float fx = srcX - iSrcX; float wx[4]; - float wy[4]; + GetCubicCoeffs(fx, wx[0], wx[1], wx[2], wx[3]); + + if (iSrcX == prevSx + 1) + { +#pragma unroll + for (int cy = 0; cy < 4; ++cy) + { + win[cy][0] = win[cy][1]; + win[cy][1] = win[cy][2]; + win[cy][2] = win[cy][3]; + win[cy][3] = cuda::StaticCast(rows[cy][cuda::clamp(iSrcX + 2, 0, srcSize.x - 1)]); + } + } + else if (iSrcX != prevSx) + { +#pragma unroll + for (int cy = 0; cy < 4; ++cy) + { +#pragma unroll + for (int cx = 0; cx < 4; ++cx) + { + win[cy][cx] = cuda::StaticCast(rows[cy][cuda::clamp(iSrcX + cx - 1, 0, srcSize.x - 1)]); + } + } + } + prevSx = iSrcX; + + FT sum = FT{}; +#pragma unroll + for (int cy = 0; cy < 4; ++cy) + { +#pragma unroll + for (int cx = 0; cx < 4; ++cx) + { + sum += win[cy][cx] * (wx[cx] * wy[cy]); + } + } + out[i] = cuda::SaturateCast(sum); + } + + T *dstRow = dst.ptr(z, dstY); + const int validCount = cuda::min(NIX, dstSize.x - x0); + + if (validCount == NIX && CheckRowAlign(dstRow)) // uniform across the warp + WritePack(dstRow[x0], out); + else + { + T *dstPtr = dstRow + x0; +#pragma unroll + for (int c = 0; c < NIX; ++c) + if (c < validCount) + dstPtr[c] = out[c]; + } +} + +// Exact-2x cubic upscale for wide float elements (float3/float4), which the register-staged +// CubicResizeExpand2x excludes: a full NIX-column window would need 75-100 registers of staged +// data. Instead each thread owns one output column x 2 output rows -- the vertical pair shares its +// 4-column x-window, so the staged window is 5 rows x 4 columns (20 wide elements) and each output +// costs 10 staged loads instead of 16 gathered ones, with the per-pixel coefficient math replaced +// by the two per-axis phase tables. Tap selection, weight values, and accumulation order match +// CubicResize (and the smem kernel), so the result is bit-exact. +template +__global__ void CubicResizeExpand2xWide(SrcWrapper src, DstWrapper dst, int2 srcSize, int2 dstSize) +{ + using T = typename DstWrapper::ValueType; + using FT = nvcv::cuda::ConvertBaseTypeTo; + + const int dstX = blockIdx.x * blockDim.x + threadIdx.x; + const int j = blockIdx.y * blockDim.y + threadIdx.y; // owned source row + const int z = blockIdx.z; + const int dstY = j * 2; + + if (dstX >= dstSize.x || dstY >= dstSize.y) + return; + + const bool odd = (dstX & 1) != 0; + const int k = dstX / 2; + // First x tap: even outputs of source col k tap k-2, odd outputs tap k-1. + const int cb = k - 2 + (odd ? 1 : 0); + + // Clamped 5-row x 4-column source window shared by the vertical output pair. + T win[5][4]; +#pragma unroll + for (int r = 0; r < 5; ++r) + { + const T *srcRow = src.ptr(z, cuda::clamp(j - 2 + r, 0, srcSize.y - 1)); +#pragma unroll + for (int c = 0; c < 4; ++c) + { + win[r][c] = srcRow[cuda::clamp(cb + c, 0, srcSize.x - 1)]; + } + } + + float wx[4], wyE[4], wyO[4]; + GetCubicCoeffs(odd ? 0.25f : 0.75f, wx[0], wx[1], wx[2], wx[3]); + GetCubicCoeffs(0.75f, wyE[0], wyE[1], wyE[2], wyE[3]); + GetCubicCoeffs(0.25f, wyO[0], wyO[1], wyO[2], wyO[3]); + + FT sum0 = FT{}; // even output row (taps j-2..j+1 -> window rows 0..3) + FT sum1 = FT{}; // odd output row (taps j-1..j+2 -> window rows 1..4) +#pragma unroll + for (int cy = 0; cy < 4; ++cy) + { +#pragma unroll + for (int cx = 0; cx < 4; ++cx) + { + sum0 += win[cy][cx] * (wx[cx] * wyE[cy]); + sum1 += win[cy + 1][cx] * (wx[cx] * wyO[cy]); + } + } + + dst[int3{dstX, dstY, z}] = cuda::SaturateCast(sum0); + dst[int3{dstX, dstY + 1, z}] = cuda::SaturateCast(sum1); +} + +inline bool UseSharedMemoryCubicExpand() +{ + int dev = 0; + NVCV_CHECK_THROW(cudaGetDevice(&dev)); + + int major = 0, minor = 0; + NVCV_CHECK_THROW(cudaDeviceGetAttribute(&major, cudaDevAttrComputeCapabilityMajor, dev)); + NVCV_CHECK_THROW(cudaDeviceGetAttribute(&minor, cudaDevAttrComputeCapabilityMinor, dev)); + + const int sm = major * 10 + minor; + + // The smem-tiled float CUBIC EXPAND path was validated on the reference CI SKUs + // (A100/H100: SM80/SM90). GA102/AD104 (SM86/SM89) regress and keep the direct gather. + return sm == 80 || sm == 90; +} + +// Exact-2x cubic upscale for 1-byte base types. The benched EXPAND configs (and any dst == 2*src +// resize) have only two coefficient phases per axis: srcX = (dstX+.5f)*.5f - .5f lands on k-0.25 +// for even dstX (fx = 0.75, taps k-2..k+1) and k+0.25 for odd dstX (fx = 0.25, taps k-1..k+2), and +// likewise per row. The general kernel is issue-bound on that per-pixel coefficient/clamp/index +// work (SM 83%, IPC 3.3, DRAM 11%), so this kernel hoists it wholesale: each thread owns NIX +// output columns x 2 output rows, computes the four 4-tap coefficient sets once (GetCubicCoeffs at +// exactly 0.25f/0.75f -- the same fx the general kernel derives, so the weights are identical +// floats), stages the shared 5-row x (NIX/2+4)-col clamped source window in registers, and emits +// two WritePack vector stores. Tap selection, weight values, and the cy-outer/cx-inner +// sum += tap * (wx*wy) accumulation order match CubicResize exactly, so the output is bit-exact. +// All loops fully unroll: window/output indices stay compile-time (runtime register indexing +// would spill to local memory). +template +__global__ void CubicResizeExpand2x(SrcWrapper src, DstWrapper dst, int2 srcSize, int2 dstSize) +{ + using T = typename DstWrapper::ValueType; + using FT = nvcv::cuda::ConvertBaseTypeTo; + + constexpr int KC = NIX / 2; // source columns owned per thread + constexpr int WC = KC + 4; // staged window columns (taps k0-2 .. k0+KC+1) + + const int k0 = (blockIdx.x * blockDim.x + threadIdx.x) * KC; // first owned source column + const int j = blockIdx.y * blockDim.y + threadIdx.y; // owned source row + const int z = blockIdx.z; + const int dstX0 = k0 * 2; + const int dstY0 = j * 2; + + if (dstX0 >= dstSize.x || dstY0 >= dstSize.y) + return; + + // Clamped source window shared by all 2*NIX outputs: win[r][c] = src[clamp(k0-2+c), clamp(j-2+r)]. + T win[5][WC]; +#pragma unroll + for (int r = 0; r < 5; ++r) + { + const T *srcRow = src.ptr(z, cuda::clamp(j - 2 + r, 0, srcSize.y - 1)); +#pragma unroll + for (int c = 0; c < WC; ++c) + { + win[r][c] = srcRow[cuda::clamp(k0 - 2 + c, 0, srcSize.x - 1)]; + } + } + // The two per-axis coefficient phases; identical values to the general kernel's per-pixel + // GetCubicCoeffs(fx) because fx is exactly 0.75f/0.25f for dst == 2*src. + float wxE[4], wxO[4], wyE[4], wyO[4]; + GetCubicCoeffs(0.75f, wxE[0], wxE[1], wxE[2], wxE[3]); + GetCubicCoeffs(0.25f, wxO[0], wxO[1], wxO[2], wxO[3]); + GetCubicCoeffs(0.75f, wyE[0], wyE[1], wyE[2], wyE[3]); + GetCubicCoeffs(0.25f, wyO[0], wyO[1], wyO[2], wyO[3]); + + T out0[NIX], out1[NIX]; +#pragma unroll + for (int i = 0; i < NIX; ++i) + { + const bool odd = (i & 1) != 0; + const float(&wx)[4] = odd ? wxO : wxE; + // First tap column within the window: even outputs of source col k tap k-2 (window index + // k-k0), odd outputs tap k-1 (window index k-k0+1). + const int cb = (i / 2) + (odd ? 1 : 0); + + FT sum0 = FT{}; // even output row (taps j-2..j+1 -> window rows 0..3) + FT sum1 = FT{}; // odd output row (taps j-1..j+2 -> window rows 1..4) +#pragma unroll + for (int cy = 0; cy < 4; ++cy) + { +#pragma unroll + for (int cx = 0; cx < 4; ++cx) + { + sum0 += win[cy][cb + cx] * (wx[cx] * wyE[cy]); + sum1 += win[cy + 1][cb + cx] * (wx[cx] * wyO[cy]); + } + } + out0[i] = cuda::SaturateCast(sum0); + out1[i] = cuda::SaturateCast(sum1); + } + + const int validCount = cuda::min(NIX, dstSize.x - dstX0); + // dstSize.y = 2*srcSize.y is even, so the odd output row always exists. +#pragma unroll + for (int p = 0; p < 2; ++p) + { + T *dstRow = dst.ptr(z, dstY0 + p); + const T(&outp)[NIX] = p == 0 ? out0 : out1; + + if (validCount == NIX && CheckRowAlign(dstRow)) // uniform across the warp + WritePack(dstRow[dstX0], outp); + else + { + T *dstPtr = dstRow + dstX0; +#pragma unroll + for (int c = 0; c < NIX; ++c) + if (c < validCount) + dstPtr[c] = outp[c]; + } + } +} + +template +__global__ void CubicResize(SrcWrapper src, DstWrapper dst, int2 srcSize, int2 dstSize, float2 scaleRatio) +{ + using T = typename DstWrapper::ValueType; + using FT = nvcv::cuda::ConvertBaseTypeTo; + + const int z = blockIdx.z; + const int dstY = blockIdx.y * blockDim.y + threadIdx.y; + + if (dstY >= dstSize.y) + return; + + // y-axis terms are row-invariant: compute once, reuse across this thread's COLS columns. + const float srcY = ((float)dstY + .5f) * scaleRatio.y - .5f; + const int iSrcY = (int)floor(srcY); + const float fy = srcY - iSrcY; + + float wy[4]; + GetCubicCoeffs(fy, wy[0], wy[1], wy[2], wy[3]); + + // Clamp each y tap coordinate to [0, srcSize-1] (replicate border) -- shared by all columns. + int sy[4]; +#pragma unroll + for (int k = 0; k < 4; ++k) sy[k] = cuda::clamp(iSrcY + k - 1, 0, srcSize.y - 1); + + const int xBase = blockIdx.x * blockDim.x * COLS + threadIdx.x; + +#pragma unroll + for (int i = 0; i < COLS; ++i) + { + const int dstX = xBase + i * (int)blockDim.x; + if (dstX >= dstSize.x) + continue; + + const float srcX = ((float)dstX + .5f) * scaleRatio.x - .5f; + const int iSrcX = (int)floor(srcX); + const float fx = srcX - iSrcX; + + float wx[4]; GetCubicCoeffs(fx, wx[0], wx[1], wx[2], wx[3]); - GetCubicCoeffs(fy, wy[0], wy[1], wy[2], wy[3]); FT sum = FT{}; + // Clamp each source tap coordinate independently to [0, srcSize-1] (replicate border), + // matching the behavior of OpenCV INTER_CUBIC and PyTorch bicubic interpolation. #pragma unroll - for (int cy = -1; cy <= 2; cy++) + for (int cy = 0; cy < 4; cy++) { #pragma unroll - for (int cx = -1; cx <= 2; cx++) + for (int cx = 0; cx < 4; cx++) { - sum += src[int3{iSrcCoord.x + cx, iSrcCoord.y + cy, iSrcCoord.z}] * (wx[cx + 1] * wy[cy + 1]); + int sx = cuda::clamp(iSrcX + cx - 1, 0, srcSize.x - 1); + sum += src[int3{sx, sy[cy], z}] * (wx[cx] * wy[cy]); } } - dst[dstCoord] = cuda::SaturateCast(cuda::abs(sum)); + // SaturateCast clamps to the valid output range (e.g. [0,255] for uint8). + // No abs() -- cubic interpolation can produce slightly negative intermediate values; + // the correct behavior is to clamp to 0, which SaturateCast already handles. + dst[int3{dstX, dstY, z}] = cuda::SaturateCast(sum); + } +} + +// Planar cubic resize: one thread owns an output pixel across ALL channel planes of a sample. +// The cubic coefficients and clamped tap coordinates depend only on (x, y), not on the channel, +// so they are computed once and reused for every plane -- avoiding the (channels - 1)x redundant +// coefficient math that results from launching the single-channel CubicResize over N*C flattened +// planes. The flattened single-channel view maps plane (n, c) to sample index n*channels + c. +template +__global__ void CubicResizePlanar(SrcWrapper src, DstWrapper dst, int2 srcSize, int2 dstSize, float2 scaleRatio, + int channels) +{ + using T = typename DstWrapper::ValueType; + using FT = nvcv::cuda::ConvertBaseTypeTo; + + const int dstX = blockIdx.x * blockDim.x + threadIdx.x; + const int dstY = blockIdx.y * blockDim.y + threadIdx.y; + const int n = blockIdx.z; + + if (dstY < dstSize.y && dstX < dstSize.x) + { + float2 srcCoord{((float)dstX + .5f) * scaleRatio.x - .5f, ((float)dstY + .5f) * scaleRatio.y - .5f}; + int2 iSrcCoord{(int)floor(srcCoord.x), (int)floor(srcCoord.y)}; + + float fx = srcCoord.x - iSrcCoord.x; + float fy = srcCoord.y - iSrcCoord.y; + + float wx[4]; + float wy[4]; + GetCubicCoeffs(fx, wx[0], wx[1], wx[2], wx[3]); + GetCubicCoeffs(fy, wy[0], wy[1], wy[2], wy[3]); + + // Clamp each source tap coordinate once (replicate border); reused across all planes. + // Precompute the 16 separable tap weights once too -- they are channel-independent, so the + // channel loop becomes a pure load + FMA over shared weights. + int sx[4]; + int sy[4]; + float w[4][4]; +#pragma unroll + for (int k = 0; k < 4; ++k) + { + sx[k] = cuda::clamp(iSrcCoord.x + k - 1, 0, srcSize.x - 1); + sy[k] = cuda::clamp(iSrcCoord.y + k - 1, 0, srcSize.y - 1); + } +#pragma unroll + for (int cy = 0; cy < 4; ++cy) +#pragma unroll + for (int cx = 0; cx < 4; ++cx) w[cy][cx] = wx[cx] * wy[cy]; + + const int base = n * channels; + for (int c = 0; c < channels; ++c) + { + const int plane = base + c; + + FT sum = FT{}; +#pragma unroll + for (int cy = 0; cy < 4; cy++) + { +#pragma unroll + for (int cx = 0; cx < 4; cx++) + { + sum += src[int3{sx[cx], sy[cy], plane}] * w[cy][cx]; + } + } + dst[int3{dstX, dstY, plane}] = cuda::SaturateCast(sum); + } } } // Area ------------------------------------------------------------------------ +// Each thread produces NIX consecutive output pixels of a row and writes them as one vector +// store. The scalar one-pixel-per-thread kernel left the narrow-element AREA path (uint8 NIX=16, +// uchar3/uchar4 NIX=4) bound by tiny, poorly-coalesced stores and per-thread launch overhead; +// batching NIX pixels + a single WritePack fixes that. The per-pixel value +// `src[StaticCast(coord)]` (the AREA box filter inside the interpolation wrap) is identical +// to the scalar kernel, so the result is bit-exact. Wide elements with NIX==1 (float3/float4) +// degrade to one pixel per thread -- same work as before. +template +__global__ void AreaResizeVec(SrcWrapper src, DstWrapper dst, int2 dstSize) +{ + using T = typename DstWrapper::ValueType; + + int3 dstCoord; + dstCoord.z = blockIdx.z; + dstCoord.y = blockIdx.y * blockDim.y + threadIdx.y; + dstCoord.x = (blockIdx.x * blockDim.x + threadIdx.x) * NIX; + + if (dstCoord.y >= dstSize.y || dstCoord.x >= dstSize.x) + return; + + T *dstRow = dst.ptr(dstCoord.z, dstCoord.y); + const int validCount = cuda::min(NIX, dstSize.x - dstCoord.x); + + T dstPack[NIX]; +#pragma unroll + for (int p = 0; p < NIX; ++p) + { + if (p >= validCount) + break; + dstPack[p] = src[cuda::StaticCast(int3{dstCoord.x + p, dstCoord.y, dstCoord.z})]; + } + + if (validCount == NIX && CheckRowAlign(dstRow)) // uniform across the warp + WritePack(dstRow[dstCoord.x], dstPack); + else + { + T *dstPtr = dstRow + dstCoord.x; +#pragma unroll + for (int i = 0; i < NIX; ++i) + if (i < validCount) + dstPtr[i] = dstPack[i]; + } +} + +// Integer-ratio AREA downscale (the is_area_fast case: scale is an exact integer >= 1, e.g. the 2x +// CONTRACT configs). Each output pixel is the mean of an iscale.x * iscale.y source box. A dedicated +// kernel for this path -- dispatched at launch when the scale is integer -- replaces the +// InterpolationWrap's per-access AREA-mode runtime branch and index math with direct source-row- +// pointer accumulation (the wrap path was issue-bound on that ALU: ~56% compute / ALU-top at IPC +// 2.2). NIX columns/thread + WritePack keep stores coalesced (mirrors AreaResizeVec). The four source +// row pointers for the box are resolved per column from src.ptr(z, y); the integer box is always +// in-bounds for an exact ratio, but taps are min-clamped defensively. +// +// PRECISION TRADE (not bit-exact): the accumulation sums the box then normalizes once (sum * 1/area), +// which can differ from the wrap's per-tap weighting by <= 1 ULP-scale rounding -- the result stays +// within the +/-1 tolerance of the OpenCV AREA reference but is not byte-identical to the wrap-based +// kernel. Both the interleaved and planar(flattened) AREA paths use this same kernel for the integer +// case, so the planar==interleaved parity (EXPECT_EQ) is preserved. +template +__global__ void AreaResizeFastDirect(SrcWrapper src, DstWrapper dst, int2 srcSize, int2 dstSize, int2 iscale) +{ + using T = typename DstWrapper::ValueType; + using FT = nvcv::cuda::ConvertBaseTypeTo; + + const int z = blockIdx.z; + const int dstY = blockIdx.y * blockDim.y + threadIdx.y; + const int dstX0 = (blockIdx.x * blockDim.x + threadIdx.x) * NIX; + + if (dstY >= dstSize.y || dstX0 >= dstSize.x) + return; + + const int ymin = dstY * iscale.y; + const float invArea = 1.f / (float)(iscale.x * iscale.y); + T *dstRow = dst.ptr(z, dstY); + const int validCount = cuda::min(NIX, dstSize.x - dstX0); + + T dstPack[NIX]; +#pragma unroll + for (int p = 0; p < NIX; ++p) + { + if (p >= validCount) + break; + + const int xmin = (dstX0 + p) * iscale.x; + + FT acc = FT{}; + for (int yy = 0; yy < iscale.y; ++yy) + { + const T *srcRow = src.ptr(z, cuda::min(ymin + yy, srcSize.y - 1)); + for (int xx = 0; xx < iscale.x; ++xx) + acc += cuda::StaticCast(srcRow[cuda::min(xmin + xx, srcSize.x - 1)]); + } + dstPack[p] = cuda::SaturateCast(acc * invArea); + } + + if (validCount == NIX && CheckRowAlign(dstRow)) + WritePack(dstRow[dstX0], dstPack); + else + { + T *dstPtr = dstRow + dstX0; +#pragma unroll + for (int i = 0; i < NIX; ++i) + if (i < validCount) + dstPtr[i] = dstPack[i]; + } +} + +// Exact-2x specialization of AreaResizeFastDirect: every output is the mean of a fully-in-bounds +// 2x2 box, so the runtime-bounded box loops and defensive tap clamps collapse to four unrolled +// taps, and the 2*NIX consecutive source elements each thread needs per row are staged as two +// vector packs instead of single-element loads (the general integer kernel is L1-transaction-bound +// on the byte rows: 33% A100 / 24% H100 BWUtil). Accumulation is the same raw sum then one +// * invArea (0.25f) normalize as AreaResizeFastDirect, so the result is bit-exact with it; the +// flattened planar view takes this path too. template -__global__ void AreaResize(SrcWrapper src, DstWrapper dst, int2 dstSize) +__global__ void AreaResizeContract2x(SrcWrapper src, DstWrapper dst, int2 dstSize) +{ + using T = typename DstWrapper::ValueType; + using FT = nvcv::cuda::ConvertBaseTypeTo; + + const int z = blockIdx.z; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const int x0 = (blockIdx.x * blockDim.x + threadIdx.x) * NIX; + + if (y >= dstSize.y || x0 >= dstSize.x) + return; + + const T *row0 = src.ptr(z, 2 * y); + const T *row1 = src.ptr(z, 2 * y + 1); + + const int sx0 = 2 * x0; + const int validCount = cuda::min(NIX, dstSize.x - x0); + + T win0[2 * NIX], win1[2 * NIX]; + if (validCount == NIX && CheckRowAlign(const_cast(row0) + sx0) + && CheckRowAlign(const_cast(row1) + sx0)) + { +#pragma unroll + for (int p = 0; p < 2; ++p) + { + reinterpret_cast *>(win0)[p] = reinterpret_cast *>(row0 + sx0)[p]; + reinterpret_cast *>(win1)[p] = reinterpret_cast *>(row1 + sx0)[p]; + } + } + else + { + // Source width is exactly 2*dstSize.x, so tail loads stay guarded per element. +#pragma unroll + for (int c = 0; c < 2 * NIX; ++c) + { + const int sx = sx0 + c; + win0[c] = (sx < 2 * dstSize.x) ? row0[sx] : T{}; + win1[c] = (sx < 2 * dstSize.x) ? row1[sx] : T{}; + } + } + + T dstPack[NIX]; +#pragma unroll + for (int i = 0; i < NIX; ++i) + { + FT acc = FT{}; + acc += cuda::StaticCast(win0[2 * i]); + acc += cuda::StaticCast(win0[2 * i + 1]); + acc += cuda::StaticCast(win1[2 * i]); + acc += cuda::StaticCast(win1[2 * i + 1]); + dstPack[i] = cuda::SaturateCast(acc * 0.25f); + } + + T *dstRow = dst.ptr(z, y); + if (validCount == NIX && CheckRowAlign(dstRow)) + WritePack(dstRow[x0], dstPack); + else + { + T *dstPtr = dstRow + x0; +#pragma unroll + for (int i = 0; i < NIX; ++i) + if (i < validCount) + dstPtr[i] = dstPack[i]; + } +} + +inline bool UseContract2xSingleOutputPack() +{ + int sm = 0; + NVCV_CHECK_THROW(cvcuda::priv::GetCurrentDeviceSM(sm)); + + // A single-output pack has no cross-output load reuse, and its staged load burst stalls on + // Turing. Ampere and newer architectures retain the profitable specialization. + return sm >= 80; +} + +// Fractional AREA downscale for the tensor path with NIX consecutive output columns per thread. +// Every box tap of a zoom-out is in bounds, so the InterpolationWrap's per-tap border handling and +// per-access bounds recomputation are replaced by hoisted row pointers and once-per-thread y box +// terms; the NIX results leave as one vector pack (the wrap-based kernel is issue/L1-bound on the +// anisotropic rows: 12.8-30% BWUtil). Term order replicates the wrap's AddFractionalArea exactly -- +// interior rows (columns ascending, then left, then right edge), top edge row (columns, then +// top-left, then top-right corner), bottom edge row (columns, then bottom-right, then bottom-left +// corner), each tap * scale -- so the result is bit-exact with operator[] and with +// AreaResizePlanar's bounds-sharing path. Integer ratios and zoom-ins keep their existing kernels. +template +__global__ void AreaResizeFractional(SrcWrapper src, DstWrapper dst, int2 srcSize, int2 dstSize, float2 scaleRatio) +{ + using T = typename DstWrapper::ValueType; + using FT = nvcv::cuda::ConvertBaseTypeTo; + + // Independent accumulation chains per thread: the serial one-output-at-a-time walk left the + // kernel memory-latency bound (IPC 0.78, 39 warp cycles per instruction, DRAM 64%). Grouping + // GN outputs and bounding the interior box width at compile time (dispatch guarantees + // scale < 3, so at most kMaxC interior columns) turns each source row into an unrolled grid of + // independent guarded loads and FMAs. Per-output term order is unchanged -- interior columns + // ascending, then left, then right edge per row; top row with its corners; bottom row with its + // corners -- so the result stays bit-exact with the wrap's AddFractionalArea. + // Three-channel elements lose more to L1 pressure than they gain from extra chains. + constexpr int GN = (cuda::NumElements == 3) ? 2 : 4; + constexpr int kMaxC = 3; + + const int z = blockIdx.z; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const int x0 = (blockIdx.x * blockDim.x + threadIdx.x) * NIX; + + if (y >= dstSize.y || x0 >= dstSize.x) + return; + + const float fsy1 = y * scaleRatio.y; + const float fsy2 = fsy1 + scaleRatio.y; + const int sy1 = cuda::round(fsy1); + const int sy2 = cuda::round(fsy2); + + // Guard the edge taps against the float-rounding boundary: fsx2/fsy2 = dst*scale can exceed + // the source extent by an ulp, putting sx2/sy2 one past the last element. The wrap-based + // kernels read the constant border (zero) there, contributing exactly +0.0, so skipping the + // tap is bit-identical -- and it is what keeps this kernel's reads in bounds. + const bool hasTop = (float)sy1 > fsy1; + const bool hasBot = (float)sy2 < fsy2 && sy2 < srcSize.y; + const float wTop = (float)sy1 - fsy1; + const float wBot = fsy2 - (float)sy2; + + T out[NIX]; + +#pragma unroll + for (int g = 0; g < NIX; g += GN) + { + float fsx1[GN], fsx2[GN], scaleA[GN], wL[GN], wR[GN]; + int sx1[GN], sx2[GN], nC[GN]; + bool hasL[GN], hasR[GN]; +#pragma unroll + for (int j = 0; j < GN; ++j) + { + const int x = cuda::min(x0 + g + j, dstSize.x - 1); + fsx1[j] = x * scaleRatio.x; + fsx2[j] = fsx1[j] + scaleRatio.x; + sx1[j] = cuda::round(fsx1[j]); + sx2[j] = cuda::round(fsx2[j]); + nC[j] = sx2[j] - sx1[j]; + hasL[j] = (float)sx1[j] > fsx1[j]; + hasR[j] = (float)sx2[j] < fsx2[j] && sx2[j] < srcSize.x; + wL[j] = (float)sx1[j] - fsx1[j]; + wR[j] = fsx2[j] - (float)sx2[j]; + scaleA[j] = 1.f / (fminf(scaleRatio.x, srcSize.x - fsx1[j]) * fminf(scaleRatio.y, srcSize.y - fsy1)); + } + + FT acc[GN]; +#pragma unroll + for (int j = 0; j < GN; ++j) acc[j] = FT{}; + + for (int cy = sy1; cy < sy2; ++cy) + { + const T *row = src.ptr(z, cy); +#pragma unroll + for (int j = 0; j < GN; ++j) + { +#pragma unroll + for (int c = 0; c < kMaxC; ++c) + { + if (c < nC[j]) + acc[j] += row[sx1[j] + c] * scaleA[j]; + } + } +#pragma unroll + for (int j = 0; j < GN; ++j) + { + if (hasL[j]) + acc[j] += row[sx1[j] - 1] * (wL[j] * scaleA[j]); + if (hasR[j]) + acc[j] += row[sx2[j]] * (wR[j] * scaleA[j]); + } + } + + if (hasTop) + { + const T *row = src.ptr(z, sy1 - 1); +#pragma unroll + for (int j = 0; j < GN; ++j) + { +#pragma unroll + for (int c = 0; c < kMaxC; ++c) + { + if (c < nC[j]) + acc[j] += row[sx1[j] + c] * (wTop * scaleA[j]); + } + if (hasL[j]) + acc[j] += row[sx1[j] - 1] * (wTop * wL[j] * scaleA[j]); + if (hasR[j]) + acc[j] += row[sx2[j]] * (wTop * wR[j] * scaleA[j]); + } + } + + if (hasBot) + { + const T *row = src.ptr(z, sy2); +#pragma unroll + for (int j = 0; j < GN; ++j) + { +#pragma unroll + for (int c = 0; c < kMaxC; ++c) + { + if (c < nC[j]) + acc[j] += row[sx1[j] + c] * (wBot * scaleA[j]); + } + if (hasR[j]) + acc[j] += row[sx2[j]] * (wBot * wR[j] * scaleA[j]); + if (hasL[j]) + acc[j] += row[sx1[j] - 1] * (wBot * wL[j] * scaleA[j]); + } + } + +#pragma unroll + for (int j = 0; j < GN; ++j) out[g + j] = cuda::SaturateCast(acc[j]); + } + + T *dstRow = dst.ptr(z, y); + const int validCount = cuda::min(NIX, dstSize.x - x0); + + if (validCount == NIX && CheckRowAlign(dstRow)) // uniform across the warp + WritePack(dstRow[x0], out); + else + { + T *dstPtr = dstRow + x0; +#pragma unroll + for (int c = 0; c < NIX; ++c) + if (c < validCount) + dstPtr[c] = out[c]; + } +} + +// Planar area resize: one thread owns an output pixel across ALL channel planes of a sample. The area +// box geometry depends only on (x, y), not the channel, so it is computed once (via the interpolation +// wrap's reusable bounds) and reused for every plane -- avoiding the per-plane recomputation incurred +// by running the single-channel AreaResize over N*C flattened planes. Plane (n, c) is sample n*C + c. +template +__global__ void AreaResizePlanar(SrcWrapper src, DstWrapper dst, int2 dstSize, int channels) { int x = blockDim.x * blockIdx.x + threadIdx.x; int y = blockDim.y * blockIdx.y + threadIdx.y; - int z = blockIdx.z; + int n = blockIdx.z; if (x >= dstSize.x || y >= dstSize.y) return; - int3 coord{x, y, z}; + const int base = n * channels; + + // Bounds depend only on (x, y); compute once for this sample and reuse across its planes. + const auto bounds = src.computeAreaBounds(float3{(float)x, (float)y, (float)base}); - dst[coord] = src[cuda::StaticCast(coord)]; + for (int c = 0; c < channels; ++c) + { + const int plane = base + c; + dst[int3{x, y, plane}] = src.interpolateWithBounds(float3{(float)x, (float)y, (float)plane}, bounds); + } } // Host run resize functions --------------------------------------------------- @@ -409,7 +1354,7 @@ __global__ void AreaResize(SrcWrapper src, DstWrapper dst, int2 dstSize) template void RunResizeInterp(cudaStream_t stream, const nvcv::TensorDataStridedCuda &srcData, const nvcv::TensorDataStridedCuda &dstData, int2 srcSize, int2 dstSize, int batchSize, - const NVCVInterpolationType interpolation) + const NVCVInterpolationType interpolation, int planarChannels = 1) { float2 scaleRatio{(float)srcSize.x / dstSize.x, (float)srcSize.y / dstSize.y}; @@ -434,6 +1379,18 @@ void RunResizeInterp(cudaStream_t stream, const nvcv::TensorDataStridedCuda &src break; case NVCV_INTERP_LINEAR: + if constexpr (sizeof(cuda::BaseType) == 1) + { + // Exact-2x upscale fast path for byte types; the flattened planar view takes it too. + if (dstSize.x == 2 * srcSize.x && dstSize.y == 2 * srcSize.y) + { + dim3 e2Block(32, 4, 1); + dim3 e2Grid(util::DivUp(dstSize.x, e2Block.x * NIX), util::DivUp(dstSize.y, e2Block.y * 2), + batchSize); + LinearResizeExpand2x<<>>(srcTW, dstTW, srcSize, dstSize); + break; + } + } if (scaleRatio.x < 2) LinearResize<<>>(srcTW, dstTW, srcSize, dstSize, scaleRatio); else @@ -441,12 +1398,151 @@ void RunResizeInterp(cudaStream_t stream, const nvcv::TensorDataStridedCuda &src break; case NVCV_INTERP_CUBIC: - CubicResize<<>>(srcTW, dstTW, srcSize, dstSize, scaleRatio); + { + // Byte types (any channel count) and scalar types (any base size) fit the register-staged + // window; wide float3/float4 interleaved take the vertical-pair variant below. + if constexpr (sizeof(cuda::BaseType) == 1 || cuda::NumElements == 1) + { + // Exact-2x upscale fast path (issue-bound in the general kernel). The flattened planar + // view takes it too -- including float planes: per-plane phases and weights are + // identical, so it supersedes CubicResizePlanar's per-pixel channel amortization for + // this case while remaining bit-exact with it. + if (dstSize.x == 2 * srcSize.x && dstSize.y == 2 * srcSize.y) + { + dim3 e2Block(32, 4, 1); + dim3 e2Grid(util::DivUp(dstSize.x, e2Block.x * NIX), util::DivUp(dstSize.y, e2Block.y * 2), + batchSize); + CubicResizeExpand2x<<>>(srcTW, dstTW, srcSize, dstSize); + break; + } + // Any other upscale: consecutive columns with the float-cached sliding window. + if (dstSize.x > srcSize.x && dstSize.y > srcSize.y) + { + dim3 upBlock(32, 4, 1); + dim3 upGrid(util::DivUp(dstSize.x, upBlock.x * NIX), util::DivUp(dstSize.y, upBlock.y), batchSize); + CubicResizeUpscale<<>>(srcTW, dstTW, srcSize, dstSize, scaleRatio); + break; + } + } + if (planarChannels > 1) + { + // grid.z spans samples (not N*C planes); each thread loops the channel planes. + // A 2D (64x2) block gives the 4x4 cubic window better L1 reuse across neighbouring rows + // than the default 1D (128x1) block, which matters most for the load-bound float planes. + dim3 pcBlock(64, 2, 1); + dim3 blocksP(util::DivUp(dstSize.x, pcBlock.x), util::DivUp(dstSize.y, pcBlock.y), + batchSize / planarChannels); + CubicResizePlanar<<>>(srcTW, dstTW, srcSize, dstSize, scaleRatio, + planarChannels); + break; + } + if constexpr (sizeof(cuda::BaseType) == 4 && cuda::NumElements < T >> 1) + { + // Exact-2x upscale for wide float elements: the vertical-pair register window replaces + // the per-pixel coefficient math and the smem tile round-trip on this case. + if (dstSize.x == 2 * srcSize.x && dstSize.y == 2 * srcSize.y) + { + dim3 wBlock(32, 4, 1); + dim3 wGrid(util::DivUp(dstSize.x, wBlock.x), util::DivUp(dstSize.y, wBlock.y * 2), batchSize); + CubicResizeExpand2xWide<<>>(srcTW, dstTW, srcSize, dstSize); + break; + } + } + if constexpr (sizeof(cuda::BaseType) == 4) + { + if (scaleRatio.x < 1.f && scaleRatio.y < 1.f && UseSharedMemoryCubicExpand()) + { + // Float (float32/float3/float4) upscale: the 4x4 gather is L1/TEX-bound on reference SKUs + // (DRAM has slack). Stage the small per-block source tile in shared memory and serve the + // 16 taps/pixel from smem. tile dims are an upper bound on (block_extent*scale + cubic + // support); generous slack keeps every block's taps in range. Bit-exact with CubicResize. + dim3 scBlock(32, 8, 1); + dim3 scGrid(util::DivUp(dstSize.x, scBlock.x), util::DivUp(dstSize.y, scBlock.y), batchSize); + const int tileW = (int)ceilf(scBlock.x * scaleRatio.x) + 6; + const int tileH = (int)ceilf(scBlock.y * scaleRatio.y) + 6; + const size_t smemBytes = (size_t)tileW * tileH * sizeof(T); + CubicResizeSharedExpand<<>>(srcTW, dstTW, srcSize, dstSize, + scaleRatio, tileW, tileH); + break; + } + } + + // Each thread processes CUBIC_COLS output columns so the row-invariant y-axis cubic + // coefficients and clamps are computed once and reused (the kernel was issue-bound on + // redundant per-pixel ALU work); columns are grid-strided by blockDim.x for coalescing. + // Gated to 1-byte-base-type elements (uint8/uchar3/uchar4): a CI run on A100 showed the + // multi-column path improves uint8 ~11% but regresses float32/float3 CUBIC by +17-44% + // (memory/latency-bound there, hurt by the added register pressure), so float base types + // keep the original one-column path. Scalar uint8 profiles as instruction-bound even at + // COLS=4, so it gets a little more y-axis reuse while byte-vector types keep the proven + // lower-register grouping. CUBIC_COLS=1 is byte-identical to the scalar kernel. + constexpr int CUBIC_COLS = (sizeof(cuda::BaseType) == 1) ? (cuda::NumElements == 1 ? 8 : 4) : 1; + dim3 blocksC(util::DivUp(dstSize.x, threads2.x * CUBIC_COLS), util::DivUp(dstSize.y, threads2.y), batchSize); + CubicResize<<>>(srcTW, dstTW, srcSize, dstSize, scaleRatio); break; + } case NVCV_INTERP_AREA: - AreaResize<<>>(srcIW, dstTW, dstSize); + { + // Exact integer-ratio downscale (e.g. the 2x CONTRACT configs) takes a dedicated direct-pointer + // kernel that drops the InterpolationWrap's per-access AREA-mode runtime branch + index math + // (precision trade, within +/-1 of the AREA reference). Used for BOTH interleaved and planar + // (flattened) so their parity is preserved. All other AREA cases (non-integer downscale, any + // upscale/zoom-in) keep the wrap-based kernels below. + const int isx = (int)(scaleRatio.x + 0.5f); + const int isy = (int)(scaleRatio.y + 0.5f); + const bool areaFast = scaleRatio.x >= 1.f && scaleRatio.y >= 1.f && fabsf(scaleRatio.x - isx) < 1e-5f + && fabsf(scaleRatio.y - isy) < 1e-5f; + if (areaFast) + { + const bool exact2x = isx == 2 && isy == 2 && dstSize.x * 2 == srcSize.x && dstSize.y * 2 == srcSize.y; + bool useContract2x = exact2x; + + // Multi-output packs remain profitable on every supported GPU. Only unamortized + // single-output packs need an architecture-specific decision; flattened planar float + // has NIX=4 and never enters this branch. + if constexpr (NIX == 1) + useContract2x = useContract2x && UseContract2xSingleOutputPack(); + + if (useContract2x) + AreaResizeContract2x<<>>(srcTW, dstTW, dstSize); + else + AreaResizeFastDirect<<>>(srcTW, dstTW, srcSize, dstSize, int2{isx, isy}); + } + // Vectorized wrap path: each thread emits NIX output pixels with one vector store. For + // interleaved this wins on narrow elements and is a no-op (NIX=1) on float3/float4. For planar + // (NCHW) on the flattened single-channel view, a 1-byte element (uint8, NIX=16) vectorizes + // hugely, but a wide float plane (NIX=4) loses the per-output bounds-sharing that + // AreaResizePlanar gets across channels and regresses CONTRACT, so wide-float planar keeps the + // channel-amortized kernel. + else + { + // Fractional zoom-out: every tap is in bounds, so the dedicated kernel replaces the + // wrap-based paths (see AreaResizeFractional); zoom-ins keep the wrap kernels, and + // wide-float planar planes keep AreaResizePlanar's cross-channel bounds amortization + // (routing them here regressed float3/float4 planes 14-28% locally). + // The kernel's compile-time interior-column bound (kMaxC) holds for x scales < 3; + // larger fractional downscales keep the wrap path. + if (scaleRatio.x >= 1.f && scaleRatio.y >= 1.f && scaleRatio.x < 3.f + && (planarChannels == 1 || sizeof(cuda::BaseType) == 1)) + { + AreaResizeFractional<<>>(srcTW, dstTW, srcSize, dstSize, scaleRatio); + break; + } + if constexpr (NIX < 8) + { + if (planarChannels > 1) + { + dim3 blocksP(util::DivUp(dstSize.x, threads2.x), util::DivUp(dstSize.y, threads2.y), + batchSize / planarChannels); + AreaResizePlanar<<>>(srcIW, dstTW, dstSize, planarChannels); + break; + } + } + AreaResizeVec<<>>(srcIW, dstTW, dstSize); + } break; + } default: throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Invalid interpolation"); @@ -455,7 +1551,7 @@ void RunResizeInterp(cudaStream_t stream, const nvcv::TensorDataStridedCuda &src inline void RunResizeInterpType(cudaStream_t stream, const nvcv::TensorDataStridedCuda &srcData, const nvcv::TensorDataStridedCuda &dstData, int2 srcSize, int2 dstSize, int numChannels, - int batchSize, const NVCVInterpolationType interpolation) + int batchSize, const NVCVInterpolationType interpolation, int planarChannels = 1) { // The data type may contain the channels baked in or the number of channels is in the tensor shape @@ -464,7 +1560,7 @@ inline void RunResizeInterpType(cudaStream_t stream, const nvcv::TensorDataStrid #define CVCUDA_RUN_RESIZE(BT, DT, T) \ ((srcData.dtype() == nvcv::TYPE_##BT && numChannels == cuda::NumElements) \ || (srcData.dtype() == nvcv::TYPE_##DT && numChannels == 1)) \ - RunResizeInterp(stream, srcData, dstData, srcSize, dstSize, batchSize, interpolation); + RunResizeInterp(stream, srcData, dstData, srcSize, dstSize, batchSize, interpolation, planarChannels); if CVCUDA_RUN_RESIZE(U8, U8, uchar1) else if CVCUDA_RUN_RESIZE(U8, 3U8, uchar3) @@ -505,9 +1601,11 @@ void Resize::RunResize(cudaStream_t stream, const nvcv::TensorDataStridedCuda &s { throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input and output data layout are different"); } - if (srcData.layout() != nvcv::TENSOR_HWC && srcData.layout() != nvcv::TENSOR_NHWC) + const nvcv::TensorLayout layout = srcData.layout(); + const bool isPlanar = (layout == nvcv::TENSOR_NCHW || layout == nvcv::TENSOR_CHW); + if (layout != nvcv::TENSOR_HWC && layout != nvcv::TENSOR_NHWC && !isPlanar) { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input must have (N)HWC layout"); + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input must have (N)HWC or (N)CHW layout"); } auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(srcData); @@ -544,6 +1642,35 @@ void Resize::RunResize(cudaStream_t stream, const nvcv::TensorDataStridedCuda &s int2 srcSize{(int)srcAccess->numCols(), (int)srcAccess->numRows()}; int2 dstSize{(int)dstAccess->numCols(), (int)dstAccess->numRows()}; + if (interpolation == NVCV_INTERP_LINEAR && (srcSize.x < 2 || srcSize.y < 2)) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Linear interpolation requires source dimensions of at least 2x2"); + } + + if (isPlanar) + { + // View each of the N*C channel planes as a single-channel sample and reuse the + // interleaved single-channel resize path. Channels are independent in resize, so this + // produces identical results to resizing the equivalent NHWC single-channel data. + // The flattened plane count becomes the kernel's grid z-dimension, so it must fit both a + // 32-bit batch size and CUDA's 65535 grid-z limit; compute it in 64-bit to avoid overflow. + constexpr int64_t kMaxGridZ = 65535; + const int64_t planarBatch = static_cast(numChannels) * batchSize; + if (planarBatch > kMaxGridZ) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Planar resize requires numSamples * numChannels <= 65535 (CUDA grid-z limit)"); + } + + auto srcView = PlanarAsSingleChannelView(srcData, *srcAccess); + auto dstView = PlanarAsSingleChannelView(dstData, *dstAccess); + RunResizeInterpType(stream, srcView, dstView, srcSize, dstSize, /*numChannels=*/1, + /*batchSize=*/static_cast(planarBatch), interpolation, + /*planarChannels=*/static_cast(numChannels)); + return; + } + RunResizeInterpType(stream, srcData, dstData, srcSize, dstSize, numChannels, batchSize, interpolation); } diff --git a/src/cvcuda/priv/OpResizeCropConvertReformat.cu b/src/cvcuda/priv/OpResizeCropConvertReformat.cu index 12753b907..777fad38f 100644 --- a/src/cvcuda/priv/OpResizeCropConvertReformat.cu +++ b/src/cvcuda/priv/OpResizeCropConvertReformat.cu @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,6 +15,7 @@ * limitations under the License. */ +#include "Nvtx.hpp" #include "OpResizeCropConvertReformat.hpp" #include "legacy/CvCudaLegacy.h" #include "legacy/CvCudaLegacyHelpers.hpp" @@ -121,6 +122,146 @@ private: DstT *m_dst; }; +template +__device__ __forceinline__ PixelT MakePlanarPixel(const BaseT *plane0, int64_t planeStride) +{ + static_assert(cuda::NumElements == 1 || cuda::NumElements == 3); + + if constexpr (cuda::NumElements == 1) + { + return PixelT{plane0[0]}; + } + else + { + return PixelT{plane0[0], plane0[planeStride], plane0[2 * planeStride]}; + } +} + +template +class TensorPlanarSrc +{ +public: + using ValueType = SrcT; + using BaseT = cuda::BaseType; + + TensorPlanarSrc(const nvcv::TensorDataStridedCuda &srcData, + const nvcv::TensorDataAccessStridedImagePlanar &srcAccess) + : m_src{reinterpret_cast(srcData.basePtr())} + , m_sampleStride{srcAccess.sampleStride() / static_cast(sizeof(BaseT))} + , m_planeStride{srcAccess.planeStride() / static_cast(sizeof(BaseT))} + , m_rowStride{srcAccess.rowStride() / static_cast(sizeof(BaseT))} + , m_colStride{srcAccess.colStride() / static_cast(sizeof(BaseT))} + { + } + + __device__ __forceinline__ SrcT read(int n, int y, int x) const + { + const BaseT *plane0 = m_src + n * m_sampleStride + y * m_rowStride + x * m_colStride; + return MakePlanarPixel(plane0, m_planeStride); + } + +private: + const BaseT *m_src; + int64_t m_sampleStride; + int64_t m_planeStride; + int64_t m_rowStride; + int64_t m_colStride; +}; + +template +class ImageBatchVarShapePlanarSrc +{ +public: + using ValueType = SrcT; + using BaseT = cuda::BaseType; + + explicit ImageBatchVarShapePlanarSrc(const nvcv::ImageBatchVarShapeDataStridedCuda &srcData) + : m_src{srcData} + { + } + + __host__ __device__ int width(int s) const + { + return m_src.width(s, 0); + } + + __host__ __device__ int height(int s) const + { + return m_src.height(s, 0); + } + + __device__ __forceinline__ SrcT read(int n, int y, int x) const + { + const BaseT *plane0 = m_src.ptr(n, 0, y, x); + + if constexpr (cuda::NumElements == 1) + { + return SrcT{plane0[0]}; + } + else + { + return SrcT{plane0[0], *m_src.ptr(n, 1, y, x), *m_src.ptr(n, 2, y, x)}; + } + } + +private: + cuda::ImageBatchVarShapeWrap m_src; +}; + +template +__device__ __forceinline__ std::remove_cv_t +ReadPixel(const SrcWrapper &src, int n, int y, int x) +{ + return *src.ptr(n, y, x); +} + +template +__device__ __forceinline__ SrcT ReadPixel(const TensorPlanarSrc &src, int n, int y, int x) +{ + return src.read(n, y, x); +} + +template +__device__ __forceinline__ SrcT ReadPixel(const ImageBatchVarShapePlanarSrc &src, int n, int y, int x) +{ + return src.read(n, y, x); +} + +template +__device__ __forceinline__ void WriteBilinearPixel(DstMap dst, const SrcWrapper &src, int n, int dstY, int dstX, + float fx, float fy, int srcW, int srcH, float scale, float offset, + bool srcCast) +{ + using SrcT = std::remove_cv_t; + + int sx0 = __float2int_rd(fx); + int sy0 = __float2int_rd(fy); + int sx1 = cuda::min(sx0 + 1, srcW - 1); + int sy1 = cuda::min(sy0 + 1, srcH - 1); + + fx -= sx0; + fy -= sy0; + sx0 = cuda::max(0, sx0); + sy0 = cuda::max(0, sy0); + sx1 = sx1 > sx0 ? sx1 : sx0; + + const SrcT src00 = ReadPixel(src, n, sy0, sx0); + const SrcT src01 = ReadPixel(src, n, sy0, sx1); + const SrcT src10 = ReadPixel(src, n, sy1, sx0); + const SrcT src11 = ReadPixel(src, n, sy1, sx1); + + if (srcCast) + { + dst(n, dstY, dstX, scale * cuda::SaturateCast((1-fy) * ((1-fx) * src00 + src01 * fx) + + fy * ((1-fx) * src10 + src11 * fx)) + offset); + } + else + { + dst(n, dstY, dstX, scale * ((1-fy) * ((1-fx) * src00 + src01 * fx) + + fy * ((1-fx) * src10 + src11 * fx)) + offset); + } +} + //******************** Tensor Source ********************// //******************** NN = Nearest Neighbor (TensorWrap) @@ -140,7 +281,7 @@ __global__ void resizeCrop_NN(DstMap dst, SrcWrapper src, const int sy = __float2int_rd((dst_y + crop.y + 0.5f) * resize.y); // Rescale, channel manipulation, convert type, and reformat. - dst(blockIdx.z, dst_y, dst_x, scale * *src.ptr((int)blockIdx.z, sy, sx) + offset); + dst(blockIdx.z, dst_y, dst_x, scale * ReadPixel(src, (int)blockIdx.z, sy, sx) + offset); } } // resizeCrop_NN @@ -150,8 +291,6 @@ __global__ void resizeCrop_bilinear(DstMap dst, SrcWrapper src, const int src_w, const float2 resize, const int2 crop, const float scale, const float offset, bool src_cast) { - using SrcT = typename SrcWrapper::ValueType; - const int dst_x = blockIdx.x * blockDim.x + threadIdx.x; const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; @@ -162,29 +301,8 @@ __global__ void resizeCrop_bilinear(DstMap dst, SrcWrapper src, const int src_w, float fx = (dst_x + crop.x + 0.5f) * resize.x - 0.5f; float fy = (dst_y + crop.y + 0.5f) * resize.y - 0.5f; - int sx0 = __float2int_rd(fx); - int sy0 = __float2int_rd(fy); - int sx1 = cuda::min(sx0 + 1, src_w - 1); - int sy1 = cuda::min(sy0 + 1, src_h - 1); - - fx -= sx0; - fy -= sy0; - sx0 = cuda::max(0, sx0); - sy0 = cuda::max(0, sy0); - sx1 = (sx1 > sx0); - - // Set up source row pointers. - const SrcT *ptr0 = src.ptr((int)blockIdx.z, sy0, sx0); // Pointer in upper row. - const SrcT *ptr1 = src.ptr((int)blockIdx.z, sy1, sx0); // Pointer in lower row. - // Bi-linear interpolation, rescale, channel manipulation, convert type, and reformat. - if (src_cast) - dst(blockIdx.z, dst_y, dst_x, - scale * cuda::SaturateCast((1-fy) * ((1-fx) * ptr0[0] + ptr0[sx1] * fx) - + fy * ((1-fx) * ptr1[0] + ptr1[sx1] * fx)) + offset); - else - dst(blockIdx.z, dst_y, dst_x, scale * ((1-fy) * ((1-fx) * ptr0[0] + ptr0[sx1] * fx) - + fy * ((1-fx) * ptr1[0] + ptr1[sx1] * fx)) + offset); + WriteBilinearPixel(dst, src, (int)blockIdx.z, dst_y, dst_x, fx, fy, src_w, src_h, scale, offset, src_cast); } } // resizeCrop_bilinear @@ -213,7 +331,7 @@ __global__ void resizeCrop_NN_varShape(DstMap dst, SrcWrapper src, const int sy = __float2int_rd((dst_y + crop.y + 0.5f) * resize_y); // Rescale, channel manipulation, convert type, and reformat. - dst(blockIdx.z, dst_y, dst_x, scale * *src.ptr((int)blockIdx.z, sy, sx) + offset); + dst(blockIdx.z, dst_y, dst_x, scale * ReadPixel(src, (int)blockIdx.z, sy, sx) + offset); } } // resizeCrop_NN_varShape @@ -223,8 +341,6 @@ __global__ void resizeCrop_bilinear_varShape(DstMap dst, SrcWrapper src, const NVCVSize2D resize, const int2 crop, float scale, float offset, bool src_cast) { - using SrcT = typename SrcWrapper::ValueType; - const int dst_x = blockIdx.x * blockDim.x + threadIdx.x; const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; @@ -242,32 +358,51 @@ __global__ void resizeCrop_bilinear_varShape(DstMap dst, SrcWrapper src, float fx = (dst_x + crop.x + 0.5f) * resize_x - 0.5f; float fy = (dst_y + crop.y + 0.5f) * resize_y - 0.5f; - int sx0 = __float2int_rd(fx); - int sy0 = __float2int_rd(fy); - int sx1 = cuda::min(sx0 + 1, src_w - 1); - int sy1 = cuda::min(sy0 + 1, src_h - 1); - - fx -= sx0; - fy -= sy0; - sx0 = cuda::max(0, sx0); - sy0 = cuda::max(0, sy0); - sx1 = (sx1 > sx0); - - // Set up source row pointers. - const SrcT *ptr0 = src.ptr((int)blockIdx.z, sy0, sx0); // Pointer in upper row. - const SrcT *ptr1 = src.ptr((int)blockIdx.z, sy1, sx0); // Pointer in lower row. - // Bi-linear interpolation, rescale, channel manipulation, convert type, and reformat. - if (src_cast) - dst(blockIdx.z, dst_y, dst_x, - scale * cuda::SaturateCast((1-fy) * ((1-fx) * ptr0[0] + ptr0[sx1] * fx) - + fy * ((1-fx) * ptr1[0] + ptr1[sx1] * fx)) + offset); - else - dst(blockIdx.z, dst_y, dst_x, scale * ((1-fy) * ((1-fx) * ptr0[0] + ptr0[sx1] * fx) - + fy * ((1-fx) * ptr1[0] + ptr1[sx1] * fx)) + offset); + WriteBilinearPixel(dst, src, (int)blockIdx.z, dst_y, dst_x, fx, fy, src_w, src_h, scale, offset, src_cast); } } // resizeCrop_bilinear_varShape +template +void LaunchTensorResizeCrop(DstMap dst, SrcWrapper src, const dim3 &gridSize, const dim3 &blockSize, + cudaStream_t stream, NVCVInterpolationType interp, int src_w, int src_h, float2 resize, + int2 cropPos, float scale, float offset, bool srcCast) +{ + switch (interp) + { + case NVCV_INTERP_NEAREST: + resizeCrop_NN<<>>(dst, src, resize, cropPos, scale, offset); + break; + + case NVCV_INTERP_LINEAR: + resizeCrop_bilinear<<>>(dst, src, src_w, src_h, resize, cropPos, scale, offset, + srcCast); + break; + default: + break; + } // switch +} + +template +void LaunchVarShapeResizeCrop(DstMap dst, SrcWrapper src, const dim3 &gridSize, const dim3 &blockSize, + cudaStream_t stream, NVCVInterpolationType interp, NVCVSize2D resizeDim, int2 cropPos, + float scale, float offset, bool srcCast) +{ + switch (interp) + { + case NVCV_INTERP_NEAREST: + resizeCrop_NN_varShape<<>>(dst, src, resizeDim, cropPos, scale, offset); + break; + + case NVCV_INTERP_LINEAR: + resizeCrop_bilinear_varShape<<>>(dst, src, resizeDim, cropPos, scale, offset, + srcCast); + break; + default: + break; + } // switch +} + // clang-format on template @@ -319,23 +454,23 @@ void resizeCropConvertReformat(const nvcv::TensorDataStridedCuda &srcData, const const dim3 blockSize(BLOCK_WIDTH, THREADS_PER_BLOCK / BLOCK_WIDTH, 1); const dim3 gridSize(util::DivUp(dst_w, blockSize.x), util::DivUp(dst_h, blockSize.y), samples); - auto src = cuda::CreateTensorWrapNHW(srcData); - // Note: resize is fundamentally a gather memory operation, with a little bit of compute // our goals are to (a) maximize throughput, and (b) minimize occupancy for the same performance - switch (interp) + // The channel dispatcher uses scalar SrcT only for a single-channel input, which has one plane. + if constexpr (NumElems > 1) { - case NVCV_INTERP_NEAREST: - resizeCrop_NN<<>>(dst, src, resize, cropPos, scale, offset); - break; + if (srcAccess->numPlanes() > 1) + { + TensorPlanarSrc src{srcData, *srcAccess}; + LaunchTensorResizeCrop(dst, src, gridSize, blockSize, stream, interp, src_w, src_h, resize, cropPos, scale, + offset, srcCast); + return; + } + } - case NVCV_INTERP_LINEAR: - resizeCrop_bilinear<<>>(dst, src, src_w, src_h, resize, cropPos, scale, offset, - srcCast); - break; - default: - break; - } //switch + auto src = cuda::CreateTensorWrapNHW(srcData); + LaunchTensorResizeCrop(dst, src, gridSize, blockSize, stream, interp, src_w, src_h, resize, cropPos, scale, offset, + srcCast); } //resize template @@ -376,26 +511,25 @@ void resizeCropConvertReformat(const nvcv::ImageBatchVarShapeDataStridedCuda &sr DstMapT dst{dstPtr, addN, addH, addW, addC, manip, dst_w, dst_h}; const int THREADS_PER_BLOCK = 256; // Performance degrades above 256 and below 16 (GMEM speed limited) - const int BLOCK_WIDTH = 8; // as in 32x4 or 32x8 or 8x32. + const int BLOCK_WIDTH = 32; // Keep each warp on one contiguous output row. const dim3 blockSize(BLOCK_WIDTH, THREADS_PER_BLOCK / BLOCK_WIDTH, 1); const dim3 gridSize(util::DivUp(dst_w, blockSize.x), util::DivUp(dst_h, blockSize.y), samples); - cuda::ImageBatchVarShapeWrap src(srcData); - - switch (interp) + // The channel dispatcher uses scalar SrcT only for a single-channel input, which has one plane. + if constexpr (NumElems > 1) { - case NVCV_INTERP_NEAREST: - resizeCrop_NN_varShape<<>>(dst, src, resizeDim, cropPos, scale, offset); - break; + if (srcFrmt.numPlanes() > 1) + { + ImageBatchVarShapePlanarSrc src(srcData); + LaunchVarShapeResizeCrop(dst, src, gridSize, blockSize, stream, interp, resizeDim, cropPos, scale, offset, + srcCast); + return; + } + } - case NVCV_INTERP_LINEAR: - resizeCrop_bilinear_varShape<<>>(dst, src, resizeDim, cropPos, scale, offset, - srcCast); - break; - default: - break; - } // switch + cuda::ImageBatchVarShapeWrap src(srcData); + LaunchVarShapeResizeCrop(dst, src, gridSize, blockSize, stream, interp, resizeDim, cropPos, scale, offset, srcCast); } } // anonymous namespace @@ -411,6 +545,7 @@ void ResizeCropConvertReformat::operator()(cudaStream_t stream, const nvcv::Tens const int2 cropPos, const NVCVChannelManip manip, float scale, float offset, bool srcCast) const { + CVCUDA_NVTX_RANGE("cvcuda::ResizeCropConvertReformat::operator()[Tensor]"); auto srcData = src.exportData(); if (!srcData) { @@ -449,10 +584,10 @@ void ResizeCropConvertReformat::operator()(cudaStream_t stream, const nvcv::Tens throw nvcv::Exception(nvcv::Status::ERROR_NOT_COMPATIBLE, "%s", msg.c_str()); } - if (channels != 3) + if (channels != 1 && channels != 3) { - std::string msg = "Only three-channel input is currently supported: Provided " + std::to_string(channels) - + " input channels"; + std::string msg + = "Only 1- or 3-channel input is supported: Provided " + std::to_string(channels) + " input channels"; throw nvcv::Exception(nvcv::Status::ERROR_NOT_COMPATIBLE, "%s", msg.c_str()); } @@ -472,10 +607,12 @@ void ResizeCropConvertReformat::operator()(cudaStream_t stream, const nvcv::Tens nvcv::TensorLayout srcLayout = srcData->layout(); nvcv::TensorLayout dstLayout = dstData->layout(); - if (!(srcLayout == NVCV_TENSOR_NHWC || srcLayout == NVCV_TENSOR_HWC)) + if (!(srcLayout == NVCV_TENSOR_NHWC || srcLayout == NVCV_TENSOR_HWC || srcLayout == NVCV_TENSOR_NCHW + || srcLayout == NVCV_TENSOR_CHW)) { const char *layout = nvcvTensorLayoutGetName(&srcLayout.m_layout); - std::string msg = "Input tensor must have 'NHWC' or 'HWC' layout: Layout provided " + std::string(layout); + std::string msg + = "Input tensor must have 'NHWC', 'NCHW', 'HWC', or 'CHW' layout: Layout provided " + std::string(layout); throw nvcv::Exception(nvcv::Status::ERROR_NOT_COMPATIBLE, "%s", msg.c_str()); } @@ -531,15 +668,23 @@ void ResizeCropConvertReformat::operator()(cudaStream_t stream, const nvcv::Tens if (srcType == cuda_op::kCV_8U) { - if (dstType == cuda_op::kCV_8U) + if (channels == 1) { - resizeCropConvertReformat(*srcData, *dstData, resizeDim, interp, cropPos, manip, scale, - offset, srcCast, stream); + if (dstType == cuda_op::kCV_8U) + resizeCropConvertReformat(*srcData, *dstData, resizeDim, interp, cropPos, manip, scale, + offset, srcCast, stream); + else if (dstType == cuda_op::kCV_32F) + resizeCropConvertReformat(*srcData, *dstData, resizeDim, interp, cropPos, manip, scale, + offset, srcCast, stream); } - else if (dstType == cuda_op::kCV_32F) + else { - resizeCropConvertReformat(*srcData, *dstData, resizeDim, interp, cropPos, manip, scale, - offset, srcCast, stream); + if (dstType == cuda_op::kCV_8U) + resizeCropConvertReformat(*srcData, *dstData, resizeDim, interp, cropPos, manip, scale, + offset, srcCast, stream); + else if (dstType == cuda_op::kCV_32F) + resizeCropConvertReformat(*srcData, *dstData, resizeDim, interp, cropPos, manip, scale, + offset, srcCast, stream); } } } @@ -549,6 +694,7 @@ void ResizeCropConvertReformat::operator()(cudaStream_t stream, const nvcv::Imag const NVCVInterpolationType interp, const int2 cropPos, const NVCVChannelManip manip, float scale, float offset, bool srcCast) const { + CVCUDA_NVTX_RANGE("cvcuda::ResizeCropConvertReformat::operator()[ImageBatchVarShape]"); auto srcData = src.exportData(stream); if (!srcData) { @@ -594,10 +740,10 @@ void ResizeCropConvertReformat::operator()(cudaStream_t stream, const nvcv::Imag throw nvcv::Exception(nvcv::Status::ERROR_NOT_COMPATIBLE, "%s", msg.c_str()); } - if (channels != 3) + if (channels != 1 && channels != 3) { - std::string msg = "Only three-channel input is currently supported: Provided " + std::to_string(channels) - + " input channels"; + std::string msg + = "Only 1- or 3-channel input is supported: Provided " + std::to_string(channels) + " input channels"; throw nvcv::Exception(nvcv::Status::ERROR_NOT_COMPATIBLE, "%s", msg.c_str()); } @@ -616,11 +762,6 @@ void ResizeCropConvertReformat::operator()(cudaStream_t stream, const nvcv::Imag nvcv::TensorLayout dstLayout = dstData->layout(); - if (srcFrmt.numPlanes() > 1) - { - throw nvcv::Exception(nvcv::Status::ERROR_NOT_COMPATIBLE, "Input must be non-planar (i.e., interleaved)."); - } - if (!(dstLayout == NVCV_TENSOR_NHWC || dstLayout == NVCV_TENSOR_HWC || dstLayout == NVCV_TENSOR_NCHW || dstLayout == NVCV_TENSOR_CHW)) { @@ -660,15 +801,23 @@ void ResizeCropConvertReformat::operator()(cudaStream_t stream, const nvcv::Imag if (srcType == cuda_op::kCV_8U) { - if (dstType == cuda_op::kCV_8U) + if (channels == 1) { - resizeCropConvertReformat(*srcData, *dstData, resizeDim, interp, cropPos, manip, scale, - offset, srcCast, stream); + if (dstType == cuda_op::kCV_8U) + resizeCropConvertReformat(*srcData, *dstData, resizeDim, interp, cropPos, manip, scale, + offset, srcCast, stream); + else if (dstType == cuda_op::kCV_32F) + resizeCropConvertReformat(*srcData, *dstData, resizeDim, interp, cropPos, manip, scale, + offset, srcCast, stream); } - else if (dstType == cuda_op::kCV_32F) + else { - resizeCropConvertReformat(*srcData, *dstData, resizeDim, interp, cropPos, manip, scale, - offset, srcCast, stream); + if (dstType == cuda_op::kCV_8U) + resizeCropConvertReformat(*srcData, *dstData, resizeDim, interp, cropPos, manip, scale, + offset, srcCast, stream); + else if (dstType == cuda_op::kCV_32F) + resizeCropConvertReformat(*srcData, *dstData, resizeDim, interp, cropPos, manip, scale, + offset, srcCast, stream); } } } diff --git a/src/cvcuda/priv/OpRotate.cpp b/src/cvcuda/priv/OpRotate.cpp index f7ec89a40..ac7c7a307 100644 --- a/src/cvcuda/priv/OpRotate.cpp +++ b/src/cvcuda/priv/OpRotate.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "OpRotate.hpp" +#include "Nvtx.hpp" #include "legacy/CvCudaLegacy.h" #include "legacy/CvCudaLegacyHelpers.hpp" @@ -27,17 +28,25 @@ namespace cvcuda::priv { namespace legacy = nvcv::legacy::cuda_op; +std::unique_ptr Rotate::CreateLegacyOp(int) +{ + legacy::DataShape maxIn; + legacy::DataShape maxOut; + return std::make_unique(maxIn, maxOut); +} + Rotate::Rotate(const int maxVarShapeBatchSize) + // Legacy operators are single-device by design. PerDeviceResource creates + // one instance per CUDA device for transparent multi-GPU support. + : m_legacyOpVarShape([maxVarShapeBatchSize](int) + { return std::make_unique(maxVarShapeBatchSize); }) { - legacy::DataShape maxIn, maxOut; - // maxIn/maxOut not used by op. - m_legacyOp = std::make_unique(maxIn, maxOut); - m_legacyOpVarShape = std::make_unique(maxVarShapeBatchSize); } void Rotate::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, const double angleDeg, const double2 shift, const NVCVInterpolationType interpolation) const { + CVCUDA_NVTX_RANGE("cvcuda::Rotate::operator()[Tensor]"); auto inData = in.exportData(); if (inData == nullptr) { @@ -52,13 +61,14 @@ void Rotate::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv: "Output must be cuda-accessible, pitch-linear tensor"); } - NVCV_CHECK_THROW(m_legacyOp->infer(*inData, *outData, angleDeg, shift, interpolation, stream)); + NVCV_CHECK_THROW(m_legacyOp.get().infer(*inData, *outData, angleDeg, shift, interpolation, stream)); } void Rotate::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, const nvcv::Tensor &angleDeg, const nvcv::Tensor &shift, const NVCVInterpolationType interpolation) const { + CVCUDA_NVTX_RANGE("cvcuda::Rotate::operator()[ImageBatchVarShape]"); auto inData = in.exportData(stream); if (inData == nullptr) { @@ -83,7 +93,8 @@ void Rotate::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "shift must be a tensor"); } - NVCV_CHECK_THROW(m_legacyOpVarShape->infer(*inData, *outData, *angleDegData, *shiftData, interpolation, stream)); + NVCV_CHECK_THROW( + m_legacyOpVarShape.get().infer(*inData, *outData, *angleDegData, *shiftData, interpolation, stream)); } } // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpRotate.hpp b/src/cvcuda/priv/OpRotate.hpp index a58ad5f5b..1f5091010 100644 --- a/src/cvcuda/priv/OpRotate.hpp +++ b/src/cvcuda/priv/OpRotate.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,6 +25,7 @@ #define CVCUDA_PRIV_ROTATE_HPP #include "IOperator.hpp" +#include "PerDeviceResource.hpp" #include "legacy/CvCudaLegacy.h" #include @@ -47,8 +48,10 @@ class Rotate final : public IOperator const NVCVInterpolationType interpolation) const; private: - std::unique_ptr m_legacyOp; - std::unique_ptr m_legacyOpVarShape; + static std::unique_ptr CreateLegacyOp(int deviceId); + + mutable PerDeviceResource m_legacyOp{CreateLegacyOp}; + mutable PerDeviceResource m_legacyOpVarShape; }; } // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpSIFT.cu b/src/cvcuda/priv/OpSIFT.cu index 4ebffdc12..d18d7ed33 100644 --- a/src/cvcuda/priv/OpSIFT.cu +++ b/src/cvcuda/priv/OpSIFT.cu @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,6 +15,7 @@ * limitations under the License. */ +#include "Nvtx.hpp" #include "OpSIFT.hpp" #include @@ -53,6 +54,7 @@ constexpr NVCVInterpolationType kInterpUp{NVCV_INTERP_LINEAR}; // default constexpr float kMinSigma = 0.01f; // minimum sigma to be used as base sigma constexpr float kPrevSigma = 0.5f; // previous sigma before base image constexpr int kMaxKernelSize = 59; // maximum Gaussian kernel size +constexpr int kDescriptorOctaveBatch = 2; // octaves handled by one descriptor launch constexpr int2 kBorderSize = int2{5, 5}; // ignore keypoints close to the border constexpr int kMaxInterpolationSteps = 5; // max. steps of keypoint interpolation before failure @@ -107,7 +109,7 @@ inline __host__ nvcv::Tensor GetViewFrom(const nvcv::TensorDataStridedCuda &par viewData.shape[r] = viewShape[r]; } - return nvcv::TensorWrapData(viewData); + return nvcv::TensorWrapData(nvcv::TensorData{viewData}); } // Function to compute number of octaves for an WxH image with W=width and H=height @@ -177,20 +179,22 @@ __global__ void DoComputePyramids(BorderWrapLNHW prevGauss, TensorWrapLNH TensorWrapLNHW currDoG, int3 currShape, int currLayer, int currKernelSize, cuda::math::Vector gaussSepKernel) { - constexpr int SW = BW + kMaxKernelSize; // block width with kernel maximum support (halo) for Gaussian filtering - constexpr int SH = BH + kMaxKernelSize; // block height with kernel maximum support (halo) for Gaussian filtering - - // Based on kMaxKernelSize = 59 and kDataTile = 32x32x1 + // Based on kMaxKernelSize = 59 and kDataTile = 32x32x1. The launch reserves only the space required by the + // current Gaussian kernel, up to this maximum. // Registers used: 40 // SMEM usage (max. 48KB): 44772 B = ((32 + 59) * (32 + 59)) * 4 + (32 * (32 + 59)) * 4 // CMEM usage (max. 4KB): 688 B = 59 * 4 + 4 + 4 + 4 + 4 * 3 + (4 * 3 + 8) * 2 + (4 * 3 + 4 * 2 + 8) + (360) - __shared__ float gaussInData[SH * SW]; // plain 1D Gaussian input data in shared memory (SMEM) - __shared__ float gaussOutData[SH * BW]; // plain 1D Gaussian output (intermediary) data in SMEM + extern __shared__ float sharedData[]; - // Using TensorWrap with compile-time strides for easy multi-dimensional access of Gaussian data in SMEM - cuda::TensorWrap32 gaussIn(&gaussInData[0]); - cuda::TensorWrap32 gaussOut(&gaussOutData[0]); + const int gaussInWidth = BW + currKernelSize; + float *gaussInData = sharedData; + float *gaussOutData = gaussInData + (BH + currKernelSize) * gaussInWidth; + + // The input row stride follows the current kernel support instead of the maximum supported one. + cuda::TensorWrap32 gaussIn(gaussInData, + static_cast(gaussInWidth * sizeof(float))); + cuda::TensorWrap32 gaussOut(gaussOutData); int half = currKernelSize / 2; // i.e. the halo or support data outside block to compute Gaussian filter @@ -320,8 +324,9 @@ __global__ void DoComputeDescriptors(TensorWrapForDescriptor featDes cuda::Tensor2DWrap featCoords, cuda::Tensor2DWrap featMetadata, cuda::Tensor1DWrap numFeatures, - TensorWrapLNHW currGauss, int3 currShape, int featOctave, - float unscaleOctave) + TensorWrapLNHW firstGauss, int3 firstShape, int firstOctave, + float firstScale, TensorWrapLNHW secondGauss, int3 secondShape, + int secondOctave, float secondScale, bool hasSecondOctave) { constexpr int BW = (kDescMaxRadius + 1) * 2 + 1; // block width with maximum support radius for descriptor constexpr int BH = (kDescMaxRadius + 1) * 2 + 1; // block height with maximum support radius for descriptor @@ -346,10 +351,21 @@ __global__ void DoComputeDescriptors(TensorWrapForDescriptor featDes return; // each kernel invocation handles only valid features } - float *pFeatCoords = reinterpret_cast(featCoords.ptr(sampleIdx, featIdx)); - if (pFeatCoords[2] != featOctave) + float *pFeatCoords = reinterpret_cast(featCoords.ptr(sampleIdx, featIdx)); + TensorWrapLNHW currGauss = firstGauss; + int3 currShape = firstShape; + float unscaleOctave = firstScale; + + if (pFeatCoords[2] != firstOctave) { - return; // each kernel invocation handles only features of the current octave + if (!hasSecondOctave || pFeatCoords[2] != secondOctave) + { + return; + } + + currGauss = secondGauss; + currShape = secondShape; + unscaleOctave = secondScale; } // The coordinate (x, y) and layer of the feature to compute descriptor from @@ -575,9 +591,10 @@ __global__ void DoFindExtrema(cuda::Tensor2DWrap featCoords, // CMEM usage (max. 4KB): 488 B = (4 * 1 + 8) * 2 + 4 + 8 + (4 * 3 + 8) * 2 + 4 * 3 + 4 * 7 + (372) // ! 218072 bytes gmem ! 208 Bytes stack frame - float cv; // central value - cuda::math::Vector dD, sol; // derivative distances and solver solution - cuda::math::Matrix H; // Hessian matrix + float cv; // central value + cuda::math::Vector dD; // derivative distances + cuda::math::Vector sol; // solver solution + cuda::math::Matrix H; // Hessian matrix float hist[kHistogramBins]; // histogram for angle computation @@ -887,6 +904,11 @@ void SIFT::FindExtrema(const nvcv::TensorDataStridedCuda &featCoordsData, dim3 compBlocks1; dim3 compBlocks2(maxCapacity, 1, currShape.z); + TensorWrapLNHW firstDescriptorGauss; + int3 firstDescriptorShape; + int firstDescriptorOctave = 0; + float firstDescriptorScale = 1.f; + cuda::Tensor2DWrap featCoordsWrap(featCoordsData.basePtr(), (int)featCoordsData.stride(0)); cuda::Tensor2DWrap featMetadataWrap(featMetadataData.basePtr(), (int)featMetadataData.stride(0)); cuda::Tensor1DWrap numFeaturesWrap(numFeaturesData.basePtr()); @@ -917,17 +939,30 @@ void SIFT::FindExtrema(const nvcv::TensorDataStridedCuda &featCoordsData, float scaleOctave = ::pow(2, featOctave); // scale feature coordinate or size back to base image float unscaleOctave = 1.f / scaleOctave; // un-scale feature coordinate or size back to current octave + int descriptorOctaveIndex = octave % kDescriptorOctaveBatch; + if (descriptorOctaveIndex == 0) + { + firstDescriptorGauss = currGaussWrap; + firstDescriptorShape = currShape; + firstDescriptorOctave = featOctave; + firstDescriptorScale = unscaleOctave; + } + // First run the DoFindExtrema kernel to find extrema points (the features) and compute their metadata DoFindExtrema<<>>( featCoordsWrap, featMetadataWrap, maxCapacity, numFeaturesWrap, currGaussWrap, currDoGWrap, currShape, featOctave, scaleOctave, numOctaveLayers, intThreshold, contrastThreshold, edgeThreshold, initSigma); - // Second run the DoComputeDescriptors kernel to compute the descriptor of each feature found + if (descriptorOctaveIndex == kDescriptorOctaveBatch - 1 || octave == numOctaves - 1) + { + bool hasSecondOctave = descriptorOctaveIndex == kDescriptorOctaveBatch - 1; - DoComputeDescriptors<<>>(featDescriptorsWrap, featCoordsWrap, - featMetadataWrap, numFeaturesWrap, currGaussWrap, - currShape, featOctave, unscaleOctave); + DoComputeDescriptors<<>>( + featDescriptorsWrap, featCoordsWrap, featMetadataWrap, numFeaturesWrap, firstDescriptorGauss, + firstDescriptorShape, firstDescriptorOctave, firstDescriptorScale, currGaussWrap, currShape, featOctave, + unscaleOctave, hasSecondOctave); + } currShape.x /= 2; currShape.y /= 2; @@ -1028,11 +1063,12 @@ void SIFT::ComputePyramids(const nvcv::TensorDataStridedCuda &inData, int3 currS } // Compute the separable Gaussian filter kernel for the Gaussian pyramid - int ksize = ComputeGaussianKernelSize(currSigma); - int half = ksize / 2; - float ss2 = currSigma * currSigma * 2; - float sp2 = currSigma * cuda::sqrt(M_PI * 2); - float sum = 0.f; + int ksize = ComputeGaussianKernelSize(currSigma); + int half = ksize / 2; + int smemSize = ((BW + ksize) * (BH + ksize) + BW * (BH + ksize)) * sizeof(float); + float ss2 = currSigma * currSigma * 2; + float sp2 = currSigma * cuda::sqrt(M_PI * 2); + float sum = 0.f; for (int kx = -half; kx <= half; ++kx) { @@ -1053,13 +1089,13 @@ void SIFT::ComputePyramids(const nvcv::TensorDataStridedCuda &inData, int3 currS if (octave == 0 && layer == 0) { // Only for the first octave and first layer the base level previous Gaussian data is used - DoComputePyramids<<>>( + DoComputePyramids<<>>( prevGaussBW, currGaussTW, currDoGTW, currShape, layer, ksize, gaussSepKernel); } else { // For every other octave and layer the current Gaussian data (border-aware) is used - DoComputePyramids<<>>( + DoComputePyramids<<>>( currGaussBW, currGaussTW, currDoGTW, currShape, layer, ksize, gaussSepKernel); } } @@ -1186,11 +1222,14 @@ void SIFT::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::T const nvcv::Tensor &numFeatures, int numOctaveLayers, float contrastThreshold, float edgeThreshold, float initSigma, NVCVSIFTFlagType flags) const { + CVCUDA_NVTX_RANGE("cvcuda::SIFT::operator()[Tensor]"); // Check each tensor layout, strides, shape and data type if it is conforming to what is expected - if (!(in.layout() == nvcv::TENSOR_HWC || in.layout() == nvcv::TENSOR_NHWC)) + if (!(in.layout() == nvcv::TENSOR_HWC || in.layout() == nvcv::TENSOR_NHWC || in.layout() == nvcv::TENSOR_CHW + || in.layout() == nvcv::TENSOR_NCHW)) { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input tensor layout must be HWC or NHWC"); + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input tensor layout must be HWC, NHWC, CHW or NCHW"); } auto inData = in.exportData(); diff --git a/src/cvcuda/priv/OpSIFT.hpp b/src/cvcuda/priv/OpSIFT.hpp index fef42ef34..75ea09e66 100644 --- a/src/cvcuda/priv/OpSIFT.hpp +++ b/src/cvcuda/priv/OpSIFT.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -66,11 +66,14 @@ class SIFT final : public IOperator cudaStream_t stream) const; int3 m_maxShape; - int m_maxOctaves, m_maxOctaveLayers; + int m_maxOctaves; + int m_maxOctaveLayers; // Maximum allowed pyramids and run (submit) time pyramids - PyramidType m_maxPyramidGaussian, m_maxPyramidDoG; - mutable PyramidType m_runPyramidGaussian, m_runPyramidDoG; // mutable as it changes during run-time + PyramidType m_maxPyramidGaussian; + PyramidType m_maxPyramidDoG; + mutable PyramidType m_runPyramidGaussian; // mutable as it changes during run-time + mutable PyramidType m_runPyramidDoG; }; } // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpSolarize.cu b/src/cvcuda/priv/OpSolarize.cu new file mode 100644 index 000000000..85f419444 --- /dev/null +++ b/src/cvcuda/priv/OpSolarize.cu @@ -0,0 +1,764 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "Nvtx.hpp" +#include "OpSolarize.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace cuda = nvcv::cuda; +namespace util = nvcv::util; + +namespace { + +// Photometric-negative bound per base type: dtype max for unsigned integers, 1.0 for float. +template +inline __host__ __device__ BT InvertBound() +{ + if constexpr (std::is_floating_point_v) + { + return BT(1); + } + else + { + return cuda::TypeTraits::max; + } +} + +// Threshold comparison hoisted out of the FP64 pipe. The gold semantics are +// `static_cast(v) >= threshold`; per element that runs on the (1/64-rate on consumer SM) +// double pipe and makes Solarize compute-bound (NCU: ~87% SM, ~26% DRAM, decoupled-math stalls). +// For the discrete supported value types this is *exactly* equivalent to comparing v against a +// precomputed value-domain boundary in the integer / float32 domain, so the result stays bit-identical +// to the double-domain gold reference while the per-element FP64 disappears: +// - integer v: (double)v >= threshold <=> (long long)v >= ceil(threshold) +// - float32 v: (double)v >= threshold <=> v >= vmin, vmin = smallest float >= threshold +// The boundary is computed once per thread (one FP64 op, amortized) instead of once per element. +template +struct SolarizeThreshold +{ + long long vmin_i = 0; + float vmin_f = 0.0f; + + inline __device__ explicit SolarizeThreshold(double threshold) + { + if constexpr (std::is_floating_point_v) + { + float f = static_cast(threshold); + if (static_cast(f) < threshold) // round up to the smallest float >= threshold + { + f = nextafterf(f, 3.402823466e38f); + } + vmin_f = f; + } + else if (isnan(threshold)) + { + // (v >= NaN) is always false; use the max sentinel so no integer value inverts, + // and avoid the undefined behavior of casting a NaN to long long. + vmin_i = 9223372036854775807LL; + } + else + { + const double c = ceil(threshold); + vmin_i = c >= 9.2233720368547758e18 + ? 9223372036854775807LL + : (c <= -9.2233720368547758e18 ? (-9223372036854775807LL - 1) : static_cast(c)); + } + } + + inline __device__ bool ge(BT v) const + { + if constexpr (std::is_floating_point_v) + { + return v >= vmin_f; + } + else + { + return static_cast(v) >= vmin_i; + } + } +}; + +// out = (in >= threshold) ? (bound - in) : in, per channel component (threshold hoisted, see above). +template +inline __device__ BT SolarizeApply(BT v, BT bound, const SolarizeThreshold &th) +{ + return th.ge(v) ? static_cast(bound - v) : v; +} + +// Scalar per-pixel path (multi-channel interleaved + planar fallback). For multi-channel INTEGER +// pixels the FP64 threshold compares dominate (uchar3/uchar4 NHWC are ~36% BWUtil, FP64-bound), so +// hoist the boundary once per pixel (one ceil amortized over 3-4 integer compares = net win). For +// 1-channel (hoist would add a ceil with nothing to amortize -> regression) and float (already +// bandwidth-bound at the ridge) keep the original double compare. Bit-identical to the gold either way. +template +inline __device__ T SolarizeElem(T pixel, double threshold) +{ + using BT = cuda::BaseType; + static constexpr int numChannels = cuda::NumElements; + const BT bound = InvertBound(); + + T out{}; + if constexpr (numChannels > 1 && !std::is_floating_point_v) + { + const SolarizeThreshold th(threshold); +#pragma unroll + for (int c = 0; c < numChannels; ++c) + { + cuda::GetElement(out, c) = SolarizeApply(cuda::GetElement(pixel, c), bound, th); + } + } + else + { +#pragma unroll + for (int c = 0; c < numChannels; ++c) + { + const BT v = cuda::GetElement(pixel, c); + cuda::GetElement(out, c) = (static_cast(v) >= threshold) ? static_cast(bound - v) : v; + } + } + return out; +} + +template +inline __device__ std::conditional_t GetCoordForLayout(int3 nhwCoord, int p) +{ + if constexpr (!IsPlanar) + { + return nhwCoord; + } + else + { + return {nhwCoord.x, nhwCoord.y, p, nhwCoord.z}; + } +} + +template +inline __device__ void DoSolarize(SrcWrapper src, DstWrapper dst, const int2 size, const int p, double threshold) +{ + using SrcT = std::remove_const_t; + using DstT = typename DstWrapper::ValueType; + static constexpr int numChannels = cuda::NumElements; + static_assert(numChannels == cuda::NumElements); + static_assert(!IsPlanar || numChannels == 1); + + int3 nhwCoord = cuda::StaticCast(blockIdx * blockDim + threadIdx); + if (nhwCoord.x >= size.x || nhwCoord.y >= size.y) + { + return; + } + auto coord = GetCoordForLayout(nhwCoord, p); + dst[coord] = SolarizeElem(src[coord], threshold); +} + +// Solarize kernel ------------------------------------------------------------------------ + +// Tensor variant +template +__global__ void Solarize(SrcWrapper src, DstWrapper dst, int2 size, int numPlanes, double threshold) +{ + assert(isPlanar || numPlanes == 1); + if constexpr (!isPlanar) + { + DoSolarize(src, dst, size, 0, threshold); + } + else + { + for (int p = 0; p < numPlanes; p++) + { + DoSolarize(src, dst, size, p, threshold); + } + } +} + +// VarShape variant +template +__global__ void Solarize(SrcWrapper src, DstWrapper dst, int numPlanes, double threshold) +{ + assert(isPlanar || numPlanes == 1); + int z = blockIdx.z; + int2 size{dst.width(z), dst.height(z)}; + + if constexpr (!isPlanar) + { + DoSolarize(src, dst, size, 0, threshold); + } + else + { + for (int p = 0; p < numPlanes; p++) + { + DoSolarize(src, dst, size, p, threshold); + } + } +} + +// Vectorized planar / 1-channel kernels -------------------------------------------------- +// +// The scalar kernels above move one element/thread/plane, leaving the small-dtype planar and +// 1-channel-interleaved paths memory-latency bound (long-scoreboard stalls, low BWUtil). These map +// each (sample, plane) to grid.z and have each thread issue NGROUP wide vector loads (uchar4 / +// ushort4 / float4) before compute, raising memory-level parallelism. Per element bit-identical to +// SolarizeElem (same (in>=threshold)?(bound-in):in). Modeled on legacy/normalize_planar.cuh; caller +// guards sizeof(Vec4)-aligned base+strides with a scalar fallback; per-thread tail handles width%4. +template +struct SolarizeVec4Type; + +template +struct SolarizeVec4Type +{ + using type = uchar4; +}; + +template +struct SolarizeVec4Type +{ + using type = ushort4; +}; + +template +struct SolarizeVec4Type +{ + using type = float4; +}; + +template +__global__ void SolarizePlanarVec4Kernel(cuda::Tensor4DWrap src, cuda::Tensor4DWrap dst, + int4 inout_size, double threshold) +{ + const int g0 = blockIdx.x * blockDim.x * NGROUP + threadIdx.x; + const int src_y = blockIdx.y * blockDim.y + threadIdx.y; + const int nc = blockIdx.z; + const int batch = nc / inout_size.y; + const int channel = nc % inout_size.y; + const int width = inout_size.w; + + if (g0 * 4 >= width || src_y >= inout_size.z) + { + return; + } + + using Vec4 = typename SolarizeVec4Type::type; + const BT bound = InvertBound(); + const SolarizeThreshold th(threshold); + + int cx[NGROUP]; + bool full[NGROUP]; + Vec4 in4[NGROUP]; +#pragma unroll + for (int i = 0; i < NGROUP; ++i) + { + cx[i] = (g0 + i * blockDim.x) * 4; + full[i] = cx[i] + 4 <= width; + if (full[i]) + { + in4[i] = *reinterpret_cast(src.ptr(batch, channel, src_y, cx[i])); + } + } + +#pragma unroll + for (int i = 0; i < NGROUP; ++i) + { + if (full[i]) + { + Vec4 out4; + out4.x = SolarizeApply(in4[i].x, bound, th); + out4.y = SolarizeApply(in4[i].y, bound, th); + out4.z = SolarizeApply(in4[i].z, bound, th); + out4.w = SolarizeApply(in4[i].w, bound, th); + *reinterpret_cast(dst.ptr(batch, channel, src_y, cx[i])) = out4; + } + else if (cx[i] < width) + { + for (int x = cx[i]; x < width; ++x) + { + *dst.ptr(batch, channel, src_y, x) = SolarizeApply(*src.ptr(batch, channel, src_y, x), bound, th); + } + } + } +} + +template +__global__ void SolarizePlanarVarShapeVec4Kernel(cuda::ImageBatchVarShapeWrap src, + cuda::ImageBatchVarShapeWrap dst, int num_channels, + double threshold) +{ + const int g0 = blockIdx.x * blockDim.x * NGROUP + threadIdx.x; + const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int nc = blockIdx.z; + const int batch = nc / num_channels; + const int channel = nc % num_channels; + const int width = dst.width(batch, channel); + + if (g0 * 4 >= width || dst_y >= dst.height(batch, channel)) + { + return; + } + + using Vec4 = uchar4; // 1-byte planes only (caller guards sizeof(BT) == 1) + const BT bound = InvertBound(); + const SolarizeThreshold th(threshold); + + int cx[NGROUP]; + bool full[NGROUP]; + Vec4 in4[NGROUP]; +#pragma unroll + for (int i = 0; i < NGROUP; ++i) + { + cx[i] = (g0 + i * blockDim.x) * 4; + full[i] = cx[i] + 4 <= width; + if (full[i]) + { + in4[i] = *reinterpret_cast(src.ptr(batch, channel, dst_y, cx[i])); + } + } + +#pragma unroll + for (int i = 0; i < NGROUP; ++i) + { + if (full[i]) + { + Vec4 out4; + out4.x = SolarizeApply(in4[i].x, bound, th); + out4.y = SolarizeApply(in4[i].y, bound, th); + out4.z = SolarizeApply(in4[i].z, bound, th); + out4.w = SolarizeApply(in4[i].w, bound, th); + *reinterpret_cast(dst.ptr(batch, channel, dst_y, cx[i])) = out4; + } + else if (cx[i] < width) + { + for (int x = cx[i]; x < width; ++x) + { + *dst.ptr(batch, channel, dst_y, x) = SolarizeApply(*src.ptr(batch, channel, dst_y, x), bound, th); + } + } + } +} + +// Run Solarize kernel -------------------------------------------------------------------- + +template +inline void RunSolarize(cudaStream_t stream, const SrcData &srcData, const DstData &dstData, double threshold) +{ + dim3 block(32, 4, 1); + if constexpr (std::is_same_v) + { + auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(srcData); + auto dstAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(dstData); + int2 size = cuda::StaticCast(long2{srcAccess->numCols(), srcAccess->numRows()}); + + // Each sample maps to one grid.z block (planar channels are looped inside the kernel, so + // grid.z is the sample count, not N*C); CUDA caps grid.z at 65535. + if (srcAccess->numSamples() > 65535) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Batch size exceeds the CUDA grid.z limit of 65535"); + } + dim3 grid(util::DivUp(size.x, block.x), util::DivUp(size.y, block.y), srcAccess->numSamples()); + + int64_t inMaxStride = srcAccess->sampleStride() * srcAccess->numSamples(); + int64_t outMaxStride = dstAccess->sampleStride() * dstAccess->numSamples(); + if (std::max(inMaxStride, outMaxStride) > cuda::TypeTraits::max) + { + throw nvcv::Exception(nvcv::Status::ERROR_OVERFLOW, "Input or output size exceeds %d. Tensor is too large.", + cuda::TypeTraits::max); + } + using StrideType = int32_t; + + using BT = cuda::BaseType; + if constexpr (!isPlanar) + { + bool launchedVec = false; + // 1-channel interleaved (e.g. U16) is byte-identical to a single plane and latency-bound; + // route it through the vectorized planar kernel (C == 1). Multi-channel interleaved + // (uchar3/uchar4/float3/float4) already moves 3-4 B/thread at the ridge -> keep scalar. + if constexpr (cuda::NumElements == 1) + { + using Vec4 = typename SolarizeVec4Type::type; + constexpr int NGROUP = 4; + const int64_t sStride = srcAccess->sampleStride(), rStride = srcAccess->rowStride(); + const int64_t dsStride = dstAccess->sampleStride(), drStride = dstAccess->rowStride(); + const bool aligned = reinterpret_cast(srcData.basePtr()) % sizeof(Vec4) == 0 + && reinterpret_cast(dstData.basePtr()) % sizeof(Vec4) == 0 + && sStride % sizeof(Vec4) == 0 && rStride % sizeof(Vec4) == 0 + && dsStride % sizeof(Vec4) == 0 && drStride % sizeof(Vec4) == 0; + if (aligned) + { + auto srcV = cuda::Tensor4DWrap(srcData.basePtr(), static_cast(sStride), + static_cast(sStride), + static_cast(rStride)); + auto dstV + = cuda::Tensor4DWrap(dstData.basePtr(), static_cast(dsStride), + static_cast(dsStride), static_cast(drStride)); + dim3 vgrid(util::DivUp(util::DivUp(size.x, 4), static_cast(block.x) * NGROUP), + util::DivUp(size.y, block.y), srcAccess->numSamples()); + SolarizePlanarVec4Kernel<<>>( + srcV, dstV, int4{static_cast(srcAccess->numSamples()), 1, size.y, size.x}, threshold); + launchedVec = true; + } + } + if (!launchedVec) + { + auto src = cuda::CreateTensorWrapNHW(srcData); + auto dst = cuda::CreateTensorWrapNHW(dstData); + Solarize<<>>(src, dst, size, 1, threshold); + } + } + else + { + const int numPlanes = srcAccess->numPlanes(); + const int numSamples = static_cast(srcAccess->numSamples()); + bool launchedVec = false; + if constexpr (sizeof(BT) == 1 || sizeof(BT) == 4) + { + using Vec4 = typename SolarizeVec4Type::type; + constexpr int NGROUP = sizeof(BT) == 1 ? 4 : 2; + const int64_t planes = static_cast(numSamples) * numPlanes; + const int64_t sStride = srcAccess->sampleStride(), pStride = srcAccess->planeStride(), + rStride = srcAccess->rowStride(); + const int64_t dsStride = dstAccess->sampleStride(), dpStride = dstAccess->planeStride(), + drStride = dstAccess->rowStride(); + const bool aligned + = planes <= 65535 && reinterpret_cast(srcData.basePtr()) % sizeof(Vec4) == 0 + && reinterpret_cast(dstData.basePtr()) % sizeof(Vec4) == 0 && sStride % sizeof(Vec4) == 0 + && pStride % sizeof(Vec4) == 0 && rStride % sizeof(Vec4) == 0 && dsStride % sizeof(Vec4) == 0 + && dpStride % sizeof(Vec4) == 0 && drStride % sizeof(Vec4) == 0; + if (aligned) + { + auto srcV = cuda::Tensor4DWrap(srcData.basePtr(), static_cast(sStride), + static_cast(pStride), + static_cast(rStride)); + auto dstV + = cuda::Tensor4DWrap(dstData.basePtr(), static_cast(dsStride), + static_cast(dpStride), static_cast(drStride)); + dim3 vgrid(util::DivUp(util::DivUp(size.x, 4), static_cast(block.x) * NGROUP), + util::DivUp(size.y, block.y), static_cast(planes)); + SolarizePlanarVec4Kernel<<>>( + srcV, dstV, int4{numSamples, numPlanes, size.y, size.x}, threshold); + launchedVec = true; + } + } + if (!launchedVec) + { + auto src = cuda::Tensor4DWrap( + srcData.basePtr(), static_cast(srcAccess->sampleStride()), + static_cast(srcAccess->planeStride()), static_cast(srcAccess->rowStride())); + auto dst = cuda::Tensor4DWrap( + dstData.basePtr(), static_cast(dstAccess->sampleStride()), + static_cast(dstAccess->planeStride()), static_cast(dstAccess->rowStride())); + Solarize<<>>(src, dst, size, numPlanes, threshold); + } + } + NVCV_CHECK_THROW(cudaGetLastError()); + } + else + { + static_assert(std::is_same_v); + // One grid.z block per image; CUDA caps grid.z at 65535. + if (dstData.numImages() > 65535) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Batch size exceeds the CUDA grid.z limit of 65535"); + } + int3 dstMaxSize{dstData.maxSize().w, dstData.maxSize().h, dstData.numImages()}; + dim3 grid(util::DivUp(dstMaxSize.x, block.x), util::DivUp(dstMaxSize.y, block.y), dstMaxSize.z); + + const int numPlanes = dstData.uniqueFormat().numPlanes(); + + // Vectorized planar var-shape path for 1-byte planes (uchar4 wide load; NVCV row pitch is + // >= 4-byte aligned). float/uint16 var-shape stay scalar (already at ridge / no safe wide align). + using BT = cuda::BaseType; + if constexpr (isPlanar && sizeof(BT) == 1) + { + constexpr int NGROUP = 4; + const int64_t planes = static_cast(dstData.numImages()) * numPlanes; + if (planes <= 65535) + { + cuda::ImageBatchVarShapeWrap srcV(srcData); + cuda::ImageBatchVarShapeWrap dstV(dstData); + dim3 vgrid(util::DivUp(util::DivUp(dstMaxSize.x, 4), static_cast(block.x) * NGROUP), + util::DivUp(dstMaxSize.y, block.y), static_cast(planes)); + SolarizePlanarVarShapeVec4Kernel + <<>>(srcV, dstV, numPlanes, threshold); + NVCV_CHECK_THROW(cudaGetLastError()); + return; + } + } + + cuda::ImageBatchVarShapeWrap src(srcData); + cuda::ImageBatchVarShapeWrap dst(dstData); + Solarize<<>>(src, dst, numPlanes, threshold); + NVCV_CHECK_THROW(cudaGetLastError()); + } +} + +// Dispatch over base data type (u8 / u16 / f32) and channel count (1 / 3 / 4) ------------- + +template +inline void RunTypeSwitch(nvcv::DataType dType, const Cb &cb) +{ + using uchar = unsigned char; + using ushort = unsigned short; + +#define NVCV_SOLARIZE_RUN_TYPED(DYN_BASE_TYPE, STATIC_BASE_TYPE) \ + ((dType == nvcv::TYPE_4##DYN_BASE_TYPE) || (dType == nvcv::TYPE_3##DYN_BASE_TYPE) \ + || (dType == nvcv::TYPE_2##DYN_BASE_TYPE) || (dType == nvcv::TYPE_##DYN_BASE_TYPE)) cb(STATIC_BASE_TYPE{}); + + // clang-format off + if NVCV_SOLARIZE_RUN_TYPED(U8, uchar) + else if NVCV_SOLARIZE_RUN_TYPED(U16, ushort) + else if NVCV_SOLARIZE_RUN_TYPED(F32, float) + else + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Invalid data type: Solarize supports 8-bit unsigned, 16-bit unsigned and 32-bit float"); + } + // clang-format on + +#undef NVCV_SOLARIZE_RUN_TYPED +} + +template +inline void RunChannelSwitch(int numChannels, int numPlanes, nvcv::DataType dType, const Cb &cb) +{ + RunTypeSwitch(dType, + [&numChannels, &numPlanes, &cb](auto dummyVal) + { + using ValBase = decltype(dummyVal); + // clang-format off + if (numChannels == 1) + { + using Val = cuda::MakeType; + if (numPlanes == 1) + { + cb(Val{}, std::integral_constant{}); + } + else + { + cb(Val{}, std::integral_constant{}); + } + } + else if (numChannels == 3) + { + cb(cuda::MakeType{}, std::integral_constant{}); + } + else if (numChannels == 4) + { + cb(cuda::MakeType{}, std::integral_constant{}); + } + else + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Invalid number of channels: Solarize supports 1, 3 or 4 channels"); + } + // clang-format on + }); +} + +// Validation ------------------------------------------------------------------------------ + +inline void ValidateSrcDstTensors(int &numInterleavedChannels, int &numPlanes, nvcv::DataType &dtype, + const nvcv::Optional &srcData, + const nvcv::Optional &dstData) +{ + if (!srcData) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input must be cuda-accessible, pitch-linear tensor"); + } + if (!dstData) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Output must be cuda-accessible, pitch-linear tensor"); + } + if (srcData->layout() != dstData->layout()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input and output must have the same layout"); + } + if (!(srcData->layout() == nvcv::TENSOR_HWC || srcData->layout() == nvcv::TENSOR_NHWC + || srcData->layout() == nvcv::TENSOR_CHW || srcData->layout() == nvcv::TENSOR_NCHW)) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input must have (N)HWC or (N)CHW layout"); + } + if (srcData->dtype() != dstData->dtype()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input and output must have the same data type"); + } + + auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcData); + auto dstAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*dstData); + NVCV_ASSERT(srcAccess && dstAccess); + + if (srcAccess->numSamples() != dstAccess->numSamples()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of samples"); + } + + int numChannels = srcAccess->numChannels(); + if (numChannels != dstAccess->numChannels()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of channels"); + } + + numPlanes = srcAccess->numPlanes(); + if (numPlanes != dstAccess->numPlanes()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of planes"); + } + if (numPlanes > 1 && numChannels == 2) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "2-channel planar images are not supported"); + } + + if (srcAccess->numCols() != dstAccess->numCols() || srcAccess->numRows() != dstAccess->numRows()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input and output must have matching width and height"); + } + + dtype = srcData->dtype(); + numInterleavedChannels = srcAccess->infoLayout().isChannelLast() ? numChannels : 1; +} + +inline auto ValidateSrcDstVarBatch(int &numInterleavedChannels, int &numPlanes, nvcv::DataType &dtype, + cudaStream_t stream, const nvcv::ImageBatchVarShape &src, + const nvcv::ImageBatchVarShape &dst) +{ + using maybeVarShape = nvcv::Optional; + std::tuple srcDstData{ + src.exportData(stream), + dst.exportData(stream)}; + auto &[srcData, dstData] = srcDstData; + + if (!srcData) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input must be cuda-accessible, varshape pitch-linear image batch"); + } + if (!dstData) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Output must be cuda-accessible, varshape pitch-linear image batch"); + } + + int numSamples = srcData->numImages(); + if (numSamples != dstData->numImages()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Incompatible input/output number of samples"); + } + + const auto &srcFormat = srcData->uniqueFormat(); + const auto &dstFormat = dstData->uniqueFormat(); + if (!srcFormat || !dstFormat) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "All images in a batch must have the same format"); + } + if (srcFormat != dstFormat) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Input and output must have the same format"); + } + + int numChannels = srcFormat.numChannels(); + numPlanes = srcFormat.numPlanes(); + if (numPlanes > 1 && numChannels == 2) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "2-channel planar images are not supported"); + } + + dtype = srcFormat.planeDataType(0); + for (int i = 1; i < numPlanes; ++i) + { + if (dtype != srcFormat.planeDataType(i)) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "All planes in the input image must have the same data type"); + } + } + + numInterleavedChannels = dtype.numChannels(); + + for (int i = 0; i < numSamples; i++) + { + if (src[i].size() != dst[i].size()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input and output must have matching width and height"); + } + } + + return srcDstData; +} + +} // anonymous namespace + +namespace cvcuda::priv { + +Solarize::Solarize() {} + +// Tensor input variant +void Solarize::operator()(cudaStream_t stream, const nvcv::Tensor &src, const nvcv::Tensor &dst, double threshold) const +{ + CVCUDA_NVTX_RANGE("cvcuda::Solarize::operator()[Tensor]"); + int numInterleavedChannels; + int numPlanes; + nvcv::DataType dtype; + auto srcData = src.exportData(); + auto dstData = dst.exportData(); + ValidateSrcDstTensors(numInterleavedChannels, numPlanes, dtype, srcData, dstData); + + RunChannelSwitch(numInterleavedChannels, numPlanes, dtype, + [&stream, &srcData, &dstData, threshold](auto dummyVal, auto isPlanar) + { + using ValueT = decltype(dummyVal); + using IsPlanar = decltype(isPlanar); + RunSolarize(stream, *srcData, *dstData, threshold); + }); +} + +// VarShape input variant +void Solarize::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &src, const nvcv::ImageBatchVarShape &dst, + double threshold) const +{ + CVCUDA_NVTX_RANGE("cvcuda::Solarize::operator()[ImageBatchVarShape]"); + int numInterleavedChannels; + int numPlanes; + nvcv::DataType dtype; + auto srcDstData = ValidateSrcDstVarBatch(numInterleavedChannels, numPlanes, dtype, stream, src, dst); + + RunChannelSwitch(numInterleavedChannels, numPlanes, dtype, + [&stream, &srcDstData, threshold](auto dummyVal, auto isPlanar) + { + using ValueT = decltype(dummyVal); + using IsPlanar = decltype(isPlanar); + auto &[srcData, dstData] = srcDstData; + RunSolarize(stream, *srcData, *dstData, threshold); + }); +} + +} // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpSolarize.hpp b/src/cvcuda/priv/OpSolarize.hpp new file mode 100644 index 000000000..958021636 --- /dev/null +++ b/src/cvcuda/priv/OpSolarize.hpp @@ -0,0 +1,48 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +/** + * @file OpSolarize.hpp + * + * @brief Defines the private C++ Class for the Solarize operation. + */ + +#ifndef CVCUDA_PRIV__SOLARIZE_HPP +#define CVCUDA_PRIV__SOLARIZE_HPP + +#include "IOperator.hpp" + +#include +#include +#include + +namespace cvcuda::priv { + +class Solarize final : public IOperator +{ +public: + explicit Solarize(); + + void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, double threshold) const; + + void operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, + double threshold) const; +}; + +} // namespace cvcuda::priv + +#endif // CVCUDA_PRIV__SOLARIZE_HPP diff --git a/src/cvcuda/priv/OpStack.cpp b/src/cvcuda/priv/OpStack.cpp index a2a59b3b1..c8b636461 100644 --- a/src/cvcuda/priv/OpStack.cpp +++ b/src/cvcuda/priv/OpStack.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,15 +17,61 @@ #include "OpStack.hpp" +#include "Nvtx.hpp" +#include "OpStackKernels.hpp" #include "nvcv/TensorDataAccess.hpp" #include +#include #include namespace cvcuda::priv { +namespace { + +bool CanRunTensorBatchKernel(cudaStream_t stream, const nvcv::TensorBatch &in, const nvcv::Tensor &out) +{ + const nvcv::TensorLayout inLayout = in.layout(); + const bool layoutMatches = (out.layout() == nvcv::TENSOR_NCHW && inLayout == nvcv::TENSOR_CHW) + || (out.layout() == nvcv::TENSOR_NHWC && inLayout == nvcv::TENSOR_HWC); + return stream != nullptr && in.rank() == 3 && in.dtype() == out.dtype() && layoutMatches; +} + +void ValidateStackTensor(const nvcv::Tensor &in, uint32_t copyIndex, uint32_t outN, uint32_t outH, uint32_t outW, + uint32_t outC) +{ + if (copyIndex >= outN) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Output tensor is not large enough to hold all input tensors"); + } + + if (in.rank() != 3 && in.rank() != 4) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input tensors must be 3D (CHW, HWC) or 4D (NCHW, NHWC)"); + } + + if (nvcv::TensorLayout inLayout = in.layout(); !(inLayout == nvcv::TENSOR_CHW || inLayout == nvcv::TENSOR_HWC + || inLayout == nvcv::TENSOR_NCHW || inLayout == nvcv::TENSOR_NHWC)) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input tensor layout must be CHW, HWC, NCHW, or NHWC"); + } + + const uint32_t isN = in.rank() == 4 ? 1 : 0; + if (outH != in.shape()[0 + isN] || outW != in.shape()[1 + isN] || outC != in.shape()[2 + isN]) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input tensors must have the same H, W, and C as output Tensor"); + } +} + +} // namespace + void Stack::operator()(cudaStream_t stream, const nvcv::TensorBatch &in, const nvcv::Tensor &out) const { + CVCUDA_NVTX_RANGE("cvcuda::Stack::operator()[TensorBatch]"); auto outData = out.exportData(); if (outData == nullptr) { @@ -36,41 +82,63 @@ void Stack::operator()(cudaStream_t stream, const nvcv::TensorBatch &in, const n // read out data N, H, W and C if (out.rank() != 4) { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Output must be NCHW orNHWC tensor"); + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Output must be NCHW or NHWC tensor"); + } + + if (nvcv::TensorLayout outLayout = out.layout(); + !(outLayout == nvcv::TENSOR_NCHW || outLayout == nvcv::TENSOR_NHWC)) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Output tensor layout must be NCHW or NHWC"); } - uint32_t outN = out.shape()[0]; + auto outN = static_cast(out.shape()[0]); // this works for both NCHW and NHWC since we are just checking if H,W,C are the same - uint32_t outH = out.shape()[1]; - uint32_t outW = out.shape()[2]; - uint32_t outC = out.shape()[3]; + auto outH = static_cast(out.shape()[1]); + auto outW = static_cast(out.shape()[2]); + auto outC = static_cast(out.shape()[3]); - uint32_t copyIndex = 0; + // A rank-3 batch maps one input tensor to one output sample, so the device descriptor index is the output index. + // Rank-4 or conversion-like inputs need the existing copy path to preserve their sample and layout semantics. + const bool useKernel = CanRunTensorBatchKernel(stream, in, out); + uint32_t copyIndex = 0; for (auto it = in.begin(); it != in.end(); ++it) { - // check if output is large enough since we could have a combo of N and non N tensors on input. - if (copyIndex >= outN) + ValidateStackTensor(*it, copyIndex, outN, outH, outW, outC); + + auto inData = it->exportData(); + if (inData == nullptr) { throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, - "Output tensor is not large enough to hold all input tensors"); + "Input must be cuda-accessible, pitch-linear tensor"); } - //check if data layout and shape is are equal. - uint32_t isN = (it->rank() == 4) ? 1 : 0; - if (outH != it->shape()[0 + isN] || outW != it->shape()[1 + isN] || outC != it->shape()[2 + isN]) + if (useKernel) { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, - "Input tensors must have the same H, W, and C as output Tensor"); + auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*inData); + NVCV_ASSERT(inAccess); + copyIndex += inAccess->numSamples(); + } + else + { + copyIndex = copyTensorToNTensor(*outData, *inData, copyIndex, stream); } + } - auto inData = it->exportData(); - if (inData == nullptr) + if (useKernel) + { + if (auto inBatchData = in.exportData(stream).cast(); + inBatchData && RunStackTensorBatchKernel(stream, *inBatchData, *outData)) { - throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, - "Output must be cuda-accessible, pitch-linear tensor"); + return; } - copyIndex = copyTensorToNTensor(*outData, *inData, copyIndex, stream); + copyIndex = 0; + for (auto it = in.begin(); it != in.end(); ++it) + { + auto inData = it->exportData(); + NVCV_ASSERT(inData); + copyIndex = copyTensorToNTensor(*outData, *inData, copyIndex, stream); + } } } @@ -98,4 +166,121 @@ int Stack::copyTensorToNTensor(const nvcv::TensorDataStridedCuda &outData, const return outIndex; } +void Stack::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::Tensor &out) const +{ + CVCUDA_NVTX_RANGE("cvcuda::Stack::operator()[ImageBatchVarShape]"); + auto outData = out.exportData(); + if (outData == nullptr) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Output must be cuda-accessible, pitch-linear tensor"); + } + + // read out data N, H, W and C + if (out.rank() != 4) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Output must be NCHW or NHWC tensor"); + } + + auto outAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*outData); + if (!outAccess) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Output must be a valid image-like tensor (NHWC or NCHW)"); + } + + auto outN = static_cast(outAccess->numSamples()); + uint32_t outH = outAccess->numRows(); + uint32_t outW = outAccess->numCols(); + uint32_t outC = outAccess->numChannels(); + + int32_t numImages = in.numImages(); + + if (static_cast(numImages) > outN) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Output tensor is not large enough to hold all input images"); + } + + bool useKernel = stream != nullptr; + uint32_t copyIndex = 0; + for (int32_t i = 0; i < numImages; ++i) + { + // Get each image from the batch using operator[] + nvcv::Image img = in[i]; + + auto imgData = img.exportData(); + if (imgData == nullptr) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input image must be cuda-accessible, pitch-linear"); + } + + const NVCVImageBufferStrided &imgBuffer = imgData->cdata().buffer.strided; + + // Check dimensions match (using plane 0 as reference) + if (static_cast(imgBuffer.planes[0].height) != outH + || static_cast(imgBuffer.planes[0].width) != outW) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input images must have the same H, W as output Tensor"); + } + + // Check number of planes matches channels expectation + if (static_cast(imgBuffer.numPlanes) != outC && imgBuffer.numPlanes != 1) + { + // For interleaved format (1 plane), we can't directly check channel count here + // For planar format, numPlanes should match outC + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input images must have the same number of channels as output Tensor"); + } + + useKernel &= imgBuffer.numPlanes == outAccess->numPlanes(); + for (int32_t planeIndex = 0; planeIndex < imgBuffer.numPlanes; ++planeIndex) + { + const NVCVImagePlaneStrided &plane = imgBuffer.planes[planeIndex]; + // Subsampled planes cannot share the rectangular launch used by the fast path. + useKernel &= static_cast(plane.height) == outH && static_cast(plane.width) == outW; + } + + ++copyIndex; + } + + if (useKernel) + { + auto inData = in.exportData(stream); + if (inData && RunStackVarShapeKernel(stream, *inData, *outData)) + { + return; + } + } + + copyIndex = 0; + for (int32_t i = 0; i < numImages; ++i) + { + auto imgData = in[i].exportData(); + NVCV_ASSERT(imgData); + copyIndex = copyImageToNTensor(imgData->cdata().buffer.strided, *outData, copyIndex, stream); + } +} + +int Stack::copyImageToNTensor(const NVCVImageBufferStrided &inData, const nvcv::TensorDataStridedCuda &outData, + uint32_t outIndex, cudaStream_t stream) const +{ + auto out = nvcv::TensorDataAccessStridedImagePlanar::Create(outData); + NVCV_ASSERT(out); + + nvcv::Byte *outSampData = out->sampleData(outIndex); + + for (int32_t p = 0; p < inData.numPlanes; ++p) + { + const NVCVImagePlaneStrided &plane = inData.planes[p]; + NVCV_CHECK_LOG(cudaMemcpy2DAsync(out->planeData(p, outSampData), out->rowStride(), plane.basePtr, + plane.rowStride, plane.width * out->colStride(), plane.height, + cudaMemcpyDeviceToDevice, stream)); + } + + return outIndex + 1; +} + } // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpStack.hpp b/src/cvcuda/priv/OpStack.hpp index 744af9887..f97c7047f 100644 --- a/src/cvcuda/priv/OpStack.hpp +++ b/src/cvcuda/priv/OpStack.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -27,6 +27,9 @@ #include "IOperator.hpp" #include +#include +#include +#include #include #include @@ -38,10 +41,13 @@ class Stack final : public IOperator { public: void operator()(cudaStream_t stream, const nvcv::TensorBatch &in, const nvcv::Tensor &out) const; + void operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::Tensor &out) const; private: int copyTensorToNTensor(const nvcv::TensorDataStridedCuda &inData, const nvcv::TensorDataStridedCuda &outData, uint32_t outIndex, cudaStream_t stream) const; + int copyImageToNTensor(const NVCVImageBufferStrided &inData, const nvcv::TensorDataStridedCuda &outData, + uint32_t outIndex, cudaStream_t stream) const; }; } // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpStackKernels.cu b/src/cvcuda/priv/OpStackKernels.cu new file mode 100644 index 000000000..5b90e49ff --- /dev/null +++ b/src/cvcuda/priv/OpStackKernels.cu @@ -0,0 +1,165 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "OpStackKernels.hpp" + +#include +#include +#include + +#include + +namespace cvcuda::priv { + +namespace { + +constexpr int kBlockSize = 256; +constexpr int kBytesPerThread = 16; + +static __device__ __forceinline__ void CopyBytes(unsigned char *dst, const unsigned char *src, int64_t offset, + int64_t rowBytes) +{ + if (offset + kBytesPerThread <= rowBytes + && ((reinterpret_cast(dst + offset) | reinterpret_cast(src + offset)) + & (alignof(uint4) - 1)) + == 0) + { + *reinterpret_cast(dst + offset) = *reinterpret_cast(src + offset); + return; + } + +#pragma unroll + for (int byte = 0; byte < kBytesPerThread; ++byte) + { + if (offset + byte < rowBytes) + { + dst[offset + byte] = src[offset + byte]; + } + } +} + +__global__ void StackTensorBatchRows(const NVCVTensorBatchElementStrided *in, nvcv::Byte *out, int64_t outSampleStride, + int64_t outPlaneStride, int64_t outRowStride, int64_t rowBytes, int32_t numPlanes, + bool isPlanar) +{ + const int32_t sample = blockIdx.z / numPlanes; + const int32_t plane = blockIdx.z - sample * numPlanes; + const int32_t row = blockIdx.y; + const int64_t offset = (static_cast(blockIdx.x) * blockDim.x + threadIdx.x) * kBytesPerThread; + + const NVCVTensorBatchElementStrided &tensor = in[sample]; + const int32_t rowDimension = isPlanar ? 1 : 0; + const unsigned char *inRow = reinterpret_cast( + tensor.data + static_cast(plane) * (isPlanar ? tensor.stride[0] : 0) + + static_cast(row) * tensor.stride[rowDimension]); + unsigned char *outRow = reinterpret_cast(out + static_cast(sample) * outSampleStride + + static_cast(plane) * outPlaneStride + + static_cast(row) * outRowStride); + + CopyBytes(outRow, inRow, offset, rowBytes); +} + +__global__ void StackVarShapeRows(const NVCVImageBufferStrided *in, nvcv::Byte *out, int64_t outSampleStride, + int64_t outPlaneStride, int64_t outRowStride, int64_t rowBytes, int32_t numPlanes) +{ + const int32_t sample = blockIdx.z / numPlanes; + const int32_t plane = blockIdx.z - sample * numPlanes; + const int32_t row = blockIdx.y; + const int64_t offset = (static_cast(blockIdx.x) * blockDim.x + threadIdx.x) * kBytesPerThread; + + const NVCVImagePlaneStrided &inPlane = in[sample].planes[plane]; + const unsigned char *inRow + = reinterpret_cast(inPlane.basePtr + static_cast(row) * inPlane.rowStride); + unsigned char *outRow = reinterpret_cast(out + static_cast(sample) * outSampleStride + + static_cast(plane) * outPlaneStride + + static_cast(row) * outRowStride); + + CopyBytes(outRow, inRow, offset, rowBytes); +} + +static bool MakeGrid(dim3 &grid, int64_t rowBytes, int32_t numRows, int64_t numSamplePlanes) +{ + constexpr int32_t kMaxGridYZ = 65535; + if (rowBytes <= 0 || numRows <= 0 || numRows > kMaxGridYZ || numSamplePlanes <= 0 || numSamplePlanes > kMaxGridYZ) + { + return false; + } + + const int64_t bytesPerBlock = static_cast(kBlockSize) * kBytesPerThread; + const int64_t numBlocks = (rowBytes + bytesPerBlock - 1) / bytesPerBlock; + if (numBlocks > INT32_MAX) + { + return false; + } + + grid = dim3(static_cast(numBlocks), numRows, static_cast(numSamplePlanes)); + return true; +} + +} // namespace + +bool RunStackTensorBatchKernel(cudaStream_t stream, const nvcv::TensorBatchDataStridedCuda &inData, + const nvcv::TensorDataStridedCuda &outData) +{ + auto out = nvcv::TensorDataAccessStridedImagePlanar::Create(outData); + if (!out || inData.rank() != 3) + { + return false; + } + + const int32_t numSamples = inData.numTensors(); + const int32_t numPlanes = out->numPlanes(); + const int64_t rowBytes = static_cast(out->numCols()) * out->colStride(); + dim3 grid; + if (!MakeGrid(grid, rowBytes, out->numRows(), static_cast(numSamples) * numPlanes)) + { + return false; + } + + const bool isPlanar = outData.layout() == nvcv::TENSOR_NCHW; + StackTensorBatchRows<<>>(inData.buffer().tensors, outData.basePtr(), + out->sampleStride(), out->planeStride(), out->rowStride(), + rowBytes, numPlanes, isPlanar); + NVCV_CHECK_THROW(cudaGetLastError()); + return true; +} + +bool RunStackVarShapeKernel(cudaStream_t stream, const nvcv::ImageBatchVarShapeDataStridedCuda &inData, + const nvcv::TensorDataStridedCuda &outData) +{ + auto out = nvcv::TensorDataAccessStridedImagePlanar::Create(outData); + if (!out) + { + return false; + } + + const int32_t numSamples = inData.numImages(); + const int32_t numPlanes = out->numPlanes(); + const int64_t rowBytes = static_cast(out->numCols()) * out->colStride(); + dim3 grid; + if (!MakeGrid(grid, rowBytes, out->numRows(), static_cast(numSamples) * numPlanes)) + { + return false; + } + + StackVarShapeRows<<>>(inData.imageList(), outData.basePtr(), out->sampleStride(), + out->planeStride(), out->rowStride(), rowBytes, numPlanes); + NVCV_CHECK_THROW(cudaGetLastError()); + return true; +} + +} // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpStackKernels.hpp b/src/cvcuda/priv/OpStackKernels.hpp new file mode 100644 index 000000000..ef561f75e --- /dev/null +++ b/src/cvcuda/priv/OpStackKernels.hpp @@ -0,0 +1,36 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#ifndef CVCUDA_PRIV__STACK_KERNELS_HPP +#define CVCUDA_PRIV__STACK_KERNELS_HPP + +#include +#include +#include +#include + +namespace cvcuda::priv { + +bool RunStackTensorBatchKernel(cudaStream_t stream, const nvcv::TensorBatchDataStridedCuda &inData, + const nvcv::TensorDataStridedCuda &outData); + +bool RunStackVarShapeKernel(cudaStream_t stream, const nvcv::ImageBatchVarShapeDataStridedCuda &inData, + const nvcv::TensorDataStridedCuda &outData); + +} // namespace cvcuda::priv + +#endif // CVCUDA_PRIV__STACK_KERNELS_HPP diff --git a/src/cvcuda/priv/OpThreshold.cpp b/src/cvcuda/priv/OpThreshold.cpp index 959adc28c..5e4eab504 100644 --- a/src/cvcuda/priv/OpThreshold.cpp +++ b/src/cvcuda/priv/OpThreshold.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "OpThreshold.hpp" +#include "Nvtx.hpp" #include "legacy/CvCudaLegacy.h" #include "legacy/CvCudaLegacyHelpers.hpp" @@ -28,16 +29,33 @@ namespace cvcuda::priv { namespace legacy = nvcv::legacy::cuda_op; Threshold::Threshold(uint32_t type, int maxBatchSize) + // Legacy operators are single-device by design. PerDeviceResource creates + // one instance per CUDA device for transparent multi-GPU support. + : m_legacyOp( + [type, maxBatchSize](int) + { + legacy::DataShape maxIn; + legacy::DataShape maxOut; + return std::make_unique(maxIn, maxOut, type, maxBatchSize); + }) + , m_legacyOpVarShape( + [type, maxBatchSize](int) + { + legacy::DataShape maxIn; + legacy::DataShape maxOut; + return std::make_unique(maxIn, maxOut, type, maxBatchSize); + }) { - legacy::DataShape maxIn, maxOut; - // maxIn/maxOut not used by op. - m_legacyOp = std::make_unique(maxIn, maxOut, type, maxBatchSize); - m_legacyOpVarShape = std::make_unique(maxIn, maxOut, type, maxBatchSize); + if (maxBatchSize < 0) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "maxBatchSize must be >= 0"); + } } void Threshold::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, const nvcv::Tensor &thresh, const nvcv::Tensor &maxval) const { + CVCUDA_NVTX_RANGE("cvcuda::Threshold::operator()[Tensor]"); auto inData = in.exportData(); if (inData == nullptr) { @@ -66,12 +84,13 @@ void Threshold::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nv "maxval must be cuda-accessible, pitch-linear tensor"); } - NVCV_CHECK_THROW(m_legacyOp->infer(*inData, *outData, *threshData, *maxvalData, stream)); + NVCV_CHECK_THROW(m_legacyOp.get().infer(*inData, *outData, *threshData, *maxvalData, stream)); } void Threshold::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, const nvcv::Tensor &thresh, const nvcv::Tensor &maxval) const { + CVCUDA_NVTX_RANGE("cvcuda::Threshold::operator()[ImageBatchVarShape]"); auto inData = in.exportData(stream); if (inData == nullptr) { @@ -98,7 +117,7 @@ void Threshold::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape & "maxval must be cuda-accessible, pitch-linear tensor"); } - NVCV_CHECK_THROW(m_legacyOpVarShape->infer(*inData, *outData, *threshData, *maxvalData, stream)); + NVCV_CHECK_THROW(m_legacyOpVarShape.get().infer(*inData, *outData, *threshData, *maxvalData, stream)); } } // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpThreshold.hpp b/src/cvcuda/priv/OpThreshold.hpp index ff9f655a3..54dd007eb 100644 --- a/src/cvcuda/priv/OpThreshold.hpp +++ b/src/cvcuda/priv/OpThreshold.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,13 +25,12 @@ #define CVCUDA_PRIV_THRESHOLD_HPP #include "IOperator.hpp" +#include "PerDeviceResource.hpp" #include "legacy/CvCudaLegacy.h" #include #include -#include - namespace cvcuda::priv { class Threshold final : public IOperator @@ -46,8 +45,8 @@ class Threshold final : public IOperator const nvcv::Tensor &thresh, const nvcv::Tensor &maxval) const; private: - std::unique_ptr m_legacyOp; - std::unique_ptr m_legacyOpVarShape; + mutable PerDeviceResource m_legacyOp; + mutable PerDeviceResource m_legacyOpVarShape; }; } // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpWarpAffine.cpp b/src/cvcuda/priv/OpWarpAffine.cpp index 96e2564cd..56e8ccc15 100644 --- a/src/cvcuda/priv/OpWarpAffine.cpp +++ b/src/cvcuda/priv/OpWarpAffine.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "OpWarpAffine.hpp" +#include "Nvtx.hpp" #include "legacy/CvCudaLegacy.h" #include "legacy/CvCudaLegacyHelpers.hpp" @@ -27,18 +28,26 @@ namespace cvcuda::priv { namespace legacy = nvcv::legacy::cuda_op; +std::unique_ptr WarpAffine::CreateLegacyOp(int) +{ + legacy::DataShape maxIn; + legacy::DataShape maxOut; + return std::make_unique(maxIn, maxOut); +} + WarpAffine::WarpAffine(const int32_t maxVarShapeBatchSize) + // Legacy operators are single-device by design. PerDeviceResource creates + // one instance per CUDA device for transparent multi-GPU support. + : m_legacyOpVarShape([maxVarShapeBatchSize](int) + { return std::make_unique(maxVarShapeBatchSize); }) { - legacy::DataShape maxIn, maxOut; - //maxIn/maxOut not used by op. - m_legacyOp = std::make_unique(maxIn, maxOut); - m_legacyOpVarShape = std::make_unique(maxVarShapeBatchSize); } void WarpAffine::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, const NVCVAffineTransform xform, const int32_t flags, const NVCVBorderType borderMode, const float4 borderValue) const { + CVCUDA_NVTX_RANGE("cvcuda::WarpAffine::operator()[Tensor]"); auto inData = in.exportData(); if (inData == nullptr) { @@ -53,13 +62,14 @@ void WarpAffine::operator()(cudaStream_t stream, const nvcv::Tensor &in, const n "Output must be cuda-accessible, pitch-linear tensor"); } - NVCV_CHECK_THROW(m_legacyOp->infer(*inData, *outData, xform, flags, borderMode, borderValue, stream)); + NVCV_CHECK_THROW(m_legacyOp.get().infer(*inData, *outData, xform, flags, borderMode, borderValue, stream)); } void WarpAffine::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, const nvcv::Tensor &transMatrix, const int32_t flags, const NVCVBorderType borderMode, const float4 borderValue) const { + CVCUDA_NVTX_RANGE("cvcuda::WarpAffine::operator()[ImageBatchVarShape]"); auto inData = in.exportData(stream); if (inData == nullptr) { @@ -80,7 +90,7 @@ void WarpAffine::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape } NVCV_CHECK_THROW( - m_legacyOpVarShape->infer(*inData, *outData, *transMatrixData, flags, borderMode, borderValue, stream)); + m_legacyOpVarShape.get().infer(*inData, *outData, *transMatrixData, flags, borderMode, borderValue, stream)); } } // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpWarpAffine.hpp b/src/cvcuda/priv/OpWarpAffine.hpp index 67695ca3f..c2778dbf5 100644 --- a/src/cvcuda/priv/OpWarpAffine.hpp +++ b/src/cvcuda/priv/OpWarpAffine.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,6 +25,7 @@ #define CVCUDA_PRIV_WARP_AFFINE_HPP #include "IOperator.hpp" +#include "PerDeviceResource.hpp" #include "legacy/CvCudaLegacy.h" #include @@ -49,8 +50,10 @@ class WarpAffine final : public IOperator const float4 borderValue) const; private: - std::unique_ptr m_legacyOp; - std::unique_ptr m_legacyOpVarShape; + static std::unique_ptr CreateLegacyOp(int deviceId); + + mutable PerDeviceResource m_legacyOp{CreateLegacyOp}; + mutable PerDeviceResource m_legacyOpVarShape; }; } // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpWarpPerspective.cpp b/src/cvcuda/priv/OpWarpPerspective.cpp index 92e784aa4..056bbfc44 100644 --- a/src/cvcuda/priv/OpWarpPerspective.cpp +++ b/src/cvcuda/priv/OpWarpPerspective.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "OpWarpPerspective.hpp" +#include "Nvtx.hpp" #include "legacy/CvCudaLegacy.h" #include "legacy/CvCudaLegacyHelpers.hpp" @@ -27,18 +28,26 @@ namespace cvcuda::priv { namespace legacy = nvcv::legacy::cuda_op; +std::unique_ptr WarpPerspective::CreateLegacyOp(int) +{ + legacy::DataShape maxIn; + legacy::DataShape maxOut; + return std::make_unique(maxIn, maxOut); +} + WarpPerspective::WarpPerspective(const int32_t maxVarShapeBatchSize) + // Legacy operators are single-device by design. PerDeviceResource creates + // one instance per CUDA device for transparent multi-GPU support. + : m_legacyOpVarShape([maxVarShapeBatchSize](int) + { return std::make_unique(maxVarShapeBatchSize); }) { - legacy::DataShape maxIn, maxOut; - //maxIn/maxOut not used by op. - m_legacyOp = std::make_unique(maxIn, maxOut); - m_legacyOpVarShape = std::make_unique(maxVarShapeBatchSize); } void WarpPerspective::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out, const NVCVPerspectiveTransform transMatrix, const int32_t flags, const NVCVBorderType borderMode, const float4 borderValue) const { + CVCUDA_NVTX_RANGE("cvcuda::WarpPerspective::operator()[Tensor]"); auto inData = in.exportData(); if (inData == nullptr) { @@ -53,13 +62,14 @@ void WarpPerspective::operator()(cudaStream_t stream, const nvcv::Tensor &in, co "Output must be cuda-accessible, pitch-linear tensor"); } - NVCV_CHECK_THROW(m_legacyOp->infer(*inData, *outData, transMatrix, flags, borderMode, borderValue, stream)); + NVCV_CHECK_THROW(m_legacyOp.get().infer(*inData, *outData, transMatrix, flags, borderMode, borderValue, stream)); } void WarpPerspective::operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &in, const nvcv::ImageBatchVarShape &out, const nvcv::Tensor &transMatrix, const int32_t flags, const NVCVBorderType borderMode, const float4 borderValue) const { + CVCUDA_NVTX_RANGE("cvcuda::WarpPerspective::operator()[ImageBatchVarShape]"); auto inData = in.exportData(stream); if (inData == nullptr) { @@ -80,7 +90,7 @@ void WarpPerspective::operator()(cudaStream_t stream, const nvcv::ImageBatchVarS } NVCV_CHECK_THROW( - m_legacyOpVarShape->infer(*inData, *outData, *transMatrixData, flags, borderMode, borderValue, stream)); + m_legacyOpVarShape.get().infer(*inData, *outData, *transMatrixData, flags, borderMode, borderValue, stream)); } } // namespace cvcuda::priv diff --git a/src/cvcuda/priv/OpWarpPerspective.hpp b/src/cvcuda/priv/OpWarpPerspective.hpp index 4953c27a1..da2b19811 100644 --- a/src/cvcuda/priv/OpWarpPerspective.hpp +++ b/src/cvcuda/priv/OpWarpPerspective.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,6 +25,7 @@ #define CVCUDA_PRIV_WARP_PERSPECTIVE_HPP #include "IOperator.hpp" +#include "PerDeviceResource.hpp" #include "legacy/CvCudaLegacy.h" #include @@ -49,8 +50,10 @@ class WarpPerspective final : public IOperator const float4 borderValue) const; private: - std::unique_ptr m_legacyOp; - std::unique_ptr m_legacyOpVarShape; + static std::unique_ptr CreateLegacyOp(int deviceId); + + mutable PerDeviceResource m_legacyOp{CreateLegacyOp}; + mutable PerDeviceResource m_legacyOpVarShape; }; } // namespace cvcuda::priv diff --git a/src/cvcuda/priv/PerDeviceResource.hpp b/src/cvcuda/priv/PerDeviceResource.hpp new file mode 100644 index 000000000..253309cd2 --- /dev/null +++ b/src/cvcuda/priv/PerDeviceResource.hpp @@ -0,0 +1,109 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#ifndef CVCUDA_PRIV_PER_DEVICE_RESOURCE_HPP +#define CVCUDA_PRIV_PER_DEVICE_RESOURCE_HPP + +#include +#include + +#include +#include +#include +#include +#include + +namespace cvcuda::priv { + +// Lazily creates and caches a resource T per CUDA device. On the first call +// from a new device the factory runs (with that device already current). +// Subsequent calls from the same device return the cached instance. +// +// Thread-safe: concurrent get() calls from different threads are serialized +// via a shared_mutex (readers) / unique_mutex (writers) pair. +// +// Multi-GPU contract: legacy operators are single-device by design -- they +// allocate GPU memory in their constructor and free it in their destructor, +// with no device-switching logic. PerDeviceResource is the layer that +// provides multi-GPU support: it maintains one operator instance per device +// and ensures each instance is created (and destroyed) with the correct +// CUDA device active. Callers simply use get() and always receive the +// instance that belongs to the current device. +template +class PerDeviceResource +{ +public: + using Factory = std::function(int deviceId)>; + + explicit PerDeviceResource(Factory factory) + : m_factory(std::move(factory)) + { + } + + ~PerDeviceResource() + { + int savedDevice = -1; + cudaGetDevice(&savedDevice); + + // Each resource was allocated on a specific device; set that device + // before destroying it, then restore the caller's device context. + for (auto &[dev, ptr] : m_resources) + { + cudaSetDevice(dev); + ptr.reset(); + } + if (savedDevice >= 0) + { + cudaSetDevice(savedDevice); + } + } + + PerDeviceResource(const PerDeviceResource &) = delete; + PerDeviceResource &operator=(const PerDeviceResource &) = delete; + + T &get() + { + int dev; + { + cudaError_t err = cudaGetDevice(&dev); + if (err != cudaSuccess) + throw nvcv::Exception(nvcv::Status::ERROR_INTERNAL, "cudaGetDevice failed"); + } + + { + std::shared_lock lock(m_mutex); + auto it = m_resources.find(dev); + if (it != m_resources.end()) + return *it->second; + } + + std::unique_lock lock(m_mutex); + auto [it, _] = m_resources.try_emplace(dev, nullptr); + if (!it->second) + it->second = m_factory(dev); + return *it->second; + } + +private: + Factory m_factory; + std::unordered_map> m_resources; + mutable std::shared_mutex m_mutex; +}; + +} // namespace cvcuda::priv + +#endif // CVCUDA_PRIV_PER_DEVICE_RESOURCE_HPP diff --git a/src/cvcuda/priv/PlanarTensorView.hpp b/src/cvcuda/priv/PlanarTensorView.hpp new file mode 100644 index 000000000..c58a49474 --- /dev/null +++ b/src/cvcuda/priv/PlanarTensorView.hpp @@ -0,0 +1,123 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#ifndef CVCUDA_PRIV_PLANAR_TENSOR_VIEW_HPP +#define CVCUDA_PRIV_PLANAR_TENSOR_VIEW_HPP + +#include +#include +#include + +#include +#include + +namespace cvcuda::priv { + +// Build a single-channel (N*C, H, W, 1) NHWC view of a packed planar (NCHW/CHW) tensor. +// +// Operators that treat color channels independently (Resize, ConvertTo, Remap, ...) can process a +// planar image by viewing each (sample, channel) plane as a single-channel image: flattening the +// planes into N*C "samples" lets the existing interleaved single-channel kernel run unchanged. The +// view treats plane (n, c) as sample index n*C + c at byte offset (n*C + c) * chStride, which +// matches the real layout only when the channel planes are tightly packed +// (sampleStride == numChannels * chStride). That always holds within a single sample (n == 0); a +// batched tensor additionally requires tight packing across samples, which is validated here. +inline nvcv::TensorDataStridedCuda PlanarAsSingleChannelView(const nvcv::TensorDataStridedCuda &data, + const nvcv::TensorDataAccessStridedImagePlanar &access) +{ + const int64_t numSamples = access.numSamples(); + const int64_t numChannels = access.numChannels(); + const int64_t numRows = access.numRows(); + const int64_t numCols = access.numCols(); + + // A single sample may legitimately have sampleStride > C*chStride due to allocation alignment, + // so the uniform-stride flatten is only valid for batches when the planes are tightly packed. + if (numSamples > 1 && access.sampleStride() != numChannels * access.chStride()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Planar processing of a batched tensor requires tightly packed channel planes"); + } + + nvcv::TensorDataStridedCuda::Buffer buf; + buf.basePtr = reinterpret_cast(data.basePtr()); + buf.strides[0] = access.chStride(); // N*C flattened planes + buf.strides[1] = access.rowStride(); // H + buf.strides[2] = access.colStride(); // W + buf.strides[3] = access.colStride(); // C == 1 + return nvcv::TensorDataStridedCuda{ + nvcv::TensorShape{{numSamples * numChannels, numRows, numCols, 1}, "NHWC"}, + data.dtype(), buf + }; +} + +// Validate an input/output tensor pair for an independent-channel op that supports planar layout, +// and, when the pair is planar, return flattened single-channel (N*C, H, W, 1) views of each so the +// caller can run its interleaved single-channel kernel unchanged. Returns std::nullopt for the +// interleaved case (the caller runs its normal path). Throws ERROR_INVALID_ARGUMENT on a mixed +// planar/interleaved pair, an unsupported channel count (only 1/3/4; 2-channel planar is +// unsupported), a sample/channel-count mismatch, or a flattened plane count above the CUDA grid-z +// limit. Layout is inferred from the tensors, so this adds planar as a capability with no API change. +inline std::optional> + PlanarSingleChannelViews(const nvcv::TensorDataStridedCuda &inData, const nvcv::TensorDataStridedCuda &outData) +{ + const nvcv::TensorLayout inLayout = inData.layout(); + const nvcv::TensorLayout outLayout = outData.layout(); + const bool inPlanar = (inLayout == nvcv::TENSOR_NCHW || inLayout == nvcv::TENSOR_CHW); + + if (const bool outPlanar = (outLayout == nvcv::TENSOR_NCHW || outLayout == nvcv::TENSOR_CHW); + inPlanar != outPlanar) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input and output must both be interleaved (N)HWC or both planar (N)CHW"); + } + if (!inPlanar) + { + return std::nullopt; + } + + auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(inData); + auto outAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(outData); + if (!inAccess || !outAccess) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Planar processing requires image-like NCHW/CHW tensors"); + } + + const int64_t numChannels = inAccess->numChannels(); + if (numChannels < 1 || numChannels > 4 || numChannels == 2) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Invalid number of channels (2-channel planar is unsupported)"); + } + if (inAccess->numSamples() != outAccess->numSamples() || numChannels != outAccess->numChannels()) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Input and output must have the same sample and channel counts"); + } + + if (const int64_t planarBatch = numChannels * inAccess->numSamples(); planarBatch > 65535) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, + "Planar processing requires numSamples * numChannels <= 65535 (CUDA grid-z limit)"); + } + + return std::make_pair(PlanarAsSingleChannelView(inData, *inAccess), PlanarAsSingleChannelView(outData, *outAccess)); +} + +} // namespace cvcuda::priv + +#endif // CVCUDA_PRIV_PLANAR_TENSOR_VIEW_HPP diff --git a/src/cvcuda/priv/SafeSize.hpp b/src/cvcuda/priv/SafeSize.hpp new file mode 100644 index 000000000..f801d43d7 --- /dev/null +++ b/src/cvcuda/priv/SafeSize.hpp @@ -0,0 +1,56 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef CVCUDA_PRIV_SAFE_SIZE_HPP +#define CVCUDA_PRIV_SAFE_SIZE_HPP + +#include + +#include +#include +#include + +namespace cvcuda::priv { + +inline size_t CheckedMul(size_t a, size_t b, const char *message) +{ + if (b != 0 && a > std::numeric_limits::max() / b) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "%s", message); + } + return a * b; +} + +inline size_t CheckedMulMany(std::initializer_list factors, const char *message) +{ + size_t result = 1; + for (size_t factor : factors) + { + result = CheckedMul(result, factor, message); + } + return result; +} + +inline size_t CheckedNonNegativeToSize(int value, const char *name) +{ + if (value < 0) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "%s must be >= 0", name); + } + return static_cast(value); +} + +inline size_t CheckedPositiveToSize(int value, const char *name) +{ + if (value <= 0) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "%s must be > 0", name); + } + return static_cast(value); +} + +} // namespace cvcuda::priv + +#endif // CVCUDA_PRIV_SAFE_SIZE_HPP diff --git a/src/cvcuda/priv/Types.hpp b/src/cvcuda/priv/Types.hpp index d666e0d5c..c0383ad61 100644 --- a/src/cvcuda/priv/Types.hpp +++ b/src/cvcuda/priv/Types.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,15 +18,22 @@ #ifndef CVCUDA_TYPES_HPP #define CVCUDA_TYPES_HPP +#include "SafeSize.hpp" + +#include #include #include +#include +#include #include +#include +#include #include namespace cvcuda::priv { -#define checkERR(call) check_error(call, #call, __LINE__, __FILE__) +#define checkERR(call) check_error(call, #call, __LINE__, __FILE__) // NOSONAR: std::source_location is C++20. inline static bool check_error(cudaError_t e, const char *call, int line, const char *file) { @@ -42,83 +49,38 @@ inline static bool check_error(cudaError_t e, const char *call, int line, const // Default font, user can install via below command: // sudo apt-get update // sudo apt-get install ttf-dejavu fonts-dejavu -#define DEFAULT_OSD_FONT "DejaVuSansMono" +inline constexpr char DEFAULT_OSD_FONT[] = "DejaVuSansMono"; + +inline size_t NVCVPolyLinePointCount(int32_t numPoints) +{ + return CheckedMulMany({2U, CheckedPositiveToSize(numPoints, "NVCVPolyLine numPoints")}, + "NVCVPolyLine point count overflow"); +} + +inline size_t NVCVPolyLineByteCount(int32_t numPoints) +{ + return CheckedMulMany({NVCVPolyLinePointCount(numPoints), sizeof(int32_t)}, + "NVCVPolyLine allocation size overflow"); +} class NVCVText { public: - const char *utf8Text = nullptr; // Text to draw in utf8 format. - int32_t fontSize; // Font size for the text. - const char *fontName = nullptr; // Font name for the text. - NVCVPointI tlPos; // Top-left corner point for label text, \ref NVCVPointI. - NVCVColorRGBA fontColor; // Font color of the text. - NVCVColorRGBA bgColor; // Background color of text box. + std::string utf8Text; // Text to draw in utf8 format. + int32_t fontSize; // Font size for the text. + std::string fontName; // Font name for the text. + NVCVPointI tlPos; // Top-left corner point for label text, \ref NVCVPointI. + NVCVColorRGBA fontColor; // Font color of the text. + NVCVColorRGBA bgColor; // Background color of text box. NVCVText(const char *_utf8Text, int32_t _fontSize, const char *_fontName, NVCVPointI _tlPos, NVCVColorRGBA _fontColor, NVCVColorRGBA _bgColor) - : fontSize(_fontSize) + : utf8Text(_utf8Text) + , fontSize(_fontSize) + , fontName(_fontName) , tlPos(_tlPos) , fontColor(_fontColor) - , bgColor(_bgColor) - { - size_t len = std::char_traits::length(_utf8Text); - char *tmp_utf8Text = (char *)malloc(len + 1); - std::copy_n(_utf8Text, len + 1, tmp_utf8Text); - len = std::char_traits::length(_fontName); - char *tmp_fontName = (char *)malloc(len + 1); - std::copy_n(_fontName, len + 1, tmp_fontName); - utf8Text = tmp_utf8Text; - fontName = tmp_fontName; - } - - NVCVText(const NVCVText &text) - : fontSize(text.fontSize) - , tlPos(text.tlPos) - , fontColor(text.fontColor) - , bgColor(text.bgColor) - { - size_t len = std::char_traits::length(text.utf8Text); - char *tmp_utf8Text = (char *)malloc(len + 1); - std::copy_n(text.utf8Text, len + 1, tmp_utf8Text); - len = std::char_traits::length(text.fontName); - char *tmp_fontName = (char *)malloc(len + 1); - std::copy_n(text.fontName, len + 1, tmp_fontName); - utf8Text = tmp_utf8Text; - fontName = tmp_fontName; - } - - NVCVText &operator=(const NVCVText &text) - { - if (this != &text) - { - if (utf8Text != nullptr) - { - free((void *)utf8Text); - utf8Text = nullptr; - } - if (fontName != nullptr) - { - free((void *)fontName); - fontName = nullptr; - } - *this = NVCVText(text); - } - return *this; - }; - - ~NVCVText() - { - if (utf8Text != nullptr) - { - free((void *)utf8Text); - utf8Text = nullptr; - } - if (fontName != nullptr) - { - free((void *)fontName); - fontName = nullptr; - } - }; + , bgColor(_bgColor){}; }; class NVCVSegment @@ -138,7 +100,7 @@ class NVCVSegment NVCVColorRGBA borderColor; // Line color of segment outter rect. NVCVColorRGBA segColor; // Segment mask color. - NVCVSegment(NVCVBoxI _box, int32_t _thickness, float *_hSeg, int32_t _segWidth, int32_t _segHeight, + NVCVSegment(NVCVBoxI _box, int32_t _thickness, const float *_hSeg, int32_t _segWidth, int32_t _segHeight, float _segThreshold, NVCVColorRGBA _borderColor, NVCVColorRGBA _segColor) : box(_box) , thickness(_thickness) @@ -182,19 +144,19 @@ class NVCVSegment class NVCVPolyLine { public: - int32_t *hPoints = nullptr; // Host pointer for polyline points' xy, cannot be nullptr. - // Array length: 2 * numPoints. - // Format : X0, Y0, X1, Y1, ..., Xk, Yk, ... - int32_t *dPoints = nullptr; // Device pointer for polyline points' xy. - // Can be nullptr only if fillColor.a == 0. - // Array length: 2 * numPoints. - // Format: X0, Y0, X1, Y1, ..., Xk, Yk, ... - int32_t numPoints; // Number of polyline points. - int32_t thickness; // Polyline thickness. - bool isClosed; // Connect p(0) to p(n-1) or not. - NVCVColorRGBA borderColor; // Line color of polyline border. - NVCVColorRGBA fillColor; // Fill color of poly fill area. - bool interpolation; // Default: true + std::vector hPoints; // Host polyline points' xy. + // Array length: 2 * numPoints. + // Format : X0, Y0, X1, Y1, ..., Xk, Yk, ... + int32_t *dPoints = nullptr; // Device pointer for polyline points' xy. + // Can be nullptr only if fillColor.a == 0. + // Array length: 2 * numPoints. + // Format: X0, Y0, X1, Y1, ..., Xk, Yk, ... + int32_t numPoints; // Number of polyline points. + int32_t thickness; // Polyline thickness. + bool isClosed; // Connect p(0) to p(n-1) or not. + NVCVColorRGBA borderColor; // Line color of polyline border. + NVCVColorRGBA fillColor; // Fill color of poly fill area. + bool interpolation; // Default: true NVCVPolyLine(int32_t *_hPoints, int32_t _numPoints, int32_t _thickness, bool _isClosed, NVCVColorRGBA _borderColor, NVCVColorRGBA _fillColor, bool _interpolation) @@ -205,37 +167,31 @@ class NVCVPolyLine , fillColor(_fillColor) , interpolation(_interpolation) { - hPoints = (int *)malloc(numPoints * 2 * sizeof(int)); - checkERR(cudaMalloc(&dPoints, 2 * numPoints * sizeof(int))); - - std::copy_n(_hPoints, 2 * numPoints, hPoints); - checkERR(cudaMemcpy(dPoints, _hPoints, 2 * numPoints * sizeof(int), cudaMemcpyHostToDevice)); + const size_t pointCount = NVCVPolyLinePointCount(numPoints); + const size_t pointBytes = NVCVPolyLineByteCount(numPoints); + hPoints.assign(_hPoints, _hPoints + pointCount); + checkERR(cudaMalloc(&dPoints, pointBytes)); + checkERR(cudaMemcpy(dPoints, hPoints.data(), pointBytes, cudaMemcpyHostToDevice)); } NVCVPolyLine(const NVCVPolyLine &pl) - : numPoints(pl.numPoints) + : hPoints(pl.hPoints) + , numPoints(pl.numPoints) , thickness(pl.thickness) , isClosed(pl.isClosed) , borderColor(pl.borderColor) , fillColor(pl.fillColor) , interpolation(pl.interpolation) { - hPoints = (int *)malloc(numPoints * 2 * sizeof(int)); - checkERR(cudaMalloc(&dPoints, 2 * numPoints * sizeof(int))); - - std::copy_n(pl.hPoints, 2 * numPoints, hPoints); - checkERR(cudaMemcpy(dPoints, pl.dPoints, 2 * numPoints * sizeof(int), cudaMemcpyDeviceToDevice)); + const size_t pointBytes = NVCVPolyLineByteCount(numPoints); + checkERR(cudaMalloc(&dPoints, pointBytes)); + checkERR(cudaMemcpy(dPoints, pl.dPoints, pointBytes, cudaMemcpyDeviceToDevice)); } NVCVPolyLine &operator=(const NVCVPolyLine &) = delete; ~NVCVPolyLine() { - if (hPoints != nullptr) - { - free(hPoints); - hPoints = nullptr; - } if (dPoints != nullptr) { checkERR(cudaFree(dPoints)); @@ -247,80 +203,56 @@ class NVCVPolyLine class NVCVClock { public: - NVCVClockFormat clockFormat; // Pre-defined clock format. - long time; // Clock time. - int32_t fontSize; // Font size. - const char *font = nullptr; // Font name. - NVCVPointI tlPos; // Top-left corner point, \ref NVCVPointI. - NVCVColorRGBA fontColor; // Font color of the text. - NVCVColorRGBA bgColor; // Background color of text box. + NVCVClockFormat clockFormat; // Pre-defined clock format. + long time; // Clock time. + int32_t fontSize; // Font size. + std::string font; // Font name. + NVCVPointI tlPos; // Top-left corner point, \ref NVCVPointI. + NVCVColorRGBA fontColor; // Font color of the text. + NVCVColorRGBA bgColor; // Background color of text box. NVCVClock(NVCVClockFormat _clockFormat, long _time, int32_t _fontSize, const char *_font, NVCVPointI _tlPos, NVCVColorRGBA _fontColor, NVCVColorRGBA _bgColor) : clockFormat(_clockFormat) , time(_time) , fontSize(_fontSize) + , font(_font) , tlPos(_tlPos) , fontColor(_fontColor) - , bgColor(_bgColor) - { - const size_t len = std::char_traits::length(_font); - char *tmp_font = (char *)malloc(len + 1); - std::copy_n(_font, len + 1, tmp_font); - font = tmp_font; - } - - NVCVClock(const NVCVClock &clock) - : clockFormat(clock.clockFormat) - , time(clock.time) - , fontSize(clock.fontSize) - , tlPos(clock.tlPos) - , fontColor(clock.fontColor) - , bgColor(clock.bgColor) - { - const size_t len = std::char_traits::length(clock.font); - char *tmp_font = (char *)malloc(len + 1); - std::copy_n(clock.font, len + 1, tmp_font); - font = tmp_font; - } - - NVCVClock &operator=(const NVCVClock &clock) - { - if (this != &clock) - { - if (font != nullptr) - { - free((void *)font); - font = nullptr; - } - *this = NVCVClock(clock); - } - return *this; - }; - - ~NVCVClock() - { - if (font != nullptr) - { - free((void *)font); - font = nullptr; - } - }; + , bgColor(_bgColor){}; }; class NVCVElement { public: - NVCVElement(NVCVOSDType osd_type, const void *src); + using Data = std::variant; + + explicit NVCVElement(NVCVOSDType osd_type); + NVCVElement(NVCVOSDType osd_type, std::nullptr_t); + + template + NVCVElement(NVCVOSDType osd_type, const ElementData *src); + NVCVElement(const NVCVElement &) = delete; NVCVElement &operator=(const NVCVElement &) = delete; - ~NVCVElement(); + ~NVCVElement() = default; - NVCVOSDType type(); - void *ptr(); - // void assign(const void* src); + NVCVOSDType type() const; + Data &data(); private: + void setData(const NVCVBndBoxI &src); + void setData(const NVCVText &src); + void setData(const NVCVSegment &src); + void setData(const NVCVPoint &src); + void setData(const NVCVLine &src); + void setData(const NVCVPolyLine &src); + void setData(const NVCVRotatedBox &src); + void setData(const NVCVCircle &src); + void setData(const NVCVArrow &src); + void setData(const NVCVClock &src); + /* * type: * NVCV_OSD_RECT - \ref NVCVBndBoxI. @@ -335,194 +267,115 @@ class NVCVElement * NVCV_OSD_CLOCK - \ref NVCVClock. */ NVCVOSDType m_type; // OSD element type to draw. - void *m_data; // OSD element data pointer. + Data m_data; // OSD element data. }; -inline NVCVElement::NVCVElement(NVCVOSDType osd_type, const void *src) +inline NVCVElement::NVCVElement(NVCVOSDType osd_type) : m_type(osd_type) { - switch (m_type) - { - case NVCVOSDType::NVCV_OSD_RECT: - { - auto rect = NVCVBndBoxI(*(NVCVBndBoxI *)src); - m_data = new NVCVBndBoxI(rect); - break; - } - case NVCVOSDType::NVCV_OSD_TEXT: - { - auto text = NVCVText(*(NVCVText *)src); - m_data = new NVCVText(text); - break; - } - case NVCVOSDType::NVCV_OSD_SEGMENT: - { - auto segment = NVCVSegment(*(NVCVSegment *)src); - m_data = new NVCVSegment(segment); - break; - } - case NVCVOSDType::NVCV_OSD_POINT: - { - auto point = NVCVPoint(*(NVCVPoint *)src); - m_data = new NVCVPoint(point); - break; - } - case NVCVOSDType::NVCV_OSD_LINE: - { - auto line = NVCVLine(*(NVCVLine *)src); - m_data = new NVCVLine(line); - break; - } - case NVCVOSDType::NVCV_OSD_POLYLINE: - { - auto pl = NVCVPolyLine(*(NVCVPolyLine *)src); - m_data = new NVCVPolyLine(pl); - break; - } - case NVCVOSDType::NVCV_OSD_ROTATED_RECT: - { - auto rb = NVCVRotatedBox(*(NVCVRotatedBox *)src); - m_data = new NVCVRotatedBox(rb); - break; - } - case NVCVOSDType::NVCV_OSD_CIRCLE: - { - auto circle = NVCVCircle(*(NVCVCircle *)src); - m_data = new NVCVCircle(circle); - break; - } - case NVCVOSDType::NVCV_OSD_ARROW: - { - auto arrow = NVCVArrow(*(NVCVArrow *)src); - m_data = new NVCVArrow(arrow); - break; - } - case NVCVOSDType::NVCV_OSD_CLOCK: +} + +inline NVCVElement::NVCVElement(NVCVOSDType osd_type, std::nullptr_t) + : NVCVElement(osd_type) +{ +} + +template +inline NVCVElement::NVCVElement(NVCVOSDType osd_type, const ElementData *src) + : NVCVElement(osd_type) +{ + if (src != nullptr) { - auto clock = NVCVClock(*(NVCVClock *)src); - m_data = new NVCVClock(clock); - break; - } - default: - break; + setData(*src); } } -inline NVCVElement::~NVCVElement() +inline void NVCVElement::setData(const NVCVBndBoxI &src) { - switch (m_type) + if (m_type == NVCVOSDType::NVCV_OSD_RECT) { - case NVCVOSDType::NVCV_OSD_RECT: - { - NVCVBndBoxI *bndBox = (NVCVBndBoxI *)m_data; - if (bndBox != nullptr) - { - delete (bndBox); - bndBox = nullptr; - } - break; + m_data.emplace(src); } - case NVCVOSDType::NVCV_OSD_TEXT: +} + +inline void NVCVElement::setData(const NVCVText &src) +{ + if (m_type == NVCVOSDType::NVCV_OSD_TEXT) { - NVCVText *label = (NVCVText *)m_data; - if (label != nullptr) - { - delete (label); - label = nullptr; - } - break; + m_data.emplace(src); } - case NVCVOSDType::NVCV_OSD_SEGMENT: +} + +inline void NVCVElement::setData(const NVCVSegment &src) +{ + if (m_type == NVCVOSDType::NVCV_OSD_SEGMENT) { - NVCVSegment *segment = (NVCVSegment *)m_data; - if (segment != nullptr) - { - delete (segment); - segment = nullptr; - } - break; + m_data.emplace(src); } - case NVCVOSDType::NVCV_OSD_POINT: +} + +inline void NVCVElement::setData(const NVCVPoint &src) +{ + if (m_type == NVCVOSDType::NVCV_OSD_POINT) { - NVCVPoint *point = (NVCVPoint *)m_data; - if (point != nullptr) - { - delete (point); - point = nullptr; - } - break; + m_data.emplace(src); } - case NVCVOSDType::NVCV_OSD_LINE: +} + +inline void NVCVElement::setData(const NVCVLine &src) +{ + if (m_type == NVCVOSDType::NVCV_OSD_LINE) { - NVCVLine *line = (NVCVLine *)m_data; - if (line != nullptr) - { - delete (line); - line = nullptr; - } - break; + m_data.emplace(src); } - case NVCVOSDType::NVCV_OSD_POLYLINE: +} + +inline void NVCVElement::setData(const NVCVPolyLine &src) +{ + if (m_type == NVCVOSDType::NVCV_OSD_POLYLINE) { - NVCVPolyLine *pl = (NVCVPolyLine *)m_data; - if (pl != nullptr) - { - delete (pl); - pl = nullptr; - } - break; + m_data.emplace(src); } - case NVCVOSDType::NVCV_OSD_ROTATED_RECT: +} + +inline void NVCVElement::setData(const NVCVRotatedBox &src) +{ + if (m_type == NVCVOSDType::NVCV_OSD_ROTATED_RECT) { - NVCVRotatedBox *rb = (NVCVRotatedBox *)m_data; - if (rb != nullptr) - { - delete (rb); - rb = nullptr; - } - break; + m_data.emplace(src); } - case NVCVOSDType::NVCV_OSD_CIRCLE: +} + +inline void NVCVElement::setData(const NVCVCircle &src) +{ + if (m_type == NVCVOSDType::NVCV_OSD_CIRCLE) { - NVCVCircle *circle = (NVCVCircle *)m_data; - if (circle != nullptr) - { - delete (circle); - circle = nullptr; - } - break; + m_data.emplace(src); } - case NVCVOSDType::NVCV_OSD_ARROW: +} + +inline void NVCVElement::setData(const NVCVArrow &src) +{ + if (m_type == NVCVOSDType::NVCV_OSD_ARROW) { - NVCVArrow *arrow = (NVCVArrow *)m_data; - if (arrow != nullptr) - { - delete (arrow); - arrow = nullptr; - } - break; + m_data.emplace(src); } - case NVCVOSDType::NVCV_OSD_CLOCK: +} + +inline void NVCVElement::setData(const NVCVClock &src) +{ + if (m_type == NVCVOSDType::NVCV_OSD_CLOCK) { - NVCVClock *clock = (NVCVClock *)m_data; - if (clock != nullptr) - { - delete (clock); - clock = nullptr; - } - break; - } - default: - break; + m_data.emplace(src); } } -inline NVCVOSDType NVCVElement::type() +inline NVCVOSDType NVCVElement::type() const { return m_type; } -inline void *NVCVElement::ptr() +inline NVCVElement::Data &NVCVElement::data() { return m_data; } @@ -530,7 +383,7 @@ inline void *NVCVElement::ptr() class NVCVBlurBoxesImpl { public: - NVCVBlurBoxesImpl(const std::vector> &blurboxes_vec); + explicit NVCVBlurBoxesImpl(const std::vector> &blurboxes_vec); NVCVBlurBoxesImpl(const NVCVBlurBoxesImpl &) = delete; NVCVBlurBoxesImpl &operator=(const NVCVBlurBoxesImpl &) = delete; ~NVCVBlurBoxesImpl(); @@ -544,8 +397,8 @@ class NVCVBlurBoxesImpl }; inline NVCVBlurBoxesImpl::NVCVBlurBoxesImpl(const std::vector> &blurboxes_vec) + : m_blurboxes_vec(blurboxes_vec) { - m_blurboxes_vec = blurboxes_vec; } inline NVCVBlurBoxesImpl::~NVCVBlurBoxesImpl() @@ -556,12 +409,12 @@ inline NVCVBlurBoxesImpl::~NVCVBlurBoxesImpl() inline int32_t NVCVBlurBoxesImpl::batch() const { - return m_blurboxes_vec.size(); + return static_cast(m_blurboxes_vec.size()); } inline int32_t NVCVBlurBoxesImpl::numBoxesAt(int32_t b) const { - return m_blurboxes_vec[b].size(); + return static_cast(m_blurboxes_vec[b].size()); } inline NVCVBlurBoxI NVCVBlurBoxesImpl::boxAt(int32_t b, int32_t i) const @@ -572,7 +425,7 @@ inline NVCVBlurBoxI NVCVBlurBoxesImpl::boxAt(int32_t b, int32_t i) const class NVCVBndBoxesImpl { public: - NVCVBndBoxesImpl(const std::vector> &bndboxes_vec); + explicit NVCVBndBoxesImpl(const std::vector> &bndboxes_vec); NVCVBndBoxesImpl(const NVCVBndBoxesImpl &) = delete; NVCVBndBoxesImpl &operator=(const NVCVBndBoxesImpl &) = delete; ~NVCVBndBoxesImpl(); @@ -586,8 +439,8 @@ class NVCVBndBoxesImpl }; inline NVCVBndBoxesImpl::NVCVBndBoxesImpl(const std::vector> &bndboxes_vec) + : m_bndboxes_vec(bndboxes_vec) { - m_bndboxes_vec = bndboxes_vec; } inline NVCVBndBoxesImpl::~NVCVBndBoxesImpl() @@ -598,12 +451,12 @@ inline NVCVBndBoxesImpl::~NVCVBndBoxesImpl() inline int32_t NVCVBndBoxesImpl::batch() const { - return m_bndboxes_vec.size(); + return static_cast(m_bndboxes_vec.size()); } inline int32_t NVCVBndBoxesImpl::numBoxesAt(int32_t b) const { - return m_bndboxes_vec[b].size(); + return static_cast(m_bndboxes_vec[b].size()); } inline NVCVBndBoxI NVCVBndBoxesImpl::boxAt(int32_t b, int32_t i) const @@ -614,7 +467,7 @@ inline NVCVBndBoxI NVCVBndBoxesImpl::boxAt(int32_t b, int32_t i) const class NVCVElementsImpl { public: - NVCVElementsImpl(const std::vector>> &elements_vec); + explicit NVCVElementsImpl(const std::vector>> &elements_vec); NVCVElementsImpl(const NVCVElementsImpl &) = delete; NVCVElementsImpl &operator=(const NVCVElementsImpl &) = delete; ~NVCVElementsImpl(); @@ -628,8 +481,8 @@ class NVCVElementsImpl }; inline NVCVElementsImpl::NVCVElementsImpl(const std::vector>> &elements_vec) + : m_elements_vec(elements_vec) { - m_elements_vec = elements_vec; } inline NVCVElementsImpl::~NVCVElementsImpl() @@ -640,12 +493,12 @@ inline NVCVElementsImpl::~NVCVElementsImpl() inline int32_t NVCVElementsImpl::batch() const { - return m_elements_vec.size(); + return static_cast(m_elements_vec.size()); } inline int32_t NVCVElementsImpl::numElementsAt(int32_t b) const { - return m_elements_vec[b].size(); + return static_cast(m_elements_vec[b].size()); } inline std::shared_ptr NVCVElementsImpl::elementAt(int32_t b, int32_t i) const diff --git a/src/cvcuda/priv/WorkspaceAllocator.hpp b/src/cvcuda/priv/WorkspaceAllocator.hpp index bb5575fd4..57abda4d9 100644 --- a/src/cvcuda/priv/WorkspaceAllocator.hpp +++ b/src/cvcuda/priv/WorkspaceAllocator.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,10 +20,19 @@ #include +#include +#include #include +#include namespace cvcuda { +class WorkspaceAllocatorError : public std::logic_error +{ +public: + using std::logic_error::logic_error; +}; + class WorkspaceMemAllocator { public: @@ -43,7 +52,8 @@ class WorkspaceMemAllocator * @param mem Workspace memory * @param acquireReleaseStream A stream on which the data will be used (or nullopt to denote host usage) */ - WorkspaceMemAllocator(const WorkspaceMem &mem, std::optional acquireReleaseStream = std::nullopt) + explicit WorkspaceMemAllocator(const WorkspaceMem &mem, + std::optional acquireReleaseStream = std::nullopt) : WorkspaceMemAllocator(mem, acquireReleaseStream, acquireReleaseStream) { } @@ -70,10 +80,23 @@ class WorkspaceMemAllocator { } - ~WorkspaceMemAllocator() + ~WorkspaceMemAllocator() noexcept { if (!m_released) - release(m_releaseStream); + { + try + { + release(m_releaseStream); + } + catch (const std::exception &e) + { + std::fprintf(stderr, "WorkspaceMemAllocator release failed during destruction: %s\n", e.what()); + } + catch (...) + { + std::fprintf(stderr, "WorkspaceMemAllocator release failed during destruction\n"); + } + } } /** @@ -92,7 +115,7 @@ class WorkspaceMemAllocator assert(alignment >= alignof(T)); if (m_released) - throw std::logic_error("This workspace memory has been released."); + throw WorkspaceAllocatorError("This workspace memory has been released."); if (!m_acquired && count > 0) acquire(m_acquireStream); @@ -105,7 +128,7 @@ class WorkspaceMemAllocator } size_t offset = nvcv::detail::AlignUp(m_offset, alignment); - T *ret = reinterpret_cast(static_cast(m_mem.data) + offset); + auto *ret = reinterpret_cast(static_cast(m_mem.data) + offset); size_t real_size = nvcv::detail::AlignUp(count * sizeof(T), alignment); offset += real_size; if (offset > m_mem.req.size) @@ -130,14 +153,14 @@ class WorkspaceMemAllocator void acquire(std::optional stream) { if (m_acquired) - throw std::logic_error("Acquire called multiple times"); + throw WorkspaceAllocatorError("Acquire called multiple times"); if (m_released) - throw std::logic_error("This workspace memory has been released."); + throw WorkspaceAllocatorError("This workspace memory has been released."); if (m_mem.ready) { - if (stream) + if (stream.has_value()) { if (cudaStreamWaitEvent(*stream, m_mem.ready) != cudaSuccess) throw nvcv::Exception(nvcv::Status::ERROR_INTERNAL, "cudaStreamWairEvent failed"); @@ -157,15 +180,14 @@ class WorkspaceMemAllocator void release(std::optional stream) { if (m_released) - throw std::logic_error("Release called multiple times"); + throw WorkspaceAllocatorError("Release called multiple times"); if (m_mem.ready && m_offset) { assert(m_acquired); - if (stream) - if (cudaEventRecord(m_mem.ready, *stream) != cudaSuccess) - throw nvcv::Exception(nvcv::Status::ERROR_INTERNAL, "cudaEventRecord failed"); + if (stream.has_value() && cudaEventRecord(m_mem.ready, *stream) != cudaSuccess) + throw nvcv::Exception(nvcv::Status::ERROR_INTERNAL, "cudaEventRecord failed"); } m_released = true; } @@ -173,9 +195,11 @@ class WorkspaceMemAllocator private: WorkspaceMem m_mem; size_t m_offset = 0; - bool m_acquired = false, m_released = false; + bool m_acquired = false; + bool m_released = false; - std::optional m_acquireStream, m_releaseStream; + std::optional m_acquireStream; + std::optional m_releaseStream; }; struct WorkspaceAllocator diff --git a/src/cvcuda/priv/legacy/AdaptiveThresholdPolicy.hpp b/src/cvcuda/priv/legacy/AdaptiveThresholdPolicy.hpp new file mode 100644 index 000000000..1d4ba0b9d --- /dev/null +++ b/src/cvcuda/priv/legacy/AdaptiveThresholdPolicy.hpp @@ -0,0 +1,39 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#ifndef CVCUDA_PRIV_LEGACY_ADAPTIVE_THRESHOLD_POLICY_HPP +#define CVCUDA_PRIV_LEGACY_ADAPTIVE_THRESHOLD_POLICY_HPP + +namespace nvcv::legacy::cuda_op { + +enum class AdaptiveThresholdKernelPolicy +{ + kLegacyX4, + kCoefficientReuseX8 +}; + +constexpr AdaptiveThresholdKernelPolicy AdaptiveThresholdKernelPolicyForSM(int sm) +{ + return sm == 75 ? AdaptiveThresholdKernelPolicy::kLegacyX4 : AdaptiveThresholdKernelPolicy::kCoefficientReuseX8; +} + +static_assert(AdaptiveThresholdKernelPolicyForSM(75) == AdaptiveThresholdKernelPolicy::kLegacyX4); +static_assert(AdaptiveThresholdKernelPolicyForSM(80) == AdaptiveThresholdKernelPolicy::kCoefficientReuseX8); + +} // namespace nvcv::legacy::cuda_op + +#endif // CVCUDA_PRIV_LEGACY_ADAPTIVE_THRESHOLD_POLICY_HPP diff --git a/src/cvcuda/priv/legacy/CMakeLists.txt b/src/cvcuda/priv/legacy/CMakeLists.txt index 4a265c8ea..876299591 100644 --- a/src/cvcuda/priv/legacy/CMakeLists.txt +++ b/src/cvcuda/priv/legacy/CMakeLists.txt @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -57,6 +57,7 @@ set(CV_CUDA_PRIV_LEGACY_OP_FILES normalize.cu pad_and_stack.cu normalize_var_shape.cu + gamma_contrast.cu gamma_contrast_var_shape.cu pillow_resize.cu pillow_resize_var_shape.cu @@ -104,6 +105,42 @@ else() list(APPEND CV_CUDA_PRIV_LEGACY_FILES ${CV_CUDA_PRIV_LEGACY_OP_FILES}) endif() +cvcuda_add_targeted_cuda_architectures_to_sources( + ARCHITECTURES 86 89 + SOURCES + bilateral_filter.cu + bilateral_filter_var_shape.cu + filter.cu + filter_var_shape.cu + joint_bilateral_filter.cu + pillow_resize.cu + pillow_resize_var_shape.cu + random_resized_crop.cu + random_resized_crop_var_shape.cu + reformat.cu + resize_var_shape.cu + rotate.cu + rotate_var_shape.cu + warp.cu + warp_var_shape.cu + SELECTED_SOURCES_VAR CV_CUDA_PRIV_LEGACY_FILES +) + +cvcuda_add_targeted_cuda_architectures_to_sources( + ARCHITECTURES 86 + SOURCES + gamma_contrast.cu + gamma_contrast_var_shape.cu + SELECTED_SOURCES_VAR CV_CUDA_PRIV_LEGACY_FILES +) + +cvcuda_add_targeted_cuda_architectures_to_sources( + ARCHITECTURES 89 + SOURCES + flip.cu + SELECTED_SOURCES_VAR CV_CUDA_PRIV_LEGACY_FILES +) + add_library(cvcuda_legacy STATIC ${CV_CUDA_PRIV_LEGACY_FILES} ) @@ -116,3 +153,5 @@ target_link_libraries(cvcuda_legacy cvcuda_headers -lrt ) + +target_link_libraries(cvcuda_legacy PRIVATE cvcuda_nvtx_config) diff --git a/src/cvcuda/priv/legacy/CopyMakeBorderPolicy.hpp b/src/cvcuda/priv/legacy/CopyMakeBorderPolicy.hpp new file mode 100644 index 000000000..ecc3fcef4 --- /dev/null +++ b/src/cvcuda/priv/legacy/CopyMakeBorderPolicy.hpp @@ -0,0 +1,33 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#ifndef CVCUDA_PRIV_LEGACY_COPY_MAKE_BORDER_POLICY_HPP +#define CVCUDA_PRIV_LEGACY_COPY_MAKE_BORDER_POLICY_HPP + +namespace nvcv::legacy::cuda_op { + +constexpr bool UsePackedRGB8Reflect101ForSM(int sm) +{ + return sm != 120; +} + +static_assert(!UsePackedRGB8Reflect101ForSM(120)); +static_assert(UsePackedRGB8Reflect101ForSM(119)); + +} // namespace nvcv::legacy::cuda_op + +#endif // CVCUDA_PRIV_LEGACY_COPY_MAKE_BORDER_POLICY_HPP diff --git a/src/cvcuda/priv/legacy/CvCudaLegacy.h b/src/cvcuda/priv/legacy/CvCudaLegacy.h index fd9ad2515..41dc2dd3d 100644 --- a/src/cvcuda/priv/legacy/CvCudaLegacy.h +++ b/src/cvcuda/priv/legacy/CvCudaLegacy.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,6 +18,7 @@ #ifndef CV_CUDA_LEGACY_H #define CV_CUDA_LEGACY_H +#include "AdaptiveThresholdPolicy.hpp" #include "CvCudaOSD.hpp" #include @@ -28,9 +29,11 @@ #include #include #include +#include #include #include +#include #include #include @@ -76,8 +79,7 @@ enum DataType struct DataShape { DataShape() - : N(1) - , C(0) + : C(0) , H(0) , W(0){}; DataShape(int n, int c, int h, int w) @@ -86,17 +88,16 @@ struct DataShape , H(h) , W(w){}; DataShape(int c, int h, int w) - : N(1) - , C(c) + : C(c) , H(h) , W(w){}; - bool operator==(const DataShape &s) + bool operator==(const DataShape &s) const { return s.N == N && s.H == H && s.W == W && s.C == C; } - bool operator!=(const DataShape &s) + bool operator!=(const DataShape &s) const { return !(*this == s); } @@ -144,19 +145,21 @@ struct WarpAffineTransform { static __device__ __forceinline__ float2 calcCoord(const float *c_warpMat, int x, int y) { - const float xcoo = c_warpMat[0] * x + c_warpMat[1] * y + c_warpMat[2]; - const float ycoo = c_warpMat[3] * x + c_warpMat[4] * y + c_warpMat[5]; + const auto fx = static_cast(x); + const auto fy = static_cast(y); + const float xcoo = c_warpMat[0] * fx + c_warpMat[1] * fy + c_warpMat[2]; + const float ycoo = c_warpMat[3] * fx + c_warpMat[4] * fy + c_warpMat[5]; return make_float2(xcoo, ycoo); } // declare a 3x3 matrix/array to avoid conflicts in shared GPU kernel with warpPerspective - float xform[9]; + float xform[9]; // NOSONAR: CUDA kernels consume this fixed-size transform storage. }; struct PerspectiveTransform { - PerspectiveTransform(const float *transMatrix) + explicit PerspectiveTransform(const float *transMatrix) { xform[0] = transMatrix[0]; xform[1] = transMatrix[1]; @@ -171,22 +174,24 @@ struct PerspectiveTransform static __device__ __forceinline__ float2 calcCoord(const float *c_warpMat, int x, int y) { - const float coeff = 1.0f / (c_warpMat[6] * x + c_warpMat[7] * y + c_warpMat[8]); + const auto fx = static_cast(x); + const auto fy = static_cast(y); + const float coeff = 1.0f / (c_warpMat[6] * fx + c_warpMat[7] * fy + c_warpMat[8]); - const float xcoo = coeff * (c_warpMat[0] * x + c_warpMat[1] * y + c_warpMat[2]); - const float ycoo = coeff * (c_warpMat[3] * x + c_warpMat[4] * y + c_warpMat[5]); + const float xcoo = coeff * (c_warpMat[0] * fx + c_warpMat[1] * fy + c_warpMat[2]); + const float ycoo = coeff * (c_warpMat[3] * fx + c_warpMat[4] * fy + c_warpMat[5]); return make_float2(xcoo, ycoo); } - float xform[9]; + float xform[9]; // NOSONAR: CUDA kernels consume this fixed-size transform storage. }; // cuda base operator class class CudaBaseOp { public: - CudaBaseOp(){}; + CudaBaseOp() = default; CudaBaseOp(DataShape max_input_shape, DataShape max_output_shape) : max_input_shape_(max_input_shape) @@ -194,18 +199,20 @@ class CudaBaseOp { } + virtual ~CudaBaseOp() = default; + /** * @brief calculate the cpu/gpu buffer size needed by this operator * @param max_input_shape maximum input DataShape that may be used * @param max_output_shape maximum output DataShape that may be used * @param max_data_type DataType with the maximum size that may be used */ - size_t calBufferSize(DataShape max_input_shape, DataShape max_output_shape, DataType max_data_type) + virtual size_t calBufferSize(DataShape max_input_shape, DataShape max_output_shape, DataType max_data_type) { return 0; }; - bool checkDataShapeValid(DataShape input_shape, DataShape output_shape) + bool checkDataShapeValid(DataShape input_shape, DataShape output_shape) const { int input_size = input_shape.N * input_shape.C * input_shape.H * input_shape.W; int max_input_size = max_input_shape_.N * max_input_shape_.C * max_input_shape_.H * max_input_shape_.W; @@ -214,7 +221,7 @@ class CudaBaseOp return (input_size <= max_input_size) && (output_size <= max_output_size); } -protected: +private: DataShape max_input_shape_; DataShape max_output_shape_; }; @@ -299,7 +306,7 @@ class ConvertTo : public CudaBaseOp * */ ErrorCode infer(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, const double alpha, - const double beta, cudaStream_t stream); + const double beta, NVCVRoundMode roundMode, cudaStream_t stream); }; class CustomCrop : public CudaBaseOp @@ -381,7 +388,7 @@ class MinAreaRect : public CudaBaseOp MinAreaRect() = delete; MinAreaRect(DataShape max_input_shape, DataShape max_output_shape, int maxContourNum); - ~MinAreaRect(); + ~MinAreaRect() override; /** * @brief Creating Bounding rotated boxes and ellipses for contours @@ -444,9 +451,9 @@ class MinAreaRect : public CudaBaseOp const TensorDataStridedCuda &numPointsInContour, const int totalContours, cudaStream_t stream); private: - int mMaxContourNum; - void *mRotateCoeffsBufDev = nullptr; - void *mRotatedPointsDev = nullptr; + int mMaxContourNum; + float *mRotateCoeffsBufDev = nullptr; + int *mRotatedPointsDev = nullptr; }; class Flip : public CudaBaseOp @@ -655,8 +662,8 @@ class Reformat : public CudaBaseOp class Morphology : public CudaBaseOp { public: - Morphology() = default; - virtual ~Morphology() = default; + Morphology() = default; + ~Morphology() override = default; /** * @brief Dilates/Erodes an image @@ -722,7 +729,7 @@ class MorphologyVarShape : public CudaBaseOp public: MorphologyVarShape() = default; - virtual ~MorphologyVarShape() = default; + ~MorphologyVarShape() override = default; /** * @brief Dilates/Erodes an image * @@ -779,7 +786,8 @@ class MorphologyVarShape : public CudaBaseOp */ ErrorCode infer(const nvcv::ImageBatchVarShape &inBatch, const nvcv::ImageBatchVarShape &outBatch, NVCVMorphologyType morph_type, const TensorDataStridedCuda &masks, - const TensorDataStridedCuda &anchors, bool noop, NVCVBorderType borderMode, cudaStream_t stream); + const TensorDataStridedCuda &anchors, bool noop, NVCVBorderType borderMode, + bool enableGenericInterior, cudaStream_t stream); }; class Normalize : public CudaBaseOp @@ -877,7 +885,30 @@ class Normalize : public CudaBaseOp const float global_scale, const float shift, const float epsilon, const uint32_t flags, cudaStream_t stream); - void checkParamShape(DataShape input_shape, DataShape param_shape); + /** + * @brief Tensor-free overload of the normalize operation: base and scale are supplied by value + * (packed into float4 lanes) instead of as parameter tensors, so no per-channel parameter tensor + * needs to be allocated or uploaded. The normalization math and flags semantics match the tensor + * overload above and results are bit-identical for the same values. Interleaved (kNHWC / kHWC) and + * planar (kNCHW / kCHW) layouts are supported; per-axis spatial parameters are not supported on this + * path. + * + * @param base base values passed by value (up to four channels in a float4). + * @param scale scale values passed by value (up to four channels in a float4). + * @param baseCount number of meaningful base lanes: 1 (broadcast to all channels) or the channel count. + * @param scaleCount number of meaningful scale lanes: 1 (broadcast to all channels) or the channel count. + * @param global_scale additional scaling factor, used e.g. when output is of integral type. + * @param shift additional bias value, used e.g. when output is of unsigned type. + * @param epsilon regularizing term added to variance; only used if scale_is_stddev = true + * @param flags if true, scale is interpreted as standard deviation and it's regularized and its + * reciprocal is used when scaling. + * @param stream for the asynchronous execution. + */ + ErrorCode infer(const TensorDataStridedCuda &inData, const float4 base, const float4 scale, const int baseCount, + const int scaleCount, const TensorDataStridedCuda &outData, const float global_scale, + const float shift, const float epsilon, const uint32_t flags, cudaStream_t stream); + + bool checkParamShape(DataShape input_shape, DataShape param_shape); }; class PadAndStack : public CudaBaseOp @@ -951,7 +982,7 @@ class Rotate : public CudaBaseOp Rotate() = delete; Rotate(DataShape max_input_shape, DataShape max_output_shape); - ~Rotate(); + ~Rotate() override; /** * @brief Rotates input images around the origin (0,0) and then shifts it. @@ -977,10 +1008,10 @@ class Rotate : public CudaBaseOp * @param max_output_shape maximum output DataShape that may be used * @param max_data_type DataType with the maximum size that may be used */ - size_t calBufferSize(DataShape max_input_shape, DataShape max_output_shape, DataType max_data_type); + size_t calBufferSize(DataShape max_input_shape, DataShape max_output_shape, DataType max_data_type) override; -protected: - double *d_aCoeffs; +private: + float *d_aCoeffs; }; class MedianBlur : public CudaBaseOp @@ -1074,6 +1105,18 @@ class NormalizeVarShape : public CudaBaseOp const float epsilon, const uint32_t flags, cudaStream_t stream); }; +// Host-verified uniform batch scale for the var-shape resize fast paths: per-image sizes are only +// host-accessible through the batch handles, so the caller classifies the batch once and the +// dispatch picks the matching specialized kernel. kGeneric covers mixed batches and every case +// without a fast path. +enum class ResizeVarShapeScale +{ + kGeneric, + kExpand2x, // every image pair is exactly 2x up on both axes + kContract2x, // every image pair is exactly 2x down on both axes + kFractionalZoomOut, // every image pair zooms out with a non-integer ratio on both axes +}; + class ResizeVarShape : public CudaBaseOp { public: @@ -1113,7 +1156,8 @@ class ResizeVarShape : public CudaBaseOp * */ ErrorCode infer(const ImageBatchVarShapeDataStridedCuda &inData, const ImageBatchVarShapeDataStridedCuda &outData, - const NVCVInterpolationType interpolation, cudaStream_t stream); + const NVCVInterpolationType interpolation, cudaStream_t stream, + ResizeVarShapeScale batchScale = ResizeVarShapeScale::kGeneric); }; class CopyMakeBorder : public CudaBaseOp @@ -1268,9 +1312,9 @@ class RotateVarShape : public CudaBaseOp public: RotateVarShape() = delete; - RotateVarShape(const int maxVarShapeBatchSize); + explicit RotateVarShape(const int maxVarShapeBatchSize); - ~RotateVarShape(); + ~RotateVarShape() override; /** * @brief Rotates input images around the origin (0,0) and then shifts it. @@ -1295,8 +1339,8 @@ class RotateVarShape : public CudaBaseOp const TensorDataStridedCuda &angleDeg, const TensorDataStridedCuda &shift, const NVCVInterpolationType interpolation, cudaStream_t stream); -protected: - double *d_aCoeffs; +private: + float *d_aCoeffs; const int m_maxBatchSize; }; @@ -1373,7 +1417,7 @@ class Gaussian : public CudaBaseOp Gaussian(DataShape max_input_shape, DataShape max_output_shape, Size2D maxKernelSize); - ~Gaussian(); + ~Gaussian() override; /** * Limitations: @@ -1444,9 +1488,9 @@ class Erase : public CudaBaseOp public: Erase() = delete; - Erase(DataShape max_input_shape, DataShape max_output_shape, int num_erasing_area); + Erase(DataShape max_input_shape, DataShape max_output_shape, int num_erasing_area, bool useBulkCopy); - ~Erase(); + ~Erase() override; /** * @brief erase areas of images. Different images in the same batch can be erased differently. @@ -1470,11 +1514,12 @@ class Erase : public CudaBaseOp const TensorDataStridedCuda &values, const TensorDataStridedCuda &imgIdx, bool random, unsigned int seed, bool inplace, cudaStream_t stream); -protected: - int3 *d_max_values; - void *temp_storage; - size_t storage_bytes; - int max_num_erasing_area; +private: + int3 *d_max_values; + std::byte *temp_storage; + size_t storage_bytes; + int max_num_erasing_area; + bool m_useBulkCopy; }; class AverageBlur : public CudaBaseOp @@ -1484,7 +1529,7 @@ class AverageBlur : public CudaBaseOp AverageBlur(DataShape max_input_shape, DataShape max_output_shape, Size2D maxKernelSize); - ~AverageBlur(); + ~AverageBlur() override; /** * Limitations: @@ -1542,8 +1587,6 @@ class AverageBlur : public CudaBaseOp private: Size2D m_maxKernelSize = {0, 0}; - Size2D m_curKernelSize = {0, 0}; - float *m_kernel = nullptr; }; class Conv2DVarShape : public CudaBaseOp @@ -1698,7 +1741,7 @@ class GammaContrastVarShape : public CudaBaseOp GammaContrastVarShape(const int32_t maxVarShapeBatchSize, const int32_t maxVarShapeChannelCount); - ~GammaContrastVarShape(); + ~GammaContrastVarShape() override; /** * @brief Adjust image contrast by scaling pixel values to 255*((v/255)**gamma) @@ -1728,14 +1771,40 @@ class GammaContrastVarShape : public CudaBaseOp float *m_gammaArray = nullptr; }; +// Tensor (non-var-shape) GammaContrast. Supports interleaved (kNHWC/kHWC) and planar (kNCHW/kCHW) +// layouts. The gamma tensor is per-sample or per-sample-per-channel, normalized into a dense +// [numSamples*channels] array shared with the var-shape path so results are bit-exact. +class GammaContrast : public CudaBaseOp +{ +public: + GammaContrast() = delete; + + GammaContrast(const int32_t maxBatchSize, const int32_t maxChannelCount); + + ~GammaContrast() override; + + ErrorCode infer(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, + const TensorDataStridedCuda &gammas, cudaStream_t stream); + + // Scalar (host-float) gamma/gain path: out = gain * in**gamma applied with a single gamma/gain for + // all samples/channels. Uses no gamma scratch (the scalars are passed straight into the kernel). + ErrorCode infer(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, float gamma, float gain, + NVCVRoundMode roundMode, cudaStream_t stream); + +private: + int m_maxBatchSize = 0; + int m_maxChannelCount = 0; + float *m_gammaArray = nullptr; +}; + class EraseVarShape : public CudaBaseOp { public: EraseVarShape() = delete; - EraseVarShape(DataShape max_input_shape, DataShape max_output_shape, int num_erasing_area); + EraseVarShape(DataShape max_input_shape, DataShape max_output_shape, int num_erasing_area, bool useBulkCopy); - ~EraseVarShape(); + ~EraseVarShape() override; /** * @brief erase areas of images. Different images in the same batch can be erased differently. @@ -1756,11 +1825,12 @@ class EraseVarShape : public CudaBaseOp const TensorDataStridedCuda &values, const TensorDataStridedCuda &imgIdx, bool random, unsigned int seed, bool inplace, cudaStream_t stream); -protected: - int3 *d_max_values; - void *temp_storage; - size_t storage_bytes; - int max_num_erasing_area; +private: + int3 *d_max_values; + std::byte *temp_storage; + size_t storage_bytes; + int max_num_erasing_area; + bool m_useBulkCopy; }; class GaussianVarShape : public CudaBaseOp @@ -1770,7 +1840,7 @@ class GaussianVarShape : public CudaBaseOp GaussianVarShape(DataShape max_input_shape, DataShape max_output_shape, Size2D maxKernelSize, int maxBatchSize); - ~GaussianVarShape(); + ~GaussianVarShape() override; /** * Limitations: @@ -1842,7 +1912,7 @@ class AverageBlurVarShape : public CudaBaseOp AverageBlurVarShape(DataShape max_input_shape, DataShape max_output_shape, Size2D maxKernelSize, int maxBatchSize); - ~AverageBlurVarShape(); + ~AverageBlurVarShape() override; /** * Limitations: @@ -1906,16 +1976,15 @@ class AverageBlurVarShape : public CudaBaseOp private: Size2D m_maxKernelSize = {0, 0}; int m_maxBatchSize = 0; - float *m_kernel = nullptr; }; class MedianBlurVarShape : public CudaBaseOp { public: MedianBlurVarShape() = delete; - MedianBlurVarShape(const int maxVarShapeBatchSize); + explicit MedianBlurVarShape(const int maxVarShapeBatchSize); - ~MedianBlurVarShape(); + ~MedianBlurVarShape() override; /** * @brief Blur an image using a median kernel. * @param inputs gpu pointer, inputs[i] is input image where i ranges from 0 to batch-1, whose shape is @@ -1935,7 +2004,7 @@ class MedianBlurVarShape : public CudaBaseOp ErrorCode infer(const ImageBatchVarShapeDataStridedCuda &in, const ImageBatchVarShapeDataStridedCuda &out, const TensorDataStridedCuda &ksize, cudaStream_t stream); -protected: +private: const int m_maxBatchSize; std::vector m_kernelSizes; }; @@ -2050,7 +2119,7 @@ class OSD : public CudaBaseOp OSD(DataShape max_input_shape, DataShape max_output_shape); - ~OSD(); + ~OSD() override; /** * @brief Draw OSD elements onto input tensor, then return back output tensor. @@ -2076,7 +2145,7 @@ class OSD : public CudaBaseOp * @param max_output_shape maximum output DataShape that may be used * @param max_data_type DataType with the maximum size that may be used */ - size_t calBufferSize(DataShape max_input_shape, DataShape max_output_shape, DataType max_data_type); + size_t calBufferSize(DataShape max_input_shape, DataShape max_output_shape, DataType max_data_type) override; private: nvcv::cuda::osd::cuOSDContext_t m_context; @@ -2089,7 +2158,7 @@ class BoxBlur : public CudaBaseOp BoxBlur(DataShape max_input_shape, DataShape max_output_shape); - ~BoxBlur(); + ~BoxBlur() override; /** * @brief Converts an image from one color space to another. @@ -2098,7 +2167,7 @@ class BoxBlur : public CudaBaseOp * @param boxes Bounding boxes to blur, \ref NVCVBlurBoxesI. */ ErrorCode infer(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, NVCVBlurBoxesI bboxes, - cudaStream_t stream); + cudaStream_t stream, bool skipCopy = false); /** * @brief calculate the cpu/gpu buffer size needed by this operator @@ -2106,7 +2175,7 @@ class BoxBlur : public CudaBaseOp * @param max_output_shape maximum output DataShape that may be used * @param max_data_type DataType with the maximum size that may be used */ - size_t calBufferSize(DataShape max_input_shape, DataShape max_output_shape, DataType max_data_type); + size_t calBufferSize(DataShape max_input_shape, DataShape max_output_shape, DataType max_data_type) override; private: nvcv::cuda::osd::cuOSDContext_t m_context; @@ -2198,9 +2267,9 @@ class WarpPerspectiveVarShape : public CudaBaseOp public: WarpPerspectiveVarShape() = delete; - WarpPerspectiveVarShape(const int32_t maxBatchSize); + explicit WarpPerspectiveVarShape(const int32_t maxBatchSize); - ~WarpPerspectiveVarShape(); + ~WarpPerspectiveVarShape() override; /** * @brief Applies a perspective transformation to an image. Same function as nvcv::warpPerspective. @@ -2229,7 +2298,7 @@ class WarpPerspectiveVarShape : public CudaBaseOp const TensorDataStridedCuda &transMatrix, const int32_t flags, const NVCVBorderType borderMode, const float4 borderValue, cudaStream_t stream); -protected: +private: const int m_maxBatchSize; float *m_transformationMatrix = nullptr; }; @@ -2239,9 +2308,9 @@ class WarpAffineVarShape : public CudaBaseOp public: WarpAffineVarShape() = delete; - WarpAffineVarShape(const int32_t maxBatchSize); + explicit WarpAffineVarShape(const int32_t maxBatchSize); - ~WarpAffineVarShape(); + ~WarpAffineVarShape() override; /** * @brief Applies an affine transformation to an image. Same function as nvcv::warpAffine. * @param inputs gpu pointer, inputs[i] is input image where i ranges from 0 to batch-1, whose shape is @@ -2268,7 +2337,7 @@ class WarpAffineVarShape : public CudaBaseOp const TensorDataStridedCuda &transMatrix, const int32_t flags, const NVCVBorderType borderMode, const float4 borderValue, cudaStream_t stream); -protected: +private: const int m_maxBatchSize; float *m_transformationMatrix = nullptr; }; @@ -2441,7 +2510,7 @@ class Threshold : public CudaBaseOp Threshold(DataShape max_input_shape, DataShape max_output_shape, uint32_t type, int maxBatchSize); - ~Threshold(); + ~Threshold() override; /** * @brief Applies a fixed-level threshold to each array element. @@ -2461,6 +2530,7 @@ class Threshold : public CudaBaseOp int *m_histogram; uint32_t m_type; uint32_t m_automatic_thresh; + int m_maxBatchSize; }; class AdaptiveThreshold : public CudaBaseOp @@ -2468,9 +2538,10 @@ class AdaptiveThreshold : public CudaBaseOp public: AdaptiveThreshold() = delete; - AdaptiveThreshold(DataShape maxInputShape, DataShape maxOutputShape, int32_t maxBlockSize); + AdaptiveThreshold(DataShape maxInputShape, DataShape maxOutputShape, int32_t maxBlockSize, + AdaptiveThresholdKernelPolicy kernelPolicy); - ~AdaptiveThreshold(); + ~AdaptiveThreshold() override; /** * @brief Applies an adaptive threshold to input images. @@ -2488,9 +2559,11 @@ class AdaptiveThreshold : public CudaBaseOp const int32_t blockSize, const double c, cudaStream_t stream); private: - int m_blockSize = -1; - int m_adaptiveMethod = -1; - void *m_kernel = nullptr; + const int m_maxBlockSize; + const AdaptiveThresholdKernelPolicy m_kernelPolicy; + int m_blockSize = -1; + int m_adaptiveMethod = -1; + float *m_kernel = nullptr; }; class AdaptiveThresholdVarShape : public CudaBaseOp @@ -2499,9 +2572,9 @@ class AdaptiveThresholdVarShape : public CudaBaseOp AdaptiveThresholdVarShape() = delete; AdaptiveThresholdVarShape(DataShape maxInputShape, DataShape maxOutputShape, int32_t maxBlockSize, - int32_t maxVarShapeBatchSize); + int32_t maxVarShapeBatchSize, AdaptiveThresholdKernelPolicy kernelPolicy); - ~AdaptiveThresholdVarShape(); + ~AdaptiveThresholdVarShape() override; /** * @brief Applies an adaptive threshold to input images. @@ -2522,9 +2595,10 @@ class AdaptiveThresholdVarShape : public CudaBaseOp const TensorDataStridedCuda &c, cudaStream_t stream); private: - const int m_maxBatchSize; - const int m_maxBlockSize; - void *m_kernel = nullptr; + const int m_maxBatchSize; + const int m_maxBlockSize; + const AdaptiveThresholdKernelPolicy m_kernelPolicy; + float *m_kernel = nullptr; }; class ThresholdVarShape : public CudaBaseOp @@ -2534,7 +2608,7 @@ class ThresholdVarShape : public CudaBaseOp ThresholdVarShape(DataShape max_input_shape, DataShape max_output_shape, uint32_t type, int maxBatchSize); - ~ThresholdVarShape(); + ~ThresholdVarShape() override; /** * @brief Applies a fixed-level threshold to each array element. @@ -2554,18 +2628,21 @@ class ThresholdVarShape : public CudaBaseOp int *m_histogram; uint32_t m_type; uint32_t m_automatic_thresh; + int m_maxBatchSize; }; class RandomResizedCrop : public CudaBaseOp { public: + using CudaBaseOp::calBufferSize; + RandomResizedCrop() = delete; RandomResizedCrop(DataShape max_input_shape, DataShape max_output_shape, const double min_scale, const double max_scale, const double min_ratio, const double max_ratio, int32_t maxBatchSize, uint32_t seed); - ~RandomResizedCrop(); + ~RandomResizedCrop() override; /** * @brief Resize and crop images @@ -2585,23 +2662,39 @@ class RandomResizedCrop : public CudaBaseOp size_t calBufferSize(int batch_size); protected: + struct CropParamBuffers + { + float *scaleY; + float *scaleX; + int *tops; + int *lefts; + }; + + int32_t maxBatchSize() const noexcept; + CropParamBuffers hostCropParams(int batch) noexcept; + CropParamBuffers deviceCropParams(int batch) noexcept; + std::byte *hostCropParamStorage() noexcept; + std::byte *deviceCropParamStorage() noexcept; + void getCropParams(int input_rows, int input_cols, int *top_indices, int *left_indices, int *crop_rows, int *crop_cols); -protected: +private: double min_scale_; double max_scale_; double min_ratio_; double max_ratio_; std::mt19937 generator_; int32_t m_maxBatchSize; - void *m_cpuCropParams = nullptr; - void *m_gpuCropParams = nullptr; + std::byte *m_cpuCropParams = nullptr; + std::byte *m_gpuCropParams = nullptr; }; class RandomResizedCropVarShape : public RandomResizedCrop { public: + using RandomResizedCrop::infer; + RandomResizedCropVarShape() = delete; RandomResizedCropVarShape(DataShape max_input_shape, DataShape max_output_shape, const double min_scale, @@ -2628,7 +2721,7 @@ class GaussianNoise : public CudaBaseOp GaussianNoise(DataShape max_input_shape, DataShape max_output_shape, int maxBatchSize); - ~GaussianNoise(); + ~GaussianNoise() override; /** * @brief Add gaussian noise on images. @@ -2644,8 +2737,12 @@ class GaussianNoise : public CudaBaseOp const TensorDataStridedCuda &mu, const TensorDataStridedCuda &sigma, bool per_channel, unsigned long long seed, cudaStream_t stream); + ErrorCode infer(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, float mu, float sigma, + bool per_channel, unsigned long long seed, bool reseed, bool clip, cudaStream_t stream); + private: curandState *m_states; + curandState *m_nextStates; unsigned long long m_seed; bool m_setupDone = false; int m_maxBatchSize; @@ -2658,7 +2755,7 @@ class GaussianNoiseVarShape : public CudaBaseOp GaussianNoiseVarShape(DataShape max_input_shape, DataShape max_output_shape, int maxBatchSize); - ~GaussianNoiseVarShape(); + ~GaussianNoiseVarShape() override; /** * @brief Add gaussian noise on images. @@ -2676,6 +2773,7 @@ class GaussianNoiseVarShape : public CudaBaseOp private: curandState *m_states; + curandState *m_nextStates; unsigned long long m_seed; bool m_setupDone = false; int m_maxBatchSize; @@ -2703,7 +2801,7 @@ class Inpaint : public CudaBaseOp Inpaint(DataShape max_input_shape, DataShape max_output_shape, int maxBatchSize, Size2D maxShape); - ~Inpaint(); + ~Inpaint() override; /** * @brief Restores the selected region in an image using the region neighborhood. TELEA algorithm is used here. @@ -2720,7 +2818,7 @@ class Inpaint : public CudaBaseOp bool m_init_dilate = false; // whether kernel is initialized int m_maxBatchSize; uint8_t *m_kernel_ptr; - void *m_workspace; + uint8_t *m_workspace; }; class InpaintVarShape : public CudaBaseOp @@ -2730,7 +2828,7 @@ class InpaintVarShape : public CudaBaseOp InpaintVarShape(DataShape max_input_shape, DataShape max_output_shape, int maxBatchSize, Size2D maxShape); - ~InpaintVarShape(); + ~InpaintVarShape() override; /** * @brief Restores the selected region in an image using the region neighborhood. TELEA algorithm is used here. @@ -2747,7 +2845,7 @@ class InpaintVarShape : public CudaBaseOp bool m_init_dilate = false; // whether kernel is initialized int m_maxBatchSize; uint8_t *m_kernel_ptr; - void *m_workspace; + uint8_t *m_workspace; }; class HistogramEq : public CudaBaseOp @@ -2755,9 +2853,9 @@ class HistogramEq : public CudaBaseOp public: HistogramEq() = delete; - HistogramEq(int maxBatchSize); + explicit HistogramEq(int maxBatchSize); - ~HistogramEq(); + ~HistogramEq() override; ErrorCode infer(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, cudaStream_t stream); @@ -2773,9 +2871,9 @@ class HistogramEqVarShape : public CudaBaseOp public: HistogramEqVarShape() = delete; - HistogramEqVarShape(int maxBatchSize); + explicit HistogramEqVarShape(int maxBatchSize); - ~HistogramEqVarShape(); + ~HistogramEqVarShape() override; ErrorCode infer(const ImageBatchVarShapeDataStridedCuda &inData, const ImageBatchVarShapeDataStridedCuda &outData, cudaStream_t stream); diff --git a/src/cvcuda/priv/legacy/CvCudaLegacyHelpers.cpp b/src/cvcuda/priv/legacy/CvCudaLegacyHelpers.cpp index edcf1f0c6..080512986 100644 --- a/src/cvcuda/priv/legacy/CvCudaLegacyHelpers.cpp +++ b/src/cvcuda/priv/legacy/CvCudaLegacyHelpers.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -115,7 +115,7 @@ cuda_op::DataType GetLegacyDataType(DataType dtype) } } - return GetLegacyDataType(bpc[0], (nvcv::DataKind)dtype.dataKind()); + return GetLegacyDataType(bpc[0], dtype.dataKind()); } cuda_op::DataType GetLegacyDataType(ImageFormat fmt) @@ -133,7 +133,7 @@ cuda_op::DataType GetLegacyDataType(ImageFormat fmt) cuda_op::DataShape GetLegacyDataShape(const TensorShapeInfoImage &shapeInfo) { - return cuda_op::DataShape(shapeInfo.numSamples(), shapeInfo.numChannels(), shapeInfo.numRows(), + return cuda_op::DataShape(static_cast(shapeInfo.numSamples()), shapeInfo.numChannels(), shapeInfo.numRows(), shapeInfo.numCols()); } @@ -298,7 +298,8 @@ NVCVStatus TranslateError(legacy::cuda_op::ErrorCode err) const char *ToString(legacy::cuda_op::ErrorCode err, const char **perrdescr) { - const char *errorName = "UNKNOWN", *errorDescr = "Unknown error"; + const char *errorName; + const char *errorDescr; using legacy::cuda_op::ErrorCode; @@ -326,6 +327,10 @@ const char *ToString(legacy::cuda_op::ErrorCode err, const char **perrdescr) errorName = "INVALID_DATA_TYPE"; errorDescr = "Data type is outside its acceptable range"; break; + default: + errorName = "UNKNOWN"; + errorDescr = "Unknown error"; + break; } if (perrdescr != nullptr) diff --git a/src/cvcuda/priv/legacy/CvCudaOSD.hpp b/src/cvcuda/priv/legacy/CvCudaOSD.hpp index 4bbb170ad..63eadf823 100644 --- a/src/cvcuda/priv/legacy/CvCudaOSD.hpp +++ b/src/cvcuda/priv/legacy/CvCudaOSD.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -29,11 +29,11 @@ #include #include -namespace nvcv::cuda { namespace osd { +namespace nvcv::cuda::osd { -#define PREALLOC_CMD_NUM 100 +inline constexpr int PREALLOC_CMD_NUM = 100; -#define checkRuntime(call) check_runtime(call, #call, __LINE__, __FILE__) +#define checkRuntime(call) check_runtime(call, #call, __LINE__, __FILE__) // NOSONAR: std::source_location is C++20. inline static bool check_runtime(cudaError_t e, const char *call, int line, const char *file) { @@ -160,16 +160,21 @@ enum class CommandType : int struct TextLocation { - int image_x, image_y; + int image_x; + int image_y; int text_x; - int text_w, text_h; + int text_w; + int text_h; }; // cuOSDContextCommand includes basic attributes for color and bounding box coordinate struct cuOSDContextCommand { CommandType type = CommandType::None; - unsigned char c0, c1, c2, c3; + unsigned char c0; + unsigned char c1; + unsigned char c2; + unsigned char c3; int bounding_left = 0; int bounding_top = 0; int bounding_right = 0; @@ -183,17 +188,20 @@ struct cuOSDContextCommand // thickness: border width in case > 0, -1 stands for fill mode struct CircleCommand : cuOSDContextCommand { - int cx, cy, radius, thickness; + int cx; + int cy; + int radius; + int thickness; CircleCommand(int batch_idx, int cx, int cy, int radius, int thickness, unsigned char c0, unsigned char c1, unsigned char c2, unsigned char c3) + : cx(cx) + , cy(cy) + , radius(radius) + , thickness(thickness) { this->batch_index = batch_idx; this->type = CommandType::Circle; - this->cx = cx; - this->cy = cy; - this->radius = radius; - this->thickness = thickness; this->c0 = c0; this->c1 = c1; this->c2 = c2; @@ -213,8 +221,10 @@ struct CircleCommand : cuOSDContextCommand struct SegmentCommand : cuOSDContextCommand { float *dSeg; - int segWidth, segHeight; - float scale_x, scale_y; + int segWidth; + int segHeight; + float scale_x; + float scale_y; float segThreshold; SegmentCommand() @@ -243,8 +253,22 @@ struct RectangleCommand : cuOSDContextCommand { int thickness = -1; bool interpolation = false; - float ax1, ay1, bx1, by1, cx1, cy1, dx1, dy1; - float ax2, ay2, bx2, by2, cx2, cy2, dx2, dy2; + float ax1; + float ay1; + float bx1; + float by1; + float cx1; + float cy1; + float dx1; + float dy1; + float ax2; + float ay2; + float bx2; + float by2; + float cx2; + float cy2; + float dx2; + float dy2; RectangleCommand() { @@ -254,7 +278,10 @@ struct RectangleCommand : cuOSDContextCommand struct BoxBlurCommand { - uint8_t c0, c1, c2, c3; + uint8_t c0; + uint8_t c1; + uint8_t c2; + uint8_t c3; int bounding_left = 0; int bounding_top = 0; int bounding_right = 0; @@ -275,14 +302,14 @@ struct TextCommand : cuOSDContextCommand TextCommand(int text_line_size, int ilocation, unsigned char c0, unsigned char c1, unsigned char c2, unsigned char c3) + : text_line_size(text_line_size) + , ilocation(ilocation) { - this->text_line_size = text_line_size; - this->ilocation = ilocation; - this->type = CommandType::Text; - this->c0 = c0; - this->c1 = c1; - this->c2 = c2; - this->c3 = c3; + this->type = CommandType::Text; + this->c0 = c0; + this->c1 = c1; + this->c2 = c2; + this->c3 = c3; } }; @@ -292,18 +319,19 @@ struct TextHostCommand : cuOSDContextCommand std::vector text; unsigned short font_size; std::string font_name; - int x, y; + int x; + int y; TextHostCommand(int batch_idx, const std::vector &text, unsigned short font_size, const char *font, int x, int y, unsigned char c0, unsigned char c1, unsigned char c2, unsigned char c3) + : text(text) + , font_size(font_size) + , font_name(font) + , x(x) + , y(y) { this->batch_index = batch_idx; - this->text = text; - this->font_size = font_size; - this->font_name = font; - this->x = x; - this->y = y; this->c0 = c0; this->c1 = c1; this->c2 = c2; @@ -334,8 +362,8 @@ struct cuOSDContext int bounding_bottom = 0; }; -typedef cuOSDContext *cuOSDContext_t; +using cuOSDContext_t = cuOSDContext *; -}} // namespace nvcv::cuda::osd +} // namespace nvcv::cuda::osd #endif // CV_CUDA_OSD_HPP diff --git a/src/cvcuda/priv/legacy/CvCudaUtils.cuh b/src/cvcuda/priv/legacy/CvCudaUtils.cuh index bd937d20c..bffac5512 100644 --- a/src/cvcuda/priv/legacy/CvCudaUtils.cuh +++ b/src/cvcuda/priv/legacy/CvCudaUtils.cuh @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -46,12 +46,31 @@ #include #include #include +#include namespace nvcv::legacy::cuda_op { typedef unsigned char uchar; typedef signed char schar; +class LegacyCudaAllocationError : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + +class LegacyImageBatchExportError : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + +class LegacyImageBatchFormatError : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + #define get_batch_idx() (blockIdx.z) #define get_lid() (threadIdx.y * blockDim.x + threadIdx.x) #define get_bid() (blockIdx.x + blockIdx.y * gridDim.x) @@ -472,7 +491,7 @@ struct Ptr2dVarShapeNHWC : batches(data.numImages()) , imgList(data.imageList()) , nch( - [&] + [&data, &nch_] { // If not using number of channels, if (nch_ < 0) @@ -480,7 +499,7 @@ struct Ptr2dVarShapeNHWC // Require that all images have the same format (it'd be better if we had data.uniqueDataType) if (!data.uniqueFormat()) { - throw std::runtime_error("Images in a batch must all have the same format"); + throw LegacyImageBatchFormatError("Images in a batch must all have the same format"); } assert(1 == data.uniqueFormat().numPlanes() && "This class is only for NHWC"); diff --git a/src/cvcuda/priv/legacy/EraseCopyPolicy.hpp b/src/cvcuda/priv/legacy/EraseCopyPolicy.hpp new file mode 100644 index 000000000..957cc2008 --- /dev/null +++ b/src/cvcuda/priv/legacy/EraseCopyPolicy.hpp @@ -0,0 +1,36 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#ifndef CVCUDA_PRIV_LEGACY_ERASE_COPY_POLICY_HPP +#define CVCUDA_PRIV_LEGACY_ERASE_COPY_POLICY_HPP + +#include + +namespace nvcv::legacy::cuda_op { + +constexpr bool UseBulkEraseCopyForDevice(int sm, std::string_view deviceName) +{ + return sm != 89 || deviceName != "NVIDIA L4"; +} + +static_assert(!UseBulkEraseCopyForDevice(89, "NVIDIA L4")); +static_assert(UseBulkEraseCopyForDevice(89, "NVIDIA L40")); +static_assert(UseBulkEraseCopyForDevice(90, "NVIDIA L4")); + +} // namespace nvcv::legacy::cuda_op + +#endif // CVCUDA_PRIV_LEGACY_ERASE_COPY_POLICY_HPP diff --git a/src/cvcuda/priv/legacy/ReformatCopyPolicy.hpp b/src/cvcuda/priv/legacy/ReformatCopyPolicy.hpp new file mode 100644 index 000000000..0293e0ec3 --- /dev/null +++ b/src/cvcuda/priv/legacy/ReformatCopyPolicy.hpp @@ -0,0 +1,50 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#ifndef CVCUDA_PRIV_LEGACY_REFORMAT_COPY_POLICY_HPP +#define CVCUDA_PRIV_LEGACY_REFORMAT_COPY_POLICY_HPP + +#include +#include + +namespace nvcv::legacy::cuda_op::detail { + +inline constexpr std::size_t kSingleSampleMinRowBytes = 256; +inline constexpr std::size_t kMultiSampleMinRowBytes = 288; +inline constexpr std::size_t kLimitedBatchMinPixels = 1600 * 900; +inline constexpr std::size_t kAnyBatchMinPixels = 1792 * 1056; + +constexpr bool ShouldUsePitchedCopy(std::uint32_t numSamples, std::size_t pixelsPerSample, + std::size_t rowBytes) noexcept +{ + if (numSamples == 0) + { + return true; + } + + if (numSamples == 1) + { + return rowBytes >= kSingleSampleMinRowBytes; + } + + return rowBytes >= kMultiSampleMinRowBytes + && (pixelsPerSample >= kAnyBatchMinPixels || (numSamples <= 8 && pixelsPerSample >= kLimitedBatchMinPixels)); +} + +} // namespace nvcv::legacy::cuda_op::detail + +#endif // CVCUDA_PRIV_LEGACY_REFORMAT_COPY_POLICY_HPP diff --git a/src/cvcuda/priv/legacy/adaptive_threshold.cu b/src/cvcuda/priv/legacy/adaptive_threshold.cu index 8756cef3b..27f20e82b 100644 --- a/src/cvcuda/priv/legacy/adaptive_threshold.cu +++ b/src/cvcuda/priv/legacy/adaptive_threshold.cu @@ -1,4 +1,4 @@ -/* Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +/* Copyright (c) 2021-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * * SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES * SPDX-License-Identifier: Apache-2.0 @@ -50,21 +50,22 @@ struct MyLessEqual #define BLOCK_DIM_X 16 #define BLOCK_DIM_Y 16 -#define X_STEPS 4 -template +template __global__ void adaptive_threshold(SrcWrapper src, DstWrapper dst, Size2D dstSize, const uchar maxValue, KernelWrapper kernel, const int blockSize, const int idelta) { - const int batch_idx = get_batch_idx(); - const int r = blockSize >> 1; - // (2 * r + BLOCK_DIM_X * X_STEPS) * (2 * r + BLOCK_DIM_Y) + blockSize * blockSize * sizeof(float) + const int batch_idx = get_batch_idx(); + const int effectiveBlockSize = KSize > 0 ? KSize : blockSize; + const int r = effectiveBlockSize >> 1; + // (2 * r + BLOCK_DIM_X * XSteps) * (2 * r + BLOCK_DIM_Y) + blockSize * blockSize * sizeof(float) extern __shared__ __align__(sizeof(float)) uchar s[]; - const int s_width = 2 * r + BLOCK_DIM_X * X_STEPS; + const int s_width = 2 * r + BLOCK_DIM_X * XSteps; const int s_height = 2 * r + BLOCK_DIM_Y; float *s_k = (float *)(s + s_width * s_height); // for kernel // load image data into shared memory - const int shift_x = blockIdx.x * BLOCK_DIM_X * X_STEPS - r; + const int shift_x = blockIdx.x * BLOCK_DIM_X * XSteps - r; const int shift_y = blockIdx.y * BLOCK_DIM_Y - r; int3 srcCoord{0, 0, batch_idx}; for (int start_y = 0; start_y < s_height; start_y += BLOCK_DIM_Y) @@ -83,7 +84,7 @@ __global__ void adaptive_threshold(SrcWrapper src, DstWrapper dst, Size2D dstSiz } } // load kernel data into shared memory - const int kernel_size = blockSize * blockSize; + const int kernel_size = effectiveBlockSize * effectiveBlockSize; int local_idx = threadIdx.y * BLOCK_DIM_X + threadIdx.x; while (local_idx < kernel_size) { @@ -93,42 +94,117 @@ __global__ void adaptive_threshold(SrcWrapper src, DstWrapper dst, Size2D dstSiz __syncthreads(); // calculate convolution - int out_x = blockIdx.x * BLOCK_DIM_X * X_STEPS + threadIdx.x; + int out_x = blockIdx.x * BLOCK_DIM_X * XSteps + threadIdx.x; int out_y = blockIdx.y * BLOCK_DIM_Y + threadIdx.y; if (out_x >= dstSize.w || out_y >= dstSize.h) return; CMP cmp; -#pragma unroll - for (int k = 0; k < X_STEPS; ++k) + if constexpr (ReuseCoefficients) { - float res = 0.f; + // Accumulate the XSteps outputs together so each shared kernel coefficient is loaded once. + // Each output still visits coefficients in the original row-major order, preserving rounding. + float res[XSteps]{}; int kInd = 0; int start_x = out_x - shift_x - r; int start_y = out_y - shift_y - r; uchar *p = s + start_y * s_width + start_x; - for (int i = 0; i < blockSize; ++i) + if constexpr (KSize > 0) + { +#pragma unroll + for (int i = 0; i < KSize; ++i) + { +#pragma unroll + for (int j = 0; j < KSize; ++j) + { + float coeff = s_k[kInd++]; +#pragma unroll + for (int k = 0; k < XSteps; ++k) + { + res[k] += p[j + k * BLOCK_DIM_X] * coeff; + } + } + p += s_width; + } + } + else + { + for (int i = 0; i < blockSize; ++i) + { + for (int j = 0; j < blockSize; ++j) + { + float coeff = s_k[kInd++]; +#pragma unroll + for (int k = 0; k < XSteps; ++k) + { + res[k] += p[j + k * BLOCK_DIM_X] * coeff; + } + } + p += s_width; + } + } + +#pragma unroll + for (int k = 0; k < XSteps; ++k) + { + uchar t = cmp(s[(out_y - shift_y) * s_width + out_x - shift_x] + idelta, cuda::SaturateCast(res[k])) + ? maxValue + : 0; + *dst.ptr(batch_idx, out_y, out_x) = t; + out_x += BLOCK_DIM_X; + if (out_x >= dstSize.w) + return; + } + } + else + { +#pragma unroll + for (int k = 0; k < XSteps; ++k) { - for (int j = 0; j < blockSize; ++j) + float res = 0.f; + int kInd = 0; + int start_x = out_x - shift_x - r; + int start_y = out_y - shift_y - r; + uchar *p = s + start_y * s_width + start_x; + if constexpr (KSize > 0) { - res += p[j] * s_k[kInd++]; +#pragma unroll + for (int i = 0; i < KSize; ++i) + { +#pragma unroll + for (int j = 0; j < KSize; ++j) + { + res += p[j] * s_k[kInd++]; + } + p += s_width; + } + } + else + { + for (int i = 0; i < blockSize; ++i) + { + for (int j = 0; j < blockSize; ++j) + { + res += p[j] * s_k[kInd++]; + } + // next row in shared memory + p += s_width; + } } - // next row in shared memory - p += s_width; + uchar t = cmp(s[(out_y - shift_y) * s_width + out_x - shift_x] + idelta, cuda::SaturateCast(res)) + ? maxValue + : 0; + *dst.ptr(batch_idx, out_y, out_x) = t; + out_x += BLOCK_DIM_X; + if (out_x >= dstSize.w) + return; } - uchar t = cmp(s[(out_y - shift_y) * s_width + out_x - shift_x] + idelta, cuda::SaturateCast(res)) - ? maxValue - : 0; - *dst.ptr(batch_idx, out_y, out_x) = t; - out_x += BLOCK_DIM_X; - if (out_x >= dstSize.w) - return; } } -template -ErrorCode adaptive_threshold_caller(const TensorDataStridedCuda &in, const TensorDataStridedCuda &out, - const uchar maxValue, KernelWrapper kernel, const int blockSize, const int idelta, - cudaStream_t stream) +template +ErrorCode adaptive_threshold_caller_impl(const TensorDataStridedCuda &in, const TensorDataStridedCuda &out, + const uchar maxValue, KernelWrapper kernel, const int blockSize, + const int idelta, cudaStream_t stream) { auto outAccess = TensorDataAccessStridedImagePlanar::Create(out); NVCV_ASSERT(outAccess); @@ -139,9 +215,9 @@ ErrorCode adaptive_threshold_caller(const TensorDataStridedCuda &in, const Tenso Size2D dstSize{outAccess->numCols(), outAccess->numRows()}; dim3 block(BLOCK_DIM_X, BLOCK_DIM_Y); - dim3 grid(divUp(dstSize.w, BLOCK_DIM_X * X_STEPS), divUp(dstSize.h, block.y), outAccess->numSamples()); + dim3 grid(divUp(dstSize.w, BLOCK_DIM_X * XSteps), divUp(dstSize.h, block.y), outAccess->numSamples()); - int s_mem_size = (blockSize - 1 + BLOCK_DIM_X * X_STEPS) * (blockSize - 1 + BLOCK_DIM_Y) + int s_mem_size = (blockSize - 1 + BLOCK_DIM_X * XSteps) * (blockSize - 1 + BLOCK_DIM_Y) + blockSize * blockSize * sizeof(float); int64_t inMaxStride = inAccess->sampleStride() * inAccess->numSamples(); @@ -151,8 +227,21 @@ ErrorCode adaptive_threshold_caller(const TensorDataStridedCuda &in, const Tenso auto src = cuda::CreateBorderWrapNHW(in, cuda::SetAll(0.f)); auto dst = cuda::CreateTensorWrapNHW(out); - adaptive_threshold - <<>>(src, dst, dstSize, maxValue, kernel, blockSize, idelta); + if (blockSize == 7) + { + adaptive_threshold<7, XSteps, ReuseCoefficients, CMP> + <<>>(src, dst, dstSize, maxValue, kernel, blockSize, idelta); + } + else if (blockSize == 3) + { + adaptive_threshold<3, XSteps, ReuseCoefficients, CMP> + <<>>(src, dst, dstSize, maxValue, kernel, blockSize, idelta); + } + else + { + adaptive_threshold<0, XSteps, ReuseCoefficients, CMP> + <<>>(src, dst, dstSize, maxValue, kernel, blockSize, idelta); + } } else { @@ -168,8 +257,24 @@ ErrorCode adaptive_threshold_caller(const TensorDataStridedCuda &in, const Tenso return ErrorCode::SUCCESS; } -AdaptiveThreshold::AdaptiveThreshold(DataShape maxInputShape, DataShape maxOutputShape, int32_t maxBlockSize) +template +ErrorCode adaptive_threshold_caller(const TensorDataStridedCuda &in, const TensorDataStridedCuda &out, + const uchar maxValue, KernelWrapper kernel, const int blockSize, const int idelta, + AdaptiveThresholdKernelPolicy kernelPolicy, cudaStream_t stream) +{ + if (kernelPolicy == AdaptiveThresholdKernelPolicy::kLegacyX4) + { + return adaptive_threshold_caller_impl<4, false, T, B, CMP>(in, out, maxValue, kernel, blockSize, idelta, + stream); + } + return adaptive_threshold_caller_impl<8, true, T, B, CMP>(in, out, maxValue, kernel, blockSize, idelta, stream); +} + +AdaptiveThreshold::AdaptiveThreshold(DataShape maxInputShape, DataShape maxOutputShape, int32_t maxBlockSize, + AdaptiveThresholdKernelPolicy kernelPolicy) : CudaBaseOp(maxInputShape, maxOutputShape) + , m_maxBlockSize(maxBlockSize) + , m_kernelPolicy(kernelPolicy) { if (maxBlockSize <= 0) { @@ -207,9 +312,10 @@ ErrorCode AdaptiveThreshold::infer(const TensorDataStridedCuda &in, const Tensor return ErrorCode::INVALID_DATA_FORMAT; } - if ((input_format != kNHWC) && (input_format != kHWC)) + const bool isPlanar = input_format == kNCHW || input_format == kCHW; + if ((input_format != kNHWC) && (input_format != kHWC) && !isPlanar) { - LOG_ERROR("Invalid DataFormat both Input and Output must be kHWC or kNHWC"); + LOG_ERROR("Invalid DataFormat both Input and Output must be kHWC, kNHWC, kCHW or kNCHW"); return ErrorCode::INVALID_DATA_FORMAT; } @@ -247,13 +353,13 @@ ErrorCode AdaptiveThreshold::infer(const TensorDataStridedCuda &in, const Tensor return ErrorCode::INVALID_PARAMETER; } - if (!(blockSize % 2 == 1 && blockSize > 1)) + if (!(blockSize % 2 == 1 && blockSize > 1 && blockSize <= m_maxBlockSize)) { LOG_ERROR("Invalid BlockSize " << blockSize); return ErrorCode::INVALID_PARAMETER; } - float *kernelPtr = (float *)m_kernel; + float *kernelPtr = m_kernel; if (m_adaptiveMethod != adaptiveMethod || m_blockSize != blockSize) { if (adaptiveMethod == NVCV_ADAPTIVE_THRESH_MEAN_C) @@ -281,13 +387,13 @@ ErrorCode AdaptiveThreshold::infer(const TensorDataStridedCuda &in, const Tensor int idelta = thresholdType == NVCV_THRESH_BINARY ? (int)std::ceil(c) : (int)std::floor(c); if (thresholdType == NVCV_THRESH_BINARY) { - return adaptive_threshold_caller>(in, out, imaxval, kernelPtr, - blockSize, idelta, stream); + return adaptive_threshold_caller>( + in, out, imaxval, kernelPtr, blockSize, idelta, m_kernelPolicy, stream); } else { - return adaptive_threshold_caller>(in, out, imaxval, kernelPtr, - blockSize, idelta, stream); + return adaptive_threshold_caller>( + in, out, imaxval, kernelPtr, blockSize, idelta, m_kernelPolicy, stream); } } diff --git a/src/cvcuda/priv/legacy/adaptive_threshold_var_shape.cu b/src/cvcuda/priv/legacy/adaptive_threshold_var_shape.cu index a59beb1f7..b3e03c5bb 100644 --- a/src/cvcuda/priv/legacy/adaptive_threshold_var_shape.cu +++ b/src/cvcuda/priv/legacy/adaptive_threshold_var_shape.cu @@ -1,4 +1,4 @@ -/* Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +/* Copyright (c) 2021-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * * SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES * SPDX-License-Identifier: Apache-2.0 @@ -24,6 +24,8 @@ #include "CvCudaUtils.cuh" #include "filter_utils.cuh" +#include + using namespace nvcv; using namespace nvcv::legacy::cuda_op; using namespace nvcv::legacy::helpers; @@ -32,7 +34,6 @@ namespace nvcv::legacy::cuda_op { #define BLOCK_DIM_X 16 #define BLOCK_DIM_Y 16 -#define X_STEPS 4 template struct MyGreater @@ -52,14 +53,45 @@ struct MyLessEqual } }; -template +static ErrorCode validateParameterTensor(const TensorDataStridedCuda &tensor, nvcv::DataType expectedType, + int expectedLength, const char *name) +{ + if (tensor.dtype() != expectedType) + { + LOG_ERROR("Invalid " << name << " DataType " << tensor.dtype()); + return ErrorCode::INVALID_DATA_TYPE; + } + + const int rank = tensor.layout().rank(); + if (rank != 1) + { + LOG_ERROR("Invalid " << name << " Dim " << rank); + return ErrorCode::INVALID_DATA_FORMAT; + } + + if (tensor.shape()[0] != expectedLength) + { + LOG_ERROR("Invalid " << name << " length " << tensor.shape()[0] << ", expected " << expectedLength); + return ErrorCode::INVALID_DATA_SHAPE; + } + + if (tensor.stride(0) != expectedType.strideBytes()) + { + LOG_ERROR("Invalid " << name << " stride " << tensor.stride(0)); + return ErrorCode::INVALID_DATA_FORMAT; + } + + return ErrorCode::SUCCESS; +} + +template __global__ void adaptive_threshold(const SrcWrapper src, DstWrapper dst, cuda::Tensor1DWrap maxValueArr, cuda::Tensor1DWrap blockSizeArr, cuda::Tensor1DWrap cArr, cuda::Tensor3DWrap kernel) { const int batch_idx = get_batch_idx(); - int out_x = blockIdx.x * BLOCK_DIM_X * X_STEPS; + int out_x = blockIdx.x * BLOCK_DIM_X * XSteps; int out_y = blockIdx.y * BLOCK_DIM_Y; int out_height = dst.height(batch_idx), out_width = dst.width(batch_idx); // var shape version, the upper-left corner may be invalid @@ -67,13 +99,13 @@ __global__ void adaptive_threshold(const SrcWrapper src, DstWrapper dst, return; const int kbs = blockSizeArr[batch_idx]; const int r = kbs >> 1; - // (2 * r + BLOCK_DIM_X * X_STEPS) * (2 * r + BLOCK_DIM_Y) + kbs * kbs * sizeof(float) + // (2 * r + BLOCK_DIM_X * XSteps) * (2 * r + BLOCK_DIM_Y) + kbs * kbs * sizeof(float) extern __shared__ __align__(sizeof(float)) uchar s[]; - const int s_width = 2 * r + BLOCK_DIM_X * X_STEPS; + const int s_width = 2 * r + BLOCK_DIM_X * XSteps; const int s_height = 2 * r + BLOCK_DIM_Y; float *s_k = (float *)(s + s_width * s_height); // for kernel // load image data into shared memory - const int shift_x = blockIdx.x * BLOCK_DIM_X * X_STEPS - r; + const int shift_x = blockIdx.x * BLOCK_DIM_X * XSteps - r; const int shift_y = blockIdx.y * BLOCK_DIM_Y - r; int3 srcCoord{0, 0, batch_idx}; for (int start_y = 0; start_y < s_height; start_y += BLOCK_DIM_Y) @@ -111,10 +143,11 @@ __global__ void adaptive_threshold(const SrcWrapper src, DstWrapper dst, CMP cmp; const uchar maxv = cuda::SaturateCast(maxValueArr[batch_idx]); const int delta = cuda::round(cArr[batch_idx]); -#pragma unroll - for (int k = 0; k < X_STEPS; ++k) + if constexpr (ReuseCoefficients) { - float res = 0.f; + // Accumulate the XSteps outputs together so each shared kernel coefficient is loaded once. + // Each output still visits coefficients in the original row-major order, preserving rounding. + float res[XSteps]{}; int kInd = 0; int start_x = out_x - shift_x - r; int start_y = out_y - shift_y - r; @@ -123,28 +156,68 @@ __global__ void adaptive_threshold(const SrcWrapper src, DstWrapper dst, { for (int j = 0; j < kbs; ++j) { - res += p[j] * s_k[kInd++]; + float coeff = s_k[kInd++]; +#pragma unroll + for (int k = 0; k < XSteps; ++k) + { + res[k] += p[j + k * BLOCK_DIM_X] * coeff; + } } - // next row in shared memory p += s_width; } - uchar t - = cmp(s[(out_y - shift_y) * s_width + out_x - shift_x] + delta, cuda::SaturateCast(res)) ? maxv : 0; - *dst.ptr(batch_idx, out_y, out_x) = t; - out_x += BLOCK_DIM_X; - if (out_x >= out_width) - return; + +#pragma unroll + for (int k = 0; k < XSteps; ++k) + { + uchar t = cmp(s[(out_y - shift_y) * s_width + out_x - shift_x] + delta, cuda::SaturateCast(res[k])) + ? maxv + : 0; + *dst.ptr(batch_idx, out_y, out_x) = t; + out_x += BLOCK_DIM_X; + if (out_x >= out_width) + return; + } + } + else + { +#pragma unroll + for (int k = 0; k < XSteps; ++k) + { + float res = 0.f; + int kInd = 0; + int start_x = out_x - shift_x - r; + int start_y = out_y - shift_y - r; + uchar *p = s + start_y * s_width + start_x; + for (int i = 0; i < kbs; ++i) + { + for (int j = 0; j < kbs; ++j) + { + res += p[j] * s_k[kInd++]; + } + // next row in shared memory + p += s_width; + } + uchar t = cmp(s[(out_y - shift_y) * s_width + out_x - shift_x] + delta, cuda::SaturateCast(res)) + ? maxv + : 0; + *dst.ptr(batch_idx, out_y, out_x) = t; + out_x += BLOCK_DIM_X; + if (out_x >= out_width) + return; + } } } -template -void adaptive_threshold_caller(const ImageBatchVarShapeDataStridedCuda &in, - const ImageBatchVarShapeDataStridedCuda &out, - cuda::Tensor1DWrap maxValueArr, - NVCVAdaptiveThresholdType adaptiveMethod, NVCVThresholdType thresholdType, - cuda::Tensor1DWrap blockSizeArr, cuda::Tensor1DWrap cArr, - cuda::Tensor3DWrap kernel, int maxBlockSize, cudaStream_t stream) +template +void adaptive_threshold_caller_impl(const ImageBatchVarShapeDataStridedCuda &in, + const ImageBatchVarShapeDataStridedCuda &out, + cuda::Tensor1DWrap maxValueArr, + NVCVAdaptiveThresholdType adaptiveMethod, NVCVThresholdType thresholdType, + cuda::Tensor1DWrap blockSizeArr, + cuda::Tensor1DWrap cArr, cuda::Tensor3DWrap kernel, + int maxBlockSize, cudaStream_t stream) { + (void)thresholdType; float borderValue = .0f; cuda::BorderVarShapeWrap src(in, cuda::SetAll(borderValue)); cuda::ImageBatchVarShapeWrap dst(out); @@ -153,17 +226,17 @@ void adaptive_threshold_caller(const ImageBatchVarShapeDataStridedCuda &in, int maxHeight = in.maxSize().h; int maxWidth = in.maxSize().w; dim3 grid(divUp(maxWidth, block.x), divUp(maxHeight, block.y), out.numImages()); - int s_mem_size = (maxBlockSize - 1 + BLOCK_DIM_X * X_STEPS) * (maxBlockSize - 1 + BLOCK_DIM_Y) + int s_mem_size = (maxBlockSize - 1 + BLOCK_DIM_X * XSteps) * (maxBlockSize - 1 + BLOCK_DIM_Y) + maxBlockSize * maxBlockSize * sizeof(float); - if (thresholdType == NVCV_THRESH_BINARY) + if constexpr (std::is_same_v>) { - adaptive_threshold + adaptive_threshold <<>>(src, dst, maxValueArr, blockSizeArr, cArr, kernel); } else { - adaptive_threshold + adaptive_threshold <<>>(src, dst, maxValueArr, blockSizeArr, cArr, kernel); } @@ -174,11 +247,34 @@ void adaptive_threshold_caller(const ImageBatchVarShapeDataStridedCuda &in, #endif } +template +void adaptive_threshold_caller(const ImageBatchVarShapeDataStridedCuda &in, + const ImageBatchVarShapeDataStridedCuda &out, + cuda::Tensor1DWrap maxValueArr, + NVCVAdaptiveThresholdType adaptiveMethod, NVCVThresholdType thresholdType, + cuda::Tensor1DWrap blockSizeArr, cuda::Tensor1DWrap cArr, + cuda::Tensor3DWrap kernel, int maxBlockSize, + AdaptiveThresholdKernelPolicy kernelPolicy, cudaStream_t stream) +{ + if (kernelPolicy == AdaptiveThresholdKernelPolicy::kLegacyX4) + { + adaptive_threshold_caller_impl<4, false, D, B, CMP>(in, out, maxValueArr, adaptiveMethod, thresholdType, + blockSizeArr, cArr, kernel, maxBlockSize, stream); + } + else + { + adaptive_threshold_caller_impl<8, true, D, B, CMP>(in, out, maxValueArr, adaptiveMethod, thresholdType, + blockSizeArr, cArr, kernel, maxBlockSize, stream); + } +} + AdaptiveThresholdVarShape::AdaptiveThresholdVarShape(DataShape maxInputShape, DataShape maxOutputShape, - int32_t maxBlockSize, int32_t maxVarShapeBatchSize) + int32_t maxBlockSize, int32_t maxVarShapeBatchSize, + AdaptiveThresholdKernelPolicy kernelPolicy) : CudaBaseOp(maxInputShape, maxOutputShape) - , m_maxBlockSize(maxBlockSize) , m_maxBatchSize(maxVarShapeBatchSize) + , m_maxBlockSize(maxBlockSize) + , m_kernelPolicy(kernelPolicy) { if (maxBlockSize <= 0) { @@ -232,9 +328,11 @@ ErrorCode AdaptiveThresholdVarShape::infer(const ImageBatchVarShapeDataStridedCu DataFormat format = input_format; - if (!(format == kNHWC || format == kHWC)) + const bool isPlanar = format == kNCHW || format == kCHW; + if (!(format == kNHWC || format == kHWC || isPlanar)) { - LOG_ERROR("Invalid input DataFormat " << format << ", the valid DataFormats are: \"NHWC\", \"HWC\""); + LOG_ERROR("Invalid input DataFormat " << format + << ", the valid DataFormats are: \"NHWC\", \"HWC\", \"NCHW\", \"CHW\""); return ErrorCode::INVALID_DATA_FORMAT; } @@ -266,6 +364,44 @@ ErrorCode AdaptiveThresholdVarShape::infer(const ImageBatchVarShapeDataStridedCu return ErrorCode::INVALID_PARAMETER; } + const int numImages = in.numImages(); + ErrorCode status = validateParameterTensor(maxValue, nvcv::TYPE_F64, numImages, "maxValue"); + if (status != ErrorCode::SUCCESS) + { + return status; + } + + status = validateParameterTensor(blockSize, nvcv::TYPE_S32, numImages, "blockSize"); + if (status != ErrorCode::SUCCESS) + { + return status; + } + + status = validateParameterTensor(c, nvcv::TYPE_F64, numImages, "c"); + if (status != ErrorCode::SUCCESS) + { + return status; + } + + auto blockSizeAccess = nvcv::TensorDataAccessStrided::Create(blockSize); + NVCV_ASSERT(blockSizeAccess); + + std::vector blockSizes(numImages); + checkCudaErrors(cudaMemcpy2DAsync(blockSizes.data(), sizeof(int), blockSizeAccess->sampleData(0), + blockSizeAccess->sampleStride(), sizeof(int), numImages, cudaMemcpyDeviceToHost, + stream)); + checkCudaErrors(cudaStreamSynchronize(stream)); + + for (int i = 0; i < numImages; ++i) + { + const int bsize = blockSizes[i]; + if (!(bsize % 2 == 1 && bsize > 1 && bsize <= m_maxBlockSize)) + { + LOG_ERROR("Invalid BlockSize " << bsize); + return ErrorCode::INVALID_PARAMETER; + } + } + cuda::Tensor1DWrap maxValueArr(maxValue); cuda::Tensor1DWrap blockSizeArr(blockSize); cuda::Tensor1DWrap cArr(c); @@ -273,7 +409,7 @@ ErrorCode AdaptiveThresholdVarShape::infer(const ImageBatchVarShapeDataStridedCu int kernelPitch2 = static_cast(m_maxBlockSize * sizeof(float)); int kernelPitch1 = m_maxBlockSize * kernelPitch2; - float *kernelPtr = (float *)m_kernel; + float *kernelPtr = m_kernel; cuda::Tensor3DWrap kernelTensor(kernelPtr, kernelPitch1, kernelPitch2); dim3 block(32, 4); @@ -291,15 +427,15 @@ ErrorCode AdaptiveThresholdVarShape::infer(const ImageBatchVarShapeDataStridedCu if (thresholdType == NVCV_THRESH_BINARY) { - adaptive_threshold_caller>(in, out, maxValueArr, adaptiveMethod, - thresholdType, blockSizeArr, cArr, - kernelTensor, m_maxBlockSize, stream); + adaptive_threshold_caller>( + in, out, maxValueArr, adaptiveMethod, thresholdType, blockSizeArr, cArr, kernelTensor, m_maxBlockSize, + m_kernelPolicy, stream); } else { - adaptive_threshold_caller>(in, out, maxValueArr, adaptiveMethod, - thresholdType, blockSizeArr, cArr, - kernelTensor, m_maxBlockSize, stream); + adaptive_threshold_caller>( + in, out, maxValueArr, adaptiveMethod, thresholdType, blockSizeArr, cArr, kernelTensor, m_maxBlockSize, + m_kernelPolicy, stream); } checkKernelErrors(); diff --git a/src/cvcuda/priv/legacy/bilateral_filter.cu b/src/cvcuda/priv/legacy/bilateral_filter.cu index 84838bc02..7d9f68152 100644 --- a/src/cvcuda/priv/legacy/bilateral_filter.cu +++ b/src/cvcuda/priv/legacy/bilateral_filter.cu @@ -27,6 +27,8 @@ #include +#include + using namespace nvcv::legacy::cuda_op; using namespace nvcv::legacy::helpers; @@ -52,6 +54,201 @@ static __device__ __forceinline__ float norm1(const float4 &a) return cuda::abs(a.x) + cuda::abs(a.y) + cuda::abs(a.z) + cuda::abs(a.w); } +template +struct BilateralFilterPlanarTensorWrap +{ + const NVCVByte *srcBase; + NVCVByte *dstBase; + int64_t srcSampleStride; + int64_t srcChStride; + int64_t srcRowStride; + int64_t srcColStride; + int64_t dstSampleStride; + int64_t dstChStride; + int64_t dstRowStride; + int64_t dstColStride; + + __device__ __forceinline__ float read(int sample, int channel, int y, int x) const + { + const NVCVByte *ptr + = srcBase + sample * srcSampleStride + channel * srcChStride + y * srcRowStride + x * srcColStride; + return static_cast(*reinterpret_cast(ptr)); + } + + __device__ __forceinline__ void write(int sample, int channel, int y, int x, T value) const + { + NVCVByte *ptr + = dstBase + sample * dstSampleStride + channel * dstChStride + y * dstRowStride + x * dstColStride; + *reinterpret_cast(ptr) = value; + } +}; + +template +__device__ __forceinline__ bool mapBorderCoordinate(int &x, int &y, int columns, int rows, bool windowInside) +{ + if constexpr (B == NVCV_BORDER_CONSTANT) + { + return !cuda::IsOutside(x, columns) && !cuda::IsOutside(y, rows); + } + else + { + if constexpr (FAST_INTERIOR) + { + if (windowInside || (!cuda::IsOutside(x, columns) && !cuda::IsOutside(y, rows))) + { + return true; + } + } + x = cuda::GetIndexWithBorder(x, columns); + y = cuda::GetIndexWithBorder(y, rows); + return true; + } +} + +template +__device__ __forceinline__ typename SrcWrapper::ValueType readPackedPixel(const SrcWrapper &src, int3 coord, int rows, + int columns, bool windowInside) +{ + if constexpr (SrcWrapper::kBorderType != NVCV_BORDER_CONSTANT) + { + if (windowInside || (!cuda::IsOutside(coord.x, columns) && !cuda::IsOutside(coord.y, rows))) + { + return src.tensorWrap()[coord]; + } + } + return src[coord]; +} + +template +__device__ __forceinline__ void BilateralFilterPlanarTile(BilateralFilterPlanarTensorWrap img, int batch_idx, + int colIdx, int rowIdx, int rows, int columns, int radius, + int squared_radius, float color_coefficient, + float space_coefficient) +{ + const int x[4] = {colIdx, colIdx + 1, colIdx, colIdx + 1}; + const int y[4] = {rowIdx, rowIdx, rowIdx + 1, rowIdx + 1}; + const bool windowInside + = colIdx >= radius && rowIdx >= radius && colIdx + radius + 1 < columns && rowIdx + radius + 1 < rows; + + bool valid[4]; + valid[0] = colIdx < columns && rowIdx < rows; + valid[1] = colIdx + 1 < columns && rowIdx < rows; + valid[2] = colIdx < columns && rowIdx + 1 < rows; + valid[3] = colIdx + 1 < columns && rowIdx + 1 < rows; + + if (!(valid[0] || valid[1] || valid[2] || valid[3])) + { + return; + } + + float center[4][CHANNELS] = {}; + float numerator[4][CHANNELS] = {}; + float denominator[4] = {}; + +#pragma unroll + for (int p = 0; p < 4; ++p) + { + if (valid[p]) + { +#pragma unroll + for (int ch = 0; ch < CHANNELS; ++ch) + { + center[p][ch] = img.read(batch_idx, ch, y[p], x[p]); + } + } + } + + for (int r = rowIdx - radius; r < rowIdx + radius + 2; r++) + { + for (int c = colIdx - radius; c < colIdx + radius + 2; c++) + { + const int dx0 = std::abs(c - colIdx); + const int dy0 = cuda::abs(r - rowIdx); + const int dx1 = std::abs(c - (colIdx + 1)); + const int dy1 = cuda::abs(r - (rowIdx + 1)); + const int squared_dis0 = dx0 * dx0 + dy0 * dy0; + const int squared_dis1 = dx1 * dx1 + dy0 * dy0; + const int squared_dis2 = dx0 * dx0 + dy1 * dy1; + const int squared_dis3 = dx1 * dx1 + dy1 * dy1; + + if (!(squared_dis0 <= squared_radius || squared_dis1 <= squared_radius || squared_dis2 <= squared_radius + || squared_dis3 <= squared_radius)) + { + continue; + } + + int srcX = c; + int srcY = r; + constexpr bool fastInterior = !(std::is_same_v && CHANNELS == 4); + bool inside = mapBorderCoordinate(srcX, srcY, columns, rows, windowInside); + + float curr[CHANNELS] = {}; + if (inside) + { +#pragma unroll + for (int ch = 0; ch < CHANNELS; ++ch) + { + curr[ch] = img.read(batch_idx, ch, srcY, srcX); + } + } + + const int squared_dis[4] = {squared_dis0, squared_dis1, squared_dis2, squared_dis3}; +#pragma unroll + for (int p = 0; p < 4; ++p) + { + if (valid[p] && squared_dis[p] <= squared_radius) + { + float one_norm_size = 0.f; +#pragma unroll + for (int ch = 0; ch < CHANNELS; ++ch) + { + one_norm_size += cuda::abs(curr[ch] - center[p][ch]); + } + + const float e_space = squared_dis[p] * space_coefficient; + const float e_color = one_norm_size * one_norm_size * color_coefficient; + const float weight = cuda::exp(e_space + e_color); + denominator[p] += weight; +#pragma unroll + for (int ch = 0; ch < CHANNELS; ++ch) + { + numerator[p][ch] += weight * curr[ch]; + } + } + } + } + } + +#pragma unroll + for (int p = 0; p < 4; ++p) + { + if (valid[p]) + { +#pragma unroll + for (int ch = 0; ch < CHANNELS; ++ch) + { + img.write(batch_idx, ch, y[p], x[p], cuda::SaturateCast(numerator[p][ch] / denominator[p])); + } + } + } +} + +template +__global__ void BilateralFilterPlanarKernel(BilateralFilterPlanarTensorWrap img, int radius, float sigmaColor, + float sigmaSpace, int rows, int columns) +{ + const int colIdx = (blockIdx.x * blockDim.x + threadIdx.x) * 2; + const int rowIdx = (blockIdx.y * blockDim.y + threadIdx.y) * 2; + const int batch_idx = blockIdx.z; + + const int squared_radius = radius * radius; + const float space_coefficient = -1 / (2 * sigmaSpace * sigmaSpace); + const float color_coefficient = -1 / (2 * sigmaColor * sigmaColor); + + BilateralFilterPlanarTile(img, batch_idx, colIdx, rowIdx, rows, columns, radius, squared_radius, + color_coefficient, space_coefficient); +} + template __global__ void BilateralFilterKernel(SrcWrapper src, DstWrapper dst, const int radius, const float sigmaColor, const float sigmaSpace, const int rows, const int columns) @@ -62,14 +259,16 @@ __global__ void BilateralFilterKernel(SrcWrapper src, DstWrapper dst, const int using T = typename DstWrapper::ValueType; using work_type = cuda::ConvertBaseTypeTo; - int3 coord0{colIdx, rowIdx, batch_idx}; - int3 coord1{colIdx + 1, rowIdx, batch_idx}; - int3 coord2{colIdx, rowIdx + 1, batch_idx}; - int3 coord3{colIdx + 1, rowIdx + 1, batch_idx}; - work_type center0 = cuda::StaticCast(src[coord0]); - work_type center1 = cuda::StaticCast(src[coord1]); - work_type center2 = cuda::StaticCast(src[coord2]); - work_type center3 = cuda::StaticCast(src[coord3]); + int3 coord0{colIdx, rowIdx, batch_idx}; + int3 coord1{colIdx + 1, rowIdx, batch_idx}; + int3 coord2{colIdx, rowIdx + 1, batch_idx}; + int3 coord3{colIdx + 1, rowIdx + 1, batch_idx}; + const bool windowInside = !std::is_same_v && colIdx >= radius && rowIdx >= radius + && colIdx + radius + 1 < columns && rowIdx + radius + 1 < rows; + work_type center0 = cuda::StaticCast(readPackedPixel(src, coord0, rows, columns, windowInside)); + work_type center1 = cuda::StaticCast(readPackedPixel(src, coord1, rows, columns, windowInside)); + work_type center2 = cuda::StaticCast(readPackedPixel(src, coord2, rows, columns, windowInside)); + work_type center3 = cuda::StaticCast(readPackedPixel(src, coord3, rows, columns, windowInside)); int squared_radius = radius * radius; float space_coefficient = -1 / (2 * sigmaSpace * sigmaSpace); @@ -101,7 +300,7 @@ __global__ void BilateralFilterKernel(SrcWrapper src, DstWrapper dst, const int } int3 coord{c, r, batch_idx}; - work_type curr = cuda::StaticCast(src[coord]); + work_type curr = cuda::StaticCast(readPackedPixel(src, coord, rows, columns, windowInside)); if (squared_dis0 <= squared_radius) { @@ -162,12 +361,74 @@ __global__ void BilateralFilterKernel(SrcWrapper src, DstWrapper dst, const int } } +template +ErrorCode BilateralFilterPlanarCaller(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, + const nvcv::TensorDataAccessStridedImagePlanar &inAccess, + const nvcv::TensorDataAccessStridedImagePlanar &outAccess, const int batch, + int rows, int columns, int channels, int radius, float sigmaColor, + float sigmaSpace, cudaStream_t stream) +{ + if (inAccess.sampleStride() * inAccess.numSamples() <= cuda::TypeTraits::max) + { + dim3 block(32, 2); + dim3 grid(divUp(columns, block.x * 2), divUp(rows, block.y * 2), batch); + + BilateralFilterPlanarTensorWrap img{ + reinterpret_cast(inData.basePtr()), + reinterpret_cast(outData.basePtr()), + inAccess.sampleStride(), + inAccess.chStride(), + inAccess.rowStride(), + inAccess.colStride(), + outAccess.sampleStride(), + outAccess.chStride(), + outAccess.rowStride(), + outAccess.colStride(), + }; + +#ifdef CUDA_DEBUG_LOG + checkCudaErrors(cudaStreamSynchronize(stream)); + checkCudaErrors(cudaGetLastError()); +#endif + + switch (channels) + { + case 1: + BilateralFilterPlanarKernel + <<>>(img, radius, sigmaColor, sigmaSpace, rows, columns); + break; + case 3: + BilateralFilterPlanarKernel + <<>>(img, radius, sigmaColor, sigmaSpace, rows, columns); + break; + case 4: + BilateralFilterPlanarKernel + <<>>(img, radius, sigmaColor, sigmaSpace, rows, columns); + break; + default: + LOG_ERROR("Invalid planar channel number ch = " << channels); + return ErrorCode::INVALID_DATA_SHAPE; + } + +#ifdef CUDA_DEBUG_LOG + checkCudaErrors(cudaStreamSynchronize(stream)); + checkCudaErrors(cudaGetLastError()); +#endif + } + else + { + LOG_ERROR("Input size exceeds " << cuda::TypeTraits::max << ". Tensor is too large."); + return ErrorCode::INVALID_PARAMETER; + } + return ErrorCode::SUCCESS; +} + template void BilateralFilterCallerS(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, const int batch, int rows, int columns, int radius, float sigmaColor, float sigmaSpace, float borderValue, cudaStream_t stream) { - dim3 block(8, 8); + dim3 block(32, 2); dim3 grid(divUp(columns, block.x * 2), divUp(rows, block.y * 2), batch); auto src = cuda::CreateBorderWrapNHW(inData, cuda::SetAll(borderValue)); @@ -225,11 +486,12 @@ ErrorCode BilateralFilter::infer(const TensorDataStridedCuda &inData, const Tens return ErrorCode::INVALID_DATA_FORMAT; } - if ((input_format != kNHWC) && (input_format != kHWC)) + if (!(input_format == kNHWC || input_format == kHWC || input_format == kNCHW || input_format == kCHW)) { - LOG_ERROR("Invalid DataFormat both Input and Output must be kHWC or kNHWC"); + LOG_ERROR("Invalid DataFormat both Input and Output must be kHWC, kNHWC, kCHW, or kNCHW"); return ErrorCode::INVALID_DATA_FORMAT; } + const bool isPlanar = input_format == kNCHW || input_format == kCHW; if (!(borderMode == NVCV_BORDER_CONSTANT || borderMode == NVCV_BORDER_REPLICATE || borderMode == NVCV_BORDER_REFLECT || borderMode == NVCV_BORDER_WRAP || borderMode == NVCV_BORDER_REFLECT101)) @@ -275,6 +537,21 @@ ErrorCode BilateralFilter::infer(const TensorDataStridedCuda &inData, const Tens { return ErrorCode::INVALID_DATA_FORMAT; } + auto outAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(outData); + if (!outAccess) + { + return ErrorCode::INVALID_DATA_FORMAT; + } + + cuda_op::DataShape inputShape = GetLegacyDataShape(inAccess->infoShape()); + cuda_op::DataShape outputShape = GetLegacyDataShape(outAccess->infoShape()); + + if (inputShape != outputShape) + { + LOG_ERROR("Input/output shape is different " << inputShape << "/" << outputShape); + return ErrorCode::INVALID_DATA_SHAPE; + } + int batch = inAccess->numSamples(); int channels = inAccess->numChannels(); int rows = inAccess->numRows(); @@ -284,6 +561,11 @@ ErrorCode BilateralFilter::infer(const TensorDataStridedCuda &inData, const Tens LOG_ERROR("Invalid channel number ch = " << channels); return ErrorCode::INVALID_DATA_SHAPE; } + if (isPlanar && channels == 2) + { + LOG_ERROR("Planar BilateralFilter does not support 2-channel images"); + return ErrorCode::INVALID_DATA_SHAPE; + } float borderValue = .0f; @@ -292,13 +574,12 @@ ErrorCode BilateralFilter::infer(const TensorDataStridedCuda &inData, const Tens float sigmaSpace, float borderValue, cudaStream_t stream); // All templated functions instantiated here to remove one level of indirection that just hides the same lookup - // table in 5 parts + // table in 5 parts. The kCV_8S row is null because validation above rejects signed 8-bit input. static const bilateral_filter_t funcs[5][6][4] = { { {BilateralFilterCaller, BilateralFilterCaller, BilateralFilterCaller, BilateralFilterCaller}, - {BilateralFilterCaller, BilateralFilterCaller, - BilateralFilterCaller, BilateralFilterCaller}, + {nullptr, nullptr, nullptr, nullptr}, {BilateralFilterCaller, BilateralFilterCaller, BilateralFilterCaller, BilateralFilterCaller}, @@ -313,8 +594,7 @@ ErrorCode BilateralFilter::infer(const TensorDataStridedCuda &inData, const Tens {BilateralFilterCaller, BilateralFilterCaller, BilateralFilterCaller, BilateralFilterCaller}, - {BilateralFilterCaller, BilateralFilterCaller, - BilateralFilterCaller, BilateralFilterCaller}, + {nullptr, nullptr, nullptr, nullptr}, {BilateralFilterCaller, BilateralFilterCaller, BilateralFilterCaller, @@ -331,8 +611,7 @@ ErrorCode BilateralFilter::infer(const TensorDataStridedCuda &inData, const Tens { {BilateralFilterCaller, BilateralFilterCaller, BilateralFilterCaller, BilateralFilterCaller}, - {BilateralFilterCaller, BilateralFilterCaller, - BilateralFilterCaller, BilateralFilterCaller}, + {nullptr, nullptr, nullptr, nullptr}, {BilateralFilterCaller, BilateralFilterCaller, BilateralFilterCaller, BilateralFilterCaller}, {BilateralFilterCaller, BilateralFilterCaller, @@ -345,8 +624,7 @@ ErrorCode BilateralFilter::infer(const TensorDataStridedCuda &inData, const Tens { {BilateralFilterCaller, BilateralFilterCaller, BilateralFilterCaller, BilateralFilterCaller}, - {BilateralFilterCaller, BilateralFilterCaller, - BilateralFilterCaller, BilateralFilterCaller}, + {nullptr, nullptr, nullptr, nullptr}, {BilateralFilterCaller, BilateralFilterCaller, BilateralFilterCaller, BilateralFilterCaller}, {BilateralFilterCaller, BilateralFilterCaller, @@ -361,9 +639,7 @@ ErrorCode BilateralFilter::infer(const TensorDataStridedCuda &inData, const Tens BilateralFilterCaller, BilateralFilterCaller, BilateralFilterCaller}, - {BilateralFilterCaller, BilateralFilterCaller, - BilateralFilterCaller, - BilateralFilterCaller}, + {nullptr, nullptr, nullptr, nullptr}, {BilateralFilterCaller, BilateralFilterCaller, BilateralFilterCaller, @@ -380,6 +656,40 @@ ErrorCode BilateralFilter::infer(const TensorDataStridedCuda &inData, const Tens BilateralFilterCaller}, }, }; + typedef ErrorCode (*bilateral_filter_planar_t)( + const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, + const nvcv::TensorDataAccessStridedImagePlanar &inAccess, + const nvcv::TensorDataAccessStridedImagePlanar &outAccess, int batch, int rows, int columns, int channels, + int radius, float sigmaColor, float sigmaSpace, cudaStream_t stream); + static const bilateral_filter_planar_t planarFuncs[5][6] = { + {BilateralFilterPlanarCaller, nullptr, + BilateralFilterPlanarCaller, + BilateralFilterPlanarCaller, + BilateralFilterPlanarCaller, + BilateralFilterPlanarCaller }, + {BilateralFilterPlanarCaller, nullptr, + BilateralFilterPlanarCaller, + BilateralFilterPlanarCaller, + BilateralFilterPlanarCaller, + BilateralFilterPlanarCaller }, + {BilateralFilterPlanarCaller, nullptr, + BilateralFilterPlanarCaller, + BilateralFilterPlanarCaller, BilateralFilterPlanarCaller, + BilateralFilterPlanarCaller }, + {BilateralFilterPlanarCaller, nullptr, + BilateralFilterPlanarCaller, BilateralFilterPlanarCaller, + BilateralFilterPlanarCaller, BilateralFilterPlanarCaller}, + {BilateralFilterPlanarCaller, nullptr, + BilateralFilterPlanarCaller, + BilateralFilterPlanarCaller, + BilateralFilterPlanarCaller, + BilateralFilterPlanarCaller }, + }; + if (isPlanar) + { + return planarFuncs[borderMode][data_type](inData, outData, *inAccess, *outAccess, batch, rows, columns, + channels, radius, sigmaColor, sigmaSpace, stream); + } return funcs[borderMode][data_type][channels - 1](inData, outData, batch, rows, columns, radius, sigmaColor, sigmaSpace, borderValue, stream); } diff --git a/src/cvcuda/priv/legacy/bilateral_filter_var_shape.cu b/src/cvcuda/priv/legacy/bilateral_filter_var_shape.cu index e063f29ee..0c96265db 100644 --- a/src/cvcuda/priv/legacy/bilateral_filter_var_shape.cu +++ b/src/cvcuda/priv/legacy/bilateral_filter_var_shape.cu @@ -50,6 +50,233 @@ static __device__ __forceinline__ float norm1(const float4 &a) return cuda::abs(a.x) + cuda::abs(a.y) + cuda::abs(a.z) + cuda::abs(a.w); } +template +__device__ __forceinline__ bool mapBorderCoordinate(int &x, int &y, int columns, int rows, bool windowInside) +{ + if constexpr (B == NVCV_BORDER_CONSTANT) + { + return !cuda::IsOutside(x, columns) && !cuda::IsOutside(y, rows); + } + else + { + if (windowInside || (!cuda::IsOutside(x, columns) && !cuda::IsOutside(y, rows))) + { + return true; + } + x = cuda::GetIndexWithBorder(x, columns); + y = cuda::GetIndexWithBorder(y, rows); + return true; + } +} + +template +__device__ __forceinline__ typename SrcWrapper::ValueType readPackedPixel(const SrcWrapper &src, int3 coord, int rows, + int columns, bool windowInside) +{ + if constexpr (SrcWrapper::kBorderType != NVCV_BORDER_CONSTANT) + { + if (windowInside || (!cuda::IsOutside(coord.x, columns) && !cuda::IsOutside(coord.y, rows))) + { + return src.imageBatchWrap()[coord]; + } + } + return src[coord]; +} + +template +struct BilateralFilterVarShapePlanarImageWrap +{ + const NVCVByte *srcBase[CHANNELS]; + NVCVByte *dstBase[CHANNELS]; + int srcRowStride[CHANNELS]; + int dstRowStride[CHANNELS]; + + __device__ __forceinline__ float read(int channel, int y, int x) const + { + const NVCVByte *ptr = srcBase[channel] + y * srcRowStride[channel] + x * sizeof(T); + return static_cast(*reinterpret_cast(ptr)); + } + + __device__ __forceinline__ void write(int channel, int y, int x, T value) const + { + NVCVByte *ptr = dstBase[channel] + y * dstRowStride[channel] + x * sizeof(T); + *reinterpret_cast(ptr) = value; + } +}; + +template +__device__ __forceinline__ void BilateralFilterVarShapePlanarTile( + BilateralFilterVarShapePlanarImageWrap img, int colIdx, int rowIdx, int rows, int columns, int radius, + int squared_radius, float color_coefficient, float space_coefficient) +{ + const int x[4] = {colIdx, colIdx + 1, colIdx, colIdx + 1}; + const int y[4] = {rowIdx, rowIdx, rowIdx + 1, rowIdx + 1}; + const bool windowInside + = colIdx >= radius && rowIdx >= radius && colIdx + radius + 1 < columns && rowIdx + radius + 1 < rows; + + bool valid[4]; + valid[0] = colIdx < columns && rowIdx < rows; + valid[1] = colIdx + 1 < columns && rowIdx < rows; + valid[2] = colIdx < columns && rowIdx + 1 < rows; + valid[3] = colIdx + 1 < columns && rowIdx + 1 < rows; + + if (!(valid[0] || valid[1] || valid[2] || valid[3])) + { + return; + } + + float center[4][CHANNELS] = {}; + float numerator[4][CHANNELS] = {}; + float denominator[4] = {}; + +#pragma unroll + for (int p = 0; p < 4; ++p) + { + if (valid[p]) + { +#pragma unroll + for (int ch = 0; ch < CHANNELS; ++ch) + { + center[p][ch] = img.read(ch, y[p], x[p]); + } + } + } + + for (int r = rowIdx - radius; r < rowIdx + radius + 2; r++) + { + for (int c = colIdx - radius; c < colIdx + radius + 2; c++) + { + const int dx0 = std::abs(c - colIdx); + const int dy0 = cuda::abs(r - rowIdx); + const int dx1 = std::abs(c - (colIdx + 1)); + const int dy1 = cuda::abs(r - (rowIdx + 1)); + const int squared_dis0 = dx0 * dx0 + dy0 * dy0; + const int squared_dis1 = dx1 * dx1 + dy0 * dy0; + const int squared_dis2 = dx0 * dx0 + dy1 * dy1; + const int squared_dis3 = dx1 * dx1 + dy1 * dy1; + + if (!(squared_dis0 <= squared_radius || squared_dis1 <= squared_radius || squared_dis2 <= squared_radius + || squared_dis3 <= squared_radius)) + { + continue; + } + + float curr[CHANNELS] = {}; + int srcX = c; + int srcY = r; + bool inside = mapBorderCoordinate(srcX, srcY, columns, rows, windowInside); + if (inside) + { +#pragma unroll + for (int ch = 0; ch < CHANNELS; ++ch) + { + curr[ch] = img.read(ch, srcY, srcX); + } + } + + const int squared_dis[4] = {squared_dis0, squared_dis1, squared_dis2, squared_dis3}; +#pragma unroll + for (int p = 0; p < 4; ++p) + { + if (valid[p] && squared_dis[p] <= squared_radius) + { + float one_norm_size = 0.f; +#pragma unroll + for (int ch = 0; ch < CHANNELS; ++ch) + { + one_norm_size += cuda::abs(curr[ch] - center[p][ch]); + } + + const float e_space = squared_dis[p] * space_coefficient; + const float e_color = one_norm_size * one_norm_size * color_coefficient; + const float weight = cuda::exp(e_space + e_color); + denominator[p] += weight; +#pragma unroll + for (int ch = 0; ch < CHANNELS; ++ch) + { + numerator[p][ch] += weight * curr[ch]; + } + } + } + } + } + +#pragma unroll + for (int p = 0; p < 4; ++p) + { + if (valid[p]) + { +#pragma unroll + for (int ch = 0; ch < CHANNELS; ++ch) + { + img.write(ch, y[p], x[p], nvcv::cuda::SaturateCast(numerator[p][ch] / denominator[p])); + } + } + } +} + +template +__global__ __launch_bounds__(64, CHANNELS == 4 ? 12 : 16) void BilateralFilterVarShapePlanarKernel( + const SrcWrapper src, DstWrapper dst, const cuda::Tensor1DWrap inDiameter, + const cuda::Tensor1DWrap inSigmaColor, const cuda::Tensor1DWrap inSigmaSpace) +{ + const int batch_idx = get_batch_idx(); + const int rows = dst.height(batch_idx); + const int columns = dst.width(batch_idx); + + float sigmaColor = inSigmaColor[batch_idx]; + if (sigmaColor <= 0) + { + sigmaColor = 1; + } + float sigmaSpace = inSigmaSpace[batch_idx]; + if (sigmaSpace <= 0) + { + sigmaSpace = 1; + } + + int radius; + int diameter = inDiameter[batch_idx]; + if (diameter <= 0) + { + radius = std::roundf(sigmaSpace * 1.5f); + } + else + { + radius = diameter / 2; + } + if (radius < 1) + { + radius = 1; + } + assert(radius < 10000); + + const int colIdx = (blockIdx.x * blockDim.x + threadIdx.x) * 2; + const int rowIdx = (blockIdx.y * blockDim.y + threadIdx.y) * 2; + + const int squared_radius = radius * radius; + const float space_coefficient = -1 / (2 * sigmaSpace * sigmaSpace); + const float color_coefficient = -1 / (2 * sigmaColor * sigmaColor); + + using T = typename DstWrapper::ValueType; + + BilateralFilterVarShapePlanarImageWrap img{}; +#pragma unroll + for (int ch = 0; ch < CHANNELS; ++ch) + { + const NVCVImagePlaneStrided srcPlane = src.imageBatchWrap().plane(batch_idx, ch); + const NVCVImagePlaneStrided dstPlane = dst.plane(batch_idx, ch); + + img.srcBase[ch] = srcPlane.basePtr; + img.dstBase[ch] = dstPlane.basePtr; + img.srcRowStride[ch] = srcPlane.rowStride; + img.dstRowStride[ch] = dstPlane.rowStride; + } + + BilateralFilterVarShapePlanarTile(img, colIdx, rowIdx, rows, columns, radius, squared_radius, + color_coefficient, space_coefficient); +} + template __global__ void BilateralFilterVarShapeKernel(const SrcWrapper src, DstWrapper dst, const cuda::Tensor1DWrap inDiameter, @@ -92,14 +319,16 @@ __global__ void BilateralFilterVarShapeKernel(const SrcWrapper src, DstWrapper d const int colIdx = (blockIdx.x * blockDim.x + threadIdx.x) * 2; const int rowIdx = (blockIdx.y * blockDim.y + threadIdx.y) * 2; using work_type = cuda::ConvertBaseTypeTo; - int3 coord0{colIdx, rowIdx, batch_idx}; - int3 coord1{colIdx + 1, rowIdx, batch_idx}; - int3 coord2{colIdx, rowIdx + 1, batch_idx}; - int3 coord3{colIdx + 1, rowIdx + 1, batch_idx}; - work_type center0 = cuda::StaticCast(src[coord0]); - work_type center1 = cuda::StaticCast(src[coord1]); - work_type center2 = cuda::StaticCast(src[coord2]); - work_type center3 = cuda::StaticCast(src[coord3]); + int3 coord0{colIdx, rowIdx, batch_idx}; + int3 coord1{colIdx + 1, rowIdx, batch_idx}; + int3 coord2{colIdx, rowIdx + 1, batch_idx}; + int3 coord3{colIdx + 1, rowIdx + 1, batch_idx}; + const bool windowInside + = colIdx >= radius && rowIdx >= radius && colIdx + radius + 1 < columns && rowIdx + radius + 1 < rows; + work_type center0 = cuda::StaticCast(readPackedPixel(src, coord0, rows, columns, windowInside)); + work_type center1 = cuda::StaticCast(readPackedPixel(src, coord1, rows, columns, windowInside)); + work_type center2 = cuda::StaticCast(readPackedPixel(src, coord2, rows, columns, windowInside)); + work_type center3 = cuda::StaticCast(readPackedPixel(src, coord3, rows, columns, windowInside)); int squared_radius = radius * radius; float space_coefficient = -1 / (2 * sigmaSpace * sigmaSpace); @@ -131,7 +360,7 @@ __global__ void BilateralFilterVarShapeKernel(const SrcWrapper src, DstWrapper d } int3 srcCoord{c, r, batch_idx}; - work_type curr = cuda::StaticCast(src[srcCoord]); + work_type curr = cuda::StaticCast(readPackedPixel(src, srcCoord, rows, columns, windowInside)); if (squared_dis0 <= squared_radius) { @@ -192,6 +421,51 @@ __global__ void BilateralFilterVarShapeKernel(const SrcWrapper src, DstWrapper d } } +template +ErrorCode BilateralFilterVarShapePlanarCaller(const ImageBatchVarShapeDataStridedCuda &inData, + const ImageBatchVarShapeDataStridedCuda &outData, int batch, int channels, + const cuda::Tensor1DWrap &inDiameter, + const cuda::Tensor1DWrap &inSigmaColor, + const cuda::Tensor1DWrap &inSigmaSpace, cudaStream_t stream) +{ + cuda::BorderVarShapeWrap src(inData); + cuda::ImageBatchVarShapeWrap dst(outData); + + Size2D outMaxSize = outData.maxSize(); + dim3 block(32, 2); + dim3 grid(divUp(outMaxSize.w, block.x * 2), divUp(outMaxSize.h, block.y * 2), batch); + +#ifdef CUDA_DEBUG_LOG + checkCudaErrors(cudaStreamSynchronize(stream)); + checkCudaErrors(cudaGetLastError()); +#endif + + switch (channels) + { + case 1: + BilateralFilterVarShapePlanarKernel + <<>>(src, dst, inDiameter, inSigmaColor, inSigmaSpace); + break; + case 3: + BilateralFilterVarShapePlanarKernel + <<>>(src, dst, inDiameter, inSigmaColor, inSigmaSpace); + break; + case 4: + BilateralFilterVarShapePlanarKernel + <<>>(src, dst, inDiameter, inSigmaColor, inSigmaSpace); + break; + default: + LOG_ERROR("Invalid planar channel number ch = " << channels); + return ErrorCode::INVALID_DATA_SHAPE; + } + +#ifdef CUDA_DEBUG_LOG + checkCudaErrors(cudaStreamSynchronize(stream)); + checkCudaErrors(cudaGetLastError()); +#endif + return ErrorCode::SUCCESS; +} + template void BilateralFilterVarShapeCaller(const ImageBatchVarShapeDataStridedCuda &inData, const ImageBatchVarShapeDataStridedCuda &outData, int batch, @@ -203,7 +477,7 @@ void BilateralFilterVarShapeCaller(const ImageBatchVarShapeDataStridedCuda &inDa cuda::ImageBatchVarShapeWrap dst(outData); Size2D outMaxSize = outData.maxSize(); - dim3 block(8, 8); + dim3 block(32, 2); dim3 grid(divUp(outMaxSize.w, block.x * 2), divUp(outMaxSize.h, block.y * 2), batch); #ifdef CUDA_DEBUG_LOG @@ -242,11 +516,12 @@ ErrorCode BilateralFilterVarShape::infer(const ImageBatchVarShapeDataStridedCuda return ErrorCode::INVALID_DATA_FORMAT; } - if ((input_format != kNHWC) && (input_format != kHWC)) + if (!(input_format == kNHWC || input_format == kHWC || input_format == kNCHW || input_format == kCHW)) { - LOG_ERROR("Invalid DataFormat both Input and Output must be kHWC or kNHWC"); + LOG_ERROR("Invalid DataFormat both Input and Output must be kHWC, kNHWC, kCHW, or kNCHW"); return ErrorCode::INVALID_DATA_FORMAT; } + const bool isPlanar = input_format == kNCHW || input_format == kCHW; if (inData.uniqueFormat() != outData.uniqueFormat()) { @@ -306,6 +581,11 @@ ErrorCode BilateralFilterVarShape::infer(const ImageBatchVarShapeDataStridedCuda LOG_ERROR("Invalid channel number ch = " << channels); return ErrorCode::INVALID_DATA_SHAPE; } + if (isPlanar && channels == 2) + { + LOG_ERROR("Planar BilateralFilter does not support 2-channel images"); + return ErrorCode::INVALID_DATA_SHAPE; + } // Create Tensor wrappers for parameter arrays cuda::Tensor1DWrap inDiameter(diameterData); @@ -316,19 +596,20 @@ ErrorCode BilateralFilterVarShape::infer(const ImageBatchVarShapeDataStridedCuda const ImageBatchVarShapeDataStridedCuda &inData, const ImageBatchVarShapeDataStridedCuda &outData, int batch, const cuda::Tensor1DWrap &inDiameter, const cuda::Tensor1DWrap &inSigmaColor, const cuda::Tensor1DWrap &inSigmaSpace, cudaStream_t stream); + typedef ErrorCode (*bilateral_filter_var_shape_planar_t)( + const ImageBatchVarShapeDataStridedCuda &inData, const ImageBatchVarShapeDataStridedCuda &outData, int batch, + int channels, const cuda::Tensor1DWrap &inDiameter, const cuda::Tensor1DWrap &inSigmaColor, + const cuda::Tensor1DWrap &inSigmaSpace, cudaStream_t stream); // All templated functions instantiated here to remove one level of indirection that just hides the same lookup - // table in 5 parts + // table in 5 parts. The kCV_8S row is null because validation above rejects signed 8-bit input. static const bilateral_filter_var_shape_t funcs[5][6][4] = { { {BilateralFilterVarShapeCaller, BilateralFilterVarShapeCaller, BilateralFilterVarShapeCaller, BilateralFilterVarShapeCaller}, - {BilateralFilterVarShapeCaller, - BilateralFilterVarShapeCaller, - BilateralFilterVarShapeCaller, - BilateralFilterVarShapeCaller}, + {nullptr, nullptr, nullptr, nullptr}, {BilateralFilterVarShapeCaller, BilateralFilterVarShapeCaller, BilateralFilterVarShapeCaller, @@ -351,10 +632,7 @@ ErrorCode BilateralFilterVarShape::infer(const ImageBatchVarShapeDataStridedCuda BilateralFilterVarShapeCaller, BilateralFilterVarShapeCaller, BilateralFilterVarShapeCaller}, - {BilateralFilterVarShapeCaller, - BilateralFilterVarShapeCaller, - BilateralFilterVarShapeCaller, - BilateralFilterVarShapeCaller}, + {nullptr, nullptr, nullptr, nullptr}, {BilateralFilterVarShapeCaller, BilateralFilterVarShapeCaller, BilateralFilterVarShapeCaller, @@ -377,10 +655,7 @@ ErrorCode BilateralFilterVarShape::infer(const ImageBatchVarShapeDataStridedCuda BilateralFilterVarShapeCaller, BilateralFilterVarShapeCaller, BilateralFilterVarShapeCaller}, - {BilateralFilterVarShapeCaller, - BilateralFilterVarShapeCaller, - BilateralFilterVarShapeCaller, - BilateralFilterVarShapeCaller}, + {nullptr, nullptr, nullptr, nullptr}, {BilateralFilterVarShapeCaller, BilateralFilterVarShapeCaller, BilateralFilterVarShapeCaller, @@ -403,10 +678,7 @@ ErrorCode BilateralFilterVarShape::infer(const ImageBatchVarShapeDataStridedCuda BilateralFilterVarShapeCaller, BilateralFilterVarShapeCaller, BilateralFilterVarShapeCaller}, - {BilateralFilterVarShapeCaller, - BilateralFilterVarShapeCaller, - BilateralFilterVarShapeCaller, - BilateralFilterVarShapeCaller}, + {nullptr, nullptr, nullptr, nullptr}, {BilateralFilterVarShapeCaller, BilateralFilterVarShapeCaller, BilateralFilterVarShapeCaller, @@ -429,10 +701,7 @@ ErrorCode BilateralFilterVarShape::infer(const ImageBatchVarShapeDataStridedCuda BilateralFilterVarShapeCaller, BilateralFilterVarShapeCaller, BilateralFilterVarShapeCaller}, - {BilateralFilterVarShapeCaller, - BilateralFilterVarShapeCaller, - BilateralFilterVarShapeCaller, - BilateralFilterVarShapeCaller}, + {nullptr, nullptr, nullptr, nullptr}, {BilateralFilterVarShapeCaller, BilateralFilterVarShapeCaller, BilateralFilterVarShapeCaller, @@ -451,7 +720,39 @@ ErrorCode BilateralFilterVarShape::infer(const ImageBatchVarShapeDataStridedCuda BilateralFilterVarShapeCaller}, }, }; + static const bilateral_filter_var_shape_planar_t planarFuncs[5][6] = { + {BilateralFilterVarShapePlanarCaller, nullptr, + BilateralFilterVarShapePlanarCaller, + BilateralFilterVarShapePlanarCaller, + BilateralFilterVarShapePlanarCaller, + BilateralFilterVarShapePlanarCaller}, + {BilateralFilterVarShapePlanarCaller, nullptr, + BilateralFilterVarShapePlanarCaller, + BilateralFilterVarShapePlanarCaller, + BilateralFilterVarShapePlanarCaller, + BilateralFilterVarShapePlanarCaller}, + {BilateralFilterVarShapePlanarCaller, nullptr, + BilateralFilterVarShapePlanarCaller, + BilateralFilterVarShapePlanarCaller, + BilateralFilterVarShapePlanarCaller, + BilateralFilterVarShapePlanarCaller}, + {BilateralFilterVarShapePlanarCaller, nullptr, + BilateralFilterVarShapePlanarCaller, + BilateralFilterVarShapePlanarCaller, + BilateralFilterVarShapePlanarCaller, + BilateralFilterVarShapePlanarCaller}, + {BilateralFilterVarShapePlanarCaller, nullptr, + BilateralFilterVarShapePlanarCaller, + BilateralFilterVarShapePlanarCaller, + BilateralFilterVarShapePlanarCaller, + BilateralFilterVarShapePlanarCaller}, + }; + if (isPlanar) + { + return planarFuncs[borderMode][data_type](inData, outData, batch, channels, inDiameter, inSigmaColor, + inSigmaSpace, stream); + } funcs[borderMode][data_type][channels - 1](inData, outData, batch, inDiameter, inSigmaColor, inSigmaSpace, stream); return ErrorCode::SUCCESS; } diff --git a/src/cvcuda/priv/legacy/box_blur.cu b/src/cvcuda/priv/legacy/box_blur.cu index 0f8fd1d88..8b3898ecc 100644 --- a/src/cvcuda/priv/legacy/box_blur.cu +++ b/src/cvcuda/priv/legacy/box_blur.cu @@ -1,4 +1,4 @@ -/* Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +/* Copyright (c) 2021-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * * SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES * SPDX-License-Identifier: Apache-2.0 @@ -23,12 +23,15 @@ #include "CvCudaUtils.cuh" +#include #include #include #include #include +#include #include +#include using namespace nvcv::legacy::cuda_op; using namespace nvcv::legacy::helpers; @@ -43,6 +46,11 @@ static __forceinline__ __device__ _T limit(_T value, _T low, _T high) return value < low ? low : (value > high ? high : value); } +static bool IsPlanar(DataFormat format) +{ + return format == kNCHW || format == kCHW; +} + template static __global__ void render_p2p_kernel(SrcWrapper src, DstWrapper dst, int batch, int height, int width, int channel) { @@ -63,6 +71,23 @@ static __global__ void render_p2p_kernel(SrcWrapper src, DstWrapper dst, int bat } } +template +static __global__ void render_p2p_planar_kernel(SrcWrapper src, DstWrapper dst, int batch, int height, int width, + int channels) +{ + int ix = blockDim.x * blockIdx.x + threadIdx.x; + int iy = blockDim.y * blockIdx.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + + if (ix >= width || iy >= height || batch_idx >= batch) + return; + + for (int c = 0; c < channels; ++c) + { + *dst.ptr(batch_idx, c, iy, ix) = *src.ptr(batch_idx, c, iy, ix); + } +} + template static __global__ void render_blur_rgb_kernel(SrcWrapper src, DstWrapper dst, const BoxBlurCommand *commands, int num_command, int image_batch, int image_width, int image_height) @@ -73,20 +98,23 @@ static __global__ void render_blur_rgb_kernel(SrcWrapper src, DstWrapper dst, co if (box.batch_index >= image_batch) return; - __shared__ uchar3 crop[32][32]; - int ix = threadIdx.x; - int iy = threadIdx.y; + using ChannelType = std::remove_const_t; + using PixelType = nvcv::cuda::MakeType; + + __shared__ PixelType crop[32][32]; + int ix = threadIdx.x; + int iy = threadIdx.y; int boxwidth = box.bounding_right - box.bounding_left; int boxheight = box.bounding_bottom - box.bounding_top; int sx = limit((int)(ix / 32.0f * (float)boxwidth + 0.5f + box.bounding_left), 0, image_width); int sy = limit((int)(iy / 32.0f * (float)boxheight + 0.5f + box.bounding_top), 0, image_height); - crop[iy][ix] = *(uchar3 *)(src.ptr(box.batch_index, sy, sx, 0)); + crop[iy][ix] = *reinterpret_cast(src.ptr(box.batch_index, sy, sx, 0)); __syncthreads(); - uint3 color = make_uint3(0, 0, 0); - int n = 0; + int3 color = make_int3(0, 0, 0); + int n = 0; for (int i = -box.kernel_size / 2; i <= box.kernel_size / 2; ++i) { for (int j = -box.kernel_size / 2; j <= box.kernel_size / 2; ++j) @@ -104,7 +132,8 @@ static __global__ void render_blur_rgb_kernel(SrcWrapper src, DstWrapper dst, co } } __syncthreads(); - crop[iy][ix] = make_uchar3(color.x / n, color.y / n, color.z / n); + crop[iy][ix] = PixelType{static_cast(color.x / n), static_cast(color.y / n), + static_cast(color.z / n)}; __syncthreads(); int gap_width = (boxwidth + 31) / 32; @@ -121,8 +150,8 @@ static __global__ void render_blur_rgb_kernel(SrcWrapper src, DstWrapper dst, co int sy = (iy * gap_height + i) / (float)boxheight * 32; if (sx < 32 && sy < 32) { - auto &pix = crop[sy][sx]; - *(uchar3 *)(dst.ptr(box.batch_index, fy, fx, 0)) = make_uchar3(pix.x, pix.y, pix.z); + auto &pix = crop[sy][sx]; + *reinterpret_cast(dst.ptr(box.batch_index, fy, fx, 0)) = pix; } } } @@ -139,20 +168,24 @@ static __global__ void render_blur_rgba_kernel(SrcWrapper src, DstWrapper dst, c if (box.batch_index >= image_batch) return; - __shared__ uchar3 crop[32][32]; - int ix = threadIdx.x; - int iy = threadIdx.y; + using ChannelType = std::remove_const_t; + using Pixel3Type = nvcv::cuda::MakeType; + using Pixel4Type = nvcv::cuda::MakeType; + + __shared__ Pixel3Type crop[32][32]; + int ix = threadIdx.x; + int iy = threadIdx.y; int boxwidth = box.bounding_right - box.bounding_left; int boxheight = box.bounding_bottom - box.bounding_top; int sx = limit((int)(ix / 32.0f * (float)boxwidth + 0.5f + box.bounding_left), 0, image_width); int sy = limit((int)(iy / 32.0f * (float)boxheight + 0.5f + box.bounding_top), 0, image_height); - crop[iy][ix] = *(uchar3 *)(src.ptr(box.batch_index, sy, sx, 0)); + crop[iy][ix] = *reinterpret_cast(src.ptr(box.batch_index, sy, sx, 0)); __syncthreads(); - uint3 color = make_uint3(0, 0, 0); - int n = 0; + int3 color = make_int3(0, 0, 0); + int n = 0; for (int i = -box.kernel_size / 2; i <= box.kernel_size / 2; ++i) { for (int j = -box.kernel_size / 2; j <= box.kernel_size / 2; ++j) @@ -170,7 +203,8 @@ static __global__ void render_blur_rgba_kernel(SrcWrapper src, DstWrapper dst, c } } __syncthreads(); - crop[iy][ix] = make_uchar3(color.x / n, color.y / n, color.z / n); + crop[iy][ix] = Pixel3Type{static_cast(color.x / n), static_cast(color.y / n), + static_cast(color.z / n)}; __syncthreads(); int gap_width = (boxwidth + 31) / 32; @@ -187,8 +221,103 @@ static __global__ void render_blur_rgba_kernel(SrcWrapper src, DstWrapper dst, c int sy = (iy * gap_height + i) / (float)boxheight * 32; if (sx < 32 && sy < 32) { - auto &pix = crop[sy][sx]; - *(uchar4 *)(dst.ptr(box.batch_index, fy, fx, 0)) = make_uchar4(pix.x, pix.y, pix.z, 255); + auto &pix = crop[sy][sx]; + *reinterpret_cast(dst.ptr(box.batch_index, fy, fx, 0)) + = Pixel4Type{pix.x, pix.y, pix.z, nvcv::cuda::TypeTraits::max}; + } + } + } + } +} + +template +static __global__ void render_blur_planar_kernel(SrcWrapper src, DstWrapper dst, const BoxBlurCommand *commands, + int num_command, int image_batch, int image_width, int image_height, + int channels) +{ + if (blockIdx.x >= num_command) + return; + const BoxBlurCommand &box = commands[blockIdx.x]; + if (box.batch_index >= image_batch) + return; + + using ChannelType = std::remove_const_t; + + int plane = blockIdx.y; + if (plane >= channels) + return; + + int ix = threadIdx.x; + int iy = threadIdx.y; + + int boxwidth = box.bounding_right - box.bounding_left; + int boxheight = box.bounding_bottom - box.bounding_top; + + int gap_width = (boxwidth + 31) / 32; + int gap_height = (boxheight + 31) / 32; + + if (plane == 3) + { + for (int i = 0; i < gap_height; ++i) + { + for (int j = 0; j < gap_width; ++j) + { + int fx = ix * gap_width + j + box.bounding_left; + int fy = iy * gap_height + i + box.bounding_top; + if (fx >= 0 && fx < image_width && fy >= 0 && fy < image_height) + { + int sx = (ix * gap_width + j) / (float)boxwidth * 32; + int sy = (iy * gap_height + i) / (float)boxheight * 32; + if (sx < 32 && sy < 32) + { + *dst.ptr(box.batch_index, plane, fy, fx) = nvcv::cuda::TypeTraits::max; + } + } + } + } + return; + } + + __shared__ ChannelType crop[32][32]; + + int sx = limit((int)(ix / 32.0f * (float)boxwidth + 0.5f + box.bounding_left), 0, image_width); + int sy = limit((int)(iy / 32.0f * (float)boxheight + 0.5f + box.bounding_top), 0, image_height); + + crop[iy][ix] = *src.ptr(box.batch_index, plane, sy, sx); + __syncthreads(); + + int color = 0; + int n = 0; + for (int i = -box.kernel_size / 2; i <= box.kernel_size / 2; ++i) + { + for (int j = -box.kernel_size / 2; j <= box.kernel_size / 2; ++j) + { + int u = i + iy; + int v = j + ix; + if (u >= 0 && u < 32 && v >= 0 && v < 32) + { + color += crop[u][v]; + n++; + } + } + } + __syncthreads(); + crop[iy][ix] = static_cast(color / n); + __syncthreads(); + + for (int i = 0; i < gap_height; ++i) + { + for (int j = 0; j < gap_width; ++j) + { + int fx = ix * gap_width + j + box.bounding_left; + int fy = iy * gap_height + i + box.bounding_top; + if (fx >= 0 && fx < image_width && fy >= 0 && fy < image_height) + { + int sx = (ix * gap_width + j) / (float)boxwidth * 32; + int sy = (iy * gap_height + i) / (float)boxheight * 32; + if (sx < 32 && sy < 32) + { + *dst.ptr(box.batch_index, plane, fy, fx) = crop[sy][sx]; } } } @@ -218,9 +347,9 @@ static void cuosd_apply(cuOSDContext_t context, cudaStream_t stream) template inline void RenderBlur_RGB(SrcWrap src, DstWrap dst, const cuda_op::DataShape &inputShape, cuOSDContext_t context, - cudaStream_t stream) + cudaStream_t stream, bool skipCopy) { - if (src.ptr(0) != dst.ptr(0)) + if (!skipCopy && src.ptr(0) != dst.ptr(0)) { dim3 blockSize(32, 32); dim3 gridSize(divUp(int(inputShape.W + 1), (int)blockSize.x), divUp(int(inputShape.H + 1), (int)blockSize.y), @@ -243,8 +372,9 @@ inline void RenderBlur_RGB(SrcWrap src, DstWrap dst, const cuda_op::DataShape &i } } +template inline ErrorCode ApplyBoxBlur_RGB(const nvcv::TensorDataStridedCuda &inData, const nvcv::TensorDataStridedCuda &outData, - cuOSDContext_t context, cudaStream_t stream) + cuOSDContext_t context, cudaStream_t stream, bool skipCopy) { auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(inData); NVCV_ASSERT(inAccess); @@ -270,10 +400,10 @@ inline ErrorCode ApplyBoxBlur_RGB(const nvcv::TensorDataStridedCuda &inData, con if (std::max(srcMaxStride, dstMaxStride) <= cuda::TypeTraits::max) { - auto src = nvcv::cuda::CreateTensorWrapNHWC(inData); - auto dst = nvcv::cuda::CreateTensorWrapNHWC(outData); + auto src = nvcv::cuda::CreateTensorWrapNHWC(inData); + auto dst = nvcv::cuda::CreateTensorWrapNHWC(outData); - RenderBlur_RGB(src, dst, inputShape, context, stream); + RenderBlur_RGB(src, dst, inputShape, context, stream, skipCopy); return ErrorCode::SUCCESS; } else @@ -285,9 +415,9 @@ inline ErrorCode ApplyBoxBlur_RGB(const nvcv::TensorDataStridedCuda &inData, con template inline void RenderBlur_RGBA(SrcWrap src, DstWrap dst, const cuda_op::DataShape &inputShape, cuOSDContext_t context, - cudaStream_t stream) + cudaStream_t stream, bool skipCopy) { - if (src.ptr(0) != dst.ptr(0)) + if (!skipCopy && src.ptr(0) != dst.ptr(0)) { dim3 blockSize(32, 32); dim3 gridSize(divUp(int(inputShape.W + 1), (int)blockSize.x), divUp(int(inputShape.H + 1), (int)blockSize.y), @@ -310,9 +440,10 @@ inline void RenderBlur_RGBA(SrcWrap src, DstWrap dst, const cuda_op::DataShape & } } +template inline ErrorCode ApplyBoxBlur_RGBA(const nvcv::TensorDataStridedCuda &inData, const nvcv::TensorDataStridedCuda &outData, cuOSDContext_t context, - cudaStream_t stream) + cudaStream_t stream, bool skipCopy) { auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(inData); NVCV_ASSERT(inAccess); @@ -338,10 +469,10 @@ inline ErrorCode ApplyBoxBlur_RGBA(const nvcv::TensorDataStridedCuda &inData, if (std::max(srcMaxStride, dstMaxStride) <= cuda::TypeTraits::max) { - auto src = nvcv::cuda::CreateTensorWrapNHWC(inData); - auto dst = nvcv::cuda::CreateTensorWrapNHWC(outData); + auto src = nvcv::cuda::CreateTensorWrapNHWC(inData); + auto dst = nvcv::cuda::CreateTensorWrapNHWC(outData); - RenderBlur_RGBA(src, dst, inputShape, context, stream); + RenderBlur_RGBA(src, dst, inputShape, context, stream, skipCopy); return ErrorCode::SUCCESS; } else @@ -351,6 +482,77 @@ inline ErrorCode ApplyBoxBlur_RGBA(const nvcv::TensorDataStridedCuda &inData, } } +template +inline void RenderBlur_Planar(SrcWrap src, DstWrap dst, const cuda_op::DataShape &inputShape, cuOSDContext_t context, + cudaStream_t stream, bool skipCopy) +{ + if (!skipCopy && src.ptr(0, 0, 0, 0) != dst.ptr(0, 0, 0, 0)) + { + dim3 blockSize(32, 32); + dim3 gridSize(divUp(int(inputShape.W + 1), (int)blockSize.x), divUp(int(inputShape.H + 1), (int)blockSize.y), + inputShape.N); + + render_p2p_planar_kernel<<>>(src, dst, inputShape.N, inputShape.H, inputShape.W, + inputShape.C); + checkKernelErrors(); + } + + if (context->blur_commands.size() > 0) + { + dim3 blockSize(32, 32); + dim3 gridSize(context->blur_commands.size(), inputShape.C); + + render_blur_planar_kernel<<>>( + src, dst, context->gpu_blur_commands ? context->gpu_blur_commands->device() : nullptr, + context->blur_commands.size(), inputShape.N, inputShape.W, inputShape.H, inputShape.C); + checkKernelErrors(); + } +} + +template +inline ErrorCode ApplyBoxBlur_Planar(const nvcv::TensorDataStridedCuda &inData, + const nvcv::TensorDataStridedCuda &outData, cuOSDContext_t context, + cudaStream_t stream, bool skipCopy) +{ + auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(inData); + NVCV_ASSERT(inAccess); + + cuda_op::DataShape inputShape = helpers::GetLegacyDataShape(inAccess->infoShape()); + + auto outAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(outData); + NVCV_ASSERT(outAccess); + + cuda_op::DataShape outputShape = helpers::GetLegacyDataShape(outAccess->infoShape()); + + if (outputShape.H != inputShape.H || outputShape.W != inputShape.W || outputShape.N != inputShape.N + || outputShape.C != inputShape.C || (outputShape.C != 3 && outputShape.C != 4)) + { + LOG_ERROR("Invalid output shape " << outputShape); + return ErrorCode::INVALID_DATA_SHAPE; + } + + cuosd_apply(context, stream); + + int64_t maxStride + = std::max({inAccess->sampleStride() * inAccess->numSamples(), inAccess->chStride() * inAccess->numChannels(), + inAccess->rowStride(), outAccess->sampleStride() * outAccess->numSamples(), + outAccess->chStride() * outAccess->numChannels(), outAccess->rowStride()}); + + if (maxStride <= cuda::TypeTraits::max) + { + auto src = nvcv::cuda::CreateTensorWrapNCHW(inData); + auto dst = nvcv::cuda::CreateTensorWrapNCHW(outData); + + RenderBlur_Planar(src, dst, inputShape, context, stream, skipCopy); + return ErrorCode::SUCCESS; + } + else + { + LOG_ERROR("Input or output stride exceeds " << cuda::TypeTraits::max << ". Tensor is too large."); + return ErrorCode::INVALID_PARAMETER; + } +} + static ErrorCode cuosd_draw_boxblur(cuOSDContext_t context, int width, int height, NVCVBlurBoxesImpl *bboxes) { for (int n = 0; n < bboxes->batch(); n++) @@ -413,15 +615,22 @@ BoxBlur::~BoxBlur() } } +size_t BoxBlur::calBufferSize(DataShape max_input_shape, DataShape max_output_shape, DataType max_data_type) +{ + return CudaBaseOp::calBufferSize(max_input_shape, max_output_shape, max_data_type); +} + ErrorCode BoxBlur::infer(const nvcv::TensorDataStridedCuda &inData, const nvcv::TensorDataStridedCuda &outData, - NVCVBlurBoxesI bboxes, cudaStream_t stream) + NVCVBlurBoxesI bboxes, cudaStream_t stream, bool skipCopy) { cuda_op::DataFormat input_format = GetLegacyDataFormat(inData.layout()); cuda_op::DataFormat output_format = GetLegacyDataFormat(outData.layout()); - if (!(input_format == kNHWC || input_format == kHWC) || !(output_format == kNHWC || output_format == kHWC)) + if (!(input_format == kNHWC || input_format == kHWC || input_format == kNCHW || input_format == kCHW) + || !(output_format == kNHWC || output_format == kHWC || output_format == kNCHW || output_format == kCHW) + || IsPlanar(input_format) != IsPlanar(output_format)) { - LOG_ERROR("Invliad DataFormat both Input and Output must be kNHWC or kHWC"); + LOG_ERROR("Invalid DataFormat both Input and Output must be kNHWC, kHWC, kNCHW or kCHW"); return ErrorCode::INVALID_DATA_FORMAT; } @@ -432,6 +641,14 @@ ErrorCode BoxBlur::infer(const nvcv::TensorDataStridedCuda &inData, const nvcv:: return ErrorCode::INVALID_DATA_FORMAT; } + const cuda_op::DataType data_type = GetLegacyDataType(inData.dtype()); + + if (!(data_type == kCV_8U || data_type == kCV_8S)) + { + LOG_ERROR("Invalid DataType " << data_type); + return ErrorCode::INVALID_DATA_TYPE; + } + auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(inData); if (!inAccess) { @@ -443,7 +660,7 @@ ErrorCode BoxBlur::infer(const nvcv::TensorDataStridedCuda &inData, const nvcv:: int rows = inAccess->numRows(); int cols = inAccess->numCols(); - if (channels > 4 || channels < 1) + if (channels > 4 || channels < 3) { LOG_ERROR("Invalid channel number ch = " << channels); return ErrorCode::INVALID_DATA_SHAPE; @@ -469,15 +686,25 @@ ErrorCode BoxBlur::infer(const nvcv::TensorDataStridedCuda &inData, const nvcv:: } typedef ErrorCode (*func_t)(const nvcv::TensorDataStridedCuda &inData, const nvcv::TensorDataStridedCuda &outData, - cuOSDContext_t context, cudaStream_t stream); + cuOSDContext_t context, cudaStream_t stream, bool skipCopy); - static const func_t funcs[] = { - ApplyBoxBlur_RGB, - ApplyBoxBlur_RGBA, + static const func_t funcs[][2] = { + {ApplyBoxBlur_RGB, ApplyBoxBlur_RGBA}, + { ApplyBoxBlur_RGB, ApplyBoxBlur_RGBA}, }; - int type_idx = channels - 3; - ErrorCode status = funcs[type_idx](inData, outData, m_context, stream); + const int dataTypeIdx = data_type == kCV_8S ? 1 : 0; + ErrorCode status = ErrorCode::SUCCESS; + if (IsPlanar(input_format)) + { + status = dataTypeIdx == 0 ? ApplyBoxBlur_Planar(inData, outData, m_context, stream, skipCopy) + : ApplyBoxBlur_Planar(inData, outData, m_context, stream, skipCopy); + } + else + { + const int channelIdx = channels - 3; + status = funcs[dataTypeIdx][channelIdx](inData, outData, m_context, stream, skipCopy); + } m_context->blur_commands.clear(); // Clear the command buffer so next render does not contain previous boxes. return status; } diff --git a/src/cvcuda/priv/legacy/calc_hist.cu b/src/cvcuda/priv/legacy/calc_hist.cu index 63ced7300..099f660f8 100644 --- a/src/cvcuda/priv/legacy/calc_hist.cu +++ b/src/cvcuda/priv/legacy/calc_hist.cu @@ -1,4 +1,4 @@ -/* Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +/* Copyright (c) 2021-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * * SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES * SPDX-License-Identifier: Apache-2.0 @@ -18,6 +18,7 @@ * limitations under the License. */ +#include "../CudaDeviceUtils.hpp" #include "CvCudaLegacy.h" #include "CvCudaLegacyHelpers.hpp" @@ -26,11 +27,27 @@ #include #include +#include + using namespace nvcv::legacy::helpers; using namespace nvcv::legacy::cuda_op; +namespace { + +constexpr bool UseOnePixelHistogramKernel(int sm, int64_t numPixels) +{ + return sm == 75 && numPixels <= std::numeric_limits::max(); +} + +static_assert(UseOnePixelHistogramKernel(75, std::numeric_limits::max())); +static_assert(!UseOnePixelHistogramKernel(75, static_cast(std::numeric_limits::max()) + 1)); +static_assert(!UseOnePixelHistogramKernel(80, 1)); +static_assert(!UseOnePixelHistogramKernel(90, 1)); + +} // namespace + template -__global__ void calc_hist_kernel(const SrcWrapper src, DstWrapper histogram, int numPixels, int width) +__global__ void calc_hist_one_pixel_kernel(const SrcWrapper src, DstWrapper histogram, int numPixels, int width) { extern __shared__ int shist[]; //size 256 * sizeof(int) @@ -63,12 +80,13 @@ __global__ void calc_hist_kernel(const SrcWrapper src, DstWrapper histogram, int } template -__global__ void calc_hist_kernel(const SrcWrapper src, DstWrapper histogram, MaskWrapper mask, int numPixels, int width) +__global__ void calc_hist_one_pixel_kernel(const SrcWrapper src, DstWrapper histogram, MaskWrapper mask, int numPixels, + int width) { extern __shared__ int shist[]; //size 256 * sizeof(int) int batch_idx = get_batch_idx(); //Z this will be the batch index. int tid = blockIdx.x * blockDim.x + threadIdx.x; // this is the thread index in the block - int x = tid % width; // this is the x index of the pixel assigned to this tid + int x = tid % width; // this is the x index in the pixel assigned to this tid int y = tid / width; // this is the y index in the pixel assigned to this tid int3 coord{x, y, batch_idx}; @@ -95,6 +113,83 @@ __global__ void calc_hist_kernel(const SrcWrapper src, DstWrapper histogram, Mas } } +template +__global__ void calc_hist_kernel(const SrcWrapper src, DstWrapper histogram, int height, int width) +{ + extern __shared__ int shist[]; //size 256 * sizeof(int) + + int batch_idx = get_batch_idx(); //Z this will be the batch index. + int x = (blockIdx.x * blockDim.x + threadIdx.x) * 16; + int y = blockIdx.y * blockDim.y + threadIdx.y; + + // histogram index only used to sub the output histogram + int sub_tid = threadIdx.y * blockDim.x + threadIdx.x; //histogram index + + shist[sub_tid] = 0; //initialize the histogram for this bin in this block + + __syncthreads(); + + if (y < height) + { +#pragma unroll + for (int i = 0; i < 16; ++i) + { + if (x + i < width) + { + atomicAdd(&shist[src[int3{x + i, y, batch_idx}]], 1); + } + } + } + __syncthreads(); // wait for all of the threads in this block to finish + + int hist_val = shist[sub_tid]; // get the bin value for this thread + + // this is the output histogram must be init to and atomicly added to. + if (hist_val > 0) + { + atomicAdd(histogram.ptr(batch_idx, sub_tid), hist_val); + } +} + +template +__global__ void calc_hist_kernel(const SrcWrapper src, DstWrapper histogram, MaskWrapper mask, int height, int width) +{ + extern __shared__ int shist[]; //size 256 * sizeof(int) + int batch_idx = get_batch_idx(); //Z this will be the batch index. + int x = (blockIdx.x * blockDim.x + threadIdx.x) * 16; + int y = blockIdx.y * blockDim.y + threadIdx.y; + + // histogram index only used to sub the output histogram + int sub_tid = threadIdx.y * blockDim.x + threadIdx.x; //histogram index + + shist[sub_tid] = 0; //initialize the histogram for this bin in this block + + __syncthreads(); + + if (y < height) + { +#pragma unroll + for (int i = 0; i < 16; ++i) + { + if (x + i < width) + { + int3 coord{x + i, y, batch_idx}; + if (mask[coord]) + atomicAdd(&shist[src[coord]], 1); + } + } + } + __syncthreads(); // wait for all of the threads in this block to finish + + int hist_val = shist[sub_tid]; // get the bin value for this thread + + // this is the output histogram must be init to and atomicly added to. + if (hist_val > 0) + { + atomicAdd(histogram.ptr(batch_idx, sub_tid), hist_val); + } +} + namespace nvcv::legacy::cuda_op { ErrorCode Histogram::infer(const TensorDataStridedCuda &inData, OptionalTensorConstRef mask, @@ -103,6 +198,7 @@ ErrorCode Histogram::infer(const TensorDataStridedCuda &inData, OptionalTensorCo DataFormat input_format = GetLegacyDataFormat(inData.layout()); DataFormat histo_format = GetLegacyDataFormat(histogram.layout()); DataType data_type = GetLegacyDataType(inData.dtype()); + DataType histo_type = GetLegacyDataType(histogram.dtype()); auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(inData); NVCV_ASSERT(inAccess); @@ -117,7 +213,7 @@ ErrorCode Histogram::infer(const TensorDataStridedCuda &inData, OptionalTensorCo if (!(histo_format == kNHWC || histo_format == kHWC)) { - LOG_ERROR("Invalid histogram DataFormat for calculating histogram " << input_format); + LOG_ERROR("Invalid histogram DataFormat for calculating histogram " << histo_format); return ErrorCode::INVALID_DATA_FORMAT; } @@ -127,6 +223,12 @@ ErrorCode Histogram::infer(const TensorDataStridedCuda &inData, OptionalTensorCo return ErrorCode::INVALID_DATA_TYPE; } + if (histo_type != kCV_32S) + { + LOG_ERROR("Invalid histogram DataType for calculating histogram " << histo_type); + return ErrorCode::INVALID_DATA_TYPE; + } + DataShape input_shape = GetLegacyDataShape(inAccess->infoShape()); DataShape histo_shape = GetLegacyDataShape(histoAccess->infoShape()); @@ -137,6 +239,18 @@ ErrorCode Histogram::infer(const TensorDataStridedCuda &inData, OptionalTensorCo return ErrorCode::INVALID_DATA_SHAPE; } + if (histo_shape.W < 256) + { + LOG_ERROR("Histogram tensor must have at least 256 columns, got " << histo_shape.W); + return ErrorCode::INVALID_DATA_SHAPE; + } + + if (histo_shape.C != 1) + { + LOG_ERROR("Invalid histogram channel number " << histo_shape.C); + return ErrorCode::INVALID_DATA_SHAPE; + } + int batch = input_shape.N; int channels = input_shape.C; int rows = input_shape.H; @@ -149,22 +263,48 @@ ErrorCode Histogram::infer(const TensorDataStridedCuda &inData, OptionalTensorCo } // clear the histogram since we will just add to it only 1 sample in thi HW tensor by definition - checkCudaErrors(cudaMemset2DAsync(histoAccess->sampleData(0), histoAccess->rowStride(), 0, histoAccess->numCols(), - histoAccess->numRows(), stream)); + checkCudaErrors(cudaMemset2DAsync(histoAccess->sampleData(0), histoAccess->rowStride(), 0, + histoAccess->numCols() * histoAccess->colStride(), histoAccess->numRows(), + stream)); auto src = nvcv::cuda::CreateTensorWrapNHW(inData); auto histo = nvcv::cuda::Tensor2DWrap(histogram); - int threads_block = 256; + const int64_t num_pixels = static_cast(rows) * cols; + + int sm = 0; + NVCV_CHECK_THROW(cvcuda::priv::GetCurrentDeviceSM(sm)); + const bool use_one_pixel_kernel = UseOnePixelHistogramKernel(sm, num_pixels); + + constexpr int one_pixel_block_size = 256; + int one_pixel_num_pixels = 0; + dim3 one_pixel_grid_size; + if (use_one_pixel_kernel) + { + one_pixel_num_pixels = static_cast(num_pixels); + one_pixel_grid_size.x + = one_pixel_num_pixels / one_pixel_block_size + (one_pixel_num_pixels % one_pixel_block_size != 0); + one_pixel_grid_size.z = batch; + } - // Setup 1 thread / pixel slpit into blocks of 256 threads for local binning. - // grid in y could be colors. - dim3 grid_size((rows * cols + threads_block - 1) / threads_block, 1, batch); + dim3 block_size(32, 8); + + // Amortize each block-local histogram across sixteen horizontal pixels per lane. + dim3 grid_size((cols + block_size.x * 16 - 1) / (block_size.x * 16), (rows + block_size.y - 1) / block_size.y, + batch); int smem_size = 256 * sizeof(int); if (mask == nullptr) { - calc_hist_kernel<<>>(src, histo, rows * cols, cols); + if (use_one_pixel_kernel) + { + calc_hist_one_pixel_kernel<<>>( + src, histo, one_pixel_num_pixels, cols); + } + else + { + calc_hist_kernel<<>>(src, histo, rows, cols); + } checkKernelErrors(); } else @@ -176,7 +316,23 @@ ErrorCode Histogram::infer(const TensorDataStridedCuda &inData, OptionalTensorCo auto inMask = nvcv::TensorDataAccessStridedImagePlanar::Create(*maskTensorData); NVCV_ASSERT(inMask); - if (GetLegacyDataShape(inMask->infoShape()) != GetLegacyDataShape(inAccess->infoShape())) + DataFormat mask_format = GetLegacyDataFormat(maskTensorData->layout()); + DataType mask_type = GetLegacyDataType(maskTensorData->dtype()); + DataShape mask_shape = GetLegacyDataShape(inMask->infoShape()); + + if (!(mask_format == kNHWC || mask_format == kHWC)) + { + LOG_ERROR("Invalid mask DataFormat for calculating histogram " << mask_format); + return ErrorCode::INVALID_DATA_FORMAT; + } + + if (mask_type != kCV_8U) + { + LOG_ERROR("Invalid mask DataType for calculating histogram " << mask_type); + return ErrorCode::INVALID_DATA_TYPE; + } + + if (mask_shape != input_shape) { LOG_ERROR("Mask tensor does not match input tensor shape"); return ErrorCode::INVALID_DATA_SHAPE; @@ -184,7 +340,15 @@ ErrorCode Histogram::infer(const TensorDataStridedCuda &inData, OptionalTensorCo auto maskAccess = nvcv::cuda::CreateTensorWrapNHW(*maskTensorData); - calc_hist_kernel<<>>(src, histo, maskAccess, rows * cols, cols); + if (use_one_pixel_kernel) + { + calc_hist_one_pixel_kernel<<>>( + src, histo, maskAccess, one_pixel_num_pixels, cols); + } + else + { + calc_hist_kernel<<>>(src, histo, maskAccess, rows, cols); + } checkKernelErrors(); } diff --git a/src/cvcuda/priv/legacy/center_crop.cu b/src/cvcuda/priv/legacy/center_crop.cu index 38e2fc4f0..b36dcc43d 100644 --- a/src/cvcuda/priv/legacy/center_crop.cu +++ b/src/cvcuda/priv/legacy/center_crop.cu @@ -1,4 +1,4 @@ -/* Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +/* Copyright (c) 2021-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * * SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES * SPDX-License-Identifier: Apache-2.0 @@ -93,6 +93,15 @@ ErrorCode CenterCrop::infer(const TensorDataStridedCuda &inData, const TensorDat return ErrorCode::INVALID_DATA_FORMAT; } + const cuda_op::DataType data_type = GetLegacyDataType(inData.dtype()); + + if (!(data_type == kCV_8U || data_type == kCV_8S || data_type == kCV_16U || data_type == kCV_16S + || data_type == kCV_16F || data_type == kCV_32S || data_type == kCV_32F || data_type == kCV_64F)) + { + LOG_ERROR("Invalid DataType " << data_type); + return ErrorCode::INVALID_DATA_TYPE; + } + auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(inData); if (!inAccess) { diff --git a/src/cvcuda/priv/legacy/channel_reorder_var_shape.cu b/src/cvcuda/priv/legacy/channel_reorder_var_shape.cu index c2c386747..9826b71db 100644 --- a/src/cvcuda/priv/legacy/channel_reorder_var_shape.cu +++ b/src/cvcuda/priv/legacy/channel_reorder_var_shape.cu @@ -26,10 +26,32 @@ #include +#include +#include + #define BLOCK 32 namespace nvcv::legacy::cuda_op { +namespace { + +static bool IsPlanar(DataFormat format) +{ + return format == kNCHW || format == kCHW; +} + +static bool SameLayoutFamily(DataFormat lhs, DataFormat rhs) +{ + return IsPlanar(lhs) == IsPlanar(rhs); +} + +static DataFormat ImageLayout(nvcv::ImageFormat fmt, int numImages) +{ + return helpers::GetLegacyDataFormat(fmt.numChannels(), fmt.numPlanes(), numImages); +} + +} // namespace + template __global__ void channel_reorder_kernel(const cuda::ImageBatchVarShapeWrapNHWC src, cuda::ImageBatchVarShapeWrapNHWC dst, const cuda::Tensor2DWrap orders) @@ -61,15 +83,15 @@ __global__ void channel_reorder_kernel(const cuda::ImageBatchVarShapeWrapNHWC void reorder(const ImageBatchVarShapeDataStridedCuda &inData, const ImageBatchVarShapeDataStridedCuda &outData, - const TensorDataStridedCuda &orderData, int numChannels, cudaStream_t stream) + const TensorDataStridedCuda &orderData, int numSrcChannels, int numDstChannels, cudaStream_t stream) { int batch_size = inData.numImages(); dim3 blockSize(BLOCK, BLOCK / 4, 1); dim3 gridSize(divUp(inData.maxSize().w, blockSize.x), divUp(inData.maxSize().h, blockSize.y), batch_size); - cuda::ImageBatchVarShapeWrapNHWC src_ptr(inData, numChannels); - cuda::ImageBatchVarShapeWrapNHWC dst_ptr(outData, numChannels); + cuda::ImageBatchVarShapeWrapNHWC src_ptr(inData, numSrcChannels); + cuda::ImageBatchVarShapeWrapNHWC dst_ptr(outData, numDstChannels); cuda::Tensor2DWrap order_ptr(orderData); channel_reorder_kernel<<>>(src_ptr, dst_ptr, order_ptr); @@ -82,6 +104,203 @@ void reorder(const ImageBatchVarShapeDataStridedCuda &inData, const ImageBatchVa #endif } +template +__global__ void channel_reorder_planar_kernel(const cuda::ImageBatchVarShapeWrap src, + cuda::ImageBatchVarShapeWrap dst, const cuda::Tensor2DWrap orders, + int numSrcChannels, int numDstChannels) +{ + const int dst_x = blockIdx.x * blockDim.x + threadIdx.x; + const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + int out_height = dst.height(batch_idx), out_width = dst.width(batch_idx); + if (dst_x >= out_width || dst_y >= out_height) + return; + + const int *chOrder = orders.ptr(batch_idx); + + for (int ch = 0; ch < numDstChannels; ch++) + { + int src_ch = chOrder[ch]; + if (src_ch < 0) + { + *dst.ptr(batch_idx, ch, dst_y, dst_x) = 0; + } + else + { + NVCV_CUDA_ASSERT(0 <= src_ch && src_ch < numSrcChannels, "Index to source channel %d is out of bounds (%d)", + src_ch, numSrcChannels); + *dst.ptr(batch_idx, ch, dst_y, dst_x) = *src.ptr(batch_idx, src_ch, dst_y, dst_x); + } + } +} + +template +constexpr uintptr_t kPackAlignmentMask = (sizeof(PackT) == sizeof(uint3) ? sizeof(uint) : sizeof(PackT)) - 1; + +template +__global__ void channel_reorder_u8_kernel(const SrcWrapper src, DstWrapper dst, const cuda::Tensor2DWrap orders) +{ + static constexpr int kNix = 4; + + const int dst_x0 = (blockIdx.x * blockDim.x + threadIdx.x) * kNix; + const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + const int out_height = dst.height(batch_idx); + const int out_width = dst.width(batch_idx); + if (dst_x0 >= out_width || dst_y >= out_height) + return; + + int chOrder[NumChannels]; +#pragma unroll + for (int ch = 0; ch < NumChannels; ++ch) + { + chOrder[ch] = orders.ptr(batch_idx)[ch]; + NVCV_CUDA_ASSERT(chOrder[ch] < NumChannels, "Index to source channel %d is out of bounds (%d)", chOrder[ch], + NumChannels); + } + + if constexpr (IsPlanar) + { + using PackT = uint; + +#pragma unroll + for (int ch = 0; ch < NumChannels; ++ch) + { + const int src_ch = chOrder[ch]; + uchar *dp = dst.ptr(batch_idx, ch, dst_y, dst_x0); + + if (dst_x0 + kNix <= out_width && (reinterpret_cast(dp) & kPackAlignmentMask) == 0) + { + PackT out{}; + if (src_ch >= 0) + { + const uchar *sp = src.ptr(batch_idx, src_ch, dst_y, dst_x0); + if ((reinterpret_cast(sp) & kPackAlignmentMask) == 0) + { + out = *reinterpret_cast(sp); + *reinterpret_cast(dp) = out; + continue; + } + } + else + { + *reinterpret_cast(dp) = out; + continue; + } + } + +#pragma unroll + for (int i = 0; i < kNix; ++i) + { + if (dst_x0 + i < out_width) + { + dp[i] = src_ch < 0 ? 0 : *src.ptr(batch_idx, src_ch, dst_y, dst_x0 + i); + } + } + } + } + else + { + using PackT = std::conditional_t; + + const uchar *sp = src.ptr(batch_idx, dst_y, dst_x0); + uchar *dp = dst.ptr(batch_idx, dst_y, dst_x0); + if (dst_x0 + kNix <= out_width && (reinterpret_cast(sp) & kPackAlignmentMask) == 0 + && (reinterpret_cast(dp) & kPackAlignmentMask) == 0) + { + PackT in = *reinterpret_cast(sp); + PackT out; + + const uchar *inBytes = reinterpret_cast(&in); + uchar *outBytes = reinterpret_cast(&out); +#pragma unroll + for (int i = 0; i < kNix; ++i) + { +#pragma unroll + for (int ch = 0; ch < NumChannels; ++ch) + { + const int src_ch = chOrder[ch]; + outBytes[i * NumChannels + ch] = src_ch < 0 ? 0 : inBytes[i * NumChannels + src_ch]; + } + } + *reinterpret_cast(dp) = out; + } + else + { +#pragma unroll + for (int i = 0; i < kNix; ++i) + { + if (dst_x0 + i < out_width) + { +#pragma unroll + for (int ch = 0; ch < NumChannels; ++ch) + { + const int src_ch = chOrder[ch]; + dp[i * NumChannels + ch] = src_ch < 0 ? 0 : *src.ptr(batch_idx, dst_y, dst_x0 + i, src_ch); + } + } + } + } + } +} + +template +void reorder_u8(const ImageBatchVarShapeDataStridedCuda &inData, const ImageBatchVarShapeDataStridedCuda &outData, + const TensorDataStridedCuda &orderData, cudaStream_t stream) +{ + static constexpr int kNix = 4; + + int batch_size = inData.numImages(); + const auto maxSize = IsPlanar ? outData.maxSize() : inData.maxSize(); + dim3 blockSize(BLOCK, BLOCK / 4, 1); + dim3 gridSize(divUp(maxSize.w, blockSize.x * kNix), divUp(maxSize.h, blockSize.y), batch_size); + cuda::Tensor2DWrap order_ptr(orderData); + + if constexpr (IsPlanar) + { + cuda::ImageBatchVarShapeWrap src_ptr(inData); + cuda::ImageBatchVarShapeWrap dst_ptr(outData); + channel_reorder_u8_kernel<<>>(src_ptr, dst_ptr, order_ptr); + } + else + { + cuda::ImageBatchVarShapeWrapNHWC src_ptr(inData, NumChannels); + cuda::ImageBatchVarShapeWrapNHWC dst_ptr(outData, NumChannels); + channel_reorder_u8_kernel<<>>(src_ptr, dst_ptr, order_ptr); + } + + checkKernelErrors(); + +#ifdef CUDA_DEBUG_LOG + checkCudaErrors(cudaDeviceSynchronize()); + checkCudaErrors(cudaGetLastError()); +#endif +} + +template +void reorder_planar(const ImageBatchVarShapeDataStridedCuda &inData, const ImageBatchVarShapeDataStridedCuda &outData, + const TensorDataStridedCuda &orderData, int numSrcChannels, int numDstChannels, cudaStream_t stream) +{ + int batch_size = inData.numImages(); + + dim3 blockSize(BLOCK, BLOCK / 4, 1); + dim3 gridSize(divUp(outData.maxSize().w, blockSize.x), divUp(outData.maxSize().h, blockSize.y), batch_size); + + cuda::ImageBatchVarShapeWrap src_ptr(inData); + cuda::ImageBatchVarShapeWrap dst_ptr(outData); + cuda::Tensor2DWrap order_ptr(orderData); + + channel_reorder_planar_kernel + <<>>(src_ptr, dst_ptr, order_ptr, numSrcChannels, numDstChannels); + + checkKernelErrors(); + +#ifdef CUDA_DEBUG_LOG + checkCudaErrors(cudaDeviceSynchronize()); + checkCudaErrors(cudaGetLastError()); +#endif +} + ErrorCode ChannelReorderVarShape::infer(const ImageBatchVarShapeDataStridedCuda &inData, const ImageBatchVarShapeDataStridedCuda &outData, const TensorDataStridedCuda &orderData, cudaStream_t stream) @@ -98,14 +317,43 @@ ErrorCode ChannelReorderVarShape::infer(const ImageBatchVarShapeDataStridedCuda return ErrorCode::SUCCESS; } - DataType data_type; - int channels; + DataType data_type; + DataFormat input_format; + DataFormat output_format; + int channels; + int outChannels; { nvcv::ImageFormat fmt(inData.hostFormatList()[0]); - data_type = helpers::GetLegacyDataType(fmt); - channels = fmt.numChannels(); + nvcv::ImageFormat outFmt(outData.hostFormatList()[0]); + data_type = helpers::GetLegacyDataType(fmt); + input_format = ImageLayout(fmt, inData.numImages()); + output_format = ImageLayout(outFmt, outData.numImages()); + channels = fmt.numChannels(); + outChannels = outFmt.numChannels(); + } + + if (!(input_format == kNHWC || input_format == kHWC || input_format == kNCHW || input_format == kCHW)) + { + LOG_ERROR("Invalid input DataFormat " << input_format + << ", the valid DataFormats are: \"NHWC\", \"HWC\", \"NCHW\", \"CHW\""); + return ErrorCode::INVALID_DATA_FORMAT; + } + + if (!(output_format == kNHWC || output_format == kHWC || output_format == kNCHW || output_format == kCHW)) + { + LOG_ERROR("Invalid output DataFormat " << output_format + << ", the valid DataFormats are: \"NHWC\", \"HWC\", \"NCHW\", \"CHW\""); + return ErrorCode::INVALID_DATA_FORMAT; + } + + if (!SameLayoutFamily(input_format, output_format)) + { + LOG_ERROR("Invalid DataFormat between input (" << input_format << ") and output (" << output_format << ")"); + return ErrorCode::INVALID_DATA_FORMAT; } + const bool isPlanar = IsPlanar(input_format); + if (!(data_type == kCV_8U || data_type == kCV_16U || data_type == kCV_16S || data_type == kCV_32S || data_type == kCV_32F)) { @@ -148,15 +396,20 @@ ErrorCode ChannelReorderVarShape::infer(const ImageBatchVarShapeDataStridedCuda nvcv::ImageFormat inFmt(inData.hostFormatList()[i]); nvcv::ImageFormat outFmt(outData.hostFormatList()[i]); - if (inFmt.numPlanes() != 1) + DataFormat imageInputFormat = ImageLayout(inFmt, inData.numImages()); + DataFormat imageOutputFormat = ImageLayout(outFmt, outData.numImages()); + + if (imageInputFormat != input_format) { - LOG_ERROR("Format of input image #" << i << " must have only 1 plane"); + LOG_ERROR("Input image #" << i << " has DataFormat " << imageInputFormat << ", expected " << input_format); return ErrorCode::INVALID_DATA_FORMAT; } - if (outFmt.numPlanes() != 1) + if (!SameLayoutFamily(imageInputFormat, imageOutputFormat)) { - LOG_ERROR("Format of output image #" << i << " must have only 1 plane"); + LOG_ERROR("Invalid DataFormat between input image #" + << i << " (" << imageInputFormat << ") and output image #" << i << " (" << imageOutputFormat + << ")"); return ErrorCode::INVALID_DATA_FORMAT; } @@ -167,12 +420,24 @@ ErrorCode ChannelReorderVarShape::infer(const ImageBatchVarShapeDataStridedCuda return ErrorCode::INVALID_DATA_SHAPE; } - if (inFmt.numChannels() > 4) + if (inFmt.numChannels() > 4 || (isPlanar && inFmt.numChannels() == 2)) { LOG_ERROR("Invalid input channel number " << inFmt.numChannels()); return ErrorCode::INVALID_DATA_SHAPE; } + if (outFmt.numChannels() != outChannels) + { + LOG_ERROR("Output channel " << outFmt.numChannels() << " differs from " << outChannels); + return ErrorCode::INVALID_DATA_SHAPE; + } + + if (outFmt.numChannels() > 4 || (isPlanar && outFmt.numChannels() == 2)) + { + LOG_ERROR("Invalid output channel number " << outFmt.numChannels()); + return ErrorCode::INVALID_DATA_SHAPE; + } + if (outFmt.numChannels() > orderData.shape(1)) { LOG_ERROR("Invalid output channel number " << outFmt.numChannels() << ", must be at most " @@ -197,16 +462,61 @@ ErrorCode ChannelReorderVarShape::infer(const ImageBatchVarShapeDataStridedCuda } } + if (isPlanar && static_cast(outData.numImages()) > 65535) + { + LOG_ERROR("Planar ChannelReorder requires numImages <= 65535 (CUDA grid-z limit)"); + return ErrorCode::INVALID_DATA_SHAPE; + } + + if (isPlanar) + { + if (data_type == kCV_8U && channels == outChannels && channels == 3) + { + reorder_u8(inData, outData, orderData, stream); + return ErrorCode::SUCCESS; + } + if (data_type == kCV_8U && channels == outChannels && channels == 4) + { + reorder_u8(inData, outData, orderData, stream); + return ErrorCode::SUCCESS; + } + + typedef void (*planar_func_t)( + const ImageBatchVarShapeDataStridedCuda &inData, const ImageBatchVarShapeDataStridedCuda &outData, + const TensorDataStridedCuda &orderData, int numSrcChannels, int numDstChannels, cudaStream_t stream); + + static const planar_func_t planar_funcs[6] + = {reorder_planar, 0, reorder_planar, reorder_planar, reorder_planar, + reorder_planar}; + + const planar_func_t planar_func = planar_funcs[data_type]; + NVCV_ASSERT(planar_func != 0); + + planar_func(inData, outData, orderData, channels, outChannels, stream); + return ErrorCode::SUCCESS; + } + + if (data_type == kCV_8U && channels == outChannels && channels == 3) + { + reorder_u8(inData, outData, orderData, stream); + return ErrorCode::SUCCESS; + } + if (data_type == kCV_8U && channels == outChannels && channels == 4) + { + reorder_u8(inData, outData, orderData, stream); + return ErrorCode::SUCCESS; + } + typedef void (*func_t)(const ImageBatchVarShapeDataStridedCuda &inData, const ImageBatchVarShapeDataStridedCuda &outData, const TensorDataStridedCuda &orderData, - int numChannels, cudaStream_t stream); + int numSrcChannels, int numDstChannels, cudaStream_t stream); static const func_t funcs[6] = {reorder, 0, reorder, reorder, reorder, reorder}; const func_t func = funcs[data_type]; NVCV_ASSERT(func != 0); - func(inData, outData, orderData, channels, stream); + func(inData, outData, orderData, channels, outChannels, stream); return ErrorCode::SUCCESS; } diff --git a/src/cvcuda/priv/legacy/composite.cu b/src/cvcuda/priv/legacy/composite.cu index 7434e40ae..b7b70e713 100644 --- a/src/cvcuda/priv/legacy/composite.cu +++ b/src/cvcuda/priv/legacy/composite.cu @@ -27,8 +27,59 @@ using namespace nvcv; using namespace nvcv::legacy::helpers; using namespace nvcv::legacy::cuda_op; -#define Inv_255 0.00392156862f // 1.f/255.f -#define AlphaLerp(c0, c1, a) int(((int)c1 - (int)c0) * (int)a * Inv_255 + c0 + 0.5f) +namespace { + +__device__ __forceinline__ int AlphaLerp(uint8_t c0, uint8_t c1, uint8_t alpha) +{ + int value = (int)c0 * 255 + ((int)c1 - (int)c0) * (int)alpha + 128; + // This is exact rounded division by 255 over the full uint8 input range. + return (value + (value >> 8)) >> 8; +} + +static bool IsInterleaved(DataFormat format) +{ + return format == kNHWC || format == kHWC; +} + +static bool IsPlanar(DataFormat format) +{ + return format == kNCHW || format == kCHW; +} + +static bool IsImageLayout(DataFormat format) +{ + return IsInterleaved(format) || IsPlanar(format); +} + +struct TensorU8ImageAccess +{ + uint8_t *base; + int64_t sampleStride; + int64_t rowStride; + int64_t colStride; + int64_t chStride; + int rows; + int cols; +}; + +static TensorU8ImageAccess MakeTensorU8Access(const TensorDataAccessStridedImagePlanar &access) +{ + return {reinterpret_cast(access.sampleData(0)), + access.sampleStride(), + access.rowStride(), + access.colStride(), + access.chStride(), + access.numRows(), + access.numCols()}; +} + +__device__ __forceinline__ uint8_t *At(const TensorU8ImageAccess &access, int sample, int channel, int y, int x) +{ + return access.base + sample * access.sampleStride + channel * access.chStride + y * access.rowStride + + x * access.colStride; +} + +} // namespace template __global__ void composite_kernel(const Ptr2dNHWC fg, const Ptr2dNHWC bg, const Ptr2dNHWC fgMask, @@ -42,8 +93,9 @@ __global__ void composite_kernel(const Ptr2dNHWC fg, const Ptr2dNHWC bg, c const int batch_idx = get_batch_idx(); - int dst_ch = dst.ch; - int src_ch = fg.ch; + // Compile-time channels let each instantiation fold the loop and avoid wrapper metadata loads. + constexpr int dst_ch = cuda::NumElements; + constexpr int src_ch = cuda::NumElements; U mask_val = *fgMask.ptr(batch_idx, dst_y, dst_x); T bg_val = *bg.ptr(batch_idx, dst_y, dst_x); @@ -61,6 +113,39 @@ __global__ void composite_kernel(const Ptr2dNHWC fg, const Ptr2dNHWC bg, c *dst.ptr(batch_idx, dst_y, dst_x) = out; } +template +__global__ void composite_planar_kernel(TensorU8ImageAccess fg, TensorU8ImageAccess bg, TensorU8ImageAccess fgMask, + TensorU8ImageAccess dst) +{ + int dst_x = blockIdx.x * (blockDim.x * 2) + threadIdx.x; + int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + + if (dst_x >= dst.cols || dst_y >= dst.rows) + return; + + const int batch_idx = get_batch_idx(); + for (int dx = 0; dx < 2; ++dx) + { + const int x = dst_x + dx * blockDim.x; + if (x < dst.cols) + { + uint8_t mask_val = __ldg(At(fgMask, batch_idx, 0, dst_y, x)); + +#pragma unroll + for (int c = 0; c < 3; ++c) + { + const uint8_t c0 = __ldg(At(bg, batch_idx, c, dst_y, x)); + const uint8_t c1 = __ldg(At(fg, batch_idx, c, dst_y, x)); + *At(dst, batch_idx, c, dst_y, x) = AlphaLerp(c0, c1, mask_val); + } + if constexpr (dcn == 4) + { + *At(dst, batch_idx, 3, dst_y, x) = 255; + } + } + } +} + template // uchar void composite(const nvcv::TensorDataAccessStridedImagePlanar &foregroundData, const nvcv::TensorDataAccessStridedImagePlanar &backgroundData, @@ -71,7 +156,7 @@ void composite(const nvcv::TensorDataAccessStridedImagePlanar &foregroundData, const int out_width = outData.numCols(); const int out_height = outData.numRows(); - dim3 blockSize(16, 16, 1); + dim3 blockSize(32, 8, 1); dim3 gridSize(divUp(out_width, blockSize.x), divUp(out_height, blockSize.y), batch_size); typedef typename cuda::MakeType src_type; @@ -91,6 +176,30 @@ void composite(const nvcv::TensorDataAccessStridedImagePlanar &foregroundData, #endif } +template +void composite_planar(const nvcv::TensorDataAccessStridedImagePlanar &foregroundData, + const nvcv::TensorDataAccessStridedImagePlanar &backgroundData, + const nvcv::TensorDataAccessStridedImagePlanar &fgMaskData, + const nvcv::TensorDataAccessStridedImagePlanar &outData, cudaStream_t stream) +{ + const int batch_size = foregroundData.numSamples(); + const int out_width = outData.numCols(); + const int out_height = outData.numRows(); + + dim3 blockSize(64, 4, 1); + dim3 gridSize(divUp(out_width, blockSize.x * 2), divUp(out_height, blockSize.y), batch_size); + + composite_planar_kernel + <<>>(MakeTensorU8Access(foregroundData), MakeTensorU8Access(backgroundData), + MakeTensorU8Access(fgMaskData), MakeTensorU8Access(outData)); + checkKernelErrors(); + +#ifdef CUDA_DEBUG_LOG + checkCudaErrors(cudaStreamSynchronize(stream)); + checkCudaErrors(cudaGetLastError()); +#endif +} + namespace nvcv::legacy::cuda_op { ErrorCode Composite::infer(const TensorDataStridedCuda &foreground, const TensorDataStridedCuda &background, @@ -102,8 +211,8 @@ ErrorCode Composite::infer(const TensorDataStridedCuda &foreground, const Tensor DataFormat fgMask_format = GetLegacyDataFormat(fgMask.layout()); DataFormat output_format = GetLegacyDataFormat(outData.layout()); - if (!((foreground_format == background_format) && (foreground_format == fgMask_format) - && (foreground_format == output_format))) + if (!((foreground_format == background_format) && (foreground_format == output_format) + && IsImageLayout(foreground_format) && IsImageLayout(fgMask_format))) { LOG_ERROR("Invalid DataFormat between foreground (" << foreground_format << "), background (" << background_format << "), foreground mask (" @@ -113,12 +222,6 @@ ErrorCode Composite::infer(const TensorDataStridedCuda &foreground, const Tensor DataFormat format = foreground_format; - if (!(format == kNHWC || format == kHWC)) - { - LOG_ERROR("Invalid foreground DataFormat " << format << ", the valid DataFormats are: \"NHWC\", \"HWC\""); - return ErrorCode::INVALID_DATA_FORMAT; - } - auto foregroundAccess = TensorDataAccessStridedImagePlanar::Create(foreground); NVCV_ASSERT(foregroundAccess); @@ -141,6 +244,18 @@ ErrorCode Composite::infer(const TensorDataStridedCuda &foreground, const Tensor DataShape fgMask_shape = GetLegacyDataShape(fgMaskAccess->infoShape()); DataShape output_shape = GetLegacyDataShape(outAccess->infoShape()); + if (!((foreground_shape.N == background_shape.N) && (foreground_shape.N == fgMask_shape.N) + && (foreground_shape.N == output_shape.N) && (foreground_shape.H == background_shape.H) + && (foreground_shape.H == fgMask_shape.H) && (foreground_shape.H == output_shape.H) + && (foreground_shape.W == background_shape.W) && (foreground_shape.W == fgMask_shape.W) + && (foreground_shape.W == output_shape.W))) + { + LOG_ERROR("Invalid input/output shape: foreground " << foreground_shape << ", background " << background_shape + << ", foreground mask " << fgMask_shape << ", output " + << output_shape); + return ErrorCode::INVALID_DATA_SHAPE; + } + int foreground_channels = foreground_shape.C; int background_channels = background_shape.C; int fgMask_channels = fgMask_shape.C; @@ -161,6 +276,26 @@ ErrorCode Composite::infer(const TensorDataStridedCuda &foreground, const Tensor return ErrorCode::INVALID_DATA_TYPE; } + const bool isPlanar = IsPlanar(format); + if (isPlanar) + { + if (foregroundAccess->numSamples() > 65535) + { + LOG_ERROR("Planar Composite requires number of images <= 65535"); + return ErrorCode::INVALID_DATA_SHAPE; + } + + if (output_channels == 3) + { + composite_planar<3>(*foregroundAccess, *backgroundAccess, *fgMaskAccess, *outAccess, stream); + } + else + { + composite_planar<4>(*foregroundAccess, *backgroundAccess, *fgMaskAccess, *outAccess, stream); + } + return SUCCESS; + } + typedef void (*func_t)(const nvcv::TensorDataAccessStridedImagePlanar &foregroundData, const nvcv::TensorDataAccessStridedImagePlanar &backgroundData, const nvcv::TensorDataAccessStridedImagePlanar &fgMaskData, diff --git a/src/cvcuda/priv/legacy/composite_var_shape.cu b/src/cvcuda/priv/legacy/composite_var_shape.cu index eed6d97a8..e53d40680 100644 --- a/src/cvcuda/priv/legacy/composite_var_shape.cu +++ b/src/cvcuda/priv/legacy/composite_var_shape.cu @@ -27,8 +27,31 @@ using namespace nvcv; using namespace nvcv::legacy::helpers; using namespace nvcv::legacy::cuda_op; -#define Inv_255 0.00392156862f // 1.f/255.f -#define AlphaLerp(c0, c1, a) int(((int)c1 - (int)c0) * (int)a * Inv_255 + c0 + 0.5f) +namespace { + +__device__ __forceinline__ int AlphaLerp(uint8_t c0, uint8_t c1, uint8_t alpha) +{ + int value = (int)c0 * 255 + ((int)c1 - (int)c0) * (int)alpha + 128; + // This is exact rounded division by 255 over the full uint8 input range. + return (value + (value >> 8)) >> 8; +} + +static bool IsInterleaved(DataFormat format) +{ + return format == kNHWC || format == kHWC; +} + +static bool IsPlanar(DataFormat format) +{ + return format == kNCHW || format == kCHW; +} + +static bool IsImageLayout(DataFormat format) +{ + return IsInterleaved(format) || IsPlanar(format); +} + +} // namespace template __global__ void composite_kernel(const cuda::ImageBatchVarShapeWrap fg, const cuda::ImageBatchVarShapeWrap bg, @@ -41,6 +64,7 @@ __global__ void composite_kernel(const cuda::ImageBatchVarShapeWrap fg, const if (dst_x >= dst.width(batch_idx) || dst_y >= dst.height(batch_idx)) return; + // Compile-time channels let each instantiation fold the loop and avoid wrapper metadata loads. constexpr int dst_ch = cuda::NumElements; constexpr int src_ch = cuda::NumElements; @@ -60,6 +84,34 @@ __global__ void composite_kernel(const cuda::ImageBatchVarShapeWrap fg, const *dst.ptr(batch_idx, dst_y, dst_x) = out; } +template +__global__ void composite_planar_kernel(const cuda::ImageBatchVarShapeWrap fg, + const cuda::ImageBatchVarShapeWrap bg, + const cuda::ImageBatchVarShapeWrap fgMask, + cuda::ImageBatchVarShapeWrap dst) +{ + int dst_x = blockIdx.x * blockDim.x + threadIdx.x; + int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + + if (dst_x >= dst.width(batch_idx) || dst_y >= dst.height(batch_idx)) + return; + + uchar mask_val = *fgMask.ptr(batch_idx, 0, dst_y, dst_x); + +#pragma unroll + for (int c = 0; c < 3; ++c) + { + const uchar c0 = *bg.ptr(batch_idx, c, dst_y, dst_x); + const uchar c1 = *fg.ptr(batch_idx, c, dst_y, dst_x); + *dst.ptr(batch_idx, c, dst_y, dst_x) = AlphaLerp(c0, c1, mask_val); + } + if constexpr (dcn == 4) + { + *dst.ptr(batch_idx, 3, dst_y, dst_x) = 255; + } +} + template // uchar void composite(const nvcv::ImageBatchVarShapeDataStridedCuda &foregroundData, const nvcv::ImageBatchVarShapeDataStridedCuda &backgroundData, @@ -77,7 +129,7 @@ void composite(const nvcv::ImageBatchVarShapeDataStridedCuda &foregroundData, const int batch_size = outData.numImages(); Size2D outMaxSize = outData.maxSize(); - dim3 blockSize(16, 16, 1); + dim3 blockSize(dcn == 3 ? 32 : 16, dcn == 3 ? 8 : 16, 1); dim3 gridSize(divUp(outMaxSize.w, blockSize.x), divUp(outMaxSize.h, blockSize.y), batch_size); composite_kernel<<>>(fg_ptr, bg_ptr, fgMask_ptr, dst_ptr); @@ -89,6 +141,32 @@ void composite(const nvcv::ImageBatchVarShapeDataStridedCuda &foregroundData, #endif } +template +void composite_planar(const nvcv::ImageBatchVarShapeDataStridedCuda &foregroundData, + const nvcv::ImageBatchVarShapeDataStridedCuda &backgroundData, + const nvcv::ImageBatchVarShapeDataStridedCuda &fgMaskData, + const nvcv::ImageBatchVarShapeDataStridedCuda &outData, cudaStream_t stream) +{ + cuda::ImageBatchVarShapeWrap fg_ptr(foregroundData); + cuda::ImageBatchVarShapeWrap bg_ptr(backgroundData); + cuda::ImageBatchVarShapeWrap fgMask_ptr(fgMaskData); + cuda::ImageBatchVarShapeWrap dst_ptr(outData); + + const int batch_size = outData.numImages(); + Size2D outMaxSize = outData.maxSize(); + + dim3 blockSize(32, 8, 1); + dim3 gridSize(divUp(outMaxSize.w, blockSize.x), divUp(outMaxSize.h, blockSize.y), batch_size); + + composite_planar_kernel<<>>(fg_ptr, bg_ptr, fgMask_ptr, dst_ptr); + checkKernelErrors(); + +#ifdef CUDA_DEBUG_LOG + checkCudaErrors(cudaStreamSynchronize(stream)); + checkCudaErrors(cudaGetLastError()); +#endif +} + namespace nvcv::legacy::cuda_op { ErrorCode CompositeVarShape::infer(const ImageBatchVarShapeDataStridedCuda &foreground, @@ -107,8 +185,8 @@ ErrorCode CompositeVarShape::infer(const ImageBatchVarShapeDataStridedCuda &fore DataFormat fgMask_format = helpers::GetLegacyDataFormat(fgMask); DataFormat output_format = helpers::GetLegacyDataFormat(outData); - if (!((foreground_format == background_format) && (foreground_format == fgMask_format) - && (foreground_format == output_format))) + if (!((foreground_format == background_format) && (foreground_format == output_format) + && IsImageLayout(foreground_format) && IsImageLayout(fgMask_format))) { LOG_ERROR("Invalid DataFormat between foreground (" << foreground_format << "), background (" << background_format << "), foreground mask (" @@ -118,12 +196,6 @@ ErrorCode CompositeVarShape::infer(const ImageBatchVarShapeDataStridedCuda &fore DataFormat format = foreground_format; - if (!(format == kNHWC || format == kHWC)) - { - LOG_ERROR("Invalid foreground DataFormat " << format << ", the valid DataFormats are: \"NHWC\", \"HWC\""); - return ErrorCode::INVALID_DATA_FORMAT; - } - DataType foreground_data_type = helpers::GetLegacyDataType(foreground.uniqueFormat()); DataType background_data_type = helpers::GetLegacyDataType(background.uniqueFormat()); DataType fgMask_data_type = helpers::GetLegacyDataType(fgMask.uniqueFormat()); @@ -149,6 +221,35 @@ ErrorCode CompositeVarShape::infer(const ImageBatchVarShapeDataStridedCuda &fore return ErrorCode::INVALID_DATA_SHAPE; } + if (!((foreground.numImages() == background.numImages()) && (foreground.numImages() == fgMask.numImages()) + && (foreground.numImages() == outData.numImages()))) + { + LOG_ERROR("Invalid input/output batch size: foreground " + << foreground.numImages() << ", background " << background.numImages() << ", foreground mask " + << fgMask.numImages() << ", output " << outData.numImages()); + return ErrorCode::INVALID_DATA_SHAPE; + } + + const bool isPlanar = IsPlanar(format); + if (isPlanar) + { + if (outData.numImages() > 65535) + { + LOG_ERROR("Planar Composite requires number of images <= 65535"); + return ErrorCode::INVALID_DATA_SHAPE; + } + + if (output_channels == 3) + { + composite_planar<3>(foreground, background, fgMask, outData, stream); + } + else + { + composite_planar<4>(foreground, background, fgMask, outData, stream); + } + return SUCCESS; + } + typedef void (*func_t)(const nvcv::ImageBatchVarShapeDataStridedCuda &foregroundData, const nvcv::ImageBatchVarShapeDataStridedCuda &backgroundData, const nvcv::ImageBatchVarShapeDataStridedCuda &fgMaskData, diff --git a/src/cvcuda/priv/legacy/convert_to.cu b/src/cvcuda/priv/legacy/convert_to.cu index d0603b12b..066edd230 100644 --- a/src/cvcuda/priv/legacy/convert_to.cu +++ b/src/cvcuda/priv/legacy/convert_to.cu @@ -1,4 +1,4 @@ -/* Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +/* Copyright (c) 2021-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * * SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES * SPDX-License-Identifier: Apache-2.0 @@ -18,17 +18,21 @@ * limitations under the License. */ +#include "../CudaDeviceUtils.hpp" #include "CvCudaLegacy.h" #include "CvCudaLegacyHelpers.hpp" #include "CvCudaUtils.cuh" +#include #include #include #include #include +#include #include +#include using namespace nvcv::legacy::cuda_op; using namespace nvcv::legacy::helpers; @@ -36,31 +40,67 @@ using namespace nvcv::legacy::helpers; template struct Convertor { - S alpha; - S beta; + S alpha; + S beta; + bool truncate; // round toward zero instead of to-nearest (only affects integer outputs) __device__ __forceinline__ DST_TYPE operator()(SRC_TYPE src) const { - return nvcv::cuda::SaturateCast(alpha * src + beta); + auto work = alpha * src + beta; // scalar S or a MakeType vector, matching DST_TYPE's channels + // SaturateCast rounds float->integer to-nearest, so for truncation pre-round toward zero + // first. Compiled out for floating-point outputs, where the rounding mode has no effect. + if constexpr (std::is_integral_v>) + { + if (truncate) + work = nvcv::cuda::round(work); + } + return nvcv::cuda::SaturateCast(work); } }; -template +// Element-wise convert is latency-bound (1 pixel/thread leaves too few memory requests in flight). Each +// thread processes NIX columns strided by the total x-thread count: consecutive threads read consecutive +// columns (coalesced), and the NIX per-thread accesses are issued as a batch so NIX independent loads are +// in flight at once (memory-level parallelism), hiding load latency. NIX>=2 needs the grid sized to +// divUp(size.x, NIX) x-threads. +template __global__ void convertFormat(SrcWrapper src, DstWrapper dst, UnOp op, int2 size) { - const int src_x = blockIdx.x * blockDim.x + threadIdx.x; const int src_y = blockIdx.y * blockDim.y + threadIdx.y; const int batch_idx = get_batch_idx(); - - if (src_x >= size.x || src_y >= size.y) + if (src_y >= size.y) return; - *dst.ptr(batch_idx, src_y, src_x) = op(*src.ptr(batch_idx, src_y, src_x)); + const int x0 = blockIdx.x * blockDim.x + threadIdx.x; + const int stride = gridDim.x * blockDim.x; + + using SrcPx = std::remove_cv_t>; + SrcPx v[NIX]; +#pragma unroll + for (int i = 0; i < NIX; ++i) + { + const int x = x0 + i * stride; + if (x < size.x) + v[i] = *src.ptr(batch_idx, src_y, x); + } +#pragma unroll + for (int i = 0; i < NIX; ++i) + { + const int x = x0 + i * stride; + if (x < size.x) + *dst.ptr(batch_idx, src_y, x) = op(v[i]); + } } -template -ErrorCode convertToScaleCN(const nvcv::TensorDataStridedCuda &inData, const nvcv::TensorDataStridedCuda &outData, - const double alpha, const double beta, cudaStream_t stream) +inline int CurrentDeviceSMOrZero() +{ + int sm = 0; + return cvcuda::priv::GetCurrentDeviceSM(sm) == cudaSuccess ? sm : 0; +} + +template +ErrorCode convertToScaleCNImpl(const nvcv::TensorDataStridedCuda &inData, const nvcv::TensorDataStridedCuda &outData, + const double alpha, const double beta, bool truncate, cudaStream_t stream) { auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(inData); NVCV_ASSERT(inAccess); @@ -72,7 +112,7 @@ ErrorCode convertToScaleCN(const nvcv::TensorDataStridedCuda &inData, const nvcv const int batch_size = inAccess->numSamples(); dim3 block(32, 8); - dim3 grid(divUp(size.x, block.x), divUp(size.y, block.y), batch_size); + dim3 grid(divUp(divUp(size.x, NIX), static_cast(block.x)), divUp(size.y, block.y), batch_size); using DT_AB = decltype(float() * DT_SOURCE() * DT_DEST()); //pick correct scalar using SRC_DATA_TYPE = nvcv::cuda::MakeType; @@ -80,8 +120,9 @@ ErrorCode convertToScaleCN(const nvcv::TensorDataStridedCuda &inData, const nvcv Convertor op; - op.alpha = nvcv::cuda::SaturateCast(alpha); - op.beta = nvcv::cuda::SaturateCast(beta); + op.alpha = nvcv::cuda::SaturateCast(alpha); + op.beta = nvcv::cuda::SaturateCast(beta); + op.truncate = truncate; auto outMaxStride = outAccess->sampleStride() * outAccess->numSamples(); auto inMaxStride = inAccess->sampleStride() * inAccess->numSamples(); @@ -90,7 +131,111 @@ ErrorCode convertToScaleCN(const nvcv::TensorDataStridedCuda &inData, const nvcv auto src = nvcv::cuda::CreateTensorWrapNHW(inData); auto dst = nvcv::cuda::CreateTensorWrapNHW(outData); - convertFormat<<>>(src, dst, op, size); + convertFormat<<>>(src, dst, op, size); + } + else + { + LOG_ERROR("Input or output size exceeds " << nvcv::cuda::TypeTraits::max << ". Tensor is too large."); + return ErrorCode::INVALID_PARAMETER; + } + return ErrorCode::SUCCESS; +} + +template +ErrorCode convertToScaleCN(const nvcv::TensorDataStridedCuda &inData, const nvcv::TensorDataStridedCuda &outData, + const double alpha, const double beta, bool truncate, cudaStream_t stream) +{ + // A100 and H100 profiles favor the legacy one-pixel policy for these wide float32 paths, while + // SM86 regresses beyond paired noise. Keep NIX=4 on unmeasured architectures rather than + // extrapolating the reference result. The short2 path uses NIX=1 only on SM80; NIX=4 is 5% faster + // on SM90 and remains at ridge on SM86. + constexpr bool kWideFloat32 + = std::is_same_v< + DT_DEST, + float> && ((NC == 3 && (std::is_same_v || std::is_same_v)) || (NC == 4 && std::is_same_v)); + if constexpr (kWideFloat32) + { + int sm = CurrentDeviceSMOrZero(); + if (sm == 80 || sm == 90) + return convertToScaleCNImpl(inData, outData, alpha, beta, truncate, stream); + } + else if constexpr (std::is_same_v && std::is_same_v && NC == 2) + { + if (CurrentDeviceSMOrZero() == 80) + return convertToScaleCNImpl(inData, outData, alpha, beta, truncate, stream); + } + return convertToScaleCNImpl(inData, outData, alpha, beta, truncate, stream); +} + +// Build a single-channel (N*C, H, W, 1) NHWC view of a packed planar (NCHW/CHW) tensor. +// ConvertTo applies the same scalar alpha/beta to every element regardless of channel, so each +// (sample, channel) plane is just a single-channel image; viewing the planes as N*C flat samples +// lets the existing interleaved single-channel kernel process planar data unchanged and bit-exact. +// The view places plane (n, c) at byte offset n*sampleStride + c*chStride, which equals a uniform +// (n*C + c)*chStride stride only when the channel planes are tightly packed across samples +// (sampleStride == numChannels * chStride); always true within a single sample. See the reference +// PlanarAsSingleChannelView in OpResize.cu (.agents/guidance/PLANAR_GUIDELINES.md). +inline nvcv::TensorDataStridedCuda PlanarAsSingleChannelView(const nvcv::TensorDataStridedCuda &data, + const nvcv::TensorDataAccessStridedImagePlanar &access) +{ + const int64_t numSamples = access.numSamples(); + const int64_t numChannels = access.numChannels(); + const int64_t numRows = access.numRows(); + const int64_t numCols = access.numCols(); + + nvcv::TensorDataStridedCuda::Buffer buf; + buf.basePtr = reinterpret_cast(data.basePtr()); + buf.strides[0] = access.chStride(); // N*C flattened planes + buf.strides[1] = access.rowStride(); // H + buf.strides[2] = access.colStride(); // W + buf.strides[3] = access.colStride(); // C == 1 + return nvcv::TensorDataStridedCuda{ + nvcv::TensorShape{{numSamples * numChannels, numRows, numCols, 1}, "NHWC"}, + data.dtype(), buf + }; +} + +// Single-channel ConvertTo is issue-bound on the reference SKUs (A100 ncu: Issue Slots 82%, DRAM only +// 46% — 1 element/thread pays the full per-thread overhead, index math + convert, for a single element). +// VEC contiguous columns are coalesced and reinterpretable as one MakeType "pixel", so reusing +// the existing vector Convertor over them cuts the per-element instruction count ~VEC× (relieving the +// issue bound) -- the inverse of NIX-strided MLP, which only helped the latency-bound dev GPU. Selected +// only when cols % VEC == 0 and the buffers are vector-aligned (caller-checked); otherwise the scalar +// single-channel path runs. +template +ErrorCode convertToScaleWideSingleChannel(const nvcv::TensorDataStridedCuda &inData, + const nvcv::TensorDataStridedCuda &outData, const double alpha, + const double beta, bool truncate, cudaStream_t stream) +{ + auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(inData); + NVCV_ASSERT(inAccess); + auto outAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(outData); + NVCV_ASSERT(outAccess); + + // Each "pixel" is VEC contiguous columns; cols is divisible by VEC (caller-checked). + const int2 size = {inAccess->numCols() / VEC, inAccess->numRows()}; + const int batch_size = inAccess->numSamples(); + + constexpr int kNIX = 4; + dim3 block(32, 8); + dim3 grid(divUp(divUp(size.x, kNIX), static_cast(block.x)), divUp(size.y, block.y), batch_size); + + using DT_AB = decltype(float() * DT_SOURCE() * DT_DEST()); + using SRC_DATA_TYPE = nvcv::cuda::MakeType; + using DST_DATA_TYPE = nvcv::cuda::MakeType; + + Convertor op; + op.alpha = nvcv::cuda::SaturateCast(alpha); + op.beta = nvcv::cuda::SaturateCast(beta); + op.truncate = truncate; + + auto outMaxStride = outAccess->sampleStride() * outAccess->numSamples(); + auto inMaxStride = inAccess->sampleStride() * inAccess->numSamples(); + if (std::max(outMaxStride, inMaxStride) <= nvcv::cuda::TypeTraits::max) + { + auto src = nvcv::cuda::CreateTensorWrapNHW(inData); + auto dst = nvcv::cuda::CreateTensorWrapNHW(outData); + convertFormat<<>>(src, dst, op, size); } else { @@ -100,23 +245,50 @@ ErrorCode convertToScaleCN(const nvcv::TensorDataStridedCuda &inData, const nvcv return ErrorCode::SUCCESS; } +// True when a single-channel convert can use VEC-wide contiguous vectorization: cols divisible by VEC, +// and the in/out base pointers + row/sample strides aligned to the VEC-wide vector type. +template +static bool wideSingleChannelEligible(const nvcv::TensorDataStridedCuda &inData, + const nvcv::TensorDataStridedCuda &outData) +{ + auto in = nvcv::TensorDataAccessStridedImagePlanar::Create(inData); + auto out = nvcv::TensorDataAccessStridedImagePlanar::Create(outData); + if (!in || !out) + return false; + if (in->numCols() % VEC != 0) + return false; + const std::uintptr_t aS = alignof(nvcv::cuda::MakeType); + const std::uintptr_t aD = alignof(nvcv::cuda::MakeType); + auto ok = [](std::uintptr_t base, int64_t rowS, int64_t smpS, std::uintptr_t a) + { + return ((base | static_cast(rowS) | static_cast(smpS)) & (a - 1)) == 0; + }; + return ok(reinterpret_cast(inData.basePtr()), in->rowStride(), in->sampleStride(), aS) + && ok(reinterpret_cast(outData.basePtr()), out->rowStride(), out->sampleStride(), aD); +} + template // ErrorCode convertToScale(const nvcv::TensorDataStridedCuda &inData, const nvcv::TensorDataStridedCuda &outData, - int numChannels, const double alpha, const double beta, cudaStream_t stream) + int numChannels, const double alpha, const double beta, bool truncate, cudaStream_t stream) { switch (numChannels) { case 1: - return convertToScaleCN(inData, outData, alpha, beta, stream); + // Wide contiguous vectorization (issue-bound fix); falls back to the scalar single-channel path + // when cols isn't VEC-divisible or the buffers aren't vector-aligned. + if (wideSingleChannelEligible(inData, outData)) + return convertToScaleWideSingleChannel(inData, outData, alpha, beta, truncate, + stream); + return convertToScaleCN(inData, outData, alpha, beta, truncate, stream); case 2: - return convertToScaleCN(inData, outData, alpha, beta, stream); + return convertToScaleCN(inData, outData, alpha, beta, truncate, stream); case 3: - return convertToScaleCN(inData, outData, alpha, beta, stream); + return convertToScaleCN(inData, outData, alpha, beta, truncate, stream); case 4: - return convertToScaleCN(inData, outData, alpha, beta, stream); + return convertToScaleCN(inData, outData, alpha, beta, truncate, stream); default: LOG_ERROR("Unknown number of channels"); @@ -132,16 +304,24 @@ ErrorCode convertToScale(const nvcv::TensorDataStridedCuda &inData, const nvcv:: namespace nvcv::legacy::cuda_op { ErrorCode ConvertTo::infer(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, - const double alpha, const double beta, cudaStream_t stream) + const double alpha, const double beta, NVCVRoundMode roundMode, cudaStream_t stream) { + const bool truncate = (roundMode == NVCV_ROUND_TRUNCATE); + cuda_op::DataFormat input_format = GetLegacyDataFormat(inData.layout()); cuda_op::DataFormat output_format = GetLegacyDataFormat(outData.layout()); cuda_op::DataType input_datatype = GetLegacyDataType(inData.dtype()); cuda_op::DataType output_datatype = GetLegacyDataType(outData.dtype()); - if (!(input_format == kNHWC || output_format == kHWC) || !(output_format == kNHWC || output_format == kHWC)) + const bool inPlanar = (input_format == kNCHW || input_format == kCHW); + const bool outPlanar = (output_format == kNCHW || output_format == kCHW); + const bool inOk = (input_format == kNHWC || input_format == kHWC) || inPlanar; + const bool outOk = (output_format == kNHWC || output_format == kHWC) || outPlanar; + + // Input and output must both be interleaved or both planar; the operator does not transpose. + if (!(inOk && outOk && inPlanar == outPlanar)) { - LOG_ERROR("Invalid DataFormat format must be kHWC/kNHWC"); + LOG_ERROR("Invalid DataFormat, must be kHWC/kNHWC or kCHW/kNCHW with matching layouts"); return ErrorCode::INVALID_DATA_FORMAT; } @@ -188,7 +368,8 @@ ErrorCode ConvertTo::infer(const TensorDataStridedCuda &inData, const TensorData } typedef ErrorCode (*func_t)(const nvcv::TensorDataStridedCuda &inData, const nvcv::TensorDataStridedCuda &outData, - int numChannels, const double alpha, const double beta, cudaStream_t stream); + int numChannels, const double alpha, const double beta, bool truncate, + cudaStream_t stream); // clang-format off static const func_t funcs[7][7] = { @@ -203,7 +384,38 @@ ErrorCode ConvertTo::infer(const TensorDataStridedCuda &inData, const TensorData // clang-format on const func_t func = funcs[input_datatype][output_datatype]; - return func(inData, outData, channels, alpha, beta, stream); + + if (inPlanar) + { + // Each channel plane is processed as an independent single-channel image flattened into the + // N*C sample dimension. That flattened count becomes the kernel's grid z-dimension, capped at + // CUDA's 65535 limit; compute it in 64-bit to avoid overflow. + constexpr int64_t kMaxGridZ = 65535; + const int64_t planarBatch = static_cast(channels) * batch; + if (planarBatch > kMaxGridZ) + { + LOG_ERROR("Planar ConvertTo requires numSamples * numChannels <= 65535 (CUDA grid-z limit)"); + return ErrorCode::INVALID_DATA_SHAPE; + } + + // The flattened view assumes channel planes are tightly packed across samples. + if (batch > 1 && inAccess->sampleStride() != channels * inAccess->chStride()) + { + LOG_ERROR("Planar ConvertTo of a batched tensor requires tightly packed input channel planes"); + return ErrorCode::INVALID_DATA_SHAPE; + } + if (batch > 1 && outAccess->sampleStride() != channels * outAccess->chStride()) + { + LOG_ERROR("Planar ConvertTo of a batched tensor requires tightly packed output channel planes"); + return ErrorCode::INVALID_DATA_SHAPE; + } + + auto inView = PlanarAsSingleChannelView(inData, *inAccess); + auto outView = PlanarAsSingleChannelView(outData, *outAccess); + return func(inView, outView, 1, alpha, beta, truncate, stream); + } + + return func(inData, outData, channels, alpha, beta, truncate, stream); } } // namespace nvcv::legacy::cuda_op diff --git a/src/cvcuda/priv/legacy/copy_make_border.cu b/src/cvcuda/priv/legacy/copy_make_border.cu index 07d1cc771..c18d45fa0 100644 --- a/src/cvcuda/priv/legacy/copy_make_border.cu +++ b/src/cvcuda/priv/legacy/copy_make_border.cu @@ -1,4 +1,4 @@ -/* Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +/* Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * * SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES * SPDX-License-Identifier: Apache-2.0 @@ -27,6 +27,40 @@ namespace nvcv::legacy::cuda_op { +namespace { + +static bool IsPlanar(DataFormat format) +{ + return format == kNCHW || format == kCHW; +} + +static nvcv::TensorDataStridedCuda PlanarChannelView(const nvcv::TensorDataStridedCuda &data, + const nvcv::TensorDataAccessStridedImagePlanar &access, int plane) +{ + nvcv::TensorDataStridedCuda::Buffer buf; + buf.basePtr = reinterpret_cast(data.basePtr()) + plane * access.chStride(); + buf.strides[0] = access.sampleStride(); + buf.strides[1] = access.rowStride(); + buf.strides[2] = access.colStride(); + buf.strides[3] = access.colStride(); + + return nvcv::TensorDataStridedCuda{ + nvcv::TensorShape{{access.numSamples(), access.numRows(), access.numCols(), 1}, "NHWC"}, + data.dtype(), buf + }; +} + +static float4 PlanarBorderValue(const float4 &borderValue, int plane) +{ + const float value = plane == 0 ? borderValue.x + : plane == 1 ? borderValue.y + : plane == 2 ? borderValue.z + : borderValue.w; + return float4{value, value, value, value}; +} + +} // namespace + template __global__ void copyMakeBorderKernel(SrcWrapper src, DstWrapper dst, int2 dstSize, int left, int top) { @@ -39,6 +73,61 @@ __global__ void copyMakeBorderKernel(SrcWrapper src, DstWrapper dst, int2 dstSiz } } +// Vector pack type for T: uint3 (12B) for 3-element T, else uint4 (16B). NIX = elements/thread. +template +using CMB_DPT = std::conditional_t == 3, uint3, uint4>; +template +constexpr int CMB_NIX = sizeof(CMB_DPT) / sizeof(T); +template +constexpr uintptr_t CMB_MSK = (sizeof(CMB_DPT) == sizeof(uint3) ? sizeof(uint) : sizeof(CMB_DPT)) - 1; + +// copy_make_border is a pure copy: the only per-pixel cost is the BorderWrap's bounds test + index +// clamp, which dominates (compute-bound) even though the interior is just a straight copy. This kernel +// processes CMB_NIX consecutive output x-elements per thread and takes a fast path for the INTERIOR +// (the [left,left+srcW) x [top,top+srcH) region, i.e. the bulk): a single aligned vector load+store +// straight from the raw source, skipping the BorderWrap entirely. Threads touching the border (or an +// unaligned/partial span) fall back to the per-element BorderWrap path. Bit-exact (a copy is exact; +// border pixels go through the same BorderWrap as before). +template +__global__ void copyMakeBorderKernelVec(SrcWrapper src, SrcRawWrapper srcRaw, DstWrapper dst, int2 dstSize, + int2 srcSize, int left, int top) +{ + const int dstY = blockIdx.y * blockDim.y + threadIdx.y; + const int z = blockIdx.z; + if (dstY >= dstSize.y) + return; + const int dstX0 = (blockIdx.x * blockDim.x + threadIdx.x) * CMB_NIX; + if (dstX0 >= dstSize.x) + return; + + const int srcY = dstY - top; + const int srcX0 = dstX0 - left; + const bool full = (dstX0 + CMB_NIX - 1 < dstSize.x); + const bool interiorRow = (srcY >= 0 && srcY < srcSize.y); + const bool interiorSpan = (srcX0 >= 0 && srcX0 + CMB_NIX - 1 < srcSize.x); + + if (full && interiorRow && interiorSpan) + { + const T *sp = &srcRaw[int3{srcX0, srcY, z}]; + T *dp = &dst[int3{dstX0, dstY, z}]; + if ((reinterpret_cast(sp) & CMB_MSK) == 0 && (reinterpret_cast(dp) & CMB_MSK) == 0) + *reinterpret_cast *>(dp) = *reinterpret_cast *>(sp); + else +#pragma unroll + for (int i = 0; i < CMB_NIX; ++i) dp[i] = sp[i]; + } + else + { +#pragma unroll + for (int i = 0; i < CMB_NIX; ++i) + { + const int dstX = dstX0 + i; + if (dstX < dstSize.x) + dst[int3{dstX, dstY, z}] = src[int3{dstX - left, srcY, z}]; + } + } +} + template struct copyMakeBorderDispatcher { @@ -54,17 +143,35 @@ struct copyMakeBorderDispatcher int2 dstSize{outAccess->numCols(), outAccess->numRows()}; dim3 blockSize(BLOCK, BLOCK / 4, 1); - dim3 gridSize(divUp(dstSize.x, blockSize.x), divUp(dstSize.y, blockSize.y), outAccess->numSamples()); int64_t srcMaxStride = inAccess->sampleStride() * inAccess->numSamples(); int64_t dstMaxStride = outAccess->sampleStride() * outAccess->numSamples(); if (std::max(srcMaxStride, dstMaxStride) <= cuda::TypeTraits::max) { + // BorderWrap for the border pixels; a raw TensorWrap for the interior vector fast path. auto src = cuda::CreateBorderWrapNHW(inData, borderValue); auto dst = cuda::CreateTensorWrapNHW(outData); - copyMakeBorderKernel<<>>(src, dst, dstSize, left, top); + // The uint3 (12B, 3-element interleaved) vector pack and the REPLICATE border both regress + // on A100 (and give no win on H100), so keep the scalar copy for those; the vectorized + // interior fast path is used only where it measurably helps (1-/4-element interleaved and + // planar single-channel, which dispatch through the single-channel path). + if constexpr (cuda::NumElements == 3 || B == NVCV_BORDER_REPLICATE) + { + dim3 gridSize(divUp(dstSize.x, blockSize.x), divUp(dstSize.y, blockSize.y), outAccess->numSamples()); + copyMakeBorderKernel<<>>(src, dst, dstSize, left, top); + } + else + { + int2 srcSize{inAccess->numCols(), inAccess->numRows()}; + dim3 gridSize(divUp(dstSize.x, blockSize.x * CMB_NIX), divUp(dstSize.y, blockSize.y), + outAccess->numSamples()); + auto srcRaw = cuda::CreateTensorWrapNHW(inData); + + copyMakeBorderKernelVec + <<>>(src, srcRaw, dst, dstSize, srcSize, left, top); + } } else { @@ -109,12 +216,15 @@ ErrorCode CopyMakeBorder::infer(const TensorDataStridedCuda &inData, const Tenso return ErrorCode::INVALID_DATA_FORMAT; } - if (!(input_format == kNHWC || input_format == kHWC)) + if (!(input_format == kNHWC || input_format == kHWC || input_format == kNCHW || input_format == kCHW)) { - LOG_ERROR("Invalid input DataFormat " << input_format << ", the valid DataFormats are: \"NHWC\", \"HWC\""); + LOG_ERROR("Invalid input DataFormat " << input_format + << ", the valid DataFormats are: \"NHWC\", \"HWC\", \"NCHW\", \"CHW\""); return ErrorCode::INVALID_DATA_FORMAT; } + const bool isPlanar = IsPlanar(input_format); + if (inData.dtype() != outData.dtype()) { LOG_ERROR("Invalid DataType between input (" << inData.dtype() << ") and output (" << outData.dtype() << ")"); @@ -131,12 +241,19 @@ ErrorCode CopyMakeBorder::infer(const TensorDataStridedCuda &inData, const Tenso const int channels = inAccess->numChannels(); - if (channels > 4) + if (channels > 4 || (isPlanar && channels == 2)) { LOG_ERROR("Invalid channel number " << channels); return ErrorCode::INVALID_DATA_SHAPE; } + if (!isPlanar && channels == 2 && data_type != kCV_8U) + { + LOG_ERROR("Invalid channel number " << channels << " for DataType " << data_type + << ", 2 channels only supported for 8bit Unsigned"); + return ErrorCode::INVALID_DATA_SHAPE; + } + if (!(data_type == kCV_8U || data_type == kCV_16U || data_type == kCV_16S || data_type == kCV_32F)) { LOG_ERROR("Invalid DataType " << data_type); @@ -181,6 +298,26 @@ ErrorCode CopyMakeBorder::infer(const TensorDataStridedCuda &inData, const Tenso // clang-format on const func_t func = funcs[data_type][channels - 1]; + + if (isPlanar) + { + const func_t planarFunc = funcs[data_type][0]; + NVCV_ASSERT(planarFunc != 0); + + for (int c = 0; c < channels; ++c) + { + auto planeIn = PlanarChannelView(inData, *inAccess, c); + auto planeOut = PlanarChannelView(outData, *outAccess, c); + ErrorCode ec + = planarFunc(planeIn, planeOut, top, left, border_type, PlanarBorderValue(borderValue, c), stream); + if (ec != ErrorCode::SUCCESS) + { + return ec; + } + } + return ErrorCode::SUCCESS; + } + NVCV_ASSERT(func != 0); return func(inData, outData, top, left, border_type, borderValue, stream); diff --git a/src/cvcuda/priv/legacy/copy_make_border_var_shape.cu b/src/cvcuda/priv/legacy/copy_make_border_var_shape.cu index c3a17d971..4f90d3f80 100644 --- a/src/cvcuda/priv/legacy/copy_make_border_var_shape.cu +++ b/src/cvcuda/priv/legacy/copy_make_border_var_shape.cu @@ -18,11 +18,15 @@ * limitations under the License. */ +#include "../CudaDeviceUtils.hpp" +#include "CopyMakeBorderPolicy.hpp" #include "CvCudaLegacy.h" #include "CvCudaLegacyHelpers.hpp" #include "CvCudaUtils.cuh" +#include + #define BLOCK 32 using namespace nvcv; @@ -32,6 +36,73 @@ using namespace nvcv::legacy::helpers; namespace nvcv::legacy::cuda_op { namespace { +static bool IsPlanar(DataFormat format) +{ + return format == kNCHW || format == kCHW; +} + +static bool SameLayoutFamily(DataFormat lhs, DataFormat rhs) +{ + return IsPlanar(lhs) == IsPlanar(rhs); +} + +template +DataType GetOutputDataType(const OutType &data_out) +{ + if constexpr (std::is_same_v) + { + return GetLegacyDataType(data_out.dtype()); + } + else + { + return GetLegacyDataType(data_out.uniqueFormat()); + } +} + +static float PlanarBorderComponent(const float4 &borderValue, int plane) +{ + return plane == 0 ? borderValue.x : plane == 1 ? borderValue.y : plane == 2 ? borderValue.z : borderValue.w; +} + +static TensorDataStridedCuda PlanarTensorView(const TensorDataStridedCuda &data) +{ + auto access = TensorDataAccessStridedImagePlanar::Create(data); + NVCV_ASSERT(access); + + TensorDataStridedCuda::Buffer buf; + buf.basePtr = reinterpret_cast(data.basePtr()); + buf.strides[0] = access->sampleStride(); + buf.strides[1] = access->chStride(); + buf.strides[2] = access->rowStride(); + buf.strides[3] = access->colStride(); + + return TensorDataStridedCuda{ + TensorShape{{access->numSamples(), access->numChannels(), access->numRows(), access->numCols()}, "NCHW"}, + data.dtype(), buf + }; +} + +template +constexpr int CMBVS_NIX = sizeof(T) == 1 ? 8 + : std::is_same_v, float> && sizeof(T) == 4 + ? 2 + : (cuda::NumElements == 3 ? sizeof(uint3) : sizeof(uint4)) / sizeof(T); + +template +using CMBVS_DPT + = std::conditional_t>>; + +template +constexpr uintptr_t CMBVS_MSK = alignof(CMBVS_DPT) - 1; + +inline int CurrentDeviceSMOrZero() +{ + int sm = 0; + return cvcuda::priv::GetCurrentDeviceSM(sm) == cudaSuccess ? sm : 0; +} + template __global__ void copyMakeBorderKernel(const SrcWrapper src, DstWrapper dst, const cuda::Tensor3DWrap left_, const cuda::Tensor3DWrap top_, int out_height, int out_width) @@ -52,6 +123,101 @@ __global__ void copyMakeBorderKernel(const SrcWrapper src, DstWrapper dst, const } } +template> +__global__ void copyMakeBorderKernelVec(const SrcWrapper src, const SrcRawWrapper srcRaw, DstWrapper dst, + const cuda::Tensor3DWrap left_, + const cuda::Tensor3DWrap top_, int fixedHeight, int fixedWidth, + int plane) +{ + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const int x0 = (blockIdx.x * blockDim.x + threadIdx.x) * NIX; + const int batch_idx = get_batch_idx(); + + int outHeight = fixedHeight; + int outWidth = fixedWidth; + if constexpr (!FIXED_OUTPUT) + { + if constexpr (PLANAR) + { + outHeight = dst.height(batch_idx, plane); + outWidth = dst.width(batch_idx, plane); + } + else + { + outHeight = dst.height(batch_idx); + outWidth = dst.width(batch_idx); + } + } + + if (x0 >= outWidth || y >= outHeight) + return; + + const int left = *left_.ptr(0, 0, batch_idx); + const int top = *top_.ptr(0, 0, batch_idx); + const int sx0 = x0 - left; + const int sy = y - top; + + int srcHeight; + int srcWidth; + if constexpr (PLANAR) + { + srcHeight = srcRaw.height(batch_idx, plane); + srcWidth = srcRaw.width(batch_idx, plane); + } + else + { + srcHeight = srcRaw.height(batch_idx); + srcWidth = srcRaw.width(batch_idx); + } + + const bool full = x0 + NIX - 1 < outWidth; + const bool interiorRow = sy >= 0 && sy < srcHeight; + const bool interiorSpan = sx0 >= 0 && sx0 + NIX - 1 < srcWidth; + + if (full && interiorRow && interiorSpan) + { + const T *sp; + T *dp; + if constexpr (PLANAR) + { + sp = srcRaw.ptr(batch_idx, plane, sy, sx0); + dp = dst.ptr(batch_idx, plane, y, x0); + } + else + { + sp = srcRaw.ptr(batch_idx, sy, sx0); + dp = dst.ptr(batch_idx, y, x0); + } + + if ((reinterpret_cast(sp) & CMBVS_MSK) == 0 + && (reinterpret_cast(dp) & CMBVS_MSK) == 0) + { + *reinterpret_cast *>(dp) = *reinterpret_cast *>(sp); + } + else + { +#pragma unroll + for (int i = 0; i < NIX; ++i) dp[i] = sp[i]; + } + } + else + { +#pragma unroll + for (int i = 0; i < NIX; ++i) + { + const int x = x0 + i; + if (x < outWidth) + { + if constexpr (PLANAR) + *dst.ptr(batch_idx, plane, y, x) = src[int4{x - left, sy, plane, batch_idx}]; + else + *dst.ptr(batch_idx, y, x) = src[int3{x - left, sy, batch_idx}]; + } + } + } +} + template __global__ void copyMakeBorderKernel(const SrcWrapper src, DstWrapper dst, const cuda::Tensor3DWrap left_, const cuda::Tensor3DWrap top_) @@ -74,19 +240,79 @@ __global__ void copyMakeBorderKernel(const SrcWrapper src, DstWrapper dst, const } } -template +template +__global__ void copyMakeBorderPlanarKernel(const SrcWrapper src, cuda::Tensor4DWrap dst, + const cuda::Tensor3DWrap left_, + const cuda::Tensor3DWrap top_, int out_height, int out_width, + int plane) +{ + const int x = blockIdx.x * blockDim.x + threadIdx.x; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + + const int left = *left_.ptr(0, 0, batch_idx); + const int top = *top_.ptr(0, 0, batch_idx); + const int x_shift = x - left; + const int y_shift = y - top; + + if (x < out_width && y < out_height) + { + *dst.ptr(batch_idx, plane, y, x) = src[int4{x_shift, y_shift, plane, batch_idx}]; + } +} + +template +__global__ void copyMakeBorderPlanarKernel(const SrcWrapper src, cuda::ImageBatchVarShapeWrap dst, + const cuda::Tensor3DWrap left_, + const cuda::Tensor3DWrap top_, int plane) +{ + const int x = blockIdx.x * blockDim.x + threadIdx.x; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + + const int left = *left_.ptr(0, 0, batch_idx); + const int top = *top_.ptr(0, 0, batch_idx); + const int x_shift = x - left; + const int y_shift = y - top; + + int out_height = dst.height(batch_idx, plane), out_width = dst.width(batch_idx, plane); + + if (x < out_width && y < out_height) + { + *dst.ptr(batch_idx, plane, y, x) = src[int4{x_shift, y_shift, plane, batch_idx}]; + } +} + +template struct copyMakeBorderDispatcher { static void call(const ImageBatchVarShapeDataStridedCuda &src, cuda::Tensor3DWrap dst, const T &borderValue, const cuda::Tensor3DWrap &left, const cuda::Tensor3DWrap &top, int max_height, int max_width, cudaStream_t stream) { - dim3 blockSize(BLOCK, BLOCK / 4, 1); - dim3 gridSize(divUp(max_width, blockSize.x), divUp(max_height, blockSize.y), src.numImages()); + constexpr int BLOCK_Y + = FORCE_SCALAR ? BLOCK / 4 + : B == NVCV_BORDER_CONSTANT && sizeof(T) == 1 ? 2 + : B == NVCV_BORDER_REFLECT101 && std::is_same_v, uchar> && cuda::NumElements >= 3 ? 2 + : B == NVCV_BORDER_REFLECT101 || B == NVCV_BORDER_REPLICATE ? 4 + : 8; + dim3 blockSize(BLOCK, BLOCK_Y, 1); cuda::BorderVarShapeWrap brdSrc(src, borderValue); - - copyMakeBorderKernel<<>>(brdSrc, dst, left, top, max_height, max_width); + constexpr int NIX + = sizeof(T) == 1 && (B == NVCV_BORDER_CONSTANT || B == NVCV_BORDER_REPLICATE) ? 4 : CMBVS_NIX; + if constexpr (NIX > 1 && !FORCE_SCALAR) + { + dim3 gridSize(divUp(max_width, blockSize.x * NIX), divUp(max_height, blockSize.y), src.numImages()); + cuda::ImageBatchVarShapeWrap srcRaw(src); + copyMakeBorderKernelVec + <<>>(brdSrc, srcRaw, dst, left, top, max_height, max_width, 0); + } + else + { + dim3 gridSize(divUp(max_width, blockSize.x), divUp(max_height, blockSize.y), src.numImages()); + copyMakeBorderKernel<<>>(brdSrc, dst, left, top, max_height, max_width); + } checkKernelErrors(); #ifdef CUDA_DEBUG_LOG @@ -99,12 +325,29 @@ struct copyMakeBorderDispatcher const T &borderValue, const cuda::Tensor3DWrap &left, const cuda::Tensor3DWrap &top, int max_height, int max_width, cudaStream_t stream) { - dim3 blockSize(BLOCK, BLOCK / 4, 1); - dim3 gridSize(divUp(max_width, blockSize.x), divUp(max_height, blockSize.y), src.numImages()); + constexpr int BLOCK_Y + = FORCE_SCALAR ? BLOCK / 4 + : B == NVCV_BORDER_CONSTANT && sizeof(T) == 1 ? 2 + : B == NVCV_BORDER_REFLECT101 && std::is_same_v, uchar> && cuda::NumElements >= 3 ? 2 + : B == NVCV_BORDER_REFLECT101 || B == NVCV_BORDER_REPLICATE ? 4 + : 8; + dim3 blockSize(BLOCK, BLOCK_Y, 1); cuda::BorderVarShapeWrap brdSrc(src, borderValue); - - copyMakeBorderKernel<<>>(brdSrc, dst, left, top); + constexpr int NIX + = sizeof(T) == 1 && (B == NVCV_BORDER_CONSTANT || B == NVCV_BORDER_REPLICATE) ? 4 : CMBVS_NIX; + if constexpr (NIX > 1 && !FORCE_SCALAR) + { + dim3 gridSize(divUp(max_width, blockSize.x * NIX), divUp(max_height, blockSize.y), src.numImages()); + cuda::ImageBatchVarShapeWrap srcRaw(src); + copyMakeBorderKernelVec + <<>>(brdSrc, srcRaw, dst, left, top, 0, 0, 0); + } + else + { + dim3 gridSize(divUp(max_width, blockSize.x), divUp(max_height, blockSize.y), src.numImages()); + copyMakeBorderKernel<<>>(brdSrc, dst, left, top); + } checkKernelErrors(); #ifdef CUDA_DEBUG_LOG @@ -114,6 +357,68 @@ struct copyMakeBorderDispatcher } }; +template +struct copyMakeBorderPlanarDispatcher +{ + static void call(const ImageBatchVarShapeDataStridedCuda &src, cuda::Tensor4DWrap dst, const float4 &borderValue, + const cuda::Tensor3DWrap &left, const cuda::Tensor3DWrap &top, + int max_height, int max_width, int channels, cudaStream_t stream) + { + dim3 blockSize(BLOCK, BLOCK / 4, 1); + + for (int c = 0; c < channels; ++c) + { + const T bVal = static_cast(PlanarBorderComponent(borderValue, c)); + + cuda::BorderVarShapeWrap brdSrc(src, bVal); + constexpr int NIX = sizeof(T) == 1 ? 4 : CMBVS_NIX; + if constexpr (NIX > 1) + { + dim3 gridSize(divUp(max_width, blockSize.x * NIX), divUp(max_height, blockSize.y), src.numImages()); + cuda::ImageBatchVarShapeWrap srcRaw(src); + copyMakeBorderKernelVec + <<>>(brdSrc, srcRaw, dst, left, top, max_height, max_width, c); + } + else + { + dim3 gridSize(divUp(max_width, blockSize.x), divUp(max_height, blockSize.y), src.numImages()); + copyMakeBorderPlanarKernel<<>>(brdSrc, dst, left, top, max_height, + max_width, c); + } + } + checkKernelErrors(); + } + + static void call(const ImageBatchVarShapeDataStridedCuda &src, cuda::ImageBatchVarShapeWrap dst, + const float4 &borderValue, const cuda::Tensor3DWrap &left, + const cuda::Tensor3DWrap &top, int max_height, int max_width, int channels, + cudaStream_t stream) + { + dim3 blockSize(BLOCK, BLOCK / 4, 1); + + for (int c = 0; c < channels; ++c) + { + const T bVal = static_cast(PlanarBorderComponent(borderValue, c)); + + cuda::BorderVarShapeWrap brdSrc(src, bVal); + constexpr int NIX = sizeof(T) == 1 ? 4 : CMBVS_NIX; + if constexpr (NIX > 1) + { + dim3 gridSize(divUp(max_width, blockSize.x * NIX), divUp(max_height, blockSize.y), src.numImages()); + cuda::ImageBatchVarShapeWrap srcRaw(src); + copyMakeBorderKernelVec + <<>>(brdSrc, srcRaw, dst, left, top, 0, 0, c); + } + else + { + dim3 gridSize(divUp(max_width, blockSize.x), divUp(max_height, blockSize.y), src.numImages()); + copyMakeBorderPlanarKernel<<>>(brdSrc, dst, left, top, c); + } + } + checkKernelErrors(); + } +}; + template // uchar3 float3 uchar float void copyMakeBorder(const ImageBatchVarShapeDataStridedCuda &inData, const OutType &outData, const TensorDataStridedCuda &top, const TensorDataStridedCuda &left, @@ -135,6 +440,16 @@ void copyMakeBorder(const ImageBatchVarShapeDataStridedCuda &inData, const OutTy out_type dstWrap(outData); + if constexpr (std::is_same_v) + { + if (borderType == NVCV_BORDER_REFLECT101 && !UsePackedRGB8Reflect101ForSM(CurrentDeviceSMOrZero())) + { + copyMakeBorderDispatcher::call( + inData, dstWrap, brdVal, leftVec, topVec, outSize.h, outSize.w, stream); + return; + } + } + typedef void (*func_t)(const ImageBatchVarShapeDataStridedCuda &src, out_type dst, const src_type &borderValue, const cuda::Tensor3DWrap &left, const cuda::Tensor3DWrap &top, int max_height, int max_width, cudaStream_t stream); @@ -147,6 +462,57 @@ void copyMakeBorder(const ImageBatchVarShapeDataStridedCuda &inData, const OutTy funcs[borderType](inData, dstWrap, brdVal, leftVec, topVec, outSize.h, outSize.w, stream); } + +template +void copyMakeBorderPlanar(const ImageBatchVarShapeDataStridedCuda &inData, const OutType &outData, + const TensorDataStridedCuda &top, const TensorDataStridedCuda &left, + const NVCVBorderType borderType, const float4 value, cudaStream_t stream, int channels) +{ + cuda::Tensor3DWrap topVec(top); + cuda::Tensor3DWrap leftVec(left); + + auto outSize = GetMaxImageSize(outData); + + if constexpr (std::is_same_v) + { + using out_type = cuda::Tensor4DWrap; + + auto outView = PlanarTensorView(outData); + out_type dstWrap(outView); + + typedef void (*func_t)(const ImageBatchVarShapeDataStridedCuda &src, out_type dst, const float4 &borderValue, + const cuda::Tensor3DWrap &left, + const cuda::Tensor3DWrap &top, int max_height, int max_width, int channels, + cudaStream_t stream); + + static const func_t funcs[] = {copyMakeBorderPlanarDispatcher::call, + copyMakeBorderPlanarDispatcher::call, + copyMakeBorderPlanarDispatcher::call, + copyMakeBorderPlanarDispatcher::call, + copyMakeBorderPlanarDispatcher::call}; + + funcs[borderType](inData, dstWrap, value, leftVec, topVec, outSize.h, outSize.w, channels, stream); + } + else + { + using out_type = cuda::ImageBatchVarShapeWrap; + + out_type dstWrap(outData); + + typedef void (*func_t)(const ImageBatchVarShapeDataStridedCuda &src, out_type dst, const float4 &borderValue, + const cuda::Tensor3DWrap &left, + const cuda::Tensor3DWrap &top, int max_height, int max_width, int channels, + cudaStream_t stream); + + static const func_t funcs[] = {copyMakeBorderPlanarDispatcher::call, + copyMakeBorderPlanarDispatcher::call, + copyMakeBorderPlanarDispatcher::call, + copyMakeBorderPlanarDispatcher::call, + copyMakeBorderPlanarDispatcher::call}; + + funcs[borderType](inData, dstWrap, value, leftVec, topVec, outSize.h, outSize.w, channels, stream); + } +} } // namespace template @@ -164,22 +530,45 @@ ErrorCode CopyMakeBorderVarShape::inferWarp(const ImageBatchVarShapeDataStridedC return ErrorCode::INVALID_DATA_FORMAT; } } + else if (!SameLayoutFamily(input_format, output_format)) + { + LOG_ERROR("Invalid DataFormat between input (" << input_format << ") and output (" << output_format << ")"); + return ErrorCode::INVALID_DATA_FORMAT; + } auto format = input_format; - if (!(format == kNHWC || format == kHWC)) + if (!(format == kNHWC || format == kHWC || format == kNCHW || format == kCHW)) { - LOG_ERROR("Invalid input DataFormat " << format << ", the valid DataFormats are: \"NHWC\", \"HWC\""); + LOG_ERROR("Invalid input DataFormat " << format + << ", the valid DataFormats are: \"NHWC\", \"HWC\", \"NCHW\", \"CHW\""); return ErrorCode::INVALID_DATA_FORMAT; } + const bool isPlanar = IsPlanar(format); + int channels = data_in.uniqueFormat().numChannels(); - if (channels > 4) + if (channels > 4 || (isPlanar && channels == 2)) { LOG_ERROR("Invalid channel number " << channels); return ErrorCode::INVALID_DATA_SHAPE; } - DataType data_type = GetLegacyDataType(data_in.uniqueFormat()); + DataType data_type = GetLegacyDataType(data_in.uniqueFormat()); + DataType out_data_type = GetOutputDataType(data_out); + + if (data_type != out_data_type) + { + LOG_ERROR("DataType of input and output must be equal, but got " << data_type << " and " << out_data_type); + return ErrorCode::INVALID_DATA_TYPE; + } + + if (!isPlanar && channels == 2 && data_type != kCV_8U) + { + LOG_ERROR("Invalid channel number " << channels << " for DataType " << data_type + << ", 2 channels only supported for 8bit Unsigned"); + return ErrorCode::INVALID_DATA_SHAPE; + } + if (!(data_type == kCV_8U || data_type == kCV_16U || data_type == kCV_16S || data_type == kCV_32F)) { LOG_ERROR("Invalid DataType " << data_type); @@ -235,6 +624,25 @@ ErrorCode CopyMakeBorderVarShape::inferWarp(const ImageBatchVarShapeDataStridedC // clang-format on const func_t func = funcs[data_type][channels - 1]; + + if (isPlanar) + { + typedef void (*planar_func_t)(const ImageBatchVarShapeDataStridedCuda &d_in, const OutType &d_out, + const TensorDataStridedCuda &top, const TensorDataStridedCuda &left, + const NVCVBorderType borderType, const float4 value, cudaStream_t stream, + int channels); + + static const planar_func_t planarFuncs[6] = { + copyMakeBorderPlanar, 0 /*schar*/, copyMakeBorderPlanar, + copyMakeBorderPlanar, 0 /*int*/, copyMakeBorderPlanar, + }; + + const planar_func_t planarFunc = planarFuncs[data_type]; + NVCV_ASSERT(planarFunc != 0); + planarFunc(data_in, data_out, top, left, borderType, value, stream, channels); + return SUCCESS; + } + NVCV_ASSERT(func != 0); func(data_in, data_out, top, left, borderType, value, stream); diff --git a/src/cvcuda/priv/legacy/custom_crop.cu b/src/cvcuda/priv/legacy/custom_crop.cu index 056ca17fe..880af6020 100644 --- a/src/cvcuda/priv/legacy/custom_crop.cu +++ b/src/cvcuda/priv/legacy/custom_crop.cu @@ -1,4 +1,4 @@ -/* Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +/* Copyright (c) 2021-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * * SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES * SPDX-License-Identifier: Apache-2.0 @@ -23,27 +23,36 @@ #include "CvCudaUtils.cuh" +#include #include #include #include #include +#include #include using namespace nvcv::legacy::cuda_op; using namespace nvcv::legacy::helpers; -template +template __global__ void custom_crop_kernel(const SrcWrapper src, DstWrapper dst, int start_x, int start_y, int width, int height) { const int x = blockIdx.x * blockDim.x + threadIdx.x; - const int y = blockIdx.y * blockDim.y + threadIdx.y; + const int y = (blockIdx.y * blockDim.y + threadIdx.y) * NRY; const int batch_idx = get_batch_idx(); - if (x >= width || y >= height) + if (x >= width) return; - *dst.ptr(batch_idx, y, x) = *src.ptr(batch_idx, y + start_y, x + start_x); +#pragma unroll + for (int i = 0; i < NRY; ++i) + { + if (y + i < height) + { + *dst.ptr(batch_idx, y + i, x) = *src.ptr(batch_idx, y + i + start_y, x + start_x); + } + } } template @@ -56,8 +65,10 @@ ErrorCode customCrop(const nvcv::TensorDataStridedCuda &inData, const nvcv::Tens auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(inData); NVCV_ASSERT(inAccess); - dim3 block(16, 16); - dim3 grid(divUp(roi.width, block.x), divUp(roi.height, block.y), outAccess->numSamples()); + constexpr int blockWidth = sizeof(T) <= 3 ? 32 : 16; + constexpr int rowsPerThread = sizeof(T) == 3 ? 2 : 1; + dim3 block(blockWidth, 256 / blockWidth); + dim3 grid(divUp(roi.width, block.x), divUp(roi.height, block.y * rowsPerThread), outAccess->numSamples()); auto outMaxStride = outAccess->sampleStride() * outAccess->numSamples(); auto inMaxStride = inAccess->sampleStride() * inAccess->numSamples(); @@ -66,7 +77,7 @@ ErrorCode customCrop(const nvcv::TensorDataStridedCuda &inData, const nvcv::Tens auto src = nvcv::cuda::CreateTensorWrapNHW(inData); auto dst = nvcv::cuda::CreateTensorWrapNHW(outData); - custom_crop_kernel<<>>(src, dst, roi.x, roi.y, roi.width, roi.height); + custom_crop_kernel<<>>(src, dst, roi.x, roi.y, roi.width, roi.height); } else { @@ -77,11 +88,69 @@ ErrorCode customCrop(const nvcv::TensorDataStridedCuda &inData, const nvcv::Tens return ErrorCode::SUCCESS; } +static bool tryCopyDenseU8NHWC(const nvcv::TensorDataStridedCuda &inData, const nvcv::TensorDataStridedCuda &outData, + NVCVRectI roi, cudaStream_t stream) +{ + if (inData.rank() != 4 || outData.rank() != 4 || inData.layout() != nvcv::TENSOR_NHWC + || outData.layout() != nvcv::TENSOR_NHWC || inData.dtype() != outData.dtype()) + { + return false; + } + + const auto bitsPerChannel = inData.dtype().bitsPerChannel(); + if (bitsPerChannel[0] != 8) + { + return false; + } + const int64_t bytesPerChannel = bitsPerChannel[0] / 8; + + const int64_t inBatch = inData.shape(0); + const int64_t inRows = inData.shape(1); + const int64_t inCols = inData.shape(2); + const int64_t inCh = inData.shape(3); + + const int64_t outBatch = outData.shape(0); + const int64_t outRows = outData.shape(1); + const int64_t outCols = outData.shape(2); + const int64_t outCh = outData.shape(3); + + const int64_t pixelBytes = inCh * bytesPerChannel; + if (inBatch != outBatch || inCh != 1 || outCh != 1 || inData.stride(3) != bytesPerChannel + || outData.stride(3) != bytesPerChannel || inData.stride(2) != pixelBytes || outData.stride(2) != pixelBytes + || inData.stride(1) < inCols * pixelBytes || outData.stride(1) < outCols * pixelBytes + || inData.stride(0) != inData.stride(1) * inRows || outData.stride(0) != outData.stride(1) * outRows) + { + return false; + } + + cudaMemcpy3DParms params{}; + params.srcPtr + = make_cudaPitchedPtr(inData.basePtr(), static_cast(inData.stride(1)), + static_cast(inCols * pixelBytes), static_cast(inRows)); + params.dstPtr + = make_cudaPitchedPtr(outData.basePtr(), static_cast(outData.stride(1)), + static_cast(outCols * pixelBytes), static_cast(outRows)); + params.srcPos = make_cudaPos(static_cast(roi.x * pixelBytes), static_cast(roi.y), 0); + params.dstPos = make_cudaPos(0, 0, 0); + params.extent = make_cudaExtent(static_cast(roi.width * pixelBytes), + static_cast(roi.height), static_cast(inBatch)); + params.kind = cudaMemcpyDeviceToDevice; + + checkCudaErrors(cudaMemcpy3DAsync(¶ms, stream)); + return true; +} + namespace nvcv::legacy::cuda_op { ErrorCode CustomCrop::infer(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, NVCVRectI roi, cudaStream_t stream) { + if (roi.width <= 0 || roi.height <= 0) + { + LOG_ERROR("ROI width and height must be positive"); + return ErrorCode::INVALID_PARAMETER; + } + cuda_op::DataFormat input_format = GetLegacyDataFormat(inData.layout()); cuda_op::DataFormat output_format = GetLegacyDataFormat(outData.layout()); @@ -98,6 +167,15 @@ ErrorCode CustomCrop::infer(const TensorDataStridedCuda &inData, const TensorDat return ErrorCode::INVALID_DATA_FORMAT; } + const cuda_op::DataType data_type = GetLegacyDataType(inData.dtype()); + + if (!(data_type == kCV_8U || data_type == kCV_8S || data_type == kCV_16U || data_type == kCV_16S + || data_type == kCV_16F || data_type == kCV_32S || data_type == kCV_32F || data_type == kCV_64F)) + { + LOG_ERROR("Invalid DataType " << data_type); + return ErrorCode::INVALID_DATA_TYPE; + } + auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(inData); if (!inAccess) { @@ -121,6 +199,12 @@ ErrorCode CustomCrop::infer(const TensorDataStridedCuda &inData, const TensorDat return ErrorCode::INVALID_DATA_FORMAT; } + if (batch != outAccess->numSamples() || channels != outAccess->numChannels()) + { + LOG_ERROR("Input and output must have the same sample and channel counts"); + return ErrorCode::INVALID_DATA_SHAPE; + } + if (roi.height > outAccess->size().h || roi.width > outAccess->size().w) { LOG_ERROR("ROI larger than dst buffer"); @@ -143,6 +227,11 @@ ErrorCode CustomCrop::infer(const TensorDataStridedCuda &inData, const TensorDat return ErrorCode::INVALID_PARAMETER; } + if (tryCopyDenseU8NHWC(inData, outData, roi, stream)) + { + return ErrorCode::SUCCESS; + } + typedef ErrorCode (*func_t)(const nvcv::TensorDataStridedCuda &inData, const nvcv::TensorDataStridedCuda &outData, NVCVRectI roi, cudaStream_t stream); diff --git a/src/cvcuda/priv/legacy/cvt_color.cu b/src/cvcuda/priv/legacy/cvt_color.cu index eadc8d999..7a23edb9d 100644 --- a/src/cvcuda/priv/legacy/cvt_color.cu +++ b/src/cvcuda/priv/legacy/cvt_color.cu @@ -97,6 +97,11 @@ using TensorWrap3D = nvcv::cuda::Tensor3DWrap; template using TensorWrap4D = nvcv::cuda::Tensor4DWrap; +static bool IsPlanar(DataFormat format) +{ + return format == kNCHW || format == kCHW; +} + template struct CvtKernelPolicy { @@ -213,6 +218,47 @@ DEVICE_INLINE void store_bgra_nhwc(const TensorWrap3D &dst, EltT *dst.ptr(batch_idx, y, x) = vec; } +template +DEVICE_INLINE void load3_nchw(const TensorWrap4D &src, EltT &C0, EltT &C1, EltT &C2, int batch_idx, + int x, int y) +{ + C0 = *src.ptr(batch_idx, 0, y, x); + C1 = *src.ptr(batch_idx, 1, y, x); + C2 = *src.ptr(batch_idx, 2, y, x); +} + +template +DEVICE_INLINE void store3_nchw(const TensorWrap4D &dst, EltT C0, EltT C1, EltT C2, int batch_idx, int x, + int y) +{ + *dst.ptr(batch_idx, 0, y, x) = C0; + *dst.ptr(batch_idx, 1, y, x) = C1; + *dst.ptr(batch_idx, 2, y, x) = C2; +} + +template +DEVICE_INLINE void load_bgra_nchw(const TensorWrap4D &src, EltT &B, EltT &G, EltT &R, EltT &A, + int batch_idx, int x, int y, int bidx, int srcChannels) +{ + B = *src.ptr(batch_idx, bidx, y, x); + G = *src.ptr(batch_idx, 1, y, x); + R = *src.ptr(batch_idx, bidx ^ 2, y, x); + A = srcChannels == 4 ? *src.ptr(batch_idx, 3, y, x) : Alpha; +} + +template +DEVICE_INLINE void store_bgra_nchw(const TensorWrap4D &dst, EltT B, EltT G, EltT R, EltT A, + int batch_idx, int x, int y, int bidx, int dstChannels) +{ + *dst.ptr(batch_idx, bidx, y, x) = B; + *dst.ptr(batch_idx, 1, y, x) = G; + *dst.ptr(batch_idx, bidx ^ 2, y, x) = R; + if (dstChannels == 4) + { + *dst.ptr(batch_idx, 3, y, x) = A; + } +} + template GLOBAL_BOUNDS void rgb_to_bgr_nhwc(const TensorWrap3D src, const TensorWrap3D dst, int2 dstSize, int bidx) @@ -231,6 +277,24 @@ GLOBAL_BOUNDS void rgb_to_bgr_nhwc(const TensorWrap3D src, dstSize); } +template +GLOBAL_BOUNDS void rgb_to_bgr_nchw(const TensorWrap4D src, const TensorWrap4D dst, + int2 dstSize, int bidx, int srcChannels, int dstChannels) +{ + using EltT = nvcv::cuda::BaseType; + color_conversion_common( + [&src, bidx, srcChannels] __device__(EltT(&r_in)[4], int batch_idx, int x, int y) + { load_bgra_nchw(src, r_in[0], r_in[1], r_in[2], r_in[3], batch_idx, x, y, bidx, srcChannels); }, + [] __device__(const EltT(&r_in)[4], EltT(&r_out)[4]) + { +#pragma unroll + for (int i = 0; i < 4; i++) r_out[i] = r_in[i]; + }, + [&dst, dstChannels] __device__(const EltT(&r_out)[4], int batch_idx, int x, int y) + { store_bgra_nchw(dst, r_out[0], r_out[1], r_out[2], r_out[3], batch_idx, x, y, 0, dstChannels); }, + dstSize); +} + template GLOBAL_BOUNDS void gray_to_bgr_nhwc(const TensorWrap3D src, const TensorWrap3D dst, int2 dstSize) @@ -248,6 +312,23 @@ GLOBAL_BOUNDS void gray_to_bgr_nhwc(const TensorWrap3D src, dstSize); } +template +GLOBAL_BOUNDS void gray_to_bgr_nchw(const TensorWrap4D src, const TensorWrap4D dst, + int2 dstSize, int dstChannels) +{ + using EltT = nvcv::cuda::BaseType; + color_conversion_common( + [&src] __device__(EltT(&r_gray)[1], int batch_idx, int x, int y) { r_gray[0] = *src.ptr(batch_idx, 0, y, x); }, + [] __device__(const EltT(&r_gray)[1], EltT(&r_BGRA)[4]) + { +#pragma unroll + for (int i = 0; i < 4; i++) r_BGRA[i] = r_gray[0]; + }, + [&dst, dstChannels] __device__(const EltT(&r_BGRA)[4], int batch_idx, int x, int y) + { store_bgra_nchw(dst, r_BGRA[0], r_BGRA[1], r_BGRA[2], r_BGRA[3], batch_idx, x, y, 0, dstChannels); }, + dstSize); +} + template GLOBAL_BOUNDS void bgr_to_gray_nhwc(const TensorWrap3D src, const TensorWrap3D dst, int2 dstSize, int bidx) @@ -272,6 +353,30 @@ GLOBAL_BOUNDS void bgr_to_gray_nhwc(const TensorWrap3D src, dstSize); } +template +GLOBAL_BOUNDS void bgr_to_gray_nchw(const TensorWrap4D src, const TensorWrap4D dst, + int2 dstSize, int bidx, int srcChannels) +{ + using EltT = nvcv::cuda::BaseType; + color_conversion_common( + [&src, bidx, srcChannels] __device__(EltT(&r_BGR)[3], int batch_idx, int x, int y) + { + EltT A; + load_bgra_nchw(src, r_BGR[0], r_BGR[1], r_BGR[2], A, batch_idx, x, y, bidx, srcChannels); + }, + [] __device__(const EltT(&r_BGR)[3], EltT(&r_gray)[1]) + { + if constexpr (std::is_integral_v) + r_gray[0] + = (EltT)CV_DESCALE((int)r_BGR[0] * BY15 + (int)r_BGR[1] * GY15 + (int)r_BGR[2] * RY15, gray_shift); + else + r_gray[0] = (EltT)(r_BGR[0] * B2YF + r_BGR[1] * G2YF + r_BGR[2] * R2YF); + }, + [&dst] __device__(const EltT(&r_gray)[1], int batch_idx, int x, int y) + { *dst.ptr(batch_idx, 0, y, x) = r_gray[0]; }, + dstSize); +} + template DEVICE_INLINE void bgr_to_yuv_int(T B_, T G_, T R_, T &Y_, T &Cb_, T &Cr_) { @@ -322,6 +427,29 @@ GLOBAL_BOUNDS void bgr_to_yuv_nhwc(const TensorWrap3D src, dstSize); } +template +GLOBAL_BOUNDS void bgr_to_yuv_nchw(const TensorWrap4D src, const TensorWrap4D dst, + int2 dstSize, int bidx, int srcChannels) +{ + using EltT = nvcv::cuda::BaseType; + color_conversion_common( + [&src, bidx, srcChannels] __device__(EltT(&r_BGR)[3], int batch_idx, int x, int y) + { + EltT A; + load_bgra_nchw(src, r_BGR[0], r_BGR[1], r_BGR[2], A, batch_idx, x, y, bidx, srcChannels); + }, + [] __device__(const EltT(&r_BGR)[3], EltT(&r_YCbCr)[3]) + { + if constexpr (std::is_integral_v) + bgr_to_yuv_int(r_BGR[0], r_BGR[1], r_BGR[2], r_YCbCr[0], r_YCbCr[1], r_YCbCr[2]); + else + bgr_to_yuv_float(r_BGR[0], r_BGR[1], r_BGR[2], r_YCbCr[0], r_YCbCr[1], r_YCbCr[2]); + }, + [&dst] __device__(const EltT(&r_YCbCr)[3], int batch_idx, int x, int y) + { store3_nchw(dst, r_YCbCr[0], r_YCbCr[1], r_YCbCr[2], batch_idx, x, y); }, + dstSize); +} + template DEVICE_INLINE void yuv_to_bgr_int(T Y_, T Cb_, T Cr_, T &B_, T &G_, T &R_) { @@ -368,6 +496,26 @@ GLOBAL_BOUNDS void yuv_to_bgr_nhwc(const TensorWrap3D src, dstSize); } +template +GLOBAL_BOUNDS void yuv_to_bgr_nchw(const TensorWrap4D src, const TensorWrap4D dst, + int2 dstSize, int bidx, int dstChannels) +{ + using EltT = nvcv::cuda::BaseType; + color_conversion_common( + [&src] __device__(EltT(&r_YCbCr)[3], int batch_idx, int x, int y) + { load3_nchw(src, r_YCbCr[0], r_YCbCr[1], r_YCbCr[2], batch_idx, x, y); }, + [] __device__(const EltT(&r_YCbCr)[3], EltT(&r_BGR)[3]) + { + if constexpr (std::is_integral_v) + yuv_to_bgr_int(r_YCbCr[0], r_YCbCr[1], r_YCbCr[2], r_BGR[0], r_BGR[1], r_BGR[2]); + else + yuv_to_bgr_flt(r_YCbCr[0], r_YCbCr[1], r_YCbCr[2], r_BGR[0], r_BGR[1], r_BGR[2]); + }, + [&dst, bidx, dstChannels] __device__(const EltT(&r_BGR)[3], int batch_idx, int x, int y) + { store_bgra_nchw(dst, r_BGR[0], r_BGR[1], r_BGR[2], Alpha, batch_idx, x, y, bidx, dstChannels); }, + dstSize); +} + DEVICE_INLINE void bgr_to_hsv_uchar(uchar b8, uchar g8, uchar r8, uchar &h8, uchar &s8, uchar &v8, bool isFullRange) { const int hrange = isFullRange ? 256 : 180; @@ -438,6 +586,29 @@ GLOBAL_BOUNDS void bgr_to_hsv_nhwc(const TensorWrap3D src, dstSize); } +template +GLOBAL_BOUNDS void bgr_to_hsv_nchw(const TensorWrap4D src, const TensorWrap4D dst, + int2 dstSize, int bidx, bool isFullRange, int srcChannels) +{ + using EltT = nvcv::cuda::BaseType; + color_conversion_common( + [&src, bidx, srcChannels] __device__(EltT(&r_BGR)[3], int batch_idx, int x, int y) + { + EltT A; + load_bgra_nchw(src, r_BGR[0], r_BGR[1], r_BGR[2], A, batch_idx, x, y, bidx, srcChannels); + }, + [isFullRange] __device__(const EltT(&r_BGR)[3], EltT(&r_HSV)[3]) + { + if constexpr (std::is_integral_v) + bgr_to_hsv_uchar(r_BGR[0], r_BGR[1], r_BGR[2], r_HSV[0], r_HSV[1], r_HSV[2], isFullRange); + else + bgr_to_hsv_float(r_BGR[0], r_BGR[1], r_BGR[2], r_HSV[0], r_HSV[1], r_HSV[2]); + }, + [&dst] __device__(const EltT(&r_HSV)[3], int batch_idx, int x, int y) + { store3_nchw(dst, r_HSV[0], r_HSV[1], r_HSV[2], batch_idx, x, y); }, + dstSize); +} + template DEVICE_INLINE T select4_reg(const T (&tab)[4], int idx) { @@ -513,6 +684,41 @@ GLOBAL_BOUNDS void hsv_to_bgr_nhwc(const TensorWrap3D src, dstSize); } +template +GLOBAL_BOUNDS void hsv_to_bgr_nchw(const TensorWrap4D src, const TensorWrap4D dst, + int2 dstSize, int bidx, bool isFullRange, int dstChannels) +{ + using EltT = nvcv::cuda::BaseType; + color_conversion_common( + [&src] __device__(EltT(&r_HSV)[3], int batch_idx, int x, int y) + { load3_nchw(src, r_HSV[0], r_HSV[1], r_HSV[2], batch_idx, x, y); }, + [isFullRange] __device__(const EltT(&r_HSV)[3], EltT(&r_BGR)[3]) + { + if constexpr (std::is_same_v) + { + const float scaleH = isFullRange ? (6.0f / 256.0f) : (6.0f / 180.0f); + constexpr float scaleSV = 1.0f / 255.0f; + + float Bf, Gf, Rf; + + hsv_to_bgr_float((float)r_HSV[0] * scaleH, r_HSV[1] * scaleSV, r_HSV[2] * scaleSV, Bf, Gf, Rf); + + r_BGR[0] = cuda::SaturateCast(Bf * 255.0f); + r_BGR[1] = cuda::SaturateCast(Gf * 255.0f); + r_BGR[2] = cuda::SaturateCast(Rf * 255.0f); + } + else + { + constexpr float scaleH = 6.0f / 360.0f; + + hsv_to_bgr_float(r_HSV[0] * scaleH, r_HSV[1], r_HSV[2], r_BGR[0], r_BGR[1], r_BGR[2]); + } + }, + [&dst, bidx, dstChannels] __device__(const EltT(&r_BGR)[3], int batch_idx, int x, int y) + { store_bgra_nchw(dst, r_BGR[0], r_BGR[1], r_BGR[2], Alpha, batch_idx, x, y, bidx, dstChannels); }, + dstSize); +} + template DEVICE_INLINE void load_yuv420(const nvcv::cuda::Tensor4DWrap &src, EltT &Y, EltT &U, EltT &V, int2 size, int batch_idx, int x, int y, int uidx) @@ -774,6 +980,32 @@ inline ErrorCode Launch_BGR_to_RGB(const TensorDataStridedCuda &inData, const Te return ErrorCode::SUCCESS; } +template +inline ErrorCode Launch_BGR_to_RGB_Planar(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, + cuda_op::DataShape shape, int bidx, int srcChannels, int dstChannels, + cudaStream_t stream) +{ + using Policy = CvtKernelPolicy<32, 4, 4>; + + if (shape.N > 65535) + { + LOG_ERROR("Planar CvtColor requires numImages <= 65535 (CUDA grid-z limit)"); + return ErrorCode::INVALID_DATA_SHAPE; + } + + dim3 blockSize(Policy::BlockWidth, Policy::BlockHeight); + dim3 gridSize(divUp(shape.W, Policy::TileWidth), divUp(shape.H, Policy::TileHeight), shape.N); + int2 dstSize{shape.W, shape.H}; + + auto srcWrap = cuda::CreateTensorWrapNCHW(inData); + auto dstWrap = cuda::CreateTensorWrapNCHW(outData); + rgb_to_bgr_nchw + <<>>(srcWrap, dstWrap, dstSize, bidx, srcChannels, dstChannels); + checkKernelErrors(); + + return ErrorCode::SUCCESS; +} + inline ErrorCode BGR_to_RGB(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, NVCVColorConversionCode code, cudaStream_t stream) { @@ -790,6 +1022,8 @@ inline ErrorCode BGR_to_RGB(const TensorDataStridedCuda &inData, const TensorDat auto outAccess = TensorDataAccessStridedImagePlanar::Create(outData); NVCV_ASSERT(outAccess); + const bool isPlanar = IsPlanar(helpers::GetLegacyDataFormat(inData.layout())); + cuda_op::DataType outDataType = helpers::GetLegacyDataType(outData.dtype()); cuda_op::DataShape outputShape = helpers::GetLegacyDataShape(outAccess->infoShape()); @@ -820,6 +1054,32 @@ inline ErrorCode BGR_to_RGB(const TensorDataStridedCuda &inData, const TensorDat return ErrorCode::INVALID_DATA_SHAPE; } + if (isPlanar) + { +#define CVCUDA_BGR2RGB_PLANAR_CASE(T) \ + return Launch_BGR_to_RGB_Planar(inData, outData, inputShape, bidx, sch, dch, stream) + switch (inDataType) + { + case kCV_8U: + case kCV_8S: + CVCUDA_BGR2RGB_PLANAR_CASE(uchar); + case kCV_16F: // Not properly handled when adding alpha to the destination. + case kCV_16U: + case kCV_16S: + CVCUDA_BGR2RGB_PLANAR_CASE(ushort); + case kCV_32S: + CVCUDA_BGR2RGB_PLANAR_CASE(int); + case kCV_32F: + CVCUDA_BGR2RGB_PLANAR_CASE(float); + case kCV_64F: + CVCUDA_BGR2RGB_PLANAR_CASE(double); + default: + LOG_ERROR("Unsupported DataType " << inDataType); + return ErrorCode::INVALID_DATA_TYPE; + } +#undef CVCUDA_BGR2RGB_PLANAR_CASE + } + #define CVCUDA_BGR2RGB_IF(SCH, DCH, SRC_T, DST_T) \ if (sch == SCH && dch == DCH) \ return Launch_BGR_to_RGB(inData, outData, code, inputShape, bidx, stream) @@ -874,6 +1134,31 @@ inline ErrorCode Launch_GRAY_to_BGR(const TensorDataStridedCuda &inData, const T return ErrorCode::SUCCESS; } +template +inline ErrorCode Launch_GRAY_to_BGR_Planar(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, + cuda_op::DataShape shape, int dstChannels, cudaStream_t stream) +{ + using Policy = CvtKernelPolicy<32, 4, 8>; + + if (shape.N > 65535) + { + LOG_ERROR("Planar CvtColor requires numImages <= 65535 (CUDA grid-z limit)"); + return ErrorCode::INVALID_DATA_SHAPE; + } + + dim3 blockSize(Policy::BlockWidth, Policy::BlockHeight); + dim3 gridSize(divUp(shape.W, Policy::TileWidth), divUp(shape.H, Policy::TileHeight), shape.N); + + int2 dstSize{shape.W, shape.H}; + + auto srcWrap = cuda::CreateTensorWrapNCHW(inData); + auto dstWrap = cuda::CreateTensorWrapNCHW(outData); + gray_to_bgr_nchw<<>>(srcWrap, dstWrap, dstSize, dstChannels); + checkKernelErrors(); + + return ErrorCode::SUCCESS; +} + inline ErrorCode GRAY_to_BGR(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, NVCVColorConversionCode code, cudaStream_t stream) { @@ -888,6 +1173,8 @@ inline ErrorCode GRAY_to_BGR(const TensorDataStridedCuda &inData, const TensorDa auto outAccess = TensorDataAccessStridedImagePlanar::Create(outData); NVCV_ASSERT(outAccess); + const bool isPlanar = IsPlanar(helpers::GetLegacyDataFormat(inData.layout())); + cuda_op::DataType outDataType = helpers::GetLegacyDataType(outData.dtype()); cuda_op::DataShape outputShape = helpers::GetLegacyDataShape(outAccess->infoShape()); @@ -918,6 +1205,31 @@ inline ErrorCode GRAY_to_BGR(const TensorDataStridedCuda &inData, const TensorDa return ErrorCode::INVALID_DATA_SHAPE; } + if (isPlanar) + { +#define CVCUDA_GRAY2BGR_PLANAR_CASE(T) return Launch_GRAY_to_BGR_Planar(inData, outData, inputShape, dch, stream) + switch (inDataType) + { + case kCV_8U: + case kCV_8S: + CVCUDA_GRAY2BGR_PLANAR_CASE(uchar); + case kCV_16F: // Not properly handled when adding alpha to the destination. + case kCV_16U: + case kCV_16S: + CVCUDA_GRAY2BGR_PLANAR_CASE(ushort); + case kCV_32S: + CVCUDA_GRAY2BGR_PLANAR_CASE(int); + case kCV_32F: + CVCUDA_GRAY2BGR_PLANAR_CASE(float); + case kCV_64F: + CVCUDA_GRAY2BGR_PLANAR_CASE(double); + default: + LOG_ERROR("Unsupported DataType " << inDataType); + return ErrorCode::INVALID_DATA_TYPE; + } +#undef CVCUDA_GRAY2BGR_PLANAR_CASE + } + #define CVCUDA_GRAY2BGR_IF(DCH, SRC_T, DST_T) \ if (dch == DCH) \ return Launch_GRAY_to_BGR(inData, outData, inputShape, stream) @@ -970,6 +1282,31 @@ inline ErrorCode Launch_BGR_to_GRAY(const TensorDataStridedCuda &inData, const T return ErrorCode::SUCCESS; } +template +inline ErrorCode Launch_BGR_to_GRAY_Planar(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, + cuda_op::DataShape shape, int bidx, int srcChannels, cudaStream_t stream) +{ + using Policy = CvtKernelPolicy<32, 4, 4>; + + if (shape.N > 65535) + { + LOG_ERROR("Planar CvtColor requires numImages <= 65535 (CUDA grid-z limit)"); + return ErrorCode::INVALID_DATA_SHAPE; + } + + dim3 blockSize(Policy::BlockWidth, Policy::BlockHeight); + dim3 gridSize(divUp(shape.W, Policy::TileWidth), divUp(shape.H, Policy::TileHeight), shape.N); + + int2 dstSize{shape.W, shape.H}; + + auto srcWrap = cuda::CreateTensorWrapNCHW(inData); + auto dstWrap = cuda::CreateTensorWrapNCHW(outData); + bgr_to_gray_nchw<<>>(srcWrap, dstWrap, dstSize, bidx, srcChannels); + checkKernelErrors(); + + return ErrorCode::SUCCESS; +} + inline ErrorCode BGR_to_GRAY(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, NVCVColorConversionCode code, cudaStream_t stream) { @@ -985,6 +1322,8 @@ inline ErrorCode BGR_to_GRAY(const TensorDataStridedCuda &inData, const TensorDa auto outAccess = TensorDataAccessStridedImagePlanar::Create(outData); NVCV_ASSERT(outAccess); + const bool isPlanar = IsPlanar(helpers::GetLegacyDataFormat(inData.layout())); + cuda_op::DataType outDataType = helpers::GetLegacyDataType(outData.dtype()); cuda_op::DataShape outputShape = helpers::GetLegacyDataShape(outAccess->infoShape()); @@ -1010,6 +1349,25 @@ inline ErrorCode BGR_to_GRAY(const TensorDataStridedCuda &inData, const TensorDa return ErrorCode::INVALID_DATA_SHAPE; } + if (isPlanar) + { +#define CVCUDA_BGR2GRAY_PLANAR_CASE(T) \ + return Launch_BGR_to_GRAY_Planar(inData, outData, inputShape, bidx, sch, stream) + switch (inDataType) + { + case kCV_8U: + CVCUDA_BGR2GRAY_PLANAR_CASE(uchar); + case kCV_16U: + CVCUDA_BGR2GRAY_PLANAR_CASE(ushort); + case kCV_32F: + CVCUDA_BGR2GRAY_PLANAR_CASE(float); + default: + LOG_ERROR("Unsupported DataType " << inDataType); + return ErrorCode::INVALID_DATA_TYPE; + } +#undef CVCUDA_BGR2GRAY_PLANAR_CASE + } + #define CVCUDA_BGR2GRAY_IF(SCH, SRC_T, DST_T) \ if (sch == SCH) \ return Launch_BGR_to_GRAY(inData, outData, inputShape, bidx, stream) @@ -1055,6 +1413,31 @@ inline ErrorCode Launch_BGR_to_YUV(const TensorDataStridedCuda &inData, const Te return ErrorCode::SUCCESS; } +template +inline ErrorCode Launch_BGR_to_YUV_Planar(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, + cuda_op::DataShape shape, int bidx, int srcChannels, cudaStream_t stream) +{ + using Policy = CvtKernelPolicy<32, 4, 4>; + + if (shape.N > 65535) + { + LOG_ERROR("Planar CvtColor requires numImages <= 65535 (CUDA grid-z limit)"); + return ErrorCode::INVALID_DATA_SHAPE; + } + + dim3 blockSize(Policy::BlockWidth, Policy::BlockHeight); + dim3 gridSize(divUp(shape.W, Policy::TileWidth), divUp(shape.H, Policy::TileHeight), shape.N); + + int2 dstSize{shape.W, shape.H}; + + auto srcWrap = cuda::CreateTensorWrapNCHW(inData); + auto dstWrap = cuda::CreateTensorWrapNCHW(outData); + bgr_to_yuv_nchw<<>>(srcWrap, dstWrap, dstSize, bidx, srcChannels); + checkKernelErrors(); + + return ErrorCode::SUCCESS; +} + inline ErrorCode BGR_to_YUV(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, NVCVColorConversionCode code, cudaStream_t stream) { @@ -1069,6 +1452,8 @@ inline ErrorCode BGR_to_YUV(const TensorDataStridedCuda &inData, const TensorDat auto outAccess = TensorDataAccessStridedImagePlanar::Create(outData); NVCV_ASSERT(outAccess); + const bool isPlanar = IsPlanar(helpers::GetLegacyDataFormat(inData.layout())); + cuda_op::DataType outDataType = helpers::GetLegacyDataType(outData.dtype()); cuda_op::DataShape outputShape = helpers::GetLegacyDataShape(outAccess->infoShape()); @@ -1088,6 +1473,25 @@ inline ErrorCode BGR_to_YUV(const TensorDataStridedCuda &inData, const TensorDat return ErrorCode::INVALID_DATA_SHAPE; } + if (isPlanar) + { +#define CVCUDA_BGR2YUV_PLANAR_CASE(T) \ + return Launch_BGR_to_YUV_Planar(inData, outData, inputShape, bidx, inputShape.C, stream) + switch (inDataType) + { + case kCV_8U: + CVCUDA_BGR2YUV_PLANAR_CASE(uchar); + case kCV_16U: + CVCUDA_BGR2YUV_PLANAR_CASE(ushort); + case kCV_32F: + CVCUDA_BGR2YUV_PLANAR_CASE(float); + default: + LOG_ERROR("Unsupported DataType " << inDataType); + return ErrorCode::INVALID_DATA_TYPE; + } +#undef CVCUDA_BGR2YUV_PLANAR_CASE + } + #define CVCUDA_BGR2YUV_CASE(T3) return Launch_BGR_to_YUV(inData, outData, inputShape, bidx, stream) switch (inDataType) { @@ -1124,6 +1528,31 @@ inline ErrorCode Launch_YUV_to_BGR(const TensorDataStridedCuda &inData, const Te return ErrorCode::SUCCESS; } +template +inline ErrorCode Launch_YUV_to_BGR_Planar(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, + cuda_op::DataShape shape, int bidx, int dstChannels, cudaStream_t stream) +{ + using Policy = CvtKernelPolicy<32, 4, 4>; + + if (shape.N > 65535) + { + LOG_ERROR("Planar CvtColor requires numImages <= 65535 (CUDA grid-z limit)"); + return ErrorCode::INVALID_DATA_SHAPE; + } + + dim3 blockSize(Policy::BlockWidth, Policy::BlockHeight); + dim3 gridSize(divUp(shape.W, Policy::TileWidth), divUp(shape.H, Policy::TileHeight), shape.N); + + int2 dstSize{shape.W, shape.H}; + + auto srcWrap = cuda::CreateTensorWrapNCHW(inData); + auto dstWrap = cuda::CreateTensorWrapNCHW(outData); + yuv_to_bgr_nchw<<>>(srcWrap, dstWrap, dstSize, bidx, dstChannels); + checkKernelErrors(); + + return ErrorCode::SUCCESS; +} + inline ErrorCode YUV_to_BGR(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, NVCVColorConversionCode code, cudaStream_t stream) { @@ -1138,6 +1567,8 @@ inline ErrorCode YUV_to_BGR(const TensorDataStridedCuda &inData, const TensorDat auto outAccess = TensorDataAccessStridedImagePlanar::Create(outData); NVCV_ASSERT(outAccess); + const bool isPlanar = IsPlanar(helpers::GetLegacyDataFormat(inData.layout())); + cuda_op::DataType outDataType = helpers::GetLegacyDataType(outData.dtype()); cuda_op::DataShape outputShape = helpers::GetLegacyDataShape(outAccess->infoShape()); @@ -1157,6 +1588,25 @@ inline ErrorCode YUV_to_BGR(const TensorDataStridedCuda &inData, const TensorDat return ErrorCode::INVALID_DATA_SHAPE; } + if (isPlanar) + { +#define CVCUDA_YUV2BGR_PLANAR_CASE(T) \ + return Launch_YUV_to_BGR_Planar(inData, outData, inputShape, bidx, outputShape.C, stream) + switch (inDataType) + { + case kCV_8U: + CVCUDA_YUV2BGR_PLANAR_CASE(uchar); + case kCV_16U: + CVCUDA_YUV2BGR_PLANAR_CASE(ushort); + case kCV_32F: + CVCUDA_YUV2BGR_PLANAR_CASE(float); + default: + LOG_ERROR("Unsupported DataType " << inDataType); + return ErrorCode::INVALID_DATA_TYPE; + } +#undef CVCUDA_YUV2BGR_PLANAR_CASE + } + #define CVCUDA_YUV2BGR_CASE(T3) return Launch_YUV_to_BGR(inData, outData, inputShape, bidx, stream) switch (inDataType) { @@ -1203,6 +1653,43 @@ inline ErrorCode Launch_BGR_to_HSV(const TensorDataStridedCuda &inData, const Te return ErrorCode::SUCCESS; } +template +inline ErrorCode Launch_BGR_to_HSV_Planar(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, + cuda_op::DataShape shape, int bidx, bool isFullRange, bool strides_64b, + int srcChannels, cudaStream_t stream) +{ + using Policy = CvtKernelPolicy<32, 4, 4>; + + if (shape.N > 65535) + { + LOG_ERROR("Planar CvtColor requires numImages <= 65535 (CUDA grid-z limit)"); + return ErrorCode::INVALID_DATA_SHAPE; + } + + dim3 blockSize(Policy::BlockWidth, Policy::BlockHeight); + dim3 gridSize(divUp(shape.W, Policy::TileWidth), divUp(shape.H, Policy::TileHeight), shape.N); + + int2 dstSize{shape.W, shape.H}; + + if (strides_64b) + { + auto srcWrap = cuda::CreateTensorWrapNCHW(inData); + auto dstWrap = cuda::CreateTensorWrapNCHW(outData); + bgr_to_hsv_nchw + <<>>(srcWrap, dstWrap, dstSize, bidx, isFullRange, srcChannels); + } + else + { + auto srcWrap = cuda::CreateTensorWrapNCHW(inData); + auto dstWrap = cuda::CreateTensorWrapNCHW(outData); + bgr_to_hsv_nchw + <<>>(srcWrap, dstWrap, dstSize, bidx, isFullRange, srcChannels); + } + checkKernelErrors(); + + return ErrorCode::SUCCESS; +} + inline ErrorCode BGR_to_HSV(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, NVCVColorConversionCode code, cudaStream_t stream) { @@ -1218,6 +1705,8 @@ inline ErrorCode BGR_to_HSV(const TensorDataStridedCuda &inData, const TensorDat auto outAccess = TensorDataAccessStridedImagePlanar::Create(outData); NVCV_ASSERT(outAccess); + const bool isPlanar = IsPlanar(helpers::GetLegacyDataFormat(inData.layout())); + cuda_op::DataType outDataType = helpers::GetLegacyDataType(outData.dtype()); cuda_op::DataShape outputShape = helpers::GetLegacyDataShape(outAccess->infoShape()); @@ -1244,6 +1733,24 @@ inline ErrorCode BGR_to_HSV(const TensorDataStridedCuda &inData, const TensorDat #define CVCUDA_BGR2HSV_CASE(T3) \ return Launch_BGR_to_HSV(inData, outData, inputShape, bidx, isFullRange, strides_64b, stream) + if (isPlanar) + { +#define CVCUDA_BGR2HSV_PLANAR_CASE(T) \ + return Launch_BGR_to_HSV_Planar(inData, outData, inputShape, bidx, isFullRange, strides_64b, inputShape.C, \ + stream) + switch (inDataType) + { + case kCV_8U: + CVCUDA_BGR2HSV_PLANAR_CASE(uchar); + case kCV_32F: + CVCUDA_BGR2HSV_PLANAR_CASE(float); + default: + LOG_ERROR("Unsupported DataType " << inDataType); + return ErrorCode::INVALID_DATA_TYPE; + } +#undef CVCUDA_BGR2HSV_PLANAR_CASE + } + switch (inDataType) { case kCV_8U: @@ -1287,6 +1794,43 @@ inline ErrorCode Launch_HSV_to_BGR(const TensorDataStridedCuda &inData, const Te return ErrorCode::SUCCESS; } +template +inline ErrorCode Launch_HSV_to_BGR_Planar(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, + cuda_op::DataShape shape, int bidx, bool isFullRange, bool strides_64b, + int dstChannels, cudaStream_t stream) +{ + using Policy = CvtKernelPolicy<32, 4, 4>; + + if (shape.N > 65535) + { + LOG_ERROR("Planar CvtColor requires numImages <= 65535 (CUDA grid-z limit)"); + return ErrorCode::INVALID_DATA_SHAPE; + } + + dim3 blockSize(Policy::BlockWidth, Policy::BlockHeight); + dim3 gridSize(divUp(shape.W, Policy::TileWidth), divUp(shape.H, Policy::TileHeight), shape.N); + + int2 dstSize{shape.W, shape.H}; + + if (strides_64b) + { + auto srcWrap = cuda::CreateTensorWrapNCHW(inData); + auto dstWrap = cuda::CreateTensorWrapNCHW(outData); + hsv_to_bgr_nchw + <<>>(srcWrap, dstWrap, dstSize, bidx, isFullRange, dstChannels); + } + else + { + auto srcWrap = cuda::CreateTensorWrapNCHW(inData); + auto dstWrap = cuda::CreateTensorWrapNCHW(outData); + hsv_to_bgr_nchw + <<>>(srcWrap, dstWrap, dstSize, bidx, isFullRange, dstChannels); + } + checkKernelErrors(); + + return ErrorCode::SUCCESS; +} + inline ErrorCode HSV_to_BGR(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, NVCVColorConversionCode code, cudaStream_t stream) { @@ -1302,6 +1846,8 @@ inline ErrorCode HSV_to_BGR(const TensorDataStridedCuda &inData, const TensorDat auto outAccess = TensorDataAccessStridedImagePlanar::Create(outData); NVCV_ASSERT(outAccess); + const bool isPlanar = IsPlanar(helpers::GetLegacyDataFormat(inData.layout())); + cuda_op::DataType outDataType = helpers::GetLegacyDataType(outData.dtype()); cuda_op::DataShape outputShape = helpers::GetLegacyDataShape(outAccess->infoShape()); @@ -1337,6 +1883,23 @@ inline ErrorCode HSV_to_BGR(const TensorDataStridedCuda &inData, const TensorDat else \ return Launch_HSV_to_BGR(inData, outData, inputShape, bidx, isFullRange, strides_64b, stream) + if (isPlanar) + { +#define CVCUDA_HSV2BGR_PLANAR_CASE(T) \ + return Launch_HSV_to_BGR_Planar(inData, outData, inputShape, bidx, isFullRange, strides_64b, dcn, stream) + switch (inDataType) + { + case kCV_8U: + CVCUDA_HSV2BGR_PLANAR_CASE(uchar); + case kCV_32F: + CVCUDA_HSV2BGR_PLANAR_CASE(float); + default: + LOG_ERROR("Unsupported DataType " << inDataType); + return ErrorCode::INVALID_DATA_TYPE; + } +#undef CVCUDA_HSV2BGR_PLANAR_CASE + } + switch (inDataType) { case kCV_8U: @@ -1389,6 +1952,12 @@ inline ErrorCode Launch_YUV420xp_to_BGR(const TensorDataStridedCuda &inData, con inline ErrorCode YUV420xp_to_BGR(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, NVCVColorConversionCode code, cudaStream_t stream) { + if (IsPlanar(helpers::GetLegacyDataFormat(inData.layout()))) + { + LOG_ERROR("Planar CvtColor does not support subsampled YUV420 conversion codes"); + return ErrorCode::INVALID_DATA_FORMAT; + } + int bidx = (code == NVCV_COLOR_YUV2BGR_NV12 || code == NVCV_COLOR_YUV2BGRA_NV12 || code == NVCV_COLOR_YUV2BGR_NV21 || code == NVCV_COLOR_YUV2BGRA_NV21 || code == NVCV_COLOR_YUV2BGR_YV12 || code == NVCV_COLOR_YUV2BGRA_YV12 @@ -1553,6 +2122,12 @@ inline ErrorCode Launch_BGR_to_YUV420xp(const TensorDataStridedCuda &inData, con inline ErrorCode BGR_to_YUV420xp(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, NVCVColorConversionCode code, cudaStream_t stream) { + if (IsPlanar(helpers::GetLegacyDataFormat(inData.layout()))) + { + LOG_ERROR("Planar CvtColor does not support subsampled YUV420 conversion codes"); + return ErrorCode::INVALID_DATA_FORMAT; + } + int bidx = (code == NVCV_COLOR_BGR2YUV_NV12 || code == NVCV_COLOR_BGRA2YUV_NV12 || code == NVCV_COLOR_BGR2YUV_NV21 || code == NVCV_COLOR_BGRA2YUV_NV21 || code == NVCV_COLOR_BGR2YUV_YV12 || code == NVCV_COLOR_BGRA2YUV_YV12 @@ -1663,6 +2238,12 @@ inline ErrorCode BGR_to_YUV420xp(const TensorDataStridedCuda &inData, const Tens inline ErrorCode YUV422_to_BGR(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, NVCVColorConversionCode code, cudaStream_t stream) { + if (IsPlanar(helpers::GetLegacyDataFormat(inData.layout()))) + { + LOG_ERROR("Planar CvtColor does not support packed YUV422 conversion codes"); + return ErrorCode::INVALID_DATA_FORMAT; + } + int bidx = (code == NVCV_COLOR_YUV2BGR_YUY2 || code == NVCV_COLOR_YUV2BGRA_YUY2 || code == NVCV_COLOR_YUV2BGR_YVYU || code == NVCV_COLOR_YUV2BGRA_YVYU || code == NVCV_COLOR_YUV2BGR_UYVY || code == NVCV_COLOR_YUV2BGRA_UYVY) @@ -1781,9 +2362,10 @@ ErrorCode CvtColor::infer(const TensorDataStridedCuda &inData, const TensorDataS DataFormat format = input_format; - if (!(format == kNHWC || format == kHWC)) + if (!(format == kNHWC || format == kHWC || format == kNCHW || format == kCHW)) { - LOG_ERROR("Invalid input DataFormat " << format << ", the valid DataFormats are: \"NHWC\", \"HWC\""); + LOG_ERROR("Invalid input DataFormat " << format + << ", the valid DataFormats are: \"NHWC\", \"HWC\", \"NCHW\", \"CHW\""); return ErrorCode::INVALID_DATA_FORMAT; } @@ -1996,6 +2578,12 @@ ErrorCode CvtColor::infer(const TensorDataStridedCuda &inData, const TensorDataS 0, // CV_COLORCVT_MAX = 148 }; + if (code < 0 || static_cast(code) >= sizeof(funcs) / sizeof(funcs[0])) + { + LOG_ERROR("Invalid convert color code: " << code); + return ErrorCode::INVALID_PARAMETER; + } + func_t func = funcs[code]; if (func == 0) diff --git a/src/cvcuda/priv/legacy/cvt_color_var_shape.cu b/src/cvcuda/priv/legacy/cvt_color_var_shape.cu index 26a8c6688..6c327af9b 100644 --- a/src/cvcuda/priv/legacy/cvt_color_var_shape.cu +++ b/src/cvcuda/priv/legacy/cvt_color_var_shape.cu @@ -79,6 +79,11 @@ static constexpr int ITUR_BT_601_CBV = -74448; namespace nvcv::legacy::cuda_op { +static bool IsPlanar(DataFormat format) +{ + return format == kNCHW || format == kCHW; +} + inline __device__ bool checkShapeFromYUV420(int rows, int cols, NVCVColorConversionCode code) { int valid_row = 1, valid_col = 1; @@ -133,6 +138,29 @@ inline __device__ bool checkShapeFromYUV420(int rows, int cols, NVCVColorConvers return true; } +template +__device__ __forceinline__ void load_bgra_chw(SrcWrapper src, T &B, T &G, T &R, T &A, int batch_idx, int x, int y, + int bidx, int srcChannels) +{ + B = *src.ptr(batch_idx, bidx, y, x); + G = *src.ptr(batch_idx, 1, y, x); + R = *src.ptr(batch_idx, bidx ^ 2, y, x); + A = srcChannels == 4 ? *src.ptr(batch_idx, 3, y, x) : cuda::TypeTraits::max; +} + +template +__device__ __forceinline__ void store_bgra_chw(DstWrapper dst, T B, T G, T R, T A, int batch_idx, int x, int y, + int bidx, int dstChannels) +{ + *dst.ptr(batch_idx, bidx, y, x) = B; + *dst.ptr(batch_idx, 1, y, x) = G; + *dst.ptr(batch_idx, bidx ^ 2, y, x) = R; + if (dstChannels == 4) + { + *dst.ptr(batch_idx, 3, y, x) = A; + } +} + template __global__ void rgb_to_bgr_nhwc(cuda::ImageBatchVarShapeWrapNHWC src, cuda::ImageBatchVarShapeWrapNHWC dst, int bidx) @@ -158,6 +186,21 @@ __global__ void rgb_to_bgr_nhwc(cuda::ImageBatchVarShapeWrapNHWC src, cuda::I } } +template +__global__ void rgb_to_bgr_chw(cuda::ImageBatchVarShapeWrap src, cuda::ImageBatchVarShapeWrap dst, int bidx, + int srcChannels, int dstChannels) +{ + int dst_x = blockIdx.x * blockDim.x + threadIdx.x; + int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + if (dst_x >= dst.width(batch_idx, 0) || dst_y >= dst.height(batch_idx, 0)) + return; + + T b, g, r, a; + load_bgra_chw(src, b, g, r, a, batch_idx, dst_x, dst_y, bidx, srcChannels); + store_bgra_chw(dst, b, g, r, a, batch_idx, dst_x, dst_y, 0, dstChannels); +} + template __global__ void gray_to_bgr_nhwc(cuda::ImageBatchVarShapeWrapNHWC src, cuda::ImageBatchVarShapeWrapNHWC dst) { @@ -179,21 +222,69 @@ __global__ void gray_to_bgr_nhwc(cuda::ImageBatchVarShapeWrapNHWC src, cuda:: } template +__global__ void gray_to_bgr_chw(cuda::ImageBatchVarShapeWrap src, cuda::ImageBatchVarShapeWrap dst, + int dstChannels) +{ + int dst_x = blockIdx.x * blockDim.x + threadIdx.x; + int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + if (dst_x >= dst.width(batch_idx, 0) || dst_y >= dst.height(batch_idx, 0)) + return; + + T g = *src.ptr(batch_idx, 0, dst_y, dst_x); + + *dst.ptr(batch_idx, 0, dst_y, dst_x) = g; + *dst.ptr(batch_idx, 1, dst_y, dst_x) = g; + *dst.ptr(batch_idx, 2, dst_y, dst_x) = g; + if (dstChannels == 4) + { + *dst.ptr(batch_idx, 3, dst_y, dst_x) = g; + } +} + +template __global__ void bgr_to_gray_char_nhwc(cuda::ImageBatchVarShapeWrapNHWC src, cuda::ImageBatchVarShapeWrapNHWC dst, int bidx) +{ + int dst_x0 = (blockIdx.x * blockDim.x + threadIdx.x) * NIX; + int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + const int dst_width = dst.width(batch_idx); + if (dst_x0 >= dst_width || dst_y >= dst.height(batch_idx)) + return; + +#pragma unroll + for (int i = 0; i < NIX; ++i) + { + int dst_x = dst_x0 + i; + if (dst_x >= dst_width) + break; + + int b = *src.ptr(batch_idx, dst_y, dst_x, bidx); + int g = *src.ptr(batch_idx, dst_y, dst_x, 1); + int r = *src.ptr(batch_idx, dst_y, dst_x, bidx ^ 2); + + T gray = (T)CV_DESCALE(b * BY15 + g * GY15 + r * RY15, gray_shift); + *dst.ptr(batch_idx, dst_y, dst_x, 0) = gray; + } +} + +template +__global__ void bgr_to_gray_char_chw(cuda::ImageBatchVarShapeWrap src, cuda::ImageBatchVarShapeWrap dst, int bidx, + int srcChannels) { int dst_x = blockIdx.x * blockDim.x + threadIdx.x; int dst_y = blockIdx.y * blockDim.y + threadIdx.y; const int batch_idx = get_batch_idx(); - if (dst_x >= dst.width(batch_idx) || dst_y >= dst.height(batch_idx)) + if (dst_x >= dst.width(batch_idx, 0) || dst_y >= dst.height(batch_idx, 0)) return; - int b = *src.ptr(batch_idx, dst_y, dst_x, bidx); - int g = *src.ptr(batch_idx, dst_y, dst_x, 1); - int r = *src.ptr(batch_idx, dst_y, dst_x, bidx ^ 2); + T unusedA; + T b, g, r; + load_bgra_chw(src, b, g, r, unusedA, batch_idx, dst_x, dst_y, bidx, srcChannels); T gray = (T)CV_DESCALE(b * BY15 + g * GY15 + r * RY15, gray_shift); - *dst.ptr(batch_idx, dst_y, dst_x, 0) = gray; + *dst.ptr(batch_idx, 0, dst_y, dst_x) = gray; } template @@ -214,6 +305,24 @@ __global__ void bgr_to_gray_float_nhwc(cuda::ImageBatchVarShapeWrapNHWC src, *dst.ptr(batch_idx, dst_y, dst_x, 0) = gray; } +template +__global__ void bgr_to_gray_float_chw(cuda::ImageBatchVarShapeWrap src, cuda::ImageBatchVarShapeWrap dst, + int bidx, int srcChannels) +{ + int dst_x = blockIdx.x * blockDim.x + threadIdx.x; + int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + if (dst_x >= dst.width(batch_idx, 0) || dst_y >= dst.height(batch_idx, 0)) + return; + + T unusedA; + T b, g, r; + load_bgra_chw(src, b, g, r, unusedA, batch_idx, dst_x, dst_y, bidx, srcChannels); + + T gray = (T)(b * B2YF + g * G2YF + r * R2YF); + *dst.ptr(batch_idx, 0, dst_y, dst_x) = gray; +} + template __global__ void bgr_to_yuv_char_nhwc(cuda::ImageBatchVarShapeWrapNHWC src, cuda::ImageBatchVarShapeWrapNHWC dst, int bidx) @@ -239,6 +348,31 @@ __global__ void bgr_to_yuv_char_nhwc(cuda::ImageBatchVarShapeWrapNHWC src, cu *dst.ptr(batch_idx, dst_y, dst_x, 2) = cuda::SaturateCast(Cr); } +template +__global__ void bgr_to_yuv_char_chw(cuda::ImageBatchVarShapeWrap src, cuda::ImageBatchVarShapeWrap dst, int bidx, + int srcChannels) +{ + int dst_x = blockIdx.x * blockDim.x + threadIdx.x; + int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + if (dst_x >= dst.width(batch_idx, 0) || dst_y >= dst.height(batch_idx, 0)) + return; + + T unusedA; + T B, G, R; + load_bgra_chw(src, B, G, R, unusedA, batch_idx, dst_x, dst_y, bidx, srcChannels); + + int C0 = R2Y, C1 = G2Y, C2 = B2Y, C3 = R2VI, C4 = B2UI; + int delta = ((T)(cuda::TypeTraits::max / 2 + 1)) * (1 << yuv_shift); + int Y = CV_DESCALE(R * C0 + G * C1 + B * C2, yuv_shift); + int Cr = CV_DESCALE((R - Y) * C3 + delta, yuv_shift); + int Cb = CV_DESCALE((B - Y) * C4 + delta, yuv_shift); + + *dst.ptr(batch_idx, 0, dst_y, dst_x) = cuda::SaturateCast(Y); + *dst.ptr(batch_idx, 1, dst_y, dst_x) = cuda::SaturateCast(Cb); + *dst.ptr(batch_idx, 2, dst_y, dst_x) = cuda::SaturateCast(Cr); +} + template __global__ void bgr_to_yuv_float_nhwc(cuda::ImageBatchVarShapeWrapNHWC src, cuda::ImageBatchVarShapeWrapNHWC dst, int bidx) @@ -263,6 +397,31 @@ __global__ void bgr_to_yuv_float_nhwc(cuda::ImageBatchVarShapeWrapNHWC src, c *dst.ptr(batch_idx, dst_y, dst_x, 2) = Cr; } +template +__global__ void bgr_to_yuv_float_chw(cuda::ImageBatchVarShapeWrap src, cuda::ImageBatchVarShapeWrap dst, int bidx, + int srcChannels) +{ + int dst_x = blockIdx.x * blockDim.x + threadIdx.x; + int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + if (dst_x >= dst.width(batch_idx, 0) || dst_y >= dst.height(batch_idx, 0)) + return; + + T unusedA; + T B, G, R; + load_bgra_chw(src, B, G, R, unusedA, batch_idx, dst_x, dst_y, bidx, srcChannels); + + T C0 = R2YF, C1 = G2YF, C2 = B2YF, C3 = R2VF, C4 = B2UF; + T delta = 0.5f; + T Y = R * C0 + G * C1 + B * C2; + T Cr = (R - Y) * C3 + delta; + T Cb = (B - Y) * C4 + delta; + + *dst.ptr(batch_idx, 0, dst_y, dst_x) = Y; + *dst.ptr(batch_idx, 1, dst_y, dst_x) = Cb; + *dst.ptr(batch_idx, 2, dst_y, dst_x) = Cr; +} + template __global__ void yuv_to_bgr_char_nhwc(cuda::ImageBatchVarShapeWrapNHWC src, cuda::ImageBatchVarShapeWrapNHWC dst, int bidx) @@ -288,6 +447,30 @@ __global__ void yuv_to_bgr_char_nhwc(cuda::ImageBatchVarShapeWrapNHWC src, cu *dst.ptr(batch_idx, dst_y, dst_x, bidx ^ 2) = cuda::SaturateCast(r); } +template +__global__ void yuv_to_bgr_char_chw(cuda::ImageBatchVarShapeWrap src, cuda::ImageBatchVarShapeWrap dst, int bidx, + int dstChannels) +{ + int dst_x = blockIdx.x * blockDim.x + threadIdx.x; + int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + if (dst_x >= dst.width(batch_idx, 0) || dst_y >= dst.height(batch_idx, 0)) + return; + + T Y = *src.ptr(batch_idx, 0, dst_y, dst_x); + T Cb = *src.ptr(batch_idx, 1, dst_y, dst_x); + T Cr = *src.ptr(batch_idx, 2, dst_y, dst_x); + + int C0 = V2RI, C1 = V2GI, C2 = U2GI, C3 = U2BI; + int delta = ((T)(cuda::TypeTraits::max / 2 + 1)); + int b = Y + CV_DESCALE((Cb - delta) * C3, yuv_shift); + int g = Y + CV_DESCALE((Cb - delta) * C2 + (Cr - delta) * C1, yuv_shift); + int r = Y + CV_DESCALE((Cr - delta) * C0, yuv_shift); + + store_bgra_chw(dst, cuda::SaturateCast(b), cuda::SaturateCast(g), cuda::SaturateCast(r), + cuda::TypeTraits::max, batch_idx, dst_x, dst_y, bidx, dstChannels); +} + template __global__ void yuv_to_bgr_float_nhwc(cuda::ImageBatchVarShapeWrapNHWC src, cuda::ImageBatchVarShapeWrapNHWC dst, int bidx) @@ -313,6 +496,29 @@ __global__ void yuv_to_bgr_float_nhwc(cuda::ImageBatchVarShapeWrapNHWC src, c *dst.ptr(batch_idx, dst_y, dst_x, bidx ^ 2) = r; } +template +__global__ void yuv_to_bgr_float_chw(cuda::ImageBatchVarShapeWrap src, cuda::ImageBatchVarShapeWrap dst, int bidx, + int dstChannels) +{ + int dst_x = blockIdx.x * blockDim.x + threadIdx.x; + int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + if (dst_x >= dst.width(batch_idx, 0) || dst_y >= dst.height(batch_idx, 0)) + return; + + T Y = *src.ptr(batch_idx, 0, dst_y, dst_x); + T Cb = *src.ptr(batch_idx, 1, dst_y, dst_x); + T Cr = *src.ptr(batch_idx, 2, dst_y, dst_x); + + T C0 = V2RF, C1 = V2GF, C2 = U2GF, C3 = U2BF; + T delta = 0.5f; + T b = Y + (Cb - delta) * C3; + T g = Y + (Cb - delta) * C2 + (Cr - delta) * C1; + T r = Y + (Cr - delta) * C0; + + store_bgra_chw(dst, b, g, r, cuda::TypeTraits::max, batch_idx, dst_x, dst_y, bidx, dstChannels); +} + template __global__ void bgr_to_hsv_char_nhwc(cuda::ImageBatchVarShapeWrapNHWC src, cuda::ImageBatchVarShapeWrapNHWC dst, int bidx, bool isFullRange) @@ -354,6 +560,51 @@ __global__ void bgr_to_hsv_char_nhwc(cuda::ImageBatchVarShapeWrapNHWC src, cu *dst.ptr(batch_idx, dst_y, dst_x, 2) = (uint8_t)v; } +template +__global__ void bgr_to_hsv_char_chw(cuda::ImageBatchVarShapeWrap src, cuda::ImageBatchVarShapeWrap dst, int bidx, + bool isFullRange, int srcChannels) +{ + int dst_x = blockIdx.x * blockDim.x + threadIdx.x; + int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + if (dst_x >= dst.width(batch_idx, 0) || dst_y >= dst.height(batch_idx, 0)) + return; + + T unusedA; + T b8, g8, r8; + load_bgra_chw(src, b8, g8, r8, unusedA, batch_idx, dst_x, dst_y, bidx, srcChannels); + + int b = b8; + int g = g8; + int r = r8; + int hrange = isFullRange ? 256 : 180; + int hr = hrange; + const int hsv_shift = 12; + int h, s, v = b; + int vmin = b; + int vr, vg; + + v = cuda::max(v, g); + v = cuda::max(v, r); + vmin = min(vmin, g); + vmin = min(vmin, r); + + uint8_t diff = cuda::SaturateCast(v - vmin); + vr = v == r ? -1 : 0; + vg = v == g ? -1 : 0; + + int hdiv_table = diff == 0 ? 0 : cuda::SaturateCast((hrange << hsv_shift) / (6. * diff)); + int sdiv_table = v == 0 ? 0 : cuda::SaturateCast((255 << hsv_shift) / (1. * v)); + s = (diff * sdiv_table + (1 << (hsv_shift - 1))) >> hsv_shift; + h = (vr & (g - b)) + (~vr & ((vg & (b - r + 2 * diff)) + ((~vg) & (r - g + 4 * diff)))); + h = (h * hdiv_table + (1 << (hsv_shift - 1))) >> hsv_shift; + h += h < 0 ? hr : 0; + + *dst.ptr(batch_idx, 0, dst_y, dst_x) = cuda::SaturateCast(h); + *dst.ptr(batch_idx, 1, dst_y, dst_x) = (uint8_t)s; + *dst.ptr(batch_idx, 2, dst_y, dst_x) = (uint8_t)v; +} + template __global__ void bgr_to_hsv_float_nhwc(cuda::ImageBatchVarShapeWrapNHWC src, cuda::ImageBatchVarShapeWrapNHWC dst, int bidx) @@ -401,65 +652,163 @@ __global__ void bgr_to_hsv_float_nhwc(cuda::ImageBatchVarShapeWrapNHWC src, c *dst.ptr(batch_idx, dst_y, dst_x, 2) = v; } +template +__global__ void bgr_to_hsv_float_chw(cuda::ImageBatchVarShapeWrap src, cuda::ImageBatchVarShapeWrap dst, int bidx, + int srcChannels) +{ + int dst_x = blockIdx.x * blockDim.x + threadIdx.x; + int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + if (dst_x >= dst.width(batch_idx, 0) || dst_y >= dst.height(batch_idx, 0)) + return; + + T unusedA; + T b, g, r; + load_bgra_chw(src, b, g, r, unusedA, batch_idx, dst_x, dst_y, bidx, srcChannels); + float h, s, v; + float hrange = 360.0; + float hscale = hrange * (1.f / 360.f); + + float vmin, diff; + + v = vmin = r; + if (v < g) + v = g; + if (v < b) + v = b; + if (vmin > g) + vmin = g; + if (vmin > b) + vmin = b; + + diff = v - vmin; + s = diff / (float)(fabs(v) + FLT_EPSILON); + diff = (float)(60. / (diff + FLT_EPSILON)); + if (v == r) + h = (g - b) * diff; + else if (v == g) + h = (b - r) * diff + 120.f; + else + h = (r - g) * diff + 240.f; + + if (h < 0) + h += 360.f; + + *dst.ptr(batch_idx, 0, dst_y, dst_x) = h * hscale; + *dst.ptr(batch_idx, 1, dst_y, dst_x) = s; + *dst.ptr(batch_idx, 2, dst_y, dst_x) = v; +} + inline __device__ void HSV2RGB_native_var_shape(float h, float s, float v, float &b, float &g, float &r) { if (s == 0) b = g = r = v; else { - static const int sector_data[][3] = { - {1, 3, 0}, - {1, 0, 2}, - {3, 0, 1}, - {0, 2, 1}, - {0, 1, 3}, - {2, 1, 0} - }; - h += 6 * (h < 0); // Add 6 if h < 0. int idx = static_cast(h); // Sector index. h -= idx; // Fractional part of h. idx %= 6; // Make sure index is in valid range. - // clang-format off - const float tab[4] {v, - v * (1 - s), - v * (1 - s * h), - v * (1 - s * (1 - h))}; - // clang-format on - - b = tab[sector_data[idx][0]]; - g = tab[sector_data[idx][1]]; - r = tab[sector_data[idx][2]]; + const float p = v * (1 - s); + const float q = v * (1 - s * h); + const float t = v * (1 - s * (1 - h)); + switch (idx) + { + case 0: + b = p; + g = t; + r = v; + break; + case 1: + b = p; + g = v; + r = q; + break; + case 2: + b = t; + g = v; + r = p; + break; + case 3: + b = v; + g = q; + r = p; + break; + case 4: + b = v; + g = p; + r = t; + break; + default: + b = q; + g = p; + r = v; + break; + } } } -template +template __global__ void hsv_to_bgr_char_nhwc(cuda::ImageBatchVarShapeWrapNHWC src, cuda::ImageBatchVarShapeWrapNHWC dst, int bidx, bool isFullRange) +{ + int dst_x0 = (blockIdx.x * blockDim.x + threadIdx.x) * NIX; + int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + const int dst_width = dst.width(batch_idx); + if (dst_x0 >= dst_width || dst_y >= dst.height(batch_idx)) + return; + + const float scaleH = 6.f / (isFullRange ? 256 : 180); + constexpr float scaleSV = 1.0f / 255.0f; + constexpr T alpha = cuda::TypeTraits::max; + +#pragma unroll + for (int i = 0; i < NIX; ++i) + { + int dst_x = dst_x0 + i; + if (dst_x >= dst_width) + break; + + float h = *src.ptr(batch_idx, dst_y, dst_x, 0) * scaleH; + float s = *src.ptr(batch_idx, dst_y, dst_x, 1) * scaleSV; + float v = *src.ptr(batch_idx, dst_y, dst_x, 2) * scaleSV; + + float b, g, r; + HSV2RGB_native_var_shape(h, s, v, b, g, r); + + *dst.ptr(batch_idx, dst_y, dst_x, bidx) = cuda::SaturateCast(b * 255.0f); + *dst.ptr(batch_idx, dst_y, dst_x, 1) = cuda::SaturateCast(g * 255.0f); + *dst.ptr(batch_idx, dst_y, dst_x, bidx ^ 2) = cuda::SaturateCast(r * 255.0f); + if (dst.numChannels() == 4) + *dst.ptr(batch_idx, dst_y, dst_x, 3) = alpha; + } +} + +template +__global__ void hsv_to_bgr_char_chw(cuda::ImageBatchVarShapeWrap src, cuda::ImageBatchVarShapeWrap dst, int bidx, + bool isFullRange, int dstChannels) { int dst_x = blockIdx.x * blockDim.x + threadIdx.x; int dst_y = blockIdx.y * blockDim.y + threadIdx.y; const int batch_idx = get_batch_idx(); - if (dst_x >= dst.width(batch_idx) || dst_y >= dst.height(batch_idx)) + if (dst_x >= dst.width(batch_idx, 0) || dst_y >= dst.height(batch_idx, 0)) return; const float scaleH = 6.f / (isFullRange ? 256 : 180); constexpr float scaleSV = 1.0f / 255.0f; constexpr T alpha = cuda::TypeTraits::max; - float h = *src.ptr(batch_idx, dst_y, dst_x, 0) * scaleH; - float s = *src.ptr(batch_idx, dst_y, dst_x, 1) * scaleSV; - float v = *src.ptr(batch_idx, dst_y, dst_x, 2) * scaleSV; + float h = *src.ptr(batch_idx, 0, dst_y, dst_x) * scaleH; + float s = *src.ptr(batch_idx, 1, dst_y, dst_x) * scaleSV; + float v = *src.ptr(batch_idx, 2, dst_y, dst_x) * scaleSV; float b, g, r; HSV2RGB_native_var_shape(h, s, v, b, g, r); - *dst.ptr(batch_idx, dst_y, dst_x, bidx) = cuda::SaturateCast(b * 255.0f); - *dst.ptr(batch_idx, dst_y, dst_x, 1) = cuda::SaturateCast(g * 255.0f); - *dst.ptr(batch_idx, dst_y, dst_x, bidx ^ 2) = cuda::SaturateCast(r * 255.0f); - if (dst.numChannels() == 4) - *dst.ptr(batch_idx, dst_y, dst_x, 3) = alpha; + store_bgra_chw(dst, cuda::SaturateCast(b * 255.0f), cuda::SaturateCast(g * 255.0f), + cuda::SaturateCast(r * 255.0f), alpha, batch_idx, dst_x, dst_y, bidx, dstChannels); } template @@ -489,6 +838,30 @@ __global__ void hsv_to_bgr_float_nhwc(cuda::ImageBatchVarShapeWrapNHWC src, c *dst.ptr(batch_idx, dst_y, dst_x, 3) = alpha; } +template +__global__ void hsv_to_bgr_float_chw(cuda::ImageBatchVarShapeWrap src, cuda::ImageBatchVarShapeWrap dst, int bidx, + int dstChannels) +{ + int dst_x = blockIdx.x * blockDim.x + threadIdx.x; + int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + if (dst_x >= dst.width(batch_idx, 0) || dst_y >= dst.height(batch_idx, 0)) + return; + + constexpr float scaleH = 6.0f / 360.0f; + constexpr float alpha = 1.0f; + + float h = *src.ptr(batch_idx, 0, dst_y, dst_x) * scaleH; + float s = *src.ptr(batch_idx, 1, dst_y, dst_x); + float v = *src.ptr(batch_idx, 2, dst_y, dst_x); + + float b, g, r; + HSV2RGB_native_var_shape(h, s, v, b, g, r); + + store_bgra_chw(dst, static_cast(b), static_cast(g), static_cast(r), static_cast(alpha), batch_idx, + dst_x, dst_y, bidx, dstChannels); +} + __device__ __forceinline__ void yuv42xxp_to_bgr_kernel(const int &Y, const int &U, const int &V, uchar &r, uchar &g, uchar &b) { @@ -809,6 +1182,7 @@ inline ErrorCode BGR_to_RGB(const ImageBatchVarShapeDataStridedCuda &inData, int channels = inData.uniqueFormat().numChannels(); DataType data_type = helpers::GetLegacyDataType(inData.uniqueFormat()); DataType out_data_type = helpers::GetLegacyDataType(outData.uniqueFormat()); + bool isPlanar = IsPlanar(helpers::GetLegacyDataFormat(inData)); if (channels != sch) { @@ -839,56 +1213,66 @@ inline ErrorCode BGR_to_RGB(const ImageBatchVarShapeDataStridedCuda &inData, int max_width = inData.maxSize().w; int max_height = inData.maxSize().h; int batch_size = inData.numImages(); + if (isPlanar && batch_size > 65535) + { + LOG_ERROR("Planar CvtColor requires numImages <= 65535 (CUDA grid-z limit)"); + return ErrorCode::INVALID_DATA_SHAPE; + } dim3 blockSize(BLOCK, BLOCK / 4, 1); dim3 gridSize(divUp(max_width, blockSize.x), divUp(max_height, blockSize.y), batch_size); +#define CVCUDA_RUN_BGR2RGB(T) \ + do \ + { \ + if (isPlanar) \ + { \ + cuda::ImageBatchVarShapeWrap src_ptr(inData); \ + cuda::ImageBatchVarShapeWrap dst_ptr(outData); \ + rgb_to_bgr_chw<<>>(src_ptr, dst_ptr, bidx, sch, dch); \ + } \ + else \ + { \ + cuda::ImageBatchVarShapeWrapNHWC src_ptr(inData, sch); \ + cuda::ImageBatchVarShapeWrapNHWC dst_ptr(outData, dch); \ + rgb_to_bgr_nhwc<<>>(src_ptr, dst_ptr, bidx); \ + } \ + checkKernelErrors(); \ + } \ + while (0) + switch (data_type) { case kCV_8U: case kCV_8S: { - cuda::ImageBatchVarShapeWrapNHWC src_ptr(inData, sch); - cuda::ImageBatchVarShapeWrapNHWC dst_ptr(outData, dch); - rgb_to_bgr_nhwc<<>>(src_ptr, dst_ptr, bidx); - checkKernelErrors(); + CVCUDA_RUN_BGR2RGB(unsigned char); } break; case kCV_16F: // Not properly handled when adding alpha to the destination. case kCV_16U: case kCV_16S: { - cuda::ImageBatchVarShapeWrapNHWC src_ptr(inData, sch); - cuda::ImageBatchVarShapeWrapNHWC dst_ptr(outData, dch); - rgb_to_bgr_nhwc<<>>(src_ptr, dst_ptr, bidx); - checkKernelErrors(); + CVCUDA_RUN_BGR2RGB(uint16_t); } break; case kCV_32S: { - cuda::ImageBatchVarShapeWrapNHWC src_ptr(inData, sch); - cuda::ImageBatchVarShapeWrapNHWC dst_ptr(outData, dch); - rgb_to_bgr_nhwc<<>>(src_ptr, dst_ptr, bidx); - checkKernelErrors(); + CVCUDA_RUN_BGR2RGB(int32_t); } break; case kCV_32F: { - cuda::ImageBatchVarShapeWrapNHWC src_ptr(inData, sch); - cuda::ImageBatchVarShapeWrapNHWC dst_ptr(outData, dch); - rgb_to_bgr_nhwc<<>>(src_ptr, dst_ptr, bidx); - checkKernelErrors(); + CVCUDA_RUN_BGR2RGB(float); } break; case kCV_64F: { - cuda::ImageBatchVarShapeWrapNHWC src_ptr(inData, sch); - cuda::ImageBatchVarShapeWrapNHWC dst_ptr(outData, dch); - rgb_to_bgr_nhwc<<>>(src_ptr, dst_ptr, bidx); - checkKernelErrors(); + CVCUDA_RUN_BGR2RGB(double); } break; } +#undef CVCUDA_RUN_BGR2RGB return ErrorCode::SUCCESS; } @@ -901,6 +1285,7 @@ inline ErrorCode GRAY_to_BGR(const ImageBatchVarShapeDataStridedCuda &inData, int channels = inData.uniqueFormat().numChannels(); DataType data_type = helpers::GetLegacyDataType(inData.uniqueFormat()); DataType out_data_type = helpers::GetLegacyDataType(outData.uniqueFormat()); + bool isPlanar = IsPlanar(helpers::GetLegacyDataFormat(inData)); if (channels != 1) { @@ -931,56 +1316,66 @@ inline ErrorCode GRAY_to_BGR(const ImageBatchVarShapeDataStridedCuda &inData, int max_width = inData.maxSize().w; int max_height = inData.maxSize().h; int batch_size = inData.numImages(); + if (isPlanar && batch_size > 65535) + { + LOG_ERROR("Planar CvtColor requires numImages <= 65535 (CUDA grid-z limit)"); + return ErrorCode::INVALID_DATA_SHAPE; + } dim3 blockSize(BLOCK, BLOCK / 4, 1); dim3 gridSize(divUp(max_width, blockSize.x), divUp(max_height, blockSize.y), batch_size); +#define CVCUDA_RUN_GRAY2BGR(T) \ + do \ + { \ + if (isPlanar) \ + { \ + cuda::ImageBatchVarShapeWrap src_ptr(inData); \ + cuda::ImageBatchVarShapeWrap dst_ptr(outData); \ + gray_to_bgr_chw<<>>(src_ptr, dst_ptr, dch); \ + } \ + else \ + { \ + cuda::ImageBatchVarShapeWrapNHWC src_ptr(inData, channels); \ + cuda::ImageBatchVarShapeWrapNHWC dst_ptr(outData, dch); \ + gray_to_bgr_nhwc<<>>(src_ptr, dst_ptr); \ + } \ + checkKernelErrors(); \ + } \ + while (0) + switch (data_type) { case kCV_8U: case kCV_8S: { - cuda::ImageBatchVarShapeWrapNHWC src_ptr(inData, channels); - cuda::ImageBatchVarShapeWrapNHWC dst_ptr(outData, dch); - gray_to_bgr_nhwc<<>>(src_ptr, dst_ptr); - checkKernelErrors(); + CVCUDA_RUN_GRAY2BGR(unsigned char); } break; case kCV_16F: // Not properly handled when adding alpha to the destination. case kCV_16U: case kCV_16S: { - cuda::ImageBatchVarShapeWrapNHWC src_ptr(inData, channels); - cuda::ImageBatchVarShapeWrapNHWC dst_ptr(outData, dch); - gray_to_bgr_nhwc<<>>(src_ptr, dst_ptr); - checkKernelErrors(); + CVCUDA_RUN_GRAY2BGR(uint16_t); } break; case kCV_32S: { - cuda::ImageBatchVarShapeWrapNHWC src_ptr(inData, channels); - cuda::ImageBatchVarShapeWrapNHWC dst_ptr(outData, dch); - gray_to_bgr_nhwc<<>>(src_ptr, dst_ptr); - checkKernelErrors(); + CVCUDA_RUN_GRAY2BGR(int32_t); } break; case kCV_32F: { - cuda::ImageBatchVarShapeWrapNHWC src_ptr(inData, channels); - cuda::ImageBatchVarShapeWrapNHWC dst_ptr(outData, dch); - gray_to_bgr_nhwc<<>>(src_ptr, dst_ptr); - checkKernelErrors(); + CVCUDA_RUN_GRAY2BGR(float); } break; case kCV_64F: { - cuda::ImageBatchVarShapeWrapNHWC src_ptr(inData, channels); - cuda::ImageBatchVarShapeWrapNHWC dst_ptr(outData, dch); - gray_to_bgr_nhwc<<>>(src_ptr, dst_ptr); - checkKernelErrors(); + CVCUDA_RUN_GRAY2BGR(double); } break; } +#undef CVCUDA_RUN_GRAY2BGR return ErrorCode::SUCCESS; } @@ -994,6 +1389,7 @@ inline ErrorCode BGR_to_GRAY(const ImageBatchVarShapeDataStridedCuda &inData, int channels = inData.uniqueFormat().numChannels(); DataType data_type = helpers::GetLegacyDataType(inData.uniqueFormat()); DataType out_data_type = helpers::GetLegacyDataType(outData.uniqueFormat()); + bool isPlanar = IsPlanar(helpers::GetLegacyDataFormat(inData)); if (channels != sch) { @@ -1018,40 +1414,78 @@ inline ErrorCode BGR_to_GRAY(const ImageBatchVarShapeDataStridedCuda &inData, int max_width = inData.maxSize().w; int max_height = inData.maxSize().h; int batch_size = inData.numImages(); + if (isPlanar && batch_size > 65535) + { + LOG_ERROR("Planar CvtColor requires numImages <= 65535 (CUDA grid-z limit)"); + return ErrorCode::INVALID_DATA_SHAPE; + } dim3 blockSize(BLOCK, BLOCK / 4, 1); dim3 gridSize(divUp(max_width, blockSize.x), divUp(max_height, blockSize.y), batch_size); + dim3 gridSize4(divUp(max_width, blockSize.x * 4), gridSize.y, gridSize.z); + +#define CVCUDA_RUN_BGR2GRAY_CHAR(T, NIX) \ + do \ + { \ + if (isPlanar) \ + { \ + cuda::ImageBatchVarShapeWrap src_ptr(inData); \ + cuda::ImageBatchVarShapeWrap dst_ptr(outData); \ + bgr_to_gray_char_chw<<>>(src_ptr, dst_ptr, bidx, sch); \ + } \ + else \ + { \ + cuda::ImageBatchVarShapeWrapNHWC src_ptr(inData, channels); \ + cuda::ImageBatchVarShapeWrapNHWC dst_ptr(outData, dcn); \ + bgr_to_gray_char_nhwc \ + <<>>(src_ptr, dst_ptr, bidx); \ + } \ + checkKernelErrors(); \ + } \ + while (0) + +#define CVCUDA_RUN_BGR2GRAY_FLOAT(T) \ + do \ + { \ + if (isPlanar) \ + { \ + cuda::ImageBatchVarShapeWrap src_ptr(inData); \ + cuda::ImageBatchVarShapeWrap dst_ptr(outData); \ + bgr_to_gray_float_chw<<>>(src_ptr, dst_ptr, bidx, sch); \ + } \ + else \ + { \ + cuda::ImageBatchVarShapeWrapNHWC src_ptr(inData, channels); \ + cuda::ImageBatchVarShapeWrapNHWC dst_ptr(outData, dcn); \ + bgr_to_gray_float_nhwc<<>>(src_ptr, dst_ptr, bidx); \ + } \ + checkKernelErrors(); \ + } \ + while (0) switch (data_type) { case kCV_8U: { - cuda::ImageBatchVarShapeWrapNHWC src_ptr(inData, channels); - cuda::ImageBatchVarShapeWrapNHWC dst_ptr(outData, dcn); - bgr_to_gray_char_nhwc<<>>(src_ptr, dst_ptr, bidx); - checkKernelErrors(); + CVCUDA_RUN_BGR2GRAY_CHAR(unsigned char, 4); } break; case kCV_16U: { - cuda::ImageBatchVarShapeWrapNHWC src_ptr(inData, channels); - cuda::ImageBatchVarShapeWrapNHWC dst_ptr(outData, dcn); - bgr_to_gray_char_nhwc<<>>(src_ptr, dst_ptr, bidx); - checkKernelErrors(); + CVCUDA_RUN_BGR2GRAY_CHAR(unsigned short, 1); } break; case kCV_32F: { - cuda::ImageBatchVarShapeWrapNHWC src_ptr(inData, channels); - cuda::ImageBatchVarShapeWrapNHWC dst_ptr(outData, dcn); - bgr_to_gray_float_nhwc<<>>(src_ptr, dst_ptr, bidx); - checkKernelErrors(); + CVCUDA_RUN_BGR2GRAY_FLOAT(float); } break; default: LOG_ERROR("Unsupported DataType " << data_type); return ErrorCode::INVALID_DATA_TYPE; } +#undef CVCUDA_RUN_BGR2GRAY_FLOAT +#undef CVCUDA_RUN_BGR2GRAY_CHAR return ErrorCode::SUCCESS; } @@ -1064,6 +1498,7 @@ inline ErrorCode BGR_to_YUV(const ImageBatchVarShapeDataStridedCuda &inData, int channels = inData.uniqueFormat().numChannels(); DataType data_type = helpers::GetLegacyDataType(inData.uniqueFormat()); DataType out_data_type = helpers::GetLegacyDataType(outData.uniqueFormat()); + bool isPlanar = IsPlanar(helpers::GetLegacyDataFormat(inData)); if (channels != 3) { @@ -1088,40 +1523,76 @@ inline ErrorCode BGR_to_YUV(const ImageBatchVarShapeDataStridedCuda &inData, int max_width = inData.maxSize().w; int max_height = inData.maxSize().h; int batch_size = inData.numImages(); + if (isPlanar && batch_size > 65535) + { + LOG_ERROR("Planar CvtColor requires numImages <= 65535 (CUDA grid-z limit)"); + return ErrorCode::INVALID_DATA_SHAPE; + } dim3 blockSize(BLOCK, BLOCK / 4, 1); dim3 gridSize(divUp(max_width, blockSize.x), divUp(max_height, blockSize.y), batch_size); +#define CVCUDA_RUN_BGR2YUV_CHAR(T) \ + do \ + { \ + if (isPlanar) \ + { \ + cuda::ImageBatchVarShapeWrap src_ptr(inData); \ + cuda::ImageBatchVarShapeWrap dst_ptr(outData); \ + bgr_to_yuv_char_chw<<>>(src_ptr, dst_ptr, bidx, channels); \ + } \ + else \ + { \ + cuda::ImageBatchVarShapeWrapNHWC src_ptr(inData, channels); \ + cuda::ImageBatchVarShapeWrapNHWC dst_ptr(outData, dcn); \ + bgr_to_yuv_char_nhwc<<>>(src_ptr, dst_ptr, bidx); \ + } \ + checkKernelErrors(); \ + } \ + while (0) + +#define CVCUDA_RUN_BGR2YUV_FLOAT(T) \ + do \ + { \ + if (isPlanar) \ + { \ + cuda::ImageBatchVarShapeWrap src_ptr(inData); \ + cuda::ImageBatchVarShapeWrap dst_ptr(outData); \ + bgr_to_yuv_float_chw<<>>(src_ptr, dst_ptr, bidx, channels); \ + } \ + else \ + { \ + cuda::ImageBatchVarShapeWrapNHWC src_ptr(inData, channels); \ + cuda::ImageBatchVarShapeWrapNHWC dst_ptr(outData, dcn); \ + bgr_to_yuv_float_nhwc<<>>(src_ptr, dst_ptr, bidx); \ + } \ + checkKernelErrors(); \ + } \ + while (0) + switch (data_type) { case kCV_8U: { - cuda::ImageBatchVarShapeWrapNHWC src_ptr(inData, channels); - cuda::ImageBatchVarShapeWrapNHWC dst_ptr(outData, dcn); - bgr_to_yuv_char_nhwc<<>>(src_ptr, dst_ptr, bidx); - checkKernelErrors(); + CVCUDA_RUN_BGR2YUV_CHAR(unsigned char); } break; case kCV_16U: { - cuda::ImageBatchVarShapeWrapNHWC src_ptr(inData, channels); - cuda::ImageBatchVarShapeWrapNHWC dst_ptr(outData, dcn); - bgr_to_yuv_char_nhwc<<>>(src_ptr, dst_ptr, bidx); - checkKernelErrors(); + CVCUDA_RUN_BGR2YUV_CHAR(unsigned short); } break; case kCV_32F: { - cuda::ImageBatchVarShapeWrapNHWC src_ptr(inData, channels); - cuda::ImageBatchVarShapeWrapNHWC dst_ptr(outData, dcn); - bgr_to_yuv_float_nhwc<<>>(src_ptr, dst_ptr, bidx); - checkKernelErrors(); + CVCUDA_RUN_BGR2YUV_FLOAT(float); } break; default: LOG_ERROR("Unsupported DataType " << data_type); return ErrorCode::INVALID_DATA_TYPE; } +#undef CVCUDA_RUN_BGR2YUV_FLOAT +#undef CVCUDA_RUN_BGR2YUV_CHAR return ErrorCode::SUCCESS; } @@ -1134,6 +1605,7 @@ inline ErrorCode YUV_to_BGR(const ImageBatchVarShapeDataStridedCuda &inData, int channels = inData.uniqueFormat().numChannels(); DataType data_type = helpers::GetLegacyDataType(inData.uniqueFormat()); DataType out_data_type = helpers::GetLegacyDataType(outData.uniqueFormat()); + bool isPlanar = IsPlanar(helpers::GetLegacyDataFormat(inData)); if (channels != 3) { @@ -1158,40 +1630,76 @@ inline ErrorCode YUV_to_BGR(const ImageBatchVarShapeDataStridedCuda &inData, int max_width = inData.maxSize().w; int max_height = inData.maxSize().h; int batch_size = inData.numImages(); + if (isPlanar && batch_size > 65535) + { + LOG_ERROR("Planar CvtColor requires numImages <= 65535 (CUDA grid-z limit)"); + return ErrorCode::INVALID_DATA_SHAPE; + } dim3 blockSize(BLOCK, BLOCK / 4, 1); dim3 gridSize(divUp(max_width, blockSize.x), divUp(max_height, blockSize.y), batch_size); +#define CVCUDA_RUN_YUV2BGR_CHAR(T) \ + do \ + { \ + if (isPlanar) \ + { \ + cuda::ImageBatchVarShapeWrap src_ptr(inData); \ + cuda::ImageBatchVarShapeWrap dst_ptr(outData); \ + yuv_to_bgr_char_chw<<>>(src_ptr, dst_ptr, bidx, dcn); \ + } \ + else \ + { \ + cuda::ImageBatchVarShapeWrapNHWC src_ptr(inData, channels); \ + cuda::ImageBatchVarShapeWrapNHWC dst_ptr(outData, dcn); \ + yuv_to_bgr_char_nhwc<<>>(src_ptr, dst_ptr, bidx); \ + } \ + checkKernelErrors(); \ + } \ + while (0) + +#define CVCUDA_RUN_YUV2BGR_FLOAT(T) \ + do \ + { \ + if (isPlanar) \ + { \ + cuda::ImageBatchVarShapeWrap src_ptr(inData); \ + cuda::ImageBatchVarShapeWrap dst_ptr(outData); \ + yuv_to_bgr_float_chw<<>>(src_ptr, dst_ptr, bidx, dcn); \ + } \ + else \ + { \ + cuda::ImageBatchVarShapeWrapNHWC src_ptr(inData, channels); \ + cuda::ImageBatchVarShapeWrapNHWC dst_ptr(outData, dcn); \ + yuv_to_bgr_float_nhwc<<>>(src_ptr, dst_ptr, bidx); \ + } \ + checkKernelErrors(); \ + } \ + while (0) + switch (data_type) { case kCV_8U: { - cuda::ImageBatchVarShapeWrapNHWC src_ptr(inData, channels); - cuda::ImageBatchVarShapeWrapNHWC dst_ptr(outData, dcn); - yuv_to_bgr_char_nhwc<<>>(src_ptr, dst_ptr, bidx); - checkKernelErrors(); + CVCUDA_RUN_YUV2BGR_CHAR(unsigned char); } break; case kCV_16U: { - cuda::ImageBatchVarShapeWrapNHWC src_ptr(inData, channels); - cuda::ImageBatchVarShapeWrapNHWC dst_ptr(outData, dcn); - yuv_to_bgr_char_nhwc<<>>(src_ptr, dst_ptr, bidx); - checkKernelErrors(); + CVCUDA_RUN_YUV2BGR_CHAR(unsigned short); } break; case kCV_32F: { - cuda::ImageBatchVarShapeWrapNHWC src_ptr(inData, channels); - cuda::ImageBatchVarShapeWrapNHWC dst_ptr(outData, dcn); - yuv_to_bgr_float_nhwc<<>>(src_ptr, dst_ptr, bidx); - checkKernelErrors(); + CVCUDA_RUN_YUV2BGR_FLOAT(float); } break; default: LOG_ERROR("Unsupported DataType " << data_type); return ErrorCode::INVALID_DATA_TYPE; } +#undef CVCUDA_RUN_YUV2BGR_FLOAT +#undef CVCUDA_RUN_YUV2BGR_CHAR return ErrorCode::SUCCESS; } @@ -1205,6 +1713,7 @@ inline ErrorCode BGR_to_HSV(const ImageBatchVarShapeDataStridedCuda &inData, int channels = inData.uniqueFormat().numChannels(); DataType data_type = helpers::GetLegacyDataType(inData.uniqueFormat()); DataType out_data_type = helpers::GetLegacyDataType(outData.uniqueFormat()); + bool isPlanar = IsPlanar(helpers::GetLegacyDataFormat(inData)); if (channels != 3) { @@ -1229,32 +1738,71 @@ inline ErrorCode BGR_to_HSV(const ImageBatchVarShapeDataStridedCuda &inData, int max_width = inData.maxSize().w; int max_height = inData.maxSize().h; int batch_size = inData.numImages(); + if (isPlanar && batch_size > 65535) + { + LOG_ERROR("Planar CvtColor requires numImages <= 65535 (CUDA grid-z limit)"); + return ErrorCode::INVALID_DATA_SHAPE; + } dim3 blockSize(BLOCK, BLOCK / 4, 1); dim3 gridSize(divUp(max_width, blockSize.x), divUp(max_height, blockSize.y), batch_size); +#define CVCUDA_RUN_BGR2HSV_CHAR(T) \ + do \ + { \ + if (isPlanar) \ + { \ + cuda::ImageBatchVarShapeWrap src_ptr(inData); \ + cuda::ImageBatchVarShapeWrap dst_ptr(outData); \ + bgr_to_hsv_char_chw<<>>(src_ptr, dst_ptr, bidx, isFullRange, channels); \ + } \ + else \ + { \ + cuda::ImageBatchVarShapeWrapNHWC src_ptr(inData, channels); \ + cuda::ImageBatchVarShapeWrapNHWC dst_ptr(outData, dcn); \ + bgr_to_hsv_char_nhwc<<>>(src_ptr, dst_ptr, bidx, isFullRange); \ + } \ + checkKernelErrors(); \ + } \ + while (0) + +#define CVCUDA_RUN_BGR2HSV_FLOAT(T) \ + do \ + { \ + if (isPlanar) \ + { \ + cuda::ImageBatchVarShapeWrap src_ptr(inData); \ + cuda::ImageBatchVarShapeWrap dst_ptr(outData); \ + bgr_to_hsv_float_chw<<>>(src_ptr, dst_ptr, bidx, channels); \ + } \ + else \ + { \ + cuda::ImageBatchVarShapeWrapNHWC src_ptr(inData, channels); \ + cuda::ImageBatchVarShapeWrapNHWC dst_ptr(outData, dcn); \ + bgr_to_hsv_float_nhwc<<>>(src_ptr, dst_ptr, bidx); \ + } \ + checkKernelErrors(); \ + } \ + while (0) + switch (data_type) { case kCV_8U: { - cuda::ImageBatchVarShapeWrapNHWC src_ptr(inData, channels); - cuda::ImageBatchVarShapeWrapNHWC dst_ptr(outData, dcn); - bgr_to_hsv_char_nhwc<<>>(src_ptr, dst_ptr, bidx, isFullRange); - checkKernelErrors(); + CVCUDA_RUN_BGR2HSV_CHAR(unsigned char); } break; case kCV_32F: { - cuda::ImageBatchVarShapeWrapNHWC src_ptr(inData, channels); - cuda::ImageBatchVarShapeWrapNHWC dst_ptr(outData, dcn); - bgr_to_hsv_float_nhwc<<>>(src_ptr, dst_ptr, bidx); - checkKernelErrors(); + CVCUDA_RUN_BGR2HSV_FLOAT(float); } break; default: LOG_ERROR("Unsupported DataType " << data_type); return ErrorCode::INVALID_DATA_TYPE; } +#undef CVCUDA_RUN_BGR2HSV_FLOAT +#undef CVCUDA_RUN_BGR2HSV_CHAR return ErrorCode::SUCCESS; } @@ -1268,6 +1816,7 @@ inline ErrorCode HSV_to_BGR(const ImageBatchVarShapeDataStridedCuda &inData, int channels = inData.uniqueFormat().numChannels(); DataType data_type = helpers::GetLegacyDataType(inData.uniqueFormat()); DataType out_data_type = helpers::GetLegacyDataType(outData.uniqueFormat()); + bool isPlanar = IsPlanar(helpers::GetLegacyDataFormat(inData)); if (channels != 3) { @@ -1292,32 +1841,72 @@ inline ErrorCode HSV_to_BGR(const ImageBatchVarShapeDataStridedCuda &inData, int max_width = inData.maxSize().w; int max_height = inData.maxSize().h; int batch_size = inData.numImages(); + if (isPlanar && batch_size > 65535) + { + LOG_ERROR("Planar CvtColor requires numImages <= 65535 (CUDA grid-z limit)"); + return ErrorCode::INVALID_DATA_SHAPE; + } dim3 blockSize(BLOCK, BLOCK / 4, 1); dim3 gridSize(divUp(max_width, blockSize.x), divUp(max_height, blockSize.y), batch_size); + dim3 gridSize2(divUp(max_width, blockSize.x * 2), gridSize.y, gridSize.z); + +#define CVCUDA_RUN_HSV2BGR_CHAR(T) \ + do \ + { \ + if (isPlanar) \ + { \ + cuda::ImageBatchVarShapeWrap src_ptr(inData); \ + cuda::ImageBatchVarShapeWrap dst_ptr(outData); \ + hsv_to_bgr_char_chw<<>>(src_ptr, dst_ptr, bidx, isFullRange, dcn); \ + } \ + else \ + { \ + cuda::ImageBatchVarShapeWrapNHWC src_ptr(inData, channels); \ + cuda::ImageBatchVarShapeWrapNHWC dst_ptr(outData, dcn); \ + hsv_to_bgr_char_nhwc<2, T><<>>(src_ptr, dst_ptr, bidx, isFullRange); \ + } \ + checkKernelErrors(); \ + } \ + while (0) + +#define CVCUDA_RUN_HSV2BGR_FLOAT(T) \ + do \ + { \ + if (isPlanar) \ + { \ + cuda::ImageBatchVarShapeWrap src_ptr(inData); \ + cuda::ImageBatchVarShapeWrap dst_ptr(outData); \ + hsv_to_bgr_float_chw<<>>(src_ptr, dst_ptr, bidx, dcn); \ + } \ + else \ + { \ + cuda::ImageBatchVarShapeWrapNHWC src_ptr(inData, channels); \ + cuda::ImageBatchVarShapeWrapNHWC dst_ptr(outData, dcn); \ + hsv_to_bgr_float_nhwc<<>>(src_ptr, dst_ptr, bidx); \ + } \ + checkKernelErrors(); \ + } \ + while (0) switch (data_type) { case kCV_8U: { - cuda::ImageBatchVarShapeWrapNHWC src_ptr(inData, channels); - cuda::ImageBatchVarShapeWrapNHWC dst_ptr(outData, dcn); - hsv_to_bgr_char_nhwc<<>>(src_ptr, dst_ptr, bidx, isFullRange); - checkKernelErrors(); + CVCUDA_RUN_HSV2BGR_CHAR(unsigned char); } break; case kCV_32F: { - cuda::ImageBatchVarShapeWrapNHWC src_ptr(inData, channels); - cuda::ImageBatchVarShapeWrapNHWC dst_ptr(outData, dcn); - hsv_to_bgr_float_nhwc<<>>(src_ptr, dst_ptr, bidx); - checkKernelErrors(); + CVCUDA_RUN_HSV2BGR_FLOAT(float); } break; default: LOG_ERROR("Unsupported DataType " << data_type); return ErrorCode::INVALID_DATA_TYPE; } +#undef CVCUDA_RUN_HSV2BGR_FLOAT +#undef CVCUDA_RUN_HSV2BGR_CHAR return ErrorCode::SUCCESS; } @@ -1325,6 +1914,12 @@ inline ErrorCode YUV420xp_to_BGR(const ImageBatchVarShapeDataStridedCuda &inData const ImageBatchVarShapeDataStridedCuda &outData, NVCVColorConversionCode code, cudaStream_t stream) { + if (IsPlanar(helpers::GetLegacyDataFormat(inData)) || IsPlanar(helpers::GetLegacyDataFormat(outData))) + { + LOG_ERROR("Planar CvtColor does not support subsampled YUV420 conversion codes"); + return ErrorCode::INVALID_DATA_FORMAT; + } + int bidx = (code == NVCV_COLOR_YUV2BGR_NV12 || code == NVCV_COLOR_YUV2BGRA_NV12 || code == NVCV_COLOR_YUV2BGR_NV21 || code == NVCV_COLOR_YUV2BGRA_NV21 || code == NVCV_COLOR_YUV2BGR_YV12 || code == NVCV_COLOR_YUV2BGRA_YV12 @@ -1417,6 +2012,12 @@ inline ErrorCode YUV422_to_BGR(const ImageBatchVarShapeDataStridedCuda &inData, const ImageBatchVarShapeDataStridedCuda &outData, NVCVColorConversionCode code, cudaStream_t stream) { + if (IsPlanar(helpers::GetLegacyDataFormat(inData)) || IsPlanar(helpers::GetLegacyDataFormat(outData))) + { + LOG_ERROR("Planar CvtColor does not support packed YUV422 conversion codes"); + return ErrorCode::INVALID_DATA_FORMAT; + } + int bidx = (code == NVCV_COLOR_YUV2BGR_YUY2 || code == NVCV_COLOR_YUV2BGRA_YUY2 || code == NVCV_COLOR_YUV2BGR_YVYU || code == NVCV_COLOR_YUV2BGRA_YVYU || code == NVCV_COLOR_YUV2BGR_UYVY || code == NVCV_COLOR_YUV2BGRA_UYVY) @@ -1560,6 +2161,12 @@ inline ErrorCode BGR_to_YUV420xp(const ImageBatchVarShapeDataStridedCuda &inData const ImageBatchVarShapeDataStridedCuda &outData, NVCVColorConversionCode code, cudaStream_t stream) { + if (IsPlanar(helpers::GetLegacyDataFormat(inData)) || IsPlanar(helpers::GetLegacyDataFormat(outData))) + { + LOG_ERROR("Planar CvtColor does not support subsampled YUV420 conversion codes"); + return ErrorCode::INVALID_DATA_FORMAT; + } + int bidx = (code == NVCV_COLOR_BGR2YUV_NV12 || code == NVCV_COLOR_BGRA2YUV_NV12 || code == NVCV_COLOR_BGR2YUV_NV21 || code == NVCV_COLOR_BGRA2YUV_NV21 || code == NVCV_COLOR_BGR2YUV_YV12 || code == NVCV_COLOR_BGRA2YUV_YV12 @@ -1659,9 +2266,10 @@ ErrorCode CvtColorVarShape::infer(const ImageBatchVarShapeDataStridedCuda &inDat DataFormat format = input_format; - if (!(format == kNHWC || format == kHWC)) + if (!(format == kNHWC || format == kHWC || format == kNCHW || format == kCHW)) { - LOG_ERROR("Invalid input DataFormat " << format << ", the valid DataFormats are: \"NHWC\", \"HWC\""); + LOG_ERROR("Invalid input DataFormat " << format + << ", the valid DataFormats are: \"NHWC\", \"HWC\", \"NCHW\", \"CHW\""); return ErrorCode::INVALID_DATA_FORMAT; } @@ -1875,9 +2483,19 @@ ErrorCode CvtColorVarShape::infer(const ImageBatchVarShapeDataStridedCuda &inDat 0, // CV_COLORCVT_MAX = 148 }; + if (code < 0 || static_cast(code) >= sizeof(funcs) / sizeof(funcs[0])) + { + LOG_ERROR("Invalid convert color code: " << code); + return ErrorCode::INVALID_PARAMETER; + } + func_t func = funcs[code]; - NVCV_ASSERT(func != 0); + if (func == 0) + { + LOG_ERROR("Invalid convert color code: " << code); + return ErrorCode::INVALID_PARAMETER; + } return func(inData, outData, code, stream); } diff --git a/src/cvcuda/priv/legacy/erase.cu b/src/cvcuda/priv/legacy/erase.cu index fdab1090a..daf9bc50e 100644 --- a/src/cvcuda/priv/legacy/erase.cu +++ b/src/cvcuda/priv/legacy/erase.cu @@ -1,4 +1,4 @@ -/* Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +/* Copyright (c) 2021-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * * SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES * SPDX-License-Identifier: Apache-2.0 @@ -22,7 +22,10 @@ #include "CvCudaLegacyHelpers.hpp" #include "CvCudaUtils.cuh" -#include "cub/cub.cuh" + +#include + +#include using namespace nvcv::legacy::helpers; @@ -70,6 +73,40 @@ __global__ void erase(Wrapper img, int imgH, int imgW, nvcv::cuda::Tensor1DWrap< } } +template +__global__ void erase_planar(Wrapper img, int imgH, int imgW, nvcv::cuda::Tensor1DWrap anchorVec, + nvcv::cuda::Tensor1DWrap erasingVec, nvcv::cuda::Tensor1DWrap valuesVec, + nvcv::cuda::Tensor1DWrap imgIdxVec, int channels, int random, unsigned int seed) +{ + unsigned int id = threadIdx.x + blockIdx.x * blockDim.x; + int c = blockIdx.y; + int eraseId = blockIdx.z; + int2 anchor = anchorVec[eraseId]; + int3 erasing = erasingVec[eraseId]; + float value = valuesVec[eraseId * channels + c]; + int batchId = imgIdxVec[eraseId]; + if (id < erasing.y * erasing.x && (0x1 & (erasing.z >> c)) == 1) + { + int x = id % erasing.x; + int y = id / erasing.x; + if (anchor.x + x < imgW && anchor.y + y < imgH) + { + if (random) + { + unsigned int hashValue = seed + threadIdx.x + + 0x26AD0C9 * blockDim.x * blockDim.y * blockDim.z * (blockIdx.x + 1) + * (blockIdx.y + 1) * (blockIdx.z + 1); + *img.ptr(batchId, c, anchor.y + y, anchor.x + x) + = nvcv::cuda::SaturateCast(erase_hash(hashValue) % 256); + } + else + { + *img.ptr(batchId, c, anchor.y + y, anchor.x + x) = nvcv::cuda::SaturateCast(value); + } + } + } +} + template void eraseCaller(const nvcv::TensorDataStridedCuda &imgs, const nvcv::TensorDataStridedCuda &anchor, const nvcv::TensorDataStridedCuda &erasing, const nvcv::TensorDataStridedCuda &imgIdx, @@ -91,7 +128,81 @@ void eraseCaller(const nvcv::TensorDataStridedCuda &imgs, const nvcv::TensorData seed); } +template +void erasePlanarCaller(const nvcv::TensorDataStridedCuda &imgs, const nvcv::TensorDataStridedCuda &anchor, + const nvcv::TensorDataStridedCuda &erasing, const nvcv::TensorDataStridedCuda &imgIdx, + const nvcv::TensorDataStridedCuda &values, int max_eh, int max_ew, int num_erasing_area, + bool random, unsigned int seed, int rows, int cols, int channels, cudaStream_t stream) +{ + auto wrap = nvcv::cuda::CreateTensorWrapNCHW(imgs); + + nvcv::cuda::Tensor1DWrap anchorVec(anchor); + nvcv::cuda::Tensor1DWrap erasingVec(erasing); + nvcv::cuda::Tensor1DWrap imgIdxVec(imgIdx); + nvcv::cuda::Tensor1DWrap valuesVec(values); + + int blockSize = (max_eh * max_ew < 1024) ? max_eh * max_ew : 1024; + int gridSize = divUp(max_eh * max_ew, 1024); + dim3 block(blockSize); + dim3 grid(gridSize, channels, num_erasing_area); + erase_planar<<>>(wrap, rows, cols, anchorVec, erasingVec, valuesVec, imgIdxVec, channels, + random, seed); +} + namespace { +static ErrorCode validateParameterLength(const nvcv::TensorDataStridedCuda &tensor, int expectedLength, + const char *name) +{ + if (tensor.shape()[0] < expectedLength) + { + LOG_ERROR("Invalid " << name << " length " << tensor.shape()[0] << ", expected at least " << expectedLength); + return ErrorCode::INVALID_PARAMETER; + } + + return ErrorCode::SUCCESS; +} + +// Per-element host-side validation of anchor / imgIdx. The kernel indexes +// img.ptr(imgIdx[i], ...) without a bound check on the batch axis, so an +// out-of-range imgIdx is an unchecked OOB write — this validation closes +// that hole. It necessarily issues a D→H copy and synchronizes the user's +// stream before launching the erase kernel; the sync is intentional and +// is the cost of throwing a clean INVALID_PARAMETER exception instead of +// silently corrupting memory. +static ErrorCode validateEraseAreaData(const nvcv::TensorDataStridedCuda &anchor, + const nvcv::TensorDataStridedCuda &imgIdx, int numErasingArea, int numSamples, + cudaStream_t stream) +{ + std::vector hostAnchor(numErasingArea); + std::vector hostImgIdx(numErasingArea); + + // Use cudaMemcpy2DAsync to honor the source tensor's element stride; a + // rank-1 view of a larger tensor is not guaranteed to be packed. + checkCudaErrors(cudaMemcpy2DAsync(hostAnchor.data(), sizeof(int2), anchor.basePtr(), anchor.stride(0), sizeof(int2), + numErasingArea, cudaMemcpyDeviceToHost, stream)); + checkCudaErrors(cudaMemcpy2DAsync(hostImgIdx.data(), sizeof(int), imgIdx.basePtr(), imgIdx.stride(0), sizeof(int), + numErasingArea, cudaMemcpyDeviceToHost, stream)); + checkCudaErrors(cudaStreamSynchronize(stream)); + + for (int i = 0; i < numErasingArea; ++i) + { + if (hostAnchor[i].x < 0 || hostAnchor[i].y < 0) + { + LOG_ERROR("Invalid anchor at erase area " << i << ": (" << hostAnchor[i].x << ", " << hostAnchor[i].y + << ")"); + return ErrorCode::INVALID_PARAMETER; + } + if (hostImgIdx[i] < 0 || hostImgIdx[i] >= numSamples) + { + LOG_ERROR("Invalid imgIdx at erase area " << i << ": " << hostImgIdx[i] << ", expected [0, " << numSamples + << ")"); + return ErrorCode::INVALID_PARAMETER; + } + } + + return ErrorCode::SUCCESS; +} + struct MaxWH { __device__ __forceinline__ int3 operator()(const int3 &a, const int3 &b) const @@ -99,20 +210,71 @@ struct MaxWH return int3{max(a.x, b.x), max(a.y, b.y), 0}; } }; + +static bool copyContiguousTensor(const nvcv::TensorDataAccessStridedImagePlanar &inAccess, + const nvcv::TensorDataAccessStridedImagePlanar &outAccess, bool isPlanar, int channels, + cudaStream_t stream) +{ + if (inAccess.numSamples() <= 0 || inAccess.numSamples() != outAccess.numSamples() + || inAccess.numRows() != outAccess.numRows() || inAccess.numCols() != outAccess.numCols() + || inAccess.numChannels() != outAccess.numChannels() || channels != inAccess.numChannels() + || inAccess.colStride() != outAccess.colStride()) + { + return false; + } + + const int64_t rowBytes = static_cast(inAccess.numCols()) * inAccess.colStride(); + if (rowBytes <= 0 || inAccess.rowStride() != rowBytes || outAccess.rowStride() != rowBytes) + { + return false; + } + + const int64_t planeBytes = static_cast(inAccess.numRows()) * inAccess.rowStride(); + int64_t sampleBytes; + if (isPlanar) + { + if (inAccess.chStride() != planeBytes || outAccess.chStride() != planeBytes) + { + return false; + } + sampleBytes = static_cast(channels) * planeBytes; + } + else + { + sampleBytes = planeBytes; + } + + if (sampleBytes <= 0) + { + return false; + } + + if (inAccess.numSamples() > 1 + && (inAccess.sampleStride() != sampleBytes || outAccess.sampleStride() != sampleBytes)) + { + return false; + } + + const size_t totalBytes = static_cast(sampleBytes) * static_cast(inAccess.numSamples()); + checkCudaErrors( + cudaMemcpyAsync(outAccess.sampleData(0), inAccess.sampleData(0), totalBytes, cudaMemcpyDeviceToDevice, stream)); + return true; +} } // namespace namespace nvcv::legacy::cuda_op { -Erase::Erase(DataShape max_input_shape, DataShape max_output_shape, int num_erasing_area) +Erase::Erase(DataShape max_input_shape, DataShape max_output_shape, int num_erasing_area, bool useBulkCopy) : CudaBaseOp(max_input_shape, max_output_shape) , d_max_values(nullptr) , temp_storage(nullptr) + , m_useBulkCopy(useBulkCopy) { cudaError_t err = cudaMalloc(&d_max_values, sizeof(int3)); if (err != cudaSuccess) { LOG_ERROR("CUDA memory allocation error of size: " << sizeof(int3)); - throw std::runtime_error("CUDA memory allocation error!"); + throw LegacyCudaAllocationError("CUDA memory allocation error!"); } max_num_erasing_area = num_erasing_area; @@ -122,20 +284,22 @@ Erase::Erase(DataShape max_input_shape, DataShape max_output_shape, int num_eras LOG_ERROR("Invalid num of erasing area" << max_num_erasing_area); throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "max_num_erasing_area must be >= 0"); } - temp_storage = NULL; + temp_storage = nullptr; storage_bytes = 0; MaxWH mwh; int3 init = {0, 0, 0}; cub::DeviceReduce::Reduce(temp_storage, storage_bytes, (int3 *)nullptr, (int3 *)nullptr, max_num_erasing_area, mwh, init); - err = cudaMalloc(&temp_storage, storage_bytes); + void *raw_storage = nullptr; + err = cudaMalloc(&raw_storage, storage_bytes); if (err != cudaSuccess) { cudaFree(d_max_values); LOG_ERROR("CUDA memory allocation error of size: " << storage_bytes); - throw std::runtime_error("CUDA memory allocation error!"); + throw LegacyCudaAllocationError("CUDA memory allocation error!"); } + temp_storage = static_cast(raw_storage); } Erase::~Erase() @@ -160,16 +324,24 @@ ErrorCode Erase::infer(const TensorDataStridedCuda &inData, const TensorDataStri DataType data_type = GetLegacyDataType(inData.dtype()); DataType out_data_type = GetLegacyDataType(outData.dtype()); - if (!(format == kNHWC || format == kHWC)) + if (!(format == kNHWC || format == kHWC || format == kNCHW || format == kCHW)) { - LOG_ERROR("Invalid input DataFormat " << format << ", the valid DataFormats are: \"NHWC\", \"HWC\""); + LOG_ERROR("Invalid input DataFormat " << format + << ", the valid DataFormats are: \"NHWC\", \"HWC\", \"NCHW\", \"CHW\""); return ErrorCode::INVALID_DATA_FORMAT; } - if (!(out_format == kNHWC || out_format == kHWC)) + if (!(out_format == kNHWC || out_format == kHWC || out_format == kNCHW || out_format == kCHW)) { - LOG_ERROR("Invalid output DataFormat " << out_format << ", the valid DataFormats are: \"NHWC\", \"HWC\""); + LOG_ERROR("Invalid output DataFormat " << out_format + << ", the valid DataFormats are: \"NHWC\", \"HWC\", \"NCHW\", \"CHW\""); return ErrorCode::INVALID_DATA_FORMAT; } + if (format != out_format) + { + LOG_ERROR("Invalid DataFormat between input (" << format << ") and output (" << out_format << ")"); + return ErrorCode::INVALID_DATA_FORMAT; + } + const bool isPlanar = (format == kNCHW || format == kCHW); if (!(data_type == kCV_8U || data_type == kCV_16U || data_type == kCV_16S || data_type == kCV_32S || data_type == kCV_32F)) @@ -220,6 +392,10 @@ ErrorCode Erase::infer(const TensorDataStridedCuda &inData, const TensorDataStri LOG_ERROR("Invalid erasing Dim " << erasing_dim); return ErrorCode::INVALID_DATA_FORMAT; } + if (auto status = validateParameterLength(erasing, num_erasing_area, "erasing"); status != ErrorCode::SUCCESS) + { + return status; + } DataType imgidx_data_type = GetLegacyDataType(imgIdx.dtype()); if (imgidx_data_type != kCV_32S) @@ -233,6 +409,10 @@ ErrorCode Erase::infer(const TensorDataStridedCuda &inData, const TensorDataStri LOG_ERROR("Invalid imgIdx Dim " << imgidx_dim); return ErrorCode::INVALID_DATA_FORMAT; } + if (auto status = validateParameterLength(imgIdx, num_erasing_area, "imgIdx"); status != ErrorCode::SUCCESS) + { + return status; + } DataType values_data_type = GetLegacyDataType(values.dtype()); if (values_data_type != kCV_32F) @@ -253,16 +433,56 @@ ErrorCode Erase::infer(const TensorDataStridedCuda &inData, const TensorDataStri auto outAccess = TensorDataAccessStridedImagePlanar::Create(outData); NVCV_ASSERT(outAccess); - if (!inplace) + const int channels = inAccess->numChannels(); + if (channels > 4 || (isPlanar && channels == 2)) + { + LOG_ERROR("Invalid channel number " << channels); + return ErrorCode::INVALID_DATA_SHAPE; + } + if (auto status = validateParameterLength(values, num_erasing_area * channels, "values"); + status != ErrorCode::SUCCESS) { - for (uint32_t i = 0; i < inAccess->numSamples(); ++i) + return status; + } + if (num_erasing_area > 0) + { + if (auto status = validateEraseAreaData(anchor, imgIdx, num_erasing_area, inAccess->numSamples(), stream); + status != ErrorCode::SUCCESS) { - void *inSampData = inAccess->sampleData(i); - void *outSampData = outAccess->sampleData(i); + return status; + } + } - checkCudaErrors(cudaMemcpy2DAsync(outSampData, outAccess->rowStride(), inSampData, inAccess->rowStride(), - inAccess->numCols() * inAccess->colStride(), inAccess->numRows(), - cudaMemcpyDeviceToDevice, stream)); + if (!inplace) + { + if (!m_useBulkCopy || !copyContiguousTensor(*inAccess, *outAccess, isPlanar, channels, stream)) + { + for (uint32_t i = 0; i < inAccess->numSamples(); ++i) + { + if (isPlanar) + { + for (int c = 0; c < channels; ++c) + { + nvcv::Byte *inSampData = inAccess->sampleData(i) + c * inAccess->chStride(); + nvcv::Byte *outSampData = outAccess->sampleData(i) + c * outAccess->chStride(); + + checkCudaErrors(cudaMemcpy2DAsync(outSampData, outAccess->rowStride(), inSampData, + inAccess->rowStride(), + inAccess->numCols() * inAccess->colStride(), + inAccess->numRows(), cudaMemcpyDeviceToDevice, stream)); + } + } + else + { + void *inSampData = inAccess->sampleData(i); + void *outSampData = outAccess->sampleData(i); + + checkCudaErrors(cudaMemcpy2DAsync(outSampData, outAccess->rowStride(), inSampData, + inAccess->rowStride(), + inAccess->numCols() * inAccess->colStride(), inAccess->numRows(), + cudaMemcpyDeviceToDevice, stream)); + } + } } } @@ -295,15 +515,20 @@ ErrorCode Erase::infer(const TensorDataStridedCuda &inData, const TensorDataStri const TensorDataStridedCuda &values, int max_eh, int max_ew, int num_erasing_area, bool random, unsigned int seed, int rows, int cols, int channels, cudaStream_t stream); - static const erase_t funcs[6] = {eraseCaller, eraseCaller, eraseCaller, - eraseCaller, eraseCaller, eraseCaller}; + static const erase_t funcs[6] + = {eraseCaller, 0, eraseCaller, eraseCaller, eraseCaller, eraseCaller}; + static const erase_t planarFuncs[6] + = {erasePlanarCaller, 0, erasePlanarCaller, erasePlanarCaller, erasePlanarCaller, + erasePlanarCaller}; + + const erase_t func = isPlanar ? planarFuncs[data_type] : funcs[data_type]; if (inplace) - funcs[data_type](inData, anchor, erasing, imgIdx, values, max_eh, max_ew, num_erasing_area, random, seed, - inAccess->numRows(), inAccess->numCols(), inAccess->numChannels(), stream); + func(inData, anchor, erasing, imgIdx, values, max_eh, max_ew, num_erasing_area, random, seed, + inAccess->numRows(), inAccess->numCols(), inAccess->numChannels(), stream); else - funcs[data_type](outData, anchor, erasing, imgIdx, values, max_eh, max_ew, num_erasing_area, random, seed, - outAccess->numRows(), outAccess->numCols(), outAccess->numChannels(), stream); + func(outData, anchor, erasing, imgIdx, values, max_eh, max_ew, num_erasing_area, random, seed, + outAccess->numRows(), outAccess->numCols(), outAccess->numChannels(), stream); return SUCCESS; } diff --git a/src/cvcuda/priv/legacy/erase_var_shape.cu b/src/cvcuda/priv/legacy/erase_var_shape.cu index 44418c4de..132d842ba 100644 --- a/src/cvcuda/priv/legacy/erase_var_shape.cu +++ b/src/cvcuda/priv/legacy/erase_var_shape.cu @@ -1,4 +1,4 @@ -/* Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +/* Copyright (c) 2021-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * * SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES * SPDX-License-Identifier: Apache-2.0 @@ -22,7 +22,11 @@ #include "CvCudaLegacyHelpers.hpp" #include "CvCudaUtils.cuh" -#include "cub/cub.cuh" + +#include + +#include +#include using namespace nvcv::legacy::helpers; @@ -70,6 +74,76 @@ __global__ void erase(nvcv::cuda::ImageBatchVarShapeWrapNHWC img, nvcv::cuda: } } +template +__global__ void erase_planar(nvcv::cuda::ImageBatchVarShapeWrap img, nvcv::cuda::Tensor1DWrap anchorVec, + nvcv::cuda::Tensor1DWrap erasingVec, nvcv::cuda::Tensor1DWrap valuesVec, + nvcv::cuda::Tensor1DWrap imgIdxVec, int channels, int random, unsigned int seed) +{ + unsigned int id = threadIdx.x + blockIdx.x * blockDim.x; + int c = blockIdx.y; + int eraseId = blockIdx.z; + int2 anchor = anchorVec[eraseId]; + int3 erasing = erasingVec[eraseId]; + float value = valuesVec[eraseId * channels + c]; + int batchId = imgIdxVec[eraseId]; + if (id < erasing.y * erasing.x && (0x1 & (erasing.z >> c)) == 1) + { + int x = id % erasing.x; + int y = id / erasing.x; + if ((anchor.x + x) < img.width(batchId) && (anchor.y + y) < img.height(batchId)) + { + if (random) + { + unsigned int hashValue = seed + threadIdx.x + + 0x26AD0C9 * blockDim.x * blockDim.y * blockDim.z * (blockIdx.x + 1) + * (blockIdx.y + 1) * (blockIdx.z + 1); + *img.ptr(batchId, c, anchor.y + y, anchor.x + x) + = nvcv::cuda::SaturateCast(erase_var_shape_hash(hashValue) % 256); + } + else + { + *img.ptr(batchId, c, anchor.y + y, anchor.x + x) = nvcv::cuda::SaturateCast(value); + } + } + } +} + +constexpr int kCopyVarShapeNIX = 4; + +template +__global__ void copyVarShapeRows(nvcv::cuda::ImageBatchVarShapeWrap src, + nvcv::cuda::ImageBatchVarShapeWrap dst, int channels, int elementBytes, + bool isPlanar) +{ + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + const int x0 = blockIdx.x * blockDim.x + threadIdx.x; + const int stride = gridDim.x * blockDim.x; + const int planes = isPlanar ? channels : 1; + + for (int p = 0; p < planes; ++p) + { + if (y >= dst.height(batch_idx, p)) + { + continue; + } + + const int rowVecs = dst.width(batch_idx, p) * elementBytes * (isPlanar ? 1 : channels) / sizeof(uint4); + const uint4 *srcRow = reinterpret_cast(src.ptr(batch_idx, p, y, 0)); + uint4 *dstRow = reinterpret_cast(dst.ptr(batch_idx, p, y, 0)); + +#pragma unroll + for (int i = 0; i < NIX; ++i) + { + const int x = x0 + i * stride; + if (x < rowVecs) + { + dstRow[x] = srcRow[x]; + } + } + } +} + template void eraseCaller(const nvcv::ImageBatchVarShapeDataStridedCuda &imgs, const nvcv::TensorDataStridedCuda &anchor, const nvcv::TensorDataStridedCuda &erasing, const nvcv::TensorDataStridedCuda &imgIdx, @@ -91,7 +165,82 @@ void eraseCaller(const nvcv::ImageBatchVarShapeDataStridedCuda &imgs, const nvcv erase<<>>(src, anchorVec, erasingVec, valuesVec, imgIdxVec, channel, random, seed); } +template +void erasePlanarCaller(const nvcv::ImageBatchVarShapeDataStridedCuda &imgs, const nvcv::TensorDataStridedCuda &anchor, + const nvcv::TensorDataStridedCuda &erasing, const nvcv::TensorDataStridedCuda &imgIdx, + const nvcv::TensorDataStridedCuda &values, int max_eh, int max_ew, int num_erasing_area, + bool random, unsigned int seed, cudaStream_t stream) +{ + nvcv::cuda::ImageBatchVarShapeWrap src(imgs); + + nvcv::cuda::Tensor1DWrap anchorVec(anchor); + nvcv::cuda::Tensor1DWrap erasingVec(erasing); + nvcv::cuda::Tensor1DWrap imgIdxVec(imgIdx); + nvcv::cuda::Tensor1DWrap valuesVec(values); + + int channel = imgs.uniqueFormat().numChannels(); + int blockSize = (max_eh * max_ew < 1024) ? max_eh * max_ew : 1024; + int gridSize = divUp(max_eh * max_ew, 1024); + dim3 block(blockSize); + dim3 grid(gridSize, channel, num_erasing_area); + erase_planar + <<>>(src, anchorVec, erasingVec, valuesVec, imgIdxVec, channel, random, seed); +} + namespace { +static ErrorCode validateParameterLength(const nvcv::TensorDataStridedCuda &tensor, int expectedLength, + const char *name) +{ + if (tensor.shape()[0] < expectedLength) + { + LOG_ERROR("Invalid " << name << " length " << tensor.shape()[0] << ", expected at least " << expectedLength); + return ErrorCode::INVALID_PARAMETER; + } + + return ErrorCode::SUCCESS; +} + +// Per-element host-side validation of anchor / imgIdx. The kernel indexes +// img.ptr(imgIdx[i], ...) without a bound check on the batch axis, so an +// out-of-range imgIdx is an unchecked OOB write — this validation closes +// that hole. It necessarily issues a D→H copy and synchronizes the user's +// stream before launching the erase kernel; the sync is intentional and +// is the cost of throwing a clean INVALID_PARAMETER exception instead of +// silently corrupting memory. +static ErrorCode validateEraseAreaData(const nvcv::TensorDataStridedCuda &anchor, + const nvcv::TensorDataStridedCuda &imgIdx, int numErasingArea, int numSamples, + cudaStream_t stream) +{ + std::vector hostAnchor(numErasingArea); + std::vector hostImgIdx(numErasingArea); + + // Use cudaMemcpy2DAsync to honor the source tensor's element stride; a + // rank-1 view of a larger tensor is not guaranteed to be packed. + checkCudaErrors(cudaMemcpy2DAsync(hostAnchor.data(), sizeof(int2), anchor.basePtr(), anchor.stride(0), sizeof(int2), + numErasingArea, cudaMemcpyDeviceToHost, stream)); + checkCudaErrors(cudaMemcpy2DAsync(hostImgIdx.data(), sizeof(int), imgIdx.basePtr(), imgIdx.stride(0), sizeof(int), + numErasingArea, cudaMemcpyDeviceToHost, stream)); + checkCudaErrors(cudaStreamSynchronize(stream)); + + for (int i = 0; i < numErasingArea; ++i) + { + if (hostAnchor[i].x < 0 || hostAnchor[i].y < 0) + { + LOG_ERROR("Invalid anchor at erase area " << i << ": (" << hostAnchor[i].x << ", " << hostAnchor[i].y + << ")"); + return ErrorCode::INVALID_PARAMETER; + } + if (hostImgIdx[i] < 0 || hostImgIdx[i] >= numSamples) + { + LOG_ERROR("Invalid imgIdx at erase area " << i << ": " << hostImgIdx[i] << ", expected [0, " << numSamples + << ")"); + return ErrorCode::INVALID_PARAMETER; + } + } + + return ErrorCode::SUCCESS; +} + struct MaxWH { __device__ __forceinline__ int3 operator()(const int3 &a, const int3 &b) const @@ -99,20 +248,117 @@ struct MaxWH return int3{max(a.x, b.x), max(a.y, b.y), 0}; } }; + +static int elementSizeBytes(DataType dataType) +{ + switch (dataType) + { + case kCV_8U: + return 1; + case kCV_16U: + case kCV_16S: + return 2; + case kCV_32S: + case kCV_32F: + return 4; + default: + return 0; + } +} + +static bool canUseVarShapeRowCopy(const nvcv::ImageBatchVarShape &inbatch, const nvcv::ImageBatchVarShape &outbatch, + bool isPlanar, int channels, int elementBytes) +{ + const int expectedPlanes = isPlanar ? channels : 1; + if (expectedPlanes <= 0 || elementBytes <= 0) + { + return false; + } + + for (auto init = inbatch.begin(), outit = outbatch.begin(); init != inbatch.end() && outit != outbatch.end(); + ++init, ++outit) + { + const nvcv::Image &inimg = *init; + const nvcv::Image &outimg = *outit; + auto inimgdata = inimg.exportData(); + auto outimgdata = outimg.exportData(); + if (inimgdata->numPlanes() != expectedPlanes || outimgdata->numPlanes() != expectedPlanes) + { + return false; + } + + for (int p = 0; p < expectedPlanes; ++p) + { + const nvcv::ImagePlaneStrided &inplane = inimgdata->plane(p); + const nvcv::ImagePlaneStrided &outplane = outimgdata->plane(p); + if (inplane.width != outplane.width || inplane.height != outplane.height) + { + return false; + } + + const int64_t rowBytes = static_cast(inplane.width) * elementBytes * (isPlanar ? 1 : channels); + if (rowBytes <= 0 || rowBytes % static_cast(sizeof(uint4)) != 0 || inplane.rowStride < rowBytes + || outplane.rowStride < rowBytes || inplane.rowStride % static_cast(sizeof(uint4)) != 0 + || outplane.rowStride % static_cast(sizeof(uint4)) != 0 + || (reinterpret_cast(inplane.basePtr) & (sizeof(uint4) - 1)) != 0 + || (reinterpret_cast(outplane.basePtr) & (sizeof(uint4) - 1)) != 0) + { + return false; + } + } + } + + return true; +} + +static bool copyVarShapeBatch(const nvcv::ImageBatchVarShapeDataStridedCuda &inData, + const nvcv::ImageBatchVarShapeDataStridedCuda &outData, bool isPlanar, int channels, + int elementBytes, cudaStream_t stream) +{ + if (outData.numImages() <= 0 || outData.numImages() > 65535) + { + return false; + } + + const nvcv::Size2D maxSize = outData.maxSize(); + if (maxSize.w <= 0 || maxSize.h <= 0) + { + return false; + } + + const int rowBytes = maxSize.w * elementBytes * (isPlanar ? 1 : channels); + if (rowBytes <= 0 || rowBytes % static_cast(sizeof(uint4)) != 0) + { + return false; + } + + const int rowVecs = rowBytes / sizeof(uint4); + dim3 blockSize(32, 8, 1); + dim3 gridSize(divUp(divUp(rowVecs, kCopyVarShapeNIX), static_cast(blockSize.x)), + divUp(maxSize.h, static_cast(blockSize.y)), outData.numImages()); + + nvcv::cuda::ImageBatchVarShapeWrap src(inData); + nvcv::cuda::ImageBatchVarShapeWrap dst(outData); + copyVarShapeRows<<>>(src, dst, channels, elementBytes, isPlanar); + checkKernelErrors(); + return true; +} } // namespace namespace nvcv::legacy::cuda_op { -EraseVarShape::EraseVarShape(DataShape max_input_shape, DataShape max_output_shape, int num_erasing_area) +EraseVarShape::EraseVarShape(DataShape max_input_shape, DataShape max_output_shape, int num_erasing_area, + bool useBulkCopy) : CudaBaseOp(max_input_shape, max_output_shape) , d_max_values(nullptr) , temp_storage(nullptr) + , m_useBulkCopy(useBulkCopy) { cudaError_t err = cudaMalloc(&d_max_values, sizeof(int3)); if (err != cudaSuccess) { LOG_ERROR("CUDA memory allocation error of size: " << sizeof(int3)); - throw std::runtime_error("CUDA memory allocation error!"); + throw LegacyCudaAllocationError("CUDA memory allocation error!"); } max_num_erasing_area = num_erasing_area; @@ -122,20 +368,22 @@ EraseVarShape::EraseVarShape(DataShape max_input_shape, DataShape max_output_sha LOG_ERROR("Invalid num of erasing area" << max_num_erasing_area); throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "max_num_erasing_area must be >= 0"); } - temp_storage = NULL; + temp_storage = nullptr; storage_bytes = 0; MaxWH mwh; int3 init = {0, 0, 0}; cub::DeviceReduce::Reduce(temp_storage, storage_bytes, (int3 *)nullptr, (int3 *)nullptr, max_num_erasing_area, mwh, init); - err = cudaMalloc(&temp_storage, storage_bytes); + void *raw_storage = nullptr; + err = cudaMalloc(&raw_storage, storage_bytes); if (err != cudaSuccess) { cudaFree(d_max_values); LOG_ERROR("CUDA memory allocation error of size: " << storage_bytes); - throw std::runtime_error("CUDA memory allocation error!"); + throw LegacyCudaAllocationError("CUDA memory allocation error!"); } + temp_storage = static_cast(raw_storage); } EraseVarShape::~EraseVarShape() @@ -159,11 +407,19 @@ ErrorCode EraseVarShape::infer(const nvcv::ImageBatchVarShape &inbatch, const nv if (inData == nullptr) { LOG_ERROR("Input must be varshape image batch"); + return ErrorCode::INVALID_PARAMETER; } auto outData = outbatch.exportData(stream); if (outData == nullptr) { LOG_ERROR("Output must be varshape image batch"); + return ErrorCode::INVALID_PARAMETER; + } + + if (inData->numImages() != outData->numImages()) + { + LOG_ERROR("Input and output batches must have the same number of images"); + return ErrorCode::INVALID_DATA_SHAPE; } if (!inData->uniqueFormat()) @@ -179,16 +435,24 @@ ErrorCode EraseVarShape::infer(const nvcv::ImageBatchVarShape &inbatch, const nv DataFormat format = helpers::GetLegacyDataFormat(*inData); DataFormat out_format = helpers::GetLegacyDataFormat(*outData); - if (!(format == kNHWC || format == kHWC)) + if (!(format == kNHWC || format == kHWC || format == kNCHW || format == kCHW)) { - LOG_ERROR("Invalid input DataFormat " << format << ", the valid DataFormats are: \"NHWC\", \"HWC\""); + LOG_ERROR("Invalid input DataFormat " << format + << ", the valid DataFormats are: \"NHWC\", \"HWC\", \"NCHW\", \"CHW\""); return ErrorCode::INVALID_DATA_FORMAT; } - if (!(out_format == kNHWC || out_format == kHWC)) + if (!(out_format == kNHWC || out_format == kHWC || out_format == kNCHW || out_format == kCHW)) { - LOG_ERROR("Invalid output DataFormat " << out_format << ", the valid DataFormats are: \"NHWC\", \"HWC\""); + LOG_ERROR("Invalid output DataFormat " << out_format + << ", the valid DataFormats are: \"NHWC\", \"HWC\", \"NCHW\", \"CHW\""); return ErrorCode::INVALID_DATA_FORMAT; } + if (format != out_format) + { + LOG_ERROR("Invalid DataFormat between input (" << format << ") and output (" << out_format << ")"); + return ErrorCode::INVALID_DATA_FORMAT; + } + const bool isPlanar = (format == kNCHW || format == kCHW); DataType data_type = helpers::GetLegacyDataType(inData->uniqueFormat()); DataType out_data_type = helpers::GetLegacyDataType(outData->uniqueFormat()); @@ -198,12 +462,19 @@ ErrorCode EraseVarShape::infer(const nvcv::ImageBatchVarShape &inbatch, const nv LOG_ERROR("Invalid DataType " << data_type); return ErrorCode::INVALID_DATA_TYPE; } + const int elementBytes = elementSizeBytes(data_type); if (data_type != out_data_type) { LOG_ERROR("DataType of input and output must be equal, but got " << data_type << " and " << out_data_type); return ErrorCode::INVALID_DATA_TYPE; } + const int channels = inData->uniqueFormat().numChannels(); + if (channels > 4 || (isPlanar && channels == 2)) + { + LOG_ERROR("Invalid channel number " << channels); + return ErrorCode::INVALID_DATA_SHAPE; + } DataType anchor_data_type = GetLegacyDataType(anchor.dtype()); if (anchor_data_type != kCV_32S) { @@ -241,6 +512,10 @@ ErrorCode EraseVarShape::infer(const nvcv::ImageBatchVarShape &inbatch, const nv LOG_ERROR("Invalid erasing_w Dim " << erasing_dim); return ErrorCode::INVALID_DATA_FORMAT; } + if (auto status = validateParameterLength(erasing, num_erasing_area, "erasing"); status != ErrorCode::SUCCESS) + { + return status; + } DataType imgidx_data_type = GetLegacyDataType(imgIdx.dtype()); if (imgidx_data_type != kCV_32S) @@ -254,6 +529,10 @@ ErrorCode EraseVarShape::infer(const nvcv::ImageBatchVarShape &inbatch, const nv LOG_ERROR("Invalid imgIdx Dim " << imgidx_dim); return ErrorCode::INVALID_DATA_FORMAT; } + if (auto status = validateParameterLength(imgIdx, num_erasing_area, "imgIdx"); status != ErrorCode::SUCCESS) + { + return status; + } DataType values_data_type = GetLegacyDataType(values.dtype()); if (values_data_type != kCV_32F) @@ -267,20 +546,43 @@ ErrorCode EraseVarShape::infer(const nvcv::ImageBatchVarShape &inbatch, const nv LOG_ERROR("Invalid values Dim " << values_dim); return ErrorCode::INVALID_DATA_FORMAT; } + if (auto status = validateParameterLength(values, num_erasing_area * channels, "values"); + status != ErrorCode::SUCCESS) + { + return status; + } + if (num_erasing_area > 0) + { + if (auto status = validateEraseAreaData(anchor, imgIdx, num_erasing_area, inbatch.numImages(), stream); + status != ErrorCode::SUCCESS) + { + return status; + } + } if (!inplace) { - for (auto init = inbatch.begin(), outit = outbatch.begin(); init != inbatch.end(), outit != outbatch.end(); - ++init, ++outit) + if (!m_useBulkCopy || !canUseVarShapeRowCopy(inbatch, outbatch, isPlanar, channels, elementBytes) + || !copyVarShapeBatch(*inData, *outData, isPlanar, channels, elementBytes, stream)) { - const Image &inimg = *init; - const Image &outimg = *outit; - auto inimgdata = inimg.exportData(); - auto outimgdata = outimg.exportData(); - const ImagePlaneStrided &inplane = inimgdata->plane(0); - const ImagePlaneStrided &outplane = outimgdata->plane(0); - checkCudaErrors(cudaMemcpy2DAsync(outplane.basePtr, outplane.rowStride, inplane.basePtr, inplane.rowStride, - inplane.rowStride, inplane.height, cudaMemcpyDeviceToDevice, stream)); + for (auto init = inbatch.begin(), outit = outbatch.begin(); + init != inbatch.end() && outit != outbatch.end(); ++init, ++outit) + { + const Image &inimg = *init; + const Image &outimg = *outit; + auto inimgdata = inimg.exportData(); + auto outimgdata = outimg.exportData(); + for (int p = 0; p < inimgdata->numPlanes(); ++p) + { + const ImagePlaneStrided &inplane = inimgdata->plane(p); + const ImagePlaneStrided &outplane = outimgdata->plane(p); + const size_t rowBytes + = static_cast(inplane.width) * inimgdata->format().planePixelStrideBytes(p); + checkCudaErrors(cudaMemcpy2DAsync(outplane.basePtr, outplane.rowStride, inplane.basePtr, + inplane.rowStride, rowBytes, inplane.height, + cudaMemcpyDeviceToDevice, stream)); + } + } } } @@ -313,15 +615,18 @@ ErrorCode EraseVarShape::infer(const nvcv::ImageBatchVarShape &inbatch, const nv const TensorDataStridedCuda &values, int max_eh, int max_ew, int num_erasing_area, bool random, unsigned int seed, cudaStream_t stream); - static const erase_t funcs[6] = {eraseCaller, eraseCaller, eraseCaller, - eraseCaller, eraseCaller, eraseCaller}; + static const erase_t funcs[6] + = {eraseCaller, 0, eraseCaller, eraseCaller, eraseCaller, eraseCaller}; + static const erase_t planarFuncs[6] + = {erasePlanarCaller, 0, erasePlanarCaller, erasePlanarCaller, erasePlanarCaller, + erasePlanarCaller}; + + const erase_t func = isPlanar ? planarFuncs[data_type] : funcs[data_type]; if (inplace) - funcs[data_type](*inData, anchor, erasing, imgIdx, values, max_eh, max_ew, num_erasing_area, random, seed, - stream); + func(*inData, anchor, erasing, imgIdx, values, max_eh, max_ew, num_erasing_area, random, seed, stream); else - funcs[data_type](*outData, anchor, erasing, imgIdx, values, max_eh, max_ew, num_erasing_area, random, seed, - stream); + func(*outData, anchor, erasing, imgIdx, values, max_eh, max_ew, num_erasing_area, random, seed, stream); return SUCCESS; } diff --git a/src/cvcuda/priv/legacy/filter.cu b/src/cvcuda/priv/legacy/filter.cu index a90c223df..5cd37a181 100644 --- a/src/cvcuda/priv/legacy/filter.cu +++ b/src/cvcuda/priv/legacy/filter.cu @@ -1,4 +1,4 @@ -/* Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +/* Copyright (c) 2021-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * * SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES * SPDX-License-Identifier: Apache-2.0 @@ -20,6 +20,7 @@ * limitations under the License. */ +#include "../PlanarTensorView.hpp" #include "CvCudaLegacy.h" #include "CvCudaLegacyHelpers.hpp" @@ -28,49 +29,864 @@ #include +#include + using namespace nvcv::legacy::cuda_op; using namespace nvcv::legacy::helpers; -namespace nvcv::legacy::cuda_op { +namespace nvcv::legacy::cuda_op { + +namespace { + +static bool IsPlanar(DataFormat format) +{ + return format == kNCHW || format == kCHW; +} + +} // namespace + +template +__global__ void filter2D(SrcWrapper src, DstWrapper dst, Size2D dstSize, KernelWrapper kernel, Size2D kernelSize, + int2 kernelAnchor) +{ + using T = typename DstWrapper::ValueType; + using work_type = cuda::ConvertBaseTypeTo; + work_type res = cuda::SetAll(0); + + const int x = blockIdx.x * blockDim.x + threadIdx.x; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + + if (x >= dstSize.w || y >= dstSize.h) + return; + + int kInd = 0; + int3 coord{x, y, batch_idx}; + + for (int i = 0; i < kernelSize.h; ++i) + { + coord.y = y - kernelAnchor.y + i; + + for (int j = 0; j < kernelSize.w; ++j) + { + coord.x = x - kernelAnchor.x + j; + + res = res + src[coord] * kernel[kInd++]; + } + } + + *dst.ptr(batch_idx, y, x) = cuda::SaturateCast(res); +} + +template +__global__ void filter2DTiled(SrcWrapper src, DstWrapper dst, Size2D dstSize, KernelWrapper kernel, Size2D kernelSize, + int2 kernelAnchor) +{ + using T = typename DstWrapper::ValueType; + using work_type = cuda::ConvertBaseTypeTo; + + const int x = blockIdx.x * blockDim.x + threadIdx.x; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + const int tileW = blockDim.x + kernelSize.w - 1; + const int tileH = blockDim.y + kernelSize.h - 1; + const int baseX = blockIdx.x * blockDim.x - kernelAnchor.x; + const int baseY = blockIdx.y * blockDim.y - kernelAnchor.y; + const int tileElems = tileW * tileH; + const int kernelElems = kernelSize.w * kernelSize.h; + + extern __shared__ __align__(16) unsigned char tileRaw[]; + T *tile = reinterpret_cast(tileRaw); + const size_t kernelOffset = (tileElems * sizeof(T) + sizeof(float) - 1) & ~(sizeof(float) - 1); + float *kernelTile = reinterpret_cast(tileRaw + kernelOffset); + + const int tid = threadIdx.y * blockDim.x + threadIdx.x; + const int blockThreads = blockDim.x * blockDim.y; + + for (int idx = tid; idx < tileElems; idx += blockThreads) + { + int3 coord{baseX + idx % tileW, baseY + idx / tileW, batch_idx}; + tile[idx] = src[coord]; + } + for (int idx = tid; idx < kernelElems; idx += blockThreads) + { + kernelTile[idx] = kernel[idx]; + } + + __syncthreads(); + + if (x >= dstSize.w || y >= dstSize.h) + return; + + work_type res = cuda::SetAll(0); + int kInd = 0; + + for (int i = 0; i < kernelSize.h; ++i) + { + const int tileY = threadIdx.y + i; + + for (int j = 0; j < kernelSize.w; ++j) + { + const int tileX = threadIdx.x + j; + + res = res + tile[tileY * tileW + tileX] * kernelTile[kInd++]; + } + } + + *dst.ptr(batch_idx, y, x) = cuda::SaturateCast(res); +} + +template +__global__ void filter2DTiledX2(SrcWrapper src, DstWrapper dst, Size2D dstSize, KernelWrapper kernel, Size2D kernelSize, + int2 kernelAnchor) +{ + using T = typename DstWrapper::ValueType; + using work_type = cuda::ConvertBaseTypeTo; + + const int outputW = blockDim.x * 2; + const int x0 = blockIdx.x * outputW + threadIdx.x * 2; + const int x1 = x0 + 1; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + const int tileW = outputW + kernelSize.w - 1; + const int tileH = blockDim.y + kernelSize.h - 1; + const int baseX = blockIdx.x * outputW - kernelAnchor.x; + const int baseY = blockIdx.y * blockDim.y - kernelAnchor.y; + const int tileElems = tileW * tileH; + const int kernelElems = kernelSize.w * kernelSize.h; + + extern __shared__ __align__(16) unsigned char tileRaw[]; + T *tile = reinterpret_cast(tileRaw); + const size_t kernelOffset = (tileElems * sizeof(T) + sizeof(float) - 1) & ~(sizeof(float) - 1); + float *kernelTile = reinterpret_cast(tileRaw + kernelOffset); + + const int tid = threadIdx.y * blockDim.x + threadIdx.x; + const int blockThreads = blockDim.x * blockDim.y; + + for (int idx = tid; idx < tileElems; idx += blockThreads) + { + int3 coord{baseX + idx % tileW, baseY + idx / tileW, batch_idx}; + tile[idx] = src[coord]; + } + for (int idx = tid; idx < kernelElems; idx += blockThreads) + { + kernelTile[idx] = kernel[idx]; + } + + __syncthreads(); + + if (x0 >= dstSize.w || y >= dstSize.h) + return; + + work_type res0 = cuda::SetAll(0); + work_type res1 = cuda::SetAll(0); + int kInd = 0; + + for (int i = 0; i < kernelSize.h; ++i) + { + const int tileY = threadIdx.y + i; + + for (int j = 0; j < kernelSize.w; ++j) + { + const int tileX = threadIdx.x * 2 + j; + const float k = kernelTile[kInd++]; + + res0 = res0 + tile[tileY * tileW + tileX] * k; + res1 = res1 + tile[tileY * tileW + tileX + 1] * k; + } + } + + *dst.ptr(batch_idx, y, x0) = cuda::SaturateCast(res0); + if (x1 < dstSize.w) + { + *dst.ptr(batch_idx, y, x1) = cuda::SaturateCast(res1); + } +} + +template +__global__ void filter2DTiledX4(SrcWrapper src, DstWrapper dst, Size2D dstSize, KernelWrapper kernel, Size2D kernelSize, + int2 kernelAnchor) +{ + using T = typename DstWrapper::ValueType; + using work_type = cuda::ConvertBaseTypeTo; + + const int outputW = blockDim.x * 4; + const int x0 = blockIdx.x * outputW + threadIdx.x * 4; + const int x1 = x0 + 1; + const int x2 = x0 + 2; + const int x3 = x0 + 3; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + const int tileW = outputW + kernelSize.w - 1; + const int tileH = blockDim.y + kernelSize.h - 1; + const int baseX = blockIdx.x * outputW - kernelAnchor.x; + const int baseY = blockIdx.y * blockDim.y - kernelAnchor.y; + const int tileElems = tileW * tileH; + const int kernelElems = kernelSize.w * kernelSize.h; + + extern __shared__ __align__(16) unsigned char tileRaw[]; + T *tile = reinterpret_cast(tileRaw); + const size_t kernelOffset = (tileElems * sizeof(T) + sizeof(float) - 1) & ~(sizeof(float) - 1); + float *kernelTile = reinterpret_cast(tileRaw + kernelOffset); + + const int tid = threadIdx.y * blockDim.x + threadIdx.x; + const int blockThreads = blockDim.x * blockDim.y; + + for (int idx = tid; idx < tileElems; idx += blockThreads) + { + int3 coord{baseX + idx % tileW, baseY + idx / tileW, batch_idx}; + tile[idx] = src[coord]; + } + for (int idx = tid; idx < kernelElems; idx += blockThreads) + { + kernelTile[idx] = kernel[idx]; + } + + __syncthreads(); + + if (x0 >= dstSize.w || y >= dstSize.h) + return; + + work_type res0 = cuda::SetAll(0); + work_type res1 = cuda::SetAll(0); + work_type res2 = cuda::SetAll(0); + work_type res3 = cuda::SetAll(0); + int kInd = 0; + + for (int i = 0; i < kernelSize.h; ++i) + { + const int tileY = threadIdx.y + i; + + for (int j = 0; j < kernelSize.w; ++j) + { + const int tileX = threadIdx.x * 4 + j; + const float k = kernelTile[kInd++]; + + res0 = res0 + tile[tileY * tileW + tileX] * k; + res1 = res1 + tile[tileY * tileW + tileX + 1] * k; + res2 = res2 + tile[tileY * tileW + tileX + 2] * k; + res3 = res3 + tile[tileY * tileW + tileX + 3] * k; + } + } + + *dst.ptr(batch_idx, y, x0) = cuda::SaturateCast(res0); + if (x1 < dstSize.w) + { + *dst.ptr(batch_idx, y, x1) = cuda::SaturateCast(res1); + } + if (x2 < dstSize.w) + { + *dst.ptr(batch_idx, y, x2) = cuda::SaturateCast(res2); + } + if (x3 < dstSize.w) + { + *dst.ptr(batch_idx, y, x3) = cuda::SaturateCast(res3); + } +} + +template +__global__ void filter2DTiledFixed(SrcWrapper src, DstWrapper dst, Size2D dstSize, KernelWrapper kernel, + int2 kernelAnchor) +{ + using T = typename DstWrapper::ValueType; + using work_type = cuda::ConvertBaseTypeTo; + + const int outputW = blockDim.x * Outputs; + const int x0 = blockIdx.x * outputW + threadIdx.x * Outputs; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + const int tileW = outputW + KernelW - 1; + const int tileH = blockDim.y + KernelH - 1; + const int baseX = blockIdx.x * outputW - kernelAnchor.x; + const int baseY = blockIdx.y * blockDim.y - kernelAnchor.y; + const int tileElems = tileW * tileH; + const int kernelElems = KernelW * KernelH; + + extern __shared__ __align__(16) unsigned char tileRaw[]; + T *tile = reinterpret_cast(tileRaw); + const size_t kernelOffset = (tileElems * sizeof(T) + sizeof(float) - 1) & ~(sizeof(float) - 1); + float *kernelTile = reinterpret_cast(tileRaw + kernelOffset); + + const int tid = threadIdx.y * blockDim.x + threadIdx.x; + const int blockThreads = blockDim.x * blockDim.y; + + for (int idx = tid; idx < tileElems; idx += blockThreads) + { + int3 coord{baseX + idx % tileW, baseY + idx / tileW, batch_idx}; + tile[idx] = src[coord]; + } + for (int idx = tid; idx < kernelElems; idx += blockThreads) + { + kernelTile[idx] = kernel[idx]; + } + + __syncthreads(); + + if (x0 >= dstSize.w || y >= dstSize.h) + return; + + work_type res[Outputs]; +#pragma unroll + for (int output = 0; output < Outputs; ++output) + { + res[output] = cuda::SetAll(0); + } + + int kInd = 0; +#pragma unroll + for (int i = 0; i < KernelH; ++i) + { + const int tileY = threadIdx.y + i; + +#pragma unroll + for (int j = 0; j < KernelW; ++j) + { + const int tileX = threadIdx.x * Outputs + j; + const float k = kernelTile[kInd++]; + +#pragma unroll + for (int output = 0; output < Outputs; ++output) + { + res[output] = res[output] + tile[tileY * tileW + tileX + output] * k; + } + } + } + +#pragma unroll + for (int output = 0; output < Outputs; ++output) + { + const int x = x0 + output; + if (x < dstSize.w) + { + *dst.ptr(batch_idx, y, x) = cuda::SaturateCast(res[output]); + } + } +} + +template +__global__ void laplacian2DU8(SrcWrapper src, DstWrapper dst, Size2D dstSize, float scale) +{ + using T = typename DstWrapper::ValueType; + using work_type = cuda::ConvertBaseTypeTo; + + const int x = blockIdx.x * blockDim.x + threadIdx.x; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + + if (x >= dstSize.w || y >= dstSize.h) + return; + + int3 coord{x, y - 1, batch_idx}; + + work_type res; + if constexpr (KSize == 1) + { + const float axisK = scale; + const float centerK = -4.0f * scale; + + res = src[coord] * axisK; + coord.x = x - 1; + coord.y = y; + res = res + src[coord] * axisK; + coord.x = x; + res = res + src[coord] * centerK; + coord.x = x + 1; + res = res + src[coord] * axisK; + coord.x = x; + coord.y = y + 1; + res = res + src[coord] * axisK; + } + else + { + static_assert(KSize == 3); + const float cornerK = 2.0f * scale; + const float centerK = -8.0f * scale; + + coord.x = x - 1; + res = src[coord] * cornerK; + coord.x = x + 1; + res = res + src[coord] * cornerK; + coord.x = x; + coord.y = y; + res = res + src[coord] * centerK; + coord.x = x - 1; + coord.y = y + 1; + res = res + src[coord] * cornerK; + coord.x = x + 1; + res = res + src[coord] * cornerK; + } + + *dst.ptr(batch_idx, y, x) = cuda::SaturateCast(res); +} + +template +__global__ void laplacian2DFloat(SrcWrapper src, DstWrapper dst, Size2D dstSize, float scale) +{ + using T = typename DstWrapper::ValueType; + using work_type = cuda::ConvertBaseTypeTo; + + const int x = blockIdx.x * blockDim.x + threadIdx.x; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + + if (x >= dstSize.w || y >= dstSize.h) + return; + + int3 coord{x, y - 1, batch_idx}; + + if constexpr (KSize == 1) + { + const float axisK = scale; + const float centerK = -4.0f * scale; + + work_type res = LaplacianFloatMulRN(src[coord], axisK); + coord.x = x - 1; + coord.y = y; + res = LaplacianFloatFmaRN(src[coord], axisK, res); + coord.x = x; + res = LaplacianFloatFmaRN(src[coord], centerK, res); + coord.x = x + 1; + res = LaplacianFloatFmaRN(src[coord], axisK, res); + + coord.y = y + 1; + coord.x = x; + res = LaplacianFloatFmaRN(src[coord], axisK, res); + + *dst.ptr(batch_idx, y, x) = cuda::SaturateCast(res); + } + else + { + static_assert(KSize == 3); + const float cornerK = 2.0f * scale; + const float centerK = -8.0f * scale; + + coord.x = x - 1; + work_type res = LaplacianFloatMulRN(src[coord], cornerK); + coord.x = x + 1; + res = LaplacianFloatFmaRN(src[coord], cornerK, res); + + coord.x = x; + coord.y = y; + res = LaplacianFloatFmaRN(src[coord], centerK, res); + + coord.x = x - 1; + coord.y = y + 1; + res = LaplacianFloatFmaRN(src[coord], cornerK, res); + coord.x = x + 1; + res = LaplacianFloatFmaRN(src[coord], cornerK, res); + + *dst.ptr(batch_idx, y, x) = cuda::SaturateCast(res); + } +} + +constexpr int kLaplacianPlanarBlockWidth = 32; + +template +__global__ void laplacian2DPlanarTiled(SrcWrapper src, DstWrapper dst, Size2D dstSize, float scale) +{ + using T = typename DstWrapper::ValueType; + using work_type = cuda::ConvertBaseTypeTo; + + constexpr int kRadius = 1; + + constexpr int outputW = kLaplacianPlanarBlockWidth * NIX; + const int outBaseX = blockIdx.x * outputW; + const int outBaseY = blockIdx.y * blockDim.y; + const int batch_idx = get_batch_idx(); + + if (outBaseX >= dstSize.w || outBaseY >= dstSize.h) + return; + + constexpr int tileW = outputW + 2 * kRadius; + const int tileH = blockDim.y + 2 * kRadius; + const int tileElems = tileW * tileH; + const int tid = threadIdx.y * blockDim.x + threadIdx.x; + + extern __shared__ __align__(16) unsigned char tileRaw[]; + T *tile = reinterpret_cast(tileRaw); + + for (int idx = tid; idx < tileElems; idx += blockDim.x * blockDim.y) + { + int3 coord{outBaseX - kRadius + idx % tileW, outBaseY - kRadius + idx / tileW, batch_idx}; + tile[idx] = src[coord]; + } + + __syncthreads(); + + const int y = outBaseY + threadIdx.y; + if (y >= dstSize.h) + return; + + const int tileY = threadIdx.y + kRadius; + auto computeOutput = [&](int tileX) + { + work_type res; + if constexpr (KSize == 1) + { + const float axisK = scale; + const float centerK = -4.0f * scale; + + if constexpr (std::is_same_v) + { + res = LaplacianFloatMulRN(tile[(tileY - 1) * tileW + tileX], axisK); + res = LaplacianFloatFmaRN(tile[tileY * tileW + tileX - 1], axisK, res); + res = LaplacianFloatFmaRN(tile[tileY * tileW + tileX], centerK, res); + res = LaplacianFloatFmaRN(tile[tileY * tileW + tileX + 1], axisK, res); + res = LaplacianFloatFmaRN(tile[(tileY + 1) * tileW + tileX], axisK, res); + } + else + { + res = tile[(tileY - 1) * tileW + tileX] * axisK; + res = res + tile[tileY * tileW + tileX - 1] * axisK; + res = res + tile[tileY * tileW + tileX] * centerK; + res = res + tile[tileY * tileW + tileX + 1] * axisK; + res = res + tile[(tileY + 1) * tileW + tileX] * axisK; + } + } + else + { + static_assert(KSize == 3); + const float cornerK = 2.0f * scale; + const float centerK = -8.0f * scale; + + if constexpr (std::is_same_v) + { + res = LaplacianFloatMulRN(tile[(tileY - 1) * tileW + tileX - 1], cornerK); + res = LaplacianFloatFmaRN(tile[(tileY - 1) * tileW + tileX + 1], cornerK, res); + res = LaplacianFloatFmaRN(tile[tileY * tileW + tileX], centerK, res); + res = LaplacianFloatFmaRN(tile[(tileY + 1) * tileW + tileX - 1], cornerK, res); + res = LaplacianFloatFmaRN(tile[(tileY + 1) * tileW + tileX + 1], cornerK, res); + } + else + { + res = tile[(tileY - 1) * tileW + tileX - 1] * cornerK; + res = res + tile[(tileY - 1) * tileW + tileX + 1] * cornerK; + res = res + tile[tileY * tileW + tileX] * centerK; + res = res + tile[(tileY + 1) * tileW + tileX - 1] * cornerK; + res = res + tile[(tileY + 1) * tileW + tileX + 1] * cornerK; + } + } + + return cuda::SaturateCast(res); + }; + + if constexpr (sizeof(T) == 1 && NIX == 4) + { + const int x0 = outBaseX + threadIdx.x * NIX; + if (x0 >= dstSize.w) + return; + + const int valid = dstSize.w - x0 < NIX ? dstSize.w - x0 : NIX; + T outputs[NIX]; + +#pragma unroll + for (int i = 0; i < NIX; ++i) + { + if (i < valid) + outputs[i] = computeOutput(threadIdx.x * NIX + i + kRadius); + } + + T *dstPtr = dst.ptr(batch_idx, y, x0); + if (valid == NIX && (reinterpret_cast(dstPtr) & (alignof(uchar4) - 1)) == 0) + { + *reinterpret_cast(dstPtr) = make_uchar4(outputs[0], outputs[1], outputs[2], outputs[3]); + } + else + { +#pragma unroll + for (int i = 0; i < NIX; ++i) + { + if (i < valid) + dstPtr[i] = outputs[i]; + } + } + } + else + { +#pragma unroll + for (int i = 0; i < NIX; ++i) + { + const int x = outBaseX + threadIdx.x + i * blockDim.x; + if (x < dstSize.w) + *dst.ptr(batch_idx, y, x) = computeOutput(threadIdx.x + i * blockDim.x + kRadius); + } + } +} + +template +ErrorCode Filter2DCaller(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, + KernelWrapper kernel, Size2D kernelSize, int2 kernelAnchor, float borderValue, + cudaStream_t stream) +{ + auto outAccess = TensorDataAccessStridedImagePlanar::Create(outData); + NVCV_ASSERT(outAccess); + + auto inAccess = TensorDataAccessStridedImagePlanar::Create(inData); + NVCV_ASSERT(inAccess); + + Size2D dstSize{outAccess->numCols(), outAccess->numRows()}; + + dim3 block(16, 16); + dim3 grid(divUp(dstSize.w, block.x), divUp(dstSize.h, block.y), outAccess->numSamples()); + + auto outMaxStride = outAccess->sampleStride() * outAccess->numSamples(); + auto inMaxStride = inAccess->sampleStride() * inAccess->numSamples(); + if (std::max(outMaxStride, inMaxStride) <= cuda::TypeTraits::max) + { + auto src = cuda::CreateBorderWrapNHW(inData, cuda::SetAll(borderValue)); + auto dst = cuda::CreateTensorWrapNHW(outData); + filter2D<<>>(src, dst, dstSize, kernel, kernelSize, kernelAnchor); + } + else + { + LOG_ERROR("Input or output size exceeds " << cuda::TypeTraits::max << ". Tensor is too large."); + return ErrorCode::INVALID_PARAMETER; + } + + checkKernelErrors(); +#ifdef CUDA_DEBUG_LOG + checkCudaErrors(cudaStreamSynchronize(stream)); + checkCudaErrors(cudaGetLastError()); +#endif + return ErrorCode::SUCCESS; +} + +template +ErrorCode LaplacianFloatCaller(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, float scale, + float borderValue, cudaStream_t stream) +{ + auto outAccess = TensorDataAccessStridedImagePlanar::Create(outData); + NVCV_ASSERT(outAccess); + + auto inAccess = TensorDataAccessStridedImagePlanar::Create(inData); + NVCV_ASSERT(inAccess); + + Size2D dstSize{outAccess->numCols(), outAccess->numRows()}; + + dim3 block(16, 16); + dim3 grid(divUp(dstSize.w, block.x), divUp(dstSize.h, block.y), outAccess->numSamples()); + + auto outMaxStride = outAccess->sampleStride() * outAccess->numSamples(); + auto inMaxStride = inAccess->sampleStride() * inAccess->numSamples(); + if (std::max(outMaxStride, inMaxStride) <= cuda::TypeTraits::max) + { + auto src = cuda::CreateBorderWrapNHW(inData, cuda::SetAll(borderValue)); + auto dst = cuda::CreateTensorWrapNHW(outData); + laplacian2DFloat<<>>(src, dst, dstSize, scale); + } + else + { + LOG_ERROR("Input or output size exceeds " << cuda::TypeTraits::max << ". Tensor is too large."); + return ErrorCode::INVALID_PARAMETER; + } + + checkKernelErrors(); +#ifdef CUDA_DEBUG_LOG + checkCudaErrors(cudaStreamSynchronize(stream)); + checkCudaErrors(cudaGetLastError()); +#endif + return ErrorCode::SUCCESS; +} + +template +ErrorCode LaplacianFloat(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, float scale, + NVCVBorderType borderMode, float borderValue, cudaStream_t stream) +{ + switch (borderMode) + { +#define NVCV_LAPLACIAN_FLOAT_CASE(BORDERTYPE) \ + case BORDERTYPE: \ + return LaplacianFloatCaller(inData, outData, scale, borderValue, stream) + + NVCV_LAPLACIAN_FLOAT_CASE(NVCV_BORDER_CONSTANT); + NVCV_LAPLACIAN_FLOAT_CASE(NVCV_BORDER_REPLICATE); + NVCV_LAPLACIAN_FLOAT_CASE(NVCV_BORDER_REFLECT); + NVCV_LAPLACIAN_FLOAT_CASE(NVCV_BORDER_WRAP); + NVCV_LAPLACIAN_FLOAT_CASE(NVCV_BORDER_REFLECT101); + +#undef NVCV_LAPLACIAN_FLOAT_CASE + default: + return ErrorCode::INVALID_PARAMETER; + } +} + +template +ErrorCode LaplacianFloat(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, int ksize, + float scale, NVCVBorderType borderMode, float borderValue, cudaStream_t stream) +{ + if (ksize == 1) + { + return LaplacianFloat<1, T>(inData, outData, scale, borderMode, borderValue, stream); + } + return LaplacianFloat<3, T>(inData, outData, scale, borderMode, borderValue, stream); +} + +template +ErrorCode LaplacianU8Caller(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, float scale, + float borderValue, cudaStream_t stream) +{ + auto outAccess = TensorDataAccessStridedImagePlanar::Create(outData); + NVCV_ASSERT(outAccess); + + auto inAccess = TensorDataAccessStridedImagePlanar::Create(inData); + NVCV_ASSERT(inAccess); + + Size2D dstSize{outAccess->numCols(), outAccess->numRows()}; + + dim3 block(16, 16); + dim3 grid(divUp(dstSize.w, block.x), divUp(dstSize.h, block.y), outAccess->numSamples()); + + auto outMaxStride = outAccess->sampleStride() * outAccess->numSamples(); + auto inMaxStride = inAccess->sampleStride() * inAccess->numSamples(); + if (std::max(outMaxStride, inMaxStride) <= cuda::TypeTraits::max) + { + auto src = cuda::CreateBorderWrapNHW(inData, cuda::SetAll(borderValue)); + auto dst = cuda::CreateTensorWrapNHW(outData); + laplacian2DU8<<>>(src, dst, dstSize, scale); + } + else + { + LOG_ERROR("Input or output size exceeds " << cuda::TypeTraits::max << ". Tensor is too large."); + return ErrorCode::INVALID_PARAMETER; + } + + checkKernelErrors(); +#ifdef CUDA_DEBUG_LOG + checkCudaErrors(cudaStreamSynchronize(stream)); + checkCudaErrors(cudaGetLastError()); +#endif + return ErrorCode::SUCCESS; +} + +template +ErrorCode LaplacianU8(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, float scale, + NVCVBorderType borderMode, float borderValue, cudaStream_t stream) +{ + switch (borderMode) + { +#define NVCV_LAPLACIAN_U8_CASE(BORDERTYPE) \ + case BORDERTYPE: \ + return LaplacianU8Caller(inData, outData, scale, borderValue, stream) + + NVCV_LAPLACIAN_U8_CASE(NVCV_BORDER_CONSTANT); + NVCV_LAPLACIAN_U8_CASE(NVCV_BORDER_REPLICATE); + NVCV_LAPLACIAN_U8_CASE(NVCV_BORDER_REFLECT); + NVCV_LAPLACIAN_U8_CASE(NVCV_BORDER_WRAP); + NVCV_LAPLACIAN_U8_CASE(NVCV_BORDER_REFLECT101); + +#undef NVCV_LAPLACIAN_U8_CASE + default: + return ErrorCode::INVALID_PARAMETER; + } +} + +template +ErrorCode LaplacianU8(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, int ksize, float scale, + NVCVBorderType borderMode, float borderValue, cudaStream_t stream) +{ + if (ksize == 1) + { + return LaplacianU8<1, T>(inData, outData, scale, borderMode, borderValue, stream); + } + return LaplacianU8<3, T>(inData, outData, scale, borderMode, borderValue, stream); +} + +template +ErrorCode LaplacianPlanarTiledCaller(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, + float scale, float borderValue, cudaStream_t stream) +{ + auto outAccess = TensorDataAccessStridedImagePlanar::Create(outData); + NVCV_ASSERT(outAccess); + + auto inAccess = TensorDataAccessStridedImagePlanar::Create(inData); + NVCV_ASSERT(inAccess); -template -__global__ void filter2D(SrcWrapper src, DstWrapper dst, Size2D dstSize, KernelWrapper kernel, Size2D kernelSize, - int2 kernelAnchor) -{ - using T = typename DstWrapper::ValueType; - using work_type = cuda::ConvertBaseTypeTo; - work_type res = cuda::SetAll(0); + Size2D dstSize{outAccess->numCols(), outAccess->numRows()}; - const int x = blockIdx.x * blockDim.x + threadIdx.x; - const int y = blockIdx.y * blockDim.y + threadIdx.y; - const int batch_idx = get_batch_idx(); + dim3 block(kLaplacianPlanarBlockWidth, 16); + dim3 grid(divUp(dstSize.w, block.x * NIX), divUp(dstSize.h, block.y), outAccess->numSamples()); - if (x >= dstSize.w || y >= dstSize.h) - return; + auto outMaxStride = outAccess->sampleStride() * outAccess->numSamples(); + auto inMaxStride = inAccess->sampleStride() * inAccess->numSamples(); + if (std::max(outMaxStride, inMaxStride) <= cuda::TypeTraits::max) + { + auto src = cuda::CreateBorderWrapNHW(inData, cuda::SetAll(borderValue)); + auto dst = cuda::CreateTensorWrapNHW(outData); - int kInd = 0; - int3 coord{x, y, batch_idx}; + const size_t sharedBytes = static_cast(block.x * NIX + 2) * (block.y + 2) * sizeof(T); + laplacian2DPlanarTiled<<>>(src, dst, dstSize, scale); + } + else + { + LOG_ERROR("Input or output size exceeds " << cuda::TypeTraits::max << ". Tensor is too large."); + return ErrorCode::INVALID_PARAMETER; + } - for (int i = 0; i < kernelSize.h; ++i) + checkKernelErrors(); +#ifdef CUDA_DEBUG_LOG + checkCudaErrors(cudaStreamSynchronize(stream)); + checkCudaErrors(cudaGetLastError()); +#endif + return ErrorCode::SUCCESS; +} + +template +ErrorCode LaplacianPlanarTiled(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, float scale, + NVCVBorderType borderMode, float borderValue, cudaStream_t stream) +{ + switch (borderMode) { - coord.y = y - kernelAnchor.y + i; +#define NVCV_LAPLACIAN_PLANAR_TILED_CASE(BORDERTYPE) \ + case BORDERTYPE: \ + return LaplacianPlanarTiledCaller(inData, outData, scale, borderValue, stream) - for (int j = 0; j < kernelSize.w; ++j) - { - coord.x = x - kernelAnchor.x + j; + NVCV_LAPLACIAN_PLANAR_TILED_CASE(NVCV_BORDER_CONSTANT); + NVCV_LAPLACIAN_PLANAR_TILED_CASE(NVCV_BORDER_REPLICATE); + NVCV_LAPLACIAN_PLANAR_TILED_CASE(NVCV_BORDER_REFLECT); + NVCV_LAPLACIAN_PLANAR_TILED_CASE(NVCV_BORDER_WRAP); + NVCV_LAPLACIAN_PLANAR_TILED_CASE(NVCV_BORDER_REFLECT101); - res = res + src[coord] * kernel[kInd++]; - } +#undef NVCV_LAPLACIAN_PLANAR_TILED_CASE + default: + return ErrorCode::INVALID_PARAMETER; } +} - *dst.ptr(batch_idx, y, x) = cuda::SaturateCast(res); +template +ErrorCode LaplacianPlanarTiled(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, int ksize, + float scale, NVCVBorderType borderMode, float borderValue, cudaStream_t stream) +{ + if (ksize == 1) + { + return LaplacianPlanarTiled(inData, outData, scale, borderMode, borderValue, stream); + } + return LaplacianPlanarTiled(inData, outData, scale, borderMode, borderValue, stream); +} + +constexpr int kGaussianBlockWidth = 16; +constexpr int kGaussianBlockHeight = 16; + +template +constexpr size_t GaussianFixedSharedBytes() +{ + constexpr size_t tileBytes = static_cast(kGaussianBlockWidth * Outputs + KSize - 1) + * (kGaussianBlockHeight + KSize - 1) * sizeof(T); + constexpr size_t kernelOffset = (tileBytes + sizeof(float) - 1) & ~(sizeof(float) - 1); + return kernelOffset + static_cast(KSize) * KSize * sizeof(float); } template -ErrorCode Filter2DCaller(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, - KernelWrapper kernel, Size2D kernelSize, int2 kernelAnchor, float borderValue, - cudaStream_t stream) +ErrorCode GaussianFilter2DTiledCaller(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, + KernelWrapper kernel, Size2D kernelSize, int2 kernelAnchor, float borderValue, + bool enableX4, cudaStream_t stream) { + using BaseT = cuda::BaseType; + + constexpr bool kX4Always = std::is_same_v || std::is_same_v; + // Scalar U16/S16/S32 share this instantiation: packed C1 selects x4, while planar selects x2. + constexpr bool kX4LayoutDependent = cuda::NumElements == 1 && !kX4Always; + constexpr bool kDynamicX4Possible = kX4Always || kX4LayoutDependent || std::is_same_v; + auto outAccess = TensorDataAccessStridedImagePlanar::Create(outData); NVCV_ASSERT(outAccess); @@ -79,8 +895,10 @@ ErrorCode Filter2DCaller(const TensorDataStridedCuda &inData, const TensorDataSt Size2D dstSize{outAccess->numCols(), outAccess->numRows()}; - dim3 block(16, 16); + dim3 block(kGaussianBlockWidth, kGaussianBlockHeight); dim3 grid(divUp(dstSize.w, block.x), divUp(dstSize.h, block.y), outAccess->numSamples()); + dim3 x2Grid(divUp(dstSize.w, block.x * 2), divUp(dstSize.h, block.y), outAccess->numSamples()); + dim3 x4Grid(divUp(dstSize.w, block.x * 4), divUp(dstSize.h, block.y), outAccess->numSamples()); auto outMaxStride = outAccess->sampleStride() * outAccess->numSamples(); auto inMaxStride = inAccess->sampleStride() * inAccess->numSamples(); @@ -88,7 +906,93 @@ ErrorCode Filter2DCaller(const TensorDataStridedCuda &inData, const TensorDataSt { auto src = cuda::CreateBorderWrapNHW(inData, cuda::SetAll(borderValue)); auto dst = cuda::CreateTensorWrapNHW(outData); - filter2D<<>>(src, dst, dstSize, kernel, kernelSize, kernelAnchor); + + const size_t kernelBytes = static_cast(kernelSize.w) * kernelSize.h * sizeof(float); + const size_t tileElems = static_cast(block.x + kernelSize.w - 1) * (block.y + kernelSize.h - 1); + const size_t tileBytes = tileElems * sizeof(T); + const size_t kernelOffset = (tileBytes + sizeof(float) - 1) & ~(sizeof(float) - 1); + const size_t sharedBytes = kernelOffset + kernelBytes; + const size_t x2TileElems = static_cast(block.x * 2 + kernelSize.w - 1) * (block.y + kernelSize.h - 1); + const size_t x2TileBytes = x2TileElems * sizeof(T); + const size_t x2KernelOffset = (x2TileBytes + sizeof(float) - 1) & ~(sizeof(float) - 1); + const size_t x2SharedBytes = x2KernelOffset + kernelBytes; + const size_t x4TileElems = static_cast(block.x * 4 + kernelSize.w - 1) * (block.y + kernelSize.h - 1); + const size_t x4TileBytes = x4TileElems * sizeof(T); + const size_t x4KernelOffset = (x4TileBytes + sizeof(float) - 1) & ~(sizeof(float) - 1); + const size_t x4SharedBytes = x4KernelOffset + kernelBytes; + +#define NVCV_GAUSSIAN_FILTER_TILED_FIXED_CASE(KSIZE) \ + if (kernelSize.w == KSIZE && kernelSize.h == KSIZE) \ + { \ + if constexpr (kX4Always) \ + { \ + static_assert(GaussianFixedSharedBytes<4, KSIZE, T>() <= 48 * 1024); \ + filter2DTiledFixed<4, KSIZE, KSIZE> \ + <<>>(src, dst, dstSize, kernel, kernelAnchor); \ + } \ + else if constexpr (kX4LayoutDependent) \ + { \ + static_assert(GaussianFixedSharedBytes<4, KSIZE, T>() <= 48 * 1024); \ + static_assert(GaussianFixedSharedBytes<2, KSIZE, T>() <= 48 * 1024); \ + if (enableX4) \ + { \ + filter2DTiledFixed<4, KSIZE, KSIZE> \ + <<>>(src, dst, dstSize, kernel, kernelAnchor); \ + } \ + else \ + { \ + filter2DTiledFixed<2, KSIZE, KSIZE> \ + <<>>(src, dst, dstSize, kernel, kernelAnchor); \ + } \ + } \ + else \ + { \ + static_assert(GaussianFixedSharedBytes<2, KSIZE, T>() <= 48 * 1024); \ + filter2DTiledFixed<2, KSIZE, KSIZE> \ + <<>>(src, dst, dstSize, kernel, kernelAnchor); \ + } \ + } \ + else + + NVCV_GAUSSIAN_FILTER_TILED_FIXED_CASE(3) + NVCV_GAUSSIAN_FILTER_TILED_FIXED_CASE(5) + if constexpr (kDynamicX4Possible) + { + if (enableX4 && x4SharedBytes <= 48 * 1024) + { + filter2DTiledX4<<>>(src, dst, dstSize, kernel, kernelSize, + kernelAnchor); + } + else if (x2SharedBytes <= 48 * 1024) + { + filter2DTiledX2<<>>(src, dst, dstSize, kernel, kernelSize, + kernelAnchor); + } + else if (sharedBytes <= 48 * 1024) + { + filter2DTiled<<>>(src, dst, dstSize, kernel, kernelSize, + kernelAnchor); + } + else + { + filter2D<<>>(src, dst, dstSize, kernel, kernelSize, kernelAnchor); + } + } + else if (x2SharedBytes <= 48 * 1024) + { + filter2DTiledX2<<>>(src, dst, dstSize, kernel, kernelSize, + kernelAnchor); + } + else if (sharedBytes <= 48 * 1024) + { + filter2DTiled<<>>(src, dst, dstSize, kernel, kernelSize, kernelAnchor); + } + else + { + filter2D<<>>(src, dst, dstSize, kernel, kernelSize, kernelAnchor); + } + +#undef NVCV_GAUSSIAN_FILTER_TILED_FIXED_CASE } else { @@ -104,6 +1008,31 @@ ErrorCode Filter2DCaller(const TensorDataStridedCuda &inData, const TensorDataSt return ErrorCode::SUCCESS; } +template +ErrorCode GaussianFilter2DTiled(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, + KernelWrapper kernel, Size2D kernelSize, int2 kernelAnchor, NVCVBorderType borderMode, + float borderValue, bool enableX4, cudaStream_t stream) +{ + switch (borderMode) + { +#define NVCV_GAUSSIAN_FILTER_TILED_CASE(BORDERTYPE) \ + case BORDERTYPE: \ + return GaussianFilter2DTiledCaller(inData, outData, kernel, kernelSize, kernelAnchor, \ + borderValue, enableX4, stream) + + NVCV_GAUSSIAN_FILTER_TILED_CASE(NVCV_BORDER_CONSTANT); + NVCV_GAUSSIAN_FILTER_TILED_CASE(NVCV_BORDER_REPLICATE); + NVCV_GAUSSIAN_FILTER_TILED_CASE(NVCV_BORDER_REFLECT); + NVCV_GAUSSIAN_FILTER_TILED_CASE(NVCV_BORDER_WRAP); + NVCV_GAUSSIAN_FILTER_TILED_CASE(NVCV_BORDER_REFLECT101); + +#undef NVCV_GAUSSIAN_FILTER_TILED_CASE + default: + break; + } + return ErrorCode::SUCCESS; +} + template ErrorCode Filter2D(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, KernelWrapper kernel, Size2D kernelSize, int2 kernelAnchor, NVCVBorderType borderMode, float borderValue, @@ -132,6 +1061,9 @@ ErrorCode Filter2D(const TensorDataStridedCuda &inData, const TensorDataStridedC // @brief Laplacian 3x3 kernels for ksize == 1 and ksize == 3 +constexpr int kLaplacianPlanarU8NIX = 4; +constexpr int kLaplacianPlanarFloatNIX = 4; + // clang-format off constexpr Size2D kLaplacianKernelSize{3, 3}; @@ -174,12 +1106,15 @@ ErrorCode Laplacian::infer(const TensorDataStridedCuda &inData, const TensorData DataFormat format = input_format; - if (!(format == kNHWC || format == kHWC)) + if (!(format == kNHWC || format == kHWC || format == kNCHW || format == kCHW)) { - LOG_ERROR("Invalid input DataFormat " << format << ", the valid DataFormats are: \"NHWC\", \"HWC\""); + LOG_ERROR("Invalid input DataFormat " << format + << ", the valid DataFormats are: \"NHWC\", \"HWC\", \"NCHW\", \"CHW\""); return ErrorCode::INVALID_DATA_FORMAT; } + const bool isPlanar = IsPlanar(format); + if (!(borderMode == NVCV_BORDER_REFLECT101 || borderMode == NVCV_BORDER_REPLICATE || borderMode == NVCV_BORDER_CONSTANT || borderMode == NVCV_BORDER_REFLECT || borderMode == NVCV_BORDER_WRAP)) { @@ -201,6 +1136,12 @@ ErrorCode Laplacian::infer(const TensorDataStridedCuda &inData, const TensorData const int channels = input_shape.C; + if (channels > 4 || channels == 2) + { + LOG_ERROR("Invalid channel number " << channels); + return ErrorCode::INVALID_DATA_SHAPE; + } + int2 kernelAnchor{-1, -1}; normalizeAnchor(kernelAnchor, kLaplacianKernelSize); float borderValue = .0f; @@ -208,16 +1149,26 @@ ErrorCode Laplacian::infer(const TensorDataStridedCuda &inData, const TensorData typedef ErrorCode (*filter2D_t)(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, cuda::math::Vector kernel, Size2D kernelSize, int2 kernelAnchor, NVCVBorderType borderMode, float borderValue, cudaStream_t stream); + typedef ErrorCode (*laplacian_u8_t)(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, + int ksize, float scale, NVCVBorderType borderMode, float borderValue, + cudaStream_t stream); + typedef ErrorCode (*laplacian_float_t)(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, + int ksize, float scale, NVCVBorderType borderMode, float borderValue, + cudaStream_t stream); static const filter2D_t funcs[6][4] = { - { Filter2D, 0, Filter2D, Filter2D}, + { 0, 0, 0, 0}, { 0, 0, 0, 0}, {Filter2D, 0, Filter2D, Filter2D}, { 0, 0, 0, 0}, { 0, 0, 0, 0}, - { Filter2D, 0, Filter2D, Filter2D}, + { 0, 0, 0, 0}, }; + static const laplacian_u8_t u8Funcs[4] = {LaplacianU8, 0, LaplacianU8, LaplacianU8}; + static const laplacian_float_t floatFuncs[4] + = {LaplacianFloat, 0, LaplacianFloat, LaplacianFloat}; + cuda::math::Vector kernel; if (ksize == 1) @@ -234,6 +1185,59 @@ ErrorCode Laplacian::infer(const TensorDataStridedCuda &inData, const TensorData kernel *= scale; } + if (isPlanar) + { + auto outAccess = TensorDataAccessStridedImagePlanar::Create(outData); + NVCV_ASSERT(outAccess); + + const int64_t numSamples = inAccess->numSamples(); + if (outAccess->numSamples() != numSamples || outAccess->numChannels() != channels) + { + LOG_ERROR("Planar Laplacian input and output must have matching sample and channel counts"); + return ErrorCode::INVALID_DATA_SHAPE; + } + if (numSamples > 1 + && (inAccess->sampleStride() != static_cast(channels) * inAccess->chStride() + || outAccess->sampleStride() != static_cast(channels) * outAccess->chStride())) + { + LOG_ERROR("Planar Laplacian of a batched tensor requires tightly packed channel planes"); + return ErrorCode::INVALID_DATA_SHAPE; + } + if (numSamples * channels > 65535) + { + LOG_ERROR("Planar Laplacian requires numSamples * numChannels <= 65535 (CUDA grid-z limit)"); + return ErrorCode::INVALID_DATA_SHAPE; + } + + auto inView = cvcuda::priv::PlanarAsSingleChannelView(inData, *inAccess); + auto outView = cvcuda::priv::PlanarAsSingleChannelView(outData, *outAccess); + if (data_type == kCV_8U) + { + return LaplacianPlanarTiled(inView, outView, ksize, scale, borderMode, + borderValue, stream); + } + if (data_type == kCV_32F) + { + return LaplacianPlanarTiled(inView, outView, ksize, scale, borderMode, + borderValue, stream); + } + return funcs[data_type][0](inView, outView, kernel, kLaplacianKernelSize, kernelAnchor, borderMode, borderValue, + stream); + } + + if (data_type == kCV_8U) + { + const laplacian_u8_t u8Func = u8Funcs[channels - 1]; + NVCV_ASSERT(u8Func != 0); + return u8Func(inData, outData, ksize, scale, borderMode, borderValue, stream); + } + if (data_type == kCV_32F) + { + const laplacian_float_t floatFunc = floatFuncs[channels - 1]; + NVCV_ASSERT(floatFunc != 0); + return floatFunc(inData, outData, ksize, scale, borderMode, borderValue, stream); + } + return funcs[data_type][channels - 1](inData, outData, kernel, kLaplacianKernelSize, kernelAnchor, borderMode, borderValue, stream); } @@ -272,12 +1276,15 @@ ErrorCode Gaussian::infer(const TensorDataStridedCuda &inData, const TensorDataS DataFormat format = input_format; - if (!(format == kNHWC || format == kHWC)) + if (!(format == kNHWC || format == kHWC || format == kNCHW || format == kCHW)) { - LOG_ERROR("Invalid input DataFormat " << format << ", the valid DataFormats are: \"NHWC\", \"HWC\""); + LOG_ERROR("Invalid input DataFormat " << format + << ", the valid DataFormats are: \"NHWC\", \"HWC\", \"NCHW\", \"CHW\""); return ErrorCode::INVALID_DATA_FORMAT; } + const bool isPlanar = IsPlanar(format); + if (!(borderMode == NVCV_BORDER_REFLECT101 || borderMode == NVCV_BORDER_REPLICATE || borderMode == NVCV_BORDER_CONSTANT || borderMode == NVCV_BORDER_REFLECT || borderMode == NVCV_BORDER_WRAP)) { @@ -332,25 +1339,59 @@ ErrorCode Gaussian::infer(const TensorDataStridedCuda &inData, const TensorDataS const int channels = input_shape.C; + if (channels > 4 || channels == 2) + { + LOG_ERROR("Invalid channel number " << channels); + return ErrorCode::INVALID_DATA_SHAPE; + } + int2 kernelAnchor{-1, -1}; normalizeAnchor(kernelAnchor, kernelSize); float borderValue = .0f; typedef ErrorCode (*filter2D_t)(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, float *kernel, Size2D kernelSize, int2 kernelAnchor, NVCVBorderType borderMode, - float borderValue, cudaStream_t stream); + float borderValue, bool enableX4, cudaStream_t stream); static const filter2D_t funcs[6][4] = { - { Filter2D, 0, Filter2D, Filter2D}, - { 0, 0, 0, 0}, - {Filter2D, 0, Filter2D, Filter2D}, - { Filter2D, 0, Filter2D, Filter2D}, - { Filter2D, 0, Filter2D, Filter2D}, - { Filter2D, 0, Filter2D, Filter2D}, + { GaussianFilter2DTiled, 0, GaussianFilter2DTiled, GaussianFilter2DTiled}, + { 0, 0, 0, 0}, + {GaussianFilter2DTiled, 0, GaussianFilter2DTiled, GaussianFilter2DTiled}, + { GaussianFilter2DTiled, 0, GaussianFilter2DTiled, GaussianFilter2DTiled}, + { GaussianFilter2DTiled, 0, GaussianFilter2DTiled, GaussianFilter2DTiled}, + { GaussianFilter2DTiled, 0, GaussianFilter2DTiled, GaussianFilter2DTiled}, }; + if (isPlanar) + { + auto outAccess = TensorDataAccessStridedImagePlanar::Create(outData); + NVCV_ASSERT(outAccess); + + const int64_t numSamples = inAccess->numSamples(); + if (numSamples > 1 + && (inAccess->sampleStride() != static_cast(channels) * inAccess->chStride() + || outAccess->sampleStride() != static_cast(channels) * outAccess->chStride())) + { + LOG_ERROR("Planar Gaussian of a batched tensor requires tightly packed channel planes"); + return ErrorCode::INVALID_DATA_SHAPE; + } + if (numSamples * channels > 65535) + { + LOG_ERROR("Planar Gaussian requires numSamples * numChannels <= 65535 (CUDA grid-z limit)"); + return ErrorCode::INVALID_DATA_SHAPE; + } + + auto inView = cvcuda::priv::PlanarAsSingleChannelView(inData, *inAccess); + auto outView = cvcuda::priv::PlanarAsSingleChannelView(outData, *outAccess); + const bool enableX4 = data_type == kCV_8U || data_type == kCV_32F; + return funcs[data_type][0](inView, outView, m_kernel, kernelSize, kernelAnchor, borderMode, borderValue, + enableX4, stream); + } + + const bool enableX4 = data_type == kCV_8U || channels == 1 + || (data_type == kCV_32F && channels == 3 && kernelSize.w > 5 && kernelSize.h > 5); return funcs[data_type][channels - 1](inData, outData, m_kernel, kernelSize, kernelAnchor, borderMode, borderValue, - stream); + enableX4, stream); } // Average Blur ---------------------------------------------------------------- @@ -359,12 +1400,190 @@ AverageBlur::AverageBlur(DataShape max_input_shape, DataShape max_output_shape, : CudaBaseOp(max_input_shape, max_output_shape) , m_maxKernelSize(maxKernelSize) { - NVCV_CHECK_THROW(cudaMalloc(&m_kernel, maxKernelSize.w * maxKernelSize.h * sizeof(float))); } -AverageBlur::~AverageBlur() +AverageBlur::~AverageBlur() {} + +template +__global__ void averageBlur2D5x5(BorderWrapper borderSrc, SrcWrapper src, DstWrapper dst, Size2D dstSize) { - NVCV_CHECK_LOG(cudaFree(m_kernel)); + using T = typename DstWrapper::ValueType; + using work_type = cuda::ConvertBaseTypeTo; + work_type res = cuda::SetAll(0); + + const int x = blockIdx.x * blockDim.x + threadIdx.x; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + + if (x >= dstSize.w || y >= dstSize.h) + return; + + constexpr float kernelValue = static_cast(1.0 / 25); + int3 coord{x, y, batch_idx}; + + if (x >= 2 && x < dstSize.w - 2 && y >= 2 && y < dstSize.h - 2) + { +#pragma unroll + for (int i = 0; i < 5; ++i) + { + coord.y = y - 2 + i; + +#pragma unroll + for (int j = 0; j < 5; ++j) + { + coord.x = x - 2 + j; + + res = res + src[coord] * kernelValue; + } + } + + *dst.ptr(batch_idx, y, x) = cuda::SaturateCast(res); + return; + } + +#pragma unroll + for (int i = 0; i < 5; ++i) + { + coord.y = y - 2 + i; + +#pragma unroll + for (int j = 0; j < 5; ++j) + { + coord.x = x - 2 + j; + + res = res + borderSrc[coord] * kernelValue; + } + } + + *dst.ptr(batch_idx, y, x) = cuda::SaturateCast(res); +} + +template +__global__ void averageBlur2D(BorderWrapper borderSrc, SrcWrapper src, DstWrapper dst, Size2D dstSize, + Size2D kernelSize, int2 kernelAnchor) +{ + using T = typename DstWrapper::ValueType; + using work_type = cuda::ConvertBaseTypeTo; + work_type res = cuda::SetAll(0); + + const int x = blockIdx.x * blockDim.x + threadIdx.x; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + + if (x >= dstSize.w || y >= dstSize.h) + return; + + const float kernelValue = static_cast(1.0 / (kernelSize.w * kernelSize.h)); + int3 coord{x, y, batch_idx}; + + const bool isInterior = x >= kernelAnchor.x && x < dstSize.w - (kernelSize.w - kernelAnchor.x - 1) + && y >= kernelAnchor.y && y < dstSize.h - (kernelSize.h - kernelAnchor.y - 1); + + if (isInterior) + { + for (int i = 0; i < kernelSize.h; ++i) + { + coord.y = y - kernelAnchor.y + i; + + for (int j = 0; j < kernelSize.w; ++j) + { + coord.x = x - kernelAnchor.x + j; + + res = res + src[coord] * kernelValue; + } + } + } + else + { + for (int i = 0; i < kernelSize.h; ++i) + { + coord.y = y - kernelAnchor.y + i; + + for (int j = 0; j < kernelSize.w; ++j) + { + coord.x = x - kernelAnchor.x + j; + + res = res + borderSrc[coord] * kernelValue; + } + } + } + + *dst.ptr(batch_idx, y, x) = cuda::SaturateCast(res); +} + +template +ErrorCode AverageBlur2DCaller(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, + Size2D kernelSize, int2 kernelAnchor, float borderValue, cudaStream_t stream) +{ + auto outAccess = TensorDataAccessStridedImagePlanar::Create(outData); + NVCV_ASSERT(outAccess); + + auto inAccess = TensorDataAccessStridedImagePlanar::Create(inData); + NVCV_ASSERT(inAccess); + + Size2D dstSize{outAccess->numCols(), outAccess->numRows()}; + + dim3 block(32, 8); + dim3 grid(divUp(dstSize.w, block.x), divUp(dstSize.h, block.y), outAccess->numSamples()); + + auto outMaxStride = outAccess->sampleStride() * outAccess->numSamples(); + auto inMaxStride = inAccess->sampleStride() * inAccess->numSamples(); + if (std::max(outMaxStride, inMaxStride) <= cuda::TypeTraits::max) + { + auto src = cuda::CreateBorderWrapNHW(inData, cuda::SetAll(borderValue)); + auto srcRaw = cuda::CreateTensorWrapNHW(inData); + auto dst = cuda::CreateTensorWrapNHW(outData); + if constexpr (cuda::NumElements == 1) + { + if (kernelSize.w == 5 && kernelSize.h == 5 && kernelAnchor.x == 2 && kernelAnchor.y == 2) + { + averageBlur2D5x5<<>>(src, srcRaw, dst, dstSize); + } + else + { + averageBlur2D<<>>(src, srcRaw, dst, dstSize, kernelSize, kernelAnchor); + } + } + else + { + averageBlur2D<<>>(src, srcRaw, dst, dstSize, kernelSize, kernelAnchor); + } + } + else + { + LOG_ERROR("Input or output size exceeds " << cuda::TypeTraits::max << ". Tensor is too large."); + return ErrorCode::INVALID_PARAMETER; + } + + checkKernelErrors(); +#ifdef CUDA_DEBUG_LOG + checkCudaErrors(cudaStreamSynchronize(stream)); + checkCudaErrors(cudaGetLastError()); +#endif + return ErrorCode::SUCCESS; +} + +template +ErrorCode AverageBlur2D(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, Size2D kernelSize, + int2 kernelAnchor, NVCVBorderType borderMode, float borderValue, cudaStream_t stream) +{ + switch (borderMode) + { +#define NVCV_AVG_BLUR_CASE(BORDERTYPE) \ + case BORDERTYPE: \ + return AverageBlur2DCaller(inData, outData, kernelSize, kernelAnchor, borderValue, stream) + + NVCV_AVG_BLUR_CASE(NVCV_BORDER_CONSTANT); + NVCV_AVG_BLUR_CASE(NVCV_BORDER_REPLICATE); + NVCV_AVG_BLUR_CASE(NVCV_BORDER_REFLECT); + NVCV_AVG_BLUR_CASE(NVCV_BORDER_WRAP); + NVCV_AVG_BLUR_CASE(NVCV_BORDER_REFLECT101); + +#undef NVCV_AVG_BLUR_CASE + default: + break; + } + return ErrorCode::SUCCESS; } ErrorCode AverageBlur::infer(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, @@ -387,12 +1606,15 @@ ErrorCode AverageBlur::infer(const TensorDataStridedCuda &inData, const TensorDa DataFormat format = input_format; - if (!(format == kNHWC || format == kHWC)) + if (!(format == kNHWC || format == kHWC || format == kNCHW || format == kCHW)) { - LOG_ERROR("Invalid input DataFormat " << format << ", the valid DataFormats are: \"NHWC\", \"HWC\""); + LOG_ERROR("Invalid input DataFormat " << format + << ", the valid DataFormats are: \"NHWC\", \"HWC\", \"NCHW\", \"CHW\""); return ErrorCode::INVALID_DATA_FORMAT; } + const bool isPlanar = IsPlanar(format); + if (!(borderMode == NVCV_BORDER_REFLECT101 || borderMode == NVCV_BORDER_REPLICATE || borderMode == NVCV_BORDER_CONSTANT || borderMode == NVCV_BORDER_REFLECT || borderMode == NVCV_BORDER_WRAP)) { @@ -434,35 +1656,58 @@ ErrorCode AverageBlur::infer(const TensorDataStridedCuda &inData, const TensorDa const int channels = input_shape.C; + if (channels > 4 || channels == 2) + { + LOG_ERROR("Invalid channel number " << channels); + return ErrorCode::INVALID_DATA_SHAPE; + } + normalizeAnchor(kernelAnchor, kernelSize); float borderValue = .0f; typedef ErrorCode (*filter2D_t)(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, - float *kernel, Size2D kernelSize, int2 kernelAnchor, NVCVBorderType borderMode, - float borderValue, cudaStream_t stream); + Size2D kernelSize, int2 kernelAnchor, NVCVBorderType borderMode, float borderValue, + cudaStream_t stream); static const filter2D_t funcs[6][4] = { - { Filter2D, 0, Filter2D, Filter2D}, - { 0, 0, 0, 0}, - {Filter2D, 0, Filter2D, Filter2D}, - { Filter2D, 0, Filter2D, Filter2D}, - { Filter2D, 0, Filter2D, Filter2D}, - { Filter2D, 0, Filter2D, Filter2D}, + { AverageBlur2D, 0, AverageBlur2D, AverageBlur2D}, + { 0, 0, 0, 0}, + {AverageBlur2D, 0, AverageBlur2D, AverageBlur2D}, + { AverageBlur2D, 0, AverageBlur2D, AverageBlur2D}, + { AverageBlur2D, 0, AverageBlur2D, AverageBlur2D}, + { AverageBlur2D, 0, AverageBlur2D, AverageBlur2D}, }; - if (m_curKernelSize != kernelSize) + if (isPlanar) { - int k_size = kernelSize.h * kernelSize.w; + auto outAccess = TensorDataAccessStridedImagePlanar::Create(outData); + NVCV_ASSERT(outAccess); - computeMeanKernel<<<1, k_size, 0, stream>>>(m_kernel, k_size); - - checkKernelErrors(); + const int64_t numSamples = inAccess->numSamples(); + if (outAccess->numSamples() != numSamples || outAccess->numChannels() != channels) + { + LOG_ERROR("Planar AverageBlur input and output must have matching sample and channel counts"); + return ErrorCode::INVALID_DATA_SHAPE; + } + if (numSamples > 1 + && (inAccess->sampleStride() != static_cast(channels) * inAccess->chStride() + || outAccess->sampleStride() != static_cast(channels) * outAccess->chStride())) + { + LOG_ERROR("Planar AverageBlur of a batched tensor requires tightly packed channel planes"); + return ErrorCode::INVALID_DATA_SHAPE; + } + if (numSamples * channels > 65535) + { + LOG_ERROR("Planar AverageBlur requires numSamples * numChannels <= 65535 (CUDA grid-z limit)"); + return ErrorCode::INVALID_DATA_SHAPE; + } - m_curKernelSize = kernelSize; + auto inView = cvcuda::priv::PlanarAsSingleChannelView(inData, *inAccess); + auto outView = cvcuda::priv::PlanarAsSingleChannelView(outData, *outAccess); + return funcs[data_type][0](inView, outView, kernelSize, kernelAnchor, borderMode, borderValue, stream); } - return funcs[data_type][channels - 1](inData, outData, m_kernel, kernelSize, kernelAnchor, borderMode, borderValue, - stream); + return funcs[data_type][channels - 1](inData, outData, kernelSize, kernelAnchor, borderMode, borderValue, stream); } } // namespace nvcv::legacy::cuda_op diff --git a/src/cvcuda/priv/legacy/filter_utils.cu b/src/cvcuda/priv/legacy/filter_utils.cu index 219f72d27..15c5aa608 100644 --- a/src/cvcuda/priv/legacy/filter_utils.cu +++ b/src/cvcuda/priv/legacy/filter_utils.cu @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/src/cvcuda/priv/legacy/filter_utils.cuh b/src/cvcuda/priv/legacy/filter_utils.cuh index 48776ca89..1a8b560b2 100644 --- a/src/cvcuda/priv/legacy/filter_utils.cuh +++ b/src/cvcuda/priv/legacy/filter_utils.cuh @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,8 +20,38 @@ #include // for TensorWrap, etc. +#include + namespace nvcv::legacy::cuda_op { +// Keep scalar planar and compound interleaved kernels on the same explicitly rounded instruction sequence. +template +__device__ __forceinline__ T LaplacianFloatMulRN(T value, float coefficient) +{ + static_assert(std::is_same_v, float>); + + T result; +#pragma unroll + for (int c = 0; c < cuda::NumElements; ++c) + { + cuda::GetElement(result, c) = __fmul_rn(cuda::GetElement(value, c), coefficient); + } + return result; +} + +template +__device__ __forceinline__ T LaplacianFloatFmaRN(T value, float coefficient, T result) +{ + static_assert(std::is_same_v, float>); + +#pragma unroll + for (int c = 0; c < cuda::NumElements; ++c) + { + cuda::GetElement(result, c) = __fmaf_rn(cuda::GetElement(value, c), coefficient, cuda::GetElement(result, c)); + } + return result; +} + __global__ void computeMeanKernel(float *kernel_ptr, int k_size); __global__ void computeGaussianKernel(float *kernel, Size2D kernelSize, double2 sigma); diff --git a/src/cvcuda/priv/legacy/filter_var_shape.cu b/src/cvcuda/priv/legacy/filter_var_shape.cu index 173d332d2..cb30d0dff 100644 --- a/src/cvcuda/priv/legacy/filter_var_shape.cu +++ b/src/cvcuda/priv/legacy/filter_var_shape.cu @@ -1,4 +1,4 @@ -/* Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +/* Copyright (c) 2021-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * * SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES * SPDX-License-Identifier: Apache-2.0 @@ -21,17 +21,50 @@ */ #include "../Assert.h" +#include "../SafeSize.hpp" #include "CvCudaLegacy.h" #include "CvCudaLegacyHelpers.hpp" #include "CvCudaUtils.cuh" #include "filter_utils.cuh" +#include + using namespace nvcv::legacy::cuda_op; using namespace nvcv::legacy::helpers; namespace nvcv::legacy::cuda_op { +namespace { + +static bool IsPlanar(DataFormat format) +{ + return format == kNCHW || format == kCHW; +} + +__device__ __forceinline__ int2 ResolveGaussianKernelSize(cuda::Tensor1DWrap kernelSizeArr, + cuda::Tensor1DWrap sigmaArr, + Size2D maxKernelSize, int dataKernelSize, int batch_idx) +{ + int2 kernelSize = kernelSizeArr[batch_idx]; + double2 sigma = sigmaArr[batch_idx]; + + // automatic detection of kernel size from sigma + if (kernelSize.x <= 0 && sigma.x > 0) + kernelSize.x = cuda::round(sigma.x * dataKernelSize * 2 + 1) | 1; + if (kernelSize.y <= 0 && sigma.y > 0) + kernelSize.y = cuda::round(sigma.y * dataKernelSize * 2 + 1) | 1; + + NVCV_CUDA_ASSERT(kernelSize.x > 0 && (kernelSize.x % 2 == 1) && kernelSize.x <= maxKernelSize.w, + "E Wrong kernelSize.x = %d, expected > 0, odd and <= %d\n", kernelSize.x, maxKernelSize.w); + NVCV_CUDA_ASSERT(kernelSize.y > 0 && (kernelSize.y % 2 == 1) && kernelSize.y <= maxKernelSize.h, + "E Wrong kernelSize.y = %d, expected > 0, odd and <= %d\n", kernelSize.y, maxKernelSize.h); + + return kernelSize; +} + +} // namespace + template __global__ void filter2D(const SrcWrapper src, DstWrapper dst, cuda::ImageBatchVarShapeWrap kernel, cuda::Tensor1DWrap kernelAnchor) @@ -66,34 +99,849 @@ __global__ void filter2D(const SrcWrapper src, DstWrapper dst, cuda::ImageBatchV { srcCoord.x = x - anchor.x + j; - res = res + src[srcCoord] * (*kernel.ptr(batch_idx, i, j)); - } + res = res + src[srcCoord] * (*kernel.ptr(batch_idx, i, j)); + } + } + + *dst.ptr(batch_idx, y, x) = cuda::SaturateCast(res); +} + +template +__global__ void filter2DTiledFixed(const SrcWrapper src, DstWrapper dst, cuda::ImageBatchVarShapeWrap kernel, + cuda::Tensor1DWrap kernelAnchor) +{ + using T = typename DstWrapper::ValueType; + using work_type = cuda::ConvertBaseTypeTo; + + const int outputW = blockDim.x * Outputs; + const int x0 = blockIdx.x * outputW + threadIdx.x * Outputs; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + + int2 anchor = kernelAnchor[batch_idx]; + + int2 kernelSize{kernel.width(batch_idx), kernel.height(batch_idx)}; + + if (anchor.x < 0) + anchor.x = kernelSize.x / 2; + + if (anchor.y < 0) + anchor.y = kernelSize.y / 2; + + const int outBaseX = blockIdx.x * outputW; + const int outBaseY = blockIdx.y * blockDim.y; + + if (outBaseX >= dst.width(batch_idx) || outBaseY >= dst.height(batch_idx)) + return; + + const int tileW = outputW + KernelW - 1; + const int tileH = blockDim.y + KernelH - 1; + const int baseX = outBaseX - anchor.x; + const int baseY = outBaseY - anchor.y; + const int tileElems = tileW * tileH; + const int kernelElems = kernelSize.x * kernelSize.y; + + extern __shared__ __align__(16) unsigned char tileRaw[]; + T *tile = reinterpret_cast(tileRaw); + const size_t kernelOffset = (tileElems * sizeof(T) + sizeof(float) - 1) & ~(sizeof(float) - 1); + float *kernelTile = reinterpret_cast(tileRaw + kernelOffset); + + const int tid = threadIdx.y * blockDim.x + threadIdx.x; + const int blockThreads = blockDim.x * blockDim.y; + + for (int idx = tid; idx < tileElems; idx += blockThreads) + { + int3 srcCoord{baseX + idx % tileW, baseY + idx / tileW, batch_idx}; + tile[idx] = src[srcCoord]; + } + + for (int idx = tid; idx < kernelElems; idx += blockThreads) + { + kernelTile[idx] = *kernel.ptr(batch_idx, idx / kernelSize.x, idx % kernelSize.x); + } + + __syncthreads(); + + if (x0 >= dst.width(batch_idx) || y >= dst.height(batch_idx)) + return; + + work_type res[Outputs]; +#pragma unroll + for (int output = 0; output < Outputs; ++output) + { + res[output] = cuda::SetAll(0); + } + + int kInd = 0; + if (kernelSize.x == KernelW && kernelSize.y == KernelH) + { +#pragma unroll + for (int i = 0; i < KernelH; ++i) + { + const int tileY = threadIdx.y + i; + +#pragma unroll + for (int j = 0; j < KernelW; ++j) + { + const int tileX = threadIdx.x * Outputs + j; + const float k = kernelTile[kInd++]; + +#pragma unroll + for (int output = 0; output < Outputs; ++output) + { + res[output] = res[output] + tile[tileY * tileW + tileX + output] * k; + } + } + } + } + else + { + for (int i = 0; i < kernelSize.y; ++i) + { + const int tileY = threadIdx.y + i; + + for (int j = 0; j < kernelSize.x; ++j) + { + const int tileX = threadIdx.x * Outputs + j; + const float k = kernelTile[kInd++]; + +#pragma unroll + for (int output = 0; output < Outputs; ++output) + { + res[output] = res[output] + tile[tileY * tileW + tileX + output] * k; + } + } + } + } + +#pragma unroll + for (int output = 0; output < Outputs; ++output) + { + const int x = x0 + output; + if (x < dst.width(batch_idx)) + { + *dst.ptr(batch_idx, y, x) = cuda::SaturateCast(res[output]); + } + } +} + +template +__global__ void filter2DPlanar(const SrcWrapper src, DstWrapper dst, cuda::ImageBatchVarShapeWrap kernel, + cuda::Tensor1DWrap kernelAnchor, int channels) +{ + using T = typename DstWrapper::ValueType; + using work_type = cuda::ConvertBaseTypeTo; + + const int x = blockIdx.x * blockDim.x + threadIdx.x; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + + if (x >= dst.width(batch_idx, 0) || y >= dst.height(batch_idx, 0)) + return; + + int2 anchor = kernelAnchor[batch_idx]; + + int2 kernelSize{kernel.width(batch_idx), kernel.height(batch_idx)}; + + if (anchor.x < 0) + anchor.x = kernelSize.x / 2; + + if (anchor.y < 0) + anchor.y = kernelSize.y / 2; + + for (int plane = 0; plane < channels; ++plane) + { + work_type res = cuda::SetAll(0); + int4 srcCoord = {0, 0, plane, batch_idx}; + + for (int i = 0; i < kernelSize.y; ++i) + { + srcCoord.y = y - anchor.y + i; + + for (int j = 0; j < kernelSize.x; ++j) + { + srcCoord.x = x - anchor.x + j; + + res = res + src[srcCoord] * (*kernel.ptr(batch_idx, i, j)); + } + } + + *dst.ptr(batch_idx, plane, y, x) = cuda::SaturateCast(res); + } +} + +template +__global__ void filter2DPlanarTiledFixed(const SrcWrapper src, DstWrapper dst, + cuda::ImageBatchVarShapeWrap kernel, + cuda::Tensor1DWrap kernelAnchor, int channels) +{ + using T = typename DstWrapper::ValueType; + using work_type = cuda::ConvertBaseTypeTo; + + const int outputW = blockDim.x * Outputs; + const int x0 = blockIdx.x * outputW + threadIdx.x * Outputs; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = blockIdx.z / channels; + const int plane = blockIdx.z - batch_idx * channels; + + int2 anchor = kernelAnchor[batch_idx]; + + int2 kernelSize{kernel.width(batch_idx), kernel.height(batch_idx)}; + + if (anchor.x < 0) + anchor.x = kernelSize.x / 2; + + if (anchor.y < 0) + anchor.y = kernelSize.y / 2; + + const int outBaseX = blockIdx.x * outputW; + const int outBaseY = blockIdx.y * blockDim.y; + + if (outBaseX >= dst.width(batch_idx, plane) || outBaseY >= dst.height(batch_idx, plane)) + return; + + const int tileW = outputW + KernelW - 1; + const int tileH = blockDim.y + KernelH - 1; + const int baseX = outBaseX - anchor.x; + const int baseY = outBaseY - anchor.y; + const int tileElems = tileW * tileH; + const int kernelElems = kernelSize.x * kernelSize.y; + + extern __shared__ __align__(16) unsigned char tileRaw[]; + T *tile = reinterpret_cast(tileRaw); + const size_t kernelOffset = (tileElems * sizeof(T) + sizeof(float) - 1) & ~(sizeof(float) - 1); + float *kernelTile = reinterpret_cast(tileRaw + kernelOffset); + + const int tid = threadIdx.y * blockDim.x + threadIdx.x; + const int blockThreads = blockDim.x * blockDim.y; + + for (int idx = tid; idx < tileElems; idx += blockThreads) + { + int4 srcCoord{baseX + idx % tileW, baseY + idx / tileW, plane, batch_idx}; + tile[idx] = src[srcCoord]; + } + + for (int idx = tid; idx < kernelElems; idx += blockThreads) + { + kernelTile[idx] = *kernel.ptr(batch_idx, idx / kernelSize.x, idx % kernelSize.x); + } + + __syncthreads(); + + if (x0 >= dst.width(batch_idx, plane) || y >= dst.height(batch_idx, plane)) + return; + + work_type res[Outputs]; +#pragma unroll + for (int output = 0; output < Outputs; ++output) + { + res[output] = cuda::SetAll(0); + } + + int kInd = 0; + if (kernelSize.x == KernelW && kernelSize.y == KernelH) + { +#pragma unroll + for (int i = 0; i < KernelH; ++i) + { + const int tileY = threadIdx.y + i; + +#pragma unroll + for (int j = 0; j < KernelW; ++j) + { + const int tileX = threadIdx.x * Outputs + j; + const float k = kernelTile[kInd++]; + +#pragma unroll + for (int output = 0; output < Outputs; ++output) + { + res[output] = res[output] + tile[tileY * tileW + tileX + output] * k; + } + } + } + } + else + { + for (int i = 0; i < kernelSize.y; ++i) + { + const int tileY = threadIdx.y + i; + + for (int j = 0; j < kernelSize.x; ++j) + { + const int tileX = threadIdx.x * Outputs + j; + const float k = kernelTile[kInd++]; + +#pragma unroll + for (int output = 0; output < Outputs; ++output) + { + res[output] = res[output] + tile[tileY * tileW + tileX + output] * k; + } + } + } + } + +#pragma unroll + for (int output = 0; output < Outputs; ++output) + { + const int x = x0 + output; + if (x < dst.width(batch_idx, plane)) + { + *dst.ptr(batch_idx, plane, y, x) = cuda::SaturateCast(res[output]); + } + } +} + +template +__global__ void gaussianFilter2DPlanar(const SrcWrapper src, DstWrapper dst, cuda::Tensor3DWrap kernel, + cuda::Tensor1DWrap kernelSizeArr, Size2D maxKernelSize, + cuda::Tensor1DWrap sigmaArr, int dataKernelSize, int channels) +{ + using T = typename DstWrapper::ValueType; + using work_type = cuda::ConvertBaseTypeTo; + + const int x = blockIdx.x * blockDim.x + threadIdx.x; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + + if (x >= dst.width(batch_idx, 0) || y >= dst.height(batch_idx, 0)) + return; + + int2 kernelSize = kernelSizeArr[batch_idx]; + double2 sigma = sigmaArr[batch_idx]; + + // automatic detection of kernel size from sigma + if (kernelSize.x <= 0 && sigma.x > 0) + kernelSize.x = cuda::round(sigma.x * dataKernelSize * 2 + 1) | 1; + if (kernelSize.y <= 0 && sigma.y > 0) + kernelSize.y = cuda::round(sigma.y * dataKernelSize * 2 + 1) | 1; + + NVCV_CUDA_ASSERT(kernelSize.x > 0 && (kernelSize.x % 2 == 1) && kernelSize.x <= maxKernelSize.w, + "E Wrong kernelSize.x = %d, expected > 0, odd and <= %d\n", kernelSize.x, maxKernelSize.w); + NVCV_CUDA_ASSERT(kernelSize.y > 0 && (kernelSize.y % 2 == 1) && kernelSize.y <= maxKernelSize.h, + "E Wrong kernelSize.y = %d, expected > 0, odd and <= %d\n", kernelSize.y, maxKernelSize.h); + + int2 anchor{kernelSize.x / 2, kernelSize.y / 2}; + + for (int plane = 0; plane < channels; ++plane) + { + work_type res = cuda::SetAll(0); + int4 srcCoord = {0, 0, plane, batch_idx}; + + for (int i = 0; i < kernelSize.y; ++i) + { + srcCoord.y = y - anchor.y + i; + + for (int j = 0; j < kernelSize.x; ++j) + { + srcCoord.x = x - anchor.x + j; + + res = res + src[srcCoord] * (*kernel.ptr(batch_idx, i, j)); + } + } + + *dst.ptr(batch_idx, plane, y, x) = cuda::SaturateCast(res); + } +} + +template +__global__ void gaussianFilter2DPlanarTiled(const SrcWrapper src, DstWrapper dst, + cuda::Tensor3DWrap kernel, + cuda::Tensor1DWrap kernelSizeArr, Size2D maxKernelSize, + cuda::Tensor1DWrap sigmaArr, int dataKernelSize, + int channels) +{ + using T = typename DstWrapper::ValueType; + using work_type = cuda::ConvertBaseTypeTo; + + const int x = blockIdx.x * blockDim.x + threadIdx.x; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = blockIdx.z / channels; + const int plane = blockIdx.z - batch_idx * channels; + + int2 kernelSize = kernelSizeArr[batch_idx]; + double2 sigma = sigmaArr[batch_idx]; + + // automatic detection of kernel size from sigma + if (kernelSize.x <= 0 && sigma.x > 0) + kernelSize.x = cuda::round(sigma.x * dataKernelSize * 2 + 1) | 1; + if (kernelSize.y <= 0 && sigma.y > 0) + kernelSize.y = cuda::round(sigma.y * dataKernelSize * 2 + 1) | 1; + + NVCV_CUDA_ASSERT(kernelSize.x > 0 && (kernelSize.x % 2 == 1) && kernelSize.x <= maxKernelSize.w, + "E Wrong kernelSize.x = %d, expected > 0, odd and <= %d\n", kernelSize.x, maxKernelSize.w); + NVCV_CUDA_ASSERT(kernelSize.y > 0 && (kernelSize.y % 2 == 1) && kernelSize.y <= maxKernelSize.h, + "E Wrong kernelSize.y = %d, expected > 0, odd and <= %d\n", kernelSize.y, maxKernelSize.h); + + const int2 anchor{kernelSize.x / 2, kernelSize.y / 2}; + const int tileW = blockDim.x + kernelSize.x - 1; + const int tileH = blockDim.y + kernelSize.y - 1; + const int baseX = blockIdx.x * blockDim.x - anchor.x; + const int baseY = blockIdx.y * blockDim.y - anchor.y; + const int tileElems = tileW * tileH; + const int kernelElems = kernelSize.x * kernelSize.y; + + extern __shared__ __align__(16) unsigned char tileRaw[]; + T *tile = reinterpret_cast(tileRaw); + const size_t kernelOffset = (tileElems * sizeof(T) + sizeof(float) - 1) & ~(sizeof(float) - 1); + float *kernelTile = reinterpret_cast(tileRaw + kernelOffset); + + const int tid = threadIdx.y * blockDim.x + threadIdx.x; + const int blockThreads = blockDim.x * blockDim.y; + + for (int idx = tid; idx < tileElems; idx += blockThreads) + { + int4 srcCoord{baseX + idx % tileW, baseY + idx / tileW, plane, batch_idx}; + tile[idx] = src[srcCoord]; + } + for (int idx = tid; idx < kernelElems; idx += blockThreads) + { + kernelTile[idx] = *kernel.ptr(batch_idx, idx / kernelSize.x, idx % kernelSize.x); + } + + __syncthreads(); + + if (x >= dst.width(batch_idx, plane) || y >= dst.height(batch_idx, plane)) + return; + + work_type res = cuda::SetAll(0); + int kInd = 0; + + for (int i = 0; i < kernelSize.y; ++i) + { + const int tileY = threadIdx.y + i; + + for (int j = 0; j < kernelSize.x; ++j) + { + const int tileX = threadIdx.x + j; + + res = res + tile[tileY * tileW + tileX] * kernelTile[kInd++]; + } + } + + *dst.ptr(batch_idx, plane, y, x) = cuda::SaturateCast(res); +} + +template +__global__ void gaussianFilter2DPlanarTiledX2(const SrcWrapper src, DstWrapper dst, + cuda::Tensor3DWrap kernel, + cuda::Tensor1DWrap kernelSizeArr, Size2D maxKernelSize, + cuda::Tensor1DWrap sigmaArr, int dataKernelSize, + int channels) +{ + using T = typename DstWrapper::ValueType; + using work_type = cuda::ConvertBaseTypeTo; + + const int outputW = blockDim.x * 2; + const int x0 = blockIdx.x * outputW + threadIdx.x * 2; + const int x1 = x0 + 1; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = blockIdx.z / channels; + const int plane = blockIdx.z - batch_idx * channels; + + int2 kernelSize = kernelSizeArr[batch_idx]; + double2 sigma = sigmaArr[batch_idx]; + + // automatic detection of kernel size from sigma + if (kernelSize.x <= 0 && sigma.x > 0) + kernelSize.x = cuda::round(sigma.x * dataKernelSize * 2 + 1) | 1; + if (kernelSize.y <= 0 && sigma.y > 0) + kernelSize.y = cuda::round(sigma.y * dataKernelSize * 2 + 1) | 1; + + NVCV_CUDA_ASSERT(kernelSize.x > 0 && (kernelSize.x % 2 == 1) && kernelSize.x <= maxKernelSize.w, + "E Wrong kernelSize.x = %d, expected > 0, odd and <= %d\n", kernelSize.x, maxKernelSize.w); + NVCV_CUDA_ASSERT(kernelSize.y > 0 && (kernelSize.y % 2 == 1) && kernelSize.y <= maxKernelSize.h, + "E Wrong kernelSize.y = %d, expected > 0, odd and <= %d\n", kernelSize.y, maxKernelSize.h); + + const int2 anchor{kernelSize.x / 2, kernelSize.y / 2}; + const int tileW = outputW + kernelSize.x - 1; + const int tileH = blockDim.y + kernelSize.y - 1; + const int baseX = blockIdx.x * outputW - anchor.x; + const int baseY = blockIdx.y * blockDim.y - anchor.y; + const int tileElems = tileW * tileH; + const int kernelElems = kernelSize.x * kernelSize.y; + + extern __shared__ __align__(16) unsigned char tileRaw[]; + T *tile = reinterpret_cast(tileRaw); + const size_t kernelOffset = (tileElems * sizeof(T) + sizeof(float) - 1) & ~(sizeof(float) - 1); + float *kernelTile = reinterpret_cast(tileRaw + kernelOffset); + + const int tid = threadIdx.y * blockDim.x + threadIdx.x; + const int blockThreads = blockDim.x * blockDim.y; + + for (int idx = tid; idx < tileElems; idx += blockThreads) + { + int4 srcCoord{baseX + idx % tileW, baseY + idx / tileW, plane, batch_idx}; + tile[idx] = src[srcCoord]; + } + for (int idx = tid; idx < kernelElems; idx += blockThreads) + { + kernelTile[idx] = *kernel.ptr(batch_idx, idx / kernelSize.x, idx % kernelSize.x); + } + + __syncthreads(); + + if (x0 >= dst.width(batch_idx, plane) || y >= dst.height(batch_idx, plane)) + return; + + work_type res0 = cuda::SetAll(0); + work_type res1 = cuda::SetAll(0); + int kInd = 0; + + for (int i = 0; i < kernelSize.y; ++i) + { + const int tileY = threadIdx.y + i; + + for (int j = 0; j < kernelSize.x; ++j) + { + const int tileX = threadIdx.x * 2 + j; + const float k = kernelTile[kInd++]; + + res0 = res0 + tile[tileY * tileW + tileX] * k; + res1 = res1 + tile[tileY * tileW + tileX + 1] * k; + } + } + + *dst.ptr(batch_idx, plane, y, x0) = cuda::SaturateCast(res0); + if (x1 < dst.width(batch_idx, plane)) + { + *dst.ptr(batch_idx, plane, y, x1) = cuda::SaturateCast(res1); + } +} + +template +__global__ void gaussianFilter2DPlanarTiledX4(const SrcWrapper src, DstWrapper dst, + cuda::Tensor3DWrap kernel, + cuda::Tensor1DWrap kernelSizeArr, Size2D maxKernelSize, + cuda::Tensor1DWrap sigmaArr, int dataKernelSize, + int channels) +{ + using T = typename DstWrapper::ValueType; + using work_type = cuda::ConvertBaseTypeTo; + + const int outputW = blockDim.x * 4; + const int x0 = blockIdx.x * outputW + threadIdx.x * 4; + const int x1 = x0 + 1; + const int x2 = x0 + 2; + const int x3 = x0 + 3; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = blockIdx.z / channels; + const int plane = blockIdx.z - batch_idx * channels; + + int2 kernelSize = kernelSizeArr[batch_idx]; + double2 sigma = sigmaArr[batch_idx]; + + // automatic detection of kernel size from sigma + if (kernelSize.x <= 0 && sigma.x > 0) + kernelSize.x = cuda::round(sigma.x * dataKernelSize * 2 + 1) | 1; + if (kernelSize.y <= 0 && sigma.y > 0) + kernelSize.y = cuda::round(sigma.y * dataKernelSize * 2 + 1) | 1; + + NVCV_CUDA_ASSERT(kernelSize.x > 0 && (kernelSize.x % 2 == 1) && kernelSize.x <= maxKernelSize.w, + "E Wrong kernelSize.x = %d, expected > 0, odd and <= %d\n", kernelSize.x, maxKernelSize.w); + NVCV_CUDA_ASSERT(kernelSize.y > 0 && (kernelSize.y % 2 == 1) && kernelSize.y <= maxKernelSize.h, + "E Wrong kernelSize.y = %d, expected > 0, odd and <= %d\n", kernelSize.y, maxKernelSize.h); + + const int2 anchor{kernelSize.x / 2, kernelSize.y / 2}; + const int tileW = outputW + kernelSize.x - 1; + const int tileH = blockDim.y + kernelSize.y - 1; + const int baseX = blockIdx.x * outputW - anchor.x; + const int baseY = blockIdx.y * blockDim.y - anchor.y; + const int tileElems = tileW * tileH; + const int kernelElems = kernelSize.x * kernelSize.y; + + extern __shared__ __align__(16) unsigned char tileRaw[]; + T *tile = reinterpret_cast(tileRaw); + const size_t kernelOffset = (tileElems * sizeof(T) + sizeof(float) - 1) & ~(sizeof(float) - 1); + float *kernelTile = reinterpret_cast(tileRaw + kernelOffset); + + const int tid = threadIdx.y * blockDim.x + threadIdx.x; + const int blockThreads = blockDim.x * blockDim.y; + + for (int idx = tid; idx < tileElems; idx += blockThreads) + { + int4 srcCoord{baseX + idx % tileW, baseY + idx / tileW, plane, batch_idx}; + tile[idx] = src[srcCoord]; + } + for (int idx = tid; idx < kernelElems; idx += blockThreads) + { + kernelTile[idx] = *kernel.ptr(batch_idx, idx / kernelSize.x, idx % kernelSize.x); + } + + __syncthreads(); + + if (x0 >= dst.width(batch_idx, plane) || y >= dst.height(batch_idx, plane)) + return; + + work_type res0 = cuda::SetAll(0); + work_type res1 = cuda::SetAll(0); + work_type res2 = cuda::SetAll(0); + work_type res3 = cuda::SetAll(0); + int kInd = 0; + + for (int i = 0; i < kernelSize.y; ++i) + { + const int tileY = threadIdx.y + i; + + for (int j = 0; j < kernelSize.x; ++j) + { + const int tileX = threadIdx.x * 4 + j; + const float k = kernelTile[kInd++]; + + res0 = res0 + tile[tileY * tileW + tileX] * k; + res1 = res1 + tile[tileY * tileW + tileX + 1] * k; + res2 = res2 + tile[tileY * tileW + tileX + 2] * k; + res3 = res3 + tile[tileY * tileW + tileX + 3] * k; + } + } + + *dst.ptr(batch_idx, plane, y, x0) = cuda::SaturateCast(res0); + if (x1 < dst.width(batch_idx, plane)) + { + *dst.ptr(batch_idx, plane, y, x1) = cuda::SaturateCast(res1); + } + if (x2 < dst.width(batch_idx, plane)) + { + *dst.ptr(batch_idx, plane, y, x2) = cuda::SaturateCast(res2); + } + if (x3 < dst.width(batch_idx, plane)) + { + *dst.ptr(batch_idx, plane, y, x3) = cuda::SaturateCast(res3); + } +} + +template +__global__ void gaussianFilter2DPlanarTiledFixed(const SrcWrapper src, DstWrapper dst, + cuda::Tensor3DWrap kernel, + cuda::Tensor1DWrap kernelSizeArr, Size2D maxKernelSize, + cuda::Tensor1DWrap sigmaArr, int dataKernelSize, + int channels) +{ + using T = typename DstWrapper::ValueType; + using work_type = cuda::ConvertBaseTypeTo; + + const int outputW = blockDim.x * Outputs; + const int x0 = blockIdx.x * outputW + threadIdx.x * Outputs; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = blockIdx.z / channels; + const int plane = blockIdx.z - batch_idx * channels; + + const int2 kernelSize + = ResolveGaussianKernelSize(kernelSizeArr, sigmaArr, maxKernelSize, dataKernelSize, batch_idx); + const int2 anchor{kernelSize.x / 2, kernelSize.y / 2}; + const int tileW = outputW + KernelW - 1; + const int tileH = blockDim.y + KernelH - 1; + const int baseX = blockIdx.x * outputW - anchor.x; + const int baseY = blockIdx.y * blockDim.y - anchor.y; + const int tileElems = tileW * tileH; + const int kernelElems = kernelSize.x * kernelSize.y; + + extern __shared__ __align__(16) unsigned char tileRaw[]; + T *tile = reinterpret_cast(tileRaw); + const size_t kernelOffset = (tileElems * sizeof(T) + sizeof(float) - 1) & ~(sizeof(float) - 1); + float *kernelTile = reinterpret_cast(tileRaw + kernelOffset); + + const int tid = threadIdx.y * blockDim.x + threadIdx.x; + const int blockThreads = blockDim.x * blockDim.y; + + for (int idx = tid; idx < tileElems; idx += blockThreads) + { + int4 srcCoord{baseX + idx % tileW, baseY + idx / tileW, plane, batch_idx}; + tile[idx] = src[srcCoord]; + } + for (int idx = tid; idx < kernelElems; idx += blockThreads) + { + kernelTile[idx] = *kernel.ptr(batch_idx, idx / kernelSize.x, idx % kernelSize.x); + } + + __syncthreads(); + + if (x0 >= dst.width(batch_idx, plane) || y >= dst.height(batch_idx, plane)) + return; + + work_type res[Outputs]; +#pragma unroll + for (int output = 0; output < Outputs; ++output) + { + res[output] = cuda::SetAll(0); + } + + int kInd = 0; + if (kernelSize.x == KernelW && kernelSize.y == KernelH) + { +#pragma unroll + for (int i = 0; i < KernelH; ++i) + { + const int tileY = threadIdx.y + i; + +#pragma unroll + for (int j = 0; j < KernelW; ++j) + { + const int tileX = threadIdx.x * Outputs + j; + const float k = kernelTile[kInd++]; + +#pragma unroll + for (int output = 0; output < Outputs; ++output) + { + res[output] = res[output] + tile[tileY * tileW + tileX + output] * k; + } + } + } + } + else + { + for (int i = 0; i < kernelSize.y; ++i) + { + const int tileY = threadIdx.y + i; + + for (int j = 0; j < kernelSize.x; ++j) + { + const int tileX = threadIdx.x * Outputs + j; + const float k = kernelTile[kInd++]; + +#pragma unroll + for (int output = 0; output < Outputs; ++output) + { + res[output] = res[output] + tile[tileY * tileW + tileX + output] * k; + } + } + } + } + +#pragma unroll + for (int output = 0; output < Outputs; ++output) + { + const int x = x0 + output; + if (x < dst.width(batch_idx, plane)) + { + *dst.ptr(batch_idx, plane, y, x) = cuda::SaturateCast(res[output]); + } + } +} + +template +size_t Filter2DSharedBytes(dim3 block, Size2D maxKernelSize) +{ + const size_t kernelBytes = static_cast(maxKernelSize.w) * maxKernelSize.h * sizeof(float); + const size_t tileElems + = static_cast(block.x * Outputs + maxKernelSize.w - 1) * (block.y + maxKernelSize.h - 1); + const size_t tileBytes = tileElems * sizeof(T); + const size_t kernelOffset = (tileBytes + sizeof(float) - 1) & ~(sizeof(float) - 1); + + return kernelOffset + kernelBytes; +} + +template +void Filter2DCaller(const ImageBatchVarShapeDataStridedCuda &inData, const ImageBatchVarShapeDataStridedCuda &outData, + const ImageBatchVarShapeDataStridedCuda &kernelData, const TensorDataStridedCuda &kernelAnchorData, + float borderValue, cudaStream_t stream) +{ + cuda::BorderVarShapeWrap src(inData, cuda::SetAll(borderValue)); + cuda::ImageBatchVarShapeWrap dst(outData); + cuda::ImageBatchVarShapeWrap kernel(kernelData); + cuda::Tensor1DWrap kernelAnchor(kernelAnchorData); + + // Output count is dtype-gated from full-surface benchmarks: x8 wins for byte scalar/uchar4, + // while float3/float4 need x2 to avoid register pressure from the wider vector work type. + constexpr int kOutputs = std::is_same_v || std::is_same_v + ? 8 + : (std::is_same_v || std::is_same_v ? 2 : 4); + + dim3 block(16, 16); + dim3 grid(divUp(inData.maxSize().w, block.x), divUp(inData.maxSize().h, block.y), outData.numImages()); + dim3 tiledGrid(divUp(inData.maxSize().w, block.x * kOutputs), divUp(inData.maxSize().h, block.y), + outData.numImages()); + +#ifdef CUDA_DEBUG_LOG + checkCudaErrors(cudaStreamSynchronize(stream)); + checkCudaErrors(cudaGetLastError()); +#endif + + const Size2D maxKernelSize = kernelData.maxSize(); + const size_t sharedBytes = Filter2DSharedBytes(block, maxKernelSize); + +#define NVCV_CONV2D_VARSHAPE_FILTER_TILED_FIXED_CASE(KSIZE) \ + if (maxKernelSize.w == KSIZE && maxKernelSize.h == KSIZE) \ + { \ + if (sharedBytes <= 48 * 1024) \ + { \ + filter2DTiledFixed \ + <<>>(src, dst, kernel, kernelAnchor); \ + } \ + else \ + { \ + filter2D<<>>(src, dst, kernel, kernelAnchor); \ + } \ + } \ + else + + NVCV_CONV2D_VARSHAPE_FILTER_TILED_FIXED_CASE(3) + NVCV_CONV2D_VARSHAPE_FILTER_TILED_FIXED_CASE(7) + { + filter2D<<>>(src, dst, kernel, kernelAnchor); } - *dst.ptr(batch_idx, y, x) = cuda::SaturateCast(res); +#undef NVCV_CONV2D_VARSHAPE_FILTER_TILED_FIXED_CASE + checkKernelErrors(); +#ifdef CUDA_DEBUG_LOG + checkCudaErrors(cudaStreamSynchronize(stream)); + checkCudaErrors(cudaGetLastError()); +#endif } template -void Filter2DCaller(const ImageBatchVarShapeDataStridedCuda &inData, const ImageBatchVarShapeDataStridedCuda &outData, - const ImageBatchVarShapeDataStridedCuda &kernelData, const TensorDataStridedCuda &kernelAnchorData, - float borderValue, cudaStream_t stream) +void Filter2DPlanarCaller(const ImageBatchVarShapeDataStridedCuda &inData, + const ImageBatchVarShapeDataStridedCuda &outData, + const ImageBatchVarShapeDataStridedCuda &kernelData, + const TensorDataStridedCuda &kernelAnchorData, int channels, float borderValue, + cudaStream_t stream) { cuda::BorderVarShapeWrap src(inData, cuda::SetAll(borderValue)); cuda::ImageBatchVarShapeWrap dst(outData); cuda::ImageBatchVarShapeWrap kernel(kernelData); cuda::Tensor1DWrap kernelAnchor(kernelAnchorData); - using work_type = cuda::ConvertBaseTypeTo; + constexpr int kOutputs = std::is_same_v ? 8 : 4; - dim3 block(16, 16); - dim3 grid(divUp(inData.maxSize().w, block.x), divUp(inData.maxSize().h, block.y), outData.numImages()); + dim3 block(16, 16); + const int tiledGridX = divUp(outData.maxSize().w, block.x * kOutputs); + const int tiledGridY = divUp(outData.maxSize().h, block.y); + const int64_t tiledGridZ = static_cast(outData.numImages()) * channels; #ifdef CUDA_DEBUG_LOG checkCudaErrors(cudaStreamSynchronize(stream)); checkCudaErrors(cudaGetLastError()); #endif - filter2D<<>>(src, dst, kernel, kernelAnchor); + const Size2D maxKernelSize = kernelData.maxSize(); + const size_t sharedBytes = Filter2DSharedBytes(block, maxKernelSize); + +#define NVCV_CONV2D_VARSHAPE_PLANAR_FILTER_TILED_FIXED_CASE(KSIZE) \ + if (maxKernelSize.w == KSIZE && maxKernelSize.h == KSIZE) \ + { \ + if (sharedBytes <= 48 * 1024 && tiledGridZ <= 65535) \ + { \ + dim3 grid(tiledGridX, tiledGridY, static_cast(tiledGridZ)); \ + filter2DPlanarTiledFixed \ + <<>>(src, dst, kernel, kernelAnchor, channels); \ + } \ + else \ + { \ + dim3 fallbackGrid(divUp(outData.maxSize().w, block.x), divUp(outData.maxSize().h, block.y), \ + outData.numImages()); \ + filter2DPlanar<<>>(src, dst, kernel, kernelAnchor, channels); \ + } \ + } \ + else + + NVCV_CONV2D_VARSHAPE_PLANAR_FILTER_TILED_FIXED_CASE(3) + NVCV_CONV2D_VARSHAPE_PLANAR_FILTER_TILED_FIXED_CASE(7) + { + dim3 fallbackGrid(divUp(outData.maxSize().w, block.x), divUp(outData.maxSize().h, block.y), + outData.numImages()); + filter2DPlanar<<>>(src, dst, kernel, kernelAnchor, channels); + } + +#undef NVCV_CONV2D_VARSHAPE_PLANAR_FILTER_TILED_FIXED_CASE checkKernelErrors(); #ifdef CUDA_DEBUG_LOG checkCudaErrors(cudaStreamSynchronize(stream)); @@ -118,6 +966,24 @@ void Filter2D(const ImageBatchVarShapeDataStridedCuda &inData, const ImageBatchV funcs[borderMode](inData, outData, kernelData, kernelAnchorData, borderValue, stream); } +template +void Filter2DPlanar(const ImageBatchVarShapeDataStridedCuda &inData, const ImageBatchVarShapeDataStridedCuda &outData, + const ImageBatchVarShapeDataStridedCuda &kernelData, const TensorDataStridedCuda &kernelAnchorData, + NVCVBorderType borderMode, float borderValue, int channels, cudaStream_t stream) +{ + typedef void (*func_t)( + const ImageBatchVarShapeDataStridedCuda &inData, const ImageBatchVarShapeDataStridedCuda &outData, + const ImageBatchVarShapeDataStridedCuda &kernelData, const TensorDataStridedCuda &kernelAnchorData, + int channels, float borderValue, cudaStream_t stream); + + static const func_t funcs[] + = {Filter2DPlanarCaller, Filter2DPlanarCaller, + Filter2DPlanarCaller, Filter2DPlanarCaller, + Filter2DPlanarCaller}; + + funcs[borderMode](inData, outData, kernelData, kernelAnchorData, channels, borderValue, stream); +} + // Conv2DVarShape -------------------------------------------------------------- ErrorCode Conv2DVarShape::infer(const ImageBatchVarShapeDataStridedCuda &inData, @@ -147,11 +1013,13 @@ ErrorCode Conv2DVarShape::infer(const ImageBatchVarShapeDataStridedCuda &inData, DataFormat format = input_format; - if (!(format == kNHWC || format == kHWC)) + if (!(format == kNHWC || format == kHWC || format == kNCHW || format == kCHW)) { - LOG_ERROR("Invalid input DataFormat " << format << ", the valid DataFormats are: \"NHWC\", \"HWC\""); + LOG_ERROR("Invalid input DataFormat " << format + << ", the valid DataFormats are: \"NHWC\", \"HWC\", \"NCHW\", \"CHW\""); return ErrorCode::INVALID_DATA_FORMAT; } + const bool isPlanar = IsPlanar(format); if (!(borderMode == NVCV_BORDER_REFLECT101 || borderMode == NVCV_BORDER_REPLICATE || borderMode == NVCV_BORDER_CONSTANT || borderMode == NVCV_BORDER_REFLECT || borderMode == NVCV_BORDER_WRAP)) @@ -171,7 +1039,7 @@ ErrorCode Conv2DVarShape::infer(const ImageBatchVarShapeDataStridedCuda &inData, const int channels = inData.uniqueFormat().numChannels(); - if (channels > 4) + if (channels > 4 || channels == 2) { LOG_ERROR("Invalid channel number " << channels); return ErrorCode::INVALID_DATA_SHAPE; @@ -193,6 +1061,32 @@ ErrorCode Conv2DVarShape::infer(const ImageBatchVarShapeDataStridedCuda &inData, { Filter2D, 0, Filter2D, Filter2D}, }; + if (isPlanar) + { + if (outData.numImages() > 65535) + { + LOG_ERROR("Planar Conv2D requires numImages <= 65535 (CUDA grid-z limit)"); + return ErrorCode::INVALID_DATA_SHAPE; + } + + typedef void (*filter2D_planar_t)( + const ImageBatchVarShapeDataStridedCuda &inData, const ImageBatchVarShapeDataStridedCuda &outData, + const ImageBatchVarShapeDataStridedCuda &kernelData, const TensorDataStridedCuda &kernelAnchorData, + NVCVBorderType borderMode, float borderValue, int channels, cudaStream_t stream); + + static const filter2D_planar_t planarFuncs[6] = { + Filter2DPlanar, 0, Filter2DPlanar, Filter2DPlanar, Filter2DPlanar, + Filter2DPlanar, + }; + + const filter2D_planar_t func = planarFuncs[data_type]; + + NVCV_ASSERT(func != 0); + + func(inData, outData, kernelData, kernelAnchorData, borderMode, borderValue, channels, stream); + return ErrorCode::SUCCESS; + } + const filter2D_t func = funcs[data_type][channels - 1]; NVCV_ASSERT(func != 0); @@ -206,6 +1100,10 @@ ErrorCode Conv2DVarShape::infer(const ImageBatchVarShapeDataStridedCuda &inData, // @brief Laplacian 3x3 kernels for ksize == 1 and ksize == 3 +constexpr int kLaplacianPlanarU8NIX = 4; +constexpr int kLaplacianPlanarFloatNIX = 4; +constexpr int kLaplacianPlanarBlockWidth = 32; + // clang-format off __device__ cuda::math::Vector kLaplacianKernel1{ @@ -263,30 +1161,508 @@ __global__ void laplacianFilter2D(const SrcWrapper src, DstWrapper dst, cuda::Te } } - *dst.ptr(batch_idx, y, x) = cuda::SaturateCast(res); + *dst.ptr(batch_idx, y, x) = cuda::SaturateCast(res); +} + +template +__global__ void laplacianFilter2DPlanar(const SrcWrapper src, DstWrapper dst, cuda::Tensor1DWrap ksize, + cuda::Tensor1DWrap scale, int channels) +{ + using T = typename DstWrapper::ValueType; + using work_type = cuda::ConvertBaseTypeTo; + + const int x = blockIdx.x * blockDim.x + threadIdx.x; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + + if (x >= dst.width(batch_idx, 0) || y >= dst.height(batch_idx, 0)) + return; + + constexpr int2 kernelSize = int2{3, 3}; + constexpr int2 anchor = int2{1, 1}; + + const int ksizeVal = ksize[batch_idx]; + + NVCV_CUDA_ASSERT(ksizeVal == 1 || ksizeVal == 3, "E Wrong ksize = %d, expected: 1 or 3", ksizeVal); + cuda::math::Vector kernel = ksizeVal == 1 ? kLaplacianKernel1 : kLaplacianKernel3; + + kernel *= scale[batch_idx]; + + for (int plane = 0; plane < channels; ++plane) + { + work_type res = cuda::SetAll(0); + int kidx = 0; + int4 srcCoord = {0, 0, plane, batch_idx}; + + for (int i = 0; i < kernelSize.y; ++i) + { + srcCoord.y = y - anchor.y + i; + + for (int j = 0; j < kernelSize.x; ++j) + { + srcCoord.x = x - anchor.x + j; + + res = res + src[srcCoord] * kernel[kidx++]; + } + } + + *dst.ptr(batch_idx, plane, y, x) = cuda::SaturateCast(res); + } +} + +template +__global__ void laplacianFilter2DU8(const SrcWrapper src, DstWrapper dst, cuda::Tensor1DWrap ksize, + cuda::Tensor1DWrap scale) +{ + using T = typename DstWrapper::ValueType; + using work_type = cuda::ConvertBaseTypeTo; + + const int x = blockIdx.x * blockDim.x + threadIdx.x; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + + if (x >= dst.width(batch_idx) || y >= dst.height(batch_idx)) + return; + + const int ksizeVal = ksize[batch_idx]; + const float scaleVal = scale[batch_idx]; + int3 srcCoord{x, y - 1, batch_idx}; + + NVCV_CUDA_ASSERT(ksizeVal == 1 || ksizeVal == 3, "E Wrong ksize = %d, expected: 1 or 3", ksizeVal); + + work_type res; + if (ksizeVal == 1) + { + const float axisK = scaleVal; + const float centerK = -4.0f * scaleVal; + + res = src[srcCoord] * axisK; + srcCoord.x = x - 1; + srcCoord.y = y; + res = res + src[srcCoord] * axisK; + srcCoord.x = x; + res = res + src[srcCoord] * centerK; + srcCoord.x = x + 1; + res = res + src[srcCoord] * axisK; + srcCoord.x = x; + srcCoord.y = y + 1; + res = res + src[srcCoord] * axisK; + } + else + { + const float cornerK = 2.0f * scaleVal; + const float centerK = -8.0f * scaleVal; + + srcCoord.x = x - 1; + res = src[srcCoord] * cornerK; + srcCoord.x = x + 1; + res = res + src[srcCoord] * cornerK; + srcCoord.x = x; + srcCoord.y = y; + res = res + src[srcCoord] * centerK; + srcCoord.x = x - 1; + srcCoord.y = y + 1; + res = res + src[srcCoord] * cornerK; + srcCoord.x = x + 1; + res = res + src[srcCoord] * cornerK; + } + + *dst.ptr(batch_idx, y, x) = cuda::SaturateCast(res); +} + +template +__global__ void laplacianFilter2DPlanarTiled(const SrcWrapper src, DstWrapper dst, + cuda::Tensor1DWrap ksize, + cuda::Tensor1DWrap scale, int channels) +{ + using T = typename DstWrapper::ValueType; + using work_type = cuda::ConvertBaseTypeTo; + + constexpr int kRadius = 1; + + constexpr int outputW = kLaplacianPlanarBlockWidth * NIX; + const int outBaseX = blockIdx.x * outputW; + const int outBaseY = blockIdx.y * blockDim.y; + const int batch_idx = get_batch_idx(); + const int dstWidth = dst.width(batch_idx, 0); + const int dstHeight = dst.height(batch_idx, 0); + + if (outBaseX >= dstWidth || outBaseY >= dstHeight) + return; + + const int ksizeVal = ksize[batch_idx]; + + NVCV_CUDA_ASSERT(ksizeVal == 1 || ksizeVal == 3, "E Wrong ksize = %d, expected: 1 or 3", ksizeVal); + + const float scaleVal = scale[batch_idx]; + constexpr int tileW = outputW + 2 * kRadius; + const int tileH = blockDim.y + 2 * kRadius; + const int tileElems = tileW * tileH; + const int tid = threadIdx.y * blockDim.x + threadIdx.x; + const int y = outBaseY + threadIdx.y; + + extern __shared__ __align__(16) unsigned char tileRaw[]; + T *tile = reinterpret_cast(tileRaw); + + for (int plane = 0; plane < channels; ++plane) + { + for (int idx = tid; idx < tileElems; idx += blockDim.x * blockDim.y) + { + int4 coord{outBaseX - kRadius + idx % tileW, outBaseY - kRadius + idx / tileW, plane, batch_idx}; + tile[idx] = src[coord]; + } + + __syncthreads(); + + if (y < dstHeight) + { + const int tileY = threadIdx.y + kRadius; + auto computeOutput = [&](int tileX) + { + work_type res; + if (ksizeVal == 1) + { + const float axisK = scaleVal; + const float centerK = -4.0f * scaleVal; + + if constexpr (std::is_same_v) + { + res = LaplacianFloatMulRN(tile[(tileY - 1) * tileW + tileX], axisK); + res = LaplacianFloatFmaRN(tile[tileY * tileW + tileX - 1], axisK, res); + res = LaplacianFloatFmaRN(tile[tileY * tileW + tileX], centerK, res); + res = LaplacianFloatFmaRN(tile[tileY * tileW + tileX + 1], axisK, res); + res = LaplacianFloatFmaRN(tile[(tileY + 1) * tileW + tileX], axisK, res); + } + else + { + res = tile[(tileY - 1) * tileW + tileX] * axisK; + res = res + tile[tileY * tileW + tileX - 1] * axisK; + res = res + tile[tileY * tileW + tileX] * centerK; + res = res + tile[tileY * tileW + tileX + 1] * axisK; + res = res + tile[(tileY + 1) * tileW + tileX] * axisK; + } + } + else + { + const float cornerK = 2.0f * scaleVal; + const float centerK = -8.0f * scaleVal; + + if constexpr (std::is_same_v) + { + res = LaplacianFloatMulRN(tile[(tileY - 1) * tileW + tileX - 1], cornerK); + res = LaplacianFloatFmaRN(tile[(tileY - 1) * tileW + tileX + 1], cornerK, res); + res = LaplacianFloatFmaRN(tile[tileY * tileW + tileX], centerK, res); + res = LaplacianFloatFmaRN(tile[(tileY + 1) * tileW + tileX - 1], cornerK, res); + res = LaplacianFloatFmaRN(tile[(tileY + 1) * tileW + tileX + 1], cornerK, res); + } + else + { + res = tile[(tileY - 1) * tileW + tileX - 1] * cornerK; + res = res + tile[(tileY - 1) * tileW + tileX + 1] * cornerK; + res = res + tile[tileY * tileW + tileX] * centerK; + res = res + tile[(tileY + 1) * tileW + tileX - 1] * cornerK; + res = res + tile[(tileY + 1) * tileW + tileX + 1] * cornerK; + } + } + + return cuda::SaturateCast(res); + }; + + if constexpr (NIX == 4 && (std::is_same_v || std::is_same_v)) + { + const int x0 = outBaseX + threadIdx.x * NIX; + if (x0 < dstWidth) + { + const int valid = dstWidth - x0 < NIX ? dstWidth - x0 : NIX; + T outputs[NIX]; + +#pragma unroll + for (int i = 0; i < NIX; ++i) + { + if (i < valid) + outputs[i] = computeOutput(threadIdx.x * NIX + i + kRadius); + } + + T *dstPtr = dst.ptr(batch_idx, plane, y, x0); + constexpr size_t kVectorAlignment = sizeof(T) * NIX; + if (valid == NIX && (reinterpret_cast(dstPtr) & (kVectorAlignment - 1)) == 0) + { + if constexpr (std::is_same_v) + *reinterpret_cast(dstPtr) + = make_uchar4(outputs[0], outputs[1], outputs[2], outputs[3]); + else + *reinterpret_cast(dstPtr) + = make_float4(outputs[0], outputs[1], outputs[2], outputs[3]); + } + else + { +#pragma unroll + for (int i = 0; i < NIX; ++i) + { + if (i < valid) + dstPtr[i] = outputs[i]; + } + } + } + } + else + { +#pragma unroll + for (int i = 0; i < NIX; ++i) + { + const int x = outBaseX + threadIdx.x + i * blockDim.x; + if (x < dstWidth) + *dst.ptr(batch_idx, plane, y, x) = computeOutput(threadIdx.x + i * blockDim.x + kRadius); + } + } + } + + __syncthreads(); + } +} + +template +__global__ void laplacianFilter2DFloat(const SrcWrapper src, DstWrapper dst, cuda::Tensor1DWrap ksize, + cuda::Tensor1DWrap scale) +{ + using work_type = cuda::ConvertBaseTypeTo; + + const int x = blockIdx.x * blockDim.x + threadIdx.x; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + + if (x >= dst.width(batch_idx) || y >= dst.height(batch_idx)) + return; + + const int ksizeVal = ksize[batch_idx]; + const float scaleVal = scale[batch_idx]; + + NVCV_CUDA_ASSERT(ksizeVal == 1 || ksizeVal == 3, "E Wrong ksize = %d, expected: 1 or 3", ksizeVal); + + int3 srcCoord{x, y - 1, batch_idx}; + + if (ksizeVal == 1) + { + const float axisK = scaleVal; + const float centerK = -4.0f * scaleVal; + + work_type res = LaplacianFloatMulRN(src[srcCoord], axisK); + srcCoord.x = x - 1; + srcCoord.y = y; + res = LaplacianFloatFmaRN(src[srcCoord], axisK, res); + srcCoord.x = x; + res = LaplacianFloatFmaRN(src[srcCoord], centerK, res); + srcCoord.x = x + 1; + res = LaplacianFloatFmaRN(src[srcCoord], axisK, res); + + srcCoord.y = y + 1; + srcCoord.x = x; + res = LaplacianFloatFmaRN(src[srcCoord], axisK, res); + + *dst.ptr(batch_idx, y, x) = cuda::SaturateCast(res); + } + else + { + const float cornerK = 2.0f * scaleVal; + const float centerK = -8.0f * scaleVal; + + srcCoord.x = x - 1; + work_type res = LaplacianFloatMulRN(src[srcCoord], cornerK); + srcCoord.x = x + 1; + res = LaplacianFloatFmaRN(src[srcCoord], cornerK, res); + + srcCoord.x = x; + srcCoord.y = y; + res = LaplacianFloatFmaRN(src[srcCoord], centerK, res); + + srcCoord.x = x - 1; + srcCoord.y = y + 1; + res = LaplacianFloatFmaRN(src[srcCoord], cornerK, res); + srcCoord.x = x + 1; + res = LaplacianFloatFmaRN(src[srcCoord], cornerK, res); + + *dst.ptr(batch_idx, y, x) = cuda::SaturateCast(res); + } +} + +template +void LaplacianFilter2DCaller(const ImageBatchVarShapeDataStridedCuda &inData, + const ImageBatchVarShapeDataStridedCuda &outData, const TensorDataStridedCuda &ksize, + const TensorDataStridedCuda &scale, float borderValue, cudaStream_t stream) +{ + cuda::BorderVarShapeWrap src(inData, cuda::SetAll(borderValue)); + cuda::ImageBatchVarShapeWrap dst(outData); + cuda::Tensor1DWrap kernelApertureSize(ksize); + cuda::Tensor1DWrap kernelScale(scale); + + using work_type = cuda::ConvertBaseTypeTo; + + dim3 block(16, 16); + dim3 grid(divUp(inData.maxSize().w, block.x), divUp(inData.maxSize().h, block.y), outData.numImages()); + +#ifdef CUDA_DEBUG_LOG + checkCudaErrors(cudaStreamSynchronize(stream)); + checkCudaErrors(cudaGetLastError()); +#endif + + laplacianFilter2D<<>>(src, dst, kernelApertureSize, kernelScale); + checkKernelErrors(); +#ifdef CUDA_DEBUG_LOG + checkCudaErrors(cudaStreamSynchronize(stream)); + checkCudaErrors(cudaGetLastError()); +#endif +} + +template +void LaplacianFilter2DU8Caller(const ImageBatchVarShapeDataStridedCuda &inData, + const ImageBatchVarShapeDataStridedCuda &outData, const TensorDataStridedCuda &ksize, + const TensorDataStridedCuda &scale, float borderValue, cudaStream_t stream) +{ + cuda::BorderVarShapeWrap src(inData, cuda::SetAll(borderValue)); + cuda::ImageBatchVarShapeWrap dst(outData); + cuda::Tensor1DWrap kernelApertureSize(ksize); + cuda::Tensor1DWrap kernelScale(scale); + + dim3 block(16, 16); + dim3 grid(divUp(inData.maxSize().w, block.x), divUp(inData.maxSize().h, block.y), outData.numImages()); + +#ifdef CUDA_DEBUG_LOG + checkCudaErrors(cudaStreamSynchronize(stream)); + checkCudaErrors(cudaGetLastError()); +#endif + + laplacianFilter2DU8<<>>(src, dst, kernelApertureSize, kernelScale); + checkKernelErrors(); +#ifdef CUDA_DEBUG_LOG + checkCudaErrors(cudaStreamSynchronize(stream)); + checkCudaErrors(cudaGetLastError()); +#endif +} + +template +void LaplacianFilter2DFloatCaller(const ImageBatchVarShapeDataStridedCuda &inData, + const ImageBatchVarShapeDataStridedCuda &outData, const TensorDataStridedCuda &ksize, + const TensorDataStridedCuda &scale, float borderValue, cudaStream_t stream) +{ + cuda::BorderVarShapeWrap src(inData, cuda::SetAll(borderValue)); + cuda::ImageBatchVarShapeWrap dst(outData); + cuda::Tensor1DWrap kernelApertureSize(ksize); + cuda::Tensor1DWrap kernelScale(scale); + + dim3 block(16, 16); + dim3 grid(divUp(inData.maxSize().w, block.x), divUp(inData.maxSize().h, block.y), outData.numImages()); + +#ifdef CUDA_DEBUG_LOG + checkCudaErrors(cudaStreamSynchronize(stream)); + checkCudaErrors(cudaGetLastError()); +#endif + + laplacianFilter2DFloat<<>>(src, dst, kernelApertureSize, kernelScale); + checkKernelErrors(); +#ifdef CUDA_DEBUG_LOG + checkCudaErrors(cudaStreamSynchronize(stream)); + checkCudaErrors(cudaGetLastError()); +#endif +} + +template +void LaplacianFilter2D(const ImageBatchVarShapeDataStridedCuda &inData, + const ImageBatchVarShapeDataStridedCuda &outData, const TensorDataStridedCuda &ksize, + const TensorDataStridedCuda &scale, NVCVBorderType borderMode, float borderValue, + cudaStream_t stream) +{ + typedef void (*func_t)(const ImageBatchVarShapeDataStridedCuda &inData, + const ImageBatchVarShapeDataStridedCuda &outData, const TensorDataStridedCuda &ksize, + const TensorDataStridedCuda &scale, float borderValue, cudaStream_t stream); + + static const func_t funcs[] + = {LaplacianFilter2DCaller, LaplacianFilter2DCaller, + LaplacianFilter2DCaller, LaplacianFilter2DCaller, + LaplacianFilter2DCaller}; + + funcs[borderMode](inData, outData, ksize, scale, borderValue, stream); +} + +template +void LaplacianFilter2DU8(const ImageBatchVarShapeDataStridedCuda &inData, + const ImageBatchVarShapeDataStridedCuda &outData, const TensorDataStridedCuda &ksize, + const TensorDataStridedCuda &scale, NVCVBorderType borderMode, float borderValue, + cudaStream_t stream) +{ + typedef void (*func_t)(const ImageBatchVarShapeDataStridedCuda &inData, + const ImageBatchVarShapeDataStridedCuda &outData, const TensorDataStridedCuda &ksize, + const TensorDataStridedCuda &scale, float borderValue, cudaStream_t stream); + + static const func_t funcs[] + = {LaplacianFilter2DU8Caller, LaplacianFilter2DU8Caller, + LaplacianFilter2DU8Caller, LaplacianFilter2DU8Caller, + LaplacianFilter2DU8Caller}; + + funcs[borderMode](inData, outData, ksize, scale, borderValue, stream); +} + +template +void LaplacianFilter2DFloat(const ImageBatchVarShapeDataStridedCuda &inData, + const ImageBatchVarShapeDataStridedCuda &outData, const TensorDataStridedCuda &ksize, + const TensorDataStridedCuda &scale, NVCVBorderType borderMode, float borderValue, + cudaStream_t stream) +{ + typedef void (*func_t)(const ImageBatchVarShapeDataStridedCuda &inData, + const ImageBatchVarShapeDataStridedCuda &outData, const TensorDataStridedCuda &ksize, + const TensorDataStridedCuda &scale, float borderValue, cudaStream_t stream); + + static const func_t funcs[] + = {LaplacianFilter2DFloatCaller, + LaplacianFilter2DFloatCaller, LaplacianFilter2DFloatCaller, + LaplacianFilter2DFloatCaller, LaplacianFilter2DFloatCaller}; + + funcs[borderMode](inData, outData, ksize, scale, borderValue, stream); } template -void LaplacianFilter2DCaller(const ImageBatchVarShapeDataStridedCuda &inData, - const ImageBatchVarShapeDataStridedCuda &outData, const TensorDataStridedCuda &ksize, - const TensorDataStridedCuda &scale, float borderValue, cudaStream_t stream) +void LaplacianFilter2DPlanarCaller(const ImageBatchVarShapeDataStridedCuda &inData, + const ImageBatchVarShapeDataStridedCuda &outData, const TensorDataStridedCuda &ksize, + const TensorDataStridedCuda &scale, int channels, float borderValue, + cudaStream_t stream) { cuda::BorderVarShapeWrap src(inData, cuda::SetAll(borderValue)); cuda::ImageBatchVarShapeWrap dst(outData); cuda::Tensor1DWrap kernelApertureSize(ksize); cuda::Tensor1DWrap kernelScale(scale); - using work_type = cuda::ConvertBaseTypeTo; - dim3 block(16, 16); - dim3 grid(divUp(inData.maxSize().w, block.x), divUp(inData.maxSize().h, block.y), outData.numImages()); + dim3 grid(divUp(outData.maxSize().w, block.x), divUp(outData.maxSize().h, block.y), outData.numImages()); #ifdef CUDA_DEBUG_LOG checkCudaErrors(cudaStreamSynchronize(stream)); checkCudaErrors(cudaGetLastError()); #endif - laplacianFilter2D<<>>(src, dst, kernelApertureSize, kernelScale); + laplacianFilter2DPlanar<<>>(src, dst, kernelApertureSize, kernelScale, channels); + checkKernelErrors(); +#ifdef CUDA_DEBUG_LOG + checkCudaErrors(cudaStreamSynchronize(stream)); + checkCudaErrors(cudaGetLastError()); +#endif +} + +template +void LaplacianFilter2DPlanarTiledCaller(const ImageBatchVarShapeDataStridedCuda &inData, + const ImageBatchVarShapeDataStridedCuda &outData, + const TensorDataStridedCuda &ksize, const TensorDataStridedCuda &scale, + int channels, float borderValue, cudaStream_t stream) +{ + cuda::BorderVarShapeWrap src(inData, cuda::SetAll(borderValue)); + cuda::ImageBatchVarShapeWrap dst(outData); + cuda::Tensor1DWrap kernelApertureSize(ksize); + cuda::Tensor1DWrap kernelScale(scale); + + dim3 block(kLaplacianPlanarBlockWidth, std::is_same_v ? 8 : 16); + dim3 grid(divUp(outData.maxSize().w, block.x * NIX), divUp(outData.maxSize().h, block.y), outData.numImages()); + + const size_t sharedBytes = static_cast(block.x * NIX + 2) * (block.y + 2) * sizeof(D); + laplacianFilter2DPlanarTiled + <<>>(src, dst, kernelApertureSize, kernelScale, channels); checkKernelErrors(); #ifdef CUDA_DEBUG_LOG checkCudaErrors(cudaStreamSynchronize(stream)); @@ -295,21 +1671,40 @@ void LaplacianFilter2DCaller(const ImageBatchVarShapeDataStridedCuda &inData, } template -void LaplacianFilter2D(const ImageBatchVarShapeDataStridedCuda &inData, - const ImageBatchVarShapeDataStridedCuda &outData, const TensorDataStridedCuda &ksize, - const TensorDataStridedCuda &scale, NVCVBorderType borderMode, float borderValue, - cudaStream_t stream) +void LaplacianFilter2DPlanar(const ImageBatchVarShapeDataStridedCuda &inData, + const ImageBatchVarShapeDataStridedCuda &outData, const TensorDataStridedCuda &ksize, + const TensorDataStridedCuda &scale, NVCVBorderType borderMode, float borderValue, + int channels, cudaStream_t stream) { typedef void (*func_t)(const ImageBatchVarShapeDataStridedCuda &inData, const ImageBatchVarShapeDataStridedCuda &outData, const TensorDataStridedCuda &ksize, - const TensorDataStridedCuda &scale, float borderValue, cudaStream_t stream); + const TensorDataStridedCuda &scale, int channels, float borderValue, cudaStream_t stream); - static const func_t funcs[] - = {LaplacianFilter2DCaller, LaplacianFilter2DCaller, - LaplacianFilter2DCaller, LaplacianFilter2DCaller, - LaplacianFilter2DCaller}; + static const func_t funcs[] = { + LaplacianFilter2DPlanarCaller, LaplacianFilter2DPlanarCaller, + LaplacianFilter2DPlanarCaller, LaplacianFilter2DPlanarCaller, + LaplacianFilter2DPlanarCaller}; - funcs[borderMode](inData, outData, ksize, scale, borderValue, stream); + funcs[borderMode](inData, outData, ksize, scale, channels, borderValue, stream); +} + +template +void LaplacianFilter2DPlanarTiled(const ImageBatchVarShapeDataStridedCuda &inData, + const ImageBatchVarShapeDataStridedCuda &outData, const TensorDataStridedCuda &ksize, + const TensorDataStridedCuda &scale, NVCVBorderType borderMode, float borderValue, + int channels, cudaStream_t stream) +{ + typedef void (*func_t)(const ImageBatchVarShapeDataStridedCuda &inData, + const ImageBatchVarShapeDataStridedCuda &outData, const TensorDataStridedCuda &ksize, + const TensorDataStridedCuda &scale, int channels, float borderValue, cudaStream_t stream); + + static const func_t funcs[] = {LaplacianFilter2DPlanarTiledCaller, + LaplacianFilter2DPlanarTiledCaller, + LaplacianFilter2DPlanarTiledCaller, + LaplacianFilter2DPlanarTiledCaller, + LaplacianFilter2DPlanarTiledCaller}; + + funcs[borderMode](inData, outData, ksize, scale, channels, borderValue, stream); } ErrorCode LaplacianVarShape::infer(const ImageBatchVarShapeDataStridedCuda &inData, @@ -337,12 +1732,15 @@ ErrorCode LaplacianVarShape::infer(const ImageBatchVarShapeDataStridedCuda &inDa DataFormat format = input_format; - if (!(format == kNHWC || format == kHWC)) + if (!(format == kNHWC || format == kHWC || format == kNCHW || format == kCHW)) { - LOG_ERROR("Invalid input DataFormat " << format << ", the valid DataFormats are: \"NHWC\", \"HWC\""); + LOG_ERROR("Invalid input DataFormat " << format + << ", the valid DataFormats are: \"NHWC\", \"HWC\", \"NCHW\", \"CHW\""); return ErrorCode::INVALID_DATA_FORMAT; } + const bool isPlanar = IsPlanar(format); + if (!(borderMode == NVCV_BORDER_REFLECT101 || borderMode == NVCV_BORDER_REPLICATE || borderMode == NVCV_BORDER_CONSTANT || borderMode == NVCV_BORDER_REFLECT || borderMode == NVCV_BORDER_WRAP)) { @@ -360,7 +1758,7 @@ ErrorCode LaplacianVarShape::infer(const ImageBatchVarShapeDataStridedCuda &inDa const int channels = inData.uniqueFormat().numChannels(); - if (channels > 4) + if (channels > 4 || (isPlanar && channels == 2)) { LOG_ERROR("Invalid channel number " << channels); return ErrorCode::INVALID_DATA_SHAPE; @@ -372,20 +1770,84 @@ ErrorCode LaplacianVarShape::infer(const ImageBatchVarShapeDataStridedCuda &inDa const ImageBatchVarShapeDataStridedCuda &outData, const TensorDataStridedCuda &ksize, const TensorDataStridedCuda &scale, NVCVBorderType borderMode, float borderValue, cudaStream_t stream); + typedef void (*laplacian_u8_t)(const ImageBatchVarShapeDataStridedCuda &inData, + const ImageBatchVarShapeDataStridedCuda &outData, const TensorDataStridedCuda &ksize, + const TensorDataStridedCuda &scale, NVCVBorderType borderMode, float borderValue, + cudaStream_t stream); + typedef void (*laplacian_float_t)(const ImageBatchVarShapeDataStridedCuda &inData, + const ImageBatchVarShapeDataStridedCuda &outData, + const TensorDataStridedCuda &ksize, const TensorDataStridedCuda &scale, + NVCVBorderType borderMode, float borderValue, cudaStream_t stream); static const filter2D_t funcs[6][4] = { - { LaplacianFilter2D, 0, LaplacianFilter2D, LaplacianFilter2D}, + { 0, 0, 0, 0}, { 0, 0, 0, 0}, {LaplacianFilter2D, 0, LaplacianFilter2D, LaplacianFilter2D}, { 0, 0, 0, 0}, { 0, 0, 0, 0}, - { LaplacianFilter2D, 0, LaplacianFilter2D, LaplacianFilter2D}, + { 0, 0, 0, 0}, }; - const filter2D_t func = funcs[data_type][channels - 1]; + static const laplacian_u8_t u8Funcs[4] + = {LaplacianFilter2DU8, 0, LaplacianFilter2DU8, LaplacianFilter2DU8}; + static const laplacian_float_t floatFuncs[4] + = {LaplacianFilter2DFloat, 0, LaplacianFilter2DFloat, LaplacianFilter2DFloat}; - NVCV_ASSERT(func != 0); + if (isPlanar) + { + if (outData.numImages() > 65535) + { + LOG_ERROR("Planar Laplacian requires numImages <= 65535 (CUDA grid-z limit)"); + return ErrorCode::INVALID_DATA_SHAPE; + } + + typedef void (*planar_filter2D_t)( + const ImageBatchVarShapeDataStridedCuda &inData, const ImageBatchVarShapeDataStridedCuda &outData, + const TensorDataStridedCuda &ksize, const TensorDataStridedCuda &scale, NVCVBorderType borderMode, + float borderValue, int channels, cudaStream_t stream); + static const planar_filter2D_t planarFuncs[6] = { + 0, 0, LaplacianFilter2DPlanar, 0, 0, 0, + }; + + if (data_type == kCV_8U) + { + LaplacianFilter2DPlanarTiled(inData, outData, ksize, scale, borderMode, + borderValue, channels, stream); + return ErrorCode::SUCCESS; + } + if (data_type == kCV_32F) + { + LaplacianFilter2DPlanarTiled(inData, outData, ksize, scale, borderMode, + borderValue, channels, stream); + return ErrorCode::SUCCESS; + } + + const planar_filter2D_t planarFunc = planarFuncs[data_type]; + + NVCV_ASSERT(planarFunc != 0); + + planarFunc(inData, outData, ksize, scale, borderMode, borderValue, channels, stream); + + return ErrorCode::SUCCESS; + } + if (data_type == kCV_8U) + { + const laplacian_u8_t u8Func = u8Funcs[channels - 1]; + NVCV_ASSERT(u8Func != 0); + u8Func(inData, outData, ksize, scale, borderMode, borderValue, stream); + return ErrorCode::SUCCESS; + } + if (data_type == kCV_32F) + { + const laplacian_float_t floatFunc = floatFuncs[channels - 1]; + NVCV_ASSERT(floatFunc != 0); + floatFunc(inData, outData, ksize, scale, borderMode, borderValue, stream); + return ErrorCode::SUCCESS; + } + + const filter2D_t func = funcs[data_type][channels - 1]; + NVCV_ASSERT(func != 0); func(inData, outData, ksize, scale, borderMode, borderValue, stream); return ErrorCode::SUCCESS; @@ -405,40 +1867,416 @@ __global__ void gaussianFilter2D(const SrcWrapper src, DstWrapper dst, cuda::Ten const int y = blockIdx.y * blockDim.y + threadIdx.y; const int batch_idx = get_batch_idx(); - if (x >= dst.width(batch_idx) || y >= dst.height(batch_idx)) + if (x >= dst.width(batch_idx) || y >= dst.height(batch_idx)) + return; + + int2 kernelSize = kernelSizeArr[batch_idx]; + double2 sigma = sigmaArr[batch_idx]; + + // automatic detection of kernel size from sigma + if (kernelSize.x <= 0 && sigma.x > 0) + kernelSize.x = cuda::round(sigma.x * dataKernelSize * 2 + 1) | 1; + if (kernelSize.y <= 0 && sigma.y > 0) + kernelSize.y = cuda::round(sigma.y * dataKernelSize * 2 + 1) | 1; + + NVCV_CUDA_ASSERT(kernelSize.x > 0 && (kernelSize.x % 2 == 1) && kernelSize.x <= maxKernelSize.w, + "E Wrong kernelSize.x = %d, expected > 0, odd and <= %d\n", kernelSize.x, maxKernelSize.w); + NVCV_CUDA_ASSERT(kernelSize.y > 0 && (kernelSize.y % 2 == 1) && kernelSize.y <= maxKernelSize.h, + "E Wrong kernelSize.y = %d, expected > 0, odd and <= %d\n", kernelSize.y, maxKernelSize.h); + + int2 anchor{kernelSize.x / 2, kernelSize.y / 2}; + + int3 srcCoord{0, 0, batch_idx}; + + for (int i = 0; i < kernelSize.y; ++i) + { + srcCoord.y = y - anchor.y + i; + + for (int j = 0; j < kernelSize.x; ++j) + { + srcCoord.x = x - anchor.x + j; + + res = res + src[srcCoord] * (*kernel.ptr(batch_idx, i, j)); + } + } + + *dst.ptr(batch_idx, y, x) = cuda::SaturateCast(res); +} + +template +__global__ void gaussianFilter2DTiled(const SrcWrapper src, DstWrapper dst, cuda::Tensor3DWrap kernel, + cuda::Tensor1DWrap kernelSizeArr, Size2D maxKernelSize, + cuda::Tensor1DWrap sigmaArr, int dataKernelSize) +{ + using T = typename DstWrapper::ValueType; + using work_type = cuda::ConvertBaseTypeTo; + + const int x = blockIdx.x * blockDim.x + threadIdx.x; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + + int2 kernelSize = kernelSizeArr[batch_idx]; + double2 sigma = sigmaArr[batch_idx]; + + // automatic detection of kernel size from sigma + if (kernelSize.x <= 0 && sigma.x > 0) + kernelSize.x = cuda::round(sigma.x * dataKernelSize * 2 + 1) | 1; + if (kernelSize.y <= 0 && sigma.y > 0) + kernelSize.y = cuda::round(sigma.y * dataKernelSize * 2 + 1) | 1; + + NVCV_CUDA_ASSERT(kernelSize.x > 0 && (kernelSize.x % 2 == 1) && kernelSize.x <= maxKernelSize.w, + "E Wrong kernelSize.x = %d, expected > 0, odd and <= %d\n", kernelSize.x, maxKernelSize.w); + NVCV_CUDA_ASSERT(kernelSize.y > 0 && (kernelSize.y % 2 == 1) && kernelSize.y <= maxKernelSize.h, + "E Wrong kernelSize.y = %d, expected > 0, odd and <= %d\n", kernelSize.y, maxKernelSize.h); + + const int2 anchor{kernelSize.x / 2, kernelSize.y / 2}; + const int tileW = blockDim.x + kernelSize.x - 1; + const int tileH = blockDim.y + kernelSize.y - 1; + const int baseX = blockIdx.x * blockDim.x - anchor.x; + const int baseY = blockIdx.y * blockDim.y - anchor.y; + const int tileElems = tileW * tileH; + const int kernelElems = kernelSize.x * kernelSize.y; + + extern __shared__ __align__(16) unsigned char tileRaw[]; + T *tile = reinterpret_cast(tileRaw); + const size_t kernelOffset = (tileElems * sizeof(T) + sizeof(float) - 1) & ~(sizeof(float) - 1); + float *kernelTile = reinterpret_cast(tileRaw + kernelOffset); + + const int tid = threadIdx.y * blockDim.x + threadIdx.x; + const int blockThreads = blockDim.x * blockDim.y; + + for (int idx = tid; idx < tileElems; idx += blockThreads) + { + int3 srcCoord{baseX + idx % tileW, baseY + idx / tileW, batch_idx}; + tile[idx] = src[srcCoord]; + } + for (int idx = tid; idx < kernelElems; idx += blockThreads) + { + kernelTile[idx] = *kernel.ptr(batch_idx, idx / kernelSize.x, idx % kernelSize.x); + } + + __syncthreads(); + + if (x >= dst.width(batch_idx) || y >= dst.height(batch_idx)) + return; + + work_type res = cuda::SetAll(0); + int kInd = 0; + + for (int i = 0; i < kernelSize.y; ++i) + { + const int tileY = threadIdx.y + i; + + for (int j = 0; j < kernelSize.x; ++j) + { + const int tileX = threadIdx.x + j; + + res = res + tile[tileY * tileW + tileX] * kernelTile[kInd++]; + } + } + + *dst.ptr(batch_idx, y, x) = cuda::SaturateCast(res); +} + +template +__global__ void gaussianFilter2DTiledX2(const SrcWrapper src, DstWrapper dst, cuda::Tensor3DWrap kernel, + cuda::Tensor1DWrap kernelSizeArr, Size2D maxKernelSize, + cuda::Tensor1DWrap sigmaArr, int dataKernelSize) +{ + using T = typename DstWrapper::ValueType; + using work_type = cuda::ConvertBaseTypeTo; + + const int outputW = blockDim.x * 2; + const int x0 = blockIdx.x * outputW + threadIdx.x * 2; + const int x1 = x0 + 1; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + + int2 kernelSize = kernelSizeArr[batch_idx]; + double2 sigma = sigmaArr[batch_idx]; + + // automatic detection of kernel size from sigma + if (kernelSize.x <= 0 && sigma.x > 0) + kernelSize.x = cuda::round(sigma.x * dataKernelSize * 2 + 1) | 1; + if (kernelSize.y <= 0 && sigma.y > 0) + kernelSize.y = cuda::round(sigma.y * dataKernelSize * 2 + 1) | 1; + + NVCV_CUDA_ASSERT(kernelSize.x > 0 && (kernelSize.x % 2 == 1) && kernelSize.x <= maxKernelSize.w, + "E Wrong kernelSize.x = %d, expected > 0, odd and <= %d\n", kernelSize.x, maxKernelSize.w); + NVCV_CUDA_ASSERT(kernelSize.y > 0 && (kernelSize.y % 2 == 1) && kernelSize.y <= maxKernelSize.h, + "E Wrong kernelSize.y = %d, expected > 0, odd and <= %d\n", kernelSize.y, maxKernelSize.h); + + const int2 anchor{kernelSize.x / 2, kernelSize.y / 2}; + const int tileW = outputW + kernelSize.x - 1; + const int tileH = blockDim.y + kernelSize.y - 1; + const int baseX = blockIdx.x * outputW - anchor.x; + const int baseY = blockIdx.y * blockDim.y - anchor.y; + const int tileElems = tileW * tileH; + const int kernelElems = kernelSize.x * kernelSize.y; + + extern __shared__ __align__(16) unsigned char tileRaw[]; + T *tile = reinterpret_cast(tileRaw); + const size_t kernelOffset = (tileElems * sizeof(T) + sizeof(float) - 1) & ~(sizeof(float) - 1); + float *kernelTile = reinterpret_cast(tileRaw + kernelOffset); + + const int tid = threadIdx.y * blockDim.x + threadIdx.x; + const int blockThreads = blockDim.x * blockDim.y; + + for (int idx = tid; idx < tileElems; idx += blockThreads) + { + int3 srcCoord{baseX + idx % tileW, baseY + idx / tileW, batch_idx}; + tile[idx] = src[srcCoord]; + } + for (int idx = tid; idx < kernelElems; idx += blockThreads) + { + kernelTile[idx] = *kernel.ptr(batch_idx, idx / kernelSize.x, idx % kernelSize.x); + } + + __syncthreads(); + + if (x0 >= dst.width(batch_idx) || y >= dst.height(batch_idx)) + return; + + work_type res0 = cuda::SetAll(0); + work_type res1 = cuda::SetAll(0); + int kInd = 0; + + for (int i = 0; i < kernelSize.y; ++i) + { + const int tileY = threadIdx.y + i; + + for (int j = 0; j < kernelSize.x; ++j) + { + const int tileX = threadIdx.x * 2 + j; + const float k = kernelTile[kInd++]; + + res0 = res0 + tile[tileY * tileW + tileX] * k; + res1 = res1 + tile[tileY * tileW + tileX + 1] * k; + } + } + + *dst.ptr(batch_idx, y, x0) = cuda::SaturateCast(res0); + if (x1 < dst.width(batch_idx)) + { + *dst.ptr(batch_idx, y, x1) = cuda::SaturateCast(res1); + } +} + +template +__global__ void gaussianFilter2DTiledX4(const SrcWrapper src, DstWrapper dst, cuda::Tensor3DWrap kernel, + cuda::Tensor1DWrap kernelSizeArr, Size2D maxKernelSize, + cuda::Tensor1DWrap sigmaArr, int dataKernelSize) +{ + using T = typename DstWrapper::ValueType; + using work_type = cuda::ConvertBaseTypeTo; + + const int outputW = blockDim.x * 4; + const int x0 = blockIdx.x * outputW + threadIdx.x * 4; + const int x1 = x0 + 1; + const int x2 = x0 + 2; + const int x3 = x0 + 3; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + + int2 kernelSize = kernelSizeArr[batch_idx]; + double2 sigma = sigmaArr[batch_idx]; + + // automatic detection of kernel size from sigma + if (kernelSize.x <= 0 && sigma.x > 0) + kernelSize.x = cuda::round(sigma.x * dataKernelSize * 2 + 1) | 1; + if (kernelSize.y <= 0 && sigma.y > 0) + kernelSize.y = cuda::round(sigma.y * dataKernelSize * 2 + 1) | 1; + + NVCV_CUDA_ASSERT(kernelSize.x > 0 && (kernelSize.x % 2 == 1) && kernelSize.x <= maxKernelSize.w, + "E Wrong kernelSize.x = %d, expected > 0, odd and <= %d\n", kernelSize.x, maxKernelSize.w); + NVCV_CUDA_ASSERT(kernelSize.y > 0 && (kernelSize.y % 2 == 1) && kernelSize.y <= maxKernelSize.h, + "E Wrong kernelSize.y = %d, expected > 0, odd and <= %d\n", kernelSize.y, maxKernelSize.h); + + const int2 anchor{kernelSize.x / 2, kernelSize.y / 2}; + const int tileW = outputW + kernelSize.x - 1; + const int tileH = blockDim.y + kernelSize.y - 1; + const int baseX = blockIdx.x * outputW - anchor.x; + const int baseY = blockIdx.y * blockDim.y - anchor.y; + const int tileElems = tileW * tileH; + const int kernelElems = kernelSize.x * kernelSize.y; + + extern __shared__ __align__(16) unsigned char tileRaw[]; + T *tile = reinterpret_cast(tileRaw); + const size_t kernelOffset = (tileElems * sizeof(T) + sizeof(float) - 1) & ~(sizeof(float) - 1); + float *kernelTile = reinterpret_cast(tileRaw + kernelOffset); + + const int tid = threadIdx.y * blockDim.x + threadIdx.x; + const int blockThreads = blockDim.x * blockDim.y; + + for (int idx = tid; idx < tileElems; idx += blockThreads) + { + int3 srcCoord{baseX + idx % tileW, baseY + idx / tileW, batch_idx}; + tile[idx] = src[srcCoord]; + } + for (int idx = tid; idx < kernelElems; idx += blockThreads) + { + kernelTile[idx] = *kernel.ptr(batch_idx, idx / kernelSize.x, idx % kernelSize.x); + } + + __syncthreads(); + + if (x0 >= dst.width(batch_idx) || y >= dst.height(batch_idx)) + return; + + work_type res0 = cuda::SetAll(0); + work_type res1 = cuda::SetAll(0); + work_type res2 = cuda::SetAll(0); + work_type res3 = cuda::SetAll(0); + int kInd = 0; + + for (int i = 0; i < kernelSize.y; ++i) + { + const int tileY = threadIdx.y + i; + + for (int j = 0; j < kernelSize.x; ++j) + { + const int tileX = threadIdx.x * 4 + j; + const float k = kernelTile[kInd++]; + + res0 = res0 + tile[tileY * tileW + tileX] * k; + res1 = res1 + tile[tileY * tileW + tileX + 1] * k; + res2 = res2 + tile[tileY * tileW + tileX + 2] * k; + res3 = res3 + tile[tileY * tileW + tileX + 3] * k; + } + } + + *dst.ptr(batch_idx, y, x0) = cuda::SaturateCast(res0); + if (x1 < dst.width(batch_idx)) + { + *dst.ptr(batch_idx, y, x1) = cuda::SaturateCast(res1); + } + if (x2 < dst.width(batch_idx)) + { + *dst.ptr(batch_idx, y, x2) = cuda::SaturateCast(res2); + } + if (x3 < dst.width(batch_idx)) + { + *dst.ptr(batch_idx, y, x3) = cuda::SaturateCast(res3); + } +} + +template +__global__ void gaussianFilter2DTiledFixed(const SrcWrapper src, DstWrapper dst, + cuda::Tensor3DWrap kernel, + cuda::Tensor1DWrap kernelSizeArr, Size2D maxKernelSize, + cuda::Tensor1DWrap sigmaArr, int dataKernelSize) +{ + using T = typename DstWrapper::ValueType; + using work_type = cuda::ConvertBaseTypeTo; + + const int outputW = blockDim.x * Outputs; + const int x0 = blockIdx.x * outputW + threadIdx.x * Outputs; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + + const int2 kernelSize + = ResolveGaussianKernelSize(kernelSizeArr, sigmaArr, maxKernelSize, dataKernelSize, batch_idx); + const int2 anchor{kernelSize.x / 2, kernelSize.y / 2}; + const int tileW = outputW + KernelW - 1; + const int tileH = blockDim.y + KernelH - 1; + const int baseX = blockIdx.x * outputW - anchor.x; + const int baseY = blockIdx.y * blockDim.y - anchor.y; + const int tileElems = tileW * tileH; + const int kernelElems = kernelSize.x * kernelSize.y; + + extern __shared__ __align__(16) unsigned char tileRaw[]; + T *tile = reinterpret_cast(tileRaw); + const size_t kernelOffset = (tileElems * sizeof(T) + sizeof(float) - 1) & ~(sizeof(float) - 1); + float *kernelTile = reinterpret_cast(tileRaw + kernelOffset); + + const int tid = threadIdx.y * blockDim.x + threadIdx.x; + const int blockThreads = blockDim.x * blockDim.y; + + for (int idx = tid; idx < tileElems; idx += blockThreads) + { + int3 srcCoord{baseX + idx % tileW, baseY + idx / tileW, batch_idx}; + tile[idx] = src[srcCoord]; + } + for (int idx = tid; idx < kernelElems; idx += blockThreads) + { + kernelTile[idx] = *kernel.ptr(batch_idx, idx / kernelSize.x, idx % kernelSize.x); + } + + __syncthreads(); + + if (x0 >= dst.width(batch_idx) || y >= dst.height(batch_idx)) return; - int2 kernelSize = kernelSizeArr[batch_idx]; - double2 sigma = sigmaArr[batch_idx]; + work_type res[Outputs]; +#pragma unroll + for (int output = 0; output < Outputs; ++output) + { + res[output] = cuda::SetAll(0); + } - // automatic detection of kernel size from sigma - if (kernelSize.x <= 0 && sigma.x > 0) - kernelSize.x = cuda::round(sigma.x * dataKernelSize * 2 + 1) | 1; - if (kernelSize.y <= 0 && sigma.y > 0) - kernelSize.y = cuda::round(sigma.y * dataKernelSize * 2 + 1) | 1; + int kInd = 0; + if (kernelSize.x == KernelW && kernelSize.y == KernelH) + { +#pragma unroll + for (int i = 0; i < KernelH; ++i) + { + const int tileY = threadIdx.y + i; - NVCV_CUDA_ASSERT(kernelSize.x > 0 && (kernelSize.x % 2 == 1) && kernelSize.x <= maxKernelSize.w, - "E Wrong kernelSize.x = %d, expected > 0, odd and <= %d\n", kernelSize.x, maxKernelSize.w); - NVCV_CUDA_ASSERT(kernelSize.y > 0 && (kernelSize.y % 2 == 1) && kernelSize.y <= maxKernelSize.h, - "E Wrong kernelSize.y = %d, expected > 0, odd and <= %d\n", kernelSize.y, maxKernelSize.h); +#pragma unroll + for (int j = 0; j < KernelW; ++j) + { + const int tileX = threadIdx.x * Outputs + j; + const float k = kernelTile[kInd++]; - int2 anchor{kernelSize.x / 2, kernelSize.y / 2}; +#pragma unroll + for (int output = 0; output < Outputs; ++output) + { + res[output] = res[output] + tile[tileY * tileW + tileX + output] * k; + } + } + } + } + else + { + for (int i = 0; i < kernelSize.y; ++i) + { + const int tileY = threadIdx.y + i; - int3 srcCoord{0, 0, batch_idx}; + for (int j = 0; j < kernelSize.x; ++j) + { + const int tileX = threadIdx.x * Outputs + j; + const float k = kernelTile[kInd++]; - for (int i = 0; i < kernelSize.y; ++i) - { - srcCoord.y = y - anchor.y + i; +#pragma unroll + for (int output = 0; output < Outputs; ++output) + { + res[output] = res[output] + tile[tileY * tileW + tileX + output] * k; + } + } + } + } - for (int j = 0; j < kernelSize.x; ++j) +#pragma unroll + for (int output = 0; output < Outputs; ++output) + { + const int x = x0 + output; + if (x < dst.width(batch_idx)) { - srcCoord.x = x - anchor.x + j; - - res = res + src[srcCoord] * (*kernel.ptr(batch_idx, i, j)); + *dst.ptr(batch_idx, y, x) = cuda::SaturateCast(res[output]); } } +} - *dst.ptr(batch_idx, y, x) = cuda::SaturateCast(res); +constexpr int kGaussianBlockWidth = 16; +constexpr int kGaussianBlockHeight = 16; + +template +constexpr size_t GaussianFixedSharedBytes() +{ + constexpr size_t tileBytes = static_cast(kGaussianBlockWidth * Outputs + KSize - 1) + * (kGaussianBlockHeight + KSize - 1) * sizeof(T); + constexpr size_t kernelOffset = (tileBytes + sizeof(float) - 1) & ~(sizeof(float) - 1); + return kernelOffset + static_cast(KSize) * KSize * sizeof(float); } template @@ -446,24 +2284,233 @@ void GaussianFilter2DCaller(const ImageBatchVarShapeDataStridedCuda &inData, const ImageBatchVarShapeDataStridedCuda &outData, const cuda::Tensor3DWrap &kernelTensor, const cuda::Tensor1DWrap &kernelSizeTensor, Size2D maxKernelSize, - const cuda::Tensor1DWrap &sigmaTensor, int dataKernelSize, + const cuda::Tensor1DWrap &sigmaTensor, int dataKernelSize, bool enableX4, float borderValue, cudaStream_t stream) { + using BaseT = cuda::BaseType; + + constexpr bool kX4Always = std::is_same_v || cuda::NumElements == 1; + constexpr bool kDynamicX4Possible = kX4Always || std::is_same_v; + cuda::BorderVarShapeWrap src(inData, cuda::SetAll(borderValue)); cuda::ImageBatchVarShapeWrap dst(outData); using work_type = cuda::ConvertBaseTypeTo; - dim3 block(16, 16); + dim3 block(kGaussianBlockWidth, kGaussianBlockHeight); dim3 grid(divUp(inData.maxSize().w, block.x), divUp(inData.maxSize().h, block.y), outData.numImages()); + dim3 x2Grid(divUp(inData.maxSize().w, block.x * 2), divUp(inData.maxSize().h, block.y), outData.numImages()); + dim3 x4Grid(divUp(inData.maxSize().w, block.x * 4), divUp(inData.maxSize().h, block.y), outData.numImages()); + +#ifdef CUDA_DEBUG_LOG + checkCudaErrors(cudaStreamSynchronize(stream)); + checkCudaErrors(cudaGetLastError()); +#endif + + const size_t kernelBytes = static_cast(maxKernelSize.w) * maxKernelSize.h * sizeof(float); + const size_t tileElems = static_cast(block.x + maxKernelSize.w - 1) * (block.y + maxKernelSize.h - 1); + const size_t tileBytes = tileElems * sizeof(D); + const size_t kernelOffset = (tileBytes + sizeof(float) - 1) & ~(sizeof(float) - 1); + const size_t sharedBytes = kernelOffset + kernelBytes; + const size_t x2TileElems = static_cast(block.x * 2 + maxKernelSize.w - 1) * (block.y + maxKernelSize.h - 1); + const size_t x2TileBytes = x2TileElems * sizeof(D); + const size_t x2KernelOffset = (x2TileBytes + sizeof(float) - 1) & ~(sizeof(float) - 1); + const size_t x2SharedBytes = x2KernelOffset + kernelBytes; + const size_t x4TileElems = static_cast(block.x * 4 + maxKernelSize.w - 1) * (block.y + maxKernelSize.h - 1); + const size_t x4TileBytes = x4TileElems * sizeof(D); + const size_t x4KernelOffset = (x4TileBytes + sizeof(float) - 1) & ~(sizeof(float) - 1); + const size_t x4SharedBytes = x4KernelOffset + kernelBytes; + +#define NVCV_GAUSSIAN_VARSHAPE_FILTER_TILED_FIXED_CASE(KSIZE) \ + if (maxKernelSize.w == KSIZE && maxKernelSize.h == KSIZE) \ + { \ + if constexpr (kX4Always) \ + { \ + static_assert(GaussianFixedSharedBytes<4, KSIZE, D>() <= 48 * 1024); \ + gaussianFilter2DTiledFixed<4, KSIZE, KSIZE><<>>( \ + src, dst, kernelTensor, kernelSizeTensor, maxKernelSize, sigmaTensor, dataKernelSize); \ + } \ + else \ + { \ + static_assert(GaussianFixedSharedBytes<2, KSIZE, D>() <= 48 * 1024); \ + gaussianFilter2DTiledFixed<2, KSIZE, KSIZE><<>>( \ + src, dst, kernelTensor, kernelSizeTensor, maxKernelSize, sigmaTensor, dataKernelSize); \ + } \ + } \ + else + + NVCV_GAUSSIAN_VARSHAPE_FILTER_TILED_FIXED_CASE(3) + NVCV_GAUSSIAN_VARSHAPE_FILTER_TILED_FIXED_CASE(5) + if constexpr (kDynamicX4Possible) + { + if (enableX4 && x4SharedBytes <= 48 * 1024) + { + gaussianFilter2DTiledX4<<>>( + src, dst, kernelTensor, kernelSizeTensor, maxKernelSize, sigmaTensor, dataKernelSize); + } + else if (x2SharedBytes <= 48 * 1024) + { + gaussianFilter2DTiledX2<<>>( + src, dst, kernelTensor, kernelSizeTensor, maxKernelSize, sigmaTensor, dataKernelSize); + } + else if (sharedBytes <= 48 * 1024) + { + gaussianFilter2DTiled<<>>(src, dst, kernelTensor, kernelSizeTensor, + maxKernelSize, sigmaTensor, dataKernelSize); + } + else + { + gaussianFilter2D<<>>(src, dst, kernelTensor, kernelSizeTensor, maxKernelSize, + sigmaTensor, dataKernelSize); + } + } + else if (x2SharedBytes <= 48 * 1024) + { + gaussianFilter2DTiledX2<<>>(src, dst, kernelTensor, kernelSizeTensor, + maxKernelSize, sigmaTensor, dataKernelSize); + } + else if (sharedBytes <= 48 * 1024) + { + gaussianFilter2DTiled<<>>(src, dst, kernelTensor, kernelSizeTensor, + maxKernelSize, sigmaTensor, dataKernelSize); + } + else + { + gaussianFilter2D<<>>(src, dst, kernelTensor, kernelSizeTensor, maxKernelSize, + sigmaTensor, dataKernelSize); + } + +#undef NVCV_GAUSSIAN_VARSHAPE_FILTER_TILED_FIXED_CASE + checkKernelErrors(); +#ifdef CUDA_DEBUG_LOG + checkCudaErrors(cudaStreamSynchronize(stream)); + checkCudaErrors(cudaGetLastError()); +#endif +} + +template +void GaussianFilter2DPlanarCaller(const ImageBatchVarShapeDataStridedCuda &inData, + const ImageBatchVarShapeDataStridedCuda &outData, + const cuda::Tensor3DWrap &kernelTensor, + const cuda::Tensor1DWrap &kernelSizeTensor, Size2D maxKernelSize, + const cuda::Tensor1DWrap &sigmaTensor, int dataKernelSize, + int channels, bool enableX4, float borderValue, cudaStream_t stream) +{ + constexpr bool kX4Always = std::is_same_v || std::is_same_v; + + cuda::BorderVarShapeWrap src(inData, cuda::SetAll(borderValue)); + cuda::ImageBatchVarShapeWrap dst(outData); + + dim3 block(kGaussianBlockWidth, kGaussianBlockHeight); + const int64_t tiledGridZ = static_cast(outData.numImages()) * channels; #ifdef CUDA_DEBUG_LOG checkCudaErrors(cudaStreamSynchronize(stream)); checkCudaErrors(cudaGetLastError()); #endif - gaussianFilter2D<<>>(src, dst, kernelTensor, kernelSizeTensor, maxKernelSize, sigmaTensor, - dataKernelSize); + const size_t kernelBytes = static_cast(maxKernelSize.w) * maxKernelSize.h * sizeof(float); + const size_t tileElems = static_cast(block.x + maxKernelSize.w - 1) * (block.y + maxKernelSize.h - 1); + const size_t tileBytes = tileElems * sizeof(D); + const size_t kernelOffset = (tileBytes + sizeof(float) - 1) & ~(sizeof(float) - 1); + const size_t sharedBytes = kernelOffset + kernelBytes; + const size_t x2TileElems = static_cast(block.x * 2 + maxKernelSize.w - 1) * (block.y + maxKernelSize.h - 1); + const size_t x2TileBytes = x2TileElems * sizeof(D); + const size_t x2KernelOffset = (x2TileBytes + sizeof(float) - 1) & ~(sizeof(float) - 1); + const size_t x2SharedBytes = x2KernelOffset + kernelBytes; + const size_t x4TileElems = static_cast(block.x * 4 + maxKernelSize.w - 1) * (block.y + maxKernelSize.h - 1); + const size_t x4TileBytes = x4TileElems * sizeof(D); + const size_t x4KernelOffset = (x4TileBytes + sizeof(float) - 1) & ~(sizeof(float) - 1); + const size_t x4SharedBytes = x4KernelOffset + kernelBytes; + +#define NVCV_GAUSSIAN_VARSHAPE_PLANAR_FILTER_TILED_FIXED_CASE(KSIZE) \ + if (maxKernelSize.w == KSIZE && maxKernelSize.h == KSIZE) \ + { \ + if (tiledGridZ <= 65535) \ + { \ + if constexpr (kX4Always) \ + { \ + static_assert(GaussianFixedSharedBytes<4, KSIZE, D>() <= 48 * 1024); \ + dim3 grid(divUp(outData.maxSize().w, block.x * 4), divUp(outData.maxSize().h, block.y), \ + static_cast(tiledGridZ)); \ + gaussianFilter2DPlanarTiledFixed<4, KSIZE, KSIZE><<>>( \ + src, dst, kernelTensor, kernelSizeTensor, maxKernelSize, sigmaTensor, dataKernelSize, channels); \ + } \ + else \ + { \ + static_assert(GaussianFixedSharedBytes<2, KSIZE, D>() <= 48 * 1024); \ + dim3 grid(divUp(outData.maxSize().w, block.x * 2), divUp(outData.maxSize().h, block.y), \ + static_cast(tiledGridZ)); \ + gaussianFilter2DPlanarTiledFixed<2, KSIZE, KSIZE><<>>( \ + src, dst, kernelTensor, kernelSizeTensor, maxKernelSize, sigmaTensor, dataKernelSize, channels); \ + } \ + } \ + else \ + { \ + dim3 fallbackGrid(divUp(outData.maxSize().w, block.x), divUp(outData.maxSize().h, block.y), \ + outData.numImages()); \ + gaussianFilter2DPlanar<<>>( \ + src, dst, kernelTensor, kernelSizeTensor, maxKernelSize, sigmaTensor, dataKernelSize, channels); \ + } \ + } \ + else + + NVCV_GAUSSIAN_VARSHAPE_PLANAR_FILTER_TILED_FIXED_CASE(3) + NVCV_GAUSSIAN_VARSHAPE_PLANAR_FILTER_TILED_FIXED_CASE(5) + if constexpr (kX4Always) + { + if (enableX4 && x4SharedBytes <= 48 * 1024 && tiledGridZ <= 65535) + { + dim3 grid(divUp(outData.maxSize().w, block.x * 4), divUp(outData.maxSize().h, block.y), + static_cast(tiledGridZ)); + gaussianFilter2DPlanarTiledX4<<>>( + src, dst, kernelTensor, kernelSizeTensor, maxKernelSize, sigmaTensor, dataKernelSize, channels); + } + else if (x2SharedBytes <= 48 * 1024 && tiledGridZ <= 65535) + { + dim3 grid(divUp(outData.maxSize().w, block.x * 2), divUp(outData.maxSize().h, block.y), + static_cast(tiledGridZ)); + gaussianFilter2DPlanarTiledX2<<>>( + src, dst, kernelTensor, kernelSizeTensor, maxKernelSize, sigmaTensor, dataKernelSize, channels); + } + else if (sharedBytes <= 48 * 1024 && tiledGridZ <= 65535) + { + dim3 grid(divUp(outData.maxSize().w, block.x), divUp(outData.maxSize().h, block.y), + static_cast(tiledGridZ)); + gaussianFilter2DPlanarTiled<<>>( + src, dst, kernelTensor, kernelSizeTensor, maxKernelSize, sigmaTensor, dataKernelSize, channels); + } + else + { + dim3 fallbackGrid(divUp(outData.maxSize().w, block.x), divUp(outData.maxSize().h, block.y), + outData.numImages()); + gaussianFilter2DPlanar<<>>( + src, dst, kernelTensor, kernelSizeTensor, maxKernelSize, sigmaTensor, dataKernelSize, channels); + } + } + else if (x2SharedBytes <= 48 * 1024 && tiledGridZ <= 65535) + { + dim3 grid(divUp(outData.maxSize().w, block.x * 2), divUp(outData.maxSize().h, block.y), + static_cast(tiledGridZ)); + gaussianFilter2DPlanarTiledX2<<>>( + src, dst, kernelTensor, kernelSizeTensor, maxKernelSize, sigmaTensor, dataKernelSize, channels); + } + else if (sharedBytes <= 48 * 1024 && tiledGridZ <= 65535) + { + dim3 grid(divUp(outData.maxSize().w, block.x), divUp(outData.maxSize().h, block.y), + static_cast(tiledGridZ)); + gaussianFilter2DPlanarTiled<<>>( + src, dst, kernelTensor, kernelSizeTensor, maxKernelSize, sigmaTensor, dataKernelSize, channels); + } + else + { + dim3 fallbackGrid(divUp(outData.maxSize().w, block.x), divUp(outData.maxSize().h, block.y), + outData.numImages()); + gaussianFilter2DPlanar<<>>( + src, dst, kernelTensor, kernelSizeTensor, maxKernelSize, sigmaTensor, dataKernelSize, channels); + } + +#undef NVCV_GAUSSIAN_VARSHAPE_PLANAR_FILTER_TILED_FIXED_CASE checkKernelErrors(); #ifdef CUDA_DEBUG_LOG checkCudaErrors(cudaStreamSynchronize(stream)); @@ -476,13 +2523,13 @@ void GaussianFilter2D(const ImageBatchVarShapeDataStridedCuda &inData, const Ima const cuda::Tensor3DWrap &kernelTensor, const cuda::Tensor1DWrap &kernelSizeTensor, Size2D maxKernelSize, const cuda::Tensor1DWrap &sigmaTensor, int dataKernelSize, - NVCVBorderType borderMode, float borderValue, cudaStream_t stream) + NVCVBorderType borderMode, float borderValue, bool enableX4, cudaStream_t stream) { typedef void (*func_t)(const ImageBatchVarShapeDataStridedCuda &inData, const ImageBatchVarShapeDataStridedCuda &outData, const cuda::Tensor3DWrap &kernelTensor, const cuda::Tensor1DWrap &kernelSizeTensor, Size2D maxKernelSize, - const cuda::Tensor1DWrap &sigmaTensor, int dataKernelSize, + const cuda::Tensor1DWrap &sigmaTensor, int dataKernelSize, bool enableX4, float borderValue, cudaStream_t stream); static const func_t funcs[] @@ -491,7 +2538,32 @@ void GaussianFilter2D(const ImageBatchVarShapeDataStridedCuda &inData, const Ima GaussianFilter2DCaller}; funcs[borderMode](inData, outData, kernelTensor, kernelSizeTensor, maxKernelSize, sigmaTensor, dataKernelSize, - borderValue, stream); + enableX4, borderValue, stream); +} + +template +void GaussianFilter2DPlanar(const ImageBatchVarShapeDataStridedCuda &inData, + const ImageBatchVarShapeDataStridedCuda &outData, + const cuda::Tensor3DWrap &kernelTensor, + const cuda::Tensor1DWrap &kernelSizeTensor, Size2D maxKernelSize, + const cuda::Tensor1DWrap &sigmaTensor, int dataKernelSize, + NVCVBorderType borderMode, float borderValue, int channels, bool enableX4, + cudaStream_t stream) +{ + typedef void (*func_t)(const ImageBatchVarShapeDataStridedCuda &inData, + const ImageBatchVarShapeDataStridedCuda &outData, + const cuda::Tensor3DWrap &kernelTensor, + const cuda::Tensor1DWrap &kernelSizeTensor, Size2D maxKernelSize, + const cuda::Tensor1DWrap &sigmaTensor, int dataKernelSize, int channels, + bool enableX4, float borderValue, cudaStream_t stream); + + static const func_t funcs[] + = {GaussianFilter2DPlanarCaller, + GaussianFilter2DPlanarCaller, GaussianFilter2DPlanarCaller, + GaussianFilter2DPlanarCaller, GaussianFilter2DPlanarCaller}; + + funcs[borderMode](inData, outData, kernelTensor, kernelSizeTensor, maxKernelSize, sigmaTensor, dataKernelSize, + channels, enableX4, borderValue, stream); } GaussianVarShape::GaussianVarShape(DataShape max_input_shape, DataShape max_output_shape, Size2D maxKernelSize, @@ -502,7 +2574,12 @@ GaussianVarShape::GaussianVarShape(DataShape max_input_shape, DataShape max_outp { if (maxBatchSize > 0) { - NVCV_CHECK_THROW(cudaMalloc(&m_kernel, maxKernelSize.w * maxKernelSize.h * maxBatchSize * sizeof(float))); + const size_t kernelBytes = cvcuda::priv::CheckedMulMany( + {sizeof(float), cvcuda::priv::CheckedPositiveToSize(maxKernelSize.w, "maxKernelSize.w"), + cvcuda::priv::CheckedPositiveToSize(maxKernelSize.h, "maxKernelSize.h"), + cvcuda::priv::CheckedPositiveToSize(maxBatchSize, "maxBatchSize")}, + "GaussianVarShape kernel allocation size overflow"); + NVCV_CHECK_THROW(cudaMalloc(&m_kernel, kernelBytes)); } } @@ -543,12 +2620,15 @@ ErrorCode GaussianVarShape::infer(const ImageBatchVarShapeDataStridedCuda &inDat DataFormat format = input_format; - if (!(format == kNHWC || format == kHWC)) + if (!(format == kNHWC || format == kHWC || format == kNCHW || format == kCHW)) { - LOG_ERROR("Invalid input DataFormat " << format << ", the valid DataFormats are: \"NHWC\", \"HWC\""); + LOG_ERROR("Invalid input DataFormat " << format + << ", the valid DataFormats are: \"NHWC\", \"HWC\", \"NCHW\", \"CHW\""); return ErrorCode::INVALID_DATA_FORMAT; } + const bool isPlanar = IsPlanar(format); + if (!(borderMode == NVCV_BORDER_REFLECT101 || borderMode == NVCV_BORDER_REPLICATE || borderMode == NVCV_BORDER_CONSTANT || borderMode == NVCV_BORDER_REFLECT || borderMode == NVCV_BORDER_WRAP)) { @@ -567,7 +2647,7 @@ ErrorCode GaussianVarShape::infer(const ImageBatchVarShapeDataStridedCuda &inDat const int channels = inData.uniqueFormat().numChannels(); - if (channels > 4) + if (channels > 4 || (isPlanar && channels == 2)) { LOG_ERROR("Invalid channel number " << channels); return ErrorCode::INVALID_DATA_SHAPE; @@ -598,7 +2678,7 @@ ErrorCode GaussianVarShape::infer(const ImageBatchVarShapeDataStridedCuda &inDat const cuda::Tensor3DWrap &kernelTensor, const cuda::Tensor1DWrap &kernelSizeTensor, Size2D maxKernelSize, const cuda::Tensor1DWrap &sigmaTensor, int dataKernelSize, - NVCVBorderType borderMode, float borderValue, cudaStream_t stream); + NVCVBorderType borderMode, float borderValue, bool enableX4, cudaStream_t stream); static const filter2D_t funcs[6][4] = { { GaussianFilter2D, 0, GaussianFilter2D, GaussianFilter2D}, @@ -613,16 +2693,49 @@ ErrorCode GaussianVarShape::infer(const ImageBatchVarShapeDataStridedCuda &inDat NVCV_ASSERT(func != 0); + if (isPlanar) + { + if (outData.numImages() > 65535) + { + LOG_ERROR("Planar Gaussian requires numImages <= 65535 (CUDA grid-z limit)"); + return ErrorCode::INVALID_DATA_SHAPE; + } + + typedef void (*planar_filter2D_t)( + const ImageBatchVarShapeDataStridedCuda &inData, const ImageBatchVarShapeDataStridedCuda &outData, + const cuda::Tensor3DWrap &kernelTensor, + const cuda::Tensor1DWrap &kernelSizeTensor, Size2D maxKernelSize, + const cuda::Tensor1DWrap &sigmaTensor, int dataKernelSize, NVCVBorderType borderMode, + float borderValue, int channels, bool enableX4, cudaStream_t stream); + + static const planar_filter2D_t planarFuncs[6] = { + GaussianFilter2DPlanar, 0, + GaussianFilter2DPlanar, GaussianFilter2DPlanar, + GaussianFilter2DPlanar, GaussianFilter2DPlanar, + }; + + const planar_filter2D_t planarFunc = planarFuncs[data_type]; + NVCV_ASSERT(planarFunc != 0); + + const bool enableX4 = data_type == kCV_8U || data_type == kCV_32F; + planarFunc(inData, outData, kernelTensor, kernelSizeTensor, m_maxKernelSize, sigmaTensor, dataKernelSize, + borderMode, borderValue, channels, enableX4, stream); + + return ErrorCode::SUCCESS; + } + + const bool enableX4 = data_type == kCV_8U || channels == 1 + || (data_type == kCV_32F && channels == 3 && m_maxKernelSize.w > 5 && m_maxKernelSize.h > 5); func(inData, outData, kernelTensor, kernelSizeTensor, m_maxKernelSize, sigmaTensor, dataKernelSize, borderMode, - borderValue, stream); + borderValue, enableX4, stream); return ErrorCode::SUCCESS; } // AverageBlurVarShape --------------------------------------------------------- -template -__global__ void avgBlurFilter2D(const SrcWrapper src, DstWrapper dst, cuda::Tensor3DWrap kernel, +template +__global__ void avgBlurFilter2D(const BorderWrapper src, const SrcWrapper srcRaw, DstWrapper dst, cuda::Tensor1DWrap kernelSizeArr, cuda::Tensor1DWrap kernelAnchorArr) { @@ -637,10 +2750,60 @@ __global__ void avgBlurFilter2D(const SrcWrapper src, DstWrapper dst, cuda::Tens return; int2 kernelSize = kernelSizeArr[batch_idx]; + NVCV_CUDA_ASSERT(kernelSize.x > 0 && kernelSize.y > 0, "E Wrong kernelSize=(%d,%d), expected both > 0", + kernelSize.x, kernelSize.y); int2 anchor = kernelAnchorArr[batch_idx]; + if (anchor.x < 0) + anchor.x = kernelSize.x / 2; + if (anchor.y < 0) + anchor.y = kernelSize.y / 2; - int3 srcCoord{0, 0, batch_idx}; + if (kernelSize.x == 5 && kernelSize.y == 5 && anchor.x == 2 && anchor.y == 2) + { + constexpr float kernelValue = static_cast(1.0 / 25); + int3 srcCoord{0, 0, batch_idx}; + + if (x >= 2 && x < dst.width(batch_idx) - 2 && y >= 2 && y < dst.height(batch_idx) - 2) + { +#pragma unroll + for (int i = 0; i < 5; ++i) + { + srcCoord.y = y - 2 + i; + +#pragma unroll + for (int j = 0; j < 5; ++j) + { + srcCoord.x = x - 2 + j; + + res = res + srcRaw[srcCoord] * kernelValue; + } + } + + *dst.ptr(batch_idx, y, x) = cuda::SaturateCast(res); + return; + } + +#pragma unroll + for (int i = 0; i < 5; ++i) + { + srcCoord.y = y - 2 + i; + +#pragma unroll + for (int j = 0; j < 5; ++j) + { + srcCoord.x = x - 2 + j; + + res = res + src[srcCoord] * kernelValue; + } + } + + *dst.ptr(batch_idx, y, x) = cuda::SaturateCast(res); + return; + } + + const float kernelValue = static_cast(1.0 / (kernelSize.x * kernelSize.y)); + int3 srcCoord{0, 0, batch_idx}; for (int i = 0; i < kernelSize.y; ++i) { @@ -650,27 +2813,124 @@ __global__ void avgBlurFilter2D(const SrcWrapper src, DstWrapper dst, cuda::Tens { srcCoord.x = x - anchor.x + j; - res = res + src[srcCoord] * (*kernel.ptr(batch_idx, i, j)); + res = res + src[srcCoord] * kernelValue; } } *dst.ptr(batch_idx, y, x) = cuda::SaturateCast(res); } +template +__global__ void avgBlurFilter2DPlanar(const BorderWrapper src, const SrcWrapper srcRaw, DstWrapper dst, + cuda::Tensor1DWrap kernelSizeArr, + cuda::Tensor1DWrap kernelAnchorArr, int channels) +{ + using T = typename DstWrapper::ValueType; + using work_type = cuda::ConvertBaseTypeTo; + + const int x = blockIdx.x * blockDim.x + threadIdx.x; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + + if (x >= dst.width(batch_idx, 0) || y >= dst.height(batch_idx, 0)) + return; + + int2 kernelSize = kernelSizeArr[batch_idx]; + NVCV_CUDA_ASSERT(kernelSize.x > 0 && kernelSize.y > 0, "E Wrong kernelSize=(%d,%d), expected both > 0", + kernelSize.x, kernelSize.y); + + int2 anchor = kernelAnchorArr[batch_idx]; + if (anchor.x < 0) + anchor.x = kernelSize.x / 2; + if (anchor.y < 0) + anchor.y = kernelSize.y / 2; + + if (kernelSize.x == 5 && kernelSize.y == 5 && anchor.x == 2 && anchor.y == 2) + { + constexpr float kernelValue = static_cast(1.0 / 25); + const bool isInterior = x >= 2 && x < dst.width(batch_idx, 0) - 2 && y >= 2 && y < dst.height(batch_idx, 0) - 2; + + for (int plane = 0; plane < channels; ++plane) + { + work_type res = cuda::SetAll(0); + int4 srcCoord = {0, 0, plane, batch_idx}; + + if (isInterior) + { +#pragma unroll + for (int i = 0; i < 5; ++i) + { + srcCoord.y = y - 2 + i; + +#pragma unroll + for (int j = 0; j < 5; ++j) + { + srcCoord.x = x - 2 + j; + + res = res + srcRaw[srcCoord] * kernelValue; + } + } + } + else + { +#pragma unroll + for (int i = 0; i < 5; ++i) + { + srcCoord.y = y - 2 + i; + +#pragma unroll + for (int j = 0; j < 5; ++j) + { + srcCoord.x = x - 2 + j; + + res = res + src[srcCoord] * kernelValue; + } + } + } + + *dst.ptr(batch_idx, plane, y, x) = cuda::SaturateCast(res); + } + + return; + } + + const float kernelValue = static_cast(1.0 / (kernelSize.x * kernelSize.y)); + + for (int plane = 0; plane < channels; ++plane) + { + work_type res = cuda::SetAll(0); + int4 srcCoord = {0, 0, plane, batch_idx}; + + for (int i = 0; i < kernelSize.y; ++i) + { + srcCoord.y = y - anchor.y + i; + + for (int j = 0; j < kernelSize.x; ++j) + { + srcCoord.x = x - anchor.x + j; + + res = res + src[srcCoord] * kernelValue; + } + } + + *dst.ptr(batch_idx, plane, y, x) = cuda::SaturateCast(res); + } +} + template -void AverageBlurFilter2DCaller(const ImageBatchVarShapeDataStridedCuda &inData, - const ImageBatchVarShapeDataStridedCuda &outData, - const cuda::Tensor3DWrap &kernelTensor, - const cuda::Tensor1DWrap &kernelSizeTensor, +void AverageBlurFilter2DCaller(const ImageBatchVarShapeDataStridedCuda &inData, + const ImageBatchVarShapeDataStridedCuda &outData, + const cuda::Tensor1DWrap &kernelSizeTensor, const cuda::Tensor1DWrap &kernelAnchorTensor, float borderValue, cudaStream_t stream) { - cuda::BorderVarShapeWrap src(inData, cuda::SetAll(borderValue)); - cuda::ImageBatchVarShapeWrap dst(outData); + cuda::BorderVarShapeWrap src(inData, cuda::SetAll(borderValue)); + cuda::ImageBatchVarShapeWrap srcRaw(inData); + cuda::ImageBatchVarShapeWrap dst(outData); using work_type = cuda::ConvertBaseTypeTo; - dim3 block(16, 16); + dim3 block(32, 8); dim3 grid(divUp(inData.maxSize().w, block.x), divUp(inData.maxSize().h, block.y), outData.numImages()); #ifdef CUDA_DEBUG_LOG @@ -678,7 +2938,34 @@ void AverageBlurFilter2DCaller(const ImageBatchVarShapeDataStridedCuda &inData, checkCudaErrors(cudaGetLastError()); #endif - avgBlurFilter2D<<>>(src, dst, kernelTensor, kernelSizeTensor, kernelAnchorTensor); + avgBlurFilter2D<<>>(src, srcRaw, dst, kernelSizeTensor, kernelAnchorTensor); + checkKernelErrors(); +#ifdef CUDA_DEBUG_LOG + checkCudaErrors(cudaStreamSynchronize(stream)); + checkCudaErrors(cudaGetLastError()); +#endif +} + +template +void AverageBlurFilter2DPlanarCaller(const ImageBatchVarShapeDataStridedCuda &inData, + const ImageBatchVarShapeDataStridedCuda &outData, + const cuda::Tensor1DWrap &kernelSizeTensor, + const cuda::Tensor1DWrap &kernelAnchorTensor, int channels, + float borderValue, cudaStream_t stream) +{ + cuda::BorderVarShapeWrap src(inData, cuda::SetAll(borderValue)); + cuda::ImageBatchVarShapeWrap srcRaw(inData); + cuda::ImageBatchVarShapeWrap dst(outData); + + dim3 block(32, 8); + dim3 grid(divUp(outData.maxSize().w, block.x), divUp(outData.maxSize().h, block.y), outData.numImages()); + +#ifdef CUDA_DEBUG_LOG + checkCudaErrors(cudaStreamSynchronize(stream)); + checkCudaErrors(cudaGetLastError()); +#endif + + avgBlurFilter2DPlanar<<>>(src, srcRaw, dst, kernelSizeTensor, kernelAnchorTensor, channels); checkKernelErrors(); #ifdef CUDA_DEBUG_LOG checkCudaErrors(cudaStreamSynchronize(stream)); @@ -687,17 +2974,15 @@ void AverageBlurFilter2DCaller(const ImageBatchVarShapeDataStridedCuda &inData, } template -void AverageBlurFilter2D(const ImageBatchVarShapeDataStridedCuda &inData, - const ImageBatchVarShapeDataStridedCuda &outData, - const cuda::Tensor3DWrap &kernelTensor, - const cuda::Tensor1DWrap &kernelSizeTensor, +void AverageBlurFilter2D(const ImageBatchVarShapeDataStridedCuda &inData, + const ImageBatchVarShapeDataStridedCuda &outData, + const cuda::Tensor1DWrap &kernelSizeTensor, const cuda::Tensor1DWrap &kernelAnchorTensor, NVCVBorderType borderMode, float borderValue, cudaStream_t stream) { typedef void (*func_t)( const ImageBatchVarShapeDataStridedCuda &inData, const ImageBatchVarShapeDataStridedCuda &outData, - const cuda::Tensor3DWrap &kernelTensor, - const cuda::Tensor1DWrap &kernelSizeTensor, + const cuda::Tensor1DWrap &kernelSizeTensor, const cuda::Tensor1DWrap &kernelAnchorTensor, float borderValue, cudaStream_t stream); static const func_t funcs[] @@ -705,7 +2990,29 @@ void AverageBlurFilter2D(const ImageBatchVarShapeDataStridedCuda &inData, AverageBlurFilter2DCaller, AverageBlurFilter2DCaller, AverageBlurFilter2DCaller}; - funcs[borderMode](inData, outData, kernelTensor, kernelSizeTensor, kernelAnchorTensor, borderValue, stream); + funcs[borderMode](inData, outData, kernelSizeTensor, kernelAnchorTensor, borderValue, stream); +} + +template +void AverageBlurFilter2DPlanar(const ImageBatchVarShapeDataStridedCuda &inData, + const ImageBatchVarShapeDataStridedCuda &outData, + const cuda::Tensor1DWrap &kernelSizeTensor, + const cuda::Tensor1DWrap &kernelAnchorTensor, NVCVBorderType borderMode, + float borderValue, int channels, cudaStream_t stream) +{ + typedef void (*func_t)(const ImageBatchVarShapeDataStridedCuda &inData, + const ImageBatchVarShapeDataStridedCuda &outData, + const cuda::Tensor1DWrap &kernelSizeTensor, + const cuda::Tensor1DWrap &kernelAnchorTensor, int channels, float borderValue, + cudaStream_t stream); + + static const func_t funcs[] = {AverageBlurFilter2DPlanarCaller, + AverageBlurFilter2DPlanarCaller, + AverageBlurFilter2DPlanarCaller, + AverageBlurFilter2DPlanarCaller, + AverageBlurFilter2DPlanarCaller}; + + funcs[borderMode](inData, outData, kernelSizeTensor, kernelAnchorTensor, channels, borderValue, stream); } AverageBlurVarShape::AverageBlurVarShape(DataShape max_input_shape, DataShape max_output_shape, Size2D maxKernelSize, @@ -714,16 +3021,9 @@ AverageBlurVarShape::AverageBlurVarShape(DataShape max_input_shape, DataShape ma , m_maxKernelSize(maxKernelSize) , m_maxBatchSize(maxBatchSize) { - if (maxBatchSize > 0) - { - NVCV_CHECK_THROW(cudaMalloc(&m_kernel, maxKernelSize.w * maxKernelSize.h * maxBatchSize * sizeof(float))); - } } -AverageBlurVarShape::~AverageBlurVarShape() -{ - NVCV_CHECK_LOG(cudaFree(m_kernel)); -} +AverageBlurVarShape::~AverageBlurVarShape() {} ErrorCode AverageBlurVarShape::infer(const ImageBatchVarShapeDataStridedCuda &inData, const ImageBatchVarShapeDataStridedCuda &outData, @@ -757,12 +3057,15 @@ ErrorCode AverageBlurVarShape::infer(const ImageBatchVarShapeDataStridedCuda &in DataFormat format = input_format; - if (!(format == kNHWC || format == kHWC)) + if (!(format == kNHWC || format == kHWC || format == kNCHW || format == kCHW)) { - LOG_ERROR("Invalid input DataFormat " << format << ", the valid DataFormats are: \"NHWC\", \"HWC\""); + LOG_ERROR("Invalid input DataFormat " << format + << ", the valid DataFormats are: \"NHWC\", \"HWC\", \"NCHW\", \"CHW\""); return ErrorCode::INVALID_DATA_FORMAT; } + const bool isPlanar = IsPlanar(format); + if (!(borderMode == NVCV_BORDER_REFLECT101 || borderMode == NVCV_BORDER_REPLICATE || borderMode == NVCV_BORDER_CONSTANT || borderMode == NVCV_BORDER_REFLECT || borderMode == NVCV_BORDER_WRAP)) { @@ -781,7 +3084,7 @@ ErrorCode AverageBlurVarShape::infer(const ImageBatchVarShapeDataStridedCuda &in const int channels = inData.uniqueFormat().numChannels(); - if (channels > 4) + if (channels > 4 || (isPlanar && channels == 2)) { LOG_ERROR("Invalid channel number " << channels); return ErrorCode::INVALID_DATA_SHAPE; @@ -789,24 +3092,12 @@ ErrorCode AverageBlurVarShape::infer(const ImageBatchVarShapeDataStridedCuda &in float borderValue = .0f; - dim3 block(32, 4); - dim3 grid(divUp(m_maxKernelSize.w, block.x), divUp(m_maxKernelSize.h, block.y), outData.numImages()); - cuda::Tensor1DWrap kernelSizeTensor(kernelSize); cuda::Tensor1DWrap kernelAnchorTensor(kernelAnchor); - int kernelPitch2 = static_cast(m_maxKernelSize.w * sizeof(float)); - int kernelPitch1 = m_maxKernelSize.h * kernelPitch2; - - cuda::Tensor3DWrap kernelTensor(m_kernel, kernelPitch1, kernelPitch2); - - computeMeanKernelVarShape<<>>(kernelTensor, kernelSizeTensor, kernelAnchorTensor); - - checkKernelErrors(); // clang-format off typedef void (*filter2D_t)(const ImageBatchVarShapeDataStridedCuda &inData, const ImageBatchVarShapeDataStridedCuda &outData, - const cuda::Tensor3DWrap &kernelTensor, const cuda::Tensor1DWrap &kernelSizeTensor, const cuda::Tensor1DWrap &kernelAnchorTensor, NVCVBorderType borderMode, @@ -827,7 +3118,36 @@ ErrorCode AverageBlurVarShape::infer(const ImageBatchVarShapeDataStridedCuda &in NVCV_ASSERT(func != 0); - func(inData, outData, kernelTensor, kernelSizeTensor, kernelAnchorTensor, borderMode, borderValue, stream); + if (isPlanar) + { + if (outData.numImages() > 65535) + { + LOG_ERROR("Planar AverageBlur requires numImages <= 65535 (CUDA grid-z limit)"); + return ErrorCode::INVALID_DATA_SHAPE; + } + + typedef void (*planar_filter2D_t)( + const ImageBatchVarShapeDataStridedCuda &inData, const ImageBatchVarShapeDataStridedCuda &outData, + const cuda::Tensor1DWrap &kernelSizeTensor, + const cuda::Tensor1DWrap &kernelAnchorTensor, NVCVBorderType borderMode, float borderValue, + int channels, cudaStream_t stream); + + static const planar_filter2D_t planarFuncs[6] = { + AverageBlurFilter2DPlanar, 0, + AverageBlurFilter2DPlanar, AverageBlurFilter2DPlanar, + AverageBlurFilter2DPlanar, AverageBlurFilter2DPlanar, + }; + + const planar_filter2D_t planarFunc = planarFuncs[data_type]; + + NVCV_ASSERT(planarFunc != 0); + + planarFunc(inData, outData, kernelSizeTensor, kernelAnchorTensor, borderMode, borderValue, channels, stream); + + return ErrorCode::SUCCESS; + } + + func(inData, outData, kernelSizeTensor, kernelAnchorTensor, borderMode, borderValue, stream); return ErrorCode::SUCCESS; } diff --git a/src/cvcuda/priv/legacy/flip.cu b/src/cvcuda/priv/legacy/flip.cu index 7d37f325e..2bef06066 100644 --- a/src/cvcuda/priv/legacy/flip.cu +++ b/src/cvcuda/priv/legacy/flip.cu @@ -1,4 +1,4 @@ -/* Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +/* Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * * SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES * SPDX-License-Identifier: Apache-2.0 @@ -19,91 +19,140 @@ * limitations under the License. */ +#include "../PlanarTensorView.hpp" #include "CvCudaLegacy.h" #include "CvCudaLegacyHelpers.hpp" #include "CvCudaUtils.cuh" +#include + +#include + namespace nvcv::legacy::cuda_op { using namespace nvcv::legacy::helpers; -template +// Flip is a pure pixel remap, so it is memory/latency-bound: one element per thread leaves too few +// memory requests in flight (ncu on the single-channel uchar kernel: DRAM 45%, "latency issues"). +// Each thread instead processes NIX columns strided by the total x-thread count, so NIX independent +// loads/stores are issued together (memory-level parallelism) while consecutive threads still touch +// consecutive columns (coalesced). NIX>1 needs the grid sized to divUp(width, NIX) x-threads. The +// remap is unchanged, so every output is bit-exact with the one-element-per-thread version. +constexpr int kFlipNIX = 4; +constexpr int kFlipScalarNIX = 1; + +inline bool CurrentDeviceIsSM86() +{ + int dev = 0; + if (cudaGetDevice(&dev) != cudaSuccess) + return false; + + cudaDeviceProp prop{}; + if (cudaGetDeviceProperties(&prop, dev) != cudaSuccess) + return false; + + return prop.major == 8 && prop.minor == 6; +} + +template __global__ void flipHorizontal(SrcWrapper src, DstWrapper dst, Size2D dstSize) { - const int32_t dst_x = blockIdx.x * blockDim.x + threadIdx.x; const int32_t dst_y = blockIdx.y * blockDim.y + threadIdx.y; const int32_t batch_idx = get_batch_idx(); + if (dst_y >= dstSize.h) + return; - int32_t out_height = dstSize.h; - int32_t out_width = dstSize.w; + const int32_t x0 = blockIdx.x * blockDim.x + threadIdx.x; + const int32_t stride = gridDim.x * blockDim.x; + const int32_t width = dstSize.w; - if (dst_x < out_width && dst_y < out_height) +#pragma unroll + for (int i = 0; i < NIX; ++i) { - *dst.ptr(batch_idx, dst_y, dst_x) = *src.ptr(batch_idx, dst_y, (out_width - 1 - dst_x)); + const int32_t dst_x = x0 + i * stride; + if (dst_x < width) + { + *dst.ptr(batch_idx, dst_y, dst_x) = *src.ptr(batch_idx, dst_y, (width - 1 - dst_x)); + } } } -template +template __global__ void flipVertical(SrcWrapper src, DstWrapper dst, Size2D dstSize) { - const int32_t dst_x = blockIdx.x * blockDim.x + threadIdx.x; const int32_t dst_y = blockIdx.y * blockDim.y + threadIdx.y; const int32_t batch_idx = get_batch_idx(); + if (dst_y >= dstSize.h) + return; - int32_t out_height = dstSize.h; - int32_t out_width = dstSize.w; + const int32_t x0 = blockIdx.x * blockDim.x + threadIdx.x; + const int32_t stride = gridDim.x * blockDim.x; + const int32_t width = dstSize.w; + const int32_t src_y = dstSize.h - 1 - dst_y; - if (dst_x < out_width && dst_y < out_height) +#pragma unroll + for (int i = 0; i < NIX; ++i) { - *dst.ptr(batch_idx, dst_y, dst_x) = *src.ptr(batch_idx, (out_height - 1 - dst_y), dst_x); + const int32_t dst_x = x0 + i * stride; + if (dst_x < width) + { + *dst.ptr(batch_idx, dst_y, dst_x) = *src.ptr(batch_idx, src_y, dst_x); + } } } -template +template __global__ void flipHorizontalVertical(SrcWrapper src, DstWrapper dst, Size2D dstSize) { - const int32_t dst_x = blockIdx.x * blockDim.x + threadIdx.x; const int32_t dst_y = blockIdx.y * blockDim.y + threadIdx.y; const int32_t batch_idx = get_batch_idx(); + if (dst_y >= dstSize.h) + return; - int32_t out_height = dstSize.h; - int32_t out_width = dstSize.w; + const int32_t x0 = blockIdx.x * blockDim.x + threadIdx.x; + const int32_t stride = gridDim.x * blockDim.x; + const int32_t width = dstSize.w; + const int32_t src_y = dstSize.h - 1 - dst_y; - if (dst_x < out_width && dst_y < out_height) +#pragma unroll + for (int i = 0; i < NIX; ++i) { - *dst.ptr(batch_idx, dst_y, dst_x) = *src.ptr(batch_idx, (out_height - 1 - dst_y), (out_width - 1 - dst_x)); + const int32_t dst_x = x0 + i * stride; + if (dst_x < width) + { + *dst.ptr(batch_idx, dst_y, dst_x) = *src.ptr(batch_idx, src_y, (width - 1 - dst_x)); + } } } -template +template void runFlipKernel(SrcWrap src, DstWrap dst, Size2D dstSize, int numSamples, int32_t flipCode, cudaStream_t stream) { - constexpr uint32_t BLOCK = 32; - - dim3 blockSize(BLOCK, BLOCK / 4, 1); - dim3 gridSize(divUp(dstSize.w, blockSize.x), divUp(dstSize.h, blockSize.y), numSamples); + dim3 blockSize(32, 8, 1); + dim3 gridSize(divUp(divUp(dstSize.w, NIX), static_cast(blockSize.x)), divUp(dstSize.h, blockSize.y), + numSamples); if (flipCode > 0) { - flipHorizontal<<>>(src, dst, dstSize); + flipHorizontal<<>>(src, dst, dstSize); checkKernelErrors(); } else if (flipCode == 0) { - flipVertical<<>>(src, dst, dstSize); + flipVertical<<>>(src, dst, dstSize); checkKernelErrors(); } else { - flipHorizontalVertical<<>>(src, dst, dstSize); + flipHorizontalVertical<<>>(src, dst, dstSize); checkKernelErrors(); } } -template -ErrorCode flip(const TensorDataStridedCuda &input, const TensorDataStridedCuda &output, const int32_t flipCode, - cudaStream_t stream) +template +ErrorCode flipImpl(const TensorDataStridedCuda &input, const TensorDataStridedCuda &output, const int32_t flipCode, + cudaStream_t stream) { auto outAccess = TensorDataAccessStridedImagePlanar::Create(output); NVCV_ASSERT(outAccess); @@ -120,7 +169,7 @@ ErrorCode flip(const TensorDataStridedCuda &input, const TensorDataStridedCuda & auto src = cuda::CreateTensorWrapNHW(input); auto dst = cuda::CreateTensorWrapNHW(output); - runFlipKernel(src, dst, dstSize, outAccess->numSamples(), flipCode, stream); + runFlipKernel(src, dst, dstSize, outAccess->numSamples(), flipCode, stream); } else { @@ -136,6 +185,188 @@ ErrorCode flip(const TensorDataStridedCuda &input, const TensorDataStridedCuda & return ErrorCode::SUCCESS; } +template +ErrorCode flip(const TensorDataStridedCuda &input, const TensorDataStridedCuda &output, const int32_t flipCode, + cudaStream_t stream) +{ + return flipImpl(input, output, flipCode, stream); +} + +template +ErrorCode flipScalar(const TensorDataStridedCuda &input, const TensorDataStridedCuda &output, const int32_t flipCode, + cudaStream_t stream) +{ + return flipImpl(input, output, flipCode, stream); +} + +// Wide single-channel flip: a run of VEC contiguous columns is loaded/stored as one MakeType +// vector. A single-channel uchar plane moves only 1 byte/thread, so even with NIX MLP the kernel pays +// the per-element index/branch overhead VEC times more than it must; coalescing VEC columns into one +// wide access cuts that overhead ~VEC x and raises bytes/thread. Vertical flip keeps the column order, +// so the source vector is the same column block in row (H-1-y) -- a direct wide copy. Horizontal flip +// reverses the columns, so a destination block [j*VEC .. j*VEC+VEC-1] maps to the mirrored source block +// (W/VEC-1-j) with its VEC lanes reversed in registers; this is exact only when the column count is a +// multiple of VEC (caller-checked) so the block boundaries line up. The result is bit-identical to the +// scalar single-channel flip. +template +__device__ __forceinline__ WideT flipLanes(WideT v) +{ + WideT out; +#pragma unroll + for (int k = 0; k < VEC; ++k) + { + cuda::GetElement(out, k) = cuda::GetElement(v, VEC - 1 - k); + } + return out; +} + +template +__global__ void flipWideHorizontal(SrcWrapper src, DstWrapper dst, Size2D vecSize) +{ + const int32_t dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int32_t batch_idx = get_batch_idx(); + if (dst_y >= vecSize.h) + return; + + const int32_t x0 = blockIdx.x * blockDim.x + threadIdx.x; + const int32_t stride = gridDim.x * blockDim.x; + const int32_t wvec = vecSize.w; + +#pragma unroll + for (int i = 0; i < NIX; ++i) + { + const int32_t dst_x = x0 + i * stride; + if (dst_x < wvec) + { + *dst.ptr(batch_idx, dst_y, dst_x) = flipLanes(*src.ptr(batch_idx, dst_y, (wvec - 1 - dst_x))); + } + } +} + +template +__global__ void flipWideVertical(SrcWrapper src, DstWrapper dst, Size2D vecSize) +{ + const int32_t dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int32_t batch_idx = get_batch_idx(); + if (dst_y >= vecSize.h) + return; + + const int32_t x0 = blockIdx.x * blockDim.x + threadIdx.x; + const int32_t stride = gridDim.x * blockDim.x; + const int32_t wvec = vecSize.w; + const int32_t src_y = vecSize.h - 1 - dst_y; + +#pragma unroll + for (int i = 0; i < NIX; ++i) + { + const int32_t dst_x = x0 + i * stride; + if (dst_x < wvec) + { + *dst.ptr(batch_idx, dst_y, dst_x) = *src.ptr(batch_idx, src_y, dst_x); + } + } +} + +template +__global__ void flipWideHorizontalVertical(SrcWrapper src, DstWrapper dst, Size2D vecSize) +{ + const int32_t dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int32_t batch_idx = get_batch_idx(); + if (dst_y >= vecSize.h) + return; + + const int32_t x0 = blockIdx.x * blockDim.x + threadIdx.x; + const int32_t stride = gridDim.x * blockDim.x; + const int32_t wvec = vecSize.w; + const int32_t src_y = vecSize.h - 1 - dst_y; + +#pragma unroll + for (int i = 0; i < NIX; ++i) + { + const int32_t dst_x = x0 + i * stride; + if (dst_x < wvec) + { + *dst.ptr(batch_idx, dst_y, dst_x) = flipLanes(*src.ptr(batch_idx, src_y, (wvec - 1 - dst_x))); + } + } +} + +// True when the single-channel flip can use VEC-wide contiguous vectorization: cols divisible by VEC, +// and both base pointers + row/sample strides aligned to the VEC-wide vector type. +template +static bool wideSingleChannelEligible(const TensorDataStridedCuda &input, const TensorDataStridedCuda &output) +{ + auto in = TensorDataAccessStridedImagePlanar::Create(input); + auto out = TensorDataAccessStridedImagePlanar::Create(output); + if (!in || !out) + return false; + if (in->numCols() % VEC != 0) + return false; + const std::uintptr_t a = alignof(cuda::MakeType); + auto ok = [a](std::uintptr_t base, int64_t rowS, int64_t smpS) + { + return ((base | static_cast(rowS) | static_cast(smpS)) & (a - 1)) == 0; + }; + return ok(reinterpret_cast(input.basePtr()), in->rowStride(), in->sampleStride()) + && ok(reinterpret_cast(output.basePtr()), out->rowStride(), out->sampleStride()); +} + +template +ErrorCode flipWideSingleChannel(const TensorDataStridedCuda &input, const TensorDataStridedCuda &output, + const int32_t flipCode, cudaStream_t stream) +{ + using WideT = cuda::MakeType; + + auto inAccess = TensorDataAccessStridedImagePlanar::Create(input); + auto outAccess = TensorDataAccessStridedImagePlanar::Create(output); + NVCV_ASSERT(inAccess); + NVCV_ASSERT(outAccess); + + // Each "pixel" spans VEC contiguous columns; numCols is divisible by VEC (caller-checked). + Size2D vecSize{outAccess->numCols() / VEC, outAccess->numRows()}; + int64_t inMaxStride = inAccess->sampleStride() * inAccess->numSamples(); + int64_t outMaxStride = outAccess->sampleStride() * outAccess->numSamples(); + if (std::max(inMaxStride, outMaxStride) > cuda::TypeTraits::max) + { + LOG_ERROR("Input or output size exceeds " << cuda::TypeTraits::max << ". Tensor is too large."); + return ErrorCode::INVALID_PARAMETER; + } + + auto src = cuda::CreateTensorWrapNHW(input); + auto dst = cuda::CreateTensorWrapNHW(output); + + dim3 blockSize(32, 8, 1); + dim3 gridSize(divUp(divUp(vecSize.w, kFlipNIX), static_cast(blockSize.x)), divUp(vecSize.h, blockSize.y), + outAccess->numSamples()); + + if (flipCode > 0) + { + flipWideHorizontal<<>>(src, dst, vecSize); + } + else if (flipCode == 0) + { + flipWideVertical<<>>(src, dst, vecSize); + } + else + { + flipWideHorizontalVertical<<>>(src, dst, vecSize); + } + checkKernelErrors(); + return ErrorCode::SUCCESS; +} + +// Single-channel flip: use VEC-wide contiguous vectorization when the columns are VEC-divisible and the +// buffers are vector-aligned, otherwise fall back to the scalar single-channel kernel. Used by the +// interleaved C==1 path and by the planar path (each NCHW plane is a single-channel image). +template +ErrorCode flipSingleChannel(const TensorDataStridedCuda &input, const TensorDataStridedCuda &output, + const int32_t flipCode, cudaStream_t stream) +{ + if (wideSingleChannelEligible(input, output)) + return flipWideSingleChannel(input, output, flipCode, stream); + return flip(input, output, flipCode, stream); +} + ErrorCode Flip::infer(const TensorDataStridedCuda &input, const TensorDataStridedCuda &output, const int32_t flipCode, cudaStream_t stream) { @@ -154,12 +385,15 @@ ErrorCode Flip::infer(const TensorDataStridedCuda &input, const TensorDataStride } DataFormat format = inputFormat; - if (!(format == kNHWC || format == kHWC)) + if (!(format == kNHWC || format == kHWC || format == kNCHW || format == kCHW)) { - LOG_ERROR("Invalid input DataFormat " << format << ", the valid DataFormats are: \"NHWC\", \"HWC\""); + LOG_ERROR("Invalid input DataFormat " << format + << ", the valid DataFormats are: \"NHWC\", \"HWC\", \"NCHW\", \"CHW\""); return ErrorCode::INVALID_DATA_FORMAT; } + const bool isPlanar = (format == kNCHW || format == kCHW); + cuda_op::DataType dataType = GetLegacyDataType(input.dtype()); if (!(dataType == kCV_8U || dataType == kCV_16U || dataType == kCV_32S || dataType == kCV_32F)) { @@ -171,7 +405,9 @@ ErrorCode Flip::infer(const TensorDataStridedCuda &input, const TensorDataStride NVCV_ASSERT(inputWrapper); cuda_op::DataShape inputShape = GetLegacyDataShape(inputWrapper->infoShape()); - if (inputShape.C > 4) + // Planar 2-channel layout is rejected (no defined 2-plane format); the interleaved path likewise + // does not support 2 channels (matches the Normalize and Resize operators). + if (inputShape.C > 4 || inputShape.C == 2) { LOG_ERROR("Invalid channel number " << inputShape.C); return ErrorCode::INVALID_DATA_SHAPE; @@ -183,16 +419,54 @@ ErrorCode Flip::infer(const TensorDataStridedCuda &input, const TensorDataStride typedef ErrorCode (*flip_t)(const TensorDataStridedCuda &input, const TensorDataStridedCuda &output, const int32_t flipCode, cudaStream_t stream); + // Column 0 (single channel) routes through flipSingleChannel, which picks the wide-vectorized + // kernel when eligible and otherwise the scalar single-channel kernel. The planar path also uses + // column 0 (each NCHW plane is a single-channel image). static const flip_t funcs[6][4] = { - { flip, 0, flip, flip}, - { 0, 0, 0, 0}, - { flip, 0, flip, flip}, - { 0, 0, 0, 0}, - {flip, 0, flip, flip}, - { flip, 0, flip, flip} + { flipSingleChannel, 0, flip, flip}, + { 0, 0, 0, 0}, + { flipSingleChannel, 0, flip, flip}, + { 0, 0, 0, 0}, + {flipSingleChannel, 0, flip, flip}, + { flipSingleChannel, 0, flipScalar, flip} }; const int32_t channels = inputShape.C; + + if (isPlanar) + { + // View each of the N*C channel planes as a single-channel sample and reuse the interleaved + // single-channel flip kernel (funcs column 0). Channels are independent in flip, so this is + // bit-exact with flipping the equivalent NHWC single-channel data. The flattened plane count + // becomes the kernel's grid.z, capped at CUDA's 65535 limit; compute it in 64-bit to avoid + // overflow. The flattened view is only valid when the planes are tightly packed across samples. + auto outputWrapper = TensorDataAccessStridedImagePlanar::Create(output); + NVCV_ASSERT(outputWrapper); + + const int64_t numSamples = inputWrapper->numSamples(); + if (numSamples > 1 + && (inputWrapper->sampleStride() != static_cast(channels) * inputWrapper->chStride() + || outputWrapper->sampleStride() != static_cast(channels) * outputWrapper->chStride())) + { + LOG_ERROR("Planar flip of a batched tensor requires tightly packed channel planes"); + return ErrorCode::INVALID_DATA_SHAPE; + } + if (numSamples * channels > 65535) + { + LOG_ERROR("Planar flip requires numSamples * numChannels <= 65535 (CUDA grid-z limit)"); + return ErrorCode::INVALID_DATA_SHAPE; + } + + auto srcView = cvcuda::priv::PlanarAsSingleChannelView(input, *inputWrapper); + auto dstView = cvcuda::priv::PlanarAsSingleChannelView(output, *outputWrapper); + // GA102/SM86 regresses on vectorized planar tensor float3; keep other SKUs on the wider path. + if (dataType == kCV_32F && channels == 3 && CurrentDeviceIsSM86()) + { + return flipScalar(srcView, dstView, flipCode, stream); + } + return funcs[dataType][0](srcView, dstView, flipCode, stream); + } + return funcs[dataType][channels - 1](input, output, flipCode, stream); } diff --git a/src/cvcuda/priv/legacy/flip_or_copy_var_shape.cu b/src/cvcuda/priv/legacy/flip_or_copy_var_shape.cu index 345d45fe3..435aecb96 100644 --- a/src/cvcuda/priv/legacy/flip_or_copy_var_shape.cu +++ b/src/cvcuda/priv/legacy/flip_or_copy_var_shape.cu @@ -24,61 +24,307 @@ #include "CvCudaUtils.cuh" +#include + using namespace nvcv::legacy::cuda_op; using namespace nvcv::legacy::helpers; namespace nvcv::legacy::cuda_op { -template +// Var-shape flip is a pure pixel remap and memory/latency-bound at one element per thread (mirrors the +// tensor path: ncu showed the single-channel kernel latency-bound, DRAM well below peak). Each thread +// now processes NIX columns strided by the total x-thread count, issuing NIX independent loads/stores +// together (memory-level parallelism) while consecutive threads keep touching consecutive columns +// (coalesced); the per-thread var-shape height/width/flip-code lookups are also resolved once and +// amortized over NIX elements. NIX>1 needs the grid sized to divUp(maxWidth, NIX) x-threads. The remap +// is unchanged, so every output is bit-exact with the one-element-per-thread version. +constexpr int kFlipVarNIX = 4; +constexpr int kFlipVarScalarNIX = 1; +constexpr int kFlipVarU8Vec = 4; + +template __global__ void flip_kernel(const cuda::ImageBatchVarShapeWrap src, cuda::ImageBatchVarShapeWrap dst, const cuda::Tensor1DWrap flipCode) { - const int x = blockIdx.x * blockDim.x + threadIdx.x; const int y = blockIdx.y * blockDim.y + threadIdx.y; const int batch_idx = get_batch_idx(); - int out_height = dst.height(batch_idx), out_width = dst.width(batch_idx); - if (x >= out_width || y >= out_height) + const int out_height = dst.height(batch_idx), out_width = dst.width(batch_idx); + if (y >= out_height) return; - int flip_code = flipCode[batch_idx]; - if (flip_code == 1) // flip_code = 1, horizontal flip + const int x0 = blockIdx.x * blockDim.x + threadIdx.x; + const int stride = gridDim.x * blockDim.x; + const int flip_code = flipCode[batch_idx]; + +#pragma unroll + for (int i = 0; i < NIX; ++i) { - *dst.ptr(batch_idx, y, x) = *src.ptr(batch_idx, y, (out_width - 1 - x)); + const int x = x0 + i * stride; + if (x >= out_width) + continue; + + if (flip_code == 1) // flip_code = 1, horizontal flip + { + *dst.ptr(batch_idx, y, x) = *src.ptr(batch_idx, y, (out_width - 1 - x)); + } + else if (flip_code == 0) // flip_code = 0, vertical flip + { + *dst.ptr(batch_idx, y, x) = *src.ptr(batch_idx, (out_height - 1 - y), x); + } + else if (flip_code == -1) // flip_code = -1, horizontal and vertical flip + { + *dst.ptr(batch_idx, y, x) = *src.ptr(batch_idx, (out_height - 1 - y), (out_width - 1 - x)); + } + else // just copy + { + *dst.ptr(batch_idx, y, x) = *src.ptr(batch_idx, y, x); + } } - else if (flip_code == 0) // flip_code = 0, vertical flip +} + +template +void flipImpl(const ImageBatchVarShapeDataStridedCuda &input, const ImageBatchVarShapeDataStridedCuda &output, + const TensorDataStridedCuda &flipCode, cudaStream_t stream) +{ + dim3 blockSize(32, 8, 1); + dim3 gridSize(divUp(divUp(input.maxSize().w, NIX), static_cast(blockSize.x)), + divUp(input.maxSize().h, blockSize.y), output.numImages()); + + cuda::ImageBatchVarShapeWrap src(input); + cuda::ImageBatchVarShapeWrap dst(output); + cuda::Tensor1DWrap flip_code(flipCode); + +#ifdef CUDA_DEBUG_LOG + checkCudaErrors(cudaStreamSynchronize(stream)); + checkCudaErrors(cudaGetLastError()); +#endif // CUDA_DEBUG_LOG + + flip_kernel<<>>(src, dst, flip_code); + checkKernelErrors(); +#ifdef CUDA_DEBUG_LOG + checkCudaErrors(cudaStreamSynchronize(stream)); + checkCudaErrors(cudaGetLastError()); +#endif // CUDA_DEBUG_LOG +} + +template +void flip(const ImageBatchVarShapeDataStridedCuda &input, const ImageBatchVarShapeDataStridedCuda &output, + const TensorDataStridedCuda &flipCode, cudaStream_t stream) +{ + flipImpl(input, output, flipCode, stream); +} + +template +void flipScalar(const ImageBatchVarShapeDataStridedCuda &input, const ImageBatchVarShapeDataStridedCuda &output, + const TensorDataStridedCuda &flipCode, cudaStream_t stream) +{ + flipImpl(input, output, flipCode, stream); +} + +__device__ __forceinline__ uchar4 reverseU8Lanes(uchar4 value) +{ + return make_uchar4(value.w, value.z, value.y, value.x); +} + +__device__ __forceinline__ void flipU8Group(const uchar *src_row, uchar *dst_row, int width, int dst_x, bool reverse_x) +{ + const int src_x = reverse_x ? width - kFlipVarU8Vec - dst_x : dst_x; + uchar *dst_ptr = dst_row + dst_x; + const uchar *src_ptr = src_x >= 0 ? src_row + src_x : nullptr; + const bool full = dst_x + kFlipVarU8Vec <= width; + const bool aligned = full && src_ptr != nullptr + && ((reinterpret_cast(src_ptr) | reinterpret_cast(dst_ptr)) + & (alignof(uchar4) - 1)) + == 0; + + if (aligned) { - *dst.ptr(batch_idx, y, x) = *src.ptr(batch_idx, (out_height - 1 - y), x); + const uchar4 value = *reinterpret_cast(src_ptr); + *reinterpret_cast(dst_ptr) = reverse_x ? reverseU8Lanes(value) : value; + return; } - else if (flip_code == -1) // flip_code = -1, horizontal and vertical flip + +#pragma unroll + for (int k = 0; k < kFlipVarU8Vec; ++k) { - *dst.ptr(batch_idx, y, x) = *src.ptr(batch_idx, (out_height - 1 - y), (out_width - 1 - x)); + const int x = dst_x + k; + if (x >= width) + break; + dst_row[x] = src_row[reverse_x ? width - 1 - x : x]; } - else // just copy +} + +// Single-channel U8 VarShape uses four adjacent columns per thread. Consecutive threads therefore +// move a full 128-byte warp segment instead of only 32 bytes, while NIX retains the independent +// requests that hide the ImageBatchVarShape pointer-lookup latency. Images whose row pointers or +// mirrored block boundaries are not 4-byte aligned stay on the bit-exact scalar fallback below. +template +__global__ void flip_u8_wide_kernel(const cuda::ImageBatchVarShapeWrap src, + cuda::ImageBatchVarShapeWrap dst, const cuda::Tensor1DWrap flipCode) +{ + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + + const int out_height = dst.height(batch_idx), out_width = dst.width(batch_idx); + if (y >= out_height) + return; + + const int group0 = blockIdx.x * blockDim.x + threadIdx.x; + const int group_step = gridDim.x * blockDim.x; + const int flip_code = flipCode[batch_idx]; + const bool reverse_x = flip_code == 1 || flip_code == -1; + const int src_y = (flip_code == 0 || flip_code == -1) ? out_height - 1 - y : y; + + const uchar *src_row = src.ptr(batch_idx, src_y, 0); + uchar *dst_row = dst.ptr(batch_idx, y, 0); + +#pragma unroll + for (int i = 0; i < NIX; ++i) { - *dst.ptr(batch_idx, y, x) = *src.ptr(batch_idx, y, x); + const int dst_x = (group0 + i * group_step) * kFlipVarU8Vec; + if (dst_x >= out_width) + continue; + + flipU8Group(src_row, dst_row, out_width, dst_x, reverse_x); } } -template -void flip(const ImageBatchVarShapeDataStridedCuda &input, const ImageBatchVarShapeDataStridedCuda &output, - const TensorDataStridedCuda &flipCode, cudaStream_t stream) +static void flipU8Wide(const ImageBatchVarShapeDataStridedCuda &input, const ImageBatchVarShapeDataStridedCuda &output, + const TensorDataStridedCuda &flipCode, cudaStream_t stream) +{ + dim3 blockSize(32, 8, 1); + const int groups = divUp(input.maxSize().w, kFlipVarU8Vec); + dim3 gridSize(divUp(divUp(groups, kFlipVarNIX), static_cast(blockSize.x)), + divUp(input.maxSize().h, blockSize.y), output.numImages()); + + cuda::ImageBatchVarShapeWrap src(input); + cuda::ImageBatchVarShapeWrap dst(output); + cuda::Tensor1DWrap flip_code(flipCode); + + flip_u8_wide_kernel<<>>(src, dst, flip_code); + checkKernelErrors(); +} + +template +__global__ void flip_planar_u8_wide_kernel(const cuda::ImageBatchVarShapeWrap src, + cuda::ImageBatchVarShapeWrap dst, + const cuda::Tensor1DWrap flipCode, int channels) { - constexpr uint32_t BLOCK = 32; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + + const int out_height = dst.height(batch_idx), out_width = dst.width(batch_idx); + if (y >= out_height) + return; + + const int group0 = blockIdx.x * blockDim.x + threadIdx.x; + const int group_step = gridDim.x * blockDim.x; + const int flip_code = flipCode[batch_idx]; + const bool reverse_x = flip_code == 1 || flip_code == -1; + const int src_y = (flip_code == 0 || flip_code == -1) ? out_height - 1 - y : y; + + for (int plane = 0; plane < channels; ++plane) + { + const uchar *src_row = src.ptr(batch_idx, plane, src_y, 0); + uchar *dst_row = dst.ptr(batch_idx, plane, y, 0); + +#pragma unroll + for (int i = 0; i < NIX; ++i) + { + const int dst_x = (group0 + i * group_step) * kFlipVarU8Vec; + if (dst_x < out_width) + flipU8Group(src_row, dst_row, out_width, dst_x, reverse_x); + } + } +} + +static void flip_planar_u8_wide(const ImageBatchVarShapeDataStridedCuda &input, + const ImageBatchVarShapeDataStridedCuda &output, const TensorDataStridedCuda &flipCode, + const int channels, cudaStream_t stream) +{ + dim3 blockSize(32, 8, 1); + const int groups = divUp(input.maxSize().w, kFlipVarU8Vec); + dim3 gridSize(divUp(divUp(groups, kFlipVarNIX), static_cast(blockSize.x)), + divUp(input.maxSize().h, blockSize.y), output.numImages()); + + cuda::ImageBatchVarShapeWrap src(input); + cuda::ImageBatchVarShapeWrap dst(output); + cuda::Tensor1DWrap flip_code(flipCode); + + flip_planar_u8_wide_kernel<<>>(src, dst, flip_code, channels); + checkKernelErrors(); +} + +// Planar (NCHW/CHW) flip. Flip remaps each output pixel to a source pixel independently of the +// channel, so the (src_x, src_y) mapping is computed once per output pixel and reused across every +// channel plane (grid.z runs over images, the kernel loops the planes). This avoids the per-plane +// redundant coordinate math of one thread per (image, plane) and keeps each plane's result +// bit-exact with the interleaved single-channel flip. +template +__global__ void flip_planar_kernel(const cuda::ImageBatchVarShapeWrap src, cuda::ImageBatchVarShapeWrap dst, + const cuda::Tensor1DWrap flipCode, int channels) +{ + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + + const int out_height = dst.height(batch_idx), out_width = dst.width(batch_idx); + if (y >= out_height) + return; + + const int x0 = blockIdx.x * blockDim.x + threadIdx.x; + const int stride = gridDim.x * blockDim.x; + const int flip_code = flipCode[batch_idx]; + +#pragma unroll + for (int i = 0; i < NIX; ++i) + { + const int x = x0 + i * stride; + if (x >= out_width) + continue; + + int src_x, src_y; + if (flip_code == 1) // horizontal flip + { + src_x = out_width - 1 - x; + src_y = y; + } + else if (flip_code == 0) // vertical flip + { + src_x = x; + src_y = out_height - 1 - y; + } + else if (flip_code == -1) // horizontal and vertical flip + { + src_x = out_width - 1 - x; + src_y = out_height - 1 - y; + } + else // just copy + { + src_x = x; + src_y = y; + } - dim3 blockSize(BLOCK, BLOCK / 4, 1); - dim3 gridSize(divUp(input.maxSize().w, blockSize.x), divUp(input.maxSize().h, blockSize.y), output.numImages()); + for (int plane = 0; plane < channels; ++plane) + { + *dst.ptr(batch_idx, plane, y, x) = *src.ptr(batch_idx, plane, src_y, src_x); + } + } +} + +template +void flip_planar(const ImageBatchVarShapeDataStridedCuda &input, const ImageBatchVarShapeDataStridedCuda &output, + const TensorDataStridedCuda &flipCode, const int channels, cudaStream_t stream) +{ + dim3 blockSize(32, 8, 1); + dim3 gridSize(divUp(divUp(input.maxSize().w, kFlipVarNIX), static_cast(blockSize.x)), + divUp(input.maxSize().h, blockSize.y), output.numImages()); cuda::ImageBatchVarShapeWrap src(input); cuda::ImageBatchVarShapeWrap dst(output); cuda::Tensor1DWrap flip_code(flipCode); -#ifdef CUDA_DEBUG_LOG - checkCudaErrors(cudaStreamSynchronize(stream)); - checkCudaErrors(cudaGetLastError()); -#endif // CUDA_DEBUG_LOG - - flip_kernel<<>>(src, dst, flip_code); + flip_planar_kernel<<>>(src, dst, flip_code, channels); checkKernelErrors(); #ifdef CUDA_DEBUG_LOG checkCudaErrors(cudaStreamSynchronize(stream)); @@ -111,12 +357,15 @@ ErrorCode FlipOrCopyVarShape::infer(const ImageBatchVarShapeDataStridedCuda &inp } DataFormat format = inputFormat; - if (!(format == kNHWC || format == kHWC)) + if (!(format == kNHWC || format == kHWC || format == kNCHW || format == kCHW)) { - LOG_ERROR("Invalid input DataFormat " << format << ", the valid DataFormats are: \"NHWC\", \"HWC\""); + LOG_ERROR("Invalid input DataFormat " << format + << ", the valid DataFormats are: \"NHWC\", \"HWC\", \"NCHW\", \"CHW\""); return ErrorCode::INVALID_DATA_FORMAT; } + const bool isPlanar = (format == kNCHW || format == kCHW); + DataType dataType = helpers::GetLegacyDataType(input.uniqueFormat()); DataType outDataType = helpers::GetLegacyDataType(output.uniqueFormat()); @@ -134,27 +383,55 @@ ErrorCode FlipOrCopyVarShape::infer(const ImageBatchVarShapeDataStridedCuda &inp } const int channels = input.uniqueFormat().numChannels(); - if (channels > 4) + // Two-channel input is unsupported by both the interleaved dispatch table and planar formats. + if (channels > 4 || channels == 2) { LOG_ERROR("Invalid channel number " << channels); return ErrorCode::INVALID_DATA_SHAPE; } + // The planar path launches grid.z over images and loops the channel planes inside the kernel, so + // numImages must fit CUDA's 65535 grid-z limit. Compute in 64-bit to avoid overflow. + if (isPlanar && static_cast(output.numImages()) > 65535) + { + LOG_ERROR("Planar flip requires numImages <= 65535 (CUDA grid-z limit)"); + return ErrorCode::INVALID_DATA_SHAPE; + } + // using flip_t = void(const ImageBatchVarShapeDataStridedCuda & input, // const ImageBatchVarShapeDataStridedCuda & output, // const TensorDataStridedCuda & flipCode, // cudaStream_t stream); + if (isPlanar) + { + // Planar dispatch indexes by dtype only: each channel is flipped as a separate single-channel + // plane, so one scalar specialization per dtype covers any channel count. + typedef void (*planar_flip_t)(const ImageBatchVarShapeDataStridedCuda &input, + const ImageBatchVarShapeDataStridedCuda &output, + const TensorDataStridedCuda &flipCode, const int channels, cudaStream_t stream); + + static const planar_flip_t planar_funcs[6] = { + flip_planar_u8_wide, 0 /*schar*/, flip_planar, + flip_planar, flip_planar, flip_planar, + }; + + const planar_flip_t planar_func = planar_funcs[dataType]; + NVCV_ASSERT(planar_func != 0); + planar_func(input, output, flipCode, channels, stream); + return ErrorCode::SUCCESS; + } + typedef void (*flip_t)(const ImageBatchVarShapeDataStridedCuda &input, const ImageBatchVarShapeDataStridedCuda &output, const TensorDataStridedCuda &flipCode, cudaStream_t stream); static const flip_t funcs[6][4] = { - { flip, 0, flip, flip}, - { 0, 0, 0, 0}, - {flip, 0, flip, flip}, - { flip, 0, flip, flip}, - { flip, 0, flip, flip}, - { flip, 0, flip, flip} + { flipU8Wide, 0, flip, flip}, + { 0, 0, 0, 0}, + {flip, 0, flip, flip}, + { flip, 0, flip, flip}, + { flip, 0, flip, flip}, + { flip, 0, flipScalar, flip} }; funcs[dataType][channels - 1](input, output, flipCode, stream); diff --git a/src/cvcuda/priv/legacy/gamma_contrast.cu b/src/cvcuda/priv/legacy/gamma_contrast.cu new file mode 100644 index 000000000..544d75ed0 --- /dev/null +++ b/src/cvcuda/priv/legacy/gamma_contrast.cu @@ -0,0 +1,795 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "CvCudaLegacy.h" +#include "CvCudaLegacyHelpers.hpp" + +#include "CvCudaUtils.cuh" +#include "gamma_contrast_common.cuh" + +#include + +#include + +#define BLOCK 32 + +using namespace nvcv::legacy::cuda_op; +using namespace nvcv::legacy::helpers; + +namespace nvcv::legacy::cuda_op { + +namespace { + +namespace detail = gamma_contrast_detail; + +// Interleaved (NHWC/HWC) tensor gamma contrast: one thread per output pixel, applying the per-channel +// gamma vector gamma_[sample] -- the same dense [sample*channels] layout the var-shape path uses, so +// results are bit-exact with the var-shape kernel. +template +__global__ void gamma_contrast_tensor_kernel(SrcWrapper src, DstWrapper dst, + const cuda::Tensor1DWrap gamma_, int2 size) +{ + const int x = blockIdx.x * blockDim.x + threadIdx.x; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const int z = blockIdx.z; + if (x >= size.x || y >= size.y) + return; + + gamma_type gamma = gamma_[z]; + gamma_type tmp = (src[int3{x, y, z}] + 0.0f) / 255.0f; + + dst[int3{x, y, z}] = nvcv::cuda::SaturateCast(cuda::pow(tmp, gamma) * 255.0f); +} + +template +__global__ void gamma_contrast_tensor_u8_batched_kernel(SrcWrapper src, DstWrapper dst, + const cuda::Tensor1DWrap gamma_, int2 size) +{ + const int x = (blockIdx.x * blockDim.x + threadIdx.x) * NIX; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const int z = blockIdx.z; + if (x >= size.x || y >= size.y) + return; + + const gamma_type gamma = gamma_[z]; + +#pragma unroll + for (int i = 0; i < NIX; ++i) + { + if (x + i < size.x) + { + const gamma_type tmp = (src[int3{x + i, y, z}] + 0.0f) / 255.0f; + dst[int3{x + i, y, z}] = nvcv::cuda::SaturateCast(cuda::pow(tmp, gamma) * 255.0f); + } + } +} + +template +__global__ void gamma_contrast_tensor_float_kernel(SrcWrapper src, DstWrapper dst, + const cuda::Tensor1DWrap gamma_, int2 size) +{ + const int x = blockIdx.x * blockDim.x + threadIdx.x; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const int z = blockIdx.z; + if (x >= size.x || y >= size.y) + return; + + gamma_type gamma = gamma_[z]; + + D out = nvcv::cuda::SaturateCast(cuda::pow(cuda::StaticCast(src[int3{x, y, z}]), gamma)); + + dst[int3{x, y, z}] = cuda::clamp(cuda::StaticCast(out), 0.f, 1.f); +} + +// Planar (NCHW/CHW) tensor gamma contrast: one thread per output pixel, looping the channel planes and +// reading the per-channel gamma as gammaArray[sample*channels + plane]. Bit-exact with the interleaved +// and var-shape paths per channel. +template +__global__ void gamma_contrast_tensor_planar_kernel(SrcWrapper src, DstWrapper dst, + const cuda::Tensor1DWrap gamma_, int2 size, int channels) +{ + const int x = blockIdx.x * blockDim.x + threadIdx.x; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const int z = blockIdx.z; + if (x >= size.x || y >= size.y) + return; + + for (int plane = 0; plane < channels; ++plane) + { + const float gamma = gamma_[z * channels + plane]; + const float tmp = (src[int4{x, y, plane, z}] + 0.0f) / 255.0f; + + dst[int4{x, y, plane, z}] = nvcv::cuda::SaturateCast(cuda::pow(tmp, gamma) * 255.0f); + } +} + +template +__global__ void gamma_contrast_tensor_planar_u8_kernel(SrcWrapper src, DstWrapper dst, + const cuda::Tensor1DWrap gamma_, int2 size, int channels) +{ + constexpr int NIX = 8; + + const int x = (blockIdx.x * blockDim.x + threadIdx.x) * NIX; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const int z = blockIdx.z; + if (x >= size.x || y >= size.y) + return; + + for (int plane = 0; plane < channels; ++plane) + { + const float gamma = gamma_[z * channels + plane]; + auto apply = [gamma](uchar value) + { + const float tmp = (value + 0.0f) / 255.0f; + return nvcv::cuda::SaturateCast(cuda::pow(tmp, gamma) * 255.0f); + }; + + const uchar *src_ptr = &src[int4{x, y, plane, z}]; + uchar *dst_ptr = &dst[int4{x, y, plane, z}]; + const bool aligned = ((reinterpret_cast(src_ptr) | reinterpret_cast(dst_ptr)) + & (alignof(uchar4) - 1)) + == 0; + + if (x + NIX <= size.x && aligned) + { + const uchar4 input0 = *reinterpret_cast(src_ptr); + const uchar4 input1 = *reinterpret_cast(src_ptr + 4); + const uchar4 output0 = make_uchar4(apply(input0.x), apply(input0.y), apply(input0.z), apply(input0.w)); + const uchar4 output1 = make_uchar4(apply(input1.x), apply(input1.y), apply(input1.z), apply(input1.w)); + *reinterpret_cast(dst_ptr) = output0; + *reinterpret_cast(dst_ptr + 4) = output1; + } + else + { +#pragma unroll + for (int i = 0; i < NIX; ++i) + { + if (x + i < size.x) + { + dst_ptr[i] = apply(src_ptr[i]); + } + } + } + } +} + +template +__global__ void gamma_contrast_tensor_planar_float_kernel(SrcWrapper src, DstWrapper dst, + const cuda::Tensor1DWrap gamma_, int2 size, + int channels) +{ + const int x = blockIdx.x * blockDim.x + threadIdx.x; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const int z = blockIdx.z; + if (x >= size.x || y >= size.y) + return; + + for (int plane = 0; plane < channels; ++plane) + { + const float gamma = gamma_[z * channels + plane]; + + D out = nvcv::cuda::SaturateCast(cuda::pow(cuda::StaticCast(src[int4{x, y, plane, z}]), gamma)); + + dst[int4{x, y, plane, z}] = cuda::clamp(cuda::StaticCast(out), 0.f, 1.f); + } +} + +// Scalar (host-float) gamma/gain variants. The gamma and gain are passed by value as kernel launch +// arguments (no device gamma array, no ExpandGamma copy) and applied as out = gain * in**gamma -- the +// torchvision adjust_gamma formula. Nearest rounding with gain == 1.0f (an exact IEEE no-op) is +// bit-exact with the device-tensor counterpart above fed a gamma tensor filled with the same value; +// truncation provides torchvision-compatible integer conversion. +template +__device__ __forceinline__ D gamma_contrast_saturate_cast(U value) +{ + if constexpr (kTruncate) + { + value = nvcv::cuda::round(value); + } + return nvcv::cuda::SaturateCast(value); +} + +template +__global__ void gamma_contrast_tensor_scalar_kernel(SrcWrapper src, DstWrapper dst, float gamma, float gain, int2 size) +{ + const int x = blockIdx.x * blockDim.x + threadIdx.x; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const int z = blockIdx.z; + if (x >= size.x || y >= size.y) + return; + + gamma_type g = nvcv::cuda::SetAll(gamma); + gamma_type tmp = (src[int3{x, y, z}] + 0.0f) / 255.0f; + + dst[int3{x, y, z}] = gamma_contrast_saturate_cast(cuda::pow(tmp, g) * gain * 255.0f); +} + +template +__global__ void gamma_contrast_tensor_scalar_float_kernel(SrcWrapper src, DstWrapper dst, float gamma, float gain, + int2 size) +{ + const int x = blockIdx.x * blockDim.x + threadIdx.x; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const int z = blockIdx.z; + if (x >= size.x || y >= size.y) + return; + + gamma_type g = nvcv::cuda::SetAll(gamma); + + D out = nvcv::cuda::SaturateCast(cuda::pow(cuda::StaticCast(src[int3{x, y, z}]), g) * gain); + + dst[int3{x, y, z}] = cuda::clamp(cuda::StaticCast(out), 0.f, 1.f); +} + +template +__global__ void gamma_contrast_tensor_planar_scalar_kernel(SrcWrapper src, DstWrapper dst, float gamma, float gain, + int2 size, int channels) +{ + const int x = blockIdx.x * blockDim.x + threadIdx.x; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const int z = blockIdx.z; + if (x >= size.x || y >= size.y) + return; + + for (int plane = 0; plane < channels; ++plane) + { + const float tmp = (src[int4{x, y, plane, z}] + 0.0f) / 255.0f; + + dst[int4{x, y, plane, z}] = gamma_contrast_saturate_cast(cuda::pow(tmp, gamma) * gain * 255.0f); + } +} + +template +__global__ void gamma_contrast_tensor_planar_scalar_float_kernel(SrcWrapper src, DstWrapper dst, float gamma, + float gain, int2 size, int channels) +{ + const int x = blockIdx.x * blockDim.x + threadIdx.x; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const int z = blockIdx.z; + if (x >= size.x || y >= size.y) + return; + + for (int plane = 0; plane < channels; ++plane) + { + D out + = nvcv::cuda::SaturateCast(cuda::pow(cuda::StaticCast(src[int4{x, y, plane, z}]), gamma) * gain); + + dst[int4{x, y, plane, z}] = cuda::clamp(cuda::StaticCast(out), 0.f, 1.f); + } +} + +// Interleaved launcher (T = vector pixel type for the channel count). +template +void gamma_contrast_tensor(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, + float *gammaValues, cudaStream_t stream) +{ + auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(inData); + auto dstAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(outData); + + int2 size{static_cast(srcAccess->numCols()), static_cast(srcAccess->numRows())}; + int batch = srcAccess->numSamples(); + + dim3 block(BLOCK, BLOCK / 4, 1); + dim3 grid(divUp(size.x, block.x), divUp(size.y, block.y), batch); + + auto src = cuda::CreateTensorWrapNHW(inData); + auto dst = cuda::CreateTensorWrapNHW(outData); + + using gamma_type = cuda::ConvertBaseTypeTo; + cuda::Tensor1DWrap gamma(gammaValues); + gamma_contrast_tensor_kernel<<>>(src, dst, gamma, size); + checkKernelErrors(); +} + +template +void gamma_contrast_tensor_u8_batched(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, + float *gammaValues, cudaStream_t stream) +{ + constexpr int NIX = 2; + + auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(inData); + + int2 size{static_cast(srcAccess->numCols()), static_cast(srcAccess->numRows())}; + int batch = srcAccess->numSamples(); + + dim3 block(BLOCK, BLOCK / 4, 1); + dim3 grid(divUp(size.x, block.x * NIX), divUp(size.y, block.y), batch); + + auto src = cuda::CreateTensorWrapNHW(inData); + auto dst = cuda::CreateTensorWrapNHW(outData); + + using gamma_type = cuda::ConvertBaseTypeTo; + cuda::Tensor1DWrap gamma(gammaValues); + gamma_contrast_tensor_u8_batched_kernel + <<>>(src, dst, gamma, size); + checkKernelErrors(); +} + +template +void gamma_contrast_tensor_float(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, + float *gammaValues, cudaStream_t stream) +{ + auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(inData); + auto dstAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(outData); + + int2 size{static_cast(srcAccess->numCols()), static_cast(srcAccess->numRows())}; + int batch = srcAccess->numSamples(); + + dim3 block(BLOCK, BLOCK / 4, 1); + dim3 grid(divUp(size.x, block.x), divUp(size.y, block.y), batch); + + auto src = cuda::CreateTensorWrapNHW(inData); + auto dst = cuda::CreateTensorWrapNHW(outData); + + using gamma_type = cuda::ConvertBaseTypeTo; + cuda::Tensor1DWrap gamma(gammaValues); + gamma_contrast_tensor_float_kernel<<>>(src, dst, gamma, size); + checkKernelErrors(); +} + +// Planar launcher (T = scalar base type, one plane per channel). +template +void gamma_contrast_tensor_planar(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, + float *gammaValues, int channels, cudaStream_t stream) +{ + auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(inData); + auto dstAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(outData); + + int2 size{static_cast(srcAccess->numCols()), static_cast(srcAccess->numRows())}; + int batch = srcAccess->numSamples(); + + dim3 block(BLOCK, BLOCK / 4, 1); + dim3 grid(divUp(size.x, block.x), divUp(size.y, block.y), batch); + + auto src = cuda::Tensor4DWrap(inData.basePtr(), static_cast(srcAccess->sampleStride()), + static_cast(srcAccess->planeStride()), + static_cast(srcAccess->rowStride())); + auto dst = cuda::Tensor4DWrap(outData.basePtr(), static_cast(dstAccess->sampleStride()), + static_cast(dstAccess->planeStride()), + static_cast(dstAccess->rowStride())); + + cuda::Tensor1DWrap gamma(gammaValues); + gamma_contrast_tensor_planar_kernel<<>>(src, dst, gamma, size, channels); + checkKernelErrors(); +} + +static void gamma_contrast_tensor_planar_u8(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, + float *gammaValues, int channels, cudaStream_t stream) +{ + constexpr int NIX = 8; + + auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(inData); + auto dstAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(outData); + + int2 size{static_cast(srcAccess->numCols()), static_cast(srcAccess->numRows())}; + int batch = srcAccess->numSamples(); + + dim3 block(BLOCK, BLOCK / 4, 1); + dim3 grid(divUp(size.x, block.x * NIX), divUp(size.y, block.y), batch); + + auto src = cuda::Tensor4DWrap( + inData.basePtr(), static_cast(srcAccess->sampleStride()), + static_cast(srcAccess->planeStride()), static_cast(srcAccess->rowStride())); + auto dst = cuda::Tensor4DWrap(outData.basePtr(), static_cast(dstAccess->sampleStride()), + static_cast(dstAccess->planeStride()), + static_cast(dstAccess->rowStride())); + + cuda::Tensor1DWrap gamma(gammaValues); + gamma_contrast_tensor_planar_u8_kernel<<>>(src, dst, gamma, size, channels); + checkKernelErrors(); +} + +template +void gamma_contrast_tensor_planar_float(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, + float *gammaValues, int channels, cudaStream_t stream) +{ + auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(inData); + auto dstAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(outData); + + int2 size{static_cast(srcAccess->numCols()), static_cast(srcAccess->numRows())}; + int batch = srcAccess->numSamples(); + + dim3 block(BLOCK, BLOCK / 4, 1); + dim3 grid(divUp(size.x, block.x), divUp(size.y, block.y), batch); + + auto src = cuda::Tensor4DWrap(inData.basePtr(), static_cast(srcAccess->sampleStride()), + static_cast(srcAccess->planeStride()), + static_cast(srcAccess->rowStride())); + auto dst = cuda::Tensor4DWrap(outData.basePtr(), static_cast(dstAccess->sampleStride()), + static_cast(dstAccess->planeStride()), + static_cast(dstAccess->rowStride())); + + cuda::Tensor1DWrap gamma(gammaValues); + gamma_contrast_tensor_planar_float_kernel<<>>(src, dst, gamma, size, channels); + checkKernelErrors(); +} + +// Scalar (host-float) launchers -- same grids/wrappers as the device-tensor launchers above, but the +// gamma/gain scalars are passed straight into the kernel launch (no gamma array). +template +void gamma_contrast_tensor_scalar(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, + float gamma, float gain, NVCVRoundMode roundMode, cudaStream_t stream) +{ + auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(inData); + auto dstAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(outData); + + int2 size{static_cast(srcAccess->numCols()), static_cast(srcAccess->numRows())}; + int batch = srcAccess->numSamples(); + + dim3 block(BLOCK, BLOCK / 4, 1); + dim3 grid(divUp(size.x, block.x), divUp(size.y, block.y), batch); + + auto src = cuda::CreateTensorWrapNHW(inData); + auto dst = cuda::CreateTensorWrapNHW(outData); + + using gamma_type = cuda::ConvertBaseTypeTo; + if (roundMode == NVCV_ROUND_TRUNCATE) + { + gamma_contrast_tensor_scalar_kernel + <<>>(src, dst, gamma, gain, size); + } + else + { + gamma_contrast_tensor_scalar_kernel + <<>>(src, dst, gamma, gain, size); + } + checkKernelErrors(); +} + +template +void gamma_contrast_tensor_scalar_float(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, + float gamma, float gain, cudaStream_t stream) +{ + auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(inData); + auto dstAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(outData); + + int2 size{static_cast(srcAccess->numCols()), static_cast(srcAccess->numRows())}; + int batch = srcAccess->numSamples(); + + dim3 block(BLOCK, BLOCK / 4, 1); + dim3 grid(divUp(size.x, block.x), divUp(size.y, block.y), batch); + + auto src = cuda::CreateTensorWrapNHW(inData); + auto dst = cuda::CreateTensorWrapNHW(outData); + + using gamma_type = cuda::ConvertBaseTypeTo; + gamma_contrast_tensor_scalar_float_kernel<<>>(src, dst, gamma, gain, size); + checkKernelErrors(); +} + +template +void gamma_contrast_tensor_planar_scalar(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, + float gamma, float gain, int channels, NVCVRoundMode roundMode, + cudaStream_t stream) +{ + auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(inData); + auto dstAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(outData); + + int2 size{static_cast(srcAccess->numCols()), static_cast(srcAccess->numRows())}; + int batch = srcAccess->numSamples(); + + dim3 block(BLOCK, BLOCK / 4, 1); + dim3 grid(divUp(size.x, block.x), divUp(size.y, block.y), batch); + + auto src = cuda::Tensor4DWrap(inData.basePtr(), static_cast(srcAccess->sampleStride()), + static_cast(srcAccess->planeStride()), + static_cast(srcAccess->rowStride())); + auto dst = cuda::Tensor4DWrap(outData.basePtr(), static_cast(dstAccess->sampleStride()), + static_cast(dstAccess->planeStride()), + static_cast(dstAccess->rowStride())); + + if (roundMode == NVCV_ROUND_TRUNCATE) + { + gamma_contrast_tensor_planar_scalar_kernel + <<>>(src, dst, gamma, gain, size, channels); + } + else + { + gamma_contrast_tensor_planar_scalar_kernel + <<>>(src, dst, gamma, gain, size, channels); + } + checkKernelErrors(); +} + +template +void gamma_contrast_tensor_planar_scalar_float(const TensorDataStridedCuda &inData, + const TensorDataStridedCuda &outData, float gamma, float gain, + int channels, cudaStream_t stream) +{ + auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(inData); + auto dstAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(outData); + + int2 size{static_cast(srcAccess->numCols()), static_cast(srcAccess->numRows())}; + int batch = srcAccess->numSamples(); + + dim3 block(BLOCK, BLOCK / 4, 1); + dim3 grid(divUp(size.x, block.x), divUp(size.y, block.y), batch); + + auto src = cuda::Tensor4DWrap(inData.basePtr(), static_cast(srcAccess->sampleStride()), + static_cast(srcAccess->planeStride()), + static_cast(srcAccess->rowStride())); + auto dst = cuda::Tensor4DWrap(outData.basePtr(), static_cast(dstAccess->sampleStride()), + static_cast(dstAccess->planeStride()), + static_cast(dstAccess->rowStride())); + + gamma_contrast_tensor_planar_scalar_float_kernel + <<>>(src, dst, gamma, gain, size, channels); + checkKernelErrors(); +} + +struct TensorInfo +{ + DataType dataType; + bool isPlanar; + int numSamples; + int channels; +}; + +static ErrorCode validateTensorPair(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, + TensorInfo &info) +{ + DataFormat inputFormat = helpers::GetLegacyDataFormat(inData.layout()); + DataFormat outputFormat = helpers::GetLegacyDataFormat(outData.layout()); + if (inputFormat != outputFormat) + { + LOG_ERROR("Invalid DataFormat between input (" << inputFormat << ") and output (" << outputFormat << ")"); + return ErrorCode::INVALID_DATA_FORMAT; + } + + if (!(inputFormat == kNHWC || inputFormat == kHWC || inputFormat == kNCHW || inputFormat == kCHW)) + { + LOG_ERROR("Invalid DataFormat " << inputFormat + << ", the valid DataFormats are: \"NHWC\", \"HWC\", \"NCHW\", \"CHW\""); + return ErrorCode::INVALID_DATA_FORMAT; + } + + const bool isPlanar = (inputFormat == kNCHW || inputFormat == kCHW); + + DataType dataType = GetLegacyDataType(inData.dtype()); + DataType outDataType = GetLegacyDataType(outData.dtype()); + if (dataType != outDataType) + { + LOG_ERROR("Input DataType " << dataType << " must match output DataType " << outDataType); + return ErrorCode::INVALID_DATA_TYPE; + } + if (!(dataType == kCV_8U || dataType == kCV_16U || dataType == kCV_16S || dataType == kCV_32S + || dataType == kCV_32F)) + { + LOG_ERROR("Invalid DataType " << dataType); + return ErrorCode::INVALID_DATA_TYPE; + } + + auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(inData); + auto dstAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(outData); + if (!srcAccess || !dstAccess) + { + LOG_ERROR("Failed to create tensor access"); + return ErrorCode::INVALID_DATA_FORMAT; + } + + if (srcAccess->numSamples() != dstAccess->numSamples() || srcAccess->numRows() != dstAccess->numRows() + || srcAccess->numCols() != dstAccess->numCols()) + { + LOG_ERROR("Input and output must have matching sample count, width, and height"); + return ErrorCode::INVALID_DATA_SHAPE; + } + + const int numSamples = srcAccess->numSamples(); + const int channels = srcAccess->numChannels(); + if (channels != dstAccess->numChannels()) + { + LOG_ERROR("Input and output channel counts must match"); + return ErrorCode::INVALID_DATA_SHAPE; + } + if (channels > 4 || (isPlanar && channels == 2)) + { + LOG_ERROR("Invalid channel number " << channels); + return ErrorCode::INVALID_DATA_SHAPE; + } + + info = {dataType, isPlanar, numSamples, channels}; + return ErrorCode::SUCCESS; +} + +} // namespace + +GammaContrast::GammaContrast(const int32_t maxBatchSize, const int32_t maxChannelCount) + : CudaBaseOp() + , m_maxBatchSize(maxBatchSize) + , m_maxChannelCount(maxChannelCount) +{ + if (m_maxBatchSize > 0 && m_maxChannelCount > 0) + { + NVCV_CHECK_THROW(cudaMalloc(&m_gammaArray, m_maxBatchSize * m_maxChannelCount * sizeof(float))); + } +} + +GammaContrast::~GammaContrast() +{ + NVCV_CHECK_LOG(cudaFree(m_gammaArray)); +} + +ErrorCode GammaContrast::infer(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, + const TensorDataStridedCuda &gammas, cudaStream_t stream) +{ + TensorInfo info; + ErrorCode validateErr = validateTensorPair(inData, outData, info); + if (validateErr != ErrorCode::SUCCESS) + { + return validateErr; + } + + const DataType data_type = info.dataType; + const bool isPlanar = info.isPlanar; + const int numSamples = info.numSamples; + const int channels = info.channels; + + if (m_maxBatchSize <= 0 || numSamples > m_maxBatchSize) + { + LOG_ERROR("Invalid maximum batch size"); + return ErrorCode::INVALID_PARAMETER; + } + if (m_maxChannelCount <= 0 || channels > m_maxChannelCount) + { + LOG_ERROR("Invalid maximum channel count"); + return ErrorCode::INVALID_PARAMETER; + } + + // The planar path launches grid.z over samples; enforce CUDA's 65535 grid-z limit. + if (static_cast(numSamples) > 65535) + { + LOG_ERROR("GammaContrast tensor requires numSamples <= 65535 (CUDA grid-z limit)"); + return ErrorCode::INVALID_DATA_SHAPE; + } + + ErrorCode gammaErr = detail::ExpandGamma(gammas, numSamples, channels, m_gammaArray, stream); + if (gammaErr != ErrorCode::SUCCESS) + { + return gammaErr; + } + + if (isPlanar) + { + typedef void (*planar_func_t)(const TensorDataStridedCuda &, const TensorDataStridedCuda &, float *, int, + cudaStream_t); + static const planar_func_t planar_funcs[5] + = {gamma_contrast_tensor_planar_u8, 0 /*schar*/, gamma_contrast_tensor_planar, + gamma_contrast_tensor_planar, gamma_contrast_tensor_planar}; + if (data_type == kCV_32F) + { + gamma_contrast_tensor_planar_float(inData, outData, m_gammaArray, channels, stream); + } + else + { + NVCV_ASSERT(planar_funcs[data_type] != nullptr); + planar_funcs[data_type](inData, outData, m_gammaArray, channels, stream); + } + return ErrorCode::SUCCESS; + } + + typedef void (*func_t)(const TensorDataStridedCuda &, const TensorDataStridedCuda &, float *, cudaStream_t); + + static const func_t funcs[5][4] = { + { gamma_contrast_tensor, gamma_contrast_tensor, gamma_contrast_tensor_u8_batched, + gamma_contrast_tensor_u8_batched }, + { 0, 0, 0, 0}, + {gamma_contrast_tensor, gamma_contrast_tensor, gamma_contrast_tensor, + gamma_contrast_tensor }, + { gamma_contrast_tensor, gamma_contrast_tensor, gamma_contrast_tensor, + gamma_contrast_tensor }, + { gamma_contrast_tensor, gamma_contrast_tensor, gamma_contrast_tensor, + gamma_contrast_tensor }, + }; + static const func_t funcs_float[4] = {gamma_contrast_tensor_float, gamma_contrast_tensor_float, + gamma_contrast_tensor_float, gamma_contrast_tensor_float}; + + if (data_type == kCV_32F) + { + funcs_float[channels - 1](inData, outData, m_gammaArray, stream); + } + else + { + NVCV_ASSERT(funcs[data_type][channels - 1] != nullptr); + funcs[data_type][channels - 1](inData, outData, m_gammaArray, stream); + } + + return ErrorCode::SUCCESS; +} + +// Scalar (host-float) gamma/gain path. The scalars are baked into the kernel launch, so there is no +// gamma tensor, ExpandGamma copy, or m_gammaArray/max-batch/max-channel scratch bound. +ErrorCode GammaContrast::infer(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, float gamma, + float gain, NVCVRoundMode roundMode, cudaStream_t stream) +{ + if (roundMode != NVCV_ROUND_NEAREST && roundMode != NVCV_ROUND_TRUNCATE) + { + LOG_ERROR("Invalid round mode " << static_cast(roundMode)); + return ErrorCode::INVALID_PARAMETER; + } + + TensorInfo info; + ErrorCode validateErr = validateTensorPair(inData, outData, info); + if (validateErr != ErrorCode::SUCCESS) + { + return validateErr; + } + + const DataType data_type = info.dataType; + const bool isPlanar = info.isPlanar; + const int numSamples = info.numSamples; + const int channels = info.channels; + + // The planar path launches grid.z over samples; enforce CUDA's 65535 grid-z limit. + if (static_cast(numSamples) > 65535) + { + LOG_ERROR("GammaContrast tensor requires numSamples <= 65535 (CUDA grid-z limit)"); + return ErrorCode::INVALID_DATA_SHAPE; + } + + if (isPlanar) + { + typedef void (*planar_func_t)(const TensorDataStridedCuda &, const TensorDataStridedCuda &, float, float, int, + NVCVRoundMode, cudaStream_t); + static const planar_func_t planar_funcs[5] + = {gamma_contrast_tensor_planar_scalar, 0 /*schar*/, gamma_contrast_tensor_planar_scalar, + gamma_contrast_tensor_planar_scalar, gamma_contrast_tensor_planar_scalar}; + if (data_type == kCV_32F) + { + gamma_contrast_tensor_planar_scalar_float(inData, outData, gamma, gain, channels, stream); + } + else + { + NVCV_ASSERT(planar_funcs[data_type] != nullptr); + planar_funcs[data_type](inData, outData, gamma, gain, channels, roundMode, stream); + } + return ErrorCode::SUCCESS; + } + + typedef void (*func_t)(const TensorDataStridedCuda &, const TensorDataStridedCuda &, float, float, NVCVRoundMode, + cudaStream_t); + + static const func_t funcs[5][4] = { + { gamma_contrast_tensor_scalar, gamma_contrast_tensor_scalar, + gamma_contrast_tensor_scalar,gamma_contrast_tensor_scalar }, + { 0, 0, 0, 0}, + {gamma_contrast_tensor_scalar, gamma_contrast_tensor_scalar, + gamma_contrast_tensor_scalar, gamma_contrast_tensor_scalar }, + { gamma_contrast_tensor_scalar, gamma_contrast_tensor_scalar, + gamma_contrast_tensor_scalar, gamma_contrast_tensor_scalar }, + { gamma_contrast_tensor_scalar, gamma_contrast_tensor_scalar, gamma_contrast_tensor_scalar, + gamma_contrast_tensor_scalar }, + }; + typedef void (*float_func_t)(const TensorDataStridedCuda &, const TensorDataStridedCuda &, float, float, + cudaStream_t); + static const float_func_t funcs_float[4] + = {gamma_contrast_tensor_scalar_float, gamma_contrast_tensor_scalar_float, + gamma_contrast_tensor_scalar_float, gamma_contrast_tensor_scalar_float}; + + if (data_type == kCV_32F) + { + funcs_float[channels - 1](inData, outData, gamma, gain, stream); + } + else + { + NVCV_ASSERT(funcs[data_type][channels - 1] != nullptr); + funcs[data_type][channels - 1](inData, outData, gamma, gain, roundMode, stream); + } + + return ErrorCode::SUCCESS; +} + +} // namespace nvcv::legacy::cuda_op diff --git a/src/cvcuda/priv/legacy/gamma_contrast_common.cuh b/src/cvcuda/priv/legacy/gamma_contrast_common.cuh new file mode 100644 index 000000000..8c652b108 --- /dev/null +++ b/src/cvcuda/priv/legacy/gamma_contrast_common.cuh @@ -0,0 +1,199 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +// Shared gamma-value handling for the tensor (gamma_contrast.cu) and var-shape +// (gamma_contrast_var_shape.cu) GammaContrast paths. Both normalize the user's gamma tensor (which +// may be per-sample or per-sample-per-channel, dense or strided) into a dense [numSamples*channels] +// float array consumed identically by their kernels -- so the two paths stay bit-exact. + +#ifndef CVCUDA_LEGACY_GAMMA_CONTRAST_COMMON_CUH +#define CVCUDA_LEGACY_GAMMA_CONTRAST_COMMON_CUH + +#include "CvCudaLegacy.h" + +#include "CvCudaUtils.cuh" + +namespace nvcv::legacy::cuda_op { namespace gamma_contrast_detail { + +#define GAMMA_CONTRAST_BLOCK 256 + +__global__ static void copyGammaValues(float *gammaArray, const uint8_t *gammaBase, int64_t gammaStride, + const int numImages, const int channelCount) +{ + int index = threadIdx.x + blockIdx.x * blockDim.x; + if (index >= numImages) + { + return; + } + + const float gamma = *reinterpret_cast(gammaBase + index * gammaStride); + for (int i = 0; i < channelCount; i++) + { + gammaArray[index * channelCount + i] = gamma; + } +} + +__global__ static void copyPerChannelGammaValues(float *gammaArray, const uint8_t *gammaBase, int64_t sampleStride, + int64_t channelStride, const int numImages, const int channelCount) +{ + int index = threadIdx.x + blockIdx.x * blockDim.x; + if (index >= numImages * channelCount) + { + return; + } + + int imageIndex = index / channelCount; + int channelIndex = index % channelCount; + gammaArray[index] + = *reinterpret_cast(gammaBase + imageIndex * sampleStride + channelIndex * channelStride); +} + +inline bool IsTensorDense(const TensorDataStridedCuda &tensor) +{ + int64_t expectedStride = sizeof(float); + for (int dim = tensor.rank() - 1; dim >= 0; --dim) + { + if (tensor.stride(dim) != expectedStride) + { + return false; + } + expectedStride *= tensor.shape(dim); + } + return true; +} + +inline bool GetPerImageGammaStride(const TensorDataStridedCuda &gammas, int numImages, int64_t &sampleStride) +{ + if (IsTensorDense(gammas)) + { + sampleStride = sizeof(float); + return true; + } + + const int sampleDim = gammas.layout().find(nvcv::LABEL_BATCH); + if (sampleDim >= 0 && gammas.shape(sampleDim) == numImages) + { + sampleStride = gammas.stride(sampleDim); + return true; + } + + if (gammas.rank() == 1 && gammas.shape(0) == numImages) + { + sampleStride = gammas.stride(0); + return true; + } + + if (gammas.rank() == 2 && gammas.shape(0) == numImages && gammas.shape(1) == 1) + { + sampleStride = gammas.stride(0); + return true; + } + + return false; +} + +inline bool GetPerChannelGammaStrides(const TensorDataStridedCuda &gammas, int numImages, int channelCount, + int64_t &sampleStride, int64_t &channelStride) +{ + if (gammas.rank() == 1 && gammas.shape(0) == numImages * channelCount) + { + sampleStride = gammas.stride(0) * channelCount; + channelStride = gammas.stride(0); + return true; + } + + const int sampleDim = gammas.layout().find(nvcv::LABEL_BATCH); + const int channelDim = gammas.layout().find(nvcv::LABEL_CHANNEL); + if (sampleDim >= 0 && channelDim >= 0 && gammas.shape(sampleDim) == numImages + && gammas.shape(channelDim) == channelCount) + { + sampleStride = gammas.stride(sampleDim); + channelStride = gammas.stride(channelDim); + return true; + } + + if (gammas.rank() == 2 && gammas.shape(0) == numImages && gammas.shape(1) == channelCount) + { + sampleStride = gammas.stride(0); + channelStride = gammas.stride(1); + return true; + } + + return false; +} + +// Validate the gamma tensor length and expand it into the dense [numImages*channels] gammaArray. +// Returns SUCCESS, or an error code on an invalid gamma shape. +inline ErrorCode ExpandGamma(const TensorDataStridedCuda &gammas, int numImages, int channels, float *gammaArray, + cudaStream_t stream) +{ + int numElements = 1; + for (int i = 0; i < gammas.rank(); i++) + { + numElements *= gammas.shape(i); + } + + if (numElements != numImages && numElements != numImages * channels) + { + LOG_ERROR("Invalid gamma tensor length " << numElements << ", expected " << numImages << " or " + << numImages * channels); + return ErrorCode::INVALID_DATA_SHAPE; + } + + if (numImages * channels == numElements) + { + if (IsTensorDense(gammas)) + { + checkCudaErrors(cudaMemcpyAsync(gammaArray, gammas.basePtr(), sizeof(float) * numImages * channels, + cudaMemcpyDeviceToDevice, stream)); + } + else + { + int64_t sampleStride; + int64_t channelStride; + if (!GetPerChannelGammaStrides(gammas, numImages, channels, sampleStride, channelStride)) + { + LOG_ERROR("Per-channel gamma tensor must be dense or shaped as per-image channel slices"); + return ErrorCode::INVALID_DATA_SHAPE; + } + + copyPerChannelGammaValues<<>>(gammaArray, reinterpret_cast(gammas.basePtr()), + sampleStride, channelStride, numImages, channels); + checkKernelErrors(); + } + } + else + { + int64_t sampleStride; + if (!GetPerImageGammaStride(gammas, numImages, sampleStride)) + { + LOG_ERROR("Per-image gamma tensor must be dense or have a sample dimension matching the input batch"); + return ErrorCode::INVALID_DATA_SHAPE; + } + + copyGammaValues<<>>( + gammaArray, reinterpret_cast(gammas.basePtr()), sampleStride, numImages, channels); + checkKernelErrors(); + } + + return ErrorCode::SUCCESS; +} + +}} // namespace nvcv::legacy::cuda_op::gamma_contrast_detail + +#endif // CVCUDA_LEGACY_GAMMA_CONTRAST_COMMON_CUH diff --git a/src/cvcuda/priv/legacy/gamma_contrast_var_shape.cu b/src/cvcuda/priv/legacy/gamma_contrast_var_shape.cu index d9f32cfad..c390490b2 100644 --- a/src/cvcuda/priv/legacy/gamma_contrast_var_shape.cu +++ b/src/cvcuda/priv/legacy/gamma_contrast_var_shape.cu @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,6 +20,8 @@ #include "CvCudaUtils.cuh" +#include + #define BLOCK 32 using namespace nvcv::legacy::cuda_op; @@ -27,7 +29,7 @@ using namespace nvcv::legacy::helpers; namespace nvcv::legacy::cuda_op { -__global__ void copyGammaValues(float *gammaArray, const cuda::Tensor1DWrap gamma, const int numImages, +__global__ void copyGammaValues(float *gammaArray, const uint8_t *gammaBase, int64_t gammaStride, const int numImages, const int channelCount) { int index = threadIdx.x + blockIdx.x * blockDim.x; @@ -36,10 +38,100 @@ __global__ void copyGammaValues(float *gammaArray, const cuda::Tensor1DWrap(gammaBase + index * gammaStride); for (int i = 0; i < channelCount; i++) { - gammaArray[index * channelCount + i] = gamma[index]; + gammaArray[index * channelCount + i] = gamma; + } +} + +__global__ void copyPerChannelGammaValues(float *gammaArray, const uint8_t *gammaBase, int64_t sampleStride, + int64_t channelStride, const int numImages, const int channelCount) +{ + int index = threadIdx.x + blockIdx.x * blockDim.x; + if (index >= numImages * channelCount) + { + return; + } + + int imageIndex = index / channelCount; + int channelIndex = index % channelCount; + gammaArray[index] + = *reinterpret_cast(gammaBase + imageIndex * sampleStride + channelIndex * channelStride); +} + +static bool IsTensorDense(const TensorDataStridedCuda &tensor) +{ + int64_t expectedStride = sizeof(float); + for (int dim = tensor.rank() - 1; dim >= 0; --dim) + { + if (tensor.stride(dim) != expectedStride) + { + return false; + } + expectedStride *= tensor.shape(dim); } + return true; +} + +static bool GetPerImageGammaStride(const TensorDataStridedCuda &gammas, int numImages, int64_t &sampleStride) +{ + if (IsTensorDense(gammas)) + { + sampleStride = sizeof(float); + return true; + } + + const int sampleDim = gammas.layout().find(nvcv::LABEL_BATCH); + if (sampleDim >= 0 && gammas.shape(sampleDim) == numImages) + { + sampleStride = gammas.stride(sampleDim); + return true; + } + + if (gammas.rank() == 1 && gammas.shape(0) == numImages) + { + sampleStride = gammas.stride(0); + return true; + } + + if (gammas.rank() == 2 && gammas.shape(0) == numImages && gammas.shape(1) == 1) + { + sampleStride = gammas.stride(0); + return true; + } + + return false; +} + +static bool GetPerChannelGammaStrides(const TensorDataStridedCuda &gammas, int numImages, int channelCount, + int64_t &sampleStride, int64_t &channelStride) +{ + if (gammas.rank() == 1 && gammas.shape(0) == numImages * channelCount) + { + sampleStride = gammas.stride(0) * channelCount; + channelStride = gammas.stride(0); + return true; + } + + const int sampleDim = gammas.layout().find(nvcv::LABEL_BATCH); + const int channelDim = gammas.layout().find(nvcv::LABEL_CHANNEL); + if (sampleDim >= 0 && channelDim >= 0 && gammas.shape(sampleDim) == numImages + && gammas.shape(channelDim) == channelCount) + { + sampleStride = gammas.stride(sampleDim); + channelStride = gammas.stride(channelDim); + return true; + } + + if (gammas.rank() == 2 && gammas.shape(0) == numImages && gammas.shape(1) == channelCount) + { + sampleStride = gammas.stride(0); + channelStride = gammas.stride(1); + return true; + } + + return false; } // apply 255*((x/255)**gamma) on each pixel @@ -60,6 +152,90 @@ __global__ void gamma_contrast_kernel(const cuda::ImageBatchVarShapeWrap src, *dst.ptr(batch_idx, dst_y, dst_x) = out; } +// Single-channel U8 var-shape path: one thread owns sixteen adjacent pixels so image metadata and gamma +// lookup are amortized across sixteen independent power evaluations. Misaligned rows and trailing +// columns remain scalar. +__global__ void gamma_contrast_u8_kernel(const cuda::ImageBatchVarShapeWrap src, + cuda::ImageBatchVarShapeWrap dst, + const cuda::Tensor1DWrap gamma_) +{ + constexpr int NIX = 16; + + const int dst_x = (blockIdx.x * blockDim.x + threadIdx.x) * NIX; + const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + const int width = dst.width(batch_idx); + if (dst_x >= width || dst_y >= dst.height(batch_idx)) + return; + + const float gamma = gamma_[batch_idx]; + auto apply = [gamma](uchar value) + { + const float tmp = (value + 0.0f) / 255.0f; + return nvcv::cuda::SaturateCast(cuda::pow(tmp, gamma) * 255.0f); + }; + + const uchar *src_ptr = src.ptr(batch_idx, dst_y, dst_x); + uchar *dst_ptr = dst.ptr(batch_idx, dst_y, dst_x); + const bool aligned = ((reinterpret_cast(src_ptr) | reinterpret_cast(dst_ptr)) + & (alignof(uchar4) - 1)) + == 0; + + if (dst_x + NIX <= width && aligned) + { + const uchar4 input0 = *reinterpret_cast(src_ptr); + const uchar4 input1 = *reinterpret_cast(src_ptr + 4); + const uchar4 input2 = *reinterpret_cast(src_ptr + 8); + const uchar4 input3 = *reinterpret_cast(src_ptr + 12); + const uchar4 output0 = make_uchar4(apply(input0.x), apply(input0.y), apply(input0.z), apply(input0.w)); + const uchar4 output1 = make_uchar4(apply(input1.x), apply(input1.y), apply(input1.z), apply(input1.w)); + const uchar4 output2 = make_uchar4(apply(input2.x), apply(input2.y), apply(input2.z), apply(input2.w)); + const uchar4 output3 = make_uchar4(apply(input3.x), apply(input3.y), apply(input3.z), apply(input3.w)); + *reinterpret_cast(dst_ptr) = output0; + *reinterpret_cast(dst_ptr + 4) = output1; + *reinterpret_cast(dst_ptr + 8) = output2; + *reinterpret_cast(dst_ptr + 12) = output3; + } + else + { +#pragma unroll + for (int i = 0; i < NIX; ++i) + { + if (dst_x + i < width) + { + dst_ptr[i] = apply(src_ptr[i]); + } + } + } +} + +template +__global__ void gamma_contrast_u8_batched_kernel(const cuda::ImageBatchVarShapeWrap src, + cuda::ImageBatchVarShapeWrap dst, + const cuda::Tensor1DWrap gamma_) +{ + const int dst_x = (blockIdx.x * blockDim.x + threadIdx.x) * NIX; + const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + const int width = dst.width(batch_idx); + if (dst_x >= width || dst_y >= dst.height(batch_idx)) + return; + + const gamma_type gamma = gamma_[batch_idx]; + const D *src_ptr = src.ptr(batch_idx, dst_y, dst_x); + D *dst_ptr = dst.ptr(batch_idx, dst_y, dst_x); + +#pragma unroll + for (int i = 0; i < NIX; ++i) + { + if (dst_x + i < width) + { + const gamma_type tmp = (src_ptr[i] + 0.0f) / 255.0f; + dst_ptr[i] = nvcv::cuda::SaturateCast(cuda::pow(tmp, gamma) * 255.0f); + } + } +} + // apply (x**gamma) on each pixel template __global__ void gamma_contrast_float_kernel(const cuda::ImageBatchVarShapeWrap src, @@ -79,6 +255,199 @@ __global__ void gamma_contrast_float_kernel(const cuda::ImageBatchVarShapeWrap(out), 0.f, 1.f); } +template +__global__ void gamma_contrast_float_batched_kernel(const cuda::ImageBatchVarShapeWrap src, + cuda::ImageBatchVarShapeWrap dst, + const cuda::Tensor1DWrap gamma_) +{ + const int dst_x = (blockIdx.x * blockDim.x + threadIdx.x) * NIX; + const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + const int width = dst.width(batch_idx); + if (dst_x >= width || dst_y >= dst.height(batch_idx)) + return; + + const gamma_type gamma = gamma_[batch_idx]; + const D *src_ptr = src.ptr(batch_idx, dst_y, dst_x); + D *dst_ptr = dst.ptr(batch_idx, dst_y, dst_x); + +#pragma unroll + for (int i = 0; i < NIX; ++i) + { + if (dst_x + i < width) + { + const D out = nvcv::cuda::SaturateCast(cuda::pow(cuda::StaticCast(src_ptr[i]), gamma)); + dst_ptr[i] = cuda::clamp(cuda::StaticCast(out), 0.f, 1.f); + } + } +} + +// Planar (NCHW/CHW) gamma contrast. Gamma is applied per pixel per channel and is independent of the +// other channels, so grid.z runs over images and the kernel loops the channel planes -- reading the +// per-channel gamma as gammaValues[batch*channels + plane], exactly the value the interleaved kernel +// pulls from its vector gamma_[batch]. Each plane's result is bit-exact with the interleaved path. +template +__global__ void gamma_contrast_planar_kernel(const cuda::ImageBatchVarShapeWrap src, + cuda::ImageBatchVarShapeWrap dst, + const cuda::Tensor1DWrap gamma_, int channels) +{ + const int dst_x = blockIdx.x * blockDim.x + threadIdx.x; + const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + if (dst_x >= dst.width(batch_idx) || dst_y >= dst.height(batch_idx)) + return; + + for (int plane = 0; plane < channels; ++plane) + { + const float gamma = gamma_[batch_idx * channels + plane]; + const float tmp = (*src.ptr(batch_idx, plane, dst_y, dst_x) + 0.0f) / 255.0f; + + *dst.ptr(batch_idx, plane, dst_y, dst_x) = nvcv::cuda::SaturateCast(cuda::pow(tmp, gamma) * 255.0f); + } +} + +__global__ void gamma_contrast_planar_u8_kernel(const cuda::ImageBatchVarShapeWrap src, + cuda::ImageBatchVarShapeWrap dst, + const cuda::Tensor1DWrap gamma_, int channels) +{ + constexpr int NIX = 16; + + const int dst_x = (blockIdx.x * blockDim.x + threadIdx.x) * NIX; + const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + const int width = dst.width(batch_idx); + if (dst_x >= width || dst_y >= dst.height(batch_idx)) + return; + + for (int plane = 0; plane < channels; ++plane) + { + const float gamma = gamma_[batch_idx * channels + plane]; + auto apply = [gamma](uchar value) + { + const float tmp = (value + 0.0f) / 255.0f; + return nvcv::cuda::SaturateCast(cuda::pow(tmp, gamma) * 255.0f); + }; + + const uchar *src_ptr = src.ptr(batch_idx, plane, dst_y, dst_x); + uchar *dst_ptr = dst.ptr(batch_idx, plane, dst_y, dst_x); + const bool aligned = ((reinterpret_cast(src_ptr) | reinterpret_cast(dst_ptr)) + & (alignof(uchar4) - 1)) + == 0; + + if (dst_x + NIX <= width && aligned) + { + const uchar4 input0 = *reinterpret_cast(src_ptr); + const uchar4 input1 = *reinterpret_cast(src_ptr + 4); + const uchar4 input2 = *reinterpret_cast(src_ptr + 8); + const uchar4 input3 = *reinterpret_cast(src_ptr + 12); + const uchar4 output0 = make_uchar4(apply(input0.x), apply(input0.y), apply(input0.z), apply(input0.w)); + const uchar4 output1 = make_uchar4(apply(input1.x), apply(input1.y), apply(input1.z), apply(input1.w)); + const uchar4 output2 = make_uchar4(apply(input2.x), apply(input2.y), apply(input2.z), apply(input2.w)); + const uchar4 output3 = make_uchar4(apply(input3.x), apply(input3.y), apply(input3.z), apply(input3.w)); + *reinterpret_cast(dst_ptr) = output0; + *reinterpret_cast(dst_ptr + 4) = output1; + *reinterpret_cast(dst_ptr + 8) = output2; + *reinterpret_cast(dst_ptr + 12) = output3; + } + else + { +#pragma unroll + for (int i = 0; i < NIX; ++i) + { + if (dst_x + i < width) + { + dst_ptr[i] = apply(src_ptr[i]); + } + } + } + } +} + +// Planar gamma contrast for float images: apply (x**gamma) and clamp to [0, 1], per channel plane. +template +__global__ void gamma_contrast_float_planar_kernel(const cuda::ImageBatchVarShapeWrap src, + cuda::ImageBatchVarShapeWrap dst, + const cuda::Tensor1DWrap gamma_, int channels) +{ + const int dst_x = blockIdx.x * blockDim.x + threadIdx.x; + const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + if (dst_x >= dst.width(batch_idx) || dst_y >= dst.height(batch_idx)) + return; + + for (int plane = 0; plane < channels; ++plane) + { + const float gamma = gamma_[batch_idx * channels + plane]; + + D out = nvcv::cuda::SaturateCast( + cuda::pow(cuda::StaticCast(*src.ptr(batch_idx, plane, dst_y, dst_x)), gamma)); + + *dst.ptr(batch_idx, plane, dst_y, dst_x) = cuda::clamp(cuda::StaticCast(out), 0.f, 1.f); + } +} + +template +void gamma_contrast_planar(const ImageBatchVarShapeDataStridedCuda &in, const ImageBatchVarShapeDataStridedCuda &out, + float *gammaValues, int channels, cudaStream_t stream) +{ + int max_width = in.maxSize().w; + int max_height = in.maxSize().h; + int batch = in.numImages(); + + dim3 block(BLOCK, BLOCK / 4, 1); + dim3 grid(divUp(max_width, block.x), divUp(max_height, block.y), batch); + cuda::ImageBatchVarShapeWrap src_ptr(in); + cuda::ImageBatchVarShapeWrap dst_ptr(out); + + cuda::Tensor1DWrap gamma(gammaValues); + gamma_contrast_planar_kernel<<>>(src_ptr, dst_ptr, gamma, channels); + + checkKernelErrors(); +#ifdef CUDA_DEBUG_LOG + checkCudaErrors(cudaStreamSynchronize(stream)); + checkCudaErrors(cudaGetLastError()); +#endif +} + +static void gamma_contrast_planar_u8(const ImageBatchVarShapeDataStridedCuda &in, + const ImageBatchVarShapeDataStridedCuda &out, float *gammaValues, int channels, + cudaStream_t stream) +{ + constexpr int NIX = 16; + + int max_width = in.maxSize().w; + int max_height = in.maxSize().h; + int batch = in.numImages(); + + dim3 block(BLOCK, BLOCK / 4, 1); + dim3 grid(divUp(max_width, block.x * NIX), divUp(max_height, block.y), batch); + cuda::ImageBatchVarShapeWrap src_ptr(in); + cuda::ImageBatchVarShapeWrap dst_ptr(out); + + cuda::Tensor1DWrap gamma(gammaValues); + gamma_contrast_planar_u8_kernel<<>>(src_ptr, dst_ptr, gamma, channels); + checkKernelErrors(); +} + +template +void gamma_contrast_float_planar(const ImageBatchVarShapeDataStridedCuda &in, + const ImageBatchVarShapeDataStridedCuda &out, float *gammaValues, int channels, + cudaStream_t stream) +{ + int max_width = in.maxSize().w; + int max_height = in.maxSize().h; + int batch = in.numImages(); + + dim3 block(BLOCK, BLOCK / 4, 1); + dim3 grid(divUp(max_width, block.x), divUp(max_height, block.y), batch); + cuda::ImageBatchVarShapeWrap src_ptr(in); + cuda::ImageBatchVarShapeWrap dst_ptr(out); + + cuda::Tensor1DWrap gamma(gammaValues); + gamma_contrast_float_planar_kernel<<>>(src_ptr, dst_ptr, gamma, channels); + checkKernelErrors(); +} + template void gamma_contrast(const ImageBatchVarShapeDataStridedCuda &in, const ImageBatchVarShapeDataStridedCuda &out, float *gammaValues, cudaStream_t stream) @@ -103,6 +472,46 @@ void gamma_contrast(const ImageBatchVarShapeDataStridedCuda &in, const ImageBatc #endif } +static void gamma_contrast_u8(const ImageBatchVarShapeDataStridedCuda &in, const ImageBatchVarShapeDataStridedCuda &out, + float *gammaValues, cudaStream_t stream) +{ + constexpr int NIX = 16; + + int max_width = in.maxSize().w; + int max_height = in.maxSize().h; + int batch = in.numImages(); + + dim3 block(BLOCK, BLOCK / 4, 1); + dim3 grid(divUp(max_width, block.x * NIX), divUp(max_height, block.y), batch); + cuda::ImageBatchVarShapeWrap src_ptr(in); + cuda::ImageBatchVarShapeWrap dst_ptr(out); + + cuda::Tensor1DWrap gamma(gammaValues); + gamma_contrast_u8_kernel<<>>(src_ptr, dst_ptr, gamma); + checkKernelErrors(); +} + +template +void gamma_contrast_u8_batched(const ImageBatchVarShapeDataStridedCuda &in, + const ImageBatchVarShapeDataStridedCuda &out, float *gammaValues, cudaStream_t stream) +{ + constexpr int NIX = sizeof(T) == sizeof(uchar4) ? 3 : 2; + + int max_width = in.maxSize().w; + int max_height = in.maxSize().h; + int batch = in.numImages(); + + dim3 block(BLOCK, BLOCK / 4, 1); + dim3 grid(divUp(max_width, block.x * NIX), divUp(max_height, block.y), batch); + cuda::ImageBatchVarShapeWrap src_ptr(in); + cuda::ImageBatchVarShapeWrap dst_ptr(out); + + using gamma_type = cuda::ConvertBaseTypeTo; + cuda::Tensor1DWrap gamma(gammaValues); + gamma_contrast_u8_batched_kernel<<>>(src_ptr, dst_ptr, gamma); + checkKernelErrors(); +} + template void gamma_contrast_float(const ImageBatchVarShapeDataStridedCuda &in, const ImageBatchVarShapeDataStridedCuda &out, float *gammaValues, cudaStream_t stream) @@ -122,6 +531,27 @@ void gamma_contrast_float(const ImageBatchVarShapeDataStridedCuda &in, const Ima checkKernelErrors(); } +template +void gamma_contrast_float_batched(const ImageBatchVarShapeDataStridedCuda &in, + const ImageBatchVarShapeDataStridedCuda &out, float *gammaValues, cudaStream_t stream) +{ + constexpr int NIX = 2; + + int max_width = in.maxSize().w; + int max_height = in.maxSize().h; + int batch = in.numImages(); + + dim3 block(BLOCK, BLOCK / 4, 1); + dim3 grid(divUp(max_width, block.x * NIX), divUp(max_height, block.y), batch); + cuda::ImageBatchVarShapeWrap src_ptr(in); + cuda::ImageBatchVarShapeWrap dst_ptr(out); + + using gamma_type = cuda::ConvertBaseTypeTo; + cuda::Tensor1DWrap gamma(gammaValues); + gamma_contrast_float_batched_kernel<<>>(src_ptr, dst_ptr, gamma); + checkKernelErrors(); +} + GammaContrastVarShape::GammaContrastVarShape(const int32_t maxVarShapeBatchSize, const int32_t maxVarShapeChannelCount) : CudaBaseOp() , m_maxBatchSize(maxVarShapeBatchSize) @@ -176,38 +606,49 @@ ErrorCode GammaContrastVarShape::infer(const ImageBatchVarShapeDataStridedCuda & DataFormat format = input_format; - if (!(format == kNHWC || format == kHWC)) + if (!(format == kNHWC || format == kHWC || format == kNCHW || format == kCHW)) { - LOG_ERROR("Invliad DataFormat " << format); + LOG_ERROR("Invalid DataFormat " << format + << ", the valid DataFormats are: \"NHWC\", \"HWC\", \"NCHW\", \"CHW\""); return ErrorCode::INVALID_DATA_FORMAT; } + const bool isPlanar = (format == kNCHW || format == kCHW); + DataType data_type = helpers::GetLegacyDataType(inData.uniqueFormat()); - if (!(data_type == kCV_8U || data_type == kCV_8S || data_type == kCV_16U || data_type == kCV_16S - || data_type == kCV_32S || data_type == kCV_32F)) + if (!(data_type == kCV_8U || data_type == kCV_16U || data_type == kCV_16S || data_type == kCV_32S + || data_type == kCV_32F)) { LOG_ERROR("Invalid DataType " << data_type); return ErrorCode::INVALID_DATA_TYPE; } DataType out_data_type = helpers::GetLegacyDataType(outData.uniqueFormat()); + int channels = inData.uniqueFormat().numChannels(); + int outChannels = outData.uniqueFormat().numChannels(); - if (!(out_data_type == kCV_8U || out_data_type == kCV_32F)) + if (out_data_type != data_type || outChannels != channels) { - LOG_ERROR("Invalid Output DataType " << out_data_type); - return ErrorCode::INVALID_DATA_TYPE; + LOG_ERROR("Input DataType " << data_type << " and channel count " << channels << " must match Output DataType " + << out_data_type << " and channel count " << outChannels); + return out_data_type != data_type ? ErrorCode::INVALID_DATA_TYPE : ErrorCode::INVALID_DATA_SHAPE; } - int channels = inData.uniqueFormat().numChannels(); - if (channels > 4) + // Planar 2-channel is rejected: there is no defined 2-plane planar format (matches Resize/Flip). + if (channels > 4 || (isPlanar && channels == 2)) { LOG_ERROR("Invalid channel number " << channels); return ErrorCode::INVALID_DATA_SHAPE; } - auto gammasAccess = nvcv::TensorDataAccessStrided::Create(gammas); - NVCV_ASSERT(gammasAccess); + // The planar path launches grid.z over images and loops the channel planes inside the kernel, so + // numImages must fit CUDA's 65535 grid-z limit. + if (isPlanar && static_cast(inData.numImages()) > 65535) + { + LOG_ERROR("Planar gamma contrast requires numImages <= 65535 (CUDA grid-z limit)"); + return ErrorCode::INVALID_DATA_SHAPE; + } int numElements = 1; for (int i = 0; i < gammas.rank(); i++) @@ -215,18 +656,48 @@ ErrorCode GammaContrastVarShape::infer(const ImageBatchVarShapeDataStridedCuda & numElements *= gammas.shape(i); } - if (inData.numImages() * channels == numElements) + int numImages = inData.numImages(); + if (numElements != numImages && numElements != numImages * channels) { - // Copy the data device to device - checkCudaErrors(cudaMemcpyAsync(m_gammaArray, gammasAccess->sampleData(0), - sizeof(float) * inData.numImages() * channels, cudaMemcpyDeviceToDevice, - stream)); + LOG_ERROR("Invalid gamma tensor length " << numElements << ", expected " << numImages << " or " + << numImages * channels); + return ErrorCode::INVALID_DATA_SHAPE; + } + + if (numImages * channels == numElements) + { + if (IsTensorDense(gammas)) + { + checkCudaErrors(cudaMemcpyAsync(m_gammaArray, gammas.basePtr(), sizeof(float) * numImages * channels, + cudaMemcpyDeviceToDevice, stream)); + } + else + { + int64_t sampleStride; + int64_t channelStride; + if (!GetPerChannelGammaStrides(gammas, numImages, channels, sampleStride, channelStride)) + { + LOG_ERROR("Per-channel gamma tensor must be dense or shaped as per-image channel slices"); + return ErrorCode::INVALID_DATA_SHAPE; + } + + copyPerChannelGammaValues<<>>( + m_gammaArray, reinterpret_cast(gammas.basePtr()), sampleStride, channelStride, + numImages, channels); + checkKernelErrors(); + } } else { - cuda::Tensor1DWrap gammaTensorWrap(gammas); - copyGammaValues<<<1, inData.numImages(), 0, stream>>>(m_gammaArray, gammaTensorWrap, inData.numImages(), - channels); + int64_t sampleStride; + if (!GetPerImageGammaStride(gammas, numImages, sampleStride)) + { + LOG_ERROR("Per-image gamma tensor must be dense or have a sample dimension matching the input batch"); + return ErrorCode::INVALID_DATA_SHAPE; + } + + copyGammaValues<<>>( + m_gammaArray, reinterpret_cast(gammas.basePtr()), sampleStride, numImages, channels); checkKernelErrors(); } @@ -234,17 +705,44 @@ ErrorCode GammaContrastVarShape::infer(const ImageBatchVarShapeDataStridedCuda & const nvcv::ImageBatchVarShapeDataStridedCuda &out, float *gammas, cudaStream_t stream); static const func_t funcs[5][4] = { - { gamma_contrast, gamma_contrast, gamma_contrast,gamma_contrast }, - {0 /*gamma_contrast*/, 0 /*gamma_contrast*/, 0 /*gamma_contrast*/, - 0 /*gamma_contrast*/ }, - { gamma_contrast, gamma_contrast, gamma_contrast, gamma_contrast}, - { gamma_contrast, gamma_contrast, gamma_contrast, gamma_contrast}, - { gamma_contrast, gamma_contrast, gamma_contrast, gamma_contrast}, + { gamma_contrast_u8, gamma_contrast, gamma_contrast_u8_batched, + gamma_contrast_u8_batched }, + {0 /*gamma_contrast*/, 0 /*gamma_contrast*/, 0 /*gamma_contrast*/, + 0 /*gamma_contrast*/ }, + { gamma_contrast, gamma_contrast, gamma_contrast, gamma_contrast}, + { gamma_contrast, gamma_contrast, gamma_contrast, gamma_contrast}, + { gamma_contrast, gamma_contrast, gamma_contrast, gamma_contrast}, }; - static const func_t funcs_float[4] = {gamma_contrast_float, gamma_contrast_float, + static const func_t funcs_float[4] = {gamma_contrast_float_batched, gamma_contrast_float, gamma_contrast_float, gamma_contrast_float}; + if (isPlanar) + { + // Planar dispatch indexes by dtype only: each channel is a separate single-channel plane, so + // one scalar specialization per dtype covers any channel count (the kernel loops the planes). + typedef void (*planar_func_t)(const ImageBatchVarShapeDataStridedCuda &in, + const ImageBatchVarShapeDataStridedCuda &out, float *gammas, int channels, + cudaStream_t stream); + + static const planar_func_t planar_funcs[5] + = {gamma_contrast_planar_u8, 0 /*schar*/, gamma_contrast_planar, gamma_contrast_planar, + gamma_contrast_planar}; + + if (data_type == kCV_32F) + { + gamma_contrast_float_planar(inData, outData, m_gammaArray, channels, stream); + } + else + { + const planar_func_t planar_func = planar_funcs[data_type]; + NVCV_ASSERT(planar_func != nullptr); + planar_func(inData, outData, m_gammaArray, channels, stream); + } + + return ErrorCode::SUCCESS; + } + if (data_type == kCV_32F) { const func_t func = funcs_float[channels - 1]; diff --git a/src/cvcuda/priv/legacy/gaussian_noise.cu b/src/cvcuda/priv/legacy/gaussian_noise.cu index 91b097e32..e2f265d20 100644 --- a/src/cvcuda/priv/legacy/gaussian_noise.cu +++ b/src/cvcuda/priv/legacy/gaussian_noise.cu @@ -1,4 +1,4 @@ -/* Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +/* Copyright (c) 2021-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * * SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES * SPDX-License-Identifier: Apache-2.0 @@ -18,6 +18,7 @@ * limitations under the License. */ +#include "../SafeSize.hpp" #include "CvCudaLegacy.h" #include "CvCudaLegacyHelpers.hpp" @@ -35,161 +36,765 @@ using namespace nvcv::cuda; template __global__ void gaussian_noise_kernel(const Tensor3DWrap src, Tensor3DWrap dst, - curandState *state, Tensor1DWrap mu, + curandState *state, curandState *nextState, Tensor1DWrap mu, Tensor1DWrap sigma, int rows, int cols) { - int offset = threadIdx.x; int batch_idx = blockIdx.x; int id = threadIdx.x + blockIdx.x * blockDim.x; int total_size = rows * cols; - curandState localState = state[id]; - while (offset < total_size) + int offset = 0; + int segmentEnd = 0; + const float batchMu = mu[batch_idx]; + const float batchSigma = sigma[batch_idx]; + curandState localState; + if (!gaussian_noise_segment_state(state, id, total_size, 1, offset, segmentEnd, localState)) + { + return; + } + while (offset + blockDim.x < segmentEnd) + { + int dst_x0 = offset % cols; + int dst_y0 = offset / cols; + int offset1 = offset + blockDim.x; + int dst_x1 = offset1 % cols; + int dst_y1 = offset1 / cols; + float2 rand = gaussian_noise_normal2(localState); + float delta0 = batchMu + rand.x * batchSigma; + float delta1 = batchMu + rand.y * batchSigma; + *dst.ptr(batch_idx, dst_y0, dst_x0) = SaturateCast(*src.ptr(batch_idx, dst_y0, dst_x0) + delta0); + *dst.ptr(batch_idx, dst_y1, dst_x1) = SaturateCast(*src.ptr(batch_idx, dst_y1, dst_x1) + delta1); + offset += 2 * blockDim.x; + } + while (offset < segmentEnd) { int dst_x = offset % cols; int dst_y = offset / cols; float rand = curand_normal(&localState); - float delta = mu[batch_idx] + rand * sigma[batch_idx]; + float delta = batchMu + rand * batchSigma; *dst.ptr(batch_idx, dst_y, dst_x) = SaturateCast(*src.ptr(batch_idx, dst_y, dst_x) + delta); offset += blockDim.x; } - state[id] = localState; + gaussian_noise_store_segment_state(state, nextState, id, total_size, segmentEnd, localState); } template __global__ void gaussian_noise_per_channel_kernel(const Tensor4DWrap src, Tensor4DWrap dst, curandState *state, - Tensor1DWrap mu, Tensor1DWrap sigma, - int rows, int cols, int channel) + curandState *nextState, Tensor1DWrap mu, + Tensor1DWrap sigma, int rows, int cols, int channel) { - int offset = threadIdx.x; int batch_idx = blockIdx.x; int id = threadIdx.x + blockIdx.x * blockDim.x; int total_size = rows * cols; - curandState localState = state[id]; - while (offset < total_size) + int offset = 0; + int segmentEnd = 0; + const float batchMu = mu[batch_idx]; + const float batchSigma = sigma[batch_idx]; + curandState localState; + if (!gaussian_noise_segment_state(state, id, total_size, channel, offset, segmentEnd, localState)) + { + return; + } + while (offset + blockDim.x < segmentEnd) + { + int dst_x0 = offset % cols; + int dst_y0 = offset / cols; + int offset1 = offset + blockDim.x; + int dst_x1 = offset1 % cols; + int dst_y1 = offset1 / cols; + gaussian_noise_store_channel_pair(src, dst, localState, batch_idx, dst_y0, dst_x0, dst_y1, dst_x1, + channel, batchMu, batchSigma); + offset += 2 * blockDim.x; + } + while (offset < segmentEnd) { int dst_x = offset % cols; int dst_y = offset / cols; for (int ch = 0; ch < channel; ch++) { float rand = curand_normal(&localState); - float delta = mu[batch_idx] + rand * sigma[batch_idx]; + float delta = batchMu + rand * batchSigma; *dst.ptr(batch_idx, dst_y, dst_x, ch) = SaturateCast(*src.ptr(batch_idx, dst_y, dst_x, ch) + delta); } offset += blockDim.x; } - state[id] = localState; + gaussian_noise_store_segment_state(state, nextState, id, total_size, segmentEnd, localState); } template __global__ void gaussian_noise_float_kernel(const Tensor3DWrap src, Tensor3DWrap dst, - curandState *state, Tensor1DWrap mu, + curandState *state, curandState *nextState, Tensor1DWrap mu, Tensor1DWrap sigma, int rows, int cols) { - int offset = threadIdx.x; int batch_idx = blockIdx.x; int id = threadIdx.x + blockIdx.x * blockDim.x; int total_size = rows * cols; - curandState localState = state[id]; - while (offset < total_size) + int offset = 0; + int segmentEnd = 0; + const float batchMu = mu[batch_idx]; + const float batchSigma = sigma[batch_idx]; + curandState localState; + if (!gaussian_noise_segment_state(state, id, total_size, 1, offset, segmentEnd, localState)) + { + return; + } + while (offset + blockDim.x < segmentEnd) + { + int dst_x0 = offset % cols; + int dst_y0 = offset / cols; + int offset1 = offset + blockDim.x; + int dst_x1 = offset1 % cols; + int dst_y1 = offset1 / cols; + float2 rand = gaussian_noise_normal2(localState); + float delta0 = batchMu + rand.x * batchSigma; + float delta1 = batchMu + rand.y * batchSigma; + T out0 = SaturateCast(*src.ptr(batch_idx, dst_y0, dst_x0) + delta0); + T out1 = SaturateCast(*src.ptr(batch_idx, dst_y1, dst_x1) + delta1); + *dst.ptr(batch_idx, dst_y0, dst_x0) = clamp(StaticCast(out0), 0.f, 1.f); + *dst.ptr(batch_idx, dst_y1, dst_x1) = clamp(StaticCast(out1), 0.f, 1.f); + offset += 2 * blockDim.x; + } + while (offset < segmentEnd) { int dst_x = offset % cols; int dst_y = offset / cols; float rand = curand_normal(&localState); - float delta = mu[batch_idx] + rand * sigma[batch_idx]; + float delta = batchMu + rand * batchSigma; T out = SaturateCast(*src.ptr(batch_idx, dst_y, dst_x) + delta); *dst.ptr(batch_idx, dst_y, dst_x) = clamp(StaticCast(out), 0.f, 1.f); offset += blockDim.x; } - state[id] = localState; + gaussian_noise_store_segment_state(state, nextState, id, total_size, segmentEnd, localState); } template __global__ void gaussian_noise_float_per_channel_kernel(const Tensor4DWrap src, Tensor4DWrap dst, curandState *state, - Tensor1DWrap mu, + curandState *nextState, Tensor1DWrap mu, Tensor1DWrap sigma, int rows, int cols, int channel) { - int offset = threadIdx.x; int batch_idx = blockIdx.x; int id = threadIdx.x + blockIdx.x * blockDim.x; int total_size = rows * cols; - curandState localState = state[id]; - while (offset < total_size) + int offset = 0; + int segmentEnd = 0; + const float batchMu = mu[batch_idx]; + const float batchSigma = sigma[batch_idx]; + curandState localState; + if (!gaussian_noise_segment_state(state, id, total_size, channel, offset, segmentEnd, localState)) + { + return; + } + while (offset + blockDim.x < segmentEnd) + { + int dst_x0 = offset % cols; + int dst_y0 = offset / cols; + int offset1 = offset + blockDim.x; + int dst_x1 = offset1 % cols; + int dst_y1 = offset1 / cols; + gaussian_noise_store_channel_pair(src, dst, localState, batch_idx, dst_y0, dst_x0, dst_y1, dst_x1, + channel, batchMu, batchSigma); + offset += 2 * blockDim.x; + } + while (offset < segmentEnd) { int dst_x = offset % cols; int dst_y = offset / cols; for (int ch = 0; ch < channel; ch++) { float rand = curand_normal(&localState); - float delta = mu[batch_idx] + rand * sigma[batch_idx]; + float delta = batchMu + rand * batchSigma; T out = SaturateCast(*src.ptr(batch_idx, dst_y, dst_x, ch) + delta); *dst.ptr(batch_idx, dst_y, dst_x, ch) = clamp(StaticCast(out), 0.f, 1.f); } offset += blockDim.x; } - state[id] = localState; + gaussian_noise_store_segment_state(state, nextState, id, total_size, segmentEnd, localState); +} + +template +__global__ void gaussian_noise_planar_kernel(const Tensor4DWrap src, Tensor4DWrap dst, + curandState *state, curandState *nextState, + Tensor1DWrap mu, Tensor1DWrap sigma, + int rows, int cols, int channels) +{ + int batch_idx = blockIdx.x; + int id = threadIdx.x + blockIdx.x * blockDim.x; + int total_size = rows * cols; + int offset = 0; + int segmentEnd = 0; + const float batchMu = mu[batch_idx]; + const float batchSigma = sigma[batch_idx]; + curandState localState; + if (!gaussian_noise_segment_state(state, id, total_size, 1, offset, segmentEnd, localState)) + { + return; + } + while (offset + blockDim.x < segmentEnd) + { + int dst_x0 = offset % cols; + int dst_y0 = offset / cols; + int offset1 = offset + blockDim.x; + int dst_x1 = offset1 % cols; + int dst_y1 = offset1 / cols; + float2 rand = gaussian_noise_normal2(localState); + float delta0 = batchMu + rand.x * batchSigma; + float delta1 = batchMu + rand.y * batchSigma; + for (int ch = 0; ch < channels; ch++) + { + *dst.ptr(batch_idx, ch, dst_y0, dst_x0) = SaturateCast(*src.ptr(batch_idx, ch, dst_y0, dst_x0) + delta0); + *dst.ptr(batch_idx, ch, dst_y1, dst_x1) = SaturateCast(*src.ptr(batch_idx, ch, dst_y1, dst_x1) + delta1); + } + offset += 2 * blockDim.x; + } + while (offset < segmentEnd) + { + int dst_x = offset % cols; + int dst_y = offset / cols; + float rand = curand_normal(&localState); + float delta = batchMu + rand * batchSigma; + for (int ch = 0; ch < channels; ch++) + { + *dst.ptr(batch_idx, ch, dst_y, dst_x) = SaturateCast(*src.ptr(batch_idx, ch, dst_y, dst_x) + delta); + } + offset += blockDim.x; + } + gaussian_noise_store_segment_state(state, nextState, id, total_size, segmentEnd, localState); +} + +template +__global__ void gaussian_noise_planar_per_channel_kernel(const Tensor4DWrap src, + Tensor4DWrap dst, curandState *state, + curandState *nextState, Tensor1DWrap mu, + Tensor1DWrap sigma, int rows, int cols, + int channels) +{ + int batch_idx = blockIdx.x; + int id = threadIdx.x + blockIdx.x * blockDim.x; + int total_size = rows * cols; + int offset = 0; + int segmentEnd = 0; + const float batchMu = mu[batch_idx]; + const float batchSigma = sigma[batch_idx]; + curandState localState; + if (!gaussian_noise_segment_state(state, id, total_size, channels, offset, segmentEnd, localState)) + { + return; + } + while (offset + blockDim.x < segmentEnd) + { + int dst_x0 = offset % cols; + int dst_y0 = offset / cols; + int offset1 = offset + blockDim.x; + int dst_x1 = offset1 % cols; + int dst_y1 = offset1 / cols; + gaussian_noise_store_channel_pair(src, dst, localState, batch_idx, dst_y0, dst_x0, dst_y1, dst_x1, + channels, batchMu, batchSigma); + offset += 2 * blockDim.x; + } + while (offset < segmentEnd) + { + int dst_x = offset % cols; + int dst_y = offset / cols; + for (int ch = 0; ch < channels; ch++) + { + float rand = curand_normal(&localState); + float delta = batchMu + rand * batchSigma; + *dst.ptr(batch_idx, ch, dst_y, dst_x) = SaturateCast(*src.ptr(batch_idx, ch, dst_y, dst_x) + delta); + } + offset += blockDim.x; + } + gaussian_noise_store_segment_state(state, nextState, id, total_size, segmentEnd, localState); +} + +template +__global__ void gaussian_noise_float_planar_kernel(const Tensor4DWrap src, + Tensor4DWrap dst, curandState *state, + curandState *nextState, Tensor1DWrap mu, + Tensor1DWrap sigma, int rows, int cols, int channels) +{ + int batch_idx = blockIdx.x; + int id = threadIdx.x + blockIdx.x * blockDim.x; + int total_size = rows * cols; + int offset = 0; + int segmentEnd = 0; + const float batchMu = mu[batch_idx]; + const float batchSigma = sigma[batch_idx]; + curandState localState; + if (!gaussian_noise_segment_state(state, id, total_size, 1, offset, segmentEnd, localState)) + { + return; + } + while (offset + blockDim.x < segmentEnd) + { + int dst_x0 = offset % cols; + int dst_y0 = offset / cols; + int offset1 = offset + blockDim.x; + int dst_x1 = offset1 % cols; + int dst_y1 = offset1 / cols; + float2 rand = gaussian_noise_normal2(localState); + float delta0 = batchMu + rand.x * batchSigma; + float delta1 = batchMu + rand.y * batchSigma; + for (int ch = 0; ch < channels; ch++) + { + T out0 = SaturateCast(*src.ptr(batch_idx, ch, dst_y0, dst_x0) + delta0); + T out1 = SaturateCast(*src.ptr(batch_idx, ch, dst_y1, dst_x1) + delta1); + *dst.ptr(batch_idx, ch, dst_y0, dst_x0) = clamp(StaticCast(out0), 0.f, 1.f); + *dst.ptr(batch_idx, ch, dst_y1, dst_x1) = clamp(StaticCast(out1), 0.f, 1.f); + } + offset += 2 * blockDim.x; + } + while (offset < segmentEnd) + { + int dst_x = offset % cols; + int dst_y = offset / cols; + float rand = curand_normal(&localState); + float delta = batchMu + rand * batchSigma; + for (int ch = 0; ch < channels; ch++) + { + T out = SaturateCast(*src.ptr(batch_idx, ch, dst_y, dst_x) + delta); + *dst.ptr(batch_idx, ch, dst_y, dst_x) = clamp(StaticCast(out), 0.f, 1.f); + } + offset += blockDim.x; + } + gaussian_noise_store_segment_state(state, nextState, id, total_size, segmentEnd, localState); +} + +template +__global__ void gaussian_noise_float_planar_per_channel_kernel(const Tensor4DWrap src, + Tensor4DWrap dst, curandState *state, + curandState *nextState, Tensor1DWrap mu, + Tensor1DWrap sigma, int rows, int cols, + int channels) +{ + int batch_idx = blockIdx.x; + int id = threadIdx.x + blockIdx.x * blockDim.x; + int total_size = rows * cols; + int offset = 0; + int segmentEnd = 0; + const float batchMu = mu[batch_idx]; + const float batchSigma = sigma[batch_idx]; + curandState localState; + if (!gaussian_noise_segment_state(state, id, total_size, channels, offset, segmentEnd, localState)) + { + return; + } + while (offset + blockDim.x < segmentEnd) + { + int dst_x0 = offset % cols; + int dst_y0 = offset / cols; + int offset1 = offset + blockDim.x; + int dst_x1 = offset1 % cols; + int dst_y1 = offset1 / cols; + gaussian_noise_store_channel_pair(src, dst, localState, batch_idx, dst_y0, dst_x0, dst_y1, dst_x1, + channels, batchMu, batchSigma); + offset += 2 * blockDim.x; + } + while (offset < segmentEnd) + { + int dst_x = offset % cols; + int dst_y = offset / cols; + for (int ch = 0; ch < channels; ch++) + { + float rand = curand_normal(&localState); + float delta = batchMu + rand * batchSigma; + T out = SaturateCast(*src.ptr(batch_idx, ch, dst_y, dst_x) + delta); + *dst.ptr(batch_idx, ch, dst_y, dst_x) = clamp(StaticCast(out), 0.f, 1.f); + } + offset += blockDim.x; + } + gaussian_noise_store_segment_state(state, nextState, id, total_size, segmentEnd, localState); +} + +template +struct GaussianNoiseScalarOutput; + +template +struct GaussianNoiseScalarOutput +{ + static __device__ uchar Convert(uchar value, float delta) + { + // Torchvision converts the generated noise to int16 before adding it to + // uint8 input. The conversion truncates toward zero; the final uint8 + // conversion either clamps or wraps depending on clip. + int noise = static_cast(static_cast(__float2int_rz(delta))); + int sum = static_cast(value) + noise; + if constexpr (Clip) + { + sum = sum < 0 ? 0 : (sum > 255 ? 255 : sum); + } + return static_cast(sum); + } +}; + +template +struct GaussianNoiseScalarOutput +{ + __device__ static float Convert(float value, float delta) + { + float out = value + delta; + if constexpr (Clip) + { + out = clamp(out, 0.f, 1.f); + } + return out; + } +}; + +template +__device__ __forceinline__ void gaussian_noise_scalar_store_channel(const SrcWrap &src, const DstWrap &dst, + int batchIdx, int y, int x, int ch, float delta) +{ + if constexpr (Planar) + { + *dst.ptr(batchIdx, ch, y, x) = GaussianNoiseScalarOutput::Convert(*src.ptr(batchIdx, ch, y, x), delta); + } + else + { + *dst.ptr(batchIdx, y, x, ch) = GaussianNoiseScalarOutput::Convert(*src.ptr(batchIdx, y, x, ch), delta); + } +} + +template +__device__ __forceinline__ void gaussian_noise_scalar_store_channel_pair(const SrcWrap &src, const DstWrap &dst, + curandState &localState, int batchIdx, int y0, + int x0, int y1, int x1, int channels, float mu, + float sigma) +{ + int ch = 0; + for (; ch + 1 < channels; ch += 2) + { + float2 rand = gaussian_noise_normal2(localState); + gaussian_noise_scalar_store_channel(src, dst, batchIdx, y0, x0, ch, mu + rand.x * sigma); + gaussian_noise_scalar_store_channel(src, dst, batchIdx, y0, x0, ch + 1, mu + rand.y * sigma); + } + if (ch < channels) + { + float2 rand = gaussian_noise_normal2(localState); + gaussian_noise_scalar_store_channel(src, dst, batchIdx, y0, x0, ch, mu + rand.x * sigma); + gaussian_noise_scalar_store_channel(src, dst, batchIdx, y1, x1, 0, mu + rand.y * sigma); + ch = 1; + } + else + { + ch = 0; + } + for (; ch + 1 < channels; ch += 2) + { + float2 rand = gaussian_noise_normal2(localState); + gaussian_noise_scalar_store_channel(src, dst, batchIdx, y1, x1, ch, mu + rand.x * sigma); + gaussian_noise_scalar_store_channel(src, dst, batchIdx, y1, x1, ch + 1, mu + rand.y * sigma); + } + if (ch < channels) + { + float rand = curand_normal(&localState); + gaussian_noise_scalar_store_channel(src, dst, batchIdx, y1, x1, ch, mu + rand * sigma); + } +} + +template +__global__ void gaussian_noise_scalar_kernel(const SrcWrap src, DstWrap dst, curandState *state, curandState *nextState, + float mu, float sigma, int rows, int cols, int channels) +{ + int batchIdx = blockIdx.x; + int id = threadIdx.x + blockIdx.x * blockDim.x; + int totalSize = rows * cols; + int offset = 0; + int segmentEnd = 0; + curandState localState; + constexpr int kSharedNoiseRngs = 1; + int rngsPerPixel = PerChannel ? channels : kSharedNoiseRngs; + if (!gaussian_noise_segment_state(state, id, totalSize, rngsPerPixel, offset, segmentEnd, localState)) + { + return; + } + + while (offset + blockDim.x < segmentEnd) + { + int x0 = offset % cols; + int y0 = offset / cols; + int offset1 = offset + blockDim.x; + int x1 = offset1 % cols; + int y1 = offset1 / cols; + if constexpr (PerChannel) + { + gaussian_noise_scalar_store_channel_pair(src, dst, localState, batchIdx, y0, x0, y1, x1, + channels, mu, sigma); + } + else + { + float2 rand = gaussian_noise_normal2(localState); + float delta0 = mu + rand.x * sigma; + float delta1 = mu + rand.y * sigma; + for (int ch = 0; ch < channels; ++ch) + { + gaussian_noise_scalar_store_channel(src, dst, batchIdx, y0, x0, ch, delta0); + gaussian_noise_scalar_store_channel(src, dst, batchIdx, y1, x1, ch, delta1); + } + } + offset += 2 * blockDim.x; + } + + while (offset < segmentEnd) + { + int x = offset % cols; + int y = offset / cols; + if constexpr (PerChannel) + { + for (int ch = 0; ch < channels; ++ch) + { + float rand = curand_normal(&localState); + gaussian_noise_scalar_store_channel(src, dst, batchIdx, y, x, ch, mu + rand * sigma); + } + } + else + { + float rand = curand_normal(&localState); + float delta = mu + rand * sigma; + for (int ch = 0; ch < channels; ++ch) + { + gaussian_noise_scalar_store_channel(src, dst, batchIdx, y, x, ch, delta); + } + } + offset += blockDim.x; + } + gaussian_noise_store_segment_state(state, nextState, id, totalSize, segmentEnd, localState); +} + +template +void launch_gaussian_noise_scalar(const nvcv::TensorDataStridedCuda &inData, const nvcv::TensorDataStridedCuda &outData, + int batch, int channels, int rows, int cols, curandState *states, + curandState *nextStates, float mu, float sigma, cudaStream_t stream) +{ + int totalSize = rows * cols; + int segments = gaussian_noise_segments(totalSize, BLOCK); + gaussian_noise_copy_segment_states(nextStates, states, batch, BLOCK, segments, stream); + + if constexpr (Planar) + { + auto src = CreateTensorWrapNCHW(inData); + auto dst = CreateTensorWrapNCHW(outData); + gaussian_noise_scalar_kernel<<>>( + src, dst, states, nextStates, mu, sigma, rows, cols, channels); + } + else + { + auto src = CreateTensorWrapNHWC(inData); + auto dst = CreateTensorWrapNHWC(outData); + gaussian_noise_scalar_kernel<<>>( + src, dst, states, nextStates, mu, sigma, rows, cols, channels); + } + + checkKernelErrors(); + gaussian_noise_copy_segment_states(states, nextStates, batch, BLOCK, segments, stream); +} + +template +void dispatch_gaussian_noise_scalar(const nvcv::TensorDataStridedCuda &inData, + const nvcv::TensorDataStridedCuda &outData, int batch, int channels, int rows, + int cols, curandState *states, curandState *nextStates, float mu, float sigma, + bool planar, bool perChannel, bool clip, cudaStream_t stream) +{ +#define CVCUDA_GAUSSIAN_NOISE_SCALAR_LAUNCH(PLANAR, PER_CHANNEL, CLIP) \ + launch_gaussian_noise_scalar(inData, outData, batch, channels, rows, cols, states, \ + nextStates, mu, sigma, stream) + + if (planar) + { + if (perChannel) + { + clip ? CVCUDA_GAUSSIAN_NOISE_SCALAR_LAUNCH(true, true, true) + : CVCUDA_GAUSSIAN_NOISE_SCALAR_LAUNCH(true, true, false); + } + else + { + clip ? CVCUDA_GAUSSIAN_NOISE_SCALAR_LAUNCH(true, false, true) + : CVCUDA_GAUSSIAN_NOISE_SCALAR_LAUNCH(true, false, false); + } + } + else if (perChannel) + { + clip ? CVCUDA_GAUSSIAN_NOISE_SCALAR_LAUNCH(false, true, true) + : CVCUDA_GAUSSIAN_NOISE_SCALAR_LAUNCH(false, true, false); + } + else + { + clip ? CVCUDA_GAUSSIAN_NOISE_SCALAR_LAUNCH(false, false, true) + : CVCUDA_GAUSSIAN_NOISE_SCALAR_LAUNCH(false, false, false); + } + +#undef CVCUDA_GAUSSIAN_NOISE_SCALAR_LAUNCH } template void gaussian_noise(const nvcv::TensorDataStridedCuda &d_in, const nvcv::TensorDataStridedCuda &d_out, int batch, - int rows, int cols, curandState *m_states, const nvcv::TensorDataStridedCuda &_mu, - const nvcv::TensorDataStridedCuda &_sigma, cudaStream_t stream) + int rows, int cols, curandState *m_states, curandState *m_nextStates, + const nvcv::TensorDataStridedCuda &_mu, const nvcv::TensorDataStridedCuda &_sigma, + cudaStream_t stream) { auto src_ptr = CreateTensorWrapNHW(d_in); auto dst_ptr = CreateTensorWrapNHW(d_out); Tensor1DWrap mu(_mu); Tensor1DWrap sigma(_sigma); - gaussian_noise_kernel<<>>(src_ptr, dst_ptr, m_states, mu, sigma, rows, cols); + int total_size = rows * cols; + int segments = gaussian_noise_segments(total_size, BLOCK); + + gaussian_noise_copy_segment_states(m_nextStates, m_states, batch, BLOCK, segments, stream); + gaussian_noise_kernel + <<>>(src_ptr, dst_ptr, m_states, m_nextStates, mu, sigma, rows, cols); checkKernelErrors(); + gaussian_noise_copy_segment_states(m_states, m_nextStates, batch, BLOCK, segments, stream); } template void gaussian_noise_per_channel(const nvcv::TensorDataStridedCuda &d_in, const nvcv::TensorDataStridedCuda &d_out, int batch, int channels, int rows, int cols, curandState *m_states, - const nvcv::TensorDataStridedCuda &_mu, const nvcv::TensorDataStridedCuda &_sigma, - cudaStream_t stream) + curandState *m_nextStates, const nvcv::TensorDataStridedCuda &_mu, + const nvcv::TensorDataStridedCuda &_sigma, cudaStream_t stream) { auto src_ptr = CreateTensorWrapNHWC(d_in); auto dst_ptr = CreateTensorWrapNHWC(d_out); Tensor1DWrap mu(_mu); Tensor1DWrap sigma(_sigma); - gaussian_noise_per_channel_kernel - <<>>(src_ptr, dst_ptr, m_states, mu, sigma, rows, cols, channels); + int total_size = rows * cols; + int segments = gaussian_noise_segments(total_size, BLOCK); + + gaussian_noise_copy_segment_states(m_nextStates, m_states, batch, BLOCK, segments, stream); + gaussian_noise_per_channel_kernel<<>>( + src_ptr, dst_ptr, m_states, m_nextStates, mu, sigma, rows, cols, channels); checkKernelErrors(); + gaussian_noise_copy_segment_states(m_states, m_nextStates, batch, BLOCK, segments, stream); } template void gaussian_noise_float(const nvcv::TensorDataStridedCuda &d_in, const nvcv::TensorDataStridedCuda &d_out, int batch, - int rows, int cols, curandState *m_states, const nvcv::TensorDataStridedCuda &_mu, - const nvcv::TensorDataStridedCuda &_sigma, cudaStream_t stream) + int rows, int cols, curandState *m_states, curandState *m_nextStates, + const nvcv::TensorDataStridedCuda &_mu, const nvcv::TensorDataStridedCuda &_sigma, + cudaStream_t stream) { auto src_ptr = CreateTensorWrapNHW(d_in); auto dst_ptr = CreateTensorWrapNHW(d_out); Tensor1DWrap mu(_mu); Tensor1DWrap sigma(_sigma); - gaussian_noise_float_kernel<<>>(src_ptr, dst_ptr, m_states, mu, sigma, rows, cols); + int total_size = rows * cols; + int segments = gaussian_noise_segments(total_size, BLOCK); + + gaussian_noise_copy_segment_states(m_nextStates, m_states, batch, BLOCK, segments, stream); + gaussian_noise_float_kernel + <<>>(src_ptr, dst_ptr, m_states, m_nextStates, mu, sigma, rows, cols); checkKernelErrors(); + gaussian_noise_copy_segment_states(m_states, m_nextStates, batch, BLOCK, segments, stream); } template void gaussian_noise_float_per_channel(const nvcv::TensorDataStridedCuda &d_in, const nvcv::TensorDataStridedCuda &d_out, int batch, int channels, int rows, int cols, curandState *m_states, - const nvcv::TensorDataStridedCuda &_mu, const nvcv::TensorDataStridedCuda &_sigma, - cudaStream_t stream) + curandState *m_nextStates, const nvcv::TensorDataStridedCuda &_mu, + const nvcv::TensorDataStridedCuda &_sigma, cudaStream_t stream) { auto src_ptr = CreateTensorWrapNHWC(d_in); auto dst_ptr = CreateTensorWrapNHWC(d_out); Tensor1DWrap mu(_mu); Tensor1DWrap sigma(_sigma); - gaussian_noise_float_per_channel_kernel - <<>>(src_ptr, dst_ptr, m_states, mu, sigma, rows, cols, channels); + int total_size = rows * cols; + int segments = gaussian_noise_segments(total_size, BLOCK); + + gaussian_noise_copy_segment_states(m_nextStates, m_states, batch, BLOCK, segments, stream); + gaussian_noise_float_per_channel_kernel<<>>( + src_ptr, dst_ptr, m_states, m_nextStates, mu, sigma, rows, cols, channels); + checkKernelErrors(); + gaussian_noise_copy_segment_states(m_states, m_nextStates, batch, BLOCK, segments, stream); +} + +template +void gaussian_noise_planar(const nvcv::TensorDataStridedCuda &d_in, const nvcv::TensorDataStridedCuda &d_out, int batch, + int channels, int rows, int cols, curandState *m_states, curandState *m_nextStates, + const nvcv::TensorDataStridedCuda &_mu, const nvcv::TensorDataStridedCuda &_sigma, + cudaStream_t stream) +{ + auto src_ptr = CreateTensorWrapNCHW(d_in); + auto dst_ptr = CreateTensorWrapNCHW(d_out); + Tensor1DWrap mu(_mu); + Tensor1DWrap sigma(_sigma); + + int total_size = rows * cols; + int segments = gaussian_noise_segments(total_size, BLOCK); + + gaussian_noise_copy_segment_states(m_nextStates, m_states, batch, BLOCK, segments, stream); + gaussian_noise_planar_kernel<<>>( + src_ptr, dst_ptr, m_states, m_nextStates, mu, sigma, rows, cols, channels); + checkKernelErrors(); + gaussian_noise_copy_segment_states(m_states, m_nextStates, batch, BLOCK, segments, stream); +} + +template +void gaussian_noise_planar_per_channel(const nvcv::TensorDataStridedCuda &d_in, + const nvcv::TensorDataStridedCuda &d_out, int batch, int channels, int rows, + int cols, curandState *m_states, curandState *m_nextStates, + const nvcv::TensorDataStridedCuda &_mu, + const nvcv::TensorDataStridedCuda &_sigma, cudaStream_t stream) +{ + auto src_ptr = CreateTensorWrapNCHW(d_in); + auto dst_ptr = CreateTensorWrapNCHW(d_out); + Tensor1DWrap mu(_mu); + Tensor1DWrap sigma(_sigma); + + int total_size = rows * cols; + int segments = gaussian_noise_segments(total_size, BLOCK); + + gaussian_noise_copy_segment_states(m_nextStates, m_states, batch, BLOCK, segments, stream); + gaussian_noise_planar_per_channel_kernel<<>>( + src_ptr, dst_ptr, m_states, m_nextStates, mu, sigma, rows, cols, channels); + checkKernelErrors(); + gaussian_noise_copy_segment_states(m_states, m_nextStates, batch, BLOCK, segments, stream); +} + +template +void gaussian_noise_float_planar(const nvcv::TensorDataStridedCuda &d_in, const nvcv::TensorDataStridedCuda &d_out, + int batch, int channels, int rows, int cols, curandState *m_states, + curandState *m_nextStates, const nvcv::TensorDataStridedCuda &_mu, + const nvcv::TensorDataStridedCuda &_sigma, cudaStream_t stream) +{ + auto src_ptr = CreateTensorWrapNCHW(d_in); + auto dst_ptr = CreateTensorWrapNCHW(d_out); + Tensor1DWrap mu(_mu); + Tensor1DWrap sigma(_sigma); + + int total_size = rows * cols; + int segments = gaussian_noise_segments(total_size, BLOCK); + + gaussian_noise_copy_segment_states(m_nextStates, m_states, batch, BLOCK, segments, stream); + gaussian_noise_float_planar_kernel<<>>( + src_ptr, dst_ptr, m_states, m_nextStates, mu, sigma, rows, cols, channels); + checkKernelErrors(); + gaussian_noise_copy_segment_states(m_states, m_nextStates, batch, BLOCK, segments, stream); +} + +template +void gaussian_noise_float_planar_per_channel(const nvcv::TensorDataStridedCuda &d_in, + const nvcv::TensorDataStridedCuda &d_out, int batch, int channels, + int rows, int cols, curandState *m_states, curandState *m_nextStates, + const nvcv::TensorDataStridedCuda &_mu, + const nvcv::TensorDataStridedCuda &_sigma, cudaStream_t stream) +{ + auto src_ptr = CreateTensorWrapNCHW(d_in); + auto dst_ptr = CreateTensorWrapNCHW(d_out); + Tensor1DWrap mu(_mu); + Tensor1DWrap sigma(_sigma); + + int total_size = rows * cols; + int segments = gaussian_noise_segments(total_size, BLOCK); + + gaussian_noise_copy_segment_states(m_nextStates, m_states, batch, BLOCK, segments, stream); + gaussian_noise_float_planar_per_channel_kernel<<>>( + src_ptr, dst_ptr, m_states, m_nextStates, mu, sigma, rows, cols, channels); checkKernelErrors(); + gaussian_noise_copy_segment_states(m_states, m_nextStates, batch, BLOCK, segments, stream); } namespace nvcv::legacy::cuda_op { @@ -197,6 +802,7 @@ namespace nvcv::legacy::cuda_op { GaussianNoise::GaussianNoise(DataShape max_input_shape, DataShape max_output_shape, int maxBatchSize) : CudaBaseOp(max_input_shape, max_output_shape) , m_states(nullptr) + , m_nextStates(nullptr) , m_seed(0) , m_maxBatchSize(maxBatchSize) , m_setupDone(false) @@ -206,12 +812,19 @@ GaussianNoise::GaussianNoise(DataShape max_input_shape, DataShape max_output_sha LOG_ERROR("Invalid num of max batch size " << maxBatchSize); throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Parameter error!"); } - cudaError_t err = cudaMalloc((void **)&m_states, sizeof(curandState) * BLOCK * maxBatchSize); + const size_t stateBytes + = cvcuda::priv::CheckedMulMany({sizeof(curandState), static_cast(BLOCK), + cvcuda::priv::CheckedNonNegativeToSize(maxBatchSize, "maxBatchSize")}, + "GaussianNoise curand state allocation size overflow"); + const size_t allocationBytes + = cvcuda::priv::CheckedMulMany({stateBytes, 2}, "GaussianNoise curand state allocation size overflow"); + cudaError_t err = cudaMalloc((void **)&m_states, allocationBytes); if (err != cudaSuccess) { - LOG_ERROR("CUDA memory allocation error of size: " << sizeof(curandState) * BLOCK * maxBatchSize); - throw std::runtime_error("CUDA memory allocation error!"); + LOG_ERROR("CUDA memory allocation error of size: " << allocationBytes); + throw LegacyCudaAllocationError("CUDA memory allocation error!"); } + m_nextStates = maxBatchSize > 0 ? m_states + static_cast(BLOCK) * maxBatchSize : m_states; } GaussianNoise::~GaussianNoise() @@ -227,27 +840,43 @@ ErrorCode GaussianNoise::infer(const TensorDataStridedCuda &inData, const Tensor { DataFormat in_format = GetLegacyDataFormat(inData.layout()); DataFormat out_format = GetLegacyDataFormat(outData.layout()); - if (!(in_format == kNHWC || in_format == kHWC)) + if (!(in_format == kNHWC || in_format == kHWC || in_format == kNCHW || in_format == kCHW)) + { + LOG_ERROR("Invalid input DataFormat " << in_format + << ", the valid DataFormats are: \"NHWC\", \"HWC\", \"NCHW\", \"CHW\""); + return ErrorCode::INVALID_DATA_FORMAT; + } + if (!(out_format == kNHWC || out_format == kHWC || out_format == kNCHW || out_format == kCHW)) { - LOG_ERROR("Invalid input DataFormat " << in_format << ", the valid DataFormats are: \"NHWC\", \"HWC\""); + LOG_ERROR("Invalid output DataFormat " << out_format + << ", the valid DataFormats are: \"NHWC\", \"HWC\", \"NCHW\", \"CHW\""); return ErrorCode::INVALID_DATA_FORMAT; } - if (!(out_format == kNHWC || out_format == kHWC)) + const bool isPlanar = in_format == kNCHW || in_format == kCHW; + const bool isOutPlanar = out_format == kNCHW || out_format == kCHW; + if (isPlanar != isOutPlanar) { - LOG_ERROR("Invalid output DataFormat " << out_format << ", the valid DataFormats are: \"NHWC\", \"HWC\""); + LOG_ERROR("Input and output must both be interleaved or both be planar"); return ErrorCode::INVALID_DATA_FORMAT; } auto inAccess = TensorDataAccessStridedImagePlanar::Create(inData); NVCV_ASSERT(inAccess); int channels = inAccess->numChannels(); - if (channels > 4) + if (channels > 4 || (isPlanar && channels == 2)) { LOG_ERROR("Invalid channel number " << channels); return ErrorCode::INVALID_DATA_SHAPE; } - auto inMaxStride = inAccess->sampleStride() * inAccess->numSamples(); + const int batch = inAccess->numSamples(); + if (batch > m_maxBatchSize) + { + LOG_ERROR("Input batch exceeds maxBatchSize"); + return ErrorCode::INVALID_PARAMETER; + } + + auto inMaxStride = inAccess->sampleStride() * batch; if (inMaxStride > cuda::TypeTraits::max) { LOG_ERROR("Input size exceeds " << nvcv::cuda::TypeTraits::max << ". Tensor is too large."); @@ -256,6 +885,25 @@ ErrorCode GaussianNoise::infer(const TensorDataStridedCuda &inData, const Tensor auto outAccess = TensorDataAccessStridedImagePlanar::Create(outData); NVCV_ASSERT(outAccess); + if (isPlanar + && (outAccess->numSamples() != batch || outAccess->numChannels() != channels + || outAccess->numRows() != inAccess->numRows() || outAccess->numCols() != inAccess->numCols())) + { + LOG_ERROR("Planar input and output must have matching sample, channel, height, and width"); + return ErrorCode::INVALID_DATA_SHAPE; + } + if (isPlanar && batch > 1 + && (inAccess->sampleStride() != channels * inAccess->chStride() + || outAccess->sampleStride() != channels * outAccess->chStride())) + { + LOG_ERROR("Planar GaussianNoise of a batched tensor requires tightly packed channel planes"); + return ErrorCode::INVALID_PARAMETER; + } + if (isPlanar && static_cast(batch) * channels > 65535) + { + LOG_ERROR("Planar GaussianNoise requires numSamples * channels <= 65535 (CUDA grid-z limit)"); + return ErrorCode::INVALID_PARAMETER; + } auto outMaxStride = outAccess->sampleStride() * outAccess->numSamples(); if (outMaxStride > cuda::TypeTraits::max) @@ -315,8 +963,9 @@ ErrorCode GaussianNoise::infer(const TensorDataStridedCuda &inData, const Tensor if (per_channel) { typedef void (*func_t)(const TensorDataStridedCuda &d_in, const TensorDataStridedCuda &d_out, int batch, - int channels, int rows, int cols, curandState *m_states, const TensorDataStridedCuda &mu, - const TensorDataStridedCuda &sigma, cudaStream_t stream); + int channels, int rows, int cols, curandState *m_states, curandState *m_nextStates, + const TensorDataStridedCuda &mu, const TensorDataStridedCuda &sigma, + cudaStream_t stream); static const func_t funcs[5] = { gaussian_noise_per_channel, 0, gaussian_noise_per_channel, gaussian_noise_per_channel, @@ -326,27 +975,40 @@ ErrorCode GaussianNoise::infer(const TensorDataStridedCuda &inData, const Tensor static const func_t float_funcs[1] = { gaussian_noise_float_per_channel, }; + static const func_t planar_funcs[5] = { + gaussian_noise_planar_per_channel, 0, + gaussian_noise_planar_per_channel, gaussian_noise_planar_per_channel, + gaussian_noise_planar_per_channel, + }; + static const func_t planar_float_funcs[1] = { + gaussian_noise_float_planar_per_channel, + }; if (in_data_type == kCV_32F) { - const func_t func = float_funcs[0]; + const func_t func = isPlanar ? planar_float_funcs[0] : float_funcs[0]; assert(func != 0); - func(inData, outData, inAccess->numSamples(), channels, inAccess->numRows(), inAccess->numCols(), m_states, - mu, sigma, stream); + func(inData, outData, batch, channels, inAccess->numRows(), inAccess->numCols(), m_states, m_nextStates, mu, + sigma, stream); } else { - const func_t func = funcs[in_data_type]; + const func_t func = isPlanar ? planar_funcs[in_data_type] : funcs[in_data_type]; assert(func != 0); - func(inData, outData, inAccess->numSamples(), channels, inAccess->numRows(), inAccess->numCols(), m_states, - mu, sigma, stream); + func(inData, outData, batch, channels, inAccess->numRows(), inAccess->numCols(), m_states, m_nextStates, mu, + sigma, stream); } } else { typedef void (*func_t)(const TensorDataStridedCuda &d_in, const TensorDataStridedCuda &d_out, int batch, - int rows, int cols, curandState *m_states, const TensorDataStridedCuda &mu, - const TensorDataStridedCuda &sigma, cudaStream_t stream); + int rows, int cols, curandState *m_states, curandState *m_nextStates, + const TensorDataStridedCuda &mu, const TensorDataStridedCuda &sigma, + cudaStream_t stream); + typedef void (*planar_func_t)(const TensorDataStridedCuda &d_in, const TensorDataStridedCuda &d_out, int batch, + int channels, int rows, int cols, curandState *m_states, + curandState *m_nextStates, const TensorDataStridedCuda &mu, + const TensorDataStridedCuda &sigma, cudaStream_t stream); static const func_t funcs[5][4] = { { gaussian_noise, gaussian_noise, gaussian_noise,gaussian_noise }, @@ -357,25 +1019,147 @@ ErrorCode GaussianNoise::infer(const TensorDataStridedCuda &inData, const Tensor { gaussian_noise, gaussian_noise, gaussian_noise, gaussian_noise}, }; - static const func_t float_funcs[4] = {gaussian_noise_float, gaussian_noise_float, - gaussian_noise_float, gaussian_noise_float}; + static const func_t float_funcs[4] = {gaussian_noise_float, gaussian_noise_float, + gaussian_noise_float, gaussian_noise_float}; + static const planar_func_t planar_funcs[5] = { + gaussian_noise_planar, 0, gaussian_noise_planar, gaussian_noise_planar, + gaussian_noise_planar, + }; + static const planar_func_t planar_float_funcs[1] = { + gaussian_noise_float_planar, + }; if (in_data_type == kCV_32F) { - const func_t func = float_funcs[channels - 1]; - assert(func != 0); - func(inData, outData, inAccess->numSamples(), inAccess->numRows(), inAccess->numCols(), m_states, mu, sigma, - stream); + if (isPlanar) + { + const planar_func_t func = planar_float_funcs[0]; + assert(func != 0); + func(inData, outData, batch, channels, inAccess->numRows(), inAccess->numCols(), m_states, m_nextStates, + mu, sigma, stream); + } + else + { + const func_t func = float_funcs[channels - 1]; + assert(func != 0); + func(inData, outData, batch, inAccess->numRows(), inAccess->numCols(), m_states, m_nextStates, mu, + sigma, stream); + } } else { - const func_t func = funcs[in_data_type][channels - 1]; - assert(func != 0); - func(inData, outData, inAccess->numSamples(), inAccess->numRows(), inAccess->numCols(), m_states, mu, sigma, - stream); + if (isPlanar) + { + const planar_func_t func = planar_funcs[in_data_type]; + assert(func != 0); + func(inData, outData, batch, channels, inAccess->numRows(), inAccess->numCols(), m_states, m_nextStates, + mu, sigma, stream); + } + else + { + const func_t func = funcs[in_data_type][channels - 1]; + assert(func != 0); + func(inData, outData, batch, inAccess->numRows(), inAccess->numCols(), m_states, m_nextStates, mu, + sigma, stream); + } } } return SUCCESS; } +ErrorCode GaussianNoise::infer(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, float mu, + float sigma, bool per_channel, unsigned long long seed, bool reseed, bool clip, + cudaStream_t stream) +{ + DataFormat inFormat = GetLegacyDataFormat(inData.layout()); + DataFormat outFormat = GetLegacyDataFormat(outData.layout()); + if (!(inFormat == kNHWC || inFormat == kHWC || inFormat == kNCHW || inFormat == kCHW) || inFormat != outFormat) + { + LOG_ERROR("Input and output must have the same NHWC, HWC, NCHW, or CHW layout"); + return ErrorCode::INVALID_DATA_FORMAT; + } + + auto inAccess = TensorDataAccessStridedImagePlanar::Create(inData); + auto outAccess = TensorDataAccessStridedImagePlanar::Create(outData); + if (!inAccess || !outAccess) + { + LOG_ERROR("Input and output must be image tensors"); + return ErrorCode::INVALID_DATA_FORMAT; + } + + const bool isPlanar = inFormat == kNCHW || inFormat == kCHW; + const int channels = inAccess->numChannels(); + const int batch = inAccess->numSamples(); + if (channels < 1 || channels > 4 || (isPlanar && channels == 2)) + { + LOG_ERROR("Invalid channel number " << channels); + return ErrorCode::INVALID_DATA_SHAPE; + } + if (batch > m_maxBatchSize) + { + LOG_ERROR("Input batch exceeds maxBatchSize"); + return ErrorCode::INVALID_PARAMETER; + } + if (outAccess->numSamples() != batch || outAccess->numChannels() != channels + || outAccess->numRows() != inAccess->numRows() || outAccess->numCols() != inAccess->numCols()) + { + LOG_ERROR("Input and output must have identical shape"); + return ErrorCode::INVALID_DATA_SHAPE; + } + if (isPlanar && batch > 1 + && (inAccess->sampleStride() != channels * inAccess->chStride() + || outAccess->sampleStride() != channels * outAccess->chStride())) + { + LOG_ERROR("Planar GaussianNoise of a batched tensor requires tightly packed channel planes"); + return ErrorCode::INVALID_PARAMETER; + } + if (isPlanar && static_cast(batch) * channels > 65535) + { + LOG_ERROR("Planar GaussianNoise requires numSamples * channels <= 65535"); + return ErrorCode::INVALID_PARAMETER; + } + + auto inMaxStride = inAccess->sampleStride() * batch; + auto outMaxStride = outAccess->sampleStride() * batch; + if (inMaxStride > cuda::TypeTraits::max || outMaxStride > cuda::TypeTraits::max) + { + LOG_ERROR("Input or output tensor is too large for 32-bit indexing"); + return ErrorCode::INVALID_PARAMETER; + } + + DataType inType = GetLegacyDataType(inData.dtype()); + DataType outType = GetLegacyDataType(outData.dtype()); + if (!(inType == kCV_8U || inType == kCV_32F) || inType != outType) + { + LOG_ERROR("Scalar GaussianNoise supports matching U8 or F32 input and output"); + return ErrorCode::INVALID_DATA_TYPE; + } + if (sigma < 0.f) + { + LOG_ERROR("sigma must be non-negative"); + return ErrorCode::INVALID_PARAMETER; + } + + if (reseed || !m_setupDone) + { + m_seed = seed; + setup_gaussian_rand_kernel<<>>(m_states, m_seed); + m_setupDone = true; + } + + if (inType == kCV_32F) + { + dispatch_gaussian_noise_scalar(inData, outData, batch, channels, inAccess->numRows(), + inAccess->numCols(), m_states, m_nextStates, mu, sigma, isPlanar, + per_channel, clip, stream); + } + else + { + dispatch_gaussian_noise_scalar(inData, outData, batch, channels, inAccess->numRows(), + inAccess->numCols(), m_states, m_nextStates, mu, sigma, isPlanar, + per_channel, clip, stream); + } + return SUCCESS; +} + } // namespace nvcv::legacy::cuda_op diff --git a/src/cvcuda/priv/legacy/gaussian_noise_util.cuh b/src/cvcuda/priv/legacy/gaussian_noise_util.cuh index d543723b9..0f46fb91c 100644 --- a/src/cvcuda/priv/legacy/gaussian_noise_util.cuh +++ b/src/cvcuda/priv/legacy/gaussian_noise_util.cuh @@ -18,8 +18,8 @@ * limitations under the License. */ -#ifndef GAUSSIAN_NOISE_UTILS_CUH -#define GAUSSIAN_NOISE_UTILS_CUH +#ifndef GAUSSIAN_NOISE_UTIL_CUH +#define GAUSSIAN_NOISE_UTIL_CUH #include "CvCudaUtils.cuh" @@ -27,4 +27,192 @@ __global__ void setup_gaussian_rand_kernel(curandState *state, unsigned long long seed); -#endif // GAUSSIAN_NOISE_UTILS_CUH +// Tuned pixels-per-RNG-segment: balances per-thread RNG state/register pressure +// with enough segmented work to keep target GPUs occupied. Retune if the RNG +// state layout or the per-pixel RNG consumption changes. +constexpr int kGaussianNoiseSegmentPixels = 128; + +inline int gaussian_noise_segments(int totalSize, int blockSize) +{ + int segments + = (totalSize + blockSize * kGaussianNoiseSegmentPixels - 1) / (blockSize * kGaussianNoiseSegmentPixels); + return segments > 0 ? segments : 1; +} + +__device__ __forceinline__ bool gaussian_noise_segment_range(int totalSize, int &segmentStart, int &offset, + int &segmentEnd) +{ + segmentStart = blockIdx.y * kGaussianNoiseSegmentPixels; + offset = threadIdx.x + segmentStart * blockDim.x; + if (offset >= totalSize) + { + return false; + } + + segmentEnd = threadIdx.x + (segmentStart + kGaussianNoiseSegmentPixels) * blockDim.x; + segmentEnd = segmentEnd < totalSize ? segmentEnd : totalSize; + return true; +} + +__device__ __forceinline__ void gaussian_noise_advance_normal_state(unsigned long long normalCount, + curandState &localState) +{ + if (normalCount == 0) + { + return; + } + + // curand's Box-Muller path caches one normal sample; consume it before + // skipahead so localState.boxmuller_flag / EXTRA_FLAG_NORMAL advancement + // stays deterministic. Re-check this if curand_normal internals change. + if (localState.boxmuller_flag == EXTRA_FLAG_NORMAL) + { + (void)curand_normal(&localState); + if (--normalCount == 0) + { + return; + } + } + + if ((normalCount & 1) == 0) + { + skipahead(normalCount, &localState); + } + else + { + if (normalCount > 1) + { + skipahead(normalCount - 1, &localState); + } + (void)curand_normal(&localState); + } +} + +__device__ __forceinline__ unsigned long long gaussian_noise_thread_pixels(int totalSize) +{ + return threadIdx.x < totalSize ? 1ull + static_cast(totalSize - 1 - threadIdx.x) / blockDim.x + : 0ull; +} + +__device__ __forceinline__ bool gaussian_noise_segment_state(curandState *state, int id, int totalSize, + int rngsPerPixel, int &offset, int &segmentEnd, + curandState &localState) +{ + int segmentStart; + if (!gaussian_noise_segment_range(totalSize, segmentStart, offset, segmentEnd)) + { + return false; + } + + // Segmented launches share state[id] across gridDim.y, so each segment + // advances a localState by its deterministic rngsPerPixel offset. + localState = state[id]; + if (segmentStart > 0) + { + gaussian_noise_advance_normal_state(static_cast(segmentStart) * rngsPerPixel, localState); + } + return true; +} + +__device__ __forceinline__ void gaussian_noise_store_segment_state(curandState *state, curandState *nextState, int id, + int totalSize, int segmentEnd, + curandState localState) +{ + if (gridDim.y == 1) + { + state[id] = localState; + } + else if (segmentEnd == totalSize) + { + // Exactly one segment per RNG thread reaches the end of its range. + // Publish to a separate buffer so all segments keep reading the same + // immutable starting state. + nextState[id] = localState; + } +} + +inline void gaussian_noise_copy_segment_states(curandState *dst, const curandState *src, int batch, int blockSize, + int segments, cudaStream_t stream) +{ + if (segments <= 1) + { + return; + } + + const size_t stateBytes = static_cast(batch) * blockSize * sizeof(curandState); + nvcv::legacy::cuda_op::__checkCudaErrors(cudaMemcpyAsync(dst, src, stateBytes, cudaMemcpyDeviceToDevice, stream), + __FILE__, __LINE__); +} + +__device__ __forceinline__ float2 gaussian_noise_normal2(curandState &localState) +{ + if (localState.boxmuller_flag == EXTRA_FLAG_NORMAL) + { + return {curand_normal(&localState), curand_normal(&localState)}; + } + return curand_normal2(&localState); +} + +template +__device__ __forceinline__ T gaussian_noise_saturate(T value, float delta) +{ + return nvcv::cuda::SaturateCast(value + delta); +} + +template<> +__device__ __forceinline__ float gaussian_noise_saturate(float value, float delta) +{ + return nvcv::cuda::clamp(value + delta, 0.f, 1.f); +} + +template +__device__ __forceinline__ void gaussian_noise_store_channel(const SrcWrap &src, const DstWrap &dst, int batchIdx, + int y, int x, int ch, float delta) +{ + if constexpr (Planar) + { + *dst.ptr(batchIdx, ch, y, x) = gaussian_noise_saturate(*src.ptr(batchIdx, ch, y, x), delta); + } + else + { + *dst.ptr(batchIdx, y, x, ch) = gaussian_noise_saturate(*src.ptr(batchIdx, y, x, ch), delta); + } +} + +template +__device__ __forceinline__ void gaussian_noise_store_channel_pair(const SrcWrap &src, const DstWrap &dst, + curandState &localState, int batchIdx, int y0, int x0, + int y1, int x1, int channels, float mu, float sigma) +{ + int ch = 0; + for (; ch + 1 < channels; ch += 2) + { + float2 rand = gaussian_noise_normal2(localState); + gaussian_noise_store_channel(src, dst, batchIdx, y0, x0, ch, mu + rand.x * sigma); + gaussian_noise_store_channel(src, dst, batchIdx, y0, x0, ch + 1, mu + rand.y * sigma); + } + if (ch < channels) + { + float2 rand = gaussian_noise_normal2(localState); + gaussian_noise_store_channel(src, dst, batchIdx, y0, x0, ch, mu + rand.x * sigma); + gaussian_noise_store_channel(src, dst, batchIdx, y1, x1, 0, mu + rand.y * sigma); + ch = 1; + } + else + { + ch = 0; + } + for (; ch + 1 < channels; ch += 2) + { + float2 rand = gaussian_noise_normal2(localState); + gaussian_noise_store_channel(src, dst, batchIdx, y1, x1, ch, mu + rand.x * sigma); + gaussian_noise_store_channel(src, dst, batchIdx, y1, x1, ch + 1, mu + rand.y * sigma); + } + if (ch < channels) + { + float rand = curand_normal(&localState); + gaussian_noise_store_channel(src, dst, batchIdx, y1, x1, ch, mu + rand * sigma); + } +} + +#endif // GAUSSIAN_NOISE_UTIL_CUH diff --git a/src/cvcuda/priv/legacy/gaussian_noise_var_shape.cu b/src/cvcuda/priv/legacy/gaussian_noise_var_shape.cu index 7ae517ad4..72cd44f34 100644 --- a/src/cvcuda/priv/legacy/gaussian_noise_var_shape.cu +++ b/src/cvcuda/priv/legacy/gaussian_noise_var_shape.cu @@ -1,4 +1,4 @@ -/* Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +/* Copyright (c) 2021-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * * SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES * SPDX-License-Identifier: Apache-2.0 @@ -18,6 +18,7 @@ * limitations under the License. */ +#include "../SafeSize.hpp" #include "CvCudaLegacy.h" #include "CvCudaLegacyHelpers.hpp" @@ -33,164 +34,561 @@ using namespace nvcv::cuda; #define BLOCK 512 +static int gaussian_noise_var_shape_segments(const nvcv::ImageBatchVarShapeDataStridedCuda &d_out) +{ + nvcv::Size2D maxSize = d_out.maxSize(); + return gaussian_noise_segments(maxSize.w * maxSize.h, BLOCK); +} + template __global__ void gaussian_noise_kernel(const ImageBatchVarShapeWrap src, ImageBatchVarShapeWrap dst, - curandState *state, Tensor1DWrap mu, Tensor1DWrap sigma) + curandState *state, curandState *nextState, Tensor1DWrap mu, + Tensor1DWrap sigma) { - int offset = threadIdx.x; int batch_idx = blockIdx.x; int id = threadIdx.x + blockIdx.x * blockDim.x; - int total_size = dst.height(batch_idx) * dst.width(batch_idx); - curandState localState = state[id]; - while (offset < total_size) + const int width = dst.width(batch_idx); + int total_size = dst.height(batch_idx) * width; + int offset = 0; + int segmentEnd = 0; + const float batchMu = mu[batch_idx]; + const float batchSigma = sigma[batch_idx]; + curandState localState; + if (!gaussian_noise_segment_state(state, id, total_size, 1, offset, segmentEnd, localState)) + { + return; + } + while (offset + blockDim.x < segmentEnd) { - int dst_x = offset % dst.width(batch_idx); - int dst_y = offset / dst.width(batch_idx); + int dst_x0 = offset % width; + int dst_y0 = offset / width; + int offset1 = offset + blockDim.x; + int dst_x1 = offset1 % width; + int dst_y1 = offset1 / width; + float2 rand = gaussian_noise_normal2(localState); + float delta0 = batchMu + rand.x * batchSigma; + float delta1 = batchMu + rand.y * batchSigma; + *dst.ptr(batch_idx, dst_y0, dst_x0) = SaturateCast(*src.ptr(batch_idx, dst_y0, dst_x0) + delta0); + *dst.ptr(batch_idx, dst_y1, dst_x1) = SaturateCast(*src.ptr(batch_idx, dst_y1, dst_x1) + delta1); + offset += 2 * blockDim.x; + } + while (offset < segmentEnd) + { + int dst_x = offset % width; + int dst_y = offset / width; float rand = curand_normal(&localState); - float delta = mu[batch_idx] + rand * sigma[batch_idx]; + float delta = batchMu + rand * batchSigma; *dst.ptr(batch_idx, dst_y, dst_x) = SaturateCast(*src.ptr(batch_idx, dst_y, dst_x) + delta); offset += blockDim.x; } - state[id] = localState; + gaussian_noise_store_segment_state(state, nextState, id, total_size, segmentEnd, localState); } template __global__ void gaussian_noise_per_channel_kernel(const ImageBatchVarShapeWrapNHWC src, ImageBatchVarShapeWrapNHWC dst, curandState *state, - Tensor1DWrap mu, Tensor1DWrap sigma) + curandState *nextState, Tensor1DWrap mu, + Tensor1DWrap sigma) { - int offset = threadIdx.x; int batch_idx = blockIdx.x; int id = threadIdx.x + blockIdx.x * blockDim.x; - int total_size = dst.height(batch_idx) * dst.width(batch_idx); + const int width = dst.width(batch_idx); + int total_size = dst.height(batch_idx) * width; + int offset = 0; + int segmentEnd = 0; int channel = src.numChannels(); - curandState localState = state[id]; - while (offset < total_size) + const float batchMu = mu[batch_idx]; + const float batchSigma = sigma[batch_idx]; + curandState localState; + if (!gaussian_noise_segment_state(state, id, total_size, channel, offset, segmentEnd, localState)) { - int dst_x = offset % dst.width(batch_idx); - int dst_y = offset / dst.width(batch_idx); + return; + } + while (offset + blockDim.x < segmentEnd) + { + int dst_x0 = offset % width; + int dst_y0 = offset / width; + int offset1 = offset + blockDim.x; + int dst_x1 = offset1 % width; + int dst_y1 = offset1 / width; + gaussian_noise_store_channel_pair(src, dst, localState, batch_idx, dst_y0, dst_x0, dst_y1, dst_x1, + channel, batchMu, batchSigma); + offset += 2 * blockDim.x; + } + while (offset < segmentEnd) + { + int dst_x = offset % width; + int dst_y = offset / width; for (int ch = 0; ch < channel; ch++) { float rand = curand_normal(&localState); - float delta = mu[batch_idx] + rand * sigma[batch_idx]; + float delta = batchMu + rand * batchSigma; *dst.ptr(batch_idx, dst_y, dst_x, ch) = SaturateCast(*src.ptr(batch_idx, dst_y, dst_x, ch) + delta); } offset += blockDim.x; } - state[id] = localState; + gaussian_noise_store_segment_state(state, nextState, id, total_size, segmentEnd, localState); } template __global__ void gaussian_noise_float_kernel(const ImageBatchVarShapeWrap src, ImageBatchVarShapeWrap dst, - curandState *state, Tensor1DWrap mu, Tensor1DWrap sigma) + curandState *state, curandState *nextState, Tensor1DWrap mu, + Tensor1DWrap sigma) { - int offset = threadIdx.x; int batch_idx = blockIdx.x; int id = threadIdx.x + blockIdx.x * blockDim.x; - int total_size = dst.height(batch_idx) * dst.width(batch_idx); - curandState localState = state[id]; - while (offset < total_size) + const int width = dst.width(batch_idx); + int total_size = dst.height(batch_idx) * width; + int offset = 0; + int segmentEnd = 0; + const float batchMu = mu[batch_idx]; + const float batchSigma = sigma[batch_idx]; + curandState localState; + if (!gaussian_noise_segment_state(state, id, total_size, 1, offset, segmentEnd, localState)) + { + return; + } + while (offset + blockDim.x < segmentEnd) + { + int dst_x0 = offset % width; + int dst_y0 = offset / width; + int offset1 = offset + blockDim.x; + int dst_x1 = offset1 % width; + int dst_y1 = offset1 / width; + float2 rand = gaussian_noise_normal2(localState); + float delta0 = batchMu + rand.x * batchSigma; + float delta1 = batchMu + rand.y * batchSigma; + T out0 = SaturateCast(*src.ptr(batch_idx, dst_y0, dst_x0) + delta0); + T out1 = SaturateCast(*src.ptr(batch_idx, dst_y1, dst_x1) + delta1); + *dst.ptr(batch_idx, dst_y0, dst_x0) = clamp(StaticCast(out0), 0.f, 1.f); + *dst.ptr(batch_idx, dst_y1, dst_x1) = clamp(StaticCast(out1), 0.f, 1.f); + offset += 2 * blockDim.x; + } + while (offset < segmentEnd) { - int dst_x = offset % dst.width(batch_idx); - int dst_y = offset / dst.width(batch_idx); + int dst_x = offset % width; + int dst_y = offset / width; float rand = curand_normal(&localState); - float delta = mu[batch_idx] + rand * sigma[batch_idx]; + float delta = batchMu + rand * batchSigma; T out = SaturateCast(*src.ptr(batch_idx, dst_y, dst_x) + delta); *dst.ptr(batch_idx, dst_y, dst_x) = clamp(StaticCast(out), 0.f, 1.f); offset += blockDim.x; } - state[id] = localState; + gaussian_noise_store_segment_state(state, nextState, id, total_size, segmentEnd, localState); } template __global__ void gaussian_noise_float_per_channel_kernel(const ImageBatchVarShapeWrapNHWC src, ImageBatchVarShapeWrapNHWC dst, curandState *state, - Tensor1DWrap mu, Tensor1DWrap sigma) + curandState *nextState, Tensor1DWrap mu, + Tensor1DWrap sigma) { - int offset = threadIdx.x; int batch_idx = blockIdx.x; int id = threadIdx.x + blockIdx.x * blockDim.x; - int total_size = dst.height(batch_idx) * dst.width(batch_idx); + const int width = dst.width(batch_idx); + int total_size = dst.height(batch_idx) * width; + int offset = 0; + int segmentEnd = 0; int channel = src.numChannels(); - curandState localState = state[id]; - while (offset < total_size) + const float batchMu = mu[batch_idx]; + const float batchSigma = sigma[batch_idx]; + curandState localState; + if (!gaussian_noise_segment_state(state, id, total_size, channel, offset, segmentEnd, localState)) + { + return; + } + while (offset + blockDim.x < segmentEnd) + { + int dst_x0 = offset % width; + int dst_y0 = offset / width; + int offset1 = offset + blockDim.x; + int dst_x1 = offset1 % width; + int dst_y1 = offset1 / width; + gaussian_noise_store_channel_pair(src, dst, localState, batch_idx, dst_y0, dst_x0, dst_y1, dst_x1, + channel, batchMu, batchSigma); + offset += 2 * blockDim.x; + } + while (offset < segmentEnd) { - int dst_x = offset % dst.width(batch_idx); - int dst_y = offset / dst.width(batch_idx); + int dst_x = offset % width; + int dst_y = offset / width; for (int ch = 0; ch < channel; ch++) { float rand = curand_normal(&localState); - float delta = mu[batch_idx] + rand * sigma[batch_idx]; + float delta = batchMu + rand * batchSigma; T out = SaturateCast(*src.ptr(batch_idx, dst_y, dst_x, ch) + delta); *dst.ptr(batch_idx, dst_y, dst_x, ch) = clamp(StaticCast(out), 0.f, 1.f); } offset += blockDim.x; } - state[id] = localState; + gaussian_noise_store_segment_state(state, nextState, id, total_size, segmentEnd, localState); +} + +template +__global__ void gaussian_noise_planar_kernel(const ImageBatchVarShapeWrap src, ImageBatchVarShapeWrap dst, + curandState *state, curandState *nextState, Tensor1DWrap mu, + Tensor1DWrap sigma, int channels) +{ + int batch_idx = blockIdx.x; + int id = threadIdx.x + blockIdx.x * blockDim.x; + const int width = dst.width(batch_idx); + int total_size = dst.height(batch_idx) * width; + int offset = 0; + int segmentEnd = 0; + const float batchMu = mu[batch_idx]; + const float batchSigma = sigma[batch_idx]; + curandState localState; + if (!gaussian_noise_segment_state(state, id, total_size, 1, offset, segmentEnd, localState)) + { + return; + } + while (offset + blockDim.x < segmentEnd) + { + int dst_x0 = offset % width; + int dst_y0 = offset / width; + int offset1 = offset + blockDim.x; + int dst_x1 = offset1 % width; + int dst_y1 = offset1 / width; + float2 rand = gaussian_noise_normal2(localState); + float delta0 = batchMu + rand.x * batchSigma; + float delta1 = batchMu + rand.y * batchSigma; + for (int ch = 0; ch < channels; ch++) + { + *dst.ptr(batch_idx, ch, dst_y0, dst_x0) = SaturateCast(*src.ptr(batch_idx, ch, dst_y0, dst_x0) + delta0); + *dst.ptr(batch_idx, ch, dst_y1, dst_x1) = SaturateCast(*src.ptr(batch_idx, ch, dst_y1, dst_x1) + delta1); + } + offset += 2 * blockDim.x; + } + while (offset < segmentEnd) + { + int dst_x = offset % width; + int dst_y = offset / width; + float rand = curand_normal(&localState); + float delta = batchMu + rand * batchSigma; + for (int ch = 0; ch < channels; ch++) + { + *dst.ptr(batch_idx, ch, dst_y, dst_x) = SaturateCast(*src.ptr(batch_idx, ch, dst_y, dst_x) + delta); + } + offset += blockDim.x; + } + gaussian_noise_store_segment_state(state, nextState, id, total_size, segmentEnd, localState); +} + +template +__global__ void gaussian_noise_planar_per_channel_kernel(const ImageBatchVarShapeWrap src, + ImageBatchVarShapeWrap dst, curandState *state, + curandState *nextState, Tensor1DWrap mu, + Tensor1DWrap sigma, int channels) +{ + int batch_idx = blockIdx.x; + int id = threadIdx.x + blockIdx.x * blockDim.x; + const int width = dst.width(batch_idx); + int total_size = dst.height(batch_idx) * width; + int offset = 0; + int segmentEnd = 0; + const float batchMu = mu[batch_idx]; + const float batchSigma = sigma[batch_idx]; + curandState localState; + if (!gaussian_noise_segment_state(state, id, total_size, channels, offset, segmentEnd, localState)) + { + return; + } + while (offset + blockDim.x < segmentEnd) + { + int dst_x0 = offset % width; + int dst_y0 = offset / width; + int offset1 = offset + blockDim.x; + int dst_x1 = offset1 % width; + int dst_y1 = offset1 / width; + gaussian_noise_store_channel_pair(src, dst, localState, batch_idx, dst_y0, dst_x0, dst_y1, dst_x1, + channels, batchMu, batchSigma); + offset += 2 * blockDim.x; + } + while (offset < segmentEnd) + { + int dst_x = offset % width; + int dst_y = offset / width; + for (int ch = 0; ch < channels; ch++) + { + float rand = curand_normal(&localState); + float delta = batchMu + rand * batchSigma; + *dst.ptr(batch_idx, ch, dst_y, dst_x) = SaturateCast(*src.ptr(batch_idx, ch, dst_y, dst_x) + delta); + } + offset += blockDim.x; + } + gaussian_noise_store_segment_state(state, nextState, id, total_size, segmentEnd, localState); +} + +template +__global__ void gaussian_noise_float_planar_kernel(const ImageBatchVarShapeWrap src, ImageBatchVarShapeWrap dst, + curandState *state, curandState *nextState, Tensor1DWrap mu, + Tensor1DWrap sigma, int channels) +{ + int batch_idx = blockIdx.x; + int id = threadIdx.x + blockIdx.x * blockDim.x; + const int width = dst.width(batch_idx); + int total_size = dst.height(batch_idx) * width; + int offset = 0; + int segmentEnd = 0; + const float batchMu = mu[batch_idx]; + const float batchSigma = sigma[batch_idx]; + curandState localState; + if (!gaussian_noise_segment_state(state, id, total_size, 1, offset, segmentEnd, localState)) + { + return; + } + while (offset + blockDim.x < segmentEnd) + { + int dst_x0 = offset % width; + int dst_y0 = offset / width; + int offset1 = offset + blockDim.x; + int dst_x1 = offset1 % width; + int dst_y1 = offset1 / width; + float2 rand = gaussian_noise_normal2(localState); + float delta0 = batchMu + rand.x * batchSigma; + float delta1 = batchMu + rand.y * batchSigma; + for (int ch = 0; ch < channels; ch++) + { + T out0 = SaturateCast(*src.ptr(batch_idx, ch, dst_y0, dst_x0) + delta0); + T out1 = SaturateCast(*src.ptr(batch_idx, ch, dst_y1, dst_x1) + delta1); + *dst.ptr(batch_idx, ch, dst_y0, dst_x0) = clamp(StaticCast(out0), 0.f, 1.f); + *dst.ptr(batch_idx, ch, dst_y1, dst_x1) = clamp(StaticCast(out1), 0.f, 1.f); + } + offset += 2 * blockDim.x; + } + while (offset < segmentEnd) + { + int dst_x = offset % width; + int dst_y = offset / width; + float rand = curand_normal(&localState); + float delta = batchMu + rand * batchSigma; + for (int ch = 0; ch < channels; ch++) + { + T out = SaturateCast(*src.ptr(batch_idx, ch, dst_y, dst_x) + delta); + *dst.ptr(batch_idx, ch, dst_y, dst_x) = clamp(StaticCast(out), 0.f, 1.f); + } + offset += blockDim.x; + } + gaussian_noise_store_segment_state(state, nextState, id, total_size, segmentEnd, localState); +} + +template +__global__ void gaussian_noise_float_planar_per_channel_kernel(const ImageBatchVarShapeWrap src, + ImageBatchVarShapeWrap dst, curandState *state, + curandState *nextState, Tensor1DWrap mu, + Tensor1DWrap sigma, int channels) +{ + int batch_idx = blockIdx.x; + int id = threadIdx.x + blockIdx.x * blockDim.x; + const int width = dst.width(batch_idx); + int total_size = dst.height(batch_idx) * width; + int offset = 0; + int segmentEnd = 0; + const float batchMu = mu[batch_idx]; + const float batchSigma = sigma[batch_idx]; + curandState localState; + if (!gaussian_noise_segment_state(state, id, total_size, channels, offset, segmentEnd, localState)) + { + return; + } + while (offset + blockDim.x < segmentEnd) + { + int dst_x0 = offset % width; + int dst_y0 = offset / width; + int offset1 = offset + blockDim.x; + int dst_x1 = offset1 % width; + int dst_y1 = offset1 / width; + gaussian_noise_store_channel_pair(src, dst, localState, batch_idx, dst_y0, dst_x0, dst_y1, dst_x1, + channels, batchMu, batchSigma); + offset += 2 * blockDim.x; + } + while (offset < segmentEnd) + { + int dst_x = offset % width; + int dst_y = offset / width; + for (int ch = 0; ch < channels; ch++) + { + float rand = curand_normal(&localState); + float delta = batchMu + rand * batchSigma; + T out = SaturateCast(*src.ptr(batch_idx, ch, dst_y, dst_x) + delta); + *dst.ptr(batch_idx, ch, dst_y, dst_x) = clamp(StaticCast(out), 0.f, 1.f); + } + offset += blockDim.x; + } + gaussian_noise_store_segment_state(state, nextState, id, total_size, segmentEnd, localState); } template void gaussian_noise(const nvcv::ImageBatchVarShapeDataStridedCuda &d_in, const nvcv::ImageBatchVarShapeDataStridedCuda &d_out, curandState *m_states, - const nvcv::TensorDataStridedCuda &_mu, const nvcv::TensorDataStridedCuda &_sigma, - cudaStream_t stream) + curandState *m_nextStates, const nvcv::TensorDataStridedCuda &_mu, + const nvcv::TensorDataStridedCuda &_sigma, cudaStream_t stream) { ImageBatchVarShapeWrap src_ptr(d_in); ImageBatchVarShapeWrap dst_ptr(d_out); Tensor1DWrap mu(_mu); Tensor1DWrap sigma(_sigma); - int batch = d_in.numImages(); - gaussian_noise_kernel<<>>(src_ptr, dst_ptr, m_states, mu, sigma); + int batch = d_in.numImages(); + int segments = gaussian_noise_var_shape_segments(d_out); + + gaussian_noise_copy_segment_states(m_nextStates, m_states, batch, BLOCK, segments, stream); + gaussian_noise_kernel + <<>>(src_ptr, dst_ptr, m_states, m_nextStates, mu, sigma); checkKernelErrors(); + gaussian_noise_copy_segment_states(m_states, m_nextStates, batch, BLOCK, segments, stream); } template void gaussian_noise_per_channel(const nvcv::ImageBatchVarShapeDataStridedCuda &d_in, const nvcv::ImageBatchVarShapeDataStridedCuda &d_out, int channels, - curandState *m_states, const nvcv::TensorDataStridedCuda &_mu, - const nvcv::TensorDataStridedCuda &_sigma, cudaStream_t stream) + curandState *m_states, curandState *m_nextStates, + const nvcv::TensorDataStridedCuda &_mu, const nvcv::TensorDataStridedCuda &_sigma, + cudaStream_t stream) { ImageBatchVarShapeWrapNHWC src_ptr(d_in, channels); ImageBatchVarShapeWrapNHWC dst_ptr(d_out, channels); Tensor1DWrap mu(_mu); Tensor1DWrap sigma(_sigma); - int batch = d_in.numImages(); - gaussian_noise_per_channel_kernel<<>>(src_ptr, dst_ptr, m_states, mu, sigma); + int batch = d_in.numImages(); + int segments = gaussian_noise_var_shape_segments(d_out); + + gaussian_noise_copy_segment_states(m_nextStates, m_states, batch, BLOCK, segments, stream); + gaussian_noise_per_channel_kernel + <<>>(src_ptr, dst_ptr, m_states, m_nextStates, mu, sigma); checkKernelErrors(); + gaussian_noise_copy_segment_states(m_states, m_nextStates, batch, BLOCK, segments, stream); } template void gaussian_noise_float(const nvcv::ImageBatchVarShapeDataStridedCuda &d_in, const nvcv::ImageBatchVarShapeDataStridedCuda &d_out, curandState *m_states, - const nvcv::TensorDataStridedCuda &_mu, const nvcv::TensorDataStridedCuda &_sigma, - cudaStream_t stream) + curandState *m_nextStates, const nvcv::TensorDataStridedCuda &_mu, + const nvcv::TensorDataStridedCuda &_sigma, cudaStream_t stream) { ImageBatchVarShapeWrap src_ptr(d_in); ImageBatchVarShapeWrap dst_ptr(d_out); Tensor1DWrap mu(_mu); Tensor1DWrap sigma(_sigma); - int batch = d_in.numImages(); - gaussian_noise_float_kernel<<>>(src_ptr, dst_ptr, m_states, mu, sigma); + int batch = d_in.numImages(); + int segments = gaussian_noise_var_shape_segments(d_out); + + gaussian_noise_copy_segment_states(m_nextStates, m_states, batch, BLOCK, segments, stream); + gaussian_noise_float_kernel + <<>>(src_ptr, dst_ptr, m_states, m_nextStates, mu, sigma); checkKernelErrors(); + gaussian_noise_copy_segment_states(m_states, m_nextStates, batch, BLOCK, segments, stream); } template void gaussian_noise_float_per_channel(const nvcv::ImageBatchVarShapeDataStridedCuda &d_in, const nvcv::ImageBatchVarShapeDataStridedCuda &d_out, int channels, - curandState *m_states, const nvcv::TensorDataStridedCuda &_mu, - const nvcv::TensorDataStridedCuda &_sigma, cudaStream_t stream) + curandState *m_states, curandState *m_nextStates, + const nvcv::TensorDataStridedCuda &_mu, const nvcv::TensorDataStridedCuda &_sigma, + cudaStream_t stream) { ImageBatchVarShapeWrapNHWC src_ptr(d_in, channels); ImageBatchVarShapeWrapNHWC dst_ptr(d_out, channels); Tensor1DWrap mu(_mu); Tensor1DWrap sigma(_sigma); - int batch = d_in.numImages(); - gaussian_noise_float_per_channel_kernel<<>>(src_ptr, dst_ptr, m_states, mu, sigma); + int batch = d_in.numImages(); + int segments = gaussian_noise_var_shape_segments(d_out); + + gaussian_noise_copy_segment_states(m_nextStates, m_states, batch, BLOCK, segments, stream); + gaussian_noise_float_per_channel_kernel + <<>>(src_ptr, dst_ptr, m_states, m_nextStates, mu, sigma); + checkKernelErrors(); + gaussian_noise_copy_segment_states(m_states, m_nextStates, batch, BLOCK, segments, stream); +} + +template +void gaussian_noise_planar(const nvcv::ImageBatchVarShapeDataStridedCuda &d_in, + const nvcv::ImageBatchVarShapeDataStridedCuda &d_out, int channels, curandState *m_states, + curandState *m_nextStates, const nvcv::TensorDataStridedCuda &_mu, + const nvcv::TensorDataStridedCuda &_sigma, cudaStream_t stream) +{ + ImageBatchVarShapeWrap src_ptr(d_in); + ImageBatchVarShapeWrap dst_ptr(d_out); + Tensor1DWrap mu(_mu); + Tensor1DWrap sigma(_sigma); + + int batch = d_in.numImages(); + int segments = gaussian_noise_var_shape_segments(d_out); + + gaussian_noise_copy_segment_states(m_nextStates, m_states, batch, BLOCK, segments, stream); + gaussian_noise_planar_kernel + <<>>(src_ptr, dst_ptr, m_states, m_nextStates, mu, sigma, channels); checkKernelErrors(); + gaussian_noise_copy_segment_states(m_states, m_nextStates, batch, BLOCK, segments, stream); +} + +template +void gaussian_noise_planar_per_channel(const nvcv::ImageBatchVarShapeDataStridedCuda &d_in, + const nvcv::ImageBatchVarShapeDataStridedCuda &d_out, int channels, + curandState *m_states, curandState *m_nextStates, + const nvcv::TensorDataStridedCuda &_mu, + const nvcv::TensorDataStridedCuda &_sigma, cudaStream_t stream) +{ + ImageBatchVarShapeWrap src_ptr(d_in); + ImageBatchVarShapeWrap dst_ptr(d_out); + Tensor1DWrap mu(_mu); + Tensor1DWrap sigma(_sigma); + + int batch = d_in.numImages(); + int segments = gaussian_noise_var_shape_segments(d_out); + + gaussian_noise_copy_segment_states(m_nextStates, m_states, batch, BLOCK, segments, stream); + gaussian_noise_planar_per_channel_kernel + <<>>(src_ptr, dst_ptr, m_states, m_nextStates, mu, sigma, channels); + checkKernelErrors(); + gaussian_noise_copy_segment_states(m_states, m_nextStates, batch, BLOCK, segments, stream); +} + +template +void gaussian_noise_float_planar(const nvcv::ImageBatchVarShapeDataStridedCuda &d_in, + const nvcv::ImageBatchVarShapeDataStridedCuda &d_out, int channels, + curandState *m_states, curandState *m_nextStates, + const nvcv::TensorDataStridedCuda &_mu, const nvcv::TensorDataStridedCuda &_sigma, + cudaStream_t stream) +{ + ImageBatchVarShapeWrap src_ptr(d_in); + ImageBatchVarShapeWrap dst_ptr(d_out); + Tensor1DWrap mu(_mu); + Tensor1DWrap sigma(_sigma); + + int batch = d_in.numImages(); + int segments = gaussian_noise_var_shape_segments(d_out); + + gaussian_noise_copy_segment_states(m_nextStates, m_states, batch, BLOCK, segments, stream); + gaussian_noise_float_planar_kernel + <<>>(src_ptr, dst_ptr, m_states, m_nextStates, mu, sigma, channels); + checkKernelErrors(); + gaussian_noise_copy_segment_states(m_states, m_nextStates, batch, BLOCK, segments, stream); +} + +template +void gaussian_noise_float_planar_per_channel(const nvcv::ImageBatchVarShapeDataStridedCuda &d_in, + const nvcv::ImageBatchVarShapeDataStridedCuda &d_out, int channels, + curandState *m_states, curandState *m_nextStates, + const nvcv::TensorDataStridedCuda &_mu, + const nvcv::TensorDataStridedCuda &_sigma, cudaStream_t stream) +{ + ImageBatchVarShapeWrap src_ptr(d_in); + ImageBatchVarShapeWrap dst_ptr(d_out); + Tensor1DWrap mu(_mu); + Tensor1DWrap sigma(_sigma); + + int batch = d_in.numImages(); + int segments = gaussian_noise_var_shape_segments(d_out); + + gaussian_noise_copy_segment_states(m_nextStates, m_states, batch, BLOCK, segments, stream); + gaussian_noise_float_planar_per_channel_kernel + <<>>(src_ptr, dst_ptr, m_states, m_nextStates, mu, sigma, channels); + checkKernelErrors(); + gaussian_noise_copy_segment_states(m_states, m_nextStates, batch, BLOCK, segments, stream); } namespace nvcv::legacy::cuda_op { @@ -198,6 +596,7 @@ namespace nvcv::legacy::cuda_op { GaussianNoiseVarShape::GaussianNoiseVarShape(DataShape max_input_shape, DataShape max_output_shape, int maxBatchSize) : CudaBaseOp(max_input_shape, max_output_shape) , m_states(nullptr) + , m_nextStates(nullptr) , m_seed(0) , m_maxBatchSize(maxBatchSize) , m_setupDone(false) @@ -207,12 +606,19 @@ GaussianNoiseVarShape::GaussianNoiseVarShape(DataShape max_input_shape, DataShap LOG_ERROR("Invalid num of max batch size " << maxBatchSize); throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "maxBatchSize must be >= 0"); } - cudaError_t err = cudaMalloc((void **)&m_states, sizeof(curandState) * BLOCK * maxBatchSize); + const size_t stateBytes + = cvcuda::priv::CheckedMulMany({sizeof(curandState), static_cast(BLOCK), + cvcuda::priv::CheckedNonNegativeToSize(maxBatchSize, "maxBatchSize")}, + "GaussianNoiseVarShape curand state allocation size overflow"); + const size_t allocationBytes + = cvcuda::priv::CheckedMulMany({stateBytes, 2}, "GaussianNoiseVarShape curand state allocation size overflow"); + cudaError_t err = cudaMalloc((void **)&m_states, allocationBytes); if (err != cudaSuccess) { - LOG_ERROR("CUDA memory allocation error of size: " << sizeof(curandState) * BLOCK * maxBatchSize); - throw std::runtime_error("CUDA memory allocation error!"); + LOG_ERROR("CUDA memory allocation error of size: " << allocationBytes); + throw LegacyCudaAllocationError("CUDA memory allocation error!"); } + m_nextStates = maxBatchSize > 0 ? m_states + static_cast(BLOCK) * maxBatchSize : m_states; } GaussianNoiseVarShape::~GaussianNoiseVarShape() @@ -229,23 +635,48 @@ ErrorCode GaussianNoiseVarShape::infer(const ImageBatchVarShapeDataStridedCuda & { DataFormat in_format = helpers::GetLegacyDataFormat(inData); DataFormat out_format = helpers::GetLegacyDataFormat(outData); - if (!(in_format == kNHWC || in_format == kHWC)) + if (!(in_format == kNHWC || in_format == kHWC || in_format == kNCHW || in_format == kCHW)) { - LOG_ERROR("Invalid input DataFormat " << in_format << ", the valid DataFormats are: \"NHWC\", \"HWC\""); + LOG_ERROR("Invalid input DataFormat " << in_format + << ", the valid DataFormats are: \"NHWC\", \"HWC\", \"NCHW\", \"CHW\""); return ErrorCode::INVALID_DATA_FORMAT; } - if (!(out_format == kNHWC || out_format == kHWC)) + if (!(out_format == kNHWC || out_format == kHWC || out_format == kNCHW || out_format == kCHW)) { - LOG_ERROR("Invalid output DataFormat " << out_format << ", the valid DataFormats are: \"NHWC\", \"HWC\""); + LOG_ERROR("Invalid output DataFormat " << out_format + << ", the valid DataFormats are: \"NHWC\", \"HWC\", \"NCHW\", \"CHW\""); + return ErrorCode::INVALID_DATA_FORMAT; + } + const bool isPlanar = in_format == kNCHW || in_format == kCHW; + const bool isOutPlanar = out_format == kNCHW || out_format == kCHW; + if (isPlanar != isOutPlanar) + { + LOG_ERROR("Input and output must both be interleaved or both be planar"); return ErrorCode::INVALID_DATA_FORMAT; } int channels = inData.uniqueFormat().numChannels(); - if (channels > 4) + if (channels > 4 || (isPlanar && channels == 2)) { LOG_ERROR("Invalid channel number " << channels); return ErrorCode::INVALID_DATA_SHAPE; } + if (isPlanar && outData.uniqueFormat().numChannels() != channels) + { + LOG_ERROR("Planar input and output must have matching channel counts"); + return ErrorCode::INVALID_DATA_SHAPE; + } + if (isPlanar && static_cast(inData.numImages()) * channels > 65535) + { + LOG_ERROR("Planar GaussianNoise requires numImages * channels <= 65535 (CUDA grid-z limit)"); + return ErrorCode::INVALID_PARAMETER; + } + + if (inData.numImages() > m_maxBatchSize) + { + LOG_ERROR("Input batch exceeds maxBatchSize"); + return ErrorCode::INVALID_PARAMETER; + } DataType in_data_type = helpers::GetLegacyDataType(inData.uniqueFormat()); if (!(in_data_type == kCV_8U || in_data_type == kCV_16U || in_data_type == kCV_16S || in_data_type == kCV_32S @@ -299,8 +730,8 @@ ErrorCode GaussianNoiseVarShape::infer(const ImageBatchVarShapeDataStridedCuda & { typedef void (*func_t)(const ImageBatchVarShapeDataStridedCuda &d_in, const ImageBatchVarShapeDataStridedCuda &d_out, int channels, curandState *m_states, - const TensorDataStridedCuda &mu, const TensorDataStridedCuda &sigma, - cudaStream_t stream); + curandState *m_nextStates, const TensorDataStridedCuda &mu, + const TensorDataStridedCuda &sigma, cudaStream_t stream); static const func_t funcs[5] = { gaussian_noise_per_channel, 0, gaussian_noise_per_channel, gaussian_noise_per_channel, @@ -310,26 +741,38 @@ ErrorCode GaussianNoiseVarShape::infer(const ImageBatchVarShapeDataStridedCuda & static const func_t float_funcs[1] = { gaussian_noise_float_per_channel, }; + static const func_t planar_funcs[5] = { + gaussian_noise_planar_per_channel, 0, + gaussian_noise_planar_per_channel, gaussian_noise_planar_per_channel, + gaussian_noise_planar_per_channel, + }; + static const func_t planar_float_funcs[1] = { + gaussian_noise_float_planar_per_channel, + }; if (in_data_type == kCV_32F) { - const func_t func = float_funcs[0]; + const func_t func = isPlanar ? planar_float_funcs[0] : float_funcs[0]; assert(func != 0); - func(inData, outData, channels, m_states, mu, sigma, stream); + func(inData, outData, channels, m_states, m_nextStates, mu, sigma, stream); } else { - const func_t func = funcs[in_data_type]; + const func_t func = isPlanar ? planar_funcs[in_data_type] : funcs[in_data_type]; assert(func != 0); - func(inData, outData, channels, m_states, mu, sigma, stream); + func(inData, outData, channels, m_states, m_nextStates, mu, sigma, stream); } } else { typedef void (*func_t)(const ImageBatchVarShapeDataStridedCuda &d_in, const ImageBatchVarShapeDataStridedCuda &d_out, curandState *m_states, - const TensorDataStridedCuda &mu, const TensorDataStridedCuda &sigma, - cudaStream_t stream); + curandState *m_nextStates, const TensorDataStridedCuda &mu, + const TensorDataStridedCuda &sigma, cudaStream_t stream); + typedef void (*planar_func_t)(const ImageBatchVarShapeDataStridedCuda &d_in, + const ImageBatchVarShapeDataStridedCuda &d_out, int channels, + curandState *m_states, curandState *m_nextStates, const TensorDataStridedCuda &mu, + const TensorDataStridedCuda &sigma, cudaStream_t stream); static const func_t funcs[5][4] = { { gaussian_noise, gaussian_noise, gaussian_noise,gaussian_noise }, @@ -340,20 +783,45 @@ ErrorCode GaussianNoiseVarShape::infer(const ImageBatchVarShapeDataStridedCuda & { gaussian_noise, gaussian_noise, gaussian_noise, gaussian_noise}, }; - static const func_t float_funcs[4] = {gaussian_noise_float, gaussian_noise_float, - gaussian_noise_float, gaussian_noise_float}; + static const func_t float_funcs[4] = {gaussian_noise_float, gaussian_noise_float, + gaussian_noise_float, gaussian_noise_float}; + static const planar_func_t planar_funcs[5] = { + gaussian_noise_planar, 0, gaussian_noise_planar, gaussian_noise_planar, + gaussian_noise_planar, + }; + static const planar_func_t planar_float_funcs[1] = { + gaussian_noise_float_planar, + }; if (in_data_type == kCV_32F) { - const func_t func = float_funcs[channels - 1]; - assert(func != 0); - func(inData, outData, m_states, mu, sigma, stream); + if (isPlanar) + { + const planar_func_t func = planar_float_funcs[0]; + assert(func != 0); + func(inData, outData, channels, m_states, m_nextStates, mu, sigma, stream); + } + else + { + const func_t func = float_funcs[channels - 1]; + assert(func != 0); + func(inData, outData, m_states, m_nextStates, mu, sigma, stream); + } } else { - const func_t func = funcs[in_data_type][channels - 1]; - assert(func != 0); - func(inData, outData, m_states, mu, sigma, stream); + if (isPlanar) + { + const planar_func_t func = planar_funcs[in_data_type]; + assert(func != 0); + func(inData, outData, channels, m_states, m_nextStates, mu, sigma, stream); + } + else + { + const func_t func = funcs[in_data_type][channels - 1]; + assert(func != 0); + func(inData, outData, m_states, m_nextStates, mu, sigma, stream); + } } } return SUCCESS; diff --git a/src/cvcuda/priv/legacy/histogram_eq.cu b/src/cvcuda/priv/legacy/histogram_eq.cu index ca497c7cf..a3c3c5f82 100644 --- a/src/cvcuda/priv/legacy/histogram_eq.cu +++ b/src/cvcuda/priv/legacy/histogram_eq.cu @@ -1,4 +1,4 @@ -/* Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +/* Copyright (c) 2021-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * * SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES * SPDX-License-Identifier: Apache-2.0 @@ -33,10 +33,23 @@ using namespace nvcv::legacy::cuda_op; using namespace nvcv::legacy::helpers; -template +template +__device__ __forceinline__ int4 imageCoord(int batch, int y, int x, int ch) +{ + if constexpr (IsPlanar) + { + return int4{x, y, ch, batch}; + } + else + { + return int4{ch, x, y, batch}; + } +} + +template __global__ void hist_kernel(const SrcWrapper src, DstWrapper histogram, int channels, nvcv::Size2D dstSize) { - const int src_x = blockIdx.x * blockDim.x + threadIdx.x; + const int src_x0 = blockIdx.x * blockDim.x * PIXELS_PER_THREAD_X + threadIdx.x; const int src_y = blockIdx.y * blockDim.y + threadIdx.y; const int batch_idx = get_batch_idx(); const int local_id = threadIdx.y * blockDim.x + threadIdx.x; @@ -49,15 +62,21 @@ __global__ void hist_kernel(const SrcWrapper src, DstWrapper histogram, int chan } __syncthreads(); // wait for all threads to finish initialization - //check if we are in the image. - if (src_x < dstSize.w && src_y < dstSize.h) +#pragma unroll + for (int i = 0; i < PIXELS_PER_THREAD_X; ++i) { - for (int ch = 0; ch < channels; ch++) + const int src_x = src_x0 + i * blockDim.x; + + // Skip widened X positions that extend beyond the final partial tile. + if (src_x < dstSize.w && src_y < dstSize.h) { - int4 coordImg{ch, src_x, src_y, batch_idx}; - uchar out = src[coordImg]; - int idx = out + (256 * ch); - atomicAdd(&shist[idx], 1); + for (int ch = 0; ch < channels; ch++) + { + int4 coordImg = imageCoord(batch_idx, src_y, src_x, ch); + uchar out = src[coordImg]; + int idx = out + (256 * ch); + atomicAdd(&shist[idx], 1); + } } } __syncthreads(); @@ -139,7 +158,7 @@ __global__ void prefix_sum_with_norm_kernel(CdfWrapper histogram, nvcv::Size2D d } } -template +template __global__ void lookup(const SrcWrapper src, DstWrapper dst, CdfWrapper cdf, int channels, nvcv::Size2D dstSize) { @@ -163,9 +182,8 @@ __global__ void lookup(const SrcWrapper src, DstWrapper dst, CdfWrapper cdf, int int offset = 0; for (int ch = 0; ch < channels; ch++) { - offset = 256 * ch; - int4 coordImg{ch, src_x, src_y, batch_idx}; - int2 coordHisto{src[coordImg] + offset, batch_idx}; + offset = 256 * ch; + int4 coordImg = imageCoord(batch_idx, src_y, src_x, ch); dst[coordImg] = nvcv::cuda::SaturateCast((temp[src[coordImg] + offset])); } } @@ -201,7 +219,7 @@ HistogramEq::~HistogramEq() } } -template +template ErrorCode infer_histogram(SrcWrap src, DstWrap dst, HistWrap histo, int batch, nvcv::Size2D dstSize, int channels, cudaStream_t stream) { @@ -213,7 +231,7 @@ ErrorCode infer_histogram(SrcWrap src, DstWrap dst, HistWrap histo, int batch, n switch (channels) { case 1: - bsX = 16; // 256 (1 ch) + bsX = 32; // 512 (1 ch) bsY = 16; break; case 2: @@ -228,11 +246,25 @@ ErrorCode infer_histogram(SrcWrap src, DstWrap dst, HistWrap histo, int batch, n break; } + constexpr int kPixelsPerThreadC1 = 8; + constexpr int kPixelsPerThreadCN = 1; + int histPixelsPerThreadX = channels == 1 ? kPixelsPerThreadC1 : kPixelsPerThreadCN; + // each block is going to be 256bins * channels = threads dim3 histBlockSize(bsX, bsY, 1); - dim3 histGridSize(divUp(dstSize.w, histBlockSize.x), divUp(dstSize.h, histBlockSize.y), batch); + dim3 histGridSize(divUp(dstSize.w, histBlockSize.x * histPixelsPerThreadX), divUp(dstSize.h, histBlockSize.y), + batch); size_t sharedMemSize = 256 * channels * sizeof(int); - hist_kernel<<>>(src, histo, channels, dstSize); + if (channels == 1) + { + hist_kernel + <<>>(src, histo, channels, dstSize); + } + else + { + hist_kernel + <<>>(src, histo, channels, dstSize); + } checkKernelErrors(); } @@ -250,8 +282,8 @@ ErrorCode infer_histogram(SrcWrap src, DstWrap dst, HistWrap histo, int batch, n { dim3 lookupBlockSize(32, 32, 1); dim3 lookupGridSize(divUp(dstSize.w, lookupBlockSize.x), divUp(dstSize.h, lookupBlockSize.y), batch); - lookup<<>>(src, dst, histo, channels, - dstSize); + lookup<<>>(src, dst, histo, + channels, dstSize); checkKernelErrors(); } @@ -280,11 +312,12 @@ ErrorCode HistogramEq::infer(const TensorDataStridedCuda &inData, const TensorDa DataFormat format = input_format; - if (!(format == kNHWC || format == kHWC)) + if (!(format == kNHWC || format == kHWC || format == kNCHW || format == kCHW)) { LOG_ERROR("Invliad DataFormat " << format); return ErrorCode::INVALID_DATA_FORMAT; } + const bool isPlanar = (format == kNCHW || format == kCHW); if (!(data_type == kCV_8U)) { @@ -303,17 +336,40 @@ ErrorCode HistogramEq::infer(const TensorDataStridedCuda &inData, const TensorDa int height = inAccess->numRows(); nvcv::Size2D dstSize{width, height}; + if (m_maxBatchSize <= 0 || batch > m_maxBatchSize) + { + LOG_ERROR("Invalid maximum batch size " << m_maxBatchSize << " for input batch " << batch); + return ErrorCode::INVALID_PARAMETER; + } + if (channels > 4 || channels < 1) { LOG_ERROR("Invalid channel number ch = " << channels); return ErrorCode::INVALID_DATA_SHAPE; } + if (isPlanar && channels == 2) + { + LOG_ERROR("2-channel planar HistogramEq is unsupported"); + return ErrorCode::INVALID_DATA_SHAPE; + } auto outAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(outData); if (!outAccess) { return ErrorCode::INVALID_DATA_FORMAT; } + if (outAccess->numSamples() != batch || outAccess->numChannels() != channels || outAccess->numCols() != width + || outAccess->numRows() != height) + { + LOG_ERROR("Input and output tensor shapes must match"); + return ErrorCode::INVALID_DATA_SHAPE; + } + + if (batch > 65535) + { + LOG_ERROR("HistogramEq input exceeds the 65535 batch launch limit"); + return ErrorCode::INVALID_DATA_SHAPE; + } //clear the histogram. checkCudaErrors(cudaMemsetAsync(m_histoArray, 0, m_sizeOfHisto, stream)); @@ -326,10 +382,20 @@ ErrorCode HistogramEq::infer(const TensorDataStridedCuda &inData, const TensorDa if (std::max(srcMaxStride, dstMaxStride) <= cuda::TypeTraits::max) { - auto src = nvcv::cuda::CreateTensorWrapNHWC(inData); - auto dst = nvcv::cuda::CreateTensorWrapNHWC(outData); + if (isPlanar) + { + auto src = nvcv::cuda::CreateTensorWrapNCHW(inData); + auto dst = nvcv::cuda::CreateTensorWrapNCHW(outData); - return infer_histogram(src, dst, histo, batch, dstSize, channels, stream); + return infer_histogram(src, dst, histo, batch, dstSize, channels, stream); + } + else + { + auto src = nvcv::cuda::CreateTensorWrapNHWC(inData); + auto dst = nvcv::cuda::CreateTensorWrapNHWC(outData); + + return infer_histogram(src, dst, histo, batch, dstSize, channels, stream); + } } else { diff --git a/src/cvcuda/priv/legacy/histogram_eq_var_shape.cu b/src/cvcuda/priv/legacy/histogram_eq_var_shape.cu index c8193ccc5..68c20690a 100644 --- a/src/cvcuda/priv/legacy/histogram_eq_var_shape.cu +++ b/src/cvcuda/priv/legacy/histogram_eq_var_shape.cu @@ -1,4 +1,4 @@ -/* Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +/* Copyright (c) 2021-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * * SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES * SPDX-License-Identifier: Apache-2.0 @@ -29,7 +29,20 @@ using namespace nvcv::legacy::cuda_op; using namespace nvcv::legacy::helpers; -template +template +__device__ __forceinline__ int4 imageCoord(int batch, int y, int x, int ch) +{ + if constexpr (IsPlanar) + { + return int4{x, y, ch, batch}; + } + else + { + return int4{batch, y, x, ch}; + } +} + +template __global__ void hist_kernel(const SrcWrapper src, DstWrapper histogram, int channels) { const int src_x = blockIdx.x * blockDim.x + threadIdx.x; @@ -51,9 +64,9 @@ __global__ void hist_kernel(const SrcWrapper src, DstWrapper histogram, int chan { for (int ch = 0; ch < channels; ch++) { - int4 coordImg{batch_idx, src_y, src_x, ch}; - uchar out = src[coordImg]; - int idx = out + (256 * ch); + int4 coordImg = imageCoord(batch_idx, src_y, src_x, ch); + uchar out = src[coordImg]; + int idx = out + (256 * ch); atomicAdd(&shist[idx], 1); } } @@ -135,7 +148,7 @@ __global__ void prefix_sum_with_norm_kernel(CdfWrapper histogram, SrcWrapper dst } } -template +template __global__ void lookup(const SrcWrapper src, DstWrapper dst, CdfWrapper cdf, int channels) { @@ -159,9 +172,8 @@ __global__ void lookup(const SrcWrapper src, DstWrapper dst, CdfWrapper cdf, int int offset = 0; for (int ch = 0; ch < channels; ch++) { - offset = 256 * ch; - int4 coordImg{batch_idx, src_y, src_x, ch}; - int2 coordHisto{src[coordImg] + offset, batch_idx}; + offset = 256 * ch; + int4 coordImg = imageCoord(batch_idx, src_y, src_x, ch); dst[coordImg] = nvcv::cuda::SaturateCast((temp[src[coordImg] + offset])); } } @@ -222,11 +234,12 @@ ErrorCode HistogramEqVarShape::infer(const nvcv::ImageBatchVarShapeDataStridedCu DataFormat format = input_format; - if (!(format == kNHWC || format == kHWC)) + if (!(format == kNHWC || format == kHWC || format == kNCHW || format == kCHW)) { LOG_ERROR("Invliad DataFormat " << format); return ErrorCode::INVALID_DATA_FORMAT; } + const bool isPlanar = (format == kNCHW || format == kCHW); DataType data_type = helpers::GetLegacyDataType(inData.uniqueFormat()); DataType out_data_type = helpers::GetLegacyDataType(outData.uniqueFormat()); @@ -250,6 +263,11 @@ ErrorCode HistogramEqVarShape::infer(const nvcv::ImageBatchVarShapeDataStridedCu LOG_ERROR("Invalid channel number " << channels); return ErrorCode::INVALID_DATA_SHAPE; } + if (isPlanar && channels == 2) + { + LOG_ERROR("2-channel planar HistogramEq is unsupported"); + return ErrorCode::INVALID_DATA_SHAPE; + } if (inData.numImages() != outData.numImages()) { @@ -257,9 +275,79 @@ ErrorCode HistogramEqVarShape::infer(const nvcv::ImageBatchVarShapeDataStridedCu return ErrorCode::INVALID_DATA_SHAPE; } + if (m_maxBatchSize <= 0 || batch > m_maxBatchSize) + { + LOG_ERROR("Invalid maximum batch size " << m_maxBatchSize << " for input batch " << batch); + return ErrorCode::INVALID_PARAMETER; + } + + if (batch > 65535) + { + LOG_ERROR("HistogramEq input exceeds the 65535 batch launch limit"); + return ErrorCode::INVALID_DATA_SHAPE; + } + + auto histo = nvcv::cuda::Tensor2DWrap(m_histoArray, (int)(256 * channels * sizeof(int))); + + checkCudaErrors(cudaMemsetAsync(m_histoArray, 0, m_sizeOfHisto, stream)); + + if (isPlanar) + { + cuda::ImageBatchVarShapeWrap dst(outData); + cuda::ImageBatchVarShapeWrap src(inData); + + { + //compute the histogram for each image in the batch into m_histoArray + int bsX = 32; //1024 ( 4 ch of 256 bins) + int bsY = 32; + + switch (channels) + { + case 1: + bsX = 16; // 256 (1 ch) + bsY = 16; + break; + case 3: + bsX = 32; // 768 (3 ch) + bsY = 24; + break; + default: + break; + } + + // each block is going to be 256bins * channels = threads + dim3 histBlockSize(bsX, bsY, 1); + dim3 histGridSize(divUp(inData.maxSize().w, histBlockSize.x), divUp(inData.maxSize().h, histBlockSize.y), + batch); + size_t sharedMemSize = 256 * channels * sizeof(int); + hist_kernel<<>>(src, histo, channels); + checkKernelErrors(); + } + //compute cfd + { + int bsX = 256; + int bsY = 1; + int bsZ = 1; + dim3 prefixSumBlockSize(bsX, bsY, bsZ); + dim3 prefixSumGridSize(channels, 1, batch); + prefix_sum_with_norm_kernel<<>>(histo, dst); + checkKernelErrors(); + } + //lookup + { + dim3 lookupBlockSize(32, 32, 1); + dim3 lookupGridSize(divUp(inData.maxSize().w, lookupBlockSize.x), + divUp(inData.maxSize().h, lookupBlockSize.y), batch); + lookup + <<>>(src, dst, histo, channels); + checkKernelErrors(); + } + + return ErrorCode::SUCCESS; + } + cuda::ImageBatchVarShapeWrapNHWC dst(outData, channels); cuda::ImageBatchVarShapeWrapNHWC src(inData, channels); - auto histo = nvcv::cuda::Tensor2DWrap(m_histoArray, (int)(256 * channels * sizeof(int))); { //compute the histogram for each image in the batch into m_histoArray @@ -289,7 +377,7 @@ ErrorCode HistogramEqVarShape::infer(const nvcv::ImageBatchVarShapeDataStridedCu dim3 histGridSize(divUp(inData.maxSize().w, histBlockSize.x), divUp(inData.maxSize().h, histBlockSize.y), batch); size_t sharedMemSize = 256 * channels * sizeof(int); - hist_kernel<<>>(src, histo, channels); + hist_kernel<<>>(src, histo, channels); checkKernelErrors(); } //compute cfd @@ -307,7 +395,8 @@ ErrorCode HistogramEqVarShape::infer(const nvcv::ImageBatchVarShapeDataStridedCu dim3 lookupBlockSize(32, 32, 1); dim3 lookupGridSize(divUp(inData.maxSize().w, lookupBlockSize.x), divUp(inData.maxSize().h, lookupBlockSize.y), batch); - lookup<<>>(src, dst, histo, channels); + lookup + <<>>(src, dst, histo, channels); checkKernelErrors(); } diff --git a/src/cvcuda/priv/legacy/inpaint.cu b/src/cvcuda/priv/legacy/inpaint.cu index d60b4ea26..2e339d1a4 100644 --- a/src/cvcuda/priv/legacy/inpaint.cu +++ b/src/cvcuda/priv/legacy/inpaint.cu @@ -1,4 +1,4 @@ -/* Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +/* Copyright (c) 2021-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * * SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES * SPDX-License-Identifier: Apache-2.0 @@ -22,7 +22,6 @@ #include "CvCudaLegacyHelpers.hpp" #include "CvCudaUtils.cuh" -#include "cub/cub.cuh" #include "inpaint_utils.cuh" #include "reduce_kernel_utils.cuh" @@ -34,11 +33,6 @@ using namespace nvcv::legacy::cuda_op; using namespace nvcv::cuda; -#define KNOWN 0 //known outside narrow band -#define BAND 1 //narrow band (known) -#define INSIDE 2 //unknown -#define CHANGE 3 //servise - #define BLOCK 32 #define BLOCK_S 16 #define REDUCE_GRID_SIZE 64 @@ -62,6 +56,19 @@ __global__ void copy_mask_data(MaskWrapper src, Ptr2dNHWC dst, int row_offset } } +template +__device__ __forceinline__ auto inpaint_out_ptr(OutWrapper out, int batch, int y, int x, int channel) + -> decltype(out.ptr(batch, y, x, channel)) +{ + return out.ptr(batch, y, x, channel); +} + +template +__device__ __forceinline__ T *inpaint_out_ptr(Ptr2dNCHW out, int batch, int y, int x, int channel) +{ + return out.ptr(batch, y, x, channel); +} + template __device__ void inpaint(Ptr2dNHWC f, Ptr2dNHWC t, OutWrapper out, int i, int j, int range, int ch) { @@ -130,34 +137,37 @@ __device__ void inpaint(Ptr2dNHWC f, Ptr2dNHWC t, OutWrapp r.y = (float)(i - k); r.x = (float)(j - l); - dst = (float)(1. / (VectorLength(r) * sqrt((double)VectorLength(r)))); - lev = (float)(1. / (1 + fabs(*t.ptr(batch_idx, k, l) - *t.ptr(batch_idx, i, j)))); + float len = VectorLength(r); + dst = 1.0f / (len * sqrtf(len)); + lev = 1.0f / (1.0f + fabsf(*t.ptr(batch_idx, k, l) - *t.ptr(batch_idx, i, j))); dir = VectorScalMult(r, gradT); - if (fabs(dir) <= 0.01) + // Preserve the Telea weighting floor: near-orthogonal directions get a tiny + // nonzero contribution instead of disappearing through floating-point noise. + if (fabsf(dir) <= 0.01f) dir = 0.000001f; - w = (float)fabs(dst * lev * dir); + w = fabsf(dst * lev * dir); if (*f.ptr(batch_idx, k, l + 1) != INSIDE) { if (*f.ptr(batch_idx, k, l - 1) != INSIDE) { - gradI.x = (float)((*out.ptr(batch_idx, km, lp + 1, color) - - *out.ptr(batch_idx, km, lm - 1, color))) + gradI.x = (float)((*inpaint_out_ptr(out, batch_idx, km, lp + 1, color) + - *inpaint_out_ptr(out, batch_idx, km, lm - 1, color))) * 2.0f; } else { - gradI.x = (float)((*out.ptr(batch_idx, km, lp + 1, color) - - *out.ptr(batch_idx, km, lm, color))); + gradI.x = (float)((*inpaint_out_ptr(out, batch_idx, km, lp + 1, color) + - *inpaint_out_ptr(out, batch_idx, km, lm, color))); } } else { if (*f.ptr(batch_idx, k, l - 1) != INSIDE) { - gradI.x = (float)((*out.ptr(batch_idx, km, lp, color) - - *out.ptr(batch_idx, km, lm - 1, color))); + gradI.x = (float)((*inpaint_out_ptr(out, batch_idx, km, lp, color) + - *inpaint_out_ptr(out, batch_idx, km, lm - 1, color))); } else { @@ -168,22 +178,22 @@ __device__ void inpaint(Ptr2dNHWC f, Ptr2dNHWC t, OutWrapp { if (*f.ptr(batch_idx, k - 1, l) != INSIDE) { - gradI.y = (float)((*out.ptr(batch_idx, kp + 1, lm, color) - - *out.ptr(batch_idx, km - 1, lm, color))) + gradI.y = (float)((*inpaint_out_ptr(out, batch_idx, kp + 1, lm, color) + - *inpaint_out_ptr(out, batch_idx, km - 1, lm, color))) * 2.0f; } else { - gradI.y = (float)((*out.ptr(batch_idx, kp + 1, lm, color) - - *out.ptr(batch_idx, km, lm, color))); + gradI.y = (float)((*inpaint_out_ptr(out, batch_idx, kp + 1, lm, color) + - *inpaint_out_ptr(out, batch_idx, km, lm, color))); } } else { if (*f.ptr(batch_idx, k - 1, l) != INSIDE) { - gradI.y = (float)((*out.ptr(batch_idx, kp, lm, color) - - *out.ptr(batch_idx, km - 1, lm, color))); + gradI.y = (float)((*inpaint_out_ptr(out, batch_idx, kp, lm, color) + - *inpaint_out_ptr(out, batch_idx, km - 1, lm, color))); } else { @@ -191,7 +201,7 @@ __device__ void inpaint(Ptr2dNHWC f, Ptr2dNHWC t, OutWrapp } } // float Iaorg = Ia, Jxorg = Jx, Jyorg = Jy, sorg = s; - Ia += (float)w * (float)(*out.ptr(batch_idx, km, lm, color)); + Ia += (float)w * (float)(*inpaint_out_ptr(out, batch_idx, km, lm, color)); Jx -= (float)w * (float)(gradI.x * r.x); Jy -= (float)w * (float)(gradI.y * r.y); s += w; @@ -199,9 +209,9 @@ __device__ void inpaint(Ptr2dNHWC f, Ptr2dNHWC t, OutWrapp } } } - sat = (float)((Ia / s + (Jx + Jy) / (sqrt(Jx * Jx + Jy * Jy) + 1.0e-20f) + 0.5f)); + sat = Ia / s + (Jx + Jy) / (sqrtf(Jx * Jx + Jy * Jy) + 1.0e-20f) + 0.5f; { - *out.ptr(batch_idx, i - 1, j - 1, color) = SaturateCast(sat); // nan + *inpaint_out_ptr(out, batch_idx, i - 1, j - 1, color) = SaturateCast(sat); // nan } } } @@ -445,11 +455,83 @@ inline int finish_flag_reduce(Ptr2dNHWC src_ptr, int *d_out, int return rst; } +inline bool IsPlanarFormat(DataFormat format) +{ + return format == kNCHW || format == kCHW; +} + +inline bool IsSupportedInpaintFormat(DataFormat format) +{ + return format == kNHWC || format == kHWC || IsPlanarFormat(format); +} + +inline void CopyTensorInputToOutput(const nvcv::TensorDataAccessStridedImagePlanar &inAccess, + const nvcv::TensorDataAccessStridedImagePlanar &outAccess, bool isPlanar, + cudaStream_t stream) +{ + for (uint32_t i = 0; i < inAccess.numSamples(); ++i) + { + if (isPlanar) + { + const int rowBytes = inAccess.numCols() * inAccess.colStride(); + for (int c = 0; c < inAccess.numChannels(); ++c) + { + void *inSampData = inAccess.sampleData(i) + c * inAccess.chStride(); + void *outSampData = outAccess.sampleData(i) + c * outAccess.chStride(); + + checkCudaErrors(cudaMemcpy2DAsync(outSampData, outAccess.rowStride(), inSampData, inAccess.rowStride(), + rowBytes, inAccess.numRows(), cudaMemcpyDeviceToDevice, stream)); + } + } + else + { + void *inSampData = inAccess.sampleData(i); + void *outSampData = outAccess.sampleData(i); + + checkCudaErrors(cudaMemcpy2DAsync(outSampData, outAccess.rowStride(), inSampData, inAccess.rowStride(), + inAccess.numCols() * inAccess.colStride(), inAccess.numRows(), + cudaMemcpyDeviceToDevice, stream)); + } + } +} + +template +ErrorCode RunTeleaInpaint(Ptr2dNHWC inpaint_mask, Ptr2dNHWC t, Ptr2dNHWC band, + const nvcv::TensorDataStridedCuda &outData, int range, int channel, bool isPlanar, + cudaStream_t stream) +{ + int iteration = 20; + dim3 block(BLOCK_S, BLOCK_S); + dim3 grid(divUp(inpaint_mask.rows, block.x), divUp(inpaint_mask.cols, block.y), inpaint_mask.batches); + + if (isPlanar) + { + auto outAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(outData); + NVCV_ASSERT(outAccess); + Ptr2dNCHW dst(*outAccess); + for (int i = 0; i < iteration; i++) + { + TeleaInpaintFMM<<>>(inpaint_mask, t, dst, range, band, channel); + } + } + else + { + auto dst = CreateTensorWrapNHWC(outData); + for (int i = 0; i < iteration; i++) + { + TeleaInpaintFMM<<>>(inpaint_mask, t, dst, range, band, channel); + /* icvTeleaInpaintFMM(mask,t,output_img,range,Heap); */ + } + } + + return ErrorCode::SUCCESS; +} + template ErrorCode inpaint_helper(const nvcv::TensorDataStridedCuda &inData, const nvcv::TensorDataStridedCuda &mask, const nvcv::TensorDataStridedCuda &outData, void *workspace, unsigned char *kernel_ptr, int range, bool &init_flag, int batch, int height, int width, int channel, int maxBatchSize, - cudaStream_t stream) + bool isPlanar, cudaStream_t stream) { dim3 blockSize(BLOCK, BLOCK / 4, 1); dim3 gridSize(divUp(width + 2, blockSize.x), divUp(height + 2, blockSize.y), batch); @@ -478,15 +560,7 @@ ErrorCode inpaint_helper(const nvcv::TensorDataStridedCuda &inData, const nvcv:: auto outAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(outData); NVCV_ASSERT(outAccess); - for (uint32_t i = 0; i < inAccess->numSamples(); ++i) - { - void *inSampData = inAccess->sampleData(i); - void *outSampData = outAccess->sampleData(i); - - checkCudaErrors(cudaMemcpy2DAsync(outSampData, outAccess->rowStride(), inSampData, inAccess->rowStride(), - inAccess->numCols() * inAccess->colStride(), inAccess->numRows(), - cudaMemcpyDeviceToDevice, stream)); - } + CopyTensorInputToOutput(*inAccess, *outAccess, isPlanar, stream); // step1 init mask @@ -518,8 +592,7 @@ ErrorCode inpaint_helper(const nvcv::TensorDataStridedCuda &inData, const nvcv:: checkCudaErrors(cudaMemsetAsync(f_ptr, KNOWN, sizeof(unsigned char) * batch * erows * ecols * 1, stream)); // cvSet(f,cvScalar(KNOWN,0,0,0)); - checkCudaErrors(cudaMemsetAsync(t_ptr, 1.0e6f, sizeof(float) * batch * erows * ecols * 1, - stream)); // cvSet(t,cvScalar(1.0e6f,0,0,0)); + fill_value(t, 1.0e6f, stream); // cvSet(t,cvScalar(1.0e6f,0,0,0)); // step3 init band @@ -548,21 +621,13 @@ ErrorCode inpaint_helper(const nvcv::TensorDataStridedCuda &inData, const nvcv:: // step5 FMM - int iteration = 20; - dim3 block(BLOCK_S, BLOCK_S); - dim3 grid(divUp(f.rows, block.x), divUp(f.cols, block.y), f.batches); - int flag = 1; + int flag = 1; if (outAccess->sampleStride() * batch <= nvcv::cuda::TypeTraits::max) { - auto dst = CreateTensorWrapNHWC(outData); while (flag) { - for (int i = 0; i < iteration; i++) - { - TeleaInpaintFMM<<>>(inpaint_mask, t, dst, range, band, channel); - /* icvTeleaInpaintFMM(mask,t,output_img,range,Heap); */ - } + RunTeleaInpaint(inpaint_mask, t, band, outData, range, channel, isPlanar, stream); flag = finish_flag_reduce(band, block_reduce_buffer1, block_reduce_buffer2, stream); } } @@ -585,23 +650,21 @@ Inpaint::Inpaint(DataShape max_input_shape, DataShape max_output_shape, int maxB , m_kernel_ptr(nullptr) , m_workspace(nullptr) { - cudaError_t err = cudaMalloc(&m_kernel_ptr, sizeof(unsigned char) * maxBatchSize * 3 * 3); + size_t kernelSize = ComputeInpaintKernelSize(maxBatchSize); + cudaError_t err = cudaMalloc(&m_kernel_ptr, kernelSize); if (err != cudaSuccess) { - LOG_ERROR("CUDA memory allocation error of size: " << sizeof(uchar) * maxBatchSize * 3 * 3); - throw std::runtime_error("CUDA memory allocation error!"); + LOG_ERROR("CUDA memory allocation error of size: " << kernelSize); + throw LegacyCudaAllocationError("CUDA memory allocation error!"); } - int erows = (maxShape.h + 2); - int ecols = (maxShape.w + 2); - size_t buffersize = sizeof(int) * (REDUCE_GRID_SIZE * maxBatchSize + 1) - + maxBatchSize * erows * ecols * 1 * (sizeof(float) + sizeof(unsigned char) * 3); - err = cudaMalloc(&m_workspace, buffersize); + size_t buffersize = ComputeInpaintWorkspaceSize(maxBatchSize, maxShape, REDUCE_GRID_SIZE); + err = cudaMalloc(&m_workspace, buffersize); if (err != cudaSuccess) { cudaFree(m_kernel_ptr); LOG_ERROR("CUDA memory allocation error of size: " << buffersize); - throw std::runtime_error("CUDA memory allocation error!"); + throw LegacyCudaAllocationError("CUDA memory allocation error!"); } } @@ -620,11 +683,13 @@ ErrorCode Inpaint::infer(const TensorDataStridedCuda &inData, const TensorDataSt { DataFormat in_format = GetLegacyDataFormat(inData.layout()); DataType in_data_type = GetLegacyDataType(inData.dtype()); - if (!(in_format == kNHWC || in_format == kHWC)) + if (!IsSupportedInpaintFormat(in_format)) { - LOG_ERROR("Invalid input DataFormat " << in_format << ", the valid DataFormats are: \"NHWC\", \"HWC\""); + LOG_ERROR("Invalid input DataFormat " << in_format + << ", the valid DataFormats are: \"NHWC\", \"HWC\", \"NCHW\", \"CHW\""); return ErrorCode::INVALID_DATA_FORMAT; } + const bool isPlanar = IsPlanarFormat(in_format); if (!(in_data_type == kCV_8U || in_data_type == kCV_32S || in_data_type == kCV_32F)) { @@ -641,12 +706,23 @@ ErrorCode Inpaint::infer(const TensorDataStridedCuda &inData, const TensorDataSt LOG_ERROR("Invalid input channel number " << in_channels); return ErrorCode::INVALID_DATA_SHAPE; } + if (isPlanar && in_channels == 2) + { + LOG_ERROR("2-channel planar Inpaint is unsupported"); + return ErrorCode::INVALID_DATA_SHAPE; + } DataFormat out_format = GetLegacyDataFormat(outData.layout()); DataType out_data_type = GetLegacyDataType(outData.dtype()); - if (!(out_format == kNHWC || out_format == kHWC)) + if (!IsSupportedInpaintFormat(out_format)) + { + LOG_ERROR("Invalid output DataFormat " << out_format + << ", the valid DataFormats are: \"NHWC\", \"HWC\", \"NCHW\", \"CHW\""); + return ErrorCode::INVALID_DATA_FORMAT; + } + if (IsPlanarFormat(out_format) != isPlanar) { - LOG_ERROR("Invalid output DataFormat " << out_format << ", the valid DataFormats are: \"NHWC\", \"HWC\""); + LOG_ERROR("Input and output must both be interleaved or both planar"); return ErrorCode::INVALID_DATA_FORMAT; } @@ -684,10 +760,10 @@ ErrorCode Inpaint::infer(const TensorDataStridedCuda &inData, const TensorDataSt typedef ErrorCode (*inpaint_t)(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &mask, const TensorDataStridedCuda &outData, void *workspace, unsigned char *kernel_ptr, int range, bool &init_flag, int batch, int height, int width, int channel, - int maxBatchSize, cudaStream_t stream); + int maxBatchSize, bool isPlanar, cudaStream_t stream); static const inpaint_t funcs[6] = { - inpaint_helper, inpaint_helper, 0, 0, inpaint_helper, inpaint_helper, + inpaint_helper, 0, 0, 0, inpaint_helper, inpaint_helper, }; int range = (int)std::round(inpaintRadius); @@ -695,7 +771,7 @@ ErrorCode Inpaint::infer(const TensorDataStridedCuda &inData, const TensorDataSt range = std::min(range, 100); return funcs[in_data_type](inData, masks, outData, m_workspace, m_kernel_ptr, range, m_init_dilate, inAccess->numSamples(), inAccess->numRows(), inAccess->numCols(), in_channels, - m_maxBatchSize, stream); + m_maxBatchSize, isPlanar, stream); } } // namespace nvcv::legacy::cuda_op diff --git a/src/cvcuda/priv/legacy/inpaint_utils.cuh b/src/cvcuda/priv/legacy/inpaint_utils.cuh index 83e8b70ad..997be307f 100644 --- a/src/cvcuda/priv/legacy/inpaint_utils.cuh +++ b/src/cvcuda/priv/legacy/inpaint_utils.cuh @@ -1,4 +1,4 @@ -/* Copyright (c) 2021-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +/* Copyright (c) 2021-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * * SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES * SPDX-License-Identifier: Apache-2.0 @@ -22,6 +22,9 @@ #define INPAINT_UTILS_CUH #include "CvCudaUtils.cuh" + +#include + using namespace nvcv::legacy::cuda_op; #define KNOWN 0 //known outside narrow band @@ -29,6 +32,44 @@ using namespace nvcv::legacy::cuda_op; #define INSIDE 2 //unknown #define CHANGE 3 //servise +inline size_t InpaintCheckedAdd(size_t a, size_t b) +{ + if (a > std::numeric_limits::max() - b) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Inpaint workspace size overflow"); + } + return a + b; +} + +inline size_t InpaintCheckedMul(size_t a, size_t b) +{ + if (b != 0 && a > std::numeric_limits::max() / b) + { + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Inpaint workspace size overflow"); + } + return a * b; +} + +inline size_t ComputeInpaintKernelSize(int maxBatchSize) +{ + return InpaintCheckedMul( + InpaintCheckedMul(InpaintCheckedMul(sizeof(unsigned char), static_cast(maxBatchSize)), 3), 3); +} + +inline size_t ComputeInpaintWorkspaceSize(int maxBatchSize, nvcv::Size2D maxShape, int reduceGridSize) +{ + size_t batch = static_cast(maxBatchSize); + size_t erows = InpaintCheckedAdd(static_cast(maxShape.h), 2); + size_t ecols = InpaintCheckedAdd(static_cast(maxShape.w), 2); + + size_t reduceSize + = InpaintCheckedMul(sizeof(int), InpaintCheckedAdd(InpaintCheckedMul(reduceGridSize, batch), 1)); + size_t imageSize = InpaintCheckedMul(InpaintCheckedMul(InpaintCheckedMul(batch, erows), ecols), + sizeof(float) + sizeof(unsigned char) * 3); + + return InpaintCheckedAdd(reduceSize, imageSize); +} + __inline__ __device__ float min4(float a, float b, float c, float d) { a = min(a, b); @@ -79,6 +120,30 @@ inline int init_dilate_kernel(unsigned char *cross_kernel, cudaStream_t stream) return 0; } +template +__global__ void fill_value_kernel(Ptr2dNHWC dst, T value) +{ + const int x = blockIdx.x * blockDim.x + threadIdx.x; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + if (x >= dst.cols || y >= dst.rows) + return; + + for (int c = 0; c < dst.ch; ++c) + { + *dst.ptr(batch_idx, y, x, c) = value; + } +} + +template +void fill_value(Ptr2dNHWC dst, T value, cudaStream_t stream) +{ + dim3 block(16, 16); + dim3 grid(divUp(dst.cols, block.x), divUp(dst.rows, block.y), dst.batches); + fill_value_kernel<<>>(dst, value); + checkKernelErrors(); +} + __global__ void deviceReducePoints(const int *g_in, int *g_out, const int N); #endif // INPAINT_UTILS_CUH diff --git a/src/cvcuda/priv/legacy/inpaint_var_shape.cu b/src/cvcuda/priv/legacy/inpaint_var_shape.cu index 2f29004d1..4280beefe 100644 --- a/src/cvcuda/priv/legacy/inpaint_var_shape.cu +++ b/src/cvcuda/priv/legacy/inpaint_var_shape.cu @@ -1,4 +1,4 @@ -/* Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +/* Copyright (c) 2021-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * * SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES * SPDX-License-Identifier: Apache-2.0 @@ -22,7 +22,6 @@ #include "CvCudaLegacyHelpers.hpp" #include "CvCudaUtils.cuh" -#include "cub/cub.cuh" #include "inpaint_utils.cuh" #include "reduce_kernel_utils.cuh" @@ -32,11 +31,6 @@ using namespace nvcv::legacy::cuda_op; using namespace nvcv::cuda; -#define KNOWN 0 //known outside narrow band -#define BAND 1 //narrow band (known) -#define INSIDE 2 //unknown -#define CHANGE 3 //servise - #define BLOCK 32 #define BLOCK_S 16 #define REDUCE_GRID_SIZE 64 @@ -61,14 +55,28 @@ __global__ void copy_mask_data(ImageBatchVarShapeWrapNHWC src, Ptr2dNHWC d } } +template +__device__ __forceinline__ auto inpaint_out_ptr(OutWrapper out, int batch, int y, int x, int channel) + -> decltype(out.ptr(batch, y, x, channel)) +{ + return out.ptr(batch, y, x, channel); +} + template -__device__ void inpaint(Ptr2dNHWC f, Ptr2dNHWC t, ImageBatchVarShapeWrapNHWC out, int i, int j, - int range) +__device__ __forceinline__ T *inpaint_out_ptr(nvcv::cuda::ImageBatchVarShapeWrap out, int batch, int y, int x, + int channel) +{ + return out.ptr(batch, channel, y, x); +} + +template +__device__ void inpaint(Ptr2dNHWC f, Ptr2dNHWC t, OutWrapper out, int i, int j, int range, + int channels) { const int batch_idx = get_batch_idx(); int rows = out.height(batch_idx) + 2, cols = out.width(batch_idx) + 2; - for (int color = 0; color < out.numChannels(); color++) + for (int color = 0; color < channels; color++) { float2 gradI, gradT, r; float Ia = 0, Jx = 0, Jy = 0, s = 1.0e-20f, w, dst, lev, dir, sat; @@ -143,22 +151,22 @@ __device__ void inpaint(Ptr2dNHWC f, Ptr2dNHWC t, ImageBat { if (*f.ptr(batch_idx, k, l - 1) != INSIDE) { - gradI.x = (float)((*out.ptr(batch_idx, km, lp + 1, color) - - *out.ptr(batch_idx, km, lm - 1, color))) + gradI.x = (float)((*inpaint_out_ptr(out, batch_idx, km, lp + 1, color) + - *inpaint_out_ptr(out, batch_idx, km, lm - 1, color))) * 2.0f; } else { - gradI.x = (float)((*out.ptr(batch_idx, km, lp + 1, color) - - *out.ptr(batch_idx, km, lm, color))); + gradI.x = (float)((*inpaint_out_ptr(out, batch_idx, km, lp + 1, color) + - *inpaint_out_ptr(out, batch_idx, km, lm, color))); } } else { if (*f.ptr(batch_idx, k, l - 1) != INSIDE) { - gradI.x = (float)((*out.ptr(batch_idx, km, lp, color) - - *out.ptr(batch_idx, km, lm - 1, color))); + gradI.x = (float)((*inpaint_out_ptr(out, batch_idx, km, lp, color) + - *inpaint_out_ptr(out, batch_idx, km, lm - 1, color))); } else { @@ -169,22 +177,22 @@ __device__ void inpaint(Ptr2dNHWC f, Ptr2dNHWC t, ImageBat { if (*f.ptr(batch_idx, k - 1, l) != INSIDE) { - gradI.y = (float)((*out.ptr(batch_idx, kp + 1, lm, color) - - *out.ptr(batch_idx, km - 1, lm, color))) + gradI.y = (float)((*inpaint_out_ptr(out, batch_idx, kp + 1, lm, color) + - *inpaint_out_ptr(out, batch_idx, km - 1, lm, color))) * 2.0f; } else { - gradI.y = (float)((*out.ptr(batch_idx, kp + 1, lm, color) - - *out.ptr(batch_idx, km, lm, color))); + gradI.y = (float)((*inpaint_out_ptr(out, batch_idx, kp + 1, lm, color) + - *inpaint_out_ptr(out, batch_idx, km, lm, color))); } } else { if (*f.ptr(batch_idx, k - 1, l) != INSIDE) { - gradI.y = (float)((*out.ptr(batch_idx, kp, lm, color) - - *out.ptr(batch_idx, km - 1, lm, color))); + gradI.y = (float)((*inpaint_out_ptr(out, batch_idx, kp, lm, color) + - *inpaint_out_ptr(out, batch_idx, km - 1, lm, color))); } else { @@ -192,7 +200,7 @@ __device__ void inpaint(Ptr2dNHWC f, Ptr2dNHWC t, ImageBat } } // float Iaorg = Ia, Jxorg = Jx, Jyorg = Jy, sorg = s; - Ia += (float)w * (float)(*out.ptr(batch_idx, km, lm, color)); + Ia += (float)w * (float)(*inpaint_out_ptr(out, batch_idx, km, lm, color)); Jx -= (float)w * (float)(gradI.x * r.x); Jy -= (float)w * (float)(gradI.y * r.y); s += w; @@ -202,14 +210,14 @@ __device__ void inpaint(Ptr2dNHWC f, Ptr2dNHWC t, ImageBat } sat = (float)((Ia / s + (Jx + Jy) / (sqrt(Jx * Jx + Jy * Jy) + 1.0e-20f) + 0.5f)); { - *out.ptr(batch_idx, i - 1, j - 1, color) = SaturateCast(sat); // nan + *inpaint_out_ptr(out, batch_idx, i - 1, j - 1, color) = SaturateCast(sat); // nan } } } -template -__global__ void TeleaInpaintFMM(Ptr2dNHWC f, Ptr2dNHWC t, ImageBatchVarShapeWrapNHWC out, - int range, Ptr2dNHWC band) +template +__global__ void TeleaInpaintFMM(Ptr2dNHWC f, Ptr2dNHWC t, OutWrapper out, int range, + Ptr2dNHWC band, int channels) { int i = 0, j = 0; float dist; @@ -254,7 +262,7 @@ __global__ void TeleaInpaintFMM(Ptr2dNHWC f, Ptr2dNHWC t, FastMarching_solve(i - 1, j, i, j + 1, f, t), FastMarching_solve(i + 1, j, i, j + 1, f, t)); *t.ptr(batch_idx, i, j) = dist; - inpaint(f, t, out, i, j, range); + inpaint(f, t, out, i, j, range, channels); *f.ptr(batch_idx, i, j) = BAND; *band.ptr(batch_idx, i, j) = 1; // non-zero @@ -456,18 +464,40 @@ inline int finish_flag_reduce(Ptr2dNHWC src_ptr, int *d_out, int return rst; } +inline bool IsPlanarFormat(DataFormat format) +{ + return format == kNCHW || format == kCHW; +} + +inline bool IsSupportedInpaintFormat(DataFormat format) +{ + return format == kNHWC || format == kHWC || IsPlanarFormat(format); +} + +inline void CopyImageToOutput(const nvcv::ImageDataStridedCuda &inimgdata, const nvcv::ImageDataStridedCuda &outimgdata, + cudaStream_t stream) +{ + for (int p = 0; p < inimgdata.numPlanes(); ++p) + { + const nvcv::ImagePlaneStrided &inplane = inimgdata.plane(p); + const nvcv::ImagePlaneStrided &outplane = outimgdata.plane(p); + const int rowBytes = inplane.width * inimgdata.format().planePixelStrideBytes(p); + checkCudaErrors(cudaMemcpy2DAsync(outplane.basePtr, outplane.rowStride, inplane.basePtr, inplane.rowStride, + rowBytes, inplane.height, cudaMemcpyDeviceToDevice, stream)); + } +} + template void inpaint_helper(const nvcv::ImageBatchVarShapeDataStridedCuda &inData, const nvcv::ImageBatchVarShapeDataStridedCuda &mask, const nvcv::ImageBatchVarShapeDataStridedCuda &outData, void *workspace, unsigned char *kernel_ptr, - int range, bool &init_flag, int channel, int maxBatchSize, cudaStream_t stream) + int range, bool &init_flag, int channel, int maxBatchSize, bool isPlanar, cudaStream_t stream) { nvcv::Size2D maxsize = inData.maxSize(); int batch = inData.numImages(); dim3 blockSize(BLOCK, BLOCK / 4, 1); dim3 gridSize(divUp(maxsize.w + 2, blockSize.x), divUp(maxsize.h + 2, blockSize.y), batch); - ImageBatchVarShapeWrapNHWC dst(outData, channel); // data type for mask is 8UC1 ImageBatchVarShapeWrapNHWC org_mask(mask, 1); @@ -509,8 +539,7 @@ void inpaint_helper(const nvcv::ImageBatchVarShapeDataStridedCuda &inData, checkCudaErrors(cudaMemsetAsync(f_ptr, KNOWN, sizeof(unsigned char) * batch * erows * ecols * 1, stream)); // cvSet(f,cvScalar(KNOWN,0,0,0)); - checkCudaErrors(cudaMemsetAsync(t_ptr, 1.0e6f, sizeof(float) * batch * erows * ecols * 1, - stream)); // cvSet(t,cvScalar(1.0e6f,0,0,0)); + fill_value(t, 1.0e6f, stream); // cvSet(t,cvScalar(1.0e6f,0,0,0)); checkCudaErrors(cudaMemsetAsync(band_ptr, KNOWN, sizeof(unsigned char) * batch * erows * ecols * 1, stream)); // step3 init band @@ -549,10 +578,25 @@ void inpaint_helper(const nvcv::ImageBatchVarShapeDataStridedCuda &inData, while (flag) { - for (int i = 0; i < iteration; i++) + if (isPlanar) { - TeleaInpaintFMM<<>>( - inpaint_mask, t, dst, range, band); // icvTeleaInpaintFMM(mask,t,output_img,range,Heap); + nvcv::cuda::ImageBatchVarShapeWrap dst(outData); + for (int i = 0; i < iteration; i++) + { + TeleaInpaintFMM<<>>( + inpaint_mask, t, dst, range, band, + channel); // icvTeleaInpaintFMM(mask,t,output_img,range,Heap); + } + } + else + { + ImageBatchVarShapeWrapNHWC dst(outData, channel); + for (int i = 0; i < iteration; i++) + { + TeleaInpaintFMM<<>>( + inpaint_mask, t, dst, range, band, + channel); // icvTeleaInpaintFMM(mask,t,output_img,range,Heap); + } } flag = finish_flag_reduce(band, block_reduce_buffer1, block_reduce_buffer2, org_mask, stream); } @@ -570,23 +614,21 @@ InpaintVarShape::InpaintVarShape(DataShape max_input_shape, DataShape max_output , m_kernel_ptr(nullptr) , m_workspace(nullptr) { - cudaError_t err = cudaMalloc(&m_kernel_ptr, sizeof(unsigned char) * maxBatchSize * 3 * 3); + size_t kernelSize = ComputeInpaintKernelSize(maxBatchSize); + cudaError_t err = cudaMalloc(&m_kernel_ptr, kernelSize); if (err != cudaSuccess) { - LOG_ERROR("CUDA memory allocation error of size: " << sizeof(uchar) * maxBatchSize * 3 * 3); - throw std::runtime_error("CUDA memory allocation error!"); + LOG_ERROR("CUDA memory allocation error of size: " << kernelSize); + throw LegacyCudaAllocationError("CUDA memory allocation error!"); } - int erows = (maxShape.h + 2); - int ecols = (maxShape.w + 2); - size_t buffersize = sizeof(int) * (REDUCE_GRID_SIZE * maxBatchSize + 1) - + maxBatchSize * erows * ecols * 1 * (sizeof(float) + sizeof(unsigned char) * 3); - err = cudaMalloc(&m_workspace, buffersize); + size_t buffersize = ComputeInpaintWorkspaceSize(maxBatchSize, maxShape, REDUCE_GRID_SIZE); + err = cudaMalloc(&m_workspace, buffersize); if (err != cudaSuccess) { cudaFree(m_kernel_ptr); LOG_ERROR("CUDA memory allocation error of size: " << buffersize); - throw std::runtime_error("CUDA memory allocation error!"); + throw LegacyCudaAllocationError("CUDA memory allocation error!"); } } @@ -608,20 +650,30 @@ ErrorCode InpaintVarShape::infer(const nvcv::ImageBatchVarShape &inBatc if (inData == nullptr) { LOG_ERROR("Input must be varshape image batch"); + return ErrorCode::INVALID_PARAMETER; } auto outData = outBatch.exportData(stream); if (outData == nullptr) { LOG_ERROR("Output must be varshape image batch"); + return ErrorCode::INVALID_PARAMETER; + } + + if (inData->numImages() != masks.numImages() || inData->numImages() != outData->numImages()) + { + LOG_ERROR("Input, mask, and output batches must have the same number of images"); + return ErrorCode::INVALID_DATA_SHAPE; } DataFormat in_format = helpers::GetLegacyDataFormat(*inData); DataType in_data_type = helpers::GetLegacyDataType(inData->uniqueFormat()); - if (!(in_format == kNHWC || in_format == kHWC)) + if (!IsSupportedInpaintFormat(in_format)) { - LOG_ERROR("Invalid input DataFormat " << in_format << ", the valid DataFormats are: \"NHWC\", \"HWC\""); + LOG_ERROR("Invalid input DataFormat " << in_format + << ", the valid DataFormats are: \"NHWC\", \"HWC\", \"NCHW\", \"CHW\""); return ErrorCode::INVALID_DATA_FORMAT; } + const bool isPlanar = IsPlanarFormat(in_format); if (!(in_data_type == kCV_8U || in_data_type == kCV_32S || in_data_type == kCV_32F)) { @@ -636,6 +688,11 @@ ErrorCode InpaintVarShape::infer(const nvcv::ImageBatchVarShape &inBatc LOG_ERROR("Invalid channel number " << in_channels); return ErrorCode::INVALID_DATA_SHAPE; } + if (isPlanar && in_channels == 2) + { + LOG_ERROR("2-channel planar Inpaint is unsupported"); + return ErrorCode::INVALID_DATA_SHAPE; + } DataFormat out_format = helpers::GetLegacyDataFormat(*outData); DataType out_data_type = helpers::GetLegacyDataType(outData->uniqueFormat()); @@ -674,33 +731,30 @@ ErrorCode InpaintVarShape::infer(const nvcv::ImageBatchVarShape &inBatc } //copy input to output - for (auto init = inBatch.begin(), outit = outBatch.begin(); init != inBatch.end(), outit != outBatch.end(); + for (auto init = inBatch.begin(), outit = outBatch.begin(); init != inBatch.end() && outit != outBatch.end(); ++init, ++outit) { - const Image &inimg = *init; - const Image &outimg = *outit; - auto inimgdata = inimg.exportData(); - auto outimgdata = outimg.exportData(); - const ImagePlaneStrided &inplane = inimgdata->plane(0); - const ImagePlaneStrided &outplane = outimgdata->plane(0); - checkCudaErrors(cudaMemcpy2DAsync(outplane.basePtr, outplane.rowStride, inplane.basePtr, inplane.rowStride, - inplane.rowStride, inplane.height, cudaMemcpyDeviceToDevice, stream)); + const Image &inimg = *init; + const Image &outimg = *outit; + auto inimgdata = inimg.exportData(); + auto outimgdata = outimg.exportData(); + CopyImageToOutput(*inimgdata, *outimgdata, stream); } typedef void (*inpaint_t)( const ImageBatchVarShapeDataStridedCuda &inData, const ImageBatchVarShapeDataStridedCuda &mask, const ImageBatchVarShapeDataStridedCuda &outData, void *workspace, unsigned char *kernel_ptr, int range, - bool &init_flag, int channel, int maxBatchSize, cudaStream_t stream); + bool &init_flag, int channel, int maxBatchSize, bool isPlanar, cudaStream_t stream); static const inpaint_t funcs[6] = { - inpaint_helper, inpaint_helper, 0, 0, inpaint_helper, inpaint_helper, + inpaint_helper, 0, 0, 0, inpaint_helper, inpaint_helper, }; int range = (int)std::round(inpaintRadius); range = std::max(range, 1); range = std::min(range, 100); funcs[in_data_type](*inData, masks, *outData, m_workspace, m_kernel_ptr, range, m_init_dilate, in_channels, - m_maxBatchSize, stream); + m_maxBatchSize, isPlanar, stream); return SUCCESS; } diff --git a/src/cvcuda/priv/legacy/joint_bilateral_filter.cu b/src/cvcuda/priv/legacy/joint_bilateral_filter.cu index 420a5d5ae..1714113c4 100644 --- a/src/cvcuda/priv/legacy/joint_bilateral_filter.cu +++ b/src/cvcuda/priv/legacy/joint_bilateral_filter.cu @@ -25,6 +25,8 @@ #include "CvCudaUtils.cuh" +#include + using namespace nvcv::legacy::cuda_op; using namespace nvcv::legacy::helpers; @@ -50,6 +52,192 @@ static __device__ __forceinline__ float norm1(const float4 &a) return cuda::abs(a.x) + cuda::abs(a.y) + cuda::abs(a.z) + cuda::abs(a.w); } +template +struct JointBilateralFilterPlanarTensorWrap +{ + const NVCVByte *srcBase; + const NVCVByte *srcColorBase; + NVCVByte *dstBase; + int64_t srcSampleStride; + int64_t srcChStride; + int64_t srcRowStride; + int64_t srcColStride; + int64_t srcColorSampleStride; + int64_t srcColorChStride; + int64_t srcColorRowStride; + int64_t srcColorColStride; + int64_t dstSampleStride; + int64_t dstChStride; + int64_t dstRowStride; + int64_t dstColStride; + + __device__ __forceinline__ float readSrc(int sample, int channel, int y, int x) const + { + const NVCVByte *ptr + = srcBase + sample * srcSampleStride + channel * srcChStride + y * srcRowStride + x * srcColStride; + return static_cast(*reinterpret_cast(ptr)); + } + + __device__ __forceinline__ float readColor(int sample, int channel, int y, int x) const + { + const NVCVByte *ptr = srcColorBase + sample * srcColorSampleStride + channel * srcColorChStride + + y * srcColorRowStride + x * srcColorColStride; + return static_cast(*reinterpret_cast(ptr)); + } + + __device__ __forceinline__ void write(int sample, int channel, int y, int x, T value) const + { + NVCVByte *ptr + = dstBase + sample * dstSampleStride + channel * dstChStride + y * dstRowStride + x * dstColStride; + *reinterpret_cast(ptr) = value; + } +}; + +template +__device__ __forceinline__ bool mapBorderCoordinate(int &x, int &y, int columns, int rows) +{ + if constexpr (B == NVCV_BORDER_CONSTANT) + { + return !cuda::IsOutside(x, columns) && !cuda::IsOutside(y, rows); + } + else + { + x = cuda::GetIndexWithBorder(x, columns); + y = cuda::GetIndexWithBorder(y, rows); + return true; + } +} + +template +__device__ __forceinline__ void JointBilateralFilterPlanarTile(JointBilateralFilterPlanarTensorWrap img, + int batch_idx, int colIdx, int rowIdx, int rows, + int columns, int radius, int squared_radius, + float color_coefficient, float space_coefficient) +{ + const int x[4] = {colIdx, colIdx + 1, colIdx, colIdx + 1}; + const int y[4] = {rowIdx, rowIdx, rowIdx + 1, rowIdx + 1}; + + bool valid[4]; + valid[0] = colIdx < columns && rowIdx < rows; + valid[1] = colIdx + 1 < columns && rowIdx < rows; + valid[2] = colIdx < columns && rowIdx + 1 < rows; + valid[3] = colIdx + 1 < columns && rowIdx + 1 < rows; + + if (!(valid[0] || valid[1] || valid[2] || valid[3])) + { + return; + } + + float centerColor[4][CHANNELS] = {}; + float numerator[4][CHANNELS] = {}; + float denominator[4] = {}; + +#pragma unroll + for (int p = 0; p < 4; ++p) + { + if (valid[p]) + { +#pragma unroll + for (int ch = 0; ch < CHANNELS; ++ch) + { + centerColor[p][ch] = img.readColor(batch_idx, ch, y[p], x[p]); + } + } + } + + for (int c = colIdx - radius; c < colIdx + radius + 2; c++) + { + for (int r = rowIdx - radius; r < rowIdx + radius + 2; r++) + { + const int dx0 = cuda::abs(c - colIdx); + const int dy0 = cuda::abs(r - rowIdx); + const int dx1 = cuda::abs(c - (colIdx + 1)); + const int dy1 = cuda::abs(r - (rowIdx + 1)); + const int squared_dis0 = dx0 * dx0 + dy0 * dy0; + const int squared_dis1 = dx1 * dx1 + dy0 * dy0; + const int squared_dis2 = dx0 * dx0 + dy1 * dy1; + const int squared_dis3 = dx1 * dx1 + dy1 * dy1; + + if (!(squared_dis0 <= squared_radius || squared_dis1 <= squared_radius || squared_dis2 <= squared_radius + || squared_dis3 <= squared_radius)) + { + continue; + } + + int srcX = c; + int srcY = r; + bool inside = mapBorderCoordinate(srcX, srcY, columns, rows); + + float curr[CHANNELS] = {}; + float currColor[CHANNELS] = {}; + if (inside) + { +#pragma unroll + for (int ch = 0; ch < CHANNELS; ++ch) + { + curr[ch] = img.readSrc(batch_idx, ch, srcY, srcX); + currColor[ch] = img.readColor(batch_idx, ch, srcY, srcX); + } + } + + const int squared_dis[4] = {squared_dis0, squared_dis1, squared_dis2, squared_dis3}; +#pragma unroll + for (int p = 0; p < 4; ++p) + { + if (valid[p] && squared_dis[p] <= squared_radius) + { + float one_norm_size = 0.f; +#pragma unroll + for (int ch = 0; ch < CHANNELS; ++ch) + { + one_norm_size += cuda::abs(currColor[ch] - centerColor[p][ch]); + } + + const float e_space = squared_dis[p] * space_coefficient; + const float e_color = one_norm_size * one_norm_size * color_coefficient; + const float weight = cuda::exp(e_space + e_color); + denominator[p] += weight; +#pragma unroll + for (int ch = 0; ch < CHANNELS; ++ch) + { + numerator[p][ch] += weight * curr[ch]; + } + } + } + } + } + +#pragma unroll + for (int p = 0; p < 4; ++p) + { + if (valid[p]) + { + const float den = denominator[p] != 0.f ? denominator[p] : 1.f; +#pragma unroll + for (int ch = 0; ch < CHANNELS; ++ch) + { + img.write(batch_idx, ch, y[p], x[p], cuda::SaturateCast(numerator[p][ch] / den)); + } + } + } +} + +template +__global__ void JointBilateralFilterPlanarKernel(JointBilateralFilterPlanarTensorWrap img, int radius, + float sigmaColor, float sigmaSpace, int rows, int columns) +{ + const int colIdx = (blockIdx.x * blockDim.x + threadIdx.x) * 2; + const int rowIdx = (blockIdx.y * blockDim.y + threadIdx.y) * 2; + const int batch_idx = blockIdx.z; + + const int squared_radius = radius * radius; + const float space_coefficient = -1 / (2 * sigmaSpace * sigmaSpace); + const float color_coefficient = -1 / (2 * sigmaColor * sigmaColor); + + JointBilateralFilterPlanarTile(img, batch_idx, colIdx, rowIdx, rows, columns, radius, + squared_radius, color_coefficient, space_coefficient); +} + template __global__ void JointBilateralFilterKernel(SrcWrapper src, SrcWrapper srcColor, DstWrapper dst, const int radius, const float sigmaColor, const float sigmaSpace, const int rows, @@ -172,8 +360,11 @@ ErrorCode JointBilateralFilterCaller(const TensorDataStridedCuda &inData, const int radius, float sigmaColor, float sigmaSpace, float borderValue, cudaStream_t stream) { - dim3 block(8, 8); - dim3 grid(divUp(columns, block.x * 2), divUp(rows, block.y * 2), batch); + using BT = cuda::BaseType; + constexpr int numElements = cuda::NumElements; + constexpr bool useWideBlock = !(sizeof(BT) == 4 && numElements > 1); + dim3 block(useWideBlock ? 32 : 8, useWideBlock ? 2 : 8); + dim3 grid(divUp(columns, block.x * 2), divUp(rows, block.y * 2), batch); auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(inData); NVCV_ASSERT(inAccess); @@ -205,6 +396,66 @@ ErrorCode JointBilateralFilterCaller(const TensorDataStridedCuda &inData, const return ErrorCode::SUCCESS; } +template +ErrorCode JointBilateralFilterPlanarCaller(const TensorDataStridedCuda &inData, + const TensorDataStridedCuda &inColorData, + const TensorDataStridedCuda &outData, + const nvcv::TensorDataAccessStridedImagePlanar &inAccess, + const nvcv::TensorDataAccessStridedImagePlanar &inColorAccess, + const nvcv::TensorDataAccessStridedImagePlanar &outAccess, const int batch, + int rows, int columns, int channels, int radius, float sigmaColor, + float sigmaSpace, cudaStream_t stream) +{ + dim3 block(32, 2); + dim3 grid(divUp(columns, block.x * 2), divUp(rows, block.y * 2), batch); + + JointBilateralFilterPlanarTensorWrap img{reinterpret_cast(inData.basePtr()), + reinterpret_cast(inColorData.basePtr()), + reinterpret_cast(outData.basePtr()), + inAccess.sampleStride(), + inAccess.chStride(), + inAccess.rowStride(), + inAccess.colStride(), + inColorAccess.sampleStride(), + inColorAccess.chStride(), + inColorAccess.rowStride(), + inColorAccess.colStride(), + outAccess.sampleStride(), + outAccess.chStride(), + outAccess.rowStride(), + outAccess.colStride()}; + +#ifdef CUDA_DEBUG_LOG + checkCudaErrors(cudaStreamSynchronize(stream)); + checkCudaErrors(cudaGetLastError()); +#endif + + switch (channels) + { + case 1: + JointBilateralFilterPlanarKernel + <<>>(img, radius, sigmaColor, sigmaSpace, rows, columns); + break; + case 3: + JointBilateralFilterPlanarKernel + <<>>(img, radius, sigmaColor, sigmaSpace, rows, columns); + break; + case 4: + JointBilateralFilterPlanarKernel + <<>>(img, radius, sigmaColor, sigmaSpace, rows, columns); + break; + default: + LOG_ERROR("Invalid planar channel number ch = " << channels); + return ErrorCode::INVALID_DATA_SHAPE; + } + +#ifdef CUDA_DEBUG_LOG + checkCudaErrors(cudaStreamSynchronize(stream)); + checkCudaErrors(cudaGetLastError()); +#endif + return ErrorCode::SUCCESS; +} + ErrorCode JointBilateralFilter::infer(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &inColorData, const TensorDataStridedCuda &outData, int d, float sigmaColor, float sigmaSpace, NVCVBorderType borderMode, cudaStream_t stream) @@ -241,13 +492,13 @@ ErrorCode JointBilateralFilter::infer(const TensorDataStridedCuda &inData, const return ErrorCode::INVALID_DATA_FORMAT; } - if ((input_format != kNHWC) && (input_format != kHWC)) + if (!(input_format == kNHWC || input_format == kHWC || input_format == kNCHW || input_format == kCHW)) { - LOG_ERROR("Invalid DataFormat both Input and Output must be kHWC or kNHWC"); + LOG_ERROR("Invalid DataFormat both Input and Output must be kHWC, kNHWC, kCHW, or kNCHW"); return ErrorCode::INVALID_DATA_FORMAT; } - // the (inputColor_format != kNHWC) && (inputColor_format != kHWC) check can be safely removed + const bool isPlanar = input_format == kNCHW || input_format == kCHW; if (!(borderMode == NVCV_BORDER_CONSTANT || borderMode == NVCV_BORDER_REPLICATE || borderMode == NVCV_BORDER_REFLECT || borderMode == NVCV_BORDER_WRAP || borderMode == NVCV_BORDER_REFLECT101)) @@ -293,6 +544,16 @@ ErrorCode JointBilateralFilter::infer(const TensorDataStridedCuda &inData, const { return ErrorCode::INVALID_DATA_FORMAT; } + auto inColorAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(inColorData); + if (!inColorAccess) + { + return ErrorCode::INVALID_DATA_FORMAT; + } + auto outAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(outData); + if (!outAccess) + { + return ErrorCode::INVALID_DATA_FORMAT; + } int batch = inAccess->numSamples(); int channels = inAccess->numChannels(); int rows = inAccess->numRows(); @@ -302,6 +563,11 @@ ErrorCode JointBilateralFilter::infer(const TensorDataStridedCuda &inData, const LOG_ERROR("Invalid channel number ch = " << channels); return ErrorCode::INVALID_DATA_SHAPE; } + if (isPlanar && channels == 2) + { + LOG_ERROR("Planar JointBilateralFilter does not support 2-channel images"); + return ErrorCode::INVALID_DATA_SHAPE; + } float borderValue = .0f; @@ -311,17 +577,14 @@ ErrorCode JointBilateralFilter::infer(const TensorDataStridedCuda &inData, const float sigmaSpace, float borderValue, cudaStream_t stream); // All templated functions instantiated here to remove one level of indirection that just hides the same lookup - // table in 5 parts + // table in 5 parts. The kCV_8S row is null because validation above rejects signed 8-bit input. static const joint_bilateral_filter_t funcs[5][6][4] = { { {JointBilateralFilterCaller, JointBilateralFilterCaller, JointBilateralFilterCaller, JointBilateralFilterCaller}, - {JointBilateralFilterCaller, - JointBilateralFilterCaller, - JointBilateralFilterCaller, - JointBilateralFilterCaller}, + {nullptr, nullptr, nullptr, nullptr}, {JointBilateralFilterCaller, JointBilateralFilterCaller, JointBilateralFilterCaller, @@ -344,10 +607,7 @@ ErrorCode JointBilateralFilter::infer(const TensorDataStridedCuda &inData, const JointBilateralFilterCaller, JointBilateralFilterCaller, JointBilateralFilterCaller}, - {JointBilateralFilterCaller, - JointBilateralFilterCaller, - JointBilateralFilterCaller, - JointBilateralFilterCaller}, + {nullptr, nullptr, nullptr, nullptr}, {JointBilateralFilterCaller, JointBilateralFilterCaller, JointBilateralFilterCaller, @@ -370,10 +630,7 @@ ErrorCode JointBilateralFilter::infer(const TensorDataStridedCuda &inData, const JointBilateralFilterCaller, JointBilateralFilterCaller, JointBilateralFilterCaller}, - {JointBilateralFilterCaller, - JointBilateralFilterCaller, - JointBilateralFilterCaller, - JointBilateralFilterCaller}, + {nullptr, nullptr, nullptr, nullptr}, {JointBilateralFilterCaller, JointBilateralFilterCaller, JointBilateralFilterCaller, @@ -395,8 +652,7 @@ ErrorCode JointBilateralFilter::infer(const TensorDataStridedCuda &inData, const {JointBilateralFilterCaller, JointBilateralFilterCaller, JointBilateralFilterCaller, JointBilateralFilterCaller}, - {JointBilateralFilterCaller, JointBilateralFilterCaller, - JointBilateralFilterCaller, JointBilateralFilterCaller}, + {nullptr, nullptr, nullptr, nullptr}, {JointBilateralFilterCaller, JointBilateralFilterCaller, JointBilateralFilterCaller, @@ -415,10 +671,7 @@ ErrorCode JointBilateralFilter::infer(const TensorDataStridedCuda &inData, const JointBilateralFilterCaller, JointBilateralFilterCaller, JointBilateralFilterCaller}, - {JointBilateralFilterCaller, - JointBilateralFilterCaller, - JointBilateralFilterCaller, - JointBilateralFilterCaller}, + {nullptr, nullptr, nullptr, nullptr}, {JointBilateralFilterCaller, JointBilateralFilterCaller, JointBilateralFilterCaller, @@ -437,6 +690,48 @@ ErrorCode JointBilateralFilter::infer(const TensorDataStridedCuda &inData, const JointBilateralFilterCaller}, }, }; + typedef ErrorCode (*joint_bilateral_filter_planar_t)( + const TensorDataStridedCuda &inData, const TensorDataStridedCuda &inColorData, + const TensorDataStridedCuda &outData, const nvcv::TensorDataAccessStridedImagePlanar &inAccess, + const nvcv::TensorDataAccessStridedImagePlanar &inColorAccess, + const nvcv::TensorDataAccessStridedImagePlanar &outAccess, int batch, int rows, int columns, int channels, + int radius, float sigmaColor, float sigmaSpace, cudaStream_t stream); + + static const joint_bilateral_filter_planar_t planarFuncs[5][6] = { + {JointBilateralFilterPlanarCaller, nullptr, + JointBilateralFilterPlanarCaller, + JointBilateralFilterPlanarCaller, + JointBilateralFilterPlanarCaller, + JointBilateralFilterPlanarCaller}, + {JointBilateralFilterPlanarCaller, nullptr, + JointBilateralFilterPlanarCaller, + JointBilateralFilterPlanarCaller, + JointBilateralFilterPlanarCaller, + JointBilateralFilterPlanarCaller}, + {JointBilateralFilterPlanarCaller, nullptr, + JointBilateralFilterPlanarCaller, + JointBilateralFilterPlanarCaller, + JointBilateralFilterPlanarCaller, + JointBilateralFilterPlanarCaller}, + {JointBilateralFilterPlanarCaller, nullptr, + JointBilateralFilterPlanarCaller, + JointBilateralFilterPlanarCaller, + JointBilateralFilterPlanarCaller, + JointBilateralFilterPlanarCaller}, + {JointBilateralFilterPlanarCaller, nullptr, + JointBilateralFilterPlanarCaller, + JointBilateralFilterPlanarCaller, + JointBilateralFilterPlanarCaller, + JointBilateralFilterPlanarCaller}, + }; + + if (isPlanar) + { + return planarFuncs[borderMode][data_type](inData, inColorData, outData, *inAccess, *inColorAccess, *outAccess, + batch, rows, columns, channels, radius, sigmaColor, sigmaSpace, + stream); + } + return funcs[borderMode][data_type][channels - 1](inData, inColorData, outData, batch, rows, columns, radius, sigmaColor, sigmaSpace, borderValue, stream); } diff --git a/src/cvcuda/priv/legacy/joint_bilateral_filter_var_shape.cu b/src/cvcuda/priv/legacy/joint_bilateral_filter_var_shape.cu index 950ddb599..f1cb2240f 100644 --- a/src/cvcuda/priv/legacy/joint_bilateral_filter_var_shape.cu +++ b/src/cvcuda/priv/legacy/joint_bilateral_filter_var_shape.cu @@ -25,6 +25,8 @@ #include "CvCudaUtils.cuh" +#include + using namespace nvcv::legacy::cuda_op; using namespace nvcv::legacy::helpers; @@ -50,6 +52,227 @@ static __device__ __forceinline__ float norm1(const float4 &a) return cuda::abs(a.x) + cuda::abs(a.y) + cuda::abs(a.z) + cuda::abs(a.w); } +template +__device__ __forceinline__ bool mapBorderCoordinate(int &x, int &y, int columns, int rows) +{ + if constexpr (B == NVCV_BORDER_CONSTANT) + { + return !cuda::IsOutside(x, columns) && !cuda::IsOutside(y, rows); + } + else + { + x = cuda::GetIndexWithBorder(x, columns); + y = cuda::GetIndexWithBorder(y, rows); + return true; + } +} + +template +struct JointBilateralFilterVarShapePlanarImageWrap +{ + const NVCVByte *srcBase[CHANNELS]; + const NVCVByte *srcColorBase[CHANNELS]; + NVCVByte *dstBase[CHANNELS]; + int srcRowStride[CHANNELS]; + int srcColorRowStride[CHANNELS]; + int dstRowStride[CHANNELS]; + + __device__ __forceinline__ float readSrc(int channel, int y, int x) const + { + const NVCVByte *ptr = srcBase[channel] + y * srcRowStride[channel] + x * sizeof(T); + return static_cast(*reinterpret_cast(ptr)); + } + + __device__ __forceinline__ float readColor(int channel, int y, int x) const + { + const NVCVByte *ptr = srcColorBase[channel] + y * srcColorRowStride[channel] + x * sizeof(T); + return static_cast(*reinterpret_cast(ptr)); + } + + __device__ __forceinline__ void write(int channel, int y, int x, T value) const + { + NVCVByte *ptr = dstBase[channel] + y * dstRowStride[channel] + x * sizeof(T); + *reinterpret_cast(ptr) = value; + } +}; + +template +__device__ __forceinline__ void JointBilateralFilterVarShapePlanarTile( + JointBilateralFilterVarShapePlanarImageWrap img, int colIdx, int rowIdx, int rows, int columns, + int radius, int squared_radius, float color_coefficient, float space_coefficient) +{ + const int x[4] = {colIdx, colIdx + 1, colIdx, colIdx + 1}; + const int y[4] = {rowIdx, rowIdx, rowIdx + 1, rowIdx + 1}; + + bool valid[4]; + valid[0] = colIdx < columns && rowIdx < rows; + valid[1] = colIdx + 1 < columns && rowIdx < rows; + valid[2] = colIdx < columns && rowIdx + 1 < rows; + valid[3] = colIdx + 1 < columns && rowIdx + 1 < rows; + + if (!(valid[0] || valid[1] || valid[2] || valid[3])) + { + return; + } + + float centerColor[4][CHANNELS] = {}; + float numerator[4][CHANNELS] = {}; + float denominator[4] = {}; + +#pragma unroll + for (int p = 0; p < 4; ++p) + { + if (valid[p]) + { +#pragma unroll + for (int ch = 0; ch < CHANNELS; ++ch) + { + centerColor[p][ch] = img.readColor(ch, y[p], x[p]); + } + } + } + + for (int c = colIdx - radius; c < colIdx + radius + 2; c++) + { + for (int r = rowIdx - radius; r < rowIdx + radius + 2; r++) + { + const int dx0 = cuda::abs(c - colIdx); + const int dy0 = cuda::abs(r - rowIdx); + const int dx1 = cuda::abs(c - (colIdx + 1)); + const int dy1 = cuda::abs(r - (rowIdx + 1)); + const int squared_dis0 = dx0 * dx0 + dy0 * dy0; + const int squared_dis1 = dx1 * dx1 + dy0 * dy0; + const int squared_dis2 = dx0 * dx0 + dy1 * dy1; + const int squared_dis3 = dx1 * dx1 + dy1 * dy1; + + if (!(squared_dis0 <= squared_radius || squared_dis1 <= squared_radius || squared_dis2 <= squared_radius + || squared_dis3 <= squared_radius)) + { + continue; + } + + float curr[CHANNELS] = {}; + float currColor[CHANNELS] = {}; + int srcX = c; + int srcY = r; + bool inside = mapBorderCoordinate(srcX, srcY, columns, rows); + if (inside) + { +#pragma unroll + for (int ch = 0; ch < CHANNELS; ++ch) + { + curr[ch] = img.readSrc(ch, srcY, srcX); + currColor[ch] = img.readColor(ch, srcY, srcX); + } + } + + const int squared_dis[4] = {squared_dis0, squared_dis1, squared_dis2, squared_dis3}; +#pragma unroll + for (int p = 0; p < 4; ++p) + { + if (valid[p] && squared_dis[p] <= squared_radius) + { + float one_norm_size = 0.f; +#pragma unroll + for (int ch = 0; ch < CHANNELS; ++ch) + { + one_norm_size += cuda::abs(currColor[ch] - centerColor[p][ch]); + } + + const float e_space = squared_dis[p] * space_coefficient; + const float e_color = one_norm_size * one_norm_size * color_coefficient; + const float weight = cuda::exp(e_space + e_color); + denominator[p] += weight; +#pragma unroll + for (int ch = 0; ch < CHANNELS; ++ch) + { + numerator[p][ch] += weight * curr[ch]; + } + } + } + } + } + +#pragma unroll + for (int p = 0; p < 4; ++p) + { + if (valid[p]) + { + const float den = denominator[p] != 0.f ? denominator[p] : 1.f; +#pragma unroll + for (int ch = 0; ch < CHANNELS; ++ch) + { + img.write(ch, y[p], x[p], nvcv::cuda::SaturateCast(numerator[p][ch] / den)); + } + } + } +} + +template +__global__ void JointBilateralFilterVarShapePlanarKernel(const SrcWrapper src, const SrcWrapper srcColor, + DstWrapper dst, const cuda::Tensor1DWrap inDiameter, + const cuda::Tensor1DWrap inSigmaColor, + const cuda::Tensor1DWrap inSigmaSpace) +{ + using T = typename DstWrapper::ValueType; + const int batch_idx = get_batch_idx(); + const int rows = dst.height(batch_idx); + const int columns = dst.width(batch_idx); + + float sigmaColor = inSigmaColor[batch_idx]; + if (sigmaColor <= 0) + { + sigmaColor = 1; + } + float sigmaSpace = inSigmaSpace[batch_idx]; + if (sigmaSpace <= 0) + { + sigmaSpace = 1; + } + + int radius; + int diameter = inDiameter[batch_idx]; + if (diameter <= 0) + { + radius = std::roundf(sigmaSpace * 1.5f); + } + else + { + radius = diameter / 2; + } + if (radius < 1) + { + radius = 1; + } + assert(radius < 10000); + + const int colIdx = (blockIdx.x * blockDim.x + threadIdx.x) * 2; + const int rowIdx = (blockIdx.y * blockDim.y + threadIdx.y) * 2; + + const int squared_radius = radius * radius; + const float space_coefficient = -1 / (2 * sigmaSpace * sigmaSpace); + const float color_coefficient = -1 / (2 * sigmaColor * sigmaColor); + + JointBilateralFilterVarShapePlanarImageWrap img{}; +#pragma unroll + for (int ch = 0; ch < CHANNELS; ++ch) + { + const NVCVImagePlaneStrided srcPlane = src.imageBatchWrap().plane(batch_idx, ch); + const NVCVImagePlaneStrided srcColorPlane = srcColor.imageBatchWrap().plane(batch_idx, ch); + const NVCVImagePlaneStrided dstPlane = dst.plane(batch_idx, ch); + + img.srcBase[ch] = srcPlane.basePtr; + img.srcColorBase[ch] = srcColorPlane.basePtr; + img.dstBase[ch] = dstPlane.basePtr; + img.srcRowStride[ch] = srcPlane.rowStride; + img.srcColorRowStride[ch] = srcColorPlane.rowStride; + img.dstRowStride[ch] = dstPlane.rowStride; + } + + JointBilateralFilterVarShapePlanarTile(img, colIdx, rowIdx, rows, columns, radius, squared_radius, + color_coefficient, space_coefficient); +} + template __global__ void JointBilateralFilterVarShapeKernel(const SrcWrapper src, const SrcWrapper srcColor, DstWrapper dst, const cuda::Tensor1DWrap inDiameter, @@ -209,9 +432,12 @@ void JointBilateralFilterVarShapeCaller(const ImageBatchVarShapeDataStridedCuda cuda::BorderVarShapeWrap srcColor(inColorData); cuda::ImageBatchVarShapeWrap dst(outData); - Size2D outMaxSize = outData.maxSize(); - dim3 block(8, 8); - dim3 grid(divUp(outMaxSize.w, block.x * 2), divUp(outMaxSize.h, block.y * 2), batch); + Size2D outMaxSize = outData.maxSize(); + using BT = cuda::BaseType; + constexpr int numElements = cuda::NumElements; + constexpr bool useWideBlock = !(sizeof(BT) == 4 && numElements > 1); + dim3 block(useWideBlock ? 32 : 8, useWideBlock ? 2 : 8); + dim3 grid(divUp(outMaxSize.w, block.x * 2), divUp(outMaxSize.h, block.y * 2), batch); #ifdef CUDA_DEBUG_LOG checkCudaErrors(cudaStreamSynchronize(stream)); @@ -227,6 +453,52 @@ void JointBilateralFilterVarShapeCaller(const ImageBatchVarShapeDataStridedCuda #endif } +template +void JointBilateralFilterVarShapePlanarCaller(const ImageBatchVarShapeDataStridedCuda &inData, + const ImageBatchVarShapeDataStridedCuda &inColorData, + const ImageBatchVarShapeDataStridedCuda &outData, int batch, int channels, + const cuda::Tensor1DWrap &inDiameter, + const cuda::Tensor1DWrap &inSigmaColor, + const cuda::Tensor1DWrap &inSigmaSpace, cudaStream_t stream) +{ + cuda::BorderVarShapeWrap src(inData); + cuda::BorderVarShapeWrap srcColor(inColorData); + cuda::ImageBatchVarShapeWrap dst(outData); + + Size2D outMaxSize = outData.maxSize(); + dim3 block(32, 2); + dim3 grid(divUp(outMaxSize.w, block.x * 2), divUp(outMaxSize.h, block.y * 2), batch); + +#ifdef CUDA_DEBUG_LOG + checkCudaErrors(cudaStreamSynchronize(stream)); + checkCudaErrors(cudaGetLastError()); +#endif + + switch (channels) + { + case 1: + JointBilateralFilterVarShapePlanarKernel + <<>>(src, srcColor, dst, inDiameter, inSigmaColor, inSigmaSpace); + break; + case 3: + JointBilateralFilterVarShapePlanarKernel + <<>>(src, srcColor, dst, inDiameter, inSigmaColor, inSigmaSpace); + break; + case 4: + JointBilateralFilterVarShapePlanarKernel + <<>>(src, srcColor, dst, inDiameter, inSigmaColor, inSigmaSpace); + break; + default: + assert(false && "invalid planar channel count"); + return; + } + +#ifdef CUDA_DEBUG_LOG + checkCudaErrors(cudaStreamSynchronize(stream)); + checkCudaErrors(cudaGetLastError()); +#endif +} + ErrorCode JointBilateralFilterVarShape::infer(const ImageBatchVarShapeDataStridedCuda &inData, const ImageBatchVarShapeDataStridedCuda &inColorData, const ImageBatchVarShapeDataStridedCuda &outData, @@ -265,12 +537,14 @@ ErrorCode JointBilateralFilterVarShape::infer(const ImageBatchVarShapeDataStride return ErrorCode::INVALID_DATA_FORMAT; } - if ((input_format != kNHWC) && (input_format != kHWC)) + if (!(input_format == kNHWC || input_format == kHWC || input_format == kNCHW || input_format == kCHW)) { - LOG_ERROR("Invalid DataFormat both Input and Output must be kHWC or kNHWC"); + LOG_ERROR("Invalid DataFormat both Input and Output must be kHWC, kNHWC, kCHW, or kNCHW"); return ErrorCode::INVALID_DATA_FORMAT; } + const bool isPlanar = input_format == kNCHW || input_format == kCHW; + if (inData.uniqueFormat() != outData.uniqueFormat()) { LOG_ERROR("Input and Output formats must be same input format = " @@ -337,6 +611,11 @@ ErrorCode JointBilateralFilterVarShape::infer(const ImageBatchVarShapeDataStride LOG_ERROR("Invalid channel number ch = " << channels); return ErrorCode::INVALID_DATA_SHAPE; } + if (isPlanar && channels == 2) + { + LOG_ERROR("Planar JointBilateralFilter does not support 2-channel images"); + return ErrorCode::INVALID_DATA_SHAPE; + } // Create Tensor wrappers for parameter arrays cuda::Tensor1DWrap inDiameter(diameterData); @@ -349,18 +628,21 @@ ErrorCode JointBilateralFilterVarShape::infer(const ImageBatchVarShapeDataStride const cuda::Tensor1DWrap &inSigmaColor, const cuda::Tensor1DWrap &inSigmaSpace, cudaStream_t stream); + typedef void (*joint_bilateral_filter_var_shape_planar_t)( + const ImageBatchVarShapeDataStridedCuda &inData, const ImageBatchVarShapeDataStridedCuda &inColorData, + const ImageBatchVarShapeDataStridedCuda &outData, int batch, int channels, + const cuda::Tensor1DWrap &inDiameter, const cuda::Tensor1DWrap &inSigmaColor, + const cuda::Tensor1DWrap &inSigmaSpace, cudaStream_t stream); + // All templated functions instantiated here to remove one level of indirection that just hides the same lookup - // table in 5 parts + // table in 5 parts. The kCV_8S row is null because validation above rejects signed 8-bit input. static const joint_bilateral_filter_var_shape_t funcs[5][6][4] = { { {JointBilateralFilterVarShapeCaller, JointBilateralFilterVarShapeCaller, JointBilateralFilterVarShapeCaller, JointBilateralFilterVarShapeCaller}, - {JointBilateralFilterVarShapeCaller, - JointBilateralFilterVarShapeCaller, - JointBilateralFilterVarShapeCaller, - JointBilateralFilterVarShapeCaller}, + {nullptr, nullptr, nullptr, nullptr}, {JointBilateralFilterVarShapeCaller, JointBilateralFilterVarShapeCaller, JointBilateralFilterVarShapeCaller, @@ -383,10 +665,7 @@ ErrorCode JointBilateralFilterVarShape::infer(const ImageBatchVarShapeDataStride JointBilateralFilterVarShapeCaller, JointBilateralFilterVarShapeCaller, JointBilateralFilterVarShapeCaller}, - {JointBilateralFilterVarShapeCaller, - JointBilateralFilterVarShapeCaller, - JointBilateralFilterVarShapeCaller, - JointBilateralFilterVarShapeCaller}, + {nullptr, nullptr, nullptr, nullptr}, {JointBilateralFilterVarShapeCaller, JointBilateralFilterVarShapeCaller, JointBilateralFilterVarShapeCaller, @@ -409,10 +688,7 @@ ErrorCode JointBilateralFilterVarShape::infer(const ImageBatchVarShapeDataStride JointBilateralFilterVarShapeCaller, JointBilateralFilterVarShapeCaller, JointBilateralFilterVarShapeCaller}, - {JointBilateralFilterVarShapeCaller, - JointBilateralFilterVarShapeCaller, - JointBilateralFilterVarShapeCaller, - JointBilateralFilterVarShapeCaller}, + {nullptr, nullptr, nullptr, nullptr}, {JointBilateralFilterVarShapeCaller, JointBilateralFilterVarShapeCaller, JointBilateralFilterVarShapeCaller, @@ -435,10 +711,7 @@ ErrorCode JointBilateralFilterVarShape::infer(const ImageBatchVarShapeDataStride JointBilateralFilterVarShapeCaller, JointBilateralFilterVarShapeCaller, JointBilateralFilterVarShapeCaller}, - {JointBilateralFilterVarShapeCaller, - JointBilateralFilterVarShapeCaller, - JointBilateralFilterVarShapeCaller, - JointBilateralFilterVarShapeCaller}, + {nullptr, nullptr, nullptr, nullptr}, {JointBilateralFilterVarShapeCaller, JointBilateralFilterVarShapeCaller, JointBilateralFilterVarShapeCaller, @@ -461,10 +734,7 @@ ErrorCode JointBilateralFilterVarShape::infer(const ImageBatchVarShapeDataStride JointBilateralFilterVarShapeCaller, JointBilateralFilterVarShapeCaller, JointBilateralFilterVarShapeCaller}, - {JointBilateralFilterVarShapeCaller, - JointBilateralFilterVarShapeCaller, - JointBilateralFilterVarShapeCaller, - JointBilateralFilterVarShapeCaller}, + {nullptr, nullptr, nullptr, nullptr}, {JointBilateralFilterVarShapeCaller, JointBilateralFilterVarShapeCaller, JointBilateralFilterVarShapeCaller, @@ -484,6 +754,41 @@ ErrorCode JointBilateralFilterVarShape::infer(const ImageBatchVarShapeDataStride }, }; + static const joint_bilateral_filter_var_shape_planar_t planarFuncs[5][6] = { + {JointBilateralFilterVarShapePlanarCaller, nullptr, + JointBilateralFilterVarShapePlanarCaller, + JointBilateralFilterVarShapePlanarCaller, + JointBilateralFilterVarShapePlanarCaller, + JointBilateralFilterVarShapePlanarCaller}, + {JointBilateralFilterVarShapePlanarCaller, nullptr, + JointBilateralFilterVarShapePlanarCaller, + JointBilateralFilterVarShapePlanarCaller, + JointBilateralFilterVarShapePlanarCaller, + JointBilateralFilterVarShapePlanarCaller}, + {JointBilateralFilterVarShapePlanarCaller, nullptr, + JointBilateralFilterVarShapePlanarCaller, + JointBilateralFilterVarShapePlanarCaller, + JointBilateralFilterVarShapePlanarCaller, + JointBilateralFilterVarShapePlanarCaller}, + {JointBilateralFilterVarShapePlanarCaller, nullptr, + JointBilateralFilterVarShapePlanarCaller, + JointBilateralFilterVarShapePlanarCaller, + JointBilateralFilterVarShapePlanarCaller, + JointBilateralFilterVarShapePlanarCaller}, + {JointBilateralFilterVarShapePlanarCaller, nullptr, + JointBilateralFilterVarShapePlanarCaller, + JointBilateralFilterVarShapePlanarCaller, + JointBilateralFilterVarShapePlanarCaller, + JointBilateralFilterVarShapePlanarCaller}, + }; + + if (isPlanar) + { + planarFuncs[borderMode][data_type](inData, inColorData, outData, batch, channels, inDiameter, inSigmaColor, + inSigmaSpace, stream); + return ErrorCode::SUCCESS; + } + funcs[borderMode][data_type][channels - 1](inData, inColorData, outData, batch, inDiameter, inSigmaColor, inSigmaSpace, stream); return ErrorCode::SUCCESS; diff --git a/src/cvcuda/priv/legacy/median_blur.cu b/src/cvcuda/priv/legacy/median_blur.cu index bf9b26b18..11cc1d255 100644 --- a/src/cvcuda/priv/legacy/median_blur.cu +++ b/src/cvcuda/priv/legacy/median_blur.cu @@ -18,11 +18,15 @@ * limitations under the License. */ +#include "../PlanarTensorView.hpp" #include "CvCudaLegacy.h" #include "CvCudaLegacyHelpers.hpp" #include "CvCudaUtils.cuh" +#include +#include + #define GENERAL_KERNEL_BLOCK 32 #define SMALL_KERNEL_BLOCK 16 @@ -325,6 +329,61 @@ __global__ void medianForSmallKernel(const Ptr2dNHWC src, Ptr2dNHWC dst, c } } +template +__device__ __forceinline__ T medianFromSortedWindow(T (&arr)[LENGTH]) +{ +#pragma unroll + for (int i = 0; i < LENGTH - 1; ++i) + { +#pragma unroll + for (int j = i + 1; j < LENGTH; ++j) + { + if (arr[j] < arr[i]) + { + T tmp = arr[i]; + arr[i] = arr[j]; + arr[j] = tmp; + } + } + } + + return arr[LENGTH / 2]; +} + +template +__global__ void medianForFixedSmallKernel(const Ptr2dNHWC src, Ptr2dNHWC dst) +{ + constexpr int kWidth = KWidth; + constexpr int kHeight = KHeight; + constexpr int length = KWidth * KHeight; + + int blockX = blockIdx.x * blockDim.x; + int blockY = blockIdx.y * blockDim.y; + int x = blockX + threadIdx.x; + int y = blockY + threadIdx.y; + int channel = blockIdx.z % dst.ch; + int batchIdx = blockIdx.z / dst.ch; + int h = src.rows, w = src.cols; + + if ((x < w && y < h)) + { + T arr[length]; +#pragma unroll + for (int i = 0; i < length; i++) + { + int gx = x - (kWidth / 2) + (i % kWidth); + int gy = y - (kHeight / 2) + (i / kWidth); + + gx = min(max(gx, 0), w - 1); + gy = min(max(gy, 0), h - 1); + + arr[i] = *src.ptr(batchIdx, gy, gx, channel); + } + + *dst.ptr(batchIdx, y, x, channel) = medianFromSortedWindow(arr); + } +} + #undef fetch_ #undef fetchAs1d @@ -339,20 +398,63 @@ void median(const nvcv::TensorDataAccessStridedImagePlanar &inData, checkCudaErrors(cudaStreamSynchronize(stream)); checkCudaErrors(cudaGetLastError()); #endif - long unsigned int sharedMemSize = SMALL_KERNEL_BLOCK * SMALL_KERNEL_BLOCK * kWidth * kHeight * sizeof(T); - if (sharedMemSize < 48 * 1024) + long unsigned int sharedMemSize = SMALL_KERNEL_BLOCK * SMALL_KERNEL_BLOCK * kWidth * kHeight * sizeof(T); + auto runDynamicKernel = [&] + { + if (sharedMemSize < 48 * 1024) + { + dim3 block(SMALL_KERNEL_BLOCK, SMALL_KERNEL_BLOCK); + dim3 grid(divUp(dst.cols, block.x), divUp(dst.rows, block.y), dst.ch * dst.batches); + medianForSmallKernel<<>>(src, dst, kWidth, kHeight); + checkKernelErrors(); + } + else + { + dim3 block(GENERAL_KERNEL_BLOCK, GENERAL_KERNEL_BLOCK); + dim3 grid(divUp(dst.cols, block.x), divUp(dst.rows, block.y), dst.ch * dst.batches); + median<<>>(src, dst, kWidth, kHeight); + checkKernelErrors(); + } + }; + + if (kWidth == 3 && kHeight == 3) { dim3 block(SMALL_KERNEL_BLOCK, SMALL_KERNEL_BLOCK); dim3 grid(divUp(dst.cols, block.x), divUp(dst.rows, block.y), dst.ch * dst.batches); - medianForSmallKernel<<>>(src, dst, kWidth, kHeight); + medianForFixedSmallKernel<<>>(src, dst); checkKernelErrors(); } + else if constexpr (std::is_same_v || std::is_same_v) + { + if (kWidth == 5 && kHeight == 5) + { + dim3 block(SMALL_KERNEL_BLOCK, SMALL_KERNEL_BLOCK); + dim3 grid(divUp(dst.cols, block.x), divUp(dst.rows, block.y), dst.ch * dst.batches); + medianForFixedSmallKernel<<>>(src, dst); + checkKernelErrors(); + } + else if constexpr (std::is_same_v) + { + if (kWidth == 7 && kHeight == 7) + { + dim3 block(SMALL_KERNEL_BLOCK, SMALL_KERNEL_BLOCK); + dim3 grid(divUp(dst.cols, block.x), divUp(dst.rows, block.y), dst.ch * dst.batches); + medianForFixedSmallKernel<<>>(src, dst); + checkKernelErrors(); + } + else + { + runDynamicKernel(); + } + } + else + { + runDynamicKernel(); + } + } else { - dim3 block(GENERAL_KERNEL_BLOCK, GENERAL_KERNEL_BLOCK); - dim3 grid(divUp(dst.cols, block.x), divUp(dst.rows, block.y), dst.ch * dst.batches); - median<<>>(src, dst, kWidth, kHeight); - checkKernelErrors(); + runDynamicKernel(); } #ifdef CUDA_DEBUG_LOG @@ -377,11 +479,13 @@ ErrorCode MedianBlur::infer(const TensorDataStridedCuda &inData, const TensorDat DataFormat format = input_format; - if (!(format == kNHWC || format == kHWC)) + if (!(format == kNHWC || format == kHWC || format == kNCHW || format == kCHW)) { - LOG_ERROR("Invalid input DataFormat " << format << ", the valid DataFormats are: \"NHWC\", \"HWC\""); + LOG_ERROR("Invalid input DataFormat " << format + << ", the valid DataFormats are: \"NHWC\", \"HWC\", \"NCHW\", \"CHW\""); return ErrorCode::INVALID_DATA_FORMAT; } + const bool isPlanar = (format == kNCHW || format == kCHW); auto inAccess = TensorDataAccessStridedImagePlanar::Create(inData); NVCV_ASSERT(inAccess); @@ -406,7 +510,7 @@ ErrorCode MedianBlur::infer(const TensorDataStridedCuda &inData, const TensorDat const int channels = input_shape.C; - if (channels > 4) + if (channels > 4 || channels == 2) { LOG_ERROR("Invalid channel number " << channels); return ErrorCode::INVALID_DATA_SHAPE; @@ -417,9 +521,32 @@ ErrorCode MedianBlur::infer(const TensorDataStridedCuda &inData, const TensorDat cudaStream_t stream); static const median_t funcs[6] = { - median, 0, median, 0, median, median, + median, 0, median, 0, 0, median, }; + + if (isPlanar) + { + const int64_t numSamples = inAccess->numSamples(); + if (numSamples * channels > 65535) + { + LOG_ERROR("Planar median blur requires numSamples * numChannels <= 65535 (CUDA grid-z limit)"); + return ErrorCode::INVALID_DATA_SHAPE; + } + + auto inView = cvcuda::priv::PlanarAsSingleChannelView(inData, *inAccess); + auto outView = cvcuda::priv::PlanarAsSingleChannelView(outData, *outAccess); + + auto inViewAccess = TensorDataAccessStridedImagePlanar::Create(inView); + NVCV_ASSERT(inViewAccess); + + auto outViewAccess = TensorDataAccessStridedImagePlanar::Create(outView); + NVCV_ASSERT(outViewAccess); + + funcs[data_type](*inViewAccess, *outViewAccess, ksize.w, ksize.h, stream); + return SUCCESS; + } + funcs[data_type](*inAccess, *outAccess, ksize.w, ksize.h, stream); return SUCCESS; } diff --git a/src/cvcuda/priv/legacy/median_blur_var_shape.cu b/src/cvcuda/priv/legacy/median_blur_var_shape.cu index 6bc55d8c1..7e86b16f8 100644 --- a/src/cvcuda/priv/legacy/median_blur_var_shape.cu +++ b/src/cvcuda/priv/legacy/median_blur_var_shape.cu @@ -23,6 +23,9 @@ #include "CvCudaUtils.cuh" +#include +#include + #define GENERAL_KERNEL_BLOCK 32 #define SMALL_KERNEL_BLOCK 16 @@ -31,6 +34,20 @@ using namespace nvcv::legacy::helpers; namespace nvcv::legacy::cuda_op { +template +__device__ __forceinline__ T *pixelPtr(const cuda::ImageBatchVarShapeWrapNHWC img, int batchIdx, int channel, int y, + int x) +{ + return img.ptr(batchIdx, y, x, channel); +} + +template +__device__ __forceinline__ T *pixelPtr(const cuda::ImageBatchVarShapeWrap img, int batchIdx, int channel, int y, + int x) +{ + return img.ptr(batchIdx, channel, y, x); +} + /** * This function fetches the pixel from the shared if possible. * Otherwise, the pixel is read from global memory. @@ -49,9 +66,9 @@ namespace nvcv::legacy::cuda_op { * @param gy the vertical index of the desired pixel in the image. * @return the pixel at given index */ -template -__device__ T fetch(T *shared, const cuda::ImageBatchVarShapeWrapNHWC src, int batchIdx, int h, int w, int c, - int sxOffset, int syOffset, int gx, int gy, int block_size) +template +__device__ T fetch(T *shared, const SrcWrapper src, int batchIdx, int h, int w, int c, int sxOffset, int syOffset, + int gx, int gy, int block_size) { // check for nvcv::BORDER_REPLICATE. if (gx < 0) @@ -73,7 +90,7 @@ __device__ T fetch(T *shared, const cuda::ImageBatchVarShapeWrapNHWC src, int // check if the desired pixel is not in shared memory. if (gy - syOffset < 0 || gy - syOffset >= blockDim.y || gx - sxOffset < 0 || gx - sxOffset >= blockDim.x) { - return *src.ptr(batchIdx, gy, gx, c); // fetch from global memory. + return *pixelPtr(src, batchIdx, c, gy, gx); // fetch from global memory. } else { @@ -89,9 +106,8 @@ __device__ T fetch(T *shared, const cuda::ImageBatchVarShapeWrapNHWC src, int * @param kWidth width of the kernel. * @param kHeight height of the kernel. */ -template -__global__ void median(const cuda::ImageBatchVarShapeWrapNHWC src, cuda::ImageBatchVarShapeWrapNHWC dst, - cuda::Tensor1DWrap ksize) +template +__global__ void median(const SrcWrapper src, DstWrapper dst, cuda::Tensor1DWrap ksize, const int channels) { #define fetch_(gx, gy, block_size) \ fetch(tails, src, batchIdx, h, w, channel, blockX, blockY, (gx), (gy), (block_size)) @@ -102,8 +118,8 @@ __global__ void median(const cuda::ImageBatchVarShapeWrapNHWC src, cuda::Imag int blockY = blockIdx.y * blockDim.y; int x = blockX + threadIdx.x; int y = blockY + threadIdx.y; - int channel = blockIdx.z % dst.numChannels(); - int batchIdx = blockIdx.z / dst.numChannels(); + int channel = blockIdx.z % channels; + int batchIdx = blockIdx.z / channels; int h = src.height(batchIdx), w = src.width(batchIdx); int2 kernelSize = ksize[batchIdx]; @@ -113,7 +129,7 @@ __global__ void median(const cuda::ImageBatchVarShapeWrapNHWC src, cuda::Imag __shared__ T tails[GENERAL_KERNEL_BLOCK * GENERAL_KERNEL_BLOCK]; if (x < w && y < h) { - tails[ty * GENERAL_KERNEL_BLOCK + tx] = *src.ptr(batchIdx, y, x, channel); + tails[ty * GENERAL_KERNEL_BLOCK + tx] = *pixelPtr(src, batchIdx, channel, y, x); } __syncthreads(); @@ -227,7 +243,7 @@ __global__ void median(const cuda::ImageBatchVarShapeWrapNHWC src, cuda::Imag numOfEq = 0; numOfGt = 0; } - *dst.ptr(batchIdx, y, x, channel) = pivot0; + *pixelPtr(dst, batchIdx, channel, y, x) = pivot0; } } @@ -277,17 +293,17 @@ __inline__ __device__ T placePivot(T *arr, int length) return pivot0; } -template -__global__ void medianForSmallKernel(const cuda::ImageBatchVarShapeWrapNHWC src, - cuda::ImageBatchVarShapeWrapNHWC dst, cuda::Tensor1DWrap ksize) +template +__global__ void medianForSmallKernel(const SrcWrapper src, DstWrapper dst, cuda::Tensor1DWrap ksize, + const int channels) { int tx = threadIdx.x, ty = threadIdx.y; int blockX = blockIdx.x * blockDim.x; int blockY = blockIdx.y * blockDim.y; int x = blockX + threadIdx.x; int y = blockY + threadIdx.y; - int channel = blockIdx.z % dst.numChannels(); - int batchIdx = blockIdx.z / dst.numChannels(); + int channel = blockIdx.z % channels; + int batchIdx = blockIdx.z / channels; int h = src.height(batchIdx), w = src.width(batchIdx); int2 kernelSize = ksize[batchIdx]; @@ -297,7 +313,7 @@ __global__ void medianForSmallKernel(const cuda::ImageBatchVarShapeWrapNHWC s __shared__ T tails[SMALL_KERNEL_BLOCK * SMALL_KERNEL_BLOCK]; if (x < w && y < h) { - tails[ty * SMALL_KERNEL_BLOCK + tx] = *src.ptr(batchIdx, y, x, channel); + tails[ty * SMALL_KERNEL_BLOCK + tx] = *pixelPtr(src, batchIdx, channel, y, x); } __syncthreads(); @@ -323,7 +339,7 @@ __global__ void medianForSmallKernel(const cuda::ImageBatchVarShapeWrapNHWC s } else if (k < (middle + numOfEq)) { - *dst.ptr(batchIdx, y, x, channel) = pivot; + *pixelPtr(dst, batchIdx, channel, y, x) = pivot; return; } else @@ -333,46 +349,143 @@ __global__ void medianForSmallKernel(const cuda::ImageBatchVarShapeWrapNHWC s arr = arr + middle + 1; } } - *dst.ptr(batchIdx, y, x, channel) = arr[0]; + *pixelPtr(dst, batchIdx, channel, y, x) = arr[0]; + } +} + +template +__device__ __forceinline__ T medianFromSortedWindow(T (&arr)[LENGTH]) +{ +#pragma unroll + for (int i = 0; i < LENGTH - 1; ++i) + { +#pragma unroll + for (int j = i + 1; j < LENGTH; ++j) + { + if (arr[j] < arr[i]) + { + T tmp = arr[i]; + arr[i] = arr[j]; + arr[j] = tmp; + } + } + } + + return arr[LENGTH / 2]; +} + +template +__global__ void medianForFixedSmallKernel(const SrcWrapper src, DstWrapper dst, const int channels) +{ + constexpr int kWidth = KWidth; + constexpr int kHeight = KHeight; + constexpr int length = KWidth * KHeight; + + int blockX = blockIdx.x * blockDim.x; + int blockY = blockIdx.y * blockDim.y; + int x = blockX + threadIdx.x; + int y = blockY + threadIdx.y; + int channel = blockIdx.z % channels; + int batchIdx = blockIdx.z / channels; + int h = src.height(batchIdx), w = src.width(batchIdx); + + if ((x < w && y < h)) + { + T arr[length]; +#pragma unroll + for (int i = 0; i < length; i++) + { + int gx = x - (kWidth / 2) + (i % kWidth); + int gy = y - (kHeight / 2) + (i / kWidth); + + gx = min(max(gx, 0), w - 1); + gy = min(max(gy, 0), h - 1); + + arr[i] = *pixelPtr(src, batchIdx, channel, gy, gx); + } + + *pixelPtr(dst, batchIdx, channel, y, x) = medianFromSortedWindow(arr); } } #undef fetch_ #undef fetchAs1d -template -void median(const ImageBatchVarShapeDataStridedCuda &in, const ImageBatchVarShapeDataStridedCuda &out, - const TensorDataStridedCuda &ksize, int maxKHeight, int maxKWidth, cudaStream_t stream) +template +void medianImpl(const ImageBatchVarShapeDataStridedCuda &out, const TensorDataStridedCuda &ksize, int maxKHeight, + int maxKWidth, const int channels, SrcWrapper src, DstWrapper dst, bool hasUniformKernelSize, + cudaStream_t stream) { Size2D outMaxSize = out.maxSize(); int maxWidth = outMaxSize.w; int maxHeight = outMaxSize.h; - int channels = in.uniqueFormat().numChannels(); - - cuda::ImageBatchVarShapeWrapNHWC src(in, channels); - cuda::ImageBatchVarShapeWrapNHWC dst(out, channels); + cuda::Tensor1DWrap ksizeWrap(ksize); #ifdef CUDA_DEBUG_LOG checkCudaErrors(cudaStreamSynchronize(stream)); checkCudaErrors(cudaGetLastError()); #endif - long unsigned int sharedMemSize = SMALL_KERNEL_BLOCK * SMALL_KERNEL_BLOCK * maxKWidth * maxKHeight * sizeof(T); - if (sharedMemSize < 48 * 1024) + long unsigned int sharedMemSize = SMALL_KERNEL_BLOCK * SMALL_KERNEL_BLOCK * maxKWidth * maxKHeight * sizeof(T); + auto runDynamicKernel = [&] + { + if (sharedMemSize < 48 * 1024) + { + dim3 block(SMALL_KERNEL_BLOCK, SMALL_KERNEL_BLOCK); + dim3 grid(divUp(maxWidth, block.x), divUp(maxHeight, block.y), channels * out.numImages()); + medianForSmallKernel<<>>(src, dst, ksizeWrap, channels); + checkKernelErrors(); + } + else + { + dim3 block(GENERAL_KERNEL_BLOCK, GENERAL_KERNEL_BLOCK); + dim3 grid(divUp(maxWidth, block.x), divUp(maxHeight, block.y), channels * out.numImages()); + median<<>>(src, dst, ksizeWrap, channels); + checkKernelErrors(); + } + }; + + if (hasUniformKernelSize && maxKWidth == 3 && maxKHeight == 3) { dim3 block(SMALL_KERNEL_BLOCK, SMALL_KERNEL_BLOCK); dim3 grid(divUp(maxWidth, block.x), divUp(maxHeight, block.y), channels * out.numImages()); - medianForSmallKernel<<>>(src, dst, ksize); + medianForFixedSmallKernel<<>>(src, dst, channels); checkKernelErrors(); } + else if constexpr (std::is_same_v || std::is_same_v) + { + if (hasUniformKernelSize && maxKWidth == 5 && maxKHeight == 5) + { + dim3 block(SMALL_KERNEL_BLOCK, SMALL_KERNEL_BLOCK); + dim3 grid(divUp(maxWidth, block.x), divUp(maxHeight, block.y), channels * out.numImages()); + medianForFixedSmallKernel<<>>(src, dst, channels); + checkKernelErrors(); + } + else if constexpr (std::is_same_v) + { + if (hasUniformKernelSize && maxKWidth == 7 && maxKHeight == 7) + { + dim3 block(SMALL_KERNEL_BLOCK, SMALL_KERNEL_BLOCK); + dim3 grid(divUp(maxWidth, block.x), divUp(maxHeight, block.y), channels * out.numImages()); + medianForFixedSmallKernel + <<>>(src, dst, channels); + checkKernelErrors(); + } + else + { + runDynamicKernel(); + } + } + else + { + runDynamicKernel(); + } + } else { - dim3 block(GENERAL_KERNEL_BLOCK, GENERAL_KERNEL_BLOCK); - dim3 grid(divUp(maxWidth, block.x), divUp(maxHeight, block.y), channels * out.numImages()); - median<<>>(src, dst, ksize); - checkKernelErrors(); + runDynamicKernel(); } #ifdef CUDA_DEBUG_LOG @@ -381,6 +494,32 @@ void median(const ImageBatchVarShapeDataStridedCuda &in, const ImageBatchVarShap #endif } +template +void median(const ImageBatchVarShapeDataStridedCuda &in, const ImageBatchVarShapeDataStridedCuda &out, + const TensorDataStridedCuda &ksize, int maxKHeight, int maxKWidth, bool hasUniformKernelSize, + cudaStream_t stream) +{ + int channels = in.uniqueFormat().numChannels(); + + cuda::ImageBatchVarShapeWrapNHWC src(in, channels); + cuda::ImageBatchVarShapeWrapNHWC dst(out, channels); + + medianImpl(out, ksize, maxKHeight, maxKWidth, channels, src, dst, hasUniformKernelSize, stream); +} + +template +void median_planar(const ImageBatchVarShapeDataStridedCuda &in, const ImageBatchVarShapeDataStridedCuda &out, + const TensorDataStridedCuda &ksize, int maxKHeight, int maxKWidth, bool hasUniformKernelSize, + cudaStream_t stream) +{ + int channels = in.uniqueFormat().numChannels(); + + cuda::ImageBatchVarShapeWrap src(in); + cuda::ImageBatchVarShapeWrap dst(out); + + medianImpl(out, ksize, maxKHeight, maxKWidth, channels, src, dst, hasUniformKernelSize, stream); +} + MedianBlurVarShape::MedianBlurVarShape(const int maxBatchSize) : CudaBaseOp() , m_maxBatchSize(maxBatchSize) @@ -432,11 +571,13 @@ ErrorCode MedianBlurVarShape::infer(const ImageBatchVarShapeDataStridedCuda &inD DataFormat format = input_format; - if (!(format == kNHWC || format == kHWC)) + if (!(format == kNHWC || format == kHWC || format == kNCHW || format == kCHW)) { - LOG_ERROR("Invalid input DataFormat " << format << ", the valid DataFormats are: \"NHWC\", \"HWC\""); + LOG_ERROR("Invalid input DataFormat " << format + << ", the valid DataFormats are: \"NHWC\", \"HWC\", \"NCHW\", \"CHW\""); return ErrorCode::INVALID_DATA_FORMAT; } + const bool isPlanar = (format == kNCHW || format == kCHW); DataType data_type = helpers::GetLegacyDataType(inData.uniqueFormat()); @@ -448,12 +589,18 @@ ErrorCode MedianBlurVarShape::infer(const ImageBatchVarShapeDataStridedCuda &inD int channels = inData.uniqueFormat().numChannels(); - if (channels > 4) + if (channels > 4 || channels == 2) { LOG_ERROR("Invalid channel number " << channels); return ErrorCode::INVALID_DATA_SHAPE; } + if (isPlanar && static_cast(inData.numImages()) * channels > 65535) + { + LOG_ERROR("Planar median blur requires numImages * channels <= 65535 (CUDA grid-z limit)"); + return ErrorCode::INVALID_DATA_SHAPE; + } + auto ksizeDataAccess = nvcv::TensorDataAccessStrided::Create(ksize); NVCV_ASSERT(ksizeDataAccess); @@ -464,7 +611,8 @@ ErrorCode MedianBlurVarShape::infer(const ImageBatchVarShapeDataStridedCuda &inD checkCudaErrors(cudaStreamSynchronize(stream)); // Compute the max width & height of kernel sizes - int maxKHeight = 0, maxKWidth = 0; + int maxKHeight = 0, maxKWidth = 0; + bool hasUniformKernelSize = true; for (int b = 0; b < inData.numImages(); b++) { int wIndex = b * 2; @@ -485,16 +633,35 @@ ErrorCode MedianBlurVarShape::infer(const ImageBatchVarShapeDataStridedCuda &inD { maxKHeight = m_kernelSizes[hIndex]; } + + if (b > 0 && (m_kernelSizes[wIndex] != m_kernelSizes[0] || m_kernelSizes[hIndex] != m_kernelSizes[1])) + { + hasUniformKernelSize = false; + } } typedef void (*median_t)(const ImageBatchVarShapeDataStridedCuda &in, const ImageBatchVarShapeDataStridedCuda &out, - const TensorDataStridedCuda &ksize, int maxKHeight, int maxKWidth, cudaStream_t stream); + const TensorDataStridedCuda &ksize, int maxKHeight, int maxKWidth, + bool hasUniformKernelSize, cudaStream_t stream); static const median_t funcs[6] = { - median, 0, median, 0, median, median, + median, 0, median, 0, 0, median, + + }; + + static const median_t planarFuncs[6] = { + median_planar, 0, median_planar, 0, 0, median_planar, }; - funcs[data_type](inData, outData, ksize, maxKHeight, maxKWidth, stream); + + if (isPlanar) + { + planarFuncs[data_type](inData, outData, ksize, maxKHeight, maxKWidth, hasUniformKernelSize, stream); + } + else + { + funcs[data_type](inData, outData, ksize, maxKHeight, maxKWidth, hasUniformKernelSize, stream); + } return SUCCESS; } diff --git a/src/cvcuda/priv/legacy/min_area_rect.cu b/src/cvcuda/priv/legacy/min_area_rect.cu index 354c85096..892915a81 100644 --- a/src/cvcuda/priv/legacy/min_area_rect.cu +++ b/src/cvcuda/priv/legacy/min_area_rect.cu @@ -1,4 +1,4 @@ -/* Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +/* Copyright (c) 2021-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * * SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES * SPDX-License-Identifier: Apache-2.0 @@ -75,24 +75,18 @@ void calculateRotateCoefCUDA(cuda::Tensor2DWrap rotateCoefBuf, const int template __global__ void calculateRotateArea(cuda::Tensor3DWrap inContourPointsData, cuda::Tensor3DWrap rotatedPointsTensor, cuda::Tensor2DWrap rotateCoeffs, - cuda::Tensor2DWrap numPointsInContourBuf) + cuda::Tensor2DWrap numPointsInContourBuf, int maxNumPointsInContour) { int pointIdx = blockIdx.x * blockDim.x + threadIdx.x; int contourIdx = blockIdx.y; - int angleIdx = blockIdx.z; - extern __shared__ float rotateCoeffs_sm[]; - rotateCoeffs_sm[2 * angleIdx] = *rotateCoeffs.ptr(angleIdx, 0); - rotateCoeffs_sm[2 * angleIdx + 1] = *rotateCoeffs.ptr(angleIdx, 1); - - __syncthreads(); - - if (pointIdx < *numPointsInContourBuf.ptr(0, contourIdx)) + if (pointIdx < min(*numPointsInContourBuf.ptr(0, contourIdx), maxNumPointsInContour)) { + int angleIdx = blockIdx.z; T px = *inContourPointsData.ptr(contourIdx, pointIdx, 0); T py = *inContourPointsData.ptr(contourIdx, pointIdx, 1); - float cos_coeff = rotateCoeffs_sm[2 * angleIdx]; - float sin_coeff = rotateCoeffs_sm[2 * angleIdx + 1]; + float cos_coeff = *rotateCoeffs.ptr(angleIdx, 0); + float sin_coeff = *rotateCoeffs.ptr(angleIdx, 1); int px_rot = (px * cos_coeff) - (py * sin_coeff); int py_rot = (px * sin_coeff) + (py * cos_coeff); //xmin @@ -126,19 +120,16 @@ __global__ void findMinAreaAndAngle(TensorWrapper rotatedPointsTensor, cuda::Ten // Determine the angle index from the thread's X-dimension index. int angleIdx = threadIdx.x; - // If the angle index exceeds the number of degrees, exit the thread to avoid out-of-bounds access. - if (angleIdx > numOfDegrees) - { - return; - } - // Determine the rectangle index from the block's X-dimension index. int rectIdx = blockIdx.x; extern __shared__ int areaAngleBuf_sm[]; - // Load area and angle data from the input tensor into shared memory for efficient access. - // rotatedPointsTensor is a 3D tensor with dimensions (rectIdx (N), angleIdx (0-90), 6). - areaAngleBuf_sm[2 * angleIdx] = *rotatedPointsTensor.ptr(rectIdx, angleIdx, 4); - areaAngleBuf_sm[(2 * angleIdx) + 1] = *rotatedPointsTensor.ptr(rectIdx, angleIdx, 5); + if (angleIdx < numOfDegrees) + { + // Load area and angle data from the input tensor into shared memory for efficient access. + // rotatedPointsTensor is a 3D tensor with dimensions (rectIdx (N), angleIdx (0-90), 6). + areaAngleBuf_sm[2 * angleIdx] = *rotatedPointsTensor.ptr(rectIdx, angleIdx, 4); + areaAngleBuf_sm[(2 * angleIdx) + 1] = *rotatedPointsTensor.ptr(rectIdx, angleIdx, 5); + } // Synchronize threads within a block to ensure shared memory is fully populated. __syncthreads(); @@ -168,7 +159,7 @@ __global__ void findMinAreaAndAngle(TensorWrapper rotatedPointsTensor, cuda::Ten __syncthreads(); // Handle the case when stride is odd, ensuring the first element is the minimum. - if (stride % 2 == 1 && areaAngleBuf_sm[0] > areaAngleBuf_sm[2 * (stride - 1)]) + if (angleIdx == 0 && stride % 2 == 1 && areaAngleBuf_sm[0] > areaAngleBuf_sm[2 * (stride - 1)]) { areaAngleBuf_sm[0] = areaAngleBuf_sm[2 * (stride - 1)]; areaAngleBuf_sm[1] = areaAngleBuf_sm[2 * (stride - 1) + 1]; @@ -179,7 +170,7 @@ __global__ void findMinAreaAndAngle(TensorWrapper rotatedPointsTensor, cuda::Ten } // Handle the case for odd number of degrees. - if (numOfDegrees % 2 == 1 && areaAngleBuf_sm[0] > areaAngleBuf_sm[2 * (numOfDegrees - 1)]) + if (angleIdx == 0 && numOfDegrees % 2 == 1 && areaAngleBuf_sm[0] > areaAngleBuf_sm[2 * (numOfDegrees - 1)]) { areaAngleBuf_sm[0] = areaAngleBuf_sm[2 * (numOfDegrees - 1)]; areaAngleBuf_sm[1] = areaAngleBuf_sm[2 * (numOfDegrees - 1) + 1]; @@ -222,7 +213,7 @@ __global__ void findMinAreaAndAngle(TensorWrapper rotatedPointsTensor, cuda::Ten } template -void minAreaRect(const TensorDataStridedCuda &inData, void *rotatedPointsDev, +void minAreaRect(const TensorDataStridedCuda &inData, int *rotatedPointsDev, const cuda::Tensor2DWrap rotateCoeffsData, const TensorDataStridedCuda &numPointsInContour, const TensorDataStridedCuda &outData, int contourBatch, int maxNumPointsInContour, cudaStream_t stream) { @@ -242,18 +233,19 @@ void minAreaRect(const TensorDataStridedCuda &inData, void *rotatedPointsDev, resetRotatedPointsBuf<<>>(rotatedPointsTensor, _MAX_ROTATE_DEGREES); checkKernelErrors(); - dim3 block2(256); - dim3 grid2(divUp(maxNumPointsInContour, block2.x), contourBatch, _MAX_ROTATE_DEGREES); - // Shared mem should be ((2 * (_MAX_ROTATE_DEGREES + 1))* sizeof(int) since there are 2 entries per angle and its inclusive (0-90) - size_t smem_size = (2 * (_MAX_ROTATE_DEGREES + 1)) * sizeof(int); - calculateRotateArea<<>>(inContourPointsData, rotatedPointsTensor, - rotateCoeffsData, pointsInContourData); + dim3 block2(maxNumPointsInContour <= 512 ? 128 : 256); + dim3 grid2(divUp(maxNumPointsInContour, block2.x), contourBatch, _MAX_ROTATE_DEGREES); + calculateRotateArea<<>>(inContourPointsData, rotatedPointsTensor, rotateCoeffsData, + pointsInContourData, maxNumPointsInContour); checkKernelErrors(); cudaStreamSynchronize(stream); + dim3 block3(128); dim3 grid3(contourBatch); - findMinAreaAndAngle<<>>(rotatedPointsTensor, outMinAreaRectData, + // The reduction stores an area and angle for every produced angle. + size_t smem_size = 2 * _MAX_ROTATE_DEGREES * sizeof(int); + findMinAreaAndAngle<<>>(rotatedPointsTensor, outMinAreaRectData, _MAX_ROTATE_DEGREES); checkKernelErrors(); } @@ -306,7 +298,7 @@ ErrorCode MinAreaRect::infer(const TensorDataStridedCuda &inData, const TensorDa cuda::Tensor2DWrap rotateCoeffsData(mRotateCoeffsBufDev, static_cast(2 * sizeof(float))); calculateRotateCoefCUDA(rotateCoeffsData, _MAX_ROTATE_DEGREES, stream); - typedef void (*minAreaRect_t)(const TensorDataStridedCuda &inData, void *rotatedPointsDev, + typedef void (*minAreaRect_t)(const TensorDataStridedCuda &inData, int *rotatedPointsDev, const cuda::Tensor2DWrap rotateCoeffsData, const TensorDataStridedCuda &numPointsInContour, const TensorDataStridedCuda &outData, int batch, int maxNumPointsInContour, cudaStream_t stream); diff --git a/src/cvcuda/priv/legacy/morphology.cu b/src/cvcuda/priv/legacy/morphology.cu index 4cd3c9fb7..68b49b69f 100644 --- a/src/cvcuda/priv/legacy/morphology.cu +++ b/src/cvcuda/priv/legacy/morphology.cu @@ -1,4 +1,4 @@ -/* Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +/* Copyright (c) 2021-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * * SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES * SPDX-License-Identifier: Apache-2.0 @@ -18,6 +18,7 @@ * limitations under the License. */ +#include "../PlanarTensorView.hpp" #include "CvCudaLegacy.h" #include "CvCudaLegacyHelpers.hpp" @@ -31,8 +32,90 @@ using namespace nvcv::legacy::cuda_op; namespace nvcv::legacy::cuda_op { -template -__global__ void dilate(SrcWrapper src, DstWrapper dst, Size2D dstSize, Size2D kernelSize, int2 kernelAnchor, T maxmin) +namespace { + +static bool IsPlanar(DataFormat format) +{ + return format == kNCHW || format == kCHW; +} + +} // namespace + +template +__device__ __forceinline__ PT dilate3x3Interior(const SrcWrapper &src, int batch, int y, int x, PT res) +{ + res = cuda::max(res, *src.ptr(batch, y - 1, x - 1)); + res = cuda::max(res, *src.ptr(batch, y - 1, x)); + res = cuda::max(res, *src.ptr(batch, y - 1, x + 1)); + res = cuda::max(res, *src.ptr(batch, y, x - 1)); + res = cuda::max(res, *src.ptr(batch, y, x)); + res = cuda::max(res, *src.ptr(batch, y, x + 1)); + res = cuda::max(res, *src.ptr(batch, y + 1, x - 1)); + res = cuda::max(res, *src.ptr(batch, y + 1, x)); + res = cuda::max(res, *src.ptr(batch, y + 1, x + 1)); + return res; +} + +template +__device__ __forceinline__ PT erode3x3Interior(const SrcWrapper &src, int batch, int y, int x, PT res) +{ + res = cuda::min(res, *src.ptr(batch, y - 1, x - 1)); + res = cuda::min(res, *src.ptr(batch, y - 1, x)); + res = cuda::min(res, *src.ptr(batch, y - 1, x + 1)); + res = cuda::min(res, *src.ptr(batch, y, x - 1)); + res = cuda::min(res, *src.ptr(batch, y, x)); + res = cuda::min(res, *src.ptr(batch, y, x + 1)); + res = cuda::min(res, *src.ptr(batch, y + 1, x - 1)); + res = cuda::min(res, *src.ptr(batch, y + 1, x)); + res = cuda::min(res, *src.ptr(batch, y + 1, x + 1)); + return res; +} + +__device__ __forceinline__ bool IsMorphInterior(Size2D kernelSize, int2 anchor, int x, int y, Size2D dstSize) +{ + return x >= anchor.x && y >= anchor.y && x + kernelSize.w - anchor.x <= dstSize.w + && y + kernelSize.h - anchor.y <= dstSize.h; +} + +template +__device__ __forceinline__ PT dilateInterior(const SrcWrapper &src, int batch, int y, int x, Size2D kernelSize, + int2 anchor, PT res) +{ + const int srcY0 = y - anchor.y; + const int srcX0 = x - anchor.x; + + for (int i = 0; i < kernelSize.h; ++i) + { + for (int j = 0; j < kernelSize.w; ++j) + { + res = cuda::max(res, *src.ptr(batch, srcY0 + i, srcX0 + j)); + } + } + + return res; +} + +template +__device__ __forceinline__ PT erodeInterior(const SrcWrapper &src, int batch, int y, int x, Size2D kernelSize, + int2 anchor, PT res) +{ + const int srcY0 = y - anchor.y; + const int srcX0 = x - anchor.x; + + for (int i = 0; i < kernelSize.h; ++i) + { + for (int j = 0; j < kernelSize.w; ++j) + { + res = cuda::min(res, *src.ptr(batch, srcY0 + i, srcX0 + j)); + } + } + + return res; +} + +template +__global__ void dilate(SrcWrapper src, RawSrcWrapper rawSrc, DstWrapper dst, Size2D dstSize, Size2D kernelSize, + int2 kernelAnchor, T maxmin) { using PT = typename DstWrapper::ValueType; PT res = cuda::SetAll(maxmin); @@ -44,22 +127,53 @@ __global__ void dilate(SrcWrapper src, DstWrapper dst, Size2D dstSize, Size2D ke if (x >= dstSize.w || y >= dstSize.h) return; - int3 coord{x, y, batch_idx}; - - for (int i = 0; i < kernelSize.h; ++i) + if (kernelSize.w == 3 && kernelSize.h == 3 && kernelAnchor.x == 1 && kernelAnchor.y == 1 && x > 0 && y > 0 + && x + 1 < dstSize.w && y + 1 < dstSize.h) { - coord.y = y - kernelAnchor.y + i; - for (int j = 0; j < kernelSize.w; ++j) + res = dilate3x3Interior(rawSrc, batch_idx, y, x, res); + } + else + { + if constexpr (UseGenericInterior) { - coord.x = x - kernelAnchor.x + j; - res = cuda::max(res, src[coord]); + if (IsMorphInterior(kernelSize, kernelAnchor, x, y, dstSize)) + { + res = dilateInterior(rawSrc, batch_idx, y, x, kernelSize, kernelAnchor, res); + } + else + { + int3 coord{x, y, batch_idx}; + for (int i = 0; i < kernelSize.h; ++i) + { + coord.y = y - kernelAnchor.y + i; + for (int j = 0; j < kernelSize.w; ++j) + { + coord.x = x - kernelAnchor.x + j; + res = cuda::max(res, src[coord]); + } + } + } + } + else + { + int3 coord{x, y, batch_idx}; + for (int i = 0; i < kernelSize.h; ++i) + { + coord.y = y - kernelAnchor.y + i; + for (int j = 0; j < kernelSize.w; ++j) + { + coord.x = x - kernelAnchor.x + j; + res = cuda::max(res, src[coord]); + } + } } } *dst.ptr(batch_idx, y, x) = cuda::SaturateCast(res); } -template -__global__ void erode(SrcWrapper src, DstWrapper dst, Size2D dstSize, Size2D kernelSize, int2 kernelAnchor, T maxmin) +template +__global__ void erode(SrcWrapper src, RawSrcWrapper rawSrc, DstWrapper dst, Size2D dstSize, Size2D kernelSize, + int2 kernelAnchor, T maxmin) { using PT = typename DstWrapper::ValueType; PT res = cuda::SetAll(maxmin); @@ -71,26 +185,59 @@ __global__ void erode(SrcWrapper src, DstWrapper dst, Size2D dstSize, Size2D ker if (x >= dstSize.w || y >= dstSize.h) return; - int3 coord{x, y, batch_idx}; - - for (int i = 0; i < kernelSize.h; ++i) + if (kernelSize.w == 3 && kernelSize.h == 3 && kernelAnchor.x == 1 && kernelAnchor.y == 1 && x > 0 && y > 0 + && x + 1 < dstSize.w && y + 1 < dstSize.h) { - coord.y = y - kernelAnchor.y + i; - for (int j = 0; j < kernelSize.w; ++j) + res = erode3x3Interior(rawSrc, batch_idx, y, x, res); + } + else + { + if constexpr (UseGenericInterior) + { + if (IsMorphInterior(kernelSize, kernelAnchor, x, y, dstSize)) + { + res = erodeInterior(rawSrc, batch_idx, y, x, kernelSize, kernelAnchor, res); + } + else + { + int3 coord{x, y, batch_idx}; + for (int i = 0; i < kernelSize.h; ++i) + { + coord.y = y - kernelAnchor.y + i; + for (int j = 0; j < kernelSize.w; ++j) + { + coord.x = x - kernelAnchor.x + j; + res = cuda::min(res, src[coord]); + } + } + } + } + else { - coord.x = x - kernelAnchor.x + j; - res = cuda::min(res, src[coord]); + int3 coord{x, y, batch_idx}; + for (int i = 0; i < kernelSize.h; ++i) + { + coord.y = y - kernelAnchor.y + i; + for (int j = 0; j < kernelSize.w; ++j) + { + coord.x = x - kernelAnchor.x + j; + res = cuda::min(res, src[coord]); + } + } } } *dst.ptr(batch_idx, y, x) = cuda::SaturateCast(res); } -template -void MorphFilter2DCaller(const SrcWrapper &src, const DstWrapper &dst, NVCVMorphologyType morph_type, Size2D kernelSize, - int2 kernelAnchor, BT maxmin, Size2D dstSize, int numSamples, cudaStream_t stream) +template +void MorphFilter2DCaller(const SrcWrapper &src, const RawSrcWrapper &rawSrc, const DstWrapper &dst, + NVCVMorphologyType morph_type, Size2D kernelSize, int2 kernelAnchor, BT maxmin, Size2D dstSize, + int numSamples, cudaStream_t stream) { - dim3 block(16, 16); - dim3 grid(divUp(dstSize.w, block.x), divUp(dstSize.h, block.y), numSamples); + dim3 block(16, 16); + dim3 grid(divUp(dstSize.w, block.x), divUp(dstSize.h, block.y), numSamples); + const bool useGenericInterior + = !(kernelSize.w == 3 && kernelSize.h == 3 && kernelAnchor.x == 1 && kernelAnchor.y == 1); #ifdef CUDA_DEBUG_LOG checkCudaErrors(cudaStreamSynchronize(stream)); checkCudaErrors(cudaGetLastError()); @@ -98,12 +245,26 @@ void MorphFilter2DCaller(const SrcWrapper &src, const DstWrapper &dst, NVCVMorph if (morph_type == NVCVMorphologyType::NVCV_ERODE) { - erode<<>>(src, dst, dstSize, kernelSize, kernelAnchor, maxmin); + if (useGenericInterior) + { + erode<<>>(src, rawSrc, dst, dstSize, kernelSize, kernelAnchor, maxmin); + } + else + { + erode<<>>(src, rawSrc, dst, dstSize, kernelSize, kernelAnchor, maxmin); + } checkKernelErrors(); } else if (morph_type == NVCVMorphologyType::NVCV_DILATE) { - dilate<<>>(src, dst, dstSize, kernelSize, kernelAnchor, maxmin); + if (useGenericInterior) + { + dilate<<>>(src, rawSrc, dst, dstSize, kernelSize, kernelAnchor, maxmin); + } + else + { + dilate<<>>(src, rawSrc, dst, dstSize, kernelSize, kernelAnchor, maxmin); + } checkKernelErrors(); } @@ -134,10 +295,11 @@ ErrorCode MorphFilter2DCaller(const TensorDataStridedCuda &inData, const TensorD auto inMaxStride = inAccess->sampleStride() * numSamples; if (std::max(inMaxStride, outMaxStride) <= cuda::TypeTraits::max) { - auto src = cuda::CreateBorderWrapNHW(inData, cuda::SetAll(val)); - auto dst = cuda::CreateTensorWrapNHW(outData); + auto src = cuda::CreateBorderWrapNHW(inData, cuda::SetAll(val)); + auto rawSrc = cuda::CreateTensorWrapNHW(inData); + auto dst = cuda::CreateTensorWrapNHW(outData); - MorphFilter2DCaller(src, dst, morph_type, kernelSize, kernelAnchor, val, dstSize, numSamples, stream); + MorphFilter2DCaller(src, rawSrc, dst, morph_type, kernelSize, kernelAnchor, val, dstSize, numSamples, stream); } else { @@ -166,8 +328,8 @@ ErrorCode MorphFilter2D(const TensorDataStridedCuda &inData, const TensorDataStr #undef NVCV_MORPH_CASE default: - NVCV_ASSERT("Unknown bortertype"); - break; + NVCV_ASSERT(!"Unknown border type"); + return ErrorCode::INVALID_PARAMETER; } return ErrorCode::SUCCESS; } @@ -196,11 +358,13 @@ ErrorCode Morphology::infer(const TensorDataStridedCuda &inData, const TensorDat } DataFormat format = input_format; - if (!(format == kNHWC || format == kHWC)) + if (!(format == kNHWC || format == kHWC || format == kNCHW || format == kCHW)) { - LOG_ERROR("Invalid input DataFormat " << format << ", the valid DataFormats are: \"NHWC\", \"HWC\""); + LOG_ERROR("Invalid input DataFormat " << format + << ", the valid DataFormats are: \"NHWC\", \"HWC\", \"NCHW\", \"CHW\""); return ErrorCode::INVALID_DATA_FORMAT; } + const bool isPlanar = IsPlanar(format); if (!(data_type == kCV_8U || data_type == kCV_16U || data_type == kCV_32F)) { @@ -226,6 +390,13 @@ ErrorCode Morphology::infer(const TensorDataStridedCuda &inData, const TensorDat return ErrorCode::INVALID_PARAMETER; } + if (isPlanar) + { + auto views = cvcuda::priv::PlanarSingleChannelViews(inData, outData); + NVCV_ASSERT(views); + return infer(views->first, views->second, morph_type, mask_size, anchor, noop, borderMode, stream); + } + Size2D mask_size_ = mask_size; if (mask_size.w == -1 || mask_size.h == -1) { diff --git a/src/cvcuda/priv/legacy/morphology_var_shape.cu b/src/cvcuda/priv/legacy/morphology_var_shape.cu index 159bc263d..a719994d3 100644 --- a/src/cvcuda/priv/legacy/morphology_var_shape.cu +++ b/src/cvcuda/priv/legacy/morphology_var_shape.cu @@ -1,4 +1,4 @@ -/* Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +/* Copyright (c) 2021-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * * SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES * SPDX-License-Identifier: Apache-2.0 @@ -26,11 +26,22 @@ #include #include +#include + using namespace nvcv::legacy::helpers; using namespace nvcv::legacy::cuda_op; namespace nvcv::legacy::cuda_op { +namespace { + +static bool IsPlanar(DataFormat format) +{ + return format == kNCHW || format == kCHW; +} + +} // namespace + __global__ void UpdateMasksAnchors(cuda::Tensor1DWrap masks, cuda::Tensor1DWrap anchors, int numImages, int iteration) { @@ -56,76 +67,571 @@ __global__ void UpdateMasksAnchors(cuda::Tensor1DWrap masks, cuda::Tensor1 anchors[coord] = anchor; } -template> -__global__ void dilate(const SrcWrapper src, DstWrapper dst, cuda::Tensor1DWrap kernelSizeArr, - cuda::Tensor1DWrap kernelAnchorArr, BT maxmin) +template +__device__ __forceinline__ PT dilate3x3Interior(const SrcWrapper &src, int batch, int y, int x, PT res) +{ + res = cuda::max(res, *src.ptr(batch, y - 1, x - 1)); + res = cuda::max(res, *src.ptr(batch, y - 1, x)); + res = cuda::max(res, *src.ptr(batch, y - 1, x + 1)); + res = cuda::max(res, *src.ptr(batch, y, x - 1)); + res = cuda::max(res, *src.ptr(batch, y, x)); + res = cuda::max(res, *src.ptr(batch, y, x + 1)); + res = cuda::max(res, *src.ptr(batch, y + 1, x - 1)); + res = cuda::max(res, *src.ptr(batch, y + 1, x)); + res = cuda::max(res, *src.ptr(batch, y + 1, x + 1)); + return res; +} + +template +__device__ __forceinline__ PT dilate3x3Interior(const SrcWrapper &src, int batch, int channel, int y, int x, PT res) +{ + res = cuda::max(res, *src.ptr(batch, channel, y - 1, x - 1)); + res = cuda::max(res, *src.ptr(batch, channel, y - 1, x)); + res = cuda::max(res, *src.ptr(batch, channel, y - 1, x + 1)); + res = cuda::max(res, *src.ptr(batch, channel, y, x - 1)); + res = cuda::max(res, *src.ptr(batch, channel, y, x)); + res = cuda::max(res, *src.ptr(batch, channel, y, x + 1)); + res = cuda::max(res, *src.ptr(batch, channel, y + 1, x - 1)); + res = cuda::max(res, *src.ptr(batch, channel, y + 1, x)); + res = cuda::max(res, *src.ptr(batch, channel, y + 1, x + 1)); + return res; +} + +template +__device__ __forceinline__ PT erode3x3Interior(const SrcWrapper &src, int batch, int y, int x, PT res) +{ + res = cuda::min(res, *src.ptr(batch, y - 1, x - 1)); + res = cuda::min(res, *src.ptr(batch, y - 1, x)); + res = cuda::min(res, *src.ptr(batch, y - 1, x + 1)); + res = cuda::min(res, *src.ptr(batch, y, x - 1)); + res = cuda::min(res, *src.ptr(batch, y, x)); + res = cuda::min(res, *src.ptr(batch, y, x + 1)); + res = cuda::min(res, *src.ptr(batch, y + 1, x - 1)); + res = cuda::min(res, *src.ptr(batch, y + 1, x)); + res = cuda::min(res, *src.ptr(batch, y + 1, x + 1)); + return res; +} + +template +__device__ __forceinline__ PT erode3x3Interior(const SrcWrapper &src, int batch, int channel, int y, int x, PT res) +{ + res = cuda::min(res, *src.ptr(batch, channel, y - 1, x - 1)); + res = cuda::min(res, *src.ptr(batch, channel, y - 1, x)); + res = cuda::min(res, *src.ptr(batch, channel, y - 1, x + 1)); + res = cuda::min(res, *src.ptr(batch, channel, y, x - 1)); + res = cuda::min(res, *src.ptr(batch, channel, y, x)); + res = cuda::min(res, *src.ptr(batch, channel, y, x + 1)); + res = cuda::min(res, *src.ptr(batch, channel, y + 1, x - 1)); + res = cuda::min(res, *src.ptr(batch, channel, y + 1, x)); + res = cuda::min(res, *src.ptr(batch, channel, y + 1, x + 1)); + return res; +} + +__device__ __forceinline__ bool IsMorphInterior(int2 kernelSize, int2 anchor, int x, int y, int width, int height) +{ + return x >= anchor.x && y >= anchor.y && x + kernelSize.x - anchor.x <= width + && y + kernelSize.y - anchor.y <= height; +} + +template +__device__ __forceinline__ PT dilateInterior(const SrcWrapper &src, int batch, int y, int x, int2 kernelSize, + int2 anchor, PT res) +{ + const int srcY0 = y - anchor.y; + const int srcX0 = x - anchor.x; + + for (int i = 0; i < kernelSize.y; ++i) + { + for (int j = 0; j < kernelSize.x; ++j) + { + res = cuda::max(res, *src.ptr(batch, srcY0 + i, srcX0 + j)); + } + } + + return res; +} + +template +__device__ __forceinline__ PT dilateInterior(const SrcWrapper &src, int batch, int channel, int y, int x, + int2 kernelSize, int2 anchor, PT res) +{ + const int srcY0 = y - anchor.y; + const int srcX0 = x - anchor.x; + + for (int i = 0; i < kernelSize.y; ++i) + { + for (int j = 0; j < kernelSize.x; ++j) + { + res = cuda::max(res, *src.ptr(batch, channel, srcY0 + i, srcX0 + j)); + } + } + + return res; +} + +template +__device__ __forceinline__ PT erodeInterior(const SrcWrapper &src, int batch, int y, int x, int2 kernelSize, + int2 anchor, PT res) +{ + const int srcY0 = y - anchor.y; + const int srcX0 = x - anchor.x; + + for (int i = 0; i < kernelSize.y; ++i) + { + for (int j = 0; j < kernelSize.x; ++j) + { + res = cuda::min(res, *src.ptr(batch, srcY0 + i, srcX0 + j)); + } + } + + return res; +} + +template +__device__ __forceinline__ PT erodeInterior(const SrcWrapper &src, int batch, int channel, int y, int x, + int2 kernelSize, int2 anchor, PT res) +{ + const int srcY0 = y - anchor.y; + const int srcX0 = x - anchor.x; + + for (int i = 0; i < kernelSize.y; ++i) + { + for (int j = 0; j < kernelSize.x; ++j) + { + res = cuda::min(res, *src.ptr(batch, channel, srcY0 + i, srcX0 + j)); + } + } + + return res; +} + +template> +__global__ void dilate(const SrcWrapper src, const RawSrcWrapper rawSrc, DstWrapper dst, + cuda::Tensor1DWrap kernelSizeArr, cuda::Tensor1DWrap kernelAnchorArr, BT maxmin) { D res = cuda::SetAll(maxmin); const int x = blockIdx.x * blockDim.x + threadIdx.x; const int y = blockIdx.y * blockDim.y + threadIdx.y; const int batch_idx = get_batch_idx(); - if (x >= dst.width(batch_idx) || y >= dst.height(batch_idx)) + const int width = dst.width(batch_idx); + const int height = dst.height(batch_idx); + + if (x >= width || y >= height) return; int2 kernelSize = kernelSizeArr[batch_idx]; int2 anchor = kernelAnchorArr[batch_idx]; - int3 srcCoord = {0, 0, batch_idx}; - - for (int i = 0; i < kernelSize.y; ++i) + if (kernelSize.x == 3 && kernelSize.y == 3 && anchor.x == 1 && anchor.y == 1 && x > 0 && y > 0 && x + 1 < width + && y + 1 < height) { - srcCoord.y = y - anchor.y + i; + res = dilate3x3Interior(rawSrc, batch_idx, y, x, res); + } + else + { + if constexpr (UseGenericInterior) + { + if (IsMorphInterior(kernelSize, anchor, x, y, width, height)) + { + res = dilateInterior(rawSrc, batch_idx, y, x, kernelSize, anchor, res); + } + else + { + int3 srcCoord = {0, 0, batch_idx}; + for (int i = 0; i < kernelSize.y; ++i) + { + srcCoord.y = y - anchor.y + i; + + for (int j = 0; j < kernelSize.x; ++j) + { + srcCoord.x = x - anchor.x + j; + + res = cuda::max(res, src[srcCoord]); + } + } + } + } + else + { + int3 srcCoord = {0, 0, batch_idx}; + for (int i = 0; i < kernelSize.y; ++i) + { + srcCoord.y = y - anchor.y + i; + + for (int j = 0; j < kernelSize.x; ++j) + { + srcCoord.x = x - anchor.x + j; + + res = cuda::max(res, src[srcCoord]); + } + } + } + } - for (int j = 0; j < kernelSize.x; ++j) + *dst.ptr(batch_idx, y, x) = cuda::SaturateCast(res); +} + +template> +__global__ void dilatePlanar(const SrcWrapper src, const RawSrcWrapper rawSrc, DstWrapper dst, + cuda::Tensor1DWrap kernelSizeArr, cuda::Tensor1DWrap kernelAnchorArr, + int channels, BT maxmin) +{ + D res = cuda::SetAll(maxmin); + const int x = blockIdx.x * blockDim.x + threadIdx.x; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const int channel = blockIdx.z % channels; + const int batch_idx = blockIdx.z / channels; + + const int width = dst.width(batch_idx, channel); + const int height = dst.height(batch_idx, channel); + + if (x >= width || y >= height) + return; + + int2 kernelSize = kernelSizeArr[batch_idx]; + int2 anchor = kernelAnchorArr[batch_idx]; + + if (kernelSize.x == 3 && kernelSize.y == 3 && anchor.x == 1 && anchor.y == 1 && x > 0 && y > 0 && x + 1 < width + && y + 1 < height) + { + res = dilate3x3Interior(rawSrc, batch_idx, channel, y, x, res); + } + else + { + if constexpr (UseGenericInterior) + { + if (IsMorphInterior(kernelSize, anchor, x, y, width, height)) + { + res = dilateInterior(rawSrc, batch_idx, channel, y, x, kernelSize, anchor, res); + } + else + { + int4 srcCoord = {0, 0, channel, batch_idx}; + for (int i = 0; i < kernelSize.y; ++i) + { + srcCoord.y = y - anchor.y + i; + + for (int j = 0; j < kernelSize.x; ++j) + { + srcCoord.x = x - anchor.x + j; + + res = cuda::max(res, src[srcCoord]); + } + } + } + } + else { - srcCoord.x = x - anchor.x + j; + int4 srcCoord = {0, 0, channel, batch_idx}; + for (int i = 0; i < kernelSize.y; ++i) + { + srcCoord.y = y - anchor.y + i; + + for (int j = 0; j < kernelSize.x; ++j) + { + srcCoord.x = x - anchor.x + j; + + res = cuda::max(res, src[srcCoord]); + } + } + } + } + + *dst.ptr(batch_idx, channel, y, x) = cuda::SaturateCast(res); +} + +template> +__global__ void erode(const SrcWrapper src, const RawSrcWrapper rawSrc, DstWrapper dst, + cuda::Tensor1DWrap kernelSizeArr, cuda::Tensor1DWrap kernelAnchorArr, BT maxmin) +{ + D res = cuda::SetAll(maxmin); + const int x = blockIdx.x * blockDim.x + threadIdx.x; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); - res = cuda::max(res, src[srcCoord]); + const int width = dst.width(batch_idx); + const int height = dst.height(batch_idx); + + if (x >= width || y >= height) + return; + + int2 kernelSize = kernelSizeArr[batch_idx]; + int2 anchor = kernelAnchorArr[batch_idx]; + + if (kernelSize.x == 3 && kernelSize.y == 3 && anchor.x == 1 && anchor.y == 1 && x > 0 && y > 0 && x + 1 < width + && y + 1 < height) + { + res = erode3x3Interior(rawSrc, batch_idx, y, x, res); + } + else + { + if constexpr (UseGenericInterior) + { + if (IsMorphInterior(kernelSize, anchor, x, y, width, height)) + { + res = erodeInterior(rawSrc, batch_idx, y, x, kernelSize, anchor, res); + } + else + { + int3 srcCoord = {0, 0, batch_idx}; + for (int i = 0; i < kernelSize.y; ++i) + { + srcCoord.y = y - anchor.y + i; + + for (int j = 0; j < kernelSize.x; ++j) + { + srcCoord.x = x - anchor.x + j; + + res = cuda::min(res, src[srcCoord]); + } + } + } + } + else + { + int3 srcCoord = {0, 0, batch_idx}; + for (int i = 0; i < kernelSize.y; ++i) + { + srcCoord.y = y - anchor.y + i; + + for (int j = 0; j < kernelSize.x; ++j) + { + srcCoord.x = x - anchor.x + j; + + res = cuda::min(res, src[srcCoord]); + } + } } } *dst.ptr(batch_idx, y, x) = cuda::SaturateCast(res); } -template> -__global__ void erode(const SrcWrapper src, DstWrapper dst, cuda::Tensor1DWrap kernelSizeArr, - cuda::Tensor1DWrap kernelAnchorArr, BT maxmin) +template> +__global__ void erodePlanar(const SrcWrapper src, const RawSrcWrapper rawSrc, DstWrapper dst, + cuda::Tensor1DWrap kernelSizeArr, cuda::Tensor1DWrap kernelAnchorArr, + int channels, BT maxmin) { D res = cuda::SetAll(maxmin); const int x = blockIdx.x * blockDim.x + threadIdx.x; const int y = blockIdx.y * blockDim.y + threadIdx.y; - const int batch_idx = get_batch_idx(); + const int channel = blockIdx.z % channels; + const int batch_idx = blockIdx.z / channels; + + const int width = dst.width(batch_idx, channel); + const int height = dst.height(batch_idx, channel); - if (x >= dst.width(batch_idx) || y >= dst.height(batch_idx)) + if (x >= width || y >= height) return; int2 kernelSize = kernelSizeArr[batch_idx]; int2 anchor = kernelAnchorArr[batch_idx]; - int3 srcCoord = {0, 0, batch_idx}; + if (kernelSize.x == 3 && kernelSize.y == 3 && anchor.x == 1 && anchor.y == 1 && x > 0 && y > 0 && x + 1 < width + && y + 1 < height) + { + res = erode3x3Interior(rawSrc, batch_idx, channel, y, x, res); + } + else + { + if constexpr (UseGenericInterior) + { + if (IsMorphInterior(kernelSize, anchor, x, y, width, height)) + { + res = erodeInterior(rawSrc, batch_idx, channel, y, x, kernelSize, anchor, res); + } + else + { + int4 srcCoord = {0, 0, channel, batch_idx}; + for (int i = 0; i < kernelSize.y; ++i) + { + srcCoord.y = y - anchor.y + i; + + for (int j = 0; j < kernelSize.x; ++j) + { + srcCoord.x = x - anchor.x + j; + + res = cuda::min(res, src[srcCoord]); + } + } + } + } + else + { + int4 srcCoord = {0, 0, channel, batch_idx}; + for (int i = 0; i < kernelSize.y; ++i) + { + srcCoord.y = y - anchor.y + i; + + for (int j = 0; j < kernelSize.x; ++j) + { + srcCoord.x = x - anchor.x + j; + + res = cuda::min(res, src[srcCoord]); + } + } + } + } - for (int i = 0; i < kernelSize.y; ++i) + *dst.ptr(batch_idx, channel, y, x) = cuda::SaturateCast(res); +} + +template> +__global__ void dilatePlanarChannels(const SrcWrapper src, const RawSrcWrapper rawSrc, DstWrapper dst, + cuda::Tensor1DWrap kernelSizeArr, cuda::Tensor1DWrap kernelAnchorArr, + int channels, BT maxmin) +{ + const int x = blockIdx.x * blockDim.x + threadIdx.x; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + + int2 kernelSize = kernelSizeArr[batch_idx]; + int2 anchor = kernelAnchorArr[batch_idx]; + + for (int channel = 0; channel < channels; ++channel) { - srcCoord.y = y - anchor.y + i; + const int width = dst.width(batch_idx, channel); + const int height = dst.height(batch_idx, channel); - for (int j = 0; j < kernelSize.x; ++j) - { - srcCoord.x = x - anchor.x + j; + if (x >= width || y >= height) + continue; - res = cuda::min(res, src[srcCoord]); + D res = cuda::SetAll(maxmin); + + if (kernelSize.x == 3 && kernelSize.y == 3 && anchor.x == 1 && anchor.y == 1 && x > 0 && y > 0 && x + 1 < width + && y + 1 < height) + { + res = dilate3x3Interior(rawSrc, batch_idx, channel, y, x, res); } + else + { + if constexpr (UseGenericInterior) + { + if (IsMorphInterior(kernelSize, anchor, x, y, width, height)) + { + res = dilateInterior(rawSrc, batch_idx, channel, y, x, kernelSize, anchor, res); + } + else + { + int4 srcCoord = {0, 0, channel, batch_idx}; + for (int i = 0; i < kernelSize.y; ++i) + { + srcCoord.y = y - anchor.y + i; + + for (int j = 0; j < kernelSize.x; ++j) + { + srcCoord.x = x - anchor.x + j; + + res = cuda::max(res, src[srcCoord]); + } + } + } + } + else + { + int4 srcCoord = {0, 0, channel, batch_idx}; + for (int i = 0; i < kernelSize.y; ++i) + { + srcCoord.y = y - anchor.y + i; + + for (int j = 0; j < kernelSize.x; ++j) + { + srcCoord.x = x - anchor.x + j; + + res = cuda::max(res, src[srcCoord]); + } + } + } + } + + *dst.ptr(batch_idx, channel, y, x) = cuda::SaturateCast(res); } +} - *dst.ptr(batch_idx, y, x) = cuda::SaturateCast(res); +template> +__global__ void erodePlanarChannels(const SrcWrapper src, const RawSrcWrapper rawSrc, DstWrapper dst, + cuda::Tensor1DWrap kernelSizeArr, cuda::Tensor1DWrap kernelAnchorArr, + int channels, BT maxmin) +{ + const int x = blockIdx.x * blockDim.x + threadIdx.x; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + + int2 kernelSize = kernelSizeArr[batch_idx]; + int2 anchor = kernelAnchorArr[batch_idx]; + + for (int channel = 0; channel < channels; ++channel) + { + const int width = dst.width(batch_idx, channel); + const int height = dst.height(batch_idx, channel); + + if (x >= width || y >= height) + continue; + + D res = cuda::SetAll(maxmin); + + if (kernelSize.x == 3 && kernelSize.y == 3 && anchor.x == 1 && anchor.y == 1 && x > 0 && y > 0 && x + 1 < width + && y + 1 < height) + { + res = erode3x3Interior(rawSrc, batch_idx, channel, y, x, res); + } + else + { + if constexpr (UseGenericInterior) + { + if (IsMorphInterior(kernelSize, anchor, x, y, width, height)) + { + res = erodeInterior(rawSrc, batch_idx, channel, y, x, kernelSize, anchor, res); + } + else + { + int4 srcCoord = {0, 0, channel, batch_idx}; + for (int i = 0; i < kernelSize.y; ++i) + { + srcCoord.y = y - anchor.y + i; + + for (int j = 0; j < kernelSize.x; ++j) + { + srcCoord.x = x - anchor.x + j; + + res = cuda::min(res, src[srcCoord]); + } + } + } + } + else + { + int4 srcCoord = {0, 0, channel, batch_idx}; + for (int i = 0; i < kernelSize.y; ++i) + { + srcCoord.y = y - anchor.y + i; + + for (int j = 0; j < kernelSize.x; ++j) + { + srcCoord.x = x - anchor.x + j; + + res = cuda::min(res, src[srcCoord]); + } + } + } + } + + *dst.ptr(batch_idx, channel, y, x) = cuda::SaturateCast(res); + } } template void MorphFilter2DCaller(const ImageBatchVarShapeDataStridedCuda &inData, const ImageBatchVarShapeDataStridedCuda &outData, const TensorDataStridedCuda &kMasks, - const TensorDataStridedCuda &kAnchors, NVCVMorphologyType morph_type, cudaStream_t stream) + const TensorDataStridedCuda &kAnchors, NVCVMorphologyType morph_type, + bool enableGenericInterior, cudaStream_t stream) { cuda::Tensor1DWrap kernelSizeTensor(kMasks); cuda::Tensor1DWrap kernelAnchorTensor(kAnchors); @@ -141,8 +647,9 @@ void MorphFilter2DCaller(const ImageBatchVarShapeDataStridedCuda &inData, BT val = (morph_type == NVCVMorphologyType::NVCV_DILATE) ? std::numeric_limits::min() : std::numeric_limits::max(); - cuda::BorderVarShapeWrap src(inData, cuda::SetAll(val)); - cuda::ImageBatchVarShapeWrap dst(outData); + cuda::BorderVarShapeWrap src(inData, cuda::SetAll(val)); + cuda::ImageBatchVarShapeWrap rawSrc(inData); + cuda::ImageBatchVarShapeWrap dst(outData); #ifdef CUDA_DEBUG_LOG checkCudaErrors(cudaStreamSynchronize(stream)); @@ -151,12 +658,124 @@ void MorphFilter2DCaller(const ImageBatchVarShapeDataStridedCuda &inData, if (morph_type == NVCVMorphologyType::NVCV_ERODE) { - erode<<>>(src, dst, kernelSizeTensor, kernelAnchorTensor, val); + if (enableGenericInterior) + { + erode<<>>(src, rawSrc, dst, kernelSizeTensor, kernelAnchorTensor, val); + } + else + { + erode<<>>(src, rawSrc, dst, kernelSizeTensor, kernelAnchorTensor, val); + } checkKernelErrors(); } else if (morph_type == NVCVMorphologyType::NVCV_DILATE) { - dilate<<>>(src, dst, kernelSizeTensor, kernelAnchorTensor, val); + if (enableGenericInterior) + { + dilate<<>>(src, rawSrc, dst, kernelSizeTensor, kernelAnchorTensor, val); + } + else + { + dilate<<>>(src, rawSrc, dst, kernelSizeTensor, kernelAnchorTensor, val); + } + checkKernelErrors(); + } + +#ifdef CUDA_DEBUG_LOG + checkCudaErrors(cudaStreamSynchronize(stream)); + checkCudaErrors(cudaGetLastError()); +#endif +} + +template +void MorphFilter2DCallerPlanar(const ImageBatchVarShapeDataStridedCuda &inData, + const ImageBatchVarShapeDataStridedCuda &outData, const TensorDataStridedCuda &kMasks, + const TensorDataStridedCuda &kAnchors, NVCVMorphologyType morph_type, int channels, + bool enableGenericInterior, cudaStream_t stream) +{ + cuda::Tensor1DWrap kernelSizeTensor(kMasks); + cuda::Tensor1DWrap kernelAnchorTensor(kAnchors); + + Size2D outMaxSize = outData.maxSize(); + int maxWidth = outMaxSize.w; + int maxHeight = outMaxSize.h; + + dim3 block(16, 16); + dim3 grid(divUp(maxWidth, block.x), divUp(maxHeight, block.y), channels * outData.numImages()); + + using BT = nvcv::cuda::BaseType; + BT val = (morph_type == NVCVMorphologyType::NVCV_DILATE) ? std::numeric_limits::min() + : std::numeric_limits::max(); + + cuda::BorderVarShapeWrap src(inData, cuda::SetAll(val)); + cuda::ImageBatchVarShapeWrap rawSrc(inData); + cuda::ImageBatchVarShapeWrap dst(outData); + +#ifdef CUDA_DEBUG_LOG + checkCudaErrors(cudaStreamSynchronize(stream)); + checkCudaErrors(cudaGetLastError()); +#endif + + if (morph_type == NVCVMorphologyType::NVCV_ERODE) + { + if constexpr (std::is_same_v) + { + dim3 channelGrid(divUp(maxWidth, block.x), divUp(maxHeight, block.y), outData.numImages()); + if (enableGenericInterior) + { + erodePlanarChannels<<>>(src, rawSrc, dst, kernelSizeTensor, + kernelAnchorTensor, channels, val); + } + else + { + erodePlanarChannels<<>>(src, rawSrc, dst, kernelSizeTensor, + kernelAnchorTensor, channels, val); + } + } + else + { + if (enableGenericInterior) + { + erodePlanar + <<>>(src, rawSrc, dst, kernelSizeTensor, kernelAnchorTensor, channels, val); + } + else + { + erodePlanar + <<>>(src, rawSrc, dst, kernelSizeTensor, kernelAnchorTensor, channels, val); + } + } + checkKernelErrors(); + } + else if (morph_type == NVCVMorphologyType::NVCV_DILATE) + { + if constexpr (std::is_same_v) + { + dim3 channelGrid(divUp(maxWidth, block.x), divUp(maxHeight, block.y), outData.numImages()); + if (enableGenericInterior) + { + dilatePlanarChannels<<>>(src, rawSrc, dst, kernelSizeTensor, + kernelAnchorTensor, channels, val); + } + else + { + dilatePlanarChannels<<>>(src, rawSrc, dst, kernelSizeTensor, + kernelAnchorTensor, channels, val); + } + } + else + { + if (enableGenericInterior) + { + dilatePlanar + <<>>(src, rawSrc, dst, kernelSizeTensor, kernelAnchorTensor, channels, val); + } + else + { + dilatePlanar + <<>>(src, rawSrc, dst, kernelSizeTensor, kernelAnchorTensor, channels, val); + } + } checkKernelErrors(); } @@ -169,24 +788,45 @@ void MorphFilter2DCaller(const ImageBatchVarShapeDataStridedCuda &inData, template void MorphFilter2D(const ImageBatchVarShapeDataStridedCuda &inData, const ImageBatchVarShapeDataStridedCuda &outData, const TensorDataStridedCuda &kMasks, const TensorDataStridedCuda &kAnchors, - NVCVMorphologyType morph_type, NVCVBorderType borderMode, cudaStream_t stream) + NVCVMorphologyType morph_type, NVCVBorderType borderMode, bool enableGenericInterior, + cudaStream_t stream) { typedef void (*func_t)(const ImageBatchVarShapeDataStridedCuda &inData, const ImageBatchVarShapeDataStridedCuda &outData, const TensorDataStridedCuda &kMasks, - const TensorDataStridedCuda &kAnchors, NVCVMorphologyType morph_type, cudaStream_t stream); + const TensorDataStridedCuda &kAnchors, NVCVMorphologyType morph_type, + bool enableGenericInterior, cudaStream_t stream); static const func_t funcs[] = {MorphFilter2DCaller, MorphFilter2DCaller, MorphFilter2DCaller, MorphFilter2DCaller, MorphFilter2DCaller}; - funcs[borderMode](inData, outData, kMasks, kAnchors, morph_type, stream); + funcs[borderMode](inData, outData, kMasks, kAnchors, morph_type, enableGenericInterior, stream); +} + +template +void MorphFilter2DPlanar(const ImageBatchVarShapeDataStridedCuda &inData, + const ImageBatchVarShapeDataStridedCuda &outData, const TensorDataStridedCuda &kMasks, + const TensorDataStridedCuda &kAnchors, NVCVMorphologyType morph_type, + NVCVBorderType borderMode, int channels, bool enableGenericInterior, cudaStream_t stream) +{ + typedef void (*func_t)(const ImageBatchVarShapeDataStridedCuda &inData, + const ImageBatchVarShapeDataStridedCuda &outData, const TensorDataStridedCuda &kMasks, + const TensorDataStridedCuda &kAnchors, NVCVMorphologyType morph_type, int channels, + bool enableGenericInterior, cudaStream_t stream); + + static const func_t funcs[] + = {MorphFilter2DCallerPlanar, MorphFilter2DCallerPlanar, + MorphFilter2DCallerPlanar, MorphFilter2DCallerPlanar, + MorphFilter2DCallerPlanar}; + + funcs[borderMode](inData, outData, kMasks, kAnchors, morph_type, channels, enableGenericInterior, stream); } ErrorCode MorphologyVarShape::infer(const nvcv::ImageBatchVarShape &inBatch, const nvcv::ImageBatchVarShape &outBatch, NVCVMorphologyType morph_type, const TensorDataStridedCuda &masks, const TensorDataStridedCuda &anchors, bool noop, NVCVBorderType borderMode, - cudaStream_t stream) + bool enableGenericInterior, cudaStream_t stream) { auto inData = inBatch.exportData(stream); if (inData == nullptr) @@ -212,11 +852,13 @@ ErrorCode MorphologyVarShape::infer(const nvcv::ImageBatchVarShape &inBatch, con } DataFormat format = input_format; - if (!(format == kNHWC || format == kHWC)) + if (!(format == kNHWC || format == kHWC || format == kNCHW || format == kCHW)) { - LOG_ERROR("Invalid input DataFormat " << format << ", the valid DataFormats are: \"NHWC\", \"HWC\""); + LOG_ERROR("Invalid input DataFormat " << format + << ", the valid DataFormats are: \"NHWC\", \"HWC\", \"NCHW\", \"CHW\""); return ErrorCode::INVALID_DATA_FORMAT; } + const bool isPlanar = IsPlanar(format); if (!(data_type == kCV_8U || data_type == kCV_16U || data_type == kCV_32F)) { @@ -245,19 +887,31 @@ ErrorCode MorphologyVarShape::infer(const nvcv::ImageBatchVarShape &inBatch, con return ErrorCode::INVALID_PARAMETER; } + if (isPlanar && static_cast(inData->numImages()) * channels > 65535) + { + LOG_ERROR("Planar Morphology requires numImages * channels <= 65535 (CUDA grid-z limit)"); + return ErrorCode::INVALID_PARAMETER; + } + if (noop) { - for (auto init = inBatch.begin(), outit = outBatch.begin(); init != inBatch.end(), outit != outBatch.end(); + for (auto init = inBatch.begin(), outit = outBatch.begin(); init != inBatch.end() && outit != outBatch.end(); ++init, ++outit) { - const Image &inimg = *init; - const Image &outimg = *outit; - auto inimgdata = inimg.exportData(); - auto outimgdata = outimg.exportData(); - const ImagePlaneStrided &inplane = inimgdata->plane(0); - const ImagePlaneStrided &outplane = outimgdata->plane(0); - checkCudaErrors(cudaMemcpy2DAsync(outplane.basePtr, outplane.rowStride, inplane.basePtr, inplane.rowStride, - inplane.rowStride, inplane.height, cudaMemcpyDeviceToDevice, stream)); + const Image &inimg = *init; + const Image &outimg = *outit; + auto inimgdata = inimg.exportData(); + auto outimgdata = outimg.exportData(); + for (int32_t p = 0; p < inimgdata->numPlanes(); ++p) + { + const ImagePlaneStrided &inplane = inimgdata->plane(p); + const ImagePlaneStrided &outplane = outimgdata->plane(p); + const size_t rowBytes + = static_cast(inplane.width) * inData->uniqueFormat().planePixelStrideBytes(p); + checkCudaErrors(cudaMemcpy2DAsync(outplane.basePtr, outplane.rowStride, inplane.basePtr, + inplane.rowStride, rowBytes, inplane.height, cudaMemcpyDeviceToDevice, + stream)); + } } return ErrorCode::SUCCESS; } @@ -269,7 +923,7 @@ ErrorCode MorphologyVarShape::infer(const nvcv::ImageBatchVarShape &inBatch, con typedef void (*filter2D_t)(const ImageBatchVarShapeDataStridedCuda &inData, const ImageBatchVarShapeDataStridedCuda &outData, const TensorDataStridedCuda &kMasks, const TensorDataStridedCuda &kAnchors, NVCVMorphologyType morph_type, - NVCVBorderType borderMode, cudaStream_t stream); + NVCVBorderType borderMode, bool enableGenericInterior, cudaStream_t stream); static const filter2D_t funcs[6][4] = { { MorphFilter2D, 0, MorphFilter2D, MorphFilter2D}, @@ -280,7 +934,25 @@ ErrorCode MorphologyVarShape::infer(const nvcv::ImageBatchVarShape &inBatch, con { MorphFilter2D, 0, MorphFilter2D, MorphFilter2D}, }; - funcs[data_type][channels - 1](*inData, *outData, masks, anchors, morph_type, borderMode, stream); + if (isPlanar) + { + typedef void (*filter2D_planar_t)( + const ImageBatchVarShapeDataStridedCuda &inData, const ImageBatchVarShapeDataStridedCuda &outData, + const TensorDataStridedCuda &kMasks, const TensorDataStridedCuda &kAnchors, NVCVMorphologyType morph_type, + NVCVBorderType borderMode, int channels, bool enableGenericInterior, cudaStream_t stream); + + static const filter2D_planar_t planarFuncs[6] = { + MorphFilter2DPlanar, 0, MorphFilter2DPlanar, 0, 0, MorphFilter2DPlanar, + }; + + planarFuncs[data_type](*inData, *outData, masks, anchors, morph_type, borderMode, channels, + enableGenericInterior, stream); + } + else + { + funcs[data_type][channels - 1](*inData, *outData, masks, anchors, morph_type, borderMode, enableGenericInterior, + stream); + } return ErrorCode::SUCCESS; } diff --git a/src/cvcuda/priv/legacy/normalize.cu b/src/cvcuda/priv/legacy/normalize.cu index e224a8d14..74cd14f32 100644 --- a/src/cvcuda/priv/legacy/normalize.cu +++ b/src/cvcuda/priv/legacy/normalize.cu @@ -1,4 +1,4 @@ -/* Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +/* Copyright (c) 2021-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * * SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES * SPDX-License-Identifier: Apache-2.0 @@ -22,14 +22,297 @@ #include "CvCudaLegacyHelpers.hpp" #include "CvCudaUtils.cuh" +#include "normalize_planar.cuh" // for PlanarBroadcastIndex, ApplyPlanarNormalize #include // for CVCUDA_NORMALIZE_SCALE_IS_STDDEV, etc. #include // for TypeTraits +#include + using namespace nvcv::legacy::cuda_op; using namespace nvcv::legacy::helpers; namespace cuda = nvcv::cuda; +// Planar (NCHW / CHW) kernel: src/dst are 4D scalar wraps indexed (n, c, y, x); base/scale are 4D +// scalar wraps with per-axis broadcasting determined by their logical (N, C, H, W) shape. is_stddev +// selects the inverse-stddev formula; the per-element math is shared with the var-shape path. +// base/scale are template wrapper types (not a fixed Tensor4DWrap) so the same kernel serves +// both the tensor path (Tensor4DWrap) and the by-value path (a register-backed per-channel +// wrap); both only need ptr(n, c, y, x) -> const float*. The tensor path deduces the wrap types from +// its Tensor4DWrap arguments, so it stays bit-identical. +template +__global__ void normalizePlanarKernel(const nvcv::cuda::Tensor4DWrap src, const BaseWrap base, + const ScaleWrap scale, nvcv::cuda::Tensor4DWrap dst, int4 inout_size, + int4 base_size, int4 scale_size, float global_scale, float global_shift, + float epsilon, bool is_stddev) +{ + const int src_x = blockIdx.x * blockDim.x + threadIdx.x; + const int src_y = blockIdx.y * blockDim.y + threadIdx.y; + const int nc = blockIdx.z; + const int batch = nc / inout_size.y; + const int channel = nc % inout_size.y; + + if (src_x >= inout_size.w || src_y >= inout_size.z) + return; + + const int4 b = PlanarBroadcastIndex(base_size, batch, channel, src_y, src_x); + const int4 s = PlanarBroadcastIndex(scale_size, batch, channel, src_y, src_x); + + *dst.ptr(batch, channel, src_y, src_x) + = ApplyPlanarNormalize(*src.ptr(batch, channel, src_y, src_x), *base.ptr(b.x, b.y, b.z, b.w), + *scale.ptr(s.x, s.y, s.z, s.w), global_scale, global_shift, is_stddev, epsilon); +} + +// ILP depth for the 1-byte planar path: each thread owns NGROUP independent uchar4 groups (4*NGROUP +// columns) of plane (batch, channel) and issues all NGROUP loads before any compute, raising the +// number of outstanding memory requests per resident thread (memory-level parallelism) at fixed +// occupancy. A plane is single-channel from each thread's view, so the latency-bound 1-byte planar +// path benefits like the single-channel U8 interleaved path (rather than regressing like the +// multi-channel interleaved hoist). NGROUP=2 is a smaller depth than the single-channel interleaved +// knob: the broadcast fast path here carries more live per-group state, so depth 2 overlaps two +// outstanding loads per thread while keeping registers (hence occupancy) from ballooning. +static constexpr int kNormalizePlanarILPNGroup = 2; + +// ILP depth for the F32 planar path: each thread owns NGROUP independent float4 groups (4*NGROUP +// columns) of plane (batch, channel) and issues all NGROUP float4 loads before any compute, raising +// memory-level parallelism at fixed occupancy on the latency-bound single-channel F32 planar path. +// A float4 group is 16 bytes of live register state (vs a uchar4 group's 4 bytes), so depth 2 is used +// (rather than 4) to overlap two outstanding loads per thread while keeping registers from ballooning +// and cutting occupancy. +static constexpr int kNormalizePlanarF32ILPNGroup = 2; + +// Vectorized 1-byte planar kernel: each thread owns NGROUP independent char4/uchar4 groups (4*NGROUP +// columns) of plane (batch, channel), strided by blockDim.x so each warp iteration stays coalesced. +// All NGROUP loads are issued into a local array before any compute, so several memory requests +// overlap per resident thread (loads-first ILP). The per-element math reuses ApplyPlanarNormalize +// (with the broadcast fast path hoisting base/mul once when base/scale are broadcast across the four +// columns, the common per-channel case), so output is bit-identical to normalizePlanarKernel. +// Requires 4-byte-aligned plane/row/sample strides (caller-guarded); columns past the last full +// uchar4 group fall back to scalar. +template +__global__ void normalizePlanarVec4Kernel(const nvcv::cuda::Tensor4DWrap src, const BaseWrap base, + const ScaleWrap scale, nvcv::cuda::Tensor4DWrap dst, + int4 inout_size, int4 base_size, int4 scale_size, float global_scale, + float global_shift, float epsilon, bool is_stddev) +{ + const int g0 = blockIdx.x * blockDim.x * NGROUP + threadIdx.x; + const int src_y = blockIdx.y * blockDim.y + threadIdx.y; + const int nc = blockIdx.z; + const int batch = nc / inout_size.y; + const int channel = nc % inout_size.y; + + const int cx0 = g0 * 4; + if (cx0 >= inout_size.w || src_y >= inout_size.z) + return; + + const int width = inout_size.w; + + using Vec4 = typename PlanarVec4Type::type; + + // Loads first: issue all NGROUP vector loads (for full in-range groups) into a local array before + // any compute, then compute, then store. The vector width is char4/uchar4 (32-bit) for 1-byte planes and + // float4 (128-bit) for F32 planes; both move 4 columns/group. + int cx[NGROUP]; + bool full[NGROUP]; + Vec4 in4[NGROUP]; +#pragma unroll + for (int i = 0; i < NGROUP; ++i) + { + cx[i] = (g0 + i * blockDim.x) * 4; + full[i] = cx[i] + 4 <= width; + if (full[i]) + in4[i] = *reinterpret_cast(src.ptr(batch, channel, src_y, cx[i])); + } + + if (base_size.w == 1 && scale_size.w == 1) + { + // base/scale are broadcast across the columns (the common per-channel case): resolve base and + // the inverse-std-dev multiplier once and reuse for every column. Bit-identical to the + // per-element path (same inputs). + const int4 b = PlanarBroadcastIndex(base_size, batch, channel, src_y, 0); + const int4 s = PlanarBroadcastIndex(scale_size, batch, channel, src_y, 0); + const float baseV = *base.ptr(b.x, b.y, b.z, b.w); + const float scaleV = *scale.ptr(s.x, s.y, s.z, s.w); + const float mul = is_stddev ? (1.0f / nvcv::cuda::sqrt(scaleV * scaleV + epsilon)) : scaleV; + auto apply = [&](nvcv::cuda::BaseType raw) -> T + { + return nvcv::cuda::SaturateCast((static_cast(raw) - baseV) * mul * global_scale + global_shift); + }; + +#pragma unroll + for (int i = 0; i < NGROUP; ++i) + { + if (full[i]) + { + Vec4 out4; + out4.x = apply(in4[i].x); + out4.y = apply(in4[i].y); + out4.z = apply(in4[i].z); + out4.w = apply(in4[i].w); + *reinterpret_cast(dst.ptr(batch, channel, src_y, cx[i])) = out4; + } + else if (cx[i] < width) + { + for (int x = cx[i]; x < width; ++x) + { + *dst.ptr(batch, channel, src_y, x) = apply(*src.ptr(batch, channel, src_y, x)); + } + } + } + } + else + { + // Per-column base/scale: index each column independently (matches normalizePlanarKernel). + auto applyAt = [&](nvcv::cuda::BaseType raw, int x) -> T + { + const int4 b = PlanarBroadcastIndex(base_size, batch, channel, src_y, x); + const int4 s = PlanarBroadcastIndex(scale_size, batch, channel, src_y, x); + return ApplyPlanarNormalize(static_cast(raw), *base.ptr(b.x, b.y, b.z, b.w), + *scale.ptr(s.x, s.y, s.z, s.w), global_scale, global_shift, is_stddev, + epsilon); + }; + +#pragma unroll + for (int i = 0; i < NGROUP; ++i) + { + if (full[i]) + { + const nvcv::cuda::BaseType raw[4] = {in4[i].x, in4[i].y, in4[i].z, in4[i].w}; + Vec4 out4; + out4.x = applyAt(raw[0], cx[i] + 0); + out4.y = applyAt(raw[1], cx[i] + 1); + out4.z = applyAt(raw[2], cx[i] + 2); + out4.w = applyAt(raw[3], cx[i] + 3); + *reinterpret_cast(dst.ptr(batch, channel, src_y, cx[i])) = out4; + } + else if (cx[i] < width) + { + for (int x = cx[i]; x < width; ++x) + { + *dst.ptr(batch, channel, src_y, x) = applyAt(*src.ptr(batch, channel, src_y, x), x); + } + } + } + } +} + +// Launch path for the planar variants; isStdDev/epsilon are forwarded to the unified kernel +// (epsilon is unused when isStdDev is false). +template +ErrorCode normalizePlanarImpl(const nvcv::TensorDataStridedCuda &inData, const nvcv::TensorDataStridedCuda &baseData, + const nvcv::TensorDataStridedCuda &scaleData, const nvcv::TensorDataStridedCuda &outData, + float global_scale, float shift, bool isStdDev, float epsilon, cudaStream_t stream) +{ + auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(inData); + auto outAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(outData); + auto baseAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(baseData); + auto scaleAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(scaleData); + NVCV_ASSERT(inAccess && outAccess && baseAccess && scaleAccess); + + auto inMaxStride = inAccess->sampleStride() * inAccess->numSamples(); + auto outMaxStride = outAccess->sampleStride() * outAccess->numSamples(); + auto baseMaxStride = baseAccess->sampleStride() * baseAccess->numSamples(); + auto scaleMaxStride = scaleAccess->sampleStride() * scaleAccess->numSamples(); + if (std::max(std::max(inMaxStride, outMaxStride), std::max(baseMaxStride, scaleMaxStride)) + > cuda::TypeTraits::max) + { + LOG_ERROR("Input, output, base, or scale size exceeds " << cuda::TypeTraits::max + << ". Tensor is too large."); + return ErrorCode::INVALID_PARAMETER; + } + + auto srcWrap = nvcv::cuda::CreateTensorWrapNCHW(inData); + auto dstWrap = nvcv::cuda::CreateTensorWrapNCHW(outData); + auto baseWrap = nvcv::cuda::CreateTensorWrapNCHW(baseData); + auto scaleWrap = nvcv::cuda::CreateTensorWrapNCHW(scaleData); + + int4 inout_size = {static_cast(inAccess->numSamples()), static_cast(inAccess->numChannels()), + static_cast(inAccess->numRows()), static_cast(inAccess->numCols())}; + int4 base_size = {static_cast(baseAccess->numSamples()), static_cast(baseAccess->numChannels()), + static_cast(baseAccess->numRows()), static_cast(baseAccess->numCols())}; + int4 scale_size = {static_cast(scaleAccess->numSamples()), static_cast(scaleAccess->numChannels()), + static_cast(scaleAccess->numRows()), static_cast(scaleAccess->numCols())}; + + const uint64_t planes = static_cast(inout_size.x) * inout_size.y; + if (planes > 65535u) + { + LOG_ERROR("Planar normalize launch exceeds CUDA grid.z limit: N*C=" << planes); + return ErrorCode::INVALID_PARAMETER; + } + + // Vectorized fast path for the latency-bound 1-byte planar case (the scalar kernel moves 1 + // byte/thread). Each thread covers NGROUP uchar4 groups (4*NGROUP columns) with loads-first ILP; + // bit-identical output. Requires 4-byte-aligned plane/row/sample strides, else the scalar kernel + // runs. + if constexpr (sizeof(T) == 1) + { + const bool aligned = inAccess->rowStride() % 4 == 0 && inAccess->chStride() % 4 == 0 + && inAccess->sampleStride() % 4 == 0 && outAccess->rowStride() % 4 == 0 + && outAccess->chStride() % 4 == 0 && outAccess->sampleStride() % 4 == 0; + if (aligned) + { + constexpr int NGROUP = kNormalizePlanarILPNGroup; + dim3 vblock(32, 8); + dim3 vgrid(divUp(divUp(inout_size.w, 4), static_cast(vblock.x) * NGROUP), + divUp(inout_size.z, static_cast(vblock.y)), static_cast(planes)); + normalizePlanarVec4Kernel<<>>(srcWrap, baseWrap, scaleWrap, dstWrap, + inout_size, base_size, scale_size, + global_scale, shift, epsilon, isStdDev); + checkKernelErrors(); + return ErrorCode::SUCCESS; + } + } + + // Vectorized fast path for the latency-bound F32 planar case (the scalar kernel moves 4 + // bytes/thread). Each thread covers NGROUP float4 groups (4*NGROUP columns, 128-bit transfers) + // with loads-first ILP; bit-identical output (no SaturateCast needed for float out). Requires + // 16-byte-aligned plane/row/sample strides, else the scalar kernel runs. + if constexpr (std::is_same_v) + { + const bool aligned = inAccess->rowStride() % 16 == 0 && inAccess->chStride() % 16 == 0 + && inAccess->sampleStride() % 16 == 0 && outAccess->rowStride() % 16 == 0 + && outAccess->chStride() % 16 == 0 && outAccess->sampleStride() % 16 == 0; + if (aligned) + { + constexpr int NGROUP = kNormalizePlanarF32ILPNGroup; + dim3 vblock(32, 8); + dim3 vgrid(divUp(divUp(inout_size.w, 4), static_cast(vblock.x) * NGROUP), + divUp(inout_size.z, static_cast(vblock.y)), static_cast(planes)); + normalizePlanarVec4Kernel<<>>(srcWrap, baseWrap, scaleWrap, dstWrap, + inout_size, base_size, scale_size, + global_scale, shift, epsilon, isStdDev); + checkKernelErrors(); + return ErrorCode::SUCCESS; + } + } + + dim3 block(32, 8); + dim3 grid(divUp(inout_size.w, block.x), divUp(inout_size.z, block.y), static_cast(planes)); + + normalizePlanarKernel<<>>(srcWrap, baseWrap, scaleWrap, dstWrap, inout_size, base_size, + scale_size, global_scale, shift, epsilon, isStdDev); + checkKernelErrors(); + return ErrorCode::SUCCESS; +} + +template +ErrorCode normalizePlanar(const nvcv::TensorDataStridedCuda &inData, const nvcv::TensorDataStridedCuda &baseData, + const nvcv::TensorDataStridedCuda &scaleData, const nvcv::TensorDataStridedCuda &outData, + float global_scale, float shift, cudaStream_t stream) +{ + return normalizePlanarImpl(inData, baseData, scaleData, outData, global_scale, shift, false, 0.f, stream); +} + +template +ErrorCode normalizePlanarInvStdDev(const nvcv::TensorDataStridedCuda &inData, + const nvcv::TensorDataStridedCuda &baseData, + const nvcv::TensorDataStridedCuda &scaleData, + const nvcv::TensorDataStridedCuda &outData, float global_scale, float shift, + float epsilon, cudaStream_t stream) +{ + return normalizePlanarImpl(inData, baseData, scaleData, outData, global_scale, shift, true, epsilon, stream); +} + // (float3 - float3) * float3 / (float3 - float) * float3 / (float3 - float3) * float / (float3 - float) * float template __global__ void normalizeKernel(const input_type src, const base_type base, const scale_type scale, input_type dst, @@ -92,6 +375,335 @@ __global__ void normalizeInvStdDevKernel(const input_type src, const base_type b + global_shift); } +// ILP depth used by the single-channel 8-bit inverse-std-dev wide-load path: each thread issues this +// many independent uint4 loads before computing, raising the number of outstanding memory requests +// per resident thread (memory-level parallelism) at fixed occupancy. NGROUP=4 measured the best +// long-scoreboard-stall reduction on the single-channel uint4 path without register pressure growing +// enough to cut occupancy. +static constexpr int kNormalizeILPNGroup = 4; + +// Inverse-std-dev normalize that processes NIX pixels per thread, used when base and scale are +// broadcast across the spatial extent (per-channel or scalar params, the common case). The scalar +// normalizeInvStdDevKernel recomputes the inverse-std-dev multiplier mul = 1 / sqrt(scale^2 + eps) +// for every pixel; for multi-channel interleaved input (uchar3 / uchar4) that is several sqrt + +// reciprocal per pixel, all redundant because scale is identical for every pixel of a sample. That +// makes those paths issue-bound (~83% issue-slot utilization on Ampere) rather than memory-bound. +// Here each thread reads base/scale and computes mul once, then applies it to NIX pixels, cutting the +// redundant SFU/issue work NIX-fold. Pixels are strided by blockDim.x so each warp iteration stays +// coalesced. Output is bit-identical to normalizeInvStdDevKernel (same per-element arithmetic; only +// the multiplier hoists out of the per-pixel loop). +template +__global__ void normalizeInvStdDevHoistKernel(const input_type src, const base_type base, const scale_type scale, + input_type dst, int2 inout_size, int3 base_size, int3 scale_size, + float global_scale, float global_shift, float epsilon) +{ + const int src_y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + + if (src_y >= inout_size.y) + return; + + const int base_batch_idx = base_size.z == 1 ? 0 : batch_idx; + const int scale_batch_idx = scale_size.z == 1 ? 0 : batch_idx; + + using input_value_type = typename input_type::ValueType; + using base_value_type = typename base_type::ValueType; + using scale_value_type = typename scale_type::ValueType; + + const base_value_type b = *base.ptr(base_batch_idx, 0, 0); + const scale_value_type s = *scale.ptr(scale_batch_idx, 0, 0); + const scale_value_type x = s * s + epsilon; + const scale_value_type mul = 1.0f / nvcv::cuda::sqrt(x); + + const int x0 = blockIdx.x * blockDim.x * NIX + threadIdx.x; +#pragma unroll + for (int i = 0; i < NIX; ++i) + { + const int src_x = x0 + i * blockDim.x; + if (src_x < inout_size.x) + { + *dst.ptr(batch_idx, src_y, src_x) = nvcv::cuda::SaturateCast( + (*src.ptr(batch_idx, src_y, src_x) - b) * mul * global_scale + global_shift); + } + } +} + +// Vectorized single-channel 8-bit inverse-std-dev normalize. The scalar kernel above processes one +// byte per thread, which leaves the 1-byte path latency-bound (~44% memory SOL, 0.72 issued +// warps/scheduler): too few bytes in flight per thread to hide load latency. Here each thread owns +// four consecutive columns and moves them with a single 32-bit (uchar4) load/store, so a warp +// transfers a full 128-byte line and address/compute overhead is amortized 4x. The per-element math +// is identical to normalizeInvStdDevKernel (same expression, same SaturateCast), so output is +// bit-for-bit unchanged. Requires 4-byte-aligned row/sample strides (caller-guarded); a width that +// is not a multiple of 4 falls back to scalar for the tail columns. base/scale are read per element, +// so all broadcast shapes stay correct. +template +__global__ void normalizeInvStdDevU8Vec4Kernel(SrcWrapper src, BaseWrapper base, ScaleWrapper scale, DstWrapper dst, + int2 inout_size, int3 base_size, int3 scale_size, float global_scale, + float global_shift, float epsilon) +{ + const int cx = (blockIdx.x * blockDim.x + threadIdx.x) * 4; + const int src_y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + + if (cx >= inout_size.x || src_y >= inout_size.y) + return; + + const int base_y = base_size.y == 1 ? 0 : src_y; + const int base_batch_idx = base_size.z == 1 ? 0 : batch_idx; + const int scale_y = scale_size.y == 1 ? 0 : src_y; + const int scale_batch_idx = scale_size.z == 1 ? 0 : batch_idx; + + auto normOne = [&](unsigned char raw, int x) -> unsigned char + { + const int base_x = base_size.x == 1 ? 0 : x; + const int scale_x = scale_size.x == 1 ? 0 : x; + const float s = *scale.ptr(scale_batch_idx, scale_y, scale_x); + const float mul = 1.0f / nvcv::cuda::sqrt(s * s + epsilon); + return nvcv::cuda::SaturateCast( + (static_cast(raw) - *base.ptr(base_batch_idx, base_y, base_x)) * mul * global_scale + global_shift); + }; + + if (cx + 4 <= inout_size.x) + { + const uchar4 in4 = *reinterpret_cast(src.ptr(batch_idx, src_y, cx)); + uchar4 out4; + if (base_size.x == 1 && scale_size.x == 1) + { + // base/scale are broadcast across these four columns (the common per-channel case), so + // the expensive inverse-std-dev multiplier and the base are identical for all four: + // compute them once and reuse. Bit-identical to the per-element path (same inputs). + const float baseV = *base.ptr(base_batch_idx, base_y, 0); + const float s = *scale.ptr(scale_batch_idx, scale_y, 0); + const float mul = 1.0f / nvcv::cuda::sqrt(s * s + epsilon); + auto apply = [&](unsigned char raw) -> unsigned char + { + return nvcv::cuda::SaturateCast((static_cast(raw) - baseV) * mul * global_scale + + global_shift); + }; + out4.x = apply(in4.x); + out4.y = apply(in4.y); + out4.z = apply(in4.z); + out4.w = apply(in4.w); + } + else + { + out4.x = normOne(in4.x, cx + 0); + out4.y = normOne(in4.y, cx + 1); + out4.z = normOne(in4.z, cx + 2); + out4.w = normOne(in4.w, cx + 3); + } + *reinterpret_cast(dst.ptr(batch_idx, src_y, cx)) = out4; + } + else + { + for (int x = cx; x < inout_size.x; ++x) + { + *dst.ptr(batch_idx, src_y, x) = normOne(*src.ptr(batch_idx, src_y, x), x); + } + } +} + +// Vectorized single-channel 8-bit inverse-std-dev kernel: each thread owns NGROUP independent uint4 groups +// (16*NGROUP consecutive columns). All NGROUP loads are issued into a local array before any compute, +// so NGROUP load requests are outstanding per thread at once. A single wide uint4 request raises +// bytes-per-request but not the number of in-flight requests; issuing several independent loads first +// raises memory-level parallelism (more outstanding requests per resident thread) at fixed occupancy, +// which is the real lever on the latency-bound (not bandwidth-bound) single-channel 8-bit path. The +// per-element math, broadcast fast-path, and SaturateCast are identical to the uint4 kernel, so output +// is bit-for-bit unchanged. Requires 16-byte-aligned row/sample strides (caller-guarded); columns past +// the last full 16*NGROUP block fall back to scalar. +template +__global__ void normalizeInvStdDevU8VecILPKernel(SrcWrapper src, BaseWrapper base, ScaleWrapper scale, DstWrapper dst, + int2 inout_size, int3 base_size, int3 scale_size, float global_scale, + float global_shift, float epsilon) +{ + const int base_cx = (blockIdx.x * blockDim.x + threadIdx.x) * (16 * NGROUP); + const int src_y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + + if (base_cx >= inout_size.x || src_y >= inout_size.y) + return; + + const int base_y = base_size.y == 1 ? 0 : src_y; + const int base_batch_idx = base_size.z == 1 ? 0 : batch_idx; + const int scale_y = scale_size.y == 1 ? 0 : src_y; + const int scale_batch_idx = scale_size.z == 1 ? 0 : batch_idx; + + auto normOne = [&](unsigned char raw, int x) -> unsigned char + { + const int base_x = base_size.x == 1 ? 0 : x; + const int scale_x = scale_size.x == 1 ? 0 : x; + const float s = *scale.ptr(scale_batch_idx, scale_y, scale_x); + const float mul = 1.0f / nvcv::cuda::sqrt(s * s + epsilon); + return nvcv::cuda::SaturateCast( + (static_cast(raw) - *base.ptr(base_batch_idx, base_y, base_x)) * mul * global_scale + global_shift); + }; + + if (base_cx + 16 * NGROUP <= inout_size.x) + { + // Issue all NGROUP loads first so the requests overlap, then compute, then store. + uint4 v[NGROUP]; +#pragma unroll + for (int g = 0; g < NGROUP; ++g) + { + v[g] = *reinterpret_cast(src.ptr(batch_idx, src_y, base_cx + g * 16)); + } + + unsigned char o[NGROUP][16]; + if (base_size.x == 1 && scale_size.x == 1) + { + const float baseV = *base.ptr(base_batch_idx, base_y, 0); + const float s = *scale.ptr(scale_batch_idx, scale_y, 0); + const float mul = 1.0f / nvcv::cuda::sqrt(s * s + epsilon); + auto apply = [&](unsigned char raw) -> unsigned char + { + return nvcv::cuda::SaturateCast((static_cast(raw) - baseV) * mul * global_scale + + global_shift); + }; +#pragma unroll + for (int g = 0; g < NGROUP; ++g) + { + const unsigned char *b = reinterpret_cast(&v[g]); +#pragma unroll + for (int i = 0; i < 16; ++i) + { + o[g][i] = apply(b[i]); + } + } + } + else + { +#pragma unroll + for (int g = 0; g < NGROUP; ++g) + { + const unsigned char *b = reinterpret_cast(&v[g]); +#pragma unroll + for (int i = 0; i < 16; ++i) + { + o[g][i] = normOne(b[i], base_cx + g * 16 + i); + } + } + } + +#pragma unroll + for (int g = 0; g < NGROUP; ++g) + { + *reinterpret_cast(dst.ptr(batch_idx, src_y, base_cx + g * 16)) + = *reinterpret_cast(o[g]); + } + } + else + { + for (int x = base_cx; x < inout_size.x; ++x) + { + *dst.ptr(batch_idx, src_y, x) = normOne(*src.ptr(batch_idx, src_y, x), x); + } + } +} + +// ILP depth used by the single-channel F32 inverse-std-dev wide-load path. The scalar F32 kernel +// moves one float (4 bytes) per thread, leaving the single-channel F32 path latency-bound (too few +// bytes in flight per thread to hide load latency). Here each thread owns NGROUP independent float4 +// groups (4*NGROUP consecutive columns) and issues all NGROUP loads before any compute, raising the +// number of outstanding memory requests per resident thread (memory-level parallelism) at fixed +// occupancy. NGROUP=4 matches the proven U8 value; the float4 path carries 4 floats (16 bytes) of +// live state per group rather than 16 bytes of packed bytes, so registers are watched. +static constexpr int kNormalizeF32ILPNGroup = 4; + +// Vectorized single-channel F32 inverse-std-dev normalize: each thread owns NGROUP independent float4 +// groups (4*NGROUP consecutive columns). All NGROUP float4 loads are issued into a local array before +// any compute, so NGROUP load requests are outstanding per thread at once -- raising memory-level +// parallelism (more outstanding requests per resident thread) at fixed occupancy, the lever on the +// latency-bound single-channel F32 path. When base/scale are broadcast across the four columns (the +// common per-channel case) the base and the inverse-std-dev multiplier are computed once per thread +// and reused. The per-element math is identical to normalizeInvStdDevKernel (same expression; F32 +// output needs no SaturateCast since the value is already float), so output is bit-for-bit unchanged. +// Requires 16-byte-aligned row/sample strides (caller-guarded); columns past the last full 4*NGROUP +// block fall back to scalar. +template +__global__ void normalizeInvStdDevF32VecILPKernel(SrcWrapper src, BaseWrapper base, ScaleWrapper scale, DstWrapper dst, + int2 inout_size, int3 base_size, int3 scale_size, float global_scale, + float global_shift, float epsilon) +{ + const int base_cx = (blockIdx.x * blockDim.x + threadIdx.x) * (4 * NGROUP); + const int src_y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + + if (base_cx >= inout_size.x || src_y >= inout_size.y) + return; + + const int base_y = base_size.y == 1 ? 0 : src_y; + const int base_batch_idx = base_size.z == 1 ? 0 : batch_idx; + const int scale_y = scale_size.y == 1 ? 0 : src_y; + const int scale_batch_idx = scale_size.z == 1 ? 0 : batch_idx; + + auto normOne = [&](float raw, int x) -> float + { + const int base_x = base_size.x == 1 ? 0 : x; + const int scale_x = scale_size.x == 1 ? 0 : x; + const float s = *scale.ptr(scale_batch_idx, scale_y, scale_x); + const float mul = 1.0f / nvcv::cuda::sqrt(s * s + epsilon); + return (raw - *base.ptr(base_batch_idx, base_y, base_x)) * mul * global_scale + global_shift; + }; + + if (base_cx + 4 * NGROUP <= inout_size.x) + { + // Issue all NGROUP float4 loads first so the requests overlap, then compute, then store. + float4 v[NGROUP]; +#pragma unroll + for (int g = 0; g < NGROUP; ++g) + { + v[g] = *reinterpret_cast(src.ptr(batch_idx, src_y, base_cx + g * 4)); + } + + float4 o[NGROUP]; + if (base_size.x == 1 && scale_size.x == 1) + { + const float baseV = *base.ptr(base_batch_idx, base_y, 0); + const float s = *scale.ptr(scale_batch_idx, scale_y, 0); + const float mul = 1.0f / nvcv::cuda::sqrt(s * s + epsilon); + auto apply = [&](float raw) -> float + { + return (raw - baseV) * mul * global_scale + global_shift; + }; +#pragma unroll + for (int g = 0; g < NGROUP; ++g) + { + o[g].x = apply(v[g].x); + o[g].y = apply(v[g].y); + o[g].z = apply(v[g].z); + o[g].w = apply(v[g].w); + } + } + else + { +#pragma unroll + for (int g = 0; g < NGROUP; ++g) + { + const int cx = base_cx + g * 4; + o[g].x = normOne(v[g].x, cx + 0); + o[g].y = normOne(v[g].y, cx + 1); + o[g].z = normOne(v[g].z, cx + 2); + o[g].w = normOne(v[g].w, cx + 3); + } + } + +#pragma unroll + for (int g = 0; g < NGROUP; ++g) + { + *reinterpret_cast(dst.ptr(batch_idx, src_y, base_cx + g * 4)) = o[g]; + } + } + else + { + for (int x = base_cx; x < inout_size.x; ++x) + { + *dst.ptr(batch_idx, src_y, x) = normOne(*src.ptr(batch_idx, src_y, x), x); + } + } +} + template void normalizeWrap(WrapInput srcWrap, WrapOutput dstWrap, DataShape input_shape, const nvcv::TensorDataStridedCuda &baseData, const nvcv::TensorDataStridedCuda &scaleData, @@ -143,6 +755,25 @@ void normalizeInvStdDevWrap(WrapInput srcWrap, WrapOutput dstWrap, DataShape inp int3 scale_size = {static_cast(scaleAccess->numCols()), static_cast(scaleAccess->numRows()), static_cast(scaleAccess->numSamples())}; + // Multi-byte-element (float / float3 / ...) inputs are already memory-bound here, so recomputing + // the multiplier per pixel costs nothing; only the 1-byte interleaved paths (uchar3 / uchar4) are + // issue-bound on the redundant per-pixel sqrt. When base and scale are spatially broadcast (the + // common per-channel case) hoist the multiplier and process NIX pixels per thread. + using pixel_type = typename WrapInput::ValueType; + if constexpr (sizeof(typename nvcv::cuda::BaseType) == 1) + { + const bool spatialBroadcast = base_size.x == 1 && base_size.y == 1 && scale_size.x == 1 && scale_size.y == 1; + if (spatialBroadcast) + { + constexpr int NIX = 4; + dim3 hgrid(divUp(input_shape.W, block.x * NIX), divUp(input_shape.H, block.y), input_shape.N); + normalizeInvStdDevHoistKernel<<>>( + srcWrap, baseWrap, scaleWrap, dstWrap, inout_size, base_size, scale_size, global_scale, shift, epsilon); + checkKernelErrors(); + return; + } + } + normalizeInvStdDevKernel<<>>(srcWrap, baseWrap, scaleWrap, dstWrap, inout_size, base_size, scale_size, global_scale, shift, epsilon); checkKernelErrors(); @@ -296,14 +927,499 @@ ErrorCode normalizeInvStdDev(const nvcv::TensorDataStridedCuda &inData, const nv return ErrorCode::SUCCESS; } +// Launcher for the vectorized single-channel 8-bit inverse-std-dev path. Mirrors normalizeInvStdDev +// (single channel => base/scale resolve to scalar float wraps) but launches the uchar4 kernel with +// one thread per four columns. Defers to the scalar specialization for tensors too large for 32-bit +// indexing, so behavior is preserved on that path. +inline ErrorCode normalizeInvStdDevU8Vec(const nvcv::TensorDataStridedCuda &inData, + const nvcv::TensorDataStridedCuda &baseData, + const nvcv::TensorDataStridedCuda &scaleData, + const nvcv::TensorDataStridedCuda &outData, float global_scale, float shift, + float epsilon, cudaStream_t stream) +{ + auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(inData); + auto outAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(outData); + NVCV_ASSERT(inAccess && outAccess); + + auto inMaxStride = inAccess->sampleStride() * inAccess->numSamples(); + auto outMaxStride = outAccess->sampleStride() * outAccess->numSamples(); + if (std::max(inMaxStride, outMaxStride) > cuda::TypeTraits::max) + { + return normalizeInvStdDev(inData, baseData, scaleData, outData, global_scale, shift, epsilon, stream); + } + + DataShape input_shape = GetLegacyDataShape(inAccess->infoShape()); + + auto srcWrap = nvcv::cuda::CreateTensorWrapNHW(inData); + auto dstWrap = nvcv::cuda::CreateTensorWrapNHW(outData); + auto baseWrap = nvcv::cuda::CreateTensorWrapNHW(baseData); + auto scaleWrap = nvcv::cuda::CreateTensorWrapNHW(scaleData); + + auto baseAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(baseData); + auto scaleAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(scaleData); + NVCV_ASSERT(baseAccess && scaleAccess); + + int2 inout_size = {input_shape.W, input_shape.H}; + int3 base_size = {static_cast(baseAccess->numCols()), static_cast(baseAccess->numRows()), + static_cast(baseAccess->numSamples())}; + int3 scale_size = {static_cast(scaleAccess->numCols()), static_cast(scaleAccess->numRows()), + static_cast(scaleAccess->numSamples())}; + + dim3 block(32, 8); + dim3 grid(divUp(divUp(input_shape.W, 4), static_cast(block.x)), + divUp(input_shape.H, static_cast(block.y)), input_shape.N); + + normalizeInvStdDevU8Vec4Kernel<<>>(srcWrap, baseWrap, scaleWrap, dstWrap, inout_size, + base_size, scale_size, global_scale, shift, epsilon); + checkKernelErrors(); + return ErrorCode::SUCCESS; +} + +// Launcher for the ILP (NGROUP independent uint4 groups/thread) single-channel 8-bit inverse-std-dev +// path. Mirrors normalizeInvStdDevU8Vec but each thread covers 16*NGROUP columns and the grid shrinks +// accordingly. Defers to the scalar specialization for tensors too large for 32-bit indexing. +template +inline ErrorCode normalizeInvStdDevU8VecILP(const nvcv::TensorDataStridedCuda &inData, + const nvcv::TensorDataStridedCuda &baseData, + const nvcv::TensorDataStridedCuda &scaleData, + const nvcv::TensorDataStridedCuda &outData, float global_scale, float shift, + float epsilon, cudaStream_t stream) +{ + auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(inData); + auto outAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(outData); + NVCV_ASSERT(inAccess && outAccess); + + auto inMaxStride = inAccess->sampleStride() * inAccess->numSamples(); + auto outMaxStride = outAccess->sampleStride() * outAccess->numSamples(); + if (std::max(inMaxStride, outMaxStride) > cuda::TypeTraits::max) + { + return normalizeInvStdDev(inData, baseData, scaleData, outData, global_scale, shift, epsilon, stream); + } + + DataShape input_shape = GetLegacyDataShape(inAccess->infoShape()); + + auto srcWrap = nvcv::cuda::CreateTensorWrapNHW(inData); + auto dstWrap = nvcv::cuda::CreateTensorWrapNHW(outData); + auto baseWrap = nvcv::cuda::CreateTensorWrapNHW(baseData); + auto scaleWrap = nvcv::cuda::CreateTensorWrapNHW(scaleData); + + auto baseAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(baseData); + auto scaleAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(scaleData); + NVCV_ASSERT(baseAccess && scaleAccess); + + int2 inout_size = {input_shape.W, input_shape.H}; + int3 base_size = {static_cast(baseAccess->numCols()), static_cast(baseAccess->numRows()), + static_cast(baseAccess->numSamples())}; + int3 scale_size = {static_cast(scaleAccess->numCols()), static_cast(scaleAccess->numRows()), + static_cast(scaleAccess->numSamples())}; + + dim3 block(32, 8); + dim3 grid(divUp(divUp(input_shape.W, 16 * NGROUP), static_cast(block.x)), + divUp(input_shape.H, static_cast(block.y)), input_shape.N); + + normalizeInvStdDevU8VecILPKernel<<>>( + srcWrap, baseWrap, scaleWrap, dstWrap, inout_size, base_size, scale_size, global_scale, shift, epsilon); + checkKernelErrors(); + return ErrorCode::SUCCESS; +} + +// Launcher for the ILP (NGROUP independent float4 groups/thread) single-channel F32 inverse-std-dev +// path. Mirrors normalizeInvStdDevU8VecILP but for float input/output: each thread covers 4*NGROUP +// columns and the grid shrinks accordingly. Defers to the scalar specialization for tensors too large +// for 32-bit indexing. +template +inline ErrorCode normalizeInvStdDevF32VecILP(const nvcv::TensorDataStridedCuda &inData, + const nvcv::TensorDataStridedCuda &baseData, + const nvcv::TensorDataStridedCuda &scaleData, + const nvcv::TensorDataStridedCuda &outData, float global_scale, + float shift, float epsilon, cudaStream_t stream) +{ + auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(inData); + auto outAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(outData); + NVCV_ASSERT(inAccess && outAccess); + + auto inMaxStride = inAccess->sampleStride() * inAccess->numSamples(); + auto outMaxStride = outAccess->sampleStride() * outAccess->numSamples(); + if (std::max(inMaxStride, outMaxStride) > cuda::TypeTraits::max) + { + return normalizeInvStdDev(inData, baseData, scaleData, outData, global_scale, shift, epsilon, stream); + } + + DataShape input_shape = GetLegacyDataShape(inAccess->infoShape()); + + auto srcWrap = nvcv::cuda::CreateTensorWrapNHW(inData); + auto dstWrap = nvcv::cuda::CreateTensorWrapNHW(outData); + auto baseWrap = nvcv::cuda::CreateTensorWrapNHW(baseData); + auto scaleWrap = nvcv::cuda::CreateTensorWrapNHW(scaleData); + + auto baseAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(baseData); + auto scaleAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(scaleData); + NVCV_ASSERT(baseAccess && scaleAccess); + + int2 inout_size = {input_shape.W, input_shape.H}; + int3 base_size = {static_cast(baseAccess->numCols()), static_cast(baseAccess->numRows()), + static_cast(baseAccess->numSamples())}; + int3 scale_size = {static_cast(scaleAccess->numCols()), static_cast(scaleAccess->numRows()), + static_cast(scaleAccess->numSamples())}; + + dim3 block(32, 8); + dim3 grid(divUp(divUp(input_shape.W, 4 * NGROUP), static_cast(block.x)), + divUp(input_shape.H, static_cast(block.y)), input_shape.N); + + normalizeInvStdDevF32VecILPKernel<<>>( + srcWrap, baseWrap, scaleWrap, dstWrap, inout_size, base_size, scale_size, global_scale, shift, epsilon); + checkKernelErrors(); + return ErrorCode::SUCCESS; +} + +// --------------------------------------------------------------------------------------------------- +// Tensor-free (by-value scalar / per-channel parameter) interleaved path. +// +// base/scale are supplied as host constants packed into a float4 and passed by value, so the caller +// avoids allocating and uploading per-channel parameter tensors (no device alloc, no H2D copy). +// ConstantValueWrap adapts a register-resident value to the same wrap interface (.ptr()/ValueType) +// the interleaved kernels already consume, so the existing normalizeKernel / normalizeInvStdDevKernel +// run unchanged with base_size/scale_size of {1,1,1} -- producing output bit-identical to the tensor +// path for the same values. Interleaved (NHWC/HWC) only; the planar (NCHW/CHW) by-value path is below. +// The 1-byte multiplier-hoist fast path is shared with the tensor dispatch. The specialized +// single-channel U8/F32 vectorized ILP kernels are not wired into the by-value dispatch yet. +// --------------------------------------------------------------------------------------------------- +template +struct ConstantValueWrap +{ + using ValueType = T; + + T value; + + inline const __host__ __device__ T *ptr(int, int, int) const + { + return &value; + } +}; + +// Build a work value (float / float3 / float4) from the leading lanes of a float4 on the host. For a +// scalar work type the identity of lane 0 is returned; NumElements is constexpr so the loop is fixed. +template +inline VecT MakeWorkVec(float4 v) +{ + VecT out{}; + for (int i = 0; i < nvcv::cuda::NumElements; ++i) + { + nvcv::cuda::GetElement(out, i) = nvcv::cuda::GetElement(v, i); + } + return out; +} + +template +void normalizeScalarWrap(WrapInput srcWrap, WrapOutput dstWrap, DataShape input_shape, base_type baseVal, + scale_type scaleVal, float global_scale, float shift, cudaStream_t stream) +{ + dim3 block(32, 8); + dim3 grid(divUp(input_shape.W, block.x), divUp(input_shape.H, block.y), input_shape.N); + + ConstantValueWrap baseWrap{baseVal}; + ConstantValueWrap scaleWrap{scaleVal}; + + int2 inout_size = {input_shape.W, input_shape.H}; + int3 base_size = {1, 1, 1}; + int3 scale_size = {1, 1, 1}; + + normalizeKernel<<>>(srcWrap, baseWrap, scaleWrap, dstWrap, inout_size, base_size, + scale_size, global_scale, shift); + checkKernelErrors(); +} + +template +void normalizeScalarInvStdDevWrap(WrapInput srcWrap, WrapOutput dstWrap, DataShape input_shape, base_type baseVal, + scale_type scaleVal, float global_scale, float shift, float epsilon, + cudaStream_t stream) +{ + dim3 block(32, 8); + dim3 grid(divUp(input_shape.W, block.x), divUp(input_shape.H, block.y), input_shape.N); + + ConstantValueWrap baseWrap{baseVal}; + ConstantValueWrap scaleWrap{scaleVal}; + + int2 inout_size = {input_shape.W, input_shape.H}; + int3 base_size = {1, 1, 1}; + int3 scale_size = {1, 1, 1}; + + // Match the tensor path's multi-channel 8-bit fast path. By-value parameters are spatially + // broadcast by definition, so the inverse-std-dev multiplier can be hoisted and reused across + // four pixels per thread instead of recomputed for every pixel. + using pixel_type = typename WrapInput::ValueType; + if constexpr (sizeof(typename nvcv::cuda::BaseType) == 1) + { + constexpr int NIX = 4; + dim3 hgrid(divUp(input_shape.W, block.x * NIX), divUp(input_shape.H, block.y), input_shape.N); + normalizeInvStdDevHoistKernel<<>>( + srcWrap, baseWrap, scaleWrap, dstWrap, inout_size, base_size, scale_size, global_scale, shift, epsilon); + checkKernelErrors(); + return; + } + + normalizeInvStdDevKernel<<>>(srcWrap, baseWrap, scaleWrap, dstWrap, inout_size, base_size, + scale_size, global_scale, shift, epsilon); + checkKernelErrors(); +} + +// Selects per-channel (work_type: float3/float4) vs scalar-broadcast (float) base/scale wraps from the +// provided counts, mirroring callNormalizeWrap's numChannels()-based branch. count == 1 broadcasts one +// value to every channel; count == channels supplies per-channel values. +template +void callNormalizeScalarWrap(const input_wrapper &input, const DataShape &inputShape, float4 base, float4 scale, + int baseCount, int scaleCount, const output_wrapper &output, float global_scale, + float shift, cudaStream_t stream) +{ + using input_type = typename input_wrapper::ValueType; + using work_type = nvcv::cuda::ConvertBaseTypeTo; + + if (baseCount != 1 && scaleCount != 1) + { + normalizeScalarWrap(input, output, inputShape, MakeWorkVec(base), + MakeWorkVec(scale), global_scale, shift, stream); + } + else if (baseCount != 1) + { + normalizeScalarWrap(input, output, inputShape, MakeWorkVec(base), scale.x, + global_scale, shift, stream); + } + else if (scaleCount != 1) + { + normalizeScalarWrap(input, output, inputShape, base.x, MakeWorkVec(scale), + global_scale, shift, stream); + } + else + { + normalizeScalarWrap(input, output, inputShape, base.x, scale.x, global_scale, shift, stream); + } +} + +template +void callNormalizeScalarInvStdDevWrap(const input_wrapper &input, const DataShape &inputShape, float4 base, + float4 scale, int baseCount, int scaleCount, const output_wrapper &output, + float global_scale, float shift, float epsilon, cudaStream_t stream) +{ + using input_type = typename input_wrapper::ValueType; + using work_type = nvcv::cuda::ConvertBaseTypeTo; + + if (baseCount != 1 && scaleCount != 1) + { + normalizeScalarInvStdDevWrap(input, output, inputShape, MakeWorkVec(base), + MakeWorkVec(scale), global_scale, shift, epsilon, + stream); + } + else if (baseCount != 1) + { + normalizeScalarInvStdDevWrap(input, output, inputShape, MakeWorkVec(base), scale.x, + global_scale, shift, epsilon, stream); + } + else if (scaleCount != 1) + { + normalizeScalarInvStdDevWrap(input, output, inputShape, base.x, MakeWorkVec(scale), + global_scale, shift, epsilon, stream); + } + else + { + normalizeScalarInvStdDevWrap(input, output, inputShape, base.x, scale.x, global_scale, shift, + epsilon, stream); + } +} + +template +ErrorCode normalizeScalar(const nvcv::TensorDataStridedCuda &inData, float4 base, float4 scale, int baseCount, + int scaleCount, const nvcv::TensorDataStridedCuda &outData, float global_scale, float shift, + cudaStream_t stream) +{ + auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(inData); + NVCV_ASSERT(inAccess); + + auto outAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(outData); + NVCV_ASSERT(outAccess); + + DataShape inputShape = GetLegacyDataShape(inAccess->infoShape()); + + auto inMaxStride = inAccess->sampleStride() * inAccess->numSamples(); + auto outMaxStride = outAccess->sampleStride() * outAccess->numSamples(); + if (std::max(inMaxStride, outMaxStride) > cuda::TypeTraits::max) + { + LOG_ERROR("Input or output size exceeds " << cuda::TypeTraits::max << ". Tensor is too large."); + return ErrorCode::INVALID_PARAMETER; + } + + auto srcWrap = nvcv::cuda::CreateTensorWrapNHW(inData); + auto dstWrap = nvcv::cuda::CreateTensorWrapNHW(outData); + callNormalizeScalarWrap(srcWrap, inputShape, base, scale, baseCount, scaleCount, dstWrap, global_scale, shift, + stream); + return ErrorCode::SUCCESS; +} + +template +ErrorCode normalizeScalarInvStdDev(const nvcv::TensorDataStridedCuda &inData, float4 base, float4 scale, int baseCount, + int scaleCount, const nvcv::TensorDataStridedCuda &outData, float global_scale, + float shift, float epsilon, cudaStream_t stream) +{ + auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(inData); + NVCV_ASSERT(inAccess); + + auto outAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(outData); + NVCV_ASSERT(outAccess); + + DataShape inputShape = GetLegacyDataShape(inAccess->infoShape()); + + auto inMaxStride = inAccess->sampleStride() * inAccess->numSamples(); + auto outMaxStride = outAccess->sampleStride() * outAccess->numSamples(); + if (std::max(inMaxStride, outMaxStride) > cuda::TypeTraits::max) + { + LOG_ERROR("Input or output size exceeds " << cuda::TypeTraits::max << ". Tensor is too large."); + return ErrorCode::INVALID_PARAMETER; + } + + auto srcWrap = nvcv::cuda::CreateTensorWrapNHW(inData); + auto dstWrap = nvcv::cuda::CreateTensorWrapNHW(outData); + callNormalizeScalarInvStdDevWrap(srcWrap, inputShape, base, scale, baseCount, scaleCount, dstWrap, global_scale, + shift, epsilon, stream); + return ErrorCode::SUCCESS; +} + +// --------------------------------------------------------------------------------------------------- +// Tensor-free (by-value scalar / per-channel parameter) planar path. +// +// The planar kernels read one scalar base/scale per (batch, channel), where the channel is a grid +// dimension -- unlike the interleaved kernels, whose base/scale vector lanes ARE the channels. So the +// by-value planar wrap must be channel-indexed (return &data[channel]) rather than a single broadcast +// value. Launched with param extents {N=1, C=count, H=1, W=1}, PlanarBroadcastIndex maps channel -> 0 +// for a broadcast scalar (count == 1) and channel -> channel for per-channel values (count == C), so +// output is bit-identical to the tensor planar path fed a {1, C, 1, 1} float parameter tensor. Reuses +// the base normalizePlanarKernel (like the interleaved by-value path reuses normalizeKernel) and the +// vectorized planar fast path used by the tensor path. +// --------------------------------------------------------------------------------------------------- +struct PlanarConstParamWrap +{ + using ValueType = float; + + float data[4]; + + inline const __host__ __device__ float *ptr(int, int channel, int, int) const + { + return &data[channel]; + } +}; + +template +ErrorCode normalizeScalarPlanarImpl(const nvcv::TensorDataStridedCuda &inData, float4 base, float4 scale, int baseCount, + int scaleCount, const nvcv::TensorDataStridedCuda &outData, float global_scale, + float shift, bool isStdDev, float epsilon, cudaStream_t stream) +{ + auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(inData); + auto outAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(outData); + NVCV_ASSERT(inAccess && outAccess); + + auto inMaxStride = inAccess->sampleStride() * inAccess->numSamples(); + auto outMaxStride = outAccess->sampleStride() * outAccess->numSamples(); + if (std::max(inMaxStride, outMaxStride) > cuda::TypeTraits::max) + { + LOG_ERROR("Input or output size exceeds " << cuda::TypeTraits::max << ". Tensor is too large."); + return ErrorCode::INVALID_PARAMETER; + } + + auto srcWrap = nvcv::cuda::CreateTensorWrapNCHW(inData); + auto dstWrap = nvcv::cuda::CreateTensorWrapNCHW(outData); + + PlanarConstParamWrap baseWrap{ + {base.x, base.y, base.z, base.w} + }; + PlanarConstParamWrap scaleWrap{ + {scale.x, scale.y, scale.z, scale.w} + }; + + int4 inout_size = {static_cast(inAccess->numSamples()), static_cast(inAccess->numChannels()), + static_cast(inAccess->numRows()), static_cast(inAccess->numCols())}; + int4 base_size = {1, baseCount, 1, 1}; + int4 scale_size = {1, scaleCount, 1, 1}; + + const uint64_t planes = static_cast(inout_size.x) * inout_size.y; + if (planes > 65535u) + { + LOG_ERROR("Planar normalize launch exceeds CUDA grid.z limit: N*C=" << planes); + return ErrorCode::INVALID_PARAMETER; + } + + // Match the tensor path's vectorized planar dispatch. The kernels accept generic parameter + // wrappers, so by-value constants retain the same wide load/store and loads-first ILP behavior. + if constexpr (sizeof(input_type) == 1) + { + const bool aligned = inAccess->rowStride() % 4 == 0 && inAccess->chStride() % 4 == 0 + && inAccess->sampleStride() % 4 == 0 && outAccess->rowStride() % 4 == 0 + && outAccess->chStride() % 4 == 0 && outAccess->sampleStride() % 4 == 0; + if (aligned) + { + constexpr int NGROUP = kNormalizePlanarILPNGroup; + dim3 vblock(32, 8); + dim3 vgrid(divUp(divUp(inout_size.w, 4), static_cast(vblock.x) * NGROUP), + divUp(inout_size.z, static_cast(vblock.y)), static_cast(planes)); + normalizePlanarVec4Kernel + <<>>(srcWrap, baseWrap, scaleWrap, dstWrap, inout_size, base_size, scale_size, + global_scale, shift, epsilon, isStdDev); + checkKernelErrors(); + return ErrorCode::SUCCESS; + } + } + + if constexpr (std::is_same_v) + { + const bool aligned = inAccess->rowStride() % 16 == 0 && inAccess->chStride() % 16 == 0 + && inAccess->sampleStride() % 16 == 0 && outAccess->rowStride() % 16 == 0 + && outAccess->chStride() % 16 == 0 && outAccess->sampleStride() % 16 == 0; + if (aligned) + { + constexpr int NGROUP = kNormalizePlanarF32ILPNGroup; + dim3 vblock(32, 8); + dim3 vgrid(divUp(divUp(inout_size.w, 4), static_cast(vblock.x) * NGROUP), + divUp(inout_size.z, static_cast(vblock.y)), static_cast(planes)); + normalizePlanarVec4Kernel + <<>>(srcWrap, baseWrap, scaleWrap, dstWrap, inout_size, base_size, scale_size, + global_scale, shift, epsilon, isStdDev); + checkKernelErrors(); + return ErrorCode::SUCCESS; + } + } + + dim3 block(32, 8); + dim3 grid(divUp(inout_size.w, block.x), divUp(inout_size.z, block.y), static_cast(planes)); + normalizePlanarKernel<<>>(srcWrap, baseWrap, scaleWrap, dstWrap, inout_size, + base_size, scale_size, global_scale, shift, epsilon, + isStdDev); + checkKernelErrors(); + return ErrorCode::SUCCESS; +} + +template +ErrorCode normalizeScalarPlanar(const nvcv::TensorDataStridedCuda &inData, float4 base, float4 scale, int baseCount, + int scaleCount, const nvcv::TensorDataStridedCuda &outData, float global_scale, + float shift, cudaStream_t stream) +{ + return normalizeScalarPlanarImpl(inData, base, scale, baseCount, scaleCount, outData, global_scale, + shift, false, 0.f, stream); +} + +template +ErrorCode normalizeScalarPlanarInvStdDev(const nvcv::TensorDataStridedCuda &inData, float4 base, float4 scale, + int baseCount, int scaleCount, const nvcv::TensorDataStridedCuda &outData, + float global_scale, float shift, float epsilon, cudaStream_t stream) +{ + return normalizeScalarPlanarImpl(inData, base, scale, baseCount, scaleCount, outData, global_scale, + shift, true, epsilon, stream); +} + namespace nvcv::legacy::cuda_op { -void Normalize::checkParamShape(DataShape input_shape, DataShape param_shape) +bool Normalize::checkParamShape(DataShape input_shape, DataShape param_shape) { - NVCV_ASSERT(param_shape.N == input_shape.N || param_shape.N == 1); - NVCV_ASSERT(param_shape.C == input_shape.C || param_shape.C == 1); - NVCV_ASSERT(param_shape.H == input_shape.H || param_shape.H == 1); - NVCV_ASSERT(param_shape.W == input_shape.W || param_shape.W == 1); + return (param_shape.N == input_shape.N || param_shape.N == 1) + && (param_shape.C == input_shape.C || param_shape.C == 1) + && (param_shape.H == input_shape.H || param_shape.H == 1) + && (param_shape.W == input_shape.W || param_shape.W == 1); } ErrorCode Normalize::infer(const nvcv::TensorDataStridedCuda &inData, const nvcv::TensorDataStridedCuda &baseData, @@ -319,12 +1435,47 @@ ErrorCode Normalize::infer(const nvcv::TensorDataStridedCuda &inData, const nvcv return ErrorCode::INVALID_DATA_FORMAT; } - if (!(format == kNHWC || format == kHWC)) + if (!(format == kNHWC || format == kHWC || format == kNCHW || format == kCHW)) { - LOG_ERROR("Invalid input DataFormat " << format << ", the valid DataFormats are: \"NHWC\", \"HWC\""); + LOG_ERROR("Invalid input DataFormat " << format + << ", the valid DataFormats are: \"NHWC\", \"HWC\", \"NCHW\", \"CHW\""); return ErrorCode::INVALID_DATA_FORMAT; } + const bool isPlanar = (format == kNCHW || format == kCHW); + + if (isPlanar) + { + DataFormat base_format = helpers::GetLegacyDataFormat(baseData); + DataFormat scale_format = helpers::GetLegacyDataFormat(scaleData); + // base/scale must use a planar layout matching the input. NHWC/HWC are still allowed when + // the parameter tensor's channel axis is 1 (i.e. truly scalar), which both layouts express + // identically. + auto isParamCompatible = [&](DataFormat f, const nvcv::TensorDataStridedCuda &data) + { + if (f == kNCHW || f == kCHW) + return true; + if (f == kNHWC || f == kHWC) + { + auto access = TensorDataAccessStridedImagePlanar::Create(data); + return access && access->numChannels() == 1; + } + return false; + }; + if (!isParamCompatible(base_format, baseData)) + { + LOG_ERROR("base DataFormat " << base_format + << " is not compatible with planar input; use NCHW/CHW (or a scalar layout)"); + return ErrorCode::INVALID_DATA_FORMAT; + } + if (!isParamCompatible(scale_format, scaleData)) + { + LOG_ERROR("scale DataFormat " << scale_format + << " is not compatible with planar input; use NCHW/CHW (or a scalar layout)"); + return ErrorCode::INVALID_DATA_FORMAT; + } + } + auto inAccess = TensorDataAccessStridedImagePlanar::Create(inData); if (!inAccess) { @@ -360,7 +1511,7 @@ ErrorCode Normalize::infer(const nvcv::TensorDataStridedCuda &inData, const nvcv int channels = input_shape.C; - if (channels > 4) + if (channels > 4 || channels == 2) { LOG_ERROR("Invalid channel number " << channels); return ErrorCode::INVALID_DATA_SHAPE; @@ -373,8 +1524,19 @@ ErrorCode Normalize::infer(const nvcv::TensorDataStridedCuda &inData, const nvcv return ErrorCode::INVALID_DATA_TYPE; } - checkParamShape(input_shape, base_param_shape); - checkParamShape(input_shape, scale_param_shape); + if (!checkParamShape(input_shape, base_param_shape)) + { + LOG_ERROR("Invalid base shape " << base_param_shape << " for input shape " << input_shape + << "; each dimension must either match the input or be 1"); + return ErrorCode::INVALID_DATA_SHAPE; + } + + if (!checkParamShape(input_shape, scale_param_shape)) + { + LOG_ERROR("Invalid scale shape " << scale_param_shape << " for input shape " << input_shape + << "; each dimension must either match the input or be 1"); + return ErrorCode::INVALID_DATA_SHAPE; + } typedef ErrorCode (*normalize_t)(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &baseData, const TensorDataStridedCuda &scaleData, const TensorDataStridedCuda &outData, @@ -408,8 +1570,65 @@ ErrorCode Normalize::infer(const nvcv::TensorDataStridedCuda &inData, const nvcv normalizeInvStdDev } }; + if (isPlanar) + { + // Planar dispatch indexes by dtype only: the kernel treats each channel as a separate + // plane of scalars, so the channel count (1/3/4) reuses one specialization per dtype. + static const normalize_t funcs_planar[6] = { + normalizePlanar, normalizePlanar, normalizePlanar, + normalizePlanar, normalizePlanar, normalizePlanar, + }; + static const normalizeInvStdDev_t funcs_planar_stddev[6] = { + normalizePlanarInvStdDev, normalizePlanarInvStdDev, normalizePlanarInvStdDev, + normalizePlanarInvStdDev, normalizePlanarInvStdDev, normalizePlanarInvStdDev, + }; + + if (flags & CVCUDA_NORMALIZE_SCALE_IS_STDDEV) + { + return funcs_planar_stddev[data_type](inData, baseData, scaleData, outData, global_scale, shift, epsilon, + stream); + } + return funcs_planar[data_type](inData, baseData, scaleData, outData, global_scale, shift, stream); + } + if (flags & CVCUDA_NORMALIZE_SCALE_IS_STDDEV) { + // Vectorized fast path for the latency-bound single-channel 8-bit case (the scalar kernel + // moves 1 byte/thread). Prefer the 16-wide uint4 path (16 columns/thread, 128-bit transfers) + // when row/sample strides are 16-byte aligned to maximize bytes-in-flight per request; else + // fall back to the 4-wide uchar4 path (4-byte aligned); else the scalar kernel runs. Output is + // bit-identical across all three. + if (data_type == kCV_8U && channels == 1) + { + // 16-byte-aligned single-channel U8 stddev: ILP wide-load kernel (kNormalizeILPNGroup + // independent uint4 groups per thread → more outstanding memory requests / MLP). Falls back + // to the 4-wide uchar4 path (4-byte aligned), else scalar. Output is bit-identical across all. + if (inAccess->rowStride() % 16 == 0 && inAccess->sampleStride() % 16 == 0 + && outAccess->rowStride() % 16 == 0 && outAccess->sampleStride() % 16 == 0) + { + return normalizeInvStdDevU8VecILP(inData, baseData, scaleData, outData, + global_scale, shift, epsilon, stream); + } + if (inAccess->rowStride() % 4 == 0 && inAccess->sampleStride() % 4 == 0 && outAccess->rowStride() % 4 == 0 + && outAccess->sampleStride() % 4 == 0) + { + return normalizeInvStdDevU8Vec(inData, baseData, scaleData, outData, global_scale, shift, epsilon, + stream); + } + } + // Vectorized fast path for the latency-bound single-channel F32 case (the scalar kernel moves + // 4 bytes/thread). Each thread owns kNormalizeF32ILPNGroup independent float4 groups (128-bit + // transfers, loads-first) when row/sample strides are 16-byte aligned, else the scalar kernel + // runs. Output is bit-identical (same float expression, no SaturateCast needed for float out). + if (data_type == kCV_32F && channels == 1) + { + if (inAccess->rowStride() % 16 == 0 && inAccess->sampleStride() % 16 == 0 + && outAccess->rowStride() % 16 == 0 && outAccess->sampleStride() % 16 == 0) + { + return normalizeInvStdDevF32VecILP(inData, baseData, scaleData, outData, + global_scale, shift, epsilon, stream); + } + } return funcs_normalize_stddev[data_type][channels - 1](inData, baseData, scaleData, outData, global_scale, shift, epsilon, stream); } @@ -420,4 +1639,157 @@ ErrorCode Normalize::infer(const nvcv::TensorDataStridedCuda &inData, const nvcv } } +ErrorCode Normalize::infer(const nvcv::TensorDataStridedCuda &inData, const float4 base, const float4 scale, + const int baseCount, const int scaleCount, const nvcv::TensorDataStridedCuda &outData, + const float global_scale, const float shift, const float epsilon, const uint32_t flags, + cudaStream_t stream) +{ + DataFormat format = GetLegacyDataFormat(inData.layout()); + DataFormat output_format = helpers::GetLegacyDataFormat(outData); + if (format != output_format) + { + LOG_ERROR("Invalid DataFormat between input (" << format << ") and output (" << output_format << ")"); + return ErrorCode::INVALID_DATA_FORMAT; + } + + if (!(format == kNHWC || format == kHWC || format == kNCHW || format == kCHW)) + { + LOG_ERROR("By-value (scalar/per-channel) normalize supports NHWC/HWC/NCHW/CHW; got " << format); + return ErrorCode::INVALID_DATA_FORMAT; + } + + const bool isPlanar = (format == kNCHW || format == kCHW); + + auto inAccess = TensorDataAccessStridedImagePlanar::Create(inData); + if (!inAccess) + { + LOG_ERROR("Invalid DataFormat(in) " << format); + return ErrorCode::INVALID_DATA_FORMAT; + } + + auto outAccess = TensorDataAccessStridedImagePlanar::Create(outData); + if (!outAccess) + { + LOG_ERROR("Invalid DataFormat(out) " << format); + return ErrorCode::INVALID_DATA_FORMAT; + } + + DataType data_type = GetLegacyDataType(inData.dtype()); + DataShape input_shape = GetLegacyDataShape(inAccess->infoShape()); + int channels = input_shape.C; + + if (channels > 4 || channels == 2) + { + LOG_ERROR("Invalid channel number " << channels); + return ErrorCode::INVALID_DATA_SHAPE; + } + + if (!(data_type == kCV_8U || data_type == kCV_8S || data_type == kCV_16U || data_type == kCV_16S + || data_type == kCV_32S || data_type == kCV_32F)) + { + LOG_ERROR("Invalid DataType " << data_type); + return ErrorCode::INVALID_DATA_TYPE; + } + + // The public contract (OpNormalize.h Input/Output dependency) requires the output dtype and + // extents to equal the input's; the kernels bounds-check against the input extents only, so a + // mismatched output would be written out of bounds (or with the wrong element type) through its + // wrap. Reject the mismatch up front. + DataType out_data_type = GetLegacyDataType(outData.dtype()); + if (data_type != out_data_type) + { + LOG_ERROR("DataType of input and output must be equal, but got " << data_type << " and " << out_data_type); + return ErrorCode::INVALID_DATA_TYPE; + } + + DataShape output_shape = GetLegacyDataShape(outAccess->infoShape()); + if (input_shape != output_shape) + { + LOG_ERROR("Shape of input and output must be equal, but got " << input_shape << " and " << output_shape); + return ErrorCode::INVALID_DATA_SHAPE; + } + + // Each by-value parameter must be a single scalar (broadcast to all channels) or one value per + // channel; a float4 holds at most 4 lanes so per-channel is bounded by the 1/3/4 channel support. + if (!(baseCount == 1 || baseCount == channels)) + { + LOG_ERROR("Invalid base value count " << baseCount << " for " << channels + << "-channel input; must be 1 (broadcast) or the channel count"); + return ErrorCode::INVALID_DATA_SHAPE; + } + if (!(scaleCount == 1 || scaleCount == channels)) + { + LOG_ERROR("Invalid scale value count " << scaleCount << " for " << channels + << "-channel input; must be 1 (broadcast) or the channel count"); + return ErrorCode::INVALID_DATA_SHAPE; + } + + const uint64_t launchPlanes = static_cast(input_shape.N) * static_cast(isPlanar ? channels : 1); + if (launchPlanes > 65535u) + { + LOG_ERROR("By-value normalize launch exceeds CUDA grid.z limit: " << (isPlanar ? "N*C=" : "N=") + << launchPlanes); + return ErrorCode::INVALID_DATA_SHAPE; + } + + typedef ErrorCode (*normalize_scalar_t)(const TensorDataStridedCuda &inData, float4 base, float4 scale, + int baseCount, int scaleCount, const TensorDataStridedCuda &outData, + float global_scale, float shift, cudaStream_t stream); + + typedef ErrorCode (*normalize_scalar_stddev_t)(const TensorDataStridedCuda &inData, float4 base, float4 scale, + int baseCount, int scaleCount, const TensorDataStridedCuda &outData, + float global_scale, float shift, float epsilon, cudaStream_t stream); + + if (isPlanar) + { + // Planar dispatch indexes by dtype only: the kernel treats each channel as a separate plane of + // scalars, so the channel count (1/3/4) reuses one specialization per dtype (mirrors funcs_planar + // in the tensor path). + static const normalize_scalar_t funcs_normalize_scalar_planar[6] = { + normalizeScalarPlanar, normalizeScalarPlanar, normalizeScalarPlanar, + normalizeScalarPlanar, normalizeScalarPlanar, normalizeScalarPlanar, + }; + static const normalize_scalar_stddev_t funcs_normalize_scalar_planar_stddev[6] = { + normalizeScalarPlanarInvStdDev, normalizeScalarPlanarInvStdDev, + normalizeScalarPlanarInvStdDev, normalizeScalarPlanarInvStdDev, + normalizeScalarPlanarInvStdDev, normalizeScalarPlanarInvStdDev, + }; + + if (flags & CVCUDA_NORMALIZE_SCALE_IS_STDDEV) + { + return funcs_normalize_scalar_planar_stddev[data_type](inData, base, scale, baseCount, scaleCount, outData, + global_scale, shift, epsilon, stream); + } + return funcs_normalize_scalar_planar[data_type](inData, base, scale, baseCount, scaleCount, outData, + global_scale, shift, stream); + } + + // Same [dtype][channels-1] layout as funcs_normalize; the channels==2 slot is unused (rejected above). + static const normalize_scalar_t funcs_normalize_scalar[6][4] = { + { normalizeScalar, 0, normalizeScalar, normalizeScalar}, + { normalizeScalar, 0, normalizeScalar, normalizeScalar}, + {normalizeScalar, 0, normalizeScalar, normalizeScalar}, + { normalizeScalar, 0, normalizeScalar, normalizeScalar}, + { normalizeScalar, 0, normalizeScalar, normalizeScalar}, + { normalizeScalar, 0, normalizeScalar, normalizeScalar} + }; + + static const normalize_scalar_stddev_t funcs_normalize_scalar_stddev[6][4] = { + { normalizeScalarInvStdDev, 0, normalizeScalarInvStdDev, normalizeScalarInvStdDev}, + { normalizeScalarInvStdDev, 0, normalizeScalarInvStdDev, normalizeScalarInvStdDev}, + {normalizeScalarInvStdDev, 0, normalizeScalarInvStdDev, normalizeScalarInvStdDev}, + { normalizeScalarInvStdDev, 0, normalizeScalarInvStdDev, normalizeScalarInvStdDev}, + { normalizeScalarInvStdDev, 0, normalizeScalarInvStdDev, normalizeScalarInvStdDev}, + { normalizeScalarInvStdDev, 0, normalizeScalarInvStdDev, normalizeScalarInvStdDev} + }; + + if (flags & CVCUDA_NORMALIZE_SCALE_IS_STDDEV) + { + return funcs_normalize_scalar_stddev[data_type][channels - 1](inData, base, scale, baseCount, scaleCount, + outData, global_scale, shift, epsilon, stream); + } + return funcs_normalize_scalar[data_type][channels - 1](inData, base, scale, baseCount, scaleCount, outData, + global_scale, shift, stream); +} + } // namespace nvcv::legacy::cuda_op diff --git a/src/cvcuda/priv/legacy/normalize_planar.cuh b/src/cvcuda/priv/legacy/normalize_planar.cuh new file mode 100644 index 000000000..7ba23e95b --- /dev/null +++ b/src/cvcuda/priv/legacy/normalize_planar.cuh @@ -0,0 +1,159 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#ifndef CVCUDA_PRIV_LEGACY_NORMALIZE_PLANAR_CUH +#define CVCUDA_PRIV_LEGACY_NORMALIZE_PLANAR_CUH + +#include // for sqrt +#include // for SaturateCast + +#include + +// Shared device helpers for the planar (NCHW / CHW) Normalize kernels. Both the tensor +// (normalize.cu) and image-batch var-shape (normalize_var_shape.cu) paths index base/scale the +// same way and apply the same per-element formula, so the logic lives here once. +namespace nvcv::legacy::cuda_op { + +// Per-axis broadcast read index for a parameter with logical (N, C, H, W) extents in param_size: +// use 0 wherever the extent is 1 (broadcast over that axis), otherwise the data index. +__device__ __forceinline__ int4 PlanarBroadcastIndex(int4 param_size, int batch, int channel, int y, int x) +{ + return int4{param_size.x == 1 ? 0 : batch, param_size.y == 1 ? 0 : channel, param_size.z == 1 ? 0 : y, + param_size.w == 1 ? 0 : x}; +} + +// Per-element planar normalize: out = saturate((src - base) * mul * global_scale + global_shift), +// where mul is the raw scale, or 1 / sqrt(scale^2 + epsilon) when is_stddev (the +// CVCUDA_NORMALIZE_SCALE_IS_STDDEV flag). base/scale are always float; OutT is the output type. +template +__device__ __forceinline__ OutT ApplyPlanarNormalize(SrcT src_val, float base_val, float scale_val, float global_scale, + float global_shift, bool is_stddev, float epsilon) +{ + const float mul = is_stddev ? (1.0f / nvcv::cuda::sqrt(scale_val * scale_val + epsilon)) : scale_val; + return nvcv::cuda::SaturateCast((static_cast(src_val) - base_val) * mul * global_scale + global_shift); +} + +// Width of one vectorized planar group: always 4 columns/thread, but the load/store width depends on +// the element size -- a char4/uchar4 (4 bytes) for 1-byte planes, a float4 (16 bytes) for 4-byte float +// planes. PlanarVec4Type selects the 4-wide vector type while preserving signed byte semantics. +template +struct PlanarVec4Type; + +template +struct PlanarVec4Type +{ + using type = std::conditional_t, char4, uchar4>; +}; + +template +struct PlanarVec4Type +{ + using type = float4; +}; + +// Vectorized (4 columns/thread) planar normalize body shared by the tensor and var-shape kernels and +// across element widths. The scalar planar kernels move one element per thread, leaving the planar +// paths latency-bound (1-byte ~30-36% BWUtil, F32 single-channel similarly). Here one thread owns four +// consecutive columns of plane (batch, channel) and moves them with a single 4-wide vector load/store +// (char4/uchar4 for 1-byte planes -> 32-bit transfer; float4 for F32 planes -> 128-bit transfer), so a warp +// transfers a full coalesced line. When base/scale are broadcast across those columns (base_size.w == +// 1 && scale_size.w == 1, the common per-channel case) the base and the inverse-std-dev multiplier are +// computed once. Output is bit-identical to ApplyPlanarNormalize per element. +// +// Caller contract: T is a 1-byte or 4-byte float type, both src and dst plane/row/sample strides are +// sizeof(vec4)-byte aligned, and `cx` is a multiple of 4. A width that is not a multiple of 4 is +// handled scalar for the tail. SrcWrap/DstWrap/ParamWrap expose ptr(batch, channel, y, x); src/dst are +// T, params are float. +template +__device__ __forceinline__ void NormalizePlanarVec4(const SrcWrap &src, const DstWrap &dst, const ParamWrap &base, + const ParamWrap &scale, int batch, int channel, int src_y, int cx, + int4 inout_size, int4 base_size, int4 scale_size, + float global_scale, float global_shift, bool is_stddev, + float epsilon) +{ + using Vec4 = typename PlanarVec4Type::type; + + // The 1-byte vector load needs only 4-byte alignment, which every NVCV row pitch satisfies. The + // float4 load needs 16-byte alignment; default-allocated planes satisfy it (texture-pitch row + // alignment, texture-aligned base), but a user-wrapped buffer could pick a row pitch that is a + // multiple of 4 yet not 16. Guard the wide path on the actual row-base alignment and fall back to + // the scalar tail loop otherwise, so the kernel stays correct for any aligned-or-not input. + const bool vecAligned = sizeof(T) == 1 + || (reinterpret_cast(src.ptr(batch, channel, src_y, cx)) % sizeof(Vec4) == 0 + && reinterpret_cast(dst.ptr(batch, channel, src_y, cx)) % sizeof(Vec4) == 0); + + if (cx + 4 <= inout_size.w && vecAligned) + { + const Vec4 in4 = *reinterpret_cast(src.ptr(batch, channel, src_y, cx)); + Vec4 out4; + if (base_size.w == 1 && scale_size.w == 1) + { + const int4 b = PlanarBroadcastIndex(base_size, batch, channel, src_y, 0); + const int4 s = PlanarBroadcastIndex(scale_size, batch, channel, src_y, 0); + const float baseV = *base.ptr(b.x, b.y, b.z, b.w); + const float scaleV = *scale.ptr(s.x, s.y, s.z, s.w); + const float mul = is_stddev ? (1.0f / nvcv::cuda::sqrt(scaleV * scaleV + epsilon)) : scaleV; + + out4.x + = nvcv::cuda::SaturateCast((static_cast(in4.x) - baseV) * mul * global_scale + global_shift); + out4.y + = nvcv::cuda::SaturateCast((static_cast(in4.y) - baseV) * mul * global_scale + global_shift); + out4.z + = nvcv::cuda::SaturateCast((static_cast(in4.z) - baseV) * mul * global_scale + global_shift); + out4.w + = nvcv::cuda::SaturateCast((static_cast(in4.w) - baseV) * mul * global_scale + global_shift); + } + else + { + const T raw[4] + = {static_cast(in4.x), static_cast(in4.y), static_cast(in4.z), static_cast(in4.w)}; + T res[4]; + for (int i = 0; i < 4; ++i) + { + const int x = cx + i; + const int4 b = PlanarBroadcastIndex(base_size, batch, channel, src_y, x); + const int4 s = PlanarBroadcastIndex(scale_size, batch, channel, src_y, x); + res[i] = ApplyPlanarNormalize(raw[i], *base.ptr(b.x, b.y, b.z, b.w), *scale.ptr(s.x, s.y, s.z, s.w), + global_scale, global_shift, is_stddev, epsilon); + } + out4.x = res[0]; + out4.y = res[1]; + out4.z = res[2]; + out4.w = res[3]; + } + *reinterpret_cast(dst.ptr(batch, channel, src_y, cx)) = out4; + } + else + { + // Scalar path for this thread's group: either the trailing partial group (cx+4 > width) or an + // unaligned wide access. Bound to this group's 4 columns (or fewer at the row end) so threads + // do not overlap; for a partial group cx+4 already exceeds width so the bound clamps to width. + const int xEnd = cx + 4 < inout_size.w ? cx + 4 : inout_size.w; + for (int x = cx; x < xEnd; ++x) + { + const int4 b = PlanarBroadcastIndex(base_size, batch, channel, src_y, x); + const int4 s = PlanarBroadcastIndex(scale_size, batch, channel, src_y, x); + *dst.ptr(batch, channel, src_y, x) = ApplyPlanarNormalize( + *src.ptr(batch, channel, src_y, x), *base.ptr(b.x, b.y, b.z, b.w), *scale.ptr(s.x, s.y, s.z, s.w), + global_scale, global_shift, is_stddev, epsilon); + } + } +} + +} // namespace nvcv::legacy::cuda_op + +#endif // CVCUDA_PRIV_LEGACY_NORMALIZE_PLANAR_CUH diff --git a/src/cvcuda/priv/legacy/normalize_var_shape.cu b/src/cvcuda/priv/legacy/normalize_var_shape.cu index e974b6c0a..f18291c32 100644 --- a/src/cvcuda/priv/legacy/normalize_var_shape.cu +++ b/src/cvcuda/priv/legacy/normalize_var_shape.cu @@ -22,9 +22,15 @@ #include "CvCudaLegacyHelpers.hpp" #include "CvCudaUtils.cuh" +#include "normalize_planar.cuh" // for PlanarBroadcastIndex, ApplyPlanarNormalize #include // for CVCUDA_NORMALIZE_SCALE_IS_STDDEV, etc. #include +#include // for TypeTraits + +#include +#include +#include namespace nvcv::legacy::cuda_op { @@ -32,6 +38,23 @@ namespace { #define BLOCK 32 +// ILP depth used by the single-channel 1-byte inverse-std-dev var-shape wide-load path: each thread +// issues this many independent uchar4 loads before computing, raising the number of outstanding +// memory requests per resident thread (memory-level parallelism) at fixed occupancy. Matches the +// tensor-path knob; NGROUP=4 measured the best long-scoreboard-stall reduction without register +// pressure cutting occupancy. +static constexpr int kNormalizeILPNGroup = 4; + +// ILP depth used by the single-channel F32 inverse-std-dev var-shape wide-load path. The scalar F32 +// var-shape kernel moves one float (4 bytes) per thread, leaving the single-channel F32 path +// latency-bound (too few bytes in flight per thread to hide load latency). Here each thread owns +// NGROUP independent float4 groups (4*NGROUP consecutive columns) and issues all NGROUP loads before +// any compute, raising the number of outstanding memory requests per resident thread (memory-level +// parallelism) at fixed occupancy. NGROUP=4 matches the single-channel U8 var-shape value; a float4 +// group is 16 bytes of live register state, so if registers balloon and cut occupancy this is the +// knob to drop to 2. +static constexpr int kNormalizeF32ILPNGroup = 4; + // (float3 - float3) * float3 / (float3 - float) * float3 / (float3 - float3) * float / (float3 - float) * float template __global__ void normKernel(const cuda::ImageBatchVarShapeWrap src, cuda::ImageBatchVarShapeWrap dst, @@ -67,6 +90,291 @@ __global__ void normInvStdDevKernel(const cuda::ImageBatchVarShapeWrap *dst.ptr(batch_idx, dst_y, dst_x) = cuda::SaturateCast((out - *base) * mul * global_scale + global_shift); } +// Vectorized 1-byte single-channel interleaved (NHWC) var-shape inverse-std-dev kernel: one thread +// owns NGROUP uchar4 groups (4*NGROUP pixels) of an image row, each moved with a single uchar4 +// load/store, so a warp transfers full 128-byte lines. base/scale are scalar for single-channel +// input, so the inverse-std-dev multiplier is computed once per thread. The NGROUP uchar4 loads are +// issued up front (loads-first ILP) so several memory requests overlap per resident thread, raising +// memory-level parallelism at fixed occupancy on the latency-bound 1-byte path. Bit-identical to +// normInvStdDevKernel per element; columns past the last full uchar4 group stay scalar. Relies on +// NVCV's image row-pitch alignment (>= 4 bytes). +template +__global__ void normInvStdDevVec4Kernel(const cuda::ImageBatchVarShapeWrap src, + cuda::ImageBatchVarShapeWrap dst, const scale_type *scale, + const base_type *base, float global_scale, float global_shift, float epsilon) +{ + const int g0 = blockIdx.x * blockDim.x * NGROUP + threadIdx.x; + const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + + const int width = dst.width(batch_idx); + const int cx0 = g0 * 4; + if (cx0 >= width || dst_y >= dst.height(batch_idx)) + return; + + const scale_type s = *scale; + const scale_type mul = 1.0f / cuda::sqrt(s * s + epsilon); + const base_type b = *base; + + auto apply = [&](uchar raw) -> out_T + { return cuda::SaturateCast((raw - b) * mul * global_scale + global_shift); }; + + // Loads first: issue all NGROUP uchar4 loads (for full in-range groups) before any compute. + int cx[NGROUP]; + bool full[NGROUP]; + uchar4 in4[NGROUP]; +#pragma unroll + for (int i = 0; i < NGROUP; ++i) + { + cx[i] = (g0 + i * blockDim.x) * 4; + full[i] = cx[i] + 4 <= width; + if (full[i]) + in4[i] = *reinterpret_cast(src.ptr(batch_idx, dst_y, cx[i])); + } + + // Compute then store. Groups that are not a full uchar4 (the trailing columns) fall back to scalar. +#pragma unroll + for (int i = 0; i < NGROUP; ++i) + { + if (full[i]) + { + uchar4 out4; + out4.x = apply(in4[i].x); + out4.y = apply(in4[i].y); + out4.z = apply(in4[i].z); + out4.w = apply(in4[i].w); + *reinterpret_cast(dst.ptr(batch_idx, dst_y, cx[i])) = out4; + } + else if (cx[i] < width) + { + for (int x = cx[i]; x < width; ++x) + { + *dst.ptr(batch_idx, dst_y, x) = apply(*src.ptr(batch_idx, dst_y, x)); + } + } + } +} + +// Vectorized single-channel F32 interleaved (NHWC) var-shape inverse-std-dev kernel: one thread owns +// NGROUP independent float4 groups (4*NGROUP columns) of an image row, each moved with a single 128-bit +// float4 load/store. base/scale are scalar for single-channel input, so the inverse-std-dev multiplier +// is computed once per thread. All NGROUP float4 loads are issued up front (loads-first ILP) so several +// memory requests overlap per resident thread, raising memory-level parallelism at fixed occupancy on +// the latency-bound single-channel F32 path. The per-element math matches normInvStdDevKernel (same FMA +// expression; F32 output needs no SaturateCast since the value is already float), so output is +// bit-for-bit unchanged. Relies on NVCV's image row-pitch alignment (>= 16 bytes, caller-guarded); +// columns past the last full float4 group stay scalar. +template +__global__ void normInvStdDevF32Vec4Kernel(const cuda::ImageBatchVarShapeWrap src, + cuda::ImageBatchVarShapeWrap dst, const scale_type *scale, + const base_type *base, float global_scale, float global_shift, float epsilon) +{ + const int g0 = blockIdx.x * blockDim.x * NGROUP + threadIdx.x; + const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + + const int width = dst.width(batch_idx); + const int cx0 = g0 * 4; + if (cx0 >= width || dst_y >= dst.height(batch_idx)) + return; + + const scale_type s = *scale; + const scale_type mul = 1.0f / cuda::sqrt(s * s + epsilon); + const base_type b = *base; + + auto apply = [&](float raw) -> out_T { return (raw - b) * mul * global_scale + global_shift; }; + + // Loads first: issue all NGROUP float4 loads (for full in-range groups) before any compute. + int cx[NGROUP]; + bool full[NGROUP]; + float4 in4[NGROUP]; +#pragma unroll + for (int i = 0; i < NGROUP; ++i) + { + cx[i] = (g0 + i * blockDim.x) * 4; + full[i] = cx[i] + 4 <= width; + if (full[i]) + in4[i] = *reinterpret_cast(src.ptr(batch_idx, dst_y, cx[i])); + } + + // Compute then store. Groups that are not a full float4 (the trailing columns) fall back to scalar. +#pragma unroll + for (int i = 0; i < NGROUP; ++i) + { + if (full[i]) + { + float4 out4; + out4.x = apply(in4[i].x); + out4.y = apply(in4[i].y); + out4.z = apply(in4[i].z); + out4.w = apply(in4[i].w); + *reinterpret_cast(dst.ptr(batch_idx, dst_y, cx[i])) = out4; + } + else if (cx[i] < width) + { + for (int x = cx[i]; x < width; ++x) + { + *dst.ptr(batch_idx, dst_y, x) = apply(*src.ptr(batch_idx, dst_y, x)); + } + } + } +} + +// Inverse-std-dev interleaved (NHWC) var-shape kernel processing NIX pixels per thread. base/scale +// are per-channel constants for the whole batch (single pointer deref), so the multiplier +// mul = 1 / sqrt(scale^2 + eps) is identical for every pixel; the scalar normInvStdDevKernel +// recomputes it per pixel, which makes the multi-channel paths (uchar3 / uchar4) issue-bound on the +// redundant sqrt rather than memory-bound. Here each thread computes mul once and applies it to NIX +// pixels strided by blockDim.x so each warp iteration stays coalesced. Bit-identical to +// normInvStdDevKernel (same per-element arithmetic; only the multiplier hoists out of the loop). +template +__global__ void normInvStdDevHoistKernel(const cuda::ImageBatchVarShapeWrap src, + cuda::ImageBatchVarShapeWrap dst, const scale_type *scale, + const base_type *base, float global_scale, float global_shift, float epsilon) +{ + const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + + const int width = dst.width(batch_idx); + if (dst_y >= dst.height(batch_idx)) + return; + + const base_type b = *base; + const scale_type s = *scale; + const scale_type x = s * s + epsilon; + const scale_type mul = 1.0f / cuda::sqrt(x); + + const int x0 = blockIdx.x * blockDim.x * NIX + threadIdx.x; +#pragma unroll + for (int i = 0; i < NIX; ++i) + { + const int dst_x = x0 + i * blockDim.x; + if (dst_x < width) + { + *dst.ptr(batch_idx, dst_y, dst_x) + = cuda::SaturateCast((*src.ptr(batch_idx, dst_y, dst_x) - b) * mul * global_scale + global_shift); + } + } +} + +// Planar (NCHW / CHW) var-shape kernel: src/dst are per-image planar wraps indexed (n, c, y, x); +// base/scale are 4D scalar wraps broadcast per their logical (N, C, H, W) shape. is_stddev selects +// the inverse-stddev formula; the per-element math is shared with the tensor path. +template +__global__ void normPlanarKernel(const cuda::ImageBatchVarShapeWrap src, + cuda::ImageBatchVarShapeWrap dst, + const cuda::Tensor4DWrap base, + const cuda::Tensor4DWrap scale, int num_channels, int4 base_size, + int4 scale_size, float global_scale, float global_shift, float epsilon, + bool is_stddev) +{ + const int dst_x = blockIdx.x * blockDim.x + threadIdx.x; + const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int nc = blockIdx.z; + const int batch_idx = nc / num_channels; + const int channel = nc % num_channels; + + if (dst_x >= dst.width(batch_idx, channel) || dst_y >= dst.height(batch_idx, channel)) + return; + + const int4 b = PlanarBroadcastIndex(base_size, batch_idx, channel, dst_y, dst_x); + const int4 s = PlanarBroadcastIndex(scale_size, batch_idx, channel, dst_y, dst_x); + + *dst.ptr(batch_idx, channel, dst_y, dst_x) + = ApplyPlanarNormalize(*src.ptr(batch_idx, channel, dst_y, dst_x), *base.ptr(b.x, b.y, b.z, b.w), + *scale.ptr(s.x, s.y, s.z, s.w), global_scale, global_shift, is_stddev, epsilon); +} + +// Vectorized 1-byte planar var-shape kernel: one thread per four columns of plane (batch, channel), +// via the shared uchar4 body. Bit-identical to normPlanarKernel. Relies on NVCV's image row-pitch +// alignment (>= 4 bytes) so the uchar4 access is aligned; the existing planar var-shape tests +// (RGB8p / RGBA8p) validate correctness, including the W%4 scalar tail. +template +__global__ void normPlanarVec4Kernel(const cuda::ImageBatchVarShapeWrap src, + cuda::ImageBatchVarShapeWrap dst, + const cuda::Tensor4DWrap base, + const cuda::Tensor4DWrap scale, int num_channels, int4 base_size, + int4 scale_size, float global_scale, float global_shift, float epsilon, + bool is_stddev) +{ + const int cx = (blockIdx.x * blockDim.x + threadIdx.x) * 4; + const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int nc = blockIdx.z; + const int batch_idx = nc / num_channels; + const int channel = nc % num_channels; + + const int width = dst.width(batch_idx, channel); + if (cx >= width || dst_y >= dst.height(batch_idx, channel)) + return; + + // The shared body reads only inout_size.w (the per-image width) for the vector/tail split. + const int4 inout_size = {0, 0, 0, width}; + NormalizePlanarVec4(src, dst, base, scale, batch_idx, channel, dst_y, cx, inout_size, base_size, scale_size, + global_scale, global_shift, is_stddev, epsilon); +} + +// Vectorized 1-byte planar var-shape kernel for spatially-broadcast base/scale (the common +// per-channel case), processing NGROUP uchar4 groups (NGROUP*4 columns) per thread. normPlanarVec4Kernel +// above moves only 4 bytes per thread (one channel), so the fixed per-thread cost -- the per-element +// multiplier and, more importantly, the var-shape ImageBatchVarShapeWrap::ptr metadata lookup -- is +// amortized over 3x less data than the interleaved path, leaving the kernel co-limited by compute +// (~76% memory and ~76% compute SOL). Here each thread resolves base/scale and mul once and the plane +// row base pointer once (a single var-shape lookup; plane columns are contiguous), then strides across +// NGROUP coalesced uchar4 groups. Output is bit-identical to the broadcast path of NormalizePlanarVec4. +template +__global__ void normPlanarVec4HoistKernel(const cuda::ImageBatchVarShapeWrap src, + cuda::ImageBatchVarShapeWrap dst, + const cuda::Tensor4DWrap base, + const cuda::Tensor4DWrap scale, int num_channels, + int4 base_size, int4 scale_size, float global_scale, float global_shift, + float epsilon, bool is_stddev) +{ + const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int nc = blockIdx.z; + const int batch_idx = nc / num_channels; + const int channel = nc % num_channels; + + const int width = dst.width(batch_idx, channel); + if (dst_y >= dst.height(batch_idx, channel)) + return; + + // base/scale are broadcast across H and W here (caller-guarded), so read them once per thread. + const int4 b = PlanarBroadcastIndex(base_size, batch_idx, channel, 0, 0); + const int4 s = PlanarBroadcastIndex(scale_size, batch_idx, channel, 0, 0); + const float baseV = *base.ptr(b.x, b.y, b.z, b.w); + const float scaleV = *scale.ptr(s.x, s.y, s.z, s.w); + const float mul = is_stddev ? (1.0f / nvcv::cuda::sqrt(scaleV * scaleV + epsilon)) : scaleV; + + // One var-shape lookup per thread; plane columns are contiguous, so groups index off the row base. + const T *const rowSrc = src.ptr(batch_idx, channel, dst_y, 0); + out_T *const rowDst = dst.ptr(batch_idx, channel, dst_y, 0); + + const int g0 = blockIdx.x * blockDim.x * NGROUP + threadIdx.x; +#pragma unroll + for (int i = 0; i < NGROUP; ++i) + { + const int cx = (g0 + i * blockDim.x) * 4; + if (cx + 4 <= width) + { + const uchar4 in4 = *reinterpret_cast(rowSrc + cx); + uchar4 out4; + out4.x = cuda::SaturateCast((static_cast(in4.x) - baseV) * mul * global_scale + global_shift); + out4.y = cuda::SaturateCast((static_cast(in4.y) - baseV) * mul * global_scale + global_shift); + out4.z = cuda::SaturateCast((static_cast(in4.z) - baseV) * mul * global_scale + global_shift); + out4.w = cuda::SaturateCast((static_cast(in4.w) - baseV) * mul * global_scale + global_shift); + *reinterpret_cast(rowDst + cx) = out4; + } + else if (cx < width) + { + for (int x = cx; x < width; ++x) + { + rowDst[x] = cuda::SaturateCast((static_cast(rowSrc[x]) - baseV) * mul * global_scale + + global_shift); + } + } + } +} + template void normWrap(const ImageBatchVarShapeDataStridedCuda &in, const base_type *base, const scale_type *scale, const ImageBatchVarShapeDataStridedCuda &out, float global_scale, float shift, cudaStream_t stream) @@ -98,11 +406,179 @@ void normInvStdDevWrap(const ImageBatchVarShapeDataStridedCuda &in, const base_t cuda::ImageBatchVarShapeWrap src_ptr(in); cuda::ImageBatchVarShapeWrap dst_ptr(out); + + // Single-channel 1-byte input goes through the vectorized (4 pixels/thread) path; sizeof(T) == 1 + // implies one channel here, so base/scale are scalar. NVCV row-pitch alignment makes the uchar4 + // access aligned; the W%4 tail is handled scalar. + if constexpr (sizeof(T) == 1 && sizeof(out_T) == 1) + { + constexpr int NGROUP = kNormalizeILPNGroup; + // Each thread covers NGROUP uchar4 groups (4*NGROUP columns) with loads-first ILP. + dim3 vgrid(divUp(divUp(max_width, 4), static_cast(block.x) * NGROUP), + divUp(max_height, static_cast(block.y)), batch); + normInvStdDevVec4Kernel + <<>>(src_ptr, dst_ptr, scale, base, global_scale, shift, epsilon); + checkKernelErrors(); + return; + } + + // Single-channel F32 input goes through the vectorized (4*NGROUP columns/thread, 128-bit float4 + // transfers, loads-first ILP) path; T == float implies one channel here, so base/scale are scalar. + // NVCV image row pitch is aligned to >= 256 bytes and float4 column offsets are multiples of 16 + // bytes, so the float4 access is aligned; the W%4 tail is handled scalar. Output is bit-identical to + // normInvStdDevKernel (same float expression, no SaturateCast needed for float out). + if constexpr (std::is_same_v && std::is_same_v) + { + constexpr int NGROUP = kNormalizeF32ILPNGroup; + // Each thread covers NGROUP float4 groups (4*NGROUP columns) with loads-first ILP. + dim3 vgrid(divUp(divUp(max_width, 4), static_cast(block.x) * NGROUP), + divUp(max_height, static_cast(block.y)), batch); + normInvStdDevF32Vec4Kernel + <<>>(src_ptr, dst_ptr, scale, base, global_scale, shift, epsilon); + checkKernelErrors(); + return; + } + + // Multi-channel 1-byte interleaved input (uchar3 / uchar4) is issue-bound on the redundant + // per-pixel sqrt; hoist the multiplier and process NIX pixels per thread. float3 keeps the scalar + // kernel since it is already memory-bound. + if constexpr (sizeof(cuda::BaseType) == 1) + { + constexpr int NIX = 4; + dim3 hgrid(divUp(max_width, static_cast(block.x) * NIX), divUp(max_height, static_cast(block.y)), + batch); + normInvStdDevHoistKernel + <<>>(src_ptr, dst_ptr, scale, base, global_scale, shift, epsilon); + checkKernelErrors(); + return; + } + normInvStdDevKernel <<>>(src_ptr, dst_ptr, scale, base, global_scale, shift, epsilon); checkKernelErrors(); } +// Launch path for the planar varshape variants; isStdDev/epsilon are forwarded to the unified +// kernel (epsilon is unused when isStdDev is false). +template +ErrorCode normPlanarImpl(const ImageBatchVarShapeDataStridedCuda &in, const TensorDataStridedCuda &baseData, + const TensorDataStridedCuda &scaleData, const ImageBatchVarShapeDataStridedCuda &out, + int num_channels, float global_scale, float shift, bool isStdDev, float epsilon, + cudaStream_t stream) +{ + auto baseAccess = TensorDataAccessStridedImagePlanar::Create(baseData); + NVCV_ASSERT(baseAccess); + + auto scaleAccess = TensorDataAccessStridedImagePlanar::Create(scaleData); + NVCV_ASSERT(scaleAccess); + + auto baseMaxStride = baseAccess->sampleStride() * baseAccess->numSamples(); + auto scaleMaxStride = scaleAccess->sampleStride() * scaleAccess->numSamples(); + if (std::max(baseMaxStride, scaleMaxStride) > cuda::TypeTraits::max) + { + LOG_ERROR("Base or scale size exceeds " << cuda::TypeTraits::max << ". Tensor is too large."); + return ErrorCode::INVALID_PARAMETER; + } + + int max_width = in.maxSize().w; + int max_height = in.maxSize().h; + int batch = in.numImages(); + + int4 base_size = {static_cast(baseAccess->numSamples()), static_cast(baseAccess->numChannels()), + static_cast(baseAccess->numRows()), static_cast(baseAccess->numCols())}; + int4 scale_size = {static_cast(scaleAccess->numSamples()), static_cast(scaleAccess->numChannels()), + static_cast(scaleAccess->numRows()), static_cast(scaleAccess->numCols())}; + + const uint64_t planes = static_cast(batch) * num_channels; + if (planes > 65535u) + { + LOG_ERROR("Planar normalize launch exceeds CUDA grid.z limit: N*C=" << planes); + return ErrorCode::INVALID_PARAMETER; + } + + dim3 block(BLOCK, BLOCK / 4, 1); + dim3 grid(divUp(max_width, block.x), divUp(max_height, block.y), static_cast(planes)); + + cuda::ImageBatchVarShapeWrap src_ptr(in); + cuda::ImageBatchVarShapeWrap dst_ptr(out); + auto baseWrap = cuda::CreateTensorWrapNCHW(baseData); + auto scaleWrap = cuda::CreateTensorWrapNCHW(scaleData); + + // 1-byte planes go through the vectorized (4 columns/thread) body; NVCV row-pitch alignment + // (>= 4 bytes) guarantees the uchar4 access is aligned, and the W%4 tail is handled scalar. + if constexpr (sizeof(T) == 1 && sizeof(out_T) == 1) + { + dim3 vblock(BLOCK, BLOCK / 4, 1); + + // When base/scale are broadcast across H and W (the common per-channel case) each thread can + // resolve them and the plane row pointer once, then process several uchar4 groups -- amortizing + // the var-shape lookup over more data so the kernel reaches memory-bound throughput. + const bool spatialBroadcast + = base_size.z == 1 && base_size.w == 1 && scale_size.z == 1 && scale_size.w == 1; + if (spatialBroadcast) + { + constexpr int NGROUP = 4; + dim3 hgrid(divUp(divUp(max_width, 4), static_cast(vblock.x) * NGROUP), + divUp(max_height, static_cast(vblock.y)), static_cast(planes)); + normPlanarVec4HoistKernel<<>>( + src_ptr, dst_ptr, baseWrap, scaleWrap, num_channels, base_size, scale_size, global_scale, shift, + epsilon, isStdDev); + checkKernelErrors(); + return ErrorCode::SUCCESS; + } + + dim3 vgrid(divUp(divUp(max_width, 4), static_cast(vblock.x)), divUp(max_height, static_cast(vblock.y)), + static_cast(planes)); + normPlanarVec4Kernel<<>>(src_ptr, dst_ptr, baseWrap, scaleWrap, + num_channels, base_size, scale_size, global_scale, + shift, epsilon, isStdDev); + checkKernelErrors(); + return ErrorCode::SUCCESS; + } + + // F32 planes (float in, float out) go through the same vectorized body but with a 128-bit float4 + // load/store per thread (4 columns). Plain float4 vectorization only -- no loads-first ILP here, + // since ILP regressed the 1-byte planar var-shape path. The shared body checks the per-row 16-byte + // alignment of the float4 access and falls back to a scalar group for any unaligned plane, so + // user-wrapped buffers with a sub-16-byte row pitch stay correct; the W%4 tail is handled scalar. + // Bit-identical to normPlanarKernel. + if constexpr (std::is_same_v && std::is_same_v) + { + dim3 vblock(BLOCK, BLOCK / 4, 1); + dim3 vgrid(divUp(divUp(max_width, 4), static_cast(vblock.x)), + divUp(max_height, static_cast(vblock.y)), static_cast(planes)); + normPlanarVec4Kernel<<>>(src_ptr, dst_ptr, baseWrap, scaleWrap, + num_channels, base_size, scale_size, global_scale, + shift, epsilon, isStdDev); + checkKernelErrors(); + return ErrorCode::SUCCESS; + } + + normPlanarKernel<<>>(src_ptr, dst_ptr, baseWrap, scaleWrap, num_channels, + base_size, scale_size, global_scale, shift, epsilon, + isStdDev); + checkKernelErrors(); + return ErrorCode::SUCCESS; +} + +template +ErrorCode normPlanar(const ImageBatchVarShapeDataStridedCuda &in, const TensorDataStridedCuda &baseData, + const TensorDataStridedCuda &scaleData, const ImageBatchVarShapeDataStridedCuda &out, + int num_channels, float global_scale, float shift, cudaStream_t stream) +{ + return normPlanarImpl(in, baseData, scaleData, out, num_channels, global_scale, shift, false, 0.f, + stream); +} + +template +ErrorCode normPlanarInvStdDev(const ImageBatchVarShapeDataStridedCuda &in, const TensorDataStridedCuda &baseData, + const TensorDataStridedCuda &scaleData, const ImageBatchVarShapeDataStridedCuda &out, + int num_channels, float global_scale, float shift, float epsilon, cudaStream_t stream) +{ + return normPlanarImpl(in, baseData, scaleData, out, num_channels, global_scale, shift, true, epsilon, + stream); +} + template void norm(const ImageBatchVarShapeDataStridedCuda &in, const TensorDataAccessStridedImagePlanar &base, const TensorDataAccessStridedImagePlanar &scale, const ImageBatchVarShapeDataStridedCuda &out, @@ -209,12 +685,14 @@ ErrorCode NormalizeVarShape::infer(const nvcv::ImageBatchVarShapeDataStridedCuda DataFormat format = input_format; - if (!(format == kNHWC || format == kHWC)) + if (!(format == kNHWC || format == kHWC || format == kNCHW || format == kCHW)) { - LOG_ERROR("Invliad DataFormat " << format); + LOG_ERROR("Invalid DataFormat " << format << ", the valid DataFormats are: \"NHWC\", \"HWC\", \"NCHW\", \"CHW\""); return ErrorCode::INVALID_DATA_FORMAT; } + const bool isPlanar = (format == kNCHW || format == kCHW); + DataType data_type = helpers::GetLegacyDataType(inData.uniqueFormat()); if (!(data_type == kCV_8U || data_type == kCV_8S || data_type == kCV_16U || data_type == kCV_16S @@ -233,7 +711,7 @@ ErrorCode NormalizeVarShape::infer(const nvcv::ImageBatchVarShapeDataStridedCuda } int channels = inData.uniqueFormat().numChannels(); - if (channels > 4) + if (channels > 4 || (isPlanar && channels == 2)) { LOG_ERROR("Invalid channel number " << channels); return ErrorCode::INVALID_DATA_SHAPE; @@ -253,6 +731,54 @@ ErrorCode NormalizeVarShape::infer(const nvcv::ImageBatchVarShapeDataStridedCuda return ErrorCode::INVALID_DATA_FORMAT; } + if (isPlanar) + { + auto isParamCompatible = [](const TensorDataStridedCuda &data, + const TensorDataAccessStridedImagePlanar &access) + { + DataFormat param_format = helpers::GetLegacyDataFormat(data); + if (param_format == kNCHW || param_format == kCHW) + { + return true; + } + if (param_format == kNHWC || param_format == kHWC) + { + return access.numChannels() == 1; + } + return false; + }; + + if (!isParamCompatible(baseData, *baseAccess)) + { + LOG_ERROR("base DataFormat " << helpers::GetLegacyDataFormat(baseData) + << " is not compatible with planar input; use NCHW/CHW (or a scalar layout)"); + return ErrorCode::INVALID_DATA_FORMAT; + } + if (!isParamCompatible(scaleData, *scaleAccess)) + { + LOG_ERROR("scale DataFormat " << helpers::GetLegacyDataFormat(scaleData) + << " is not compatible with planar input; use NCHW/CHW (or a scalar layout)"); + return ErrorCode::INVALID_DATA_FORMAT; + } + + auto isParamShapeCompatible = [channels](const TensorDataAccessStridedImagePlanar &access) + { + return (access.numSamples() == 1) && (access.numChannels() == 1 || access.numChannels() == channels) + && access.numRows() == 1 && access.numCols() == 1; + }; + + if (!isParamShapeCompatible(*baseAccess)) + { + LOG_ERROR("Invalid planar base shape; expected scalar [1,1,1,1] or per-channel [1,C,1,1]"); + return ErrorCode::INVALID_DATA_SHAPE; + } + if (!isParamShapeCompatible(*scaleAccess)) + { + LOG_ERROR("Invalid planar scale shape; expected scalar [1,1,1,1] or per-channel [1,C,1,1]"); + return ErrorCode::INVALID_DATA_SHAPE; + } + } + typedef void (*normalize_t)( const ImageBatchVarShapeDataStridedCuda &in, const TensorDataAccessStridedImagePlanar &base, const TensorDataAccessStridedImagePlanar &scale, const ImageBatchVarShapeDataStridedCuda &out, @@ -307,6 +833,51 @@ normInvStdDev}, normInvStdDev} }, }; + if (isPlanar) + { + typedef ErrorCode (*normalizePlanar_t)(const ImageBatchVarShapeDataStridedCuda &in, + const TensorDataStridedCuda &baseData, + const TensorDataStridedCuda &scaleData, + const ImageBatchVarShapeDataStridedCuda &out, int num_channels, + float global_scale, float shift, cudaStream_t stream); + + typedef ErrorCode (*normalizePlanarInvStdDev_t)(const ImageBatchVarShapeDataStridedCuda &in, + const TensorDataStridedCuda &baseData, + const TensorDataStridedCuda &scaleData, + const ImageBatchVarShapeDataStridedCuda &out, int num_channels, + float global_scale, float shift, float epsilon, + cudaStream_t stream); + + static const normalizePlanar_t funcs_planar[6][2] = { + { normPlanar, normPlanar}, + { normPlanar, normPlanar}, + {normPlanar, normPlanar}, + { normPlanar, normPlanar}, + { normPlanar, normPlanar}, + { normPlanar, normPlanar}, + }; + + static const normalizePlanarInvStdDev_t funcs_planar_stddev[6][2] = { + { normPlanarInvStdDev, normPlanarInvStdDev}, + { normPlanarInvStdDev, normPlanarInvStdDev}, + {normPlanarInvStdDev, normPlanarInvStdDev}, + { normPlanarInvStdDev, normPlanarInvStdDev}, + { normPlanarInvStdDev, normPlanarInvStdDev}, + { normPlanarInvStdDev, normPlanarInvStdDev}, + }; + + if (flags & CVCUDA_NORMALIZE_SCALE_IS_STDDEV) + { + return funcs_planar_stddev[data_type][out_type_code](inData, baseData, scaleData, outData, channels, + global_scale, shift, epsilon, stream); + } + else + { + return funcs_planar[data_type][out_type_code](inData, baseData, scaleData, outData, channels, global_scale, + shift, stream); + } + } + if (flags & CVCUDA_NORMALIZE_SCALE_IS_STDDEV) { funcs_normalize_stddev[data_type][out_type_code][channels - 1](inData, *baseAccess, *scaleAccess, outData, diff --git a/src/cvcuda/priv/legacy/osd.cu b/src/cvcuda/priv/legacy/osd.cu index 1ebbbf41f..ba68fce0e 100644 --- a/src/cvcuda/priv/legacy/osd.cu +++ b/src/cvcuda/priv/legacy/osd.cu @@ -1,4 +1,4 @@ -/* Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +/* Copyright (c) 2021-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * * SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES * SPDX-License-Identifier: Apache-2.0 @@ -284,7 +284,7 @@ static void cuosd_text_prepare(cuOSDContext_t context, int width, int height, cu context->text_backend->add_build_text(text_cmd->text, text_cmd->font_size, text_cmd->font_name.c_str()); } } - context->text_backend->build_bitmap((void *)stream); + context->text_backend->build_bitmap(stream); std::vector> locations; int total_locations = 0; @@ -399,7 +399,8 @@ static void cuosd_text_prepare(cuOSDContext_t context, int width, int height, cu context->text_location->copy_host_to_device(stream); } -static void cuosd_apply(cuOSDContext_t context, int width, int height, cuOSDImageFormat format, cudaStream_t stream) +static void cuosd_apply(cuOSDContext_t context, int width, int height, int batch, cuOSDImageFormat format, + cudaStream_t stream) { if (context->commands.empty()) { @@ -416,12 +417,15 @@ static void cuosd_apply(cuOSDContext_t context, int width, int height, cuOSDImag context->bounding_right = 0; context->bounding_bottom = 0; - size_t byte_of_commands = 0; - std::vector cmd_offset(context->commands.size()); + size_t byte_of_commands = 0; + int text_line = 0; + const int num_commands = context->commands.size(); + std::vector cmd_offset(num_commands + batch + 1); for (int i = 0; i < (int)context->commands.size(); ++i) { auto &cmd = context->commands[i]; cmd_offset[i] = byte_of_commands; + cmd->reserved = text_line; context->bounding_left = min(context->bounding_left, cmd->bounding_left); context->bounding_top = min(context->bounding_top, cmd->bounding_top); @@ -429,7 +433,13 @@ static void cuosd_apply(cuOSDContext_t context, int width, int height, cuOSDImag context->bounding_bottom = max(context->bounding_bottom, cmd->bounding_bottom); if (cmd->type == CommandType::Text) + { + auto text_cmd = std::static_pointer_cast(cmd); + text_cmd->gputile.reserved = text_line; + if (text_cmd->gputile.type == CommandType::Text) + ++text_line; byte_of_commands += sizeof(TextCommand); + } else if (cmd->type == CommandType::Rectangle) byte_of_commands += sizeof(RectangleCommand); else if (cmd->type == CommandType::Circle) @@ -440,6 +450,14 @@ static void cuosd_apply(cuOSDContext_t context, int width, int height, cuOSDImag byte_of_commands += sizeof(PolyFillCommand); } + int command_index = 0; + for (int batch_index = 0; batch_index <= batch; ++batch_index) + { + while (command_index < num_commands && context->commands[command_index]->batch_index < batch_index) + ++command_index; + cmd_offset[num_commands + batch_index] = command_index; + } + if (context->gpu_commands == nullptr) context->gpu_commands = std::make_unique>(); if (context->gpu_commands_offset == nullptr) @@ -1009,22 +1027,33 @@ static __global__ void render_elements_kernel(int bx, int by, const TextLocation DstWrapper dst, int image_width, int stride, int image_height, bool inplace) { - int ix = ((blockDim.x * blockIdx.x + threadIdx.x) << 1) + bx; - int iy = ((blockDim.y * blockIdx.y + threadIdx.y) << 1) + by; + int ix = ((blockDim.x * blockIdx.x + threadIdx.x) << 1) + bx; + int iy = ((blockDim.y * blockIdx.y + threadIdx.y) << 1) + by; + const int batch_idx = get_batch_idx(); + + const int command_begin = command_offsets[num_command + batch_idx]; + const int command_end = command_offsets[num_command + batch_idx + 1]; + + int text_line_begin = 0; + if (command_begin < command_end) + { + const auto *command = reinterpret_cast(commands + command_offsets[command_begin]); + text_line_begin = command->reserved; + } + if (ix < 0 || iy < 0 || ix >= image_width - 1 || iy >= image_height - 1) return; - int itext_line = 0; - uchar4 context_color[4] = {0}; - const int batch_idx = get_batch_idx(); + int itext_line = text_line_begin; + uchar4 context_color[4] = {0}; - for (int i = 0; i < num_command; ++i) + for (int i = command_begin; i < command_end; ++i) { cuOSDContextCommand *pcommand = (cuOSDContextCommand *)(commands + command_offsets[i]); // because there is four pixel to operator - if (pcommand->batch_index != batch_idx || ix + 1 < pcommand->bounding_left || ix > pcommand->bounding_right - || iy + 1 < pcommand->bounding_top || iy > pcommand->bounding_bottom) + if (ix + 1 < pcommand->bounding_left || ix > pcommand->bounding_right || iy + 1 < pcommand->bounding_top + || iy > pcommand->bounding_bottom) { if (pcommand->type == CommandType::Text) itext_line++; @@ -1214,8 +1243,8 @@ void cuosd_launch(cuOSDContext_t context, SrcWrapper src, DstWrapper dst, int wi static ErrorCode cuosd_draw_text(cuOSDContext_t context, int batch_idx, NVCVText text) { - const char *utf8_text = text.utf8Text; - const char *font = text.fontName; + const char *utf8_text = text.utf8Text.c_str(); + const char *font = text.fontName.c_str(); int font_size = text.fontSize; int x = text.tlPos.x; int y = text.tlPos.y; @@ -1723,8 +1752,8 @@ static ErrorCode cuosd_draw_clock(cuOSDContext_t context, int batch_idx, NVCVClo } auto utf8_str = oss.str(); - cuosd_draw_text(context, batch_idx, utf8_str.c_str(), clock.fontSize, clock.font, clock.tlPos.x, clock.tlPos.y, - *(cuOSDColor *)(&clock.fontColor), *(cuOSDColor *)(&clock.bgColor)); + cuosd_draw_text(context, batch_idx, utf8_str.c_str(), clock.fontSize, clock.font.c_str(), clock.tlPos.x, + clock.tlPos.y, *(cuOSDColor *)(&clock.fontColor), *(cuOSDColor *)(&clock.bgColor)); return ErrorCode::SUCCESS; } @@ -1736,9 +1765,9 @@ static ErrorCode cuosd_draw_elements(cuOSDContext_t context, int width, int heig for (int i = 0; i < numElements; i++) { - auto element = ctx->elementAt(n, i); - auto type = element->type(); - auto data = element->ptr(); + auto element = ctx->elementAt(n, i); + auto type = element->type(); + auto &data = element->data(); switch (type) { case NVCVOSDType::NVCV_OSD_NONE: @@ -1747,52 +1776,52 @@ static ErrorCode cuosd_draw_elements(cuOSDContext_t context, int width, int heig } case NVCVOSDType::NVCV_OSD_RECT: { - cuosd_draw_rectangle(context, n, width, height, *((NVCVBndBoxI *)data)); + cuosd_draw_rectangle(context, n, width, height, std::get(data)); break; } case NVCVOSDType::NVCV_OSD_TEXT: { - cuosd_draw_text(context, n, *((NVCVText *)data)); + cuosd_draw_text(context, n, std::get(data)); break; } case NVCVOSDType::NVCV_OSD_SEGMENT: { - cuosd_draw_segmentmask(context, n, width, height, *((NVCVSegment *)data)); + cuosd_draw_segmentmask(context, n, width, height, std::get(data)); break; } case NVCVOSDType::NVCV_OSD_POINT: { - cuosd_draw_point(context, n, *((NVCVPoint *)data)); + cuosd_draw_point(context, n, std::get(data)); break; } case NVCVOSDType::NVCV_OSD_LINE: { - cuosd_draw_line(context, n, *((NVCVLine *)data)); + cuosd_draw_line(context, n, std::get(data)); break; } case NVCVOSDType::NVCV_OSD_POLYLINE: { - cuosd_draw_polyline(context, n, *((NVCVPolyLine *)data)); + cuosd_draw_polyline(context, n, std::get(data)); break; } case NVCVOSDType::NVCV_OSD_ROTATED_RECT: { - cuosd_draw_rotationbox(context, n, *((NVCVRotatedBox *)data)); + cuosd_draw_rotationbox(context, n, std::get(data)); break; } case NVCVOSDType::NVCV_OSD_CIRCLE: { - cuosd_draw_circle(context, n, *((NVCVCircle *)data)); + cuosd_draw_circle(context, n, std::get(data)); break; } case NVCVOSDType::NVCV_OSD_ARROW: { - cuosd_draw_arrow(context, n, *((NVCVArrow *)data)); + cuosd_draw_arrow(context, n, std::get(data)); break; } case NVCVOSDType::NVCV_OSD_CLOCK: { - cuosd_draw_clock(context, n, *((NVCVClock *)data)); + cuosd_draw_clock(context, n, std::get(data)); break; } default: @@ -1833,6 +1862,11 @@ OSD::~OSD() } } +size_t OSD::calBufferSize(DataShape max_input_shape, DataShape max_output_shape, DataType max_data_type) +{ + return CudaBaseOp::calBufferSize(max_input_shape, max_output_shape, max_data_type); +} + ErrorCode OSD::infer(const nvcv::TensorDataStridedCuda &inData, const nvcv::TensorDataStridedCuda &outData, NVCVElements elements, cudaStream_t stream) { @@ -1852,6 +1886,14 @@ ErrorCode OSD::infer(const nvcv::TensorDataStridedCuda &inData, const nvcv::Tens return ErrorCode::INVALID_DATA_FORMAT; } + const cuda_op::DataType data_type = GetLegacyDataType(inData.dtype()); + + if (!(data_type == kCV_8U)) + { + LOG_ERROR("Invalid DataType " << data_type); + return ErrorCode::INVALID_DATA_TYPE; + } + auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(inData); if (!inAccess) { @@ -1863,7 +1905,7 @@ ErrorCode OSD::infer(const nvcv::TensorDataStridedCuda &inData, const nvcv::Tens int rows = inAccess->numRows(); int cols = inAccess->numCols(); - if (channels > 4 || channels < 1) + if (channels > 4 || channels < 3) { LOG_ERROR("Invalid channel number ch = " << channels); return ErrorCode::INVALID_DATA_SHAPE; @@ -1912,7 +1954,7 @@ ErrorCode OSD::infer(const nvcv::TensorDataStridedCuda &inData, const nvcv::Tens if (inputShape.C == 3) format = cuOSDImageFormat::RGB; - cuosd_apply(m_context, inputShape.W, inputShape.H, format, stream); + cuosd_apply(m_context, inputShape.W, inputShape.H, inputShape.N, format, stream); auto src = nvcv::cuda::CreateTensorWrapNHWC(inData); auto dst = nvcv::cuda::CreateTensorWrapNHWC(outData); @@ -1947,6 +1989,14 @@ ErrorCode OSD::inferBox(const nvcv::TensorDataStridedCuda &inData, const nvcv::T return ErrorCode::INVALID_DATA_FORMAT; } + const cuda_op::DataType data_type = GetLegacyDataType(inData.dtype()); + + if (!(data_type == kCV_8U)) + { + LOG_ERROR("Invalid DataType " << data_type); + return ErrorCode::INVALID_DATA_TYPE; + } + auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(inData); if (!inAccess) { @@ -1974,7 +2024,7 @@ ErrorCode OSD::inferBox(const nvcv::TensorDataStridedCuda &inData, const nvcv::T int rows = inAccess->numRows(); int cols = inAccess->numCols(); - if (channels > 4 || channels < 1) + if (channels > 4 || channels < 3) { LOG_ERROR("Invalid channel number ch = " << channels); return ErrorCode::INVALID_DATA_SHAPE; @@ -1998,7 +2048,7 @@ ErrorCode OSD::inferBox(const nvcv::TensorDataStridedCuda &inData, const nvcv::T if (inputShape.C == 3) format = cuOSDImageFormat::RGB; - cuosd_apply(m_context, inputShape.W, inputShape.H, format, stream); + cuosd_apply(m_context, inputShape.W, inputShape.H, inputShape.N, format, stream); auto src = nvcv::cuda::CreateTensorWrapNHWC(inData); auto dst = nvcv::cuda::CreateTensorWrapNHWC(outData); diff --git a/src/cvcuda/priv/legacy/pad_and_stack.cu b/src/cvcuda/priv/legacy/pad_and_stack.cu index 925232da7..8eae05f7c 100644 --- a/src/cvcuda/priv/legacy/pad_and_stack.cu +++ b/src/cvcuda/priv/legacy/pad_and_stack.cu @@ -25,6 +25,38 @@ namespace nvcv::legacy::cuda_op { +namespace { + +static bool IsPlanar(DataFormat format) +{ + return format == kNCHW || format == kCHW; +} + +static bool SameLayoutFamily(DataFormat lhs, DataFormat rhs) +{ + return IsPlanar(lhs) == IsPlanar(rhs); +} + +static TensorDataStridedCuda PlanarTensorView(const TensorDataStridedCuda &data) +{ + auto access = TensorDataAccessStridedImagePlanar::Create(data); + NVCV_ASSERT(access); + + TensorDataStridedCuda::Buffer buf; + buf.basePtr = reinterpret_cast(data.basePtr()); + buf.strides[0] = access->sampleStride(); + buf.strides[1] = access->chStride(); + buf.strides[2] = access->rowStride(); + buf.strides[3] = access->colStride(); + + return TensorDataStridedCuda{ + TensorShape{{access->numSamples(), access->numChannels(), access->numRows(), access->numCols()}, "NCHW"}, + data.dtype(), buf + }; +} + +} // namespace + template __global__ void padAndStack(SrcWrapper src, DstWrapper dst, VecWrapper topVec, VecWrapper leftVec, int2 dstSize) { @@ -41,6 +73,25 @@ __global__ void padAndStack(SrcWrapper src, DstWrapper dst, VecWrapper topVec, V } } +template +__global__ void padAndStackPlanar(SrcWrapper src, cuda::Tensor4DWrap dst, VecWrapper topVec, VecWrapper leftVec, + int2 dstSize, int channels) +{ + int3 dstCoord = cuda::StaticCast(blockDim * blockIdx + threadIdx); + + const int top = *topVec.ptr(0, 0, dstCoord.z); + const int left = *leftVec.ptr(0, 0, dstCoord.z); + + if (dstCoord.x < dstSize.x && dstCoord.y < dstSize.y) + { + for (int plane = 0; plane < channels; ++plane) + { + *dst.ptr(dstCoord.z, plane, dstCoord.y, dstCoord.x) + = src[int4{dstCoord.x - left, dstCoord.y - top, plane, dstCoord.z}]; + } + } +} + template ErrorCode padAndStackCaller(const ImageBatchVarShapeDataStridedCuda &inData, const TensorDataStridedCuda &outData, const TensorDataStridedCuda &top, const TensorDataStridedCuda &left, @@ -72,6 +123,39 @@ ErrorCode padAndStackCaller(const ImageBatchVarShapeDataStridedCuda &inData, con return ErrorCode::SUCCESS; } +template +ErrorCode padAndStackPlanarCaller(const ImageBatchVarShapeDataStridedCuda &inData, const TensorDataStridedCuda &outData, + const TensorDataStridedCuda &top, const TensorDataStridedCuda &left, + const float borderValue, cudaStream_t stream) +{ + cuda::BorderVarShapeWrap src(inData, static_cast(borderValue)); + + auto topVec = cuda::CreateTensorWrapNHW(top); + auto leftVec = cuda::CreateTensorWrapNHW(left); + + auto outAccess = TensorDataAccessStridedImagePlanar::Create(outData); + NVCV_ASSERT(outAccess); + + const int channels = outAccess->numChannels(); + const auto samples = static_cast(outAccess->numSamples()); + if (samples > 65535) + { + LOG_ERROR("Planar batch size " << samples << " exceeds CUDA grid z limit 65535."); + return ErrorCode::INVALID_PARAMETER; + } + + int2 dstSize{outAccess->numCols(), outAccess->numRows()}; + + auto outView = PlanarTensorView(outData); + auto dst = cuda::Tensor4DWrap(outView); + + dim3 block(32, sizeof(T) == 1 ? 4 : 8); + dim3 grid(divUp(dstSize.x, block.x), divUp(dstSize.y, block.y), outAccess->numSamples()); + + padAndStackPlanar<<>>(src, dst, topVec, leftVec, dstSize, channels); + return ErrorCode::SUCCESS; +} + template ErrorCode padAndStack(const ImageBatchVarShapeDataStridedCuda &inData, const TensorDataStridedCuda &outData, const TensorDataStridedCuda &top, const TensorDataStridedCuda &left, @@ -90,6 +174,24 @@ ErrorCode padAndStack(const ImageBatchVarShapeDataStridedCuda &inData, const Ten return funcs[borderMode](inData, outData, top, left, borderValue, stream); } +template +ErrorCode padAndStackPlanar(const ImageBatchVarShapeDataStridedCuda &inData, const TensorDataStridedCuda &outData, + const TensorDataStridedCuda &top, const TensorDataStridedCuda &left, + const NVCVBorderType borderMode, const float borderValue, cudaStream_t stream) +{ + typedef ErrorCode (*padAndStack_caller)(const ImageBatchVarShapeDataStridedCuda &inData, + const TensorDataStridedCuda &outData, const TensorDataStridedCuda &top, + const TensorDataStridedCuda &left, const float borderValue, + cudaStream_t stream); + + static const padAndStack_caller funcs[] + = {padAndStackPlanarCaller, padAndStackPlanarCaller, + padAndStackPlanarCaller, padAndStackPlanarCaller, + padAndStackPlanarCaller}; + + return funcs[borderMode](inData, outData, top, left, borderValue, stream); +} + ErrorCode PadAndStack::infer(const ImageBatchVarShapeDataStridedCuda &inData, const TensorDataStridedCuda &outData, const TensorDataStridedCuda &top, const TensorDataStridedCuda &left, const NVCVBorderType borderMode, const float borderValue, cudaStream_t stream) @@ -106,14 +208,21 @@ ErrorCode PadAndStack::infer(const ImageBatchVarShapeDataStridedCuda &inData, co DataFormat format = helpers::GetLegacyDataFormat(outData.layout()); DataType data_type = helpers::GetLegacyDataType(outData.dtype()); - if (!(input_format == kNHWC || input_format == kHWC)) + if (!(input_format == kNHWC || input_format == kHWC || input_format == kNCHW || input_format == kCHW)) + { + LOG_ERROR("Invalid input DataFormat " << input_format + << ", the valid DataFormats are: \"NHWC\", \"HWC\", \"NCHW\", \"CHW\""); + return ErrorCode::INVALID_DATA_FORMAT; + } + if (!(format == kNHWC || format == kHWC || format == kNCHW || format == kCHW)) { - LOG_ERROR("Invalid input DataFormat " << input_format << ", the valid DataFormats are: \"NHWC\", \"HWC\""); + LOG_ERROR("Invalid output DataFormat " << format + << ", the valid DataFormats are: \"NHWC\", \"HWC\", \"NCHW\", \"CHW\""); return ErrorCode::INVALID_DATA_FORMAT; } - if (!(format == kNHWC || format == kHWC)) + if (!SameLayoutFamily(input_format, format)) { - LOG_ERROR("Invalid output DataFormat " << format << ", the valid DataFormats are: \"NHWC\", \"HWC\""); + LOG_ERROR("Invalid DataFormat between input (" << input_format << ") and output (" << format << ")"); return ErrorCode::INVALID_DATA_FORMAT; } @@ -178,12 +287,30 @@ ErrorCode PadAndStack::infer(const ImageBatchVarShapeDataStridedCuda &inData, co return ErrorCode::INVALID_DATA_TYPE; } - const int channels = outAccess->numChannels(); - if (channels > 4) + const int channels = outAccess->numChannels(); + const int inputChannels = inData.uniqueFormat().numChannels(); + const bool isPlanar = IsPlanar(format); + if (channels != inputChannels) + { + LOG_ERROR("Input and output channel counts must match, but got " << inputChannels << " and " << channels); + return ErrorCode::INVALID_DATA_SHAPE; + } + if (channels > 4 || (isPlanar && channels == 2)) { LOG_ERROR("Invalid channel number " << channels); return ErrorCode::INVALID_DATA_SHAPE; } + if (data_type != input_data_type) + { + LOG_ERROR("Input and output DataTypes must match, but got " << input_data_type << " and " << data_type); + return ErrorCode::INVALID_DATA_TYPE; + } + if (!isPlanar && channels == 2 && data_type != kCV_8U) + { + LOG_ERROR("Invalid channel number " << channels << " for DataType " << data_type + << ", 2 channels only supported for 8bit Unsigned"); + return ErrorCode::INVALID_DATA_SHAPE; + } typedef ErrorCode (*func_t)(const ImageBatchVarShapeDataStridedCuda &inData, const TensorDataStridedCuda &outData, const TensorDataStridedCuda &top, const TensorDataStridedCuda &left, @@ -201,6 +328,17 @@ ErrorCode PadAndStack::infer(const ImageBatchVarShapeDataStridedCuda &inData, co const func_t func = funcs[data_type][channels - 1]; NVCV_ASSERT(func != 0); + if (isPlanar) + { + static const func_t planarFuncs[6] + = {padAndStackPlanar, 0, padAndStackPlanar, padAndStackPlanar, padAndStackPlanar, + padAndStackPlanar}; + + const func_t planarFunc = planarFuncs[data_type]; + NVCV_ASSERT(planarFunc != 0); + return planarFunc(inData, outData, top, left, borderMode, borderValue, stream); + } + return func(inData, outData, top, left, borderMode, borderValue, stream); } diff --git a/src/cvcuda/priv/legacy/pillow_resize.cu b/src/cvcuda/priv/legacy/pillow_resize.cu index 625b10d51..cb8b0efe6 100644 --- a/src/cvcuda/priv/legacy/pillow_resize.cu +++ b/src/cvcuda/priv/legacy/pillow_resize.cu @@ -26,6 +26,12 @@ #include +#include +#include +#include +#include +#include + using namespace nvcv; using namespace nvcv::legacy::cuda_op; using namespace nvcv::legacy::helpers; @@ -40,190 +46,750 @@ __global__ void _precomputeCoeffs(int in_size, int in0, work_type scale, work_ty int out_size, int k_size, Filter filterp, int *bounds_out, work_type *kk_out, bool normalize_coeff, bool use_share_mem) { - const int xx = blockIdx.x * blockDim.x + threadIdx.x; - const int local_id = threadIdx.x; - const int x_offset = blockIdx.x * blockDim.x; - work_type *kk = kk_out + x_offset * k_size; - if (use_share_mem) + const int xx = blockIdx.x * blockDim.x + threadIdx.x; + const int local_id = threadIdx.x; + const int x_offset = blockIdx.x * blockDim.x; + + PillowPrecomputeCoeffs(xx, local_id, x_offset, in_size, in0, scale, filterscale, support, out_size, k_size, + filterp, bounds_out, kk_out, normalize_coeff, use_share_mem, precision_bits); +} + +// SrcPtr2d / DstPtr2d are Ptr2dNHWC (interleaved) or Ptr2dNCHW (planar). Both expose the same +// ptr(b, y, x, c) / rows / cols / ch interface, so one kernel handles both layouts. For planar the +// grid is launched with one z-slice per sample (not per N*C flattened plane), and the per-output- +// pixel bounds / weight-pointer setup is computed once and reused across all `ch` planes -- amortizing +// the work the flattened single-channel view would otherwise repeat once per channel. +// +// NC is the compile-time channel count for the interleaved (NHWC) path: the channels of one output +// pixel are contiguous in memory, so the whole pixel is loaded/stored as one `MakeType` vector +// and accumulated in a `MakeType` register. This collapses the per-channel scalar loop +// (NC separate 1-byte loads + NC full ptr() index computations per tap) into a single wide, coalesced +// access per tap -- the resize passes are issue-bound on that per-channel address arithmetic, not on +// bandwidth. NC == 0 selects the original scalar channel loop, used for the planar (NCHW) layout where +// the channels live in separate planes and are not contiguous. +template +__global__ void horizontal_pass(const SrcPtr2d src, DstPtr2d dst, NVCVRectI roi, Filter &filterp, int h_ksize, + int v_ksize, int *h_bounds, work_type *h_kk, int *v_bounds, work_type *v_kk, + work_type init_buffer, bool round_up, bool use_share_mem) +{ + const int dst_x = blockIdx.x * blockDim.x + threadIdx.x; + const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int local_x = threadIdx.x; + const int x_offset = blockIdx.x * blockDim.x; + const int batch_idx = get_batch_idx(); + int out_height = dst.rows, out_width = dst.cols; + + extern __shared__ __align__(sizeof(work_type)) unsigned char kk_smem_h[]; + work_type *h_k_tmp = PillowStageCoeffs(h_kk, reinterpret_cast(kk_smem_h), x_offset, h_ksize, out_width, + blockDim.x, use_share_mem); + + if (dst_x < out_width && dst_y < out_height) { - extern __shared__ __align__(sizeof(work_type)) unsigned char smem_raw[]; - kk = reinterpret_cast(smem_raw); + int xmin = h_bounds[dst_x * 2]; + int xmax = h_bounds[dst_x * 2 + 1]; + + work_type *h_k = &h_k_tmp[local_x * h_ksize]; + + if constexpr (NC > 0) + { + // Interleaved: load the contiguous NC-channel pixel as one vector; advance one pixel + // (NC elements) per horizontal tap. + using vec_t = cuda::MakeType; + using accum_t = cuda::MakeType; + + const std::byte *p = reinterpret_cast(src.ptr(batch_idx, dst_y, xmin, 0)); + accum_t h_ss = cuda::SetAll(work_type{0}); + for (int x = 0; x < xmax; ++x) + { + h_ss = h_ss + h_k[x] * cuda::StaticCast(*reinterpret_cast(p)); + p += sizeof(vec_t); + } + + vec_t *out = reinterpret_cast(dst.ptr(batch_idx, dst_y, dst_x, 0)); + if (round_up) + { + // Signed-integer path keeps std::round (half away from zero) per channel for bit + // exactness with the scalar path; cuda::round has no half-away mode. + work_type *acc = reinterpret_cast(&h_ss); +#pragma unroll + for (int c = 0; c < NC; ++c) reinterpret_cast(out)[c] = cuda::SaturateCast(std::round(acc[c])); + } + else + { + *out = cuda::SaturateCast(h_ss); + } + } + else + { + for (int c = 0; c < src.ch; ++c) + { + work_type h_ss = 0.0; + for (int x = 0; x < xmax; ++x) + { + h_ss = h_ss + *src.ptr(batch_idx, dst_y, x + xmin, c) * h_k[x]; + } + + if (round_up) + *dst.ptr(batch_idx, dst_y, dst_x, c) = cuda::SaturateCast(std::round(h_ss)); + else + *dst.ptr(batch_idx, dst_y, dst_x, c) = cuda::SaturateCast(h_ss); + } + } } +} + +// Interleaved horizontal pass computing two adjacent output pixels per thread. Neighboring tap +// windows overlap by roughly k minus the scale step, so the shared segment is loaded once and +// accumulated into both outputs. Three unpredicated segment loops (prefix / shared / suffix) keep +// the fma count and the ascending per-output tap order identical to horizontal_pass (bit-exact); +// only the load count drops. The launch site doubles the block's output span for the staged +// coefficients. +template +__global__ void horizontal_pass_paired(const SrcPtr2d src, DstPtr2d dst, NVCVRectI roi, Filter &filterp, int h_ksize, + int v_ksize, int *h_bounds, work_type *h_kk, int *v_bounds, work_type *v_kk, + work_type init_buffer, bool round_up, bool use_share_mem) +{ + static_assert(NC > 0, "paired horizontal pass is the interleaved channel-vectorized path"); + const int pair0 = (blockIdx.x * blockDim.x + threadIdx.x) * 2; + const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int local_x = threadIdx.x; + const int x_offset = blockIdx.x * blockDim.x * 2; + const int batch_idx = get_batch_idx(); + int out_height = dst.rows, out_width = dst.cols; - if (xx < out_size) + extern __shared__ __align__(sizeof(work_type)) unsigned char kk_smem_hq[]; + work_type *h_k_tmp = PillowStageCoeffs(h_kk, reinterpret_cast(kk_smem_hq), x_offset, h_ksize, + out_width, blockDim.x * 2, use_share_mem); + + if (pair0 < out_width && dst_y < out_height) { - int x = 0; - int xmin = 0; - int xmax = 0; - work_type center = 0; - work_type ww = 0; - work_type ss = 0; - - const work_type half_pixel = 0.5; - - center = in0 + (xx + half_pixel) * scale; - ww = 0.0; - ss = 1.0 / filterscale; - // Round the value. - xmin = static_cast(center - support + half_pixel); - if (xmin < 0) + using vec_t = cuda::MakeType; + using accum_t = cuda::MakeType; + + const bool has1 = pair0 + 1 < out_width; + const int xmin0 = h_bounds[pair0 * 2]; + const int xmax0 = h_bounds[pair0 * 2 + 1]; + const int end0 = xmin0 + xmax0; + // Without a second output, park its window at end0 so the shared and suffix loops are + // empty and k1 (one slot past this image's coefficients) is never read. + const int xmin1 = has1 ? h_bounds[(pair0 + 1) * 2] : end0; + const int xmax1 = has1 ? h_bounds[(pair0 + 1) * 2 + 1] : 0; + + work_type *k0 = &h_k_tmp[(local_x * 2) * h_ksize]; + work_type *k1 = &h_k_tmp[(local_x * 2 + 1) * h_ksize]; + + const int end1 = xmin1 + xmax1; + + accum_t s0 = cuda::SetAll(work_type{0}); + accum_t s1 = cuda::SetAll(work_type{0}); + + const std::byte *base = reinterpret_cast(src.ptr(batch_idx, dst_y, 0, 0)); + const int preEnd = min(xmin1, end0); + for (int x = xmin0; x < preEnd; ++x) { - xmin = 0; + s0 = s0 + + k0[x - xmin0] + * cuda::StaticCast( + *reinterpret_cast(base + static_cast(x) * sizeof(vec_t))); } - // Round the value. - xmax = static_cast(center + support + half_pixel); - if (xmax > in_size) + for (int x = xmin1; x < end0; ++x) { - xmax = in_size; + const accum_t px = cuda::StaticCast( + *reinterpret_cast(base + static_cast(x) * sizeof(vec_t))); + s0 = s0 + k0[x - xmin0] * px; + s1 = s1 + k1[x - xmin1] * px; } - xmax -= xmin; - work_type *k = &kk[local_id * k_size]; - for (x = 0; x < xmax; ++x) + const int sufBegin = max(xmin1, end0); + for (int x = sufBegin; x < end1; ++x) { - work_type w = filterp.filter((x + xmin - center + half_pixel) * ss); - k[x] = w; - ww += w; + s1 = s1 + + k1[x - xmin1] + * cuda::StaticCast( + *reinterpret_cast(base + static_cast(x) * sizeof(vec_t))); } - for (x = 0; x < xmax; ++x) + + vec_t *out0 = reinterpret_cast(dst.ptr(batch_idx, dst_y, pair0, 0)); + if (round_up) + { + work_type *acc0 = reinterpret_cast(&s0); +#pragma unroll + for (int c = 0; c < NC; ++c) reinterpret_cast(out0)[c] = cuda::SaturateCast(std::round(acc0[c])); + } + else + { + *out0 = cuda::SaturateCast(s0); + } + if (has1) { - if (std::fabs(ww) > 1e-5) + vec_t *out1 = reinterpret_cast(dst.ptr(batch_idx, dst_y, pair0 + 1, 0)); + if (round_up) + { + work_type *acc1 = reinterpret_cast(&s1); +#pragma unroll + for (int c = 0; c < NC; ++c) + reinterpret_cast(out1)[c] = cuda::SaturateCast(std::round(acc1[c])); + } + else { - k[x] /= ww; + *out1 = cuda::SaturateCast(s1); } } - // Remaining values should stay empty if they are used despite of xmax. - for (; x < k_size; ++x) + } +} + +template +__global__ void horizontal_pass_planar_channels(const SrcPtr2d src, DstPtr2d dst, NVCVRectI roi, Filter &filterp, + int h_ksize, int v_ksize, int *h_bounds, work_type *h_kk, int *v_bounds, + work_type *v_kk, work_type init_buffer, bool round_up, + bool use_share_mem) +{ + const int dst_x = blockIdx.x * blockDim.x + threadIdx.x; + const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int local_x = threadIdx.x; + const int x_offset = blockIdx.x * blockDim.x; + const int batch_idx = get_batch_idx(); + int out_height = dst.rows, out_width = dst.cols; + + extern __shared__ __align__(sizeof(work_type)) unsigned char kk_smem_hp[]; + work_type *h_k_tmp = PillowStageCoeffs(h_kk, reinterpret_cast(kk_smem_hp), x_offset, h_ksize, + out_width, blockDim.x, use_share_mem); + + if (dst_x < out_width && dst_y < out_height) + { + int xmin = h_bounds[dst_x * 2]; + int xmax = h_bounds[dst_x * 2 + 1]; + work_type *h_k = &h_k_tmp[local_x * h_ksize]; + work_type h_ss[PC]; + +#pragma unroll + for (int c = 0; c < PC; ++c) { - k[x] = .0f; + h_ss[c] = 0.0; } - if (normalize_coeff) + + // Hoist the per-plane row pointers: the tap loop then advances plain pointers instead of + // re-deriving the full (b, y, x, c) offset for every tap of every plane. Three byte planes + // only: at four planes the extra pointer registers measured 3.5-6.6% slower on multi-tap + // rows, and float planes regressed contract cubic 6-16%. + if constexpr (PC == 3 && sizeof(T) == 1) { - for (int i = 0; i < k_size; i++) + const T *row[PC]; +#pragma unroll + for (int c = 0; c < PC; ++c) row[c] = src.ptr(batch_idx, dst_y, xmin, c); + + for (int x = 0; x < xmax; ++x) { - work_type val = k[i]; - if (val < 0) + const work_type k = h_k[x]; +#pragma unroll + for (int c = 0; c < PC; ++c) { - k[i] = static_cast(-half_pixel + val * (1U << precision_bits)); + h_ss[c] = h_ss[c] + row[c][x] * k; } - else + } + } + else + { + for (int x = 0; x < xmax; ++x) + { + const work_type k = h_k[x]; +#pragma unroll + for (int c = 0; c < PC; ++c) { - k[i] = static_cast(half_pixel + val * (1U << precision_bits)); + h_ss[c] = h_ss[c] + *src.ptr(batch_idx, dst_y, x + xmin, c) * k; } } } - bounds_out[xx * 2] = xmin; - bounds_out[xx * 2 + 1] = xmax; - } - if (use_share_mem) - { - __syncthreads(); - for (int i = local_id; i < (out_size - x_offset) * k_size && i < blockDim.x * k_size; i += blockDim.x) +#pragma unroll + for (int c = 0; c < PC; ++c) { - kk_out[x_offset * k_size + i] = kk[i]; + if (round_up) + *dst.ptr(batch_idx, dst_y, dst_x, c) = cuda::SaturateCast(std::round(h_ss[c])); + else + *dst.ptr(batch_idx, dst_y, dst_x, c) = cuda::SaturateCast(h_ss[c]); } } } -template -__global__ void horizontal_pass(const cuda_op::Ptr2dNHWC src, cuda_op::Ptr2dNHWC dst, NVCVRectI roi, - Filter &filterp, int h_ksize, int v_ksize, int *h_bounds, work_type *h_kk, - int *v_bounds, work_type *v_kk, work_type init_buffer, bool round_up, - bool use_share_mem) +template +__global__ void vertical_pass(const SrcPtr2d src, DstPtr2d dst, NVCVRectI roi, Filter &filterp, int h_ksize, + int v_ksize, int *h_bounds, work_type *h_kk, int *v_bounds, work_type *v_kk, + work_type init_buffer, bool round_up, bool use_share_mem) { - const int dst_x = blockIdx.x * blockDim.x + threadIdx.x; - const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; - const int local_x = threadIdx.x; - const int x_offset = blockIdx.x * blockDim.x; - const int batch_idx = get_batch_idx(); - int out_height = dst.rows, out_width = dst.cols; - work_type *h_k_tmp = h_kk + x_offset * h_ksize; - - if (use_share_mem) + const int dst_x = blockIdx.x * blockDim.x + threadIdx.x; + const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int local_y = threadIdx.y; + const int y_offset = blockIdx.y * blockDim.y; + const int batch_idx = get_batch_idx(); + int out_height = dst.rows, out_width = dst.cols; + + extern __shared__ __align__(sizeof(work_type)) unsigned char kk_smem_v[]; + work_type *v_k_tmp = PillowStageCoeffs(v_kk, reinterpret_cast(kk_smem_v), y_offset, v_ksize, + out_height, blockDim.y, use_share_mem); + + if (dst_x < out_width && dst_y < out_height) { - const int local_tid = threadIdx.x + blockDim.x * threadIdx.y; - extern __shared__ __align__(sizeof(work_type)) unsigned char kk_smem_h[]; - h_k_tmp = reinterpret_cast(kk_smem_h); + int ymin = v_bounds[dst_y * 2]; + int ymax = v_bounds[dst_y * 2 + 1]; + + work_type *v_k = &v_k_tmp[local_y * v_ksize]; - for (int i = local_tid; i < blockDim.x * h_ksize && i < (out_width - x_offset) * h_ksize; - i += blockDim.x * blockDim.y) + if constexpr (NC > 0) { - h_k_tmp[i] = h_kk[x_offset * h_ksize + i]; + // Interleaved: load the contiguous NC-channel pixel as one vector; advance one row + // (rowStride bytes) per vertical tap. + using vec_t = cuda::MakeType; + using accum_t = cuda::MakeType; + + const std::byte *p = reinterpret_cast(src.ptr(batch_idx, ymin, dst_x, 0)); + accum_t ss = cuda::SetAll(init_buffer); + for (int y = 0; y < ymax; ++y) + { + ss = ss + v_k[y] * cuda::StaticCast(*reinterpret_cast(p)); + p += src.rowStride; + } + + vec_t *out = reinterpret_cast(dst.ptr(batch_idx, dst_y, dst_x, 0)); + if (round_up) + { + work_type *acc = reinterpret_cast(&ss); +#pragma unroll + for (int c = 0; c < NC; ++c) reinterpret_cast(out)[c] = cuda::SaturateCast(std::round(acc[c])); + } + else + { + *out = cuda::SaturateCast(ss); + } } + else + { + for (int c = 0; c < src.ch; ++c) + { + work_type ss = init_buffer; + for (int y = 0; y < ymax; ++y) + { + ss = ss + *src.ptr(batch_idx, y + ymin, dst_x, c) * v_k[y]; + } - __syncthreads(); + if (round_up) + *dst.ptr(batch_idx, dst_y, dst_x, c) = cuda::SaturateCast(std::round(ss)); + else + *dst.ptr(batch_idx, dst_y, dst_x, c) = cuda::SaturateCast(ss); + } + } } +} - if (dst_x < out_width && dst_y < out_height) +// Interleaved (NHWC) vertical pass vectorized along the flattened row. The vertical weights depend +// only on dst_y, so all cols*ch scalars of an output row share v_k: each thread accumulates VEC +// consecutive scalars with one MakeType load per tap instead of one per-pixel channel +// vector, cutting the load/store and address-arithmetic issue count these passes are bound on. +// Per-element arithmetic (taps, order, cast, saturate) is identical to vertical_pass. The launch +// site gates on vector alignment of both buffers; the row tail falls back to a scalar loop. +template +__global__ void vertical_pass_vec(const SrcPtr2d src, DstPtr2d dst, NVCVRectI roi, Filter &filterp, int h_ksize, + int v_ksize, int *h_bounds, work_type *h_kk, int *v_bounds, work_type *v_kk, + work_type init_buffer, bool round_up, bool use_share_mem) +{ + const int elem0 = (blockIdx.x * blockDim.x + threadIdx.x) * VEC; + const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int local_y = threadIdx.y; + const int y_offset = blockIdx.y * blockDim.y; + const int batch_idx = get_batch_idx(); + const int out_height = dst.rows; + const int row_elems = dst.cols * dst.ch; + + extern __shared__ __align__(sizeof(work_type)) unsigned char kk_smem_vv[]; + work_type *v_k_tmp = PillowStageCoeffs(v_kk, reinterpret_cast(kk_smem_vv), y_offset, v_ksize, + out_height, blockDim.y, use_share_mem); + + if (elem0 < row_elems && dst_y < out_height) { - int xmin = h_bounds[dst_x * 2]; - int xmax = h_bounds[dst_x * 2 + 1]; + const int ymin = v_bounds[dst_y * 2]; + const int ymax = v_bounds[dst_y * 2 + 1]; - work_type *h_k = &h_k_tmp[local_x * h_ksize]; + work_type *v_k = &v_k_tmp[local_y * v_ksize]; - for (int c = 0; c < src.ch; ++c) + const std::byte *src_row + = reinterpret_cast(src.ptr(batch_idx, ymin, 0, 0)) + elem0 * sizeof(T); + T *dst_row = dst.ptr(batch_idx, dst_y, 0, 0) + elem0; + + if (elem0 + VEC <= row_elems) { - work_type h_ss = 0.0; - for (int x = 0; x < xmax; ++x) + using vec_t = cuda::MakeType; + using accum_t = cuda::MakeType; + + accum_t ss = cuda::SetAll(init_buffer); + for (int y = 0; y < ymax; ++y) { - h_ss = h_ss + *src.ptr(batch_idx, dst_y, x + xmin, c) * h_k[x]; + ss = ss + v_k[y] * cuda::StaticCast(*reinterpret_cast(src_row)); + src_row += src.rowStride; } - if (round_up) - *dst.ptr(batch_idx, dst_y, dst_x, c) = cuda::SaturateCast(std::round(h_ss)); + { + work_type *acc = reinterpret_cast(&ss); +#pragma unroll + for (int i = 0; i < VEC; ++i) dst_row[i] = cuda::SaturateCast(std::round(acc[i])); + } else - *dst.ptr(batch_idx, dst_y, dst_x, c) = cuda::SaturateCast(h_ss); + { + *reinterpret_cast(dst_row) = cuda::SaturateCast(ss); + } + } + else + { + for (int i = 0; elem0 + i < row_elems; ++i) + { + const std::byte *p = src_row + i * sizeof(T); + work_type ss = init_buffer; + for (int y = 0; y < ymax; ++y) + { + ss = ss + *reinterpret_cast(p) * v_k[y]; + p += src.rowStride; + } + dst_row[i] = round_up ? cuda::SaturateCast(std::round(ss)) : cuda::SaturateCast(ss); + } } } } -template -__global__ void vertical_pass(const cuda_op::Ptr2dNHWC src, cuda_op::Ptr2dNHWC dst, NVCVRectI roi, - Filter &filterp, int h_ksize, int v_ksize, int *h_bounds, work_type *h_kk, int *v_bounds, - work_type *v_kk, work_type init_buffer, bool round_up, bool use_share_mem) +// Fused single-pass resize for the interleaved (NHWC) FLOAT path. Mathematically identical, op-for-op, +// to running horizontal_pass then vertical_pass through a FLOAT intermediate: for each contributing +// source row the inner horizontal sum reproduces exactly what the float intermediate would store +// (SaturateCast is the identity, so no value is lost between passes), and accumulating +// v_k[y] * inner then yields the same float result as the separable path. The intermediate stays in a +// register, eliminating its full-frame DRAM round-trip -- the dominant cost at high resolution, where +// both passes sit at the DRAM ridge. FLOAT ONLY: an integer intermediate is quantized between passes +// (Pillow semantics), so fusing it would change results; that path keeps the separable kernels. +template +__global__ void fused_pass(const SrcPtr2d src, DstPtr2d dst, NVCVRectI roi, Filter &filterp, int h_ksize, int v_ksize, + int *h_bounds, work_type *h_kk, int *v_bounds, work_type *v_kk, work_type init_buffer, + bool round_up, bool use_share_mem) { - const int dst_x = blockIdx.x * blockDim.x + threadIdx.x; - const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; - const int local_y = threadIdx.y; - const int y_offset = blockIdx.y * blockDim.y; - const int batch_idx = get_batch_idx(); - int out_height = dst.rows, out_width = dst.cols; - work_type *v_k_tmp = v_kk + y_offset * v_ksize; - - if (use_share_mem) + static_assert(NC > 0, "fused_pass is the interleaved channel-vectorized path"); + const int dst_x = blockIdx.x * blockDim.x + threadIdx.x; + const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + const int out_width = dst.cols; + const int out_height = dst.rows; + + if (dst_x >= out_width || dst_y >= out_height) + return; + + const int xmin = h_bounds[dst_x * 2]; + const int xmax = h_bounds[dst_x * 2 + 1]; + const int ymin = v_bounds[dst_y * 2]; + const int ymax = v_bounds[dst_y * 2 + 1]; + + const work_type *h_k = &h_kk[dst_x * h_ksize]; + const work_type *v_k = &v_kk[dst_y * v_ksize]; + + using vec_t = cuda::MakeType; + using accum_t = cuda::MakeType; + + accum_t ss = cuda::SetAll(init_buffer); + for (int y = 0; y < ymax; ++y) { - const int local_tid = threadIdx.x + blockDim.x * threadIdx.y; - extern __shared__ __align__(sizeof(work_type)) unsigned char kk_smem_v[]; - v_k_tmp = reinterpret_cast(kk_smem_v); + // inner == the intermediate value at (ymin + y, dst_x): identical taps, identical order. + const std::byte *p = reinterpret_cast(src.ptr(batch_idx, ymin + y, xmin, 0)); + accum_t inner = cuda::SetAll(work_type{0}); + for (int x = 0; x < xmax; ++x) + { + inner = inner + h_k[x] * cuda::StaticCast(*reinterpret_cast(p)); + p += sizeof(vec_t); + } + ss = ss + v_k[y] * inner; + } - for (int i = local_tid; i < blockDim.y * v_ksize && i < (out_height - y_offset) * v_ksize; - i += blockDim.x * blockDim.y) + vec_t *out = reinterpret_cast(dst.ptr(batch_idx, dst_y, dst_x, 0)); + if (round_up) + { + work_type *acc = reinterpret_cast(&ss); +#pragma unroll + for (int c = 0; c < NC; ++c) reinterpret_cast(out)[c] = cuda::SaturateCast(std::round(acc[c])); + } + else + { + *out = cuda::SaturateCast(ss); + } +} + +// Fused single-pass resize for the planar (NCHW) FLOAT path with a compile-time channel count. +// Channels live in separate planes, so each plane's horizontal window is walked from a hoisted +// per-plane row pointer; taps and order match horizontal_pass_planar_channels followed by the +// planar vertical pass through the float intermediate (identity between passes -- bit-exact), with +// the intermediate kept in registers instead of round-tripping DRAM. FLOAT ONLY for the same +// quantization reason as fused_pass; the byte rows are issue-bound and measured slower fused. The +// per-output bounds/weight setup is shared across all PC planes, like the separable planar kernels. +template +__global__ void fused_pass_planar(const SrcPtr2d src, DstPtr2d dst, NVCVRectI roi, Filter &filterp, int h_ksize, + int v_ksize, int *h_bounds, work_type *h_kk, int *v_bounds, work_type *v_kk, + work_type init_buffer, bool round_up, bool use_share_mem) +{ + static_assert(PC > 0, "fused planar pass needs a compile-time plane count"); + const int dst_x = blockIdx.x * blockDim.x + threadIdx.x; + const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + const int out_width = dst.cols; + const int out_height = dst.rows; + + if (dst_x >= out_width || dst_y >= out_height) + return; + + const int xmin = h_bounds[dst_x * 2]; + const int xmax = h_bounds[dst_x * 2 + 1]; + const int ymin = v_bounds[dst_y * 2]; + const int ymax = v_bounds[dst_y * 2 + 1]; + + const work_type *h_k = &h_kk[dst_x * h_ksize]; + const work_type *v_k = &v_kk[dst_y * v_ksize]; + + work_type ss[PC]; +#pragma unroll + for (int c = 0; c < PC; ++c) ss[c] = init_buffer; + + for (int y = 0; y < ymax; ++y) + { + const T *row[PC]; +#pragma unroll + for (int c = 0; c < PC; ++c) row[c] = src.ptr(batch_idx, ymin + y, xmin, c); + + work_type inner[PC]; +#pragma unroll + for (int c = 0; c < PC; ++c) inner[c] = work_type{0}; + for (int x = 0; x < xmax; ++x) { - v_k_tmp[i] = v_kk[y_offset * v_ksize + i]; + const work_type k = h_k[x]; +#pragma unroll + for (int c = 0; c < PC; ++c) inner[c] = inner[c] + row[c][x] * k; } + const work_type vk = v_k[y]; +#pragma unroll + for (int c = 0; c < PC; ++c) ss[c] = ss[c] + vk * inner[c]; + } - __syncthreads(); +#pragma unroll + for (int c = 0; c < PC; ++c) + { + if (round_up) + *dst.ptr(batch_idx, dst_y, dst_x, c) = cuda::SaturateCast(std::round(ss[c])); + else + *dst.ptr(batch_idx, dst_y, dst_x, c) = cuda::SaturateCast(ss[c]); } +} - if (dst_x < out_width && dst_y < out_height) +// Planar (NCHW) vertical pass, vectorized across the X (column) dimension. Channels live in separate +// planes so the channel loop stays, but within a plane consecutive output columns are contiguous in +// memory AND share the same vertical resampling weights v_k (they only depend on dst_y). Each thread +// therefore computes VEC consecutive output columns: one MakeType coalesced load per tap feeds +// VEC accumulators, collapsing VEC byte-loads + VEC index computations per tap into one wide access. +// The launch site only selects this kernel when every relevant base pointer / stride is aligned to +// sizeof(MakeType) (vector loads require natural alignment); otherwise the scalar pass runs. +// The partial tail block (out_width not a multiple of VEC) falls back to a per-column scalar loop. +template +__global__ void vertical_pass_planar_vec(const SrcPtr2d src, DstPtr2d dst, NVCVRectI roi, Filter &filterp, int h_ksize, + int v_ksize, int *h_bounds, work_type *h_kk, int *v_bounds, work_type *v_kk, + work_type init_buffer, bool round_up, bool use_share_mem) +{ + const int dst_x0 = (blockIdx.x * blockDim.x + threadIdx.x) * VEC; + const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int local_y = threadIdx.y; + const int y_offset = blockIdx.y * blockDim.y; + const int batch_idx = get_batch_idx(); + int out_height = dst.rows, out_width = dst.cols; + + extern __shared__ __align__(sizeof(work_type)) unsigned char kk_smem_vp[]; + work_type *v_k_tmp = PillowStageCoeffs(v_kk, reinterpret_cast(kk_smem_vp), y_offset, v_ksize, + out_height, blockDim.y, use_share_mem); + + if (dst_x0 < out_width && dst_y < out_height) { int ymin = v_bounds[dst_y * 2]; int ymax = v_bounds[dst_y * 2 + 1]; work_type *v_k = &v_k_tmp[local_y * v_ksize]; + using vec_t = cuda::MakeType; + using accum_t = cuda::MakeType; + + const bool full = (dst_x0 + VEC <= out_width); for (int c = 0; c < src.ch; ++c) { - work_type ss = init_buffer; - for (int y = 0; y < ymax; ++y) + if (full) { - ss = ss + *src.ptr(batch_idx, y + ymin, dst_x, c) * v_k[y]; + const std::byte *p = reinterpret_cast(src.ptr(batch_idx, ymin, dst_x0, c)); + accum_t ss = cuda::SetAll(init_buffer); + for (int y = 0; y < ymax; ++y) + { + ss = ss + v_k[y] * cuda::StaticCast(*reinterpret_cast(p)); + p += src.rowStride; + } + + vec_t *out = reinterpret_cast(dst.ptr(batch_idx, dst_y, dst_x0, c)); + if (round_up) + { + work_type *acc = reinterpret_cast(&ss); +#pragma unroll + for (int i = 0; i < VEC; ++i) + reinterpret_cast(out)[i] = cuda::SaturateCast(std::round(acc[i])); + } + else + { + *out = cuda::SaturateCast(ss); + } } + else + { + for (int i = 0; i < VEC && dst_x0 + i < out_width; ++i) + { + const int dst_x = dst_x0 + i; + work_type ss = init_buffer; + for (int y = 0; y < ymax; ++y) + { + ss = ss + *src.ptr(batch_idx, y + ymin, dst_x, c) * v_k[y]; + } - if (round_up) - *dst.ptr(batch_idx, dst_y, dst_x, c) = cuda::SaturateCast(std::round(ss)); + if (round_up) + *dst.ptr(batch_idx, dst_y, dst_x, c) = cuda::SaturateCast(std::round(ss)); + else + *dst.ptr(batch_idx, dst_y, dst_x, c) = cuda::SaturateCast(ss); + } + } + } + } +} + +// Planar vertical pass computing two adjacent output rows per thread (VEC columns each). Adjacent +// output rows' vertical tap windows overlap by roughly k minus the scale step, so the shared source +// rows are loaded -- and converted to work_type -- once, then accumulated into both outputs (three +// segment loops like horizontal_pass_paired). Per-output tap order and FMA sequence are identical +// to vertical_pass_planar_vec (bit-exact); only the load/convert count drops. The launch site +// doubles the per-block output-row span for the coefficient indexing. +template +__global__ void vertical_pass_planar_vec_paired(const SrcPtr2d src, DstPtr2d dst, NVCVRectI roi, Filter &filterp, + int h_ksize, int v_ksize, int *h_bounds, work_type *h_kk, int *v_bounds, + work_type *v_kk, work_type init_buffer, bool round_up, + bool use_share_mem) +{ + const int dst_x0 = (blockIdx.x * blockDim.x + threadIdx.x) * VEC; + const int row0 = (blockIdx.y * blockDim.y + threadIdx.y) * 2; + const int local_y = threadIdx.y; + const int y_offset = blockIdx.y * blockDim.y * 2; + const int batch_idx = get_batch_idx(); + int out_height = dst.rows, out_width = dst.cols; + + extern __shared__ __align__(sizeof(work_type)) unsigned char kk_smem_vpq[]; + work_type *v_k_tmp = PillowStageCoeffs(v_kk, reinterpret_cast(kk_smem_vpq), y_offset, v_ksize, + out_height, blockDim.y * 2, use_share_mem); + + if (dst_x0 < out_width && row0 < out_height) + { + const bool has1 = row0 + 1 < out_height; + const int ymin0 = v_bounds[row0 * 2]; + const int ymax0 = v_bounds[row0 * 2 + 1]; + const int end0 = ymin0 + ymax0; + // Without a second output row, park its window at end0 so the shared and suffix loops are + // empty and k1 (one slot past this image's coefficients) is never read. + const int ymin1 = has1 ? v_bounds[(row0 + 1) * 2] : end0; + const int ymax1 = has1 ? v_bounds[(row0 + 1) * 2 + 1] : 0; + const int end1 = ymin1 + ymax1; + + work_type *k0 = &v_k_tmp[(local_y * 2) * v_ksize]; + work_type *k1 = &v_k_tmp[(local_y * 2 + 1) * v_ksize]; + + const int preEnd = min(ymin1, end0); + const int sufBegin = max(ymin1, end0); + + using vec_t = cuda::MakeType; + using accum_t = cuda::MakeType; + + const bool full = (dst_x0 + VEC <= out_width); + for (int c = 0; c < src.ch; ++c) + { + if (full) + { + accum_t s0 = cuda::SetAll(init_buffer); + accum_t s1 = cuda::SetAll(init_buffer); + + const std::byte *p = reinterpret_cast(src.ptr(batch_idx, ymin0, dst_x0, c)); + for (int y = ymin0; y < preEnd; ++y) + { + s0 = s0 + k0[y - ymin0] * cuda::StaticCast(*reinterpret_cast(p)); + p += src.rowStride; + } + for (int y = preEnd; y < end0; ++y) + { + const accum_t px = cuda::StaticCast(*reinterpret_cast(p)); + s0 = s0 + k0[y - ymin0] * px; + s1 = s1 + k1[y - ymin1] * px; + p += src.rowStride; + } + const std::byte *ps = reinterpret_cast(src.ptr(batch_idx, sufBegin, dst_x0, c)); + for (int y = sufBegin; y < end1; ++y) + { + s1 = s1 + k1[y - ymin1] * cuda::StaticCast(*reinterpret_cast(ps)); + ps += src.rowStride; + } + + vec_t *out0 = reinterpret_cast(dst.ptr(batch_idx, row0, dst_x0, c)); + if (round_up) + { + work_type *acc = reinterpret_cast(&s0); +#pragma unroll + for (int i = 0; i < VEC; ++i) + reinterpret_cast(out0)[i] = cuda::SaturateCast(std::round(acc[i])); + } + else + { + *out0 = cuda::SaturateCast(s0); + } + if (has1) + { + vec_t *out1 = reinterpret_cast(dst.ptr(batch_idx, row0 + 1, dst_x0, c)); + if (round_up) + { + work_type *acc = reinterpret_cast(&s1); +#pragma unroll + for (int i = 0; i < VEC; ++i) + reinterpret_cast(out1)[i] = cuda::SaturateCast(std::round(acc[i])); + } + else + { + *out1 = cuda::SaturateCast(s1); + } + } + } else - *dst.ptr(batch_idx, dst_y, dst_x, c) = cuda::SaturateCast(ss); + { + // Column tail: two independent scalar walks, identical to vertical_pass_planar_vec's + // tail loop for each output row. + for (int i = 0; i < VEC && dst_x0 + i < out_width; ++i) + { + const int dst_x = dst_x0 + i; + work_type ss = init_buffer; + for (int y = 0; y < ymax0; ++y) + { + ss = ss + *src.ptr(batch_idx, y + ymin0, dst_x, c) * k0[y]; + } + if (round_up) + *dst.ptr(batch_idx, row0, dst_x, c) = cuda::SaturateCast(std::round(ss)); + else + *dst.ptr(batch_idx, row0, dst_x, c) = cuda::SaturateCast(ss); + + if (has1) + { + work_type st = init_buffer; + for (int y = 0; y < ymax1; ++y) + { + st = st + *src.ptr(batch_idx, y + ymin1, dst_x, c) * k1[y]; + } + if (round_up) + *dst.ptr(batch_idx, row0 + 1, dst_x, c) = cuda::SaturateCast(std::round(st)); + else + *dst.ptr(batch_idx, row0 + 1, dst_x, c) = cuda::SaturateCast(st); + } + } + } } } } @@ -231,7 +797,7 @@ __global__ void vertical_pass(const cuda_op::Ptr2dNHWC src, cuda_op::Ptr2dNHW template void pillow_resize_v2(const TensorDataAccessStridedImagePlanar &inData, const TensorDataAccessStridedImagePlanar &outData, void *gpu_workspace, bool normalize_coeff, - work_type init_buffer, bool round_up, cudaStream_t stream) + work_type init_buffer, bool round_up, bool planar, cudaStream_t stream) { cuda_op::DataShape input_shape = GetLegacyDataShape(inData.infoShape()); Ptr2dNHWC src_ptr(inData); @@ -263,13 +829,16 @@ void pillow_resize_v2(const TensorDataAccessStridedImagePlanar &inData, int h_k_size = static_cast(ceil(h_support)) * 2 + 1; int v_k_size = static_cast(ceil(v_support)) * 2 + 1; - work_type *h_kk = (work_type *)((char *)gpu_workspace); - work_type *v_kk = (work_type *)((char *)h_kk + dst_ptr.cols * h_k_size * sizeof(work_type)); - int *h_bounds = (int *)((char *)v_kk + dst_ptr.rows * v_k_size * sizeof(work_type)); - int *v_bounds = (int *)((char *)h_bounds + dst_ptr.cols * 2 * sizeof(int)); - elem_type *d_h_data = (elem_type *)((char *)v_bounds + dst_ptr.rows * 2 * sizeof(int)); - - Ptr2dNHWC h_ptr(input_shape.N, input_shape.H, out_width, input_shape.C, (elem_type *)d_h_data); + work_type *h_kk = (work_type *)((char *)gpu_workspace); + work_type *v_kk = (work_type *)((char *)h_kk + dst_ptr.cols * h_k_size * sizeof(work_type)); + int *h_bounds = (int *)((char *)v_kk + dst_ptr.rows * v_k_size * sizeof(work_type)); + int *v_bounds = (int *)((char *)h_bounds + dst_ptr.cols * 2 * sizeof(int)); + // The intermediate buffer is read/written with up-to-16-byte vector loads/stores (MakeType), + // which require natural alignment. The coefficient/bounds regions above are work_type/int sized, so + // the raw offset is only 4-byte aligned -- round d_h_data up to 16 bytes. getWorkspaceRequirements + // reserves an extra 16 bytes for this padding. + std::uintptr_t d_h_raw = reinterpret_cast((char *)v_bounds + dst_ptr.rows * 2 * sizeof(int)); + elem_type *d_h_data = reinterpret_cast((d_h_raw + 15) & ~std::uintptr_t(15)); dim3 blockSize(BLOCK, BLOCK / 4, 1); dim3 gridSizeH(divUp(out_width, blockSize.x), divUp(input_shape.H, blockSize.y), input_shape.N); @@ -322,15 +891,255 @@ void pillow_resize_v2(const TensorDataAccessStridedImagePlanar &inData, checkCudaErrors(cudaGetLastError()); #endif - horizontal_pass - <<>>(src_ptr, h_ptr, roi, filterp, h_k_size, v_k_size, h_bounds, - h_kk, v_bounds, v_kk, init_buffer, round_up, hv_use_share_mem); - checkKernelErrors(); - vertical_pass - <<>>(h_ptr, dst_ptr, roi, filterp, h_k_size, v_k_size, h_bounds, - h_kk, v_bounds, v_kk, init_buffer, round_up, hv_use_share_mem); + // Horizontal then vertical separable pass. The intermediate buffer holds the horizontally-resized + // image (out_width columns, source rows) in the SAME layout as the input so the channel loop and + // grid (z = N) match. Interleaved uses Ptr2dNHWC; planar uses Ptr2dNCHW, which keeps one z-slice + // per sample and amortizes the per-pixel bounds/weight setup across the C planes. + auto launch = [&](auto nc_const, auto src_p, auto inter_p, auto dst_p) + { + constexpr int NC = decltype(nc_const)::value; + // The fused kernel removes the intermediate write/read, but recomputes each horizontal sum for + // every vertical tap. Upscales win across the measured GPUs; small-kernel downscales win only on + // the architecture families selected by PillowResizeSupportsFusedDownscale. FLOAT only: an + // integer intermediate is quantized between passes (Pillow semantics), and the byte rows are + // issue-bound, not DRAM-bound -- the fused recompute measured 25-41% slower for uchar3. + constexpr bool can_fuse = (NC > 0 && std::is_same_v); + const bool is_upscale = h_scale < work_type(1) && v_scale < work_type(1); + const bool small_kernel = h_k_size <= kPillowResizeFuseMaxKSize && v_k_size <= kPillowResizeFuseMaxKSize; + const bool fuse_pass = can_fuse && filterp.support() <= work_type(1) + && (is_upscale || (small_kernel && PillowResizeSupportsFusedDownscale())); + if (fuse_pass) + { + if constexpr (can_fuse) // guard instantiation: fused_pass static_asserts NC > 0 + { + (void)inter_p; // the intermediate buffer is unused on the fused path + fused_pass + <<>>(src_p, dst_p, roi, filterp, h_k_size, v_k_size, h_bounds, + h_kk, v_bounds, v_kk, init_buffer, round_up, false); + checkKernelErrors(); + } + } + else + { + // Pairing wins where neighboring tap windows are wide and overlap: three-channel byte + // pixels on downscale. Float pixels and upscale windows measured slower paired locally; + // single-channel and four-channel rows regressed on the A100/H100 regen burnins. + bool paired = false; + if constexpr (NC == 3 && sizeof(elem_type) == 1) + { + if (h_scale > work_type(1)) + { + const size_t paired_sm = 2 * hv_sm_size1; + const bool paired_use = hv_use_share_mem && paired_sm <= SHARE_MEM_LIMIT; + dim3 gridSizeHP(divUp(out_width, 2 * static_cast(blockSize.x)), + divUp(input_shape.H, blockSize.y), input_shape.N); + horizontal_pass_paired + <<>>( + src_p, inter_p, roi, filterp, h_k_size, v_k_size, h_bounds, h_kk, v_bounds, v_kk, + init_buffer, round_up, paired_use); + paired = true; + } + } + if (!paired) + { + horizontal_pass<<>>( + src_p, inter_p, roi, filterp, h_k_size, v_k_size, h_bounds, h_kk, v_bounds, v_kk, init_buffer, + round_up, hv_use_share_mem); + } + checkKernelErrors(); + constexpr int VEC = 4; + constexpr size_t amask = alignof(cuda::MakeType) - 1; + auto flatOk = [&](const auto &p) + { + return ((reinterpret_cast(p.data) | static_cast(p.imgStride) + | static_cast(p.rowStride)) + & amask) + == 0; + }; + // Four-channel pixels already move as one vector per tap in vertical_pass; the flat + // mapping only reshuffles threads there and measured slower on A100 (expand uchar4). + if (NC != 4 && flatOk(inter_p) && flatOk(dst_p)) + { + const int row_elems = out_width * input_shape.C; + dim3 gridSizeVV(divUp(divUp(row_elems, VEC), static_cast(blockSize.x)), + divUp(out_height, blockSize.y), input_shape.N); + // Threads of a warp share dst_y here, so the per-tap weight loads are L1 + // broadcasts; staging them through shared memory only adds the block barrier. + vertical_pass_vec + <<>>(inter_p, dst_p, roi, filterp, h_k_size, v_k_size, h_bounds, + h_kk, v_bounds, v_kk, init_buffer, round_up, false); + } + else + { + vertical_pass<<>>( + inter_p, dst_p, roi, filterp, h_k_size, v_k_size, h_bounds, h_kk, v_bounds, v_kk, init_buffer, + round_up, hv_use_share_mem); + } + checkKernelErrors(); + } + }; - checkKernelErrors(); + if (planar) + { + // Planar (NCHW): channels are in separate planes. The horizontal pass uses the scalar channel + // loop (NC == 0); the vertical pass is x-vectorized (consecutive columns share v_k) when the + // intermediate and output buffers are vector-aligned, else scalar. + Ptr2dNCHW src_p(inData); + Ptr2dNCHW inter_p(input_shape.N, input_shape.H, out_width, input_shape.C, (elem_type *)d_h_data); + Ptr2dNCHW dst_p(outData); + + // Fused planar resize (FLOAT only): drops the intermediate round-trip and reproduces the + // separable result bit-exactly. Upscale fusion is gated to four channels and moderate ratios: + // on the reference GPUs three-channel expands measured 0.95-1.00x fused (A100) and extreme + // upscales (scale < 0.4, e.g. 4.5x anisotropic) regressed to 0.80x -- the intermediate + // shrinks relative to the output, so the recompute and the lost vertical vectorization + // outweigh the saved traffic. Downscale fusion wins on both reference GPUs for 3 and 4 + // channels (1.03-1.47x) and keeps the architecture gate. + if constexpr (std::is_same_v) + { + const bool is_upscale = h_scale < work_type(1) && v_scale < work_type(1); + const bool moderate_up = h_scale >= work_type(0.4) && v_scale >= work_type(0.4); + const bool small_kernel = h_k_size <= kPillowResizeFuseMaxKSize && v_k_size <= kPillowResizeFuseMaxKSize; + const bool fuse_planar = filterp.support() <= work_type(1) + && ((is_upscale && moderate_up && input_shape.C == 4) + || (!is_upscale && small_kernel && PillowResizeSupportsFusedDownscale() + && (input_shape.C == 3 || input_shape.C == 4))); + if (fuse_planar) + { + if (input_shape.C == 3) + { + fused_pass_planar<3, elem_type, Filter> + <<>>(src_p, dst_p, roi, filterp, h_k_size, v_k_size, h_bounds, + h_kk, v_bounds, v_kk, init_buffer, round_up, false); + } + else + { + fused_pass_planar<4, elem_type, Filter> + <<>>(src_p, dst_p, roi, filterp, h_k_size, v_k_size, h_bounds, + h_kk, v_bounds, v_kk, init_buffer, round_up, false); + } + checkKernelErrors(); + return; + } + } + + switch (input_shape.C) + { + case 3: + horizontal_pass_planar_channels<3, elem_type, Filter><<>>( + src_p, inter_p, roi, filterp, h_k_size, v_k_size, h_bounds, h_kk, v_bounds, v_kk, init_buffer, round_up, + hv_use_share_mem); + break; + case 4: + if constexpr (!std::is_same_v) + { + horizontal_pass_planar_channels<4, elem_type, Filter><<>>( + src_p, inter_p, roi, filterp, h_k_size, v_k_size, h_bounds, h_kk, v_bounds, v_kk, init_buffer, + round_up, hv_use_share_mem); + } + else + { + horizontal_pass<0, elem_type, Filter><<>>( + src_p, inter_p, roi, filterp, h_k_size, v_k_size, h_bounds, h_kk, v_bounds, v_kk, init_buffer, + round_up, hv_use_share_mem); + } + break; + default: + horizontal_pass<0, elem_type, Filter><<>>( + src_p, inter_p, roi, filterp, h_k_size, v_k_size, h_bounds, h_kk, v_bounds, v_kk, init_buffer, round_up, + hv_use_share_mem); + break; + } + checkKernelErrors(); + + constexpr int VEC = 4; + const size_t alignMask = VEC * sizeof(elem_type) - 1; + auto aligned = [&](const Ptr2dNCHW &p) + { + return ((reinterpret_cast(p.data) | static_cast(p.imgStride) + | static_cast(p.chStride) | static_cast(p.rowStride)) + & alignMask) + == 0; + }; + if (aligned(inter_p) && aligned(dst_p)) + { + // Threads of a warp share the output row (or row pair), so per-tap weight loads are L1 + // broadcasts; skip the shared-memory staging and its block barrier. Row pairing only + // pays where adjacent output rows actually share source taps AND the shared load also + // shares a byte-to-float convert: three-plane byte linear upscale. Downscale windows are + // disjoint, float rows have no convert to share, and four byte planes regressed 6% on + // A100 (the extra plane iteration doubles the paired accumulator pressure). + if (sizeof(elem_type) == 1 && input_shape.C == 3 && v_scale < work_type(1) + && filterp.support() <= work_type(1)) + { + dim3 gridSizeVP(divUp(divUp(out_width, VEC), static_cast(blockSize.x)), + divUp(out_height, 2 * static_cast(blockSize.y)), input_shape.N); + vertical_pass_planar_vec_paired + <<>>(inter_p, dst_p, roi, filterp, h_k_size, v_k_size, h_bounds, + h_kk, v_bounds, v_kk, init_buffer, round_up, false); + } + else + { + dim3 gridSizeVP(divUp(divUp(out_width, VEC), static_cast(blockSize.x)), + divUp(out_height, blockSize.y), input_shape.N); + vertical_pass_planar_vec + <<>>(inter_p, dst_p, roi, filterp, h_k_size, v_k_size, h_bounds, + h_kk, v_bounds, v_kk, init_buffer, round_up, false); + } + } + else + { + vertical_pass<0, elem_type, Filter><<>>( + inter_p, dst_p, roi, filterp, h_k_size, v_k_size, h_bounds, h_kk, v_bounds, v_kk, init_buffer, round_up, + hv_use_share_mem); + } + checkKernelErrors(); + } + else + { + // Interleaved (NHWC): vectorize over the contiguous channels at the known channel count. + Ptr2dNHWC src_p(inData); + Ptr2dNHWC inter_p(input_shape.N, input_shape.H, out_width, input_shape.C, (elem_type *)d_h_data); + Ptr2dNHWC dst_p(outData); + + // The vectorized pixel load/store requires the buffers be aligned to alignof(MakeType). + // The intermediate is 16-byte aligned above, but the user input/output tensors may have + // arbitrary row strides -- gate on all three and fall back to the scalar loop (NC=0) otherwise. + auto try_vec = [&](auto nc_const) + { + constexpr int NC = decltype(nc_const)::value; + constexpr size_t a = alignof(cuda::MakeType); + auto ok = [](const Ptr2dNHWC &p) + { + return ((reinterpret_cast(p.data) | static_cast(p.imgStride) + | static_cast(p.rowStride)) + & (a - 1)) + == 0; + }; + if (ok(src_p) && ok(inter_p) && ok(dst_p)) + launch(nc_const, src_p, inter_p, dst_p); + else + launch(std::integral_constant{}, src_p, inter_p, dst_p); + }; + switch (input_shape.C) + { + case 1: + try_vec(std::integral_constant{}); + break; + case 2: + try_vec(std::integral_constant{}); + break; + case 3: + try_vec(std::integral_constant{}); + break; + case 4: + try_vec(std::integral_constant{}); + break; + default: + launch(std::integral_constant{}, src_p, inter_p, dst_p); + break; + } + } #ifdef CUDA_DEBUG_LOG checkCudaErrors(cudaStreamSynchronize(stream)); checkCudaErrors(cudaGetLastError()); @@ -340,28 +1149,28 @@ void pillow_resize_v2(const TensorDataAccessStridedImagePlanar &inData, template void pillow_resize_filter(const TensorDataAccessStridedImagePlanar &inData, const TensorDataAccessStridedImagePlanar &outData, void *gpu_workspace, - NVCVInterpolationType interpolation, cudaStream_t stream) + NVCVInterpolationType interpolation, bool planar, cudaStream_t stream) { cuda_op::DataType data_type = GetLegacyDataType(inData.dtype()); switch (data_type) { case kCV_8U: - pillow_resize_v2(inData, outData, gpu_workspace, false, 0., false, stream); + pillow_resize_v2(inData, outData, gpu_workspace, false, 0., false, planar, stream); break; case kCV_8S: - pillow_resize_v2(inData, outData, gpu_workspace, false, 0., true, stream); + pillow_resize_v2(inData, outData, gpu_workspace, false, 0., true, planar, stream); break; case kCV_16U: - pillow_resize_v2(inData, outData, gpu_workspace, false, 0., false, stream); + pillow_resize_v2(inData, outData, gpu_workspace, false, 0., false, planar, stream); break; case kCV_16S: - pillow_resize_v2(inData, outData, gpu_workspace, false, 0., true, stream); + pillow_resize_v2(inData, outData, gpu_workspace, false, 0., true, planar, stream); break; case kCV_32S: - pillow_resize_v2(inData, outData, gpu_workspace, false, 0., true, stream); + pillow_resize_v2(inData, outData, gpu_workspace, false, 0., true, planar, stream); break; case kCV_32F: - pillow_resize_v2(inData, outData, gpu_workspace, false, 0., false, stream); + pillow_resize_v2(inData, outData, gpu_workspace, false, 0., false, planar, stream); break; default: break; @@ -380,7 +1189,9 @@ WorkspaceRequirements PillowResize::getWorkspaceRequirements(DataShape max_input + max_output_shape.W * (((1.0 * max_input_shape.W / max_output_shape.W + 1) * max_support * 2 + 1) * sizeof(work_type) + 2 * sizeof(int))) - + max_input_shape.N * max_input_shape.C * max_input_shape.H * max_output_shape.W * DataSize(max_data_type); + + static_cast(max_input_shape.N) * max_input_shape.C * max_input_shape.H * max_output_shape.W + * DataSize(max_data_type) + + 16; // padding to 16-byte-align the intermediate buffer (vector loads/stores) WorkspaceRequirements req{}; req.cudaMem = {size, 256}; return req; @@ -402,11 +1213,13 @@ ErrorCode PillowResize::infer(const TensorDataStridedCuda &inData, const TensorD LOG_ERROR("Invalid DataFormat between input (" << format << ") and output (" << output_format << ")"); return ErrorCode::INVALID_DATA_FORMAT; } - if (!(format == kNHWC || format == kHWC)) + if (!(format == kNHWC || format == kHWC || format == kNCHW || format == kCHW)) { - LOG_ERROR("Invalid input DataFormat " << format << ", the valid DataFormats are: \"NHWC\", \"HWC\""); + LOG_ERROR("Invalid input DataFormat " << format + << ", the valid DataFormats are: \"NHWC\", \"HWC\", \"NCHW\", \"CHW\""); return ErrorCode::INVALID_DATA_FORMAT; } + const bool planar = (format == kNCHW || format == kCHW); auto inAccess = TensorDataAccessStridedImagePlanar::Create(inData); NVCV_ASSERT(inAccess); @@ -432,22 +1245,40 @@ ErrorCode PillowResize::infer(const TensorDataStridedCuda &inData, const TensorD return ErrorCode::INVALID_DATA_TYPE; } + // The kernels compute addresses as 32-bit products of sample/row strides (Ptr2dNHWC/Ptr2dNCHW), + // so any tensor -- input, output, or the horizontally-resized intermediate -- whose byte extent + // exceeds INT32_MAX overflows the addressing. Reject those instead of corrupting memory. + { + constexpr int64_t kMaxByteExtent = std::numeric_limits::max(); + const int64_t interExtent = static_cast(input_shape.N) * input_shape.C * input_shape.H + * outAccess->numCols() * DataSize(data_type); + const int64_t maxExtent = std::max( + {inAccess->numSamples() * inAccess->sampleStride(), outAccess->numSamples() * outAccess->sampleStride(), + inAccess->numRows() * inAccess->rowStride(), outAccess->numRows() * outAccess->rowStride(), interExtent}); + if (maxExtent > kMaxByteExtent) + { + LOG_ERROR("Tensor byte extent " << maxExtent << " exceeds the 32-bit addressing limit " << kMaxByteExtent + << "; split the batch into smaller submissions"); + return ErrorCode::INVALID_DATA_SHAPE; + } + } + switch (interpolation) { case NVCV_INTERP_LINEAR: - pillow_resize_filter(*inAccess, *outAccess, gpu_workspace, interpolation, stream); + pillow_resize_filter(*inAccess, *outAccess, gpu_workspace, interpolation, planar, stream); break; case NVCV_INTERP_CUBIC: - pillow_resize_filter(*inAccess, *outAccess, gpu_workspace, interpolation, stream); + pillow_resize_filter(*inAccess, *outAccess, gpu_workspace, interpolation, planar, stream); break; case NVCV_INTERP_LANCZOS: - pillow_resize_filter(*inAccess, *outAccess, gpu_workspace, interpolation, stream); + pillow_resize_filter(*inAccess, *outAccess, gpu_workspace, interpolation, planar, stream); break; case NVCV_INTERP_BOX: - pillow_resize_filter(*inAccess, *outAccess, gpu_workspace, interpolation, stream); + pillow_resize_filter(*inAccess, *outAccess, gpu_workspace, interpolation, planar, stream); break; case NVCV_INTERP_HAMMING: - pillow_resize_filter(*inAccess, *outAccess, gpu_workspace, interpolation, stream); + pillow_resize_filter(*inAccess, *outAccess, gpu_workspace, interpolation, planar, stream); break; default: LOG_ERROR("Unsupported interpolation method " << interpolation); diff --git a/src/cvcuda/priv/legacy/pillow_resize.h b/src/cvcuda/priv/legacy/pillow_resize.h index 37429a997..8c990832c 100644 --- a/src/cvcuda/priv/legacy/pillow_resize.h +++ b/src/cvcuda/priv/legacy/pillow_resize.h @@ -35,6 +35,26 @@ using work_type = float; namespace nvcv::legacy::cuda_op { +static constexpr int kPillowResizeFuseMaxKSize = 5; + +inline bool PillowResizeSupportsFusedDownscale() +{ + // The fused kernel's compute-versus-memory tradeoff is not monotonic across GPU generations. + // Keep downscale fusion on the validated architectures and use the separable path elsewhere. + thread_local int cached_device = -1; + thread_local int cached_major = -1; + + int device; + checkCudaErrors(cudaGetDevice(&device)); + if (device != cached_device) + { + checkCudaErrors(cudaDeviceGetAttribute(&cached_major, cudaDevAttrComputeCapabilityMajor, device)); + cached_device = device; + } + + return cached_major == 8 || cached_major == 9; +} + static constexpr float bilinear_filter_support = 1.f; static constexpr float box_filter_support = 0.5f; static constexpr float hamming_filter_support = 1.f; @@ -196,4 +216,118 @@ class LanczosFilter work_type _support; }; +// Per-thread resampling-coefficient computation for one output position, shared by the tensor +// (`_precomputeCoeffs`) and var-shape (`_precomputeCoeffsVarShape`) precompute kernels. The two only +// differ in how they fetch their scalar parameters (direct kernel args vs. per-image arrays) and in +// the fixed-point `precision` shift, so the math itself lives here once. Writes the resampling weights +// into `kk_out` (cooperatively staged through shared memory when `use_share_mem`) and the [xmin, xmax) +// support bounds for output index `xx` into `bounds_out`. +template +__device__ inline void PillowPrecomputeCoeffs(int xx, int local_id, int x_offset, int in_size, int in0, work_type scale, + work_type filterscale, work_type support, int out_size, int k_size, + Filter &filterp, int *bounds_out, work_type *kk_out, bool normalize_coeff, + bool use_share_mem, unsigned int precision) +{ + work_type *kk = kk_out + x_offset * k_size; + if (use_share_mem) + { + extern __shared__ __align__(sizeof(work_type)) unsigned char smem_raw[]; + kk = reinterpret_cast(smem_raw); + } + + if (xx < out_size) + { + int x = 0; + int xmin = 0; + int xmax = 0; + work_type center = 0; + work_type ww = 0; + work_type ss = 0; + const work_type half_pixel = 0.5; + + center = in0 + (xx + half_pixel) * scale; + ww = 0.0; + ss = 1.0 / filterscale; + // Round the value. + xmin = static_cast(center - support + half_pixel); + if (xmin < 0) + { + xmin = 0; + } + // Round the value. + xmax = static_cast(center + support + half_pixel); + if (xmax > in_size) + { + xmax = in_size; + } + xmax -= xmin; + work_type *k = &kk[local_id * k_size]; + for (x = 0; x < xmax; ++x) + { + work_type w = filterp.filter((x + xmin - center + half_pixel) * ss); + k[x] = w; + ww += w; + } + for (x = 0; x < xmax; ++x) + { + if (std::fabs(ww) > 1e-5) + { + k[x] /= ww; + } + } + // Remaining values should stay empty if they are used despite of xmax. + for (; x < k_size; ++x) + { + k[x] = .0f; + } + if (normalize_coeff) + { + for (int i = 0; i < k_size; i++) + { + work_type val = k[i]; + if (val < 0) + { + k[i] = static_cast(-half_pixel + val * (1U << precision)); + } + else + { + k[i] = static_cast(half_pixel + val * (1U << precision)); + } + } + } + + bounds_out[xx * 2] = xmin; + bounds_out[xx * 2 + 1] = xmax; + } + if (use_share_mem) + { + __syncthreads(); + for (int i = local_id; i < (out_size - x_offset) * k_size && i < blockDim.x * k_size; i += blockDim.x) + { + kk_out[x_offset * k_size + i] = kk[i]; + } + } +} + +// Cooperatively stage this thread block's slice of the coefficient table into shared memory and return +// the pointer the thread should read its per-position weights from. When shared memory is disabled, +// returns the thread block's slice of the global table directly. `block_extent` is the block dimension +// spanning the staged axis (blockDim.x for the horizontal pass, blockDim.y for the vertical pass). +// Shared by the horizontal/vertical resize passes across the tensor, var-shape, and planar paths. +inline __device__ work_type *PillowStageCoeffs(work_type *kk_global, work_type *smem, int offset, int ksize, + int out_extent, int block_extent, bool use_share_mem) +{ + if (!use_share_mem) + { + return kk_global + offset * ksize; + } + const int local_tid = threadIdx.x + blockDim.x * threadIdx.y; + for (int i = local_tid; i < block_extent * ksize && i < (out_extent - offset) * ksize; i += blockDim.x * blockDim.y) + { + smem[i] = kk_global[offset * ksize + i]; + } + __syncthreads(); + return smem; +} + } // namespace nvcv::legacy::cuda_op diff --git a/src/cvcuda/priv/legacy/pillow_resize_var_shape.cu b/src/cvcuda/priv/legacy/pillow_resize_var_shape.cu index 5524f62a0..586e38941 100644 --- a/src/cvcuda/priv/legacy/pillow_resize_var_shape.cu +++ b/src/cvcuda/priv/legacy/pillow_resize_var_shape.cu @@ -26,6 +26,11 @@ #include +#include +#include +#include +#include + using namespace nvcv::legacy::cuda_op; using namespace nvcv::legacy::helpers; @@ -60,92 +65,537 @@ __global__ void _precomputeCoeffsVarShape(int *in_size_batch, int *in0_batch, wo int *bounds_out = bounds_out_batch + bound_out_offset[batch_idx]; work_type *kk_out = kk_out_batch + kk_out_offset[batch_idx]; - work_type *kk = kk_out + x_offset * k_size; - if (use_share_mem) - { - extern __shared__ __align__(sizeof(work_type)) unsigned char smem_raw[]; - kk = reinterpret_cast(smem_raw); - } + PillowPrecomputeCoeffs(xx, local_id, x_offset, in_size, in0, scale, filterscale, support, out_size, k_size, + filterp, bounds_out, kk_out, normalize_coeff, use_share_mem, + precision_bits_var_shape); +} + +template +__global__ void horizontal_pass_var_shape(const Ptr2dVarShapeNHWC src, Ptr2dNHWC dst, Filter &filterp, + int *h_ksize_batch, int *v_ksize_batch, int *h_bounds_batch, + int *h_bounds_offset, work_type *h_kk_batch, int *h_kk_offset, + int *v_bounds_batch, int *v_bounds_offset, work_type *v_kk_batch, + int *v_kk_offset, work_type init_buffer, bool round_up, bool use_share_mem) +{ + const int dst_x = blockIdx.x * blockDim.x + threadIdx.x; + const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int local_x = threadIdx.x; + const int x_offset = blockIdx.x * blockDim.x; + + const int batch_idx = get_batch_idx(); + int h_ksize = h_ksize_batch[batch_idx]; + int *h_bounds = h_bounds_batch + h_bounds_offset[batch_idx]; + work_type *h_kk = h_kk_batch + h_kk_offset[batch_idx]; + + int out_height = dst.at_rows(batch_idx), out_width = dst.at_cols(batch_idx); + int in_height = src.at_rows(batch_idx); + + extern __shared__ __align__(sizeof(work_type)) unsigned char kk_smem_h[]; + work_type *h_k_tmp = PillowStageCoeffs(h_kk, reinterpret_cast(kk_smem_h), x_offset, h_ksize, out_width, + blockDim.x, use_share_mem); - if (xx < out_size) + if (dst_x < out_width && dst_y < out_height) { - int x = 0; - int xmin = 0; - int xmax = 0; - work_type center = 0; - work_type ww = 0; - work_type ss = 0; - const work_type half_pixel = 0.5; - - center = in0 + (xx + half_pixel) * scale; - ww = 0.0; - ss = 1.0 / filterscale; - // Round the value. - xmin = static_cast(center - support + half_pixel); - if (xmin < 0) + int xmin = h_bounds[dst_x * 2]; + int xmax = h_bounds[dst_x * 2 + 1]; + + work_type *h_k = &h_k_tmp[local_x * h_ksize]; + + // The precompute step clamps xmin and the tap count to the source width, so this support + // stays in one row. The max-sized intermediate still needs the per-image height guard, but + // avoiding quotient/remainder work for every tap is material inside the hot loop. + + // Interleaved channel-vectorization (NC > 0): load the contiguous NC-channel input pixel as one + // MakeType vector and write the NC-channel float intermediate as one vector. The per-image + // input alignment is checked once (uniform across this block's image, blockIdx.z == image) and the + // scalar path runs when it is not vector-aligned. The intermediate is 16-byte aligned by the host. + bool did_vec = false; + if constexpr (NC > 0) { - xmin = 0; + using vin_t = cuda::MakeType; + using vout_t = cuda::MakeType; + using accum_t = cuda::MakeType; + const std::uintptr_t base = reinterpret_cast(src.ptr(batch_idx, 0, 0)); + const std::uintptr_t step + = reinterpret_cast(src.ptr(batch_idx, 1, 0)) - base; // row stride in bytes + if (((base | step) & (alignof(vin_t) - 1)) == 0) + { + accum_t h_ss = cuda::SetAll(work_type{0}); + if (dst_y < in_height) + { + const std::byte *p = reinterpret_cast(src.ptr(batch_idx, dst_y, xmin, 0)); + for (int x = 0; x < xmax; ++x) + { + h_ss = h_ss + h_k[x] * cuda::StaticCast(*reinterpret_cast(p)); + p += sizeof(vin_t); + } + } + vout_t *out = reinterpret_cast(dst.ptr(batch_idx, dst_y, dst_x, 0)); + if (round_up) + { + work_type *acc = reinterpret_cast(&h_ss); +#pragma unroll + for (int c = 0; c < NC; ++c) + reinterpret_cast(out)[c] = cuda::SaturateCast(std::round(acc[c])); + } + else + { + *out = cuda::SaturateCast(h_ss); + } + did_vec = true; + } } - // Round the value. - xmax = static_cast(center + support + half_pixel); - if (xmax > in_size) + if (!did_vec) { - xmax = in_size; + for (int c = 0; c < src.nch; ++c) + { + work_type h_ss = 0.0; + if (dst_y < in_height) + { + const std::byte *p = reinterpret_cast(src.ptr(batch_idx, dst_y, xmin, c)); + for (int x = 0; x < xmax; ++x) + { + h_ss = h_ss + *reinterpret_cast(p) * h_k[x]; + p += src.nch * sizeof(T1); + } + } + if (round_up) + *dst.ptr(batch_idx, dst_y, dst_x, c) = cuda::SaturateCast(std::round(h_ss)); + else + *dst.ptr(batch_idx, dst_y, dst_x, c) = cuda::SaturateCast(h_ss); + } } - xmax -= xmin; - work_type *k = &kk[local_id * k_size]; - for (x = 0; x < xmax; ++x) + } +} + +// Interleaved var-shape horizontal pass computing two adjacent output pixels per thread (see +// horizontal_pass_paired): the shared tap-window segment is loaded once and accumulated into both +// outputs with fma count and ascending per-output tap order identical to the per-pixel kernel. +// The per-image input alignment is checked once (uniform across this block's image); unaligned +// images take a per-output scalar channel loop. +template +__global__ void horizontal_pass_var_shape_paired(const Ptr2dVarShapeNHWC src, Ptr2dNHWC dst, Filter &filterp, + int *h_ksize_batch, int *out_cols_batch, int *h_bounds_batch, + int *h_bounds_offset, work_type *h_kk_batch, int *h_kk_offset, + work_type init_buffer, bool round_up, bool use_share_mem) +{ + static_assert(NC > 0, "paired var-shape horizontal pass is the interleaved channel-vectorized path"); + const int pair0 = (blockIdx.x * blockDim.x + threadIdx.x) * 2; + const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int local_x = threadIdx.x; + const int x_offset = blockIdx.x * blockDim.x * 2; + + const int batch_idx = get_batch_idx(); + int h_ksize = h_ksize_batch[batch_idx]; + int *h_bounds = h_bounds_batch + h_bounds_offset[batch_idx]; + work_type *h_kk = h_kk_batch + h_kk_offset[batch_idx]; + + // The intermediate is max-sized and uniform; the pair guards need this image's true output + // width so the second output of the last pair never indexes the next image's bounds. + int out_height = dst.at_rows(batch_idx), out_width = out_cols_batch[batch_idx]; + int in_height = src.at_rows(batch_idx); + + extern __shared__ __align__(sizeof(work_type)) unsigned char kk_smem_hp2[]; + work_type *h_k_tmp = PillowStageCoeffs(h_kk, reinterpret_cast(kk_smem_hp2), x_offset, h_ksize, + out_width, blockDim.x * 2, use_share_mem); + + if (pair0 < out_width && dst_y < out_height) + { + const bool has1 = pair0 + 1 < out_width; + const int xmin0 = h_bounds[pair0 * 2]; + const int xmax0 = h_bounds[pair0 * 2 + 1]; + const int end0 = xmin0 + xmax0; + // Without a second output, park its window at end0 so the shared and suffix loops are + // empty and k1 (one slot past this image's coefficients) is never read. + const int xmin1 = has1 ? h_bounds[(pair0 + 1) * 2] : end0; + const int xmax1 = has1 ? h_bounds[(pair0 + 1) * 2 + 1] : 0; + + work_type *k0 = &h_k_tmp[(local_x * 2) * h_ksize]; + work_type *k1 = &h_k_tmp[(local_x * 2 + 1) * h_ksize]; + + const int end1 = xmin1 + xmax1; + + using vin_t = cuda::MakeType; + using vout_t = cuda::MakeType; + using accum_t = cuda::MakeType; + + const std::uintptr_t ibase = reinterpret_cast(src.ptr(batch_idx, 0, 0)); + const std::uintptr_t istep = reinterpret_cast(src.ptr(batch_idx, 1, 0)) - ibase; + if (((ibase | istep) & (alignof(vin_t) - 1)) == 0) { - work_type w = filterp.filter((x + xmin - center + half_pixel) * ss); - k[x] = w; - ww += w; + accum_t s0 = cuda::SetAll(work_type{0}); + accum_t s1 = cuda::SetAll(work_type{0}); + if (dst_y < in_height) + { + const std::byte *base = reinterpret_cast(src.ptr(batch_idx, dst_y, 0)); + const int preEnd = min(xmin1, end0); + for (int x = xmin0; x < preEnd; ++x) + { + s0 = s0 + + k0[x - xmin0] + * cuda::StaticCast( + *reinterpret_cast(base + static_cast(x) * sizeof(vin_t))); + } + for (int x = xmin1; x < end0; ++x) + { + const accum_t px = cuda::StaticCast( + *reinterpret_cast(base + static_cast(x) * sizeof(vin_t))); + s0 = s0 + k0[x - xmin0] * px; + s1 = s1 + k1[x - xmin1] * px; + } + const int sufBegin = max(xmin1, end0); + for (int x = sufBegin; x < end1; ++x) + { + s1 = s1 + + k1[x - xmin1] + * cuda::StaticCast( + *reinterpret_cast(base + static_cast(x) * sizeof(vin_t))); + } + } + vout_t *out0 = reinterpret_cast(dst.ptr(batch_idx, dst_y, pair0, 0)); + if (round_up) + { + work_type *acc0 = reinterpret_cast(&s0); +#pragma unroll + for (int c = 0; c < NC; ++c) + reinterpret_cast(out0)[c] = cuda::SaturateCast(std::round(acc0[c])); + } + else + { + *out0 = cuda::SaturateCast(s0); + } + if (has1) + { + vout_t *out1 = reinterpret_cast(dst.ptr(batch_idx, dst_y, pair0 + 1, 0)); + if (round_up) + { + work_type *acc1 = reinterpret_cast(&s1); +#pragma unroll + for (int c = 0; c < NC; ++c) + reinterpret_cast(out1)[c] = cuda::SaturateCast(std::round(acc1[c])); + } + else + { + *out1 = cuda::SaturateCast(s1); + } + } } - for (x = 0; x < xmax; ++x) + else { - if (std::fabs(ww) > 1e-5) + for (int o = 0; o < (has1 ? 2 : 1); ++o) { - k[x] /= ww; + const int dst_x = pair0 + o; + const int xmin = o ? xmin1 : xmin0; + const int xmax = o ? xmax1 : xmax0; + work_type *hk = o ? k1 : k0; + for (int c = 0; c < src.nch; ++c) + { + work_type h_ss = 0.0; + if (dst_y < in_height) + { + const std::byte *p = reinterpret_cast(src.ptr(batch_idx, dst_y, xmin, c)); + for (int x = 0; x < xmax; ++x) + { + h_ss = h_ss + *reinterpret_cast(p) * hk[x]; + p += src.nch * sizeof(T1); + } + } + if (round_up) + *dst.ptr(batch_idx, dst_y, dst_x, c) = cuda::SaturateCast(std::round(h_ss)); + else + *dst.ptr(batch_idx, dst_y, dst_x, c) = cuda::SaturateCast(h_ss); + } } } - // Remaining values should stay empty if they are used despite of xmax. - for (; x < k_size; ++x) + } +} + +template +__global__ void vertical_pass_var_shape(const Ptr2dNHWC src, Ptr2dVarShapeNHWC dst, Filter &filterp, + int *h_ksize_batch, int *v_ksize_batch, int *h_bounds_batch, + int *h_bounds_offset, work_type *h_kk_batch, int *h_kk_offset, + int *v_bounds_batch, int *v_bounds_offset, work_type *v_kk_batch, + int *v_kk_offset, work_type init_buffer, bool round_up, bool use_share_mem) +{ + const int dst_x = blockIdx.x * blockDim.x + threadIdx.x; + const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int local_y = threadIdx.y; + const int y_offset = blockIdx.y * blockDim.y; + + const int batch_idx = get_batch_idx(); + int v_ksize = v_ksize_batch[batch_idx]; + int *v_bounds = v_bounds_batch + v_bounds_offset[batch_idx]; + work_type *v_kk = v_kk_batch + v_kk_offset[batch_idx]; + + int out_height = dst.at_rows(batch_idx), out_width = dst.at_cols(batch_idx); + + extern __shared__ __align__(sizeof(work_type)) unsigned char kk_smem_v[]; + work_type *v_k_tmp = PillowStageCoeffs(v_kk, reinterpret_cast(kk_smem_v), y_offset, v_ksize, + out_height, blockDim.y, use_share_mem); + + if (dst_x < out_width && dst_y < out_height) + { + int ymin = v_bounds[dst_y * 2]; + int ymax = v_bounds[dst_y * 2 + 1]; + + work_type *v_k = &v_k_tmp[local_y * v_ksize]; + + // The intermediate has output width and precompute clamps ymin plus the tap count to its + // height, so neither coordinate can wrap. Keep the hot loop to direct 2-D addressing. + + // Interleaved channel-vectorization: load the NC-channel float intermediate pixel as one vector + // (intermediate is 16-byte aligned by the host, always vector-safe) and write the NC-channel output + // pixel as one vector. The per-image OUTPUT alignment is checked once (uniform across this block's + // image); the scalar path runs when it is not vector-aligned. + bool did_vec = false; + if constexpr (NC > 0) { - k[x] = .0f; + using vin_t = cuda::MakeType; + using vout_t = cuda::MakeType; + using accum_t = cuda::MakeType; + const std::uintptr_t obase = reinterpret_cast(dst.ptr(batch_idx, 0, 0)); + const std::uintptr_t ostep + = reinterpret_cast(dst.ptr(batch_idx, 1, 0)) - obase; // row stride in bytes + if (((obase | ostep) & (alignof(vout_t) - 1)) == 0) + { + const std::byte *p = reinterpret_cast(src.ptr(batch_idx, ymin, dst_x, 0)); + accum_t ss = cuda::SetAll(init_buffer); + for (int y = 0; y < ymax; ++y) + { + ss = ss + v_k[y] * cuda::StaticCast(*reinterpret_cast(p)); + p += src.rowStride; + } + vout_t *out = reinterpret_cast(dst.ptr(batch_idx, dst_y, dst_x, 0)); + if (round_up) + { + work_type *acc = reinterpret_cast(&ss); +#pragma unroll + for (int c = 0; c < NC; ++c) + reinterpret_cast(out)[c] = cuda::SaturateCast(std::round(acc[c])); + } + else + { + *out = cuda::SaturateCast(ss); + } + did_vec = true; + } } - if (normalize_coeff) + if (!did_vec) { - for (int i = 0; i < k_size; i++) + for (int c = 0; c < src.ch; ++c) { - work_type val = k[i]; - if (val < 0) + const std::byte *p = reinterpret_cast(src.ptr(batch_idx, ymin, dst_x, c)); + work_type ss = init_buffer; + for (int y = 0; y < ymax; ++y) { - k[i] = static_cast(-half_pixel + val * (1U << precision_bits_var_shape)); + ss = ss + *reinterpret_cast(p) * v_k[y]; + p += src.rowStride; } + + if (round_up) + *dst.ptr(batch_idx, dst_y, dst_x, c) = cuda::SaturateCast(std::round(ss)); else + *dst.ptr(batch_idx, dst_y, dst_x, c) = cuda::SaturateCast(ss); + } + } + } +} + +// Interleaved var-shape vertical pass vectorized along the flattened row (see vertical_pass_vec in +// pillow_resize.cu): all cols*nch scalars of an output row share v_k, so each thread accumulates VEC +// consecutive scalars with one wide load per tap. The float intermediate rows are 16-byte aligned by +// the host; the per-image OUTPUT alignment is checked once per thread (uniform across this block's +// image) and the row tail or an unaligned image falls back to a per-element loop with identical +// per-element arithmetic. Weights are read straight from L1: threads of a warp share dst_y, so the +// per-tap loads are broadcasts and shared-memory staging would only add a block barrier. +template +__global__ void vertical_pass_var_shape_vec(const Ptr2dNHWC src, Ptr2dVarShapeNHWC dst, Filter &filterp, + int *v_ksize_batch, int *v_bounds_batch, int *v_bounds_offset, + work_type *v_kk_batch, int *v_kk_offset, work_type init_buffer, + bool round_up) +{ + const int elem0 = (blockIdx.x * blockDim.x + threadIdx.x) * VEC; + const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + + const int batch_idx = get_batch_idx(); + int v_ksize = v_ksize_batch[batch_idx]; + int *v_bounds = v_bounds_batch + v_bounds_offset[batch_idx]; + work_type *v_kk = v_kk_batch + v_kk_offset[batch_idx]; + + int out_height = dst.at_rows(batch_idx); + int row_elems = dst.at_cols(batch_idx) * dst.nch; + + if (elem0 < row_elems && dst_y < out_height) + { + const int ymin = v_bounds[dst_y * 2]; + const int ymax = v_bounds[dst_y * 2 + 1]; + + const work_type *v_k = &v_kk[dst_y * v_ksize]; + + const std::byte *src_row + = reinterpret_cast(src.ptr(batch_idx, ymin, 0, 0)) + elem0 * sizeof(T1); + T2 *dst_row = dst.ptr(batch_idx, dst_y, 0, 0) + elem0; + + using vin_t = cuda::MakeType; + using vout_t = cuda::MakeType; + const std::uintptr_t obase = reinterpret_cast(dst.ptr(batch_idx, 0, 0)); + const std::uintptr_t ostep = reinterpret_cast(dst.ptr(batch_idx, 1, 0)) - obase; + const bool vec_ok = elem0 + VEC <= row_elems + && (((obase | ostep) & (alignof(vout_t) - 1)) + | (static_cast(src.rowStride) & (alignof(vin_t) - 1))) + == 0; + + if (vec_ok) + { + using accum_t = cuda::MakeType; + + accum_t ss = cuda::SetAll(init_buffer); + for (int y = 0; y < ymax; ++y) + { + ss = ss + v_k[y] * cuda::StaticCast(*reinterpret_cast(src_row)); + src_row += src.rowStride; + } + if (round_up) + { + work_type *acc = reinterpret_cast(&ss); +#pragma unroll + for (int i = 0; i < VEC; ++i) dst_row[i] = cuda::SaturateCast(std::round(acc[i])); + } + else + { + *reinterpret_cast(dst_row) = cuda::SaturateCast(ss); + } + } + else + { + for (int i = 0; i < VEC && elem0 + i < row_elems; ++i) + { + const std::byte *p = src_row + i * sizeof(T1); + work_type ss = init_buffer; + for (int y = 0; y < ymax; ++y) { - k[i] = static_cast(half_pixel + val * (1U << precision_bits_var_shape)); + ss = ss + *reinterpret_cast(p) * v_k[y]; + p += src.rowStride; } + dst_row[i] = round_up ? cuda::SaturateCast(std::round(ss)) : cuda::SaturateCast(ss); + } + } + } +} + +// Fused single-pass var-shape resize for the interleaved FLOAT path. Mathematically identical, +// op-for-op, to horizontal_pass_var_shape + vertical_pass_var_shape: the var-shape intermediate is +// already work_type (float), so for each contributing source row the inner horizontal sum reproduces +// exactly what the float intermediate would hold, and accumulating v_k[y] * inner yields the same +// float result -- but the intermediate stays in registers, dropping its full-frame DRAM round-trip. +// Used only for small filter kernels (host-side max-k gate): the inner sum is recomputed v_ksize times, +// so large supports would go compute-bound and regress, and keep the separable passes. The per-image +// vector path is gated on input AND output alignment (vec and scalar give identical float results). +template +__global__ void fused_pass_var_shape(const Ptr2dVarShapeNHWC src, Ptr2dVarShapeNHWC dst, Filter &filterp, + int *h_ksize_batch, int *v_ksize_batch, int *h_bounds_batch, int *h_bounds_offset, + work_type *h_kk_batch, int *h_kk_offset, int *v_bounds_batch, int *v_bounds_offset, + work_type *v_kk_batch, int *v_kk_offset, work_type init_buffer, bool round_up, + bool use_share_mem) +{ + static_assert(NC > 0, "fused_pass_var_shape is the interleaved channel-vectorized path"); + const int dst_x = blockIdx.x * blockDim.x + threadIdx.x; + const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + + const int h_ksize = h_ksize_batch[batch_idx]; + const int v_ksize = v_ksize_batch[batch_idx]; + int *h_bounds = h_bounds_batch + h_bounds_offset[batch_idx]; + int *v_bounds = v_bounds_batch + v_bounds_offset[batch_idx]; + work_type *h_kk = h_kk_batch + h_kk_offset[batch_idx]; + work_type *v_kk = v_kk_batch + v_kk_offset[batch_idx]; + + const int out_height = dst.at_rows(batch_idx), out_width = dst.at_cols(batch_idx); + + if (dst_x >= out_width || dst_y >= out_height) + return; + + const int xmin = h_bounds[dst_x * 2]; + const int xmax = h_bounds[dst_x * 2 + 1]; + const int ymin = v_bounds[dst_y * 2]; + const int ymax = v_bounds[dst_y * 2 + 1]; + work_type *h_k = &h_kk[dst_x * h_ksize]; + work_type *v_k = &v_kk[dst_y * v_ksize]; + + // Precomputed bounds clamp both support windows to the source image, so direct coordinates are + // equivalent to the old quotient/remainder reconstruction and avoid divisions in the nested taps. + + using vin_t = cuda::MakeType; + using accum_t = cuda::MakeType; + + // Per-image alignment for the vectorized load (input) and store (output); uniform across the image. + const std::uintptr_t ibase = reinterpret_cast(src.ptr(batch_idx, 0, 0)); + const std::uintptr_t istep = reinterpret_cast(src.ptr(batch_idx, 1, 0)) - ibase; + const std::uintptr_t obase = reinterpret_cast(dst.ptr(batch_idx, 0, 0)); + const std::uintptr_t ostep = reinterpret_cast(dst.ptr(batch_idx, 1, 0)) - obase; + const bool aligned = (((ibase | istep | obase | ostep) & (alignof(vin_t) - 1)) == 0); + + if (aligned) + { + accum_t ss = cuda::SetAll(init_buffer); + for (int y = 0; y < ymax; ++y) + { + // inner == the float intermediate at (ymin + y, dst_x): identical taps, order, and bounds. + const std::byte *p = reinterpret_cast(src.ptr(batch_idx, ymin + y, xmin, 0)); + accum_t inner = cuda::SetAll(work_type{0}); + for (int x = 0; x < xmax; ++x) + { + inner = inner + h_k[x] * cuda::StaticCast(*reinterpret_cast(p)); + p += sizeof(vin_t); } + ss = ss + v_k[y] * inner; + } + vin_t *out = reinterpret_cast(dst.ptr(batch_idx, dst_y, dst_x, 0)); + if (round_up) + { + work_type *acc = reinterpret_cast(&ss); +#pragma unroll + for (int c = 0; c < NC; ++c) reinterpret_cast(out)[c] = cuda::SaturateCast(std::round(acc[c])); + } + else + { + *out = cuda::SaturateCast(ss); } - bounds_out[xx * 2] = xmin; - bounds_out[xx * 2 + 1] = xmax; } - if (use_share_mem) + else { - __syncthreads(); - for (int i = local_id; i < (out_size - x_offset) * k_size && i < blockDim.x * k_size; i += blockDim.x) + for (int c = 0; c < src.nch; ++c) { - kk_out[x_offset * k_size + i] = kk[i]; + work_type ss = init_buffer; + for (int y = 0; y < ymax; ++y) + { + const std::byte *p = reinterpret_cast(src.ptr(batch_idx, ymin + y, xmin, c)); + work_type inner = 0; + for (int x = 0; x < xmax; ++x) + { + inner = inner + *reinterpret_cast(p) * h_k[x]; + p += src.nch * sizeof(T); + } + ss = ss + v_k[y] * inner; + } + if (round_up) + *dst.ptr(batch_idx, dst_y, dst_x, c) = cuda::SaturateCast(std::round(ss)); + else + *dst.ptr(batch_idx, dst_y, dst_x, c) = cuda::SaturateCast(ss); } } } +// Planar (NCHW / CHW) var-shape passes. PillowResize resizes each channel independently. The external +// src/dst are per-plane wraps indexed (image, plane, y, x); the internal scratch buffer stays +// interleaved (indexed by plane in its channel slot). One grid-z slice is launched per image +// (blockIdx.z = image); the C channel planes are looped inside the thread so the per-output-pixel +// bounds / weight setup -- shared by all planes of an image -- is computed once and reused. template -__global__ void horizontal_pass_var_shape(const Ptr2dVarShapeNHWC src, Ptr2dNHWC dst, Filter &filterp, - int *h_ksize_batch, int *v_ksize_batch, int *h_bounds_batch, - int *h_bounds_offset, work_type *h_kk_batch, int *h_kk_offset, - int *v_bounds_batch, int *v_bounds_offset, work_type *v_kk_batch, - int *v_kk_offset, work_type init_buffer, bool round_up, bool use_share_mem) +__global__ void horizontal_pass_var_shape_planar(const cuda::ImageBatchVarShapeWrap src, Ptr2dNHWC dst, + Filter &filterp, int *h_ksize_batch, int channels, int *h_bounds_batch, + int *h_bounds_offset, work_type *h_kk_batch, int *h_kk_offset, + work_type init_buffer, bool round_up, bool use_share_mem) { const int dst_x = blockIdx.x * blockDim.x + threadIdx.x; const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; @@ -157,24 +607,68 @@ __global__ void horizontal_pass_var_shape(const Ptr2dVarShapeNHWC src, Ptr2d int *h_bounds = h_bounds_batch + h_bounds_offset[batch_idx]; work_type *h_kk = h_kk_batch + h_kk_offset[batch_idx]; + // All channel planes of an image share its size, so the per-image setup (bounds, coeffs, in-size) + // is computed once and reused across the channels looped below. int out_height = dst.at_rows(batch_idx), out_width = dst.at_cols(batch_idx); - int in_height = src.at_rows(batch_idx), in_width = src.at_cols(batch_idx); + int in_height = src.height(batch_idx, 0); - work_type *h_k_tmp = h_kk + x_offset * h_ksize; + extern __shared__ __align__(sizeof(work_type)) unsigned char kk_smem_h[]; + work_type *h_k_tmp = PillowStageCoeffs(h_kk, reinterpret_cast(kk_smem_h), x_offset, h_ksize, out_width, + blockDim.x, use_share_mem); - if (use_share_mem) + if (dst_x < out_width && dst_y < out_height) { - const int local_tid = threadIdx.x + blockDim.x * threadIdx.y; - extern __shared__ __align__(sizeof(work_type)) unsigned char kk_smem_h[]; - h_k_tmp = reinterpret_cast(kk_smem_h); + int xmin = h_bounds[dst_x * 2]; + int xmax = h_bounds[dst_x * 2 + 1]; - for (int i = local_tid; i < blockDim.x * h_ksize && i < (out_width - x_offset) * h_ksize; - i += blockDim.x * blockDim.y) + work_type *h_k = &h_k_tmp[local_x * h_ksize]; + // The precompute step clamps xmin and the tap count to the source width, so the support stays + // inside row dst_y of every plane: walk a hoisted row pointer instead of re-deriving the + // coordinate (and its quotient/remainder) for every tap. The max-sized launch still needs the + // per-image height guard: shorter images store zero rows, as the interleaved kernel does. + const bool validRow = dst_y < in_height; + for (int plane = 0; plane < channels; ++plane) { - h_k_tmp[i] = h_kk[x_offset * h_ksize + i]; + work_type h_ss = 0.0; + if (validRow) + { + const T1 *row = src.ptr(batch_idx, plane, dst_y, xmin); + for (int x = 0; x < xmax; ++x) + { + h_ss = h_ss + row[x] * h_k[x]; + } + } + if (round_up) + *dst.ptr(batch_idx, dst_y, dst_x, plane) = cuda::SaturateCast(std::round(h_ss)); + else + *dst.ptr(batch_idx, dst_y, dst_x, plane) = cuda::SaturateCast(h_ss); } - __syncthreads(); } +} + +template +__global__ void horizontal_pass_var_shape_planar_channels(const cuda::ImageBatchVarShapeWrap src, + Ptr2dNHWC dst, Filter &filterp, int *h_ksize_batch, + int *h_bounds_batch, int *h_bounds_offset, + work_type *h_kk_batch, int *h_kk_offset, + work_type init_buffer, bool round_up, bool use_share_mem) +{ + const int dst_x = blockIdx.x * blockDim.x + threadIdx.x; + const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int local_x = threadIdx.x; + const int x_offset = blockIdx.x * blockDim.x; + + const int batch_idx = get_batch_idx(); + int h_ksize = h_ksize_batch[batch_idx]; + int *h_bounds = h_bounds_batch + h_bounds_offset[batch_idx]; + work_type *h_kk = h_kk_batch + h_kk_offset[batch_idx]; + + int out_height = dst.at_rows(batch_idx), out_width = dst.at_cols(batch_idx); + int in_height = src.height(batch_idx, 0); + + extern __shared__ __align__(sizeof(work_type)) unsigned char kk_smem_h[]; + work_type *h_k_tmp = PillowStageCoeffs(h_kk, reinterpret_cast(kk_smem_h), x_offset, h_ksize, out_width, + blockDim.x, use_share_mem); if (dst_x < out_width && dst_y < out_height) { @@ -182,36 +676,55 @@ __global__ void horizontal_pass_var_shape(const Ptr2dVarShapeNHWC src, Ptr2d int xmax = h_bounds[dst_x * 2 + 1]; work_type *h_k = &h_k_tmp[local_x * h_ksize]; - // int offset_src = dst_y * src.at_cols(batch_idx) * src.nch + xmin * src.nch; - // int offset_dst = dst_y * dst.at_cols(batch_idx) * dst.nch + dst_x * dst.nch; - for (int c = 0; c < src.nch; ++c) + work_type h_ss[PC]; + // The precompute step clamps xmin and the tap count to the source width, so the support stays + // inside row dst_y of every plane: walk hoisted per-plane row pointers instead of re-deriving + // the coordinate (and its quotient/remainder) for every tap. + const T1 *row[PC]; + +#pragma unroll + for (int plane = 0; plane < PC; ++plane) { - work_type h_ss = 0.0; + h_ss[plane] = 0.0; + } + + // The max-sized launch still needs the per-image height guard: shorter images store zero + // rows, as the interleaved kernel does. + if (dst_y < in_height) + { +#pragma unroll + for (int plane = 0; plane < PC; ++plane) + { + row[plane] = src.ptr(batch_idx, plane, dst_y, xmin); + } + for (int x = 0; x < xmax; ++x) { - // offset = offset_src + x * src.nch + c = (dst_y * src.at_cols(batch_idx) + xmin + x) * src.nch + c - int src_y = dst_y + (xmin + x) / src.at_cols(batch_idx); - int src_x = (xmin + x) % src.at_cols(batch_idx); - if (src_y < 0 || src_y >= in_height || src_x < 0 || src_x >= in_width) + const work_type k = h_k[x]; +#pragma unroll + for (int plane = 0; plane < PC; ++plane) { - continue; + h_ss[plane] = h_ss[plane] + row[plane][x] * k; } - h_ss = h_ss + *src.ptr(batch_idx, src_y, src_x, c) * h_k[x]; } + } + +#pragma unroll + for (int plane = 0; plane < PC; ++plane) + { if (round_up) - *dst.ptr(batch_idx, dst_y, dst_x, c) = cuda::SaturateCast(std::round(h_ss)); + *dst.ptr(batch_idx, dst_y, dst_x, plane) = cuda::SaturateCast(std::round(h_ss[plane])); else - *dst.ptr(batch_idx, dst_y, dst_x, c) = cuda::SaturateCast(h_ss); + *dst.ptr(batch_idx, dst_y, dst_x, plane) = cuda::SaturateCast(h_ss[plane]); } } } template -__global__ void vertical_pass_var_shape(const Ptr2dNHWC src, Ptr2dVarShapeNHWC dst, Filter &filterp, - int *h_ksize_batch, int *v_ksize_batch, int *h_bounds_batch, - int *h_bounds_offset, work_type *h_kk_batch, int *h_kk_offset, - int *v_bounds_batch, int *v_bounds_offset, work_type *v_kk_batch, - int *v_kk_offset, work_type init_buffer, bool round_up, bool use_share_mem) +__global__ void vertical_pass_var_shape_planar(const Ptr2dNHWC src, cuda::ImageBatchVarShapeWrap dst, + Filter &filterp, int *v_ksize_batch, int channels, int *v_bounds_batch, + int *v_bounds_offset, work_type *v_kk_batch, int *v_kk_offset, + work_type init_buffer, bool round_up, bool use_share_mem) { const int dst_x = blockIdx.x * blockDim.x + threadIdx.x; const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; @@ -223,23 +736,12 @@ __global__ void vertical_pass_var_shape(const Ptr2dNHWC src, Ptr2dVarShapeNH int *v_bounds = v_bounds_batch + v_bounds_offset[batch_idx]; work_type *v_kk = v_kk_batch + v_kk_offset[batch_idx]; - int out_height = dst.at_rows(batch_idx), out_width = dst.at_cols(batch_idx); - int in_height = src.at_rows(batch_idx), in_width = src.at_cols(batch_idx); + // All channel planes of an image share its size; setup is computed once and reused across planes. + int out_height = dst.height(batch_idx, 0), out_width = dst.width(batch_idx, 0); - work_type *v_k_tmp = v_kk + y_offset * v_ksize; - if (use_share_mem) - { - const int local_tid = threadIdx.x + blockDim.x * threadIdx.y; - extern __shared__ __align__(sizeof(work_type)) unsigned char kk_smem_v[]; - v_k_tmp = reinterpret_cast(kk_smem_v); - - for (int i = local_tid; i < blockDim.y * v_ksize && i < (out_height - y_offset) * v_ksize; - i += blockDim.x * blockDim.y) - { - v_k_tmp[i] = v_kk[y_offset * v_ksize + i]; - } - __syncthreads(); - } + extern __shared__ __align__(sizeof(work_type)) unsigned char kk_smem_v[]; + work_type *v_k_tmp = PillowStageCoeffs(v_kk, reinterpret_cast(kk_smem_v), y_offset, v_ksize, + out_height, blockDim.y, use_share_mem); if (dst_x < out_width && dst_y < out_height) { @@ -247,33 +749,28 @@ __global__ void vertical_pass_var_shape(const Ptr2dNHWC src, Ptr2dVarShapeNH int ymax = v_bounds[dst_y * 2 + 1]; work_type *v_k = &v_k_tmp[local_y * v_ksize]; - // int offset_src = ymin * src.at_cols(batch_idx) * src.nch + dst_x * src.nch; - // int col_offset_src = src.at_cols(batch_idx) * src.nch; - // int offset_dst = dst_y * dst.at_cols(batch_idx) * dst.nch + dst_x * dst.nch; - for (int c = 0; c < src.ch; ++c) + // The precompute step clamps ymin and the tap count to the intermediate height and dst_x is + // bounded by its width, so the support stays inside column dst_x: walk a hoisted column + // pointer by row stride instead of re-deriving the coordinate for every tap. + for (int plane = 0; plane < channels; ++plane) { - work_type ss = init_buffer; + const std::byte *p = reinterpret_cast(src.ptr(batch_idx, ymin, dst_x, plane)); + work_type ss = init_buffer; for (int y = 0; y < ymax; ++y) { - // offset = offset_src + y * col_offset_src + c = ((y + ymin)* src.at_cols(batch_idx) + dst_x) * src.nch + c - int src_y = y + ymin + (dst_x / src.at_cols(batch_idx)); - int src_x = dst_x % src.at_cols(batch_idx); - if (src_y < 0 || src_y >= in_height || src_x < 0 || src_x >= in_width) - { - continue; - } - ss = ss + *src.ptr(batch_idx, src_y, src_x, c) * v_k[y]; + ss = ss + *reinterpret_cast(p) * v_k[y]; + p += src.rowStride; } if (round_up) - *dst.ptr(batch_idx, dst_y, dst_x, c) = cuda::SaturateCast(std::round(ss)); + *dst.ptr(batch_idx, plane, dst_y, dst_x) = cuda::SaturateCast(std::round(ss)); else - *dst.ptr(batch_idx, dst_y, dst_x, c) = cuda::SaturateCast(ss); + *dst.ptr(batch_idx, plane, dst_y, dst_x) = cuda::SaturateCast(ss); } } } -template +template void pillow_resize_var_shape(const ImageBatchVarShape &inDataBase, const ImageBatchVarShape &outDataBase, const Workspace &ws, bool normalize_coeff, work_type init_buffer, bool round_up, cudaStream_t stream) @@ -290,13 +787,13 @@ void pillow_resize_var_shape(const ImageBatchVarShape &inDataBase, const ImageBa auto inDataPtr = inDataBase.exportData(stream); if (!inDataPtr) { - throw std::runtime_error("Something wrong happened during conversion of type...!!!"); + throw LegacyImageBatchExportError("Something wrong happened during conversion of type...!!!"); } auto outDataPtr = outDataBase.exportData(stream); if (!outDataPtr) { - throw std::runtime_error("Something wrong happened during conversion of type...!!!"); + throw LegacyImageBatchExportError("Something wrong happened during conversion of type...!!!"); } const ImageBatchVarShapeDataStridedCuda &inData = *inDataPtr; @@ -335,9 +832,11 @@ void pillow_resize_var_shape(const ImageBatchVarShape &inDataBase, const ImageBa int *h_kk_offset = (int *)((char *)v_bounds_offset + sizeof(int) * batch); int *v_kk_offset = (int *)((char *)h_kk_offset + sizeof(int) * batch); - int h_kk_total = 0, v_kk_total = 0; - int max_h_k_size = 0, max_v_k_size = 0; - int h_bounds_total = 0, v_bounds_total = 0; + int h_kk_total = 0, v_kk_total = 0; + int max_h_k_size = 0, max_v_k_size = 0; + int h_bounds_total = 0, v_bounds_total = 0; + bool all_upscale = true; + bool all_hcontract = true; for (int i = 0; i < batch; i++) { @@ -353,6 +852,8 @@ void pillow_resize_var_shape(const ImageBatchVarShape &inDataBase, const ImageBa work_type h_filterscale = 0, v_filterscale = 0; h_filterscale = h_scale = static_cast(inDataBase[i].size().w) / out_cols[i]; v_filterscale = v_scale = static_cast(inDataBase[i].size().h) / out_rows[i]; + all_upscale = all_upscale && h_scale < work_type(1) && v_scale < work_type(1); + all_hcontract = all_hcontract && h_scale > work_type(1); if (h_filterscale < 1.0) { h_filterscale = 1.0; @@ -418,26 +919,34 @@ void pillow_resize_var_shape(const ImageBatchVarShape &inDataBase, const ImageBa int *h_bounds_batch_gpu = (int *)((char *)v_kk_batch_gpu + sizeof(work_type) * v_kk_total); int *v_bounds_batch_gpu = (int *)((char *)h_bounds_batch_gpu + sizeof(int) * h_bounds_total); - int current_buffer_size = (sizeof(void *) * 3 + sizeof(int) * 12 + sizeof(work_type) * 6) * batch - + sizeof(work_type) * (h_kk_total + v_kk_total) - + sizeof(int) * (h_bounds_total + v_bounds_total); - - // buffer for storing results from horizontal pass - void *hori_gpu_data = (void *)((char *)gpu_workspace + current_buffer_size); - - checkCudaErrors(cudaMemcpyAsync((void *)gpu_workspace, (void *)cpu_workspace, current_buffer_size, + const size_t metadata_buffer_size = (sizeof(void *) * 3 + sizeof(int) * 12 + sizeof(work_type) * 6) * batch; + const size_t current_buffer_size = metadata_buffer_size + sizeof(work_type) * (h_kk_total + v_kk_total) + + sizeof(int) * (h_bounds_total + v_bounds_total); + + // Buffer for storing results from the horizontal pass. The intermediate is elem_type: integer + // dtypes quantize between passes exactly like the tensor path and Pillow's 8bpc pipeline (and move + // a quarter of the float bytes); float keeps the full-precision work_type intermediate. Round up to + // 16 bytes so the intermediate can be read/written with up-to-16-byte vector accesses + // (getWorkspaceRequirements sizes the region for the float worst case and reserves +16). + std::uintptr_t hori_raw = reinterpret_cast((char *)gpu_workspace + current_buffer_size); + void *hori_gpu_data = reinterpret_cast((hori_raw + 15) & ~std::uintptr_t(15)); + + // The large coefficient/bounds regions are produced by _precomputeCoeffsVarShape below; only the + // small per-sample metadata prefix is initialized on the host and needs to be uploaded. + checkCudaErrors(cudaMemcpyAsync((void *)gpu_workspace, (void *)cpu_workspace, metadata_buffer_size, cudaMemcpyHostToDevice, stream)); if (ws.hostMem.ready != nullptr) checkCudaErrors(cudaEventRecord(ws.hostMem.ready, stream)); - Ptr2dVarShapeNHWC src_ptr(inData); - Ptr2dVarShapeNHWC dst_ptr(outData); - Ptr2dNHWC ptr_h_out(batch, max_input_height, max_width, channels, (work_type *)hori_gpu_data); + Ptr2dNHWC ptr_h_out(batch, max_input_height, max_width, channels, (elem_type *)hori_gpu_data); - dim3 blockSize(BLOCK, BLOCK / 4, 1); - dim3 gridSizeH(divUp(max_width, blockSize.x), divUp(max_input_height, blockSize.y), batch); - dim3 gridSizeV(divUp(max_width, blockSize.x), divUp(max_height, blockSize.y), batch); + dim3 blockSize(BLOCK, BLOCK / 4, 1); + // Both planar and interleaved passes launch one grid-z slice per image and loop the channel planes + // internally, so the per-image / per-output-pixel filter setup is shared across channels. + const int pass_z = batch; + dim3 gridSizeH(divUp(max_width, blockSize.x), divUp(max_input_height, blockSize.y), pass_z); + dim3 gridSizeV(divUp(max_width, blockSize.x), divUp(max_height, blockSize.y), pass_z); dim3 coef_block(BLOCK * 2, 1, 1); dim3 h_coef_grid(divUp(max_width, coef_block.x), 1, batch); @@ -479,16 +988,159 @@ void pillow_resize_var_shape(const ImageBatchVarShape &inDataBase, const ImageBa normalize_coeff, v_use_share_mem); checkKernelErrors(); // checkCudaErrors(cudaStreamSynchronize(stream)); - horizontal_pass_var_shape<<>>( - src_ptr, ptr_h_out, filterp, h_k_size_batch_gpu, v_k_size_batch_gpu, h_bounds_batch_gpu, h_bounds_offset_gpu, - h_kk_batch_gpu, h_kk_offset_gpu, v_bounds_batch_gpu, v_bounds_offset_gpu, v_kk_batch_gpu, v_kk_offset_gpu, - init_buffer, round_up, hv_use_share_mem); - checkKernelErrors(); - // checkCudaErrors(cudaStreamSynchronize(stream)); - vertical_pass_var_shape<<>>( - ptr_h_out, dst_ptr, filterp, h_k_size_batch_gpu, v_k_size_batch_gpu, h_bounds_batch_gpu, h_bounds_offset_gpu, - h_kk_batch_gpu, h_kk_offset_gpu, v_bounds_batch_gpu, v_bounds_offset_gpu, v_kk_batch_gpu, v_kk_offset_gpu, - init_buffer, round_up, hv_use_share_mem); + if constexpr (kPlanar) + { + cuda::ImageBatchVarShapeWrap src_ptr(inData); + cuda::ImageBatchVarShapeWrap dst_ptr(outData); + switch (channels) + { + case 3: + horizontal_pass_var_shape_planar_channels<3, elem_type, elem_type, Filter> + <<>>( + src_ptr, ptr_h_out, filterp, h_k_size_batch_gpu, h_bounds_batch_gpu, h_bounds_offset_gpu, + h_kk_batch_gpu, h_kk_offset_gpu, init_buffer, round_up, hv_use_share_mem); + break; + case 4: + if constexpr (!std::is_same_v) + { + horizontal_pass_var_shape_planar_channels<4, elem_type, elem_type, Filter> + <<>>( + src_ptr, ptr_h_out, filterp, h_k_size_batch_gpu, h_bounds_batch_gpu, h_bounds_offset_gpu, + h_kk_batch_gpu, h_kk_offset_gpu, init_buffer, round_up, hv_use_share_mem); + } + else + { + horizontal_pass_var_shape_planar + <<>>( + src_ptr, ptr_h_out, filterp, h_k_size_batch_gpu, channels, h_bounds_batch_gpu, + h_bounds_offset_gpu, h_kk_batch_gpu, h_kk_offset_gpu, init_buffer, round_up, hv_use_share_mem); + } + break; + default: + horizontal_pass_var_shape_planar + <<>>( + src_ptr, ptr_h_out, filterp, h_k_size_batch_gpu, channels, h_bounds_batch_gpu, h_bounds_offset_gpu, + h_kk_batch_gpu, h_kk_offset_gpu, init_buffer, round_up, hv_use_share_mem); + break; + } + checkKernelErrors(); + // Same L1-broadcast property as the interleaved vertical: skip coefficient staging. + vertical_pass_var_shape_planar<<>>( + ptr_h_out, dst_ptr, filterp, v_k_size_batch_gpu, channels, v_bounds_batch_gpu, v_bounds_offset_gpu, + v_kk_batch_gpu, v_kk_offset_gpu, init_buffer, round_up, false); + } + else + { + Ptr2dVarShapeNHWC src_ptr(inData); + Ptr2dVarShapeNHWC dst_ptr(outData); + // NC = compile-time channel count enables the interleaved channel-vectorized fast path (the + // kernels gate it per-image on alignment and fall back to the scalar loop otherwise). NC=0 keeps + // the scalar path for unexpected channel counts. + auto launch_v = [&](auto nc_const) + { + constexpr int NC = decltype(nc_const)::value; + // A single channel does too little work per staged coefficient to amortize the shared-memory + // copy and synchronization. Multi-channel paths retain staging because all channels reuse it. + const bool pass_use_share_mem = NC > 1 && hv_use_share_mem; + const size_t pass_h_sm_size = pass_use_share_mem ? hv_sm_size1 : 0; + const size_t pass_v_sm_size = pass_use_share_mem ? hv_sm_size2 : 0; + // The fused kernel removes the intermediate write/read, but recomputes each horizontal sum + // for every vertical tap. Upscales win across the measured GPUs; small-kernel downscales win + // only on the architecture families selected by PillowResizeSupportsFusedDownscale. + // Bit-exact: for float the T-typed intermediate is work_type, so the in-register inner sum + // reproduces it op-for-op. + constexpr bool can_fuse = (NC > 0 && std::is_same_v); + const bool small_kernel + = max_h_k_size <= kPillowResizeFuseMaxKSize && max_v_k_size <= kPillowResizeFuseMaxKSize; + const bool fuse_pass = can_fuse && filterp.support() <= work_type(1) + && (all_upscale || (small_kernel && PillowResizeSupportsFusedDownscale())); + if (fuse_pass) + { + if constexpr (can_fuse) // guard instantiation: fused_pass_var_shape static_asserts NC > 0 + { + fused_pass_var_shape<<>>( + src_ptr, dst_ptr, filterp, h_k_size_batch_gpu, v_k_size_batch_gpu, h_bounds_batch_gpu, + h_bounds_offset_gpu, h_kk_batch_gpu, h_kk_offset_gpu, v_bounds_batch_gpu, v_bounds_offset_gpu, + v_kk_batch_gpu, v_kk_offset_gpu, init_buffer, round_up, pass_use_share_mem); + checkKernelErrors(); + } + } + else + { + // See horizontal_pass_paired: byte-dtype horizontal downscales load the shared + // window segment once for two adjacent outputs. + // Three-channel byte pixels only: single-channel var-shape rows regressed 22-40% + // on the A100/H100 regen burnins when paired. + bool paired = false; + if constexpr (NC == 3 && sizeof(elem_type) == 1) + { + if (all_hcontract) + { + const size_t paired_sm = 2 * pass_h_sm_size; + const bool paired_use = pass_use_share_mem && paired_sm <= SHARE_MEM_LIMIT; + dim3 gridSizeHP(divUp(max_width, 2 * static_cast(blockSize.x)), + divUp(max_input_height, blockSize.y), pass_z); + horizontal_pass_var_shape_paired + <<>>( + src_ptr, ptr_h_out, filterp, h_k_size_batch_gpu, out_cols_gpu, h_bounds_batch_gpu, + h_bounds_offset_gpu, h_kk_batch_gpu, h_kk_offset_gpu, init_buffer, round_up, + paired_use); + paired = true; + } + } + if (!paired) + { + horizontal_pass_var_shape + <<>>( + src_ptr, ptr_h_out, filterp, h_k_size_batch_gpu, v_k_size_batch_gpu, h_bounds_batch_gpu, + h_bounds_offset_gpu, h_kk_batch_gpu, h_kk_offset_gpu, v_bounds_batch_gpu, + v_bounds_offset_gpu, v_kk_batch_gpu, v_kk_offset_gpu, init_buffer, round_up, + pass_use_share_mem); + } + checkKernelErrors(); + // Upscale batches are bound by the vertical pass's per-element load/store issue + // rate, where the row vectorization wins; downscale verticals sit at the DRAM + // ridge and keep the per-pixel kernel. + if (all_upscale) + { + constexpr int VEC = 4; + dim3 gridSizeVV(divUp(divUp(max_width * channels, VEC), static_cast(blockSize.x)), + divUp(max_height, blockSize.y), pass_z); + vertical_pass_var_shape_vec + <<>>(ptr_h_out, dst_ptr, filterp, v_k_size_batch_gpu, + v_bounds_batch_gpu, v_bounds_offset_gpu, v_kk_batch_gpu, + v_kk_offset_gpu, init_buffer, round_up); + } + else + { + vertical_pass_var_shape + <<>>( + ptr_h_out, dst_ptr, filterp, h_k_size_batch_gpu, v_k_size_batch_gpu, h_bounds_batch_gpu, + h_bounds_offset_gpu, h_kk_batch_gpu, h_kk_offset_gpu, v_bounds_batch_gpu, + v_bounds_offset_gpu, v_kk_batch_gpu, v_kk_offset_gpu, init_buffer, round_up, + pass_use_share_mem); + } + } + }; + switch (channels) + { + case 1: + launch_v(std::integral_constant{}); + break; + case 2: + launch_v(std::integral_constant{}); + break; + case 3: + launch_v(std::integral_constant{}); + break; + case 4: + launch_v(std::integral_constant{}); + break; + default: + launch_v(std::integral_constant{}); + break; + } + } checkKernelErrors(); @@ -498,30 +1150,32 @@ void pillow_resize_var_shape(const ImageBatchVarShape &inDataBase, const ImageBa } // namespace -template -void pillow_resize_filter_var_shape(const ImageBatchVarShape &inData, const ImageBatchVarShape &outData, - const Workspace &ws, NVCVInterpolationType interpolation, cudaStream_t stream) +// Single dtype switch shared by the interleaved and planar paths via the kPlanar template parameter, +// so the launch table is written once. +template +void pillow_resize_dispatch_dtype(const ImageBatchVarShape &inData, const ImageBatchVarShape &outData, + const Workspace &ws, cudaStream_t stream) { DataType data_type = helpers::GetLegacyDataType(inData.uniqueFormat()); switch (data_type) { case kCV_8U: - pillow_resize_var_shape(inData, outData, ws, false, 0., false, stream); + pillow_resize_var_shape(inData, outData, ws, false, 0., false, stream); break; case kCV_8S: - pillow_resize_var_shape(inData, outData, ws, false, 0., true, stream); + pillow_resize_var_shape(inData, outData, ws, false, 0., true, stream); break; case kCV_16U: - pillow_resize_var_shape(inData, outData, ws, false, 0., false, stream); + pillow_resize_var_shape(inData, outData, ws, false, 0., false, stream); break; case kCV_16S: - pillow_resize_var_shape(inData, outData, ws, false, 0., true, stream); + pillow_resize_var_shape(inData, outData, ws, false, 0., true, stream); break; case kCV_32S: - pillow_resize_var_shape(inData, outData, ws, false, 0., true, stream); + pillow_resize_var_shape(inData, outData, ws, false, 0., true, stream); break; case kCV_32F: - pillow_resize_var_shape(inData, outData, ws, false, 0., false, stream); + pillow_resize_var_shape(inData, outData, ws, false, 0., false, stream); break; case kCV_64F: default: @@ -529,6 +1183,17 @@ void pillow_resize_filter_var_shape(const ImageBatchVarShape &inData, const Imag } } +template +void pillow_resize_filter_var_shape(const ImageBatchVarShape &inData, const ImageBatchVarShape &outData, + const Workspace &ws, NVCVInterpolationType interpolation, bool isPlanar, + cudaStream_t stream) +{ + if (isPlanar) + pillow_resize_dispatch_dtype(inData, outData, ws, stream); + else + pillow_resize_dispatch_dtype(inData, outData, ws, stream); +} + WorkspaceRequirements PillowResizeVarShape::getWorkspaceRequirements(DataShape max_input_shape, DataShape max_output_shape, DataType max_data_type) { @@ -545,12 +1210,16 @@ WorkspaceRequirements PillowResizeVarShape::getWorkspaceRequirements(DataShape m * (((1.0 * max_input_shape.W / max_output_shape.W + 1) * max_support * 2 + 1) * sizeof(work_type) + 2 * sizeof(int))); - size_t buffer_size = (sizeof(void *) * 3 + sizeof(int) * 12 + sizeof(work_type) * 6 + size) * max_input_shape.N; + const size_t metadata_buffer_size + = (sizeof(void *) * 3 + sizeof(int) * 12 + sizeof(work_type) * 6) * max_input_shape.N; + size_t buffer_size = metadata_buffer_size + size * max_input_shape.N; - req.hostMem.size = buffer_size; + req.hostMem.size = metadata_buffer_size; req.hostMem.alignment = alignof(std::max_align_t); - buffer_size += max_input_shape.N * max_input_shape.C * max_input_shape.H * max_output_shape.W * sizeof(float); + buffer_size += static_cast(max_input_shape.N) * max_input_shape.C * max_input_shape.H * max_output_shape.W + * sizeof(float); + buffer_size += 16; // padding to 16-byte-align the float intermediate buffer (vector loads/stores) req.cudaMem.size = buffer_size; req.cudaMem.alignment = kDefaultDeviceAlignment; @@ -577,20 +1246,33 @@ ErrorCode PillowResizeVarShape::infer(const nvcv::ImageBatchVarShape &inDataBase DataFormat format = GetLegacyDataFormat(inDataBase); - if (!(format == kNHWC || format == kHWC)) + if (!(format == kNHWC || format == kHWC || format == kNCHW || format == kCHW)) { - LOG_ERROR("Invalid input DataFormat " << format << ", the valid DataFormats are: \"NHWC\", \"HWC\""); + LOG_ERROR("Invalid input DataFormat " << format + << ", the valid DataFormats are: \"NHWC\", \"HWC\", \"NCHW\", \"CHW\""); return ErrorCode::INVALID_DATA_FORMAT; } + const bool isPlanar = (format == kNCHW || format == kCHW); + int channels = inDataBase.uniqueFormat().numChannels(); - if (channels > 4) + // Planar 2-channel layout is rejected: there is no defined 2-plane planar format (matches the + // Resize and Normalize operators). + if (channels > 4 || (isPlanar && channels == 2)) { LOG_ERROR("Invalid channel number " << channels); return ErrorCode::INVALID_DATA_SHAPE; } + // The planar passes launch one grid-z slice per image (channels are looped inside the kernel, see + // pass_z = batch below), so numImages must fit CUDA's 65535 grid-z limit. + if (isPlanar && inDataBase.numImages() > 65535) + { + LOG_ERROR("Planar PillowResize requires numImages <= 65535 (CUDA grid-z limit)"); + return ErrorCode::INVALID_DATA_SHAPE; + } + DataType data_type = helpers::GetLegacyDataType(inDataBase.uniqueFormat()); if (!(data_type == kCV_8U || data_type == kCV_16U || data_type == kCV_16S || data_type == kCV_32F)) @@ -599,22 +1281,38 @@ ErrorCode PillowResizeVarShape::infer(const nvcv::ImageBatchVarShape &inDataBase return ErrorCode::INVALID_DATA_TYPE; } + // The horizontally-resized intermediate is one dense float image slot per batch entry + // (numImages x maxInH x maxOutW x C), and the kernels compute its per-image offsets as 32-bit + // products (Ptr2dNHWC). Reject batches whose intermediate exceeds INT32_MAX bytes instead of + // overflowing the addressing and corrupting memory. + { + constexpr int64_t kMaxByteExtent = std::numeric_limits::max(); + const int64_t interExtent = static_cast(inDataBase.numImages()) * inDataBase.maxSize().h + * outDataBase.maxSize().w * channels * DataSize(data_type); + if (interExtent > kMaxByteExtent) + { + LOG_ERROR("Intermediate byte extent " << interExtent << " exceeds the 32-bit addressing limit " + << kMaxByteExtent << "; split the batch into smaller submissions"); + return ErrorCode::INVALID_DATA_SHAPE; + } + } + switch (interpolation) { case NVCV_INTERP_LINEAR: - pillow_resize_filter_var_shape(inDataBase, outDataBase, ws, interpolation, stream); + pillow_resize_filter_var_shape(inDataBase, outDataBase, ws, interpolation, isPlanar, stream); break; case NVCV_INTERP_BOX: - pillow_resize_filter_var_shape(inDataBase, outDataBase, ws, interpolation, stream); + pillow_resize_filter_var_shape(inDataBase, outDataBase, ws, interpolation, isPlanar, stream); break; case NVCV_INTERP_HAMMING: - pillow_resize_filter_var_shape(inDataBase, outDataBase, ws, interpolation, stream); + pillow_resize_filter_var_shape(inDataBase, outDataBase, ws, interpolation, isPlanar, stream); break; case NVCV_INTERP_CUBIC: - pillow_resize_filter_var_shape(inDataBase, outDataBase, ws, interpolation, stream); + pillow_resize_filter_var_shape(inDataBase, outDataBase, ws, interpolation, isPlanar, stream); break; case NVCV_INTERP_LANCZOS: - pillow_resize_filter_var_shape(inDataBase, outDataBase, ws, interpolation, stream); + pillow_resize_filter_var_shape(inDataBase, outDataBase, ws, interpolation, isPlanar, stream); break; default: LOG_ERROR("Unsupported interpolation method " << interpolation); diff --git a/src/cvcuda/priv/legacy/random_resized_crop.cu b/src/cvcuda/priv/legacy/random_resized_crop.cu index 053fe16c9..440b43322 100644 --- a/src/cvcuda/priv/legacy/random_resized_crop.cu +++ b/src/cvcuda/priv/legacy/random_resized_crop.cu @@ -1,4 +1,4 @@ -/* Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +/* Copyright (c) 2021-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * * SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES * SPDX-License-Identifier: Apache-2.0 @@ -22,12 +22,15 @@ #include "CvCudaLegacyHelpers.hpp" #include "CvCudaUtils.cuh" +#include "random_resized_crop_common.cuh" #include #include #include +#include #include +#include using namespace nvcv; using namespace nvcv::legacy::cuda_op; @@ -37,58 +40,6 @@ namespace nvcv::legacy::cuda_op { #define BLOCK 32 -#define MAX_BUFFER_BYTES 128 //multiple of 4 for word-aligned read, multiple of 16 for cacheline alignment (float4) -#define MAX_BUFFER_WORDS (MAX_BUFFER_BYTES / 4) //extra bytes for cache alignment - -#define LEGACY_BICUBIC_MATH //apparently the legacy code has an abs() that needs to be matched - -// Replaced below 15 to 0 due to a reported regression -#define CACHE_MEMORY_ALIGNMENT 0 //this is 'M' for _cacheAlignedBufferedRead - -//legal values for CACHE_MEMORY_ALIGNMENT are: -// 31: 256-bit alignment -// 15: 128-bit alignment <-- should be ideal for Ampere -// 7: 64-bit alignment -// 3: 32-bit alignment (word) -// 0: disable buffering -template -inline const __device__ ValueType *_cacheAlignedBufferedRead(SrcWrapper srcImage, int width, uint *pReadBuffer, - uint nReadBufferWordsMax, int nBatch, int nYPos, - int nXPosMin, int nXPosMax) -{ - const ValueType *lineStartPtr = srcImage.ptr(nBatch, nYPos); //do not access prior to this address - const ValueType *pixSrcPtr = &lineStartPtr[nXPosMin]; - if (M == 0) - return pixSrcPtr; //return GMEM pointer instead - else - { - uint *memSrcPtr = (uint *)(((size_t)pixSrcPtr) & (~M)); //(M+1) byte alignment - const ValueType *pixBeyondPtr = &lineStartPtr[nXPosMax + 1]; - const int functionalWidth = ((size_t)pixBeyondPtr + M) & (~M) - ((size_t)lineStartPtr); - const int nWordsToRead = (((size_t)pixBeyondPtr + M) & (~M) - (size_t)memSrcPtr) / 4; - - if (((size_t)memSrcPtr < (size_t)lineStartPtr) || (width * sizeof(ValueType) < functionalWidth) - || (nWordsToRead > nReadBufferWordsMax)) - return pixSrcPtr; //return GMEM pointer instead if running off the image - else - { //copy out source data, aligned based upon M (31, 15, 7, 3) - const int skew = ((size_t)pixSrcPtr) & M; //byte offset for nXPosMin - int i = 0; - if (M >= 31) //256-bit align, 32 bytes at a time - for (; i < nWordsToRead; i += 8) - *((double4_16a *)(&pReadBuffer[i])) = *((double4_16a *)(&memSrcPtr[i])); - if (M == 15) //128-bit align, 16 bytes at a time - for (; i < nWordsToRead; i += 4) *((float4 *)(&pReadBuffer[i])) = *((float4 *)(&memSrcPtr[i])); - if (M == 7) //64-bit align, 8 bytes at a time - for (; i < nWordsToRead; i += 2) *((float2 *)(&pReadBuffer[i])) = *((float2 *)(&memSrcPtr[i])); - //32-bit align, 4 bytes at a time - for (; i < nWordsToRead; ++i) pReadBuffer[i] = memSrcPtr[i]; - - return (const ValueType *)(((size_t)pReadBuffer) + skew); //buffered pixel data - } - } -} //_cacheAlignedBufferedRead - template __global__ void resize_linear_v1(const SrcWrapper src, DstWrapper dst, int2 srcSize, int2 dstSize, const int *top_, const int *left_, const float *scale_x_, const float *scale_y_) @@ -133,6 +84,70 @@ __global__ void resize_linear_v1(const SrcWrapper src, DstWrapper dst, int2 srcS } } +template +__global__ void resize_linear_nix(const SrcWrapper src, DstWrapper dst, int2 srcSize, int2 dstSize, const int *top_, + const int *left_, const float *scale_x_, const float *scale_y_) +{ + const int dst_x0 = (blockIdx.x * blockDim.x + threadIdx.x) * NIX; + const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + int height = srcSize.y, width = srcSize.x, out_height = dstSize.y, out_width = dstSize.x; + + if (dst_y < out_height) + { + const float scale_x = scale_x_[batch_idx]; + const float scale_y = scale_y_[batch_idx]; + const int top = top_[batch_idx]; + const int left = left_[batch_idx]; + + float fy = (float)((dst_y + 0.5f) * scale_y - 0.5f + top); + int sy = cuda::round(fy); + + fy = ((sy < 0) ? 0 : ((sy > height - 2) ? 1 : fy - sy)); + sy = cuda::max(0, cuda::min(sy, height - 2)); + + const T *aPtr = src.ptr(batch_idx, sy, 0); + const T *bPtr = src.ptr(batch_idx, sy + 1, 0); + + T dstPack[NIX]; +#pragma unroll + for (int i = 0; i < NIX; ++i) + { + const int dst_x = dst_x0 + i; + if (dst_x < out_width) + { + float fx = (float)((dst_x + 0.5f) * scale_x - 0.5f + left); + int sx = cuda::round(fx); + + fx = ((sx < 0) ? 0 : ((sx > width - 2) ? 1 : fx - sx)); + sx = cuda::max(0, cuda::min(sx, width - 2)); + + dstPack[i] = cuda::SaturateCast((1.0f - fx) * (aPtr[sx] * (1.0f - fy) + bPtr[sx] * fy) + + fx * (aPtr[sx + 1] * (1.0f - fy) + bPtr[sx + 1] * fy)); + } + } + + T *dstRow = dst.ptr(batch_idx, dst_y, 0); + // dst_x0 advances by NIX, so an aligned row base keeps the packed write aligned. + if (dst_x0 + NIX - 1 < out_width && RRCCheckRowAlign(dstRow)) + { + RRCWritePack(dstRow[dst_x0], dstPack); + } + else + { +#pragma unroll + for (int i = 0; i < NIX; ++i) + { + const int dst_x = dst_x0 + i; + if (dst_x < out_width) + { + dstRow[dst_x] = dstPack[i]; + } + } + } + } +} + template __global__ void resize_nearest_v1(const SrcWrapper src, DstWrapper dst, int2 srcSize, int2 dstSize, const int *top_, const int *left_, const float *scale_x_, const float *scale_y_) @@ -172,16 +187,11 @@ __global__ void resize_cubic_v1(const SrcWrapper src, DstWrapper dst, int2 srcSi const int top = top_[batch_idx]; const int left = left_[batch_idx]; - //float space for weighted addition using work_type = cuda::ConvertBaseTypeTo; - uint readBuffer[MAX_BUFFER_WORDS]; - - //y coordinate float fy = (float)((dst_y + 0.5f) * scale_y - 0.5f + top); int sy = cuda::round(fy); fy -= sy; - sy = cuda::max(1, cuda::min(sy, height - 3)); const float A = -0.75f; @@ -191,37 +201,56 @@ __global__ void resize_cubic_v1(const SrcWrapper src, DstWrapper dst, int2 srcSi cY[2] = ((A + 2) * (1 - fy) - (A + 3)) * (1 - fy) * (1 - fy) + 1; cY[3] = 1.f - cY[0] - cY[1] - cY[2]; - work_type accum = cuda::SetAll(0); - float fx = (float)((dst_x + 0.5f) * scale_x - 0.5f + left); int sx = cuda::round(fx); fx -= sx; - fx *= ((sx >= 1) && (sx < width - 3)); - sx = cuda::max(1, cuda::min(sx, width - 3)); float cX[4]; cX[0] = ((A * (fx + 1.0f) - 5.0f * A) * (fx + 1.0f) + 8.0f * A) * (fx + 1.0f) - 4.0f * A; cX[1] = ((A + 2.0f) * fx - (A + 3.0f)) * fx * fx + 1.0f; cX[2] = ((A + 2.0f) * (1.0f - fx) - (A + 3.0f)) * (1.0f - fx) * (1.0f - fx) + 1.0f; cX[3] = 1.0f - cX[0] - cX[1] - cX[2]; + + work_type accum = cuda::SetAll(0); + + if constexpr (RRC_USE_NIX) + { + int csy[4]; + int csx[4]; +#pragma unroll + for (int k = 0; k < 4; ++k) + { + csy[k] = cuda::clamp(sy + k - 1, 0, height - 1); + csx[k] = cuda::clamp(sx + k - 1, 0, width - 1); + } + #pragma unroll - for (int row = 0; row < 4; ++row) + for (int ky = 0; ky < 4; ++ky) + { + const T *srcRow = src.ptr(batch_idx, csy[ky], 0); +#pragma unroll + for (int kx = 0; kx < 4; ++kx) + { + accum += cY[ky] * cX[kx] * srcRow[csx[kx]]; + } + } + } + else { - //1 - load each sub row from sx-1 to sx+3 inclusive, aligned - //const T * aPtr = src.ptr(batch_idx, sy + row - 1, sx-1); - const T *aPtr = _cacheAlignedBufferedRead( - src, srcSize.x, readBuffer, MAX_BUFFER_WORDS, batch_idx, sy + row - 1, sx - 1, sx + 2); - - //2 - do a pixel's partial on this row - accum += cY[row] * (cX[0] * aPtr[0] + cX[1] * aPtr[1] + cX[2] * aPtr[2] + cX[3] * aPtr[3]); - } //for row -#ifndef LEGACY_BICUBIC_MATH - //correct math +#pragma unroll + for (int ky = 0; ky < 4; ++ky) + { + int csy = cuda::clamp(sy + ky - 1, 0, height - 1); +#pragma unroll + for (int kx = 0; kx < 4; ++kx) + { + int csx = cuda::clamp(sx + kx - 1, 0, width - 1); + accum += cY[ky] * cX[kx] * *src.ptr(batch_idx, csy, csx); + } + } + } + *dst.ptr(batch_idx, dst_y, dst_x) = cuda::SaturateCast(accum); -#else - //abs() needed to match legacy operator. - *dst.ptr(batch_idx, dst_y, dst_x) = cuda::SaturateCast(cuda::abs(accum)); -#endif } } @@ -233,9 +262,22 @@ void resize(const SrcWrapper &src, const DstWrapper &dst, const NVCVInterpolatio dim3 blockSize(BLOCK, BLOCK / 4, 1); dim3 gridSize(divUp(dstSize.x, blockSize.x), divUp(dstSize.y, blockSize.y), batchSize); + using T = typename DstWrapper::ValueType; + if (interpolation == NVCV_INTERP_LINEAR) { - resize_linear_v1<<>>(src, dst, srcSize, dstSize, top, left, scale_x, scale_y); + constexpr int INIX = RRC_USE_NIX ? RRC_NIX : 1; + if constexpr (INIX > 1) + { + dim3 linearGrid(divUp(dstSize.x, blockSize.x * INIX), divUp(dstSize.y, blockSize.y), batchSize); + resize_linear_nix + <<>>(src, dst, srcSize, dstSize, top, left, scale_x, scale_y); + } + else + { + resize_linear_v1<<>>(src, dst, srcSize, dstSize, top, left, scale_x, + scale_y); + } checkKernelErrors(); } else if (interpolation == NVCV_INTERP_NEAREST) @@ -255,6 +297,276 @@ void resize(const SrcWrapper &src, const DstWrapper &dst, const NVCVInterpolatio #endif } +template +__global__ void resize_linear_planar(const SrcWrapper src, DstWrapper dst, int2 srcSize, int2 dstSize, const int *top_, + const int *left_, const float *scale_x_, const float *scale_y_, int channels) +{ + const int dst_x = blockIdx.x * blockDim.x + threadIdx.x; + const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + int height = srcSize.y, width = srcSize.x, out_height = dstSize.y, out_width = dstSize.x; + + if ((dst_x < out_width) && (dst_y < out_height)) + { + const float scale_x = scale_x_[batch_idx]; + const float scale_y = scale_y_[batch_idx]; + const int top = top_[batch_idx]; + const int left = left_[batch_idx]; + + using work_type = cuda::ConvertBaseTypeTo; + + float fy = (float)((dst_y + 0.5f) * scale_y - 0.5f + top); + int sy = cuda::round(fy); + + fy = ((sy < 0) ? 0 : ((sy > height - 2) ? 1 : fy - sy)); + sy = cuda::max(0, cuda::min(sy, height - 2)); + + float fx = (float)((dst_x + 0.5f) * scale_x - 0.5f + left); + int sx = cuda::round(fx); + + fx = ((sx < 0) ? 0 : ((sx > width - 2) ? 1 : fx - sx)); + sx = cuda::max(0, cuda::min(sx, width - 2)); + + for (int plane = 0; plane < channels; ++plane) + { + const T *aPtr = src.ptr(batch_idx, plane, sy, 0); + const T *bPtr = src.ptr(batch_idx, plane, sy + 1, 0); + + *dst.ptr(batch_idx, plane, dst_y, dst_x) + = cuda::SaturateCast((1.0f - fx) * (aPtr[sx] * (1.0f - fy) + bPtr[sx] * fy) + + fx * (aPtr[sx + 1] * (1.0f - fy) + bPtr[sx + 1] * fy)); + } + } +} + +template +__global__ void resize_linear_planar_nix(const SrcWrapper src, DstWrapper dst, int2 srcSize, int2 dstSize, + const int *top_, const int *left_, const float *scale_x_, + const float *scale_y_, int channels) +{ + const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + int height = srcSize.y, width = srcSize.x, out_height = dstSize.y, out_width = dstSize.x; + + if (dst_y < out_height) + { + const float scale_x = scale_x_[batch_idx]; + const float scale_y = scale_y_[batch_idx]; + const int top = top_[batch_idx]; + const int left = left_[batch_idx]; + + float fy = (float)((dst_y + 0.5f) * scale_y - 0.5f + top); + int sy = cuda::round(fy); + + fy = ((sy < 0) ? 0 : ((sy > height - 2) ? 1 : fy - sy)); + sy = cuda::max(0, cuda::min(sy, height - 2)); + + const int xBase = blockIdx.x * blockDim.x * NIX + threadIdx.x; + + int dx[NIX], sxA[NIX]; + float fxA[NIX]; +#pragma unroll + for (int i = 0; i < NIX; ++i) + { + dx[i] = xBase + i * static_cast(blockDim.x); + float fx = (float)((dx[i] + 0.5f) * scale_x - 0.5f + left); + int sx = cuda::round(fx); + + fxA[i] = ((sx < 0) ? 0 : ((sx > width - 2) ? 1 : fx - sx)); + sxA[i] = cuda::max(0, cuda::min(sx, width - 2)); + } + + for (int plane = 0; plane < channels; ++plane) + { + const T *aPtr = src.ptr(batch_idx, plane, sy, 0); + const T *bPtr = src.ptr(batch_idx, plane, sy + 1, 0); + +#pragma unroll + for (int i = 0; i < NIX; ++i) + { + if (dx[i] >= out_width) + continue; + + const int sx = sxA[i]; + const float fx = fxA[i]; + + *dst.ptr(batch_idx, plane, dst_y, dx[i]) + = cuda::SaturateCast((1.0f - fx) * (aPtr[sx] * (1.0f - fy) + bPtr[sx] * fy) + + fx * (aPtr[sx + 1] * (1.0f - fy) + bPtr[sx + 1] * fy)); + } + } + } +} + +template +__global__ void resize_nearest_planar(const SrcWrapper src, DstWrapper dst, int2 srcSize, int2 dstSize, const int *top_, + const int *left_, const float *scale_x_, const float *scale_y_, int channels) +{ + const int dst_x = blockIdx.x * blockDim.x + threadIdx.x; + const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + int out_height = dstSize.y, out_width = dstSize.x; + + if ((dst_x < out_width) && (dst_y < out_height)) + { + const float scale_x = scale_x_[batch_idx]; + const float scale_y = scale_y_[batch_idx]; + const int top = top_[batch_idx]; + const int left = left_[batch_idx]; + + const int sx = cuda::min(__float2int_rd((dst_x + 0.5f) * scale_x) + left, srcSize.x - 1); + const int sy = cuda::min(__float2int_rd((dst_y + 0.5f) * scale_y) + top, srcSize.y - 1); + + for (int plane = 0; plane < channels; ++plane) + { + *dst.ptr(batch_idx, plane, dst_y, dst_x) = *src.ptr(batch_idx, plane, sy, sx); + } + } +} + +template +__global__ void resize_cubic_planar(const SrcWrapper src, DstWrapper dst, int2 srcSize, int2 dstSize, const int *top_, + const int *left_, const float *scale_x_, const float *scale_y_, int channels) +{ + const int dst_x = blockIdx.x * blockDim.x + threadIdx.x; + const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + int height = srcSize.y, width = srcSize.x, out_height = dstSize.y, out_width = dstSize.x; + + if ((dst_x < out_width) & (dst_y < out_height)) + { + const float scale_x = scale_x_[batch_idx]; + const float scale_y = scale_y_[batch_idx]; + const int top = top_[batch_idx]; + const int left = left_[batch_idx]; + + using work_type = cuda::ConvertBaseTypeTo; + + float fy = (float)((dst_y + 0.5f) * scale_y - 0.5f + top); + int sy = cuda::round(fy); + fy -= sy; + + const float A = -0.75f; + + float cY[4]; + cY[0] = ((A * (fy + 1) - 5 * A) * (fy + 1) + 8 * A) * (fy + 1) - 4 * A; + cY[1] = ((A + 2) * fy - (A + 3)) * fy * fy + 1; + cY[2] = ((A + 2) * (1 - fy) - (A + 3)) * (1 - fy) * (1 - fy) + 1; + cY[3] = 1.f - cY[0] - cY[1] - cY[2]; + + float fx = (float)((dst_x + 0.5f) * scale_x - 0.5f + left); + int sx = cuda::round(fx); + fx -= sx; + + float cX[4]; + cX[0] = ((A * (fx + 1.0f) - 5.0f * A) * (fx + 1.0f) + 8.0f * A) * (fx + 1.0f) - 4.0f * A; + cX[1] = ((A + 2.0f) * fx - (A + 3.0f)) * fx * fx + 1.0f; + cX[2] = ((A + 2.0f) * (1.0f - fx) - (A + 3.0f)) * (1.0f - fx) * (1.0f - fx) + 1.0f; + cX[3] = 1.0f - cX[0] - cX[1] - cX[2]; + + int csy[4]; + int csx[4]; +#pragma unroll + for (int k = 0; k < 4; ++k) + { + csy[k] = cuda::clamp(sy + k - 1, 0, height - 1); + csx[k] = cuda::clamp(sx + k - 1, 0, width - 1); + } + + for (int plane = 0; plane < channels; ++plane) + { + work_type accum = cuda::SetAll(0); + +#pragma unroll + for (int ky = 0; ky < 4; ++ky) + { +#pragma unroll + for (int kx = 0; kx < 4; ++kx) + { + accum += cY[ky] * cX[kx] * *src.ptr(batch_idx, plane, csy[ky], csx[kx]); + } + } + + *dst.ptr(batch_idx, plane, dst_y, dst_x) = cuda::SaturateCast(accum); + } + } +} + +template +ErrorCode resize_planar(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, + const NVCVInterpolationType interpolation, cudaStream_t stream, const int *top, const int *left, + const float *scale_x, const float *scale_y) +{ + auto inAccess = TensorDataAccessStridedImagePlanar::Create(inData); + NVCV_ASSERT(inAccess); + + auto outAccess = TensorDataAccessStridedImagePlanar::Create(outData); + NVCV_ASSERT(outAccess); + + const int2 srcSize{inAccess->numCols(), inAccess->numRows()}; + const int2 dstSize{outAccess->numCols(), outAccess->numRows()}; + const int batchSize{static_cast(outAccess->numSamples())}; + const int channels{inAccess->numChannels()}; + + int64_t srcMaxStride = inAccess->sampleStride() * inAccess->numSamples(); + int64_t dstMaxStride = outAccess->sampleStride() * outAccess->numSamples(); + + if (std::max(srcMaxStride, dstMaxStride) > cuda::TypeTraits::max) + { + LOG_ERROR("Input or output size exceeds " << cuda::TypeTraits::max << ". Tensor is too large."); + return ErrorCode::INVALID_PARAMETER; + } + + auto src = cuda::CreateTensorWrapNCHW(inData); + auto dst = cuda::CreateTensorWrapNCHW(outData); + + const int THREADS_PER_BLOCK = 256; + int planarWidth = 32 / static_cast(sizeof(T)); + if (planarWidth < 1) + { + planarWidth = 1; + } + + dim3 blockSize(planarWidth, THREADS_PER_BLOCK / planarWidth, 1); + dim3 gridSize(divUp(dstSize.x, blockSize.x), divUp(dstSize.y, blockSize.y), batchSize); + + if (interpolation == NVCV_INTERP_LINEAR) + { + constexpr int PNIX = (sizeof(T) == 1) ? 4 : 1; + if constexpr (PNIX > 1) + { + dim3 linearGrid(divUp(dstSize.x, blockSize.x * PNIX), divUp(dstSize.y, blockSize.y), batchSize); + resize_linear_planar_nix<<>>(src, dst, srcSize, dstSize, top, left, + scale_x, scale_y, channels); + } + else + { + resize_linear_planar<<>>(src, dst, srcSize, dstSize, top, left, scale_x, + scale_y, channels); + } + checkKernelErrors(); + } + else if (interpolation == NVCV_INTERP_NEAREST) + { + resize_nearest_planar<<>>(src, dst, srcSize, dstSize, top, left, scale_x, + scale_y, channels); + checkKernelErrors(); + } + else + { + resize_cubic_planar<<>>(src, dst, srcSize, dstSize, top, left, scale_x, scale_y, + channels); + checkKernelErrors(); + } + +#ifdef CUDA_DEBUG_LOG + checkCudaErrors(cudaStreamSynchronize(stream)); + checkCudaErrors(cudaGetLastError()); +#endif + + return ErrorCode::SUCCESS; +} + template ErrorCode resize(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, const NVCVInterpolationType interpolation, cudaStream_t stream, const int *top, const int *left, @@ -305,12 +617,12 @@ RandomResizedCrop::RandomResizedCrop(DataShape max_input_shape, DataShape max_ou if (maxBatchSize > 0) { size_t bufferSize = (sizeof(int) * 2 + sizeof(float) * 2) * maxBatchSize; - NVCV_CHECK_LOG(cudaMalloc(&m_gpuCropParams, bufferSize)); - m_cpuCropParams = malloc(bufferSize); + NVCV_CHECK_LOG(cudaMalloc(reinterpret_cast(&m_gpuCropParams), bufferSize)); + m_cpuCropParams = static_cast(std::malloc(bufferSize)); if (!m_cpuCropParams) { LOG_ERROR("Memory allocation error of size: " << bufferSize); - throw std::runtime_error("Memory allocation error!"); + throw LegacyCudaAllocationError("Memory allocation error!"); } } if (seed == 0) @@ -327,7 +639,7 @@ RandomResizedCrop::RandomResizedCrop(DataShape max_input_shape, DataShape max_ou RandomResizedCrop::~RandomResizedCrop() { NVCV_CHECK_LOG(cudaFree(m_gpuCropParams)); - free(m_cpuCropParams); + std::free(m_cpuCropParams); } size_t RandomResizedCrop::calBufferSize(int batch_size) @@ -336,6 +648,41 @@ size_t RandomResizedCrop::calBufferSize(int batch_size) return (sizeof(int) * 2 + sizeof(float) * 2) * batch_size; } +int32_t RandomResizedCrop::maxBatchSize() const noexcept +{ + return m_maxBatchSize; +} + +RandomResizedCrop::CropParamBuffers RandomResizedCrop::hostCropParams(int batch) noexcept +{ + return { + reinterpret_cast(m_cpuCropParams), + reinterpret_cast(m_cpuCropParams + sizeof(float) * batch), + reinterpret_cast(m_cpuCropParams + sizeof(float) * 2 * batch), + reinterpret_cast(m_cpuCropParams + (sizeof(float) * 2 + sizeof(int)) * batch), + }; +} + +RandomResizedCrop::CropParamBuffers RandomResizedCrop::deviceCropParams(int batch) noexcept +{ + return { + reinterpret_cast(m_gpuCropParams), + reinterpret_cast(m_gpuCropParams + sizeof(float) * batch), + reinterpret_cast(m_gpuCropParams + sizeof(float) * 2 * batch), + reinterpret_cast(m_gpuCropParams + (sizeof(float) * 2 + sizeof(int)) * batch), + }; +} + +std::byte *RandomResizedCrop::hostCropParamStorage() noexcept +{ + return m_cpuCropParams; +} + +std::byte *RandomResizedCrop::deviceCropParamStorage() noexcept +{ + return m_gpuCropParams; +} + void RandomResizedCrop::getCropParams(int input_rows, int input_cols, int *top_indices, int *left_indices, int *crop_rows, int *crop_cols) { @@ -404,12 +751,15 @@ ErrorCode RandomResizedCrop::infer(const TensorDataStridedCuda &inData, const Te DataFormat format = input_format; - if (!(format == kNHWC || format == kHWC)) + if (!(format == kNHWC || format == kHWC || format == kNCHW || format == kCHW)) { - LOG_ERROR("Invalid input DataFormat " << format << ", the valid DataFormats are: \"NHWC\", \"HWC\""); + LOG_ERROR("Invalid input DataFormat " << format + << ", the valid DataFormats are: \"NHWC\", \"HWC\", \"NCHW\", \"CHW\""); return ErrorCode::INVALID_DATA_FORMAT; } + const bool isPlanar = (format == kNCHW || format == kCHW); + auto inAccess = TensorDataAccessStridedImagePlanar::Create(inData); NVCV_ASSERT(inAccess); @@ -419,7 +769,7 @@ ErrorCode RandomResizedCrop::infer(const TensorDataStridedCuda &inData, const Te int channels = input_shape.C; - if (channels > 4) + if (channels > 4 || channels == 2) { LOG_ERROR("Invalid channel number " << channels); return ErrorCode::INVALID_DATA_SHAPE; @@ -454,29 +804,29 @@ ErrorCode RandomResizedCrop::infer(const TensorDataStridedCuda &inData, const Te int in_cols = inAccess->numCols(); int in_rows = inAccess->numRows(); - float *scale_y = (float *)(m_cpuCropParams); - float *scale_x = (float *)((char *)scale_y + sizeof(float) * batch); - int *tops = (int *)((char *)scale_x + sizeof(float) * batch); - int *lefts = (int *)((char *)tops + sizeof(int) * batch); + if (maxBatchSize() <= 0 || batch > maxBatchSize()) + { + LOG_ERROR("Invalid maximum batch size " << maxBatchSize()); + return ErrorCode::INVALID_PARAMETER; + } + + CropParamBuffers hostParams = hostCropParams(batch); for (int i = 0; i < batch; ++i) { int top, left, crop_rows, crop_cols; getCropParams(in_rows, in_cols, &top, &left, &crop_rows, &crop_cols); - scale_x[i] = ((float)crop_cols) / out_cols; - scale_y[i] = ((float)crop_rows) / out_rows; - tops[i] = top; - lefts[i] = left; + hostParams.scaleX[i] = ((float)crop_cols) / out_cols; + hostParams.scaleY[i] = ((float)crop_rows) / out_rows; + hostParams.tops[i] = top; + hostParams.lefts[i] = left; } - float *scale_y_gpu = (float *)(m_gpuCropParams); - float *scale_x_gpu = (float *)((char *)scale_y_gpu + sizeof(float) * batch); - int *tops_gpu = (int *)((char *)scale_x_gpu + sizeof(float) * batch); - int *lefts_gpu = (int *)((char *)tops_gpu + sizeof(int) * batch); + CropParamBuffers deviceParams = deviceCropParams(batch); size_t buffer_size = calBufferSize(batch); checkCudaErrors( - cudaMemcpyAsync((void *)m_gpuCropParams, (void *)m_cpuCropParams, buffer_size, cudaMemcpyHostToDevice, stream)); + cudaMemcpyAsync(deviceCropParamStorage(), hostCropParamStorage(), buffer_size, cudaMemcpyHostToDevice, stream)); typedef ErrorCode (*func_t)(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, const NVCVInterpolationType interpolation, cudaStream_t stream, const int *top, @@ -491,8 +841,28 @@ ErrorCode RandomResizedCrop::infer(const TensorDataStridedCuda &inData, const Te { resize, 0 /*resize*/, resize, resize} }; + if (isPlanar) + { + if (static_cast(batch) > 65535) + { + LOG_ERROR("Planar random resized crop requires numSamples <= 65535 (CUDA grid-z limit)"); + return ErrorCode::INVALID_DATA_SHAPE; + } + static const func_t planar_funcs[6] = { + resize_planar, 0 /*resize_planar*/, resize_planar, + resize_planar, 0 /*resize_planar*/, resize_planar, + }; + + const func_t planar_func = planar_funcs[in_data_type]; + NVCV_ASSERT(planar_func != 0); + return planar_func(inData, outData, interpolation, stream, deviceParams.tops, deviceParams.lefts, + deviceParams.scaleX, deviceParams.scaleY); + } + const func_t func = funcs[in_data_type][channels - 1]; - return func(inData, outData, interpolation, stream, tops_gpu, lefts_gpu, scale_x_gpu, scale_y_gpu); + NVCV_ASSERT(func != 0); + return func(inData, outData, interpolation, stream, deviceParams.tops, deviceParams.lefts, deviceParams.scaleX, + deviceParams.scaleY); } } // namespace nvcv::legacy::cuda_op diff --git a/src/cvcuda/priv/legacy/random_resized_crop_common.cuh b/src/cvcuda/priv/legacy/random_resized_crop_common.cuh new file mode 100644 index 000000000..4e264df96 --- /dev/null +++ b/src/cvcuda/priv/legacy/random_resized_crop_common.cuh @@ -0,0 +1,59 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#ifndef CVCUDA_LEGACY_RANDOM_RESIZED_CROP_COMMON_CUH +#define CVCUDA_LEGACY_RANDOM_RESIZED_CROP_COMMON_CUH + +#include + +#include +#include + +namespace nvcv::legacy::cuda_op { + +// Three-channel pixels need uint3-sized stores; other packed 8-bit pixels use +// uint4 to keep the vectorized write width naturally aligned. +template +using RRC_DPT = std::conditional_t == 3, uint3, uint4>; + +// Number of logical pixels carried by one vectorized destination store. +template +constexpr int RRC_NIX = sizeof(RRC_DPT) / sizeof(T); + +// uint3 has 4-byte alignment despite its 12-byte size; uint4 uses its full width. +template +constexpr unsigned int RRC_MSK = (sizeof(RRC_DPT) == sizeof(uint3) ? sizeof(unsigned int) : sizeof(RRC_DPT)) - 1; + +// The NIX kernels only pack 8-bit base types; wider types stay on scalar paths. +template +constexpr bool RRC_USE_NIX = sizeof(cuda::BaseType) == 1; + +template +__device__ __forceinline__ void RRCWritePack(T &u, const T (&v)[RRC_NIX]) +{ + reinterpret_cast &>(u) = reinterpret_cast &>(v); +} + +template +__device__ __forceinline__ bool RRCCheckRowAlign(T *row) +{ + return (static_cast(reinterpret_cast(row)) & RRC_MSK) == 0; +} + +} // namespace nvcv::legacy::cuda_op + +#endif // CVCUDA_LEGACY_RANDOM_RESIZED_CROP_COMMON_CUH diff --git a/src/cvcuda/priv/legacy/random_resized_crop_var_shape.cu b/src/cvcuda/priv/legacy/random_resized_crop_var_shape.cu index 79f135f42..92abefa48 100644 --- a/src/cvcuda/priv/legacy/random_resized_crop_var_shape.cu +++ b/src/cvcuda/priv/legacy/random_resized_crop_var_shape.cu @@ -1,4 +1,4 @@ -/* Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +/* Copyright (c) 2021-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * * SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES * SPDX-License-Identifier: Apache-2.0 @@ -22,12 +22,14 @@ #include "CvCudaLegacyHelpers.hpp" #include "CvCudaUtils.cuh" +#include "random_resized_crop_common.cuh" #include #include #include #include +#include using namespace nvcv; using namespace nvcv::legacy::cuda_op; @@ -37,58 +39,6 @@ namespace nvcv::legacy::cuda_op { #define BLOCK 32 -#define MAX_BUFFER_BYTES_VS 128 //multiple of 4 for word-aligned read, multiple of 16 for cacheline alignment (float4) -#define MAX_BUFFER_WORDS_VS (MAX_BUFFER_BYTES_VS / 4) //extra bytes for cache alignment - -#define LEGACY_BICUBIC_MATH_VS //apparently the legacy code has an abs() that needs to be matched - -// Replaced below 15 to 0 due to a reported regression -#define CACHE_MEMORY_ALIGNMENT_VS 0 //this is 'M' for _cacheAlignedBufferedReadVS - -//legal values for CACHE_MEMORY_ALIGNMENT_VS are: -// 31: 256-bit alignment -// 15: 128-bit alignment <-- should be ideal for Ampere -// 7: 64-bit alignment -// 3: 32-bit alignment (word) -// 0: disable buffering -template -inline __device__ T *_cacheAlignedBufferedReadVS(cuda::ImageBatchVarShapeWrap srcImage, int width, - uint *pReadBuffer, uint nReadBufferWordsMax, int nBatch, int nYPos, - int nXPosMin, int nXPosMax) -{ - const T *lineStartPtr = srcImage.ptr(nBatch, nYPos, 0); //do not access prior to this address - const T *pixSrcPtr = &lineStartPtr[nXPosMin]; - if (M == 0) - return (T *)pixSrcPtr; //return GMEM pointer instead - else - { - uint *memSrcPtr = (uint *)(((size_t)pixSrcPtr) & (~M)); //(M+1) byte alignment - const T *pixBeyondPtr = &lineStartPtr[nXPosMax + 1]; - const int functionalWidth = ((size_t)pixBeyondPtr + M) & (~M) - ((size_t)lineStartPtr); - const int nWordsToRead = (((size_t)pixBeyondPtr + M) & (~M) - (size_t)memSrcPtr) / 4; - - if (((size_t)memSrcPtr < (size_t)lineStartPtr) || (width * sizeof(T) < functionalWidth) - || (nWordsToRead > nReadBufferWordsMax)) - return (T *)pixSrcPtr; //return GMEM pointer instead if running off the image - else - { //copy out source data, aligned based upon M (31, 15, 7, 3) - const int skew = ((size_t)pixSrcPtr) & M; //byte offset for nXPosMin - int i = 0; - if (M >= 31) //256-bit align, 32 bytes at a time - for (; i < nWordsToRead; i += 8) - *((double4_16a *)(&pReadBuffer[i])) = *((double4_16a *)(&memSrcPtr[i])); - if (M == 15) //128-bit align, 16 bytes at a time - for (; i < nWordsToRead; i += 4) *((float4 *)(&pReadBuffer[i])) = *((float4 *)(&memSrcPtr[i])); - if (M == 7) //64-bit align, 8 bytes at a time - for (; i < nWordsToRead; i += 2) *((float2 *)(&pReadBuffer[i])) = *((float2 *)(&memSrcPtr[i])); - //32-bit align, 4 bytes at a time - for (; i < nWordsToRead; ++i) pReadBuffer[i] = memSrcPtr[i]; - - return (T *)(((size_t)pReadBuffer) + skew); //buffered pixel data - } - } -} //_cacheAlignedBufferedReadVS - template __global__ void resize_linear_v1(const SrcWrapper src, DstWrapper dst, const int *top_, const int *left_, const float *scale_x_, const float *scale_y_) @@ -137,6 +87,74 @@ __global__ void resize_linear_v1(const SrcWrapper src, DstWrapper dst, const int } } +template +__global__ void resize_linear_nix(const SrcWrapper src, DstWrapper dst, const int *top_, const int *left_, + const float *scale_x_, const float *scale_y_) +{ + const int dst_x0 = (blockIdx.x * blockDim.x + threadIdx.x) * NIX; + const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + const int dstWidth = dst.width(batch_idx); + const int dstHeight = dst.height(batch_idx); + + if (dst_y < dstHeight) + { + const int width = src.width(batch_idx); + const int height = src.height(batch_idx); + + const float scale_x = scale_x_[batch_idx]; + const float scale_y = scale_y_[batch_idx]; + const int top = top_[batch_idx]; + const int left = left_[batch_idx]; + + float fy = (float)((dst_y + 0.5f) * scale_y - 0.5f + top); + int sy = cuda::round(fy); + + fy = ((sy < 0) ? 0 : ((sy > height - 2) ? 1 : fy - sy)); + sy = cuda::max(0, cuda::min(sy, height - 2)); + + const T *aPtr = src.ptr(batch_idx, sy, 0); + const T *bPtr = src.ptr(batch_idx, sy + 1, 0); + + T dstPack[NIX]; +#pragma unroll + for (int i = 0; i < NIX; ++i) + { + const int dst_x = dst_x0 + i; + if (dst_x < dstWidth) + { + float fx = (float)((dst_x + 0.5f) * scale_x - 0.5f + left); + int sx = cuda::round(fx); + + fx = ((sx < 0) ? 0 : ((sx > width - 2) ? 1 : fx - sx)); + sx = cuda::max(0, cuda::min(sx, width - 2)); + + dstPack[i] = cuda::SaturateCast((1.0f - fx) * (aPtr[sx] * (1.0f - fy) + bPtr[sx] * fy) + + fx * (aPtr[sx + 1] * (1.0f - fy) + bPtr[sx + 1] * fy)); + } + } + + T *dstRow = dst.ptr(batch_idx, dst_y, 0); + // dst_x0 advances by NIX, so an aligned row base keeps the packed write aligned. + if (dst_x0 + NIX - 1 < dstWidth && RRCCheckRowAlign(dstRow)) + { + RRCWritePack(dstRow[dst_x0], dstPack); + } + else + { +#pragma unroll + for (int i = 0; i < NIX; ++i) + { + const int dst_x = dst_x0 + i; + if (dst_x < dstWidth) + { + dstRow[dst_x] = dstPack[i]; + } + } + } + } +} + template __global__ void resize_nearest_v1(const SrcWrapper src, DstWrapper dst, const int *top_, const int *left_, const float *scale_x_, const float *scale_y_) @@ -184,16 +202,11 @@ __global__ void resize_cubic_v1(const SrcWrapper src, DstWrapper dst, const int const int top = top_[batch_idx]; const int left = left_[batch_idx]; - //float space for weighted addition using work_type = cuda::ConvertBaseTypeTo; - uint readBuffer[MAX_BUFFER_WORDS_VS]; - - //y coordinate float fy = (float)((dst_y + 0.5f) * scale_y - 0.5f + top); int sy = cuda::round(fy); fy -= sy; - sy = cuda::max(1, cuda::min(sy, height - 3)); const float A = -0.75f; @@ -203,37 +216,56 @@ __global__ void resize_cubic_v1(const SrcWrapper src, DstWrapper dst, const int cY[2] = ((A + 2) * (1 - fy) - (A + 3)) * (1 - fy) * (1 - fy) + 1; cY[3] = 1.f - cY[0] - cY[1] - cY[2]; - work_type accum = cuda::SetAll(0); - float fx = (float)((dst_x + 0.5f) * scale_x - 0.5f + left); int sx = cuda::round(fx); fx -= sx; - fx *= ((sx >= 1) && (sx < width - 3)); - sx = cuda::max(1, cuda::min(sx, width - 3)); float cX[4]; cX[0] = ((A * (fx + 1.0f) - 5.0f * A) * (fx + 1.0f) + 8.0f * A) * (fx + 1.0f) - 4.0f * A; cX[1] = ((A + 2.0f) * fx - (A + 3.0f)) * fx * fx + 1.0f; cX[2] = ((A + 2.0f) * (1.0f - fx) - (A + 3.0f)) * (1.0f - fx) * (1.0f - fx) + 1.0f; cX[3] = 1.0f - cX[0] - cX[1] - cX[2]; + + work_type accum = cuda::SetAll(0); + + if constexpr (RRC_USE_NIX) + { + int csy[4]; + int csx[4]; #pragma unroll - for (int row = 0; row < 4; ++row) + for (int k = 0; k < 4; ++k) + { + csy[k] = cuda::clamp(sy + k - 1, 0, height - 1); + csx[k] = cuda::clamp(sx + k - 1, 0, width - 1); + } + +#pragma unroll + for (int ky = 0; ky < 4; ++ky) + { + const T *srcRow = src.ptr(batch_idx, csy[ky], 0); +#pragma unroll + for (int kx = 0; kx < 4; ++kx) + { + accum += cY[ky] * cX[kx] * srcRow[csx[kx]]; + } + } + } + else { - //1 - load each sub row from sx-1 to sx+3 inclusive, aligned - //const T * aPtr = src.ptr(batch_idx, sy + row - 1, sx-1); - T *aPtr = _cacheAlignedBufferedReadVS( - src, width, readBuffer, MAX_BUFFER_WORDS_VS, batch_idx, sy + row - 1, sx - 1, sx + 2); - - //2 - do a pixel's partial on this row - accum += cY[row] * (cX[0] * aPtr[0] + cX[1] * aPtr[1] + cX[2] * aPtr[2] + cX[3] * aPtr[3]); - } //for row -#ifndef LEGACY_BICUBIC_MATH_VS - //correct math +#pragma unroll + for (int ky = 0; ky < 4; ++ky) + { + int csy = cuda::clamp(sy + ky - 1, 0, height - 1); +#pragma unroll + for (int kx = 0; kx < 4; ++kx) + { + int csx = cuda::clamp(sx + kx - 1, 0, width - 1); + accum += cY[ky] * cX[kx] * *src.ptr(batch_idx, csy, csx); + } + } + } + *dst.ptr(batch_idx, dst_y, dst_x) = cuda::SaturateCast(accum); -#else - //abs() needed to match legacy operator. - *dst.ptr(batch_idx, dst_y, dst_x) = cuda::SaturateCast(cuda::abs(accum)); -#endif } } @@ -253,7 +285,16 @@ void resize(const ImageBatchVarShapeDataStridedCuda &in, const ImageBatchVarShap if (interpolation == NVCV_INTERP_LINEAR) { - resize_linear_v1<<>>(src, dst, top, left, scale_x, scale_y); + constexpr int INIX = RRC_USE_NIX ? RRC_NIX : 1; + if constexpr (INIX > 1) + { + dim3 linearGrid(divUp(outMaxSize.w, blockSize.x * INIX), divUp(outMaxSize.h, blockSize.y), in.numImages()); + resize_linear_nix<<>>(src, dst, top, left, scale_x, scale_y); + } + else + { + resize_linear_v1<<>>(src, dst, top, left, scale_x, scale_y); + } checkKernelErrors(); } else if (interpolation == NVCV_INTERP_NEAREST) @@ -273,6 +314,270 @@ void resize(const ImageBatchVarShapeDataStridedCuda &in, const ImageBatchVarShap #endif } +template +__global__ void resize_linear_planar(const SrcWrapper src, DstWrapper dst, const int *top_, const int *left_, + const float *scale_x_, const float *scale_y_, int channels) +{ + const int dst_x = blockIdx.x * blockDim.x + threadIdx.x; + const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + const int dstWidth = dst.width(batch_idx); + const int dstHeight = dst.height(batch_idx); + + if ((dst_x < dstWidth) && (dst_y < dstHeight)) + { + const int width = src.width(batch_idx); + const int height = src.height(batch_idx); + + const float scale_x = scale_x_[batch_idx]; + const float scale_y = scale_y_[batch_idx]; + const int top = top_[batch_idx]; + const int left = left_[batch_idx]; + + using work_type = cuda::ConvertBaseTypeTo; + + float fy = (float)((dst_y + 0.5f) * scale_y - 0.5f + top); + int sy = cuda::round(fy); + + fy = ((sy < 0) ? 0 : ((sy > height - 2) ? 1 : fy - sy)); + sy = cuda::max(0, cuda::min(sy, height - 2)); + + float fx = (float)((dst_x + 0.5f) * scale_x - 0.5f + left); + int sx = cuda::round(fx); + + fx = ((sx < 0) ? 0 : ((sx > width - 2) ? 1 : fx - sx)); + sx = cuda::max(0, cuda::min(sx, width - 2)); + + for (int plane = 0; plane < channels; ++plane) + { + const T *aPtr = src.ptr(batch_idx, plane, sy, 0); + const T *bPtr = src.ptr(batch_idx, plane, sy + 1, 0); + + *dst.ptr(batch_idx, plane, dst_y, dst_x) + = cuda::SaturateCast((1.0f - fx) * (aPtr[sx] * (1.0f - fy) + bPtr[sx] * fy) + + fx * (aPtr[sx + 1] * (1.0f - fy) + bPtr[sx + 1] * fy)); + } + } +} + +template +__global__ void resize_linear_planar_nix(const SrcWrapper src, DstWrapper dst, const int *top_, const int *left_, + const float *scale_x_, const float *scale_y_, int channels) +{ + const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + const int dstWidth = dst.width(batch_idx); + const int dstHeight = dst.height(batch_idx); + + if (dst_y < dstHeight) + { + const int width = src.width(batch_idx); + const int height = src.height(batch_idx); + + const float scale_x = scale_x_[batch_idx]; + const float scale_y = scale_y_[batch_idx]; + const int top = top_[batch_idx]; + const int left = left_[batch_idx]; + + float fy = (float)((dst_y + 0.5f) * scale_y - 0.5f + top); + int sy = cuda::round(fy); + + fy = ((sy < 0) ? 0 : ((sy > height - 2) ? 1 : fy - sy)); + sy = cuda::max(0, cuda::min(sy, height - 2)); + + const int xBase = blockIdx.x * blockDim.x * NIX + threadIdx.x; + + int dx[NIX], sxA[NIX]; + float fxA[NIX]; +#pragma unroll + for (int i = 0; i < NIX; ++i) + { + dx[i] = xBase + i * static_cast(blockDim.x); + float fx = (float)((dx[i] + 0.5f) * scale_x - 0.5f + left); + int sx = cuda::round(fx); + + fxA[i] = ((sx < 0) ? 0 : ((sx > width - 2) ? 1 : fx - sx)); + sxA[i] = cuda::max(0, cuda::min(sx, width - 2)); + } + + for (int plane = 0; plane < channels; ++plane) + { + const T *aPtr = src.ptr(batch_idx, plane, sy, 0); + const T *bPtr = src.ptr(batch_idx, plane, sy + 1, 0); + +#pragma unroll + for (int i = 0; i < NIX; ++i) + { + if (dx[i] >= dstWidth) + continue; + + const int sx = sxA[i]; + const float fx = fxA[i]; + + *dst.ptr(batch_idx, plane, dst_y, dx[i]) + = cuda::SaturateCast((1.0f - fx) * (aPtr[sx] * (1.0f - fy) + bPtr[sx] * fy) + + fx * (aPtr[sx + 1] * (1.0f - fy) + bPtr[sx + 1] * fy)); + } + } + } +} + +template +__global__ void resize_nearest_planar(const SrcWrapper src, DstWrapper dst, const int *top_, const int *left_, + const float *scale_x_, const float *scale_y_, int channels) +{ + const int dst_x = blockIdx.x * blockDim.x + threadIdx.x; + const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + const int dstWidth = dst.width(batch_idx); + const int dstHeight = dst.height(batch_idx); + + if ((dst_x < dstWidth) && (dst_y < dstHeight)) + { + const int width = src.width(batch_idx); + const int height = src.height(batch_idx); + + const float scale_x = scale_x_[batch_idx]; + const float scale_y = scale_y_[batch_idx]; + const int top = top_[batch_idx]; + const int left = left_[batch_idx]; + + const int sx = cuda::min(__float2int_rd((dst_x + 0.5f) * scale_x) + left, width - 1); + const int sy = cuda::min(__float2int_rd((dst_y + 0.5f) * scale_y) + top, height - 1); + + for (int plane = 0; plane < channels; ++plane) + { + *dst.ptr(batch_idx, plane, dst_y, dst_x) = *src.ptr(batch_idx, plane, sy, sx); + } + } +} + +template +__global__ void resize_cubic_planar(const SrcWrapper src, DstWrapper dst, const int *top_, const int *left_, + const float *scale_x_, const float *scale_y_, int channels) +{ + const int dst_x = blockIdx.x * blockDim.x + threadIdx.x; + const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + const int dstWidth = dst.width(batch_idx); + const int dstHeight = dst.height(batch_idx); + + if ((dst_x < dstWidth) & (dst_y < dstHeight)) + { + const int width = src.width(batch_idx); + const int height = src.height(batch_idx); + + const float scale_x = scale_x_[batch_idx]; + const float scale_y = scale_y_[batch_idx]; + const int top = top_[batch_idx]; + const int left = left_[batch_idx]; + + using work_type = cuda::ConvertBaseTypeTo; + + float fy = (float)((dst_y + 0.5f) * scale_y - 0.5f + top); + int sy = cuda::round(fy); + fy -= sy; + + const float A = -0.75f; + + float cY[4]; + cY[0] = ((A * (fy + 1) - 5 * A) * (fy + 1) + 8 * A) * (fy + 1) - 4 * A; + cY[1] = ((A + 2) * fy - (A + 3)) * fy * fy + 1; + cY[2] = ((A + 2) * (1 - fy) - (A + 3)) * (1 - fy) * (1 - fy) + 1; + cY[3] = 1.f - cY[0] - cY[1] - cY[2]; + + float fx = (float)((dst_x + 0.5f) * scale_x - 0.5f + left); + int sx = cuda::round(fx); + fx -= sx; + + float cX[4]; + cX[0] = ((A * (fx + 1.0f) - 5.0f * A) * (fx + 1.0f) + 8.0f * A) * (fx + 1.0f) - 4.0f * A; + cX[1] = ((A + 2.0f) * fx - (A + 3.0f)) * fx * fx + 1.0f; + cX[2] = ((A + 2.0f) * (1.0f - fx) - (A + 3.0f)) * (1.0f - fx) * (1.0f - fx) + 1.0f; + cX[3] = 1.0f - cX[0] - cX[1] - cX[2]; + + int csy[4]; + int csx[4]; +#pragma unroll + for (int k = 0; k < 4; ++k) + { + csy[k] = cuda::clamp(sy + k - 1, 0, height - 1); + csx[k] = cuda::clamp(sx + k - 1, 0, width - 1); + } + + for (int plane = 0; plane < channels; ++plane) + { + work_type accum = cuda::SetAll(0); + +#pragma unroll + for (int ky = 0; ky < 4; ++ky) + { +#pragma unroll + for (int kx = 0; kx < 4; ++kx) + { + accum += cY[ky] * cX[kx] * *src.ptr(batch_idx, plane, csy[ky], csx[kx]); + } + } + + *dst.ptr(batch_idx, plane, dst_y, dst_x) = cuda::SaturateCast(accum); + } + } +} + +template +void resize_planar(const ImageBatchVarShapeDataStridedCuda &in, const ImageBatchVarShapeDataStridedCuda &out, + const NVCVInterpolationType interpolation, cudaStream_t stream, float *scale_y, float *scale_x, + int *top, int *left, int channels) +{ + NVCV_ASSERT(in.numImages() == out.numImages()); + + Size2D outMaxSize = out.maxSize(); + + const int THREADS_PER_BLOCK = 256; + int planarWidth = 32 / static_cast(sizeof(T)); + if (planarWidth < 1) + { + planarWidth = 1; + } + + dim3 blockSize(planarWidth, THREADS_PER_BLOCK / planarWidth, 1); + dim3 gridSize(divUp(outMaxSize.w, blockSize.x), divUp(outMaxSize.h, blockSize.y), in.numImages()); + + cuda::ImageBatchVarShapeWrap src(in); + cuda::ImageBatchVarShapeWrap dst(out); + + if (interpolation == NVCV_INTERP_LINEAR) + { + constexpr int PNIX = (sizeof(T) == 1) ? 4 : 1; + if constexpr (PNIX > 1) + { + dim3 linearGrid(divUp(outMaxSize.w, blockSize.x * PNIX), divUp(outMaxSize.h, blockSize.y), in.numImages()); + resize_linear_planar_nix + <<>>(src, dst, top, left, scale_x, scale_y, channels); + } + else + { + resize_linear_planar<<>>(src, dst, top, left, scale_x, scale_y, channels); + } + checkKernelErrors(); + } + else if (interpolation == NVCV_INTERP_NEAREST) + { + resize_nearest_planar<<>>(src, dst, top, left, scale_x, scale_y, channels); + checkKernelErrors(); + } + else + { + resize_cubic_planar<<>>(src, dst, top, left, scale_x, scale_y, channels); + checkKernelErrors(); + } + +#ifdef CUDA_DEBUG_LOG + checkCudaErrors(cudaStreamSynchronize(stream)); + checkCudaErrors(cudaGetLastError()); +#endif +} + RandomResizedCropVarShape::RandomResizedCropVarShape(DataShape max_input_shape, DataShape max_output_shape, const double min_scale, const double max_scale, const double min_ratio, const double max_ratio, @@ -311,9 +616,9 @@ ErrorCode RandomResizedCropVarShape::infer(const ImageBatchVarShape &in, const I return ErrorCode::INVALID_DATA_FORMAT; } - if (m_maxBatchSize <= 0 || inData.numImages() > m_maxBatchSize) + if (maxBatchSize() <= 0 || inData.numImages() > maxBatchSize()) { - LOG_ERROR("Invalid maximum batch size " << m_maxBatchSize); + LOG_ERROR("Invalid maximum batch size " << maxBatchSize()); return ErrorCode::INVALID_PARAMETER; } @@ -328,15 +633,18 @@ ErrorCode RandomResizedCropVarShape::infer(const ImageBatchVarShape &in, const I DataFormat format = input_format; - if (!(format == kNHWC || format == kHWC)) + if (!(format == kNHWC || format == kHWC || format == kNCHW || format == kCHW)) { - LOG_ERROR("Invalid input DataFormat " << format << ", the valid DataFormats are: \"NHWC\", \"HWC\""); + LOG_ERROR("Invalid input DataFormat " << format + << ", the valid DataFormats are: \"NHWC\", \"HWC\", \"NCHW\", \"CHW\""); return ErrorCode::INVALID_DATA_FORMAT; } + const bool isPlanar = (format == kNCHW || format == kCHW); + int channels = inData.uniqueFormat().numChannels(); - if (channels > 4) + if (channels > 4 || channels == 2) { LOG_ERROR("Invalid channel number " << channels); return ErrorCode::INVALID_DATA_SHAPE; @@ -366,10 +674,7 @@ ErrorCode RandomResizedCropVarShape::infer(const ImageBatchVarShape &in, const I int batch = inData.numImages(); - float *scale_y = (float *)(m_cpuCropParams); - float *scale_x = (float *)((char *)scale_y + sizeof(float) * batch); - int *tops = (int *)((char *)scale_x + sizeof(float) * batch); - int *lefts = (int *)((char *)tops + sizeof(int) * batch); + CropParamBuffers hostParams = hostCropParams(batch); for (int i = 0; i < batch; ++i) { @@ -380,20 +685,17 @@ ErrorCode RandomResizedCropVarShape::infer(const ImageBatchVarShape &in, const I } int top, left, crop_rows, crop_cols; getCropParams(in[i].size().h, in[i].size().w, &top, &left, &crop_rows, &crop_cols); - scale_x[i] = ((float)crop_cols) / out[i].size().w; - scale_y[i] = ((float)crop_rows) / out[i].size().h; - tops[i] = top; - lefts[i] = left; + hostParams.scaleX[i] = ((float)crop_cols) / out[i].size().w; + hostParams.scaleY[i] = ((float)crop_rows) / out[i].size().h; + hostParams.tops[i] = top; + hostParams.lefts[i] = left; } - float *scale_y_gpu = (float *)(m_gpuCropParams); - float *scale_x_gpu = (float *)((char *)scale_y_gpu + sizeof(float) * batch); - int *tops_gpu = (int *)((char *)scale_x_gpu + sizeof(float) * batch); - int *lefts_gpu = (int *)((char *)tops_gpu + sizeof(int) * batch); + CropParamBuffers deviceParams = deviceCropParams(batch); size_t buffer_size = calBufferSize(batch); checkCudaErrors( - cudaMemcpyAsync((void *)m_gpuCropParams, (void *)m_cpuCropParams, buffer_size, cudaMemcpyHostToDevice, stream)); + cudaMemcpyAsync(deviceCropParamStorage(), hostCropParamStorage(), buffer_size, cudaMemcpyHostToDevice, stream)); typedef void (*func_t)(const ImageBatchVarShapeDataStridedCuda &in, const ImageBatchVarShapeDataStridedCuda &out, const NVCVInterpolationType interpolation, cudaStream_t stream, float *scale_y, @@ -408,8 +710,34 @@ ErrorCode RandomResizedCropVarShape::infer(const ImageBatchVarShape &in, const I { resize, 0 /*resize*/, resize, resize} }; + if (isPlanar) + { + if (static_cast(batch) > 65535) + { + LOG_ERROR("Planar random resized crop requires numImages <= 65535 (CUDA grid-z limit)"); + return ErrorCode::INVALID_DATA_SHAPE; + } + typedef void (*planar_func_t)(const ImageBatchVarShapeDataStridedCuda &in, + const ImageBatchVarShapeDataStridedCuda &out, + const NVCVInterpolationType interpolation, cudaStream_t stream, float *scale_y, + float *scale_x, int *top, int *left, int channels); + + static const planar_func_t planar_funcs[6] = { + resize_planar, 0 /*resize_planar*/, resize_planar, + resize_planar, 0 /*resize_planar*/, resize_planar, + }; + + const planar_func_t planar_func = planar_funcs[in_data_type]; + NVCV_ASSERT(planar_func != 0); + planar_func(inData, outData, interpolation, stream, deviceParams.scaleY, deviceParams.scaleX, deviceParams.tops, + deviceParams.lefts, channels); + return SUCCESS; + } + const func_t func = funcs[in_data_type][channels - 1]; - func(inData, outData, interpolation, stream, scale_y_gpu, scale_x_gpu, tops_gpu, lefts_gpu); + NVCV_ASSERT(func != 0); + func(inData, outData, interpolation, stream, deviceParams.scaleY, deviceParams.scaleX, deviceParams.tops, + deviceParams.lefts); return SUCCESS; } diff --git a/src/cvcuda/priv/legacy/reformat.cu b/src/cvcuda/priv/legacy/reformat.cu index 2b467c45f..4c9daca88 100644 --- a/src/cvcuda/priv/legacy/reformat.cu +++ b/src/cvcuda/priv/legacy/reformat.cu @@ -20,6 +20,7 @@ #include "CvCudaLegacy.h" #include "CvCudaLegacyHelpers.hpp" +#include "ReformatCopyPolicy.hpp" #include "CvCudaUtils.cuh" @@ -27,45 +28,57 @@ #include #include +#include +#include namespace cuda = nvcv::cuda; namespace cuda_op = nvcv::legacy::cuda_op; -template +template __global__ void transformFormat(const SrcWrapper src, DstWrapper dst, int3 inout_size) { - int3 thrCoord = cuda::StaticCast(blockIdx * blockDim + threadIdx); + const int x = blockIdx.x * blockDim.x + threadIdx.x; + const int firstY = blockIdx.y * blockDim.y * RowsPerThread + threadIdx.y; - if (thrCoord.x >= inout_size.x || thrCoord.y >= inout_size.y) + if (x >= inout_size.x) return; using DimType = cuda::MakeType; DimType srcCoord, dstCoord; - for (int c = 0; c < inout_size.z; c++) +#pragma unroll + for (int row = 0; row < RowsPerThread; ++row) { - if constexpr (SrcFormat == cuda_op::kNCHW) - { - srcCoord = {thrCoord.x, thrCoord.y, c, thrCoord.z}; - dstCoord = {c, thrCoord.x, thrCoord.y, thrCoord.z}; - } - else if constexpr (SrcFormat == cuda_op::kNHWC) - { - srcCoord = {c, thrCoord.x, thrCoord.y, thrCoord.z}; - dstCoord = {thrCoord.x, thrCoord.y, c, thrCoord.z}; - } - else if constexpr (SrcFormat == cuda_op::kCHW) - { - srcCoord = {thrCoord.x, thrCoord.y, c}; - dstCoord = {c, thrCoord.x, thrCoord.y}; - } - else if constexpr (SrcFormat == cuda_op::kHWC) + const int y = firstY + row * blockDim.y; + if (y >= inout_size.y) + continue; + +#pragma unroll 4 + for (int c = 0; c < inout_size.z; c++) { - srcCoord = {c, thrCoord.x, thrCoord.y}; - dstCoord = {thrCoord.x, thrCoord.y, c}; - } + if constexpr (SrcFormat == cuda_op::kNCHW) + { + srcCoord = {x, y, c, static_cast(blockIdx.z)}; + dstCoord = {c, x, y, static_cast(blockIdx.z)}; + } + else if constexpr (SrcFormat == cuda_op::kNHWC) + { + srcCoord = {c, x, y, static_cast(blockIdx.z)}; + dstCoord = {x, y, c, static_cast(blockIdx.z)}; + } + else if constexpr (SrcFormat == cuda_op::kCHW) + { + srcCoord = {x, y, c}; + dstCoord = {c, x, y}; + } + else if constexpr (SrcFormat == cuda_op::kHWC) + { + srcCoord = {c, x, y}; + dstCoord = {x, y, c}; + } - dst[dstCoord] = src[srcCoord]; + dst[dstCoord] = src[srcCoord]; + } } } @@ -83,8 +96,11 @@ ErrorCode transform(const nvcv::TensorDataStridedCuda &inData, const nvcv::Tenso const int3 inout_size = {inAccess->numCols(), inAccess->numRows(), outAccess->numChannels()}; - dim3 block(32, 8); - dim3 grid(cuda_op::divUp(inout_size.x, block.x), cuda_op::divUp(inout_size.y, block.y), inAccess->numSamples()); + constexpr bool kUseWideBlock = std::is_same_v; + constexpr int kRowsPerThread = kUseWideBlock ? 8 : 1; + dim3 block = kUseWideBlock ? dim3{128, 2} : dim3{32, 8}; + dim3 grid(cuda_op::divUp(inout_size.x, block.x), cuda_op::divUp(inout_size.y, block.y * kRowsPerThread), + inAccess->numSamples()); auto inMaxStride = inAccess->sampleStride() * inAccess->numSamples(); auto outMaxStride = outAccess->sampleStride() * outAccess->numSamples(); @@ -93,7 +109,7 @@ ErrorCode transform(const nvcv::TensorDataStridedCuda &inData, const nvcv::Tenso cuda::TensorNDWrap, int32_t> src(inData); cuda::TensorNDWrap, int32_t> dst(outData); - transformFormat<<>>(src, dst, inout_size); + transformFormat<<>>(src, dst, inout_size); } else { @@ -130,6 +146,13 @@ ErrorCode Reformat::infer(const nvcv::TensorDataStridedCuda &inData, const nvcv: auto outAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(outData); NVCV_ASSERT(outAccess); + if (inAccess->numSamples() != outAccess->numSamples() || inAccess->numChannels() != outAccess->numChannels() + || inAccess->numRows() != outAccess->numRows() || inAccess->numCols() != outAccess->numCols()) + { + LOG_ERROR("Input and output logical image extents must match"); + return ErrorCode::INVALID_DATA_SHAPE; + } + if (inData.dtype() == outData.dtype() && inData.shape() == outData.shape()) { #ifdef CUDA_DEBUG_LOG @@ -178,6 +201,50 @@ ErrorCode Reformat::infer(const nvcv::TensorDataStridedCuda &inData, const nvcv: return ErrorCode::INVALID_DATA_TYPE; } + if (data_type == kCV_8U && inAccess->numChannels() == 1 && outAccess->numChannels() == 1 + && inAccess->numPlanes() == 1 && outAccess->numPlanes() == 1 && inAccess->numRows() == outAccess->numRows() + && inAccess->numCols() == outAccess->numCols() && inAccess->colStride() == outAccess->colStride()) + { + const auto rowBytes = inAccess->numCols() * inAccess->colStride(); + + if (inAccess->rowStride() == rowBytes && outAccess->rowStride() == rowBytes + && inAccess->sampleStride() == rowBytes * inAccess->numRows() + && outAccess->sampleStride() == rowBytes * outAccess->numRows()) + { + if (inAccess->numSamples() > 0) + { + const auto totalBytes = static_cast(rowBytes) * static_cast(inAccess->numRows()) + * static_cast(inAccess->numSamples()); + checkCudaErrors(cudaMemcpyAsync(outAccess->sampleData(0), inAccess->sampleData(0), totalBytes, + cudaMemcpyDeviceToDevice, stream)); + } + return SUCCESS; + } + + // A pitched copy needs one CUDA operation per sample. Below the measured + // crossover, that setup cost is higher than one reformat kernel launch. + const size_t pixelsPerSample + = static_cast(inAccess->numRows()) * static_cast(inAccess->numCols()); + const uint32_t numSamples = inAccess->numSamples(); + const bool usePitchedCopy + = detail::ShouldUsePitchedCopy(numSamples, pixelsPerSample, static_cast(rowBytes)); + + if (usePitchedCopy) + { + for (uint32_t i = 0; i < numSamples; ++i) + { + nvcv::Byte *inSampData = inAccess->sampleData(i); + nvcv::Byte *outSampData = outAccess->sampleData(i); + + checkCudaErrors(cudaMemcpy2DAsync( + outAccess->planeData(0, outSampData), outAccess->rowStride(), inAccess->planeData(0, inSampData), + inAccess->rowStride(), static_cast(rowBytes), static_cast(inAccess->numRows()), + cudaMemcpyDeviceToDevice, stream)); + } + return SUCCESS; + } + } + typedef ErrorCode (*transform_t)(const TensorDataStridedCuda &input, const TensorDataStridedCuda &output, cudaStream_t stream); diff --git a/src/cvcuda/priv/legacy/resize_var_shape.cu b/src/cvcuda/priv/legacy/resize_var_shape.cu index e7342bb16..ab215456e 100644 --- a/src/cvcuda/priv/legacy/resize_var_shape.cu +++ b/src/cvcuda/priv/legacy/resize_var_shape.cu @@ -1,4 +1,4 @@ -/* Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +/* Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * * SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES * SPDX-License-Identifier: Apache-2.0 @@ -88,161 +88,515 @@ inline __device__ T *_cacheAlignedBufferedReadVS(cuda::ImageBatchVarShapeWrap +using RVS_DPT = std::conditional_t == 3, uint3, uint4>; + +template +constexpr int RVS_NIX = sizeof(RVS_DPT) / sizeof(T); + +template +constexpr unsigned int RVS_MSK = (sizeof(RVS_DPT) == sizeof(uint3) ? sizeof(unsigned int) : sizeof(RVS_DPT)) - 1; + +template +__device__ __forceinline__ void RVSWritePack(T &u, const T (&v)[RVS_NIX]) +{ + reinterpret_cast &>(u) = reinterpret_cast &>(v); +} template +__device__ __forceinline__ bool RVSCheckRowAlign(T *row) +{ + return (static_cast(reinterpret_cast(row)) & RVS_MSK) == 0; +} + +// The interleaved (one plane, channels baked into T) and planar (one scalar plane per channel) +// var-shape paths share identical per-pixel math; the only difference is which (image, plane) a +// thread maps to. Each interpolation has its work factored into a plane-aware __device__ body that +// both kernels call: the interleaved kernel passes plane 0 and the planar kernel decodes the plane +// from grid-z. With __forceinline__ and ptr(s, 0, y, x) == ptr(s, y, x), the interleaved codegen is +// unchanged. + +//******************** NN = Nearest Neighbor + +// Interleaved var-shape nearest-neighbour. Each thread owns NN_NIX grid-strided output columns of +// one destination row and hoists the row-invariant terms -- the source scales (two FP divisions), +// the source row index sy, and the source row pointer -- computing them once and reusing them across +// the columns; the original kernel recomputed all of that per output pixel. Columns are grid-strided +// by blockDim.x so each store step stays coalesced. The per-pixel index math is byte-identical, so +// the result is bit-exact. The shared planar body (resize_NN_planar) is unaffected. +template __global__ void resize_NN(cuda::ImageBatchVarShapeWrap src, cuda::ImageBatchVarShapeWrap dst) { - const int dst_x = blockIdx.x * blockDim.x + threadIdx.x; - const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; const int batch_idx = get_batch_idx(); const int dstWidth = dst.width(batch_idx); const int dstHeight = dst.height(batch_idx); + const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; - if ((dst_x < dstWidth) && (dst_y < dstHeight)) - { //generic copy pixel to pixel - const int width = src.width(batch_idx); - const int height = src.height(batch_idx); + if (dst_y >= dstHeight) + return; - const float scale_x = static_cast(width) / dstWidth; - const float scale_y = static_cast(height) / dstHeight; - const int sx = cuda::min(__float2int_rd((dst_x + 0.5f) * scale_x), width - 1); - const int sy = cuda::min(__float2int_rd((dst_y + 0.5f) * scale_y), height - 1); + const int width = src.width(batch_idx); + const int height = src.height(batch_idx); + + const float scale_x = static_cast(width) / dstWidth; + const float scale_y = static_cast(height) / dstHeight; + + // Source row is row-invariant: compute sy and the row pointer once. + const int sy = cuda::min(__float2int_rd((dst_y + 0.5f) * scale_y), height - 1); + const T *srcRow = src.ptr(batch_idx, 0, sy, 0); + + const int xBase = blockIdx.x * blockDim.x * NN_NIX + threadIdx.x; + +#pragma unroll + for (int i = 0; i < NN_NIX; ++i) + { + const int dst_x = xBase + i * static_cast(blockDim.x); + if (dst_x >= dstWidth) + continue; - *dst.ptr(batch_idx, dst_y, dst_x) = *src.ptr(batch_idx, sy, sx); + const int sx = cuda::min(__float2int_rd((dst_x + 0.5f) * scale_x), width - 1); + *dst.ptr(batch_idx, 0, dst_y, dst_x) = srcRow[sx]; } } //resize_NN //******************** Bilinear -template +// Interleaved var-shape bilinear. Each thread owns BILINEAR_NIX output columns of one destination +// row and hoists the row-invariant terms -- the source scales (two FP divisions), the y +// coordinate/weight, and the two source row pointers -- computing them once and reusing them across +// the columns. The original kernel ran one thread per output pixel and recomputed all of that per +// pixel, leaving it co-limited by L1/TEX and compute (~80% each, two divisions per pixel). Columns +// are grid-strided by blockDim.x so each store step stays coalesced. The per-pixel arithmetic is +// byte-identical to the scalar kernel, so the result is bit-exact. (The planar bilinear kernel has +// its own channel-amortized body, resize_bilinear_planar, and is unaffected.) +template __global__ void resize_bilinear(cuda::ImageBatchVarShapeWrap src, cuda::ImageBatchVarShapeWrap dst) { - const int dst_x = blockIdx.x * blockDim.x + threadIdx.x; + const int batch_idx = get_batch_idx(); + const int dstWidth = dst.width(batch_idx); + const int dstHeight = dst.height(batch_idx); const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + + if (dst_y >= dstHeight) + return; + + const int width = src.width(batch_idx); + const int height = src.height(batch_idx); + + const float scale_x = static_cast(width) / dstWidth; + const float scale_y = static_cast(height) / dstHeight; + + // y coordinate + weight: row-invariant, computed once and reused across the thread's columns. + float fy = (float)((dst_y + 0.5f) * scale_y - 0.5f); + int sy = cuda::round(fy); + + fy = ((sy < 0) ? 0 : ((sy > height - 2) ? 1 : fy - sy)); + sy = cuda::max(0, cuda::min(sy, height - 2)); + + // Source row pointers: also row-invariant, hoisted out of the column loop. + const T *aPtr = src.ptr(batch_idx, 0, sy, 0); // upper row + const T *bPtr = src.ptr(batch_idx, 0, sy + 1, 0); // lower row + + const int xBase = blockIdx.x * blockDim.x * BILINEAR_NIX + threadIdx.x; + +#pragma unroll + for (int i = 0; i < BILINEAR_NIX; ++i) + { + const int dst_x = xBase + i * static_cast(blockDim.x); + if (dst_x >= dstWidth) + continue; + + float fx = (float)((dst_x + 0.5f) * scale_x - 0.5f); + int sx = cuda::round(fx); + + fx = ((sx < 0) ? 0 : ((sx > width - 2) ? 1 : fx - sx)); + sx = cuda::max(0, cuda::min(sx, width - 2)); + + *dst.ptr(batch_idx, 0, dst_y, dst_x) + = cuda::SaturateCast((1.0f - fx) * (aPtr[sx] * (1.0f - fy) + bPtr[sx] * fy) + + fx * (aPtr[sx + 1] * (1.0f - fy) + bPtr[sx + 1] * fy)); + } +} //resize_bilinear + +// Interleaved var-shape bilinear for batches where every image is an exact-2x upscale, mirroring +// the tensor path's LinearResizeExpand2x: the weights are exactly 0.25f/0.75f per axis (the same +// fx/fy resize_bilinear derives), so each thread owns RVS_NIX output columns x 2 output rows, +// stages the shared clamped 3-row source window in registers, and writes two vector packs instead +// of RVS_NIX*2 single-byte stores (the general kernel is issue/L1-bound on those: SM 85%, IPC 3.0, +// DRAM 30% for U8). Border outputs replicate resize_bilinear's weight overrides (w=0/w=1), which +// zero exactly the taps whose window values differ from clamped-coordinate loads, and the blend +// uses resize_bilinear's exact expression tree, so the result is bit-exact. Dispatched only when +// the host verifies every image pair in the batch is exactly 2x on both axes. +template +__global__ void resize_bilinear_expand2x(cuda::ImageBatchVarShapeWrap src, cuda::ImageBatchVarShapeWrap dst) +{ + constexpr int KC = RVS_NIX / 2; // source columns owned per thread + constexpr int WC = KC + 2; // staged window columns (taps k0-1 .. k0+KC) + const int batch_idx = get_batch_idx(); const int dstWidth = dst.width(batch_idx); const int dstHeight = dst.height(batch_idx); - if ((dst_x < dstWidth) && (dst_y < dstHeight)) + const int k0 = (blockIdx.x * blockDim.x + threadIdx.x) * KC; // first owned source column + const int j = blockIdx.y * blockDim.y + threadIdx.y; // owned source row + const int dst_x0 = k0 * 2; + const int dst_y0 = j * 2; + + if (dst_x0 >= dstWidth || dst_y0 >= dstHeight) + return; + + const int width = src.width(batch_idx); + const int height = src.height(batch_idx); + + // Clamped source window shared by all 2*RVS_NIX outputs. + T win[3][WC]; +#pragma unroll + for (int r = 0; r < 3; ++r) { - const int width = src.width(batch_idx); - const int height = src.height(batch_idx); + const T *srcRow = src.ptr(batch_idx, cuda::clamp(j - 1 + r, 0, height - 1), 0); +#pragma unroll + for (int c = 0; c < WC; ++c) + { + win[r][c] = srcRow[cuda::clamp(k0 - 1 + c, 0, width - 1)]; + } + } - const float scale_x = static_cast(width) / dstWidth; - const float scale_y = static_cast(height) / dstHeight; + // Even output row 2j: sy = j-1, fy = 0.75 (window rows 0,1); odd row 2j+1: sy = j, fy = 0.25 + // (rows 1,2) -- with resize_bilinear's border overrides. + const float fyE = (j - 1 < 0) ? 0.f : 0.75f; // j-1 > height-2 cannot happen (j < height) + const float fyO = (j > height - 2) ? 1.f : 0.25f; - //float space for weighted addition - using work_type = cuda::ConvertBaseTypeTo; + T out0[RVS_NIX], out1[RVS_NIX]; +#pragma unroll + for (int i = 0; i < RVS_NIX; ++i) + { + const bool odd = (i & 1) != 0; + const int sx = odd ? (k0 + i / 2) : (k0 + i / 2 - 1); + const float fx = (sx < 0) ? 0.f : ((sx > width - 2) ? 1.f : (odd ? 0.25f : 0.75f)); + const int cb = (i / 2) + (odd ? 1 : 0); // window column of the first x tap + + out0[i] = cuda::SaturateCast((1.0f - fx) * (win[0][cb] * (1.0f - fyE) + win[1][cb] * fyE) + + fx * (win[0][cb + 1] * (1.0f - fyE) + win[1][cb + 1] * fyE)); + out1[i] = cuda::SaturateCast((1.0f - fx) * (win[1][cb] * (1.0f - fyO) + win[2][cb] * fyO) + + fx * (win[1][cb + 1] * (1.0f - fyO) + win[2][cb + 1] * fyO)); + } - //y coordinate - float fy = (float)((dst_y + 0.5f) * scale_y - 0.5f); - int sy = cuda::round(fy); + const int validCount = cuda::min(RVS_NIX, dstWidth - dst_x0); + // dstHeight = 2*height is even, so the odd output row always exists. +#pragma unroll + for (int p = 0; p < 2; ++p) + { + T *dstRow = dst.ptr(batch_idx, dst_y0 + p, 0); + const T(&outp)[RVS_NIX] = p == 0 ? out0 : out1; - fy = ((sy < 0) ? 0 : ((sy > height - 2) ? 1 : fy - sy)); - sy = cuda::max(0, cuda::min(sy, height - 2)); + if (validCount == RVS_NIX && RVSCheckRowAlign(dstRow + dst_x0)) // uniform across the warp + RVSWritePack(dstRow[dst_x0], outp); + else + { + T *dstPtr = dstRow + dst_x0; +#pragma unroll + for (int c = 0; c < RVS_NIX; ++c) + if (c < validCount) + dstPtr[c] = outp[c]; + } + } +} - //row pointers - const T *aPtr = src.ptr(batch_idx, sy, 0); //start of upper row - const T *bPtr = src.ptr(batch_idx, sy + 1, 0); //start of lower row +// Interleaved var-shape bilinear for byte types with RVS_NIX consecutive output columns per +// thread: upscales revisit the same source cell for several adjacent outputs, so the four taps are +// cached as work-type floats (float(byte) conversion is exact) and refreshed only when the source +// column advances -- the grid-strided general kernel re-reads and re-converts them per output. The +// consecutive columns also allow one vector-pack store per row segment instead of RVS_NIX scalar +// stores. Coordinate math, border overrides, and the blend expression tree are identical to +// resize_bilinear, so the result is bit-exact at every scale; downscales simply never hit the +// cache-reuse fast case. +template +__global__ void resize_bilinear_pack(cuda::ImageBatchVarShapeWrap src, cuda::ImageBatchVarShapeWrap dst) +{ + using FT = cuda::ConvertBaseTypeTo; - { //compute source data position and weight for [x0] components - float fx = (float)((dst_x + 0.5f) * scale_x - 0.5f); - int sx = cuda::round(fx); + const int batch_idx = get_batch_idx(); + const int dstWidth = dst.width(batch_idx); + const int dstHeight = dst.height(batch_idx); + const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int x0 = (blockIdx.x * blockDim.x + threadIdx.x) * RVS_NIX; - fx = ((sx < 0) ? 0 : ((sx > width - 2) ? 1 : fx - sx)); - sx = cuda::max(0, cuda::min(sx, width - 2)); + if (dst_y >= dstHeight || x0 >= dstWidth) + return; - *dst.ptr(batch_idx, dst_y, dst_x) - = cuda::SaturateCast((1.0f - fx) * (aPtr[sx] * (1.0f - fy) + bPtr[sx] * fy) - + fx * (aPtr[sx + 1] * (1.0f - fy) + bPtr[sx + 1] * fy)); + const int width = src.width(batch_idx); + const int height = src.height(batch_idx); + + const float scale_x = static_cast(width) / dstWidth; + const float scale_y = static_cast(height) / dstHeight; + + float fy = (float)((dst_y + 0.5f) * scale_y - 0.5f); + int sy = cuda::round(fy); + + fy = ((sy < 0) ? 0 : ((sy > height - 2) ? 1 : fy - sy)); + sy = cuda::max(0, cuda::min(sy, height - 2)); + + const T *aPtr = src.ptr(batch_idx, 0, sy, 0); // upper row + const T *bPtr = src.ptr(batch_idx, 0, sy + 1, 0); // lower row + + // Cached float taps of the current source cell [prevSx, prevSx+1]. + FT a0{}, a1{}, b0{}, b1{}; + int prevSx = -2; // never matches or adjoins a valid first column + + T out[RVS_NIX]; +#pragma unroll + for (int i = 0; i < RVS_NIX; ++i) + { + const int dst_x = x0 + i; + + float fx = (float)((dst_x + 0.5f) * scale_x - 0.5f); + int sx = cuda::round(fx); + + fx = ((sx < 0) ? 0 : ((sx > width - 2) ? 1 : fx - sx)); + sx = cuda::max(0, cuda::min(sx, width - 2)); + + if (sx == prevSx + 1) + { + a0 = a1; + b0 = b1; + a1 = cuda::StaticCast(aPtr[sx + 1]); + b1 = cuda::StaticCast(bPtr[sx + 1]); + } + else if (sx != prevSx) + { + a0 = cuda::StaticCast(aPtr[sx]); + a1 = cuda::StaticCast(aPtr[sx + 1]); + b0 = cuda::StaticCast(bPtr[sx]); + b1 = cuda::StaticCast(bPtr[sx + 1]); } + prevSx = sx; + + out[i] = cuda::SaturateCast((1.0f - fx) * (a0 * (1.0f - fy) + b0 * fy) + fx * (a1 * (1.0f - fy) + b1 * fy)); } -} //resize_bilinear + + T *dstRow = dst.ptr(batch_idx, 0, dst_y, 0); + const int validCount = cuda::min(RVS_NIX, dstWidth - x0); + + if (validCount == RVS_NIX && RVSCheckRowAlign(dstRow + x0)) + RVSWritePack(dstRow[x0], out); + else + { + T *dstPtr = dstRow + x0; +#pragma unroll + for (int c = 0; c < RVS_NIX; ++c) + if (c < validCount) + dstPtr[c] = out[c]; + } +} //******************** Bicubic -template +// Interleaved var-shape bicubic. Each thread owns BICUBIC_NIX grid-strided output columns of one +// destination row and hoists the row-invariant terms -- the source scales (two FP divisions), the y +// coordinate/fraction, the four y cubic coefficients cY[4], and the four clamped source-row pointers +// -- computing them once and reusing them across the columns; the original kernel recomputed all of +// that per output pixel (16 taps each). Columns are grid-strided by blockDim.x so each store step +// stays coalesced. The per-pixel arithmetic and accumulation order are byte-identical to the scalar +// kernel, so the result is bit-exact. The shared planar body (resize_bicubic_planar) is unaffected. +template __global__ void resize_bicubic(cuda::ImageBatchVarShapeWrap src, cuda::ImageBatchVarShapeWrap dst) -{ //optimized for aligned read - const int dst_x = blockIdx.x * blockDim.x + threadIdx.x; - const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; +{ const int batch_idx = get_batch_idx(); const int dstWidth = dst.width(batch_idx); const int dstHeight = dst.height(batch_idx); + const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; - if ((dst_x < dstWidth) & (dst_y < dstHeight)) - { - const int width = src.width(batch_idx); - const int height = src.height(batch_idx); + if (dst_y >= dstHeight) + return; - const float scale_x = static_cast(width) / dstWidth; - const float scale_y = static_cast(height) / dstHeight; + const int width = src.width(batch_idx); + const int height = src.height(batch_idx); - //float space for weighted addition - using work_type = cuda::ConvertBaseTypeTo; + const float scale_x = static_cast(width) / dstWidth; + const float scale_y = static_cast(height) / dstHeight; - uint readBuffer[MAX_BUFFER_WORDS_VS]; + using work_type = cuda::ConvertBaseTypeTo; - //y coordinate - float fy = (float)((dst_y + 0.5f) * scale_y - 0.5f); - int sy = cuda::round(fy); - fy -= sy; - sy = cuda::max(1, cuda::min(sy, height - 3)); + const float A = -0.75f; - const float A = -0.75f; + // y coordinate, y cubic coefficients, and the four clamped source-row pointers are all + // row-invariant: compute them once and reuse across this thread's columns. + float fy = (float)((dst_y + 0.5f) * scale_y - 0.5f); + int sy = cuda::round(fy); + fy -= sy; - float cY[4]; - cY[0] = ((A * (fy + 1) - 5 * A) * (fy + 1) + 8 * A) * (fy + 1) - 4 * A; - cY[1] = ((A + 2) * fy - (A + 3)) * fy * fy + 1; - cY[2] = ((A + 2) * (1 - fy) - (A + 3)) * (1 - fy) * (1 - fy) + 1; - cY[3] = 1.f - cY[0] - cY[1] - cY[2]; + float cY[4]; + cY[0] = ((A * (fy + 1) - 5 * A) * (fy + 1) + 8 * A) * (fy + 1) - 4 * A; + cY[1] = ((A + 2) * fy - (A + 3)) * fy * fy + 1; + cY[2] = ((A + 2) * (1 - fy) - (A + 3)) * (1 - fy) * (1 - fy) + 1; + cY[3] = 1.f - cY[0] - cY[1] - cY[2]; - work_type accum = cuda::SetAll(0); + const T *rowPtr[4]; +#pragma unroll + for (int row = 0; row < 4; ++row) rowPtr[row] = src.ptr(batch_idx, 0, cuda::clamp(sy + row - 1, 0, height - 1), 0); + + const int xBase = blockIdx.x * blockDim.x * BICUBIC_NIX + threadIdx.x; + +#pragma unroll + for (int i = 0; i < BICUBIC_NIX; ++i) + { + const int dst_x = xBase + i * static_cast(blockDim.x); + if (dst_x >= dstWidth) + continue; float fx = (float)((dst_x + 0.5f) * scale_x - 0.5f); int sx = cuda::round(fx); fx -= sx; - fx *= ((sx >= 1) && (sx < width - 3)); - sx = cuda::max(1, cuda::min(sx, width - 3)); + // Cubic polynomial coefficients -- coordinate clamping is deferred to the sampling loop. float cX[4]; cX[0] = ((A * (fx + 1.0f) - 5.0f * A) * (fx + 1.0f) + 8.0f * A) * (fx + 1.0f) - 4.0f * A; cX[1] = ((A + 2.0f) * fx - (A + 3.0f)) * fx * fx + 1.0f; cX[2] = ((A + 2.0f) * (1.0f - fx) - (A + 3.0f)) * (1.0f - fx) * (1.0f - fx) + 1.0f; cX[3] = 1.0f - cX[0] - cX[1] - cX[2]; + + work_type accum = cuda::SetAll(0); + + // Clamp each source tap coordinate independently to [0, size-1] (replicate border). #pragma unroll for (int row = 0; row < 4; ++row) { - //1 - load each sub row from sx-1 to sx+3 inclusive, aligned - //const T * aPtr = src.ptr(batch_idx, sy + row - 1, sx-1); - T *aPtr = _cacheAlignedBufferedReadVS( - src, width, readBuffer, MAX_BUFFER_WORDS_VS, batch_idx, sy + row - 1, sx - 1, sx + 2); - - //2 - do a pixel's partial on this row - accum += cY[row] * (cX[0] * aPtr[0] + cX[1] * aPtr[1] + cX[2] * aPtr[2] + cX[3] * aPtr[3]); +#pragma unroll + for (int col = 0; col < 4; ++col) + { + int csx = cuda::clamp(sx + col - 1, 0, width - 1); + accum += cY[row] * cX[col] * rowPtr[row][csx]; + } } //for row -#ifndef LEGACY_BICUBIC_MATH_VS - //correct math - *dst.ptr(batch_idx, dst_y, dst_x) = cuda::SaturateCast(accum); -#else - //abs() needed to match legacy operator. - *dst.ptr(batch_idx, dst_y, dst_x) = cuda::SaturateCast(cuda::abs(accum)); -#endif + *dst.ptr(batch_idx, 0, dst_y, dst_x) = cuda::SaturateCast(accum); } } //resize_bicubic +// Shared-memory-tiled var-shape bicubic for FLOAT elements. Per block (one image): if the image +// upscales (both scales < 1) the block's output tile maps to a SMALL source tile, so it is staged +// once in shared memory (replicate-border clamp baked in at load) and the 16 taps/pixel are served +// from smem instead of the L1/TEX-bound gather; CONTRACT/same-size blocks fall back to the direct +// gather (identical to resize_bicubic). Because every upscale block has scale < 1, the tile never +// exceeds blockDim + cubic support, so a single launch-time tile size (blockDim + 6) is valid for +// any per-image scale in the batch. Bit-exact with resize_bicubic: tile[tap-origin] == src[clamp(tap)] +// and the coefficient formula/accumulation order are identical. +template +__global__ void resize_bicubic_smem(cuda::ImageBatchVarShapeWrap src, cuda::ImageBatchVarShapeWrap dst, + int tileW, int tileH) +{ + extern __shared__ __align__(16) unsigned char smemRaw[]; + T *tile = reinterpret_cast(smemRaw); + + using work_type = cuda::ConvertBaseTypeTo; + const float A = -0.75f; + + const int b = get_batch_idx(); + const int width = src.width(b), height = src.height(b); + const int dstW = dst.width(b), dstH = dst.height(b); + const float scale_x = static_cast(width) / dstW; + const float scale_y = static_cast(height) / dstH; + + const int dst_x = blockIdx.x * blockDim.x + threadIdx.x; + const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + + // Uniform across the block (depends only on the image): keeps the __syncthreads() below uniform. + const bool expand = (scale_x < 1.f && scale_y < 1.f); + + auto coeffs = [&](float f, float c[4]) + { + c[0] = ((A * (f + 1) - 5 * A) * (f + 1) + 8 * A) * (f + 1) - 4 * A; + c[1] = ((A + 2) * f - (A + 3)) * f * f + 1; + c[2] = ((A + 2) * (1 - f) - (A + 3)) * (1 - f) * (1 - f) + 1; + c[3] = 1.f - c[0] - c[1] - c[2]; + }; + + if (expand) + { + const int ox0 = blockIdx.x * blockDim.x; + const int oy0 = blockIdx.y * blockDim.y; + const int tx0 = cuda::round((ox0 + 0.5f) * scale_x - 0.5f) - 1; + const int ty0 = cuda::round((oy0 + 0.5f) * scale_y - 0.5f) - 1; + + const int nThreads = blockDim.x * blockDim.y; + for (int idx = threadIdx.y * blockDim.x + threadIdx.x; idx < tileW * tileH; idx += nThreads) + { + const int sx = cuda::clamp(tx0 + idx % tileW, 0, width - 1); + const int sy = cuda::clamp(ty0 + idx / tileW, 0, height - 1); + tile[idx] = *src.ptr(b, 0, sy, sx); + } + __syncthreads(); + + if (dst_x >= dstW || dst_y >= dstH) + return; + + float fx = (dst_x + 0.5f) * scale_x - 0.5f; + int sx = cuda::round(fx); + fx -= sx; + float fy = (dst_y + 0.5f) * scale_y - 0.5f; + int sy = cuda::round(fy); + fy -= sy; + + float cX[4], cY[4]; + coeffs(fx, cX); + coeffs(fy, cY); + + work_type accum = cuda::SetAll(0); +#pragma unroll + for (int row = 0; row < 4; ++row) + { + const int jj = (sy + row - 1) - ty0; +#pragma unroll + for (int col = 0; col < 4; ++col) + { + const int ii = (sx + col - 1) - tx0; + accum += cY[row] * cX[col] * tile[jj * tileW + ii]; + } + } + *dst.ptr(b, 0, dst_y, dst_x) = cuda::SaturateCast(accum); + } + else + { + if (dst_x >= dstW || dst_y >= dstH) + return; + + float fx = (dst_x + 0.5f) * scale_x - 0.5f; + int sx = cuda::round(fx); + fx -= sx; + float fy = (dst_y + 0.5f) * scale_y - 0.5f; + int sy = cuda::round(fy); + fy -= sy; + + float cX[4], cY[4]; + coeffs(fx, cX); + coeffs(fy, cY); + + const T *rowPtr[4]; +#pragma unroll + for (int row = 0; row < 4; ++row) rowPtr[row] = src.ptr(b, 0, cuda::clamp(sy + row - 1, 0, height - 1), 0); + + work_type accum = cuda::SetAll(0); +#pragma unroll + for (int row = 0; row < 4; ++row) +#pragma unroll + for (int col = 0; col < 4; ++col) + { + const int csx = cuda::clamp(sx + col - 1, 0, width - 1); + accum += cY[row] * cX[col] * rowPtr[row][csx]; + } + *dst.ptr(b, 0, dst_y, dst_x) = cuda::SaturateCast(accum); + } +} + //******************** Integrate area template -__global__ void resize_area_ocv_align(const cuda::ImageBatchVarShapeWrap src, - const cuda::BorderVarShapeWrap brd_src, - cuda::ImageBatchVarShapeWrap dst) +__device__ __forceinline__ void resizeAreaPlane(const cuda::ImageBatchVarShapeWrap src, + const cuda::BorderVarShapeWrap brd_src, + cuda::ImageBatchVarShapeWrap dst, int batch_idx, int plane, int x, + int y) { - const int x = blockDim.x * blockIdx.x + threadIdx.x; - const int y = blockDim.y * blockIdx.y + threadIdx.y; - const int batch_idx = get_batch_idx(); - int dstWidth = dst.width(batch_idx); int dstHeight = dst.height(batch_idx); @@ -253,11 +607,14 @@ __global__ void resize_area_ocv_align(const cuda::ImageBatchVarShapeWrap(width) / dstWidth; float scale_y = static_cast(height) / dstHeight; - double inv_scale_x = 1. / scale_x; - double inv_scale_y = 1. / scale_y; - int iscale_x = cuda::SaturateCast(scale_x); - int iscale_y = cuda::SaturateCast(scale_y); - bool is_area_fast = abs(scale_x - iscale_x) < DBL_EPSILON && abs(scale_y - iscale_y) < DBL_EPSILON; + // Coordinate-space inverse scales kept in FP32: they only feed back into the + // per-pixel float fy/fx computation below. FP64 here was throughput-pinning + // the kernel on consumer GPUs where FP64 runs at 1/64 of FP32. + float inv_scale_x = 1.f / scale_x; + float inv_scale_y = 1.f / scale_y; + int iscale_x = cuda::SaturateCast(scale_x); + int iscale_y = cuda::SaturateCast(scale_y); + bool is_area_fast = abs(scale_x - iscale_x) < DBL_EPSILON && abs(scale_y - iscale_y) < DBL_EPSILON; if (scale_x >= 1.0f && scale_y >= 1.0f) // zoom out { @@ -279,20 +636,19 @@ __global__ void resize_area_ocv_align(const cuda::ImageBatchVarShapeWrap; work_type out = {0}; - int3 srcCoord = {0, 0, batch_idx}; - + // Integer downscale: the box [sx1,sx2) x [sy1,sy2) is fully in-bounds, so read the source + // rows directly (pointer hoisted per row) instead of paying the per-tap border-wrap bounds + // check. Bit-exact: same values, same per-tap scale, same accumulation order as brd_src[]. for (int dy = sy1; dy < sy2; ++dy) { - srcCoord.y = dy; + const T *srcRow = src.ptr(batch_idx, plane, dy, 0); for (int dx = sx1; dx < sx2; ++dx) { - srcCoord.x = dx; - - out = out + brd_src[srcCoord] * scale; + out = out + srcRow[dx] * scale; } } - *dst.ptr(batch_idx, y, x) = cuda::SaturateCast(out); + *dst.ptr(batch_idx, plane, y, x) = cuda::SaturateCast(out); return; } @@ -314,17 +670,20 @@ __global__ void resize_area_ocv_align(const cuda::ImageBatchVarShapeWrap; work_type out = {0}; - int3 srcCoord = {0, 0, batch_idx}; + int4 srcCoord = {0, 0, plane, batch_idx}; for (int dy = sy1; dy < sy2; ++dy) { srcCoord.y = dy; + // Interior box columns [sx1,sx2) are in-bounds for a downscale, so read the row directly + // (pointer hoisted once) instead of the per-tap border-wrap. Fractional edge taps below may + // sit at sx1-1 / sx2 and keep the border-wrapped access. Bit-exact with the scalar path. + const T *srcRow = src.ptr(batch_idx, plane, dy, 0); + for (int dx = sx1; dx < sx2; ++dx) { - srcCoord.x = dx; - - out = out + brd_src[srcCoord] * scale; + out = out + srcRow[dx] * scale; } if (sx1 > fsx1) @@ -388,13 +747,13 @@ __global__ void resize_area_ocv_align(const cuda::ImageBatchVarShapeWrap(out); + *dst.ptr(batch_idx, plane, y, x) = cuda::SaturateCast(out); return; } // zoom in, it is emulated using some variant of bilinear interpolation int sy = cuda::round(y * scale_y); - float fy = (float)((y + 1) - (sy + 1) * inv_scale_y); + float fy = (y + 1) - (sy + 1) * inv_scale_y; fy = fy <= 0 ? 0.f : fy - cuda::round(fy); float cbufy[2]; @@ -402,7 +761,7 @@ __global__ void resize_area_ocv_align(const cuda::ImageBatchVarShapeWrap(x * scale_x); - float fx = (float)((x + 1) - (sx + 1) * inv_scale_x); + float fx = (x + 1) - (sx + 1) * inv_scale_x; fx = fx < 0 ? 0.f : fx - cuda::round(fx); if (sx < 0) @@ -423,51 +782,1005 @@ __global__ void resize_area_ocv_align(const cuda::ImageBatchVarShapeWrap((*src.ptr(batch_idx, sy, sx) * cbufx[0] * cbufy[0] - + *src.ptr(batch_idx, sy + 1, sx) * cbufx[0] * cbufy[1] - + *src.ptr(batch_idx, sy, sx + 1) * cbufx[1] * cbufy[0] - + *src.ptr(batch_idx, sy + 1, sx + 1) * cbufx[1] * cbufy[1])); + *dst.ptr(batch_idx, plane, y, x) + = cuda::SaturateCast((*src.ptr(batch_idx, plane, sy, sx) * cbufx[0] * cbufy[0] + + *src.ptr(batch_idx, plane, sy + 1, sx) * cbufx[0] * cbufy[1] + + *src.ptr(batch_idx, plane, sy, sx + 1) * cbufx[1] * cbufy[0] + + *src.ptr(batch_idx, plane, sy + 1, sx + 1) * cbufx[1] * cbufy[1])); } template -void resize(const ImageBatchVarShapeDataStridedCuda &in, const ImageBatchVarShapeDataStridedCuda &out, - const int interpolation, cudaStream_t stream) +__global__ void resize_area_ocv_align(const cuda::ImageBatchVarShapeWrap src, + const cuda::BorderVarShapeWrap brd_src, + cuda::ImageBatchVarShapeWrap dst) { - NVCV_ASSERT(in.numImages() == out.numImages()); - - cuda::ImageBatchVarShapeWrap src_ptr(in); - cuda::ImageBatchVarShapeWrap dst_ptr(out); + const int x = blockDim.x * blockIdx.x + threadIdx.x; + const int y = blockDim.y * blockIdx.y + threadIdx.y; + resizeAreaPlane(src, brd_src, dst, get_batch_idx(), 0, x, y); +} - Size2D outMaxSize = out.maxSize(); +// Exact-2x AREA downscale body shared by the interleaved and planar kernels: every output is the +// mean of a fully-in-bounds 2x2 source box (sx1 = 2x, sy1 = 2y, scale = 1/(2*2) = 0.25f -- the same +// values resizeAreaPlane's integer branch derives), so the per-pixel coordinate rounding and the +// runtime-bounded box loops collapse to four unrolled taps. Each thread emits RVS_NIX consecutive +// outputs of one row as a vector pack instead of single-element stores (the general kernel is +// issue-bound on the U8 rows: SM 71%, IPC 2.8, DRAM 40% local). Accumulation order matches the +// general integer branch exactly (row-major taps, per-tap * scale), so the result is bit-exact. +template +__device__ __forceinline__ void resizeAreaContract2xPlane(const cuda::ImageBatchVarShapeWrap src, + cuda::ImageBatchVarShapeWrap dst, int batch_idx, int plane, + int x0, int y, int dstWidth) +{ + using work_type = cuda::ConvertBaseTypeTo; - const int THREADS_PER_BLOCK = 256; //Performance degrades above 256 and below 16 (GMEM speed limited) - const int BLOCK_WIDTH = 8; //as in 32x4 or 32x8 or 8x32. + const T *row0 = src.ptr(batch_idx, plane, 2 * y, 0); + const T *row1 = src.ptr(batch_idx, plane, 2 * y + 1, 0); - const dim3 blockSize(BLOCK_WIDTH, THREADS_PER_BLOCK / BLOCK_WIDTH, 1); - const dim3 gridSize(divUp(outMaxSize.w, blockSize.x), divUp(outMaxSize.h, blockSize.y), in.numImages()); + const int sx0 = 2 * x0; + const int validCount = cuda::min(RVS_NIX, dstWidth - x0); - //quad permits aligned writes to output image, if image is multiple of 4. kernels in resize_varshape are smart - const int out_quad_width = outMaxSize.w / 4; - const dim3 quadGridSize(divUp(out_quad_width, blockSize.x), divUp(outMaxSize.h, blockSize.y), in.numImages()); + // Each thread consumes 2*RVS_NIX consecutive source elements per row; read them as two vector + // packs per row instead of 4*RVS_NIX single-element loads -- the scalar-load version of this + // body was L1/TEX-transaction-bound (99% L1 SOL, DRAM 70%, SM 29%). The staged values are + // identical, so the accumulation below is unchanged. + T win0[2 * RVS_NIX], win1[2 * RVS_NIX]; + if (validCount == RVS_NIX && RVSCheckRowAlign(row0 + sx0) && RVSCheckRowAlign(row1 + sx0)) + { +#pragma unroll + for (int p = 0; p < 2; ++p) + { + reinterpret_cast *>(win0)[p] = reinterpret_cast *>(row0 + sx0)[p]; + reinterpret_cast *>(win1)[p] = reinterpret_cast *>(row1 + sx0)[p]; + } + } + else + { + // Source width is exactly 2*dstWidth, so tail loads stay guarded per element. +#pragma unroll + for (int c = 0; c < 2 * RVS_NIX; ++c) + { + const int sx = sx0 + c; + win0[c] = (sx < 2 * dstWidth) ? row0[sx] : T{}; + win1[c] = (sx < 2 * dstWidth) ? row1[sx] : T{}; + } + } - switch (interpolation) + T out[RVS_NIX]; +#pragma unroll + for (int i = 0; i < RVS_NIX; ++i) + { + work_type acc = {0}; + acc = acc + win0[2 * i] * 0.25f; + acc = acc + win0[2 * i + 1] * 0.25f; + acc = acc + win1[2 * i] * 0.25f; + acc = acc + win1[2 * i + 1] * 0.25f; + out[i] = cuda::SaturateCast(acc); + } + + T *dstRow = dst.ptr(batch_idx, plane, y, 0); + + if (validCount == RVS_NIX && RVSCheckRowAlign(dstRow + x0)) + RVSWritePack(dstRow[x0], out); + else + { + T *dstPtr = dstRow + x0; +#pragma unroll + for (int c = 0; c < RVS_NIX; ++c) + if (c < validCount) + dstPtr[c] = out[c]; + } +} + +template +__global__ void resize_area_contract2x(cuda::ImageBatchVarShapeWrap src, cuda::ImageBatchVarShapeWrap dst) +{ + const int batch_idx = get_batch_idx(); + const int x0 = (blockDim.x * blockIdx.x + threadIdx.x) * RVS_NIX; + const int y = blockDim.y * blockIdx.y + threadIdx.y; + + if (x0 >= dst.width(batch_idx) || y >= dst.height(batch_idx)) + return; + + resizeAreaContract2xPlane(src, dst, batch_idx, 0, x0, y, dst.width(batch_idx)); +} + +template +__global__ void resize_area_contract2x_planar(cuda::ImageBatchVarShapeWrap src, + cuda::ImageBatchVarShapeWrap dst, int channels) +{ + const int batch_idx = get_batch_idx(); + const int x0 = (blockDim.x * blockIdx.x + threadIdx.x) * RVS_NIX; + const int y = blockDim.y * blockIdx.y + threadIdx.y; + + if (x0 >= dst.width(batch_idx) || y >= dst.height(batch_idx)) + return; + + for (int plane = 0; plane < channels; ++plane) + { + resizeAreaContract2xPlane(src, dst, batch_idx, plane, x0, y, dst.width(batch_idx)); + } +} + +// Interleaved var-shape fractional AREA downscale with RVS_NIX consecutive output columns per +// thread. For a zoom-out every box tap -- including the fractional edge/corner taps -- is in +// bounds (sx1-1 >= floor(fsx1) >= 0 and sx2 <= width-1, likewise in y), so the general body's +// per-tap border-wrap accesses are replaced by plain hoisted row-pointer reads (value-identical), +// the row-invariant y box terms are computed once per thread, and the RVS_NIX results leave as one +// vector pack instead of scalar stores (the general kernel is issue/L1-bound: SM 75%, IPC 2.8, +// L1 83%, DRAM 34% local). Per-output term order -- interior rows (cols ascending, then left, +// then right edge), top row, bottom row, then TL/TR/BR/BL corners, each * scale -- replicates +// resizeAreaPlane exactly, so the result is bit-exact. Integer ratios and zoom-ins keep their +// existing kernels. +template +__global__ void resize_area_fractional_pack(cuda::ImageBatchVarShapeWrap src, + cuda::ImageBatchVarShapeWrap dst) +{ + using work_type = cuda::ConvertBaseTypeTo; + + // Grouped independent accumulation chains (GN outputs at a time) with a compile-time interior + // column bound (dispatch guarantees x scale < 3, so at most kMaxC interior columns): the + // serial one-output walk was memory-latency bound. Per-output term order is unchanged + // (interior columns ascending, then left, then right edge per row; top row; bottom row; then + // TL/TR/BR/BL corners), so the result stays bit-exact with resizeAreaPlane. + // Three-channel elements lose to L1 pressure with four chains: GN = 4 regressed the uchar3 + // row +14% on A100 against its reference baseline (locally it was +5%); GN = 2 matches the + // tensor kernel's measured split. + constexpr int GN = (cuda::NumElements == 3) ? 2 : 4; + constexpr int kMaxC = 3; + + const int batch_idx = get_batch_idx(); + const int dstWidth = dst.width(batch_idx); + const int dstHeight = dst.height(batch_idx); + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const int x0 = (blockIdx.x * blockDim.x + threadIdx.x) * RVS_NIX; + + if (y >= dstHeight || x0 >= dstWidth) + return; + + const int width = src.width(batch_idx); + const int height = src.height(batch_idx); + + const float scale_x = static_cast(width) / dstWidth; + const float scale_y = static_cast(height) / dstHeight; + + const float fsy1 = y * scale_y; + const float fsy2 = fsy1 + scale_y; + const int sy1 = cuda::round(fsy1); + const int sy2 = cuda::round(fsy2); + + // Skip edge taps that float rounding pushes one past the source extent: the border-wrapped + // kernel reads the zero constant border there (contribution exactly +0.0), so skipping is + // bit-identical and keeps the direct reads in bounds. + const bool hasTop = (float)sy1 > fsy1; + const bool hasBot = (float)sy2 < fsy2 && sy2 < height; + const float wTop = (float)sy1 - fsy1; + const float wBot = fsy2 - (float)sy2; + + T out[RVS_NIX]; + +#pragma unroll + for (int g = 0; g < RVS_NIX; g += GN) + { + float fsx1[GN], fsx2[GN], scaleA[GN], wL[GN], wR[GN]; + int sx1[GN], sx2[GN], nC[GN]; + bool hasL[GN], hasR[GN]; +#pragma unroll + for (int j = 0; j < GN; ++j) + { + const int x = cuda::min(x0 + g + j, dstWidth - 1); + fsx1[j] = x * scale_x; + fsx2[j] = fsx1[j] + scale_x; + sx1[j] = cuda::round(fsx1[j]); + sx2[j] = cuda::round(fsx2[j]); + nC[j] = sx2[j] - sx1[j]; + hasL[j] = (float)sx1[j] > fsx1[j]; + hasR[j] = (float)sx2[j] < fsx2[j] && sx2[j] < width; + wL[j] = (float)sx1[j] - fsx1[j]; + wR[j] = fsx2[j] - (float)sx2[j]; + scaleA[j] = 1.f / (fminf(scale_x, width - fsx1[j]) * fminf(scale_y, height - fsy1)); + } + + work_type acc[GN]; +#pragma unroll + for (int j = 0; j < GN; ++j) acc[j] = work_type{0}; + + for (int dy = sy1; dy < sy2; ++dy) + { + const T *row = src.ptr(batch_idx, 0, dy, 0); +#pragma unroll + for (int j = 0; j < GN; ++j) + { +#pragma unroll + for (int c = 0; c < kMaxC; ++c) + { + if (c < nC[j]) + acc[j] = acc[j] + row[sx1[j] + c] * scaleA[j]; + } + } +#pragma unroll + for (int j = 0; j < GN; ++j) + { + if (hasL[j]) + acc[j] = acc[j] + row[sx1[j] - 1] * (wL[j] * scaleA[j]); + if (hasR[j]) + acc[j] = acc[j] + row[sx2[j]] * (wR[j] * scaleA[j]); + } + } + + if (hasTop) + { + const T *row = src.ptr(batch_idx, 0, sy1 - 1, 0); +#pragma unroll + for (int j = 0; j < GN; ++j) + { +#pragma unroll + for (int c = 0; c < kMaxC; ++c) + { + if (c < nC[j]) + acc[j] = acc[j] + row[sx1[j] + c] * (wTop * scaleA[j]); + } + } + } + if (hasBot) + { + const T *row = src.ptr(batch_idx, 0, sy2, 0); +#pragma unroll + for (int j = 0; j < GN; ++j) + { +#pragma unroll + for (int c = 0; c < kMaxC; ++c) + { + if (c < nC[j]) + acc[j] = acc[j] + row[sx1[j] + c] * (wBot * scaleA[j]); + } + } + } +#pragma unroll + for (int j = 0; j < GN; ++j) + { + if (hasTop && hasL[j]) + acc[j] = acc[j] + src.ptr(batch_idx, 0, sy1 - 1, 0)[sx1[j] - 1] * (wTop * wL[j] * scaleA[j]); + if (hasTop && hasR[j]) + acc[j] = acc[j] + src.ptr(batch_idx, 0, sy1 - 1, 0)[sx2[j]] * (wTop * wR[j] * scaleA[j]); + if (hasBot && hasR[j]) + acc[j] = acc[j] + src.ptr(batch_idx, 0, sy2, 0)[sx2[j]] * (wBot * wR[j] * scaleA[j]); + if (hasBot && hasL[j]) + acc[j] = acc[j] + src.ptr(batch_idx, 0, sy2, 0)[sx1[j] - 1] * (wBot * wL[j] * scaleA[j]); + } + +#pragma unroll + for (int j = 0; j < GN; ++j) out[g + j] = cuda::SaturateCast(acc[j]); + } + + T *dstRow = dst.ptr(batch_idx, 0, y, 0); + const int validCount = cuda::min(RVS_NIX, dstWidth - x0); + + if (validCount == RVS_NIX && RVSCheckRowAlign(dstRow + x0)) + RVSWritePack(dstRow[x0], out); + else + { + T *dstPtr = dstRow + x0; +#pragma unroll + for (int c = 0; c < RVS_NIX; ++c) + if (c < validCount) + dstPtr[c] = out[c]; + } +} + +template +void resize(const ImageBatchVarShapeDataStridedCuda &in, const ImageBatchVarShapeDataStridedCuda &out, + const int interpolation, cudaStream_t stream, ResizeVarShapeScale batchScale) +{ + NVCV_ASSERT(in.numImages() == out.numImages()); + + cuda::ImageBatchVarShapeWrap src_ptr(in); + cuda::ImageBatchVarShapeWrap dst_ptr(out); + + Size2D outMaxSize = out.maxSize(); + + const int THREADS_PER_BLOCK = 256; //Performance degrades above 256 and below 16 (GMEM speed limited) + int BLOCK_WIDTH = 8; //as in 32x4 or 32x8 or 8x32. + + // A warp row spans BLOCK_WIDTH * sizeof(T) bytes of the output. At width 8 the vectorized + // interleaved types (uchar3/uchar4/float/float3/float4) already cover a 32-byte L1 sector, but a + // 1-byte single-channel element (U8) covers only 8 bytes/row -- the bilinear/nearest kernels are + // then L1/TEX-throughput bound (~85% L1 SOL) on those tiny, poorly-coalesced loads. Widen the + // block to a full sector (32x8) for that case; the per-thread work is unchanged, so the result is + // bit-exact. Wider types keep width 8 (widening only shrinks their grid), so their launch is + // byte-identical to before. + if (sizeof(T) == 1) + BLOCK_WIDTH = 32; + + const dim3 blockSize(BLOCK_WIDTH, THREADS_PER_BLOCK / BLOCK_WIDTH, 1); + const dim3 gridSize(divUp(outMaxSize.w, blockSize.x), divUp(outMaxSize.h, blockSize.y), in.numImages()); + + //quad permits aligned writes to output image, if image is multiple of 4. kernels in resize_varshape are smart + const int out_quad_width = outMaxSize.w / 4; + const dim3 quadGridSize(divUp(out_quad_width, blockSize.x), divUp(outMaxSize.h, blockSize.y), in.numImages()); + + switch (interpolation) { case NVCV_INTERP_NEAREST: - resize_NN<<>>(src_ptr, dst_ptr); + { + // Each thread handles NN_NIX grid-strided columns, hoisting the scales and source row; the + // wide float3/float4 elements (already near the bandwidth ridge) keep one column. + constexpr int NN_NIX = (sizeof(T) <= 4) ? 4 : 1; + const dim3 nnGrid(divUp(outMaxSize.w, blockSize.x * NN_NIX), divUp(outMaxSize.h, blockSize.y), in.numImages()); + resize_NN<<>>(src_ptr, dst_ptr); break; + } case NVCV_INTERP_LINEAR: - resize_bilinear<<>>(src_ptr, dst_ptr); + { + // Exact-2x upscale fast path for byte types: dispatched only when the caller verified from + // the host-side image handles that every image pair in the batch is exactly 2x on both axes + // (the exported imageList is device memory, so it cannot be checked here). + if constexpr (sizeof(cuda::BaseType) == 1) + { + if (batchScale == ResizeVarShapeScale::kExpand2x) + { + const dim3 e2Block(32, 4, 1); + const dim3 e2Grid(divUp(outMaxSize.w, e2Block.x * RVS_NIX), divUp(outMaxSize.h, e2Block.y * 2), + in.numImages()); + resize_bilinear_expand2x<<>>(src_ptr, dst_ptr); + break; + } + // Generic byte path: consecutive columns with float-cached taps and pack stores (see + // resize_bilinear_pack); bit-exact with resize_bilinear at every scale. + const dim3 pkBlock(32, 4, 1); + const dim3 pkGrid(divUp(outMaxSize.w, pkBlock.x * RVS_NIX), divUp(outMaxSize.h, pkBlock.y), + in.numImages()); + resize_bilinear_pack<<>>(src_ptr, dst_ptr); + break; + } + // Each thread handles BILINEAR_NIX grid-strided columns, hoisting the row-invariant scales, + // y-terms, and row pointers; shrink grid.x accordingly. Multi-column only helps elements up + // to 4 bytes (U8/uchar3/uchar4/float), which were L1/compute co-limited; the wide float3/ + // float4 elements are already near the memory-bandwidth ridge (~89% BWUtil) and lose ~3% + // from the added register pressure, so they keep one column (NIX=1, original behavior). + constexpr int BILINEAR_NIX = (sizeof(T) <= 4) ? 4 : 1; + const dim3 linGrid(divUp(outMaxSize.w, blockSize.x * BILINEAR_NIX), divUp(outMaxSize.h, blockSize.y), + in.numImages()); + resize_bilinear<<>>(src_ptr, dst_ptr); break; + } case NVCV_INTERP_CUBIC: - resize_bicubic<<>>(src_ptr, dst_ptr); + { + // Wide-float (float3/float4) take the shared-memory-tiled kernel: upscale blocks serve the + // 16-tap L1/TEX-bound gather from a staged source tile (per-block EXPAND gate, others fall + // back to the direct gather identical to resize_bicubic NIX=1). Tile bound = block + cubic + // support, valid for any upscale scale. Other dtypes keep the NIX grid-strided kernel. + constexpr bool isWideFloat = (sizeof(T) > 4) && (sizeof(T) == sizeof(cuda::ConvertBaseTypeTo)); + if constexpr (isWideFloat) + { + const dim3 scBlock(32, 8, 1); + const dim3 scGrid(divUp(outMaxSize.w, scBlock.x), divUp(outMaxSize.h, scBlock.y), in.numImages()); + const int tileW = scBlock.x + 6, tileH = scBlock.y + 6; + const size_t smemBytes = (size_t)tileW * tileH * sizeof(T); + resize_bicubic_smem<<>>(src_ptr, dst_ptr, tileW, tileH); + } + else + { + // Each thread handles BICUBIC_NIX grid-strided columns, hoisting the scales, y cubic + // coefficients, and the four source-row pointers (16 taps/pixel). + constexpr int BICUBIC_NIX = (sizeof(T) <= 4) ? 4 : 1; + const dim3 cubicGrid(divUp(outMaxSize.w, blockSize.x * BICUBIC_NIX), divUp(outMaxSize.h, blockSize.y), + in.numImages()); + resize_bicubic<<>>(src_ptr, dst_ptr); + } break; + } case NVCV_INTERP_AREA: + { + // Exact-2x downscale fast path, dispatched only when the caller verified every image pair + // in the batch is exactly 2x on both axes. + if (batchScale == ResizeVarShapeScale::kContract2x) + { + const dim3 c2Grid(divUp(outMaxSize.w, blockSize.x * RVS_NIX), divUp(outMaxSize.h, blockSize.y), + in.numImages()); + resize_area_contract2x<<>>(src_ptr, dst_ptr); + break; + } + // Fractional zoom-out fast path (see resize_area_fractional_pack); every tap is in bounds, + // so the border wrap is not needed. + if (batchScale == ResizeVarShapeScale::kFractionalZoomOut) + { + const dim3 fpGrid(divUp(outMaxSize.w, blockSize.x * RVS_NIX), divUp(outMaxSize.h, blockSize.y), + in.numImages()); + resize_area_fractional_pack<<>>(src_ptr, dst_ptr); + break; + } cuda::BorderVarShapeWrap brdSrc(in); resize_area_ocv_align<<>>(src_ptr, brdSrc, dst_ptr); break; + } + + } //switch interpolation + checkKernelErrors(); + +#ifdef CUDA_DEBUG_LOG + checkCudaErrors(cudaStreamSynchronize(stream)); + checkCudaErrors(cudaGetLastError()); +#endif +} + +//******************** Planar (NCHW/CHW) variants +// +// Resize treats channels independently, so each plane of a planar image is resized exactly like a +// single-channel image. One thread owns an output pixel across ALL channel planes of an image +// (grid.z runs over numImages), computing the per-pixel coordinates/weights -- which depend only on +// (x, y), not the channel -- once and looping the planes. This avoids the (channels - 1)x redundant +// coordinate math of one thread per (image, plane), and keeps the per-plane result bit-exact with +// the interleaved path. + +template +__global__ void resize_NN_planar(cuda::ImageBatchVarShapeWrap src, cuda::ImageBatchVarShapeWrap dst, + int channels) +{ + const int dst_x = blockIdx.x * blockDim.x + threadIdx.x; + const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + + const int dstWidth = dst.width(batch_idx); + const int dstHeight = dst.height(batch_idx); + + if ((dst_x < dstWidth) && (dst_y < dstHeight)) + { + const int width = src.width(batch_idx); + const int height = src.height(batch_idx); + + const float scale_x = static_cast(width) / dstWidth; + const float scale_y = static_cast(height) / dstHeight; + const int sx = cuda::min(__float2int_rd((dst_x + 0.5f) * scale_x), width - 1); + const int sy = cuda::min(__float2int_rd((dst_y + 0.5f) * scale_y), height - 1); + + for (int plane = 0; plane < channels; ++plane) + { + *dst.ptr(batch_idx, plane, dst_y, dst_x) = *src.ptr(batch_idx, plane, sy, sx); + } + } +} + +// Planar bilinear, vectorized: each thread owns NIX grid-strided output columns of one row. The +// row-invariant y terms (scale, fy, sy) are hoisted once, and the per-column x terms (fx, sx) are +// precomputed once and reused across every channel plane -- the scalar kernel recomputed the source +// scales (two FP divisions) and x/y coordinates per output pixel. Each plane's two row pointers are +// resolved once. The per-pixel arithmetic is byte-identical, so the result is bit-exact. +template +__global__ void resize_bilinear_planar(cuda::ImageBatchVarShapeWrap src, cuda::ImageBatchVarShapeWrap dst, + int channels) +{ + const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + + const int dstWidth = dst.width(batch_idx); + const int dstHeight = dst.height(batch_idx); + if (dst_y >= dstHeight) + return; + + const int width = src.width(batch_idx); + const int height = src.height(batch_idx); + const float scale_x = static_cast(width) / dstWidth; + const float scale_y = static_cast(height) / dstHeight; + + float fy = (float)((dst_y + 0.5f) * scale_y - 0.5f); + int sy = cuda::round(fy); + fy = ((sy < 0) ? 0 : ((sy > height - 2) ? 1 : fy - sy)); + sy = cuda::max(0, cuda::min(sy, height - 2)); + + const int xBase = blockIdx.x * blockDim.x * NIX + threadIdx.x; + + // Per-column x terms, shared across all channel planes. + int dx[NIX], sxA[NIX]; + float fxA[NIX]; +#pragma unroll + for (int i = 0; i < NIX; ++i) + { + dx[i] = xBase + i * static_cast(blockDim.x); + float fx = (float)((dx[i] + 0.5f) * scale_x - 0.5f); + int sx = cuda::round(fx); + fxA[i] = ((sx < 0) ? 0 : ((sx > width - 2) ? 1 : fx - sx)); + sxA[i] = cuda::max(0, cuda::min(sx, width - 2)); + } + + for (int plane = 0; plane < channels; ++plane) + { + const T *aPtr = src.ptr(batch_idx, plane, sy, 0); // upper row + const T *bPtr = src.ptr(batch_idx, plane, sy + 1, 0); // lower row +#pragma unroll + for (int i = 0; i < NIX; ++i) + { + if (dx[i] >= dstWidth) + continue; + const int sx = sxA[i]; + const float fx = fxA[i]; + *dst.ptr(batch_idx, plane, dst_y, dx[i]) + = cuda::SaturateCast((1.0f - fx) * (aPtr[sx] * (1.0f - fy) + bPtr[sx] * fy) + + fx * (aPtr[sx + 1] * (1.0f - fy) + bPtr[sx + 1] * fy)); + } + } +} + +// Channel-amortized planar bicubic: grid.z runs over images (not image*plane), so one thread owns an +// output pixel across ALL channel planes. The cubic coefficients and clamped tap coordinates depend +// only on (x, y), not the channel, so they are computed once and reused for every plane -- avoiding +// the (channels - 1)x redundant coefficient math of calling resizeBicubicPlane once per plane. The +// per-plane accumulation is identical, so output stays bit-exact with the interleaved path. +template +__global__ void resize_bicubic_planar(cuda::ImageBatchVarShapeWrap src, cuda::ImageBatchVarShapeWrap dst, + int channels) +{ + const int dst_x = blockIdx.x * blockDim.x + threadIdx.x; + const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + + const int dstWidth = dst.width(batch_idx); + const int dstHeight = dst.height(batch_idx); + + if ((dst_x < dstWidth) & (dst_y < dstHeight)) + { + const int width = src.width(batch_idx); + const int height = src.height(batch_idx); + + const float scale_x = static_cast(width) / dstWidth; + const float scale_y = static_cast(height) / dstHeight; + + using work_type = cuda::ConvertBaseTypeTo; + + float fy = (float)((dst_y + 0.5f) * scale_y - 0.5f); + int sy = cuda::round(fy); + fy -= sy; + + const float A = -0.75f; + + float cY[4]; + cY[0] = ((A * (fy + 1) - 5 * A) * (fy + 1) + 8 * A) * (fy + 1) - 4 * A; + cY[1] = ((A + 2) * fy - (A + 3)) * fy * fy + 1; + cY[2] = ((A + 2) * (1 - fy) - (A + 3)) * (1 - fy) * (1 - fy) + 1; + cY[3] = 1.f - cY[0] - cY[1] - cY[2]; + + float fx = (float)((dst_x + 0.5f) * scale_x - 0.5f); + int sx = cuda::round(fx); + fx -= sx; + + float cX[4]; + cX[0] = ((A * (fx + 1.0f) - 5.0f * A) * (fx + 1.0f) + 8.0f * A) * (fx + 1.0f) - 4.0f * A; + cX[1] = ((A + 2.0f) * fx - (A + 3.0f)) * fx * fx + 1.0f; + cX[2] = ((A + 2.0f) * (1.0f - fx) - (A + 3.0f)) * (1.0f - fx) * (1.0f - fx) + 1.0f; + cX[3] = 1.0f - cX[0] - cX[1] - cX[2]; + + // Clamp each tap coordinate once (replicate border); reused across all planes. + int csy[4]; + int csx[4]; +#pragma unroll + for (int k = 0; k < 4; ++k) + { + csy[k] = cuda::clamp(sy + k - 1, 0, height - 1); + csx[k] = cuda::clamp(sx + k - 1, 0, width - 1); + } + + for (int plane = 0; plane < channels; ++plane) + { + work_type accum = cuda::SetAll(0); +#pragma unroll + for (int row = 0; row < 4; ++row) + { +#pragma unroll + for (int col = 0; col < 4; ++col) + { + accum += cY[row] * cX[col] * *src.ptr(batch_idx, plane, csy[row], csx[col]); + } + } + *dst.ptr(batch_idx, plane, dst_y, dst_x) = cuda::SaturateCast(accum); + } + } +} + +// Planar var-shape fractional AREA downscale: one thread owns RVS_NIX consecutive output columns +// of one row and loops the channel planes. The x/y box geometry (bounds, edge weights, scale) is +// hoisted into per-column arrays outside the plane loop -- keeping the cross-channel amortization +// of the general planar kernel while adding the consecutive-column/pack-store and in-bounds +// row-pointer wins of resize_area_fractional_pack. Per-output term order matches resizeAreaPlane +// exactly, so the result is bit-exact with the general planar kernel and the interleaved path. +template +__global__ void resize_area_fractional_pack_planar(cuda::ImageBatchVarShapeWrap src, + cuda::ImageBatchVarShapeWrap dst, int channels) +{ + using work_type = cuda::ConvertBaseTypeTo; + + const int batch_idx = get_batch_idx(); + const int dstWidth = dst.width(batch_idx); + const int dstHeight = dst.height(batch_idx); + const int y = blockIdx.y * blockDim.y + threadIdx.y; + const int x0 = (blockIdx.x * blockDim.x + threadIdx.x) * RVS_NIX; + + if (y >= dstHeight || x0 >= dstWidth) + return; + + const int width = src.width(batch_idx); + const int height = src.height(batch_idx); + + const float scale_x = static_cast(width) / dstWidth; + const float scale_y = static_cast(height) / dstHeight; + + const float fsy1 = y * scale_y; + const float fsy2 = fsy1 + scale_y; + const int sy1 = cuda::round(fsy1); + const int sy2 = cuda::round(fsy2); + + // Per-column x box terms, shared by every channel plane. Wide elements (RVS_NIX == 4) hoist + // them into arrays; byte planes (RVS_NIX == 16) recompute them inline per plane instead -- + // sixteen hoisted term sets spill to local memory and regressed the byte rows 1.6x. + constexpr int XN = (RVS_NIX <= 4) ? RVS_NIX : 1; + constexpr bool HOISTED = XN == RVS_NIX; + float fsx1[XN], fsx2[XN], scale[XN]; + int sx1[XN], sx2[XN]; + if constexpr (HOISTED) + { +#pragma unroll + for (int i = 0; i < XN; ++i) + { + const int x = cuda::min(x0 + i, dstWidth - 1); + fsx1[i] = x * scale_x; + fsx2[i] = fsx1[i] + scale_x; + sx1[i] = cuda::round(fsx1[i]); + sx2[i] = cuda::round(fsx2[i]); + scale[i] = 1.f / (fminf(scale_x, width - fsx1[i]) * fminf(scale_y, height - fsy1)); + } + } + + const int validCount = cuda::min(RVS_NIX, dstWidth - x0); + + for (int plane = 0; plane < channels; ++plane) + { + T out[RVS_NIX]; +#pragma unroll + for (int i = 0; i < RVS_NIX; ++i) + { + if (i >= validCount) + break; + + const int ti = HOISTED ? i : 0; + if constexpr (!HOISTED) + { + const int x = cuda::min(x0 + i, dstWidth - 1); + fsx1[0] = x * scale_x; + fsx2[0] = fsx1[0] + scale_x; + sx1[0] = cuda::round(fsx1[0]); + sx2[0] = cuda::round(fsx2[0]); + scale[0] = 1.f / (fminf(scale_x, width - fsx1[0]) * fminf(scale_y, height - fsy1)); + } + + work_type acc = {0}; + + for (int dy = sy1; dy < sy2; ++dy) + { + const T *srcRow = src.ptr(batch_idx, plane, dy, 0); + for (int dx = sx1[ti]; dx < sx2[ti]; ++dx) + { + acc = acc + srcRow[dx] * scale[ti]; + } + if (sx1[ti] > fsx1[ti]) + { + acc = acc + srcRow[sx1[ti] - 1] * ((sx1[ti] - fsx1[ti]) * scale[ti]); + } + if (sx2[ti] < fsx2[ti] && sx2[ti] < width) + { + acc = acc + srcRow[sx2[ti]] * ((fsx2[ti] - sx2[ti]) * scale[ti]); + } + } + if (sy1 > fsy1) + { + const T *topRow = src.ptr(batch_idx, plane, sy1 - 1, 0); + for (int dx = sx1[ti]; dx < sx2[ti]; ++dx) + { + acc = acc + topRow[dx] * ((sy1 - fsy1) * scale[ti]); + } + } + if (sy2 < fsy2 && sy2 < height) + { + const T *botRow = src.ptr(batch_idx, plane, sy2, 0); + for (int dx = sx1[ti]; dx < sx2[ti]; ++dx) + { + acc = acc + botRow[dx] * ((fsy2 - sy2) * scale[ti]); + } + } + if ((sy1 > fsy1) && (sx1[ti] > fsx1[ti])) + { + acc = acc + + src.ptr(batch_idx, plane, sy1 - 1, 0)[sx1[ti] - 1] + * ((sy1 - fsy1) * (sx1[ti] - fsx1[ti]) * scale[ti]); + } + if ((sy1 > fsy1) && (sx2[ti] < fsx2[ti] && sx2[ti] < width)) + { + acc = acc + + src.ptr(batch_idx, plane, sy1 - 1, 0)[sx2[ti]] + * ((sy1 - fsy1) * (fsx2[ti] - sx2[ti]) * scale[ti]); + } + if ((sy2 < fsy2 && sy2 < height) && (sx2[ti] < fsx2[ti] && sx2[ti] < width)) + { + acc = acc + + src.ptr(batch_idx, plane, sy2, 0)[sx2[ti]] * ((fsy2 - sy2) * (fsx2[ti] - sx2[ti]) * scale[ti]); + } + if ((sy2 < fsy2 && sy2 < height) && (sx1[ti] > fsx1[ti])) + { + acc = acc + + src.ptr(batch_idx, plane, sy2, 0)[sx1[ti] - 1] + * ((fsy2 - sy2) * (sx1[ti] - fsx1[ti]) * scale[ti]); + } + + out[i] = cuda::SaturateCast(acc); + } + + T *dstRow = dst.ptr(batch_idx, plane, y, 0); + + if (validCount == RVS_NIX && RVSCheckRowAlign(dstRow + x0)) + RVSWritePack(dstRow[x0], out); + else + { + T *dstPtr = dstRow + x0; +#pragma unroll + for (int c = 0; c < RVS_NIX; ++c) + if (c < validCount) + dstPtr[c] = out[c]; + } + } +} + +// Channel-amortized planar area: one thread per output pixel per image (grid.z = numImages) loops the +// channel planes. The area geometry (scale factors, integer box bounds, fractional edge weights, and +// the zoom-in coordinates/weights) depends only on (x, y), not the channel, so it is computed once +// and reused for every plane; only the box accumulation reads the per-plane data. Per-plane results +// are identical to resizeAreaPlane, so output stays bit-exact with the interleaved path. +template +__global__ void resize_area_ocv_align_planar(const cuda::ImageBatchVarShapeWrap src, + const cuda::BorderVarShapeWrap brd_src, + cuda::ImageBatchVarShapeWrap dst, int channels) +{ + const int x = blockDim.x * blockIdx.x + threadIdx.x; + const int y = blockDim.y * blockIdx.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + + const int dstWidth = dst.width(batch_idx); + const int dstHeight = dst.height(batch_idx); + + if (x >= dstWidth || y >= dstHeight) + return; + + const int height = src.height(batch_idx), width = src.width(batch_idx); + + const float scale_x = static_cast(width) / dstWidth; + const float scale_y = static_cast(height) / dstHeight; + + const float inv_scale_x = 1.f / scale_x; + const float inv_scale_y = 1.f / scale_y; + const int iscale_x = cuda::SaturateCast(scale_x); + const int iscale_y = cuda::SaturateCast(scale_y); + const bool is_area_fast = abs(scale_x - iscale_x) < DBL_EPSILON && abs(scale_y - iscale_y) < DBL_EPSILON; + + using work_type = cuda::ConvertBaseTypeTo; + + if (scale_x >= 1.0f && scale_y >= 1.0f) // zoom out + { + const float fsx1 = x * scale_x; + const float fsx2 = fsx1 + scale_x; + const int sx1 = cuda::round(fsx1); + const int sx2 = cuda::round(fsx2); + const float fsy1 = y * scale_y; + const float fsy2 = fsy1 + scale_y; + const int sy1 = cuda::round(fsy1); + const int sy2 = cuda::round(fsy2); + + if (is_area_fast) // integer multiples + { + const float scale = 1.f / (scale_x * scale_y); + + // Integer downscale: the box is fully in-bounds; read source rows directly (pointer hoisted + // per row) instead of the per-tap border-wrap. Bit-exact and identical to the interleaved path. + for (int plane = 0; plane < channels; ++plane) + { + work_type out = {0}; + for (int dy = sy1; dy < sy2; ++dy) + { + const T *srcRow = src.ptr(batch_idx, plane, dy, 0); + for (int dx = sx1; dx < sx2; ++dx) + { + out = out + srcRow[dx] * scale; + } + } + *dst.ptr(batch_idx, plane, y, x) = cuda::SaturateCast(out); + } + return; + } + + const float scale + = 1.f / (fminf(scale_x, src.width(batch_idx) - fsx1) * fminf(scale_y, src.height(batch_idx) - fsy1)); + + for (int plane = 0; plane < channels; ++plane) + { + work_type out = {0}; + int4 srcCoord = {0, 0, plane, batch_idx}; + + for (int dy = sy1; dy < sy2; ++dy) + { + srcCoord.y = dy; + // Interior columns [sx1,sx2) are in-bounds for a downscale: read the row directly + // (hoisted) instead of the per-tap border-wrap; fractional edges keep brd_src. Bit-exact. + const T *srcRow = src.ptr(batch_idx, plane, dy, 0); + for (int dx = sx1; dx < sx2; ++dx) + { + out = out + srcRow[dx] * scale; + } + if (sx1 > fsx1) + { + srcCoord.x = sx1 - 1; + out = out + brd_src[srcCoord] * ((sx1 - fsx1) * scale); + } + if (sx2 < fsx2) + { + srcCoord.x = sx2; + out = out + brd_src[srcCoord] * ((fsx2 - sx2) * scale); + } + } + if (sy1 > fsy1) + { + srcCoord.y = sy1 - 1; + for (int dx = sx1; dx < sx2; ++dx) + { + srcCoord.x = dx; + out = out + brd_src[srcCoord] * ((sy1 - fsy1) * scale); + } + } + if (sy2 < fsy2) + { + srcCoord.y = sy2; + for (int dx = sx1; dx < sx2; ++dx) + { + srcCoord.x = dx; + out = out + brd_src[srcCoord] * ((fsy2 - sy2) * scale); + } + } + if ((sy1 > fsy1) && (sx1 > fsx1)) + { + srcCoord.y = (sy1 - 1); + srcCoord.x = (sx1 - 1); + out = out + brd_src[srcCoord] * ((sy1 - fsy1) * (sx1 - fsx1) * scale); + } + if ((sy1 > fsy1) && (sx2 < fsx2)) + { + srcCoord.y = (sy1 - 1); + srcCoord.x = sx2; + out = out + brd_src[srcCoord] * ((sy1 - fsy1) * (fsx2 - sx2) * scale); + } + if ((sy2 < fsy2) && (sx2 < fsx2)) + { + srcCoord.y = sy2; + srcCoord.x = sx2; + out = out + brd_src[srcCoord] * ((fsy2 - sy2) * (fsx2 - sx2) * scale); + } + if ((sy2 < fsy2) && (sx1 > fsx1)) + { + srcCoord.y = sy2; + srcCoord.x = sx1 - 1; + out = out + brd_src[srcCoord] * ((fsy2 - sy2) * (sx1 - fsx1) * scale); + } + *dst.ptr(batch_idx, plane, y, x) = cuda::SaturateCast(out); + } + return; + } + + // zoom in, it is emulated using some variant of bilinear interpolation + int sy = cuda::round(y * scale_y); + float fy = (y + 1) - (sy + 1) * inv_scale_y; + fy = fy <= 0 ? 0.f : fy - cuda::round(fy); + + float cbufy[2]; + cbufy[0] = 1.f - fy; + cbufy[1] = fy; + + int sx = cuda::round(x * scale_x); + float fx = (x + 1) - (sx + 1) * inv_scale_x; + fx = fx < 0 ? 0.f : fx - cuda::round(fx); + + if (sx < 0) + { + fx = 0, sx = 0; + } + if (sx >= src.width(batch_idx) - 1) + { + fx = 0, sx = src.width(batch_idx) - 2; + } + if (sy >= src.height(batch_idx) - 1) + { + sy = src.height(batch_idx) - 2; + } + + float cbufx[2]; + cbufx[0] = 1.f - fx; + cbufx[1] = fx; + + for (int plane = 0; plane < channels; ++plane) + { + *dst.ptr(batch_idx, plane, y, x) + = cuda::SaturateCast((*src.ptr(batch_idx, plane, sy, sx) * cbufx[0] * cbufy[0] + + *src.ptr(batch_idx, plane, sy + 1, sx) * cbufx[0] * cbufy[1] + + *src.ptr(batch_idx, plane, sy, sx + 1) * cbufx[1] * cbufy[0] + + *src.ptr(batch_idx, plane, sy + 1, sx + 1) * cbufx[1] * cbufy[1])); + } +} + +template +void resize_planar(const ImageBatchVarShapeDataStridedCuda &in, const ImageBatchVarShapeDataStridedCuda &out, + const int channels, const int interpolation, cudaStream_t stream, ResizeVarShapeScale batchScale) +{ + NVCV_ASSERT(in.numImages() == out.numImages()); + + cuda::ImageBatchVarShapeWrap src_ptr(in); + cuda::ImageBatchVarShapeWrap dst_ptr(out); + + Size2D outMaxSize = out.maxSize(); + + const int THREADS_PER_BLOCK = 256; + + // The planar kernels run one thread per output pixel and loop the channel planes, so grid.z spans + // images. Size the block width so a warp row spans ~one 32-byte sector of a single (scalar) plane: + // width = 32 / sizeof(element). The default 8-wide block underuses L1/TEX for 8-bit planes (only + // 8 bytes/row), while wider-than-a-sector just shrinks the grid for wide elements; the interleaved + // path's vectorized type already fills a sector at width 8. + int planarWidth = 32 / static_cast(sizeof(T)); + if (planarWidth < 1) + planarWidth = 1; + const dim3 planarBlock(planarWidth, THREADS_PER_BLOCK / planarWidth, 1); + const dim3 planarGrid(divUp(outMaxSize.w, planarBlock.x), divUp(outMaxSize.h, planarBlock.y), in.numImages()); + + switch (interpolation) + { + case NVCV_INTERP_NEAREST: + resize_NN_planar<<>>(src_ptr, dst_ptr, channels); + break; + + case NVCV_INTERP_LINEAR: + { + // Vectorize only narrow (1-byte-base, i.e. uint8) planar elements: wide-float planar (NIX=1 + // here) keeps the scalar path, which avoids the CONTRACT regression seen when vectorizing it + // (float3 CONTRACT +21%). PNIX=1 is the original one-column behaviour. + constexpr int PNIX = (sizeof(T) == 1) ? 4 : 1; + const dim3 pbGrid(divUp(outMaxSize.w, planarBlock.x * PNIX), divUp(outMaxSize.h, planarBlock.y), + in.numImages()); + resize_bilinear_planar<<>>(src_ptr, dst_ptr, channels); + } + break; + + case NVCV_INTERP_CUBIC: + resize_bicubic_planar<<>>(src_ptr, dst_ptr, channels); + break; + + case NVCV_INTERP_AREA: + { + // Exact-2x downscale fast path (see resizeAreaContract2xPlane); the channel loop stays + // per-thread like the general planar kernel. + if (batchScale == ResizeVarShapeScale::kContract2x) + { + const dim3 c2Grid(divUp(outMaxSize.w, planarBlock.x * RVS_NIX), divUp(outMaxSize.h, planarBlock.y), + in.numImages()); + resize_area_contract2x_planar<<>>(src_ptr, dst_ptr, channels); + break; + } + // Fractional zoom-out fast path with cross-channel geometry amortization (see + // resize_area_fractional_pack_planar). + if (batchScale == ResizeVarShapeScale::kFractionalZoomOut) + { + const dim3 fpGrid(divUp(outMaxSize.w, planarBlock.x * RVS_NIX), divUp(outMaxSize.h, planarBlock.y), + in.numImages()); + resize_area_fractional_pack_planar<<>>(src_ptr, dst_ptr, channels); + break; + } + cuda::BorderVarShapeWrap brdSrc(in); + resize_area_ocv_align_planar<<>>(src_ptr, brdSrc, dst_ptr, channels); + break; + } } //switch interpolation checkKernelErrors(); @@ -482,7 +1795,8 @@ void resize(const ImageBatchVarShapeDataStridedCuda &in, const ImageBatchVarShap ErrorCode ResizeVarShape::infer(const ImageBatchVarShapeDataStridedCuda &inData, const ImageBatchVarShapeDataStridedCuda &outData, - const NVCVInterpolationType interpolation, cudaStream_t stream) + const NVCVInterpolationType interpolation, cudaStream_t stream, + ResizeVarShapeScale batchScale) { if (!inData.uniqueFormat()) { @@ -507,20 +1821,33 @@ ErrorCode ResizeVarShape::infer(const ImageBatchVarShapeDataStridedCuda &inData, DataFormat format = input_format; - if (!(format == kNHWC || format == kHWC)) + if (!(format == kNHWC || format == kHWC || format == kNCHW || format == kCHW)) { - LOG_ERROR("Invalid input DataFormat " << format << ", the valid DataFormats are: \"NHWC\", \"HWC\""); + LOG_ERROR("Invalid input DataFormat " << format + << ", the valid DataFormats are: \"NHWC\", \"HWC\", \"NCHW\", \"CHW\""); return ErrorCode::INVALID_DATA_FORMAT; } + const bool isPlanar = (format == kNCHW || format == kCHW); + int channels = inData.uniqueFormat().numChannels(); - if (channels > 4) + // Planar 2-channel layout is rejected: there is no defined 2-plane planar format, and the + // interleaved path likewise does not support 2 channels (matches the Normalize operator). + if (channels > 4 || (isPlanar && channels == 2)) { LOG_ERROR("Invalid channel number " << channels); return ErrorCode::INVALID_DATA_SHAPE; } + // The planar path launches one grid-z slice per (image, plane), so numImages * channels must + // fit CUDA's 65535 grid-z limit. Compute in 64-bit to avoid overflow before the comparison. + if (isPlanar && static_cast(inData.numImages()) * channels > 65535) + { + LOG_ERROR("Planar resize requires numImages * channels <= 65535 (CUDA grid-z limit)"); + return ErrorCode::INVALID_DATA_SHAPE; + } + DataType data_type = helpers::GetLegacyDataType(inData.uniqueFormat()); if (!(data_type == kCV_8U || data_type == kCV_16U || data_type == kCV_16S || data_type == kCV_32F)) @@ -536,8 +1863,27 @@ ErrorCode ResizeVarShape::infer(const ImageBatchVarShapeDataStridedCuda &inData, return ErrorCode::INVALID_PARAMETER; } + if (isPlanar) + { + // Planar dispatch indexes by dtype only: each channel is resized as a separate + // single-channel plane, so one scalar specialization per dtype covers any channel count. + typedef void (*planar_func_t)(const ImageBatchVarShapeDataStridedCuda &in, + const ImageBatchVarShapeDataStridedCuda &out, const int channels, + const int interpolation, cudaStream_t stream, ResizeVarShapeScale batchScale); + + static const planar_func_t planar_funcs[6] = { + resize_planar, 0 /*schar*/, resize_planar, + resize_planar, 0 /*int*/, resize_planar, + }; + + const planar_func_t planar_func = planar_funcs[data_type]; + NVCV_ASSERT(planar_func != 0); + planar_func(inData, outData, channels, interpolation, stream, batchScale); + return ErrorCode::SUCCESS; + } + typedef void (*func_t)(const ImageBatchVarShapeDataStridedCuda &in, const ImageBatchVarShapeDataStridedCuda &out, - const int interpolation, cudaStream_t stream); + const int interpolation, cudaStream_t stream, ResizeVarShapeScale batchScale); static const func_t funcs[6][4] = { { resize, 0 /*resize*/, resize, resize}, @@ -551,7 +1897,7 @@ ErrorCode ResizeVarShape::infer(const ImageBatchVarShapeDataStridedCuda &inData, const func_t func = funcs[data_type][channels - 1]; assert(func != 0); - func(inData, outData, interpolation, stream); + func(inData, outData, interpolation, stream, batchScale); return ErrorCode::SUCCESS; } // namespace diff --git a/src/cvcuda/priv/legacy/rotate.cu b/src/cvcuda/priv/legacy/rotate.cu index 2360c36b0..7d0c7e1a9 100644 --- a/src/cvcuda/priv/legacy/rotate.cu +++ b/src/cvcuda/priv/legacy/rotate.cu @@ -1,4 +1,4 @@ -/* Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +/* Copyright (c) 2021-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * * SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES * SPDX-License-Identifier: Apache-2.0 @@ -22,48 +22,136 @@ #include "CvCudaLegacyHelpers.hpp" #include "CvCudaUtils.cuh" +#include "warp_cubic.cuh" #define BLOCK 32 #define PI 3.1415926535897932384626433832795 namespace nvcv::legacy::cuda_op { -__global__ void compute_warpAffine(const double angle, const double xShift, const double yShift, double *aCoeffs) +__global__ void compute_warpAffine(const double angle, const double xShift, const double yShift, float *aCoeffs) { - aCoeffs[0] = cos(angle * PI / 180); - aCoeffs[1] = sin(angle * PI / 180); - aCoeffs[2] = xShift; - aCoeffs[3] = -sin(angle * PI / 180); - aCoeffs[4] = cos(angle * PI / 180); - aCoeffs[5] = yShift; + // Trig in FP64 for accuracy (one-shot, 1 thread; perf-irrelevant). + // Stored as FP32 — the per-pixel rotate kernel reads these in FP32 to avoid + // the 1/64-rate FP64 path on consumer GPUs. + aCoeffs[0] = static_cast(cos(angle * PI / 180)); + aCoeffs[1] = static_cast(sin(angle * PI / 180)); + aCoeffs[2] = static_cast(xShift); + aCoeffs[3] = static_cast(-sin(angle * PI / 180)); + aCoeffs[4] = static_cast(cos(angle * PI / 180)); + aCoeffs[5] = static_cast(yShift); } -template -__global__ void rotate(SrcWrapper src, DstWrapper dst, int2 dstSize, const double *d_aCoeffs) +// Number of output rows each thread emits (Y-tiling factor), chosen per interpolation type and element +// size. The rotate kernel is latency/issue-bound on consumer Ampere (one interpolated gather per pixel +// through L1/TEX, low SOL). For small-element NEAREST/LINEAR the per-pixel working set is tiny, so a wide +// Y-tile amortizes the per-thread invariants (six affine coefficients, the column term, the source-bounds +// load) and overlaps several independent per-row gathers to hide latency -- up to ~2x on uint8 NEAREST, +// ~1.5x on uint8 LINEAR. Two cases must stay at one row per thread (the original one-pixel-per-thread +// mapping) to avoid regressing: CUBIC touches a 4x4 neighborhood per pixel and tiling it thrashes L1 +// (measured up to ~5x slower on float4); and wide (>2-byte component) elements such as float move enough +// bytes per pixel that tiling also costs more than it saves (measured ~1.25x slower on float NEAREST). +// Y-tiling (rather than X-tiling) keeps the X dimension one thread per column so warp stores stay +// coalesced. +template +constexpr int RotateRowsPerThread = (I == NVCV_INTERP_CUBIC || sizeof(cuda::BaseType) > 2) ? 1 : 8; + +// Bit-exactness: src_x/src_y are recomputed per pixel with the same FP multiply form and operand order as +// the original scalar kernel (the column term is the same left operand of the same add), not accumulated +// incrementally, so the rounding is identical to one-pixel-per-thread. With NIY == 1 the body collapses +// to exactly the original mapping. +template +__global__ void rotate(SrcWrapper src, DstWrapper dst, int2 dstSize, const float *d_aCoeffs) { - int3 dstCoord = cuda::StaticCast(blockIdx * blockDim + threadIdx); + const int dst_x = blockIdx.x * blockDim.x + threadIdx.x; + const int dst_y0 = (blockIdx.y * blockDim.y + threadIdx.y) * NIY; + const int dst_z = blockIdx.z * blockDim.z + threadIdx.z; - if (dstCoord.x >= dstSize.x || dstCoord.y >= dstSize.y) + if (dst_x >= dstSize.x) { return; } - const double dst_x_shift = dstCoord.x - d_aCoeffs[2]; - const double dst_y_shift = dstCoord.y - d_aCoeffs[5]; - const float3 srcCoord{static_cast(dst_x_shift * d_aCoeffs[0] + dst_y_shift * (-d_aCoeffs[1])), - static_cast(dst_x_shift * (-d_aCoeffs[3]) + dst_y_shift * d_aCoeffs[4]), - static_cast(dstCoord.z)}; + const float c0 = d_aCoeffs[0]; + const float c1 = d_aCoeffs[1]; + const float c2 = d_aCoeffs[2]; + const float c3 = d_aCoeffs[3]; + const float c4 = d_aCoeffs[4]; + const float c5 = d_aCoeffs[5]; + + const float dst_x_shift = static_cast(dst_x) - c2; + const float src_x_col = dst_x_shift * c0; + const float src_y_col = dst_x_shift * (-c3); const long2 srcSize{src.borderWrap().tensorShape()[1], src.borderWrap().tensorShape()[0]}; - if (srcCoord.x > -0.5 && srcCoord.x < srcSize.x && srcCoord.y > -0.5 && srcCoord.y < srcSize.y) +#pragma unroll + for (int i = 0; i < NIY; ++i) + { + const int dst_y = dst_y0 + i; + if (dst_y >= dstSize.y) + { + break; + } + + const float dst_y_shift = static_cast(dst_y) - c5; + const float3 srcCoord{src_x_col + dst_y_shift * (-c1), src_y_col + dst_y_shift * c4, static_cast(dst_z)}; + + if (srcCoord.x > -0.5 && srcCoord.x < srcSize.x && srcCoord.y > -0.5 && srcCoord.y < srcSize.y) + { + const int3 dstCoord{dst_x, dst_y, dst_z}; + + using SrcValueT = std::remove_cv_t; + constexpr bool kFastCubic + = SrcWrapper::kInterpolationType == NVCV_INTERP_CUBIC && kCubicFastSampler; + if constexpr (kFastCubic) + { + dst[dstCoord] = CubicSampleTensor( + src, dst_z, float2{srcCoord.x, srcCoord.y}, + int2{static_cast(srcSize.x), static_cast(srcSize.y)}); + } + else + { + dst[dstCoord] = src[srcCoord]; + } + } + } +} + +// Fused planar CUBIC rotate: one thread maps its output pixel once and gathers all NP channel +// planes, sharing the coordinate transform, cubic weights, and border resolution that the +// flattened per-plane launch recomputes NP times. Wraps address the flattened (N*C, H, W) view; +// plane p of sample z sits at flattened sample z*NP + p. +template +__global__ void rotate_planar_fused(SrcWrapper src, DstWrapper dst, int2 dstSize, int2 srcSize, const float *d_aCoeffs) +{ + const int dst_x = blockIdx.x * blockDim.x + threadIdx.x; + const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int dst_z = blockIdx.z; + + if (dst_x >= dstSize.x || dst_y >= dstSize.y) + { + return; + } + + const float dst_x_shift = static_cast(dst_x) - d_aCoeffs[2]; + const float dst_y_shift = static_cast(dst_y) - d_aCoeffs[5]; + const float2 coord{dst_x_shift * d_aCoeffs[0] + dst_y_shift * (-d_aCoeffs[1]), + dst_x_shift * (-d_aCoeffs[3]) + dst_y_shift * d_aCoeffs[4]}; + + if (coord.x > -0.5f && coord.x < static_cast(srcSize.x) && coord.y > -0.5f + && coord.y < static_cast(srcSize.y)) { - dst[dstCoord] = src[srcCoord]; + using BT = std::remove_cv_t; + CubicWarpPlanes( + [&](int p, int yy) { return src.ptr(dst_z * NP + p, yy); }, + [&](int p, BT v) { *(dst.ptr(dst_z * NP + p, dst_y) + dst_x) = v; }, float4{0.f, 0.f, 0.f, 0.f}, srcSize, + coord); } } template -ErrorCode rotate(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, double *d_aCoeffs, +ErrorCode rotate(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, float *d_aCoeffs, const double angleDeg, const double2 shift, cudaStream_t stream) { auto outAccess = TensorDataAccessStridedImagePlanar::Create(outData); @@ -78,8 +166,11 @@ ErrorCode rotate(const TensorDataStridedCuda &inData, const TensorDataStridedCud compute_warpAffine<<<1, 1, 0, stream>>>(angleDeg, shift.x, shift.y, d_aCoeffs); checkKernelErrors(); + constexpr int kNIY = RotateRowsPerThread; + dim3 blockSize(BLOCK, BLOCK / 4, 1); - dim3 gridSize(divUp(dstSize.x, blockSize.x), divUp(dstSize.y, blockSize.y), batchSize); + // Each thread emits kNIY output rows, so the grid covers ceil(dstSize.y / (blockY*kNIY)). + dim3 gridSize(divUp(dstSize.x, blockSize.x), divUp(dstSize.y, blockSize.y * kNIY), batchSize); int64_t inMaxStride = inAccess->sampleStride() * inAccess->numSamples(); int64_t outMaxStride = outAccess->sampleStride() * outAccess->numSamples(); @@ -88,7 +179,7 @@ ErrorCode rotate(const TensorDataStridedCuda &inData, const TensorDataStridedCud auto src = cuda::CreateInterpolationWrapNHW(inData); auto dst = cuda::CreateTensorWrapNHW(outData); - rotate<<>>(src, dst, dstSize, d_aCoeffs); + rotate<<>>(src, dst, dstSize, d_aCoeffs); } else { @@ -105,7 +196,7 @@ ErrorCode rotate(const TensorDataStridedCuda &inData, const TensorDataStridedCud } template // uchar3 float3 uchar1 float3 -ErrorCode rotate(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, double *d_aCoeffs, +ErrorCode rotate(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, float *d_aCoeffs, const double angleDeg, const double2 shift, const NVCVInterpolationType interpolation, cudaStream_t stream) { @@ -126,6 +217,99 @@ ErrorCode rotate(const TensorDataStridedCuda &inData, const TensorDataStridedCud } } +// Build a single-channel (N*C, H, W, 1) NHWC view of a packed planar (NCHW/CHW) tensor. Rotate maps +// each output pixel to a source pixel with the same per-sample affine coefficients regardless of the +// channel, so plane (n, c) is just a single-channel image; viewing the N*C planes as flat samples lets +// the existing interleaved single-channel rotate kernel handle planar data unchanged (bit-exact with +// the equivalent NHWC single-channel rotate). Plane (n, c) is placed at sample index n*C + c, byte +// offset (n*C + c)*chStride, which matches the real layout only when the channel planes are tightly +// packed across samples (sampleStride == C*chStride); the caller guards that. A single sample +// (N == 1) always satisfies it. The scalar angle/shift apply to every plane, so the single set of +// d_aCoeffs computed by compute_warpAffine drives all N*C flattened samples. +static nvcv::TensorDataStridedCuda PlanarAsSingleChannelView(const nvcv::TensorDataStridedCuda &data, + const nvcv::TensorDataAccessStridedImagePlanar &access) +{ + const int64_t numSamples = access.numSamples(); + const int64_t numChannels = access.numChannels(); + + nvcv::TensorDataStridedCuda::Buffer buf; + buf.basePtr = reinterpret_cast(data.basePtr()); + buf.strides[0] = access.chStride(); // N*C flattened planes + buf.strides[1] = access.rowStride(); // H + buf.strides[2] = access.colStride(); // W + buf.strides[3] = access.colStride(); // C == 1 + return nvcv::TensorDataStridedCuda{ + nvcv::TensorShape{{numSamples * numChannels, access.numRows(), access.numCols(), 1}, "NHWC"}, + data.dtype(), + buf + }; +} + +template +ErrorCode rotatePlanarFusedCubic(const TensorDataStridedCuda &inView, const TensorDataStridedCuda &outView, + int numPlanes, int numSamples, float *d_aCoeffs, const double angleDeg, + const double2 shift, cudaStream_t stream) +{ + auto outAccess = TensorDataAccessStridedImagePlanar::Create(outView); + NVCV_ASSERT(outAccess); + auto inAccess = TensorDataAccessStridedImagePlanar::Create(inView); + NVCV_ASSERT(inAccess); + + const int2 dstSize{outAccess->numCols(), outAccess->numRows()}; + const int2 srcSize{inAccess->numCols(), inAccess->numRows()}; + + const int64_t inMaxStride = inAccess->sampleStride() * inAccess->numSamples(); + const int64_t outMaxStride = outAccess->sampleStride() * outAccess->numSamples(); + if (std::max(inMaxStride, outMaxStride) > cuda::TypeTraits::max) + { + LOG_ERROR("Input or output size exceeds " << cuda::TypeTraits::max << ". Tensor is too large."); + return ErrorCode::INVALID_PARAMETER; + } + + compute_warpAffine<<<1, 1, 0, stream>>>(angleDeg, shift.x, shift.y, d_aCoeffs); + checkKernelErrors(); + + auto src = cuda::CreateTensorWrapNHW(inView); + auto dst = cuda::CreateTensorWrapNHW(outView); + + dim3 blockSize(BLOCK, BLOCK / 4, 1); + dim3 gridSize(divUp(dstSize.x, blockSize.x), divUp(dstSize.y, blockSize.y), numSamples); + + switch (numPlanes) + { + case 1: + rotate_planar_fused<1><<>>(src, dst, dstSize, srcSize, d_aCoeffs); + break; + case 3: + rotate_planar_fused<3><<>>(src, dst, dstSize, srcSize, d_aCoeffs); + break; + case 4: + rotate_planar_fused<4><<>>(src, dst, dstSize, srcSize, d_aCoeffs); + break; + default: + LOG_ERROR("Invalid planar channel number " << numPlanes); + return ErrorCode::INVALID_DATA_SHAPE; + } + checkKernelErrors(); + return ErrorCode::SUCCESS; +} + +static ErrorCode rotatePlanarFusedCubicCaller(const TensorDataStridedCuda &inView, const TensorDataStridedCuda &outView, + int dataType, int numPlanes, int numSamples, float *d_aCoeffs, + const double angleDeg, const double2 shift, cudaStream_t stream) +{ + typedef ErrorCode (*func_t)(const TensorDataStridedCuda &inView, const TensorDataStridedCuda &outView, + int numPlanes, int numSamples, float *d_aCoeffs, const double angleDeg, + const double2 shift, cudaStream_t stream); + + static const func_t funcs[6] = {rotatePlanarFusedCubic, 0, rotatePlanarFusedCubic, + rotatePlanarFusedCubic, 0, rotatePlanarFusedCubic}; + + const func_t func = funcs[dataType]; + NVCV_ASSERT(func != 0); + return func(inView, outView, numPlanes, numSamples, d_aCoeffs, angleDeg, shift, stream); +} + Rotate::Rotate(DataShape max_input_shape, DataShape max_output_shape) : CudaBaseOp(max_input_shape, max_output_shape) , d_aCoeffs(nullptr) @@ -135,7 +319,7 @@ Rotate::Rotate(DataShape max_input_shape, DataShape max_output_shape) if (err != cudaSuccess) { LOG_ERROR("CUDA memory allocation error of size: " << bufferSize); - throw std::runtime_error("CUDA memory allocation error!"); + throw LegacyCudaAllocationError("CUDA memory allocation error!"); } } @@ -154,7 +338,7 @@ Rotate::~Rotate() size_t Rotate::calBufferSize(DataShape max_input_shape, DataShape max_output_shape, DataType max_data_type) { - return 6 * sizeof(double); + return 6 * sizeof(float); } ErrorCode Rotate::infer(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, @@ -172,12 +356,15 @@ ErrorCode Rotate::infer(const TensorDataStridedCuda &inData, const TensorDataStr DataFormat format = input_format; - if (!(format == kNHWC || format == kHWC)) + if (!(format == kNHWC || format == kHWC || format == kNCHW || format == kCHW)) { - LOG_ERROR("Invalid input DataFormat " << format << ", the valid DataFormats are: \"NHWC\", \"HWC\""); + LOG_ERROR("Invalid input DataFormat " << format + << ", the valid DataFormats are: \"NHWC\", \"HWC\", \"NCHW\", \"CHW\""); return ErrorCode::INVALID_DATA_FORMAT; } + const bool isPlanar = (format == kNCHW || format == kCHW); + auto inAccess = TensorDataAccessStridedImagePlanar::Create(inData); NVCV_ASSERT(inAccess); @@ -186,7 +373,7 @@ ErrorCode Rotate::infer(const TensorDataStridedCuda &inData, const TensorDataStr int channels = input_shape.C; - if (channels > 4) + if (channels > 4 || channels == 2) { LOG_ERROR("Invalid channel number " << channels); return ErrorCode::INVALID_DATA_SHAPE; @@ -206,7 +393,7 @@ ErrorCode Rotate::infer(const TensorDataStridedCuda &inData, const TensorDataStr } typedef ErrorCode (*func_t)(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, - double *d_aCoeffs, const double angleDeg, const double2 shift, + float *d_aCoeffs, const double angleDeg, const double2 shift, const NVCVInterpolationType interpolation, cudaStream_t stream); static const func_t funcs[6][4] = { @@ -218,6 +405,42 @@ ErrorCode Rotate::infer(const TensorDataStridedCuda &inData, const TensorDataStr { rotate, 0 /*rotate*/, rotate, rotate} }; + if (isPlanar) + { + // View each of the N*C channel planes as a single-channel sample and reuse the interleaved + // single-channel rotate kernel (funcs column 0). Channels are independent in rotate, so this + // is bit-exact with rotating the equivalent NHWC single-channel data. The flattened plane count + // becomes the kernel's grid.z, capped at CUDA's 65535 limit; compute it in 64-bit to avoid + // overflow. The flattened view is only valid when the planes are tightly packed across samples. + auto outAccess = TensorDataAccessStridedImagePlanar::Create(outData); + NVCV_ASSERT(outAccess); + + const int64_t numSamples = inAccess->numSamples(); + if (numSamples > 1 + && (inAccess->sampleStride() != static_cast(channels) * inAccess->chStride() + || outAccess->sampleStride() != static_cast(channels) * outAccess->chStride())) + { + LOG_ERROR("Planar rotate of a batched tensor requires tightly packed channel planes"); + return ErrorCode::INVALID_DATA_SHAPE; + } + if (numSamples * channels > 65535) + { + LOG_ERROR("Planar rotate requires numSamples * numChannels <= 65535 (CUDA grid-z limit)"); + return ErrorCode::INVALID_DATA_SHAPE; + } + + auto srcView = PlanarAsSingleChannelView(inData, *inAccess); + auto dstView = PlanarAsSingleChannelView(outData, *outAccess); + + if (interpolation == NVCV_INTERP_CUBIC) + { + return rotatePlanarFusedCubicCaller(srcView, dstView, data_type, channels, static_cast(numSamples), + d_aCoeffs, angleDeg, shift, stream); + } + + return funcs[data_type][0](srcView, dstView, d_aCoeffs, angleDeg, shift, interpolation, stream); + } + const func_t func = funcs[data_type][channels - 1]; NVCV_ASSERT(func != 0); diff --git a/src/cvcuda/priv/legacy/rotate_var_shape.cu b/src/cvcuda/priv/legacy/rotate_var_shape.cu index 09e773ce1..42b9fac8a 100644 --- a/src/cvcuda/priv/legacy/rotate_var_shape.cu +++ b/src/cvcuda/priv/legacy/rotate_var_shape.cu @@ -1,4 +1,4 @@ -/* Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +/* Copyright (c) 2021-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * * SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES * SPDX-License-Identifier: Apache-2.0 @@ -22,6 +22,7 @@ #include "CvCudaLegacyHelpers.hpp" #include "CvCudaUtils.cuh" +#include "warp_cubic.cuh" #define BLOCK 32 #define PI 3.1415926535897932384626433832795 @@ -32,7 +33,7 @@ using namespace nvcv::legacy::helpers; namespace nvcv::legacy::cuda_op { __global__ void compute_warpAffine(const int numImages, const cuda::Tensor1DWrap angleDeg, - const cuda::Tensor2DWrap shift, double *d_aCoeffs) + const cuda::Tensor2DWrap shift, float *d_aCoeffs) { int index = threadIdx.x + blockIdx.x * blockDim.x; if (index >= numImages) @@ -40,64 +41,112 @@ __global__ void compute_warpAffine(const int numImages, const cuda::Tensor1DWrap return; } - double *aCoeffs = (double *)((char *)d_aCoeffs + (sizeof(double) * 6) * index); + // Trig in FP64 for accuracy (one-shot, 1 thread per image; perf-irrelevant). + // Stored as FP32 — the per-pixel rotate kernel reads these in FP32 to avoid + // the 1/64-rate FP64 path on consumer GPUs. + float *aCoeffs = (float *)((char *)d_aCoeffs + (sizeof(float) * 6) * index); double angle = angleDeg[index]; double xShift = *shift.ptr(index, 0); double yShift = *shift.ptr(index, 1); - aCoeffs[0] = cos(angle * PI / 180); - aCoeffs[1] = sin(angle * PI / 180); - aCoeffs[2] = xShift; - aCoeffs[3] = -sin(angle * PI / 180); - aCoeffs[4] = cos(angle * PI / 180); - aCoeffs[5] = yShift; + aCoeffs[0] = static_cast(cos(angle * PI / 180)); + aCoeffs[1] = static_cast(sin(angle * PI / 180)); + aCoeffs[2] = static_cast(xShift); + aCoeffs[3] = static_cast(-sin(angle * PI / 180)); + aCoeffs[4] = static_cast(cos(angle * PI / 180)); + aCoeffs[5] = static_cast(yShift); } -template -__global__ void rotate(SrcWrapper src, DstWrapper dst, const double *d_aCoeffs_) +// Number of output rows each thread emits (Y-tiling factor), chosen per interpolation type and element +// size, matching the tensor kernel rationale: small-element NEAREST/LINEAR benefit from a wide Y-tile +// (amortized per-thread invariants + overlapped per-row gathers); CUBIC's 4x4 neighborhood and wide +// (>2-byte component) elements regress when tiled, so they stay at one row per thread (the original +// mapping). Y-tiling keeps the X dimension one thread per column so warp stores stay coalesced. +template +constexpr int RotateRowsPerThread = (I == NVCV_INTERP_CUBIC || sizeof(cuda::BaseType) > 2) ? 1 : 8; + +// Bit-exact: src_x/src_y use the same FP multiply form and operand order as the original scalar kernel +// (the column term is the same left operand of the same add). With NIY == 1 the body collapses to the +// original one-pixel-per-thread mapping. +template +__global__ void rotate(SrcWrapper src, DstWrapper dst, const float *d_aCoeffs_) { - int3 dstCoord = cuda::StaticCast(blockDim * blockIdx + threadIdx); + const int dst_x = blockDim.x * blockIdx.x + threadIdx.x; + const int dst_y0 = (blockDim.y * blockIdx.y + threadIdx.y) * NIY; + const int batch_idx = blockIdx.z * blockDim.z + threadIdx.z; - if (dstCoord.x >= dst.width(dstCoord.z) || dstCoord.y >= dst.height(dstCoord.z)) + const int dstWidth = dst.width(batch_idx); + const int dstHeight = dst.height(batch_idx); + if (dst_x >= dstWidth) return; - const double *d_aCoeffs = (const double *)((char *)d_aCoeffs_ + (sizeof(double) * 6) * dstCoord.z); - const double dst_x_shift = dstCoord.x - d_aCoeffs[2]; - const double dst_y_shift = dstCoord.y - d_aCoeffs[5]; - float src_x = (float)(dst_x_shift * d_aCoeffs[0] + dst_y_shift * (-d_aCoeffs[1])); - float src_y = (float)(dst_x_shift * (-d_aCoeffs[3]) + dst_y_shift * d_aCoeffs[4]); - - const int width = src.borderWrap().imageBatchWrap().width(dstCoord.z); - const int height = src.borderWrap().imageBatchWrap().height(dstCoord.z); - - if (src_x > -0.5 && src_x < width && src_y > -0.5 && src_y < height) + const float *d_aCoeffs = (const float *)((char *)d_aCoeffs_ + (sizeof(float) * 6) * batch_idx); + const float c0 = d_aCoeffs[0]; + const float c1 = d_aCoeffs[1]; + const float c3 = d_aCoeffs[3]; + const float c4 = d_aCoeffs[4]; + const float c5 = d_aCoeffs[5]; + const float dst_x_shift = static_cast(dst_x) - d_aCoeffs[2]; + const float src_x_col = dst_x_shift * c0; + const float src_y_col = dst_x_shift * (-c3); + + const int width = src.borderWrap().imageBatchWrap().width(batch_idx); + const int height = src.borderWrap().imageBatchWrap().height(batch_idx); + +#pragma unroll + for (int i = 0; i < NIY; ++i) { - const float3 srcCoord{src_x, src_y, static_cast(dstCoord.z)}; + const int dst_y = dst_y0 + i; + if (dst_y >= dstHeight) + break; + + const float dst_y_shift = static_cast(dst_y) - c5; + const float src_x = src_x_col + dst_y_shift * (-c1); + const float src_y = src_y_col + dst_y_shift * c4; - dst[dstCoord] = src[srcCoord]; + if (src_x > -0.5 && src_x < width && src_y > -0.5 && src_y < height) + { + const int3 dstCoord{dst_x, dst_y, batch_idx}; + + using SrcValueT = std::remove_cv_t; + constexpr bool kFastCubic + = SrcWrapper::kInterpolationType == NVCV_INTERP_CUBIC && kCubicFastSampler; + if constexpr (kFastCubic) + { + dst[dstCoord] = CubicSampleVarShape(src, batch_idx, float2{src_x, src_y}); + } + else + { + const float3 srcCoord{src_x, src_y, static_cast(batch_idx)}; + dst[dstCoord] = src[srcCoord]; + } + } } } template -void rotate(const ImageBatchVarShapeDataStridedCuda &in, const ImageBatchVarShapeDataStridedCuda &out, - double *d_aCoeffs, cudaStream_t stream) +void rotate(const ImageBatchVarShapeDataStridedCuda &in, const ImageBatchVarShapeDataStridedCuda &out, float *d_aCoeffs, + cudaStream_t stream) { Size2D outMaxSize = out.maxSize(); + constexpr int kNIY = RotateRowsPerThread; + dim3 blockSize(BLOCK, BLOCK / 4, 1); - dim3 gridSize(divUp(outMaxSize.w, blockSize.x), divUp(outMaxSize.h, blockSize.y), in.numImages()); + // Each thread emits kNIY output rows, so the grid covers ceil(maxH / (blockY*kNIY)). + dim3 gridSize(divUp(outMaxSize.w, blockSize.x), divUp(outMaxSize.h, blockSize.y * kNIY), in.numImages()); cuda::InterpolationVarShapeWrap src(in); cuda::ImageBatchVarShapeWrap dst(out); - rotate<<>>(src, dst, d_aCoeffs); + rotate<<>>(src, dst, d_aCoeffs); checkKernelErrors(); } template // uchar3 float3 uchar1 float3 -void rotate(const ImageBatchVarShapeDataStridedCuda &in, const ImageBatchVarShapeDataStridedCuda &out, - double *d_aCoeffs, const NVCVInterpolationType interpolation, cudaStream_t stream) +void rotate(const ImageBatchVarShapeDataStridedCuda &in, const ImageBatchVarShapeDataStridedCuda &out, float *d_aCoeffs, + const NVCVInterpolationType interpolation, cudaStream_t stream) { NVCV_ASSERT(in.numImages() == out.numImages()); @@ -120,6 +169,159 @@ void rotate(const ImageBatchVarShapeDataStridedCuda &in, const ImageBatchVarShap } } +// Planar (NCHW/CHW) rotate. Rotate maps each output pixel to a source pixel with the same affine +// coefficients regardless of the channel, so the (src_x, src_y) mapping is computed once per output +// pixel and reused across every channel plane (grid.z runs over images, the kernel loops the planes). +// This avoids the per-plane redundant coordinate math of one thread per (image, plane). Each plane is +// sampled through the same InterpolationVarShapeWrap the interleaved kernel uses -- its operator[] +// takes a 4D {x, y, plane, sample} coordinate -- so each plane's result is bit-exact with the +// interleaved single-channel rotate and the interleaved codegen is left untouched. +template +__global__ void rotate_planar(SrcWrapper src, DstWrapper dst, const float *d_aCoeffs_, int channels) +{ + const int dst_x = blockIdx.x * blockDim.x + threadIdx.x; + const int dst_y0 = (blockIdx.y * blockDim.y + threadIdx.y) * NIY; + const int batch_idx = get_batch_idx(); + + const int dstWidth = dst.width(batch_idx); + const int dstHeight = dst.height(batch_idx); + if (dst_x >= dstWidth) + return; + + const float *d_aCoeffs = (const float *)((char *)d_aCoeffs_ + (sizeof(float) * 6) * batch_idx); + const float c0 = d_aCoeffs[0]; + const float c1 = d_aCoeffs[1]; + const float c3 = d_aCoeffs[3]; + const float c4 = d_aCoeffs[4]; + const float c5 = d_aCoeffs[5]; + const float dst_x_shift = static_cast(dst_x) - d_aCoeffs[2]; + const float src_x_col = dst_x_shift * c0; + const float src_y_col = dst_x_shift * (-c3); + + const int width = src.borderWrap().imageBatchWrap().width(batch_idx); + const int height = src.borderWrap().imageBatchWrap().height(batch_idx); + +#pragma unroll + for (int i = 0; i < NIY; ++i) + { + const int dst_y = dst_y0 + i; + if (dst_y >= dstHeight) + break; + + const float dst_y_shift = static_cast(dst_y) - c5; + const float src_x = src_x_col + dst_y_shift * (-c1); + const float src_y = src_y_col + dst_y_shift * c4; + + if (src_x > -0.5 && src_x < width && src_y > -0.5 && src_y < height) + { + for (int plane = 0; plane < channels; ++plane) + { + const float4 srcCoord{src_x, src_y, static_cast(plane), static_cast(batch_idx)}; + *dst.ptr(batch_idx, plane, dst_y, dst_x) = src[srcCoord]; + } + } + } +} + +// Fused planar CUBIC rotate: shares the coordinate transform, cubic weights, and border +// resolution across all NP channel planes; the generic kernel above re-resolves them per plane +// through the interpolation wrap. +template +__global__ void rotate_planar_fused(SrcWrapper src, DstWrapper dst, const float *d_aCoeffs_) +{ + const int dst_x = blockIdx.x * blockDim.x + threadIdx.x; + const int dst_y = blockIdx.y * blockDim.y + threadIdx.y; + const int batch_idx = get_batch_idx(); + + if (dst_x >= dst.width(batch_idx) || dst_y >= dst.height(batch_idx)) + return; + + const float *d_aCoeffs = (const float *)((char *)d_aCoeffs_ + (sizeof(float) * 6) * batch_idx); + const float dst_x_shift = static_cast(dst_x) - d_aCoeffs[2]; + const float dst_y_shift = static_cast(dst_y) - d_aCoeffs[5]; + const float2 coord{dst_x_shift * d_aCoeffs[0] + dst_y_shift * (-d_aCoeffs[1]), + dst_x_shift * (-d_aCoeffs[3]) + dst_y_shift * d_aCoeffs[4]}; + + const int width = src.width(batch_idx); + const int height = src.height(batch_idx); + + if (coord.x > -0.5f && coord.x < static_cast(width) && coord.y > -0.5f + && coord.y < static_cast(height)) + { + using BT = std::remove_cv_t; + CubicWarpPlanes( + [&](int p, int yy) { return src.ptr(batch_idx, p, yy, 0); }, + [&](int p, BT v) { *dst.ptr(batch_idx, p, dst_y, dst_x) = v; }, float4{0.f, 0.f, 0.f, 0.f}, + int2{width, height}, coord); + } +} + +template +void rotate_planar(const ImageBatchVarShapeDataStridedCuda &in, const ImageBatchVarShapeDataStridedCuda &out, + float *d_aCoeffs, const int channels, cudaStream_t stream) +{ + Size2D outMaxSize = out.maxSize(); + + constexpr int kNIY = RotateRowsPerThread; + + dim3 blockSize(BLOCK, BLOCK / 4, 1); + // Each thread emits kNIY output rows, so the grid covers ceil(maxH / (blockY*kNIY)). + dim3 gridSize(divUp(outMaxSize.w, blockSize.x), divUp(outMaxSize.h, blockSize.y * kNIY), in.numImages()); + + cuda::ImageBatchVarShapeWrap dst(out); + + if constexpr (I == NVCV_INTERP_CUBIC) + { + cuda::ImageBatchVarShapeWrap src(in); + + switch (channels) + { + case 1: + rotate_planar_fused<1><<>>(src, dst, d_aCoeffs); + checkKernelErrors(); + return; + case 3: + rotate_planar_fused<3><<>>(src, dst, d_aCoeffs); + checkKernelErrors(); + return; + case 4: + rotate_planar_fused<4><<>>(src, dst, d_aCoeffs); + checkKernelErrors(); + return; + default: + break; + } + } + + cuda::InterpolationVarShapeWrap src(in); + + rotate_planar<<>>(src, dst, d_aCoeffs, channels); + checkKernelErrors(); +} + +template +void rotate_planar(const ImageBatchVarShapeDataStridedCuda &in, const ImageBatchVarShapeDataStridedCuda &out, + float *d_aCoeffs, const int channels, const NVCVInterpolationType interpolation, cudaStream_t stream) +{ + switch (interpolation) + { + case NVCV_INTERP_NEAREST: + rotate_planar(in, out, d_aCoeffs, channels, stream); + break; + + case NVCV_INTERP_LINEAR: + rotate_planar(in, out, d_aCoeffs, channels, stream); + break; + + case NVCV_INTERP_CUBIC: + rotate_planar(in, out, d_aCoeffs, channels, stream); + break; + + default: + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Invalid interpolation type"); + } +} + RotateVarShape::RotateVarShape(const int maxBatchSize) : CudaBaseOp() , d_aCoeffs(nullptr) @@ -127,12 +329,12 @@ RotateVarShape::RotateVarShape(const int maxBatchSize) { if (m_maxBatchSize > 0) { - size_t bufferSize = sizeof(double) * 6 * m_maxBatchSize; + size_t bufferSize = sizeof(float) * 6 * m_maxBatchSize; cudaError_t err = cudaMalloc(&d_aCoeffs, bufferSize); if (err != cudaSuccess) { LOG_ERROR("CUDA memory allocation error of size: " << bufferSize); - throw std::runtime_error("CUDA memory allocation error!"); + throw LegacyCudaAllocationError("CUDA memory allocation error!"); } } } @@ -190,20 +392,33 @@ ErrorCode RotateVarShape::infer(const ImageBatchVarShapeDataStridedCuda &inData, DataFormat format = input_format; - if (!(format == kNHWC || format == kHWC)) + if (!(format == kNHWC || format == kHWC || format == kNCHW || format == kCHW)) { - LOG_ERROR("Invalid input DataFormat " << format << ", the valid DataFormats are: \"NHWC\", \"HWC\""); + LOG_ERROR("Invalid input DataFormat " << format + << ", the valid DataFormats are: \"NHWC\", \"HWC\", \"NCHW\", \"CHW\""); return ErrorCode::INVALID_DATA_FORMAT; } + const bool isPlanar = (format == kNCHW || format == kCHW); + int channels = inData.uniqueFormat().numChannels(); - if (channels > 4) + // Planar 2-channel layout is rejected: there is no defined 2-plane planar format, and it matches + // the Resize and Normalize operators. + if (channels > 4 || (isPlanar && channels == 2)) { LOG_ERROR("Invalid channel number " << channels); return ErrorCode::INVALID_DATA_SHAPE; } + // The planar path launches grid.z over images and loops the channel planes inside the kernel, so + // numImages must fit CUDA's 65535 grid-z limit. Compute in 64-bit to avoid overflow. + if (isPlanar && static_cast(outData.numImages()) > 65535) + { + LOG_ERROR("Planar rotate requires numImages <= 65535 (CUDA grid-z limit)"); + return ErrorCode::INVALID_DATA_SHAPE; + } + DataType data_type = helpers::GetLegacyDataType(inData.uniqueFormat()); DataType angleDec_data_type = helpers::GetLegacyDataType(angleDeg.dtype()); DataType shift_data_type = helpers::GetLegacyDataType(shift.dtype()); @@ -239,8 +454,28 @@ ErrorCode RotateVarShape::infer(const ImageBatchVarShapeDataStridedCuda &inData, compute_warpAffine<<<1, inData.numImages(), 0, stream>>>(inData.numImages(), angleDecPtr, shiftPtr, d_aCoeffs); checkKernelErrors(); + if (isPlanar) + { + // Planar dispatch indexes by dtype only: each channel is rotated as a separate single-channel + // plane, so one scalar specialization per dtype covers any channel count. + typedef void (*planar_func_t)( + const ImageBatchVarShapeDataStridedCuda &in, const ImageBatchVarShapeDataStridedCuda &out, float *d_aCoeffs, + const int channels, const NVCVInterpolationType interpolation, cudaStream_t stream); + + static const planar_func_t planar_funcs[6] = { + rotate_planar, 0 /*schar*/, rotate_planar, + rotate_planar, 0 /*int*/, rotate_planar, + }; + + const planar_func_t planar_func = planar_funcs[data_type]; + NVCV_ASSERT(planar_func != 0); + + planar_func(inData, outData, d_aCoeffs, channels, interpolation, stream); + return SUCCESS; + } + typedef void (*func_t)(const ImageBatchVarShapeDataStridedCuda &in, const ImageBatchVarShapeDataStridedCuda &out, - double *d_aCoeffs, const NVCVInterpolationType interpolation, cudaStream_t stream); + float *d_aCoeffs, const NVCVInterpolationType interpolation, cudaStream_t stream); static const func_t funcs[6][4] = { { rotate, 0 /*rotate*/, rotate, rotate}, diff --git a/src/cvcuda/priv/legacy/textbackend/backend.cpp b/src/cvcuda/priv/legacy/textbackend/backend.cpp index 250f1efd4..3634ba28d 100644 --- a/src/cvcuda/priv/legacy/textbackend/backend.cpp +++ b/src/cvcuda/priv/legacy/textbackend/backend.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Permission is hereby granted, free of charge, to any person obtaining a @@ -33,28 +33,24 @@ const char *text_backend_type_name(TextBackendType backend) { - switch (backend) + if (backend == TextBackendType::StbTrueType) { - case TextBackendType::StbTrueType: return "StbTrueType"; - default: - return "Unknow"; } + return "Unknow"; } std::shared_ptr create_text_backend(TextBackendType backend) { - switch (backend) - { #ifdef ENABLE_TEXT_BACKEND_STB - case TextBackendType::StbTrueType: + if (backend == TextBackendType::StbTrueType) + { return create_stb_backend(); + } #endif - default: - printf("Unsupport text backend: %s\n", text_backend_type_name(backend)); - return nullptr; - } + printf("Unsupport text backend: %s\n", text_backend_type_name(backend)); + return nullptr; } std::string concat_font_name_size(const char *name, int size) diff --git a/src/cvcuda/priv/legacy/textbackend/backend.hpp b/src/cvcuda/priv/legacy/textbackend/backend.hpp index 90a305943..21ec4b3af 100644 --- a/src/cvcuda/priv/legacy/textbackend/backend.hpp +++ b/src/cvcuda/priv/legacy/textbackend/backend.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Permission is hereby granted, free of charge, to any person obtaining a @@ -24,11 +24,13 @@ #ifndef TEXT_BACKEND_HPP #define TEXT_BACKEND_HPP +#include + #include #include #include -#define MAX_FONT_SIZE 200 +inline constexpr int MAX_FONT_SIZE = 200; enum class TextBackendType : int { @@ -39,6 +41,8 @@ enum class TextBackendType : int class WordMeta { public: + virtual ~WordMeta() = default; + virtual int width() const = 0; virtual int height() const = 0; virtual int x_offset_on_bitmap() const = 0; @@ -48,19 +52,23 @@ class WordMeta class WordMetaMapper { public: + virtual ~WordMetaMapper() = default; + virtual WordMeta *query(unsigned long int word) = 0; }; class TextBackend { public: + virtual ~TextBackend() = default; + virtual std::vector split_utf8(const char *utf8_text) = 0; virtual std::tuple measure_text(const std::vector &words, unsigned int font_size, const char *font) = 0; virtual void add_build_text(const std::vector &words, unsigned int font_size, const char *font) = 0; - virtual void build_bitmap(void *stream = nullptr) = 0; + virtual void build_bitmap(cudaStream_t stream = nullptr) = 0; virtual WordMetaMapper *query(const char *font, int font_size) = 0; virtual unsigned char *bitmap_device_pointer() const = 0; virtual int bitmap_width() const = 0; diff --git a/src/cvcuda/priv/legacy/textbackend/memory.hpp b/src/cvcuda/priv/legacy/textbackend/memory.hpp index 49a202a5f..be2bae790 100644 --- a/src/cvcuda/priv/legacy/textbackend/memory.hpp +++ b/src/cvcuda/priv/legacy/textbackend/memory.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Permission is hereby granted, free of charge, to any person obtaining a @@ -49,6 +49,13 @@ template class Memory { public: + Memory() = default; + + Memory(const Memory &) = delete; + Memory(Memory &&) = delete; + Memory &operator=(const Memory &) = delete; + Memory &operator=(Memory &&) = delete; + T *host() const { return host_; diff --git a/src/cvcuda/priv/legacy/textbackend/stb.cpp b/src/cvcuda/priv/legacy/textbackend/stb.cpp index 3cc6852b5..6a5158df3 100644 --- a/src/cvcuda/priv/legacy/textbackend/stb.cpp +++ b/src/cvcuda/priv/legacy/textbackend/stb.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Permission is hereby granted, free of charge, to any person obtaining a @@ -28,15 +28,24 @@ # include "memory.hpp" # include +# include # include # include # include # include # include +# include +# include +# include # include +# include # include +# include # include +# include +# include +# include # define strtok_s strtok_r # define STB_TRUETYPE_IMPLEMENTATION @@ -48,49 +57,116 @@ using namespace std; +static constexpr size_t kMaxTrustedFontFileBytes = 64U * 1024U * 1024U; + +static const vector &system_font_search_paths() +{ + static const vector search_paths = { + "/usr/share/fonts/truetype", // Debian/Ubuntu + "/usr/share/fonts/dejavu", // RHEL/AlmaLinux + "/usr/share/fonts/liberation", // RHEL/AlmaLinux alternative + "/usr/local/share/fonts", + "/usr/share/fonts" // Generic fallback (will search all subdirectories) + }; + return search_paths; +} + +static string canonical_path(string_view path) +{ + string pathString(path); + array resolvedPath{}; + if (realpath(pathString.c_str(), resolvedPath.data()) == nullptr) + { + return ""; + } + return string(resolvedPath.data()); +} + +static bool path_is_under_directory(string_view path, string_view directory) +{ + string root = canonical_path(directory); + if (root.empty()) + { + return false; + } + if (path == root) + { + return true; + } + if (root.back() != '/') + { + root += "/"; + } + return path.compare(0, root.size(), root) == 0; +} + +static bool is_trusted_font_path(string_view font_path) +{ + string resolvedPath = canonical_path(font_path); + if (resolvedPath.empty()) + { + return false; + } + + const auto &searchPaths = system_font_search_paths(); + return any_of(searchPaths.begin(), searchPaths.end(), + [&](const string &root) { return path_is_under_directory(resolvedPath, root); }); +} + +static int stb_word_xadvance(int width, int height, float scale, int advance, bool empty = false) +{ + if (empty || width < 1 || height < 1) + { + return static_cast(static_cast(advance) * scale * 0.5f); + } + return static_cast(static_cast(width) + std::max(1.0f, static_cast(advance) * scale / 5.0f)); +} + class StbWordMeta : public WordMeta { public: - int x0, y0, x1, y1, advance, glyph, offset_x; + int x0; + int y0; + int x1; + int y1; + int advance; + int glyph; + int offset_x; float scale; - virtual int width() const override + int width() const override { return x1 - x0; } - virtual int height() const override + int height() const override { return y1 - y0; } - virtual int x_offset_on_bitmap() const override + int x_offset_on_bitmap() const override { return offset_x; } - virtual int xadvance(int font_size, bool empty) const override + int xadvance(int font_size, bool empty) const override { (void)font_size; - if (empty) - { - return this->advance * this->scale * 0.5; - } - return width() + std::max(1.0f, this->advance * this->scale / 5.0f); + return stb_word_xadvance(width(), height(), this->scale, this->advance, empty); } StbWordMeta() = default; StbWordMeta(int x0, int y0, int x1, int y1, float scale, int advance, int glyph, int offset_x) + : x0(x0) + , y0(y0) + , x1(x1) + , y1(y1) + , advance(advance) + , glyph(glyph) + , offset_x(offset_x) + , scale(scale) { - this->x0 = x0; - this->y0 = y0; - this->x1 = x1; - this->y1 = y1; - this->scale = scale; - this->advance = advance; - this->glyph = glyph; - this->offset_x = offset_x; } }; @@ -99,7 +175,7 @@ class StbWordMetaMapperImpl , public map { public: - virtual WordMeta *query(unsigned long int word) override + WordMeta *query(unsigned long int word) override { auto iter = this->find(word); if (iter == this->end()) @@ -119,118 +195,174 @@ static bool file_exist(const string &path) return access(path.c_str(), R_OK) == 0; } -static vector> find_files(const string &directory, const string &suffix, bool includeSubDirectory) +static bool suffix_match(string_view path, string_view suffix) { - string realpath = directory; - if (realpath.empty()) - realpath = "./"; - - char backchar = realpath.back(); - if (backchar not_eq '\\' and backchar not_eq '/') - realpath += "/"; + if (path.size() < suffix.size()) + { + return false; + } + if (suffix.empty()) + { + return true; + } + return path.substr(path.size() - suffix.size()) == suffix; +} - struct dirent *fileinfo; - DIR *handle; - stack ps; - vector> out; - ps.push(realpath); +static bool font_name_matches(string_view lower_name, string_view query_name) +{ + if (lower_name == query_name) + { + return true; + } - auto suffix_match = [&](const string &path) + size_t dot_pos = lower_name.rfind('.'); + if (dot_pos == string::npos) { - if (path.size() < suffix.size()) - return false; - if (suffix.empty()) - return true; - return path.substr(path.size() - suffix.size()) == suffix; - }; + return false; + } - while (!ps.empty()) + return lower_name.substr(0, dot_pos) == query_name; +} + +static void warn_font_fallback(bool fallback, const char *requested_family, const string &fallback_name) +{ + if (fallback) { - string search_path = ps.top(); - ps.pop(); + CUOSD_PRINT_W("Can not find any fonts to match %s, fallback to %s\n", requested_family, fallback_name.c_str()); + } +} - handle = opendir(search_path.c_str()); - if (handle) +static bool is_current_or_parent_directory(const char *name) +{ + return strcmp(name, ".") == 0 || strcmp(name, "..") == 0; +} + +struct DirectoryCloser +{ + void operator()(DIR *dir) const + { + if (dir != nullptr) { - while (true) - { - fileinfo = readdir(handle); - if (fileinfo == nullptr) - break; + closedir(dir); + } + } +}; - struct stat file_stat; - if (strcmp(fileinfo->d_name, ".") == 0 or strcmp(fileinfo->d_name, "..") == 0) - continue; +static void collect_files_from_directory(const string &search_path, const string &suffix, bool includeSubDirectory, + stack &pending_paths, vector> &out) +{ + std::unique_ptr handle(opendir(search_path.c_str())); + if (!handle) + { + return; + } - if (lstat((search_path + fileinfo->d_name).c_str(), &file_stat) < 0) - continue; + struct dirent *fileinfo = nullptr; + while ((fileinfo = readdir(handle.get())) != nullptr) + { + if (is_current_or_parent_directory(fileinfo->d_name)) + { + continue; + } - if (!S_ISDIR(file_stat.st_mode)) - { - if (suffix_match(fileinfo->d_name)) - out.push_back(make_tuple(search_path + fileinfo->d_name, fileinfo->d_name)); - } + string path = search_path + fileinfo->d_name; + struct stat file_stat; + if (lstat(path.c_str(), &file_stat) < 0) + { + continue; + } - if (includeSubDirectory && S_ISDIR(file_stat.st_mode)) - ps.push(search_path + fileinfo->d_name + "/"); + if (S_ISDIR(file_stat.st_mode)) + { + if (includeSubDirectory) + { + pending_paths.push(path + "/"); } - closedir(handle); + continue; } + + if (suffix_match(fileinfo->d_name, suffix)) + { + out.emplace_back(path, fileinfo->d_name); + } + } +} + +static vector> find_files(const string &directory, const string &suffix, bool includeSubDirectory) +{ + string realpath = directory; + if (realpath.empty()) + { + realpath = "./"; + } + + if (char backchar = realpath.back(); backchar != '\\' && backchar != '/') + { + realpath += "/"; } + + stack pending_paths; + vector> out; + pending_paths.push(realpath); + + while (!pending_paths.empty()) + { + string search_path = pending_paths.top(); + pending_paths.pop(); + collect_files_from_directory(search_path, suffix, includeSubDirectory, pending_paths, out); + } + return out; } static void cuda_font_free(TrueTypeFontInternal *ptr) { - if (ptr) - delete ptr; + std::unique_ptr font(ptr); + (void)font; } static TrueTypeFontInternal *load_true_type_font(istream &infile, int file_size) { - TrueTypeFontInternal *output = new TrueTypeFontInternal(); + auto output = std::make_unique(); output->data.resize(file_size); if (!infile.read((char *)output->data.data(), file_size).good()) { - cuda_font_free(output); CUOSD_PRINT_E("Failed to read %d bytes.\n", file_size); return nullptr; } stbtt_fontinfo *font = &output->font; int offset = stbtt_GetFontOffsetForIndex(output->data.data(), 0); - int ret = stbtt_InitFont(font, output->data.data(), offset); - if (ret == 0) + if (offset < 0) + { + CUOSD_PRINT_E("Failed to find a TrueType font in file.\n"); + return nullptr; + } + if (int ret = stbtt_InitFont(font, output->data.data(), offset); ret == 0) { - cuda_font_free(output); CUOSD_PRINT_E("Failed to init font, ret = %d.\n", ret); return nullptr; } - return output; + return output.release(); } -static string get_ttf_path_from_family_name(const char *_font_family) -{ - // Search multiple common font directories for better Linux distribution compatibility - // - Debian/Ubuntu: /usr/share/fonts/truetype - // - RHEL/AlmaLinux/CentOS: /usr/share/fonts/dejavu, /usr/share/fonts/liberation - // - Generic fallback: /usr/share/fonts (recursive search) - vector search_paths = { - "/usr/share/fonts/truetype", // Debian/Ubuntu - "/usr/share/fonts/dejavu", // RHEL/AlmaLinux - "/usr/share/fonts/liberation", // RHEL/AlmaLinux alternative - "/usr/share/fonts" // Generic fallback (will search all subdirectories) - }; +string get_ttf_path_from_family_name(const char *_font_family, const vector &search_paths); +string get_ttf_path_from_family_name(const char *_font_family, const vector &search_paths) +{ vector> files; + unordered_set seen_paths; for (const auto &search_path : search_paths) { - auto found_files = find_files(search_path.c_str(), ".ttf", true); - files.insert(files.end(), found_files.begin(), found_files.end()); - // If we found files, we can stop searching additional paths - if (!files.empty()) - break; + auto found_files = find_files(search_path, ".ttf", true); + for (const auto &file : found_files) + { + if (seen_paths.insert(get<0>(file)).second) + { + files.push_back(file); + } + } } if (files.empty()) @@ -244,29 +376,20 @@ static string get_ttf_path_from_family_name(const char *_font_family) vector match_list{font_family, "dejavusansmono"}; for (size_t imatch = 0; imatch < match_list.size(); ++imatch) { - auto query_name = match_list[imatch]; - for (auto file : files) + const string &query_name = match_list[imatch]; + for (const auto &file : files) { - string path, lower_name, raw_name; + string path; + string lower_name; + string raw_name; tie(path, raw_name) = file; lower_name = raw_name; std::transform(raw_name.begin(), raw_name.end(), lower_name.begin(), ::tolower); - bool matched = lower_name == query_name; - if (!matched) - { - int p = lower_name.rfind("."); - if (p != -1) - matched = lower_name.substr(0, p) == query_name; - } - if (matched) + if (font_name_matches(lower_name, query_name)) { - if (imatch > 0) - { - CUOSD_PRINT_W("Can not find any fonts to match %s, fallback to %s\n", _font_family, - raw_name.c_str()); - } + warn_font_fallback(imatch > 0, _font_family, raw_name); return path; } } @@ -276,6 +399,11 @@ static string get_ttf_path_from_family_name(const char *_font_family) return get<0>(files[0]); } +static string get_ttf_path_from_family_name(const char *_font_family) +{ + return get_ttf_path_from_family_name(_font_family, system_font_search_paths()); +} + static TrueTypeFontInternal *create_cuda_font(const char *font_file_or_family); static TrueTypeFontInternal *load_true_type_from_family_name(const char *_font_family) @@ -297,6 +425,12 @@ static TrueTypeFontInternal *create_cuda_font(const char *font_file_or_family) return load_true_type_from_family_name(font_file_or_family); } + if (!is_trusted_font_path(font_file_or_family)) + { + CUOSD_PRINT_E("Refusing to parse untrusted font path: %s\n", font_file_or_family); + return nullptr; + } + fstream infile(font_file_or_family, ios::binary | ios::in); if (!infile) { @@ -313,98 +447,185 @@ static TrueTypeFontInternal *create_cuda_font(const char *font_file_or_family) } infile.seekg(0, ios::beg); - return load_true_type_font(infile, file_size); + if (file_size > kMaxTrustedFontFileBytes || file_size > static_cast(std::numeric_limits::max())) + { + CUOSD_PRINT_E("Invalid font file. File is too large. %s\n", font_file_or_family); + return nullptr; + } + + return load_true_type_font(infile, static_cast(file_size)); +} + +static bool decode_utf8_one_byte(const unsigned char *&str, unsigned long int &codepoint) +{ + codepoint = *str++; + return true; +} + +static bool decode_utf8_two_bytes(const unsigned char *&str, unsigned long int &codepoint) +{ + if (*str < 0xc2) + { + return false; + } + + unsigned int c = (*str++ & 0x1f) << 6; + if ((*str & 0xc0) != 0x80) + { + return false; + } + + codepoint = c + (*str++ & 0x3f); + return true; +} + +static bool decode_utf8_three_bytes(const unsigned char *&str, unsigned long int &codepoint) +{ + if (*str == 0xe0 && (str[1] < 0xa0 || str[1] > 0xbf)) + { + return false; + } + if (*str == 0xed && str[1] > 0x9f) + { + return false; // str[1] < 0x80 is checked below + } + + unsigned int c = (*str++ & 0x0f) << 12; + if ((*str & 0xc0) != 0x80) + { + return false; + } + + c += (*str++ & 0x3f) << 6; + if ((*str & 0xc0) != 0x80) + { + return false; + } + + codepoint = c + (*str++ & 0x3f); + return true; +} + +static bool decode_utf8_four_bytes(const unsigned char *&str, unsigned long int &codepoint) +{ + if (*str > 0xf4) + { + return false; + } + if (*str == 0xf0 && (str[1] < 0x90 || str[1] > 0xbf)) + { + return false; + } + if (*str == 0xf4 && str[1] > 0x8f) + { + return false; // str[1] < 0x80 is checked below + } + + unsigned int c = (*str++ & 0x07) << 18; + if ((*str & 0xc0) != 0x80) + { + return false; + } + + c += (*str++ & 0x3f) << 12; + if ((*str & 0xc0) != 0x80) + { + return false; + } + + c += (*str++ & 0x3f) << 6; + if ((*str & 0xc0) != 0x80) + { + return false; + } + + c += (*str++ & 0x3f); + if ((c & 0xFFFFF800) == 0xD800) + { + return false; + } + + codepoint = c; + return true; +} + +static bool decode_utf8_codepoint(const unsigned char *&str, unsigned long int &codepoint) +{ + if (!(*str & 0x80)) + { + return decode_utf8_one_byte(str, codepoint); + } + if ((*str & 0xe0) == 0xc0) + { + return decode_utf8_two_bytes(str, codepoint); + } + if ((*str & 0xf0) == 0xe0) + { + return decode_utf8_three_bytes(str, codepoint); + } + if ((*str & 0xf8) == 0xf0) + { + return decode_utf8_four_bytes(str, codepoint); + } + + return false; +} + +struct FontNameAndSize +{ + string name; + int size; +}; + +static FontNameAndSize parse_font_name_and_size(const string &font_name_and_size) +{ + size_t sep_pos = font_name_and_size.rfind(' '); + if (sep_pos == string::npos) + { + return {font_name_and_size, 0}; + } + + return {font_name_and_size.substr(0, sep_pos), std::atoi(font_name_and_size.c_str() + sep_pos + 1)}; } class StbTrueTypeBackend : public TextBackend { private: - unique_ptr> text_bitmap; - unique_ptr> single_word_bitmap; - map glyph_sets; - map> build_use_textes; - int text_bitmap_width = 0; - int text_bitmap_height = 0; - int temp_size = 0; - map> font_map; - bool has_new_text_need_build_bitmap = false; + unique_ptr> text_bitmap; + unique_ptr> single_word_bitmap; + map> glyph_sets; + map, less<>> build_use_textes; + int text_bitmap_width = 0; + int text_bitmap_height = 0; + int temp_size = 0; + map, less<>> font_map; + bool has_new_text_need_build_bitmap = false; public: StbTrueTypeBackend() { - int temp_size = MAX_FONT_SIZE * 2; - this->temp_size = temp_size; + this->temp_size = MAX_FONT_SIZE * 2; this->single_word_bitmap = std::make_unique>(); - this->single_word_bitmap->alloc_or_resize_to(temp_size * temp_size); + this->single_word_bitmap->alloc_or_resize_to(this->temp_size * this->temp_size); memset(this->single_word_bitmap->host(), 0, this->single_word_bitmap->bytes()); } - virtual ~StbTrueTypeBackend() {} + ~StbTrueTypeBackend() override = default; - virtual vector split_utf8(const char *utf8_text) override + vector split_utf8(const char *utf8_text) override { vector output; output.reserve(std::char_traits::length(utf8_text)); - unsigned char *str = (unsigned char *)utf8_text; - unsigned int c; + const auto *str = reinterpret_cast(utf8_text); while (*str) { - if (!(*str & 0x80)) - output.emplace_back(*str++); - else if ((*str & 0xe0) == 0xc0) - { - if (*str < 0xc2) - return {}; - c = (*str++ & 0x1f) << 6; - if ((*str & 0xc0) != 0x80) - return {}; - output.emplace_back(c + (*str++ & 0x3f)); - } - else if ((*str & 0xf0) == 0xe0) - { - if (*str == 0xe0 && (str[1] < 0xa0 || str[1] > 0xbf)) - return {}; - if (*str == 0xed && str[1] > 0x9f) - return {}; // str[1] < 0x80 is checked below - c = (*str++ & 0x0f) << 12; - if ((*str & 0xc0) != 0x80) - return {}; - c += (*str++ & 0x3f) << 6; - if ((*str & 0xc0) != 0x80) - return {}; - output.emplace_back(c + (*str++ & 0x3f)); - } - else if ((*str & 0xf8) == 0xf0) + unsigned long int codepoint = 0; + if (!decode_utf8_codepoint(str, codepoint)) { - if (*str > 0xf4) - return {}; - if (*str == 0xf0 && (str[1] < 0x90 || str[1] > 0xbf)) - return {}; - if (*str == 0xf4 && str[1] > 0x8f) - return {}; // str[1] < 0x80 is checked below - c = (*str++ & 0x07) << 18; - if ((*str & 0xc0) != 0x80) - return {}; - c += (*str++ & 0x3f) << 12; - if ((*str & 0xc0) != 0x80) - return {}; - c += (*str++ & 0x3f) << 6; - if ((*str & 0xc0) != 0x80) - return {}; - c += (*str++ & 0x3f); - // utf-8 encodings of values used in surrogate pairs are invalid - if ((c & 0xFFFFF800) == 0xD800) - return {}; - if (c >= 0x10000) - { - c -= 0x10000; - output.emplace_back(0xD800 | (0x3ff & (c >> 10))); - output.emplace_back(0xDC00 | (0x3ff & (c))); - } - } - else return {}; + } + output.emplace_back(codepoint); } return output; } @@ -419,75 +640,198 @@ class StbTrueTypeBackend : public TextBackend return font.get(); } - virtual std::tuple measure_text(const std::vector &words, unsigned int font_size, - const char *font_name) override + struct GlyphMeasure { - int draw_x = 0; - int xadvance = font_size * 0.1; - int min_y = font_size; - int max_b = 0; - auto font_and_size = concat_font_name_size(font_name, font_size); - TrueTypeFontInternal *font_ptr = nullptr; - auto &word_map = this->glyph_sets[font_and_size]; - for (auto &word : words) + int width; + int height; + int advance; + int y0; + float scale; + }; + + bool measure_word(StbWordMetaMapperImpl &word_map, TrueTypeFontInternal *&font_ptr, const char *font_name, + unsigned long int word, unsigned int font_size, GlyphMeasure &measure) + { + if (auto iter = word_map.find(word); iter != word_map.end()) { - int w, h; - int advance = 0; - float scale = 0; - int y0 = 0; - auto iter = word_map.find(word); - if (iter == word_map.end()) + measure.width = iter->second.x1 - iter->second.x0; + measure.height = iter->second.y1 - iter->second.y0; + measure.advance = iter->second.advance; + measure.scale = iter->second.scale; + measure.y0 = iter->second.y0; + return true; + } + + if (font_ptr == nullptr) + { + font_ptr = get_font(font_name); + if (font_ptr == nullptr) { - if (font_ptr == nullptr) - { - font_ptr = get_font(font_name); - if (font_ptr == nullptr) - return make_tuple(-1, -1, -1); - } - - auto pfont = &font_ptr->font; - int x0, x1, y1; - int glyph = stbtt_FindGlyphIndex(pfont, word); - scale = stbtt_ScaleForPixelHeight(pfont, font_size); - stbtt_GetGlyphHMetrics(pfont, glyph, &advance, nullptr); - stbtt_GetGlyphBitmapBoxSubpixel(pfont, glyph, scale, scale, 0, 0, &x0, &y0, &x1, &y1); - - w = x1 - x0; - h = y1 - y0; + return false; } - else + } + + auto pfont = &font_ptr->font; + int x0; + int x1; + int y1; + int glyph = stbtt_FindGlyphIndex(pfont, static_cast(word)); + measure.scale = stbtt_ScaleForPixelHeight(pfont, static_cast(font_size)); + stbtt_GetGlyphHMetrics(pfont, glyph, &measure.advance, nullptr); + stbtt_GetGlyphBitmapBoxSubpixel(pfont, glyph, measure.scale, measure.scale, 0, 0, &x0, &measure.y0, &x1, &y1); + measure.width = x1 - x0; + measure.height = y1 - measure.y0; + return true; + } + + void add_missing_glyph(StbWordMetaMapperImpl &glyph_map, const stbtt_fontinfo *pfont, unsigned long int word, + int font_size) const + { + if (glyph_map.find(word) != glyph_map.end()) + { + return; + } + + int x0; + int y0; + int x1; + int y1; + int advance; + int glyph = stbtt_FindGlyphIndex(pfont, static_cast(word)); + float scale = stbtt_ScaleForPixelHeight(pfont, static_cast(font_size)); + stbtt_GetGlyphHMetrics(pfont, glyph, &advance, nullptr); + stbtt_GetGlyphBitmapBoxSubpixel(pfont, glyph, scale, scale, 0, 0, &x0, &y0, &x1, &y1); + glyph_map.insert(make_pair(word, StbWordMeta(x0, y0, x1, y1, scale, advance, glyph, 0))); + } + + void add_default_glyphs(StbWordMetaMapperImpl &glyph_map, const stbtt_fontinfo *pfont, int font_size) const + { + const char *default_words + = R"stb(ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789:-&./&^%$#@!+=\[];,'"?` )stb"; + + for (const char *pword = default_words; *pword; ++pword) + { + unsigned long int word = static_cast(*pword) | (1UL << 32); + add_missing_glyph(glyph_map, pfont, word, font_size); + } + } + + void add_pending_glyphs(const string &font_name_and_size, const vector &words) + { + auto &glyph_map = this->glyph_sets[font_name_and_size]; + auto font_key = parse_font_name_and_size(font_name_and_size); + auto font = get_font(font_key.name.c_str()); + if (font == nullptr) + { + return; + } + + auto pfont = &font->font; + for (const auto &word : words) + { + add_missing_glyph(glyph_map, pfont, word, font_key.size); + } + add_default_glyphs(glyph_map, pfont, font_key.size); + } + + std::tuple compute_bitmap_size() const + { + int max_glyph_height = 0; + int total_glyph_width = 0; + + for (const auto &[fontNameAndSize, glyphMap] : this->glyph_sets) + { + for (const auto &[codePoint, glyph] : glyphMap) { - w = iter->second.x1 - iter->second.x0; - h = iter->second.y1 - iter->second.y0; - advance = iter->second.advance; - scale = iter->second.scale; - y0 = iter->second.y0; + int w = glyph.x1 - glyph.x0; + int h = glyph.y1 - glyph.y0; + max_glyph_height = std::max(max_glyph_height, h); + total_glyph_width += w; } + } + return make_tuple(total_glyph_width, max_glyph_height); + } + + void resize_text_bitmap(int width, int height) + { + if (this->text_bitmap == nullptr) + { + this->text_bitmap = std::make_unique>(); + } + + this->text_bitmap_width = width; + this->text_bitmap_height = height; + this->text_bitmap->alloc_or_resize_to(width * height); + memset(this->text_bitmap->host(), 0, this->text_bitmap->bytes()); + } + + void rasterize_glyphs(const string &font_name_and_size, StbWordMetaMapperImpl &glyph_map, int &offset_x) + { + auto font_key = parse_font_name_and_size(font_name_and_size); + auto font = get_font(font_key.name.c_str()); + if (font == nullptr) + { + return; + } + + auto pfont = &font->font; + for (auto &[codePoint, glyph] : glyph_map) + { + int w = glyph.x1 - glyph.x0; + int h = glyph.y1 - glyph.y0; if (w < 1 || h < 1) { - draw_x += advance * scale * 0.5; + continue; } - else + + glyph.offset_x = offset_x; + stbtt_vertex *vertices = nullptr; + int num_verts = stbtt_GetGlyphShape(pfont, glyph.glyph, &vertices); + stbtt__bitmap gbm; + gbm.pixels = this->text_bitmap->host() + offset_x; + gbm.w = w; + gbm.h = h; + gbm.stride = this->text_bitmap_width; + stbtt_Rasterize(&gbm, 0.35f, vertices, num_verts, glyph.scale, glyph.scale, 0, 0, glyph.x0, glyph.y0, 1, + pfont->userdata); + STBTT_free(vertices, pfont->userdata); + offset_x += w; + } + } + + std::tuple measure_text(const std::vector &words, unsigned int font_size, + const char *font_name) override + { + int draw_x = 0; + int min_y = font_size; + int max_b = 0; + auto font_and_size = concat_font_name_size(font_name, font_size); + TrueTypeFontInternal *font_ptr = nullptr; + auto &word_map = this->glyph_sets[font_and_size]; + for (const auto &word : words) + { + GlyphMeasure measure{}; + if (!measure_word(word_map, font_ptr, font_name, word, font_size, measure)) { - draw_x += w + std::max(1.0f, advance * scale / 5.0f); + return make_tuple(-1, -1, -1); } - int y = font_size + y0; + draw_x += stb_word_xadvance(measure.width, measure.height, measure.scale, measure.advance); + + int y = font_size + measure.y0; min_y = min(min_y, y); - max_b = max(max_b, y + h); + max_b = max(max_b, y + measure.height); } - draw_x -= xadvance; return make_tuple(draw_x, max_b - min_y, min_y); } - virtual void add_build_text(const std::vector &words, unsigned int font_size, - const char *font) override + void add_build_text(const std::vector &words, unsigned int font_size, const char *font) override { auto font_and_size = concat_font_name_size(font, font_size); auto &maps = build_use_textes[font_and_size]; auto &glyph_map = this->glyph_sets[font_and_size]; - for (auto &word : words) + for (const auto &word : words) { if (glyph_map.find(word) != glyph_map.end()) continue; @@ -496,7 +840,7 @@ class StbTrueTypeBackend : public TextBackend } } - virtual WordMetaMapper *query(const char *font, int font_size) override + WordMetaMapper *query(const char *font, int font_size) override { auto font_and_size = concat_font_name_size(font, font_size); auto iter = this->glyph_sets.find(font_and_size); @@ -505,10 +849,8 @@ class StbTrueTypeBackend : public TextBackend return &iter->second; } - virtual void build_bitmap(void *_stream) override + void build_bitmap(cudaStream_t stream) override { - cudaStream_t stream = (cudaStream_t)_stream; - // 1. collect all word shape. if (!has_new_text_need_build_bitmap) { @@ -517,132 +859,45 @@ class StbTrueTypeBackend : public TextBackend return; } - for (auto &textes : build_use_textes) - { - auto &glyph_map = this->glyph_sets[textes.first]; - auto &words = textes.second; - string font_name_and_size = textes.first; - int p = font_name_and_size.rfind(' '); - font_name_and_size[p] = 0; - int font_size = std::atoi(font_name_and_size.c_str() + p + 1); - const char *font_name = font_name_and_size.c_str(); - auto font = get_font(font_name); - if (font == nullptr) - continue; - - auto pfont = &font->font; - int x0, y0, x1, y1, advance; - - for (auto &word : words) - { - if (glyph_map.find(word) != glyph_map.end()) - continue; - - int glyph = stbtt_FindGlyphIndex(pfont, word); - float scale = stbtt_ScaleForPixelHeight(pfont, font_size); - stbtt_GetGlyphHMetrics(pfont, glyph, &advance, nullptr); - stbtt_GetGlyphBitmapBoxSubpixel(pfont, glyph, scale, scale, 0, 0, &x0, &y0, &x1, &y1); - glyph_map.insert(make_pair(word, StbWordMeta(x0, y0, x1, y1, scale, advance, glyph, 0))); - } - - const char *default_words - = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789:-&./&^%$#@!+=\\[];,'\"?` "; - const char *pword = default_words; - for (; *pword; ++pword) - { - unsigned long int word = (unsigned int)*pword | (1ul << 32); - if (glyph_map.find(word) != glyph_map.end()) - continue; - - int glyph = stbtt_FindGlyphIndex(pfont, word); - float scale = stbtt_ScaleForPixelHeight(pfont, font_size); - stbtt_GetGlyphHMetrics(pfont, glyph, &advance, nullptr); - stbtt_GetGlyphBitmapBoxSubpixel(pfont, glyph, scale, scale, 0, 0, &x0, &y0, &x1, &y1); - glyph_map.insert(make_pair(word, StbWordMeta(x0, y0, x1, y1, scale, advance, glyph, 0))); - } - } - - int max_glyph_height = 0; - int max_glyph_width = 0; - int total_glyph_width = 0; - for (auto &map : this->glyph_sets) + for (const auto &[fontNameAndSize, texts] : build_use_textes) { - for (auto &item : map.second) - { - int w = item.second.x1 - item.second.x0, h = item.second.y1 - item.second.y0; - max_glyph_width = std::max(max_glyph_width, w); - max_glyph_height = std::max(max_glyph_height, h); - total_glyph_width += w; - } + add_pending_glyphs(fontNameAndSize, texts); } - if (this->text_bitmap == nullptr) - this->text_bitmap = std::make_unique>(); - this->text_bitmap_width = total_glyph_width; - this->text_bitmap_height = max_glyph_height; - this->text_bitmap->alloc_or_resize_to(total_glyph_width * max_glyph_height); - memset(this->text_bitmap->host(), 0, this->text_bitmap->bytes()); + auto [total_glyph_width, max_glyph_height] = compute_bitmap_size(); + resize_text_bitmap(total_glyph_width, max_glyph_height); // Rasterize word to bitmap int offset_x = 0; - for (auto &map : this->glyph_sets) + for (auto &[fontNameAndSize, glyphMap] : this->glyph_sets) { - string font_name_and_size = map.first; - int p = font_name_and_size.rfind(' '); - font_name_and_size[p] = 0; - const char *font_name = font_name_and_size.c_str(); - auto font = get_font(font_name); - if (font == nullptr) - continue; - - auto pfont = &font->font; - for (auto &item : map.second) - { - auto &glyph = item.second; - - int w = glyph.x1 - glyph.x0, h = glyph.y1 - glyph.y0; - if (w < 1 || h < 1) - continue; - - glyph.offset_x = offset_x; - stbtt_vertex *vertices = nullptr; - int num_verts = stbtt_GetGlyphShape(pfont, glyph.glyph, &vertices); - stbtt__bitmap gbm; - gbm.pixels = this->text_bitmap->host() + offset_x; - gbm.w = w; - gbm.h = h; - gbm.stride = this->text_bitmap_width; - stbtt_Rasterize(&gbm, 0.35f, vertices, num_verts, glyph.scale, glyph.scale, 0, 0, glyph.x0, glyph.y0, 1, - pfont->userdata); - STBTT_free(vertices, pfont->userdata); - offset_x += w; - } + rasterize_glyphs(fontNameAndSize, glyphMap, offset_x); } this->text_bitmap->copy_host_to_device(stream); this->has_new_text_need_build_bitmap = false; this->build_use_textes.clear(); } - virtual unsigned char *bitmap_device_pointer() const override + unsigned char *bitmap_device_pointer() const override { if (!this->text_bitmap) return nullptr; return this->text_bitmap->device(); } - virtual int bitmap_width() const override + int bitmap_width() const override { return this->text_bitmap_width; } - virtual int compute_y_offset(int max_glyph_height, int h, WordMeta *word, int font_size) const override + int compute_y_offset(int max_glyph_height, int h, WordMeta *word, int font_size) const override { (void)max_glyph_height; (void)h; return font_size + ((StbWordMeta *)word)->y0; } - virtual int uniform_font_size(int size) const override + int uniform_font_size(int size) const override { return size * 3; } diff --git a/src/cvcuda/priv/legacy/textbackend/stb_truetype.h b/src/cvcuda/priv/legacy/textbackend/stb_truetype.h index 80cc54a3c..a7935b2df 100644 --- a/src/cvcuda/priv/legacy/textbackend/stb_truetype.h +++ b/src/cvcuda/priv/legacy/textbackend/stb_truetype.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -2081,7 +2081,18 @@ static int stbtt__run_charstring(const stbtt_fontinfo *info, int glyph_index, st case 0x07: // vlineto if (sp < 1) return STBTT__CSERR("vlineto stack"); - goto vlineto; + for (;;) + { + if (i >= sp) + break; + stbtt__csctx_rline_to(c, 0, s[i]); + i++; + if (i >= sp) + break; + stbtt__csctx_rline_to(c, s[i], 0); + i++; + } + break; case 0x06: // hlineto if (sp < 1) return STBTT__CSERR("hlineto stack"); @@ -2091,7 +2102,6 @@ static int stbtt__run_charstring(const stbtt_fontinfo *info, int glyph_index, st break; stbtt__csctx_rline_to(c, s[i], 0); i++; -vlineto: if (i >= sp) break; stbtt__csctx_rline_to(c, 0, s[i]); @@ -2102,7 +2112,18 @@ static int stbtt__run_charstring(const stbtt_fontinfo *info, int glyph_index, st case 0x1F: // hvcurveto if (sp < 4) return STBTT__CSERR("hvcurveto stack"); - goto hvcurveto; + for (;;) + { + if (i + 3 >= sp) + break; + stbtt__csctx_rccurve_to(c, s[i], 0, s[i + 1], s[i + 2], (sp - i == 5) ? s[i + 4] : 0.0f, s[i + 3]); + i += 4; + if (i + 3 >= sp) + break; + stbtt__csctx_rccurve_to(c, 0, s[i], s[i + 1], s[i + 2], s[i + 3], (sp - i == 5) ? s[i + 4] : 0.0f); + i += 4; + } + break; case 0x1E: // vhcurveto if (sp < 4) return STBTT__CSERR("vhcurveto stack"); @@ -2112,7 +2133,6 @@ static int stbtt__run_charstring(const stbtt_fontinfo *info, int glyph_index, st break; stbtt__csctx_rccurve_to(c, 0, s[i], s[i + 1], s[i + 2], s[i + 3], (sp - i == 5) ? s[i + 4] : 0.0f); i += 4; -hvcurveto: if (i + 3 >= sp) break; stbtt__csctx_rccurve_to(c, s[i], 0, s[i + 1], s[i + 2], (sp - i == 5) ? s[i + 4] : 0.0f, s[i + 3]); @@ -3074,7 +3094,15 @@ static void stbtt__rasterize_sorted_edges(stbtt__bitmap *result, stbtt__edge *e, STBTT__NOTUSED(vsubsample); if (result->w > 64) - scanline = (float *)STBTT_malloc((result->w * 2 + 1) * sizeof(float), userdata); + { + size_t scanline_len = (size_t)result->w * 2 + 1; + if (scanline_len > ((size_t)-1) / sizeof(float)) + return; + + scanline = (float *)STBTT_malloc(scanline_len * sizeof(float), userdata); + if (scanline == NULL) + return; + } else scanline = scanline_data; diff --git a/src/cvcuda/priv/legacy/threshold.cu b/src/cvcuda/priv/legacy/threshold.cu index c002d0121..41bfeef6c 100644 --- a/src/cvcuda/priv/legacy/threshold.cu +++ b/src/cvcuda/priv/legacy/threshold.cu @@ -1,4 +1,4 @@ -/* Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +/* Copyright (c) 2021-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * * SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES * SPDX-License-Identifier: Apache-2.0 @@ -22,9 +22,12 @@ #include "CvCudaLegacyHelpers.hpp" #include "CvCudaUtils.cuh" -#include "cub/cub.cuh" #include "threshold_util.cuh" +#include + +#include + using namespace nvcv::legacy::helpers; using namespace nvcv::legacy::cuda_op; @@ -455,15 +458,21 @@ __global__ void hist_kernel(Tensor3DWrap img, int *histogram, in if (h < rows) { - if (w + 16 > cols) + uchar *ptr = img.ptr(batch, h, w); + // Use a 16-byte vectorized load when the pointer is aligned and 16 bytes remain. + // When the tensor stride does not guarantee 16-byte alignment, or fewer than + // 16 columns remain, fall back to per-element loads to avoid out-of-bounds + // reads and misaligned-access crashes. + if (w + 16 <= cols && (reinterpret_cast(ptr) % 16) == 0) { - for (int i = w; i < cols; i++) atomicAdd(&hist[*img.ptr(batch, h, i)], 1); + int4 src = *((int4 *)ptr); + uchar *inval = reinterpret_cast((void *)&src); + for (int i = 0; i < 16; i++) atomicAdd(&hist[inval[i]], 1); } else { - int4 src = *((int4 *)img.ptr(batch, h, w)); - uchar *inval = reinterpret_cast((void *)&src); - for (int i = 0; i < 16; i++) atomicAdd(&hist[inval[i]], 1); + int end = min(w + 16, cols); + for (int i = w; i < end; i++) atomicAdd(&hist[*img.ptr(batch, h, i)], 1); } } __syncthreads(); @@ -699,6 +708,9 @@ ErrorCode thresholdDispatch(const nvcv::TensorDataStridedCuda &input, const nvcv int batch, int rows, int cols, int channel, NVCVThresholdType type, DataType data_type, cudaStream_t stream) { + (void)data_type; + using vectype = nvcv::cuda::MakeType; + int size = rows * cols * channel; Tensor1DWrap thresh(_thresh); Tensor1DWrap maxval(_maxval); @@ -714,7 +726,19 @@ ErrorCode thresholdDispatch(const nvcv::TensorDataStridedCuda &input, const nvcv return ErrorCode::INVALID_PARAMETER; } - using vectype = nvcv::cuda::MakeType; + if constexpr (N > 1) + { + std::uintptr_t packAlignmentBits + = reinterpret_cast(input.basePtr()) | reinterpret_cast(output.basePtr()) + | static_cast(inAccess->rowStride()) | static_cast(outAccess->rowStride()) + | static_cast(inAccess->sampleStride()) + | static_cast(outAccess->sampleStride()); + + if ((packAlignmentBits & (alignof(vectype) - 1)) != 0) + return thresholdDispatch(input, output, _thresh, _maxval, batch, rows, cols, channel, type, data_type, + stream); + } + using StrideType = int32_t; auto src_ptr = CreateTensorWrapNHWC(input); auto dst_ptr = CreateTensorWrapNHWC(output); @@ -724,13 +748,13 @@ ErrorCode thresholdDispatch(const nvcv::TensorDataStridedCuda &input, const nvcv switch (type) { case NVCV_THRESH_BINARY: - if (data_type == kCV_32F || data_type == kCV_64F) + if constexpr (std::is_floating_point_v) Binary_Generic<<>>(src_ptr, dst_ptr, thresh, maxval, rows, cols, channel); else Binary_overflow<<>>(src_ptr, dst_ptr, thresh, maxval, rows, cols, channel); break; case NVCV_THRESH_BINARY_INV: - if (data_type == kCV_32F || data_type == kCV_64F) + if constexpr (std::is_floating_point_v) BinaryInv_Generic <<>>(src_ptr, dst_ptr, thresh, maxval, rows, cols, channel); else @@ -738,19 +762,19 @@ ErrorCode thresholdDispatch(const nvcv::TensorDataStridedCuda &input, const nvcv <<>>(src_ptr, dst_ptr, thresh, maxval, rows, cols, channel); break; case NVCV_THRESH_TRUNC: - if (data_type == kCV_32F || data_type == kCV_64F) + if constexpr (std::is_floating_point_v) Trunc_Generic<<>>(src_ptr, dst_ptr, thresh, rows, cols, channel); else Trunc_overflow<<>>(src_ptr, dst_ptr, thresh, rows, cols, channel); break; case NVCV_THRESH_TOZERO: - if (data_type == kCV_32F || data_type == kCV_64F) + if constexpr (std::is_floating_point_v) Tozero_Generic<<>>(src_ptr, dst_ptr, thresh, rows, cols, channel); else Tozero_overflow<<>>(src_ptr, dst_ptr, thresh, rows, cols, channel); break; default: //NVCV_THRESH_TOZERO_INV - if (data_type == kCV_32F || data_type == kCV_64F) + if constexpr (std::is_floating_point_v) TozeroInv_Generic<<>>(src_ptr, dst_ptr, thresh, rows, cols, channel); else TozeroInv_overflow<<>>(src_ptr, dst_ptr, thresh, rows, cols, channel); @@ -771,7 +795,7 @@ ErrorCode thresholdScale(const nvcv::TensorDataStridedCuda &input, const nvcv::T if (stride % 4 == 0) { - if (std::is_same::value) + if constexpr (std::is_same_v) return thresholdDispatch(input, output, threshold, maxval, batch, rows, cols, channel, type, data_type, stream); else @@ -786,6 +810,136 @@ ErrorCode thresholdScale(const nvcv::TensorDataStridedCuda &input, const nvcv::T stream); } +template +__device__ __forceinline__ T ThresholdOverflowValue(T inval, double th, double maxv, NVCVThresholdType type) +{ + T maxType = TypeTraits::max; + T minType = TypeTraits::min; + int imaxval = round(maxv); + T maxval = nvcv::cuda::SaturateCast(imaxval); + int ithresh = floor(th); + + switch (type) + { + case NVCV_THRESH_BINARY: + if (ithresh >= minType && ithresh <= maxType) + { + T thresh = (T)ithresh; + return inval > thresh ? maxval : 0; + } + return ithresh < minType ? maxval : 0; + case NVCV_THRESH_BINARY_INV: + if (ithresh >= minType && ithresh <= maxType) + { + T thresh = (T)ithresh; + return inval > thresh ? 0 : maxval; + } + return ithresh < minType ? 0 : maxval; + case NVCV_THRESH_TRUNC: + if (ithresh >= minType && ithresh <= maxType) + { + T thresh = (T)ithresh; + return inval > thresh ? thresh : inval; + } + return ithresh < minType ? minType : inval; + case NVCV_THRESH_TOZERO: + if (ithresh >= minType && ithresh <= maxType) + { + T thresh = (T)ithresh; + return inval > thresh ? inval : 0; + } + return ithresh < minType ? inval : 0; + default: // NVCV_THRESH_TOZERO_INV + if (ithresh >= minType && ithresh <= maxType) + { + T thresh = (T)ithresh; + return inval > thresh ? 0 : inval; + } + return ithresh < minType ? 0 : inval; + } +} + +template +__device__ __forceinline__ T ThresholdGenericValue(T inval, double th, double maxv, NVCVThresholdType type) +{ + T thresh = (T)th; + T maxval = (T)maxv; + + switch (type) + { + case NVCV_THRESH_BINARY: + return inval > thresh ? maxval : 0; + case NVCV_THRESH_BINARY_INV: + return inval > thresh ? 0 : maxval; + case NVCV_THRESH_TRUNC: + return inval > thresh ? thresh : inval; + case NVCV_THRESH_TOZERO: + return inval > thresh ? inval : 0; + default: // NVCV_THRESH_TOZERO_INV + return inval > thresh ? 0 : inval; + } +} + +template +__global__ void ThresholdPlanarTensor(SrcWrap src, DstWrap dst, Tensor1DWrap _thresh, + Tensor1DWrap _maxval, int rows, int cols, int channels, + NVCVThresholdType type, DataType data_type) +{ + int globalid = blockIdx.x * blockDim.x + threadIdx.x; + if (globalid >= rows * cols) + return; + + int batch = blockIdx.z / channels; + int plane = blockIdx.z % channels; + int y = globalid / cols; + int x = globalid % cols; + + T inval = *src.ptr(batch, plane, y, x); + T out = (data_type == kCV_32F || data_type == kCV_64F) + ? ThresholdGenericValue(inval, _thresh[batch], _maxval[batch], type) + : ThresholdOverflowValue(inval, _thresh[batch], _maxval[batch], type); + *dst.ptr(batch, plane, y, x) = out; +} + +template +ErrorCode thresholdScalePlanar(const nvcv::TensorDataStridedCuda &input, const nvcv::TensorDataStridedCuda &output, + const nvcv::TensorDataStridedCuda &threshold, const nvcv::TensorDataStridedCuda &maxval, + int batch, int rows, int cols, int channels, NVCVThresholdType type, DataType data_type, + cudaStream_t stream) +{ + Tensor1DWrap thresh(threshold); + Tensor1DWrap maxv(maxval); + + auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(input); + auto outAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(output); + + auto outMaxStride = outAccess->sampleStride() * outAccess->numSamples(); + auto inMaxStride = inAccess->sampleStride() * inAccess->numSamples(); + if (std::max(outMaxStride, inMaxStride) > TypeTraits::max) + { + LOG_ERROR("Input or output size exceeds " << TypeTraits::max << ". Tensor is too large."); + return ErrorCode::INVALID_PARAMETER; + } + + const int64_t planarBatch = static_cast(batch) * channels; + if (planarBatch > 65535) + { + LOG_ERROR("Planar Threshold requires batch * channels <= 65535 (CUDA grid-z limit)"); + return ErrorCode::INVALID_PARAMETER; + } + + auto src_ptr = CreateTensorWrapNCHW(input); + auto dst_ptr = CreateTensorWrapNCHW(output); + + dim3 block(256); + dim3 grid(divUp(rows * cols, block.x), 1, batch * channels); + ThresholdPlanarTensor + <<>>(src_ptr, dst_ptr, thresh, maxv, rows, cols, channels, type, data_type); + + checkKernelErrors(); + return ErrorCode::SUCCESS; +} + static void getThreshVal_Triangle(const nvcv::TensorDataStridedCuda &inData, const nvcv::TensorDataStridedCuda &threshold, int *histogram, int rows, int cols, int batch, cudaStream_t stream) @@ -830,6 +984,7 @@ Threshold::Threshold(DataShape max_input_shape, DataShape max_output_shape, uint : CudaBaseOp(max_input_shape, max_output_shape) , m_histogram(nullptr) , m_type(type) + , m_maxBatchSize(maxBatchSize) { if (maxBatchSize < 0) { @@ -843,7 +998,7 @@ Threshold::Threshold(DataShape max_input_shape, DataShape max_output_shape, uint if (err != cudaSuccess) { LOG_ERROR("CUDA memory allocation error of size: " << sizeof(int) * 256 * maxBatchSize); - throw std::runtime_error("CUDA memory allocation error!"); + throw LegacyCudaAllocationError("CUDA memory allocation error!"); } } } @@ -878,6 +1033,28 @@ ErrorCode Threshold::infer(const TensorDataStridedCuda &inData, const TensorData return ErrorCode::INVALID_DATA_TYPE; } + const DataFormat input_format = GetLegacyDataFormat(inData.layout()); + const DataFormat output_format = GetLegacyDataFormat(outData.layout()); + + if (!(input_format == kNHWC || input_format == kHWC || input_format == kNCHW || input_format == kCHW)) + { + LOG_ERROR("Invalid input DataFormat " << input_format); + return ErrorCode::INVALID_DATA_FORMAT; + } + + if (!(output_format == kNHWC || output_format == kHWC || output_format == kNCHW || output_format == kCHW)) + { + LOG_ERROR("Invalid output DataFormat " << output_format); + return ErrorCode::INVALID_DATA_FORMAT; + } + + if (input_format != output_format) + { + LOG_ERROR("Invalid DataFormat between input (" << input_format << ") and output (" << output_format << ")"); + return ErrorCode::INVALID_DATA_FORMAT; + } + const bool isPlanar = (input_format == kNCHW || input_format == kCHW); + DataType thresh_data_type = GetLegacyDataType(thresh.dtype()); if (thresh_data_type != kCV_64F) { @@ -907,6 +1084,13 @@ ErrorCode Threshold::infer(const TensorDataStridedCuda &inData, const TensorData auto inAccess = TensorDataAccessStridedImagePlanar::Create(inData); NVCV_ASSERT(inAccess); + const int channels = inAccess->numChannels(); + if (channels > 4 || (isPlanar && channels == 2)) + { + LOG_ERROR("Invalid channel number " << channels); + return ErrorCode::INVALID_DATA_SHAPE; + } + if (m_automatic_thresh == ((uint32_t)NVCV_THRESH_OTSU | (uint32_t)NVCV_THRESH_TRIANGLE)) { LOG_ERROR("Invalid Threshold Type " << m_type); @@ -914,12 +1098,34 @@ ErrorCode Threshold::infer(const TensorDataStridedCuda &inData, const TensorData } m_type &= (uint32_t)NVCV_THRESH_MASK; - if (m_type & (m_type - 1) != 0) + if ((m_type & (m_type - 1)) != 0) { LOG_ERROR("Invalid Threhold Type " << m_type); return ErrorCode::INVALID_PARAMETER; } + const int batch = inAccess->numSamples(); + if (m_automatic_thresh != 0 && batch > m_maxBatchSize) + { + LOG_ERROR("Input batch exceeds maxBatchSize"); + return ErrorCode::INVALID_PARAMETER; + } + + std::optional> planarViews; + const TensorDataStridedCuda *workInData = &inData; + const TensorDataStridedCuda *workOutData = &outData; + int workBatch = batch; + int workChannels = channels; + if (isPlanar && m_automatic_thresh != 0) + { + planarViews = cvcuda::priv::PlanarSingleChannelViews(inData, outData); + NVCV_ASSERT(planarViews); + workInData = &planarViews->first; + workOutData = &planarViews->second; + workBatch = batch * channels; + workChannels = 1; + } + if (m_automatic_thresh == NVCV_THRESH_OTSU) { if (in_data_type != kCV_8U) @@ -932,12 +1138,12 @@ ErrorCode Threshold::infer(const TensorDataStridedCuda &inData, const TensorData LOG_ERROR("Only support 1 channel"); return ErrorCode::INVALID_DATA_FORMAT; } - if (inAccess->sampleStride() * inAccess->numSamples() > TypeTraits::max) + if (inAccess->sampleStride() * batch > TypeTraits::max) { LOG_ERROR("Input size exceeds " << TypeTraits::max << ". Tensor is too large."); return ErrorCode::INVALID_PARAMETER; } - getThreshVal_Otsu(inData, thresh, m_histogram, inAccess->numRows(), inAccess->numCols(), inAccess->numSamples(), + getThreshVal_Otsu(*workInData, thresh, m_histogram, inAccess->numRows(), inAccess->numCols(), workBatch, stream); } else if (m_automatic_thresh == NVCV_THRESH_TRIANGLE) @@ -952,13 +1158,13 @@ ErrorCode Threshold::infer(const TensorDataStridedCuda &inData, const TensorData LOG_ERROR("Only support 1 channel"); return ErrorCode::INVALID_DATA_FORMAT; } - if (inAccess->sampleStride() * inAccess->numSamples() > TypeTraits::max) + if (inAccess->sampleStride() * batch > TypeTraits::max) { LOG_ERROR("Input size exceeds " << TypeTraits::max << ". Tensor is too large."); return ErrorCode::INVALID_PARAMETER; } - getThreshVal_Triangle(inData, thresh, m_histogram, inAccess->numRows(), inAccess->numCols(), - inAccess->numSamples(), stream); + getThreshVal_Triangle(*workInData, thresh, m_histogram, inAccess->numRows(), inAccess->numCols(), workBatch, + stream); } typedef ErrorCode (*threshold_t)(const TensorDataStridedCuda &input, const TensorDataStridedCuda &output, @@ -971,8 +1177,22 @@ ErrorCode Threshold::infer(const TensorDataStridedCuda &inData, const TensorData threshold_t func = funcs[in_data_type]; NVCVThresholdType th_type = NVCVThresholdType(m_type); - return func(inData, outData, thresh, maxval, inAccess->numSamples(), inAccess->numRows(), inAccess->numCols(), - inAccess->numChannels(), th_type, in_data_type, stream); + if (isPlanar && m_automatic_thresh == 0) + { + typedef ErrorCode (*threshold_planar_t)( + const TensorDataStridedCuda &input, const TensorDataStridedCuda &output, + const TensorDataStridedCuda &threshold, const TensorDataStridedCuda &maxval, int batch, int rows, int cols, + int channels, NVCVThresholdType type, DataType data_type, cudaStream_t stream); + static const threshold_planar_t planarFuncs[7] = {thresholdScalePlanar, 0, thresholdScalePlanar, + thresholdScalePlanar, 0, thresholdScalePlanar, + thresholdScalePlanar}; + threshold_planar_t planarFunc = planarFuncs[in_data_type]; + return planarFunc(inData, outData, thresh, maxval, batch, inAccess->numRows(), inAccess->numCols(), channels, + th_type, in_data_type, stream); + } + + return func(*workInData, *workOutData, thresh, maxval, workBatch, inAccess->numRows(), inAccess->numCols(), + workChannels, th_type, in_data_type, stream); } } // namespace nvcv::legacy::cuda_op diff --git a/src/cvcuda/priv/legacy/threshold_util.cu b/src/cvcuda/priv/legacy/threshold_util.cu index cb50f560a..0e705b4df 100644 --- a/src/cvcuda/priv/legacy/threshold_util.cu +++ b/src/cvcuda/priv/legacy/threshold_util.cu @@ -1,4 +1,4 @@ -/* Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +/* Copyright (c) 2021-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * * SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES * SPDX-License-Identifier: Apache-2.0 @@ -19,7 +19,6 @@ */ #include "CvCudaUtils.cuh" -#include "cub/cub.cuh" __global__ void triangle_cal(int *histogram, nvcv::cuda::Tensor1DWrap thresh) { diff --git a/src/cvcuda/priv/legacy/threshold_util.cuh b/src/cvcuda/priv/legacy/threshold_util.cuh index 6e34739cc..211fb98fc 100644 --- a/src/cvcuda/priv/legacy/threshold_util.cuh +++ b/src/cvcuda/priv/legacy/threshold_util.cuh @@ -1,4 +1,4 @@ -/* Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +/* Copyright (c) 2021-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * * SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES * SPDX-License-Identifier: Apache-2.0 @@ -22,7 +22,39 @@ #define THRESHOLD_UTILS_CUH #include "CvCudaUtils.cuh" -#include "cub/cub.cuh" + +#include + +template +__device__ __forceinline__ P LoadPacked(const T *ptr) +{ + static_assert(sizeof(P) % sizeof(T) == 0); + + if (reinterpret_cast(ptr) % alignof(P) == 0) + return *reinterpret_cast(ptr); + + P value; + T *elements = reinterpret_cast(&value); +#pragma unroll + for (int i = 0; i < sizeof(P) / sizeof(T); ++i) elements[i] = ptr[i]; + return value; +} + +template +__device__ __forceinline__ void StorePacked(T *ptr, P value) +{ + static_assert(sizeof(P) % sizeof(T) == 0); + + if (reinterpret_cast(ptr) % alignof(P) == 0) + { + *reinterpret_cast

(ptr) = value; + return; + } + + const T *elements = reinterpret_cast(&value); +#pragma unroll + for (int i = 0; i < sizeof(P) / sizeof(T); ++i) ptr[i] = elements[i]; +} __global__ void triangle_cal(int *histogram, nvcv::cuda::Tensor1DWrap thresh); diff --git a/src/cvcuda/priv/legacy/threshold_var_shape.cu b/src/cvcuda/priv/legacy/threshold_var_shape.cu index e0c865101..72c664d3a 100644 --- a/src/cvcuda/priv/legacy/threshold_var_shape.cu +++ b/src/cvcuda/priv/legacy/threshold_var_shape.cu @@ -1,4 +1,4 @@ -/* Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +/* Copyright (c) 2021-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * * SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES * SPDX-License-Identifier: Apache-2.0 @@ -22,15 +22,41 @@ #include "CvCudaLegacyHelpers.hpp" #include "CvCudaUtils.cuh" -#include "cub/cub.cuh" #include "threshold_util.cuh" +#include + using namespace nvcv::legacy::helpers; using namespace nvcv::legacy::cuda_op; using namespace nvcv::cuda; +constexpr int kU8BinaryElementsPerThread = sizeof(uint4) / sizeof(uchar); + +static __device__ __forceinline__ uint4 SetAllU8Pack(uchar value) +{ + unsigned int word = 0x01010101u * value; + uint4 out; + out.x = word; + out.y = word; + out.z = word; + out.w = word; + return out; +} + +static __device__ __forceinline__ uint4 BinaryThresholdU8Pack(uint4 in, uchar thresh, uchar maxval) +{ + uint4 out; + uchar *inval = reinterpret_cast(&in); + uchar *outval = reinterpret_cast(&out); + +#pragma unroll + for (int i = 0; i < kU8BinaryElementsPerThread; i++) outval[i] = inval[i] > thresh ? maxval : 0; + + return out; +} + template> __global__ void Binary_overflow(ImageBatchVarShapeWrapNHWC src, ImageBatchVarShapeWrapNHWC dst, Tensor1DWrap _thresh, Tensor1DWrap _maxval, @@ -64,7 +90,7 @@ __global__ void Binary_overflow(ImageBatchVarShapeWrapNHWC src, ImageBatchVar if (ithresh >= MIN && ithresh <= MAX) { T thresh = (T)ithresh; - P in = *((P *)src.ptr(batch, h, w, c)); + P in = LoadPacked

(src.ptr(batch, h, w, c)); T *inval = reinterpret_cast((void *)&in); T outval[4]; #pragma unroll @@ -73,18 +99,18 @@ __global__ void Binary_overflow(ImageBatchVarShapeWrapNHWC src, ImageBatchVar outval[i] = inval[i] > thresh ? maxval : 0; GetElement

(out, i) = outval[i]; } - *((P *)dst.ptr(batch, h, w, c)) = out; + StorePacked(dst.ptr(batch, h, w, c), out); return; } if (ithresh < MIN) { - out = SetAll

(maxval); - *((P *)dst.ptr(batch, h, w, c)) = out; + out = SetAll

(maxval); + StorePacked(dst.ptr(batch, h, w, c), out); return; } - out = SetAll

(0); - *((P *)dst.ptr(batch, h, w, c)) = out; + out = SetAll

(0); + StorePacked(dst.ptr(batch, h, w, c), out); } else { @@ -111,6 +137,99 @@ __global__ void Binary_overflow(ImageBatchVarShapeWrapNHWC src, ImageBatchVar } } +__global__ void Binary_overflow_u8_nix16(ImageBatchVarShapeWrapNHWC src, ImageBatchVarShapeWrapNHWC dst, + Tensor1DWrap _thresh, Tensor1DWrap _maxval, + int channel) +{ + int globalid = blockIdx.x * blockDim.x + threadIdx.x; + int batch = blockIdx.z; + int width = src.width(batch); + int height = src.height(batch); + int rowElems = width * channel; + if (rowElems == 0) + return; + + int threadCol = (rowElems + kU8BinaryElementsPerThread - 1) / kU8BinaryElementsPerThread; + int h = globalid / threadCol; + int elem = (globalid % threadCol) * kU8BinaryElementsPerThread; + if (h >= height || elem >= rowElems) + return; + + int imaxval = round(_maxval[batch]); + uchar maxval = nvcv::cuda::SaturateCast(imaxval); + int ithresh = floor(_thresh[batch]); + int loop = rowElems - elem; + int c = elem % channel; + int w = elem / channel; + uchar *srcRow = src.ptr(batch, h, w, c); + uchar *dstRow = dst.ptr(batch, h, w, c); + bool aligned + = ((reinterpret_cast(srcRow) | reinterpret_cast(dstRow)) & (alignof(uint4) - 1)) + == 0; + + if (loop >= kU8BinaryElementsPerThread) + { + uint4 out; + if (ithresh >= TypeTraits::min && ithresh <= TypeTraits::max) + { + uchar thresh = (uchar)ithresh; + if (aligned) + { + uint4 in = *(reinterpret_cast(srcRow)); + *(reinterpret_cast(dstRow)) = BinaryThresholdU8Pack(in, thresh, maxval); + } + else + { +#pragma unroll + for (int i = 0; i < kU8BinaryElementsPerThread; i++) + { + uchar inval = srcRow[i]; + dstRow[i] = inval > thresh ? maxval : 0; + } + } + return; + } + + uchar value; + if (ithresh < TypeTraits::min) + { + out = SetAllU8Pack(maxval); + value = maxval; + } + else + { + out = SetAllU8Pack(0); + value = 0; + } + + if (aligned) + *(reinterpret_cast(dstRow)) = out; + else + { +#pragma unroll + for (int i = 0; i < kU8BinaryElementsPerThread; i++) dstRow[i] = value; + } + } + else + { + if (ithresh >= TypeTraits::min && ithresh <= TypeTraits::max) + { + uchar thresh = (uchar)ithresh; +#pragma unroll + for (int i = 0; i < loop; i++) + { + uchar inval = srcRow[i]; + dstRow[i] = inval > thresh ? maxval : 0; + } + return; + } + + uchar out = ithresh < TypeTraits::min ? maxval : 0; +#pragma unroll + for (int i = 0; i < loop; i++) dstRow[i] = out; + } +} + template> __global__ void Binary_Generic(ImageBatchVarShapeWrapNHWC src, ImageBatchVarShapeWrapNHWC dst, Tensor1DWrap _thresh, Tensor1DWrap _maxval, @@ -136,7 +255,7 @@ __global__ void Binary_Generic(ImageBatchVarShapeWrapNHWC src, ImageBatchVarS if (loop >= cn) { P out; - P in = *((P *)src.ptr(batch, h, w, c)); + P in = LoadPacked

(src.ptr(batch, h, w, c)); T *inval = reinterpret_cast((void *)&in); T outval[4]; #pragma unroll @@ -145,7 +264,7 @@ __global__ void Binary_Generic(ImageBatchVarShapeWrapNHWC src, ImageBatchVarS outval[i] = inval[i] > thresh ? maxval : 0; GetElement

(out, i) = outval[i]; } - *((P *)dst.ptr(batch, h, w, c)) = out; + StorePacked(dst.ptr(batch, h, w, c), out); } else { @@ -191,7 +310,7 @@ __global__ void BinaryInv_overflow(ImageBatchVarShapeWrapNHWC src, ImageBatch if (ithresh >= MIN && ithresh <= MAX) { T thresh = (T)ithresh; - P in = *((P *)src.ptr(batch, h, w, c)); + P in = LoadPacked

(src.ptr(batch, h, w, c)); T *inval = reinterpret_cast((void *)&in); T outval[4]; #pragma unroll @@ -200,18 +319,18 @@ __global__ void BinaryInv_overflow(ImageBatchVarShapeWrapNHWC src, ImageBatch outval[i] = inval[i] > thresh ? 0 : maxval; GetElement

(out, i) = outval[i]; } - *((P *)dst.ptr(batch, h, w, c)) = out; + StorePacked(dst.ptr(batch, h, w, c), out); return; } if (ithresh < MIN) { - out = SetAll

(0); - *((P *)dst.ptr(batch, h, w, c)) = out; + out = SetAll

(0); + StorePacked(dst.ptr(batch, h, w, c), out); return; } - out = SetAll

(maxval); - *((P *)dst.ptr(batch, h, w, c)) = out; + out = SetAll

(maxval); + StorePacked(dst.ptr(batch, h, w, c), out); } else { @@ -260,7 +379,7 @@ __global__ void BinaryInv_Generic(ImageBatchVarShapeWrapNHWC src, ImageBatchV if (loop >= cn) { P out; - P in = *((P *)src.ptr(batch, h, w, c)); + P in = LoadPacked

(src.ptr(batch, h, w, c)); T *inval = reinterpret_cast((void *)&in); T outval[4]; #pragma unroll @@ -269,7 +388,7 @@ __global__ void BinaryInv_Generic(ImageBatchVarShapeWrapNHWC src, ImageBatchV outval[i] = inval[i] > thresh ? 0 : maxval; GetElement

(out, i) = outval[i]; } - *((P *)dst.ptr(batch, h, w, c)) = out; + StorePacked(dst.ptr(batch, h, w, c), out); } else { @@ -311,7 +430,7 @@ __global__ void Trunc_overflow(ImageBatchVarShapeWrapNHWC src, ImageBatchVarS if (ithresh >= MIN && ithresh <= MAX) { T thresh = (T)ithresh; - P in = *((P *)src.ptr(batch, h, w, c)); + P in = LoadPacked

(src.ptr(batch, h, w, c)); T *inval = reinterpret_cast((void *)&in); T outval[4]; #pragma unroll @@ -320,17 +439,17 @@ __global__ void Trunc_overflow(ImageBatchVarShapeWrapNHWC src, ImageBatchVarS outval[i] = inval[i] > thresh ? thresh : inval[i]; GetElement

(out, i) = outval[i]; } - *((P *)dst.ptr(batch, h, w, c)) = out; + StorePacked(dst.ptr(batch, h, w, c), out); return; } if (ithresh < MIN) { - out = SetAll

(MIN); - *((P *)dst.ptr(batch, h, w, c)) = out; + out = SetAll

(MIN); + StorePacked(dst.ptr(batch, h, w, c), out); return; } - *((P *)dst.ptr(batch, h, w, c)) = *((P *)src.ptr(batch, h, w, c)); + StorePacked(dst.ptr(batch, h, w, c), LoadPacked

(src.ptr(batch, h, w, c))); } else { @@ -380,7 +499,7 @@ __global__ void Trunc_Generic(ImageBatchVarShapeWrapNHWC src, ImageBatchVarSh if (loop >= cn) { P out; - P in = *((P *)src.ptr(batch, h, w, c)); + P in = LoadPacked

(src.ptr(batch, h, w, c)); T *inval = reinterpret_cast((void *)&in); T outval[4]; #pragma unroll @@ -389,7 +508,7 @@ __global__ void Trunc_Generic(ImageBatchVarShapeWrapNHWC src, ImageBatchVarSh outval[i] = inval[i] > thresh ? thresh : inval[i]; GetElement

(out, i) = outval[i]; } - *((P *)dst.ptr(batch, h, w, c)) = out; + StorePacked(dst.ptr(batch, h, w, c), out); } else { @@ -431,7 +550,7 @@ __global__ void Tozero_overflow(ImageBatchVarShapeWrapNHWC src, ImageBatchVar if (ithresh >= MIN && ithresh <= MAX) { T thresh = (T)ithresh; - P in = *((P *)src.ptr(batch, h, w, c)); + P in = LoadPacked

(src.ptr(batch, h, w, c)); T *inval = reinterpret_cast((void *)&in); T outval[4]; #pragma unroll @@ -440,17 +559,17 @@ __global__ void Tozero_overflow(ImageBatchVarShapeWrapNHWC src, ImageBatchVar outval[i] = inval[i] > thresh ? inval[i] : 0; GetElement

(out, i) = outval[i]; } - *((P *)dst.ptr(batch, h, w, c)) = out; + StorePacked(dst.ptr(batch, h, w, c), out); return; } if (ithresh < MIN) { - *((P *)dst.ptr(batch, h, w, c)) = *((P *)src.ptr(batch, h, w, c)); + StorePacked(dst.ptr(batch, h, w, c), LoadPacked

(src.ptr(batch, h, w, c))); return; } - out = SetAll

(0); - *((P *)dst.ptr(batch, h, w, c)) = out; + out = SetAll

(0); + StorePacked(dst.ptr(batch, h, w, c), out); } else { @@ -500,7 +619,7 @@ __global__ void Tozero_Generic(ImageBatchVarShapeWrapNHWC src, ImageBatchVarS if (loop >= cn) { P out; - P in = *((P *)src.ptr(batch, h, w, c)); + P in = LoadPacked

(src.ptr(batch, h, w, c)); T *inval = reinterpret_cast((void *)&in); T outval[4]; #pragma unroll @@ -509,7 +628,7 @@ __global__ void Tozero_Generic(ImageBatchVarShapeWrapNHWC src, ImageBatchVarS outval[i] = inval[i] > thresh ? inval[i] : 0; GetElement

(out, i) = outval[i]; } - *((P *)dst.ptr(batch, h, w, c)) = out; + StorePacked(dst.ptr(batch, h, w, c), out); } else { @@ -551,7 +670,7 @@ __global__ void TozeroInv_overflow(ImageBatchVarShapeWrapNHWC src, ImageBatch if (ithresh >= MIN && ithresh <= MAX) { T thresh = (T)ithresh; - P in = *((P *)src.ptr(batch, h, w, c)); + P in = LoadPacked

(src.ptr(batch, h, w, c)); T *inval = reinterpret_cast((void *)&in); T outval[4]; #pragma unroll @@ -560,17 +679,17 @@ __global__ void TozeroInv_overflow(ImageBatchVarShapeWrapNHWC src, ImageBatch outval[i] = inval[i] > thresh ? 0 : inval[i]; GetElement

(out, i) = outval[i]; } - *((P *)dst.ptr(batch, h, w, c)) = out; + StorePacked(dst.ptr(batch, h, w, c), out); return; } if (ithresh < MIN) { - out = SetAll

(0); - *((P *)dst.ptr(batch, h, w, c)) = out; + out = SetAll

(0); + StorePacked(dst.ptr(batch, h, w, c), out); return; } - *((P *)dst.ptr(batch, h, w, c)) = *((P *)src.ptr(batch, h, w, c)); + StorePacked(dst.ptr(batch, h, w, c), LoadPacked

(src.ptr(batch, h, w, c))); } else { @@ -620,7 +739,7 @@ __global__ void TozeroInv_Generic(ImageBatchVarShapeWrapNHWC src, ImageBatchV if (loop >= cn) { P out; - P in = *((P *)src.ptr(batch, h, w, c)); + P in = LoadPacked

(src.ptr(batch, h, w, c)); T *inval = reinterpret_cast((void *)&in); T outval[4]; #pragma unroll @@ -629,7 +748,7 @@ __global__ void TozeroInv_Generic(ImageBatchVarShapeWrapNHWC src, ImageBatchV outval[i] = inval[i] > thresh ? 0 : inval[i]; GetElement

(out, i) = outval[i]; } - *((P *)dst.ptr(batch, h, w, c)) = out; + StorePacked(dst.ptr(batch, h, w, c), out); } else { @@ -642,6 +761,100 @@ __global__ void TozeroInv_Generic(ImageBatchVarShapeWrapNHWC src, ImageBatchV } } +template +__device__ __forceinline__ T ThresholdOverflowValue(T inval, double th, double maxv, NVCVThresholdType type) +{ + T maxType = TypeTraits::max; + T minType = TypeTraits::min; + int imaxval = round(maxv); + T maxval = nvcv::cuda::SaturateCast(imaxval); + int ithresh = floor(th); + + switch (type) + { + case NVCV_THRESH_BINARY: + if (ithresh >= minType && ithresh <= maxType) + { + T thresh = (T)ithresh; + return inval > thresh ? maxval : 0; + } + return ithresh < minType ? maxval : 0; + case NVCV_THRESH_BINARY_INV: + if (ithresh >= minType && ithresh <= maxType) + { + T thresh = (T)ithresh; + return inval > thresh ? 0 : maxval; + } + return ithresh < minType ? 0 : maxval; + case NVCV_THRESH_TRUNC: + if (ithresh >= minType && ithresh <= maxType) + { + T thresh = (T)ithresh; + return inval > thresh ? thresh : inval; + } + return ithresh < minType ? minType : inval; + case NVCV_THRESH_TOZERO: + if (ithresh >= minType && ithresh <= maxType) + { + T thresh = (T)ithresh; + return inval > thresh ? inval : 0; + } + return ithresh < minType ? inval : 0; + default: // NVCV_THRESH_TOZERO_INV + if (ithresh >= minType && ithresh <= maxType) + { + T thresh = (T)ithresh; + return inval > thresh ? 0 : inval; + } + return ithresh < minType ? 0 : inval; + } +} + +template +__device__ __forceinline__ T ThresholdGenericValue(T inval, double th, double maxv, NVCVThresholdType type) +{ + T thresh = (T)th; + T maxval = (T)maxv; + + switch (type) + { + case NVCV_THRESH_BINARY: + return inval > thresh ? maxval : 0; + case NVCV_THRESH_BINARY_INV: + return inval > thresh ? 0 : maxval; + case NVCV_THRESH_TRUNC: + return inval > thresh ? thresh : inval; + case NVCV_THRESH_TOZERO: + return inval > thresh ? inval : 0; + default: // NVCV_THRESH_TOZERO_INV + return inval > thresh ? 0 : inval; + } +} + +template +__global__ void ThresholdPlanar(ImageBatchVarShapeWrap src, ImageBatchVarShapeWrap dst, + Tensor1DWrap _thresh, Tensor1DWrap _maxval, + int channels, NVCVThresholdType type, DataType data_type) +{ + int globalid = blockIdx.x * blockDim.x + threadIdx.x; + int batch = blockIdx.z / channels; + int plane = blockIdx.z % channels; + int width = src.width(batch); + int height = src.height(batch); + + if (globalid >= width * height) + return; + + int y = globalid / width; + int x = globalid % width; + + T inval = *src.ptr(batch, plane, y, x); + T out = (data_type == kCV_32F || data_type == kCV_64F) + ? ThresholdGenericValue(inval, _thresh[batch], _maxval[batch], type) + : ThresholdOverflowValue(inval, _thresh[batch], _maxval[batch], type); + *dst.ptr(batch, plane, y, x) = out; +} + __global__ void hist_kernel(ImageBatchVarShapeWrapNHWC img, int *histogram) { __shared__ int hist[256]; @@ -665,7 +878,7 @@ __global__ void hist_kernel(ImageBatchVarShapeWrapNHWC img, int *histogra } else { - int4 src = *((int4 *)img.ptr(batch, h, w)); + int4 src = LoadPacked(img.ptr(batch, h, w)); uchar *inval = reinterpret_cast((void *)&src); for (int i = 0; i < 16; i++) atomicAdd(&hist[inval[i]], 1); } @@ -905,6 +1118,7 @@ void thresholdDispatch(const nvcv::ImageBatchVarShapeDataStridedCuda &input, const nvcv::TensorDataStridedCuda &_thresh, const nvcv::TensorDataStridedCuda &_maxval, NVCVThresholdType type, DataType data_type, cudaStream_t stream) { + (void)data_type; Tensor1DWrap thresh(_thresh); Tensor1DWrap maxval(_maxval); @@ -923,31 +1137,37 @@ void thresholdDispatch(const nvcv::ImageBatchVarShapeDataStridedCuda &input, switch (type) { case NVCV_THRESH_BINARY: - if (data_type == kCV_32F || data_type == kCV_64F) + if constexpr (std::is_floating_point_v) Binary_Generic<<>>(src_ptr, dst_ptr, thresh, maxval, channel); + else if constexpr (std::is_same_v) + { + int tdU8 = divUp(maxsize.w * channel, kU8BinaryElementsPerThread) * maxsize.h; + dim3 gridU8(divUp(tdU8, 256), 1, batch); + Binary_overflow_u8_nix16<<>>(src_ptr, dst_ptr, thresh, maxval, channel); + } else Binary_overflow<<>>(src_ptr, dst_ptr, thresh, maxval, channel); break; case NVCV_THRESH_BINARY_INV: - if (data_type == kCV_32F || data_type == kCV_64F) + if constexpr (std::is_floating_point_v) BinaryInv_Generic<<>>(src_ptr, dst_ptr, thresh, maxval, channel); else BinaryInv_overflow<<>>(src_ptr, dst_ptr, thresh, maxval, channel); break; case NVCV_THRESH_TRUNC: - if (data_type == kCV_32F || data_type == kCV_64F) + if constexpr (std::is_floating_point_v) Trunc_Generic<<>>(src_ptr, dst_ptr, thresh, channel); else Trunc_overflow<<>>(src_ptr, dst_ptr, thresh, channel); break; case NVCV_THRESH_TOZERO: - if (data_type == kCV_32F || data_type == kCV_64F) + if constexpr (std::is_floating_point_v) Tozero_Generic<<>>(src_ptr, dst_ptr, thresh, channel); else Tozero_overflow<<>>(src_ptr, dst_ptr, thresh, channel); break; default: //NVCV_THRESH_TOZERO_INV - if (data_type == kCV_32F || data_type == kCV_64F) + if constexpr (std::is_floating_point_v) TozeroInv_Generic<<>>(src_ptr, dst_ptr, thresh, channel); else TozeroInv_overflow<<>>(src_ptr, dst_ptr, thresh, channel); @@ -957,6 +1177,28 @@ void thresholdDispatch(const nvcv::ImageBatchVarShapeDataStridedCuda &input, checkKernelErrors(); } +template +void thresholdDispatchPlanar(const nvcv::ImageBatchVarShapeDataStridedCuda &input, + const nvcv::ImageBatchVarShapeDataStridedCuda &output, + const nvcv::TensorDataStridedCuda &_thresh, const nvcv::TensorDataStridedCuda &_maxval, + int channels, NVCVThresholdType type, DataType data_type, cudaStream_t stream) +{ + Tensor1DWrap thresh(_thresh); + Tensor1DWrap maxval(_maxval); + + nvcv::Size2D maxsize = input.maxSize(); + int batch = input.numImages(); + + ImageBatchVarShapeWrap src_ptr(input); + ImageBatchVarShapeWrap dst_ptr(output); + + dim3 block(256); + int td = maxsize.w * maxsize.h; + dim3 grid(divUp(td, block.x), 1, batch * channels); + ThresholdPlanar<<>>(src_ptr, dst_ptr, thresh, maxval, channels, type, data_type); + checkKernelErrors(); +} + static void getThreshVal_Triangle(const nvcv::ImageBatchVarShapeDataStridedCuda &inData, const nvcv::TensorDataStridedCuda &threshold, int *histogram, cudaStream_t stream) { @@ -1004,6 +1246,7 @@ ThresholdVarShape::ThresholdVarShape(DataShape max_input_shape, DataShape max_ou : CudaBaseOp(max_input_shape, max_output_shape) , m_histogram(nullptr) , m_type(type) + , m_maxBatchSize(maxBatchSize) { if (maxBatchSize < 0) { @@ -1017,7 +1260,7 @@ ThresholdVarShape::ThresholdVarShape(DataShape max_input_shape, DataShape max_ou if (err != cudaSuccess) { LOG_ERROR("CUDA memory allocation error of size: " << sizeof(int) * 256 * maxBatchSize); - throw std::runtime_error("CUDA memory allocation error!"); + throw LegacyCudaAllocationError("CUDA memory allocation error!"); } } } @@ -1037,6 +1280,22 @@ ErrorCode ThresholdVarShape::infer(const ImageBatchVarShapeDataStridedCuda &inDa const TensorDataStridedCuda &thresh, const TensorDataStridedCuda &maxval, cudaStream_t stream) { + DataFormat input_format = helpers::GetLegacyDataFormat(inData); + DataFormat output_format = helpers::GetLegacyDataFormat(outData); + if (input_format != output_format) + { + LOG_ERROR("Invalid DataFormat between input (" << input_format << ") and output (" << output_format << ")"); + return ErrorCode::INVALID_DATA_FORMAT; + } + + if (!(input_format == kNHWC || input_format == kHWC || input_format == kNCHW || input_format == kCHW)) + { + LOG_ERROR("Invalid input DataFormat " << input_format + << ", the valid DataFormats are: \"NHWC\", \"HWC\", \"NCHW\", \"CHW\""); + return ErrorCode::INVALID_DATA_FORMAT; + } + const bool isPlanar = (input_format == kNCHW || input_format == kCHW); + DataType in_data_type = helpers::GetLegacyDataType(inData.uniqueFormat()); if (!(in_data_type == kCV_8U || in_data_type == kCV_16S || in_data_type == kCV_16U || in_data_type == kCV_32F @@ -1052,6 +1311,12 @@ ErrorCode ThresholdVarShape::infer(const ImageBatchVarShapeDataStridedCuda &inDa LOG_ERROR("DataType of input and output must be equal, but got " << in_data_type << " and " << out_data_type); return ErrorCode::INVALID_DATA_TYPE; } + const int channels = inData.uniqueFormat().numChannels(); + if (channels > 4 || (isPlanar && channels == 2)) + { + LOG_ERROR("Invalid channel number " << channels); + return ErrorCode::INVALID_DATA_FORMAT; + } DataType thresh_data_type = GetLegacyDataType(thresh.dtype()); if (thresh_data_type != kCV_64F) @@ -1086,11 +1351,23 @@ ErrorCode ThresholdVarShape::infer(const ImageBatchVarShapeDataStridedCuda &inDa } m_type &= (uint32_t)NVCV_THRESH_MASK; - if (m_type & (m_type - 1) != 0) + if ((m_type & (m_type - 1)) != 0) { LOG_ERROR("Invalid Threhold Type " << m_type); return ErrorCode::INVALID_PARAMETER; } + + if (m_automatic_thresh != 0 && inData.numImages() > m_maxBatchSize) + { + LOG_ERROR("Input batch exceeds maxBatchSize"); + return ErrorCode::INVALID_PARAMETER; + } + if (isPlanar && static_cast(inData.numImages()) * channels > 65535) + { + LOG_ERROR("Planar Threshold requires numImages * channels <= 65535 (CUDA grid-z limit)"); + return ErrorCode::INVALID_PARAMETER; + } + if (m_automatic_thresh == NVCV_THRESH_OTSU) { if (in_data_type != kCV_8U) @@ -1130,7 +1407,23 @@ ErrorCode ThresholdVarShape::infer(const ImageBatchVarShapeDataStridedCuda &inDa threshold_t func = funcs[in_data_type]; NVCVThresholdType th_type = NVCVThresholdType(m_type); - func(inData, outData, thresh, maxval, th_type, in_data_type, stream); + if (isPlanar) + { + typedef void (*threshold_planar_t)( + const ImageBatchVarShapeDataStridedCuda &input, const ImageBatchVarShapeDataStridedCuda &output, + const TensorDataStridedCuda &threshold, const TensorDataStridedCuda &maxval, int channels, + NVCVThresholdType type, DataType data_type, cudaStream_t stream); + static const threshold_planar_t planarFuncs[7] + = {thresholdDispatchPlanar, 0, thresholdDispatchPlanar, + thresholdDispatchPlanar, 0, thresholdDispatchPlanar, + thresholdDispatchPlanar}; + threshold_planar_t planarFunc = planarFuncs[in_data_type]; + planarFunc(inData, outData, thresh, maxval, channels, th_type, in_data_type, stream); + } + else + { + func(inData, outData, thresh, maxval, th_type, in_data_type, stream); + } return SUCCESS; } diff --git a/src/cvcuda/priv/legacy/warp.cu b/src/cvcuda/priv/legacy/warp.cu index b12a86cd8..21ed19b04 100644 --- a/src/cvcuda/priv/legacy/warp.cu +++ b/src/cvcuda/priv/legacy/warp.cu @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,32 +19,108 @@ #include "CvCudaLegacyHelpers.hpp" #include "CvCudaUtils.cuh" +#include "warp_cubic.cuh" + +#include #define BLOCK 32 namespace nvcv::legacy::cuda_op { -template -__global__ void warp(SrcWrapper src, DstWrapper dst, int2 dstSize, Transform transform) +template +__global__ void warp(SrcWrapper src, DstWrapper dst, int2 dstSize, int2 srcSize, Transform transform) { - int3 dstCoord = cuda::StaticCast(blockDim * blockIdx + threadIdx); - const int lid = threadIdx.y * blockDim.x + threadIdx.x; + using SrcValueT = std::remove_cv_t; + constexpr bool kCubic = SrcWrapper::kInterpolationType == NVCV_INTERP_CUBIC; + // The restructured sampler pays off for affine maps (the scatter-heavy case); perspective + // maps keep the wrap path, whose per-tap loads coalesce well and carry no extra registers. + constexpr bool kFast = kCubic && kCubicFastSampler && std::is_same_v; + + int3 dstCoord = cuda::StaticCast(blockDim * blockIdx + threadIdx); + + if constexpr (UseSharedTransform) + { + extern __shared__ float coeff[]; - extern __shared__ float coeff[]; + const int lid = threadIdx.y * blockDim.x + threadIdx.x; + if (lid < 9) + { + coeff[lid] = transform.xform[lid]; + } - if (lid < 9) + __syncthreads(); + + if (dstCoord.x < dstSize.x && dstCoord.y < dstSize.y) + { + const float2 coord = Transform::calcCoord(coeff, dstCoord.x, dstCoord.y); + + if constexpr (kFast) + { + dst[dstCoord] = CubicSampleTensor(src, dstCoord.z, coord, srcSize); + } + else + { + dst[dstCoord] = src[float3{coord.x, coord.y, static_cast(dstCoord.z)}]; + } + } + } + else if (dstCoord.x < dstSize.x && dstCoord.y < dstSize.y) { - coeff[lid] = transform.xform[lid]; + const float2 coord = Transform::calcCoord(transform.xform, dstCoord.x, dstCoord.y); + + if constexpr (kFast) + { + dst[dstCoord] = CubicSampleTensor(src, dstCoord.z, coord, srcSize); + } + else + { + dst[dstCoord] = src[float3{coord.x, coord.y, static_cast(dstCoord.z)}]; + } } +} - __syncthreads(); +// Fused planar (NCHW/CHW) CUBIC warp: one launch covers every channel plane, sharing the +// transformed coordinate, cubic weights, and border resolution across planes instead of +// relaunching the single-channel kernel per plane. src/dst wrap plane 0; the other planes are +// addressed by the plane byte stride. +template +__global__ void warp_planar_fused(SrcWrapper src, DstWrapper dst, int2 dstSize, int2 srcSize, int64_t srcPlaneStride, + int64_t dstPlaneStride, float4 borderValue, Transform transform) +{ + const int3 dstCoord = cuda::StaticCast(blockDim * blockIdx + threadIdx); - if (dstCoord.x < dstSize.x && dstCoord.y < dstSize.y) + const float *xform = transform.xform; + if constexpr (UseSharedTransform) { - const float2 coord = Transform::calcCoord(coeff, dstCoord.x, dstCoord.y); - const float3 srcCoord{coord.x, coord.y, static_cast(dstCoord.z)}; + extern __shared__ float coeff[]; + + const int lid = threadIdx.y * blockDim.x + threadIdx.x; + if (lid < 9) + { + coeff[lid] = transform.xform[lid]; + } + + __syncthreads(); + xform = coeff; + } - dst[dstCoord] = src[srcCoord]; + if (dstCoord.x < dstSize.x && dstCoord.y < dstSize.y) + { + const float2 coord = Transform::calcCoord(xform, dstCoord.x, dstCoord.y); + + CubicWarpPlanes( + [&](int p, int yy) { + return reinterpret_cast(reinterpret_cast(src.ptr(dstCoord.z, yy)) + + p * srcPlaneStride); + }, + [&](int p, BT v) + { + *(reinterpret_cast(reinterpret_cast(dst.ptr(dstCoord.z, dstCoord.y)) + p * dstPlaneStride) + + dstCoord.x) + = v; + }, + borderValue, srcSize, coord); } } @@ -57,7 +133,7 @@ struct WarpDispatcher auto outAccess = TensorDataAccessStridedImagePlanar::Create(outData); NVCV_ASSERT(outAccess); - auto inAccess = TensorDataAccessStridedImagePlanar::Create(outData); + auto inAccess = TensorDataAccessStridedImagePlanar::Create(inData); NVCV_ASSERT(inAccess); const int2 dstSize{outAccess->numCols(), outAccess->numRows()}; @@ -68,8 +144,6 @@ struct WarpDispatcher auto bVal = cuda::StaticCast>(cuda::DropCast>(borderValue)); - int smem_size = 9 * sizeof(float); - int64_t srcMaxStride = inAccess->sampleStride() * batchSize; int64_t dstMaxStride = outAccess->sampleStride() * batchSize; @@ -78,7 +152,16 @@ struct WarpDispatcher auto src = cuda::CreateInterpolationWrapNHW(inData, bVal); auto dst = cuda::CreateTensorWrapNHW(outData); - warp<<>>(src, dst, dstSize, transform); + const int2 srcSize{inAccess->numCols(), inAccess->numRows()}; + + // PerspectiveTransform is already passed by value, so only WarpAffine's measured + // specializations need the shared-memory copy. + constexpr bool useSharedTransform + = std::is_same_v && (cuda::NumElements == 3 || I == NVCV_INTERP_CUBIC); + constexpr int smemSize = useSharedTransform ? 9 * sizeof(float) : 0; + warp + <<>>(src, dst, dstSize, srcSize, transform); } else { @@ -149,6 +232,133 @@ static void invertMat(const float *M, float *h_aCoeffs) h_aCoeffs[5] = (float)(M[3] * M[2] - M[0] * M[5]) * den; } +// Build a single-channel (N, H, W, 1) NHWC view of channel plane `plane` of a packed planar +// (NCHW/CHW) tensor. Warp samples each channel at the same transformed coordinate, so plane (n, c) +// is an independent single-channel image: viewing one plane across all N samples (sample stride +// unchanged, base offset by plane*chStride) lets the existing single-channel warp kernel handle it, +// bit-exact with the equivalent NHWC single-channel warp. One such view is processed per plane. +static nvcv::TensorDataStridedCuda PlanarChannelView(const nvcv::TensorDataStridedCuda &data, + const nvcv::TensorDataAccessStridedImagePlanar &access, int plane) +{ + nvcv::TensorDataStridedCuda::Buffer buf; + buf.basePtr = reinterpret_cast(data.basePtr()) + plane * access.chStride(); + buf.strides[0] = access.sampleStride(); // N + buf.strides[1] = access.rowStride(); // H + buf.strides[2] = access.colStride(); // W + buf.strides[3] = access.colStride(); // C == 1 + return nvcv::TensorDataStridedCuda{ + nvcv::TensorShape{{access.numSamples(), access.numRows(), access.numCols(), 1}, "NHWC"}, + data.dtype(), buf + }; +} + +// Select the constant-border component for a given channel plane. The interleaved path fills the +// border with (x, y, z, w) per channel; a single-channel plane uses only component `plane` (the +// single-channel kernel reads borderValue.x), so it is replicated into .x. +static float4 PlanarBorderValue(const float4 &borderValue, int plane) +{ + const float c = plane == 0 ? borderValue.x + : plane == 1 ? borderValue.y + : plane == 2 ? borderValue.z + : borderValue.w; + return float4{c, c, c, c}; +} + +template +struct WarpPlanarFusedDispatcher +{ + static ErrorCode call(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, + Transform transform, const float4 &borderValue, int numPlanes, cudaStream_t stream) + { + auto inAccess = TensorDataAccessStridedImagePlanar::Create(inData); + NVCV_ASSERT(inAccess); + auto outAccess = TensorDataAccessStridedImagePlanar::Create(outData); + NVCV_ASSERT(outAccess); + + const int2 dstSize{outAccess->numCols(), outAccess->numRows()}; + const int2 srcSize{inAccess->numCols(), inAccess->numRows()}; + const int batchSize{static_cast(outAccess->numSamples())}; + + dim3 block(BLOCK, BLOCK / 4); + dim3 grid(divUp(dstSize.x, block.x), divUp(dstSize.y, block.y), batchSize); + + const int64_t srcMaxStride = inAccess->sampleStride() * batchSize; + const int64_t dstMaxStride = outAccess->sampleStride() * batchSize; + if (std::max(srcMaxStride, dstMaxStride) > cuda::TypeTraits::max) + { + LOG_ERROR("Input or output size exceeds " << cuda::TypeTraits::max << ". Tensor is too large."); + return ErrorCode::INVALID_PARAMETER; + } + + auto planeIn = PlanarChannelView(inData, *inAccess, 0); + auto planeOut = PlanarChannelView(outData, *outAccess, 0); + + auto src = cuda::CreateTensorWrapNHW(planeIn); + auto dst = cuda::CreateTensorWrapNHW(planeOut); + + constexpr bool useSharedTransform = std::is_same_v; + constexpr int smemSize = useSharedTransform ? 9 * sizeof(float) : 0; + + switch (numPlanes) + { + case 1: + warp_planar_fused<<>>( + src, dst, dstSize, srcSize, inAccess->chStride(), outAccess->chStride(), borderValue, transform); + break; + case 3: + warp_planar_fused<<>>( + src, dst, dstSize, srcSize, inAccess->chStride(), outAccess->chStride(), borderValue, transform); + break; + case 4: + warp_planar_fused<<>>( + src, dst, dstSize, srcSize, inAccess->chStride(), outAccess->chStride(), borderValue, transform); + break; + default: + LOG_ERROR("Invalid planar channel number " << numPlanes); + return ErrorCode::INVALID_DATA_SHAPE; + } + checkKernelErrors(); + return ErrorCode::SUCCESS; + } +}; + +template +ErrorCode warpPlanarFusedCubic(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, + Transform transform, int borderMode, const float4 &borderValue, int numPlanes, + cudaStream_t stream) +{ + typedef ErrorCode (*func_t)(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, + Transform transform, const float4 &borderValue, int numPlanes, cudaStream_t stream); + + static const func_t funcs[5] = { + WarpPlanarFusedDispatcher::call, + WarpPlanarFusedDispatcher::call, + WarpPlanarFusedDispatcher::call, + WarpPlanarFusedDispatcher::call, + WarpPlanarFusedDispatcher::call, + }; + + return funcs[borderMode](inData, outData, transform, borderValue, numPlanes, stream); +} + +template +ErrorCode warpPlanarFusedCubicCaller(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, + Transform transform, int dataType, int borderMode, const float4 &borderValue, + int numPlanes, cudaStream_t stream) +{ + typedef ErrorCode (*func_t)(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, + Transform transform, int borderMode, const float4 &borderValue, int numPlanes, + cudaStream_t stream); + + static const func_t funcs[6] = {warpPlanarFusedCubic, 0, warpPlanarFusedCubic, + warpPlanarFusedCubic, 0, warpPlanarFusedCubic}; + + const func_t func = funcs[dataType]; + NVCV_ASSERT(func != 0); + + return func(inData, outData, transform, borderMode, borderValue, numPlanes, stream); +} + ErrorCode WarpAffine::infer(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, const float *xform, const int32_t flags, const NVCVBorderType borderMode, const float4 borderValue, cudaStream_t stream) @@ -164,12 +374,15 @@ ErrorCode WarpAffine::infer(const TensorDataStridedCuda &inData, const TensorDat DataFormat format = input_format; - if (!(format == kNHWC || format == kHWC)) + if (!(format == kNHWC || format == kHWC || format == kNCHW || format == kCHW)) { - LOG_ERROR("Invalid input DataFormat " << format << ", the valid DataFormats are: \"NHWC\", \"HWC\""); + LOG_ERROR("Invalid input DataFormat " << format + << ", the valid DataFormats are: \"NHWC\", \"HWC\", \"NCHW\", \"CHW\""); return ErrorCode::INVALID_DATA_FORMAT; } + const bool isPlanar = (format == kNCHW || format == kCHW); + auto inAccess = TensorDataAccessStridedImagePlanar::Create(inData); NVCV_ASSERT(inAccess); @@ -179,24 +392,30 @@ ErrorCode WarpAffine::infer(const TensorDataStridedCuda &inData, const TensorDat int channels = input_shape.C; const int interpolation = flags & NVCV_INTERP_MAX; - if (channels > 4) + if (channels > 4 || channels == 2) { LOG_ERROR("Invalid channel number " << channels); return ErrorCode::INVALID_DATA_SHAPE; } - if (!(data_type == kCV_8U || data_type == kCV_8S || data_type == kCV_16U || data_type == kCV_16S - || data_type == kCV_32S || data_type == kCV_32F)) + if (!(data_type == kCV_8U || data_type == kCV_16U || data_type == kCV_16S || data_type == kCV_32F)) { LOG_ERROR("Invalid DataType " << data_type); return ErrorCode::INVALID_DATA_TYPE; } - NVCV_ASSERT(interpolation == NVCV_INTERP_NEAREST || interpolation == NVCV_INTERP_LINEAR - || interpolation == NVCV_INTERP_CUBIC); - NVCV_ASSERT(borderMode == NVCV_BORDER_REFLECT101 || borderMode == NVCV_BORDER_REPLICATE - || borderMode == NVCV_BORDER_CONSTANT || borderMode == NVCV_BORDER_REFLECT - || borderMode == NVCV_BORDER_WRAP); + if (!(interpolation == NVCV_INTERP_NEAREST || interpolation == NVCV_INTERP_LINEAR + || interpolation == NVCV_INTERP_CUBIC)) + { + LOG_ERROR("Invalid interpolation " << interpolation); + return ErrorCode::INVALID_PARAMETER; + } + if (!(borderMode == NVCV_BORDER_CONSTANT || borderMode == NVCV_BORDER_REPLICATE || borderMode == NVCV_BORDER_REFLECT + || borderMode == NVCV_BORDER_WRAP || borderMode == NVCV_BORDER_REFLECT101)) + { + LOG_ERROR("Invalid borderMode " << borderMode); + return ErrorCode::INVALID_PARAMETER; + } typedef ErrorCode (*func_t)(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, WarpAffineTransform transform, const int interpolation, int borderMode, @@ -211,9 +430,6 @@ ErrorCode WarpAffine::infer(const TensorDataStridedCuda &inData, const TensorDat { warpAffine, 0, warpAffine, warpAffine} }; - const func_t func = funcs[data_type][channels - 1]; - NVCV_ASSERT(func != 0); - WarpAffineTransform transform; if (flags & NVCV_WARP_INVERSE_MAP) @@ -228,6 +444,43 @@ ErrorCode WarpAffine::infer(const TensorDataStridedCuda &inData, const TensorDat invertMat(xform, transform.xform); } + if (isPlanar) + { + // Fused planar only pays for byte-based types; other dtypes keep per-plane launches + // (each already using the fast CUBIC sampler). + if (interpolation == NVCV_INTERP_CUBIC && data_type == kCV_8U) + { + return warpPlanarFusedCubicCaller(inData, outData, transform, data_type, borderMode, borderValue, channels, + stream); + } + + // Warp each channel plane as an independent single-channel image (same transform, per-channel + // border value), reusing the interleaved single-channel kernel (funcs column 0). This is + // bit-exact with warping the equivalent NHWC single-channel data. The planar layout carries a + // per-channel border value, so each plane is given its own. + const func_t planarFunc = funcs[data_type][0]; + NVCV_ASSERT(planarFunc != 0); + + auto outAccess = TensorDataAccessStridedImagePlanar::Create(outData); + NVCV_ASSERT(outAccess); + + for (int c = 0; c < channels; ++c) + { + auto planeIn = PlanarChannelView(inData, *inAccess, c); + auto planeOut = PlanarChannelView(outData, *outAccess, c); + ErrorCode ec = planarFunc(planeIn, planeOut, transform, interpolation, borderMode, + PlanarBorderValue(borderValue, c), stream); + if (ec != ErrorCode::SUCCESS) + { + return ec; + } + } + return ErrorCode::SUCCESS; + } + + const func_t func = funcs[data_type][channels - 1]; + NVCV_ASSERT(func != 0); + return func(inData, outData, transform, interpolation, borderMode, borderValue, stream); } @@ -246,12 +499,15 @@ ErrorCode WarpPerspective::infer(const TensorDataStridedCuda &inData, const Tens DataFormat format = input_format; - if (!(format == kNHWC || format == kHWC)) + if (!(format == kNHWC || format == kHWC || format == kNCHW || format == kCHW)) { - LOG_ERROR("Invalid input DataFormat " << format << ", the valid DataFormats are: \"NHWC\", \"HWC\""); + LOG_ERROR("Invalid input DataFormat " << format + << ", the valid DataFormats are: \"NHWC\", \"HWC\", \"NCHW\", \"CHW\""); return ErrorCode::INVALID_DATA_FORMAT; } + const bool isPlanar = (format == kNCHW || format == kCHW); + auto inAccess = TensorDataAccessStridedImagePlanar::Create(inData); NVCV_ASSERT(inAccess); @@ -261,24 +517,30 @@ ErrorCode WarpPerspective::infer(const TensorDataStridedCuda &inData, const Tens int channels = input_shape.C; const int interpolation = flags & NVCV_INTERP_MAX; - if (channels > 4) + if (channels > 4 || channels == 2) { LOG_ERROR("Invalid channel number " << channels); return ErrorCode::INVALID_DATA_SHAPE; } - if (!(data_type == kCV_8U || data_type == kCV_8S || data_type == kCV_16U || data_type == kCV_16S - || data_type == kCV_32S || data_type == kCV_32F)) + if (!(data_type == kCV_8U || data_type == kCV_16U || data_type == kCV_16S || data_type == kCV_32F)) { LOG_ERROR("Invalid DataType " << data_type); return ErrorCode::INVALID_DATA_TYPE; } - NVCV_ASSERT(interpolation == NVCV_INTERP_NEAREST || interpolation == NVCV_INTERP_LINEAR - || interpolation == NVCV_INTERP_CUBIC); - NVCV_ASSERT(borderMode == NVCV_BORDER_REFLECT101 || borderMode == NVCV_BORDER_REPLICATE - || borderMode == NVCV_BORDER_CONSTANT || borderMode == NVCV_BORDER_REFLECT - || borderMode == NVCV_BORDER_WRAP); + if (!(interpolation == NVCV_INTERP_NEAREST || interpolation == NVCV_INTERP_LINEAR + || interpolation == NVCV_INTERP_CUBIC)) + { + LOG_ERROR("Invalid interpolation " << interpolation); + return ErrorCode::INVALID_PARAMETER; + } + if (!(borderMode == NVCV_BORDER_CONSTANT || borderMode == NVCV_BORDER_REPLICATE || borderMode == NVCV_BORDER_REFLECT + || borderMode == NVCV_BORDER_WRAP || borderMode == NVCV_BORDER_REFLECT101)) + { + LOG_ERROR("Invalid borderMode " << borderMode); + return ErrorCode::INVALID_PARAMETER; + } typedef ErrorCode (*func_t)(const TensorDataStridedCuda &inData, const TensorDataStridedCuda &outData, PerspectiveTransform transform, const int interpolation, int borderMode, @@ -295,9 +557,6 @@ ErrorCode WarpPerspective::infer(const TensorDataStridedCuda &inData, const Tens { warpPerspective, 0 /*warpPerspective*/, warpPerspective, warpPerspective} }; - const func_t func = funcs[data_type][channels - 1]; - NVCV_ASSERT(func != 0); - PerspectiveTransform transform(transMatrix); if (!(flags & NVCV_WARP_INVERSE_MAP)) @@ -311,6 +570,42 @@ ErrorCode WarpPerspective::infer(const TensorDataStridedCuda &inData, const Tens tempMatrixForInverse.store(transform.xform); } + if (isPlanar) + { + // Fused planar only pays for byte-based types; other dtypes keep per-plane launches + // (each already using the fast CUBIC sampler). + if (interpolation == NVCV_INTERP_CUBIC && data_type == kCV_8U) + { + return warpPlanarFusedCubicCaller(inData, outData, transform, data_type, borderMode, borderValue, channels, + stream); + } + + // Warp each channel plane as an independent single-channel image (same transform, per-channel + // border value), reusing the interleaved single-channel kernel (funcs column 0). This is + // bit-exact with warping the equivalent NHWC single-channel data, and matches WarpAffine. + const func_t planarFunc = funcs[data_type][0]; + NVCV_ASSERT(planarFunc != 0); + + auto outAccess = TensorDataAccessStridedImagePlanar::Create(outData); + NVCV_ASSERT(outAccess); + + for (int c = 0; c < channels; ++c) + { + auto planeIn = PlanarChannelView(inData, *inAccess, c); + auto planeOut = PlanarChannelView(outData, *outAccess, c); + ErrorCode ec = planarFunc(planeIn, planeOut, transform, interpolation, borderMode, + PlanarBorderValue(borderValue, c), stream); + if (ec != ErrorCode::SUCCESS) + { + return ec; + } + } + return ErrorCode::SUCCESS; + } + + const func_t func = funcs[data_type][channels - 1]; + NVCV_ASSERT(func != 0); + return func(inData, outData, transform, interpolation, borderMode, borderValue, stream); } diff --git a/src/cvcuda/priv/legacy/warp_cubic.cuh b/src/cvcuda/priv/legacy/warp_cubic.cuh new file mode 100644 index 000000000..ebbc3cfe8 --- /dev/null +++ b/src/cvcuda/priv/legacy/warp_cubic.cuh @@ -0,0 +1,420 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#ifndef CV_CUDA_PRIV_LEGACY_WARP_CUBIC_CUH +#define CV_CUDA_PRIV_LEGACY_WARP_CUBIC_CUH + +#include "CvCudaUtils.cuh" + +#include + +namespace nvcv::legacy::cuda_op { + +// Fast CUBIC sampling for the warp kernels. The InterpolationWrap CUBIC path issues one +// border-resolved scalar load per tap (48 loads/pixel for uchar3), which leaves the kernel +// LSU-throttled. Interior 4x4 windows need no border handling and their taps are 4 contiguous +// pixels per row, so each tap row is fetched with a few 8B loads and realigned in registers. +// Values, weights (GetCubicCoeffs), accumulation order, and SaturateCast are identical to +// InterpolationWrap::operator[], so results stay bit-exact; non-interior +// pixels defer to the wrap itself. The 8B loads assume the data base address is at least +// 8-byte aligned (NVCV device buffers are at least 256-byte aligned). + +// Wide pixels stay on the InterpolationWrap path: uchar4's aligned per-tap loads are already +// issue-efficient, and float3/float4-class pixels regressed on the reference SKUs (their taps +// are wide loads already, so the restructured gather only adds overhead where big L2s absorb +// the footprint). The fast sampler pays off for byte/short and scalar pixels. +template +constexpr bool kCubicFastSampler = sizeof(T) < 12 && !(sizeof(T) == 4 && cuda::NumElements == 4); + +// Number of 8-byte loads covering one row of 4 T pixels at arbitrary 8B phase. +template +constexpr int kCubicRowLoads = (7 + 4 * static_cast(sizeof(T)) + 7) / 8; + +// Right-edge margin (in pixels) covering the widened-load overread past the last tap. +template +constexpr int kCubicRowMarginPx + = (8 * kCubicRowLoads - 4 * static_cast(sizeof(T)) + static_cast(sizeof(T)) - 1) + / static_cast(sizeof(T)); + +template +inline __device__ void LoadTapRow(const T *rowp, T (&out)[4]) +{ + using BT = cuda::BaseType; + + constexpr int kSpan = 4 * static_cast(sizeof(T)); + constexpr int kLoads = kCubicRowLoads; + constexpr int kWords = kSpan / 4; + + const uintptr_t addr = reinterpret_cast(rowp); + const uint2 *p8 = reinterpret_cast(addr & ~uintptr_t(7)); + const int off = static_cast(addr & 7); + + uint32_t raw[2 * kLoads]; +#pragma unroll + for (int i = 0; i < kLoads; ++i) + { + const uint2 v = __ldg(p8 + i); + raw[2 * i] = v.x; + raw[2 * i + 1] = v.y; + } + + // Realign in two steps with compile-time register indices: drop one 4B word when the 8B + // phase is >= 4, then funnel-shift out the sub-word phase. + const bool dropWord = (off & 4) != 0; + uint32_t w[kWords + 1]; +#pragma unroll + for (int i = 0; i < kWords + 1; ++i) + { + w[i] = dropWord ? raw[i + 1] : raw[i]; + } + + const uint32_t shift = static_cast((off & 3) * 8); + uint32_t aligned[kWords]; +#pragma unroll + for (int i = 0; i < kWords; ++i) + { + aligned[i] = __funnelshift_r(w[i], w[i + 1], shift); + } + +#pragma unroll + for (int k = 0; k < 4; ++k) + { +#pragma unroll + for (int c = 0; c < cuda::NumElements; ++c) + { + const int bo = k * static_cast(sizeof(T)) + c * static_cast(sizeof(BT)); + const uint32_t bits = aligned[bo >> 2] >> ((bo & 3) * 8); + + if constexpr (sizeof(BT) == 4) + { + cuda::GetElement(out[k], c) = cuda::BaseType(__uint_as_float(aligned[bo >> 2])); + } + else if constexpr (sizeof(BT) == 2) + { + cuda::GetElement(out[k], c) = static_cast(bits & 0xFFFFu); + } + else + { + cuda::GetElement(out[k], c) = static_cast(bits & 0xFFu); + } + } + } +} + +// Interior taps default to individual loads from their row pointer: for coherent (gentle) maps +// the per-tap loads coalesce across the warp, and skipping border resolution is the entire win. +// WideInterior opts into the widened row loads for access patterns that scatter enough for the +// load-count reduction to dominate (measured per operator; rotate's diagonal access qualifies, +// perspective's gentle maps do not). +template +inline __device__ T CubicAccumulate(RowPtrOp rowPtr, float x, float y, StrideType ix, StrideType iy) +{ + float wx[4]; // NOSONAR: CUDA cubic coefficients are indexed in the unrolled loop. + cuda::GetCubicCoeffs(x - static_cast(ix), wx[0], wx[1], wx[2], wx[3]); + float wy[4]; // NOSONAR: CUDA cubic coefficients are indexed in the unrolled loop. + cuda::GetCubicCoeffs(y - static_cast(iy), wy[0], wy[1], wy[2], wy[3]); + + using FT = cuda::ConvertBaseTypeTo; + auto sum = cuda::SetAll(0); + +#pragma unroll + for (int r = 0; r < 4; ++r) + { + if constexpr (WideInterior) + { + T row[4]; + LoadTapRow(rowPtr(static_cast(iy) + r - 1, static_cast(ix) - 1), row); +#pragma unroll + for (int k = 0; k < 4; ++k) + { + sum += row[k] * (wx[k] * wy[r]); + } + } + else + { + const T *row = rowPtr(static_cast(iy) + r - 1, static_cast(ix) - 1); +#pragma unroll + for (int k = 0; k < 4; ++k) + { + sum += row[k] * (wx[k] * wy[r]); + } + } + } + + return cuda::SaturateCast(sum); +} + +// Border-resolved CUBIC gather. Border remapping is separable per axis, so the 4 column and 4 +// row indices are resolved once (8 GetIndexWithBorder calls — REFLECT costs an integer modulo +// each) instead of per tap (32), and each tap row is addressed from its row base pointer. +// Index math, tap values, weights, accumulation order, and SaturateCast match the +// InterpolationWrap CUBIC path exactly. +template +inline __device__ T CubicAccumulateBorder(RowPtrOp rowPtr, T borderValue, int2 size, float x, float y, StrideType ix, + StrideType iy) +{ + float wx[4]; // NOSONAR: CUDA cubic coefficients are indexed in the unrolled loop. + cuda::GetCubicCoeffs(x - static_cast(ix), wx[0], wx[1], wx[2], wx[3]); + float wy[4]; // NOSONAR: CUDA cubic coefficients are indexed in the unrolled loop. + cuda::GetCubicCoeffs(y - static_cast(iy), wy[0], wy[1], wy[2], wy[3]); + + StrideType xr[4], yr[4]; + bool xin[4], yin[4]; +#pragma unroll + for (int k = 0; k < 4; ++k) + { + if constexpr (B == NVCV_BORDER_CONSTANT) + { + xin[k] = !cuda::IsOutside(ix + k - 1, static_cast(size.x)); + yin[k] = !cuda::IsOutside(iy + k - 1, static_cast(size.y)); + xr[k] = xin[k] ? ix + k - 1 : 0; + yr[k] = yin[k] ? iy + k - 1 : 0; + } + else + { + xin[k] = yin[k] = true; + xr[k] = cuda::GetIndexWithBorder(ix + k - 1, static_cast(size.x)); + yr[k] = cuda::GetIndexWithBorder(iy + k - 1, static_cast(size.y)); + } + } + + using FT = cuda::ConvertBaseTypeTo; + auto sum = cuda::SetAll(0); + + // Border remapping preserves adjacency away from fold points, so most resolved windows are + // still 4 contiguous columns (ascending, or descending in reflected segments) and take the + // widened row loads; windows straddling a fold or a constant-border edge stay scalar. + const bool xAsc = xr[1] == xr[0] + 1 && xr[2] == xr[0] + 2 && xr[3] == xr[0] + 3; + const bool xDesc = xr[1] == xr[0] - 1 && xr[2] == xr[0] - 2 && xr[3] == xr[0] - 3; + const bool xIn = xin[0] && xin[1] && xin[2] && xin[3] && yin[0] && yin[1] && yin[2] && yin[3]; + const StrideType x0 = xAsc ? xr[0] : xr[3]; + + if ((xAsc || xDesc) && xIn && x0 + 3 + kCubicRowMarginPx < size.x) + { +#pragma unroll + for (int r = 0; r < 4; ++r) + { + T row[4]; + LoadTapRow(rowPtr(static_cast(yr[r])) + x0, row); +#pragma unroll + for (int k = 0; k < 4; ++k) + { + const T v = xAsc ? row[k] : row[3 - k]; + sum += v * (wx[k] * wy[r]); + } + } + } + else + { +#pragma unroll + for (int r = 0; r < 4; ++r) + { + const T *row = rowPtr(static_cast(yr[r])); +#pragma unroll + for (int k = 0; k < 4; ++k) + { + const T v = (yin[r] && xin[k]) ? row[xr[k]] : borderValue; + sum += v * (wx[k] * wy[r]); + } + } + } + + return cuda::SaturateCast(sum); +} + +// Tensor (NHW wrap) CUBIC sample; srcSize is (numCols, numRows). +template +inline __device__ std::remove_cv_t CubicSampleTensor(const SrcWrapper &src, int z, + float2 coord, int2 srcSize) +{ + using T = std::remove_cv_t; + using StrideType = typename SrcWrapper::StrideType; + + const StrideType ix = cuda::GetIndexForInterpolation(coord.x); + const StrideType iy = cuda::GetIndexForInterpolation(coord.y); + + const auto &tw = src.borderWrap().tensorWrap(); + + if (ix >= 1 && iy >= 1 && iy + 2 < srcSize.y && ix + 2 + kCubicRowMarginPx < srcSize.x) + { + return CubicAccumulate([&](int yy, int xx) { return tw.ptr(z, yy, xx); }, coord.x, coord.y, ix, + iy); + } + + constexpr NVCVBorderType kB = SrcWrapper::BorderWrapper::kBorderType; + return CubicAccumulateBorder([&](int yy) { return tw.ptr(z, yy); }, src.borderWrap().borderValue(), srcSize, + coord.x, coord.y, ix, iy); +} + +// Var-shape (image batch wrap) CUBIC sample; sizes come from the wrapped batch per sample. +template +inline __device__ std::remove_cv_t CubicSampleVarShape(const SrcWrapper &src, int z, + float2 coord) +{ + using T = std::remove_cv_t; + using StrideType = int; + + const StrideType ix = cuda::GetIndexForInterpolation(coord.x); + const StrideType iy = cuda::GetIndexForInterpolation(coord.y); + + const auto &ibw = src.borderWrap().imageBatchWrap(); + const int2 srcSize = {ibw.width(z), ibw.height(z)}; + + if (ix >= 1 && iy >= 1 && iy + 2 < srcSize.y && ix + 2 + kCubicRowMarginPx < srcSize.x) + { + return CubicAccumulate([&](int yy, int xx) { return ibw.ptr(z, yy, xx); }, coord.x, coord.y, + ix, iy); + } + + constexpr NVCVBorderType kB = SrcWrapper::BorderWrapper::kBorderType; + return CubicAccumulateBorder([&](int yy) { return ibw.ptr(z, yy); }, src.borderWrap().borderValue(), srcSize, + coord.x, coord.y, ix, iy); +} + +// Var-shape planar (plane-indexed) CUBIC sample for the per-plane kernel. +template +inline __device__ std::remove_cv_t CubicSampleVarShapePlane(const SrcWrapper &src, + int z, int plane, + float2 coord) +{ + using T = std::remove_cv_t; + using StrideType = int; + + const StrideType ix = cuda::GetIndexForInterpolation(coord.x); + const StrideType iy = cuda::GetIndexForInterpolation(coord.y); + + const auto &ibw = src.borderWrap().imageBatchWrap(); + const int2 srcSize = {ibw.width(z, plane), ibw.height(z, plane)}; + + if (ix >= 1 && iy >= 1 && iy + 2 < srcSize.y && ix + 2 + kCubicRowMarginPx < srcSize.x) + { + return CubicAccumulate([&](int yy, int xx) { return ibw.ptr(z, plane, yy, xx); }, coord.x, + coord.y, ix, iy); + } + + constexpr NVCVBorderType kB = SrcWrapper::BorderWrapper::kBorderType; + return CubicAccumulateBorder([&](int yy) { return ibw.ptr(z, plane, yy, 0); }, + src.borderWrap().borderValue(), srcSize, coord.x, coord.y, ix, iy); +} + +// Fused planar CUBIC warp for one output pixel across all channel planes. Every plane samples +// the same transformed coordinate, so the interpolation indices, weights, and border resolution +// are computed once and only the gather + accumulate runs per plane. Per-plane values match the +// single-channel per-plane kernel launches bit-exactly (same index math, weights, accumulation +// order, and SaturateCast; scalar float math is identical to the single-channel vector math). +// rowPtr(p, yy) returns plane p's row base; store(p, v) writes the plane's output pixel. +template +inline __device__ void CubicWarpPlanes(RowPtrOp rowPtr, StoreOp store, const float4 &borderValue4, int2 size, + float2 coord) +{ + const StrideType ix = cuda::GetIndexForInterpolation(coord.x); + const StrideType iy = cuda::GetIndexForInterpolation(coord.y); + + float wx[4]; // NOSONAR: CUDA cubic coefficients are indexed in the unrolled loop. + cuda::GetCubicCoeffs(coord.x - static_cast(ix), wx[0], wx[1], wx[2], wx[3]); + float wy[4]; // NOSONAR: CUDA cubic coefficients are indexed in the unrolled loop. + cuda::GetCubicCoeffs(coord.y - static_cast(iy), wy[0], wy[1], wy[2], wy[3]); + + if (ix >= 1 && iy >= 1 && iy + 2 < size.y && ix + 2 + kCubicRowMarginPx < size.x) + { +#pragma unroll + for (int p = 0; p < NP; ++p) + { + float sum = 0; +#pragma unroll + for (int r = 0; r < 4; ++r) + { + BT row[4]; + LoadTapRow(rowPtr(p, static_cast(iy) + r - 1) + (static_cast(ix) - 1), row); +#pragma unroll + for (int k = 0; k < 4; ++k) + { + sum += row[k] * (wx[k] * wy[r]); + } + } + store(p, cuda::SaturateCast(sum)); + } + return; + } + + StrideType xr[4], yr[4]; + bool xin[4], yin[4]; +#pragma unroll + for (int k = 0; k < 4; ++k) + { + if constexpr (B == NVCV_BORDER_CONSTANT) + { + xin[k] = !cuda::IsOutside(ix + k - 1, static_cast(size.x)); + yin[k] = !cuda::IsOutside(iy + k - 1, static_cast(size.y)); + xr[k] = xin[k] ? ix + k - 1 : 0; + yr[k] = yin[k] ? iy + k - 1 : 0; + } + else + { + xin[k] = yin[k] = true; + xr[k] = cuda::GetIndexWithBorder(ix + k - 1, static_cast(size.x)); + yr[k] = cuda::GetIndexWithBorder(iy + k - 1, static_cast(size.y)); + } + } + + const bool xAsc = xr[1] == xr[0] + 1 && xr[2] == xr[0] + 2 && xr[3] == xr[0] + 3; + const bool xDesc = xr[1] == xr[0] - 1 && xr[2] == xr[0] - 2 && xr[3] == xr[0] - 3; + const bool allIn = xin[0] && xin[1] && xin[2] && xin[3] && yin[0] && yin[1] && yin[2] && yin[3]; + const StrideType x0 = xAsc ? xr[0] : xr[3]; + const bool vecRow = (xAsc || xDesc) && allIn && x0 + 3 + kCubicRowMarginPx < size.x; + +#pragma unroll + for (int p = 0; p < NP; ++p) + { + const BT bval = cuda::StaticCast(cuda::GetElement(borderValue4, p)); + + float sum = 0; + if (vecRow) + { +#pragma unroll + for (int r = 0; r < 4; ++r) + { + BT row[4]; + LoadTapRow(rowPtr(p, static_cast(yr[r])) + x0, row); +#pragma unroll + for (int k = 0; k < 4; ++k) + { + sum += (xAsc ? row[k] : row[3 - k]) * (wx[k] * wy[r]); + } + } + } + else + { +#pragma unroll + for (int r = 0; r < 4; ++r) + { + const BT *row = rowPtr(p, static_cast(yr[r])); +#pragma unroll + for (int k = 0; k < 4; ++k) + { + const BT v = (yin[r] && xin[k]) ? row[xr[k]] : bval; + sum += v * (wx[k] * wy[r]); + } + } + } + store(p, cuda::SaturateCast(sum)); + } +} + +} // namespace nvcv::legacy::cuda_op + +#endif // CV_CUDA_PRIV_LEGACY_WARP_CUBIC_CUH diff --git a/src/cvcuda/priv/legacy/warp_var_shape.cu b/src/cvcuda/priv/legacy/warp_var_shape.cu index 1ccf22323..505b80fb1 100644 --- a/src/cvcuda/priv/legacy/warp_var_shape.cu +++ b/src/cvcuda/priv/legacy/warp_var_shape.cu @@ -1,4 +1,4 @@ -/* Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +/* Copyright (c) 2021-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * * SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES * SPDX-License-Identifier: Apache-2.0 @@ -25,6 +25,9 @@ #include "CvCudaLegacyHelpers.hpp" #include "CvCudaUtils.cuh" +#include "warp_cubic.cuh" + +#include #define BLOCK 32 @@ -79,6 +82,12 @@ __global__ void inverseMatWarpAffine(const int numImages, const cuda::Tensor2DWr template __global__ void warp(SrcWrapper src, DstWrapper dst, const cuda::Tensor2DWrap coeffs) { + using SrcValueT = std::remove_cv_t; + constexpr bool kCubic = SrcWrapper::kInterpolationType == NVCV_INTERP_CUBIC; + // The restructured sampler pays off for affine maps (the scatter-heavy case); perspective + // maps keep the wrap path, whose per-tap loads coalesce well and carry no extra registers. + constexpr bool kFast = kCubic && kCubicFastSampler && std::is_same_v; + int3 dstCoord = cuda::StaticCast(blockDim * blockIdx + threadIdx); const int lid = threadIdx.y * blockDim.x + threadIdx.x; @@ -94,9 +103,54 @@ __global__ void warp(SrcWrapper src, DstWrapper dst, const cuda::Tensor2DWrap(dstCoord.z)}; - dst[dstCoord] = src[srcCoord]; + if constexpr (kFast) + { + dst[dstCoord] = CubicSampleVarShape(src, dstCoord.z, coord); + } + else + { + dst[dstCoord] = src[float3{coord.x, coord.y, static_cast(dstCoord.z)}]; + } + } +} + +// LINEAR Perspective benefits from amortizing per-block coefficient staging across two coalesced X outputs. Keep a +// separate kernel so WarpAffine and the other interpolation modes retain their qualified code generation. +template +__global__ void warp_x2(SrcWrapper src, DstWrapper dst, const cuda::Tensor2DWrap coeffs) +{ + constexpr int kNIX = 2; + const int dstX0 = blockIdx.x * blockDim.x * kNIX + threadIdx.x; + const int dstY = blockIdx.y * blockDim.y + threadIdx.y; + const int batch = blockIdx.z; + const int lid = threadIdx.y * blockDim.x + threadIdx.x; + + extern __shared__ float coeff[]; + + if (lid < 9) + { + coeff[lid] = *coeffs.ptr(batch, lid); + } + + __syncthreads(); + + const int dstWidth = dst.width(batch); + const int dstHeight = dst.height(batch); + if (dstY < dstHeight) + { +#pragma unroll + for (int i = 0; i < kNIX; ++i) + { + const int dstX = dstX0 + i * static_cast(blockDim.x); + if (dstX < dstWidth) + { + const float2 coord = Transform::calcCoord(coeff, dstX, dstY); + const float3 srcCoord{coord.x, coord.y, static_cast(batch)}; + + dst[int3{dstX, dstY, batch}] = src[srcCoord]; + } + } } } @@ -108,8 +162,10 @@ struct WarpDispatcher { Size2D outMaxSize = outData.maxSize(); - dim3 block(BLOCK, BLOCK / 4); - dim3 grid(divUp(outMaxSize.w, block.x), divUp(outMaxSize.h, block.y), outData.numImages()); + constexpr bool useX2 = std::is_same_v && I == NVCV_INTERP_LINEAR; + constexpr int kNIX = useX2 ? 2 : 1; + dim3 block(BLOCK, BLOCK / 4); + dim3 grid(divUp(outMaxSize.w, block.x * kNIX), divUp(outMaxSize.h, block.y), outData.numImages()); auto bVal = cuda::StaticCast>(cuda::DropCast>(borderValue)); @@ -118,7 +174,14 @@ struct WarpDispatcher size_t smem_size = 9 * sizeof(float); - warp<<>>(src, dst, transform); + if constexpr (useX2) + { + warp_x2<<>>(src, dst, transform); + } + else + { + warp<<>>(src, dst, transform); + } checkKernelErrors(); } }; @@ -169,6 +232,189 @@ void warpPerspective(const ImageBatchVarShapeDataStridedCuda &inData, const Imag warp_caller(inData, outData, transform, interpolation, borderMode, borderValue, stream); } +// Planar (NCHW/CHW) var-shape warp. Warp samples each channel at the same transformed coordinate, so +// each plane is an independent single-channel image: this kernel warps one plane (selected by the +// plane-indexed (sample, plane, y, x) coordinate), reusing the interpolation/border wrap. One launch +// per plane lets each plane carry its own constant-border component. Bit-exact with the interleaved +// single-channel warp. +// Fused planar var-shape CUBIC warp: one launch covers every channel plane, sharing the +// transformed coordinate, cubic weights, and border resolution across planes instead of +// relaunching the single-plane kernel per plane. +template +__global__ void warp_planar_fused(SrcWrapper src, DstWrapper dst, const cuda::Tensor2DWrap coeffs, + float4 borderValue) +{ + const int3 dstCoord = cuda::StaticCast(blockDim * blockIdx + threadIdx); + const int lid = threadIdx.y * blockDim.x + threadIdx.x; + + extern __shared__ float coeff[]; + + if (lid < 9) + { + coeff[lid] = *coeffs.ptr(dstCoord.z, lid); + } + + __syncthreads(); + + const int z = dstCoord.z; + if (dstCoord.x < dst.width(z) && dstCoord.y < dst.height(z)) + { + const float2 coord = Transform::calcCoord(coeff, dstCoord.x, dstCoord.y); + + CubicWarpPlanes( + [&](int p, int yy) { return reinterpret_cast(src.ptr(z, p, yy, 0)); }, + [&](int p, BT v) { *reinterpret_cast(dst.ptr(z, p, dstCoord.y, dstCoord.x)) = v; }, borderValue, + int2{src.width(z), src.height(z)}, coord); + } +} + +template +__global__ void warp_planar(SrcWrapper src, DstWrapper dst, const cuda::Tensor2DWrap coeffs, int plane) +{ + int3 dstCoord = cuda::StaticCast(blockDim * blockIdx + threadIdx); + const int lid = threadIdx.y * blockDim.x + threadIdx.x; + + extern __shared__ float coeff[]; + + if (lid < 9) + { + coeff[lid] = *coeffs.ptr(dstCoord.z, lid); + } + + __syncthreads(); + + const int batch = dstCoord.z; + if (dstCoord.x < dst.width(batch) && dstCoord.y < dst.height(batch)) + { + const float2 coord = Transform::calcCoord(coeff, dstCoord.x, dstCoord.y); + + constexpr bool kFast + = SrcWrapper::kInterpolationType == NVCV_INTERP_CUBIC + && kCubicFastSampler< + std::remove_cv_t> && std::is_same_v; + if constexpr (kFast) + { + dst[int4{dstCoord.x, dstCoord.y, plane, batch}] = CubicSampleVarShapePlane(src, batch, plane, coord); + } + else + { + dst[int4{dstCoord.x, dstCoord.y, plane, batch}] + = src[float4{coord.x, coord.y, static_cast(plane), static_cast(batch)}]; + } + } +} + +template +struct WarpPlanarDispatcher +{ + static void call(const ImageBatchVarShapeDataStridedCuda &inData, const ImageBatchVarShapeDataStridedCuda &outData, + const cuda::Tensor2DWrap transform, const float4 &borderValue, int channels, + cudaStream_t stream) + { + Size2D outMaxSize = outData.maxSize(); + + dim3 block(BLOCK, BLOCK / 4); + dim3 grid(divUp(outMaxSize.w, block.x), divUp(outMaxSize.h, block.y), outData.numImages()); + size_t smem_size = 9 * sizeof(float); + + cuda::ImageBatchVarShapeWrap dst(outData); + + // Fused planar only pays for byte-based types; float planes keep per-plane launches + // (each already using the fast CUBIC sampler). + if constexpr (I == NVCV_INTERP_CUBIC && sizeof(cuda::BaseType) == 1) + { + cuda::ImageBatchVarShapeWrap src(inData); + + switch (channels) + { + case 1: + warp_planar_fused, B, 1> + <<>>(src, dst, transform, borderValue); + break; + case 3: + warp_planar_fused, B, 3> + <<>>(src, dst, transform, borderValue); + break; + case 4: + warp_planar_fused, B, 4> + <<>>(src, dst, transform, borderValue); + break; + default: + break; + } + } + else + { + for (int c = 0; c < channels; ++c) + { + const float bc = c == 0 ? borderValue.x + : c == 1 ? borderValue.y + : c == 2 ? borderValue.z + : borderValue.w; + auto bVal + = cuda::StaticCast>(cuda::DropCast>(float4{bc, bc, bc, bc})); + + cuda::InterpolationVarShapeWrap src(inData, bVal); + + warp_planar<<>>(src, dst, transform, c); + } + } + checkKernelErrors(); + } +}; + +template +void warp_planar_caller(const ImageBatchVarShapeDataStridedCuda &inData, + const ImageBatchVarShapeDataStridedCuda &outData, cuda::Tensor2DWrap transform, + const int interpolation, const int borderMode, int channels, const float4 &borderValue, + cudaStream_t stream) +{ + typedef void (*func_t)(const ImageBatchVarShapeDataStridedCuda &inData, + const ImageBatchVarShapeDataStridedCuda &outData, + const cuda::Tensor2DWrap transform, const float4 &borderValue, int channels, + cudaStream_t stream); + + static const func_t funcs[3][5] = { + {WarpPlanarDispatcher::call, + WarpPlanarDispatcher::call, + WarpPlanarDispatcher::call, + WarpPlanarDispatcher::call, + WarpPlanarDispatcher::call}, + {WarpPlanarDispatcher::call, + WarpPlanarDispatcher::call, + WarpPlanarDispatcher::call, + WarpPlanarDispatcher::call, + WarpPlanarDispatcher::call}, + {WarpPlanarDispatcher::call, + WarpPlanarDispatcher::call, + WarpPlanarDispatcher::call, + WarpPlanarDispatcher::call, + WarpPlanarDispatcher::call}, + }; + + funcs[interpolation][borderMode](inData, outData, transform, borderValue, channels, stream); +} + +template +void warpAffine_planar(const ImageBatchVarShapeDataStridedCuda &inData, + const ImageBatchVarShapeDataStridedCuda &outData, cuda::Tensor2DWrap transform, + const int interpolation, const int borderMode, int channels, const float4 &borderValue, + cudaStream_t stream) +{ + warp_planar_caller(inData, outData, transform, interpolation, borderMode, channels, + borderValue, stream); +} + +template +void warpPerspective_planar(const ImageBatchVarShapeDataStridedCuda &inData, + const ImageBatchVarShapeDataStridedCuda &outData, + cuda::Tensor2DWrap transform, const int interpolation, const int borderMode, + int channels, const float4 &borderValue, cudaStream_t stream) +{ + warp_planar_caller(inData, outData, transform, interpolation, borderMode, channels, + borderValue, stream); +} + WarpAffineVarShape::WarpAffineVarShape(const int32_t maxBatchSize) : CudaBaseOp() , m_maxBatchSize(maxBatchSize) @@ -226,15 +472,18 @@ ErrorCode WarpAffineVarShape::infer(const ImageBatchVarShapeDataStridedCuda &inD DataFormat format = input_format; - if (!(format == kNHWC || format == kHWC)) + if (!(format == kNHWC || format == kHWC || format == kNCHW || format == kCHW)) { - LOG_ERROR("Invalid input DataFormat " << format << ", the valid DataFormats are: \"NHWC\", \"HWC\""); + LOG_ERROR("Invalid input DataFormat " << format + << ", the valid DataFormats are: \"NHWC\", \"HWC\", \"NCHW\", \"CHW\""); return ErrorCode::INVALID_DATA_FORMAT; } + const bool isPlanar = (format == kNCHW || format == kCHW); + int channels = inData.uniqueFormat().numChannels(); - if (channels > 4) + if (channels > 4 || channels == 2) { LOG_ERROR("Invalid channel number " << channels); return ErrorCode::INVALID_DATA_SHAPE; @@ -244,18 +493,24 @@ ErrorCode WarpAffineVarShape::infer(const ImageBatchVarShapeDataStridedCuda &inD DataType data_type = helpers::GetLegacyDataType(inData.uniqueFormat()); - if (!(data_type == kCV_8U || data_type == kCV_8S || data_type == kCV_16U || data_type == kCV_16S - || data_type == kCV_32S || data_type == kCV_32F)) + if (!(data_type == kCV_8U || data_type == kCV_16U || data_type == kCV_16S || data_type == kCV_32F)) { LOG_ERROR("Invalid DataType " << data_type); return ErrorCode::INVALID_DATA_TYPE; } - NVCV_ASSERT(interpolation == NVCV_INTERP_NEAREST || interpolation == NVCV_INTERP_LINEAR - || interpolation == NVCV_INTERP_CUBIC); - NVCV_ASSERT(borderMode == NVCV_BORDER_REFLECT101 || borderMode == NVCV_BORDER_REPLICATE - || borderMode == NVCV_BORDER_CONSTANT || borderMode == NVCV_BORDER_REFLECT - || borderMode == NVCV_BORDER_WRAP); + if (!(interpolation == NVCV_INTERP_NEAREST || interpolation == NVCV_INTERP_LINEAR + || interpolation == NVCV_INTERP_CUBIC)) + { + LOG_ERROR("Invalid interpolation " << interpolation); + return ErrorCode::INVALID_PARAMETER; + } + if (!(borderMode == NVCV_BORDER_CONSTANT || borderMode == NVCV_BORDER_REPLICATE || borderMode == NVCV_BORDER_REFLECT + || borderMode == NVCV_BORDER_WRAP || borderMode == NVCV_BORDER_REFLECT101)) + { + LOG_ERROR("Invalid borderMode " << borderMode); + return ErrorCode::INVALID_PARAMETER; + } // Check if inverse op is needed bool performInverse = !(flags & NVCV_WARP_INVERSE_MAP); @@ -291,6 +546,26 @@ ErrorCode WarpAffineVarShape::infer(const ImageBatchVarShapeDataStridedCuda &inD { warpAffine, 0 /*warpAffine*/, warpAffine, warpAffine} }; + if (isPlanar) + { + // Planar dispatch indexes by dtype only: each channel is warped as a single-channel plane + // (per-channel border value handled inside the planar caller). + typedef void (*planar_func_t)( + const ImageBatchVarShapeDataStridedCuda &inData, const ImageBatchVarShapeDataStridedCuda &outData, + cuda::Tensor2DWrap transform, const int interpolation, const int borderMode, int channels, + const float4 &borderValue, cudaStream_t stream); + + static const planar_func_t planar_funcs[6] = { + warpAffine_planar, 0 /*schar*/, warpAffine_planar, + warpAffine_planar, 0 /*int*/, warpAffine_planar, + }; + + const planar_func_t planarFunc = planar_funcs[data_type]; + NVCV_ASSERT(planarFunc != 0); + planarFunc(inData, outData, transMatrixOutput, interpolation, borderMode, channels, borderValue, stream); + return SUCCESS; + } + const func_t func = funcs[data_type][channels - 1]; NVCV_ASSERT(func != 0); @@ -353,15 +628,18 @@ ErrorCode WarpPerspectiveVarShape::infer(const ImageBatchVarShapeDataStridedCuda DataFormat format = input_format; - if (!(format == kNHWC || format == kHWC)) + if (!(format == kNHWC || format == kHWC || format == kNCHW || format == kCHW)) { - LOG_ERROR("Invalid input DataFormat " << format << ", the valid DataFormats are: \"NHWC\", \"HWC\""); + LOG_ERROR("Invalid input DataFormat " << format + << ", the valid DataFormats are: \"NHWC\", \"HWC\", \"NCHW\", \"CHW\""); return ErrorCode::INVALID_DATA_FORMAT; } + const bool isPlanar = (format == kNCHW || format == kCHW); + int channels = inData.uniqueFormat().numChannels(); - if (channels > 4) + if (channels > 4 || channels == 2) { LOG_ERROR("Invalid channel number " << channels); return ErrorCode::INVALID_DATA_SHAPE; @@ -371,18 +649,24 @@ ErrorCode WarpPerspectiveVarShape::infer(const ImageBatchVarShapeDataStridedCuda DataType data_type = helpers::GetLegacyDataType(inData.uniqueFormat()); - if (!(data_type == kCV_8U || data_type == kCV_8S || data_type == kCV_16U || data_type == kCV_16S - || data_type == kCV_32S || data_type == kCV_32F)) + if (!(data_type == kCV_8U || data_type == kCV_16U || data_type == kCV_16S || data_type == kCV_32F)) { LOG_ERROR("Invalid DataType " << data_type); return ErrorCode::INVALID_DATA_TYPE; } - NVCV_ASSERT(interpolation == NVCV_INTERP_NEAREST || interpolation == NVCV_INTERP_LINEAR - || interpolation == NVCV_INTERP_CUBIC); - NVCV_ASSERT(borderMode == NVCV_BORDER_REFLECT101 || borderMode == NVCV_BORDER_REPLICATE - || borderMode == NVCV_BORDER_CONSTANT || borderMode == NVCV_BORDER_REFLECT - || borderMode == NVCV_BORDER_WRAP); + if (!(interpolation == NVCV_INTERP_NEAREST || interpolation == NVCV_INTERP_LINEAR + || interpolation == NVCV_INTERP_CUBIC)) + { + LOG_ERROR("Invalid interpolation " << interpolation); + return ErrorCode::INVALID_PARAMETER; + } + if (!(borderMode == NVCV_BORDER_CONSTANT || borderMode == NVCV_BORDER_REPLICATE || borderMode == NVCV_BORDER_REFLECT + || borderMode == NVCV_BORDER_WRAP || borderMode == NVCV_BORDER_REFLECT101)) + { + LOG_ERROR("Invalid borderMode " << borderMode); + return ErrorCode::INVALID_PARAMETER; + } // Check if inverse op is needed bool performInverse = flags & NVCV_WARP_INVERSE_MAP; @@ -420,6 +704,26 @@ ErrorCode WarpPerspectiveVarShape::infer(const ImageBatchVarShapeDataStridedCuda { warpPerspective, 0 /*warpPerspective*/, warpPerspective, warpPerspective} }; + if (isPlanar) + { + // Planar dispatch indexes by dtype only: each channel is warped as a single-channel plane + // (per-channel border value handled inside the planar caller). Matches WarpAffineVarShape. + typedef void (*planar_func_t)( + const ImageBatchVarShapeDataStridedCuda &inData, const ImageBatchVarShapeDataStridedCuda &outData, + cuda::Tensor2DWrap transform, const int interpolation, const int borderMode, int channels, + const float4 &borderValue, cudaStream_t stream); + + static const planar_func_t planar_funcs[6] = { + warpPerspective_planar, 0 /*schar*/, warpPerspective_planar, + warpPerspective_planar, 0 /*int*/, warpPerspective_planar, + }; + + const planar_func_t planarFunc = planar_funcs[data_type]; + NVCV_ASSERT(planarFunc != 0); + planarFunc(inData, outData, transMatrixOutput, interpolation, borderMode, channels, borderValue, stream); + return SUCCESS; + } + const func_t func = funcs[data_type][channels - 1]; NVCV_ASSERT(func != 0); diff --git a/src/cvcuda/util/Event.hpp b/src/cvcuda/util/Event.hpp index c9fc724b0..0afb55b62 100644 --- a/src/cvcuda/util/Event.hpp +++ b/src/cvcuda/util/Event.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2020-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -42,6 +42,10 @@ class CudaEvent : public UniqueHandle NVCV_INHERIT_UNIQUE_HANDLE(cudaEvent_t, CudaEvent) constexpr CudaEvent() = default; + CudaEvent(CudaEvent &&) noexcept = default; + + CudaEvent &operator=(CudaEvent &&) noexcept = default; + /** @brief Creates an event on specified device (or current device, if deviceId < 0) */ static CudaEvent Create(int deviceId = -1); diff --git a/src/cvcuda/util/PerStreamCache.hpp b/src/cvcuda/util/PerStreamCache.hpp index 3d4edf1a7..29e20ff91 100644 --- a/src/cvcuda/util/PerStreamCache.hpp +++ b/src/cvcuda/util/PerStreamCache.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -24,8 +24,12 @@ #include +#include #include +#include +#include #include +#include #include #include #include @@ -70,7 +74,8 @@ namespace detail { template struct StreamCacheItem { - StreamCacheItem *next = nullptr, *prev = nullptr; + StreamCacheItem *next = nullptr; + StreamCacheItem *prev = nullptr; mutable bool wasReady = false; @@ -115,29 +120,30 @@ class StreamCacheItemAllocator assert(m_allocated == 0); while (m_head) { - auto *next = m_head->next; - delete m_head; - m_head = next; + std::unique_ptr current = std::move(m_head); + m_head.reset(current->next); + current->next = nullptr; } } item_t *allocate() { - if (auto *p = m_head) + if (m_head) { - m_head = p->next; + std::unique_ptr p = std::move(m_head); + m_head.reset(p->next); p->next = nullptr; assert(!p->prev); m_allocated++; m_free--; *p = {}; // clear the object - return p; + return p.release(); } - auto *p = new item_t(); + std::unique_ptr p = std::make_unique(); m_allocated++; - return p; + return p.release(); } void deallocate(item_t *item) @@ -148,16 +154,17 @@ class StreamCacheItemAllocator assert(!item->next && !item->prev && "The item is still linked"); item->payload = {}; - item->next = m_head; - m_head = item; + item->next = m_head.release(); + m_head.reset(item); m_allocated--; m_free++; } private: - item_t *m_head = nullptr; + std::unique_ptr m_head; - size_t m_allocated = 0, m_free = 0; + size_t m_allocated = 0; + size_t m_free = 0; }; template> @@ -171,9 +178,25 @@ class StreamOrderedCache { } - ~StreamOrderedCache() + StreamOrderedCache(const StreamOrderedCache &) = delete; + StreamOrderedCache(StreamOrderedCache &&) = delete; + StreamOrderedCache &operator=(const StreamOrderedCache &) = delete; + StreamOrderedCache &operator=(StreamOrderedCache &&) = delete; + + ~StreamOrderedCache() noexcept { - waitAndPurge(); + try + { + waitAndPurge(); + } + catch (const std::exception &e) + { + std::fprintf(stderr, "WARNING: failed to purge stream-ordered cache in destructor: %s\n", e.what()); + } + catch (...) + { + std::fputs("WARNING: failed to purge stream-ordered cache in destructor\n", stderr); + } } void waitAndPurge(); @@ -196,7 +219,7 @@ class StreamOrderedCache std::optional get(size_t minSize, size_t minAlignment) { return getIf( - minSize, [=](const Payload &p) + minSize, [minSize, minAlignment](const Payload &p) { return StreamCachePayloadSize(p) >= minSize && StreamCachePayloadAlignment(p) >= minAlignment; }); } @@ -209,7 +232,8 @@ class StreamOrderedCache std::set> m_bySize; - item_t *m_head = nullptr, *m_tail = nullptr; + item_t *m_head = nullptr; + item_t *m_tail = nullptr; }; } // namespace detail @@ -227,7 +251,7 @@ class PerStreamCache { return getIf( minSize, - [=](const Payload &p) + [minSize, minAlignment](const Payload &p) { return StreamCachePayloadSize(p) >= minSize && StreamCachePayloadAlignment(p) >= minAlignment; }, stream); } @@ -261,6 +285,6 @@ class PerStreamCache } // namespace nvcv::util -#include "PerStreamCacheImpl.hpp" +#include "PerStreamCacheImpl.hpp" // NOSONAR: inline definitions require the declarations above. #endif // NVCV_UTIL_PER_STREAM_CACHE_HPP diff --git a/src/cvcuda/util/PerStreamCacheImpl.hpp b/src/cvcuda/util/PerStreamCacheImpl.hpp index dd4ab194f..1a61d301c 100644 --- a/src/cvcuda/util/PerStreamCacheImpl.hpp +++ b/src/cvcuda/util/PerStreamCacheImpl.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -34,8 +34,7 @@ void StreamOrderedCache::waitAndPurge() { if (!ready && m_tail->readyEvent()) { - auto err = cudaEventSynchronize(m_tail->readyEvent()); - if (err != cudaErrorCudartUnloading) + if (auto err = cudaEventSynchronize(m_tail->readyEvent()); err != cudaErrorCudartUnloading) NVCV_CHECK_THROW(err); ready = true; } @@ -96,9 +95,9 @@ void StreamOrderedCache::removeAllReady(PayloadCallback callback) template auto StreamOrderedCache::findNewestReady() -> item_t * { - constexpr int kMaxItemsOnStack = 256; - item_t *tmp[kMaxItemsOnStack]; - item_t *sectionStart = m_tail; + constexpr int kMaxItemsOnStack = 256; + std::array tmp; + item_t *sectionStart = m_tail; // Process the items in blocks of up to kMaxItemsOnStack. On each block, a binary search is performed. while (sectionStart) { @@ -119,7 +118,8 @@ auto StreamOrderedCache::findNewestReady() -> item_t * continue; } - int lo = 0, m = (lo + hi) >> 1; + int lo = 0; + int m = (lo + hi) >> 1; // After this loop, `m` is going to contain the index of the newest ready element while (lo < hi) // exclusive upper bound { @@ -235,16 +235,16 @@ std::optional PerStreamCache::getIf(size_t minSize, Pred std::lock_guard guard(m_lock); - if (stream) + if (stream.has_value()) { - ret = tryGetPerStream(minSize, pred, *stream); + ret = tryGetPerStream(minSize, std::forward(pred), *stream); if (ret) return ret; } do { - ret = tryGetGlobal(minSize, pred); + ret = tryGetGlobal(minSize, std::forward(pred)); if (ret) return ret; } @@ -288,7 +288,7 @@ int PerStreamCache::moveReadyToGlobal() for (auto it = m_perStreamCache.begin(); it != m_perStreamCache.end();) { it->second.removeAllReady( - [&](Payload &&payload) + [this, &moved](Payload &&payload) { m_globalCache.emplace(StreamCachePayloadSize(payload), std::move(payload)); moved++; @@ -311,7 +311,7 @@ void PerStreamCache::put(Payload &&payload, std::optional payload; }; - std::unique_ptr m_items, m_empty; + std::unique_ptr m_items; + std::unique_ptr m_empty; LockType m_lock; }; diff --git a/src/cvcuda/util/StreamId.cpp b/src/cvcuda/util/StreamId.cpp index baff3f54c..2a9383bb7 100644 --- a/src/cvcuda/util/StreamId.cpp +++ b/src/cvcuda/util/StreamId.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -27,7 +27,7 @@ using cuStreamGetId_t = CUresult(CUstream, unsigned long long *); namespace { -cuStreamGetId_t *_cuStreamGetId = cuStreamGetId; +cuStreamGetId_t *const _cuStreamGetId = cuStreamGetId; bool _hasPreciseHint() { @@ -123,8 +123,7 @@ uint64_t GetCudaStreamIdHint(CUstream stream) static auto initResult = cuInit(0); (void)initResult; unsigned long long id; - CUresult err = _cuStreamGetId(stream, &id); - if (err != CUDA_SUCCESS) + if (CUresult err = _cuStreamGetId(stream, &id); err != CUDA_SUCCESS) { switch (err) { diff --git a/src/cvcuda/util/UniqueHandle.hpp b/src/cvcuda/util/UniqueHandle.hpp index 5a2392007..e52c64b1c 100644 --- a/src/cvcuda/util/UniqueHandle.hpp +++ b/src/cvcuda/util/UniqueHandle.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2020-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -53,7 +53,7 @@ class UniqueHandle public: using handle_type = HandleType; - constexpr inline UniqueHandle() + constexpr UniqueHandle() : handle_(Actual::null_handle()) { } @@ -68,13 +68,13 @@ class UniqueHandle UniqueHandle &operator=(const UniqueHandle &) = delete; - inline UniqueHandle(UniqueHandle &&other) + inline UniqueHandle(UniqueHandle &&other) noexcept : handle_(other.handle_) { other.handle_ = Actual::null_handle(); } - inline UniqueHandle &operator=(UniqueHandle &&other) + inline UniqueHandle &operator=(UniqueHandle &&other) noexcept { std::swap(handle_, other.handle_); other.reset(); @@ -101,7 +101,7 @@ class UniqueHandle constexpr handle_type get() && = delete; /// @brief Make the wrapper usable in most context in which the handle type can be used - constexpr operator handle_type() const &noexcept + explicit constexpr operator handle_type() const &noexcept { return get(); } @@ -116,7 +116,7 @@ class UniqueHandle * * If the handle is already null, this function is a no-op. * * The null value to replace the handle with, is taken from `Actual::null_value()`. */ - inline void reset() + inline void reset() noexcept { if (!Actual::is_null_handle(handle_)) { @@ -129,7 +129,7 @@ class UniqueHandle * @brief Replaces the managed handle by the new one and destroying the old handle. * @remarks If `handle` is equal to the currently managed handle, this function is no-op */ - inline void reset(handle_type handle) + inline void reset(handle_type handle) noexcept { if (handle != handle_) { @@ -175,6 +175,7 @@ class UniqueHandle reset(); } +private: handle_type handle_; }; diff --git a/src/nvcv/CMakeLists.txt b/src/nvcv/CMakeLists.txt index 0a1698c36..afafb619c 100644 --- a/src/nvcv/CMakeLists.txt +++ b/src/nvcv/CMakeLists.txt @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,13 +17,10 @@ cmake_minimum_required(VERSION 3.20.1) project(nvcv LANGUAGES C CXX - VERSION 0.16.0 + VERSION 0.17.0 DESCRIPTION "NVCV is NVIDIA Computer Vision library" ) -# Used when creating special builds -set(PROJECT_VERSION_SUFFIX "") - include(CMakeDependentOption) option(NVCV_BUILD_SHARED_LIBS "Builds NVCV as shared library" ON) diff --git a/src/nvcv/cmake/VersionDef.h.in b/src/nvcv/cmake/VersionDef.h.in index 0c8eb76b4..99e6a1c96 100644 --- a/src/nvcv/cmake/VersionDef.h.in +++ b/src/nvcv/cmake/VersionDef.h.in @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -45,7 +45,7 @@ extern "C" { * @returns The numeric version representation. */ #define @LIBPREFIX@_MAKE_VERSION4(major, minor, patch, tweak) \ - ((uint32_t)((major)*1000000 + (minor)*10000 + (patch)*100 + (tweak))) + ((major)*1000000 + (minor)*10000 + (patch)*100 + (tweak)) /** Make a @LIBPREFIX@ version identifier with three components. * diff --git a/src/nvcv/src/Allocator.cpp b/src/nvcv/src/Allocator.cpp index 565e39475..945364f82 100644 --- a/src/nvcv/src/Allocator.cpp +++ b/src/nvcv/src/Allocator.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -37,7 +37,7 @@ NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvAllocatorConstructCustom, NVCVAllocatorHandle *handle)) { return priv::ProtectCall( - [&] + [&handle, &numCustomAllocators, &customAllocators] { if (handle == nullptr) { @@ -58,7 +58,7 @@ NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvAllocatorConstructCustom, NVCV_DEFINE_API(0, 3, NVCVStatus, nvcvAllocatorDecRef, (NVCVAllocatorHandle handle, int *newRefCount)) { return priv::ProtectCall( - [&] + [&handle, &newRefCount] { int newRef = priv::CoreObjectDecRef(handle); if (newRefCount) @@ -69,7 +69,7 @@ NVCV_DEFINE_API(0, 3, NVCVStatus, nvcvAllocatorDecRef, (NVCVAllocatorHandle hand NVCV_DEFINE_API(0, 3, NVCVStatus, nvcvAllocatorIncRef, (NVCVAllocatorHandle handle, int *newRefCount)) { return priv::ProtectCall( - [&] + [&handle, &newRefCount] { int newRef = priv::CoreObjectIncRef(handle); if (newRefCount) @@ -79,23 +79,24 @@ NVCV_DEFINE_API(0, 3, NVCVStatus, nvcvAllocatorIncRef, (NVCVAllocatorHandle hand NVCV_DEFINE_API(0, 3, NVCVStatus, nvcvAllocatorRefCount, (NVCVAllocatorHandle handle, int *refCount)) { - return priv::ProtectCall([&] { *refCount = priv::CoreObjectRefCount(handle); }); + return priv::ProtectCall([&refCount, &handle] { *refCount = priv::CoreObjectRefCount(handle); }); } -NVCV_DEFINE_API(0, 3, NVCVStatus, nvcvAllocatorSetUserPointer, (NVCVAllocatorHandle handle, void *userPtr)) +NVCV_DEFINE_API(0, 3, NVCVStatus, nvcvAllocatorSetUserPointer, (NVCVAllocatorHandle handle, NVCVUserPointer userPtr)) { return priv::ProtectCall( - [&] + [&handle, &userPtr] { auto &img = priv::ToStaticRef(handle); img.setUserPointer(userPtr); }); } -NVCV_DEFINE_API(0, 3, NVCVStatus, nvcvAllocatorGetUserPointer, (NVCVAllocatorHandle handle, void **outUserPtr)) +NVCV_DEFINE_API(0, 3, NVCVStatus, nvcvAllocatorGetUserPointer, + (NVCVAllocatorHandle handle, NVCVUserPointer *outUserPtr)) { return priv::ProtectCall( - [&] + [&outUserPtr, &handle] { if (outUserPtr == nullptr) { @@ -111,7 +112,7 @@ NVCV_DEFINE_API(0, 3, NVCVStatus, nvcvAllocatorGet, (NVCVAllocatorHandle halloc, NVCVResourceType resType, NVCVResourceAllocator *result)) { return priv::ProtectCall( - [&] + [&halloc, &result, &resType] { auto &alloc = priv::ToStaticRef(halloc); *result = alloc.get(resType); @@ -119,10 +120,10 @@ NVCV_DEFINE_API(0, 3, NVCVStatus, nvcvAllocatorGet, } NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvAllocatorAllocHostMemory, - (NVCVAllocatorHandle halloc, void **ptr, int64_t sizeBytes, int32_t alignBytes)) + (NVCVAllocatorHandle halloc, NVCVMemoryBuffer *ptr, int64_t sizeBytes, int32_t alignBytes)) { return priv::ProtectCall( - [&] + [&ptr, &halloc, &sizeBytes, &alignBytes] { if (ptr == nullptr) { @@ -134,10 +135,10 @@ NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvAllocatorAllocHostMemory, } NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvAllocatorFreeHostMemory, - (NVCVAllocatorHandle halloc, void *ptr, int64_t sizeBytes, int32_t alignBytes)) + (NVCVAllocatorHandle halloc, NVCVMemoryBuffer ptr, int64_t sizeBytes, int32_t alignBytes)) { return priv::ProtectCall( - [&] + [&ptr, &halloc, &sizeBytes, &alignBytes] { if (ptr != nullptr) { @@ -147,10 +148,10 @@ NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvAllocatorFreeHostMemory, } NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvAllocatorAllocHostPinnedMemory, - (NVCVAllocatorHandle halloc, void **ptr, int64_t sizeBytes, int32_t alignBytes)) + (NVCVAllocatorHandle halloc, NVCVMemoryBuffer *ptr, int64_t sizeBytes, int32_t alignBytes)) { return priv::ProtectCall( - [&] + [&ptr, &halloc, &sizeBytes, &alignBytes] { if (ptr == nullptr) { @@ -162,10 +163,10 @@ NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvAllocatorAllocHostPinnedMemory, } NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvAllocatorFreeHostPinnedMemory, - (NVCVAllocatorHandle halloc, void *ptr, int64_t sizeBytes, int32_t alignBytes)) + (NVCVAllocatorHandle halloc, NVCVMemoryBuffer ptr, int64_t sizeBytes, int32_t alignBytes)) { return priv::ProtectCall( - [&] + [&ptr, &halloc, &sizeBytes, &alignBytes] { if (ptr != nullptr) { @@ -175,10 +176,10 @@ NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvAllocatorFreeHostPinnedMemory, } NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvAllocatorAllocCudaMemory, - (NVCVAllocatorHandle halloc, void **ptr, int64_t sizeBytes, int32_t alignBytes)) + (NVCVAllocatorHandle halloc, NVCVMemoryBuffer *ptr, int64_t sizeBytes, int32_t alignBytes)) { return priv::ProtectCall( - [&] + [&ptr, &halloc, &sizeBytes, &alignBytes] { if (ptr == nullptr) { @@ -190,10 +191,10 @@ NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvAllocatorAllocCudaMemory, } NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvAllocatorFreeCudaMemory, - (NVCVAllocatorHandle halloc, void *ptr, int64_t sizeBytes, int32_t alignBytes)) + (NVCVAllocatorHandle halloc, NVCVMemoryBuffer ptr, int64_t sizeBytes, int32_t alignBytes)) { return priv::ProtectCall( - [&] + [&ptr, &halloc, &sizeBytes, &alignBytes] { if (ptr != nullptr) { @@ -206,8 +207,8 @@ NVCV_DEFINE_API(0, 4, const char *, nvcvResourceTypeGetName, (NVCVResourceType r { priv::CoreTLS &tls = priv::GetCoreTLS(); - char *buffer = tls.bufResourceTypeName; - constexpr int bufSize = sizeof(tls.bufResourceTypeName); + char *buffer = tls.bufResourceTypeName.data(); + auto bufSize = static_cast(tls.bufResourceTypeName.size()); try { diff --git a/src/nvcv/src/Array.cpp b/src/nvcv/src/Array.cpp index f316d3667..cf245b765 100644 --- a/src/nvcv/src/Array.cpp +++ b/src/nvcv/src/Array.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -36,7 +36,7 @@ NVCV_DEFINE_API(0, 4, NVCVStatus, nvcvArrayCalcRequirements, (int64_t capacity, NVCVDataType dtype, int32_t alignment, NVCVArrayRequirements *reqs)) { return priv::ProtectCall( - [&] + [&reqs, &dtype, &capacity, &alignment] { if (reqs == nullptr) { @@ -54,7 +54,7 @@ NVCV_DEFINE_API(0, 4, NVCVStatus, nvcvArrayCalcRequirementsWithTarget, NVCVArrayRequirements *reqs)) { return priv::ProtectCall( - [&] + [&reqs, &dtype, &capacity, &alignment, &target] { if (reqs == nullptr) { @@ -71,7 +71,7 @@ NVCV_DEFINE_API(0, 4, NVCVStatus, nvcvArrayConstruct, (const NVCVArrayRequirements *reqs, NVCVAllocatorHandle halloc, NVCVArrayHandle *handle)) { return priv::ProtectCall( - [&] + [&reqs, &handle, &halloc] { if (reqs == nullptr) { @@ -94,7 +94,7 @@ NVCV_DEFINE_API(0, 4, NVCVStatus, nvcvArrayConstructWithTarget, NVCVArrayHandle *handle)) { return priv::ProtectCall( - [&] + [&reqs, &handle, &halloc, &target] { if (reqs == nullptr) { @@ -117,7 +117,7 @@ NVCV_DEFINE_API(0, 4, NVCVStatus, nvcvArrayWrapDataConstruct, NVCVArrayHandle *handle)) { return priv::ProtectCall( - [&] + [&data, &handle, &cleanup, &ctxCleanup] { if (data == nullptr) { @@ -129,14 +129,15 @@ NVCV_DEFINE_API(0, 4, NVCVStatus, nvcvArrayWrapDataConstruct, throw priv::Exception(NVCV_ERROR_INVALID_ARGUMENT, "Pointer to output handle must not be NULL"); } - *handle = priv::CreateCoreObject(*data, cleanup, ctxCleanup); + *handle + = priv::CreateCoreObject(*data, cleanup, static_cast(ctxCleanup)); }); } NVCV_DEFINE_API(0, 4, NVCVStatus, nvcvArrayDecRef, (NVCVArrayHandle handle, int *newRefCount)) { return priv::ProtectCall( - [&] + [&handle, &newRefCount] { int newRef = priv::CoreObjectDecRef(handle); if (newRefCount) @@ -147,7 +148,7 @@ NVCV_DEFINE_API(0, 4, NVCVStatus, nvcvArrayDecRef, (NVCVArrayHandle handle, int NVCV_DEFINE_API(0, 4, NVCVStatus, nvcvArrayIncRef, (NVCVArrayHandle handle, int *newRefCount)) { return priv::ProtectCall( - [&] + [&handle, &newRefCount] { int newRef = priv::CoreObjectIncRef(handle); if (newRefCount) @@ -157,23 +158,23 @@ NVCV_DEFINE_API(0, 4, NVCVStatus, nvcvArrayIncRef, (NVCVArrayHandle handle, int NVCV_DEFINE_API(0, 4, NVCVStatus, nvcvArrayRefCount, (NVCVArrayHandle handle, int *refCount)) { - return priv::ProtectCall([&] { *refCount = priv::CoreObjectRefCount(handle); }); + return priv::ProtectCall([&refCount, &handle] { *refCount = priv::CoreObjectRefCount(handle); }); } -NVCV_DEFINE_API(0, 4, NVCVStatus, nvcvArraySetUserPointer, (NVCVArrayHandle handle, void *userPtr)) +NVCV_DEFINE_API(0, 4, NVCVStatus, nvcvArraySetUserPointer, (NVCVArrayHandle handle, NVCVUserPointer userPtr)) { return priv::ProtectCall( - [&] + [&handle, &userPtr] { auto &array = priv::ToStaticRef(handle); array.setUserPointer(userPtr); }); } -NVCV_DEFINE_API(0, 4, NVCVStatus, nvcvArrayGetUserPointer, (NVCVArrayHandle handle, void **outUserPtr)) +NVCV_DEFINE_API(0, 4, NVCVStatus, nvcvArrayGetUserPointer, (NVCVArrayHandle handle, NVCVUserPointer *outUserPtr)) { return priv::ProtectCall( - [&] + [&outUserPtr, &handle] { if (outUserPtr == nullptr) { @@ -189,7 +190,7 @@ NVCV_DEFINE_API(0, 4, NVCVStatus, nvcvArrayGetUserPointer, (NVCVArrayHandle hand NVCV_DEFINE_API(0, 4, NVCVStatus, nvcvArrayGetDataType, (NVCVArrayHandle handle, NVCVDataType *dtype)) { return priv::ProtectCall( - [&] + [&dtype, &handle] { if (dtype == nullptr) { @@ -204,7 +205,7 @@ NVCV_DEFINE_API(0, 4, NVCVStatus, nvcvArrayGetDataType, (NVCVArrayHandle handle, NVCV_DEFINE_API(0, 4, NVCVStatus, nvcvArrayGetAllocator, (NVCVArrayHandle handle, NVCVAllocatorHandle *halloc)) { return priv::ProtectCall( - [&] + [&halloc, &handle] { if (halloc == nullptr) { @@ -220,7 +221,7 @@ NVCV_DEFINE_API(0, 4, NVCVStatus, nvcvArrayGetAllocator, (NVCVArrayHandle handle NVCV_DEFINE_API(0, 4, NVCVStatus, nvcvArrayExportData, (NVCVArrayHandle handle, NVCVArrayData *data)) { return priv::ProtectCall( - [&] + [&data, &handle] { if (data == nullptr) { @@ -235,7 +236,7 @@ NVCV_DEFINE_API(0, 4, NVCVStatus, nvcvArrayExportData, (NVCVArrayHandle handle, NVCV_DEFINE_API(0, 4, NVCVStatus, nvcvArrayGetLength, (NVCVArrayHandle handle, int64_t *length)) { return priv::ProtectCall( - [&] + [&handle, &length] { auto &array = priv::ToStaticRef(handle); @@ -251,7 +252,7 @@ NVCV_DEFINE_API(0, 4, NVCVStatus, nvcvArrayGetLength, (NVCVArrayHandle handle, i NVCV_DEFINE_API(0, 4, NVCVStatus, nvcvArrayGetCapacity, (NVCVArrayHandle handle, int64_t *capacity)) { return priv::ProtectCall( - [&] + [&handle, &capacity] { auto &array = priv::ToStaticRef(handle); @@ -267,7 +268,7 @@ NVCV_DEFINE_API(0, 4, NVCVStatus, nvcvArrayGetCapacity, (NVCVArrayHandle handle, NVCV_DEFINE_API(0, 5, NVCVStatus, nvcvArrayResize, (NVCVArrayHandle handle, int64_t length)) { return priv::ProtectCall( - [&] + [&handle, &length] { auto &array = priv::ToStaticRef(handle); @@ -284,7 +285,7 @@ NVCV_DEFINE_API(0, 5, NVCVStatus, nvcvArrayResize, (NVCVArrayHandle handle, int6 NVCV_DEFINE_API(0, 4, NVCVStatus, nvcvArrayGetTarget, (NVCVArrayHandle handle, NVCVResourceType *target)) { return priv::ProtectCall( - [&] + [&handle, &target] { auto &array = priv::ToStaticRef(handle); diff --git a/src/nvcv/src/ColorSpec.cpp b/src/nvcv/src/ColorSpec.cpp index 0d5592d5f..51310a090 100644 --- a/src/nvcv/src/ColorSpec.cpp +++ b/src/nvcv/src/ColorSpec.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -37,7 +37,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvMakeColorSpec, NVCVChromaLocation locVert)) { return priv::ProtectCall( - [&] + [&outColorSpec, &cspace, &encoding, &xferfunc, &range, &locHoriz, &locVert] { if (outColorSpec == nullptr) { @@ -52,7 +52,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvMakeColorSpec, {locHoriz, locVert} }; - *outColorSpec = pcspec; + *outColorSpec = static_cast(pcspec); }); } @@ -60,7 +60,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvMakeChromaSubsampling, (NVCVChromaSubsampling * outCSS, int samplesHoriz, int samplesVert)) { return priv::ProtectCall( - [&] + [&outCSS, &samplesHoriz, &samplesVert] { if (outCSS == nullptr) { @@ -76,7 +76,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvChromaSubsamplingGetNumSamples, (NVCVChromaSubsampling css, int32_t *outSamplesHoriz, int32_t *outSamplesVert)) { return priv::ProtectCall( - [&] + [&outSamplesHoriz, &outSamplesVert, &css] { if (outSamplesHoriz == nullptr && outSamplesVert == nullptr) { @@ -85,15 +85,15 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvChromaSubsamplingGetNumSamples, "Pointer to output number of horizontal and horizontal samples cannot both be NULL"); } - std::pair nsamp = priv::GetChromaSamples(css); + auto [samplesHoriz, samplesVert] = priv::GetChromaSamples(css); if (outSamplesHoriz != nullptr) { - *outSamplesHoriz = nsamp.first; + *outSamplesHoriz = samplesHoriz; } if (outSamplesVert != nullptr) { - *outSamplesVert = nsamp.second; + *outSamplesVert = samplesVert; } }); } @@ -101,7 +101,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvChromaSubsamplingGetNumSamples, NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvColorSpecSetRange, (NVCVColorSpec * colorSpec, NVCVColorRange range)) { return priv::ProtectCall( - [&] + [&colorSpec, &range] { if (colorSpec == nullptr) { @@ -109,14 +109,14 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvColorSpecSetRange, (NVCVColorSpec * colorS } priv::ColorSpec pcspec{*colorSpec}; - *colorSpec = pcspec.colorRange(range); + *colorSpec = static_cast(pcspec.colorRange(range)); }); } NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvColorSpecGetRange, (NVCVColorSpec colorSpec, NVCVColorRange *outColorRange)) { return priv::ProtectCall( - [&] + [&outColorRange, &colorSpec] { if (outColorRange == nullptr) { @@ -131,7 +131,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvColorSpecGetRange, (NVCVColorSpec colorSpe NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvColorSpecSetColorSpace, (NVCVColorSpec * colorSpec, NVCVColorSpace cspace)) { return priv::ProtectCall( - [&] + [&colorSpec, &cspace] { if (colorSpec == nullptr) { @@ -139,14 +139,14 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvColorSpecSetColorSpace, (NVCVColorSpec * c } priv::ColorSpec pcspec{*colorSpec}; - *colorSpec = pcspec.colorSpace(cspace); + *colorSpec = static_cast(pcspec.colorSpace(cspace)); }); } NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvColorSpecGetColorSpace, (NVCVColorSpec colorSpec, NVCVColorSpace *outColorSpace)) { return priv::ProtectCall( - [&] + [&outColorSpace, &colorSpec] { if (outColorSpace == nullptr) { @@ -162,7 +162,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvColorSpecGetYCbCrEncoding, (NVCVColorSpec colorSpec, NVCVYCbCrEncoding *outEncoding)) { return priv::ProtectCall( - [&] + [&outEncoding, &colorSpec] { if (outEncoding == nullptr) { @@ -178,7 +178,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvColorSpecSetYCbCrEncoding, (NVCVColorSpec * colorSpec, NVCVYCbCrEncoding encoding)) { return priv::ProtectCall( - [&] + [&colorSpec, &encoding] { if (colorSpec == nullptr) { @@ -186,7 +186,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvColorSpecSetYCbCrEncoding, } priv::ColorSpec pcspec{*colorSpec}; - *colorSpec = pcspec.YCbCrEncoding(encoding); + *colorSpec = static_cast(pcspec.YCbCrEncoding(encoding)); }); } @@ -194,7 +194,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvColorSpecSetColorTransferFunction, (NVCVColorSpec * colorSpec, NVCVColorTransferFunction xferFunc)) { return priv::ProtectCall( - [&] + [&colorSpec, &xferFunc] { if (colorSpec == nullptr) { @@ -202,7 +202,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvColorSpecSetColorTransferFunction, } priv::ColorSpec pcspec{*colorSpec}; - *colorSpec = pcspec.xferFunc(xferFunc); + *colorSpec = static_cast(pcspec.xferFunc(xferFunc)); }); } @@ -210,7 +210,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvColorSpecGetColorTransferFunction, (NVCVColorSpec colorSpec, NVCVColorTransferFunction *outXferFunc)) { return priv::ProtectCall( - [&] + [&outXferFunc, &colorSpec] { if (outXferFunc == nullptr) { @@ -227,7 +227,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvColorSpecGetChromaLoc, (NVCVColorSpec colorSpec, NVCVChromaLocation *outLocHoriz, NVCVChromaLocation *outLocVert)) { return priv::ProtectCall( - [&] + [&outLocHoriz, &outLocVert, &colorSpec] { if (outLocHoriz == nullptr && outLocVert == nullptr) { @@ -254,7 +254,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvColorSpecSetChromaLoc, (NVCVColorSpec * colorSpec, NVCVChromaLocation locHoriz, NVCVChromaLocation locVert)) { return priv::ProtectCall( - [&] + [&colorSpec, &locHoriz, &locVert] { if (colorSpec == nullptr) { @@ -262,7 +262,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvColorSpecSetChromaLoc, } priv::ColorSpec pcspec{*colorSpec}; - *colorSpec = pcspec.chromaLoc({locHoriz, locVert}); + *colorSpec = static_cast(pcspec.chromaLoc({locHoriz, locVert})); }); } @@ -270,8 +270,8 @@ NVCV_DEFINE_API(0, 0, const char *, nvcvColorSpecGetName, (NVCVColorSpec cspec)) { priv::CoreTLS &tls = priv::GetCoreTLS(); - char *buffer = tls.bufColorSpecName; - constexpr int bufSize = sizeof(tls.bufColorSpecName); + char *buffer = tls.bufColorSpecName.data(); + auto bufSize = static_cast(tls.bufColorSpecName.size()); try { @@ -303,7 +303,7 @@ NVCV_DEFINE_API(0, 0, const char *, nvcvColorSpecGetName, (NVCVColorSpec cspec)) NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvColorModelNeedsColorspec, (NVCVColorModel cmodel, int8_t *outBool)) { return priv::ProtectCall( - [&] + [&outBool, &cmodel] { if (outBool == nullptr) { diff --git a/src/nvcv/src/Config.cpp b/src/nvcv/src/Config.cpp index c8397bb3a..355a264b5 100644 --- a/src/nvcv/src/Config.cpp +++ b/src/nvcv/src/Config.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -30,7 +30,7 @@ namespace priv = nvcv::priv; NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvConfigSetMaxImageCount, (int32_t maxCount)) { return priv::ProtectCall( - [&] + [&maxCount] { auto &mgr = std::get(priv::GlobalContext().managerList()); if (maxCount >= 0) @@ -47,7 +47,7 @@ NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvConfigSetMaxImageCount, (int32_t maxCount) NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvConfigSetMaxImageBatchCount, (int32_t maxCount)) { return priv::ProtectCall( - [&] + [&maxCount] { auto &mgr = std::get(priv::GlobalContext().managerList()); if (maxCount >= 0) @@ -64,7 +64,7 @@ NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvConfigSetMaxImageBatchCount, (int32_t maxC NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvConfigSetMaxTensorCount, (int32_t maxCount)) { return priv::ProtectCall( - [&] + [&maxCount] { auto &mgr = std::get(priv::GlobalContext().managerList()); if (maxCount >= 0) @@ -81,7 +81,7 @@ NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvConfigSetMaxTensorCount, (int32_t maxCount NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvConfigSetMaxArrayCount, (int32_t maxCount)) { return priv::ProtectCall( - [&] + [&maxCount] { auto &mgr = std::get(priv::GlobalContext().managerList()); if (maxCount >= 0) @@ -98,7 +98,7 @@ NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvConfigSetMaxArrayCount, (int32_t maxCount) NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvConfigSetMaxAllocatorCount, (int32_t maxCount)) { return priv::ProtectCall( - [&] + [&maxCount] { auto &mgr = std::get(priv::GlobalContext().managerList()); if (maxCount >= 0) diff --git a/src/nvcv/src/DataLayout.cpp b/src/nvcv/src/DataLayout.cpp index db0cbe1ba..4007c3a6b 100644 --- a/src/nvcv/src/DataLayout.cpp +++ b/src/nvcv/src/DataLayout.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -34,7 +34,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvMakeSwizzle, (NVCVSwizzle * outSwizzle, NVCVChannel x, NVCVChannel y, NVCVChannel z, NVCVChannel w)) { return priv::ProtectCall( - [&] + [&outSwizzle, &x, &y, &z, &w] { if (outSwizzle == nullptr) { @@ -52,7 +52,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvMakeSwizzle, NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvSwizzleGetChannels, (NVCVSwizzle swizzle, NVCVChannel *outChannels)) { return priv::ProtectCall( - [&] + [&outChannels, &swizzle] { if (outChannels == nullptr) { @@ -68,7 +68,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvSwizzleGetChannels, (NVCVSwizzle swizzle, NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvSwizzleGetNumChannels, (NVCVSwizzle swizzle, int32_t *outNumChannels)) { return priv::ProtectCall( - [&] + [&outNumChannels, &swizzle] { if (outNumChannels == nullptr) { @@ -83,7 +83,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvSwizzleGetNumChannels, (NVCVSwizzle swizzl NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvMakePacking, (NVCVPacking * outPacking, const NVCVPackingParams *params)) { return priv::ProtectCall( - [&] + [¶ms, &outPacking] { if (params == nullptr) { @@ -111,7 +111,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvMakePacking, (NVCVPacking * outPacking, co NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvPackingGetParams, (NVCVPacking packing, NVCVPackingParams *outParams)) { return priv::ProtectCall( - [&] + [&outParams, &packing] { if (outParams == nullptr) { @@ -124,7 +124,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvPackingGetParams, (NVCVPacking packing, NV NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvPackingGetNumComponents, (NVCVPacking packing, int32_t *outNumComponents)) { return priv::ProtectCall( - [&] + [&outNumComponents, &packing] { if (outNumComponents == nullptr) { @@ -139,7 +139,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvPackingGetNumComponents, (NVCVPacking pack NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvPackingGetBitsPerComponent, (NVCVPacking packing, int32_t *outBits)) { return priv::ProtectCall( - [&] + [&outBits, &packing] { if (outBits == nullptr) { @@ -156,7 +156,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvPackingGetBitsPerComponent, (NVCVPacking p NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvPackingGetBitsPerPixel, (NVCVPacking packing, int32_t *outBPP)) { return priv::ProtectCall( - [&] + [&outBPP, &packing] { if (outBPP == nullptr) { @@ -171,7 +171,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvPackingGetBitsPerPixel, (NVCVPacking packi NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvPackingGetAlignment, (NVCVPacking packing, int32_t *outAlignment)) { return priv::ProtectCall( - [&] + [&outAlignment, &packing] { if (outAlignment == nullptr) { diff --git a/src/nvcv/src/DataType.cpp b/src/nvcv/src/DataType.cpp index b0f07302b..b0307aa6d 100644 --- a/src/nvcv/src/DataType.cpp +++ b/src/nvcv/src/DataType.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -36,7 +36,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvMakeDataType, (NVCVDataType * outDataType, NVCVDataKind dataKind, NVCVPacking packing)) { return priv::ProtectCall( - [&] + [&outDataType, &dataKind, &packing] { if (outDataType == nullptr) { @@ -50,7 +50,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvMakeDataType, NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvDataTypeGetPacking, (NVCVDataType type, NVCVPacking *outPacking)) { return priv::ProtectCall( - [&] + [&outPacking, &type] { if (outPacking == nullptr) { @@ -65,7 +65,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvDataTypeGetPacking, (NVCVDataType type, NV NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvDataTypeGetBitsPerPixel, (NVCVDataType type, int32_t *outBPP)) { return priv::ProtectCall( - [&] + [&outBPP, &type] { if (outBPP == nullptr) { @@ -80,7 +80,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvDataTypeGetBitsPerPixel, (NVCVDataType typ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvDataTypeGetBitsPerChannel, (NVCVDataType type, int32_t *outBits)) { return priv::ProtectCall( - [&] + [&outBits, &type] { if (outBits == nullptr) { @@ -97,7 +97,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvDataTypeGetBitsPerChannel, (NVCVDataType t NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvDataTypeGetDataKind, (NVCVDataType type, NVCVDataKind *outDataKind)) { return priv::ProtectCall( - [&] + [&outDataKind, &type] { if (outDataKind == nullptr) { @@ -112,7 +112,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvDataTypeGetDataKind, (NVCVDataType type, N NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvDataTypeGetNumChannels, (NVCVDataType type, int32_t *outNumChannels)) { return priv::ProtectCall( - [&] + [&outNumChannels, &type] { if (outNumChannels == nullptr) { @@ -129,7 +129,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvDataTypeGetChannelType, (NVCVDataType type, int32_t channel, NVCVDataType *outChannelType)) { return priv::ProtectCall( - [&] + [&outChannelType, &type, &channel] { if (outChannelType == nullptr) { @@ -145,8 +145,8 @@ NVCV_DEFINE_API(0, 0, const char *, nvcvDataTypeGetName, (NVCVDataType type)) { priv::CoreTLS &tls = priv::GetCoreTLS(); // noexcept - char *buffer = tls.bufDataTypeName; - constexpr int bufSize = sizeof(tls.bufDataTypeName); + char *buffer = tls.bufDataTypeName.data(); + auto bufSize = static_cast(tls.bufDataTypeName.size()); try { @@ -172,7 +172,7 @@ NVCV_DEFINE_API(0, 0, const char *, nvcvDataTypeGetName, (NVCVDataType type)) NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvDataTypeGetStrideBytes, (NVCVDataType type, int32_t *dtypeStride)) { return priv::ProtectCall( - [&] + [&dtypeStride, &type] { if (dtypeStride == nullptr) { @@ -187,7 +187,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvDataTypeGetStrideBytes, (NVCVDataType type NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvDataTypeGetAlignment, (NVCVDataType type, int32_t *outAlignment)) { return priv::ProtectCall( - [&] + [&outAlignment, &type] { if (outAlignment == nullptr) { diff --git a/src/nvcv/src/Image.cpp b/src/nvcv/src/Image.cpp index 3ec564c38..032797321 100644 --- a/src/nvcv/src/Image.cpp +++ b/src/nvcv/src/Image.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -33,7 +33,7 @@ NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvImageCalcRequirements, NVCVImageRequirements *reqs)) { return priv::ProtectCall( - [&] + [&reqs, &width, &height, &format, &baseAlign, &rowAlign] { if (reqs == nullptr) { @@ -48,7 +48,7 @@ NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvImageConstruct, (const NVCVImageRequirements *reqs, NVCVAllocatorHandle halloc, NVCVImageHandle *handle)) { return priv::ProtectCall( - [&] + [&reqs, &handle, &halloc] { if (reqs == nullptr) { @@ -71,7 +71,7 @@ NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvImageWrapDataConstruct, NVCVImageHandle *handle)) { return priv::ProtectCall( - [&] + [&handle, &data, &cleanup, &ctxCleanup] { if (handle == nullptr) { @@ -83,14 +83,15 @@ NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvImageWrapDataConstruct, throw priv::Exception(NVCV_ERROR_INVALID_ARGUMENT, "Image data must not be NULL"); } - *handle = priv::CreateCoreObject(*data, cleanup, ctxCleanup); + *handle + = priv::CreateCoreObject(*data, cleanup, static_cast(ctxCleanup)); }); } NVCV_DEFINE_API(0, 3, NVCVStatus, nvcvImageDecRef, (NVCVImageHandle handle, int *newRefCount)) { return priv::ProtectCall( - [&] + [&handle, &newRefCount] { int newRef = priv::CoreObjectDecRef(handle); if (newRefCount) @@ -101,7 +102,7 @@ NVCV_DEFINE_API(0, 3, NVCVStatus, nvcvImageDecRef, (NVCVImageHandle handle, int NVCV_DEFINE_API(0, 3, NVCVStatus, nvcvImageIncRef, (NVCVImageHandle handle, int *newRefCount)) { return priv::ProtectCall( - [&] + [&handle, &newRefCount] { int newRef = priv::CoreObjectIncRef(handle); if (newRefCount) @@ -111,13 +112,22 @@ NVCV_DEFINE_API(0, 3, NVCVStatus, nvcvImageIncRef, (NVCVImageHandle handle, int NVCV_DEFINE_API(0, 3, NVCVStatus, nvcvImageRefCount, (NVCVImageHandle handle, int *refCount)) { - return priv::ProtectCall([&] { *refCount = priv::CoreObjectRefCount(handle); }); + return priv::ProtectCall( + [&refCount, &handle] + { + if (refCount == nullptr) + { + throw priv::Exception(NVCV_ERROR_INVALID_ARGUMENT, "Pointer to output reference count cannot be NULL"); + } + + *refCount = priv::CoreObjectRefCount(handle); + }); } NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvImageGetSize, (NVCVImageHandle handle, int32_t *width, int32_t *height)) { return priv::ProtectCall( - [&] + [&width, &height, &handle] { if (width == nullptr) { @@ -140,7 +150,7 @@ NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvImageGetSize, (NVCVImageHandle handle, int NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvImageGetFormat, (NVCVImageHandle handle, NVCVImageFormat *fmt)) { return priv::ProtectCall( - [&] + [&fmt, &handle] { if (fmt == nullptr) { @@ -156,7 +166,7 @@ NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvImageGetFormat, (NVCVImageHandle handle, N NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvImageGetAllocator, (NVCVImageHandle handle, NVCVAllocatorHandle *halloc)) { return priv::ProtectCall( - [&] + [&halloc, &handle] { if (halloc == nullptr) { @@ -172,7 +182,7 @@ NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvImageGetAllocator, (NVCVImageHandle handle NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvImageGetType, (NVCVImageHandle handle, NVCVTypeImage *type)) { return priv::ProtectCall( - [&] + [&type, &handle] { if (type == nullptr) { @@ -188,7 +198,7 @@ NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvImageGetType, (NVCVImageHandle handle, NVC NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvImageExportData, (NVCVImageHandle handle, NVCVImageData *data)) { return priv::ProtectCall( - [&] + [&data, &handle] { if (data == nullptr) { @@ -200,20 +210,20 @@ NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvImageExportData, (NVCVImageHandle handle, }); } -NVCV_DEFINE_API(0, 3, NVCVStatus, nvcvImageSetUserPointer, (NVCVImageHandle handle, void *userPtr)) +NVCV_DEFINE_API(0, 3, NVCVStatus, nvcvImageSetUserPointer, (NVCVImageHandle handle, NVCVUserPointer userPtr)) { return priv::ProtectCall( - [&] + [&handle, &userPtr] { auto &img = priv::ToStaticRef(handle); img.setUserPointer(userPtr); }); } -NVCV_DEFINE_API(0, 3, NVCVStatus, nvcvImageGetUserPointer, (NVCVImageHandle handle, void **outUserPtr)) +NVCV_DEFINE_API(0, 3, NVCVStatus, nvcvImageGetUserPointer, (NVCVImageHandle handle, NVCVUserPointer *outUserPtr)) { return priv::ProtectCall( - [&] + [&outUserPtr, &handle] { if (outUserPtr == nullptr) { diff --git a/src/nvcv/src/ImageBatch.cpp b/src/nvcv/src/ImageBatch.cpp index 0403a875a..a6376d20f 100644 --- a/src/nvcv/src/ImageBatch.cpp +++ b/src/nvcv/src/ImageBatch.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -32,7 +32,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvImageBatchVarShapeCalcRequirements, (int32_t capacity, NVCVImageBatchVarShapeRequirements *reqs)) { return priv::ProtectCall( - [&] + [&reqs, &capacity] { if (reqs == nullptr) { @@ -53,7 +53,7 @@ NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvImageBatchVarShapeConstruct, NVCVImageBatchHandle *handle)) { return priv::ProtectCall( - [&] + [&reqs, &handle, &halloc] { if (reqs == nullptr) { @@ -75,7 +75,7 @@ NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvImageBatchVarShapeConstruct, NVCV_DEFINE_API(0, 3, NVCVStatus, nvcvImageBatchDecRef, (NVCVImageBatchHandle handle, int *newRefCount)) { return priv::ProtectCall( - [&] + [&handle, &newRefCount] { int newRef = priv::CoreObjectDecRef(handle); if (newRefCount) @@ -86,7 +86,7 @@ NVCV_DEFINE_API(0, 3, NVCVStatus, nvcvImageBatchDecRef, (NVCVImageBatchHandle ha NVCV_DEFINE_API(0, 3, NVCVStatus, nvcvImageBatchIncRef, (NVCVImageBatchHandle handle, int *newRefCount)) { return priv::ProtectCall( - [&] + [&handle, &newRefCount] { int newRef = priv::CoreObjectIncRef(handle); if (newRefCount) @@ -96,23 +96,24 @@ NVCV_DEFINE_API(0, 3, NVCVStatus, nvcvImageBatchIncRef, (NVCVImageBatchHandle ha NVCV_DEFINE_API(0, 3, NVCVStatus, nvcvImageBatchRefCount, (NVCVImageBatchHandle handle, int *refCount)) { - return priv::ProtectCall([&] { *refCount = priv::CoreObjectRefCount(handle); }); + return priv::ProtectCall([&refCount, &handle] { *refCount = priv::CoreObjectRefCount(handle); }); } -NVCV_DEFINE_API(0, 3, NVCVStatus, nvcvImageBatchSetUserPointer, (NVCVImageBatchHandle handle, void *userPtr)) +NVCV_DEFINE_API(0, 3, NVCVStatus, nvcvImageBatchSetUserPointer, (NVCVImageBatchHandle handle, NVCVUserPointer userPtr)) { return priv::ProtectCall( - [&] + [&handle, &userPtr] { auto &img = priv::ToStaticRef(handle); img.setUserPointer(userPtr); }); } -NVCV_DEFINE_API(0, 3, NVCVStatus, nvcvImageBatchGetUserPointer, (NVCVImageBatchHandle handle, void **outUserPtr)) +NVCV_DEFINE_API(0, 3, NVCVStatus, nvcvImageBatchGetUserPointer, + (NVCVImageBatchHandle handle, NVCVUserPointer *outUserPtr)) { return priv::ProtectCall( - [&] + [&outUserPtr, &handle] { if (outUserPtr == nullptr) { @@ -127,7 +128,7 @@ NVCV_DEFINE_API(0, 3, NVCVStatus, nvcvImageBatchGetUserPointer, (NVCVImageBatchH NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvImageBatchGetNumImages, (NVCVImageBatchHandle handle, int32_t *size)) { return priv::ProtectCall( - [&] + [&size, &handle] { if (size == nullptr) { @@ -143,7 +144,7 @@ NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvImageBatchGetNumImages, (NVCVImageBatchHan NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvImageBatchGetCapacity, (NVCVImageBatchHandle handle, int32_t *capacity)) { return priv::ProtectCall( - [&] + [&capacity, &handle] { if (capacity == nullptr) { @@ -160,7 +161,7 @@ NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvImageBatchVarShapeGetMaxSize, (NVCVImageBatchHandle handle, int32_t *maxWidth, int32_t *maxHeight)) { return priv::ProtectCall( - [&] + [&maxWidth, &maxHeight, &handle] { if (maxWidth == nullptr && maxHeight == nullptr) { @@ -186,7 +187,7 @@ NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvImageBatchVarShapeGetUniqueFormat, (NVCVImageBatchHandle handle, NVCVImageFormat *fmt)) { return priv::ProtectCall( - [&] + [&fmt, &handle] { if (fmt == nullptr) { @@ -203,7 +204,7 @@ NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvImageBatchGetAllocator, (NVCVImageBatchHandle handle, NVCVAllocatorHandle *halloc)) { return priv::ProtectCall( - [&] + [&halloc, &handle] { if (halloc == nullptr) { @@ -219,7 +220,7 @@ NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvImageBatchGetAllocator, NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvImageBatchGetType, (NVCVImageBatchHandle handle, NVCVTypeImageBatch *type)) { return priv::ProtectCall( - [&] + [&type, &handle] { if (type == nullptr) { @@ -236,7 +237,7 @@ NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvImageBatchExportData, (NVCVImageBatchHandle handle, CUstream stream, NVCVImageBatchData *data)) { return priv::ProtectCall( - [&] + [&data, &handle, &stream] { if (data == nullptr) { @@ -252,7 +253,7 @@ NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvImageBatchVarShapePushImages, (NVCVImageBatchHandle handle, const NVCVImageHandle *images, int32_t numImages)) { return priv::ProtectCall( - [&] + [&handle, &images, &numImages] { auto &batch = priv::ToDynamicRef(handle); @@ -264,18 +265,18 @@ NVCV_DEFINE_API(0, 3, NVCVStatus, nvcvImageBatchVarShapePushImagesCallback, (NVCVImageBatchHandle handle, NVCVPushImageFunc cbPushImage, void *ctxCallback)) { return priv::ProtectCall( - [&] + [&handle, &cbPushImage, &ctxCallback] { auto &batch = priv::ToDynamicRef(handle); - batch.pushImages(cbPushImage, ctxCallback); + batch.pushImages(cbPushImage, static_cast(ctxCallback)); }); } NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvImageBatchVarShapePopImages, (NVCVImageBatchHandle handle, int32_t numImages)) { return priv::ProtectCall( - [&] + [&handle, &numImages] { auto &batch = priv::ToDynamicRef(handle); @@ -286,7 +287,7 @@ NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvImageBatchVarShapePopImages, (NVCVImageBat NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvImageBatchVarShapeClear, (NVCVImageBatchHandle handle)) { return priv::ProtectCall( - [&] + [&handle] { auto &batch = priv::ToDynamicRef(handle); @@ -298,7 +299,7 @@ NVCV_DEFINE_API(0, 3, NVCVStatus, nvcvImageBatchVarShapeGetImages, (NVCVImageBatchHandle handle, int32_t begIndex, NVCVImageHandle *outImages, int32_t numImages)) { return priv::ProtectCall( - [&] + [&outImages, &handle, &begIndex, &numImages] { if (outImages == nullptr) { diff --git a/src/nvcv/src/ImageFormat.cpp b/src/nvcv/src/ImageFormat.cpp index d8ff52141..1302e7b00 100644 --- a/src/nvcv/src/ImageFormat.cpp +++ b/src/nvcv/src/ImageFormat.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -39,14 +39,19 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvMakeColorImageFormat, const NVCVExtraChannelInfo *exChannelInfo)) { return priv::ProtectCall( - [&] + [&outFormat, &colorModel, &colorSpec, &memLayout, &dataKind, &swizzle, &packing0, &packing1, &packing2, + &packing3, &alphaType, &exChannelInfo] { if (outFormat == nullptr) { throw priv::Exception(NVCV_ERROR_INVALID_ARGUMENT, "Pointer to output image format cannot be NULL"); } - priv::ImageFormat pout{colorModel, colorSpec, NVCV_CSS_NONE, memLayout, dataKind, swizzle, - packing0, packing1, packing2, packing3, alphaType, exChannelInfo}; + priv::ImageFormat pout{colorModel, priv::ColorSpec{colorSpec}, + NVCV_CSS_NONE, memLayout, + dataKind, swizzle, + packing0, packing1, + packing2, packing3, + alphaType, exChannelInfo}; *outFormat = pout.value(); }); } @@ -58,7 +63,8 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvMakeYCbCrImageFormat, const NVCVExtraChannelInfo *exChannelInfo)) { return priv::ProtectCall( - [&] + [&outFormat, &colorSpec, &chromaSub, &memLayout, &dataKind, &swizzle, &packing0, &packing1, &packing2, + &packing3, &alphaType, &exChannelInfo] { if (outFormat == nullptr) { @@ -66,7 +72,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvMakeYCbCrImageFormat, } priv::ImageFormat pout{NVCV_COLOR_MODEL_YCbCr, - colorSpec, + priv::ColorSpec{colorSpec}, chromaSub, memLayout, dataKind, @@ -87,7 +93,8 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvMakeNonColorImageFormat, NVCVAlphaType alphaType, const NVCVExtraChannelInfo *exChannelInfo)) { return priv::ProtectCall( - [&] + [&outFormat, &memLayout, &dataKind, &swizzle, &packing0, &packing1, &packing2, &packing3, &alphaType, + &exChannelInfo] { if (outFormat == nullptr) { @@ -106,7 +113,8 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvMakeRawImageFormat, NVCVPacking packing3, NVCVAlphaType alphaType, const NVCVExtraChannelInfo *exChannelInfo)) { return priv::ProtectCall( - [&] + [&outFormat, &rawPattern, &memLayout, &dataKind, &swizzle, &packing0, &packing1, &packing2, &packing3, + &alphaType, &exChannelInfo] { if (outFormat == nullptr) { @@ -123,7 +131,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvImageFormatGetPlanePacking, (NVCVImageFormat fmt, int plane, NVCVPacking *outPacking)) { return priv::ProtectCall( - [&] + [&outPacking, &fmt, &plane] { if (outPacking == nullptr) { @@ -139,7 +147,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvImageFormatGetPlaneBitsPerPixel, (NVCVImageFormat fmt, int32_t plane, int32_t *outBPP)) { return priv::ProtectCall( - [&] + [&outBPP, &fmt, &plane] { if (outBPP == nullptr) { @@ -155,7 +163,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvImageFormatSetSwizzleAndPacking, NVCVPacking newPacking2, NVCVPacking newPacking3)) { return priv::ProtectCall( - [&] + [&fmt, &newSwizzle, &newPacking0, &newPacking1, &newPacking2, &newPacking3] { if (fmt == nullptr) { @@ -170,7 +178,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvImageFormatSetSwizzleAndPacking, NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvImageFormatSetDataKind, (NVCVImageFormat * fmt, NVCVDataKind newDataKind)) { return priv::ProtectCall( - [&] + [&fmt, &newDataKind] { if (fmt == nullptr) { @@ -184,7 +192,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvImageFormatSetDataKind, (NVCVImageFormat * NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvImageFormatGetDataKind, (NVCVImageFormat fmt, NVCVDataKind *outDataKind)) { return priv::ProtectCall( - [&] + [&outDataKind, &fmt] { if (outDataKind == nullptr) { @@ -198,7 +206,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvImageFormatGetDataKind, (NVCVImageFormat f NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvImageFormatGetSwizzle, (NVCVImageFormat fmt, NVCVSwizzle *outSwizzle)) { return priv::ProtectCall( - [&] + [&outSwizzle, &fmt] { if (outSwizzle == nullptr) { @@ -212,7 +220,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvImageFormatGetSwizzle, (NVCVImageFormat fm NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvImageFormatSetMemLayout, (NVCVImageFormat * fmt, NVCVMemLayout newMemLayout)) { return priv::ProtectCall( - [&] + [&fmt, &newMemLayout] { if (fmt == nullptr) { @@ -226,7 +234,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvImageFormatSetMemLayout, (NVCVImageFormat NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvImageFormatGetMemLayout, (NVCVImageFormat fmt, NVCVMemLayout *outMemLayout)) { return priv::ProtectCall( - [&] + [&outMemLayout, &fmt] { if (outMemLayout == nullptr) { @@ -240,21 +248,21 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvImageFormatGetMemLayout, (NVCVImageFormat NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvImageFormatSetColorSpec, (NVCVImageFormat * fmt, NVCVColorSpec newColorSpec)) { return priv::ProtectCall( - [&] + [&fmt, &newColorSpec] { if (fmt == nullptr) { throw priv::Exception(NVCV_ERROR_INVALID_ARGUMENT, "Pointer to input image format cannot be NULL"); } priv::ImageFormat pfmt{*fmt}; - *fmt = pfmt.colorSpec(newColorSpec).value(); + *fmt = pfmt.colorSpec(priv::ColorSpec{newColorSpec}).value(); }); } NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvImageFormatGetColorSpec, (NVCVImageFormat fmt, NVCVColorSpec *outColorSpec)) { return priv::ProtectCall( - [&] + [&outColorSpec, &fmt] { if (outColorSpec == nullptr) { @@ -262,14 +270,14 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvImageFormatGetColorSpec, (NVCVImageFormat } priv::ImageFormat pfmt{fmt}; - *outColorSpec = pfmt.colorSpec(); + *outColorSpec = static_cast(pfmt.colorSpec()); }); } NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvImageFormatGetColorModel, (NVCVImageFormat fmt, NVCVColorModel *outColorModel)) { return priv::ProtectCall( - [&] + [&outColorModel, &fmt] { if (outColorModel == nullptr) { @@ -284,7 +292,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvImageFormatSetChromaSubsampling, (NVCVImageFormat * fmt, NVCVChromaSubsampling newCSS)) { return priv::ProtectCall( - [&] + [&fmt, &newCSS] { if (fmt == nullptr) { @@ -299,7 +307,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvImageFormatGetChromaSubsampling, (NVCVImageFormat fmt, NVCVChromaSubsampling *outCSS)) { return priv::ProtectCall( - [&] + [&outCSS, &fmt] { if (outCSS == nullptr) { @@ -315,7 +323,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvImageFormatGetPlaneNumChannels, (NVCVImageFormat fmt, int32_t plane, int32_t *outNumChannels)) { return priv::ProtectCall( - [&] + [&outNumChannels, &fmt, &plane] { if (outNumChannels == nullptr) { @@ -331,7 +339,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvImageFormatGetPlanePixelStrideBytes, (NVCVImageFormat fmt, int32_t plane, int32_t *outStrideBytes)) { return priv::ProtectCall( - [&] + [&outStrideBytes, &fmt, &plane] { if (outStrideBytes == nullptr) { @@ -345,7 +353,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvImageFormatGetPlanePixelStrideBytes, NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvImageFormatGetBitsPerChannel, (NVCVImageFormat fmt, int32_t *outBits)) { return priv::ProtectCall( - [&] + [&outBits, &fmt] { if (outBits == nullptr) { @@ -363,7 +371,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvImageFormatGetBitsPerChannel, (NVCVImageFo NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvImageFormatGetNumPlanes, (NVCVImageFormat fmt, int32_t *outNumPlanes)) { return priv::ProtectCall( - [&] + [&outNumPlanes, &fmt] { if (outNumPlanes == nullptr) { @@ -377,7 +385,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvImageFormatGetNumPlanes, (NVCVImageFormat NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvImageFormatGetNumChannels, (NVCVImageFormat fmt, int32_t *outNumChannels)) { return priv::ProtectCall( - [&] + [&outNumChannels, &fmt] { if (outNumChannels == nullptr) { @@ -393,7 +401,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvImageFormatGetPlaneDataType, (NVCVImageFormat fmt, int plane, NVCVDataType *outPixType)) { return priv::ProtectCall( - [&] + [&outPixType, &fmt, &plane] { if (outPixType == nullptr) { @@ -408,7 +416,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvImageFormatGetPlaneSwizzle, (NVCVImageFormat fmt, int plane, NVCVSwizzle *outSwizzle)) { return priv::ProtectCall( - [&] + [&outSwizzle, &fmt, &plane] { if (outSwizzle == nullptr) { @@ -424,7 +432,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvMakeImageFormatFromPlanes, NVCVImageFormat plane3)) { return priv::ProtectCall( - [&] + [&outFormat, &plane0, &plane1, &plane2, &plane3] { if (outFormat == nullptr) { @@ -457,7 +465,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvImageFormatGetPlaneFormat, (NVCVImageFormat fmt, int plane, NVCVImageFormat *outFormat)) { return priv::ProtectCall( - [&] + [&outFormat, &fmt, &plane] { if (outFormat == nullptr) { @@ -473,7 +481,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvImageFormatGetPlaneFormat, NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvImageFormatGetRawPattern, (NVCVImageFormat fmt, NVCVRawPattern *outPattern)) { return priv::ProtectCall( - [&] + [&outPattern, &fmt] { if (outPattern == nullptr) { @@ -496,7 +504,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvImageFormatGetRawPattern, (NVCVImageFormat NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvImageFormatSetRawPattern, (NVCVImageFormat * fmt, NVCVRawPattern newRawPattern)) { return priv::ProtectCall( - [&] + [&fmt, &newRawPattern] { if (fmt == nullptr) { @@ -511,13 +519,14 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvImageFormatHasSameDataLayout, (NVCVImageFormat a, NVCVImageFormat b, int8_t *outBool)) { return priv::ProtectCall( - [&] + [&outBool, &a, &b] { if (outBool == nullptr) { throw priv::Exception(NVCV_ERROR_INVALID_ARGUMENT, "Pointer to boolean output cannot be NULL"); } - priv::ImageFormat pfmtA{a}, pfmtB{b}; + priv::ImageFormat pfmtA{a}; + priv::ImageFormat pfmtB{b}; *outBool = HasSameDataLayout(pfmtA, pfmtB) ? 1 : 0; }); } @@ -526,21 +535,21 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvMakeImageFormatFromFourCC, (NVCVImageFormat * outFormat, uint32_t fourcc, NVCVColorSpec colorSpec, NVCVMemLayout memLayout)) { return priv::ProtectCall( - [&] + [&outFormat, &fourcc, &colorSpec, &memLayout] { if (outFormat == nullptr) { throw priv::Exception(NVCV_ERROR_INVALID_ARGUMENT, "Pointer to output image plane format cannot be NULL"); } - *outFormat = priv::ImageFormat::FromFourCC(fourcc, colorSpec, memLayout).value(); + *outFormat = priv::ImageFormat::FromFourCC(fourcc, priv::ColorSpec{colorSpec}, memLayout).value(); }); } NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvImageFormatToFourCC, (NVCVImageFormat fmt, uint32_t *outFourCC)) { return priv::ProtectCall( - [&] + [&outFourCC, &fmt] { if (outFourCC == nullptr) { @@ -556,7 +565,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvImageFormatGetPlaneSize, int32_t *outPlaneHeight)) { return priv::ProtectCall( - [&] + [&outPlaneWidth, &outPlaneHeight, &fmt, &imgWidth, &imgHeight, &plane] { if (outPlaneWidth == nullptr && outPlaneHeight == nullptr) { @@ -580,8 +589,8 @@ NVCV_DEFINE_API(0, 0, const char *, nvcvImageFormatGetName, (NVCVImageFormat fmt { priv::CoreTLS &tls = priv::GetCoreTLS(); // noexcept - char *buffer = tls.bufImageFormatName; - constexpr int bufSize = sizeof(tls.bufImageFormatName); + char *buffer = tls.bufImageFormatName.data(); + auto bufSize = static_cast(tls.bufImageFormatName.size()); try { @@ -617,7 +626,7 @@ NVCV_DEFINE_API(0, 0, const char *, nvcvImageFormatGetName, (NVCVImageFormat fmt NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvImageFormatGetAlphaType, (NVCVImageFormat fmt, NVCVAlphaType *alphaChannelType)) { return priv::ProtectCall( - [&] + [&alphaChannelType, &fmt] { if (alphaChannelType == nullptr) { @@ -631,7 +640,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvImageFormatGetAlphaType, (NVCVImageFormat NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvImageFormatSetAlphaType, (NVCVImageFormat * fmt, NVCVAlphaType alphaChannelType)) { return priv::ProtectCall( - [&] + [&fmt, &alphaChannelType] { if (fmt == nullptr) { @@ -646,7 +655,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvImageFormatGetExtraChannelInfo, (NVCVImageFormat fmt, NVCVExtraChannelInfo *exChannelInfo)) { return priv::ProtectCall( - [&] + [&exChannelInfo, &fmt] { if (exChannelInfo == nullptr) { @@ -661,7 +670,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvImageFormatSetExtraChannelInfo, (NVCVImageFormat * fmt, const NVCVExtraChannelInfo *exChannelInfo)) { return priv::ProtectCall( - [&] + [&fmt, &exChannelInfo] { if (fmt == nullptr) { diff --git a/src/nvcv/src/Requirements.cpp b/src/nvcv/src/Requirements.cpp index c1cf1558c..7819ec6fb 100644 --- a/src/nvcv/src/Requirements.cpp +++ b/src/nvcv/src/Requirements.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -28,7 +28,7 @@ namespace priv = nvcv::priv; NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvRequirementsInit, (NVCVRequirements * reqs)) { return priv::ProtectCall( - [&] + [&reqs] { if (reqs == nullptr) { @@ -42,7 +42,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvRequirementsInit, (NVCVRequirements * reqs NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvRequirementsAdd, (NVCVRequirements * reqSum, const NVCVRequirements *req)) { return priv::ProtectCall( - [&] + [&reqSum, &req] { if (reqSum == nullptr) { @@ -62,7 +62,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvMemRequirementsCalcTotalSizeBytes, (const NVCVMemRequirements *memReq, int64_t *sizeBytes)) { return priv::ProtectCall( - [&] + [&memReq, &sizeBytes] { if (memReq == nullptr) { @@ -83,7 +83,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvMemRequirementsAddBuffer, (NVCVMemRequirements * memReq, int64_t bufSize, int64_t bufAlignment)) { return priv::ProtectCall( - [&] + [&memReq, &bufSize, &bufAlignment] { if (memReq == nullptr) { diff --git a/src/nvcv/src/Status.cpp b/src/nvcv/src/Status.cpp index e462db456..378c80800 100644 --- a/src/nvcv/src/Status.cpp +++ b/src/nvcv/src/Status.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -53,7 +53,7 @@ NVCV_DEFINE_API(0, 0, const char *, nvcvStatusGetName, (NVCVStatus err)) NVCV_DEFINE_API(0, 2, void, nvcvSetThreadStatusVarArgList, (NVCVStatus status, const char *fmt, va_list va)) { NVCVStatus ret = priv::ProtectCall( - [&] + [&fmt, &status, &va] { if (fmt) { @@ -74,7 +74,7 @@ NVCV_DEFINE_API(0, 2, void, nvcvSetThreadStatus, (NVCVStatus status, const char va_start(va, fmt); NVCVStatus ret = priv::ProtectCall( - [&] + [&fmt, &status, &va] { if (fmt) { diff --git a/src/nvcv/src/Tensor.cpp b/src/nvcv/src/Tensor.cpp index 146e04098..f5f73699e 100644 --- a/src/nvcv/src/Tensor.cpp +++ b/src/nvcv/src/Tensor.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -42,7 +42,7 @@ NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvTensorCalcRequirementsForImages, int32_t rowAlign, NVCVTensorRequirements *reqs)) { return priv::ProtectCall( - [&] + [&reqs, &batch, &width, &height, &format, &baseAlign, &rowAlign] { if (reqs == nullptr) { @@ -70,7 +70,7 @@ NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvTensorCalcRequirements, int32_t rowAlign, NVCVTensorRequirements *reqs)) { return priv::ProtectCall( - [&] + [&reqs, &dtype, &rank, &shape, &layout, &baseAlign, &rowAlign] { if (reqs == nullptr) { @@ -87,7 +87,7 @@ NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvTensorConstruct, (const NVCVTensorRequirements *reqs, NVCVAllocatorHandle halloc, NVCVTensorHandle *handle)) { return priv::ProtectCall( - [&] + [&reqs, &handle, &halloc] { if (reqs == nullptr) { @@ -111,7 +111,7 @@ NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvTensorWrapDataConstruct, NVCVTensorHandle *handle)) { return priv::ProtectCall( - [&] + [&data, &handle, &cleanup, &ctxCleanup] { if (data == nullptr) { @@ -123,13 +123,13 @@ NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvTensorWrapDataConstruct, throw priv::Exception(NVCV_ERROR_INVALID_ARGUMENT, "Pointer to output handle must not be NULL"); } - switch (data->bufferType) + if (data->bufferType == NVCV_TENSOR_BUFFER_STRIDED_CUDA) + { + *handle = priv::CreateCoreObject(*data, cleanup, + static_cast(ctxCleanup)); + } + else { - case NVCV_TENSOR_BUFFER_STRIDED_CUDA: - *handle = priv::CreateCoreObject(*data, cleanup, ctxCleanup); - break; - - default: throw priv::Exception(NVCV_ERROR_INVALID_ARGUMENT) << "Image buffer type not supported"; } }); @@ -138,7 +138,7 @@ NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvTensorWrapDataConstruct, NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvTensorWrapImageConstruct, (NVCVImageHandle himg, NVCVTensorHandle *handle)) { return priv::ProtectCall( - [&] + [&himg, &handle] { if (himg == nullptr) { @@ -156,13 +156,13 @@ NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvTensorWrapImageConstruct, (NVCVImageHandle FillTensorData(*img, tensorData); // The cleanup consists of dropping the reference - auto cleanup = [](void *h, const NVCVTensorData *) + auto cleanup = [](auto h, const NVCVTensorData *) { priv::CoreObjectDecRef(static_cast(h)); }; - void *cleanup_ctx = himg; - *handle = priv::CreateCoreObject(tensorData, cleanup, cleanup_ctx); + *handle = priv::CreateCoreObject( + tensorData, cleanup, static_cast(static_cast(himg))); (void)img.release(); // now the image reference is owned by the tensor, so we should release it here }); } @@ -170,7 +170,7 @@ NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvTensorWrapImageConstruct, (NVCVImageHandle NVCV_DEFINE_API(0, 3, NVCVStatus, nvcvTensorDecRef, (NVCVTensorHandle handle, int *newRefCount)) { return priv::ProtectCall( - [&] + [&handle, &newRefCount] { int newRef = priv::CoreObjectDecRef(handle); if (newRefCount) @@ -181,7 +181,7 @@ NVCV_DEFINE_API(0, 3, NVCVStatus, nvcvTensorDecRef, (NVCVTensorHandle handle, in NVCV_DEFINE_API(0, 3, NVCVStatus, nvcvTensorIncRef, (NVCVTensorHandle handle, int *newRefCount)) { return priv::ProtectCall( - [&] + [&handle, &newRefCount] { int newRef = priv::CoreObjectIncRef(handle); if (newRefCount) @@ -191,13 +191,13 @@ NVCV_DEFINE_API(0, 3, NVCVStatus, nvcvTensorIncRef, (NVCVTensorHandle handle, in NVCV_DEFINE_API(0, 3, NVCVStatus, nvcvTensorRefCount, (NVCVTensorHandle handle, int *refCount)) { - return priv::ProtectCall([&] { *refCount = priv::CoreObjectRefCount(handle); }); + return priv::ProtectCall([&refCount, &handle] { *refCount = priv::CoreObjectRefCount(handle); }); } NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvTensorGetLayout, (NVCVTensorHandle handle, NVCVTensorLayout *layout)) { return priv::ProtectCall( - [&] + [&layout, &handle] { if (layout == nullptr) { @@ -213,7 +213,7 @@ NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvTensorGetLayout, (NVCVTensorHandle handle, NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvTensorGetAllocator, (NVCVTensorHandle handle, NVCVAllocatorHandle *halloc)) { return priv::ProtectCall( - [&] + [&halloc, &handle] { if (halloc == nullptr) { @@ -229,7 +229,7 @@ NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvTensorGetAllocator, (NVCVTensorHandle hand NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvTensorExportData, (NVCVTensorHandle handle, NVCVTensorData *data)) { return priv::ProtectCall( - [&] + [&data, &handle] { if (data == nullptr) { @@ -244,7 +244,7 @@ NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvTensorExportData, (NVCVTensorHandle handle NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvTensorGetShape, (NVCVTensorHandle handle, int32_t *rank, int64_t *shape)) { return priv::ProtectCall( - [&] + [&handle, &rank, &shape] { auto &tensor = priv::ToStaticRef(handle); @@ -254,8 +254,7 @@ NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvTensorGetShape, (NVCVTensorHandle handle, } // Number of shape elements to copy - int n = std::min(*rank, tensor.rank()); - if (n > 0) + if (int n = std::min(*rank, tensor.rank()); n > 0) { if (shape == nullptr) { @@ -274,7 +273,7 @@ NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvTensorGetShape, (NVCVTensorHandle handle, NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvTensorGetDataType, (NVCVTensorHandle handle, NVCVDataType *dtype)) { return priv::ProtectCall( - [&] + [&dtype, &handle] { if (dtype == nullptr) { @@ -286,20 +285,20 @@ NVCV_DEFINE_API(0, 2, NVCVStatus, nvcvTensorGetDataType, (NVCVTensorHandle handl }); } -NVCV_DEFINE_API(0, 3, NVCVStatus, nvcvTensorSetUserPointer, (NVCVTensorHandle handle, void *userPtr)) +NVCV_DEFINE_API(0, 3, NVCVStatus, nvcvTensorSetUserPointer, (NVCVTensorHandle handle, NVCVUserPointer userPtr)) { return priv::ProtectCall( - [&] + [&handle, &userPtr] { auto &tensor = priv::ToStaticRef(handle); tensor.setUserPointer(userPtr); }); } -NVCV_DEFINE_API(0, 3, NVCVStatus, nvcvTensorGetUserPointer, (NVCVTensorHandle handle, void **outUserPtr)) +NVCV_DEFINE_API(0, 3, NVCVStatus, nvcvTensorGetUserPointer, (NVCVTensorHandle handle, NVCVUserPointer *outUserPtr)) { return priv::ProtectCall( - [&] + [&outUserPtr, &handle] { if (outUserPtr == nullptr) { @@ -317,7 +316,7 @@ NVCV_DEFINE_API(0, 5, NVCVStatus, nvcvTensorReshape, NVCVTensorHandle *out_handle)) { return priv::ProtectCall( - [&] + [&handle, &out_handle, &rank, &shape, &layout] { if (handle == nullptr) { @@ -338,13 +337,13 @@ NVCV_DEFINE_API(0, 5, NVCVStatus, nvcvTensorReshape, priv::ReshapeTensorData(new_tensor_data, rank, shape, layout); // The cleanup consists of dropping the reference to the handle we reference - auto cleanup = [](void *h, const NVCVTensorData *) + auto cleanup = [](auto h, const NVCVTensorData *) { priv::CoreObjectDecRef(static_cast(h)); }; - void *cleanup_ctx = handle; - *out_handle = priv::CreateCoreObject(new_tensor_data, cleanup, cleanup_ctx); + *out_handle = priv::CreateCoreObject( + new_tensor_data, cleanup, static_cast(static_cast(handle))); (void)tensor_ptr.release(); // we transferred ownership, we can release }); diff --git a/src/nvcv/src/TensorBatch.cpp b/src/nvcv/src/TensorBatch.cpp index 024f745b8..ca3dc9463 100644 --- a/src/nvcv/src/TensorBatch.cpp +++ b/src/nvcv/src/TensorBatch.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -29,7 +29,7 @@ NVCV_DEFINE_API(0, 5, NVCVStatus, nvcvTensorBatchCalcRequirements, (int32_t capacity, NVCVTensorBatchRequirements *reqs)) { return priv::ProtectCall( - [&] + [&reqs, &capacity] { if (reqs == nullptr) { @@ -44,7 +44,7 @@ NVCV_DEFINE_API(0, 5, NVCVStatus, nvcvTensorBatchConstruct, (const NVCVTensorBatchRequirements *reqs, NVCVAllocatorHandle halloc, NVCVTensorBatchHandle *outHandle)) { return priv::ProtectCall( - [&] + [&reqs, &outHandle, &halloc] { if (reqs == nullptr) { @@ -63,7 +63,7 @@ NVCV_DEFINE_API(0, 5, NVCVStatus, nvcvTensorBatchConstruct, NVCV_DEFINE_API(0, 5, NVCVStatus, nvcvTensorBatchClear, (NVCVTensorBatchHandle handle)) { return priv::ProtectCall( - [&] + [&handle] { auto &tb = priv::ToStaticRef(handle); tb.clear(); @@ -74,7 +74,7 @@ NVCV_DEFINE_API(0, 5, NVCVStatus, nvcvTensorBatchPushTensors, (NVCVTensorBatchHandle handle, const NVCVTensorHandle *tensors, int32_t numTensors)) { return priv::ProtectCall( - [&] + [&tensors, &handle, &numTensors] { if (tensors == nullptr) { @@ -88,7 +88,7 @@ NVCV_DEFINE_API(0, 5, NVCVStatus, nvcvTensorBatchPushTensors, NVCV_DEFINE_API(0, 5, NVCVStatus, nvcvTensorBatchDecRef, (NVCVTensorBatchHandle handle, int32_t *newRefCount)) { return priv::ProtectCall( - [&] + [&handle, &newRefCount] { int32_t newRef = priv::CoreObjectDecRef(handle); if (newRefCount) @@ -99,7 +99,7 @@ NVCV_DEFINE_API(0, 5, NVCVStatus, nvcvTensorBatchDecRef, (NVCVTensorBatchHandle NVCV_DEFINE_API(0, 5, NVCVStatus, nvcvTensorBatchIncRef, (NVCVTensorBatchHandle handle, int32_t *newRefCount)) { return priv::ProtectCall( - [&] + [&handle, &newRefCount] { int32_t refCount = priv::CoreObjectIncRef(handle); if (newRefCount) @@ -110,7 +110,7 @@ NVCV_DEFINE_API(0, 5, NVCVStatus, nvcvTensorBatchIncRef, (NVCVTensorBatchHandle NVCV_DEFINE_API(0, 5, NVCVStatus, nvcvTensorBatchRefCount, (NVCVTensorBatchHandle handle, int32_t *outRefCount)) { return priv::ProtectCall( - [&] + [&outRefCount, &handle] { if (outRefCount == nullptr) { @@ -123,13 +123,13 @@ NVCV_DEFINE_API(0, 5, NVCVStatus, nvcvTensorBatchRefCount, (NVCVTensorBatchHandl NVCV_DEFINE_API(0, 5, NVCVStatus, nvcvTensorBatchGetCapacity, (NVCVTensorBatchHandle handle, int32_t *outCapacityPtr)) { return priv::ProtectCall( - [&] + [&outCapacityPtr, &handle] { if (outCapacityPtr == nullptr) { throw priv::Exception(NVCV_ERROR_INVALID_ARGUMENT, "Pointer to capacity must not be NULL"); } - auto &tb = priv::ToStaticRef(handle); + const auto &tb = priv::ToStaticRef(handle); *outCapacityPtr = tb.capacity(); }); } @@ -137,28 +137,28 @@ NVCV_DEFINE_API(0, 5, NVCVStatus, nvcvTensorBatchGetCapacity, (NVCVTensorBatchHa NVCV_DEFINE_API(0, 5, NVCVStatus, nvcvTensorBatchGetRank, (NVCVTensorBatchHandle handle, int32_t *outRankPtr)) { return priv::ProtectCall( - [&] + [&outRankPtr, &handle] { if (outRankPtr == nullptr) { throw priv::Exception(NVCV_ERROR_INVALID_ARGUMENT, "Pointer to rank must not be NULL"); } - auto &tb = priv::ToStaticRef(handle); - *outRankPtr = tb.rank(); + const auto &tb = priv::ToStaticRef(handle); + *outRankPtr = tb.rank(); }); } NVCV_DEFINE_API(0, 5, NVCVStatus, nvcvTensorBatchGetDType, (NVCVTensorBatchHandle handle, NVCVDataType *outDTypePtr)) { return priv::ProtectCall( - [&] + [&outDTypePtr, &handle] { if (outDTypePtr == nullptr) { throw priv::Exception(NVCV_ERROR_INVALID_ARGUMENT, "Pointer to data type must not be NULL"); } - auto &tb = priv::ToStaticRef(handle); - *outDTypePtr = tb.dtype(); + const auto &tb = priv::ToStaticRef(handle); + *outDTypePtr = tb.dtype(); }); } @@ -166,14 +166,14 @@ NVCV_DEFINE_API(0, 5, NVCVStatus, nvcvTensorBatchGetLayout, (NVCVTensorBatchHandle handle, NVCVTensorLayout *outLayoutPtr)) { return priv::ProtectCall( - [&] + [&outLayoutPtr, &handle] { if (outLayoutPtr == nullptr) { throw priv::Exception(NVCV_ERROR_INVALID_ARGUMENT, "Pointer to layout must not be NULL"); } - auto &tb = priv::ToStaticRef(handle); - *outLayoutPtr = tb.layout(); + const auto &tb = priv::ToStaticRef(handle); + *outLayoutPtr = tb.layout(); }); } @@ -181,14 +181,14 @@ NVCV_DEFINE_API(0, 5, NVCVStatus, nvcvTensorBatchGetType, (NVCVTensorBatchHandle handle, NVCVTensorBufferType *outTypePtr)) { return priv::ProtectCall( - [&] + [&outTypePtr, &handle] { if (outTypePtr == nullptr) { throw priv::Exception(NVCV_ERROR_INVALID_ARGUMENT, "Pointer to buffer type must not be NULL"); } - auto &tb = priv::ToStaticRef(handle); - *outTypePtr = tb.type(); + const auto &tb = priv::ToStaticRef(handle); + *outTypePtr = tb.type(); }); } @@ -196,13 +196,13 @@ NVCV_DEFINE_API(0, 5, NVCVStatus, nvcvTensorBatchGetNumTensors, (NVCVTensorBatchHandle handle, int32_t *outNumTensorsPtr)) { return priv::ProtectCall( - [&] + [&outNumTensorsPtr, &handle] { if (outNumTensorsPtr == nullptr) { throw priv::Exception(NVCV_ERROR_INVALID_ARGUMENT, "Pointer to tensors number must not be NULL"); } - auto &tb = priv::ToStaticRef(handle); + const auto &tb = priv::ToStaticRef(handle); *outNumTensorsPtr = tb.numTensors(); }); } @@ -211,13 +211,13 @@ NVCV_DEFINE_API(0, 5, NVCVStatus, nvcvTensorBatchGetAllocator, (NVCVTensorBatchHandle handle, NVCVAllocatorHandle *outAllocatorPtr)) { return priv::ProtectCall( - [&] + [&outAllocatorPtr, &handle] { if (outAllocatorPtr == nullptr) { throw priv::Exception(NVCV_ERROR_INVALID_ARGUMENT, "Pointer to allocator must not be NULL"); } - auto &tb = priv::ToStaticRef(handle); + const auto &tb = priv::ToStaticRef(handle); *outAllocatorPtr = tb.alloc().release()->handle(); }); } @@ -226,7 +226,7 @@ NVCV_DEFINE_API(0, 5, NVCVStatus, nvcvTensorBatchExportData, (NVCVTensorBatchHandle handle, CUstream stream, NVCVTensorBatchData *data)) { return priv::ProtectCall( - [&] + [&data, &handle, &stream] { if (data == nullptr) { @@ -240,7 +240,7 @@ NVCV_DEFINE_API(0, 5, NVCVStatus, nvcvTensorBatchExportData, NVCV_DEFINE_API(0, 5, NVCVStatus, nvcvTensorBatchPopTensors, (NVCVTensorBatchHandle handle, int32_t numTensors)) { return priv::ProtectCall( - [&] + [&handle, &numTensors] { auto &tb = priv::ToStaticRef(handle); tb.popTensors(numTensors); @@ -251,7 +251,7 @@ NVCV_DEFINE_API(0, 5, NVCVStatus, nvcvTensorBatchGetTensors, (NVCVTensorBatchHandle handle, int32_t index, NVCVTensorHandle *outTensors, int32_t numTensors)) { return priv::ProtectCall( - [&] + [&outTensors, &index, &numTensors, &handle] { if (outTensors == nullptr) { @@ -265,7 +265,7 @@ NVCV_DEFINE_API(0, 5, NVCVStatus, nvcvTensorBatchGetTensors, { throw priv::Exception(NVCV_ERROR_INVALID_ARGUMENT, "Number of tensors cannot be negative"); } - auto &tb = priv::ToStaticRef(handle); + const auto &tb = priv::ToStaticRef(handle); tb.getTensors(index, outTensors, numTensors); }); } @@ -274,7 +274,7 @@ NVCV_DEFINE_API(0, 5, NVCVStatus, nvcvTensorBatchSetTensors, (NVCVTensorBatchHandle handle, int32_t index, const NVCVTensorHandle *tensors, int32_t numTensors)) { return priv::ProtectCall( - [&] + [&tensors, &index, &numTensors, &handle] { if (tensors == nullptr) { @@ -293,26 +293,28 @@ NVCV_DEFINE_API(0, 5, NVCVStatus, nvcvTensorBatchSetTensors, }); } -NVCV_DEFINE_API(0, 5, NVCVStatus, nvcvTensorBatchSetUserPointer, (NVCVTensorBatchHandle handle, void *userPointer)) +NVCV_DEFINE_API(0, 5, NVCVStatus, nvcvTensorBatchSetUserPointer, + (NVCVTensorBatchHandle handle, NVCVUserPointer userPointer)) { return priv::ProtectCall( - [&] + [&handle, &userPointer] { auto &tb = priv::ToStaticRef(handle); tb.setUserPointer(userPointer); }); } -NVCV_DEFINE_API(0, 5, NVCVStatus, nvcvTensorBatchGetUserPointer, (NVCVTensorBatchHandle handle, void **outUserPointer)) +NVCV_DEFINE_API(0, 5, NVCVStatus, nvcvTensorBatchGetUserPointer, + (NVCVTensorBatchHandle handle, NVCVUserPointer *outUserPointer)) { return priv::ProtectCall( - [&] + [&outUserPointer, &handle] { if (outUserPointer == nullptr) { throw priv::Exception(NVCV_ERROR_INVALID_ARGUMENT, "Pointer to user poniter must not be NULL"); } - auto &tb = priv::ToStaticRef(handle); + const auto &tb = priv::ToStaticRef(handle); *outUserPointer = tb.userPointer(); }); } diff --git a/src/nvcv/src/TensorLayout.cpp b/src/nvcv/src/TensorLayout.cpp index ea0144a4c..16a66838b 100644 --- a/src/nvcv/src/TensorLayout.cpp +++ b/src/nvcv/src/TensorLayout.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,7 @@ #include "priv/TensorLayout.hpp" -#include "priv//Exception.hpp" +#include "priv/Exception.hpp" #include "priv/Status.hpp" #include "priv/SymbolVersioning.hpp" @@ -28,7 +28,7 @@ namespace priv = nvcv::priv; NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvTensorLayoutMake, (const char *descr, NVCVTensorLayout *layout)) { return priv::ProtectCall( - [&] + [&layout, &descr] { if (layout == nullptr) { @@ -43,7 +43,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvTensorLayoutMakeRange, (const char *beg, const char *end, NVCVTensorLayout *layout)) { return priv::ProtectCall( - [&] + [&layout, &beg, &end] { if (layout == nullptr) { @@ -57,7 +57,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvTensorLayoutMakeRange, NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvTensorLayoutMakeFirst, (NVCVTensorLayout in, int32_t n, NVCVTensorLayout *layout)) { return priv::ProtectCall( - [&] + [&layout, &in, &n] { if (layout == nullptr) { @@ -71,7 +71,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvTensorLayoutMakeFirst, (NVCVTensorLayout i NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvTensorLayoutMakeLast, (NVCVTensorLayout in, int32_t n, NVCVTensorLayout *layout)) { return priv::ProtectCall( - [&] + [&layout, &in, &n] { if (layout == nullptr) { @@ -86,7 +86,7 @@ NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvTensorLayoutMakeSubRange, (NVCVTensorLayout in, int32_t beg, int32_t end, NVCVTensorLayout *layout)) { return priv::ProtectCall( - [&] + [&layout, &in, &beg, &end] { if (layout == nullptr) { diff --git a/src/nvcv/src/TensorShape.cpp b/src/nvcv/src/TensorShape.cpp index 37e185edd..d8056c698 100644 --- a/src/nvcv/src/TensorShape.cpp +++ b/src/nvcv/src/TensorShape.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -27,5 +27,6 @@ namespace priv = nvcv::priv; NVCV_DEFINE_API(0, 0, NVCVStatus, nvcvTensorShapePermute, (NVCVTensorLayout srcLayout, const int64_t *srcShape, NVCVTensorLayout dstLayout, int64_t *dstShape)) { - return priv::ProtectCall([&] { priv::PermuteShape(srcLayout, srcShape, dstLayout, dstShape); }); + return priv::ProtectCall([&srcLayout, &srcShape, &dstLayout, &dstShape] + { priv::PermuteShape(srcLayout, srcShape, dstLayout, dstShape); }); } diff --git a/src/nvcv/src/include/nvcv/Array.h b/src/nvcv/src/include/nvcv/Array.h index b47f4d873..12a118434 100644 --- a/src/nvcv/src/include/nvcv/Array.h +++ b/src/nvcv/src/include/nvcv/Array.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -212,7 +212,7 @@ NVCV_PUBLIC NVCVStatus nvcvArrayRefCount(NVCVArrayHandle handle, int *newRefCoun * @retval #NVCV_ERROR_INVALID_ARGUMENT Some parameter is outside valid range. * @retval #NVCV_SUCCESS Operation executed successfully. */ -NVCV_PUBLIC NVCVStatus nvcvArraySetUserPointer(NVCVArrayHandle handle, void *userPtr); +NVCV_PUBLIC NVCVStatus nvcvArraySetUserPointer(NVCVArrayHandle handle, NVCVUserPointer userPtr); /** Returns the user pointer associated with the array handle. * @@ -226,7 +226,7 @@ NVCV_PUBLIC NVCVStatus nvcvArraySetUserPointer(NVCVArrayHandle handle, void *use * @retval #NVCV_ERROR_INVALID_ARGUMENT Some parameter is outside valid range. * @retval #NVCV_SUCCESS Operation executed successfully. */ -NVCV_PUBLIC NVCVStatus nvcvArrayGetUserPointer(NVCVArrayHandle handle, void **outUserPtr); +NVCV_PUBLIC NVCVStatus nvcvArrayGetUserPointer(NVCVArrayHandle handle, NVCVUserPointer *outUserPtr); /** * Get the type of the array elements (its data type). diff --git a/src/nvcv/src/include/nvcv/Array.hpp b/src/nvcv/src/include/nvcv/Array.hpp index 719f2f067..d995a35e7 100644 --- a/src/nvcv/src/include/nvcv/Array.hpp +++ b/src/nvcv/src/include/nvcv/Array.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -54,18 +54,46 @@ class Array : public CoreResource return exportData().cast(); } - void setUserPointer(void *ptr); - void *userPointer() const; + void setUserPointer(NVCVUserPointer ptr); + NVCVUserPointer userPointer() const; static Requirements CalcRequirements(int64_t capacity, DataType dtype, int32_t alignment = 0, NVCVResourceType target = NVCV_RESOURCE_MEM_CUDA); - NVCV_IMPLEMENT_SHARED_RESOURCE(Array, Base); + using Base::Base; + using Base::operator=; + + Array(const Array &other) + : Base(other) + { + } + + Array(Array &&other) noexcept + : Base(std::move(other)) + { + } + + Array &operator=(const Array &other) + { + Base::operator=(other); + return *this; + } + + Array &operator=(Array &&other) noexcept + { + Base::operator=(std::move(other)); + return *this; + } + + ~Array() + { + this->reset(); + } explicit Array(const Requirements &reqs, NVCVResourceType target = NVCV_RESOURCE_MEM_CUDA, - const Allocator &alloc = nullptr); + const Allocator &alloc = Allocator{nullptr}); explicit Array(int64_t capacity, DataType dtype, int32_t alignment = 0, - NVCVResourceType target = NVCV_RESOURCE_MEM_CUDA, const Allocator &alloc = nullptr); + NVCVResourceType target = NVCV_RESOURCE_MEM_CUDA, const Allocator &alloc = Allocator{nullptr}); }; // ArrayWrapData definition ------------------------------------- @@ -92,6 +120,6 @@ using ArrayWrapHandle = NonOwningResource; } // namespace nvcv -#include "detail/ArrayImpl.hpp" +#include "detail/ArrayImpl.hpp" // NOSONAR: inline definitions require the declarations above. #endif // NVCV_ARRAY_HPP diff --git a/src/nvcv/src/include/nvcv/ArrayData.hpp b/src/nvcv/src/include/nvcv/ArrayData.hpp index 5a1494094..2b319140b 100644 --- a/src/nvcv/src/include/nvcv/ArrayData.hpp +++ b/src/nvcv/src/include/nvcv/ArrayData.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -29,7 +29,7 @@ namespace nvcv { class ArrayData { public: - ArrayData(const NVCVArrayData &data); + explicit ArrayData(const NVCVArrayData &data); int rank() const; int64_t length() const; @@ -73,7 +73,7 @@ class ArrayDataCuda : public ArrayData public: using Buffer = NVCVArrayBufferStrided; - ArrayDataCuda(const NVCVArrayData &data); + explicit ArrayDataCuda(const NVCVArrayData &data); ArrayDataCuda(int64_t length, const DataType &dtype, const Buffer &buffer); static bool IsCompatibleKind(NVCVArrayBufferType kind) @@ -87,7 +87,7 @@ class ArrayDataHost : public ArrayData public: using Buffer = NVCVArrayBufferStrided; - ArrayDataHost(const NVCVArrayData &data); + explicit ArrayDataHost(const NVCVArrayData &data); ArrayDataHost(int64_t length, const DataType &dtype, const Buffer &buffer); static bool IsCompatibleKind(NVCVArrayBufferType kind) @@ -101,7 +101,7 @@ class ArrayDataHostPinned : public ArrayData public: using Buffer = NVCVArrayBufferStrided; - ArrayDataHostPinned(const NVCVArrayData &data); + explicit ArrayDataHostPinned(const NVCVArrayData &data); ArrayDataHostPinned(int64_t length, const DataType &dtype, const Buffer &buffer); static bool IsCompatibleKind(NVCVArrayBufferType kind) @@ -112,6 +112,6 @@ class ArrayDataHostPinned : public ArrayData } // namespace nvcv -#include "detail/ArrayDataImpl.hpp" +#include "detail/ArrayDataImpl.hpp" // NOSONAR: inline definitions require the declarations above. #endif // NVCV_ARRAYDATA_HPP diff --git a/src/nvcv/src/include/nvcv/ArrayDataAccess.hpp b/src/nvcv/src/include/nvcv/ArrayDataAccess.hpp index 6450cbfba..0ac17c2d9 100644 --- a/src/nvcv/src/include/nvcv/ArrayDataAccess.hpp +++ b/src/nvcv/src/include/nvcv/ArrayDataAccess.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -105,9 +105,7 @@ class ArrayDataAccessImpl } protected: - ArrayType m_data; - - ArrayDataAccessImpl(const ArrayType &data) + explicit ArrayDataAccessImpl(const ArrayType &data) : m_data{data} , m_length{data.length()} , m_idxShift{0} @@ -147,6 +145,7 @@ class ArrayDataAccessImpl } private: + ArrayType m_data; int64_t m_length; int64_t m_idxShift; difference_type m_memShift; @@ -168,11 +167,13 @@ class ArrayDataAccess : public detail::ArrayDataAccessImpl auto castData = data.cast(); if (castData) { - return ArrayDataAccess{castData.value(), length, start}; + return Optional{ + ArrayDataAccess{castData.value(), length, start} + }; } else { - return NullOpt; + return Optional{NullOpt}; } } @@ -199,11 +200,13 @@ class ArrayDataAccessHost : public detail::ArrayDataAccessImpl auto castData = data.cast(); if (castData) { - return ArrayDataAccessHost{castData.value(), length, start}; + return Optional{ + ArrayDataAccessHost{castData.value(), length, start} + }; } else { - return NullOpt; + return Optional{NullOpt}; } } @@ -230,11 +233,13 @@ class ArrayDataAccessHostPinned : public detail::ArrayDataAccessImpl(); if (castData) { - return ArrayDataAccessHostPinned{castData.value(), length, start}; + return Optional{ + ArrayDataAccessHostPinned{castData.value(), length, start} + }; } else { - return NullOpt; + return Optional{NullOpt}; } } @@ -261,11 +266,13 @@ class ArrayDataAccessCuda : public detail::ArrayDataAccessImpl auto castData = data.cast(); if (castData) { - return ArrayDataAccessCuda{castData.value(), length, start}; + return Optional{ + ArrayDataAccessCuda{castData.value(), length, start} + }; } else { - return NullOpt; + return Optional{NullOpt}; } } diff --git a/src/nvcv/src/include/nvcv/Casts.hpp b/src/nvcv/src/include/nvcv/Casts.hpp index ede4f1c7c..62b6f391a 100644 --- a/src/nvcv/src/include/nvcv/Casts.hpp +++ b/src/nvcv/src/include/nvcv/Casts.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/src/nvcv/src/include/nvcv/ColorSpec.hpp b/src/nvcv/src/include/nvcv/ColorSpec.hpp index 08de090fa..26ce739f0 100644 --- a/src/nvcv/src/include/nvcv/ColorSpec.hpp +++ b/src/nvcv/src/include/nvcv/ColorSpec.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -135,7 +135,7 @@ class ColorSpec * @brief Construct a new ColorSpec object. * @param cspec Existing NVCVColorSpec object. */ - constexpr ColorSpec(NVCVColorSpec cspec) + explicit constexpr ColorSpec(NVCVColorSpec cspec) : m_cspec(cspec) { } @@ -172,7 +172,7 @@ class ColorSpec * * @return NVCVColorSpec object. */ - constexpr operator NVCVColorSpec() const; + explicit constexpr operator NVCVColorSpec() const; /** * @brief Set the chroma location and return a new ColorSpec. @@ -262,26 +262,26 @@ class ColorSpec }; #ifndef DOXYGEN_SHOULD_SKIP_THIS -constexpr ColorSpec CSPEC_UNDEFINED = NVCV_COLOR_SPEC_UNDEFINED; -constexpr ColorSpec CSPEC_BT601 = NVCV_COLOR_SPEC_BT601; -constexpr ColorSpec CSPEC_BT601_ER = NVCV_COLOR_SPEC_BT601_ER; -constexpr ColorSpec CSPEC_BT709 = NVCV_COLOR_SPEC_BT709; -constexpr ColorSpec CSPEC_BT709_ER = NVCV_COLOR_SPEC_BT709_ER; -constexpr ColorSpec CSPEC_BT709_LINEAR = NVCV_COLOR_SPEC_BT709_LINEAR; -constexpr ColorSpec CSPEC_BT2020 = NVCV_COLOR_SPEC_BT2020; -constexpr ColorSpec CSPEC_BT2020_ER = NVCV_COLOR_SPEC_BT2020_ER; -constexpr ColorSpec CSPEC_BT2020_LINEAR = NVCV_COLOR_SPEC_BT2020_LINEAR; -constexpr ColorSpec CSPEC_BT2020_PQ = NVCV_COLOR_SPEC_BT2020_PQ; -constexpr ColorSpec CSPEC_BT2020_PQ_ER = NVCV_COLOR_SPEC_BT2020_PQ_ER; -constexpr ColorSpec CSPEC_BT2020c_ER = NVCV_COLOR_SPEC_BT2020c_ER; -constexpr ColorSpec CSPEC_MPEG2_BT601 = NVCV_COLOR_SPEC_MPEG2_BT601; -constexpr ColorSpec CSPEC_MPEG2_BT709 = NVCV_COLOR_SPEC_MPEG2_BT709; -constexpr ColorSpec CSPEC_MPEG2_SMPTE240M = NVCV_COLOR_SPEC_MPEG2_SMPTE240M; -constexpr ColorSpec CSPEC_sRGB = NVCV_COLOR_SPEC_sRGB; -constexpr ColorSpec CSPEC_sYCC = NVCV_COLOR_SPEC_sYCC; -constexpr ColorSpec CSPEC_SMPTE240M = NVCV_COLOR_SPEC_SMPTE240M; -constexpr ColorSpec CSPEC_DISPLAYP3 = NVCV_COLOR_SPEC_DISPLAYP3; -constexpr ColorSpec CSPEC_DISPLAYP3_LINEAR = NVCV_COLOR_SPEC_DISPLAYP3_LINEAR; +constexpr ColorSpec CSPEC_UNDEFINED = ColorSpec{NVCV_COLOR_SPEC_UNDEFINED}; +constexpr ColorSpec CSPEC_BT601 = ColorSpec{NVCV_COLOR_SPEC_BT601}; +constexpr ColorSpec CSPEC_BT601_ER = ColorSpec{NVCV_COLOR_SPEC_BT601_ER}; +constexpr ColorSpec CSPEC_BT709 = ColorSpec{NVCV_COLOR_SPEC_BT709}; +constexpr ColorSpec CSPEC_BT709_ER = ColorSpec{NVCV_COLOR_SPEC_BT709_ER}; +constexpr ColorSpec CSPEC_BT709_LINEAR = ColorSpec{NVCV_COLOR_SPEC_BT709_LINEAR}; +constexpr ColorSpec CSPEC_BT2020 = ColorSpec{NVCV_COLOR_SPEC_BT2020}; +constexpr ColorSpec CSPEC_BT2020_ER = ColorSpec{NVCV_COLOR_SPEC_BT2020_ER}; +constexpr ColorSpec CSPEC_BT2020_LINEAR = ColorSpec{NVCV_COLOR_SPEC_BT2020_LINEAR}; +constexpr ColorSpec CSPEC_BT2020_PQ = ColorSpec{NVCV_COLOR_SPEC_BT2020_PQ}; +constexpr ColorSpec CSPEC_BT2020_PQ_ER = ColorSpec{NVCV_COLOR_SPEC_BT2020_PQ_ER}; +constexpr ColorSpec CSPEC_BT2020c_ER = ColorSpec{NVCV_COLOR_SPEC_BT2020c_ER}; +constexpr ColorSpec CSPEC_MPEG2_BT601 = ColorSpec{NVCV_COLOR_SPEC_MPEG2_BT601}; +constexpr ColorSpec CSPEC_MPEG2_BT709 = ColorSpec{NVCV_COLOR_SPEC_MPEG2_BT709}; +constexpr ColorSpec CSPEC_MPEG2_SMPTE240M = ColorSpec{NVCV_COLOR_SPEC_MPEG2_SMPTE240M}; +constexpr ColorSpec CSPEC_sRGB = ColorSpec{NVCV_COLOR_SPEC_sRGB}; +constexpr ColorSpec CSPEC_sYCC = ColorSpec{NVCV_COLOR_SPEC_sYCC}; +constexpr ColorSpec CSPEC_SMPTE240M = ColorSpec{NVCV_COLOR_SPEC_SMPTE240M}; +constexpr ColorSpec CSPEC_DISPLAYP3 = ColorSpec{NVCV_COLOR_SPEC_DISPLAYP3}; +constexpr ColorSpec CSPEC_DISPLAYP3_LINEAR = ColorSpec{NVCV_COLOR_SPEC_DISPLAYP3_LINEAR}; #endif constexpr ColorSpec ColorSpec::ConstCreate(ColorSpace cspace, YCbCrEncoding encoding, ColorTransferFunction xferFunc, @@ -309,14 +309,16 @@ constexpr ColorSpec::operator NVCVColorSpec() const inline ChromaLocation ColorSpec::chromaLocHoriz() const { - NVCVChromaLocation outH, outV; + NVCVChromaLocation outH; + NVCVChromaLocation outV; detail::CheckThrow(nvcvColorSpecGetChromaLoc(m_cspec, &outH, &outV)); return static_cast(outH); } inline ChromaLocation ColorSpec::chromaLocVert() const { - NVCVChromaLocation outH, outV; + NVCVChromaLocation outH; + NVCVChromaLocation outV; detail::CheckThrow(nvcvColorSpecGetChromaLoc(m_cspec, &outH, &outV)); return static_cast(outV); } @@ -450,7 +452,7 @@ inline std::ostream &operator<<(std::ostream &out, WhitePoint whitePoint) inline std::ostream &operator<<(std::ostream &out, ColorSpace color_space) { - return out << nvcvColorSpecGetName(static_cast(color_space)); + return out << nvcvColorSpaceGetName(static_cast(color_space)); } inline std::ostream &operator<<(std::ostream &out, ChromaLocation loc) diff --git a/src/nvcv/src/include/nvcv/Config.hpp b/src/nvcv/src/include/nvcv/Config.hpp index b53f1609b..c02c50712 100644 --- a/src/nvcv/src/include/nvcv/Config.hpp +++ b/src/nvcv/src/include/nvcv/Config.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -27,7 +27,7 @@ #include "Config.h" #include "detail/CheckError.hpp" -namespace nvcv { namespace cfg { +namespace nvcv { namespace cfg { // NOSONAR: public header compatibility is validated with C++11. /** * @brief Sets the maximum number of image handles that can be created. diff --git a/src/nvcv/src/include/nvcv/CoreResource.hpp b/src/nvcv/src/include/nvcv/CoreResource.hpp index 1c7d1ac6a..195d5a9eb 100644 --- a/src/nvcv/src/include/nvcv/CoreResource.hpp +++ b/src/nvcv/src/include/nvcv/CoreResource.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -46,7 +46,7 @@ class CoreResource : private SharedHandle /** * @brief A constructor that constructs an empty `CoreResource` from a nullptr. */ - CoreResource(std::nullptr_t) {} + explicit CoreResource(std::nullptr_t) {} /** Wraps and assumes ownership of a handle. * @@ -56,7 +56,7 @@ class CoreResource : private SharedHandle * * @param handle The handle to the resource. */ - explicit CoreResource(HandleType &&handle) + explicit CoreResource(HandleType &&handle) noexcept : Base(std::move(handle)) { } @@ -80,7 +80,7 @@ class CoreResource : private SharedHandle * * @param other The other instance to copy from. */ - CoreResource(const Actual &other) + explicit CoreResource(const Actual &other) : Base(other) { } @@ -90,7 +90,7 @@ class CoreResource : private SharedHandle * * @param other The other instance to move from. */ - CoreResource(Actual &&other) + explicit CoreResource(Actual &&other) noexcept : Base(std::move(other)) { } @@ -113,7 +113,7 @@ class CoreResource : private SharedHandle * @param other The other instance to move from. * @return This instance after the move. */ - CoreResource &operator=(Actual &&other) + CoreResource &operator=(Actual &&other) noexcept { Base::operator=(std::move(other)); return *this; @@ -124,7 +124,7 @@ class CoreResource : private SharedHandle * * @return The handle to the resource. */ - const HandleType handle() const noexcept + HandleType handle() const noexcept { return this->get(); } @@ -199,7 +199,7 @@ class CoreResource : private SharedHandle : BaseClassName(other) \ { \ } \ - ClassName(ClassName &&other) \ + ClassName(ClassName &&other) noexcept \ : BaseClassName(std::move(other)) \ { \ } \ @@ -208,10 +208,14 @@ class CoreResource : private SharedHandle BaseClassName::operator=(other); \ return *this; \ } \ - ClassName &operator=(ClassName &&other) \ + ClassName &operator=(ClassName &&other) noexcept \ { \ BaseClassName::operator=(std::move(other)); \ return *this; \ + } \ + ~ClassName() \ + { \ + this->reset(); \ } /** A non-owning wrapper around a handle which can be trivially converted to a reference-counting wrapper @@ -252,7 +256,7 @@ class NonOwningResource * * @param handle The handle to the resource. */ - NonOwningResource(HandleType handle) + explicit NonOwningResource(HandleType handle) : m_resource(std::move(handle)) { } @@ -263,9 +267,12 @@ class NonOwningResource NonOwningResource(const NonOwningResource &) = delete; /** - * @brief The move constructor is defaulted. + * @brief Moves a non-owning resource wrapper. */ - NonOwningResource(NonOwningResource &&) = default; + NonOwningResource(NonOwningResource &&other) noexcept + : m_resource(std::move(other.m_resource)) + { + } /** * @brief The copy assignment operator is deleted to prevent copying. @@ -273,16 +280,24 @@ class NonOwningResource NonOwningResource &operator=(const NonOwningResource &) = delete; /** - * @brief The move assignment operator is defaulted. + * @brief Moves a non-owning resource wrapper. */ - NonOwningResource &operator=(NonOwningResource &&) = default; + NonOwningResource &operator=(NonOwningResource &&other) noexcept + { + if (this != &other) + { + (void)m_resource.release(); + m_resource = std::move(other.m_resource); + } + return *this; + } /** * @brief Returns the handle to the resource. * * @return The handle to the resource. */ - const HandleType handle() const + HandleType handle() const { return m_resource.handle(); } @@ -296,15 +311,25 @@ class NonOwningResource } /** - * @brief Conversion operator to the underlying resource type. + * @brief Returns the underlying resource. * * @return A const reference to the underlying resource. */ - operator const Resource &() const & + const Resource &resource() const & { return m_resource; } + /** + * @brief Conversion operator to the underlying resource type. + * + * @return A const reference to the underlying resource. + */ + explicit operator const Resource &() const & + { + return resource(); + } + private: Resource m_resource; }; diff --git a/src/nvcv/src/include/nvcv/DataLayout.hpp b/src/nvcv/src/include/nvcv/DataLayout.hpp index b7dcc48c1..a1966d749 100644 --- a/src/nvcv/src/include/nvcv/DataLayout.hpp +++ b/src/nvcv/src/include/nvcv/DataLayout.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -287,8 +287,8 @@ inline Swizzle MakeSwizzle(Channel x, Channel y, Channel z, Channel w) inline std::array GetChannels(Swizzle swizzle) { - NVCVChannel channels[4]; - detail::CheckThrow(nvcvSwizzleGetChannels(static_cast(swizzle), channels)); + std::array channels; + detail::CheckThrow(nvcvSwizzleGetChannels(static_cast(swizzle), channels.data())); return {static_cast(channels[0]), static_cast(channels[1]), static_cast(channels[2]), static_cast(channels[3])}; @@ -301,10 +301,6 @@ inline int32_t GetNumChannels(Swizzle swizzle) return out; } -#ifdef BIG_ENDIAN -# undef BIG_ENDIAN -#endif - enum class ByteOrder : int8_t { LSB, @@ -359,9 +355,9 @@ inline int32_t GetNumComponents(Packing packing) inline std::array GetBitsPerComponent(Packing packing) { - int32_t bits[4]; - detail::CheckThrow(nvcvPackingGetBitsPerComponent(static_cast(packing), bits)); - return {bits[0], bits[1], bits[2], bits[3]}; + std::array bits; + detail::CheckThrow(nvcvPackingGetBitsPerComponent(static_cast(packing), bits.data())); + return bits; } inline int32_t GetBitsPerPixel(Packing packing) diff --git a/src/nvcv/src/include/nvcv/DataType.hpp b/src/nvcv/src/include/nvcv/DataType.hpp index 68d85ebe8..0fc922e2e 100644 --- a/src/nvcv/src/include/nvcv/DataType.hpp +++ b/src/nvcv/src/include/nvcv/DataType.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -82,11 +82,17 @@ class DataType static constexpr DataType ConstCreate(DataKind dataKind, Packing packing); /** - * @brief Implicit conversion to the native data type. + * @brief Explicit conversion to the native data type. * * @return The native data type. */ - constexpr operator NVCVDataType() const; + explicit constexpr operator NVCVDataType() const; + + constexpr bool operator==(DataType that) const noexcept; + constexpr bool operator!=(DataType that) const noexcept; + constexpr bool operator<(DataType that) const noexcept; + constexpr bool operator==(NVCVDataType that) const noexcept; + constexpr bool operator!=(NVCVDataType that) const noexcept; // Accessors for data type properties Packing packing() const; @@ -243,6 +249,31 @@ constexpr DataType::operator NVCVDataType() const return m_type; } +constexpr bool DataType::operator==(DataType that) const noexcept +{ + return m_type == that.m_type; +} + +constexpr bool DataType::operator!=(DataType that) const noexcept +{ + return !operator==(that); +} + +constexpr bool DataType::operator<(DataType that) const noexcept +{ + return m_type < that.m_type; +} + +constexpr bool DataType::operator==(NVCVDataType that) const noexcept +{ + return m_type == that; +} + +constexpr bool DataType::operator!=(NVCVDataType that) const noexcept +{ + return !operator==(that); +} + inline Packing DataType::packing() const { NVCVPacking out; @@ -259,9 +290,9 @@ inline int32_t DataType::bitsPerPixel() const inline std::array DataType::bitsPerChannel() const { - int32_t bits[4]; - detail::CheckThrow(nvcvDataTypeGetBitsPerChannel(m_type, bits)); - return {bits[0], bits[1], bits[2], bits[3]}; + std::array bits; + detail::CheckThrow(nvcvDataTypeGetBitsPerChannel(m_type, bits.data())); + return bits; } inline DataKind DataType::dataKind() const @@ -299,6 +330,11 @@ inline int32_t DataType::alignment() const return out; } +inline const char *nvcvDataTypeGetName(DataType type) +{ + return ::nvcvDataTypeGetName(static_cast(type)); +} + inline std::ostream &operator<<(std::ostream &out, DataType type) { return out << nvcvDataTypeGetName(type); diff --git a/src/nvcv/src/include/nvcv/Exception.hpp b/src/nvcv/src/include/nvcv/Exception.hpp index 9f729ec76..f029a3acb 100644 --- a/src/nvcv/src/include/nvcv/Exception.hpp +++ b/src/nvcv/src/include/nvcv/Exception.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,14 +25,20 @@ #define NVCV_EXCEPTION_HPP #include +#include +#include #include +#include #include +#include +#include +#include namespace nvcv { namespace detail { -void ThrowException(NVCVStatus status); +[[noreturn]] void ThrowException(NVCVStatus status); } /** @@ -54,22 +60,24 @@ class Exception : public std::exception * * @param code The error status code. * @param fmt The format string for the error message. - * @param ... The format arguments. + * @param args The format arguments. */ - explicit Exception(Status code, const char *fmt = nullptr, ...) -#if __GNUC__ - __attribute__((format(printf, 3, 4))) -#endif - : m_code(code) + explicit Exception(Status code) + : Exception(code, "%s", "") + { + } + + explicit Exception(Status code, const char *msg) + : Exception(code, "%s", msg != nullptr ? msg : "") { - va_list va; - va_start(va, fmt); - nvcvSetThreadStatusVarArgList(static_cast(code), fmt, va); - va_end(va); + } - va_start(va, fmt); - doSetMessage(fmt, va); - va_end(va); + template + explicit Exception(Status code, const char (&fmt)[N], Args &&...args) + : m_code(code) + { + doSetMessage(fmt, std::forward(args)...); + nvcvSetThreadStatus(static_cast(code), "%s", m_msg); } /** @@ -101,7 +109,7 @@ class Exception : public std::exception */ const char *what() const noexcept override { - return m_msgBuffer; + return m_msgBuffer.data(); } private: @@ -110,7 +118,7 @@ class Exception : public std::exception // 64: maximum size of string representation of a status enum // 2: ': ' - char m_msgBuffer[NVCV_MAX_STATUS_MESSAGE_LENGTH + 64 + 2]; + std::array m_msgBuffer; friend void detail::ThrowException(NVCVStatus status); @@ -118,36 +126,42 @@ class Exception : public std::exception { }; - // Constructor that doesn't set the C thread status. - // Used when converting C statuses to C++. - Exception(InternalCtorTag, Status code, const char *fmt = nullptr, ...) -#if __GNUC__ - __attribute__((format(printf, 4, 5))) -#endif - : m_code(code) + Exception(InternalCtorTag, Status code) + : Exception(InternalCtorTag{}, code, "%s", "") { - va_list va; - va_start(va, fmt); + } - doSetMessage(fmt, va); + Exception(InternalCtorTag, Status code, const char *msg) + : Exception(InternalCtorTag{}, code, "%s", msg != nullptr ? msg : "") + { + } - va_end(va); + // Constructor that doesn't set the C thread status. Used when converting C statuses to C++. + template + Exception(InternalCtorTag, Status code, const char (&fmt)[N], Args &&...args) + : m_code(code) + { + doSetMessage(fmt, std::forward(args)...); } - void doSetMessage(const char *fmt, va_list va) + template + void doSetMessage(const char (&fmt)[N], Args &&...args) { - int buflen = sizeof(m_msgBuffer); - int nwritten = snprintf(m_msgBuffer, buflen, "%s: ", GetName(m_code)); + auto buflen = static_cast(m_msgBuffer.size()); // no truncation? - if (nwritten < buflen) + detail::FormatTo(m_msgBuffer.data(), m_msgBuffer.size(), "%s: ", GetName(m_code)); + + int nwritten = static_cast(std::char_traits::length(m_msgBuffer.data())); + if (nwritten < buflen - 1) { buflen -= nwritten; - m_msg = m_msgBuffer + nwritten; - vsnprintf(m_msgBuffer + nwritten, buflen, fmt, va); + m_msg = m_msgBuffer.data() + nwritten; + detail::FormatTo(m_msgBuffer.data() + nwritten, static_cast(buflen), fmt, + std::forward(args)...); } - m_msgBuffer[sizeof(m_msgBuffer) - 1] = '\0'; + m_msgBuffer.back() = '\0'; } }; @@ -180,15 +194,35 @@ inline void SetThreadError(std::exception_ptr e) { nvcvSetThreadStatus(NVCV_ERROR_INVALID_ARGUMENT, "%s", e.what()); } + catch (const std::domain_error &e) + { + nvcvSetThreadStatus(NVCV_ERROR_INTERNAL, "%s", e.what()); + } + catch (const std::length_error &e) + { + nvcvSetThreadStatus(NVCV_ERROR_INTERNAL, "%s", e.what()); + } + catch (const std::out_of_range &e) + { + nvcvSetThreadStatus(NVCV_ERROR_INTERNAL, "%s", e.what()); + } catch (const std::bad_alloc &) { nvcvSetThreadStatus(NVCV_ERROR_OUT_OF_MEMORY, "Not enough space for resource allocation"); } - catch (const std::exception &e) + catch (const std::range_error &e) + { + nvcvSetThreadStatus(NVCV_ERROR_INTERNAL, "%s", e.what()); + } + catch (const std::overflow_error &e) + { + nvcvSetThreadStatus(NVCV_ERROR_INTERNAL, "%s", e.what()); + } + catch (const std::underflow_error &e) { nvcvSetThreadStatus(NVCV_ERROR_INTERNAL, "%s", e.what()); } - catch (...) + catch (...) // NOSONAR: API boundary converts any non-standard exception to NVCV status. { nvcvSetThreadStatus(NVCV_ERROR_INTERNAL, "Unexpected error"); } @@ -214,7 +248,7 @@ NVCVStatus ProtectCall(F &&fn) fn(); return NVCV_SUCCESS; } - catch (...) + catch (...) // NOSONAR: this API boundary translates any exception to an NVCVStatus. { SetThreadError(std::current_exception()); return nvcvPeekAtLastError(); diff --git a/src/nvcv/src/include/nvcv/HandleWrapper.hpp b/src/nvcv/src/include/nvcv/HandleWrapper.hpp index faf8dc2b5..bd877fa76 100644 --- a/src/nvcv/src/include/nvcv/HandleWrapper.hpp +++ b/src/nvcv/src/include/nvcv/HandleWrapper.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -127,7 +127,7 @@ class UniqueHandle * emphasize the transfer of ownership. */ explicit UniqueHandle(HandleType &&handle) - : m_handle(handle) + : m_handle(std::move(handle)) { handle = HandleOps::Null(); } @@ -135,7 +135,7 @@ class UniqueHandle UniqueHandle() = default; UniqueHandle(const UniqueHandle &other) = delete; - UniqueHandle(UniqueHandle &&other) + UniqueHandle(UniqueHandle &&other) noexcept : m_handle(std::move(other.m_handle)) { other.m_handle = HandleOps::Null(); @@ -143,7 +143,7 @@ class UniqueHandle /** Moves the handle owned by `other` into this object and releases the old handle. */ - UniqueHandle &operator=(UniqueHandle &&other) + UniqueHandle &operator=(UniqueHandle &&other) noexcept { if (&other == this) return *this; // avoid self-reset in self-move @@ -168,7 +168,7 @@ class UniqueHandle * @remarks Passing a non-empty handle that's already owned by this UniqueHandle is forbidden and will * result in double destruction of the handle. */ - void reset(HandleType &&handle = HandleOps::Null()) + void reset(HandleType &&handle = HandleOps::Null()) noexcept { assert(HandleOps::IsNull(handle) || handle != m_handle); if (*this) @@ -197,7 +197,7 @@ class UniqueHandle * * @return The managed handle. */ - constexpr const HandleType get() const noexcept + constexpr HandleType get() const noexcept { return m_handle; } @@ -212,7 +212,7 @@ class UniqueHandle return !empty(); } - bool operator==(const UniqueHandle &other) const + bool operator==(const UniqueHandle &other) const // NOSONAR: defaulted comparisons are C++20. { return m_handle == other.m_handle; } @@ -255,7 +255,7 @@ class SharedHandle * The reference count on the handle is _not_ incremented. */ explicit SharedHandle(HandleType &&handle) noexcept - : m_handle(handle) + : m_handle(std::move(handle)) { handle = HandleOps::Null(); } @@ -275,7 +275,7 @@ class SharedHandle /** Moves the handle owned by `other` into this object and releases the old handle. */ - SharedHandle &operator=(SharedHandle &&other) + SharedHandle &operator=(SharedHandle &&other) noexcept { if (&other == this) return *this; // we must not reset the "other" in case of self-move @@ -327,7 +327,7 @@ class SharedHandle * the object was destroyed or the handle was already null. If it's >0, the object * still had some live references. */ - int reset(HandleType &&handle = HandleOps::Null()) + int reset(HandleType &&handle = HandleOps::Null()) noexcept { auto old = m_handle; m_handle = std::move(handle); @@ -353,7 +353,7 @@ class SharedHandle /** Returns the currently managed handle. */ - constexpr const HandleType get() const noexcept + constexpr HandleType get() const noexcept { return m_handle; } @@ -374,7 +374,7 @@ class SharedHandle return !empty(); } - bool operator==(const SharedHandle &other) const + bool operator==(const SharedHandle &other) const // NOSONAR: defaulted comparisons are C++20. { return m_handle == other.m_handle; } diff --git a/src/nvcv/src/include/nvcv/Image.h b/src/nvcv/src/include/nvcv/Image.h index 92a7f3783..f0738dc8f 100644 --- a/src/nvcv/src/include/nvcv/Image.h +++ b/src/nvcv/src/include/nvcv/Image.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -60,8 +60,9 @@ typedef void (*NVCVImageDataCleanupFunc)(void *ctx, const NVCVImageData *data); /** Stores the requirements of an image. */ typedef struct NVCVImageRequirementsRec { - int32_t width, height; /*< Image dimensions. */ - NVCVImageFormat format; /*< Image format. */ + int32_t width; /*< Image width. */ + int32_t height; /*< Image height. */ + NVCVImageFormat format; /*< Image format. */ /** Row stride of each plane, in bytes */ int32_t planeRowStride[NVCV_MAX_PLANE_COUNT]; @@ -203,7 +204,7 @@ NVCV_PUBLIC NVCVStatus nvcvImageRefCount(NVCVImageHandle handle, int *newRefCoun * @retval #NVCV_ERROR_INVALID_ARGUMENT Some parameter is outside valid range. * @retval #NVCV_SUCCESS Operation executed successfully. */ -NVCV_PUBLIC NVCVStatus nvcvImageSetUserPointer(NVCVImageHandle handle, void *userPtr); +NVCV_PUBLIC NVCVStatus nvcvImageSetUserPointer(NVCVImageHandle handle, NVCVUserPointer userPtr); /** Returns the user pointer associated with the image handle. * @@ -217,7 +218,7 @@ NVCV_PUBLIC NVCVStatus nvcvImageSetUserPointer(NVCVImageHandle handle, void *use * @retval #NVCV_ERROR_INVALID_ARGUMENT Some parameter is outside valid range. * @retval #NVCV_SUCCESS Operation executed successfully. */ -NVCV_PUBLIC NVCVStatus nvcvImageGetUserPointer(NVCVImageHandle handle, void **outUserPtr); +NVCV_PUBLIC NVCVStatus nvcvImageGetUserPointer(NVCVImageHandle handle, NVCVUserPointer *outUserPtr); /** Returns the underlying image type. * diff --git a/src/nvcv/src/include/nvcv/Image.hpp b/src/nvcv/src/include/nvcv/Image.hpp index 65d67fcab..0895fcc31 100644 --- a/src/nvcv/src/include/nvcv/Image.hpp +++ b/src/nvcv/src/include/nvcv/Image.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -58,7 +58,35 @@ class Image : public CoreResource */ static Requirements CalcRequirements(const Size2D &size, ImageFormat fmt, const MemAlignment &bufAlign = {}); - NVCV_IMPLEMENT_SHARED_RESOURCE(Image, Base); + using Base::Base; + using Base::operator=; + + Image(const Image &other) + : Base(other) + { + } + + Image(Image &&other) noexcept + : Base(std::move(other)) + { + } + + Image &operator=(const Image &other) + { + Base::operator=(other); + return *this; + } + + Image &operator=(Image &&other) noexcept + { + Base::operator=(std::move(other)); + return *this; + } + + ~Image() + { + this->reset(); + } /** * @brief Construct an Image with specific requirements. @@ -66,7 +94,7 @@ class Image : public CoreResource * @param reqs The requirements for the image. * @param alloc The allocator to use (optional). */ - explicit Image(const Requirements &reqs, const Allocator &alloc = nullptr); + explicit Image(const Requirements &reqs, const Allocator &alloc = Allocator{nullptr}); /** * @brief Construct an Image with specified size, format, and alignment. @@ -76,7 +104,7 @@ class Image : public CoreResource * @param alloc The allocator to use (optional). * @param bufAlign The memory alignment (optional). */ - explicit Image(const Size2D &size, ImageFormat fmt, const Allocator &alloc = nullptr, + explicit Image(const Size2D &size, ImageFormat fmt, const Allocator &alloc = Allocator{nullptr}, const MemAlignment &bufAlign = {}); /** @@ -114,14 +142,14 @@ class Image : public CoreResource * * @param ptr The pointer to set. */ - void setUserPointer(void *ptr); + void setUserPointer(NVCVUserPointer ptr); /** * @brief Retrieve the user-defined pointer associated with the image. * * @return The user pointer. */ - void *userPointer() const; + NVCVUserPointer userPointer() const; }; // ImageWrapData definition ------------------------------------- @@ -149,6 +177,6 @@ using ImageWrapHandle = NonOwningResource; } // namespace nvcv -#include "detail/ImageImpl.hpp" +#include "detail/ImageImpl.hpp" // NOSONAR: inline definitions require the declarations above. #endif // NVCV_IMAGE_HPP diff --git a/src/nvcv/src/include/nvcv/ImageBatch.h b/src/nvcv/src/include/nvcv/ImageBatch.h index 14d655c13..00a9b6e83 100644 --- a/src/nvcv/src/include/nvcv/ImageBatch.h +++ b/src/nvcv/src/include/nvcv/ImageBatch.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -154,7 +154,7 @@ NVCV_PUBLIC NVCVStatus nvcvImageBatchRefCount(NVCVImageBatchHandle handle, int * * @retval #NVCV_ERROR_INVALID_ARGUMENT Some parameter is outside valid range. * @retval #NVCV_SUCCESS Operation executed successfully. */ -NVCV_PUBLIC NVCVStatus nvcvImageBatchSetUserPointer(NVCVImageBatchHandle handle, void *userPtr); +NVCV_PUBLIC NVCVStatus nvcvImageBatchSetUserPointer(NVCVImageBatchHandle handle, NVCVUserPointer userPtr); /** Returns the user pointer associated with the image batch handle. * @@ -168,7 +168,7 @@ NVCV_PUBLIC NVCVStatus nvcvImageBatchSetUserPointer(NVCVImageBatchHandle handle, * @retval #NVCV_ERROR_INVALID_ARGUMENT Some parameter is outside valid range. * @retval #NVCV_SUCCESS Operation executed successfully. */ -NVCV_PUBLIC NVCVStatus nvcvImageBatchGetUserPointer(NVCVImageBatchHandle handle, void **outUserPtr); +NVCV_PUBLIC NVCVStatus nvcvImageBatchGetUserPointer(NVCVImageBatchHandle handle, NVCVUserPointer *outUserPtr); /** Returns the underlying type of the image batch. * diff --git a/src/nvcv/src/include/nvcv/ImageBatch.hpp b/src/nvcv/src/include/nvcv/ImageBatch.hpp index 6d4e2c03f..6b990dc15 100644 --- a/src/nvcv/src/include/nvcv/ImageBatch.hpp +++ b/src/nvcv/src/include/nvcv/ImageBatch.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,6 +23,10 @@ #include "ImageBatch.h" #include "ImageBatchData.hpp" #include "Optional.hpp" +#include "detail/TypeTraits.hpp" + +#include +#include namespace nvcv { @@ -41,7 +45,35 @@ class ImageBatch : public CoreResource public: using Base = CoreResource; - NVCV_IMPLEMENT_SHARED_RESOURCE(ImageBatch, Base); + using Base::Base; + using Base::operator=; + + ImageBatch(const ImageBatch &other) + : Base(other) + { + } + + ImageBatch(ImageBatch &&other) noexcept + : Base(std::move(other)) + { + } + + ImageBatch &operator=(const ImageBatch &other) + { + Base::operator=(other); + return *this; + } + + ImageBatch &operator=(ImageBatch &&other) noexcept + { + Base::operator=(std::move(other)); + return *this; + } + + ~ImageBatch() + { + this->reset(); + } using HandleType = NVCVImageBatchHandle; @@ -89,14 +121,14 @@ class ImageBatch : public CoreResource * * @param ptr The pointer to set. */ - void setUserPointer(void *ptr); + void setUserPointer(NVCVUserPointer ptr); /** * @brief Retrieve the user-defined pointer associated with the image batch. * * @return The user pointer. */ - void *userPointer() const; + NVCVUserPointer userPointer() const; /** * @brief Check if a kind is compatible with the ImageBatch class. @@ -142,15 +174,26 @@ class ImageBatchVarShape : public ImageBatch */ static Requirements CalcRequirements(int32_t capacity); - NVCV_IMPLEMENT_SHARED_RESOURCE(ImageBatchVarShape, ImageBatch); + using ImageBatch::ImageBatch; + using ImageBatch::operator=; + + ImageBatchVarShape(const ImageBatchVarShape &other) = default; + + ImageBatchVarShape(ImageBatchVarShape &&other) noexcept = default; + + ImageBatchVarShape &operator=(const ImageBatchVarShape &other) = default; + + ImageBatchVarShape &operator=(ImageBatchVarShape &&other) noexcept = default; + + ~ImageBatchVarShape() = default; explicit ImageBatchVarShape(NVCVImageBatchHandle &&handle); ///< Construct from an existing NVCV handle. - ImageBatchVarShape(const ImageBatch &batch); ///< Construct from an existing `ImageBatch`. - ImageBatchVarShape(ImageBatch &&batch); ///< Move construct from an existing `ImageBatch`. + explicit ImageBatchVarShape(const ImageBatch &batch); ///< Construct from an existing `ImageBatch`. + explicit ImageBatchVarShape(ImageBatch &&batch); ///< Move construct from an existing `ImageBatch`. explicit ImageBatchVarShape(const Requirements &reqs, - const Allocator &alloc = nullptr); ///< Construct with specific requirements. + const Allocator &alloc = Allocator{nullptr}); ///< Construct with specific requirements. explicit ImageBatchVarShape(int32_t capacity, - const Allocator &alloc = nullptr); ///< Construct with a specified capacity. + const Allocator &alloc = Allocator{nullptr}); ///< Construct with a specified capacity. ImageBatchVarShape &operator=(const ImageBatch &batch); ImageBatchVarShape &operator=(ImageBatch &&batch); @@ -212,19 +255,25 @@ class ImageBatchVarShape : public ImageBatch ConstIterator cbegin() const; ConstIterator cend() const; - using ImageBatch::exportData; - /** * @brief Export the underlying data of the image batch. * * @param stream The CUDA stream. * @return The image batch data. */ + template(), int> = 0> ImageBatchVarShapeData exportData(CUstream stream) const { return *ImageBatch::template exportData(stream); } + template(), int> = 0> + Optional exportData(CUstream stream) const + { + return ImageBatch::template exportData(stream); + } + /** * @brief Check if a specific kind is compatible with the ImageBatchVarShape class. * @@ -250,7 +299,12 @@ class ImageBatchVarShape::Iterator using iterator_category = std::random_access_iterator_tag; using difference_type = int32_t; - Iterator() = default; + Iterator() = default; + Iterator(const Iterator &) = default; + Iterator(Iterator &&) noexcept = default; + + Iterator &operator=(const Iterator &) = default; + Iterator &operator=(Iterator &&) noexcept = default; reference operator*() const; pointer operator->() const; @@ -290,11 +344,15 @@ class ImageBatchVarShape::Iterator using OptionalImageBatchVarShapeConstRef = nvcv::Optional>; -#define NVCV_IMAGE_BATCH_VAR_SHAPE_HANDLE_TO_OPTIONAL(X) \ - X ? nvcv::OptionalImageBatchVarShapeConstRef(nvcv::ImageBatchVarShapeWrapHandle{X}) : nvcv::NullOpt +#define NVCV_IMAGE_BATCH_VAR_SHAPE_HANDLE_TO_OPTIONAL(X) \ + X ? nvcv::OptionalImageBatchVarShapeConstRef(nvcv::ImageBatchVarShapeWrapHandle{X}.resource()) \ + : nvcv::OptionalImageBatchVarShapeConstRef \ + { \ + nvcv::NullOpt \ + } } // namespace nvcv -#include "detail/ImageBatchImpl.hpp" +#include "detail/ImageBatchImpl.hpp" // NOSONAR: inline definitions require the declarations above. #endif // NVCV_IMAGEBATCH_HPP diff --git a/src/nvcv/src/include/nvcv/ImageBatchData.h b/src/nvcv/src/include/nvcv/ImageBatchData.h index 6ac31108d..d50123c9c 100644 --- a/src/nvcv/src/include/nvcv/ImageBatchData.h +++ b/src/nvcv/src/include/nvcv/ImageBatchData.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,7 +23,7 @@ #include /** Stores the image plane in a variable shape image batch. */ -typedef struct NVCVImageBatchVarShapeBufferStridedRec +typedef struct NVCVImageBatchVarShapeBufferStridedRec // NOSONAR: C ABI typedef. { /** Format of all images in the batch. * If images don't have all the same format, or the batch is empty, @@ -33,7 +33,8 @@ typedef struct NVCVImageBatchVarShapeBufferStridedRec /** Union of all image dimensions. * If 0 and number of images is >= 1, this value * must not be relied upon. */ - int32_t maxWidth, maxHeight; + int32_t maxWidth; + int32_t maxHeight; /** Pointer to an array of formats, one for each image in `imageList`. */ NVCVImageFormat *formatList; @@ -50,7 +51,7 @@ typedef struct NVCVImageBatchVarShapeBufferStridedRec } NVCVImageBatchVarShapeBufferStrided; /** Stores the tensor plane contents. */ -typedef struct NVCVImageBatchTensorBufferStridedRec +typedef struct NVCVImageBatchTensorBufferStridedRec // NOSONAR: C ABI typedef. { /** Distance in bytes from beginning of first plane of one image to the * first plane of the next image. @@ -63,18 +64,19 @@ typedef struct NVCVImageBatchTensorBufferStridedRec /** Dimensions of each image. * + Must be >= 1x1 */ - int32_t imgWidth, imgHeight; + int32_t imgWidth; + int32_t imgHeight; /** Buffer of all image planes in pitch-linear layout. * It assumes all planes have same dimension specified by imgWidth/imgHeight, * and that all planes have the same row pitch. * + Only the first N elements must have valid data, where N is the number of planes * defined by @ref NVCVImageBatchData::format. */ - void *planeBuffer[NVCV_MAX_PLANE_COUNT]; + void *planeBuffer[NVCV_MAX_PLANE_COUNT]; // NOSONAR: C ABI fixed-capacity plane buffer. } NVCVImageBatchTensorBufferStrided; /** Represents how the image buffer data is stored. */ -typedef enum +typedef enum // NOSONAR: C ABI typedef. { /** Invalid buffer type. * This is commonly used to inform that no buffer type was selected. */ @@ -86,7 +88,7 @@ typedef enum /** Represents the available methods to access image batch contents. * The correct method depends on \ref NVCVImageBatchData::bufferType. */ -typedef union NVCVImageBatchBufferRec +typedef union NVCVImageBatchBufferRec // NOSONAR: C ABI typedef. { /** Varshape image batch stored in pitch-linear layout. * To be used when \ref NVCVImageBatchData::bufferType is: @@ -96,7 +98,7 @@ typedef union NVCVImageBatchBufferRec } NVCVImageBatchBuffer; /** Stores information about image batch characteristics and content. */ -typedef struct NVCVImageBatchDataRec +typedef struct NVCVImageBatchDataRec // NOSONAR: C ABI typedef. { /** Number of images in the image batch */ int32_t numImages; diff --git a/src/nvcv/src/include/nvcv/ImageBatchData.hpp b/src/nvcv/src/include/nvcv/ImageBatchData.hpp index 9ad06bfeb..84228e4c9 100644 --- a/src/nvcv/src/include/nvcv/ImageBatchData.hpp +++ b/src/nvcv/src/include/nvcv/ImageBatchData.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -63,7 +63,7 @@ class ImageBatchData * * @param data The NVCVImageBatchData to use for initialization. */ - ImageBatchData(const NVCVImageBatchData &data) + explicit ImageBatchData(const NVCVImageBatchData &data) : m_data(data) { } @@ -212,6 +212,6 @@ class ImageBatchVarShapeDataStridedCuda : public ImageBatchVarShapeDataStrided } // namespace nvcv -#include "detail/ImageBatchDataImpl.hpp" +#include "detail/ImageBatchDataImpl.hpp" // NOSONAR: inline definitions require the declarations above. #endif // NVCV_IMAGEBATCHDATA_HPP diff --git a/src/nvcv/src/include/nvcv/ImageData.h b/src/nvcv/src/include/nvcv/ImageData.h index 5d06fb196..a7ddf8652 100644 --- a/src/nvcv/src/include/nvcv/ImageData.h +++ b/src/nvcv/src/include/nvcv/ImageData.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -24,7 +24,7 @@ #include #include -typedef struct NVCVImagePlaneStridedRec +typedef struct NVCVImagePlaneStridedRec // NOSONAR: C ABI typedef. { /** Width of this plane in pixels. * + It must be >= 1. */ @@ -54,7 +54,7 @@ typedef struct NVCVImagePlaneStridedRec #define NVCV_MAX_PLANE_COUNT (6) /** Stores the image plane contents. */ -typedef struct NVCVImageBufferStridedRec +typedef struct NVCVImageBufferStridedRec // NOSONAR: C ABI typedef. { /** Number of planes. * + Must be >= 1. */ @@ -62,10 +62,10 @@ typedef struct NVCVImageBufferStridedRec /** Data of all image planes in pitch-linear layout. * + Only the first \ref numPlanes elements must have valid data. */ - NVCVImagePlaneStrided planes[NVCV_MAX_PLANE_COUNT]; + NVCVImagePlaneStrided planes[NVCV_MAX_PLANE_COUNT]; // NOSONAR: C ABI fixed-capacity plane buffer. } NVCVImageBufferStrided; -typedef struct NVCVImageBufferCudaArrayRec +typedef struct NVCVImageBufferCudaArrayRec // NOSONAR: C ABI typedef. { /** Number of planes. * + Must be >= 1. */ @@ -73,11 +73,11 @@ typedef struct NVCVImageBufferCudaArrayRec /** Data of all image planes in pitch-linear layout. * + Only the first \ref numPlanes elements must have valid data. */ - cudaArray_t planes[NVCV_MAX_PLANE_COUNT]; + cudaArray_t planes[NVCV_MAX_PLANE_COUNT]; // NOSONAR: C ABI fixed-capacity plane buffer. } NVCVImageBufferCudaArray; /** Represents how the image data is stored. */ -typedef enum +typedef enum // NOSONAR: C ABI typedef. { /** Invalid buffer type. * This is commonly used to inform that no buffer type was selected. */ @@ -97,7 +97,7 @@ typedef enum /** Represents the available methods to access image contents. * The correct method depends on \ref NVCVImageData::bufferType. */ -typedef union NVCVImageBufferRec +typedef union NVCVImageBufferRec // NOSONAR: C ABI typedef. { /** Image stored in pitch-linear layout. * To be used when \ref NVCVImageData::bufferType is: @@ -114,7 +114,7 @@ typedef union NVCVImageBufferRec } NVCVImageBuffer; /** Stores information about image characteristics and content. */ -typedef struct NVCVImageDataRec +typedef struct NVCVImageDataRec // NOSONAR: C ABI typedef. { /** Image format. */ NVCVImageFormat format; diff --git a/src/nvcv/src/include/nvcv/ImageData.hpp b/src/nvcv/src/include/nvcv/ImageData.hpp index 0a5e9670c..f64e39c8b 100644 --- a/src/nvcv/src/include/nvcv/ImageData.hpp +++ b/src/nvcv/src/include/nvcv/ImageData.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -42,7 +42,7 @@ class ImageData * * @param data The NVCVImageData to use for initialization. */ - ImageData(const NVCVImageData &data); + explicit ImageData(const NVCVImageData &data); /** * @brief Get the image format. @@ -262,6 +262,6 @@ class ImageDataStridedHost : public ImageDataStrided } // namespace nvcv -#include "detail/ImageDataImpl.hpp" +#include "detail/ImageDataImpl.hpp" // NOSONAR: inline definitions require the declarations above. #endif // NVCV_DETAIL_IMAGEDATA_HPP diff --git a/src/nvcv/src/include/nvcv/ImageFormat.hpp b/src/nvcv/src/include/nvcv/ImageFormat.hpp index 7faea8ecc..1732901ce 100644 --- a/src/nvcv/src/include/nvcv/ImageFormat.hpp +++ b/src/nvcv/src/include/nvcv/ImageFormat.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -48,7 +48,7 @@ namespace nvcv { * @defgroup NVCV_CPP_CORE_IMAGETYPE Image Formats * @{ */ -class ImageFormat +class ImageFormat // NOSONAR: public image-format API intentionally groups format accessors. { public: constexpr ImageFormat(); @@ -76,7 +76,7 @@ class ImageFormat ImageFormat(ColorSpec colorSpec, ChromaSubsampling chromaSub, MemLayout memLayout, DataKind dataKind, Swizzle swizzle, Packing packing0, Packing packing1 = Packing::NONE, Packing packing2 = Packing::NONE, Packing packing3 = Packing::NONE, AlphaType alphaType = AlphaType::ASSOCIATED, - const ExtraChannelInfo *exChannelInfo = 0); + const ExtraChannelInfo *exChannelInfo = nullptr); /** * @brief Constructs an `ImageFormat` using the provided parameters. @@ -94,7 +94,7 @@ class ImageFormat ImageFormat(ColorModel colorModel, ColorSpec colorSpec, MemLayout memLayout, DataKind dataKind, Swizzle swizzle, Packing packing0, Packing packing1 = Packing::NONE, Packing packing2 = Packing::NONE, Packing packing3 = Packing::NONE, AlphaType alphaType = AlphaType::ASSOCIATED, - const ExtraChannelInfo *exChannelInfo = 0); + const ExtraChannelInfo *exChannelInfo = nullptr); /** * @brief Constructs an `ImageFormat` using the given memory layout, data kind, swizzle, and packing parameters. @@ -109,7 +109,7 @@ class ImageFormat */ ImageFormat(MemLayout memLayout, DataKind dataKind, Swizzle swizzle, Packing packing0, Packing packing1 = Packing::NONE, Packing packing2 = Packing::NONE, Packing packing3 = Packing::NONE, - AlphaType alphaType = AlphaType::ASSOCIATED, const ExtraChannelInfo *exChannelInfo = 0); + AlphaType alphaType = AlphaType::ASSOCIATED, const ExtraChannelInfo *exChannelInfo = nullptr); /** * @brief Constructs an `ImageFormat` using a raw pattern along with other parameters. @@ -125,7 +125,7 @@ class ImageFormat */ ImageFormat(RawPattern rawPattern, MemLayout memLayout, DataKind dataKind, Swizzle swizzle, Packing packing0, Packing packing1 = Packing::NONE, Packing packing2 = Packing::NONE, Packing packing3 = Packing::NONE, - AlphaType alphaType = AlphaType::ASSOCIATED, const ExtraChannelInfo *exChannelInfo = 0); + AlphaType alphaType = AlphaType::ASSOCIATED, const ExtraChannelInfo *exChannelInfo = nullptr); /// Const versions of `ImageFormat` using the provided parameters. static constexpr ImageFormat ConstCreate(ColorSpec colorSpec, ChromaSubsampling chromaSub, MemLayout memLayout, @@ -174,52 +174,56 @@ class ImageFormat static ImageFormat FromPlanes(ImageFormat plane0, ImageFormat plane1 = {}, ImageFormat plane2 = {}, ImageFormat plane3 = {}); - constexpr operator NVCVImageFormat() const noexcept; + explicit constexpr operator NVCVImageFormat() const noexcept; + explicit constexpr operator bool() const noexcept; constexpr NVCVImageFormat cvalue() const noexcept; constexpr bool operator==(ImageFormat that) const noexcept; constexpr bool operator!=(ImageFormat that) const noexcept; + constexpr bool operator<(ImageFormat that) const noexcept; + constexpr bool operator==(NVCVImageFormat that) const noexcept; + constexpr bool operator!=(NVCVImageFormat that) const noexcept; ImageFormat dataKind(DataKind dataKind) const; - DataKind dataKind() const noexcept; + DataKind dataKind() const; ImageFormat memLayout(MemLayout newMemLayout) const; - MemLayout memLayout() const noexcept; + MemLayout memLayout() const; ImageFormat colorSpec(ColorSpec newColorSpec) const; - ColorSpec colorSpec() const noexcept; + ColorSpec colorSpec() const; ImageFormat chromaSubsampling(ChromaSubsampling css) const; - ChromaSubsampling chromaSubsampling() const noexcept; + ChromaSubsampling chromaSubsampling() const; ImageFormat rawPattern(RawPattern newRawPattern) const; - RawPattern rawPattern() const noexcept; + RawPattern rawPattern() const; - AlphaType alphaType() const noexcept; + AlphaType alphaType() const; ImageFormat alphaType(AlphaType newAlphaType) const; - void extraChannelInfo(ExtraChannelInfo *exChannelInfo) const noexcept; + void extraChannelInfo(ExtraChannelInfo *exChannelInfo) const; ImageFormat extraChannelInfo(const ExtraChannelInfo *newExChannelInfo) const; - Swizzle swizzle() const noexcept; - ColorModel colorModel() const noexcept; - int32_t numChannels() const noexcept; - std::array bitsPerChannel() const noexcept; + Swizzle swizzle() const; + ColorModel colorModel() const; + int32_t numChannels() const; + std::array bitsPerChannel() const; uint32_t fourCC() const; - int32_t numPlanes() const noexcept; + int32_t numPlanes() const; ImageFormat swizzleAndPacking(Swizzle newSwizzle, Packing newPacking0, Packing newPacking1, Packing newPacking2, Packing newPacking3) const; - Packing planePacking(int32_t plane) const noexcept; - int32_t planePixelStrideBytes(int32_t plane) const noexcept; - DataType planeDataType(int32_t plane) const noexcept; - int32_t planeNumChannels(int32_t plane) const noexcept; - int32_t planeBitsPerPixel(int32_t plane) const noexcept; - int32_t planeRowAlignment(int32_t plane) const noexcept; - Size2D planeSize(Size2D imgSize, int32_t plane) const noexcept; - Swizzle planeSwizzle(int32_t plane) const noexcept; - ImageFormat planeFormat(int32_t plane) const noexcept; + Packing planePacking(int32_t plane) const; + int32_t planePixelStrideBytes(int32_t plane) const; + DataType planeDataType(int32_t plane) const; + int32_t planeNumChannels(int32_t plane) const; + int32_t planeBitsPerPixel(int32_t plane) const; + int32_t planeRowAlignment(int32_t plane) const; + Size2D planeSize(Size2D imgSize, int32_t plane) const; + Swizzle planeSwizzle(int32_t plane) const; + ImageFormat planeFormat(int32_t plane) const; private: NVCVImageFormat m_format; @@ -600,8 +604,7 @@ inline ImageFormat::ImageFormat(ColorSpec colorSpec, ChromaSubsampling chromaSub &m_format, static_cast(colorSpec), static_cast(chromaSub), static_cast(memLayout), static_cast(dataKind), static_cast(swizzle), static_cast(packing0), static_cast(packing1), static_cast(packing2), - static_cast(packing3), static_cast(alphaType), - static_cast(exChannelInfo))); + static_cast(packing3), static_cast(alphaType), exChannelInfo)); } constexpr ImageFormat ImageFormat::ConstCreate(ColorSpec colorSpec, ChromaSubsampling chromaSub, MemLayout memLayout, @@ -623,8 +626,7 @@ inline ImageFormat::ImageFormat(ColorModel colorModel, ColorSpec colorSpec, MemL &m_format, static_cast(colorModel), static_cast(colorSpec), static_cast(memLayout), static_cast(dataKind), static_cast(swizzle), static_cast(packing0), static_cast(packing1), static_cast(packing2), - static_cast(packing3), static_cast(alphaType), - static_cast(exChannelInfo))); + static_cast(packing3), static_cast(alphaType), exChannelInfo)); } constexpr ImageFormat ImageFormat::ConstCreate(ColorModel colorModel, ColorSpec colorSpec, MemLayout memLayout, @@ -646,7 +648,7 @@ inline ImageFormat::ImageFormat(MemLayout memLayout, DataKind dataKind, Swizzle &m_format, static_cast(memLayout), static_cast(dataKind), static_cast(swizzle), static_cast(packing0), static_cast(packing1), static_cast(packing2), static_cast(packing3), static_cast(alphaType), - static_cast(exChannelInfo))); + exChannelInfo)); } constexpr ImageFormat ImageFormat::ConstCreate(MemLayout memLayout, DataKind dataKind, Swizzle swizzle, @@ -667,7 +669,7 @@ inline ImageFormat::ImageFormat(RawPattern rawPattern, MemLayout memLayout, Data &m_format, static_cast(rawPattern), static_cast(memLayout), static_cast(dataKind), static_cast(swizzle), static_cast(packing0), static_cast(packing1), static_cast(packing2), static_cast(packing3), - static_cast(alphaType), static_cast(exChannelInfo))); + static_cast(alphaType), exChannelInfo)); } constexpr ImageFormat ImageFormat::ConstCreate(RawPattern rawPattern, MemLayout memLayout, DataKind dataKind, @@ -694,7 +696,9 @@ inline ImageFormat ImageFormat::FromPlanes(ImageFormat plane0, ImageFormat plane { NVCVImageFormat fmt; - detail::CheckThrow(nvcvMakeImageFormatFromPlanes(&fmt, plane0, plane1, plane2, plane3)); + detail::CheckThrow( + nvcvMakeImageFormatFromPlanes(&fmt, static_cast(plane0), static_cast(plane1), + static_cast(plane2), static_cast(plane3))); return ImageFormat{fmt}; } @@ -703,6 +707,11 @@ constexpr ImageFormat::operator NVCVImageFormat() const noexcept return m_format; } +constexpr ImageFormat::operator bool() const noexcept +{ + return m_format != NVCV_IMAGE_FORMAT_NONE; +} + constexpr NVCVImageFormat ImageFormat::cvalue() const noexcept { return m_format; @@ -718,6 +727,21 @@ constexpr bool ImageFormat::operator!=(ImageFormat that) const noexcept return !operator==(that); } +constexpr bool ImageFormat::operator<(ImageFormat that) const noexcept +{ + return m_format < that.m_format; +} + +constexpr bool ImageFormat::operator==(NVCVImageFormat that) const noexcept +{ + return m_format == that; +} + +constexpr bool ImageFormat::operator!=(NVCVImageFormat that) const noexcept +{ + return !operator==(that); +} + inline ImageFormat ImageFormat::dataKind(DataKind newDataKind) const { NVCVImageFormat out = m_format; @@ -725,7 +749,7 @@ inline ImageFormat ImageFormat::dataKind(DataKind newDataKind) const return ImageFormat{out}; } -inline DataKind ImageFormat::dataKind() const noexcept +inline DataKind ImageFormat::dataKind() const { NVCVDataKind out; detail::CheckThrow(nvcvImageFormatGetDataKind(m_format, &out)); @@ -739,7 +763,7 @@ inline ImageFormat ImageFormat::alphaType(AlphaType newAlphaType) const return ImageFormat{out}; } -inline AlphaType ImageFormat::alphaType() const noexcept +inline AlphaType ImageFormat::alphaType() const { NVCVAlphaType out; detail::CheckThrow(nvcvImageFormatGetAlphaType(m_format, &out)); @@ -749,15 +773,13 @@ inline AlphaType ImageFormat::alphaType() const noexcept inline ImageFormat ImageFormat::extraChannelInfo(const ExtraChannelInfo *exChannelInfo) const { NVCVImageFormat out = m_format; - detail::CheckThrow( - nvcvImageFormatSetExtraChannelInfo(&out, static_cast(exChannelInfo))); + detail::CheckThrow(nvcvImageFormatSetExtraChannelInfo(&out, exChannelInfo)); return ImageFormat{out}; } -inline void ImageFormat::extraChannelInfo(ExtraChannelInfo *exChannelInfo) const noexcept +inline void ImageFormat::extraChannelInfo(ExtraChannelInfo *exChannelInfo) const { - detail::CheckThrow( - nvcvImageFormatGetExtraChannelInfo(m_format, static_cast(exChannelInfo))); + detail::CheckThrow(nvcvImageFormatGetExtraChannelInfo(m_format, exChannelInfo)); } inline ImageFormat ImageFormat::memLayout(MemLayout newMemLayout) const @@ -767,7 +789,7 @@ inline ImageFormat ImageFormat::memLayout(MemLayout newMemLayout) const return ImageFormat{out}; } -inline MemLayout ImageFormat::memLayout() const noexcept +inline MemLayout ImageFormat::memLayout() const { NVCVMemLayout out; detail::CheckThrow(nvcvImageFormatGetMemLayout(m_format, &out)); @@ -781,7 +803,7 @@ inline ImageFormat ImageFormat::colorSpec(ColorSpec newColorSpec) const return ImageFormat{out}; } -inline ColorSpec ImageFormat::colorSpec() const noexcept +inline ColorSpec ImageFormat::colorSpec() const { NVCVColorSpec out; detail::CheckThrow(nvcvImageFormatGetColorSpec(m_format, &out)); @@ -795,7 +817,7 @@ inline ImageFormat ImageFormat::chromaSubsampling(ChromaSubsampling newCSS) cons return ImageFormat{out}; } -inline ChromaSubsampling ImageFormat::chromaSubsampling() const noexcept +inline ChromaSubsampling ImageFormat::chromaSubsampling() const { NVCVChromaSubsampling out; detail::CheckThrow(nvcvImageFormatGetChromaSubsampling(m_format, &out)); @@ -809,35 +831,35 @@ inline ImageFormat ImageFormat::rawPattern(RawPattern newRawPattern) const return ImageFormat{out}; } -inline RawPattern ImageFormat::rawPattern() const noexcept +inline RawPattern ImageFormat::rawPattern() const { NVCVRawPattern out; detail::CheckThrow(nvcvImageFormatGetRawPattern(m_format, &out)); return static_cast(out); } -inline Swizzle ImageFormat::swizzle() const noexcept +inline Swizzle ImageFormat::swizzle() const { NVCVSwizzle out; detail::CheckThrow(nvcvImageFormatGetSwizzle(m_format, &out)); return static_cast(out); } -inline ColorModel ImageFormat::colorModel() const noexcept +inline ColorModel ImageFormat::colorModel() const { NVCVColorModel out; detail::CheckThrow(nvcvImageFormatGetColorModel(m_format, &out)); return static_cast(out); } -inline int32_t ImageFormat::numChannels() const noexcept +inline int32_t ImageFormat::numChannels() const { int32_t out; detail::CheckThrow(nvcvImageFormatGetNumChannels(m_format, &out)); return out; } -inline std::array ImageFormat::bitsPerChannel() const noexcept +inline std::array ImageFormat::bitsPerChannel() const { std::array out; detail::CheckThrow(nvcvImageFormatGetBitsPerChannel(m_format, &out[0])); @@ -851,7 +873,7 @@ inline uint32_t ImageFormat::fourCC() const return out; } -inline int32_t ImageFormat::numPlanes() const noexcept +inline int32_t ImageFormat::numPlanes() const { int32_t out; detail::CheckThrow(nvcvImageFormatGetNumPlanes(m_format, &out)); @@ -869,61 +891,61 @@ inline ImageFormat ImageFormat::swizzleAndPacking(Swizzle newSwizzle, Packing ne return ImageFormat{out}; } -inline Packing ImageFormat::planePacking(int32_t plane) const noexcept +inline Packing ImageFormat::planePacking(int32_t plane) const { NVCVPacking out; detail::CheckThrow(nvcvImageFormatGetPlanePacking(m_format, plane, &out)); return static_cast(out); } -inline DataType ImageFormat::planeDataType(int32_t plane) const noexcept +inline DataType ImageFormat::planeDataType(int32_t plane) const { NVCVDataType out; detail::CheckThrow(nvcvImageFormatGetPlaneDataType(m_format, plane, &out)); return static_cast(out); } -inline int32_t ImageFormat::planePixelStrideBytes(int32_t plane) const noexcept +inline int32_t ImageFormat::planePixelStrideBytes(int32_t plane) const { int32_t out; detail::CheckThrow(nvcvImageFormatGetPlanePixelStrideBytes(m_format, plane, &out)); return out; } -inline int32_t ImageFormat::planeNumChannels(int32_t plane) const noexcept +inline int32_t ImageFormat::planeNumChannels(int32_t plane) const { int32_t out; detail::CheckThrow(nvcvImageFormatGetPlaneNumChannels(m_format, plane, &out)); return out; } -inline int32_t ImageFormat::planeBitsPerPixel(int32_t plane) const noexcept +inline int32_t ImageFormat::planeBitsPerPixel(int32_t plane) const { int32_t out; detail::CheckThrow(nvcvImageFormatGetPlaneBitsPerPixel(m_format, plane, &out)); return out; } -inline int32_t ImageFormat::planeRowAlignment(int32_t plane) const noexcept +inline int32_t ImageFormat::planeRowAlignment(int32_t plane) const { return planeDataType(plane).alignment(); } -inline Size2D ImageFormat::planeSize(Size2D imgSize, int32_t plane) const noexcept +inline Size2D ImageFormat::planeSize(Size2D imgSize, int32_t plane) const { Size2D psize; detail::CheckThrow(nvcvImageFormatGetPlaneSize(m_format, plane, imgSize.w, imgSize.h, &psize.w, &psize.h)); return psize; } -inline Swizzle ImageFormat::planeSwizzle(int32_t plane) const noexcept +inline Swizzle ImageFormat::planeSwizzle(int32_t plane) const { NVCVSwizzle out; detail::CheckThrow(nvcvImageFormatGetPlaneSwizzle(m_format, plane, &out)); return static_cast(out); } -inline ImageFormat ImageFormat::planeFormat(int32_t plane) const noexcept +inline ImageFormat ImageFormat::planeFormat(int32_t plane) const { NVCVImageFormat out; detail::CheckThrow(nvcvImageFormatGetPlaneFormat(m_format, plane, &out)); @@ -933,13 +955,14 @@ inline ImageFormat ImageFormat::planeFormat(int32_t plane) const noexcept inline bool HasSameDataLayout(ImageFormat a, ImageFormat b) { int8_t out; - detail::CheckThrow(nvcvImageFormatHasSameDataLayout(a, b, &out)); + detail::CheckThrow( + nvcvImageFormatHasSameDataLayout(static_cast(a), static_cast(b), &out)); return out != 0; } inline std::ostream &operator<<(std::ostream &out, ImageFormat fmt) { - return out << nvcvImageFormatGetName(fmt); + return out << nvcvImageFormatGetName(static_cast(fmt)); } /**@}*/ diff --git a/src/nvcv/src/include/nvcv/Optional.hpp b/src/nvcv/src/include/nvcv/Optional.hpp index 5017fa552..c1c100482 100644 --- a/src/nvcv/src/include/nvcv/Optional.hpp +++ b/src/nvcv/src/include/nvcv/Optional.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,22 +18,34 @@ #ifndef NVCV_OPTIONAL_HPP #define NVCV_OPTIONAL_HPP -// C++>=17 ? -#if __cplusplus >= 201703L -# include // for std::launder -#endif - #include "detail/InPlace.hpp" #include "detail/TypeTraits.hpp" #include #include // for std::nullptr_t +#include #include #include #include // for std::move, std::forward namespace nvcv { +class OptionalBadAccess : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + +namespace detail { +using std::swap; + +template +struct IsNothrowSwappable +{ + static constexpr bool value = noexcept(swap(std::declval(), std::declval())); +}; +} // namespace detail + struct NullOptT { }; @@ -58,11 +70,12 @@ class Optional /// @brief Default constructor that initializes an empty `Optional` Optional() noexcept : m_hasValue(false) + , m_storage() { } /// @brief Constructs an empty `Optional` using the specified `NullOptT` tag. - Optional(NullOptT) noexcept + explicit Optional(NullOptT) noexcept : Optional() { } @@ -70,22 +83,24 @@ class Optional /// @brief Copy constructor. /// If the other `Optional` contains a value, it will be copied to this `Optional`. Optional(const Optional &that) - : m_hasValue(that.m_hasValue) + : m_hasValue(false) + , m_storage() { - if (m_hasValue) + if (that.m_hasValue) { - new (&m_storage) T(that.value()); + construct(that.value()); } } /// @brief Move constructor. - Optional(Optional &&that) noexcept(std::is_nothrow_move_constructible::value) - : m_hasValue(that.m_hasValue) + Optional(Optional &&that) noexcept + : m_hasValue(false) + , m_storage() { - if (m_hasValue) + if (that.m_hasValue) { - new (&m_storage) T(std::move(that.value())); - // do not set that.m_hasValue to false as per c++17 standard. + construct(std::move(that.value())); + // Moving from the source optional keeps its engagement state as per the C++17 standard. } } @@ -101,12 +116,13 @@ class Optional * @param that The source `Optional` object to be copied from. */ template::value, int> = 0> - Optional(const Optional &that) - : m_hasValue(that.m_hasValue) + explicit Optional(const Optional &that) + : m_hasValue(false) + , m_storage() { - if (m_hasValue) + if (that.hasValue()) { - new (&m_storage) T(that.value()); + construct(that.value()); } } @@ -126,13 +142,14 @@ class Optional * @param that The source `Optional` object to be moved from. */ template::value, int> = 0> - Optional(Optional &&that) noexcept(std::is_nothrow_constructible::value) - : m_hasValue(that.m_hasValue) + explicit Optional(Optional &&that) noexcept(std::is_nothrow_constructible::value) + : m_hasValue(false) + , m_storage() { - if (m_hasValue) + if (that.hasValue()) { - new (&m_storage) T(std::move(that.value())); - // do not set that.m_hasValue to false as per c++17 standard. + construct(std::move(that.value())); + // Moving from the source optional keeps its engagement state as per the C++17 standard. } } @@ -158,10 +175,11 @@ class Optional && !std::is_same::type, detail::InPlaceT>::value && !std::is_same::type, Optional>::value, int> = 0> - Optional(U &&that) - : m_hasValue(true) + explicit Optional(U &&that) + : m_hasValue(false) + , m_storage() { - new (&m_storage) T(std::forward(that)); + construct(std::forward(that)); } /** @@ -182,29 +200,23 @@ class Optional * @param args... The arguments used for in-place construction of the `Optional` object's contained value. */ template::value, int> = 0> - Optional(detail::InPlaceT, AA &&...args) - : m_hasValue(true) + explicit Optional(detail::InPlaceT, AA &&...args) + : m_hasValue(false) + , m_storage() { - new (&m_storage) T(std::forward(args)...); + construct(std::forward(args)...); } // Dtor ~Optional() { - if (m_hasValue) - { - this->value().~T(); - } + reset(); } /// Comparison operators below Optional &operator=(NullOptT) noexcept { - if (m_hasValue) - { - this->value().~T(); - m_hasValue = false; - } + reset(); return *this; } @@ -213,23 +225,11 @@ class Optional { if (that.hasValue()) { - if (m_hasValue) - { - this->value() = that.value(); - } - else - { - new (&m_storage) T(that.value()); - m_hasValue = true; - } + assignValue(that.value()); } else { - if (m_hasValue) - { - this->value().~T(); - m_hasValue = false; - } + reset(); } return *this; } @@ -239,24 +239,12 @@ class Optional { if (that.hasValue()) { - if (m_hasValue) - { - this->value() = std::move(that.value()); - } - else - { - new (&m_storage) T(std::move(that.value())); - m_hasValue = true; - } - // do not set that.m_hasValue to false as per c++17 standard. + assignValue(std::move(that.value())); + // Moving from the source optional keeps its engagement state as per the C++17 standard. } else { - if (m_hasValue) - { - this->value().~T(); - m_hasValue = false; - } + reset(); } return *this; } @@ -264,57 +252,47 @@ class Optional // copy/move assignment Optional &operator=(const Optional &that) { - return this->operator=(that); - } - - Optional &operator=(Optional &&that) - { - return this->operator=(std::move(that)); - } - - Optional &operator=(const T &value) - { - if (m_hasValue) + if (that.m_hasValue) { - this->value() = value; + assignValue(that.value()); } else { - new (&m_storage) T(value); - m_hasValue = true; + reset(); } return *this; } - Optional &operator=(T &&value) + Optional &operator=(Optional &&that) noexcept { - if (m_hasValue) + if (that.m_hasValue) { - this->value() = std::move(value); + assignValue(std::move(that.value())); } else { - new (&m_storage) T(std::move(value)); - m_hasValue = true; + reset(); } return *this; } + Optional &operator=(const T &value) + { + assignValue(value); + return *this; + } + + Optional &operator=(T &&value) + { + assignValue(std::move(value)); + return *this; + } + template::value, int> = 0> T &emplace(AA &&...args) { - T *p; - if (m_hasValue) - { - this->value().~T(); - p = new (&m_storage) T(std::forward(args)...); - } - else - { - p = new (&m_storage) T(std::forward(args)...); - m_hasValue = true; - } - return *p; + reset(); + return *construct(std::forward(args)...); } /** @@ -327,7 +305,8 @@ class Optional { if (m_hasValue) { - this->value().~T(); + Allocator alloc; + AllocTraits::destroy(alloc, valuePtr()); m_hasValue = false; } } @@ -340,12 +319,12 @@ class Optional * * @param that Another `Optional` object of the same type. */ - void swap(Optional &that) + void swap(Optional &that) noexcept { if (m_hasValue && that.m_hasValue) { using std::swap; - swap(this->value() && that.value()); + swap(this->value(), that.value()); } else if (!m_hasValue && !that.m_hasValue) { @@ -353,7 +332,8 @@ class Optional } else { - Optional *a, *b; + Optional *a; + Optional *b; if (m_hasValue) { a = this; @@ -365,10 +345,8 @@ class Optional a = &that; b = this; } - new (&b->m_storage) T(std::move(a->value())); - a->value().~T(); - a->m_hasValue = false; - b->m_hasValue = true; + b->construct(std::move(a->value())); + a->reset(); } } @@ -399,15 +377,10 @@ class Optional { if (!m_hasValue) { - throw std::runtime_error("Bad optional access"); + throw OptionalBadAccess("Bad optional access"); } - T *p = reinterpret_cast(&m_storage); -#if __cplusplus >= 201703L - return *std::launder(p); -#else - return *p; -#endif + return *valuePtr(); } /** @@ -419,15 +392,10 @@ class Optional { if (!m_hasValue) { - throw std::runtime_error("Bad optional access"); + throw OptionalBadAccess("Bad optional access"); } - const T *p = reinterpret_cast(&m_storage); -#if __cplusplus >= 201703L - return *std::launder(p); -#else - return *p; -#endif + return *valuePtr(); } T *operator->() @@ -451,6 +419,41 @@ class Optional } private: + using Allocator = std::allocator; + using AllocTraits = std::allocator_traits; + + T *valuePtr() noexcept + { + return reinterpret_cast(&m_storage); + } + + const T *valuePtr() const noexcept + { + return reinterpret_cast(&m_storage); + } + + template + void assignValue(Value &&value) + { + if (m_hasValue) + { + this->value() = std::forward(value); + } + else + { + construct(std::forward(value)); + } + } + + template + T *construct(AA &&...args) + { + Allocator alloc; + AllocTraits::construct(alloc, valuePtr(), std::forward(args)...); + m_hasValue = true; + return valuePtr(); + } + bool m_hasValue; typename std::aligned_storage::type m_storage; }; diff --git a/src/nvcv/src/include/nvcv/RoundMode.h b/src/nvcv/src/include/nvcv/RoundMode.h new file mode 100644 index 000000000..028c13d91 --- /dev/null +++ b/src/nvcv/src/include/nvcv/RoundMode.h @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#ifndef NVCV_ROUND_MODE_H +#define NVCV_ROUND_MODE_H + +#ifdef __cplusplus +extern "C" +{ +#endif + +/* @brief Selects how floating-point values are rounded when converting to an integer type. + * + * Only affects integer outputs; conversions whose output type is floating-point are unaffected. + */ +typedef enum +{ + NVCV_ROUND_NEAREST = 0, ///< Round to nearest, ties to even (default). + NVCV_ROUND_TRUNCATE = 1, ///< Truncate toward zero (drop the fractional part). +} NVCVRoundMode; + +#ifdef __cplusplus +} +#endif + +#endif // NVCV_ROUND_MODE_H diff --git a/src/nvcv/src/include/nvcv/Shape.hpp b/src/nvcv/src/include/nvcv/Shape.hpp index f8daf8369..8cd0ed157 100644 --- a/src/nvcv/src/include/nvcv/Shape.hpp +++ b/src/nvcv/src/include/nvcv/Shape.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -54,8 +54,7 @@ class Shape constexpr static int MAX_RANK = N; // Constructors - Shape(); - Shape(const Shape &that); + Shape() = default; /** * @brief Construct with a given rank, sizes default to 0. @@ -100,30 +99,17 @@ class Shape bool operator<(const Shape &that) const; private: - Data m_data; - size_type m_size; + Data m_data{}; + size_type m_size = 0; }; // Implementation -template -Shape::Shape() - : m_size(0) -{ -} - template Shape::Shape(int size) : m_size(size) { - std::fill(this->begin(), this->end(), 0); -} - -template -Shape::Shape(const Shape &that) - : m_size(that.m_size) -{ - std::copy(that.begin(), that.end(), m_data.begin()); + std::fill(this->begin(), this->end(), 0); // NOSONAR: std::ranges::fill is C++20. } template @@ -134,7 +120,6 @@ Shape::Shape(std::initializer_list shape) template Shape::Shape(const T *data, size_t n) - : m_size(n) { if (data == nullptr) { @@ -146,6 +131,7 @@ Shape::Shape(const T *data, size_t n) throw Exception(Status::ERROR_INVALID_ARGUMENT, "Shape ranks is too big"); } + m_size = static_cast(n); std::copy_n(data, n, m_data.begin()); } @@ -185,7 +171,8 @@ bool Shape::operator!=(const Shape &that) const template bool Shape::operator<(const Shape &that) const { - return std::lexicographical_compare(this->begin(), this->end(), that.begin(), that.end()); + return std::lexicographical_compare( // NOSONAR: std::ranges::lexicographical_compare is C++20. + this->begin(), this->end(), that.begin(), that.end()); } template diff --git a/src/nvcv/src/include/nvcv/Size.h b/src/nvcv/src/include/nvcv/Size.h index 4c3e5d551..2dc3975e5 100644 --- a/src/nvcv/src/include/nvcv/Size.h +++ b/src/nvcv/src/include/nvcv/Size.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -32,7 +32,8 @@ extern "C" */ typedef struct { - int32_t w, h; + int32_t w; + int32_t h; } NVCVSize2D; #ifdef __cplusplus diff --git a/src/nvcv/src/include/nvcv/Size.hpp b/src/nvcv/src/include/nvcv/Size.hpp index 181bdf9c2..5b6a896bd 100644 --- a/src/nvcv/src/include/nvcv/Size.hpp +++ b/src/nvcv/src/include/nvcv/Size.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -46,7 +46,7 @@ struct Size2D : NVCVSize2D { } - constexpr Size2D(const NVCVSize2D &s) + explicit constexpr Size2D(const NVCVSize2D &s) : NVCVSize2D{s.w, s.h} { } diff --git a/src/nvcv/src/include/nvcv/Tensor.h b/src/nvcv/src/include/nvcv/Tensor.h index 7ae1b4b46..de8637d88 100644 --- a/src/nvcv/src/include/nvcv/Tensor.h +++ b/src/nvcv/src/include/nvcv/Tensor.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,8 +21,8 @@ * @brief Public C interface to NVCV tensor representation. */ -#ifndef NVCV_TENSOR_H -#define NVCV_TENSOR_H +#ifndef NVCV_TENSOR_H_INCLUDED +#define NVCV_TENSOR_H_INCLUDED #include "Export.h" #include "Fwd.h" @@ -274,7 +274,7 @@ NVCV_PUBLIC NVCVStatus nvcvTensorRefCount(NVCVTensorHandle handle, int *newRefCo * @retval #NVCV_ERROR_INVALID_ARGUMENT Some parameter is outside valid range. * @retval #NVCV_SUCCESS Operation executed successfully. */ -NVCV_PUBLIC NVCVStatus nvcvTensorSetUserPointer(NVCVTensorHandle handle, void *userPtr); +NVCV_PUBLIC NVCVStatus nvcvTensorSetUserPointer(NVCVTensorHandle handle, NVCVUserPointer userPtr); /** Returns the user pointer associated with the tensor handle. * @@ -288,7 +288,7 @@ NVCV_PUBLIC NVCVStatus nvcvTensorSetUserPointer(NVCVTensorHandle handle, void *u * @retval #NVCV_ERROR_INVALID_ARGUMENT Some parameter is outside valid range. * @retval #NVCV_SUCCESS Operation executed successfully. */ -NVCV_PUBLIC NVCVStatus nvcvTensorGetUserPointer(NVCVTensorHandle handle, void **outUserPtr); +NVCV_PUBLIC NVCVStatus nvcvTensorGetUserPointer(NVCVTensorHandle handle, NVCVUserPointer *outUserPtr); /** * Get the type of the tensor elements (its data type). @@ -400,4 +400,4 @@ NVCV_PUBLIC NVCVStatus nvcvTensorReshape(NVCVTensorHandle handle, int32_t rank, } #endif -#endif // NVCV_TENSOR_H +#endif // NVCV_TENSOR_H_INCLUDED diff --git a/src/nvcv/src/include/nvcv/Tensor.hpp b/src/nvcv/src/include/nvcv/Tensor.hpp index b2b955044..d8c7c972a 100644 --- a/src/nvcv/src/include/nvcv/Tensor.hpp +++ b/src/nvcv/src/include/nvcv/Tensor.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -97,20 +97,20 @@ class Tensor : public CoreResource * * @param ptr Pointer to set. */ - void setUserPointer(void *ptr); + void setUserPointer(NVCVUserPointer ptr); /** * @brief Retrieves the user-defined pointer associated with the tensor. * * @return User-defined pointer. */ - void *userPointer() const; + NVCVUserPointer userPointer() const; /** * @brief Creates a view of the tensor with a new shape and layout * */ - Tensor reshape(const TensorShape &new_shape); + Tensor reshape(const TensorShape &new_shape) const; /** * @brief Calculates the requirements for a tensor given its shape and data type. @@ -134,16 +134,44 @@ class Tensor : public CoreResource static Requirements CalcRequirements(int numImages, Size2D imgSize, ImageFormat fmt, const MemAlignment &bufAlign = {}); - NVCV_IMPLEMENT_SHARED_RESOURCE(Tensor, Base); + using Base::Base; + using Base::operator=; + + Tensor(const Tensor &other) + : Base(other) + { + } + + Tensor(Tensor &&other) noexcept + : Base(std::move(other)) + { + } + + Tensor &operator=(const Tensor &other) + { + Base::operator=(other); + return *this; + } + + Tensor &operator=(Tensor &&other) noexcept + { + Base::operator=(std::move(other)); + return *this; + } + + ~Tensor() + { + this->reset(); + } /** * @brief Constructors */ - explicit Tensor(const Requirements &reqs, const Allocator &alloc = nullptr); + explicit Tensor(const Requirements &reqs, const Allocator &alloc = Allocator{nullptr}); explicit Tensor(const TensorShape &shape, DataType dtype, const MemAlignment &bufAlign = {}, - const Allocator &alloc = nullptr); + const Allocator &alloc = Allocator{nullptr}); explicit Tensor(int numImages, Size2D imgSize, ImageFormat fmt, const MemAlignment &bufAlign = {}, - const Allocator &alloc = nullptr); + const Allocator &alloc = Allocator{nullptr}); }; // TensorWrapData definition ------------------------------------- @@ -186,11 +214,15 @@ using TensorWrapHandle = NonOwningResource; using OptionalTensorConstRef = nvcv::Optional>; -#define NVCV_TENSOR_HANDLE_TO_OPTIONAL(X) X ? nvcv::OptionalTensorConstRef(nvcv::TensorWrapHandle{X}) : nvcv::NullOpt -#define NVCV_OPTIONAL_TO_HANDLE(X) X ? X->get().handle() : nullptr +#define NVCV_TENSOR_HANDLE_TO_OPTIONAL(X) \ + X ? nvcv::OptionalTensorConstRef(nvcv::TensorWrapHandle{X}.resource()) : nvcv::OptionalTensorConstRef \ + { \ + nvcv::NullOpt \ + } +#define NVCV_OPTIONAL_TO_HANDLE(X) X ? X->get().handle() : nullptr } // namespace nvcv -#include "detail/TensorImpl.hpp" +#include "detail/TensorImpl.hpp" // NOSONAR: inline definitions require the declarations above. #endif // NVCV_TENSOR_HPP diff --git a/src/nvcv/src/include/nvcv/TensorBatch.h b/src/nvcv/src/include/nvcv/TensorBatch.h index c552acab0..6a10b6e4e 100644 --- a/src/nvcv/src/include/nvcv/TensorBatch.h +++ b/src/nvcv/src/include/nvcv/TensorBatch.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -203,7 +203,7 @@ NVCV_PUBLIC NVCVStatus nvcvTensorBatchRefCount(NVCVTensorBatchHandle handle, int * @retval #NVCV_ERROR_INVALID_ARGUMENT Some parameter is outside valid range. * @retval #NVCV_SUCCESS Operation executed successfully. */ -NVCV_PUBLIC NVCVStatus nvcvTensorBatchSetUserPointer(NVCVTensorBatchHandle handle, void *userPtr); +NVCV_PUBLIC NVCVStatus nvcvTensorBatchSetUserPointer(NVCVTensorBatchHandle handle, NVCVUserPointer userPtr); /** Returns the user pointer associated with the Tensor batch handle. * @@ -217,7 +217,7 @@ NVCV_PUBLIC NVCVStatus nvcvTensorBatchSetUserPointer(NVCVTensorBatchHandle handl * @retval #NVCV_ERROR_INVALID_ARGUMENT Some parameter is outside valid range. * @retval #NVCV_SUCCESS Operation executed successfully. */ -NVCV_PUBLIC NVCVStatus nvcvTensorBatchGetUserPointer(NVCVTensorBatchHandle handle, void **outUserPtr); +NVCV_PUBLIC NVCVStatus nvcvTensorBatchGetUserPointer(NVCVTensorBatchHandle handle, NVCVUserPointer *outUserPtr); /** Returns the capacity of the Tensor batch handle. * diff --git a/src/nvcv/src/include/nvcv/TensorBatch.hpp b/src/nvcv/src/include/nvcv/TensorBatch.hpp index 636c2a191..b7a439364 100644 --- a/src/nvcv/src/include/nvcv/TensorBatch.hpp +++ b/src/nvcv/src/include/nvcv/TensorBatch.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -46,11 +46,39 @@ class TensorBatch : public CoreResource static Requirements CalcRequirements(int32_t capacity); - NVCV_IMPLEMENT_SHARED_RESOURCE(TensorBatch, Base); + using Base::Base; + using Base::operator=; - TensorBatch(const Requirements &reqs, const Allocator &alloc = nullptr); + TensorBatch(const TensorBatch &other) + : Base(other) + { + } + + TensorBatch(TensorBatch &&other) noexcept + : Base(std::move(other)) + { + } + + TensorBatch &operator=(const TensorBatch &other) + { + Base::operator=(other); + return *this; + } + + TensorBatch &operator=(TensorBatch &&other) noexcept + { + Base::operator=(std::move(other)); + return *this; + } + + ~TensorBatch() + { + this->reset(); + } - TensorBatch(int32_t capacity, const Allocator &alloc = nullptr); + explicit TensorBatch(const Requirements &reqs, const Allocator &alloc = Allocator{nullptr}); + + explicit TensorBatch(int32_t capacity, const Allocator &alloc = Allocator{nullptr}); /** * @brief Return the maximal number of tensors the tensor batch can hold. @@ -122,7 +150,7 @@ class TensorBatch : public CoreResource * * @param stream CUDA stream on which the buffers copy will be scheduled. */ - TensorBatchData exportData(CUstream stream); + TensorBatchData exportData(CUstream stream) const; void clear(); @@ -131,13 +159,13 @@ class TensorBatch : public CoreResource * * @param ptr User pointer */ - void setUserPointer(void *ptr); + void setUserPointer(NVCVUserPointer ptr); /** * @brief Get the user pointer that was previously assciated to the tensor batch - * with the setUserPointer(void*) method. Returns nullptr if no pointer was set. + * with the setUserPointer(NVCVUserPointer) method. Returns nullptr if no pointer was set. */ - void *getUserPointer() const; + NVCVUserPointer getUserPointer() const; /** * @brief Return a handle to a tensor at a given positon. @@ -187,33 +215,13 @@ class TensorBatch::Iterator bool operator<=(const Iterator &rhs) const; bool operator>=(const Iterator &rhs) const; - Iterator(Iterator &other) - : Iterator() - { - *this = other; - } - - Iterator(Iterator &&other) - : Iterator() - { - *this = std::move(other); - } + Iterator(const Iterator &other) = default; + Iterator(Iterator &&other) noexcept = default; - Iterator &operator=(Iterator &other) - { - m_tensorBatch = other.m_tensorBatch; - m_idx = other.m_idx; - m_currentTensor = other.m_currentTensor; - return *this; - } + Iterator &operator=(const Iterator &other) = default; + Iterator &operator=(Iterator &&other) noexcept = default; - Iterator &operator=(Iterator &&other) - { - m_tensorBatch = other.m_tensorBatch; - m_idx = other.m_idx; - m_currentTensor = std::move(other.m_currentTensor); - return *this; - } + ~Iterator() = default; private: friend class TensorBatch; @@ -223,7 +231,6 @@ class TensorBatch::Iterator Iterator(const TensorBatch *tensorBatch, int32_t idx) : m_tensorBatch(tensorBatch) , m_idx(idx) - , m_currentTensor{} { UpdateCurrentTensor(); } @@ -239,6 +246,6 @@ using TensorBatchWrapHandle = NonOwningResource; } // namespace nvcv -#include "detail/TensorBatchImpl.hpp" +#include "detail/TensorBatchImpl.hpp" // NOSONAR: inline definitions require the declarations above. #endif // NVCV_TENSORBATCH_HPP diff --git a/src/nvcv/src/include/nvcv/TensorBatchData.hpp b/src/nvcv/src/include/nvcv/TensorBatchData.hpp index a601372b3..da3fcb1e0 100644 --- a/src/nvcv/src/include/nvcv/TensorBatchData.hpp +++ b/src/nvcv/src/include/nvcv/TensorBatchData.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -33,7 +33,7 @@ namespace nvcv { class TensorBatchData { public: - TensorBatchData(const NVCVTensorBatchData &data) + explicit TensorBatchData(const NVCVTensorBatchData &data) : m_data(data) { } @@ -51,7 +51,7 @@ class TensorBatchData */ TensorLayout layout() const { - return m_data.layout; + return TensorLayout{m_data.layout}; } /** @@ -96,11 +96,11 @@ class TensorBatchData if (IsCompatible()) { - return {Derived(m_data)}; + return Optional{Derived{m_data}}; } else { - return {}; + return Optional{}; } } diff --git a/src/nvcv/src/include/nvcv/TensorData.hpp b/src/nvcv/src/include/nvcv/TensorData.hpp index ded1562cc..8cae82b0b 100644 --- a/src/nvcv/src/include/nvcv/TensorData.hpp +++ b/src/nvcv/src/include/nvcv/TensorData.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -41,7 +41,12 @@ class TensorData * * @param data The underlying tensor data representation. */ - TensorData(const NVCVTensorData &data); + explicit TensorData(const NVCVTensorData &data); + TensorData(const TensorData &) = default; + TensorData(TensorData &&) noexcept = default; + + TensorData &operator=(const TensorData &) = default; + TensorData &operator=(TensorData &&) noexcept = default; /// @brief Retrieves the rank (number of dimensions) of the tensor. int rank() const; @@ -184,7 +189,12 @@ class TensorDataStridedCuda : public TensorDataStrided * * @param data The underlying tensor data representation. */ - TensorDataStridedCuda(const NVCVTensorData &data); + explicit TensorDataStridedCuda(const NVCVTensorData &data); + TensorDataStridedCuda(const TensorDataStridedCuda &) = default; + TensorDataStridedCuda(TensorDataStridedCuda &&) noexcept = default; + + TensorDataStridedCuda &operator=(const TensorDataStridedCuda &) = default; + TensorDataStridedCuda &operator=(TensorDataStridedCuda &&) noexcept = default; /** * @brief Constructs a `TensorDataStridedCuda` object from tensor shape, data type, and buffer. @@ -210,6 +220,6 @@ class TensorDataStridedCuda : public TensorDataStrided } // namespace nvcv -#include "detail/TensorDataImpl.hpp" +#include "detail/TensorDataImpl.hpp" // NOSONAR: inline definitions require the declarations above. #endif // NVCV_TENSORDATA_HPP diff --git a/src/nvcv/src/include/nvcv/TensorDataAccess.hpp b/src/nvcv/src/include/nvcv/TensorDataAccess.hpp index 9371d6878..ce70de72e 100644 --- a/src/nvcv/src/include/nvcv/TensorDataAccess.hpp +++ b/src/nvcv/src/include/nvcv/TensorDataAccess.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -56,6 +56,12 @@ class TensorDataAccessStridedImpl { } + TensorDataAccessStridedImpl(const TensorDataAccessStridedImpl &) = default; + TensorDataAccessStridedImpl(TensorDataAccessStridedImpl &&) noexcept = default; + + TensorDataAccessStridedImpl &operator=(const TensorDataAccessStridedImpl &) = default; + TensorDataAccessStridedImpl &operator=(TensorDataAccessStridedImpl &&) noexcept = default; + /** * @brief Returns the number of samples in the tensor. * @@ -169,7 +175,10 @@ class TensorDataAccessStridedImpl } protected: - TensorDataStrided m_tdata; + const TensorDataStrided &tensorData() const + { + return m_tdata; + } TensorDataAccessStridedImpl(const TensorDataAccessStridedImpl &that, const TensorShapeInfo &infoShape) : m_tdata(that.m_tdata) @@ -178,7 +187,8 @@ class TensorDataAccessStridedImpl } private: - ShapeInfo m_infoShape; + TensorDataStrided m_tdata; + ShapeInfo m_infoShape; }; /** @@ -209,6 +219,12 @@ class TensorDataAccessStridedImageImpl : public TensorDataAccessStridedImplinfoLayout().idxChannel(); if (idx >= 0) { - return this->m_tdata.stride(idx); + return this->tensorData().stride(idx); } else { @@ -277,7 +293,7 @@ class TensorDataAccessStridedImageImpl : public TensorDataAccessStridedImplinfoLayout().idxWidth(); if (idx >= 0) { - return this->m_tdata.stride(idx); + return this->tensorData().stride(idx); } else { @@ -295,7 +311,7 @@ class TensorDataAccessStridedImageImpl : public TensorDataAccessStridedImplinfoLayout().idxHeight(); if (idx >= 0) { - return this->m_tdata.stride(idx); + return this->tensorData().stride(idx); } else { @@ -313,7 +329,7 @@ class TensorDataAccessStridedImageImpl : public TensorDataAccessStridedImplinfoLayout().idxDepth(); if (idx >= 0) { - return this->m_tdata.stride(idx); + return this->tensorData().stride(idx); } else { @@ -329,7 +345,7 @@ class TensorDataAccessStridedImageImpl : public TensorDataAccessStridedImplm_tdata.basePtr()); + return rowData(y, this->tensorData().basePtr()); } /** @@ -353,7 +369,7 @@ class TensorDataAccessStridedImageImpl : public TensorDataAccessStridedImplm_tdata.basePtr()); + return chData(c, this->tensorData().basePtr()); } /** @@ -403,6 +419,12 @@ class TensorDataAccessStridedImagePlanarImpl : public TensorDataAccessStridedIma { } + TensorDataAccessStridedImagePlanarImpl(const TensorDataAccessStridedImagePlanarImpl &) = default; + TensorDataAccessStridedImagePlanarImpl(TensorDataAccessStridedImagePlanarImpl &&) noexcept = default; + + TensorDataAccessStridedImagePlanarImpl &operator=(const TensorDataAccessStridedImagePlanarImpl &) = default; + TensorDataAccessStridedImagePlanarImpl &operator=(TensorDataAccessStridedImagePlanarImpl &&) noexcept = default; + /** * @brief Returns the number of planes in the planar image tensor. * @@ -424,7 +446,7 @@ class TensorDataAccessStridedImagePlanarImpl : public TensorDataAccessStridedIma { int ichannel = this->infoLayout().idxChannel(); assert(ichannel >= 0); - return this->m_tdata.stride(ichannel); + return this->tensorData().stride(ichannel); } else { @@ -440,7 +462,7 @@ class TensorDataAccessStridedImagePlanarImpl : public TensorDataAccessStridedIma */ Byte *planeData(int p) const { - return planeData(p, this->m_tdata.basePtr()); + return planeData(p, this->tensorData().basePtr()); } /** @@ -471,6 +493,12 @@ class TensorDataAccessStrided : public detail::TensorDataAccessStridedImpl; public: + TensorDataAccessStrided(const TensorDataAccessStrided &) = default; + TensorDataAccessStrided(TensorDataAccessStrided &&) noexcept = default; + + TensorDataAccessStrided &operator=(const TensorDataAccessStrided &) = default; + TensorDataAccessStrided &operator=(TensorDataAccessStrided &&) noexcept = default; + /** * @brief Checks if the provided tensor data is compatible with a strided layout. * @@ -492,16 +520,16 @@ class TensorDataAccessStrided : public detail::TensorDataAccessStridedImpl dataStrided = data.cast()) { - return TensorDataAccessStrided(dataStrided.value()); + return Optional{TensorDataAccessStrided{dataStrided.value()}}; } else { - return NullOpt; + return Optional{NullOpt}; } } private: - TensorDataAccessStrided(const TensorDataStrided &data) + explicit TensorDataAccessStrided(const TensorDataStrided &data) : Base(data, *TensorShapeInfo::Create(data.shape())) { } @@ -518,6 +546,12 @@ class TensorDataAccessStridedImage : public detail::TensorDataAccessStridedImage using Base = detail::TensorDataAccessStridedImageImpl; public: + TensorDataAccessStridedImage(const TensorDataAccessStridedImage &) = default; + TensorDataAccessStridedImage(TensorDataAccessStridedImage &&) noexcept = default; + + TensorDataAccessStridedImage &operator=(const TensorDataAccessStridedImage &) = default; + TensorDataAccessStridedImage &operator=(TensorDataAccessStridedImage &&) noexcept = default; + /** * @brief Checks if the provided tensor data is compatible with an image tensor layout. * @@ -539,16 +573,17 @@ class TensorDataAccessStridedImage : public detail::TensorDataAccessStridedImage { if (IsCompatible(data)) { - return TensorDataAccessStridedImage(data.cast().value()); + return Optional{ + TensorDataAccessStridedImage{data.cast().value()}}; } else { - return NullOpt; + return Optional{NullOpt}; } } protected: - TensorDataAccessStridedImage(const TensorDataStrided &data) + explicit TensorDataAccessStridedImage(const TensorDataStrided &data) : Base(data, *TensorShapeInfoImage::Create(data.shape())) { } @@ -567,6 +602,12 @@ class TensorDataAccessStridedImagePlanar using Base = detail::TensorDataAccessStridedImagePlanarImpl; public: + TensorDataAccessStridedImagePlanar(const TensorDataAccessStridedImagePlanar &) = default; + TensorDataAccessStridedImagePlanar(TensorDataAccessStridedImagePlanar &&) noexcept = default; + + TensorDataAccessStridedImagePlanar &operator=(const TensorDataAccessStridedImagePlanar &) = default; + TensorDataAccessStridedImagePlanar &operator=(TensorDataAccessStridedImagePlanar &&) noexcept = default; + /** * @brief Checks if the provided tensor data is compatible with a planar image tensor layout. * @@ -589,16 +630,17 @@ class TensorDataAccessStridedImagePlanar { if (IsCompatible(data)) { - return TensorDataAccessStridedImagePlanar(data.cast().value()); + return Optional{ + TensorDataAccessStridedImagePlanar{data.cast().value()}}; } else { - return NullOpt; + return Optional{NullOpt}; } } protected: - TensorDataAccessStridedImagePlanar(const TensorDataStrided &data) + explicit TensorDataAccessStridedImagePlanar(const TensorDataStrided &data) : Base(data, *TensorShapeInfoImagePlanar::Create(data.shape())) { } diff --git a/src/nvcv/src/include/nvcv/TensorLayout.h b/src/nvcv/src/include/nvcv/TensorLayout.h index eaa8b2c7c..aa5cb967e 100644 --- a/src/nvcv/src/include/nvcv/TensorLayout.h +++ b/src/nvcv/src/include/nvcv/TensorLayout.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -181,11 +181,15 @@ inline static int32_t nvcvTensorLayoutFindDimIndex(NVCVTensorLayout layout, char idxStart = layout.rank + idxStart; } - int n = layout.rank - idxStart; - if (n > 0) + int n = layout.rank - idxStart; // NOSONAR: this public C header cannot use a C++ if init-statement. + if (n > 0) // NOSONAR: this public C header cannot use a C++ if init-statement. { void *p = memchr(layout.data + idxStart, dimLabel, n); +#ifdef __cplusplus + if (p != nullptr) +#else if (p != NULL) +#endif { return (int32_t)((char *)p - (char *)layout.data); } @@ -193,6 +197,15 @@ inline static int32_t nvcvTensorLayoutFindDimIndex(NVCVTensorLayout layout, char return -1; } +#ifdef __cplusplus +constexpr static int32_t nvcvTensorLayoutNormalizeIndex(NVCVTensorLayout layout, int idx) +#else +inline static int32_t nvcvTensorLayoutNormalizeIndex(NVCVTensorLayout layout, int idx) +#endif +{ + return idx < 0 ? layout.rank + idx : idx; +} + /** Returns the layout label at the given index. * * @param [in] layout Layout to be queried @@ -201,11 +214,13 @@ inline static int32_t nvcvTensorLayoutFindDimIndex(NVCVTensorLayout layout, char * @returns If @p idx >= 0 and < layout size, returns the correspondign label. * Returns '\0' otherwise. */ -NVCV_CONSTEXPR inline static char nvcvTensorLayoutGetLabel(NVCVTensorLayout layout, int idx) +NVCV_CONSTEXPR inline static char + nvcvTensorLayoutGetLabel( // NOSONAR: inline is required when this public header is compiled as C. + NVCVTensorLayout layout, int idx) { - // Must be all a single statement for C++11 compatibility - return idx < 0 ? (0 <= layout.rank + idx && layout.rank + idx < layout.rank ? layout.data[layout.rank + idx] : '\0') - : (0 <= idx && idx < layout.rank ? layout.data[idx] : '\0'); + return 0 <= nvcvTensorLayoutNormalizeIndex(layout, idx) && nvcvTensorLayoutNormalizeIndex(layout, idx) < layout.rank + ? layout.data[nvcvTensorLayoutNormalizeIndex(layout, idx)] + : '\0'; } /** Returns the number of dimensions of the tensor layout @@ -214,7 +229,9 @@ NVCV_CONSTEXPR inline static char nvcvTensorLayoutGetLabel(NVCVTensorLayout layo * * @returns Number of dimensions. */ -NVCV_CONSTEXPR inline static int32_t nvcvTensorLayoutGetNumDim(NVCVTensorLayout layout) +NVCV_CONSTEXPR inline static int32_t + nvcvTensorLayoutGetNumDim( // NOSONAR: inline is required when this public header is compiled as C. + NVCVTensorLayout layout) { return layout.rank; } @@ -285,9 +302,15 @@ inline static int32_t nvcvTensorLayoutEndsWith(NVCVTensorLayout layout, NVCVTens * * @returns Null-terminated string with the layout name. */ -NVCV_CONSTEXPR inline static const char *nvcvTensorLayoutGetName(const NVCVTensorLayout *layout) +NVCV_CONSTEXPR inline static const char * + nvcvTensorLayoutGetName( // NOSONAR: inline is required when this public header is compiled as C. + const NVCVTensorLayout *layout) { +#ifdef __cplusplus + return layout == nullptr ? "" : layout->data; +#else return layout == NULL ? "" : layout->data; +#endif } #ifdef __cplusplus diff --git a/src/nvcv/src/include/nvcv/TensorLayout.hpp b/src/nvcv/src/include/nvcv/TensorLayout.hpp index ad6639daa..1b795d00d 100644 --- a/src/nvcv/src/include/nvcv/TensorLayout.hpp +++ b/src/nvcv/src/include/nvcv/TensorLayout.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,6 +22,7 @@ #include "detail/CheckError.hpp" #include "detail/Concepts.hpp" +#include #include #include @@ -107,7 +108,7 @@ class TensorLayout final * @brief Constructs a TensorLayout from an NVCVTensorLayout. * @param layout The NVCVTensorLayout to wrap. */ - constexpr TensorLayout(const NVCVTensorLayout &layout) + explicit constexpr TensorLayout(const NVCVTensorLayout &layout) : m_layout(layout) { } @@ -220,7 +221,7 @@ class TensorLayout final constexpr const_iterator cbegin() const; constexpr const_iterator cend() const; - constexpr operator const NVCVTensorLayout &() const; + explicit constexpr operator const NVCVTensorLayout &() const; /** * @brief Outputs the TensorLayout to a stream. @@ -240,6 +241,14 @@ NVCV_DETAIL_DEF_TLAYOUT(NONE) #include "TensorLayoutDef.inc" #undef NVCV_DETAIL_DEF_TLAYOUT +namespace detail { + +constexpr std::array kImplicitTensorLayouts{ + {&TENSOR_NONE, &TENSOR_W, &TENSOR_HW, &TENSOR_NHW, &TENSOR_NCHW, &TENSOR_NCDHW, &TENSOR_NCFDHW} +}; + +} // namespace detail + /** * @brief Retrieves the default tensor layout based on the rank (number of dimensions). * @@ -251,26 +260,9 @@ NVCV_DETAIL_DEF_TLAYOUT(NONE) */ constexpr const TensorLayout &GetImplicitTensorLayout(int rank) { - // clang-format off - return rank == 1 - ? TENSOR_W - : (rank == 2 - ? TENSOR_HW - : (rank == 3 - ? TENSOR_NHW - : (rank == 4 - ? TENSOR_NCHW - : (rank == 5 - ? TENSOR_NCDHW - : (rank == 6 - ? TENSOR_NCFDHW - : TENSOR_NONE - ) - ) - ) - ) - ); - // clang-format on + return 0 <= rank && rank < static_cast(detail::kImplicitTensorLayouts.size()) + ? *detail::kImplicitTensorLayouts[rank] + : TENSOR_NONE; } constexpr char TensorLayout::operator[](int idx) const @@ -293,7 +285,7 @@ constexpr TensorLayout::operator const NVCVTensorLayout &() const return m_layout; } -inline bool operator==(const TensorLayout &a, const TensorLayout &b) +inline bool operator==(const TensorLayout &a, const TensorLayout &b) // NOSONAR: defaulted comparisons are C++20. { return a.m_layout == b.m_layout; } @@ -313,7 +305,7 @@ constexpr auto TensorLayout::begin() const -> const_iterator return nvcvTensorLayoutGetName(&m_layout); } -constexpr inline auto TensorLayout::end() const -> const_iterator +constexpr auto TensorLayout::end() const -> const_iterator { return this->begin() + this->rank(); } diff --git a/src/nvcv/src/include/nvcv/TensorLayoutDef.inc b/src/nvcv/src/include/nvcv/TensorLayoutDef.inc index adae2e0fe..27df8d40e 100644 --- a/src/nvcv/src/include/nvcv/TensorLayoutDef.inc +++ b/src/nvcv/src/include/nvcv/TensorLayoutDef.inc @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,7 +15,13 @@ * limitations under the License. */ +NVCV_DETAIL_DEF_TLAYOUT(N) +NVCV_DETAIL_DEF_TLAYOUT(C) +NVCV_DETAIL_DEF_TLAYOUT(F) +NVCV_DETAIL_DEF_TLAYOUT(D) +NVCV_DETAIL_DEF_TLAYOUT(H) NVCV_DETAIL_DEF_TLAYOUT(W) + NVCV_DETAIL_DEF_TLAYOUT(WC) NVCV_DETAIL_DEF_TLAYOUT(CW) diff --git a/src/nvcv/src/include/nvcv/TensorLayoutInfo.hpp b/src/nvcv/src/include/nvcv/TensorLayoutInfo.hpp index b213ee8b0..bba36fbd4 100644 --- a/src/nvcv/src/include/nvcv/TensorLayoutInfo.hpp +++ b/src/nvcv/src/include/nvcv/TensorLayoutInfo.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -56,7 +56,7 @@ class TensorLayoutInfo */ static Optional Create(const TensorLayout &layout) { - return TensorLayoutInfo{layout}; + return Optional{TensorLayoutInfo{layout}}; } /** @@ -101,7 +101,7 @@ class TensorLayoutInfo } protected: - TensorLayoutInfo(const TensorLayout &layout) + explicit TensorLayoutInfo(const TensorLayout &layout) : m_layout(layout) { // isBatch ---------------- @@ -168,11 +168,11 @@ class TensorLayoutInfoImage : public TensorLayoutInfo { if (IsCompatible(layout)) { - return TensorLayoutInfoImage{layout}; + return Optional{TensorLayoutInfoImage{layout}}; } else { - return NullOpt; + return Optional{NullOpt}; } } @@ -258,22 +258,23 @@ class TensorLayoutInfoImage : public TensorLayoutInfo } protected: - TensorLayoutInfoImage(const TensorLayout &layout) + explicit TensorLayoutInfoImage(const TensorLayout &layout) : TensorLayoutInfo(layout) { - m_cacheNumSpatialDims = std::count_if(layout.begin(), layout.end(), - [](char v) - { - switch (v) - { - case LABEL_WIDTH: - case LABEL_HEIGHT: - case LABEL_DEPTH: - return true; - default: - return false; - } - }); + m_cacheNumSpatialDims = static_cast(std::count_if( // NOSONAR: std::ranges::count_if is C++20. + layout.begin(), layout.end(), + [](char v) + { + switch (v) + { + case LABEL_WIDTH: + case LABEL_HEIGHT: + case LABEL_DEPTH: + return true; + default: + return false; + } + })); m_cacheIsRowMajor = layout.endsWith(TENSOR_W) || layout.endsWith(TENSOR_WC); m_cacheIdxChannel = layout.find(LABEL_CHANNEL); diff --git a/src/nvcv/src/include/nvcv/TensorShape.hpp b/src/nvcv/src/include/nvcv/TensorShape.hpp index 3bf3d6fc1..5440deaa0 100644 --- a/src/nvcv/src/include/nvcv/TensorShape.hpp +++ b/src/nvcv/src/include/nvcv/TensorShape.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -39,7 +39,12 @@ class TensorShape /** * @brief Default constructor. */ - TensorShape() = default; + TensorShape() = default; + TensorShape(const TensorShape &) = default; + TensorShape(TensorShape &&) noexcept = default; + + TensorShape &operator=(const TensorShape &) = default; + TensorShape &operator=(TensorShape &&) noexcept = default; /** * @brief Constructs a TensorShape with the given shape and layout. @@ -180,7 +185,7 @@ class TensorShape * @param that The TensorShape to compare with. * @return True if this TensorShape is equal to `that`, false otherwise. */ - bool operator==(const TensorShape &that) const + bool operator==(const TensorShape &that) const // NOSONAR: defaulted comparisons are C++20. { return std::tie(m_shape, m_layout) == std::tie(that.m_shape, that.m_layout); } @@ -245,7 +250,8 @@ class TensorShape inline TensorShape Permute(const TensorShape &src, TensorLayout dstLayout) { TensorShape::ShapeType dst(dstLayout.rank()); - detail::CheckThrow(nvcvTensorShapePermute(src.layout(), &src[0], dstLayout, &dst[0])); + detail::CheckThrow(nvcvTensorShapePermute(static_cast(src.layout()), &src[0], + static_cast(dstLayout), &dst[0])); return {std::move(dst), std::move(dstLayout)}; } diff --git a/src/nvcv/src/include/nvcv/TensorShapeInfo.hpp b/src/nvcv/src/include/nvcv/TensorShapeInfo.hpp index 75ef9148f..76d21134e 100644 --- a/src/nvcv/src/include/nvcv/TensorShapeInfo.hpp +++ b/src/nvcv/src/include/nvcv/TensorShapeInfo.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -73,7 +73,7 @@ class TensorShapeInfoImpl int idx = m_infoLayout.idxSample(); if (idx >= 0) { - m_cacheNumSamples = m_shape[idx]; + m_cacheNumSamples = static_cast(m_shape[idx]); } else if (m_shape.layout() != TENSOR_NONE) { @@ -85,6 +85,12 @@ class TensorShapeInfoImpl } } + TensorShapeInfoImpl(const TensorShapeInfoImpl &) = default; + TensorShapeInfoImpl(TensorShapeInfoImpl &&) noexcept = default; + + TensorShapeInfoImpl &operator=(const TensorShapeInfoImpl &) = default; + TensorShapeInfoImpl &operator=(TensorShapeInfoImpl &&) noexcept = default; + /** * @brief Returns the shape of the tensor. * @@ -135,7 +141,7 @@ class TensorShapeInfoImpl return m_infoLayout.isImage(); } -protected: +private: TensorShape m_shape; LayoutInfo m_infoLayout; int m_cacheNumSamples; @@ -155,6 +161,12 @@ class TensorShapeInfo : public detail::TensorShapeInfoImpl using Base = detail::TensorShapeInfoImpl; public: + TensorShapeInfo(const TensorShapeInfo &) = default; + TensorShapeInfo(TensorShapeInfo &&) noexcept = default; + + TensorShapeInfo &operator=(const TensorShapeInfo &) = default; + TensorShapeInfo &operator=(TensorShapeInfo &&) noexcept = default; + /** * @brief Checks if the provided tensor shape is compatible with this class. * In this case, all tensor shapes are considered compatible. @@ -178,16 +190,14 @@ class TensorShapeInfo : public detail::TensorShapeInfoImpl */ static Optional Create(const TensorShape &tshape) { - return TensorShapeInfo(tshape); + return Optional{TensorShapeInfo{tshape}}; } private: - TensorShapeInfo(const TensorShape &tshape) + explicit TensorShapeInfo(const TensorShape &tshape) : Base(tshape, *TensorLayoutInfo::Create(tshape.layout())) { } - - Optional m_infoLayout; }; /** @@ -202,6 +212,12 @@ class TensorShapeInfoImage : public detail::TensorShapeInfoImpl; public: + TensorShapeInfoImage(const TensorShapeInfoImage &) = default; + TensorShapeInfoImage(TensorShapeInfoImage &&) noexcept = default; + + TensorShapeInfoImage &operator=(const TensorShapeInfoImage &) = default; + TensorShapeInfoImage &operator=(TensorShapeInfoImage &&) noexcept = default; + /** * @brief Checks if the provided tensor shape is compatible with this class. * A tensor shape is considered compatible if both `TensorShapeInfo` and `TensorLayoutInfo` deem it compatible. @@ -226,11 +242,11 @@ class TensorShapeInfoImage : public detail::TensorShapeInfoImpl{TensorShapeInfoImage{tshape}}; } else { - return NullOpt; + return Optional{NullOpt}; } } @@ -270,7 +286,7 @@ class TensorShapeInfoImage : public detail::TensorShapeInfoImplinfoLayout().idxChannel(); if (idx >= 0) { - m_cacheNumChannels = m_shape[idx]; + m_cacheNumChannels = static_cast(this->shape()[idx]); } else { @@ -295,13 +311,13 @@ class TensorShapeInfoImage : public detail::TensorShapeInfoImpl(this->shape()[idx]); // idxHeight idx = this->infoLayout().idxHeight(); if (idx >= 0) { - m_cacheSize.h = m_shape[idx]; + m_cacheSize.h = static_cast(this->shape()[idx]); } else { @@ -309,6 +325,7 @@ class TensorShapeInfoImage : public detail::TensorShapeInfoImpl{TensorShapeInfoImagePlanar{tshape}}; } else { - return NullOpt; + return Optional{NullOpt}; } } @@ -392,7 +415,7 @@ class TensorShapeInfoImagePlanar : public TensorShapeInfoImage private: int m_cacheNumPlanes; - TensorShapeInfoImagePlanar(const TensorShape &tshape) + explicit TensorShapeInfoImagePlanar(const TensorShape &tshape) : TensorShapeInfoImage(tshape) { // numPlanes @@ -405,7 +428,7 @@ class TensorShapeInfoImagePlanar : public TensorShapeInfoImage int ichannel = this->infoLayout().idxChannel(); if (ichannel >= 0) { - m_cacheNumPlanes = m_shape[ichannel]; + m_cacheNumPlanes = static_cast(this->shape()[ichannel]); } else { diff --git a/src/nvcv/src/include/nvcv/alloc/Allocator.h b/src/nvcv/src/include/nvcv/alloc/Allocator.h index 67c67f950..4580882ed 100644 --- a/src/nvcv/src/include/nvcv/alloc/Allocator.h +++ b/src/nvcv/src/include/nvcv/alloc/Allocator.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -74,7 +74,7 @@ extern "C" * @returns Pointer to allocated memory buffer. * Must return NULL if buffer cannot be allocated. */ -typedef void *(*NVCVMemAllocFunc)(void *ctx, int64_t sizeBytes, int32_t alignBytes); +typedef NVCVMemoryBuffer (*NVCVMemAllocFunc)(NVCVResourceContext ctx, int64_t sizeBytes, int32_t alignBytes); /** Function type for memory deallocation. * @@ -83,7 +83,7 @@ typedef void *(*NVCVMemAllocFunc)(void *ctx, int64_t sizeBytes, int32_t alignByt * If NULL, the operation must do nothing, successfully. * @param [in] sizeBytes, alignBytes Parameters passed during buffer allocation. */ -typedef void (*NVCVMemFreeFunc)(void *ctx, void *ptr, int64_t sizeBytes, int32_t alignBytes); +typedef void (*NVCVMemFreeFunc)(NVCVResourceContext ctx, NVCVMemoryBuffer ptr, int64_t sizeBytes, int32_t alignBytes); /** Memory types handled by the memory resource allocator. */ typedef enum @@ -118,7 +118,7 @@ typedef union NVCVCustomResourceAllocatorRec typedef struct NVCVResourceAllocatorRec NVCVResourceAllocator; /** Custom allocator cleanup function type */ -typedef void (*NVCVResourceAllocatorCleanupFunc)(void *ctx, NVCVResourceAllocator *data); +typedef void (*NVCVResourceAllocatorCleanupFunc)(NVCVResourceContext ctx, NVCVResourceAllocator *data); struct NVCVResourceAllocatorRec { @@ -126,7 +126,7 @@ struct NVCVResourceAllocatorRec * It's passed unchanged to memory allocation/deallocation functions. * It can be NULL, in this case no context is passed in. */ - void *ctx; + NVCVResourceContext ctx; /** Type of memory being handled by fnMemAlloc and fnMemFree. */ NVCVResourceType resType; @@ -229,7 +229,7 @@ NVCV_PUBLIC NVCVStatus nvcvAllocatorGet(NVCVAllocatorHandle handle, NVCVResource * @retval #NVCV_ERROR_INVALID_ARGUMENT Some parameter is outside valid range. * @retval #NVCV_SUCCESS Operation executed successfully. */ -NVCV_PUBLIC NVCVStatus nvcvAllocatorSetUserPointer(NVCVAllocatorHandle handle, void *userPtr); +NVCV_PUBLIC NVCVStatus nvcvAllocatorSetUserPointer(NVCVAllocatorHandle handle, NVCVUserPointer userPtr); /** Returns the user pointer associated with the allocator handle. * @@ -243,7 +243,7 @@ NVCV_PUBLIC NVCVStatus nvcvAllocatorSetUserPointer(NVCVAllocatorHandle handle, v * @retval #NVCV_ERROR_INVALID_ARGUMENT Some parameter is outside valid range. * @retval #NVCV_SUCCESS Operation executed successfully. */ -NVCV_PUBLIC NVCVStatus nvcvAllocatorGetUserPointer(NVCVAllocatorHandle handle, void **outUserPtr); +NVCV_PUBLIC NVCVStatus nvcvAllocatorGetUserPointer(NVCVAllocatorHandle handle, NVCVUserPointer *outUserPtr); /** Allocates a memory buffer of a host-accessible memory. * @@ -264,8 +264,8 @@ NVCV_PUBLIC NVCVStatus nvcvAllocatorGetUserPointer(NVCVAllocatorHandle handle, v * @retval #NVCV_ERROR_OUT_OF_MEMORY Not enough free memory. * @retval #NVCV_SUCCESS Operation completed successfully. */ -NVCV_PUBLIC NVCVStatus nvcvAllocatorAllocHostMemory(NVCVAllocatorHandle halloc, void **ptr, int64_t sizeBytes, - int32_t alignBytes); +NVCV_PUBLIC NVCVStatus nvcvAllocatorAllocHostMemory(NVCVAllocatorHandle halloc, NVCVMemoryBuffer *ptr, + int64_t sizeBytes, int32_t alignBytes); /** Frees a host-accessible memory buffer. * @@ -284,7 +284,7 @@ NVCV_PUBLIC NVCVStatus nvcvAllocatorAllocHostMemory(NVCVAllocatorHandle halloc, * @retval #NVCV_ERROR_INVALID_ARGUMENT Some parameter is outside its valid range. * @retval #NVCV_SUCCESS Operation completed successfully. */ -NVCV_PUBLIC NVCVStatus nvcvAllocatorFreeHostMemory(NVCVAllocatorHandle halloc, void *ptr, int64_t sizeBytes, +NVCV_PUBLIC NVCVStatus nvcvAllocatorFreeHostMemory(NVCVAllocatorHandle halloc, NVCVMemoryBuffer ptr, int64_t sizeBytes, int32_t alignBytes); /** Allocates a memory buffer of both host- and cuda-accessible memory. @@ -306,8 +306,8 @@ NVCV_PUBLIC NVCVStatus nvcvAllocatorFreeHostMemory(NVCVAllocatorHandle halloc, v * @retval #NVCV_ERROR_OUT_OF_MEMORY Not enough free memory. * @retval #NVCV_SUCCESS Operation completed successfully. */ -NVCV_PUBLIC NVCVStatus nvcvAllocatorAllocHostPinnedMemory(NVCVAllocatorHandle halloc, void **ptr, int64_t sizeBytes, - int32_t alignBytes); +NVCV_PUBLIC NVCVStatus nvcvAllocatorAllocHostPinnedMemory(NVCVAllocatorHandle halloc, NVCVMemoryBuffer *ptr, + int64_t sizeBytes, int32_t alignBytes); /** Frees a both host- and cuda-accessible memory buffer. * @@ -326,8 +326,8 @@ NVCV_PUBLIC NVCVStatus nvcvAllocatorAllocHostPinnedMemory(NVCVAllocatorHandle ha * @retval #NVCV_ERROR_INVALID_ARGUMENT Some parameter is outside its valid range. * @retval #NVCV_SUCCESS Operation completed successfully. */ -NVCV_PUBLIC NVCVStatus nvcvAllocatorFreeHostPinnedMemory(NVCVAllocatorHandle halloc, void *ptr, int64_t sizeBytes, - int32_t alignBytes); +NVCV_PUBLIC NVCVStatus nvcvAllocatorFreeHostPinnedMemory(NVCVAllocatorHandle halloc, NVCVMemoryBuffer ptr, + int64_t sizeBytes, int32_t alignBytes); /** Allocates a memory buffer of cuda-accessible memory. * @@ -348,8 +348,8 @@ NVCV_PUBLIC NVCVStatus nvcvAllocatorFreeHostPinnedMemory(NVCVAllocatorHandle hal * @retval #NVCV_ERROR_OUT_OF_MEMORY Not enough free memory. * @retval #NVCV_SUCCESS Operation completed successfully. */ -NVCV_PUBLIC NVCVStatus nvcvAllocatorAllocCudaMemory(NVCVAllocatorHandle halloc, void **ptr, int64_t sizeBytes, - int32_t alignBytes); +NVCV_PUBLIC NVCVStatus nvcvAllocatorAllocCudaMemory(NVCVAllocatorHandle halloc, NVCVMemoryBuffer *ptr, + int64_t sizeBytes, int32_t alignBytes); /** Frees a cuda-accessible memory buffer. * @@ -368,7 +368,7 @@ NVCV_PUBLIC NVCVStatus nvcvAllocatorAllocCudaMemory(NVCVAllocatorHandle halloc, * @retval #NVCV_ERROR_INVALID_ARGUMENT Some parameter is outside its valid range. * @retval #NVCV_SUCCESS Operation completed successfully. */ -NVCV_PUBLIC NVCVStatus nvcvAllocatorFreeCudaMemory(NVCVAllocatorHandle halloc, void *ptr, int64_t sizeBytes, +NVCV_PUBLIC NVCVStatus nvcvAllocatorFreeCudaMemory(NVCVAllocatorHandle halloc, NVCVMemoryBuffer ptr, int64_t sizeBytes, int32_t alignBytes); /** Returns a string representation of the resource type. diff --git a/src/nvcv/src/include/nvcv/alloc/Allocator.hpp b/src/nvcv/src/include/nvcv/alloc/Allocator.hpp index d5a388059..1aa44d0e1 100644 --- a/src/nvcv/src/include/nvcv/alloc/Allocator.hpp +++ b/src/nvcv/src/include/nvcv/alloc/Allocator.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,15 +22,26 @@ #include "../detail/Callback.hpp" #include "../detail/CompilerUtils.h" #include "../detail/TypeTraits.hpp" +#include "../detail/UniqueObj.hpp" #include "Allocator.h" +#include #include #include #include #include +#include +#include +#include namespace nvcv { +class CustomAllocatorError : public std::logic_error +{ +public: + using std::logic_error::logic_error; +}; + // Helper class to explicitly assign // address alignments. class MemAlignment @@ -120,6 +131,7 @@ class ResourceAllocator return m_data; } +private: NVCVResourceAllocator m_data{}; }; @@ -134,16 +146,22 @@ class MemAllocator : public ResourceAllocator /** Calls the allocation function from the underlying descriptor */ - void *alloc(int64_t size, int32_t align = DEFAULT_ALIGN) + NVCVMemoryBuffer alloc(int64_t size, int32_t align = DEFAULT_ALIGN) { - return m_data.res.mem.fnAlloc(m_data.ctx, size, align); + return data().res.mem.fnAlloc(data().ctx, size, align); } /** Calls the deallocation function from the underlying descriptor */ - void free(void *ptr, int64_t size, int32_t align = DEFAULT_ALIGN) noexcept + void free(NVCVMemoryBuffer ptr, int64_t size, int32_t align = DEFAULT_ALIGN) noexcept + { + data().res.mem.fnFree(data().ctx, ptr, size, align); + } + + template + void free(T *ptr, int64_t size, int32_t align = DEFAULT_ALIGN) noexcept { - m_data.res.mem.fnFree(m_data.ctx, ptr, size, align); + free(static_cast(static_cast(ptr)), size, align); } static constexpr bool IsCompatibleKind(NVCVResourceType resType) @@ -165,7 +183,7 @@ class MemAllocatorWithKind : public MemAllocator MemAllocatorWithKind() = default; - MemAllocatorWithKind(const NVCVResourceAllocator &data); + explicit MemAllocatorWithKind(const NVCVResourceAllocator &data); static constexpr bool IsCompatibleKind(NVCVResourceType resType) { @@ -211,7 +229,37 @@ NVCV_IMPL_SHARED_HANDLE(Allocator); class Allocator : public CoreResource { public: - using CoreResource::CoreResource; + using Base = CoreResource; + + using Base::Base; + using Base::operator=; + + Allocator(const Allocator &other) + : Base(other) + { + } + + Allocator(Allocator &&other) noexcept + : Base(std::move(other)) + { + } + + Allocator &operator=(const Allocator &other) + { + Base::operator=(other); + return *this; + } + + Allocator &operator=(Allocator &&other) noexcept + { + Base::operator=(std::move(other)); + return *this; + } + + ~Allocator() + { + reset(); + } HostMemAllocator hostMem() const; HostPinnedMemAllocator hostPinnedMem() const; @@ -251,8 +299,8 @@ class CustomMemAllocator template struct by_value : std::integral_constant::value - && sizeof(Callable) <= sizeof(void *) - && alignof(Callable) <= alignof(void *)> + && sizeof(Callable) <= sizeof(NVCVResourceContext) + && alignof(Callable) <= alignof(NVCVResourceContext)> { }; @@ -262,6 +310,22 @@ class CustomMemAllocator return std::is_empty::value ? 0 : sizeof(T); } + static NVCVMemoryBuffer ToMemoryBuffer(std::nullptr_t) noexcept + { + return nullptr; + } + + static NVCVMemoryBuffer ToMemoryBuffer(NVCVMemoryBuffer ptr) noexcept + { + return ptr; + } + + template + static NVCVMemoryBuffer ToMemoryBuffer(T *ptr) noexcept + { + return static_cast(static_cast(ptr)); + } + public: /** Constructs a custom memory allocator from a pair of alloc/free functions * @@ -273,7 +337,7 @@ class CustomMemAllocator * { * return alloc.allocate(size, align); * }, - * [&alloc](void *mem, int64_t size, int32_t align) + * [&alloc](NVCVMemoryBuffer mem, int64_t size, int32_t align) * { * alloc.free(mem, size, align); * }); @@ -284,13 +348,14 @@ class CustomMemAllocator */ template::value>, - typename = detail::EnableIf_t::value>> + typename + = detail::EnableIf_t::value>> CustomMemAllocator(AllocFunction &&alloc, FreeFunction &&free); - // TODO(michalz): Add a way of constructing a custom allocator without using lambdas/captures, e.g. + // REVISIT(michalz): Add a way of constructing a custom allocator without using lambdas/captures, e.g. // from an object that matches the allocator concept. - CustomMemAllocator(CustomMemAllocator &&other) + CustomMemAllocator(CustomMemAllocator &&other) noexcept { *this = std::move(other); } @@ -328,16 +393,19 @@ class CustomMemAllocator void reset(NVCVResourceAllocator &&alloc) noexcept { reset(); - std::swap(m_data, alloc); + m_data = std::move(alloc); + alloc = {}; } /** Clears the allocator descriptor, performing cleanup, if necessary. */ void reset() noexcept { - if (m_data.cleanup) - m_data.cleanup(m_data.ctx, &m_data); - m_data = {}; + NVCVResourceAllocator data = release(); + if (data.cleanup) + { + data.cleanup(data.ctx, &data); + } } /** Moves the descriptor from another CustomMemAllocator to this one. @@ -362,7 +430,7 @@ class CustomMemAllocator void Construct(AllocFunction &&alloc, FreeFunction &&free, std::false_type); template - void ConstructFromDuplicateValues(AllocFunction &&alloc, FreeFunction &&free, std::true_type); + void ConstructFromDuplicateValues(const AllocFunction &alloc, const FreeFunction &free, std::true_type); #if __cplusplus < 201703L template @@ -394,6 +462,20 @@ class CustomAllocator final : public Allocator public: explicit CustomAllocator(ResourceAllocators &&...allocators); + CustomAllocator(const CustomAllocator &) = delete; + CustomAllocator &operator=(const CustomAllocator &) = delete; + + CustomAllocator(CustomAllocator &&other) noexcept + : Allocator(std::move(other)) + { + } + + CustomAllocator &operator=(CustomAllocator &&other) noexcept + { + Allocator::operator=(std::move(other)); + return *this; + } + ~CustomAllocator() { preDestroy(); @@ -404,16 +486,17 @@ class CustomAllocator final : public Allocator = detail::Disjunction>...>::value; template - detail::EnableIf_t preDestroy() + detail::EnableIf_t preDestroy() // NOSONAR: this public header is validated with C++11. { if (this->reset() != 0) - throw std::logic_error( + throw CustomAllocatorError( "The allocator context contains references. The handle must not outlive the context."); } template - detail::EnableIf_t preDestroy() noexcept + detail::EnableIf_t preDestroy() noexcept // NOSONAR: keep overload pair C++11-compatible. { + // No external references are held, so there is nothing to release early. } }; @@ -422,11 +505,11 @@ class CustomAllocator final : public Allocator template CustomAllocator CreateCustomAllocator(ResourceAllocators &&...allocators) { - return CustomAllocator{std::move(allocators)...}; + return CustomAllocator{std::forward(allocators)...}; } } // namespace nvcv -#include "AllocatorImpl.hpp" +#include "AllocatorImpl.hpp" // NOSONAR: inline definitions require the declarations above. #endif // NVCV_ALLOC_ALLOCATOR_HPP diff --git a/src/nvcv/src/include/nvcv/alloc/AllocatorImpl.hpp b/src/nvcv/src/include/nvcv/alloc/AllocatorImpl.hpp index ca439be92..58acd11bb 100644 --- a/src/nvcv/src/include/nvcv/alloc/AllocatorImpl.hpp +++ b/src/nvcv/src/include/nvcv/alloc/AllocatorImpl.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -71,10 +71,11 @@ CustomMemAllocator::CustomMemAllocator(AllocFunction &&alloc, Fre const bool trivial = has_trivial_copy_and_destruction::value && has_trivial_copy_and_destruction::value; - const bool tuple_by_value = trivial && sizeof(T) <= sizeof(void *) && alignof(T) <= alignof(void *); + const bool tuple_by_value + = trivial && sizeof(T) <= sizeof(NVCVResourceContext) && alignof(T) <= alignof(NVCVResourceContext); - const bool construct_from_one_value_if_equal = trivial && by_value::value - && by_value::value + const bool construct_from_one_value_if_equal = trivial && std::is_same::value + && by_value::value && by_value::value && sizeof(AllocFunction) == sizeof(FreeFunction); // Can we fit the tuple inside a single pointer? If yes, go for it! @@ -83,8 +84,8 @@ CustomMemAllocator::CustomMemAllocator(AllocFunction &&alloc, Fre Construct(std::forward(alloc), std::forward(free), std::integral_constant()); } - // Are the two callables trivial and do they context objects coincide? If yes, use that object and reinterpret the data - // This might be useful in a common case where both alloc and free are lambdas that capture only one - and the same - pointer-like value. + // Are the same-type callables trivial and do their context objects coincide? If yes, use that object. + // Different callable types must not share bytes because that would interpret one object as another type. else if NVCV_IF_CONSTEXPR (construct_from_one_value_if_equal) { if (!std::memcmp(&alloc, &free, std::min(DataSize(), DataSize()))) @@ -109,20 +110,26 @@ template template void CustomMemAllocator::Construct(AllocFunction &&alloc, FreeFunction &&free, std::true_type) { - using T = std::tuple; // TODO - use something that's trivially copyable - T ctx{std::move(alloc), std::move(free)}; - static_assert(sizeof(T) <= sizeof(void *), "Internal error - this should never be invoked with a type that large."); + struct T + { + AllocFunction alloc; + FreeFunction free; + }; + + T ctx{std::forward(alloc), std::forward(free)}; + static_assert(sizeof(T) <= sizeof(NVCVResourceContext), + "Internal error - this should never be invoked with a type that large."); - m_data.res.mem.fnAlloc = [](void *c, int64_t size, int32_t align) -> void * + m_data.res.mem.fnAlloc = [](NVCVResourceContext c, int64_t size, int32_t align) -> NVCVMemoryBuffer { - T *target = reinterpret_cast(&c); - auto &&callable = std::get<0>(*target); - return callable(size, align); + auto *target = reinterpret_cast(&c); + auto &&callable = target->alloc; + return CustomMemAllocator::ToMemoryBuffer(callable(size, align)); }; - m_data.res.mem.fnFree = [](void *c, void *ptr, int64_t size, int32_t align) + m_data.res.mem.fnFree = [](NVCVResourceContext c, NVCVMemoryBuffer ptr, int64_t size, int32_t align) { - T *target = reinterpret_cast(&c); - auto &&callable = std::get<1>(*target); + auto *target = reinterpret_cast(&c); + auto &&callable = target->free; callable(ptr, size, align); }; @@ -136,43 +143,46 @@ template template void CustomMemAllocator::Construct(AllocFunction &&alloc, FreeFunction &&free, std::false_type) { - using T = std::tuple; - std::unique_ptr ctx(new T{std::move(alloc), std::move(free)}); - auto cleanup = [](void *ctx, NVCVResourceAllocator *) noexcept + using T = std::tuple; + auto ctx = detail::MakeUniqueObj(std::forward(alloc), std::forward(free)); + auto cleanup = [](NVCVResourceContext ctx, NVCVResourceAllocator *) noexcept { - delete (T *)ctx; + detail::UniqueObj storage(static_cast(static_cast(ctx))); + (void)storage; }; - m_data.res.mem.fnAlloc = [](void *c, int64_t size, int32_t align) -> void * + m_data.res.mem.fnAlloc = [](NVCVResourceContext c, int64_t size, int32_t align) -> NVCVMemoryBuffer { - return std::get<0>(*static_cast(c))(size, align); + auto *ctx = static_cast(static_cast(c)); + return CustomMemAllocator::ToMemoryBuffer(std::get<0>(*ctx)(size, align)); }; - m_data.res.mem.fnFree = [](void *c, void *ptr, int64_t size, int32_t align) + m_data.res.mem.fnFree = [](NVCVResourceContext c, NVCVMemoryBuffer ptr, int64_t size, int32_t align) { - std::get<1> (*static_cast(c))(ptr, size, align); + auto *ctx = static_cast(static_cast(c)); + std::get<1> (*ctx)(ptr, size, align); }; m_data.cleanup = cleanup; - m_data.ctx = ctx.release(); + m_data.ctx = static_cast(static_cast(ctx.release())); } template template -void CustomMemAllocator::ConstructFromDuplicateValues(AllocFunction &&alloc, FreeFunction &&free, - std::true_type) +void CustomMemAllocator::ConstructFromDuplicateValues(const AllocFunction &alloc, + const FreeFunction &free, std::true_type) { static_assert(std::is_trivially_copyable::value || std::is_empty::value, "Internal error - should not pick this overload"); static_assert(std::is_trivially_copyable::value || std::is_empty::value, "Internal error - should not pick this overload"); - m_data.res.mem.fnAlloc = [](void *c, int64_t size, int32_t align) -> void * + m_data.res.mem.fnAlloc = [](NVCVResourceContext c, int64_t size, int32_t align) -> NVCVMemoryBuffer { - AllocFunction *alloc = reinterpret_cast(&c); - return (*alloc)(size, align); + auto *alloc = reinterpret_cast(&c); + return CustomMemAllocator::ToMemoryBuffer((*alloc)(size, align)); }; - m_data.res.mem.fnFree = [](void *c, void *ptr, int64_t size, int32_t align) + m_data.res.mem.fnFree = [](NVCVResourceContext c, NVCVMemoryBuffer ptr, int64_t size, int32_t align) { - FreeFunction *free = reinterpret_cast(&c); + auto *free = reinterpret_cast(&c); (*free)(ptr, size, align); }; @@ -190,12 +200,11 @@ void CustomMemAllocator::ConstructFromDuplicateValues(AllocFuncti template inline CustomAllocator::CustomAllocator(ResourceAllocators &&...allocators) { - NVCVResourceAllocator data[] = {allocators.cdata()...}; - NVCVAllocatorHandle h = {}; - detail::CheckThrow(nvcvAllocatorConstructCustom(data, sizeof...(allocators), &h)); + std::array data = {allocators.cdata()...}; + NVCVAllocatorHandle h = {}; + detail::CheckThrow(nvcvAllocatorConstructCustom(data.data(), static_cast(data.size()), &h)); // void-cast the (nodiscard) result of the allocators - we know what we're doing here... - int dummy[] = {((void)allocators.release(), 0)...}; - (void)dummy; + (void)std::initializer_list{((void)allocators.release(), 0)...}; reset(std::move(h)); } diff --git a/src/nvcv/src/include/nvcv/alloc/Fwd.h b/src/nvcv/src/include/nvcv/alloc/Fwd.h index 3920eec1d..10bbe1438 100644 --- a/src/nvcv/src/include/nvcv/alloc/Fwd.h +++ b/src/nvcv/src/include/nvcv/alloc/Fwd.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -29,4 +29,17 @@ typedef struct NVCVAllocator *NVCVAllocatorHandle; typedef struct NVCVResourceAllocatorRec NVCVResourceAllocator; +typedef struct NVCVResourceContextRec NVCVResourceContextRec; +typedef struct NVCVMemoryBufferRec NVCVMemoryBufferRec; +typedef struct NVCVUserPointerRec NVCVUserPointerRec; + +/** Opaque user context passed to custom allocator callbacks. */ +typedef NVCVResourceContextRec *NVCVResourceContext; + +/** Opaque memory buffer returned by allocator callbacks. */ +typedef NVCVMemoryBufferRec *NVCVMemoryBuffer; + +/** Opaque user pointer associated with NVCV resources. */ +typedef NVCVUserPointerRec *NVCVUserPointer; + #endif // NVCV_ALLOC_FWD_H diff --git a/src/nvcv/src/include/nvcv/alloc/Requirements.hpp b/src/nvcv/src/include/nvcv/alloc/Requirements.hpp index 2f6ab1493..838fc915d 100644 --- a/src/nvcv/src/include/nvcv/alloc/Requirements.hpp +++ b/src/nvcv/src/include/nvcv/alloc/Requirements.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -39,7 +39,7 @@ class Requirements final class ConstMemory { friend class Requirements; - ConstMemory(const NVCVMemRequirements &reqs); + explicit ConstMemory(const NVCVMemRequirements &reqs); protected: const NVCVMemRequirements &m_reqs; @@ -54,7 +54,9 @@ class Requirements final class Memory : public ConstMemory { friend class Requirements; - Memory(NVCVMemRequirements &reqs); + explicit Memory(NVCVMemRequirements &reqs); + + NVCVMemRequirements &m_mutReqs; public: void addBuffer(int64_t bufSize, int64_t bufAlignment); @@ -94,7 +96,7 @@ inline Requirements::ConstMemory::ConstMemory(const NVCVMemRequirements &reqs) { } -inline constexpr int Requirements::ConstMemory::size() +constexpr int Requirements::ConstMemory::size() { return NVCV_MAX_MEM_REQUIREMENTS_LOG2_BLOCK_SIZE; } @@ -122,12 +124,13 @@ inline int64_t Requirements::ConstMemory::numBlocks(int log2BlockSizeBytes) cons inline Requirements::Memory::Memory(NVCVMemRequirements &reqs) : ConstMemory(reqs) + , m_mutReqs(reqs) { } inline NVCVMemRequirements &Requirements::Memory::cdata() { - return const_cast(m_reqs); + return m_mutReqs; } inline void Requirements::Memory::addBuffer(int64_t bufSize, int64_t bufAlign) diff --git a/src/nvcv/src/include/nvcv/detail/Align.hpp b/src/nvcv/src/include/nvcv/detail/Align.hpp index 4b8dd584b..d0f590534 100644 --- a/src/nvcv/src/include/nvcv/detail/Align.hpp +++ b/src/nvcv/src/include/nvcv/detail/Align.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -49,7 +49,7 @@ constexpr T AlignDown(T value, T alignment_pow2) // NOTE: This is much more efficient than (value/alignment) * alignment for run-time alignment values, where // the compiler cannot replace the division/multiplication with bit shifts. - return value & -alignment_pow2; + return value & ~(alignment_pow2 - T{1}); } /** @@ -96,9 +96,10 @@ constexpr bool IsAligned(T value, T alignment_pow2) * @return true if value is a multiple of alignment_pow2 * @return false otherwise */ -inline bool IsAligned(const void *ptr, uintptr_t alignment_pow2) +template +inline bool IsAligned(const T *ptr, uintptr_t alignment_pow2) { - return IsAligned((uintptr_t)ptr, alignment_pow2); + return IsAligned(reinterpret_cast(ptr), alignment_pow2); } }} // namespace nvcv::detail diff --git a/src/nvcv/src/include/nvcv/detail/ArrayDataImpl.hpp b/src/nvcv/src/include/nvcv/detail/ArrayDataImpl.hpp index 38d85496b..d77ea6825 100644 --- a/src/nvcv/src/include/nvcv/detail/ArrayDataImpl.hpp +++ b/src/nvcv/src/include/nvcv/detail/ArrayDataImpl.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -84,11 +84,11 @@ inline Optional ArrayData::cast() const if (IsCompatible()) { - return Derived{this->cdata()}; + return Optional{Derived{this->cdata()}}; } else { - return NullOpt; + return Optional{NullOpt}; } } @@ -121,7 +121,7 @@ inline ArrayDataCuda::ArrayDataCuda(int64_t length, const DataType &dtype, const data.length = length; data.capacity = length; - data.dtype = dtype; + data.dtype = static_cast(dtype); data.bufferType = NVCV_ARRAY_BUFFER_CUDA; data.buffer.strided = buffer; @@ -143,7 +143,7 @@ inline ArrayDataHost::ArrayDataHost(int64_t length, const DataType &dtype, const data.length = length; data.capacity = length; - data.dtype = dtype; + data.dtype = static_cast(dtype); data.bufferType = NVCV_ARRAY_BUFFER_HOST; data.buffer.strided = buffer; @@ -165,7 +165,7 @@ inline ArrayDataHostPinned::ArrayDataHostPinned(int64_t length, const DataType & data.length = length; data.capacity = length; - data.dtype = dtype; + data.dtype = static_cast(dtype); data.bufferType = NVCV_ARRAY_BUFFER_HOST_PINNED; data.buffer.strided = buffer; diff --git a/src/nvcv/src/include/nvcv/detail/ArrayImpl.hpp b/src/nvcv/src/include/nvcv/detail/ArrayImpl.hpp index 037277b1b..c6aa9c94b 100644 --- a/src/nvcv/src/include/nvcv/detail/ArrayImpl.hpp +++ b/src/nvcv/src/include/nvcv/detail/ArrayImpl.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -36,7 +36,7 @@ inline int64_t Array::length() const return length; } -inline void Array::resize(int64_t length) +inline void Array::resize(int64_t length) // NOSONAR: mutates state through the wrapped C handle. { NVCVArrayHandle harray = this->handle(); @@ -83,14 +83,14 @@ inline ArrayData Array::exportData() const return ArrayData(data); } -inline void Array::setUserPointer(void *ptr) +inline void Array::setUserPointer(NVCVUserPointer ptr) // NOSONAR: mutates state through the wrapped C handle. { detail::CheckThrow(nvcvArraySetUserPointer(this->handle(), ptr)); } -inline void *Array::userPointer() const +inline NVCVUserPointer Array::userPointer() const { - void *ptr; + NVCVUserPointer ptr; detail::CheckThrow(nvcvArrayGetUserPointer(this->handle(), &ptr)); return ptr; } @@ -99,7 +99,8 @@ inline auto Array::CalcRequirements(int64_t capacity, DataType dtype, int32_t al -> Requirements { Requirements reqs; - detail::CheckThrow(nvcvArrayCalcRequirementsWithTarget(capacity, dtype, alignment, target, &reqs)); + detail::CheckThrow( + nvcvArrayCalcRequirementsWithTarget(capacity, static_cast(dtype), alignment, target, &reqs)); return reqs; } @@ -123,7 +124,7 @@ inline Array ArrayWrapData(const ArrayData &data, ArrayDataCleanupCallback &&cle NVCVArrayHandle handle; detail::CheckThrow( nvcvArrayWrapDataConstruct(&data.cdata(), cleanup.targetFunc(), cleanup.targetHandle(), &handle)); - cleanup.release(); // already owned by the array + std::move(cleanup).release(); // already owned by the array return Array(std::move(handle)); } diff --git a/src/nvcv/src/include/nvcv/detail/Callback.hpp b/src/nvcv/src/include/nvcv/detail/Callback.hpp index 17e2566a5..a385180d3 100644 --- a/src/nvcv/src/include/nvcv/detail/Callback.hpp +++ b/src/nvcv/src/include/nvcv/detail/Callback.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,7 +19,9 @@ #define NVCV_CALLBACK_HPP #include "TypeTraits.hpp" +#include "UniqueObj.hpp" +#include #include #include #include @@ -35,7 +37,7 @@ struct NoTranslation template auto operator()(Callable &&c, Args &&...args) -> decltype(c(std::forward(args)...)) { - return c(std::forward(args)...); + return c(std::forward(args)...); // NOSONAR: callback wrapper preserves the requested return type. } }; @@ -109,14 +111,14 @@ class Callback static_assert(std::is_same::value, "The first argument to the C wrapper must be of type void *"); using FunctionType = Ret(Args...); - using WrappedFunc = CRet(void *, CArgs...); - using CleanupFunc = void(void *); + using WrappedFunc = CRet(CtxArg, CArgs...); + using CleanupFunc = void(CtxArg); Callback() = default; Callback(const Callback &) = delete; - Callback(Callback &&cb) + Callback(Callback &&cb) noexcept { *this = std::move(cb); } @@ -135,7 +137,7 @@ class Callback * If SingleUse is true, all required cleanup must be also performed by `function` * and `cleanup` will not be called. */ - void reset(WrappedFunc *function = nullptr, void *target = nullptr, CleanupFunc *cleanup = nullptr) + void reset(WrappedFunc *function = nullptr, CtxArg target = nullptr, CleanupFunc *cleanup = nullptr) noexcept { if (m_cleanup) m_cleanup(m_target.asOpaqueHandle()); @@ -144,7 +146,7 @@ class Callback m_cleanup = cleanup; } - std::tuple release() + std::tuple release() noexcept { auto ret = std::make_tuple(m_call, m_target.asOpaqueHandle(), m_cleanup); m_call = nullptr; @@ -158,7 +160,7 @@ class Callback template Callback &operator=(const Callback &f) = delete; - Callback &operator=(Callback &&cb) + Callback &operator=(Callback &&cb) noexcept { if (&cb != this) { @@ -171,7 +173,7 @@ class Callback } template - detail::EnableIf_t::value, Callback &> &operator=(FunctionLike &&f) + detail::EnableIf_t::value, Callback &> &operator=(FunctionLike &&f) { return *this = Callback(std::forward(f)); } @@ -186,17 +188,18 @@ class Callback typename = detail::EnableIf_t< detail::IsInvocableR::value && !detail::IsCallback>::value && + !std::is_same, Callback>::value && !detail::IsStdFunction>::value >> // clang-format on - Callback(Callable &&c) + explicit Callback(Callable &&c) { fromCallable(std::forward(c)); } /** Wraps a function pointer */ - Callback(FunctionType *f) + explicit Callback(FunctionType *f) { fromFunction(f); } @@ -205,7 +208,7 @@ class Callback */ template::value>> - Callback(RetF (*f)(ArgsF...)) + explicit Callback(RetF (*f)(ArgsF...)) { fromFunction(f); } @@ -217,7 +220,7 @@ class Callback */ template, Args...>::value>> - Callback(const std::function &f) + explicit Callback(const std::function &f) { // Suppress false positive warning from g++-12-14 about std::function::target() #if defined(__GNUC__) && __GNUC__ >= 12 @@ -274,7 +277,7 @@ class Callback /** Returns an opaque, type-erased value that describes the invocation target. */ - void *targetHandle() const + CtxArg targetHandle() const { return m_target.asOpaqueHandle(); } @@ -336,20 +339,20 @@ class Callback private: struct alignas(void *) TargetBlob { - char data[sizeof(void *)]; + std::array data; /** Reinterprets the contents of the blob as `void*` */ - void *asOpaqueHandle() const noexcept + CtxArg asOpaqueHandle() const noexcept { - void *h; - std::memcpy(&h, data, sizeof(h)); + CtxArg h; + std::memcpy(&h, data.data(), sizeof(h)); return h; } /** Copies the opaque handle to the data blob */ - void fromOpaqueHandle(void *h) noexcept + void fromOpaqueHandle(CtxArg h) noexcept { - std::memcpy(data, &h, sizeof(data)); + std::memcpy(data.data(), &h, data.size()); } }; @@ -375,13 +378,25 @@ class Callback ByValue }; + template + struct CallableKindSelector + { + static constexpr CallableKind value = isEmpty() ? CallableKind::Empty // NOSONAR + : isByValue() ? CallableKind::ByValue // NOSONAR + : CallableKind::Other; + }; + + template + static constexpr CallableKind GetCallableKind() + { + return CallableKindSelector::value; + } + template void fromCallable(Callable &&c) { using C = detail::RemoveCVRef_t; - constexpr CallableKind kind = isEmpty() ? CallableKind::Empty - : isByValue() ? CallableKind::ByValue - : CallableKind::Other; + constexpr CallableKind kind = GetCallableKind(); fromCallable(std::forward(c), std::integral_constant(), std::integral_constant()); } @@ -390,7 +405,7 @@ class Callback void fromCallable(Callable &&, std::integral_constant, std::integral_constant) { - m_call = [](void *, CArgs... args) -> CRet + m_call = [](CtxArg, CArgs... args) -> CRet { TranslateCall tr; return tr(Callable{}, std::move(args)...); @@ -402,10 +417,10 @@ class Callback std::integral_constant) { using C = detail::RemoveCVRef_t; - new (m_target.data) C{std::forward(c)}; - m_call = [](void *target, CArgs... args) -> CRet + new (m_target.data.data()) C{std::forward(c)}; + m_call = [](CtxArg target, CArgs... args) -> CRet { - C *c = reinterpret_cast(&target); + auto *c = reinterpret_cast(&target); TranslateCall tr; return tr(*c, std::move(args)...); @@ -418,16 +433,17 @@ class Callback { using C = detail::RemoveCVRef_t; - std::unique_ptr ptr(new C(std::forward(c))); + auto ptr = detail::MakeUniqueObj(std::forward(c)); - m_call = [](void *target, CArgs... args) -> CRet + m_call = [](CtxArg target, CArgs... args) -> CRet { TranslateCall tr; - return tr(*static_cast(target), std::move(args)...); + return tr(*static_cast(target), std::move(args)...); // NOSONAR: CRet controls callback conversion. }; - m_cleanup = [](void *target) + m_cleanup = [](CtxArg target) { - delete static_cast(target); + detail::UniqueObj c(static_cast(target)); + (void)c; }; m_target.fromOpaqueHandle(ptr.release()); } @@ -438,18 +454,19 @@ class Callback { using C = detail::RemoveCVRef_t; - std::unique_ptr ptr(new C(std::forward(c))); + auto ptr = detail::MakeUniqueObj(std::forward(c)); - m_call = [](void *target, CArgs... args) -> CRet + m_call = [](CtxArg target, CArgs... args) -> CRet { // this will get destroyed even if the invocation or translation throws - std::unique_ptr c(static_cast(target)); - TranslateCall tr; + detail::UniqueObj c(static_cast(target)); + TranslateCall tr; return tr(*c, std::move(args)...); }; - m_cleanup = [](void *target) + m_cleanup = [](CtxArg target) { - delete static_cast(target); + detail::UniqueObj c(static_cast(target)); + (void)c; }; m_target.fromOpaqueHandle(ptr.release()); } @@ -458,12 +475,12 @@ class Callback void fromFunction(RetF (*f)(ArgsF...)) { using FuncType = RetF(ArgsF...); - m_target.fromOpaqueHandle(reinterpret_cast(f)); - m_call = [](void *target, CArgs... args) -> CRet + m_target.fromOpaqueHandle(reinterpret_cast(f)); + m_call = [](CtxArg target, CArgs... args) -> CRet { - FuncType *ff = reinterpret_cast(target); + auto *ff = reinterpret_cast(target); TranslateCall tr; - return tr(ff, std::move(args)...); + return tr(ff, std::move(args)...); // NOSONAR: CRet controls callback conversion. }; } diff --git a/src/nvcv/src/include/nvcv/detail/CastsImpl.hpp b/src/nvcv/src/include/nvcv/detail/CastsImpl.hpp index a87d37293..1b5a64f1a 100644 --- a/src/nvcv/src/include/nvcv/detail/CastsImpl.hpp +++ b/src/nvcv/src/include/nvcv/detail/CastsImpl.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -60,7 +60,7 @@ class WrapHandle : public IFACE }; template -void SetObjectAssociation(NVCVStatus (*setUserPointer)(H, void *), IFACE *obj, H handle) +void SetObjectAssociation(NVCVStatus (*setUserPointer)(H, NVCVUserPointer), IFACE *obj, H handle) { static_assert(std::is_same::value, "handle type must match interface's"); @@ -69,7 +69,8 @@ void SetObjectAssociation(NVCVStatus (*setUserPointer)(H, void *), IFACE *obj, H } template -IFACE *CastImpl(NVCVStatus (*getUserPointer)(H, void **), NVCVStatus (*setUserPointer)(H, void *), H handle) +IFACE *CastImpl(NVCVStatus (*getUserPointer)(H, NVCVUserPointer *), NVCVStatus (*setUserPointer)(H, NVCVUserPointer), + H handle) { static_assert(std::is_same::value, "handle type must matchinterface's"); assert(getUserPointer != nullptr); diff --git a/src/nvcv/src/include/nvcv/detail/CheckError.hpp b/src/nvcv/src/include/nvcv/detail/CheckError.hpp index 12993b097..80db7ec05 100644 --- a/src/nvcv/src/include/nvcv/detail/CheckError.hpp +++ b/src/nvcv/src/include/nvcv/detail/CheckError.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,22 +21,23 @@ #include "../Exception.hpp" #include "../Status.h" +#include #include namespace nvcv { namespace detail { -inline void ThrowException(NVCVStatus status) +[[noreturn]] inline void ThrowException(NVCVStatus status) { // Because of this stack allocation, compiler might // not inline this call. This it happens only in // error cases, it's ok. - char msg[NVCV_MAX_STATUS_MESSAGE_LENGTH]; + std::array msg; - NVCVStatus tmp = nvcvGetLastErrorMessage(msg, sizeof(msg)); + NVCVStatus tmp = nvcvGetLastErrorMessage(msg.data(), static_cast(msg.size())); (void)tmp; assert(tmp == status); - throw Exception(Exception::InternalCtorTag{}, static_cast(status), "%s", msg); + throw Exception(Exception::InternalCtorTag{}, static_cast(status), "%s", msg.data()); } inline void CheckThrow(NVCVStatus status) diff --git a/src/nvcv/src/include/nvcv/detail/Concepts.hpp b/src/nvcv/src/include/nvcv/detail/Concepts.hpp index a2c676d1b..be421717d 100644 --- a/src/nvcv/src/include/nvcv/detail/Concepts.hpp +++ b/src/nvcv/src/include/nvcv/detail/Concepts.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/src/nvcv/src/include/nvcv/detail/CudaFwd.h b/src/nvcv/src/include/nvcv/detail/CudaFwd.h index 79dac8dee..75746b160 100644 --- a/src/nvcv/src/include/nvcv/detail/CudaFwd.h +++ b/src/nvcv/src/include/nvcv/detail/CudaFwd.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * NVIDIA CORPORATION, its affiliates and licensors retain all intellectual @@ -13,7 +13,7 @@ #ifndef NVCV_DETAIL_FWD_CUDA_H #define NVCV_DETAIL_FWD_CUDA_H -typedef struct CUstream_st *CUstream; -typedef struct cudaArray *cudaArray_t; +typedef struct CUstream_st *CUstream; // NOSONAR: C-compatible forward declaration. +typedef struct cudaArray *cudaArray_t; // NOSONAR: C-compatible forward declaration. #endif // NVCV_DETAIL_FWD_CUDA_H diff --git a/src/nvcv/src/include/nvcv/detail/Format.hpp b/src/nvcv/src/include/nvcv/detail/Format.hpp new file mode 100644 index 000000000..6c6b05c60 --- /dev/null +++ b/src/nvcv/src/include/nvcv/detail/Format.hpp @@ -0,0 +1,782 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#ifndef NVCV_DETAIL_FORMAT_HPP +#define NVCV_DETAIL_FORMAT_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace nvcv { namespace detail { + +class FixedBufferStreamBuf : public std::streambuf +{ +public: + FixedBufferStreamBuf(char *buffer, std::size_t bufferSize) + : m_buffer(buffer) + , m_bufferSize(bufferSize) + { + if (m_buffer != nullptr && m_bufferSize > 0) + { + setp(m_buffer, m_buffer + m_bufferSize - 1); + *m_buffer = '\0'; + } + } + +protected: + int sync() noexcept override + { + if (m_buffer != nullptr && m_bufferSize > 0) + { + if (pptr() < epptr()) + { + *pptr() = '\0'; + } + else + { + m_buffer[m_bufferSize - 1] = '\0'; + } + } + return 0; + } + + int_type overflow(int_type ch) noexcept override + { + return traits_type::eq_int_type(ch, traits_type::eof()) ? traits_type::not_eof(ch) : traits_type::eof(); + } + +private: + char *m_buffer = nullptr; + std::size_t m_bufferSize = 0; +}; + +class FixedBufferOStream +{ +public: + FixedBufferOStream(char *buffer, std::size_t bufferSize) + : m_buf(buffer, bufferSize) + , m_out(&m_buf) + { + } + + std::ostream &get() + { + return m_out; + } + +private: + FixedBufferStreamBuf m_buf; + std::ostream m_out; +}; + +enum class PrintfLength +{ + kNone, + kHH, + kH, + kL, + kLL, + kJ, + kZ, + kT, + kLongDouble +}; + +struct PrintfSpec +{ + bool left = false; + bool showpos = false; + bool space = false; + bool alt = false; + bool zero = false; + int width = -1; + int precision = -1; + + PrintfLength length = PrintfLength::kNone; + char conversion = '\0'; +}; + +inline bool IsDigit(char ch) +{ + return ch >= '0' && ch <= '9'; +} + +inline int ParseInt(const char *&fmt) +{ + int value = 0; + while (IsDigit(*fmt)) + { + value = value * 10 + (*fmt - '0'); + ++fmt; + } + return value; +} + +inline const char *ParsePrintfSpec(const char *fmt, PrintfSpec &spec) +{ + for (;;) + { + switch (*fmt) + { + case '-': + spec.left = true; + ++fmt; + break; + case '+': + spec.showpos = true; + ++fmt; + break; + case ' ': + spec.space = true; + ++fmt; + break; + case '#': + spec.alt = true; + ++fmt; + break; + case '0': + spec.zero = true; + ++fmt; + break; + default: + goto parse_width; + } + } + +parse_width: + if (IsDigit(*fmt)) + { + spec.width = ParseInt(fmt); + } + + if (*fmt == '.') + { + ++fmt; + spec.precision = IsDigit(*fmt) ? ParseInt(fmt) : 0; + } + + switch (*fmt) + { + case 'h': + ++fmt; + if (*fmt == 'h') + { + ++fmt; + spec.length = PrintfLength::kHH; + } + else + { + spec.length = PrintfLength::kH; + } + break; + case 'l': + ++fmt; + if (*fmt == 'l') + { + ++fmt; + spec.length = PrintfLength::kLL; + } + else + { + spec.length = PrintfLength::kL; + } + break; + case 'j': + ++fmt; + spec.length = PrintfLength::kJ; + break; + case 'z': + ++fmt; + spec.length = PrintfLength::kZ; + break; + case 't': + ++fmt; + spec.length = PrintfLength::kT; + break; + case 'L': + ++fmt; + spec.length = PrintfLength::kLongDouble; + break; + default: + break; + } + + spec.conversion = *fmt; + if (*fmt != '\0') + { + ++fmt; + } + return fmt; +} + +inline void ApplyPrintfSpec(std::ostream &out, const PrintfSpec &spec) +{ + if (spec.width >= 0) + { + out.width(spec.width); + } + if (spec.precision >= 0) + { + out.precision(spec.precision); + } + if (spec.left) + { + out.setf(std::ios_base::left, std::ios_base::adjustfield); + } + if (spec.showpos) + { + out.setf(std::ios_base::showpos); + } + if (spec.alt) + { + out.setf(std::ios_base::showbase); + } + if (spec.zero && !spec.left) + { + out.fill('0'); + } +} + +inline void AppendLiteral(std::ostream &out, const char *begin, const char *end) +{ + if (begin < end) + { + out.write(begin, end - begin); + } +} + +inline const char *AppendLiteralUntilPercent(std::ostream &out, const char *fmt, const char *&literalBegin) +{ + while (*fmt != '\0') + { + if (*fmt == '%') + { + AppendLiteral(out, literalBegin, fmt); + return fmt + 1; + } + ++fmt; + } + + AppendLiteral(out, literalBegin, fmt); + return nullptr; +} + +struct StringSlice +{ + const char *data; + std::size_t size; +}; + +inline StringSlice ToStringSlice(const char *value) +{ + const char *out = value != nullptr ? value : "(null)"; + return StringSlice{out, std::char_traits::length(out)}; +} + +template +StringSlice ToStringSlice(const char (&value)[N]) +{ + return StringSlice{value, std::char_traits::length(value)}; +} + +inline StringSlice ToStringSlice(const std::string &value) +{ + return StringSlice{value.data(), value.size()}; +} + +#if __cplusplus >= 201402L +template +using DecayT = std::decay_t; + +template +using EnableIfT = std::enable_if_t; + +template +using ConditionalT = std::conditional_t; + +template +using UnderlyingTypeT = std::underlying_type_t; + +template +using MakeUnsignedT = std::make_unsigned_t; +#else +template +using DecayT = typename std::decay::type; + +template +using EnableIfT = typename std::enable_if::type; + +template +using ConditionalT = typename std::conditional::type; + +template +using UnderlyingTypeT = typename std::underlying_type::type; + +template +using MakeUnsignedT = typename std::make_unsigned::type; +#endif + +template +constexpr bool IsSame() +{ +#if __cplusplus >= 201703L + return std::is_same_v; +#else + return std::is_same::value; +#endif +} + +template +constexpr bool IsEnum() +{ +#if __cplusplus >= 201703L + return std::is_enum_v; +#else + return std::is_enum::value; +#endif +} + +template +constexpr bool IsIntegral() +{ +#if __cplusplus >= 201703L + return std::is_integral_v; +#else + return std::is_integral::value; +#endif +} + +template +constexpr bool IsArithmetic() +{ +#if __cplusplus >= 201703L + return std::is_arithmetic_v; +#else + return std::is_arithmetic::value; +#endif +} + +template +struct IsStringLike +{ + using Decayed = DecayT; + static constexpr bool value + = IsSame() || IsSame() || IsSame(); +}; + +inline void AppendSpaces(std::ostream &out, std::size_t count) +{ + while (count-- > 0) + { + out.put(' '); + } +} + +inline void AppendString(std::ostream &out, const PrintfSpec &spec, StringSlice value) +{ + if (spec.precision >= 0) + { + value.size = std::min(value.size, static_cast(spec.precision)); + } + + if (spec.width > static_cast(value.size) && !spec.left) + { + AppendSpaces(out, static_cast(spec.width) - value.size); + } + + out.write(value.data, value.size); + + if (spec.width > static_cast(value.size) && spec.left) + { + AppendSpaces(out, static_cast(spec.width) - value.size); + } +} + +template()> +struct PrintfIntegralBase +{ + using type = T; +}; + +template +struct PrintfIntegralBase +{ + using type = UnderlyingTypeT; +}; + +template +struct PrintfValueType +{ + using Decayed = DecayT; + using Base = typename PrintfIntegralBase::type; + using Signed = ConditionalT<(sizeof(Base) < sizeof(int)), int, Base>; +}; + +template()> +struct PrintfUnsignedBase +{ + using type = MakeUnsignedT; +}; + +template +struct PrintfUnsignedBase +{ + using type = unsigned int; +}; + +template +struct PrintfUnsignedValueType +{ + using Decayed = DecayT; + using Base = typename PrintfIntegralBase::type; + using UnsignedBase = typename PrintfUnsignedBase::type; + using Unsigned = ConditionalT<(sizeof(UnsignedBase) < sizeof(unsigned int)), unsigned int, UnsignedBase>; +}; + +template +EnableIfT>() || IsEnum>(), void> AppendSigned(std::ostream &out, const T &value) +{ + using Signed = typename PrintfValueType::Signed; + out << static_cast(value); +} + +template +EnableIfT>() && !IsEnum>(), void> AppendSigned(std::ostream &out, T &&value) +{ + out << std::forward(value); +} + +template +EnableIfT>() || IsEnum>(), void> AppendUnsigned(std::ostream &out, const T &value) +{ + using Unsigned = typename PrintfUnsignedValueType::Unsigned; + out << static_cast(value); +} + +template +EnableIfT>() && !IsEnum>(), void> AppendUnsigned(std::ostream &out, T &&value) +{ + out << std::forward(value); +} + +template +EnableIfT::value, void> AppendStringValue(std::ostream &out, const PrintfSpec &spec, T &&value) +{ + AppendString(out, spec, ToStringSlice(std::forward(value))); +} + +template +EnableIfT::value, void> AppendStringValue(std::ostream &out, const PrintfSpec &, T &&value) +{ + out << std::forward(value); +} + +template +EnableIfT>() || IsEnum>(), void> AppendChar(std::ostream &out, const T &value) +{ + out << static_cast(value); +} + +template +EnableIfT>() && !IsEnum>(), void> AppendChar(std::ostream &out, T &&value) +{ + out << std::forward(value); +} + +template +void AppendPointer(std::ostream &out, T *value) +{ + out << static_cast(value); +} + +template +void AppendPointer(std::ostream &out, const T &value) +{ + out << value; +} + +inline void AppendUnknownPrintfConversion(std::ostream &out, const PrintfSpec &spec) +{ + out.put('%'); + if (spec.conversion != '\0') + { + out.put(spec.conversion); + } +} + +template +void AppendFormattedValue(std::ostream &out, const PrintfSpec &spec, T &&value) +{ + const auto oldFlags = out.flags(); + const auto oldPrecision = out.precision(); + const auto oldFill = out.fill(); + + ApplyPrintfSpec(out, spec); + + switch (spec.conversion) + { + case 'd': + case 'i': + AppendSigned(out, std::forward(value)); + break; + case 'u': + AppendUnsigned(out, std::forward(value)); + break; + case 'o': + out.setf(std::ios_base::oct, std::ios_base::basefield); + AppendUnsigned(out, std::forward(value)); + break; + case 'x': + case 'X': + out.setf(std::ios_base::hex, std::ios_base::basefield); + if (spec.conversion == 'X') + { + out.setf(std::ios_base::uppercase); + } + AppendUnsigned(out, std::forward(value)); + break; + case 'f': + case 'F': + out.setf(std::ios_base::fixed, std::ios_base::floatfield); + if (spec.conversion == 'F') + { + out.setf(std::ios_base::uppercase); + } + out << value; + break; + case 'e': + case 'E': + out.setf(std::ios_base::scientific, std::ios_base::floatfield); + if (spec.conversion == 'E') + { + out.setf(std::ios_base::uppercase); + } + out << value; + break; + case 'g': + case 'G': + if (spec.conversion == 'G') + { + out.setf(std::ios_base::uppercase); + } + out << value; + break; + case 's': + AppendStringValue(out, spec, std::forward(value)); + break; + case 'c': + AppendChar(out, std::forward(value)); + break; + case 'p': + AppendPointer(out, std::forward(value)); + break; + default: + AppendUnknownPrintfConversion(out, spec); + break; + } + + out.flags(oldFlags); + out.precision(oldPrecision); + out.fill(oldFill); +} + +inline void FormatImpl(std::ostream &out, const char *fmt) +{ + if (fmt == nullptr) + { + return; + } + + const char *literalBegin = fmt; + while ((fmt = AppendLiteralUntilPercent(out, fmt, literalBegin)) != nullptr) + { + out.put('%'); + if (*fmt == '%') + { + ++fmt; + } + literalBegin = fmt; + } +} + +template +void FormatImpl(std::ostream &out, const char *fmt, Arg &&arg, Args &&...args) +{ + if (fmt == nullptr) + { + return; + } + + const char *literalBegin = fmt; + while ((fmt = AppendLiteralUntilPercent(out, fmt, literalBegin)) != nullptr) + { + if (*fmt == '%') + { + ++fmt; + out.put('%'); + literalBegin = fmt; + continue; + } + + PrintfSpec spec; + const char *next = ParsePrintfSpec(fmt, spec); + AppendFormattedValue(out, spec, std::forward(arg)); + FormatImpl(out, next, std::forward(args)...); + return; + } +} + +template +void FormatTo(char *buffer, std::size_t bufferSize, const char *fmt, Args &&...args) +{ + if (buffer == nullptr || bufferSize == 0) + { + return; + } + + FixedBufferOStream out(buffer, bufferSize); + FormatImpl(out.get(), fmt, std::forward(args)...); + out.get().flush(); + buffer[bufferSize - 1] = '\0'; +} + +inline void AppendVaArg(std::ostream &out, const PrintfSpec &spec, va_list &va) +{ + switch (spec.conversion) + { + case 'd': + case 'i': + switch (spec.length) + { + case PrintfLength::kLL: + AppendFormattedValue(out, spec, va_arg(va, long long)); + break; + case PrintfLength::kL: + AppendFormattedValue(out, spec, va_arg(va, long)); + break; + case PrintfLength::kJ: + AppendFormattedValue(out, spec, va_arg(va, std::intmax_t)); + break; + case PrintfLength::kZ: + AppendFormattedValue(out, spec, va_arg(va, std::ptrdiff_t)); + break; + case PrintfLength::kT: + AppendFormattedValue(out, spec, va_arg(va, std::ptrdiff_t)); + break; + default: + AppendFormattedValue(out, spec, va_arg(va, int)); + break; + } + break; + case 'u': + case 'o': + case 'x': + case 'X': + switch (spec.length) + { + case PrintfLength::kLL: + AppendFormattedValue(out, spec, va_arg(va, unsigned long long)); + break; + case PrintfLength::kL: + AppendFormattedValue(out, spec, va_arg(va, unsigned long)); + break; + case PrintfLength::kJ: + AppendFormattedValue(out, spec, va_arg(va, std::uintmax_t)); + break; + case PrintfLength::kZ: + AppendFormattedValue(out, spec, va_arg(va, std::size_t)); + break; + default: + AppendFormattedValue(out, spec, va_arg(va, unsigned int)); + break; + } + break; + case 'f': + case 'F': + case 'e': + case 'E': + case 'g': + case 'G': + if (spec.length == PrintfLength::kLongDouble) + { + AppendFormattedValue(out, spec, va_arg(va, long double)); + } + else + { + AppendFormattedValue(out, spec, va_arg(va, double)); + } + break; + case 's': + AppendFormattedValue(out, spec, va_arg(va, const char *)); + break; + case 'c': + AppendFormattedValue(out, spec, va_arg(va, int)); + break; + case 'p': + AppendFormattedValue(out, spec, va_arg(va, void *)); + break; + default: + AppendUnknownPrintfConversion(out, spec); + break; + } +} + +inline void VFormatTo(char *buffer, std::size_t bufferSize, const char *fmt, va_list va) +{ + if (buffer == nullptr || bufferSize == 0 || fmt == nullptr) + { + return; + } + + FixedBufferOStream out(buffer, bufferSize); + va_list vaCopy; + va_copy(vaCopy, va); + + const char *literalBegin = fmt; + while ((fmt = AppendLiteralUntilPercent(out.get(), fmt, literalBegin)) != nullptr) + { + if (*fmt == '%') + { + ++fmt; + out.get().put('%'); + literalBegin = fmt; + continue; + } + + PrintfSpec spec; + fmt = ParsePrintfSpec(fmt, spec); + AppendVaArg(out.get(), spec, vaCopy); + literalBegin = fmt; + } + + va_end(vaCopy); + out.get().flush(); + buffer[bufferSize - 1] = '\0'; +} + +}} // namespace nvcv::detail + +#endif // NVCV_DETAIL_FORMAT_HPP diff --git a/src/nvcv/src/include/nvcv/detail/ImageBatchDataImpl.hpp b/src/nvcv/src/include/nvcv/detail/ImageBatchDataImpl.hpp index 0b90d2e44..292a59fc2 100644 --- a/src/nvcv/src/include/nvcv/detail/ImageBatchDataImpl.hpp +++ b/src/nvcv/src/include/nvcv/detail/ImageBatchDataImpl.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -56,11 +56,11 @@ Optional ImageBatchData::cast() const if (Derived::IsCompatibleKind(m_data.bufferType)) { - return Derived{m_data}; + return Optional{Derived{m_data}}; } else { - return NullOpt; + return Optional{NullOpt}; } } diff --git a/src/nvcv/src/include/nvcv/detail/ImageBatchImpl.hpp b/src/nvcv/src/include/nvcv/detail/ImageBatchImpl.hpp index 10a952811..202e45cf6 100644 --- a/src/nvcv/src/include/nvcv/detail/ImageBatchImpl.hpp +++ b/src/nvcv/src/include/nvcv/detail/ImageBatchImpl.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,6 +22,8 @@ # error "You must not include this header directly" #endif +#include "Callback.hpp" + namespace nvcv { // ImageBatch implementation --------------------- @@ -69,14 +71,14 @@ Optional ImageBatch::exportData(CUstream stream) const return exportData(stream).cast(); } -inline void ImageBatch::setUserPointer(void *ptr) +inline void ImageBatch::setUserPointer(NVCVUserPointer ptr) // NOSONAR: mutates state through the wrapped C handle. { detail::CheckThrow(nvcvImageBatchSetUserPointer(this->handle(), ptr)); } -inline void *ImageBatch::userPointer() const +inline NVCVUserPointer ImageBatch::userPointer() const { - void *ptr; + NVCVUserPointer ptr; detail::CheckThrow(nvcvImageBatchGetUserPointer(this->handle(), &ptr)); return ptr; } @@ -134,7 +136,7 @@ inline ImageBatchVarShape &ImageBatchVarShape::operator=(ImageBatch &&batch) } template -void ImageBatchVarShape::pushBack(IT itBeg, IT itEnd) +void ImageBatchVarShape::pushBack(IT itBeg, IT itEnd) // NOSONAR: accepts input iterators and consumes the range once. { auto cb = [itBeg, &itEnd]() mutable { @@ -151,24 +153,28 @@ void ImageBatchVarShape::pushBack(IT itBeg, IT itEnd) pushBack(cb); } -inline void ImageBatchVarShape::pushBack(const Image &img) +inline void ImageBatchVarShape::pushBack(const Image &img) // NOSONAR: mutates state through the wrapped C handle. { NVCVImageHandle himg = img.handle(); detail::CheckThrow(nvcvImageBatchVarShapePushImages(this->handle(), &himg, 1)); } template -inline void ImageBatchVarShape::pushBack(F &&cb) +inline void ImageBatchVarShape::pushBack(F &&cb) // NOSONAR: mutates state through the wrapped C handle. { - auto *pcb = &cb; - auto ccb = [](void *ctx) -> NVCVImageHandle + F &&forwardedCb = std::forward(cb); + + auto cbPushImage = [&forwardedCb]() -> NVCVImageHandle { - return detail::GetImageHandleForPushBack((*decltype(pcb)(ctx))()); + return detail::GetImageHandleForPushBack(forwardedCb()); }; - detail::CheckThrow(nvcvImageBatchVarShapePushImagesCallback(this->handle(), ccb, pcb)); + + using PushBackCallback = Callback>; + PushBackCallback ccb{cbPushImage}; + detail::CheckThrow(nvcvImageBatchVarShapePushImagesCallback(this->handle(), ccb.targetFunc(), ccb.targetHandle())); } -inline void ImageBatchVarShape::popBack(int32_t imgCount) +inline void ImageBatchVarShape::popBack(int32_t imgCount) // NOSONAR: mutates state through the wrapped C handle. { detail::CheckThrow(nvcvImageBatchVarShapePopImages(this->handle(), imgCount)); } @@ -176,11 +182,11 @@ inline void ImageBatchVarShape::popBack(int32_t imgCount) inline Image ImageBatchVarShape::operator[](ptrdiff_t n) const { NVCVImageHandle himg; - detail::CheckThrow(nvcvImageBatchVarShapeGetImages(this->handle(), n, &himg, 1)); + detail::CheckThrow(nvcvImageBatchVarShapeGetImages(this->handle(), static_cast(n), &himg, 1)); return Image(std::move(himg)); } -inline void ImageBatchVarShape::clear() +inline void ImageBatchVarShape::clear() // NOSONAR: mutates state through the wrapped C handle. { detail::CheckThrow(nvcvImageBatchVarShapeClear(this->handle())); } diff --git a/src/nvcv/src/include/nvcv/detail/ImageDataImpl.hpp b/src/nvcv/src/include/nvcv/detail/ImageDataImpl.hpp index 512367438..3aa0b40f1 100644 --- a/src/nvcv/src/include/nvcv/detail/ImageDataImpl.hpp +++ b/src/nvcv/src/include/nvcv/detail/ImageDataImpl.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -57,11 +57,11 @@ inline Optional ImageData::cast() const if (Derived::IsCompatibleKind(m_data.bufferType)) { - return Derived{m_data}; + return Optional{Derived{m_data}}; } else { - return NullOpt; + return Optional{NullOpt}; } } @@ -137,7 +137,7 @@ inline ImageDataCudaArray::ImageDataCudaArray(ImageFormat format, const Buffer & { NVCVImageData &data = this->cdata(); - data.format = format; + data.format = static_cast(format); data.bufferType = NVCV_IMAGE_BUFFER_CUDA_ARRAY; data.buffer.cudaarray = buffer; } @@ -148,7 +148,7 @@ inline ImageDataStridedCuda::ImageDataStridedCuda(ImageFormat format, const Buff { NVCVImageData &data = this->cdata(); - data.format = format; + data.format = static_cast(format); data.bufferType = NVCV_IMAGE_BUFFER_STRIDED_CUDA; data.buffer.strided = buffer; } @@ -169,7 +169,7 @@ inline ImageDataStridedHost::ImageDataStridedHost(ImageFormat format, const Buff { NVCVImageData &data = this->cdata(); - data.format = format; + data.format = static_cast(format); data.bufferType = NVCV_IMAGE_BUFFER_STRIDED_HOST; data.buffer.strided = buffer; } diff --git a/src/nvcv/src/include/nvcv/detail/ImageImpl.hpp b/src/nvcv/src/include/nvcv/detail/ImageImpl.hpp index babd6ebda..437fb41f6 100644 --- a/src/nvcv/src/include/nvcv/detail/ImageImpl.hpp +++ b/src/nvcv/src/include/nvcv/detail/ImageImpl.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -60,14 +60,14 @@ inline Optional Image::exportData() const return exportData().cast(); } -inline void Image::setUserPointer(void *ptr) +inline void Image::setUserPointer(NVCVUserPointer ptr) // NOSONAR: mutates state through the wrapped C handle. { detail::CheckThrow(nvcvImageSetUserPointer(this->handle(), ptr)); } -inline void *Image::userPointer() const +inline NVCVUserPointer Image::userPointer() const { - void *ptr; + NVCVUserPointer ptr; detail::CheckThrow(nvcvImageGetUserPointer(this->handle(), &ptr)); return ptr; } @@ -75,7 +75,8 @@ inline void *Image::userPointer() const inline auto Image::CalcRequirements(const Size2D &size, ImageFormat fmt, const MemAlignment &bufAlign) -> Requirements { Requirements reqs; - detail::CheckThrow(nvcvImageCalcRequirements(size.w, size.h, fmt, bufAlign.baseAddr(), bufAlign.rowAddr(), &reqs)); + detail::CheckThrow(nvcvImageCalcRequirements(size.w, size.h, static_cast(fmt), bufAlign.baseAddr(), + bufAlign.rowAddr(), &reqs)); return reqs; } @@ -98,7 +99,7 @@ inline Image ImageWrapData(const ImageData &data, ImageDataCleanupCallback &&cle NVCVImageHandle handle = nullptr; detail::CheckThrow( nvcvImageWrapDataConstruct(&data.cdata(), cleanup.targetFunc(), cleanup.targetHandle(), &handle)); - (void)cleanup.release(); // The cleanup callback is now owned by the image object. + (void)std::move(cleanup).release(); // The cleanup callback is now owned by the image object. return Image(std::move(handle)); } diff --git a/src/nvcv/src/include/nvcv/detail/InPlace.hpp b/src/nvcv/src/include/nvcv/detail/InPlace.hpp index 4362d7cdb..923d2931f 100644 --- a/src/nvcv/src/include/nvcv/detail/InPlace.hpp +++ b/src/nvcv/src/include/nvcv/detail/InPlace.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/src/nvcv/src/include/nvcv/detail/IndexSequence.hpp b/src/nvcv/src/include/nvcv/detail/IndexSequence.hpp index 22bf79d05..c717c6619 100644 --- a/src/nvcv/src/include/nvcv/detail/IndexSequence.hpp +++ b/src/nvcv/src/include/nvcv/detail/IndexSequence.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/src/nvcv/src/include/nvcv/detail/TensorBatchImpl.hpp b/src/nvcv/src/include/nvcv/detail/TensorBatchImpl.hpp index 2be99edf0..42b747179 100644 --- a/src/nvcv/src/include/nvcv/detail/TensorBatchImpl.hpp +++ b/src/nvcv/src/include/nvcv/detail/TensorBatchImpl.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -94,7 +94,7 @@ inline Allocator TensorBatch::alloc() const } template -inline void TensorBatch::pushBack(It begin, It end) +inline void TensorBatch::pushBack(It begin, It end) // NOSONAR: accepts input iterators and consumes the range once. { std::vector handles; handles.reserve(capacity() - numTensors()); @@ -102,45 +102,45 @@ inline void TensorBatch::pushBack(It begin, It end) { handles.push_back(it->handle()); } - detail::CheckThrow(nvcvTensorBatchPushTensors(handle(), handles.data(), handles.size())); + detail::CheckThrow(nvcvTensorBatchPushTensors(handle(), handles.data(), static_cast(handles.size()))); } -inline void TensorBatch::pushBack(const Tensor &tensor) +inline void TensorBatch::pushBack(const Tensor &tensor) // NOSONAR: mutates state through the wrapped C handle. { auto hTensor = tensor.handle(); detail::CheckThrow(nvcvTensorBatchPushTensors(handle(), &hTensor, 1)); } -inline void TensorBatch::popTensors(int32_t numTensors) +inline void TensorBatch::popTensors(int32_t numTensors) // NOSONAR: mutates state through the wrapped C handle. { detail::CheckThrow(nvcvTensorBatchPopTensors(handle(), numTensors)); } -inline void TensorBatch::popTensor() +inline void TensorBatch::popTensor() // NOSONAR: mutates state through the wrapped C handle. { detail::CheckThrow(nvcvTensorBatchPopTensors(handle(), 1)); } -inline TensorBatchData TensorBatch::exportData(CUstream stream) +inline TensorBatchData TensorBatch::exportData(CUstream stream) const { NVCVTensorBatchData output = {}; detail::CheckThrow(nvcvTensorBatchExportData(handle(), stream, &output)); return TensorBatchData(output); } -inline void TensorBatch::clear() +inline void TensorBatch::clear() // NOSONAR: mutates state through the wrapped C handle. { detail::CheckThrow(nvcvTensorBatchClear(handle())); } -inline void TensorBatch::setUserPointer(void *ptr) +inline void TensorBatch::setUserPointer(NVCVUserPointer ptr) // NOSONAR: mutates state through the wrapped C handle. { detail::CheckThrow(nvcvTensorBatchSetUserPointer(handle(), ptr)); } -inline void *TensorBatch::getUserPointer() const +inline NVCVUserPointer TensorBatch::getUserPointer() const { - void *outPtr = nullptr; + NVCVUserPointer outPtr = nullptr; detail::CheckThrow(nvcvTensorBatchGetUserPointer(handle(), &outPtr)); return outPtr; } @@ -152,7 +152,7 @@ inline Tensor TensorBatch::operator[](int32_t idx) const return Tensor(std::move(hTensor)); } -inline void TensorBatch::setTensor(int32_t idx, const Tensor &tensor) +inline void TensorBatch::setTensor(int32_t idx, const Tensor &tensor) // NOSONAR: mutates through the wrapped C handle. { auto hTensor = tensor.handle(); detail::CheckThrow(nvcvTensorBatchSetTensors(handle(), idx, &hTensor, 1)); diff --git a/src/nvcv/src/include/nvcv/detail/TensorDataImpl.hpp b/src/nvcv/src/include/nvcv/detail/TensorDataImpl.hpp index ff9139e4c..c08856417 100644 --- a/src/nvcv/src/include/nvcv/detail/TensorDataImpl.hpp +++ b/src/nvcv/src/include/nvcv/detail/TensorDataImpl.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -43,14 +43,14 @@ inline const TensorShape &TensorData::shape() const & if (!m_cacheShape) { const NVCVTensorData &data = this->cdata(); - // coverity[overrun-buffer-val] - data.rank validated below, cannot exceed NVCV_TENSOR_MAX_RANK + // coverity[overrun-buffer-val] - data.rank validated below, cannot exceed NVCV_TENSOR_MAX_RANK // NOSONAR if (data.rank < 0 || data.rank > NVCV_TENSOR_MAX_RANK) { throw Exception(Status::ERROR_INVALID_ARGUMENT, "Tensor rank %d is out of valid range [0,%d]", data.rank, NVCV_TENSOR_MAX_RANK); } - // coverity[overrun-buffer-val] - data.rank validated above, cannot exceed NVCV_TENSOR_MAX_RANK - m_cacheShape.emplace(data.shape, data.rank, data.layout); + // coverity[overrun-buffer-val] - data.rank validated above, cannot exceed NVCV_TENSOR_MAX_RANK // NOSONAR + m_cacheShape.emplace(data.shape, data.rank, TensorLayout{data.layout}); } return *m_cacheShape; @@ -106,11 +106,11 @@ inline Optional TensorData::cast() const if (IsCompatible()) { - return Derived(m_data); + return Optional{Derived{m_data}}; } else { - return NullOpt; + return Optional{NullOpt}; } } @@ -140,10 +140,10 @@ inline TensorDataStridedCuda::TensorDataStridedCuda(const TensorShape &tshape, c { NVCVTensorData &data = this->data(); - std::copy(tshape.shape().begin(), tshape.shape().end(), data.shape); + std::copy(tshape.shape().begin(), tshape.shape().end(), data.shape); // NOSONAR: std::ranges::copy is C++20. data.rank = tshape.rank(); - data.dtype = dtype; - data.layout = tshape.layout(); + data.dtype = static_cast(dtype); + data.layout = static_cast(tshape.layout()); data.bufferType = NVCV_TENSOR_BUFFER_STRIDED_CUDA; data.buffer.strided = buffer; diff --git a/src/nvcv/src/include/nvcv/detail/TensorImpl.hpp b/src/nvcv/src/include/nvcv/detail/TensorImpl.hpp index 804a9b03a..5531c9341 100644 --- a/src/nvcv/src/include/nvcv/detail/TensorImpl.hpp +++ b/src/nvcv/src/include/nvcv/detail/TensorImpl.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -38,7 +38,7 @@ inline TensorShape Tensor::shape() const TensorShape::ShapeType shape(rank); detail::CheckThrow(nvcvTensorGetShape(htensor, &rank, shape.begin())); - return {shape, layout}; + return TensorShape{shape, TensorLayout{layout}}; } inline int Tensor::rank() const @@ -69,7 +69,7 @@ inline TensorData Tensor::exportData() const throw Exception(Status::ERROR_INVALID_OPERATION, "The tensor handle is null."); NVCVTensorData data; - detail::CheckThrow(nvcvTensorExportData(this->handle(), &data)); + detail::CheckThrow(nvcvTensorExportData(h, &data)); if (data.bufferType != NVCV_TENSOR_BUFFER_STRIDED_CUDA) { @@ -79,23 +79,23 @@ inline TensorData Tensor::exportData() const return TensorData(data); } -inline void Tensor::setUserPointer(void *ptr) +inline void Tensor::setUserPointer(NVCVUserPointer ptr) // NOSONAR: mutates state through the wrapped C handle. { detail::CheckThrow(nvcvTensorSetUserPointer(this->handle(), ptr)); } -inline void *Tensor::userPointer() const +inline NVCVUserPointer Tensor::userPointer() const { - void *ptr; + NVCVUserPointer ptr; detail::CheckThrow(nvcvTensorGetUserPointer(this->handle(), &ptr)); return ptr; } -inline Tensor Tensor::reshape(const TensorShape &new_shape) +inline Tensor Tensor::reshape(const TensorShape &new_shape) const { NVCVTensorHandle out_handle; - detail::CheckThrow( - nvcvTensorReshape(this->handle(), new_shape.rank(), &new_shape.shape()[0], new_shape.layout(), &out_handle)); + detail::CheckThrow(nvcvTensorReshape(this->handle(), new_shape.rank(), &new_shape.shape()[0], + static_cast(new_shape.layout()), &out_handle)); Tensor out_tensor(std::move(out_handle)); return out_tensor; } @@ -104,7 +104,7 @@ inline auto Tensor::CalcRequirements(const TensorShape &shape, DataType dtype, c -> Requirements { Requirements reqs; - detail::CheckThrow(nvcvTensorCalcRequirements(shape.size(), &shape[0], dtype, + detail::CheckThrow(nvcvTensorCalcRequirements(shape.size(), &shape[0], static_cast(dtype), static_cast(shape.layout()), bufAlign.baseAddr(), bufAlign.rowAddr(), &reqs)); return reqs; @@ -114,7 +114,8 @@ inline auto Tensor::CalcRequirements(int numImages, Size2D imgSize, ImageFormat -> Requirements { Requirements reqs; - detail::CheckThrow(nvcvTensorCalcRequirementsForImages(numImages, imgSize.w, imgSize.h, fmt, bufAlign.baseAddr(), + detail::CheckThrow(nvcvTensorCalcRequirementsForImages(numImages, imgSize.w, imgSize.h, + static_cast(fmt), bufAlign.baseAddr(), bufAlign.rowAddr(), &reqs)); return reqs; } @@ -144,7 +145,7 @@ inline Tensor TensorWrapData(const TensorData &data, TensorDataCleanupCallback & NVCVTensorHandle handle; detail::CheckThrow( nvcvTensorWrapDataConstruct(&data.cdata(), cleanup.targetFunc(), cleanup.targetHandle(), &handle)); - cleanup.release(); // already owned by the tensor + std::move(cleanup).release(); // already owned by the tensor return Tensor(std::move(handle)); } diff --git a/src/nvcv/src/include/nvcv/detail/TypeTraits.hpp b/src/nvcv/src/include/nvcv/detail/TypeTraits.hpp index f8bb6fcf9..5cede434f 100644 --- a/src/nvcv/src/include/nvcv/detail/TypeTraits.hpp +++ b/src/nvcv/src/include/nvcv/detail/TypeTraits.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -26,6 +26,16 @@ namespace nvcv { namespace detail { template using EnableIf_t = typename std::enable_if::type; +template +constexpr bool IsSameType() +{ +#if __cplusplus >= 201703L + return std::is_same_v; +#else + return std::is_same::value; +#endif +} + template using Conditional_t = typename std::conditional::type; diff --git a/src/nvcv/src/include/nvcv/detail/UniqueObj.hpp b/src/nvcv/src/include/nvcv/detail/UniqueObj.hpp new file mode 100644 index 000000000..b10996210 --- /dev/null +++ b/src/nvcv/src/include/nvcv/detail/UniqueObj.hpp @@ -0,0 +1,64 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#ifndef NVCV_DETAIL_UNIQUE_OBJ_HPP +#define NVCV_DETAIL_UNIQUE_OBJ_HPP + +#include +#include + +namespace nvcv { namespace detail { + +template +struct UniqueObjDeleter +{ + void operator()(T *ptr) const noexcept + { + if (ptr != nullptr) + { + std::allocator alloc; + std::allocator_traits>::destroy(alloc, ptr); + std::allocator_traits>::deallocate(alloc, ptr, 1); + } + } +}; + +template +using UniqueObj = std::unique_ptr>; + +template +UniqueObj MakeUniqueObj(Args &&...args) +{ + std::allocator alloc; + T *ptr = std::allocator_traits>::allocate(alloc, 1); + + try + { + std::allocator_traits>::construct(alloc, ptr, std::forward(args)...); + } + catch (...) + { + std::allocator_traits>::deallocate(alloc, ptr, 1); + throw; + } + + return UniqueObj(ptr); +} + +}} // namespace nvcv::detail + +#endif // NVCV_DETAIL_UNIQUE_OBJ_HPP diff --git a/src/nvcv/src/priv/Array.cpp b/src/nvcv/src/priv/Array.cpp index 6940aa3f8..e6097989a 100644 --- a/src/nvcv/src/priv/Array.cpp +++ b/src/nvcv/src/priv/Array.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -89,7 +89,7 @@ NVCVArrayRequirements Array::CalcRequirements(int64_t capacity, const DataType & throw Exception(NVCV_ERROR_INVALID_ARGUMENT) << "Unknown Resource type " << target; } - align = std::lcm(align, util::RoundUpNextPowerOfTwo(dtype.strideBytes())); + align = static_cast(std::lcm(align, util::RoundUpNextPowerOfTwo(dtype.strideBytes()))); } else { @@ -136,15 +136,16 @@ Array::Array(NVCVArrayRequirements reqs, IAllocator &alloc, NVCVResourceType tar { case NVCV_RESOURCE_MEM_CUDA: bufSize = CalcTotalSizeBytes(m_reqs.mem.cudaMem); - m_memBuffer = m_alloc->allocCudaMem(bufSize, m_reqs.alignBytes); + m_memBuffer = static_cast(static_cast(m_alloc->allocCudaMem(bufSize, m_reqs.alignBytes))); break; case NVCV_RESOURCE_MEM_HOST: bufSize = CalcTotalSizeBytes(m_reqs.mem.hostMem); - m_memBuffer = m_alloc->allocHostMem(bufSize, m_reqs.alignBytes); + m_memBuffer = static_cast(static_cast(m_alloc->allocHostMem(bufSize, m_reqs.alignBytes))); break; case NVCV_RESOURCE_MEM_HOST_PINNED: - bufSize = CalcTotalSizeBytes(m_reqs.mem.hostPinnedMem); - m_memBuffer = m_alloc->allocHostPinnedMem(bufSize, m_reqs.alignBytes); + bufSize = CalcTotalSizeBytes(m_reqs.mem.hostPinnedMem); + m_memBuffer + = static_cast(static_cast(m_alloc->allocHostPinnedMem(bufSize, m_reqs.alignBytes))); break; default: throw Exception(NVCV_ERROR_INVALID_ARGUMENT) << "Unknown Resource type " << m_target; @@ -162,13 +163,16 @@ Array::~Array() switch (m_target) { case NVCV_RESOURCE_MEM_CUDA: - m_alloc->freeCudaMem(m_memBuffer, CalcTotalSizeBytes(m_reqs.mem.cudaMem), m_reqs.alignBytes); + m_alloc->freeCudaMem(static_cast(static_cast(m_memBuffer)), + CalcTotalSizeBytes(m_reqs.mem.cudaMem), m_reqs.alignBytes); break; case NVCV_RESOURCE_MEM_HOST: - m_alloc->freeHostMem(m_memBuffer, CalcTotalSizeBytes(m_reqs.mem.hostMem), m_reqs.alignBytes); + m_alloc->freeHostMem(static_cast(static_cast(m_memBuffer)), + CalcTotalSizeBytes(m_reqs.mem.hostMem), m_reqs.alignBytes); break; case NVCV_RESOURCE_MEM_HOST_PINNED: - m_alloc->freeHostPinnedMem(m_memBuffer, CalcTotalSizeBytes(m_reqs.mem.hostPinnedMem), m_reqs.alignBytes); + m_alloc->freeHostPinnedMem(static_cast(static_cast(m_memBuffer)), + CalcTotalSizeBytes(m_reqs.mem.hostPinnedMem), m_reqs.alignBytes); break; default: break; @@ -217,7 +221,7 @@ void Array::exportData(NVCVArrayData &data) const auto &buf = data.buffer.strided; { buf.stride = m_reqs.stride; - buf.basePtr = reinterpret_cast(m_memBuffer); + buf.basePtr = m_memBuffer; } } diff --git a/src/nvcv/src/priv/Array.hpp b/src/nvcv/src/priv/Array.hpp index b868e0d41..9172560a0 100644 --- a/src/nvcv/src/priv/Array.hpp +++ b/src/nvcv/src/priv/Array.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -24,13 +24,15 @@ #include +#include + namespace nvcv::priv { class Array final : public CoreObjectBase { public: explicit Array(NVCVArrayRequirements reqs, IAllocator &alloc, NVCVResourceType target); - ~Array(); + ~Array() override; static NVCVArrayRequirements CalcRequirements(int64_t capacity, const DataType &dtype, int32_t alignment, NVCVResourceType target = NVCV_RESOURCE_MEM_CUDA); @@ -55,7 +57,7 @@ class Array final : public CoreObjectBase NVCVResourceType m_target; NVCVArrayData m_data; - void *m_memBuffer; + NVCVByte *m_memBuffer; }; } // namespace nvcv::priv diff --git a/src/nvcv/src/priv/ArrayWrapData.cpp b/src/nvcv/src/priv/ArrayWrapData.cpp index bbc36383f..9ff020245 100644 --- a/src/nvcv/src/priv/ArrayWrapData.cpp +++ b/src/nvcv/src/priv/ArrayWrapData.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -34,8 +34,7 @@ static NVCVResourceType ValidateArrayBuffer(const NVCVArrayData &data) { NVCVResourceType resource = NVCV_RESOURCE_MEM_CUDA; - const auto &buffer = data.buffer.strided; - if (buffer.basePtr == nullptr) + if (const auto &buffer = data.buffer.strided; buffer.basePtr == nullptr) { throw Exception(NVCV_ERROR_INVALID_ARGUMENT) << "Memory buffer must not be NULL"; } @@ -68,7 +67,7 @@ static NVCVResourceType ValidateArrayBuffer(const NVCVArrayData &data) return resource; } -ArrayWrapData::ArrayWrapData(const NVCVArrayData &data, NVCVArrayDataCleanupFunc cleanup, void *ctxCleanup) +ArrayWrapData::ArrayWrapData(const NVCVArrayData &data, NVCVArrayDataCleanupFunc cleanup, NVCVUserPointer ctxCleanup) : m_data{data} , m_cleanup{cleanup} , m_ctxCleanup{ctxCleanup} @@ -106,7 +105,7 @@ DataType ArrayWrapData::dtype() const SharedCoreObj ArrayWrapData::alloc() const { - return GetDefaultAllocator(); + return SharedCoreObj{GetDefaultAllocator()}; } NVCVResourceType ArrayWrapData::target() const diff --git a/src/nvcv/src/priv/ArrayWrapData.hpp b/src/nvcv/src/priv/ArrayWrapData.hpp index 2489da680..1d45f36f5 100644 --- a/src/nvcv/src/priv/ArrayWrapData.hpp +++ b/src/nvcv/src/priv/ArrayWrapData.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -27,8 +27,8 @@ namespace nvcv::priv { class ArrayWrapData final : public CoreObjectBase { public: - explicit ArrayWrapData(const NVCVArrayData &data, NVCVArrayDataCleanupFunc cleanup, void *ctxCleanup); - ~ArrayWrapData(); + explicit ArrayWrapData(const NVCVArrayData &data, NVCVArrayDataCleanupFunc cleanup, NVCVUserPointer ctxCleanup); + ~ArrayWrapData() override; int32_t rank() const override; int64_t capacity() const override; @@ -49,7 +49,7 @@ class ArrayWrapData final : public CoreObjectBase NVCVResourceType m_target; NVCVArrayDataCleanupFunc m_cleanup; - void *m_ctxCleanup; + NVCVUserPointer m_ctxCleanup; }; } // namespace nvcv::priv diff --git a/src/nvcv/src/priv/ColorSpec.cpp b/src/nvcv/src/priv/ColorSpec.cpp index 0316e9c9d..35adec555 100644 --- a/src/nvcv/src/priv/ColorSpec.cpp +++ b/src/nvcv/src/priv/ColorSpec.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -24,7 +24,9 @@ #include #include +#include #include +#include namespace nvcv::priv { @@ -41,7 +43,7 @@ ColorSpec::operator NVCVColorSpec() const noexcept NVCVColorSpace ColorSpec::colorSpace() const noexcept { - int32_t val = ExtractBitfield(m_cspec, 0, 3); + auto val = static_cast(ExtractBitfield(m_cspec, 0, 3)); return (NVCVColorSpace)val; } @@ -113,6 +115,8 @@ NVCVChromaSubsampling MakeNVCVChromaSubsampling(int samplesHoriz, int samplesVer return NVCV_CSS_422R; case 1: return NVCV_CSS_411R; + default: + break; } break; @@ -125,6 +129,8 @@ NVCVChromaSubsampling MakeNVCVChromaSubsampling(int samplesHoriz, int samplesVer return NVCV_CSS_420; case 1: return NVCV_CSS_410R; + default: + break; } break; @@ -137,8 +143,12 @@ NVCVChromaSubsampling MakeNVCVChromaSubsampling(int samplesHoriz, int samplesVer return NVCV_CSS_410; case 4: return NVCV_CSS_411; + default: + break; } break; + default: + break; } throw Exception(NVCV_ERROR_INVALID_ARGUMENT) @@ -173,6 +183,8 @@ std::pair GetChromaSamples(NVCVChromaSubsampling css) case NVCV_CSS_410: return {1, 2}; + default: + break; } throw Exception(NVCV_ERROR_INVALID_ARGUMENT) << "Invalid chroma subsampling: " << css; @@ -180,7 +192,7 @@ std::pair GetChromaSamples(NVCVChromaSubsampling css) std::ostream &operator<<(std::ostream &out, ColorSpec cspec) { - switch (cspec) + switch (static_cast(cspec)) { #define ENUM_CASE(X) \ case X: \ @@ -240,8 +252,8 @@ const char *GetName(NVCVColorSpec cspec) { priv::CoreTLS &tls = priv::GetCoreTLS(); - char *buffer = tls.bufColorSpecName; - constexpr int bufSize = sizeof(tls.bufColorSpecName); + char *buffer = tls.bufColorSpecName.data(); + auto bufSize = static_cast(tls.bufColorSpecName.size()); try { @@ -255,11 +267,39 @@ const char *GetName(NVCVColorSpec cspec) util::ReplaceAllInline(buffer, bufSize, "NVCV_COLOR_RANGE_", "RANGE_"); util::ReplaceAllInline(buffer, bufSize, "NVCV_COLOR_SPACE_", "SPACE_"); } - catch (std::exception &e) + catch (const std::invalid_argument &e) + { + snprintf(buffer, bufSize, "%s", e.what()); + } + catch (const std::domain_error &e) + { + snprintf(buffer, bufSize, "%s", e.what()); + } + catch (const std::length_error &e) + { + snprintf(buffer, bufSize, "%s", e.what()); + } + catch (const std::out_of_range &e) + { + snprintf(buffer, bufSize, "%s", e.what()); + } + catch (const std::range_error &e) + { + snprintf(buffer, bufSize, "%s", e.what()); + } + catch (const std::overflow_error &e) + { + snprintf(buffer, bufSize, "%s", e.what()); + } + catch (const std::underflow_error &e) + { + snprintf(buffer, bufSize, "%s", e.what()); + } + catch (const std::bad_alloc &e) { snprintf(buffer, bufSize, "%s", e.what()); } - catch (...) + catch (...) // NOSONAR: API boundary converts any non-standard exception to a fallback message. { snprintf(buffer, bufSize, "Unexpected error retrieving NVCVColorSpec string representation"); } @@ -287,10 +327,10 @@ const char *GetName(NVCVColorModel colorModel) priv::CoreTLS &tls = priv::GetCoreTLS(); - util::BufferOStream(tls.bufColorModelName, sizeof(tls.bufColorModelName)) + util::BufferOStream(tls.bufColorModelName.data(), static_cast(tls.bufColorModelName.size())) << "NVCVColorModel(" << (int)colorModel << ")"; - return tls.bufColorModelName; + return tls.bufColorModelName.data(); } const char *GetName(NVCVChromaLocation loc) @@ -309,9 +349,9 @@ const char *GetName(NVCVChromaLocation loc) priv::CoreTLS &tls = priv::GetCoreTLS(); - util::BufferOStream(tls.bufChromaLocationName, sizeof(tls.bufChromaLocationName)) + util::BufferOStream(tls.bufChromaLocationName.data(), static_cast(tls.bufChromaLocationName.size())) << "NVCVChromaLocation(" << (int)loc << ")"; - return tls.bufChromaLocationName; + return tls.bufChromaLocationName.data(); } const char *GetName(NVCVRawPattern raw) @@ -341,9 +381,10 @@ const char *GetName(NVCVRawPattern raw) } priv::CoreTLS &tls = priv::GetCoreTLS(); - util::BufferOStream(tls.bufRawPatternName, sizeof(tls.bufRawPatternName)) << "NVCVRawPattern(" << (int)raw << ")"; + util::BufferOStream(tls.bufRawPatternName.data(), static_cast(tls.bufRawPatternName.size())) + << "NVCVRawPattern(" << (int)raw << ")"; - return tls.bufRawPatternName; + return tls.bufRawPatternName.data(); } const char *GetName(NVCVColorSpace color_space) @@ -361,9 +402,9 @@ const char *GetName(NVCVColorSpace color_space) } priv::CoreTLS &tls = priv::GetCoreTLS(); - util::BufferOStream(tls.bufColorSpaceName, sizeof(tls.bufColorSpaceName)) + util::BufferOStream(tls.bufColorSpaceName.data(), static_cast(tls.bufColorSpaceName.size())) << "NVCVColorSpace(" << (int)color_space << ")"; - return tls.bufColorSpaceName; + return tls.bufColorSpaceName.data(); } const char *GetName(NVCVWhitePoint whitePoint) @@ -380,9 +421,9 @@ const char *GetName(NVCVWhitePoint whitePoint) } priv::CoreTLS &tls = priv::GetCoreTLS(); - util::BufferOStream(tls.bufWhitePointName, sizeof(tls.bufWhitePointName)) + util::BufferOStream(tls.bufWhitePointName.data(), static_cast(tls.bufWhitePointName.size())) << "NVCVWhitePoint(" << (int)whitePoint << ")"; - return tls.bufWhitePointName; + return tls.bufWhitePointName.data(); } const char *GetName(NVCVColorTransferFunction xferFunc) @@ -403,9 +444,10 @@ const char *GetName(NVCVColorTransferFunction xferFunc) } priv::CoreTLS &tls = priv::GetCoreTLS(); - util::BufferOStream(tls.bufColorTransferFunctionName, sizeof(tls.bufColorTransferFunctionName)) + util::BufferOStream(tls.bufColorTransferFunctionName.data(), + static_cast(tls.bufColorTransferFunctionName.size())) << "NVCVColorTransferFunction(" << (int)xferFunc << ")"; - return tls.bufColorTransferFunctionName; + return tls.bufColorTransferFunctionName.data(); } const char *GetName(NVCVColorRange range) @@ -421,8 +463,9 @@ const char *GetName(NVCVColorRange range) } priv::CoreTLS &tls = priv::GetCoreTLS(); - util::BufferOStream(tls.bufColorRangeName, sizeof(tls.bufColorRangeName)) << "NVCVColorRange(" << (int)range << ")"; - return tls.bufColorRangeName; + util::BufferOStream(tls.bufColorRangeName.data(), static_cast(tls.bufColorRangeName.size())) + << "NVCVColorRange(" << (int)range << ")"; + return tls.bufColorRangeName.data(); } const char *GetName(NVCVYCbCrEncoding encoding) @@ -442,9 +485,9 @@ const char *GetName(NVCVYCbCrEncoding encoding) } priv::CoreTLS &tls = priv::GetCoreTLS(); - util::BufferOStream(tls.bufYCbCrEncodingName, sizeof(tls.bufYCbCrEncodingName)) + util::BufferOStream(tls.bufYCbCrEncodingName.data(), static_cast(tls.bufYCbCrEncodingName.size())) << "NVCVYCbCrEncoding(" << (int)encoding << ")"; - return tls.bufYCbCrEncodingName; + return tls.bufYCbCrEncodingName.data(); } const char *GetName(NVCVChromaSubsampling chromaSub) @@ -452,7 +495,8 @@ const char *GetName(NVCVChromaSubsampling chromaSub) priv::CoreTLS &tls = priv::GetCoreTLS(); { - util::BufferOStream ss(tls.bufChromaSubsamplingName, sizeof(tls.bufChromaSubsamplingName)); + util::BufferOStream ss(tls.bufChromaSubsamplingName.data(), + static_cast(tls.bufChromaSubsamplingName.size())); bool ok = false; @@ -479,7 +523,7 @@ const char *GetName(NVCVChromaSubsampling chromaSub) ss << "NVCVChromaSubsampling(" << (int)chromaSub << ")"; } } - return tls.bufChromaSubsamplingName; + return tls.bufChromaSubsamplingName.data(); } } // namespace nvcv::priv diff --git a/src/nvcv/src/priv/ColorSpec.hpp b/src/nvcv/src/priv/ColorSpec.hpp index cbe45b4ff..60e7b2e70 100644 --- a/src/nvcv/src/priv/ColorSpec.hpp +++ b/src/nvcv/src/priv/ColorSpec.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -26,13 +26,14 @@ namespace nvcv::priv { struct ChromaLoc { - NVCVChromaLocation horiz, vert; + NVCVChromaLocation horiz; + NVCVChromaLocation vert; }; class ColorSpec { public: - constexpr ColorSpec(NVCVColorSpec cspec) + explicit constexpr ColorSpec(NVCVColorSpec cspec) : m_cspec{cspec} { } @@ -40,7 +41,7 @@ class ColorSpec ColorSpec(NVCVColorSpace cspace, NVCVYCbCrEncoding encoding, NVCVColorTransferFunction xferfunc, NVCVColorRange range, const ChromaLoc &loc) noexcept; - operator NVCVColorSpec() const noexcept; + explicit operator NVCVColorSpec() const noexcept; ChromaLoc chromaLoc() const noexcept; ColorSpec chromaLoc(const ChromaLoc &newLoc) const; diff --git a/src/nvcv/src/priv/Context.cpp b/src/nvcv/src/priv/Context.cpp index 41d3dcb75..2e4415e10 100644 --- a/src/nvcv/src/priv/Context.cpp +++ b/src/nvcv/src/priv/Context.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -29,22 +29,9 @@ IContext &GlobalContext() return g_ctx; } -Context::Context() - : m_allocatorManager("Allocator") - , m_imageManager("Image") - , m_imageBatchManager("ImageBatch") - , m_tensorManager("Tensor") - , m_tensorBatchManager("TensorBatch") - , m_arrayManager("Array") - , m_managerList{m_allocatorManager, m_imageManager, m_imageBatchManager, - m_tensorManager, m_tensorBatchManager, m_arrayManager} -{ -} +Context::Context() = default; -Context::~Context() -{ - // empty -} +Context::~Context() = default; IAllocator &Context::allocDefault() { diff --git a/src/nvcv/src/priv/Context.hpp b/src/nvcv/src/priv/Context.hpp index d1be14ace..29291840c 100644 --- a/src/nvcv/src/priv/Context.hpp +++ b/src/nvcv/src/priv/Context.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -33,7 +33,7 @@ class Context final : public IContext { public: Context(); - ~Context(); + ~Context() override; const Managers &managerList() const override; IAllocator &allocDefault() override; @@ -41,14 +41,15 @@ class Context final : public IContext private: // Order is important due to inter-dependencies DefaultAllocator m_allocDefault; - AllocatorManager m_allocatorManager; - ImageManager m_imageManager; - ImageBatchManager m_imageBatchManager; - TensorManager m_tensorManager; - TensorBatchManager m_tensorBatchManager; - ArrayManager m_arrayManager; - - Managers m_managerList; + AllocatorManager m_allocatorManager{"Allocator"}; + ImageManager m_imageManager{"Image"}; + ImageBatchManager m_imageBatchManager{"ImageBatch"}; + TensorManager m_tensorManager{"Tensor"}; + TensorBatchManager m_tensorBatchManager{"TensorBatch"}; + ArrayManager m_arrayManager{"Array"}; + + Managers m_managerList{m_allocatorManager, m_imageManager, m_imageBatchManager, + m_tensorManager, m_tensorBatchManager, m_arrayManager}; }; } // namespace nvcv::priv diff --git a/src/nvcv/src/priv/CustomAllocator.cpp b/src/nvcv/src/priv/CustomAllocator.cpp index 54bfde07f..2113ee201 100644 --- a/src/nvcv/src/priv/CustomAllocator.cpp +++ b/src/nvcv/src/priv/CustomAllocator.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -125,14 +125,14 @@ NVCVResourceAllocator CustomAllocator::doGet(NVCVResourceType resType) // Host Memory ------------------ -void *CustomAllocator::doAllocHostMem(int64_t size, int32_t align) +NVCVMemoryBuffer CustomAllocator::doAllocHostMem(int64_t size, int32_t align) { NVCVResourceAllocator &custom = m_allocators[NVCV_RESOURCE_MEM_HOST]; NVCV_ASSERT(custom.res.mem.fnAlloc != nullptr); return custom.res.mem.fnAlloc(custom.ctx, size, align); } -void CustomAllocator::doFreeHostMem(void *ptr, int64_t size, int32_t align) noexcept +void CustomAllocator::doFreeHostMem(NVCVMemoryBuffer ptr, int64_t size, int32_t align) noexcept { NVCVResourceAllocator &custom = m_allocators[NVCV_RESOURCE_MEM_HOST]; NVCV_ASSERT(custom.res.mem.fnFree != nullptr); @@ -141,14 +141,14 @@ void CustomAllocator::doFreeHostMem(void *ptr, int64_t size, int32_t align) noex // Host Pinned Memory ------------------ -void *CustomAllocator::doAllocHostPinnedMem(int64_t size, int32_t align) +NVCVMemoryBuffer CustomAllocator::doAllocHostPinnedMem(int64_t size, int32_t align) { NVCVResourceAllocator &custom = m_allocators[NVCV_RESOURCE_MEM_HOST_PINNED]; NVCV_ASSERT(custom.res.mem.fnAlloc != nullptr); return custom.res.mem.fnAlloc(custom.ctx, size, align); } -void CustomAllocator::doFreeHostPinnedMem(void *ptr, int64_t size, int32_t align) noexcept +void CustomAllocator::doFreeHostPinnedMem(NVCVMemoryBuffer ptr, int64_t size, int32_t align) noexcept { NVCVResourceAllocator &custom = m_allocators[NVCV_RESOURCE_MEM_HOST_PINNED]; NVCV_ASSERT(custom.res.mem.fnFree != nullptr); @@ -157,14 +157,14 @@ void CustomAllocator::doFreeHostPinnedMem(void *ptr, int64_t size, int32_t align // Cuda Memory ------------------ -void *CustomAllocator::doAllocCudaMem(int64_t size, int32_t align) +NVCVMemoryBuffer CustomAllocator::doAllocCudaMem(int64_t size, int32_t align) { NVCVResourceAllocator &custom = m_allocators[NVCV_RESOURCE_MEM_CUDA]; NVCV_ASSERT(custom.res.mem.fnAlloc != nullptr); return custom.res.mem.fnAlloc(custom.ctx, size, align); } -void CustomAllocator::doFreeCudaMem(void *ptr, int64_t size, int32_t align) noexcept +void CustomAllocator::doFreeCudaMem(NVCVMemoryBuffer ptr, int64_t size, int32_t align) noexcept { NVCVResourceAllocator &custom = m_allocators[NVCV_RESOURCE_MEM_CUDA]; NVCV_ASSERT(custom.res.mem.fnFree != nullptr); diff --git a/src/nvcv/src/priv/CustomAllocator.hpp b/src/nvcv/src/priv/CustomAllocator.hpp index 1295714ed..b9326db3e 100644 --- a/src/nvcv/src/priv/CustomAllocator.hpp +++ b/src/nvcv/src/priv/CustomAllocator.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,26 +22,28 @@ #include +#include + namespace nvcv::priv { class CustomAllocator final : public CoreObjectBase { public: CustomAllocator(const NVCVResourceAllocator *customAllocators, int32_t numCustomAllocators); - ~CustomAllocator(); + ~CustomAllocator() override; private: - NVCVResourceAllocator m_allocators[NVCV_NUM_RESOURCE_TYPES] = {}; - uint32_t m_customAllocatorMask = 0; + std::array m_allocators = {}; + uint32_t m_customAllocatorMask = 0; - void *doAllocHostMem(int64_t size, int32_t align) override; - void doFreeHostMem(void *ptr, int64_t size, int32_t align) noexcept override; + NVCVMemoryBuffer doAllocHostMem(int64_t size, int32_t align) override; + void doFreeHostMem(NVCVMemoryBuffer ptr, int64_t size, int32_t align) noexcept override; - void *doAllocHostPinnedMem(int64_t size, int32_t align) override; - void doFreeHostPinnedMem(void *ptr, int64_t size, int32_t align) noexcept override; + NVCVMemoryBuffer doAllocHostPinnedMem(int64_t size, int32_t align) override; + void doFreeHostPinnedMem(NVCVMemoryBuffer ptr, int64_t size, int32_t align) noexcept override; - void *doAllocCudaMem(int64_t size, int32_t align) override; - void doFreeCudaMem(void *ptr, int64_t size, int32_t align) noexcept override; + NVCVMemoryBuffer doAllocCudaMem(int64_t size, int32_t align) override; + void doFreeCudaMem(NVCVMemoryBuffer ptr, int64_t size, int32_t align) noexcept override; NVCVResourceAllocator doGet(NVCVResourceType resType) override; }; diff --git a/src/nvcv/src/priv/DataLayout.cpp b/src/nvcv/src/priv/DataLayout.cpp index 8974a9d08..8de11be35 100644 --- a/src/nvcv/src/priv/DataLayout.cpp +++ b/src/nvcv/src/priv/DataLayout.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,6 +25,7 @@ #include #include +#include #include // |63 62 61|60 59 58|57 56 55|54|53 52 51|50 49 48|47|46 45 44|43 42 41 40|39 38|37 36 35| @@ -324,14 +325,16 @@ const std::multimap g_dataToPacking = [] { std::multimap map; - for (const auto &item : g_packingToData) + for (const auto &[packing, data] : g_packingToData) { - map.emplace(item.second, item.first); + map.emplace(data, packing); } return map; }(); +using PackingLookupIterator = std::multimap::const_iterator; + } // namespace std::optional MakeNVCVPacking(int bitsX, int bitsY, int bitsZ, int bitsW) noexcept @@ -340,7 +343,8 @@ std::optional MakeNVCVPacking(int bitsX, int bitsY, int bitsZ, int if (bitsY == 0 || bitsZ == 0) { - int origX = bitsX, origY = bitsY; + int origX = bitsX; + int origY = bitsY; // We use MSB representation, e.g. X10b6 switch (origX) @@ -355,6 +359,8 @@ std::optional MakeNVCVPacking(int bitsX, int bitsY, int bitsZ, int bitsY = 32 - origX; params.swizzle = NVCV_SWIZZLE_X000; break; + default: + break; } switch (origY) @@ -369,6 +375,8 @@ std::optional MakeNVCVPacking(int bitsX, int bitsY, int bitsZ, int bitsZ = 32 - origY; params.swizzle = NVCV_SWIZZLE_XZ00; break; + default: + break; } } @@ -382,7 +390,7 @@ std::optional MakeNVCVPacking(int bitsX, int bitsY, int bitsZ, int static NVCVSwizzle MakeNVCVSwizzleFromBits(const int (&bits)[4]) { - NVCVChannel swc[4]; + std::array swc; for (int i = 0; i < 4; ++i) { swc[i] = bits[i] != 0 ? (NVCVChannel)(i + NVCV_CHANNEL_X) : NVCV_CHANNEL_0; @@ -390,6 +398,65 @@ static NVCVSwizzle MakeNVCVSwizzleFromBits(const int (&bits)[4]) return MakeNVCVSwizzle(swc[0], swc[1], swc[2], swc[3]); } +static PackingLookupIterator FindSmallestAlignment(PackingLookupIterator itbegin, PackingLookupIterator itend) noexcept +{ + auto it = itbegin; + for (; itbegin != itend; ++itbegin) // NOSONAR: iterator search must return the matching candidate. + { + if (itbegin->first.params.alignment < it->first.params.alignment) + { + it = itbegin; + } + } + return it; +} + +static PackingLookupIterator FindCompatibleAlignment(PackingLookupIterator itbegin, + PackingLookupIterator itend) noexcept +{ + auto it = itbegin; + for (; itbegin != itend; ++itbegin) // NOSONAR: the loop must return the matching candidate iterator. + { + // Smaller alignments are valid. + if (it->first.params.alignment >= itbegin->first.params.alignment) + { + return itbegin; + } + } + return itend; +} + +static PackingLookupIterator FindPackingForAlignment(PackingLookupIterator itbegin, PackingLookupIterator itend, + int alignment) noexcept +{ + if (alignment == 0) + { + return FindSmallestAlignment(itbegin, itend); + } + return FindCompatibleAlignment(itbegin, itend); +} + +static bool MatchesByteOrder(const NVCVPackingParams ¶ms, const PackingData &candidate) noexcept +{ + return GetNumChannels(params.swizzle) < 2 || candidate.params.byteOrder == params.byteOrder; +} + +static bool MatchesSwizzle(const NVCVPackingParams ¶ms, const PackingData &candidate) +{ + if (params.swizzle == NVCV_SWIZZLE_0000) + { + return true; + } + + NVCVSwizzle sw = candidate.params.swizzle; + if (sw == NVCV_SWIZZLE_0000) + { + sw = MakeNVCVSwizzleFromBits(params.bits); + } + + return sw == params.swizzle; +} + std::optional MakeNVCVPacking(const NVCVPackingParams ¶ms) noexcept { PackingData key = {}; @@ -402,74 +469,21 @@ std::optional MakeNVCVPacking(const NVCVPackingParams ¶ms) noex } auto [itbegin, itend] = g_dataToPacking.equal_range(key); - if (itbegin != itend) + if (itbegin == itend) { - auto it = itbegin; - if (params.alignment == 0) - { - for (; itbegin != itend; ++itbegin) - { - // choose smallest alignment - if (itbegin->first.params.alignment < it->first.params.alignment) - { - it = itbegin; - } - } - } - else - { - for (; itbegin != itend; ++itbegin) - { - // Smaller alignments are valid. - if (it->first.params.alignment >= itbegin->first.params.alignment) - { - it = itbegin; - break; - } - } - // packing with needed alignment not found - if (itbegin == itend) - { - return std::nullopt; - } - } - - // if 0 or one channel, packing is both host and big endian, so don't need to filter out. - if (GetNumChannels(params.swizzle) >= 2) - { - // Endian don't match? - if (it->first.params.byteOrder != params.byteOrder) - { - return std::nullopt; - } - } - - // use filters by swizzle? - if (params.swizzle != NVCV_SWIZZLE_0000) - { - // If our swizzle is not specified, let's reconstruct it from bits - NVCVSwizzle sw = it->first.params.swizzle; - if (sw == NVCV_SWIZZLE_0000) - { - sw = MakeNVCVSwizzleFromBits(params.bits); - } - - // now we can apply the filter. - if (sw != params.swizzle) - { - return std::nullopt; - } - } - - return it->second; + return std::nullopt; } - else + + auto it = FindPackingForAlignment(itbegin, itend, params.alignment); + if (it == itend || !MatchesByteOrder(params, it->first) || !MatchesSwizzle(params, it->first)) { return std::nullopt; } + + return it->second; } -const uint64_t swizzleBitsArray[NVCV_MAX_SWIZZLE_COUNT] +const std::array swizzleBitsArray = {NVCV_DETAIL_MAKE_SWZL(0, 0, 0, 0), NVCV_DETAIL_MAKE_SWZL(X, 0, 0, 0), NVCV_DETAIL_MAKE_SWZL(X, Y, 0, 0), NVCV_DETAIL_MAKE_SWZL(X, Y, Z, 0), NVCV_DETAIL_MAKE_SWZL(X, Y, Z, W), NVCV_DETAIL_MAKE_SWZL(1, 0, 0, 0), NVCV_DETAIL_MAKE_SWZL(0, 0, 0, 1), NVCV_DETAIL_MAKE_SWZL(Z, Y, X, W), NVCV_DETAIL_MAKE_SWZL(W, X, Y, Z), @@ -517,9 +531,9 @@ bool IsSubWord(const NVCVPackingParams &p) } int chbits = 0; - for (int i = 0; i < 4; ++i) + for (int bits : p.bits) { - if (p.bits[i] != 0) + if (bits != 0) { chbits += 1; } @@ -610,7 +624,7 @@ int GetBitsPerPixel(NVCVPacking packing) noexcept } else { - // invalid; + // No valid bit-depth encoding uses this value. return 0; } } @@ -646,7 +660,7 @@ int GetNumChannels(NVCVSwizzle swizzle) noexcept { std::array channels = GetChannels(swizzle); - int hist[4] = {}; + std::array hist = {}; int count = 0; for (int i = 0; i < 4; ++i) @@ -683,7 +697,7 @@ int GetNumComponents(NVCVPacking packing) noexcept } else { - return ExtractBitfield(packing, 4, 2) + 1; + return static_cast(ExtractBitfield(packing, 4, 2) + 1); } } @@ -734,6 +748,49 @@ int GetAlignment(NVCVPacking packing) noexcept return GetPackingParams(packing).alignment; } +static NVCVChannel MaxSwizzleChannel(NVCVSwizzle swizzle) noexcept +{ + NVCVChannel maxSwChannel = NVCV_CHANNEL_0; + for (int j = 0; j < 4; ++j) + { + NVCVChannel swch = GetSwizzleChannel(swizzle, j); + if (NVCV_CHANNEL_X <= swch && swch <= NVCV_CHANNEL_W) + { + maxSwChannel = std::max(maxSwChannel, swch); + } + } + return maxSwChannel; +} + +static NVCVChannel NextAlphaChannel(NVCVChannel maxSwChannel) +{ + auto ch = static_cast(maxSwChannel + 1); + if (ch >= NVCV_CHANNEL_1) + { + throw Exception(NVCV_ERROR_INVALID_ARGUMENT, + "When swizzle has W channel, it must not have channel with maximum value (channel '1')"); + } + return ch; +} + +static void FillReverseSwizzle(std::array &rev, NVCVSwizzle swizzle) +{ + NVCVChannel maxSwChannel = MaxSwizzleChannel(swizzle); + + for (int j = 0; j < 4; ++j) + { + NVCVChannel ch = GetSwizzleChannel(swizzle, j); + if (ch == NVCV_CHANNEL_1) + { + rev[NextAlphaChannel(maxSwChannel)] = NVCV_CHANNEL_1; + } + else + { + rev[ch] = (NVCVChannel)(NVCV_CHANNEL_X + j); + } + } +} + NVCVSwizzle MergePlaneSwizzles(NVCVSwizzle sw0, NVCVSwizzle sw1, NVCVSwizzle sw2, NVCVSwizzle sw3) { // just one plane? @@ -750,48 +807,17 @@ NVCVSwizzle MergePlaneSwizzles(NVCVSwizzle sw0, NVCVSwizzle sw1, NVCVSwizzle sw2 } } - NVCVSwizzle sw[4] = {sw0, sw1, sw2, sw3}; + std::array sw = {sw0, sw1, sw2, sw3}; - NVCVChannel swResult[4] = {}; + std::array swResult = {}; int curch = 0; for (int i = 0; i < 4 && sw[i] != NVCV_SWIZZLE_0000; ++i) { int nchannels = GetNumChannels(sw[i]); - NVCVChannel rev[6] = {}; - - NVCVChannel maxSwChannel = NVCV_CHANNEL_0; - - for (int j = 0; j < 4; ++j) - { - NVCVChannel swch = GetSwizzleChannel(sw[i], j); - if (NVCV_CHANNEL_X <= swch && swch <= NVCV_CHANNEL_W) - { - maxSwChannel = std::max(maxSwChannel, swch); - } - } - - for (int j = 0; j < 4; ++j) - { - NVCVChannel ch = GetSwizzleChannel(sw[i], j); - if (ch == NVCV_CHANNEL_1) - { - ch = (NVCVChannel)(maxSwChannel + 1); - // you can't specify W and also have 1 in the swizzle - if (ch >= NVCV_CHANNEL_1) - { - throw Exception( - NVCV_ERROR_INVALID_ARGUMENT, - "When swizzle has W channel, it must not have channel with maximum value (channel '1')"); - } - rev[ch] = NVCV_CHANNEL_1; - } - else - { - rev[ch] = (NVCVChannel)(NVCV_CHANNEL_X + j); - } - } + std::array rev = {}; + FillReverseSwizzle(rev, sw[i]); for (int j = 0; j < nchannels; ++j) { @@ -845,8 +871,9 @@ NVCVSwizzle FlipByteOrder(NVCVSwizzle swizzle, int off, int len) noexcept // So first map swizzle to memory space, i.e., sort components in order // they will show up in memory, from lowest address to highest. - NVCVChannel mem[4] = {}; - int m = INT32_MAX, M = INT32_MIN; + std::array mem = {}; + int m = INT32_MAX; + int M = INT32_MIN; for (int i = 0; i < 4; ++i) { @@ -861,7 +888,7 @@ NVCVSwizzle FlipByteOrder(NVCVSwizzle swizzle, int off, int len) noexcept } // Now flip in memory space - NVCVChannel flipped[4] = {}; + std::array flipped = {}; for (int i = m; i <= M; ++i) { NVCVChannel ch = mem[m + ((M - m) - (i - m))]; @@ -898,8 +925,9 @@ const char *GetName(NVCVDataKind dataKind) #undef ENUM_CASE } priv::CoreTLS &tls = priv::GetCoreTLS(); - util::BufferOStream(tls.bufDataKindName, sizeof(tls.bufDataKindName)) << "NVCVDataKind(" << (int)dataKind << ")"; - return tls.bufDataKindName; + util::BufferOStream(tls.bufDataKindName.data(), static_cast(tls.bufDataKindName.size())) + << "NVCVDataKind(" << (int)dataKind << ")"; + return tls.bufDataKindName.data(); } const char *GetName(NVCVMemLayout memLayout) @@ -920,9 +948,9 @@ const char *GetName(NVCVMemLayout memLayout) } priv::CoreTLS &tls = priv::GetCoreTLS(); - util::BufferOStream(tls.bufMemLayoutName, sizeof(tls.bufMemLayoutName)) + util::BufferOStream(tls.bufMemLayoutName.data(), static_cast(tls.bufMemLayoutName.size())) << "NVCVMemLayout(" << (int)memLayout << ")"; - return tls.bufMemLayoutName; + return tls.bufMemLayoutName.data(); } const char *GetName(NVCVChannel swizzleChannel) @@ -946,8 +974,9 @@ const char *GetName(NVCVChannel swizzleChannel) } priv::CoreTLS &tls = priv::GetCoreTLS(); - util::BufferOStream(tls.bufChannelName, sizeof(tls.bufChannelName)) << "NVCVChannel(" << (int)swizzleChannel << ")"; - return tls.bufChannelName; + util::BufferOStream(tls.bufChannelName.data(), static_cast(tls.bufChannelName.size())) + << "NVCVChannel(" << (int)swizzleChannel << ")"; + return tls.bufChannelName.data(); } const char *GetName(NVCVAlphaType alphaType) @@ -961,9 +990,9 @@ const char *GetName(NVCVAlphaType alphaType) } priv::CoreTLS &tls = priv::GetCoreTLS(); - util::BufferOStream(tls.bufAlphaTypeName, sizeof(tls.bufAlphaTypeName)) + util::BufferOStream(tls.bufAlphaTypeName.data(), static_cast(tls.bufAlphaTypeName.size())) << "NVCVAlphaType(" << (int)alphaType << ")"; - return tls.bufAlphaTypeName; + return tls.bufAlphaTypeName.data(); } const char *GetName(NVCVExtraChannel channelType) @@ -979,9 +1008,9 @@ const char *GetName(NVCVExtraChannel channelType) } priv::CoreTLS &tls = priv::GetCoreTLS(); - util::BufferOStream(tls.bufExtraChannelTypeName, sizeof(tls.bufExtraChannelTypeName)) + util::BufferOStream(tls.bufExtraChannelTypeName.data(), static_cast(tls.bufExtraChannelTypeName.size())) << "NVCVExtraChannel(" << (int)channelType << ")"; - return tls.bufExtraChannelTypeName; + return tls.bufExtraChannelTypeName.data(); } const char *GetName(NVCVSwizzle swizzle) @@ -989,9 +1018,9 @@ const char *GetName(NVCVSwizzle swizzle) std::array channels = priv::GetChannels(swizzle); priv::CoreTLS &tls = priv::GetCoreTLS(); - util::BufferOStream(tls.bufSwizzleName, sizeof(tls.bufSwizzleName)) + util::BufferOStream(tls.bufSwizzleName.data(), static_cast(tls.bufSwizzleName.size())) << channels[0] << channels[1] << channels[2] << channels[3]; - return tls.bufSwizzleName; + return tls.bufSwizzleName.data(); } const char *GetName(NVCVByteOrder byteOrder) @@ -1005,9 +1034,9 @@ const char *GetName(NVCVByteOrder byteOrder) } priv::CoreTLS &tls = priv::GetCoreTLS(); - util::BufferOStream(tls.bufByteOrderName, sizeof(tls.bufByteOrderName)) + util::BufferOStream(tls.bufByteOrderName.data(), static_cast(tls.bufByteOrderName.size())) << "NVCVByteOrder(" << (int)byteOrder << ")"; - return tls.bufByteOrderName; + return tls.bufByteOrderName.data(); } const char *GetName(NVCVPacking packing) @@ -1020,8 +1049,9 @@ const char *GetName(NVCVPacking packing) else { priv::CoreTLS &tls = priv::GetCoreTLS(); - util::BufferOStream(tls.bufPackingName, sizeof(tls.bufPackingName)) << "NVCVPacking(" << (int)packing << ")"; - return tls.bufPackingName; + util::BufferOStream(tls.bufPackingName.data(), static_cast(tls.bufPackingName.size())) + << "NVCVPacking(" << (int)packing << ")"; + return tls.bufPackingName.data(); } } diff --git a/src/nvcv/src/priv/DataType.cpp b/src/nvcv/src/priv/DataType.cpp index 0c2299bc9..fa2f8952a 100644 --- a/src/nvcv/src/priv/DataType.cpp +++ b/src/nvcv/src/priv/DataType.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -84,75 +84,77 @@ int DataType::alignment() const noexcept std::ostream &operator<<(std::ostream &out, DataType type) { - switch (type.value()) + switch (type.value()) // NOSONAR: enum stringification must cover every ABI value. { #define NVCV_ENUM(E) \ case E: \ return out << #E; - NVCV_ENUM(NVCV_DATA_TYPE_NONE); - - NVCV_ENUM(NVCV_DATA_TYPE_U8); - NVCV_ENUM(NVCV_DATA_TYPE_2U8); - NVCV_ENUM(NVCV_DATA_TYPE_3U8); - NVCV_ENUM(NVCV_DATA_TYPE_4U8); - - NVCV_ENUM(NVCV_DATA_TYPE_S8); - NVCV_ENUM(NVCV_DATA_TYPE_2S8); - NVCV_ENUM(NVCV_DATA_TYPE_3S8); - NVCV_ENUM(NVCV_DATA_TYPE_4S8); - - NVCV_ENUM(NVCV_DATA_TYPE_U16); - NVCV_ENUM(NVCV_DATA_TYPE_2U16); - NVCV_ENUM(NVCV_DATA_TYPE_3U16); - NVCV_ENUM(NVCV_DATA_TYPE_4U16); - - NVCV_ENUM(NVCV_DATA_TYPE_S16); - NVCV_ENUM(NVCV_DATA_TYPE_2S16); - NVCV_ENUM(NVCV_DATA_TYPE_3S16); - NVCV_ENUM(NVCV_DATA_TYPE_4S16); - - NVCV_ENUM(NVCV_DATA_TYPE_U32); - NVCV_ENUM(NVCV_DATA_TYPE_2U32); - NVCV_ENUM(NVCV_DATA_TYPE_3U32); - NVCV_ENUM(NVCV_DATA_TYPE_4U32); - - NVCV_ENUM(NVCV_DATA_TYPE_S32); - NVCV_ENUM(NVCV_DATA_TYPE_2S32); - NVCV_ENUM(NVCV_DATA_TYPE_3S32); - NVCV_ENUM(NVCV_DATA_TYPE_4S32); - - NVCV_ENUM(NVCV_DATA_TYPE_F16); - NVCV_ENUM(NVCV_DATA_TYPE_2F16); - NVCV_ENUM(NVCV_DATA_TYPE_3F16); - NVCV_ENUM(NVCV_DATA_TYPE_4F16); - - NVCV_ENUM(NVCV_DATA_TYPE_F32); - NVCV_ENUM(NVCV_DATA_TYPE_2F32); - NVCV_ENUM(NVCV_DATA_TYPE_3F32); - NVCV_ENUM(NVCV_DATA_TYPE_4F32); - - NVCV_ENUM(NVCV_DATA_TYPE_U64); - NVCV_ENUM(NVCV_DATA_TYPE_2U64); - NVCV_ENUM(NVCV_DATA_TYPE_3U64); - NVCV_ENUM(NVCV_DATA_TYPE_4U64); - - NVCV_ENUM(NVCV_DATA_TYPE_S64); - NVCV_ENUM(NVCV_DATA_TYPE_2S64); - NVCV_ENUM(NVCV_DATA_TYPE_3S64); - NVCV_ENUM(NVCV_DATA_TYPE_4S64); - - NVCV_ENUM(NVCV_DATA_TYPE_F64); - NVCV_ENUM(NVCV_DATA_TYPE_2F64); - NVCV_ENUM(NVCV_DATA_TYPE_3F64); - NVCV_ENUM(NVCV_DATA_TYPE_4F64); - - NVCV_ENUM(NVCV_DATA_TYPE_C64); - NVCV_ENUM(NVCV_DATA_TYPE_2C64); - NVCV_ENUM(NVCV_DATA_TYPE_3C64); - NVCV_ENUM(NVCV_DATA_TYPE_4C64); - - NVCV_ENUM(NVCV_DATA_TYPE_C128); - NVCV_ENUM(NVCV_DATA_TYPE_2C128); + NVCV_ENUM(NVCV_DATA_TYPE_NONE) + + NVCV_ENUM(NVCV_DATA_TYPE_U8) + NVCV_ENUM(NVCV_DATA_TYPE_2U8) + NVCV_ENUM(NVCV_DATA_TYPE_3U8) + NVCV_ENUM(NVCV_DATA_TYPE_4U8) + + NVCV_ENUM(NVCV_DATA_TYPE_S8) + NVCV_ENUM(NVCV_DATA_TYPE_2S8) + NVCV_ENUM(NVCV_DATA_TYPE_3S8) + NVCV_ENUM(NVCV_DATA_TYPE_4S8) + + NVCV_ENUM(NVCV_DATA_TYPE_U16) + NVCV_ENUM(NVCV_DATA_TYPE_2U16) + NVCV_ENUM(NVCV_DATA_TYPE_3U16) + NVCV_ENUM(NVCV_DATA_TYPE_4U16) + + NVCV_ENUM(NVCV_DATA_TYPE_S16) + NVCV_ENUM(NVCV_DATA_TYPE_2S16) + NVCV_ENUM(NVCV_DATA_TYPE_3S16) + NVCV_ENUM(NVCV_DATA_TYPE_4S16) + + NVCV_ENUM(NVCV_DATA_TYPE_U32) + NVCV_ENUM(NVCV_DATA_TYPE_2U32) + NVCV_ENUM(NVCV_DATA_TYPE_3U32) + NVCV_ENUM(NVCV_DATA_TYPE_4U32) + + NVCV_ENUM(NVCV_DATA_TYPE_S32) + NVCV_ENUM(NVCV_DATA_TYPE_2S32) + NVCV_ENUM(NVCV_DATA_TYPE_3S32) + NVCV_ENUM(NVCV_DATA_TYPE_4S32) + + NVCV_ENUM(NVCV_DATA_TYPE_F16) + NVCV_ENUM(NVCV_DATA_TYPE_2F16) + NVCV_ENUM(NVCV_DATA_TYPE_3F16) + NVCV_ENUM(NVCV_DATA_TYPE_4F16) + + NVCV_ENUM(NVCV_DATA_TYPE_F32) + NVCV_ENUM(NVCV_DATA_TYPE_2F32) + NVCV_ENUM(NVCV_DATA_TYPE_3F32) + NVCV_ENUM(NVCV_DATA_TYPE_4F32) + + NVCV_ENUM(NVCV_DATA_TYPE_U64) + NVCV_ENUM(NVCV_DATA_TYPE_2U64) + NVCV_ENUM(NVCV_DATA_TYPE_3U64) + NVCV_ENUM(NVCV_DATA_TYPE_4U64) + + NVCV_ENUM(NVCV_DATA_TYPE_S64) + NVCV_ENUM(NVCV_DATA_TYPE_2S64) + NVCV_ENUM(NVCV_DATA_TYPE_3S64) + NVCV_ENUM(NVCV_DATA_TYPE_4S64) + + NVCV_ENUM(NVCV_DATA_TYPE_F64) + NVCV_ENUM(NVCV_DATA_TYPE_2F64) + NVCV_ENUM(NVCV_DATA_TYPE_3F64) + NVCV_ENUM(NVCV_DATA_TYPE_4F64) + + NVCV_ENUM(NVCV_DATA_TYPE_C64) + NVCV_ENUM(NVCV_DATA_TYPE_2C64) + NVCV_ENUM(NVCV_DATA_TYPE_3C64) + NVCV_ENUM(NVCV_DATA_TYPE_4C64) + + NVCV_ENUM(NVCV_DATA_TYPE_C128) + NVCV_ENUM(NVCV_DATA_TYPE_2C128) + default: + break; #undef NVCV_ENUM } diff --git a/src/nvcv/src/priv/DefaultAllocator.cpp b/src/nvcv/src/priv/DefaultAllocator.cpp index df9a2c921..ae5fc556a 100644 --- a/src/nvcv/src/priv/DefaultAllocator.cpp +++ b/src/nvcv/src/priv/DefaultAllocator.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,37 +22,37 @@ #include #include -#include // for aligned_alloc +#include namespace nvcv::priv { -void *DefaultAllocator::doAllocHostMem(int64_t size, int32_t align) +NVCVMemoryBuffer DefaultAllocator::doAllocHostMem(int64_t size, int32_t align) { - return std::aligned_alloc(align, size); + return static_cast( + ::operator new (static_cast(size), std::align_val_t{static_cast(align)})); } -void DefaultAllocator::doFreeHostMem(void *ptr, int64_t size, int32_t align) noexcept +void DefaultAllocator::doFreeHostMem(NVCVMemoryBuffer ptr, int64_t size, int32_t align) noexcept { (void)size; - (void)align; - std::free(ptr); + ::operator delete (ptr, std::align_val_t{static_cast(align)}); } -void *DefaultAllocator::doAllocHostPinnedMem(int64_t size, int32_t align) +NVCVMemoryBuffer DefaultAllocator::doAllocHostPinnedMem(int64_t size, int32_t align) { void *ptr = nullptr; NVCV_CHECK_THROW(::cudaHostAlloc(&ptr, size, cudaHostAllocWriteCombined | cudaHostAllocMapped)); - // TODO: can we do better than this? + // REVISIT: can we do better than this? if (reinterpret_cast(ptr) % align != 0) { NVCV_CHECK_LOG(::cudaFreeHost(ptr)); throw Exception(NVCV_ERROR_INTERNAL, "Can't allocate %ld bytes of CUDA memory with alignment at %d bytes", size, align); } - return ptr; + return static_cast(ptr); } -void DefaultAllocator::doFreeHostPinnedMem(void *ptr, int64_t size, int32_t align) noexcept +void DefaultAllocator::doFreeHostPinnedMem(NVCVMemoryBuffer ptr, int64_t size, int32_t align) noexcept { (void)size; (void)align; @@ -60,22 +60,22 @@ void DefaultAllocator::doFreeHostPinnedMem(void *ptr, int64_t size, int32_t alig NVCV_CHECK_LOG(::cudaFreeHost(ptr)); } -void *DefaultAllocator::doAllocCudaMem(int64_t size, int32_t align) +NVCVMemoryBuffer DefaultAllocator::doAllocCudaMem(int64_t size, int32_t align) { void *ptr = nullptr; NVCV_CHECK_THROW(::cudaMalloc(&ptr, size)); - // TODO: can we do better than this? + // REVISIT: can we do better than this? if (reinterpret_cast(ptr) % align != 0) { NVCV_CHECK_LOG(::cudaFree(ptr)); throw Exception(NVCV_ERROR_INTERNAL, "Can't allocate %ld bytes of CUDA memory with alignment at %d bytes", size, align); } - return ptr; + return static_cast(ptr); } -void DefaultAllocator::doFreeCudaMem(void *ptr, int64_t size, int32_t align) noexcept +void DefaultAllocator::doFreeCudaMem(NVCVMemoryBuffer ptr, int64_t size, int32_t align) noexcept { (void)size; (void)align; @@ -86,20 +86,20 @@ void DefaultAllocator::doFreeCudaMem(void *ptr, int64_t size, int32_t align) noe NVCVResourceAllocator DefaultAllocator::doGet(NVCVResourceType resType) { NVCVResourceAllocator custAllocator = {}; - custAllocator.ctx = this; + custAllocator.ctx = static_cast(static_cast(this)); custAllocator.resType = resType; switch (resType) { case NVCV_RESOURCE_MEM_HOST: - static auto defAllocHostMem = [](void *ctx, int64_t size, int32_t align) + static auto defAllocHostMem = [](NVCVResourceContext ctx, int64_t size, int32_t align) { - auto *self = static_cast(ctx); + auto *self = static_cast(static_cast(ctx)); return self->allocHostMem(size, align); }; - static auto defFreeHostMem = [](void *ctx, void *ptr, int64_t size, int32_t align) + static auto defFreeHostMem = [](NVCVResourceContext ctx, NVCVMemoryBuffer ptr, int64_t size, int32_t align) { - auto *self = static_cast(ctx); + auto *self = static_cast(static_cast(ctx)); return self->freeHostMem(ptr, size, align); }; custAllocator.res.mem.fnAlloc = defAllocHostMem; @@ -107,14 +107,14 @@ NVCVResourceAllocator DefaultAllocator::doGet(NVCVResourceType resType) break; case NVCV_RESOURCE_MEM_CUDA: - static auto defAllocCudaMem = [](void *ctx, int64_t size, int32_t align) + static auto defAllocCudaMem = [](NVCVResourceContext ctx, int64_t size, int32_t align) { - auto *self = static_cast(ctx); + auto *self = static_cast(static_cast(ctx)); return self->allocCudaMem(size, align); }; - static auto defFreeCudaMem = [](void *ctx, void *ptr, int64_t size, int32_t align) + static auto defFreeCudaMem = [](NVCVResourceContext ctx, NVCVMemoryBuffer ptr, int64_t size, int32_t align) { - auto *self = static_cast(ctx); + auto *self = static_cast(static_cast(ctx)); return self->freeCudaMem(ptr, size, align); }; custAllocator.res.mem.fnAlloc = defAllocCudaMem; @@ -122,14 +122,15 @@ NVCVResourceAllocator DefaultAllocator::doGet(NVCVResourceType resType) break; case NVCV_RESOURCE_MEM_HOST_PINNED: - static auto defAllocHostPinnedMem = [](void *ctx, int64_t size, int32_t align) + static auto defAllocHostPinnedMem = [](NVCVResourceContext ctx, int64_t size, int32_t align) { - auto *self = static_cast(ctx); + auto *self = static_cast(static_cast(ctx)); return self->allocHostPinnedMem(size, align); }; - static auto defFreeHostPinnedMem = [](void *ctx, void *ptr, int64_t size, int32_t align) + static auto defFreeHostPinnedMem + = [](NVCVResourceContext ctx, NVCVMemoryBuffer ptr, int64_t size, int32_t align) { - auto *self = static_cast(ctx); + auto *self = static_cast(static_cast(ctx)); return self->freeHostPinnedMem(ptr, size, align); }; custAllocator.res.mem.fnAlloc = defAllocHostPinnedMem; diff --git a/src/nvcv/src/priv/DefaultAllocator.hpp b/src/nvcv/src/priv/DefaultAllocator.hpp index 36405e19c..85468c322 100644 --- a/src/nvcv/src/priv/DefaultAllocator.hpp +++ b/src/nvcv/src/priv/DefaultAllocator.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,14 +25,14 @@ namespace nvcv::priv { class DefaultAllocator final : public CoreObjectBase { private: - void *doAllocHostMem(int64_t size, int32_t align) override; - void doFreeHostMem(void *ptr, int64_t size, int32_t align) noexcept override; + NVCVMemoryBuffer doAllocHostMem(int64_t size, int32_t align) override; + void doFreeHostMem(NVCVMemoryBuffer ptr, int64_t size, int32_t align) noexcept override; - void *doAllocHostPinnedMem(int64_t size, int32_t align) override; - void doFreeHostPinnedMem(void *ptr, int64_t size, int32_t align) noexcept override; + NVCVMemoryBuffer doAllocHostPinnedMem(int64_t size, int32_t align) override; + void doFreeHostPinnedMem(NVCVMemoryBuffer ptr, int64_t size, int32_t align) noexcept override; - void *doAllocCudaMem(int64_t size, int32_t align) override; - void doFreeCudaMem(void *ptr, int64_t size, int32_t align) noexcept override; + NVCVMemoryBuffer doAllocCudaMem(int64_t size, int32_t align) override; + void doFreeCudaMem(NVCVMemoryBuffer ptr, int64_t size, int32_t align) noexcept override; NVCVResourceAllocator doGet(NVCVResourceType resType) override; }; diff --git a/src/nvcv/src/priv/Exception.cpp b/src/nvcv/src/priv/Exception.cpp index fd71c6133..1bee55abe 100644 --- a/src/nvcv/src/priv/Exception.cpp +++ b/src/nvcv/src/priv/Exception.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -26,39 +26,74 @@ namespace nvcv::priv { Exception::Exception(NVCVStatus code) - : Exception(code, "%s", "") + : Exception(code, "") { } Exception::Exception(NVCVStatus code, const char *fmt, va_list va) : m_code(code) - , m_strbuf{m_buffer, sizeof(m_buffer), m_buffer} { - snprintf(m_buffer, sizeof(m_buffer) - 1, "%s: ", GetName(code)); + initStreamBuffer(); - size_t len = std::char_traits::length(m_buffer); - vsnprintf(m_buffer + len, sizeof(m_buffer) - len - 1, fmt, va); + detail::FormatTo(m_buffer.data(), m_buffer.size(), "%s: ", GetName(code)); + + size_t len = std::char_traits::length(m_buffer.data()); + detail::VFormatTo(m_buffer.data() + len, m_buffer.size() - len, fmt, va); // Next character written will be appended to m_buffer - m_strbuf.seekpos(std::char_traits::length(m_buffer), std::ios_base::out); + m_strbuf.seekpos(std::char_traits::length(m_buffer.data()), std::ios_base::out); } -Exception::Exception(NVCVStatus code, const char *fmt, ...) +Exception::Exception(NVCVStatus code, const char *msg) : m_code(code) - , m_strbuf{m_buffer, sizeof(m_buffer), m_buffer} { - va_list va; - va_start(va, fmt); + initStreamBuffer(); + formatMessage("%s", msg != nullptr ? msg : ""); +} - snprintf(m_buffer, sizeof(m_buffer) - 1, "%s: ", GetName(code)); +Exception::Exception(const Exception &that) noexcept +{ + initStreamBuffer(); + copyFrom(that); +} - size_t len = std::char_traits::length(m_buffer); - vsnprintf(m_buffer + len, sizeof(m_buffer) - len - 1, fmt, va); +Exception::Exception(Exception &&that) noexcept + : Exception(static_cast(that)) +{ +} - va_end(va); +Exception &Exception::operator=(const Exception &that) noexcept +{ + copyFrom(that); + return *this; +} - // Next character written will be appended to m_buffer - m_strbuf.seekpos(std::char_traits::length(m_buffer), std::ios_base::out); +Exception &Exception::operator=(Exception &&that) noexcept +{ + copyFrom(that); + return *this; +} + +Exception::~Exception() noexcept +{ + m_buffer.back() = '\0'; +} + +void Exception::copyFrom(const Exception &that) noexcept +{ + m_code = that.m_code; + std::memcpy(m_buffer.data(), that.m_buffer.data(), m_buffer.size()); + resetStreamPosition(); +} + +void Exception::initStreamBuffer() noexcept +{ + m_strbuf.reset(m_buffer.data(), static_cast(m_buffer.size())); +} + +void Exception::resetStreamPosition() noexcept +{ + m_strbuf.seekpos(std::char_traits::length(m_buffer.data()), std::ios_base::out); } NVCVStatus Exception::code() const @@ -69,15 +104,15 @@ NVCVStatus Exception::code() const const char *Exception::msg() const { // Only return the message part - const char *out = strchr(m_buffer, ':'); + const char *out = strchr(m_buffer.data(), ':'); NVCV_ASSERT(out != nullptr); - return out += 2; // skip ': ' + return out + 2; // skip ': ' } const char *Exception::what() const noexcept { - return m_buffer; + return m_buffer.data(); } } // namespace nvcv::priv diff --git a/src/nvcv/src/priv/Exception.hpp b/src/nvcv/src/priv/Exception.hpp index 6c7f12e9a..b610d2330 100644 --- a/src/nvcv/src/priv/Exception.hpp +++ b/src/nvcv/src/priv/Exception.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,14 +18,16 @@ #ifndef NVCV_CORE_PRIV_EXCEPTION_HPP #define NVCV_CORE_PRIV_EXCEPTION_HPP +#include "Status.hpp" + #include +#include +#include +#include +#include #include - -#ifdef __GNUC__ -# undef __DEPRECATED -#endif -#include +#include namespace nvcv::priv { @@ -34,16 +36,25 @@ class Exception : public std::exception public: explicit Exception(NVCVStatus code, const char *fmt, va_list va); - explicit Exception(NVCVStatus code, const char *fmt, ...) -#if __GNUC__ - // first argument is actually 'this' - __attribute__((format(printf, 3, 4))); -#else - ; -#endif + explicit Exception(NVCVStatus code, const char *msg); + + template + explicit Exception(NVCVStatus code, const char (&fmt)[N], Args &&...args) + : m_code(code) + { + initStreamBuffer(); + formatMessage(fmt, std::forward(args)...); + } explicit Exception(NVCVStatus code); + Exception(const Exception &that) noexcept; + Exception(Exception &&that) noexcept; + Exception &operator=(const Exception &that) noexcept; + Exception &operator=(Exception &&that) noexcept; + + ~Exception() noexcept override; + NVCVStatus code() const; const char *msg() const; @@ -52,24 +63,32 @@ class Exception : public std::exception template Exception &&operator<<(const T &v) && { - // TODO: must avoid allocating memory from heap, can't use ostringstream + // REVISIT: must avoid allocating memory from heap, can't use ostringstream std::ostream ss(&m_strbuf); ss << v << std::flush; return std::move(*this); } private: - NVCVStatus m_code = NVCV_ERROR_INTERNAL; - char m_buffer[NVCV_MAX_STATUS_MESSAGE_LENGTH + 64 + 2]{}; + void copyFrom(const Exception &that) noexcept; + void initStreamBuffer() noexcept; + void resetStreamPosition() noexcept; - class StrBuffer : public std::strstreambuf + template + void formatMessage(const char (&fmt)[N], Args &&...args) { - public: - using std::strstreambuf::seekpos; - using std::strstreambuf::strstreambuf; - }; + detail::FormatTo(m_buffer.data(), m_buffer.size(), "%s: ", GetName(m_code)); + + size_t len = std::char_traits::length(m_buffer.data()); + detail::FormatTo(m_buffer.data() + len, m_buffer.size() - len, fmt, std::forward(args)...); + + resetStreamPosition(); + } + + NVCVStatus m_code = NVCV_ERROR_INTERNAL; + std::array m_buffer = {}; - StrBuffer m_strbuf; + util::FixedBufferStreamBuf m_strbuf; }; } // namespace nvcv::priv diff --git a/src/nvcv/src/priv/HandleManager.hpp b/src/nvcv/src/priv/HandleManager.hpp index c5c0b4aba..7537d7e11 100644 --- a/src/nvcv/src/priv/HandleManager.hpp +++ b/src/nvcv/src/priv/HandleManager.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,6 +23,7 @@ #include #include +#include #include #include #include @@ -36,7 +37,7 @@ namespace detail { template struct GetHandleType { - using type = void *; + using type = NVCVUserPointer; }; template @@ -89,7 +90,7 @@ class HandleManager static_assert(alignof(Storage) % alignof(T) == 0); NVCV_ASSERT(!this->live()); - T *obj = new (getStorage()) T{std::forward(args)...}; + auto *obj = new (getStorage()) T{std::forward(args)...}; this->m_ptrObj = obj; this->generation++; @@ -110,7 +111,7 @@ class HandleManager return ++m_refCount; } - int refCount() + int refCount() const { return m_refCount; } @@ -125,18 +126,20 @@ class HandleManager return m_ptrObj != nullptr; } - protected: - void *getStorage(); + private: + std::byte *getStorage(); - ~ResourceBase(); Interface *m_ptrObj = nullptr; std::atomic_int m_refCount{0}; + + protected: + ~ResourceBase(); }; public: using HandleType = GetHandleType; - HandleManager(const char *name); + explicit HandleManager(const char *name); ~HandleManager(); template @@ -202,7 +205,7 @@ class HandleManager template struct alignas(util::Max(alignof(AA)...)) CompatibleStorage { - std::byte storage[util::Max(sizeof(AA)...)]; + std::array storage; }; } // namespace nvcv::priv diff --git a/src/nvcv/src/priv/HandleManagerImpl.hpp b/src/nvcv/src/priv/HandleManagerImpl.hpp index 4329acc9f..9b1ce7c25 100644 --- a/src/nvcv/src/priv/HandleManagerImpl.hpp +++ b/src/nvcv/src/priv/HandleManagerImpl.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -26,12 +26,13 @@ #include #include #include +#include #include #include namespace nvcv::priv { -static const char *LEAK_DETECTION_ENVVAR = "NVCV_LEAK_DETECTION"; +static const char *const LEAK_DETECTION_ENVVAR = "NVCV_LEAK_DETECTION"; template class ManagedLockFreeStack : protected LockFreeStack @@ -47,9 +48,10 @@ class ManagedLockFreeStack : protected LockFreeStack template Node *emplace(Args &&...args) { - Node *n = new Node{std::forward(args)...}; - push(n); - return n; + std::unique_ptr n = std::make_unique(std::forward(args)...); + Node *out = n.get(); + push(n.release()); + return out; } ~ManagedLockFreeStack() @@ -61,11 +63,11 @@ class ManagedLockFreeStack : protected LockFreeStack { if (Node *h = this->release()) { - while (h) + std::unique_ptr node(h); + while (node) { - auto *n = h->next; - delete h; - h = n; + std::unique_ptr next(node->next); + node = std::move(next); } } } @@ -74,7 +76,7 @@ class ManagedLockFreeStack : protected LockFreeStack template HandleManager::ResourceBase::ResourceBase() { - this->generation = 0; + generation = 0; } template @@ -88,7 +90,7 @@ void HandleManager::ResourceBase::destroyObject() { if (m_ptrObj) { - m_ptrObj->~Interface(); + std::destroy_at(m_ptrObj); m_ptrObj = nullptr; } @@ -96,10 +98,10 @@ void HandleManager::ResourceBase::destroyObject() } template -void *HandleManager::ResourceBase::getStorage() +std::byte *HandleManager::ResourceBase::getStorage() { using Resource = typename HandleManager::Impl::Resource; - return static_cast(this)->getStorage(); + return static_cast(this)->storage(); } template @@ -115,13 +117,13 @@ struct HandleManager::Impl this->destroyObject(); } - void *getStorage() + std::byte *storage() { - return m_storage; + return m_storage.data(); } private: - alignas(Storage) std::byte m_storage[sizeof(Storage)]; + alignas(Storage) std::array m_storage; }; static constexpr int kMinHandles = 1024; diff --git a/src/nvcv/src/priv/IAllocator.cpp b/src/nvcv/src/priv/IAllocator.cpp index 205ced716..44aff626d 100644 --- a/src/nvcv/src/priv/IAllocator.cpp +++ b/src/nvcv/src/priv/IAllocator.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -29,7 +29,7 @@ NVCVResourceAllocator IAllocator::get(NVCVResourceType resType) return doGet(resType); } -void *IAllocator::allocHostMem(int64_t size, int32_t align) +NVCVMemoryBuffer IAllocator::allocHostMem(int64_t size, int32_t align) { if (size < 0) { @@ -52,12 +52,12 @@ void *IAllocator::allocHostMem(int64_t size, int32_t align) return doAllocHostMem(size, align); } -void IAllocator::freeHostMem(void *ptr, int64_t size, int32_t align) noexcept +void IAllocator::freeHostMem(NVCVMemoryBuffer ptr, int64_t size, int32_t align) noexcept { doFreeHostMem(ptr, size, align); } -void *IAllocator::allocHostPinnedMem(int64_t size, int32_t align) +NVCVMemoryBuffer IAllocator::allocHostPinnedMem(int64_t size, int32_t align) { if (size < 0) { @@ -80,12 +80,12 @@ void *IAllocator::allocHostPinnedMem(int64_t size, int32_t align) return doAllocHostPinnedMem(size, align); } -void IAllocator::freeHostPinnedMem(void *ptr, int64_t size, int32_t align) noexcept +void IAllocator::freeHostPinnedMem(NVCVMemoryBuffer ptr, int64_t size, int32_t align) noexcept { doFreeHostPinnedMem(ptr, size, align); } -void *IAllocator::allocCudaMem(int64_t size, int32_t align) +NVCVMemoryBuffer IAllocator::allocCudaMem(int64_t size, int32_t align) { if (size < 0) { @@ -108,7 +108,7 @@ void *IAllocator::allocCudaMem(int64_t size, int32_t align) return doAllocCudaMem(size, align); } -void IAllocator::freeCudaMem(void *ptr, int64_t size, int32_t align) noexcept +void IAllocator::freeCudaMem(NVCVMemoryBuffer ptr, int64_t size, int32_t align) noexcept { doFreeCudaMem(ptr, size, align); } diff --git a/src/nvcv/src/priv/IAllocator.hpp b/src/nvcv/src/priv/IAllocator.hpp index 5e72a9a3e..b3cf690b7 100644 --- a/src/nvcv/src/priv/IAllocator.hpp +++ b/src/nvcv/src/priv/IAllocator.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -29,27 +29,27 @@ namespace nvcv::priv { class IAllocator : public ICoreObjectHandle { public: - void *allocHostMem(int64_t size, int32_t align); - void freeHostMem(void *ptr, int64_t size, int32_t align) noexcept; + NVCVMemoryBuffer allocHostMem(int64_t size, int32_t align); + void freeHostMem(NVCVMemoryBuffer ptr, int64_t size, int32_t align) noexcept; - void *allocHostPinnedMem(int64_t size, int32_t align); - void freeHostPinnedMem(void *ptr, int64_t size, int32_t align) noexcept; + NVCVMemoryBuffer allocHostPinnedMem(int64_t size, int32_t align); + void freeHostPinnedMem(NVCVMemoryBuffer ptr, int64_t size, int32_t align) noexcept; - void *allocCudaMem(int64_t size, int32_t align); - void freeCudaMem(void *ptr, int64_t size, int32_t align) noexcept; + NVCVMemoryBuffer allocCudaMem(int64_t size, int32_t align); + void freeCudaMem(NVCVMemoryBuffer ptr, int64_t size, int32_t align) noexcept; NVCVResourceAllocator get(NVCVResourceType resType); private: // NVI idiom - virtual void *doAllocHostMem(int64_t size, int32_t align) = 0; - virtual void doFreeHostMem(void *ptr, int64_t size, int32_t align) noexcept = 0; + virtual NVCVMemoryBuffer doAllocHostMem(int64_t size, int32_t align) = 0; + virtual void doFreeHostMem(NVCVMemoryBuffer ptr, int64_t size, int32_t align) noexcept = 0; - virtual void *doAllocHostPinnedMem(int64_t size, int32_t align) = 0; - virtual void doFreeHostPinnedMem(void *ptr, int64_t size, int32_t align) noexcept = 0; + virtual NVCVMemoryBuffer doAllocHostPinnedMem(int64_t size, int32_t align) = 0; + virtual void doFreeHostPinnedMem(NVCVMemoryBuffer ptr, int64_t size, int32_t align) noexcept = 0; - virtual void *doAllocCudaMem(int64_t size, int32_t align) = 0; - virtual void doFreeCudaMem(void *ptr, int64_t size, int32_t align) noexcept = 0; + virtual NVCVMemoryBuffer doAllocCudaMem(int64_t size, int32_t align) = 0; + virtual void doFreeCudaMem(NVCVMemoryBuffer ptr, int64_t size, int32_t align) noexcept = 0; virtual NVCVResourceAllocator doGet(NVCVResourceType resType) = 0; }; @@ -57,7 +57,7 @@ class IAllocator : public ICoreObjectHandle template std::unique_ptr AllocHostObj(IAllocator &alloc, ARGS &&...args) { - void *arena = alloc.allocHostMem(sizeof(T), alignof(T)); + NVCVMemoryBuffer arena = alloc.allocHostMem(sizeof(T), alignof(T)); try { return std::unique_ptr{new (arena) T{std::forward(args)...}}; @@ -75,7 +75,7 @@ void FreeHostObj(IAllocator &alloc, T *ptr) noexcept if (ptr != nullptr) { ptr->~T(); - alloc.freeHostMem(ptr, sizeof(T), alignof(T)); + alloc.freeHostMem(reinterpret_cast(ptr), sizeof(T), alignof(T)); } } diff --git a/src/nvcv/src/priv/IContext.hpp b/src/nvcv/src/priv/IContext.hpp index db20f5532..103b2cc9a 100644 --- a/src/nvcv/src/priv/IContext.hpp +++ b/src/nvcv/src/priv/IContext.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -43,6 +43,8 @@ class IContext using Managers = std::tuple; + virtual ~IContext() = default; + template CoreObjManager &manager() { diff --git a/src/nvcv/src/priv/ICoreObject.hpp b/src/nvcv/src/priv/ICoreObject.hpp index 22edbd6fb..6d967e3ff 100644 --- a/src/nvcv/src/priv/ICoreObject.hpp +++ b/src/nvcv/src/priv/ICoreObject.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -48,21 +48,24 @@ class alignas(kResourceAlignment) ICoreObject { public: // Disable copy/move to avoid slicing. - ICoreObject(const ICoreObject &) = delete; + ICoreObject(const ICoreObject &) = delete; + ICoreObject(ICoreObject &&) = delete; + ICoreObject &operator=(const ICoreObject &) = delete; + ICoreObject &operator=(ICoreObject &&) = delete; virtual ~ICoreObject() = default; virtual Version version() const = 0; - virtual void setUserPointer(void *ptr) = 0; - virtual void *userPointer() const = 0; + virtual void setUserPointer(NVCVUserPointer ptr) = 0; + virtual NVCVUserPointer userPointer() const = 0; protected: ICoreObject() = default; }; template -class IHandleHolder +class IHandleHolder // NOSONAR: this interface participates in the stable core-object ABI; do not add virtual slots. { public: using HandleType = HANDLE; @@ -104,19 +107,19 @@ class CoreObjectBase : public Interface return CURRENT_VERSION; } - void setUserPointer(void *ptr) final + void setUserPointer(NVCVUserPointer ptr) final { m_userPtr = ptr; } - void *userPointer() const final + NVCVUserPointer userPointer() const final { return m_userPtr; } private: - HandleType m_handle = {}; - void *m_userPtr = nullptr; + HandleType m_handle = {}; + NVCVUserPointer m_userPtr = nullptr; }; template diff --git a/src/nvcv/src/priv/IImageBatch.hpp b/src/nvcv/src/priv/IImageBatch.hpp index be78d77c1..6063b77d8 100644 --- a/src/nvcv/src/priv/IImageBatch.hpp +++ b/src/nvcv/src/priv/IImageBatch.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -44,8 +44,8 @@ class IImageBatch : public ICoreObjectHandle class IImageBatchVarShape : public IImageBatch { public: - virtual void pushImages(const NVCVImageHandle *images, int32_t numImages) = 0; - virtual void pushImages(NVCVPushImageFunc cbPushImage, void *ctxCallback) = 0; + virtual void pushImages(const NVCVImageHandle *images, int32_t numImages) = 0; + virtual void pushImages(NVCVPushImageFunc cbPushImage, NVCVUserPointer ctxCallback) = 0; virtual void popImages(int32_t numImages) = 0; diff --git a/src/nvcv/src/priv/Image.cpp b/src/nvcv/src/priv/Image.cpp index 9cccf661e..ef043525c 100644 --- a/src/nvcv/src/priv/Image.cpp +++ b/src/nvcv/src/priv/Image.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -62,22 +62,22 @@ NVCVImageRequirements Image::CalcRequirements(Size2D size, ImageFormat fmt, int3 // Pitch alignment must be compatible with each plane's pixel stride. for (int p = 0; p < fmt.numPlanes(); ++p) { - int rowAlign; + int planeAlign; if (userRowAlign == 0) { // Safest thing we can do - rowAlign = fmt.planePixelStrideBytes(p); + planeAlign = fmt.planePixelStrideBytes(p); } else { // Strictest thing we can do - rowAlign = fmt.planeRowAlignment(p); + planeAlign = fmt.planeRowAlignment(p); } - rowAlign = std::lcm(rowAlign, rowAlign); + rowAlign = std::lcm(rowAlign, planeAlign); } - rowAlign = util::RoundUpNextPowerOfTwo(rowAlign); + rowAlign = static_cast(util::RoundUpNextPowerOfTwo(rowAlign)); int baseAlign; if (userBaseAlign == 0) @@ -112,7 +112,8 @@ NVCVImageRequirements Image::CalcRequirements(Size2D size, ImageFormat fmt, int3 NVCV_ASSERT((size_t)p < sizeof(reqs.planeRowStride) / sizeof(reqs.planeRowStride[0])); - reqs.planeRowStride[p] = util::RoundUpPowerOfTwo((int64_t)planeSize.w * fmt.planePixelStrideBytes(p), rowAlign); + reqs.planeRowStride[p] = static_cast( + util::RoundUpPowerOfTwo((int64_t)planeSize.w * fmt.planePixelStrideBytes(p), rowAlign)); AddBuffer(reqs.mem.cudaMem, (int64_t)reqs.planeRowStride[p] * planeSize.h, baseAlign); } @@ -120,7 +121,7 @@ NVCVImageRequirements Image::CalcRequirements(Size2D size, ImageFormat fmt, int3 return reqs; } -void *Image::AllocateBuffer(IAllocator &alloc, const NVCVImageRequirements &reqs) +NVCVByte *Image::AllocateBuffer(IAllocator &alloc, const NVCVImageRequirements &reqs) { if (ImageFormat{reqs.format}.memLayout() != NVCV_MEM_LAYOUT_PL) { @@ -128,7 +129,7 @@ void *Image::AllocateBuffer(IAllocator &alloc, const NVCVImageRequirements &reqs } int64_t bufSize = CalcTotalSizeBytes(reqs.mem.cudaMem); - void *buffer = alloc.allocCudaMem(bufSize, reqs.alignBytes); + auto *buffer = static_cast(static_cast(alloc.allocCudaMem(bufSize, reqs.alignBytes))); NVCV_ASSERT(buffer != nullptr); return buffer; } @@ -144,7 +145,8 @@ Image::Image(NVCVImageRequirements reqs, IAllocator &alloc) Image::~Image() { - m_alloc->freeCudaMem(m_memBuffer, CalcTotalSizeBytes(m_reqs.mem.cudaMem), m_reqs.alignBytes); + m_alloc->freeCudaMem(static_cast(static_cast(m_memBuffer)), + CalcTotalSizeBytes(m_reqs.mem.cudaMem), m_reqs.alignBytes); } NVCVTypeImage Image::type() const @@ -189,7 +191,7 @@ void Image::exportData(NVCVImageData &data) const plane.width = planeSize.w; plane.height = planeSize.h; plane.rowStride = m_reqs.planeRowStride[p]; - plane.basePtr = reinterpret_cast(m_memBuffer) + planeOffsetBytes; + plane.basePtr = m_memBuffer + planeOffsetBytes; planeOffsetBytes += (int64_t)plane.height * plane.rowStride; } @@ -201,13 +203,12 @@ void Image::exportData(NVCVImageData &data) const // ImageWrap implementation ------------------------------------------- -ImageWrapData::ImageWrapData(const NVCVImageData &data, NVCVImageDataCleanupFunc cleanup, void *ctxCleanup) - : m_cleanup(cleanup) +ImageWrapData::ImageWrapData(const NVCVImageData &data, NVCVImageDataCleanupFunc cleanup, NVCVUserPointer ctxCleanup) + : m_data(data) + , m_cleanup(cleanup) , m_ctxCleanup(ctxCleanup) { - doValidateData(data); - - m_data = data; + doValidateData(m_data); } ImageWrapData::~ImageWrapData() @@ -234,6 +235,11 @@ void ImageWrapData::doValidateData(const NVCVImageData &data) const throw Exception(NVCV_ERROR_INVALID_ARGUMENT) << "Number of planes must be >= 1, not " << data.buffer.strided.numPlanes; } + if (data.buffer.strided.numPlanes > NVCV_MAX_PLANE_COUNT) + { + throw Exception(NVCV_ERROR_INVALID_ARGUMENT) + << "Number of planes must be <= " << NVCV_MAX_PLANE_COUNT << ", not " << data.buffer.strided.numPlanes; + } for (int p = 0; p < data.buffer.strided.numPlanes; ++p) { @@ -248,6 +254,14 @@ void ImageWrapData::doValidateData(const NVCVImageData &data) const { throw Exception(NVCV_ERROR_INVALID_ARGUMENT) << "Plane #" << p << "'s base pointer must not be NULL"; } + + int64_t minRowStride = (int64_t)plane.width * format.planePixelStrideBytes(p); + if (plane.rowStride < minRowStride) + { + throw Exception(NVCV_ERROR_INVALID_ARGUMENT) + << "Plane #" << p << "'s row stride " << plane.rowStride << " is smaller than the minimum required " + << minRowStride; + } } success = true; break; @@ -271,7 +285,7 @@ void ImageWrapData::doValidateData(const NVCVImageData &data) const SharedCoreObj ImageWrapData::alloc() const { - return GetDefaultAllocator(); + return SharedCoreObj{GetDefaultAllocator()}; } Size2D ImageWrapData::size() const diff --git a/src/nvcv/src/priv/Image.hpp b/src/nvcv/src/priv/Image.hpp index 45fc25aaa..2290b4fc7 100644 --- a/src/nvcv/src/priv/Image.hpp +++ b/src/nvcv/src/priv/Image.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -28,7 +28,7 @@ class Image final : public CoreObjectBase { public: explicit Image(NVCVImageRequirements reqs, IAllocator &alloc); - ~Image(); + ~Image() override; static NVCVImageRequirements CalcRequirements(Size2D size, ImageFormat fmt, int32_t baseAlign, int32_t rowAlign); @@ -40,19 +40,19 @@ class Image final : public CoreObjectBase void exportData(NVCVImageData &data) const override; private: - static void *AllocateBuffer(IAllocator &alloc, const NVCVImageRequirements &reqs); + static NVCVByte *AllocateBuffer(IAllocator &alloc, const NVCVImageRequirements &reqs); SharedCoreObj m_alloc; NVCVImageRequirements m_reqs; - void *m_memBuffer; + NVCVByte *m_memBuffer; }; class ImageWrapData final : public CoreObjectBase { public: - explicit ImageWrapData(const NVCVImageData &data, NVCVImageDataCleanupFunc cleanup, void *ctxCleanup); + explicit ImageWrapData(const NVCVImageData &data, NVCVImageDataCleanupFunc cleanup, NVCVUserPointer ctxCleanup); - ~ImageWrapData(); + ~ImageWrapData() override; Size2D size() const override; ImageFormat format() const override; @@ -65,7 +65,7 @@ class ImageWrapData final : public CoreObjectBase NVCVImageData m_data; NVCVImageDataCleanupFunc m_cleanup; - void *m_ctxCleanup; + NVCVUserPointer m_ctxCleanup; void doCleanup() noexcept; diff --git a/src/nvcv/src/priv/ImageBatchVarShape.cpp b/src/nvcv/src/priv/ImageBatchVarShape.cpp index 67a449f14..94b8cab44 100644 --- a/src/nvcv/src/priv/ImageBatchVarShape.cpp +++ b/src/nvcv/src/priv/ImageBatchVarShape.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -41,11 +41,11 @@ NVCVImageBatchVarShapeRequirements ImageBatchVarShape::CalcRequirements(int32_t reqs.capacity = capacity; reqs.mem = {}; - reqs.alignBytes = alignof(NVCVImageBufferStrided); - reqs.alignBytes = std::lcm(alignof(NVCVImageHandle), reqs.alignBytes); - reqs.alignBytes = std::lcm(alignof(NVCVImageFormat), reqs.alignBytes); + reqs.alignBytes = static_cast(alignof(NVCVImageBufferStrided)); + reqs.alignBytes = static_cast(std::lcm(alignof(NVCVImageHandle), static_cast(reqs.alignBytes))); + reqs.alignBytes = static_cast(std::lcm(alignof(NVCVImageFormat), static_cast(reqs.alignBytes))); - reqs.alignBytes = util::RoundUpNextPowerOfTwo(reqs.alignBytes); + reqs.alignBytes = static_cast(util::RoundUpNextPowerOfTwo(reqs.alignBytes)); if (reqs.alignBytes > NVCV_MAX_MEM_REQUIREMENTS_BLOCK_SIZE) { @@ -68,36 +68,31 @@ NVCVImageBatchVarShapeRequirements ImageBatchVarShape::CalcRequirements(int32_t ImageBatchVarShape::ImageBatchVarShape(NVCVImageBatchVarShapeRequirements reqs, IAllocator &alloc) : m_alloc{alloc} , m_reqs{std::move(reqs)} - , m_dirtyStartingFromIndex(0) - , m_numImages(0) - , m_cacheMaxSize{Size2D{0,0}} { - m_evPostFence = nullptr; - m_devImagesBuffer = m_hostImagesBuffer = nullptr; - m_devFormatsBuffer = m_hostFormatsBuffer = nullptr; - m_imgHandleBuffer = nullptr; - int64_t bufImagesSize = m_reqs.capacity * sizeof(NVCVImageBufferStrided); int64_t bufFormatsSize = m_reqs.capacity * sizeof(NVCVImageFormat); int64_t imgHandlesSize = m_reqs.capacity * sizeof(NVCVImageHandle); try { - m_devImagesBuffer - = static_cast(m_alloc->allocCudaMem(bufImagesSize, m_reqs.alignBytes)); + m_devImagesBuffer = static_cast( + static_cast(m_alloc->allocCudaMem(bufImagesSize, m_reqs.alignBytes))); NVCV_ASSERT(m_devImagesBuffer != nullptr); - m_hostImagesBuffer - = static_cast(m_alloc->allocHostMem(bufImagesSize, m_reqs.alignBytes)); - NVCV_ASSERT(m_devImagesBuffer != nullptr); + m_hostImagesBuffer = static_cast( + static_cast(m_alloc->allocHostMem(bufImagesSize, m_reqs.alignBytes))); + NVCV_ASSERT(m_hostImagesBuffer != nullptr); - m_devFormatsBuffer = static_cast(m_alloc->allocCudaMem(bufFormatsSize, m_reqs.alignBytes)); + m_devFormatsBuffer = static_cast( + static_cast(m_alloc->allocCudaMem(bufFormatsSize, m_reqs.alignBytes))); NVCV_ASSERT(m_devFormatsBuffer != nullptr); - m_hostFormatsBuffer = static_cast(m_alloc->allocHostMem(bufFormatsSize, m_reqs.alignBytes)); - NVCV_ASSERT(m_devFormatsBuffer != nullptr); + m_hostFormatsBuffer = static_cast( + static_cast(m_alloc->allocHostMem(bufFormatsSize, m_reqs.alignBytes))); + NVCV_ASSERT(m_hostFormatsBuffer != nullptr); - m_imgHandleBuffer = static_cast(m_alloc->allocHostMem(imgHandlesSize, m_reqs.alignBytes)); + m_imgHandleBuffer = static_cast( + static_cast(m_alloc->allocHostMem(imgHandlesSize, m_reqs.alignBytes))); NVCV_ASSERT(m_imgHandleBuffer != nullptr); NVCV_CHECK_THROW(cudaEventCreateWithFlags(&m_evPostFence, cudaEventDisableTiming)); @@ -109,13 +104,7 @@ ImageBatchVarShape::ImageBatchVarShape(NVCVImageBatchVarShapeRequirements reqs, NVCV_CHECK_LOG(cudaEventDestroy(m_evPostFence)); } - m_alloc->freeCudaMem(m_devImagesBuffer, bufImagesSize, m_reqs.alignBytes); - m_alloc->freeHostMem(m_hostImagesBuffer, bufImagesSize, m_reqs.alignBytes); - - m_alloc->freeCudaMem(m_devFormatsBuffer, bufFormatsSize, m_reqs.alignBytes); - m_alloc->freeHostMem(m_hostFormatsBuffer, bufFormatsSize, m_reqs.alignBytes); - - m_alloc->freeHostMem(m_imgHandleBuffer, imgHandlesSize, m_reqs.alignBytes); + freeBuffers(); throw; } } @@ -125,19 +114,29 @@ ImageBatchVarShape::~ImageBatchVarShape() NVCV_CHECK_LOG(cudaEventSynchronize(m_evPostFence)); clear(); + freeBuffers(); + + NVCV_CHECK_LOG(cudaEventDestroy(m_evPostFence)); +} + +void ImageBatchVarShape::freeBuffers() noexcept +{ int64_t bufImagesSize = m_reqs.capacity * sizeof(NVCVImageBufferStrided); int64_t bufFormatsSize = m_reqs.capacity * sizeof(NVCVImageFormat); int64_t imgHandlesSize = m_reqs.capacity * sizeof(NVCVImageHandle); - m_alloc->freeCudaMem(m_devImagesBuffer, bufImagesSize, m_reqs.alignBytes); - m_alloc->freeHostMem(m_hostImagesBuffer, bufImagesSize, m_reqs.alignBytes); + m_alloc->freeCudaMem(static_cast(static_cast(m_devImagesBuffer)), bufImagesSize, + m_reqs.alignBytes); + m_alloc->freeHostMem(static_cast(static_cast(m_hostImagesBuffer)), bufImagesSize, + m_reqs.alignBytes); - m_alloc->freeCudaMem(m_devFormatsBuffer, bufFormatsSize, m_reqs.alignBytes); - m_alloc->freeHostMem(m_hostFormatsBuffer, bufFormatsSize, m_reqs.alignBytes); + m_alloc->freeCudaMem(static_cast(static_cast(m_devFormatsBuffer)), bufFormatsSize, + m_reqs.alignBytes); + m_alloc->freeHostMem(static_cast(static_cast(m_hostFormatsBuffer)), bufFormatsSize, + m_reqs.alignBytes); - m_alloc->freeHostMem(m_imgHandleBuffer, imgHandlesSize, m_reqs.alignBytes); - - NVCV_CHECK_LOG(cudaEventDestroy(m_evPostFence)); + m_alloc->freeHostMem(static_cast(static_cast(m_imgHandleBuffer)), imgHandlesSize, + m_reqs.alignBytes); } NVCVTypeImageBatch ImageBatchVarShape::type() const @@ -193,7 +192,7 @@ void ImageBatchVarShape::doUpdateCache() const m_cacheMaxSize->h = std::max(m_cacheMaxSize->h, m_hostImagesBuffer[i].planes[0].height); } - constexpr ImageFormat fmt_none = ImageFormat{NVCV_IMAGE_FORMAT_NONE}; + constexpr auto fmt_none = ImageFormat{NVCV_IMAGE_FORMAT_NONE}; if (!m_cacheUniqueFormat) { @@ -201,7 +200,7 @@ void ImageBatchVarShape::doUpdateCache() const } else if (*m_cacheUniqueFormat != fmt_none && *m_cacheUniqueFormat != ImageFormat{m_hostFormatsBuffer[i]}) { - *m_cacheUniqueFormat = fmt_none; + m_cacheUniqueFormat = fmt_none; } } @@ -224,10 +223,20 @@ void ImageBatchVarShape::exportData(CUstream stream, NVCVImageBatchData &data) c NVCV_ASSERT(m_dirtyStartingFromIndex <= m_numImages); - if (m_dirtyStartingFromIndex < m_numImages) + // Gate every reader stream against the most recent producer-stream H2D of + // m_devImagesBuffer/m_devFormatsBuffer. cvcuda Streams are created with + // cudaStreamNonBlocking, so a user stream does NOT implicitly synchronize + // with the legacy default stream that performed the original copy when + // the batch was first exported. Without this wait, a kernel queued on + // user stream can read m_devImagesBuffer before the prior copy is visible + // and see garbage basePtr/rowStride/width/height for some image entries. + if (m_numImages > 0) { NVCV_CHECK_THROW(cudaStreamWaitEvent(stream, m_evPostFence)); + } + if (m_dirtyStartingFromIndex < m_numImages) + { NVCV_CHECK_THROW(cudaMemcpyAsync( m_devImagesBuffer + m_dirtyStartingFromIndex, m_hostImagesBuffer + m_dirtyStartingFromIndex, (m_numImages - m_dirtyStartingFromIndex) * sizeof(*m_devImagesBuffer), cudaMemcpyHostToDevice, stream)); @@ -288,7 +297,7 @@ void ImageBatchVarShape::pushImages(const NVCVImageHandle *images, int32_t numIm } } -void ImageBatchVarShape::pushImages(NVCVPushImageFunc cbPushImage, void *ctxCallback) +void ImageBatchVarShape::pushImages(NVCVPushImageFunc cbPushImage, NVCVUserPointer ctxCallback) { if (cbPushImage == nullptr) { @@ -326,7 +335,7 @@ void ImageBatchVarShape::doPushImage(NVCVImageHandle imgHandle) { NVCV_ASSERT(m_numImages < m_reqs.capacity); - auto &img = ToStaticRef(imgHandle); + const auto &img = ToStaticRef(imgHandle); if (img.format().memLayout() != NVCV_MEM_LAYOUT_PL) { diff --git a/src/nvcv/src/priv/ImageBatchVarShape.hpp b/src/nvcv/src/priv/ImageBatchVarShape.hpp index 3bbb803b9..2bc92d80a 100644 --- a/src/nvcv/src/priv/ImageBatchVarShape.hpp +++ b/src/nvcv/src/priv/ImageBatchVarShape.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -32,7 +32,7 @@ class ImageBatchVarShape final : public CoreObjectBase { public: explicit ImageBatchVarShape(NVCVImageBatchVarShapeRequirements reqs, IAllocator &alloc); - ~ImageBatchVarShape(); + ~ImageBatchVarShape() override; static NVCVImageBatchVarShapeRequirements CalcRequirements(int32_t capacity); @@ -51,7 +51,7 @@ class ImageBatchVarShape final : public CoreObjectBase void exportData(CUstream stream, NVCVImageBatchData &data) const override; void pushImages(const NVCVImageHandle *images, int32_t numImages) override; - void pushImages(NVCVPushImageFunc cbPushImage, void *ctxCallback) override; + void pushImages(NVCVPushImageFunc cbPushImage, NVCVUserPointer ctxCallback) override; void popImages(int32_t numImages) override; void clear() override; @@ -59,26 +59,27 @@ class ImageBatchVarShape final : public CoreObjectBase SharedCoreObj m_alloc; NVCVImageBatchVarShapeRequirements m_reqs; - mutable int32_t m_dirtyStartingFromIndex; + mutable int32_t m_dirtyStartingFromIndex = 0; - int32_t m_numImages; - NVCVImageBufferStrided *m_hostImagesBuffer; - NVCVImageBufferStrided *m_devImagesBuffer; + int32_t m_numImages = 0; + NVCVImageBufferStrided *m_hostImagesBuffer = nullptr; + NVCVImageBufferStrided *m_devImagesBuffer = nullptr; - NVCVImageFormat *m_hostFormatsBuffer; - NVCVImageFormat *m_devFormatsBuffer; + NVCVImageFormat *m_hostFormatsBuffer = nullptr; + NVCVImageFormat *m_devFormatsBuffer = nullptr; - NVCVImageHandle *m_imgHandleBuffer; + NVCVImageHandle *m_imgHandleBuffer = nullptr; // Max width/height up to m_numImages. // If nullopt, must be recalculated from the beginning. - mutable std::optional m_cacheMaxSize; + mutable std::optional m_cacheMaxSize = Size2D{0, 0}; mutable std::optional m_cacheUniqueFormat; void doUpdateCache() const; + void freeBuffers() noexcept; - // TODO: must be retrieved from the resource allocator; - cudaEvent_t m_evPostFence; + // REVISIT: must be retrieved from the resource allocator; + cudaEvent_t m_evPostFence = nullptr; // Assumes there's enough space for image. // Does not update dirty count diff --git a/src/nvcv/src/priv/ImageFormat.cpp b/src/nvcv/src/priv/ImageFormat.cpp index 593f6ecd9..268d08004 100644 --- a/src/nvcv/src/priv/ImageFormat.cpp +++ b/src/nvcv/src/priv/ImageFormat.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -26,6 +26,9 @@ #include #include +#include +#include +#include #include #include #include @@ -65,7 +68,7 @@ static void ValidateSwizzlePacking(NVCVSwizzle swizzle, NVCVPacking packing0, NV } int swchannels = GetNumChannels(swizzle); - NVCVPacking packing[] = {packing0, packing1, packing2, packing3}; + std::array packing = {packing0, packing1, packing2, packing3}; int packchannels = 0; for (int i = 0; i < 4; ++i) @@ -81,14 +84,11 @@ static void ValidateSwizzlePacking(NVCVSwizzle swizzle, NVCVPacking packing0, NV "Packing of 3rd plane must have at most 64 bits per pixel"); } } - else if (i > 0) + else if (i > 0 && GetBitsPerPixel(packing[i]) > 128) { // only packing 0 can have more than 128 bpp - if (GetBitsPerPixel(packing[i]) > 128) - { - throw Exception(NVCV_ERROR_INVALID_ARGUMENT, - "Packing of plane other than 1st must have at most 128 bits per pixel"); - } + throw Exception(NVCV_ERROR_INVALID_ARGUMENT, + "Packing of plane other than 1st must have at most 128 bits per pixel"); } // packing 1 can't have more than 2 channels @@ -139,6 +139,102 @@ static void ValidateExtraChannelInfo(const NVCVExtraChannelInfo *exChannelInfo, } } +static bool HasExtraChannels(const NVCVExtraChannelInfo *exChannelInfo) +{ + return exChannelInfo != nullptr && exChannelInfo->numChannels > 0; +} + +static NVCVImageFormat MakeYCbCrImageFormat(ColorSpec colorSpec, NVCVChromaSubsampling chromaSub, + NVCVMemLayout memLayout, NVCVDataKind dataKind, NVCVSwizzle swizzle, + NVCVAlphaType alphaType, const NVCVExtraChannelInfo *exChannelInfo, + NVCVPacking packing0, NVCVPacking packing1, NVCVPacking packing2, + NVCVPacking packing3) +{ + auto cspec = static_cast(colorSpec); + if (HasExtraChannels(exChannelInfo)) + { + return NVCV_MAKE_YCbCr_IMAGE_EXTRA_CHANNELS_FORMAT(cspec, chromaSub, memLayout, dataKind, swizzle, alphaType, + exChannelInfo->numChannels, exChannelInfo->bitsPerPixel, + exChannelInfo->datakind, exChannelInfo->channelType, 4, + packing0, packing1, packing2, packing3); + } + + return NVCV_MAKE_YCbCr_IMAGE_FORMAT(cspec, chromaSub, memLayout, dataKind, swizzle, alphaType, 4, packing0, + packing1, packing2, packing3); +} + +static NVCVImageFormat MakeColorImageFormat(NVCVColorModel colorModel, ColorSpec colorSpec, NVCVMemLayout memLayout, + NVCVDataKind dataKind, NVCVSwizzle swizzle, NVCVAlphaType alphaType, + const NVCVExtraChannelInfo *exChannelInfo, NVCVPacking packing0, + NVCVPacking packing1, NVCVPacking packing2, NVCVPacking packing3) +{ + auto cspec = static_cast(colorSpec); + if (HasExtraChannels(exChannelInfo)) + { + return NVCV_MAKE_COLOR_IMAGE_EXTRA_CHANNELS_FORMAT(colorModel, cspec, memLayout, dataKind, swizzle, alphaType, + exChannelInfo->numChannels, exChannelInfo->bitsPerPixel, + exChannelInfo->datakind, exChannelInfo->channelType, 4, + packing0, packing1, packing2, packing3); + } + + return NVCV_MAKE_COLOR_IMAGE_FORMAT(colorModel, cspec, memLayout, dataKind, swizzle, alphaType, 4, packing0, + packing1, packing2, packing3); +} + +static NVCVImageFormat MakeRawImageFormat(NVCVRawPattern rawPattern, NVCVMemLayout memLayout, NVCVDataKind dataKind, + NVCVSwizzle swizzle, NVCVAlphaType alphaType, + const NVCVExtraChannelInfo *exChannelInfo, NVCVPacking packing0, + NVCVPacking packing1, NVCVPacking packing2, NVCVPacking packing3) +{ + if (HasExtraChannels(exChannelInfo)) + { + return NVCV_MAKE_RAW_IMAGE_EXTRA_CHANNELS_FORMAT(rawPattern, memLayout, dataKind, swizzle, alphaType, + exChannelInfo->numChannels, exChannelInfo->bitsPerPixel, + exChannelInfo->datakind, exChannelInfo->channelType, 4, + packing0, packing1, packing2, packing3); + } + + return NVCV_MAKE_RAW_IMAGE_FORMAT(rawPattern, memLayout, dataKind, swizzle, alphaType, 4, packing0, packing1, + packing2, packing3); +} + +static NVCVImageFormat MakeNonColorImageFormat(NVCVMemLayout memLayout, NVCVDataKind dataKind, NVCVSwizzle swizzle, + NVCVAlphaType alphaType, const NVCVExtraChannelInfo *exChannelInfo, + NVCVPacking packing0, NVCVPacking packing1, NVCVPacking packing2, + NVCVPacking packing3) +{ + if (HasExtraChannels(exChannelInfo)) + { + return NVCV_MAKE_NONCOLOR_IMAGE_EXTRA_CHANNELS_FORMAT( + memLayout, dataKind, swizzle, alphaType, exChannelInfo->numChannels, exChannelInfo->bitsPerPixel, + exChannelInfo->datakind, exChannelInfo->channelType, 4, packing0, packing1, packing2, packing3); + } + + return NVCV_MAKE_NONCOLOR_IMAGE_FORMAT(memLayout, dataKind, swizzle, alphaType, 4, packing0, packing1, packing2, + packing3); +} + +static void ValidateColorSpecForModel(NVCVColorModel colorModel, ColorSpec colorSpec) +{ + switch (colorModel) + { + case NVCV_COLOR_MODEL_RGB: + case NVCV_COLOR_MODEL_YCCK: + case NVCV_COLOR_MODEL_CMYK: + case NVCV_COLOR_MODEL_YCbCr: + return; + + default: + break; + } + + if (static_cast(colorSpec) != NVCV_COLOR_SPEC_UNDEFINED) + { + throw Exception(NVCV_ERROR_INVALID_ARGUMENT) + << "When color model is not RGB or YCbCr, colorspec must be undefined, not " << colorSpec; + } +} + ImageFormat::ImageFormat(NVCVColorModel colorModel, ColorSpec colorSpec, NVCVChromaSubsampling chromaSub, NVCVMemLayout memLayout, NVCVDataKind dataKind, NVCVSwizzle swizzle, NVCVPacking packing0, NVCVPacking packing1, NVCVPacking packing2, NVCVPacking packing3, NVCVAlphaType alphaType, @@ -153,86 +249,31 @@ ImageFormat::ImageFormat(NVCVColorModel colorModel, ColorSpec colorSpec, NVCVChr } if (colorModel == NVCV_COLOR_MODEL_UNDEFINED - && (colorSpec != NVCV_COLOR_SPEC_UNDEFINED || chromaSub != NVCV_CSS_NONE)) + && (static_cast(colorSpec) != NVCV_COLOR_SPEC_UNDEFINED || chromaSub != NVCV_CSS_NONE)) { throw Exception(NVCV_ERROR_INVALID_ARGUMENT) << "If color model is undefined," << " colorspec must be undefined (not " << colorSpec << " and chroma subsampling must be none (not " << chromaSub << ")"; } - else - { - switch (colorModel) - { - case NVCV_COLOR_MODEL_RGB: - case NVCV_COLOR_MODEL_YCCK: - case NVCV_COLOR_MODEL_CMYK: - case NVCV_COLOR_MODEL_YCbCr: - break; - default: - if (colorSpec != NVCV_COLOR_SPEC_UNDEFINED) - { - throw Exception(NVCV_ERROR_INVALID_ARGUMENT) - << "When color model is not RGB or YCbCr, colorspec must be undefined, not " << colorSpec; - } - break; - } + ValidateColorSpecForModel(colorModel, colorSpec); + ValidateExtraChannelInfo(exChannelInfo, packing0, packing1, packing2, packing3); - ValidateExtraChannelInfo(exChannelInfo, packing0, packing1, packing2, packing3); + if (colorModel == NVCV_COLOR_MODEL_YCbCr) + { + m_format = MakeYCbCrImageFormat(colorSpec, chromaSub, memLayout, dataKind, swizzle, alphaType, exChannelInfo, + packing0, packing1, packing2, packing3); + return; + } - if (colorModel == NVCV_COLOR_MODEL_YCbCr) - { - if (exChannelInfo == nullptr) - { - m_format = NVCV_MAKE_YCbCr_IMAGE_FORMAT(colorSpec, chromaSub, memLayout, dataKind, swizzle, alphaType, - 4, packing0, packing1, packing2, packing3); - } - else - { - if (exChannelInfo->numChannels > 0) - { - m_format = NVCV_MAKE_YCbCr_IMAGE_EXTRA_CHANNELS_FORMAT( - colorSpec, chromaSub, memLayout, dataKind, swizzle, alphaType, exChannelInfo->numChannels, - exChannelInfo->bitsPerPixel, exChannelInfo->datakind, exChannelInfo->channelType, 4, packing0, - packing1, packing2, packing3); - } - else - { - m_format = NVCV_MAKE_YCbCr_IMAGE_FORMAT(colorSpec, chromaSub, memLayout, dataKind, swizzle, - alphaType, 4, packing0, packing1, packing2, packing3); - } - } - } - else if (chromaSub != NVCV_CSS_NONE) - { - throw Exception(NVCV_ERROR_INVALID_ARGUMENT) - << "When color model isn't YCbCr, chroma subsampling must be NONE"; - } - else - { - if (exChannelInfo == nullptr) - { - m_format = NVCV_MAKE_COLOR_IMAGE_FORMAT(colorModel, colorSpec, memLayout, dataKind, swizzle, alphaType, - 4, packing0, packing1, packing2, packing3); - } - else - { - if (exChannelInfo->numChannels > 0) - { - m_format = NVCV_MAKE_COLOR_IMAGE_EXTRA_CHANNELS_FORMAT( - colorModel, colorSpec, memLayout, dataKind, swizzle, alphaType, exChannelInfo->numChannels, - exChannelInfo->bitsPerPixel, exChannelInfo->datakind, exChannelInfo->channelType, 4, packing0, - packing1, packing2, packing3); - } - else - { - m_format = NVCV_MAKE_COLOR_IMAGE_FORMAT(colorModel, colorSpec, memLayout, dataKind, swizzle, - alphaType, 4, packing0, packing1, packing2, packing3); - } - } - } + if (chromaSub != NVCV_CSS_NONE) + { + throw Exception(NVCV_ERROR_INVALID_ARGUMENT) << "When color model isn't YCbCr, chroma subsampling must be NONE"; } + + m_format = MakeColorImageFormat(colorModel, colorSpec, memLayout, dataKind, swizzle, alphaType, exChannelInfo, + packing0, packing1, packing2, packing3); } ImageFormat::ImageFormat(NVCVRawPattern rawPattern, NVCVMemLayout memLayout, NVCVDataKind dataKind, NVCVSwizzle swizzle, @@ -242,26 +283,8 @@ ImageFormat::ImageFormat(NVCVRawPattern rawPattern, NVCVMemLayout memLayout, NVC ValidateSwizzlePacking(swizzle, packing0, packing1, packing2, packing3); ValidateExtraChannelInfo(exChannelInfo, packing0, packing1, packing2, packing3); - if (exChannelInfo == nullptr) - { - m_format = NVCV_MAKE_RAW_IMAGE_FORMAT(rawPattern, memLayout, dataKind, swizzle, alphaType, 4, packing0, - packing1, packing2, packing3); - } - else - { - if (exChannelInfo->numChannels > 0) - { - m_format = NVCV_MAKE_RAW_IMAGE_EXTRA_CHANNELS_FORMAT( - rawPattern, memLayout, dataKind, swizzle, alphaType, exChannelInfo->numChannels, - exChannelInfo->bitsPerPixel, exChannelInfo->datakind, exChannelInfo->channelType, 4, packing0, packing1, - packing2, packing3); - } - else - { - m_format = NVCV_MAKE_RAW_IMAGE_FORMAT(rawPattern, memLayout, dataKind, swizzle, alphaType, 4, packing0, - packing1, packing2, packing3); - } - } + m_format = MakeRawImageFormat(rawPattern, memLayout, dataKind, swizzle, alphaType, exChannelInfo, packing0, + packing1, packing2, packing3); } ImageFormat::ImageFormat(NVCVMemLayout memLayout, NVCVDataKind dataKind, NVCVSwizzle swizzle, NVCVPacking packing0, @@ -271,25 +294,8 @@ ImageFormat::ImageFormat(NVCVMemLayout memLayout, NVCVDataKind dataKind, NVCVSwi ValidateSwizzlePacking(swizzle, packing0, packing1, packing2, packing3); ValidateExtraChannelInfo(exChannelInfo, packing0, packing1, packing2, packing3); - if (exChannelInfo == nullptr) - { - m_format = NVCV_MAKE_NONCOLOR_IMAGE_FORMAT(memLayout, dataKind, swizzle, alphaType, 4, packing0, packing1, - packing2, packing3); - } - else - { - if (exChannelInfo->numChannels > 0) - { - m_format = NVCV_MAKE_NONCOLOR_IMAGE_EXTRA_CHANNELS_FORMAT( - memLayout, dataKind, swizzle, alphaType, exChannelInfo->numChannels, exChannelInfo->bitsPerPixel, - exChannelInfo->datakind, exChannelInfo->channelType, 4, packing0, packing1, packing2, packing3); - } - else - { - m_format = NVCV_MAKE_NONCOLOR_IMAGE_FORMAT(memLayout, dataKind, swizzle, alphaType, 4, packing0, packing1, - packing2, packing3); - } - } + m_format = MakeNonColorImageFormat(memLayout, dataKind, swizzle, alphaType, exChannelInfo, packing0, packing1, + packing2, packing3); } ImageFormat::ImageFormat(const ColorFormat &colorFormat, NVCVChromaSubsampling chromaSub, NVCVMemLayout memLayout, @@ -311,12 +317,105 @@ ImageFormat::ImageFormat(const ColorFormat &colorFormat, NVCVChromaSubsampling c } else { - m_format = ImageFormat{colorFormat.model, colorFormat.cspec, chromaSub, memLayout, dataKind, swizzle, - packing0, packing1, packing2, packing3, alphaType, exChannelInfo} + m_format = ImageFormat{colorFormat.model, ColorSpec{colorFormat.cspec}, + chromaSub, memLayout, + dataKind, swizzle, + packing0, packing1, + packing2, packing3, + alphaType, exChannelInfo} .value(); } } +static int CountPlanes(const util::StaticVector &fmtPlanes) +{ + return static_cast(fmtPlanes.size()); +} + +static void ValidatePlaneFormat(const ImageFormat &format, int plane, NVCVAlphaType alphaChannelType) +{ + if (format.numPlanes() != 1) + { + throw Exception(NVCV_ERROR_INVALID_ARGUMENT) << "Format for plane #" << plane << " must have only one plane"; + } + + if (format.planePacking(0) == NVCV_PACKING_0) + { + throw Exception(NVCV_ERROR_INVALID_ARGUMENT) + << "Format for plane #" << plane << " must have a non-zero packing"; + } + + if (format.alphaType() != alphaChannelType) + { + throw Exception(NVCV_ERROR_INVALID_IMAGE_FORMAT) << "All image planes must have same alphaType"; + } + + NVCV_ASSERT(format.planePacking(1) == NVCV_PACKING_0); + NVCV_ASSERT(format.planePacking(2) == NVCV_PACKING_0); + NVCV_ASSERT(format.planePacking(3) == NVCV_PACKING_0); +} + +static void ValidatePlaneCompatibility(const ImageFormat &format, int plane, const ColorFormat &colorFormat, + NVCVMemLayout memLayout, NVCVDataKind dataKind, + std::optional rawPattern) +{ + if (format.rawPattern() != rawPattern) + { + throw Exception(NVCV_ERROR_INVALID_ARGUMENT) << "Raw pattern of all plane formats must be the same"; + } + if (format.colorFormat() != colorFormat) + { + throw Exception(NVCV_ERROR_INVALID_ARGUMENT) << "Color format of all plane formats must be the same"; + } + if (format.memLayout() != memLayout) + { + throw Exception(NVCV_ERROR_INVALID_ARGUMENT) + << "Memory layout of all plane formats must be the same, but plane #" << plane << "'s is " << memLayout; + } + if (format.dataKind() != dataKind) + { + throw Exception(NVCV_ERROR_INVALID_ARGUMENT) + << "Data type of all plane formats must be the same, but plane #" << plane << "'s is " << dataKind; + } +} + +static int CountRemainingChannels(const util::StaticVector &fmtPlanes, int firstPlane, + int totalChannels) +{ + for (int i = firstPlane; i < CountPlanes(fmtPlanes); ++i) + { + totalChannels += fmtPlanes[i].numChannels(); + } + return totalChannels; +} + +static NVCVChromaSubsampling MergeChromaSubsampling(NVCVChromaSubsampling css, NVCVChromaSubsampling planeCss, + int plane) +{ + if (css == NVCV_CSS_NONE) + { + return planeCss; + } + + if (css != planeCss) + { + throw Exception(NVCV_ERROR_INVALID_ARGUMENT) + << "Only one chroma-subsampling type must be specified, but plane #" << plane << "'s differ from " << css; + } + + return css; +} + +static NVCVSwizzle PlaneSwizzleOrZero(const util::StaticVector &fmtPlanes, int plane) +{ + return CountPlanes(fmtPlanes) > plane ? fmtPlanes[plane].swizzle() : NVCV_SWIZZLE_0000; +} + +static NVCVPacking PlanePackingOrZero(const util::StaticVector &fmtPlanes, int plane) +{ + return CountPlanes(fmtPlanes) > plane ? fmtPlanes[plane].planePacking(0) : NVCV_PACKING_0; +} + ImageFormat ImageFormat::FromPlanes(const util::StaticVector &fmtPlanes) { if (fmtPlanes.empty()) @@ -333,39 +432,15 @@ ImageFormat ImageFormat::FromPlanes(const util::StaticVector &fm NVCVChromaSubsampling css = NVCV_CSS_NONE; int totChannels = 0; - for (size_t i = 0; i < fmtPlanes.size(); ++i) + for (int i = 0; i < CountPlanes(fmtPlanes); ++i) { - // all plane types must have just one plane. - if (fmtPlanes[i].numPlanes() != 1) - { - throw Exception(NVCV_ERROR_INVALID_ARGUMENT) << "Format for plane #" << i << " must have only one plane"; - } - - // first plane must have a valid packing - if (fmtPlanes[i].planePacking(0) == NVCV_PACKING_0) - { - throw Exception(NVCV_ERROR_INVALID_ARGUMENT) - << "Format for plane #" << i << " must have a non-zero packing"; - } - - if (fmtPlanes[i].alphaType() != alphaChannelType) - { - throw Exception(NVCV_ERROR_INVALID_IMAGE_FORMAT) << "All image planes must have same alphaType"; - } - - NVCV_ASSERT(fmtPlanes[i].planePacking(1) == NVCV_PACKING_0); - NVCV_ASSERT(fmtPlanes[i].planePacking(2) == NVCV_PACKING_0); - NVCV_ASSERT(fmtPlanes[i].planePacking(3) == NVCV_PACKING_0); + ValidatePlaneFormat(fmtPlanes[i], i, alphaChannelType); // total number of channels must be at most 4. totChannels += fmtPlanes[i].numChannels(); if (totChannels > 4) { - // Get the total number of channels for the exception error message. - for (++i; i < fmtPlanes.size(); ++i) - { - totChannels += fmtPlanes[i].numChannels(); - } + totChannels = CountRemainingChannels(fmtPlanes, i + 1, totChannels); throw Exception(NVCV_ERROR_INVALID_ARGUMENT) << "Total number of channels comprised by all valid plane formats" << " must be at most 4, not " << totChannels; @@ -373,38 +448,10 @@ ImageFormat ImageFormat::FromPlanes(const util::StaticVector &fm if (i >= 1) { - // color spec, mem layout and data type of all planes must be the same - if (fmtPlanes[i].rawPattern() != rawPattern) - { - throw Exception(NVCV_ERROR_INVALID_ARGUMENT) << "Raw pattern of all plane formats must be the same"; - } - if (fmtPlanes[i].colorFormat() != colorFormat) - { - throw Exception(NVCV_ERROR_INVALID_ARGUMENT) << "Color format of all plane formats must be the same"; - } - if (fmtPlanes[i].memLayout() != memLayout) - { - throw Exception(NVCV_ERROR_INVALID_ARGUMENT) - << "Memory layout of all plane formats must be the same, but plane #" << i << "'s is " << memLayout; - } - if (fmtPlanes[i].dataKind() != dataKind) - { - throw Exception(NVCV_ERROR_INVALID_ARGUMENT) - << "Data type of all plane formats must be the same, but plane #" << i << "'s is " << dataKind; - } + ValidatePlaneCompatibility(fmtPlanes[i], i, colorFormat, memLayout, dataKind, rawPattern); } - NVCVChromaSubsampling plcss = fmtPlanes[i].css(); - if (css == NVCV_CSS_NONE) - { - css = plcss; - } - else if (css != plcss) - { - // only one kind of chroma subsampling is allowed. - throw Exception(NVCV_ERROR_INVALID_ARGUMENT) - << "Only one chroma-subsampling type must be specified, but plane #" << i << "'s differ from " << css; - } + css = MergeChromaSubsampling(css, fmtPlanes[i].css(), i); } // at least one channel is allowed @@ -413,20 +460,20 @@ ImageFormat ImageFormat::FromPlanes(const util::StaticVector &fm throw Exception(NVCV_ERROR_INVALID_ARGUMENT) << "Total number of channels cannot be 0"; } - NVCVSwizzle swPlane[4] = { - fmtPlanes[0].swizzle(), - fmtPlanes.size() > 1 ? fmtPlanes[1].swizzle() : NVCV_SWIZZLE_0000, - fmtPlanes.size() > 2 ? fmtPlanes[2].swizzle() : NVCV_SWIZZLE_0000, - fmtPlanes.size() > 3 ? fmtPlanes[3].swizzle() : NVCV_SWIZZLE_0000, + std::array swPlane = { + PlaneSwizzleOrZero(fmtPlanes, 0), + PlaneSwizzleOrZero(fmtPlanes, 1), + PlaneSwizzleOrZero(fmtPlanes, 2), + PlaneSwizzleOrZero(fmtPlanes, 3), }; NVCVSwizzle swizzle = MergePlaneSwizzles(swPlane[0], swPlane[1], swPlane[2], swPlane[3]); - NVCVPacking packPlane[4] = { - fmtPlanes[0].planePacking(0), - fmtPlanes.size() > 1 ? fmtPlanes[1].planePacking(0) : NVCV_PACKING_0, - fmtPlanes.size() > 2 ? fmtPlanes[2].planePacking(0) : NVCV_PACKING_0, - fmtPlanes.size() > 3 ? fmtPlanes[3].planePacking(0) : NVCV_PACKING_0, + std::array packPlane = { + PlanePackingOrZero(fmtPlanes, 0), + PlanePackingOrZero(fmtPlanes, 1), + PlanePackingOrZero(fmtPlanes, 2), + PlanePackingOrZero(fmtPlanes, 3), }; return ImageFormat{colorFormat, css, memLayout, dataKind, swizzle, packPlane[0], @@ -438,19 +485,21 @@ namespace { constexpr uint32_t FCC(char a, char b, char c, char d) { #if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ - return static_cast((d << 24) | (c << 16) | (b << 8) | a); + const uint32_t value = (d << 24) | (c << 16) | (b << 8) | a; // NOSONAR + return value; #elif __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ - return static_cast((a << 24) | (b << 16) | (c << 8) | d); + const uint32_t value = (a << 24) | (b << 16) | (c << 8) | d; // NOSONAR + return value; #else # error Insert that old PDP-11 joke here. #endif } -#define FCC_IF(model, css, type, swizzle, ...) \ - ImageFormat \ - { \ - NVCV_COLOR_MODEL_##model, NVCV_COLOR_SPEC_UNDEFINED, NVCV_CSS_##css, NVCV_MEM_LAYOUT_PL, \ - NVCV_DATA_KIND_##type, NVCV_SWIZZLE_##swizzle, __VA_ARGS__ \ +#define FCC_IF(model, css, type, swizzle, ...) \ + ImageFormat \ + { \ + NVCV_COLOR_MODEL_##model, ColorSpec{NVCV_COLOR_SPEC_UNDEFINED}, NVCV_CSS_##css, NVCV_MEM_LAYOUT_PL, \ + NVCV_DATA_KIND_##type, NVCV_SWIZZLE_##swizzle, __VA_ARGS__ \ } #define FCC_BAYER_IF(pattern, type, swizzle, ...) \ @@ -510,20 +559,21 @@ ImageFormat ImageFormat::FromFourCC(uint32_t fourcc, ColorSpec colorSpec, NVCVMe { // First make sure fourcc is uppercase. - char *tmp = reinterpret_cast(&fourcc); - for (int i = 0; i < 4; ++i) + std::array tmp = {}; + std::memcpy(tmp.data(), &fourcc, tmp.size()); + for (unsigned char &ch : tmp) { - tmp[i] = toupper(tmp[i]); + ch = static_cast(std::toupper(ch)); } - fourcc = *reinterpret_cast(tmp); + std::memcpy(&fourcc, tmp.data(), tmp.size()); - for (auto &p : g_FourCC) + for (auto &[formatFourCC, format] : g_FourCC) { - if (p.first == fourcc) + if (formatFourCC == fourcc) { - ImageFormat newFmt = p.second; + ImageFormat newFmt = format; - if (NeedsColorspec(p.second.colorModel())) + if (NeedsColorspec(format.colorModel())) { newFmt = newFmt.colorSpec(colorSpec); } @@ -584,7 +634,7 @@ NVCVColorModel ImageFormat::colorModel() const noexcept } else { - uint32_t tmp = ExtractBitfield(m_format, 17, 3); + auto tmp = static_cast(ExtractBitfield(m_format, 17, 3)); if (tmp < 7) { // models (other than YCbCr) with color spec @@ -625,7 +675,7 @@ ColorSpec ImageFormat::colorSpec() const noexcept } else { - return NVCV_COLOR_SPEC_UNDEFINED; + return ColorSpec{NVCV_COLOR_SPEC_UNDEFINED}; } } @@ -636,17 +686,16 @@ NVCVMemLayout ImageFormat::memLayout() const noexcept ColorFormat ImageFormat::colorFormat() const noexcept { - ColorFormat colorFormat{this->colorModel()}; + ColorFormat colorFormat{}; + colorFormat.model = this->colorModel(); if (colorFormat.model == NVCV_COLOR_MODEL_RAW) { - std::optional raw = this->rawPattern(); - NVCV_ASSERT(raw); - colorFormat.raw = *raw; + colorFormat.raw = this->rawPattern().value(); } else { - colorFormat.cspec = this->colorSpec(); + colorFormat.cspec = static_cast(this->colorSpec()); } return colorFormat; @@ -698,8 +747,7 @@ ImageFormat ImageFormat::alphaType(NVCVAlphaType newAlphaType) const } else { - return ImageFormat{static_cast( - (((uint64_t)m_format & ~MaskBitfield(6, 1)) | SetBitfield(newAlphaType, 6, 1)))}; + return ImageFormat{((m_format & ~MaskBitfield(6, 1)) | SetBitfield(newAlphaType, 6, 1))}; } } @@ -713,11 +761,11 @@ void ImageFormat::extraChannelInfo(NVCVExtraChannelInfo *exChannelInfo) const no int nPlanes = this->numPlanes(); if (nPlanes == 1) { - int32_t numExtraChannels = static_cast(ExtractBitfield(m_format, 47, 3)); + auto numExtraChannels = static_cast(ExtractBitfield(m_format, 47, 3)); if (numExtraChannels > 0) { exChannelInfo->numChannels = numExtraChannels; - exChannelInfo->bitsPerPixel = std::pow(2, 3 + static_cast(ExtractBitfield(m_format, 50, 3))); + exChannelInfo->bitsPerPixel = 1 << (3 + static_cast(ExtractBitfield(m_format, 50, 3))); exChannelInfo->datakind = static_cast(ExtractBitfield(m_format, 53, 3)); exChannelInfo->channelType = static_cast(ExtractBitfield(m_format, 44, 3)); } @@ -764,11 +812,11 @@ ImageFormat ImageFormat::extraChannelInfo(const NVCVExtraChannelInfo *newExChann if (this->numPlanes() == 1) { - return ImageFormat{static_cast(( - ((uint64_t)m_format & ~MaskBitfield(44, 12)) - | (SetBitfield(newExChannelInfo->numChannels, 47, 3) - | SetBitfield(NVCV_DETAIL_ENCODE_BPP(newExChannelInfo->bitsPerPixel), 50, 3) - | SetBitfield(newExChannelInfo->datakind, 53, 3) | SetBitfield(newExChannelInfo->channelType, 44, 3))))}; + return ImageFormat{ + ((m_format & ~MaskBitfield(44, 12)) + | (SetBitfield(newExChannelInfo->numChannels, 47, 3) + | SetBitfield(NVCV_DETAIL_ENCODE_BPP(newExChannelInfo->bitsPerPixel), 50, 3) + | SetBitfield(newExChannelInfo->datakind, 53, 3) | SetBitfield(newExChannelInfo->channelType, 44, 3)))}; } else { @@ -808,7 +856,8 @@ std::array ImageFormat::bpc() const << "Inconsistent image format, sum of planes' channel count " << nch << " > 4"; } - bits[nch++] = pbits[c]; + bits[nch] = pbits[c]; + ++nch; } } @@ -837,8 +886,7 @@ Size2D ImageFormat::planeSize(Size2D imgSize, int plane) const noexcept NVCVSwizzle ImageFormat::planeSwizzle(int plane) const { // shortcut - int totPlanes = this->numPlanes(); - if (totPlanes == 1) + if (int totPlanes = this->numPlanes(); totPlanes == 1) { if (plane == 0) { @@ -850,7 +898,7 @@ NVCVSwizzle ImageFormat::planeSwizzle(int plane) const } } - NVCVChannel plsw[4] = {}; + std::array plsw = {}; int ch = 0; for (int i = 0; i < plane; ++i) @@ -864,7 +912,7 @@ NVCVSwizzle ImageFormat::planeSwizzle(int plane) const int nch = this->planeNumChannels(plane); bool empty = true; - for (int i = ch; i < ch + nch; ++i) + for (int i = ch; i < ch + nch; ++i) // NOSONAR: swizzle channel positions are index-based. { assert(i < 4); switch (tch[i]) @@ -887,24 +935,28 @@ NVCVSwizzle ImageFormat::planeSwizzle(int plane) const } } - if (!empty) + if (empty) + { + return MakeNVCVSwizzle(plsw[0], plsw[1], plsw[2], plsw[3]); + } + + // if swizzle is using channel '1', it must be set in all + // plane swizzles, as it represents that the pixel has an + // opaque alpha, although the alpha channel isn't physically there. + for (int i = 0; i < 4; ++i) { - // if swizzle is using channel '1', it must be set in all - // plane swizzles, as it represents that the pixel has an - // opaque alpha, although the alpha channel isn't physically there. - for (int i = 0; i < 4; ++i) + if (tch[i] != NVCV_CHANNEL_1) { - if (tch[i] == NVCV_CHANNEL_1) - { - if (plsw[i] != NVCV_CHANNEL_0) - { - throw Exception( - NVCV_ERROR_INVALID_IMAGE_FORMAT, - "Plane swizzle is inconsistent, if using '1' channel, it must be set in all planes' swizzles,"); - } - plsw[i] = tch[i]; - } + continue; + } + + if (plsw[i] != NVCV_CHANNEL_0) + { + throw Exception( + NVCV_ERROR_INVALID_IMAGE_FORMAT, + "Plane swizzle is inconsistent, if using '1' channel, it must be set in all planes' swizzles,"); } + plsw[i] = tch[i]; } return MakeNVCVSwizzle(plsw[0], plsw[1], plsw[2], plsw[3]); @@ -964,13 +1016,13 @@ int ImageFormat::planePixelStrideBytes(int plane) const noexcept uint32_t ImageFormat::fourCC() const { // normalize - ImageFormat fmtNorm = this->colorSpec(NVCV_COLOR_SPEC_UNDEFINED).memLayout(NVCV_MEM_LAYOUT_PL); + ImageFormat fmtNorm = this->colorSpec(ColorSpec{NVCV_COLOR_SPEC_UNDEFINED}).memLayout(NVCV_MEM_LAYOUT_PL); - for (auto &p : g_FourCC) + for (auto &[formatFourCC, format] : g_FourCC) { - if (p.second == fmtNorm) + if (format == fmtNorm) { - return p.first; + return formatFourCC; } } @@ -1035,14 +1087,13 @@ ImageFormat ImageFormat::dataKind(NVCVDataKind newDataKind) const // bit, the sign bit, will be 1, as we want. return ImageFormat{static_cast( -(1 - + (int64_t)(~(((uint64_t)m_format & ~MaskBitfield(61, 3)) | SetBitfield(newDataKind, 61, 2)) - & ~(1ULL << 63))))}; + + static_cast(~((m_format & ~MaskBitfield(61, 3)) | SetBitfield(newDataKind, 61, 2)) + & ~(1ULL << 63))))}; } else { // the result fits into an int64_t, we don't need any hackery. - return ImageFormat{static_cast( - (((uint64_t)m_format & ~MaskBitfield(61, 3)) | SetBitfield(newDataKind, 61, 3)))}; + return ImageFormat{((m_format & ~MaskBitfield(61, 3)) | SetBitfield(newDataKind, 61, 3))}; } } @@ -1055,8 +1106,7 @@ ImageFormat ImageFormat::rawPattern(NVCVRawPattern newRawPattern) const if (this->colorModel() == NVCV_COLOR_MODEL_RAW) { - return ImageFormat{static_cast( - (((uint64_t)m_format & ~MaskBitfield(21, 6)) | SetBitfield(newRawPattern, 21, 6)))}; + return ImageFormat{((m_format & ~MaskBitfield(21, 6)) | SetBitfield(newRawPattern, 21, 6))}; } else { @@ -1074,7 +1124,7 @@ ImageFormat ImageFormat::colorFormat(const ColorFormat &newColorFormat) const } else { - return this->colorSpec(newColorFormat.cspec); + return this->colorSpec(ColorSpec{newColorFormat.cspec}); } } @@ -1100,8 +1150,7 @@ ImageFormat ImageFormat::memLayout(NVCVMemLayout newMemLayout) const throw Exception(NVCV_ERROR_INVALID_ARGUMENT, "Can't set memory layout of NONE format"); } - return ImageFormat{ - static_cast((((uint64_t)m_format & ~MaskBitfield(12, 3)) | SetBitfield(newMemLayout, 12, 3)))}; + return ImageFormat{((m_format & ~MaskBitfield(12, 3)) | SetBitfield(newMemLayout, 12, 3))}; } ImageFormat ImageFormat::colorSpec(ColorSpec newColorSpec) const @@ -1128,7 +1177,7 @@ ImageFormat ImageFormat::colorSpec(ColorSpec newColorSpec) const break; default: - if (this->colorSpec() != NVCV_COLOR_SPEC_UNDEFINED) + if (static_cast(this->colorSpec()) != NVCV_COLOR_SPEC_UNDEFINED) { throw Exception(NVCV_ERROR_INVALID_ARGUMENT) << "If image format's color model isn't RGB or YCbCr," @@ -1138,8 +1187,8 @@ ImageFormat ImageFormat::colorSpec(ColorSpec newColorSpec) const return *this; } - return ImageFormat{static_cast( - (((uint64_t)m_format & ~MaskBitfield(20, 15)) | SetBitfield(newColorSpec, 20, 15)))}; + return ImageFormat{ + ((m_format & ~MaskBitfield(20, 15)) | SetBitfield(static_cast(newColorSpec), 20, 15))}; } ImageFormat ImageFormat::css(NVCVChromaSubsampling newCSS) const @@ -1151,8 +1200,7 @@ ImageFormat ImageFormat::css(NVCVChromaSubsampling newCSS) const if (this->colorModel() == NVCV_COLOR_MODEL_YCbCr) { - return ImageFormat{ - static_cast((((uint64_t)m_format & ~MaskBitfield(17, 3)) | SetBitfield(newCSS, 17, 3)))}; + return ImageFormat{((m_format & ~MaskBitfield(17, 3)) | SetBitfield(newCSS, 17, 3))}; } else if (newCSS == NVCV_CSS_NONE) { @@ -1183,7 +1231,8 @@ ImageFormat UpdateColorSpec(ImageFormat fmt, ColorSpec source) ColorSpec cspec = fmt.colorSpec(); - if (cspec == NVCV_COLOR_SPEC_UNDEFINED && source != NVCV_COLOR_SPEC_UNDEFINED) + if (static_cast(cspec) == NVCV_COLOR_SPEC_UNDEFINED + && static_cast(source) != NVCV_COLOR_SPEC_UNDEFINED) { cspec = cspec.colorSpace(source.colorSpace()); @@ -1218,73 +1267,75 @@ ImageFormat UpdateColorSpec(ImageFormat fmt, ColorSpec source) std::ostream &operator<<(std::ostream &out, ImageFormat fmt) { - switch (fmt.value()) + switch (fmt.value()) // NOSONAR: enum stringification must cover every ABI value. { #define NVCV_ENUM(E) \ case E: \ return out << #E; - NVCV_ENUM(NVCV_IMAGE_FORMAT_NONE); - NVCV_ENUM(NVCV_IMAGE_FORMAT_U8); - NVCV_ENUM(NVCV_IMAGE_FORMAT_U8_BL); - NVCV_ENUM(NVCV_IMAGE_FORMAT_S8); - NVCV_ENUM(NVCV_IMAGE_FORMAT_U16); - NVCV_ENUM(NVCV_IMAGE_FORMAT_S16); - NVCV_ENUM(NVCV_IMAGE_FORMAT_S16_BL); - NVCV_ENUM(NVCV_IMAGE_FORMAT_U32); - NVCV_ENUM(NVCV_IMAGE_FORMAT_S32); - NVCV_ENUM(NVCV_IMAGE_FORMAT_Y8); - NVCV_ENUM(NVCV_IMAGE_FORMAT_Y8_BL); - NVCV_ENUM(NVCV_IMAGE_FORMAT_Y8_ER); - NVCV_ENUM(NVCV_IMAGE_FORMAT_Y8_ER_BL); - NVCV_ENUM(NVCV_IMAGE_FORMAT_Y16); - NVCV_ENUM(NVCV_IMAGE_FORMAT_Y16_BL); - NVCV_ENUM(NVCV_IMAGE_FORMAT_Y16_ER); - NVCV_ENUM(NVCV_IMAGE_FORMAT_Y16_ER_BL); - NVCV_ENUM(NVCV_IMAGE_FORMAT_NV12); - NVCV_ENUM(NVCV_IMAGE_FORMAT_NV12_BL); - NVCV_ENUM(NVCV_IMAGE_FORMAT_NV12_ER); - NVCV_ENUM(NVCV_IMAGE_FORMAT_NV12_ER_BL); - NVCV_ENUM(NVCV_IMAGE_FORMAT_NV24); - NVCV_ENUM(NVCV_IMAGE_FORMAT_NV24_BL); - NVCV_ENUM(NVCV_IMAGE_FORMAT_NV24_ER); - NVCV_ENUM(NVCV_IMAGE_FORMAT_NV24_ER_BL); - NVCV_ENUM(NVCV_IMAGE_FORMAT_RGB8); - NVCV_ENUM(NVCV_IMAGE_FORMAT_RGBA8); - NVCV_ENUM(NVCV_IMAGE_FORMAT_BGR8); - NVCV_ENUM(NVCV_IMAGE_FORMAT_BGRA8); - NVCV_ENUM(NVCV_IMAGE_FORMAT_F32); - NVCV_ENUM(NVCV_IMAGE_FORMAT_F64); - NVCV_ENUM(NVCV_IMAGE_FORMAT_2S16); - NVCV_ENUM(NVCV_IMAGE_FORMAT_2S16_BL); - NVCV_ENUM(NVCV_IMAGE_FORMAT_2F32); - NVCV_ENUM(NVCV_IMAGE_FORMAT_C64); - NVCV_ENUM(NVCV_IMAGE_FORMAT_2C64); - NVCV_ENUM(NVCV_IMAGE_FORMAT_C128); - NVCV_ENUM(NVCV_IMAGE_FORMAT_2C128); - NVCV_ENUM(NVCV_IMAGE_FORMAT_UYVY); - NVCV_ENUM(NVCV_IMAGE_FORMAT_UYVY_BL); - NVCV_ENUM(NVCV_IMAGE_FORMAT_UYVY_ER); - NVCV_ENUM(NVCV_IMAGE_FORMAT_UYVY_ER_BL); - NVCV_ENUM(NVCV_IMAGE_FORMAT_VYUY); - NVCV_ENUM(NVCV_IMAGE_FORMAT_VYUY_BL); - NVCV_ENUM(NVCV_IMAGE_FORMAT_VYUY_ER); - NVCV_ENUM(NVCV_IMAGE_FORMAT_VYUY_ER_BL); - NVCV_ENUM(NVCV_IMAGE_FORMAT_YUYV); - NVCV_ENUM(NVCV_IMAGE_FORMAT_YUYV_BL); - NVCV_ENUM(NVCV_IMAGE_FORMAT_YUYV_ER); - NVCV_ENUM(NVCV_IMAGE_FORMAT_YUYV_ER_BL); - NVCV_ENUM(NVCV_IMAGE_FORMAT_YUV8p); - NVCV_ENUM(NVCV_IMAGE_FORMAT_YUV8p_ER); - NVCV_ENUM(NVCV_IMAGE_FORMAT_RGB8_1U_U8); - NVCV_ENUM(NVCV_IMAGE_FORMAT_RGB8_7U_U8); - NVCV_ENUM(NVCV_IMAGE_FORMAT_RGBA8_3U_U16); - NVCV_ENUM(NVCV_IMAGE_FORMAT_RGBA8_3POS3D_U32); - NVCV_ENUM(NVCV_IMAGE_FORMAT_RGB8_3D_F32); - NVCV_ENUM(NVCV_IMAGE_FORMAT_YCCK8); - NVCV_ENUM(NVCV_IMAGE_FORMAT_CMYK8); - NVCV_ENUM(NVCV_IMAGE_FORMAT_HSV8); - NVCV_ENUM(NVCV_IMAGE_FORMAT_RGBAf32); - NVCV_ENUM(NVCV_IMAGE_FORMAT_RGBAf32p); + NVCV_ENUM(NVCV_IMAGE_FORMAT_NONE) + NVCV_ENUM(NVCV_IMAGE_FORMAT_U8) + NVCV_ENUM(NVCV_IMAGE_FORMAT_U8_BL) + NVCV_ENUM(NVCV_IMAGE_FORMAT_S8) + NVCV_ENUM(NVCV_IMAGE_FORMAT_U16) + NVCV_ENUM(NVCV_IMAGE_FORMAT_S16) + NVCV_ENUM(NVCV_IMAGE_FORMAT_S16_BL) + NVCV_ENUM(NVCV_IMAGE_FORMAT_U32) + NVCV_ENUM(NVCV_IMAGE_FORMAT_S32) + NVCV_ENUM(NVCV_IMAGE_FORMAT_Y8) + NVCV_ENUM(NVCV_IMAGE_FORMAT_Y8_BL) + NVCV_ENUM(NVCV_IMAGE_FORMAT_Y8_ER) + NVCV_ENUM(NVCV_IMAGE_FORMAT_Y8_ER_BL) + NVCV_ENUM(NVCV_IMAGE_FORMAT_Y16) + NVCV_ENUM(NVCV_IMAGE_FORMAT_Y16_BL) + NVCV_ENUM(NVCV_IMAGE_FORMAT_Y16_ER) + NVCV_ENUM(NVCV_IMAGE_FORMAT_Y16_ER_BL) + NVCV_ENUM(NVCV_IMAGE_FORMAT_NV12) + NVCV_ENUM(NVCV_IMAGE_FORMAT_NV12_BL) + NVCV_ENUM(NVCV_IMAGE_FORMAT_NV12_ER) + NVCV_ENUM(NVCV_IMAGE_FORMAT_NV12_ER_BL) + NVCV_ENUM(NVCV_IMAGE_FORMAT_NV24) + NVCV_ENUM(NVCV_IMAGE_FORMAT_NV24_BL) + NVCV_ENUM(NVCV_IMAGE_FORMAT_NV24_ER) + NVCV_ENUM(NVCV_IMAGE_FORMAT_NV24_ER_BL) + NVCV_ENUM(NVCV_IMAGE_FORMAT_RGB8) + NVCV_ENUM(NVCV_IMAGE_FORMAT_RGBA8) + NVCV_ENUM(NVCV_IMAGE_FORMAT_BGR8) + NVCV_ENUM(NVCV_IMAGE_FORMAT_BGRA8) + NVCV_ENUM(NVCV_IMAGE_FORMAT_F32) + NVCV_ENUM(NVCV_IMAGE_FORMAT_F64) + NVCV_ENUM(NVCV_IMAGE_FORMAT_2S16) + NVCV_ENUM(NVCV_IMAGE_FORMAT_2S16_BL) + NVCV_ENUM(NVCV_IMAGE_FORMAT_2F32) + NVCV_ENUM(NVCV_IMAGE_FORMAT_C64) + NVCV_ENUM(NVCV_IMAGE_FORMAT_2C64) + NVCV_ENUM(NVCV_IMAGE_FORMAT_C128) + NVCV_ENUM(NVCV_IMAGE_FORMAT_2C128) + NVCV_ENUM(NVCV_IMAGE_FORMAT_UYVY) + NVCV_ENUM(NVCV_IMAGE_FORMAT_UYVY_BL) + NVCV_ENUM(NVCV_IMAGE_FORMAT_UYVY_ER) + NVCV_ENUM(NVCV_IMAGE_FORMAT_UYVY_ER_BL) + NVCV_ENUM(NVCV_IMAGE_FORMAT_VYUY) + NVCV_ENUM(NVCV_IMAGE_FORMAT_VYUY_BL) + NVCV_ENUM(NVCV_IMAGE_FORMAT_VYUY_ER) + NVCV_ENUM(NVCV_IMAGE_FORMAT_VYUY_ER_BL) + NVCV_ENUM(NVCV_IMAGE_FORMAT_YUYV) + NVCV_ENUM(NVCV_IMAGE_FORMAT_YUYV_BL) + NVCV_ENUM(NVCV_IMAGE_FORMAT_YUYV_ER) + NVCV_ENUM(NVCV_IMAGE_FORMAT_YUYV_ER_BL) + NVCV_ENUM(NVCV_IMAGE_FORMAT_YUV8p) + NVCV_ENUM(NVCV_IMAGE_FORMAT_YUV8p_ER) + NVCV_ENUM(NVCV_IMAGE_FORMAT_RGB8_1U_U8) + NVCV_ENUM(NVCV_IMAGE_FORMAT_RGB8_7U_U8) + NVCV_ENUM(NVCV_IMAGE_FORMAT_RGBA8_3U_U16) + NVCV_ENUM(NVCV_IMAGE_FORMAT_RGBA8_3POS3D_U32) + NVCV_ENUM(NVCV_IMAGE_FORMAT_RGB8_3D_F32) + NVCV_ENUM(NVCV_IMAGE_FORMAT_YCCK8) + NVCV_ENUM(NVCV_IMAGE_FORMAT_CMYK8) + NVCV_ENUM(NVCV_IMAGE_FORMAT_HSV8) + NVCV_ENUM(NVCV_IMAGE_FORMAT_RGBAf32) + NVCV_ENUM(NVCV_IMAGE_FORMAT_RGBAf32p) + default: + break; #undef NVCV_ENUM } @@ -1294,9 +1345,7 @@ std::ostream &operator<<(std::ostream &out, ImageFormat fmt) { case NVCV_COLOR_MODEL_RAW: { - std::optional raw = fmt.rawPattern(); - NVCV_ASSERT(raw); - out << *raw << ","; + out << fmt.rawPattern().value() << ","; } break; diff --git a/src/nvcv/src/priv/ImageFormat.hpp b/src/nvcv/src/priv/ImageFormat.hpp index 1019ddaeb..24fb9b2a1 100644 --- a/src/nvcv/src/priv/ImageFormat.hpp +++ b/src/nvcv/src/priv/ImageFormat.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -36,7 +36,7 @@ class ColorFormat; class DataType; // Wrapper to NVCVImageFormat to make it properly typed. -class ImageFormat +class ImageFormat // NOSONAR: private image-format wrapper mirrors the public format API. { public: explicit constexpr ImageFormat(NVCVImageFormat format) @@ -48,22 +48,22 @@ class ImageFormat NVCVMemLayout memLayout, NVCVDataKind dataKind, NVCVSwizzle swizzle, NVCVPacking packing0, NVCVPacking packing1 = NVCV_PACKING_0, NVCVPacking packing2 = NVCV_PACKING_0, NVCVPacking packing3 = NVCV_PACKING_0, NVCVAlphaType alphaType = NVCV_ALPHA_ASSOCIATED, - const NVCVExtraChannelInfo *exChannelInfo = 0); + const NVCVExtraChannelInfo *exChannelInfo = nullptr); ImageFormat(NVCVRawPattern rawPattern, NVCVMemLayout memLayout, NVCVDataKind dataKind, NVCVSwizzle swizzle, NVCVPacking packing0, NVCVPacking packing1 = NVCV_PACKING_0, NVCVPacking packing2 = NVCV_PACKING_0, NVCVPacking packing3 = NVCV_PACKING_0, NVCVAlphaType alphaType = NVCV_ALPHA_ASSOCIATED, - const NVCVExtraChannelInfo *exChannelInfo = 0); + const NVCVExtraChannelInfo *exChannelInfo = nullptr); ImageFormat(NVCVMemLayout memLayout, NVCVDataKind dataKind, NVCVSwizzle swizzle, NVCVPacking packing0, NVCVPacking packing1 = NVCV_PACKING_0, NVCVPacking packing2 = NVCV_PACKING_0, NVCVPacking packing3 = NVCV_PACKING_0, NVCVAlphaType alphaType = NVCV_ALPHA_ASSOCIATED, - const NVCVExtraChannelInfo *exChannelInfo = 0); + const NVCVExtraChannelInfo *exChannelInfo = nullptr); ImageFormat(const ColorFormat &colorFormat, NVCVChromaSubsampling chromaSub, NVCVMemLayout memLayout, NVCVDataKind dataKind, NVCVSwizzle swizzle, NVCVPacking packing0, NVCVPacking packing1 = NVCV_PACKING_0, NVCVPacking packing2 = NVCV_PACKING_0, NVCVPacking packing3 = NVCV_PACKING_0, - NVCVAlphaType alphaType = NVCV_ALPHA_ASSOCIATED, const NVCVExtraChannelInfo *exChannelInfo = 0); + NVCVAlphaType alphaType = NVCV_ALPHA_ASSOCIATED, const NVCVExtraChannelInfo *exChannelInfo = nullptr); static ImageFormat FromFourCC(uint32_t fourcc, ColorSpec colorSpec, NVCVMemLayout memLayout); @@ -155,8 +155,7 @@ constexpr bool ImageFormat::operator!=(ImageFormat that) const noexcept constexpr NVCVPacking ImageFormat::planePacking(int plane) const noexcept { - // |11 10 09 08|05 04|03 02 01 00| - // | ENC(BPP) |#CH-1| PACK | + // Packing fields: encoded bits per pixel, channel count minus one, and packing id. auto decode = [](uint32_t plane, uint32_t isPlanar, uint32_t value, int chlen, int packlen, int bpplen) { @@ -165,9 +164,9 @@ constexpr NVCVPacking ImageFormat::planePacking(int plane) const noexcept uint32_t pack; if (isPlanar) { - bpp = ExtractBitfield(value, packlen + chlen, bpplen); - nch = ExtractBitfield(value, packlen, chlen); - pack = ExtractBitfield(value, 0, packlen); + bpp = static_cast(ExtractBitfield(value, packlen + chlen, bpplen)); + nch = static_cast(ExtractBitfield(value, packlen, chlen)); + pack = static_cast(ExtractBitfield(value, 0, packlen)); } else { @@ -190,20 +189,20 @@ constexpr NVCVPacking ImageFormat::planePacking(int plane) const noexcept return SetBitfield(bpp, 6, 4) | SetBitfield(nch, 4, 2) | SetBitfield(pack, 0, 4); }; - uint32_t isPlanar = ExtractBitfield(m_format, 7, 1); + auto isPlanar = static_cast(ExtractBitfield(m_format, 7, 1)); switch (plane) { case 0: - return (NVCVPacking)decode(plane, 1, ExtractBitfield(m_format, 35, 9), 2, 3, 4); + return (NVCVPacking)decode(plane, 1, static_cast(ExtractBitfield(m_format, 35, 9)), 2, 3, 4); case 1: - return (NVCVPacking)decode(plane, isPlanar, ExtractBitfield(m_format, 44, 7), 1, 3, 3); + return (NVCVPacking)decode(plane, isPlanar, static_cast(ExtractBitfield(m_format, 44, 7)), 1, 3, 3); case 2: - return (NVCVPacking)decode(plane, isPlanar, ExtractBitfield(m_format, 51, 7), 1, 3, 3); + return (NVCVPacking)decode(plane, isPlanar, static_cast(ExtractBitfield(m_format, 51, 7)), 1, 3, 3); case 3: - return (NVCVPacking)decode(plane, isPlanar, ExtractBitfield(m_format, 58, 3), 0, 0, 3); + return (NVCVPacking)decode(plane, isPlanar, static_cast(ExtractBitfield(m_format, 58, 3)), 0, 0, 3); default: return NVCV_PACKING_0; diff --git a/src/nvcv/src/priv/LockFreeStack.hpp b/src/nvcv/src/priv/LockFreeStack.hpp index a1681e1b0..d33956857 100644 --- a/src/nvcv/src/priv/LockFreeStack.hpp +++ b/src/nvcv/src/priv/LockFreeStack.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -27,7 +27,19 @@ namespace nvcv::priv { template constexpr bool IsForwardListNode = std::is_convertible_v().next), T *>; -template, int> = 0> +template +struct LockFreeStackRequirement; + +template +struct LockFreeStackRequirement +{ + using type = T; +}; + +template +using LockFreeStackRequire = typename LockFreeStackRequirement::type; + +template, int> = 0> class LockFreeStack { public: @@ -51,7 +63,8 @@ class LockFreeStack } // Set the newHead to oldHead->next and return oldHead - Node *oldHead, *newHead; + Node *oldHead; + Node *newHead; do { oldHead = m_head.load(std::memory_order_relaxed); @@ -80,6 +93,7 @@ class LockFreeStack Node *h = m_head.load(std::memory_order_relaxed); while (!m_head.compare_exchange_weak(h, nullptr, std::memory_order_relaxed, std::memory_order_release)) { + // Retry until the observed head is released. } return h; diff --git a/src/nvcv/src/priv/Requirements.cpp b/src/nvcv/src/priv/Requirements.cpp index 908a52f33..a82012e84 100644 --- a/src/nvcv/src/priv/Requirements.cpp +++ b/src/nvcv/src/priv/Requirements.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,6 +23,7 @@ #include #include +#include namespace nvcv::priv { @@ -38,7 +39,7 @@ void Add(NVCVRequirements &reqSum, const NVCVRequirements &req) const NVCVMemRequirements &reqHostMem = req.hostMem; const NVCVMemRequirements &reqHostPinnedMem = req.hostPinnedMem; - for (size_t i = 0; i < sizeof(NVCVMemRequirements::numBlocks) / sizeof(NVCVMemRequirements::numBlocks[0]); ++i) + for (size_t i = 0; i < std::size(reqCudaMem.numBlocks); ++i) { reqSum.cudaMem.numBlocks[i] += reqCudaMem.numBlocks[i]; reqSum.hostMem.numBlocks[i] += reqHostMem.numBlocks[i]; @@ -70,8 +71,8 @@ void AddBuffer(NVCVMemRequirements &memReq, int64_t bufSize, int64_t bufAlignmen if (bufSize >= 0) { - int64_t maxBlocks = std::numeric_limits>::max(); - if (memReq.numBlocks[log2Align] + if (int64_t maxBlocks = std::numeric_limits>::max(); + memReq.numBlocks[log2Align] > maxBlocks - numBlocks) // codeQL findings: Testing for signed overflow may produce undefined results. { throw Exception(NVCV_ERROR_OVERFLOW, @@ -90,9 +91,9 @@ void AddBuffer(NVCVMemRequirements &memReq, int64_t bufSize, int64_t bufAlignmen int64_t CalcTotalSizeBytes(const NVCVMemRequirements &memReq) { uint64_t total = 0; - for (size_t i = 0; i < sizeof(memReq.numBlocks) / sizeof(memReq.numBlocks[0]); ++i) + for (size_t i = 0; i < std::size(memReq.numBlocks); ++i) { - uint64_t cur = memReq.numBlocks[i] * (1ull << i); + uint64_t cur = memReq.numBlocks[i] * (1ULL << i); if (total + cur < total) { throw Exception(NVCV_ERROR_INVALID_ARGUMENT, "Memory size overflow"); diff --git a/src/nvcv/src/priv/SharedCoreObj.hpp b/src/nvcv/src/priv/SharedCoreObj.hpp index 84b32b141..aa4ba30cb 100644 --- a/src/nvcv/src/priv/SharedCoreObj.hpp +++ b/src/nvcv/src/priv/SharedCoreObj.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,10 +23,30 @@ #include #include +#include #include +#include namespace nvcv::priv { +class SharedCoreObjError : public std::logic_error +{ +public: + using std::logic_error::logic_error; +}; + +template +struct SharedCoreObjRequirement; + +template +struct SharedCoreObjRequirement +{ + using type = T; +}; + +template +using SharedCoreObjRequire = typename SharedCoreObjRequirement::type; + template class SharedCoreObj { @@ -36,7 +56,7 @@ class SharedCoreObj SharedCoreObj() = default; - SharedCoreObj(std::nullptr_t) {} + explicit SharedCoreObj(std::nullptr_t) {} static SharedCoreObj FromHandle(typename CoreObj::HandleType handle, bool incRef) { @@ -62,13 +82,13 @@ class SharedCoreObj if (auto h = obj->handle()) CoreObjectIncRef(h); else - throw std::logic_error("Cannot use incRef on an object without a handle"); + throw SharedCoreObjError("Cannot use incRef on an object without a handle"); } return SharedCoreObj(std::move(obj)); } explicit SharedCoreObj(CoreObj *&&obj) - : m_obj(obj) + : m_obj(std::move(obj)) { obj = nullptr; } @@ -81,20 +101,20 @@ class SharedCoreObj SharedCoreObj(CoreObj &&obj) = delete; // Temporary workaround to avoid too many changes in the code - SharedCoreObj(CoreObj &obj) + explicit SharedCoreObj(CoreObj &obj) : m_obj(&obj) { if (auto h = obj.handle()) CoreObjectIncRef(h); } - template, int> = 0> - SharedCoreObj(const SharedCoreObj &obj) + template, int> = 0> + explicit SharedCoreObj(const SharedCoreObj &obj) { *this = obj; } - SharedCoreObj(SharedCoreObj &&obj) + SharedCoreObj(SharedCoreObj &&obj) noexcept { *this = std::move(obj); } @@ -104,19 +124,19 @@ class SharedCoreObj reset(nullptr); } - int reset(CoreObj *&&obj) + int reset(CoreObj *&&obj) noexcept { int ret = 0; if (m_obj) if (auto h = m_obj->handle()) ret = CoreObjectDecRef(h); - m_obj = obj; + m_obj = std::move(obj); obj = nullptr; return ret; } - NVCV_NODISCARD CoreObj *release() + NVCV_NODISCARD CoreObj *release() noexcept { CoreObj *ret = m_obj; m_obj = nullptr; @@ -131,9 +151,15 @@ class SharedCoreObj return *this; } - SharedCoreObj &operator=(SharedCoreObj &&obj) + SharedCoreObj &operator=(SharedCoreObj &&obj) noexcept { - reset(obj.release()); + reset(std::move(obj).release()); + return *this; + } + + SharedCoreObj &operator=(std::nullptr_t) noexcept + { + reset(nullptr); return *this; } @@ -149,7 +175,7 @@ class SharedCoreObj template SharedCoreObj &operator=(SharedCoreObj &&obj) { - reset(obj.release()); + reset(std::move(obj).release()); return *this; } @@ -188,7 +214,7 @@ class SharedCoreObj return static_cast(get()) < static_cast(other.get()); } - constexpr operator bool() const noexcept + explicit constexpr operator bool() const noexcept { return m_obj != nullptr; } @@ -206,7 +232,7 @@ constexpr bool operator==(std::nullptr_t, const SharedCoreObj &x) template constexpr bool operator!=(std::nullptr_t, const SharedCoreObj &x) { - return x; + return static_cast(x); } template @@ -218,7 +244,7 @@ constexpr bool operator==(const SharedCoreObj &x, std::nullptr_t) template constexpr bool operator!=(const SharedCoreObj &x, std::nullptr_t) { - return x; + return static_cast(x); } template diff --git a/src/nvcv/src/priv/Status.cpp b/src/nvcv/src/priv/Status.cpp index be8bccafe..d853851e3 100644 --- a/src/nvcv/src/priv/Status.cpp +++ b/src/nvcv/src/priv/Status.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,6 +25,8 @@ #include #include +#include +#include namespace nvcv::priv { @@ -32,7 +34,7 @@ void SetThreadError(std::exception_ptr e) { CoreTLS &tls = GetCoreTLS(); - const int errorMessageLen = sizeof(tls.lastErrorMessage) - 1; + const int errorMessageLen = static_cast(tls.lastErrorMessage.size()) - 1; try { @@ -43,10 +45,10 @@ void SetThreadError(std::exception_ptr e) else { tls.lastErrorStatus = NVCV_SUCCESS; - snprintf(tls.lastErrorMessage, errorMessageLen, "success"); + snprintf(tls.lastErrorMessage.data(), errorMessageLen, "success"); } } - catch (const ::nvcv::Exception &e) + catch (const ::nvcv::Exception &) { tls.lastErrorStatus = NVCV_ERROR_INTERNAL; NVCV_ASSERT(!"Exception from public API cannot be originated from internal library implementation"); @@ -54,27 +56,52 @@ void SetThreadError(std::exception_ptr e) catch (const Exception &e) { tls.lastErrorStatus = e.code(); - snprintf(tls.lastErrorMessage, errorMessageLen, "%s", e.msg()); + snprintf(tls.lastErrorMessage.data(), errorMessageLen, "%s", e.msg()); } catch (const std::invalid_argument &e) { tls.lastErrorStatus = NVCV_ERROR_INVALID_ARGUMENT; - snprintf(tls.lastErrorMessage, errorMessageLen, "%s", e.what()); + snprintf(tls.lastErrorMessage.data(), errorMessageLen, "%s", e.what()); + } + catch (const std::domain_error &e) + { + tls.lastErrorStatus = NVCV_ERROR_INTERNAL; + snprintf(tls.lastErrorMessage.data(), errorMessageLen, "%s", e.what()); + } + catch (const std::length_error &e) + { + tls.lastErrorStatus = NVCV_ERROR_INTERNAL; + snprintf(tls.lastErrorMessage.data(), errorMessageLen, "%s", e.what()); + } + catch (const std::out_of_range &e) + { + tls.lastErrorStatus = NVCV_ERROR_INTERNAL; + snprintf(tls.lastErrorMessage.data(), errorMessageLen, "%s", e.what()); } catch (const std::bad_alloc &) { tls.lastErrorStatus = NVCV_ERROR_OUT_OF_MEMORY; - snprintf(tls.lastErrorMessage, errorMessageLen, "Not enough space for resource allocation"); + snprintf(tls.lastErrorMessage.data(), errorMessageLen, "Not enough space for resource allocation"); + } + catch (const std::range_error &e) + { + tls.lastErrorStatus = NVCV_ERROR_INTERNAL; + snprintf(tls.lastErrorMessage.data(), errorMessageLen, "%s", e.what()); + } + catch (const std::overflow_error &e) + { + tls.lastErrorStatus = NVCV_ERROR_INTERNAL; + snprintf(tls.lastErrorMessage.data(), errorMessageLen, "%s", e.what()); } - catch (const std::exception &e) + catch (const std::underflow_error &e) { tls.lastErrorStatus = NVCV_ERROR_INTERNAL; - snprintf(tls.lastErrorMessage, errorMessageLen, "%s", e.what()); + snprintf(tls.lastErrorMessage.data(), errorMessageLen, "%s", e.what()); } - catch (...) + catch (...) // NOSONAR: API boundary converts any non-standard exception to NVCV status. { tls.lastErrorStatus = NVCV_ERROR_INTERNAL; - snprintf(tls.lastErrorMessage, errorMessageLen, "Unexpected error"); + snprintf(tls.lastErrorMessage.data(), errorMessageLen, "Unexpected error"); } tls.lastErrorMessage[errorMessageLen] = '\0'; // Make sure it's null-terminated @@ -104,7 +131,7 @@ NVCVStatus PeekAtLastThreadError(char *outMessage, int outMessageLen) noexcept CoreTLS &tls = GetCoreTLS(); if (outMessage != nullptr && outMessageLen > 0) - snprintf(outMessage, outMessageLen, "%s", tls.lastErrorMessage); + snprintf(outMessage, outMessageLen, "%s", tls.lastErrorMessage.data()); return tls.lastErrorStatus; } diff --git a/src/nvcv/src/priv/Status.hpp b/src/nvcv/src/priv/Status.hpp index 0765bd824..e2f56b494 100644 --- a/src/nvcv/src/priv/Status.hpp +++ b/src/nvcv/src/priv/Status.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -43,7 +43,7 @@ NVCVStatus ProtectCall(F &&fn) fn(); return NVCV_SUCCESS; } - catch (...) + catch (...) // NOSONAR: API boundary converts any non-standard exception to NVCV status. { SetThreadError(std::current_exception()); return PeekAtLastThreadError(); diff --git a/src/nvcv/src/priv/TLS.cpp b/src/nvcv/src/priv/TLS.cpp index a3df90527..8eef9e6db 100644 --- a/src/nvcv/src/priv/TLS.cpp +++ b/src/nvcv/src/priv/TLS.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,12 +20,16 @@ namespace nvcv::priv { namespace { -thread_local CoreTLS s_TLS; +CoreTLS &TLS() noexcept +{ + thread_local CoreTLS tls; + return tls; } +} // namespace CoreTLS &GetCoreTLS() noexcept { - return s_TLS; + return TLS(); } } // namespace nvcv::priv diff --git a/src/nvcv/src/priv/TLS.hpp b/src/nvcv/src/priv/TLS.hpp index 7b4bb12c2..57b208776 100644 --- a/src/nvcv/src/priv/TLS.hpp +++ b/src/nvcv/src/priv/TLS.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,39 +20,40 @@ #include +#include #include namespace nvcv::priv { -struct CoreTLS +struct CoreTLS // NOSONAR: TLS buffers stay flat so C API string-return helpers can reuse stable storage. { - NVCVStatus lastErrorStatus; - char lastErrorMessage[NVCV_MAX_STATUS_MESSAGE_LENGTH]; - - char bufColorSpecName[1024]; - char bufColorModelName[128]; - char bufChromaLocationName[128]; - char bufRawPatternName[128]; - char bufColorSpaceName[128]; - char bufColorTransferFunctionName[128]; - char bufColorRangeName[128]; - char bufWhitePointName[128]; - char bufYCbCrEncodingName[128]; - char bufChromaSubsamplingName[128]; - - char bufDataKindName[128]; - char bufMemLayoutName[128]; - char bufChannelName[128]; - char bufSwizzleName[128]; - char bufByteOrderName[128]; - char bufPackingName[128]; - - char bufDataTypeName[1024]; - char bufImageFormatName[1024]; - char bufAlphaTypeName[1024]; - char bufExtraChannelTypeName[1024]; - - char bufResourceTypeName[128]; + NVCVStatus lastErrorStatus; + std::array lastErrorMessage; + + std::array bufColorSpecName; + std::array bufColorModelName; + std::array bufChromaLocationName; + std::array bufRawPatternName; + std::array bufColorSpaceName; + std::array bufColorTransferFunctionName; + std::array bufColorRangeName; + std::array bufWhitePointName; + std::array bufYCbCrEncodingName; + std::array bufChromaSubsamplingName; + + std::array bufDataKindName; + std::array bufMemLayoutName; + std::array bufChannelName; + std::array bufSwizzleName; + std::array bufByteOrderName; + std::array bufPackingName; + + std::array bufDataTypeName; + std::array bufImageFormatName; + std::array bufAlphaTypeName; + std::array bufExtraChannelTypeName; + + std::array bufResourceTypeName; }; CoreTLS &GetCoreTLS() noexcept; diff --git a/src/nvcv/src/priv/Tensor.cpp b/src/nvcv/src/priv/Tensor.cpp index c772bc488..6f69997ae 100644 --- a/src/nvcv/src/priv/Tensor.cpp +++ b/src/nvcv/src/priv/Tensor.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -31,7 +31,9 @@ #include #include +#include #include +#include #include namespace nvcv::priv { @@ -71,16 +73,16 @@ NVCVTensorRequirements Tensor::CalcRequirements(int32_t numImages, Size2D imgSiz // Calculate the shape based on image parameters NVCVTensorLayout layout = GetTensorLayoutFor(fmt, numImages); - int64_t shapeNCHW[4] = {numImages, fmt.numChannels(), imgSize.h, imgSize.w}; + std::array shapeNCHW = {numImages, fmt.numChannels(), imgSize.h, imgSize.w}; - int64_t shape[NVCV_TENSOR_MAX_RANK]; - PermuteShape(NVCV_TENSOR_NCHW, shapeNCHW, layout, shape); + std::array shape; + PermuteShape(NVCV_TENSOR_NCHW, shapeNCHW.data(), layout, shape.data()); // Calculate the element type. It's the data type of the // first channel. It assumes that all channels have same packing. NVCVPackingParams params = GetPackingParams(fmt.planePacking(0)); params.swizzle = NVCV_SWIZZLE_X000; - std::fill(params.bits + 1, params.bits + sizeof(params.bits) / sizeof(params.bits[0]), 0); + std::fill(params.bits + 1, params.bits + std::size(params.bits), 0); std::optional chPacking = MakeNVCVPacking(params); if (!chPacking) { @@ -89,7 +91,7 @@ NVCVTensorRequirements Tensor::CalcRequirements(int32_t numImages, Size2D imgSiz DataType dtype{fmt.dataKind(), *chPacking}; - return CalcRequirements(layout.rank, shape, dtype, layout, userBaseAlign, userRowAlign); + return CalcRequirements(layout.rank, shape.data(), dtype, layout, userBaseAlign, userRowAlign); } NVCVTensorRequirements Tensor::CalcRequirements(int32_t rank, const int64_t *shape, const DataType &dtype, @@ -126,7 +128,7 @@ NVCVTensorRequirements Tensor::CalcRequirements(int32_t rank, const int64_t *sha { // it usually returns 32 bytes NVCV_CHECK_THROW(cudaDeviceGetAttribute(&rowAlign, cudaDevAttrTexturePitchAlignment, dev)); - rowAlign = std::lcm(rowAlign, util::RoundUpNextPowerOfTwo(dtype.strideBytes())); + rowAlign = static_cast(std::lcm(rowAlign, util::RoundUpNextPowerOfTwo(dtype.strideBytes()))); } else { @@ -148,7 +150,7 @@ NVCVTensorRequirements Tensor::CalcRequirements(int32_t rank, const int64_t *sha // it usually returns 512 bytes NVCV_CHECK_THROW(cudaDeviceGetAttribute(&addrAlign, cudaDevAttrTextureAlignment, dev)); reqs.alignBytes = std::lcm(addrAlign, rowAlign); - reqs.alignBytes = util::RoundUpNextPowerOfTwo(reqs.alignBytes); + reqs.alignBytes = static_cast(util::RoundUpNextPowerOfTwo(reqs.alignBytes)); if (reqs.alignBytes > NVCV_MAX_MEM_REQUIREMENTS_BLOCK_SIZE) { @@ -189,10 +191,10 @@ NVCVTensorRequirements Tensor::CalcRequirements(int32_t rank, const int64_t *sha return reqs; } -void *Tensor::AllocateBuffer(IAllocator &alloc, const NVCVTensorRequirements &reqs) +NVCVByte *Tensor::AllocateBuffer(IAllocator &alloc, const NVCVTensorRequirements &reqs) { int64_t bufSize = CalcTotalSizeBytes(reqs.mem.cudaMem); - void *buffer = alloc.allocCudaMem(bufSize, reqs.alignBytes); + auto *buffer = static_cast(static_cast(alloc.allocCudaMem(bufSize, reqs.alignBytes))); NVCV_ASSERT(buffer != nullptr); return buffer; } @@ -209,7 +211,8 @@ Tensor::Tensor(NVCVTensorRequirements reqs, IAllocator &alloc) Tensor::~Tensor() { - m_alloc->freeCudaMem(m_memBuffer, CalcTotalSizeBytes(m_reqs.mem.cudaMem), m_reqs.alignBytes); + m_alloc->freeCudaMem(static_cast(static_cast(m_memBuffer)), + CalcTotalSizeBytes(m_reqs.mem.cudaMem), m_reqs.alignBytes); } int32_t Tensor::rank() const @@ -242,7 +245,7 @@ void Tensor::exportData(NVCVTensorData &data) const data.bufferType = NVCV_TENSOR_BUFFER_STRIDED_CUDA; data.dtype = m_reqs.dtype; - data.layout = m_reqs.layout; + data.layout = static_cast(m_reqs.layout); data.rank = m_reqs.rank; std::copy_n(m_reqs.shape, NVCV_TENSOR_MAX_RANK, data.shape); @@ -254,7 +257,7 @@ void Tensor::exportData(NVCVTensorData &data) const std::is_same_v, std::decay_t>); std::copy_n(m_reqs.strides, NVCV_TENSOR_MAX_RANK, buf.strides); - buf.basePtr = reinterpret_cast(m_memBuffer); + buf.basePtr = m_memBuffer; } } diff --git a/src/nvcv/src/priv/Tensor.hpp b/src/nvcv/src/priv/Tensor.hpp index a947b0a09..06887fff7 100644 --- a/src/nvcv/src/priv/Tensor.hpp +++ b/src/nvcv/src/priv/Tensor.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -30,7 +30,7 @@ class Tensor final : public CoreObjectBase { public: explicit Tensor(NVCVTensorRequirements reqs, IAllocator &alloc); - ~Tensor(); + ~Tensor() override; static NVCVTensorRequirements CalcRequirements(int32_t numImages, Size2D imgSize, ImageFormat fmt, int32_t baseAlign, int32_t rowAlign); @@ -49,12 +49,12 @@ class Tensor final : public CoreObjectBase void exportData(NVCVTensorData &data) const override; private: - static void *AllocateBuffer(IAllocator &alloc, const NVCVTensorRequirements &reqs); + static NVCVByte *AllocateBuffer(IAllocator &alloc, const NVCVTensorRequirements &reqs); SharedCoreObj m_alloc; NVCVTensorRequirements m_reqs; - void *m_memBuffer; + NVCVByte *m_memBuffer; }; } // namespace nvcv::priv diff --git a/src/nvcv/src/priv/TensorBatch.cpp b/src/nvcv/src/priv/TensorBatch.cpp index 3c5b75887..54b2e2f0a 100644 --- a/src/nvcv/src/priv/TensorBatch.cpp +++ b/src/nvcv/src/priv/TensorBatch.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -28,29 +28,21 @@ namespace nvcv::priv { TensorBatch::TensorBatch(const NVCVTensorBatchRequirements &reqs, IAllocator &alloc) : m_alloc(alloc) , m_reqs(reqs) - , m_dirtyBegin(0) - , m_dirtyEnd(0) - , m_dtype(NVCV_DATA_TYPE_NONE) - , m_layout(NVCV_TENSOR_LAYOUT_MAKE("")) - , m_rank(-1) - , m_userPointer(nullptr) { - m_evPostFence = nullptr; - m_devTensorsBuffer = nullptr; - m_pinnedTensorsBuffer = nullptr; - m_Tensors = nullptr; - int64_t bufferSize = m_reqs.capacity * sizeof(BatchElement); try { - m_devTensorsBuffer = static_cast(m_alloc->allocCudaMem(bufferSize, m_reqs.alignBytes)); + m_devTensorsBuffer + = static_cast(static_cast(m_alloc->allocCudaMem(bufferSize, m_reqs.alignBytes))); NVCV_ASSERT(m_devTensorsBuffer != nullptr); - m_pinnedTensorsBuffer = static_cast(m_alloc->allocHostPinnedMem(bufferSize, m_reqs.alignBytes)); + m_pinnedTensorsBuffer = static_cast( + static_cast(m_alloc->allocHostPinnedMem(bufferSize, m_reqs.alignBytes))); NVCV_ASSERT(m_pinnedTensorsBuffer != nullptr); - m_Tensors = static_cast(m_alloc->allocHostMem(bufferSize, m_reqs.alignBytes)); + m_Tensors = static_cast( + static_cast(m_alloc->allocHostMem(bufferSize, m_reqs.alignBytes))); NVCV_ASSERT(m_Tensors != nullptr); NVCV_CHECK_THROW(cudaEventCreateWithFlags(&m_evPostFence, cudaEventDisableTiming)); @@ -69,7 +61,7 @@ NVCVTensorBatchRequirements TensorBatch::CalcRequirements(int32_t capacity) reqs.mem = {}; reqs.alignBytes = alignof(BatchElement); - reqs.alignBytes = util::RoundUpNextPowerOfTwo(reqs.alignBytes); + reqs.alignBytes = static_cast(util::RoundUpNextPowerOfTwo(reqs.alignBytes)); if (reqs.alignBytes > NVCV_MAX_MEM_REQUIREMENTS_BLOCK_SIZE) { @@ -104,9 +96,11 @@ void TensorBatch::cleanUp() int64_t bufferSize = m_reqs.capacity * sizeof(BatchElement); - m_alloc->freeCudaMem(m_devTensorsBuffer, bufferSize, m_reqs.alignBytes); - m_alloc->freeHostPinnedMem(m_pinnedTensorsBuffer, bufferSize, m_reqs.alignBytes); - m_alloc->freeHostMem(m_Tensors, bufferSize, m_reqs.alignBytes); + m_alloc->freeCudaMem(static_cast(static_cast(m_devTensorsBuffer)), bufferSize, + m_reqs.alignBytes); + m_alloc->freeHostPinnedMem(static_cast(static_cast(m_pinnedTensorsBuffer)), bufferSize, + m_reqs.alignBytes); + m_alloc->freeHostMem(static_cast(static_cast(m_Tensors)), bufferSize, m_reqs.alignBytes); } void TensorBatch::exportData(CUstream stream, NVCVTensorBatchData &data) @@ -118,7 +112,7 @@ void TensorBatch::exportData(CUstream stream, NVCVTensorBatchData &data) for (auto i = m_dirtyBegin; i < m_dirtyEnd; ++i) { - auto &t = ToStaticRef(m_Tensors[i]); + const auto &t = ToStaticRef(m_Tensors[i]); NVCVTensorData tdata; t.exportData(tdata); auto &element = m_pinnedTensorsBuffer[i]; @@ -144,15 +138,15 @@ void TensorBatch::exportData(CUstream stream, NVCVTensorBatchData &data) data.type = NVCV_TENSOR_BUFFER_STRIDED_CUDA; data.rank = m_rank; data.dtype = m_dtype; - data.layout = m_layout; + data.layout = static_cast(m_layout); data.numTensors = m_numTensors; } -void TensorBatch::validateTensors(const NVCVTensorHandle *tensors, int32_t numTensors) +void TensorBatch::validateTensors(const NVCVTensorHandle *tensors, int32_t numTensors) const { for (int32_t i = 0; i < numTensors; ++i) { - auto &t = ToStaticRef(tensors[i]); + const auto &t = ToStaticRef(tensors[i]); if (m_rank != -1 && t.rank() != m_rank) { throw Exception(NVCV_ERROR_INVALID_ARGUMENT, @@ -163,7 +157,8 @@ void TensorBatch::validateTensors(const NVCVTensorHandle *tensors, int32_t numTe throw Exception(NVCV_ERROR_INVALID_ARGUMENT, "Trying to add a tensor to a tensor batch with an inconsistent type."); } - if (nvcvTensorLayoutCompare(t.layout(), m_layout) != 0) + if (nvcvTensorLayoutCompare(static_cast(t.layout()), static_cast(m_layout)) + != 0) { throw Exception(NVCV_ERROR_INVALID_ARGUMENT, "Trying to add a tensor to a tensor batch with an inconsistent layout."); @@ -175,10 +170,10 @@ void TensorBatch::setLayoutAndDType(const NVCVTensorHandle *tensors, int32_t num { if (numTensors > 0 && m_numTensors == 0) { - auto &t = ToStaticRef(tensors[0]); - m_rank = t.rank(); - m_dtype = t.dtype().value(); - m_layout = t.layout(); + const auto &t = ToStaticRef(tensors[0]); + m_rank = t.rank(); + m_dtype = t.dtype().value(); + m_layout = t.layout(); } } diff --git a/src/nvcv/src/priv/TensorBatch.hpp b/src/nvcv/src/priv/TensorBatch.hpp index 9dd075962..effc13fb5 100644 --- a/src/nvcv/src/priv/TensorBatch.hpp +++ b/src/nvcv/src/priv/TensorBatch.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -38,7 +38,7 @@ class TensorBatch final : public CoreObjectBase TensorBatch(const NVCVTensorBatchRequirements &reqs, IAllocator &alloc); - ~TensorBatch(); + ~TensorBatch() override; SharedCoreObj alloc() const override; @@ -72,32 +72,30 @@ class TensorBatch final : public CoreObjectBase // Dirty begin and end describe a range containing all the tensors that have been modified // since the previous exportData call and thus should be updated in the exported buffer. - int32_t m_dirtyBegin; - int32_t m_dirtyEnd; + int32_t m_dirtyBegin = 0; + int32_t m_dirtyEnd = 0; int32_t m_numTensors = 0; - NVCVTensorHandle *m_Tensors; // host buffer for tensor handles + NVCVTensorHandle *m_Tensors = nullptr; // host buffer for tensor handles // Pinned buffer for the tensor data descriptors // It's updated every time the user updates the tensor batch. // Changes are tracked with the m_dirty flags. - NVCVTensorBatchElementStrided *m_pinnedTensorsBuffer; + NVCVTensorBatchElementStrided *m_pinnedTensorsBuffer = nullptr; // Device buffer for the tensor data descriptors. // It's updated and returned when the exportData method is called. - NVCVTensorBatchElementStrided *m_devTensorsBuffer; + NVCVTensorBatchElementStrided *m_devTensorsBuffer = nullptr; - NVCVDataType m_dtype; - NVCVTensorLayout m_layout; - int32_t m_rank; + NVCVDataType m_dtype = NVCV_DATA_TYPE_NONE; + NVCVTensorLayout m_layout = NVCV_TENSOR_LAYOUT_MAKE(""); + int32_t m_rank = -1; - // TODO: must be retrieved from the resource allocator; - cudaEvent_t m_evPostFence; - - void *m_userPointer; + // REVISIT: must be retrieved from the resource allocator; + cudaEvent_t m_evPostFence = nullptr; void cleanUp(); - void validateTensors(const NVCVTensorHandle *tensors, int32_t numTensors); + void validateTensors(const NVCVTensorHandle *tensors, int32_t numTensors) const; void setLayoutAndDType(const NVCVTensorHandle *tensors, int32_t numTensors); }; diff --git a/src/nvcv/src/priv/TensorData.cpp b/src/nvcv/src/priv/TensorData.cpp index 579ff6e91..7c3a9ffcf 100644 --- a/src/nvcv/src/priv/TensorData.cpp +++ b/src/nvcv/src/priv/TensorData.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,6 +23,7 @@ #include +#include #include namespace nvcv::priv { @@ -47,18 +48,14 @@ NVCVTensorLayout GetTensorLayoutFor(ImageFormat fmt, int nbatches) } } -void FillTensorData(IImage &img, NVCVTensorData &tensorData) +static void ValidateTensorCompatibleFormat(ImageFormat fmt) { - ImageFormat fmt = img.format(); - - // Must do a lot of checks to see if image is compatible with a tensor representation. - - if (img.format().memLayout() != NVCV_MEM_LAYOUT_PL) + if (fmt.memLayout() != NVCV_MEM_LAYOUT_PL) { throw Exception(NVCV_ERROR_INVALID_ARGUMENT) << "Image format's memory layout must be pitch-linear"; } - if (img.format().css() != NVCV_CSS_444) + if (fmt.css() != NVCV_CSS_444) { throw Exception(NVCV_ERROR_INVALID_ARGUMENT) << "Image format's memory layout must not have sub-sampled planes"; } @@ -70,18 +67,24 @@ void FillTensorData(IImage &img, NVCVTensorData &tensorData) throw Exception(NVCV_ERROR_INVALID_ARGUMENT) << "Data type of all image planes must be the same"; } } +} - NVCVImageData imgData; - img.exportData(imgData); - +static void ValidateTensorCompatibleData(const NVCVImageData &imgData) +{ if (imgData.bufferType != NVCV_IMAGE_BUFFER_STRIDED_CUDA) { throw Exception(NVCV_ERROR_INVALID_ARGUMENT) << "Only cuda-accessible images with pitch-linear data are accepted"; } +} - NVCVImageBufferStrided &imgStrided = imgData.buffer.strided; +static intptr_t PlaneAddressStride(const NVCVImagePlaneStrided &prev, const NVCVImagePlaneStrided &next) +{ + return reinterpret_cast(next.basePtr) - reinterpret_cast(prev.basePtr); +} +static void ValidateTensorCompatiblePlanes(const NVCVImageBufferStrided &imgStrided) +{ for (int p = 1; p < imgStrided.numPlanes; ++p) { if (imgStrided.planes[p].width != imgStrided.planes[0].width @@ -103,89 +106,102 @@ void FillTensorData(IImage &img, NVCVTensorData &tensorData) if (p >= 2) { - intptr_t planeStride = reinterpret_cast(imgStrided.planes[1].basePtr) - - reinterpret_cast(imgStrided.planes[0].basePtr); + intptr_t planeStride = PlaneAddressStride(imgStrided.planes[0], imgStrided.planes[1]); - if (reinterpret_cast(imgStrided.planes[p].basePtr) - - reinterpret_cast(imgStrided.planes[p - 1].basePtr) - != planeStride) + if (PlaneAddressStride(imgStrided.planes[p - 1], imgStrided.planes[p]) != planeStride) { throw Exception(NVCV_ERROR_INVALID_ARGUMENT) << "Image planes must have the same plane pitch"; } } } +} - // Now fill up tensor data with image data - - tensorData = {}; // start everything afresh - tensorData.bufferType = NVCV_TENSOR_BUFFER_STRIDED_CUDA; - - NVCVTensorBufferStrided &tensorStrided = tensorData.buffer.strided; - - // Infer layout and shape - std::array bits = fmt.bpc(); - bool sameBPC = true; +static bool HasSameBitsPerChannel(ImageFormat fmt) +{ + std::array bits = fmt.bpc(); for (int i = 1; i < fmt.numChannels(); ++i) { if (bits[i] != bits[0]) { - sameBPC = false; - break; + return false; } } + return true; +} - if (imgStrided.numPlanes == 1) - { - if (fmt.numChannels() >= 2 && sameBPC) - { - // If same BPC, we can have channels as its own dimension, - // as all channels have the same type. - tensorData.layout = NVCV_TENSOR_NHWC; - } - else - { - tensorData.layout = NVCV_TENSOR_NCHW; - } - } - else +static NVCVTensorLayout InferTensorLayout(const NVCVImageBufferStrided &imgStrided, ImageFormat fmt) +{ + if (imgStrided.numPlanes == 1 && fmt.numChannels() >= 2 && HasSameBitsPerChannel(fmt)) { - tensorData.layout = NVCV_TENSOR_NCHW; + // If same BPC, we can have channels as its own dimension, + // as all channels have the same type. + return NVCV_TENSOR_NHWC; } - tensorData.rank = 4; - if (tensorData.layout == NVCV_TENSOR_NHWC) - { - tensorData.shape[0] = 1; - tensorData.shape[1] = imgStrided.planes[0].height; - tensorData.shape[2] = imgStrided.planes[0].width; - tensorData.shape[3] = fmt.numChannels(); + return NVCV_TENSOR_NCHW; +} + +static void FillTensorStridedNHWC(const NVCVImageBufferStrided &imgStrided, ImageFormat fmt, NVCVTensorData &tensorData) +{ + tensorData.shape[0] = 1; + tensorData.shape[1] = imgStrided.planes[0].height; + tensorData.shape[2] = imgStrided.planes[0].width; + tensorData.shape[3] = fmt.numChannels(); + + NVCVTensorBufferStrided &tensorStrided = tensorData.buffer.strided; + tensorStrided.strides[3] = fmt.planePixelStrideBytes(0) / fmt.numChannels(); + tensorStrided.strides[2] = fmt.planePixelStrideBytes(0); + tensorStrided.strides[1] = imgStrided.planes[0].rowStride; + tensorStrided.strides[0] = tensorStrided.strides[1] * tensorData.shape[1]; + + tensorData.dtype = fmt.planeDataType(0).channelType(0).value(); +} + +static void FillTensorStridedNCHW(const NVCVImageBufferStrided &imgStrided, ImageFormat fmt, NVCVTensorData &tensorData) +{ + tensorData.shape[0] = 1; + tensorData.shape[1] = imgStrided.numPlanes; + tensorData.shape[2] = imgStrided.planes[0].height; + tensorData.shape[3] = imgStrided.planes[0].width; + + NVCVTensorBufferStrided &tensorStrided = tensorData.buffer.strided; + tensorStrided.strides[3] = fmt.planePixelStrideBytes(0); + tensorStrided.strides[2] = imgStrided.planes[0].rowStride; + tensorStrided.strides[1] = tensorStrided.strides[2] * tensorData.shape[2]; + tensorStrided.strides[0] = tensorStrided.strides[1] * tensorData.shape[1]; - tensorStrided.strides[3] = fmt.planePixelStrideBytes(0) / fmt.numChannels(); - tensorStrided.strides[2] = fmt.planePixelStrideBytes(0); - tensorStrided.strides[1] = imgStrided.planes[0].rowStride; - tensorStrided.strides[0] = tensorStrided.strides[1] * tensorData.shape[1]; + tensorData.dtype = fmt.planeDataType(0).value(); +} - tensorData.dtype = fmt.planeDataType(0).channelType(0).value(); +void FillTensorData(const IImage &img, NVCVTensorData &tensorData) +{ + ImageFormat fmt = img.format(); + ValidateTensorCompatibleFormat(fmt); + + NVCVImageData imgData; + img.exportData(imgData); + ValidateTensorCompatibleData(imgData); + + NVCVImageBufferStrided &imgStrided = imgData.buffer.strided; + ValidateTensorCompatiblePlanes(imgStrided); + + tensorData = {}; + tensorData.bufferType = NVCV_TENSOR_BUFFER_STRIDED_CUDA; + tensorData.layout = InferTensorLayout(imgStrided, fmt); + tensorData.rank = 4; + + if (tensorData.layout == NVCV_TENSOR_NHWC) + { + FillTensorStridedNHWC(imgStrided, fmt, tensorData); } else { NVCV_ASSERT(tensorData.layout == NVCV_TENSOR_NCHW); - - tensorData.shape[0] = 1; - tensorData.shape[1] = imgStrided.numPlanes; - tensorData.shape[2] = imgStrided.planes[0].height; - tensorData.shape[3] = imgStrided.planes[0].width; - - tensorStrided.strides[3] = fmt.planePixelStrideBytes(0); - tensorStrided.strides[2] = imgStrided.planes[0].rowStride; - tensorStrided.strides[1] = tensorStrided.strides[2] * tensorData.shape[2]; - tensorStrided.strides[0] = tensorStrided.strides[1] * tensorData.shape[1]; - - tensorData.dtype = fmt.planeDataType(0).value(); + FillTensorStridedNCHW(imgStrided, fmt, tensorData); } // Finally, assign the pointer to the memory buffer. - tensorStrided.basePtr = imgStrided.planes[0].basePtr; + tensorData.buffer.strided.basePtr = imgStrided.planes[0].basePtr; } /** @@ -198,7 +214,7 @@ void FillTensorData(IImage &img, NVCVTensorData &tensorData) * @param[out] out_strides * @return int out_rank */ -static int Simplify(int rank, int64_t *shape, int64_t *stride, int64_t *out_shape, int64_t *out_strides) +static int Simplify(int rank, const int64_t *shape, const int64_t *stride, int64_t *out_shape, int64_t *out_strides) { if (rank <= 1) // Nothing to simplify { @@ -248,8 +264,9 @@ static int Simplify(int rank, int64_t *shape, int64_t *stride, int64_t *out_shap static bool ReshapeSimplified(int in_rank, const int64_t *in_shape, const int64_t *in_strides, int target_rank, const int64_t *target_shape, int64_t *out_strides) { - int i = 0, j = 0; - for (; i < in_rank && j < target_rank; i++) + int i = 0; + int j = 0; + for (; i < in_rank && j < target_rank; i++) // NOSONAR: input and target ranks advance independently. { int64_t in_e = in_shape[i]; int64_t out_v = 1; @@ -288,7 +305,7 @@ void ReshapeTensorData(NVCVTensorData &tensor_data, int new_rank, const int64_t int64_t old_volume = 1; for (int d = 0; d < tensor_data.rank; d++) old_volume *= tensor_data.shape[d]; - // TODO: Add 0D tensor support, once it's supported accross the board + // REVISIT: Add 0D tensor support, once it's supported accross the board if (new_rank < 1 || new_rank > NVCV_TENSOR_MAX_RANK) throw Exception(NVCV_ERROR_INVALID_ARGUMENT) << "Number of dimensions must be between 1 and " << NVCV_TENSOR_MAX_RANK << ", not " << new_rank; @@ -304,13 +321,12 @@ void ReshapeTensorData(NVCVTensorData &tensor_data, int new_rank, const int64_t } // layout ------------ - if (new_layout.rank > 0) + if (new_layout.rank > 0 && new_layout.rank != new_rank) { - if (new_layout.rank != new_rank) - throw Exception(NVCV_ERROR_INVALID_ARGUMENT) - << "The number of dimensions of the provided layout and shape do not match. Got a " - "shape with " - << new_rank << " dimensions and a layout with " << new_layout.rank << " dimensions"; + throw Exception(NVCV_ERROR_INVALID_ARGUMENT) + << "The number of dimensions of the provided layout and shape do not match. Got a " + "shape with " + << new_rank << " dimensions and a layout with " << new_layout.rank << " dimensions"; } tensor_data.layout = new_layout; @@ -326,15 +342,15 @@ void ReshapeTensorData(NVCVTensorData &tensor_data, int new_rank, const int64_t // Example 2: // A tensor with shape (480, 640, 3) and strides (2560, 3, 1) // will be collapsed into (921600,) with strides (1,). - int64_t simplified_shape[NVCV_TENSOR_MAX_RANK]; - int64_t simplified_strides[NVCV_TENSOR_MAX_RANK]; - int simplified_rank = Simplify(tensor_data.rank, tensor_data.shape, tensor_data.buffer.strided.strides, - simplified_shape, simplified_strides); + std::array simplified_shape; + std::array simplified_strides; + int simplified_rank = Simplify(tensor_data.rank, tensor_data.shape, tensor_data.buffer.strided.strides, + simplified_shape.data(), simplified_strides.data()); // Calculate output strides (if reshape is possible) or throw an error - bool ret = ReshapeSimplified(simplified_rank, simplified_shape, simplified_strides, new_rank, new_shape, - tensor_data.buffer.strided.strides); - if (!ret) + if (bool ret = ReshapeSimplified(simplified_rank, simplified_shape.data(), simplified_strides.data(), new_rank, + new_shape, tensor_data.buffer.strided.strides); + !ret) { throw Exception(NVCV_ERROR_INVALID_ARGUMENT) << "Cannot reshape" diff --git a/src/nvcv/src/priv/TensorData.hpp b/src/nvcv/src/priv/TensorData.hpp index a227ac19a..5b13f557c 100644 --- a/src/nvcv/src/priv/TensorData.hpp +++ b/src/nvcv/src/priv/TensorData.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -28,7 +28,7 @@ namespace nvcv::priv { NVCVTensorLayout GetTensorLayoutFor(ImageFormat fmt, int nbatches); -void FillTensorData(IImage &img, NVCVTensorData &data); +void FillTensorData(const IImage &img, NVCVTensorData &data); void ReshapeTensorData(NVCVTensorData &tensor_data, int new_rank, const int64_t *new_shape, NVCVTensorLayout new_layout); diff --git a/src/nvcv/src/priv/TensorLayout.cpp b/src/nvcv/src/priv/TensorLayout.cpp index bc3800b3d..9f5e845ab 100644 --- a/src/nvcv/src/priv/TensorLayout.cpp +++ b/src/nvcv/src/priv/TensorLayout.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,6 +22,7 @@ #include #include +#include namespace nvcv::priv { @@ -44,7 +45,7 @@ NVCVTensorLayout CreateLayout(const char *beg, const char *end) } NVCVTensorLayout out; - out.rank = end - beg; + out.rank = static_cast(end - beg); std::copy(beg, end, out.data); return out; @@ -70,19 +71,19 @@ NVCVTensorLayout CreateLayout(const char *descr) { // Avoids going through the whole descr buffer, which might pose a // security hazard. - char buf[32]; - int reqs = snprintf(buf, sizeof(buf), "%s", descr); + std::array buf; + int reqs = snprintf(buf.data(), buf.size(), "%s", descr); if (reqs < 0) { reqs = 0; buf[0] = '\0'; } throw Exception(NVCV_ERROR_INVALID_ARGUMENT) - << "Tensor layout description is too big, must have at most 16 labels: " << buf - << (reqs <= static_cast(sizeof(buf)) - 1 ? "" : "..."); + << "Tensor layout description is too big, must have at most 16 labels: " << buf.data() + << (reqs <= static_cast(buf.size()) - 1 ? "" : "..."); } - out.rank = cur - descr; + out.rank = static_cast(cur - descr); NVCV_ASSERT(0 <= out.rank && (size_t)out.rank < sizeof(out.data) / sizeof(out.data[0])); out.data[out.rank] = '\0'; // add null terminator } @@ -93,7 +94,8 @@ int FindDimIndex(const NVCVTensorLayout &layout, char dimLabel) { if (const void *p = memchr(layout.data, dimLabel, layout.rank)) { - return std::distance(reinterpret_cast(layout.data), reinterpret_cast(p)); + return static_cast( + std::distance(reinterpret_cast(layout.data), reinterpret_cast(p))); } else { diff --git a/src/nvcv/src/priv/TensorWrapDataStrided.cpp b/src/nvcv/src/priv/TensorWrapDataStrided.cpp index c199668d5..3d46e6b73 100644 --- a/src/nvcv/src/priv/TensorWrapDataStrided.cpp +++ b/src/nvcv/src/priv/TensorWrapDataStrided.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -62,8 +62,8 @@ static void ValidateTensorBufferStrided(const NVCVTensorData &tdata) int firstPacked = IsChannelLast(tdata.layout) ? std::max(0, rank - 2) : rank - 1; - int prevStrideConsidered = buffer.strides[rank - 1]; - int prevSizeConsidered = tdata.shape[rank - 1]; + int64_t prevStrideConsidered = buffer.strides[rank - 1]; + int64_t prevSizeConsidered = tdata.shape[rank - 1]; // Test packed dimensions int dim; @@ -74,8 +74,8 @@ static void ValidateTensorBufferStrided(const NVCVTensorData &tdata) continue; } - int correctPitch = dim == rank - 1 ? dtype.strideBytes() : prevStrideConsidered * prevSizeConsidered; - if (buffer.strides[dim] != correctPitch) + if (int64_t correctPitch = dim == rank - 1 ? dtype.strideBytes() : prevStrideConsidered * prevSizeConsidered; + buffer.strides[dim] != correctPitch) { throw Exception(NVCV_ERROR_INVALID_ARGUMENT) << "Pitch of dimension " << dim << " must be == " << correctPitch << " (packed)" @@ -93,8 +93,7 @@ static void ValidateTensorBufferStrided(const NVCVTensorData &tdata) continue; } - int minPitch = prevStrideConsidered * prevSizeConsidered; - if (buffer.strides[dim] < minPitch) + if (int64_t minPitch = prevStrideConsidered * prevSizeConsidered; buffer.strides[dim] < minPitch) { throw Exception(NVCV_ERROR_INVALID_ARGUMENT) << "Pitch of dimension " << dim << " must be >= " << minPitch << ", but it is " << buffer.strides[dim]; @@ -105,7 +104,7 @@ static void ValidateTensorBufferStrided(const NVCVTensorData &tdata) } TensorWrapDataStrided::TensorWrapDataStrided(const NVCVTensorData &tdata, NVCVTensorDataCleanupFunc cleanup, - void *ctxCleanup) + NVCVUserPointer ctxCleanup) : m_tdata(tdata) , m_cleanup(cleanup) , m_ctxCleanup(ctxCleanup) @@ -143,7 +142,7 @@ DataType TensorWrapDataStrided::dtype() const SharedCoreObj TensorWrapDataStrided::alloc() const { - return GetDefaultAllocator(); + return SharedCoreObj{GetDefaultAllocator()}; } void TensorWrapDataStrided::exportData(NVCVTensorData &tdata) const diff --git a/src/nvcv/src/priv/TensorWrapDataStrided.hpp b/src/nvcv/src/priv/TensorWrapDataStrided.hpp index 039871875..198c650fb 100644 --- a/src/nvcv/src/priv/TensorWrapDataStrided.hpp +++ b/src/nvcv/src/priv/TensorWrapDataStrided.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -27,8 +27,9 @@ namespace nvcv::priv { class TensorWrapDataStrided final : public CoreObjectBase { public: - explicit TensorWrapDataStrided(const NVCVTensorData &tdata, NVCVTensorDataCleanupFunc cleanup, void *ctxCleanup); - ~TensorWrapDataStrided(); + explicit TensorWrapDataStrided(const NVCVTensorData &tdata, NVCVTensorDataCleanupFunc cleanup, + NVCVUserPointer ctxCleanup); + ~TensorWrapDataStrided() override; int32_t rank() const override; const int64_t *shape() const override; @@ -45,7 +46,7 @@ class TensorWrapDataStrided final : public CoreObjectBase NVCVTensorData m_tdata; NVCVTensorDataCleanupFunc m_cleanup; - void *m_ctxCleanup; + NVCVUserPointer m_ctxCleanup; }; } // namespace nvcv::priv diff --git a/src/nvcv/util/Assert.h b/src/nvcv/util/Assert.h index 908b087fa..dd49af238 100644 --- a/src/nvcv/util/Assert.h +++ b/src/nvcv/util/Assert.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -39,13 +39,19 @@ NVCV_ASSERT_NORETURN void NvCVAssert(const char *file, int line, const char *con #endif #if NVCV_EXPOSE_CODE -# define NVCV_SOURCE_FILE_NAME __FILE__ -# define NVCV_SOURCE_FILE_LINENO __LINE__ +# define NVCV_SOURCE_FILE_NAME __FILE__ // NOSONAR: std::source_location is C++20. +# define NVCV_SOURCE_FILE_LINENO __LINE__ // NOSONAR: std::source_location is C++20. # define NVCV_OPTIONAL_STRINGIFY(X) # X #else -# define NVCV_SOURCE_FILE_NAME "" -# define NVCV_SOURCE_FILE_LINENO 0 -# define NVCV_OPTIONAL_STRINGIFY(X) "" +static const char NVCV_SOURCE_FILE_NAME[] = ""; +static const char NVCV_HIDDEN_CONDITION_TEXT[] = ""; + +enum +{ + NVCV_SOURCE_FILE_LINENO = 0 +}; + +# define NVCV_OPTIONAL_STRINGIFY(X) NVCV_HIDDEN_CONDITION_TEXT #endif // allows overriding of NVCV_ASSERT definition diff --git a/src/nvcv/util/CheckError.cpp b/src/nvcv/util/CheckError.cpp index d711bc645..4ab0dbd3d 100644 --- a/src/nvcv/util/CheckError.cpp +++ b/src/nvcv/util/CheckError.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,7 +19,6 @@ #include -#include #include #include @@ -42,7 +41,7 @@ static std::string_view GetFunctionName(const std::string_view &stmt) } namespace detail { -const char *GetCheckMessage(char *buf, int bufsize) +const char *GetCheckMessage(const char *buf, int bufsize) { NVCV_ASSERT(buf != nullptr); (void)buf; @@ -51,27 +50,12 @@ const char *GetCheckMessage(char *buf, int bufsize) return ""; } -char *GetCheckMessage(char *buf, int bufsize, const char *fmt, ...) -{ - NVCV_ASSERT(buf != nullptr); - NVCV_ASSERT(fmt != nullptr); - - va_list va; - va_start(va, fmt); - - vsnprintf(buf, bufsize - 1, fmt, va); - - va_end(va); - - return buf; -} - std::string FormatErrorMessage(const std::string_view &errname, const std::string_view &callstr, const std::string_view &msg) { std::string_view funcName = GetFunctionName(callstr); - // TODO: avoid heap memory allocation here + // REVISIT: avoid heap memory allocation here std::ostringstream ss; ss << '('; if (!funcName.empty()) @@ -108,7 +92,7 @@ NVCVStatus TranslateError(cudaError_t err) } } -void PreprocessError(cudaError_t err) +void PreprocessError(cudaError_t) { // consume the error cudaGetLastError(); diff --git a/src/nvcv/util/CheckError.hpp b/src/nvcv/util/CheckError.hpp index b24c5c770..aee3ef374 100644 --- a/src/nvcv/util/CheckError.hpp +++ b/src/nvcv/util/CheckError.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,11 +21,14 @@ #include "Assert.h" #include // for cudaError +#include +#include #include #include #include #include +#include #if NVCV_EXPORTING # include @@ -48,8 +51,19 @@ namespace nvcv::util { namespace detail { -const char *GetCheckMessage(char *buf, int buflen); -char *GetCheckMessage(char *buf, int buflen, const char *fmt, ...); +using CheckMessageBuffer = std::array; + +const char *GetCheckMessage(const char *buf, int buflen); + +template +char *GetCheckMessage(char *buf, int buflen, const char (&fmt)[N], Args &&...args) +{ + NVCV_ASSERT(buf != nullptr); + nvcv::detail::FormatTo(buf, static_cast(buflen), fmt, std::forward(args)...); + buf[buflen - 1] = '\0'; + return buf; +} + std::string FormatErrorMessage(const std::string_view &errname, const std::string_view &callstr, const std::string_view &msg); } // namespace detail @@ -83,6 +97,7 @@ NVCVStatus TranslateError(T err) template inline void PreprocessError(T err) { + (void)err; } namespace detail { @@ -114,7 +129,7 @@ void DoThrow(T error, const char *file, int line, const std::string_view &stmt, template void DoLog(T error, const char *file, int line, const std::string_view &stmt, const std::string_view &errmsg) { - // TODO: replace with a real log facility + // REVISIT: replace with a real log facility // Can we expose source file data? if (file != nullptr) @@ -126,20 +141,20 @@ void DoLog(T error, const char *file, int line, const std::string_view &stmt, co } // namespace detail -#define NVCV_CHECK_THROW(STMT, ...) \ - [&]() \ - { \ - using ::nvcv::util::PreprocessError; \ - using ::nvcv::util::CheckSucceeded; \ - auto status = (STMT); \ - PreprocessError(status); \ - if (!CheckSucceeded(status)) \ - { \ - char buf[NVCV_MAX_STATUS_MESSAGE_LENGTH]; \ - ::nvcv::util::detail::DoThrow(status, NVCV_SOURCE_FILE_NAME, NVCV_SOURCE_FILE_LINENO, \ - NVCV_OPTIONAL_STRINGIFY(STMT), \ - ::nvcv::util::detail::GetCheckMessage(buf, sizeof(buf), ##__VA_ARGS__)); \ - } \ +#define NVCV_CHECK_THROW(STMT, ...) \ + [&]() \ + { \ + using ::nvcv::util::PreprocessError; \ + using ::nvcv::util::CheckSucceeded; \ + auto status = (STMT); \ + PreprocessError(status); \ + if (!CheckSucceeded(status)) \ + { \ + ::nvcv::util::detail::CheckMessageBuffer buf; \ + ::nvcv::util::detail::DoThrow( \ + status, NVCV_SOURCE_FILE_NAME, NVCV_SOURCE_FILE_LINENO, NVCV_OPTIONAL_STRINGIFY(STMT), \ + ::nvcv::util::detail::GetCheckMessage(buf.data(), static_cast(buf.size()), ##__VA_ARGS__)); \ + } \ }() #define NVCV_CHECK_LOG(STMT, ...) \ @@ -151,10 +166,10 @@ void DoLog(T error, const char *file, int line, const std::string_view &stmt, co PreprocessError(status); \ if (!CheckSucceeded(status)) \ { \ - char buf[NVCV_MAX_STATUS_MESSAGE_LENGTH]; \ - ::nvcv::util::detail::DoLog(status, NVCV_SOURCE_FILE_NAME, NVCV_SOURCE_FILE_LINENO, \ - NVCV_OPTIONAL_STRINGIFY(STMT), \ - ::nvcv::util::detail::GetCheckMessage(buf, sizeof(buf), ##__VA_ARGS__)); \ + ::nvcv::util::detail::CheckMessageBuffer buf; \ + ::nvcv::util::detail::DoLog( \ + status, NVCV_SOURCE_FILE_NAME, NVCV_SOURCE_FILE_LINENO, NVCV_OPTIONAL_STRINGIFY(STMT), \ + ::nvcv::util::detail::GetCheckMessage(buf.data(), static_cast(buf.size()), ##__VA_ARGS__)); \ return false; \ } \ else \ diff --git a/src/nvcv/util/Compat.cpp b/src/nvcv/util/Compat.cpp index e018475a6..51f5f36c3 100644 --- a/src/nvcv/util/Compat.cpp +++ b/src/nvcv/util/Compat.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,6 +22,7 @@ #include #include +#include #include // ================================================ @@ -33,6 +34,12 @@ namespace { +struct ThreadDestructorArg; +struct DsoHandle; + +using ThreadDestructor = void(ThreadDestructorArg *); +using PthreadDestructorArg = void; + // Encapsulates the original thread_atexit implementation // by retrieving the function from libc.so.6, if it exists. class OrigImpl @@ -40,13 +47,18 @@ class OrigImpl public: OrigImpl() { - m_libc = dlopen("libc.so.6", RTLD_LOCAL | RTLD_LAZY); + m_libc = static_cast(dlopen("libc.so.6", RTLD_LOCAL | RTLD_LAZY)); if (m_libc) { - m_fn = (OrigImplFn)dlvsym(m_libc, "__cxa_thread_atexit_impl", "GLIBC_2.18"); + m_fn = reinterpret_cast(dlvsym(m_libc, "__cxa_thread_atexit_impl", "GLIBC_2.18")); } } + OrigImpl(const OrigImpl &) = delete; + OrigImpl &operator=(const OrigImpl &) = delete; + OrigImpl(OrigImpl &&) = delete; + OrigImpl &operator=(OrigImpl &&) = delete; + ~OrigImpl() { if (m_libc) @@ -60,41 +72,58 @@ class OrigImpl return m_fn != nullptr; } - int operator()(void (*func)(void *), void *arg, void *d) const + int operator()(ThreadDestructor *func, ThreadDestructorArg *arg, DsoHandle *d) const { assert(m_fn); return m_fn(func, arg, d); } private: - void *m_libc = nullptr; + DsoHandle *m_libc = nullptr; - using OrigImplFn = int (*)(void (*func)(void *), void *arg, void *d); + using OrigImplFn = int (*)(ThreadDestructor *func, ThreadDestructorArg *arg, DsoHandle *d); OrigImplFn m_fn = nullptr; }; struct DestructorInfo { - void (*func)(void *); - void *arg; + ThreadDestructor *func; + ThreadDestructorArg *arg; }; // Called upon thread destruction (not main thread!) -void my_thread_atexit_cleanup(void *arg) +void my_thread_atexit_cleanup(ThreadDestructorArg *arg) { - auto *list = reinterpret_cast *>(arg); + std::unique_ptr> list( + static_cast *>(static_cast(arg))); + if (!list) + { + return; + } // Call all destructors for (DestructorInfo &info : *list) { info.func(info.arg); } +} + +void pthread_thread_atexit_cleanup(PthreadDestructorArg *arg) +{ + my_thread_atexit_cleanup(static_cast(arg)); +} - delete list; +pthread_key_t &ThreadKey() +{ + static pthread_key_t key; + return key; } -static pthread_key_t g_key; -static pthread_once_t g_keyOnce = PTHREAD_ONCE_INIT; +pthread_once_t &ThreadKeyOnce() +{ + static pthread_once_t keyOnce = PTHREAD_ONCE_INIT; + return keyOnce; +} bool IsMainThread() { @@ -103,29 +132,56 @@ bool IsMainThread() return syscall(SYS_gettid) == getpid(); } -// Destructor list to be used for objects in main thread. -static std::vector *g_ListMainThread = nullptr; +std::vector *&MainThreadDestructorList() +{ + // Destructor list to be used for objects in main thread. + static std::vector *list = nullptr; + return list; +} -int my_thread_atexit_impl(void (*func)(void *), void *arg, void *d) +std::unique_ptr> CreateDestructorList() noexcept +{ + try + { + return std::make_unique>(); + } + catch (...) + { + return nullptr; + } +} + +int my_thread_atexit_impl(ThreadDestructor *func, ThreadDestructorArg *arg, [[maybe_unused]] DsoHandle *d) { std::vector *list = nullptr; if (IsMainThread()) { + auto &mainThreadList = MainThreadDestructorList(); + // List not created yet? - if (g_ListMainThread == nullptr) + if (mainThreadList == nullptr) { // Create it! - list = new (std::nothrow) std::vector(); - if (list == nullptr) + auto newList = CreateDestructorList(); + if (!newList) { return -1; } - g_ListMainThread = list; + list = newList.get(); + mainThreadList = newList.release(); // Make sure it's cleaned up when main thread exits. - atexit([] { my_thread_atexit_cleanup(g_ListMainThread); }); + atexit( + [] { + my_thread_atexit_cleanup( + static_cast(static_cast(MainThreadDestructorList()))); + }); + } + else + { + list = mainThreadList; } } else @@ -135,31 +191,37 @@ int my_thread_atexit_impl(void (*func)(void *), void *arg, void *d) // to set up the destructor. // Make sure we create the key only once - pthread_once(&g_keyOnce, + pthread_once(&ThreadKeyOnce(), [] { // At every thread destruction (not main thread!), it'll call the cleanup function, passing // the list as parameter. - int ret = pthread_key_create(&g_key, &my_thread_atexit_cleanup); + int ret = pthread_key_create(&ThreadKey(), &pthread_thread_atexit_cleanup); (void)ret; assert(ret == 0); }); // TLS list not created yet? - list = reinterpret_cast *>(pthread_getspecific(g_key)); + list = static_cast *>(pthread_getspecific(ThreadKey())); if (list == nullptr) { // Create it! - list = new (std::nothrow) std::vector(); - if (list == nullptr) + auto newList = CreateDestructorList(); + if (!newList) { return -1; } + list = newList.get(); // Assign it to current thread! - int ret = pthread_setspecific(g_key, list); + int ret = pthread_setspecific(ThreadKey(), list); (void)ret; assert(ret == 0); + if (ret != 0) + { + return -1; + } + newList.release(); } } @@ -180,7 +242,7 @@ extern "C" #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wmissing-declarations" -__attribute__((weak)) int __cxa_thread_atexit_impl(void (*func)(void *), void *arg, void *d) +__attribute__((weak)) int __cxa_thread_atexit_impl(ThreadDestructor *func, ThreadDestructorArg *arg, DsoHandle *d) { static OrigImpl origImpl; diff --git a/src/nvcv/util/Compiler.hpp b/src/nvcv/util/Compiler.hpp index ef4a0a1f6..521b65080 100644 --- a/src/nvcv/util/Compiler.hpp +++ b/src/nvcv/util/Compiler.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -34,14 +34,6 @@ # define NVCV_CUDACC_VERSION (__CUDACC_VER_MAJOR__ * 10000 + __CUDACC_VER_MINOR__ * 100 + __CUDACC_VER_BUILD__) #endif -#if defined(_WIN32) -# define NVCV_WINDOWS 1 -#endif - -#if defined(__unix__) -# define NVCV_UNIX 1 -#endif - #if NVCV_GCC_VERSION || NVCV_CLANG_VERSION # define NVCV_FORCE_INLINE __attribute__((always_inline)) inline # define NVCV_NO_INLINE __attribute__((noinline)) diff --git a/src/nvcv/util/Math.hpp b/src/nvcv/util/Math.hpp index b708fcef1..b200e19cc 100644 --- a/src/nvcv/util/Math.hpp +++ b/src/nvcv/util/Math.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -27,21 +27,27 @@ namespace nvcv::util { -template && std::is_integral_v>> +template NVCV_CUDA_HOST_DEVICE constexpr T RoundUp(T value, U multiple) { + static_assert(std::is_integral_v && std::is_integral_v, "RoundUp requires integral arguments"); + return (value + multiple - 1) / multiple * multiple; } -template>> +template NVCV_CUDA_HOST_DEVICE constexpr bool IsPowerOfTwo(T value) { + static_assert(std::is_integral_v, "IsPowerOfTwo requires an integral argument"); + return (value & (value - 1)) == 0; } -template>> +template NVCV_CUDA_HOST_DEVICE constexpr auto RoundUpNextPowerOfTwo(T x) { + static_assert(std::is_integral_v, "RoundUpNextPowerOfTwo requires an integral argument"); + assert(x >= 0); // Source: Hacker's Delight 1st ed, p.48, @@ -68,18 +74,22 @@ NVCV_CUDA_HOST_DEVICE constexpr auto RoundUpNextPowerOfTwo(T x) } } -template>> +template NVCV_CUDA_HOST_DEVICE constexpr auto DivUp(T num, TypeIdentity den) { + static_assert(std::is_integral_v, "DivUp requires integral arguments"); + assert(num >= 0); assert(den > 0); return (num + (den - 1)) / den; } -template && std::is_integral_v>> +template NVCV_CUDA_HOST_DEVICE constexpr auto RoundUpPowerOfTwo(T value, U multiple) { + static_assert(std::is_integral_v && std::is_integral_v, "RoundUpPowerOfTwo requires integral arguments"); + assert(value >= 0); assert(multiple >= 0); @@ -90,9 +100,11 @@ NVCV_CUDA_HOST_DEVICE constexpr auto RoundUpPowerOfTwo(T value, U multiple) return (value + (multiple - 1)) & -multiple; } -template>> +template constexpr int ILog2(T value) { + static_assert(std::is_integral_v, "ILog2 requires an integral argument"); + assert(value > 0); if constexpr (sizeof(T) <= sizeof(unsigned)) @@ -114,22 +126,25 @@ constexpr int ILog2(T value) } } -template>> +template NVCV_CUDA_HOST_DEVICE constexpr auto DivUpPowerOfTwo(T num, TypeIdentity den) { + static_assert(std::is_integral_v, "DivUpPowerOfTwo requires integral arguments"); + assert(num >= 0); assert(den > 0); assert(IsPowerOfTwo(den)); - return (num >> ILog2(den)) + !!(num & (den - 1)); + return (num >> ILog2(den)) + ((num & (den - 1)) != 0 ? 1 : 0); } /// @brief Calculates normalized sinc i.e. `sin(pi * x) / (pi * x)` -template>> +template NVCV_CUDA_HOST_DEVICE NVCV_FORCE_INLINE T sinc(T x) { + static_assert(std::is_floating_point_v, "sinc requires a floating-point argument"); static_assert(sizeof(T) >= sizeof(float)); // not analyzed for smaller floats, eps may require adjustment - constexpr T eps = sizeof(T) <= sizeof(float) ? 1e-5 : 1e-8; + constexpr T eps = sizeof(T) <= sizeof(float) ? static_cast(1e-5) : static_cast(1e-8); x *= static_cast(M_PI); if (std::abs(x) < eps) return static_cast(1.0) - x * x * (static_cast(1.0) / 6); // remove singularity by using Taylor expansion diff --git a/src/nvcv/util/Ranges.hpp b/src/nvcv/util/Ranges.hpp index f094181be..3f50871c7 100644 --- a/src/nvcv/util/Ranges.hpp +++ b/src/nvcv/util/Ranges.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -69,21 +69,21 @@ auto End(R (&r)[N]) } template -auto Begin(R &&r) +auto Begin(R &r) { using std::begin; return begin(r); } template -auto End(R &&r) +auto End(R &r) { using std::end; return end(r); } template -auto Data(R &&r) +auto Data(R &r) { return &*Begin(r); } @@ -96,7 +96,7 @@ auto Size(const R &r) } template -using RangeValue = std::remove_reference_t()))>; +using RangeValue = std::remove_reference_t()))>; namespace detail { template @@ -104,8 +104,8 @@ constexpr bool IsRandomAccessRange() { if constexpr (IsRange) { - return std::is_same_v()))>>::iterator_category, + return std::is_same_v()))>>::iterator_category, // NOSONAR: range trait, not an array. std::random_access_iterator_tag>; } else diff --git a/src/nvcv/util/Size.hpp b/src/nvcv/util/Size.hpp index ee88af225..be4d66a93 100644 --- a/src/nvcv/util/Size.hpp +++ b/src/nvcv/util/Size.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,10 +22,11 @@ namespace nvcv::util { struct Size2D { - int w, h; + int w; + int h; }; -inline bool operator==(const Size2D &a, const Size2D &b) +inline bool operator==(const Size2D &a, const Size2D &b) // NOSONAR: defaulted comparisons are C++20. { return a.w == b.w && a.h == b.h; } diff --git a/src/nvcv/util/StaticVector.hpp b/src/nvcv/util/StaticVector.hpp index 57c046eb6..fb0547d89 100644 --- a/src/nvcv/util/StaticVector.hpp +++ b/src/nvcv/util/StaticVector.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -28,9 +28,16 @@ #include // for std::bad_alloc #include #include +#include namespace nvcv::util { +class StaticVectorError : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + // We want StaticVector to have the same characteristics of // T, i.e., if T isn't copiable, so isn't StaticVector. In order // to accomplish it, we have this StaticVectorHelper with a base @@ -38,9 +45,9 @@ namespace nvcv::util { // where T is copy-constructible and/or copy-assignable. // Base case, class is move constructible and assignable -template::value, - bool IS_COPY_ASSIGNABLE = std::is_copy_assignable::value> -class StaticVectorHelper +template, + bool IS_COPY_ASSIGNABLE = std::is_copy_assignable_v> +class StaticVectorHelper // NOSONAR: small-vector helper keeps STL-like operations together. { static_assert(N >= 0, "StaticVector capacity can't be negative!"); @@ -58,14 +65,15 @@ class StaticVectorHelper explicit StaticVectorHelper(size_t count) { - if (count > N) + if (count > static_cast(N)) { throw std::bad_alloc(); } - m_size = count; + m_size = static_cast(count); - std::uninitialized_default_construct(this->begin(), this->end()); + std::uninitialized_default_construct( // NOSONAR: std::ranges overload is C++20. + this->begin(), this->end()); doCheckInvariants(); } @@ -76,15 +84,15 @@ class StaticVectorHelper NVCV_ASSERT(beg != nullptr); NVCV_ASSERT(end != nullptr); - int count = std::distance(beg, end); + auto count = std::distance(beg, end); NVCV_ASSERT(count >= 0); - if (count > N) + if (count > static_cast(N)) { throw std::bad_alloc(); } - m_size = count; + m_size = static_cast(count); std::uninitialized_copy(beg, end, this->begin()); @@ -93,35 +101,35 @@ class StaticVectorHelper explicit StaticVectorHelper(size_t count, const T &value) { - if (count > N) + if (count > static_cast(N)) { throw std::bad_alloc(); } - m_size = count; + m_size = static_cast(count); - std::uninitialized_fill(this->begin(), this->end(), value); + std::uninitialized_fill(this->begin(), this->end(), value); // NOSONAR: std::ranges overload is C++20. doCheckInvariants(); } ~StaticVectorHelper() { - std::destroy(this->begin(), this->end()); + std::destroy(this->begin(), this->end()); // NOSONAR: std::ranges overload is C++20. } - StaticVectorHelper(StaticVectorHelper &&that) noexcept(std::is_nothrow_move_constructible::value) + StaticVectorHelper(StaticVectorHelper &&that) noexcept : StaticVectorHelper() { if constexpr (std::is_trivially_copyable_v) { - std::copy(that.begin(), that.end(), this->begin()); + std::copy(that.begin(), that.end(), this->begin()); // NOSONAR: std::ranges overload is C++20. } else { - std::uninitialized_move(that.begin(), that.end(), this->end()); + std::uninitialized_move(that.begin(), that.end(), this->end()); // NOSONAR: std::ranges overload is C++20. } - m_size = that.size(); + m_size = static_cast(that.size()); // not setting that's size to 0 on purpose // Since we didn't allocate memory for the vector, we're effectively @@ -130,14 +138,13 @@ class StaticVectorHelper doCheckInvariants(); } - StaticVectorHelper &operator=(StaticVectorHelper &&that) noexcept(std::is_nothrow_move_assignable::value) + StaticVectorHelper &operator=(StaticVectorHelper &&that) noexcept { if (this != &that) { - using std::swap; if constexpr (std::is_trivially_copyable_v) { - std::copy(that.begin(), that.end(), this->begin()); + std::copy(that.begin(), that.end(), this->begin()); // NOSONAR: std::ranges overload is C++20. } else if (this->size() <= that.size()) { @@ -146,11 +153,11 @@ class StaticVectorHelper } else { - std::move(that.begin(), that.end(), this->begin()); - std::destroy(this->begin() + that.size(), this->end()); + std::move(that.begin(), that.end(), this->begin()); // NOSONAR: std::ranges overload is C++20. + std::destroy(this->begin() + that.size(), this->end()); // NOSONAR: std::ranges overload is C++20. } - m_size = that.size(); + m_size = static_cast(that.size()); // not setting that's size to 0 on purpose // Since we didn't allocate memory for the vector, we're effectively @@ -171,16 +178,16 @@ class StaticVectorHelper // According to the Holy Standard as of C++17, we // can't move an item out of an std::initializer_list /facepalm - std::uninitialized_copy(list.begin(), list.end(), this->begin()); + std::uninitialized_copy(list.begin(), list.end(), this->begin()); // NOSONAR: std::ranges overload is C++20. - m_size = list.size(); + m_size = static_cast(list.size()); doCheckInvariants(); } void resize(size_t newSize) { - if (newSize > N) + if (newSize > static_cast(N)) { throw std::bad_alloc(); } @@ -199,11 +206,11 @@ class StaticVectorHelper } else { - throw std::runtime_error("Can't create non-default-constructible type"); - }; + throw StaticVectorError("Can't create non-default-constructible type"); + } } - m_size = newSize; + m_size = static_cast(newSize); doCheckInvariants(); } @@ -262,13 +269,13 @@ class StaticVectorHelper void clear() { - std::destroy(this->begin(), this->end()); + std::destroy(this->begin(), this->end()); // NOSONAR: std::ranges overload is C++20. m_size = 0; doCheckInvariants(); } - friend void swap(StaticVectorHelper &a, StaticVectorHelper &b) + friend void swap(StaticVectorHelper &a, StaticVectorHelper &b) noexcept { using std::swap; if constexpr (std::is_trivially_copyable_v) @@ -313,11 +320,12 @@ class StaticVectorHelper NVCV_ASSERT(end <= this->end()); NVCV_ASSERT(beg <= end); - int rangeLength = std::distance(beg, end); + auto rangeLength = std::distance(beg, end); + NVCV_ASSERT(rangeLength >= 0); std::swap_ranges(this->begin() + std::distance(this->cbegin(), end), this->end(), this->begin() + std::distance(this->cbegin(), beg)); - this->resize(this->size() - rangeLength); + this->resize(this->size() - static_cast(rangeLength)); // must return the iterator following the last removed element. If the // iterator pos refers to the last element, the end() iterator is @@ -452,7 +460,7 @@ class StaticVectorHelper } private: - void doCheckInvariants() + void doCheckInvariants() const { NVCV_ASSERT(m_size <= N); } @@ -460,7 +468,7 @@ class StaticVectorHelper int m_size; // our memory buffer - std::aligned_storage_t m_arena[N]; + std::aligned_storage_t m_arena[N]; // NOSONAR: raw storage for placement-new elements. // our partial specializations will have initialize m_size template @@ -479,13 +487,21 @@ class StaticVectorHelper : public StaticVectorHelperbegin()); + std::uninitialized_copy(that.begin(), that.end(), this->begin()); // NOSONAR: std::ranges overload is C++20. } - StaticVectorHelper(StaticVectorHelper &&that) = default; + StaticVectorHelper(StaticVectorHelper &&that) noexcept + : Base(std::move(that)) + { + } StaticVectorHelper &operator=(const StaticVectorHelper &that) = default; - StaticVectorHelper &operator=(StaticVectorHelper &&that) = default; + + StaticVectorHelper &operator=(StaticVectorHelper &&that) noexcept + { + Base::operator=(std::move(that)); + return *this; + } }; // Partial specialization, class is NOT copy constructible but IS copy assignable @@ -497,7 +513,16 @@ class StaticVectorHelper : public StaticVectorHelper::value) + ~StaticVectorHelper() + { + this->clear(); + } + + StaticVectorHelper &operator=(const StaticVectorHelper &that) = delete; + +protected: + void copyAssignFrom(const StaticVectorHelper &that) noexcept( + std::is_nothrow_copy_assignable_v &&std::is_nothrow_copy_constructible_v) { if (this != &that) { @@ -515,13 +540,21 @@ class StaticVectorHelper : public StaticVectorHelper : public StaticVectorHelper::value) + ~StaticVectorHelper() { - Base::m_size = that.size(); - std::uninitialized_copy(that.begin(), that.end(), this->begin()); + this->clear(); } - StaticVectorHelper(StaticVectorHelper &&that) = default; + StaticVectorHelper(const StaticVectorHelper &that) noexcept(std::is_nothrow_copy_constructible_v) + { + Base::m_size = static_cast(that.size()); + std::uninitialized_copy(that.begin(), that.end(), this->begin()); // NOSONAR: std::ranges overload is C++20. + } - StaticVectorHelper &operator=(const StaticVectorHelper &that) = default; - StaticVectorHelper &operator=(StaticVectorHelper &&that) = default; + StaticVectorHelper(StaticVectorHelper &&that) noexcept + : Base(std::move(that)) + { + } + + StaticVectorHelper &operator=(const StaticVectorHelper &that) noexcept( + std::is_nothrow_copy_assignable_v &&std::is_nothrow_copy_constructible_v) + { + this->copyAssignFrom(that); + return *this; + } + + StaticVectorHelper &operator=(StaticVectorHelper &&that) noexcept + { + Base::operator=(std::move(that)); + return *this; + } private: using Base::m_size; diff --git a/src/nvcv/util/String.cpp b/src/nvcv/util/String.cpp index 2eb5b6aff..8fa7a2004 100644 --- a/src/nvcv/util/String.cpp +++ b/src/nvcv/util/String.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,74 +19,106 @@ #include "Assert.h" +#include #include +#include namespace nvcv::util { -void ReplaceAllInline(char *strBuffer, int bufferSize, const char *what, const char *replace) noexcept +void ReplaceAllInline(char *strBuffer, int bufferSize, std::string_view what, std::string_view replace) noexcept { - if (strBuffer == nullptr || what == nullptr || replace == nullptr || bufferSize <= 0) + if (strBuffer == nullptr || what.empty() || bufferSize <= 0) { return; } - size_t whatSize = std::strlen(what); - size_t replaceSize = std::strlen(replace); - size_t strSize = std::strlen(strBuffer); - - char *searchStart = strBuffer; - char *writePos = nullptr; - char *endOfNewString = nullptr; - char *endPos = strBuffer + bufferSize - 1; //to make sure we do not overflow. + auto *bufferEnd = strBuffer + bufferSize; + auto *nulPos = std::find(strBuffer, bufferEnd, '\0'); + if (nulPos == bufferEnd) + { + nulPos = bufferEnd - 1; + *nulPos = '\0'; + } - while (searchStart < strBuffer + strSize) + char *searchStart = strBuffer; + while (searchStart < nulPos) { - char *foundPos = std::strstr(searchStart, what); - if (foundPos == nullptr) + auto *foundPos = std::search(searchStart, nulPos, what.begin(), what.end()); + if (foundPos == nulPos) { - // No more occurrences of 'what' found return; } - searchStart += replaceSize; // update for next token - ptrdiff_t sizeOfRest = 0; - // Move string after token only if there is data after the token. - if (foundPos + (replaceSize - 1) < endPos) - { - char *restOfString = (foundPos + whatSize); // string after the what token. - ptrdiff_t moveAmount = static_cast( - replaceSize); // how far from beginning of token to move the rest of the string. - writePos = foundPos + moveAmount; // where to start writing the rest of the string. - sizeOfRest = std::strlen(restOfString); //size of rest of string. - - // Move string after token - // check for overflow we just want to write to buffer size - if (writePos + sizeOfRest > endPos) - { - sizeOfRest = endPos - writePos; - } - NVCV_ASSERT(writePos <= endPos); - NVCV_ASSERT(writePos + (sizeOfRest - 1) <= endPos); - std::memmove(writePos, restOfString, - sizeOfRest); // move the remainder of the string to allow for replacement of what. - } - // Replace token - // check for overflow - if (foundPos + replaceSize > endPos) + const char *tailStart = foundPos + what.size(); + auto tailSize = nulPos - tailStart; + auto replacementRoom = bufferEnd - foundPos - 1; + size_t replacementSize = std::min(replace.size(), static_cast(replacementRoom)); + char *tailWritePos = foundPos + replacementSize; + auto tailRoom = bufferEnd - tailWritePos - 1; + auto movedTailSize = std::min(tailSize, tailRoom); + + std::memmove(tailWritePos, tailStart, movedTailSize); + if (replacementSize > 0) { - replaceSize = endPos - foundPos; + std::memcpy(foundPos, replace.data(), replacementSize); } - NVCV_ASSERT(foundPos <= endPos); - NVCV_ASSERT(foundPos + (replaceSize - 1) <= endPos); - std::memmove(foundPos, replace, replaceSize); // replace the found token with the replacement string. - endOfNewString = std::max(foundPos + replaceSize, - writePos + sizeOfRest); // update the end position to the new end of the string. - *endOfNewString = '\0'; // Null-terminate the output in case token is last. + nulPos = tailWritePos + movedTailSize; + *nulPos = '\0'; + searchStart = tailWritePos; + } +} + +FixedBufferStreamBuf::FixedBufferStreamBuf(char *buffer, std::streamsize bufferSize) +{ + reset(buffer, bufferSize); +} + +void FixedBufferStreamBuf::reset(char *buffer, std::streamsize bufferSize) noexcept +{ + m_buffer = buffer; + m_bufferSize = bufferSize; + + if (m_buffer != nullptr && m_bufferSize > 0) + { + setp(m_buffer, m_buffer + m_bufferSize); + *m_buffer = '\0'; + } +} + +std::streampos FixedBufferStreamBuf::seekpos(std::streampos pos, std::ios_base::openmode which) noexcept +{ + auto offset = static_cast(pos); + if ((which & std::ios_base::out) == 0 || m_buffer == nullptr || offset < 0 || offset >= m_bufferSize) + { + return std::streampos{std::streamoff{-1}}; + } + + setp(m_buffer, m_buffer + m_bufferSize); + pbump(static_cast(offset)); + return std::streampos{offset}; +} + +FixedBufferStreamBuf::int_type FixedBufferStreamBuf::overflow(int_type ch) noexcept +{ + if (traits_type::eq_int_type(ch, traits_type::eof())) + { + return traits_type::not_eof(ch); + } + + return traits_type::eof(); +} + +int FixedBufferStreamBuf::sync() noexcept +{ + if (m_buffer != nullptr && m_bufferSize > 0 && pptr() < epptr()) + { + *pptr() = '\0'; } + return 0; } BufferOStream::BufferOStream(char *buffer, int len) - : m_buf(buffer, len, buffer) + : m_buf(buffer, len) { this->init(&m_buf); } diff --git a/src/nvcv/util/String.hpp b/src/nvcv/util/String.hpp index 24bbbb1d4..e6f03518b 100644 --- a/src/nvcv/util/String.hpp +++ b/src/nvcv/util/String.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,25 +15,51 @@ * limitations under the License. */ -#include +#ifndef NVCV_UTIL_STRING_HPP +#define NVCV_UTIL_STRING_HPP -#ifdef __GNUC__ -# undef __DEPRECATED -#endif -#include +#include +#include +#include namespace nvcv::util { -void ReplaceAllInline(char *strBuffer, int bufferSize, const char *what, const char *replace) noexcept; +void ReplaceAllInline(char *strBuffer, int bufferSize, std::string_view what, std::string_view replace) noexcept; + +class FixedBufferStreamBuf : public std::streambuf +{ +public: + FixedBufferStreamBuf() = default; + FixedBufferStreamBuf(char *buffer, std::streamsize bufferSize); + + FixedBufferStreamBuf(const FixedBufferStreamBuf &) = delete; + FixedBufferStreamBuf &operator=(const FixedBufferStreamBuf &) = delete; + FixedBufferStreamBuf(FixedBufferStreamBuf &&) = delete; + FixedBufferStreamBuf &operator=(FixedBufferStreamBuf &&) = delete; + + void reset(char *buffer, std::streamsize bufferSize) noexcept; + + std::streampos seekpos(std::streampos pos, std::ios_base::openmode which = std::ios_base::out) noexcept override; + +protected: + int_type overflow(int_type ch) noexcept override; + int sync() noexcept override; + +private: + char *m_buffer = nullptr; + std::streamsize m_bufferSize = 0; +}; class BufferOStream : public std::ostream { public: BufferOStream(char *buffer, int len); - ~BufferOStream(); + ~BufferOStream() override; private: - std::strstreambuf m_buf; + FixedBufferStreamBuf m_buf; }; } // namespace nvcv::util + +#endif // NVCV_UTIL_STRING_HPP diff --git a/src/nvcv/util/Version.hpp b/src/nvcv/util/Version.hpp index 35f7484fe..f044b11d4 100644 --- a/src/nvcv/util/Version.hpp +++ b/src/nvcv/util/Version.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * NVIDIA CORPORATION, its affiliates and licensors retain all intellectual @@ -94,7 +94,7 @@ class Version return m_code >= that.m_code; } - constexpr bool operator==(const Version &that) const + constexpr bool operator==(const Version &that) const // NOSONAR: defaulted comparisons are C++20. { return m_code == that.m_code; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ea0e91dad..40d36de1b 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -28,6 +28,11 @@ set(CMAKE_CXX_STANDARD 20) enable_testing() +if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 11.0) + message(WARNING "GCC-${CMAKE_CXX_COMPILER_VERSION}: System libgtest may have ABI incompatibilities. Build with GCC-11+ for full test coverage.") +endif() +find_package(GTest REQUIRED) + add_library(nvcv_test_main Main.cpp) target_link_libraries(nvcv_test_main PUBLIC @@ -112,6 +117,8 @@ endif() # Create build tree +add_subdirectory(cmake) + if(BUILD_TESTS_CPP OR BUILD_TESTS_PYTHON) add_subdirectory(common) add_subdirectory(nvcv_types) diff --git a/tests/Main.cpp b/tests/Main.cpp index baa56bf2d..5814e5970 100644 --- a/tests/Main.cpp +++ b/tests/Main.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * NVIDIA CORPORATION, its affiliates and licensors retain all intellectual @@ -26,7 +26,7 @@ namespace { class EventListener : public t::EmptyTestEventListener { public: - virtual void OnTestStart(const t::TestInfo &tinfo) override + void OnTestStart(const t::TestInfo &tinfo) override { // Swallow any existing error so that test isn't affected by it. // Actual error must have been already trapped in the previous test. @@ -39,7 +39,7 @@ class EventListener : public t::EmptyTestEventListener g_HasSanitizerError = false; } - virtual void OnTestEnd(const t::TestInfo &tinfo) override + void OnTestEnd(const t::TestInfo &tinfo) override { int devCount = 0; cudaGetDeviceCount(&devCount); diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 000000000..f5879b59d --- /dev/null +++ b/tests/README.md @@ -0,0 +1,58 @@ + + +# CV-CUDA Tests + +C++ (googletest) and Python (pytest) test suites for CV-CUDA operators and APIs. + +## Quick Start + +### 1. Build with tests enabled + +```bash +bash build.sh -DBUILD_TESTS=1 +``` + +### 2. Install Python dependencies + +Inside Docker devel images, all dependencies are pre-installed and this step is a no-op. + +```bash +# NumPy 2 + CuPy/cuda-python for CUDA 13 +tests/install_test_dependencies.sh numpy2 cu13 + +# NumPy 2 + CuPy/cuda-python for CUDA 12 +tests/install_test_dependencies.sh numpy2 cu12 + +# NumPy 1 + compatible CuPy for CUDA 12 (Python 3.10-3.12 only) +tests/install_test_dependencies.sh numpy1 cu12 +``` + +### 3. Run + +```bash +build-rel/bin/run_tests.sh +``` + +`run_tests.sh` is generated by CMake from `tests/run_tests.sh.in` during the build step — it will not exist until after a successful build. + +## Directory Structure + +``` +tests/ +├── cvcuda/ +│ ├── python/ # Python pytest tests (test_op*.py) +│ └── ... # C++ googletest sources +├── nvcv_types/ # nvcv_types unit tests +├── install_test_dependencies.sh +├── requirements.tests.common.txt # pytest, typing-extensions (all Python versions) +├── requirements.tests.numpy1.txt # NumPy 1.x (Python 3.10-3.12 only) +├── requirements.tests.numpy2.txt # NumPy 2.x (Python 3.10-3.14) +├── requirements.tests.cu12.txt # CuPy 14 and cuda-python for CUDA 12.x/NumPy 2 +├── requirements.tests.cu12.numpy1.txt # CuPy 13.6 and cuda-python for CUDA 12.x/NumPy 1 +└── requirements.tests.cu13.txt # CuPy and cuda-python for CUDA 13.x +``` + +All `requirements.tests.*.txt` files are **auto-generated** from their `.template` counterparts via `generate_requirements.sh` — do not edit them directly. diff --git a/tests/cmake/CMakeLists.txt b/tests/cmake/CMakeLists.txt new file mode 100644 index 000000000..f16bf193a --- /dev/null +++ b/tests/cmake/CMakeLists.txt @@ -0,0 +1,30 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +set(CVCUDA_CUDA_ARCHITECTURE_POLICY_TEST cvcuda_test_cuda_architecture_policy) +configure_file( + test_cuda_architecture_policy.py + "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${CVCUDA_CUDA_ARCHITECTURE_POLICY_TEST}" + COPYONLY +) + +set(CVCUDA_CUDA_ARCHITECTURE_POLICY_TEST_DATA_DIR + "${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_DATADIR}/cvcuda/tests/cmake" +) +file(MAKE_DIRECTORY "${CVCUDA_CUDA_ARCHITECTURE_POLICY_TEST_DATA_DIR}") +configure_file( + "${CMAKE_SOURCE_DIR}/cmake/CUDAArchitecturePolicy.cmake" + "${CVCUDA_CUDA_ARCHITECTURE_POLICY_TEST_DATA_DIR}/CUDAArchitecturePolicy.cmake" + COPYONLY +) + +install( + FILES "${CMAKE_SOURCE_DIR}/cmake/CUDAArchitecturePolicy.cmake" + DESTINATION "${CMAKE_INSTALL_DATADIR}/cvcuda/tests/cmake" + COMPONENT tests +) + +nvcv_add_test( + "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${CVCUDA_CUDA_ARCHITECTURE_POLICY_TEST}" + cmake +) diff --git a/tests/cmake/test_cuda_architecture_policy.py b/tests/cmake/test_cuda_architecture_policy.py new file mode 100755 index 000000000..07ca46d69 --- /dev/null +++ b/tests/cmake/test_cuda_architecture_policy.py @@ -0,0 +1,420 @@ +#!/usr/bin/env python3 + +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +import os +from pathlib import Path +import subprocess +import tempfile +import textwrap +import unittest + + +SCRIPT_PATH = Path(__file__).resolve() +POLICY_MODULE_CANDIDATES = ( + SCRIPT_PATH.parents[2] / "cmake" / "CUDAArchitecturePolicy.cmake", + SCRIPT_PATH.parents[1] + / "share" + / "cvcuda" + / "tests" + / "cmake" + / "CUDAArchitecturePolicy.cmake", +) +POLICY_MODULE = next( + (path for path in POLICY_MODULE_CANDIDATES if path.is_file()), + POLICY_MODULE_CANDIDATES[0], +) + + +class CUDAArchitecturePolicyTests(unittest.TestCase): + def setUp(self): + self.temp_dir = tempfile.TemporaryDirectory() + self.source_dir = Path(self.temp_dir.name) / "source" + self.build_dir = Path(self.temp_dir.name) / "build" + self.source_dir.mkdir() + (self.source_dir / "CMakeLists.txt").write_text( + textwrap.dedent( + f"""\ + cmake_minimum_required(VERSION 3.20.1) + project(cuda_architecture_policy NONE) + include("{POLICY_MODULE}") + cvcuda_detect_cuda_architecture_source() + + set(CMAKE_CUDA_COMPILER_VERSION "${{TEST_CUDA_VERSION}}") + if(TEST_PROCESSOR STREQUAL "aarch64") + set(ARCH_X86_64 OFF) + set(ARCH_AARCH64 ON) + else() + set(ARCH_X86_64 ON) + set(ARCH_AARCH64 OFF) + endif() + cvcuda_configure_cuda_architecture_policy() + + file(WRITE "${{CMAKE_BINARY_DIR}}/policy-result.txt" + "architectures=${{CMAKE_CUDA_ARCHITECTURES}}\n" + "generated=${{CVCUDA_GENERATED_CUDA_ARCHITECTURES}}\n" + "mode=${{CVCUDA_TARGETED_SM8X_CUBINS}}\n" + "active=${{CVCUDA_TARGETED_SM8X_CUBINS_ACTIVE}}\n" + "source=${{_CVCUDA_CUDA_ARCHITECTURES_SOURCE}}\n") + """ + ), + encoding="utf-8", + ) + + def tearDown(self): + self.temp_dir.cleanup() + + def configure(self, *arguments, cudaarchs=None, expect_success=True): + environment = os.environ.copy() + if cudaarchs is None: + environment.pop("CUDAARCHS", None) + else: + environment["CUDAARCHS"] = cudaarchs + command = [ + "cmake", + "-S", + str(self.source_dir), + "-B", + str(self.build_dir), + "-DTEST_CUDA_VERSION=12.5", + *arguments, + ] + result = subprocess.run( + command, + check=False, + capture_output=True, + env=environment, + text=True, + timeout=60, + ) + if expect_success: + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + return dict( + line.split("=", maxsplit=1) + for line in (self.build_dir / "policy-result.txt") + .read_text(encoding="utf-8") + .splitlines() + ) + self.assertNotEqual(result.returncode, 0, result.stdout + result.stderr) + return result + + def test_compact_x86_defaults_follow_toolkit(self): + result = self.configure() + self.assertEqual(result["architectures"], "80-real;90-real;75-real") + self.assertEqual(result["generated"], result["architectures"]) + self.assertEqual(result["active"], "ON") + + result = self.configure("-DTEST_CUDA_VERSION=13.2") + self.assertEqual( + result["architectures"], + "80-real;90-real;100-real;120-real;75-real", + ) + self.assertEqual(result["source"], "GENERATED") + self.assertEqual(result["active"], "ON") + + def test_x86_defaults_use_native_turing_without_ptx(self): + architectures = self.configure()["architectures"].split(";") + self.assertIn("75-real", architectures) + self.assertNotIn("75-virtual", architectures) + + def test_aarch64_defaults_follow_platform_and_auto_is_inactive(self): + result = self.configure("-DTEST_PROCESSOR=aarch64") + self.assertEqual( + result["architectures"], "80-real;86-real;89-real;90-real;75-real" + ) + self.assertEqual(result["active"], "OFF") + + result = self.configure( + "-DTEST_CUDA_VERSION=13.2", + "-DTEST_PROCESSOR=aarch64", + "-DCVCUDA_AARCH64_JETSON=ON", + ) + self.assertEqual(result["architectures"], "86-real;87-real;89-real") + self.assertEqual(result["active"], "OFF") + + def test_explicit_cache_values_are_exact(self): + for architectures in ( + "86-real;89-real", + "OFF", + "FALSE", + "0", + "native", + "all", + "all-major", + ): + with self.subTest(architectures=architectures): + with tempfile.TemporaryDirectory() as build_dir: + self.build_dir = Path(build_dir) + result = self.configure( + f"-DCMAKE_CUDA_ARCHITECTURES={architectures}" + ) + self.assertEqual(result["architectures"], architectures) + self.assertEqual(result["generated"], "") + self.assertEqual(result["source"], "EXPLICIT_CACHE") + self.assertEqual(result["active"], "OFF") + + def test_explicit_canonical_list_activates_auto_deterministically(self): + for cuda_version, canonical_architectures in ( + ("12.5", "80-real;90-real;75-real"), + ("13.2", "80-real;90-real;100-real;120-real;75-real"), + ): + with self.subTest(cuda_version=cuda_version): + version_argument = f"-DTEST_CUDA_VERSION={cuda_version}" + architecture_argument = ( + f"-DCMAKE_CUDA_ARCHITECTURES={canonical_architectures}" + ) + + with tempfile.TemporaryDirectory() as build_dir: + self.build_dir = Path(build_dir) + fresh_result = self.configure( + version_argument, architecture_argument + ) + + with tempfile.TemporaryDirectory() as build_dir: + self.build_dir = Path(build_dir) + self.configure(version_argument) + reconfigured_result = self.configure( + version_argument, architecture_argument + ) + + self.assertEqual(fresh_result["architectures"], canonical_architectures) + self.assertEqual( + reconfigured_result["architectures"], canonical_architectures + ) + self.assertEqual(fresh_result["mode"], "AUTO") + self.assertEqual(reconfigured_result["mode"], "AUTO") + self.assertEqual(fresh_result["active"], "ON") + self.assertEqual(reconfigured_result["active"], "ON") + + self.build_dir = Path(self.temp_dir.name) / "off-build" + result = self.configure("-DCVCUDA_TARGETED_SM8X_CUBINS=OFF") + self.assertEqual(result["active"], "OFF") + + def test_auto_compares_effective_architecture_lists(self): + result = self.configure( + "-DCMAKE_CUDA_ARCHITECTURES=75-real;90-real;80-real;80-real" + ) + self.assertEqual(result["architectures"], "75-real;90-real;80-real;80-real") + self.assertEqual(result["active"], "ON") + + def test_environment_is_exact_only_for_a_fresh_cache(self): + result = self.configure(cudaarchs="86-real;89-real") + self.assertEqual(result["architectures"], "86-real;89-real") + self.assertEqual(result["source"], "ENVIRONMENT") + self.assertEqual(result["active"], "OFF") + + result = self.configure(cudaarchs="native") + self.assertEqual(result["architectures"], "86-real;89-real") + self.assertEqual(result["source"], "EXPLICIT_CACHE") + + def test_cache_false_value_wins_over_environment(self): + result = self.configure("-DCMAKE_CUDA_ARCHITECTURES=OFF", cudaarchs="native") + self.assertEqual(result["architectures"], "OFF") + self.assertEqual(result["source"], "EXPLICIT_CACHE") + + def test_toolchain_cache_value_is_exact(self): + toolchain_file = self.source_dir / "toolchain.cmake" + toolchain_file.write_text( + 'set(CMAKE_CUDA_ARCHITECTURES "89-real" CACHE STRING "" FORCE)\n', + encoding="utf-8", + ) + + result = self.configure(f"-DCMAKE_TOOLCHAIN_FILE={toolchain_file}") + self.assertEqual(result["architectures"], "89-real") + self.assertEqual(result["generated"], "") + self.assertEqual(result["source"], "EXPLICIT_CACHE") + self.assertEqual(result["active"], "OFF") + + def test_generated_cache_remains_generated_until_explicitly_replaced(self): + self.configure() + result = self.configure(cudaarchs="native") + self.assertEqual(result["architectures"], "80-real;90-real;75-real") + self.assertEqual(result["source"], "GENERATED") + self.assertEqual(result["active"], "ON") + + result = self.configure("-DCMAKE_CUDA_ARCHITECTURES=86-real") + self.assertEqual(result["architectures"], "86-real") + self.assertEqual(result["generated"], "") + self.assertEqual(result["source"], "EXPLICIT_CACHE") + self.assertEqual(result["active"], "OFF") + + result = self.configure() + self.assertEqual(result["architectures"], "86-real") + self.assertEqual(result["source"], "EXPLICIT_CACHE") + + def test_targeted_cubin_mode_is_validated_and_canonicalized(self): + result = self.configure("-DCVCUDA_TARGETED_SM8X_CUBINS=off") + self.assertEqual(result["mode"], "OFF") + self.assertEqual(result["active"], "OFF") + + with tempfile.TemporaryDirectory() as build_dir: + self.build_dir = Path(build_dir) + result = self.configure( + "-DCMAKE_CUDA_ARCHITECTURES=86-real", + "-DCVCUDA_TARGETED_SM8X_CUBINS=on", + ) + self.assertEqual(result["mode"], "ON") + self.assertEqual(result["active"], "ON") + + with tempfile.TemporaryDirectory() as build_dir: + self.build_dir = Path(build_dir) + result = self.configure( + "-DCVCUDA_TARGETED_SM8X_CUBINS=sometimes", expect_success=False + ) + self.assertIn("must be AUTO, ON, or OFF", result.stdout + result.stderr) + + def test_targeted_source_helper_is_active_gated_and_filter_safe(self): + helper_source_dir = Path(self.temp_dir.name) / "helper-source" + helper_source_dir.mkdir() + (helper_source_dir / "targeted.cu").write_text( + "__global__ void targeted() {}\n", encoding="utf-8" + ) + (helper_source_dir / "CMakeLists.txt").write_text( + textwrap.dedent( + f"""\ + cmake_minimum_required(VERSION 3.20.1) + project(targeted_source_helper NONE) + include("{POLICY_MODULE}") + + set(CVCUDA_TARGETED_SM8X_CUBINS_ACTIVE "${{TEST_ACTIVE}}") + set(CMAKE_CUDA_ARCHITECTURES "${{TEST_GLOBAL_ARCHITECTURES}}") + set(CMAKE_CUDA_ARCHITECTURES_ALL "${{TEST_GLOBAL_ARCHITECTURES_ALL}}") + set(CMAKE_CUDA_ARCHITECTURES_ALL_MAJOR "${{TEST_GLOBAL_ARCHITECTURES_ALL_MAJOR}}") + set(CMAKE_CUDA_ARCHITECTURES_NATIVE "${{TEST_GLOBAL_ARCHITECTURES_NATIVE}}") + set(SELECTED_SOURCES unselected.cu) + if(TEST_SOURCE_SELECTED) + list(APPEND SELECTED_SOURCES targeted.cu) + endif() + + cvcuda_add_targeted_cuda_architectures_to_sources( + ARCHITECTURES ${{TEST_ARCHITECTURES}} + SOURCES targeted.cu + SELECTED_SOURCES_VAR SELECTED_SOURCES) + # Repeated declarations must not duplicate nvcc flags. + cvcuda_add_targeted_cuda_architectures_to_sources( + ARCHITECTURES ${{TEST_ARCHITECTURES}} + SOURCES targeted.cu + SELECTED_SOURCES_VAR SELECTED_SOURCES) + + get_property(TARGETED_OPTIONS SOURCE targeted.cu PROPERTY COMPILE_OPTIONS) + list(LENGTH TARGETED_OPTIONS TARGETED_OPTION_COUNT) + string(JOIN "|" TARGETED_OPTIONS_JOINED ${{TARGETED_OPTIONS}}) + file(WRITE "${{CMAKE_BINARY_DIR}}/helper-result.txt" + "count=${{TARGETED_OPTION_COUNT}}\noptions=${{TARGETED_OPTIONS_JOINED}}\n") + """ + ), + encoding="utf-8", + ) + + def configure_helper( + active, + source_selected, + architectures="86-real;89", + global_architectures="", + global_architectures_all="", + global_architectures_all_major="", + global_architectures_native="", + expect_success=True, + ): + with tempfile.TemporaryDirectory() as build_dir: + result = subprocess.run( + [ + "cmake", + "-S", + str(helper_source_dir), + "-B", + build_dir, + f"-DTEST_ACTIVE={active}", + f"-DTEST_SOURCE_SELECTED={source_selected}", + f"-DTEST_ARCHITECTURES={architectures}", + f"-DTEST_GLOBAL_ARCHITECTURES={global_architectures}", + f"-DTEST_GLOBAL_ARCHITECTURES_ALL={global_architectures_all}", + f"-DTEST_GLOBAL_ARCHITECTURES_ALL_MAJOR={global_architectures_all_major}", + f"-DTEST_GLOBAL_ARCHITECTURES_NATIVE={global_architectures_native}", + ], + check=False, + capture_output=True, + text=True, + timeout=60, + ) + if not expect_success: + self.assertNotEqual( + result.returncode, 0, result.stdout + result.stderr + ) + return result + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + return dict( + line.split("=", maxsplit=1) + for line in (Path(build_dir) / "helper-result.txt") + .read_text(encoding="utf-8") + .splitlines() + ) + + result = configure_helper("ON", "ON") + self.assertEqual(result["count"], "2") + self.assertEqual( + result["options"], + "$<$:--generate-code=arch=compute_86,code=sm_86>" + "|$<$:--generate-code=arch=compute_89,code=sm_89>", + ) + self.assertNotIn("code=compute_", result["options"]) + + result = configure_helper("ON", "ON", global_architectures="86;89-real") + self.assertEqual(result, {"count": "0", "options": ""}) + + result = configure_helper("ON", "ON", global_architectures="86-real;89-virtual") + self.assertEqual(result["count"], "1") + self.assertEqual( + result["options"], + "$<$:--generate-code=arch=compute_89,code=sm_89>", + ) + + result = configure_helper( + "ON", "ON", global_architectures="86-virtual;89-virtual" + ) + self.assertEqual(result["count"], "2") + + result = configure_helper( + "ON", + "ON", + global_architectures="all", + global_architectures_all="80;86;89;90", + ) + self.assertEqual(result, {"count": "0", "options": ""}) + + result = configure_helper( + "ON", + "ON", + global_architectures="all-major", + global_architectures_all_major="80;90", + ) + self.assertEqual(result["count"], "2") + + result = configure_helper( + "ON", + "ON", + global_architectures="native", + global_architectures_native="86", + ) + self.assertEqual(result["count"], "1") + self.assertEqual( + result["options"], + "$<$:--generate-code=arch=compute_89,code=sm_89>", + ) + + result = configure_helper("ON", "OFF") + self.assertEqual(result, {"count": "0", "options": ""}) + + result = configure_helper("OFF", "ON") + self.assertEqual(result, {"count": "0", "options": ""}) + + result = configure_helper("ON", "ON", "89-virtual", expect_success=False) + self.assertIn( + "Targeted CUDA architecture must be 86, 86-real, 89, or 89-real", + result.stdout + result.stderr, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/common/CMakeLists.txt b/tests/common/CMakeLists.txt index d7912c3de..50a23cee4 100644 --- a/tests/common/CMakeLists.txt +++ b/tests/common/CMakeLists.txt @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,7 @@ # Utilities used in unit and system tests. They don't # call the public API functions. -set(OPENSSL_USE_STATIC_LIBS TRUE) +option(OPENSSL_USE_STATIC_LIBS "Link the test OpenSSL dependency statically" ON) find_package(OpenSSL COMPONENTS Crypto REQUIRED) # on some distros (e.g CentOS 7) we need to add libz when diff --git a/tests/common/CheckStatus.hpp b/tests/common/CheckStatus.hpp index 902da811f..0cb0cfbb4 100644 --- a/tests/common/CheckStatus.hpp +++ b/tests/common/CheckStatus.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -24,6 +24,7 @@ # include #endif +#include #include #if !defined(NVCV_UNIT_TESTS) || !NVCV_UNIT_TESTS @@ -38,12 +39,11 @@ inline ::testing::AssertionResult CmpHelperEQFailure(const char *lhs_expression, auto res = EqFailure(lhs_expression, rhs_expression, FormatForComparisonFailureMessage(lhs, rhs), FormatForComparisonFailureMessage(rhs, lhs), false); - char detail[NVCV_MAX_STATUS_MESSAGE_LENGTH]; - NVCVStatus last = nvcvPeekAtLastErrorMessage(detail, sizeof(detail)); - - if (last != NVCV_SUCCESS && (last == lhs || last == rhs)) + std::array detail; + if (NVCVStatus last = nvcvPeekAtLastErrorMessage(detail.data(), detail.size()); + last != NVCV_SUCCESS && (last == lhs || last == rhs)) { - res << "\n Detail: " << detail; + res << "\n Detail: " << detail.data(); } return res; diff --git a/tests/common/HashMD5.cpp b/tests/common/HashMD5.cpp index 15b2963b0..13029d180 100644 --- a/tests/common/HashMD5.cpp +++ b/tests/common/HashMD5.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,51 +20,65 @@ #include #include +#include #include namespace nvcv::test { -struct HashMD5::Impl +struct EvpMdCtxDeleter { - EVP_MD_CTX *ctx; + void operator()(EVP_MD_CTX *ctx) const + { + EVP_MD_CTX_destroy(ctx); + } }; -HashMD5::HashMD5() - : pimpl{std::make_unique()} +struct HashMD5::Impl { - pimpl->ctx = EVP_MD_CTX_create(); - NVCV_ASSERT(pimpl->ctx != nullptr); + std::unique_ptr ctx{EVP_MD_CTX_create()}; +}; - int ret = EVP_DigestInit_ex(pimpl->ctx, EVP_md5(), NULL); - NVCV_ASSERT(ret == 1); +void HashMD5::ImplDeleter::operator()(Impl *impl) const +{ + std::default_delete{}(impl); } -HashMD5::~HashMD5() +HashMD5::ImplPtr HashMD5::CreateImpl() { - EVP_MD_CTX_destroy(pimpl->ctx); + ImplPtr impl{new Impl}; + NVCV_ASSERT(impl->ctx != nullptr); + + int ret = EVP_DigestInit_ex(impl->ctx.get(), EVP_md5(), nullptr); + NVCV_ASSERT(ret == 1); + + return impl; } -void HashMD5::operator()(const void *data, size_t lenBytes) +HashMD5::HashMD5() = default; + +HashMD5::~HashMD5() = default; + +void HashMD5::operator()(const std::byte *data, size_t lenBytes) const { - int ret = EVP_DigestUpdate(pimpl->ctx, data, lenBytes); + int ret = EVP_DigestUpdate(pimpl->ctx.get(), data, lenBytes); NVCV_ASSERT(ret == 1); } -std::array HashMD5::getHashAndReset() +std::array HashMD5::getHashAndReset() const { - unsigned char buf[EVP_MAX_MD_SIZE]; - unsigned int nwritten = sizeof(buf); + std::array buf; + unsigned int nwritten = buf.size(); // it also resets the context - int ret = EVP_DigestFinal(pimpl->ctx, buf, &nwritten); + int ret = EVP_DigestFinal(pimpl->ctx.get(), buf.data(), &nwritten); NVCV_ASSERT(ret == 1); // Be ready for a new run - ret = EVP_DigestInit_ex(pimpl->ctx, EVP_md5(), NULL); + ret = EVP_DigestInit_ex(pimpl->ctx.get(), EVP_md5(), nullptr); NVCV_ASSERT(ret == 1); NVCV_ASSERT(nwritten == 16); std::array hash; - memcpy(&hash[0], buf, sizeof(hash)); + memcpy(hash.data(), buf.data(), hash.size()); return hash; } diff --git a/tests/common/HashMD5.hpp b/tests/common/HashMD5.hpp index 3e6796e16..a77e13307 100644 --- a/tests/common/HashMD5.hpp +++ b/tests/common/HashMD5.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,9 +21,12 @@ #include #include +#include #include #include #include +#include +#include #include #include #include @@ -37,38 +40,49 @@ class HashMD5 HashMD5(const HashMD5 &) = delete; ~HashMD5(); - void operator()(const void *data, size_t lenBytes); - std::array getHashAndReset(); + void operator()(const std::byte *data, size_t lenBytes) const; + std::array getHashAndReset() const; template - void operator()(const T &value) + void operator()(const T &value) const { static_assert(std::has_unique_object_representations_v, "Can't hash this type"); - this->operator()(&value, sizeof(value)); + auto bytes = std::as_bytes(std::span{&value, size_t{1}}); + this->operator()(bytes.data(), bytes.size()); } private: struct Impl; - std::unique_ptr pimpl; + + struct ImplDeleter + { + void operator()(Impl *impl) const; + }; + + using ImplPtr = std::unique_ptr; + + static ImplPtr CreateImpl(); + + ImplPtr pimpl = CreateImpl(); }; template -std::enable_if_t && !util::ranges::IsRange> Update(HashMD5 &hash, - const T &value) +requires(std::has_unique_object_representations_v && !util::ranges::IsRange) void Update(HashMD5 &hash, + const T &value) { hash(value); } template -void Update(HashMD5 &hash, const T *value) +void Update(HashMD5 &, const T *) { static_assert(sizeof(T) == 0, "Won't do md5 of a pointer"); } void Update(HashMD5 &hash, const char *value); -template, int> = 0> -void Update(nvcv::test::HashMD5 &hash, const R &r) +template +requires util::ranges::IsRange void Update(nvcv::test::HashMD5 &hash, const R &r) { Update(hash, util::ranges::Size(r)); // With C++20 we should use std::ranges::contiguous_range instead @@ -76,7 +90,8 @@ void Update(nvcv::test::HashMD5 &hash, const R &r) R> && std::has_unique_object_representations_v>) { // It's faster to do this if range is contiguous and elements have unique object representation - hash(util::ranges::Data(r), util::ranges::Size(r) * sizeof(util::ranges::RangeValue)); + auto bytes = std::as_bytes(std::span{util::ranges::Data(r), static_cast(util::ranges::Size(r))}); + hash(bytes.data(), bytes.size()); } else { @@ -88,58 +103,43 @@ void Update(nvcv::test::HashMD5 &hash, const R &r) } } -template -void Update(HashMD5 &hash, const T1 &v1, const T2 &v2, const TT &...v) -{ - Update(hash, v1); - Update(hash, v2); - - (..., Update(hash, v)); -} - template -std::enable_if_t> Update(HashMD5 &hash, const T &value) +requires std::is_floating_point_v void Update(HashMD5 &hash, const T &value) { hash(std::hash()(value)); } -} // namespace nvcv::test - -namespace std { - template -void Update(nvcv::test::HashMD5 &hash, const tuple &t) +void Update(HashMD5 &hash, const std::tuple &t) { - if constexpr (has_unique_object_representations_v>) + if constexpr (std::has_unique_object_representations_v>) { return hash(t); } - auto th = forward_as_tuple(hash); - - apply(nvcv::test::Update, tuple_cat(th, t)); + std::apply([&hash](const auto &...v) { (..., Update(hash, v)); }, t); }; -inline void Update(nvcv::test::HashMD5 &hash, const string &s) +inline void Update(HashMD5 &hash, const std::string &s) { - return hash(s.data(), s.size()); + auto bytes = std::as_bytes(std::span{s.data(), s.size()}); + return hash(bytes.data(), bytes.size()); } -inline void Update(nvcv::test::HashMD5 &hash, const string_view &s) +inline void Update(HashMD5 &hash, const std::string_view &s) { - return hash(s.data(), s.size()); + auto bytes = std::as_bytes(std::span{s.data(), s.size()}); + return hash(bytes.data(), bytes.size()); } -inline void Update(nvcv::test::HashMD5 &hash, const std::type_info &t) +inline void Update(HashMD5 &hash, const std::type_info &t) { return hash(t.hash_code()); } template -void Update(nvcv::test::HashMD5 &hash, const optional &o) +void Update(HashMD5 &hash, const std::optional &o) { - using nvcv::test::Update; - // We can't rely on std::hash for optionals because they // require a valid hash specialization for T. Since our // types use HashValue overloads, we have to do this instead. @@ -149,10 +149,19 @@ void Update(nvcv::test::HashMD5 &hash, const optional &o) } else { - return Update(hash, std::hash>()(nullopt)); + return Update(hash, std::hash>()(std::nullopt)); } } -} // namespace std +template +void Update(HashMD5 &hash, const T1 &v1, const T2 &v2, const TT &...v) +{ + Update(hash, v1); + Update(hash, v2); + + (..., Update(hash, v)); +} + +} // namespace nvcv::test #endif // NVCV_TEST_COMMON_HASHMD5_HPP diff --git a/tests/common/InterpUtils.hpp b/tests/common/InterpUtils.hpp index 6da75443d..34544a050 100644 --- a/tests/common/InterpUtils.hpp +++ b/tests/common/InterpUtils.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -24,6 +24,7 @@ #include // for cuda::round, etc. #include // for cuda::SaturateCast, etc. +#include #include #define VEC_EXPECT_NEAR(vec1, vec2, delta) \ @@ -72,14 +73,15 @@ inline const T &ValueAt(const std::vector &vec, long3 strides, int2 siz } template -inline T &ValueAt(std::vector &vec, long4_16a strides, int4 coord) +inline T &ValueAt(std::vector &vec, const long4_16a &strides, int4 coord) { return *reinterpret_cast( &vec[coord.w * strides.x + coord.z * strides.y + coord.y * strides.z + coord.x * strides.w]); } template -inline const T &ValueAt(const std::vector &vec, long4_16a strides, int2 size, const T &borderValue, int4 coord) +inline const T &ValueAt(const std::vector &vec, const long4_16a &strides, int2 size, const T &borderValue, + int4 coord) { int2 inCoord{coord.y, coord.z}; @@ -119,139 +121,254 @@ inline void GetBicubicCoeffs(float delta, float &w0, float &w1, float &w2, float w3 = 1 - w0 - w1 - w2; } -template -inline ValueType GoldInterp(const std::vector &vec, const StridesType &strides, const int2 &size, - const ValueType &bValue, float2 scale, float2 coord, int z = 0, int k = 0) +template +struct GoldInterpContext { - constexpr int N = cuda::NumElements; + const std::vector &vec; + const StridesType &strides; + int2 size; + const ValueType &bValue; + int z; + int k; +}; + +struct AreaWindow +{ + float fsx1; + float fsx2; + float fsy1; + float fsy2; + int sx1; + int sx2; + int sy1; + int sy2; +}; + +inline float AreaDelta(int edge, float fractionalEdge) +{ + return static_cast(edge) - fractionalEdge; +} - if constexpr (I == NVCV_INTERP_NEAREST) - { - int2 c = cuda::round(coord + .5f); +inline bool AreaEdgeAfter(int edge, float fractionalEdge) +{ + return static_cast(edge) > fractionalEdge; +} - return ValueAt(vec, strides, size, bValue, GetCoord(c.x, c.y, z, k)); - } - else if constexpr (I == NVCV_INTERP_LINEAR) - { - int2 c1 = cuda::round(coord); - int2 c2 = c1 + 1; +inline bool AreaEdgeBefore(int edge, float fractionalEdge) +{ + return static_cast(edge) < fractionalEdge; +} - ValueType v1 = ValueAt(vec, strides, size, bValue, GetCoord(c1.x, c1.y, z, k)); - ValueType v2 = ValueAt(vec, strides, size, bValue, GetCoord(c2.x, c1.y, z, k)); - ValueType v3 = ValueAt(vec, strides, size, bValue, GetCoord(c1.x, c2.y, z, k)); - ValueType v4 = ValueAt(vec, strides, size, bValue, GetCoord(c2.x, c2.y, z, k)); +template +inline const ValueType &GoldValueAt(const GoldInterpContext &ctx, int x, int y) +{ + return ValueAt(ctx.vec, ctx.strides, ctx.size, ctx.bValue, GetCoord(x, y, ctx.z, ctx.k)); +} - auto out = cuda::SetAll>(0); +template +inline ValueType GoldInterpNearest(const GoldInterpContext &ctx, float2 coord) +{ + int2 c = cuda::round(coord + .5f); - out += v1 * (c2.x - coord.x) * (c2.y - coord.y); - out += v2 * (coord.x - c1.x) * (c2.y - coord.y); - out += v3 * (c2.x - coord.x) * (coord.y - c1.y); - out += v4 * (coord.x - c1.x) * (coord.y - c1.y); + return GoldValueAt(ctx, c.x, c.y); +} - return cuda::SaturateCast(out); - } - else if constexpr (I == NVCV_INTERP_CUBIC) - { - int ix = cuda::round(coord.x); - int iy = cuda::round(coord.y); +template +inline ValueType GoldInterpLinear(const GoldInterpContext &ctx, float2 coord) +{ + int2 c1 = cuda::round(coord); + int2 c2 = c1 + 1; + + ValueType v1 = GoldValueAt(ctx, c1.x, c1.y); + ValueType v2 = GoldValueAt(ctx, c2.x, c1.y); + ValueType v3 = GoldValueAt(ctx, c1.x, c2.y); + ValueType v4 = GoldValueAt(ctx, c2.x, c2.y); - using FT = cuda::ConvertBaseTypeTo; - auto sum = cuda::SetAll(0); + auto out = cuda::SetAll>(0); - float wx[4]; - test::GetBicubicCoeffs(coord.x - ix, wx[0], wx[1], wx[2], wx[3]); - float wy[4]; - test::GetBicubicCoeffs(coord.y - iy, wy[0], wy[1], wy[2], wy[3]); + out += v1 * (static_cast(c2.x) - coord.x) * (static_cast(c2.y) - coord.y); + out += v2 * (coord.x - static_cast(c1.x)) * (static_cast(c2.y) - coord.y); + out += v3 * (static_cast(c2.x) - coord.x) * (coord.y - static_cast(c1.y)); + out += v4 * (coord.x - static_cast(c1.x)) * (coord.y - static_cast(c1.y)); - for (int cy = -1; cy <= 2; cy++) + return cuda::SaturateCast(out); +} + +template +inline ValueType GoldInterpCubic(const GoldInterpContext &ctx, float2 coord) +{ + int ix = cuda::round(coord.x); + int iy = cuda::round(coord.y); + + using FT = cuda::ConvertBaseTypeTo; + auto sum = cuda::SetAll(0); + + std::array wx; + test::GetBicubicCoeffs(coord.x - static_cast(ix), wx[0], wx[1], wx[2], wx[3]); + std::array wy; + test::GetBicubicCoeffs(coord.y - static_cast(iy), wy[0], wy[1], wy[2], wy[3]); + + for (int cy = -1; cy <= 2; cy++) + { + for (int cx = -1; cx <= 2; cx++) { - for (int cx = -1; cx <= 2; cx++) - { - sum += (wx[cx + 1] * wy[cy + 1]) - * ValueAt(vec, strides, size, bValue, GetCoord(ix + cx, iy + cy, z, k)); - } + sum += (wx[cx + 1] * wy[cy + 1]) * GoldValueAt(ctx, ix + cx, iy + cy); } + } + + return cuda::SaturateCast(sum); +} + +inline AreaWindow GetAreaWindow(float2 scale, float2 coord) +{ + float fsx1 = coord.x * scale.x; + float fsx2 = fsx1 + scale.x; + float fsy1 = coord.y * scale.y; + float fsy2 = fsy1 + scale.y; + + return AreaWindow{fsx1, + fsx2, + fsy1, + fsy2, + cuda::round(fsx1), + cuda::round(fsx2), + cuda::round(fsy1), + cuda::round(fsy2)}; +} - return cuda::SaturateCast(sum); +template +inline void AddAreaBlock(AccumType &out, const GoldInterpContext &ctx, int yBegin, int yEnd, + int xBegin, int xEnd, float weight) +{ + for (int dy = yBegin; dy < yEnd; ++dy) + { + for (int dx = xBegin; dx < xEnd; ++dx) + { + out = out + GoldValueAt(ctx, dx, dy) * weight; + } } - else if constexpr (I == NVCV_INTERP_AREA) +} + +template +inline void AddAreaSideColumns(AccumType &out, const GoldInterpContext &ctx, + const AreaWindow &window, float invscale) +{ + for (int dy = window.sy1; dy < window.sy2; ++dy) { - float fsx1 = coord.x * scale.x; - float fsx2 = fsx1 + scale.x; - float fsy1 = coord.y * scale.y; - float fsy2 = fsy1 + scale.y; - int sx1 = cuda::round(fsx1); - int sx2 = cuda::round(fsx2); - int sy1 = cuda::round(fsy1); - int sy2 = cuda::round(fsy2); - - auto out = cuda::SetAll>(0); - - if (std::ceil(scale.x) == scale.x && std::ceil(scale.y) == scale.y) + if (AreaEdgeAfter(window.sx1, window.fsx1)) + { + out = out + GoldValueAt(ctx, window.sx1 - 1, dy) * (AreaDelta(window.sx1, window.fsx1) * invscale); + } + + if (AreaEdgeBefore(window.sx2, window.fsx2)) { - float invscale = 1.f / (scale.x * scale.y); + out = out + GoldValueAt(ctx, window.sx2, dy) * (-AreaDelta(window.sx2, window.fsx2) * invscale); + } + } +} - for (int dy = sy1; dy < sy2; ++dy) - for (int dx = sx1; dx < sx2; ++dx) - { - out = out + ValueAt(vec, strides, size, bValue, GetCoord(dx, dy, z, k)) * invscale; - } +template +inline void AddAreaSideRows(AccumType &out, const GoldInterpContext &ctx, + const AreaWindow &window, float invscale) +{ + if (AreaEdgeAfter(window.sy1, window.fsy1)) + { + for (int dx = window.sx1; dx < window.sx2; ++dx) + { + out = out + GoldValueAt(ctx, dx, window.sy1 - 1) * (AreaDelta(window.sy1, window.fsy1) * invscale); } - else + } + + if (AreaEdgeBefore(window.sy2, window.fsy2)) + { + for (int dx = window.sx1; dx < window.sx2; ++dx) { - float invscale = 1.f / (std::min(scale.x, size.x - fsx1) * std::min(scale.y, size.y - fsy1)); - - for (int dy = sy1; dy < sy2; ++dy) - { - for (int dx = sx1; dx < sx2; ++dx) - out = out + ValueAt(vec, strides, size, bValue, GetCoord(dx, dy, z, k)) * invscale; - - if (sx1 > fsx1) - out = out - + ValueAt(vec, strides, size, bValue, GetCoord(sx1 - 1, dy, z, k)) - * ((sx1 - fsx1) * invscale); - - if (sx2 < fsx2) - out = out - + ValueAt(vec, strides, size, bValue, GetCoord(sx2, dy, z, k)) - * ((fsx2 - sx2) * invscale); - } - - if (sy1 > fsy1) - for (int dx = sx1; dx < sx2; ++dx) - out = out - + ValueAt(vec, strides, size, bValue, GetCoord(dx, sy1 - 1, z, k)) - * ((sy1 - fsy1) * invscale); - - if (sy2 < fsy2) - for (int dx = sx1; dx < sx2; ++dx) - out = out - + ValueAt(vec, strides, size, bValue, GetCoord(dx, sy2, z, k)) - * ((fsy2 - sy2) * invscale); - - if ((sy1 > fsy1) && (sx1 > fsx1)) - out = out - + ValueAt(vec, strides, size, bValue, GetCoord(sx1 - 1, sy1 - 1, z, k)) - * ((sy1 - fsy1) * (sx1 - fsx1) * invscale); - - if ((sy1 > fsy1) && (sx2 < fsx2)) - out = out - + ValueAt(vec, strides, size, bValue, GetCoord(sx2, sy1 - 1, z, k)) - * ((sy1 - fsy1) * (fsx2 - sx2) * invscale); - - if ((sy2 < fsy2) && (sx2 < fsx2)) - out = out - + ValueAt(vec, strides, size, bValue, GetCoord(sx2, sy2, z, k)) - * ((fsy2 - sy2) * (fsx2 - sx2) * invscale); - - if ((sy2 < fsy2) && (sx1 > fsx1)) - out = out - + ValueAt(vec, strides, size, bValue, GetCoord(sx1 - 1, sy2, z, k)) - * ((fsy2 - sy2) * (sx1 - fsx1) * invscale); + out = out + GoldValueAt(ctx, dx, window.sy2) * (-AreaDelta(window.sy2, window.fsy2) * invscale); } + } +} + +template +inline void AddAreaCorners(AccumType &out, const GoldInterpContext &ctx, + const AreaWindow &window, float invscale) +{ + if (AreaEdgeAfter(window.sy1, window.fsy1) && AreaEdgeAfter(window.sx1, window.fsx1)) + { + out = out + + GoldValueAt(ctx, window.sx1 - 1, window.sy1 - 1) + * (AreaDelta(window.sy1, window.fsy1) * AreaDelta(window.sx1, window.fsx1) * invscale); + } + + if (AreaEdgeAfter(window.sy1, window.fsy1) && AreaEdgeBefore(window.sx2, window.fsx2)) + { + out = out + + GoldValueAt(ctx, window.sx2, window.sy1 - 1) + * (AreaDelta(window.sy1, window.fsy1) * -AreaDelta(window.sx2, window.fsx2) * invscale); + } + + if (AreaEdgeBefore(window.sy2, window.fsy2) && AreaEdgeBefore(window.sx2, window.fsx2)) + { + out = out + + GoldValueAt(ctx, window.sx2, window.sy2) + * (-AreaDelta(window.sy2, window.fsy2) * -AreaDelta(window.sx2, window.fsx2) * invscale); + } + + if (AreaEdgeBefore(window.sy2, window.fsy2) && AreaEdgeAfter(window.sx1, window.fsx1)) + { + out = out + + GoldValueAt(ctx, window.sx1 - 1, window.sy2) + * (-AreaDelta(window.sy2, window.fsy2) * AreaDelta(window.sx1, window.fsx1) * invscale); + } +} + +template +inline ValueType GoldInterpArea(const GoldInterpContext &ctx, float2 scale, float2 coord) +{ + AreaWindow window = GetAreaWindow(scale, coord); + auto out = cuda::SetAll>(0); + if (std::ceil(scale.x) == scale.x && std::ceil(scale.y) == scale.y) + { + AddAreaBlock(out, ctx, window.sy1, window.sy2, window.sx1, window.sx2, 1.f / (scale.x * scale.y)); return cuda::SaturateCast(out); } + + float invscale = 1.f + / (std::min(scale.x, AreaDelta(ctx.size.x, window.fsx1)) + * std::min(scale.y, AreaDelta(ctx.size.y, window.fsy1))); + + AddAreaBlock(out, ctx, window.sy1, window.sy2, window.sx1, window.sx2, invscale); + AddAreaSideColumns(out, ctx, window, invscale); + AddAreaSideRows(out, ctx, window, invscale); + AddAreaCorners(out, ctx, window, invscale); + + return cuda::SaturateCast(out); +} + +template +inline ValueType GoldInterp(const std::vector &vec, const StridesType &strides, const int2 &size, + const ValueType &bValue, float2 scale, float2 coord, int z = 0, int k = 0) +{ + constexpr int N = cuda::NumElements; + + GoldInterpContext ctx{vec, strides, size, bValue, z, k}; + + if constexpr (I == NVCV_INTERP_NEAREST) + { + return GoldInterpNearest(ctx, coord); + } + else if constexpr (I == NVCV_INTERP_LINEAR) + { + return GoldInterpLinear(ctx, coord); + } + else if constexpr (I == NVCV_INTERP_CUBIC) + { + return GoldInterpCubic(ctx, coord); + } + else if constexpr (I == NVCV_INTERP_AREA) + { + return GoldInterpArea(ctx, scale, coord); + } } } // namespace nvcv::test diff --git a/tests/common/MixTypedTests.hpp b/tests/common/MixTypedTests.hpp index 2d9b6ae1d..d6797a912 100644 --- a/tests/common/MixTypedTests.hpp +++ b/tests/common/MixTypedTests.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -253,14 +253,17 @@ struct WrapParams> // Register at run time all the values for a given TYPE. template class FIXTURE, int... IDX, class TYPE, class TUPLE> -void RegisterTests(const ParamInfo &data, const char *casename, const char *testname, const char *file, - int line, int &base) +void RegisterTests(const ParamInfo &data, const char *casename, const char *testname, const char *, int, + int &base) { for (size_t i = 0; i < data.values.size(); ++i) { std::ostringstream ss; - ss << testname << '/' << base++; - ::testing::RegisterTest(casename, ss.str().c_str(), nullptr, data.values[i].second.c_str(), __FILE__, __LINE__, + ss << testname << '/' << base; + ++base; + ::testing::RegisterTest(casename, ss.str().c_str(), nullptr, data.values[i].second.c_str(), + __FILE__, // NOSONAR: std::source_location is C++20. + __LINE__, // NOSONAR: std::source_location is C++20. [value = data.values[i].first]() -> typename FIXTURE::BaseFixture * { auto fix = std::make_unique>(value); @@ -269,16 +272,16 @@ void RegisterTests(const ParamInfo &data, const char *casename, con class Skip final : public FIXTURE::BaseFixture { public: - virtual void SetUp() override + void SetUp() override { GTEST_SKIP(); }; - virtual void TestBody() override + void TestBody() override { FAIL() << "Should not be executed"; } }; - return new Skip; + return std::make_unique().release(); } else { @@ -326,7 +329,7 @@ struct MakeDependent }; \ NVCV_MIXTYPED_TEST_SUITE_F(CaseName, __VA_ARGS__) -#define NVCV_MIXTYPED_TEST(CaseName, TestName) \ +#define NVCV_MIXTYPED_TEST(CaseName, TestName) /* NOSONAR: std::source_location is C++20. */ \ template \ class CaseName##TestName##_Fixture final : public CaseName \ { \ @@ -338,7 +341,7 @@ struct MakeDependent : m_params(p) \ { \ } \ - virtual void TestBody() override; \ + void TestBody() override; \ template \ using GetType = typename ::nvcv::test::type::detail::GetTypeImpl::type; \ template \ diff --git a/tests/common/ObjectBag.cpp b/tests/common/ObjectBag.cpp index 7851d7d48..132e75838 100644 --- a/tests/common/ObjectBag.cpp +++ b/tests/common/ObjectBag.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -33,7 +33,7 @@ ObjectBag::~ObjectBag() void ObjectBag::insert(NVCVAllocatorHandle handle) { - m_objs.push([handle]() { nvcvAllocatorDecRef(handle, nullptr); }); + m_objs.emplace([handle]() { nvcvAllocatorDecRef(handle, nullptr); }); } } // namespace nvcv::test diff --git a/tests/common/ObjectBag.hpp b/tests/common/ObjectBag.hpp index 7155f4e72..ce4c9d405 100644 --- a/tests/common/ObjectBag.hpp +++ b/tests/common/ObjectBag.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -30,8 +30,9 @@ namespace nvcv::test { class ObjectBag final { public: - ObjectBag() = default; - ObjectBag(const ObjectBag &) = delete; + ObjectBag() = default; + ObjectBag(const ObjectBag &) = delete; + ObjectBag &operator=(const ObjectBag &) = delete; ~ObjectBag(); diff --git a/tests/common/TensorDataUtils.cpp b/tests/common/TensorDataUtils.cpp index 243fb69e2..3ce5f734c 100644 --- a/tests/common/TensorDataUtils.cpp +++ b/tests/common/TensorDataUtils.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -93,7 +93,6 @@ void PrintImageFromByteVector(const uint8_t *data, int width, int height, int ro } TensorImageData::TensorImageData(const TensorData &tensorData, int sampleIndex) - : m_planeStride(0) { if (!nvcv::TensorDataAccessStridedImage::IsCompatible(tensorData)) throw Exception(Status::ERROR_INVALID_ARGUMENT, "Tensor Data not compatible with Pitch Access"); @@ -111,7 +110,7 @@ TensorImageData::TensorImageData(const TensorData &tensorData, int sampleIndex) if (m_layout != NVCV_TENSOR_CHW && m_layout != NVCV_TENSOR_NCHW && m_layout != NVCV_TENSOR_HWC && m_layout != NVCV_TENSOR_NHWC) { - throw std::runtime_error("Tensor layout unknown"); + throw TensorDataUtilsError("Tensor layout unknown"); } long sampleStride = tDataAc->sampleStride(); @@ -132,7 +131,7 @@ TensorImageData::TensorImageData(const TensorData &tensorData, int sampleIndex) if (m_planar) { if (!nvcv::TensorDataAccessStridedImagePlanar::IsCompatible(tensorData)) - throw std::runtime_error("Tensor Data not compatible with Pitch Planar Access"); + throw TensorDataUtilsError("Tensor Data not compatible with Pitch Planar Access"); auto tDataACp = nvcv::TensorDataAccessStridedImagePlanar::Create(tensorData); m_planeStride = tDataACp->planeStride(); @@ -150,16 +149,16 @@ std::ostream &operator<<(std::ostream &out, const TensorImageData &cvImageData) if (!cvImageData.m_planar) { - printPlane(&cvImageData.m_data[0], cvImageData.m_size.w, cvImageData.m_size.h, cvImageData.m_rowStride, - cvImageData.m_bytesPerC, cvImageData.m_numC); + printPlane(&cvImageData.m_data[0], cvImageData.m_size.w, cvImageData.m_size.h, + static_cast(cvImageData.m_rowStride), cvImageData.m_bytesPerC, cvImageData.m_numC); } else { for (int i = 0; i < cvImageData.m_numC; i++) { out << "\nPlane = " << i << "\n"; - printPlane(&cvImageData.m_data[cvImageData.m_planeStride * i], cvImageData.m_size.w, cvImageData.m_size.h, - cvImageData.m_rowStride, cvImageData.m_bytesPerC, 1); + printPlane(&cvImageData.m_data[static_cast(cvImageData.m_planeStride * i)], cvImageData.m_size.w, + cvImageData.m_size.h, static_cast(cvImageData.m_rowStride), cvImageData.m_bytesPerC, 1); } } return out << "\n"; @@ -265,10 +264,10 @@ static void GetImageByteVectorFromTensorPlanar(const TensorData &tensorData, int Optional tDataAc = nvcv::TensorDataAccessStridedImagePlanar::Create(tensorData); if (!tDataAc) - throw std::runtime_error("Tensor Data not compatible with planar access."); + throw TensorDataUtilsError("Tensor Data not compatible with planar access."); if (tDataAc->numSamples() <= sample || sample < 0) - throw std::runtime_error("Number of samples smaller than requested sample."); + throw TensorDataUtilsError("Number of samples smaller than requested sample."); // in a planar tensor the dtype represents each plane so the total bytes per pixel must be calculated int bytesPerC = tDataAc->dtype().bitsPerPixel() / 8; @@ -276,15 +275,15 @@ static void GetImageByteVectorFromTensorPlanar(const TensorData &tensorData, int // Make sure we have the right size. outData.resize(outputSizeBytes); - Byte *basePtr = tDataAc->sampleData(sample); - size_t dstWidth = tDataAc->numCols() * bytesPerC; + const Byte *basePtr = tDataAc->sampleData(sample); + size_t dstWidth = tDataAc->numCols() * bytesPerC; for (int i = 0; i < tDataAc->numChannels(); ++i) { if (cudaSuccess != cudaMemcpy2D(outData.data() + (i * (tDataAc->numCols() * tDataAc->numRows()) * bytesPerC), dstWidth, basePtr, tDataAc->rowStride(), dstWidth, tDataAc->numRows(), cudaMemcpyDeviceToHost)) { - throw std::runtime_error("CudaMemcpy failed on copy of channel plane from device to host."); + throw TensorDataUtilsError("CudaMemcpy failed on copy of channel plane from device to host."); } basePtr += tDataAc->planeStride(); } @@ -296,12 +295,12 @@ void GetImageByteVectorFromTensor(const TensorData &tensorData, int sample, std: Optional tDataAc = nvcv::TensorDataAccessStridedImage::Create(tensorData); if (!tDataAc) - throw std::runtime_error("Tensor Data not compatible with pitch access."); + throw TensorDataUtilsError("Tensor Data not compatible with pitch access."); if (tDataAc->infoLayout().isChannelFirst()) return GetImageByteVectorFromTensorPlanar(tensorData, sample, outData); if (tDataAc->numSamples() <= sample || sample < 0) - throw std::runtime_error("Number of samples smaller than requested sample."); + throw TensorDataUtilsError("Number of samples smaller than requested sample."); int bytesPerPixel = (tDataAc->dtype().bitsPerPixel() / 8) * tDataAc->numChannels(); int outputSizeBytes = tDataAc->numRows() * tDataAc->numCols() * bytesPerPixel; @@ -314,7 +313,7 @@ void GetImageByteVectorFromTensor(const TensorData &tensorData, int sample, std: tDataAc->rowStride(), tDataAc->numCols() * bytesPerPixel, tDataAc->numRows(), cudaMemcpyDeviceToHost)) { - throw std::runtime_error("CudaMemcpy failed"); + throw TensorDataUtilsError("CudaMemcpy failed"); } return; } @@ -324,31 +323,31 @@ static void SetImageTensorFromByteVectorPlanar(const TensorData &tensorData, std Optional tDataAc = nvcv::TensorDataAccessStridedImagePlanar::Create(tensorData); if (!tDataAc) - throw std::runtime_error("Tensor Data not compatible with planar image access."); + throw TensorDataUtilsError("Tensor Data not compatible with planar image access."); if (tDataAc->numSamples() <= sample) - throw std::runtime_error("Number of samples smaller than requested sample."); + throw TensorDataUtilsError("Number of samples smaller than requested sample."); if ((int64_t)data.size() != tDataAc->numCols() * tDataAc->numRows() * (tDataAc->dtype().bitsPerPixel() / 8) * tDataAc->numChannels()) - throw std::runtime_error("Data vector is incorrect size, size must be W*H*bytesPerPixel."); + throw TensorDataUtilsError("Data vector is incorrect size, size must be W*H*bytesPerPixel."); int bytesPerC = (tDataAc->dtype().bitsPerPixel() / 8); - auto copyToGpu = [&](int j) + auto copyToGpu = [&tDataAc, &data, &bytesPerC](int j) { Byte *basePtr = tDataAc->sampleData(j); for (int i = 0; i < tDataAc->numChannels(); ++i) { - Byte *srcPtr = data.data() + (i * (tDataAc->numCols() * tDataAc->numRows() * bytesPerC)); - size_t srcPitch = tDataAc->numCols() * bytesPerC; - size_t srcWidthBytes = tDataAc->numCols() * bytesPerC; - if (cudaSuccess + const Byte *srcPtr = data.data() + (i * (tDataAc->numCols() * tDataAc->numRows() * bytesPerC)); + size_t srcPitch = tDataAc->numCols() * bytesPerC; + if (size_t srcWidthBytes = tDataAc->numCols() * bytesPerC; + cudaSuccess != cudaMemcpy2D(basePtr, tDataAc->rowStride(), srcPtr, srcPitch, srcWidthBytes, tDataAc->numRows(), cudaMemcpyHostToDevice)) { - throw std::runtime_error("CudaMemcpy failed for channel plane copy from host to device."); + throw TensorDataUtilsError("CudaMemcpy failed for channel plane copy from host to device."); } basePtr += tDataAc->planeStride(); } @@ -368,32 +367,32 @@ void SetImageTensorFromByteVector(const TensorData &tensorData, std::vector tDataAc = nvcv::TensorDataAccessStridedImage::Create(tensorData); if (!tDataAc) - throw std::runtime_error("Tensor Data not compatible with pitch access."); + throw TensorDataUtilsError("Tensor Data not compatible with pitch access."); if (tDataAc->infoLayout().isChannelFirst()) // planar case return SetImageTensorFromByteVectorPlanar(tensorData, data, sample); if (tDataAc->numSamples() <= sample) - throw std::runtime_error("Number of samples smaller than requested sample."); + throw TensorDataUtilsError("Number of samples smaller than requested sample."); if ((int64_t)data.size() != tDataAc->numCols() * tDataAc->numRows() * (tDataAc->dtype().bitsPerPixel() / 8) * tDataAc->numChannels()) - throw std::runtime_error("Data vector is incorrect size, size must be N*W*sizeof(pixel)."); + throw TensorDataUtilsError("Data vector is incorrect size, size must be N*W*sizeof(pixel)."); int bytesPerC = (tDataAc->dtype().bitsPerPixel() / 8); - auto copyToGpu = [&](int i) + auto copyToGpu = [&tDataAc, &data, &bytesPerC](int i) { - Byte *basePtr = tDataAc->sampleData(i); - Byte *srcPtr = data.data(); - size_t srcPitch = tDataAc->numCols() * bytesPerC * tDataAc->numChannels(); - size_t srcWidthBytes = tDataAc->numCols() * bytesPerC * tDataAc->numChannels(); + Byte *basePtr = tDataAc->sampleData(i); + const Byte *srcPtr = data.data(); + size_t srcPitch = tDataAc->numCols() * bytesPerC * tDataAc->numChannels(); + size_t srcWidthBytes = tDataAc->numCols() * bytesPerC * tDataAc->numChannels(); if (cudaSuccess != cudaMemcpy2D(basePtr, tDataAc->rowStride(), srcPtr, srcPitch, srcWidthBytes, tDataAc->numRows(), cudaMemcpyHostToDevice)) { - throw std::runtime_error("CudaMemcpy failed on copy of image from host to device."); + throw TensorDataUtilsError("CudaMemcpy failed on copy of image from host to device."); } }; diff --git a/tests/common/TensorDataUtils.hpp b/tests/common/TensorDataUtils.hpp index 337197e2f..407183261 100644 --- a/tests/common/TensorDataUtils.hpp +++ b/tests/common/TensorDataUtils.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -24,14 +24,29 @@ #include #include #include +#include +#include #include #include #include +#include #include namespace nvcv::util { +class CudaMemcpyError : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + +class TensorDataUtilsError : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + enum chflags { C0 = 0x1 << 0, @@ -47,9 +62,8 @@ enum chflags // int3 coord{...}; // ValueAt(vec, strides, coord) = 0; template>, typename RT = std::conditional_t, const T, T>> -inline RT &ValueAt(VecType &vec, const ST &strides, const CT &coord) +inline RT &ValueAt(VecType &vec, const ST &strides, const CT &coord) requires nvcv::cuda::detail::IsSameCompound { return *reinterpret_cast(&vec[nvcv::cuda::dot(coord, strides)]); } @@ -172,28 +186,28 @@ class TensorImageData template T *item(const int x, const int y, const int c) { - uint32_t byteIndex = 0; + size_t byteIndex = 0; if (!m_planar) - byteIndex = (c * m_bytesPerC) + (x * m_bytesPerC * m_numC) + (y * m_rowStride); + byteIndex = static_cast((c * m_bytesPerC) + (x * m_bytesPerC * m_numC) + (y * m_rowStride)); else - byteIndex = (c * m_planeStride) + (x * m_bytesPerC) + (y * m_rowStride); + byteIndex = static_cast((c * m_planeStride) + (x * m_bytesPerC) + (y * m_rowStride)); if (byteIndex >= m_data.size()) - throw std::runtime_error("Requested data out of bounds"); + throw TensorDataUtilsError("Requested data out of bounds"); - return reinterpret_cast(reinterpret_cast(m_data.data()) + byteIndex); + return reinterpret_cast(m_data.data() + byteIndex); } private: - std::vector m_data; // pointer to local data - Size2D m_size; // h/w in logical pixels, byte offset == m_size.x * bytesPerPixel. - int64_t m_rowStride; // Row stride in bytes - int64_t m_planeStride; // used for (n)CHW Tensors 0 if not CHW - int m_numC; // Number of color channels usually 1,3,4 (Y, RGB, ARGB) - bool m_planar; // If true the image is (n)CHW - int32_t m_bytesPerC; // bytes per logical pixels - NVCVTensorLayout m_layout; // layout of originating ITensor NVCV_TENSOR_CHW/NVCV_TENSOR_NHWC/HWC + std::vector m_data; // pointer to local data + Size2D m_size; // h/w in logical pixels, byte offset == m_size.x * bytesPerPixel. + int64_t m_rowStride; // Row stride in bytes + int64_t m_planeStride = 0; // used for (n)CHW Tensors 0 if not CHW + int m_numC; // Number of color channels usually 1,3,4 (Y, RGB, ARGB) + bool m_planar; // If true the image is (n)CHW + int32_t m_bytesPerC; // bytes per logical pixels + NVCVTensorLayout m_layout; // layout of originating ITensor NVCV_TENSOR_CHW/NVCV_TENSOR_NHWC/HWC }; /** @@ -356,23 +370,23 @@ void PrintImageFromByteVector(const uint8_t *data, int width, int height, int ro bool planar); template -void SetTensorTo(const TensorData &tensorData, DT data, int sample) +static void SetTensorTo(const TensorData &tensorData, DT data, int sample) { if (!nvcv::TensorDataAccessStrided::IsCompatible(tensorData)) - throw std::runtime_error("Tensor Data is not pitch access capable."); + throw TensorDataUtilsError("Tensor Data is not pitch access capable."); auto tDataAc = nvcv::TensorDataAccessStrided::Create(tensorData); if (tDataAc->numSamples() <= sample) - throw std::runtime_error("Number of samples smaller than requested sample."); + throw TensorDataUtilsError("Number of samples smaller than requested sample."); - int inElements = (tDataAc->sampleStride() / sizeof(DT)); + auto inElements = static_cast(tDataAc->sampleStride() / sizeof(DT)); std::vector

srcVec(inElements, data); int totalSamples; if (sample < 0) { - totalSamples = tDataAc->numSamples(); + totalSamples = static_cast(tDataAc->numSamples()); sample = 0; } else @@ -386,10 +400,8 @@ void SetTensorTo(const TensorData &tensorData, DT data, int sample) size_t size = tDataAc->sampleStride(); if (auto err = cudaMemcpy(outSamplePtr, srcVec.data(), size, cudaMemcpyHostToDevice)) { - char msg[1024] = {}; - snprintf(msg, sizeof(msg), "CudaMemcpy failed with %s (%i): %s", cudaGetErrorName(err), err, - cudaGetErrorString(err)); - throw std::runtime_error(msg); + throw CudaMemcpyError(std::string("CudaMemcpy failed with ") + cudaGetErrorName(err) + " (" + + std::to_string(err) + "): " + cudaGetErrorString(err)); } } @@ -400,21 +412,21 @@ template static void SetTensorToRandomValueFloat(const TensorData &tensorData, DT minVal, DT maxVal, int sample) { if (!nvcv::TensorDataAccessStrided::IsCompatible(tensorData)) - throw std::runtime_error("Tensor Data is not pitch access capable."); + throw TensorDataUtilsError("Tensor Data is not pitch access capable."); auto tDataAc = nvcv::TensorDataAccessStrided::Create(tensorData); if (tDataAc->numSamples() <= sample) - throw std::runtime_error("Number of samples smaller than requested sample."); + throw TensorDataUtilsError("Number of samples smaller than requested sample."); - int inElements = (tDataAc->sampleStride() / sizeof(DT)); + auto inElements = static_cast(tDataAc->sampleStride() / sizeof(DT)); std::vector
srcVec(inElements); std::default_random_engine randEng(0); int totalSamples; if (sample < 0) { - totalSamples = tDataAc->numSamples(); + totalSamples = static_cast(tDataAc->numSamples()); sample = 0; } else @@ -425,11 +437,12 @@ static void SetTensorToRandomValueFloat(const TensorData &tensorData, DT minVal, std::uniform_real_distribution<> srcRand(minVal, maxVal); for (int i = sample; i < totalSamples; ++i) { - std::generate(srcVec.begin(), srcVec.end(), [&]() { return srcRand(randEng); }); + std::generate( // NOSONAR: std::ranges::generate is C++20. + srcVec.begin(), srcVec.end(), [&srcRand, &randEng]() { return srcRand(randEng); }); if (cudaSuccess != cudaMemcpy(tDataAc->sampleData(i), srcVec.data(), tDataAc->sampleStride(), cudaMemcpyHostToDevice)) { - throw std::runtime_error("CudaMemcpy failed"); + throw TensorDataUtilsError("CudaMemcpy failed"); } } return; @@ -451,21 +464,21 @@ template static void SetTensorToRandomValue(const TensorData &tensorData, DT minVal, DT maxVal, int sample) { if (!nvcv::TensorDataAccessStrided::IsCompatible(tensorData)) - throw std::runtime_error("Tensor Data is not pitch access capable."); + throw TensorDataUtilsError("Tensor Data is not pitch access capable."); auto tDataAc = nvcv::TensorDataAccessStrided::Create(tensorData); if (tDataAc->numSamples() <= sample) - throw std::runtime_error("Number of samples smaller than requested sample."); + throw TensorDataUtilsError("Number of samples smaller than requested sample."); - int inElements = (tDataAc->sampleStride() / sizeof(DT)); + auto inElements = static_cast(tDataAc->sampleStride() / sizeof(DT)); std::vector
srcVec(inElements); std::default_random_engine randEng(0); int totalSamples; if (sample < 0) { - totalSamples = tDataAc->numSamples(); + totalSamples = static_cast(tDataAc->numSamples()); sample = 0; } else @@ -475,11 +488,12 @@ static void SetTensorToRandomValue(const TensorData &tensorData, DT minVal, DT m std::uniform_int_distribution
srcRand{minVal, maxVal}; for (int i = sample; i < totalSamples; ++i) { - std::generate(srcVec.begin(), srcVec.end(), [&]() { return srcRand(randEng); }); + std::generate( // NOSONAR: std::ranges::generate is C++20. + srcVec.begin(), srcVec.end(), [&srcRand, &randEng]() { return srcRand(randEng); }); if (cudaSuccess != cudaMemcpy(tDataAc->sampleData(i), srcVec.data(), tDataAc->sampleStride(), cudaMemcpyHostToDevice)) { - throw std::runtime_error("CudaMemcpy failed"); + throw TensorDataUtilsError("CudaMemcpy failed"); } } @@ -487,18 +501,18 @@ static void SetTensorToRandomValue(const TensorData &tensorData, DT minVal, DT m } template -void SetTensorFromVector(const TensorData &tensorData, std::vector
&data, int sample) +static void SetTensorFromVector(const TensorData &tensorData, std::vector
&data, int sample) { if (!nvcv::TensorDataAccessStrided::IsCompatible(tensorData)) - throw std::runtime_error("Tensor Data is not pitch access capable."); + throw TensorDataUtilsError("Tensor Data is not pitch access capable."); auto tDataAc = nvcv::TensorDataAccessStrided::Create(tensorData); if ((int64_t)(data.size() * sizeof(DT)) != tDataAc->sampleStride()) - throw std::runtime_error("Data vector is incorrect size."); + throw TensorDataUtilsError("Data vector is incorrect size."); if (tDataAc->numSamples() <= sample) - throw std::runtime_error("Number of samples smaller than requested sample."); + throw TensorDataUtilsError("Number of samples smaller than requested sample."); if (sample < 0) { @@ -507,7 +521,7 @@ void SetTensorFromVector(const TensorData &tensorData, std::vector
&data, in if (cudaSuccess != cudaMemcpy(tDataAc->sampleData(i), data.data(), tDataAc->sampleStride(), cudaMemcpyHostToDevice)) { - throw std::runtime_error("CudaMemcpy failed"); + throw TensorDataUtilsError("CudaMemcpy failed"); } } } @@ -516,7 +530,7 @@ void SetTensorFromVector(const TensorData &tensorData, std::vector
&data, in if (cudaSuccess != cudaMemcpy(tDataAc->sampleData(sample), data.data(), tDataAc->sampleStride(), cudaMemcpyHostToDevice)) { - throw std::runtime_error("CudaMemcpy failed"); + throw TensorDataUtilsError("CudaMemcpy failed"); } } @@ -524,24 +538,24 @@ void SetTensorFromVector(const TensorData &tensorData, std::vector
&data, in } template -void GetVectorFromTensor(const TensorData &tensorData, int sample, std::vector
&outData) +static void GetVectorFromTensor(const TensorData &tensorData, int sample, std::vector
&outData) { if (!nvcv::TensorDataAccessStrided::IsCompatible(tensorData)) - throw std::runtime_error("Tensor Data is not pitch access capable."); + throw TensorDataUtilsError("Tensor Data is not pitch access capable."); auto tDataAc = nvcv::TensorDataAccessStrided::Create(tensorData); if (tDataAc->numSamples() <= sample || sample < 0) - throw std::runtime_error("Number of samples smaller than requested sample."); + throw TensorDataUtilsError("Number of samples smaller than requested sample."); - int elements = (tDataAc->sampleStride() / sizeof(DT)); + auto elements = static_cast(tDataAc->sampleStride() / sizeof(DT)); outData.resize(elements); if (cudaSuccess != cudaMemcpy(outData.data(), tDataAc->sampleData(sample), tDataAc->sampleStride(), cudaMemcpyDeviceToHost)) { - throw std::runtime_error("CudaMemcpy failed"); + throw TensorDataUtilsError("CudaMemcpy failed"); } return; @@ -553,15 +567,15 @@ static void SetImageTensorFromVectorPlanar(const TensorData &tensorData, std::ve Optional tDataAc = nvcv::TensorDataAccessStridedImagePlanar::Create(tensorData); if (!tDataAc) - throw std::runtime_error("Tensor Data not compatible with planar image access."); + throw TensorDataUtilsError("Tensor Data not compatible with planar image access."); if (tDataAc->numSamples() <= sample) - throw std::runtime_error("Number of samples smaller than requested sample."); + throw TensorDataUtilsError("Number of samples smaller than requested sample."); if ((int64_t)data.size() != tDataAc->numCols() * tDataAc->numRows() * tDataAc->numChannels()) - throw std::runtime_error("Data vector is incorrect size, size must be W*C*sizeof(DT)*channels."); + throw TensorDataUtilsError("Data vector is incorrect size, size must be W*C*sizeof(DT)*channels."); - auto copyToGpu = [&](int j) + auto copyToGpu = [&tDataAc, &data](int j) { Byte *basePtr = tDataAc->sampleData(j); for (int i = 0; i < tDataAc->numChannels(); ++i) @@ -572,7 +586,7 @@ static void SetImageTensorFromVectorPlanar(const TensorData &tensorData, std::ve tDataAc->numCols() * sizeof(DT), tDataAc->numCols() * sizeof(DT), tDataAc->numRows(), cudaMemcpyHostToDevice)) { - throw std::runtime_error("CudaMemcpy failed for channel plane copy from host to device."); + throw TensorDataUtilsError("CudaMemcpy failed for channel plane copy from host to device."); } basePtr += tDataAc->planeStride(); } @@ -593,18 +607,18 @@ static void SetImageTensorFromVector(const TensorData &tensorData, std::vector tDataAc = nvcv::TensorDataAccessStridedImage::Create(tensorData); if (!tDataAc) - throw std::runtime_error("Tensor Data not compatible with pitch access."); + throw TensorDataUtilsError("Tensor Data not compatible with pitch access."); if (tDataAc->infoLayout().isChannelFirst()) // planar case return SetImageTensorFromVectorPlanar
(tensorData, data, sample); if (tDataAc->numSamples() <= sample) - throw std::runtime_error("Number of samples smaller than requested sample."); + throw TensorDataUtilsError("Number of samples smaller than requested sample."); if ((int64_t)data.size() != tDataAc->numCols() * tDataAc->numRows() * tDataAc->numChannels()) - throw std::runtime_error("Data vector is incorrect size, size must be N*W*C*sizeof(DT)."); + throw TensorDataUtilsError("Data vector is incorrect size, size must be N*W*C*sizeof(DT)."); - auto copyToGpu = [&](int i) + auto copyToGpu = [&tDataAc, &data](int i) { Byte *basePtr = tDataAc->sampleData(i); if (cudaSuccess @@ -612,7 +626,7 @@ static void SetImageTensorFromVector(const TensorData &tensorData, std::vectorrowStride(), data.data(), tDataAc->numCols() * tDataAc->numChannels() * sizeof(DT), tDataAc->numCols() * tDataAc->numChannels() * sizeof(DT), tDataAc->numRows(), cudaMemcpyHostToDevice)) { - throw std::runtime_error("CudaMemcpy failed on copy of image from host to device."); + throw TensorDataUtilsError("CudaMemcpy failed on copy of image from host to device."); } }; @@ -631,10 +645,10 @@ static void GetImageVectorFromTensorPlanar(const TensorData &tensorData, int sam Optional tDataAc = nvcv::TensorDataAccessStridedImagePlanar::Create(tensorData); if (!tDataAc) - throw std::runtime_error("Tensor Data not compatible with planar access."); + throw TensorDataUtilsError("Tensor Data not compatible with planar access."); if (tDataAc->numSamples() <= sample || sample < 0) - throw std::runtime_error("Number of samples smaller than requested sample."); + throw TensorDataUtilsError("Number of samples smaller than requested sample."); int elements = tDataAc->numRows() * tDataAc->numCols() * tDataAc->numChannels(); @@ -648,7 +662,7 @@ static void GetImageVectorFromTensorPlanar(const TensorData &tensorData, int sam tDataAc->numCols() * sizeof(DT), basePtr, tDataAc->rowStride(), tDataAc->numCols() * sizeof(DT), tDataAc->numRows(), cudaMemcpyDeviceToHost)) { - throw std::runtime_error("CudaMemcpy failed on copy of channel plane from device to host."); + throw TensorDataUtilsError("CudaMemcpy failed on copy of channel plane from device to host."); } basePtr += tDataAc->planeStride(); } @@ -662,12 +676,12 @@ static void GetImageVectorFromTensor(const TensorData &tensorData, int sample, s Optional tDataAc = nvcv::TensorDataAccessStridedImage::Create(tensorData); if (!tDataAc) - throw std::runtime_error("Tensor Data not compatible with pitch access."); + throw TensorDataUtilsError("Tensor Data not compatible with pitch access."); if (tDataAc->infoLayout().isChannelFirst()) return GetImageVectorFromTensorPlanar
(tensorData, sample, outData); if (tDataAc->numSamples() <= sample || sample < 0) - throw std::runtime_error("Number of samples smaller than requested sample."); + throw TensorDataUtilsError("Number of samples smaller than requested sample."); int elements = tDataAc->numRows() * tDataAc->numCols() * tDataAc->numChannels(); @@ -680,65 +694,109 @@ static void GetImageVectorFromTensor(const TensorData &tensorData, int sample, s tDataAc->numCols() * sizeof(DT) * tDataAc->numChannels(), tDataAc->numRows(), cudaMemcpyDeviceToHost)) { - throw std::runtime_error("CudaMemcpy failed"); + throw TensorDataUtilsError("CudaMemcpy failed"); } return; } template -void SetCvDataTo(TensorImageData &cvImg, DT data, Size2D region, uint8_t chFlags) +static void SetCvDataPixelChannels(TensorImageData &cvImg, DT data, int x, int y, uint8_t chFlags) +{ + for (int c = 0; c < 4; c++) + { + if (((chFlags >> c) & 0x1) == 0x1) // NOSONAR: chFlags is a channel bitmask, not byte storage. + { + *cvImg.item
(x, y, c) = data; + } + } +} + +template +static void SetCvDataTo(TensorImageData &cvImg, DT data, Size2D region, uint8_t chFlags) { for (int x = 0; x < region.w; x++) + { for (int y = 0; y < region.h; y++) - for (int c = 0; c < 4; c++) - if (((chFlags >> c) & 0x1) == 0x1) - *cvImg.item
(x, y, c) = data; + { + SetCvDataPixelChannels(cvImg, data, x, y, chFlags); + } + } return; } // Useful for debugging +template +inline nvcv::cuda::BaseType BufferShapeElementOrOne(const ST &shape, int element) +{ + using BT = nvcv::cuda::BaseType; + + return nvcv::cuda::NumElements > element ? nvcv::cuda::GetElement(shape, element) : BT{1}; +} + template -inline void PrintBuffer(const std::vector &vec, const ST &strides, const ST &shape, const char *name = "", - uint32_t endls = 0b1111) +inline void PrintBufferValues(const std::vector &vec, const ST &strides, const ST &shape, + nvcv::cuda::BaseType x, nvcv::cuda::BaseType y, nvcv::cuda::BaseType z, + uint32_t endls) { using BT = nvcv::cuda::BaseType; using BT4 = nvcv::cuda::MakeType; using CVT = std::conditional_t; + std::cout << " " << std::flush; + for (BT w = 0; w < BufferShapeElementOrOne(shape, 3); ++w) + { + ST coord = nvcv::cuda::DropCast>(BT4{x, y, z, w}); + + std::cout << " " << static_cast(ValueAt(vec, strides, coord)) << std::flush; + } + + if (endls & 0b0010) + std::cout << std::endl; + else + std::cout << std::flush; +} + +template +inline void PrintBufferRows(const std::vector &vec, const ST &strides, const ST &shape, + nvcv::cuda::BaseType x, nvcv::cuda::BaseType y, uint32_t endls) +{ + using BT = nvcv::cuda::BaseType; + + if (endls & 0b0100) + std::cout << " [" << std::endl; + else + std::cout << " [" << std::flush; + + for (BT z = 0; z < BufferShapeElementOrOne(shape, 2); ++z) + { + PrintBufferValues(vec, strides, shape, x, y, z, endls); + } + + if (endls & 0b0001) + std::cout << " ]" << std::endl; + else + std::cout << " ]" << std::flush; +} + +template +inline void PrintBuffer(const std::vector &vec, const ST &strides, const ST &shape, const char *name = "", + uint32_t endls = 0b1111) +{ + using BT = nvcv::cuda::BaseType; + std::cout << "I Printing buffer " << name << " with:\nI\tSize = " << vec.size() << " Bytes\nI\tShape = " << shape << "\nI\tStrides = " << strides << "\nI\tValues = " << std::endl; - for (BT x = 0; x < (nvcv::cuda::NumElements >= 1 ? nvcv::cuda::GetElement(shape, 0) : 1); ++x) + for (BT x = 0; x < BufferShapeElementOrOne(shape, 0); ++x) { if (endls & 0b1000) std::cout << "{" << std::endl; else std::cout << "{" << std::flush; - for (BT y = 0; y < (nvcv::cuda::NumElements >= 2 ? nvcv::cuda::GetElement(shape, 1) : 1); ++y) + for (BT y = 0; y < BufferShapeElementOrOne(shape, 1); ++y) { - if (endls & 0b0100) - std::cout << " [" << std::endl; - else - std::cout << " [" << std::flush; - for (BT z = 0; z < (nvcv::cuda::NumElements >= 3 ? nvcv::cuda::GetElement(shape, 2) : 1); ++z) - { - std::cout << " " << std::flush; - for (BT w = 0; w < (nvcv::cuda::NumElements >= 4 ? nvcv::cuda::GetElement(shape, 3) : 1); ++w) - { - ST coord = nvcv::cuda::DropCast>(BT4{x, y, z, w}); - - std::cout << " " << static_cast(ValueAt(vec, strides, coord)) << std::flush; - } - if (endls & 0b0010) - std::cout << std::endl; - else - std::cout << std::flush; - } - if (endls & 0b0001) - std::cout << " ]" << std::endl; - else - std::cout << " ]" << std::flush; + PrintBufferRows(vec, strides, shape, x, y, endls); } std::cout << "}" << std::endl; } @@ -747,6 +805,62 @@ inline void PrintBuffer(const std::vector &vec, const ST &strides, cons // Write images in *HW tensor buffer vec to PGM files. // The file name provided should have two (one) "%ld" format substr to place the first two (one) indices. // The value type VT is converted to U8 when writing to each PGM file. +template +inline ST StripPGMCoord(long4_16a coord) +{ + if constexpr (nvcv::cuda::NumElements == 4) + return ST{coord}; + else if constexpr (nvcv::cuda::NumElements == 3) + return ST{coord.y, coord.z, coord.w}; + return ST{coord.z, coord.w}; +} + +template +inline VT ConvertPGMValue(VT val) +{ + if constexpr (std::is_same_v) + return val; + else if constexpr (std::is_integral_v && !std::is_signed_v) + return std::min((VT)255, std::max((VT)0, val)); + else if constexpr (std::is_integral_v && std::is_signed_v) + return std::min((VT)255, std::max((VT)0, (VT)std::abs(val))); + else + return std::min((VT)255, std::max((VT)0, (VT)std::round(std::abs(val)))); +} + +template +inline void WritePGMValues(std::ofstream &ofs, const std::vector &vec, const ST &strides, long c0, long c1, + int width, int height) +{ + for (long i = 0; i < height; ++i) + { + for (long j = 0; j < width; ++j) + { + ST coord = StripPGMCoord(long4_16a{c0, c1, i, j}); + VT val = util::ValueAt(vec, strides, coord); + + ofs << ConvertPGMValue(val) << ((j == width - 1) ? "\n" : " "); + } + } +} + +template +inline void WriteOnePGMImage(const char *filename, const std::vector &vec, const ST &strides, long c0, long c1, + int width, int height) +{ + std::array fn; + int numChars = std::snprintf(fn.data(), fn.size(), filename, c1, c0); + NVCV_ASSERT(numChars >= 0 && static_cast(numChars) < fn.size()); + + std::ofstream ofs(fn.data()); + + ofs << "P2\n" << width << " " << height << " 255\n"; + + WritePGMValues(ofs, vec, strides, c0, c1, width, height); + + ofs.close(); +} + template inline void WriteImagesToPGM(const char *filename, const std::vector &vec, const ST &strides, const ST &shape) { @@ -757,7 +871,8 @@ inline void WriteImagesToPGM(const char *filename, const std::vector &v int width = nvcv::cuda::GetElement(shape, widthIdx); int height = nvcv::cuda::GetElement(shape, heightIdx); - int c0size = 1, c1size = 1; + int c0size = 1; + int c1size = 1; if constexpr (nvcv::cuda::NumElements == 4) { c0size = nvcv::cuda::GetElement(shape, 0); @@ -768,52 +883,11 @@ inline void WriteImagesToPGM(const char *filename, const std::vector &v c1size = nvcv::cuda::GetElement(shape, 0); } - auto stripCoord = [](long4_16a coord) - { - if constexpr (nvcv::cuda::NumElements == 4) - return ST{coord}; - else if constexpr (nvcv::cuda::NumElements == 3) - return ST{coord.y, coord.z, coord.w}; - return ST{coord.z, coord.w}; - }; - - auto convertValue = [](VT val) - { - if constexpr (std::is_same_v) - return val; - else if constexpr (std::is_integral_v && !std::is_signed_v) - return std::min((VT)255, std::max((VT)0, val)); - else if constexpr (std::is_integral_v && std::is_signed_v) - return std::min((VT)255, std::max((VT)0, (VT)std::abs(val))); - else - return std::min((VT)255, std::max((VT)0, (VT)std::round(std::abs(val)))); - }; - - char fn[256]; - for (long c0 = 0; c0 < c0size; ++c0) { for (long c1 = 0; c1 < c1size; ++c1) { - sprintf(fn, filename, c1, c0); - - std::ofstream ofs(fn); - - ofs << "P2\n" << width << " " << height << " 255\n"; - - for (long i = 0; i < height; ++i) - { - for (long j = 0; j < width; ++j) - { - ST coord = stripCoord(long4_16a{c0, c1, i, j}); - - VT val = util::ValueAt(vec, strides, coord); - - ofs << convertValue(val) << ((j == width - 1) ? "\n" : " "); - } - } - - ofs.close(); + WriteOnePGMImage(filename, vec, strides, c0, c1, width, height); } } } @@ -829,7 +903,8 @@ inline void WritePyramidToPGM(const char *header, const std::vector(filename.c_str(), pyr[o][l], strides[o], shape[o]); } @@ -845,7 +920,7 @@ inline void WritePyramidToPGM(const char *header, const std::vector(filename.c_str(), pyr[o], strides[o], shape[o]); } diff --git a/tests/common/TypeList.hpp b/tests/common/TypeList.hpp index 0b0729ca9..2ef2cb538 100644 --- a/tests/common/TypeList.hpp +++ b/tests/common/TypeList.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -455,7 +455,7 @@ struct RemoveIfImpl> template struct RemoveIfImpl> { - using type = Concat::value, Types<>, Types>::type, + using type = Concat::value, Types<>, Types>, typename RemoveIfImpl>::type>; }; @@ -658,7 +658,7 @@ using SetDifference = RemoveIf, T>; // check if value is in Values container template -constexpr bool Contains(Types<>, T size) +constexpr bool Contains(Types<>, T) { return false; } diff --git a/tests/common/ValueList.hpp b/tests/common/ValueList.hpp index 1c30df8e6..a279bb2dd 100644 --- a/tests/common/ValueList.hpp +++ b/tests/common/ValueList.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -27,9 +27,16 @@ #include #include #include +#include namespace nvcv::test { +class ValueListError : public std::logic_error +{ +public: + using std::logic_error::logic_error; +}; + namespace detail { template @@ -98,7 +105,7 @@ struct Identity }; template -inline std::tuple<> ExtractTuple(std::tuple t) +inline std::tuple<> ExtractTuple(std::tuple) { return {}; } @@ -118,7 +125,7 @@ auto ExtractTuple(T t) } template -auto ExtractTuple(T t) +auto ExtractTuple(T) { return std::tuple<>(); } @@ -138,12 +145,12 @@ auto MaybeExtractTuple(T t) struct Default { - bool operator<(const Default &that) const + bool operator<(const Default &) const { return false; } - bool operator==(const Default &that) const + bool operator==(const Default &) const // NOSONAR: defaulted comparisons are C++20. { return true; } @@ -152,8 +159,8 @@ struct Default template void ReplaceDefaultsImpl(U &out, const T &in) { - using SRC = typename std::tuple_element::type; - using DST = typename std::tuple_element::type; + using SRC = std::tuple_element_t; + using DST = std::tuple_element_t; if constexpr (!std::is_same_v) { @@ -168,7 +175,7 @@ void ReplaceDefaultsImpl(U &out, const T &in, std::index_sequence) } template -std::enable_if_t>, std::tuple> ReplaceDefaults( +requires std::is_default_constructible_v> std::tuple ReplaceDefaults( const std::tuple &in) { static_assert(sizeof...(TT) == sizeof...(UU)); @@ -179,7 +186,7 @@ std::enable_if_t>, std::tuple< } template -std::enable_if_t>, std::tuple> ReplaceDefaults( +requires(!std::is_default_constructible_v>) std::tuple ReplaceDefaults( const std::tuple &in) { static_assert(sizeof...(TT) == sizeof...(UU)); @@ -220,8 +227,8 @@ class ValueList { } - template>, int> = 0> - explicit ValueList(const ValueList &that) + template + requires(!std::is_same_v>) explicit ValueList(const ValueList &that) { for (auto &v : that) { @@ -236,7 +243,7 @@ class ValueList } } - ValueList(const std::vector &v) + explicit ValueList(const std::vector &v) { m_list.insert(m_list.end(), v.begin(), v.end()); } @@ -292,8 +299,8 @@ class ValueList return m_list.emplace_back(std::move(v)); } - template, int> = 0> - auto push_back(tuple_value_type v) + template + requires(sizeof(X) != 0 && !std::is_same_v) auto push_back(tuple_value_type v) { return std::apply([this](auto &...args) { m_list.emplace_back(args...); }, v); } @@ -330,7 +337,7 @@ class ValueList return removedAtLeastOne; } - bool operator==(const ValueList &that) const + bool operator==(const ValueList &that) const // NOSONAR: defaulted comparisons are C++20. { return m_list == that.m_list; } @@ -342,7 +349,7 @@ class ValueList bool exists(const value_type &v) const { - return std::find(m_list.begin(), m_list.end(), v) != m_list.end(); + return std::find(m_list.begin(), m_list.end(), v) != m_list.end(); // NOSONAR: std::ranges::find is C++20. } template @@ -495,7 +502,7 @@ ValueList Difference(ValueList a, ValueList b) for (auto it = a.begin(); it != a.end();) { - if (binary_search(b.begin(), b.end(), *it)) + if (binary_search(b.begin(), b.end(), *it)) // NOSONAR: std::ranges::binary_search is C++20. { a.erase(it++); } @@ -560,7 +567,7 @@ ValueList Intersection(ValueList a, ValueList b, TAIL &&... for (auto it = a.begin(); it != a.end();) { - if (binary_search(tmp.begin(), tmp.end(), *it)) + if (binary_search(tmp.begin(), tmp.end(), *it)) // NOSONAR: std::ranges::binary_search is C++20. { ++it; } @@ -625,7 +632,7 @@ struct HasValueType : std::false_type }; template -struct HasValueType> : std::true_type +struct HasValueType> : std::true_type { }; @@ -673,13 +680,13 @@ auto Combine(ValueList a) template ValueList> Combine(T &&v) { - return {v}; + return {std::forward(v)}; } template ValueList...> Combine(TT &&...v) { - return {std::make_tuple(v...)}; + return {std::make_tuple(std::forward(v)...)}; } template @@ -687,14 +694,15 @@ auto Combine(ValueList a, TAIL &&...tail) { auto rest = Combine(std::forward(tail)...); - typename detail::NormalizeValueList< - ValueList(), typename decltype(rest)::tuple_value_type()))>>::type r; + using CombinedTuple = decltype(tuple_cat(std::declval>(), + std::declval())); + typename detail::NormalizeValueList>::type r; - for (auto ita = a.begin(); ita != a.end(); ++ita) + for (const auto &item : a) { - for (auto itr = rest.begin(); itr != rest.end(); ++itr) + for (const auto &restItem : rest) { - r.push_back(detail::JoinTuple(*ita, *itr)); + r.push_back(detail::JoinTuple(item, restItem)); } } @@ -704,7 +712,7 @@ auto Combine(ValueList a, TAIL &&...tail) template auto Combine(T &&a, TAIL &&...tail) { - return Combine(ValueList>{a}, std::forward(tail)...); + return Combine(ValueList>{std::forward(a)}, std::forward(tail)...); } // Zip ---------------------------- @@ -718,13 +726,13 @@ auto Zip(ValueList a) template ValueList> Zip(T &&v) { - return {v}; + return {std::forward(v)}; } template ValueList...> Zip(TT &&...v) { - return {std::make_tuple(v...)}; + return {std::make_tuple(std::forward(v)...)}; } template @@ -732,8 +740,9 @@ auto Zip(ValueList a, TAIL &&...tail) { auto rest = Zip(std::forward(tail)...); - typename detail::NormalizeValueList< - ValueList(), typename decltype(rest)::tuple_value_type()))>>::type r; + using CombinedTuple = decltype(tuple_cat(std::declval>(), + std::declval())); + typename detail::NormalizeValueList>::type r; if (a.size() == rest.size()) { @@ -744,7 +753,7 @@ auto Zip(ValueList a, TAIL &&...tail) } else { - throw std::logic_error("Zip: value lists can't have different sizes"); + throw ValueListError("Zip: value lists can't have different sizes"); } return r; @@ -753,26 +762,26 @@ auto Zip(ValueList a, TAIL &&...tail) template auto Zip(T &&a, TAIL &&...tail) { - return Zip(ValueList{a}, std::forward(tail)...); + return Zip(ValueList>{std::forward(a)}, std::forward(tail)...); } struct IsSameArgsFunctor { private: template - static bool isSameArgs(T &&) + static bool isSameArgs(const T &) { return false; } template - static bool isSameArgs(T &&a, U &&b) + static bool isSameArgs(const T &a, const U &b) { return a == b; } template - static bool isSameArgs(T &&a, U &&b, TAIL &&...tail) + static bool isSameArgs(const T &a, const U &b, const TAIL &...tail) { if (a == b) { @@ -802,7 +811,7 @@ struct MatchHelper private: // Termination criteria, nothing more to check. template - static bool match(std::integer_sequence, const T &item, const U &needle) + static bool match(std::integer_sequence, const T &, const U &) { static_assert(IDX == SEQ::size()); return true; @@ -818,13 +827,13 @@ struct MatchHelper ValueList m_needle; public: - MatchHelper(TT &&...needle) - : m_needle({typename ValueList::value_type{std::forward(needle)...}}) + explicit MatchHelper(TT &&...needle) + : m_needle({typename ValueList::value_type{static_cast(needle)...}}) { } // Matches any value in needle. - MatchHelper(ValueList needle) + explicit MatchHelper(ValueList needle) : m_needle(std::move(needle)) { } @@ -1078,8 +1087,8 @@ namespace detail { template struct Or { - LHS lhs; - RHS rhs; + LHS lhs; // NOSONAR: [[no_unique_address]] is C++20. + RHS rhs; // NOSONAR: [[no_unique_address]] is C++20. template constexpr auto operator()(Args &&...args) &noexcept(noexcept(std::invoke(lhs, args...) @@ -1121,8 +1130,8 @@ namespace detail { template struct And { - LHS lhs; - RHS rhs; + LHS lhs; // NOSONAR: [[no_unique_address]] is C++20. + RHS rhs; // NOSONAR: [[no_unique_address]] is C++20. template constexpr auto operator()(Args &&...args) &noexcept(noexcept(std::invoke(lhs, args...) diff --git a/tests/common/ValueTests.hpp b/tests/common/ValueTests.hpp index 2086ef71c..b9a59c1b5 100644 --- a/tests/common/ValueTests.hpp +++ b/tests/common/ValueTests.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,21 +21,46 @@ #include "HashMD5.hpp" #include "ValueList.hpp" +#include +#include +#include +#include + namespace nvcv::test { +template +std::ostream &PrintParamValue(std::ostream &out, const T &value) +{ + return out << value; +} + +template +std::ostream &PrintParamValue(std::ostream &out, const std::vector &vec) +{ + out << '{'; + const char *sep = ""; + for (const auto &value : vec) + { + out << sep; + PrintParamValue(out, value); + sep = ","; + } + return out << '}'; +} + template struct StringLiteral { constexpr StringLiteral(const char (&str)[N]) { - std::copy_n(str, N, value); + std::copy_n(str, N, value.begin()); } - char value[N]; + std::array value; friend std::ostream &operator<<(std::ostream &out, const StringLiteral &p) { - return out << p.value; + return out << p.value.data(); }; }; @@ -49,15 +74,14 @@ class Param static_assert(sizeof...(DEFAULT) <= 1); public: - template = 0> - constexpr Param() + template + requires(sizeof(U) * 0 + sizeof...(DEFAULT) == 1) constexpr Param() : m_value(DEFAULT...) { } - template && sizeof(U) * 0 + sizeof...(DEFAULT) == 0, int> = 0> - constexpr Param() + template + requires(std::is_default_constructible_v && sizeof(U) * 0 + sizeof...(DEFAULT) == 0) constexpr Param() : m_value(T{}) { } @@ -67,7 +91,18 @@ class Param { } - constexpr operator T() const + constexpr Param(const Param &) = default; + constexpr Param(Param &&) noexcept = default; + + constexpr Param &operator=(const Param &) = default; + constexpr Param &operator=(Param &&) noexcept = default; + + explicit constexpr operator T() const + { + return m_value; + } + + constexpr T value() const { return m_value; } @@ -75,12 +110,14 @@ class Param friend std::ostream &operator<<(std::ostream &out, Param p) { out << NAME << std::boolalpha; - out << '(' << p.m_value << ')'; + out << '('; + PrintParamValue(out, p.m_value); + out << ')'; out << std::noboolalpha; return out; }; - constexpr bool operator==(const Param &that) const + constexpr bool operator==(const Param &that) const // NOSONAR: defaulted comparisons are C++20. { return m_value == that.m_value; } @@ -92,7 +129,14 @@ class Param constexpr bool operator<(const Param &that) const { - return m_value < that.m_value; + if constexpr (std::is_same_v) + { + return static_cast(m_value) < static_cast(that.m_value); + } + else + { + return m_value < that.m_value; + } } private: @@ -105,6 +149,18 @@ void Update(test::HashMD5 &hash, const Param &p) Update(hash, static_cast(p)); } +template +decltype(auto) ParamValue(T &&value) +{ + return std::forward(value); +} + +template +constexpr T ParamValue(const Param &p) +{ + return p.value(); +} + namespace detail { template @@ -117,22 +173,18 @@ std::string GetTestParamHashHelper(const P &info) // We don't need 64 bit worth of variation, 32-bit is enough and leads // to shorter suffixes. - union Cast - { - uint8_t array[16]; - uint64_t value[2]; - }; + auto hashBytes = hash.getHashAndReset(); - static_assert(sizeof(hash.getHashAndReset()) == sizeof(Cast::array)); + std::array hashWords{}; + static_assert(sizeof(hashBytes) == sizeof(hashWords)); - Cast caster; - memcpy(caster.array, &hash.getHashAndReset()[0], sizeof(caster.array)); + memcpy(hashWords.data(), hashBytes.data(), sizeof(hashWords)); - uint64_t code64 = caster.value[0] ^ caster.value[1]; + uint64_t code64 = hashWords[0] ^ hashWords[1]; uint32_t code32 = (code64 & UINT32_MAX) ^ (code64 >> 32); std::ostringstream out; - out << std::hex << std::setw(sizeof(code32) * 2) << std::setfill('0') << code32; + out << std::hex << std::setw(sizeof(code32) * 2) << std::setfill('0') << code32; // NOSONAR: std::format is C++20. return out.str(); } @@ -175,7 +227,7 @@ struct TestSuffixPrinter template \ auto GetParamValue() const \ { \ - return std::get(GetParam()); \ + return ::nvcv::test::ParamValue(std::get(GetParam())); \ } \ }; \ NVCV_INSTANTIATE_TEST_SUITE_P(_, TEST, g_##TEST##_Params) diff --git a/tests/cvcuda/CMakeLists.txt b/tests/cvcuda/CMakeLists.txt index bf13809a3..1826ad1e0 100644 --- a/tests/cvcuda/CMakeLists.txt +++ b/tests/cvcuda/CMakeLists.txt @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -24,6 +24,9 @@ if(BUILD_TESTS_CPP) endif() if(BUILD_TESTS_PYTHON AND BUILD_PYTHON) + # NVTX injection probe used by the python NVTX marker test + add_subdirectory(nvtx_probe) + # System tests for cvcuda python add_subdirectory(python) endif() diff --git a/tests/cvcuda/nvtx_probe/CMakeLists.txt b/tests/cvcuda/nvtx_probe/CMakeLists.txt new file mode 100644 index 000000000..055fbea3d --- /dev/null +++ b/tests/cvcuda/nvtx_probe/CMakeLists.txt @@ -0,0 +1,27 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +# MODULE: the probe is only ever dlopen()ed (by NVTX, via NVTX_INJECTION64_PATH), never linked. +add_library(cvcuda_nvtx_probe MODULE NvtxProbe.cpp) + +target_link_libraries(cvcuda_nvtx_probe PRIVATE cvcuda_nvtx_config) + +set_target_properties(cvcuda_nvtx_probe PROPERTIES POSITION_INDEPENDENT_CODE ON) + +# Install next to the python tests so packaged (DEB/TAR) test runs can inject it too. Must match +# PYTHON_TEST_INSTDIR in ../python/CMakeLists.txt. +install(TARGETS cvcuda_nvtx_probe + LIBRARY DESTINATION "${CMAKE_INSTALL_DATADIR}/python/cvcuda" + COMPONENT tests) diff --git a/tests/cvcuda/nvtx_probe/NvtxProbe.cpp b/tests/cvcuda/nvtx_probe/NvtxProbe.cpp new file mode 100644 index 000000000..21854ef80 --- /dev/null +++ b/tests/cvcuda/nvtx_probe/NvtxProbe.cpp @@ -0,0 +1,148 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +// Test-only NVTX injection library. NVTX exposes no API to read ranges back, so to observe the +// ranges CV-CUDA emits this library is loaded via NVTX_INJECTION64_PATH: NVTX dlopen()s it on its +// first call and invokes InitializeInjectionNvtx2, which swaps in the recorders below. The test +// process reads the recorded names through the CvcudaNvtxProbe_* accessors. + +#include + +#include +#include +#include +#include + +namespace { + +// Cap on recorded names. NVTX reads the injection path once at process start, so the probe stays +// installed for the lifetime of whatever process loads it. A long-lived host (e.g. a full pytest +// session driving every operator, plus cupy's own NVTX ranges) would otherwise grow pushedNames +// without bound and can exhaust host memory on tighter runners. The marker test resets before each +// operator and reads immediately, so it never approaches this cap; recording simply stops here. +constexpr std::size_t kMaxRecordedNames = 4096; + +struct ProbeState +{ + std::mutex mutex; + std::vector pushedNames; +}; + +// A function-local static holds the recorder state: it gives the C callbacks and accessors a +// single shared, thread-safe-initialized record while avoiding non-const namespace-scope globals. +ProbeState &state() +{ + static ProbeState s; + return s; +} + +// Replacement for nvtxRangePushA: record the range name. The return value is the (1-based) range +// nesting level; NVTX callers don't depend on the exact value here. +int NVTX_API ProbeRangePushA(const char *message) +{ + std::scoped_lock lock(state().mutex); + if (state().pushedNames.size() < kMaxRecordedNames) + { + state().pushedNames.emplace_back(message ? message : ""); + } + return static_cast(state().pushedNames.size()); +} + +// Replacement for nvtxRangePop: nothing to record, pushes alone identify the ranges we assert on. +int NVTX_API ProbeRangePop(void) +{ + return 0; +} + +} // namespace + +// Forward declarations keep these exported entry points free of -Wmissing-declarations +// (CI builds with -DWARNINGS_AS_ERRORS=1) while retaining external linkage. +extern "C" int InitializeInjectionNvtx2(NvtxGetExportTableFunc_t getExportTable); +extern "C" void CvcudaNvtxProbe_Reset(void); +extern "C" unsigned int CvcudaNvtxProbe_Count(void); +extern "C" const char *CvcudaNvtxProbe_Name(unsigned int index); + +// NVTX injection entry point. Returning non-zero tells NVTX the injection succeeded and that the +// function table it handed us should be used. +extern "C" int InitializeInjectionNvtx2(NvtxGetExportTableFunc_t getExportTable) +{ + if (getExportTable == nullptr) + { + return 0; + } + + const auto *callbacks = static_cast(getExportTable(NVTX_ETID_CALLBACKS)); + if (callbacks == nullptr || callbacks->struct_size < sizeof(NvtxExportTableCallbacks) + || callbacks->GetModuleFunctionTable == nullptr) + { + return 0; + } + + // table is an array of pointers to the core module's function-pointer slots; size is the + // highest valid index. Each entry is the *address* of a slot, so we install a replacement by + // writing through it. + NvtxFunctionTable table = nullptr; + unsigned int size = 0; + if (callbacks->GetModuleFunctionTable(NVTX_CB_MODULE_CORE, &table, &size) == 0 || table == nullptr) + { + return 0; + } + + if (size < static_cast(NVTX_CBID_CORE_RangePop)) + { + return 0; + } + + if (table[NVTX_CBID_CORE_RangePushA] != nullptr) + { + *table[NVTX_CBID_CORE_RangePushA] = reinterpret_cast(&ProbeRangePushA); + } + if (table[NVTX_CBID_CORE_RangePop] != nullptr) + { + *table[NVTX_CBID_CORE_RangePop] = reinterpret_cast(&ProbeRangePop); + } + + return 1; +} + +// --- Accessors read by the test process (same process; this library is dlopen()ed into it). --- + +extern "C" void CvcudaNvtxProbe_Reset(void) +{ + std::scoped_lock lock(state().mutex); + state().pushedNames.clear(); +} + +extern "C" unsigned int CvcudaNvtxProbe_Count(void) +{ + std::scoped_lock lock(state().mutex); + return static_cast(state().pushedNames.size()); +} + +// Returns the i-th recorded range name, or "" if out of range. The pointer is valid until the next +// CvcudaNvtxProbe_Reset; the test reads names after the operator call completes (no concurrent +// pushes), so the backing storage is stable at read time. +extern "C" const char *CvcudaNvtxProbe_Name(unsigned int index) +{ + std::scoped_lock lock(state().mutex); + if (index >= state().pushedNames.size()) + { + return ""; + } + return state().pushedNames[index].c_str(); +} diff --git a/tests/cvcuda/python/CMakeLists.txt b/tests/cvcuda/python/CMakeLists.txt index 91e1b67dc..0f65fce12 100644 --- a/tests/cvcuda/python/CMakeLists.txt +++ b/tests/cvcuda/python/CMakeLists.txt @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -30,6 +30,8 @@ set(PYTHON_TEST_DIR ${CMAKE_CURRENT_SOURCE_DIR}) string(REPLACE ";" " " PYTHON_TEST_VERSIONS "${PYTHON_VERSIONS}") set(PYTHON_MODULE_DIR ${CMAKE_BINARY_DIR}/python3) +# Probe path in the build tree (target cvcuda_nvtx_probe in ../nvtx_probe). +set(NVTX_PROBE_PATH "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/${CMAKE_SHARED_MODULE_PREFIX}cvcuda_nvtx_probe${CMAKE_SHARED_MODULE_SUFFIX}") configure_file(cvcuda_test_python.in ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/cvcuda_test_python @ONLY) set(PYTHON_TEST_INSTDIR "${CMAKE_INSTALL_DATADIR}/python/cvcuda") @@ -40,6 +42,8 @@ install(FILES ${SOURCES} # create the test script that will get installed set(PYTHON_TEST_DIR ${CMAKE_INSTALL_PREFIX}/${PYTHON_TEST_INSTDIR}) +# Probe is installed alongside the python tests (see ../nvtx_probe/CMakeLists.txt). +set(NVTX_PROBE_PATH "${CMAKE_INSTALL_PREFIX}/${PYTHON_TEST_INSTDIR}/${CMAKE_SHARED_MODULE_PREFIX}cvcuda_nvtx_probe${CMAKE_SHARED_MODULE_SUFFIX}") # CRITICAL: Python modules are installed to ${CMAKE_INSTALL_LIBDIR}/python/, not just ${CMAKE_INSTALL_LIBDIR}/ # The /python suffix is required for the test script to correctly set PYTHONPATH and find cvcuda/nvcv modules. # Without this suffix, Python will fail with "ModuleNotFoundError: No module named 'cvcuda'" diff --git a/tests/cvcuda/python/cvcuda_test_python.in b/tests/cvcuda/python/cvcuda_test_python.in index 8b145b046..5cb621a27 100755 --- a/tests/cvcuda/python/cvcuda_test_python.in +++ b/tests/cvcuda/python/cvcuda_test_python.in @@ -1,6 +1,6 @@ #!/bin/bash -e # -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -37,15 +37,15 @@ if [ ! -f "$tests_dir/cvcuda_util.py" ]; then fi # Verify if correct package dependencies are installed -------- -pip_depends="pytest torch numpy==1.26" +pip_depends="pytest cupy numpy" # Collect all python versions that are indeed installed and have proper dependencies installed # Two behaviors: -# - default: skip Python versions that are not installed or don't have pytest and torch installed +# - default: skip Python versions that are not installed or don't have pytest, cuda-python and numpy installed # - if FORCE_PYTHON_TESTS is set: exit with error for ver in $python_versions_tentative; do - if ! python$ver -c "import pytest, torch, numpy" > /dev/null 2>&1; then + if ! python$ver -c "import pytest, cupy, numpy" > /dev/null 2>&1; then echo "WARNING: Python version $ver not installed or missing proper dependencies" echo "Please install Python version $ver and run the following commands before running tests: python$ver -m pip install $pip_depends" if [[ "$FORCE_PYTHON_TESTS" == 1 || "$FORCE_PYTHON_TESTS" == yes ]]; then @@ -72,6 +72,17 @@ trap 'on_exit' EXIT export PYTHONPATH="$PYTHONPATH:@PYTHON_MODULE_DIR@" +# The NVTX marker test needs the probe injected via NVTX_INJECTION64_PATH, which NVTX reads once at +# process start. Injecting it for the whole suite would route every operator call -- and cupy's own +# NVTX ranges -- through the probe for the entire session, growing its recorded-name buffer without +# bound (only the marker test resets it) and risking host-memory exhaustion. So run the marker test +# in its own pytest process with the probe injected, and run the rest of the suite without it. +nvtx_marker_test="$tests_dir/test_nvtx_markers.py" +nvtx_probe="@NVTX_PROBE_PATH@" + +# Never let an inherited NVTX_INJECTION64_PATH leak into the main (uninjected) suite run. +unset NVTX_INJECTION64_PATH + for ver in $python_versions; do if [[ "$FORCE_PYTHON_TESTS" != 1 && "$FORCE_PYTHON_TESTS" != yes ]]; then @@ -95,6 +106,16 @@ for ver in $python_versions; do exit 4 fi - # Run python tests - CVCUDA_VERSION="@CVCUDA_VERSION_FULL@" python$ver -m pytest -v --tb=line -o cache_dir="$tmpdir" "$@" "$tests_dir" + # Run python tests. The marker test is excluded here and run separately below with the probe + # injected, so the probe is never installed for this (much larger) uninjected pass. + CVCUDA_VERSION="@CVCUDA_VERSION_FULL@" python$ver -m pytest -v --tb=short -o cache_dir="$tmpdir" \ + --ignore="$nvtx_marker_test" "$@" "$tests_dir" + + # Run the NVTX marker test in its own process, with the probe injected only for that process. + if [ -f "$nvtx_probe" ]; then + CVCUDA_VERSION="@CVCUDA_VERSION_FULL@" NVTX_INJECTION64_PATH="$nvtx_probe" \ + python$ver -m pytest -v --tb=short -o cache_dir="$tmpdir" "$nvtx_marker_test" + else + echo "Skipping NVTX marker test for python-$ver: probe not found at $nvtx_probe" + fi done diff --git a/tests/cvcuda/python/cvcuda_tools.py b/tests/cvcuda/python/cvcuda_tools.py new file mode 100644 index 000000000..e9000ec6e --- /dev/null +++ b/tests/cvcuda/python/cvcuda_tools.py @@ -0,0 +1,1204 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. +from __future__ import annotations + +import itertools +from collections.abc import Callable +from dataclasses import dataclass, field +from functools import partial +from typing import Optional + +import pytest + +import cvcuda +import cvcuda_types as cv_types + + +def _create_tensor(shape: tuple, layout: str, dtype: cvcuda.Type) -> cvcuda.Tensor: + return cvcuda.Tensor(shape, cv_types.as_cvcuda_dtype(dtype), layout) + + +def _create_tensor_batch( + shape: tuple, layout: str, dtype: cvcuda.Type, count: int +) -> cvcuda.TensorBatch: + tensor_batch = cvcuda.TensorBatch(count) + for _ in range(count): + tensor_batch.pushback(_create_tensor(shape, layout, dtype)) + return tensor_batch + + +def _create_image(size: tuple, fmt: cvcuda.Format) -> cvcuda.Image: + return cvcuda.Image(size, fmt) + + +def _create_image_batch( + size: tuple, fmt: cvcuda.Format, count: int +) -> cvcuda.ImageBatchVarShape: + img_batch = cvcuda.ImageBatchVarShape(count) + for _ in range(count): + img_batch.pushback(_create_image(size, fmt)) + return img_batch + + +def _dtype_to_format( + dtype: cvcuda.Type | cvcuda.Format, + channels: int, +) -> cvcuda.Format: + if isinstance(dtype, cvcuda.Format): + return dtype + return cv_types.dtype_channels_to_format(dtype, channels) + + +def _create_image_from_dtype( + size: tuple, + dtype: cvcuda.Type | cvcuda.Format, + channels: int, +) -> cvcuda.Image: + fmt = _dtype_to_format(dtype, channels) + return _create_image(size, fmt) + + +def _create_image_batch_from_dtype( + size: tuple, dtype: cvcuda.Type | cvcuda.Format, channels: int, count: int +) -> cvcuda.ImageBatchVarShape: + fmt = _dtype_to_format(dtype, channels) + return _create_image_batch(size, fmt, count) + + +def _resolve_wrapper( + wrapper: str, + size: tuple[int, int] = (24, 24), + channels: int = 3, + layout: str | None = None, +) -> Callable[ + ..., + cvcuda.Tensor | cvcuda.Image | cvcuda.ImageBatchVarShape | cvcuda.TensorBatch, +]: + layout = "NHWC" if layout is None else layout + shape = cv_types.resolve_shape(layout, channels, size) + + if wrapper == "tensor": + create_func = partial(_create_tensor, shape=shape, layout=layout) + elif wrapper == "tensor_batch": + create_func = partial(_create_tensor_batch, shape=shape, layout=layout, count=2) + elif wrapper == "image": + create_func = partial(_create_image_from_dtype, size=size, channels=channels) + elif wrapper == "image_batch": + create_func = partial( + _create_image_batch_from_dtype, size=size, channels=channels, count=2 + ) + else: + raise ValueError(f"Invalid wrapper: {wrapper}") + + return create_func + + +def _runner( + op: Callable[ + ..., + cvcuda.Tensor | cvcuda.Image | cvcuda.ImageBatchVarShape | cvcuda.TensorBatch, + ], + data: cvcuda.Tensor | cvcuda.Image | cvcuda.ImageBatchVarShape | cvcuda.TensorBatch, + *, + negative: bool = False, + exceptions: list[type[Exception]] | None = None, +) -> None: + if not negative: + op(data) + cvcuda.Stream.current.sync() + else: + # Default: NVCV_ERROR is converted to RuntimeError + exc_tuple = tuple(exceptions) if exceptions else (RuntimeError,) + with pytest.raises(exc_tuple): + op(data) + cvcuda.Stream.current.sync() + + +def assert_dtypes( + op: Callable[..., cvcuda.Tensor], + dtypes: list[cvcuda.Type] | cvcuda.Type, + wrapper: str = "tensor", + size: tuple[int, int] = (24, 24), + channels: int = 3, + layout: str | None = None, + *, + negative: bool = False, + exceptions: list[type[Exception]] | None = None, +) -> None: + if wrapper not in {"tensor", "image", "image_batch", "tensor_batch"}: + raise ValueError(f"Invalid wrapper: {wrapper}") + if not isinstance(dtypes, list): + dtypes = [dtypes] + for dtype in dtypes: + create_func = _resolve_wrapper(wrapper, size, channels, layout) + data = create_func(dtype=dtype) + _runner(op, data, negative=negative, exceptions=exceptions) + + +def assert_layouts( + op: Callable[..., cvcuda.Tensor], + layouts: list[str] | str, + dtype: cvcuda.Type, + wrapper: str = "tensor", + size: tuple[int, int] = (24, 24), + channels: int = 3, + *, + negative: bool = False, + exceptions: list[type[Exception]] | None = None, +) -> None: + if wrapper not in {"tensor", "image", "image_batch", "tensor_batch"}: + raise ValueError(f"Invalid wrapper: {wrapper}") + if not isinstance(layouts, list): + layouts = [layouts] + for layout in layouts: + create_func = _resolve_wrapper(wrapper, size, channels, layout) + data = create_func(dtype=dtype) + _runner(op, data, negative=negative, exceptions=exceptions) + + +def assert_formats( + op: Callable[ + ..., + cvcuda.Tensor | cvcuda.Image | cvcuda.ImageBatchVarShape | cvcuda.TensorBatch, + ], + formats: list[cvcuda.Format], + wrapper: str = "tensor", + size: tuple[int, int] = (24, 24), + *, + negative: bool = False, + exceptions: list[type[Exception]] | None = None, +) -> None: + if wrapper not in {"tensor", "image", "image_batch", "tensor_batch"}: + raise ValueError(f"Invalid wrapper: {wrapper}") + create_func = _resolve_wrapper(wrapper, size) + for fmt in formats: + data = create_func(dtype=fmt) + _runner(op, data, negative=negative, exceptions=exceptions) + + +def _compute_extra_param_combos( + extra_params: dict[str, set] | None, +) -> tuple[list[str], list[tuple]]: + """ + Compute cross-product combinations for extra parameters. + + Args: + extra_params: Dict mapping param name to set of values. + + Returns: + Tuple of (keys list, combos list). If extra_params is None/empty, + returns ([], [()]) for a single empty combo to simplify iteration. + """ + if extra_params: + keys = list(extra_params.keys()) + values = [list(extra_params[k]) for k in keys] + return keys, list(itertools.product(*values)) + return [], [()] + + +def _make_extra_params_dict(keys: list[str], combo: tuple) -> dict: + """Convert a combo tuple to a dict of extra params.""" + return dict(zip(keys, combo)) if keys else {} + + +def _get_first_extra_params(extra_params: dict[str, set] | None) -> dict: + """Get first value from each extra_params set for negative tests.""" + return {k: next(iter(v)) for k, v in extra_params.items()} if extra_params else {} + + +# Type alias for runner info: (wrapper, op, param_factory) +# - wrapper: "tensor", "image_batch", "tensor_batch", "image" +# - op: The operator function to call +# - param_factory: Optional function (dtype, layout, channels) -> dict of kwargs +RunnerInfo = tuple[str, Callable, Optional[Callable]] + + +@dataclass +class _RunnerContext: + """ + Encapsulates runner information and extra params for test generation. + + This dataclass provides a unified interface for both format-based and + dtype/layout/channel-based test generation, eliminating code duplication. + """ + + runner_info: list[RunnerInfo] + extra_params: dict[str, set] | None = None + exclude_extra_params: list[tuple[str, str]] | None = None + extra_param_keys: list[str] = field(default_factory=list) + extra_param_combos: list[tuple] = field(default_factory=lambda: [()]) + + def __post_init__(self): + """Compute extra param combinations after initialization.""" + self.extra_param_keys, self.extra_param_combos = _compute_extra_param_combos( + self.extra_params + ) + # Build exclusion lookup: {wrapper: set of excluded param names} + self._exclusions: dict[str, set[str]] = {} + if self.exclude_extra_params: + for param_name, wrapper in self.exclude_extra_params: + if wrapper not in self._exclusions: + self._exclusions[wrapper] = set() + self._exclusions[wrapper].add(param_name) + + @property + def labels(self) -> list[str]: + """Generate runner labels for test IDs.""" + return [wrapper for wrapper, _, _ in self.runner_info] + + @property + def runner_count(self) -> int: + """Number of runners.""" + return len(self.runner_info) + + def get_wrapper(self, idx: int) -> str: + """Get wrapper type for runner at index.""" + return self.runner_info[idx][0] + + def get_op( + self, + idx: int, + dtype: cvcuda.Type | None, + layout: str | None, + channels: int | None, + extra_dict: dict | None = None, + ) -> Callable: + """ + Get the operator function for a runner, with param_factory applied if present. + + Args: + idx: Runner index. + dtype: Data type (None for format mode). + layout: Layout string (None for format mode). + channels: Channel count (None for format mode). + extra_dict: Additional parameters to pass to param_factory. + + Returns: + Callable operator function, potentially wrapped with partial. + """ + _, op, param_factory = self.runner_info[idx] + if param_factory is not None: + params = param_factory(dtype, layout, channels, **(extra_dict or {})) + return partial(op, **params) + return op + + def is_param_excluded(self, param_name: str, wrapper: str) -> bool: + """Check if a param is excluded for a given wrapper.""" + return param_name in self._exclusions.get(wrapper, set()) + + def filter_extra_dict(self, extra_dict: dict, wrapper: str) -> dict: + """Filter out excluded params for the given wrapper.""" + excluded = self._exclusions.get(wrapper, set()) + if not excluded: + return extra_dict + return {k: v for k, v in extra_dict.items() if k not in excluded} + + def make_extra_dict(self, combo: tuple, wrapper: str | None = None) -> dict: + """Convert a combo tuple to extra params dict, optionally filtering for wrapper.""" + extra_dict = _make_extra_params_dict(self.extra_param_keys, combo) + if wrapper is not None: + return self.filter_extra_dict(extra_dict, wrapper) + return extra_dict + + def get_first_extra_params(self, wrapper: str | None = None) -> dict: + """Get first value from each extra param for negative tests.""" + extra_dict = _get_first_extra_params(self.extra_params) + if wrapper is not None: + return self.filter_extra_dict(extra_dict, wrapper) + return extra_dict + + +def _matches_exclude_pattern( + dtype: cvcuda.Type, + layout: str, + channels: int, + exclude_pattern: tuple[cvcuda.Type | None, str | None, int | None], +) -> bool: + """ + Check if a (dtype, layout, channels) combo matches an exclude pattern. + None in the pattern acts as a wildcard (matches anything). + """ + pattern_dtype, pattern_layout, pattern_channels = exclude_pattern + if pattern_dtype is not None and dtype != pattern_dtype: + return False + if pattern_layout is not None and layout != pattern_layout: + return False + if pattern_channels is not None and channels != pattern_channels: + return False + return True + + +def _is_excluded( + dtype: cvcuda.Type, + layout: str, + channels: int, + exclude_dlc: list[tuple[cvcuda.Type | None, str | None, int | None]] | None, +) -> bool: + """Check if a (dtype, layout, channels) combo should be excluded.""" + if exclude_dlc is None: + return False + return any( + _matches_exclude_pattern(dtype, layout, channels, pattern) + for pattern in exclude_dlc + ) + + +def _has_format_mapping(dtype: cvcuda.Type, channels: int) -> bool: + """Check if a dtype/channel combo has a valid format mapping for image creation.""" + return (dtype, channels) in cv_types.DTYPE_CHANNELS_TO_FORMAT + + +def _make_extra_param_negative_tests( + name: str, + extra_params_negative: dict[str, set] | None, + ctx: _RunnerContext, + get_op_func: Callable[[int, dict], Callable], + get_assert_kwargs: Callable[[int], dict], + assert_func: Callable, + negative_exceptions: list[type[Exception]] | None = None, +) -> dict[str, Callable]: + """ + Generate negative tests for extra params - shared by both format and dtype modes. + + Tests are parametrized over all runners and all unsupported values. + + Args: + name: Operator name for test function naming. + extra_params_negative: Dict mapping param name to unsupported values. + ctx: Runner context with extra params info. + get_op_func: Function (runner_idx, extra_dict) -> operator callable. + get_assert_kwargs: Function (runner_idx) -> kwargs dict for assert_func. + assert_func: Assertion function to use (assert_formats or assert_layouts). + + Returns: + Dict of test function names to pytest-parametrized test functions. + """ + if not extra_params_negative: + return {} + + result = {} + for param_name, unsupported_values in extra_params_negative.items(): + # Cross-product of runners and bad values, excluding param/wrapper combos + test_params = [ + (runner_idx, bad_value) + for runner_idx in range(ctx.runner_count) + for bad_value in unsupported_values + if not ctx.is_param_excluded(param_name, ctx.get_wrapper(runner_idx)) + ] + + # Skip if no valid test params after filtering + if not test_params: + continue + + def _test_id(params, _param_name=param_name): + runner_idx, bad_value = params + return f"{ctx.labels[runner_idx]}-{_param_name}={bad_value}" + + @pytest.mark.parametrize( + "runner_idx,bad_value", + test_params, + ids=[_test_id(p) for p in test_params], + ) + def test_extra_param_negative( + runner_idx, + bad_value, + _param_name=param_name, + _get_op=get_op_func, + _get_assert_kwargs=get_assert_kwargs, + _assert_func=assert_func, + _ctx=ctx, + _negative_exceptions=negative_exceptions, + ): + wrapper = _ctx.get_wrapper(runner_idx) + extra_dict = _ctx.get_first_extra_params(wrapper) + extra_dict[_param_name] = bad_value + op = _get_op(runner_idx, extra_dict) + _assert_func( + op, + **_get_assert_kwargs(runner_idx), + negative=True, + exceptions=_negative_exceptions, + ) + + result[f"test_op_{name}_{param_name}_negative"] = test_extra_param_negative + + return result + + +def _format_test_id( + runner_labels: list[str], runner_idx: int, fmt: cvcuda.Format, extra_combo: tuple +) -> str: + """Generate test ID for format-based tests.""" + base_id = f"{runner_labels[runner_idx]}-{fmt.name}" + if extra_combo: + extra_str = "-".join(str(v) for v in extra_combo) + return f"{base_id}-{extra_str}" + return base_id + + +def _dtype_test_id( + runner_labels: list[str], + runner_idx: int, + dtype: cvcuda.Type, + layout: str, + channels: int, + extra_combo: tuple, +) -> str: + """Generate test ID for dtype/layout/channel-based tests.""" + base_id = f"{runner_labels[runner_idx]}-{dtype.name}-{layout}-{channels}ch" + if extra_combo: + extra_str = "-".join(str(v) for v in extra_combo) + return f"{base_id}-{extra_str}" + return base_id + + +def _build_negative_test_params( + ctx: _RunnerContext, + negative_values: list, + skip_image_wrappers: bool = False, + format_check_func: Callable[[object], bool] | None = None, +) -> list[tuple[int, object]]: + """ + Build negative test parameters for a given set of unsupported values. + + Args: + ctx: Runner context containing runner information. + negative_values: List of unsupported values to test. + skip_image_wrappers: If True, skip image/image_batch wrappers entirely. + format_check_func: Optional function to check if a value has a valid + format mapping. If provided, image wrappers are only included + when this returns True for the value. + + Returns: + List of (runner_idx, value) tuples for parametrization. + """ + params = [] + for runner_idx in range(ctx.runner_count): + wrapper = ctx.get_wrapper(runner_idx) + is_image_wrapper = wrapper in ("image_batch", "image") + + if skip_image_wrappers and is_image_wrapper: + continue + + for value in negative_values: + if is_image_wrapper and format_check_func is not None: + if not format_check_func(value): + continue + params.append((runner_idx, value)) + + return params + + +def _make_negative_test( + params: list[tuple[int, object]], + id_func: Callable[[int, object], str], + test_body: Callable[[int, object], None], +) -> Callable | None: + """ + Create a parametrized negative test function. + + Args: + params: List of (runner_idx, value) tuples. + id_func: Function (runner_idx, value) -> test ID string. + test_body: Function (runner_idx, value) -> None that runs the test. + + Returns: + Parametrized test function, or None if params is empty. + """ + if not params: + return None + + @pytest.mark.parametrize( + "runner_idx,value", + params, + ids=[id_func(idx, val) for idx, val in params], + ) + def test_negative(runner_idx, value, _body=test_body): + _body(runner_idx, value) + + return test_negative + + +def _collect_test_results( + name: str, + primary_test: Callable, + primary_suffix: str, + negative_tests: list[tuple[str, Callable | None]], + extra_neg_tests: dict[str, Callable], +) -> dict[str, Callable]: + """ + Collect all test functions into a result dictionary. + + Args: + name: Operator name. + primary_test: The main positive test function. + primary_suffix: Suffix for primary test (e.g., "input" or "format_input"). + negative_tests: List of (suffix, test_func_or_none) tuples. + extra_neg_tests: Dict of extra param negative tests. + + Returns: + Dict mapping test names to test functions. + """ + result = {f"test_op_{name}_{primary_suffix}": primary_test} + for suffix, test_func in negative_tests: + if test_func is not None: + result[f"test_op_{name}_{suffix}"] = test_func + result.update(extra_neg_tests) + return result + + +def make_op_tests( + name: str, + runner_info: list[RunnerInfo], + # Format Mode - tests image formats + supported_formats: set[cvcuda.Format] | None = None, + # DType Mode - tests dtype/layout/channel combinations + keystone_dlc: tuple[cvcuda.Type, str, int] | None = None, + supported_dtypes: set[cvcuda.Type] | None = None, + supported_layouts: set[str] | None = None, + supported_channels: set[int] | None = None, + # Exclusions + Extra params to finetune test matrix + exclude_dlc: list[tuple[cvcuda.Type | None, str | None, int | None]] | None = None, + extra_params: dict[str, set] | None = None, + extra_params_negative: dict[str, set] | None = None, + exclude_extra_params: list[tuple[str, str]] | None = None, + negative_exceptions: list[type[Exception]] | None = None, +) -> dict[str, Callable]: + """ + Generate pytest test functions for CV-CUDA operator input validation. + + Overview + -------- + Creates parameterized tests that verify operators handle supported and + unsupported inputs correctly. Tests are organized in two modes: + + - **Format Mode**: Test image formats directly. Requires image/image_batch + wrappers. Use when operator behavior depends on cvcuda.Format values. + - **DType Mode**: Test dtype/layout/channel combinations. Works with any + wrapper type. Use for most operators that accept various data types. + + These generated tests validate the Python-declared input contract against the + runtime guards in the operator implementation. They do not machine-check the + public header documentation, and they are not a substitute for output-value + correctness tests. + + Image-backed coverage is intentionally narrower than tensor-backed coverage: + layout negatives are only meaningful for tensor wrappers, and image dtype + negatives are limited to dtype/channel combinations that can be expressed as + valid image formats. + + Both modes generate: + - Positive tests: Verify operator works with supported inputs + - Negative tests: Verify operator raises RuntimeError, or configured exceptions, + for unsupported inputs + + Summary: + - Supported dtypes/formats (positive tests) + - Unsupported dtypes/formats (negative tests - expect RuntimeError or configured exceptions) + - Layout validation (tensor wrappers only) + - Channel count validation + - Extra parameter validation + + Call Flow + --------- + :: + + make_op_tests(name, runner_info, ...) + | + +-- [Format Mode] _make_format_tests() + | | + | +-- Positive test: for each (runner, format, extra_combo): + | | 1. _resolve_wrapper(wrapper) -> create_func + | | 2. create_func(dtype=format) -> data container + | | 3. param_factory(None, None, None, **extra) -> op_kwargs + | | 4. op(data, **op_kwargs) -> verify no exception + | | + | +-- Negative test: for each unsupported format: + | pytest.raises(RuntimeError or configured exceptions) + | during op(data) + | + +-- [DType Mode] _make_dtype_layout_channel_tests() + | + +-- Positive test: for each (runner, dtype, layout, channels, extra): + | 1. cv_types.resolve_shape(layout, channels, size) -> shape + | 2. _resolve_wrapper(wrapper, layout=layout) -> create_func + | 3. create_func(dtype=dtype) -> data container + | 4. param_factory(dtype, layout, channels, **extra) -> op_kwargs + | 5. op(data, **op_kwargs) -> verify no exception + | + +-- Negative tests for dtype/layout/channels: + Uses keystone_dlc as "known good" baseline, + varies one dimension at a time to test unsupported values + + Default Values + -------------- + These defaults are defined in this module and cvcuda_types: + + - **Image size**: (24, 24) width x height - from ``_resolve_wrapper()`` + - **Layout**: "NHWC" when not specified - from ``_resolve_wrapper()`` + - **Batch count**: 2 for tensor_batch and image_batch - from ``_resolve_wrapper()`` + - **Shape resolution**: Uses ``cvcuda_types.LAYOUT_TO_SHAPE`` mapping + - **Valid layouts**: ``cvcuda_types.IMAGE_LAYOUTS`` = {"NHWC", "HWC", "NCHW", ...} + - **Valid channels**: ``cvcuda_types.CHANNELS`` = {1, 2, 3, 4, 5, 6} + - **Scalar dtypes**: ``cvcuda_types.SCALAR_TYPES_SET`` = {U8, U16, S8, S16, ...} + - **Scalar formats**: ``cvcuda_types.SCALAR_FORMATS_SET`` = {U8, U16, F32, ...} + + Container Types (Wrappers) + -------------------------- + - ``"tensor"``: cvcuda.Tensor - single tensor + - ``"tensor_batch"``: cvcuda.TensorBatch - batch of 2 tensors + - ``"image"``: cvcuda.Image - single image + - ``"image_batch"``: cvcuda.ImageBatchVarShape - batch of 2 images + + Generated Test Names + -------------------- + Tests are named following the pattern ``test_op_{name}_{test_type}``: + + - ``test_op_{name}_input`` - positive dtype/layout/channels (DType Mode) + - ``test_op_{name}_format_input`` - positive formats (Format Mode) + - ``test_op_{name}_dtype_negative`` - unsupported dtypes + - ``test_op_{name}_layout_negative`` - unsupported layouts + - ``test_op_{name}_channels_negative`` - unsupported channels + - ``test_op_{name}_format_negative`` - unsupported formats + - ``test_op_{name}_{param}_negative`` - unsupported extra param values + + Test IDs follow patterns like: + - ``tensor-U8-NHWC-3ch`` (DType Mode) + - ``image_batch-RGB8`` (Format Mode) + - ``tensor-U8-NHWC-3ch-LINEAR`` (with extra_params) + + Parameters + ---------- + name : str + Operator name used in generated test function names. + + runner_info : list[RunnerInfo] + List of (wrapper, op, param_factory) tuples defining test runners: + + - **wrapper**: One of "tensor", "tensor_batch", "image", "image_batch" + - **op**: The cvcuda operator function (e.g., ``cvcuda.resize``) + - **param_factory**: Optional callable with signature:: + + def param_factory( + dtype: cvcuda.Type | None, + layout: str | None, + channels: int | None, + **extra_params + ) -> dict: + '''Return kwargs to pass to op alongside source data.''' + + For Format Mode, dtype/layout/channels are all None. + + supported_formats : set[cvcuda.Format], optional + Set of cvcuda.Format values the operator accepts (Format Mode). + Requires at least one "image" or "image_batch" runner. + Negative tests use ``SCALAR_FORMATS_SET - supported_formats``. + + keystone_dlc : tuple[cvcuda.Type, str, int], optional + **REQUIRED for DType Mode.** The "keystone" is a known-good baseline + configuration (dtype, layout, channels) used when generating negative + tests. When testing an unsupported dtype, the keystone's layout and + channels are used to ensure only one dimension varies at a time. + + Example: ``(cvcuda.Type.U8, "NHWC", 3)`` means U8/NHWC/3-channel is + the baseline for negative tests. + + supported_dtypes : set[cvcuda.Type], optional + Set of cvcuda.Type values the operator accepts (DType Mode). + Negative tests use ``SCALAR_TYPES_SET - supported_dtypes``. + + supported_layouts : set[str], optional + Set of layout strings the operator accepts (e.g., {"NHWC", "HWC"}). + Must be subset of ``IMAGE_LAYOUTS`` from cvcuda_types. + Negative tests use ``IMAGE_LAYOUTS - supported_layouts``. + + supported_channels : set[int], optional + Set of channel counts the operator accepts (e.g., {1, 3, 4}). + Negative tests use ``CHANNELS - supported_channels``. + + exclude_dlc : list[tuple], optional + List of (dtype, layout, channels) patterns to exclude from positive + tests. None acts as wildcard in any position. + + Example: ``[(cvcuda.Type.F32, None, 2)]`` excludes all F32 2-channel + combinations regardless of layout. + + extra_params : dict[str, set], optional + Additional parameters to cross-product with test cases. Each key is + a parameter name, and the value is a set of values to test. + + Example: ``{"interp": {cvcuda.Interp.LINEAR, cvcuda.Interp.CUBIC}}`` + generates tests for both interpolation methods. + + extra_params_negative : dict[str, set], optional + Unsupported values for extra parameters. Generates negative tests + using keystone as baseline, varying only the extra param. + + exclude_extra_params : list[tuple[str, str]], optional + List of (param_name, wrapper) tuples specifying which extra params + to exclude from specific wrappers. + + Example: ``[("mask_channels", "image_batch")]`` excludes mask_channels + from being passed to image_batch runners (useful when a parameter + only applies to certain wrapper types). + + negative_exceptions : list[type[Exception]], optional + Exception types accepted by negative tests. When not provided, negative + tests expect RuntimeError. When provided, any listed exception type is + accepted as a valid negative outcome. + + Returns + ------- + dict[str, Callable] + Dict mapping test function names to pytest-parametrized functions. + Use ``globals().update(make_op_tests(...))`` to register tests. + + Examples + -------- + Basic DType Mode usage:: + + def _resize_params(dtype, layout, channels): + return { + "shape": cv_types.resolve_shape(layout, channels, (10, 20)), + "interp": cvcuda.Interp.LINEAR, + } + + globals().update( + cv_tools.make_op_tests( + name="resize", + runner_info=[ + ("tensor", cvcuda.resize, _resize_params), + ("image_batch", cvcuda.resize, _resize_varshape_params), + ], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 3), + supported_dtypes={cvcuda.Type.U8, cvcuda.Type.F32}, + supported_layouts={"NHWC", "HWC"}, + supported_channels={1, 3, 4}, + ) + ) + + Format Mode usage:: + + globals().update( + cv_tools.make_op_tests( + name="channelreorder", + runner_info=[("image_batch", _channelreorder, None)], + supported_formats={ + cvcuda.Format.U8, + cvcuda.Format.RGB8, + cvcuda.Format.RGBA8, + }, + ) + ) + + With extra parameters and exclusions:: + + globals().update( + cv_tools.make_op_tests( + name="composite", + runner_info=[ + ("tensor", _composite, _composite_params), + ("image_batch", _composite_varshape, _composite_params), + ], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 3), + supported_dtypes={cvcuda.Type.U8}, + supported_layouts={"NHWC", "HWC"}, + supported_channels={3}, + extra_params={"out_channels": {3, 4}}, + extra_params_negative={ + "mask_channels": {2, 3, 4}, + "out_channels": {0, 2, 5}, + }, + exclude_extra_params=[ + ("mask_channels", "image_batch"), + ], + negative_exceptions=[RuntimeError, ValueError], + ) + ) + """ + format_mode = supported_formats is not None + dtype_mode = ( + supported_dtypes is not None + or supported_layouts is not None + or supported_channels is not None + ) + + if not format_mode and not dtype_mode: + raise ValueError( + "Must specify either supported_formats or " + "one of (supported_dtypes, supported_layouts, supported_channels)" + ) + + if dtype_mode and keystone_dlc is None: + raise ValueError( + "keystone_dlc is required when using dtype mode " + "(supported_dtypes, supported_layouts, or supported_channels)" + ) + + # Pre-filter runners by type + format_runners = [r for r in runner_info if r[0] in ("image", "image_batch")] + + result = {} + + # format-based tests (requires image or image_batch wrapper) + if format_mode: + if not format_runners: + raise ValueError( + "supported_formats requires at least one runner with 'image' or 'image_batch' wrapper" + ) + result.update( + _make_format_tests( + name, + format_runners, + supported_formats, + extra_params, + extra_params_negative, + exclude_extra_params, + negative_exceptions, + ) + ) + + # dtype/layout/channel-based tests (runs on any container wrappers) + if dtype_mode: + result.update( + _make_dtype_layout_channel_tests( + name, + runner_info, + supported_dtypes, + supported_layouts, + supported_channels, + keystone_dlc, + exclude_dlc, + extra_params, + extra_params_negative, + exclude_extra_params, + negative_exceptions, + ) + ) + + return result + + +def _make_format_tests( + name: str, + runner_info: list[RunnerInfo], + supported_formats: set[cvcuda.Format], + extra_params: dict[str, set] | None = None, + extra_params_negative: dict[str, set] | None = None, + exclude_extra_params: list[tuple[str, str]] | None = None, + negative_exceptions: list[type[Exception]] | None = None, +) -> dict[str, Callable]: + """Generate format-based test functions. + + Creates a positive test parametrized over every (runner, format, extra_combo) + combination, a negative test that expects RuntimeError for each unsupported + format (SCALAR_FORMATS_SET minus supported_formats), and per-extra-param + negative tests when extra_params_negative is provided. + + Returns a dict of ``test_op_{name}_*`` names to pytest-parametrized callables. + """ + ctx = _RunnerContext(runner_info, extra_params, exclude_extra_params) + unsupported_formats = list(cv_types.SCALAR_FORMATS_SET - supported_formats) + + # --- Positive test --- + format_params = list( + itertools.product( + range(ctx.runner_count), supported_formats, ctx.extra_param_combos + ) + ) + + def _pos_id(params): + runner_idx, fmt, extra_combo = params + return _format_test_id(ctx.labels, runner_idx, fmt, extra_combo) + + @pytest.mark.parametrize( + "runner_idx,img_format,extra_combo", + format_params, + ids=[_pos_id(p) for p in format_params], + ) + def test_format_input(runner_idx, img_format, extra_combo): + wrapper = ctx.get_wrapper(runner_idx) + extra_dict = ctx.make_extra_dict(extra_combo, wrapper) + op = ctx.get_op(runner_idx, None, None, None, extra_dict) + assert_formats(op, [img_format], wrapper=wrapper) + + # --- Negative format test --- + format_neg_params = _build_negative_test_params(ctx, unsupported_formats) + + def _format_neg_body(runner_idx, img_format): + wrapper = ctx.get_wrapper(runner_idx) + extra_dict = ctx.get_first_extra_params(wrapper) + op = ctx.get_op(runner_idx, None, None, None, extra_dict) + assert_formats( + op, + [img_format], + wrapper=wrapper, + negative=True, + exceptions=negative_exceptions, + ) + + test_format_negative = _make_negative_test( + format_neg_params, + id_func=lambda idx, fmt: f"{ctx.labels[idx]}-{fmt.name}", + test_body=_format_neg_body, + ) + + # --- Negative extra_params tests --- + extra_neg_tests: dict[str, Callable] = {} + if supported_formats: + keystone_format = [next(iter(supported_formats))] + + def _get_op_for_extra_neg(runner_idx, extra_dict): + return ctx.get_op(runner_idx, None, None, None, extra_dict) + + def _get_assert_kwargs(runner_idx): + return {"formats": keystone_format, "wrapper": ctx.get_wrapper(runner_idx)} + + extra_neg_tests = _make_extra_param_negative_tests( + name, + extra_params_negative, + ctx, + _get_op_for_extra_neg, + _get_assert_kwargs, + assert_formats, + negative_exceptions, + ) + + # --- Build result --- + return _collect_test_results( + name, + test_format_input, + "format_input", + [("format_negative", test_format_negative)], + extra_neg_tests, + ) + + +def _make_dtype_layout_channel_tests( + name: str, + runner_info: list[RunnerInfo], + supported_dtypes: set[cvcuda.Type] | None, + supported_layouts: set[str] | None, + supported_channels: set[int] | None, + keystone_dlc: tuple[cvcuda.Type, str, int], + exclude_dlc: list[tuple[cvcuda.Type | None, str | None, int | None]] | None = None, + extra_params: dict[str, set] | None = None, + extra_params_negative: dict[str, set] | None = None, + exclude_extra_params: list[tuple[str, str]] | None = None, + negative_exceptions: list[type[Exception]] | None = None, +) -> dict[str, Callable]: + """Generate dtype/layout/channel-based test functions. + + Creates a positive test parametrized over every valid (runner, dtype, layout, + channels, extra_combo) combination (after applying exclude_dlc filters), + plus three negative tests that each vary one dimension against the keystone + baseline: unsupported dtypes, unsupported layouts (tensor wrappers only), + and unsupported channel counts. Per-extra-param negative tests are added + when extra_params_negative is provided. + + A ``None`` value for any supported set means "not specified": positive tests + fall back to the keystone value for that dimension and negative tests are + skipped. + + Returns a dict of ``test_op_{name}_*`` names to pytest-parametrized callables. + """ + ctx = _RunnerContext(runner_info, extra_params, exclude_extra_params) + keystone_dtype, keystone_layout, keystone_channels = keystone_dlc + + # Default None to keystone value for testing, empty for negatives + dtypes_to_test = ( + supported_dtypes if supported_dtypes is not None else {keystone_dtype} + ) + layouts_to_test = ( + supported_layouts if supported_layouts is not None else {keystone_layout} + ) + channels_to_test = ( + supported_channels if supported_channels is not None else {keystone_channels} + ) + + unsupported_dtypes = ( + list(cv_types.SCALAR_TYPES_SET - dtypes_to_test) + if supported_dtypes is not None + else [] + ) + unsupported_layouts = ( + list(cv_types.IMAGE_LAYOUTS - layouts_to_test) + if supported_layouts is not None + else [] + ) + unsupported_channels = ( + list(cv_types.CHANNELS - channels_to_test) + if supported_channels is not None + else [] + ) + + # --- Positive test --- + input_params = [] + for runner_idx, dtype, layout, channels, extra_combo in itertools.product( + range(ctx.runner_count), + dtypes_to_test, + layouts_to_test, + channels_to_test, + ctx.extra_param_combos, + ): + wrapper = ctx.get_wrapper(runner_idx) + # Skip image wrappers for dtype/channel combos without format mappings + if wrapper in ("image_batch", "image") and not _has_format_mapping( + dtype, channels + ): + continue + # Skip excluded combinations + if _is_excluded(dtype, layout, channels, exclude_dlc): + continue + input_params.append((runner_idx, dtype, layout, channels, extra_combo)) + + def _input_id(params): + runner_idx, dtype, layout, channels, extra_combo = params + return _dtype_test_id( + ctx.labels, runner_idx, dtype, layout, channels, extra_combo + ) + + @pytest.mark.parametrize( + "runner_idx,dtype,layout,channels,extra_combo", + input_params, + ids=[_input_id(p) for p in input_params], + ) + def test_input(runner_idx, dtype, layout, channels, extra_combo): + wrapper = ctx.get_wrapper(runner_idx) + extra_dict = ctx.make_extra_dict(extra_combo, wrapper) + op = ctx.get_op(runner_idx, dtype, layout, channels, extra_dict) + assert_layouts(op, layout, dtype=dtype, wrapper=wrapper, channels=channels) + + # --- Negative dtype test --- + dtype_neg_params = _build_negative_test_params( + ctx, + unsupported_dtypes, + format_check_func=lambda dtype: _has_format_mapping(dtype, keystone_channels), + ) + + def _dtype_neg_body(runner_idx, dtype): + wrapper = ctx.get_wrapper(runner_idx) + extra_dict = ctx.get_first_extra_params(wrapper) + op = ctx.get_op( + runner_idx, dtype, keystone_layout, keystone_channels, extra_dict + ) + assert_dtypes( + op, + dtype, + wrapper=wrapper, + channels=keystone_channels, + layout=keystone_layout, + negative=True, + exceptions=negative_exceptions, + ) + + test_dtype_negative = _make_negative_test( + dtype_neg_params, + id_func=lambda idx, d: f"{ctx.labels[idx]}-{d.name}", + test_body=_dtype_neg_body, + ) + + # --- Negative layout test (skip image wrappers - no layout validation) --- + layout_neg_params = _build_negative_test_params( + ctx, unsupported_layouts, skip_image_wrappers=True + ) + + def _layout_neg_body(runner_idx, layout): + wrapper = ctx.get_wrapper(runner_idx) + extra_dict = ctx.get_first_extra_params(wrapper) + op = ctx.get_op( + runner_idx, keystone_dtype, layout, keystone_channels, extra_dict + ) + assert_layouts( + op, + layout, + dtype=keystone_dtype, + wrapper=wrapper, + channels=keystone_channels, + negative=True, + exceptions=negative_exceptions, + ) + + test_layout_negative = _make_negative_test( + layout_neg_params, + id_func=lambda idx, layout: f"{ctx.labels[idx]}-{layout}", + test_body=_layout_neg_body, + ) + + # --- Negative channels test --- + channels_neg_params = _build_negative_test_params( + ctx, + unsupported_channels, + format_check_func=lambda ch: _has_format_mapping(keystone_dtype, ch), + ) + + def _channels_neg_body(runner_idx, channels): + wrapper = ctx.get_wrapper(runner_idx) + extra_dict = ctx.get_first_extra_params(wrapper) + op = ctx.get_op( + runner_idx, keystone_dtype, keystone_layout, channels, extra_dict + ) + assert_layouts( + op, + keystone_layout, + dtype=keystone_dtype, + wrapper=wrapper, + channels=channels, + negative=True, + exceptions=negative_exceptions, + ) + + test_channels_negative = _make_negative_test( + channels_neg_params, + id_func=lambda idx, ch: f"{ctx.labels[idx]}-{ch}ch", + test_body=_channels_neg_body, + ) + + # --- Negative extra_params tests --- + def _get_op_for_extra_neg(runner_idx, extra_dict): + return ctx.get_op( + runner_idx, keystone_dtype, keystone_layout, keystone_channels, extra_dict + ) + + def _get_assert_kwargs(runner_idx): + return { + "layouts": keystone_layout, + "dtype": keystone_dtype, + "wrapper": ctx.get_wrapper(runner_idx), + "channels": keystone_channels, + } + + extra_neg_tests = _make_extra_param_negative_tests( + name, + extra_params_negative, + ctx, + _get_op_for_extra_neg, + _get_assert_kwargs, + assert_layouts, + negative_exceptions, + ) + + # --- Build result --- + return _collect_test_results( + name, + test_input, + "input", + [ + ("dtype_negative", test_dtype_negative), + ("layout_negative", test_layout_negative), + ("channels_negative", test_channels_negative), + ], + extra_neg_tests, + ) diff --git a/tests/cvcuda/python/cvcuda_types.py b/tests/cvcuda/python/cvcuda_types.py new file mode 100644 index 000000000..0c3f57b1a --- /dev/null +++ b/tests/cvcuda/python/cvcuda_types.py @@ -0,0 +1,659 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. +from __future__ import annotations + +from typing import TYPE_CHECKING + +import cvcuda +import numpy as np + +try: + import torch + + _HAS_TORCH = True +except ImportError: + _HAS_TORCH = False + +if TYPE_CHECKING: + from collections.abc import Callable + + +# CV-CUDA <-> CV-CUDA +# =================== +# maps cvcuda.Format to a cvcuda.Type +FORMAT_TO_TYPE: dict[cvcuda.Format, cvcuda.Type] = { + # BGR + cvcuda.Format.BGR8: cvcuda.Type.U8, + cvcuda.Format.BGR8p: cvcuda.Type.U8, + cvcuda.Format.BGRA8: cvcuda.Type.U8, + cvcuda.Format.BGRA8p: cvcuda.Type.U8, + cvcuda.Format.BGRf16: cvcuda.Type.F16, + cvcuda.Format.BGRf16p: cvcuda.Type.F16, + cvcuda.Format.BGRAf16: cvcuda.Type.F16, + cvcuda.Format.BGRAf16p: cvcuda.Type.F16, + cvcuda.Format.BGRf32: cvcuda.Type.F32, + cvcuda.Format.BGRf32p: cvcuda.Type.F32, + cvcuda.Format.BGRAf32: cvcuda.Type.F32, + cvcuda.Format.BGRAf32p: cvcuda.Type.F32, + # RGB + cvcuda.Format.RGB8: cvcuda.Type.U8, + cvcuda.Format.RGB8p: cvcuda.Type.U8, + cvcuda.Format.RGBA8: cvcuda.Type.U8, + cvcuda.Format.RGBA8p: cvcuda.Type.U8, + cvcuda.Format.RGBf16: cvcuda.Type.F16, + cvcuda.Format.RGBf16p: cvcuda.Type.F16, + cvcuda.Format.RGBAf16: cvcuda.Type.F16, + cvcuda.Format.RGBAf16p: cvcuda.Type.F16, + cvcuda.Format.RGBf32: cvcuda.Type.F32, + cvcuda.Format.RGBf32p: cvcuda.Type.F32, + cvcuda.Format.RGBAf32: cvcuda.Type.F32, + cvcuda.Format.RGBAf32p: cvcuda.Type.F32, + cvcuda.Format.RGB8_1U_U8: cvcuda.Type.U8, + cvcuda.Format.RGB8_3D_F32: cvcuda.Type.F32, + cvcuda.Format.RGB8_7U_U8: cvcuda.Type.U8, + cvcuda.Format.RGBA8_3POS3D_U32: cvcuda.Type.U32, + cvcuda.Format.RGBA8_3U_U16: cvcuda.Type.U16, + cvcuda.Format.RGBA8_UNASSOCIATED_ALPHA: cvcuda.Type.U8, + # CMYK + cvcuda.Format.CMYK8: cvcuda.Type.U8, + # HSV + cvcuda.Format.HSV8: cvcuda.Type.U8, + # NV12 + cvcuda.Format.NV12: cvcuda.Type.U8, + cvcuda.Format.NV12_ER: cvcuda.Type.U8, + # cvcuda.Format.NV12_BL: cvcuda.Type.U8, + # cvcuda.Format.NV12_ER_BL: cvcuda.Type.U8, + # NV24 + cvcuda.Format.NV24: cvcuda.Type.U8, + cvcuda.Format.NV24_ER: cvcuda.Type.U8, + # cvcuda.Format.NV24_BL: cvcuda.Type.U8, + # cvcuda.Format.NV24_ER_BL: cvcuda.Type.U8, + # UYVY + cvcuda.Format.UYVY: cvcuda.Type.U8, + cvcuda.Format.UYVY_ER: cvcuda.Type.U8, + # cvcuda.Format.UYVY_BL: cvcuda.Type.U8, + # cvcuda.Format.UYVY_ER_BL: cvcuda.Type.U8, + # VYUY + cvcuda.Format.VYUY: cvcuda.Type.U8, + cvcuda.Format.VYUY_ER: cvcuda.Type.U8, + # cvcuda.Format.VYUY_BL: cvcuda.Type.U8, + # cvcuda.Format.VYUY_ER_BL: cvcuda.Type.U8, + # Y16 + cvcuda.Format.Y16: cvcuda.Type.U16, + cvcuda.Format.Y16_ER: cvcuda.Type.U16, + # cvcuda.Format.Y16_BL: cvcuda.Type.U16, + # cvcuda.Format.Y16_ER_BL: cvcuda.Type.U16, + # Y8 + cvcuda.Format.Y8: cvcuda.Type.U8, + cvcuda.Format.Y8_ER: cvcuda.Type.U8, + # cvcuda.Format.Y8_BL: cvcuda.Type.U8, + # cvcuda.Format.Y8_ER_BL: cvcuda.Type.U8, + # YCCK + cvcuda.Format.YCCK8: cvcuda.Type.U8, + # YUV + cvcuda.Format.YUV8p: cvcuda.Type.U8, + cvcuda.Format.YUV8p_ER: cvcuda.Type.U8, + # YUYV + cvcuda.Format.YUYV: cvcuda.Type.U8, + cvcuda.Format.YUYV_ER: cvcuda.Type.U8, + # cvcuda.Format.YUYV_BL: cvcuda.Type.U8, + # cvcuda.Format.YUYV_ER_BL: cvcuda.Type.U8, + # Scalar + cvcuda.Format.U8: cvcuda.Type.U8, + # cvcuda.Format.U8_BL: cvcuda.Type.U8, + cvcuda.Format.U16: cvcuda.Type.U16, + cvcuda.Format.U32: cvcuda.Type.U32, + cvcuda.Format.S8: cvcuda.Type.S8, + cvcuda.Format.S16: cvcuda.Type.S16, + # cvcuda.Format.S16_BL: cvcuda.Type.S16, + cvcuda.Format.S32: cvcuda.Type.S32, + cvcuda.Format.F16: cvcuda.Type.F16, + cvcuda.Format.F32: cvcuda.Type.F32, + cvcuda.Format.F64: cvcuda.Type.F64, + cvcuda.Format.C64: cvcuda.Type.C64, + cvcuda.Format.C128: cvcuda.Type.C128, + # Scalar multi-plane + cvcuda.Format._2F16: cvcuda.Type.F16, + cvcuda.Format._2F32: cvcuda.Type.F32, + cvcuda.Format._2S16: cvcuda.Type.S16, + # cvcuda.Format._2S16_BL: cvcuda.Type.S16, + cvcuda.Format._2C64: cvcuda.Type.C64, + cvcuda.Format._2C128: cvcuda.Type.C128, +} +# all cvcuda.Format values +FORMATS: list[cvcuda.Format] = list(FORMAT_TO_TYPE.keys()) +FORMAT_SET: set[cvcuda.Format] = set(FORMATS) +SCALAR_FORMATS: list[cvcuda.Format] = [ + cvcuda.Format.U8, + cvcuda.Format.U16, + cvcuda.Format.U32, + cvcuda.Format.S8, + cvcuda.Format.S16, + cvcuda.Format.S32, + cvcuda.Format.F16, + cvcuda.Format.F32, + cvcuda.Format.F64, + cvcuda.Format.C64, + cvcuda.Format.C128, +] +SCALAR_FORMATS_SET: set[cvcuda.Format] = set(SCALAR_FORMATS) +SCALAR_TYPES: list[cvcuda.Type] = [FORMAT_TO_TYPE[f] for f in SCALAR_FORMATS] +TYPE_TO_FORMAT: dict[cvcuda.Type, cvcuda.Format] = { + FORMAT_TO_TYPE[f]: f for f in SCALAR_FORMATS +} +SCALAR_TYPES_SET: set[cvcuda.Type] = set(SCALAR_TYPES) + +# Packed types (multi-channel values packed into single element) +PACKED_TYPES: set[cvcuda.Type] = { + # 2-channel packed + cvcuda.Type._2U8, + cvcuda.Type._2S8, + cvcuda.Type._2U16, + cvcuda.Type._2S16, + cvcuda.Type._2U32, + cvcuda.Type._2S32, + cvcuda.Type._2U64, + cvcuda.Type._2S64, + cvcuda.Type._2F16, + cvcuda.Type._2F32, + cvcuda.Type._2F64, + cvcuda.Type._2C64, + cvcuda.Type._2C128, + # 3-channel packed + cvcuda.Type._3U8, + cvcuda.Type._3S8, + cvcuda.Type._3U16, + cvcuda.Type._3S16, + cvcuda.Type._3U32, + cvcuda.Type._3S32, + cvcuda.Type._3U64, + cvcuda.Type._3S64, + cvcuda.Type._3F16, + cvcuda.Type._3F32, + cvcuda.Type._3F64, + cvcuda.Type._3C64, + # 4-channel packed + cvcuda.Type._4U8, + cvcuda.Type._4S8, + cvcuda.Type._4U16, + cvcuda.Type._4S16, + cvcuda.Type._4U32, + cvcuda.Type._4S32, + cvcuda.Type._4U64, + cvcuda.Type._4S64, + cvcuda.Type._4F16, + cvcuda.Type._4F32, + cvcuda.Type._4F64, + cvcuda.Type._4C64, +} + +# Packed formats (only a limited set exist in cvcuda.Format) +PACKED_FORMATS: set[cvcuda.Format] = { + cvcuda.Format._2F16, + cvcuda.Format._2F32, + cvcuda.Format._2S16, + cvcuda.Format._2C64, + cvcuda.Format._2C128, +} + +# Mapping from (scalar_type, channels) to packed type +SCALAR_TO_PACKED_TYPE: dict[tuple[cvcuda.Type, int], cvcuda.Type] = { + # 1 channel (identity) + (cvcuda.Type.U8, 1): cvcuda.Type.U8, + (cvcuda.Type.S8, 1): cvcuda.Type.S8, + (cvcuda.Type.U16, 1): cvcuda.Type.U16, + (cvcuda.Type.S16, 1): cvcuda.Type.S16, + (cvcuda.Type.U32, 1): cvcuda.Type.U32, + (cvcuda.Type.S32, 1): cvcuda.Type.S32, + (cvcuda.Type.U64, 1): cvcuda.Type.U64, + (cvcuda.Type.S64, 1): cvcuda.Type.S64, + (cvcuda.Type.F16, 1): cvcuda.Type.F16, + (cvcuda.Type.F32, 1): cvcuda.Type.F32, + (cvcuda.Type.F64, 1): cvcuda.Type.F64, + (cvcuda.Type.C64, 1): cvcuda.Type.C64, + (cvcuda.Type.C128, 1): cvcuda.Type.C128, + # 2 channels + (cvcuda.Type.U8, 2): cvcuda.Type._2U8, + (cvcuda.Type.S8, 2): cvcuda.Type._2S8, + (cvcuda.Type.U16, 2): cvcuda.Type._2U16, + (cvcuda.Type.S16, 2): cvcuda.Type._2S16, + (cvcuda.Type.U32, 2): cvcuda.Type._2U32, + (cvcuda.Type.S32, 2): cvcuda.Type._2S32, + (cvcuda.Type.U64, 2): cvcuda.Type._2U64, + (cvcuda.Type.S64, 2): cvcuda.Type._2S64, + (cvcuda.Type.F16, 2): cvcuda.Type._2F16, + (cvcuda.Type.F32, 2): cvcuda.Type._2F32, + (cvcuda.Type.F64, 2): cvcuda.Type._2F64, + (cvcuda.Type.C64, 2): cvcuda.Type._2C64, + (cvcuda.Type.C128, 2): cvcuda.Type._2C128, + # 3 channels + (cvcuda.Type.U8, 3): cvcuda.Type._3U8, + (cvcuda.Type.S8, 3): cvcuda.Type._3S8, + (cvcuda.Type.U16, 3): cvcuda.Type._3U16, + (cvcuda.Type.S16, 3): cvcuda.Type._3S16, + (cvcuda.Type.U32, 3): cvcuda.Type._3U32, + (cvcuda.Type.S32, 3): cvcuda.Type._3S32, + (cvcuda.Type.U64, 3): cvcuda.Type._3U64, + (cvcuda.Type.S64, 3): cvcuda.Type._3S64, + (cvcuda.Type.F16, 3): cvcuda.Type._3F16, + (cvcuda.Type.F32, 3): cvcuda.Type._3F32, + (cvcuda.Type.F64, 3): cvcuda.Type._3F64, + (cvcuda.Type.C64, 3): cvcuda.Type._3C64, + # 4 channels + (cvcuda.Type.U8, 4): cvcuda.Type._4U8, + (cvcuda.Type.S8, 4): cvcuda.Type._4S8, + (cvcuda.Type.U16, 4): cvcuda.Type._4U16, + (cvcuda.Type.S16, 4): cvcuda.Type._4S16, + (cvcuda.Type.U32, 4): cvcuda.Type._4U32, + (cvcuda.Type.S32, 4): cvcuda.Type._4S32, + (cvcuda.Type.U64, 4): cvcuda.Type._4U64, + (cvcuda.Type.S64, 4): cvcuda.Type._4S64, + (cvcuda.Type.F16, 4): cvcuda.Type._4F16, + (cvcuda.Type.F32, 4): cvcuda.Type._4F32, + (cvcuda.Type.F64, 4): cvcuda.Type._4F64, + (cvcuda.Type.C64, 4): cvcuda.Type._4C64, +} + + +def is_packed_type(dtype: cvcuda.Type) -> bool: + return dtype in PACKED_TYPES + + +def get_packed_type( + scalar_type: cvcuda.Type, channels: int, *, always_get_scalar: bool = False +) -> cvcuda.Type | None: + if scalar_type not in SCALAR_TYPES_SET: + raise ValueError(f"Invalid scalar type: {scalar_type}") + if channels not in {1, 2, 3, 4} and not always_get_scalar: + raise ValueError(f"Invalid channels: {channels}") + packed_type = SCALAR_TO_PACKED_TYPE.get((scalar_type, channels)) + if packed_type is None and always_get_scalar: + return SCALAR_TO_PACKED_TYPE[(scalar_type, 1)] + return packed_type + + +# CV-CUDA <-> NumPy +# ================= +# maps cvcuda.Type to a np.dtype +TYPE_TO_NP_DTYPE: dict[cvcuda.Type, np.dtype] = { + cvcuda.Type.U8: np.uint8, + cvcuda.Type._2U8: np.dtype("2u1"), + cvcuda.Type._3U8: np.dtype("3u1"), + cvcuda.Type._4U8: np.dtype("4u1"), + cvcuda.Type.S8: np.int8, + cvcuda.Type._2S8: np.dtype("2i1"), + cvcuda.Type._3S8: np.dtype("3i1"), + cvcuda.Type._4S8: np.dtype("4i1"), + cvcuda.Type.U16: np.uint16, + cvcuda.Type._2U16: np.dtype("2u2"), + cvcuda.Type._3U16: np.dtype("3u2"), + cvcuda.Type._4U16: np.dtype("4u2"), + cvcuda.Type.S16: np.int16, + cvcuda.Type._2S16: np.dtype("2i2"), + cvcuda.Type._3S16: np.dtype("3i2"), + cvcuda.Type._4S16: np.dtype("4i2"), + cvcuda.Type.U32: np.uint32, + cvcuda.Type._2U32: np.dtype("2u4"), + cvcuda.Type._3U32: np.dtype("3u4"), + cvcuda.Type._4U32: np.dtype("4u4"), + cvcuda.Type.S32: np.int32, + cvcuda.Type._2S32: np.dtype("2i4"), + cvcuda.Type._3S32: np.dtype("3i4"), + cvcuda.Type._4S32: np.dtype("4i4"), + cvcuda.Type.U64: np.uint64, + cvcuda.Type._2U64: np.dtype("2u8"), + cvcuda.Type._3U64: np.dtype("3u8"), + cvcuda.Type._4U64: np.dtype("4u8"), + cvcuda.Type.S64: np.int64, + cvcuda.Type._2S64: np.dtype("2i8"), + cvcuda.Type._3S64: np.dtype("3i8"), + cvcuda.Type._4S64: np.dtype("4i8"), + cvcuda.Type.F16: np.float16, + cvcuda.Type._2F16: np.dtype("2e"), + cvcuda.Type._3F16: np.dtype("3e"), + cvcuda.Type._4F16: np.dtype("4e"), + cvcuda.Type.F32: np.float32, + cvcuda.Type._2F32: np.dtype("2f"), + cvcuda.Type._3F32: np.dtype("3f"), + cvcuda.Type._4F32: np.dtype("4f"), + cvcuda.Type.F64: np.float64, + cvcuda.Type._2F64: np.dtype("2d"), + cvcuda.Type._3F64: np.dtype("3d"), + cvcuda.Type._4F64: np.dtype("4d"), + cvcuda.Type.C64: np.csingle, + cvcuda.Type._2C64: np.dtype("2c8"), + cvcuda.Type._3C64: np.dtype("3c8"), + cvcuda.Type._4C64: np.dtype("4c8"), + cvcuda.Type.C128: np.cdouble, + cvcuda.Type._2C128: np.dtype("2c16"), +} +# maps np.dtype to a cvcuda.Type +NP_DTYPE_TO_TYPE: dict[np.dtype, cvcuda.Type] = { + np.dtype(v): k for k, v in TYPE_TO_NP_DTYPE.items() +} +# all cvcuda.Type values +TYPES: list[cvcuda.Type] = list(TYPE_TO_NP_DTYPE.keys()) +TYPE_SET: set[cvcuda.Type] = set(TYPES) +# all np.dtype values (with overlap to cvcuda.Type) +NP_DTYPES: list[np.dtype] = list(TYPE_TO_NP_DTYPE.values()) +NP_DTYPE_SET: set[np.dtype] = set(NP_DTYPES) +# maps cvcuda.Format to a np.dtype +FORMAT_TO_NP_DTYPE: dict[cvcuda.Format, np.dtype] = { + k: TYPE_TO_NP_DTYPE[v] for k, v in FORMAT_TO_TYPE.items() +} + +# CV-CUDA <-> PyTorch (optional -- all tables are empty when torch is absent) +# =========================================================================== +if _HAS_TORCH: + TYPE_TO_TORCH_DTYPE: dict = { + cvcuda.Type.U8: torch.uint8, + cvcuda.Type.S8: torch.int8, + cvcuda.Type.S16: torch.int16, + cvcuda.Type.S32: torch.int32, + cvcuda.Type.S64: torch.int64, + cvcuda.Type.F16: torch.float16, + cvcuda.Type.F32: torch.float32, + cvcuda.Type.F64: torch.float64, + cvcuda.Type.C64: torch.complex64, + cvcuda.Type.C128: torch.complex128, + } + # Older PyTorch versions (common on Jetson / aarch64) lack unsigned integer + # dtypes, so every downstream dict + # comprehension and lookup must guard with ``if t in TYPE_TO_TORCH_DTYPE``. + if hasattr(torch, "uint16"): + TYPE_TO_TORCH_DTYPE[cvcuda.Type.U16] = torch.uint16 + TYPE_TO_TORCH_DTYPE[cvcuda.Type.U32] = torch.uint32 + TYPE_TO_TORCH_DTYPE[cvcuda.Type.U64] = torch.uint64 + TORCH_DTYPE_TO_TYPE: dict = {v: k for k, v in TYPE_TO_TORCH_DTYPE.items()} + TORCH_DTYPES: list = list(TORCH_DTYPE_TO_TYPE.keys()) + TORCH_DTYPE_SET: set = set(TORCH_DTYPES) + FORMAT_TO_TORCH_DTYPE: dict = { + k: TYPE_TO_TORCH_DTYPE[v] + for k, v in FORMAT_TO_TYPE.items() + if v in TYPE_TO_TORCH_DTYPE + } + NP_DTYPE_TO_TORCH_DTYPE: dict = { + TYPE_TO_NP_DTYPE[k]: v for k, v in TYPE_TO_TORCH_DTYPE.items() + } + TORCH_DTYPE_TO_NP_DTYPE: dict = {v: k for k, v in NP_DTYPE_TO_TORCH_DTYPE.items()} + TORCH_DTYPE_TO_FORMAT: dict = { + TYPE_TO_TORCH_DTYPE[t]: TYPE_TO_FORMAT[t] + for t in SCALAR_TYPES + if t in TYPE_TO_TORCH_DTYPE + } +else: + TYPE_TO_TORCH_DTYPE = {} + TORCH_DTYPE_TO_TYPE = {} + TORCH_DTYPES = [] + TORCH_DTYPE_SET = set() + FORMAT_TO_TORCH_DTYPE = {} + NP_DTYPE_TO_TORCH_DTYPE = {} + TORCH_DTYPE_TO_NP_DTYPE = {} + TORCH_DTYPE_TO_FORMAT = {} + +NP_DTYPE_TO_FORMAT: dict[np.dtype, cvcuda.Format] = { + TYPE_TO_NP_DTYPE[t]: TYPE_TO_FORMAT[t] for t in SCALAR_TYPES +} + + +# Auto-conversion functions +# ========================= +_valid_types = (cvcuda.Format, cvcuda.Type, np.dtype) +if _HAS_TORCH: + _valid_types = (cvcuda.Format, cvcuda.Type, np.dtype, torch.dtype) + + +def as_cvcuda_format( + fmt: "cvcuda.Format | cvcuda.Type | np.dtype | torch.dtype", +) -> cvcuda.Format: + """Convert any supported dtype representation to a ``cvcuda.Format``.""" + if fmt in FORMAT_SET: + return fmt + if fmt in TYPE_SET: + return TYPE_TO_FORMAT[fmt] + if fmt in NP_DTYPE_SET: + return NP_DTYPE_TO_FORMAT[fmt] + if fmt in TORCH_DTYPE_SET: + return TORCH_DTYPE_TO_FORMAT[fmt] + + raise ValueError(f"Invalid format: {fmt}. Valid formats are: {FORMATS}") + + +def as_cvcuda_dtype( + dtype: "cvcuda.Format | cvcuda.Type | np.dtype | torch.dtype", +) -> cvcuda.Type: + """Convert any supported dtype representation to a ``cvcuda.Type``.""" + if dtype in TYPE_SET: + return dtype + + if dtype in FORMAT_SET: + return FORMAT_TO_TYPE[dtype] + if dtype in NP_DTYPE_SET: + return NP_DTYPE_TO_TYPE[dtype] + if dtype in TORCH_DTYPE_SET: + return TORCH_DTYPE_TO_TYPE[dtype] + + raise ValueError(f"Invalid dtype: {dtype}. Valid types are: {_valid_types}") + + +def as_np_dtype( + dtype: "cvcuda.Format | cvcuda.Type | np.dtype | torch.dtype", +) -> np.dtype: + """Convert any supported dtype representation to a ``np.dtype``.""" + if dtype in NP_DTYPE_SET: + return dtype + + if dtype in FORMAT_SET: + return FORMAT_TO_NP_DTYPE[dtype] + if dtype in TYPE_SET: + return TYPE_TO_NP_DTYPE[dtype] + if dtype in TORCH_DTYPE_SET: + return TORCH_DTYPE_TO_NP_DTYPE[dtype] + + raise ValueError(f"Invalid dtype: {dtype}. Valid types are: {_valid_types}") + + +def as_torch_dtype(dtype): + """Convert any supported dtype representation to a ``torch.dtype``. + + Requires torch to be installed; raises ``RuntimeError`` if it is not. + Not all cvcuda types have a torch equivalent in every PyTorch version. In + those cases a ``ValueError`` is raised instead of a ``KeyError``. + """ + if not _HAS_TORCH: + raise RuntimeError("as_torch_dtype() requires torch to be installed") + + if dtype in TORCH_DTYPE_SET: + return dtype + + if dtype in FORMAT_SET and dtype in FORMAT_TO_TORCH_DTYPE: + return FORMAT_TO_TORCH_DTYPE[dtype] + if dtype in TYPE_SET and dtype in TYPE_TO_TORCH_DTYPE: + return TYPE_TO_TORCH_DTYPE[dtype] + if dtype in NP_DTYPE_SET and dtype in NP_DTYPE_TO_TORCH_DTYPE: + return NP_DTYPE_TO_TORCH_DTYPE[dtype] + + raise ValueError(f"Invalid dtype: {dtype}. Valid types are: {_valid_types}") + + +# Mapping from (dtype, channels) to format for image creation +DTYPE_CHANNELS_TO_FORMAT: dict[tuple[cvcuda.Type, int], cvcuda.Format] = { + # 1 channel + (cvcuda.Type.U8, 1): cvcuda.Format.U8, + (cvcuda.Type.U16, 1): cvcuda.Format.U16, + (cvcuda.Type.U32, 1): cvcuda.Format.U32, + (cvcuda.Type.S8, 1): cvcuda.Format.S8, + (cvcuda.Type.S16, 1): cvcuda.Format.S16, + (cvcuda.Type.S32, 1): cvcuda.Format.S32, + (cvcuda.Type.F16, 1): cvcuda.Format.F16, + (cvcuda.Type.F32, 1): cvcuda.Format.F32, + (cvcuda.Type.F64, 1): cvcuda.Format.F64, + (cvcuda.Type.C64, 1): cvcuda.Format.C64, + (cvcuda.Type.C128, 1): cvcuda.Format.C128, + # 3 channels (RGB variants) + (cvcuda.Type.U8, 3): cvcuda.Format.RGB8, + (cvcuda.Type.F16, 3): cvcuda.Format.RGBf16, + (cvcuda.Type.F32, 3): cvcuda.Format.RGBf32, + # 4 channels (RGBA variants) + (cvcuda.Type.U8, 4): cvcuda.Format.RGBA8, + (cvcuda.Type.F16, 4): cvcuda.Format.RGBAf16, + (cvcuda.Type.F32, 4): cvcuda.Format.RGBAf32, +} + + +def dtype_channels_to_format(dtype: cvcuda.Type, channels: int) -> cvcuda.Format: + key = (dtype, channels) + if key in DTYPE_CHANNELS_TO_FORMAT: + return DTYPE_CHANNELS_TO_FORMAT[key] + + raise ValueError(f"No format available for dtype={dtype}, channels={channels}") + + +def get_typestr(dtype) -> str: + if dtype in FORMAT_SET: + return np.dtype(FORMAT_TO_NP_DTYPE[dtype]).str + if dtype in TYPE_SET: + return np.dtype(TYPE_TO_NP_DTYPE[dtype]).str + if dtype in NP_DTYPE_SET: + return np.dtype(dtype).str + if TORCH_DTYPE_SET and dtype in TORCH_DTYPE_SET: + return np.dtype(TORCH_DTYPE_TO_NP_DTYPE[dtype]).str + + raise ValueError(f"Cannot get typestr for dtype: {dtype}.") + + +# define other constants +LAYOUTS_STR: list[str] = [ + "CDHW", + "CFDHW", + "CFHW", + "CHW", + "CW", + "DHW", + "DHWC", + "FCDHW", + "FCHW", + "FDHW", + "FDHWC", + "FHW", + "FHWC", + "HW", + "HWC", + "NCDHW", + "NCFDHW", + "NCFHW", + "NCHW", + "NCW", + "NDHW", + "NDHWC", + "NFCDHW", + "NFCHW", + "NFDHW", + "NFDHWC", + "NFHW", + "NFHWC", + "NHW", + "NHWC", + "NW", + "NWC", + "W", + "WC", +] +LAYOUTS: dict[int, dict[str, list | set]] = {} +for layout in LAYOUTS_STR: + rank = len(layout) + if rank not in LAYOUTS: + LAYOUTS[rank] = { + "list": [], + "set": set(), + } + LAYOUTS[rank]["list"].append(layout) + LAYOUTS[rank]["set"].add(layout) + +# Image-compatible layouts: only layouts containing both H and W dimensions. +# Layouts without H (e.g., NWC, NCW, WC, CW) cause crashes in image operators +# because they lack the spatial dimensions that image operations require. +IMAGE_LAYOUTS_3D: set[str] = {"CHW", "DHW", "FHW", "HWC", "NHW"} +IMAGE_LAYOUTS_4D: set[str] = { + "CDHW", + "CFHW", + "DHWC", + "FCHW", + "FDHW", + "FHWC", + "NCHW", + "NDHW", + "NFHW", + "NHWC", +} +IMAGE_LAYOUTS: set[str] = IMAGE_LAYOUTS_3D | IMAGE_LAYOUTS_4D + +# Channel counts for testing +CHANNELS: set[int] = {1, 2, 3, 4, 5, 6} + +LAYOUT_TO_SHAPE: dict[str, Callable[[int, int, int, int], tuple[int, ...]]] = { + "W": lambda N, H, W, C: (W,), + "WC": lambda N, H, W, C: (W, C), + "CW": lambda N, H, W, C: (C, W), + "HW": lambda N, H, W, C: (H, W), + "HWC": lambda N, H, W, C: (H, W, C), + "CHW": lambda N, H, W, C: (C, H, W), + "FHW": lambda N, H, W, C: (1, H, W), + "FHWC": lambda N, H, W, C: (1, H, W, C), + "FCHW": lambda N, H, W, C: (1, C, H, W), + "NW": lambda N, H, W, C: (N, W), + "NWC": lambda N, H, W, C: (N, W, C), + "NCW": lambda N, H, W, C: (N, C, W), + "NHW": lambda N, H, W, C: (N, H, W), + "NHWC": lambda N, H, W, C: (N, H, W, C), + "NCHW": lambda N, H, W, C: (N, C, H, W), + "NFHW": lambda N, H, W, C: (N, 1, H, W), + "NFHWC": lambda N, H, W, C: (N, 1, H, W, C), + "NFCHW": lambda N, H, W, C: (N, 1, C, H, W), + "DHW": lambda N, H, W, C: (1, H, W), + "DHWC": lambda N, H, W, C: (1, H, W, C), + "CDHW": lambda N, H, W, C: (C, 1, H, W), + "FDHW": lambda N, H, W, C: (1, 1, H, W), + "FDHWC": lambda N, H, W, C: (1, 1, H, W, C), + "FCDHW": lambda N, H, W, C: (1, C, 1, H, W), + "CFHW": lambda N, H, W, C: (C, 1, H, W), + "CFDHW": lambda N, H, W, C: (C, 1, 1, H, W), + "NDHW": lambda N, H, W, C: (N, 1, H, W), + "NDHWC": lambda N, H, W, C: (N, 1, H, W, C), + "NCDHW": lambda N, H, W, C: (N, C, 1, H, W), + "NFDHW": lambda N, H, W, C: (N, 1, 1, H, W), + "NFDHWC": lambda N, H, W, C: (N, 1, 1, H, W, C), + "NFCDHW": lambda N, H, W, C: (N, 1, C, 1, H, W), + "NCFHW": lambda N, H, W, C: (N, C, 1, H, W), + "NCFDHW": lambda N, H, W, C: (N, C, 1, 1, H, W), +} + + +def resolve_shape( + layout: str, + channels: int, + size: tuple[int, int] = (24, 24), + batch_size: int = 1, +) -> tuple[int, ...]: + if layout not in LAYOUT_TO_SHAPE: + raise ValueError(f"Invalid layout: {layout}.") + return LAYOUT_TO_SHAPE[layout](batch_size, size[0], size[1], channels) + + +def get_dim_index(layout: cvcuda.TensorLayout | str, dim: str) -> int | None: + if isinstance(layout, cvcuda.TensorLayout): + layout = str(layout) + idx = layout.find(dim) + return idx if idx >= 0 else None diff --git a/tests/cvcuda/python/cvcuda_util.py b/tests/cvcuda/python/cvcuda_util.py index de3274741..3bb097284 100644 --- a/tests/cvcuda/python/cvcuda_util.py +++ b/tests/cvcuda/python/cvcuda_util.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,19 +12,20 @@ # 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. +from __future__ import annotations -import cvcuda +import colorsys import math +import numbers import os import threading -import torch -import numpy as np -import numbers -import copy -import colorsys +import numpy as np from typing_extensions import Callable, Concatenate, ParamSpec +import cupy +import cvcuda + P = ParamSpec("P") IMG_FORMAT_TO_TYPE = { @@ -44,8 +45,12 @@ cvcuda.Format.RGBA8: np.uint8, cvcuda.Format.BGR8: np.uint8, cvcuda.Format.RGB8: np.uint8, + cvcuda.Format.RGB8p: np.uint8, + cvcuda.Format.RGBA8p: np.uint8, cvcuda.Format.RGBf32: np.float32, cvcuda.Format.RGBAf32: np.float32, + cvcuda.Format.RGBf32p: np.float32, + cvcuda.Format.RGBAf32p: np.float32, cvcuda.Format.F32: np.float32, cvcuda.Format.F64: np.float64, cvcuda.Format.U8: np.uint8, @@ -59,6 +64,16 @@ } +def get_numpy_dtype_for_format(img_format): + dtype = IMG_FORMAT_TO_NUMPY_DTYPE.get(img_format) + if dtype is None: + raise ValueError( + f"Unsupported image format: {img_format}. " + f"Supported: {list(IMG_FORMAT_TO_NUMPY_DTYPE)}" + ) + return dtype + + def dist_odd(x): """Add one to x if even to make it odd @@ -100,31 +115,7 @@ def generate_data(shape, dtype, max_random=None, rng=None): return data -class CudaBuffer: - __cuda_array_interface__ = None - obj = None - - -def to_torch_dtype(data_type): - """Convert a data type into one supported by torch - - Args: - data_type (numpy dtype): Original data type - - Returns: - dtype: A data type supported by torch - """ - if data_type == np.uint16: - return np.dtype(np.int16) - elif data_type == np.uint32: - return np.dtype(np.int32) - elif data_type == np.uint64: - return np.dtype(np.int64) - else: - return data_type - - -def to_cpu_numpy_buffer(cuda_buffer): +def to_cpu_numpy_buffer(cuda_buffer) -> np.ndarray: """Convert a CUDA buffer to host (CPU) data Args: @@ -133,15 +124,7 @@ def to_cpu_numpy_buffer(cuda_buffer): Returns: numpy array: The CUDA buffer copied to the CPU """ - torch_dtype = copy.copy(cuda_buffer.dtype) - torch_dtype = to_torch_dtype(torch_dtype) - - buf = CudaBuffer - buf.obj = cuda_buffer - buf.__cuda_array_interface__ = cuda_buffer.__cuda_array_interface__ - buf.__cuda_array_interface__["typestr"] = torch_dtype.str - - return torch.as_tensor(buf).cpu().numpy() + return cupy.asarray(cuda_buffer).get() def to_cuda_buffer(host_data): @@ -151,23 +134,9 @@ def to_cuda_buffer(host_data): host_data (numpy array): Host data Returns: - CudaBuffer: The converted CUDA buffer + cupy.ndarray: The converted CUDA buffer """ - orig_dtype = copy.copy(host_data.dtype) - - host_data.dtype = to_torch_dtype(host_data.dtype) - - dev = torch.as_tensor(host_data, device="cuda").cuda() - host_data.dtype = orig_dtype # restore it - - # The cuda buffer only needs the cuda array interface. - # We can then set its dtype to whatever we want. - buf = CudaBuffer() - buf.__cuda_array_interface__ = dev.__cuda_array_interface__ - buf.__cuda_array_interface__["typestr"] = orig_dtype.str - buf.obj = dev # make sure it holds a reference to the torch buffer - - return buf + return cupy.asarray(host_data) def to_cvcuda_tensor(data, layout): @@ -215,6 +184,29 @@ def create_tensor(shape, dtype, layout, max_random=None, rng=None, transform_dis return to_cvcuda_tensor(h_data, layout) +def create_tensor_batch( + shape, dtype, layout, count=2, max_random=None, rng=None, transform_dist=None +): + """Create a list of tensors with identical shapes. + + Args: + shape (tuple or list): Tensor shape + dtype (numpy dtype): Tensor data type (e.g. np.uint8) + layout (string): Tensor layout (e.g. NC, HWC, NHWC) + count (int): Number of tensors to create (default: 2) + max_random (number or tuple or list): Maximum random value + rng (numpy random Generator): To fill tensor with random values + transform_dist (function): To transform random values + + Returns: + list[cvcuda.Tensor]: List of created tensors + """ + return [ + create_tensor(shape, dtype, layout, max_random, rng, transform_dist) + for _ in range(count) + ] + + def to_cvcuda_image(host_data): """Convert an image in host data to cvcuda.Image @@ -239,8 +231,21 @@ def create_image(size, img_format, max_random=None, rng=None): Returns: cvcuda.Image: The created image """ + dtype = get_numpy_dtype_for_format(img_format) + if img_format.planes > 1: + planes = [] + for plane_idx in range(img_format.planes): + plane_max_random = max_random + if ( + isinstance(max_random, (tuple, list)) + and len(max_random) == img_format.planes + ): + plane_max_random = max_random[plane_idx] + h_data = generate_data((size[1], size[0]), dtype, plane_max_random, rng) + planes.append(to_cuda_buffer(h_data)) + return cvcuda.as_image(planes, img_format) + shape = (size[1], size[0], img_format.channels) - dtype = IMG_FORMAT_TO_NUMPY_DTYPE[img_format] h_data = generate_data(shape, dtype, max_random, rng) return to_cvcuda_image(h_data) @@ -261,7 +266,7 @@ def create_image_pattern( np.array: The created image """ shape = (size[1], size[0], img_format.channels) - dtype = IMG_FORMAT_TO_NUMPY_DTYPE[img_format] + dtype = get_numpy_dtype_for_format(img_format) ci, cj = shape[0] - 1, shape[1] - 1 max_r = max(ci, cj) * math.sqrt(2) image = np.zeros(shape, dtype=dtype) @@ -385,3 +390,20 @@ def wrapper(thread_no: int): if exception is not None: raise exception + + +__all__ = [ + "IMG_FORMAT_TO_TYPE", + "IMG_FORMAT_TO_NUMPY_DTYPE", + "get_numpy_dtype_for_format", + "dist_odd", + "generate_data", + "to_cpu_numpy_buffer", + "to_cuda_buffer", + "create_tensor", + "create_image", + "create_image_pattern", + "create_image_batch", + "clone_image_batch", + "run_parallel", +] diff --git a/tests/cvcuda/python/test_adaptivethresholdtype.py b/tests/cvcuda/python/test_adaptivethresholdtype.py index 983178e49..122846a84 100644 --- a/tests/cvcuda/python/test_adaptivethresholdtype.py +++ b/tests/cvcuda/python/test_adaptivethresholdtype.py @@ -13,7 +13,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import torch # noqa(F401) import cvcuda diff --git a/tests/cvcuda/python/test_bordertype.py b/tests/cvcuda/python/test_bordertype.py index 4f0ba9268..411edb455 100644 --- a/tests/cvcuda/python/test_bordertype.py +++ b/tests/cvcuda/python/test_bordertype.py @@ -13,7 +13,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import torch # noqa(F401) import cvcuda diff --git a/tests/cvcuda/python/test_cache.py b/tests/cvcuda/python/test_cache.py index 56cd8d366..686caef15 100644 --- a/tests/cvcuda/python/test_cache.py +++ b/tests/cvcuda/python/test_cache.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,17 +13,20 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os import gc +import os import sys import threading import time -import cvcuda -import torch -import pytest + import numpy as np +import pytest + +import cuda.bindings.runtime as cudart +import cvcuda import cvcuda_util as util +import cupy RNG = np.random.default_rng(12345) @@ -42,25 +45,25 @@ def test_clear_cache_inside_op(): gc.collect() -def test_gcbag_is_being_emptied(): +def test_clear_cache_empties_gcbag(): # Make sure there's no work scheduled on the stream, it's all ours. workstream = cvcuda.Stream() # In order to test if the GCBag was really emptied, - # we create a torch tensor, - ttensor = torch.as_tensor(np.ndarray([100, 1500, 1500, 3], np.uint8), device="cuda") + # we create a CUDA buffer, + tensor = cupy.asarray(np.ndarray([100, 1500, 1500, 3], np.uint8)) # keep track of its initial refcount. - orig_ttensor_refcount = sys.getrefcount(ttensor) - # and wrap it in a nvcv tensor 'cvwrapper' - cvwrapper = cvcuda.as_tensor(ttensor, cvcuda.TensorLayout.NHWC) + orig_tensor_refcount = sys.getrefcount(tensor) + # and wrap it in a cvcuda tensor 'cvwrapper' + cvwrapper = cvcuda.as_tensor(tensor, cvcuda.TensorLayout.NHWC) # We can then indirectly tell if 'cvwrapper' was destroyed by - # monitoring 'ttensor's refcount. + # monitoring 'tensor's refcount. # This works because we know 'cvwrapper' holds a reference to - # 'ttensor', as proved by the following assert: - wrapped_ttensor_refcount = sys.getrefcount(ttensor) - assert wrapped_ttensor_refcount > orig_ttensor_refcount + # 'tensor', as proved by the following assert: + wrapped_tensor_refcount = sys.getrefcount(tensor) + assert wrapped_tensor_refcount > orig_tensor_refcount # We need now to make sure cvwrapper is in the GCBag. # For that, we need to use it in operator @@ -68,42 +71,26 @@ def test_gcbag_is_being_emptied(): cvcuda.median_blur(cvwrapper, [3, 3], stream=workstream) # And make sure it finishes. workstream.sync() - # Make sure the auxiliary stream has finished extending cvwrapper's lifetime - cvcuda.internal.syncAuxStream() - - # cvwrapper being referenced by others shouldn't change ttensor's refcount. - assert sys.getrefcount(ttensor) == wrapped_ttensor_refcount + # cvwrapper being referenced by others shouldn't change tensor's refcount. + assert sys.getrefcount(tensor) == wrapped_tensor_refcount - # Now remove cvwrapper from the cache by clearing it. + # Clearing the cache must also drain completed resource holds. Otherwise + # callers need to submit an unrelated operator before memory is released. cvcuda.clear_cache() # We can now release it from python side. We can't track its lifetime # directly anymore. del cvwrapper - # But we know indirectly that it is still alive - assert sys.getrefcount(ttensor) == wrapped_ttensor_refcount - - # To finally destroy cvwrapper, we empty the GCBag by executing a - # cvcuda operator, any would do. - with workstream: - cvcuda.median_blur( - cvcuda.Tensor((3, 64, 32, 3), cvcuda.Type.U8, cvcuda.TensorLayout.NHWC), - [3, 3], - ) - workstream.sync() - cvcuda.internal.syncAuxStream() - - # Lo and behold, cvwrapper is no more. - # The wrapped tensor torch has the same refcount it had when we've created it. - assert sys.getrefcount(ttensor) == orig_ttensor_refcount + # The wrapped tensor has the same refcount it had when we've created it. + assert sys.getrefcount(tensor) == orig_tensor_refcount def test_cache_limit_get_set(): cvcuda.clear_cache() # Verify initial cache limit (half of total gpu mem) - total = torch.cuda.mem_get_info()[1] + total = cupy.cuda.Device().mem_info[1] assert cvcuda.get_cache_limit_inbytes() == total // 2 # Verify we can also set the cache limit @@ -141,9 +128,9 @@ def test_cache_current_byte_size(): def test_cache_external_cacheitem(): cvcuda.clear_cache() - input_tensor = torch.rand(2, 30, 16, 1).cuda() + input_tensor = np.random.rand(2, 30, 16, 1).astype(np.uint8) input_tensor = input_tensor * 255 - input_tensor = input_tensor.to(dtype=torch.uint8) + input_tensor = cupy.asarray(input_tensor) frames_cvcuda = cvcuda.as_tensor(input_tensor, "NHWC") assert cvcuda.current_cache_size_inbytes() == 0 @@ -217,7 +204,7 @@ def create_tensors(thread_no: int, h: int, w: int): barrier.wait() # Ensure that the cache limit was not altered by another test - cvcuda.set_cache_limit_inbytes(torch.cuda.mem_get_info()[1] // 2) + cvcuda.set_cache_limit_inbytes(cupy.cuda.Device().mem_info[1] // 2) cvcuda.clear_cache() nb_threads = len(os.sched_getaffinity(0)) @@ -248,7 +235,7 @@ def clear_cache(): clear_event.set() # notify that the cache has been cleared # Ensure that the cache limit was not altered by another test - cvcuda.set_cache_limit_inbytes(torch.cuda.mem_get_info()[1] // 2) + cvcuda.set_cache_limit_inbytes(cupy.cuda.Device().mem_info[1] // 2) cvcuda.clear_cache() done_event = threading.Event() @@ -269,3 +256,86 @@ def clear_cache(): cvcuda.Tensor((h, w), np.uint8) assert cvcuda.cache_size() == 1 assert cvcuda.current_cache_size_inbytes() == size_inbytes + + +# --------------------------------------------------------------------------- +# Multi-GPU cache tests (skipped when fewer than 2 GPUs are available) +# --------------------------------------------------------------------------- + +_err, NUM_GPUS = cudart.cudaGetDeviceCount() +if _err != cudart.cudaError_t.cudaSuccess: + NUM_GPUS = 0 + +requires_multi_gpu = pytest.mark.skipif( + NUM_GPUS < 2, + reason="Multi-GPU cache tests require at least 2 GPUs", +) + + +@pytest.fixture() +def _restore_device_and_limits(): + """Restore CUDA device 0 and per-device cache limits after each multi-GPU cache test.""" + yield + for gpu_id in range(NUM_GPUS): + cudart.cudaSetDevice(gpu_id) + total = cupy.cuda.Device().mem_info[1] + cvcuda.set_cache_limit_inbytes(total // 2) + cudart.cudaSetDevice(0) + cvcuda.clear_cache() + + +@requires_multi_gpu +@pytest.mark.usefixtures("_restore_device_and_limits") +def test_per_device_cache_limits(): + """Cache limits, size accounting, and eviction must be independent per device.""" + cvcuda.clear_cache() + + # 1. Default limit is per-device: half of each GPU's total memory. + for gpu_id in range(NUM_GPUS): + cudart.cudaSetDevice(gpu_id) + total = cupy.cuda.Device().mem_info[1] + assert cvcuda.get_cache_limit_inbytes() == total // 2 + + # 2. Setting limit on device 0 does not affect device 1. + cudart.cudaSetDevice(0) + cvcuda.set_cache_limit_inbytes(12345) + assert cvcuda.get_cache_limit_inbytes() == 12345 + + cudart.cudaSetDevice(1) + total_1 = cupy.cuda.Device().mem_info[1] + assert cvcuda.get_cache_limit_inbytes() == total_1 // 2 + + # 3. Size accounting is per-device. + cudart.cudaSetDevice(0) + total_0 = cupy.cuda.Device().mem_info[1] + cvcuda.set_cache_limit_inbytes(total_0 // 2) + cvcuda.clear_cache() + + cudart.cudaSetDevice(0) + img0 = cvcuda.Image.zeros((32, 32), cvcuda.Format.RGB8) + size0 = cvcuda.current_cache_size_inbytes() + assert size0 > 0 + + cudart.cudaSetDevice(1) + assert cvcuda.current_cache_size_inbytes() == 0 + + img1 = cvcuda.Image.zeros((32, 32), cvcuda.Format.RGB8) + size1 = cvcuda.current_cache_size_inbytes() + assert size1 > 0 + + cudart.cudaSetDevice(0) + assert cvcuda.current_cache_size_inbytes() == size0 + + # 4. Eviction on device 0 does not affect device 1. + del img0 + cudart.cudaSetDevice(0) + img_size = cvcuda.internal.nbytes_in_cache( + cvcuda.Image.zeros((32, 32), cvcuda.Format.RGB8) + ) + cvcuda.set_cache_limit_inbytes(img_size - 1) + assert cvcuda.current_cache_size_inbytes() == 0 + + cudart.cudaSetDevice(1) + assert cvcuda.current_cache_size_inbytes() == size1 + + del img1 diff --git a/tests/cvcuda/python/test_cai_input_stream_race.py b/tests/cvcuda/python/test_cai_input_stream_race.py new file mode 100644 index 000000000..1f11b9704 --- /dev/null +++ b/tests/cvcuda/python/test_cai_input_stream_race.py @@ -0,0 +1,325 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""Regression guard for an input-side CAI v3 stream race. + +When a producer (e.g., cupy) launches a fill kernel on a non-blocking CUDA +stream but advertises ``__cuda_array_interface__["stream"] == 1`` (legacy +default per the CAI v3 spec), an event-based barrier on the legacy default +stream does NOT capture the producer's work — non-blocking streams have +no implicit synchronization with the legacy default. A subsequent cvcuda +op on a distinct cvcuda stream would then read the buffer before the fill +has retired. + +cvcuda's ``priv::Resource::submitSync`` falls back to ``cudaDeviceSynchronize`` +when ``prevHandle`` is one of the CAI default-stream sentinels (``cudaStreamLegacy`` +or ``cudaStreamPerThread``), which catches both this case and PyTorch's +CAI v2 (no stream field). Combined with ResourceGuard's ``run()`` pattern +that inserts barriers BEFORE the consumer kernel queues, the race is +closed in production cvcuda. This test pins both halves of the fix. +""" + +import gc + +import cupy +import pytest + +import cvcuda + +# GPU-side busy-wait kernel (idiom borrowed from test_interop_cai_stream.py). +# Runs on an SM and does not require host callbacks, so pytest's GIL is not +# involved. ~500 ms on a 1 GHz SM clock — comfortably above any host-side +# event-record/event-wait latency we might race against. +_DELAY_KERNEL = cupy.RawKernel( + r""" +extern "C" __global__ void cvcuda_test_busy_wait(unsigned long long cycles) { + unsigned long long start = clock64(); + while (clock64() - start < cycles) { /* spin */ } +} +""", + "cvcuda_test_busy_wait", +) + + +def _warm_up_varshape_autocontrast(shape, consumer_stream): + """Prime AutoContrast while keeping its warm-up resources alive.""" + size = (shape[1], shape[0]) + warm_src = cvcuda.ImageBatchVarShape(1) + warm_src.pushback(cvcuda.Image(size, cvcuda.Format.RGB8)) + warm_dst = cvcuda.ImageBatchVarShape(1) + warm_dst.pushback(cvcuda.Image(size, cvcuda.Format.RGB8)) + with consumer_stream: + cvcuda.autocontrast_into(warm_dst, warm_src, stream=consumer_stream) + consumer_stream.sync() + return size, (warm_src, warm_dst) + + +def _use_then_release_external_image_wrapper( + shape, consumer_stream, other_batch, *, external_is_output +): + """Give an external wrapper stream ownership, then make it reusable.""" + buffer = cupy.zeros(shape, dtype=cupy.uint8) + image = cvcuda.as_image(buffer) + wrapper_id = image.id + batch = cvcuda.ImageBatchVarShape(1) + batch.pushback(image) + + src_batch, dst_batch = ( + (other_batch, batch) if external_is_output else (batch, other_batch) + ) + with consumer_stream: + cvcuda.autocontrast_into(dst_batch, src_batch, stream=consumer_stream) + consumer_stream.sync() + + batch.clear() + del batch, image, buffer + gc.collect() + cvcuda.internal.syncAuxStream() + return wrapper_id + + +@pytest.mark.parametrize("iterations", [3]) +def test_cvtcolor_honors_cai_stream_when_producer_uses_nonblocking_stream(iterations): + """Producer fills on a non-blocking cupy stream; consumer (cvcuda) must + observe the fill, not race against it.""" + producer_stream = cupy.cuda.Stream(non_blocking=True) + cycles = 500_000_000 # ~500 ms + + for i in range(iterations): + cvcuda.clear_cache() + consumer_stream = cvcuda.Stream() + + with producer_stream: + # Push a long busy-wait, then the fill. Both are queued on + # producer_stream (non-blocking). The fill's actual completion + # is far enough out that any timing-flaky barrier in cvcuda + # surfaces as wrong output. + _DELAY_KERNEL((1,), (1,), (cycles,)) + src = cupy.full((1, 8, 8, 3), fill_value=100, dtype=cupy.uint8) + + src_nv = cvcuda.as_tensor(src, "NHWC") + with consumer_stream: + out = cvcuda.cvtcolor( + src_nv, + cvcuda.ColorConversion.BGR2GRAY, + stream=consumer_stream, + ) + consumer_stream.sync() + + result = cupy.asarray(out.cuda()).get() + # BGR2GRAY of (100, 100, 100) is 100. If the cvcuda kernel ran + # before the producer's fill completed, the buffer is uninitialized + # / partially filled and the output is a mix of values. + unique = sorted(set(result.flatten().tolist())) + assert (result == 100).all(), ( + f"iter {i}: cvcuda read producer buffer before fill completed. " + f"Output values: {unique}. This is the input-side CAI v3 stream " + f"race: cupy advertises stream=1 (legacy default) but launched " + f"on a non-blocking stream, so cvcuda's event-based barrier on " + f"the legacy default missed the producer's work." + ) + + +@pytest.mark.parametrize("iterations", [3]) +def test_autocontrast_honors_cai_stream_when_producer_uses_nonblocking_stream( + iterations, +): + """AutoContrast must insert ResourceGuard barriers before its kernels.""" + producer_stream = cupy.cuda.Stream(non_blocking=True) + consumer_stream = cvcuda.Stream() + cycles = 500_000_000 + shape = (1, 128, 128, 3) + + # Prime the cached operator and its internal min/max workspace. Otherwise, + # its first cudaMalloc can serialize the device and accidentally hide the race. + warm_src = cvcuda.Tensor(shape, cvcuda.Type.U8, "NHWC") + warm_dst = cvcuda.Tensor(shape, cvcuda.Type.U8, "NHWC") + with consumer_stream: + cvcuda.autocontrast_into(warm_dst, warm_src, stream=consumer_stream) + consumer_stream.sync() + + for i in range(iterations): + src = cupy.zeros(shape, dtype=cupy.uint8) + dst = cvcuda.Tensor(shape, cvcuda.Type.U8, "NHWC") + cupy.cuda.get_current_stream().synchronize() + + with producer_stream: + _DELAY_KERNEL((1,), (1,), (cycles,)) + src.fill(100) + + src_nv = cvcuda.as_tensor(src, "NHWC") + with consumer_stream: + cvcuda.autocontrast_into(dst, src_nv, stream=consumer_stream) + consumer_stream.sync() + + result = cupy.asarray(dst.cuda()).get() + unique = sorted(set(result.flatten().tolist())) + assert (result == 100).all(), ( + f"iter {i}: AutoContrast read its producer-owned input before the " + f"fill completed. Output values: {unique}. ResourceGuard barriers " + "must be submitted before the operator kernels." + ) + + +def test_autocontrast_varshape_honors_cai_stream_when_producer_uses_nonblocking_stream(): + """VarShape AutoContrast must guard image inputs before its kernels.""" + cvcuda.clear_cache() + + producer_stream = cupy.cuda.Stream(non_blocking=True) + consumer_stream = cvcuda.Stream() + cycles = 500_000_000 + shape = (128, 128, 3) + + # Prime the VarShape overload and its internal min/max workspace. Otherwise, + # its first cudaMalloc can serialize the device and accidentally hide the race. + size, _warm_batches = _warm_up_varshape_autocontrast(shape, consumer_stream) + + # Give one external wrapper prior ownership on the consumer stream, then + # release it completely so the producer-owned input below must rebind it. + first_dst_image = cvcuda.Image(size, cvcuda.Format.RGB8) + first_dst_batch = cvcuda.ImageBatchVarShape(1) + first_dst_batch.pushback(first_dst_image) + first_id = _use_then_release_external_image_wrapper( + shape, + consumer_stream, + first_dst_batch, + external_is_output=False, + ) + first_dst_batch.clear() + del first_dst_batch, first_dst_image + + src = cupy.zeros(shape, dtype=cupy.uint8) + dst_image = cvcuda.Image(size, cvcuda.Format.RGB8) + dst_batch = cvcuda.ImageBatchVarShape(1) + dst_batch.pushback(dst_image) + cupy.cuda.get_current_stream().synchronize() + + with producer_stream: + _DELAY_KERNEL((1,), (1,), (cycles,)) + src.fill(100) + + src_image = cvcuda.as_image(src) + assert src_image.id == first_id, ( + "The producer-owned input did not rebind the completed cached Image " + f"wrapper: first id={first_id}, rebound id={src_image.id}" + ) + src_batch = cvcuda.ImageBatchVarShape(1) + src_batch.pushback(src_image) + with consumer_stream: + cvcuda.autocontrast_into(dst_batch, src_batch, stream=consumer_stream) + consumer_stream.sync() + + result = cupy.asarray(dst_image.cuda()).get() + unique = sorted(set(result.flatten().tolist())) + assert (result == 100).all(), ( + "VarShape AutoContrast read its producer-owned input before the fill " + f"completed. Output values: {unique}. ResourceGuard barriers must be " + "submitted before the operator kernels." + ) + + +def test_autocontrast_varshape_honors_cai_stream_for_producer_owned_output(): + """VarShape AutoContrast must guard image outputs before its kernels.""" + cvcuda.clear_cache() + + producer_stream = cupy.cuda.Stream(non_blocking=True) + consumer_stream = cvcuda.Stream() + cycles = 500_000_000 + shape = (128, 128, 3) + + # Prime the VarShape overload and its internal min/max workspace. Otherwise, + # its first cudaMalloc can serialize the device and accidentally hide the race. + _size, _warm_batches = _warm_up_varshape_autocontrast(shape, consumer_stream) + + src = cupy.full(shape, fill_value=100, dtype=cupy.uint8) + cupy.cuda.get_current_stream().synchronize() + src_image = cvcuda.as_image(src) + src_image.submitStreamSync(consumer_stream) + src_batch = cvcuda.ImageBatchVarShape(1) + src_batch.pushback(src_image) + + # Give one external wrapper prior ownership on the consumer stream, then + # release it completely so the producer-owned output below must rebind it. + first_id = _use_then_release_external_image_wrapper( + shape, + consumer_stream, + src_batch, + external_is_output=True, + ) + + dst = cupy.zeros(shape, dtype=cupy.uint8) + cupy.cuda.get_current_stream().synchronize() + with producer_stream: + _DELAY_KERNEL((1,), (1,), (cycles,)) + dst.fill(7) + + dst_image = cvcuda.as_image(dst) + assert dst_image.id == first_id, ( + "The producer-owned output did not rebind the completed cached Image " + f"wrapper: first id={first_id}, rebound id={dst_image.id}" + ) + dst_batch = cvcuda.ImageBatchVarShape(1) + dst_batch.pushback(dst_image) + with consumer_stream: + cvcuda.autocontrast_into(dst_batch, src_batch, stream=consumer_stream) + consumer_stream.sync() + producer_stream.synchronize() + + result = dst.get() + unique = sorted(set(result.flatten().tolist())) + assert (result == 100).all(), ( + "A producer-owned VarShape output overwrote AutoContrast after " + f"submission. Output values: {unique}. ResourceGuard barriers must " + "include each output image before the operator kernels." + ) + + +def test_cleared_batch_releases_external_image_wrapper_for_rebind(): + """A cleared batch must release its external Image wrapper for reuse. + + The stream-race regressions need to prove that ``as_image`` rebound an + existing wrapper rather than created a fresh one. A cached + ``ImageBatchVarShape`` retains its pushed Images, so the deterministic + release sequence must clear the batch before dropping Python handles and + synchronizing the auxiliary callback stream. + """ + cvcuda.clear_cache() + + consumer_stream = cvcuda.Stream() + size = (128, 128) + shape = (size[1], size[0], 3) + + src_batch = cvcuda.ImageBatchVarShape(1) + src_batch.pushback(cvcuda.Image(size, cvcuda.Format.RGB8)) + + first_id = _use_then_release_external_image_wrapper( + shape, + consumer_stream, + src_batch, + external_is_output=True, + ) + + second_buffer = cupy.zeros(shape, dtype=cupy.uint8) + second_image = cvcuda.as_image(second_buffer) + second_id = second_image.id + + try: + assert second_id == first_id, ( + "The completed external Image wrapper remained held after both " + f"streams synchronized: first id={first_id}, second id={second_id}" + ) + finally: + del second_image, second_buffer + cvcuda.clear_cache() diff --git a/tests/cvcuda/python/test_datatype.py b/tests/cvcuda/python/test_datatype.py index 9c39616c0..40f84a51d 100644 --- a/tests/cvcuda/python/test_datatype.py +++ b/tests/cvcuda/python/test_datatype.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,42 +13,64 @@ # See the License for the specific language governing permissions and # limitations under the License. -import cvcuda -import pytest as t import numpy as np +import pytest as t + +import cvcuda + +import cvcuda_types as cv_types + + +def sub_check_type(cv_type: cvcuda.Type, dtype: np.dtype): + assert dtype == cv_type + t = cvcuda.Type(dtype) + assert t == cv_type + assert t == dtype + +@t.mark.parametrize("cv_type", cv_types.TYPES) +def test_datatype_dtype(cv_type): + dtype = cv_types.as_np_dtype(cv_type) + sub_check_type(cv_type, dtype) -@t.mark.parametrize( - "type,dt", - [ - (cvcuda.Type.U8, np.uint8), - (cvcuda.Type.U8, np.dtype(np.uint8)), - (cvcuda.Type.S8, np.int8), - (cvcuda.Type.U16, np.uint16), - (cvcuda.Type.S16, np.int16), - (cvcuda.Type.U32, np.uint32), - (cvcuda.Type.S32, np.int32), - (cvcuda.Type.U64, np.uint64), - (cvcuda.Type.S64, np.int64), - (cvcuda.Type.F32, np.float32), - (cvcuda.Type.F64, np.float64), - (cvcuda.Type.C64, np.complex64), - (cvcuda.Type._2C64, np.dtype("2F")), - (cvcuda.Type.C128, np.complex128), - (cvcuda.Type._2C128, np.dtype("2D")), - (cvcuda.Type._3S8, np.dtype("3i1")), - (cvcuda.Type._4S32, np.dtype("4i")), - ], -) -def test_datatype_dtype(type, dt): - assert type == dt - - t = cvcuda.Type(dt) - assert t == type - assert t == dt + +def test_datatype_repr_uses_public_type_name(): + assert repr(cvcuda.Type(np.uint8)) == "nvcv.Type.U8" + + +@t.mark.parametrize("cv_type1", cv_types.TYPES) +@t.mark.parametrize("cv_type2", cv_types.TYPES) +def test_datatype_dtype_conv(cv_type1, cv_type2): + if cv_type1 == cv_type2: + sub_check_type(cv_type1, cv_types.as_np_dtype(cv_type2)) + sub_check_type(cv_types.as_np_dtype(cv_type1), cv_type2) + else: + with t.raises(AssertionError): + sub_check_type(cv_type1, cv_types.as_np_dtype(cv_type2)) + with t.raises(AssertionError): + sub_check_type(cv_types.as_np_dtype(cv_type1), cv_type2) @t.mark.parametrize("dt", [np.dtype([("f1", np.uint64), ("f2", np.int32)]), "invalid"]) def test_datatype_dtype_conv_error(dt): with t.raises(TypeError): cvcuda.Type(dt) + + +def test_datatype_is_hashable_and_value_consistent(): + # A Type constructed via Type(...) must hash identically to the equivalent + # numpy.dtype form that Type.U8 (and friends) surface as, so that all three + # representations are interchangeable equal-and-hash-equal dict keys. + wrapper = cvcuda.Type(np.uint8) + named = cvcuda.Type.U8 + npdt = np.dtype("uint8") + + assert wrapper == named == npdt + assert hash(wrapper) == hash(named) == hash(npdt) + + d = {wrapper: "u8"} + assert d[named] == "u8" + assert d[npdt] == "u8" + + assert len({wrapper, named, npdt}) == 1 + assert len({cvcuda.Type.U8, cvcuda.Type.S8}) == 2 diff --git a/tests/cvcuda/python/test_image.py b/tests/cvcuda/python/test_image.py index 14d48a187..bae73e13c 100644 --- a/tests/cvcuda/python/test_image.py +++ b/tests/cvcuda/python/test_image.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,27 +14,110 @@ # limitations under the License. import numpy as np +import pytest as t + import cvcuda import cvcuda_util as util -import pytest as t -import torch +import cupy + +import cvcuda_types as cv_types def test_image_is_cached(): created_ids = set() - pt_img = torch.rand((1, 1), dtype=torch.float32, device="cuda") + pt_img = cupy.asarray(np.random.rand(1, 1).astype(np.float32)) img = cvcuda.as_image(pt_img) created_ids.add(img.id) del img # delete img such that only cache has a reference to it for i in range(50): - pt_img = torch.rand((1 + i, i + 2), dtype=torch.float32, device="cuda") + pt_img = cupy.asarray(np.random.rand(1 + i, i + 2).astype(np.float32)) img = cvcuda.as_image(pt_img) assert img.id in created_ids del img +def test_failed_image_rebind_preserves_stream_state(): + import subprocess + import sys + import textwrap + + script = textwrap.dedent( + """\ + import gc + + import cupy + import cvcuda + + + class DLPackOnly: + def __init__(self, array): + self.array = array + + def __dlpack_device__(self): + return self.array.__dlpack_device__() + + def __dlpack__(self, stream=None, max_version=None, dl_device=None, copy=None): + kwargs = {} + if stream is not None: + kwargs["stream"] = stream + if max_version is not None: + kwargs["max_version"] = max_version + if dl_device is not None: + kwargs["dl_device"] = dl_device + if copy is not None: + kwargs["copy"] = copy + return self.array.__dlpack__(**kwargs) + + + cvcuda.clear_cache() + source = cupy.zeros((6, 8), dtype=cupy.uint8) + image = cvcuda.as_image(source) + + producer = cupy.cuda.Stream(non_blocking=True) + stream = cvcuda.as_stream(producer) + handle = stream.handle + image.submitStreamSync(stream) + + # The cached Image's Resource is now the sole owner keeping the external + # producer stream alive. + del stream, producer, image + gc.collect() + assert cupy.cuda.runtime.streamQuery(handle) == 0 + + managed_mem = cupy.cuda.malloc_managed(3 * 4 * 2) + managed_plane = cupy.ndarray((3, 4, 2), dtype=cupy.uint8, memptr=managed_mem) + + try: + try: + cvcuda.as_image([DLPackOnly(source), DLPackOnly(managed_plane)]) + except RuntimeError as exc: + assert "All buffers must belong to the same device" in str(exc) + else: + raise AssertionError("mixed-device image planes unexpectedly accepted") + + # Failed validation must leave the cached wrapper's stream ownership + # unchanged. A premature reset destroys the otherwise-unreferenced stream. + assert cupy.cuda.runtime.streamQuery(handle) == 0 + finally: + cvcuda.clear_cache() + gc.collect() + + print("PASS") + """ + ) + proc = subprocess.run( + [sys.executable, "-c", script], capture_output=True, text=True, timeout=30 + ) + assert proc.returncode == 0, ( + f"rebind subprocess exited {proc.returncode}\n" + f"stdout:\n{proc.stdout}\n" + f"stderr:\n{proc.stderr}" + ) + assert proc.stdout.strip().endswith("PASS") + + def test_image_creation_works(): img = cvcuda.Image((7, 5), cvcuda.Format.NV12) assert img.width == 7 @@ -58,9 +141,10 @@ def test_image_creation_arg_keywords(): ([5, 7, 1], np.uint8, cvcuda.Format.U8), ([5, 7], np.uint8, cvcuda.Format.U8), ([5, 7, 1], np.int8, cvcuda.Format.S8), - ([5, 7, 1], np.uint16, cvcuda.Format.U16), ([5, 7, 1], np.int16, cvcuda.Format.S16), + ([5, 7, 1], np.float16, cvcuda.Format.F16), ([5, 7, 2], np.int16, cvcuda.Format._2S16), + ([5, 7, 2], np.float16, cvcuda.Format._2F16), ([5, 7, 1], np.float32, cvcuda.Format.F32), ([5, 7, 1], np.float64, cvcuda.Format.F64), ([5, 7, 2], np.float32, cvcuda.Format._2F32), @@ -71,6 +155,8 @@ def test_image_creation_arg_keywords(): ([5, 7], np.csingle, cvcuda.Format.C64), ([5, 7], np.cdouble, cvcuda.Format.C128), ([5, 7], np.dtype("2f"), cvcuda.Format._2F32), + ([5, 7], np.dtype("2e"), cvcuda.Format._2F16), + ([5, 7, 1], np.uint16, cvcuda.Format.U16), ] @@ -111,7 +197,7 @@ def test_wrap_host_buffer_explicit_format(shape, dt, format): buffmt2_common = [ # packed formats ( - [((6, 8), np.uint8, torch.uint8), ((3, 4, 2), np.uint8, torch.uint8)], + [((6, 8), np.uint8, np.uint8), ((3, 4, 2), np.uint8, np.uint8)], cvcuda.Format.NV12_ER, ) ] @@ -125,7 +211,7 @@ def test_wrap_host_buffer_infer_imgformat_multiple_planes(buffers, format): assert img.format == format img = cvcuda.as_image( - [torch.zeros(size=buf[0], dtype=buf[2], device="cuda") for buf in buffers] + [cupy.asarray(np.zeros(buf[0], dtype=buf[2])) for buf in buffers] ) assert img.width == 8 assert img.height == 6 @@ -140,7 +226,7 @@ def test_wrap_host_buffer_explicit_format2(buffers, format): assert img.format == format img = cvcuda.as_image( - [torch.zeros(size=buf[0], dtype=buf[2], device="cuda") for buf in buffers], + [cupy.asarray(np.zeros(buf[0], dtype=buf[2])) for buf in buffers], format, ) assert img.width == 8 @@ -178,27 +264,31 @@ def test_wrap_host_buffer_infer_format_geometry( assert img.format.channels == channels -def test_wrap_host_buffer_arg_keywords(): - img = cvcuda.Image(buffer=np.ndarray([5, 7], np.float32), format=cvcuda.Format.F32) +@t.mark.parametrize("dtype", [np.float32, np.float16]) +def test_wrap_host_buffer_arg_keywords(dtype): + fmt = cvcuda.Format.F32 if dtype == np.float32 else cvcuda.Format.F16 + img = cvcuda.Image(buffer=np.ndarray([5, 7], dtype), format=fmt) assert img.size == (7, 5) - assert img.format == cvcuda.Format.F32 + assert img.format == fmt img = cvcuda.as_image( - buffer=util.to_cuda_buffer(np.ndarray([5, 7], np.float32)), - format=cvcuda.Format.F32, + buffer=util.to_cuda_buffer(np.ndarray([5, 7], dtype)), + format=fmt, ) assert img.size == (7, 5) - assert img.format == cvcuda.Format.F32 + assert img.format == fmt -def test_wrap_host_buffer_infer_format_arg_keywords(): - img = cvcuda.Image(buffer=np.ndarray([5, 7], np.float32)) +@t.mark.parametrize("dtype", [np.float32, np.float16]) +def test_wrap_host_buffer_infer_format_arg_keywords(dtype): + fmt = cvcuda.Format.F32 if dtype == np.float32 else cvcuda.Format.F16 + img = cvcuda.Image(buffer=np.ndarray([5, 7], dtype)) assert img.size == (7, 5) - assert img.format == cvcuda.Format.F32 + assert img.format == fmt - img = cvcuda.as_image(buffer=util.to_cuda_buffer(np.ndarray([5, 7], np.float32))) + img = cvcuda.as_image(buffer=util.to_cuda_buffer(np.ndarray([5, 7], dtype))) assert img.size == (7, 5) - assert img.format == cvcuda.Format.F32 + assert img.format == fmt def test_wrap_host_image_with_format__buffer_has_unsupported_type(): @@ -351,9 +441,11 @@ def test_image_create_from_host_packed(): (1, 1, 1, 1, 1, 1, 231, 1, 1, 1, 1, 257, 1, 1), "CHW", ), + ((257, 231), cvcuda.Format.RGBAf16, None, np.float16, (231, 257, 4), "HWC"), ((257, 231), cvcuda.Format.RGBAf32, None, np.float32, (231, 257, 4), "HWC"), ((257, 231), cvcuda.Format.RGBA8, "HWC", np.uint8, (231, 257, 4), "HWC"), ((257, 231), cvcuda.Format.RGBA8p, None, np.uint8, (4, 231, 257), "CHW"), + ((257, 231), cvcuda.Format.RGBAf16p, None, np.float16, (4, 231, 257), "CHW"), ((257, 231), cvcuda.Format.RGBAf32p, "CHW", np.float32, (4, 231, 257), "CHW"), ( (258, 232), @@ -405,10 +497,12 @@ def test_image_export_cuda_buffer( rng = np.random.default_rng(0) gold_buffer = list() - # Write values in it on CUDA side for buf in cuda_buffer: - gold_buffer.append((rng.random(size=buf.shape) * 255).astype(buf.dtype)) - torch.as_tensor(buf, device="cuda").copy_(torch.as_tensor(gold_buffer[-1])) + gold_data = (rng.random(size=buf.shape) * 255).astype(buf.dtype) + gold_buffer.append(gold_data) + + cuda_buf = cupy.asarray(buf) + cuda_buf[:] = cupy.asarray(gold_data) # Get values back on cpu host_buffer = img.cpu(simple_layout) @@ -426,18 +520,29 @@ def test_image_export_cuda_buffer( def test_image_export_cuda_buffer_strides(): - # torch returns packed buffers - timg = torch.zeros((11, 37), dtype=torch.uint8, device="cuda") - img = cvcuda.as_image(timg) + # cupy returns packed buffers + cuda_img = cupy.asarray(np.zeros((11, 37), dtype=np.uint8)) + + img = cvcuda.as_image(cuda_img) data = img.cuda() assert data.strides == (37, 1) -def test_image_zeros(): - img = cvcuda.Image.zeros((67, 34), cvcuda.Format.F32) - assert (img.cpu() == np.zeros((34, 67), np.float32)).all() +@t.mark.parametrize( + "fmt", + [ + cvcuda.Format.U8, + cvcuda.Format.U16, + cvcuda.Format.U32, + cvcuda.Format.F16, + cvcuda.Format.F32, + ], +) +def test_image_zeros(fmt): + img = cvcuda.Image.zeros((67, 34), fmt) + assert (img.cpu() == np.zeros((34, 67), cv_types.as_np_dtype(fmt))).all() def test_image_is_kept_alive_by_cuda_array_interface(): @@ -473,13 +578,13 @@ def test_image_wrapper_nodeletion(): # run twice, first run is without cache re-usage, second is with cache re-usage for i in range(2): np_img = np.random.rand(1 + i, 2 + i).astype(np.float32) - pt_img = torch.from_numpy(np_img).cuda() + pt_img = cupy.asarray(np_img) nv_img = cvcuda.as_image(pt_img) del nv_img try: - assert (pt_img.cpu().numpy() == np_img).all() + assert (pt_img.get() == np_img).all() except RuntimeError: assert False, "Invalid memory" @@ -499,10 +604,58 @@ def test_image_size_in_bytes(): img_create_host_vector = cvcuda.Image([np_img, np_img]) assert cvcuda.internal.nbytes_in_cache(img_create_host_vector) > 0 - pt_img = torch.from_numpy(np_img).cuda() + pt_img = cupy.asarray(np_img) img_wrap_external_buffer = cvcuda.as_image(pt_img) assert cvcuda.internal.nbytes_in_cache(img_wrap_external_buffer) == 0 img_wrap_external_buffer_vector = cvcuda.as_image([pt_img, pt_img]) assert cvcuda.internal.nbytes_in_cache(img_wrap_external_buffer_vector) == 0 + + +def test_as_image_does_not_leak_wrappers(): + """ + Regression test for GitHub issue #258 (as_image leaks memory). + + Each call to `cvcuda.as_image` made while an earlier result is still + alive forces a *new* wrapper Image into the cache (the existing one + is "in use"). Each cached wrapper keeps its wrapped GPU buffer alive + via its ExternalBuffer. Once the caller drops those Images the + wrappers are no longer in use, but they used to stay in the cache + indefinitely — every one still pinning its external buffer — until + explicitly reused or `clear_cache` was called. + + The fix makes every as_image call run `removeAllNotInUseMatching` on + the wrapper key (matching what `Tensor::WrapExternalBuffer` already + does) so stale wrappers are freed promptly. + """ + import gc + + cvcuda.clear_cache() + gc.collect() + assert cvcuda.cache_size() == 0 + + N = 8 + tensors = [cupy.random.rand(16, 16).astype(cupy.float32) for _ in range(N)] + + # List comprehension: each as_image call sees earlier results as + # "in use", so N distinct wrappers end up in the cache. + imgs = [cvcuda.as_image(t) for t in tensors] + assert cvcuda.cache_size() == N + + # Drop the Python handles. Wrappers transition to "not in use" but + # remain in the cache with their external-buffer references. + del imgs + gc.collect() + assert cvcuda.cache_size() == N # nothing has cleaned them up yet + + # One more as_image call. With the fix this reuses one wrapper and + # evicts the rest. Without the fix the cache keeps growing. + extra = cupy.random.rand(16, 16).astype(cupy.float32) + img = cvcuda.as_image(extra) + + assert cvcuda.cache_size() == 1, ( + f"Expected 1 wrapper in cache after cleanup, got {cvcuda.cache_size()}. " + "Stale as_image wrappers are accumulating (GitHub issue #258)." + ) + del img diff --git a/tests/cvcuda/python/test_imgbatchvarshape.py b/tests/cvcuda/python/test_imgbatchvarshape.py index bf1f94c74..7b0e77747 100644 --- a/tests/cvcuda/python/test_imgbatchvarshape.py +++ b/tests/cvcuda/python/test_imgbatchvarshape.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,10 +14,11 @@ # limitations under the License. import numpy as np -import cvcuda as cvcuda -import cvcuda_util as util import pytest as t -import torch + +import cvcuda +import cvcuda_util as util +import cupy def test_imgbatchvarshape_are_cached(): @@ -26,7 +27,7 @@ def test_imgbatchvarshape_are_cached(): # Create first VarShape pt_imgs = [] for n in range(2): - pt_img = torch.rand((1 + n, 2 + n), dtype=torch.float32, device="cuda") + pt_img = cupy.asarray(np.random.rand(1 + n, 2 + n).astype(np.float32)) pt_imgs.append(pt_img) batch = cvcuda.as_images(pt_imgs) @@ -40,7 +41,7 @@ def test_imgbatchvarshape_are_cached(): for i in range(50): pt_imgs = [] for _ in range(2): - pt_img = torch.rand((1 + i, 2 + i), dtype=torch.float32, device="cuda") + pt_img = cupy.asarray(np.random.rand(1 + i, 2 + i).astype(np.float32)) pt_imgs.append(pt_img) batch = cvcuda.as_images(pt_imgs) @@ -141,9 +142,10 @@ def test_imgbatchvarshape_several_images(): ([5, 7, 1], np.uint8, cvcuda.Format.U8), ([5, 7], np.uint8, cvcuda.Format.U8), ([5, 7, 1], np.int8, cvcuda.Format.S8), - ([5, 7, 1], np.uint16, cvcuda.Format.U16), ([5, 7, 1], np.int16, cvcuda.Format.S16), + ([5, 7, 1], np.float16, cvcuda.Format.F16), ([5, 7, 2], np.int16, cvcuda.Format._2S16), + ([5, 7, 2], np.float16, cvcuda.Format._2F16), ([5, 7, 1], np.float32, cvcuda.Format.F32), ([5, 7, 1], np.float64, cvcuda.Format.F64), ([5, 7, 2], np.float32, cvcuda.Format._2F32), @@ -152,6 +154,8 @@ def test_imgbatchvarshape_several_images(): ([5, 7], np.csingle, cvcuda.Format.C64), ([5, 7], np.cdouble, cvcuda.Format.C128), ([5, 7], np.dtype("2f"), cvcuda.Format._2F32), + ([5, 7], np.dtype("2e"), cvcuda.Format._2F16), + ([5, 7, 1], np.uint16, cvcuda.Format.U16), ] @@ -193,13 +197,13 @@ def test_imgbatchvarshape_wrapper_nodeletion(): # run twice, first run is without cache re-usage, second is with cache re-usage for i in range(2): np_img = np.random.rand(1 + i, 2 + i).astype(np.float32) - pt_img = torch.from_numpy(np_img).cuda() + pt_img = cupy.asarray(np_img) batch = cvcuda.as_images([pt_img]) del batch try: - assert (pt_img.cpu().numpy() == np_img).all() + assert (pt_img.get() == np_img).all() except RuntimeError: assert False, "Invalid memory" @@ -211,6 +215,6 @@ def test_imagebatchvarshape_size_in_bytes(): batch_create = cvcuda.ImageBatchVarShape(5) assert cvcuda.internal.nbytes_in_cache(batch_create) > 0 - pt_img = torch.as_tensor(np.ndarray((16, 32, 4), dtype=np.float32), device="cuda") + pt_img = cupy.asarray(np.ndarray((16, 32, 4), dtype=np.float32)) batch_as_images = cvcuda.as_images([pt_img]) assert cvcuda.internal.nbytes_in_cache(batch_as_images) > 0 diff --git a/tests/cvcuda/python/test_imgformat.py b/tests/cvcuda/python/test_imgformat.py index e6ea177f6..842ee857b 100644 --- a/tests/cvcuda/python/test_imgformat.py +++ b/tests/cvcuda/python/test_imgformat.py @@ -13,9 +13,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -import cvcuda as cvcuda import pytest as t +import cvcuda as cvcuda + @t.mark.parametrize( "format,gold_channels", @@ -25,6 +26,7 @@ (cvcuda.Format._2S16, 2), (cvcuda.Format.S8, 1), (cvcuda.Format.NV12, 3), + (cvcuda.Format.F16, 1), ], ) def test_imgformat_numchannels(format, gold_channels): @@ -39,6 +41,7 @@ def test_imgformat_numchannels(format, gold_channels): (cvcuda.Format._2S16, 1), (cvcuda.Format.S8, 1), (cvcuda.Format.NV12, 2), + (cvcuda.Format.F16, 1), ], ) def test_imgformat_planes(format, gold_planes): diff --git a/tests/cvcuda/python/test_import_order.py b/tests/cvcuda/python/test_import_order.py index a5dcc2628..2ce94d60c 100644 --- a/tests/cvcuda/python/test_import_order.py +++ b/tests/cvcuda/python/test_import_order.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,8 +16,10 @@ # Import order is important, # torch must be loaded correctly even if cvcuda was imported first import cvcuda # noqa: F401 -import torch import numpy as np +import pytest + +torch = pytest.importorskip("torch") def test_import_cvcuda_first_works(): diff --git a/tests/cvcuda/python/test_interop_cai_stream.py b/tests/cvcuda/python/test_interop_cai_stream.py new file mode 100644 index 000000000..ef1a1c62d --- /dev/null +++ b/tests/cvcuda/python/test_interop_cai_stream.py @@ -0,0 +1,179 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +""" +Deterministic reproductions of cross-library stream contract violations in +the cvcuda Python bindings. + +Covered bugs: + +1. ``test_cuda_export_populates_stream_field`` — CAI (CUDA Array Interface) + export must emit v3 with a populated ``stream`` key. Pre-fix cvcuda + emits v2 with no stream field. Contract assertion, no timing + dependency. + +2. ``test_dlpack_export_honors_consumer_stream`` — ``__dlpack__(stream=X)`` + must synchronize cvcuda's writer stream into the consumer's stream X + before returning the capsule (DLPack v1 spec). Pre-fix cvcuda + accepts the kwarg but ignores it. Deterministic via a GPU-side + busy-wait kernel on the producer stream: post-fix, consumer stream + transitively waits for the kernel to drain; pre-fix, it does not. + +Both tests finish in well under a second on a single-GPU host. The +corresponding input-side race (cvcuda ignoring the producer's CAI +stream) is timing-sensitive and covered separately by +``test_multi_stream.py::test_wait_stream_fanout`` on multi-GPU CI. +""" + +import time + +import cupy +import numpy as np + + +# ---------------------------------------------------------------------- +# GPU-side busy-wait kernel. Used to hold a stream in a measurable +# non-idle state without relying on host callbacks (which deadlock +# against pytest's GIL). clock64() reads the SM clock; cycle counts +# are chosen to be safely above the assertion threshold on modern GPUs +# (~1 GHz SM clock: 2e8 cycles ≈ 200 ms). Even on very fast GPUs the +# wait will stay comfortably above the 40 ms threshold the tests check. +# ---------------------------------------------------------------------- +_DELAY_KERNEL = cupy.RawKernel( + r""" +extern "C" __global__ void cvcuda_test_busy_wait(unsigned long long cycles) { + unsigned long long start = clock64(); + while (clock64() - start < cycles) { /* spin */ } +} +""", + "cvcuda_test_busy_wait", +) + + +def _enqueue_gpu_delay(stream_handle: int, cycles: int = 200_000_000) -> None: + """Enqueue a busy-wait on the given CUDA stream. + + No host callbacks: the spin runs on a GPU SM, so pytest's GIL is + never involved. The stream does not complete work after this point + until the kernel retires (~100-200 ms on recent GPUs). + """ + ext = cupy.cuda.ExternalStream(stream_handle) + with ext: + _DELAY_KERNEL((1,), (1,), (cycles,)) + + +def test_cuda_export_populates_stream_field(): + """Tensor.cuda() must emit CAI v3 with the writer stream in `stream`. + + Pre-fix cvcuda emits CAI v2 with no ``stream`` field, so both + assertions trip deterministically. Post-fix the version bumps to 3 + and the stream key reports the cvcuda stream the data was last + written on. + """ + import cvcuda + + host = np.ones((1, 8, 8, 3), dtype=np.uint8) * 99 + src = cupy.asarray(host) + inp = cvcuda.as_tensor(src, "NHWC") + + s = cvcuda.Stream() + with s: + out = cvcuda.flip(inp, -1, stream=s) + + cai = out.cuda().__cuda_array_interface__ + + s.sync() + + assert cai.get("version") == 3, ( + f"expected CAI version 3 on export, got {cai.get('version')!r} " + "(pre-fix cvcuda emits v2 with no stream field)" + ) + assert ( + "stream" in cai + ), f"expected 'stream' key in exported CAI dict, got keys {sorted(cai)!r}" + assert cai["stream"] == int( + s.handle + ), f"expected stream={int(s.handle)} (the writer cvcuda.Stream), got {cai['stream']!r}" + + +def test_dlpack_export_honors_consumer_stream(): + """``__dlpack__(stream=X)`` must synchronize cvcuda's writer stream into X. + + Setup: run a cvcuda op on a cvcuda.Stream S1, then enqueue a + ~100-200 ms GPU busy-wait on the same stream, so S1 has substantial + pending work at the moment the consumer asks for a DLPack capsule. + + Act: call ``out.cuda().__dlpack__(stream=S2.ptr)`` on a different + cupy stream S2, then synchronize S2 while timing it. + + Contract (DLPack v1): the producer must arrange for work queued + on S2 after this call to wait until the tensor is ready on S1. + The cheapest compliant implementation is ``cudaEventRecord`` on S1 + + ``cudaStreamWaitEvent`` on S2 before returning the capsule. + + Expected: + + * Pre-fix: cvcuda ignores ``stream=``, no wait is queued on S2, + ``S2.synchronize()`` returns immediately — ``elapsed`` well under + the delay kernel's runtime. Assertion trips. + * Post-fix: cvcuda queues the wait; ``S2.synchronize()`` blocks + until S1 drains — ``elapsed`` is at least the delay-kernel + runtime. Assertion passes. + + The 40 ms threshold gives plenty of headroom both ways: the spin + kernel is sized for ~100-200 ms, and pre-fix elapsed is typically + under 2 ms (pure Python/capsule overhead). + """ + import cvcuda + + N, H, W, C = 1, 8, 8, 3 + host = np.ones((N, H, W, C), dtype=np.uint8) * 42 + src = cupy.asarray(host) + inp = cvcuda.as_tensor(src, "NHWC") + + cvcuda_stream = cvcuda.Stream() + with cvcuda_stream: + out = cvcuda.flip(inp, -1, stream=cvcuda_stream) + + # Hold S1 non-idle for ~100-200 ms via GPU busy-wait. + _enqueue_gpu_delay(int(cvcuda_stream.handle)) + + consumer_stream = cupy.cuda.Stream(non_blocking=True) + + # Request the DLPack capsule on the consumer's stream. Post-fix + # this call inserts cudaStreamWaitEvent(consumer_stream, ) so consumer_stream transitively waits for the + # delay kernel. + t0 = time.perf_counter() + cap = out.cuda().__dlpack__(stream=int(consumer_stream.ptr)) + consumer_stream.synchronize() + elapsed = time.perf_counter() - t0 + + # Cleanup: drain the producer stream so the spin kernel doesn't + # linger into the next test. + cvcuda_stream.sync() + + # Keep the capsule reference alive until after measurement so the + # DLManagedTensor deleter is not invoked mid-timing. + del cap + + assert elapsed > 0.040, ( + f"consumer stream synchronize took only {elapsed * 1000:.2f} ms; " + "expected > 40 ms because the producer stream has a ~100-200 ms " + "busy-wait kernel queued. This indicates __dlpack__(stream=...) " + "did NOT insert a cross-stream wait — the producer's pending " + "work is invisible to the consumer (DLPack v1 stream contract " + "violation)." + ) diff --git a/tests/cvcuda/python/test_interptype.py b/tests/cvcuda/python/test_interptype.py index 67a245c2e..5e06f6ce4 100644 --- a/tests/cvcuda/python/test_interptype.py +++ b/tests/cvcuda/python/test_interptype.py @@ -13,7 +13,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import torch # noqa(F401) import cvcuda diff --git a/tests/cvcuda/python/test_multi_gpu.py b/tests/cvcuda/python/test_multi_gpu.py new file mode 100644 index 000000000..96a3af184 --- /dev/null +++ b/tests/cvcuda/python/test_multi_gpu.py @@ -0,0 +1,402 @@ +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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 cupy +import numpy as np +import cvcuda + +import pytest as t + +NUM_GPUS = cupy.cuda.runtime.getDeviceCount() + +requires_multi_gpu = t.mark.skipif( + NUM_GPUS < 2, + reason="Multi-GPU tests require at least 2 GPUs", +) + + +@t.fixture(autouse=True) +def _restore_default_device(): + """Restore CUDA device 0 after each test so later tests are not polluted.""" + yield + cupy.cuda.Device(0).use() + + +# --------------------------------------------------------------------------- +# Single-GPU smoke test (always runs) +# --------------------------------------------------------------------------- + + +def test_dlpack_device_id_on_default_gpu(): + """Verify DLPack export reports the correct device, data, and zero-copy semantics.""" + cupy.cuda.Device(0).use() + + src = cupy.full((1, 4, 4, 3), fill_value=200, dtype=cupy.uint8) + src_nv = cvcuda.as_tensor(src, "NHWC") + + stream = cvcuda.Stream() + with stream: + out_nv = cvcuda.cvtcolor(src_nv, cvcuda.ColorConversion.BGR2GRAY, stream=stream) + stream.sync() + + cuda_buf = out_nv.cuda() + result = cupy.asarray(cuda_buf) + + assert result.device.id == 0 + assert result.shape == (1, 4, 4, 1) + + assert ( + result == 200 + ).all(), f"Expected all 200, got {cupy.unique(result).tolist()}" + + # Verify zero-copy: the cupy array shares the same device pointer. + cai = cuda_buf.__cuda_array_interface__ + assert result.data.ptr == cai["data"][0] + + +# --------------------------------------------------------------------------- +# Multi-GPU tests (skipped when fewer than 2 GPUs are available) +# --------------------------------------------------------------------------- + + +@requires_multi_gpu +@t.mark.parametrize("gpu_id", range(NUM_GPUS)) +def test_dlpack_reports_correct_device(gpu_id): + """DLPack export must report the actual owning device, not hardcoded 0.""" + cupy.cuda.Device(gpu_id).use() + + src = cupy.full((1, 4, 4, 3), fill_value=200, dtype=cupy.uint8) + src_nv = cvcuda.as_tensor(src, "NHWC") + + stream = cvcuda.Stream() + with stream: + out_nv = cvcuda.cvtcolor(src_nv, cvcuda.ColorConversion.BGR2GRAY, stream=stream) + stream.sync() + + result = cupy.asarray(out_nv.cuda()) + + assert result.device.id == gpu_id + assert result.shape == (1, 4, 4, 1) + assert ( + result == 200 + ).all(), f"Expected all 200, got {cupy.unique(result).tolist()}" + + +def _run_cvtcolor_on_gpu(gpu_id): + """Run the cvtcolor operator on the given GPU & return the result as a cupy array.""" + cupy.cuda.Device(gpu_id).use() + stream = cvcuda.Stream() + + src = cupy.zeros((1, 16, 16, 3), dtype=cupy.uint8) + src_nv = cvcuda.as_tensor(src, "NHWC") + + with stream: + out_nv = cvcuda.cvtcolor( + src_nv, + cvcuda.ColorConversion.BGR2GRAY, + stream=stream, + ) + stream.sync() + return cupy.asarray(out_nv.cuda()) + + +@requires_multi_gpu +@t.mark.parametrize("gpu_id", range(NUM_GPUS)) +def test_operator_on_each_gpu(gpu_id): + result = _run_cvtcolor_on_gpu(gpu_id) + assert result.device.id == gpu_id + assert result.shape == (1, 16, 16, 1) + + +@requires_multi_gpu +def test_operator_across_gpus_sequentially(): + for gpu_id in range(NUM_GPUS): + result = _run_cvtcolor_on_gpu(gpu_id) + assert result.device.id == gpu_id + assert result.shape == (1, 16, 16, 1) + + +@requires_multi_gpu +def test_resource_reuse_across_gpus(): + cupy.cuda.Device(0).use() + stream0 = cvcuda.Stream() + + rng = np.random.default_rng(0) + src = cupy.asarray(rng.integers(0, 256, (1, 32, 32, 3), dtype=np.uint8)) + src_nv = cvcuda.as_tensor(src, "NHWC") + + with stream0: + intermediate = cvcuda.cvtcolor( + src_nv, cvcuda.ColorConversion.BGR2GRAY, stream=stream0 + ) + stream0.sync() + + cupy.cuda.Device(1).use() + stream1 = cvcuda.Stream() + + gray_on_0 = cupy.asarray(intermediate.cuda()) + gray_on_1 = cupy.array(gray_on_0) + gray_rgb = cupy.concatenate([gray_on_1] * 3, axis=-1) + gray_nv = cvcuda.as_tensor(gray_rgb, "NHWC") + + with stream1: + result = cvcuda.cvtcolor( + gray_nv, cvcuda.ColorConversion.BGR2GRAY, stream=stream1 + ) + stream1.sync() + + result_cupy = cupy.asarray(result.cuda()) + assert result_cupy.device.id == 1 + assert result_cupy.shape == (1, 32, 32, 1) + + +@requires_multi_gpu +def test_cache_independent_across_gpus(): + cvcuda.clear_cache() + + results = {} + for gpu_id in range(NUM_GPUS): + cupy.cuda.Device(gpu_id).use() + stream = cvcuda.Stream() + + with cupy.cuda.Device(gpu_id): + src = cupy.full((1, 8, 8, 3), fill_value=100, dtype=cupy.uint8) + src_nv = cvcuda.as_tensor(src, "NHWC") + + with stream: + out_nv = cvcuda.cvtcolor( + src_nv, cvcuda.ColorConversion.BGR2GRAY, stream=stream + ) + stream.sync() + + result_cupy = cupy.asarray(out_nv.cuda()) + assert result_cupy.device.id == gpu_id + results[gpu_id] = result_cupy.get() + + for i in range(1, NUM_GPUS): + np.testing.assert_array_equal(results[0], results[i]) + + +# --------------------------------------------------------------------------- +# Resource creation on non-default device +# --------------------------------------------------------------------------- + + +def _image_device(img): + """Return the CUDA device ordinal that an Image's buffer lives on.""" + return cupy.asarray(img.cuda()).device.id + + +@requires_multi_gpu +def test_image_zeros_on_non_default_device(): + """Image.zeros() must allocate on the current device, not device 0.""" + cvcuda.clear_cache() + cupy.cuda.Device(1).use() + + img = cvcuda.Image.zeros((32, 32), cvcuda.Format.RGB8) + assert _image_device(img) == 1 + + +@requires_multi_gpu +def test_image_create_on_non_default_device(): + """Image() (uninitialized) must allocate on the current device.""" + cvcuda.clear_cache() + cupy.cuda.Device(1).use() + + img = cvcuda.Image((32, 32), cvcuda.Format.RGB8) + assert _image_device(img) == 1 + + +@requires_multi_gpu +def test_tensor_create_on_non_default_device(): + """Tensor() must allocate on the current device.""" + cvcuda.clear_cache() + cupy.cuda.Device(1).use() + + tensor = cvcuda.Tensor((1, 16, 16, 3), dtype=cvcuda.Type.U8, layout="NHWC") + result = cupy.asarray(tensor.cuda()) + assert result.device.id == 1 + + +# --------------------------------------------------------------------------- +# Cross-device cache isolation +# --------------------------------------------------------------------------- + + +@requires_multi_gpu +def test_cache_no_cross_device_image(): + """Cached Image on device 0 must not be returned when creating on device 1.""" + cvcuda.clear_cache() + size = (64, 64) + fmt = cvcuda.Format.RGB8 + + cupy.cuda.Device(0).use() + img0 = cvcuda.Image.zeros(size, fmt) + ptr0 = cupy.asarray(img0.cuda()).data.ptr + assert _image_device(img0) == 0 + del img0 + + cupy.cuda.Device(1).use() + img1 = cvcuda.Image.zeros(size, fmt) + ptr1 = cupy.asarray(img1.cuda()).data.ptr + assert _image_device(img1) == 1 + assert ptr1 != ptr0 + + +@requires_multi_gpu +def test_cache_no_cross_device_tensor(): + """Cached Tensor on device 0 must not be returned when creating on device 1.""" + cvcuda.clear_cache() + shape = (1, 32, 32, 3) + + cupy.cuda.Device(0).use() + t0 = cvcuda.Tensor(shape, dtype=cvcuda.Type.U8, layout="NHWC") + ptr0 = cupy.asarray(t0.cuda()).data.ptr + del t0 + + cupy.cuda.Device(1).use() + t1 = cvcuda.Tensor(shape, dtype=cvcuda.Type.U8, layout="NHWC") + ptr1 = cupy.asarray(t1.cuda()).data.ptr + assert cupy.asarray(t1.cuda()).device.id == 1 + assert ptr1 != ptr0 + + +# --------------------------------------------------------------------------- +# Operator device safety — operators with persistent GPU buffers must work +# on non-default devices without crashing (cudaErrorIllegalAddress). +# Each test runs an operator on GPU 0, then on GPU 1, verifying that the +# per-device internal state is allocated correctly. +# --------------------------------------------------------------------------- + + +def _run_op_on_each_gpu(op_fn): + """Run op_fn(gpu_id) on GPU 0 then GPU 1 and return both results.""" + results = {} + for gpu_id in range(min(NUM_GPUS, 2)): + cupy.cuda.Device(gpu_id).use() + results[gpu_id] = op_fn(gpu_id) + return results + + +@requires_multi_gpu +def test_gaussian_on_non_default_device(): + """Gaussian filter must work across GPUs (has persistent m_kernel buffer).""" + + def run(gpu_id): + with cupy.cuda.Device(gpu_id): + rng = np.random.default_rng(0) + src = cupy.asarray(rng.integers(0, 256, (1, 16, 16, 1), dtype=np.uint8)) + src_nv = cvcuda.as_tensor(src, "NHWC") + out_nv = cvcuda.gaussian(src_nv, (3, 3), (1.0, 1.0)) + result = cupy.asarray(out_nv.cuda()) + assert result.device.id == gpu_id + return result.shape + + results = _run_op_on_each_gpu(run) + assert results[0] == results[1] + + +@requires_multi_gpu +def test_rotate_on_non_default_device(): + """Rotate must work across GPUs (has persistent d_aCoeffs buffer).""" + + def run(gpu_id): + with cupy.cuda.Device(gpu_id): + rng = np.random.default_rng(0) + src = cupy.asarray(rng.integers(0, 256, (1, 16, 16, 3), dtype=np.uint8)) + src_nv = cvcuda.as_tensor(src, "NHWC") + out_nv = cvcuda.rotate(src_nv, 45.0, [0, 0], cvcuda.Interp.NEAREST) + result = cupy.asarray(out_nv.cuda()) + assert result.device.id == gpu_id + return result.shape + + results = _run_op_on_each_gpu(run) + assert results[0] == results[1] + + +@requires_multi_gpu +def test_inpaint_on_non_default_device(): + """Inpaint must work across GPUs (has persistent m_kernel_ptr, m_workspace).""" + + def run(gpu_id): + with cupy.cuda.Device(gpu_id): + rng = np.random.default_rng(0) + src = cupy.asarray(rng.integers(0, 256, (1, 16, 16, 3), dtype=np.uint8)) + mask = cupy.zeros((1, 16, 16, 1), dtype=cupy.uint8) + src_nv = cvcuda.as_tensor(src, "NHWC") + mask_nv = cvcuda.as_tensor(mask, "NHWC") + out_nv = cvcuda.inpaint(src_nv, mask_nv, 3.0) + result = cupy.asarray(out_nv.cuda()) + assert result.device.id == gpu_id + return result.shape + + results = _run_op_on_each_gpu(run) + assert results[0] == results[1] + + +@requires_multi_gpu +def test_hqresize_on_non_default_device(): + """HQ Resize must work across GPUs (has persistent filter coefficients).""" + + def run(gpu_id): + with cupy.cuda.Device(gpu_id): + src = cupy.arange(37 * 41, dtype=cupy.float32).reshape(1, 37, 41, 1) + src_nv = cvcuda.as_tensor(src, "NHWC") + stream = cvcuda.Stream() + with stream: + out_nv = cvcuda.hq_resize( + src_nv, + (74, 82), + interpolation=cvcuda.Interp.CUBIC, + stream=stream, + ) + stream.sync() + result = cupy.asarray(out_nv.cuda()) + assert result.device.id == gpu_id + assert bool(cupy.isfinite(result).all()) + return result.shape + + results = _run_op_on_each_gpu(run) + assert results[0] == results[1] + + +@requires_multi_gpu +def test_bndbox_on_non_default_device(): + """BndBox/OSD must work across GPUs (has persistent Memory buffers).""" + + def run(gpu_id): + with cupy.cuda.Device(gpu_id): + rng = np.random.default_rng(0) + src = cupy.asarray(rng.integers(0, 256, (1, 64, 64, 4), dtype=np.uint8)) + src_nv = cvcuda.as_tensor(src, "NHWC") + bboxes = cvcuda.BndBoxesI( + boxes=[ + [ + cvcuda.BndBoxI( + box=(5, 5, 20, 20), + thickness=1, + borderColor=(0, 255, 0, 255), + fillColor=(0, 128, 0, 128), + ), + ], + ], + ) + out_nv = cvcuda.bndbox(src_nv, bboxes) + result = cupy.asarray(out_nv.cuda()) + assert result.device.id == gpu_id + return result.shape + + results = _run_op_on_each_gpu(run) + assert results[0] == results[1] diff --git a/tests/cvcuda/python/test_multi_stream.py b/tests/cvcuda/python/test_multi_stream.py index e8a6df3d8..649463659 100644 --- a/tests/cvcuda/python/test_multi_stream.py +++ b/tests/cvcuda/python/test_multi_stream.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,11 +13,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -import torch -import cvcuda - +import numpy as np import pytest as t +import cvcuda +import cupy + def test_multiple_streams(): stream1 = cvcuda.Stream() # create a new stream @@ -25,6 +26,7 @@ def test_multiple_streams(): stream3 = cvcuda.Stream() # create a new stream assert stream1 is not stream2 assert stream1 is not stream3 + assert stream2 is not stream3 assert cvcuda.Stream.current is cvcuda.Stream.default assert cvcuda.Stream.current is not stream1 assert cvcuda.Stream.current is not stream2 @@ -55,15 +57,19 @@ def test_stream_context_nested(): assert cvcuda.Stream.current is cvcuda.Stream.default +class _CatchThisException(Exception): + """A test specific Exception to check that we raise the correct Exception.""" + + def test_stream_context_exception(): stream1 = cvcuda.Stream() # create a new stream stream2 = cvcuda.Stream() # create a new stream - with t.raises(Exception): + with t.raises(_CatchThisException): with stream1: assert cvcuda.Stream.current is stream1 with stream2: assert cvcuda.Stream.current is stream2 - raise Exception() + raise _CatchThisException() assert cvcuda.Stream.current is stream1 assert cvcuda.Stream.current is cvcuda.Stream.default with stream2: @@ -77,29 +83,33 @@ def test_operator_stream(): stream3 = cvcuda.Stream() # create a new stream assert stream1 is not stream2 assert stream1 is not stream3 + assert stream2 is not stream3 assert cvcuda.Stream.current is cvcuda.Stream.default assert cvcuda.Stream.current is not stream1 assert cvcuda.Stream.current is not stream2 assert cvcuda.Stream.current is not stream3 with stream1: assert cvcuda.Stream.current is stream1 - img = torch.zeros(10, 10, 3, dtype=torch.uint8, device="cuda") + img = cupy.asarray(np.zeros((10, 10, 3), dtype=np.uint8)) img = cvcuda.as_tensor(img, "HWC") cvcuda.cvtcolor(img, cvcuda.ColorConversion.BGR2GRAY) assert cvcuda.Stream.current is stream1 with stream2: assert cvcuda.Stream.current is stream2 - img = torch.zeros(10, 10, 3, dtype=torch.uint8, device="cuda") + img = cupy.asarray(np.zeros((10, 10, 3), dtype=np.uint8)) img = cvcuda.as_tensor(img, "HWC") cvcuda.cvtcolor(img, cvcuda.ColorConversion.BGR2GRAY) assert cvcuda.Stream.current is stream2 with stream3: assert cvcuda.Stream.current is stream3 - img = torch.zeros(10, 10, 3, dtype=torch.uint8, device="cuda") + img = cupy.asarray(np.zeros((10, 10, 3), dtype=np.uint8)) img = cvcuda.as_tensor(img, "HWC") cvcuda.cvtcolor(img, cvcuda.ColorConversion.BGR2GRAY) assert cvcuda.Stream.current is stream3 assert cvcuda.Stream.current is cvcuda.Stream.default + stream1.sync() + stream2.sync() + stream3.sync() def test_operator_changing_stream(): @@ -111,21 +121,26 @@ def test_operator_changing_stream(): Loop = 50 streams = [cvcuda.Stream() for _ in range(4)] # create a list of streams - inputTensor = torch.randint(0, 256, (N, H, W, C), dtype=torch.uint8).cuda() - outputTensor = torch.randint(0, 256, (N, H, W, C), dtype=torch.uint8).cuda() + inputTensor = cupy.asarray(np.random.randint(0, 256, (N, H, W, C), dtype=np.uint8)) + outputTensor = cupy.asarray(np.random.randint(0, 256, (N, H, W, C), dtype=np.uint8)) # Perform deep copy - inputTensor_copy = inputTensor.clone() + inputTensor_copy = inputTensor.copy() - inTensor = cvcuda.as_tensor(inputTensor.data, "NHWC") - outTensor = cvcuda.as_tensor(outputTensor.data, "NHWC") + inTensor = cvcuda.as_tensor(inputTensor, "NHWC") + outTensor = cvcuda.as_tensor(outputTensor, "NHWC") + prev_stream = None for _ in range(Loop): for stream in streams: + if prev_stream is not None: + stream.wait_stream(prev_stream) cvcuda.flip_into(outTensor, inTensor, -1, stream=stream) # output x flipped cvcuda.flip_into(inTensor, outTensor, -1, stream=stream) # output y flipped + prev_stream = stream - final_out = torch.as_tensor(inTensor.cuda()).cpu() - assert torch.equal(final_out, inputTensor_copy.cpu()) + streams[-1].sync() + final_out = cupy.asarray(inTensor.cuda()).get() + assert np.all(final_out == inputTensor_copy.get()) def test_operator_changing_stream_loaded(): @@ -138,15 +153,17 @@ def test_operator_changing_stream_loaded(): stream1 = cvcuda.Stream() stream2 = cvcuda.Stream() - inputTensor = torch.randint(0, 256, (N, H, W, C), dtype=torch.uint8).cuda() - inputTensorTmp = torch.randint(0, 256, (N, H, W, C), dtype=torch.uint8).cuda() - outputTensor = torch.randint(0, 256, (N, H, W, C), dtype=torch.uint8).cuda() + inputTensor = cupy.asarray(np.random.randint(0, 256, (N, H, W, C), dtype=np.uint8)) + inputTensorTmp = cupy.asarray( + np.random.randint(0, 256, (N, H, W, C), dtype=np.uint8) + ) + outputTensor = cupy.asarray(np.random.randint(0, 256, (N, H, W, C), dtype=np.uint8)) # Perform deep copy - inputTensor_copy = inputTensor.clone() + inputTensor_copy = inputTensor.copy() - inTensor = cvcuda.as_tensor(inputTensor.data, "NHWC") - inTensorTmp = cvcuda.as_tensor(inputTensorTmp.data, "NHWC") - outTensor = cvcuda.as_tensor(outputTensor.data, "NHWC") + inTensor = cvcuda.as_tensor(inputTensor, "NHWC") + inTensorTmp = cvcuda.as_tensor(inputTensorTmp, "NHWC") + outTensor = cvcuda.as_tensor(outputTensor, "NHWC") for _ in range(Loop): # put a bunch of work on stream 1 @@ -156,9 +173,49 @@ def test_operator_changing_stream_loaded(): cvcuda.flip_into( inTensorTmp, inTensor, -1, stream=stream1 ) # output x/y flipped + stream2.wait_stream(stream1) cvcuda.flip_into( outTensor, inTensorTmp, -1, stream=stream2 ) # output y/y flipped - final_out = torch.as_tensor(outTensor.cuda()).cpu() - assert torch.equal(final_out, inputTensor_copy.cpu()) + stream2.sync() + final_out = cupy.asarray(outTensor.cuda()).get() + assert np.all(final_out == inputTensor_copy.get()) + + +def test_wait_stream_self(): + stream = cvcuda.Stream() + # Waiting on yourself must be a no-op, not a deadlock. + stream.wait_stream(stream) + + +def test_wait_stream_fanout(): + N, H, W, C = 2, 64, 64, 3 + stream1 = cvcuda.Stream() + stream2 = cvcuda.Stream() + stream3 = cvcuda.Stream() + + inputTensor = cupy.asarray(np.random.randint(0, 256, (N, H, W, C), dtype=np.uint8)) + scratchTensor = cupy.asarray(np.zeros((N, H, W, C), dtype=np.uint8)) + outputTensor2 = cupy.asarray(np.zeros((N, H, W, C), dtype=np.uint8)) + outputTensor3 = cupy.asarray(np.zeros((N, H, W, C), dtype=np.uint8)) + inputTensor_copy = inputTensor.copy() + + inTensor = cvcuda.as_tensor(inputTensor, "NHWC") + scratch = cvcuda.as_tensor(scratchTensor, "NHWC") + outTensor2 = cvcuda.as_tensor(outputTensor2, "NHWC") + outTensor3 = cvcuda.as_tensor(outputTensor3, "NHWC") + + # stream1 writes a flipped version into scratch + cvcuda.flip_into(scratch, inTensor, -1, stream=stream1) + # stream2 and stream3 each wait for stream1, then double-flip back to the original + stream2.wait_stream(stream1) + stream3.wait_stream(stream1) + cvcuda.flip_into(outTensor2, scratch, -1, stream=stream2) + cvcuda.flip_into(outTensor3, scratch, -1, stream=stream3) + + stream2.sync() + stream3.sync() + + assert np.all(cupy.asarray(outTensor2.cuda()).get() == inputTensor_copy.get()) + assert np.all(cupy.asarray(outTensor3.cuda()).get() == inputTensor_copy.get()) diff --git a/tests/cvcuda/python/test_multi_threading.py b/tests/cvcuda/python/test_multi_threading.py index 1ace3fdfe..f152b7864 100644 --- a/tests/cvcuda/python/test_multi_threading.py +++ b/tests/cvcuda/python/test_multi_threading.py @@ -13,10 +13,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -import cvcuda - import numpy as np import pytest as t + +import cvcuda import cvcuda_util as util diff --git a/tests/cvcuda/python/test_nvtx_markers.py b/tests/cvcuda/python/test_nvtx_markers.py new file mode 100644 index 000000000..00cbac4b5 --- /dev/null +++ b/tests/cvcuda/python/test_nvtx_markers.py @@ -0,0 +1,255 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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 ctypes +import os +import re +from collections import defaultdict +from pathlib import Path + +import cupy +import cvcuda +import numpy as np +import pytest + +import cvcuda_util as util + + +ROOT = Path(__file__).resolve().parents[3] +PYTHON_OPERATOR_DIR = ROOT / "python" / "mod_cvcuda" / "operators" +C_API_INCLUDE_DIR = ROOT / "src" / "cvcuda" / "include" / "cvcuda" +C_API_SOURCE_DIR = ROOT / "src" / "cvcuda" + +PYTHON_BINDING_RE = re.compile(r'\bm\.def\(\s*"([^"]+)"') +C_API_DECLARATION_RE = re.compile( + r"\bCVCUDA_PUBLIC\s+NVCVStatus\s+(cvcuda[A-Za-z0-9]+Submit)\s*\(" +) +C_API_DEFINITION_RE = re.compile( + r"CVCUDA_DEFINE_API\(\s*[^,]+,\s*[^,]+,\s*NVCVStatus\s*,\s*" + r"(cvcuda[A-Za-z0-9]+Submit)\s*," +) + + +def _load_probe(): + path = os.environ.get("NVTX_INJECTION64_PATH") + if not path or not os.path.exists(path): + return None + lib = ctypes.CDLL(path) + lib.CvcudaNvtxProbe_Reset.restype = None + lib.CvcudaNvtxProbe_Count.restype = ctypes.c_uint + lib.CvcudaNvtxProbe_Name.restype = ctypes.c_char_p + lib.CvcudaNvtxProbe_Name.argtypes = [ctypes.c_uint] + return lib + + +_PROBE = _load_probe() +requires_probe = pytest.mark.skipif( + _PROBE is None, + reason="NVTX injection probe not available (NVTX_INJECTION64_PATH unset)", +) +requires_python_sources = pytest.mark.skipif( + not PYTHON_OPERATOR_DIR.is_dir(), + reason="Python operator sources are unavailable in installed-package tests", +) +requires_c_api_sources = pytest.mark.skipif( + not C_API_INCLUDE_DIR.is_dir() or not C_API_SOURCE_DIR.is_dir(), + reason="C API sources are unavailable in installed-package tests", +) + + +def _reset_probe(): + _PROBE.CvcudaNvtxProbe_Reset() + + +def _captured_ranges(): + count = _PROBE.CvcudaNvtxProbe_Count() + return [ + _PROBE.CvcudaNvtxProbe_Name(i).decode("utf-8", "replace") for i in range(count) + ] + + +RNG = np.random.default_rng(0) + + +def _tensor(shape=(2, 16, 24, 3), dtype=np.uint8, layout="NHWC", max_random=255): + return util.create_tensor(shape, dtype, layout, max_random=max_random, rng=RNG) + + +def _flip(): + cvcuda.flip(_tensor(), 0) + + +def _channelreorder_tensor(): + cvcuda.channelreorder(_tensor(), [2, 1, 0]) + + +def _public_operator_names(): + names = set(dir(cvcuda)) + return { + name + for name in names + if not name.startswith("_") + and callable(getattr(cvcuda, name)) + and f"{name}_into" in names + } + + +def _source_matches(paths, pattern): + matches = defaultdict(list) + for path in paths: + source = path.read_text(encoding="utf-8") + for match in pattern.finditer(source): + matches[match.group(1)].append((path, source, match)) + return matches + + +def _location(path, source, offset): + line = source.count("\n", 0, offset) + 1 + return f"{path.relative_to(ROOT)}:{line}" + + +@requires_python_sources +def test_all_python_operator_bindings_are_instrumented(): + """Every overload of every public operator must use its matching Python NVTX range.""" + sources = sorted(PYTHON_OPERATOR_DIR.glob("Op*.cpp")) + assert sources, f"no Python operator sources found under {PYTHON_OPERATOR_DIR}" + bindings = _source_matches(sources, PYTHON_BINDING_RE) + operator_names = _public_operator_names() + expected_bindings = operator_names | {f"{name}_into" for name in operator_names} + + missing = sorted(expected_bindings - bindings.keys()) + untraced = [] + for name in sorted(expected_bindings & bindings.keys()): + expected_trace = re.compile( + rf'\s*,\s*NvtxTrace\(\s*"cvcuda\.{re.escape(name)}"\s*,' + ) + for path, source, match in bindings[name]: + if not expected_trace.match(source, match.end()): + untraced.append(_location(path, source, match.start())) + + assert not missing and not untraced, ( + f"missing operator bindings: {missing}; " + f"bindings without their matching NvtxTrace: {untraced}" + ) + + +@requires_c_api_sources +def test_all_c_api_submit_entries_are_instrumented(): + """Every public submit API must push its matching range before executing its body.""" + declaration_sources = sorted(C_API_INCLUDE_DIR.glob("Op*.h")) + definition_sources = sorted(C_API_SOURCE_DIR.glob("*.cpp")) + assert declaration_sources, f"no C API headers found under {C_API_INCLUDE_DIR}" + assert definition_sources, f"no C API sources found under {C_API_SOURCE_DIR}" + declarations = _source_matches(declaration_sources, C_API_DECLARATION_RE) + definitions = _source_matches(definition_sources, C_API_DEFINITION_RE) + + missing_definitions = sorted(declarations.keys() - definitions.keys()) + undeclared_definitions = sorted(definitions.keys() - declarations.keys()) + untraced = [] + for name in sorted(declarations.keys() & definitions.keys()): + for path, source, match in definitions[name]: + body_start = source.find("{", match.end()) + expected_range = re.compile( + rf'\s*CVCUDA_NVTX_RANGE\(\s*"{re.escape(name)}"\s*\)\s*;' + ) + if body_start < 0 or not expected_range.match(source, body_start + 1): + untraced.append(_location(path, source, match.start())) + + assert not missing_definitions and not undeclared_definitions and not untraced, ( + f"submit declarations without definitions: {missing_definitions}; " + f"submit definitions without declarations: {undeclared_definitions}; " + f"submit definitions without an entry range: {untraced}" + ) + + +@requires_probe +def test_operator_python_range_wraps_submit(): + """An injected probe observes the Python range followed by the C-API submit range.""" + _reset_probe() + _flip() + captured = _captured_ranges() + assert ( + "cvcuda.flip" in captured + ), f"missing Python operator range. Captured: {captured}" + assert "cvcudaFlipSubmit" in captured, ( + "NVTX injection probe loaded but did not capture the submit range; NVTX may have " + f"initialized before the injection library was registered. Captured: {captured}" + ) + assert captured.index("cvcuda.flip") < captured.index( + "cvcudaFlipSubmit" + ), f"Python range must wrap the submit range. Captured: {captured}" + + +@requires_probe +def test_channelreorder_tensor_submit_marker(): + _reset_probe() + _channelreorder_tensor() + assert "cvcudaChannelReorderSubmit" in _captured_ranges() + + +@requires_probe +def test_stream_methods_emit_markers(): + """Stream methods and functions push their own NVTX ranges.""" + _reset_probe() + stream = cvcuda.Stream() + with stream: + _flip() + stream.sync() + cvcuda.Stream.default.wait_stream(stream) + captured = _captured_ranges() + for expected in ( + "cvcuda.Stream.__enter__", + "cvcuda.Stream.__exit__", + "cvcuda.Stream.sync", + "cvcuda.Stream.wait_stream", + ): + assert ( + expected in captured + ), f"missing stream range '{expected}'. Captured: {captured}" + + +@requires_probe +def test_container_and_transfer_markers(): + """Container factories, transfers, and stream creation push their own NVTX ranges.""" + _reset_probe() + + cvcuda.Stream() + + tensor = _tensor() + tensor.cuda() + buf = cupy.zeros((2, 16, 24, 3), dtype=cupy.uint8) + cvcuda.as_tensor(buf, "NHWC") + cvcuda.as_tensors([buf]) + cvcuda.reshape(tensor, tensor.shape, tensor.layout) + + image = util.create_image((24, 16), cvcuda.Format.RGB8, max_random=255, rng=RNG) + image.cuda() + image.cpu() + cvcuda.as_image(image.cuda(), cvcuda.Format.RGB8) + cvcuda.as_images([image.cuda()]) + + captured = _captured_ranges() + for expected in ( + "cvcuda.Stream.create", + "cvcuda.Tensor.cuda", + "cvcuda.as_tensor", + "cvcuda.as_tensors", + "cvcuda.reshape", + "cvcuda.Image.cuda", + "cvcuda.Image.cpu", + "cvcuda.as_image", + "cvcuda.as_images", + ): + assert expected in captured, f"missing range '{expected}'. Captured: {captured}" diff --git a/tests/cvcuda/python/test_opadaptivethreshold.py b/tests/cvcuda/python/test_opadaptivethreshold.py index d5cc529c5..3156e1a46 100644 --- a/tests/cvcuda/python/test_opadaptivethreshold.py +++ b/tests/cvcuda/python/test_opadaptivethreshold.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,17 +13,18 @@ # See the License for the specific language governing permissions and # limitations under the License. -import torch # noqa(F401) import cvcuda -import pytest as t +import pytest import numpy as np + import cvcuda_util as util +import cvcuda_tools as cv_tools RNG = np.random.default_rng(0) -@t.mark.parametrize( +@pytest.mark.parametrize( "tensor_args, adaptive_method, threshold_type", [ ( @@ -51,6 +52,16 @@ cvcuda.AdaptiveThresholdType.MEAN_C, cvcuda.ThresholdType.BINARY, ), + ( + ((4, 1, 360, 640), cvcuda.Type.U8, "NCHW"), + cvcuda.AdaptiveThresholdType.MEAN_C, + cvcuda.ThresholdType.BINARY, + ), + ( + ((1, 360, 640), cvcuda.Type.U8, "CHW"), + cvcuda.AdaptiveThresholdType.GAUSSIAN_C, + cvcuda.ThresholdType.BINARY_INV, + ), ], ) def test_op_adaptivethreshold(tensor_args, adaptive_method, threshold_type): @@ -98,7 +109,7 @@ def test_op_adaptivethreshold(tensor_args, adaptive_method, threshold_type): assert tmp is out -@t.mark.parametrize( +@pytest.mark.parametrize( "num_images, img_size, adaptive_method, threshold_type, max_block_size", [ ( @@ -139,13 +150,19 @@ def test_op_adaptivethresholdvarshape( num_images, cvcuda.Format.U8, size=img_size, max_random=256, rng=RNG ) - block_size = util.create_tensor( - (num_images), - np.int32, + max_odd_block_size = ( + max_block_size if max_block_size % 2 == 1 else max_block_size - 1 + ) + block_size = util.to_cvcuda_tensor( + RNG.integers( + 1, + (max_odd_block_size + 1) // 2, + size=(num_images), + dtype=np.int32, + ) + * 2 + + 1, "N", - max_random=max_block_size, - rng=RNG, - transform_dist=util.dist_odd, ) max_value = util.create_tensor( @@ -190,3 +207,45 @@ def test_op_adaptivethresholdvarshape( assert out.capacity == input.capacity assert out.uniqueformat == input.uniqueformat assert out.maxsize == input.maxsize + + +def _adaptivethreshold_params(dtype, layout, channels): + return { + "max_value": 127.0, + "adaptive_method": cvcuda.AdaptiveThresholdType.MEAN_C, + "threshold_type": cvcuda.ThresholdType.BINARY, + "block_size": 3, + "c": 2, + } + + +def _adaptivethreshold_varshape_params(dtype, layout, channels): + return { + "max_value": util.to_cvcuda_tensor( + np.array([127.0, 127.0], dtype=np.float64), "N" + ), + "adaptive_method": cvcuda.AdaptiveThresholdType.MEAN_C, + "threshold_type": cvcuda.ThresholdType.BINARY, + "max_block_size": 3, + "block_size": util.to_cvcuda_tensor(np.array([3, 3], dtype=np.int32), "N"), + "c": util.to_cvcuda_tensor(np.array([2.0, 2.0], dtype=np.float64), "N"), + } + + +globals().update( + cv_tools.make_op_tests( + name="adaptivethreshold", + runner_info=[ + ("tensor", cvcuda.adaptivethreshold, _adaptivethreshold_params), + ( + "image_batch", + cvcuda.adaptivethreshold, + _adaptivethreshold_varshape_params, + ), + ], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 1), + supported_dtypes={cvcuda.Type.U8}, + supported_layouts={"NHWC", "HWC", "NCHW", "CHW"}, + supported_channels={1}, + ) +) diff --git a/tests/cvcuda/python/test_opadjust_hue.py b/tests/cvcuda/python/test_opadjust_hue.py new file mode 100644 index 000000000..506639e46 --- /dev/null +++ b/tests/cvcuda/python/test_opadjust_hue.py @@ -0,0 +1,120 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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 cvcuda + +import pytest +import numpy as np + +import cvcuda_tools as cv_tools +import cvcuda_util as util + +RNG = np.random.default_rng(0) + + +@pytest.mark.parametrize( + "tensor_params, hue", + [ + (((5, 16, 23, 3), np.uint8, "NHWC"), 0.25), + (((4, 9, 3), np.uint8, "HWC"), -0.3), + (((3, 3, 88, 13), np.uint8, "NCHW"), 0.5), + (((2, 3, 16, 23), np.float32, "NCHW"), 0.1), + (((8, 8, 1), np.float32, "HWC"), 0.25), + (((3, 12, 12, 1), np.uint8, "NHWC"), -0.5), + ], +) +def test_op_adjusthue(tensor_params, hue): + src = cvcuda.Tensor(*tensor_params) + + out = cvcuda.adjust_hue(src, hue) + assert out.layout == src.layout + assert out.shape == src.shape + assert out.dtype == src.dtype + + stream = cvcuda.Stream() + out = cvcuda.Tensor(src.shape, src.dtype, src.layout) + tmp = cvcuda.adjust_hue_into(src=src, dst=out, hue=hue, stream=stream) + assert tmp is out + assert out.layout == src.layout + assert out.shape == src.shape + assert out.dtype == src.dtype + + +@pytest.mark.parametrize( + "num_images, img_format, img_size, max_pixel, hue", + [ + (10, cvcuda.Format.RGB8, (123, 321), 256, 0.25), + (7, cvcuda.Format.RGBf32, (62, 35), 1.0, -0.4), + (4, cvcuda.Format.U8, (26, 52), 256, 0.25), + ], +) +def test_op_adjusthue_varshape(num_images, img_format, img_size, max_pixel, hue): + src = util.create_image_batch( + num_images, img_format, size=img_size, max_random=max_pixel, rng=RNG + ) + + out = cvcuda.adjust_hue(src, hue) + assert len(out) == len(src) + assert out.capacity == src.capacity + assert out.uniqueformat == src.uniqueformat + assert out.maxsize == src.maxsize + + stream = cvcuda.Stream() + out = util.clone_image_batch(src) + tmp = cvcuda.adjust_hue_into(src=src, dst=out, hue=hue, stream=stream) + assert tmp is out + assert len(out) == len(src) + assert out.capacity == src.capacity + + +def test_op_adjusthue_negative_dtype(): + # uint16 is outside the supported dtype set (u8/f32) and must be rejected. + src = cvcuda.Tensor((1, 16, 16, 3), np.uint16, "NHWC") + with pytest.raises(RuntimeError): + cvcuda.adjust_hue(src, 0.25) + + +def test_op_adjusthue_negative_channels(): + # 4-channel (RGBA) is outside the supported channel set (1/3) and must be rejected. + src = cvcuda.Tensor((1, 16, 16, 4), np.uint8, "NHWC") + with pytest.raises(RuntimeError): + cvcuda.adjust_hue(src, 0.25) + + +def test_op_adjusthue_negative_range(): + src = cvcuda.Tensor((1, 16, 16, 3), np.uint8, "NHWC") + with pytest.raises(RuntimeError): + cvcuda.adjust_hue(src, 0.75) + with pytest.raises(RuntimeError): + cvcuda.adjust_hue(src, -0.75) + + +def _adjust_hue_params(dtype, layout, channels): + return {"hue": 0.25} + + +globals().update( + cv_tools.make_op_tests( + name="adjust_hue", + runner_info=[ + ("tensor", cvcuda.adjust_hue, _adjust_hue_params), + ("image_batch", cvcuda.adjust_hue, _adjust_hue_params), + ], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 3), + supported_dtypes={cvcuda.Type.U8, cvcuda.Type.F32}, + supported_layouts={"NHWC", "HWC", "NCHW", "CHW"}, + supported_channels={1, 3}, + ) +) diff --git a/tests/cvcuda/python/test_opadjust_saturation.py b/tests/cvcuda/python/test_opadjust_saturation.py new file mode 100644 index 000000000..23d4af021 --- /dev/null +++ b/tests/cvcuda/python/test_opadjust_saturation.py @@ -0,0 +1,124 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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 cvcuda + +import pytest +import numpy as np + +import cvcuda_tools as cv_tools +import cvcuda_util as util + +RNG = np.random.default_rng(0) + + +@pytest.mark.parametrize( + "tensor_params, saturation", + [ + (((5, 16, 23, 3), np.uint8, "NHWC"), 0.5), + (((4, 9, 3), np.uint8, "HWC"), 0.0), + (((3, 3, 88, 13), np.uint8, "NCHW"), 1.7), + (((2, 3, 16, 23), np.float32, "NCHW"), 0.5), + (((8, 8, 1), np.float32, "HWC"), 0.5), + (((3, 12, 12, 1), np.uint8, "NHWC"), 0.3), + ], +) +def test_op_adjustsaturation(tensor_params, saturation): + src = cvcuda.Tensor(*tensor_params) + + out = cvcuda.adjust_saturation(src, saturation) + assert out.layout == src.layout + assert out.shape == src.shape + assert out.dtype == src.dtype + + stream = cvcuda.Stream() + out = cvcuda.Tensor(src.shape, src.dtype, src.layout) + tmp = cvcuda.adjust_saturation_into( + src=src, dst=out, saturation=saturation, stream=stream + ) + assert tmp is out + assert out.layout == src.layout + assert out.shape == src.shape + assert out.dtype == src.dtype + + +@pytest.mark.parametrize( + "num_images, img_format, img_size, max_pixel, saturation", + [ + (10, cvcuda.Format.RGB8, (123, 321), 256, 0.5), + (7, cvcuda.Format.RGBf32, (62, 35), 1.0, 1.4), + (4, cvcuda.Format.U8, (26, 52), 256, 0.5), + ], +) +def test_op_adjustsaturation_varshape( + num_images, img_format, img_size, max_pixel, saturation +): + src = util.create_image_batch( + num_images, img_format, size=img_size, max_random=max_pixel, rng=RNG + ) + + out = cvcuda.adjust_saturation(src, saturation) + assert len(out) == len(src) + assert out.capacity == src.capacity + assert out.uniqueformat == src.uniqueformat + assert out.maxsize == src.maxsize + + stream = cvcuda.Stream() + out = util.clone_image_batch(src) + tmp = cvcuda.adjust_saturation_into( + src=src, dst=out, saturation=saturation, stream=stream + ) + assert tmp is out + assert len(out) == len(src) + assert out.capacity == src.capacity + + +def test_op_adjustsaturation_negative_dtype(): + # uint16 is outside the supported dtype set (u8/f32) and must be rejected. + src = cvcuda.Tensor((1, 16, 16, 3), np.uint16, "NHWC") + with pytest.raises(RuntimeError): + cvcuda.adjust_saturation(src, 0.5) + + +def test_op_adjustsaturation_negative_channels(): + # 4-channel (RGBA) is outside the supported channel set (1/3) and must be rejected. + src = cvcuda.Tensor((1, 16, 16, 4), np.uint8, "NHWC") + with pytest.raises(RuntimeError): + cvcuda.adjust_saturation(src, 0.5) + + +def test_op_adjustsaturation_negative_saturation(): + src = cvcuda.Tensor((1, 16, 16, 3), np.uint8, "NHWC") + with pytest.raises(RuntimeError): + cvcuda.adjust_saturation(src, -1.0) + + +def _adjust_saturation_params(dtype, layout, channels): + return {"saturation": 0.5} + + +globals().update( + cv_tools.make_op_tests( + name="adjust_saturation", + runner_info=[ + ("tensor", cvcuda.adjust_saturation, _adjust_saturation_params), + ("image_batch", cvcuda.adjust_saturation, _adjust_saturation_params), + ], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 3), + supported_dtypes={cvcuda.Type.U8, cvcuda.Type.F32}, + supported_layouts={"NHWC", "HWC", "NCHW", "CHW"}, + supported_channels={1, 3}, + ) +) diff --git a/tests/cvcuda/python/test_opadjustcontrast.py b/tests/cvcuda/python/test_opadjustcontrast.py new file mode 100644 index 000000000..cc9e31936 --- /dev/null +++ b/tests/cvcuda/python/test_opadjustcontrast.py @@ -0,0 +1,101 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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 cvcuda + +import pytest +import numpy as np + +import cvcuda_tools as cv_tools +import cvcuda_util as util + +RNG = np.random.default_rng(0) + + +@pytest.mark.parametrize( + "tensor_params, contrast_factor", + [ + (((5, 16, 23, 3), np.uint8, "NHWC"), 1.5), + (((4, 9, 3), np.uint8, "HWC"), 0.5), + (((3, 88, 13, 1), np.uint8, "NHWC"), 2.0), + (((2, 3, 16, 23), np.float32, "NCHW"), 1.25), + (((1, 8, 8), np.float32, "CHW"), 0.0), + ], +) +def test_op_adjust_contrast(tensor_params, contrast_factor): + src = cvcuda.Tensor(*tensor_params) + + out = cvcuda.adjust_contrast(src, contrast_factor) + assert (out.layout, out.shape, out.dtype) == (src.layout, src.shape, src.dtype) + + stream = cvcuda.Stream() + out = cvcuda.Tensor(src.shape, src.dtype, src.layout) + tmp = cvcuda.adjust_contrast_into( + src=src, dst=out, contrast_factor=contrast_factor, stream=stream + ) + assert tmp is out + assert (out.layout, out.shape, out.dtype) == (src.layout, src.shape, src.dtype) + + +@pytest.mark.parametrize( + "num_images, img_format, img_size, max_pixel, contrast_factor", + [ + (10, cvcuda.Format.RGB8, (123, 321), 256, 1.5), + (7, cvcuda.Format.RGBf32, (62, 35), 1.0, 0.75), + (1, cvcuda.Format.U8, (33, 48), 256, 2.0), + ], +) +def test_op_adjust_contrast_varshape( + num_images, img_format, img_size, max_pixel, contrast_factor +): + src = util.create_image_batch( + num_images, img_format, size=img_size, max_random=max_pixel, rng=RNG + ) + + out = cvcuda.adjust_contrast(src, contrast_factor) + assert (len(out), out.capacity, out.uniqueformat, out.maxsize) == ( + len(src), + src.capacity, + src.uniqueformat, + src.maxsize, + ) + + stream = cvcuda.Stream() + out = util.clone_image_batch(src) + tmp = cvcuda.adjust_contrast_into( + src=src, dst=out, contrast_factor=contrast_factor, stream=stream + ) + assert tmp is out + assert (len(out), out.capacity) == (len(src), src.capacity) + + +def _adjust_contrast_params(dtype, layout, channels, contrast_factor=1.5): + return {"contrast_factor": contrast_factor} + + +globals().update( + cv_tools.make_op_tests( + name="adjust_contrast", + runner_info=[ + ("tensor", cvcuda.adjust_contrast, _adjust_contrast_params), + ("image_batch", cvcuda.adjust_contrast, _adjust_contrast_params), + ], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 3), + supported_dtypes={cvcuda.Type.U8, cvcuda.Type.F32}, + supported_layouts={"NHWC", "HWC", "NCHW", "CHW"}, + supported_channels={1, 3}, + extra_params_negative={"contrast_factor": {-0.5}}, + ) +) diff --git a/tests/cvcuda/python/test_opadjustsharpness.py b/tests/cvcuda/python/test_opadjustsharpness.py new file mode 100644 index 000000000..9c5e28b3f --- /dev/null +++ b/tests/cvcuda/python/test_opadjustsharpness.py @@ -0,0 +1,117 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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 cvcuda + +import pytest +import numpy as np +import cvcuda_tools as cv_tools + +import cvcuda_util as util + +RNG = np.random.default_rng(0) + + +@pytest.mark.parametrize( + "tensor_params, sharpness_factor", + [ + (((5, 16, 23, 4), np.uint8, "NHWC"), 2.0), + (((4, 9, 3), np.uint8, "HWC"), 0.0), + (((3, 88, 13, 1), np.uint16, "NHWC"), 1.5), + (((2, 4, 16, 23), np.float32, "NCHW"), 0.5), + (((3, 8, 8), np.float32, "CHW"), 2.0), + ], +) +def test_op_adjust_sharpness(tensor_params, sharpness_factor): + input = cvcuda.Tensor(*tensor_params) + + out = cvcuda.adjust_sharpness(input, sharpness_factor) + assert out.layout == input.layout + assert out.shape == input.shape + assert out.dtype == input.dtype + + stream = cvcuda.Stream() + out = cvcuda.Tensor(input.shape, input.dtype, input.layout) + tmp = cvcuda.adjust_sharpness_into( + src=input, dst=out, sharpness_factor=sharpness_factor, stream=stream + ) + assert tmp is out + assert out.layout == input.layout + assert out.shape == input.shape + assert out.dtype == input.dtype + + +@pytest.mark.parametrize( + "num_images, img_format, img_size, max_pixel, sharpness_factor", + [ + (10, cvcuda.Format.RGB8, (123, 321), 256, 2.0), + (7, cvcuda.Format.RGBf32, (62, 35), 1.0, 0.5), + (1, cvcuda.Format.U16, (33, 48), 1234, 1.5), + (4, cvcuda.Format.RGBA8, (26, 52), 256, 0.0), + ], +) +def test_op_adjust_sharpness_varshape( + num_images, img_format, img_size, max_pixel, sharpness_factor +): + input = util.create_image_batch( + num_images, img_format, size=img_size, max_random=max_pixel, rng=RNG + ) + + out = cvcuda.adjust_sharpness(input, sharpness_factor) + assert len(out) == len(input) + assert out.capacity == input.capacity + assert out.uniqueformat == input.uniqueformat + assert out.maxsize == input.maxsize + + stream = cvcuda.Stream() + out = util.clone_image_batch(input) + tmp = cvcuda.adjust_sharpness_into( + src=input, dst=out, sharpness_factor=sharpness_factor, stream=stream + ) + assert tmp is out + assert len(out) == len(input) + assert out.capacity == input.capacity + + +def test_op_adjust_sharpness_negative_dtype(): + # float16 is outside the supported dtype set (u8/u16/f32) and must be rejected. + input = cvcuda.Tensor((1, 16, 16, 3), np.float16, "NHWC") + with pytest.raises(Exception): + cvcuda.adjust_sharpness(input, 2.0) + + +def test_op_adjust_sharpness_negative_factor(): + input = cvcuda.Tensor((1, 16, 16, 3), np.uint8, "NHWC") + with pytest.raises(Exception): + cvcuda.adjust_sharpness(input, -0.1) + + +def _adjust_sharpness_params(dtype, layout, channels): + return {"sharpness_factor": 2.0} + + +globals().update( + cv_tools.make_op_tests( + name="adjustsharpness", + runner_info=[ + ("tensor", cvcuda.adjust_sharpness, _adjust_sharpness_params), + ("image_batch", cvcuda.adjust_sharpness, _adjust_sharpness_params), + ], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 3), + supported_dtypes={cvcuda.Type.U8, cvcuda.Type.U16, cvcuda.Type.F32}, + supported_layouts={"NHWC", "HWC", "NCHW", "CHW"}, + supported_channels={1, 3, 4}, + ) +) diff --git a/tests/cvcuda/python/test_opadvcvtcolor.py b/tests/cvcuda/python/test_opadvcvtcolor.py index b32db6611..4bf7ab905 100644 --- a/tests/cvcuda/python/test_opadvcvtcolor.py +++ b/tests/cvcuda/python/test_opadvcvtcolor.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,11 +15,12 @@ import cvcuda -import pytest as t +import pytest import numpy as np +import cvcuda_tools as cv_tools -@t.mark.parametrize( +@pytest.mark.parametrize( "input,dtype,code", [ (((1, 1, 1, 3), np.uint8, "NHWC"), np.uint8, cvcuda.ColorConversion.BGR2YUV), @@ -31,6 +32,8 @@ cvcuda.ColorConversion.YUV2RGB, ), (((162, 231, 3), np.uint8, "HWC"), np.uint8, cvcuda.ColorConversion.YUV2RGB), + (((1, 3, 19, 23), np.uint8, "NCHW"), np.uint8, cvcuda.ColorConversion.RGB2YUV), + (((3, 17, 21), np.uint8, "CHW"), np.uint8, cvcuda.ColorConversion.YUV2BGR), ], ) def test_op_advcvtcolor(input, dtype, code): @@ -56,7 +59,7 @@ def test_op_advcvtcolor(input, dtype, code): assert out.dtype == input.dtype -@t.mark.parametrize( +@pytest.mark.parametrize( "input,dtype,code", [ # yuv must be even and Nv12/21 yuv must at least contain 3 rows and 2 columns @@ -85,6 +88,16 @@ def test_op_advcvtcolor(input, dtype, code): np.uint8, cvcuda.ColorConversion.YUV2RGB_NV21, ), + ( + ((1, 3, 24, 32), np.uint8, "NCHW"), + np.uint8, + cvcuda.ColorConversion.YUV2BGR_NV12, + ), + ( + ((3, 20, 28), np.uint8, "CHW"), + np.uint8, + cvcuda.ColorConversion.YUV2RGB_NV21, + ), ], ) def test_op_advcvtcolor_FromNV(input, dtype, code): @@ -92,12 +105,20 @@ def test_op_advcvtcolor_FromNV(input, dtype, code): # scale input size to fit NV12/21 if conversion is from NV12/21 and set c to 1 if input[2] == "HWC": inputNV = (int((input[0][0] * 3) / 2), input[0][1], 1), input[1], input[2] - else: + elif input[2] == "NHWC": inputNV = ( (input[0][0], int((input[0][1] * 3) / 2), input[0][2], 1), input[1], input[2], ) + elif input[2] == "CHW": + inputNV = (1, int((input[0][1] * 3) / 2), input[0][2]), input[1], input[2] + else: + inputNV = ( + (input[0][0], 1, int((input[0][2] * 3) / 2), input[0][3]), + input[1], + input[2], + ) inputTensor = cvcuda.Tensor(*inputNV) color_specs = [ @@ -117,7 +138,7 @@ def test_op_advcvtcolor_FromNV(input, dtype, code): assert outTensor.dtype == inputTensor.dtype -@t.mark.parametrize( +@pytest.mark.parametrize( "input,dtype,code", [ ( @@ -141,6 +162,12 @@ def test_op_advcvtcolor_FromNV(input, dtype, code): cvcuda.ColorConversion.RGB2YUV_NV21, ), (((2, 30, 3), np.uint8, "HWC"), np.uint8, cvcuda.ColorConversion.RGB2YUV_NV21), + ( + ((1, 3, 24, 32), np.uint8, "NCHW"), + np.uint8, + cvcuda.ColorConversion.BGR2YUV_NV12, + ), + (((3, 20, 28), np.uint8, "CHW"), np.uint8, cvcuda.ColorConversion.RGB2YUV_NV21), ], ) def test_op_advcvtcolor_toNV(input, dtype, code): @@ -161,3 +188,152 @@ def test_op_advcvtcolor_toNV(input, dtype, code): cvcuda.advcvtcolor_into(outTensor, inputTensor, code, spec) assert outTensor.layout == inputTensor.layout assert outTensor.dtype == inputTensor.dtype + + +_valid_conversions: list[tuple[cvcuda.ColorConversion, int, bool]] = [ + # Interleaved 444: RGB/BGR <-> YUV (3-ch in, 3-ch out) + (cvcuda.ColorConversion.RGB2YUV, 3, False), + (cvcuda.ColorConversion.BGR2YUV, 3, False), + (cvcuda.ColorConversion.YUV2RGB, 3, False), + (cvcuda.ColorConversion.YUV2BGR, 3, False), + # RGB/BGR -> NV12/NV21 (3 or 4-ch in) + (cvcuda.ColorConversion.RGB2YUV_NV12, 3, False), + (cvcuda.ColorConversion.RGB2YUV_NV12, 4, False), + (cvcuda.ColorConversion.RGB2YUV_NV21, 3, False), + (cvcuda.ColorConversion.RGB2YUV_NV21, 4, False), + (cvcuda.ColorConversion.BGR2YUV_NV12, 3, False), + (cvcuda.ColorConversion.BGR2YUV_NV12, 4, False), + (cvcuda.ColorConversion.BGR2YUV_NV21, 3, False), + (cvcuda.ColorConversion.BGR2YUV_NV21, 4, False), + # NV12/NV21 -> RGB/BGR (1-ch in with H*3/2, 3 or 4-ch out) + (cvcuda.ColorConversion.YUV2RGB_NV12, 1, True), + (cvcuda.ColorConversion.YUV2BGR_NV12, 1, True), + (cvcuda.ColorConversion.YUV2RGB_NV21, 1, True), + (cvcuda.ColorConversion.YUV2BGR_NV21, 1, True), +] +# Curated subset of unsupported (code, channels) pairs. We cannot derive +# this automatically from _valid_conversions because some untested conversion/ +# channel combos crash the operator instead of raising an error. +_invalid_conversions: list[tuple[cvcuda.ColorConversion, int, bool]] = [ + # 444 codes only support 3-ch (invalid: 1, 2, 4) + (cvcuda.ColorConversion.RGB2YUV, 1, False), + (cvcuda.ColorConversion.RGB2YUV, 2, False), + (cvcuda.ColorConversion.RGB2YUV, 4, False), + (cvcuda.ColorConversion.BGR2YUV, 1, False), + (cvcuda.ColorConversion.BGR2YUV, 2, False), + (cvcuda.ColorConversion.BGR2YUV, 4, False), + (cvcuda.ColorConversion.YUV2RGB, 1, False), + (cvcuda.ColorConversion.YUV2RGB, 2, False), + (cvcuda.ColorConversion.YUV2RGB, 4, False), + (cvcuda.ColorConversion.YUV2BGR, 1, False), + (cvcuda.ColorConversion.YUV2BGR, 2, False), + (cvcuda.ColorConversion.YUV2BGR, 4, False), + # To-NV codes support 3 or 4-ch (invalid: 1, 2) + (cvcuda.ColorConversion.RGB2YUV_NV12, 1, False), + (cvcuda.ColorConversion.RGB2YUV_NV12, 2, False), + (cvcuda.ColorConversion.RGB2YUV_NV21, 1, False), + (cvcuda.ColorConversion.RGB2YUV_NV21, 2, False), + (cvcuda.ColorConversion.BGR2YUV_NV12, 1, False), + (cvcuda.ColorConversion.BGR2YUV_NV12, 2, False), + (cvcuda.ColorConversion.BGR2YUV_NV21, 1, False), + (cvcuda.ColorConversion.BGR2YUV_NV21, 2, False), + # From-NV codes support 1-ch input (invalid: 2, 3, 4) + (cvcuda.ColorConversion.YUV2RGB_NV12, 2, True), + (cvcuda.ColorConversion.YUV2RGB_NV12, 3, True), + (cvcuda.ColorConversion.YUV2RGB_NV12, 4, True), + (cvcuda.ColorConversion.YUV2BGR_NV12, 2, True), + (cvcuda.ColorConversion.YUV2BGR_NV12, 3, True), + (cvcuda.ColorConversion.YUV2BGR_NV12, 4, True), + (cvcuda.ColorConversion.YUV2RGB_NV21, 2, True), + (cvcuda.ColorConversion.YUV2RGB_NV21, 3, True), + (cvcuda.ColorConversion.YUV2RGB_NV21, 4, True), + (cvcuda.ColorConversion.YUV2BGR_NV21, 2, True), + (cvcuda.ColorConversion.YUV2BGR_NV21, 3, True), + (cvcuda.ColorConversion.YUV2BGR_NV21, 4, True), +] +_supported_layouts = {"NHWC", "HWC", "NCHW", "CHW"} + + +def _create_input(channels: int, is_from_nv: bool, layout: str): + # Use even dimensions for NV12/21 compatibility + height, width = 24, 32 + if layout == "HWC": + if is_from_nv: + shape = ((height * 3) // 2, width, channels) + else: + shape = (height, width, channels) + elif layout == "NHWC": + if is_from_nv: + shape = (1, (height * 3) // 2, width, channels) + else: + shape = (1, height, width, channels) + elif layout == "CHW": + if is_from_nv: + shape = (channels, (height * 3) // 2, width) + else: + shape = (channels, height, width) + else: # NCHW + if is_from_nv: + shape = (1, channels, (height * 3) // 2, width) + else: + shape = (1, channels, height, width) + return cvcuda.Tensor(shape, np.uint8, layout) + + +def _op(code: cvcuda.ColorConversion, channels: int, is_from_nv: bool, layout: str): + cvcuda.advcvtcolor( + _create_input(channels, is_from_nv, layout), code, cvcuda.ColorSpec.BT601 + ) + + +@pytest.mark.parametrize( + "code,channels,is_from_nv", + [ + pytest.param(c, ch, nv, id=f"{c.name}-{ch}ch") + for c, ch, nv in _valid_conversions + ], +) +@pytest.mark.parametrize("layout", _supported_layouts) +def test_op_advcvtcolor_valid_conversions(code, channels, is_from_nv, layout): + _op(code, channels, is_from_nv, layout) + + +@pytest.mark.parametrize( + "code,channels,is_from_nv", + [ + pytest.param(c, ch, nv, id=f"{c.name}-{ch}ch") + for c, ch, nv in _invalid_conversions + ], +) +def test_op_advcvtcolor_invalid_conversions(code, channels, is_from_nv): + with pytest.raises(RuntimeError): + _op(code, channels, is_from_nv, "NHWC") + + +def _advcvtcolor(src: cvcuda.Tensor, spec: cvcuda.ColorSpec): + return cvcuda.advcvtcolor(src, code=cvcuda.ColorConversion.RGB2YUV, spec=spec) + + +def _advcvtcolor_params(dtype, layout, channels, spec): + return { + "spec": spec, + } + + +globals().update( + cv_tools.make_op_tests( + name="advcvtcolor", + runner_info=[("tensor", _advcvtcolor, _advcvtcolor_params)], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 3), + supported_dtypes={cvcuda.Type.U8}, + supported_layouts=_supported_layouts, + supported_channels={3}, + extra_params={ + "spec": { + cvcuda.ColorSpec.BT601, + cvcuda.ColorSpec.BT709, + cvcuda.ColorSpec.BT2020, + } + }, + ) +) diff --git a/tests/cvcuda/python/test_opautocontrast.py b/tests/cvcuda/python/test_opautocontrast.py new file mode 100644 index 000000000..ba23cff67 --- /dev/null +++ b/tests/cvcuda/python/test_opautocontrast.py @@ -0,0 +1,117 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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 cvcuda + +import pytest +import numpy as np +import cvcuda_tools as cv_tools +import cvcuda_util as util + +RNG = np.random.default_rng(0) + + +@pytest.mark.parametrize( + "input_spec", + [ + (((5, 16, 23, 4), cvcuda.Type.U8, "NHWC")), + (((2, 33, 21, 3), cvcuda.Type.U16, "NHWC")), + (((16, 23, 3), cvcuda.Type.F32, "HWC")), + (((3, 4, 12, 10), cvcuda.Type.U8, "NCHW")), + (((1, 460, 640, 1), cvcuda.Type.U8, "NHWC")), + ], +) +def test_op_autocontrast(input_spec): + + inputTensor = cvcuda.Tensor(*input_spec) + + out = cvcuda.autocontrast(inputTensor) + assert out.layout == inputTensor.layout + assert out.shape == inputTensor.shape + assert out.dtype == inputTensor.dtype + + out = cvcuda.Tensor(inputTensor.shape, inputTensor.dtype, inputTensor.layout) + tmp = cvcuda.autocontrast_into(out, inputTensor) + assert tmp is out + assert out.layout == inputTensor.layout + assert out.shape == inputTensor.shape + assert out.dtype == inputTensor.dtype + + stream = cvcuda.Stream() + out = cvcuda.autocontrast(src=inputTensor, stream=stream) + assert out.layout == inputTensor.layout + assert out.shape == inputTensor.shape + assert out.dtype == inputTensor.dtype + + tmp = cvcuda.autocontrast_into(dst=out, src=inputTensor, stream=stream) + assert tmp is out + assert out.layout == inputTensor.layout + assert out.shape == inputTensor.shape + assert out.dtype == inputTensor.dtype + + +@pytest.mark.parametrize( + "num_images, img_format, max_size", + [ + (1, cvcuda.Format.RGB8, (480, 720)), + (5, cvcuda.Format.RGBA8, (720, 480)), + (4, cvcuda.Format.RGBf32, (200, 200)), + (2, cvcuda.Format.F32, (100, 100)), + ], +) +def test_op_autocontrast_varshape(num_images, img_format, max_size): + + b_src = util.create_image_batch(num_images, img_format, max_size=max_size, rng=RNG) + + out = cvcuda.autocontrast(b_src) + assert out.uniqueformat is not None + assert out.uniqueformat == b_src.uniqueformat + assert len(out) == len(b_src) + assert out.capacity == b_src.capacity + assert all( + actual <= limit for actual, limit in zip(out.maxsize, max_size, strict=True) + ) + + tmp = cvcuda.autocontrast_into(out, b_src) + assert tmp is out + assert out.uniqueformat == b_src.uniqueformat + assert len(out) == len(b_src) + + stream = cvcuda.Stream() + out = cvcuda.autocontrast(src=b_src, stream=stream) + assert out.uniqueformat == b_src.uniqueformat + assert len(out) == len(b_src) + + tmp = cvcuda.autocontrast_into(src=b_src, dst=out, stream=stream) + assert tmp is out + assert out.uniqueformat == b_src.uniqueformat + assert len(out) == len(b_src) + + +# Standard input-contract coverage (supported/unsupported dtype, layout, and channel +# combinations). AutoContrast takes no parameters, so the param factory is empty. +globals().update( + cv_tools.make_op_tests( + name="autocontrast", + runner_info=[ + ("tensor", cvcuda.autocontrast, lambda dtype, layout, channels: {}), + ("image_batch", cvcuda.autocontrast, None), + ], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 3), + supported_dtypes={cvcuda.Type.U8, cvcuda.Type.U16, cvcuda.Type.F32}, + supported_layouts={"NHWC", "HWC", "NCHW", "CHW"}, + supported_channels={1, 3, 4}, + ) +) diff --git a/tests/cvcuda/python/test_opaverageblur.py b/tests/cvcuda/python/test_opaverageblur.py index 92f922ea6..0c79d8623 100644 --- a/tests/cvcuda/python/test_opaverageblur.py +++ b/tests/cvcuda/python/test_opaverageblur.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,14 +15,16 @@ import cvcuda -import pytest as t +import pytest import numpy as np + import cvcuda_util as util +import cvcuda_tools as cv_tools RNG = np.random.default_rng(0) -@t.mark.parametrize( +@pytest.mark.parametrize( "tensor_args, kernel_size, kernel_anchor, border", [ ( @@ -80,7 +82,7 @@ def test_op_averageblur(tensor_args, kernel_size, kernel_anchor, border): assert out.dtype == input.dtype -@t.mark.parametrize( +@pytest.mark.parametrize( "num_images, img_format, img_size, max_pixel, max_kernel_size, border", [ ( @@ -174,3 +176,45 @@ def test_op_averageblurvarshape( assert out.capacity == input.capacity assert out.uniqueformat == input.uniqueformat assert out.maxsize == input.maxsize + + +def _averageblur_params(dtype, layout, channels): + return { + "kernel_size": (3, 3), + "kernel_anchor": (-1, -1), + "border": cvcuda.Border.CONSTANT, + } + + +def _averageblur_varshape_params(dtype, layout, channels): + return { + "max_kernel_size": (5, 5), + "kernel_size": util.to_cvcuda_tensor( + np.array([[3, 3], [3, 3]], dtype=np.int32), "NC" + ), + "kernel_anchor": util.to_cvcuda_tensor( + np.array([[-1, -1], [-1, -1]], dtype=np.int32), "NC" + ), + "border": cvcuda.Border.CONSTANT, + } + + +globals().update( + cv_tools.make_op_tests( + name="averageblur", + runner_info=[ + ("tensor", cvcuda.averageblur, _averageblur_params), + ("image_batch", cvcuda.averageblur, _averageblur_varshape_params), + ], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 3), + supported_dtypes={ + cvcuda.Type.U8, + cvcuda.Type.U16, + cvcuda.Type.S16, + cvcuda.Type.S32, + cvcuda.Type.F32, + }, + supported_layouts={"NHWC", "HWC", "NCHW", "CHW"}, + supported_channels={1, 3, 4}, + ) +) diff --git a/tests/cvcuda/python/test_opbilateralfilter.py b/tests/cvcuda/python/test_opbilateralfilter.py index e5f56ea74..08075f869 100644 --- a/tests/cvcuda/python/test_opbilateralfilter.py +++ b/tests/cvcuda/python/test_opbilateralfilter.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,14 +15,16 @@ import cvcuda -import pytest as t +import pytest import numpy as np + import cvcuda_util as util +import cvcuda_tools as cv_tools RNG = np.random.default_rng(0) -@t.mark.parametrize( +@pytest.mark.parametrize( "tensor_args, diameter, sigma_color, sigma_space, border", [ ( @@ -81,7 +83,7 @@ def test_op_bilateral_filter(tensor_args, diameter, sigma_color, sigma_space, bo assert out.dtype == input.dtype -@t.mark.parametrize( +@pytest.mark.parametrize( "nimages, format, max_size, max_pixel, max_diameter, max_sc, max_ss, border", [ ( @@ -179,3 +181,45 @@ def test_op_bilateral_filtervarshape( assert out.capacity == input.capacity assert out.uniqueformat == input.uniqueformat assert out.maxsize == input.maxsize + + +def _bilateralfilter_params(dtype, layout, channels): + return { + "diameter": 9, + "sigma_color": 1.0, + "sigma_space": 1.0, + } + + +def _bilateralfilter_varshape_params(dtype, layout, channels): + return { + "diameter": util.to_cvcuda_tensor(np.array([9, 9], dtype=np.int32), "N"), + "sigma_color": util.to_cvcuda_tensor( + np.array([1.0, 1.0], dtype=np.float32), "N" + ), + "sigma_space": util.to_cvcuda_tensor( + np.array([1.0, 1.0], dtype=np.float32), "N" + ), + } + + +globals().update( + cv_tools.make_op_tests( + name="bilateral_filter", + runner_info=[ + ("tensor", cvcuda.bilateral_filter, _bilateralfilter_params), + ("image_batch", cvcuda.bilateral_filter, _bilateralfilter_varshape_params), + ], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 3), + supported_dtypes={ + cvcuda.Type.U8, + cvcuda.Type.U16, + cvcuda.Type.S16, + cvcuda.Type.S32, + cvcuda.Type.F32, + }, + supported_layouts={"NHWC", "HWC", "NCHW", "CHW"}, + supported_channels={1, 2, 3, 4}, + exclude_dlc=[(None, "NCHW", 2), (None, "CHW", 2)], + ) +) diff --git a/tests/cvcuda/python/test_opbndbox.py b/tests/cvcuda/python/test_opbndbox.py index 95e3d3c7a..6675101a5 100644 --- a/tests/cvcuda/python/test_opbndbox.py +++ b/tests/cvcuda/python/test_opbndbox.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,11 +15,12 @@ import cvcuda -import pytest as t +import pytest import numpy as np +import cvcuda_tools as cv_tools -@t.mark.parametrize( +@pytest.mark.parametrize( "inputp, bndboxes", [ ( @@ -118,3 +119,31 @@ def test_op_bndbox(inputp, bndboxes): assert out.layout == input.layout assert out.shape == input.shape assert out.dtype == input.dtype + + +def _bndbox_params(dtype, layout, channels): + boxes = cvcuda.BndBoxesI( + boxes=[ + [ + cvcuda.BndBoxI( + box=(5, 5, 3, 3), + thickness=1, + borderColor=(255, 255, 0), + fillColor=(0, 128, 255, 128), + ), + ], + ] + ) + return {"bboxes": boxes} + + +globals().update( + cv_tools.make_op_tests( + name="bndbox", + runner_info=[("tensor", cvcuda.bndbox, _bndbox_params)], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 3), + supported_dtypes={cvcuda.Type.U8}, + supported_layouts={"NHWC", "HWC", "NCHW", "CHW"}, + supported_channels={3, 4}, + ) +) diff --git a/tests/cvcuda/python/test_opboxblur.py b/tests/cvcuda/python/test_opboxblur.py index bcc13e00c..64ba05f0a 100644 --- a/tests/cvcuda/python/test_opboxblur.py +++ b/tests/cvcuda/python/test_opboxblur.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,11 +15,12 @@ import cvcuda -import pytest as t +import pytest import numpy as np +import cvcuda_tools as cv_tools -@t.mark.parametrize( +@pytest.mark.parametrize( "inputp, blurboxes", [ ( @@ -73,3 +74,24 @@ def test_op_boxblur(inputp, blurboxes): assert out.layout == input.layout assert out.shape == input.shape assert out.dtype == input.dtype + + +def _boxblur_params(dtype, layout, channels): + boxes = [ + [ + cvcuda.BlurBoxI(box=(5, 5, 3, 3), kernelSize=3), + ] + ] + return {"bboxes": cvcuda.BlurBoxesI(boxes=boxes)} + + +globals().update( + cv_tools.make_op_tests( + name="boxblur", + runner_info=[("tensor", cvcuda.boxblur, _boxblur_params)], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 3), + supported_dtypes={cvcuda.Type.U8, cvcuda.Type.S8}, + supported_layouts={"NHWC", "HWC", "NCHW", "CHW"}, + supported_channels={3, 4}, + ) +) diff --git a/tests/cvcuda/python/test_opbrightnesscontrast.py b/tests/cvcuda/python/test_opbrightnesscontrast.py index a3b9fdf0e..e4feb8aeb 100644 --- a/tests/cvcuda/python/test_opbrightnesscontrast.py +++ b/tests/cvcuda/python/test_opbrightnesscontrast.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,14 +14,15 @@ # limitations under the License. import cvcuda -import pytest as t +import pytest import cvcuda_util as util import numpy as np +import cvcuda_tools as cv_tools RNG = np.random.default_rng(12345) -@t.mark.parametrize( +@pytest.mark.parametrize( "src_args, dst_dtype, args_setup", [ ( @@ -56,6 +57,22 @@ (1, 1, 1, 1), ), ), + ( + ((2, 3, 16, 23), cvcuda.Type.U8, "NCHW"), + cvcuda.Type.U8, + ( + cvcuda.Type.F32, + (2, 2, 1, 2), + ), + ), + ( + ((3, 16, 23), cvcuda.Type.U8, "CHW"), + cvcuda.Type.U8, + ( + cvcuda.Type.F32, + (1, 1, 1, 1), + ), + ), ( ((9, 16, 23, 3), cvcuda.Type.U16, "NHWC"), cvcuda.Type.U16, @@ -112,6 +129,14 @@ (21, 21, 1, 21), ), ), + ( + ((5, 4, 33, 28), cvcuda.Type.F32, "NCHW"), + cvcuda.Type.F32, + ( + cvcuda.Type.F32, + (5, 5, 1, 5), + ), + ), ( ((16, 23, 3), cvcuda.Type.F32, "HWC"), cvcuda.Type.F32, @@ -167,7 +192,7 @@ def test_op_brightness_contrast_api(src_args, dst_dtype, args_setup): assert t_tmp is t_dst -@t.mark.parametrize( +@pytest.mark.parametrize( "num_images, src_format, src_dtype, max_size, dst_format, args_setup", [ ( @@ -283,3 +308,164 @@ def test_op_brightnesscontrastvarshape_api( stream=stream, ) assert b_dst is b_tmp + + +_SCALAR_ARGS = (0.75, 1.25, 0.125, 0.5) + + +def _scalar_arg_tensors(dtype=np.float32): + return tuple( + util.to_cvcuda_tensor(np.array([value], dtype), "N") for value in _SCALAR_ARGS + ) + + +@pytest.mark.parametrize( + "dtype,arg_dtype,layout,shape", + [ + (np.uint8, np.float32, "NHWC", (2, 3, 5, 3)), + (np.float32, np.float32, "NCHW", (2, 3, 3, 5)), + (np.int32, np.float64, "NHWC", (2, 3, 5, 1)), + ], +) +def test_op_brightness_contrast_scalar_tensor_matches_tensor_parameters( + dtype, arg_dtype, layout, shape +): + values = np.arange(np.prod(shape), dtype=dtype).reshape(shape) + if np.issubdtype(dtype, np.floating): + values /= np.prod(shape) + src = util.to_cvcuda_tensor( + values, + layout, + ) + arg_tensors = _scalar_arg_tensors(arg_dtype) + + ref = cvcuda.brightness_contrast(src, *arg_tensors) + out = cvcuda.brightness_contrast(src, *_SCALAR_ARGS) + np.testing.assert_array_equal( + util.to_cpu_numpy_buffer(ref.cuda()), util.to_cpu_numpy_buffer(out.cuda()) + ) + + ref = cvcuda.Tensor(src.shape, src.dtype, src.layout) + out = cvcuda.Tensor(src.shape, src.dtype, src.layout) + cvcuda.brightness_contrast_into(ref, src, *arg_tensors) + ret = cvcuda.brightness_contrast_into(out, src, *_SCALAR_ARGS) + assert ret is out + np.testing.assert_array_equal( + util.to_cpu_numpy_buffer(ref.cuda()), util.to_cpu_numpy_buffer(out.cuda()) + ) + + +def test_op_brightness_contrast_scalar_varshape_matches_tensor_parameters(): + src = cvcuda.ImageBatchVarShape(2) + for shape in ((3, 5, 3), (4, 2, 3)): + src.pushback( + util.to_cvcuda_image( + np.linspace(0, 1, np.prod(shape), dtype=np.float32).reshape(shape) + ) + ) + arg_tensors = _scalar_arg_tensors() + + ref = cvcuda.brightness_contrast(src, *arg_tensors) + out = cvcuda.brightness_contrast(src, *_SCALAR_ARGS) + for ref_image, out_image in zip(ref, out): + np.testing.assert_array_equal( + util.to_cpu_numpy_buffer(ref_image.cuda()), + util.to_cpu_numpy_buffer(out_image.cuda()), + ) + + ref = util.clone_image_batch(src) + out = util.clone_image_batch(src) + cvcuda.brightness_contrast_into(ref, src, *arg_tensors) + ret = cvcuda.brightness_contrast_into(out, src, *_SCALAR_ARGS) + assert ret is out + for ref_image, out_image in zip(ref, out): + np.testing.assert_array_equal( + util.to_cpu_numpy_buffer(ref_image.cuda()), + util.to_cpu_numpy_buffer(out_image.cuda()), + ) + + +def test_op_brightness_contrast_scalar_clamp(): + src = util.to_cvcuda_tensor( + np.array([0, 0.25, 0.75, 1], np.float32).reshape(1, 1, 4, 1), "NHWC" + ) + args = (2.0, 1.0, -0.25, 0.0) + + unclamped = cvcuda.brightness_contrast(src, *args, clamp=False) + clamped = cvcuda.brightness_contrast(src, *args, clamp=True) + np.testing.assert_array_equal( + util.to_cpu_numpy_buffer(unclamped.cuda()).reshape(-1), + [-0.25, 0.25, 1.25, 1.75], + ) + np.testing.assert_array_equal( + util.to_cpu_numpy_buffer(clamped.cuda()).reshape(-1), [0, 0.25, 1, 1] + ) + + signed_src = util.to_cvcuda_tensor( + np.array([-10, 10], np.int32).reshape(1, 1, 2, 1), "NHWC" + ) + signed_unclamped = cvcuda.brightness_contrast( + signed_src, 1.0, 1.0, 0.0, 0.0, clamp=False + ) + signed_clamped = cvcuda.brightness_contrast( + signed_src, 1.0, 1.0, 0.0, 0.0, clamp=True + ) + np.testing.assert_array_equal( + util.to_cpu_numpy_buffer(signed_unclamped.cuda()).reshape(-1), [-10, 10] + ) + np.testing.assert_array_equal( + util.to_cpu_numpy_buffer(signed_clamped.cuda()).reshape(-1), [0, 10] + ) + + +def test_op_brightness_contrast_rejects_mixed_parameter_kinds(): + src = cvcuda.Tensor((1, 3, 5, 3), cvcuda.Type.F32, "NHWC") + with pytest.raises(TypeError): + cvcuda.brightness_contrast(src, _scalar_arg_tensors()[0], *_SCALAR_ARGS[1:]) + + +def _brightness_contrast_scalar_op(data): + return cvcuda.brightness_contrast(data, *_SCALAR_ARGS) + + +globals().update( + cv_tools.make_op_tests( + name="brightnesscontrast", + runner_info=[ + ("tensor", cvcuda.brightness_contrast, None), + ("image_batch", cvcuda.brightness_contrast, None), + ], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 3), + supported_dtypes={ + cvcuda.Type.U8, + cvcuda.Type.U16, + cvcuda.Type.S16, + cvcuda.Type.S32, + cvcuda.Type.F32, + }, + supported_layouts={"NHWC", "HWC", "NCHW", "CHW"}, + supported_channels={1, 2, 3, 4}, + exclude_dlc=[(None, "NCHW", 2), (None, "CHW", 2)], + ) +) + +globals().update( + cv_tools.make_op_tests( + name="brightnesscontrast_scalar", + runner_info=[ + ("tensor", _brightness_contrast_scalar_op, None), + ("image_batch", _brightness_contrast_scalar_op, None), + ], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 3), + supported_dtypes={ + cvcuda.Type.U8, + cvcuda.Type.U16, + cvcuda.Type.S16, + cvcuda.Type.S32, + cvcuda.Type.F32, + }, + supported_layouts={"NHWC", "HWC", "NCHW", "CHW"}, + supported_channels={1, 2, 3, 4}, + exclude_dlc=[(None, "NCHW", 2), (None, "CHW", 2)], + ) +) diff --git a/tests/cvcuda/python/test_opcentercrop.py b/tests/cvcuda/python/test_opcentercrop.py index 06addcf1a..defb83a0d 100644 --- a/tests/cvcuda/python/test_opcentercrop.py +++ b/tests/cvcuda/python/test_opcentercrop.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,11 +15,12 @@ import cvcuda -import pytest as t +import pytest import numpy as np +import cvcuda_tools as cv_tools -@t.mark.parametrize( +@pytest.mark.parametrize( "tensor_args, crop_size, gold_shape", [ (((5, 9, 9, 4), np.uint8, "NHWC"), [5, 5], (5, 5, 5, 4)), @@ -47,3 +48,29 @@ def test_op_center_crop(tensor_args, crop_size, gold_shape): assert out.layout == input.layout assert out.shape == input.shape assert out.dtype == input.dtype + + +def _centercrop_params(dtype, layout, channels): + return {"crop_size": [5, 5]} + + +globals().update( + cv_tools.make_op_tests( + name="centercrop", + runner_info=[("tensor", cvcuda.center_crop, _centercrop_params)], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 3), + supported_dtypes={ + cvcuda.Type.U8, + cvcuda.Type.S8, + cvcuda.Type.U16, + cvcuda.Type.S16, + cvcuda.Type.F16, + cvcuda.Type.S32, + cvcuda.Type.F32, + cvcuda.Type.F64, + }, + supported_layouts={"NHWC", "HWC", "NCHW", "CHW"}, + supported_channels={1, 2, 3, 4}, + exclude_dlc=[(None, "NCHW", 2), (None, "CHW", 2)], + ) +) diff --git a/tests/cvcuda/python/test_opchannelreorder.py b/tests/cvcuda/python/test_opchannelreorder.py index 59312d2c9..218864380 100644 --- a/tests/cvcuda/python/test_opchannelreorder.py +++ b/tests/cvcuda/python/test_opchannelreorder.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,11 +16,61 @@ import cvcuda import numpy as np +import pytest +import cvcuda_tools as cv_tools import cvcuda_util as util +import cupy RNG = np.random.default_rng(0) +@pytest.mark.parametrize( + "shape,dtype,layout,order", + [ + ((2, 9, 11, 4), np.uint8, "NHWC", [3, 1, 1, 0]), + ((7, 5, 3), np.uint16, "HWC", [2, -1, 0]), + ((2, 3, 8, 6), np.int16, "NCHW", [2, 0, 1]), + ((4, 6, 5), np.int32, "CHW", [3, 2, -1, 0]), + ((1, 5, 7, 2), np.float32, "NHWC", [1, 0]), + ], +) +def test_op_channelreorder_tensor(shape, dtype, layout, order): + src_h = np.arange(np.prod(shape), dtype=dtype).reshape(shape) + src = util.to_cvcuda_tensor(src_h, layout) + channel_axis = layout.index("C") + expected = np.zeros_like(src_h) + for dst_channel, src_channel in enumerate(order): + if src_channel >= 0: + dst_slice = [slice(None)] * len(shape) + src_slice = [slice(None)] * len(shape) + dst_slice[channel_axis] = dst_channel + src_slice[channel_axis] = src_channel + expected[tuple(dst_slice)] = src_h[tuple(src_slice)] + + out = cvcuda.channelreorder(src, order) + assert out.shape == src.shape + assert out.layout == src.layout + assert out.dtype == src.dtype + np.testing.assert_array_equal(cupy.asarray(out.cuda()).get(), expected) + + stream = cvcuda.Stream() + dst = cvcuda.Tensor(src.shape, src.dtype, src.layout) + returned = cvcuda.channelreorder_into(dst, src, order, stream=stream) + stream.sync() + assert returned is dst + np.testing.assert_array_equal(cupy.asarray(dst.cuda()).get(), expected) + + +def test_op_channelreorder_tensor_negative(): + src = cvcuda.Tensor((1, 5, 7, 3), np.uint8, "NHWC") + with pytest.raises(RuntimeError): + cvcuda.channelreorder(src, [0, 1]) + with pytest.raises(RuntimeError): + cvcuda.channelreorder(src, [0, 1, 3]) + with pytest.raises(RuntimeError): + cvcuda.channelreorder_into(src, src, [2, 1, 0]) + + def test_op_channelreorder_varshape(): input = util.create_image_batch(10, cvcuda.Format.RGB8, size=(123, 321), rng=RNG) @@ -50,3 +100,63 @@ def test_op_channelreorder_varshape(): assert out.capacity == input.capacity assert out.uniqueformat == input.uniqueformat assert out.maxsize == input.maxsize + + +def _channelreorder(data: cvcuda.ImageBatchVarShape) -> cvcuda.ImageBatchVarShape: + num_images = len(data) + num_channels = data.uniqueformat.channels + order_data = np.tile(np.arange(num_channels, dtype=np.int32), (num_images, 1)) + order = cvcuda.as_tensor(cupy.asarray(order_data), "NC") + return cvcuda.channelreorder(data, order) + + +def _channelreorder_tensor_params(dtype, layout, channels): + return {"order": list(range(channels))} + + +globals().update( + cv_tools.make_op_tests( + name="channelreorder", + runner_info=[ + ("tensor", cvcuda.channelreorder, _channelreorder_tensor_params), + ("image_batch", _channelreorder, None), + ], + supported_formats={ + # 1-channel formats + cvcuda.Format.U8, + cvcuda.Format.U16, + cvcuda.Format.S16, + cvcuda.Format.S32, + cvcuda.Format.F32, + # 3-channel formats + cvcuda.Format.RGB8, + cvcuda.Format.BGR8, + cvcuda.Format.RGB8p, + cvcuda.Format.BGR8p, + cvcuda.Format.RGBf32, + cvcuda.Format.BGRf32, + cvcuda.Format.RGBf32p, + cvcuda.Format.BGRf32p, + # 4-channel formats + cvcuda.Format.RGBA8, + cvcuda.Format.BGRA8, + cvcuda.Format.RGBA8p, + cvcuda.Format.BGRA8p, + cvcuda.Format.RGBAf32, + cvcuda.Format.BGRAf32, + cvcuda.Format.RGBAf32p, + cvcuda.Format.BGRAf32p, + }, + keystone_dlc=(cvcuda.Type.U8, "NHWC", 3), + supported_dtypes={ + cvcuda.Type.U8, + cvcuda.Type.U16, + cvcuda.Type.S16, + cvcuda.Type.S32, + cvcuda.Type.F32, + }, + supported_layouts={"NHWC", "HWC", "NCHW", "CHW"}, + supported_channels={1, 2, 3, 4}, + exclude_dlc=[(None, "NCHW", 2), (None, "CHW", 2)], + ) +) diff --git a/tests/cvcuda/python/test_opclahe.py b/tests/cvcuda/python/test_opclahe.py new file mode 100644 index 000000000..29585ebd2 --- /dev/null +++ b/tests/cvcuda/python/test_opclahe.py @@ -0,0 +1,163 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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 cvcuda + +import numpy as np +import pytest as t +import cvcuda_util as util + + +RNG = np.random.default_rng(0) + + +@t.mark.parametrize( + "input_shape, layout", + [ + ((1, 64, 80, 1), "NHWC"), + ((3, 127, 119, 1), "NHWC"), + ((1, 1, 64, 80), "NCHW"), + ((3, 1, 127, 119), "NCHW"), + ], +) +def test_op_clahe(input_shape, layout): + src = cvcuda.Tensor(input_shape, cvcuda.Type.U8, layout) + + out = cvcuda.clahe(src) + assert out.layout == src.layout + assert out.shape == src.shape + assert out.dtype == src.dtype + + out2 = cvcuda.Tensor(src.shape, src.dtype, src.layout) + tmp = cvcuda.clahe_into(out2, src, clip_limit=2.0, tile_grid_size=(7, 9)) + assert tmp is out2 + assert out2.layout == src.layout + assert out2.shape == src.shape + assert out2.dtype == src.dtype + + stream = cvcuda.Stream() + out3 = cvcuda.clahe(src, stream=stream) + assert out3.layout == src.layout + assert out3.shape == src.shape + assert out3.dtype == src.dtype + + out4 = cvcuda.Tensor(src.shape, src.dtype, src.layout) + tmp2 = cvcuda.clahe_into( + dst=out4, + src=src, + clip_limit=2.0, + tile_grid_size=(7, 9), + stream=stream, + ) + assert tmp2 is out4 + assert out4.layout == src.layout + assert out4.shape == src.shape + assert out4.dtype == src.dtype + + +@t.mark.parametrize( + "input_shape, layout", + [ + ((64, 80, 1), "HWC"), + ((127, 119, 1), "HWC"), + ((1, 64, 80), "CHW"), + ((1, 127, 119), "CHW"), + ], +) +def test_op_clahe_hwc(input_shape, layout): + src = cvcuda.Tensor(input_shape, cvcuda.Type.U8, layout) + + out = cvcuda.clahe(src, clip_limit=2.0, tile_grid_size=(8, 8)) + assert out.layout == src.layout + assert out.shape == src.shape + assert out.dtype == src.dtype + + out2 = cvcuda.Tensor(src.shape, src.dtype, src.layout) + tmp = cvcuda.clahe_into(out2, src, clip_limit=2.0, tile_grid_size=(8, 8)) + assert tmp is out2 + assert out2.layout == src.layout + assert out2.shape == src.shape + assert out2.dtype == src.dtype + + +def test_op_clahe_varshape(): + src = util.create_image_batch(4, cvcuda.Format.Y8, max_size=(127, 121), rng=RNG) + + out = cvcuda.clahe(src) + assert out.uniqueformat == src.uniqueformat + assert len(out) == len(src) + assert out.capacity == src.capacity + assert out.maxsize == src.maxsize + + tmp = cvcuda.clahe_into(out, src, clip_limit=2.0, tile_grid_size=(8, 8)) + assert tmp is out + assert out.uniqueformat == src.uniqueformat + assert len(out) == len(src) + assert out.capacity == src.capacity + assert out.maxsize == src.maxsize + + stream = cvcuda.Stream() + out2 = cvcuda.clahe(src=src, clip_limit=2.0, tile_grid_size=(8, 8), stream=stream) + assert out2.uniqueformat == src.uniqueformat + assert len(out2) == len(src) + assert out2.capacity == src.capacity + assert out2.maxsize == src.maxsize + + tmp2 = cvcuda.clahe_into( + dst=out2, + src=src, + clip_limit=2.0, + tile_grid_size=(8, 8), + stream=stream, + ) + assert tmp2 is out2 + assert out2.uniqueformat == src.uniqueformat + assert len(out2) == len(src) + assert out2.capacity == src.capacity + assert out2.maxsize == src.maxsize + + +def test_op_clahe_negative(): + src_rgb = cvcuda.Tensor((1, 32, 48, 3), cvcuda.Type.U8, "NHWC") + with t.raises(Exception): + cvcuda.clahe(src_rgb) + + src_rgb_nchw = cvcuda.Tensor((1, 3, 32, 48), cvcuda.Type.U8, "NCHW") + with t.raises(RuntimeError, match="CLAHE supports only single-channel tensors"): + cvcuda.clahe(src_rgb_nchw) + + src_f16 = cvcuda.Tensor((1, 32, 48, 1), cvcuda.Type.F16, "NHWC") + with t.raises(Exception): + cvcuda.clahe(src_f16) + + src = cvcuda.Tensor((1, 32, 48, 1), cvcuda.Type.U8, "NHWC") + with t.raises(Exception): + cvcuda.clahe(src, clip_limit=0.0) + with t.raises(Exception): + cvcuda.clahe(src, tile_grid_size=(0, 8)) + with t.raises(Exception): + cvcuda.clahe(src, tile_grid_size=(8, 0)) + with t.raises(Exception): + cvcuda.clahe(src, tile_grid_size=(-1, 8)) + + +def test_op_clahe_varshape_negative(): + src_rgb = util.create_image_batch(3, cvcuda.Format.RGB8, max_size=(64, 64), rng=RNG) + with t.raises(Exception): + cvcuda.clahe(src_rgb) + + src_y8 = util.create_image_batch(3, cvcuda.Format.Y8, max_size=(64, 64), rng=RNG) + with t.raises(Exception): + cvcuda.clahe(src_y8, tile_grid_size=(0, 8)) diff --git a/tests/cvcuda/python/test_opcolortwist.py b/tests/cvcuda/python/test_opcolortwist.py index 0d9c83f7f..303f1cd3a 100644 --- a/tests/cvcuda/python/test_opcolortwist.py +++ b/tests/cvcuda/python/test_opcolortwist.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,14 +14,15 @@ # limitations under the License. import cvcuda -import pytest as t +import pytest import cvcuda_util as util import numpy as np +import cvcuda_tools as cv_tools RNG = np.random.default_rng(12345) -@t.mark.parametrize( +@pytest.mark.parametrize( "src_args, twist_args", [ ( @@ -102,7 +103,7 @@ def test_op_remap_api(src_args, twist_args): assert t_tmp is t_dst -@t.mark.parametrize( +@pytest.mark.parametrize( "num_images, dtype, max_size, twist_args", [ (4, np.uint8, (73, 98), ((3,), cvcuda.Type._4F32, "HW")), @@ -143,3 +144,40 @@ def test_op_colortwistvarshape_api(num_images, dtype, max_size, twist_args): stream=stream, ) assert b_dst is b_tmp + + +def _colortwist_params(dtype, layout, channels): + twist_dtype = cvcuda.Type.F32 + if dtype in {cvcuda.Type.U32, cvcuda.Type.S32}: + twist_dtype = cvcuda.Type.F64 + return {"twist": cvcuda.Tensor((3, 4), twist_dtype, "HW")} + + +def _colortwist_varshape_params(dtype, layout, channels): + np_dtype = np.float64 if dtype in {cvcuda.Type.U32, cvcuda.Type.S32} else np.float32 + twist_data = np.zeros((2, 3, 4), dtype=np_dtype) + for i in range(3): + twist_data[:, i, i] = 1.0 + return {"twist": util.to_cvcuda_tensor(twist_data, "NHW")} + + +globals().update( + cv_tools.make_op_tests( + name="colortwist", + runner_info=[ + ("tensor", cvcuda.color_twist, _colortwist_params), + ("image_batch", cvcuda.color_twist, _colortwist_varshape_params), + ], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 3), + supported_dtypes={ + cvcuda.Type.U8, + cvcuda.Type.U16, + cvcuda.Type.S16, + cvcuda.Type.U32, + cvcuda.Type.S32, + cvcuda.Type.F32, + }, + supported_layouts={"NHWC", "HWC", "NCHW", "CHW"}, + supported_channels={3, 4}, + ) +) diff --git a/tests/cvcuda/python/test_opcomposite.py b/tests/cvcuda/python/test_opcomposite.py index afa8f0f99..d5d85c7a9 100644 --- a/tests/cvcuda/python/test_opcomposite.py +++ b/tests/cvcuda/python/test_opcomposite.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,14 +15,16 @@ import cvcuda -import pytest as t +import pytest import numpy as np + import cvcuda_util as util +import cvcuda_tools as cv_tools RNG = np.random.default_rng(0) -@t.mark.parametrize( +@pytest.mark.parametrize( "fg_args, bg_args, fgMask_args, outChannels", [ ( @@ -43,6 +45,18 @@ ((5, 21, 10, 1), np.uint8, "NHWC"), 4, ), + ( + ((5, 3, 21, 10), np.uint8, "NCHW"), + ((5, 3, 21, 10), np.uint8, "NCHW"), + ((5, 1, 21, 10), np.uint8, "NCHW"), + 4, + ), + ( + ((3, 9, 9), np.uint8, "CHW"), + ((3, 9, 9), np.uint8, "CHW"), + ((1, 9, 9), np.uint8, "CHW"), + 3, + ), ], ) def test_op_composite(fg_args, bg_args, fgMask_args, outChannels): @@ -52,19 +66,15 @@ def test_op_composite(fg_args, bg_args, fgMask_args, outChannels): out = cvcuda.composite(foreground, background, fgMask, outChannels) assert out.layout == foreground.layout - assert out.shape[-1] == outChannels - if out.layout == "NHWC": - assert out.shape[0:3] == foreground.shape[0:3] - if out.layout == "HWC": - assert out.shape[0:2] == foreground.shape[0:2] + channel_idx = str(out.layout).find("C") + expected_shape = list(foreground.shape) + expected_shape[channel_idx] = outChannels + assert out.shape == tuple(expected_shape) assert out.dtype == foreground.dtype stream = cvcuda.Stream() - out_shape = list(foreground.shape) - out_shape[-1] = outChannels - out_shape = tuple(out_shape) - out = cvcuda.Tensor(out_shape, foreground.dtype, foreground.layout) + out = cvcuda.Tensor(tuple(expected_shape), foreground.dtype, foreground.layout) tmp = cvcuda.composite_into( foreground=foreground, dst=out, @@ -74,15 +84,11 @@ def test_op_composite(fg_args, bg_args, fgMask_args, outChannels): ) assert tmp is out assert out.layout == foreground.layout - assert out.shape[-1] == outChannels - if out.layout == "NHWC": - assert out.shape[0:3] == foreground.shape[0:3] - if out.layout == "HWC": - assert out.shape[0:2] == foreground.shape[0:2] + assert out.shape == tuple(expected_shape) assert out.dtype == foreground.dtype -@t.mark.parametrize( +@pytest.mark.parametrize( "nimages, max_size, outChannels", [ ( @@ -105,9 +111,13 @@ def test_op_compositevarshape(nimages, max_size, outChannels): background = util.clone_image_batch(foreground) fgMask = util.clone_image_batch(foreground, img_format=cvcuda.Format.U8) - out = cvcuda.composite(foreground, background, fgMask) + out = cvcuda.composite(foreground, background, fgMask, outChannels) assert len(out) == len(foreground) assert out.capacity == foreground.capacity + if outChannels == 3: + assert out.uniqueformat == cvcuda.Format.RGB8 + if outChannels == 4: + assert out.uniqueformat == cvcuda.Format.RGBA8 stream = cvcuda.Stream() @@ -126,3 +136,121 @@ def test_op_compositevarshape(nimages, max_size, outChannels): assert tmp is out assert len(out) == len(foreground) assert out.capacity == foreground.capacity + + +@pytest.mark.parametrize( + "outChannels, expected_format", + [ + (3, cvcuda.Format.RGB8p), + (4, cvcuda.Format.RGBA8p), + ], +) +def test_op_compositevarshape_planar(outChannels, expected_format): + foreground = util.create_image_batch( + 3, cvcuda.Format.RGB8p, max_size=(10, 20), max_random=255, rng=RNG + ) + + background = util.clone_image_batch(foreground) + fgMask = util.clone_image_batch(foreground, img_format=cvcuda.Format.U8) + + out = cvcuda.composite(foreground, background, fgMask, outChannels) + assert len(out) == len(foreground) + assert out.capacity == foreground.capacity + assert out.uniqueformat == expected_format + + stream = cvcuda.Stream() + + out = util.clone_image_batch(foreground, img_format=expected_format) + tmp = cvcuda.composite_into( + foreground=foreground, + dst=out, + background=background, + fgmask=fgMask, + stream=stream, + ) + + assert tmp is out + assert len(out) == len(foreground) + assert out.capacity == foreground.capacity + + +def test_op_compositevarshape_preserves_input_capacity(): + capacity = 4 + num_images = 2 + foreground = cvcuda.ImageBatchVarShape(capacity) + background = cvcuda.ImageBatchVarShape(capacity) + fgMask = cvcuda.ImageBatchVarShape(capacity) + + for i in range(num_images): + size = (8 + i, 9 + i) + foreground.pushback(cvcuda.Image(size, cvcuda.Format.RGB8)) + background.pushback(cvcuda.Image(size, cvcuda.Format.RGB8)) + fgMask.pushback(cvcuda.Image(size, cvcuda.Format.U8)) + + out = cvcuda.composite(foreground, background, fgMask) + + assert len(out) == num_images + assert out.capacity == capacity + out.pushback(cvcuda.Image((10, 11), cvcuda.Format.RGB8)) + assert len(out) == num_images + 1 + + +def _composite( + src: cvcuda.Tensor, + layout: str = "NHWC", + out_channels: int = 3, + mask_channels: int = 1, +): + shape = list(src.shape) + mask_shape = list(shape) + channel_idx = layout.find("C") + mask_shape[channel_idx] = mask_channels + background = cvcuda.Tensor(tuple(shape), src.dtype, layout) + fgMask = cvcuda.Tensor(tuple(mask_shape), src.dtype, layout) + return cvcuda.composite(src, background, fgMask, out_channels) + + +def _composite_varshape( + src: cvcuda.ImageBatchVarShape, + layout: str = "NHWC", + out_channels: int = 3, + mask_channels: int = 1, +): + num_image = len(src) + bg_batch = cvcuda.ImageBatchVarShape(num_image) + fg_batch = cvcuda.ImageBatchVarShape(num_image) + mask_batch = cvcuda.ImageBatchVarShape(num_image) + for image in src: + bg_batch.pushback(cvcuda.Image(image.size, image.format)) + fg_batch.pushback(cvcuda.Image(image.size, image.format)) + mask_batch.pushback(cvcuda.Image(image.size, cvcuda.Format.U8)) + return cvcuda.composite(fg_batch, bg_batch, mask_batch, out_channels) + + +def _composite_params(dtype, layout, channels, out_channels=3, mask_channels=1): + return { + "layout": layout, + "out_channels": out_channels, + "mask_channels": mask_channels, + } + + +globals().update( + cv_tools.make_op_tests( + name="composite", + runner_info=[ + ("tensor", _composite, _composite_params), + ("image_batch", _composite_varshape, _composite_params), + ], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 3), + supported_dtypes={cvcuda.Type.U8}, + supported_layouts={"NHWC", "HWC", "NCHW", "CHW"}, + supported_channels={3}, + extra_params={"out_channels": {3, 4}}, + extra_params_negative={"mask_channels": {2, 3, 4}, "out_channels": {0, 2, 5}}, + exclude_extra_params=[ + ("mask_channels", "image_batch"), + ], + negative_exceptions=[RuntimeError, ValueError], + ) +) diff --git a/tests/cvcuda/python/test_opconv2d.py b/tests/cvcuda/python/test_opconv2d.py index 005a3afb9..78d1c63fb 100644 --- a/tests/cvcuda/python/test_opconv2d.py +++ b/tests/cvcuda/python/test_opconv2d.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,14 +15,16 @@ import cvcuda -import pytest as t +import pytest import numpy as np + import cvcuda_util as util +import cvcuda_tools as cv_tools RNG = np.random.default_rng(0) -@t.mark.parametrize( +@pytest.mark.parametrize( "input_args, kernel_args, kernel_anchor_args, border", [ ( @@ -55,6 +57,18 @@ ((9, 2), np.int32, "NC", (4, 4), RNG, None), cvcuda.Border.REFLECT101, ), + ( + (3, cvcuda.Format.RGB8p, (0, 0), (64, 64), 255, RNG), + (3, cvcuda.Format.F32, (3, 3), (64, 64), 1, RNG), + ((3, 2), np.int32, "NC", (3, 3), RNG, None), + cvcuda.Border.REPLICATE, + ), + ( + (2, cvcuda.Format.RGBf32p, (0, 0), (32, 32), 1, RNG), + (2, cvcuda.Format.F32, (5, 5), (32, 32), 1, RNG), + ((2, 2), np.int32, "NC", (5, 5), RNG, None), + cvcuda.Border.REFLECT101, + ), ], ) def test_op_conv2dvarshape(input_args, kernel_args, kernel_anchor_args, border): @@ -88,3 +102,41 @@ def test_op_conv2dvarshape(input_args, kernel_args, kernel_anchor_args, border): assert out.capacity == input.capacity assert out.uniqueformat == input.uniqueformat assert out.maxsize == input.maxsize + + +def _conv2d(src: cvcuda.ImageBatchVarShape) -> cvcuda.ImageBatchVarShape: + num_images = len(src) + kernel = util.create_image_batch( + num_images, cvcuda.Format.F32, size=(3, 3), max_random=1, rng=RNG + ) + kernel_anchor = util.create_tensor( + (num_images, 2), np.int32, "NC", max_random=(-1, -1), rng=None + ) + return cvcuda.conv2d(src, kernel, kernel_anchor, cvcuda.Border.CONSTANT) + + +globals().update( + cv_tools.make_op_tests( + name="conv2d", + runner_info=[("image_batch", _conv2d, None)], + supported_formats={ + cvcuda.Format.U8, + cvcuda.Format.U16, + cvcuda.Format.S16, + cvcuda.Format.S32, + cvcuda.Format.F32, + cvcuda.Format.RGB8, + cvcuda.Format.BGR8, + cvcuda.Format.RGBf32, + cvcuda.Format.BGRf32, + cvcuda.Format.RGBA8, + cvcuda.Format.BGRA8, + cvcuda.Format.RGBAf32, + cvcuda.Format.BGRAf32, + cvcuda.Format.RGB8p, + cvcuda.Format.RGBA8p, + cvcuda.Format.RGBf32p, + cvcuda.Format.RGBAf32p, + }, + ) +) diff --git a/tests/cvcuda/python/test_opconvertto.py b/tests/cvcuda/python/test_opconvertto.py index 1ced2c90a..e7a2800b1 100644 --- a/tests/cvcuda/python/test_opconvertto.py +++ b/tests/cvcuda/python/test_opconvertto.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,11 +15,14 @@ import cvcuda -import pytest as t +import pytest import numpy as np +import cupy +import cvcuda_tools as cv_tools +import cvcuda_util as util -@t.mark.parametrize( +@pytest.mark.parametrize( "input_args,dtype,scale,offset", [ (((5, 16, 23, 4), np.uint8, "NHWC"), np.float32, 1.2, 10.2), @@ -62,3 +65,91 @@ def test_op_convertto(input_args, dtype, scale, offset): assert out.layout == input.layout assert out.shape == input.shape assert out.dtype == dtype + + +def test_op_convertto_round_mode(): + # NEAREST rounds float->int to nearest, TRUNCATE drops the fraction; values + # (incl. negatives) chosen so the two modes differ. + src = util.to_cvcuda_tensor( + np.array([[[2.7], [-2.7], [3.4], [-3.4]]], dtype=np.float32), "HWC" + ) + + def convert(**kw): + out = cvcuda.convertto(src, cvcuda.Type.S32, **kw) + return cupy.asarray(out.cuda()).get().reshape(-1) + + np.testing.assert_array_equal(convert(round=cvcuda.Round.NEAREST), [3, -3, 3, -3]) + np.testing.assert_array_equal(convert(round=cvcuda.Round.TRUNCATE), [2, -2, 3, -3]) + np.testing.assert_array_equal(convert(), [3, -3, 3, -3]) # default == NEAREST + + +def _is_float(dt): + return np.issubdtype(dt, np.floating) + + +_CONVERSION_CASES = [ + # Each supported source and destination dtype appears exactly once. Boundary + # values exercise integer saturation and float-to-integer truncation. + (np.uint8, np.float32, cvcuda.Type.F32, [0, 1, 127, 255], 1.0 / 255.0), + (np.int8, np.uint8, cvcuda.Type.U8, [-128, -1, 0, 127], 1.0), + (np.uint16, np.int16, cvcuda.Type.S16, [0, 32767, 32768, 65535], 1.0), + (np.int16, np.uint16, cvcuda.Type.U16, [-32768, -1, 0, 32767], 1.0), + (np.int32, np.float64, cvcuda.Type.F64, [-16777216, -1, 0, 16777216], 0.5), + (np.float32, np.int32, cvcuda.Type.S32, [-3.7, -2.5, 2.5, 3.7], 1.0), + (np.float64, np.int8, cvcuda.Type.S8, [-200.9, -2.7, 2.7, 200.9], 1.0), +] + + +@pytest.mark.parametrize( + "in_np,out_np,out_t,values,scale", + _CONVERSION_CASES, + ids=[f"{i.__name__}->{o.__name__}" for i, o, _, _, _ in _CONVERSION_CASES], +) +def test_op_convertto_dtype_axes(in_np, out_np, out_t, values, scale): + arr = np.asarray(values, dtype=in_np).reshape(1, len(values), 1) + work = arr.astype(np.float64) * scale + if _is_float(out_np): + gold = work.astype(out_np) + else: + info = np.iinfo(out_np) + gold = np.clip(np.trunc(work), info.min, info.max).astype(out_np) + + src = util.to_cvcuda_tensor(arr, "HWC") + allocated = cvcuda.convertto(src, out_t, scale, round=cvcuda.Round.TRUNCATE) + + into = cvcuda.Tensor(src.shape, out_t, src.layout) + returned = cvcuda.convertto_into(into, src, scale, round=cvcuda.Round.TRUNCATE) + assert returned is into + + for result in (allocated, into): + np.testing.assert_allclose( + cupy.asarray(result.cuda()).get().reshape(-1).astype(np.float64), + gold.reshape(-1).astype(np.float64), + atol=1e-7 if _is_float(out_np) else 0, + rtol=0, + ) + + +def _convertto_params(dtype, layout, channels, **extra): + return {"dtype": cvcuda.Type.F64, **extra} + + +globals().update( + cv_tools.make_op_tests( + name="convertto", + runner_info=[("tensor", cvcuda.convertto, _convertto_params)], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 3), + supported_dtypes={ + cvcuda.Type.U8, + cvcuda.Type.S8, + cvcuda.Type.U16, + cvcuda.Type.S16, + cvcuda.Type.S32, + cvcuda.Type.F32, + cvcuda.Type.F64, + }, + supported_layouts={"NHWC", "HWC", "NCHW", "CHW"}, + supported_channels={1, 2, 3, 4}, + extra_params={"round": {cvcuda.Round.NEAREST, cvcuda.Round.TRUNCATE}}, + ) +) diff --git a/tests/cvcuda/python/test_opcopymakeborder.py b/tests/cvcuda/python/test_opcopymakeborder.py index ce80b46ec..869c65c3b 100644 --- a/tests/cvcuda/python/test_opcopymakeborder.py +++ b/tests/cvcuda/python/test_opcopymakeborder.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,13 +13,17 @@ # See the License for the specific language governing permissions and # limitations under the License. +from random import randint + import cvcuda -import pytest as t +import pytest import numpy as np +import cvcuda_util as util +import cvcuda_tools as cv_tools -@t.mark.parametrize( +@pytest.mark.parametrize( "input_args, top, bottom, left, right, border_mode, border_value", [ ( @@ -116,3 +120,208 @@ def test_op_copymakeborder( assert out.layout == input.layout assert out.shape == out_shape assert out.dtype == input.dtype + + +@pytest.mark.parametrize( + "num_images, format, min_out_size, max_out_size, border_mode, border_value", + [ + ( + 4, + cvcuda.Format.RGBf32, + (1, 1), + (128, 128), + cvcuda.Border.CONSTANT, + [0], + ), + ( + 5, + cvcuda.Format.RGB8, + (1, 1), + (128, 128), + cvcuda.Border.CONSTANT, + [12, 3, 4, 55], + ), + ( + 9, + cvcuda.Format.RGBA8, + (1, 1), + (128, 128), + cvcuda.Border.WRAP, + [0], + ), + ( + 12, + cvcuda.Format.RGBAf32, + (1, 1), + (128, 128), + cvcuda.Border.REPLICATE, + [0], + ), + ( + 8, + cvcuda.Format.RGB8, + (1, 1), + (128, 128), + cvcuda.Border.REFLECT, + [0], + ), + ( + 10, + cvcuda.Format.RGBA8, + (1, 1), + (128, 128), + cvcuda.Border.REFLECT101, + [0], + ), + ], +) +def test_op_copymakeborder_varshape( + num_images, format, min_out_size, max_out_size, border_mode, border_value +): + max_out_w = randint(min_out_size[0], max_out_size[0]) + max_out_h = randint(min_out_size[1], max_out_size[1]) + + input = cvcuda.ImageBatchVarShape(num_images) + varshape_out = cvcuda.ImageBatchVarShape(num_images) + out_heights = [] + out_widths = [] + for _ in range(num_images): + w = randint(1, max_out_w) + h = randint(1, max_out_h) + img_i = cvcuda.Image([w, h], format) + input.pushback(img_i) + w_out = randint(w, max_out_size[0]) + h_out = randint(h, max_out_size[1]) + img_o = cvcuda.Image([w_out, h_out], format) + varshape_out.pushback(img_o) + out_heights.append(h_out) + out_widths.append(w_out) + + top_tensor = util.to_cvcuda_tensor( + np.zeros((1, 1, num_images, 1), dtype=np.int32), "NHWC" + ) + left_tensor = util.to_cvcuda_tensor( + np.zeros((1, 1, num_images, 1), dtype=np.int32), "NHWC" + ) + + tensor_out = cvcuda.Tensor(num_images, [max_out_w, max_out_h], format) + + out = cvcuda.copymakeborderstack( + input, + top=top_tensor, + left=left_tensor, + out_height=max_out_h, + out_width=max_out_w, + ) + assert out.layout == tensor_out.layout + assert out.shape == tensor_out.shape + assert out.dtype == tensor_out.dtype + + stream = cvcuda.Stream() + tmp = cvcuda.copymakeborderstack_into( + src=input, + dst=tensor_out, + top=top_tensor, + left=left_tensor, + border_mode=border_mode, + border_value=border_value, + stream=stream, + ) + assert tmp is tensor_out + + out = cvcuda.copymakeborder( + src=input, + top=top_tensor, + left=left_tensor, + out_heights=out_heights, + out_widths=out_widths, + stream=stream, + ) + assert out.uniqueformat is not None + assert out.uniqueformat == varshape_out.uniqueformat + for res, ref in zip(out, varshape_out): + assert res.size == ref.size + assert res.format == ref.format + + tmp = cvcuda.copymakeborder_into( + src=input, + dst=varshape_out, + top=top_tensor, + left=left_tensor, + border_mode=border_mode, + border_value=border_value, + stream=stream, + ) + assert tmp is varshape_out + + +def _copymakeborder_params(dtype, layout, channels): + return { + "top": 1, + "bottom": 1, + "left": 1, + "right": 1, + } + + +def _copymakeborder_varshape_params(dtype, layout, channels): + return { + "top": util.to_cvcuda_tensor(np.array([[[[1], [1]]]], dtype=np.int32), "NHWC"), + "left": util.to_cvcuda_tensor(np.array([[[[1], [1]]]], dtype=np.int32), "NHWC"), + "out_heights": [26, 26], + "out_widths": [26, 26], + "border_value": [0], + } + + +globals().update( + cv_tools.make_op_tests( + name="copymakeborder", + runner_info=[ + ("tensor", cvcuda.copymakeborder, _copymakeborder_params), + ("image_batch", cvcuda.copymakeborder, _copymakeborder_varshape_params), + ], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 3), + supported_dtypes={ + cvcuda.Type.U8, + cvcuda.Type.U16, + cvcuda.Type.S16, + cvcuda.Type.F32, + }, + supported_layouts={"NHWC", "HWC", "NCHW", "CHW"}, + supported_channels={1, 2, 3, 4}, + # 2 channels only supported for U8 + exclude_dlc=[ + (cvcuda.Type.U16, None, 2), + (cvcuda.Type.S16, None, 2), + (cvcuda.Type.F32, None, 2), + (None, "NCHW", 2), + (None, "CHW", 2), + ], + ) +) + + +def test_op_copymakeborder_varshape_widths_mismatch_raises(): + num_images = 3 + fmt = cvcuda.Format.RGB8 + input = cvcuda.ImageBatchVarShape(num_images) + for _ in range(num_images): + input.pushback(cvcuda.Image([16, 16], fmt)) + + top = util.to_cvcuda_tensor(np.zeros((1, 1, num_images, 1), dtype=np.int32), "NHWC") + left = util.to_cvcuda_tensor( + np.zeros((1, 1, num_images, 1), dtype=np.int32), "NHWC" + ) + + out_heights = [32] * num_images + out_widths = [32] * (num_images - 1) + + with pytest.raises(RuntimeError, match="out_widths"): + cvcuda.copymakeborder( + src=input, + top=top, + left=left, + out_heights=out_heights, + out_widths=out_widths, + ) diff --git a/tests/cvcuda/python/test_opcopymakeborder_varshape.py b/tests/cvcuda/python/test_opcopymakeborder_varshape.py deleted file mode 100644 index bb9fa704d..000000000 --- a/tests/cvcuda/python/test_opcopymakeborder_varshape.py +++ /dev/null @@ -1,148 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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 cvcuda - -import pytest as t -from random import randint - - -@t.mark.parametrize( - "num_images, format, min_out_size, max_out_size, border_mode, border_value", - [ - ( - 4, - cvcuda.Format.RGBf32, - (1, 1), - (128, 128), - cvcuda.Border.CONSTANT, - [0], - ), - ( - 5, - cvcuda.Format.RGB8, - (1, 1), - (128, 128), - cvcuda.Border.CONSTANT, - [12, 3, 4, 55], - ), - ( - 9, - cvcuda.Format.RGBA8, - (1, 1), - (128, 128), - cvcuda.Border.WRAP, - [0], - ), - ( - 12, - cvcuda.Format.RGBAf32, - (1, 1), - (128, 128), - cvcuda.Border.REPLICATE, - [0], - ), - ( - 8, - cvcuda.Format.RGB8, - (1, 1), - (128, 128), - cvcuda.Border.REFLECT, - [0], - ), - ( - 10, - cvcuda.Format.RGBA8, - (1, 1), - (128, 128), - cvcuda.Border.REFLECT101, - [0], - ), - ], -) -def test_op_copymakeborder( - num_images, format, min_out_size, max_out_size, border_mode, border_value -): - max_out_w = randint(min_out_size[0], max_out_size[0]) - max_out_h = randint(min_out_size[1], max_out_size[1]) - - input = cvcuda.ImageBatchVarShape(num_images) - varshape_out = cvcuda.ImageBatchVarShape(num_images) - out_heights = [] - out_widths = [] - for i in range(num_images): - w = randint(1, max_out_w) - h = randint(1, max_out_h) - img_i = cvcuda.Image([w, h], format) - input.pushback(img_i) - w_out = randint(w, max_out_size[0]) - h_out = randint(h, max_out_size[1]) - img_o = cvcuda.Image([w_out, h_out], format) - varshape_out.pushback(img_o) - out_heights.append(h_out) - out_widths.append(w_out) - - top_tensor = cvcuda.Tensor((1, 1, num_images, 1), cvcuda.Type.S32, "NHWC") - left_tensor = cvcuda.Tensor((1, 1, num_images, 1), cvcuda.Type.S32, "NHWC") - - tensor_out = cvcuda.Tensor(num_images, [max_out_w, max_out_h], format) - - out = cvcuda.copymakeborderstack( - input, - top=top_tensor, - left=left_tensor, - out_height=max_out_h, - out_width=max_out_w, - ) - assert out.layout == tensor_out.layout - assert out.shape == tensor_out.shape - assert out.dtype == tensor_out.dtype - - stream = cvcuda.Stream() - tmp = cvcuda.copymakeborderstack_into( - src=input, - dst=tensor_out, - top=top_tensor, - left=left_tensor, - border_mode=border_mode, - border_value=border_value, - stream=stream, - ) - assert tmp is tensor_out - - out = cvcuda.copymakeborder( - src=input, - top=top_tensor, - left=left_tensor, - out_heights=out_heights, - out_widths=out_widths, - stream=stream, - ) - assert out.uniqueformat is not None - assert out.uniqueformat == varshape_out.uniqueformat - for res, ref in zip(out, varshape_out): - assert res.size == ref.size - assert res.format == ref.format - - tmp = cvcuda.copymakeborder_into( - src=input, - dst=varshape_out, - top=top_tensor, - left=left_tensor, - border_mode=border_mode, - border_value=border_value, - stream=stream, - ) - assert tmp is varshape_out diff --git a/tests/cvcuda/python/test_opcropflipnormalizereformat.py b/tests/cvcuda/python/test_opcropflipnormalizereformat.py index e0ede4a7b..6343de995 100644 --- a/tests/cvcuda/python/test_opcropflipnormalizereformat.py +++ b/tests/cvcuda/python/test_opcropflipnormalizereformat.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,14 +15,16 @@ import cvcuda -import pytest as t +import pytest import numpy as np +import cvcuda_tools as cv_tools import cvcuda_util as util +import cupy RNG = np.random.default_rng(0) -@t.mark.parametrize( +@pytest.mark.parametrize( "format,num_images,min_size,max_size,border,bvalue,basep,scalep,gscale,gshift,eps,flags,ch,dtype,layout", [ ( @@ -132,3 +134,115 @@ def test_op_crop_flip_normalize_reformat_tensor_out( assert out_tensor.shape == out_shape assert out_tensor.dtype == dtype assert out_tensor.layout == layout + + +_supported_scalar_formats = { + cvcuda.Format.U8, + cvcuda.Format.U16, + cvcuda.Format.U32, + cvcuda.Format.S8, + cvcuda.Format.S16, + cvcuda.Format.S32, + cvcuda.Format.F32, +} +_supported_interleaved_formats = { + # 3 channels interleaved + cvcuda.Format.RGB8, + cvcuda.Format.BGR8, + cvcuda.Format.RGBf32, + cvcuda.Format.BGRf32, + # 4 channels interleaved + cvcuda.Format.RGBA8, + cvcuda.Format.BGRA8, + cvcuda.Format.RGBAf32, + cvcuda.Format.BGRAf32, +} +_supported_planar_formats = { + # 3 channels planar + cvcuda.Format.RGB8p, + cvcuda.Format.BGR8p, + cvcuda.Format.RGBf32p, + cvcuda.Format.BGRf32p, + # 4 channels planar + cvcuda.Format.RGBA8p, + cvcuda.Format.BGRA8p, + cvcuda.Format.RGBAf32p, + cvcuda.Format.BGRAf32p, +} +_supported_formats = ( + _supported_scalar_formats + | _supported_interleaved_formats + | _supported_planar_formats +) +_supported_output_layouts = {"NHWC", "NCHW"} + + +def _get_channels_for_format(fmt: cvcuda.Format) -> int: + fmt_name = fmt.name + if "RGBA" in fmt_name or "BGRA" in fmt_name: + return 4 + if "RGB" in fmt_name or "BGR" in fmt_name: + return 3 + return 1 + + +def _cropflipnormalizereformat( + data: cvcuda.ImageBatchVarShape, + out_layout: str = "NHWC", +) -> cvcuda.Tensor: + num_images = len(data) + size = data.maxsize + channels = _get_channels_for_format(data.uniqueformat) + + crop_data = np.zeros((num_images, 1, 1, 4), dtype=np.int32) + crop_data[..., 2] = size[0] + crop_data[..., 3] = size[1] + cropRect = cvcuda.as_tensor(cupy.asarray(crop_data), "NHWC") + + flipCode = util.create_tensor( + (num_images, 1), np.int32, "NC", max_random=1, rng=RNG + ) + base = cvcuda.Tensor((1, 1, 1, channels), np.float32, "NHWC") + scale = cvcuda.Tensor((1, 1, 1, channels), np.float32, "NHWC") + + if out_layout == "NHWC": + out_shape = (num_images, size[1], size[0], channels) + else: # NCHW + out_shape = (num_images, channels, size[1], size[0]) + + return cvcuda.crop_flip_normalize_reformat( + data, + out_shape=out_shape, + out_dtype=np.float32, + out_layout=out_layout, + rect=cropRect, + flip_code=flipCode, + base=base, + scale=scale, + globalscale=1.0, + globalshift=0.0, + epsilon=0.0, + flags=0, + border=cvcuda.Border.CONSTANT, + bvalue=0.0, + ) + + +def _cropflipnormalizereformat_params(dtype, layout, channels, out_layout="NHWC"): + return {"out_layout": out_layout} + + +globals().update( + cv_tools.make_op_tests( + name="cropflipnormalizereformat", + runner_info=[ + ( + "image_batch", + _cropflipnormalizereformat, + _cropflipnormalizereformat_params, + ) + ], + supported_formats=_supported_formats, + extra_params={"out_layout": _supported_output_layouts}, + ) +) diff --git a/tests/cvcuda/python/test_opcustomcrop.py b/tests/cvcuda/python/test_opcustomcrop.py index 5d879f896..e59d65e80 100644 --- a/tests/cvcuda/python/test_opcustomcrop.py +++ b/tests/cvcuda/python/test_opcustomcrop.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,11 +15,12 @@ import cvcuda -import pytest as t +import pytest import numpy as np +import cvcuda_tools as cv_tools -@t.mark.parametrize( +@pytest.mark.parametrize( "input_args,rc,out_shape", [ ( @@ -65,3 +66,29 @@ def test_op_customcrop(input_args, rc, out_shape): assert out.layout == input.layout assert out.shape == input.shape assert out.dtype == input.dtype + + +def _customcrop_params(dtype, layout, channels): + return {"rect": cvcuda.RectI(0, 0, 10, 10)} + + +globals().update( + cv_tools.make_op_tests( + name="customcrop", + runner_info=[("tensor", cvcuda.customcrop, _customcrop_params)], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 3), + supported_dtypes={ + cvcuda.Type.U8, + cvcuda.Type.S8, + cvcuda.Type.U16, + cvcuda.Type.S16, + cvcuda.Type.S32, + cvcuda.Type.F16, + cvcuda.Type.F32, + cvcuda.Type.F64, + }, + supported_layouts={"NHWC", "HWC", "NCHW", "CHW"}, + supported_channels={1, 2, 3, 4}, + exclude_dlc=[(None, "NCHW", 2), (None, "CHW", 2)], + ) +) diff --git a/tests/cvcuda/python/test_opcvtcolor.py b/tests/cvcuda/python/test_opcvtcolor.py index 550f0d37d..d668a2e6b 100644 --- a/tests/cvcuda/python/test_opcvtcolor.py +++ b/tests/cvcuda/python/test_opcvtcolor.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,14 +15,28 @@ import cvcuda -import pytest as t +import pytest import numpy as np +import cvcuda_types as cv_types +import cvcuda_tools as cv_tools import cvcuda_util as util RNG = np.random.default_rng(0) -@t.mark.parametrize( +def _create_test_image_batch(num_images, img_format, size, max_pixel): + if img_format.planes > 1: + image_batch = cvcuda.ImageBatchVarShape(num_images) + for _ in range(num_images): + image_batch.pushback(cvcuda.Image(size, img_format)) + return image_batch + + return util.create_image_batch( + num_images, img_format, size=size, max_random=max_pixel, rng=RNG + ) + + +@pytest.mark.parametrize( "input_args, code, output_args", [ ( @@ -60,6 +74,16 @@ cvcuda.ColorConversion.BGR2YUV_NV21, ((2, 60, 62, 1), np.uint8, "NHWC"), ), + ( + ((2, 3, 16, 23), np.uint8, "NCHW"), + cvcuda.ColorConversion.RGB2RGBA, + ((2, 4, 16, 23), np.uint8, "NCHW"), + ), + ( + ((3, 17, 19), np.uint8, "CHW"), + cvcuda.ColorConversion.RGB2GRAY, + ((1, 17, 19), np.uint8, "CHW"), + ), ], ) def test_op_cvtcolor(input_args, code, output_args): @@ -80,7 +104,7 @@ def test_op_cvtcolor(input_args, code, output_args): assert tmp is output -@t.mark.parametrize( +@pytest.mark.parametrize( "num_images, in_format, img_size, max_pixel, code, out_format", [ ( @@ -123,30 +147,214 @@ def test_op_cvtcolor(input_args, code, output_args): cvcuda.ColorConversion.GRAY2BGR, cvcuda.Format.BGR8, ), + ( + 3, + cvcuda.Format.BGR8p, + (23, 21), + 256, + cvcuda.ColorConversion.BGR2BGRA, + cvcuda.Format.BGRA8p, + ), + ( + 3, + cvcuda.Format.BGR8p, + (23, 21), + 256, + cvcuda.ColorConversion.BGR2RGB, + cvcuda.Format.RGB8p, + ), ], ) def test_op_cvtcolorvarshape( num_images, in_format, img_size, max_pixel, code, out_format ): - input = util.create_image_batch( - num_images, in_format, size=img_size, max_random=max_pixel, rng=RNG - ) - output = util.create_image_batch( - num_images, out_format, size=img_size, max_random=max_pixel, rng=RNG - ) - out = cvcuda.cvtcolor(input, code) + input_batch = _create_test_image_batch(num_images, in_format, img_size, max_pixel) + output = _create_test_image_batch(num_images, out_format, img_size, max_pixel) + out = cvcuda.cvtcolor(input_batch, code) + if in_format.planes > 1: + assert out.uniqueformat == output.uniqueformat assert len(out) == len(output) assert out.capacity == output.capacity assert out.maxsize == output.maxsize stream = cvcuda.Stream() tmp = cvcuda.cvtcolor_into( - src=input, + src=input_batch, dst=output, code=code, stream=stream, ) assert tmp is output - assert len(output) == len(input) - assert output.capacity == input.capacity - assert output.maxsize == input.maxsize + assert len(output) == len(input_batch) + assert output.capacity == input_batch.capacity + assert output.maxsize == input_batch.maxsize + + +def test_op_cvtcolorvarshape_planar_rejects_unsupported_auto_output_dtype(): + input_batch = _create_test_image_batch(2, cvcuda.Format.RGBf32p, (23, 21), 1.0) + + with pytest.raises(RuntimeError, match="Unsupported planar var-shape CvtColor"): + cvcuda.cvtcolor(input_batch, cvcuda.ColorConversion.RGB2BGR) + + +_valid_conversions: list[tuple[cvcuda.ColorConversion, int]] = [ + # BGR <-> RGB (3 channels only for BGR2RGB/RGB2BGR) + (cvcuda.ColorConversion.BGR2RGB, 3), + (cvcuda.ColorConversion.RGB2BGR, 3), + # BGRA <-> BGR (4 channels in, 3 out) + (cvcuda.ColorConversion.BGRA2BGR, 4), + (cvcuda.ColorConversion.RGBA2BGR, 4), + # BGR <-> BGRA (3 channels in, 4 out) + (cvcuda.ColorConversion.BGR2BGRA, 3), + (cvcuda.ColorConversion.BGR2RGBA, 3), + # RGBA <-> BGRA (4 channels) + (cvcuda.ColorConversion.RGBA2BGRA, 4), + (cvcuda.ColorConversion.BGRA2RGBA, 4), + # GRAY conversions (1-ch in for GRAY2*, 3-ch in for *2GRAY) + (cvcuda.ColorConversion.GRAY2BGR, 1), + (cvcuda.ColorConversion.GRAY2RGB, 1), + (cvcuda.ColorConversion.BGR2GRAY, 3), + (cvcuda.ColorConversion.RGB2GRAY, 3), + # HSV conversions (3 channels) + (cvcuda.ColorConversion.HSV2RGB, 3), + (cvcuda.ColorConversion.HSV2BGR, 3), + (cvcuda.ColorConversion.RGB2HSV, 3), + (cvcuda.ColorConversion.BGR2HSV, 3), +] + +_invalid_conversions: list[tuple[cvcuda.ColorConversion, int]] = [ + # BGR2RGB requires exactly 3 channels (invalid: 1, 2, 4) + (cvcuda.ColorConversion.BGR2RGB, 1), + (cvcuda.ColorConversion.BGR2RGB, 2), + (cvcuda.ColorConversion.BGR2RGB, 4), + # RGBA2BGRA requires 4 channels (invalid: 1, 2, 3) + (cvcuda.ColorConversion.RGBA2BGRA, 1), + (cvcuda.ColorConversion.RGBA2BGRA, 2), + (cvcuda.ColorConversion.RGBA2BGRA, 3), + # GRAY2BGR requires 1 channel (invalid: 2, 3, 4) + (cvcuda.ColorConversion.GRAY2BGR, 2), + (cvcuda.ColorConversion.GRAY2BGR, 3), + (cvcuda.ColorConversion.GRAY2BGR, 4), + # BGR2GRAY requires 3 channels (invalid: 1, 2, 4) + (cvcuda.ColorConversion.BGR2GRAY, 1), + (cvcuda.ColorConversion.BGR2GRAY, 2), + (cvcuda.ColorConversion.BGR2GRAY, 4), + # HSV2RGB requires 3 channels (invalid: 1, 2, 4) + (cvcuda.ColorConversion.HSV2RGB, 1), + (cvcuda.ColorConversion.HSV2RGB, 2), + (cvcuda.ColorConversion.HSV2RGB, 4), +] + + +def _create_input(channels: int, layout: str): + height, width = 24, 32 + if layout == "HWC": + shape = (height, width, channels) + elif layout == "NHWC": + shape = (1, height, width, channels) + elif layout == "CHW": + shape = (channels, height, width) + else: # NCHW + shape = (1, channels, height, width) + return cvcuda.Tensor(shape, np.uint8, layout) + + +def _op(code: cvcuda.ColorConversion, channels: int, layout: str): + cvcuda.cvtcolor(_create_input(channels, layout), code) + + +_supported_layouts = {"NHWC", "HWC", "NCHW", "CHW"} + + +@pytest.mark.parametrize( + "code,channels", + [pytest.param(c, ch, id=f"{c.name}-{ch}ch") for c, ch in _valid_conversions], +) +@pytest.mark.parametrize("layout", _supported_layouts) +def test_op_cvtcolor_valid_conversions(code, channels, layout): + _op(code, channels, layout) + + +@pytest.mark.parametrize( + "code,channels", + [pytest.param(c, ch, id=f"{c.name}-{ch}ch") for c, ch in _invalid_conversions], +) +def test_op_cvtcolor_invalid_conversions(code, channels): + with pytest.raises(RuntimeError): + _op(code, channels, "NHWC") + + +def _cvtcolor_op(src): + return cvcuda.cvtcolor(src, code=cvcuda.ColorConversion.BGR2RGB) + + +_supported_dtypes = {cvcuda.Type.U8, cvcuda.Type.U16} +_supported_channels = {3} + + +@pytest.mark.parametrize("dtype", _supported_dtypes) +@pytest.mark.parametrize("layout", _supported_layouts) +@pytest.mark.parametrize("channels", _supported_channels) +def test_op_cvtcolor_input(dtype, layout, channels): + cv_tools.assert_layouts( + _cvtcolor_op, layout, dtype=dtype, wrapper="tensor", channels=channels + ) + + +@pytest.mark.parametrize("dtype", cv_types.SCALAR_TYPES_SET - _supported_dtypes) +def test_op_cvtcolor_dtype_negative(dtype): + cv_tools.assert_dtypes(_cvtcolor_op, dtype, wrapper="tensor", negative=True) + + +@pytest.mark.parametrize("layout", cv_types.IMAGE_LAYOUTS - _supported_layouts) +def test_op_cvtcolor_layout_negative(layout): + cv_tools.assert_layouts( + _cvtcolor_op, + layout, + dtype=cvcuda.Type.U8, + wrapper="tensor", + channels=3, + negative=True, + ) + + +@pytest.mark.parametrize("channels", {1, 2, 4}) +def test_op_cvtcolor_channels_negative(channels): + cv_tools.assert_layouts( + _cvtcolor_op, + "NHWC", + dtype=cvcuda.Type.U8, + wrapper="tensor", + channels=channels, + negative=True, + ) + + +def _cvtcolor_params(dtype, layout, channels): + return {"code": cvcuda.ColorConversion.BGR2RGB} + + +def _cvtcolor_varshape_params(dtype, layout, channels): + return {"code": cvcuda.ColorConversion.BGR2RGB} + + +globals().update( + cv_tools.make_op_tests( + name="cvtcolor", + runner_info=[ + ("tensor", cvcuda.cvtcolor, _cvtcolor_params), + ("image_batch", cvcuda.cvtcolor, _cvtcolor_varshape_params), + ], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 3), + supported_dtypes={cvcuda.Type.U8, cvcuda.Type.U16}, + supported_layouts=_supported_layouts, + supported_channels={3}, + ) +) + + +# Regression: COLORCVT_MAX and CVT_MAX were sentinel end-of-enum markers +# leaked into Python; they are not real conversion codes and were removed. +@pytest.mark.parametrize("name", ["COLORCVT_MAX", "CVT_MAX"]) +def test_color_conversion_sentinels_are_not_exposed(name): + assert not hasattr(cvcuda.ColorConversion, name) diff --git a/tests/cvcuda/python/test_operase.py b/tests/cvcuda/python/test_operase.py index af6f224a2..f46adef4d 100644 --- a/tests/cvcuda/python/test_operase.py +++ b/tests/cvcuda/python/test_operase.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,12 +13,24 @@ # See the License for the specific language governing permissions and # limitations under the License. +from random import randint + +import cupy import cvcuda +import pytest +import cvcuda_tools as cv_tools +import cvcuda_types as cv_types + -import pytest as t +def _zeroed(tensor): + # cvcuda.Tensor leaves device memory uninitialized; erase validates its anchor, so zero the + # parameter tensors to give it well-defined (no-op) values instead of whatever the allocator + # pool happens to hold. + cupy.asarray(tensor.cuda())[...] = 0 + return tensor -@t.mark.parametrize( +@pytest.mark.parametrize( "input_args, erasing_area_num, random, seed", [ (((1, 460, 640, 3), cvcuda.Type.U8, "NHWC"), 1, False, 0), @@ -29,10 +41,11 @@ def test_op_erase(input_args, erasing_area_num, random, seed): input = cvcuda.Tensor(*input_args) parameter_shape = (erasing_area_num,) - anchor = cvcuda.Tensor(parameter_shape, cvcuda.Type._2S32, "N") - erasing = cvcuda.Tensor(parameter_shape, cvcuda.Type._3S32, "N") - imgIdx = cvcuda.Tensor(parameter_shape, cvcuda.Type.S32, "N") - values = cvcuda.Tensor(parameter_shape, cvcuda.Type.F32, "N") + values_shape = (erasing_area_num * input_args[0][-1],) + anchor = _zeroed(cvcuda.Tensor(parameter_shape, cvcuda.Type._2S32, "N")) + erasing = _zeroed(cvcuda.Tensor(parameter_shape, cvcuda.Type._3S32, "N")) + imgIdx = _zeroed(cvcuda.Tensor(parameter_shape, cvcuda.Type.S32, "N")) + values = _zeroed(cvcuda.Tensor(values_shape, cvcuda.Type.F32, "N")) out = cvcuda.erase(input, anchor, erasing, values, imgIdx) assert out.layout == input.layout @@ -70,3 +83,233 @@ def test_op_erase(input_args, erasing_area_num, random, seed): stream=stream, ) assert tmp is out + + +@pytest.mark.parametrize( + "shape,dtype,layout,channels", + [ + ((2, 7, 9, 3), cvcuda.Type.U8, "NHWC", 3), + ((7, 9, 4), cvcuda.Type.F32, "HWC", 4), + ((2, 2, 7, 9), cvcuda.Type.U8, "NCHW", 2), + ((1, 7, 9), cvcuda.Type.F32, "CHW", 1), + ], +) +def test_op_erase_region_overload(shape, dtype, layout, channels): + input = cvcuda.Tensor(shape, dtype, layout) + values = cvcuda.Tensor((channels, 2, 3), cvcuda.Type.F32, "CHW") + cupy.asarray(input.cuda())[...] = 4 + cupy.asarray(values.cuda())[...] = 7 + + out = cvcuda.erase(input, 2, 3, 2, 3, values) + assert out.shape == input.shape + assert out.dtype == input.dtype + assert out.layout == input.layout + + dst = cvcuda.Tensor(shape, dtype, layout) + tmp = cvcuda.erase_into(dst, input, 2, 3, 2, 3, values) + assert tmp is dst + + tmp = cvcuda.erase_into(input, input, 2, 3, 2, 3, values) + assert tmp is input + + +def test_op_erase_region_rejects_invalid_value_dtype(): + input = cvcuda.Tensor((1, 7, 9, 3), cvcuda.Type.U8, "NHWC") + values = cvcuda.Tensor((1,), cvcuda.Type.S16, "W") + + with pytest.raises(RuntimeError, match="values must match"): + cvcuda.erase(input, 2, 3, 2, 3, values) + + +@pytest.mark.parametrize( + "dtype", + [ + cvcuda.Type.U8, + cvcuda.Type.S8, + cvcuda.Type.U16, + cvcuda.Type.S16, + cvcuda.Type.U32, + cvcuda.Type.S32, + cvcuda.Type.U64, + cvcuda.Type.S64, + ], + ids=str, +) +def test_op_erase_region_float32_cast_matches_torch(dtype): + torch = pytest.importorskip("torch") + if not torch.cuda.is_available(): + pytest.skip("CUDA-enabled PyTorch is required") + + torch_dtype = cv_types.TYPE_TO_TORCH_DTYPE.get(dtype) + if torch_dtype is None: + pytest.skip(f"{dtype} is not supported by this PyTorch version") + + values = torch.tensor( + [ + -float("inf"), + -1.0e20, + -256.9, + -1.9, + float("nan"), + 0.0, + 1.9, + 255.9, + 256.1, + 1.0e20, + float("inf"), + ], + dtype=torch.float32, + device="cuda", + ) + source = torch.zeros((1, 1, 1, values.numel()), dtype=torch_dtype, device="cuda") + expected = source.clone() + expected[...] = values + + actual = cvcuda.erase( + cvcuda.as_tensor(source, "NCHW"), + 0, + 0, + 1, + values.numel(), + cvcuda.as_tensor(values, "W"), + ) + + assert torch.equal(torch.as_tensor(actual.cuda(), device=expected.device), expected) + + +@pytest.mark.parametrize( + "num_images, format, min_size, max_size, erasing_area_num, random, seed", + [ + (1, cvcuda.Format.U8, (100, 100), (200, 200), 1, False, 0), + (5, cvcuda.Format.RGB8, (100, 100), (200, 100), 1, True, 1), + ], +) +def test_op_erase_varshape( + num_images, format, min_size, max_size, erasing_area_num, random, seed +): + + parameter_shape = (erasing_area_num,) + values_shape = (erasing_area_num * format.channels,) + anchor = _zeroed(cvcuda.Tensor(parameter_shape, cvcuda.Type._2S32, "N")) + erasing = _zeroed(cvcuda.Tensor(parameter_shape, cvcuda.Type._3S32, "N")) + imgIdx = _zeroed(cvcuda.Tensor(parameter_shape, cvcuda.Type.S32, "N")) + values = _zeroed(cvcuda.Tensor(values_shape, cvcuda.Type.F32, "N")) + + input = cvcuda.ImageBatchVarShape(num_images) + output = cvcuda.ImageBatchVarShape(num_images) + for i in range(num_images): + w = randint(min_size[0], max_size[0]) + h = randint(min_size[1], max_size[1]) + img_in = cvcuda.Image([w, h], format) + input.pushback(img_in) + img_out = cvcuda.Image([w, h], format) + output.pushback(img_out) + + tmp = cvcuda.erase(input, anchor, erasing, values, imgIdx) + assert tmp.uniqueformat is not None + assert tmp.uniqueformat == output.uniqueformat + for res, ref in zip(tmp, output): + assert res.size == ref.size + assert res.format == ref.format + + tmp = cvcuda.erase_into( + output, input, anchor, erasing, values, imgIdx, random=random, seed=seed + ) + assert tmp is output + + stream = cvcuda.Stream() + tmp = cvcuda.erase( + src=input, + anchor=anchor, + erasing=erasing, + values=values, + imgIdx=imgIdx, + random=random, + seed=seed, + stream=stream, + ) + assert tmp.uniqueformat is not None + assert tmp.uniqueformat == output.uniqueformat + for res, ref in zip(tmp, output): + assert res.size == ref.size + assert res.format == ref.format + + tmp = cvcuda.erase_into( + src=input, + dst=output, + anchor=anchor, + erasing=erasing, + values=values, + imgIdx=imgIdx, + random=random, + seed=seed, + stream=stream, + ) + assert tmp is output + + +def _erase_params(dtype, layout, channels): + return { + "anchor": _zeroed(cvcuda.Tensor((1,), cvcuda.Type._2S32, "N")), + "erasing": _zeroed(cvcuda.Tensor((1,), cvcuda.Type._3S32, "N")), + "values": _zeroed(cvcuda.Tensor((channels,), cvcuda.Type.F32, "N")), + "imgIdx": _zeroed(cvcuda.Tensor((1,), cvcuda.Type.S32, "N")), + } + + +def _erase_region_params(dtype, layout, channels): + return { + "i": 0, + "j": 0, + "h": 1, + "w": 1, + "v": _zeroed(cvcuda.Tensor((channels, 1, 1), dtype, "CHW")), + } + + +globals().update( + cv_tools.make_op_tests( + name="erase", + runner_info=[ + ("tensor", cvcuda.erase, _erase_params), + ("image_batch", cvcuda.erase, _erase_params), + ], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 3), + supported_dtypes={ + cvcuda.Type.U8, + cvcuda.Type.U16, + cvcuda.Type.S16, + cvcuda.Type.S32, + cvcuda.Type.F32, + }, + supported_layouts={"NHWC", "HWC", "NCHW", "CHW"}, + supported_channels={1, 2, 3, 4}, + exclude_dlc=[(None, "NCHW", 2), (None, "CHW", 2)], + ) +) + + +globals().update( + cv_tools.make_op_tests( + name="erase_region", + runner_info=[ + ("tensor", cvcuda.erase, _erase_region_params), + ], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 3), + supported_dtypes={ + cvcuda.Type.U8, + cvcuda.Type.S8, + cvcuda.Type.U16, + cvcuda.Type.S16, + cvcuda.Type.U32, + cvcuda.Type.S32, + cvcuda.Type.U64, + cvcuda.Type.S64, + cvcuda.Type.F16, + cvcuda.Type.F32, + cvcuda.Type.F64, + }, + supported_layouts={"NHWC", "HWC", "NCHW", "CHW"}, + supported_channels={1, 2, 3, 4}, + ) +) diff --git a/tests/cvcuda/python/test_operase_varshape.py b/tests/cvcuda/python/test_operase_varshape.py deleted file mode 100644 index 9e844dc49..000000000 --- a/tests/cvcuda/python/test_operase_varshape.py +++ /dev/null @@ -1,89 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed 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 cvcuda - -import pytest as t -from random import randint - - -@t.mark.parametrize( - "num_images, format, min_size, max_size, erasing_area_num, random, seed", - [ - (1, cvcuda.Format.U8, (100, 100), (200, 200), 1, False, 0), - (5, cvcuda.Format.RGB8, (100, 100), (200, 100), 1, True, 1), - ], -) -def test_op_erase( - num_images, format, min_size, max_size, erasing_area_num, random, seed -): - - parameter_shape = (erasing_area_num,) - anchor = cvcuda.Tensor(parameter_shape, cvcuda.Type._2S32, "N") - erasing = cvcuda.Tensor(parameter_shape, cvcuda.Type._3S32, "N") - imgIdx = cvcuda.Tensor(parameter_shape, cvcuda.Type.S32, "N") - values = cvcuda.Tensor(parameter_shape, cvcuda.Type.F32, "N") - - input = cvcuda.ImageBatchVarShape(num_images) - output = cvcuda.ImageBatchVarShape(num_images) - for i in range(num_images): - w = randint(min_size[0], max_size[0]) - h = randint(min_size[1], max_size[1]) - img_in = cvcuda.Image([w, h], format) - input.pushback(img_in) - img_out = cvcuda.Image([w, h], format) - output.pushback(img_out) - - tmp = cvcuda.erase(input, anchor, erasing, values, imgIdx) - assert tmp.uniqueformat is not None - assert tmp.uniqueformat == output.uniqueformat - for res, ref in zip(tmp, output): - assert res.size == ref.size - assert res.format == ref.format - - tmp = cvcuda.erase_into( - output, input, anchor, erasing, values, imgIdx, random=random, seed=seed - ) - assert tmp is output - - stream = cvcuda.Stream() - tmp = cvcuda.erase( - src=input, - anchor=anchor, - erasing=erasing, - values=values, - imgIdx=imgIdx, - random=random, - seed=seed, - stream=stream, - ) - assert tmp.uniqueformat is not None - assert tmp.uniqueformat == output.uniqueformat - for res, ref in zip(tmp, output): - assert res.size == ref.size - assert res.format == ref.format - - tmp = cvcuda.erase_into( - src=input, - dst=output, - anchor=anchor, - erasing=erasing, - values=values, - imgIdx=imgIdx, - random=random, - seed=seed, - stream=stream, - ) - assert tmp is output diff --git a/tests/cvcuda/python/test_opfindhomography.py b/tests/cvcuda/python/test_opfindhomography.py index 76f9a3bc6..9c37479ac 100644 --- a/tests/cvcuda/python/test_opfindhomography.py +++ b/tests/cvcuda/python/test_opfindhomography.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,13 +13,16 @@ # See the License for the specific language governing permissions and # limitations under the License. +import numpy as np + import cvcuda import cvcuda_util -import pytest as t -import numpy as np +import pytest +import cvcuda_types as cv_types +import cvcuda_tools as cv_tools -@t.mark.parametrize( +@pytest.mark.parametrize( "num_samples, num_points", [ (16, 1024), @@ -53,7 +56,7 @@ def test_op_findhomography(num_samples, num_points): assert out.dtype == cvcuda.Type.F32 -@t.mark.parametrize( +@pytest.mark.parametrize( "num_samples, num_points", [ (16, 1024), @@ -92,3 +95,31 @@ def test_op_findhomographyvarshape(num_samples, num_points): assert outBatch.ndim == 3 assert outBatch.dtype == cvcuda.Type.F32 assert outBatch.capacity == srcBatch.capacity + + +def _op(src: cvcuda.Tensor) -> cvcuda.Tensor: + dst = cvcuda.Tensor(src.shape, src.dtype, src.layout) + return cvcuda.findhomography(src, dst) + + +_num_samples = 4 +_num_points = 16 + +_supported_input_configs = [ + (cvcuda.Type._2F32, "NW", 1), + (cvcuda.Type.F32, "NWC", 2), +] + + +@pytest.mark.parametrize("dtype,layout,channels", _supported_input_configs) +def test_op_findhomography_input(dtype, layout, channels): + cv_tools.assert_layouts( + _op, layout, dtype=dtype, wrapper="tensor", channels=channels + ) + + +@pytest.mark.parametrize("dtype", cv_types.SCALAR_TYPES_SET - {cvcuda.Type.F32}) +def test_op_findhomography_dtype_negative(dtype): + cv_tools.assert_layouts( + _op, "NWC", dtype=dtype, wrapper="tensor", channels=2, negative=True + ) diff --git a/tests/cvcuda/python/test_opfindhomography_cache_regression.py b/tests/cvcuda/python/test_opfindhomography_cache_regression.py new file mode 100644 index 000000000..358966832 --- /dev/null +++ b/tests/cvcuda/python/test_opfindhomography_cache_regression.py @@ -0,0 +1,84 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +# Regression for the PyOpFindHomography cache-key bug: +# +# python/mod_cvcuda/operators/OpFindHomography.cpp declares a cache Key whose +# ``doGetHash()`` returns 0, ``payloadSize()`` returns 0, and +# ``doIsCompatible()`` is unconditionally true. Combined with ``fetch()`` +# picking by ``payloadSize > maxPayloadSize`` starting at 0, this makes the +# cache hand back the first inserted op for any subsequent request, +# regardless of ``batchSize`` or ``maxNumPoints``. +# +# When a smaller op is reused for a larger request, ``RunFindHomography`` +# (src/cvcuda/priv/OpFindHomography.cu) reads ``batchSize = models.shape(0)`` +# and issues cudaMemsetAsync + kernel launches sized by the larger batch — +# but ``DeviceState`` buffers were allocated for the smaller one. The OOB +# writes corrupt adjacent device memory and the resulting homographies are +# garbage. + +import cupy +import numpy as np + +import cvcuda + + +def test_findhomography_cache_reuse_regression(): + # Start from a clean cache so the test does not depend on pytest + # collection order. + cvcuda.clear_cache() + + num_points = 1024 + rng = np.random.default_rng(0) + # Well-spread, non-collinear 2D points so the solver is non-degenerate. + pts = rng.uniform(-100.0, 100.0, size=(num_points, 2)).astype(np.float32) + + def make_input(n): + # Identical points across the batch dim; src == dst => identity H. + batch = np.broadcast_to(pts, (n, num_points, 2)).copy() + return cvcuda.as_tensor(cupy.asarray(batch), layout="NWC") + + # 1. Prime the op cache with a (batchSize=1, maxNumPoints=num_points) op. + src_small = make_input(1) + dst_small = make_input(1) + cvcuda.findhomography(src_small, dst_small) + + # 2. Larger batch. A correct cache would create a new op sized for 64; + # the buggy cache returns the cached small op, and RunFindHomography + # writes past the end of the undersized DeviceState buffers. + n = 64 + src_big = make_input(n) + dst_big = make_input(n) + out = cvcuda.findhomography(src_big, dst_big) + + h = cupy.asarray(out.cuda()).get() + assert h.shape == (n, 3, 3) + + # Expected homography: identity, up to scale. + expected = np.eye(3, dtype=np.float32) + for i in range(n): + hi = h[i] + if abs(hi[2, 2]) > 1e-8: + hi = hi / hi[2, 2] + assert np.allclose(hi, expected, atol=1e-2), ( + f"homography for sample {i} is not identity (src == dst). " + "PyOpFindHomography cache reuse bug suspected — the cached " + f"(batchSize=1, maxNumPoints={num_points}) op was returned for " + f"a batchSize={n} request, and RunFindHomography's memsets + " + f"kernel launches wrote past the end of the undersized buffers.\n" + f"Got:\n{hi}\nExpected:\n{expected}" + ) + + cvcuda.clear_cache() diff --git a/tests/cvcuda/python/test_opflip.py b/tests/cvcuda/python/test_opflip.py index 6316a5756..bb07740d6 100644 --- a/tests/cvcuda/python/test_opflip.py +++ b/tests/cvcuda/python/test_opflip.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,14 +15,16 @@ import cvcuda -import pytest as t +import pytest import numpy as np + import cvcuda_util as util +import cvcuda_tools as cv_tools RNG = np.random.default_rng(0) -@t.mark.parametrize( +@pytest.mark.parametrize( "tensor_params, flip_code", [ ( @@ -69,7 +71,7 @@ def test_op_flip(tensor_params, flip_code): assert out.dtype == input.dtype -@t.mark.parametrize( +@pytest.mark.parametrize( "num_images, img_format, img_size, max_pixel, flip_code", [ ( @@ -138,3 +140,51 @@ def test_op_flipvarshape(num_images, img_format, img_size, max_pixel, flip_code) assert out.capacity == input.capacity assert out.uniqueformat == input.uniqueformat assert out.maxsize == input.maxsize + + +def _flip_params(dtype, layout, channels): + return {"flipCode": 0} + + +def _flip_varshape_params(dtype, layout, channels): + return {"flipCode": util.to_cvcuda_tensor(np.array([0, 0], dtype=np.int32), "N")} + + +# Tensor and varshape flip do not share the same dtype contract: the tensor path +# rejects S16, while the varshape path still supports it via flip_or_copy_var_shape. +globals().update( + cv_tools.make_op_tests( + name="flip_tensor", + runner_info=[ + ("tensor", cvcuda.flip, _flip_params), + ], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 3), + supported_dtypes={ + cvcuda.Type.U8, + cvcuda.Type.U16, + cvcuda.Type.S32, + cvcuda.Type.F32, + }, + supported_layouts={"NHWC", "HWC", "NCHW", "CHW"}, + supported_channels={1, 3, 4}, + ) +) + +globals().update( + cv_tools.make_op_tests( + name="flip_varshape", + runner_info=[ + ("image_batch", cvcuda.flip, _flip_varshape_params), + ], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 3), + supported_dtypes={ + cvcuda.Type.U8, + cvcuda.Type.U16, + cvcuda.Type.S16, + cvcuda.Type.S32, + cvcuda.Type.F32, + }, + supported_layouts={"NHWC", "HWC", "NCHW", "CHW"}, + supported_channels={1, 3, 4}, + ) +) diff --git a/tests/cvcuda/python/test_opgammacontrast.py b/tests/cvcuda/python/test_opgammacontrast.py index 08925f025..fe76edd72 100644 --- a/tests/cvcuda/python/test_opgammacontrast.py +++ b/tests/cvcuda/python/test_opgammacontrast.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,14 +15,15 @@ import cvcuda -import pytest as t +import pytest import numpy as np +import cvcuda_tools as cv_tools import cvcuda_util as util RNG = np.random.default_rng(0) -@t.mark.parametrize( +@pytest.mark.parametrize( "nimages, format, max_size, max_pixel, max_gamma", [ ( @@ -91,3 +92,197 @@ def test_op_gamma_contrastvarshape( assert out.capacity == input.capacity assert out.uniqueformat == input.uniqueformat assert out.maxsize == input.maxsize + + +@pytest.mark.parametrize( + "shape, layout, dtype", + [ + ((4, 16, 23, 3), "NHWC", np.uint8), + ((16, 23, 3), "HWC", np.float32), + ((2, 3, 16, 23), "NCHW", np.uint8), + ((3, 16, 23), "CHW", np.float32), + ((4, 16, 23, 4), "NHWC", np.float32), + ], +) +@pytest.mark.parametrize("gamma, gain", [(0.5, 1.0), (1.8, 0.9), (0.7, 1.2)]) +def test_op_gamma_contrast_scalar(shape, layout, dtype, gamma, gain): + # Host-scalar (float) gamma_contrast overload: out = gain * in**gamma with gamma/gain + # passed by value (no gamma tensor). Validated against a numpy gold. + is_float = np.issubdtype(dtype, np.floating) + if is_float: + h_src = RNG.random(shape, dtype=np.float32) + else: + h_src = RNG.integers(0, 256, size=shape, dtype=dtype) + src = util.to_cvcuda_tensor(h_src, layout) + + out = cvcuda.gamma_contrast(src, gamma, gain) + assert tuple(out.shape) == tuple(src.shape) + assert out.dtype == src.dtype + assert str(out.layout) == layout + got = util.to_cpu_numpy_buffer(out.cuda()) + + if is_float: + gold = np.clip(gain * np.power(h_src, gamma), 0.0, 1.0).astype(np.float32) + np.testing.assert_allclose(got, gold, atol=5e-7, rtol=0) + else: + norm = h_src.astype(np.float32) / 255.0 + gold = np.clip(np.rint(gain * np.power(norm, gamma) * 255.0), 0, 255).astype( + dtype + ) + assert np.max(np.abs(got.astype(np.int32) - gold.astype(np.int32))) <= 1 + + # _into overload writes into the provided tensor and is deterministic. + dst = util.to_cvcuda_tensor(np.zeros_like(h_src), layout) + tmp = cvcuda.gamma_contrast_into(dst=dst, src=src, gamma=gamma, gain=gain) + assert tmp is dst + np.testing.assert_array_equal(util.to_cpu_numpy_buffer(dst.cuda()), got) + + +def test_op_gamma_contrast_scalar_matches_tensor(): + # The float overload (scalar gamma, default gain=1) must dispatch to the scalar path + # and be bit-exact with the device-tensor overload fed a gamma tensor filled with the + # same value -- proving both overload dispatch and kernel parity. + h_src = RNG.integers(0, 256, size=(3, 16, 23, 3), dtype=np.uint8) + src = util.to_cvcuda_tensor(h_src, "NHWC") + gamma = 0.75 + + out_scalar = cvcuda.gamma_contrast(src, gamma) + gamma_tensor = util.to_cvcuda_tensor(np.full((3,), gamma, np.float32), "N") + out_tensor = cvcuda.gamma_contrast(src, gamma_tensor) + + np.testing.assert_array_equal( + util.to_cpu_numpy_buffer(out_scalar.cuda()), + util.to_cpu_numpy_buffer(out_tensor.cuda()), + ) + + +@pytest.mark.parametrize( + "round_mode,expected", [(cvcuda.Round.NEAREST, 2), (cvcuda.Round.TRUNCATE, 1)] +) +def test_op_gamma_contrast_scalar_round_mode(round_mode, expected): + h_src = np.array([[[[255]]]], dtype=np.uint8) + src = util.to_cvcuda_tensor(h_src, "NHWC") + + out = cvcuda.gamma_contrast(src, 1.0, 1.5 / 255.0, round=round_mode) + + got = util.to_cpu_numpy_buffer(out.cuda()) + np.testing.assert_array_equal(got, np.full_like(h_src, expected)) + + dst = util.to_cvcuda_tensor(np.zeros_like(h_src), "NHWC") + returned = cvcuda.gamma_contrast_into( + dst=dst, + src=src, + gamma=1.0, + gain=1.5 / 255.0, + round=round_mode, + ) + assert returned is dst + np.testing.assert_array_equal( + util.to_cpu_numpy_buffer(dst.cuda()), np.full_like(h_src, expected) + ) + + +def _gamma_contrast_op(data): + num_images = len(data) + gamma = util.create_tensor((num_images,), np.float32, "N", max_random=2.0, rng=RNG) + return cvcuda.gamma_contrast(data, gamma) + + +def _gamma_contrast_tensor_op(data): + num_samples = data.shape[0] if len(data.shape) == 4 else 1 + gamma = util.create_tensor((num_samples,), np.float32, "N", max_random=2.0, rng=RNG) + return cvcuda.gamma_contrast(data, gamma) + + +def _gamma_contrast_scalar_op(data): + # Host-scalar gamma/gain overload; the values are arbitrary valid scalars since + # these generated tests validate the input contract, not output values. + return cvcuda.gamma_contrast(data, 0.75, 1.1) + + +globals().update( + cv_tools.make_op_tests( + name="gammacontrast", + runner_info=[ + ("image_batch", _gamma_contrast_op, None), + ], + supported_formats={ + # 1 channel + cvcuda.Format.U8, + cvcuda.Format.U16, + cvcuda.Format.S16, + cvcuda.Format.S32, + cvcuda.Format.F32, + # 2 channels + cvcuda.Format._2F32, + # 3 channels + cvcuda.Format.RGB8, + cvcuda.Format.BGR8, + cvcuda.Format.RGBf32, + cvcuda.Format.BGRf32, + # 4 channels + cvcuda.Format.RGBA8, + cvcuda.Format.BGRA8, + cvcuda.Format.RGBAf32, + cvcuda.Format.BGRAf32, + # 3-channel planar (NCHW/CHW) + cvcuda.Format.RGB8p, + cvcuda.Format.BGR8p, + cvcuda.Format.RGBf32p, + cvcuda.Format.BGRf32p, + # 4-channel planar (NCHW/CHW) + cvcuda.Format.RGBA8p, + cvcuda.Format.BGRA8p, + cvcuda.Format.RGBAf32p, + cvcuda.Format.BGRAf32p, + }, + ) +) + +globals().update( + cv_tools.make_op_tests( + name="gammacontrast", + runner_info=[ + ("tensor", _gamma_contrast_tensor_op, None), + ], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 3), + supported_dtypes={ + cvcuda.Type.U8, + cvcuda.Type.U16, + cvcuda.Type.S16, + cvcuda.Type.S32, + cvcuda.Type.F32, + }, + supported_layouts={"NHWC", "HWC", "NCHW", "CHW"}, + supported_channels={1, 2, 3, 4}, + exclude_dlc=[ + (None, "NCHW", 2), + (None, "CHW", 2), + ], + ) +) + +# The host-scalar gamma/gain overload declares the same dense-tensor input contract +# as the gamma-tensor overload (its legacy infer mirrors that path's validation). +globals().update( + cv_tools.make_op_tests( + name="gammacontrast_scalar", + runner_info=[ + ("tensor", _gamma_contrast_scalar_op, None), + ], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 3), + supported_dtypes={ + cvcuda.Type.U8, + cvcuda.Type.U16, + cvcuda.Type.S16, + cvcuda.Type.S32, + cvcuda.Type.F32, + }, + supported_layouts={"NHWC", "HWC", "NCHW", "CHW"}, + supported_channels={1, 2, 3, 4}, + exclude_dlc=[ + (None, "NCHW", 2), + (None, "CHW", 2), + ], + ) +) diff --git a/tests/cvcuda/python/test_opgaussian.py b/tests/cvcuda/python/test_opgaussian.py index ab6e0fe78..f3b7b0649 100644 --- a/tests/cvcuda/python/test_opgaussian.py +++ b/tests/cvcuda/python/test_opgaussian.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,14 +15,15 @@ import cvcuda -import pytest as t +import pytest import numpy as np +import cvcuda_tools as cv_tools import cvcuda_util as util RNG = np.random.default_rng(0) -@t.mark.parametrize( +@pytest.mark.parametrize( "input_args, kernel_size, sigma, border", [ ( @@ -80,7 +81,7 @@ def test_op_gaussian(input_args, kernel_size, sigma, border): assert out.dtype == input.dtype -@t.mark.parametrize( +@pytest.mark.parametrize( "num_images,img_format,img_size,max_pixel,max_kernel_size,max_sigma,border", [ ( @@ -179,3 +180,45 @@ def test_op_gaussianvarshape( assert out.capacity == input.capacity assert out.uniqueformat == input.uniqueformat assert out.maxsize == input.maxsize + + +def _gaussian_params(dtype, layout, channels): + return { + "kernel_size": [3, 3], + "sigma": [0.5, 0.5], + "border": cvcuda.Border.CONSTANT, + } + + +def _gaussian_varshape_params(dtype, layout, channels): + return { + "max_kernel_size": (3, 3), + "kernel_size": util.to_cvcuda_tensor( + np.array([[3, 3], [3, 3]], dtype=np.int32), "NC" + ), + "sigma": util.to_cvcuda_tensor( + np.array([[1.0, 1.0], [1.0, 1.0]], dtype=np.float64), "NC" + ), + "border": cvcuda.Border.CONSTANT, + } + + +globals().update( + cv_tools.make_op_tests( + name="gaussian", + runner_info=[ + ("tensor", cvcuda.gaussian, _gaussian_params), + ("image_batch", cvcuda.gaussian, _gaussian_varshape_params), + ], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 3), + supported_dtypes={ + cvcuda.Type.U8, + cvcuda.Type.U16, + cvcuda.Type.S16, + cvcuda.Type.S32, + cvcuda.Type.F32, + }, + supported_layouts={"NHWC", "HWC", "NCHW", "CHW"}, + supported_channels={1, 3, 4}, + ) +) diff --git a/tests/cvcuda/python/test_opgaussiannoise.py b/tests/cvcuda/python/test_opgaussiannoise.py index 09831913a..543a1d368 100644 --- a/tests/cvcuda/python/test_opgaussiannoise.py +++ b/tests/cvcuda/python/test_opgaussiannoise.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,13 +13,15 @@ # See the License for the specific language governing permissions and # limitations under the License. -import cvcuda - -import pytest as t from random import randint +import cupy as cp +import cvcuda +import cvcuda_tools as cv_tools +import pytest + -@t.mark.parametrize( +@pytest.mark.parametrize( "input_args, per_channel", [ ( @@ -90,7 +92,70 @@ def test_op_gaussiannoise(input_args, per_channel): assert tmp is out -@t.mark.parametrize( +@pytest.mark.parametrize( + "shape,dtype,layout", + [ + ((2, 17, 19, 3), cvcuda.Type.U8, "NHWC"), + ((3, 17, 19), cvcuda.Type.U8, "CHW"), + ((2, 3, 17, 19), cvcuda.Type.F32, "NCHW"), + ((17, 19, 3), cvcuda.Type.F32, "HWC"), + ], +) +@pytest.mark.parametrize("clip", [True, False]) +def test_op_gaussiannoise_scalar(shape, dtype, layout, clip): + src = cvcuda.Tensor(shape, dtype, layout) + cp.asarray(src.cuda())[...].fill(127 if dtype == cvcuda.Type.U8 else 0.5) + + out = cvcuda.gaussiannoise( + src, + 3.0 if dtype == cvcuda.Type.U8 else 0.01, + 25.0 if dtype == cvcuda.Type.U8 else 0.05, + True, + seed=12345, + clip=clip, + ) + assert out.shape == src.shape + assert out.dtype == src.dtype + assert out.layout == src.layout + + dst = cvcuda.Tensor(shape, dtype, layout) + ret = cvcuda.gaussiannoise_into( + dst, + src, + 3.0 if dtype == cvcuda.Type.U8 else 0.01, + 25.0 if dtype == cvcuda.Type.U8 else 0.05, + True, + seed=12345, + clip=clip, + ) + assert ret is dst + assert cp.array_equal(cp.asarray(out.cuda()), cp.asarray(dst.cuda())) + + +def test_op_gaussiannoise_scalar_seed_contract(): + src = cvcuda.Tensor((1, 31, 37, 3), cvcuda.Type.F32, "NHWC") + cp.asarray(src.cuda())[...].fill(0.5) + + first = cvcuda.gaussiannoise(src, 0.0, 0.05, True, seed=98765) + repeated = cvcuda.gaussiannoise(src, 0.0, 0.05, True, seed=98765) + implicit_first = cvcuda.gaussiannoise(src, 0.0, 0.05, True) + implicit_second = cvcuda.gaussiannoise(src, 0.0, 0.05, True) + + assert cp.array_equal(cp.asarray(first.cuda()), cp.asarray(repeated.cuda())) + assert not cp.array_equal( + cp.asarray(implicit_first.cuda()), cp.asarray(implicit_second.cuda()) + ) + + +def test_op_gaussiannoise_scalar_rejects_invalid_parameters(): + src = cvcuda.Tensor((1, 4, 4, 3), cvcuda.Type.U8, "NHWC") + with pytest.raises(RuntimeError): + cvcuda.gaussiannoise(src, 0.0, -0.1, True) + with pytest.raises((TypeError, OverflowError)): + cvcuda.gaussiannoise(src, 0.0, 0.1, True, seed=-1) + + +@pytest.mark.parametrize( "num_images, format, min_size, max_size, per_channel", [ ( @@ -189,3 +254,34 @@ def test_op_gaussiannoise_varshape(num_images, format, min_size, max_size, per_c stream=stream, ) assert tmp is output + + +def _gaussiannoise_params(dtype, layout, channels): + return { + "mu": cvcuda.Tensor((1,), cvcuda.Type.F32, "N"), + "sigma": cvcuda.Tensor((1,), cvcuda.Type.F32, "N"), + "per_channel": False, + "seed": 12345, + } + + +globals().update( + cv_tools.make_op_tests( + name="gaussiannoise", + runner_info=[ + ("tensor", cvcuda.gaussiannoise, _gaussiannoise_params), + ("image_batch", cvcuda.gaussiannoise, _gaussiannoise_params), + ], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 3), + supported_dtypes={ + cvcuda.Type.U8, + cvcuda.Type.U16, + cvcuda.Type.S16, + cvcuda.Type.S32, + cvcuda.Type.F32, + }, + supported_layouts={"NHWC", "HWC", "NCHW", "CHW"}, + supported_channels={1, 2, 3, 4}, + exclude_dlc=[(None, "NCHW", 2), (None, "CHW", 2)], + ) +) diff --git a/tests/cvcuda/python/test_ophistogram.py b/tests/cvcuda/python/test_ophistogram.py index 1e2508cf8..ebf3a2a40 100644 --- a/tests/cvcuda/python/test_ophistogram.py +++ b/tests/cvcuda/python/test_ophistogram.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,19 +13,24 @@ # See the License for the specific language governing permissions and # limitations under the License. -import torch import cvcuda -import pytest as t +import pytest import numpy as np + import cvcuda_util as util +import cvcuda_tools as cv_tools +import cupy params = [ (((10, 16, 23, 1), np.uint8, "NHWC")), + (((10, 1, 16, 23), np.uint8, "NCHW")), (((1, 160, 3, 1), np.uint8, "NHWC")), (((16, 23, 1), np.uint8, "HWC")), + (((1, 16, 23), np.uint8, "CHW")), (((257, 23, 1), np.uint8, "HWC")), (((100, 200, 3, 1), np.uint8, "NHWC")), + (((100, 1, 200, 3), np.uint8, "NCHW")), (((50, 50, 2, 1), np.uint8, "NHWC")), (((27, 25, 1), np.uint8, "HWC")), (((10, 10, 1), np.uint8, "HWC")), @@ -33,19 +38,20 @@ ] -@t.mark.parametrize("input", params) +@pytest.mark.parametrize("input", params) def test_op_histogram(input): inputT = cvcuda.Tensor(*input) out = cvcuda.histogram(inputT) + assert out.shape[1] == 256 assert out.dtype == np.int32 - result_torch = torch.as_tensor(out.cuda()) + result = cupy.asarray(out.cuda()) - # Sum up the entries in result_torch - actual_sum = torch.sum(result_torch) + # Sum up the entries in result + actual_sum = np.sum(result.get()) total_entries = np.prod(input[0]) assert actual_sum == total_entries @@ -56,7 +62,7 @@ def test_op_histogram(input): elif rank == 4: new_shape = ((input[0][0], 256, 1), np.int32, "HWC") else: - t.fail("Invalid test input") + pytest.fail("Invalid test input") out = cvcuda.Tensor(*new_shape) tmp = cvcuda.histogram_into(histogram=out, src=inputT) @@ -65,19 +71,20 @@ def test_op_histogram(input): assert out.shape[1] == 256 assert out.dtype == np.int32 - result_torch = torch.as_tensor(out.cuda()) + result = cupy.asarray(out.cuda()) - # Sum up the entries in result_torch - actual_sum = torch.sum(result_torch) + # Sum up the entries in result + actual_sum = np.sum(result.get()) total_entries = np.prod(input[0]) assert actual_sum == total_entries -@t.mark.parametrize("input", params) +@pytest.mark.parametrize("input", params) def test_op_histogram_mask(input): inputT = cvcuda.Tensor(*input) - arr = np.random.random(input[0]) + rng = np.random.default_rng(0) + arr = rng.random(input[0]) arr = (arr * 3).astype(np.uint8) maskT = util.to_cvcuda_tensor(arr, input[2]) @@ -87,10 +94,10 @@ def test_op_histogram_mask(input): assert out.shape[1] == 256 assert out.dtype == np.int32 - result_torch = torch.as_tensor(out.cuda()) + result = cupy.asarray(out.cuda()) - # Sum up the entries in result_torch - actual_sum = torch.sum(result_torch) + # Sum up the entries in result + actual_sum = np.sum(result.get()) masked_entries = np.count_nonzero(arr) assert actual_sum == masked_entries @@ -101,7 +108,7 @@ def test_op_histogram_mask(input): elif rank == 4: new_shape = ((input[0][0], 256, 1), np.int32, "HWC") else: - t.fail("Invalid test input") + pytest.fail("Invalid test input") out = cvcuda.Tensor(*new_shape) tmp = cvcuda.histogram_into(histogram=out, mask=maskT, src=inputT) @@ -110,8 +117,20 @@ def test_op_histogram_mask(input): assert out.shape[1] == 256 assert out.dtype == np.int32 - result_torch = torch.as_tensor(out.cuda()) + result = cupy.asarray(out.cuda()) - # Sum up the entries in result_torch - actual_sum = torch.sum(result_torch) + # Sum up the entries in result + actual_sum = np.sum(result.get()) assert actual_sum == masked_entries + + +globals().update( + cv_tools.make_op_tests( + name="histogram", + runner_info=[("tensor", cvcuda.histogram, None)], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 1), + supported_dtypes={cvcuda.Type.U8}, + supported_layouts={"NHWC", "HWC", "NCHW", "CHW"}, + supported_channels={1}, + ) +) diff --git a/tests/cvcuda/python/test_ophistogrameq.py b/tests/cvcuda/python/test_ophistogrameq.py index 37af66437..be9952fa8 100644 --- a/tests/cvcuda/python/test_ophistogrameq.py +++ b/tests/cvcuda/python/test_ophistogrameq.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,14 +15,15 @@ import cvcuda -import pytest as t +import pytest import numpy as np +import cvcuda_tools as cv_tools import cvcuda_util as util RNG = np.random.default_rng(0) -@t.mark.parametrize( +@pytest.mark.parametrize( "input", [ (((1, 460, 640, 1), cvcuda.Type.U8, "NHWC")), @@ -65,7 +66,7 @@ def test_op_histogrameq(input): assert out.dtype == inputTensor.dtype -@t.mark.parametrize( +@pytest.mark.parametrize( "num_images, format, max_size", [ ( @@ -145,3 +146,23 @@ def test_op_histogrameq_varshape(num_images, format, max_size): assert out.capacity == b_src.capacity assert out.uniqueformat == b_src.uniqueformat assert out.maxsize <= max_size + + +def _histogrameq_params(dtype, layout, channels): + return {"dtype": cvcuda.Type.U8} + + +globals().update( + cv_tools.make_op_tests( + name="histogrameq", + runner_info=[ + ("tensor", cvcuda.histogrameq, _histogrameq_params), + ("image_batch", cvcuda.histogrameq, None), + ], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 3), + supported_dtypes={cvcuda.Type.U8}, + supported_layouts={"NHWC", "HWC", "NCHW", "CHW"}, + supported_channels={1, 2, 3, 4}, + exclude_dlc=[(None, "NCHW", 2), (None, "CHW", 2)], + ) +) diff --git a/tests/cvcuda/python/test_ophqresize.py b/tests/cvcuda/python/test_ophqresize.py index 84d280ae6..11a4be6f3 100644 --- a/tests/cvcuda/python/test_ophqresize.py +++ b/tests/cvcuda/python/test_ophqresize.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,9 +14,12 @@ # limitations under the License. import cvcuda -import pytest as t + +import pytest import cvcuda_util as util import numpy as np +import cvcuda_types as cv_types +import cvcuda_tools as cv_tools RNG = np.random.default_rng(12345) @@ -32,7 +35,7 @@ def get_shape(in_shape, layout, out_size): ) -@t.mark.parametrize( +@pytest.mark.parametrize( "src_args, dst_args, interpolation_args, roi", [ ( @@ -116,7 +119,7 @@ def test_op_hq_resize_api(src_args, dst_args, interpolation_args, roi): assert t_dst.shape == out_shape -@t.mark.parametrize( +@pytest.mark.parametrize( "num_samples, src_args, dst_type, interpolation_args, roi", [ ( @@ -205,7 +208,7 @@ def test_op_hq_resize_var_shape_api( ) -@t.mark.parametrize( +@pytest.mark.parametrize( "num_samples, src_args, dst_type, interpolation_args, use_roi", [ ( @@ -303,3 +306,84 @@ def test_op_hq_resize_tensor_batch_api( assert b_dst.dtype == dst_type for i in range(num_samples): assert b_dst[i].shape == get_shape(src_shape, layout, out_sizes[i]) + + +def _hqresize_params(dtype, layout, channels): + return { + "out_size": (12, 12), + "interpolation": cvcuda.Interp.LINEAR, + } + + +def _hqresize_varshape_params(dtype, layout, channels): + return { + "out_size": [(12, 12), (12, 12)], + "interpolation": cvcuda.Interp.LINEAR, + } + + +def _hqresize_tensorbatch_params(dtype, layout, channels): + if "D" in layout: + return { + "out_size": [(1, 12, 12), (1, 12, 12)], + "interpolation": cvcuda.Interp.LINEAR, + } + return { + "out_size": [(12, 12), (12, 12)], + "interpolation": cvcuda.Interp.LINEAR, + } + + +_interleaved_layouts = {"NHWC", "NHW", "HWC", "HW"} +_planar_layouts = {"NCHW", "CHW", "NFHW", "FHW", "FHWC"} +# 2D channel-first layouts support multi-channel planar resize (processed plane-by-plane). +_planar_2d_layouts = {"NCHW", "CHW"} + + +globals().update( + cv_tools.make_op_tests( + name="hq_resize", + runner_info=[ + ("tensor", cvcuda.hq_resize, _hqresize_params), + ("image_batch", cvcuda.hq_resize, _hqresize_varshape_params), + ], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 3), + supported_dtypes={ + cvcuda.Type.U8, + cvcuda.Type.U16, + cvcuda.Type.S16, + cvcuda.Type.F32, + }, + supported_layouts=_interleaved_layouts | _planar_layouts, + supported_channels=cv_types.CHANNELS, + # Only 2D channel-first planar layouts support multi-channel; frame/3D planar layouts + # (NFHW/FHW/FHWC) remain single-channel. + exclude_dlc=[ + (None, layout, ch) + for layout in _planar_layouts - _planar_2d_layouts + for ch in cv_types.CHANNELS - {1} + ], + ) +) + + +globals().update( + cv_tools.make_op_tests( + name="hq_resize_tensor_batch", + runner_info=[ + ("tensor_batch", cvcuda.hq_resize, _hqresize_tensorbatch_params), + ], + keystone_dlc=(cvcuda.Type.U8, "NCHW", 1), + supported_dtypes={ + cvcuda.Type.U8, + cvcuda.Type.U16, + cvcuda.Type.S16, + cvcuda.Type.F32, + }, + # NCHW/CHW (channel-first 2D) planar tensor batches are expanded plane-by-plane and accept + # any channel count; the interleaved path supports dynamic channels too, so there is no + # channel-count upper limit to assert as negative. + supported_layouts={"HW", "HWC", "DHW", "DHWC", "NCHW", "CHW"}, + supported_channels=cv_types.CHANNELS, + ) +) diff --git a/tests/cvcuda/python/test_ophqresize_cache.py b/tests/cvcuda/python/test_ophqresize_cache.py new file mode 100644 index 000000000..47c0766e7 --- /dev/null +++ b/tests/cvcuda/python/test_ophqresize_cache.py @@ -0,0 +1,406 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""Regression tests for HQResize Python workspace-requirements caching.""" + +import subprocess +import sys +import textwrap + +import pytest + + +_SCRIPT_PRELUDE = textwrap.dedent( + """ + import gc + + import cupy + import numpy + + import cvcuda + + + def make_tensor(shape, value, layout="HWC"): + array = cupy.full(shape, value, dtype=numpy.uint8) + return array, cvcuda.as_tensor(array, layout) + + + def make_planar_tensor(hw, values): + array = cupy.empty((len(values), *hw), dtype=numpy.uint8) + for channel, value in enumerate(values): + array[channel].fill(value) + return array, cvcuda.as_tensor(array, "CHW") + + + def make_batch(capacity, samples, layout="HWC"): + batch = cvcuda.TensorBatch(capacity) + arrays = [] + for shape, value in samples: + array, tensor = make_tensor(shape, value, layout) + arrays.append(array) + batch.pushback(tensor) + return batch, arrays + + + def make_planar_batch(capacity, hw, samples): + batch = cvcuda.TensorBatch(capacity) + arrays = [] + for values in samples: + array, tensor = make_planar_tensor(hw, values) + arrays.append(array) + batch.pushback(tensor) + return batch, arrays + + + def run_resize(dst, src, stream, **kwargs): + out = cvcuda.hq_resize_into(dst, src, stream=stream, **kwargs) + stream.sync() + assert out is dst + + + def run_cubic(dst, src, stream, roi=None): + kwargs = dict(interpolation=cvcuda.Interp.CUBIC, antialias=True) + if roi is not None: + kwargs["roi"] = roi + run_resize(dst, src, stream, **kwargs) + + + def assert_uniform(array, expected): + assert bool(cupy.all(array == expected)) + + + cvcuda.clear_cache() + """ +) + +_SCRIPT_EPILOGUE = textwrap.dedent( + """ + cvcuda.internal.syncAuxStream() + print("PASS", flush=True) + """ +) + + +def _run_isolated(script, **parameters): + # WorkspaceCache is process-wide and is not cleared by cvcuda.clear_cache(). + # A fresh process ensures that an unrelated larger cached workspace cannot + # hide an undersized stale requirements entry. + assignments = "\n".join(f"{name} = {value!r}" for name, value in parameters.items()) + source = "\n".join( + (_SCRIPT_PRELUDE, assignments, textwrap.dedent(script), _SCRIPT_EPILOGUE) + ) + result = subprocess.run( + [sys.executable, "-c", source], + capture_output=True, + text=True, + timeout=60, + ) + assert result.returncode == 0 and "PASS" in result.stdout, ( + f"isolated HQResize cache regression failed (returncode={result.returncode})\n" + f"stdout:\n{result.stdout[:2000]}\n" + f"stderr:\n{result.stderr[:4000]}" + ) + + +@pytest.mark.parametrize("changed_batch", ["input_shape", "output_shape"]) +def test_hqresize_tensorbatch_cache_invalidates_mutated_shapes(changed_batch): + _run_isolated( + """ + prime_src_size = 8 if changed_batch == "input_shape" else 256 + final_dst_size = 4 if changed_batch == "input_shape" else 128 + + prime_src_array, prime_src = make_tensor( + (prime_src_size, prime_src_size, 1), 37 + ) + prime_dst_array, prime_dst = make_tensor((4, 4, 1), 0) + if changed_batch == "input_shape": + final_src_array, final_src = make_tensor((256, 256, 1), 91) + final_dst_array, final_dst = prime_dst_array, prime_dst + expected_value = 91 + else: + final_src_array, final_src = prime_src_array, prime_src + final_dst_array, final_dst = make_tensor((128, 128, 1), 0) + expected_value = 37 + + src = cvcuda.TensorBatch(1) + dst = cvcuda.TensorBatch(1) + src.pushback(prime_src) + dst.pushback(prime_dst) + stream = cvcuda.Stream() + + run_cubic(dst, src, stream) + assert_uniform(prime_dst_array, 37) + + # Preserve both TensorBatch identities while replacing exactly one live + # shape. The larger contraction needs more intermediate workspace. + if changed_batch == "input_shape": + src[0] = final_src + else: + dst[0] = final_dst + + run_cubic(dst, src, stream) + assert_uniform(final_dst_array, expected_value) + + # Replacing tensors without changing their signature should take the + # requirements-cache hit path while still using only the live tensor + # contents and handles. + replacement_src_array, replacement_src = make_tensor((256, 256, 1), 123) + replacement_dst_array, replacement_dst = make_tensor( + (final_dst_size, final_dst_size, 1), 0 + ) + src[0] = replacement_src + dst[0] = replacement_dst + + del prime_src_array, prime_src, prime_dst_array, prime_dst + del final_src_array, final_src, final_dst_array, final_dst + gc.collect() + + run_cubic(dst, src, stream) + assert_uniform(replacement_dst_array, 123) + """, + changed_batch=changed_batch, + ) + + +def test_hqresize_tensorbatch_cache_invalidates_batch_length_growth(): + _run_isolated( + """ + src, src_arrays = make_batch(2, [((8, 8, 1), 17)]) + dst, dst_arrays = make_batch(2, [((4, 4, 1), 0)]) + stream = cvcuda.Stream() + + run_cubic(dst, src, stream) + assert_uniform(dst_arrays[0], 17) + + # Keep both batch identities while growing their live lengths. The + # added contraction requires more workspace than the priming call. + src_array, src_tensor = make_tensor((256, 256, 1), 93) + dst_array, dst_tensor = make_tensor((4, 4, 1), 0) + src.pushback(src_tensor) + dst.pushback(dst_tensor) + src_arrays.append(src_array) + dst_arrays.append(dst_array) + dst_arrays[0].fill(0) + + run_cubic(dst, src, stream) + for output, expected in zip(dst_arrays, (17, 93)): + assert_uniform(output, expected) + """ + ) + + +def test_hqresize_tensorbatch_cache_invalidates_ordered_heterogeneous_shapes(): + _run_isolated( + """ + # The final call keeps the same input-shape multiset but reverses its + # order relative to these heterogeneous output shapes. + src, prime_src = make_batch( + 2, [((8, 8, 1), 19), ((256, 256, 1), 73)] + ) + dst, outputs = make_batch(2, [((4, 4, 1), 0), ((128, 128, 1), 0)]) + stream = cvcuda.Stream() + + run_cubic(dst, src, stream) + for output, expected in zip(outputs, (19, 73)): + assert_uniform(output, expected) + + final_src = [] + for index, (shape, value) in enumerate( + (((256, 256, 1), 101), ((8, 8, 1), 149)) + ): + array, tensor = make_tensor(shape, value) + final_src.append(array) + src[index] = tensor + for output in outputs: + output.fill(0) + + run_cubic(dst, src, stream) + for output, expected in zip(outputs, (101, 149)): + assert_uniform(output, expected) + """ + ) + + +def test_hqresize_planar_tensorbatch_cache_invalidates_expanded_sample_count(): + _run_isolated( + """ + src = cvcuda.TensorBatch(1) + dst = cvcuda.TensorBatch(1) + prime_src_array, prime_src = make_planar_tensor((256, 256), (13,)) + prime_dst_array, prime_dst = make_planar_tensor((4, 4), (0,)) + src.pushback(prime_src) + dst.pushback(prime_dst) + stream = cvcuda.Stream() + + run_cubic(dst, src, stream) + assert_uniform(prime_dst_array, 13) + + # Only C changes. Planar expansion therefore grows from one workspace + # sample to three while both TensorBatch lengths remain unchanged. + expected_planes = (31, 67, 109) + final_src_array, final_src = make_planar_tensor( + (256, 256), expected_planes + ) + final_dst_array, final_dst = make_planar_tensor((4, 4), (0, 0, 0)) + src[0] = final_src + dst[0] = final_dst + + run_cubic(dst, src, stream) + expected = cupy.asarray(expected_planes, dtype=numpy.uint8)[:, None, None] + assert_uniform(final_dst_array, expected) + """ + ) + + +@pytest.mark.parametrize( + "changed_parameter", + ["roi", "min_interpolation", "mag_interpolation", "antialias"], +) +def test_hqresize_planar_tensorbatch_cache_invalidates_call_parameters( + changed_parameter, +): + _run_isolated( + """ + if changed_parameter == "mag_interpolation": + src_hw = (256, 64) + dst_hw = (1024, 1024) + elif changed_parameter in ("min_interpolation", "antialias"): + src_hw = (1024, 1024) + dst_hw = (256, 64) + else: + src_hw = (256, 256) + dst_hw = (8, 8) + + expected_planes = ((11, 23, 47), (71, 89, 107)) + src, src_arrays = make_planar_batch(2, src_hw, expected_planes) + dst, outputs = make_planar_batch(2, dst_hw, ((0, 0, 0),) * 2) + stream = cvcuda.Stream() + + # Change exactly one requirements input. A single ROI is intentionally + # broadcast because planar TensorBatch execution expands N tensors to + # N*C planes internally. The asymmetric shapes force filter support to + # change the intermediate processing order and workspace size. + full_roi = [(0, 0, *src_hw)] + prime = dict( + min_interpolation=cvcuda.Interp.LANCZOS, + mag_interpolation=cvcuda.Interp.LANCZOS, + antialias=True, + roi=full_roi, + ) + final = dict(prime) + + if changed_parameter == "roi": + prime["roi"] = [(120, 120, 136, 136)] + final["roi"] = [(16, 16, 240, 240)] + elif changed_parameter == "min_interpolation": + prime["min_interpolation"] = cvcuda.Interp.NEAREST + final["min_interpolation"] = cvcuda.Interp.LANCZOS + elif changed_parameter == "mag_interpolation": + prime["mag_interpolation"] = cvcuda.Interp.NEAREST + final["mag_interpolation"] = cvcuda.Interp.LANCZOS + prime["antialias"] = final["antialias"] = False + elif changed_parameter == "antialias": + prime["antialias"] = False + final["antialias"] = True + + run_resize(dst, src, stream, **prime) + run_resize(dst, src, stream, **final) + + for dst_array, plane_values in zip(outputs, expected_planes): + expected = cupy.asarray(plane_values, dtype=numpy.uint8)[:, None, None] + assert_uniform(dst_array, expected) + """, + changed_parameter=changed_parameter, + ) + + +@pytest.mark.parametrize("roi_change", ["count", "order"]) +def test_hqresize_tensorbatch_cache_invalidates_roi_structure(roi_change): + _run_isolated( + """ + small_roi = (120, 120, 136, 136) + large_roi = (16, 16, 240, 240) + prime_rois = [small_roi] if roi_change == "count" else [small_roi, large_roi] + final_rois = [large_roi, small_roi] + + src, src_arrays = make_batch( + 2, [((256, 256, 1), 29), ((256, 256, 1), 83)] + ) + dst, outputs = make_batch(2, [((8, 8, 1), 0), ((128, 128, 1), 0)]) + stream = cvcuda.Stream() + options = dict( + min_interpolation=cvcuda.Interp.LANCZOS, + mag_interpolation=cvcuda.Interp.LANCZOS, + antialias=True, + ) + + run_resize(dst, src, stream, roi=prime_rois, **options) + for output, expected in zip(outputs, (29, 83)): + assert_uniform(output, expected) + + # Keep shapes and batch identities fixed. The count case changes a + # broadcast ROI to per-sample ROIs; the order case changes only which + # ROI is paired with each heterogeneous output shape. + for output in outputs: + output.fill(0) + run_resize(dst, src, stream, roi=final_rois, **options) + for output, expected in zip(outputs, (29, 83)): + assert_uniform(output, expected) + """, + roi_change=roi_change, + ) + + +def test_hqresize_3d_tensorbatch_cache_hit_and_invalidation(): + _run_isolated( + """ + src = cvcuda.TensorBatch(1) + dst = cvcuda.TensorBatch(1) + prime_src_array, prime_src = make_tensor((8, 8, 8, 1), 17, "DHWC") + prime_dst_array, prime_dst = make_tensor((4, 4, 4, 1), 0, "DHWC") + src.pushback(prime_src) + dst.pushback(prime_dst) + stream = cvcuda.Stream() + small_roi = [(0, 0, 0, 8, 8, 8)] + + run_cubic(dst, src, stream, small_roi) + assert_uniform(prime_dst_array, 17) + + # Same complete signature, but new handles and contents: this is the + # 3D cache-hit path and must not retain live tensors from the prime. + hit_src_array, hit_src = make_tensor((8, 8, 8, 1), 43, "DHWC") + hit_dst_array, hit_dst = make_tensor((4, 4, 4, 1), 0, "DHWC") + src[0] = hit_src + dst[0] = hit_dst + run_cubic(dst, src, stream, small_roi) + assert_uniform(hit_dst_array, 43) + + # Change the 3D input/output shapes while preserving the six-component + # ROI. The larger output also makes stale prime requirements unsafe. + shape_src_array, shape_src = make_tensor((64, 64, 64, 1), 71, "DHWC") + shape_dst_array, shape_dst = make_tensor((32, 32, 32, 1), 0, "DHWC") + src[0] = shape_src + dst[0] = shape_dst + run_cubic(dst, src, stream, small_roi) + assert_uniform(shape_dst_array, 71) + + # Keep the 3D shapes fixed and enlarge only the ROI. This also grows + # the required contraction workspace substantially. + large_roi = [(0, 0, 0, 64, 64, 64)] + shape_dst_array.fill(0) + run_cubic(dst, src, stream, large_roi) + assert_uniform(shape_dst_array, 71) + """ + ) diff --git a/tests/cvcuda/python/test_opinpaint.py b/tests/cvcuda/python/test_opinpaint.py index bb268aee8..6068eab47 100644 --- a/tests/cvcuda/python/test_opinpaint.py +++ b/tests/cvcuda/python/test_opinpaint.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,13 +13,15 @@ # See the License for the specific language governing permissions and # limitations under the License. -import cvcuda - -import pytest as t from random import randint +import cvcuda +import pytest +import cvcuda_tools as cv_tools +import cvcuda_types as cv_types + -@t.mark.parametrize( +@pytest.mark.parametrize( "input_args, mask_args, inpaintRadius", [ ( @@ -73,7 +75,7 @@ def test_op_inpaint(input_args, mask_args, inpaintRadius): assert tmp is out -@t.mark.parametrize( +@pytest.mark.parametrize( "num_images, format, min_size, max_size, inpaintRadius", [ ( @@ -145,3 +147,36 @@ def test_op_inpaint_varshape(num_images, format, min_size, max_size, inpaintRadi stream=stream, ) assert tmp is output + + +def _inpaint_params(dtype, layout, channels): + mask_shape = cv_types.resolve_shape(layout, 1) + return { + "masks": cvcuda.Tensor(mask_shape, cvcuda.Type.U8, layout), + "inpaintRadius": 5.0, + } + + +def _inpaint_varshape(src: cvcuda.ImageBatchVarShape): + num_images = len(src) + masks = cvcuda.ImageBatchVarShape(num_images) + for img in src: + mask = cvcuda.Image(img.size, cvcuda.Format.U8) + masks.pushback(mask) + return cvcuda.inpaint(src, masks, inpaintRadius=5.0) + + +globals().update( + cv_tools.make_op_tests( + name="inpaint", + runner_info=[ + ("tensor", cvcuda.inpaint, _inpaint_params), + ("image_batch", _inpaint_varshape, None), + ], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 3), + supported_dtypes={cvcuda.Type.U8, cvcuda.Type.S32, cvcuda.Type.F32}, + supported_layouts={"NHWC", "HWC", "NCHW", "CHW"}, + supported_channels={1, 2, 3, 4}, + exclude_dlc=[(None, "NCHW", 2), (None, "CHW", 2)], + ) +) diff --git a/tests/cvcuda/python/test_opinvert.py b/tests/cvcuda/python/test_opinvert.py new file mode 100644 index 000000000..21f8c41d3 --- /dev/null +++ b/tests/cvcuda/python/test_opinvert.py @@ -0,0 +1,134 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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 cvcuda + +import pytest +import numpy as np +import cupy + +import cvcuda_util as util + +RNG = np.random.default_rng(0) + + +def invert_ref(src): + """Independent CPU oracle: out = bound - in, where bound is the dtype maximum + (255 / 65535) for unsigned integers and 1.0 for float -- mirrors the operator + and torchvision.transforms.v2.functional.invert / OpenCV cv::bitwise_not.""" + if np.issubdtype(src.dtype, np.floating): + return (np.float32(1.0) - src).astype(src.dtype) + bound = np.iinfo(src.dtype).max + return (bound - src.astype(np.int64)).astype(src.dtype) + + +def assert_inverted(got, src): + ref = invert_ref(src) + # A single-channel image reads back as (H, W) while the oracle is (H, W, 1); + # normalize only that known case and assert the exact shape otherwise, so a + # real shape/layout regression still fails the test. + got = np.asarray(got) + if got.shape == ref.shape[:-1] and ref.shape[-1] == 1: + got = got[..., np.newaxis] + else: + assert ( + got.shape == ref.shape + ), f"shape mismatch: got={got.shape}, expected={ref.shape}" + if np.issubdtype(src.dtype, np.floating): + np.testing.assert_allclose(got, ref, rtol=0, atol=1e-6) + else: + np.testing.assert_array_equal(got, ref) + + +@pytest.mark.parametrize( + "shape, dtype, layout", + [ + ((5, 16, 23, 4), np.uint8, "NHWC"), # interleaved RGBA u8 + ((4, 9, 3), np.uint8, "HWC"), # interleaved RGB u8, no batch + ((3, 88, 13, 1), np.uint16, "NHWC"), # u16 single channel + ((2, 4, 16, 23), np.float32, "NCHW"), # planar float + ((3, 8, 8), np.float32, "CHW"), # planar float, no batch + ], +) +def test_op_invert(shape, dtype, layout): + src_h = util.generate_data(shape, dtype, rng=RNG) + t_src = util.to_cvcuda_tensor(src_h, layout) + + # allocating variant + out = cvcuda.invert(t_src) + assert out.layout == t_src.layout + assert out.shape == t_src.shape + assert out.dtype == t_src.dtype + assert_inverted(cupy.asarray(out.cuda()).get(), src_h) + + # into variant + stream = cvcuda.Stream() + out = cvcuda.Tensor(t_src.shape, t_src.dtype, t_src.layout) + tmp = cvcuda.invert_into(src=t_src, dst=out, stream=stream) + stream.sync() + assert tmp is out + assert out.layout == t_src.layout + assert out.shape == t_src.shape + assert out.dtype == t_src.dtype + assert_inverted(cupy.asarray(out.cuda()).get(), src_h) + + +@pytest.mark.parametrize( + "num_images, img_format, img_size, max_pixel", + [ + (10, cvcuda.Format.RGB8, (123, 321), 256), + (7, cvcuda.Format.RGBf32, (62, 35), 1.0), + (1, cvcuda.Format.U16, (33, 48), 1234), + (4, cvcuda.Format.RGBA8, (26, 52), 256), + ], +) +def test_op_invert_varshape(num_images, img_format, img_size, max_pixel): + # Build the batch from known host data so output values can be checked. + w, h = img_size + dtype = util.get_numpy_dtype_for_format(img_format) + srcs_h = [ + util.generate_data((h, w, img_format.channels), dtype, max_pixel, RNG) + for _ in range(num_images) + ] + src_batch = cvcuda.ImageBatchVarShape(num_images) + for s in srcs_h: + src_batch.pushback(util.to_cvcuda_image(s)) + + # allocating variant + out = cvcuda.invert(src_batch) + assert len(out) == len(src_batch) + assert out.capacity == src_batch.capacity + assert out.uniqueformat == src_batch.uniqueformat + assert out.maxsize == src_batch.maxsize + for got_img, src_h in zip(out, srcs_h): + assert_inverted(cupy.asarray(got_img.cuda()).get(), src_h) + + # into variant + stream = cvcuda.Stream() + out = util.clone_image_batch(src_batch) + tmp = cvcuda.invert_into(src=src_batch, dst=out, stream=stream) + stream.sync() + assert tmp is out + assert len(out) == len(src_batch) + assert out.capacity == src_batch.capacity + for got_img, src_h in zip(out, srcs_h): + assert_inverted(cupy.asarray(got_img.cuda()).get(), src_h) + + +def test_op_invert_negative_dtype(): + # float16 is outside the supported dtype set (u8/u16/f32) and must be rejected. + src = cvcuda.Tensor((1, 16, 16, 3), np.float16, "NHWC") + with pytest.raises(RuntimeError): + cvcuda.invert(src) diff --git a/tests/cvcuda/python/test_opjointbilateralfilter.py b/tests/cvcuda/python/test_opjointbilateralfilter.py index a86f6774e..67bb96f0a 100644 --- a/tests/cvcuda/python/test_opjointbilateralfilter.py +++ b/tests/cvcuda/python/test_opjointbilateralfilter.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,15 +14,15 @@ # limitations under the License. import cvcuda - -import pytest as t +import pytest import numpy as np +import cvcuda_tools as cv_tools import cvcuda_util as util RNG = np.random.default_rng(0) -@t.mark.parametrize( +@pytest.mark.parametrize( "input_args, diameter, sigma_color, sigma_space, border", [ ( @@ -85,7 +85,7 @@ def test_op_joint_bilateral_filter( assert out.dtype == input.dtype -@t.mark.parametrize( +@pytest.mark.parametrize( "nimages, format, max_size, max_pixel, max_diameter, max_sc, max_ss, border", [ ( @@ -184,3 +184,56 @@ def test_op_joint_bilateral_filtervarshape( assert out.capacity == input.capacity assert out.uniqueformat == input.uniqueformat assert out.maxsize == input.maxsize + + +def _jointbilateralfilter(src): + # srcColor needs to match src shape, so we use src as both + return cvcuda.joint_bilateral_filter( + src, + src, + diameter=5, + sigma_color=1.0, + sigma_space=1.0, + border=cvcuda.Border.CONSTANT, + ) + + +def _jointbilateralfilter_varshape(src: cvcuda.ImageBatchVarShape): + num_images = len(src) + diameter = util.to_cvcuda_tensor(np.full((num_images,), 5, dtype=np.int32), "N") + sigma_color = util.to_cvcuda_tensor( + np.full((num_images,), 1.0, dtype=np.float32), "N" + ) + sigma_space = util.to_cvcuda_tensor( + np.full((num_images,), 1.0, dtype=np.float32), "N" + ) + return cvcuda.joint_bilateral_filter( + src, + src, + diameter=diameter, + sigma_color=sigma_color, + sigma_space=sigma_space, + border=cvcuda.Border.CONSTANT, + ) + + +globals().update( + cv_tools.make_op_tests( + name="joint_bilateral_filter", + runner_info=[ + ("tensor", _jointbilateralfilter, None), + ("image_batch", _jointbilateralfilter_varshape, None), + ], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 3), + supported_dtypes={ + cvcuda.Type.U8, + cvcuda.Type.U16, + cvcuda.Type.S16, + cvcuda.Type.S32, + cvcuda.Type.F32, + }, + supported_layouts={"NHWC", "HWC", "NCHW", "CHW"}, + supported_channels={1, 2, 3, 4}, + exclude_dlc=[(None, "NCHW", 2), (None, "CHW", 2)], + ) +) diff --git a/tests/cvcuda/python/test_opjpegcompressiondistortion.py b/tests/cvcuda/python/test_opjpegcompressiondistortion.py new file mode 100644 index 000000000..6450be875 --- /dev/null +++ b/tests/cvcuda/python/test_opjpegcompressiondistortion.py @@ -0,0 +1,180 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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 cvcuda + +import pytest +import numpy as np + +import cvcuda_tools as cv_tools +import cvcuda_util as util + +RNG = np.random.default_rng(0) + + +@pytest.mark.parametrize( + "tensor_params, quality", + [ + (((5, 16, 23, 3), np.uint8, "NHWC"), 50), + (((4, 9, 3), np.uint8, "HWC"), 1), + (((3, 3, 88, 13), np.uint8, "NCHW"), 100), + (((3, 16, 23), np.uint8, "CHW"), 75), + (((8, 8, 1), np.uint8, "HWC"), 50), + (((3, 12, 12, 1), np.uint8, "NHWC"), 10), + ], +) +def test_op_jpegcompressiondistortion(tensor_params, quality): + src = cvcuda.Tensor(*tensor_params) + + out = cvcuda.jpeg_compression_distortion(src, quality) + assert out.layout == src.layout + assert out.shape == src.shape + assert out.dtype == src.dtype + + stream = cvcuda.Stream() + out = cvcuda.Tensor(src.shape, src.dtype, src.layout) + tmp = cvcuda.jpeg_compression_distortion_into( + src=src, dst=out, quality=quality, stream=stream + ) + assert tmp is out + assert out.layout == src.layout + assert out.shape == src.shape + assert out.dtype == src.dtype + + +def test_op_jpegcompressiondistortion_quality_tensor(): + batch = 4 + src = cvcuda.Tensor((batch, 16, 23, 3), np.uint8, "NHWC") + quality = util.create_tensor((batch,), np.int32, "N", max_random=100, rng=RNG) + + out = cvcuda.jpeg_compression_distortion(src, quality) + assert out.layout == src.layout + assert out.shape == src.shape + assert out.dtype == src.dtype + + stream = cvcuda.Stream() + out = cvcuda.Tensor(src.shape, src.dtype, src.layout) + tmp = cvcuda.jpeg_compression_distortion_into( + src=src, dst=out, quality=quality, stream=stream + ) + assert tmp is out + assert out.shape == src.shape + + +@pytest.mark.parametrize( + "num_images, img_format, img_size, quality", + [ + (10, cvcuda.Format.RGB8, (123, 321), 50), + (7, cvcuda.Format.RGB8, (62, 35), 10), + (4, cvcuda.Format.U8, (26, 52), 90), + ], +) +def test_op_jpegcompressiondistortion_varshape( + num_images, img_format, img_size, quality +): + src = util.create_image_batch( + num_images, img_format, size=img_size, max_random=256, rng=RNG + ) + + out = cvcuda.jpeg_compression_distortion(src, quality) + assert len(out) == len(src) + assert out.capacity == src.capacity + assert out.uniqueformat == src.uniqueformat + assert out.maxsize == src.maxsize + + stream = cvcuda.Stream() + out = util.clone_image_batch(src) + tmp = cvcuda.jpeg_compression_distortion_into( + src=src, dst=out, quality=quality, stream=stream + ) + assert tmp is out + assert len(out) == len(src) + assert out.capacity == src.capacity + + +def test_op_jpegcompressiondistortion_varshape_quality_tensor(): + num_images = 5 + src = util.create_image_batch( + num_images, cvcuda.Format.RGB8, size=(40, 30), max_random=256, rng=RNG + ) + quality = util.create_tensor((num_images,), np.int32, "N", max_random=100, rng=RNG) + + out = cvcuda.jpeg_compression_distortion(src, quality) + assert len(out) == len(src) + assert out.uniqueformat == src.uniqueformat + + +def test_op_jpegcompressiondistortion_negative_dtype(): + # float32 is outside the supported dtype set (u8 only) and must be rejected. + src = cvcuda.Tensor((1, 16, 16, 3), np.float32, "NHWC") + with pytest.raises(RuntimeError): + cvcuda.jpeg_compression_distortion(src, 50) + + +def test_op_jpegcompressiondistortion_negative_channels(): + # 4-channel (RGBA) is outside the supported channel set (1/3) and must be rejected. + src = cvcuda.Tensor((1, 16, 16, 4), np.uint8, "NHWC") + with pytest.raises(RuntimeError): + cvcuda.jpeg_compression_distortion(src, 50) + + +@pytest.mark.parametrize("quality", [0, 101, -5]) +def test_op_jpegcompressiondistortion_negative_scalar_quality(quality): + # The scalar path host-validates quality; out-of-range values are rejected, not clamped. + src = cvcuda.Tensor((1, 16, 16, 3), np.uint8, "NHWC") + with pytest.raises(RuntimeError): + cvcuda.jpeg_compression_distortion(src, quality) + + +@pytest.mark.parametrize( + "quality_params", + [ + ((2,), np.float32, "N"), # wrong dtype + ((1,), np.int32, "N"), # wrong length (batch is 2) + ((2, 1), np.int32, "NC"), # wrong rank + ], +) +def test_op_jpegcompressiondistortion_negative_quality_tensor(quality_params): + src = cvcuda.Tensor((2, 16, 16, 3), np.uint8, "NHWC") + quality = cvcuda.Tensor(*quality_params) + with pytest.raises(RuntimeError): + cvcuda.jpeg_compression_distortion(src, quality) + + +def _jpeg_compression_distortion_params(dtype, layout, channels): + return {"quality": 50} + + +globals().update( + cv_tools.make_op_tests( + name="jpeg_compression_distortion", + runner_info=[ + ( + "tensor", + cvcuda.jpeg_compression_distortion, + _jpeg_compression_distortion_params, + ), + ( + "image_batch", + cvcuda.jpeg_compression_distortion, + _jpeg_compression_distortion_params, + ), + ], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 3), + supported_dtypes={cvcuda.Type.U8}, + supported_layouts={"NHWC", "HWC", "NCHW", "CHW"}, + supported_channels={1, 3}, + ) +) diff --git a/tests/cvcuda/python/test_oplabel.py b/tests/cvcuda/python/test_oplabel.py index ae5661854..4311c09cf 100644 --- a/tests/cvcuda/python/test_oplabel.py +++ b/tests/cvcuda/python/test_oplabel.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,8 +15,10 @@ import cvcuda -import pytest as t +import pytest import numpy as np +import cvcuda_types as cv_types +import cvcuda_tools as cv_tools DEF_OUT_DTYPE = np.int32 DEF_MAX_CAPACITY = 10000 @@ -26,7 +28,7 @@ def defaultNumStats(layout): return 9 if "D" in layout else 7 -@t.mark.parametrize( +@pytest.mark.parametrize( "src_args", [ (((2, 11, 26, 32, 1), np.uint8, "NDHWC")), @@ -34,8 +36,10 @@ def defaultNumStats(layout): (((10, 22, 33, 1), np.uint8, "DHWC")), (((14, 23, 34), np.uint8, "DHW")), (((2, 15, 25, 1), np.uint8, "NHWC")), + (((2, 1, 15, 25), np.uint8, "NCHW")), (((3, 17, 24), np.uint8, "NHW")), (((28, 37, 1), np.uint8, "HWC")), + (((1, 28, 37), np.uint8, "CHW")), (((18, 16), np.uint8, "HW")), ], ) @@ -125,7 +129,7 @@ def test_op_label_api(src_args): mask = cvcuda.Tensor(src.shape, np.uint8, src.layout) - t_out, _, _ = cvcuda.label_into( + tmp_out, _, _ = cvcuda.label_into( out, count, stats, @@ -135,10 +139,12 @@ def test_op_label_api(src_args): min_size=min_size, mask=mask, ) - assert t_out is out + assert tmp_out is out - t_out, t_count, t_stats = cvcuda.label_into(out, count, stats, src, connectivity) - assert t_out is out and t_count is count and t_stats is stats + tmp_out, tmp_count, tmp_stats = cvcuda.label_into( + out, count, stats, src, connectivity + ) + assert tmp_out is out and tmp_count is count and tmp_stats is stats assert out.layout == src.layout assert out.shape == src.shape assert out.dtype == DEF_OUT_DTYPE @@ -168,3 +174,71 @@ def test_op_label_api(src_args): assert out.layout == src.layout assert out.shape == src.shape assert out.dtype == np.uint32 + + +def _op(src): + if "D" in str(src.layout): + connectivity = cvcuda.CONNECTIVITY_6_3D + else: + connectivity = cvcuda.CONNECTIVITY_4_2D + return cvcuda.label(src, connectivity) + + +_supported_dtypes = { + cvcuda.Type.U8, + cvcuda.Type.S8, + cvcuda.Type.U16, + cvcuda.Type.S16, + cvcuda.Type.U32, + cvcuda.Type.S32, +} +_supported_layouts_2d = {"HW", "HWC", "CHW", "NHW", "NHWC", "NCHW"} +_supported_layouts_3d = {"DHW", "DHWC", "NDHW", "NDHWC"} +_supported_channels = {1} + + +@pytest.mark.parametrize("dtype", _supported_dtypes) +@pytest.mark.parametrize("layout", _supported_layouts_2d) +def test_op_label_input_2d(dtype, layout): + cv_tools.assert_layouts(_op, layout, dtype=dtype, wrapper="tensor", channels=1) + + +@pytest.mark.parametrize("dtype", _supported_dtypes) +@pytest.mark.parametrize("layout", _supported_layouts_3d) +def test_op_label_input_3d(dtype, layout): + cv_tools.assert_layouts(_op, layout, dtype=dtype, wrapper="tensor", channels=1) + + +@pytest.mark.parametrize("dtype", cv_types.SCALAR_TYPES_SET - _supported_dtypes) +def test_op_label_dtype_negative(dtype): + cv_tools.assert_dtypes( + _op, dtype, wrapper="tensor", channels=1, layout="NHWC", negative=True + ) + + +@pytest.mark.parametrize( + "layout", + cv_types.IMAGE_LAYOUTS - _supported_layouts_2d - _supported_layouts_3d, +) +def test_op_label_layout_negative(layout): + cv_tools.assert_layouts( + _op, layout, dtype=cvcuda.Type.U8, wrapper="tensor", channels=1, negative=True + ) + + +@pytest.mark.parametrize("channels", [2, 3, 4]) +def test_op_label_channels_negative(channels): + cv_tools.assert_layouts( + _op, + "NHWC", + dtype=cvcuda.Type.U8, + wrapper="tensor", + channels=channels, + negative=True, + ) + + +def test_op_label_multichannel_hwc_negative(): + src = cvcuda.Tensor((16, 17, 3), np.uint8, "HWC") + with pytest.raises(RuntimeError): + cvcuda.label(src, cvcuda.CONNECTIVITY_4_2D) diff --git a/tests/cvcuda/python/test_oplaplacian.py b/tests/cvcuda/python/test_oplaplacian.py index f2cff9088..c25f5033f 100644 --- a/tests/cvcuda/python/test_oplaplacian.py +++ b/tests/cvcuda/python/test_oplaplacian.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,14 +15,15 @@ import cvcuda -import pytest as t +import pytest import numpy as np +import cvcuda_tools as cv_tools import cvcuda_util as util RNG = np.random.default_rng(0) -@t.mark.parametrize( +@pytest.mark.parametrize( "input_args, ksize, scale, border", [ ( @@ -80,7 +81,7 @@ def test_op_laplacian(input_args, ksize, scale, border): assert out.dtype == input.dtype -@t.mark.parametrize( +@pytest.mark.parametrize( "num_images, img_format, img_size, max_pixel, max_ksize, max_scale, border", [ ( @@ -139,9 +140,9 @@ def test_op_laplacianvarshape( ) ksize = util.create_tensor( - (num_images, 1), + (num_images,), np.int32, - "NC", + "N", max_random=max_ksize, rng=RNG, transform_dist=util.dist_odd, @@ -179,3 +180,35 @@ def test_op_laplacianvarshape( assert out.capacity == input.capacity assert out.uniqueformat == input.uniqueformat assert out.maxsize == input.maxsize + + +def _laplacian_params(dtype, layout, channels): + return { + "ksize": 3, + "scale": 1.0, + "border": cvcuda.Border.CONSTANT, + } + + +def _laplacian_varshape_params(dtype, layout, channels): + return { + "ksize": util.to_cvcuda_tensor(np.array([1, 1], dtype=np.int32), "N"), + "scale": util.to_cvcuda_tensor(np.array([1.0, 1.0], dtype=np.float32), "N"), + "border": cvcuda.Border.CONSTANT, + } + + +globals().update( + cv_tools.make_op_tests( + name="laplacian", + runner_info=[ + ("tensor", cvcuda.laplacian, _laplacian_params), + ("image_batch", cvcuda.laplacian, _laplacian_varshape_params), + ], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 3), + supported_dtypes={cvcuda.Type.U8, cvcuda.Type.U16, cvcuda.Type.F32}, + supported_layouts={"NHWC", "HWC", "NCHW", "CHW"}, + supported_channels={1, 3, 4}, + exclude_dlc=[(None, "NCHW", 2), (None, "CHW", 2)], + ) +) diff --git a/tests/cvcuda/python/test_opmatch.py b/tests/cvcuda/python/test_opmatch.py index 04c505b23..be3476eab 100644 --- a/tests/cvcuda/python/test_opmatch.py +++ b/tests/cvcuda/python/test_opmatch.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,8 +15,10 @@ import cvcuda -import pytest as t +import pytest import numpy as np +import cvcuda_types as cv_types +import cvcuda_tools as cv_tools import cvcuda_util as util RNG = np.random.default_rng(0) @@ -30,7 +32,7 @@ class ref: dist_dtype = np.float32 def absdiff(a, b): - if type(a) == float: + if isinstance(a, float): return abs(a - b) else: return b - a if a < b else a - b @@ -95,7 +97,7 @@ def sort(matches, num_matches, distances): return sorted(output) -@t.mark.parametrize( +@pytest.mark.parametrize( "set_shape, set_dtype", [ ((1, 11, 1), np.uint8), @@ -183,7 +185,7 @@ def test_op_match_api(set_shape, set_dtype): assert tmp[0] is matches and tmp[1] is None and tmp[2] is None -@t.mark.parametrize( +@pytest.mark.parametrize( "set_shape, set_dtype, cross_check, norm_type", [ ((1, 18, 32), np.uint8, False, cvcuda.Norm.HAMMING), @@ -222,3 +224,26 @@ def test_op_match_content(set_shape, set_dtype, cross_check, norm_type): h_gold_output = ref.sort(h_gold_matches, h_gold_num_matches, h_gold_distances) np.testing.assert_allclose(h_test_output, h_gold_output, rtol=1e-5, atol=1e-5) + + +def _op(src): + # src is a tensor created by cv_tools - extract dtype from it + # Create set1 and set2 with proper NMD layout for match operator + dtype = src.dtype + shape = (2, 16, 8) + set1 = cvcuda.Tensor(shape, dtype, "NMD") + set2 = cvcuda.Tensor(shape, dtype, "NMD") + return cvcuda.match(set1, set2) + + +_supported_dtypes = {cvcuda.Type.U8, cvcuda.Type.U32, cvcuda.Type.F32} + + +@pytest.mark.parametrize("dtype", _supported_dtypes) +def test_op_match_dtype_input(dtype): + cv_tools.assert_dtypes(_op, dtype, wrapper="tensor", negative=False) + + +@pytest.mark.parametrize("dtype", cv_types.SCALAR_TYPES_SET - _supported_dtypes) +def test_op_match_dtype_negative(dtype): + cv_tools.assert_dtypes(_op, dtype, wrapper="tensor", negative=True) diff --git a/tests/cvcuda/python/test_opmedianblur.py b/tests/cvcuda/python/test_opmedianblur.py index 9828a6a85..5ae7754c3 100644 --- a/tests/cvcuda/python/test_opmedianblur.py +++ b/tests/cvcuda/python/test_opmedianblur.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,14 +15,16 @@ import cvcuda -import pytest as t +import pytest import numpy as np +import cvcuda_tools as cv_tools import cvcuda_util as util +import cupy RNG = np.random.default_rng(0) -@t.mark.parametrize( +@pytest.mark.parametrize( "input_args, ksize", [ ( @@ -64,7 +66,7 @@ def test_op_median_blur(input_args, ksize): assert out.dtype == input.dtype -@t.mark.parametrize( +@pytest.mark.parametrize( "nimages, format, max_size, max_pixel, max_ksize", [ ( @@ -118,3 +120,29 @@ def test_op_median_blurvarshape(nimages, format, max_size, max_pixel, max_ksize) assert out.capacity == input.capacity assert out.uniqueformat == input.uniqueformat assert out.maxsize == input.maxsize + + +def _medianblur_params(dtype, layout, channels): + return {"ksize": [3, 3]} + + +def _medianblur_varshape_params(dtype, layout, channels): + # Batch dimension must be 2 to match image_batch tests + # ksize values need to be odd and > 0, so we initialize with actual values + ksize_data = cupy.asarray(np.array([[3, 3], [3, 3]], dtype=np.int32)) + return {"ksize": cvcuda.as_tensor(ksize_data, "NC")} + + +globals().update( + cv_tools.make_op_tests( + name="medianblur", + runner_info=[ + ("tensor", cvcuda.median_blur, _medianblur_params), + ("image_batch", cvcuda.median_blur, _medianblur_varshape_params), + ], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 3), + supported_dtypes={cvcuda.Type.U8, cvcuda.Type.U16, cvcuda.Type.F32}, + supported_layouts={"NHWC", "HWC", "NCHW", "CHW"}, + supported_channels={1, 3, 4}, + ) +) diff --git a/tests/cvcuda/python/test_opminarearect.py b/tests/cvcuda/python/test_opminarearect.py index 76e6cdab9..a23271ad3 100644 --- a/tests/cvcuda/python/test_opminarearect.py +++ b/tests/cvcuda/python/test_opminarearect.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,14 +13,75 @@ # See the License for the specific language governing permissions and # limitations under the License. -import torch +import numpy as np import cvcuda -import pytest as t -from torch.nn.utils.rnn import pad_sequence +import pytest +import cvcuda_tools as cv_tools +import cupy -@t.mark.parametrize( +def pad_sequence(sequences, batch_first=False, padding_value=0.0, padding_side="right"): + """ + Pad a list of variable length arrays with padding_value. + + This is a numpy equivalent of PyTorch's nn.utils.rnn.pad_sequence. + + Parameters: + ----------- + sequences : list of array-like + List of variable length sequences. + batch_first : bool, optional + If True, output will be B x T x * format, T x B x * otherwise. + padding_value : float, optional + Value for padded elements. Default: 0.0. + padding_side : str, optional + The side to pad sequences on ('right' or 'left'). Default: 'right'. + + Returns: + -------- + numpy.ndarray + Padded array of shape T x B x * if batch_first is False, + B x T x * otherwise, where B is batch size and T is the length + of the longest sequence. + """ + sequences = [np.asarray(seq) for seq in sequences] + + max_len = max(len(seq) for seq in sequences) + + batch_size = len(sequences) + + trailing_dims = sequences[0].shape[1:] if sequences[0].ndim > 1 else () + + dtype = sequences[0].dtype + + if batch_first: + out_shape = (batch_size, max_len) + trailing_dims + else: + out_shape = (max_len, batch_size) + trailing_dims + + out = np.full(out_shape, padding_value, dtype=dtype) + + for i, seq in enumerate(sequences): + length = len(seq) + if batch_first: + if padding_side == "right": + out[i, :length] = seq + else: # left + out[i, -length:] = seq + else: + if padding_side == "right": + out[:length, i] = seq + else: # left + out[-length:, i] = seq + + return out + + +RNG = np.random.default_rng(0) + + +@pytest.mark.parametrize( "contourData, numPointsInContour, openCvRes", [ ( @@ -148,38 +209,35 @@ def test_op_minarearect(contourData, numPointsInContour, openCvRes): batchSize = len(contourData) - numPointsInContour_torch = ( - torch.Tensor(numPointsInContour).type(torch.int32).unsqueeze(0) - ) - src_torch = ( - pad_sequence([torch.Tensor(t) for t in contourData], batch_first=True) - .type(torch.int16) - .reshape(batchSize, -1, 2) - ) - gold_torch = torch.Tensor(openCvRes).type(torch.float32) + numPointsInContour_np = np.asarray(numPointsInContour, dtype=np.int32)[None, :] + src_np = pad_sequence( + [np.asarray(t, dtype=np.int16) for t in contourData], batch_first=True + ).reshape(batchSize, -1, 2) + gold_np = np.asarray(openCvRes, dtype=np.float32) - src_cvcuda = cvcuda.as_tensor(src_torch.contiguous().cuda(), "NWC") - pointNumInContour_cvcuda = cvcuda.as_tensor( - numPointsInContour_torch.contiguous().cuda(), "NW" - ) - gold_cvcuda = cvcuda.as_tensor(gold_torch.contiguous().cuda(), "NW") + src_dev = cupy.asarray(src_np) + pointNumInContour_dev = cupy.asarray(numPointsInContour_np) + gold_dev = cupy.asarray(gold_np) + + src_cvcuda = cvcuda.as_tensor(src_dev, "NWC") + pointNumInContour_cvcuda = cvcuda.as_tensor(pointNumInContour_dev, "NW") + gold_cvcuda = cvcuda.as_tensor(gold_dev, "NW") result_cvcuda = cvcuda.minarearect( - src_cvcuda, pointNumInContour_cvcuda, src_torch.shape[0] + src_cvcuda, pointNumInContour_cvcuda, src_np.shape[0] ) assert result_cvcuda.layout == gold_cvcuda.layout assert result_cvcuda.shape == gold_cvcuda.shape assert result_cvcuda.dtype == gold_cvcuda.dtype - result_torch, _ = torch.sort( - torch.as_tensor(result_cvcuda.cuda()).reshape(batchSize, -1, 2), dim=1 - ) - gold_torch, _ = torch.sort(gold_torch.reshape(batchSize, -1, 2), dim=1) - assert (gold_torch.cuda() - result_torch.cuda() < 5.0).all() + result_host = cupy.asarray(result_cvcuda.cuda()).get().reshape(batchSize, -1, 2) + result_sorted = np.sort(result_host, axis=1) + gold_sorted = np.sort(gold_np.reshape(batchSize, -1, 2), axis=1) + assert np.all(np.abs(gold_sorted - result_sorted) < 5.0) stream = cvcuda.Stream() out = cvcuda.Tensor(gold_cvcuda.shape, gold_cvcuda.dtype, gold_cvcuda.layout) tmp = cvcuda.minarearect_into( - out, src_cvcuda, pointNumInContour_cvcuda, src_torch.shape[0] + out, src_cvcuda, pointNumInContour_cvcuda, src_np.shape[0] ) assert tmp is out @@ -187,7 +245,7 @@ def test_op_minarearect(contourData, numPointsInContour, openCvRes): out = cvcuda.minarearect( src=src_cvcuda, numPointsInContour=pointNumInContour_cvcuda, - totalContours=src_torch.shape[0], + totalContours=src_np.shape[0], stream=stream, ) assert out.layout == gold_cvcuda.layout @@ -198,7 +256,30 @@ def test_op_minarearect(contourData, numPointsInContour, openCvRes): src=src_cvcuda, dst=out, numPointsInContour=pointNumInContour_cvcuda, - totalContours=src_torch.shape[0], + totalContours=src_np.shape[0], stream=stream, ) assert tmp is out + + +def _minarearect_params(dtype, layout, channels): + num_contours = 1 + points_per_contour = 5 + num_points = np.array([[points_per_contour] * num_contours], dtype=np.int32) + num_points_tensor = cvcuda.as_tensor(cupy.asarray(num_points), "NW") + return { + "numPointsInContour": num_points_tensor, + "totalContours": num_contours, + } + + +globals().update( + cv_tools.make_op_tests( + name="minarearect", + runner_info=[("tensor", cvcuda.minarearect, _minarearect_params)], + keystone_dlc=(cvcuda.Type.U16, "NWC", 2), + supported_dtypes={cvcuda.Type.U16, cvcuda.Type.S16, cvcuda.Type.S32}, + supported_layouts={"NWC"}, + supported_channels={2}, + ) +) diff --git a/tests/cvcuda/python/test_opminmaxloc.py b/tests/cvcuda/python/test_opminmaxloc.py index 43aeb8bff..fb64173bf 100644 --- a/tests/cvcuda/python/test_opminmaxloc.py +++ b/tests/cvcuda/python/test_opminmaxloc.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,9 +15,11 @@ import cvcuda -import pytest as t +import pytest import numpy as np + import cvcuda_util as util +import cvcuda_tools as cv_tools RNG = np.random.default_rng(0) @@ -35,14 +37,14 @@ def gold_num_dtype(): return cvcuda.Type.S32 -@t.mark.parametrize( +@pytest.mark.parametrize( "operator", [ cvcuda.min_loc_into, cvcuda.max_loc_into, ], ) -@t.mark.parametrize( +@pytest.mark.parametrize( "val_args,loc_args,num_args", [ ( @@ -69,7 +71,7 @@ def test_opminmaxloc_output_api(operator, val_args, loc_args, num_args): assert rets[2] is t_min -@t.mark.parametrize( +@pytest.mark.parametrize( "src_args", [ ((2, 16, 23, 1), np.uint8, "NHWC"), @@ -188,7 +190,7 @@ def test_opminmaxloc_tensor_api(src_args): assert ret is out -@t.mark.parametrize( +@pytest.mark.parametrize( "num_images, img_format, max_size", [ (1, cvcuda.Format.U8, (73, 98)), @@ -293,7 +295,7 @@ def test_opminmaxloc_varshape_api(num_images, img_format, max_size): assert ret is out -@t.mark.parametrize("input_type", ["tensor", "image_batch"]) +@pytest.mark.parametrize("input_type", ["tensor", "image_batch"]) def test_opminmaxloc_content(input_type): # Test with fixed number of images and lists of minimum and maximum locations, # the lists must be in ascending order in x dimension for comparisons @@ -356,3 +358,31 @@ def test_opminmaxloc_content(input_type): np.testing.assert_array_equal(a_test_max_val, np.full([n_img, 1], 255)) np.testing.assert_array_equal(a_test_max_loc, a_gold_max_loc) np.testing.assert_array_equal(a_test_num_max, np.full([n_img, 1], len(l_max_loc))) + + +def _minmaxloc_params(dtype, layout, channels): + return {"max_locations": 10} + + +globals().update( + cv_tools.make_op_tests( + name="minmaxloc", + runner_info=[ + ("tensor", cvcuda.min_max_loc, _minmaxloc_params), + ("image_batch", cvcuda.min_max_loc, _minmaxloc_params), + ], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 1), + supported_dtypes={ + cvcuda.Type.U8, + cvcuda.Type.U16, + cvcuda.Type.U32, + cvcuda.Type.S8, + cvcuda.Type.S16, + cvcuda.Type.S32, + cvcuda.Type.F32, + cvcuda.Type.F64, + }, + supported_layouts={"NHWC", "HW", "NHW", "HWC", "CHW", "NCHW"}, + supported_channels={1}, + ) +) diff --git a/tests/cvcuda/python/test_opmorphology.py b/tests/cvcuda/python/test_opmorphology.py index 8265c3913..f02039f50 100644 --- a/tests/cvcuda/python/test_opmorphology.py +++ b/tests/cvcuda/python/test_opmorphology.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,17 +13,19 @@ # See the License for the specific language governing permissions and # limitations under the License. -import torch import cvcuda -import pytest as t +import pytest import numpy as np + import cvcuda_util as util +import cvcuda_tools as cv_tools +import cupy RNG = np.random.default_rng(0) -@t.mark.parametrize( +@pytest.mark.parametrize( "input_args, morphologyType, maskSize, anchor, iteration, border ", [ ( @@ -159,7 +161,7 @@ def test_op_morphology(input_args, morphologyType, maskSize, anchor, iteration, assert out.dtype == input.dtype -@t.mark.parametrize( +@pytest.mark.parametrize( "num_images, img_format, img_size, max_pixel, \ morphologyType, max_mask, max_anchor, iteration, border ", [ @@ -375,12 +377,16 @@ def test_op_morphology_input_output(): number = 3 # Create a tensor filled with zeros - source = torch.zeros((number, height, width, 1), dtype=torch.uint8).cuda() + source_host = np.zeros((number, height, width, 1), dtype=np.uint8) # Set the middle pixel of each image to 1 - source[:, 1, 1, :] = 1 + source_host[:, 1, 1, :] = 1 + + # Now copy to device + source = cupy.asarray(source_host) + + image = source.copy() - image = source.clone() # Copy source tensor, so we can compare later image = cvcuda.as_tensor(image, "NHWC") workspace = cvcuda.Tensor(image.shape, image.dtype, image.layout) @@ -395,17 +401,52 @@ def test_op_morphology_input_output(): image, cvcuda.MorphologyType.CLOSE, [4, 4], [-1, -1], workspace=workspace ) - outDilate = torch.as_tensor(outDilate.cuda(), device=torch.device("cuda", 0)) - outErode = torch.as_tensor(outErode.cuda(), device=torch.device("cuda", 0)) - outOpen = torch.as_tensor(outOpen.cuda(), device=torch.device("cuda", 0)) - outClose = torch.as_tensor(outClose.cuda(), device=torch.device("cuda", 0)) + outDilate = cupy.asarray(outDilate.cuda()) + outErode = cupy.asarray(outErode.cuda()) + outOpen = cupy.asarray(outOpen.cuda()) + outClose = cupy.asarray(outClose.cuda()) + + expectedDilate = cupy.asarray(np.ones((number, height, width, 1), dtype=np.uint8)) + expectedErode = cupy.asarray(np.zeros((number, height, width, 1), dtype=np.uint8)) + expectedOpen = cupy.asarray(np.zeros((number, height, width, 1), dtype=np.uint8)) + expectedClose = cupy.asarray(np.ones((number, height, width, 1), dtype=np.uint8)) + + assert np.all(outDilate.get() == expectedDilate.get()) + assert np.all(outErode.get() == expectedErode.get()) + assert np.all(outOpen.get() == expectedOpen.get()) + assert np.all(outClose.get() == expectedClose.get()) + + +def _morphology_params(dtype, layout, channels): + return { + "morphologyType": cvcuda.MorphologyType.ERODE, + "maskSize": [-1, -1], + "anchor": [-1, -1], + } - expectedDilate = torch.ones((number, height, width, 1), dtype=torch.uint8).cuda() - expectedErode = torch.zeros((number, height, width, 1), dtype=torch.uint8).cuda() - expectedOpen = torch.zeros((number, height, width, 1), dtype=torch.uint8).cuda() - expectedClose = torch.ones((number, height, width, 1), dtype=torch.uint8).cuda() - assert torch.all(outDilate.eq(expectedDilate)) - assert torch.all(outErode.eq(expectedErode)) - assert torch.all(outOpen.eq(expectedOpen)) - assert torch.all(outClose.eq(expectedClose)) +def _morphology_varshape_params(dtype, layout, channels): + return { + "morphologyType": cvcuda.MorphologyType.ERODE, + "masks": util.to_cvcuda_tensor( + np.array([[3, 3], [3, 3]], dtype=np.int32), "NC" + ), + "anchors": util.to_cvcuda_tensor( + np.array([[-1, -1], [-1, -1]], dtype=np.int32), "NC" + ), + } + + +globals().update( + cv_tools.make_op_tests( + name="morphology", + runner_info=[ + ("tensor", cvcuda.morphology, _morphology_params), + ("image_batch", cvcuda.morphology, _morphology_varshape_params), + ], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 3), + supported_dtypes={cvcuda.Type.U8, cvcuda.Type.U16, cvcuda.Type.F32}, + supported_layouts={"NHWC", "HWC", "NCHW", "CHW"}, + supported_channels={1, 3, 4}, + ) +) diff --git a/tests/cvcuda/python/test_opnms.py b/tests/cvcuda/python/test_opnms.py index affb7f9e1..1646657f6 100644 --- a/tests/cvcuda/python/test_opnms.py +++ b/tests/cvcuda/python/test_opnms.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,17 +13,19 @@ # See the License for the specific language governing permissions and # limitations under the License. -import torch import cvcuda import cvcuda_util as util -import pytest as t +import pytest import numpy as np +import cvcuda_types as cv_types +import cvcuda_tools as cv_tools +import cupy RNG = np.random.default_rng(0) -@t.mark.parametrize( +@pytest.mark.parametrize( "in_shape,in_dtype,in_layout,sc_shape,sc_dtype,sc_layout", [ ((2, 10, 4), cvcuda.Type.S16, "NWC", (2, 10, 1), cvcuda.Type.F32, "NWC"), @@ -101,7 +103,7 @@ def gold_nms(in_bboxes, in_scores, score_threshold, iou_threshold): return out_bboxes -@t.mark.parametrize("num_samples,num_bboxes", [(1, 10), (3, 33), (7, 88)]) +@pytest.mark.parametrize("num_samples,num_bboxes", [(1, 10), (3, 33), (7, 88)]) def test_op_nms_content(num_samples, num_bboxes): a_src = RNG.integers( (0, 0, 10, 10), @@ -119,7 +121,60 @@ def test_op_nms_content(num_samples, num_bboxes): t_dst = cvcuda.nms(t_src, t_scores, score_threshold, iou_threshold) - a_dst_test = torch.as_tensor(t_dst.cuda()).cpu().numpy() + a_dst_test = cupy.asarray(t_dst.cuda()).get() a_dst_gold = gold_nms(a_src, a_scores, score_threshold, iou_threshold) np.testing.assert_array_equal(a_dst_test, a_dst_gold) + + +def _op(src: cvcuda.Tensor) -> cvcuda.Tensor: + if src.layout == "NWC": + sc_shape = (src.shape[0], src.shape[1], 1) + else: + sc_shape = (src.shape[0], src.shape[1], 1) + t_sc = cvcuda.Tensor(sc_shape, cvcuda.Type.F32, "NWC") + return cvcuda.nms(src, t_sc) + + +_num_samples = 2 +_num_bboxes = 10 + +_supported_input_configs = [ + (cvcuda.Type.S16, "NWC", 4), # S16 with 4 channels via NWC layout + (cvcuda.Type._4S16, "NWC", 1), # _4S16 packed with NWC layout (channel dim = 1) + (cvcuda.Type._4S16, "NW", 1), # _4S16 packed with NW layout +] + + +@pytest.mark.parametrize("dtype,layout,channels", _supported_input_configs) +def test_op_nms_input(dtype, layout, channels): + cv_tools.assert_layouts( + _op, layout, dtype=dtype, wrapper="tensor", channels=channels + ) + + +@pytest.mark.parametrize("dtype", cv_types.SCALAR_TYPES_SET - {cvcuda.Type.S16}) +def test_op_nms_dtype_negative(dtype): + cv_tools.assert_layouts( + _op, "NWC", dtype=dtype, wrapper="tensor", channels=4, negative=True + ) + + +@pytest.mark.parametrize("dtype", cv_types.SCALAR_TYPES_SET - {cvcuda.Type.F32}) +def test_op_nms_scores_dtype_negative(dtype): + t_in = cvcuda.Tensor((_num_samples, _num_bboxes, 4), cvcuda.Type.S16, "NWC") + t_sc = cvcuda.Tensor((_num_samples, _num_bboxes, 1), dtype, "NWC") + with pytest.raises(RuntimeError): + cvcuda.nms(t_in, t_sc) + + +@pytest.mark.parametrize("channels", {1, 2, 3, 5}) +def test_op_nms_channels_negative(channels): + cv_tools.assert_layouts( + _op, + "NWC", + dtype=cvcuda.Type.S16, + wrapper="tensor", + channels=channels, + negative=True, + ) diff --git a/tests/cvcuda/python/test_opnormalize.py b/tests/cvcuda/python/test_opnormalize.py index 7ffd66b58..bb6c6c2b3 100644 --- a/tests/cvcuda/python/test_opnormalize.py +++ b/tests/cvcuda/python/test_opnormalize.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,14 +15,16 @@ import cvcuda -import pytest as t +import pytest import numpy as np + import cvcuda_util as util +import cvcuda_tools as cv_tools RNG = np.random.default_rng(0) -@t.mark.parametrize( +@pytest.mark.parametrize( "input_args,base_args,scale_args,globalscale,globalshift,epsilon,flags", [ ( @@ -113,7 +115,7 @@ def test_op_normalize( assert out.dtype == input.dtype -@t.mark.parametrize( +@pytest.mark.parametrize( "nimages,format,max_size,max_pixel,base_args,scale_args,globalscale,globalshift,epsilon,flags", [ ( @@ -207,3 +209,255 @@ def test_op_rotatevarshape( assert out.capacity == input.capacity assert out.uniqueformat == input.uniqueformat assert out.maxsize == input.maxsize + + +def _get_base_and_std(layout, channels): + if layout == "NHWC": + shape = (1, 1, 1, channels) + elif layout == "HWC": + shape = (1, 1, channels) + elif layout == "NCHW": + shape = (1, channels, 1, 1) + elif layout == "CHW": + shape = (channels, 1, 1) + else: + shape = (*[1] * (len(layout) - 1), channels) + base = cvcuda.Tensor(shape, cvcuda.Type.F32, layout) + std = cvcuda.Tensor(shape, cvcuda.Type.F32, layout) + return base, std + + +def _normalize_params(dtype, layout, channels): + base, std = _get_base_and_std(layout, channels) + return { + "base": base, + "scale": std, + } + + +def _normalize_list_params(dtype, layout, channels): + # list[float] (tensor-free / by-value) base & scale: one value per channel, so the + # by-value overload is exercised by the same support matrix as the tensor path. + # dtype and layout are unused -- the values are plain floats regardless of input. + base = [0.40 + 0.05 * i for i in range(channels)] + scale = [0.20 + 0.03 * i for i in range(channels)] + return {"base": base, "scale": scale} + + +def _normalize_varshape_params(dtype, layout, channels): + """Build base/scale tensors for image_batch normalize tests. + + layout is unused: image_batch wrappers operate on image formats, not tensor + layouts, so base/scale are always created as NHWC-shaped (batch,1,1,C) tensors. + """ + base_data = np.zeros((2, 1, 1, channels), dtype=np.float32) + scale_data = np.ones((2, 1, 1, channels), dtype=np.float32) + return { + "base": util.to_cvcuda_tensor(base_data, "NHWC"), + "scale": util.to_cvcuda_tensor(scale_data, "NHWC"), + } + + +def test_op_normalize_varshape_planar(): + nimages = 2 + img_format = cvcuda.Format.RGBA8p + base = util.to_cvcuda_tensor(np.zeros((1, 4, 1, 1), dtype=np.float32), "NCHW") + scale = util.to_cvcuda_tensor(np.ones((1, 4, 1, 1), dtype=np.float32), "NCHW") + + input = cvcuda.ImageBatchVarShape(nimages) + for i in range(nimages): + input.pushback(cvcuda.Image((16 + i * 3, 23 + i * 2), img_format)) + + out = cvcuda.normalize(input, base, scale) + assert len(out) == len(input) + assert out.capacity == input.capacity + assert out.uniqueformat == input.uniqueformat + assert out.maxsize == input.maxsize + + out = util.clone_image_batch(input) + tmp = cvcuda.normalize_into(out, input, base, scale) + assert tmp is out + assert len(out) == len(input) + assert out.capacity == input.capacity + assert out.uniqueformat == input.uniqueformat + assert out.maxsize == input.maxsize + + +def _scalar_input(dtype, layout, channels): + dims = {"N": 2, "H": 9, "W": 13, "C": channels} + shape = tuple(dims[t] for t in layout) + if np.issubdtype(np.dtype(dtype), np.floating): + data = RNG.random(shape).astype(dtype) + else: + info = np.iinfo(dtype) + data = RNG.integers(info.min, info.max, size=shape, endpoint=True).astype(dtype) + return util.to_cvcuda_tensor(data, layout) + + +def _scalar_values(channels, mode): + n = 1 if mode == "scalar" else channels + base = [0.40 + 0.05 * i for i in range(n)] + scale = [0.20 + 0.03 * i for i in range(n)] + return base, scale + + +def _scalar_param_tensor(vals, layout): + n = len(vals) + # Parameter tensor mirrors the input layout with the channel axis = n (all others 1), so the + # tensor-path oracle broadcasts base/scale per channel exactly like the by-value path. + shapes = { + "NHWC": (1, 1, 1, n), + "HWC": (1, 1, n), + "NCHW": (1, n, 1, 1), + "CHW": (n, 1, 1), + } + return util.to_cvcuda_tensor( + np.array(vals, np.float32).reshape(shapes[layout]), layout + ) + + +_STDDEV = cvcuda.NormalizeFlags.SCALE_IS_STDDEV +_DEFAULT_GLOBALS = (1.0, 0.0, 0.0) +_NONDEFAULT_GLOBALS = (2.0, 5.0, 1e-4) + + +@pytest.mark.parametrize( + "dtype,channels,layout,flags,mode,global_profile", + [ + (np.uint8, 1, "NHWC", None, "scalar", _DEFAULT_GLOBALS), + (np.int8, 3, "HWC", _STDDEV, "per_channel", _NONDEFAULT_GLOBALS), + (np.uint16, 4, "NCHW", None, "per_channel", _DEFAULT_GLOBALS), + (np.int16, 3, "CHW", _STDDEV, "per_channel", _NONDEFAULT_GLOBALS), + (np.int32, 1, "NCHW", None, "scalar", _NONDEFAULT_GLOBALS), + (np.float32, 4, "NHWC", _STDDEV, "scalar", _DEFAULT_GLOBALS), + ], + ids=["u8-nhwc", "s8-hwc", "u16-nchw", "s16-chw", "s32-nchw", "f32-nhwc"], +) +def test_op_normalize_list_matches_tensor( + dtype, channels, layout, flags, mode, global_profile +): + globalscale, globalshift, epsilon = global_profile + inp = _scalar_input(dtype, layout, channels) + base_vals, scale_vals = _scalar_values(channels, mode) + base_t = _scalar_param_tensor(base_vals, layout) + scale_t = _scalar_param_tensor(scale_vals, layout) + + kw = dict( + flags=flags, globalscale=globalscale, globalshift=globalshift, epsilon=epsilon + ) + + # Allocating overload: list result must equal tensor result byte-for-byte. + out_ref = cvcuda.normalize(inp, base_t, scale_t, **kw) + out_lst = cvcuda.normalize(inp, base_vals, scale_vals, **kw) + assert out_lst.layout == inp.layout + assert out_lst.shape == inp.shape + assert out_lst.dtype == inp.dtype + np.testing.assert_array_equal( + util.to_cpu_numpy_buffer(out_ref.cuda()), + util.to_cpu_numpy_buffer(out_lst.cuda()), + ) + + # _into overload: same equivalence. + dst_ref = cvcuda.Tensor(inp.shape, inp.dtype, inp.layout) + dst_lst = cvcuda.Tensor(inp.shape, inp.dtype, inp.layout) + cvcuda.normalize_into(dst_ref, inp, base_t, scale_t, **kw) + ret = cvcuda.normalize_into(dst_lst, inp, base_vals, scale_vals, **kw) + assert ret is dst_lst + np.testing.assert_array_equal( + util.to_cpu_numpy_buffer(dst_ref.cuda()), + util.to_cpu_numpy_buffer(dst_lst.cuda()), + ) + + +def test_op_normalize_list_accepts_tuple(): + """A tuple is accepted just like a list (both convert to std::vector).""" + inp = _scalar_input(np.float32, "HWC", 3) + out_list = cvcuda.normalize(inp, [0.4, 0.45, 0.5], [0.2, 0.23, 0.26]) + out_tuple = cvcuda.normalize(inp, (0.4, 0.45, 0.5), (0.2, 0.23, 0.26)) + np.testing.assert_array_equal( + util.to_cpu_numpy_buffer(out_list.cuda()), + util.to_cpu_numpy_buffer(out_tuple.cuda()), + ) + + +@pytest.mark.parametrize( + "bad_base,expected_exception", + [ + ([], ValueError), + ([0.5, 0.5], RuntimeError), + ([0.1, 0.2, 0.3, 0.4, 0.5], ValueError), + ], + ids=["empty", "channel-mismatch", "too-many"], +) +def test_op_normalize_list_wrong_length_raises(bad_base, expected_exception): + """The binding rejects invalid vector lengths; the operator rejects a valid + vector length that is neither scalar nor equal to the input channel count.""" + inp = _scalar_input(np.float32, "NHWC", 3) + with pytest.raises(expected_exception): + cvcuda.normalize(inp, bad_base, [0.2] * max(len(bad_base), 1)) + + +globals().update( + cv_tools.make_op_tests( + name="normalize", + runner_info=[ + ("tensor", cvcuda.normalize, _normalize_params), + ], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 3), + supported_dtypes={ + cvcuda.Type.U8, + cvcuda.Type.S8, + cvcuda.Type.U16, + cvcuda.Type.S16, + cvcuda.Type.S32, + cvcuda.Type.F32, + }, + supported_layouts={"NHWC", "HWC", "NCHW", "CHW"}, + supported_channels={1, 3, 4}, + ) +) + + +globals().update( + cv_tools.make_op_tests( + name="normalize_varshape", + runner_info=[ + ("image_batch", cvcuda.normalize, _normalize_varshape_params), + ], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 3), + supported_dtypes={ + cvcuda.Type.U8, + cvcuda.Type.F32, + }, + supported_layouts={"NHWC"}, + supported_channels={1, 3, 4}, + ) +) + + +# Tensor-free (list[float] / by-value) base & scale run through the same auto +# input-validation matrix as the tensor path. It supports interleaved and planar +# layouts and channels {1, 3, 4}, matching the tensor overload's support set. +globals().update( + cv_tools.make_op_tests( + name="normalize_list", + runner_info=[ + ("tensor", cvcuda.normalize, _normalize_list_params), + ], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 3), + supported_dtypes={ + cvcuda.Type.U8, + cvcuda.Type.S8, + cvcuda.Type.U16, + cvcuda.Type.S16, + cvcuda.Type.S32, + cvcuda.Type.F32, + }, + supported_layouts={"NHWC", "HWC", "NCHW", "CHW"}, + supported_channels={1, 3, 4}, + # The by-value overload rejects unsupported channel counts at the operator + # (RuntimeError); the binding additionally rejects list lengths > 4 (ValueError), + # which the negative-channels test hits for the 5- and 6-channel cases. + negative_exceptions=[RuntimeError, ValueError], + ) +) diff --git a/tests/cvcuda/python/test_oposd.py b/tests/cvcuda/python/test_oposd.py index ac87bd861..a5146a681 100644 --- a/tests/cvcuda/python/test_oposd.py +++ b/tests/cvcuda/python/test_oposd.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,11 +15,12 @@ import cvcuda -import pytest as t +import pytest import numpy as np +import cvcuda_tools as cv_tools -@t.mark.parametrize( +@pytest.mark.parametrize( "inputp, elements", [ ( @@ -159,3 +160,29 @@ def test_op_osd(inputp, elements): assert out.layout == input.layout assert out.shape == input.shape assert out.dtype == input.dtype + + +def _osd_params(dtype, layout, channels): + elements_list = [ + [ + cvcuda.BndBoxI( + box=(5, 5, 3, 3), + thickness=1, + borderColor=(255, 255, 0), + fillColor=(0, 128, 255, 128), + ) + ] + ] + return {"elements": cvcuda.Elements(elements=elements_list)} + + +globals().update( + cv_tools.make_op_tests( + name="osd", + runner_info=[("tensor", cvcuda.osd, _osd_params)], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 3), + supported_dtypes={cvcuda.Type.U8}, + supported_layouts={"NHWC", "HWC", "NCHW", "CHW"}, + supported_channels={3, 4}, + ) +) diff --git a/tests/cvcuda/python/test_oppadandstack.py b/tests/cvcuda/python/test_oppadandstack.py index aa7fec3ff..aa19d491a 100644 --- a/tests/cvcuda/python/test_oppadandstack.py +++ b/tests/cvcuda/python/test_oppadandstack.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,12 +14,13 @@ # limitations under the License. import cvcuda +import cvcuda_tools as cv_tools -import pytest as t +import pytest import numpy as np -@t.mark.parametrize( +@pytest.mark.parametrize( "format,num_images,min_size,max_size,border,bvalue,out_shape,out_layout,out_dtype", [ ( @@ -33,6 +34,17 @@ "NHWC", np.uint8, ), + ( + cvcuda.Format.RGBA8p, + 1, + (10, 5), + (10, 5), + cvcuda.Border.REPLICATE, + 0, + (1, 4, 5, 10), + "NCHW", + np.uint8, + ), ], ) def test_op_padandstack( @@ -92,3 +104,44 @@ def test_op_padandstack( stream=stream, ) assert tmp is out + + +def _padandstack(input_batch): + num_images = len(input_batch) + left = cvcuda.Tensor((1, 1, num_images, 1), np.int32, "NHWC") + top = cvcuda.Tensor((1, 1, num_images, 1), np.int32, "NHWC") + return cvcuda.padandstack(input_batch, top, left) + + +globals().update( + cv_tools.make_op_tests( + name="padandstack", + runner_info=[("image_batch", _padandstack, None)], + supported_formats={ + cvcuda.Format.U8, + cvcuda.Format.Y8, + cvcuda.Format.RGB8, + cvcuda.Format.BGR8, + cvcuda.Format.RGB8p, + cvcuda.Format.BGR8p, + cvcuda.Format.HSV8, + cvcuda.Format.RGBA8, + cvcuda.Format.BGRA8, + cvcuda.Format.RGBA8p, + cvcuda.Format.BGRA8p, + cvcuda.Format.U16, + cvcuda.Format.Y16, + cvcuda.Format.S16, + cvcuda.Format.S32, + cvcuda.Format.F32, + cvcuda.Format.RGBf32, + cvcuda.Format.BGRf32, + cvcuda.Format.RGBf32p, + cvcuda.Format.BGRf32p, + cvcuda.Format.RGBAf32, + cvcuda.Format.BGRAf32, + cvcuda.Format.RGBAf32p, + cvcuda.Format.BGRAf32p, + }, + ) +) diff --git a/tests/cvcuda/python/test_oppillowresize.py b/tests/cvcuda/python/test_oppillowresize.py index 9a89b1acf..c58b143e7 100644 --- a/tests/cvcuda/python/test_oppillowresize.py +++ b/tests/cvcuda/python/test_oppillowresize.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,18 +13,22 @@ # See the License for the specific language governing permissions and # limitations under the License. -import torch import cvcuda -import pytest as t +import pytest import numpy as np import cvcuda_util as util +import cvcuda_types as cv_types +import cvcuda_tools as cv_tools import threading + +import cupy + RNG = np.random.default_rng(0) -@t.mark.parametrize( +@pytest.mark.parametrize( "input_args,out_shape,interp,fmt", [ ( @@ -87,6 +91,31 @@ cvcuda.Interp.BOX, cvcuda.Format.RGBf32, ), + # Planar (NCHW / CHW) layouts: output keeps the input layout. + ( + ((5, 3, 16, 23), np.uint8, "NCHW"), + (5, 3, 132, 15), + cvcuda.Interp.LINEAR, + cvcuda.Format.RGB8, + ), + ( + ((5, 3, 55, 55), np.uint8, "NCHW"), + (5, 3, 31, 31), + cvcuda.Interp.CUBIC, + cvcuda.Format.RGB8, + ), + ( + ((3, 31, 31), np.float32, "CHW"), + (3, 55, 55), + cvcuda.Interp.LANCZOS, + cvcuda.Format.RGBf32, + ), + ( + ((4, 40, 30), np.uint8, "CHW"), + (4, 20, 15), + cvcuda.Interp.HAMMING, + cvcuda.Format.RGBA8, + ), ], ) def test_op_pillowresize(input_args, out_shape, interp, fmt): @@ -118,7 +147,7 @@ def test_op_pillowresize(input_args, out_shape, interp, fmt): assert out.dtype == input.dtype -@t.mark.parametrize( +@pytest.mark.parametrize( "nimages, format, max_size, max_pixel, interp", [ ( @@ -224,11 +253,14 @@ def test_op_pillowresize_gpuload(): src = cvcuda.Tensor(src_shape, dtype, layout) dst = cvcuda.Tensor(dst_shape, dtype, layout) - torch0 = torch.zeros(src_shape, dtype=torch.int32, device="cuda") - torch1 = torch.zeros(src_shape, dtype=torch.int32, device="cuda") + cuda0 = cupy.asarray(np.zeros(src_shape, dtype=np.int32)) + cuda1 = cupy.asarray(np.zeros(src_shape, dtype=np.int32)) thread = threading.Thread( - target=lambda: (torch.abs(torch0, out=torch1), torch.square(torch1, out=torch0)) + target=lambda: ( + np.abs(cuda0.get(), out=cuda1.get()), + np.square(cuda1.get(), out=cuda0.get()), + ) ) thread.start() @@ -239,8 +271,8 @@ def test_op_pillowresize_gpuload(): assert tmp.dtype == dtype thread.join() - assert torch0.shape == src_shape - assert torch1.shape == src_shape + assert cuda0.shape == src_shape + assert cuda1.shape == src_shape def test_op_pillowresize_user_stream_with_tensor(): @@ -257,7 +289,7 @@ def test_op_pillowresize_user_stream_with_tensor(): assert dst.dtype == dtype -@t.mark.parametrize( +@pytest.mark.parametrize( "batch_size", [ 3, @@ -281,3 +313,60 @@ def test_op_pillowresize_user_stream_with_image_batch(batch_size): assert len(dst) == len(src) assert dst.uniqueformat == dst.uniqueformat assert dst.maxsize == dst_sizes[0] + + +def _pillowresize_params(dtype, layout, channels): + return { + "shape": cv_types.resolve_shape(layout, channels, (10, 20)), + "format": cv_types.as_cvcuda_format(dtype), + "interp": cvcuda.Interp.LINEAR, + } + + +def _pillowresize_varshape_params(dtype, layout, channels): + return { + "sizes": [[10, 20], [10, 20]], + "interp": cvcuda.Interp.LINEAR, + } + + +globals().update( + cv_tools.make_op_tests( + name="pillowresize", + runner_info=[ + ("tensor", cvcuda.pillowresize, _pillowresize_params), + ], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 3), + supported_dtypes={ + cvcuda.Type.U8, + cvcuda.Type.S8, + cvcuda.Type.U16, + cvcuda.Type.S16, + cvcuda.Type.S32, + cvcuda.Type.F32, + }, + supported_layouts={"NHWC", "HWC", "NCHW", "CHW"}, + supported_channels={1, 2, 3, 4}, + # 2-channel interleaved is supported, but there is no 2-plane planar format. + exclude_dlc=[(None, "NCHW", 2), (None, "CHW", 2)], + ) +) + + +globals().update( + cv_tools.make_op_tests( + name="pillowresize_varshape", + runner_info=[ + ("image_batch", cvcuda.pillowresize, _pillowresize_varshape_params), + ], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 3), + supported_dtypes={ + cvcuda.Type.U8, + cvcuda.Type.U16, + cvcuda.Type.S16, + cvcuda.Type.F32, + }, + supported_layouts={"NHWC", "HWC"}, + supported_channels={1, 2, 3, 4}, + ) +) diff --git a/tests/cvcuda/python/test_opposterize.py b/tests/cvcuda/python/test_opposterize.py new file mode 100644 index 000000000..c8e9f0c54 --- /dev/null +++ b/tests/cvcuda/python/test_opposterize.py @@ -0,0 +1,118 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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 cvcuda + +import pytest +import numpy as np +import cupy + +import cvcuda_util as util + +RNG = np.random.default_rng(0) + + +def posterize_ref(src, bits): + info = np.iinfo(src.dtype) + mask = (info.max << (info.bits - bits)) & info.max + return (src & mask).astype(src.dtype) + + +def assert_posterized(got, src, bits): + ref = posterize_ref(src, bits) + got = np.asarray(got) + if got.shape == ref.shape[:-1] and ref.shape[-1] == 1: + got = got[..., np.newaxis] + else: + assert ( + got.shape == ref.shape + ), f"shape mismatch: got={got.shape}, expected={ref.shape}" + np.testing.assert_array_equal(got, ref) + + +@pytest.mark.parametrize( + "shape, dtype, layout, bits", + [ + ((5, 16, 23, 4), np.uint8, "NHWC", 4), + ((4, 9, 3), np.uint8, "HWC", 2), + ((3, 88, 13, 1), np.uint16, "NHWC", 5), + ((2, 4, 16, 23), np.uint8, "NCHW", 3), + ((3, 8, 8), np.uint8, "CHW", 1), + ], +) +def test_op_posterize(shape, dtype, layout, bits): + src_h = util.generate_data(shape, dtype, rng=RNG) + src = util.to_cvcuda_tensor(src_h, layout) + + out = cvcuda.posterize(src, bits) + assert out.layout == src.layout + assert out.shape == src.shape + assert out.dtype == src.dtype + assert_posterized(cupy.asarray(out.cuda()).get(), src_h, bits) + + stream = cvcuda.Stream() + out = cvcuda.Tensor(src.shape, src.dtype, src.layout) + tmp = cvcuda.posterize_into(src=src, dst=out, bits=bits, stream=stream) + stream.sync() + assert tmp is out + assert out.layout == src.layout + assert out.shape == src.shape + assert out.dtype == src.dtype + assert_posterized(cupy.asarray(out.cuda()).get(), src_h, bits) + + +@pytest.mark.parametrize( + "num_images, img_format, img_size, max_pixel, bits", + [ + (10, cvcuda.Format.RGB8, (123, 321), 256, 4), + (1, cvcuda.Format.U16, (33, 48), 1234, 5), + (4, cvcuda.Format.RGBA8, (26, 52), 256, 2), + ], +) +def test_op_posterize_varshape(num_images, img_format, img_size, max_pixel, bits): + w, h = img_size + dtype = util.get_numpy_dtype_for_format(img_format) + srcs_h = [ + util.generate_data((h, w, img_format.channels), dtype, max_pixel, RNG) + for _ in range(num_images) + ] + src_batch = cvcuda.ImageBatchVarShape(num_images) + for src_h in srcs_h: + src_batch.pushback(util.to_cvcuda_image(src_h)) + + out = cvcuda.posterize(src_batch, bits) + assert len(out) == len(src_batch) + assert out.capacity == src_batch.capacity + assert out.uniqueformat == src_batch.uniqueformat + assert out.maxsize == src_batch.maxsize + for got_img, src_h in zip(out, srcs_h): + assert_posterized(cupy.asarray(got_img.cuda()).get(), src_h, bits) + + stream = cvcuda.Stream() + out = util.clone_image_batch(src_batch) + tmp = cvcuda.posterize_into(src=src_batch, dst=out, bits=bits, stream=stream) + stream.sync() + assert tmp is out + assert len(out) == len(src_batch) + assert out.capacity == src_batch.capacity + for got_img, src_h in zip(out, srcs_h): + assert_posterized(cupy.asarray(got_img.cuda()).get(), src_h, bits) + + +def test_op_posterize_negative_dtype(): + # float32 is outside the supported dtype set (u8/u16) and must be rejected. + src = cvcuda.Tensor((1, 16, 16, 3), np.float32, "NHWC") + with pytest.raises(RuntimeError): + cvcuda.posterize(src, 4) diff --git a/tests/cvcuda/python/test_oprandomresizedcrop.py b/tests/cvcuda/python/test_oprandomresizedcrop.py index 4fabb6a5b..2609ccb8b 100644 --- a/tests/cvcuda/python/test_oprandomresizedcrop.py +++ b/tests/cvcuda/python/test_oprandomresizedcrop.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,9 +15,12 @@ import cvcuda -import pytest as t +import pytest import numpy as np + import cvcuda_util as util +import cvcuda_types as cv_types +import cvcuda_tools as cv_tools min_scale = 0.08 max_scale = 1.0 @@ -28,7 +31,7 @@ RNG = np.random.default_rng(0) -@t.mark.parametrize( +@pytest.mark.parametrize( "input_args,out_shape,interp", [ ( @@ -41,11 +44,21 @@ (5, 132, 15, 3), cvcuda.Interp.LINEAR, ), + ( + ((5, 3, 16, 23), np.uint8, "NCHW"), + (5, 3, 132, 15), + cvcuda.Interp.LINEAR, + ), ( ((16, 23, 4), np.uint8, "HWC"), (132, 15, 4), cvcuda.Interp.CUBIC, ), + ( + ((4, 16, 23), np.uint8, "CHW"), + (4, 132, 15), + cvcuda.Interp.CUBIC, + ), (((16, 23, 1), np.uint8, "HWC"), (132, 15, 1), None), ], ) @@ -97,7 +110,7 @@ def test_op_random_resized_crop(input_args, out_shape, interp): assert out.dtype == input.dtype -@t.mark.parametrize( +@pytest.mark.parametrize( "max_input_size, max_output_size, interp", [ ((123, 321), (321, 123), cvcuda.Interp.NEAREST), @@ -159,3 +172,41 @@ def test_op_random_resized_crop_varshape(max_input_size, max_output_size, interp assert out.capacity == input.capacity assert out.uniqueformat == input.uniqueformat assert out.maxsize <= max_output_size + + +def _randomresizedcrop_params(dtype, layout, channels): + return { + "shape": cv_types.resolve_shape(layout, channels, (10, 20)), + "interp": cvcuda.Interp.LINEAR, + } + + +def _randomresizedcrop_varshape_params(dtype, layout, channels): + return { + "sizes": [[20, 10], [20, 10]], + "interp": cvcuda.Interp.LINEAR, + } + + +globals().update( + cv_tools.make_op_tests( + name="random_resized_crop", + runner_info=[ + ("tensor", cvcuda.random_resized_crop, _randomresizedcrop_params), + ( + "image_batch", + cvcuda.random_resized_crop, + _randomresizedcrop_varshape_params, + ), + ], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 3), + supported_dtypes={ + cvcuda.Type.U8, + cvcuda.Type.U16, + cvcuda.Type.S16, + cvcuda.Type.F32, + }, + supported_layouts={"NHWC", "HWC", "NCHW", "CHW"}, + supported_channels={1, 3, 4}, + ) +) diff --git a/tests/cvcuda/python/test_opreformat.py b/tests/cvcuda/python/test_opreformat.py index 2b71fb270..fefc84544 100644 --- a/tests/cvcuda/python/test_opreformat.py +++ b/tests/cvcuda/python/test_opreformat.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,17 +13,19 @@ # See the License for the specific language governing permissions and # limitations under the License. -import torch import cvcuda -import pytest as t +import pytest import numpy as np import threading +import cvcuda_types as cv_types +import cvcuda_tools as cv_tools +import cupy RNG = np.random.default_rng(0) -@t.mark.parametrize( +@pytest.mark.parametrize( "input_args,out_shape,out_layout", [ (((5, 16, 23, 4), np.uint8, "NHWC"), (5, 4, 16, 23), "NCHW"), @@ -68,11 +70,14 @@ def test_op_reformat_gpuload(): src = cvcuda.Tensor(src_shape, np.uint8, src_layout) dst = cvcuda.Tensor(dst_shape, np.uint8, dst_layout) - torch0 = torch.zeros(src_shape, dtype=torch.int32, device="cuda") - torch1 = torch.zeros(src_shape, dtype=torch.int32, device="cuda") + cuda0 = cupy.asarray(np.zeros(src_shape, dtype=np.int32)) + cuda1 = cupy.asarray(np.zeros(src_shape, dtype=np.int32)) thread = threading.Thread( - target=lambda: (torch.abs(torch0, out=torch1), torch.square(torch1, out=torch0)) + target=lambda: ( + np.abs(cuda0.get(), out=cuda1.get()), + np.square(cuda1.get(), out=cuda0.get()), + ) ) thread.start() @@ -83,5 +88,40 @@ def test_op_reformat_gpuload(): assert dst.shape == dst_shape thread.join() - assert torch0.shape == src_shape - assert torch1.shape == src_shape + assert cuda0.shape == src_shape + assert cuda1.shape == src_shape + + +_layout_conversion = { + "NHWC": "NCHW", + "NCHW": "NHWC", + "HWC": "CHW", + "CHW": "HWC", +} + + +def _reformat_params(dtype, layout, channels): + inverse_layout = _layout_conversion.get(layout) + return { + "layout": inverse_layout if inverse_layout is not None else layout, + } + + +globals().update( + cv_tools.make_op_tests( + name="reformat", + runner_info=[("tensor", cvcuda.reformat, _reformat_params)], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 3), + supported_dtypes={ + cvcuda.Type.U8, + cvcuda.Type.S8, + cvcuda.Type.U16, + cvcuda.Type.S16, + cvcuda.Type.S32, + cvcuda.Type.F32, + cvcuda.Type.F64, + }, + supported_layouts={"NHWC", "NCHW", "HWC", "CHW"}, + supported_channels=cv_types.CHANNELS, + ) +) diff --git a/tests/cvcuda/python/test_opremap.py b/tests/cvcuda/python/test_opremap.py index 584516336..29f1d7190 100644 --- a/tests/cvcuda/python/test_opremap.py +++ b/tests/cvcuda/python/test_opremap.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,12 +13,15 @@ # See the License for the specific language governing permissions and # limitations under the License. -import torch - import cvcuda -import pytest as t +import pytest import numpy as np + import cvcuda_util as util +import cvcuda_types as cv_types +import cvcuda_tools as cv_tools +import cupy + RNG = np.random.default_rng(0) MAPS = { @@ -66,7 +69,7 @@ REF_SHAPE["default"] = REF_SHAPE["absolute"] -@t.mark.parametrize( +@pytest.mark.parametrize( "src_args, map_args", [ ( @@ -131,7 +134,20 @@ def test_op_remap_api(src_args, map_args): assert t_tmp is t_dst -@t.mark.parametrize( +def test_op_remap_rejects_border_value_longer_than_float4(): + t_src = cvcuda.Tensor((1, 4, 4, 3), cvcuda.Type.U8, "NHWC") + t_map = cvcuda.Tensor((1, 4, 4, 1), cvcuda.Type._2F32, "NHWC") + + with pytest.raises(RuntimeError): + cvcuda.remap( + t_src, + t_map, + border=cvcuda.Border.CONSTANT, + border_value=np.arange(5, dtype=np.float32), + ) + + +@pytest.mark.parametrize( "map_type, map_kind, num_maps, num_imgs, img_size, img_format", [ ( @@ -177,14 +193,14 @@ def test_op_remap_content(map_type, map_kind, num_maps, num_imgs, img_size, img_ t_dst = cvcuda.remap(t_src, t_map, map_type=map_type) - a_dst = torch.as_tensor(t_dst.cuda()).cpu().numpy() + a_dst = cupy.asarray(t_dst.cuda()).get() a_ref = CALC_REF[map_kind](a_src) np.testing.assert_array_equal(a_dst, a_ref) -@t.mark.parametrize( +@pytest.mark.parametrize( "num_images, img_format, max_size", [ (4, cvcuda.Format.Y8, (73, 98)), @@ -220,7 +236,7 @@ def test_op_remapvarshape_api(num_images, img_format, max_size): assert b_tmp is b_dst -@t.mark.parametrize( +@pytest.mark.parametrize( "map_type, img_size, img_format", [ (cvcuda.Remap.ABSOLUTE, (33, 65), cvcuda.Format.RGB8), @@ -234,7 +250,7 @@ def test_op_remapvarshape_content(map_type, img_size, img_format): b_src = cvcuda.ImageBatchVarShape(num_imgs) - for i in range(num_imgs): + for _ in range(num_imgs): b_src.pushback(util.to_cvcuda_image(a_img)) a_map = np.stack( @@ -254,3 +270,37 @@ def test_op_remapvarshape_content(map_type, img_size, img_format): a_ref = CALC_REF[map_kind](a_src) np.testing.assert_array_equal(a_dst, a_ref) + + +def _remap_params(dtype, layout, channels): + map_layout = "NHWC" if "N" in layout else "HWC" + map_shape = cv_types.resolve_shape( + map_layout, channels=1, size=(24, 24), batch_size=1 + ) + map_tensor = cvcuda.Tensor(map_shape, cvcuda.Type._2F32, map_layout) + return {"map": map_tensor} + + +def _remap_varshape_params(dtype, layout, channels): + map_tensor = cvcuda.Tensor((1, 24, 24, 1), cvcuda.Type._2F32, "NHWC") + return {"map": map_tensor} + + +globals().update( + cv_tools.make_op_tests( + name="remap", + runner_info=[ + ("tensor", cvcuda.remap, _remap_params), + ("image_batch", cvcuda.remap, _remap_varshape_params), + ], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 3), + supported_dtypes={cvcuda.Type.U8, cvcuda.Type.F32}, + supported_layouts={"NHWC", "HWC", "NCHW", "CHW"}, + supported_channels={1, 3, 4}, + # F32 only supports channel 1 + exclude_dlc=[ + (cvcuda.Type.F32, None, 3), + (cvcuda.Type.F32, None, 4), + ], + ) +) diff --git a/tests/cvcuda/python/test_opresize.py b/tests/cvcuda/python/test_opresize.py index a2c9666f5..c639ae700 100644 --- a/tests/cvcuda/python/test_opresize.py +++ b/tests/cvcuda/python/test_opresize.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,17 +15,21 @@ import cvcuda -import torch -import pytest as t +import pytest import numpy as np +import cvcuda_types as cv_types import cvcuda_util as util +import cvcuda_tools as cv_tools import threading import queue + +import cupy + RNG = np.random.default_rng(0) -@t.mark.parametrize( +@pytest.mark.parametrize( "input_args,out_shape,interp", [ ( @@ -42,6 +46,14 @@ (((37, 19, 3), np.uint8, "HWC"), (113, 47, 3), cvcuda.Interp.NEAREST), (((37, 19, 1), np.single, "HWC"), (113, 47, 1), None), (((37, 19, 3), np.single, "HWC"), (113, 47, 3), None), + # Planar (NCHW/CHW) layouts. + ( + ((5, 3, 16, 23), np.uint8, "NCHW"), + (5, 3, 132, 15), + cvcuda.Interp.LINEAR, + ), + (((4, 16, 23), np.uint8, "CHW"), (4, 132, 15), cvcuda.Interp.CUBIC), + (((3, 37, 19), np.single, "CHW"), (3, 113, 47), cvcuda.Interp.NEAREST), ], ) def test_op_resize(input_args, out_shape, interp): @@ -85,34 +97,29 @@ def test_op_resize(input_args, out_shape, interp): assert out.dtype == input.dtype -@t.mark.parametrize( +@pytest.mark.parametrize( "in_shape,out_shape,data_type,interp", [ - ( - (5, 720, 406, 3), - (5, 360, 203, 3), - torch.uint8, - cvcuda.Interp.NEAREST, - ), # noqa - ((5, 720, 406, 3), (5, 360, 203, 3), torch.uint8, cvcuda.Interp.LINEAR), # noqa - ((5, 720, 406, 3), (5, 360, 203, 3), torch.uint8, cvcuda.Interp.CUBIC), # noqa - ((3, 23, 23, 4), (3, 42, 42, 4), torch.uint8, cvcuda.Interp.LINEAR), # noqa - ((3, 23, 53, 1), (3, 132, 23, 1), torch.uint8, cvcuda.Interp.LINEAR), # noqa - ((3, 23, 53, 1), (3, 132, 23, 1), torch.float, cvcuda.Interp.LINEAR), # noqa - ((1, 37, 19, 1), (1, 113, 47, 1), torch.uint8, cvcuda.Interp.LINEAR), # noqa - ((1, 37, 19, 3), (1, 113, 47, 3), torch.uint8, cvcuda.Interp.NEAREST), # noqa - ((1, 37, 19, 1), (1, 113, 47, 1), torch.float, cvcuda.Interp.LINEAR), # noqa - ((1, 37, 19, 3), (1, 113, 47, 3), torch.float, cvcuda.Interp.LINEAR), # noqa + ((5, 720, 406, 3), (5, 360, 203, 3), np.uint8, cvcuda.Interp.NEAREST), # noqa + ((5, 720, 406, 3), (5, 360, 203, 3), np.uint8, cvcuda.Interp.LINEAR), # noqa + ((5, 720, 406, 3), (5, 360, 203, 3), np.uint8, cvcuda.Interp.CUBIC), # noqa + ((3, 23, 23, 4), (3, 42, 42, 4), np.uint8, cvcuda.Interp.LINEAR), # noqa + ((3, 23, 53, 1), (3, 132, 23, 1), np.uint8, cvcuda.Interp.LINEAR), # noqa + ((3, 23, 53, 1), (3, 132, 23, 1), np.float32, cvcuda.Interp.LINEAR), # noqa + ((1, 37, 19, 1), (1, 113, 47, 1), np.uint8, cvcuda.Interp.LINEAR), # noqa + ((1, 37, 19, 3), (1, 113, 47, 3), np.uint8, cvcuda.Interp.NEAREST), # noqa + ((1, 37, 19, 1), (1, 113, 47, 1), np.float32, cvcuda.Interp.LINEAR), # noqa + ((1, 37, 19, 3), (1, 113, 47, 3), np.float32, cvcuda.Interp.LINEAR), # noqa ], ) -def test_op_resize_packed_torch_tensor(in_shape, out_shape, data_type, interp): +def test_op_resize_packed_cuda_tensor(in_shape, out_shape, data_type, interp): stream = cvcuda.Stream() - input = torch.empty(in_shape, dtype=data_type, device="cuda:0") - output = torch.empty(out_shape, dtype=data_type, device="cuda:0") + input_tensor = cupy.asarray(np.empty(in_shape, dtype=data_type)) + output_tensor = cupy.asarray(np.empty(out_shape, dtype=data_type)) - src = cvcuda.as_tensor(input.cuda(0), "NHWC") - dst = cvcuda.as_tensor(output.cuda(0), "NHWC") + src = cvcuda.as_tensor(input_tensor, "NHWC") + dst = cvcuda.as_tensor(output_tensor, "NHWC") tmp = cvcuda.resize_into(dst, src, interp, stream=stream) stream.sync() @@ -122,7 +129,7 @@ def test_op_resize_packed_torch_tensor(in_shape, out_shape, data_type, interp): assert dst.dtype == src.dtype -@t.mark.parametrize( +@pytest.mark.parametrize( "inSize, outSize, interp", [((123, 321), (321, 123), cvcuda.Interp.LINEAR), ((123, 321), (321, 123), None)], ) @@ -175,7 +182,6 @@ def test_op_resize_multithread(): threads = [] def thread_run(dst_queue, src, dst_shape): - import cvcuda dst = cvcuda.resize(src, dst_shape) dst_queue.put(dst) @@ -214,7 +220,7 @@ def test_op_resize_user_stream_with_tensor(): assert dst.dtype == dtype -@t.mark.parametrize("batch_size", [5]) +@pytest.mark.parametrize("batch_size", [5]) def test_op_resize_user_stream_with_image_batch(batch_size): stream = cvcuda.Stream() src_shape = (batch_size, 1080, 1920, 4) @@ -232,3 +238,37 @@ def test_op_resize_user_stream_with_image_batch(batch_size): assert len(dst) == len(src) assert dst.uniqueformat == dst.uniqueformat assert dst.maxsize == dst_sizes[0] + + +def _resize_params(dtype, layout, channels): + return { + "shape": cv_types.resolve_shape(layout, channels, (10, 20)), + "interp": cvcuda.Interp.LINEAR, + } + + +def _resize_varshape_params(dtype, layout, channels): + return { + "sizes": [[10, 20], [10, 20]], + "interp": cvcuda.Interp.LINEAR, + } + + +globals().update( + cv_tools.make_op_tests( + name="resize", + runner_info=[ + ("tensor", cvcuda.resize, _resize_params), + ("image_batch", cvcuda.resize, _resize_varshape_params), + ], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 3), + supported_dtypes={ + cvcuda.Type.U8, + cvcuda.Type.U16, + cvcuda.Type.S16, + cvcuda.Type.F32, + }, + supported_layouts={"NHWC", "HWC", "NCHW", "CHW"}, + supported_channels={1, 3, 4}, + ) +) diff --git a/tests/cvcuda/python/test_opresizecropconvertreformat.py b/tests/cvcuda/python/test_opresizecropconvertreformat.py index 908ca9031..19616d52d 100644 --- a/tests/cvcuda/python/test_opresizecropconvertreformat.py +++ b/tests/cvcuda/python/test_opresizecropconvertreformat.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,10 +14,11 @@ # limitations under the License. import numpy as np -import pytest as t +import pytest import cvcuda -import torch +import cvcuda_tools as cv_tools +import cupy # NOTE: The following tests for resize_crop_convert_reformat DO NOT TEST: # 1. The correctness of the output data @@ -25,11 +26,37 @@ # 3. Whether the channel swapping actually worked correctly w.r.t. the data -@t.mark.parametrize( +@pytest.mark.parametrize( "tensor_params, resize_dim, resize_interpolation, crop_rect_params, " "out_layout, out_dtype, manip, out_expected_shape, scale_norm, offset_norm, " "is_positive_test", [ + ( + ((4, 512, 512, 1), np.uint8, "NHWC"), # 1-channel (grayscale) input + (256, 256), + cvcuda.Interp.LINEAR, + (0, 0, 224, 224), + "NHWC", + cvcuda.Type.F32, + cvcuda.ChannelManip.NO_OP, + (4, 224, 224, 1), + 1, + 0, + True, + ), + ( + ((4, 512, 512, 1), np.uint8, "NHWC"), # 1-channel (grayscale) to uint8 + (256, 256), + cvcuda.Interp.NEAREST, + (0, 0, 224, 224), + "NHWC", + cvcuda.Type.U8, + cvcuda.ChannelManip.NO_OP, + (4, 224, 224, 1), + 1, + 0, + True, + ), ( ((4, 512, 512, 3), np.uint8, "NHWC"), # Basic test (256, 256), @@ -135,7 +162,7 @@ True, ), ( - ((3, 512, 512), np.uint8, "CHW"), # Unsupported input CHW + ((3, 512, 512), np.uint8, "CHW"), (256, 256), cvcuda.Interp.LINEAR, (0, 0, 224, 224), @@ -145,7 +172,20 @@ (3, 224, 224), 1, 0, - False, # Negative test + True, + ), + ( + ((4, 3, 512, 512), np.uint8, "NCHW"), + (256, 256), + cvcuda.Interp.NEAREST, + (0, 0, 224, 224), + "NHWC", + cvcuda.Type.U8, + cvcuda.ChannelManip.REVERSE, + (4, 224, 224, 3), + 1, + 0, + True, ), ( ((512, 1024, 3), np.uint8, "HWC"), # Large sizes @@ -297,12 +337,12 @@ def test_op_resize_crop_convert_reformat( # Compare the two if is_positive_test: - out1 = torch.as_tensor(out1.cuda()) - out2 = torch.as_tensor(out2.cuda()) - assert torch.equal(out1, out2) + out1 = cupy.asarray(out1.cuda()) + out2 = cupy.asarray(out2.cuda()) + assert np.all(out1.get() == out2.get()) -@t.mark.parametrize( +@pytest.mark.parametrize( "num_images, min_size, max_size, resize_dim, resize_interpolation, crop_rect_params, " "out_layout, out_dtype, manip, out_expected_shape, scale_norm, offset_norm, is_positive_test", [ @@ -481,6 +521,43 @@ def test_op_resize_crop_convert_reformat_varshape( if is_positive_test: # Compare the two - out1 = torch.as_tensor(out1.cuda()) - out2 = torch.as_tensor(out2.cuda()) - assert torch.equal(out1, out2) + out1 = cupy.asarray(out1.cuda()) + out2 = cupy.asarray(out2.cuda()) + assert np.all(out1.get() == out2.get()) + + +def _resizecropconvertreformat_params(dtype, layout, channels): + return { + "resize_dim": (16, 16), + "interp": cvcuda.Interp.LINEAR, + "crop_rect": cvcuda.RectI(0, 0, 16, 16), + "layout": "", + "data_type": cvcuda.Type.U8, + "manip": cvcuda.ChannelManip.REVERSE, + "scale": 1.0, + "offset": 0.0, + "srcCast": True, + } + + +globals().update( + cv_tools.make_op_tests( + name="resize_crop_convert_reformat", + runner_info=[ + ( + "tensor", + cvcuda.resize_crop_convert_reformat, + _resizecropconvertreformat_params, + ), + ( + "image_batch", + cvcuda.resize_crop_convert_reformat, + _resizecropconvertreformat_params, + ), + ], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 3), + supported_dtypes={cvcuda.Type.U8}, + supported_layouts={"NHWC", "HWC", "NCHW", "CHW"}, + supported_channels={1, 3}, + ) +) diff --git a/tests/cvcuda/python/test_oprotate.py b/tests/cvcuda/python/test_oprotate.py index 1754465dd..17e6d2803 100644 --- a/tests/cvcuda/python/test_oprotate.py +++ b/tests/cvcuda/python/test_oprotate.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,14 +15,15 @@ import cvcuda -import pytest as t +import pytest import numpy as np +import cvcuda_tools as cv_tools import cvcuda_util as util RNG = np.random.default_rng(0) -@t.mark.parametrize( +@pytest.mark.parametrize( "input_args, angle_deg, shift, interpolation", [ ( @@ -87,7 +88,7 @@ def test_op_rotate(input_args, angle_deg, shift, interpolation): assert out.dtype == input.dtype -@t.mark.parametrize( +@pytest.mark.parametrize( "nimages, format, max_size, max_pixel, max_angle_deg, max_shift, interpolation", [ ( @@ -168,3 +169,39 @@ def test_op_rotatevarshape( assert out.capacity == input.capacity assert out.uniqueformat == input.uniqueformat assert out.maxsize == input.maxsize + + +def _rotate_params(dtype, layout, channels): + return { + "angle_deg": 45.0, + "shift": [0, 0], + "interpolation": cvcuda.Interp.LINEAR, + } + + +def _rotate_varshape_params(dtype, layout, channels): + return { + "angle_deg": util.create_tensor((2,), np.float64, "N", max_random=180, rng=RNG), + "shift": util.create_tensor((2, 2), np.float64, "NC", max_random=5, rng=RNG), + "interpolation": cvcuda.Interp.LINEAR, + } + + +globals().update( + cv_tools.make_op_tests( + name="rotate", + runner_info=[ + ("tensor", cvcuda.rotate, _rotate_params), + ("image_batch", cvcuda.rotate, _rotate_varshape_params), + ], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 3), + supported_dtypes={ + cvcuda.Type.U8, + cvcuda.Type.U16, + cvcuda.Type.S16, + cvcuda.Type.F32, + }, + supported_layouts={"NHWC", "HWC", "NCHW", "CHW"}, + supported_channels={1, 3, 4}, + ) +) diff --git a/tests/cvcuda/python/test_opsift.py b/tests/cvcuda/python/test_opsift.py index 0bdb85aee..82f4a07aa 100644 --- a/tests/cvcuda/python/test_opsift.py +++ b/tests/cvcuda/python/test_opsift.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,8 +14,9 @@ # limitations under the License. import cvcuda +import cvcuda_tools as cv_tools -import pytest as t +import pytest import numpy as np @@ -25,11 +26,13 @@ def gold_max_features(in_tensor): return max(w * h // 20, 1) -@t.mark.parametrize( +@pytest.mark.parametrize( "in_args", [ (((3, 13, 24, 1), np.uint8, "NHWC")), + (((3, 1, 13, 24), np.uint8, "NCHW")), (((23, 34, 1), np.uint8, "HWC")), + (((1, 23, 34), np.uint8, "CHW")), ], ) def test_op_sift_api(in_args): @@ -102,3 +105,15 @@ def test_op_sift_api(in_args): ) for ret, out in zip(rets, outs): assert ret is out + + +globals().update( + cv_tools.make_op_tests( + name="sift", + runner_info=[("tensor", cvcuda.sift, None)], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 1), + supported_dtypes={cvcuda.Type.U8}, + supported_layouts={"NHWC", "HWC", "NCHW", "CHW"}, + supported_channels={1}, + ) +) diff --git a/tests/cvcuda/python/test_opsolarize.py b/tests/cvcuda/python/test_opsolarize.py new file mode 100644 index 000000000..7a37d693a --- /dev/null +++ b/tests/cvcuda/python/test_opsolarize.py @@ -0,0 +1,85 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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 cvcuda + +import pytest +import numpy as np + +import cvcuda_util as util + +RNG = np.random.default_rng(0) + + +@pytest.mark.parametrize( + "tensor_params, threshold", + [ + (((5, 16, 23, 4), np.uint8, "NHWC"), 128.0), + (((4, 9, 3), np.uint8, "HWC"), 100.0), + (((3, 88, 13, 1), np.uint16, "NHWC"), 32768.0), + (((2, 4, 16, 23), np.float32, "NCHW"), 0.5), + (((3, 8, 8), np.float32, "CHW"), 0.5), + ], +) +def test_op_solarize(tensor_params, threshold): + input = cvcuda.Tensor(*tensor_params) + + out = cvcuda.solarize(input, threshold) + assert out.layout == input.layout + assert out.shape == input.shape + assert out.dtype == input.dtype + + stream = cvcuda.Stream() + out = cvcuda.Tensor(input.shape, input.dtype, input.layout) + tmp = cvcuda.solarize_into(src=input, dst=out, threshold=threshold, stream=stream) + assert tmp is out + assert out.layout == input.layout + assert out.shape == input.shape + assert out.dtype == input.dtype + + +@pytest.mark.parametrize( + "num_images, img_format, img_size, max_pixel, threshold", + [ + (10, cvcuda.Format.RGB8, (123, 321), 256, 128.0), + (7, cvcuda.Format.RGBf32, (62, 35), 1.0, 0.5), + (1, cvcuda.Format.U16, (33, 48), 1234, 600.0), + (4, cvcuda.Format.RGBA8, (26, 52), 256, 100.0), + ], +) +def test_op_solarize_varshape(num_images, img_format, img_size, max_pixel, threshold): + input = util.create_image_batch( + num_images, img_format, size=img_size, max_random=max_pixel, rng=RNG + ) + + out = cvcuda.solarize(input, threshold) + assert len(out) == len(input) + assert out.capacity == input.capacity + assert out.uniqueformat == input.uniqueformat + assert out.maxsize == input.maxsize + + stream = cvcuda.Stream() + out = util.clone_image_batch(input) + tmp = cvcuda.solarize_into(src=input, dst=out, threshold=threshold, stream=stream) + assert tmp is out + assert len(out) == len(input) + assert out.capacity == input.capacity + + +def test_op_solarize_negative_dtype(): + # float16 is outside the supported dtype set (u8/u16/f32) and must be rejected. + input = cvcuda.Tensor((1, 16, 16, 3), np.float16, "NHWC") + with pytest.raises(Exception): + cvcuda.solarize(input, 0.5) diff --git a/tests/cvcuda/python/test_opstack.py b/tests/cvcuda/python/test_opstack.py index d88f547d6..22eb92082 100644 --- a/tests/cvcuda/python/test_opstack.py +++ b/tests/cvcuda/python/test_opstack.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,14 +15,17 @@ import cvcuda -import pytest as t +import pytest import numpy as np import random +import cvcuda_types as cv_types +import cvcuda_tools as cv_tools + random.seed(1) -@t.mark.parametrize( +@pytest.mark.parametrize( "input, dtype, number", [ (((5, 16, 23, 4), np.uint8, "NHWC"), np.int8, 2), @@ -102,3 +105,116 @@ def test_op_stack(input, dtype, number): assert output_tensor.shape[1] == input_tensors[0].shape[1] assert output_tensor.shape[2] == input_tensors[0].shape[2] assert output_tensor.shape[3] == input_tensors[0].shape[3] + + +globals().update( + cv_tools.make_op_tests( + name="stack", + runner_info=[("tensor_batch", cvcuda.stack, None)], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 3), + supported_dtypes=cv_types.SCALAR_TYPES_SET, + supported_layouts={"NHWC", "NCHW", "HWC", "CHW"}, + supported_channels=cv_types.CHANNELS, + ) +) + + +@pytest.mark.parametrize( + "input, dtype, number", + [ + (((5, 16, 23, 4), np.uint8, "NHWC"), np.int8, 2), + (((1, 160, 221, 2), np.uint8, "NHWC"), np.int8, 3), + (((6, 61, 12, 3), np.uint8, "NHWC"), np.int8, 5), + (((5, 161, 23, 4), np.uint8, "NCHW"), np.int8, 2), + (((16, 23, 4), np.uint8, "HWC"), np.int8, 2), + (((161, 23, 4), np.uint8, "CHW"), np.int8, 2), + ], +) +def test_op_stack_tensorbatch(input, dtype, number): + """Test stack with TensorBatch input directly.""" + input_tensors = [] + numberOfTensors = 0 + + updated_input = list(input) + for _ in range(number): + if updated_input[2] == "NHWC" or updated_input[2] == "NCHW": + updated_input[0] = (random.randint(1, input[0][0]),) + input[0][1:] + numberOfTensors += updated_input[0][0] + else: + numberOfTensors += 1 + input_tensor = cvcuda.Tensor(*updated_input) + input_tensors.append(input_tensor) + + # Create TensorBatch from list of tensors + tensor_batch = cvcuda.TensorBatch(len(input_tensors)) + for tensor in input_tensors: + tensor_batch.pushback(tensor) + + # Test stack with TensorBatch + out = cvcuda.stack(tensor_batch) + + assert out.shape[0] == numberOfTensors + assert out.dtype == input_tensors[0].dtype + + # Test stack_into with TensorBatch + outputTensorDef = list(updated_input) + if updated_input[2] == "NHWC" or updated_input[2] == "NCHW": + outputTensorDef[0] = (numberOfTensors,) + input[0][1:] + else: + outputTensorDef[0] = (numberOfTensors,) + input[0][0:] + if updated_input[2] == "HWC": + outputTensorDef[2] = "NHWC" + else: + outputTensorDef[2] = "NCHW" + + output_tensor = cvcuda.Tensor(*outputTensorDef) + tmp = cvcuda.stack_into(output_tensor, tensor_batch) + + assert tmp is output_tensor + assert output_tensor.shape[0] == numberOfTensors + assert output_tensor.dtype == input_tensors[0].dtype + + +@pytest.mark.parametrize( + "format, num_images, width, height", + [ + (cvcuda.Format.U8, 5, 64, 48), + (cvcuda.Format.RGB8, 3, 128, 96), + (cvcuda.Format.RGBA8, 2, 32, 24), + (cvcuda.Format.RGBf32, 4, 64, 64), + (cvcuda.Format.U8, 1, 16, 16), + ], +) +def test_op_stack_varshape(format, num_images, width, height): + """Test stack with ImageBatchVarShape input.""" + # Create ImageBatchVarShape with same-size images + input_batch = cvcuda.ImageBatchVarShape(num_images) + + images = [cvcuda.Image((width, height), format) for _ in range(num_images)] + input_batch.pushback(images) + + # Test stack with ImageBatchVarShape + out = cvcuda.stack(input_batch) + + assert out.shape[0] == num_images + # Check output dimensions match input image dimensions + if format.planes == 1: + # Interleaved format (NHWC) + assert out.shape[1] == height + assert out.shape[2] == width + else: + # Planar format (NCHW) + assert out.shape[2] == height + assert out.shape[3] == width + + # Test stack_into with ImageBatchVarShape + output_tensor = cvcuda.Tensor(num_images, (width, height), format) + tmp = cvcuda.stack_into(output_tensor, input_batch) + + assert tmp is output_tensor + assert output_tensor.shape[0] == num_images + + # Test with stream + stream = cvcuda.Stream() + out_stream = cvcuda.stack(input_batch, stream=stream) + assert out_stream.shape[0] == num_images diff --git a/tests/cvcuda/python/test_opthreshold.py b/tests/cvcuda/python/test_opthreshold.py index 9d176d3f5..766d9dc3a 100644 --- a/tests/cvcuda/python/test_opthreshold.py +++ b/tests/cvcuda/python/test_opthreshold.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,13 +13,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -import cvcuda - -import pytest as t from random import randint +import cvcuda +import cvcuda_tools as cv_tools +import pytest + -@t.mark.parametrize( +@pytest.mark.parametrize( "input_args, thtype", [ ( @@ -91,7 +92,41 @@ def test_op_threshold(input_args, thtype): assert tmp is out -@t.mark.parametrize( +@pytest.mark.parametrize( + "input_args, thtype", + [ + ( + ((2, 3, 64, 48), cvcuda.Type.U8, "NCHW"), + cvcuda.ThresholdType.BINARY, + ), + ( + ((4, 32, 48), cvcuda.Type.F32, "CHW"), + cvcuda.ThresholdType.TRUNC, + ), + ( + ((3, 1, 40, 32), cvcuda.Type.U8, "NCHW"), + cvcuda.ThresholdType.OTSU | cvcuda.ThresholdType.BINARY, + ), + ], +) +def test_op_threshold_planar(input_args, thtype): + input = cvcuda.Tensor(*input_args) + batch = input.shape[0] if input_args[2].startswith("N") else 1 + + thresh = cvcuda.Tensor((batch,), cvcuda.Type.F64, "N") + maxval = cvcuda.Tensor((batch,), cvcuda.Type.F64, "N") + + out = cvcuda.threshold(input, thresh, maxval, thtype) + assert out.layout == input.layout + assert out.shape == input.shape + assert out.dtype == input.dtype + + out = cvcuda.Tensor(input.shape, input.dtype, input.layout) + tmp = cvcuda.threshold_into(out, input, thresh, maxval, thtype) + assert tmp is out + + +@pytest.mark.parametrize( "num_images, format, min_size, max_size, thtype", [ ( @@ -194,3 +229,108 @@ def test_op_threshold_varshape(num_images, format, min_size, max_size, thtype): stream=stream, ) assert tmp is output + + +@pytest.mark.parametrize( + "fmt, thtype", + [ + (cvcuda.Format.RGB8p, cvcuda.ThresholdType.BINARY), + (cvcuda.Format.RGBf32p, cvcuda.ThresholdType.TRUNC), + (cvcuda.Format.RGBAf32p, cvcuda.ThresholdType.TOZERO_INV), + ], +) +def test_op_threshold_varshape_planar(fmt, thtype): + num_images = 2 + thresh = cvcuda.Tensor((num_images,), cvcuda.Type.F64, "N") + maxval = cvcuda.Tensor((num_images,), cvcuda.Type.F64, "N") + + input = cvcuda.ImageBatchVarShape(num_images) + output = cvcuda.ImageBatchVarShape(num_images) + for i in range(num_images): + size = (48 + i, 32 + i) + input.pushback(cvcuda.Image(size, fmt)) + output.pushback(cvcuda.Image(size, fmt)) + + tmp = cvcuda.threshold(input, thresh, maxval, thtype) + assert tmp.uniqueformat is not None + assert tmp.uniqueformat == output.uniqueformat + for res, ref in zip(tmp, output, strict=True): + assert res.size == ref.size + assert res.format == ref.format + + tmp = cvcuda.threshold_into(output, input, thresh, maxval, thtype) + assert tmp is output + + +def _threshold_binary_params(dtype, layout, channels): + return { + "thresh": cvcuda.Tensor((1,), cvcuda.Type.F64, "N"), + "maxval": cvcuda.Tensor((1,), cvcuda.Type.F64, "N"), + "type": cvcuda.ThresholdType.BINARY, + } + + +def _threshold_otsu_params(dtype, layout, channels): + return { + "thresh": cvcuda.Tensor((1,), cvcuda.Type.F64, "N"), + "maxval": cvcuda.Tensor((1,), cvcuda.Type.F64, "N"), + "type": cvcuda.ThresholdType.OTSU | cvcuda.ThresholdType.BINARY, + } + + +def _threshold_triangle_params(dtype, layout, channels): + return { + "thresh": cvcuda.Tensor((1,), cvcuda.Type.F64, "N"), + "maxval": cvcuda.Tensor((1,), cvcuda.Type.F64, "N"), + "type": cvcuda.ThresholdType.TRIANGLE | cvcuda.ThresholdType.BINARY, + } + + +globals().update( + cv_tools.make_op_tests( + name="threshold_binary", + runner_info=[ + ("tensor", cvcuda.threshold, _threshold_binary_params), + ("image_batch", cvcuda.threshold, _threshold_binary_params), + ], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 3), + supported_dtypes={ + cvcuda.Type.U8, + cvcuda.Type.U16, + cvcuda.Type.S16, + cvcuda.Type.F32, + cvcuda.Type.F64, + }, + supported_layouts={"NHWC", "HWC", "NCHW", "CHW"}, + supported_channels={1, 2, 3, 4}, + exclude_dlc=[(None, "NCHW", 2), (None, "CHW", 2)], + ) +) + +globals().update( + cv_tools.make_op_tests( + name="threshold_otsu", + runner_info=[ + ("tensor", cvcuda.threshold, _threshold_otsu_params), + ("image_batch", cvcuda.threshold, _threshold_otsu_params), + ], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 1), + supported_dtypes={cvcuda.Type.U8}, + supported_layouts={"NHWC", "HWC", "NCHW", "CHW"}, + supported_channels={1}, + ) +) + +globals().update( + cv_tools.make_op_tests( + name="threshold_triangle", + runner_info=[ + ("tensor", cvcuda.threshold, _threshold_triangle_params), + ("image_batch", cvcuda.threshold, _threshold_triangle_params), + ], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 1), + supported_dtypes={cvcuda.Type.U8}, + supported_layouts={"NHWC", "HWC", "NCHW", "CHW"}, + supported_channels={1}, + ) +) diff --git a/tests/cvcuda/python/test_opwarpaffine.py b/tests/cvcuda/python/test_opwarpaffine.py index 8c0a44e74..033287768 100644 --- a/tests/cvcuda/python/test_opwarpaffine.py +++ b/tests/cvcuda/python/test_opwarpaffine.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,14 +15,16 @@ import cvcuda -import pytest as t +import pytest import numpy as np + import cvcuda_util as util +import cvcuda_tools as cv_tools RNG = np.random.default_rng(0) -@t.mark.parametrize( +@pytest.mark.parametrize( "input_args, xform, flags, border_mode, border_value", [ ( @@ -102,7 +104,7 @@ def test_op_warp_affine(input_args, xform, flags, border_mode, border_value): assert out.dtype == input.dtype -@t.mark.parametrize( +@pytest.mark.parametrize( "nimages, format, max_size, max_pixel, max_xval, flags, bmode, border_value", [ ( @@ -148,7 +150,14 @@ def test_op_warp_affine(input_args, xform, flags, border_mode, border_value): ], ) def test_op_warp_affinevarshape( - nimages, format, max_size, max_pixel, max_xval, flags, bmode, border_value + nimages, + format, + max_size, + max_pixel, + max_xval, + flags, + bmode, + border_value, ): input = util.create_image_batch( @@ -184,3 +193,39 @@ def test_op_warp_affinevarshape( assert out.capacity == input.capacity assert out.uniqueformat == input.uniqueformat assert out.maxsize == input.maxsize + + +def _warp_affine_params(dtype, layout, channels): + return { + "xform": [[1, 0, 0], [0, 1, 0]], + "flags": cvcuda.Interp.NEAREST, + } + + +def _warp_affine_varshape_params(dtype, layout, channels): + return { + "xform": util.create_tensor((2, 6), np.float32, "NC", max_random=1, rng=RNG), + "flags": cvcuda.Interp.NEAREST, + "border_mode": cvcuda.Border.CONSTANT, + "border_value": np.array([], dtype=np.float32), + } + + +globals().update( + cv_tools.make_op_tests( + name="warp_affine", + runner_info=[ + ("tensor", cvcuda.warp_affine, _warp_affine_params), + ("image_batch", cvcuda.warp_affine, _warp_affine_varshape_params), + ], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 3), + supported_dtypes={ + cvcuda.Type.U8, + cvcuda.Type.U16, + cvcuda.Type.S16, + cvcuda.Type.F32, + }, + supported_layouts={"NHWC", "HWC", "NCHW", "CHW"}, + supported_channels={1, 3, 4}, + ) +) diff --git a/tests/cvcuda/python/test_opwarpperspective.py b/tests/cvcuda/python/test_opwarpperspective.py index 5fa58a8cf..1a4cfde8b 100644 --- a/tests/cvcuda/python/test_opwarpperspective.py +++ b/tests/cvcuda/python/test_opwarpperspective.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,14 +15,16 @@ import cvcuda -import pytest as t +import pytest import numpy as np + import cvcuda_util as util +import cvcuda_tools as cv_tools RNG = np.random.default_rng(0) -@t.mark.parametrize( +@pytest.mark.parametrize( "input_args, xform, flags, border_mode, border_value", [ ( @@ -141,7 +143,33 @@ def test_op_warp_perspective(input_args, xform, flags, border_mode, border_value assert out.dtype == input.dtype -@t.mark.parametrize( +def test_op_warp_perspective_extreme_projection_replicate_issue_249(): + # Regression test for CVCUDA issue #249: warp_perspective with a projective + # matrix whose singular line falls inside the destination image produces + # source coordinates near +/-INT32_MAX. With BORDER_REPLICATE this used to + # trigger an illegal CUDA memory access. Syncing the stream is required to + # surface the kernel error as a test failure. + xform = [ + [8.08776838e-02, 2.36326631e00, -4.08795000e02], + [-1.28514739e-02, 2.55201343e-01, -8.45896673e01], + [-2.68404432e-04, -6.57235630e-04, 1.00000000e00], + ] + input = cvcuda.Tensor((1208, 1928, 3), np.uint8, "HWC") + out = cvcuda.Tensor(input.shape, input.dtype, input.layout) + stream = cvcuda.Stream() + cvcuda.warp_perspective_into( + src=input, + dst=out, + xform=xform, + flags=cvcuda.Interp.LINEAR, + border_mode=cvcuda.Border.REPLICATE, + border_value=[0], + stream=stream, + ) + stream.sync() + + +@pytest.mark.parametrize( "nimages, format, max_size, max_pixel, max_xval, flags, bmode, border_value", [ ( @@ -187,7 +215,14 @@ def test_op_warp_perspective(input_args, xform, flags, border_mode, border_value ], ) def test_op_warp_perspectivevarshape( - nimages, format, max_size, max_pixel, max_xval, flags, bmode, border_value + nimages, + format, + max_size, + max_pixel, + max_xval, + flags, + bmode, + border_value, ): input = util.create_image_batch( @@ -223,3 +258,41 @@ def test_op_warp_perspectivevarshape( assert out.capacity == input.capacity assert out.uniqueformat == input.uniqueformat assert out.maxsize == input.maxsize + + +def _warp_perspective_params(dtype, layout, channels): + return { + "xform": np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]], dtype=np.float32), + "flags": int(cvcuda.Interp.NEAREST), + "border_mode": cvcuda.Border.CONSTANT, + "border_value": np.array([], dtype=np.float32), + } + + +def _warp_perspective_varshape_params(dtype, layout, channels): + return { + "xform": util.create_tensor((2, 9), np.float32, "NC", max_random=1, rng=RNG), + "flags": int(cvcuda.Interp.NEAREST), + "border_mode": cvcuda.Border.CONSTANT, + "border_value": np.array([], dtype=np.float32), + } + + +globals().update( + cv_tools.make_op_tests( + name="warp_perspective", + runner_info=[ + ("tensor", cvcuda.warp_perspective, _warp_perspective_params), + ("image_batch", cvcuda.warp_perspective, _warp_perspective_varshape_params), + ], + keystone_dlc=(cvcuda.Type.U8, "NHWC", 3), + supported_dtypes={ + cvcuda.Type.U8, + cvcuda.Type.U16, + cvcuda.Type.S16, + cvcuda.Type.F32, + }, + supported_layouts={"NHWC", "HWC", "NCHW", "CHW"}, + supported_channels={1, 3, 4}, + ) +) diff --git a/tests/cvcuda/python/test_re_export.py b/tests/cvcuda/python/test_re_export.py index f07b36651..53264cb5b 100644 --- a/tests/cvcuda/python/test_re_export.py +++ b/tests/cvcuda/python/test_re_export.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,7 +20,7 @@ def test_nvcv_types_available_in_cvcuda(): """Verify nvcv types are accessible through cvcuda module.""" # Clear any previous imports to test fresh for mod in list(sys.modules.keys()): - if mod.startswith("nvcv") or mod.startswith("cvcuda"): + if mod.startswith("cvcuda"): del sys.modules[mod] import cvcuda @@ -43,20 +43,22 @@ def test_functional_usage(): """Test that cvcuda types work functionally.""" # Clear any previous imports to test fresh for mod in list(sys.modules.keys()): - if mod.startswith("nvcv") or mod.startswith("cvcuda"): + if mod.startswith("cvcuda"): del sys.modules[mod] + import numpy as np + import cvcuda - import torch + import cupy # Create tensor using cvcuda re-exported types - torch_tensor = torch.rand(4, 32, 32, 3, dtype=torch.float32, device="cuda") - nvcv_tensor = cvcuda.as_tensor(torch_tensor, layout="NHWC") + cupy_tensor = cupy.asarray(np.random.rand(4, 32, 32, 3).astype(np.float32)) + cvcuda_tensor = cvcuda.as_tensor(cupy_tensor, layout="NHWC") # Verify it's the right type - assert isinstance(nvcv_tensor, cvcuda.Tensor) + assert isinstance(cvcuda_tensor, cvcuda.Tensor) # Use cvcuda operator - resize requires full shape for NHWC tensors - result = cvcuda.resize(nvcv_tensor, (4, 16, 16, 3), cvcuda.Interp.LINEAR) + result = cvcuda.resize(cvcuda_tensor, (4, 16, 16, 3), cvcuda.Interp.LINEAR) assert isinstance(result, cvcuda.Tensor) assert result.shape == (4, 16, 16, 3) diff --git a/tests/cvcuda/python/test_rect.py b/tests/cvcuda/python/test_rect.py index eba654116..509bd9b68 100644 --- a/tests/cvcuda/python/test_rect.py +++ b/tests/cvcuda/python/test_rect.py @@ -13,9 +13,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -import cvcuda import pytest as t +import cvcuda + def test_recti_default(): r = cvcuda.RectI() diff --git a/tests/cvcuda/python/test_resource_submitsync.py b/tests/cvcuda/python/test_resource_submitsync.py new file mode 100644 index 000000000..8f6616a2e --- /dev/null +++ b/tests/cvcuda/python/test_resource_submitsync.py @@ -0,0 +1,204 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""Unit tests for Resource::submitSync's three control-flow paths. + +submitSync has three paths: + + 1. Fast path — resource already bound to the same stream: return immediately, + no cudaGetDevice, no event work. + 2. First-bind — resource has never been submitted: take ownership of the + stream with no sync work. + 3. Cross-stream — resource moves from stream A to stream B: record an event + on A, wait on B, so B observes all prior A work. + +The CAI-sentinel sub-path (cudaStreamLegacy / cudaStreamPerThread → fall back +to cudaDeviceSynchronize) is covered separately in test_cai_input_stream_race.py. +""" + +import cupy + +import cvcuda + +# GPU-side busy-wait: keeps a stream occupied for long enough that any missing +# cross-stream barrier surfaces as wrong output. ~500 ms at 1 GHz SM clock. +_BUSY_WAIT = cupy.RawKernel( + r""" +extern "C" __global__ void busy_wait(unsigned long long cycles) { + unsigned long long t = clock64(); + while (clock64() - t < cycles) {} +} +""", + "busy_wait", +) +_WAIT_CYCLES = 500_000_000 + + +def _fill_tensor(tensor, value, stream): + """Fill a uint8 NHWC cvcuda tensor with a constant via cupy on *stream*.""" + cp_arr = cupy.asarray(tensor.cuda(stream.handle)) + with stream._cupy_stream(): + cp_arr.fill(value) + + +class _CupyStream: + """Thin wrapper so a raw cupy Stream handle can be used as a context manager.""" + + def __init__(self, handle): + self._s = cupy.cuda.ExternalStream(handle) + + def __enter__(self): + self._s.__enter__() + return self + + def __exit__(self, *a): + self._s.__exit__(*a) + + +class _RawStreamHandle: + """Wraps a raw integer CUDA stream handle as a cupy stream protocol object.""" + + def __init__(self, handle: int): + self._handle = handle + + def __cuda_stream__(self): + return (0, self._handle) + + +def _as_cupy_stream(cvcuda_stream): + if hasattr(cupy.cuda.Stream, "from_external"): + return cupy.cuda.Stream.from_external(_RawStreamHandle(cvcuda_stream.handle)) + return _CupyStream(cvcuda_stream.handle) + + +# --------------------------------------------------------------------------- +# Path 1 — first-binding +# --------------------------------------------------------------------------- + + +def test_first_bind_takes_ownership(): + """submitStreamSync on a never-submitted resource must not raise.""" + stream = cvcuda.Stream() + tensor = cvcuda.Tensor((1, 4, 4, 1), cvcuda.Type.U8, "NHWC") + # No assertion beyond "does not throw": first-bind sets m_lastStream and + # returns without doing any event or device work. + tensor.submitStreamSync(stream) + + +def test_first_bind_then_op_produces_correct_output(): + """A resource used for the first time on a stream produces correct output.""" + stream = cvcuda.Stream() + src = cvcuda.Tensor((1, 8, 8, 3), cvcuda.Type.U8, "NHWC") + with stream: + out = cvcuda.cvtcolor(src, cvcuda.ColorConversion.RGB2BGR) + stream.sync() + # RGB2BGR on a zero-initialised buffer is still all-zero; just verify it + # completes without error and the output shape is as expected. + assert out.shape == (1, 8, 8, 3) + + +# --------------------------------------------------------------------------- +# Path 2 — same-stream fast path +# --------------------------------------------------------------------------- + + +def test_same_stream_fast_path_does_not_raise(): + """Repeated submitStreamSync calls on the same stream must not raise.""" + stream = cvcuda.Stream() + tensor = cvcuda.Tensor((1, 4, 4, 1), cvcuda.Type.U8, "NHWC") + tensor.submitStreamSync(stream) + # All subsequent calls hit the fast path: prevHandle == stream.handle() + for _ in range(10): + tensor.submitStreamSync(stream) + + +def test_same_stream_fast_path_preserves_ordering(): + """Two ops on the same stream using the same resource must be ordered.""" + stream = cvcuda.Stream() + src = cvcuda.Tensor((1, 8, 8, 1), cvcuda.Type.U8, "NHWC") + with stream: + # Both ops share the same input tensor → both hit the fast path after + # the first submission. CUDA stream ordering guarantees the second + # op sees the output of the first. + mid = cvcuda.convertto(src, cvcuda.Type.F32, scale=1 / 255.0) + out = cvcuda.convertto(mid, cvcuda.Type.U8, scale=255.0) + stream.sync() + assert out.shape == src.shape + + +# --------------------------------------------------------------------------- +# Path 3 — cross-stream slow path +# --------------------------------------------------------------------------- + + +def test_cross_stream_sync_orders_work(): + """Work enqueued on stream A must be visible on stream B after submitSync. + + A buffer is first-bound to stream A, then filled on A after a long + busy-wait (so the fill is guaranteed to be in-flight when cvcuda attempts + to consume it). submitStreamSync moves the resource to stream B by + recording an event on A and inserting a wait on B. A subsequent read on B + must observe the fill, not an uninitialised buffer. + """ + stream_a = cvcuda.Stream() + stream_b = cvcuda.Stream() + + buf = cvcuda.Tensor((1, 8, 8, 3), cvcuda.Type.U8, "NHWC") + cp_buf = cupy.asarray(buf.cuda()) + + FILL_VALUE = 77 + + # First-bind resource to stream A (no CUDA work). + buf.submitStreamSync(stream_a) + + # Queue work on stream A: long busy-wait then fill. Both are in-flight + # when submitStreamSync(stream_b) is called from the host. + with _as_cupy_stream(stream_a): + _BUSY_WAIT((1,), (1,), (_WAIT_CYCLES,)) + cp_buf.fill(FILL_VALUE) + + # Transfer ownership to stream B: cudaEventRecord on A (captures the fill), + # cudaStreamWaitEvent on B. Stream B will not start until A's fill lands. + buf.submitStreamSync(stream_b) + + with _as_cupy_stream(stream_b): + result = cp_buf.copy() + + stream_b.sync() + + unique = sorted(set(result.flatten().tolist())) + assert (result == FILL_VALUE).all(), ( + f"cross-stream sync failed: expected all {FILL_VALUE}, got {unique}. " + "submitSync did not insert an event barrier between stream A and B." + ) + + +def test_cross_stream_multiple_hops(): + """Resource hopping across three streams must stay correctly ordered.""" + streams = [cvcuda.Stream() for _ in range(3)] + tensor = cvcuda.Tensor((1, 8, 8, 1), cvcuda.Type.U8, "NHWC") + + # First bind on stream 0 + with streams[0]: + cvcuda.convertto(tensor, cvcuda.Type.F32, scale=1 / 255.0) + + # Hop to stream 1, then stream 2 — each submitSync inserts a barrier. + tensor.submitStreamSync(streams[1]) + tensor.submitStreamSync(streams[2]) + + with streams[2]: + cvcuda.convertto(tensor, cvcuda.Type.F32, scale=1 / 255.0) + + streams[2].sync() diff --git a/tests/cvcuda/python/test_resourceguard.py b/tests/cvcuda/python/test_resourceguard.py new file mode 100644 index 000000000..f2c35cccb --- /dev/null +++ b/tests/cvcuda/python/test_resourceguard.py @@ -0,0 +1,259 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""Regression tests for ResourceGuard lifetime and exception safety. + +Failure scenarios are driven by the private ``cvcuda._test`` failure-injection +toggles, which make the corresponding C API callback fail through its +production error path. Every scenario runs in a subprocess so a crash on +unfixed code (SIGABRT from a throwing noexcept destructor) does not kill +pytest. +""" + +import subprocess +import sys +import textwrap + +_ABORT_REPRO_SCRIPT = textwrap.dedent( + """\ + import cvcuda + + try: + cvcuda._test.resourceguard_destructor_error() + except SystemError as exc: + # The destructor preserves the injected pending error instead of + # clearing it; CPython surfaces the non-raising binding that returned + # with an error set as a SystemError chaining the original. + assert isinstance(exc.__cause__, RuntimeError), repr(exc.__cause__) + assert "injected ResourceGuard commit failure" in str(exc.__cause__) + + print("PASS", flush=True) + """ +) + + +_POST_SUBMIT_HOLD_FAILURE_SCRIPT = textwrap.dedent( + """\ + import numpy as np + import cupy as cp + import cvcuda + + stream = cvcuda.Stream() + external_stream = cp.cuda.ExternalStream(stream.handle) + src = cvcuda.Tensor((1, 8, 8, 1), np.uint8, "NHWC") + dst = cvcuda.Tensor((1, 8, 8, 1), np.uint8, "NHWC") + + # Warm the operator and drain its resource-release callback so the only + # outstanding work below is the deliberately delayed submission. + cvcuda.flip_into(src=src, dst=dst, flipCode=0, stream=stream) + stream.sync() + cvcuda.internal.syncAuxStream() + + delay = cp.RawKernel( + r'''\\ + extern "C" __global__ void delay(unsigned long long cycles) + { + unsigned long long start = clock64(); + while (clock64() - start < cycles) + { + } + } + ''', + "delay", + ) + delay((1,), (1,), (np.uint64(500_000_000),), stream=external_stream) + assert not external_stream.done, "delay kernel completed before failure injection" + + # Fail the post-submission hold: flip's kernel is queued behind the delay + # kernel, then Stream_HoldResources rejects the lifetime hold. + cvcuda._test.fail_hold_resources(True) + error = None + try: + cvcuda.flip_into(src=src, dst=dst, flipCode=0, stream=stream) + except Exception as exc: + error = exc + finally: + cvcuda._test.fail_hold_resources(False) + + # Query before cleanup: ResourceGuard itself must have drained the stream + # while it still owned every resource reference. + drained = external_stream.done + stream.sync() + + assert isinstance(error, ValueError), repr(error) + assert "injected Stream_HoldResources failure" in str(error), str(error) + assert drained, "ResourceGuard released references while GPU work was active" + print("PASS", flush=True) + """ +) + + +_PRESYNC_FAILURE_SCRIPT = textwrap.dedent( + """\ + import sys + import numpy as np + import cvcuda + + stream = cvcuda.Stream() + src = cvcuda.Tensor((1, 8, 8, 1), np.uint8, "NHWC") + dst = cvcuda.Tensor((1, 8, 8, 1), np.uint8, "NHWC") + + # Fail the pre-submission sync; also arm the legacy combined callback so a + # destructor retry on unfixed code fails loudly instead of silently + # re-submitting the sync it already reported as failed. + cvcuda._test.fail_submit_sync_only(True) + cvcuda._test.fail_sync_and_hold(True) + refs_before = tuple(sys.getrefcount(obj) for obj in (stream, src, dst)) + errors = [] + try: + for _ in range(5): + try: + cvcuda.flip_into(src=src, dst=dst, flipCode=0, stream=stream) + except Exception as exc: + errors.append(exc) + finally: + cvcuda._test.fail_submit_sync_only(False) + cvcuda._test.fail_sync_and_hold(False) + + refs_after = tuple(sys.getrefcount(obj) for obj in (stream, src, dst)) + assert len(errors) == 5, errors + assert all(isinstance(error, ValueError) for error in errors), errors + assert all( + "injected Resources_SubmitSyncOnly failure" in str(error) for error in errors + ), errors + assert refs_after == refs_before, (refs_before, refs_after) + print("PASS", flush=True) + """ +) + + +_SUBMISSION_ERROR_WITH_CLEANUP_FAILURE_SCRIPT = textwrap.dedent( + """\ + import numpy as np + import cvcuda + + stream = cvcuda.Stream() + src = cvcuda.Tensor((1, 8, 8, 1), np.int16, "NHWC") + dst = cvcuda.Tensor((1, 8, 8, 1), np.int16, "NHWC") + + cvcuda._test.fail_hold_resources(True) + error = None + try: + # Flip rejects S16 inside the submission callable. ResourceGuard then + # encounters the injected secondary failure while finalizing the hold + # for any work the callable might have queued before throwing. + cvcuda.flip_into(src=src, dst=dst, flipCode=0, stream=stream) + except Exception as exc: + error = exc + finally: + cvcuda._test.fail_hold_resources(False) + + assert isinstance(error, RuntimeError), repr(error) + assert "INVALID_DATA_TYPE" in str(error), str(error) + print("PASS", flush=True) + """ +) + + +def _run_repro(script): + return subprocess.run( + [sys.executable, "-c", script], + capture_output=True, + text=True, + timeout=60, + ) + + +def test_resourceguard_destructor_no_abort_on_commit_error(): + """ResourceGuard destructor must not call std::terminate() when commit() raises. + + Regression: the destructor was implicitly noexcept; CheckCAPIError() inside + commit() could throw pybind11::error_already_set, which caused + std::terminate() (SIGABRT) rather than a recoverable Python exception. + + The internal test hook constructs a real ResourceGuard and sets a Python + exception immediately before scope exit. The destructor must neither + terminate nor discard that error: it is fetched before finalization and + re-instated afterwards, so it propagates to the caller. + + Runs in a subprocess so a crash on unfixed code does not kill pytest. + """ + result = _run_repro(_ABORT_REPRO_SCRIPT) + assert result.returncode == 0 and "PASS" in result.stdout, ( + f"Process aborted or did not reach PASS " + f"(returncode={result.returncode}).\n" + f"stdout: {result.stdout[:1000]}\n" + f"stderr: {result.stderr[:2000]}" + ) + + +def test_resourceguard_post_submit_hold_failure_drains_and_propagates(): + """A failed post-submit hold must drain before references are released. + + The hold is what keeps resources alive until the submitted kernel + completes. When it fails, releasing the references immediately would free + GPU memory the kernel may still be reading or writing; the guard must + first prove completion by draining the stream, and the failure must reach + Python instead of being swallowed by the destructor. + """ + result = _run_repro(_POST_SUBMIT_HOLD_FAILURE_SCRIPT) + assert ( + result.returncode == 0 + and "PASS" in result.stdout + and "~ResourceGuard" not in result.stderr + ), ( + f"Post-submit recovery failed (returncode={result.returncode}).\n" + f"stdout: {result.stdout[:1000]}\n" + f"stderr: {result.stderr[:2000]}" + ) + + +def test_resourceguard_presync_failure_is_not_retried(): + """A failed pre-submit synchronization must not be retried in teardown. + + When run() fails before the submission callable executes, no work was + queued on behalf of the guard, so there is nothing to hold. The destructor + must not fall back to the legacy sync-and-hold call (a retry of the exact + operation that just failed), and no resource references may leak. + """ + result = _run_repro(_PRESYNC_FAILURE_SCRIPT) + assert ( + result.returncode == 0 + and "PASS" in result.stdout + and "~ResourceGuard" not in result.stderr + ), ( + f"Pre-submit failure was retried (returncode={result.returncode}).\n" + f"stdout: {result.stdout[:1000]}\n" + f"stderr: {result.stderr[:2000]}" + ) + + +def test_resourceguard_submission_error_remains_primary_during_cleanup_failure(): + """A hold failure during unwind must not replace the submission error. + + The callable's own exception is what the user needs to see; the secondary + cleanup failure is logged to stderr by the destructor instead. + """ + result = _run_repro(_SUBMISSION_ERROR_WITH_CLEANUP_FAILURE_SCRIPT) + assert ( + result.returncode == 0 + and "PASS" in result.stdout + and "~ResourceGuard: commit() threw:" in result.stderr + and "injected Stream_HoldResources failure" in result.stderr + ), ( + f"Cleanup replaced the submission error (returncode={result.returncode}).\n" + f"stdout: {result.stdout[:1000]}\n" + f"stderr: {result.stderr[:2000]}" + ) diff --git a/tests/cvcuda/python/test_samples_common.py b/tests/cvcuda/python/test_samples_common.py new file mode 100644 index 000000000..4bb6bcf1e --- /dev/null +++ b/tests/cvcuda/python/test_samples_common.py @@ -0,0 +1,129 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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 importlib.util +import runpy +import sys +from pathlib import Path +from types import ModuleType + +import pytest + + +class _NetworkCreated(Exception): + pass + + +def _find_sample(relative_path): + install_or_source_root = Path(__file__).resolve().parents[3] + candidates = ( + install_or_source_root / "samples" / relative_path, + install_or_source_root / "bin" / relative_path, + ) + sample_path = next((path for path in candidates if path.is_file()), None) + if sample_path is None: + pytest.skip(f"{relative_path} is not available in this test installation") + return sample_path + + +def _load_samples_common(monkeypatch): + common_path = _find_sample("common.py") + + cvcuda = ModuleType("cvcuda") + cuda = ModuleType("cuda") + cuda.__path__ = [] + cuda_bindings = ModuleType("cuda.bindings") + cuda_bindings.__path__ = [] + cudart = ModuleType("cuda.bindings.runtime") + cuda.bindings = cuda_bindings + cuda_bindings.runtime = cudart + nvidia = ModuleType("nvidia") + nvidia.__path__ = [] + nvimgcodec = ModuleType("nvidia.nvimgcodec") + nvidia.nvimgcodec = nvimgcodec + + for name, module in ( + ("cvcuda", cvcuda), + ("cuda", cuda), + ("cuda.bindings", cuda_bindings), + ("cuda.bindings.runtime", cudart), + ("nvidia", nvidia), + ("nvidia.nvimgcodec", nvimgcodec), + ): + monkeypatch.setitem(sys.modules, name, module) + + spec = importlib.util.spec_from_file_location( + "_samples_common_under_test", common_path + ) + assert spec is not None + assert spec.loader is not None + common = importlib.util.module_from_spec(spec) + spec.loader.exec_module(common) + return common + + +def test_engine_from_onnx_does_not_use_removed_explicit_batch_flag(monkeypatch): + common = _load_samples_common(monkeypatch) + + class FakeLogger: + WARNING = object() + + def __init__(self, severity): + self.severity = severity + + class FakeBuilder: + def __init__(self): + self.network_args = None + + def create_network(self, *args): + self.network_args = args + raise _NetworkCreated + + builder = FakeBuilder() + tensorrt = ModuleType("tensorrt") + tensorrt.init_libnvinfer_plugins = lambda *_args: None + tensorrt.Logger = FakeLogger + tensorrt.Builder = lambda _logger: builder + monkeypatch.setitem(sys.modules, "tensorrt", tensorrt) + + with pytest.raises(_NetworkCreated): + common.engine_from_onnx(Path("unused.onnx"), Path("unused.engine")) + + assert builder.network_args == () + + +def test_pynvvideocodec_sample_skips_missing_dependency_on_python314( + monkeypatch, capsys +): + sample_path = _find_sample("interoperability/pynvvideocodec_interop.py") + monkeypatch.setitem(sys.modules, "cvcuda", ModuleType("cvcuda")) + monkeypatch.setitem(sys.modules, "PyNvVideoCodec", None) + monkeypatch.setattr(sys, "version_info", (3, 14, 0, "final", 0)) + + runpy.run_path(str(sample_path), run_name="__main__") + + assert "Skipping PyNvVideoCodec interoperability sample" in capsys.readouterr().out + + +def test_pynvvideocodec_sample_requires_dependency_on_supported_python(monkeypatch): + sample_path = _find_sample("interoperability/pynvvideocodec_interop.py") + monkeypatch.setitem(sys.modules, "cvcuda", ModuleType("cvcuda")) + monkeypatch.setitem(sys.modules, "PyNvVideoCodec", None) + monkeypatch.setattr(sys, "version_info", (3, 12, 0, "final", 0)) + + with pytest.raises(ModuleNotFoundError) as error: + runpy.run_path(str(sample_path), run_name="__main__") + + assert error.value.name == "PyNvVideoCodec" diff --git a/tests/cvcuda/python/test_stream.py b/tests/cvcuda/python/test_stream.py index 507d0433c..1d8abb526 100644 --- a/tests/cvcuda/python/test_stream.py +++ b/tests/cvcuda/python/test_stream.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,18 +13,25 @@ # See the License for the specific language governing permissions and # limitations under the License. -import torch -import cvcuda import ctypes + +import numpy as np import pytest as t -import platform -from packaging import version + +import cvcuda +import cupy def test_stream_gcbag_vs_streamsync_race_condition(): - inputImage = torch.randint(0, 256, (100, 1500, 1500, 3), dtype=torch.uint8).cuda() + inputImage = cupy.asarray( + np.random.randint(0, 256, (100, 1500, 1500, 3), dtype=np.uint8) + ) cvcudaInputTensor = cvcuda.as_tensor(inputImage, "NHWC") - inputmap = torch.randint(0, 256, (100, 1500, 1500, 2), dtype=torch.float).cuda() + inputmap = cupy.asarray( + np.random.randint(0, 256, (100, 1500, 1500, 2), dtype=np.uint8).astype( + np.float32 + ) + ) cvcudaInputMap = cvcuda.as_tensor(inputmap, "NHWC") cvcuda_stream = cvcuda.Stream() @@ -35,7 +42,7 @@ def test_stream_gcbag_vs_streamsync_race_condition(): def test_current_stream(): assert cvcuda.Stream.current is cvcuda.Stream.default - assert type(cvcuda.Stream.current) == cvcuda.Stream + assert type(cvcuda.Stream.current) is cvcuda.Stream def test_user_stream(): @@ -62,9 +69,9 @@ def test_nested_streams(): def test_wrap_stream_voidp(): - stream = torch.cuda.Stream() + stream = cupy.cuda.Stream() - extStream = ctypes.c_void_p(stream.cuda_stream) + extStream = ctypes.c_void_p(stream.ptr) cvcudaStream = cvcuda.as_stream(extStream) @@ -72,9 +79,9 @@ def test_wrap_stream_voidp(): def test_wrap_stream_int(): - stream = torch.cuda.Stream() + stream = cupy.cuda.Stream() - extStream = int(stream.cuda_stream) + extStream = int(stream.ptr) cvcudaStream = cvcuda.as_stream(extStream) @@ -87,15 +94,15 @@ def test_stream_conv_to_int(): assert stream.handle == int(stream) -class TorchStream: +class MockStream: def __init__(self, cuda_stream=None): if cuda_stream: - self.m_stream = torch.cuda.ExternalStream(cuda_stream) + self.m_stream = cupy.cuda.ExternalStream(cuda_stream) else: - self.m_stream = torch.cuda.Stream() + self.m_stream = cupy.cuda.Stream() def cuda_stream(self): - return self.m_stream.cuda_stream + return self.m_stream.ptr def stream(self): return self.m_stream @@ -104,32 +111,98 @@ def stream(self): @t.mark.parametrize( "stream_type", [ - TorchStream, + MockStream, ], ) -@t.mark.skipif( - ( - platform.machine() == "aarch64" - and version.parse(torch.__version__) < version.parse("2.0.0") - ), - reason="Test not supported on ARM64 with PyTorch versions < 2.0.0", -) def test_wrap_stream_external(stream_type): extstream = stream_type() - stream = cvcuda.as_stream(extstream.stream()) + # Keep the underlying cupy stream alive across the del below. + # cupy.cuda.Stream eagerly destroys the CUDA stream in __del__, + # so we must prevent GC from reclaiming it. + underlying = extstream.stream() + + stream = cvcuda.as_stream(underlying.ptr) assert extstream.cuda_stream() == stream.handle - # stream must hold a ref to the external stream, the wrapped cudaStream - # must not have been deleted del extstream extstream = stream_type(stream.handle) - stream = cvcuda.as_stream(extstream.stream()) + stream = cvcuda.as_stream(extstream.stream().ptr) assert extstream.cuda_stream() == stream.handle + del underlying + + +def test_as_stream_cupy_object(): + """cvcuda.as_stream() must accept a cupy.cuda.Stream object directly, not just + an integer handle. Without a dedicated type_caster this raises TypeError.""" + stream = cupy.cuda.Stream() + cvcuda_stream = cvcuda.as_stream(stream) + assert cvcuda_stream.handle == stream.ptr + + +def test_as_stream_cupy_object_keeps_stream_alive(): + """When wrapping a cupy stream *by object*, cvcuda must keep the stream alive + for as long as the cvcuda wrapper exists. + If the wrapper stores only the integer (m_wrappedObj = int), the cupy stream + is destroyed the moment the caller drops their reference, leaving a dead handle.""" + import gc + + cupy_stream = cupy.cuda.Stream() + handle = cupy_stream.ptr + + cvcuda_stream = cvcuda.as_stream(cupy_stream) + + # Drop caller's reference to the cupy stream. + del cupy_stream + gc.collect() + + # cvcuda_stream must still hold the cupy stream alive via m_wrappedObj. + # If the stream was destroyed, streamSynchronize will raise. + cupy.cuda.runtime.streamSynchronize(cvcuda_stream.handle) + assert cvcuda_stream.handle == handle + + +def test_as_stream_cupy_stream_switch(): + """A resource submitted on a cupy stream (via as_stream(cupy_stream)) can be + safely used on a different stream even after the caller drops their cupy reference. + + as_stream(cupy_stream) keeps the cupy stream alive via m_wrappedObj, so the + CUDA handle remains valid when submitSync synchronizes against it. + The chain is: out_nv -> Resource -> m_lastStream -> cvcuda Stream -> cupy_stream. + """ + import gc + + src = cupy.full((1, 4, 4, 3), fill_value=100, dtype=cupy.uint8) + src_nv = cvcuda.as_tensor(src, "NHWC") + + # Wrap by object (not .ptr) so cvcuda holds a strong ref to the cupy stream. + cupy_stream = cupy.cuda.Stream() + cvcuda_stream = cvcuda.as_stream(cupy_stream) + with cvcuda_stream: + out_nv = cvcuda.cvtcolor( + src_nv, cvcuda.ColorConversion.BGR2GRAY, stream=cvcuda_stream + ) + cupy_stream.synchronize() + + # Drop caller's references. The cupy stream stays alive via the ref chain above. + del cupy_stream + del cvcuda_stream + gc.collect() + + # Use out_nv on a fresh native stream. submitSync synchronizes against the + # still-valid cupy stream handle held in m_lastStream. + stream2 = cvcuda.Stream() + with stream2: + out2 = cvcuda.cvtcolor(out_nv, cvcuda.ColorConversion.GRAY2BGR, stream=stream2) + stream2.sync() + + result = cupy.asarray(out2.cuda()) + assert result.shape == (1, 4, 4, 3) + def test_stream_default_is_zero(): assert cvcuda.Stream.default.handle == 0 diff --git a/tests/cvcuda/python/test_tensor.py b/tests/cvcuda/python/test_tensor.py index e9db2d8f9..c2abfd755 100644 --- a/tests/cvcuda/python/test_tensor.py +++ b/tests/cvcuda/python/test_tensor.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,10 +13,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -import torch -import cvcuda -import pytest as t import numpy as np +import pytest as t + +import cvcuda +import cupy @t.mark.parametrize( @@ -82,7 +83,7 @@ def test_tensor_creation_shape_works(shape, dtype, layout): assert tensor.ndim == len(shape) -params_wrap_torch = [ +params_wrap_cuda_buffer = [ ((3, 5, 7, 1), np.uint8), ((3, 5, 7, 1), np.int8), ((3, 5, 7, 1), np.int16), @@ -97,30 +98,118 @@ def test_tensor_creation_shape_works(shape, dtype, layout): ] -@t.mark.parametrize("shape,dtype", params_wrap_torch) -def test_wrap_torch_buffer(shape, dtype): - tensor = cvcuda.as_tensor( - torch.as_tensor(np.ndarray(shape, dtype=dtype), device="cuda") - ) +@t.mark.parametrize("shape,dtype", params_wrap_cuda_buffer) +def test_wrap_cuda_buffer(shape, dtype): + tensor = cvcuda.as_tensor(cupy.asarray(np.ndarray(shape, dtype=dtype))) assert tensor.shape == shape assert tensor.dtype == dtype assert tensor.layout is None assert tensor.ndim == len(shape) -@t.mark.parametrize("shape,dtype", params_wrap_torch) -def test_wrap_torch_buffer_dlpack(shape, dtype): - ttensor = torch.as_tensor(np.ndarray(shape, dtype=dtype), device="cuda") +def _make_dlpack_capsule(cupy_array): + """Build a DLPack v0 PyCapsule from a cupy array using ctypes. - # Since cvcuda.as_tensor can understand both dlpack and cuda_array_interface, - # and we don't know a priori which interfaces it'll use (torch provides both), - # let's create one object with only the dlpack interface. - class DLPackObject: + This replicates the capsule format that cvcuda's DLPack consumer expects, + bypassing cupy's v1.0 __dlpack__ protocol which triggers an abort in + cvcuda's C++ consumer for certain dtypes. + """ + import ctypes + + kDLCUDA = 2 + kDLInt, kDLUInt, kDLFloat, kDLComplex = 0, 1, 2, 5 + + dtype = cupy_array.dtype + if dtype.kind == "u": + code = kDLUInt + elif dtype.kind == "i": + code = kDLInt + elif dtype.kind == "f": + code = kDLFloat + elif dtype.kind == "c": + code = kDLComplex + else: + raise TypeError(f"Unsupported dtype: {dtype}") + + class _DLDevice(ctypes.Structure): + _fields_ = [("device_type", ctypes.c_int), ("device_id", ctypes.c_int)] + + class _DLDataType(ctypes.Structure): + _fields_ = [ + ("code", ctypes.c_uint8), + ("bits", ctypes.c_uint8), + ("lanes", ctypes.c_uint16), + ] + + class _DLTensor(ctypes.Structure): + _fields_ = [ + ("data", ctypes.c_void_p), + ("device", _DLDevice), + ("ndim", ctypes.c_int), + ("dtype", _DLDataType), + ("shape", ctypes.POINTER(ctypes.c_int64)), + ("strides", ctypes.POINTER(ctypes.c_int64)), + ("byte_offset", ctypes.c_uint64), + ] + + class _DLManagedTensor(ctypes.Structure): pass - o = DLPackObject() - o.__dlpack__ = ttensor.__dlpack__ - o.__dlpack_device__ = ttensor.__dlpack_device__ + _DELETER = ctypes.CFUNCTYPE(None, ctypes.POINTER(_DLManagedTensor)) + _DLManagedTensor._fields_ = [ + ("dl_tensor", _DLTensor), + ("manager_ctx", ctypes.c_void_p), + ("deleter", _DELETER), + ] + + ndim = cupy_array.ndim + shape_arr = (ctypes.c_int64 * ndim)(*cupy_array.shape) + strides_list = [1] + for i in range(ndim - 1, 0, -1): + strides_list.insert(0, strides_list[0] * cupy_array.shape[i]) + strides_arr = (ctypes.c_int64 * ndim)(*strides_list) + + mt = _DLManagedTensor() + mt.dl_tensor.data = ctypes.c_void_p(cupy_array.data.ptr) + mt.dl_tensor.device = _DLDevice(kDLCUDA, cupy_array.device.id) + mt.dl_tensor.ndim = ndim + mt.dl_tensor.dtype = _DLDataType(code, dtype.itemsize * 8, 1) + mt.dl_tensor.shape = ctypes.cast(shape_arr, ctypes.POINTER(ctypes.c_int64)) + mt.dl_tensor.strides = ctypes.cast(strides_arr, ctypes.POINTER(ctypes.c_int64)) + mt.dl_tensor.byte_offset = 0 + mt.manager_ctx = None + mt.deleter = _DELETER(0) + + PyCapsule_New = ctypes.pythonapi.PyCapsule_New + PyCapsule_New.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_void_p] + PyCapsule_New.restype = ctypes.py_object + + capsule = PyCapsule_New(ctypes.addressof(mt), b"dltensor", None) + + # Return the capsule and the prevent-GC refs (caller must keep them alive) + return capsule, (mt, shape_arr, strides_arr, cupy_array) + + +@t.mark.parametrize("shape,dtype", params_wrap_cuda_buffer) +def test_wrap_cuda_buffer_dlpack(shape, dtype): + cuda_buffer = cupy.asarray(np.ndarray(shape, dtype=dtype)) + + # Create an object with only __dlpack__ (no __cuda_array_interface__) + # to force cvcuda.as_tensor to use the DLPack path. + class DLPackObject: + def __init__(self, src): + self._src = src + self._prevent_gc = None + + def __dlpack__(self, *args, **kwargs): + capsule, refs = _make_dlpack_capsule(self._src) + self._prevent_gc = refs + return capsule + + def __dlpack_device__(self): + return (2, self._src.device.id) # kDLCUDA + + o = DLPackObject(cuda_buffer) tensor = cvcuda.as_tensor(o) assert tensor.shape == shape @@ -129,18 +218,42 @@ class DLPackObject: assert tensor.ndim == len(shape) -@t.mark.parametrize("shape,dtype", params_wrap_torch) -def test_wrap_torch_buffer_cuda_array_interface(shape, dtype): - ttensor = torch.as_tensor(np.ndarray(shape, dtype=dtype), device="cuda") +@t.mark.parametrize("shape,dtype", params_wrap_cuda_buffer) +def test_wrap_cuda_buffer_dlpack_v1(shape, dtype): + """Test consuming DLPack v1.0 capsules from cupy's native __dlpack__.""" + cuda_buffer = cupy.asarray(np.ndarray(shape, dtype=dtype)) + + class DLPackV1Object: + def __init__(self, src): + self._src = src + + def __dlpack__(self, *args, **kwargs): + return self._src.__dlpack__(*args, **kwargs) + + def __dlpack_device__(self): + return self._src.__dlpack_device__() + + o = DLPackV1Object(cuda_buffer) + + tensor = cvcuda.as_tensor(o) + assert tensor.shape == shape + assert tensor.dtype == dtype + assert tensor.layout is None + assert tensor.ndim == len(shape) + + +@t.mark.parametrize("shape,dtype", params_wrap_cuda_buffer) +def test_wrap_cuda_buffer_cuda_array_interface(shape, dtype): + cuda_buffer = cupy.asarray(np.ndarray(shape, dtype=dtype)) # Since cvcuda.as_tensor can understand both dlpack and cuda_array_interface, - # and we don't know a priori which interfaces it'll use (torch provides both), + # and we don't know a priori which interfaces it'll use (some CUDA libraries provide both), # let's create one object with only the cuda_array_interface. class CudaArrayInterfaceObject: pass o = CudaArrayInterfaceObject() - o.__cuda_array_interface__ = ttensor.__cuda_array_interface__ + o.__cuda_array_interface__ = cuda_buffer.__cuda_array_interface__ tensor = cvcuda.as_tensor(o) assert tensor.shape == shape @@ -160,10 +273,8 @@ class CudaArrayInterfaceObject: ((5,), np.uint8, "W"), ], ) -def test_wrap_torch_buffer_with_layout(shape, dtype, layout): - tensor = cvcuda.as_tensor( - torch.as_tensor(np.ndarray(shape, dtype=dtype), device="cuda"), layout - ) +def test_wrap_cuda_buffer_with_layout(shape, dtype, layout): + tensor = cvcuda.as_tensor(cupy.asarray(np.ndarray(shape, dtype=dtype)), layout) assert tensor.shape == shape assert tensor.shape == shape assert tensor.dtype == dtype @@ -214,10 +325,10 @@ def test_tensor_wrap_image_works(size, fmt, gold_layout, gold_shape, gold_dtype) export_cuda_buffer_params, ) def test_tensor_export_cuda_buffer(shape, dtype): - rng = np.random.default_rng() + rng = np.random.default_rng(0) hostGold = rng.integers(0, 128, shape, dtype) - devGold = torch.as_tensor(hostGold, device="cuda") + devGold = cupy.asarray(hostGold) tensor = cvcuda.as_tensor(devGold) @@ -225,7 +336,8 @@ def test_tensor_export_cuda_buffer(shape, dtype): assert devMem.dtype == dtype assert devMem.shape == shape - assert (hostGold == torch.as_tensor(devMem).cpu().numpy()).all() + devMemWrapped = cupy.asarray(devMem) + assert (hostGold == devMemWrapped.get()).all() @t.mark.parametrize( @@ -233,10 +345,10 @@ def test_tensor_export_cuda_buffer(shape, dtype): export_cuda_buffer_params, ) def test_tensor_export_cuda_buffer_dlpack(shape, dtype): - rng = np.random.default_rng() + rng = np.random.default_rng(0) hostGold = rng.integers(0, 128, shape, dtype) - devGold = torch.as_tensor(hostGold, device="cuda") + devGold = cupy.asarray(hostGold) tensor = cvcuda.as_tensor(devGold) @@ -244,22 +356,51 @@ def test_tensor_export_cuda_buffer_dlpack(shape, dtype): assert devMem.dtype == dtype assert devMem.shape == shape - assert (hostGold == torch.from_dlpack(devMem).cpu().numpy()).all() + # Use from_dlpack to import the DLPack tensor + devMemWrapped = cupy.from_dlpack(devMem) + assert (hostGold == devMemWrapped.get()).all() + + +@t.mark.parametrize( + "shape,dtype", + export_cuda_buffer_params, +) +def test_tensor_export_cuda_buffer_dlpack_v0(shape, dtype): + """Test that cvcuda produces a v0 'dltensor' capsule when max_version is not passed.""" + import ctypes + + rng = np.random.default_rng(0) + hostGold = rng.integers(0, 128, shape, dtype) + + devGold = cupy.asarray(hostGold) + tensor = cvcuda.as_tensor(devGold) + devMem = tensor.cuda() + + # Call __dlpack__ without max_version to get a legacy v0 capsule + capsule = devMem.__dlpack__() + + # Verify it's a v0 capsule named "dltensor" + PyCapsule_IsValid = ctypes.pythonapi.PyCapsule_IsValid + PyCapsule_IsValid.argtypes = [ctypes.py_object, ctypes.c_char_p] + PyCapsule_IsValid.restype = ctypes.c_int + assert PyCapsule_IsValid(capsule, b"dltensor") or PyCapsule_IsValid( + capsule, b"used_dltensor" + ) def test_tensor_hold_reference_of_wrapped_buffer(): - ttensor = torch.as_tensor(np.ndarray([10], np.int8), device="cuda") - ptr0 = ttensor.data_ptr() + cuda_buffer = cupy.asarray(np.ndarray([10], np.int8)) + ptr0 = cuda_buffer.data.ptr - cvtensor = cvcuda.as_tensor(ttensor) # noqa: F841 assigned but never used + cvtensor = cvcuda.as_tensor(cuda_buffer) # noqa: F841 assigned but never used - del ttensor # cvtensor must have held ttensor object + del cuda_buffer # cvtensor must have held cuda_buffer object - ttensor = torch.as_tensor(np.ndarray([10], np.int8), device="cuda") + cuda_buffer = cupy.asarray(np.ndarray([10], np.int8)) - # since "cvtensor" must have held the reference to the first "ttensor", - # the second "ttensor" must be a different buffer - assert ptr0 != ttensor.data_ptr() + # since "cvtensor" must have held the reference to the first "cuda_buffer", + # the second "cuda_buffer" must be a different buffer + assert ptr0 != cuda_buffer.data.ptr def test_tensor_is_kept_alive_by_cuda_array_interface(): @@ -443,7 +584,7 @@ def test_tensor_size_in_bytes(): assert cvcuda.internal.nbytes_in_cache(tensor_create) > 0 tensor_wrap = cvcuda.as_tensor( - torch.as_tensor(np.ndarray((5, 16, 32, 4), dtype=np.float32), device="cuda") + cupy.asarray(np.ndarray((5, 16, 32, 4), dtype=np.float32)) ) assert cvcuda.internal.nbytes_in_cache(tensor_wrap) == 0 diff --git a/tests/cvcuda/python/test_tensor_batch.py b/tests/cvcuda/python/test_tensor_batch.py index b7445b441..77fc34d86 100644 --- a/tests/cvcuda/python/test_tensor_batch.py +++ b/tests/cvcuda/python/test_tensor_batch.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,28 +13,27 @@ # See the License for the specific language governing permissions and # limitations under the License. -import cvcuda -import pytest as t +import re + import numpy as np +import pytest as t + +import cvcuda import cvcuda_util as util -import torch -import re +import cupy def rand_shape(rank, low=1, high=10): - return np.random.randint(low=1, high=10, size=rank) + return np.random.randint(low=low, high=high, size=rank) -def rand_torch_tensor(dtype, rank): - return torch.as_tensor( - np.random.random(size=rand_shape(rank)).astype(dtype), device="cuda" - ) +def rand_cuda_buffer(dtype, rank): + return cupy.asarray(np.random.random(size=rand_shape(rank)).astype(dtype)) def random_tensors(n, dtype, rank, layout): return [ - cvcuda.as_tensor(rand_torch_tensor(dtype, rank), layout=layout) - for _ in range(n) + cvcuda.as_tensor(rand_cuda_buffer(dtype, rank), layout=layout) for _ in range(n) ] @@ -164,8 +163,8 @@ def test_tensorbatch_wrap_buffers(): assert batch.layout is None assert batch.ndim == 3 - # from torch tensor, with layout - buffers = [rand_torch_tensor(np.int16, 4) for i in range(5)] + # from CUDA buffer, with layout + buffers = [rand_cuda_buffer(np.int16, 4) for i in range(5)] batch = cvcuda.as_tensors(buffers, layout="NHWC") assert batch.capacity == len(buffers) assert len(batch) == len(buffers) @@ -179,7 +178,7 @@ def test_tensorbatch_wrap_buffers(): match="NVCV_ERROR_INVALID_ARGUMENT: " "Trying to add a tensor to a tensor batch with an inconsistent rank.", ): - buffers = [rand_torch_tensor(np.int16, 3), rand_torch_tensor(np.int16, 4)] + buffers = [rand_cuda_buffer(np.int16, 3), rand_cuda_buffer(np.int16, 4)] cvcuda.as_tensors(buffers) # mismatching dtype @@ -188,7 +187,7 @@ def test_tensorbatch_wrap_buffers(): match="NVCV_ERROR_INVALID_ARGUMENT: " "Trying to add a tensor to a tensor batch with an inconsistent type.", ): - buffers = [rand_torch_tensor(np.int16, 3), rand_torch_tensor(np.int32, 3)] + buffers = [rand_cuda_buffer(np.int16, 3), rand_cuda_buffer(np.int32, 3)] cvcuda.as_tensors(buffers) # invalid types @@ -236,6 +235,6 @@ def test_tensorbatch_size_in_bytes(): batch_create = cvcuda.TensorBatch(10) assert cvcuda.internal.nbytes_in_cache(batch_create) > 0 - pt_img = torch.as_tensor(np.ndarray((16, 32, 4), dtype=np.float32), device="cuda") + pt_img = cupy.asarray(np.ndarray((16, 32, 4), dtype=np.float32)) batch_as_tensors = cvcuda.as_tensors([pt_img]) assert cvcuda.internal.nbytes_in_cache(batch_as_tensors) > 0 diff --git a/tests/cvcuda/python/test_tensorlayout.py b/tests/cvcuda/python/test_tensorlayout.py new file mode 100644 index 000000000..837c03cda --- /dev/null +++ b/tests/cvcuda/python/test_tensorlayout.py @@ -0,0 +1,200 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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 itertools + +import pytest as t + +import cvcuda + +# The six single-dimension labels. All must be exposed as named constants so +# users never need to reach for strings for the atomic layouts. +SINGLE_LABELS = ["N", "C", "F", "D", "H", "W"] + +# Every predefined multi-dimension layout, in the declaration order of +# src/nvcv/src/include/nvcv/TensorLayoutDef.inc. These lists are the single +# source of truth for the parametrized tests below; test_exposed_constants_match +# keeps them in lockstep with the binding, so any layout added to (or dropped +# from) the .inc forces a matching update here. +MULTI_LABELS = [ + "WC", "CW", + "NW", "NWC", "NCW", + "HW", "NHW", + "FHW", "NFHW", + "CHW", "NCHW", "HWC", "NHWC", + "CFHW", "FCHW", "FHWC", "NCFHW", "NFCHW", "NFHWC", + "DHW", "NDHW", + "CDHW", "DHWC", "NCDHW", "NDHWC", + "FDHW", "NFDHW", + "CFDHW", "FCDHW", "FDHWC", "NCFDHW", "NFCDHW", "NFDHWC", +] # fmt: skip + +# Every predefined named layout constant (excludes NONE, which is the empty +# layout and is exercised separately). +ALL_LABELS = SINGLE_LABELS + MULTI_LABELS + + +def test_exposed_constants_match_expected_set(): + # Exhaustiveness guard: the named layout constants the binding actually + # exposes must be exactly ALL_LABELS plus NONE. This fails loudly if a + # layout is ever added to TensorLayoutDef.inc without being covered here, or + # if one silently stops being exposed to Python. + exposed = { + name + for name in dir(cvcuda.TensorLayout) + if isinstance(getattr(cvcuda.TensorLayout, name), cvcuda.TensorLayout) + } + assert exposed == set(ALL_LABELS) | {"NONE"} + + +@t.mark.parametrize("label", SINGLE_LABELS) +def test_single_dim_labels_are_exposed(label): + # Each atomic label must be exposed as a named constant that is value-equal + # to its string form and round-trips through str(). + layout = getattr(cvcuda.TensorLayout, label) + assert layout == cvcuda.TensorLayout(label) + assert str(layout) == label + + +@t.mark.parametrize("label", ALL_LABELS) +def test_named_constant_matches_string_constructor(label): + assert getattr(cvcuda.TensorLayout, label) == cvcuda.TensorLayout(label) + assert str(getattr(cvcuda.TensorLayout, label)) == label + + +@t.mark.parametrize("label", ALL_LABELS) +def test_string_round_trip(label): + # A layout survives a full label -> TensorLayout -> str -> TensorLayout trip, + # from both the named constant and the string constructor. + named = getattr(cvcuda.TensorLayout, label) + built = cvcuda.TensorLayout(label) + assert cvcuda.TensorLayout(str(named)) == named + assert cvcuda.TensorLayout(str(built)) == built + assert str(named) == str(built) == label + + +@t.mark.parametrize("label", ALL_LABELS) +def test_hash_is_value_based(label): + # Two layouts that compare equal must hash equal, regardless of whether they + # came from a named constant or a string. + named = getattr(cvcuda.TensorLayout, label) + built = cvcuda.TensorLayout(label) + assert named == built + assert hash(named) == hash(built) + + +@t.mark.parametrize("label", ALL_LABELS) +def test_layout_equals_matching_string(label): + # A string implicitly converts to a TensorLayout, so equality works directly + # against the string form in either operand order. + layout = getattr(cvcuda.TensorLayout, label) + assert layout == label + assert label == layout + assert not (layout != label) + assert not (label != layout) + + +@t.mark.parametrize("label", ALL_LABELS) +def test_layout_not_equal_to_other_string(label): + # Comparing against the string of a *different* layout must be unequal in + # either operand order. "NCHW" is the reference; use "HWC" for it so the + # counterpart is never the label itself. + layout = getattr(cvcuda.TensorLayout, label) + other = "HWC" if label == "NCHW" else "NCHW" + assert layout != other + assert other != layout + assert not (layout == other) + assert not (other == layout) + + +@t.mark.parametrize("other", [123, 3.14, None, ("N", "H", "W", "C"), object()]) +def test_equality_is_total_against_foreign_types(other): + # Comparing against a non-string, non-layout object must return False (not + # raise) so TensorLayout is safe to use in heterogeneous containers. + assert not (cvcuda.TensorLayout.NHWC == other) + assert cvcuda.TensorLayout.NHWC != other + + +@t.mark.parametrize("bad", ["", "ZZZ", "nhwc"]) +def test_equality_against_unrelated_strings(bad): + # Empty, unknown, and wrong-case strings are all valid TensorLayout inputs + # (labels are case-sensitive), so they simply compare unequal to NHWC. + assert cvcuda.TensorLayout.NHWC != bad + assert not (cvcuda.TensorLayout.NHWC == bad) + + +def test_all_layouts_are_distinct(): + # No two distinct predefined layouts may compare equal or collapse together + # in a set (value-based hashing must keep them apart). + layouts = [getattr(cvcuda.TensorLayout, label) for label in ALL_LABELS] + assert len(set(layouts)) == len(ALL_LABELS) + for a, b in itertools.combinations(layouts, 2): + assert a != b + assert not (a == b) + + +def test_none_layout_is_exposed(): + assert cvcuda.TensorLayout.NONE == cvcuda.TensorLayout("") + assert cvcuda.TensorLayout.NONE == "" + assert cvcuda.TensorLayout.NONE != cvcuda.TensorLayout.N + assert cvcuda.TensorLayout.NONE != "N" + # NONE participates in value-based hashing like any other layout. + assert hash(cvcuda.TensorLayout.NONE) == hash(cvcuda.TensorLayout("")) + assert len({cvcuda.TensorLayout.NONE, cvcuda.TensorLayout("")}) == 1 + + +def test_usable_as_dict_key(): + d = {cvcuda.TensorLayout.NCHW: "planar", cvcuda.TensorLayout.NHWC: "packed"} + # Look up with a freshly-constructed, value-equal key. + assert d[cvcuda.TensorLayout("NCHW")] == "planar" + assert d[cvcuda.TensorLayout("NHWC")] == "packed" + + +def test_usable_in_set(): + # Value-equal layouts collapse to a single set element. + assert len({cvcuda.TensorLayout("NHWC"), cvcuda.TensorLayout.NHWC}) == 1 + assert len({cvcuda.TensorLayout.NCHW, cvcuda.TensorLayout.NHWC}) == 2 + + +@t.mark.parametrize( + "shape, layout", + [ + ((10,), cvcuda.TensorLayout.N), + ((10,), cvcuda.TensorLayout.C), + ((5, 16, 32, 4), cvcuda.TensorLayout.NHWC), + ((16, 32, 4), cvcuda.TensorLayout.HWC), + ], +) +def test_tensor_construction_accepts_layout_enum(shape, layout): + # The exact case from the request: passing TensorLayout.N (rather than "N") + # must work now that the atomic labels are exposed. + tensor = cvcuda.Tensor(shape, cvcuda.Type.U8, layout) + assert tensor.shape == shape + assert tensor.layout == layout + assert tensor.layout == cvcuda.TensorLayout(str(layout)) + + +@t.mark.parametrize("label", ["N", "C", "NHWC", "HWC", "NCHW"]) +def test_tensor_construction_accepts_layout_string(label): + # A string layout implicitly converts, and the resulting tensor's layout is + # value-equal to the enum-built one -- the round trip works in real usage. + shape = (10,) if len(label) == 1 else tuple(range(2, 2 + len(label))) + from_str = cvcuda.Tensor(shape, cvcuda.Type.U8, label) + from_enum = cvcuda.Tensor( + shape, cvcuda.Type.U8, getattr(cvcuda.TensorLayout, label) + ) + assert from_str.layout == from_enum.layout + assert from_str.layout == label + assert str(from_str.layout) == label diff --git a/tests/cvcuda/python/test_util.py b/tests/cvcuda/python/test_util.py index 2556994ec..a7ff06935 100644 --- a/tests/cvcuda/python/test_util.py +++ b/tests/cvcuda/python/test_util.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,11 +13,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -import cvcuda -import torch - import numpy as np + +import cvcuda import cvcuda_util as util +import cupy + RNG = np.random.default_rng(0) @@ -31,7 +32,7 @@ def test_create_tensor_odd(): tensor = util.create_tensor( [10], np.uint8, None, max_random=255, rng=RNG, transform_dist=util.dist_odd ) - h_data = torch.as_tensor(tensor.cuda(), device="cuda").cpu() + h_data = cupy.asarray(tensor.cuda()).get() assert all([bool(val % 2 == 1) for val in h_data]) diff --git a/tests/cvcuda/python/test_version.py b/tests/cvcuda/python/test_version.py index 3f903234b..631ba63c1 100644 --- a/tests/cvcuda/python/test_version.py +++ b/tests/cvcuda/python/test_version.py @@ -14,6 +14,7 @@ # limitations under the License. import os + import cvcuda diff --git a/tests/cvcuda/stressTest/cvcuda_cache_repro.py b/tests/cvcuda/stressTest/cvcuda_cache_repro.py index 002be5d23..10fd03918 100644 --- a/tests/cvcuda/stressTest/cvcuda_cache_repro.py +++ b/tests/cvcuda/stressTest/cvcuda_cache_repro.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,19 +15,17 @@ import numpy as np import cvcuda -import torch import random import threading import queue import time import gc +import cupy + def preprocess(input, out_size): - frame_nhwc = cvcuda.as_tensor( - torch.as_tensor(input).to(device="cuda:0", non_blocking=True), - "NHWC", - ) + frame_nhwc = cvcuda.as_tensor(input, "NHWC") resized = cvcuda.resize( frame_nhwc, ( @@ -46,21 +44,16 @@ def preprocess(input, out_size): def preprocess_into(input, out_size): - torch.cuda.synchronize() - cvcuda_RGBtensor = cvcuda.as_tensor(input.cuda(), "NHWC") - torch.cuda.synchronize() - torch_RGBtensor_resized = torch.empty( - ( - cvcuda_RGBtensor.shape[0], - out_size[1], - out_size[0], - cvcuda_RGBtensor.shape[3], - ), - dtype=torch.uint8, - device="cuda:0", - ) + cvcuda.Stream.current.sync() + cvcuda_RGBtensor = cvcuda.as_tensor(input, "NHWC") + cvcuda.Stream.current.sync() cvcuda_RGBtensor_resized = cvcuda.as_tensor( - torch_RGBtensor_resized.cuda(), + cupy.asarray( + np.zeros( + (input.shape[0], out_size[1], out_size[0], input.shape[3]), + dtype=np.uint8, + ) + ), "NHWC", ) cvcuda.resize_into( @@ -69,14 +62,14 @@ def preprocess_into(input, out_size): cvcuda.Interp.LINEAR, ) - torch_nchw = torch.empty( - (input.shape[0], 3, out_size[1], out_size[0]), - dtype=torch.uint8, - device="cuda:0", + cvcuda_nchw = cvcuda.as_tensor( + cupy.asarray( + np.zeros((input.shape[0], 3, out_size[1], out_size[0]), dtype=np.uint8) + ), + "NCHW", ) - cvcuda_nchw = cvcuda.as_tensor(torch_nchw.cuda(0), "NCHW") cvcuda.reformat_into(cvcuda_nchw, cvcuda_RGBtensor_resized) - return torch_nchw + return cvcuda_nchw def generate_images(N, width=None, height=None, random_size=False): @@ -86,7 +79,7 @@ def generate_images(N, width=None, height=None, random_size=False): else: w = width h = height - return torch.as_tensor(torch.rand(N, h, w, 3), dtype=torch.uint8) + return cupy.asarray(np.random.rand(N, h, w, 3).astype(np.uint8)) def worker(device_id, task_queue, result_queue): @@ -189,7 +182,7 @@ def main(): collected = gc.collect() print(f"Garbage collector: collected {collected} objects.") time.sleep(1) - torch.cuda.empty_cache() + cvcuda.Stream.current.sync() time.sleep(1) test_random_batch_size() collected = gc.collect() diff --git a/tests/cvcuda/stressTest/stress_test_inference.py b/tests/cvcuda/stressTest/stress_test_inference.py index a8f936517..32db42a69 100644 --- a/tests/cvcuda/stressTest/stress_test_inference.py +++ b/tests/cvcuda/stressTest/stress_test_inference.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,10 +16,16 @@ import logging import numpy as np import cvcuda + +# torch is intentionally imported without importorskip: this file also requires +# tensorrt and tensorflow, making it a full-ML-stack stress test that is NOT +# collected by the normal pytest run (cvcuda_test_python.in gates on cupy, not torch). +# It is only executed manually in specialized inference environments. import torch import random import os +import subprocess import sys import urllib.request import time @@ -234,16 +240,21 @@ def __init__( # Convert ETLE to TensorRT model using the TAO-Converter. self.logger.info("Converting the PeopleNet model to TensorRT...") - if os.system( - "tao-converter -e %s -k tlt_encode -d 3,%d,%d -m %d -i nchw %s" - % ( - trt_engine_file_path, - image_size[1], - image_size[0], - batch_size, - etlt_model_path, - ) - ): + conversion_cmd = [ + "tao-converter", + "-e", + trt_engine_file_path, + "-k", + "tlt_encode", + "-d", + f"3,{image_size[1]},{image_size[0]}", + "-m", + str(batch_size), + "-i", + "nchw", + etlt_model_path, + ] + if subprocess.run(conversion_cmd, check=False).returncode != 0: raise Exception("Conversion failed.") else: self.logger.info( diff --git a/tests/cvcuda/stressTest/stress_test_mt_prep.py b/tests/cvcuda/stressTest/stress_test_mt_prep.py index a6d974a96..268224a27 100644 --- a/tests/cvcuda/stressTest/stress_test_mt_prep.py +++ b/tests/cvcuda/stressTest/stress_test_mt_prep.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,18 +15,16 @@ import numpy as np import cvcuda -import torch import random import threading import queue import time +import cupy + def preprocess(input, out_size): - frame_nhwc = cvcuda.as_tensor( - torch.as_tensor(input).to(device="cuda:0", non_blocking=True), - "NHWC", - ) + frame_nhwc = cvcuda.as_tensor(input, "NHWC") resized = cvcuda.resize( frame_nhwc, ( @@ -45,21 +43,16 @@ def preprocess(input, out_size): def preprocess_into(input, out_size): - torch.cuda.synchronize() - cvcuda_RGBtensor = cvcuda.as_tensor(input.cuda(), "NHWC") - torch.cuda.synchronize() - torch_RGBtensor_resized = torch.empty( - ( - cvcuda_RGBtensor.shape[0], - out_size[1], - out_size[0], - cvcuda_RGBtensor.shape[3], - ), - dtype=torch.uint8, - device="cuda:0", - ) + cvcuda.Stream.current.sync() + cvcuda_RGBtensor = cvcuda.as_tensor(input, "NHWC") + cvcuda.Stream.current.sync() cvcuda_RGBtensor_resized = cvcuda.as_tensor( - torch_RGBtensor_resized.cuda(), + cupy.asarray( + np.zeros( + (input.shape[0], out_size[1], out_size[0], input.shape[3]), + dtype=np.uint8, + ) + ), "NHWC", ) cvcuda.resize_into( @@ -68,14 +61,14 @@ def preprocess_into(input, out_size): cvcuda.Interp.LINEAR, ) - torch_nchw = torch.empty( - (input.shape[0], 3, out_size[1], out_size[0]), - dtype=torch.uint8, - device="cuda:0", + cvcuda_nchw = cvcuda.as_tensor( + cupy.asarray( + np.zeros((input.shape[0], 3, out_size[1], out_size[0]), dtype=np.uint8) + ), + "NCHW", ) - cvcuda_nchw = cvcuda.as_tensor(torch_nchw.cuda(0), "NCHW") cvcuda.reformat_into(cvcuda_nchw, cvcuda_RGBtensor_resized) - return torch_nchw + return cvcuda_nchw def generate_images(N, width=None, height=None, random_size=False): @@ -85,7 +78,7 @@ def generate_images(N, width=None, height=None, random_size=False): else: w = width h = height - return torch.as_tensor(torch.rand(N, h, w, 3), dtype=torch.uint8) + return cupy.asarray(np.random.rand(N, h, w, 3).astype(np.uint8)) def worker(device_id, task_queue, result_queue): diff --git a/tests/cvcuda/stressTest/stress_test_preprocess.py b/tests/cvcuda/stressTest/stress_test_preprocess.py index 248d20c7a..6c7e4131e 100644 --- a/tests/cvcuda/stressTest/stress_test_preprocess.py +++ b/tests/cvcuda/stressTest/stress_test_preprocess.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,18 +15,16 @@ import numpy as np import cvcuda -import torch import random import time +import cupy + time_of_test_in_min = 0.1 def preprocess(input, out_size): - frame_nhwc = cvcuda.as_tensor( - torch.as_tensor(input).to(device="cuda:0", non_blocking=True), - "NHWC", - ) + frame_nhwc = cvcuda.as_tensor(input, "NHWC") resized = cvcuda.resize( frame_nhwc, ( @@ -45,20 +43,15 @@ def preprocess(input, out_size): def preprocess_into(input, out_size): - cvcuda_RGBtensor = cvcuda.as_tensor(input.cuda(), "NHWC") + cvcuda_RGBtensor = cvcuda.as_tensor(input, "NHWC") - torch_RGBtensor_resized = torch.empty( - ( - cvcuda_RGBtensor.shape[0], - out_size[1], - out_size[0], - cvcuda_RGBtensor.shape[3], - ), - dtype=torch.uint8, - device="cuda:0", + cuda_RGBtensor_resized = cupy.asarray( + np.zeros( + (input.shape[0], out_size[1], out_size[0], input.shape[3]), dtype=np.uint8 + ) ) cvcuda_RGBtensor_resized = cvcuda.as_tensor( - torch_RGBtensor_resized.cuda(), + cuda_RGBtensor_resized, "NHWC", ) cvcuda.resize_into( @@ -67,15 +60,15 @@ def preprocess_into(input, out_size): cvcuda.Interp.LINEAR, ) - torch_nchw = torch.empty( - (input.shape[0], 3, out_size[1], out_size[0]), - dtype=torch.uint8, - device="cuda:0", + cvcuda_nchw = cvcuda.as_tensor( + cupy.asarray( + np.zeros((input.shape[0], 3, out_size[1], out_size[0]), dtype=np.uint8) + ), + "NCHW", ) - cvcuda_nchw = cvcuda.as_tensor(torch_nchw.cuda(0), "NCHW") # normalized = cvcuda.convertto(cvcuda_nchw, np.float32, scale=1 / 255) cvcuda.reformat_into(cvcuda_nchw, cvcuda_RGBtensor_resized) - return torch_nchw + return cvcuda_nchw def generate_images(N, width=None, height=None, random_size=False): @@ -85,7 +78,7 @@ def generate_images(N, width=None, height=None, random_size=False): else: w = width h = height - return torch.as_tensor(torch.rand(N, h, w, 3), dtype=torch.uint8) + return cupy.asarray(np.random.rand(N, h, w, 3).astype(np.uint8)) def test_random_image_size(): @@ -183,7 +176,6 @@ def test_random_batch_size_into(): def main(): - print(torch.cuda.get_device_properties(0)) test_random_image_size() test_random_batch_size() test_random_image_size_into() diff --git a/tests/cvcuda/system/CMakeLists.txt b/tests/cvcuda/system/CMakeLists.txt index 134a1502f..dd15338cc 100644 --- a/tests/cvcuda/system/CMakeLists.txt +++ b/tests/cvcuda/system/CMakeLists.txt @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,26 +13,18 @@ # See the License for the specific language governing permissions and # limitations under the License. -if(ARCH_AARCH64) - include_directories( - ${CUOSD_SOURCE_DIR}/aarch64/include - ) - link_directories( - ${CUOSD_SOURCE_DIR}/aarch64/lib - ) -else() - include_directories( - ${CUOSD_SOURCE_DIR}/x86_64/include - ) - link_directories( - ${CUOSD_SOURCE_DIR}/x86_64/lib - ) -endif() - # system core ------------------------------------------------- -# Base source files that work with C++17 and don't require libcuosd set(CVCUDA_TEST_SOURCES + TestOpJpegCompressionDistortion.cpp + TestOpAdjustHue.cpp + TestOpAdjustSaturation.cpp + TestOpAdjustSharpness.cpp + TestOpAdjustContrast.cpp + TestOpInvert.cpp + TestOpSolarize.cpp + TestOpPosterize.cpp + TestOpAutoContrast.cpp TestOpPairwiseMatcher.cpp TestOpStack.cpp TestOpLabel.cpp @@ -84,13 +76,16 @@ set(CVCUDA_TEST_SOURCES TestOpInpaint.cpp TestOpFindHomography.cpp TestOpHQResize.cpp + TestOpOSD.cpp + TestOpBndBox.cpp + TestOpBoxBlur.cpp + TestOpCLAHE.cpp + TestOperatorWrapperCopy.cpp ) -# Smoke tests that don't require libcuosd - these work on all compilers including GCC-10 set(CVCUDA_SMOKE_TEST_SOURCES TestOpBndBox_Smoke.cpp TestOpBoxBlur_Smoke.cpp - TestOpOSD_Smoke.cpp ) # Test files that require C++20 (use floating-point values as template non-type parameters) @@ -102,15 +97,6 @@ set(CVCUDA_TEST_SOURCES_CPP20 TestOpSIFT.cpp ) -# Test files that require libcuosd (OSD library) -# libcuosd has ABI incompatibility with GCC-10, so these tests are excluded for GCC-10 -set(CVCUDA_TEST_SOURCES_CUOSD - TestOpOSD.cpp - TestOpBndBox.cpp - TestOpBoxBlur.cpp - OsdUtils.cu -) - # Build smoke tests unconditionally - they work on all compilers add_executable(cvcuda_test_system_smoke ${CVCUDA_SMOKE_TEST_SOURCES}) @@ -118,36 +104,25 @@ target_link_libraries(cvcuda_test_system_smoke PUBLIC cvcuda nvcv_test_common_system - # Note: Does NOT link against cuosd ) nvcv_add_test(cvcuda_test_system_smoke cvcuda cpp) -# Include C++20-requiring tests and libcuosd tests only if compiler supports them properly +# Include C++20-requiring tests only if compiler supports them if(NOT (CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 11.0)) list(APPEND CVCUDA_TEST_SOURCES ${CVCUDA_TEST_SOURCES_CPP20}) - list(APPEND CVCUDA_TEST_SOURCES ${CVCUDA_TEST_SOURCES_CUOSD}) endif() -# Build cvcuda_test_system (works with GCC-10+ but with reduced test coverage for GCC-10) add_executable(cvcuda_test_system ${CVCUDA_TEST_SOURCES}) -# Link against libcuosd only for GCC-11+ (needed for OSD tests) -if(NOT (CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 11.0)) - target_link_libraries(cvcuda_test_system - PUBLIC - cvcuda - nvcv_test_common_system - cuosd - ) -else() - # GCC-10: Build without libcuosd, excluding OSD and C++20 NTTP tests - target_link_libraries(cvcuda_test_system - PUBLIC - cvcuda - nvcv_test_common_system - ) - message(STATUS "GCC-${CMAKE_CXX_COMPILER_VERSION}: Building cvcuda_test_system without libcuosd. OSD tests and C++20 NTTP tests excluded.") +target_link_libraries(cvcuda_test_system + PUBLIC + cvcuda + nvcv_test_common_system +) + +if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 11.0) + message(STATUS "GCC-${CMAKE_CXX_COMPILER_VERSION}: Building cvcuda_test_system without C++20 NTTP tests.") endif() nvcv_add_test(cvcuda_test_system cvcuda cpp) diff --git a/tests/cvcuda/system/ConvUtils.cpp b/tests/cvcuda/system/ConvUtils.cpp index aeb9796b0..bf026fa4b 100644 --- a/tests/cvcuda/system/ConvUtils.cpp +++ b/tests/cvcuda/system/ConvUtils.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -28,22 +28,8 @@ namespace nvcv::test { namespace detail { -template -inline void Convolve(std::vector &hDst, const long3 &dstStrides, const std::vector &hSrc, - const long3 &srcStrides, const int3 &shape, const std::vector &kernel, - const Size2D &kernelSize, int2 &kernelAnchor, const NVCVBorderType &borderMode, - const float4 &borderValue) +inline void ResolveKernelAnchor(int2 &kernelAnchor, const Size2D &kernelSize) { - using BT = cuda::BaseType; - using WT = cuda::ConvertBaseTypeTo; - int2 size = cuda::DropCast<2>(shape); - - T borderValueT; - for (int e = 0; e < cuda::NumElements; ++e) - { - cuda::GetElement(borderValueT, e) = static_cast(cuda::GetElement(borderValue, e)); - } - if (kernelAnchor.x < 0) { kernelAnchor.x = kernelSize.w / 2; @@ -52,46 +38,25 @@ inline void Convolve(std::vector &hDst, const long3 &dstStrides, const { kernelAnchor.y = kernelSize.h / 2; } +} - for (int b = 0; b < shape.z; ++b) - { - for (int y = 0; y < shape.y; ++y) - { - for (int x = 0; x < shape.x; ++x) - { - WT res = cuda::SetAll(0); - - int2 coord; - - for (int ky = 0; ky < kernelSize.h; ++ky) - { - coord.y = y + ky - kernelAnchor.y; - - for (int kx = 0; kx < kernelSize.w; ++kx) - { - coord.x = x + kx - kernelAnchor.x; - - T srcValue = IsInside(coord, size, borderMode) - ? ValueAt(hSrc, srcStrides, b, coord.y, coord.x) - : borderValueT; - - res += srcValue * kernel[ky * kernelSize.w + kx]; - } - } +template +inline T MakeConvolveBorderValue(const float4 &borderValue) +{ + using BT = cuda::BaseType; - ValueAt(hDst, dstStrides, b, y, x) = cuda::SaturateCast(res); - } - } + T borderValueT; + for (int e = 0; e < cuda::NumElements; ++e) + { + cuda::GetElement(borderValueT, e) = static_cast(cuda::GetElement(borderValue, e)); } + return borderValueT; } template -inline void Morph(std::vector &hDst, const long3 &dstStrides, const std::vector &hSrc, - const long3 &srcStrides, const int3 &shape, const Size2D &kernelSize, int2 &kernelAnchor, - const NVCVBorderType &borderMode, NVCVMorphologyType type) +inline T MakeMorphBorderValue(NVCVMorphologyType type) { - using BT = cuda::BaseType; - int2 size = cuda::DropCast<2>(shape); + using BT = cuda::BaseType; BT val = (type == NVCVMorphologyType::NVCV_DILATE) ? std::numeric_limits::min() : std::numeric_limits::max(); @@ -100,43 +65,147 @@ inline void Morph(std::vector &hDst, const long3 &dstStrides, const std { cuda::GetElement(borderValueT, e) = val; } + return borderValueT; +} - if (kernelAnchor.x < 0) +template +struct ConvolveRefData +{ + const std::vector &hSrc; + const long3 &srcStrides; + const std::vector &kernel; + const Size2D &kernelSize; + const int2 &kernelAnchor; + const NVCVBorderType &borderMode; + T borderValue; + int2 size; +}; + +template +struct MorphRefData +{ + const std::vector &hSrc; + const long3 &srcStrides; + const Size2D &kernelSize; + const int2 &kernelAnchor; + const NVCVBorderType &borderMode; + NVCVMorphologyType type; + T borderValue; + int2 size; +}; + +template +inline T SourceOrBorder(const std::vector &hSrc, const long3 &srcStrides, const int2 &size, int b, int2 coord, + const NVCVBorderType &borderMode, T borderValue) +{ + return IsInside(coord, size, borderMode) ? ValueAt(hSrc, srcStrides, b, coord.y, coord.x) : borderValue; +} + +template +inline T ConvolveSourceValue(const ConvolveRefData &ref, int b, int y, int x, int ky, int kx) +{ + int2 coord{x + kx - ref.kernelAnchor.x, y + ky - ref.kernelAnchor.y}; + return SourceOrBorder(ref.hSrc, ref.srcStrides, ref.size, b, coord, ref.borderMode, ref.borderValue); +} + +template +inline auto ConvolvePixel(const ConvolveRefData &ref, int b, int y, int x) +{ + using BT = cuda::BaseType; + using WT = cuda::ConvertBaseTypeTo; + + WT res = cuda::SetAll(0); + for (int ky = 0; ky < ref.kernelSize.h; ++ky) { - kernelAnchor.x = kernelSize.w / 2; + for (int kx = 0; kx < ref.kernelSize.w; ++kx) + { + res += ConvolveSourceValue(ref, b, y, x, ky, kx) * ref.kernel[ky * ref.kernelSize.w + kx]; + } } - if (kernelAnchor.y < 0) + + return cuda::SaturateCast(res); +} + +template +inline T MorphSourceValue(const MorphRefData &ref, int b, int y, int x, int ky, int kx) +{ + int2 coord{x + kx - ref.kernelAnchor.x, y + ky - ref.kernelAnchor.y}; + return SourceOrBorder(ref.hSrc, ref.srcStrides, ref.size, b, coord, ref.borderMode, ref.borderValue); +} + +template +inline T CombineMorphValue(T current, T next, NVCVMorphologyType type) +{ + return (type == NVCVMorphologyType::NVCV_DILATE) ? cuda::max(current, next) : cuda::min(current, next); +} + +template +inline auto MorphPixel(const MorphRefData &ref, int b, int y, int x) +{ + using BT = cuda::BaseType; + + T res = ref.borderValue; + for (int ky = 0; ky < ref.kernelSize.h; ++ky) { - kernelAnchor.y = kernelSize.h / 2; + for (int kx = 0; kx < ref.kernelSize.w; ++kx) + { + res = CombineMorphValue(res, MorphSourceValue(ref, b, y, x, ky, kx), ref.type); + } } + return cuda::SaturateCast(res); +} + +template +inline void Convolve(std::vector &hDst, const long3 &dstStrides, const std::vector &hSrc, + const long3 &srcStrides, const int3 &shape, const std::vector &kernel, + const Size2D &kernelSize, int2 &kernelAnchor, const NVCVBorderType &borderMode, + const float4 &borderValue) +{ + ResolveKernelAnchor(kernelAnchor, kernelSize); + ConvolveRefData ref{hSrc, + srcStrides, + kernel, + kernelSize, + kernelAnchor, + borderMode, + MakeConvolveBorderValue(borderValue), + cuda::DropCast<2>(shape)}; + for (int b = 0; b < shape.z; ++b) { for (int y = 0; y < shape.y; ++y) { for (int x = 0; x < shape.x; ++x) { - T res = cuda::SetAll(val); - - int2 coord; - - for (int ky = 0; ky < kernelSize.h; ++ky) - { - coord.y = y + ky - kernelAnchor.y; - - for (int kx = 0; kx < kernelSize.w; ++kx) - { - coord.x = x + kx - kernelAnchor.x; + ValueAt(hDst, dstStrides, b, y, x) = ConvolvePixel(ref, b, y, x); + } + } + } +} - T srcValue = IsInside(coord, size, borderMode) - ? ValueAt(hSrc, srcStrides, b, coord.y, coord.x) - : borderValueT; +template +inline void Morph(std::vector &hDst, const long3 &dstStrides, const std::vector &hSrc, + const long3 &srcStrides, const int3 &shape, const Size2D &kernelSize, int2 &kernelAnchor, + const NVCVBorderType &borderMode, NVCVMorphologyType type) +{ + ResolveKernelAnchor(kernelAnchor, kernelSize); + MorphRefData ref{hSrc, + srcStrides, + kernelSize, + kernelAnchor, + borderMode, + type, + MakeMorphBorderValue(type), + cuda::DropCast<2>(shape)}; - res = (type == NVCVMorphologyType::NVCV_DILATE) ? cuda::max(res, srcValue) - : cuda::min(res, srcValue); - } - } - ValueAt(hDst, dstStrides, b, y, x) = cuda::SaturateCast(res); + for (int b = 0; b < shape.z; ++b) + { + for (int y = 0; y < shape.y; ++y) + { + for (int x = 0; x < shape.x; ++x) + { + ValueAt(hDst, dstStrides, b, y, x) = MorphPixel(ref, b, y, x); } } } @@ -171,7 +240,7 @@ void Convolve(std::vector &hDst, const long3 &dstStrides, const std::ve { NVCV_ASSERT(format.numPlanes() == 1); - switch (format.planeDataType(0)) + switch (static_cast(format.planeDataType(0))) { #define NVCV_TEST_CASE(DATATYPE, TYPE) \ case NVCV_DATA_TYPE_##DATATYPE: \ @@ -200,7 +269,7 @@ void Morph(std::vector &hDst, const long3 &dstStrides, const std::vecto { NVCV_ASSERT(format.numPlanes() == 1); - switch (format.planeDataType(0)) + switch (static_cast(format.planeDataType(0))) { #define NVCV_TEST_CASE(DATATYPE, TYPE) \ case NVCV_DATA_TYPE_##DATATYPE: \ @@ -224,7 +293,7 @@ void Morph(std::vector &hDst, const long3 &dstStrides, const std::vecto std::vector ComputeMeanKernel(nvcv::Size2D kernelSize) { std::size_t ks = kernelSize.w * kernelSize.h; - float kv = 1.f / ks; + float kv = 1.f / static_cast(ks); std::vector kernel(ks, kv); return kernel; @@ -236,15 +305,15 @@ std::vector ComputeGaussianKernel(nvcv::Size2D kernelSize, double2 sigma) int2 half{kernelSize.w / 2, kernelSize.h / 2}; - float sx = 2.f * sigma.x * sigma.x; - float sy = 2.f * sigma.y * sigma.y; - float s = 2.f * sigma.x * sigma.y * M_PI; + auto sx = static_cast(2.0 * sigma.x * sigma.x); + auto sy = static_cast(2.0 * sigma.y * sigma.y); + auto s = static_cast(2.0 * sigma.x * sigma.y * M_PI); float sum = 0.f; for (int y = -half.y; y <= half.y; ++y) { for (int x = -half.x; x <= half.x; ++x) { - float kv = std::exp(-((x * x) / sx + (y * y) / sy)) / s; + auto kv = std::exp(-((static_cast(x * x) / sx) + (static_cast(y * y) / sy))) / s; kernel[(y + half.y) * kernelSize.w + (x + half.x)] = kv; diff --git a/tests/cvcuda/system/ConvUtils.hpp b/tests/cvcuda/system/ConvUtils.hpp index d8a98446f..99a23be1a 100644 --- a/tests/cvcuda/system/ConvUtils.hpp +++ b/tests/cvcuda/system/ConvUtils.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,6 +19,7 @@ #define NVCV_TEST_COMMON_CONV_UTILS_HPP #include +#include #include // for long3, etc. #include #include @@ -43,6 +44,67 @@ std::vector ComputeMeanKernel(nvcv::Size2D kernelSize); std::vector ComputeGaussianKernel(nvcv::Size2D kernelSize, double2 sigma); +inline auto ConvolutionNegativeParams() +{ + ValueList params{ + { nvcv::FMT_RGB8, nvcv::FMT_RGB8p, NVCV_BORDER_CONSTANT}, + { nvcv::FMT_RGB8p, nvcv::FMT_RGB8p, NVCV_BORDER_CONSTANT}, + {nvcv::FMT_RGBf16, nvcv::FMT_RGBf16, NVCV_BORDER_CONSTANT}, + }; +#ifndef ENABLE_SANITIZER + params.emplace_back(nvcv::FMT_RGB8, nvcv::FMT_RGB8, static_cast(255)); +#endif + return params; +} + +inline auto PlanarConvolutionNegativeParams() +{ + ValueList params{ + { nvcv::FMT_RGB8, nvcv::FMT_RGB8p, NVCV_BORDER_CONSTANT}, + { nvcv::FMT_RGB8p, nvcv::FMT_RGB8, NVCV_BORDER_CONSTANT}, + {nvcv::FMT_RGBf16, nvcv::FMT_RGBf16, NVCV_BORDER_CONSTANT}, + { nvcv::FMT_2F32, nvcv::FMT_2F32, NVCV_BORDER_CONSTANT}, + }; +#ifndef ENABLE_SANITIZER + params.emplace_back(nvcv::FMT_RGB8, nvcv::FMT_RGB8, static_cast(255)); +#endif + return params; +} + +inline auto ConvolutionVarShapeNegativeParams() +{ + ValueList params{ + { nvcv::FMT_RGB8, nvcv::FMT_RGB8p, NVCV_BORDER_CONSTANT, 3, 3}, + { nvcv::FMT_RGB8p, nvcv::FMT_RGB8p, NVCV_BORDER_CONSTANT, 3, 3}, + {nvcv::FMT_RGBf16, nvcv::FMT_RGBf16, NVCV_BORDER_CONSTANT, 3, 3}, + { nvcv::FMT_RGB8, nvcv::FMT_RGB8, NVCV_BORDER_CONSTANT, 3, -1}, + { nvcv::FMT_RGB8, nvcv::FMT_RGB8, NVCV_BORDER_CONSTANT, 5, 3}, + }; +#ifndef ENABLE_SANITIZER + params.emplace_back(nvcv::FMT_RGB8, nvcv::FMT_RGB8, static_cast(255), 3, 3); +#endif + return params; +} + +// Var-shape negative cases for filter ops that DO support planar layout (AverageBlur/Gaussian/ +// Laplacian/MedianBlur/...). Same as ConvolutionVarShapeNegativeParams except a planar<->interleaved +// layout MISMATCH (RGB8p in / RGB8 out) is rejected, while planar<->planar is now valid and so is not +// a negative case. Shared so the planar-capable filter ops do not each re-declare this matrix. +inline auto PlanarFilterVarShapeNegativeParams() +{ + ValueList params{ + { nvcv::FMT_RGB8, nvcv::FMT_RGB8p, NVCV_BORDER_CONSTANT, 3, 3}, + { nvcv::FMT_RGB8p, nvcv::FMT_RGB8, NVCV_BORDER_CONSTANT, 3, 3}, + {nvcv::FMT_RGBf16, nvcv::FMT_RGBf16, NVCV_BORDER_CONSTANT, 3, 3}, + { nvcv::FMT_RGB8, nvcv::FMT_RGB8, NVCV_BORDER_CONSTANT, 3, -1}, + { nvcv::FMT_RGB8, nvcv::FMT_RGB8, NVCV_BORDER_CONSTANT, 5, 3}, + }; +#ifndef ENABLE_SANITIZER + params.emplace_back(nvcv::FMT_RGB8, nvcv::FMT_RGB8, static_cast(255), 3, 3); +#endif + return params; +} + namespace detail { template diff --git a/tests/cvcuda/system/CvtColorUtils.cpp b/tests/cvcuda/system/CvtColorUtils.cpp index cc5350b11..e00a50a1e 100644 --- a/tests/cvcuda/system/CvtColorUtils.cpp +++ b/tests/cvcuda/system/CvtColorUtils.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,11 +17,15 @@ #include "CvtColorUtils.hpp" +#include "TestUtils.hpp" + #include #include +#include #include // For std::floor #include // For std::memcpy +#include namespace cuda = nvcv::cuda; @@ -66,11 +70,7 @@ static constexpr double Red2Cr_601 = 0.71310298; // 1.0 / 1.402322 Cr/V // clang-format off -// Coefficients to convert chromaticity (U and V) components to RGB . -// static constexpr double U2Blu = 2.03211; -// static constexpr double U2Grn = -0.39465; -// static constexpr double V2Grn = -0.58060; -// static constexpr double V2Red = 1.13983; +// Coefficients to convert chromaticity (U and V) components to RGB. static constexpr double U2Blu = 2.032; static constexpr double U2Grn = -0.395; static constexpr double V2Grn = -0.581; @@ -112,12 +112,155 @@ static constexpr double Add2V_NV12 = 128.0; template> constexpr BT Alpha = std::is_floating_point_v ? 1 : cuda::TypeTraits::max; +template +struct RgbPixel +{ + T r; + T g; + T b; +}; + +struct YuvPair +{ + double y0; + double y1; + double u; + double v; +}; + +template +RgbPixel ReadRgbPixel(const T *rgb, bool bgr) +{ + RgbPixel pixel{rgb[0], rgb[1], rgb[2]}; + if (bgr) + { + std::swap(pixel.r, pixel.b); + } + return pixel; +} + +template +void StoreRgbPixel(T *&rgb, T r, T g, T b, bool rgba, bool bgr) +{ + if (bgr) + { + std::swap(r, b); + } + + *rgb++ = r; + *rgb++ = g; + *rgb++ = b; + if (rgba) + { + *rgb++ = Alpha; + } +} + +inline double ClampedLuma(double y) +{ + y -= Add2Y_NV12; + return y < 0.0 ? 0.0 : y; +} + +template +void StoreNv12RgbPixel(T *&rgb, double y, double u, double v, bool rgba, bool bgr) +{ + T r = cuda::SaturateCast(Y2R_NV12 * y + U2R_NV12 * u + V2R_NV12 * v); + T g = cuda::SaturateCast(Y2G_NV12 * y + U2G_NV12 * u + V2G_NV12 * v); + T b = cuda::SaturateCast(Y2B_NV12 * y + U2B_NV12 * u + V2B_NV12 * v); + + StoreRgbPixel(rgb, r, g, b, rgba, bgr); +} + +template +void StoreYuv420ChromaIfNeeded(T *&u, T *&v, const RgbPixel &rgb, unsigned int w, unsigned int h) +{ + if ((w & 1) != 0 || (h & 1) != 0) + { + return; + } + + double chromaU = R2U_NV12 * rgb.r + G2U_NV12 * rgb.g + B2U_NV12 * rgb.b + Add2U_NV12; + double chromaV = R2V_NV12 * rgb.r + G2V_NV12 * rgb.g + B2V_NV12 * rgb.b + Add2V_NV12; + + *u++ = cuda::SaturateCast(chromaU); + *v++ = cuda::SaturateCast(chromaV); +} + +template +void StoreNv12ChromaIfNeeded(T *&uv, const RgbPixel &rgb, unsigned int w, unsigned int h, bool yvu) +{ + if ((w & 1) != 0 || (h & 1) != 0) + { + return; + } + + double u = R2U_NV12 * rgb.r + G2U_NV12 * rgb.g + B2U_NV12 * rgb.b + Add2U_NV12; + double v = R2V_NV12 * rgb.r + G2V_NV12 * rgb.g + B2V_NV12 * rgb.b + Add2V_NV12; + if (yvu) + { + std::swap(u, v); + } + + *uv++ = cuda::SaturateCast(u); + *uv++ = cuda::SaturateCast(v); +} + +template +std::pair ReadNv12Chroma(const T *uv, bool yvu) +{ + double u = uv[0]; + double v = uv[1]; + if (yvu) + { + std::swap(u, v); + } + return {u - Add2U_NV12, v - Add2V_NV12}; +} + +template +std::pair Yuv420ChromaRows(const T *src, size_t imgPixels, unsigned int wdth, unsigned int h, + bool yvu) +{ + // NOTE: when computing subsampled row index, h needs to be integer divided by 4 before multiplying by width. + const T *u = src + imgPixels + (h / 4) * wdth + ((h / 2) & 1) * (wdth / 2); + const T *v = u + imgPixels / 4; + if (yvu) + { + std::swap(u, v); + } + return {u, v}; +} + +template +YuvPair ReadYuv422Pair(const T *img, bool yvu) +{ + constexpr unsigned int idx0 = (LumaFirst ? 0 : 1); // First luma value index. + constexpr unsigned int idx1 = idx0 + 2; // Second luma value index. + constexpr unsigned int idxU = (LumaFirst ? 1 : 0); // U chroma value index. + constexpr unsigned int idxV = idxU + 2; // V chroma value index. + + YuvPair pair{ClampedLuma(img[idx0]), ClampedLuma(img[idx1]), img[idxU] - Add2U_NV12, img[idxV] - Add2V_NV12}; + if (yvu) + { + std::swap(pair.u, pair.v); + } + return pair; +} + +template +void StoreYuv422RgbPair(T *&rgb, const YuvPair &pair, bool rgba, bool bgr) +{ + StoreNv12RgbPixel(rgb, pair.y0, pair.u, pair.v, rgba, bgr); + StoreNv12RgbPixel(rgb, pair.y1, pair.u, pair.v, rgba, bgr); +} + //-==================================================================================================================-// // Set AlphaOnly to true to add/remove alpha channel to RGB/BGR image (without switching between RGB and BGR). template static void convertRGBtoBGR(T *dst, const T *src, size_t numPixels, bool srcRGBA, bool dstRGBA) { - const unsigned int incr = 3 + srcRGBA; + const unsigned int incr = srcRGBA ? 4 : 3; for (size_t i = 0; i < numPixels; i++, src += incr) { @@ -173,7 +316,7 @@ MAKE_CHANGE_ALPHA(double); template void convertRGBtoGray(T *dst, const T *src, size_t numPixels, bool rgba, bool bgr) { - const int incr = 3 + rgba; + const int incr = rgba ? 4 : 3; for (size_t i = 0; i < numPixels; i++, dst++, src += incr) { @@ -244,7 +387,7 @@ template void convertRGBtoHSV(T *dst, const T *src, size_t numPixels, bool rgba, bool bgr) { // Set the hue range (e.g., 0-360 for float types) and scale factor (to convert the final value to output hue value). - constexpr double range = (sizeof(T) > 1) ? 360.0 : (FullRange ? 256.0 : 180.0); + constexpr double range = HsvHueRange(); constexpr double scale = range / 360.0; constexpr double norm = std::is_floating_point_v ? 1 : cuda::TypeTraits::max; constexpr double round = std::is_floating_point_v ? 0 : 0.5; @@ -263,9 +406,9 @@ void convertRGBtoHSV(T *dst, const T *src, size_t numPixels, bool rgba, bool bgr double Vmin = std::min(R, std::min(G, B)); double V = std::max(R, std::max(G, B)); - double diff = static_cast(V - Vmin); + auto diff = V - Vmin; - double S = static_cast(V) > DBL_EPSILON ? diff / V : 0.0; + double S = V > DBL_EPSILON ? diff / V : 0.0; double H = 0.0; if (diff > DBL_EPSILON) @@ -336,60 +479,60 @@ MAKE_RGBtoHSV(double); 2) H' = H / 60 3) C = V * S 4) I = (int)H - 5) h = H' - I // Fractional part of H' + 5) h = H' - I (fractional part of H') 6) X = C * (1 - fabs(fmod(H', 2.0) - 1.0)) = C * (1 - fabs(H' - (I & ~1) - 1.0)) = C * ((I & 1) ? 1 - h : h) 5) m = V - C = V - V * S = V * (1 - S) - 7) p = X + m // When I is even: (I & 1) == 0 (I = 0, 2, or 4) + 7) p = X + m (when I is even: (I & 1) == 0 (I = 0, 2, or 4)) = C * h + V - C = V * S * h + V * (1 - S) = V * (S * h + 1 - S) = V * (1 - S + S * h) = V * (1 - S * (1 - h)) - 8) q = X + m // When I is odd: (I & 1) == 1 (I = 1, 3, or 5) + 8) q = X + m (when I is odd: (I & 1) == 1 (I = 1, 3, or 5)) = C * (1 - h) + V - C = V * S * (1 - h) + V * (1 - S) = V * (S - S * h + 1 - S) = V * (1 - S * h) - 9) Cases: // Note: C + m = C + V - C = V + 9) Cases: C + m = C + V - C = V I == 0: R = C + m = V - G = X + m = p // Even case + G = X + m = p (even case) B = m - I == 1: R = X + m = q // Odd case + I == 1: R = X + m = q (odd case) G = C + m = V B = m I == 2: R = m G = C + m = V - B = X + m = p // Even case + B = X + m = p (even case) I == 3: R = m - G = X + m = q // Odd case + G = X + m = q (odd case) B = C + m = V - I == 4: R = X + m = p // Even case + I == 4: R = X + m = p (even case) G = m B = C + m = V I == 5: R = C + m = V G = m - B = X + m = q // Odd case + B = X + m = q (odd case) */ template void convertHSVtoRGB(T *dst, const T *src, size_t numPixels, bool rgba, bool bgr) { - constexpr double range = (sizeof(T) > 1) ? 360.0 : (FullRange ? 256.0 : 180.0); + constexpr double range = HsvHueRange(); constexpr double scale = 6.0 / range; constexpr double norm = std::is_floating_point_v ? 1 : cuda::TypeTraits::max; constexpr double round = std::is_floating_point_v ? 0 : 0.5; - constexpr unsigned int mapR[6] = {0, 2, 1, 1, 3, 0}; - constexpr unsigned int mapG[6] = {3, 0, 0, 2, 1, 1}; - constexpr unsigned int mapB[6] = {1, 1, 3, 0, 0, 2}; + constexpr std::array mapR = {0, 2, 1, 1, 3, 0}; + constexpr std::array mapG = {3, 0, 0, 2, 1, 1}; + constexpr std::array mapB = {1, 1, 3, 0, 0, 2}; for (size_t i = 0; i < numPixels; i++) { @@ -397,7 +540,7 @@ void convertHSVtoRGB(T *dst, const T *src, size_t numPixels, bool rgba, bool bgr double S = *src++ / norm; // 0 <= S <= 1 double V = *src++ / norm; // 0 <= V <= 1 - int idx = static_cast(std::floor(H)); + auto idx = static_cast(std::floor(H)); H -= idx; @@ -405,10 +548,7 @@ void convertHSVtoRGB(T *dst, const T *src, size_t numPixels, bool rgba, bool bgr idx %= 6; if (idx < 0) idx += 6; - double val[] = {V, - V * (1 - S), - V * (1 - S * H), - V * (1 - S * (1 - H))}; + std::array val = {V, V * (1 - S), V * (1 - S * H), V * (1 - S * (1 - H))}; unsigned int r = mapR[idx]; unsigned int g = mapG[idx]; @@ -561,7 +701,7 @@ void convertRGBtoYUV_420(T *dst, const T *src, unsigned int wdth, unsigned int h assert(wdth % 2 == 0 && hght % 2 == 0); const size_t imgPixels = (size_t)hght * (size_t)wdth; - const size_t incrPix = 3 + rgba; + const size_t incrPix = rgba ? 4 : 3; const size_t incrSrc = imgPixels * incrPix; const size_t incrDst = imgPixels * 3 / 2; @@ -581,25 +721,9 @@ void convertRGBtoYUV_420(T *dst, const T *src, unsigned int wdth, unsigned int h { for (unsigned int w = 0; w < wdth; w++, rgb += incrPix) { - T R = rgb[0]; - T G = rgb[1]; - T B = rgb[2]; - - // Convert all RGB values to Y values and store them. - // clang-format off - if (bgr) std::swap(R, B); - *y++ = cuda::SaturateCast(R2Y_NV12 * R + G2Y_NV12 * G + B2Y_NV12 * B + Add2Y_NV12); - // clang-format on - - // Convert only even pixels (in width and height) to U and V values and store them. - if ((w & 1) == 0 && (h & 1) == 0) - { - double U = R2U_NV12 * R + G2U_NV12 * G + B2U_NV12 * B + Add2U_NV12; - double V = R2V_NV12 * R + G2V_NV12 * G + B2V_NV12 * B + Add2V_NV12; - - *u++ = cuda::SaturateCast(U); - *v++ = cuda::SaturateCast(V); - } + RgbPixel pixel = ReadRgbPixel(rgb, bgr); + *y++ = cuda::SaturateCast(R2Y_NV12 * pixel.r + G2Y_NV12 * pixel.g + B2Y_NV12 * pixel.b + Add2Y_NV12); + StoreYuv420ChromaIfNeeded(u, v, pixel, w, h); } } } @@ -611,7 +735,7 @@ void convertRGBtoYUV_420(vector &dst, const vector &src, unsigned int wdth unsigned int numImgs, bool rgba, bool bgr, bool yvu) { // Ensure input data has sets of 3 or 4 (RGB/BGA with or w/o alpha) values for the given width and height and batch size. - assert(src.size() == (size_t)numImgs * (size_t)hght * (size_t)wdth * (size_t)(3 + rgba)); + assert(src.size() == (size_t)numImgs * (size_t)hght * (size_t)wdth * (size_t)(rgba ? 4 : 3)); // YUV 420 needs 3 elements for each two RGB pixels. assert(dst.size() == (size_t)numImgs * (size_t)hght * (size_t)wdth * 3 / 2); @@ -644,7 +768,7 @@ void convertYUVtoRGB_420(T *dst, const T *src, unsigned int wdth, unsigned int h const size_t imgPixels = (size_t)hght * (size_t)wdth; const size_t incrSrc = imgPixels * 3 / 2; - const size_t incrDst = imgPixels * (3 + rgba); + const size_t incrDst = imgPixels * (rgba ? 4 : 3); for (unsigned int n = 0; n < numImgs; n++, src += incrSrc, dst += incrDst) { @@ -654,36 +778,15 @@ void convertYUVtoRGB_420(T *dst, const T *src, unsigned int wdth, unsigned int h for (unsigned int h = 0; h < hght; h++) { - // clang-format off - // NOTE: when computing subsampled row index, h needs to be integer divided by 4 before multiplying by width. - const T *u = src + imgPixels + (h / 4) * wdth + ((h / 2) & 1) * (wdth / 2); - const T *v = u + imgPixels / 4; - - if (yvu) std::swap(u, v); - // clang-format on + auto [u, v] = Yuv420ChromaRows(src, imgPixels, wdth, h, yvu); for (unsigned int w = 0; w < wdth; w++) { - double Y = *y++; - double U = *u; - double V = *v; - - // Convert all YUV (ITU Rec.601) values to RGB values and store them. - Y -= Add2Y_NV12; - U -= Add2U_NV12; - V -= Add2V_NV12; - - // clang-format off - if (Y < 0.0) Y = 0.0; - T R = cuda::SaturateCast(Y2R_NV12 * Y + U2R_NV12 * U + V2R_NV12 * V); - T G = cuda::SaturateCast(Y2G_NV12 * Y + U2G_NV12 * U + V2G_NV12 * V); - T B = cuda::SaturateCast(Y2B_NV12 * Y + U2B_NV12 * U + V2B_NV12 * V); - if (bgr) std::swap(R, B); - *rgb++ = R; - *rgb++ = G; - *rgb++ = B; - if (rgba) *rgb++ = Alpha; - // clang-format on + double Y = ClampedLuma(*y++); + double U = *u - Add2U_NV12; + double V = *v - Add2V_NV12; + + StoreNv12RgbPixel(rgb, Y, U, V, rgba, bgr); u += (w & 1); v += (w & 1); @@ -698,7 +801,7 @@ void convertYUVtoRGB_420(vector &dst, const vector &src, unsigned int wdth unsigned int numImgs, bool rgba, bool bgr, bool yvu) { // Ensure output data has sets of 3 or 4 (RGB/BGA with or w/o alpha) values for the given width and height and batch size. - assert(dst.size() == (size_t)numImgs * (size_t)hght * (size_t)wdth * (size_t)(3 + rgba)); + assert(dst.size() == (size_t)numImgs * (size_t)hght * (size_t)wdth * (size_t)(rgba ? 4 : 3)); // YUV 420 needs 3 elements for each two RGB pixels. assert(src.size() == (size_t)numImgs * (size_t)hght * (size_t)wdth * 3 / 2); @@ -774,7 +877,7 @@ void convertRGBtoNV12(T *dst, const T *src, unsigned int wdth, unsigned int hght assert(wdth % 2 == 0 && hght % 2 == 0); const size_t imgPixels = (size_t)hght * (size_t)wdth; - const size_t incrPix = 3 + rgba; + const size_t incrPix = rgba ? 4 : 3; const size_t incrSrc = imgPixels * incrPix; const size_t incrDst = imgPixels * 3 / 2; @@ -789,28 +892,9 @@ void convertRGBtoNV12(T *dst, const T *src, unsigned int wdth, unsigned int hght { for (unsigned int w = 0; w < wdth; w++, rgb += incrPix) { - T R = rgb[0]; - T G = rgb[1]; - T B = rgb[2]; - - // Convert all RGB values to Y values and store them. - // clang-format off - if (bgr) std::swap(R, B); - *y++ = cuda::SaturateCast(R2Y_NV12 * R + G2Y_NV12 * G + B2Y_NV12 * B + Add2Y_NV12); - // clang-format on - - // Convert only even pixels (in width and height) to U and V values and store them. - if ((w & 1) == 0 && (h & 1) == 0) - { - double U = R2U_NV12 * R + G2U_NV12 * G + B2U_NV12 * B + Add2U_NV12; - double V = R2V_NV12 * R + G2V_NV12 * G + B2V_NV12 * B + Add2V_NV12; - - // clang-format off - if (yvu) std::swap(U, V); - // clang-format on - *uv++ = cuda::SaturateCast(U); - *uv++ = cuda::SaturateCast(V); - } + RgbPixel pixel = ReadRgbPixel(rgb, bgr); + *y++ = cuda::SaturateCast(R2Y_NV12 * pixel.r + G2Y_NV12 * pixel.g + B2Y_NV12 * pixel.b + Add2Y_NV12); + StoreNv12ChromaIfNeeded(uv, pixel, w, h, yvu); } } } @@ -822,7 +906,7 @@ void convertRGBtoNV12(vector &dst, const vector &src, unsigned int wdth, u bool rgba, bool bgr, bool yvu) { // Ensure input data has sets of 3 or 4 (RGB/BGA with or w/o alpha) values for the given width and height and batch size. - assert(src.size() == (size_t)numImgs * (size_t)hght * (size_t)wdth * (size_t)(3 + rgba)); + assert(src.size() == (size_t)numImgs * (size_t)hght * (size_t)wdth * (size_t)(rgba ? 4 : 3)); // YUV NV12 needs 3 elements for each two RGB pixels. assert(dst.size() == (size_t)numImgs * (size_t)hght * (size_t)wdth * 3 / 2); @@ -855,7 +939,7 @@ void convertNV12toRGB(T *dst, const T *src, unsigned int wdth, unsigned int hght const size_t imgPixels = (size_t)hght * (size_t)wdth; const size_t incrSrc = imgPixels * 3 / 2; - const size_t incrDst = imgPixels * (3 + rgba); + const size_t incrDst = imgPixels * (rgba ? 4 : 3); for (unsigned int n = 0; n < numImgs; n++, src += incrSrc, dst += incrDst) { @@ -870,31 +954,12 @@ void convertNV12toRGB(T *dst, const T *src, unsigned int wdth, unsigned int hght for (unsigned int w = 0; w < wdth; w++) { - double Y = *y++; - double U = uv[0]; - double V = uv[1]; - - // clang-format off - if (yvu) std::swap(U, V); - - // Convert all YUV (ITU Rec.601) values to RGB values and store them. - Y -= Add2Y_NV12; - U -= Add2U_NV12; - V -= Add2V_NV12; - if (Y < 0.0) Y = 0.0; - - T R = cuda::SaturateCast(Y2R_NV12 * Y + U2R_NV12 * U + V2R_NV12 * V); - T G = cuda::SaturateCast(Y2G_NV12 * Y + U2G_NV12 * U + V2G_NV12 * V); - T B = cuda::SaturateCast(Y2B_NV12 * Y + U2B_NV12 * U + V2B_NV12 * V); - - if (bgr) std::swap(R, B); - *rgb++ = R; - *rgb++ = G; - *rgb++ = B; - if (rgba) *rgb++ = Alpha; - - if (w & 1) uv += 2; - // clang-format on + double Y = ClampedLuma(*y++); + auto [U, V] = ReadNv12Chroma(uv, yvu); + + StoreNv12RgbPixel(rgb, Y, U, V, rgba, bgr); + + uv += (w & 1) * 2; } } } @@ -906,7 +971,7 @@ void convertNV12toRGB(vector &dst, const vector &src, unsigned int wdth, u bool rgba, bool bgr, bool yvu) { // Ensure output data has sets of 3 or 4 (RGB/BGA with or w/o alpha) values for the given width and height and batch size. - assert(dst.size() == (size_t)numImgs * (size_t)hght * (size_t)wdth * (size_t)(3 + rgba)); + assert(dst.size() == (size_t)numImgs * (size_t)hght * (size_t)wdth * (size_t)(rgba ? 4 : 3)); // YUV NV12 needs 3 elements for each two RGB pixels. assert(src.size() == (size_t)numImgs * (size_t)hght * (size_t)wdth * 3 / 2); @@ -937,14 +1002,9 @@ void convertYUVtoRGB_422(T *dst, const T *src, unsigned int wdth, unsigned int h // Ensure width is a multiple of 2. assert(wdth % 2 == 0); - constexpr unsigned int idx0 = (LumaFirst ? 0 : 1); // First luma value index. - constexpr unsigned int idx1 = idx0 + 2; // Second luma value index. - constexpr unsigned int idxU = (LumaFirst ? 1 : 0); // U chroma value index. - constexpr unsigned int idxV = idxU + 2; // V chroma value index. - const size_t imgPixels = (size_t)hght * (size_t)wdth; const size_t incrSrc = imgPixels * 2; - const size_t incrDst = imgPixels * (3 + rgba); + const size_t incrDst = imgPixels * (rgba ? 4 : 3); for (unsigned int n = 0; n < numImgs; n++, src += incrSrc, dst += incrDst) { @@ -956,51 +1016,7 @@ void convertYUVtoRGB_422(T *dst, const T *src, unsigned int wdth, unsigned int h { for (unsigned int w = 0; w < wdth; w += 2, img += 4) { - T R, G, B; - - // clang-format off - double U = img[idxU], - V = img[idxV], - Y0 = img[idx0], - Y1 = img[idx1]; - - if (yvu) std::swap(U, V); - - // Convert all YUV (ITU Rec.601) values to RGB values and store them. - Y0 -= Add2Y_NV12; - Y1 -= Add2Y_NV12; - U -= Add2U_NV12; - V -= Add2V_NV12; - - if (Y0 < 0.0) Y0 = 0.0; - if (Y1 < 0.0) Y1 = 0.0; - // clang-format on - - double Y_0 = Y2R_NV12 * Y0; // NOTE: Y2R_NV12 == Y2G_NV12 == Y2B_NV12. - double Y_1 = Y2R_NV12 * Y1; - double UV_r = U2R_NV12 * U + V2R_NV12 * V; - double UV_g = U2G_NV12 * U + V2G_NV12 * V; - double UV_b = U2B_NV12 * U + V2B_NV12 * V; - - R = cuda::SaturateCast(Y_0 + UV_r); - G = cuda::SaturateCast(Y_0 + UV_g); - B = cuda::SaturateCast(Y_0 + UV_b); - - // clang-format off - if (bgr) std::swap(R, B); - *rgb++ = R; *rgb++ = G; *rgb++ = B; - if (rgba) *rgb++ = Alpha; - // clang-format on - - R = cuda::SaturateCast(Y_1 + UV_r); - G = cuda::SaturateCast(Y_1 + UV_g); - B = cuda::SaturateCast(Y_1 + UV_b); - - // clang-format off - if (bgr) std::swap(R, B); - *rgb++ = R; *rgb++ = G; *rgb++ = B; - if (rgba) *rgb++ = Alpha; - // clang-format on + StoreYuv422RgbPair(rgb, ReadYuv422Pair(img, yvu), rgba, bgr); } } } @@ -1012,7 +1028,7 @@ void convertYUVtoRGB_422(vector &dst, const vector &src, unsigned int wdth unsigned int numImgs, bool rgba, bool bgr, bool yvu) { // Ensure output data has sets of 3 or 4 (RGB/BGA w/ or w/o alpha) values for the given width, height, & batch size. - assert(dst.size() == (size_t)numImgs * (size_t)hght * (size_t)wdth * (size_t)(3 + rgba)); + assert(dst.size() == (size_t)numImgs * (size_t)hght * (size_t)wdth * (size_t)(rgba ? 4 : 3)); assert(src.size() == (size_t)numImgs * (size_t)hght * (size_t)wdth * 2); // 4 values for each two RGB pixels. convertYUVtoRGB_422(dst.data(), src.data(), wdth, hght, numImgs, rgba, bgr, yvu); @@ -1049,7 +1065,7 @@ MAKE_422toRGB(double); template void convertYUVtoGray_422(T *dst, const T *src, size_t numPixels) { - src += (1 - LumaFirst); // Increment to first Y value if luma not first. + src += (LumaFirst ? 0 : 1); // Increment to first Y value if luma not first. for (size_t i = 0; i < numPixels; i++, src += 2) *dst++ = *src; } diff --git a/tests/cvcuda/system/ElementwiseOpHarness.hpp b/tests/cvcuda/system/ElementwiseOpHarness.hpp new file mode 100644 index 000000000..96edcb190 --- /dev/null +++ b/tests/cvcuda/system/ElementwiseOpHarness.hpp @@ -0,0 +1,372 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#ifndef NVCV_TEST_SYSTEM_ELEMENTWISE_OP_HARNESS_HPP +#define NVCV_TEST_SYSTEM_ELEMENTWISE_OP_HARNESS_HPP + +// Shared system-test scaffold for unary element-wise operators (Invert, Solarize, Posterize, ...). +// These operators share an identical test shape — CPU gold over a dtype x channel matrix, +// a var-shape correctness case, and complement negative cases — and differ only in (a) the +// per-element gold function and (b) how the operator is invoked (extra scalar params). make-op +// generates each operator's test from one template, so the structural scaffold is centralized here +// and each TestOp.cpp supplies only the op-specific gold + invoke callables. This keeps the +// real per-operator content (the gold reference) explicit while removing copy-paste of the harness. + +#include "Definitions.hpp" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace nvcv::test::elementwise { + +// 0 = u8, 1 = u16, 2 = f32 -- the base type of a test image format. +inline int BaseKind(nvcv::ImageFormat fmt) +{ + if (fmt == nvcv::FMT_F32 || fmt == nvcv::FMT_RGBf32 || fmt == nvcv::FMT_RGBAf32) + { + return 2; + } + if (fmt == nvcv::FMT_U16) + { + return 1; + } + return 0; // FMT_U8 / FMT_RGB8 / FMT_RGBA8 +} + +// The dtype maximum used as the "bound" by the photometric operators (255 / 65535 for unsigned +// integers, 1.0 for float). Exposed so each operator's gold lambda can reuse the same convention. +template +constexpr DT Bound() +{ + return std::is_floating_point_v
? static_cast
(1) : std::numeric_limits
::max(); +} + +// Stable, type-appropriate input data for exact-result tests. This deliberately uses an explicit +// arithmetic sequence rather than a pseudo-random API: reproducibility matters here, not entropy. +template +void FillDeterministicValues(std::vector
&values, size_t sequence = 0) +{ + const uint64_t range = static_cast(Bound
()) + 1; + size_t i = 0; + for (DT &value : values) + { + const auto sample = static_cast((i + sequence * 131U) * 1664525U + 1013904223U); + if constexpr (std::is_floating_point_v
) + { + value = static_cast
(sample & 0xffffU) / static_cast
(0xffffU); + } + else + { + value = static_cast
(static_cast(sample) % range); + } + ++i; + } +} + +template +void ExpectBuffer(const std::vector
&gold, const std::vector
&got, double maxDiff) +{ + if (maxDiff == 0) + { + EXPECT_EQ(gold, got); + return; + } + + ASSERT_EQ(gold.size(), got.size()); + for (size_t i = 0; i < gold.size(); ++i) + { + ASSERT_NEAR(static_cast(gold[i]), static_cast(got[i]), maxDiff) << "at flat index " << i; + } +} + +// Common tensor scaffold for gold functions that operate on the whole interleaved image buffer. +// `goldBuffer` receives the input vector and channel count, and returns the expected output vector. +template +void RunTensorCorrectBuffer(int width, int height, int batch, nvcv::ImageFormat fmt, GoldBufferFn goldBuffer, + InvokeFn invoke, double maxDiff = 0) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + const int channels = fmt.numChannels(); + const size_t count = static_cast(width) * height * channels; + + nvcv::Tensor inTensor = nvcv::util::CreateTensor(batch, width, height, fmt); + nvcv::Tensor outTensor = nvcv::util::CreateTensor(batch, width, height, fmt); + auto inData = inTensor.exportData(); + auto outData = outTensor.exportData(); + ASSERT_NE(inData, nullptr); + ASSERT_NE(outData, nullptr); + + for (int s = 0; s < batch; ++s) + { + std::vector
in(count); + FillDeterministicValues(in, static_cast(s)); + if (!in.empty()) + { + in[0] = static_cast
(0); + } + if (in.size() > 1) + { + in[1] = Bound
(); + } + nvcv::util::SetImageTensorFromVector
(*inData, in, s); + } + + ASSERT_NO_THROW(invoke(stream, inTensor, outTensor)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + for (int s = 0; s < batch; ++s) + { + SCOPED_TRACE(s); + std::vector
in; + std::vector
got; + nvcv::util::GetImageVectorFromTensor
(*inData, s, in); + nvcv::util::GetImageVectorFromTensor
(*outData, s, got); + ExpectBuffer(goldBuffer(in, channels), got, maxDiff); + } + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +// Typed tensor correctness: fill a tensor with finite, type-appropriate values, run the operator +// via `invoke`, and compare bit-exact against the per-element `gold`. Using the matching element +// type (not raw bytes) keeps SetImage/GetImage element-size-correct and avoids feeding NaN/Inf bit +// patterns into float (whose host/device arithmetic can differ). +// +// gold: DT(DT) -- per-element reference (captures op params) +// invoke: void(cudaStream_t, const Tensor&, Tensor&) -- runs the operator (captures op params) +template +void RunTensorCorrect(int width, int height, int batch, nvcv::ImageFormat fmt, GoldFn gold, InvokeFn invoke) +{ + RunTensorCorrectBuffer
( + width, height, batch, fmt, + [gold](const std::vector
&in, int) + { + std::vector
out(in.size()); + for (size_t i = 0; i < in.size(); ++i) + { + out[i] = gold(in[i]); + } + return out; + }, + invoke); +} + +// Dispatch RunTensorCorrect over the u8 / u16 / f32 base type implied by `fmt`. Both `goldFor` and +// `invokeFor` are generic callables returning, for a given element type DT, the per-element gold +// lambda and the operator-invoke lambda respectively. Making invoke a per-DT factory lets operators +// with a dtype-dependent parameter (e.g. Solarize's mid-range threshold, which differs for u8 / u16 +// / f32) keep the gold and the invocation in lockstep; parameter-free operators (Invert) simply +// return the same invoke for every DT. +template +void RunTensorCorrectDispatch(int width, int height, int batch, nvcv::ImageFormat fmt, GoldFactory goldFor, + InvokeFactory invokeFor) +{ + switch (BaseKind(fmt)) + { + case 2: + RunTensorCorrect(width, height, batch, fmt, goldFor(float{}), invokeFor(float{})); + break; + case 1: + RunTensorCorrect(width, height, batch, fmt, goldFor(uint16_t{}), invokeFor(uint16_t{})); + break; + default: + RunTensorCorrect(width, height, batch, fmt, goldFor(uint8_t{}), invokeFor(uint8_t{})); + break; + } +} + +// Typed VarShape correctness on a small batch. `gold` is the per-element reference and +// `invoke` runs the operator on an ImageBatchVarShape pair. Each image starts with explicit zero and +// maximum-value sentinels so bound arithmetic is deterministic rather than left to random coverage. +// gold: DT(DT) +// invoke: void(cudaStream_t, const ImageBatchVarShape&, ImageBatchVarShape&) +template +void RunVarShapeCorrectBufferTyped(nvcv::ImageFormat fmt, GoldBufferFn goldBuffer, InvokeFn invoke, double maxDiff = 0) +{ + ASSERT_EQ(fmt.numPlanes(), 1) << "RunVarShapeCorrectBufferTyped requires an interleaved format"; + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + constexpr std::array widths = {23, 57, 89}; + constexpr std::array heights = {31, 71, 43}; + const auto n = static_cast(widths.size()); + const int ch = fmt.numChannels(); + + std::vector srcImgs; + std::vector dstImgs; + std::vector ws(n); + std::vector hs(n); + for (int i = 0; i < n; ++i) + { + ws[i] = widths[i]; + hs[i] = heights[i]; + srcImgs.emplace_back(nvcv::Size2D{ws[i], hs[i]}, fmt); + dstImgs.emplace_back(nvcv::Size2D{ws[i], hs[i]}, fmt); + } + + std::vector> golds(n); + for (int i = 0; i < n; ++i) + { + const size_t rowElements = static_cast(ws[i]) * ch; + const size_t rowBytes = rowElements * sizeof(DT); + std::vector
hwc(rowElements * hs[i]); + FillDeterministicValues(hwc, static_cast(i)); + hwc[0] = static_cast
(0); + hwc[1] = Bound
(); + golds[i] = goldBuffer(hwc, ch); + + auto idata = srcImgs[i].exportData(); + ASSERT_NE(idata, nullptr); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(idata->plane(0).basePtr, idata->plane(0).rowStride, hwc.data(), rowBytes, + rowBytes, hs[i], cudaMemcpyHostToDevice)); + } + + nvcv::ImageBatchVarShape src(n); + nvcv::ImageBatchVarShape dst(n); + src.pushBack(srcImgs.begin(), srcImgs.end()); + dst.pushBack(dstImgs.begin(), dstImgs.end()); + + ASSERT_NO_THROW(invoke(stream, src, dst)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + for (int i = 0; i < n; ++i) + { + SCOPED_TRACE(i); + const size_t rowElements = static_cast(ws[i]) * ch; + const size_t rowBytes = rowElements * sizeof(DT); + std::vector
got(rowElements * hs[i]); + auto odata = dstImgs[i].exportData(); + ASSERT_NE(odata, nullptr); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(got.data(), rowBytes, odata->plane(0).basePtr, odata->plane(0).rowStride, + rowBytes, hs[i], cudaMemcpyDeviceToHost)); + ExpectBuffer(golds[i], got, maxDiff); + } + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +template +void RunVarShapeCorrectTyped(nvcv::ImageFormat fmt, GoldFn gold, InvokeFn invoke) +{ + RunVarShapeCorrectBufferTyped
( + fmt, + [gold](const std::vector
&in, int) + { + std::vector
out(in.size()); + for (size_t i = 0; i < in.size(); ++i) + { + out[i] = gold(in[i]); + } + return out; + }, + invoke); +} + +// Backward-compatible RGB8 entry point used by the existing unary element-wise tests. +template +void RunVarShapeCorrect(GoldFn gold, InvokeFn invoke) +{ + RunVarShapeCorrectTyped(nvcv::FMT_RGB8, gold, invoke); +} + +// A single in/out tensor pair must be rejected with NVCV_ERROR_INVALID_ARGUMENT. `invoke` runs the +// operator on the tensor pair; the harness owns the stream + ProtectCall plumbing. +// invoke: void(cudaStream_t, const Tensor&, const Tensor&) +template +void ExpectRejected(nvcv::ImageFormat inFmt, nvcv::ImageFormat outFmt, InvokeFn invoke, int width = 32, int height = 24) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + nvcv::Tensor inTensor = nvcv::util::CreateTensor(1, width, height, inFmt); + nvcv::Tensor outTensor = nvcv::util::CreateTensor(1, width, height, outFmt); + + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&] { invoke(stream, inTensor, outTensor); })); + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +template +void ExpectVarShapeRejected(nvcv::ImageFormat fmt, InvokeFn invoke, int width = 32, int height = 24) +{ + nvcv::Image srcImage({width, height}, fmt); + nvcv::Image dstImage({width, height}, fmt); + + nvcv::ImageBatchVarShape src(1); + nvcv::ImageBatchVarShape dst(1); + src.pushBack(srcImage); + dst.pushBack(dstImage); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&] { invoke(stream, src, dst); })); + EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +template +void ExpectZeroExtentTensorNoop(const nvcv::TensorShape &shape, nvcv::DataType dtype, InvokeFn invoke) +{ + std::optional src; + std::optional dst; + try + { + src.emplace(shape, dtype); + dst.emplace(shape, dtype); + } + catch (const nvcv::Exception &e) + { + GTEST_SKIP() << "zero-extent tensors are not constructible: " << e.what(); + } + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + EXPECT_EQ(NVCV_SUCCESS, nvcv::ProtectCall([&] { invoke(stream, *src, *dst); })); + EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +template +void ExpectEmptyVarShapeNoop(InvokeFn invoke) +{ + nvcv::ImageBatchVarShape src(1); + nvcv::ImageBatchVarShape dst(1); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + EXPECT_EQ(NVCV_SUCCESS, nvcv::ProtectCall([&] { invoke(stream, src, dst); })); + EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +} // namespace nvcv::test::elementwise + +#endif // NVCV_TEST_SYSTEM_ELEMENTWISE_OP_HARNESS_HPP diff --git a/tests/cvcuda/system/FlipUtils.cpp b/tests/cvcuda/system/FlipUtils.cpp index 863338656..f5417d5c2 100644 --- a/tests/cvcuda/system/FlipUtils.cpp +++ b/tests/cvcuda/system/FlipUtils.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -27,6 +27,16 @@ namespace nvcv::test { namespace detail { +inline int FlippedX(int x, int width, int flipCode) +{ + return flipCode == 0 ? x : width - 1 - x; +} + +inline int FlippedY(int y, int height, int flipCode) +{ + return flipCode > 0 ? y : height - 1 - y; +} + template inline const T &ValueAt(const std::vector &vec, long3 pitches, int b, int y, int x) { @@ -39,6 +49,13 @@ inline T &ValueAt(std::vector &vec, long3 pitches, int b, int y, int x) return *reinterpret_cast(&vec[b * pitches.x + y * pitches.y + x * pitches.z]); } +template +inline T FlippedValue(const std::vector &hSrc, const long3 &srcStrides, int b, int y, int x, int2 size, + int flipCode) +{ + return ValueAt(hSrc, srcStrides, b, FlippedY(y, size.y, flipCode), FlippedX(x, size.x, flipCode)); +} + template inline void flip(std::vector &hDst, const long3 &dstStrides, const std::vector &hSrc, const long3 &srcStrides, const int3 &shape, int flipCode) @@ -52,21 +69,8 @@ inline void flip(std::vector &hDst, const long3 &dstStrides, const std: { for (int x = 0; x < shape.x; ++x) { - T srcValue; - if (flipCode > 0) - { - srcValue = ValueAt(hSrc, srcStrides, b, y, (size.x - 1 - x)); - } - else if (flipCode == 0) - { - srcValue = ValueAt(hSrc, srcStrides, b, (size.y - 1 - y), x); - } - else - { - srcValue = ValueAt(hSrc, srcStrides, b, (size.y - 1 - y), (size.x - 1 - x)); - } - - ValueAt(hDst, dstStrides, b, y, x) = cuda::SaturateCast(srcValue); + ValueAt(hDst, dstStrides, b, y, x) + = cuda::SaturateCast(FlippedValue(hSrc, srcStrides, b, y, x, size, flipCode)); } } } @@ -94,7 +98,7 @@ void FlipCPU(std::vector &hDst, const long3 &dstStrides, const std::vec { NVCV_ASSERT(format.numPlanes() == 1); - switch (format.planeDataType(0)) + switch (static_cast(format.planeDataType(0))) { #define NVCV_TEST_CASE(DATATYPE, TYPE) \ case NVCV_DATA_TYPE_##DATATYPE: \ diff --git a/tests/cvcuda/system/GaussianNoiseUtils.cu b/tests/cvcuda/system/GaussianNoiseUtils.cu index 63a76e16a..dea433321 100644 --- a/tests/cvcuda/system/GaussianNoiseUtils.cu +++ b/tests/cvcuda/system/GaussianNoiseUtils.cu @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -28,15 +28,31 @@ __global__ void setup_states(curandState *state, unsigned long long seed, int ba curand_init(seed, id, 0, &state[threadIdx.x]); } -__global__ void rand_kernel(curandState *state, float *rand, int size, int per_channel) +__global__ void rand_kernel(curandState *state, float *rand, int size, int per_channel, int channels, int call_index) { int offset = threadIdx.x; curandState localState = state[threadIdx.x]; + for (int call = 0; call < call_index; ++call) + { + int skip_offset = offset; + while (skip_offset < size) + { + if (per_channel) + { + for (int i = 0; i < channels; i++) (void)curand_normal(&localState); + } + else + { + (void)curand_normal(&localState); + } + skip_offset += blockDim.x; + } + } while (offset < size) { if (per_channel) { - for (int i = 0; i < 3; i++) rand[offset * 3 + i] = curand_normal(&localState); + for (int i = 0; i < channels; i++) rand[offset * channels + i] = curand_normal(&localState); } else rand[offset] = curand_normal(&localState); @@ -44,7 +60,7 @@ __global__ void rand_kernel(curandState *state, float *rand, int size, int per_c } } -void get_random(float *rand_h, bool per_channel, int batch, int mem_size) +void get_random(float *rand_h, bool per_channel, int batch, int mem_size, int channels, int call_index) { curandState *states; cudaMalloc((void **)&states, sizeof(curandState) * BLOCK); @@ -54,8 +70,8 @@ void get_random(float *rand_h, bool per_channel, int batch, int mem_size) cudaMalloc((void **)&rand_d, sizeof(float) * mem_size); int img_size = mem_size; if (per_channel) - img_size /= 3; - rand_kernel<<<1, BLOCK>>>(states, rand_d, img_size, per_channel); + img_size /= channels; + rand_kernel<<<1, BLOCK>>>(states, rand_d, img_size, per_channel, channels, call_index); cudaMemcpy(rand_h, rand_d, mem_size * sizeof(float), cudaMemcpyDeviceToHost); cudaFree(states); cudaFree(rand_d); diff --git a/tests/cvcuda/system/GaussianNoiseUtils.cuh b/tests/cvcuda/system/GaussianNoiseUtils.cuh index 1d0dc39fc..8d86b4d02 100644 --- a/tests/cvcuda/system/GaussianNoiseUtils.cuh +++ b/tests/cvcuda/system/GaussianNoiseUtils.cuh @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,6 +18,6 @@ #ifndef NVCV_TEST_GAUSSIAN_NOISE_UTILS_HPP #define NVCV_TEST_GAUSSIAN_NOISE_UTILS_HPP -void get_random(float *rand_h, bool per_channel, int batch, int mem_size); +void get_random(float *rand_h, bool per_channel, int batch, int mem_size, int channels, int call_index = 0); #endif // NVCV_TEST_GAUSSIAN_NOISE_UTILS_HPP diff --git a/tests/cvcuda/system/OsdUtils.cu b/tests/cvcuda/system/OsdUtils.cu deleted file mode 100644 index 1fbf4d003..000000000 --- a/tests/cvcuda/system/OsdUtils.cu +++ /dev/null @@ -1,248 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed 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. - */ - -#include "OsdUtils.cuh" - -#include -#include - -#include -#include - -namespace nvcv::test { namespace osd { - -#define checkRuntime(call) check_runtime(call, #call, __LINE__, __FILE__) - -inline static bool check_runtime(cudaError_t e, const char *call, int line, const char *file) -{ - if (e != cudaSuccess) - { - fprintf(stderr, "CUDA Runtime error %s # %s, code = %s [ %d ] in file %s:%d\n", call, cudaGetErrorString(e), - cudaGetErrorName(e), e, file, line); - return false; - } - return true; -} - -// Create image using size and format -Image *create_image(int width, int height, ImageFormat format) -{ - Image *output = new Image(); - output->width = width; - output->height = height; - output->format = format; - - if (format == ImageFormat::RGB) - { - output->stride = output->width * 3; - checkRuntime(cudaMalloc(&output->data0, output->stride * output->height)); - } - else if (format == ImageFormat::RGBA) - { - output->stride = output->width * 4; - checkRuntime(cudaMalloc(&output->data0, output->stride * output->height)); - } - else if (format == ImageFormat::PitchLinearNV12) - { - output->stride = output->width; - if (output->width % 2 != 0 || output->height % 2 != 0) - { - fprintf(stderr, "Invalid image size(%d, %d) for NV12\n", output->width, output->height); - delete output; - return nullptr; - } - checkRuntime(cudaMalloc(&output->data0, output->stride * output->height)); - checkRuntime(cudaMalloc(&output->data1, output->stride * output->height / 2)); - } - else if (format == ImageFormat::BlockLinearNV12) - { - output->stride = output->width; - if (output->width % 2 != 0 || output->height % 2 != 0) - { - fprintf(stderr, "Invalid image size(%d, %d) for NV12\n", output->width, output->height); - delete output; - return nullptr; - } - cudaChannelFormatDesc planeDesc = cudaCreateChannelDesc(8, 0, 0, 0, cudaChannelFormatKindUnsigned); - checkRuntime(cudaMallocArray((cudaArray_t *)&output->reserve0, &planeDesc, output->stride, height)); - checkRuntime(cudaMallocArray((cudaArray_t *)&output->reserve1, &planeDesc, output->stride, height / 2)); - - cudaResourceDesc luma_desc = {}; - luma_desc.resType = cudaResourceTypeArray; - luma_desc.res.array.array = (cudaArray_t)output->reserve0; - checkRuntime(cudaCreateSurfaceObject((cudaSurfaceObject_t *)&output->data0, &luma_desc)); - - cudaResourceDesc chroma_desc = {}; - chroma_desc.resType = cudaResourceTypeArray; - chroma_desc.res.array.array = (cudaArray_t)output->reserve1; - checkRuntime(cudaCreateSurfaceObject((cudaSurfaceObject_t *)&output->data1, &chroma_desc)); - } - else - { - fprintf(stderr, "Unsupport format %d\n", (int)format); - delete output; - output = nullptr; - } - return output; -} - -Segment *create_segment() -{ - Segment *output = new Segment(); - output->width = 10; - output->height = 10; - output->data = (float *)malloc(output->width * output->height * sizeof(float)); - std::vector diamond; - diamond.insert(diamond.end(), {0, 0, 0, 0, 0.2, 0.2, 0, 0, 0, 0}); - diamond.insert(diamond.end(), {0, 0, 0, 0.2, 0.3, 0.3, 0.2, 0, 0, 0}); - diamond.insert(diamond.end(), {0, 0, 0.2, 0.3, 0.4, 0.4, 0.3, 0.2, 0, 0}); - diamond.insert(diamond.end(), {0, 0.2, 0.3, 0.4, 0.5, 0.5, 0.4, 0.3, 0.2, 0}); - diamond.insert(diamond.end(), {0.2, 0.3, 0.4, 0.5, 0.5, 0.5, 0.5, 0.4, 0.3, 0.2}); - diamond.insert(diamond.end(), {0.2, 0.3, 0.4, 0.5, 0.5, 0.5, 0.5, 0.4, 0.3, 0.2}); - diamond.insert(diamond.end(), {0, 0.2, 0.3, 0.4, 0.5, 0.5, 0.4, 0.3, 0.2, 0}); - diamond.insert(diamond.end(), {0, 0, 0.2, 0.3, 0.4, 0.4, 0.3, 0.2, 0, 0}); - diamond.insert(diamond.end(), {0, 0, 0, 0.2, 0.3, 0.3, 0.2, 0, 0, 0}); - diamond.insert(diamond.end(), {0, 0, 0, 0, 0.2, 0.2, 0, 0, 0, 0}); - memcpy(output->data, diamond.data(), output->width * output->height * sizeof(float)); - return output; -} - -void free_segment(Segment *segment) -{ - if (segment->data != nullptr) - { - free(segment->data); - segment->data = nullptr; - } - segment->width = 0; - segment->height = 0; - delete (segment); -} - -Polyline *create_polyline() -{ - Polyline *output = new Polyline(); - std::vector points; - points.push_back(Point({100, 100})); - points.push_back(Point({600, 100})); - points.push_back(Point({350, 300})); - points.push_back(Point({600, 500})); - points.push_back(Point({300, 500})); - - output->n_pts = points.size(); - output->h_pts = (int *)malloc(output->n_pts * 2 * sizeof(int)); - memcpy(output->h_pts, points.data(), output->n_pts * 2 * sizeof(int)); - return output; -} - -void free_polyline(Polyline *polyline) -{ - if (polyline->h_pts) - { - free(polyline->h_pts); - } - polyline->n_pts = 0; - delete (polyline); -} - -// Free image pointer -void free_image(Image *image) -{ - if (image == nullptr) - return; - - if (image->format == ImageFormat::RGB) - { - if (image->data0) - checkRuntime(cudaFree(image->data0)); - } - else if (image->format == ImageFormat::RGBA) - { - if (image->data0) - checkRuntime(cudaFree(image->data0)); - } - else if (image->format == ImageFormat::PitchLinearNV12) - { - if (image->data0) - checkRuntime(cudaFree(image->data0)); - if (image->data1) - checkRuntime(cudaFree(image->data1)); - } - else if (image->format == ImageFormat::BlockLinearNV12) - { - if (image->data0) - checkRuntime(cudaDestroySurfaceObject((cudaSurfaceObject_t)image->data0)); - if (image->data1) - checkRuntime(cudaDestroySurfaceObject((cudaSurfaceObject_t)image->data1)); - if (image->reserve0) - checkRuntime(cudaFreeArray((cudaArray_t)image->reserve0)); - if (image->reserve1) - checkRuntime(cudaFreeArray((cudaArray_t)image->reserve1)); - } - delete image; -} - -void cuosd_apply(cuOSDContext_t context, Image *image, void *_stream, bool launch) -{ - cudaStream_t stream = (cudaStream_t)_stream; - - cuOSDImageFormat format = cuOSDImageFormat::None; - if (image->format == ImageFormat::RGB) - { - format = cuOSDImageFormat::RGB; - } - else if (image->format == ImageFormat::RGBA) - { - format = cuOSDImageFormat::RGBA; - } - else if (image->format == ImageFormat::PitchLinearNV12) - { - format = cuOSDImageFormat::PitchLinearNV12; - } - else if (image->format == ImageFormat::BlockLinearNV12) - { - format = cuOSDImageFormat::BlockLinearNV12; - } - cuosd_apply(context, image->data0, image->data1, image->width, image->stride, image->height, format, stream, - launch); -} - -void cuosd_launch(cuOSDContext_t context, Image *image, void *_stream) -{ - cudaStream_t stream = (cudaStream_t)_stream; - - cuOSDImageFormat format = cuOSDImageFormat::None; - if (image->format == ImageFormat::RGB) - { - format = cuOSDImageFormat::RGB; - } - else if (image->format == ImageFormat::RGBA) - { - format = cuOSDImageFormat::RGBA; - } - else if (image->format == ImageFormat::PitchLinearNV12) - { - format = cuOSDImageFormat::PitchLinearNV12; - } - else if (image->format == ImageFormat::BlockLinearNV12) - { - format = cuOSDImageFormat::BlockLinearNV12; - } - cuosd_launch(context, image->data0, image->data1, image->width, image->stride, image->height, format, stream); -} - -}} // namespace nvcv::test::osd diff --git a/tests/cvcuda/system/OsdUtils.cuh b/tests/cvcuda/system/OsdUtils.cuh deleted file mode 100644 index 1686d44ca..000000000 --- a/tests/cvcuda/system/OsdUtils.cuh +++ /dev/null @@ -1,96 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed 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. - */ - -#ifndef NVCV_TEST_COMMON_OSD_UTILS_HPP -#define NVCV_TEST_COMMON_OSD_UTILS_HPP - -#include "cuosd.h" - -namespace nvcv::test { namespace osd { - -enum class ImageFormat : int -{ - None = 0, - RGB = 1, - RGBA = 2, - BlockLinearNV12 = 3, - PitchLinearNV12 = 4 -}; - -struct Image -{ - void *data0 = nullptr; - void *data1 = nullptr; - void *reserve0 = nullptr; - void *reserve1 = nullptr; - int width = 0; - int height = 0; - int stride = 0; - ImageFormat format = ImageFormat::None; -}; - -struct Segment -{ - float *data = nullptr; - int width = 0; - int height = 0; -}; - -struct Point -{ - int x = 0; - int y = 0; -}; - -struct Polyline -{ - int *h_pts = nullptr; - int n_pts = 0; -}; - -void cuosd_apply(cuOSDContext_t context, Image *image, void *_stream, bool launch = true); - -void cuosd_launch(cuOSDContext_t context, Image *image, void *_stream); - -// Get name of enumerate type -const char *image_format_name(ImageFormat format); - -// Create gpu image using size and format -Image *create_image(int width, int height, ImageFormat format); - -// Create segment with fixed size 10 x 10 -Segment *create_segment(); - -// Create polyline for test -Polyline *create_polyline(); - -// Set image color -void set_color(Image *image, unsigned char r, unsigned char g, unsigned char b, unsigned char a = 255, - void *_stream = nullptr); - -// Free image pointer -void free_image(Image *image); - -// Free segment pointer -void free_segment(Segment *segment); - -// Free polyline pointer -void free_polyline(Polyline *segment); - -}} // namespace nvcv::test::osd - -#endif // NVCV_TEST_COMMON_OSD_UTILS_HPP diff --git a/tests/cvcuda/system/PlanarParityUtils.hpp b/tests/cvcuda/system/PlanarParityUtils.hpp new file mode 100644 index 000000000..21022a125 --- /dev/null +++ b/tests/cvcuda/system/PlanarParityUtils.hpp @@ -0,0 +1,479 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#ifndef NVCV_TEST_PLANAR_PARITY_UTILS_HPP +#define NVCV_TEST_PLANAR_PARITY_UTILS_HPP + +// Shared scaffolding for "planar matches interleaved" parity tests. +// +// An operator that treats channels independently must produce byte-for-byte the same pixels in a +// planar (NCHW/CHW) layout as in the interleaved ((N)HWC) layout. These helpers feed identical data +// through an operator in both layouts and assert the (re-interleaved) planar output equals the +// interleaved output exactly. The op call is supplied as a lambda so every operator reuses the same +// upload/run/download/compare flow; see TestOpResize.cpp and TestOpFlip.cpp for usage. + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace nvcv::test::planar { + +// Deinterleave HWC bytes into C contiguous single-channel planes (no row padding). +inline std::vector DeinterleaveToPlanes(const std::vector &hwc, int w, int h, int channels, + int elemSize) +{ + std::vector planes(hwc.size()); + const int planeBytes = w * h * elemSize; + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + for (int c = 0; c < channels; ++c) + std::memcpy(&planes[c * planeBytes + (y * w + x) * elemSize], + &hwc[((y * w + x) * channels + c) * elemSize], elemSize); + return planes; +} + +// Inverse of DeinterleaveToPlanes. +inline std::vector InterleaveFromPlanes(const std::vector &planes, int w, int h, int channels, + int elemSize) +{ + std::vector hwc(planes.size()); + const int planeBytes = w * h * elemSize; + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + for (int c = 0; c < channels; ++c) + std::memcpy(&hwc[((y * w + x) * channels + c) * elemSize], + &planes[c * planeBytes + (y * w + x) * elemSize], elemSize); + return hwc; +} + +template +inline void FillDeterministicValues(std::vector &buf, size_t seed) +{ + ASSERT_EQ(size_t{0}, buf.size() % sizeof(T)); + for (size_t k = 0; k < buf.size() / sizeof(T); ++k) + { + const auto pattern = static_cast((k * 31 + seed) % 251); + T value; + if constexpr (std::is_floating_point_v) + { + value = static_cast((pattern + 1) / 252.0); + } + else if constexpr (std::is_signed_v) + { + value = static_cast(pattern % 127 - 63); + } + else + { + value = static_cast(pattern); + } + std::memcpy(buf.data() + k * sizeof(T), &value, sizeof(T)); + } +} + +// Fill the logical input with bounded values of the actual scalar type. In particular, do not +// reinterpret arbitrary bytes as floats: filter, resize, and normalization operators consume these +// values arithmetically, and NaNs or near-FLT_MAX inputs turn parity into an overflow comparison. +inline void FillDeterministicValues(std::vector &buf, size_t seed, nvcv::DataType dtype) +{ + switch (static_cast(dtype)) + { + case NVCV_DATA_TYPE_U8: + FillDeterministicValues(buf, seed); + break; + case NVCV_DATA_TYPE_S8: + FillDeterministicValues(buf, seed); + break; + case NVCV_DATA_TYPE_U16: + FillDeterministicValues(buf, seed); + break; + case NVCV_DATA_TYPE_S16: + FillDeterministicValues(buf, seed); + break; + case NVCV_DATA_TYPE_U32: + FillDeterministicValues(buf, seed); + break; + case NVCV_DATA_TYPE_S32: + FillDeterministicValues(buf, seed); + break; + case NVCV_DATA_TYPE_F32: + FillDeterministicValues(buf, seed); + break; + case NVCV_DATA_TYPE_F64: + FillDeterministicValues(buf, seed); + break; + default: + FAIL() << "Unsupported planar parity scalar dtype " << static_cast(dtype); + } +} + +// Upload an interleaved HWC host buffer into one sample of a tensor (interleaved or single channel). +inline void UploadInterleavedSample(const nvcv::TensorDataAccessStridedImagePlanar &access, int sample, + const std::vector &hwc, int /*w*/, int h, int rowStrideBytes) +{ + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(access.sampleData(sample), access.rowStride(), hwc.data(), rowStrideBytes, + rowStrideBytes, h, cudaMemcpyHostToDevice)); +} + +// Upload C contiguous host planes into one planar (NCHW/CHW) sample of a tensor. +inline void UploadPlanarSample(const nvcv::TensorDataAccessStridedImagePlanar &access, int sample, + const std::vector &planes, int w, int h, int channels, int elemSize) +{ + const int planeBytes = w * h * elemSize; + for (int c = 0; c < channels; ++c) + { + ASSERT_EQ(cudaSuccess, + cudaMemcpy2D(access.sampleData(sample) + c * access.chStride(), access.rowStride(), + planes.data() + c * planeBytes, w * elemSize, w * elemSize, h, cudaMemcpyHostToDevice)); + } +} + +// Download one interleaved sample of a tensor into an HWC host buffer. +inline std::vector DownloadInterleavedSample(const nvcv::TensorDataAccessStridedImagePlanar &access, + int sample, int /*w*/, int h, int rowStrideBytes) +{ + std::vector hwc(h * rowStrideBytes); + EXPECT_EQ(cudaSuccess, cudaMemcpy2D(hwc.data(), rowStrideBytes, access.sampleData(sample), access.rowStride(), + rowStrideBytes, h, cudaMemcpyDeviceToHost)); + return hwc; +} + +// Download one planar sample of a tensor into C contiguous host planes. +inline std::vector DownloadPlanarSample(const nvcv::TensorDataAccessStridedImagePlanar &access, int sample, + int w, int h, int channels, int elemSize) +{ + std::vector planes(w * h * channels * elemSize); + const int planeBytes = w * h * elemSize; + for (int c = 0; c < channels; ++c) + { + EXPECT_EQ(cudaSuccess, cudaMemcpy2D(planes.data() + c * planeBytes, w * elemSize, + access.sampleData(sample) + c * access.chStride(), access.rowStride(), + w * elemSize, h, cudaMemcpyDeviceToHost)); + } + return planes; +} + +// Upload one value per image into a tensor shaped ({{numImages}, "N"}, dtype). Used by var-shape +// filter ops whose per-sample parameters (e.g. kernel size / anchor int2 tensors) are passed as +// N-length tensors. Shared so per-op planar tests do not re-implement the upload boilerplate. +template +inline void UploadTensorValues(nvcv::Tensor &tensor, const std::vector &values) +{ + auto dev = tensor.exportData(); + ASSERT_NE(dev, nullptr); + ASSERT_EQ(cudaSuccess, + cudaMemcpy(dev->basePtr(), values.data(), values.size() * sizeof(T), cudaMemcpyHostToDevice)); +} + +// Create an N-length parameter tensor with the same `value` for every image and upload it. +template +inline nvcv::Tensor MakePerImageTensor(int numImages, nvcv::DataType dtype, const T &value) +{ + nvcv::Tensor tensor({{numImages}, "N"}, dtype); + UploadTensorValues(tensor, std::vector(numImages, value)); + return tensor; +} + +// Run the same data through `invoke` in interleaved and planar tensor layouts; require bit-exact +// outputs. `invoke(stream, src, dst, fmt)` performs the operator call (the caller binds op-specific +// parameters); `fmt` is that pair's image format, for ops that need it (e.g. workspace sizing). +// `srcW/srcH` is the input size, `dstW/dstH` the output size (equal for size-preserving ops). The +// data is uploaded at the input size and the result compared at the output size. +template +inline void RunTensorParity(nvcv::ImageFormat planarFmt, nvcv::ImageFormat interleavedFmt, int srcW, int srcH, int dstW, + int dstH, int numImages, OpInvoke &&invoke) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + const int channels = planarFmt.numChannels(); + const int elemSize = planarFmt.planePixelStrideBytes(0); + const int srcRowStr = srcW * channels * elemSize; + const int dstRowStr = dstW * channels * elemSize; + + nvcv::Tensor srcI = nvcv::util::CreateTensor(numImages, srcW, srcH, interleavedFmt); + nvcv::Tensor dstI = nvcv::util::CreateTensor(numImages, dstW, dstH, interleavedFmt); + nvcv::Tensor srcP = nvcv::util::CreateTensor(numImages, srcW, srcH, planarFmt); + nvcv::Tensor dstP = nvcv::util::CreateTensor(numImages, dstW, dstH, planarFmt); + + auto srcIData = srcI.exportData(); + auto dstIData = dstI.exportData(); + auto srcPData = srcP.exportData(); + auto dstPData = dstP.exportData(); + ASSERT_TRUE(srcIData && dstIData && srcPData && dstPData); + ASSERT_TRUE(srcPData->layout() == nvcv::TENSOR_NCHW || srcPData->layout() == nvcv::TENSOR_CHW); + + auto srcIAcc = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcIData); + auto dstIAcc = nvcv::TensorDataAccessStridedImagePlanar::Create(*dstIData); + auto srcPAcc = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcPData); + auto dstPAcc = nvcv::TensorDataAccessStridedImagePlanar::Create(*dstPData); + ASSERT_TRUE(srcIAcc && dstIAcc && srcPAcc && dstPAcc); + + for (int i = 0; i < numImages; ++i) + { + std::vector hwc(srcH * srcRowStr); + FillDeterministicValues(hwc, static_cast(i) * 101 + 1, planarFmt.planeDataType(0)); + + UploadInterleavedSample(*srcIAcc, i, hwc, srcW, srcH, srcRowStr); + UploadPlanarSample(*srcPAcc, i, DeinterleaveToPlanes(hwc, srcW, srcH, channels, elemSize), srcW, srcH, channels, + elemSize); + + // Different sentinels ensure a missing write cannot pass merely because both output allocations + // happen to contain the same bytes. + UploadInterleavedSample(*dstIAcc, i, std::vector(dstH * dstRowStr, 0xA5), dstW, dstH, dstRowStr); + UploadPlanarSample(*dstPAcc, i, std::vector(dstH * dstRowStr, 0x5A), dstW, dstH, channels, elemSize); + } + + invoke(stream, srcI, dstI, interleavedFmt); + invoke(stream, srcP, dstP, planarFmt); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + for (int i = 0; i < numImages; ++i) + { + SCOPED_TRACE(i); + auto gpuInter = DownloadInterleavedSample(*dstIAcc, i, dstW, dstH, dstRowStr); + auto planesOut = DownloadPlanarSample(*dstPAcc, i, dstW, dstH, channels, elemSize); + auto planarInter = InterleaveFromPlanes(planesOut, dstW, dstH, channels, elemSize); + + EXPECT_EQ(gpuInter, planarInter); + } + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +// Var-shape counterpart of RunTensorParity. `invoke(stream, batchSrc, batchDst)` performs the +// operator call on the two image batches. +template +inline void RunVarShapeParity(nvcv::ImageFormat planarFmt, nvcv::ImageFormat interleavedFmt, int srcW, int srcH, + int dstW, int dstH, int numImages, OpInvoke &&invoke) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + const int channels = planarFmt.numChannels(); + const int elemSize = planarFmt.planePixelStrideBytes(0); + const int srcRowStr = srcW * channels * elemSize; + + std::vector srcI; + std::vector dstI; + std::vector srcP; + std::vector dstP; + for (int i = 0; i < numImages; ++i) + { + srcI.emplace_back(nvcv::Size2D{srcW, srcH}, interleavedFmt); + dstI.emplace_back(nvcv::Size2D{dstW, dstH}, interleavedFmt); + srcP.emplace_back(nvcv::Size2D{srcW, srcH}, planarFmt); + dstP.emplace_back(nvcv::Size2D{dstW, dstH}, planarFmt); + } + + nvcv::ImageBatchVarShape batchSrcI(numImages); + nvcv::ImageBatchVarShape batchDstI(numImages); + nvcv::ImageBatchVarShape batchSrcP(numImages); + nvcv::ImageBatchVarShape batchDstP(numImages); + batchSrcI.pushBack(srcI.begin(), srcI.end()); + batchDstI.pushBack(dstI.begin(), dstI.end()); + batchSrcP.pushBack(srcP.begin(), srcP.end()); + batchDstP.pushBack(dstP.begin(), dstP.end()); + + for (int i = 0; i < numImages; ++i) + { + std::vector hwc(srcH * srcRowStr); + FillDeterministicValues(hwc, static_cast(i) * 101 + 7, planarFmt.planeDataType(0)); + + auto idata = srcI[i].exportData(); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(idata->plane(0).basePtr, idata->plane(0).rowStride, hwc.data(), srcRowStr, + srcRowStr, srcH, cudaMemcpyHostToDevice)); + + auto planes = DeinterleaveToPlanes(hwc, srcW, srcH, channels, elemSize); + auto pdata = srcP[i].exportData(); + const int planeBytes = srcW * srcH * elemSize; + ASSERT_EQ(pdata->numPlanes(), channels); + for (int c = 0; c < channels; ++c) + { + ASSERT_EQ(cudaSuccess, + cudaMemcpy2D(pdata->plane(c).basePtr, pdata->plane(c).rowStride, planes.data() + c * planeBytes, + srcW * elemSize, srcW * elemSize, srcH, cudaMemcpyHostToDevice)); + } + + const int dstRowStr = dstW * channels * elemSize; + const int dstPlaneByt = dstW * dstH * elemSize; + std::vector interleavedCanary(dstH * dstRowStr, 0xA5); + auto dstIData = dstI[i].exportData(); + ASSERT_EQ(cudaSuccess, + cudaMemcpy2D(dstIData->plane(0).basePtr, dstIData->plane(0).rowStride, interleavedCanary.data(), + dstRowStr, dstRowStr, dstH, cudaMemcpyHostToDevice)); + + std::vector planarCanary(dstPlaneByt, 0x5A); + auto dstPData = dstP[i].exportData(); + ASSERT_EQ(dstPData->numPlanes(), channels); + for (int c = 0; c < channels; ++c) + { + ASSERT_EQ(cudaSuccess, + cudaMemcpy2D(dstPData->plane(c).basePtr, dstPData->plane(c).rowStride, planarCanary.data(), + dstW * elemSize, dstW * elemSize, dstH, cudaMemcpyHostToDevice)); + } + } + + invoke(stream, batchSrcI, batchDstI, interleavedFmt); + invoke(stream, batchSrcP, batchDstP, planarFmt); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + const int dstRowStr = dstW * channels * elemSize; + const int dstPlaneByt = dstW * dstH * elemSize; + for (int i = 0; i < numImages; ++i) + { + SCOPED_TRACE(i); + std::vector gpuInter(dstH * dstRowStr); + auto idata = dstI[i].exportData(); + EXPECT_EQ(cudaSuccess, cudaMemcpy2D(gpuInter.data(), dstRowStr, idata->plane(0).basePtr, + idata->plane(0).rowStride, dstRowStr, dstH, cudaMemcpyDeviceToHost)); + + std::vector planesOut(dstW * dstH * channels * elemSize); + auto pdata = dstP[i].exportData(); + for (int c = 0; c < channels; ++c) + { + EXPECT_EQ(cudaSuccess, + cudaMemcpy2D(planesOut.data() + c * dstPlaneByt, dstW * elemSize, pdata->plane(c).basePtr, + pdata->plane(c).rowStride, dstW * elemSize, dstH, cudaMemcpyDeviceToHost)); + } + auto planarInter = InterleaveFromPlanes(planesOut, dstW, dstH, channels, elemSize); + + EXPECT_EQ(gpuInter, planarInter); + } + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +// Build two NCHW U8 tensors with the given {N, C, H, W} extents, invoke `invoke(stream, in, out)`, +// and require the call to fail with NVCV_ERROR_INVALID_ARGUMENT. Shared by the crop operators' +// negative planar tests, which reject 2-channel planar input and sample/channel-count mismatches +// before the N*C single-channel flattening. `invoke` binds the operator and its crop geometry. +template +inline void ExpectPlanarTensorRejected(const std::array &inNCHW, const std::array &outNCHW, + Invoke &&invoke) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + nvcv::Tensor imgIn( + { + {inNCHW[0], inNCHW[1], inNCHW[2], inNCHW[3]}, + "NCHW" + }, + nvcv::TYPE_U8); + nvcv::Tensor imgOut( + { + {outNCHW[0], outNCHW[1], outNCHW[2], outNCHW[3]}, + "NCHW" + }, + nvcv::TYPE_U8); + + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&] { invoke(stream, imgIn, imgOut); })); + + EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +// Var-shape counterpart of ExpectPlanarTensorRejected: build src/dst image-batch var-shapes with the +// given per-image formats (sizes are arbitrary — negative cases must be rejected regardless of size) +// and require `invoke(stream, batchSrc, batchDst)` to fail with NVCV_ERROR_INVALID_ARGUMENT. The +// operator and its per-sample parameter tensors are bound by the caller's `invoke`. Shared so the +// filter ops' var-shape negative tests do not each re-implement the batch-build/run/expect flow. +template +inline void ExpectVarShapeRejected(const std::vector &srcFmts, + const std::vector &dstFmts, Invoke &&invoke) +{ + ASSERT_EQ(srcFmts.size(), dstFmts.size()); + const auto numImages = static_cast(srcFmts.size()); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + std::vector imgSrc; + std::vector imgDst; + for (int i = 0; i < numImages; ++i) + { + imgSrc.emplace_back(nvcv::Size2D{32, 32}, srcFmts[i]); + imgDst.emplace_back(nvcv::Size2D{32, 32}, dstFmts[i]); + } + + nvcv::ImageBatchVarShape batchSrc(numImages); + nvcv::ImageBatchVarShape batchDst(numImages); + batchSrc.pushBack(imgSrc.begin(), imgSrc.end()); + batchDst.pushBack(imgDst.begin(), imgDst.end()); + + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&] { invoke(stream, batchSrc, batchDst); })); + + EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +// Var-shape negative case for a filter op: a uniform batch of `batches` images all in +// (srcFmt -> dstFmt) is expected to be rejected. `invoke(stream, src, dst, maxBatches, borderMode)` +// builds the op (with the given maxBatches) and its per-image parameter tensors and runs it with the +// given border. `borderMode` is threaded so the sanitizer out-of-range-border negative row is still +// exercised. Shared so planar-capable filter ops do not each re-implement the batch-build/run/reject +// flow. +template +inline void ExpectVarShapeUniformFormatRejected(nvcv::ImageFormat srcFmt, nvcv::ImageFormat dstFmt, int batches, + int maxBatches, NVCVBorderType borderMode, Invoke &&invoke) +{ + ExpectVarShapeRejected(std::vector(batches, srcFmt), + std::vector(batches, dstFmt), + [&invoke, maxBatches, borderMode](cudaStream_t s, const nvcv::ImageBatchVarShape &src, + const nvcv::ImageBatchVarShape &dst) + { invoke(s, src, dst, maxBatches, borderMode); }); +} + +// Var-shape negative case for a filter op: a batch of 3 images where one image has a mismatched +// format (covering both U8/RGB8 mismatch directions) is expected to be rejected. `invoke` is as above. +template +inline void ExpectVarShapeMixedFormatRejected(Invoke &&invoke) +{ + const nvcv::ImageFormat fmt = nvcv::FMT_RGB8; + const int batches = 3; + for (auto [inputFmtExtra, outputFmtExtra] : std::vector>{ + {nvcv::FMT_U8, fmt}, + { fmt, nvcv::FMT_U8} + }) + { + std::vector srcFmts(batches - 1, fmt); + std::vector dstFmts(batches - 1, fmt); + srcFmts.push_back(inputFmtExtra); + dstFmts.push_back(outputFmtExtra); + ExpectVarShapeRejected( + srcFmts, dstFmts, + [&invoke](cudaStream_t s, const nvcv::ImageBatchVarShape &src, const nvcv::ImageBatchVarShape &dst) + { invoke(s, src, dst, batches, NVCV_BORDER_CONSTANT); }); + } +} + +} // namespace nvcv::test::planar + +#endif // NVCV_TEST_PLANAR_PARITY_UTILS_HPP diff --git a/tests/cvcuda/system/ResizeUtils.cpp b/tests/cvcuda/system/ResizeUtils.cpp index 2dfbd9ad8..0fc610977 100644 --- a/tests/cvcuda/system/ResizeUtils.cpp +++ b/tests/cvcuda/system/ResizeUtils.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -24,16 +24,194 @@ #include // for BaseType, etc. #include // for NVCV_ASSERT, etc. +#include +#include #include namespace nvcv::test { +static bool InBounds(int y, int x, nvcv::Size2D size) +{ + return y >= 0 && y < size.h && x >= 0 && x < size.w; +} + +template +static void AccumulateArea(double &out, const T *srcPtr, int srcStep, int channels, int c, nvcv::Size2D srcSize, int y, + int x, double weight) +{ + if (InBounds(y, x, srcSize)) + { + out += srcPtr[y * srcStep + x * channels + c] * weight; + } +} + +template +static double IntegerAreaPixel(const T *srcPtr, int srcStep, nvcv::Size2D srcSize, int channels, int c, int sy1, + int sy2, int sx1, int sx2, double invScale) +{ + double out = 0.0; + for (int y = sy1; y < sy2; ++y) + { + for (int x = sx1; x < sx2; ++x) + { + AccumulateArea(out, srcPtr, srcStep, channels, c, srcSize, y, x, invScale); + } + } + return out; +} + +template +static void AccumulateAreaRow(double &out, const T *srcPtr, int srcStep, nvcv::Size2D srcSize, int channels, int c, + int y, int sx1, int sx2, double weight) +{ + for (int x = sx1; x < sx2; ++x) + { + AccumulateArea(out, srcPtr, srcStep, channels, c, srcSize, y, x, weight); + } +} + +template +static double FractionalAreaPixel(const T *srcPtr, int srcStep, nvcv::Size2D srcSize, int channels, int c, int sy1, + int sy2, int sx1, int sx2, double fsy1, double fsy2, double fsx1, double fsx2, + double invScale) +{ + double out = 0.0; + for (int y = sy1; y < sy2; ++y) + { + AccumulateAreaRow(out, srcPtr, srcStep, srcSize, channels, c, y, sx1, sx2, invScale); + + if (sx1 > fsx1) + { + AccumulateArea(out, srcPtr, srcStep, channels, c, srcSize, y, sx1 - 1, (sx1 - fsx1) * invScale); + } + + if (sx2 < fsx2) + { + AccumulateArea(out, srcPtr, srcStep, channels, c, srcSize, y, sx2, (fsx2 - sx2) * invScale); + } + } + + if (sy1 > fsy1) + { + AccumulateAreaRow(out, srcPtr, srcStep, srcSize, channels, c, sy1 - 1, sx1, sx2, (sy1 - fsy1) * invScale); + } + + if (sy2 < fsy2) + { + AccumulateAreaRow(out, srcPtr, srcStep, srcSize, channels, c, sy2, sx1, sx2, (fsy2 - sy2) * invScale); + } + + if (sy1 > fsy1 && sx1 > fsx1) + { + AccumulateArea(out, srcPtr, srcStep, channels, c, srcSize, sy1 - 1, sx1 - 1, + (sy1 - fsy1) * (sx1 - fsx1) * invScale); + } + + if (sy1 > fsy1 && sx2 < fsx2) + { + AccumulateArea(out, srcPtr, srcStep, channels, c, srcSize, sy1 - 1, sx2, + (sy1 - fsy1) * (fsx2 - sx2) * invScale); + } + + if (sy2 < fsy2 && sx2 < fsx2) + { + AccumulateArea(out, srcPtr, srcStep, channels, c, srcSize, sy2, sx2, (fsy2 - sy2) * (fsx2 - sx2) * invScale); + } + + if (sy2 < fsy2 && sx1 > fsx1) + { + AccumulateArea(out, srcPtr, srcStep, channels, c, srcSize, sy2, sx1 - 1, + (fsy2 - sy2) * (sx1 - fsx1) * invScale); + } + + return out; +} + +template +static double VarShapeZoomAreaPixel(const T *srcPtr, int srcStep, nvcv::Size2D srcSize, int channels, int c, int dstY, + int dstX, double scaleH, double scaleW, double fsy1, double fsx1) +{ + double scaleHInv = 1.0 / scaleH; + double scaleWInv = 1.0 / scaleW; + + int sx1 = cuda::round(fsx1); + int sy1 = cuda::round(fsy1); + + auto fy = static_cast(dstY + 1) - static_cast(sy1 + 1) * scaleHInv; + fy = fy <= 0.0 ? 0.0 : fy - static_cast(cuda::round(fy)); + + auto fx = static_cast(dstX + 1) - static_cast(sx1 + 1) * scaleWInv; + fx = fx <= 0.0 ? 0.0 : fx - static_cast(cuda::round(fx)); + + if (sx1 < 0) + { + fx = 0; + sx1 = 0; + } + if (sx1 >= srcSize.w - 1) + { + fx = 0; + sx1 = srcSize.w - 2; + } + if (sy1 >= srcSize.h - 1) + { + sy1 = srcSize.h - 2; + } + + std::array cbufx = {1.0 - fx, fx}; + std::array cbufy = {1.0 - fy, fy}; + + return srcPtr[sy1 * srcStep + sx1 * channels + c] * cbufx[0] * cbufy[0] + + srcPtr[(sy1 + 1) * srcStep + sx1 * channels + c] * cbufx[0] * cbufy[1] + + srcPtr[sy1 * srcStep + (sx1 + 1) * channels + c] * cbufx[1] * cbufy[0] + + srcPtr[(sy1 + 1) * srcStep + (sx1 + 1) * channels + c] * cbufx[1] * cbufy[1]; +} + +template +static T ClampResizeValue(double out, T minVal, T maxVal) +{ + if (std::numeric_limits::is_integer) + { + out = std::rint(std::numeric_limits::is_signed ? out : std::abs(out)); + } + + if (out < static_cast(minVal)) + { + return minVal; + } + if (out > static_cast(maxVal)) + { + return maxVal; + } + return static_cast(out); +} + +template +static double AreaResizePixel(const T *srcPtr, int srcStep, nvcv::Size2D srcSize, int channels, int c, int dstY, + int dstX, int sy1, int sy2, int sx1, int sx2, double fsy1, double fsy2, double fsx1, + double fsx2, double scaleH, double scaleW, bool isVarshape) +{ + if (std::ceil(scaleW) == scaleW && std::ceil(scaleH) == scaleH) + { + return IntegerAreaPixel(srcPtr, srcStep, srcSize, channels, c, sy1, sy2, sx1, sx2, 1.f / (scaleW * scaleH)); + } + + if (!isVarshape || (scaleH >= 1.0f && scaleW >= 1.0f)) + { + double invScale = 1.f / (std::min(scaleW, srcSize.w - fsx1) * std::min(scaleH, srcSize.h - fsy1)); + return FractionalAreaPixel(srcPtr, srcStep, srcSize, channels, c, sy1, sy2, sx1, sx2, fsy1, fsy2, fsx1, fsx2, + invScale); + } + + return VarShapeZoomAreaPixel(srcPtr, srcStep, srcSize, channels, c, dstY, dstX, scaleH, scaleW, fsy1, fsx1); +} + template void resize(T *dstPtr, int dstStep, nvcv::Size2D dstSize, const T *srcPtr, int srcStep, nvcv::Size2D srcSize, - nvcv::ImageFormat frmt, NVCVInterpolationType interp, bool isVarshape, T MinVal, T MaxVal) + nvcv::ImageFormat frmt, bool isVarshape, T minVal, T maxVal) { - double scaleH = static_cast(srcSize.h) / dstSize.h; - double scaleW = static_cast(srcSize.w) / dstSize.w; + auto scaleH = static_cast(srcSize.h) / dstSize.h; + auto scaleW = static_cast(srcSize.w) / dstSize.w; assert(frmt.numPlanes() == 1); @@ -43,261 +221,184 @@ void resize(T *dstPtr, int dstStep, nvcv::Size2D dstSize, const T *srcPtr, int s { for (int dx = 0; dx < dstSize.w; dx++) { - if (interp == NVCV_INTERP_AREA) + double fsx1 = dx * scaleW; + double fsx2 = fsx1 + scaleW; + double fsy1 = dy * scaleH; + double fsy2 = fsy1 + scaleH; + int sx1 = cuda::round(fsx1); + int sx2 = cuda::round(fsx2); + int sy1 = cuda::round(fsy1); + int sy2 = cuda::round(fsy2); + + for (int c = 0; c < channels; c++) { - double fsx1 = dx * scaleW; - double fsx2 = fsx1 + scaleW; - double fsy1 = dy * scaleH; - double fsy2 = fsy1 + scaleH; - int sx1 = cuda::round(fsx1); - int sx2 = cuda::round(fsx2); - int sy1 = cuda::round(fsy1); - int sy2 = cuda::round(fsy2); - - for (int c = 0; c < channels; c++) - { - double out = 0.0; - - if (std::ceil(scaleW) == scaleW && std::ceil(scaleH) == scaleH) - { - double invscale = 1.f / (scaleW * scaleH); - - for (int dy = sy1; dy < sy2; ++dy) - { - for (int dx = sx1; dx < sx2; ++dx) - { - if (dy >= 0 && dy < srcSize.h && dx >= 0 && dx < srcSize.w) - { - out = out + srcPtr[dy * srcStep + dx * channels + c] * invscale; - } - } - } - } - else - { - if (!isVarshape || (scaleH >= 1.0f && scaleW >= 1.0f)) - { - double invscale - = 1.f / (std::min(scaleW, srcSize.w - fsx1) * std::min(scaleH, srcSize.h - fsy1)); - - for (int dy = sy1; dy < sy2; ++dy) - { - for (int dx = sx1; dx < sx2; ++dx) - if (dy >= 0 && dy < srcSize.h && dx >= 0 && dx < srcSize.w) - out = out + srcPtr[dy * srcStep + dx * channels + c] * invscale; - - if (sx1 > fsx1) - if (dy >= 0 && dy < srcSize.h && sx1 - 1 >= 0 && sx1 - 1 < srcSize.w) - out = out - + srcPtr[dy * srcStep + (sx1 - 1) * channels + c] - * ((sx1 - fsx1) * invscale); - - if (sx2 < fsx2) - if (dy >= 0 && dy < srcSize.h && sx2 >= 0 && sx2 < srcSize.w) - out = out - + srcPtr[dy * srcStep + sx2 * channels + c] * ((fsx2 - sx2) * invscale); - } - - if (sy1 > fsy1) - for (int dx = sx1; dx < sx2; ++dx) - if (sy1 - 1 >= 0 && sy1 - 1 < srcSize.h && dx >= 0 && dx < srcSize.w) - out = out - + srcPtr[(sy1 - 1) * srcStep + dx * channels + c] - * ((sy1 - fsy1) * invscale); - - if (sy2 < fsy2) - for (int dx = sx1; dx < sx2; ++dx) - if (sy2 >= 0 && sy2 < srcSize.h && dx >= 0 && dx < srcSize.w) - out = out - + srcPtr[sy2 * srcStep + dx * channels + c] * ((fsy2 - sy2) * invscale); - - if ((sy1 > fsy1) && (sx1 > fsx1)) - if (sy1 - 1 >= 0 && sy1 - 1 < srcSize.h && sx1 - 1 >= 0 && sx1 - 1 < srcSize.w) - out = out - + srcPtr[(sy1 - 1) * srcStep + (sx1 - 1) * channels + c] - * ((sy1 - fsy1) * (sx1 - fsx1) * invscale); - - if ((sy1 > fsy1) && (sx2 < fsx2)) - if (sy1 - 1 >= 0 && sy1 - 1 < srcSize.h && sx2 >= 0 && sx2 < srcSize.w) - out = out - + srcPtr[(sy1 - 1) * srcStep + sx2 * channels + c] - * ((sy1 - fsy1) * (fsx2 - sx2) * invscale); - - if ((sy2 < fsy2) && (sx2 < fsx2)) - if (sy2 >= 0 && sy2 < srcSize.h && sx2 >= 0 && sx2 < srcSize.w) - out = out - + srcPtr[sy2 * srcStep + sx2 * channels + c] - * ((fsy2 - sy2) * (fsx2 - sx2) * invscale); - - if ((sy2 < fsy2) && (sx1 > fsx1)) - if (sy2 >= 0 && sy2 < srcSize.h && sx1 - 1 >= 0 && sx1 - 1 < srcSize.w) - out = out - + srcPtr[sy2 * srcStep + (sx1 - 1) * channels + c] - * ((fsy2 - sy2) * (sx1 - fsx1) * invscale); - } - else // zoom in for varshape - { - double scaleH_inv = 1.0 / scaleH; - double scaleW_inv = 1.0 / scaleW; - - sy1 = cuda::round(fsy1); - sx1 = cuda::round(fsx1); - float fy = (float)(float(dy + 1) - float(sy1 + 1) * scaleH_inv); - fy = fy <= 0 ? 0.f : fy - cuda::round(fy); - - float cbufy[2]; - cbufy[0] = 1.f - fy; - cbufy[1] = fy; - - float fx = (float)(float(dx + 1) - float(sx1 + 1) * scaleW_inv); - fx = fx <= 0 ? 0.f : fx - cuda::round(fx); - - if (sx1 < 0) - { - fx = 0, sx1 = 0; - } - if (sx1 >= srcSize.w - 1) - { - fx = 0, sx1 = srcSize.w - 2; - } - if (sy1 >= srcSize.h - 1) - { - sy1 = srcSize.h - 2; - } - - float cbufx[2]; - cbufx[0] = 1.f - fx; - cbufx[1] = fx; - out = srcPtr[sy1 * srcStep + sx1 * channels + c] * cbufx[0] * cbufy[0] - + srcPtr[(sy1 + 1) * srcStep + sx1 * channels + c] * cbufx[0] * cbufy[1] - + srcPtr[sy1 * srcStep + (sx1 + 1) * channels + c] * cbufx[1] * cbufy[0] - + srcPtr[(sy1 + 1) * srcStep + (sx1 + 1) * channels + c] * cbufx[1] * cbufy[1]; - } - } - - if (std::numeric_limits::is_integer) - { - out = std::rint(std::numeric_limits::is_signed ? out : std::abs(out)); - } - - dstPtr[dy * dstStep + dx * channels + c] = out < MinVal ? MinVal : (out > MaxVal ? MaxVal : out); - } + double out = AreaResizePixel(srcPtr, srcStep, srcSize, channels, c, dy, dx, sy1, sy2, sx1, sx2, fsy1, + fsy2, fsx1, fsx2, scaleH, scaleW, isVarshape); + + dstPtr[dy * dstStep + dx * channels + c] = ClampResizeValue(out, minVal, maxVal); } } } } template -void resizedCrop(T *dstPtr, int dstStep, nvcv::Size2D dstSize, const T *srcPtr, int srcStep, nvcv::Size2D srcSize, - int top, int left, int crop_rows, int crop_cols, nvcv::ImageFormat frmt, NVCVInterpolationType interp, - T MinVal, T MaxVal) +static void StoreNearestCropPixel(T *dstPtr, int dstStep, const T *srcPtr, int srcStep, nvcv::Size2D srcSize, + int channels, int dy, int dx, int top, int left, float scaleH, float scaleW) { - float scaleH = static_cast(crop_rows) / dstSize.h; - float scaleW = static_cast(crop_cols) / dstSize.w; + float fy = scaleH * (static_cast(dy) + 0.5f) + static_cast(top); + float fx = scaleW * (static_cast(dx) + 0.5f) + static_cast(left); - assert(frmt.numPlanes() == 1); + auto sy = static_cast(std::floor(fy)); + auto sx = static_cast(std::floor(fx)); - int channels = frmt.numChannels(); + sy = std::min(sy, srcSize.h - 1); + sx = std::min(sx, srcSize.w - 1); - for (int dy = 0; dy < dstSize.h; dy++) + int srcOffset = sy * srcStep + sx * channels; + int dstOffset = dy * dstStep + dx * channels; + + for (int c = 0; c < channels; c++) { - for (int dx = 0; dx < dstSize.w; dx++) - { - if (interp == NVCV_INTERP_NEAREST) - { - float fy = scaleH * (dy + 0.5f) + top; - float fx = scaleW * (dx + 0.5f) + left; + dstPtr[dstOffset + c] = srcPtr[srcOffset + c]; + } +} - int sy = std::floor(fy); - int sx = std::floor(fx); +template +static void StoreLinearCropPixel(T *dstPtr, int dstStep, const T *srcPtr, int srcStep, nvcv::Size2D srcSize, + int channels, int dy, int dx, int top, int left, float scaleH, float scaleW, T minVal, + T maxVal) +{ + auto fy = static_cast(scaleH) * (static_cast(dy) + 0.5) - 0.5 + static_cast(top); + auto fx = static_cast(scaleW) * (static_cast(dx) + 0.5) - 0.5 + static_cast(left); - sy = std::min(sy, srcSize.h - 1); - sx = std::min(sx, srcSize.w - 1); + auto sy = static_cast(std::floor(fy)); + auto sx = static_cast(std::floor(fx)); - for (int c = 0; c < channels; c++) - { - dstPtr[dy * dstStep + dx * channels + c] = srcPtr[sy * srcStep + sx * channels + c]; - } - } - else if (interp == NVCV_INTERP_LINEAR) - { - double fy = scaleH * (dy + 0.5) - 0.5 + top; - double fx = scaleW * (dx + 0.5) - 0.5 + left; + if (sy < 0) + { + fy = 0.0; + } + else if (sy > srcSize.h - 2) + { + fy = 1.0; + } + else + { + fy -= static_cast(sy); + } + + if (sx < 0) + { + fx = 0.0; + } + else if (sx > srcSize.w - 2) + { + fx = 1.0; + } + else + { + fx -= static_cast(sx); + } + + sy = std::clamp(sy, 0, srcSize.h - 2); + sx = std::clamp(sx, 0, srcSize.w - 2); + + std::array wghtY = {1 - fy, fy}; + std::array wghtX = {1 - fx, fx}; + + int dstOffset = dy * dstStep + dx * channels; + + for (int c = 0; c < channels; c++) + { + double res = std::rint(std::abs(srcPtr[(sy + 0) * srcStep + (sx + 0) * channels + c] * wghtY[0] * wghtX[0] + + srcPtr[(sy + 1) * srcStep + (sx + 0) * channels + c] * wghtY[1] * wghtX[0] + + srcPtr[(sy + 0) * srcStep + (sx + 1) * channels + c] * wghtY[0] * wghtX[1] + + srcPtr[(sy + 1) * srcStep + (sx + 1) * channels + c] * wghtY[1] * wghtX[1])); - int sy = std::floor(fy); - int sx = std::floor(fx); + dstPtr[dstOffset + c] = ClampResizeValue(res, minVal, maxVal); + } +} + +static std::array CubicWeights(double frac) +{ + const double a = -0.75; + + std::array weights; + weights[0] = ((a * (frac + 1) - 5 * a) * (frac + 1) + 8 * a) * (frac + 1) - 4 * a; + weights[1] = ((a + 2) * frac - (a + 3)) * frac * frac + 1; + weights[2] = ((a + 2) * (1 - frac) - (a + 3)) * (1 - frac) * (1 - frac) + 1; + weights[3] = 1 - weights[0] - weights[1] - weights[2]; + return weights; +} - fy = ((sy < 0) ? 0 : ((sy > srcSize.h - 2) ? 1 : fy - sy)); - fx = ((sx < 0) ? 0 : ((sx > srcSize.w - 2) ? 1 : fx - sx)); +template +static void StoreCubicCropPixel(T *dstPtr, int dstStep, const T *srcPtr, int srcStep, nvcv::Size2D srcSize, + int channels, int dy, int dx, int top, int left, float scaleH, float scaleW, T minVal, + T maxVal) +{ + auto fy = static_cast(scaleH) * (static_cast(dy) + 0.5) - 0.5 + static_cast(top); + auto fx = static_cast(scaleW) * (static_cast(dx) + 0.5) - 0.5 + static_cast(left); + + auto sy = static_cast(std::floor(fy)); + auto sx = static_cast(std::floor(fx)); - sy = std::max(0, std::min(sy, srcSize.h - 2)); - sx = std::max(0, std::min(sx, srcSize.w - 2)); + fy -= static_cast(sy); + fx -= static_cast(sx); - double wghtY[2] = {1 - fy, fy}; - double wghtX[2] = {1 - fx, fx}; + std::array wghtY = CubicWeights(fy); + std::array wghtX = CubicWeights(fx); - for (int c = 0; c < channels; c++) - { - double res = std::rint( - std::abs(srcPtr[(sy + 0) * srcStep + (sx + 0) * channels + c] * wghtY[0] * wghtX[0] - + srcPtr[(sy + 1) * srcStep + (sx + 0) * channels + c] * wghtY[1] * wghtX[0] - + srcPtr[(sy + 0) * srcStep + (sx + 1) * channels + c] * wghtY[0] * wghtX[1] - + srcPtr[(sy + 1) * srcStep + (sx + 1) * channels + c] * wghtY[1] * wghtX[1])); + int dstOffset = dy * dstStep + dx * channels; - dstPtr[dy * dstStep + dx * channels + c] = res < MinVal ? MinVal : (res > MaxVal ? MaxVal : res); - } + for (int c = 0; c < channels; c++) + { + double res = 0; + for (int ky = 0; ky < 4; ky++) + { + int csy = std::clamp(sy + ky - 1, 0, srcSize.h - 1); + for (int kx = 0; kx < 4; kx++) + { + int csx = std::clamp(sx + kx - 1, 0, srcSize.w - 1); + res += srcPtr[csy * srcStep + csx * channels + c] * wghtX[kx] * wghtY[ky]; } - else if (interp == NVCV_INTERP_CUBIC) + } + res = std::rint(std::clamp(res, static_cast(minVal), static_cast(maxVal))); + dstPtr[dstOffset + c] = static_cast(res); + } +} + +template +void resizedCrop(T *dstPtr, int dstStep, nvcv::Size2D dstSize, const T *srcPtr, int srcStep, nvcv::Size2D srcSize, + int top, int left, int crop_rows, int crop_cols, nvcv::ImageFormat frmt, NVCVInterpolationType interp, + T MinVal, T MaxVal) +{ + auto scaleH = static_cast(crop_rows) / static_cast(dstSize.h); + auto scaleW = static_cast(crop_cols) / static_cast(dstSize.w); + + assert(frmt.numPlanes() == 1); + + int channels = frmt.numChannels(); + + for (int dy = 0; dy < dstSize.h; dy++) + { + for (int dx = 0; dx < dstSize.w; dx++) + { + switch (interp) { - double fy = scaleH * (dy + 0.5) - 0.5 + top; - double fx = scaleW * (dx + 0.5) - 0.5 + left; - - int sy = std::floor(fy); - int sx = std::floor(fx); - - fy -= sy; - fx -= sx; - - fx = (sx < 1 || sx >= srcSize.w - 3) ? 0 : fx; - - sy = std::max(1, std::min(sy, srcSize.h - 3)); - sx = std::max(1, std::min(sx, srcSize.w - 3)); - - const double A = -0.75; - double wghtY[4]; - wghtY[0] = ((A * (fy + 1) - 5 * A) * (fy + 1) + 8 * A) * (fy + 1) - 4 * A; - wghtY[1] = ((A + 2) * fy - (A + 3)) * fy * fy + 1; - wghtY[2] = ((A + 2) * (1 - fy) - (A + 3)) * (1 - fy) * (1 - fy) + 1; - wghtY[3] = 1 - wghtY[0] - wghtY[1] - wghtY[2]; - - double wghtX[4]; - wghtX[0] = ((A * (fx + 1) - 5 * A) * (fx + 1) + 8 * A) * (fx + 1) - 4 * A; - wghtX[1] = ((A + 2) * fx - (A + 3)) * fx * fx + 1; - wghtX[2] = ((A + 2) * (1 - fx) - (A + 3)) * (1 - fx) * (1 - fx) + 1; - wghtX[3] = 1 - wghtX[0] - wghtX[1] - wghtX[2]; - - for (int c = 0; c < channels; c++) - { - double res = std::rint( - std::abs(srcPtr[(sy - 1) * srcStep + (sx - 1) * channels + c] * wghtX[0] * wghtY[0] - + srcPtr[(sy + 0) * srcStep + (sx - 1) * channels + c] * wghtX[0] * wghtY[1] - + srcPtr[(sy + 1) * srcStep + (sx - 1) * channels + c] * wghtX[0] * wghtY[2] - + srcPtr[(sy + 2) * srcStep + (sx - 1) * channels + c] * wghtX[0] * wghtY[3] - + srcPtr[(sy - 1) * srcStep + (sx + 0) * channels + c] * wghtX[1] * wghtY[0] - + srcPtr[(sy + 0) * srcStep + (sx + 0) * channels + c] * wghtX[1] * wghtY[1] - + srcPtr[(sy + 1) * srcStep + (sx + 0) * channels + c] * wghtX[1] * wghtY[2] - + srcPtr[(sy + 2) * srcStep + (sx + 0) * channels + c] * wghtX[1] * wghtY[3] - + srcPtr[(sy - 1) * srcStep + (sx + 1) * channels + c] * wghtX[2] * wghtY[0] - + srcPtr[(sy + 0) * srcStep + (sx + 1) * channels + c] * wghtX[2] * wghtY[1] - + srcPtr[(sy + 1) * srcStep + (sx + 1) * channels + c] * wghtX[2] * wghtY[2] - + srcPtr[(sy + 2) * srcStep + (sx + 1) * channels + c] * wghtX[2] * wghtY[3] - + srcPtr[(sy - 1) * srcStep + (sx + 2) * channels + c] * wghtX[3] * wghtY[0] - + srcPtr[(sy + 0) * srcStep + (sx + 2) * channels + c] * wghtX[3] * wghtY[1] - + srcPtr[(sy + 1) * srcStep + (sx + 2) * channels + c] * wghtX[3] * wghtY[2] - + srcPtr[(sy + 2) * srcStep + (sx + 2) * channels + c] * wghtX[3] * wghtY[3])); - - dstPtr[dy * dstStep + dx * channels + c] = res < MinVal ? MinVal : (res > MaxVal ? MaxVal : res); - } + case NVCV_INTERP_NEAREST: + StoreNearestCropPixel(dstPtr, dstStep, srcPtr, srcStep, srcSize, channels, dy, dx, top, left, scaleH, + scaleW); + break; + case NVCV_INTERP_LINEAR: + StoreLinearCropPixel(dstPtr, dstStep, srcPtr, srcStep, srcSize, channels, dy, dx, top, left, scaleH, + scaleW, MinVal, MaxVal); + break; + case NVCV_INTERP_CUBIC: + StoreCubicCropPixel(dstPtr, dstStep, srcPtr, srcStep, srcSize, channels, dy, dx, top, left, scaleH, + scaleW, MinVal, MaxVal); + break; + default: + break; } } } @@ -317,8 +418,8 @@ void _Resize(T *dstPtr, int dstStride, nvcv::Size2D dstSize, const T *srcPtr, in } else if (interp == NVCV_INTERP_AREA) { - resize(dstPtr, dstStep, dstSize, srcPtr, srcStep, srcSize, frmt, interp, isVarShape, - std::numeric_limits::min(), std::numeric_limits::max()); + resize(dstPtr, dstStep, dstSize, srcPtr, srcStep, srcSize, frmt, isVarShape, std::numeric_limits::min(), + std::numeric_limits::max()); } } diff --git a/tests/cvcuda/system/TestOpAdaptiveThreshold.cpp b/tests/cvcuda/system/TestOpAdaptiveThreshold.cpp index 17906f282..7c3f4850d 100644 --- a/tests/cvcuda/system/TestOpAdaptiveThreshold.cpp +++ b/tests/cvcuda/system/TestOpAdaptiveThreshold.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -37,6 +37,11 @@ namespace cuda = nvcv::cuda; using uchar = unsigned char; +static int ScaledSize(int size, double scale) +{ + return static_cast(size * scale); +} + // clang-format off NVCV_TEST_SUITE_P(OpAdaptiveThreshold, test::ValueList @@ -45,11 +50,26 @@ NVCV_TEST_SUITE_P(OpAdaptiveThreshold, test::ValueList cuda::SaturateCast(convolvedValue); + if (thresholdType == NVCV_THRESH_BINARY) + { + return isAboveThreshold ? maxValue : 0; + } + + return isAboveThreshold ? 0 : maxValue; +} + static void AdaptiveThreshold(std::vector &hDst, const std::vector &hSrc, long3 strides, int3 shape, nvcv::ImageFormat fmt, const std::vector &kernel, const nvcv::Size2D &kernelSize, double maxValue, NVCVThresholdType thresholdType, double c) @@ -60,7 +80,7 @@ static void AdaptiveThreshold(std::vector &hDst, const std::vector(maxValue); - int idelta = thresholdType == NVCV_THRESH_BINARY ? (int)std::ceil(c) : (int)std::floor(c); + int idelta = thresholdType == NVCV_THRESH_BINARY ? static_cast(std::ceil(c)) : static_cast(std::floor(c)); for (int b = 0; b < shape.z; ++b) { @@ -70,22 +90,135 @@ static void AdaptiveThreshold(std::vector &hDst, const std::vector(hSrc, strides, b, y, x); uchar res = test::detail::ValueAt(hDst, strides, b, y, x); - uchar t; - if (thresholdType == NVCV_THRESH_BINARY) - { - t = srcV + idelta > cuda::SaturateCast(res) ? iMaxValue : 0; - } - else - { - t = srcV + idelta > cuda::SaturateCast(res) ? 0 : iMaxValue; - } - - test::detail::ValueAt(hDst, strides, b, y, x) = t; + + test::detail::ValueAt(hDst, strides, b, y, x) + = AdaptiveThresholdValue(srcV, res, iMaxValue, idelta, thresholdType); } } } } +template +static void CopyTensorData(const nvcv::Tensor &tensor, const std::vector &values, cudaStream_t stream) +{ + auto dev = tensor.exportData(); + ASSERT_NE(nullptr, dev); + + ASSERT_EQ(cudaSuccess, cudaMemcpyAsync(dev->basePtr(), values.data(), values.size() * sizeof(T), + cudaMemcpyHostToDevice, stream)); +} + +static void CopySingleChannelSamples(const nvcv::Tensor &tensor, const std::vector> &values, + int width, int height, cudaStream_t stream) +{ + auto dev = tensor.exportData(); + ASSERT_NE(nullptr, dev); + + auto access = nvcv::TensorDataAccessStridedImagePlanar::Create(*dev); + ASSERT_TRUE(access); + ASSERT_EQ(static_cast(values.size()), access->numSamples()); + + for (int i = 0; i < access->numSamples(); ++i) + { + ASSERT_EQ(cudaSuccess, cudaMemcpy2DAsync(access->sampleData(i), access->rowStride(), values[i].data(), width, + width, height, cudaMemcpyHostToDevice, stream)); + } +} + +static void CopySingleChannelSamplesToHost(std::vector> &values, const nvcv::Tensor &tensor, + int width, int height) +{ + auto dev = tensor.exportData(); + ASSERT_NE(nullptr, dev); + + auto access = nvcv::TensorDataAccessStridedImagePlanar::Create(*dev); + ASSERT_TRUE(access); + + values.assign(access->numSamples(), std::vector(height * width)); + for (int i = 0; i < access->numSamples(); ++i) + { + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(values[i].data(), width, access->sampleData(i), access->rowStride(), width, + height, cudaMemcpyDeviceToHost)); + } +} + +static void RunAdaptiveThresholdTensorParity(int width, int height, int batch, double maxValue, + NVCVAdaptiveThresholdType adaptiveThresholdType, + NVCVThresholdType thresholdType, int blockSize, double c, bool batched) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + const int samples = batched ? batch : 1; + + nvcv::Tensor srcInterleaved( + batched ? nvcv::TensorShape{{samples, height, width, 1}, "NHWC"} + : nvcv::TensorShape{{height, width, 1}, "HWC"}, + nvcv::TYPE_U8); + nvcv::Tensor dstInterleaved( + batched ? nvcv::TensorShape{{samples, height, width, 1}, "NHWC"} + : nvcv::TensorShape{{height, width, 1}, "HWC"}, + nvcv::TYPE_U8); + nvcv::Tensor srcPlanar( + batched ? nvcv::TensorShape{{samples, 1, height, width}, "NCHW"} + : nvcv::TensorShape{{1, height, width}, "CHW"}, + nvcv::TYPE_U8); + nvcv::Tensor dstPlanar( + batched ? nvcv::TensorShape{{samples, 1, height, width}, "NCHW"} + : nvcv::TensorShape{{1, height, width}, "CHW"}, + nvcv::TYPE_U8); + + std::vector> srcVec(samples, std::vector(height * width)); + for (int sample = 0; sample < samples; ++sample) + { + for (size_t idx = 0; idx < srcVec[sample].size(); ++idx) + { + srcVec[sample][idx] = static_cast((idx * 37 + sample * 53 + width + height) & 0xFF); + } + } + + CopySingleChannelSamples(srcInterleaved, srcVec, width, height, stream); + CopySingleChannelSamples(srcPlanar, srcVec, width, height, stream); + + cvcuda::AdaptiveThreshold adaptiveThresholdOp(blockSize, samples); + + EXPECT_NO_THROW(adaptiveThresholdOp(stream, srcInterleaved, dstInterleaved, maxValue, adaptiveThresholdType, + thresholdType, blockSize, c)); + EXPECT_NO_THROW(adaptiveThresholdOp(stream, srcPlanar, dstPlanar, maxValue, adaptiveThresholdType, thresholdType, + blockSize, c)); + + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + std::vector> dstInterleavedVec; + std::vector> dstPlanarVec; + CopySingleChannelSamplesToHost(dstInterleavedVec, dstInterleaved, width, height); + CopySingleChannelSamplesToHost(dstPlanarVec, dstPlanar, width, height); + ASSERT_EQ(dstInterleavedVec, dstPlanarVec); + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +struct AdaptiveThresholdVarShapeTestData +{ + explicit AdaptiveThresholdVarShapeTestData(int batch) + : batchSrc(batch) + , batchDst(batch) + { + for (int i = 0; i < batch; ++i) + { + imgSrc.emplace_back(nvcv::Size2D{8, 8}, nvcv::FMT_U8); + imgDst.emplace_back(nvcv::Size2D{8, 8}, nvcv::FMT_U8); + } + + batchSrc.pushBack(imgSrc.begin(), imgSrc.end()); + batchDst.pushBack(imgDst.begin(), imgDst.end()); + } + + std::vector imgSrc; + std::vector imgDst; + nvcv::ImageBatchVarShape batchSrc; + nvcv::ImageBatchVarShape batchDst; +}; + TEST_P(OpAdaptiveThreshold, correct_output) { cudaStream_t stream; @@ -137,7 +270,7 @@ TEST_P(OpAdaptiveThreshold, correct_output) { std::uniform_int_distribution rand(0, 255); srcVec[i].resize(height * rowStride); - std::generate(srcVec[i].begin(), srcVec[i].end(), [&]() { return rand(randEng); }); + std::ranges::generate(srcVec[i], [&rand, &randEng]() { return rand(randEng); }); ASSERT_EQ(cudaSuccess, cudaMemcpy2D(inAccess->sampleData(i), inAccess->rowStride(), srcVec[i].data(), rowStride, rowStride, height, cudaMemcpyHostToDevice)); } @@ -185,6 +318,48 @@ TEST_P(OpAdaptiveThreshold, correct_output) } } +NVCV_TEST_SUITE_P( + OpAdaptiveThresholdPlanar, + test::ValueList{ + // width, height, batch, maxValue, NVCVAdaptiveThresholdType, NVCVThresholdType, blockSize, c + { 73, 41, 3, 127.0, NVCV_ADAPTIVE_THRESH_MEAN_C, NVCV_THRESH_BINARY, 3, 2.5}, + { 64, 37, 2, 100.0, NVCV_ADAPTIVE_THRESH_GAUSSIAN_C, NVCV_THRESH_BINARY_INV, 5, -4.3}, + { 15, 9, 2, 100.0, NVCV_ADAPTIVE_THRESH_MEAN_C, NVCV_THRESH_BINARY, 7, 2.5}, + {127, 17, 2, 127.0, NVCV_ADAPTIVE_THRESH_MEAN_C, NVCV_THRESH_BINARY_INV, 7, -4.3}, + {123, 33, 3, 100.0, NVCV_ADAPTIVE_THRESH_GAUSSIAN_C, NVCV_THRESH_BINARY, 7, 9.2}, + {131, 19, 2, 127.0, NVCV_ADAPTIVE_THRESH_GAUSSIAN_C, NVCV_THRESH_BINARY_INV, 7, -2.8} +}); + +TEST_P(OpAdaptiveThresholdPlanar, tensor_nchw_matches_interleaved) +{ + int width = GetParamValue<0>(); + int height = GetParamValue<1>(); + int batch = GetParamValue<2>(); + double maxValue = GetParamValue<3>(); + NVCVAdaptiveThresholdType adaptiveThresholdType = GetParamValue<4>(); + NVCVThresholdType thresholdType = GetParamValue<5>(); + int blockSize = GetParamValue<6>(); + double c = GetParamValue<7>(); + + RunAdaptiveThresholdTensorParity(width, height, batch, maxValue, adaptiveThresholdType, thresholdType, blockSize, c, + true); +} + +TEST_P(OpAdaptiveThresholdPlanar, tensor_chw_matches_interleaved) +{ + int width = GetParamValue<0>(); + int height = GetParamValue<1>(); + int batch = GetParamValue<2>(); + double maxValue = GetParamValue<3>(); + NVCVAdaptiveThresholdType adaptiveThresholdType = GetParamValue<4>(); + NVCVThresholdType thresholdType = GetParamValue<5>(); + int blockSize = GetParamValue<6>(); + double c = GetParamValue<7>(); + + RunAdaptiveThresholdTensorParity(width, height, batch, maxValue, adaptiveThresholdType, thresholdType, blockSize, c, + false); +} + TEST_P(OpAdaptiveThreshold, varshape_correct_output) { cudaStream_t stream; @@ -202,9 +377,9 @@ TEST_P(OpAdaptiveThreshold, varshape_correct_output) nvcv::ImageFormat fmt = nvcv::FMT_U8; // Create input varshape - std::default_random_engine rng; - std::uniform_int_distribution udistWidth(width * 0.8, width * 1.1); - std::uniform_int_distribution udistHeight(height * 0.8, height * 1.1); + std::default_random_engine rng; + std::uniform_int_distribution udistWidth(ScaledSize(width, 0.8), ScaledSize(width, 1.1)); + std::uniform_int_distribution udistHeight(ScaledSize(height, 0.8), ScaledSize(height, 1.1)); std::vector imgSrc; @@ -221,7 +396,7 @@ TEST_P(OpAdaptiveThreshold, varshape_correct_output) std::uniform_int_distribution udist(0, 255); srcVec[i].resize(imgSrc[i].size().h * srcRowStride); - std::generate(srcVec[i].begin(), srcVec[i].end(), [&]() { return udist(rng); }); + std::ranges::generate(srcVec[i], [&udist, &rng]() { return udist(rng); }); auto imgData = imgSrc[i].exportData(); ASSERT_NE(imgData, nvcv::NullOpt); @@ -332,32 +507,131 @@ TEST_P(OpAdaptiveThreshold, varshape_correct_output) } } -// clang-format off -NVCV_TEST_SUITE_P(OpAdaptiveThresholdVarshape_Negative, test::ValueList{ - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U8, 6, NVCV_ADAPTIVE_THRESH_MEAN_C, NVCV_THRESH_BINARY}, // exceed max batch size - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_RGB8, nvcv::FMT_RGB8p, 2, NVCV_ADAPTIVE_THRESH_MEAN_C, NVCV_THRESH_BINARY}, // data format is different - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_RGB8p, nvcv::FMT_RGB8p, 2, NVCV_ADAPTIVE_THRESH_MEAN_C, NVCV_THRESH_BINARY}, // data format is not kNHWC/kHWC - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_F16, nvcv::FMT_F16, 2, NVCV_ADAPTIVE_THRESH_MEAN_C, NVCV_THRESH_BINARY}, // invalid data type - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_RGB8, nvcv::FMT_RGB8, 2, NVCV_ADAPTIVE_THRESH_MEAN_C, NVCV_THRESH_BINARY}, // invalid channel +TEST(OpAdaptiveThresholdVarshape, mixed_block_sizes_match_cpu_gold) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + const std::vector sizes{ + { 15, 9}, + {127, 17}, + {131, 19} + }; + const std::vector blockSizes{3, 5, 7}; + const std::vector maxValues{100.0, 127.0, 255.0}; + const std::vector cValues{2.5, -4.3, 9.2}; + const auto batch = static_cast(sizes.size()); + const nvcv::ImageFormat fmt = nvcv::FMT_U8; + + std::vector imgSrc; + std::vector imgDst; + std::vector> srcVec(batch); + for (int i = 0; i < batch; ++i) + { + imgSrc.emplace_back(sizes[i], fmt); + imgDst.emplace_back(sizes[i], fmt); + + srcVec[i].resize(sizes[i].w * sizes[i].h); + for (size_t j = 0; j < srcVec[i].size(); ++j) + { + srcVec[i][j] = static_cast((j * 37 + i * 53 + sizes[i].w + sizes[i].h) & 0xFF); + } + + auto srcData = imgSrc[i].exportData(); + ASSERT_NE(srcData, nvcv::NullOpt); + ASSERT_EQ(cudaSuccess, + cudaMemcpy2DAsync(srcData->plane(0).basePtr, srcData->plane(0).rowStride, srcVec[i].data(), + sizes[i].w, sizes[i].w, sizes[i].h, cudaMemcpyHostToDevice, stream)); + } + + nvcv::ImageBatchVarShape batchSrc(batch); + nvcv::ImageBatchVarShape batchDst(batch); + batchSrc.pushBack(imgSrc.begin(), imgSrc.end()); + batchDst.pushBack(imgDst.begin(), imgDst.end()); + + nvcv::Tensor maxValueTensor({{batch}, "N"}, nvcv::TYPE_F64); + nvcv::Tensor blockSizeTensor({{batch}, "N"}, nvcv::TYPE_S32); + nvcv::Tensor cTensor({{batch}, "N"}, nvcv::TYPE_F64); + CopyTensorData(maxValueTensor, maxValues, stream); + CopyTensorData(blockSizeTensor, blockSizes, stream); + CopyTensorData(cTensor, cValues, stream); + + cvcuda::AdaptiveThreshold adaptiveThresholdOp(7, batch); + EXPECT_NO_THROW(adaptiveThresholdOp(stream, batchSrc, batchDst, maxValueTensor, NVCV_ADAPTIVE_THRESH_GAUSSIAN_C, + NVCV_THRESH_BINARY_INV, blockSizeTensor, cTensor)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + for (int i = 0; i < batch; ++i) + { + SCOPED_TRACE(i); + + auto dstData = imgDst[i].exportData(); + ASSERT_NE(dstData, nvcv::NullOpt); + + std::vector testVec(sizes[i].w * sizes[i].h); + ASSERT_EQ(cudaSuccess, + cudaMemcpy2D(testVec.data(), sizes[i].w, dstData->plane(0).basePtr, dstData->plane(0).rowStride, + sizes[i].w, sizes[i].h, cudaMemcpyDeviceToHost)); + + nvcv::Size2D kernelSize(blockSizes[i], blockSizes[i]); + const double sigmaValue = 0.3 * ((blockSizes[i] - 1) * 0.5 - 1) + 0.8; + std::vector kernel = test::ComputeGaussianKernel(kernelSize, {sigmaValue, sigmaValue}); + std::vector goldVec(testVec.size()); + long3 strides{sizes[i].w * sizes[i].h, sizes[i].w, 1}; + int3 shape{sizes[i].w, sizes[i].h, 1}; + AdaptiveThreshold(goldVec, srcVec[i], strides, shape, fmt, kernel, kernelSize, maxValues[i], + NVCV_THRESH_BINARY_INV, cValues[i]); + + EXPECT_EQ(testVec, goldVec); + } + + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +static auto OpAdaptiveThresholdVarshapeNegativeParams() +{ + test::ValueList + params{ + {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U8, 6, NVCV_ADAPTIVE_THRESH_MEAN_C, + NVCV_THRESH_BINARY}, // exceed max batch size + {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_RGB8, nvcv::FMT_RGB8p, 2, NVCV_ADAPTIVE_THRESH_MEAN_C, + NVCV_THRESH_BINARY}, // data format is different + {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_RGB8p, nvcv::FMT_RGB8p, 2, NVCV_ADAPTIVE_THRESH_MEAN_C, + NVCV_THRESH_BINARY}, // data format is not kNHWC/kHWC + {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_F16, nvcv::FMT_F16, 2, NVCV_ADAPTIVE_THRESH_MEAN_C, + NVCV_THRESH_BINARY}, // invalid data type + {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_RGB8, nvcv::FMT_RGB8, 2, NVCV_ADAPTIVE_THRESH_MEAN_C, + NVCV_THRESH_BINARY}, // invalid channel + }; #ifndef ENABLE_SANITIZER - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U8, 2, static_cast(255), NVCV_THRESH_BINARY}, - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U8, 2, NVCV_ADAPTIVE_THRESH_MEAN_C, static_cast(255)}, + params.emplace_back(NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U8, 2, + static_cast(255), NVCV_THRESH_BINARY); + params.emplace_back(NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U8, 2, NVCV_ADAPTIVE_THRESH_MEAN_C, + static_cast(255)); #endif -}); + return params; +} + +NVCV_TEST_SUITE_P(OpAdaptiveThresholdVarshape_Negative, OpAdaptiveThresholdVarshapeNegativeParams()); -NVCV_TEST_SUITE_P(OpAdaptiveThreshold_Negative, test::ValueList{ - {nvcv::FMT_RGB8, nvcv::FMT_RGB8p, NVCV_ADAPTIVE_THRESH_MEAN_C, NVCV_THRESH_BINARY, 3}, - {nvcv::FMT_RGB8p, nvcv::FMT_RGB8p, NVCV_ADAPTIVE_THRESH_MEAN_C, NVCV_THRESH_BINARY, 3}, - {nvcv::FMT_U8, nvcv::FMT_S16, NVCV_ADAPTIVE_THRESH_MEAN_C, NVCV_THRESH_BINARY, 3}, - {nvcv::FMT_S16, nvcv::FMT_S16, NVCV_ADAPTIVE_THRESH_MEAN_C, NVCV_THRESH_BINARY, 3}, - {nvcv::FMT_RGB8, nvcv::FMT_RGB8, NVCV_ADAPTIVE_THRESH_MEAN_C, NVCV_THRESH_BINARY, 3}, - {nvcv::FMT_U8, nvcv::FMT_U8, NVCV_ADAPTIVE_THRESH_MEAN_C, NVCV_THRESH_BINARY, 2}, +static auto OpAdaptiveThresholdNegativeParams() +{ + test::ValueList params{ + { nvcv::FMT_RGB8, nvcv::FMT_RGB8p, NVCV_ADAPTIVE_THRESH_MEAN_C, NVCV_THRESH_BINARY, 3}, + {nvcv::FMT_RGB8p, nvcv::FMT_RGB8p, NVCV_ADAPTIVE_THRESH_MEAN_C, NVCV_THRESH_BINARY, 3}, + { nvcv::FMT_U8, nvcv::FMT_S16, NVCV_ADAPTIVE_THRESH_MEAN_C, NVCV_THRESH_BINARY, 3}, + { nvcv::FMT_S16, nvcv::FMT_S16, NVCV_ADAPTIVE_THRESH_MEAN_C, NVCV_THRESH_BINARY, 3}, + { nvcv::FMT_RGB8, nvcv::FMT_RGB8, NVCV_ADAPTIVE_THRESH_MEAN_C, NVCV_THRESH_BINARY, 3}, + { nvcv::FMT_U8, nvcv::FMT_U8, NVCV_ADAPTIVE_THRESH_MEAN_C, NVCV_THRESH_BINARY, 2}, + }; #ifndef ENABLE_SANITIZER - {nvcv::FMT_U8, nvcv::FMT_U8, static_cast(255), NVCV_THRESH_BINARY, 3}, - {nvcv::FMT_U8, nvcv::FMT_U8, NVCV_ADAPTIVE_THRESH_MEAN_C, NVCV_THRESH_OTSU, 3}, + params.emplace_back(nvcv::FMT_U8, nvcv::FMT_U8, static_cast(255), NVCV_THRESH_BINARY, 3); + params.emplace_back(nvcv::FMT_U8, nvcv::FMT_U8, NVCV_ADAPTIVE_THRESH_MEAN_C, NVCV_THRESH_OTSU, 3); #endif -}); -// clang-format on + return params; +} + +NVCV_TEST_SUITE_P(OpAdaptiveThreshold_Negative, OpAdaptiveThresholdNegativeParams()); TEST_P(OpAdaptiveThreshold_Negative, op) { @@ -383,7 +657,8 @@ TEST_P(OpAdaptiveThreshold_Negative, op) cvcuda::AdaptiveThreshold adaptiveThresholdOp(blockSize, 1); EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall( - [&] { + [&adaptiveThresholdOp, &stream, &imgIn, &imgOut, &maxValue, + &adaptiveThresholdType, &thresholdType, &blockSize, &c] { adaptiveThresholdOp(stream, imgIn, imgOut, maxValue, adaptiveThresholdType, thresholdType, blockSize, c); @@ -393,6 +668,27 @@ TEST_P(OpAdaptiveThreshold_Negative, op) EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); } +TEST(OpAdaptiveThreshold_Negative, block_size_exceeds_maxBlockSize) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + nvcv::Tensor imgIn = nvcv::util::CreateTensor(1, 8, 8, nvcv::FMT_U8); + nvcv::Tensor imgOut = nvcv::util::CreateTensor(1, 8, 8, nvcv::FMT_U8); + + cvcuda::AdaptiveThreshold adaptiveThresholdOp(3, 1); + + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall( + [&adaptiveThresholdOp, &stream, &imgIn, &imgOut] { + adaptiveThresholdOp(stream, imgIn, imgOut, 127.0, + NVCV_ADAPTIVE_THRESH_MEAN_C, NVCV_THRESH_BINARY, + 5, 2.5); + })); + + EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + TEST_P(OpAdaptiveThresholdVarshape_Negative, op) { cudaStream_t stream; @@ -411,9 +707,9 @@ TEST_P(OpAdaptiveThresholdVarshape_Negative, op) int maxBatch = 5; // Create input varshape - std::default_random_engine rng; - std::uniform_int_distribution udistWidth(width * 0.8, width * 1.1); - std::uniform_int_distribution udistHeight(height * 0.8, height * 1.1); + std::default_random_engine rng; + std::uniform_int_distribution udistWidth(ScaledSize(width, 0.8), ScaledSize(width, 1.1)); + std::uniform_int_distribution udistHeight(ScaledSize(height, 0.8), ScaledSize(height, 1.1)); std::vector imgSrc; @@ -444,7 +740,8 @@ TEST_P(OpAdaptiveThresholdVarshape_Negative, op) cvcuda::AdaptiveThreshold adaptiveThresholdOp(blockSize, maxBatch); EXPECT_EQ(expectedReturnCode, nvcv::ProtectCall( - [&] + [&adaptiveThresholdOp, &stream, &batchSrc, &batchDst, &maxValueTensor, + &adaptiveThresholdType, &thresholdType, &blockSizeTensor, &cTensor] { adaptiveThresholdOp(stream, batchSrc, batchDst, maxValueTensor, adaptiveThresholdType, thresholdType, blockSizeTensor, @@ -455,6 +752,123 @@ TEST_P(OpAdaptiveThresholdVarshape_Negative, op) ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); } +TEST(OpAdaptiveThresholdVarshape_Negative, rejects_invalid_parameter_tensor_shapes_and_types) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + constexpr int batch = 2; + constexpr int blockSize = 3; + + AdaptiveThresholdVarShapeTestData data(batch); + + nvcv::Tensor maxValueTensor({{batch}, "N"}, nvcv::TYPE_F64); + CopyTensorData(maxValueTensor, std::vector(batch, 127.0), stream); + nvcv::Tensor blockSizeTensor({{batch}, "N"}, nvcv::TYPE_S32); + CopyTensorData(blockSizeTensor, std::vector(batch, blockSize), stream); + nvcv::Tensor cTensor({{batch}, "N"}, nvcv::TYPE_F64); + CopyTensorData(cTensor, std::vector(batch, 2.5), stream); + + nvcv::Tensor wrongMaxValueType({{batch}, "N"}, nvcv::TYPE_F32); + CopyTensorData(wrongMaxValueType, std::vector(batch, 127.0f), stream); + nvcv::Tensor wrongBlockSizeType({{batch}, "N"}, nvcv::TYPE_F64); + CopyTensorData(wrongBlockSizeType, std::vector(batch, blockSize), stream); + nvcv::Tensor wrongCType({{batch}, "N"}, nvcv::TYPE_F32); + CopyTensorData(wrongCType, std::vector(batch, 2.5f), stream); + nvcv::Tensor wrongCRank( + { + {batch, 1}, + "NC" + }, + nvcv::TYPE_F64); + CopyTensorData(wrongCRank, std::vector(batch, 2.5), stream); + nvcv::Tensor shortBlockSize({{batch - 1}, "N"}, nvcv::TYPE_S32); + CopyTensorData(shortBlockSize, std::vector(batch - 1, blockSize), stream); + + cvcuda::AdaptiveThreshold adaptiveThresholdOp(blockSize, batch); + + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall( + [&adaptiveThresholdOp, &stream, &data, &wrongMaxValueType, &blockSizeTensor, &cTensor] + { + adaptiveThresholdOp(stream, data.batchSrc, data.batchDst, wrongMaxValueType, + NVCV_ADAPTIVE_THRESH_MEAN_C, NVCV_THRESH_BINARY, blockSizeTensor, cTensor); + })); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall( + [&adaptiveThresholdOp, &stream, &data, &maxValueTensor, &wrongBlockSizeType, &cTensor] + { + adaptiveThresholdOp(stream, data.batchSrc, data.batchDst, maxValueTensor, + NVCV_ADAPTIVE_THRESH_MEAN_C, NVCV_THRESH_BINARY, wrongBlockSizeType, cTensor); + })); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall( + [&adaptiveThresholdOp, &stream, &data, &maxValueTensor, &blockSizeTensor, &wrongCType] + { + adaptiveThresholdOp(stream, data.batchSrc, data.batchDst, maxValueTensor, + NVCV_ADAPTIVE_THRESH_MEAN_C, NVCV_THRESH_BINARY, blockSizeTensor, wrongCType); + })); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall( + [&adaptiveThresholdOp, &stream, &data, &maxValueTensor, &blockSizeTensor, &wrongCRank] + { + adaptiveThresholdOp(stream, data.batchSrc, data.batchDst, maxValueTensor, + NVCV_ADAPTIVE_THRESH_MEAN_C, NVCV_THRESH_BINARY, blockSizeTensor, wrongCRank); + })); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall( + [&adaptiveThresholdOp, &stream, &data, &maxValueTensor, &shortBlockSize, &cTensor] + { + adaptiveThresholdOp(stream, data.batchSrc, data.batchDst, maxValueTensor, + NVCV_ADAPTIVE_THRESH_MEAN_C, NVCV_THRESH_BINARY, shortBlockSize, cTensor); + })); + + EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpAdaptiveThresholdVarshape_Negative, rejects_invalid_block_sizes) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + constexpr int batch = 2; + constexpr int maxBlockSize = 3; + + AdaptiveThresholdVarShapeTestData data(batch); + + nvcv::Tensor maxValueTensor({{batch}, "N"}, nvcv::TYPE_F64); + CopyTensorData(maxValueTensor, std::vector(batch, 127.0), stream); + nvcv::Tensor cTensor({{batch}, "N"}, nvcv::TYPE_F64); + CopyTensorData(cTensor, std::vector(batch, 2.5), stream); + + std::vector> invalidBlockSizes{ + {1, 3}, + {2, 3}, + {5, 3}, + }; + + cvcuda::AdaptiveThreshold adaptiveThresholdOp(maxBlockSize, batch); + + for (const auto &blockSizes : invalidBlockSizes) + { + nvcv::Tensor blockSizeTensor({{batch}, "N"}, nvcv::TYPE_S32); + CopyTensorData(blockSizeTensor, blockSizes, stream); + + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall( + [&adaptiveThresholdOp, &stream, &data, &maxValueTensor, &blockSizeTensor, &cTensor] + { + adaptiveThresholdOp(stream, data.batchSrc, data.batchDst, maxValueTensor, + NVCV_ADAPTIVE_THRESH_MEAN_C, NVCV_THRESH_BINARY, blockSizeTensor, + cTensor); + })); + } + + EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + TEST(OpAdaptiveThresholdVarshape_Negative, varshape_hasDifferentFormat) { nvcv::ImageFormat fmt = nvcv::FMT_U8; @@ -471,18 +885,15 @@ TEST(OpAdaptiveThresholdVarshape_Negative, varshape_hasDifferentFormat) { fmt, nvcv::FMT_U16} }; - for (auto testCase : testSet) + for (const auto &[inputFmtExtra, outputFmtExtra] : testSet) { - nvcv::ImageFormat inputFmtExtra = std::get<0>(testCase); - nvcv::ImageFormat outputFmtExtra = std::get<1>(testCase); - cudaStream_t stream; ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); // Create input varshape - std::default_random_engine rng; - std::uniform_int_distribution udistWidth(width * 0.8, width * 1.1); - std::uniform_int_distribution udistHeight(height * 0.8, height * 1.1); + std::default_random_engine rng; + std::uniform_int_distribution udistWidth(ScaledSize(width, 0.8), ScaledSize(width, 1.1)); + std::uniform_int_distribution udistHeight(ScaledSize(height, 0.8), ScaledSize(height, 1.1)); std::vector imgSrc; @@ -513,13 +924,14 @@ TEST(OpAdaptiveThresholdVarshape_Negative, varshape_hasDifferentFormat) // Run operator cvcuda::AdaptiveThreshold adaptiveThresholdOp(blockSize, batch); - EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall( - [&] - { - adaptiveThresholdOp(stream, batchSrc, batchDst, maxValueTensor, - adaptiveThresholdType, thresholdType, - blockSizeTensor, cTensor); - })); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall( + [&adaptiveThresholdOp, &stream, &batchSrc, &batchDst, &maxValueTensor, &adaptiveThresholdType, + &thresholdType, &blockSizeTensor, &cTensor] + { + adaptiveThresholdOp(stream, batchSrc, batchDst, maxValueTensor, adaptiveThresholdType, + thresholdType, blockSizeTensor, cTensor); + })); ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); diff --git a/tests/cvcuda/system/TestOpAdjustContrast.cpp b/tests/cvcuda/system/TestOpAdjustContrast.cpp new file mode 100644 index 000000000..416952773 --- /dev/null +++ b/tests/cvcuda/system/TestOpAdjustContrast.cpp @@ -0,0 +1,509 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "ElementwiseOpHarness.hpp" // for elementwise::Bound / ExpectRejected +#include "PlanarParityUtils.hpp" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace test = nvcv::test; +namespace ew = nvcv::test::elementwise; + +namespace { + +// A fixed contrast factor exercised across the matrix. 1.5 increases contrast, so blending away from +// the grayscale mean pushes the darkest/brightest pixels below 0 / above bound -- exercising both +// clamp directions plus the round-to-nearest SaturateCast path in one factor. +constexpr double kFactor = 1.5; + +// torchvision BT.601 luma weights (match AdjustColorCommon.cuh); the CPU gold is an independent +// reimplementation of the documented formula, not a call into the operator. +constexpr float kLumaR = 0.2989f; +constexpr float kLumaG = 0.587f; +constexpr float kLumaB = 0.114f; + +constexpr int kMeanBlock = 256; + +uint32_t FourCC(const char (&code)[5]) +{ + uint32_t value{}; + std::memcpy(&value, code, sizeof(value)); + return value; +} + +template +T RoundSaturateGold(float value) +{ + if constexpr (std::is_floating_point_v) + { + return value; + } + else + { + const float base = std::floor(value); + const float fraction = value - base; + auto rounded = static_cast(base); + if (fraction > 0.5f || (fraction == 0.5f && (rounded & 1U) != 0)) + { + ++rounded; + } + return static_cast(rounded); + } +} + +// Independent CPU gold for one interleaved HWC image. The host reduction mirrors the documented +// fixed 256-lane tree so both integer and deliberately exact float cases can be compared bit-for-bit. +template +std::vector AdjustContrastGoldImage(const std::vector &in, int width, int height, int channels, double factor) +{ + const int numPixels = width * height; + const auto bound = static_cast(ew::Bound()); + const auto ratio = static_cast(factor); + + std::array partial{}; + for (int p = 0; p < numPixels; ++p) + { + float gray; + if (channels == 1) + { + gray = static_cast(in[p]); + } + else + { + const auto r = static_cast(in[static_cast(p) * channels + 0]); + const auto g = static_cast(in[static_cast(p) * channels + 1]); + const auto b = static_cast(in[static_cast(p) * channels + 2]); + gray = kLumaR * r + kLumaG * g + kLumaB * b; + if constexpr (std::is_integral_v) + { + gray = std::floor(gray); + } + } + partial[p % kMeanBlock] += gray; + } + for (int stride = kMeanBlock / 2; stride > 0; stride >>= 1) + { + for (int i = 0; i < stride; ++i) + { + partial[i] += partial[i + stride]; + } + } + const float mean = partial[0] / static_cast(numPixels); + + std::vector out(in.size()); + for (size_t i = 0; i < in.size(); ++i) + { + float pixel = ratio * static_cast(in[i]) + (1.0f - ratio) * mean; + pixel = std::clamp(pixel, 0.0f, bound); + out[i] = RoundSaturateGold(pixel); + } + return out; +} + +// Fixed-factor invoker for var-shape / parity / negative cases. +auto invokeFactor(double factor) +{ + return [factor](cudaStream_t s, const auto &in, auto &out) + { + cvcuda::AdjustContrast op; + op(s, in, out, factor); + }; +} + +template +void FillContrastFloatPattern(std::vector
&input, int numPixels, int channels) +{ + static_assert(std::is_floating_point_v
); + for (int p = 0; p < numPixels; ++p) + { + input[static_cast(p) * channels] = (p & 1) == 0 ? 0.0f : 1.0f; + for (int c = 1; c < channels; ++c) + { + input[static_cast(p) * channels + c] = 0.0f; + } + } +} + +// Tensor correctness is bit-exact. Float inputs use a deterministic alternating pattern whose luma +// and fixed-tree mean have an exact host/device representation. +template +void RunContrastTensor(int width, int height, int batch, nvcv::ImageFormat fmt, double factor) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + const int channels = fmt.numChannels(); + const size_t count = static_cast(width) * height * channels; + const DT bound = ew::Bound
(); + + nvcv::Tensor inTensor = nvcv::util::CreateTensor(batch, width, height, fmt); + nvcv::Tensor outTensor = nvcv::util::CreateTensor(batch, width, height, fmt); + auto inData = inTensor.exportData(); + auto outData = outTensor.exportData(); + ASSERT_NE(inData, nullptr); + ASSERT_NE(outData, nullptr); + + std::default_random_engine rng(0); + std::vector> inputs(batch); + for (int s = 0; s < batch; ++s) + { + auto &in = inputs[s]; + in.resize(count); + if constexpr (std::is_floating_point_v
) + { + FillContrastFloatPattern(in, width * height, channels); + } + else + { + std::uniform_int_distribution dist(0, static_cast(bound)); + for (auto &v : in) v = static_cast
(dist(rng)); + } + nvcv::util::SetImageTensorFromVector
(*inData, in, s); + } + + ASSERT_NO_THROW(invokeFactor(factor)(stream, inTensor, outTensor)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + for (int s = 0; s < batch; ++s) + { + SCOPED_TRACE(s); + std::vector
got; + nvcv::util::GetImageVectorFromTensor
(*outData, s, got); + std::vector
gold = AdjustContrastGoldImage
(inputs[s], width, height, channels, factor); + ASSERT_EQ(gold.size(), got.size()); + EXPECT_EQ(gold, got); + } + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +// VarShape correctness over one interleaved dtype/channel variant. Float inputs use the same +// deterministic pure-red/black pattern as the Tensor test; factor zero makes the fixed-tree mean +// itself the exact expected output. The largest image selects parallel reduction for the whole +// mixed-size batch; Tensor coverage independently exercises the fallback and nonzero blending. +template +void RunContrastVarShape(nvcv::ImageFormat fmt) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + constexpr int n = 3; + const int ch = fmt.numChannels(); + const double factor = std::is_floating_point_v
? 0.0 : kFactor; + + std::default_random_engine rng(0); + std::uniform_int_distribution sizeDist(20, 90); + std::uniform_int_distribution byteDist(0, 255); + constexpr std::array floatSizes{ + {{1, 1}, {256, 32}, {257, 33}} + }; + + std::vector srcImgs; + std::vector dstImgs; + std::vector ws(n); + std::vector hs(n); + std::vector> hostIn(n); + for (int i = 0; i < n; ++i) + { + if constexpr (std::is_floating_point_v
) + { + ws[i] = floatSizes[i].w; + hs[i] = floatSizes[i].h; + } + else + { + ws[i] = sizeDist(rng); + hs[i] = sizeDist(rng); + } + srcImgs.emplace_back(nvcv::Size2D{ws[i], hs[i]}, fmt); + dstImgs.emplace_back(nvcv::Size2D{ws[i], hs[i]}, fmt); + + const size_t rowElements = static_cast(ws[i]) * ch; + const size_t rowBytes = rowElements * sizeof(DT); + hostIn[i].resize(rowElements * hs[i]); + if constexpr (std::is_floating_point_v
) + { + FillContrastFloatPattern(hostIn[i], ws[i] * hs[i], ch); + } + else + { + for (auto &value : hostIn[i]) value = static_cast
(byteDist(rng)); + } + + auto idata = srcImgs[i].exportData(); + ASSERT_NE(idata, nullptr); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(idata->plane(0).basePtr, idata->plane(0).rowStride, hostIn[i].data(), + rowBytes, rowBytes, hs[i], cudaMemcpyHostToDevice)); + } + + nvcv::ImageBatchVarShape src(n); + nvcv::ImageBatchVarShape dst(n); + src.pushBack(srcImgs.begin(), srcImgs.end()); + dst.pushBack(dstImgs.begin(), dstImgs.end()); + + ASSERT_NO_THROW(invokeFactor(factor)(stream, src, dst)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + for (int i = 0; i < n; ++i) + { + SCOPED_TRACE(i); + const size_t rowElements = static_cast(ws[i]) * ch; + const size_t rowBytes = rowElements * sizeof(DT); + std::vector
got(rowElements * hs[i]); + auto odata = dstImgs[i].exportData(); + ASSERT_NE(odata, nullptr); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(got.data(), rowBytes, odata->plane(0).basePtr, odata->plane(0).rowStride, + rowBytes, hs[i], cudaMemcpyDeviceToHost)); + const std::vector
gold = AdjustContrastGoldImage
(hostIn[i], ws[i], hs[i], ch, factor); + EXPECT_EQ(gold, got); + } + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +} // namespace + +// Tensor correctness over the declared dtype x channel matrix (u8 / f32; 1 / 3 channels) ---------- +// clang-format off +NVCV_TEST_SUITE_P(OpAdjustContrast, test::ValueList +{ + // width, height, batch, format (dtype / channels) + { 66, 55, 1, nvcv::FMT_U8 }, // u8 / 1ch + { 123, 67, 3, nvcv::FMT_RGB8 }, // u8 / 3ch + { 257, 33, 1, nvcv::FMT_F32 }, // f32 / 1ch; parallel reduction + tail + { 257, 33, 2, nvcv::FMT_RGBf32 }, // f32 / 3ch; parallel reduction + tail +}); + +// clang-format on +TEST_P(OpAdjustContrast, tensor_correct_output) +{ + const auto fmt = nvcv::ImageFormat{GetParamValue<3>()}; + if (ew::BaseKind(fmt) == 2) + { + RunContrastTensor(GetParamValue<0>(), GetParamValue<1>(), GetParamValue<2>(), fmt, kFactor); + // The 0/1 pattern clamps back to itself at kFactor, so factor zero separately exposes the + // reduction result and keeps the parallel lane-sharding path bit-exact against the CPU gold. + RunContrastTensor(GetParamValue<0>(), GetParamValue<1>(), GetParamValue<2>(), fmt, 0.0); + // Retain exact-gold coverage of the small-image fallback on both float formats. + RunContrastTensor(16, 16, GetParamValue<2>(), fmt, 0.0); + } + else + { + RunContrastTensor(GetParamValue<0>(), GetParamValue<1>(), GetParamValue<2>(), fmt, kFactor); + } +} + +// VarShape correctness over the declared dtype x channel matrix: bit-exact vs the CPU gold ------- +// clang-format off +NVCV_TEST_SUITE_P(OpAdjustContrastVarShape, test::ValueList +{ + nvcv::FMT_U8, + nvcv::FMT_RGB8, + nvcv::FMT_F32, + nvcv::FMT_RGBf32, +}); + +// clang-format on +TEST_P(OpAdjustContrastVarShape, correct_output) +{ + const nvcv::ImageFormat fmt = GetParam(); + if (ew::BaseKind(fmt) == 2) + { + RunContrastVarShape(fmt); + } + else + { + RunContrastVarShape(fmt); + } +} + +// Planar == interleaved parity ------------------------------------------------------------------ +// clang-format off +NVCV_TEST_SUITE_P(OpAdjustContrastPlanar, + test::ValueList{ + {176, 113, 2, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + {257, 33, 2, nvcv::FMT_RGBf32p, nvcv::FMT_RGBf32}, +}); + +// clang-format on +TEST_P(OpAdjustContrastPlanar, tensor_matches_interleaved) +{ + test::planar::RunTensorParity(GetParamValue<3>(), GetParamValue<4>(), GetParamValue<0>(), GetParamValue<1>(), + GetParamValue<0>(), GetParamValue<1>(), GetParamValue<2>(), + [](cudaStream_t s, const nvcv::Tensor &src, const nvcv::Tensor &dst, + nvcv::ImageFormat) { EXPECT_NO_THROW(invokeFactor(kFactor)(s, src, dst)); }); +} + +TEST_P(OpAdjustContrastPlanar, varshape_matches_interleaved) +{ + test::planar::RunVarShapeParity( + GetParamValue<3>(), GetParamValue<4>(), GetParamValue<0>(), GetParamValue<1>(), GetParamValue<0>(), + GetParamValue<1>(), GetParamValue<2>(), + [](cudaStream_t s, const nvcv::ImageBatchVarShape &src, const nvcv::ImageBatchVarShape &dst, nvcv::ImageFormat) + { EXPECT_NO_THROW(invokeFactor(kFactor)(s, src, dst)); }); +} + +// Negative tests: the complement of the support matrix must be rejected with +// NVCV_ERROR_INVALID_ARGUMENT (ew::ExpectRejected asserts this via nvcv::ProtectCall). +// clang-format off +NVCV_TEST_SUITE_P(OpAdjustContrast_Negative, test::ValueList{ + {nvcv::FMT_F16, nvcv::FMT_F16 }, // unsupported dtype (16-bit float) + {nvcv::FMT_S16, nvcv::FMT_S16 }, // unsupported dtype (signed 16-bit) + {nvcv::FMT_U16, nvcv::FMT_U16 }, // unsupported dtype (unsigned 16-bit) + {nvcv::FMT_RGBA8, nvcv::FMT_RGBA8 }, // unsupported channel count (4, interleaved) + {nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8p}, // unsupported plane count (4-plane planar; not 1 or 3) + {nvcv::FMT_RGB8, nvcv::FMT_RGB8p }, // layout mismatch (interleaved in, planar out) + {nvcv::FMT_RGB8, nvcv::FMT_RGBf32}, // input/output data type mismatch +}); + +// clang-format on +TEST_P(OpAdjustContrast_Negative, rejects_unsupported) +{ + ew::ExpectRejected(GetParamValue<0>(), GetParamValue<1>(), invokeFactor(kFactor)); +} + +TEST(OpAdjustContrast_Negative, rejects_two_channel) +{ + ew::ExpectRejected(nvcv::FMT_2F32, nvcv::FMT_2F32, invokeFactor(kFactor), 16, 16); +} + +// 2-plane planar input (NCHW C=2) has no predefined ImageFormat, so build the tensors directly. +// Before the plane-count guard, this dispatched as planar RGB and read a nonexistent plane 2 in the +// grayscale-mean kernel (out-of-bounds); it must be rejected up front. +TEST(OpAdjustContrast_Negative, rejects_two_plane_planar) +{ + test::planar::ExpectPlanarTensorRejected({1, 2, 16, 16}, {1, 2, 16, 16}, invokeFactor(kFactor)); +} + +TEST(OpAdjustContrast_Negative, rejects_negative_factor) +{ + ew::ExpectRejected(nvcv::FMT_RGB8, nvcv::FMT_RGB8, invokeFactor(-0.5)); + test::planar::ExpectVarShapeRejected({nvcv::FMT_RGB8}, {nvcv::FMT_RGB8}, invokeFactor(-0.5)); +} + +TEST(OpAdjustContrast_Negative, rejects_packed_tensor_dtype) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + nvcv::Tensor src( + { + {1, 8, 8, 1}, + "NHWC" + }, + nvcv::TYPE_3U8); + nvcv::Tensor dst( + { + {1, 8, 8, 1}, + "NHWC" + }, + nvcv::TYPE_3U8); + + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&] { invokeFactor(kFactor)(stream, src, dst); })); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpAdjustContrast, empty_inputs_are_noops) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + nvcv::Tensor srcTensor( + { + {1, 0, 8, 3}, + "NHWC" + }, + nvcv::TYPE_U8); + nvcv::Tensor dstTensor( + { + {1, 0, 8, 3}, + "NHWC" + }, + nvcv::TYPE_U8); + EXPECT_NO_THROW(invokeFactor(kFactor)(stream, srcTensor, dstTensor)); + + nvcv::ImageBatchVarShape srcBatch(1); + nvcv::ImageBatchVarShape dstBatch(1); + EXPECT_NO_THROW(invokeFactor(kFactor)(stream, srcBatch, dstBatch)); + + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpAdjustContrast_Negative, rejects_zero_extent_varshape_image) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + nvcv::Image srcImage({0, 1}, nvcv::FMT_RGB8); + nvcv::Image dstImage({0, 1}, nvcv::FMT_RGB8); + nvcv::ImageBatchVarShape src(1); + nvcv::ImageBatchVarShape dst(1); + src.pushBack(srcImage); + dst.pushBack(dstImage); + + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&] { invokeFactor(kFactor)(stream, src, dst); })); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpAdjustContrast_Negative, rejects_subsampled_varshape) +{ + const nvcv::ImageFormat fmt = nvcv::ImageFormat::FromFourCC(FourCC("I420"), nvcv::CSPEC_BT601, nvcv::MemLayout::PL); + ASSERT_EQ(3, fmt.numPlanes()); + ASSERT_EQ(nvcv::ChromaSubsampling::CSS_420, fmt.chromaSubsampling()); + test::planar::ExpectVarShapeRejected({fmt}, {fmt}, invokeFactor(kFactor)); +} + +TEST(OpAdjustContrast, varshape_bgr_uses_logical_rgb_order) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + nvcv::Image srcImage({1, 1}, nvcv::FMT_BGR8); + nvcv::Image dstImage({1, 1}, nvcv::FMT_BGR8); + auto srcData = srcImage.exportData(); + auto dstData = dstImage.exportData(); + ASSERT_TRUE(srcData); + ASSERT_TRUE(dstData); + + const std::array blue{255, 0, 0}; + ASSERT_EQ(cudaSuccess, cudaMemcpy(srcData->plane(0).basePtr, blue.data(), blue.size(), cudaMemcpyHostToDevice)); + + nvcv::ImageBatchVarShape src(1); + nvcv::ImageBatchVarShape dst(1); + src.pushBack(srcImage); + dst.pushBack(dstImage); + ASSERT_NO_THROW(invokeFactor(0.0)(stream, src, dst)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + std::array got{}; + ASSERT_EQ(cudaSuccess, cudaMemcpy(got.data(), dstData->plane(0).basePtr, got.size(), cudaMemcpyDeviceToHost)); + EXPECT_EQ((std::array{29, 29, 29}), got); + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} diff --git a/tests/cvcuda/system/TestOpAdjustHue.cpp b/tests/cvcuda/system/TestOpAdjustHue.cpp new file mode 100644 index 000000000..26d9bb81a --- /dev/null +++ b/tests/cvcuda/system/TestOpAdjustHue.cpp @@ -0,0 +1,387 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "ElementwiseOpHarness.hpp" // BaseKind, ExpectRejected +#include "PlanarParityUtils.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace test = nvcv::test; +namespace ew = nvcv::test::elementwise; + +namespace { + +// torchvision's float->uint8 scale factor, identical to the kernel (OpAdjustHue.cu). +constexpr float kU8Scale = 255.0f + 1.0f - 1e-3f; + +// Independent device and host evaluation may differ slightly while following the same operation order. +constexpr double kU8MaxDiff = 1; +constexpr double kF32MaxDiff = 2e-6; + +// HSV hue rotation on a normalized RGB pixel — identical arithmetic to the kernel's HsvHueRotate. +void HsvHueRotateGold(float r, float g, float b, float hueShift, float &outR, float &outG, float &outB) +{ + const float maxc = fmaxf(fmaxf(r, g), b); + const float minc = fminf(fminf(r, g), b); + const float v = maxc; + const float cr = maxc - minc; + const bool eqc = (maxc == minc); + + const float s = cr / (eqc ? 1.0f : maxc); + const float crd = eqc ? 1.0f : cr; + const float rc = (maxc - r) / crd; + const float gc = (maxc - g) / crd; + const float bc = (maxc - b) / crd; + + float hh; + if (maxc == r) + { + hh = bc - gc; + } + else if (maxc == g) + { + hh = 2.0f + rc - bc; + } + else + { + hh = 4.0f + gc - rc; + } + + float h = hh / 6.0f + 1.0f; + h = h - floorf(h); + + h = h + hueShift; + h = h - floorf(h); + + const float h6 = h * 6.0f; + const float ii = floorf(h6); + const float f = h6 - ii; + const int i = static_cast(ii) % 6; + + const float sxf = s * f; + const float oneMinusS = 1.0f - s; + const float p = fminf(fmaxf(oneMinusS * v, 0.0f), 1.0f); + const float q = fminf(fmaxf((1.0f - sxf) * v, 0.0f), 1.0f); + const float t = fminf(fmaxf((sxf + oneMinusS) * v, 0.0f), 1.0f); + + switch (i) + { + case 0: + outR = v; + outG = t; + outB = p; + break; + case 1: + outR = q; + outG = v; + outB = p; + break; + case 2: + outR = p; + outG = v; + outB = t; + break; + case 3: + outR = p; + outG = q; + outB = v; + break; + case 4: + outR = t; + outG = p; + outB = v; + break; + default: + outR = v; + outG = p; + outB = q; + break; + } +} + +template +float NormalizeGold(DT v) +{ + if constexpr (std::is_floating_point_v
) + { + return static_cast(v); + } + else + { + return static_cast(v) * (1.0f / 255.0f); + } +} + +template +DT StoreGold(float x) +{ + if constexpr (std::is_floating_point_v
) + { + return static_cast
(x); + } + else + { + float t = truncf(x * kU8Scale); + t = fminf(fmaxf(t, 0.0f), static_cast(std::numeric_limits
::max())); + return static_cast
(t); + } +} + +// Independent CPU gold on an interleaved HWC buffer: rotate each 3-channel pixel's hue; 1-channel is +// identity. Mirrors torchvision.transforms.v2.functional.adjust_hue. +template +std::vector
AdjustHueGold(const std::vector
&in, int channels, double hue) +{ + if (channels == 1) + { + return in; // identity + } + + const auto hueShift = static_cast(hue); + std::vector
out(in.size()); + for (size_t p = 0; p + 3 <= in.size(); p += 3) + { + float oR; + float oG; + float oB; + HsvHueRotateGold(NormalizeGold
(in[p]), NormalizeGold
(in[p + 1]), NormalizeGold
(in[p + 2]), hueShift, + oR, oG, oB); + out[p] = StoreGold
(oR); + out[p + 1] = StoreGold
(oG); + out[p + 2] = StoreGold
(oB); + } + return out; +} + +// Bind the operator with a fixed hue for the parity / negative helpers. +auto invokeHue(double hue) +{ + return [hue](cudaStream_t s, const auto &in, auto &out) + { + cvcuda::AdjustHue op; + op(s, in, out, hue); + }; +} + +} // namespace + +// Tensor correctness over the declared dtype x channel matrix + several hue factors --------------- +// clang-format off +NVCV_TEST_SUITE_P(OpAdjustHue, test::ValueList +{ + // width, height, batch, format hue + { 66, 55, 1, nvcv::FMT_U8, 0.25}, // u8 / 1ch (identity) + { 123, 67, 3, nvcv::FMT_RGB8, 0.25}, // u8 / 3ch + { 50, 40, 2, nvcv::FMT_RGB8, -0.30}, // u8 / 3ch (negative shift wraps) + { 50, 40, 2, nvcv::FMT_RGB8, 0.50}, // u8 / 3ch (edge +0.5) + { 17, 19, 1, nvcv::FMT_F32, 0.25}, // f32 / 1ch (identity) + { 101, 33, 2, nvcv::FMT_RGBf32, 0.10}, // f32 / 3ch + { 60, 48, 1, nvcv::FMT_RGBf32, -0.50}, // f32 / 3ch (edge -0.5) + { 60, 48, 1, nvcv::FMT_RGBf32, 0.00}, // f32 / 3ch (identity) +}); + +// clang-format on +TEST_P(OpAdjustHue, correct_output) +{ + const int w = GetParamValue<0>(); + const int h = GetParamValue<1>(); + const int b = GetParamValue<2>(); + const nvcv::ImageFormat fmt = GetParamValue<3>(); + const double hue = GetParamValue<4>(); + + if (ew::BaseKind(fmt) == 2) + { + ew::RunTensorCorrectBuffer( + w, h, b, fmt, + [hue](const std::vector &in, int channels) { return AdjustHueGold(in, channels, hue); }, + invokeHue(hue), fmt.numChannels() == 3 ? kF32MaxDiff : 0); + } + else + { + ew::RunTensorCorrectBuffer( + w, h, b, fmt, + [hue](const std::vector &in, int channels) { return AdjustHueGold(in, channels, hue); }, + invokeHue(hue), fmt.numChannels() == 3 ? kU8MaxDiff : 0); + } +} + +TEST(OpAdjustHue, varshape_correct_output) +{ + ew::RunVarShapeCorrectBufferTyped( + nvcv::FMT_RGB8, [](const std::vector &in, int channels) { return AdjustHueGold(in, channels, 0.25); }, + invokeHue(0.25), kU8MaxDiff); +} + +TEST(OpAdjustHue, varshape_negative_shift_output) +{ + ew::RunVarShapeCorrectBufferTyped( + nvcv::FMT_RGB8, [](const std::vector &in, int channels) { return AdjustHueGold(in, channels, -0.4); }, + invokeHue(-0.4), kU8MaxDiff); +} + +TEST(OpAdjustHue, zero_extent_tensors_are_noops) +{ + ew::ExpectZeroExtentTensorNoop( + nvcv::TensorShape{ + {0, 3, 5, 1}, + "NHWC" + }, + nvcv::TYPE_U8, invokeHue(0.25)); + ew::ExpectZeroExtentTensorNoop( + nvcv::TensorShape{ + {1, 0, 5, 1}, + "NHWC" + }, + nvcv::TYPE_U8, invokeHue(0.25)); + ew::ExpectZeroExtentTensorNoop( + nvcv::TensorShape{ + {1, 3, 0, 1}, + "NHWC" + }, + nvcv::TYPE_U8, invokeHue(0.25)); +} + +TEST(OpAdjustHue, empty_matching_varshape_is_noop) +{ + ew::ExpectEmptyVarShapeNoop(invokeHue(0.25)); +} + +// Planar == interleaved parity ------------------------------------------------------------------ +// clang-format off +NVCV_TEST_SUITE_P(OpAdjustHuePlanar, + test::ValueList{ + {176, 113, 2, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + {100, 80, 2, nvcv::FMT_RGBf32p, nvcv::FMT_RGBf32}, +}); + +// clang-format on +TEST_P(OpAdjustHuePlanar, tensor_matches_interleaved) +{ + test::planar::RunTensorParity(GetParamValue<3>(), GetParamValue<4>(), GetParamValue<0>(), GetParamValue<1>(), + GetParamValue<0>(), GetParamValue<1>(), GetParamValue<2>(), + [](cudaStream_t s, const nvcv::Tensor &src, const nvcv::Tensor &dst, + nvcv::ImageFormat) { EXPECT_NO_THROW(invokeHue(0.25)(s, src, dst)); }); +} + +TEST_P(OpAdjustHuePlanar, varshape_matches_interleaved) +{ + test::planar::RunVarShapeParity( + GetParamValue<3>(), GetParamValue<4>(), GetParamValue<0>(), GetParamValue<1>(), GetParamValue<0>(), + GetParamValue<1>(), GetParamValue<2>(), + [](cudaStream_t s, const nvcv::ImageBatchVarShape &src, const nvcv::ImageBatchVarShape &dst, nvcv::ImageFormat) + { EXPECT_NO_THROW(invokeHue(0.25)(s, src, dst)); }); +} + +// Negative tests: the complement of the support matrix must be rejected ------------------------- +// clang-format off +NVCV_TEST_SUITE_P(OpAdjustHue_Negative, test::ValueList{ + {nvcv::FMT_U16, nvcv::FMT_U16 }, // unsupported dtype (16-bit unsigned) + {nvcv::FMT_F16, nvcv::FMT_F16 }, // unsupported dtype (16-bit float) + {nvcv::FMT_RGBA8, nvcv::FMT_RGBA8 }, // unsupported channel count (4) + {nvcv::FMT_RGB8, nvcv::FMT_RGB8p }, // layout mismatch (interleaved in, planar out) + {nvcv::FMT_RGB8, nvcv::FMT_RGBf32}, // input/output data type mismatch +}); + +// clang-format on +TEST_P(OpAdjustHue_Negative, rejects_unsupported) +{ + ew::ExpectRejected(GetParamValue<0>(), GetParamValue<1>(), invokeHue(0.25)); +} + +TEST(OpAdjustHue_Negative, rejects_two_channel) +{ + ew::ExpectRejected(nvcv::FMT_2F32, nvcv::FMT_2F32, invokeHue(0.25), 16, 16); +} + +TEST(OpAdjustHue_Negative, rejects_non_rgb_varshape_formats) +{ + ew::ExpectVarShapeRejected(nvcv::FMT_YUV8, invokeHue(0.25)); + ew::ExpectVarShapeRejected(nvcv::FMT_YUV8p, invokeHue(0.25)); + ew::ExpectVarShapeRejected(nvcv::FMT_BGR8, invokeHue(0.25)); + ew::ExpectVarShapeRejected(nvcv::FMT_NV12, invokeHue(0.25)); +} + +TEST(OpAdjustHue_Negative, rejects_vector_tensor_dtype) +{ + nvcv::Tensor src( + nvcv::TensorShape{ + {1, 2, 4, 1}, + "NHWC" + }, + nvcv::TYPE_3U8); + nvcv::Tensor dst( + nvcv::TensorShape{ + {1, 2, 4, 1}, + "NHWC" + }, + nvcv::TYPE_3U8); + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&] { cvcuda::AdjustHue{}(stream, src, dst, 0.25); })); + EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpAdjustHue_Negative, rejects_out_of_range_hue) +{ + ew::ExpectRejected(nvcv::FMT_RGB8, nvcv::FMT_RGB8, invokeHue(0.75)); + ew::ExpectRejected(nvcv::FMT_RGB8, nvcv::FMT_RGB8, invokeHue(-0.75)); +} + +TEST(OpAdjustHue_Negative, rejects_mismatched_varshape_image_sizes) +{ + std::vector srcImages{ + nvcv::Image{{17, 19}, nvcv::FMT_RGB8}, + nvcv::Image{{31, 37}, nvcv::FMT_RGB8} + }; + std::vector dstImages{ + nvcv::Image{{17, 19}, nvcv::FMT_RGB8}, + nvcv::Image{{23, 29}, nvcv::FMT_RGB8} + }; + + nvcv::ImageBatchVarShape src(2); + nvcv::ImageBatchVarShape dst(2); + src.pushBack(srcImages.begin(), srcImages.end()); + dst.pushBack(dstImages.begin(), dstImages.end()); + + cvcuda::AdjustHue op; + try + { + op(nullptr, src, dst, 0.25); + FAIL() << "Expected mismatched image sizes to be rejected"; + } + catch (const nvcv::Exception &e) + { + EXPECT_EQ(nvcv::Status::ERROR_INVALID_ARGUMENT, e.code()); + EXPECT_STREQ("Input and output image 1 sizes must match: input is 31x37, output is 23x29", e.msg()); + } +} + +TEST(OpAdjustHue_Negative, create_null_handle) +{ + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, cvcudaAdjustHueCreate(nullptr)); +} diff --git a/tests/cvcuda/system/TestOpAdjustSaturation.cpp b/tests/cvcuda/system/TestOpAdjustSaturation.cpp new file mode 100644 index 000000000..2abdbb207 --- /dev/null +++ b/tests/cvcuda/system/TestOpAdjustSaturation.cpp @@ -0,0 +1,305 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "ElementwiseOpHarness.hpp" // BaseKind, ExpectRejected +#include "PlanarParityUtils.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace test = nvcv::test; +namespace ew = nvcv::test::elementwise; + +namespace { + +// torchvision luminance coefficients, identical to the kernel (OpAdjustSaturation.cu). +constexpr float kR2Y = 0.2989f; +constexpr float kG2Y = 0.587f; +constexpr float kB2Y = 0.114f; + +// Default device contraction can differ from the independent CPU evaluation by one output level. +constexpr double kU8MaxDiff = 1; +constexpr double kF32MaxDiff = 1e-6; + +// Clamp + store mirroring the kernel's StoreSaturation: integers truncate toward zero into the dtype +// range; floats clamp to [0, 1]. +template +DT StoreGold(float v) +{ + if constexpr (std::is_floating_point_v
) + { + return static_cast
(fminf(fmaxf(v, 0.0f), 1.0f)); + } + else + { + float t = truncf(v); + t = fminf(fmaxf(t, 0.0f), static_cast(std::numeric_limits
::max())); + return static_cast
(t); + } +} + +// Independent CPU gold on an interleaved HWC buffer: blend each 3-channel pixel toward its luminance; +// 1-channel is identity. Mirrors torchvision.transforms.v2.functional.adjust_saturation. +template +std::vector
AdjustSaturationGold(const std::vector
&in, int channels, double saturation) +{ + if (channels == 1) + { + return in; // identity + } + + const auto ratio = static_cast(saturation); + const auto oneMinus = static_cast(1.0 - saturation); + std::vector
out(in.size()); + for (size_t p = 0; p + 3 <= in.size(); p += 3) + { + const auto r = static_cast(in[p]); + const auto g = static_cast(in[p + 1]); + const auto b = static_cast(in[p + 2]); + + float gray = (kR2Y * r + kG2Y * g) + kB2Y * b; + if constexpr (!std::is_floating_point_v
) + { + gray = floorf(gray); + } + + out[p] = StoreGold
(ratio * r + oneMinus * gray); + out[p + 1] = StoreGold
(ratio * g + oneMinus * gray); + out[p + 2] = StoreGold
(ratio * b + oneMinus * gray); + } + return out; +} + +// Bind the operator with a fixed saturation for the parity / negative helpers. +auto invokeSat(double saturation) +{ + return [saturation](cudaStream_t s, const auto &in, auto &out) + { + cvcuda::AdjustSaturation op; + op(s, in, out, saturation); + }; +} + +} // namespace + +// Tensor correctness over the declared dtype x channel matrix + edge factors -------------------- +// clang-format off +NVCV_TEST_SUITE_P(OpAdjustSaturation, test::ValueList +{ + // width, height, batch, format saturation + { 66, 55, 1, nvcv::FMT_U8, 0.5}, // u8 / 1ch (identity) + { 123, 67, 3, nvcv::FMT_RGB8, 0.5}, // u8 / 3ch blend + { 50, 40, 2, nvcv::FMT_RGB8, 0.0}, // u8 / 3ch -> grayscale + { 50, 40, 2, nvcv::FMT_RGB8, 1.7}, // u8 / 3ch over-saturate (clamp to 255) + { 17, 19, 1, nvcv::FMT_F32, 0.5}, // f32 / 1ch (identity) + { 101, 33, 2, nvcv::FMT_RGBf32, 0.5}, // f32 / 3ch blend + { 60, 48, 1, nvcv::FMT_RGBf32, 1.5}, // f32 / 3ch over-saturate (clamp to 1) + { 60, 48, 1, nvcv::FMT_RGBf32, 0.0}, // f32 / 3ch -> grayscale +}); + +// clang-format on +TEST_P(OpAdjustSaturation, correct_output) +{ + const int w = GetParamValue<0>(); + const int h = GetParamValue<1>(); + const int b = GetParamValue<2>(); + const nvcv::ImageFormat fmt = GetParamValue<3>(); + const double sat = GetParamValue<4>(); + + if (ew::BaseKind(fmt) == 2) + { + ew::RunTensorCorrectBuffer( + w, h, b, fmt, + [sat](const std::vector &in, int channels) { return AdjustSaturationGold(in, channels, sat); }, + invokeSat(sat), fmt.numChannels() == 3 ? kF32MaxDiff : 0); + } + else + { + ew::RunTensorCorrectBuffer( + w, h, b, fmt, + [sat](const std::vector &in, int channels) { return AdjustSaturationGold(in, channels, sat); }, + invokeSat(sat), fmt.numChannels() == 3 ? kU8MaxDiff : 0); + } +} + +TEST(OpAdjustSaturation, varshape_correct_output) +{ + ew::RunVarShapeCorrectBufferTyped( + nvcv::FMT_RGB8, + [](const std::vector &in, int channels) { return AdjustSaturationGold(in, channels, 0.5); }, + invokeSat(0.5), kU8MaxDiff); +} + +TEST(OpAdjustSaturation, varshape_grayscale_output) +{ + ew::RunVarShapeCorrectBufferTyped( + nvcv::FMT_RGB8, + [](const std::vector &in, int channels) { return AdjustSaturationGold(in, channels, 0.0); }, + invokeSat(0.0), kU8MaxDiff); +} + +TEST(OpAdjustSaturation, zero_extent_tensors_are_noops) +{ + ew::ExpectZeroExtentTensorNoop( + nvcv::TensorShape{ + {0, 3, 5, 1}, + "NHWC" + }, + nvcv::TYPE_U8, invokeSat(0.5)); + ew::ExpectZeroExtentTensorNoop( + nvcv::TensorShape{ + {1, 0, 5, 1}, + "NHWC" + }, + nvcv::TYPE_U8, invokeSat(0.5)); + ew::ExpectZeroExtentTensorNoop( + nvcv::TensorShape{ + {1, 3, 0, 1}, + "NHWC" + }, + nvcv::TYPE_U8, invokeSat(0.5)); +} + +TEST(OpAdjustSaturation, empty_matching_varshape_is_noop) +{ + ew::ExpectEmptyVarShapeNoop(invokeSat(0.5)); +} + +// Planar == interleaved parity ------------------------------------------------------------------ +// clang-format off +NVCV_TEST_SUITE_P(OpAdjustSaturationPlanar, + test::ValueList{ + {176, 113, 2, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + {100, 80, 2, nvcv::FMT_RGBf32p, nvcv::FMT_RGBf32}, +}); + +// clang-format on +TEST_P(OpAdjustSaturationPlanar, tensor_matches_interleaved) +{ + test::planar::RunTensorParity(GetParamValue<3>(), GetParamValue<4>(), GetParamValue<0>(), GetParamValue<1>(), + GetParamValue<0>(), GetParamValue<1>(), GetParamValue<2>(), + [](cudaStream_t s, const nvcv::Tensor &src, const nvcv::Tensor &dst, + nvcv::ImageFormat) { EXPECT_NO_THROW(invokeSat(0.5)(s, src, dst)); }); +} + +TEST_P(OpAdjustSaturationPlanar, varshape_matches_interleaved) +{ + test::planar::RunVarShapeParity( + GetParamValue<3>(), GetParamValue<4>(), GetParamValue<0>(), GetParamValue<1>(), GetParamValue<0>(), + GetParamValue<1>(), GetParamValue<2>(), + [](cudaStream_t s, const nvcv::ImageBatchVarShape &src, const nvcv::ImageBatchVarShape &dst, nvcv::ImageFormat) + { EXPECT_NO_THROW(invokeSat(0.5)(s, src, dst)); }); +} + +// Negative tests: the complement of the support matrix must be rejected ------------------------- +// clang-format off +NVCV_TEST_SUITE_P(OpAdjustSaturation_Negative, test::ValueList{ + {nvcv::FMT_U16, nvcv::FMT_U16 }, // unsupported dtype (16-bit unsigned) + {nvcv::FMT_F16, nvcv::FMT_F16 }, // unsupported dtype (16-bit float) + {nvcv::FMT_RGBA8, nvcv::FMT_RGBA8 }, // unsupported channel count (4) + {nvcv::FMT_RGB8, nvcv::FMT_RGB8p }, // layout mismatch (interleaved in, planar out) + {nvcv::FMT_RGB8, nvcv::FMT_RGBf32}, // input/output data type mismatch +}); + +// clang-format on +TEST_P(OpAdjustSaturation_Negative, rejects_unsupported) +{ + ew::ExpectRejected(GetParamValue<0>(), GetParamValue<1>(), invokeSat(0.5)); +} + +TEST(OpAdjustSaturation_Negative, rejects_two_channel) +{ + ew::ExpectRejected(nvcv::FMT_2F32, nvcv::FMT_2F32, invokeSat(0.5), 16, 16); +} + +TEST(OpAdjustSaturation_Negative, rejects_non_rgb_varshape_formats) +{ + ew::ExpectVarShapeRejected(nvcv::FMT_YUV8, invokeSat(0.5)); + ew::ExpectVarShapeRejected(nvcv::FMT_YUV8p, invokeSat(0.5)); + ew::ExpectVarShapeRejected(nvcv::FMT_BGR8, invokeSat(0.5)); + ew::ExpectVarShapeRejected(nvcv::FMT_NV12, invokeSat(0.5)); +} + +TEST(OpAdjustSaturation_Negative, rejects_vector_tensor_dtype) +{ + nvcv::Tensor src( + nvcv::TensorShape{ + {1, 2, 4, 1}, + "NHWC" + }, + nvcv::TYPE_3U8); + nvcv::Tensor dst( + nvcv::TensorShape{ + {1, 2, 4, 1}, + "NHWC" + }, + nvcv::TYPE_3U8); + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&] { cvcuda::AdjustSaturation{}(stream, src, dst, 0.5); })); + EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpAdjustSaturation_Negative, rejects_negative_saturation) +{ + ew::ExpectRejected(nvcv::FMT_RGB8, nvcv::FMT_RGB8, invokeSat(-1.0)); +} + +TEST(OpAdjustSaturation_Negative, rejects_mismatched_varshape_image_sizes) +{ + std::vector srcImages{ + nvcv::Image{{17, 19}, nvcv::FMT_RGB8}, + nvcv::Image{{31, 37}, nvcv::FMT_RGB8} + }; + std::vector dstImages{ + nvcv::Image{{17, 19}, nvcv::FMT_RGB8}, + nvcv::Image{{23, 29}, nvcv::FMT_RGB8} + }; + + nvcv::ImageBatchVarShape src(2); + nvcv::ImageBatchVarShape dst(2); + src.pushBack(srcImages.begin(), srcImages.end()); + dst.pushBack(dstImages.begin(), dstImages.end()); + + cvcuda::AdjustSaturation op; + try + { + op(nullptr, src, dst, 0.5); + FAIL() << "Expected mismatched image sizes to be rejected"; + } + catch (const nvcv::Exception &e) + { + EXPECT_EQ(nvcv::Status::ERROR_INVALID_ARGUMENT, e.code()); + EXPECT_STREQ("Input and output image 1 sizes must match: input is 31x37, output is 23x29", e.msg()); + } +} + +TEST(OpAdjustSaturation_Negative, create_null_handle) +{ + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, cvcudaAdjustSaturationCreate(nullptr)); +} diff --git a/tests/cvcuda/system/TestOpAdjustSharpness.cpp b/tests/cvcuda/system/TestOpAdjustSharpness.cpp new file mode 100644 index 000000000..5cc099d1c --- /dev/null +++ b/tests/cvcuda/system/TestOpAdjustSharpness.cpp @@ -0,0 +1,514 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "Definitions.hpp" +#include "PlanarParityUtils.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace test = nvcv::test; + +namespace { + +// Unsigned 16-bit RGB/RGBA formats are not predefined by NVCV. +#define NVCV_IMAGE_FORMAT_RGB16U \ + NVCV_DETAIL_MAKE_COLOR_FMT1(RGB, UNDEFINED, PL, UNSIGNED, XYZ1, ASSOCIATED, X16_Y16_Z16) +#define NVCV_IMAGE_FORMAT_RGBA16U \ + NVCV_DETAIL_MAKE_COLOR_FMT1(RGB, UNDEFINED, PL, UNSIGNED, XYZW, ASSOCIATED, X16_Y16_Z16_W16) +#define NVCV_IMAGE_FORMAT_RGB16Up \ + NVCV_DETAIL_MAKE_COLOR_FMT3(RGB, UNDEFINED, PL, UNSIGNED, XYZ0, ASSOCIATED, X16, X16, X16) +#define NVCV_IMAGE_FORMAT_RGBA16Up \ + NVCV_DETAIL_MAKE_COLOR_FMT4(RGB, UNDEFINED, PL, UNSIGNED, XYZW, ASSOCIATED, X16, X16, X16, X16) + +// Blend clamp bound per base type: dtype max for unsigned integers, 1.0 for float. Mirrors +// SharpnessBound() in the kernel and torchvision's _max_value. +template +float SharpnessRefBound() +{ + if constexpr (std::is_floating_point_v) + { + return 1.0f; + } + else + { + return static_cast(std::numeric_limits::max()); + } +} + +// Independent CPU gold for a single interior pixel/channel. This is a deliberate reimplementation of +// the documented oracle (torchvision adjust_sharpness), computed independently of the kernel. It uses +// the SAME fixed accumulation order and explicit `fmaf`/`rintf`, so the correctly-rounded IEEE-754 +// result is bit-identical to the device kernel (AdjustSharpnessScalar in OpAdjustSharpness.cu) and +// the bit-exact EXPECT_EQ below is a real regression check. `c11` is the center (original) pixel. +template +BT AdjustSharpnessRefScalar(float c00, float c01, float c02, float c10, float c11, float c12, float c20, float c21, + float c22, float oneMinusFactor, float bound) +{ + constexpr float kEdge = 1.0f / 13.0f; + constexpr float kCenter = 5.0f / 13.0f; + + float blur = c11 * kCenter; + blur = std::fmaf(c00, kEdge, blur); + blur = std::fmaf(c01, kEdge, blur); + blur = std::fmaf(c02, kEdge, blur); + blur = std::fmaf(c10, kEdge, blur); + blur = std::fmaf(c12, kEdge, blur); + blur = std::fmaf(c20, kEdge, blur); + blur = std::fmaf(c21, kEdge, blur); + blur = std::fmaf(c22, kEdge, blur); + + if constexpr (!std::is_floating_point_v) + { + blur = std::rintf(blur); + } + + const float out = std::fmaf(oneMinusFactor, blur - c11, c11); + const float clamped = std::fminf(std::fmaxf(out, 0.0f), bound); + return static_cast(clamped); +} + +// Byte-offset of element (n, y, x, c) in an interleaved (N)HWC buffer described by `strides` +// {sampleStride, rowStride, colStride} in bytes. Channel c sits `c*sizeof(BT)` into the pixel. +template +long ElemOffset(long3 strides, int n, int y, int x, int c) +{ + return n * strides.x + y * strides.y + x * strides.z + static_cast(c) * static_cast(sizeof(BT)); +} + +template +BT ReadElem(const std::vector &buf, long3 strides, int n, int y, int x, int c) +{ + BT v; + std::memcpy(&v, &buf[ElemOffset(strides, n, y, x, c)], sizeof(BT)); + return v; +} + +template +void WriteElem(std::vector &buf, long3 strides, int n, int y, int x, int c, BT v) +{ + std::memcpy(&buf[ElemOffset(strides, n, y, x, c)], &v, sizeof(BT)); +} + +// Fill an interleaved buffer with per-element typed random data: float in [0, 1] (matching a +// normalized float image, keeping the blend finite), integer in [0, dtype-max]. +template +void FillTypedRandom(std::vector &buf, long3 strides, int width, int height, int batches, int channels, + unsigned seed) +{ + std::mt19937 rng(seed); + for (int i = 0; i < batches * height * width; ++i) + { + const int n = i / (height * width); + const int y = i / width % height; + const int x = i % width; + for (int c = 0; c < channels; ++c) + { + BT v; + if constexpr (std::is_floating_point_v) + { + std::uniform_real_distribution dist(0.0f, 1.0f); + v = static_cast(dist(rng)); + } + else + { + std::uniform_int_distribution dist(0, static_cast(std::numeric_limits::max())); + v = static_cast(dist(rng)); + } + WriteElem(buf, strides, n, y, x, c, v); + } + } +} + +// Full-buffer CPU gold: interior pixels are blended, the 1-pixel border is copied unchanged, and any +// image with a dimension < 3 is copied in full (every pixel is a border pixel). +template +void AdjustSharpnessGold(std::vector &dst, const std::vector &src, long3 strides, int width, + int height, int batches, int channels, float factor) +{ + const float oneMinusFactor = 1.0f - factor; + const float bound = SharpnessRefBound(); + for (int i = 0; i < batches * height * width; ++i) + { + const int n = i / (height * width); + const int y = i / width % height; + const int x = i % width; + const bool border = (x == 0 || y == 0 || x == width - 1 || y == height - 1); + for (int c = 0; c < channels; ++c) + { + if (border) + { + WriteElem(dst, strides, n, y, x, c, ReadElem(src, strides, n, y, x, c)); + continue; + } + const auto c00 = static_cast(ReadElem(src, strides, n, y - 1, x - 1, c)); + const auto c01 = static_cast(ReadElem(src, strides, n, y - 1, x, c)); + const auto c02 = static_cast(ReadElem(src, strides, n, y - 1, x + 1, c)); + const auto c10 = static_cast(ReadElem(src, strides, n, y, x - 1, c)); + const auto c11 = static_cast(ReadElem(src, strides, n, y, x, c)); + const auto c12 = static_cast(ReadElem(src, strides, n, y, x + 1, c)); + const auto c20 = static_cast(ReadElem(src, strides, n, y + 1, x - 1, c)); + const auto c21 = static_cast(ReadElem(src, strides, n, y + 1, x, c)); + const auto c22 = static_cast(ReadElem(src, strides, n, y + 1, x + 1, c)); + WriteElem( + dst, strides, n, y, x, c, + AdjustSharpnessRefScalar(c00, c01, c02, c10, c11, c12, c20, c21, c22, oneMinusFactor, bound)); + } + } +} + +// Run the tensor path for one dtype and assert bit-exact equality with the CPU gold. +template +void RunCorrectness(int width, int height, int batches, nvcv::ImageFormat format, float factor) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + const int channels = format.numChannels(); + + nvcv::Tensor inTensor = nvcv::util::CreateTensor(batches, width, height, format); + nvcv::Tensor outTensor = nvcv::util::CreateTensor(batches, width, height, format); + + auto inData = inTensor.exportData(); + auto outData = outTensor.exportData(); + ASSERT_NE(inData, nullptr); + ASSERT_NE(outData, nullptr); + + auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*inData); + auto outAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*outData); + ASSERT_TRUE(inAccess && outAccess); + + long3 strides{inAccess->sampleStride(), inAccess->rowStride(), inAccess->colStride()}; + if (inData->rank() == 3) // HWC: no sample dimension, one image + { + strides.x = inAccess->numRows() * inAccess->rowStride(); + } + const long bufSize = strides.x * batches; + + std::vector inVec(bufSize, 0); + std::vector goldVec(bufSize, 0); + std::vector testVec(bufSize, 0); + + FillTypedRandom(inVec, strides, width, height, batches, channels, /*seed=*/7u); + + // Zero-fill the device output so any inter-row/-pixel padding compares equal to the zero-filled + // gold (the kernel writes only valid pixels). + ASSERT_EQ(cudaSuccess, cudaMemset(outData->basePtr(), 0, bufSize)); + ASSERT_EQ(cudaSuccess, cudaMemcpy(inData->basePtr(), inVec.data(), bufSize, cudaMemcpyHostToDevice)); + + cvcuda::AdjustSharpness op; + EXPECT_NO_THROW(op(stream, inTensor, outTensor, factor)); + + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + ASSERT_EQ(cudaSuccess, cudaMemcpy(testVec.data(), outData->basePtr(), bufSize, cudaMemcpyDeviceToHost)); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + + AdjustSharpnessGold(goldVec, inVec, strides, width, height, batches, channels, factor); + + EXPECT_EQ(testVec, goldVec); +} + +// Dispatch a fixed-format test case to the matching base type. +void RunCorrectnessDispatch(int width, int height, int batches, nvcv::ImageFormat format, float factor) +{ + const nvcv::DataType dtype = format.planeDataType(0); + if (dtype == nvcv::TYPE_U8 || dtype == nvcv::TYPE_3U8 || dtype == nvcv::TYPE_4U8) + { + RunCorrectness(width, height, batches, format, factor); + } + else if (dtype == nvcv::TYPE_U16 || dtype == nvcv::TYPE_3U16 || dtype == nvcv::TYPE_4U16) + { + RunCorrectness(width, height, batches, format, factor); + } + else if (dtype == nvcv::TYPE_F32 || dtype == nvcv::TYPE_3F32 || dtype == nvcv::TYPE_4F32) + { + RunCorrectness(width, height, batches, format, factor); + } + else + { + FAIL() << "Unhandled format in RunCorrectnessDispatch"; + } +} + +// Create in/out tensors with the given formats and require the op to reject them. +void ExpectTensorRejected(nvcv::ImageFormat inFmt, nvcv::ImageFormat outFmt, float factor = 1.0f, int width = 24, + int height = 24) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + nvcv::Tensor inTensor = nvcv::util::CreateTensor(1, width, height, inFmt); + nvcv::Tensor outTensor = nvcv::util::CreateTensor(1, width, height, outFmt); + + cvcuda::AdjustSharpness op; + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&] { op(stream, inTensor, outTensor, factor); })); + + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +} // namespace + +// Tensor correctness over the declared dtype x channel matrix, plus factor sweep and small-image +// (border-only) passthrough. Bit-exact vs an independent CPU gold. +// clang-format off +NVCV_TEST_SUITE_P(OpAdjustSharpness, test::ValueList +{ + // width, height, batch, format, factor + { 66, 55, 1, nvcv::FMT_U8, 2.0f}, // u8 / 1ch, sharpen + { 35, 33, 1, nvcv::FMT_U8, 0.0f}, // u8 / 1ch, fully smoothed + { 123, 67, 3, nvcv::FMT_RGB8, 0.5f}, // u8 / 3ch + { 42, 53, 4, nvcv::FMT_RGBA8, 2.0f}, // u8 / 4ch + { 40, 40, 2, nvcv::FMT_RGB8, 1.0f}, // u8 / 3ch, identity + { 80, 40, 2, nvcv::FMT_U16, 1.5f}, // u16 / 1ch + { 51, 49, 1, nvcv::FMT_U16, 0.0f}, // u16 / 1ch, fully smoothed + { 47, 39, 2, nvcv::ImageFormat{NVCV_IMAGE_FORMAT_RGB16U}, 0.5f}, // u16 / 3ch + { 31, 37, 1, nvcv::ImageFormat{NVCV_IMAGE_FORMAT_RGBA16U}, 2.0f}, // u16 / 4ch + { 17, 19, 1, nvcv::FMT_F32, 2.0f}, // f32 / 1ch + { 101, 33, 2, nvcv::FMT_RGBf32, 0.5f}, // f32 / 3ch + { 64, 48, 3, nvcv::FMT_RGBAf32, 2.0f}, // f32 / 4ch + { 23, 21, 1, nvcv::FMT_RGBf32, 1.0f}, // f32 / 3ch, identity + { 3, 3, 1, nvcv::FMT_RGB8, 0.5f}, // minimal interior (single interior pixel) + { 2, 10, 1, nvcv::FMT_RGB8, 0.0f}, // width 2 -> all border, passthrough + { 10, 2, 2, nvcv::FMT_U8, 0.0f}, // height 2 -> all border, passthrough + { 1, 1, 1, nvcv::FMT_F32, 2.0f}, // 1x1 -> passthrough +}); + +// clang-format on +TEST_P(OpAdjustSharpness, correct_output) +{ + RunCorrectnessDispatch(GetParamValue<0>(), GetParamValue<1>(), GetParamValue<2>(), + nvcv::ImageFormat{GetParamValue<3>()}, GetParamValue<4>()); +} + +// VarShape correctness: bit-exact vs the CPU gold across the declared dtype x channel matrix. Each +// image has its own size, so the gold is computed per image from its own strided buffer. +template +void RunVarShapeCorrectness(int batches, nvcv::ImageFormat format, float factor) +{ + const int chans = format.numChannels(); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + std::mt19937 rng(11); + std::uniform_int_distribution udW(60, 90); + std::uniform_int_distribution udH(50, 70); + + std::vector imgSrc; + std::vector imgDst; + std::vector> srcVec(batches); + std::vector rowStride(batches); + std::vector sizes(batches); + + for (int i = 0; i < batches; ++i) + { + const int w = udW(rng); + const int h = udH(rng); + sizes[i] = int2{w, h}; + imgSrc.emplace_back(nvcv::Size2D{w, h}, format); + imgDst.emplace_back(nvcv::Size2D{w, h}, format); + + rowStride[i] = w * format.planePixelStrideBytes(0); + srcVec[i].resize(static_cast(h) * rowStride[i], 0); + long3 str{static_cast(h) * rowStride[i], rowStride[i], format.planePixelStrideBytes(0)}; + FillTypedRandom(srcVec[i], str, w, h, 1, chans, 100u + i); + + auto imgData = imgSrc[i].exportData(); + ASSERT_NE(imgData, nvcv::NullOpt); + ASSERT_EQ(cudaSuccess, + cudaMemcpy2DAsync(imgData->plane(0).basePtr, imgData->plane(0).rowStride, srcVec[i].data(), + rowStride[i], rowStride[i], h, cudaMemcpyHostToDevice, stream)); + } + + nvcv::ImageBatchVarShape batchSrc(batches); + nvcv::ImageBatchVarShape batchDst(batches); + batchSrc.pushBack(imgSrc.begin(), imgSrc.end()); + batchDst.pushBack(imgDst.begin(), imgDst.end()); + + cvcuda::AdjustSharpness op; + EXPECT_NO_THROW(op(stream, batchSrc, batchDst, factor)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + for (int i = 0; i < batches; ++i) + { + SCOPED_TRACE(i); + const int w = sizes[i].x; + const int h = sizes[i].y; + long3 str{static_cast(h) * rowStride[i], rowStride[i], format.planePixelStrideBytes(0)}; + + std::vector testVec(static_cast(h) * rowStride[i], 0); + std::vector goldVec(static_cast(h) * rowStride[i], 0); + + auto dstData = imgDst[i].exportData(); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(testVec.data(), rowStride[i], dstData->plane(0).basePtr, + dstData->plane(0).rowStride, rowStride[i], h, cudaMemcpyDeviceToHost)); + + AdjustSharpnessGold(goldVec, srcVec[i], str, w, h, 1, chans, factor); + EXPECT_EQ(testVec, goldVec); + } + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +static void RunVarShapeCorrectnessDispatch(int batches, nvcv::ImageFormat format, float factor) +{ + const nvcv::DataType dtype = format.planeDataType(0); + if (dtype == nvcv::TYPE_U8 || dtype == nvcv::TYPE_3U8 || dtype == nvcv::TYPE_4U8) + { + RunVarShapeCorrectness(batches, format, factor); + } + else if (dtype == nvcv::TYPE_U16 || dtype == nvcv::TYPE_3U16 || dtype == nvcv::TYPE_4U16) + { + RunVarShapeCorrectness(batches, format, factor); + } + else if (dtype == nvcv::TYPE_F32 || dtype == nvcv::TYPE_3F32 || dtype == nvcv::TYPE_4F32) + { + RunVarShapeCorrectness(batches, format, factor); + } + else + { + FAIL() << "Unhandled format in RunVarShapeCorrectnessDispatch"; + } +} + +// clang-format off +NVCV_TEST_SUITE_P(OpAdjustSharpnessVarShape, test::ValueList{ + {2, nvcv::FMT_U8, 2.0f}, + {3, nvcv::FMT_RGB8, 0.5f}, + {2, nvcv::FMT_RGBA8, 1.5f}, + {2, nvcv::FMT_U16, 0.5f}, + {3, nvcv::ImageFormat{NVCV_IMAGE_FORMAT_RGB16U}, 2.0f}, + {2, nvcv::ImageFormat{NVCV_IMAGE_FORMAT_RGBA16U}, 1.5f}, + {2, nvcv::FMT_F32, 2.0f}, + {3, nvcv::FMT_RGBf32, 0.5f}, + {2, nvcv::FMT_RGBAf32, 1.5f}, +}); + +// clang-format on + +TEST_P(OpAdjustSharpnessVarShape, correct_output) +{ + RunVarShapeCorrectnessDispatch(GetParamValue<0>(), nvcv::ImageFormat{GetParamValue<1>()}, GetParamValue<2>()); +} + +// Planar == interleaved parity (the op treats channels independently). --------------------------- +// clang-format off +NVCV_TEST_SUITE_P(OpAdjustSharpnessPlanar, + test::ValueList{ + {176, 113, 2, 2.0f, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + { 67, 51, 1, 0.5f, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + { 64, 48, 1, 2.0f, nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8}, + { 73, 61, 2, 0.5f, nvcv::ImageFormat{NVCV_IMAGE_FORMAT_RGB16Up}, + nvcv::ImageFormat{NVCV_IMAGE_FORMAT_RGB16U}}, + { 58, 46, 1, 2.0f, nvcv::ImageFormat{NVCV_IMAGE_FORMAT_RGBA16Up}, + nvcv::ImageFormat{NVCV_IMAGE_FORMAT_RGBA16U}}, + { 50, 40, 2, 1.5f, nvcv::FMT_RGBf32p, nvcv::FMT_RGBf32}, + {100, 80, 2, 0.5f, nvcv::FMT_RGBAf32p, nvcv::FMT_RGBAf32}, +}); + +// clang-format on +TEST_P(OpAdjustSharpnessPlanar, tensor_matches_interleaved) +{ + const float factor = GetParamValue<3>(); + test::planar::RunTensorParity( + GetParamValue<4>(), GetParamValue<5>(), GetParamValue<0>(), GetParamValue<1>(), GetParamValue<0>(), + GetParamValue<1>(), GetParamValue<2>(), + [factor](cudaStream_t s, const nvcv::Tensor &src, const nvcv::Tensor &dst, nvcv::ImageFormat) + { + cvcuda::AdjustSharpness op; + EXPECT_NO_THROW(op(s, src, dst, factor)); + }); +} + +TEST_P(OpAdjustSharpnessPlanar, varshape_matches_interleaved) +{ + if (GetParamValue<4>() == nvcv::FMT_RGBA8p) + { + GTEST_SKIP() << "uchar4 planar var-shape unsupported by the image API"; + } + const float factor = GetParamValue<3>(); + test::planar::RunVarShapeParity(GetParamValue<4>(), GetParamValue<5>(), GetParamValue<0>(), GetParamValue<1>(), + GetParamValue<0>(), GetParamValue<1>(), GetParamValue<2>(), + [factor](cudaStream_t s, const nvcv::ImageBatchVarShape &src, + const nvcv::ImageBatchVarShape &dst, nvcv::ImageFormat) + { + cvcuda::AdjustSharpness op; + EXPECT_NO_THROW(op(s, src, dst, factor)); + }); +} + +// Negative tests: the complement of the support matrix must be rejected. ------------------------- +// clang-format off +NVCV_TEST_SUITE_P(OpAdjustSharpness_Negative, test::ValueList{ + {nvcv::FMT_F16, nvcv::FMT_F16 }, // unsupported dtype (16-bit float) + {nvcv::FMT_S16, nvcv::FMT_S16 }, // unsupported dtype (signed 16-bit) + {nvcv::FMT_RGB8, nvcv::FMT_RGB8p }, // layout mismatch (interleaved in, planar out) + {nvcv::FMT_RGB8, nvcv::FMT_RGBf32}, // input/output data type mismatch +}); + +// clang-format on +TEST_P(OpAdjustSharpness_Negative, rejects_unsupported) +{ + ExpectTensorRejected(GetParamValue<0>(), GetParamValue<1>()); +} + +TEST(OpAdjustSharpness_Negative, rejects_two_channel) +{ + ExpectTensorRejected(nvcv::FMT_2F32, nvcv::FMT_2F32, 1.0f, 16, 16); +} + +TEST(OpAdjustSharpness_Negative, rejects_negative_factor) +{ + ExpectTensorRejected(nvcv::FMT_RGB8, nvcv::FMT_RGB8, -0.1f); +} + +TEST(OpAdjustSharpness_Negative, varshape_rejects_negative_factor) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + nvcv::Image srcImage({16, 16}, nvcv::FMT_RGB8); + nvcv::Image dstImage({16, 16}, nvcv::FMT_RGB8); + nvcv::ImageBatchVarShape srcBatch(1); + nvcv::ImageBatchVarShape dstBatch(1); + srcBatch.pushBack(srcImage); + dstBatch.pushBack(dstImage); + + cvcuda::AdjustSharpness op; + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&op, stream, &srcBatch, &dstBatch] { op(stream, srcBatch, dstBatch, -0.1f); })); + + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpAdjustSharpness_Negative, create_null_handle) +{ + EXPECT_EQ(cvcudaAdjustSharpnessCreate(nullptr), NVCV_ERROR_INVALID_ARGUMENT); +} diff --git a/tests/cvcuda/system/TestOpAdvCvtColor.cpp b/tests/cvcuda/system/TestOpAdvCvtColor.cpp index 83eb97909..8330d1368 100644 --- a/tests/cvcuda/system/TestOpAdvCvtColor.cpp +++ b/tests/cvcuda/system/TestOpAdvCvtColor.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,6 +16,7 @@ */ #include "Definitions.hpp" +#include "PlanarParityUtils.hpp" #include #include @@ -27,6 +28,7 @@ #include #include +#include #include #include @@ -38,27 +40,24 @@ namespace mmath = nvcv::cuda::math; template using Matrix3x3 = mmath::Matrix; -static const Matrix3x3 getRGB2YUVMatrix(nvcv::ColorSpec spec) +static Matrix3x3 getRGB2YUVMatrix(nvcv::ColorSpec spec) { Matrix3x3 matrix; - switch (spec) + switch (static_cast(spec)) { case NVCV_COLOR_SPEC_BT601: { - const float values[] = {0.299, 0.587, 0.114, -0.168736, -0.331264, 0.5, 0.5, -0.418688, -0.0813124}; - matrix.load(values); + matrix.load({0.299f, 0.587f, 0.114f, -0.168736f, -0.331264f, 0.5f, 0.5f, -0.418688f, -0.0813124f}); return matrix; } case NVCV_COLOR_SPEC_BT709: { - const float values[] = {0.2126, 0.7152, 0.0722, -0.114572, -0.385428, 0.5, 0.5, -0.454153, -0.0458471}; - matrix.load(values); + matrix.load({0.2126f, 0.7152f, 0.0722f, -0.114572f, -0.385428f, 0.5f, 0.5f, -0.454153f, -0.0458471f}); return matrix; } case NVCV_COLOR_SPEC_BT2020: { - const float values[] = {0.2627, 0.678, 0.0593, -0.13963, -0.36037, 0.5, 0.5, -0.459786, -0.0402143}; - matrix.load(values); + matrix.load({0.2627f, 0.678f, 0.0593f, -0.13963f, -0.36037f, 0.5f, 0.5f, -0.459786f, -0.0402143f}); return matrix; } default: @@ -66,14 +65,68 @@ static const Matrix3x3 getRGB2YUVMatrix(nvcv::ColorSpec spec) } } -static const Matrix3x3 getYUV2RGBMatrix(nvcv::ColorSpec spec) +static Matrix3x3 getYUV2RGBMatrix(nvcv::ColorSpec spec) { Matrix3x3 matrix = getRGB2YUVMatrix(spec); mmath::inv_inplace(matrix); return matrix; } -static bool isBGR(NVCVColorConversionCode &code) +static float ToFloat(int value) +{ + return static_cast(value); +} + +static int TruncateToInt(float value) +{ + return static_cast(value); +} + +struct RGBPixel +{ + int r; + int g; + int b; +}; + +static RGBPixel GetRGBPixel(const std::vector &rgbData, int pixelIndex, bool bgr) +{ + RGBPixel pixel{rgbData[pixelIndex], rgbData[pixelIndex + 1], rgbData[pixelIndex + 2]}; + if (bgr) + { + std::swap(pixel.r, pixel.b); + } + return pixel; +} + +struct NV12ChromaBlockData +{ + const Matrix3x3 &conversionMatrix; + const std::vector &rgbData; + int width; + bool bgr; +}; + +static void AccumulateNV12ChromaBlock(const NV12ChromaBlockData &block, int row, int col, int &uSum, int &vSum) +{ + for (int y = 0; y < 2; ++y) + { + for (int x = 0; x < 2; ++x) + { + int idx = ((row + y) * block.width + (col + x)) * 3; + RGBPixel pixel = GetRGBPixel(block.rgbData, idx, block.bgr); + + uSum += TruncateToInt(block.conversionMatrix[1][0] * ToFloat(pixel.r) + + block.conversionMatrix[1][1] * ToFloat(pixel.g) + + block.conversionMatrix[1][2] * ToFloat(pixel.b)); + vSum += TruncateToInt(block.conversionMatrix[2][0] * ToFloat(pixel.r) + + block.conversionMatrix[2][1] * ToFloat(pixel.g) + + block.conversionMatrix[2][2] * ToFloat(pixel.b)); + } + } +} + +static bool isBGR(NVCVColorConversionCode code) { switch (code) { @@ -89,7 +142,7 @@ static bool isBGR(NVCVColorConversionCode &code) } } -static bool isYVU(NVCVColorConversionCode &code) +static bool isYVU(NVCVColorConversionCode code) { switch (code) { @@ -116,9 +169,12 @@ static std::vector convertYUVtoRGB(const Matrix3x3 &conversi int U = yuvData[i + 1]; int V = yuvData[i + 2]; - int R = conversionMatrix[0][0] * Y + conversionMatrix[0][1] * (U - 128) + conversionMatrix[0][2] * (V - 128); - int G = conversionMatrix[1][0] * Y + conversionMatrix[1][1] * (U - 128) + conversionMatrix[1][2] * (V - 128); - int B = conversionMatrix[2][0] * Y + conversionMatrix[2][1] * (U - 128) + conversionMatrix[2][2] * (V - 128); + int R = TruncateToInt(conversionMatrix[0][0] * ToFloat(Y) + conversionMatrix[0][1] * ToFloat(U - 128) + + conversionMatrix[0][2] * ToFloat(V - 128)); + int G = TruncateToInt(conversionMatrix[1][0] * ToFloat(Y) + conversionMatrix[1][1] * ToFloat(U - 128) + + conversionMatrix[1][2] * ToFloat(V - 128)); + int B = TruncateToInt(conversionMatrix[2][0] * ToFloat(Y) + conversionMatrix[2][1] * ToFloat(U - 128) + + conversionMatrix[2][2] * ToFloat(V - 128)); if (bgr) { @@ -137,7 +193,7 @@ static std::vector convertRGBtoYUV(const Matrix3x3 &conversi { assert(rgbData.size() % 3 == 0); // Ensure the input data has sets of 3 (Y, U, V) std::vector yuvData; - yuvData.reserve(yuvData.size()); // Reserve space for RGB data + yuvData.reserve(rgbData.size()); // Reserve space for YUV data for (size_t i = 0; i < rgbData.size(); i += 3) { @@ -149,9 +205,12 @@ static std::vector convertRGBtoYUV(const Matrix3x3 &conversi std::swap(R, B); } - int Y = conversionMatrix[0][0] * R + conversionMatrix[0][1] * G + conversionMatrix[0][2] * B; - int U = conversionMatrix[1][0] * R + conversionMatrix[1][1] * G + conversionMatrix[1][2] * B + 128; - int V = conversionMatrix[2][0] * R + conversionMatrix[2][1] * G + conversionMatrix[2][2] * B + 128; + int Y = TruncateToInt(conversionMatrix[0][0] * ToFloat(R) + conversionMatrix[0][1] * ToFloat(G) + + conversionMatrix[0][2] * ToFloat(B)); + int U = TruncateToInt(conversionMatrix[1][0] * ToFloat(R) + conversionMatrix[1][1] * ToFloat(G) + + conversionMatrix[1][2] * ToFloat(B) + 128.0f); + int V = TruncateToInt(conversionMatrix[2][0] * ToFloat(R) + conversionMatrix[2][1] * ToFloat(G) + + conversionMatrix[2][2] * ToFloat(B) + 128.0f); yuvData.push_back(nvcv::cuda::SaturateCast(Y)); yuvData.push_back(nvcv::cuda::SaturateCast(U)); @@ -174,6 +233,8 @@ static std::vector convertRGBtoNV12(const Matrix3x3 &convers std::vector nv12Data; nv12Data.reserve(width * height * 3 / 2); // NV12 needs 1.5 bytes per RGB pixel + NV12ChromaBlockData chromaBlock{conversionMatrix, rgbData, width, bgr}; + // Convert all RGB values to Y values and store them. for (size_t i = 0; i < rgbData.size(); i += 3) { @@ -185,7 +246,8 @@ static std::vector convertRGBtoNV12(const Matrix3x3 &convers std::swap(R, B); } - int Y = conversionMatrix[0][0] * R + conversionMatrix[0][1] * G + conversionMatrix[0][2] * B; + int Y = TruncateToInt(conversionMatrix[0][0] * ToFloat(R) + conversionMatrix[0][1] * ToFloat(G) + + conversionMatrix[0][2] * ToFloat(B)); nv12Data.push_back(nvcv::cuda::SaturateCast(Y)); } @@ -197,24 +259,7 @@ static std::vector convertRGBtoNV12(const Matrix3x3 &convers int U_sum = 0; int V_sum = 0; - // Loop through the 2x2 block to compute average U and V - for (int y = 0; y < 2; ++y) - { - for (int x = 0; x < 2; ++x) - { - int idx = ((h + y) * width + (w + x)) * 3; - int R = rgbData[idx]; - int G = rgbData[idx + 1]; - int B = rgbData[idx + 2]; - if (bgr) - { - std::swap(R, B); - } - - U_sum += conversionMatrix[1][0] * R + conversionMatrix[1][1] * G + conversionMatrix[1][2] * B; - V_sum += conversionMatrix[2][0] * R + conversionMatrix[2][1] * G + conversionMatrix[2][2] * B; - } - } + AccumulateNV12ChromaBlock(chromaBlock, h, w, U_sum, V_sum); int U = (U_sum / 4) + 128; // Average of 4 U values int V = (V_sum / 4) + 128; // Average of 4 V values @@ -231,6 +276,188 @@ static std::vector convertRGBtoNV12(const Matrix3x3 &convers return nv12Data; } +struct FixedRGB2YUVConstants +{ + int r2y; + int g2y; + int b2y; + int b2u; + int r2v; +}; + +static FixedRGB2YUVConstants GetFixedRGB2YUVConstants(nvcv::ColorSpec spec) +{ + switch (static_cast(spec)) + { + case NVCV_COLOR_SPEC_BT601: + return {4899, 9671, 1868, 9246, 11686}; + case NVCV_COLOR_SPEC_BT709: + return {3483, 11718, 1265, 8829, 10404}; + case NVCV_COLOR_SPEC_BT2020: + return {4304, 11108, 972, 8708, 11111}; + default: + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Unknown color spec"); + } +} + +static int FixedDescale(int value) +{ + constexpr int shift = 14; + return (value + (1 << (shift - 1))) >> shift; +} + +static void AccumulateRGBToNVPixel(const std::vector &src, std::vector &dst, int width, int channels, + int x, int y, int bidx, const FixedRGB2YUVConstants &constants, int &uSum, int &vSum) +{ + size_t srcIdx = (static_cast(y) * width + x) * channels; + int b = src[srcIdx + bidx]; + int g = src[srcIdx + 1]; + int r = src[srcIdx + (bidx ^ 2)]; + int yValue = FixedDescale(r * constants.r2y + g * constants.g2y + b * constants.b2y); + int u = FixedDescale((b - yValue) * constants.b2u); + int v = FixedDescale((r - yValue) * constants.r2v); + + dst[static_cast(y) * width + x] = nvcv::cuda::SaturateCast(yValue); + uSum += u; + vSum += v; +} + +static std::vector ConvertRGBToNVExact(const std::vector &src, int width, int height, int channels, + NVCVColorConversionCode code, nvcv::ColorSpec spec) +{ + const auto constants = GetFixedRGB2YUVConstants(spec); + const int bidx = (code == NVCV_COLOR_BGR2YUV_NV12 || code == NVCV_COLOR_BGR2YUV_NV21) ? 0 : 2; + const int uidx = (code == NVCV_COLOR_BGR2YUV_NV12 || code == NVCV_COLOR_RGB2YUV_NV12) ? 0 : 1; + + std::vector dst(static_cast(width) * height * 3 / 2); + for (int y = 0; y < height; y += 2) + { + for (int x = 0; x < width; x += 2) + { + int uSum = 0; + int vSum = 0; + AccumulateRGBToNVPixel(src, dst, width, channels, x, y, bidx, constants, uSum, vSum); + AccumulateRGBToNVPixel(src, dst, width, channels, x + 1, y, bidx, constants, uSum, vSum); + AccumulateRGBToNVPixel(src, dst, width, channels, x, y + 1, bidx, constants, uSum, vSum); + AccumulateRGBToNVPixel(src, dst, width, channels, x + 1, y + 1, bidx, constants, uSum, vSum); + + size_t uvIdx = static_cast(width) * height + static_cast(y / 2) * width + x; + dst[uvIdx + uidx] = nvcv::cuda::SaturateCast(uSum / 4 + 128); + dst[uvIdx + 1 - uidx] = nvcv::cuda::SaturateCast(vSum / 4 + 128); + } + } + return dst; +} + +struct FixedYUV2RGBConstants +{ + int u2b; + int u2g; + int v2g; + int v2r; +}; + +static FixedYUV2RGBConstants GetFixedYUV2RGBConstants(nvcv::ColorSpec spec) +{ + switch (static_cast(spec)) + { + case NVCV_COLOR_SPEC_BT601: + return {29032, -5636, -11698, 22970}; + case NVCV_COLOR_SPEC_BT709: + return {30402, -3069, -7670, 25802}; + case NVCV_COLOR_SPEC_BT2020: + return {30825, -2696, -9361, 24160}; + default: + throw nvcv::Exception(nvcv::Status::ERROR_INVALID_ARGUMENT, "Unknown color spec"); + } +} + +static std::vector ConvertNVToRGBExact(const std::vector &src, int width, int height, int channels, + NVCVColorConversionCode code, nvcv::ColorSpec spec) +{ + const auto constants = GetFixedYUV2RGBConstants(spec); + const int bidx = (code == NVCV_COLOR_YUV2BGR_NV12 || code == NVCV_COLOR_YUV2BGR_NV21) ? 0 : 2; + const int uidx = (code == NVCV_COLOR_YUV2BGR_NV12 || code == NVCV_COLOR_YUV2RGB_NV12) ? 0 : 1; + const auto uvOffset = static_cast(width) * height; + + std::vector dst(static_cast(width) * height * channels); + for (int y = 0; y < height; ++y) + { + for (int x = 0; x < width; ++x) + { + int yValue = src[static_cast(y) * width + x]; + size_t uvIdx = uvOffset + static_cast(y / 2) * width + (x & ~1); + int u = src[uvIdx + uidx] - 128; + int v = src[uvIdx + 1 - uidx] - 128; + int b = yValue + FixedDescale(u * constants.u2b); + int g = yValue + FixedDescale(u * constants.u2g + v * constants.v2g); + int r = yValue + FixedDescale(v * constants.v2r); + + size_t dstIdx = (static_cast(y) * width + x) * channels; + dst[dstIdx + bidx] = nvcv::cuda::SaturateCast(b); + dst[dstIdx + 1] = nvcv::cuda::SaturateCast(g); + dst[dstIdx + (bidx ^ 2)] = nvcv::cuda::SaturateCast(r); + if (channels == 4) + { + dst[dstIdx + 3] = 0xff; + } + } + } + return dst; +} + +static std::vector Convert444Exact(const std::vector &src, int width, int height, + NVCVColorConversionCode code, nvcv::ColorSpec spec) +{ + std::vector dst(src.size()); + const bool toYUV = code == NVCV_COLOR_BGR2YUV || code == NVCV_COLOR_RGB2YUV; + const int bidx = (code == NVCV_COLOR_BGR2YUV || code == NVCV_COLOR_YUV2BGR) ? 0 : 2; + + if (toYUV) + { + const auto constants = GetFixedRGB2YUVConstants(spec); + for (int y = 0; y < height; ++y) + { + for (int x = 0; x < width; ++x) + { + const size_t idx = (static_cast(y) * width + x) * 3; + const int b = src[idx + bidx]; + const int g = src[idx + 1]; + const int r = src[idx + (bidx ^ 2)]; + const int yValue = FixedDescale(r * constants.r2y + g * constants.g2y + b * constants.b2y); + const int u = FixedDescale((b - yValue) * constants.b2u + (128 << 14)); + const int v = FixedDescale((r - yValue) * constants.r2v + (128 << 14)); + + dst[idx] = nvcv::cuda::SaturateCast(yValue); + dst[idx + 1] = nvcv::cuda::SaturateCast(u); + dst[idx + 2] = nvcv::cuda::SaturateCast(v); + } + } + } + else + { + const auto constants = GetFixedYUV2RGBConstants(spec); + for (int y = 0; y < height; ++y) + { + for (int x = 0; x < width; ++x) + { + const size_t idx = (static_cast(y) * width + x) * 3; + const int yValue = src[idx]; + const int u = src[idx + 1] - 128; + const int v = src[idx + 2] - 128; + const int b = yValue + FixedDescale(u * constants.u2b); + const int g = yValue + FixedDescale(u * constants.u2g + v * constants.v2g); + const int r = yValue + FixedDescale(v * constants.v2r); + + dst[idx + bidx] = nvcv::cuda::SaturateCast(b); + dst[idx + 1] = nvcv::cuda::SaturateCast(g); + dst[idx + (bidx ^ 2)] = nvcv::cuda::SaturateCast(r); + } + } + } + return dst; +} + static std::vector convertNV12toRGB(const Matrix3x3 &conversionMatrix, const std::vector &nv12Data, int width, int height, bool bgr, bool yvu) @@ -259,12 +486,12 @@ static std::vector convertNV12toRGB(const Matrix3x3 &convers std::swap(U, V); } - int R - = conversionMatrix[0][0] * Y + conversionMatrix[0][1] * (U - 128) + conversionMatrix[0][2] * (V - 128); - int G - = conversionMatrix[1][0] * Y + conversionMatrix[1][1] * (U - 128) + conversionMatrix[1][2] * (V - 128); - int B - = conversionMatrix[2][0] * Y + conversionMatrix[2][1] * (U - 128) + conversionMatrix[2][2] * (V - 128); + int R = TruncateToInt(conversionMatrix[0][0] * ToFloat(Y) + conversionMatrix[0][1] * ToFloat(U - 128) + + conversionMatrix[0][2] * ToFloat(V - 128)); + int G = TruncateToInt(conversionMatrix[1][0] * ToFloat(Y) + conversionMatrix[1][1] * ToFloat(U - 128) + + conversionMatrix[1][2] * ToFloat(V - 128)); + int B = TruncateToInt(conversionMatrix[2][0] * ToFloat(Y) + conversionMatrix[2][1] * ToFloat(U - 128) + + conversionMatrix[2][2] * ToFloat(V - 128)); if (bgr) { @@ -287,32 +514,43 @@ static std::vector convertNV12toRGB(const Matrix3x3 &convers EXPECT_NEAR(vec1[idx], vec2[idx], delta) << "At index " << idx; \ } -static void verifyOutput(int batches, nvcv::Tensor &inTensor, nvcv::Tensor &outTensor, nvcv::ImageFormat format, - NVCVColorConversionCode convCode, nvcv::ColorSpec colorSpec, float maxDiff, int width, - int height) +struct VerifyOutputParams { - for (int i = 0; i < batches; ++i) + int batches; + NVCVColorConversionCode convCode; + nvcv::ColorSpec colorSpec; + float maxDiff; + int width; + int height; +}; + +static void verifyOutput(const nvcv::Tensor &inTensor, const nvcv::Tensor &outTensor, const VerifyOutputParams ¶ms) +{ + for (int i = 0; i < params.batches; ++i) { - std::vector outData, inData; + std::vector outData; + std::vector inData; // get 0th sample since histogram is just a 2d tensor - EXPECT_NO_THROW(util::GetImageVectorFromTensor(inTensor.exportData(), i, inData)); - EXPECT_NO_THROW(util::GetImageVectorFromTensor(outTensor.exportData(), i, outData)); + util::GetImageVectorFromTensor(inTensor.exportData(), i, inData); + util::GetImageVectorFromTensor(outTensor.exportData(), i, outData); - switch (convCode) + switch (params.convCode) { case NVCV_COLOR_BGR2YUV: case NVCV_COLOR_RGB2YUV: { - std::vector goldOut = convertRGBtoYUV(getRGB2YUVMatrix(colorSpec), inData, isBGR(convCode)); - VEC_EXPECT_NEAR(goldOut, outData, maxDiff); + std::vector goldOut + = convertRGBtoYUV(getRGB2YUVMatrix(params.colorSpec), inData, isBGR(params.convCode)); + VEC_EXPECT_NEAR(goldOut, outData, params.maxDiff); break; } case NVCV_COLOR_YUV2BGR: case NVCV_COLOR_YUV2RGB: { - std::vector goldOut = convertYUVtoRGB(getYUV2RGBMatrix(colorSpec), inData, isBGR(convCode)); - VEC_EXPECT_NEAR(goldOut, outData, maxDiff); + std::vector goldOut + = convertYUVtoRGB(getYUV2RGBMatrix(params.colorSpec), inData, isBGR(params.convCode)); + VEC_EXPECT_NEAR(goldOut, outData, params.maxDiff); break; } case NVCV_COLOR_YUV2RGB_NV12: @@ -320,9 +558,10 @@ static void verifyOutput(int batches, nvcv::Tensor &inTensor, nvcv::Tensor &outT case NVCV_COLOR_YUV2RGB_NV21: case NVCV_COLOR_YUV2BGR_NV21: { - std::vector goldOut = convertNV12toRGB(getYUV2RGBMatrix(colorSpec), inData, width, height, - isBGR(convCode), isYVU(convCode)); - VEC_EXPECT_NEAR(goldOut, outData, maxDiff); + std::vector goldOut + = convertNV12toRGB(getYUV2RGBMatrix(params.colorSpec), inData, params.width, params.height, + isBGR(params.convCode), isYVU(params.convCode)); + VEC_EXPECT_NEAR(goldOut, outData, params.maxDiff); break; } case NVCV_COLOR_RGB2YUV_NV21: @@ -330,9 +569,10 @@ static void verifyOutput(int batches, nvcv::Tensor &inTensor, nvcv::Tensor &outT case NVCV_COLOR_RGB2YUV_NV12: case NVCV_COLOR_BGR2YUV_NV12: { - std::vector goldOut = convertRGBtoNV12(getRGB2YUVMatrix(colorSpec), inData, width, height, - isBGR(convCode), isYVU(convCode)); - VEC_EXPECT_NEAR(goldOut, outData, maxDiff); + std::vector goldOut + = convertRGBtoNV12(getRGB2YUVMatrix(params.colorSpec), inData, params.width, params.height, + isBGR(params.convCode), isYVU(params.convCode)); + VEC_EXPECT_NEAR(goldOut, outData, params.maxDiff); break; } default: @@ -342,10 +582,577 @@ static void verifyOutput(int batches, nvcv::Tensor &inTensor, nvcv::Tensor &outT } } +static bool isFromNV(NVCVColorConversionCode code) +{ + switch (code) + { + case NVCV_COLOR_YUV2RGB_NV12: + case NVCV_COLOR_YUV2BGR_NV12: + case NVCV_COLOR_YUV2RGB_NV21: + case NVCV_COLOR_YUV2BGR_NV21: + return true; + default: + return false; + } +} + +static bool isToNV(NVCVColorConversionCode code) +{ + switch (code) + { + case NVCV_COLOR_RGB2YUV_NV12: + case NVCV_COLOR_BGR2YUV_NV12: + case NVCV_COLOR_RGB2YUV_NV21: + case NVCV_COLOR_BGR2YUV_NV21: + return true; + default: + return false; + } +} + +static nvcv::Tensor MakeAdvCvtColorTensor(int numImages, int width, int height, int channels, const char *layout) +{ + if (std::strcmp(layout, "HWC") == 0 || std::strcmp(layout, "CHW") == 0) + { + if (std::strcmp(layout, "CHW") == 0) + { + return nvcv::Tensor( + { + {channels, height, width}, + layout + }, + nvcv::TYPE_U8); + } + return nvcv::Tensor( + { + {height, width, channels}, + layout + }, + nvcv::TYPE_U8); + } + if (std::strcmp(layout, "NCHW") == 0) + { + return nvcv::Tensor( + { + {numImages, channels, height, width}, + layout + }, + nvcv::TYPE_U8); + } + return nvcv::Tensor( + { + {numImages, height, width, channels}, + layout + }, + nvcv::TYPE_U8); +} + +static cudaError_t AllocateMisalignedTensorBuffer(int samples, int sampleStride, int rowStride, int pixelStride, + NVCVByte *&allocation, nvcv::TensorDataStridedCuda::Buffer &buffer) +{ + if (cudaError_t status + = cudaMalloc(reinterpret_cast(&allocation), static_cast(sampleStride) * samples + 1); + status != cudaSuccess) + { + return status; + } + + buffer.basePtr = allocation + 1; + buffer.strides[0] = sampleStride; + buffer.strides[1] = rowStride; + buffer.strides[2] = pixelStride; + buffer.strides[3] = 1; + return cudaSuccess; +} + +static void RunAdvCvtColorTensorPlanarParity(int width, int height, int numImages, int srcChannels, int dstChannels, + NVCVColorConversionCode code, nvcv::ColorSpec colorSpec, bool rank3) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + const int samples = rank3 ? 1 : numImages; + const int srcH = isFromNV(code) ? height * 3 / 2 : height; + const int dstH = isToNV(code) ? height * 3 / 2 : height; + const int dstC = isToNV(code) ? 1 : dstChannels; + const int srcRow = width * srcChannels * static_cast(sizeof(uint8_t)); + const int dstRow = width * dstC * static_cast(sizeof(uint8_t)); + + nvcv::Tensor srcI = MakeAdvCvtColorTensor(samples, width, srcH, srcChannels, rank3 ? "HWC" : "NHWC"); + nvcv::Tensor dstI = MakeAdvCvtColorTensor(samples, width, dstH, dstC, rank3 ? "HWC" : "NHWC"); + nvcv::Tensor srcP = MakeAdvCvtColorTensor(samples, width, srcH, srcChannels, rank3 ? "CHW" : "NCHW"); + nvcv::Tensor dstP = MakeAdvCvtColorTensor(samples, width, dstH, dstC, rank3 ? "CHW" : "NCHW"); + + auto srcIData = srcI.exportData(); + auto dstIData = dstI.exportData(); + auto srcPData = srcP.exportData(); + auto dstPData = dstP.exportData(); + ASSERT_TRUE(srcIData && dstIData && srcPData && dstPData); + + auto srcIAcc = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcIData); + auto dstIAcc = nvcv::TensorDataAccessStridedImagePlanar::Create(*dstIData); + auto srcPAcc = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcPData); + auto dstPAcc = nvcv::TensorDataAccessStridedImagePlanar::Create(*dstPData); + ASSERT_TRUE(srcIAcc && dstIAcc && srcPAcc && dstPAcc); + + for (int i = 0; i < samples; ++i) + { + std::vector hwc(static_cast(srcH) * srcRow); + test::planar::FillDeterministicValues(hwc, static_cast(i) * 101 + 13, nvcv::TYPE_U8); + test::planar::UploadInterleavedSample(*srcIAcc, i, hwc, width, srcH, srcRow); + test::planar::UploadPlanarSample( + *srcPAcc, i, test::planar::DeinterleaveToPlanes(hwc, width, srcH, srcChannels, sizeof(uint8_t)), width, + srcH, srcChannels, sizeof(uint8_t)); + } + + cvcuda::AdvCvtColor op; + EXPECT_NO_THROW(op(stream, srcI, dstI, code, colorSpec)); + EXPECT_NO_THROW(op(stream, srcP, dstP, code, colorSpec)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + for (int i = 0; i < samples; ++i) + { + SCOPED_TRACE(i); + auto gpuInter = test::planar::DownloadInterleavedSample(*dstIAcc, i, width, dstH, dstRow); + auto planesOut = test::planar::DownloadPlanarSample(*dstPAcc, i, width, dstH, dstC, sizeof(uint8_t)); + auto planarInter = test::planar::InterleaveFromPlanes(planesOut, width, dstH, dstC, sizeof(uint8_t)); + + EXPECT_EQ(gpuInter, planarInter); + } + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +// clang-format off + +NVCV_TEST_SUITE_P( + OpAdvCvtColor444PlanarExact, + test::ValueList{ + // W, H, Conversion code, Color spec, Rank-3 CHW + { 37, 35, NVCV_COLOR_RGB2YUV, NVCV_COLOR_SPEC_BT601, false}, + { 29, 7, NVCV_COLOR_BGR2YUV, NVCV_COLOR_SPEC_BT709, true }, + { 41, 33, NVCV_COLOR_RGB2YUV, NVCV_COLOR_SPEC_BT2020, false}, + { 37, 35, NVCV_COLOR_YUV2RGB, NVCV_COLOR_SPEC_BT601, false}, + { 31, 11, NVCV_COLOR_YUV2BGR, NVCV_COLOR_SPEC_BT709, true }, + { 41, 33, NVCV_COLOR_YUV2RGB, NVCV_COLOR_SPEC_BT2020, false}, + }); + +// clang-format on + +TEST_P(OpAdvCvtColor444PlanarExact, matches_fixed_point_reference) +{ + const int width = GetParamValue<0>(); + const int height = GetParamValue<1>(); + const NVCVColorConversionCode code = GetParamValue<2>(); + const nvcv::ColorSpec colorSpec{GetParamValue<3>()}; + const bool rank3 = GetParamValue<4>(); + const int samples = rank3 ? 1 : 2; + + nvcv::Tensor src = MakeAdvCvtColorTensor(samples, width, height, 3, rank3 ? "CHW" : "NCHW"); + nvcv::Tensor dst = MakeAdvCvtColorTensor(samples, width, height, 3, rank3 ? "CHW" : "NCHW"); + + auto srcData = src.exportData(); + auto dstData = dst.exportData(); + ASSERT_TRUE(srcData && dstData); + auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcData); + auto dstAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*dstData); + ASSERT_TRUE(srcAccess && dstAccess); + + std::vector> gold(samples); + for (int i = 0; i < samples; ++i) + { + std::vector input(static_cast(width) * height * 3); + test::planar::FillDeterministicValues(input, static_cast(i) * 211 + 43, nvcv::TYPE_U8); + test::planar::UploadPlanarSample(*srcAccess, i, + test::planar::DeinterleaveToPlanes(input, width, height, 3, sizeof(uint8_t)), + width, height, 3, sizeof(uint8_t)); + gold[i] = Convert444Exact(input, width, height, code, colorSpec); + } + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + cvcuda::AdvCvtColor op; + EXPECT_NO_THROW(op(stream, src, dst, code, colorSpec)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + for (int i = 0; i < samples; ++i) + { + SCOPED_TRACE(i); + auto planes = test::planar::DownloadPlanarSample(*dstAccess, i, width, height, 3, sizeof(uint8_t)); + auto output = test::planar::InterleaveFromPlanes(planes, width, height, 3, sizeof(uint8_t)); + EXPECT_EQ(gold[i], output); + } + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +// clang-format off + +NVCV_TEST_SUITE_P( + OpAdvCvtColorRGBToNVExact, + test::ValueList{ + // W, H, C, Conversion code, Color spec, NCHW + { 2, 2, 3, NVCV_COLOR_RGB2YUV_NV12, NVCV_COLOR_SPEC_BT601, false}, + { 66, 18, 3, NVCV_COLOR_BGR2YUV_NV21, NVCV_COLOR_SPEC_BT709, false}, + { 70, 22, 4, NVCV_COLOR_BGR2YUV_NV12, NVCV_COLOR_SPEC_BT2020, false}, + { 2, 2, 4, NVCV_COLOR_RGB2YUV_NV21, NVCV_COLOR_SPEC_BT601, false}, + { 66, 18, 3, NVCV_COLOR_BGR2YUV_NV12, NVCV_COLOR_SPEC_BT709, true }, + { 2, 2, 3, NVCV_COLOR_RGB2YUV_NV21, NVCV_COLOR_SPEC_BT2020, true }, + { 2, 2, 4, NVCV_COLOR_RGB2YUV_NV12, NVCV_COLOR_SPEC_BT601, true }, + { 70, 22, 4, NVCV_COLOR_BGR2YUV_NV21, NVCV_COLOR_SPEC_BT709, true }, + }); + +// clang-format on + +TEST_P(OpAdvCvtColorRGBToNVExact, matches_fixed_point_reference) +{ + const int width = GetParamValue<0>(); + const int height = GetParamValue<1>(); + const int channels = GetParamValue<2>(); + const NVCVColorConversionCode code = GetParamValue<3>(); + const nvcv::ColorSpec colorSpec{GetParamValue<4>()}; + const bool planar = GetParamValue<5>(); + constexpr int samples = 2; + const int dstHeight = height * 3 / 2; + + nvcv::Tensor src = MakeAdvCvtColorTensor(samples, width, height, channels, planar ? "NCHW" : "NHWC"); + nvcv::Tensor dst = MakeAdvCvtColorTensor(samples, width, dstHeight, 1, planar ? "NCHW" : "NHWC"); + + auto srcData = src.exportData(); + auto dstData = dst.exportData(); + ASSERT_TRUE(srcData && dstData); + auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcData); + auto dstAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*dstData); + ASSERT_TRUE(srcAccess && dstAccess); + + std::vector> gold(samples); + for (int i = 0; i < samples; ++i) + { + std::vector input(static_cast(width) * height * channels); + test::planar::FillDeterministicValues(input, static_cast(i) * 173 + 29, nvcv::TYPE_U8); + if (planar) + { + test::planar::UploadPlanarSample( + *srcAccess, i, test::planar::DeinterleaveToPlanes(input, width, height, channels, sizeof(uint8_t)), + width, height, channels, sizeof(uint8_t)); + } + else + { + test::planar::UploadInterleavedSample(*srcAccess, i, input, width, height, width * channels); + } + gold[i] = ConvertRGBToNVExact(input, width, height, channels, code, colorSpec); + } + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + cvcuda::AdvCvtColor op; + EXPECT_NO_THROW(op(stream, src, dst, code, colorSpec)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + for (int i = 0; i < samples; ++i) + { + SCOPED_TRACE(i); + std::vector output; + if (planar) + { + auto planes = test::planar::DownloadPlanarSample(*dstAccess, i, width, dstHeight, 1, sizeof(uint8_t)); + output = test::planar::InterleaveFromPlanes(planes, width, dstHeight, 1, sizeof(uint8_t)); + } + else + { + output = test::planar::DownloadInterleavedSample(*dstAccess, i, width, dstHeight, width); + } + EXPECT_EQ(gold[i], output); + } + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +static void RunMisalignedPaddedRGBToNVExact(int channels, NVCVColorConversionCode code, nvcv::ColorSpec colorSpec, + size_t seed) +{ + constexpr int width = 66; + constexpr int height = 18; + constexpr int samples = 2; + constexpr int dstHeight = height * 3 / 2; + const int srcRow = width * channels + 1; + const int srcSample = srcRow * height + 1; + constexpr int dstRow = width + 1; + constexpr int dstSample = dstRow * dstHeight + 1; + constexpr uint8_t padding = 0xa5; + + NVCVByte *srcAllocation{}; + NVCVByte *dstAllocation{}; + nvcv::TensorDataStridedCuda::Buffer srcBuffer{}; + nvcv::TensorDataStridedCuda::Buffer dstBuffer{}; + ASSERT_EQ(cudaSuccess, + AllocateMisalignedTensorBuffer(samples, srcSample, srcRow, channels, srcAllocation, srcBuffer)); + cudaError_t status = AllocateMisalignedTensorBuffer(samples, dstSample, dstRow, 1, dstAllocation, dstBuffer); + if (status != cudaSuccess) + { + cudaFree(srcAllocation); + } + ASSERT_EQ(cudaSuccess, status); + + nvcv::Tensor src = nvcv::TensorWrapData( + nvcv::TensorDataStridedCuda{ + nvcv::TensorShape{{samples, height, width, channels}, "NHWC"}, + nvcv::TYPE_U8, srcBuffer + }, + nvcv::TensorDataCleanupCallback{[srcAllocation](const nvcv::TensorData &) + { + cudaFree(srcAllocation); + }}); + nvcv::Tensor dst = nvcv::TensorWrapData( + nvcv::TensorDataStridedCuda{ + nvcv::TensorShape{{samples, dstHeight, width, 1}, "NHWC"}, + nvcv::TYPE_U8, dstBuffer + }, + nvcv::TensorDataCleanupCallback{[dstAllocation](const nvcv::TensorData &) + { + cudaFree(dstAllocation); + }}); + + auto srcData = src.exportData(); + auto dstData = dst.exportData(); + ASSERT_TRUE(srcData && dstData); + auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcData); + auto dstAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*dstData); + ASSERT_TRUE(srcAccess && dstAccess); + + ASSERT_EQ(cudaSuccess, cudaMemset(srcAllocation, padding, static_cast(srcSample) * samples + 1)); + ASSERT_EQ(cudaSuccess, cudaMemset(dstAllocation, padding, static_cast(dstSample) * samples + 1)); + std::vector> gold(samples); + for (int i = 0; i < samples; ++i) + { + std::vector input(static_cast(width) * height * channels); + test::planar::FillDeterministicValues(input, static_cast(i) * 173 + seed, nvcv::TYPE_U8); + test::planar::UploadInterleavedSample(*srcAccess, i, input, width, height, width * channels); + gold[i] = ConvertRGBToNVExact(input, width, height, channels, code, colorSpec); + } + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + cvcuda::AdvCvtColor op; + EXPECT_NO_THROW(op(stream, src, dst, code, colorSpec)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + for (int i = 0; i < samples; ++i) + { + SCOPED_TRACE(i); + auto output = test::planar::DownloadInterleavedSample(*dstAccess, i, width, dstHeight, width); + EXPECT_EQ(gold[i], output); + } + + std::vector rawOutput(static_cast(dstSample) * samples + 1); + ASSERT_EQ(cudaSuccess, cudaMemcpy(rawOutput.data(), dstAllocation, rawOutput.size(), cudaMemcpyDeviceToHost)); + EXPECT_EQ(padding, rawOutput[0]); + for (int i = 0; i < samples; ++i) + { + for (int y = 0; y < dstHeight; ++y) + { + EXPECT_EQ(padding, + rawOutput[1 + static_cast(i) * dstSample + static_cast(y) * dstRow + width]); + } + EXPECT_EQ(padding, rawOutput[1 + static_cast(i) * dstSample + static_cast(dstHeight) * dstRow]); + } + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpAdvCvtColorRGBToNVExact, misaligned_padded_nhwc_matches_fixed_point_reference) +{ + RunMisalignedPaddedRGBToNVExact(3, NVCV_COLOR_BGR2YUV_NV12, nvcv::ColorSpec{NVCV_COLOR_SPEC_BT2020}, 79); + RunMisalignedPaddedRGBToNVExact(4, NVCV_COLOR_RGB2YUV_NV21, nvcv::ColorSpec{NVCV_COLOR_SPEC_BT709}, 131); +} + +// clang-format off + +NVCV_TEST_SUITE_P( + OpAdvCvtColorNVToRGBExact, + test::ValueList{ + // W, H, C, Conversion code, Color spec, NCHW + { 2, 2, 3, NVCV_COLOR_YUV2RGB_NV12, NVCV_COLOR_SPEC_BT601, false}, + { 66, 18, 3, NVCV_COLOR_YUV2BGR_NV21, NVCV_COLOR_SPEC_BT709, false}, + { 66, 20, 3, NVCV_COLOR_YUV2BGR_NV12, NVCV_COLOR_SPEC_BT2020, false}, + { 70, 22, 4, NVCV_COLOR_YUV2BGR_NV12, NVCV_COLOR_SPEC_BT2020, false}, + { 2, 2, 4, NVCV_COLOR_YUV2RGB_NV21, NVCV_COLOR_SPEC_BT601, false}, + { 66, 18, 3, NVCV_COLOR_YUV2BGR_NV12, NVCV_COLOR_SPEC_BT709, true }, + { 2, 2, 3, NVCV_COLOR_YUV2RGB_NV21, NVCV_COLOR_SPEC_BT2020, true }, + { 2, 2, 4, NVCV_COLOR_YUV2RGB_NV12, NVCV_COLOR_SPEC_BT601, true }, + { 70, 22, 4, NVCV_COLOR_YUV2BGR_NV21, NVCV_COLOR_SPEC_BT709, true }, + }); + +// clang-format on + +TEST_P(OpAdvCvtColorNVToRGBExact, matches_fixed_point_reference) +{ + const int width = GetParamValue<0>(); + const int height = GetParamValue<1>(); + const int channels = GetParamValue<2>(); + const NVCVColorConversionCode code = GetParamValue<3>(); + const nvcv::ColorSpec colorSpec{GetParamValue<4>()}; + const bool planar = GetParamValue<5>(); + constexpr int samples = 2; + const int srcHeight = height * 3 / 2; + + nvcv::Tensor src = MakeAdvCvtColorTensor(samples, width, srcHeight, 1, planar ? "NCHW" : "NHWC"); + nvcv::Tensor dst = MakeAdvCvtColorTensor(samples, width, height, channels, planar ? "NCHW" : "NHWC"); + + auto srcData = src.exportData(); + auto dstData = dst.exportData(); + ASSERT_TRUE(srcData && dstData); + auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcData); + auto dstAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*dstData); + ASSERT_TRUE(srcAccess && dstAccess); + + std::vector> gold(samples); + for (int i = 0; i < samples; ++i) + { + std::vector input(static_cast(width) * srcHeight); + test::planar::FillDeterministicValues(input, static_cast(i) * 191 + 37, nvcv::TYPE_U8); + if (planar) + { + test::planar::UploadPlanarSample(*srcAccess, i, {input}, width, srcHeight, 1, sizeof(uint8_t)); + } + else + { + test::planar::UploadInterleavedSample(*srcAccess, i, input, width, srcHeight, width); + } + gold[i] = ConvertNVToRGBExact(input, width, height, channels, code, colorSpec); + } + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + cvcuda::AdvCvtColor op; + EXPECT_NO_THROW(op(stream, src, dst, code, colorSpec)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + for (int i = 0; i < samples; ++i) + { + SCOPED_TRACE(i); + std::vector output; + if (planar) + { + auto planes = test::planar::DownloadPlanarSample(*dstAccess, i, width, height, channels, sizeof(uint8_t)); + output = test::planar::InterleaveFromPlanes(planes, width, height, channels, sizeof(uint8_t)); + } + else + { + output = test::planar::DownloadInterleavedSample(*dstAccess, i, width, height, width * channels); + } + EXPECT_EQ(gold[i], output); + } + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +static void RunMisalignedPaddedNVToRGBExact(int channels, NVCVColorConversionCode code, nvcv::ColorSpec colorSpec, + size_t seed) +{ + constexpr int width = 66; + constexpr int height = 18; + constexpr int samples = 2; + constexpr int srcHeight = height * 3 / 2; + constexpr int srcRow = width + 1; + constexpr int srcSample = srcRow * srcHeight + 1; + const int dstRow = width * channels + 1; + const int dstSample = dstRow * height + 1; + constexpr uint8_t padding = 0xa5; + + NVCVByte *srcAllocation{}; + NVCVByte *dstAllocation{}; + nvcv::TensorDataStridedCuda::Buffer srcBuffer{}; + nvcv::TensorDataStridedCuda::Buffer dstBuffer{}; + ASSERT_EQ(cudaSuccess, AllocateMisalignedTensorBuffer(samples, srcSample, srcRow, 1, srcAllocation, srcBuffer)); + cudaError_t status = AllocateMisalignedTensorBuffer(samples, dstSample, dstRow, channels, dstAllocation, dstBuffer); + if (status != cudaSuccess) + { + cudaFree(srcAllocation); + } + ASSERT_EQ(cudaSuccess, status); + + nvcv::Tensor src = nvcv::TensorWrapData( + nvcv::TensorDataStridedCuda{ + nvcv::TensorShape{{samples, srcHeight, width, 1}, "NHWC"}, + nvcv::TYPE_U8, srcBuffer + }, + nvcv::TensorDataCleanupCallback{[srcAllocation](const nvcv::TensorData &) + { + cudaFree(srcAllocation); + }}); + nvcv::Tensor dst = nvcv::TensorWrapData( + nvcv::TensorDataStridedCuda{ + nvcv::TensorShape{{samples, height, width, channels}, "NHWC"}, + nvcv::TYPE_U8, dstBuffer + }, + nvcv::TensorDataCleanupCallback{[dstAllocation](const nvcv::TensorData &) + { + cudaFree(dstAllocation); + }}); + + auto srcData = src.exportData(); + auto dstData = dst.exportData(); + ASSERT_TRUE(srcData && dstData); + auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcData); + auto dstAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*dstData); + ASSERT_TRUE(srcAccess && dstAccess); + + ASSERT_EQ(cudaSuccess, cudaMemset(dstAllocation, padding, static_cast(dstSample) * samples + 1)); + std::vector> gold(samples); + for (int i = 0; i < samples; ++i) + { + std::vector input(static_cast(width) * srcHeight); + test::planar::FillDeterministicValues(input, static_cast(i) * 191 + seed, nvcv::TYPE_U8); + test::planar::UploadInterleavedSample(*srcAccess, i, input, width, srcHeight, width); + gold[i] = ConvertNVToRGBExact(input, width, height, channels, code, colorSpec); + } + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + cvcuda::AdvCvtColor op; + EXPECT_NO_THROW(op(stream, src, dst, code, colorSpec)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + for (int i = 0; i < samples; ++i) + { + SCOPED_TRACE(i); + auto output = test::planar::DownloadInterleavedSample(*dstAccess, i, width, height, width * channels); + EXPECT_EQ(gold[i], output); + } + + std::vector rawOutput(static_cast(dstSample) * samples + 1); + ASSERT_EQ(cudaSuccess, cudaMemcpy(rawOutput.data(), dstAllocation, rawOutput.size(), cudaMemcpyDeviceToHost)); + EXPECT_EQ(padding, rawOutput[0]); + for (int i = 0; i < samples; ++i) + { + for (int y = 0; y < height; ++y) + { + EXPECT_EQ( + padding, + rawOutput[1 + static_cast(i) * dstSample + static_cast(y) * dstRow + width * channels]); + } + EXPECT_EQ(padding, rawOutput[1 + static_cast(i) * dstSample + static_cast(height) * dstRow]); + } + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpAdvCvtColorNVToRGBExact, misaligned_padded_nhwc_matches_fixed_point_reference) +{ + RunMisalignedPaddedNVToRGBExact(3, NVCV_COLOR_YUV2RGB_NV21, nvcv::ColorSpec{NVCV_COLOR_SPEC_BT2020}, 73); + RunMisalignedPaddedNVToRGBExact(3, NVCV_COLOR_YUV2BGR_NV12, nvcv::ColorSpec{NVCV_COLOR_SPEC_BT709}, 97); +} + +TEST(OpAdvCvtColorNVToRGBExact, misaligned_padded_nhwc_c4_matches_fixed_point_reference) +{ + RunMisalignedPaddedNVToRGBExact(4, NVCV_COLOR_YUV2BGR_NV12, nvcv::ColorSpec{NVCV_COLOR_SPEC_BT709}, 113); +} + // clang-format off // Max diff is 2.0 for 8-bit images is defined here since the conversion in the test is using floats directly and not integers. -NVCV_TEST_SUITE_P(OpAdvCvtColor, test::ValueList +NVCV_TEST_SUITE_P(OpAdvCvtColor, test::ValueList { //inWidth, inHeight, numberInBatch, In format, Out format, in2outCode, colorSpec, maxDiff @@ -404,6 +1211,29 @@ NVCV_TEST_SUITE_P(OpAdvCvtColor, test::ValueList +{ + // W, H, N, SrcC, DstC, Conversion Code, Color Spec, Rank-3 CHW/HWC + { 31, 23, 2, 3, 3, NVCV_COLOR_RGB2YUV, NVCV_COLOR_SPEC_BT601, false}, + { 29, 17, 1, 3, 3, NVCV_COLOR_YUV2BGR, NVCV_COLOR_SPEC_BT709, true}, + { 32, 24, 2, 3, 1, NVCV_COLOR_RGB2YUV_NV12, NVCV_COLOR_SPEC_BT2020, false}, + { 34, 22, 1, 4, 1, NVCV_COLOR_BGR2YUV_NV21, NVCV_COLOR_SPEC_BT601, true}, + { 30, 20, 2, 1, 3, NVCV_COLOR_YUV2RGB_NV12, NVCV_COLOR_SPEC_BT709, false}, + { 28, 18, 1, 1, 4, NVCV_COLOR_YUV2BGR_NV21, NVCV_COLOR_SPEC_BT2020, true}, +}); + +// clang-format on + +TEST_P(OpAdvCvtColorPlanarTensor, tensor_matches_interleaved) +{ + RunAdvCvtColorTensorPlanarParity(GetParamValue<0>(), GetParamValue<1>(), GetParamValue<2>(), GetParamValue<3>(), + GetParamValue<4>(), GetParamValue<5>(), nvcv::ColorSpec{GetParamValue<6>()}, + GetParamValue<7>()); +} + TEST_P(OpAdvCvtColor, AdvCvtColor_sanity) { cudaStream_t stream; @@ -414,28 +1244,32 @@ TEST_P(OpAdvCvtColor, AdvCvtColor_sanity) int batches = GetParamValue<2>(); nvcv::ImageFormat formatIn{GetParamValue<3>()}; nvcv::ImageFormat formatOut{GetParamValue<4>()}; - NVCVColorConversionCode convCode = GetParamValue<5>(); - nvcv::ColorSpec colorSpec = GetParamValue<6>(); - int maxDiff = GetParamValue<7>(); + NVCVColorConversionCode convCode = GetParamValue<5>(); + nvcv::ColorSpec colorSpec{GetParamValue<6>()}; + float maxDiff = GetParamValue<7>(); nvcv::Tensor inTensor = nvcv::util::CreateTensor(batches, width, height, formatIn); nvcv::Tensor outTensor = nvcv::util::CreateTensor(batches, width, height, formatOut); // NV12/21/YUV8/ARGB are all nHWC - int colorChannels = inTensor.shape()[inTensor.shape().rank() - 1]; - int tensorHeight = inTensor.shape()[inTensor.shape().rank() - 3]; - size_t imageSizeBytes = width * tensorHeight * colorChannels * sizeof(uint8_t); + auto colorChannels = static_cast(inTensor.shape()[inTensor.shape().rank() - 1]); + auto tensorHeight = static_cast(inTensor.shape()[inTensor.shape().rank() - 3]); + size_t imageSizeBytes = static_cast(width) * static_cast(tensorHeight) + * static_cast(colorChannels) * sizeof(uint8_t); std::default_random_engine randEng(0); - std::uniform_int_distribution rand(0u, 255u); + std::uniform_int_distribution randomByte(0, 255); std::vector imageVec(imageSizeBytes, 128); for (int i = 0; i < batches; ++i) { // generate random input image - std::generate(imageVec.begin(), imageVec.end(), [&]() { return rand(randEng); }); + for (uint8_t &value : imageVec) + { + value = static_cast(randomByte(randEng)); + } // copy random input to device tensor - EXPECT_NO_THROW(util::SetImageTensorFromVector(inTensor.exportData(), imageVec, i)); + util::SetImageTensorFromVector(inTensor.exportData(), imageVec, i); } // run operator @@ -445,12 +1279,12 @@ TEST_P(OpAdvCvtColor, AdvCvtColor_sanity) ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); - verifyOutput(batches, inTensor, outTensor, formatOut, convCode, colorSpec, maxDiff, width, height); + verifyOutput(inTensor, outTensor, {batches, convCode, colorSpec, maxDiff, width, height}); } // clang-format off // inputNumSamples, outputNumSamples, inWidth, inHeight, outWidth, outHeight, inFormat, outFormat, convCode, colorSpec -NVCV_TEST_SUITE_P(OpAdvCvtColor_Negative, test::ValueList +NVCV_TEST_SUITE_P(OpAdvCvtColor_Negative, test::ValueList { {2, 2, 400, 400, 400, 400, NVCV_IMAGE_FORMAT_RGBA8, NVCV_IMAGE_FORMAT_RGB8, NVCV_COLOR_RGBA2RGB, NVCV_COLOR_SPEC_BT601}, {2, 2, 400, 400, 400, 400, NVCV_IMAGE_FORMAT_RGB8, NVCV_IMAGE_FORMAT_YUV8, NVCV_COLOR_RGB2YUV, NVCV_COLOR_SPEC_sRGB}, @@ -483,8 +1317,8 @@ TEST_P(OpAdvCvtColor_Negative, op) int outHeight = GetParamValue<5>(); nvcv::ImageFormat inFormat{GetParamValue<6>()}; nvcv::ImageFormat outFormat{GetParamValue<7>()}; - NVCVColorConversionCode convCode = GetParamValue<8>(); - nvcv::ColorSpec colorSpec = GetParamValue<9>(); + NVCVColorConversionCode convCode = GetParamValue<8>(); + nvcv::ColorSpec colorSpec{GetParamValue<9>()}; nvcv::Tensor inTensor = nvcv::util::CreateTensor(inputNumSamples, inWidth, inHeight, inFormat); nvcv::Tensor outTensor = nvcv::util::CreateTensor(outputNumSamples, outWidth, outHeight, outFormat); @@ -492,7 +1326,8 @@ TEST_P(OpAdvCvtColor_Negative, op) // run operator cvcuda::AdvCvtColor op; EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, - nvcv::ProtectCall([&] { op(stream, inTensor, outTensor, convCode, colorSpec); })); + nvcv::ProtectCall([&op, &stream, &inTensor, &outTensor, &convCode, &colorSpec] + { op(stream, inTensor, outTensor, convCode, colorSpec); })); ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); diff --git a/tests/cvcuda/system/TestOpAutoContrast.cpp b/tests/cvcuda/system/TestOpAutoContrast.cpp new file mode 100644 index 000000000..5d0606f73 --- /dev/null +++ b/tests/cvcuda/system/TestOpAutoContrast.cpp @@ -0,0 +1,1480 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../../../src/cvcuda/priv/OpAutoContrast.hpp" +#include "PlanarParityUtils.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace nvcvcuda = nvcv::cuda; +namespace test = nvcv::test; +namespace ttype = nvcv::test::type; + +using uchar = unsigned char; +using ushort = unsigned short; + +static uint32_t FourCC(const char (&code)[5]) +{ + uint32_t value{}; + std::memcpy(&value, code, sizeof(value)); + return value; +} + +template +static To BitwiseCopy(From value) +{ + static_assert(sizeof(To) == sizeof(From)); + To result{}; + std::memcpy(&result, &value, sizeof(result)); + return result; +} + +// 16-bit unsigned multi-channel interleaved formats are not predefined; build them like +// TestOpBrightnessContrast.cpp does for its 16-bit cases. +#define NVCV_IMAGE_FORMAT_RGB16U \ + NVCV_DETAIL_MAKE_COLOR_FMT1(RGB, UNDEFINED, PL, UNSIGNED, XYZ1, ASSOCIATED, X16_Y16_Z16) +#define NVCV_IMAGE_FORMAT_RGB16Up \ + NVCV_DETAIL_MAKE_COLOR_FMT3(RGB, UNDEFINED, PL, UNSIGNED, XYZ0, ASSOCIATED, X16, X16, X16) +#define NVCV_IMAGE_FORMAT_RGBA16U \ + NVCV_DETAIL_MAKE_COLOR_FMT1(RGB, UNDEFINED, PL, UNSIGNED, XYZW, ASSOCIATED, X16_Y16_Z16_W16) + +template +using uniform_distribution + = std::conditional_t, std::uniform_int_distribution, std::uniform_real_distribution>; + +// --------------------------------------------------------------------------- +// Independent CPU reference. Pillow and torchvision truncate non-negative +// integer results after scaling, whereas floating-point outputs retain the +// scaled value. Do not call production conversion helpers from this oracle. +// Only the interleaved layout is referenced directly; planar correctness is +// established by the planar-parity suite (planar output == interleaved output +// bit-for-bit). +// --------------------------------------------------------------------------- +template +inline BT RemapGold(BT in, float lo, float hi, float bound) +{ + if constexpr (std::is_floating_point_v) + { + if (!std::isfinite(in)) + { + return in; + } + } + if (hi == lo) + { + return in; // flat channel -> unchanged (torchvision semantics) + } + float range = hi - lo; + float val; + if (std::isfinite(range)) + { + val = (static_cast(in) - lo) * bound / range; + } + else + { + const double wideRange = static_cast(hi) - static_cast(lo); + val = static_cast((static_cast(in) - static_cast(lo)) * bound / wideRange); + } + val = std::clamp(val, 0.f, bound); + if constexpr (std::is_integral_v) + { + return static_cast(std::floor(val)); + } + else + { + return static_cast(val); + } +} + +inline void UpdateFiniteExtrema(float value, float &lo, float &hi) +{ + if (std::isfinite(value)) + { + lo = std::min(lo, value); + hi = std::max(hi, value); + } +} + +template +void AutoContrastGoldSample(const uint8_t *src, uint8_t *ref, int width, int height, long rowStride) +{ + using BT = nvcvcuda::BaseType; + constexpr int numChannels = nvcvcuda::NumElements; + const float bound = std::is_floating_point_v ? 1.0f : static_cast(nvcvcuda::TypeTraits::max); + + std::array lo; + std::array hi; + for (int c = 0; c < numChannels; ++c) + { + lo[c] = std::numeric_limits::infinity(); + hi[c] = -std::numeric_limits::infinity(); + } + + for (int y = 0; y < height; ++y) + { + for (int x = 0; x < width; ++x) + { + const T px = *reinterpret_cast(src + y * rowStride + x * static_cast(sizeof(T))); + for (int c = 0; c < numChannels; ++c) + { + const auto value = static_cast(nvcvcuda::GetElement(px, c)); + UpdateFiniteExtrema(value, lo[c], hi[c]); + } + } + } + + for (int y = 0; y < height; ++y) + { + for (int x = 0; x < width; ++x) + { + const long off = y * rowStride + x * static_cast(sizeof(T)); + T px = *reinterpret_cast(src + off); + T out{}; + for (int c = 0; c < numChannels; ++c) + { + nvcvcuda::GetElement(out, c) = RemapGold(nvcvcuda::GetElement(px, c), lo[c], hi[c], bound); + } + *reinterpret_cast(ref + off) = out; + } + } +} + +template +auto BitExactValue(T value) +{ + if constexpr (std::is_floating_point_v) + { + return BitwiseCopy(value); + } + else + { + return value; + } +} + +// Per-sample bit-exact comparison, factored out so the call sites stay within the 3-level +// control-nesting budget (one outer per-sample loop + this helper's row/col/channel loops). +template +void ExpectSampleEqual(const uint8_t *got, const uint8_t *ref, int width, int height, long rowStride) +{ + for (int y = 0; y < height; ++y) + { + for (int x = 0; x < width; ++x) + { + const long off = y * rowStride + x * static_cast(sizeof(T)); + const T actual = *reinterpret_cast(got + off); + const T expected = *reinterpret_cast(ref + off); + for (int c = 0; c < nvcvcuda::NumElements; ++c) + { + const auto gotValue = nvcvcuda::GetElement(actual, c); + const auto refValue = nvcvcuda::GetElement(expected, c); + ASSERT_EQ(BitExactValue(gotValue), BitExactValue(refValue)) + << "pixel (" << x << "," << y << ") channel " << c; + } + } + } +} + +template +void SetAllElements(T &pixel, nvcvcuda::BaseType value) +{ + for (int c = 0; c < nvcvcuda::NumElements; ++c) + { + nvcvcuda::GetElement(pixel, c) = value; + } +} + +template +void FillRandomSample(uint8_t *buf, int width, int height, long rowStride, Rng &rng) +{ + using BT = nvcvcuda::BaseType; + uniform_distribution dist(BT{0}, std::is_integral_v ? nvcvcuda::TypeTraits::max : BT{1}); + for (int y = 0; y < height; ++y) + { + for (int x = 0; x < width; ++x) + { + T px{}; + for (int c = 0; c < nvcvcuda::NumElements; ++c) + { + nvcvcuda::GetElement(px, c) = static_cast(dist(rng)); + } + *reinterpret_cast(buf + y * rowStride + x * static_cast(sizeof(T))) = px; + } + } +} + +template +void ExpectTensorMatchesGold(int width, int height, nvcv::ImageFormat fmt, PixelAt pixelAt) +{ + nvcv::Tensor srcTensor = nvcv::util::CreateTensor(1, width, height, fmt); + nvcv::Tensor dstTensor = nvcv::util::CreateTensor(1, width, height, fmt); + + auto srcData = srcTensor.exportData(); + auto dstData = dstTensor.exportData(); + ASSERT_TRUE(srcData && dstData); + + auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcData); + ASSERT_TRUE(srcAccess); + + const long rowStride = srcAccess->rowStride(); + const size_t bufSize = static_cast(rowStride) * height; + std::vector srcVec(bufSize, uint8_t{0}); + std::vector refVec(bufSize, uint8_t{0}); + std::vector dstVec(bufSize, uint8_t{0}); + + for (int y = 0; y < height; ++y) + { + for (int x = 0; x < width; ++x) + { + *reinterpret_cast(srcVec.data() + y * rowStride + x * static_cast(sizeof(T))) = pixelAt(x, y); + } + } + + AutoContrastGoldSample(srcVec.data(), refVec.data(), width, height, rowStride); + ASSERT_EQ(cudaSuccess, cudaMemcpy(srcData->basePtr(), srcVec.data(), bufSize, cudaMemcpyHostToDevice)); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + cvcuda::AutoContrast op; + ASSERT_NO_THROW(op(stream, srcTensor, dstTensor)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + ASSERT_EQ(cudaSuccess, cudaMemcpy(dstVec.data(), dstData->basePtr(), bufSize, cudaMemcpyDeviceToHost)); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + + ExpectSampleEqual(dstVec.data(), refVec.data(), width, height, rowStride); +} + +template +void ExpectVarShapeMatchesGold(int width, int height, nvcv::ImageFormat fmt, PixelAt pixelAt) +{ + nvcv::Image srcImage({width, height}, fmt); + nvcv::Image dstImage({width, height}, fmt); + + auto srcData = srcImage.exportData(); + auto dstData = dstImage.exportData(); + ASSERT_TRUE(srcData && dstData); + ASSERT_EQ(1, srcData->numPlanes()); + ASSERT_EQ(sizeof(T), fmt.planePixelStrideBytes(0)); + + const long rowStride = srcData->plane(0).rowStride; + const size_t bufSize = static_cast(rowStride) * height; + std::vector srcVec(bufSize, uint8_t{0}); + std::vector refVec(bufSize, uint8_t{0}); + std::vector dstVec(bufSize, uint8_t{0}); + + for (int y = 0; y < height; ++y) + { + for (int x = 0; x < width; ++x) + { + *reinterpret_cast(srcVec.data() + y * rowStride + x * static_cast(sizeof(T))) = pixelAt(x, y); + } + } + + AutoContrastGoldSample(srcVec.data(), refVec.data(), width, height, rowStride); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + ASSERT_EQ(cudaSuccess, + cudaMemcpy2DAsync(srcData->plane(0).basePtr, rowStride, srcVec.data(), rowStride, + static_cast(width) * sizeof(T), height, cudaMemcpyHostToDevice, stream)); + + nvcv::ImageBatchVarShape srcBatch(1); + nvcv::ImageBatchVarShape dstBatch(1); + srcBatch.pushBack(srcImage); + dstBatch.pushBack(dstImage); + + cvcuda::AutoContrast op; + ASSERT_NO_THROW(op(stream, srcBatch, dstBatch)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + ASSERT_EQ(cudaSuccess, + cudaMemcpy2D(dstVec.data(), rowStride, dstData->plane(0).basePtr, dstData->plane(0).rowStride, + static_cast(width) * sizeof(T), height, cudaMemcpyDeviceToHost)); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + + ExpectSampleEqual(dstVec.data(), refVec.data(), width, height, rowStride); +} + +static void ExpectZeroExtentTensorStatus(const nvcv::TensorShape &shape, nvcv::DataType dtype, NVCVStatus status) +{ + std::optional src; + std::optional dst; + try + { + src.emplace(shape, dtype); + dst.emplace(shape, dtype); + } + catch (const nvcv::Exception &e) + { + GTEST_SKIP() << "zero-extent tensors are not constructible: " << e.what(); + } + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + cvcuda::AutoContrast op; + EXPECT_EQ(status, nvcv::ProtectCall([&op, stream, &src, &dst] { op(stream, *src, *dst); })); + + EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +// clang-format off +#define NVCV_TEST_CASE(W, H, N, T, FMT) ttype::Types, ttype::Value, ttype::Value, T, ttype::Value> + +NVCV_TYPED_TEST_SUITE( + OpAutoContrast, + ttype::Types< + NVCV_TEST_CASE( 2, 2, 1, uchar, NVCV_IMAGE_FORMAT_U8), + NVCV_TEST_CASE( 41, 39, 3, uchar, NVCV_IMAGE_FORMAT_U8), + NVCV_TEST_CASE( 42, 17, 2, uchar3, NVCV_IMAGE_FORMAT_RGB8), + NVCV_TEST_CASE( 64, 64, 4, uchar4, NVCV_IMAGE_FORMAT_RGBA8), + NVCV_TEST_CASE(101, 107, 2, ushort, NVCV_IMAGE_FORMAT_U16), + NVCV_TEST_CASE( 33, 21, 3, ushort3, NVCV_IMAGE_FORMAT_RGB16U), + NVCV_TEST_CASE( 17, 19, 2, ushort4, NVCV_IMAGE_FORMAT_RGBA16U), + NVCV_TEST_CASE(128, 9, 2, float, NVCV_IMAGE_FORMAT_F32), + NVCV_TEST_CASE( 59, 77, 3, float3, NVCV_IMAGE_FORMAT_RGBf32), + NVCV_TEST_CASE( 32, 48, 4, float4, NVCV_IMAGE_FORMAT_RGBAf32), + // 64 samples * 4 channels makes the 6 MiB workspace limit, rather than + // MAX_PARTIALS_PER_SAMPLE, bound the reduction grid at this height. + NVCV_TEST_CASE( 1, 16385, 64, uchar4, NVCV_IMAGE_FORMAT_RGBA8), + // Both logical grid dimensions exceed the bounded physical grid: the + // 8193x2 logical grid is executed by a 4096x1 physical grid. + NVCV_TEST_CASE(2097153, 5, 1, uchar4, NVCV_IMAGE_FORMAT_RGBA8)>); + +// clang-format on + +TYPED_TEST(OpAutoContrast, tensor_correct_output) +{ + const int width = ttype::GetValue; + const int height = ttype::GetValue; + const int numSamples = ttype::GetValue; + using T = ttype::GetType; + const nvcv::ImageFormat fmt{ttype::GetValue}; + + nvcv::Tensor srcTensor = nvcv::util::CreateTensor(numSamples, width, height, fmt); + nvcv::Tensor dstTensor = nvcv::util::CreateTensor(numSamples, width, height, fmt); + + auto srcData = srcTensor.exportData(); + auto dstData = dstTensor.exportData(); + ASSERT_TRUE(srcData && dstData); + + auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcData); + ASSERT_TRUE(srcAccess); + + const long rowStride = srcAccess->rowStride(); + long sampleStride = srcAccess->sampleStride(); + if (sampleStride == 0) // single-sample tensors report a zero sample stride + { + sampleStride = rowStride * height; + } + const size_t bufSize = static_cast(sampleStride) * numSamples; + + std::vector srcVec(bufSize, uint8_t{0}); + std::vector refVec(bufSize, uint8_t{0}); + std::vector dstVec(bufSize, uint8_t{0}); + + std::mt19937_64 rng(12345); + for (int s = 0; s < numSamples; ++s) + { + FillRandomSample(srcVec.data() + s * sampleStride, width, height, rowStride, rng); + } + + ASSERT_EQ(cudaSuccess, cudaMemcpy(srcData->basePtr(), srcVec.data(), bufSize, cudaMemcpyHostToDevice)); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + cvcuda::AutoContrast op; + ASSERT_NO_THROW(op(stream, srcTensor, dstTensor)); + + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + ASSERT_EQ(cudaSuccess, cudaMemcpy(dstVec.data(), dstData->basePtr(), bufSize, cudaMemcpyDeviceToHost)); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + + for (int s = 0; s < numSamples; ++s) + { + AutoContrastGoldSample(srcVec.data() + s * sampleStride, refVec.data() + s * sampleStride, width, height, + rowStride); + } + + for (int s = 0; s < numSamples; ++s) + { + SCOPED_TRACE("sample " + std::to_string(s)); + ExpectSampleEqual(dstVec.data() + s * sampleStride, refVec.data() + s * sampleStride, width, height, + rowStride); + } +} + +TYPED_TEST(OpAutoContrast, varshape_correct_output) +{ + const int baseW = ttype::GetValue; + const int baseH = ttype::GetValue; + const int numSamples = ttype::GetValue; + using T = ttype::GetType; + const nvcv::ImageFormat fmt{ttype::GetValue}; + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + std::vector imgSrc; + std::vector imgDst; + std::vector> srcVec(numSamples); + std::vector rowStrides(numSamples); + + std::uniform_int_distribution randW(baseW / 2 + 1, baseW * 3 / 2 + 1); + std::uniform_int_distribution randH(baseH / 2 + 1, baseH * 3 / 2 + 1); + std::mt19937_64 rng(12345); + + ASSERT_EQ(sizeof(T), fmt.planePixelStrideBytes(0)); + + for (int s = 0; s < numSamples; ++s) + { + const bool fixedShape = numSamples * nvcvcuda::NumElements >= 256 || baseW > 1'000'000; + nvcv::Size2D imgShape = fixedShape ? nvcv::Size2D{baseW, baseH} : nvcv::Size2D{randW(rng), randH(rng)}; + imgSrc.emplace_back(imgShape, fmt); + imgDst.emplace_back(imgShape, fmt); + + auto srcImgData = imgSrc[s].exportData(); + ASSERT_NE(srcImgData, nvcv::NullOpt); + ASSERT_EQ(srcImgData->numPlanes(), 1); + + const long rowStride = srcImgData->plane(0).rowStride; + rowStrides[s] = rowStride; + srcVec[s].resize(static_cast(rowStride) * imgShape.h, uint8_t{0}); + + FillRandomSample(srcVec[s].data(), imgShape.w, imgShape.h, rowStride, rng); + if constexpr (std::is_floating_point_v>) + { + constexpr std::array nonFiniteValues{-std::numeric_limits::infinity(), + std::numeric_limits::infinity(), + std::numeric_limits::quiet_NaN()}; + for (int x = 0; x < static_cast(nonFiniteValues.size()); ++x) + { + T pixel{}; + SetAllElements(pixel, nonFiniteValues[x]); + *reinterpret_cast(srcVec[s].data() + x * static_cast(sizeof(T))) = pixel; + } + } + + ASSERT_EQ(cudaSuccess, cudaMemcpy2DAsync(srcImgData->plane(0).basePtr, rowStride, srcVec[s].data(), rowStride, + static_cast(imgShape.w) * sizeof(T), imgShape.h, + cudaMemcpyHostToDevice, stream)); + } + + nvcv::ImageBatchVarShape batchSrc(numSamples); + nvcv::ImageBatchVarShape batchDst(numSamples); + batchSrc.pushBack(imgSrc.begin(), imgSrc.end()); + batchDst.pushBack(imgDst.begin(), imgDst.end()); + + cvcuda::AutoContrast op; + ASSERT_NO_THROW(op(stream, batchSrc, batchDst)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + for (int s = 0; s < numSamples; ++s) + { + SCOPED_TRACE(s); + const int width = imgSrc[s].size().w; + const int height = imgSrc[s].size().h; + const long rowStride = rowStrides[s]; + + std::vector refVec(static_cast(rowStride) * height, uint8_t{0}); + std::vector dstVec(static_cast(rowStride) * height, uint8_t{0}); + + const auto dstImgData = imgDst[s].exportData(); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(dstVec.data(), rowStride, dstImgData->plane(0).basePtr, rowStride, + static_cast(width) * sizeof(T), height, cudaMemcpyDeviceToHost)); + + AutoContrastGoldSample(srcVec[s].data(), refVec.data(), width, height, rowStride); + ExpectSampleEqual(dstVec.data(), refVec.data(), width, height, rowStride); + } + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +// A channel that is flat (all pixels equal) must be left unchanged. +TEST(OpAutoContrast, flat_channel_passthrough) +{ + const int width = 16; + const int height = 12; + nvcv::Tensor srcTensor = nvcv::util::CreateTensor(1, width, height, nvcv::FMT_RGB8); + nvcv::Tensor dstTensor = nvcv::util::CreateTensor(1, width, height, nvcv::FMT_RGB8); + + auto srcData = srcTensor.exportData(); + auto dstData = dstTensor.exportData(); + ASSERT_TRUE(srcData && dstData); + auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcData); + ASSERT_TRUE(srcAccess); + + const long rowStride = srcAccess->rowStride(); + const size_t bufSize = static_cast(rowStride) * height; + std::vector srcVec(bufSize, uint8_t{0}); + std::vector dstVec(bufSize, uint8_t{0}); + + // Constant pixel -> every channel flat -> output must equal input. + for (int y = 0; y < height; ++y) + { + for (int x = 0; x < width; ++x) + { + uchar3 px{37, 200, 5}; + *reinterpret_cast(srcVec.data() + y * rowStride + x * 3) = px; + } + } + + ASSERT_EQ(cudaSuccess, cudaMemcpy(srcData->basePtr(), srcVec.data(), bufSize, cudaMemcpyHostToDevice)); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + cvcuda::AutoContrast op; + ASSERT_NO_THROW(op(stream, srcTensor, dstTensor)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + ASSERT_EQ(cudaSuccess, cudaMemcpy(dstVec.data(), dstData->basePtr(), bufSize, cudaMemcpyDeviceToHost)); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + + ExpectSampleEqual(dstVec.data(), srcVec.data(), width, height, rowStride); +} + +// Single-channel (grayscale) tensor: the lone channel must stretch to the full range. +TEST(OpAutoContrast, single_channel_stretch) +{ + const int width = 64; + const int height = 48; + nvcv::Tensor srcTensor = nvcv::util::CreateTensor(2, width, height, nvcv::FMT_U8); + nvcv::Tensor dstTensor = nvcv::util::CreateTensor(2, width, height, nvcv::FMT_U8); + + auto srcData = srcTensor.exportData(); + auto dstData = dstTensor.exportData(); + ASSERT_TRUE(srcData && dstData); + auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcData); + ASSERT_TRUE(srcAccess); + + const long rowStride = srcAccess->rowStride(); + long sampleStride = srcAccess->sampleStride(); + if (sampleStride == 0) + { + sampleStride = rowStride * height; + } + const size_t bufSize = static_cast(sampleStride) * 2; + std::vector srcVec(bufSize, uint8_t{0}); + std::vector refVec(bufSize, uint8_t{0}); + std::vector dstVec(bufSize, uint8_t{0}); + + std::mt19937_64 rng(7); + for (int s = 0; s < 2; ++s) + { + // Narrow input range so auto-contrast meaningfully stretches it. + std::uniform_int_distribution dist(40, 90); + for (int y = 0; y < height; ++y) + { + for (int x = 0; x < width; ++x) + { + srcVec[s * sampleStride + y * rowStride + x] = static_cast(dist(rng)); + } + } + } + + ASSERT_EQ(cudaSuccess, cudaMemcpy(srcData->basePtr(), srcVec.data(), bufSize, cudaMemcpyHostToDevice)); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + cvcuda::AutoContrast op; + ASSERT_NO_THROW(op(stream, srcTensor, dstTensor)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + ASSERT_EQ(cudaSuccess, cudaMemcpy(dstVec.data(), dstData->basePtr(), bufSize, cudaMemcpyDeviceToHost)); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + + for (int s = 0; s < 2; ++s) + { + AutoContrastGoldSample(srcVec.data() + s * sampleStride, refVec.data() + s * sampleStride, width, height, + rowStride); + } + for (int s = 0; s < 2; ++s) + { + SCOPED_TRACE("sample " + std::to_string(s)); + ExpectSampleEqual(dstVec.data() + s * sampleStride, refVec.data() + s * sampleStride, width, height, + rowStride); + } +} + +TEST(OpAutoContrast, integer_midpoints_truncate_like_pillow_and_torchvision) +{ + ExpectTensorMatchesGold(3, 1, nvcv::FMT_U8, [](int x, int) { return static_cast(x); }); + ExpectTensorMatchesGold(3, 1, nvcv::FMT_U16, [](int x, int) { return static_cast(x); }); +} + +TEST(OpAutoContrast, negative_float_reduction_matches_gold) +{ + ExpectTensorMatchesGold(37, 19, nvcv::FMT_RGBf32, + [](int x, int y) + { + return float3{ + static_cast((x % 11) - 5) * 0.25f, + static_cast((y % 7) - 3) * 0.5f, + static_cast(((x * 3 + y * 5) % 17) - 8) * 0.125f, + }; + }); +} + +TEST(OpAutoContrast, extreme_finite_float_range_matches_gold) +{ + ExpectTensorMatchesGold(3, 1, nvcv::FMT_F32, + [](int x, int) + { + constexpr float max = std::numeric_limits::max(); + constexpr std::array values{-max, 0.f, max}; + return values[x]; + }); +} + +TEST(OpAutoContrast, nonsymmetric_extreme_finite_float_range_matches_gold) +{ + // The subtraction overflows even though every input is finite. At the interior value, a float-halving fallback + // is one ULP above the double-precision reference. + static constexpr std::array values{ + -0x1.66c172p+127f, + 0x1.565eb4p+126f, + 0x1.323798p+127f, + }; + ASSERT_TRUE(std::isinf(values[2] - values[0])); + + ExpectTensorMatchesGold(3, 1, nvcv::FMT_F32, [](int x, int) { return values[x]; }); +} + +TEST(OpAutoContrast, flat_positive_infinite_float_channel_passes_through) +{ + ExpectTensorMatchesGold(7, 3, nvcv::FMT_F32, + [](int, int) { return std::numeric_limits::infinity(); }); +} + +TEST(OpAutoContrast, flat_negative_infinite_float_channel_passes_through) +{ + ExpectTensorMatchesGold(7, 3, nvcv::FMT_F32, + [](int, int) { return -std::numeric_limits::infinity(); }); +} + +TEST(OpAutoContrast, mixed_finite_and_nonfinite_float_channel_matches_gold) +{ + constexpr uint32_t nanBits = 0x7fc12345; + const float nanValue = BitwiseCopy(nanBits); + + const std::array values{-std::numeric_limits::infinity(), -2.f, 0.f, 2.f, + std::numeric_limits::infinity(), nanValue}; + ExpectTensorMatchesGold(values.size(), 1, nvcv::FMT_F32, [&values](int x, int) { return values[x]; }); +} + +constexpr int BOUNDED_GRID_TEST_HEIGHT = 16385; + +static float3 BoundedGridFloat3Pixel(int, int y) +{ + constexpr uint32_t nanBits = 0x7fc12345; + const float nanValue = BitwiseCopy(nanBits); + + float3 pixel{ + -8.f + static_cast(y % 257) * 0.0625f, + 20.f + static_cast((y * 17) % 251) * 0.125f, + -3.5f, + }; + if (y == 0) + { + pixel.x = -std::numeric_limits::infinity(); + } + else if (y == 4096) + { + pixel.x = std::numeric_limits::infinity(); + } + else if (y == 8192) + { + pixel.y = nanValue; + } + else if (y == 12288) + { + pixel.z = -std::numeric_limits::infinity(); + } + return pixel; +} + +TEST(OpAutoContrast, bounded_grid_tensor_interleaved_multichannel_nonfinite_matches_gold) +{ + static_assert((BOUNDED_GRID_TEST_HEIGHT + 3) / 4 > 4096); + ExpectTensorMatchesGold(1, BOUNDED_GRID_TEST_HEIGHT, nvcv::FMT_RGBf32, BoundedGridFloat3Pixel); +} + +TEST(OpAutoContrast, bounded_grid_varshape_interleaved_multichannel_nonfinite_matches_gold) +{ + static_assert((BOUNDED_GRID_TEST_HEIGHT + 3) / 4 > 4096); + ExpectVarShapeMatchesGold(1, BOUNDED_GRID_TEST_HEIGHT, nvcv::FMT_RGBf32, BoundedGridFloat3Pixel); +} + +TEST(OpAutoContrast, zero_batch_tensor_is_noop) +{ + ExpectZeroExtentTensorStatus( + nvcv::TensorShape{ + {0, 3, 5, 1}, + "NHWC" + }, + nvcv::TYPE_U8, NVCV_SUCCESS); +} + +TEST(OpAutoContrast, zero_height_tensor_is_noop) +{ + ExpectZeroExtentTensorStatus( + nvcv::TensorShape{ + {1, 0, 5, 1}, + "NHWC" + }, + nvcv::TYPE_U8, NVCV_SUCCESS); +} + +TEST(OpAutoContrast, zero_width_tensor_is_noop) +{ + ExpectZeroExtentTensorStatus( + nvcv::TensorShape{ + {1, 3, 0, 1}, + "NHWC" + }, + nvcv::TYPE_U8, NVCV_SUCCESS); +} + +TEST(OpAutoContrast, empty_matching_varshape_is_noop) +{ + nvcv::ImageBatchVarShape src(1); + nvcv::ImageBatchVarShape dst(1); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + cvcuda::AutoContrast op; + EXPECT_EQ(NVCV_SUCCESS, nvcv::ProtectCall([&op, stream, &src, &dst] { op(stream, src, dst); })); + + EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpAutoContrast, maximum_supported_height_matches_gold) +{ + constexpr int height = 4 * 65535; + ExpectTensorMatchesGold(1, height, nvcv::FMT_U8, [](int, int y) { return static_cast(y % 251); }); +} + +TEST(OpAutoContrast, mixed_flat_and_nonflat_channels_match_gold) +{ + ExpectTensorMatchesGold(41, 23, nvcv::FMT_RGB8, + [](int x, int y) + { + return uchar3{ + 37, + static_cast(20 + (x * 7 + y * 11) % 151), + static_cast(((x + y) & 1) != 0 ? 220 : 5), + }; + }); +} + +// ============================================================================= +// Planar (NCHW/CHW) layout support +// +// AutoContrast is independent per channel, so a planar input must produce the +// same pixels as the equivalent interleaved input. These cases run identical +// data through both layouts and require the re-interleaved planar output to +// match the interleaved output bit-for-bit. +// ============================================================================= + +namespace { + +void RunPlanarParityTensorCase(nvcv::ImageFormat planarFmt, nvcv::ImageFormat interleavedFmt, int width, int height, + int numImages) +{ + test::planar::RunTensorParity( + planarFmt, interleavedFmt, width, height, width, height, numImages, + [](cudaStream_t stream, const nvcv::Tensor &src, const nvcv::Tensor &dst, nvcv::ImageFormat) + { + cvcuda::AutoContrast op; + EXPECT_NO_THROW(op(stream, src, dst)); + }); +} + +void RunPlanarParityVarShapeCase(nvcv::ImageFormat planarFmt, nvcv::ImageFormat interleavedFmt, int width, int height, + int numImages) +{ + test::planar::RunVarShapeParity(planarFmt, interleavedFmt, width, height, width, height, numImages, + [](cudaStream_t stream, const nvcv::ImageBatchVarShape &src, + const nvcv::ImageBatchVarShape &dst, nvcv::ImageFormat) + { + cvcuda::AutoContrast op; + EXPECT_NO_THROW(op(stream, src, dst)); + }); +} + +} // namespace + +TEST(OpAutoContrast, bounded_grid_tensor_planar_matches_interleaved) +{ + RunPlanarParityTensorCase(nvcv::FMT_RGBf32p, nvcv::FMT_RGBf32, 1, BOUNDED_GRID_TEST_HEIGHT, 1); +} + +TEST(OpAutoContrast, bounded_grid_varshape_planar_matches_interleaved) +{ + RunPlanarParityVarShapeCase(nvcv::FMT_RGBf32p, nvcv::FMT_RGBf32, 1, BOUNDED_GRID_TEST_HEIGHT, 1); +} + +// Parameters: width, height, numImages, planarFmt, interleavedFmt +// clang-format off +NVCV_TEST_SUITE_P(OpAutoContrastPlanar, + test::ValueList{ + {64, 48, 2, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + {37, 29, 1, nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8}, + {41, 33, 2, nvcv::FMT_RGBf32p, nvcv::FMT_RGBf32}, + {35, 31, 1, nvcv::FMT_RGBAf32p, nvcv::FMT_RGBAf32}, + {43, 27, 2, nvcv::ImageFormat{NVCV_IMAGE_FORMAT_RGB16Up}, nvcv::ImageFormat{NVCV_IMAGE_FORMAT_RGB16U}}, + {127, 9, 2, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + {128, 7, 2, nvcv::ImageFormat{NVCV_IMAGE_FORMAT_RGB16Up}, nvcv::ImageFormat{NVCV_IMAGE_FORMAT_RGB16U}}, + {129, 5, 2, nvcv::FMT_RGBAf32p, nvcv::FMT_RGBAf32}, + {255, 9, 2, nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8}, + {256, 7, 2, nvcv::ImageFormat{NVCV_IMAGE_FORMAT_RGB16Up}, nvcv::ImageFormat{NVCV_IMAGE_FORMAT_RGB16U}}, + {257, 5, 2, nvcv::FMT_RGBf32p, nvcv::FMT_RGBf32}, +}); + +// clang-format on + +TEST_P(OpAutoContrastPlanar, tensor_matches_interleaved) +{ + RunPlanarParityTensorCase(GetParamValue<3>(), GetParamValue<4>(), GetParamValue<0>(), GetParamValue<1>(), + GetParamValue<2>()); +} + +TEST_P(OpAutoContrastPlanar, varshape_matches_interleaved) +{ + RunPlanarParityVarShapeCase(GetParamValue<3>(), GetParamValue<4>(), GetParamValue<0>(), GetParamValue<1>(), + GetParamValue<2>()); +} + +// ============================================================================= +// Negative / complement coverage: unsupported inputs must be rejected. +// ============================================================================= + +TEST(OpAutoContrast_Negative, cuda_graph_capture_is_rejected_before_enqueue) +{ + cudaStream_t stream{}; + ASSERT_EQ(cudaSuccess, cudaStreamCreateWithFlags(&stream, cudaStreamNonBlocking)); + + cvcuda::AutoContrast op; + nvcv::Tensor srcTensor = nvcv::util::CreateTensor(1, 3, 2, nvcv::FMT_U8); + nvcv::Tensor dstTensor = nvcv::util::CreateTensor(1, 3, 2, nvcv::FMT_U8); + + nvcv::Image srcImage({3, 2}, nvcv::FMT_U8); + nvcv::Image dstImage({3, 2}, nvcv::FMT_U8); + nvcv::ImageBatchVarShape srcBatch(1); + nvcv::ImageBatchVarShape dstBatch(1); + srcBatch.pushBack(srcImage); + dstBatch.pushBack(dstImage); + + auto expectRejectedWithoutNodes = [stream](const auto &submit) + { + ASSERT_EQ(cudaSuccess, cudaStreamBeginCapture(stream, cudaStreamCaptureModeGlobal)); + EXPECT_EQ(NVCV_ERROR_INVALID_OPERATION, nvcv::ProtectCall(submit)); + + cudaGraph_t graph{}; + ASSERT_EQ(cudaSuccess, cudaStreamEndCapture(stream, &graph)); + size_t numNodes = 1; + EXPECT_EQ(cudaSuccess, cudaGraphGetNodes(graph, nullptr, &numNodes)); + EXPECT_EQ(0, numNodes); + EXPECT_EQ(cudaSuccess, cudaGraphDestroy(graph)); + }; + + expectRejectedWithoutNodes([&op, stream, &srcTensor, &dstTensor] { op(stream, srcTensor, dstTensor); }); + expectRejectedWithoutNodes([&op, stream, &srcBatch, &dstBatch] { op(stream, srcBatch, dstBatch); }); + + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpAutoContrast_Negative, createWithNullHandle) +{ + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, cvcudaAutoContrastCreate(nullptr)); +} + +TEST(OpAutoContrast_Negative, zero_extent_varshape_image) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + cvcuda::AutoContrast op; + for (const nvcv::Size2D size : { + nvcv::Size2D{0, 1}, + nvcv::Size2D{1, 0} + }) + { + SCOPED_TRACE(testing::Message() << "size=" << size.w << 'x' << size.h); + + nvcv::Image srcImage(size, nvcv::FMT_U8); + nvcv::Image dstImage(size, nvcv::FMT_U8); + nvcv::ImageBatchVarShape src(1); + nvcv::ImageBatchVarShape dst(1); + src.pushBack(srcImage); + dst.pushBack(dstImage); + + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&op, stream, &src, &dst] { op(stream, src, dst); })); + } + + EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpAutoContrast_Negative, zero_extent_unsupported_dtype) +{ + ExpectZeroExtentTensorStatus( + nvcv::TensorShape{ + {1, 0, 5, 1}, + "NHWC" + }, + nvcv::TYPE_S16, NVCV_ERROR_INVALID_ARGUMENT); +} + +// clang-format off +#define NVCV_NEG_CASE(W, H, N, FMT) ttype::Types, ttype::Value, ttype::Value, ttype::Value> + +NVCV_TYPED_TEST_SUITE( + OpAutoContrast_Negative, + ttype::Types< + NVCV_NEG_CASE(32, 32, 1, NVCV_IMAGE_FORMAT_S8), // unsupported signed 8-bit + NVCV_NEG_CASE(32, 32, 1, NVCV_IMAGE_FORMAT_S16), // unsupported signed 16-bit + NVCV_NEG_CASE(32, 32, 1, NVCV_IMAGE_FORMAT_S32), // unsupported signed 32-bit + NVCV_NEG_CASE(32, 32, 1, NVCV_IMAGE_FORMAT_F64), // unsupported double + NVCV_NEG_CASE(32, 32, 1, NVCV_IMAGE_FORMAT_2F32)>); // unsupported 2-channel + +// clang-format on + +TYPED_TEST(OpAutoContrast_Negative, unsupported_dtype_or_channels) +{ + const int width = ttype::GetValue; + const int height = ttype::GetValue; + const int numSamples = ttype::GetValue; + const nvcv::ImageFormat fmt{ttype::GetValue}; + + nvcv::Tensor src = nvcv::util::CreateTensor(numSamples, width, height, fmt); + nvcv::Tensor dst = nvcv::util::CreateTensor(numSamples, width, height, fmt); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + cvcuda::AutoContrast op; + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&op, stream, &src, &dst] { op(stream, src, dst); })); + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpAutoContrast_Negative, mismatched_tensors) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + cvcuda::AutoContrast op; + + // Different number of samples. + { + nvcv::Tensor src = nvcv::util::CreateTensor(2, 32, 32, nvcv::FMT_RGB8); + nvcv::Tensor dst = nvcv::util::CreateTensor(1, 32, 32, nvcv::FMT_RGB8); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&op, stream, &src, &dst] { op(stream, src, dst); })); + } + // Different width/height. + { + nvcv::Tensor src = nvcv::util::CreateTensor(1, 32, 32, nvcv::FMT_RGB8); + nvcv::Tensor dst = nvcv::util::CreateTensor(1, 30, 32, nvcv::FMT_RGB8); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&op, stream, &src, &dst] { op(stream, src, dst); })); + } + // Different layout (interleaved vs planar). + { + nvcv::Tensor src = nvcv::util::CreateTensor(1, 32, 32, nvcv::FMT_RGB8); + nvcv::Tensor dst = nvcv::util::CreateTensor(1, 32, 32, nvcv::FMT_RGB8p); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&op, stream, &src, &dst] { op(stream, src, dst); })); + } + // Different data type. + { + nvcv::Tensor src = nvcv::util::CreateTensor(1, 32, 32, nvcv::FMT_RGB8); + nvcv::Tensor dst = nvcv::util::CreateTensor(1, 32, 32, nvcv::FMT_RGBf32); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&op, stream, &src, &dst] { op(stream, src, dst); })); + } + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpAutoContrast_Negative, compound_tensor_dtype) +{ + nvcv::Tensor src( + { + {1, 4, 5, 3}, + "NHWC" + }, + nvcv::TYPE_3U8); + nvcv::Tensor dst( + { + {1, 4, 5, 3}, + "NHWC" + }, + nvcv::TYPE_3U8); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + cvcuda::AutoContrast op; + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&op, stream, &src, &dst] { op(stream, src, dst); })); + + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpAutoContrast_Negative, nonpacked_singleton_channel_tensor) +{ + constexpr int64_t bufferSize = 64; + NVCVByte *srcAllocation{}; + NVCVByte *dstAllocation{}; + ASSERT_EQ(cudaSuccess, cudaMalloc(reinterpret_cast(&srcAllocation), bufferSize)); + ASSERT_EQ(cudaSuccess, cudaMalloc(reinterpret_cast(&dstAllocation), bufferSize)); + + nvcv::TensorDataStridedCuda::Buffer srcBuffer{}; + srcBuffer.basePtr = srcAllocation; + srcBuffer.strides[0] = 32; + srcBuffer.strides[1] = 16; + srcBuffer.strides[2] = 2; + srcBuffer.strides[3] = 2; + auto dstBuffer = srcBuffer; + dstBuffer.basePtr = dstAllocation; + + { + nvcv::Tensor src = nvcv::TensorWrapData(nvcv::TensorDataStridedCuda{ + nvcv::TensorShape{{1, 2, 4, 1}, "NHWC"}, + nvcv::TYPE_U8, srcBuffer + }); + nvcv::Tensor dst = nvcv::TensorWrapData(nvcv::TensorDataStridedCuda{ + nvcv::TensorShape{{1, 2, 4, 1}, "NHWC"}, + nvcv::TYPE_U8, dstBuffer + }); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + cvcuda::AutoContrast op; + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&op, stream, &src, &dst] { op(stream, src, dst); })); + + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + } + + ASSERT_EQ(cudaSuccess, cudaFree(srcAllocation)); + ASSERT_EQ(cudaSuccess, cudaFree(dstAllocation)); +} + +TEST(OpAutoContrast_Negative, dynamic_tensor_strides_must_fit_int32) +{ + constexpr int64_t oversizedStride = static_cast(std::numeric_limits::max()) + 1; + NVCVByte *srcAllocation{}; + NVCVByte *dstAllocation{}; + ASSERT_EQ(cudaSuccess, cudaMalloc(reinterpret_cast(&srcAllocation), 1)); + ASSERT_EQ(cudaSuccess, cudaMalloc(reinterpret_cast(&dstAllocation), 1)); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + cvcuda::AutoContrast op; + + for (const nvcv::TensorLayout layout : {nvcv::TENSOR_HWC, nvcv::TENSOR_CHW}) + { + SCOPED_TRACE(layout); + nvcv::TensorDataStridedCuda::Buffer srcBuffer{}; + srcBuffer.basePtr = srcAllocation; + srcBuffer.strides[0] = oversizedStride; + srcBuffer.strides[1] = 1; + srcBuffer.strides[2] = 1; + if (layout == nvcv::TENSOR_CHW) + { + srcBuffer.strides[1] = oversizedStride; + } + auto dstBuffer = srcBuffer; + dstBuffer.basePtr = dstAllocation; + + nvcv::Tensor src = nvcv::TensorWrapData(nvcv::TensorDataStridedCuda{ + nvcv::TensorShape{{1, 1, 1}, layout}, + nvcv::TYPE_U8, srcBuffer + }); + nvcv::Tensor dst = nvcv::TensorWrapData(nvcv::TensorDataStridedCuda{ + nvcv::TensorShape{{1, 1, 1}, layout}, + nvcv::TYPE_U8, dstBuffer + }); + + EXPECT_EQ(NVCV_ERROR_OVERFLOW, nvcv::ProtectCall([&op, stream, &src, &dst] { op(stream, src, dst); })); + } + + { + SCOPED_TRACE("CHW plane stride"); + nvcv::TensorDataStridedCuda::Buffer srcBuffer{}; + srcBuffer.basePtr = srcAllocation; + srcBuffer.strides[0] = oversizedStride; + srcBuffer.strides[1] = 1; + srcBuffer.strides[2] = 1; + auto dstBuffer = srcBuffer; + dstBuffer.basePtr = dstAllocation; + + nvcv::Tensor src = nvcv::TensorWrapData(nvcv::TensorDataStridedCuda{ + nvcv::TensorShape{{3, 1, 1}, "CHW"}, + nvcv::TYPE_U8, srcBuffer + }); + nvcv::Tensor dst = nvcv::TensorWrapData(nvcv::TensorDataStridedCuda{ + nvcv::TensorShape{{3, 1, 1}, "CHW"}, + nvcv::TYPE_U8, dstBuffer + }); + + EXPECT_EQ(NVCV_ERROR_OVERFLOW, nvcv::ProtectCall([&op, stream, &src, &dst] { op(stream, src, dst); })); + } + + { + SCOPED_TRACE("maximum byte offset"); + nvcv::TensorDataStridedCuda::Buffer srcBuffer{}; + srcBuffer.basePtr = srcAllocation; + srcBuffer.strides[0] = std::numeric_limits::max(); + srcBuffer.strides[1] = 1; + srcBuffer.strides[2] = 1; + auto dstBuffer = srcBuffer; + dstBuffer.basePtr = dstAllocation; + + nvcv::Tensor src = nvcv::TensorWrapData(nvcv::TensorDataStridedCuda{ + nvcv::TensorShape{{2, 2, 1}, "HWC"}, + nvcv::TYPE_U8, srcBuffer + }); + nvcv::Tensor dst = nvcv::TensorWrapData(nvcv::TensorDataStridedCuda{ + nvcv::TensorShape{{2, 2, 1}, "HWC"}, + nvcv::TYPE_U8, dstBuffer + }); + + EXPECT_EQ(NVCV_ERROR_OVERFLOW, nvcv::ProtectCall([&op, stream, &src, &dst] { op(stream, src, dst); })); + } + + EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + ASSERT_EQ(cudaSuccess, cudaFree(srcAllocation)); + ASSERT_EQ(cudaSuccess, cudaFree(dstAllocation)); +} + +TEST(OpAutoContrast_Negative, raw_tensor_dimensions_are_validated_before_narrowing) +{ + NVCVByte *srcAllocation{}; + NVCVByte *dstAllocation{}; + ASSERT_EQ(cudaSuccess, cudaMalloc(reinterpret_cast(&srcAllocation), 1)); + ASSERT_EQ(cudaSuccess, cudaMalloc(reinterpret_cast(&dstAllocation), 1)); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + cvcuda::AutoContrast op; + auto expectStatus = [&](const nvcv::TensorShape &shape, NVCVStatus expected) + { + nvcv::TensorDataStridedCuda::Buffer srcBuffer{}; + srcBuffer.basePtr = srcAllocation; + for (int d = 0; d < shape.rank(); ++d) + { + srcBuffer.strides[d] = 1; + } + auto dstBuffer = srcBuffer; + dstBuffer.basePtr = dstAllocation; + + nvcv::Tensor src = nvcv::TensorWrapData(nvcv::TensorDataStridedCuda{shape, nvcv::TYPE_U8, srcBuffer}); + nvcv::Tensor dst = nvcv::TensorWrapData(nvcv::TensorDataStridedCuda{shape, nvcv::TYPE_U8, dstBuffer}); + EXPECT_EQ(expected, nvcv::ProtectCall([&op, stream, &src, &dst] { op(stream, src, dst); })); + }; + + constexpr int64_t wrapsToOne = (int64_t{1} << 32) + 1; + expectStatus( + nvcv::TensorShape{ + {wrapsToOne, 1, 1, 1}, + "NHWC" + }, + NVCV_ERROR_INVALID_ARGUMENT); + expectStatus( + nvcv::TensorShape{ + {wrapsToOne, 1, 1}, + "HWC" + }, + NVCV_ERROR_INVALID_ARGUMENT); + expectStatus( + nvcv::TensorShape{ + {1, wrapsToOne, 1}, + "HWC" + }, + NVCV_ERROR_OVERFLOW); + expectStatus( + nvcv::TensorShape{ + {1, 1, wrapsToOne}, + "HWC" + }, + NVCV_ERROR_INVALID_ARGUMENT); + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + ASSERT_EQ(cudaSuccess, cudaFree(srcAllocation)); + ASSERT_EQ(cudaSuccess, cudaFree(dstAllocation)); +} + +TEST(OpAutoContrast_Negative, height_exceeding_cuda_grid_limit) +{ + constexpr int64_t height = 4 * 65535 + 1; + constexpr int64_t bufferSize = height; + NVCVByte *srcAllocation{}; + NVCVByte *dstAllocation{}; + ASSERT_EQ(cudaSuccess, cudaMalloc(reinterpret_cast(&srcAllocation), bufferSize)); + ASSERT_EQ(cudaSuccess, cudaMalloc(reinterpret_cast(&dstAllocation), bufferSize)); + + nvcv::TensorDataStridedCuda::Buffer srcBuffer{}; + srcBuffer.basePtr = srcAllocation; + srcBuffer.strides[0] = height; + srcBuffer.strides[1] = 1; + srcBuffer.strides[2] = 1; + srcBuffer.strides[3] = 1; + auto dstBuffer = srcBuffer; + dstBuffer.basePtr = dstAllocation; + + { + nvcv::Tensor src = nvcv::TensorWrapData(nvcv::TensorDataStridedCuda{ + nvcv::TensorShape{{1, height, 1, 1}, "NHWC"}, + nvcv::TYPE_U8, srcBuffer + }); + nvcv::Tensor dst = nvcv::TensorWrapData(nvcv::TensorDataStridedCuda{ + nvcv::TensorShape{{1, height, 1, 1}, "NHWC"}, + nvcv::TYPE_U8, dstBuffer + }); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + cvcuda::AutoContrast op; + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&op, stream, &src, &dst] { op(stream, src, dst); })); + + EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + } + + ASSERT_EQ(cudaSuccess, cudaFree(srcAllocation)); + ASSERT_EQ(cudaSuccess, cudaFree(dstAllocation)); +} + +TEST(OpAutoContrast_Negative, varshape_height_exceeding_cuda_grid_limit) +{ + constexpr int height = 4 * 65535 + 1; + nvcv::Image srcImage({1, height}, nvcv::FMT_U8); + nvcv::Image dstImage({1, height}, nvcv::FMT_U8); + nvcv::ImageBatchVarShape src(1); + nvcv::ImageBatchVarShape dst(1); + src.pushBack(srcImage); + dst.pushBack(dstImage); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + cvcuda::AutoContrast op; + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&op, stream, &src, &dst] { op(stream, src, dst); })); + + EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpAutoContrast_Negative, varshape_plane_offset_must_fit_int32) +{ + NVCVByte *srcAllocation{}; + NVCVByte *dstAllocation{}; + ASSERT_EQ(cudaSuccess, cudaMalloc(reinterpret_cast(&srcAllocation), 1)); + ASSERT_EQ(cudaSuccess, cudaMalloc(reinterpret_cast(&dstAllocation), 1)); + + nvcv::ImageDataStridedCuda::Buffer srcBuffer{}; + srcBuffer.numPlanes = 1; + srcBuffer.planes[0].width = 2; + srcBuffer.planes[0].height = 2; + srcBuffer.planes[0].rowStride = std::numeric_limits::max(); + srcBuffer.planes[0].basePtr = srcAllocation; + auto dstBuffer = srcBuffer; + dstBuffer.planes[0].basePtr = dstAllocation; + + { + nvcv::Image srcImage = nvcv::ImageWrapData(nvcv::ImageDataStridedCuda{nvcv::FMT_U8, srcBuffer}); + nvcv::Image dstImage = nvcv::ImageWrapData(nvcv::ImageDataStridedCuda{nvcv::FMT_U8, dstBuffer}); + nvcv::ImageBatchVarShape srcBatch(1); + nvcv::ImageBatchVarShape dstBatch(1); + srcBatch.pushBack(srcImage); + dstBatch.pushBack(dstImage); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + cvcuda::AutoContrast op; + EXPECT_EQ(NVCV_ERROR_OVERFLOW, + nvcv::ProtectCall([&op, stream, &srcBatch, &dstBatch] { op(stream, srcBatch, dstBatch); })); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + } + + ASSERT_EQ(cudaSuccess, cudaFree(srcAllocation)); + ASSERT_EQ(cudaSuccess, cudaFree(dstAllocation)); +} + +TEST(OpAutoContrast, launch_grid_arithmetic_does_not_overflow) +{ + constexpr int64_t maxWidth = std::numeric_limits::max(); + EXPECT_EQ(8388608, cvcuda::priv::detail::AutoContrastGridX(maxWidth, 32, 8)); + EXPECT_EQ(16777216, cvcuda::priv::detail::AutoContrastGridX(maxWidth, 32, 4)); + EXPECT_EQ(67108864, cvcuda::priv::detail::AutoContrastGridX(maxWidth, 32, 4, 4)); +} + +TEST(OpAutoContrast_Negative, varshape_plane_descriptors_must_match_format) +{ + NVCVByte *srcAllocation{}; + NVCVByte *dstAllocation{}; + ASSERT_EQ(cudaSuccess, cudaMalloc(reinterpret_cast(&srcAllocation), 1)); + ASSERT_EQ(cudaSuccess, cudaMalloc(reinterpret_cast(&dstAllocation), 1)); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + cvcuda::AutoContrast op; + auto expectInvalid = [&](nvcv::ImageDataStridedCuda::Buffer srcBuffer) + { + auto dstBuffer = srcBuffer; + for (int p = 0; p < NVCV_MAX_PLANE_COUNT; ++p) + { + if (srcBuffer.planes[p].width > 0) + { + srcBuffer.planes[p].basePtr = srcAllocation; + dstBuffer.planes[p].basePtr = dstAllocation; + } + } + + nvcv::Image srcImage = nvcv::ImageWrapData(nvcv::ImageDataStridedCuda{nvcv::FMT_RGB8p, srcBuffer}); + nvcv::Image dstImage = nvcv::ImageWrapData(nvcv::ImageDataStridedCuda{nvcv::FMT_RGB8p, dstBuffer}); + nvcv::ImageBatchVarShape srcBatch(1); + nvcv::ImageBatchVarShape dstBatch(1); + srcBatch.pushBack(srcImage); + dstBatch.pushBack(dstImage); + + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&op, stream, &srcBatch, &dstBatch] { op(stream, srcBatch, dstBatch); })); + }; + + nvcv::ImageDataStridedCuda::Buffer missingPlanes{}; + missingPlanes.numPlanes = 1; + for (int p = 0; p < 3; ++p) + { + missingPlanes.planes[p].width = 1; + missingPlanes.planes[p].height = 1; + missingPlanes.planes[p].rowStride = 1; + } + expectInvalid(missingPlanes); + + nvcv::ImageDataStridedCuda::Buffer wrongPlaneSize{}; + wrongPlaneSize.numPlanes = 3; + wrongPlaneSize.planes[0].width = 1; + wrongPlaneSize.planes[0].height = 1; + wrongPlaneSize.planes[0].rowStride = 1; + for (int p = 1; p < wrongPlaneSize.numPlanes; ++p) + { + wrongPlaneSize.planes[p].width = 2; + wrongPlaneSize.planes[p].height = 2; + wrongPlaneSize.planes[p].rowStride = 2; + } + expectInvalid(wrongPlaneSize); + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + ASSERT_EQ(cudaSuccess, cudaFree(srcAllocation)); + ASSERT_EQ(cudaSuccess, cudaFree(dstAllocation)); +} + +TEST(OpAutoContrast_Negative, different_format_varshape) +{ + const int numSamples = 4; + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + std::vector imgSrc; + std::vector imgDst; + for (int s = 0; s < numSamples - 1; ++s) + { + imgSrc.emplace_back(nvcv::Size2D{32, 32}, nvcv::FMT_RGB8); + imgDst.emplace_back(nvcv::Size2D{32, 32}, nvcv::FMT_RGB8); + } + // Last image has a different format -> non-unique batch format must be rejected. + imgSrc.emplace_back(nvcv::Size2D{32, 32}, nvcv::FMT_RGBA8); + imgDst.emplace_back(nvcv::Size2D{32, 32}, nvcv::FMT_RGBA8); + + nvcv::ImageBatchVarShape batchSrc(numSamples); + nvcv::ImageBatchVarShape batchDst(numSamples); + batchSrc.pushBack(imgSrc.begin(), imgSrc.end()); + batchDst.pushBack(imgDst.begin(), imgDst.end()); + + cvcuda::AutoContrast op; + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&op, stream, &batchSrc, &batchDst] { op(stream, batchSrc, batchDst); })); + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +static void ExpectRejectsSubsampledVarShape(const char (&fourcc)[5], nvcv::ChromaSubsampling expectedCss, + nvcv::Size2D expectedChromaSize) +{ + constexpr nvcv::Size2D size{64, 48}; + const nvcv::ImageFormat fmt = nvcv::ImageFormat::FromFourCC(FourCC(fourcc), nvcv::CSPEC_BT601, nvcv::MemLayout::PL); + + SCOPED_TRACE(fourcc); + ASSERT_EQ(3, fmt.numChannels()); + ASSERT_EQ(3, fmt.numPlanes()); + ASSERT_EQ(expectedCss, fmt.chromaSubsampling()); + for (int p = 0; p < 3; ++p) + { + ASSERT_EQ(nvcv::TYPE_U8, fmt.planeDataType(p)); + } + ASSERT_EQ(expectedChromaSize, fmt.planeSize(size, 1)); + + nvcv::Image srcImage(size, fmt); + nvcv::Image dstImage(size, fmt); + nvcv::ImageBatchVarShape src(1); + nvcv::ImageBatchVarShape dst(1); + src.pushBack(srcImage); + dst.pushBack(dstImage); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + cvcuda::AutoContrast op; + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&op, stream, &src, &dst] { op(stream, src, dst); })); + + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpAutoContrast_Negative, i420_varshape) +{ + ExpectRejectsSubsampledVarShape("I420", nvcv::ChromaSubsampling::CSS_420, {32, 24}); +} + +TEST(OpAutoContrast_Negative, yv16_varshape) +{ + ExpectRejectsSubsampledVarShape("YV16", nvcv::ChromaSubsampling::CSS_422, {32, 48}); +} diff --git a/tests/cvcuda/system/TestOpAverageBlur.cpp b/tests/cvcuda/system/TestOpAverageBlur.cpp index 6804debb9..7e96fa3e0 100644 --- a/tests/cvcuda/system/TestOpAverageBlur.cpp +++ b/tests/cvcuda/system/TestOpAverageBlur.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "ConvUtils.hpp" #include "Definitions.hpp" +#include "PlanarParityUtils.hpp" #include #include @@ -32,6 +33,29 @@ namespace test = nvcv::test; namespace cuda = nvcv::cuda; +namespace { + +// builds AverageBlur and its per-image parameter tensors for a var-shape negative case +inline void InvokeAverageBlurVarShapeNegative(cudaStream_t stream, const nvcv::ImageBatchVarShape &src, + const nvcv::ImageBatchVarShape &dst, int maxBatches, + NVCVBorderType borderMode) +{ + const nvcv::Size2D kernelSize(3, 3); + const int numImages = src.numImages(); + auto kernelSizeTensor + = test::planar::MakePerImageTensor(numImages, nvcv::TYPE_2S32, int2{kernelSize.w, kernelSize.h}); + auto kernelAnchorTensor = test::planar::MakePerImageTensor(numImages, nvcv::TYPE_2S32, int2{-1, -1}); + cvcuda::AverageBlur op(kernelSize, maxBatches); + op(stream, src, dst, kernelSizeTensor, kernelAnchorTensor, borderMode); +} + +} // namespace + +static int ScaledSize(int size, double scale) +{ + return static_cast(size * scale); +} + // clang-format off NVCV_TEST_SUITE_P(OpAverageBlur, test::ValueList @@ -102,10 +126,10 @@ TEST_P(OpAverageBlur, correct_output) std::vector inVec(inBufSize); - std::default_random_engine randEng(0); - std::uniform_int_distribution rand(0u, 255u); + std::default_random_engine randEng(0); + std::uniform_int_distribution rand(0, 255); - std::generate(inVec.begin(), inVec.end(), [&]() { return rand(randEng); }); + std::ranges::generate(inVec, [&rand, &randEng]() { return rand(randEng); }); // copy random input to device ASSERT_EQ(cudaSuccess, cudaMemcpy(inData->basePtr(), inVec.data(), inBufSize, cudaMemcpyHostToDevice)); @@ -158,9 +182,9 @@ TEST_P(OpAverageBlur, varshape_correct_output) int2 kernelAnchor{kanchorX, kanchorY}; // Create input varshape - std::default_random_engine rng; - std::uniform_int_distribution udistWidth(width * 0.8, width * 1.1); - std::uniform_int_distribution udistHeight(height * 0.8, height * 1.1); + std::default_random_engine rng; + std::uniform_int_distribution udistWidth(ScaledSize(width, 0.8), ScaledSize(width, 1.1)); + std::uniform_int_distribution udistHeight(ScaledSize(height, 0.8), ScaledSize(height, 1.1)); std::vector imgSrc; @@ -177,7 +201,7 @@ TEST_P(OpAverageBlur, varshape_correct_output) std::uniform_int_distribution udist(0, 255); srcVec[i].resize(imgSrc[i].size().h * srcRowStride); - std::generate(srcVec[i].begin(), srcVec[i].end(), [&]() { return udist(rng); }); + std::ranges::generate(srcVec[i], [&udist, &rng]() { return udist(rng); }); auto imgData = imgSrc[i].exportData(); ASSERT_NE(imgData, nvcv::NullOpt); @@ -267,23 +291,84 @@ TEST_P(OpAverageBlur, varshape_correct_output) } } +// AverageBlur filters each channel independently, so a planar input is filtered plane-by-plane and +// must produce exactly the same pixels as the interleaved path. These tests feed identical uint8 +// data through cvcuda::AverageBlur in both layouts and require the re-interleaved planar output to +// match the interleaved output bit-for-bit. +// ============================================================================= + +// Parameters: width, height, kernelWidth, kernelHeight, borderMode, numImages, planarFmt, interleavedFmt // clang-format off -NVCV_TEST_SUITE_P(OpAverageBlur_Negative, nvcv::test::ValueList{ - {nvcv::FMT_U8, nvcv::FMT_U16, 3, 3, -1, -1, NVCV_BORDER_CONSTANT}, // data type is different - {nvcv::FMT_RGB8, nvcv::FMT_RGB8p, 3, 3, -1, -1, NVCV_BORDER_CONSTANT}, // data format is different - {nvcv::FMT_RGB8p, nvcv::FMT_RGB8p, 3, 3, -1, -1, NVCV_BORDER_CONSTANT}, // data format is not kNHWC/kHWC - {nvcv::FMT_F16, nvcv::FMT_F16, 3, 3, -1, -1, NVCV_BORDER_CONSTANT}, // invalid data type - {nvcv::FMT_U8, nvcv::FMT_U8, 4, 3, -1, -1, NVCV_BORDER_CONSTANT}, // invalid kernel size - {nvcv::FMT_U8, nvcv::FMT_U8, 3, 4, -1, -1, NVCV_BORDER_CONSTANT}, // invalid kernel size - {nvcv::FMT_U8, nvcv::FMT_U8, 3, 3, -1, -2, NVCV_BORDER_CONSTANT}, // invalid kernel anchor - {nvcv::FMT_U8, nvcv::FMT_U8, 3, 3, -2, -1, NVCV_BORDER_CONSTANT}, // invalid kernel anchor -#ifndef ENABLE_SANITIZER - {nvcv::FMT_U8, nvcv::FMT_U8, 3, 3, -1, -1, static_cast(255)}, // invalid borderType -#endif +NVCV_TEST_SUITE_P(OpAverageBlurPlanar, + test::ValueList{ + { 64, 48, 3, 3, NVCV_BORDER_CONSTANT, 2, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + { 67, 51, 5, 3, NVCV_BORDER_REFLECT, 1, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + { 65, 49, 5, 5, NVCV_BORDER_CONSTANT, 2, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + { 50, 40, 7, 7, NVCV_BORDER_REPLICATE, 2, nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8}, + { 64, 48, 9, 5, NVCV_BORDER_REFLECT101, 1, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + { 32, 28, 3, 5, NVCV_BORDER_WRAP, 1, nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8}, }); // clang-format on +TEST_P(OpAverageBlurPlanar, tensor_matches_interleaved) +{ + nvcv::Size2D kernelSize{GetParamValue<2>(), GetParamValue<3>()}; + NVCVBorderType borderMode = GetParamValue<4>(); + int numImages = GetParamValue<5>(); + + test::planar::RunTensorParity(GetParamValue<6>(), GetParamValue<7>(), GetParamValue<0>(), GetParamValue<1>(), + GetParamValue<0>(), GetParamValue<1>(), numImages, + [kernelSize, borderMode, numImages](cudaStream_t s, const nvcv::Tensor &src, + const nvcv::Tensor &dst, nvcv::ImageFormat) + { + cvcuda::AverageBlur op(kernelSize, numImages); + int2 kernelAnchor{-1, -1}; + EXPECT_NO_THROW(op(s, src, dst, kernelSize, kernelAnchor, borderMode)); + }); +} + +TEST_P(OpAverageBlurPlanar, varshape_matches_interleaved) +{ + nvcv::Size2D kernelSize{GetParamValue<2>(), GetParamValue<3>()}; + NVCVBorderType borderMode = GetParamValue<4>(); + int numImages = GetParamValue<5>(); + + auto kernelSizeTensor + = test::planar::MakePerImageTensor(numImages, nvcv::TYPE_2S32, int2{kernelSize.w, kernelSize.h}); + auto kernelAnchorTensor = test::planar::MakePerImageTensor(numImages, nvcv::TYPE_2S32, int2{-1, -1}); + + test::planar::RunVarShapeParity( + GetParamValue<6>(), GetParamValue<7>(), GetParamValue<0>(), GetParamValue<1>(), GetParamValue<0>(), + GetParamValue<1>(), numImages, + [&kernelSizeTensor, &kernelAnchorTensor, kernelSize, borderMode, numImages]( + cudaStream_t s, const nvcv::ImageBatchVarShape &src, const nvcv::ImageBatchVarShape &dst, nvcv::ImageFormat) + { + cvcuda::AverageBlur op(kernelSize, numImages); + EXPECT_NO_THROW(op(s, src, dst, kernelSizeTensor, kernelAnchorTensor, borderMode)); + }); +} + +static auto OpAverageBlurNegativeParams() +{ + nvcv::test::ValueList params{ + { nvcv::FMT_U8, nvcv::FMT_U16, 3, 3, -1, -1, NVCV_BORDER_CONSTANT}, // data type is different + { nvcv::FMT_RGB8, nvcv::FMT_RGB8p, 3, 3, -1, -1, NVCV_BORDER_CONSTANT}, // interleaved in, planar out + {nvcv::FMT_RGB8p, nvcv::FMT_RGB8, 3, 3, -1, -1, NVCV_BORDER_CONSTANT}, // planar in, interleaved out + { nvcv::FMT_F16, nvcv::FMT_F16, 3, 3, -1, -1, NVCV_BORDER_CONSTANT}, // invalid data type + { nvcv::FMT_U8, nvcv::FMT_U8, 4, 3, -1, -1, NVCV_BORDER_CONSTANT}, // invalid kernel size + { nvcv::FMT_U8, nvcv::FMT_U8, 3, 4, -1, -1, NVCV_BORDER_CONSTANT}, // invalid kernel size + { nvcv::FMT_U8, nvcv::FMT_U8, 3, 3, -1, -2, NVCV_BORDER_CONSTANT}, // invalid kernel anchor + { nvcv::FMT_U8, nvcv::FMT_U8, 3, 3, -2, -1, NVCV_BORDER_CONSTANT}, // invalid kernel anchor + }; +#ifndef ENABLE_SANITIZER + params.emplace_back(nvcv::FMT_U8, nvcv::FMT_U8, 3, 3, -1, -1, static_cast(255)); +#endif + return params; +} + +NVCV_TEST_SUITE_P(OpAverageBlur_Negative, OpAverageBlurNegativeParams()); + TEST_P(OpAverageBlur_Negative, op) { cudaStream_t stream; @@ -313,136 +398,22 @@ TEST_P(OpAverageBlur_Negative, op) EXPECT_EQ( NVCV_ERROR_INVALID_ARGUMENT, - nvcv::ProtectCall([&] { averageBlurOp(stream, inTensor, outTensor, kernelSize, kernelAnchor, borderMode); })); + nvcv::ProtectCall([&averageBlurOp, &stream, &inTensor, &outTensor, &kernelSize, &kernelAnchor, &borderMode] + { averageBlurOp(stream, inTensor, outTensor, kernelSize, kernelAnchor, borderMode); })); } -// clang-format off -NVCV_TEST_SUITE_P(OpAverageBlurVarshape_Negative, test::ValueList{ - {nvcv::FMT_RGB8, nvcv::FMT_RGB8p, NVCV_BORDER_CONSTANT, 3, 3}, - {nvcv::FMT_RGB8p, nvcv::FMT_RGB8p, NVCV_BORDER_CONSTANT, 3, 3}, - {nvcv::FMT_RGBf16, nvcv::FMT_RGBf16, NVCV_BORDER_CONSTANT, 3, 3}, - {nvcv::FMT_RGB8, nvcv::FMT_RGB8, NVCV_BORDER_CONSTANT, 3, -1}, - {nvcv::FMT_RGB8, nvcv::FMT_RGB8, NVCV_BORDER_CONSTANT, 5, 3}, -#ifndef ENABLE_SANITIZER - {nvcv::FMT_RGB8, nvcv::FMT_RGB8, static_cast(255), 3, 3}, -#endif -}); -// clang-format on +NVCV_TEST_SUITE_P(OpAverageBlurVarshape_Negative, test::PlanarFilterVarShapeNegativeParams()); TEST_P(OpAverageBlurVarshape_Negative, varshape_correct_output) { - cudaStream_t stream; - ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); - - int width = 32; - int height = 32; - nvcv::Size2D kernelSize(3, 3); - - nvcv::ImageFormat inputFmt = GetParamValue<0>(); - nvcv::ImageFormat outputFmt = GetParamValue<1>(); - NVCVBorderType borderMode = GetParamValue<2>(); - int batches = GetParamValue<3>(); - int maxBatches = GetParamValue<4>(); - - // Create input varshape - std::default_random_engine rng; - std::uniform_int_distribution udistWidth(width * 0.8, width * 1.1); - std::uniform_int_distribution udistHeight(height * 0.8, height * 1.1); - - std::vector imgSrc; - std::vector imgDst; - - for (int i = 0; i < batches; ++i) - { - imgSrc.emplace_back(nvcv::Size2D{udistWidth(rng), udistHeight(rng)}, inputFmt); - imgDst.emplace_back(imgSrc[i].size(), outputFmt); - } - - nvcv::ImageBatchVarShape batchSrc(batches); - batchSrc.pushBack(imgSrc.begin(), imgSrc.end()); - nvcv::ImageBatchVarShape batchDst(batches); - batchDst.pushBack(imgDst.begin(), imgDst.end()); - - // Create kernel size tensor - nvcv::Tensor kernelSizeTensor({{batches}, "N"}, nvcv::TYPE_2S32); - - // Create kernel anchor tensor - nvcv::Tensor kernelAnchorTensor({{batches}, "N"}, nvcv::TYPE_2S32); - - // Run operator - cvcuda::AverageBlur averageBlurOp(kernelSize, maxBatches); - - EXPECT_EQ( - NVCV_ERROR_INVALID_ARGUMENT, - nvcv::ProtectCall( - [&] { averageBlurOp(stream, batchSrc, batchDst, kernelSizeTensor, kernelAnchorTensor, borderMode); })); - - ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); - ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + test::planar::ExpectVarShapeUniformFormatRejected(GetParamValue<0>(), GetParamValue<1>(), GetParamValue<3>(), + GetParamValue<4>(), GetParamValue<2>(), + InvokeAverageBlurVarShapeNegative); } TEST_P(OpAverageBlurVarshape_Negative, varshape_hasDifferentFormat) { - cudaStream_t stream; - ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); - - nvcv::ImageFormat fmt = nvcv::FMT_RGB8; - - std::vector> testSet{ - {nvcv::FMT_U8, fmt}, - { fmt, nvcv::FMT_U8} - }; - - for (auto testCase : testSet) - { - nvcv::ImageFormat inputFmtExtra = std::get<0>(testCase); - nvcv::ImageFormat outputFmtExtra = std::get<1>(testCase); - - int width = 32; - int height = 32; - int batches = 3; - nvcv::Size2D kernelSize(3, 3); - NVCVBorderType borderMode = NVCV_BORDER_CONSTANT; - - // Create input varshape - std::default_random_engine rng; - std::uniform_int_distribution udistWidth(width * 0.8, width * 1.1); - std::uniform_int_distribution udistHeight(height * 0.8, height * 1.1); - - std::vector imgSrc; - std::vector imgDst; - - for (int i = 0; i < batches - 1; ++i) - { - imgSrc.emplace_back(nvcv::Size2D{udistWidth(rng), udistHeight(rng)}, fmt); - imgDst.emplace_back(imgSrc[i].size(), fmt); - } - imgSrc.emplace_back(nvcv::Size2D{udistWidth(rng), udistHeight(rng)}, inputFmtExtra); - imgDst.emplace_back(imgSrc.back().size(), outputFmtExtra); - - nvcv::ImageBatchVarShape batchSrc(batches); - batchSrc.pushBack(imgSrc.begin(), imgSrc.end()); - nvcv::ImageBatchVarShape batchDst(batches); - batchDst.pushBack(imgDst.begin(), imgDst.end()); - - // Create kernel size tensor - nvcv::Tensor kernelSizeTensor({{batches}, "N"}, nvcv::TYPE_2S32); - - // Create kernel anchor tensor - nvcv::Tensor kernelAnchorTensor({{batches}, "N"}, nvcv::TYPE_2S32); - - // Run operator - cvcuda::AverageBlur averageBlurOp(kernelSize, batches); - - EXPECT_EQ( - NVCV_ERROR_INVALID_ARGUMENT, - nvcv::ProtectCall( - [&] { averageBlurOp(stream, batchSrc, batchDst, kernelSizeTensor, kernelAnchorTensor, borderMode); })); - - ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); - } - - ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + test::planar::ExpectVarShapeMixedFormatRejected(InvokeAverageBlurVarShapeNegative); } TEST(OpAverageBlur_Negative, create_null_handle) diff --git a/tests/cvcuda/system/TestOpBilateralFilter.cpp b/tests/cvcuda/system/TestOpBilateralFilter.cpp index ae1c7d966..3d678bb6b 100644 --- a/tests/cvcuda/system/TestOpBilateralFilter.cpp +++ b/tests/cvcuda/system/TestOpBilateralFilter.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,6 +16,7 @@ */ #include "Definitions.hpp" +#include "PlanarParityUtils.hpp" #include #include @@ -25,6 +26,8 @@ #include #include +#include +#include #include #include #include @@ -34,48 +37,51 @@ namespace gt = ::testing; namespace test = nvcv::test; -static uint32_t saturate_cast(float n) +using TensorDim = nvcv::TensorShape::DimType; + +static uint8_t saturate_cast(float n) { - return static_cast(std::min(255.0f, std::round(n))); + return static_cast(std::min(255.0f, std::round(n))); } -static bool CompareImages(uint8_t *pTest, uint8_t *pGold, size_t columns, size_t rows, size_t rowStride, +static bool CompareImages(const uint8_t *pTest, const uint8_t *pGold, size_t columns, size_t rows, size_t rowStride, size_t channels, float delta) { + const size_t activeRowBytes = columns * channels; for (size_t j = 0; j < rows; j++) { - for (size_t k = 0; k < columns; k++) + const size_t rowOffset = j * rowStride; + for (size_t k = 0; k < activeRowBytes; k++) { - for (size_t c = 0; c < channels; ++c) + const size_t offset = rowOffset + k; + float diff = std::abs(static_cast(pTest[offset]) - static_cast(pGold[offset])); + if (diff > delta) { - size_t offset = j * rowStride + k * channels + c; - float diff = std::abs(static_cast(pTest[offset]) - static_cast(pGold[offset])); - if (diff > delta) - { - return false; - } + return false; } } } return true; } -static bool CompareTensors(std::vector &vTest, std::vector &vGold, size_t columns, size_t rows, - size_t batch, size_t rowStride, size_t channels, size_t sampleStride, float delta) +static bool CompareTensors(const std::vector &vTest, const std::vector &vGold, size_t columns, + size_t rows, size_t batch, size_t rowStride, size_t channels, size_t sampleStride, + float delta) { for (size_t i = 0; i < batch; i++) { - uint8_t *pTest = vTest.data() + i * sampleStride; - uint8_t *pGold = vGold.data() + i * sampleStride; + const uint8_t *pTest = vTest.data() + i * sampleStride; + const uint8_t *pGold = vGold.data() + i * sampleStride; if (!CompareImages(pTest, pGold, columns, rows, rowStride, channels, delta)) return false; } return true; } -static bool CompareVarShapes(std::vector> &vTest, std::vector> &vGold, - std::vector &vColumns, std::vector &vRows, std::vector &vRowStride, - std::vector &vChannels, float delta) +static bool CompareVarShapes(const std::vector> &vTest, + const std::vector> &vGold, const std::vector &vColumns, + const std::vector &vRows, const std::vector &vRowStride, + const std::vector &vChannels, float delta) { for (size_t i = 0; i < vTest.size(); i++) { @@ -85,55 +91,86 @@ static bool CompareVarShapes(std::vector> &vTest, std::vect return true; } -static void CPUBilateralFilter(uint8_t *pIn, uint8_t *pOut, int columns, int rows, int rowStride, int channels, - int radius, float colorCoefficient, float spaceCoefficient) +static float ReadPixel(const uint8_t *pIn, TensorDim x, TensorDim y, int c, TensorDim columns, TensorDim rows, + int rowStride, int channels) +{ + return ((x >= 0) && (x < columns) && (y >= 0) && (y < rows)) + ? static_cast(pIn[y * rowStride + x * channels + c]) + : 0.0f; +} + +static std::vector ReadChannels(const uint8_t *pIn, TensorDim x, TensorDim y, TensorDim columns, TensorDim rows, + int rowStride, int channels) +{ + std::vector values(channels); + + for (int c = 0; c < channels; ++c) + { + values[c] = ReadPixel(pIn, x, y, c, columns, rows, rowStride, channels); + } + + return values; +} + +static void AccumulateBilateralSample(std::vector &numerators, float &denominator, + const std::vector ¢ers, const uint8_t *pIn, TensorDim x, TensorDim y, + TensorDim columns, TensorDim rows, int rowStride, int channels, + float distanceSquared, float colorCoefficient, float spaceCoefficient) +{ + std::vector pixels = ReadChannels(pIn, x, y, columns, rows, rowStride, channels); + float eColor = 0.0f; + + for (int c = 0; c < channels; ++c) + { + eColor += std::abs(pixels[c] - centers[c]); + } + + float weight = std::exp(distanceSquared * spaceCoefficient + eColor * eColor * colorCoefficient); + denominator += weight; + + for (int c = 0; c < channels; ++c) + { + numerators[c] += weight * pixels[c]; + } +} + +static void AccumulateBilateralWindow(std::vector &numerators, float &denominator, + const std::vector ¢ers, const uint8_t *pIn, TensorDim column, + TensorDim row, TensorDim columns, TensorDim rows, int rowStride, int channels, + int radius, float radiusSquared, float colorCoefficient, float spaceCoefficient) { - float radiusSquared = radius * radius; - for (int j = 0; j < rows; j++) + for (TensorDim y = row - radius; y <= row + radius; y++) { - for (int k = 0; k < columns; k++) + for (TensorDim x = column - radius; x <= column + radius; x++) { - std::vector numerators(channels, 0.0f); - float denominator = 0.0f; - std::vector centers{static_cast(pIn[j * rowStride + k * channels]), - channels > 1 ? static_cast(pIn[j * rowStride + k * channels + 1]) : 0, - channels > 2 ? static_cast(pIn[j * rowStride + k * channels + 2]) : 0, - channels > 3 ? static_cast(pIn[j * rowStride + k * channels + 3]) : 0}; + auto distanceSquared = static_cast((column - x) * (column - x) + (row - y) * (row - y)); - for (int y = j - radius; y <= j + radius; y++) + if (distanceSquared > radiusSquared) { - for (int x = k - radius; x <= k + radius; x++) - { - float distanceSquared = (k - x) * (k - x) + (j - y) * (j - y); - if (distanceSquared <= radiusSquared) - { - std::vector pixels; - for (auto c = 0; c < channels; ++c) - { - float pixel = ((x >= 0) && (x < columns) && (y >= 0) && (y < rows)) - ? static_cast(pIn[y * rowStride + x * channels + c]) - : 0.0f; - pixels.emplace_back(pixel); - } - float e_space = distanceSquared * spaceCoefficient; - float e_color = 0.0f; - - for (auto c = 0; c < channels; ++c) - { - e_color += std::abs(pixels[c] - centers[c]); - } - e_color = e_color * e_color * colorCoefficient; - - float weight = std::exp(e_space + e_color); - denominator += weight; - for (auto c = 0; c < channels; ++c) - { - numerators[c] += weight * pixels[c]; - } - } - } + continue; } + AccumulateBilateralSample(numerators, denominator, centers, pIn, x, y, columns, rows, rowStride, channels, + distanceSquared, colorCoefficient, spaceCoefficient); + } + } +} + +static void CPUBilateralFilter(const uint8_t *pIn, uint8_t *pOut, TensorDim columns, TensorDim rows, int rowStride, + int channels, int radius, float colorCoefficient, float spaceCoefficient) +{ + auto radiusSquared = static_cast(radius * radius); + for (TensorDim j = 0; j < rows; j++) + { + for (TensorDim k = 0; k < columns; k++) + { + std::vector numerators(channels, 0.0f); + float denominator = 0.0f; + std::vector centers = ReadChannels(pIn, k, j, columns, rows, rowStride, channels); + + AccumulateBilateralWindow(numerators, denominator, centers, pIn, k, j, columns, rows, rowStride, channels, + radius, radiusSquared, colorCoefficient, spaceCoefficient); + for (auto c = 0; c < channels; ++c) { pOut[j * rowStride + k * channels + c] = saturate_cast(numerators[c] / denominator); @@ -142,9 +179,9 @@ static void CPUBilateralFilter(uint8_t *pIn, uint8_t *pOut, int columns, int row } } -static void CPUBilateralFilterTensor(std::vector &vIn, std::vector &vOut, int columns, int rows, - int batch, int rowStride, int channels, int sampleStride, int diameter, - float sigmaColor, float sigmaSpace) +static void CPUBilateralFilterTensor(std::vector &vIn, std::vector &vOut, TensorDim columns, + TensorDim rows, TensorDim batch, int rowStride, int channels, int sampleStride, + int diameter, float sigmaColor, float sigmaSpace) { if (sigmaColor <= 0) { @@ -158,7 +195,7 @@ static void CPUBilateralFilterTensor(std::vector &vIn, std::vector(std::roundf(sigmaSpace * 1.5f)); } else { @@ -169,12 +206,12 @@ static void CPUBilateralFilterTensor(std::vector &vIn, std::vector> &vIn, s int radius; if (diameter <= 0) { - radius = std::roundf(sigmaSpace * 1.5f); + radius = static_cast(std::roundf(sigmaSpace * 1.5f)); } else { @@ -214,8 +251,8 @@ static void CPUBilateralFilterVarShape(std::vector> &vIn, s radius = 1; } - float spaceCoefficient = -1 / (2 * sigmaSpace * sigmaSpace); - float colorCoefficient = -1 / (2 * sigmaColor * sigmaColor); + float spaceCoefficient = -1.f / (2.f * sigmaSpace * sigmaSpace); + float colorCoefficient = -1.f / (2.f * sigmaColor * sigmaColor); CPUBilateralFilter(vIn[i].data(), vOut[i].data(), vColumns[i], vRows[i], vRowStride[i], vChannels[i], radius, colorCoefficient, spaceCoefficient); } @@ -261,7 +298,7 @@ TEST_P(OpBilateralFilter, BilateralFilter_packed) int numberOfImages = GetParamValue<5>(); std::vector fmts{nvcv::FMT_U8, nvcv::ImageFormat{NVCV_IMAGE_FORMAT_2U8}, nvcv::FMT_RGB8, nvcv::FMT_RGBA8}; - for (nvcv::ImageFormat fmt : fmts) + for (nvcv::ImageFormat fmt : fmts) // NOSONAR { nvcv::Tensor imgOut = nvcv::util::CreateTensor(numberOfImages, width, height, fmt); nvcv::Tensor imgIn = nvcv::util::CreateTensor(numberOfImages, width, height, fmt); @@ -279,11 +316,11 @@ TEST_P(OpBilateralFilter, BilateralFilter_packed) auto outAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*outData); ASSERT_TRUE(outAccess); - int inSampleStride = inAccess->numRows() * inAccess->rowStride(); - int outSampleStride = outAccess->numRows() * outAccess->rowStride(); + auto inSampleStride = static_cast(inAccess->numRows() * inAccess->rowStride()); + auto outSampleStride = static_cast(outAccess->numRows() * outAccess->rowStride()); - int inBufSize = inSampleStride * inAccess->numSamples(); - int outBufSize = outSampleStride * outAccess->numSamples(); + int inBufSize = inSampleStride * static_cast(inAccess->numSamples()); + int outBufSize = outSampleStride * static_cast(outAccess->numSamples()); std::vector vIn(inBufSize); std::vector vOut(outBufSize); @@ -293,8 +330,9 @@ TEST_P(OpBilateralFilter, BilateralFilter_packed) for (int i = 0; i < inBufSize; i++) inGold[i] = i % 113; // Use prime number to prevent weird tiling patterns EXPECT_EQ(cudaSuccess, cudaMemcpy(inData->basePtr(), inGold.data(), inBufSize, cudaMemcpyHostToDevice)); + const int rowStride{static_cast(inAccess->rowStride())}; CPUBilateralFilterTensor(inGold, outGold, inAccess->numCols(), inAccess->numRows(), inAccess->numSamples(), - inAccess->rowStride(), channels, inSampleStride, d, sigmaColor, sigmaSpace); + rowStride, channels, inSampleStride, d, sigmaColor, sigmaSpace); // run operator cvcuda::BilateralFilter bilateralFilterOp; @@ -306,13 +344,15 @@ TEST_P(OpBilateralFilter, BilateralFilter_packed) EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); EXPECT_EQ(cudaSuccess, cudaMemcpy(outTest.data(), outData->basePtr(), outBufSize, cudaMemcpyDeviceToHost)); - ASSERT_TRUE(CompareTensors(outTest, outGold, inAccess->numCols(), inAccess->numRows(), inAccess->numSamples(), - inAccess->rowStride(), channels, inSampleStride, 0.9f)); + ASSERT_TRUE(CompareTensors( + outTest, outGold, static_cast(inAccess->numCols()), static_cast(inAccess->numRows()), + static_cast(inAccess->numSamples()), static_cast(inAccess->rowStride()), + static_cast(channels), static_cast(inSampleStride), 0.9f)); } EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); } -TEST_P(OpBilateralFilter, BilateralFilter_VarShape) +TEST_P(OpBilateralFilter, varshape_correct_output) { cudaStream_t stream; EXPECT_EQ(cudaSuccess, cudaStreamCreate(&stream)); @@ -324,12 +364,14 @@ TEST_P(OpBilateralFilter, BilateralFilter_VarShape) int numberOfImages = GetParamValue<5>(); std::vector fmts{nvcv::FMT_U8, nvcv::ImageFormat{NVCV_IMAGE_FORMAT_2U8}, nvcv::FMT_RGB8, nvcv::FMT_RGBA8}; - for (nvcv::ImageFormat fmt : fmts) + for (nvcv::ImageFormat fmt : fmts) // NOSONAR { // Create input varshape - std::default_random_engine rng; - std::uniform_int_distribution udistWidth(width * 0.8, width * 1.1); - std::uniform_int_distribution udistHeight(height * 0.8, height * 1.1); + std::default_random_engine rng; + std::uniform_int_distribution udistWidth(static_cast(static_cast(width) * 0.8), + static_cast(static_cast(width) * 1.1)); + std::uniform_int_distribution udistHeight(static_cast(static_cast(height) * 0.8), + static_cast(static_cast(height) * 1.1)); std::vector imgSrc; @@ -353,9 +395,9 @@ TEST_P(OpBilateralFilter, BilateralFilter_VarShape) srcVec[i].resize(imgSrc[i].size().h * srcRowStride); goldVec[i].resize(imgSrc[i].size().h * srcRowStride); dstVec[i].resize(imgSrc[i].size().h * srcRowStride); - std::generate(srcVec[i].begin(), srcVec[i].end(), [&]() { return udist(rng); }); - std::generate(goldVec[i].begin(), goldVec[i].end(), [&]() { return 0; }); - std::generate(dstVec[i].begin(), dstVec[i].end(), [&]() { return 0; }); + std::ranges::generate(srcVec[i], [&udist, &rng]() { return udist(rng); }); + std::ranges::generate(goldVec[i], []() { return 0; }); + std::ranges::generate(dstVec[i], []() { return 0; }); auto imgData = imgSrc[i].exportData(); ASSERT_NE(imgData, nvcv::NullOpt); @@ -438,34 +480,576 @@ TEST_P(OpBilateralFilter, BilateralFilter_VarShape) EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); } +static void FillBilateralParamTensor(cudaStream_t stream, nvcv::Tensor &tensor, const std::vector &values) +{ + auto dev = tensor.exportData(); + ASSERT_NE(dev, nullptr); + ASSERT_EQ(cudaSuccess, cudaMemcpyAsync(dev->basePtr(), values.data(), values.size() * sizeof(int), + cudaMemcpyHostToDevice, stream)); +} + +static void FillBilateralParamTensor(cudaStream_t stream, nvcv::Tensor &tensor, const std::vector &values) +{ + auto dev = tensor.exportData(); + ASSERT_NE(dev, nullptr); + ASSERT_EQ(cudaSuccess, cudaMemcpyAsync(dev->basePtr(), values.data(), values.size() * sizeof(float), + cudaMemcpyHostToDevice, stream)); +} + +static void RunBilateralTensorPlanarParity(nvcv::ImageFormat interleavedFmt, nvcv::ImageFormat planarFmt, + NVCVBorderType border) +{ + cvcuda::BilateralFilter op; + nvcv::test::planar::RunTensorParity( + planarFmt, interleavedFmt, 33, 25, 33, 25, 2, + [&op, border](cudaStream_t stream, const nvcv::Tensor &src, const nvcv::Tensor &dst, nvcv::ImageFormat) + { op(stream, src, dst, 5, 15.f, 3.f, border); }); +} + +static void RunBilateralVarShapePlanarParity(nvcv::ImageFormat interleavedFmt, nvcv::ImageFormat planarFmt, + NVCVBorderType border) +{ + cvcuda::BilateralFilter op; + nvcv::test::planar::RunVarShapeParity( + planarFmt, interleavedFmt, 31, 23, 31, 23, 2, + [&op, border](cudaStream_t stream, const nvcv::ImageBatchVarShape &src, const nvcv::ImageBatchVarShape &dst, + nvcv::ImageFormat) + { + const int numImages = src.numImages(); + + nvcv::Tensor diameter({{numImages}, "N"}, nvcv::TYPE_S32); + nvcv::Tensor sigmaColor({{numImages}, "N"}, nvcv::TYPE_F32); + nvcv::Tensor sigmaSpace({{numImages}, "N"}, nvcv::TYPE_F32); + + FillBilateralParamTensor(stream, diameter, std::vector(numImages, 5)); + FillBilateralParamTensor(stream, sigmaColor, std::vector(numImages, 15.f)); + FillBilateralParamTensor(stream, sigmaSpace, std::vector(numImages, 3.f)); + + op(stream, src, dst, diameter, sigmaColor, sigmaSpace, border); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + }); +} + +static std::vector MakeFiniteFloatHwc(int width, int height, int channels, int seed) +{ + std::vector values(width * height * channels); + for (size_t i = 0; i < values.size(); ++i) + { + values[i] = static_cast((static_cast(i) * 17 + seed) % 113) / 11.f; + } + + std::vector bytes(values.size() * sizeof(float)); + std::memcpy(bytes.data(), values.data(), bytes.size()); + return bytes; +} + +static float ReadFloatHwc(const std::vector &src, int width, int height, int channels, int x, int y, int c) +{ + return ((x >= 0) && (x < width) && (y >= 0) && (y < height)) ? src[(y * width + x) * channels + c] : 0.f; +} + +static std::vector ReadFloatHwcPixel(const std::vector &src, int width, int height, int channels, int x, + int y) +{ + std::vector pixel(channels); + for (int c = 0; c < channels; ++c) + { + pixel[c] = ReadFloatHwc(src, width, height, channels, x, y, c); + } + return pixel; +} + +static void AccumulateBilateralFloatHwcSample(std::vector &numerator, float &denominator, + const std::vector &src, const std::vector ¢er, + int width, int height, int channels, int x, int y, int sampleX, + int sampleY, float radiusSquared, float spaceCoefficient, + float colorCoefficient) +{ + const auto dx = x - sampleX; + const auto dy = y - sampleY; + const auto distanceSquared = static_cast(dx * dx + dy * dy); + if (distanceSquared > radiusSquared) + { + return; + } + + float oneNorm = 0.f; + for (int c = 0; c < channels; ++c) + { + oneNorm += std::abs(ReadFloatHwc(src, width, height, channels, sampleX, sampleY, c) - center[c]); + } + + const float weight = std::exp(distanceSquared * spaceCoefficient + oneNorm * oneNorm * colorCoefficient); + denominator += weight; + for (int c = 0; c < channels; ++c) + { + numerator[c] += weight * ReadFloatHwc(src, width, height, channels, sampleX, sampleY, c); + } +} + +static void WriteBilateralFloatHwcPixel(std::vector &dst, const std::vector &src, int width, int height, + int channels, int x, int y, int radius, float radiusSquared, + float spaceCoefficient, float colorCoefficient) +{ + const auto center = ReadFloatHwcPixel(src, width, height, channels, x, y); + auto numerator = std::vector(channels, 0.f); + float denominator = 0.f; + + for (int sampleY = y - radius; sampleY <= y + radius; ++sampleY) + { + for (int sampleX = x - radius; sampleX <= x + radius; ++sampleX) + { + AccumulateBilateralFloatHwcSample(numerator, denominator, src, center, width, height, channels, x, y, + sampleX, sampleY, radiusSquared, spaceCoefficient, colorCoefficient); + } + } + + for (int c = 0; c < channels; ++c) + { + dst[(y * width + x) * channels + c] = numerator[c] / denominator; + } +} + +static std::vector CPUBilateralFilterFloatHwc(const std::vector &input, int width, int height, + int channels, int diameter, float sigmaColor, float sigmaSpace) +{ + std::vector src(width * height * channels); + std::memcpy(src.data(), input.data(), input.size()); + + if (sigmaColor <= 0) + { + sigmaColor = 1; + } + if (sigmaSpace <= 0) + { + sigmaSpace = 1; + } + + int radius = diameter <= 0 ? static_cast(std::roundf(sigmaSpace * 1.5f)) : diameter / 2; + if (radius < 1) + { + radius = 1; + } + + const auto radiusSquared = static_cast(radius * radius); + const float spaceCoefficient = -1.f / (2.f * sigmaSpace * sigmaSpace); + const float colorCoefficient = -1.f / (2.f * sigmaColor * sigmaColor); + + std::vector dst(src.size()); + for (int y = 0; y < height; ++y) + { + for (int x = 0; x < width; ++x) + { + WriteBilateralFloatHwcPixel(dst, src, width, height, channels, x, y, radius, radiusSquared, + spaceCoefficient, colorCoefficient); + } + } + + std::vector output(dst.size() * sizeof(float)); + std::memcpy(output.data(), dst.data(), output.size()); + return output; +} + +static void ExpectFloatHwcNear(const std::vector &test, const std::vector &gold, + float tolerance = 1e-5f) +{ + ASSERT_EQ(test.size(), gold.size()); + ASSERT_EQ(test.size() % sizeof(float), 0); + const auto numValues = test.size() / sizeof(float); + + for (size_t i = 0; i < numValues; ++i) + { + float testValue; + float goldValue; + std::memcpy(&testValue, test.data() + i * sizeof(float), sizeof(float)); + std::memcpy(&goldValue, gold.data() + i * sizeof(float), sizeof(float)); + + ASSERT_TRUE(std::isfinite(testValue)); + ASSERT_TRUE(std::isfinite(goldValue)); + EXPECT_NEAR(testValue, goldValue, tolerance) << "at float index " << i; + } +} + +static void RunBilateralPackedTensorFloatReference(nvcv::ImageFormat fmt) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + constexpr int width = 33; + constexpr int height = 25; + constexpr int numImages = 2; + + const int channels = fmt.numChannels(); + const int pixelStride = fmt.planePixelStrideBytes(0); + const int rowStride = width * pixelStride; + ASSERT_EQ(pixelStride, channels * static_cast(sizeof(float))); + + nvcv::Tensor src = nvcv::util::CreateTensor(numImages, width, height, fmt); + nvcv::Tensor dst = nvcv::util::CreateTensor(numImages, width, height, fmt); + + auto srcData = src.exportData(); + auto dstData = dst.exportData(); + ASSERT_TRUE(srcData && dstData); + + auto srcAcc = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcData); + auto dstAcc = nvcv::TensorDataAccessStridedImagePlanar::Create(*dstData); + ASSERT_TRUE(srcAcc && dstAcc); + + std::vector> gold(numImages); + for (int i = 0; i < numImages; ++i) + { + auto hwc = MakeFiniteFloatHwc(width, height, channels, i * 101 + 31); + gold[i] = CPUBilateralFilterFloatHwc(hwc, width, height, channels, 5, 15.f, 3.f); + nvcv::test::planar::UploadInterleavedSample(*srcAcc, i, hwc, width, height, rowStride); + } + + cvcuda::BilateralFilter op; + op(stream, src, dst, 5, 15.f, 3.f, NVCV_BORDER_CONSTANT); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + for (int i = 0; i < numImages; ++i) + { + SCOPED_TRACE(i); + auto gpu = nvcv::test::planar::DownloadInterleavedSample(*dstAcc, i, width, height, rowStride); + ExpectFloatHwcNear(gpu, gold[i], 1e-4f); + } + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +static void RunBilateralPackedVarShapeFloatReference(nvcv::ImageFormat fmt) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + const std::vector sizes{ + {31, 23}, + {35, 25} + }; + const auto numImages = static_cast(sizes.size()); + const int channels = fmt.numChannels(); + const int pixelStride = fmt.planePixelStrideBytes(0); + ASSERT_EQ(pixelStride, channels * static_cast(sizeof(float))); + + std::vector src; + std::vector dst; + std::vector> gold(numImages); + for (int i = 0; i < numImages; ++i) + { + src.emplace_back(sizes[i], fmt); + dst.emplace_back(sizes[i], fmt); + } + + for (int i = 0; i < numImages; ++i) + { + const int width = sizes[i].w; + const int height = sizes[i].h; + const int rowStride = width * pixelStride; + + auto hwc = MakeFiniteFloatHwc(width, height, channels, i * 101 + 37); + gold[i] = CPUBilateralFilterFloatHwc(hwc, width, height, channels, 5, 15.f, 3.f); + + auto data = src[i].exportData(); + ASSERT_NE(data, nvcv::NullOpt); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(data->plane(0).basePtr, data->plane(0).rowStride, hwc.data(), rowStride, + rowStride, height, cudaMemcpyHostToDevice)); + } + + nvcv::ImageBatchVarShape batchSrc(numImages); + nvcv::ImageBatchVarShape batchDst(numImages); + batchSrc.pushBack(src.begin(), src.end()); + batchDst.pushBack(dst.begin(), dst.end()); + + nvcv::Tensor diameter({{numImages}, "N"}, nvcv::TYPE_S32); + nvcv::Tensor sigmaColor({{numImages}, "N"}, nvcv::TYPE_F32); + nvcv::Tensor sigmaSpace({{numImages}, "N"}, nvcv::TYPE_F32); + FillBilateralParamTensor(stream, diameter, std::vector(numImages, 5)); + FillBilateralParamTensor(stream, sigmaColor, std::vector(numImages, 15.f)); + FillBilateralParamTensor(stream, sigmaSpace, std::vector(numImages, 3.f)); + + cvcuda::BilateralFilter op; + op(stream, batchSrc, batchDst, diameter, sigmaColor, sigmaSpace, NVCV_BORDER_CONSTANT); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + for (int i = 0; i < numImages; ++i) + { + SCOPED_TRACE(i); + const int width = sizes[i].w; + const int height = sizes[i].h; + const int rowStride = width * pixelStride; + + std::vector gpu(height * rowStride); + auto data = dst[i].exportData(); + ASSERT_NE(data, nvcv::NullOpt); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(gpu.data(), rowStride, data->plane(0).basePtr, data->plane(0).rowStride, + rowStride, height, cudaMemcpyDeviceToHost)); + ExpectFloatHwcNear(gpu, gold[i], 1e-4f); + } + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +static void RunBilateralTensorPlanarFloatParity(nvcv::ImageFormat interleavedFmt, nvcv::ImageFormat planarFmt, + NVCVBorderType border) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + constexpr int width = 33; + constexpr int height = 25; + constexpr int numImages = 2; + + const int channels = planarFmt.numChannels(); + const int elemSize = planarFmt.planePixelStrideBytes(0); + const int rowStride = width * channels * elemSize; + ASSERT_EQ(elemSize, static_cast(sizeof(float))); + + nvcv::Tensor srcI = nvcv::util::CreateTensor(numImages, width, height, interleavedFmt); + nvcv::Tensor dstI = nvcv::util::CreateTensor(numImages, width, height, interleavedFmt); + nvcv::Tensor srcP = nvcv::util::CreateTensor(numImages, width, height, planarFmt); + nvcv::Tensor dstP = nvcv::util::CreateTensor(numImages, width, height, planarFmt); + + auto srcIData = srcI.exportData(); + auto dstIData = dstI.exportData(); + auto srcPData = srcP.exportData(); + auto dstPData = dstP.exportData(); + ASSERT_TRUE(srcIData && dstIData && srcPData && dstPData); + + auto srcIAcc = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcIData); + auto dstIAcc = nvcv::TensorDataAccessStridedImagePlanar::Create(*dstIData); + auto srcPAcc = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcPData); + auto dstPAcc = nvcv::TensorDataAccessStridedImagePlanar::Create(*dstPData); + ASSERT_TRUE(srcIAcc && dstIAcc && srcPAcc && dstPAcc); + + for (int i = 0; i < numImages; ++i) + { + auto hwc = MakeFiniteFloatHwc(width, height, channels, i * 101 + 13); + nvcv::test::planar::UploadInterleavedSample(*srcIAcc, i, hwc, width, height, rowStride); + nvcv::test::planar::UploadPlanarSample( + *srcPAcc, i, nvcv::test::planar::DeinterleaveToPlanes(hwc, width, height, channels, elemSize), width, + height, channels, elemSize); + } + + cvcuda::BilateralFilter op; + op(stream, srcI, dstI, 5, 15.f, 3.f, border); + op(stream, srcP, dstP, 5, 15.f, 3.f, border); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + for (int i = 0; i < numImages; ++i) + { + SCOPED_TRACE(i); + auto gpuInter = nvcv::test::planar::DownloadInterleavedSample(*dstIAcc, i, width, height, rowStride); + auto planesOut = nvcv::test::planar::DownloadPlanarSample(*dstPAcc, i, width, height, channels, elemSize); + auto planarInter = nvcv::test::planar::InterleaveFromPlanes(planesOut, width, height, channels, elemSize); + ExpectFloatHwcNear(planarInter, gpuInter); + } + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +static void RunBilateralVarShapePlanarFloatParity(nvcv::ImageFormat interleavedFmt, nvcv::ImageFormat planarFmt, + NVCVBorderType border) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + constexpr int width = 31; + constexpr int height = 23; + constexpr int numImages = 2; + + const int channels = planarFmt.numChannels(); + const int elemSize = planarFmt.planePixelStrideBytes(0); + const int rowStride = width * channels * elemSize; + ASSERT_EQ(elemSize, static_cast(sizeof(float))); + + std::vector srcI; + std::vector dstI; + std::vector srcP; + std::vector dstP; + for (int i = 0; i < numImages; ++i) + { + srcI.emplace_back(nvcv::Size2D{width, height}, interleavedFmt); + dstI.emplace_back(nvcv::Size2D{width, height}, interleavedFmt); + srcP.emplace_back(nvcv::Size2D{width, height}, planarFmt); + dstP.emplace_back(nvcv::Size2D{width, height}, planarFmt); + } + + nvcv::ImageBatchVarShape batchSrcI(numImages); + nvcv::ImageBatchVarShape batchDstI(numImages); + nvcv::ImageBatchVarShape batchSrcP(numImages); + nvcv::ImageBatchVarShape batchDstP(numImages); + batchSrcI.pushBack(srcI.begin(), srcI.end()); + batchDstI.pushBack(dstI.begin(), dstI.end()); + batchSrcP.pushBack(srcP.begin(), srcP.end()); + batchDstP.pushBack(dstP.begin(), dstP.end()); + + for (int i = 0; i < numImages; ++i) + { + auto hwc = MakeFiniteFloatHwc(width, height, channels, i * 101 + 17); + + auto idata = srcI[i].exportData(); + ASSERT_NE(idata, nvcv::NullOpt); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(idata->plane(0).basePtr, idata->plane(0).rowStride, hwc.data(), rowStride, + rowStride, height, cudaMemcpyHostToDevice)); + + auto planes = nvcv::test::planar::DeinterleaveToPlanes(hwc, width, height, channels, elemSize); + auto pdata = srcP[i].exportData(); + const int planeBytes = width * height * elemSize; + ASSERT_NE(pdata, nvcv::NullOpt); + ASSERT_EQ(pdata->numPlanes(), channels); + for (int c = 0; c < channels; ++c) + { + ASSERT_EQ(cudaSuccess, + cudaMemcpy2D(pdata->plane(c).basePtr, pdata->plane(c).rowStride, planes.data() + c * planeBytes, + width * elemSize, width * elemSize, height, cudaMemcpyHostToDevice)); + } + } + + nvcv::Tensor diameter({{numImages}, "N"}, nvcv::TYPE_S32); + nvcv::Tensor sigmaColor({{numImages}, "N"}, nvcv::TYPE_F32); + nvcv::Tensor sigmaSpace({{numImages}, "N"}, nvcv::TYPE_F32); + FillBilateralParamTensor(stream, diameter, std::vector(numImages, 5)); + FillBilateralParamTensor(stream, sigmaColor, std::vector(numImages, 15.f)); + FillBilateralParamTensor(stream, sigmaSpace, std::vector(numImages, 3.f)); + + cvcuda::BilateralFilter op; + op(stream, batchSrcI, batchDstI, diameter, sigmaColor, sigmaSpace, border); + op(stream, batchSrcP, batchDstP, diameter, sigmaColor, sigmaSpace, border); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + const int planeBytes = width * height * elemSize; + for (int i = 0; i < numImages; ++i) + { + SCOPED_TRACE(i); + std::vector gpuInter(height * rowStride); + auto idata = dstI[i].exportData(); + ASSERT_NE(idata, nvcv::NullOpt); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(gpuInter.data(), rowStride, idata->plane(0).basePtr, + idata->plane(0).rowStride, rowStride, height, cudaMemcpyDeviceToHost)); + + std::vector planesOut(width * height * channels * elemSize); + auto pdata = dstP[i].exportData(); + ASSERT_NE(pdata, nvcv::NullOpt); + for (int c = 0; c < channels; ++c) + { + ASSERT_EQ(cudaSuccess, + cudaMemcpy2D(planesOut.data() + c * planeBytes, width * elemSize, pdata->plane(c).basePtr, + pdata->plane(c).rowStride, width * elemSize, height, cudaMemcpyDeviceToHost)); + } + auto planarInter = nvcv::test::planar::InterleaveFromPlanes(planesOut, width, height, channels, elemSize); + ExpectFloatHwcNear(planarInter, gpuInter); + } + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpBilateralFilterPackedFloat, tensor_rgbf32_matches_cpu_reference) +{ + RunBilateralPackedTensorFloatReference(nvcv::FMT_RGBf32); +} + +TEST(OpBilateralFilterPackedFloat, tensor_rgbaf32_matches_cpu_reference) +{ + RunBilateralPackedTensorFloatReference(nvcv::FMT_RGBAf32); +} + +TEST(OpBilateralFilterPackedFloat, varshape_rgbf32_matches_cpu_reference) +{ + RunBilateralPackedVarShapeFloatReference(nvcv::FMT_RGBf32); +} + +TEST(OpBilateralFilterPackedFloat, varshape_rgbaf32_matches_cpu_reference) +{ + RunBilateralPackedVarShapeFloatReference(nvcv::FMT_RGBAf32); +} + +TEST(OpBilateralFilterPlanar, tensor_rgb8_matches_interleaved) +{ + RunBilateralTensorPlanarParity(nvcv::FMT_RGB8, nvcv::FMT_RGB8p, NVCV_BORDER_REFLECT); +} + +TEST(OpBilateralFilterPlanar, tensor_rgba8_matches_interleaved) +{ + RunBilateralTensorPlanarParity(nvcv::FMT_RGBA8, nvcv::FMT_RGBA8p, NVCV_BORDER_CONSTANT); +} + +TEST(OpBilateralFilterPlanar, tensor_rgbf32_matches_interleaved) +{ + RunBilateralTensorPlanarFloatParity(nvcv::FMT_RGBf32, nvcv::FMT_RGBf32p, NVCV_BORDER_REFLECT); +} + +TEST(OpBilateralFilterPlanar, tensor_rgbaf32_matches_interleaved) +{ + RunBilateralTensorPlanarFloatParity(nvcv::FMT_RGBAf32, nvcv::FMT_RGBAf32p, NVCV_BORDER_CONSTANT); +} + +TEST(OpBilateralFilterPlanar, varshape_rgb8_matches_interleaved) +{ + RunBilateralVarShapePlanarParity(nvcv::FMT_RGB8, nvcv::FMT_RGB8p, NVCV_BORDER_REFLECT); +} + +TEST(OpBilateralFilterPlanar, varshape_rgba8_matches_interleaved) +{ + RunBilateralVarShapePlanarParity(nvcv::FMT_RGBA8, nvcv::FMT_RGBA8p, NVCV_BORDER_CONSTANT); +} + +TEST(OpBilateralFilterPlanar, varshape_rgbf32_matches_interleaved) +{ + RunBilateralVarShapePlanarFloatParity(nvcv::FMT_RGBf32, nvcv::FMT_RGBf32p, NVCV_BORDER_REFLECT); +} + +TEST(OpBilateralFilterPlanar, varshape_rgbaf32_matches_interleaved) +{ + RunBilateralVarShapePlanarFloatParity(nvcv::FMT_RGBAf32, nvcv::FMT_RGBAf32p, NVCV_BORDER_CONSTANT); +} + #undef NVCV_IMAGE_FORMAT_2U8 -// clang-format off -NVCV_TEST_SUITE_P(OpBilateralFilterVarshape_Negative, test::ValueList{ - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U16, NVCV_BORDER_CONSTANT, nvcv::TYPE_S32, nvcv::TYPE_F32, nvcv::TYPE_F32, 5, 5}, // in/out image format not same - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_RGB8p, nvcv::FMT_RGB8, NVCV_BORDER_CONSTANT, nvcv::TYPE_S32, nvcv::TYPE_F32, nvcv::TYPE_F32, 5, 5}, // in/out data format not same - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_RGB8p, nvcv::FMT_RGB8p, NVCV_BORDER_CONSTANT, nvcv::TYPE_S32, nvcv::TYPE_F32, nvcv::TYPE_F32, 5, 5}, // input not kHWC/kNHWC +static auto OpBilateralFilterVarshapeNegativeParams() +{ + test::ValueList + params{ + {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U16, NVCV_BORDER_CONSTANT, nvcv::TYPE_S32, + nvcv::TYPE_F32, nvcv::TYPE_F32, 5, 5}, // in/out image format not same + {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_RGB8p, nvcv::FMT_RGB8, NVCV_BORDER_CONSTANT, nvcv::TYPE_S32, + nvcv::TYPE_F32, nvcv::TYPE_F32, 5, 5}, // in/out data format not same + }; #ifndef ENABLE_SANITIZER - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U8, static_cast(255), nvcv::TYPE_S32, nvcv::TYPE_F32, nvcv::TYPE_F32, 5, 5}, // invalid border type + params.emplace_back(NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U8, static_cast(255), + nvcv::TYPE_S32, nvcv::TYPE_F32, nvcv::TYPE_F32, 5, 5); #endif - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_F16, nvcv::FMT_F16, NVCV_BORDER_CONSTANT, nvcv::TYPE_S32, nvcv::TYPE_F32, nvcv::TYPE_F32, 5, 5}, // invalid data type - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U8, NVCV_BORDER_CONSTANT, nvcv::TYPE_F32, nvcv::TYPE_F32, nvcv::TYPE_F32, 5, 5}, // invalid diameter data type - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U8, NVCV_BORDER_CONSTANT, nvcv::TYPE_S32, nvcv::TYPE_S32, nvcv::TYPE_F32, 5, 5}, // invalid sigmaColor data type - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U8, NVCV_BORDER_CONSTANT, nvcv::TYPE_S32, nvcv::TYPE_F32, nvcv::TYPE_S32, 5, 5}, // invalid sigmaSpace data type - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U8, NVCV_BORDER_CONSTANT, nvcv::TYPE_S32, nvcv::TYPE_F32, nvcv::TYPE_F32, 6, 5}, // in/out images number not equal -}); + params.emplace_back(NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_F16, nvcv::FMT_F16, NVCV_BORDER_CONSTANT, nvcv::TYPE_S32, + nvcv::TYPE_F32, nvcv::TYPE_F32, 5, 5); + params.emplace_back(NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U8, NVCV_BORDER_CONSTANT, nvcv::TYPE_F32, + nvcv::TYPE_F32, nvcv::TYPE_F32, 5, 5); + params.emplace_back(NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U8, NVCV_BORDER_CONSTANT, nvcv::TYPE_S32, + nvcv::TYPE_S32, nvcv::TYPE_F32, 5, 5); + params.emplace_back(NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U8, NVCV_BORDER_CONSTANT, nvcv::TYPE_S32, + nvcv::TYPE_F32, nvcv::TYPE_S32, 5, 5); + params.emplace_back(NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U8, NVCV_BORDER_CONSTANT, nvcv::TYPE_S32, + nvcv::TYPE_F32, nvcv::TYPE_F32, 6, 5); + return params; +} + +NVCV_TEST_SUITE_P(OpBilateralFilterVarshape_Negative, OpBilateralFilterVarshapeNegativeParams()); -NVCV_TEST_SUITE_P(OpBilateralFilter_Negative, nvcv::test::ValueList{ - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U16, NVCV_BORDER_CONSTANT}, // in/out image datatype not same - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_RGB8p, nvcv::FMT_RGB8, NVCV_BORDER_CONSTANT}, // in/out data format not same - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_RGB8p, nvcv::FMT_RGB8p, NVCV_BORDER_CONSTANT}, // input not kHWC/kNHWC +static auto OpBilateralFilterNegativeParams() +{ + nvcv::test::ValueList params{ + {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U16, + NVCV_BORDER_CONSTANT}, // in/out image datatype not same + {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_RGB8p, nvcv::FMT_RGB8, + NVCV_BORDER_CONSTANT}, // in/out data format not same + }; #ifndef ENABLE_SANITIZER - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U8, static_cast(255)}, // invalid border type + params.emplace_back(NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U8, static_cast(255)); #endif - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_F16, nvcv::FMT_F16, NVCV_BORDER_CONSTANT}, // invalid data type -}); + params.emplace_back(NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_F16, nvcv::FMT_F16, NVCV_BORDER_CONSTANT); + return params; +} -// clang-format on +NVCV_TEST_SUITE_P(OpBilateralFilter_Negative, OpBilateralFilterNegativeParams()); TEST_P(OpBilateralFilter_Negative, op) { @@ -492,11 +1076,38 @@ TEST_P(OpBilateralFilter_Negative, op) EXPECT_EQ(expectedReturnCode, nvcv::ProtectCall( - [&] { bilateralFilterOp(stream, imgIn, imgOut, diameter, sigmaColor, sigmaSpace, borderType); })); + [&bilateralFilterOp, &stream, &imgIn, &imgOut, &diameter, &sigmaColor, &sigmaSpace, &borderType] + { bilateralFilterOp(stream, imgIn, imgOut, diameter, sigmaColor, sigmaSpace, borderType); })); EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); } +TEST(OpBilateralFilter_Negative, rejects_mismatched_output_shape) +{ + cudaStream_t stream; + EXPECT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + int width = 24; + int height = 24; + int diameter = 4; + float sigmaColor = 5; + float sigmaSpace = 3; + int numberOfImages = 5; + + nvcv::Tensor imgIn = nvcv::util::CreateTensor(numberOfImages, width, height, nvcv::FMT_U8); + nvcv::Tensor imgOut = nvcv::util::CreateTensor(numberOfImages, width + 1, height, nvcv::FMT_U8); + + cvcuda::BilateralFilter bilateralFilterOp; + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall( + [&bilateralFilterOp, &stream, &imgIn, &imgOut, &diameter, &sigmaColor, &sigmaSpace] { + bilateralFilterOp(stream, imgIn, imgOut, diameter, sigmaColor, sigmaSpace, NVCV_BORDER_CONSTANT); + })); + + EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + TEST_P(OpBilateralFilterVarshape_Negative, varshape) { cudaStream_t stream; @@ -519,9 +1130,11 @@ TEST_P(OpBilateralFilterVarshape_Negative, varshape) float sigmaSpace = 3; // Create input varshape - std::default_random_engine rng; - std::uniform_int_distribution udistWidth(width * 0.8, width * 1.1); - std::uniform_int_distribution udistHeight(height * 0.8, height * 1.1); + std::default_random_engine rng; + std::uniform_int_distribution udistWidth(static_cast(static_cast(width) * 0.8), + static_cast(static_cast(width) * 1.1)); + std::uniform_int_distribution udistHeight(static_cast(static_cast(height) * 0.8), + static_cast(static_cast(height) * 1.1)); std::vector imgSrc; @@ -578,7 +1191,8 @@ TEST_P(OpBilateralFilterVarshape_Negative, varshape) // Run operator cvcuda::BilateralFilter bilateralFilterOp; EXPECT_EQ(expectedReturnCode, nvcv::ProtectCall( - [&] { + [&bilateralFilterOp, &stream, &batchSrc, &batchDst, &diameterTensor, + &sigmaColorTensor, &sigmaSpaceTensor, &borderType] { bilateralFilterOp(stream, batchSrc, batchDst, diameterTensor, sigmaColorTensor, sigmaSpaceTensor, borderType); })); @@ -596,11 +1210,8 @@ TEST(OpBilateralFilterVarshape_Negative, varshape_hasDifferentFormat) { fmt, nvcv::FMT_U8} }; - for (auto testCase : testSet) + for (const auto &[inputFmtExtra, outputFmtExtra] : testSet) { - nvcv::ImageFormat inputFmtExtra = std::get<0>(testCase); - nvcv::ImageFormat outputFmtExtra = std::get<1>(testCase); - cudaStream_t stream; EXPECT_EQ(cudaSuccess, cudaStreamCreate(&stream)); @@ -612,9 +1223,11 @@ TEST(OpBilateralFilterVarshape_Negative, varshape_hasDifferentFormat) int numberOfImages = 5; // Create input varshape - std::default_random_engine rng; - std::uniform_int_distribution udistWidth(width * 0.8, width * 1.1); - std::uniform_int_distribution udistHeight(height * 0.8, height * 1.1); + std::default_random_engine rng; + std::uniform_int_distribution udistWidth(static_cast(static_cast(width) * 0.8), + static_cast(static_cast(width) * 1.1)); + std::uniform_int_distribution udistHeight(static_cast(static_cast(height) * 0.8), + static_cast(static_cast(height) * 1.1)); std::vector imgSrc; @@ -672,7 +1285,8 @@ TEST(OpBilateralFilterVarshape_Negative, varshape_hasDifferentFormat) // Run operator cvcuda::BilateralFilter bilateralFilterOp; EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall( - [&] + [&bilateralFilterOp, &stream, &batchSrc, &batchDst, &diameterTensor, + &sigmaColorTensor, &sigmaSpaceTensor] { bilateralFilterOp(stream, batchSrc, batchDst, diameterTensor, sigmaColorTensor, sigmaSpaceTensor, diff --git a/tests/cvcuda/system/TestOpBndBox.cpp b/tests/cvcuda/system/TestOpBndBox.cpp index 2a2273225..a556ce477 100644 --- a/tests/cvcuda/system/TestOpBndBox.cpp +++ b/tests/cvcuda/system/TestOpBndBox.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,8 +16,7 @@ */ #include "Definitions.hpp" - -#include "OsdUtils.cuh" +#include "PlanarParityUtils.hpp" #include #include @@ -27,99 +26,51 @@ #include #include -#include #include -#include #include namespace gt = ::testing; namespace test = nvcv::test; using namespace cvcuda::priv; -static int randl(int l, int h) +static std::mt19937 &Rng() { - int value = rand() % (h - l + 1); - return l + value; + static std::mt19937 rng; + return rng; } -static void setGoldBuffer(std::vector &vect, nvcv::ImageFormat format, - const nvcv::TensorDataAccessStridedImagePlanar &data, nvcv::Byte *inBuf, - std::shared_ptr bboxes, cudaStream_t stream) +static int randl(int l, int h) { - auto context = cuosd_context_create(); - - for (int n = 0; n < bboxes->batch(); n++) - { - test::osd::Image *image = test::osd::create_image( - data.numCols(), data.numRows(), - format == nvcv::FMT_RGBA8 ? test::osd::ImageFormat::RGBA : test::osd::ImageFormat::RGB); - int bufSize = data.numCols() * data.numRows() * data.numChannels(); - EXPECT_EQ(cudaSuccess, cudaMemcpy(image->data0, inBuf + n * bufSize, bufSize, cudaMemcpyDeviceToDevice)); - - auto numBoxes = bboxes->numBoxesAt(n); - - for (int i = 0; i < numBoxes; i++) - { - auto bbox = bboxes->boxAt(n, i); - - int left = std::max(std::min(bbox.box.x, data.numCols() - 1), 0); - int top = std::max(std::min(bbox.box.y, data.numRows() - 1), 0); - int right = std::max(std::min(left + bbox.box.width - 1, data.numCols() - 1), 0); - int bottom = std::max(std::min(top + bbox.box.height - 1, data.numRows() - 1), 0); - - if (left == right || top == bottom || bbox.box.width <= 0 || bbox.box.height <= 0) - { - continue; - } - - int thickness = bbox.thickness; - - cuOSDColor borderColor = {bbox.borderColor.r, bbox.borderColor.g, bbox.borderColor.b, bbox.borderColor.a}; - cuOSDColor fillColor = {bbox.fillColor.r, bbox.fillColor.g, bbox.fillColor.b, bbox.fillColor.a}; - - cuosd_draw_rectangle(context, left, top, right, bottom, thickness, borderColor, fillColor); - } - - test::osd::cuosd_apply(context, image, stream); - - EXPECT_EQ(cudaSuccess, cudaMemcpy(vect.data() + n * bufSize, image->data0, bufSize, cudaMemcpyDeviceToHost)); - - test::osd::free_image(image); - } - cudaStreamSynchronize(stream); - cuosd_context_destroy(context); + return std::uniform_int_distribution(l, h)(Rng()); } -// run operator -static void runOp(cudaStream_t &stream, cvcuda::BndBox &op, int &inN, int &inW, int &inH, int &num, int &sed, - nvcv::ImageFormat &format) +static void runOp(cudaStream_t &stream, const cvcuda::BndBox &op, int inN, int inW, int inH, int num, int sed, + const nvcv::ImageFormat &format) { std::vector> bndBoxVec; - srand(sed); + Rng().seed(sed); for (int n = 0; n < inN; n++) { std::vector curVec; for (int i = 0; i < num; i++) { NVCVBndBoxI bndBox; - bndBox.box.x = randl(0, inW - 1); - bndBox.box.y = randl(0, inH - 1); - bndBox.box.width = randl(1, inW); - bndBox.box.height = randl(1, inH); - bndBox.thickness = randl(-1, 30); - bndBox.fillColor = {(unsigned char)randl(0, 255), (unsigned char)randl(0, 255), - (unsigned char)randl(0, 255), (unsigned char)randl(0, 255)}; - // Note: borderColor.a must be >= 1 to avoid corner case where 3rd party cuOSD library - // (used for gold generation) has a bug with borderColor.a=0 preventing fill from being drawn + bndBox.box.x = randl(0, inW - 1); + bndBox.box.y = randl(0, inH - 1); + bndBox.box.width = randl(1, inW); + bndBox.box.height = randl(1, inH); + bndBox.thickness = randl(-1, 30); + bndBox.fillColor = {(unsigned char)randl(0, 255), (unsigned char)randl(0, 255), + (unsigned char)randl(0, 255), (unsigned char)randl(0, 255)}; bndBox.borderColor = {(unsigned char)randl(0, 255), (unsigned char)randl(0, 255), - (unsigned char)randl(0, 255), (unsigned char)randl(1, 255)}; // alpha: 1-255 + (unsigned char)randl(0, 255), (unsigned char)randl(1, 255)}; curVec.push_back(bndBox); } bndBoxVec.push_back(curVec); } - std::shared_ptr bndBoxes = std::make_shared(bndBoxVec); + auto bndBoxes = std::make_shared(bndBoxVec); nvcv::Tensor imgIn = nvcv::util::CreateTensor(inN, inW, inH, format); nvcv::Tensor imgOut = nvcv::util::CreateTensor(inN, inW, inH, format); @@ -136,29 +87,229 @@ static void runOp(cudaStream_t &stream, cvcuda::BndBox &op, int &inN, int &inW, auto outAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*output); ASSERT_TRUE(outAccess); - long inSampleStride = inAccess->numRows() * inAccess->rowStride(); - long outSampleStride = outAccess->numRows() * outAccess->rowStride(); + const size_t inSampleStride = static_cast(inAccess->numRows()) * static_cast(inAccess->rowStride()); + const size_t outSampleStride + = static_cast(outAccess->numRows()) * static_cast(outAccess->rowStride()); - int inBufSize = inSampleStride * inAccess->numSamples(); - int outBufSize = outSampleStride * outAccess->numSamples(); + const size_t inBufSize = inSampleStride * static_cast(inAccess->numSamples()); + const size_t outBufSize = outSampleStride * static_cast(outAccess->numSamples()); EXPECT_EQ(cudaSuccess, cudaMemset(input->basePtr(), 0xFF, inSampleStride * inAccess->numSamples())); EXPECT_EQ(cudaSuccess, cudaMemset(output->basePtr(), 0xFF, outSampleStride * outAccess->numSamples())); EXPECT_NO_THROW(op(stream, imgIn, imgOut, (NVCVBndBoxesI)bndBoxes.get())); - // check cdata - std::vector test(outBufSize); - std::vector testIn(inBufSize); + std::vector outHost(outBufSize); + std::vector inHost(inBufSize, 0xFF); EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); - EXPECT_EQ(cudaSuccess, cudaMemcpy(testIn.data(), input->basePtr(), inBufSize, cudaMemcpyDeviceToHost)); - EXPECT_EQ(cudaSuccess, cudaMemcpy(test.data(), output->basePtr(), outBufSize, cudaMemcpyDeviceToHost)); + EXPECT_EQ(cudaSuccess, cudaMemcpy(outHost.data(), output->basePtr(), outBufSize, cudaMemcpyDeviceToHost)); + + EXPECT_NE(inHost, outHost) << "Output should differ from input after drawing bounding boxes"; +} + +static size_t hostPixelOffset(const nvcv::TensorDataAccessStridedImagePlanar &access, int64_t sampleStride, int sample, + int y, int x, int channel) +{ + return static_cast(sample * sampleStride + channel * access.chStride() + y * access.rowStride() + + x * access.colStride()); +} + +static void fillHostPixel(const nvcv::TensorDataAccessStridedImagePlanar &inputAccess, + const nvcv::TensorDataAccessStridedImagePlanar &outputAccess, int64_t inputSampleStride, + int64_t outputSampleStride, int sample, int y, int x, std::vector &inputHost, + std::vector &expectedHost) +{ + for (int channel = 0; channel < inputAccess.numChannels(); ++channel) + { + const auto inputOffset = hostPixelOffset(inputAccess, inputSampleStride, sample, y, x, channel); + const auto outputOffset = hostPixelOffset(outputAccess, outputSampleStride, sample, y, x, channel); + inputHost[inputOffset] = static_cast((sample * 53 + y * 19 + x * 7 + channel * 29) & 0xFF); + expectedHost[outputOffset] = inputHost[inputOffset]; + } +} + +static void fillHostPattern(const nvcv::TensorDataAccessStridedImagePlanar &inputAccess, + const nvcv::TensorDataAccessStridedImagePlanar &outputAccess, int64_t inputSampleStride, + int64_t outputSampleStride, int numSamples, int height, int width, + std::vector &inputHost, std::vector &expectedHost) +{ + for (int sample = 0; sample < numSamples; ++sample) + { + for (int y = 0; y < height; ++y) + { + for (int x = 0; x < width; ++x) + { + fillHostPixel(inputAccess, outputAccess, inputSampleStride, outputSampleStride, sample, y, x, inputHost, + expectedHost); + } + } + } +} + +static bool shouldDrawHostPixel(const NVCVBndBoxI &box, bool filled, int x, int y) +{ + const int left = box.box.x; + const int top = box.box.y; + const int right = left + box.box.width - 1; + const int bottom = top + box.box.height - 1; + return filled ? (x > left && x < right && y > top && y < bottom) + : (x < left + 2 || x > right - 2 || y < top + 2 || y > bottom - 2); +} + +static void drawHostBox(const nvcv::TensorDataAccessStridedImagePlanar &outputAccess, int64_t outputSampleStride, + int sample, const NVCVBndBoxI &box, bool filled, std::vector &expectedHost) +{ + const int left = box.box.x; + const int top = box.box.y; + const int right = left + box.box.width - 1; + const int bottom = top + box.box.height - 1; + const uint8_t color[] = {box.borderColor.r, box.borderColor.g, box.borderColor.b, box.borderColor.a}; + + for (int y = top; y <= bottom; ++y) + { + for (int x = left; x <= right; ++x) + { + if (const bool draw = shouldDrawHostPixel(box, filled, x, y); !draw) + { + continue; + } + for (int channel = 0; channel < outputAccess.numChannels(); ++channel) + { + const auto offset = hostPixelOffset(outputAccess, outputSampleStride, sample, y, x, channel); + expectedHost[offset] = color[channel]; + } + } + } +} + +static void drawHostBoxes(const nvcv::TensorDataAccessStridedImagePlanar &outputAccess, int64_t outputSampleStride, + const std::vector> &boxes, bool filled, + std::vector &expectedHost) +{ + for (int sample = 0; sample < static_cast(boxes.size()); ++sample) + { + for (const auto &box : boxes[sample]) + { + drawHostBox(outputAccess, outputSampleStride, sample, box, filled, expectedHost); + } + } +} + +static void expectHostPixel(const nvcv::TensorDataAccessStridedImagePlanar &inputAccess, + const nvcv::TensorDataAccessStridedImagePlanar &outputAccess, int64_t inputSampleStride, + int64_t outputSampleStride, int sample, int y, int x, const std::vector &inputHost, + const std::vector &inputAfterHost, const std::vector &expectedHost, + const std::vector &outputHost) +{ + for (int channel = 0; channel < outputAccess.numChannels(); ++channel) + { + const auto outputOffset = hostPixelOffset(outputAccess, outputSampleStride, sample, y, x, channel); + EXPECT_EQ(outputHost[outputOffset], expectedHost[outputOffset]) + << "sample=" << sample << ", row=" << y << ", col=" << x << ", channel=" << channel; + + const auto inputOffset = hostPixelOffset(inputAccess, inputSampleStride, sample, y, x, channel); + EXPECT_EQ(inputAfterHost[inputOffset], inputHost[inputOffset]) + << "input modified at sample=" << sample << ", row=" << y << ", col=" << x << ", channel=" << channel; + } +} + +static void expectHostGold(const nvcv::TensorDataAccessStridedImagePlanar &inputAccess, + const nvcv::TensorDataAccessStridedImagePlanar &outputAccess, int64_t inputSampleStride, + int64_t outputSampleStride, int numSamples, int height, int width, + const std::vector &inputHost, const std::vector &inputAfterHost, + const std::vector &expectedHost, const std::vector &outputHost) +{ + for (int sample = 0; sample < numSamples; ++sample) + { + for (int y = 0; y < height; ++y) + { + for (int x = 0; x < width; ++x) + { + expectHostPixel(inputAccess, outputAccess, inputSampleStride, outputSampleStride, sample, y, x, + inputHost, inputAfterHost, expectedHost, outputHost); + } + } + } +} + +static void compareOutOfPlaceToHostGold(cudaStream_t stream, const nvcv::ImageFormat &format, bool filled) +{ + constexpr int numSamples = 4; + constexpr int width = 18; + constexpr int height = 14; + + nvcv::Tensor input = nvcv::util::CreateTensor(numSamples, width, height, format); + nvcv::Tensor output = nvcv::util::CreateTensor(numSamples, width, height, format); + + auto inputData = input.exportData(); + auto outputData = output.exportData(); + ASSERT_NE(inputData, nullptr); + ASSERT_NE(outputData, nullptr); + + auto inputAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*inputData); + auto outputAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*outputData); + ASSERT_TRUE(inputAccess); + ASSERT_TRUE(outputAccess); + + ASSERT_EQ(inputAccess->numSamples(), numSamples); + ASSERT_EQ(inputAccess->numRows(), height); + ASSERT_EQ(inputAccess->numCols(), width); + ASSERT_EQ(inputAccess->numChannels(), outputAccess->numChannels()); + ASSERT_TRUE(inputAccess->numChannels() == 3 || inputAccess->numChannels() == 4); + + const int64_t inputSampleStride = inputAccess->sampleStride() == 0 + ? inputAccess->rowStride() * inputAccess->numRows() + : inputAccess->sampleStride(); + const int64_t outputSampleStride = outputAccess->sampleStride() == 0 + ? outputAccess->rowStride() * outputAccess->numRows() + : outputAccess->sampleStride(); + const auto inputSize = static_cast(inputSampleStride * numSamples); + const auto outputSize = static_cast(outputSampleStride * numSamples); + + std::vector inputHost(inputSize, 0xA5); + std::vector expectedHost(outputSize, 0x5A); + fillHostPattern(*inputAccess, *outputAccess, inputSampleStride, outputSampleStride, numSamples, height, width, + inputHost, expectedHost); + + const int filledAligned = filled ? 1 : 2; + std::vector> boxes(numSamples); + boxes[1].push_back({ + {filledAligned, filledAligned, 8, 8}, + filled ? -1 : 2, + { 31, 97, 211, 255}, + { 0, 0, 0, 0} + }); + boxes[2].push_back({ + {filledAligned, filledAligned, 6, 6}, + filled ? -1 : 2, + { 32, 98, 210, 255}, + { 0, 0, 0, 0} + }); + boxes[2].push_back({ + {filled ? 11 : 10, filled ? 3 : 4, 6, 8}, + filled ? -1 : 2, + { 67, 149, 193, 255}, + { 0, 0, 0, 0} + }); + + drawHostBoxes(*outputAccess, outputSampleStride, boxes, filled, expectedHost); - std::vector gold(outBufSize); - setGoldBuffer(gold, format, *inAccess, input->basePtr(), bndBoxes, stream); + auto bndBoxes = std::make_shared(boxes); + ASSERT_EQ(cudaSuccess, cudaMemcpy(inputData->basePtr(), inputHost.data(), inputSize, cudaMemcpyHostToDevice)); + ASSERT_EQ(cudaSuccess, cudaMemset(outputData->basePtr(), 0, outputSize)); - EXPECT_EQ(gold, test); + cvcuda::BndBox op; + EXPECT_NO_THROW(op(stream, input, output, (NVCVBndBoxesI)bndBoxes.get())); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + std::vector outputHost(outputSize); + std::vector inputAfterHost(inputSize); + ASSERT_EQ(cudaSuccess, cudaMemcpy(outputHost.data(), outputData->basePtr(), outputSize, cudaMemcpyDeviceToHost)); + ASSERT_EQ(cudaSuccess, cudaMemcpy(inputAfterHost.data(), inputData->basePtr(), inputSize, cudaMemcpyDeviceToHost)); + + expectHostGold(*inputAccess, *outputAccess, inputSampleStride, outputSampleStride, numSamples, height, width, + inputHost, inputAfterHost, expectedHost, outputHost); } // clang-format off @@ -195,6 +346,43 @@ TEST_P(OpBndBox, BndBox_sanity) EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); } +// clang-format off +NVCV_TEST_SUITE_P(OpBndBoxPlanar, test::ValueList +{ + // planar format, interleaved format, batch + {nvcv::FMT_RGB8p, nvcv::FMT_RGB8, 2}, + {nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8, 1}, +}); + +// clang-format on + +TEST_P(OpBndBoxPlanar, tensor_output_matches_interleaved) +{ + constexpr int width = 64; + constexpr int height = 48; + int batch = GetParamValue<2>(); + + std::vector> bndBoxVec; + for (int n = 0; n < batch; ++n) + { + std::vector curVec; + NVCVBndBoxI bndBox; + bndBox.box = {width / 4, height / 4, width / 2, height / 2}; + bndBox.thickness = 3; + bndBox.borderColor = {255, 255, 0, 255}; + bndBox.fillColor = {0, 128, 255, 0}; + curVec.push_back(bndBox); + bndBoxVec.push_back(curVec); + } + auto bndBoxes = std::make_shared(bndBoxVec); + + cvcuda::BndBox op; + nvcv::test::planar::RunTensorParity( + GetParamValue<0>(), GetParamValue<1>(), width, height, width, height, batch, + [&op, &bndBoxes](cudaStream_t stream, const nvcv::Tensor &src, const nvcv::Tensor &dst, nvcv::ImageFormat) + { op(stream, src, dst, (NVCVBndBoxesI)bndBoxes.get()); }); +} + // clang-format on TEST(OpBndBox, BndBox_memory) { @@ -214,6 +402,42 @@ TEST(OpBndBox, BndBox_memory) EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); } +TEST(OpBndBox, BndBox_out_of_place_matches_host_gold_rgb8) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + compareOutOfPlaceToHostGold(stream, nvcv::FMT_RGB8, true); + compareOutOfPlaceToHostGold(stream, nvcv::FMT_RGB8, false); + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpBndBox, BndBox_out_of_place_matches_host_gold_rgba8) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + compareOutOfPlaceToHostGold(stream, nvcv::FMT_RGBA8, true); + compareOutOfPlaceToHostGold(stream, nvcv::FMT_RGBA8, false); + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpBndBox, BndBox_out_of_place_matches_host_gold_rgb8p) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + compareOutOfPlaceToHostGold(stream, nvcv::FMT_RGB8p, true); + compareOutOfPlaceToHostGold(stream, nvcv::FMT_RGB8p, false); + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpBndBox, BndBox_out_of_place_matches_host_gold_rgba8p) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + compareOutOfPlaceToHostGold(stream, nvcv::FMT_RGBA8p, true); + compareOutOfPlaceToHostGold(stream, nvcv::FMT_RGBA8p, false); + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + // clang-format off NVCV_TEST_SUITE_P(OpBndBox_Negative, test::ValueList { @@ -248,37 +472,36 @@ TEST_P(OpBndBox_Negative, invalid_parameters) std::vector> bndBoxVec; - srand(0); + Rng().seed(0); for (int n = 0; n < bboxesN; n++) { std::vector curVec; for (int i = 0; i < num; i++) { NVCVBndBoxI bndBox; - bndBox.box.x = randl(0, inW - 1); - bndBox.box.y = randl(0, inH - 1); - bndBox.box.width = randl(1, inW); - bndBox.box.height = randl(1, inH); - bndBox.thickness = randl(-1, 30); - bndBox.fillColor = {(unsigned char)randl(0, 255), (unsigned char)randl(0, 255), - (unsigned char)randl(0, 255), (unsigned char)randl(0, 255)}; - // Note: borderColor.a must be >= 1 to avoid corner case where 3rd party cuOSD library - // (used for gold generation) has a bug with borderColor.a=0 preventing fill from being drawn + bndBox.box.x = randl(0, inW - 1); + bndBox.box.y = randl(0, inH - 1); + bndBox.box.width = randl(1, inW); + bndBox.box.height = randl(1, inH); + bndBox.thickness = randl(-1, 30); + bndBox.fillColor = {(unsigned char)randl(0, 255), (unsigned char)randl(0, 255), + (unsigned char)randl(0, 255), (unsigned char)randl(0, 255)}; bndBox.borderColor = {(unsigned char)randl(0, 255), (unsigned char)randl(0, 255), - (unsigned char)randl(0, 255), (unsigned char)randl(1, 255)}; // alpha: 1-255 + (unsigned char)randl(0, 255), (unsigned char)randl(1, 255)}; curVec.push_back(bndBox); } bndBoxVec.push_back(curVec); } - std::shared_ptr bndBoxes = std::make_shared(bndBoxVec); + auto bndBoxes = std::make_shared(bndBoxVec); nvcv::Tensor imgIn = nvcv::util::CreateTensor(inN, inW, inH, inFormat); nvcv::Tensor imgOut = nvcv::util::CreateTensor(outN, inW, inH, outFormat); cvcuda::BndBox op; EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, - nvcv::ProtectCall([&] { op(stream, imgIn, imgOut, (NVCVBndBoxesI)bndBoxes.get()); })); + nvcv::ProtectCall([&op, &stream, &imgIn, &imgOut, &bndBoxes] + { op(stream, imgIn, imgOut, (NVCVBndBoxesI)bndBoxes.get()); })); EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); diff --git a/tests/cvcuda/system/TestOpBndBox_Smoke.cpp b/tests/cvcuda/system/TestOpBndBox_Smoke.cpp index aab7ff7df..a1267341c 100644 --- a/tests/cvcuda/system/TestOpBndBox_Smoke.cpp +++ b/tests/cvcuda/system/TestOpBndBox_Smoke.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -79,7 +79,7 @@ TEST(OpBndBox_Smoke, basic_functionality_rgb8) bndBoxVec.push_back(boxes); } - std::shared_ptr bndBoxes = std::make_shared(bndBoxVec); + auto bndBoxes = std::make_shared(bndBoxVec); // Run operator cvcuda::BndBox op; @@ -171,7 +171,7 @@ TEST(OpBndBox_Smoke, basic_functionality_rgba8) boxes.push_back(box); bndBoxVec.push_back(boxes); - std::shared_ptr bndBoxes = std::make_shared(bndBoxVec); + auto bndBoxes = std::make_shared(bndBoxVec); cvcuda::BndBox op; EXPECT_NO_THROW(op(stream, imgIn, imgOut, (NVCVBndBoxesI)bndBoxes.get())); @@ -195,10 +195,9 @@ TEST(OpBndBox_Smoke, basic_functionality_rgba8) uint8_t r = outData[i]; uint8_t g = outData[i + 1]; uint8_t b = outData[i + 2]; - uint8_t a = outData[i + 3]; // Gray background with full alpha (should be present) - if (r == 64 && g == 64 && b == 64 && a == 255) + if (uint8_t a = outData[i + 3]; r == 64 && g == 64 && b == 64 && a == 255) hasGray = true; // Blue component should be present from fill (pure blue or blended) @@ -255,7 +254,7 @@ TEST(OpBndBox_Smoke, multiple_boxes) } bndBoxVec.push_back(boxes); - std::shared_ptr bndBoxes = std::make_shared(bndBoxVec); + auto bndBoxes = std::make_shared(bndBoxVec); cvcuda::BndBox op; EXPECT_NO_THROW(op(stream, imgIn, imgOut, (NVCVBndBoxesI)bndBoxes.get())); @@ -271,7 +270,7 @@ TEST(OpBndBox_Smoke, memory_management) cvcuda::BndBox op; - for (int iter = 0; iter < 5; iter++) + for (int iter = 0; iter < 5; iter++) // NOSONAR { nvcv::Tensor imgIn = nvcv::util::CreateTensor(1, 224, 224, nvcv::FMT_RGB8); nvcv::Tensor imgOut = nvcv::util::CreateTensor(1, 224, 224, nvcv::FMT_RGB8); @@ -301,7 +300,7 @@ TEST(OpBndBox_Smoke, memory_management) boxes.push_back(box); bndBoxVec.push_back(boxes); - std::shared_ptr bndBoxes = std::make_shared(bndBoxVec); + auto bndBoxes = std::make_shared(bndBoxVec); EXPECT_NO_THROW(op(stream, imgIn, imgOut, (NVCVBndBoxesI)bndBoxes.get())); EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); @@ -337,7 +336,7 @@ TEST(OpBndBox_Smoke, edge_cases) std::vector> bndBoxVec; std::vector boxes; // Empty bndBoxVec.push_back(boxes); - std::shared_ptr bndBoxes = std::make_shared(bndBoxVec); + auto bndBoxes = std::make_shared(bndBoxVec); EXPECT_NO_THROW(op(stream, imgIn, imgOut, (NVCVBndBoxesI)bndBoxes.get())); EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); @@ -361,7 +360,7 @@ TEST(OpBndBox_Smoke, edge_cases) box.borderColor = {255, 255, 255, 255}; boxes.push_back(box); bndBoxVec.push_back(boxes); - std::shared_ptr bndBoxes = std::make_shared(bndBoxVec); + auto bndBoxes = std::make_shared(bndBoxVec); EXPECT_NO_THROW(op(stream, imgIn, imgOut, (NVCVBndBoxesI)bndBoxes.get())); EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); diff --git a/tests/cvcuda/system/TestOpBoxBlur.cpp b/tests/cvcuda/system/TestOpBoxBlur.cpp index c967e8e8b..a584af6dd 100644 --- a/tests/cvcuda/system/TestOpBoxBlur.cpp +++ b/tests/cvcuda/system/TestOpBoxBlur.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,8 +16,7 @@ */ #include "Definitions.hpp" - -#include "OsdUtils.cuh" +#include "PlanarParityUtils.hpp" #include #include @@ -27,61 +26,339 @@ #include #include -#include +#include +#include #include -#include +#include +#include +#include namespace gt = ::testing; namespace test = nvcv::test; using namespace cvcuda::priv; -static void setGoldBuffer(std::vector &vect, nvcv::ImageFormat format, - const nvcv::TensorDataAccessStridedImagePlanar &data, nvcv::Byte *inBuf, - std::shared_ptr bboxes, cudaStream_t stream) +constexpr int kBoxBlurCropSize = 32; +constexpr int kBoxBlurColorChannels = 3; + +static std::shared_ptr MakeBlurBoxes(int numImages, const std::vector &boxes) +{ + std::vector> blurBoxVec(numImages, boxes); + return std::make_shared(blurBoxVec); +} + +template +static std::vector DownloadTensorPixelsAs(const nvcv::TensorDataAccessStridedImagePlanar &access) +{ + static_assert(sizeof(T) == 1); + + const int64_t samples = access.numSamples(); + const int64_t channels = access.numChannels(); + const int64_t rows = access.numRows(); + const int64_t cols = access.numCols(); + const bool planar = access.chStride() != 1; + + std::vector pixels; + pixels.resize(samples * rows * cols * channels); + + auto *dst = pixels.data(); + const auto rowBytes = static_cast(cols * (planar ? 1 : channels)); + const auto dstPitch = rowBytes; + const auto height = static_cast(rows); + + for (int64_t n = 0; n < samples; ++n) + { + if (planar) + { + for (int64_t c = 0; c < channels; ++c) + { + const auto *src = access.sampleData(static_cast(n)) + c * access.chStride(); + EXPECT_EQ(cudaSuccess, cudaMemcpy2D(dst, dstPitch, src, static_cast(access.rowStride()), + rowBytes, height, cudaMemcpyDeviceToHost)); + dst += rows * cols; + } + } + else + { + EXPECT_EQ(cudaSuccess, + cudaMemcpy2D(dst, dstPitch, access.sampleData(static_cast(n)), + static_cast(access.rowStride()), rowBytes, height, cudaMemcpyDeviceToHost)); + dst += rows * cols * channels; + } + } + + return pixels; +} + +static std::vector DownloadTensorPixels(const nvcv::TensorDataAccessStridedImagePlanar &access) +{ + return DownloadTensorPixelsAs(access); +} + +static size_t TensorPixelOffset(int64_t sample, int64_t channel, int64_t row, int64_t col, int64_t channels, + int64_t rows, int64_t cols, bool planar) +{ + if (planar) + { + return ((static_cast(sample) * static_cast(channels) + static_cast(channel)) + * static_cast(rows) + + static_cast(row)) + * static_cast(cols) + + static_cast(col); + } + return ((static_cast(sample) * static_cast(rows) + static_cast(row)) + * static_cast(cols) + + static_cast(col)) + * static_cast(channels) + + static_cast(channel); +} + +struct TensorPixelCoordinates { - auto context = cuosd_context_create(); + int sample; + int channel; + int row; + int col; +}; + +static TensorPixelCoordinates GetTensorPixelCoordinates(size_t index, int channels, int rows, int cols) +{ + const auto col = static_cast(index % static_cast(cols)); + index /= static_cast(cols); + const auto row = static_cast(index % static_cast(rows)); + index /= static_cast(rows); + const auto channel = static_cast(index % static_cast(channels)); + const auto sample = static_cast(index / static_cast(channels)); + return {sample, channel, row, col}; +} - for (int n = 0; n < bboxes->batch(); n++) +static std::vector MakeBoxBlurInput(int samples, int channels, int rows, int cols, bool planar) +{ + std::vector pixels(static_cast(samples) * channels * rows * cols); + for (size_t index = 0; index < pixels.size(); ++index) { - test::osd::Image *image = test::osd::create_image( - data.numCols(), data.numRows(), - format == nvcv::FMT_RGBA8 ? test::osd::ImageFormat::RGBA : test::osd::ImageFormat::RGB); - int bufSize = data.numCols() * data.numRows() * data.numChannels(); - EXPECT_EQ(cudaSuccess, cudaMemcpy(image->data0, inBuf + n * bufSize, bufSize, cudaMemcpyDeviceToDevice)); + const auto [sample, channel, row, col] = GetTensorPixelCoordinates(index, channels, rows, cols); + pixels[TensorPixelOffset(sample, channel, row, col, channels, rows, cols, planar)] + = static_cast((sample * 53 + channel * 71 + row * 29 + col * 17 + 13) & 0xFF); + } + return pixels; +} - auto numBoxes = bboxes->numBoxesAt(n); +template +static void UploadTensorPixels(const nvcv::TensorDataAccessStridedImagePlanar &access, const std::vector &pixels) +{ + static_assert(sizeof(T) == 1); + + const int64_t samples = access.numSamples(); + const int64_t channels = access.numChannels(); + const int64_t rows = access.numRows(); + const int64_t cols = access.numCols(); + const bool planar = access.chStride() != 1; - for (int i = 0; i < numBoxes; i++) + ASSERT_EQ(pixels.size(), static_cast(samples) * channels * rows * cols); + for (int64_t sample = 0; sample < samples; ++sample) + { + if (planar) { - auto bbox = bboxes->boxAt(n, i); + for (int64_t channel = 0; channel < channels; ++channel) + { + const auto offset = TensorPixelOffset(sample, channel, 0, 0, channels, rows, cols, planar); + ASSERT_EQ(cudaSuccess, + cudaMemcpy2D(access.sampleData(static_cast(sample)) + channel * access.chStride(), + static_cast(access.rowStride()), pixels.data() + offset, + static_cast(cols), static_cast(cols), static_cast(rows), + cudaMemcpyHostToDevice)); + } + } + else + { + const auto offset = TensorPixelOffset(sample, 0, 0, 0, channels, rows, cols, planar); + const auto rowBytes = static_cast(cols * channels); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(access.sampleData(static_cast(sample)), + static_cast(access.rowStride()), pixels.data() + offset, + rowBytes, rowBytes, static_cast(rows), cudaMemcpyHostToDevice)); + } + } +} + +template +using BoxBlurCropBuffer = std::array; + +static size_t BoxBlurCropOffset(int channel, int row, int col) +{ + return (static_cast(channel) * kBoxBlurCropSize + row) * kBoxBlurCropSize + col; +} + +static int BoxToCropCoordinate(int relativeCoordinate, int boxExtent) +{ + return static_cast(static_cast(relativeCoordinate) / static_cast(boxExtent) + * static_cast(kBoxBlurCropSize)); +} - int left = std::max(std::min(bbox.box.x, data.numCols() - 1), 0); - int top = std::max(std::min(bbox.box.y, data.numRows() - 1), 0); - int right = std::max(std::min(left + bbox.box.width - 1, data.numCols() - 1), 0); - int bottom = std::max(std::min(top + bbox.box.height - 1, data.numRows() - 1), 0); +static int CropToImageCoordinate(int cropCoordinate, int boxExtent, int origin, int imageExtent) +{ + const float coordinate + = static_cast(cropCoordinate) / static_cast(kBoxBlurCropSize) * static_cast(boxExtent) + + 0.5F + static_cast(origin); + return std::clamp(static_cast(coordinate), 0, imageExtent - 1); +} - if (left == right || top == bottom || bbox.box.width < 3 || bbox.box.height < 3 || bbox.kernelSize < 1) +template +static void CopyBoxToCrop(const std::vector &input, BoxBlurCropBuffer &crop, int sample, int channels, int rows, + int cols, bool planar, int left, int top, int boxWidth, int boxHeight) +{ + for (int channel = 0; channel < std::min(channels, kBoxBlurColorChannels); ++channel) + { + for (int cropRow = 0; cropRow < kBoxBlurCropSize; ++cropRow) + { + for (int cropCol = 0; cropCol < kBoxBlurCropSize; ++cropCol) + { + const int srcCol = CropToImageCoordinate(cropCol, boxWidth, left, cols); + const int srcRow = CropToImageCoordinate(cropRow, boxHeight, top, rows); + crop[BoxBlurCropOffset(channel, cropRow, cropCol)] + = input[TensorPixelOffset(sample, channel, srcRow, srcCol, channels, rows, cols, planar)]; + } + } + } +} + +template +static T BlurCropPixel(const BoxBlurCropBuffer &crop, int channel, int cropRow, int cropCol, int radius) +{ + using SumType = std::conditional_t, int32_t, uint32_t>; + SumType sum = 0; + int count = 0; + for (int filterRow = -radius; filterRow <= radius; ++filterRow) + { + for (int filterCol = -radius; filterCol <= radius; ++filterCol) + { + const int sampleRow = cropRow + filterRow; + const int sampleCol = cropCol + filterCol; + if (sampleRow < 0 || sampleRow >= kBoxBlurCropSize || sampleCol < 0 || sampleCol >= kBoxBlurCropSize) { continue; } + sum += crop[BoxBlurCropOffset(channel, sampleRow, sampleCol)]; + ++count; + } + } + if (count == 0) + { + return T{}; + } + return static_cast(sum / count); +} + +template +static void BlurCrop(const BoxBlurCropBuffer &crop, BoxBlurCropBuffer &blurred, int channels, int radius) +{ + for (int channel = 0; channel < std::min(channels, kBoxBlurColorChannels); ++channel) + { + for (int cropRow = 0; cropRow < kBoxBlurCropSize; ++cropRow) + { + for (int cropCol = 0; cropCol < kBoxBlurCropSize; ++cropCol) + { + blurred[BoxBlurCropOffset(channel, cropRow, cropCol)] + = BlurCropPixel(crop, channel, cropRow, cropCol, radius); + } + } + } +} - int kernelSize = bbox.kernelSize; +template +static void CopyCropToBox(const BoxBlurCropBuffer &blurred, std::vector &output, int sample, int channels, + int rows, int cols, bool planar, int left, int top, int boxWidth, int boxHeight) +{ + const int gapWidth = (boxWidth + kBoxBlurCropSize - 1) / kBoxBlurCropSize; + const int gapHeight = (boxHeight + kBoxBlurCropSize - 1) / kBoxBlurCropSize; + const int coveredWidth = kBoxBlurCropSize * gapWidth; - cuosd_draw_boxblur(context, left, top, right, bottom, kernelSize); + for (int relativeRow = 0; relativeRow < kBoxBlurCropSize * gapHeight; ++relativeRow) + { + const int dstRow = relativeRow + top; + if (dstRow < 0 || dstRow >= rows) + { + continue; + } + const int srcRow = BoxToCropCoordinate(relativeRow, boxHeight); + if (srcRow >= kBoxBlurCropSize) + { + continue; } - test::osd::cuosd_apply(context, image, stream); + for (int relativeCol = 0; relativeCol < coveredWidth; ++relativeCol) + { + const int dstCol = relativeCol + left; + if (dstCol < 0 || dstCol >= cols) + { + continue; + } + const int srcCol = BoxToCropCoordinate(relativeCol, boxWidth); + if (srcCol >= kBoxBlurCropSize) + { + continue; + } + + for (int channel = 0; channel < std::min(channels, kBoxBlurColorChannels); ++channel) + { + output[TensorPixelOffset(sample, channel, dstRow, dstCol, channels, rows, cols, planar)] + = blurred[BoxBlurCropOffset(channel, srcRow, srcCol)]; + } + if (channels == 4) + { + output[TensorPixelOffset(sample, 3, dstRow, dstCol, channels, rows, cols, planar)] + = std::numeric_limits::max(); + } + } + } +} + +template +static std::vector BoxBlurReference(const std::vector &input, int samples, int channels, int rows, int cols, + bool planar, const std::vector &boxes) +{ + static_assert(sizeof(T) == 1 && std::is_integral_v); - EXPECT_EQ(cudaSuccess, cudaMemcpy(vect.data() + n * bufSize, image->data0, bufSize, cudaMemcpyDeviceToHost)); + BoxBlurCropBuffer crop{}; + BoxBlurCropBuffer blurred{}; + std::vector output = input; - test::osd::free_image(image); + for (int sample = 0; sample < samples; ++sample) + { + for (const NVCVBlurBoxI &box : boxes) + { + const int left = std::clamp(box.box.x, 0, cols - 1); + const int top = std::clamp(box.box.y, 0, rows - 1); + const int right = std::clamp(left + box.box.width - 1, 0, cols - 1); + const int bottom = std::clamp(top + box.box.height - 1, 0, rows - 1); + if (left == right || top == bottom || box.box.width < 3 || box.box.height < 3 || box.kernelSize < 1) + { + continue; + } + + const int boxWidth = right - left; + const int boxHeight = bottom - top; + CopyBoxToCrop(input, crop, sample, channels, rows, cols, planar, left, top, boxWidth, boxHeight); + BlurCrop(crop, blurred, channels, box.kernelSize / 2); + CopyCropToBox(blurred, output, sample, channels, rows, cols, planar, left, top, boxWidth, boxHeight); + } } - cudaStreamSynchronize(stream); - cuosd_context_destroy(context); + return output; } -static void runOp(cudaStream_t &stream, cvcuda::BoxBlur &op, int &inN, int &inW, int &inH, int &cols, int &rows, - int &wBox, int &hBox, int &ks, nvcv::ImageFormat &format) +static std::vector MakeSignedBoxBlurInput(int samples, int channels, int rows, int cols, bool planar) +{ + std::vector pixels(static_cast(samples) * channels * rows * cols); + for (size_t index = 0; index < pixels.size(); ++index) + { + const auto [sample, channel, row, col] = GetTensorPixelCoordinates(index, channels, rows, cols); + const int value = (sample * 43 + channel * 61 + row * 23 + col * 17) % 191 - 95; + pixels[TensorPixelOffset(sample, channel, row, col, channels, rows, cols, planar)] = static_cast(value); + } + return pixels; +} + +static void runOp(cudaStream_t &stream, const cvcuda::BoxBlur &op, int inN, int inW, int inH, int cols, int rows, + int wBox, int hBox, int ks, const nvcv::ImageFormat &format) { std::vector> blurBoxVec; @@ -105,7 +382,7 @@ static void runOp(cudaStream_t &stream, cvcuda::BoxBlur &op, int &inN, int &inW, blurBoxVec.push_back(curVec); } - std::shared_ptr blurBoxes = std::make_shared(blurBoxVec); + auto blurBoxes = std::make_shared(blurBoxVec); nvcv::Tensor imgIn = nvcv::util::CreateTensor(inN, inW, inH, format); nvcv::Tensor imgOut = nvcv::util::CreateTensor(inN, inW, inH, format); @@ -125,73 +402,27 @@ static void runOp(cudaStream_t &stream, cvcuda::BoxBlur &op, int &inN, int &inW, long inSampleStride = inAccess->numRows() * inAccess->rowStride(); long outSampleStride = outAccess->numRows() * outAccess->rowStride(); - int inBufSize = inSampleStride * inAccess->numSamples(); - int outBufSize = outSampleStride * outAccess->numSamples(); + auto inBufSize = static_cast(inSampleStride * inAccess->numSamples()); + auto outBufSize = static_cast(outSampleStride * outAccess->numSamples()); std::vector inVec(inBufSize); std::default_random_engine randEng(0); std::uniform_int_distribution rand(0u, 255u); - std::generate(inVec.begin(), inVec.end(), [&]() { return rand(randEng); }); + std::ranges::generate(inVec, [&rand, &randEng]() { return rand(randEng); }); - // copy random input to device EXPECT_EQ(cudaSuccess, cudaMemcpy(input->basePtr(), inVec.data(), inBufSize, cudaMemcpyHostToDevice)); EXPECT_EQ(cudaSuccess, cudaMemcpy(output->basePtr(), inVec.data(), outBufSize, cudaMemcpyHostToDevice)); - // run operator EXPECT_NO_THROW(op(stream, imgIn, imgOut, (NVCVBlurBoxesI)blurBoxes.get())); - // check cdata - std::vector test(outBufSize); - std::vector testIn(inBufSize); + std::vector outHost(outBufSize); EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); - EXPECT_EQ(cudaSuccess, cudaMemcpy(testIn.data(), input->basePtr(), inBufSize, cudaMemcpyDeviceToHost)); - EXPECT_EQ(cudaSuccess, cudaMemcpy(test.data(), output->basePtr(), outBufSize, cudaMemcpyDeviceToHost)); - - std::vector gold(outBufSize); - setGoldBuffer(gold, format, *inAccess, input->basePtr(), blurBoxes, stream); - - EXPECT_EQ(gold, test); -} - -// clang-format off -NVCV_TEST_SUITE_P(OpBoxBlur, test::ValueList -{ - // inN, inW, inH, cols, rows, wBox, hBox, ks, format - { 1, 224, 224, 5, 5, 16, 16, 7, nvcv::FMT_RGBA8 }, - { 8, 224, 224, 5, 5, 16, 16, 7, nvcv::FMT_RGBA8 }, - { 16, 224, 224, 5, 5, 16, 16, 7, nvcv::FMT_RGBA8 }, - { 1, 224, 224, 5, 5, 16, 16, 7, nvcv::FMT_RGB8 }, - { 8, 224, 224, 5, 5, 16, 16, 7, nvcv::FMT_RGB8 }, - { 16, 224, 224, 5, 5, 16, 16, 7, nvcv::FMT_RGB8 }, - { 1, 1280, 720, 10, 10, 64, 64, 13, nvcv::FMT_RGBA8 }, - { 1, 1920, 1080, 15, 15, 64, 64, 19, nvcv::FMT_RGBA8 }, - { 1, 3840, 2160, 15, 15, 128, 128, 23, nvcv::FMT_RGBA8 }, - { 1, 1280, 720, 10, 10, 64, 64, 13, nvcv::FMT_RGB8 }, - { 1, 1920, 1080, 15, 15, 64, 64, 19, nvcv::FMT_RGB8 }, - { 1, 3840, 2160, 15, 15, 128, 128, 23, nvcv::FMT_RGB8 }, -}); + EXPECT_EQ(cudaSuccess, cudaMemcpy(outHost.data(), output->basePtr(), outBufSize, cudaMemcpyDeviceToHost)); -// clang-format on -TEST_P(OpBoxBlur, BoxBlur_sanity) -{ - cudaStream_t stream; - ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); - int inN = GetParamValue<0>(); - int inW = GetParamValue<1>(); - int inH = GetParamValue<2>(); - int cols = GetParamValue<3>(); - int rows = GetParamValue<4>(); - int wBox = GetParamValue<5>(); - int hBox = GetParamValue<6>(); - int ks = GetParamValue<7>(); - nvcv::ImageFormat format = GetParamValue<8>(); - cvcuda::BoxBlur op; - runOp(stream, op, inN, inW, inH, cols, rows, wBox, hBox, ks, format); - EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + EXPECT_NE(inVec, outHost) << "Output should differ from input after applying box blur"; } -// clang-format on TEST(OpBoxBlur, BoxBlur_memory) { cudaStream_t stream; @@ -213,6 +444,233 @@ TEST(OpBoxBlur, BoxBlur_memory) EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); } +// clang-format off +NVCV_TEST_SUITE_P(OpBoxBlurInPlaceParity, test::ValueList +{ + nvcv::FMT_RGB8, + nvcv::FMT_RGBA8, + nvcv::FMT_RGB8p, + nvcv::FMT_RGBA8p, +}); + +// clang-format on + +TEST_P(OpBoxBlurInPlaceParity, out_of_place_matches_in_place) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + constexpr int numImages = 2; + constexpr int width = 128; + constexpr int height = 96; + nvcv::ImageFormat format = GetParam(); + + nvcv::Tensor src = nvcv::util::CreateTensor(numImages, width, height, format); + nvcv::Tensor outOfPlace = nvcv::util::CreateTensor(numImages, width, height, format); + nvcv::Tensor inPlace = nvcv::util::CreateTensor(numImages, width, height, format); + + auto srcData = src.exportData(); + auto outOfPlaceData = outOfPlace.exportData(); + auto inPlaceData = inPlace.exportData(); + + ASSERT_NE(srcData, nullptr); + ASSERT_NE(outOfPlaceData, nullptr); + ASSERT_NE(inPlaceData, nullptr); + + auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcData); + ASSERT_TRUE(srcAccess); + + const int64_t bufferSize = srcAccess->sampleStride() * srcAccess->numSamples(); + ASSERT_GT(bufferSize, 0); + + std::vector input(bufferSize); + for (size_t i = 0; i < input.size(); ++i) + { + input[i] = static_cast((i * 31 + 7) & 0xFF); + } + + ASSERT_EQ(cudaSuccess, cudaMemcpy(srcData->basePtr(), input.data(), bufferSize, cudaMemcpyHostToDevice)); + ASSERT_EQ(cudaSuccess, cudaMemcpy(inPlaceData->basePtr(), input.data(), bufferSize, cudaMemcpyHostToDevice)); + ASSERT_EQ(cudaSuccess, cudaMemset(outOfPlaceData->basePtr(), 0xCD, bufferSize)); + + std::vector boxes{ + { {8, 7, 32, 31}, 5}, + {{72, 41, 29, 23}, 7}, + }; + auto blurBoxes = MakeBlurBoxes(numImages, boxes); + + cvcuda::BoxBlur op; + EXPECT_NO_THROW(op(stream, src, outOfPlace, (NVCVBlurBoxesI)blurBoxes.get())); + EXPECT_NO_THROW(op(stream, inPlace, inPlace, (NVCVBlurBoxesI)blurBoxes.get())); + + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + auto outOfPlaceAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*outOfPlaceData); + auto inPlaceAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*inPlaceData); + ASSERT_TRUE(outOfPlaceAccess); + ASSERT_TRUE(inPlaceAccess); + + EXPECT_EQ(DownloadTensorPixels(*inPlaceAccess), DownloadTensorPixels(*outOfPlaceAccess)); + + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +// clang-format off +NVCV_TEST_SUITE_P(OpBoxBlurCpuGold, test::ValueList +{ + nvcv::FMT_RGB8, + nvcv::FMT_RGBA8, + nvcv::FMT_RGB8p, + nvcv::FMT_RGBA8p, +}); + +// clang-format on + +TEST_P(OpBoxBlurCpuGold, tensor_correct_output_matches_independent_cpu_reference) +{ + constexpr int samples = 2; + constexpr int width = 128; + constexpr int height = 96; + + const nvcv::ImageFormat format = GetParam(); + nvcv::Tensor input = nvcv::util::CreateTensor(samples, width, height, format); + nvcv::Tensor output = nvcv::util::CreateTensor(samples, width, height, format); + + auto inputData = input.exportData(); + auto outputData = output.exportData(); + ASSERT_TRUE(inputData); + ASSERT_TRUE(outputData); + + auto inputAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*inputData); + auto outputAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*outputData); + ASSERT_TRUE(inputAccess); + ASSERT_TRUE(outputAccess); + + const int channels = inputAccess->numChannels(); + const bool planar = inputAccess->chStride() != 1; + const auto inputHost = MakeBoxBlurInput(samples, channels, height, width, planar); + UploadTensorPixels(*inputAccess, inputHost); + ASSERT_EQ(cudaSuccess, cudaMemset(outputData->basePtr(), 0xCD, + static_cast(outputAccess->sampleStride() * outputAccess->numSamples()))); + + const std::vector boxes{ + { {-4, -3, 7, 6}, 3}, + { {8, 7, 32, 31}, 5}, + {{72, 41, 29, 23}, 7}, + }; + const auto reference = BoxBlurReference(inputHost, samples, channels, height, width, planar, boxes); + auto blurBoxes = MakeBlurBoxes(samples, boxes); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + cvcuda::BoxBlur op; + EXPECT_NO_THROW(op(stream, input, output, (NVCVBlurBoxesI)blurBoxes.get())); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + EXPECT_EQ(reference, DownloadTensorPixels(*outputAccess)); + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +// clang-format off +NVCV_TEST_SUITE_P(OpBoxBlurSignedCpuGold, test::ValueList +{ + {nvcv::TENSOR_NHWC, 3}, + {nvcv::TENSOR_NHWC, 4}, + {nvcv::TENSOR_NCHW, 3}, + {nvcv::TENSOR_NCHW, 4}, +}); + +// clang-format on + +TEST_P(OpBoxBlurSignedCpuGold, negative_s8_values_match_independent_cpu_reference) +{ + constexpr int samples = 1; + constexpr int width = 64; + constexpr int height = 48; + + const nvcv::TensorLayout layout = GetParamValue<0>(); + const int channels = GetParamValue<1>(); + const bool planar = layout == nvcv::TENSOR_NCHW; + const nvcv::TensorShape shape + = planar ? nvcv::TensorShape{{samples, channels, height, width}, layout} + : nvcv::TensorShape{{samples, height, width, channels}, layout}; + + nvcv::Tensor input(shape, nvcv::TYPE_S8); + nvcv::Tensor output(shape, nvcv::TYPE_S8); + + auto inputData = input.exportData(); + auto outputData = output.exportData(); + ASSERT_TRUE(inputData); + ASSERT_TRUE(outputData); + + auto inputAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*inputData); + auto outputAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*outputData); + ASSERT_TRUE(inputAccess); + ASSERT_TRUE(outputAccess); + + const auto inputHost = MakeSignedBoxBlurInput(samples, channels, height, width, planar); + ASSERT_TRUE(std::ranges::any_of(inputHost, [](int8_t value) { return value < 0; })); + UploadTensorPixels(*inputAccess, inputHost); + ASSERT_EQ(cudaSuccess, cudaMemset(outputData->basePtr(), 0xCD, + static_cast(outputAccess->sampleStride() * outputAccess->numSamples()))); + + const std::vector boxes{ + {{7, 5, 41, 31}, 5} + }; + const auto reference = BoxBlurReference(inputHost, samples, channels, height, width, planar, boxes); + auto blurBoxes = MakeBlurBoxes(samples, boxes); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + cvcuda::BoxBlur op; + EXPECT_NO_THROW(op(stream, input, output, (NVCVBlurBoxesI)blurBoxes.get())); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + EXPECT_EQ(reference, DownloadTensorPixelsAs(*outputAccess)); + EXPECT_EQ(inputHost, DownloadTensorPixelsAs(*inputAccess)); + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +// clang-format off +NVCV_TEST_SUITE_P(OpBoxBlurPlanar, + test::ValueList +{ + // numImages, width, height, box x/y/w/h, kernel, planar format, interleaved format + {2, 37, 41, 3, 5, 24, 25, 5, nvcv::FMT_RGB8p, nvcv::FMT_RGB8 }, + {2, 37, 41, 3, 5, 24, 25, 5, nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8}, + {1, 64, 48, 7, 9, 31, 29, 17, nvcv::FMT_RGB8p, nvcv::FMT_RGB8 }, + {1, 64, 48, 7, 9, 31, 29, 17, nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8}, +}); + +// clang-format on + +TEST_P(OpBoxBlurPlanar, tensor_matches_interleaved) +{ + int numImages = GetParamValue<0>(); + int width = GetParamValue<1>(); + int height = GetParamValue<2>(); + int boxX = GetParamValue<3>(); + int boxY = GetParamValue<4>(); + int boxW = GetParamValue<5>(); + int boxH = GetParamValue<6>(); + int kernelSize = GetParamValue<7>(); + nvcv::ImageFormat planarFmt = GetParamValue<8>(); + nvcv::ImageFormat interleavedFmt = GetParamValue<9>(); + + NVCVBlurBoxI blurBox{ + {boxX, boxY, boxW, boxH}, + kernelSize + }; + + auto blurBoxes = MakeBlurBoxes(numImages, {blurBox}); + cvcuda::BoxBlur op; + + test::planar::RunTensorParity( + planarFmt, interleavedFmt, width, height, width, height, numImages, + [&op, &blurBoxes](cudaStream_t stream, const nvcv::Tensor &src, const nvcv::Tensor &dst, nvcv::ImageFormat) + { op(stream, src, dst, (NVCVBlurBoxesI)blurBoxes.get()); }); +} + // clang-format off NVCV_TEST_SUITE_P(OpBoxBlur_Negative, test::ValueList { @@ -273,7 +731,7 @@ TEST_P(OpBoxBlur_Negative, invalid_parameters) blurBoxVec.push_back(curVec); } - std::shared_ptr blurBoxes = std::make_shared(blurBoxVec); + auto blurBoxes = std::make_shared(blurBoxVec); nvcv::Tensor imgIn = nvcv::util::CreateTensor(inN, inW, inH, inFormat); nvcv::Tensor imgOut = nvcv::util::CreateTensor(inN, outW, inH, outFormat); @@ -281,7 +739,8 @@ TEST_P(OpBoxBlur_Negative, invalid_parameters) // run operator cvcuda::BoxBlur op; EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, - nvcv::ProtectCall([&] { op(stream, imgIn, imgOut, (NVCVBlurBoxesI)blurBoxes.get()); })); + nvcv::ProtectCall([&op, &stream, &imgIn, &imgOut, &blurBoxes] + { op(stream, imgIn, imgOut, (NVCVBlurBoxesI)blurBoxes.get()); })); EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); @@ -333,7 +792,7 @@ TEST(OpBoxBlur, test_nothing_to_apply) blurBoxVec.push_back(curVec); - std::shared_ptr blurBoxes = std::make_shared(blurBoxVec); + auto blurBoxes = std::make_shared(blurBoxVec); nvcv::Tensor img = nvcv::util::CreateTensor(inN, inW, inH, format); diff --git a/tests/cvcuda/system/TestOpBoxBlur_Smoke.cpp b/tests/cvcuda/system/TestOpBoxBlur_Smoke.cpp index c3fd3ff82..fba359f51 100644 --- a/tests/cvcuda/system/TestOpBoxBlur_Smoke.cpp +++ b/tests/cvcuda/system/TestOpBoxBlur_Smoke.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -59,18 +59,18 @@ TEST(OpBoxBlur_Smoke, basic_functionality_rgb8) // This creates sharp edges that will be smoothed by blur auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*input); ASSERT_TRUE(inAccess); - long sampleStride = inAccess->numRows() * inAccess->rowStride(); + const auto sampleStride = static_cast(inAccess->numRows()) * static_cast(inAccess->rowStride()); std::vector inData(sampleStride * N, 0); // Create white squares in the center for each batch - int rowStride = inAccess->rowStride(); + auto rowStride = static_cast(inAccess->rowStride()); for (int n = 0; n < N; n++) { for (int y = 80; y < 150; y++) { for (int x = 80; x < 150; x++) { - int offset = n * sampleStride + y * rowStride + x * 3; + auto offset = static_cast(n) * sampleStride + y * rowStride + x * 3; inData[offset] = 255; // R inData[offset + 1] = 255; // G inData[offset + 2] = 255; // B @@ -95,7 +95,7 @@ TEST(OpBoxBlur_Smoke, basic_functionality_rgb8) blurBoxVec.push_back(boxes); } - std::shared_ptr blurBoxes = std::make_shared(blurBoxVec); + auto blurBoxes = std::make_shared(blurBoxVec); // Run operator cvcuda::BoxBlur op; @@ -150,9 +150,9 @@ TEST(OpBoxBlur_Smoke, basic_functionality_rgba8) // This creates sharp color transitions that will be smoothed by blur auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*input); ASSERT_TRUE(inAccess); - long sampleStride = inAccess->numRows() * inAccess->rowStride(); + const auto sampleStride = static_cast(inAccess->numRows()) * static_cast(inAccess->rowStride()); std::vector inData(sampleStride * N); - int rowStride = inAccess->rowStride(); + auto rowStride = static_cast(inAccess->rowStride()); // Fill with blue background (0, 0, 255, 255) for (size_t i = 0; i < inData.size(); i += 4) @@ -191,7 +191,7 @@ TEST(OpBoxBlur_Smoke, basic_functionality_rgba8) boxes.push_back(box); blurBoxVec.push_back(boxes); - std::shared_ptr blurBoxes = std::make_shared(blurBoxVec); + auto blurBoxes = std::make_shared(blurBoxVec); cvcuda::BoxBlur op; EXPECT_NO_THROW(op(stream, imgIn, imgOut, (NVCVBlurBoxesI)blurBoxes.get())); @@ -240,7 +240,7 @@ TEST(OpBoxBlur_Smoke, multiple_boxes) } blurBoxVec.push_back(boxes); - std::shared_ptr blurBoxes = std::make_shared(blurBoxVec); + auto blurBoxes = std::make_shared(blurBoxVec); cvcuda::BoxBlur op; EXPECT_NO_THROW(op(stream, imgIn, imgOut, (NVCVBlurBoxesI)blurBoxes.get())); @@ -258,7 +258,7 @@ TEST(OpBoxBlur_Smoke, various_kernel_sizes) // Test different kernel sizes std::vector kernelSizes = {1, 3, 5, 7, 11, 15, 21}; - for (int ks : kernelSizes) + for (int ks : kernelSizes) // NOSONAR { nvcv::Tensor imgIn = nvcv::util::CreateTensor(1, 224, 224, nvcv::FMT_RGB8); nvcv::Tensor imgOut = nvcv::util::CreateTensor(1, 224, 224, nvcv::FMT_RGB8); @@ -274,7 +274,7 @@ TEST(OpBoxBlur_Smoke, various_kernel_sizes) boxes.push_back(box); blurBoxVec.push_back(boxes); - std::shared_ptr blurBoxes = std::make_shared(blurBoxVec); + auto blurBoxes = std::make_shared(blurBoxVec); EXPECT_NO_THROW(op(stream, imgIn, imgOut, (NVCVBlurBoxesI)blurBoxes.get())) << "Failed with kernel size " << ks; EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); @@ -291,7 +291,7 @@ TEST(OpBoxBlur_Smoke, memory_management) cvcuda::BoxBlur op; - for (int iter = 0; iter < 5; iter++) + for (int iter = 0; iter < 5; iter++) // NOSONAR { nvcv::Tensor imgIn = nvcv::util::CreateTensor(1, 320, 240, nvcv::FMT_RGB8); nvcv::Tensor imgOut = nvcv::util::CreateTensor(1, 320, 240, nvcv::FMT_RGB8); @@ -307,7 +307,7 @@ TEST(OpBoxBlur_Smoke, memory_management) boxes.push_back(box); blurBoxVec.push_back(boxes); - std::shared_ptr blurBoxes = std::make_shared(blurBoxVec); + auto blurBoxes = std::make_shared(blurBoxVec); EXPECT_NO_THROW(op(stream, imgIn, imgOut, (NVCVBlurBoxesI)blurBoxes.get())); EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); @@ -331,7 +331,7 @@ TEST(OpBoxBlur_Smoke, edge_cases) std::vector> blurBoxVec; std::vector boxes; // Empty blurBoxVec.push_back(boxes); - std::shared_ptr blurBoxes = std::make_shared(blurBoxVec); + auto blurBoxes = std::make_shared(blurBoxVec); EXPECT_NO_THROW(op(stream, imgIn, imgOut, (NVCVBlurBoxesI)blurBoxes.get())); } @@ -347,7 +347,7 @@ TEST(OpBoxBlur_Smoke, edge_cases) box.kernelSize = 5; boxes.push_back(box); blurBoxVec.push_back(boxes); - std::shared_ptr blurBoxes = std::make_shared(blurBoxVec); + auto blurBoxes = std::make_shared(blurBoxVec); EXPECT_NO_THROW(op(stream, imgIn, imgOut, (NVCVBlurBoxesI)blurBoxes.get())); } @@ -363,7 +363,7 @@ TEST(OpBoxBlur_Smoke, edge_cases) box.kernelSize = 3; boxes.push_back(box); blurBoxVec.push_back(boxes); - std::shared_ptr blurBoxes = std::make_shared(blurBoxVec); + auto blurBoxes = std::make_shared(blurBoxVec); EXPECT_NO_THROW(op(stream, imgIn, imgOut, (NVCVBlurBoxesI)blurBoxes.get())); } @@ -395,7 +395,7 @@ TEST(OpBoxBlur_Smoke, batch_processing) blurBoxVec.push_back(boxes); } - std::shared_ptr blurBoxes = std::make_shared(blurBoxVec); + auto blurBoxes = std::make_shared(blurBoxVec); cvcuda::BoxBlur op; EXPECT_NO_THROW(op(stream, imgIn, imgOut, (NVCVBlurBoxesI)blurBoxes.get())); diff --git a/tests/cvcuda/system/TestOpBrightnessContrast.cpp b/tests/cvcuda/system/TestOpBrightnessContrast.cpp index 5c4c4d1b8..7b00a6cef 100644 --- a/tests/cvcuda/system/TestOpBrightnessContrast.cpp +++ b/tests/cvcuda/system/TestOpBrightnessContrast.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,9 +15,12 @@ * limitations under the License. */ +#include "PlanarParityUtils.hpp" + #include #include #include +#include #include #include #include @@ -27,6 +30,7 @@ #include #include +#include #include #include #include @@ -41,25 +45,38 @@ template using uniform_distribution = std::conditional_t, std::uniform_int_distribution, std::uniform_real_distribution>; +template +void ComparePixel(std::vector &dst, std::vector &ref, const long4_16a &strides, const int4 &coord, + float tolerance, bool requireExact) +{ + auto dstPixel = test::ValueAt(dst, strides, coord); + auto refPixel = test::ValueAt(ref, strides, coord); + for (int k = 0; k < cuda::NumElements; ++k) + { + if (requireExact) + { + EXPECT_EQ(cuda::GetElement(dstPixel, k), cuda::GetElement(refPixel, k)); + } + else + { + EXPECT_NEAR(cuda::GetElement(dstPixel, k), cuda::GetElement(refPixel, k), tolerance); + } + } +} + template void CompareTensors(std::vector &dst, std::vector &ref, const long4_16a &strides, const int3 &shape, - int numPlanes, float tolerance) + int numPlanes, float tolerance, bool requireExact = false) { + const int numPixels = shape.x * shape.y; for (int z = 0; z < shape.z; ++z) { for (int p = 0; p < numPlanes; p++) { - for (int y = 0; y < shape.y; ++y) + for (int idx = 0; idx < numPixels; ++idx) { - for (int x = 0; x < shape.x; ++x) - { - for (int k = 0; k < cuda::NumElements; ++k) - { - auto val = cuda::GetElement(test::ValueAt(dst, strides, int4{x, y, p, z}), k); - auto ref_val = cuda::GetElement(test::ValueAt(ref, strides, int4{x, y, p, z}), k); - EXPECT_NEAR(val, ref_val, tolerance); - } - } + ComparePixel(dst, ref, strides, int4{idx % shape.x, idx / shape.x, p, z}, tolerance, + requireExact); } } } @@ -86,29 +103,29 @@ Ret GetHalfRange() { if constexpr (std::is_same_v || std::is_same_v) { - return 128.; + return static_cast(128.); } else if constexpr (std::is_same_v) { - return 32768.; + return static_cast(32768.); } else if constexpr (std::is_same_v) { - return 16384.; + return static_cast(16384.); } else if constexpr (std::is_same_v) { - return 2147483648.; + return static_cast(2147483648.); } else if constexpr (std::is_same_v) { - return 1073741824; + return static_cast(1073741824); } else { static_assert(!std::is_integral_v); { - return 0.5; + return static_cast(0.5); } } } @@ -136,8 +153,9 @@ struct Argument std::uniform_real_distribution coeffDist(lo, hi); for (int z = 0; z < numSamples; ++z) { - auto &v = *reinterpret_cast(&m_argVec[m_stride * z]); - v = coeffDist(rng); + const BT value = coeffDist(rng); + const auto offs = static_cast(m_stride) * static_cast(z); + std::memcpy(m_argVec.data() + offs, &value, sizeof(value)); } if (numSamples) @@ -156,11 +174,16 @@ struct Argument } else if (m_numSamples == 1) { - return *reinterpret_cast(&m_argVec[0]); + BT value{}; + std::memcpy(&value, m_argVec.data(), sizeof(value)); + return value; } else { - return *reinterpret_cast(&m_argVec[m_stride * idx]); + BT value{}; + const auto offs = static_cast(m_stride) * static_cast(idx); + std::memcpy(&value, m_argVec.data() + offs, sizeof(value)); + return value; } } @@ -171,35 +194,78 @@ struct Argument std::vector m_argVec = {}; }; +template +struct BrightnessContrastArguments +{ + template + void populate(Rng &rng, const int4 &argCounts, SrcDist &srcRand, DstDist &dstRand) + { + ArgType normalizationFactor = GetHalfRange() / GetHalfRange(); + brightness.populate(rng, argCounts.x, static_cast(0), static_cast(2) * normalizationFactor, + static_cast(1)); + contrast.populate(rng, argCounts.y, static_cast(0), static_cast(2), static_cast(1)); + brightnessShift.populate(rng, argCounts.z, -static_cast(dstRand(rng)) / static_cast(2), + static_cast(dstRand(rng)) / static_cast(2), static_cast(0)); + contrastCenter.populate(rng, argCounts.w, static_cast(0), static_cast(srcRand(rng)), + GetHalfRange()); + } + + Argument brightness; + Argument contrast; + Argument brightnessShift; + Argument contrastCenter; +}; + +template +void ApplyBrightnessContrastPixel(std::vector &src, std::vector &dst, const long4_16a &srcStrides, + const long4_16a &dstStrides, const int4 &coord, ArgT brightness, ArgT contrast, + ArgT brightnessShift, ArgT contrastCenter) +{ + using DstBT = cuda::BaseType; + constexpr int numChannels = cuda::NumElements; + + auto srcPixel = test::ValueAt(src, srcStrides, coord); + auto &outPixel = test::ValueAt(dst, dstStrides, coord); + for (int k = 0; k < numChannels; k++) + { + ArgT v = cuda::GetElement(srcPixel, k); + v = brightness * (contrast * (v - contrastCenter) + contrastCenter) + brightnessShift; + cuda::GetElement(outPixel, k) = cuda::SaturateCast(v); + } +} + template void BrightnessContrast(std::vector &src, std::vector &dst, const long4_16a &srcStrides, const long4_16a &dstStrides, const int3 &shape, int numPlanes, int sampleIdx, ArgT brightness, ArgT contrast, ArgT brightnessShift, ArgT contrastCenter) { - using DstBT = cuda::BaseType; constexpr int numChannels = cuda::NumElements; static_assert(cuda::NumElements == numChannels); + const int numPixels = shape.x * shape.y; for (int p = 0; p < numPlanes; p++) { - for (int y = 0; y < shape.y; ++y) + for (int idx = 0; idx < numPixels; ++idx) { - for (int x = 0; x < shape.x; ++x) - { - int4 coord{x, y, p, sampleIdx}; - auto srcPixel = test::ValueAt(src, srcStrides, coord); - auto &outPixel = test::ValueAt(dst, dstStrides, coord); - for (int k = 0; k < numChannels; k++) - { - ArgT v = cuda::GetElement(srcPixel, k); - v = brightness * (contrast * (v - contrastCenter) + contrastCenter) + brightnessShift; - cuda::GetElement(outPixel, k) = cuda::SaturateCast(v); - } - } + ApplyBrightnessContrastPixel(src, dst, srcStrides, dstStrides, + int4{idx % shape.x, idx / shape.x, p, sampleIdx}, brightness, + contrast, brightnessShift, contrastCenter); } } } +template +void FillRandomPixel(std::vector &src, const Strides &strides, const Coord &coord, Distribution &srcRand, + Rng &rng) +{ + auto &pixel = test::ValueAt(src, strides, coord); + for (int k = 0; k < cuda::NumElements; ++k) + { + using ElementType = std::remove_reference_t; + cuda::GetElement(pixel, k) = static_cast(srcRand(rng)); + } +} + #define NVCV_SHAPE(w, h, n) (int3{w, h, n}) #define NVCV_ARGS_COUNT(brightness, contrast, brightnessShift, contrastCenter) \ (int4{brightness, contrast, brightnessShift, contrastCenter}) @@ -218,9 +284,18 @@ void BrightnessContrast(std::vector &src, std::vector &dst, co NVCV_DETAIL_MAKE_COLOR_FMT1(RGB, UNDEFINED, PL, SIGNED, XYZW, ASSOCIATED, X32_Y32_Z32_W32) #define NVCV_IMAGE_FORMAT_S16 NVCV_DETAIL_MAKE_NONCOLOR_FMT1(PL, SIGNED, X000, ASSOCIATED, X16) +// clang-format off NVCV_TYPED_TEST_SUITE(OpBrightnessContrast, ttype::Types); +// clang-format on + TYPED_TEST(OpBrightnessContrast, correct_output) { const int3 shape = ttype::GetValue; @@ -309,7 +386,7 @@ TYPED_TEST(OpBrightnessContrast, correct_output) int numPlanes = srcAccess->numPlanes(); ASSERT_EQ(numPlanes, dstAccess->numPlanes()); ASSERT_TRUE(numChannels == 1 || numPlanes == 1); - int numSamples = srcAccess->numSamples(); + auto numSamples = static_cast(srcAccess->numSamples()); ASSERT_EQ(numSamples, dstAccess->numSamples()); for (auto argCount : {argCounts.x, argCounts.y, argCounts.z, argCounts.w}) { @@ -350,20 +427,14 @@ TYPED_TEST(OpBrightnessContrast, correct_output) uniform_distribution dstRand(DstBT{0}, std::is_integral_v ? cuda::TypeTraits::max : DstBT{1}); std::mt19937_64 rng(12345); + const int numPixels = shape.x * shape.y; for (int z = 0; z < shape.z; ++z) { for (int p = 0; p < numPlanes; p++) { - for (int y = 0; y < shape.y; ++y) + for (int idx = 0; idx < numPixels; ++idx) { - for (int x = 0; x < shape.x; ++x) - { - auto &pixel = test::ValueAt(srcVec, srcStrides, int4{x, y, p, z}); - for (int k = 0; k < numChannels; ++k) - { - cuda::GetElement(pixel, k) = srcRand(rng); - } - } + FillRandomPixel(srcVec, srcStrides, int4{idx % shape.x, idx / shape.x, p, z}, srcRand, rng); } } } @@ -373,19 +444,12 @@ TYPED_TEST(OpBrightnessContrast, correct_output) cudaStream_t stream; ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); - Argument brightness; - Argument contrast; - Argument brightnessShift; - Argument contrastCenter; - ArgType normalizationFactor = GetHalfRange() / GetHalfRange(); - brightness.populate(rng, argCounts.x, 0., 2. * normalizationFactor, 1.); - contrast.populate(rng, argCounts.y, 0., 2., 1.); - brightnessShift.populate(rng, argCounts.z, -dstRand(rng) / 2, dstRand(rng) / 2, 0.); - contrastCenter.populate(rng, argCounts.w, 0, srcRand(rng), GetHalfRange()); + BrightnessContrastArguments args; + args.template populate(rng, argCounts, srcRand, dstRand); cvcuda::BrightnessContrast op; - ASSERT_NO_THROW(op(stream, srcTensor, dstTensor, brightness.m_argTensor, contrast.m_argTensor, - brightnessShift.m_argTensor, contrastCenter.m_argTensor)); + ASSERT_NO_THROW(op(stream, srcTensor, dstTensor, args.brightness.m_argTensor, args.contrast.m_argTensor, + args.brightnessShift.m_argTensor, args.contrastCenter.m_argTensor)); ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); ASSERT_EQ(cudaSuccess, cudaMemcpy(dstVec.data(), dstData->basePtr(), dstBufSize, cudaMemcpyDeviceToHost)); @@ -393,12 +457,15 @@ TYPED_TEST(OpBrightnessContrast, correct_output) for (int z = 0; z < shape.z; z++) { BrightnessContrast(srcVec, refVec, srcStrides, dstStrides, shape, numPlanes, z, - brightness.GetHostElement(z), contrast.GetHostElement(z), - brightnessShift.GetHostElement(z), contrastCenter.GetHostElement(z)); + args.brightness.GetHostElement(z), args.contrast.GetHostElement(z), + args.brightnessShift.GetHostElement(z), + args.contrastCenter.GetHostElement(z)); } - float absTolerance = std::is_integral_v ? 1 : 1e-5; - CompareTensors(dstVec, refVec, dstStrides, shape, numPlanes, absTolerance); + float absTolerance = std::is_integral_v ? 1.f : 1e-5f; + const bool requireExact = std::is_integral_v && std::is_same_v && argCounts.x == 0 + && argCounts.y == 0 && argCounts.z == 0 && argCounts.w == 0; + CompareTensors(dstVec, refVec, dstStrides, shape, numPlanes, absTolerance, requireExact); ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); } @@ -427,8 +494,8 @@ TYPED_TEST(OpBrightnessContrast, varshape_correct_output) std::vector imgDst; std::vector> srcVec(shape.z); - std::uniform_int_distribution randW(shape.x * 0.5, shape.x * 1.5); - std::uniform_int_distribution randH(shape.y * 0.5, shape.y * 1.5); + std::uniform_int_distribution randW(shape.x / 2, shape.x * 3 / 2); + std::uniform_int_distribution randH(shape.y / 2, shape.y * 3 / 2); uniform_distribution srcRand(SrcBT{0}, std::is_integral_v ? cuda::TypeTraits::max : SrcBT{1}); uniform_distribution dstRand(DstBT{0}, std::is_integral_v ? cuda::TypeTraits::max : DstBT{1}); std::mt19937_64 rng(12345); @@ -466,18 +533,13 @@ TYPED_TEST(OpBrightnessContrast, varshape_correct_output) srcVec[z].resize(srcStrides.x * numPlanes); + const int numPixels = imgSrc[z].size().w * imgSrc[z].size().h; for (int p = 0; p < numPlanes; p++) { - for (int y = 0; y < imgSrc[z].size().h; ++y) + for (int idx = 0; idx < numPixels; ++idx) { - for (int x = 0; x < imgSrc[z].size().w; ++x) - { - for (int k = 0; k < numChannels; ++k) - { - cuda::GetElement(test::ValueAt(srcVec[z], srcStrides, int3{x, y, p}), k) - = srcRand(rng); - } - } + FillRandomPixel(srcVec[z], srcStrides, + int3{idx % imgSrc[z].size().w, idx / imgSrc[z].size().w, p}, srcRand, rng); } } @@ -499,19 +561,12 @@ TYPED_TEST(OpBrightnessContrast, varshape_correct_output) ASSERT_TRUE(argCount == 0 || argCount == 1 || argCount == shape.z); } - Argument brightness; - Argument contrast; - Argument brightnessShift; - Argument contrastCenter; - ArgType normalizationFactor = GetHalfRange() / GetHalfRange(); - brightness.populate(rng, argCounts.x, 0., 2. * normalizationFactor, 1.); - contrast.populate(rng, argCounts.y, 0., 2., 1.); - brightnessShift.populate(rng, argCounts.z, -dstRand(rng) / 2, dstRand(rng) / 2, 0.); - contrastCenter.populate(rng, argCounts.w, 0, srcRand(rng), GetHalfRange()); + BrightnessContrastArguments args; + args.template populate(rng, argCounts, srcRand, dstRand); cvcuda::BrightnessContrast op; - ASSERT_NO_THROW(op(stream, batchSrc, batchDst, brightness.m_argTensor, contrast.m_argTensor, - brightnessShift.m_argTensor, contrastCenter.m_argTensor)); + ASSERT_NO_THROW(op(stream, batchSrc, batchDst, args.brightness.m_argTensor, args.contrast.m_argTensor, + args.brightnessShift.m_argTensor, args.contrastCenter.m_argTensor)); ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); @@ -549,21 +604,218 @@ TYPED_TEST(OpBrightnessContrast, varshape_correct_output) } BrightnessContrast(srcVec[z], refVec, srcStrides, dstStrides, sampleShape, numPlanes, 0, - brightness.GetHostElement(z), contrast.GetHostElement(z), - brightnessShift.GetHostElement(z), contrastCenter.GetHostElement(z)); + args.brightness.GetHostElement(z), args.contrast.GetHostElement(z), + args.brightnessShift.GetHostElement(z), + args.contrastCenter.GetHostElement(z)); + + float absTolerance = std::is_integral_v ? 1.f : 1e-5f; + const bool requireExact = std::is_integral_v && std::is_same_v && argCounts.x == 0 + && argCounts.y == 0 && argCounts.z == 0 && argCounts.w == 0; + CompareTensors(dstVec, refVec, dstStrides, sampleShape, numPlanes, absTolerance, requireExact); + } + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +// ============================================================================= +// Planar (NCHW/CHW) layout support +// +// BrightnessContrast is independent per channel, so a planar input must produce +// the same pixels as the equivalent interleaved input. These cases run identical +// data and per-sample arguments through both layouts and require the +// re-interleaved planar output to match the interleaved output bit-for-bit. +// ============================================================================= - float absTolerance = std::is_integral_v ? 1 : 1e-5; - CompareTensors(dstVec, refVec, dstStrides, sampleShape, numPlanes, absTolerance); +namespace { + +nvcv::Tensor MakeF32ArgTensor(int numSamples, float first, float step) +{ + nvcv::Tensor arg({{numSamples}, "N"}, nvcv::TYPE_F32); + auto argData = arg.exportData(); + EXPECT_TRUE(argData); + if (!argData) + { + return arg; } + const long stride = argData->stride(0); + std::vector host(stride * numSamples, uint8_t{0}); + for (int i = 0; i < numSamples; ++i) + { + const float value = first + step * static_cast(i); + std::memcpy(host.data() + static_cast(stride) * static_cast(i), &value, sizeof(value)); + } + EXPECT_EQ(cudaSuccess, cudaMemcpy(argData->basePtr(), host.data(), host.size(), cudaMemcpyHostToDevice)); + return arg; +} + +struct PlanarBrightnessContrastArgs +{ + nvcv::Tensor brightness; + nvcv::Tensor contrast; + nvcv::Tensor brightnessShift; + nvcv::Tensor contrastCenter; +}; + +PlanarBrightnessContrastArgs MakePlanarBrightnessContrastArgs(int numImages) +{ + return { + MakeF32ArgTensor(numImages, 0.75f, 0.05f), + MakeF32ArgTensor(numImages, 1.25f, -0.03f), + MakeF32ArgTensor(numImages, 3.0f, 1.0f), + MakeF32ArgTensor(numImages, 97.0f, 2.0f), + }; +} + +void RunPlanarParityTensorCase(nvcv::ImageFormat planarFmt, nvcv::ImageFormat interleavedFmt, int width, int height, + int numImages) +{ + auto args = MakePlanarBrightnessContrastArgs(numImages); + test::planar::RunTensorParity( + planarFmt, interleavedFmt, width, height, width, height, numImages, + [&args](cudaStream_t stream, const nvcv::Tensor &src, const nvcv::Tensor &dst, nvcv::ImageFormat) + { + cvcuda::BrightnessContrast op; + EXPECT_NO_THROW( + op(stream, src, dst, args.brightness, args.contrast, args.brightnessShift, args.contrastCenter)); + }); +} + +void RunPlanarParityVarShapeCase(nvcv::ImageFormat planarFmt, nvcv::ImageFormat interleavedFmt, int width, int height, + int numImages) +{ + auto args = MakePlanarBrightnessContrastArgs(numImages); + test::planar::RunVarShapeParity(planarFmt, interleavedFmt, width, height, width, height, numImages, + [&args](cudaStream_t stream, const nvcv::ImageBatchVarShape &src, + const nvcv::ImageBatchVarShape &dst, nvcv::ImageFormat) + { + cvcuda::BrightnessContrast op; + EXPECT_NO_THROW(op(stream, src, dst, args.brightness, args.contrast, + args.brightnessShift, args.contrastCenter)); + }); +} + +} // namespace + +TEST(OpBrightnessContrastScalar, tensor_planar_matches_interleaved) +{ + constexpr double brightness = 0.75; + constexpr double contrast = 1.25; + constexpr double brightnessShift = 0.125; + constexpr double contrastCenter = 0.5; + + test::planar::RunTensorParity( + nvcv::FMT_RGBf32p, nvcv::FMT_RGBf32, 13, 9, 13, 9, 2, + [](cudaStream_t stream, const nvcv::Tensor &src, const nvcv::Tensor &dst, nvcv::ImageFormat) + { + cvcuda::BrightnessContrast op; + op(stream, src, dst, brightness, contrast, brightnessShift, contrastCenter); + }); +} + +TEST(OpBrightnessContrastScalar, varshape_planar_matches_interleaved) +{ + constexpr double brightness = 0.75; + constexpr double contrast = 1.25; + constexpr double brightnessShift = 0.125; + constexpr double contrastCenter = 0.5; + + test::planar::RunVarShapeParity(nvcv::FMT_RGBf32p, nvcv::FMT_RGBf32, 13, 9, 13, 9, 2, + [](cudaStream_t stream, const nvcv::ImageBatchVarShape &src, + const nvcv::ImageBatchVarShape &dst, nvcv::ImageFormat) + { + cvcuda::BrightnessContrast op; + op(stream, src, dst, brightness, contrast, brightnessShift, contrastCenter); + }); +} + +TEST(OpBrightnessContrastScalar, clamp_float_output) +{ + const std::vector srcVec{0.f, 0.25f, 0.75f, 1.f}; + const std::vector expectedUnclamped{-0.25f, 0.25f, 1.25f, 1.75f}; + const std::vector expectedClamped{0.f, 0.25f, 1.f, 1.f}; + + nvcv::Tensor src = nvcv::util::CreateTensor(1, 4, 1, nvcv::FMT_F32); + nvcv::Tensor unclampedDst = nvcv::util::CreateTensor(1, 4, 1, nvcv::FMT_F32); + nvcv::Tensor clampedDst = nvcv::util::CreateTensor(1, 4, 1, nvcv::FMT_F32); + auto srcData = src.exportData(); + auto unclampedData = unclampedDst.exportData(); + auto clampedData = clampedDst.exportData(); + ASSERT_TRUE(srcData && unclampedData && clampedData); + ASSERT_EQ(cudaSuccess, + cudaMemcpy(srcData->basePtr(), srcVec.data(), srcVec.size() * sizeof(float), cudaMemcpyHostToDevice)); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + cvcuda::BrightnessContrast op; + ASSERT_NO_THROW(op(stream, src, unclampedDst, 2., 1., -0.25, 0., false)); + ASSERT_NO_THROW(op(stream, src, clampedDst, 2., 1., -0.25, 0., true)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + std::vector unclamped(srcVec.size()); + std::vector clamped(srcVec.size()); + ASSERT_EQ(cudaSuccess, cudaMemcpy(unclamped.data(), unclampedData->basePtr(), unclamped.size() * sizeof(float), + cudaMemcpyDeviceToHost)); + ASSERT_EQ(cudaSuccess, cudaMemcpy(clamped.data(), clampedData->basePtr(), clamped.size() * sizeof(float), + cudaMemcpyDeviceToHost)); + EXPECT_EQ(expectedUnclamped, unclamped); + EXPECT_EQ(expectedClamped, clamped); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); } +// Parameters: width, height, numImages, planarFmt, interleavedFmt +// clang-format off +NVCV_TEST_SUITE_P(OpBrightnessContrastPlanar, + test::ValueList{ + {64, 48, 2, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + {37, 29, 1, nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8}, + {41, 33, 2, nvcv::FMT_RGBf32p, nvcv::FMT_RGBf32}, + {35, 31, 1, nvcv::FMT_RGBAf32p, nvcv::FMT_RGBAf32}, +}); + +// clang-format on + +TEST_P(OpBrightnessContrastPlanar, tensor_matches_interleaved) +{ + RunPlanarParityTensorCase(GetParamValue<3>(), GetParamValue<4>(), GetParamValue<0>(), GetParamValue<1>(), + GetParamValue<2>()); +} + +TEST_P(OpBrightnessContrastPlanar, varshape_matches_interleaved) +{ + RunPlanarParityVarShapeCase(GetParamValue<3>(), GetParamValue<4>(), GetParamValue<0>(), GetParamValue<1>(), + GetParamValue<2>()); +} + TEST(OpBrightnessContrast_Negative, createWithNullHandle) { EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, cvcudaBrightnessContrastCreate(nullptr)); } +TEST(OpBrightnessContrast_Negative, two_channel_planar_tensor) +{ + nvcv::TensorShape shape{ + {1, 2, 8, 8}, + "NCHW" + }; + nvcv::Tensor src(shape, nvcv::TYPE_U8); + nvcv::Tensor dst(shape, nvcv::TYPE_U8); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + cvcuda::BrightnessContrast op; + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall( + [&op, stream, &src, &dst] { + op(stream, src, dst, nvcv::Tensor{nullptr}, nvcv::Tensor{nullptr}, + nvcv::Tensor{nullptr}, nvcv::Tensor{nullptr}); + })); + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + #define NVCV_NEGATIVE_CASE(SrcShape, DstShape, SrcType, DstType, SrcImgFormat, DstImgFormat, ArgType, ArgCounts) \ ttype::Types, ttype::Value, SrcType, DstType, ttype::Value, \ ttype::Value, ArgType, ttype::Value> @@ -613,23 +865,16 @@ TYPED_TEST(OpBrightnessContrast_Negative, invalid_parameters_src_dst_tensor) cudaStream_t stream; ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); - Argument brightness; - Argument contrast; - Argument brightnessShift; - Argument contrastCenter; - ArgType normalizationFactor = GetHalfRange() / GetHalfRange(); - brightness.populate(rng, argCounts.x, 0., 2. * normalizationFactor, 1.); - contrast.populate(rng, argCounts.y, 0., 2., 1.); - brightnessShift.populate(rng, argCounts.z, -dstRand(rng) / 2, dstRand(rng) / 2, 0.); - contrastCenter.populate(rng, argCounts.w, 0, srcRand(rng), GetHalfRange()); + BrightnessContrastArguments args; + args.template populate(rng, argCounts, srcRand, dstRand); cvcuda::BrightnessContrast op; EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall( - [&] + [&op, &stream, &srcTensor, &dstTensor, &args] { - op(stream, srcTensor, dstTensor, brightness.m_argTensor, - contrast.m_argTensor, brightnessShift.m_argTensor, - contrastCenter.m_argTensor); + op(stream, srcTensor, dstTensor, args.brightness.m_argTensor, + args.contrast.m_argTensor, args.brightnessShift.m_argTensor, + args.contrastCenter.m_argTensor); })); ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); @@ -679,23 +924,16 @@ TYPED_TEST(OpBrightnessContrast_Negative, invalid_parameters_src_dst_varshape) batchSrc.pushBack(imgSrc.begin(), imgSrc.end()); batchDst.pushBack(imgDst.begin(), imgDst.end()); - Argument brightness; - Argument contrast; - Argument brightnessShift; - Argument contrastCenter; - ArgType normalizationFactor = GetHalfRange() / GetHalfRange(); - brightness.populate(rng, argCounts.x, 0., 2. * normalizationFactor, 1.); - contrast.populate(rng, argCounts.y, 0., 2., 1.); - brightnessShift.populate(rng, argCounts.z, -dstRand(rng) / 2, dstRand(rng) / 2, 0.); - contrastCenter.populate(rng, argCounts.w, 0, srcRand(rng), GetHalfRange()); + BrightnessContrastArguments args; + args.template populate(rng, argCounts, srcRand, dstRand); cvcuda::BrightnessContrast op; EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall( - [&] + [&op, &stream, &batchSrc, &batchDst, &args] { - op(stream, batchSrc, batchDst, brightness.m_argTensor, - contrast.m_argTensor, brightnessShift.m_argTensor, - contrastCenter.m_argTensor); + op(stream, batchSrc, batchDst, args.brightness.m_argTensor, + args.contrast.m_argTensor, args.brightnessShift.m_argTensor, + args.contrastCenter.m_argTensor); })); ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); @@ -709,11 +947,8 @@ TEST(OpBrightnessContrast_Negative, different_format_varshape) {nvcv::FMT_RGBA8, nvcv::FMT_RGB8} }; - for (const auto &testCase : extraFmts) + for (const auto &[extraFmtSrc, extraFmtDst] : extraFmts) { - auto extraFmtSrc = testCase.first; - auto extraFmtDst = testCase.second; - const int numSamples = 10; const int x = 32; const int3 shape{x, x, numSamples}; @@ -749,7 +984,8 @@ TEST(OpBrightnessContrast_Negative, different_format_varshape) cvcuda::BrightnessContrast op; EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall( - [&] { op(stream, batchSrc, batchDst, brightness, contrast, brightnessShift, contrastCenter); })); + [&op, &stream, &batchSrc, &batchDst, &brightness, &contrast, &brightnessShift, &contrastCenter] + { op(stream, batchSrc, batchDst, brightness, contrast, brightnessShift, contrastCenter); })); ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); @@ -774,15 +1010,18 @@ TEST(OpBrightnessContrast_Negative, invalid_parameters_tensors) ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); cvcuda::BrightnessContrast op; - auto runOp = [&](nvcv::Tensor &brightness, nvcv::Tensor &contrast, nvcv::Tensor &brightnessShift, - nvcv::Tensor &contrastCenter) + auto runOp = [&op, &stream, &srcTensor, &dstTensor](const nvcv::Tensor &brightness, const nvcv::Tensor &contrast, + const nvcv::Tensor &brightnessShift, + const nvcv::Tensor &contrastCenter) { - EXPECT_EQ( - NVCV_ERROR_INVALID_ARGUMENT, - nvcv::ProtectCall( - [&] { op(stream, srcTensor, dstTensor, brightness, contrast, brightnessShift, contrastCenter); })); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall( + [&op, &stream, &srcTensor, &dstTensor, &brightness, &contrast, &brightnessShift, &contrastCenter] + { op(stream, srcTensor, dstTensor, brightness, contrast, brightnessShift, contrastCenter); })); }; - auto runOpWithInvalidTensor = [&](nvcv::Tensor &invalidTensor) + auto runOpWithInvalidTensor + = [&runOp, &validContrastTensor, &validBrightnessShiftTensor, &validContrastCenterTensor, + &validBrightnessTensor](const nvcv::Tensor &invalidTensor) { runOp(invalidTensor, validContrastTensor, validBrightnessShiftTensor, validContrastCenterTensor); runOp(validBrightnessTensor, invalidTensor, validBrightnessShiftTensor, validContrastCenterTensor); diff --git a/tests/cvcuda/system/TestOpCLAHE.cpp b/tests/cvcuda/system/TestOpCLAHE.cpp new file mode 100644 index 000000000..d6f4a4490 --- /dev/null +++ b/tests/cvcuda/system/TestOpCLAHE.cpp @@ -0,0 +1,423 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "Definitions.hpp" +#include "PlanarParityUtils.hpp" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace test = nvcv::test; +namespace util = nvcv::util; + +namespace { + +constexpr int kBins = 256; +using Histogram = std::array; + +int Reflect101(int p, int len) +{ + if (len <= 1) + { + return 0; + } + while (p >= len) + { + p = 2 * len - p - 2; + } + return p; +} + +Histogram BuildHistogram(const std::vector &src, int width, int height, int x0, int y0, int tileW, int tileH) +{ + Histogram hist{}; + for (int yy = 0; yy < tileH; ++yy) + { + for (int xx = 0; xx < tileW; ++xx) + { + const int sx = Reflect101(x0 + xx, width); + const int sy = Reflect101(y0 + yy, height); + hist[src[sy * width + sx]]++; + } + } + return hist; +} + +void ClipHistogram(Histogram &hist, double clipLimit, int tileArea) +{ + const unsigned int clipCount = std::max((unsigned int)(clipLimit * (double)tileArea / (double)kBins), 1U); + unsigned int excess = 0; + for (auto &v : hist) + { + if (v > clipCount) + { + excess += v - clipCount; + v = clipCount; + } + } + + constexpr unsigned int kBinsU = kBins; + const unsigned int redist = excess / kBinsU; + const unsigned int rem = excess % kBinsU; + for (auto &v : hist) + { + v += redist; + } + + const unsigned int step = rem > 0U ? std::max(kBinsU / rem, 1U) : 1U; + for (unsigned int i = 0; i < rem; ++i) + { + const unsigned int idx = i * step; + if (idx < kBinsU) + { + hist[idx]++; + } + } +} + +void StoreLut(std::vector &luts, const Histogram &hist, int tileIndex, int tileArea) +{ + unsigned int cdf = 0; + for (int i = 0; i < kBins; ++i) + { + cdf += hist[i]; + unsigned int lut = (cdf * 255U + (unsigned int)(tileArea / 2)) / (unsigned int)std::max(tileArea, 1); + lut = std::min(lut, 255U); + luts[tileIndex * kBins + i] = (uint8_t)lut; + } +} + +std::vector RefCLAHE(const std::vector &src, int width, int height, double clipLimit, int tilesX, + int tilesY) +{ + const int padW = (tilesX - (width % tilesX)) % tilesX; + const int padH = (tilesY - (height % tilesY)) % tilesY; + const int extW = width + padW; + const int extH = height + padH; + const int tileW = extW / tilesX; + const int tileH = extH / tilesY; + const int tileArea = tileW * tileH; + const int numTiles = tilesX * tilesY; + + std::vector luts(numTiles * kBins, 0); + for (int ty = 0; ty < tilesY; ++ty) + { + for (int tx = 0; tx < tilesX; ++tx) + { + Histogram hist = BuildHistogram(src, width, height, tx * tileW, ty * tileH, tileW, tileH); + const int tileIndex = ty * tilesX + tx; + ClipHistogram(hist, clipLimit, tileArea); + StoreLut(luts, hist, tileIndex, tileArea); + } + } + + std::vector out(width * height); + for (int y = 0; y < height; ++y) + { + for (int x = 0; x < width; ++x) + { + float gx = (float)x / (float)tileW - 0.5f; + float gy = (float)y / (float)tileH - 0.5f; + gx = std::min(std::max(gx, 0.0f), (float)(tilesX - 1)); + gy = std::min(std::max(gy, 0.0f), (float)(tilesY - 1)); + + const auto tx0 = static_cast(std::floor(gx)); + const auto ty0 = static_cast(std::floor(gy)); + const int tx1 = std::min(tx0 + 1, tilesX - 1); + const int ty1 = std::min(ty0 + 1, tilesY - 1); + const float fx = gx - static_cast(tx0); + const float fy = gy - static_cast(ty0); + + const int v = src[y * width + x]; + const int idx00 = (ty0 * tilesX + tx0) * kBins + v; + const int idx10 = (ty0 * tilesX + tx1) * kBins + v; + const int idx01 = (ty1 * tilesX + tx0) * kBins + v; + const int idx11 = (ty1 * tilesX + tx1) * kBins + v; + + const float w00 = (1.0f - fx) * (1.0f - fy); + const float w10 = fx * (1.0f - fy); + const float w01 = (1.0f - fx) * fy; + const float w11 = fx * fy; + + const float value = w00 * luts[idx00] + w10 * luts[idx10] + w01 * luts[idx01] + w11 * luts[idx11]; + out[y * width + x] = (uint8_t)(value + 0.5f); + } + } + + return out; +} + +void ExpectNearVec(const std::vector &got, const std::vector &gold, int maxAbsDiff) +{ + ASSERT_EQ(got.size(), gold.size()); + for (size_t i = 0; i < got.size(); ++i) + { + const int diff = std::abs((int)got[i] - (int)gold[i]); + EXPECT_LE(diff, maxAbsDiff); + } +} + +void RunTensorLayoutParity(const char *interleavedLayout, const char *planarLayout, int width, int height, int batches, + double clip, int tilesX, int tilesY) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + const bool hasBatch = planarLayout[0] == 'N'; + const int samples = hasBatch ? batches : 1; + + nvcv::Tensor srcI(hasBatch ? nvcv::TensorShape{{batches, height, width, 1}, interleavedLayout} + : nvcv::TensorShape{{height, width, 1}, interleavedLayout}, + nvcv::TYPE_U8); + nvcv::Tensor dstI(hasBatch ? nvcv::TensorShape{{batches, height, width, 1}, interleavedLayout} + : nvcv::TensorShape{{height, width, 1}, interleavedLayout}, + nvcv::TYPE_U8); + nvcv::Tensor srcP(hasBatch ? nvcv::TensorShape{{batches, 1, height, width}, planarLayout} + : nvcv::TensorShape{{1, height, width}, planarLayout}, + nvcv::TYPE_U8); + nvcv::Tensor dstP(hasBatch ? nvcv::TensorShape{{batches, 1, height, width}, planarLayout} + : nvcv::TensorShape{{1, height, width}, planarLayout}, + nvcv::TYPE_U8); + + auto srcIData = srcI.exportData(); + auto dstIData = dstI.exportData(); + auto srcPData = srcP.exportData(); + auto dstPData = dstP.exportData(); + ASSERT_TRUE(srcIData && dstIData && srcPData && dstPData); + + auto srcIAcc = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcIData); + auto dstIAcc = nvcv::TensorDataAccessStridedImagePlanar::Create(*dstIData); + auto srcPAcc = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcPData); + auto dstPAcc = nvcv::TensorDataAccessStridedImagePlanar::Create(*dstPData); + ASSERT_TRUE(srcIAcc && dstIAcc && srcPAcc && dstPAcc); + + for (int i = 0; i < samples; ++i) + { + std::vector src(width * height); + test::planar::FillDeterministicValues(src, static_cast(i) * 101 + 17, nvcv::TYPE_U8); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(srcIAcc->sampleData(i), srcIAcc->rowStride(), src.data(), width, width, + height, cudaMemcpyHostToDevice)); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(srcPAcc->sampleData(i), srcPAcc->rowStride(), src.data(), width, width, + height, cudaMemcpyHostToDevice)); + } + + cvcuda::CLAHE op(samples, tilesX, tilesY); + EXPECT_NO_THROW(op(stream, srcI, dstI, clip)); + EXPECT_NO_THROW(op(stream, srcP, dstP, clip)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + for (int i = 0; i < samples; ++i) + { + SCOPED_TRACE(i); + std::vector gotI(width * height); + std::vector gotP(width * height); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(gotI.data(), width, dstIAcc->sampleData(i), dstIAcc->rowStride(), width, + height, cudaMemcpyDeviceToHost)); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(gotP.data(), width, dstPAcc->sampleData(i), dstPAcc->rowStride(), width, + height, cudaMemcpyDeviceToHost)); + EXPECT_EQ(gotI, gotP); + } + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +} // namespace + +// clang-format off +NVCV_TEST_SUITE_P(OpCLAHE, test::ValueList +{ + // width, height, batches, clip, tilesX, tilesY + { 17, 19, 1, 40.0, 8, 8}, + { 40, 24, 2, 2.0, 8, 8}, + { 101, 67, 2, 40.0, 8, 8}, + { 320, 240, 3, 2.0, 7, 9}, + { 976, 32, 1, 2.0, 8, 8}, +}); + +// clang-format on + +TEST_P(OpCLAHE, tensor_correct_output) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + const int width = GetParamValue<0>(); + const int height = GetParamValue<1>(); + const int batches = GetParamValue<2>(); + const double clip = GetParamValue<3>(); + const int tilesX = GetParamValue<4>(); + const int tilesY = GetParamValue<5>(); + + nvcv::Tensor in = util::CreateTensor(batches, width, height, nvcv::FMT_U8); + nvcv::Tensor out = util::CreateTensor(batches, width, height, nvcv::FMT_U8); + + std::default_random_engine rng(0); + std::uniform_int_distribution rand(0, 255); + for (int i = 0; i < batches; ++i) // NOSONAR + { + std::vector src(width * height); + std::ranges::generate(src, [&rand, &rng] { return (uint8_t)rand(rng); }); + EXPECT_NO_THROW(util::SetImageTensorFromVector(in.exportData(), src, i)); + } + + cvcuda::CLAHE op(batches, tilesX, tilesY); + EXPECT_NO_THROW(op(stream, in, out, clip)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + for (int i = 0; i < batches; ++i) // NOSONAR + { + std::vector src; + std::vector got; + EXPECT_NO_THROW(util::GetImageVectorFromTensor(in.exportData(), i, src)); + EXPECT_NO_THROW(util::GetImageVectorFromTensor(out.exportData(), i, got)); + const auto gold = RefCLAHE(src, width, height, clip, tilesX, tilesY); + // GPU FMA contraction in the bilinear interpolation can differ from the CPU reference by one LSB. + ExpectNearVec(got, gold, 1); + } + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpCLAHE, varshape_correct_output) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + const int batches = 4; + const double clip = 40.0; + const int tilesX = 8; + const int tilesY = 8; + + std::vector srcImages; + std::vector dstImages; + std::vector> srcVec(batches); + + std::default_random_engine rng(0); + std::uniform_int_distribution randVal(0, 255); + std::uniform_int_distribution randW(31, 97); + std::uniform_int_distribution randH(29, 111); + + for (int i = 0; i < batches; ++i) + { + nvcv::Size2D size{randW(rng), randH(rng)}; + srcImages.emplace_back(size, nvcv::FMT_U8); + dstImages.emplace_back(size, nvcv::FMT_U8); + + srcVec[i].resize(size.w * size.h); + std::ranges::generate(srcVec[i], [&randVal, &rng] { return (uint8_t)randVal(rng); }); + + auto srcData = srcImages.back().exportData(); + ASSERT_NE(srcData, nvcv::NullOpt); + ASSERT_EQ(cudaSuccess, + cudaMemcpy2DAsync(srcData->plane(0).basePtr, srcData->plane(0).rowStride, srcVec[i].data(), size.w, + size.w, size.h, cudaMemcpyHostToDevice, stream)); + } + + nvcv::ImageBatchVarShape srcBatch(batches); + + nvcv::ImageBatchVarShape dstBatch(batches); + srcBatch.pushBack(srcImages.begin(), srcImages.end()); + dstBatch.pushBack(dstImages.begin(), dstImages.end()); + + cvcuda::CLAHE op(batches, tilesX, tilesY); + EXPECT_NO_THROW(op(stream, srcBatch, dstBatch, clip)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + for (int i = 0; i < batches; ++i) + { + auto dstData = dstImages[i].exportData(); + ASSERT_NE(dstData, nvcv::NullOpt); + + const int width = dstImages[i].size().w; + const int height = dstImages[i].size().h; + std::vector got(width * height); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(got.data(), width, dstData->plane(0).basePtr, dstData->plane(0).rowStride, + width, height, cudaMemcpyDeviceToHost)); + + const auto gold = RefCLAHE(srcVec[i], width, height, clip, tilesX, tilesY); + // GPU FMA contraction in the bilinear interpolation can differ from the CPU reference by one LSB. + ExpectNearVec(got, gold, 1); + } + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpCLAHEPlanar, tensor_nchw_matches_interleaved) +{ + RunTensorLayoutParity("NHWC", "NCHW", 101, 67, 2, 2.0, 7, 9); +} + +TEST(OpCLAHEPlanar, tensor_chw_matches_interleaved) +{ + RunTensorLayoutParity("HWC", "CHW", 37, 29, 1, 40.0, 8, 8); +} + +TEST(OpCLAHEPlanar, varshape_single_plane_matches_interleaved) +{ + test::planar::RunVarShapeParity( + nvcv::FMT_U8, nvcv::FMT_U8, 53, 47, 53, 47, 3, + [](cudaStream_t s, const nvcv::ImageBatchVarShape &src, const nvcv::ImageBatchVarShape &dst, nvcv::ImageFormat) + { + cvcuda::CLAHE op(src.numImages(), 7, 9); + EXPECT_NO_THROW(op(s, src, dst, 2.0)); + }); +} + +TEST(OpCLAHE_Negative, invalid_input_format_or_type) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + cvcuda::CLAHE op(2, 8, 8); + + { + nvcv::Tensor in = util::CreateTensor(2, 32, 32, nvcv::FMT_RGB8); + nvcv::Tensor out = util::CreateTensor(2, 32, 32, nvcv::FMT_RGB8); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&op, &stream, &in, &out] { op(stream, in, out, 40.0); })); + } + { + nvcv::Tensor in = util::CreateTensor(2, 32, 32, nvcv::FMT_F16); + nvcv::Tensor out = util::CreateTensor(2, 32, 32, nvcv::FMT_F16); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&op, &stream, &in, &out] { op(stream, in, out, 40.0); })); + } + { + nvcv::Tensor in = util::CreateTensor(2, 32, 32, nvcv::FMT_U8); + nvcv::Tensor out = util::CreateTensor(2, 32, 32, nvcv::FMT_U8); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&op, &stream, &in, &out] { op(stream, in, out, 0.0); })); + } + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpCLAHE_Negative, create_invalid_arguments) +{ + NVCVOperatorHandle handle; + EXPECT_EQ(cvcudaCLAHECreate(nullptr, 1, 8, 8), NVCV_ERROR_INVALID_ARGUMENT); + EXPECT_EQ(cvcudaCLAHECreate(&handle, 0, 8, 8), NVCV_ERROR_INVALID_ARGUMENT); + EXPECT_EQ(cvcudaCLAHECreate(&handle, 1, 0, 8), NVCV_ERROR_INVALID_ARGUMENT); +} diff --git a/tests/cvcuda/system/TestOpCenterCrop.cpp b/tests/cvcuda/system/TestOpCenterCrop.cpp index 067792012..c248541ed 100644 --- a/tests/cvcuda/system/TestOpCenterCrop.cpp +++ b/tests/cvcuda/system/TestOpCenterCrop.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,22 +16,25 @@ */ #include "Definitions.hpp" +#include "PlanarParityUtils.hpp" #include #include +#include #include #include #include #include +#include +#include #include #include +#include namespace gt = ::testing; namespace test = nvcv::test; -//#define DBG_CROP_RECT - #ifdef DBG_CROP_RECT static void dbgImage(std::vector &in, int rowStride) { @@ -45,7 +48,7 @@ static void dbgImage(std::vector &in, int rowStride) } #endif -// Width is in bytes or pixels.. +// Width is specified in pixels. static void WriteData(const nvcv::TensorDataAccessStridedImagePlanar &data, uint8_t val, int crop_rows, int crop_columns) { @@ -53,12 +56,12 @@ static void WriteData(const nvcv::TensorDataAccessStridedImagePlanar &data, uint EXPECT_LE(crop_columns, data.numCols()); EXPECT_LE(crop_rows, data.numRows()); - int bytesPerChan = data.dtype().bitsPerChannel()[0] / 8; - int bytesPerPixel = data.numChannels() * bytesPerChan; - uint8_t *impPtrTop = (uint8_t *)data.sampleData(0); - uint8_t *impPtr = nullptr; - int numImages = data.numSamples(); - int rowStride = data.rowStride(); + int bytesPerChan = data.dtype().bitsPerChannel()[0] / 8; + int bytesPerPixel = data.numChannels() * bytesPerChan; + auto *impPtrTop = reinterpret_cast(data.sampleData(0)); + std::byte *impPtr = nullptr; + auto numImages = static_cast(data.numSamples()); + auto rowStride = static_cast(data.rowStride()); EXPECT_NE(nullptr, impPtrTop); int top_indices = (data.numRows() - crop_rows) / 2; @@ -74,13 +77,11 @@ static void WriteData(const nvcv::TensorDataAccessStridedImagePlanar &data, uint static void setGoldBuffer(std::vector &vect, const nvcv::TensorDataAccessStridedImagePlanar &data, int crop_rows, int crop_columns, uint8_t val) { - int bytesPerChan = data.dtype().bitsPerChannel()[0] / 8; - int bytesPerPixel = data.numChannels() * bytesPerChan; - //int top_indices = (data.numRows() - crop_rows) / 2; - //int left_indices = (data.numCols() - crop_columns) / 2; - - uint8_t *ptrTop = vect.data(); - for (int img = 0; img < data.numSamples(); img++) + int bytesPerChan = data.dtype().bitsPerChannel()[0] / 8; + int bytesPerPixel = data.numChannels() * bytesPerChan; + uint8_t *ptrTop = vect.data(); + auto numImages = static_cast(data.numSamples()); + for (int img = 0; img < numImages; img++) { uint8_t *ptr = ptrTop + data.sampleStride() * img; for (int i = 0; i < crop_rows; i++) @@ -159,14 +160,15 @@ TEST_P(OpCenterCrop, CenterCrop_packed) auto outAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*outData); ASSERT_TRUE(outAccess); - int inSampleStride = inAccess->numRows() * inAccess->rowStride(); - int outSampleStride = outAccess->numRows() * outAccess->rowStride(); + auto inSampleStride = static_cast(inAccess->numRows() * inAccess->rowStride()); + auto outSampleStride = static_cast(outAccess->numRows() * outAccess->rowStride()); + auto numSamples = static_cast(inAccess->numSamples()); - int inBufSize = inSampleStride * inAccess->numSamples(); - int outBufSize = outSampleStride * outAccess->numSamples(); + size_t inBufSize = inSampleStride * numSamples; + size_t outBufSize = outSampleStride * numSamples; - EXPECT_EQ(cudaSuccess, cudaMemset(inData->basePtr(), 0x00, inSampleStride * inAccess->numSamples())); - EXPECT_EQ(cudaSuccess, cudaMemset(outData->basePtr(), 0x00, outSampleStride * outAccess->numSamples())); + EXPECT_EQ(cudaSuccess, cudaMemset(inData->basePtr(), 0x00, inBufSize)); + EXPECT_EQ(cudaSuccess, cudaMemset(outData->basePtr(), 0x00, outBufSize)); WriteData(*inAccess, cropVal, crop_rows, crop_columns); // write data to be cropped std::vector gold(outBufSize); @@ -195,6 +197,176 @@ TEST_P(OpCenterCrop, CenterCrop_packed) EXPECT_EQ(gold, test); } +namespace { + +size_t ImageByteOffset(int x, int y, int channel, int width, int height, int channels, int bytesPerChannel, bool planar) +{ + const size_t element = planar ? (channel * height + y) * width + x : (y * width + x) * channels + channel; + return element * bytesPerChannel; +} + +std::vector MakeExactOutputInput(int width, int height, int channels, int bytesPerChannel, int seed) +{ + std::vector input(static_cast(width) * height * channels * bytesPerChannel); + for (size_t i = 0; i < input.size(); ++i) + { + input[i] = static_cast((i * 37 + seed * 53 + 11) & 0xff); + } + return input; +} + +std::vector CenterCropReference(const std::vector &input, int inWidth, int inHeight, + int cropWidth, int cropHeight, int channels, int bytesPerChannel, + bool planar) +{ + std::vector output(static_cast(cropWidth) * cropHeight * channels * bytesPerChannel); + const int left = (inWidth - cropWidth) / 2; + const int top = (inHeight - cropHeight) / 2; + + for (int channel = 0; channel < channels; ++channel) + { + for (int y = 0; y < cropHeight; ++y) + { + for (int x = 0; x < cropWidth; ++x) + { + const size_t src + = ImageByteOffset(x + left, y + top, channel, inWidth, inHeight, channels, bytesPerChannel, planar); + const size_t dst + = ImageByteOffset(x, y, channel, cropWidth, cropHeight, channels, bytesPerChannel, planar); + std::copy_n(input.begin() + src, bytesPerChannel, output.begin() + dst); + } + } + } + return output; +} + +void RunExactOutputCase(nvcv::ImageFormat fmt, int inWidth, int inHeight, int crop_columns, int crop_rows, + int numberOfImages) +{ + cudaStream_t stream; + EXPECT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + nvcv::Tensor imgOut = nvcv::util::CreateTensor(numberOfImages, crop_columns, crop_rows, fmt); + nvcv::Tensor imgIn = nvcv::util::CreateTensor(numberOfImages, inWidth, inHeight, fmt); + + auto inData = imgIn.exportData(); + auto outData = imgOut.exportData(); + + ASSERT_NE(nullptr, inData); + ASSERT_NE(nullptr, outData); + + auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*inData); + ASSERT_TRUE(inAccess); + + auto outAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*outData); + ASSERT_TRUE(outAccess); + + const int channels = inAccess->numChannels(); + const int bytesPerChannel = inAccess->dtype().bitsPerChannel()[0] / 8; + const bool planar = inAccess->layout() == NVCV_TENSOR_NCHW || inAccess->layout() == NVCV_TENSOR_CHW; + + std::vector> gold(numberOfImages); + for (int image = 0; image < numberOfImages; ++image) + { + auto input = MakeExactOutputInput(inWidth, inHeight, channels, bytesPerChannel, image); + gold[image] + = CenterCropReference(input, inWidth, inHeight, crop_columns, crop_rows, channels, bytesPerChannel, planar); + nvcv::util::SetImageTensorFromByteVector(*inData, input, image); + } + + cvcuda::CenterCrop cropOp; + EXPECT_NO_THROW(cropOp(stream, imgIn, imgOut, {crop_columns, crop_rows})); + + EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + for (int image = 0; image < numberOfImages; ++image) + { + std::vector test; + nvcv::util::GetImageByteVectorFromTensor(*outData, image, test); + EXPECT_EQ(gold[image], test); + } + + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +} // namespace + +#define NVCV_IMAGE_FORMAT_2U8 NVCV_DETAIL_MAKE_NONCOLOR_FMT1(PL, UNSIGNED, XY00, ASSOCIATED, X8_Y8) + +// clang-format off +NVCV_TEST_SUITE_P(OpCenterCropExactOutput, + test::ValueList{ + { nvcv::FMT_U8, 18, 14, 9, 7, 3}, // NHWC, byte copy, C1, odd remainder + { nvcv::FMT_S8, 19, 15, 11, 9, 2}, // NHWC, signed byte copy + {nvcv::ImageFormat{NVCV_IMAGE_FORMAT_2U8}, 21, 17, 13, 9, 2}, // NHWC, byte C2 fallback + { nvcv::FMT_RGB8, 23, 19, 11, 9, 2}, // NHWC, byte C3 fallback + { nvcv::FMT_RGBA8, 25, 21, 13, 9, 2}, // NHWC, byte C4 fallback + { nvcv::FMT_RGBAf32, 13, 11, 7, 5, 2}, // NHWC, wider-type fallback + { nvcv::FMT_RGB8, 23, 19, 11, 9, 1}, // HWC rank-3 fallback + { nvcv::FMT_RGB8p, 23, 19, 11, 9, 2}, // NCHW, byte copy, C3 + { nvcv::FMT_RGBA8p, 25, 21, 13, 9, 2}, // NCHW, byte copy, C4 + { nvcv::FMT_RGB8p, 23, 19, 11, 9, 1}, // CHW flattened byte copy + { nvcv::FMT_RGBAf32p, 13, 11, 7, 5, 2}, // NCHW, wider-type fallback +}); + +// clang-format on + +#undef NVCV_IMAGE_FORMAT_2U8 + +TEST_P(OpCenterCropExactOutput, matches_host_reference) +{ + RunExactOutputCase(GetParamValue<0>(), GetParamValue<1>(), GetParamValue<2>(), GetParamValue<3>(), + GetParamValue<4>(), GetParamValue<5>()); +} + +// ============================================================================= +// Planar (NCHW/CHW) layout support +// +// CenterCrop copies a centered sub-region of each channel plane independently, so cropping a planar +// image is just copying N*C single-channel planes (the centering offset is the same on every plane). +// These tests feed identical data through cvcuda::CenterCrop in interleaved and planar layout and +// require the (re-interleaved) planar output to match the interleaved output bit-for-bit, for every +// dtype and crop geometry. CenterCrop is tensor-only, so there is no var-shape parity case. +// ============================================================================= + +namespace { + +// Center-crop identical data in interleaved and planar tensor layout; outputs must match bit-for-bit. +// The shared scaffolding (upload/run/download/compare) lives in PlanarParityUtils.hpp; here we only +// bind the CenterCrop call and its crop size. +void RunPlanarParityTensorCase(nvcv::ImageFormat planarFmt, nvcv::ImageFormat interleavedFmt, int inW, int inH, + int cropW, int cropH, int numImages) +{ + test::planar::RunTensorParity( + planarFmt, interleavedFmt, inW, inH, cropW, cropH, numImages, + [cropW, cropH](cudaStream_t s, const nvcv::Tensor &src, const nvcv::Tensor &dst, nvcv::ImageFormat) + { + cvcuda::CenterCrop op; + EXPECT_NO_THROW(op(s, src, dst, {cropW, cropH})); + }); +} + +} // namespace + +// Parameters: inWidth, inHeight, cropWidth, cropHeight, numImages, planarFmt, interleavedFmt +// clang-format off +NVCV_TEST_SUITE_P(OpCenterCropPlanar, + test::ValueList{ + {176, 112, 100, 64, 2, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, // RGB8, even centering, batch + {123, 67, 50, 40, 1, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, // RGB8, odd dims (centering truncation) + { 64, 48, 32, 24, 2, nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8}, // RGBA8 (uchar4 planar tensor) + { 50, 40, 26, 20, 1, nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8}, // RGBA8, interior crop + { 64, 48, 40, 30, 2, nvcv::FMT_RGBAf32p, nvcv::FMT_RGBAf32}, // float planar +}); + +// clang-format on + +TEST_P(OpCenterCropPlanar, tensor_matches_interleaved) +{ + RunPlanarParityTensorCase(GetParamValue<5>(), GetParamValue<6>(), GetParamValue<0>(), GetParamValue<1>(), + GetParamValue<2>(), GetParamValue<3>(), GetParamValue<4>()); +} + // clang-format off NVCV_TEST_SUITE_P(OpCenterCrop_Negative, test::ValueList{ // inFmt, outFmt, width, height, cropCols, cropRows @@ -224,7 +396,7 @@ TEST_P(OpCenterCrop_Negative, op) // run operator cvcuda::CenterCrop cropOp; EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall( - [&] { + [&cropOp, &stream, &imgIn, &imgOut, &crop_columns, &crop_rows] { cropOp(stream, imgIn, imgOut, {crop_columns, crop_rows}); })); EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); @@ -235,3 +407,25 @@ TEST(OpCenterCrop_Negative, create_null_handle) { EXPECT_EQ(cvcudaCenterCropCreate(nullptr), NVCV_ERROR_INVALID_ARGUMENT); } + +TEST(OpCenterCrop_Negative, planar_rejects_two_channels) +{ + cvcuda::CenterCrop cropOp; + test::planar::ExpectPlanarTensorRejected({2, 2, 5, 5}, {2, 2, 2, 2}, + [&cropOp](cudaStream_t s, const nvcv::Tensor &in, const nvcv::Tensor &out) + { + cropOp(s, in, out, {2, 2}); + }); +} + +TEST(OpCenterCrop_Negative, planar_rejects_mismatched_sample_channel_shape) +{ + // Flattening planar tensors as N*C single-channel planes would make these both look like six + // samples. CenterCrop must preserve the original sample/channel contract before flattening. + cvcuda::CenterCrop cropOp; + test::planar::ExpectPlanarTensorRejected({2, 3, 5, 5}, {3, 2, 2, 2}, + [&cropOp](cudaStream_t s, const nvcv::Tensor &in, const nvcv::Tensor &out) + { + cropOp(s, in, out, {2, 2}); + }); +} diff --git a/tests/cvcuda/system/TestOpChannelReorder.cpp b/tests/cvcuda/system/TestOpChannelReorder.cpp index 37caacc0e..eaad1a55c 100644 --- a/tests/cvcuda/system/TestOpChannelReorder.cpp +++ b/tests/cvcuda/system/TestOpChannelReorder.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,28 +16,46 @@ */ #include "Definitions.hpp" +#include "PlanarParityUtils.hpp" #include #include #include #include +#include #include #include #include +#include +#include +#include +#include +#include +#include + namespace test = nvcv::test; +class ChannelReorderOrdersException : public nvcv::Exception +{ +public: + explicit ChannelReorderOrdersException(const char *message) + : nvcv::Exception(nvcv::Status::ERROR_INTERNAL, message) + { + } +}; + class TestOpChannelReorder : public ::testing::Test { protected: - TestOpChannelReorder() {} + TestOpChannelReorder() = default; - ~TestOpChannelReorder() {} + ~TestOpChannelReorder() override = default; void SetUp() override { // clang-format off - inOrders = nvcv::Tensor( + orders() = nvcv::Tensor( { {1, 4}, "NC" @@ -48,43 +66,422 @@ class TestOpChannelReorder : public ::testing::Test void pushDefaultImages() { - in.pushBack(nvcv::Image{ + input().pushBack(nvcv::Image{ nvcv::Size2D{4, 2}, nvcv::FMT_RGBA8 }); - out.pushBack(nvcv::Image{ + output().pushBack(nvcv::Image{ nvcv::Size2D{4, 2}, nvcv::FMT_RGBA8 }); } - nvcv::ImageBatchVarShape in{nvcv::ImageBatchVarShape(2)}, out{nvcv::ImageBatchVarShape(2)}; - nvcv::Tensor inOrders; - cvcuda::ChannelReorder chReorder; + nvcv::ImageBatchVarShape &input() + { + return m_in; + } + + nvcv::ImageBatchVarShape &output() + { + return m_out; + } + + nvcv::Tensor &orders() + { + return m_inOrders; + } + + cvcuda::ChannelReorder &channelReorder() + { + return m_chReorder; + } + +private: + nvcv::ImageBatchVarShape m_in{nvcv::ImageBatchVarShape(2)}; + nvcv::ImageBatchVarShape m_out{nvcv::ImageBatchVarShape(2)}; + nvcv::Tensor m_inOrders; + cvcuda::ChannelReorder m_chReorder; }; +static nvcv::Tensor MakeChannelReorderOrders(int numImages, const std::vector &order) +{ + const auto channels = static_cast(order.size()); + nvcv::Tensor orders( + { + {numImages, channels}, + "NC" + }, + nvcv::TYPE_S32); + + auto orderData = orders.exportData(); + if (!orderData) + { + throw ChannelReorderOrdersException("Failed to export ChannelReorder orders tensor"); + } + + for (int i = 0; i < numImages; ++i) + { + auto *row = orderData->basePtr() + i * orderData->stride(0); + if (cudaSuccess != cudaMemcpy(row, order.data(), channels * sizeof(int), cudaMemcpyHostToDevice)) + { + throw ChannelReorderOrdersException("Failed to upload ChannelReorder orders tensor"); + } + } + + return orders; +} + +static nvcv::Tensor MakeChannelReorderOrders(int numImages, int channels) +{ + std::vector order(channels); + for (int c = 0; c < channels; ++c) + { + order[c] = c; + } + + if (channels >= 3) + { + order[0] = 2; + order[1] = -1; + order[2] = 1; + } + if (channels == 4) + { + order[3] = 3; + } + + return MakeChannelReorderOrders(numImages, order); +} + +static std::vector ReferenceChannelReorder(const std::vector &src, int numPixels, int numSrcChannels, + const std::vector &order) +{ + std::vector dst(static_cast(numPixels) * order.size()); + for (int pixel = 0; pixel < numPixels; ++pixel) + { + for (size_t outChannel = 0; outChannel < order.size(); ++outChannel) + { + const int inChannel = order[outChannel]; + dst[static_cast(pixel) * order.size() + outChannel] + = inChannel < 0 ? 0 : src[static_cast(pixel) * numSrcChannels + inChannel]; + } + } + return dst; +} + +static void RunChannelReorderOrdersGold(nvcv::ImageFormat format, const std::vector &order) +{ + const int channels = format.numChannels(); + const std::array sizes = { + {{5, 3}, {3, 4}} + }; + ASSERT_EQ(static_cast(channels), order.size()); + + std::vector srcImages; + std::vector dstImages; + std::vector> srcValues(sizes.size()); + for (size_t image = 0; image < sizes.size(); ++image) + { + srcImages.emplace_back(sizes[image], format); + dstImages.emplace_back(sizes[image], format); + + auto &values = srcValues[image]; + values.resize(static_cast(sizes[image].w) * sizes[image].h * channels); + for (size_t i = 0; i < values.size(); ++i) + { + values[i] = static_cast((i * 17 + image * 29 + 3) & 0xff); + } + + auto srcData = srcImages.back().exportData(); + ASSERT_TRUE(srcData); + const size_t rowBytes = static_cast(sizes[image].w) * channels; + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(srcData->plane(0).basePtr, srcData->plane(0).rowStride, values.data(), + rowBytes, rowBytes, sizes[image].h, cudaMemcpyHostToDevice)); + } + + nvcv::ImageBatchVarShape srcBatch(sizes.size()); + nvcv::ImageBatchVarShape dstBatch(sizes.size()); + srcBatch.pushBack(srcImages.begin(), srcImages.end()); + dstBatch.pushBack(dstImages.begin(), dstImages.end()); + nvcv::Tensor orders = MakeChannelReorderOrders(sizes.size(), order); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + cvcuda::ChannelReorder op; + EXPECT_NO_THROW(op(stream, srcBatch, dstBatch, orders)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + for (size_t image = 0; image < sizes.size(); ++image) + { + auto dstData = dstImages[image].exportData(); + ASSERT_TRUE(dstData); + const size_t rowBytes = static_cast(sizes[image].w) * order.size(); + std::vector got(rowBytes * sizes[image].h); + ASSERT_EQ(cudaSuccess, + cudaMemcpy2D(got.data(), rowBytes, dstData->plane(0).basePtr, dstData->plane(0).rowStride, rowBytes, + sizes[image].h, cudaMemcpyDeviceToHost)); + + const auto gold = ReferenceChannelReorder(srcValues[image], sizes[image].w * sizes[image].h, channels, order); + EXPECT_EQ(gold, got); + } + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpChannelReorderVarShape, varshape_correct_output_rgb) +{ + RunChannelReorderOrdersGold(nvcv::FMT_RGB8, {2, -1, 1}); +} + +TEST(OpChannelReorderVarShape, varshape_correct_output_rgba) +{ + RunChannelReorderOrdersGold(nvcv::FMT_RGBA8, {2, -1, 1, 3}); +} + +static nvcv::TensorShape MakeChannelReorderTensorShape(nvcv::TensorLayout layout, int numSamples, int width, int height, + int channels) +{ + if (layout == nvcv::TENSOR_NHWC) + { + return nvcv::TensorShape{ + {numSamples, height, width, channels}, + layout + }; + } + if (layout == nvcv::TENSOR_NCHW) + { + return nvcv::TensorShape{ + {numSamples, channels, height, width}, + layout + }; + } + if (layout == nvcv::TENSOR_HWC) + { + return nvcv::TensorShape{ + {height, width, channels}, + layout + }; + } + return nvcv::TensorShape{ + {channels, height, width}, + layout + }; +} + +static std::vector MakeChannelReorderTensorOrder(int channels) +{ + std::vector order(channels); + std::iota(order.rbegin(), order.rend(), 0); + if (channels >= 3) + { + order[1] = -1; + order[2] = order[0]; // repeated source channel is part of the native contract. + } + return order; +} + +static size_t ChannelReorderTensorIndex(bool planar, int pixel, int channel, int pixelCount, int channels) +{ + if (planar) + { + return static_cast(channel) * pixelCount + pixel; + } + return static_cast(pixel) * channels + channel; +} + +template +static std::vector ReferenceChannelReorderTensor(const std::vector &input, const std::vector &order, + int width, int height, bool planar) +{ + const auto channels = static_cast(order.size()); + const int pixelCount = width * height; + std::vector expected(input.size()); + for (int pixel = 0; pixel < pixelCount; ++pixel) + { + for (int outputChannel = 0; outputChannel < channels; ++outputChannel) + { + const int sourceChannel = order[outputChannel]; + const int inputChannel = std::max(sourceChannel, 0); + const auto outputIndex = ChannelReorderTensorIndex(planar, pixel, outputChannel, pixelCount, channels); + const auto inputIndex = ChannelReorderTensorIndex(planar, pixel, inputChannel, pixelCount, channels); + expected[outputIndex] = sourceChannel < 0 ? T{} : input[inputIndex]; + } + } + return expected; +} + +template +static std::vector> PrepareChannelReorderTensorSamples(nvcv::Tensor &src, + const std::vector &order, int samples, + int width, int height, bool planar) +{ + const auto channels = static_cast(order.size()); + std::vector> gold(samples); + for (int sample = 0; sample < samples; ++sample) + { + std::vector input(static_cast(width) * height * channels); + std::ranges::generate( + input, [index = size_t{0}, sample]() mutable { return static_cast(index++ * 13 + sample * 17 + 1); }); + nvcv::util::SetImageTensorFromVector(src.exportData(), input, sample); + gold[sample] = ReferenceChannelReorderTensor(input, order, width, height, planar); + } + return gold; +} + +template +static void RunChannelReorderTensorCase(nvcv::DataType dtype, nvcv::TensorLayout layout, int channels) +{ + constexpr int numSamples = 2; + constexpr int width = 7; + constexpr int height = 5; + const bool batched = layout == nvcv::TENSOR_NHWC || layout == nvcv::TENSOR_NCHW; + const bool planar = layout == nvcv::TENSOR_CHW || layout == nvcv::TENSOR_NCHW; + + const auto shape = MakeChannelReorderTensorShape(layout, numSamples, width, height, channels); + nvcv::Tensor src(shape, dtype); + nvcv::Tensor dst(shape, dtype); + const int samples = batched ? numSamples : 1; + const auto order = MakeChannelReorderTensorOrder(channels); + const auto gold = PrepareChannelReorderTensorSamples(src, order, samples, width, height, planar); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + cvcuda::ChannelReorder op; + op(stream, src, dst, order.data(), static_cast(order.size())); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + + for (int n = 0; n < samples; ++n) + { + std::vector got; + nvcv::util::GetImageVectorFromTensor(dst.exportData(), n, got); + ASSERT_EQ(gold[n].size() * sizeof(T), got.size() * sizeof(T)); + EXPECT_EQ(0, std::memcmp(gold[n].data(), got.data(), got.size() * sizeof(T))); + } +} + +template +static void RunChannelReorderTensorCases(nvcv::DataType dtype) +{ + const std::array layouts{nvcv::TENSOR_HWC, nvcv::TENSOR_NHWC, nvcv::TENSOR_CHW, + nvcv::TENSOR_NCHW}; + for (nvcv::TensorLayout layout : layouts) + { + const bool planar = layout == nvcv::TENSOR_CHW || layout == nvcv::TENSOR_NCHW; + for (int channels = 1; channels <= 4; ++channels) + { + if (planar && channels == 2) + { + continue; + } + RunChannelReorderTensorCase(dtype, layout, channels); + } + } +} + +TEST(OpChannelReorderTensor, correct_output_all_declared_types_layouts_channels) +{ + RunChannelReorderTensorCases(nvcv::TYPE_U8); + RunChannelReorderTensorCases(nvcv::TYPE_U16); + RunChannelReorderTensorCases(nvcv::TYPE_S16); + RunChannelReorderTensorCases(nvcv::TYPE_S32); + RunChannelReorderTensorCases(nvcv::TYPE_F32); +} + +TEST(OpChannelReorderTensor, preserves_float_bit_patterns) +{ + nvcv::Tensor src( + { + {1, 1, 2, 4}, + "NHWC" + }, + nvcv::TYPE_F32); + nvcv::Tensor dst(src.shape(), src.dtype()); + + const std::array inputBits{0x7FC01234, 0x80000000, 0x7F800000, 0xFF800000, + 0x00000001, 0x3F800000, 0xBF800000, 0x7FA00001}; + std::vector input(inputBits.size()); + std::memcpy(input.data(), inputBits.data(), sizeof(inputBits)); + nvcv::util::SetImageTensorFromVector(src.exportData(), input, 0); + + const std::array order{3, 2, 1, 0}; + cvcuda::ChannelReorder op; + EXPECT_NO_THROW(op(nullptr, src, dst, order.data(), static_cast(order.size()))); + ASSERT_EQ(cudaSuccess, cudaDeviceSynchronize()); + + std::vector got; + nvcv::util::GetImageVectorFromTensor(dst.exportData(), 0, got); + std::array gotBits{}; + ASSERT_EQ(gotBits.size(), got.size()); + std::memcpy(gotBits.data(), got.data(), sizeof(gotBits)); + const std::array expected{inputBits[3], inputBits[2], inputBits[1], inputBits[0], + inputBits[7], inputBits[6], inputBits[5], inputBits[4]}; + EXPECT_EQ(expected, gotBits); +} + +TEST(OpChannelReorderTensor_Negative, invalid_order_and_alias_are_rejected) +{ + nvcv::Tensor src( + { + {1, 5, 7, 3}, + "NHWC" + }, + nvcv::TYPE_U8); + nvcv::Tensor dst( + { + {1, 5, 7, 3}, + "NHWC" + }, + nvcv::TYPE_U8); + cvcuda::ChannelReorder op; + std::array valid{2, 1, 0}; + std::array outOfRange{3, 1, 0}; + + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + cvcudaChannelReorderSubmit(op.handle(), nullptr, src.handle(), dst.handle(), nullptr, 3)); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + cvcudaChannelReorderSubmit(op.handle(), nullptr, src.handle(), dst.handle(), valid.data(), -1)); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + cvcudaChannelReorderSubmit(op.handle(), nullptr, src.handle(), dst.handle(), outOfRange.data(), 3)); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + cvcudaChannelReorderSubmit(op.handle(), nullptr, src.handle(), src.handle(), valid.data(), 3)); + EXPECT_EQ(cudaSuccess, cudaDeviceSynchronize()); +} + +static void RunChannelReorderPlanarParityCase(nvcv::ImageFormat planarFmt, nvcv::ImageFormat interleavedFmt, int width, + int height, int numImages) +{ + cvcuda::ChannelReorder op; + nvcv::Tensor orders = MakeChannelReorderOrders(numImages, planarFmt.numChannels()); + + nvcv::test::planar::RunVarShapeParity(planarFmt, interleavedFmt, width, height, width, height, numImages, + [&op, &orders](cudaStream_t stream, const nvcv::ImageBatchVarShape &src, + const nvcv::ImageBatchVarShape &dst, nvcv::ImageFormat) + { op(stream, src, dst, orders); }); +} + TEST_F(TestOpChannelReorder, smoke_test_works) { // Let's set up input and output images - nvcv::Image inImages[2] = { + std::array inImages = { nvcv::Image{nvcv::Size2D{4, 2}, nvcv::FMT_RGBA8}, nvcv::Image{nvcv::Size2D{4, 2}, nvcv::FMT_RGBA8} }; - nvcv::Image outImages[2] = { + std::array outImages = { nvcv::Image{nvcv::Size2D{4, 2}, nvcv::FMT_BGRA8}, nvcv::Image{nvcv::Size2D{4, 2}, nvcv::FMT_RGBA8} }; - in = nvcv::ImageBatchVarShape(2); - out = nvcv::ImageBatchVarShape(2); + input() = nvcv::ImageBatchVarShape(2); + output() = nvcv::ImageBatchVarShape(2); // Create the input and output varshapes - in.pushBack(inImages[0]); - in.pushBack(inImages[1]); + input().pushBack(inImages[0]); + input().pushBack(inImages[1]); - out.pushBack(outImages[0]); - out.pushBack(outImages[1]); + output().pushBack(outImages[0]); + output().pushBack(outImages[1]); // Populate input images std::vector inImageValues0; @@ -103,7 +500,7 @@ TEST_F(TestOpChannelReorder, smoke_test_works) // Populate the order tensor // clang-format off - inOrders = nvcv::Tensor( + orders() = nvcv::Tensor( { {2, 4}, "NC" @@ -111,22 +508,22 @@ TEST_F(TestOpChannelReorder, smoke_test_works) nvcv::TYPE_S32); // clang-format on - auto inOrderData = inOrders.exportData(); + auto inOrderData = orders().exportData(); std::vector inOrderValues(inOrderData->stride(0) / sizeof(int4)); // N==0 inOrderValues[0] = {2, -1, 1, 3}; - nvcv::util::SetTensorFromVector(inOrders.exportData(), inOrderValues, 0); + nvcv::util::SetTensorFromVector(orders().exportData(), inOrderValues, 0); // N=1 inOrderValues[0] = {3, 2, 1, -1}; - nvcv::util::SetTensorFromVector(inOrders.exportData(), inOrderValues, 1); + nvcv::util::SetTensorFromVector(orders().exportData(), inOrderValues, 1); // Execute operation cudaStream_t stream; ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); - chReorder(stream, in, out, inOrders); + channelReorder()(stream, input(), output(), orders()); ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); @@ -134,18 +531,65 @@ TEST_F(TestOpChannelReorder, smoke_test_works) // Fetch results std::vector outImageValues; - // image0 x order[0] + // First image uses the first channel order. nvcv::util::GetVectorFromTensor(nvcv::TensorWrapImage(outImages[0]).exportData(), 0, outImageValues); EXPECT_EQ(make_uchar4(3, 0, 2, 7), outImageValues[0]); EXPECT_EQ(make_uchar4(2, 0, 3, 9), outImageValues[1]); - // image1 x order[1] + // Second image uses the second channel order. outImageValues.clear(); nvcv::util::GetVectorFromTensor(nvcv::TensorWrapImage(outImages[1]).exportData(), 0, outImageValues); EXPECT_EQ(make_uchar4(4, 1, 2, 0), outImageValues[0]); EXPECT_EQ(make_uchar4(28, 10, 3, 0), outImageValues[1]); } +TEST_F(TestOpChannelReorder, smoke_test_expands_output_channels) +{ + nvcv::Image inImage{ + nvcv::Size2D{2, 1}, + nvcv::FMT_RGB8 + }; + nvcv::Image outImage{ + nvcv::Size2D{2, 1}, + nvcv::FMT_RGBA8 + }; + + input() = nvcv::ImageBatchVarShape(1); + output() = nvcv::ImageBatchVarShape(1); + input().pushBack(inImage); + output().pushBack(outImage); + + std::vector inValues = {1, 2, 3, 4, 5, 6}; + auto inImageData = inImage.exportData(); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(inImageData->plane(0).basePtr, inImageData->plane(0).rowStride, inValues.data(), + 2 * 3, 2 * 3, 1, cudaMemcpyHostToDevice)); + + std::vector outValues(2); + std::ranges::fill(outValues, make_uchar4(99, 99, 99, 99)); + auto outImageData = outImage.exportData(); + ASSERT_EQ(cudaSuccess, + cudaMemcpy2D(outImageData->plane(0).basePtr, outImageData->plane(0).rowStride, outValues.data(), + 2 * sizeof(uchar4), 2 * sizeof(uchar4), 1, cudaMemcpyHostToDevice)); + + orders() = MakeChannelReorderOrders(1, {2, -1, 1, -1}); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + channelReorder()(stream, input(), output(), orders()); + + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + + outValues.clear(); + outValues.resize(2); + ASSERT_EQ(cudaSuccess, + cudaMemcpy2D(outValues.data(), 2 * sizeof(uchar4), outImageData->plane(0).basePtr, + outImageData->plane(0).rowStride, 2 * sizeof(uchar4), 1, cudaMemcpyDeviceToHost)); + EXPECT_EQ(make_uchar4(3, 0, 2, 0), outValues[0]); + EXPECT_EQ(make_uchar4(6, 0, 5, 0), outValues[1]); +} + TEST_F(TestOpChannelReorder, create_with_null_handle) { EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, cvcudaChannelReorderCreate(nullptr)); @@ -153,44 +597,47 @@ TEST_F(TestOpChannelReorder, create_with_null_handle) TEST_F(TestOpChannelReorder, infer_different_samples) { - in.pushBack(nvcv::Image{ + input().pushBack(nvcv::Image{ nvcv::Size2D{4, 2}, nvcv::FMT_RGBA8 }); - EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&] { chReorder(NULL, in, out, inOrders); })); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([this] { channelReorder()(nullptr, input(), output(), orders()); })); } TEST_F(TestOpChannelReorder, infer_void_samples) { - EXPECT_EQ(NVCV_SUCCESS, nvcv::ProtectCall([&] { chReorder(NULL, in, out, inOrders); })); + EXPECT_EQ(NVCV_SUCCESS, nvcv::ProtectCall([this] { channelReorder()(nullptr, input(), output(), orders()); })); } TEST_F(TestOpChannelReorder, infer_invalid_input_dataType) { - in.pushBack(nvcv::Image{ + input().pushBack(nvcv::Image{ nvcv::Size2D{4, 2}, nvcv::FMT_RGBAf16 }); - out.pushBack(nvcv::Image{ + output().pushBack(nvcv::Image{ nvcv::Size2D{4, 2}, nvcv::FMT_RGBA8 }); - EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&] { chReorder(NULL, in, out, inOrders); })); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([this] { channelReorder()(nullptr, input(), output(), orders()); })); } TEST_F(TestOpChannelReorder, infer_invalid_output_dataType) { - in.pushBack(nvcv::Image{ + input().pushBack(nvcv::Image{ nvcv::Size2D{4, 2}, nvcv::FMT_RGBA8 }); - out.pushBack(nvcv::Image{ + output().pushBack(nvcv::Image{ nvcv::Size2D{4, 2}, nvcv::FMT_RGBAf16 }); - EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&] { chReorder(NULL, in, out, inOrders); })); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([this] { channelReorder()(nullptr, input(), output(), orders()); })); } TEST_F(TestOpChannelReorder, infer_invalid_order_rank) @@ -198,7 +645,7 @@ TEST_F(TestOpChannelReorder, infer_invalid_order_rank) pushDefaultImages(); // clang-format off - inOrders= nvcv::Tensor( + orders() = nvcv::Tensor( { {1, 4, 4}, "NHW" @@ -206,7 +653,8 @@ TEST_F(TestOpChannelReorder, infer_invalid_order_rank) nvcv::TYPE_S32); // clang-format on - EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&] { chReorder(NULL, in, out, inOrders); })); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([this] { channelReorder()(nullptr, input(), output(), orders()); })); } TEST_F(TestOpChannelReorder, infer_invalid_order_dataType) @@ -214,7 +662,7 @@ TEST_F(TestOpChannelReorder, infer_invalid_order_dataType) pushDefaultImages(); // clang-format off - inOrders= nvcv::Tensor( + orders() = nvcv::Tensor( { {1, 4}, "NC" @@ -222,7 +670,8 @@ TEST_F(TestOpChannelReorder, infer_invalid_order_dataType) nvcv::TYPE_F32); // clang-format on - EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&] { chReorder(NULL, in, out, inOrders); })); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([this] { channelReorder()(nullptr, input(), output(), orders()); })); } TEST_F(TestOpChannelReorder, infer_invalid_order_first_label) @@ -230,7 +679,7 @@ TEST_F(TestOpChannelReorder, infer_invalid_order_first_label) pushDefaultImages(); // clang-format off - inOrders= nvcv::Tensor( + orders() = nvcv::Tensor( { {4, 1}, "CN" @@ -238,7 +687,8 @@ TEST_F(TestOpChannelReorder, infer_invalid_order_first_label) nvcv::TYPE_S32); // clang-format on - EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&] { chReorder(NULL, in, out, inOrders); })); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([this] { channelReorder()(nullptr, input(), output(), orders()); })); } TEST_F(TestOpChannelReorder, infer_invalid_order_num_samples) @@ -246,7 +696,7 @@ TEST_F(TestOpChannelReorder, infer_invalid_order_num_samples) pushDefaultImages(); // clang-format off - inOrders= nvcv::Tensor( + orders() = nvcv::Tensor( { {2, 4}, "NC" @@ -254,7 +704,8 @@ TEST_F(TestOpChannelReorder, infer_invalid_order_num_samples) nvcv::TYPE_S32); // clang-format on - EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&] { chReorder(NULL, in, out, inOrders); })); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([this] { channelReorder()(nullptr, input(), output(), orders()); })); } TEST_F(TestOpChannelReorder, infer_invalid_order_num_channels) @@ -262,7 +713,7 @@ TEST_F(TestOpChannelReorder, infer_invalid_order_num_channels) pushDefaultImages(); // clang-format off - inOrders= nvcv::Tensor( + orders() = nvcv::Tensor( { {1, 5}, "NC" @@ -270,7 +721,8 @@ TEST_F(TestOpChannelReorder, infer_invalid_order_num_channels) nvcv::TYPE_S32); // clang-format on - EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&] { chReorder(NULL, in, out, inOrders); })); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([this] { channelReorder()(nullptr, input(), output(), orders()); })); } TEST_F(TestOpChannelReorder, infer_invalid_order_small_num_channels) @@ -278,7 +730,7 @@ TEST_F(TestOpChannelReorder, infer_invalid_order_small_num_channels) pushDefaultImages(); // clang-format off - inOrders= nvcv::Tensor( + orders() = nvcv::Tensor( { {1, 3}, "NC" @@ -286,51 +738,72 @@ TEST_F(TestOpChannelReorder, infer_invalid_order_small_num_channels) nvcv::TYPE_S32); // clang-format on - EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&] { chReorder(NULL, in, out, inOrders); })); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([this] { channelReorder()(nullptr, input(), output(), orders()); })); +} + +// Parameters: width, height, numImages, planar format, interleaved format +// clang-format off +NVCV_TEST_SUITE_P(OpChannelReorderPlanar, + test::ValueList{ + {37, 29, 2, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + {31, 23, 1, nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8}, + {35, 27, 2, nvcv::FMT_RGBf32p, nvcv::FMT_RGBf32}, + {33, 25, 1, nvcv::FMT_RGBAf32p, nvcv::FMT_RGBAf32}, +}); + +// clang-format on + +TEST_P(OpChannelReorderPlanar, varshape_matches_interleaved) +{ + RunChannelReorderPlanarParityCase(GetParamValue<3>(), GetParamValue<4>(), GetParamValue<0>(), GetParamValue<1>(), + GetParamValue<2>()); } -TEST_F(TestOpChannelReorder, infer_invalid_input_planar) +TEST_F(TestOpChannelReorder, infer_invalid_input_output_layout_mismatch_planar_to_interleaved) { - in.pushBack(nvcv::Image{ + input().pushBack(nvcv::Image{ nvcv::Size2D{4, 2}, nvcv::FMT_BGRA8p }); - out.pushBack(nvcv::Image{ + output().pushBack(nvcv::Image{ nvcv::Size2D{4, 2}, nvcv::FMT_BGRA8 }); - EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&] { chReorder(NULL, in, out, inOrders); })); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([this] { channelReorder()(nullptr, input(), output(), orders()); })); } -TEST_F(TestOpChannelReorder, infer_invalid_output_planar) +TEST_F(TestOpChannelReorder, infer_invalid_input_output_layout_mismatch_interleaved_to_planar) { - in.pushBack(nvcv::Image{ + input().pushBack(nvcv::Image{ nvcv::Size2D{4, 2}, nvcv::FMT_BGRA8 }); - out.pushBack(nvcv::Image{ + output().pushBack(nvcv::Image{ nvcv::Size2D{4, 2}, nvcv::FMT_BGRA8p }); - EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&] { chReorder(NULL, in, out, inOrders); })); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([this] { channelReorder()(nullptr, input(), output(), orders()); })); } TEST_F(TestOpChannelReorder, infer_invalid_input_different_channels) { pushDefaultImages(); - in.pushBack(nvcv::Image{ + input().pushBack(nvcv::Image{ nvcv::Size2D{4, 2}, nvcv::FMT_BGR8 }); - out.pushBack(nvcv::Image{ + output().pushBack(nvcv::Image{ nvcv::Size2D{4, 2}, nvcv::FMT_BGR8 }); // clang-format off - inOrders= nvcv::Tensor( + orders() = nvcv::Tensor( { {2, 4}, "NC" @@ -338,23 +811,24 @@ TEST_F(TestOpChannelReorder, infer_invalid_input_different_channels) nvcv::TYPE_S32); // clang-format on - EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&] { chReorder(NULL, in, out, inOrders); })); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([this] { channelReorder()(nullptr, input(), output(), orders()); })); } TEST_F(TestOpChannelReorder, infer_invalid_input_different_format) { pushDefaultImages(); - in.pushBack(nvcv::Image{ + input().pushBack(nvcv::Image{ nvcv::Size2D{4, 2}, nvcv::FMT_BGRAf32 }); - out.pushBack(nvcv::Image{ + output().pushBack(nvcv::Image{ nvcv::Size2D{4, 2}, nvcv::FMT_BGRAf32 }); // clang-format off - inOrders= nvcv::Tensor( + orders() = nvcv::Tensor( { {2, 4}, "NC" @@ -362,5 +836,6 @@ TEST_F(TestOpChannelReorder, infer_invalid_input_different_format) nvcv::TYPE_S32); // clang-format on - EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&] { chReorder(NULL, in, out, inOrders); })); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([this] { channelReorder()(nullptr, input(), output(), orders()); })); } diff --git a/tests/cvcuda/system/TestOpColorTwist.cpp b/tests/cvcuda/system/TestOpColorTwist.cpp index 55c4c4a9d..ccc550cc5 100644 --- a/tests/cvcuda/system/TestOpColorTwist.cpp +++ b/tests/cvcuda/system/TestOpColorTwist.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,6 +15,8 @@ * limitations under the License. */ +#include "PlanarParityUtils.hpp" + #include #include #include @@ -37,6 +39,11 @@ namespace cuda = nvcv::cuda; namespace test = nvcv::test; namespace ttype = nvcv::test::type; +static int ScaledSize(int size, double scale) +{ + return static_cast(size * scale); +} + template using Mat = cuda::math::Matrix; @@ -44,27 +51,121 @@ template using uniform_distribution = std::conditional_t, std::uniform_int_distribution, std::uniform_real_distribution>; -template -void ColorTwist(std::vector &src, std::vector &dst, std::vector &twist, const long3 &strides, - const long2 &twistStrides, const int3 &shape, bool usePerSampleTwist) +template +cuda::math::Vector LoadColorTwistInput(ValueType pixel) +{ + cuda::math::Vector in; + for (int k = 0; k < 3; k++) + { + in[k] = cuda::GetElement(pixel, k); + } + in[3] = 1.; + return in; +} + +template +void StoreColorTwistOutput(std::vector &dst, const long3 &strides, const int3 &coord, ValueType pixel, + const cuda::math::Vector &out) { using BT = cuda::BaseType; - using TwistT = cuda::BaseType; constexpr int numChannels = cuda::NumElements; - static_assert(numChannels == 3 || numChannels == 4); + + ValueType &dstPixel = test::ValueAt(dst, strides, coord); + for (int k = 0; k < 3; k++) + { + cuda::GetElement(dstPixel, k) = cuda::SaturateCast(out[k]); + } + for (int k = 3; k < numChannels; k++) + { + cuda::GetElement(dstPixel, k) = cuda::GetElement(pixel, k); + } +} + +template +Mat, 3, 4> LoadTwistMatrix(std::vector &twist, const long2 &twistStrides, + int twistZ) +{ + using TwistT = cuda::BaseType; Mat mix; - for (int z = 0; z < shape.z; ++z) + for (int i = 0; i < 3; i++) { - int twistZ = usePerSampleTwist ? z : 0; - for (int i = 0; i < 3; i++) + auto row = test::ValueAt(twist, twistStrides, int2{i, twistZ}); + for (int j = 0; j < 4; j++) { - auto row = test::ValueAt(twist, twistStrides, int2{i, twistZ}); - for (int j = 0; j < 4; j++) + mix[i][j] = cuda::GetElement(row, j); + } + } + return mix; +} + +template +void CompareTensorPixel(std::vector &dst, std::vector &ref, const long3 &strides, const int3 &coord, + float tolerance) +{ + for (int k = 0; k < cuda::NumElements; ++k) + { + auto val = cuda::GetElement(test::ValueAt(dst, strides, coord), k); + auto ref_val = cuda::GetElement(test::ValueAt(ref, strides, coord), k); + EXPECT_NEAR(val, ref_val, tolerance); + } +} + +template +void FillRandomTensorPixel(std::vector &src, const long3 &strides, const int3 &coord, int numChannels, + Distribution &rand, Rng &rng) +{ + ValueType &pixel = test::ValueAt(src, strides, coord); + for (int k = 0; k < numChannels; ++k) + { + cuda::GetElement(pixel, k) = rand(rng); + } +} + +template +void FillRandomImage(std::vector &src, const long2 &strides, int2 shape, int numChannels, Distribution &rand, + Rng &rng) +{ + for (int y = 0; y < shape.y; ++y) + { + for (int x = 0; x < shape.x; ++x) + { + auto &pixel = test::ValueAt(src, strides, int2{x, y}); + for (int k = 0; k < numChannels; ++k) { - mix[i][j] = cuda::GetElement(row, j); + cuda::GetElement(pixel, k) = rand(rng); } } + } +} + +template +void FillTwistSample(std::vector &twist, const long2 &twistStrides, int y, int numRows, int numCols, + Distribution &coeffDist, Rng &rng) +{ + for (int x = 0; x < numRows; ++x) + { + auto &row = test::ValueAt(twist, twistStrides, int2{x, y}); + for (int k = 0; k < numCols; ++k) + { + cuda::GetElement(row, k) = coeffDist(rng); + } + } +} + +template +void ColorTwist(std::vector &src, std::vector &dst, std::vector &twist, const long3 &strides, + const long2 &twistStrides, const int3 &shape, bool usePerSampleTwist) +{ + using TwistT = cuda::BaseType; + constexpr int numChannels = cuda::NumElements; + static_assert(numChannels == 3 || numChannels == 4); + + for (int z = 0; z < shape.z; ++z) + { + int twistZ = usePerSampleTwist ? z : 0; + Mat mix = LoadTwistMatrix(twist, twistStrides, twistZ); + for (int y = 0; y < shape.y; ++y) { for (int x = 0; x < shape.x; ++x) @@ -72,22 +173,8 @@ void ColorTwist(std::vector &src, std::vector &dst, std::vecto int3 coord{x, y, z}; auto pixel = test::ValueAt(src, strides, coord); - cuda::math::Vector in; - for (int k = 0; k < 3; k++) - { - in[k] = cuda::GetElement(pixel, k); - } - in[3] = 1.; - - cuda::math::Vector out = mix * in; - for (int k = 0; k < 3; k++) - { - cuda::GetElement(test::ValueAt(dst, strides, coord), k) = cuda::SaturateCast(out[k]); - } - for (int k = 3; k < numChannels; k++) - { - cuda::GetElement(test::ValueAt(dst, strides, coord), k) = cuda::GetElement(pixel, k); - } + cuda::math::Vector out = mix * LoadColorTwistInput(pixel); + StoreColorTwistOutput(dst, strides, coord, pixel, out); } } } @@ -103,15 +190,195 @@ void CompareTensors(std::vector &dst, std::vector &ref, const { for (int x = 0; x < shape.x; ++x) { - for (int k = 0; k < cuda::NumElements; ++k) - { - auto val = cuda::GetElement(test::ValueAt(dst, strides, int3{x, y, z}), k); - auto ref_val = cuda::GetElement(test::ValueAt(ref, strides, int3{x, y, z}), k); - EXPECT_NEAR(val, ref_val, tolerance); - } + CompareTensorPixel(dst, ref, strides, int3{x, y, z}, tolerance); + } + } + } +} + +template +void RunColorTwistTensorPlanarParity(const int3 &shape, nvcv::ImageFormat interleavedFmt, nvcv::ImageFormat planarFmt, + bool usePerSampleArgs) +{ + using BT = cuda::BaseType; + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + constexpr int numChannels = cuda::NumElements; + static_assert(numChannels == 3 || numChannels == 4); + ASSERT_EQ(planarFmt.numChannels(), numChannels); + + const int elemSize = sizeof(BT); + const int srcRowStride = shape.x * sizeof(ValueType); + const int srcSampleStr = shape.y * srcRowStride; + + nvcv::Tensor srcInterleaved = nvcv::util::CreateTensor(shape.z, shape.x, shape.y, interleavedFmt); + nvcv::Tensor dstInterleaved = nvcv::util::CreateTensor(shape.z, shape.x, shape.y, interleavedFmt); + nvcv::Tensor srcPlanar = nvcv::util::CreateTensor(shape.z, shape.x, shape.y, planarFmt); + nvcv::Tensor dstPlanar = nvcv::util::CreateTensor(shape.z, shape.x, shape.y, planarFmt); + + auto srcIData = srcInterleaved.exportData(); + auto dstIData = dstInterleaved.exportData(); + auto srcPData = srcPlanar.exportData(); + auto dstPData = dstPlanar.exportData(); + ASSERT_TRUE(srcIData && dstIData && srcPData && dstPData); + + auto srcIAcc = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcIData); + auto dstIAcc = nvcv::TensorDataAccessStridedImagePlanar::Create(*dstIData); + auto srcPAcc = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcPData); + auto dstPAcc = nvcv::TensorDataAccessStridedImagePlanar::Create(*dstPData); + ASSERT_TRUE(srcIAcc && dstIAcc && srcPAcc && dstPAcc); + + uniform_distribution rand(BT{0}, std::is_integral_v ? cuda::TypeTraits::max : BT{1}); + std::mt19937_64 rng(12345); + const long3 hostStrides{srcSampleStr, srcRowStride, sizeof(ValueType)}; + + for (int z = 0; z < shape.z; ++z) + { + std::vector hwc(srcSampleStr, uint8_t{0}); + for (int y = 0; y < shape.y; ++y) + { + for (int x = 0; x < shape.x; ++x) + { + FillRandomTensorPixel(hwc, hostStrides, int3{x, y, 0}, numChannels, rand, rng); } } + + test::planar::UploadInterleavedSample(*srcIAcc, z, hwc, shape.x, shape.y, srcRowStride); + test::planar::UploadPlanarSample( + *srcPAcc, z, test::planar::DeinterleaveToPlanes(hwc, shape.x, shape.y, numChannels, elemSize), shape.x, + shape.y, numChannels, elemSize); } + + ArgHelper arg; + arg.populate(rng, usePerSampleArgs, shape.z); + + cvcuda::ColorTwist op; + ASSERT_NO_THROW(op(stream, srcInterleaved, dstInterleaved, arg.m_twistTensor)); + ASSERT_NO_THROW(op(stream, srcPlanar, dstPlanar, arg.m_twistTensor)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + for (int z = 0; z < shape.z; ++z) + { + SCOPED_TRACE(z); + auto interleavedOut = test::planar::DownloadInterleavedSample(*dstIAcc, z, shape.x, shape.y, srcRowStride); + auto planarOut = test::planar::DownloadPlanarSample(*dstPAcc, z, shape.x, shape.y, numChannels, elemSize); + auto planarAsHwc = test::planar::InterleaveFromPlanes(planarOut, shape.x, shape.y, numChannels, elemSize); + EXPECT_EQ(interleavedOut, planarAsHwc); + } + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +template +void RunColorTwistVarShapePlanarParity(const int3 &shape, nvcv::ImageFormat interleavedFmt, nvcv::ImageFormat planarFmt, + bool usePerSampleArgs) +{ + using BT = cuda::BaseType; + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + constexpr int numChannels = cuda::NumElements; + static_assert(numChannels == 3 || numChannels == 4); + ASSERT_EQ(planarFmt.numChannels(), numChannels); + + const int elemSize = sizeof(BT); + + std::vector imgSrcI; + std::vector imgDstI; + std::vector imgSrcP; + std::vector imgDstP; + std::vector> srcHwc(shape.z); + std::vector sampleSizes(shape.z); + + std::uniform_int_distribution randW(ScaledSize(shape.x, 0.5), ScaledSize(shape.x, 1.5)); + std::uniform_int_distribution randH(ScaledSize(shape.y, 0.5), ScaledSize(shape.y, 1.5)); + uniform_distribution rand(BT{0}, std::is_integral_v ? cuda::TypeTraits::max : BT{1}); + std::mt19937_64 rng(12345); + + for (int z = 0; z < shape.z; ++z) + { + nvcv::Size2D imgShape{randW(rng), randH(rng)}; + sampleSizes[z] = imgShape; + imgSrcI.emplace_back(imgShape, interleavedFmt); + imgDstI.emplace_back(imgShape, interleavedFmt); + imgSrcP.emplace_back(imgShape, planarFmt); + imgDstP.emplace_back(imgShape, planarFmt); + + const int rowStride = imgShape.w * sizeof(ValueType); + srcHwc[z].resize(rowStride * imgShape.h); + FillRandomImage(srcHwc[z], long2{rowStride, sizeof(ValueType)}, int2{imgShape.w, imgShape.h}, + numChannels, rand, rng); + + auto srcIData = imgSrcI[z].exportData(); + ASSERT_NE(srcIData, nvcv::NullOpt); + ASSERT_EQ(srcIData->numPlanes(), 1); + ASSERT_EQ(cudaSuccess, + cudaMemcpy2DAsync(srcIData->plane(0).basePtr, srcIData->plane(0).rowStride, srcHwc[z].data(), + rowStride, rowStride, imgShape.h, cudaMemcpyHostToDevice, stream)); + + auto planes = test::planar::DeinterleaveToPlanes(srcHwc[z], imgShape.w, imgShape.h, numChannels, elemSize); + auto srcPData = imgSrcP[z].exportData(); + const int planeBytes = imgShape.w * imgShape.h * elemSize; + ASSERT_NE(srcPData, nvcv::NullOpt); + ASSERT_EQ(srcPData->numPlanes(), numChannels); + for (int c = 0; c < numChannels; ++c) + { + ASSERT_EQ(cudaSuccess, + cudaMemcpy2DAsync(srcPData->plane(c).basePtr, srcPData->plane(c).rowStride, + planes.data() + c * planeBytes, imgShape.w * elemSize, imgShape.w * elemSize, + imgShape.h, cudaMemcpyHostToDevice, stream)); + } + } + + nvcv::ImageBatchVarShape batchSrcI(shape.z); + nvcv::ImageBatchVarShape batchDstI(shape.z); + nvcv::ImageBatchVarShape batchSrcP(shape.z); + nvcv::ImageBatchVarShape batchDstP(shape.z); + batchSrcI.pushBack(imgSrcI.begin(), imgSrcI.end()); + batchDstI.pushBack(imgDstI.begin(), imgDstI.end()); + batchSrcP.pushBack(imgSrcP.begin(), imgSrcP.end()); + batchDstP.pushBack(imgDstP.begin(), imgDstP.end()); + + ArgHelper arg; + arg.populate(rng, usePerSampleArgs, shape.z); + + cvcuda::ColorTwist op; + ASSERT_NO_THROW(op(stream, batchSrcI, batchDstI, arg.m_twistTensor)); + ASSERT_NO_THROW(op(stream, batchSrcP, batchDstP, arg.m_twistTensor)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + for (int z = 0; z < shape.z; ++z) + { + SCOPED_TRACE(z); + const auto imgShape = sampleSizes[z]; + const int rowStride = imgShape.w * sizeof(ValueType); + + std::vector interleavedOut(rowStride * imgShape.h); + auto dstIData = imgDstI[z].exportData(); + ASSERT_NE(dstIData, nvcv::NullOpt); + EXPECT_EQ(cudaSuccess, + cudaMemcpy2D(interleavedOut.data(), rowStride, dstIData->plane(0).basePtr, + dstIData->plane(0).rowStride, rowStride, imgShape.h, cudaMemcpyDeviceToHost)); + + std::vector planarOut(rowStride * imgShape.h); + auto dstPData = imgDstP[z].exportData(); + const int planeBytes = imgShape.w * imgShape.h * elemSize; + ASSERT_NE(dstPData, nvcv::NullOpt); + for (int c = 0; c < numChannels; ++c) + { + EXPECT_EQ(cudaSuccess, cudaMemcpy2D(planarOut.data() + c * planeBytes, imgShape.w * elemSize, + dstPData->plane(c).basePtr, dstPData->plane(c).rowStride, + imgShape.w * elemSize, imgShape.h, cudaMemcpyDeviceToHost)); + } + + auto planarAsHwc = test::planar::InterleaveFromPlanes(planarOut, imgShape.w, imgShape.h, numChannels, elemSize); + EXPECT_EQ(interleavedOut, planarAsHwc); + } + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); } template @@ -158,17 +425,10 @@ struct TwistMatrixArgument size_t twistBufSize = m_twistStrides.x * numArgs; m_twistVec = std::vector(twistBufSize, uint8_t{0}); - std::uniform_real_distribution coeffDist(-10., 10.); + std::uniform_real_distribution coeffDist(-10.f, 10.f); for (int y = 0; y < numArgs; ++y) { - for (int x = 0; x < numRows; ++x) - { - for (int k = 0; k < numCols; ++k) - { - cuda::GetElement(test::ValueAt(m_twistVec, m_twistStrides, int2{x, y}), k) - = coeffDist(rng); - } - } + FillTwistSample(m_twistVec, m_twistStrides, y, numRows, numCols, coeffDist, rng); } ASSERT_EQ(cudaSuccess, @@ -185,6 +445,10 @@ struct TwistMatrixArgument #define NVCV_TEST_ROW(SrcDstShape, ValueType, ImgFormat, PerSampleArgs, ArgHelper) \ ttype::Types, ValueType, ttype::Value, ttype::Value, ArgHelper> +#define NVCV_PLANAR_TEST_ROW(SrcDstShape, ValueType, InterleavedFmt, PlanarFmt, PerSampleArgs, ArgHelper) \ + ttype::Types, ValueType, ttype::Value, ttype::Value, \ + ttype::Value, ArgHelper> + #define NVCV_IMAGE_FORMAT_RGB16U \ NVCV_DETAIL_MAKE_COLOR_FMT1(RGB, UNDEFINED, PL, UNSIGNED, XYZ1, ASSOCIATED, X16_Y16_Z16) #define NVCV_IMAGE_FORMAT_RGBA16U \ @@ -217,6 +481,26 @@ NVCV_TYPED_TEST_SUITE( NVCV_TEST_ROW(NVCV_SHAPE(101, 32, 5), int3, NVCV_IMAGE_FORMAT_RGB32S, false, TwistMatrixArgument)>); +NVCV_TYPED_TEST_SUITE( + OpColorTwistPlanarTensor, + ttype::Types), + NVCV_PLANAR_TEST_ROW(NVCV_SHAPE(35, 37, 2), uchar4, NVCV_IMAGE_FORMAT_RGBA8, NVCV_IMAGE_FORMAT_RGBA8p, + false, TwistMatrixArgument), + NVCV_PLANAR_TEST_ROW(NVCV_SHAPE(31, 29, 2), float3, NVCV_IMAGE_FORMAT_RGBf32, + NVCV_IMAGE_FORMAT_RGBf32p, true, TwistMatrixArgument), + NVCV_PLANAR_TEST_ROW(NVCV_SHAPE(17, 19, 1), float4, NVCV_IMAGE_FORMAT_RGBAf32, + NVCV_IMAGE_FORMAT_RGBAf32p, false, TwistMatrixArgument)>); + +NVCV_TYPED_TEST_SUITE( + OpColorTwistPlanarVarShape, + ttype::Types), + NVCV_PLANAR_TEST_ROW(NVCV_SHAPE(31, 29, 2), float3, NVCV_IMAGE_FORMAT_RGBf32, + NVCV_IMAGE_FORMAT_RGBf32p, false, TwistMatrixArgument), + NVCV_PLANAR_TEST_ROW(NVCV_SHAPE(17, 19, 2), float4, NVCV_IMAGE_FORMAT_RGBAf32, + NVCV_IMAGE_FORMAT_RGBAf32p, true, TwistMatrixArgument)>); + TYPED_TEST(OpColorTwist, correct_output) { const int3 shape = ttype::GetValue; @@ -240,7 +524,7 @@ TYPED_TEST(OpColorTwist, correct_output) auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcData); ASSERT_TRUE(srcAccess); - int numSamples = srcAccess->numSamples(); + auto numSamples = static_cast(srcAccess->numSamples()); long3 strides{srcAccess->sampleStride(), srcAccess->rowStride(), srcAccess->colStride()}; // if tensor contains multiple samples, make sure x contains sample stride strides.x = (srcData->rank() == 3) ? srcAccess->numRows() * srcAccess->rowStride() : strides.x; @@ -259,10 +543,7 @@ TYPED_TEST(OpColorTwist, correct_output) { for (int x = 0; x < shape.x; ++x) { - for (int k = 0; k < numChannels; ++k) - { - cuda::GetElement(test::ValueAt(srcVec, strides, int3{x, y, z}), k) = rand(rng); - } + FillRandomTensorPixel(srcVec, strides, int3{x, y, z}, numChannels, rand, rng); } } } @@ -284,12 +565,42 @@ TYPED_TEST(OpColorTwist, correct_output) ColorTwist(srcVec, refVec, arg.m_twistVec, strides, arg.m_twistStrides, shape, usePerSampleArgs); - float absTolerance = std::is_integral_v ? 1 : 1e-5; + float absTolerance = std::is_integral_v ? 1.f : 1e-5f; CompareTensors(dstVec, refVec, strides, shape, absTolerance); ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); } +TYPED_TEST(OpColorTwistPlanarTensor, tensor_matches_interleaved) +{ + const int3 shape = ttype::GetValue; + using ValueType = ttype::GetType; + + const nvcv::ImageFormat interleavedFmt{ttype::GetValue}; + const nvcv::ImageFormat planarFmt{ttype::GetValue}; + const bool usePerSampleArgs = ttype::GetValue; + using ArgHelper = ttype::GetType; + using TwistValueType = typename ArgHelper::TwistValueType; + + RunColorTwistTensorPlanarParity(shape, interleavedFmt, planarFmt, + usePerSampleArgs); +} + +TYPED_TEST(OpColorTwistPlanarVarShape, varshape_matches_interleaved) +{ + const int3 shape = ttype::GetValue; + using ValueType = ttype::GetType; + + const nvcv::ImageFormat interleavedFmt{ttype::GetValue}; + const nvcv::ImageFormat planarFmt{ttype::GetValue}; + const bool usePerSampleArgs = ttype::GetValue; + using ArgHelper = ttype::GetType; + using TwistValueType = typename ArgHelper::TwistValueType; + + RunColorTwistVarShapePlanarParity(shape, interleavedFmt, planarFmt, + usePerSampleArgs); +} + TYPED_TEST(OpColorTwist, varshape_correct_output) { const int3 shape = ttype::GetValue; @@ -311,10 +622,10 @@ TYPED_TEST(OpColorTwist, varshape_correct_output) std::vector imgDst; std::vector> srcVec(shape.z); - std::uniform_int_distribution randW(shape.x * 0.5, shape.x * 1.5); - std::uniform_int_distribution randH(shape.y * 0.5, shape.y * 1.5); - uniform_distribution rand(BT{0}, std::is_integral_v ? cuda::TypeTraits::max : BT{1}); - std::mt19937_64 rng(12345); + std::uniform_int_distribution randW(ScaledSize(shape.x, 0.5), ScaledSize(shape.x, 1.5)); + std::uniform_int_distribution randH(ScaledSize(shape.y, 0.5), ScaledSize(shape.y, 1.5)); + uniform_distribution rand(BT{0}, std::is_integral_v ? cuda::TypeTraits::max : BT{1}); + std::mt19937_64 rng(12345); ASSERT_EQ(sizeof(ValueType), imgFormat.planePixelStrideBytes(0)); @@ -327,15 +638,13 @@ TYPED_TEST(OpColorTwist, varshape_correct_output) auto imgData = imgSrc[z].exportData(); ASSERT_NE(imgData, nvcv::NullOpt); - int srcRowStride = imgData->plane(0).rowStride; - long2 srcStrides = long2{srcRowStride, sizeof(ValueType)}; + int srcRowStride = imgData->plane(0).rowStride; + auto srcStrides = long2{srcRowStride, sizeof(ValueType)}; srcVec[z].resize(srcRowStride * imgSrc[z].size().h); - for (int y = 0; y < imgSrc[z].size().h; ++y) - for (int x = 0; x < imgSrc[z].size().w; ++x) - for (int k = 0; k < numChannels; ++k) - cuda::GetElement(test::ValueAt(srcVec[z], srcStrides, int2{x, y}), k) = rand(rng); + FillRandomImage(srcVec[z], srcStrides, int2{imgSrc[z].size().w, imgSrc[z].size().h}, numChannels, + rand, rng); ASSERT_EQ(cudaSuccess, cudaMemcpy2DAsync(imgData->plane(0).basePtr, srcRowStride, srcVec[z].data(), srcRowStride, @@ -385,7 +694,7 @@ TYPED_TEST(OpColorTwist, varshape_correct_output) ColorTwist(srcVec[z], refVec, twistVec, sampleStrides, arg.m_twistStrides, sampleShape, usePerSampleArgs); - float absTolerance = std::is_integral_v ? 1 : 1e-5; + float absTolerance = std::is_integral_v ? 1.f : 1e-5f; CompareTensors(dstVec, refVec, sampleStrides, sampleShape, absTolerance); } @@ -396,20 +705,17 @@ TYPED_TEST(OpColorTwist, varshape_correct_output) NVCV_TEST_SUITE_P(OpColorTwistVarshape_Negative, test::ValueList{ // inFmt, outFmt, inputNumImages, outputNumImages {nvcv::FMT_RGB8, nvcv::FMT_RGB8, 4, 3}, - {nvcv::FMT_RGB8p, nvcv::FMT_RGB8p, 3, 3}, {nvcv::FMT_U8, nvcv::FMT_U8, 3, 3}, }); NVCV_TEST_SUITE_P(OpColorTwist_Negative, test::ValueList{ // inFmt, outFmt, inputSamples, outputSamples, twistDtype, layout, twistShapeSamples, twistShapeRows, twistShapeCols - //{nvcv::FMT_RGB8, nvcv::FMT_RGB8, 3, 3, nvcv::TYPE_4F32, "H", 3, 3, 4}, // Valid case // Invalid src/dst tensors {nvcv::FMT_RGB8, nvcv::FMT_RGB8, 4, 3, nvcv::TYPE_4F32, "H", 3, 3, 4}, {nvcv::FMT_RGB8, nvcv::FMT_U8, 3, 3, nvcv::TYPE_4F32, "H", 3, 3, 4}, {nvcv::FMT_U8, nvcv::FMT_U8, 3, 3, nvcv::TYPE_4F32, "H", 3, 3, 4}, {nvcv::FMT_RGB8, nvcv::FMT_RGBf32, 3, 3, nvcv::TYPE_4F32, "H", 3, 3, 4}, {nvcv::FMT_RGB8, nvcv::FMT_RGB8p, 3, 3, nvcv::TYPE_4F32, "H", 3, 3, 4}, - {nvcv::FMT_RGB8p, nvcv::FMT_RGB8p, 3, 3, nvcv::TYPE_4F32, "H", 3, 3, 4}, // Invalid twist tensor {nvcv::FMT_RGB8, nvcv::FMT_RGB8, 3, 3, nvcv::TYPE_2F32, "H", 3, 3, 4}, {nvcv::FMT_RGB8, nvcv::FMT_RGB8, 3, 3, nvcv::TYPE_4F32, "NHW", 3, 3, 4}, @@ -465,7 +771,8 @@ TEST_P(OpColorTwist_Negative, op) } cvcuda::ColorTwist op; - EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&] { op(stream, srcTensor, dstTensor, twistTensor); })); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&op, &stream, &srcTensor, &dstTensor, &twistTensor] + { op(stream, srcTensor, dstTensor, twistTensor); })); ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); @@ -484,9 +791,9 @@ TEST_P(OpColorTwistVarshape_Negative, op) std::vector imgSrc; std::vector imgDst; - std::uniform_int_distribution randW(24 * 0.5, 24 * 1.5); - std::uniform_int_distribution randH(24 * 0.5, 24 * 1.5); - std::mt19937_64 rng(12345); + std::uniform_int_distribution randW(ScaledSize(24, 0.5), ScaledSize(24, 1.5)); + std::uniform_int_distribution randH(ScaledSize(24, 0.5), ScaledSize(24, 1.5)); + std::mt19937_64 rng(12345); for (int i = 0; i < inputNumImages; ++i) { @@ -509,7 +816,8 @@ TEST_P(OpColorTwistVarshape_Negative, op) }; cvcuda::ColorTwist op; - EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&] { op(stream, batchSrc, batchDst, twistTensor); })); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&op, &stream, &batchSrc, &batchDst, &twistTensor] + { op(stream, batchSrc, batchDst, twistTensor); })); ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); @@ -528,15 +836,13 @@ TEST(OpColorTwistVarshape_Negative, varshape_hasDifferentFormat) { fmt, nvcv::FMT_RGBA8} }; - for (auto testCase : testSet) + for (const auto &[inputFmtExtra, outputFmtExtra] : testSet) { - nvcv::ImageFormat inputFmtExtra = std::get<0>(testCase); - nvcv::ImageFormat outputFmtExtra = std::get<1>(testCase); - - std::vector imgSrc, imgDst; - std::uniform_int_distribution randW(24 * 0.5, 24 * 1.5); - std::uniform_int_distribution randH(24 * 0.5, 24 * 1.5); - std::mt19937_64 rng(12345); + std::vector imgSrc; + std::vector imgDst; + std::uniform_int_distribution randW(ScaledSize(24, 0.5), ScaledSize(24, 1.5)); + std::uniform_int_distribution randH(ScaledSize(24, 0.5), ScaledSize(24, 1.5)); + std::mt19937_64 rng(12345); for (int i = 0; i < numberOfImages - 1; ++i) { @@ -558,7 +864,8 @@ TEST(OpColorTwistVarshape_Negative, varshape_hasDifferentFormat) }; cvcuda::ColorTwist op; - EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&] { op(stream, batchSrc, batchDst, twistTensor); })); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&op, &stream, &batchSrc, &batchDst, &twistTensor] + { op(stream, batchSrc, batchDst, twistTensor); })); } ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); diff --git a/tests/cvcuda/system/TestOpComposite.cpp b/tests/cvcuda/system/TestOpComposite.cpp index abe098a0a..b5aed7c07 100644 --- a/tests/cvcuda/system/TestOpComposite.cpp +++ b/tests/cvcuda/system/TestOpComposite.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -24,6 +24,9 @@ #include #include +#include +#include +#include #include #include @@ -32,8 +35,13 @@ namespace test = nvcv::test; //#define DBG_COMPOSITE 1 +static int ScaledSize(int size, double scale) +{ + return static_cast(size * scale); +} + template -static void print_img(std::vector vec, int rowStride, int height, std::string message) +static void print_img(const std::vector &vec, int rowStride, int height, const std::string &message) { std::cout << std::endl; std::cout << message << std::endl; @@ -55,19 +63,20 @@ static void setGoldBuffer(std::vector &gold, std::vector &fg, { for (int c = 0; c < width; c++) { - int fg_offset = r * inVecRowStride + c * inChannels; - int fgMask_offset = r * fgMaskVecRowStride + c; - int dst_offset = r * outVecRowStride + c * outChannels; - uint8_t *ptrGold = gold.data() + dst_offset; - uint8_t *ptrFg = fg.data() + fg_offset; - uint8_t *ptrBg = bg.data() + fg_offset; - uint8_t *ptrMat = fgMask.data() + fgMask_offset; - uint8_t a = *ptrMat; + int fg_offset = r * inVecRowStride + c * inChannels; + int fgMask_offset = r * fgMaskVecRowStride + c; + int dst_offset = r * outVecRowStride + c * outChannels; + uint8_t *ptrGold = gold.data() + dst_offset; + const uint8_t *ptrFg = fg.data() + fg_offset; + const uint8_t *ptrBg = bg.data() + fg_offset; + const uint8_t *ptrMat = fgMask.data() + fgMask_offset; + uint8_t a = *ptrMat; for (int k = 0; k < inChannels; k++) { - int c0 = ptrBg[k]; - int c1 = ptrFg[k]; - ptrGold[k] = (uint8_t)(((int)c1 - (int)c0) * (int)a * (1.0f / 255.0f) + c0 + 0.5f); + auto c0 = static_cast(ptrBg[k]); + auto c1 = static_cast(ptrFg[k]); + auto alpha = static_cast(a) / 255.f; + ptrGold[k] = static_cast(std::lerp(c0, c1, alpha) + 0.5f); } if (inChannels == 3 && outChannels == 4) { @@ -107,7 +116,8 @@ TEST_P(OpComposite, tensor_correct_output) int outWidth = inWidth; int outHeight = inHeight; - nvcv::ImageFormat inFormat, outFormat; + nvcv::ImageFormat inFormat; + nvcv::ImageFormat outFormat; if (inChannels == 3) inFormat = nvcv::FMT_RGB8; @@ -142,9 +152,8 @@ TEST_P(OpComposite, tensor_correct_output) auto fgMaskAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*fgMaskData); ASSERT_TRUE(fgMaskAccess); - int foregroundBufSize = foregroundAccess->sampleStride() - * foregroundAccess->numSamples(); //img pitch bytes can be more than the image 64, 128, etc - int fgMaskBufSize = fgMaskAccess->sampleStride() * fgMaskAccess->numSamples(); + auto foregroundBufSize = static_cast(foregroundAccess->sampleStride() * foregroundAccess->numSamples()); + auto fgMaskBufSize = static_cast(fgMaskAccess->sampleStride() * fgMaskAccess->numSamples()); EXPECT_EQ(cudaSuccess, cudaMemset(foregroundData->basePtr(), 0x00, foregroundBufSize)); EXPECT_EQ(cudaSuccess, cudaMemset(backgroundData->basePtr(), 0x00, foregroundBufSize)); @@ -166,9 +175,9 @@ TEST_P(OpComposite, tensor_correct_output) std::uniform_int_distribution udist(0, 255); - std::generate(foregroundVec[i].begin(), foregroundVec[i].end(), [&]() { return udist(rng); }); - std::generate(backgroundVec[i].begin(), backgroundVec[i].end(), [&]() { return udist(rng); }); - std::generate(fgMaskVec[i].begin(), fgMaskVec[i].end(), [&]() { return udist(rng); }); + std::ranges::generate(foregroundVec[i], [&udist, &rng]() { return udist(rng); }); + std::ranges::generate(backgroundVec[i], [&udist, &rng]() { return udist(rng); }); + std::ranges::generate(fgMaskVec[i], [&udist, &rng]() { return udist(rng); }); // Copy input data to the GPU ASSERT_EQ(cudaSuccess, @@ -211,7 +220,7 @@ TEST_P(OpComposite, tensor_correct_output) outVecRowStride, outHeight, cudaMemcpyDeviceToHost)); std::vector goldVec(outHeight * outVecRowStride); - std::generate(goldVec.begin(), goldVec.end(), [&]() { return 0; }); + std::ranges::generate(goldVec, []() { return 0; }); // generate gold result setGoldBuffer(goldVec, foregroundVec[i], backgroundVec[i], fgMaskVec[i], inWidth, inHeight, inVecRowStride, @@ -239,7 +248,8 @@ TEST_P(OpComposite, varshape_correct_output) int outChannels = GetParamValue<3>(); int numberOfImages = GetParamValue<4>(); - nvcv::ImageFormat inFormat, outFormat; + nvcv::ImageFormat inFormat; + nvcv::ImageFormat outFormat; nvcv::ImageFormat maskFormat = nvcv::FMT_U8; if (inChannels == 3) @@ -257,8 +267,8 @@ TEST_P(OpComposite, varshape_correct_output) // Create input varshape - std::uniform_int_distribution udistWidth(inWidth * 0.8, inWidth * 1.1); - std::uniform_int_distribution udistHeight(inHeight * 0.8, inHeight * 1.1); + std::uniform_int_distribution udistWidth(ScaledSize(inWidth, 0.8), ScaledSize(inWidth, 1.1)); + std::uniform_int_distribution udistHeight(ScaledSize(inHeight, 0.8), ScaledSize(inHeight, 1.1)); std::vector imgForeground; std::vector imgBackground; @@ -298,9 +308,9 @@ TEST_P(OpComposite, varshape_correct_output) fgMaskVec[i].resize(size.h * fgMaskRowStride); // populate the vector entries - generate(foregroundVec[i].begin(), foregroundVec[i].end(), [&]() { return udist(rng); }); - generate(backgroundVec[i].begin(), backgroundVec[i].end(), [&]() { return udist(rng); }); - generate(fgMaskVec[i].begin(), fgMaskVec[i].end(), [&]() { return udist(rng); }); + std::ranges::generate(foregroundVec[i], [&udist, &rng]() { return udist(rng); }); + std::ranges::generate(backgroundVec[i], [&udist, &rng]() { return udist(rng); }); + std::ranges::generate(fgMaskVec[i], [&udist, &rng]() { return udist(rng); }); auto imgDataForeground = imgForeground[i].exportData(); assert(imgDataForeground); @@ -378,6 +388,348 @@ TEST_P(OpComposite, varshape_correct_output) } } +namespace { + +template +std::vector InterleavedToPlanar(const std::vector &hwc, int width, int height, int channels) +{ + std::vector chw(hwc.size()); + const int pixels = width * height; + for (int p = 0; p < pixels; ++p) + { + for (int c = 0; c < channels; ++c) + { + chw[c * pixels + p] = hwc[p * channels + c]; + } + } + return chw; +} + +template +void UploadTensorInterleaved(const nvcv::TensorDataStridedCuda &tensorData, int sample, const std::vector &hwc, + int width, int height, int channels) +{ + auto access = nvcv::TensorDataAccessStridedImagePlanar::Create(tensorData); + ASSERT_TRUE(access); + + const size_t rowBytes = static_cast(width) * channels * sizeof(T); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(access->sampleData(sample), access->rowStride(), hwc.data(), rowBytes, rowBytes, + height, cudaMemcpyHostToDevice)); +} + +template +void UploadTensorPlanar(const nvcv::TensorDataStridedCuda &tensorData, int sample, const std::vector &chw, int width, + int height, int channels) +{ + auto access = nvcv::TensorDataAccessStridedImagePlanar::Create(tensorData); + ASSERT_TRUE(access); + + const size_t rowBytes = static_cast(width) * sizeof(T); + auto *sampleData = access->sampleData(sample); + for (int c = 0; c < channels; ++c) + { + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(access->chData(c, sampleData), access->rowStride(), + chw.data() + static_cast(c) * width * height, rowBytes, rowBytes, + height, cudaMemcpyHostToDevice)); + } +} + +template +std::vector DownloadTensorInterleaved(const nvcv::TensorDataStridedCuda &tensorData, int sample, int width, + int height, int channels) +{ + auto access = nvcv::TensorDataAccessStridedImagePlanar::Create(tensorData); + if (!access) + { + ADD_FAILURE() << "Expected image-like tensor access"; + return {}; + } + + const size_t rowBytes = static_cast(width) * channels * sizeof(T); + std::vector hwc(static_cast(height) * width * channels); + EXPECT_EQ(cudaSuccess, cudaMemcpy2D(hwc.data(), rowBytes, access->sampleData(sample), access->rowStride(), rowBytes, + height, cudaMemcpyDeviceToHost)); + return hwc; +} + +template +std::vector DownloadTensorPlanar(const nvcv::TensorDataStridedCuda &tensorData, int sample, int width, int height, + int channels) +{ + auto access = nvcv::TensorDataAccessStridedImagePlanar::Create(tensorData); + if (!access) + { + ADD_FAILURE() << "Expected image-like tensor access"; + return {}; + } + + const size_t rowBytes = static_cast(width) * sizeof(T); + std::vector chw(static_cast(height) * width * channels); + auto *sampleData = access->sampleData(sample); + for (int c = 0; c < channels; ++c) + { + EXPECT_EQ(cudaSuccess, cudaMemcpy2D(chw.data() + static_cast(c) * width * height, rowBytes, + access->chData(c, sampleData), access->rowStride(), rowBytes, height, + cudaMemcpyDeviceToHost)); + } + return chw; +} + +template +void UploadImageInterleaved(const nvcv::Image &image, const std::vector &hwc, int width, int height, int channels) +{ + auto data = image.exportData(); + ASSERT_NE(data, nvcv::NullOpt); + + const size_t rowBytes = static_cast(width) * channels * sizeof(T); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(data->plane(0).basePtr, data->plane(0).rowStride, hwc.data(), rowBytes, + rowBytes, height, cudaMemcpyHostToDevice)); +} + +template +void UploadImagePlanar(const nvcv::Image &image, const std::vector &chw, int width, int height, int channels) +{ + auto data = image.exportData(); + ASSERT_NE(data, nvcv::NullOpt); + ASSERT_EQ(data->numPlanes(), channels); + + const size_t rowBytes = static_cast(width) * sizeof(T); + for (int c = 0; c < channels; ++c) + { + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(data->plane(c).basePtr, data->plane(c).rowStride, + chw.data() + static_cast(c) * width * height, rowBytes, rowBytes, + height, cudaMemcpyHostToDevice)); + } +} + +template +std::vector DownloadImageInterleaved(const nvcv::Image &image, int width, int height, int channels) +{ + auto data = image.exportData(); + if (data == nvcv::NullOpt) + { + ADD_FAILURE() << "Expected CUDA-accessible image data"; + return {}; + } + + const size_t rowBytes = static_cast(width) * channels * sizeof(T); + std::vector hwc(static_cast(height) * width * channels); + EXPECT_EQ(cudaSuccess, cudaMemcpy2D(hwc.data(), rowBytes, data->plane(0).basePtr, data->plane(0).rowStride, + rowBytes, height, cudaMemcpyDeviceToHost)); + return hwc; +} + +template +std::vector DownloadImagePlanar(const nvcv::Image &image, int width, int height, int channels) +{ + auto data = image.exportData(); + if (data == nvcv::NullOpt) + { + ADD_FAILURE() << "Expected CUDA-accessible image data"; + return {}; + } + if (data->numPlanes() != channels) + { + ADD_FAILURE() << "Expected " << channels << " planes, got " << data->numPlanes(); + return {}; + } + + const size_t rowBytes = static_cast(width) * sizeof(T); + std::vector chw(static_cast(height) * width * channels); + for (int c = 0; c < channels; ++c) + { + EXPECT_EQ(cudaSuccess, + cudaMemcpy2D(chw.data() + static_cast(c) * width * height, rowBytes, data->plane(c).basePtr, + data->plane(c).rowStride, rowBytes, height, cudaMemcpyDeviceToHost)); + } + return chw; +} + +std::vector MakePatternBytes(size_t count, uint32_t seed) +{ + std::vector values(count); + for (size_t i = 0; i < values.size(); ++i) + { + values[i] = static_cast((i * 37 + seed * 17) & 0xFF); + } + return values; +} + +void RunCompositePlanarTensorParity(nvcv::ImageFormat interleavedOutFormat, nvcv::ImageFormat planarOutFormat, + int width, int height, int numImages, int outChannels) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + nvcv::Tensor fgI(numImages, {width, height}, nvcv::FMT_RGB8); + nvcv::Tensor bgI(numImages, {width, height}, nvcv::FMT_RGB8); + nvcv::Tensor maskI(numImages, {width, height}, nvcv::FMT_U8); + nvcv::Tensor dstI(numImages, {width, height}, interleavedOutFormat); + + nvcv::Tensor fgP(numImages, {width, height}, nvcv::FMT_RGB8p); + nvcv::Tensor bgP(numImages, {width, height}, nvcv::FMT_RGB8p); + nvcv::Tensor maskP(numImages, {width, height}, nvcv::FMT_U8); + nvcv::Tensor dstP(numImages, {width, height}, planarOutFormat); + + auto fgIData = fgI.exportData(); + auto bgIData = bgI.exportData(); + auto maskIData = maskI.exportData(); + auto dstIData = dstI.exportData(); + auto fgPData = fgP.exportData(); + auto bgPData = bgP.exportData(); + auto maskPData = maskP.exportData(); + auto dstPData = dstP.exportData(); + ASSERT_TRUE(fgIData && bgIData && maskIData && dstIData && fgPData && bgPData && maskPData && dstPData); + + for (int sample = 0; sample < numImages; ++sample) + { + const auto sampleSeed = static_cast(sample + 1); + std::vector fg = MakePatternBytes(static_cast(width) * height * 3, sampleSeed); + std::vector bg = MakePatternBytes(static_cast(width) * height * 3, sampleSeed + 11); + std::vector mask = MakePatternBytes(static_cast(width) * height, sampleSeed + 23); + + UploadTensorInterleaved(*fgIData, sample, fg, width, height, 3); + UploadTensorInterleaved(*bgIData, sample, bg, width, height, 3); + UploadTensorInterleaved(*maskIData, sample, mask, width, height, 1); + UploadTensorPlanar(*fgPData, sample, InterleavedToPlanar(fg, width, height, 3), width, height, 3); + UploadTensorPlanar(*bgPData, sample, InterleavedToPlanar(bg, width, height, 3), width, height, 3); + UploadTensorInterleaved(*maskPData, sample, mask, width, height, 1); + } + + cvcuda::Composite compositeOp; + EXPECT_NO_THROW(compositeOp(stream, fgI, bgI, maskI, dstI)); + EXPECT_NO_THROW(compositeOp(stream, fgP, bgP, maskP, dstP)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + for (int sample = 0; sample < numImages; ++sample) + { + SCOPED_TRACE(sample); + const auto sampleSeed = static_cast(sample + 1); + std::vector fg = MakePatternBytes(static_cast(width) * height * 3, sampleSeed); + std::vector bg = MakePatternBytes(static_cast(width) * height * 3, sampleSeed + 11); + std::vector mask = MakePatternBytes(static_cast(width) * height, sampleSeed + 23); + std::vector gold(static_cast(width) * height * outChannels); + setGoldBuffer(gold, fg, bg, mask, width, height, width * 3, width, width * outChannels, 3, outChannels); + + std::vector interleaved + = DownloadTensorInterleaved(*dstIData, sample, width, height, outChannels); + std::vector planar = DownloadTensorPlanar(*dstPData, sample, width, height, outChannels); + + EXPECT_EQ(gold, interleaved); + EXPECT_EQ(InterleavedToPlanar(gold, width, height, outChannels), planar); + EXPECT_EQ(InterleavedToPlanar(interleaved, width, height, outChannels), planar); + } + + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +void RunCompositePlanarVarShapeParity(nvcv::ImageFormat interleavedOutFormat, nvcv::ImageFormat planarOutFormat, + int width, int height, int numImages, int outChannels) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + std::vector fgIImgs; + std::vector bgIImgs; + std::vector maskIImgs; + std::vector dstIImgs; + std::vector fgPImgs; + std::vector bgPImgs; + std::vector maskPImgs; + std::vector dstPImgs; + + for (int sample = 0; sample < numImages; ++sample) + { + const nvcv::Size2D size{width + sample, height + sample}; + + fgIImgs.emplace_back(size, nvcv::FMT_RGB8); + bgIImgs.emplace_back(size, nvcv::FMT_RGB8); + maskIImgs.emplace_back(size, nvcv::FMT_U8); + dstIImgs.emplace_back(size, interleavedOutFormat); + fgPImgs.emplace_back(size, nvcv::FMT_RGB8p); + bgPImgs.emplace_back(size, nvcv::FMT_RGB8p); + maskPImgs.emplace_back(size, nvcv::FMT_U8); + dstPImgs.emplace_back(size, planarOutFormat); + + const auto sampleSeed = static_cast(sample + 1); + std::vector fg = MakePatternBytes(static_cast(size.w) * size.h * 3, sampleSeed); + std::vector bg = MakePatternBytes(static_cast(size.w) * size.h * 3, sampleSeed + 11); + std::vector mask = MakePatternBytes(static_cast(size.w) * size.h, sampleSeed + 23); + + UploadImageInterleaved(fgIImgs[sample], fg, size.w, size.h, 3); + UploadImageInterleaved(bgIImgs[sample], bg, size.w, size.h, 3); + UploadImageInterleaved(maskIImgs[sample], mask, size.w, size.h, 1); + UploadImagePlanar(fgPImgs[sample], InterleavedToPlanar(fg, size.w, size.h, 3), size.w, size.h, 3); + UploadImagePlanar(bgPImgs[sample], InterleavedToPlanar(bg, size.w, size.h, 3), size.w, size.h, 3); + UploadImageInterleaved(maskPImgs[sample], mask, size.w, size.h, 1); + } + + auto makeBatch = [numImages](std::vector &images) + { + nvcv::ImageBatchVarShape batch(numImages); + batch.pushBack(images.begin(), images.end()); + return batch; + }; + + nvcv::ImageBatchVarShape fgI = makeBatch(fgIImgs); + nvcv::ImageBatchVarShape bgI = makeBatch(bgIImgs); + nvcv::ImageBatchVarShape maskI = makeBatch(maskIImgs); + nvcv::ImageBatchVarShape dstI = makeBatch(dstIImgs); + nvcv::ImageBatchVarShape fgP = makeBatch(fgPImgs); + nvcv::ImageBatchVarShape bgP = makeBatch(bgPImgs); + nvcv::ImageBatchVarShape maskP = makeBatch(maskPImgs); + nvcv::ImageBatchVarShape dstP = makeBatch(dstPImgs); + + cvcuda::Composite compositeOp; + EXPECT_NO_THROW(compositeOp(stream, fgI, bgI, maskI, dstI)); + EXPECT_NO_THROW(compositeOp(stream, fgP, bgP, maskP, dstP)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + for (int sample = 0; sample < numImages; ++sample) + { + SCOPED_TRACE(sample); + const nvcv::Size2D size = fgIImgs[sample].size(); + const auto sampleSeed = static_cast(sample + 1); + std::vector fg = MakePatternBytes(static_cast(size.w) * size.h * 3, sampleSeed); + std::vector bg = MakePatternBytes(static_cast(size.w) * size.h * 3, sampleSeed + 11); + std::vector mask = MakePatternBytes(static_cast(size.w) * size.h, sampleSeed + 23); + std::vector gold(static_cast(size.w) * size.h * outChannels); + setGoldBuffer(gold, fg, bg, mask, size.w, size.h, size.w * 3, size.w, size.w * outChannels, 3, outChannels); + + std::vector interleaved + = DownloadImageInterleaved(dstIImgs[sample], size.w, size.h, outChannels); + std::vector planar = DownloadImagePlanar(dstPImgs[sample], size.w, size.h, outChannels); + + EXPECT_EQ(gold, interleaved); + EXPECT_EQ(InterleavedToPlanar(gold, size.w, size.h, outChannels), planar); + EXPECT_EQ(InterleavedToPlanar(interleaved, size.w, size.h, outChannels), planar); + } + + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +} // namespace + +TEST(OpCompositePlanar, tensor_rgb_output_matches_interleaved) +{ + RunCompositePlanarTensorParity(nvcv::FMT_RGB8, nvcv::FMT_RGB8p, 31, 19, 2, 3); +} + +TEST(OpCompositePlanar, tensor_rgba_output_matches_interleaved) +{ + RunCompositePlanarTensorParity(nvcv::FMT_RGBA8, nvcv::FMT_RGBA8p, 29, 17, 2, 4); +} + +TEST(OpCompositePlanar, varshape_rgb_output_matches_interleaved) +{ + RunCompositePlanarVarShapeParity(nvcv::FMT_RGB8, nvcv::FMT_RGB8p, 31, 19, 2, 3); +} + +TEST(OpCompositePlanar, varshape_rgba_output_matches_interleaved) +{ + RunCompositePlanarVarShapeParity(nvcv::FMT_RGBA8, nvcv::FMT_RGBA8p, 29, 17, 2, 4); +} + // clang-format off NVCV_TEST_SUITE_P(OpComposite_Negative, test::ValueList { @@ -403,6 +755,149 @@ TEST(OpComposite_Negative, createWithNullHandle) EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, cvcudaCompositeCreate(nullptr)); } +struct CompositeMismatchTestCase +{ + int foregroundImages; + int backgroundImages; + int maskImages; + int outputImages; + nvcv::Size2D foregroundSize; + nvcv::Size2D backgroundSize; + nvcv::Size2D maskSize; + nvcv::Size2D outputSize; + const char *name; +}; + +static CompositeMismatchTestCase MakeCompositeMismatchTestCase(const char *name) +{ + const nvcv::Size2D size{24, 24}; + return {2, 2, 2, 2, size, size, size, size, name}; +} + +static std::array MakeCompositeMismatchTestCases(bool useSmallerOutputBatch) +{ + const std::array names{ + { + "output_batch", "output_width", + "output_height", "foreground_batch", + "foreground_width", "foreground_height", + "background_batch", "fgMask_batch", + "background_size", "fgMask_size", + } + }; + + std::array testCases{}; + std::ranges::transform(names, testCases.begin(), MakeCompositeMismatchTestCase); + + if (useSmallerOutputBatch) + { + testCases[0].foregroundImages = 3; + testCases[0].backgroundImages = 3; + testCases[0].maskImages = 3; + } + else + { + testCases[0].outputImages = 3; + } + + testCases[1].outputSize = nvcv::Size2D{16, 24}; + testCases[2].outputSize = nvcv::Size2D{24, 16}; + testCases[3].foregroundImages++; + testCases[4].foregroundSize = nvcv::Size2D{16, 24}; + testCases[5].foregroundSize = nvcv::Size2D{24, 16}; + testCases[6].backgroundImages++; + testCases[7].maskImages++; + testCases[8].backgroundSize = nvcv::Size2D{16, 24}; + testCases[9].maskSize = nvcv::Size2D{24, 16}; + + return testCases; +} + +TEST(OpComposite_Negative, tensor_rejects_mismatched_shape) +{ + cudaStream_t stream{}; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + const std::array testCases = MakeCompositeMismatchTestCases(false); + + cvcuda::Composite compositeOp; + + for (const CompositeMismatchTestCase &testCase : testCases) + { + SCOPED_TRACE(testCase.name); + + nvcv::Tensor foregroundImg(testCase.foregroundImages, testCase.foregroundSize, nvcv::FMT_RGB8); + nvcv::Tensor backgroundImg(testCase.backgroundImages, testCase.backgroundSize, nvcv::FMT_RGB8); + nvcv::Tensor fgMaskImg(testCase.maskImages, testCase.maskSize, nvcv::FMT_U8); + nvcv::Tensor outImg(testCase.outputImages, testCase.outputSize, nvcv::FMT_RGB8); + + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&] { compositeOp(stream, foregroundImg, backgroundImg, fgMaskImg, outImg); })); + + EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + } + + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +static std::vector CreateCompositeImages(int numImages, nvcv::Size2D size, nvcv::ImageFormat format) +{ + std::vector images; + images.reserve(numImages); + + for (int i = 0; i < numImages; ++i) + { + images.emplace_back(size, format); + } + + return images; +} + +TEST(OpComposite_Negative, varshape_rejects_mismatched_shape) +{ + cudaStream_t stream{}; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + const std::array testCases = MakeCompositeMismatchTestCases(true); + + cvcuda::Composite compositeOp; + + for (const CompositeMismatchTestCase &testCase : testCases) + { + SCOPED_TRACE(testCase.name); + + std::vector imgForeground + = CreateCompositeImages(testCase.foregroundImages, testCase.foregroundSize, nvcv::FMT_RGB8); + std::vector imgBackground + = CreateCompositeImages(testCase.backgroundImages, testCase.backgroundSize, nvcv::FMT_RGB8); + std::vector imgFgMask + = CreateCompositeImages(testCase.maskImages, testCase.maskSize, nvcv::FMT_U8); + std::vector imgOutput + = CreateCompositeImages(testCase.outputImages, testCase.outputSize, nvcv::FMT_RGB8); + + int batchCapacity = std::max( + {testCase.foregroundImages, testCase.backgroundImages, testCase.maskImages, testCase.outputImages}); + + nvcv::ImageBatchVarShape batchForeground(batchCapacity); + nvcv::ImageBatchVarShape batchBackground(batchCapacity); + nvcv::ImageBatchVarShape batchFgMask(batchCapacity); + nvcv::ImageBatchVarShape batchOutput(batchCapacity); + + batchForeground.pushBack(imgForeground.begin(), imgForeground.end()); + batchBackground.pushBack(imgBackground.begin(), imgBackground.end()); + batchFgMask.pushBack(imgFgMask.begin(), imgFgMask.end()); + batchOutput.pushBack(imgOutput.begin(), imgOutput.end()); + + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall( + [&] { compositeOp(stream, batchForeground, batchBackground, batchFgMask, batchOutput); })); + + EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + } + + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + TEST_P(OpComposite_Negative, invalid_parameters) { cudaStream_t stream; @@ -417,9 +912,7 @@ TEST_P(OpComposite_Negative, invalid_parameters) nvcv::ImageFormat backgroundFormat = GetParamValue<1>(); nvcv::ImageFormat fgMaskFormat = GetParamValue<2>(); nvcv::ImageFormat outFormat = GetParamValue<3>(); - bool isDiffFormatTest = GetParamValue<4>(); - - if (isDiffFormatTest) + if (bool isDiffFormatTest = GetParamValue<4>(); isDiffFormatTest) { GTEST_SKIP() << "Skipping diff format test for image input"; } @@ -433,7 +926,8 @@ TEST_P(OpComposite_Negative, invalid_parameters) cvcuda::Composite compositeOp; EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, - nvcv::ProtectCall([&] { compositeOp(stream, foregroundImg, backgroundImg, fgMaskImg, outImg); })); + nvcv::ProtectCall([&compositeOp, &stream, &foregroundImg, &backgroundImg, &fgMaskImg, &outImg] + { compositeOp(stream, foregroundImg, backgroundImg, fgMaskImg, outImg); })); EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); @@ -458,8 +952,8 @@ TEST_P(OpComposite_Negative, varshape_invalid_parameters) // Create input varshape - std::uniform_int_distribution udistWidth(inWidth * 0.8, inWidth * 1.1); - std::uniform_int_distribution udistHeight(inHeight * 0.8, inHeight * 1.1); + std::uniform_int_distribution udistWidth(ScaledSize(inWidth, 0.8), ScaledSize(inWidth, 1.1)); + std::uniform_int_distribution udistHeight(ScaledSize(inHeight, 0.8), ScaledSize(inHeight, 1.1)); std::vector imgForeground; std::vector imgBackground; @@ -505,9 +999,9 @@ TEST_P(OpComposite_Negative, varshape_invalid_parameters) // Run operator cvcuda::Composite compositeOp; - EXPECT_EQ( - NVCV_ERROR_INVALID_ARGUMENT, - nvcv::ProtectCall([&] { compositeOp(stream, batchForeground, batchBackground, batchFgMask, batchOutput); })); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&compositeOp, &stream, &batchForeground, &batchBackground, &batchFgMask, &batchOutput] + { compositeOp(stream, batchForeground, batchBackground, batchFgMask, batchOutput); })); EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); diff --git a/tests/cvcuda/system/TestOpConv2D.cpp b/tests/cvcuda/system/TestOpConv2D.cpp index 9801d605e..626cbd71e 100644 --- a/tests/cvcuda/system/TestOpConv2D.cpp +++ b/tests/cvcuda/system/TestOpConv2D.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "ConvUtils.hpp" #include "Definitions.hpp" +#include "PlanarParityUtils.hpp" #include #include @@ -31,18 +32,57 @@ namespace cuda = nvcv::cuda; namespace test = nvcv::test; +static int ScaledSize(int size, double scale) +{ + return static_cast(size * scale); +} + +static nvcv::ImageBatchVarShape MakeConv2DKernelBatch(const std::vector &kernelSizes) +{ + const auto numImages = static_cast(kernelSizes.size()); + std::vector kernelImages; + kernelImages.reserve(numImages); + + for (int i = 0; i < numImages; ++i) + { + const nvcv::Size2D kernelSize = kernelSizes[i]; + kernelImages.emplace_back(kernelSize, nvcv::FMT_F32); + + std::vector kernel(static_cast(kernelSize.w) * kernelSize.h); + for (size_t k = 0; k < kernel.size(); ++k) + { + const int coeff = static_cast((k + static_cast(i)) % 7) - 3; + kernel[k] = static_cast(coeff) * 0.03125f; + } + + auto data = kernelImages.back().exportData(); + EXPECT_NE(data, nvcv::NullOpt); + if (data) + { + const size_t rowBytes = static_cast(kernelSize.w) * sizeof(float); + EXPECT_EQ(cudaSuccess, cudaMemcpy2D(data->plane(0).basePtr, data->plane(0).rowStride, kernel.data(), + rowBytes, rowBytes, kernelSize.h, cudaMemcpyHostToDevice)); + } + } + + nvcv::ImageBatchVarShape batchKernel(numImages); + batchKernel.pushBack(kernelImages.begin(), kernelImages.end()); + return batchKernel; +} + // clang-format off -NVCV_TEST_SUITE_P(OpConv2D, test::ValueList +NVCV_TEST_SUITE_P(OpConv2D, test::ValueList { - // width, height, numImages, kernelWidth, kernelHeight, kernelAnchorX, kernelAnchorY, borderMode - { 32, 33, 1, 3, 3, -1, -1, NVCV_BORDER_CONSTANT}, - { 123, 144, 2, 5, 5, -1, -1, NVCV_BORDER_CONSTANT}, - { 66, 99, 3, 7, 7, 5, 5, NVCV_BORDER_CONSTANT}, - { 13, 12, 13, 5, 5, 4, 4, NVCV_BORDER_WRAP}, - { 4, 3, 4, 3, 3, 1, 1, NVCV_BORDER_REPLICATE}, - { 44, 55, 5, 3, 3, -1, -1, NVCV_BORDER_REFLECT}, - { 244, 155, 6, 5, 5, -1, -1, NVCV_BORDER_REFLECT101} + // width, height, numImages, kernelWidth, kernelHeight, anchorX, anchorY, borderMode, mixedKernelSizes + { 32, 33, 1, 3, 3, -1, -1, NVCV_BORDER_CONSTANT, false}, + { 123, 144, 2, 5, 5, -1, -1, NVCV_BORDER_CONSTANT, false}, + { 66, 99, 3, 7, 7, 5, 5, NVCV_BORDER_CONSTANT, false}, + { 13, 12, 13, 5, 5, 4, 4, NVCV_BORDER_WRAP, false}, + { 4, 3, 4, 3, 3, 1, 1, NVCV_BORDER_REPLICATE, false}, + { 44, 55, 5, 3, 3, -1, -1, NVCV_BORDER_REFLECT, false}, + { 244, 155, 6, 5, 5, -1, -1, NVCV_BORDER_REFLECT101, false}, + { 64, 48, 2, 7, 7, -1, -1, NVCV_BORDER_CONSTANT, true} }); // clang-format on @@ -52,21 +92,25 @@ TEST_P(OpConv2D, varshape_correct_output) cudaStream_t stream; EXPECT_EQ(cudaSuccess, cudaStreamCreate(&stream)); - int width = GetParamValue<0>(); - int height = GetParamValue<1>(); - int numImages = GetParamValue<2>(); - int kernelWidth = GetParamValue<3>(); - int kernelHeight = GetParamValue<4>(); - int kernelAnchorX = GetParamValue<5>(); - int kernelAnchorY = GetParamValue<6>(); + int width = GetParamValue<0>(); + int height = GetParamValue<1>(); + int numImages = GetParamValue<2>(); + int kernelWidth = GetParamValue<3>(); + int kernelHeight = GetParamValue<4>(); + int kernelAnchorX = GetParamValue<5>(); + int kernelAnchorY = GetParamValue<6>(); + bool mixedKernelSizes = GetParamValue<8>(); NVCVBorderType borderMode = GetParamValue<7>(); nvcv::ImageFormat imageFormat = nvcv::FMT_RGBA8; nvcv::ImageFormat kernelFormat = nvcv::FMT_F32; - nvcv::Size2D kernelSize{kernelWidth, kernelHeight}; - int2 kernelAnchor{kernelAnchorX, kernelAnchorY}; + nvcv::Size2D kernelSize{kernelWidth, kernelHeight}; + std::vector kernelSizes(numImages, kernelSize); + if (mixedKernelSizes) + kernelSizes.back() = nvcv::Size2D{3, 3}; + int2 kernelAnchor{kernelAnchorX, kernelAnchorY}; float4 borderValue = cuda::SetAll(0); @@ -74,8 +118,8 @@ TEST_P(OpConv2D, varshape_correct_output) std::default_random_engine rng; - std::uniform_int_distribution udistWidth(width * 0.8, width * 1.1); - std::uniform_int_distribution udistHeight(height * 0.8, height * 1.1); + std::uniform_int_distribution udistWidth(ScaledSize(width, 0.8), ScaledSize(width, 1.1)); + std::uniform_int_distribution udistHeight(ScaledSize(height, 0.8), ScaledSize(height, 1.1)); std::vector imgSrc; @@ -92,7 +136,7 @@ TEST_P(OpConv2D, varshape_correct_output) std::uniform_int_distribution udist(0, 255); srcVec[i].resize(imgSrc[i].size().h * srcRowStride); - std::generate(srcVec[i].begin(), srcVec[i].end(), [&]() { return udist(rng); }); + std::ranges::generate(srcVec[i], [&udist, &rng]() { return udist(rng); }); auto imgData = imgSrc[i].exportData(); ASSERT_NE(imgData, nvcv::NullOpt); @@ -123,7 +167,7 @@ TEST_P(OpConv2D, varshape_correct_output) for (int i = 0; i < numImages; ++i) { - kernel.emplace_back(kernelSize, kernelFormat); + kernel.emplace_back(kernelSizes[i], kernelFormat); int rowStride = kernel[i].size().w * sizeof(float); @@ -131,7 +175,7 @@ TEST_P(OpConv2D, varshape_correct_output) kernelVec[i].resize(kernel[i].size().h * kernel[i].size().w); - std::generate(kernelVec[i].begin(), kernelVec[i].end(), [&]() { return udist(rng); }); + std::ranges::generate(kernelVec[i], [&udist, &rng]() { return udist(rng); }); auto data = kernel[i].exportData(); ASSERT_NE(data, nvcv::NullOpt); @@ -193,24 +237,59 @@ TEST_P(OpConv2D, varshape_correct_output) std::vector goldVec(shape.y * pitches.y); // Generate gold result - test::Convolve(goldVec, pitches, srcVec[i], pitches, shape, imageFormat, kernelVec[i], kernelSize, kernelAnchor, - borderMode, borderValue); + int2 imageKernelAnchor = kernelAnchor; + test::Convolve(goldVec, pitches, srcVec[i], pitches, shape, imageFormat, kernelVec[i], kernelSizes[i], + imageKernelAnchor, borderMode, borderValue); EXPECT_EQ(testVec, goldVec); } } +static void RunConv2DPlanarParityVarShapeCase(nvcv::ImageFormat planarFmt, nvcv::ImageFormat interleavedFmt, int width, + int height, nvcv::Size2D kernelSize, int2 kernelAnchor, + NVCVBorderType borderMode, int numImages, bool mixedKernelSizes) +{ + std::vector kernelSizes(numImages, kernelSize); + if (mixedKernelSizes) + kernelSizes.back() = nvcv::Size2D{3, 3}; + auto kernelBatch = MakeConv2DKernelBatch(kernelSizes); + auto kernelAnchorTensor + = test::planar::MakePerImageTensor(numImages, nvcv::TYPE_2S32, int2{kernelAnchor.x, kernelAnchor.y}); + + cvcuda::Conv2D conv2dOp; + + test::planar::RunVarShapeParity( + planarFmt, interleavedFmt, width, height, width, height, numImages, + [&conv2dOp, &kernelBatch, &kernelAnchorTensor, borderMode]( + cudaStream_t stream, const nvcv::ImageBatchVarShape &src, const nvcv::ImageBatchVarShape &dst, + nvcv::ImageFormat) + { EXPECT_NO_THROW(conv2dOp(stream, src, dst, kernelBatch, kernelAnchorTensor, borderMode)); }); +} + +// Parameters: width, height, kernelWidth, kernelHeight, anchorX, anchorY, borderMode, numImages, +// planarFmt, interleavedFmt, mixedKernelSizes // clang-format off -NVCV_TEST_SUITE_P(OpConv2D_Negative, test::ValueList{ - {nvcv::FMT_RGB8, nvcv::FMT_RGB8p, NVCV_BORDER_CONSTANT}, - {nvcv::FMT_RGB8p, nvcv::FMT_RGB8p, NVCV_BORDER_CONSTANT}, - {nvcv::FMT_RGBf16, nvcv::FMT_RGBf16, NVCV_BORDER_CONSTANT}, -#ifndef ENABLE_SANITIZER - {nvcv::FMT_RGB8, nvcv::FMT_RGB8, static_cast(255)}, -#endif +NVCV_TEST_SUITE_P(OpConv2DPlanar, + test::ValueList{ + { 32, 24, 3, 3, -1, -1, NVCV_BORDER_CONSTANT, 2, nvcv::FMT_RGB8p, nvcv::FMT_RGB8, false}, + { 29, 27, 5, 3, 2, 1, NVCV_BORDER_REPLICATE, 1, nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8, false}, + { 17, 15, 3, 5, -1, -1, NVCV_BORDER_REFLECT101, 2, nvcv::FMT_RGBf32p, nvcv::FMT_RGBf32, false}, + { 65, 37, 7, 7, -1, -1, NVCV_BORDER_CONSTANT, 2, nvcv::FMT_RGB8p, nvcv::FMT_RGB8, true}, }); + // clang-format on +TEST_P(OpConv2DPlanar, varshape_matches_interleaved) +{ + RunConv2DPlanarParityVarShapeCase(GetParamValue<8>(), GetParamValue<9>(), GetParamValue<0>(), GetParamValue<1>(), + nvcv::Size2D{GetParamValue<2>(), GetParamValue<3>()}, + int2{GetParamValue<4>(), GetParamValue<5>()}, GetParamValue<6>(), + GetParamValue<7>(), GetParamValue<10>()); +} + +NVCV_TEST_SUITE_P(OpConv2D_Negative, test::PlanarConvolutionNegativeParams()); + TEST_P(OpConv2D_Negative, varshape_op) { cudaStream_t stream; @@ -234,8 +313,8 @@ TEST_P(OpConv2D_Negative, varshape_op) std::default_random_engine rng; - std::uniform_int_distribution udistWidth(width * 0.8, width * 1.1); - std::uniform_int_distribution udistHeight(height * 0.8, height * 1.1); + std::uniform_int_distribution udistWidth(ScaledSize(width, 0.8), ScaledSize(width, 1.1)); + std::uniform_int_distribution udistHeight(ScaledSize(height, 0.8), ScaledSize(height, 1.1)); std::vector imgSrc; std::vector imgDst; @@ -271,7 +350,8 @@ TEST_P(OpConv2D_Negative, varshape_op) cvcuda::Conv2D conv2dOp; EXPECT_EQ( NVCV_ERROR_INVALID_ARGUMENT, - nvcv::ProtectCall([&] { conv2dOp(stream, batchSrc, batchDst, batchKernel, kernelAnchorTensor, borderMode); })); + nvcv::ProtectCall([&conv2dOp, &stream, &batchSrc, &batchDst, &batchKernel, &kernelAnchorTensor, &borderMode] + { conv2dOp(stream, batchSrc, batchDst, batchKernel, kernelAnchorTensor, borderMode); })); EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); @@ -289,11 +369,8 @@ TEST(OpConv2D_Negative, varshape_hasDifferentFormat) { fmt, nvcv::FMT_U8} }; - for (auto testCase : testSet) + for (const auto &[inputFmtExtra, outputFmtExtra] : testSet) { - nvcv::ImageFormat inputFmtExtra = std::get<0>(testCase); - nvcv::ImageFormat outputFmtExtra = std::get<1>(testCase); - int width = 32; int height = 32; int numImages = 2; @@ -310,8 +387,8 @@ TEST(OpConv2D_Negative, varshape_hasDifferentFormat) std::default_random_engine rng; - std::uniform_int_distribution udistWidth(width * 0.8, width * 1.1); - std::uniform_int_distribution udistHeight(height * 0.8, height * 1.1); + std::uniform_int_distribution udistWidth(ScaledSize(width, 0.8), ScaledSize(width, 1.1)); + std::uniform_int_distribution udistHeight(ScaledSize(height, 0.8), ScaledSize(height, 1.1)); std::vector imgSrc; std::vector imgDst; @@ -347,9 +424,10 @@ TEST(OpConv2D_Negative, varshape_hasDifferentFormat) // Generate test result cvcuda::Conv2D conv2dOp; - EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, - nvcv::ProtectCall( - [&] { conv2dOp(stream, batchSrc, batchDst, batchKernel, kernelAnchorTensor, borderMode); })); + EXPECT_EQ( + NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&conv2dOp, &stream, &batchSrc, &batchDst, &batchKernel, &kernelAnchorTensor, &borderMode] + { conv2dOp(stream, batchSrc, batchDst, batchKernel, kernelAnchorTensor, borderMode); })); EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); } diff --git a/tests/cvcuda/system/TestOpConvertTo.cpp b/tests/cvcuda/system/TestOpConvertTo.cpp index 850974191..d6c82b2ab 100644 --- a/tests/cvcuda/system/TestOpConvertTo.cpp +++ b/tests/cvcuda/system/TestOpConvertTo.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -27,6 +27,7 @@ #include #include +#include namespace gt = ::testing; namespace test = nvcv::test; @@ -50,8 +51,8 @@ static void setGoldBuffer(std::vector &vect, DT_DEST val, int width, in } template -const void testConvertTo(nvcv::ImageFormat fmtIn, nvcv::ImageFormat fmtOut, int batch, int width, int height, - double alpha, double beta, DT_SOURCE setVal, DT_DEST expVal) +void testConvertTo(nvcv::ImageFormat fmtIn, nvcv::ImageFormat fmtOut, int batch, int width, int height, double alpha, + double beta, DT_SOURCE setVal, DT_DEST expVal, NVCVRoundMode roundMode = NVCV_ROUND_NEAREST) { cudaStream_t stream; EXPECT_EQ(cudaSuccess, cudaStreamCreate(&stream)); @@ -71,20 +72,22 @@ const void testConvertTo(nvcv::ImageFormat fmtIn, nvcv::ImageFormat fmtOut, int auto outAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*outData); ASSERT_TRUE(outAccess); - int inSampleStride = inAccess->numRows() * inAccess->rowStride(); - int outSampleStride = outAccess->numRows() * outAccess->rowStride(); + auto inSampleStride = static_cast(inAccess->numRows() * inAccess->rowStride()); + auto outSampleStride = static_cast(outAccess->numRows() * outAccess->rowStride()); + auto numSamples = static_cast(inAccess->numSamples()); - int inBufSizeElements = (inSampleStride / sizeof(DT_SOURCE)) * inAccess->numSamples(); - int outBufSizeElements = (outSampleStride / sizeof(DT_DEST)) * outAccess->numSamples(); - int inBufSizeBytes = inSampleStride * inAccess->numSamples(); - int outBufSizeBytes = outSampleStride * outAccess->numSamples(); + size_t inBufSizeElements = (inSampleStride / sizeof(DT_SOURCE)) * numSamples; + size_t outBufSizeElements = (outSampleStride / sizeof(DT_DEST)) * numSamples; + size_t inBufSizeBytes = inSampleStride * numSamples; + size_t outBufSizeBytes = outSampleStride * numSamples; std::vector srcVec(inBufSizeElements, setVal); std::vector goldVec(outBufSizeElements); std::vector testVec(outBufSizeElements); setGoldBuffer(goldVec, expVal, width * outAccess->numChannels(), height, - (outAccess->rowStride() / sizeof(DT_DEST)), (outSampleStride / sizeof(DT_DEST)), batch); + static_cast(outAccess->rowStride() / sizeof(DT_DEST)), + static_cast(outSampleStride / sizeof(DT_DEST)), batch); // Copy input data to the GPU EXPECT_EQ(cudaSuccess, @@ -94,14 +97,12 @@ const void testConvertTo(nvcv::ImageFormat fmtIn, nvcv::ImageFormat fmtOut, int // run operator cvcuda::ConvertTo convertToOp; - EXPECT_NO_THROW(convertToOp(stream, imgIn, imgOut, alpha, beta)); + EXPECT_NO_THROW(convertToOp(stream, imgIn, imgOut, alpha, beta, roundMode)); EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); EXPECT_EQ(cudaSuccess, cudaMemcpy(testVec.data(), outData->basePtr(), outBufSizeBytes, cudaMemcpyDeviceToHost)); EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); - //dbgImage(goldVec, inData->rowStride()); - //dbgImage(testVec, outData->rowStride()); EXPECT_EQ(goldVec, testVec); } @@ -236,10 +237,170 @@ TEST_P(OpConvertTo, OpConvertTo_RGB8toRGBf32) testConvertTo(nvcv::FMT_RGB8, nvcv::FMT_RGBf32, batch, width, height, alpha, beta, val, valExp); } +TEST_P(OpConvertTo, OpConvertTo_RGBf32toRGBf32) +{ + using fromType = float; + using toType = float; + + int width = GetParamValue<0>(); + int height = GetParamValue<1>(); + double alpha = GetParamValue<2>(); + double beta = GetParamValue<3>(); + int batch = GetParamValue<4>(); + + fromType val = 0x10; + toType valExp = nvcv::cuda::SaturateCast(alpha * val + beta); + + testConvertTo(nvcv::FMT_RGBf32, nvcv::FMT_RGBf32, batch, width, height, alpha, beta, val, valExp); +} + +// ============================================================================= +// Planar (NCHW/CHW) layout support +// +// ConvertTo applies the same scalar alpha/beta to every element regardless of channel, so a planar +// input is converted plane-by-plane and must produce exactly the same pixels as the interleaved +// path. These tests feed identical data in both layouts through cvcuda::ConvertTo and require the +// (re-interleaved) planar output to match the interleaved output bit-for-bit. +// ============================================================================= + +namespace { + +// Reorder an interleaved (HWC) element vector into planar (CHW) element order. Applied to both the +// input (to seed the planar tensor) and the interleaved output (to compare against the planar +// output), so a single direction suffices. +template +std::vector
InterleavedToPlanar(const std::vector
&hwc, int w, int h, int channels) +{ + std::vector
chw(hwc.size()); + const int hw = w * h; + for (int p = 0; p < hw; ++p) + { + for (int c = 0; c < channels; ++c) + { + chw[c * hw + p] = hwc[p * channels + c]; + } + } + return chw; +} + +// Deterministic, finite source values valid for every supported input dtype (max 250 fits u8/u16 +// and is exactly representable in float; avoids the float NaN/Inf that random bytes would produce). +template +std::vector
MakeDeterministic(int count, int seed) +{ + std::vector
v(count); + for (int i = 0; i < count; ++i) + { + v[i] = static_cast
((i * 7 + seed * 31 + 13) % 251); + } + return v; +} + +// Convert identical data in interleaved and planar layout, require the outputs to match exactly. +// Seeds/reads the tensors with util::{Set,Get}ImageTensorFromVector, which transparently handle the +// planar layout (interleaved vectors are HWC-ordered, planar vectors CHW-ordered). +template +void RunConvertToPlanarParityCase(nvcv::ImageFormat interleavedInFmt, nvcv::ImageFormat planarInFmt, + nvcv::ImageFormat interleavedOutFmt, nvcv::ImageFormat planarOutFmt, int w, int h, + double alpha, double beta, int numImages) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + const int channels = planarInFmt.numChannels(); + + nvcv::Tensor srcI = nvcv::util::CreateTensor(numImages, w, h, interleavedInFmt); + nvcv::Tensor dstI = nvcv::util::CreateTensor(numImages, w, h, interleavedOutFmt); + nvcv::Tensor srcP = nvcv::util::CreateTensor(numImages, w, h, planarInFmt); + nvcv::Tensor dstP = nvcv::util::CreateTensor(numImages, w, h, planarOutFmt); + + auto srcIData = srcI.exportData(); + auto dstIData = dstI.exportData(); + auto srcPData = srcP.exportData(); + auto dstPData = dstP.exportData(); + ASSERT_TRUE(srcIData && dstIData && srcPData && dstPData); + ASSERT_TRUE(srcPData->layout() == nvcv::TENSOR_NCHW || srcPData->layout() == nvcv::TENSOR_CHW); + + for (int i = 0; i < numImages; ++i) + { + std::vector hwc = MakeDeterministic(w * h * channels, i); + std::vector chw = InterleavedToPlanar(hwc, w, h, channels); + nvcv::util::SetImageTensorFromVector(*srcIData, hwc, i); + nvcv::util::SetImageTensorFromVector(*srcPData, chw, i); + } + + cvcuda::ConvertTo op; + EXPECT_NO_THROW(op(stream, srcI, dstI, alpha, beta)); + EXPECT_NO_THROW(op(stream, srcP, dstP, alpha, beta)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + for (int i = 0; i < numImages; ++i) + { + SCOPED_TRACE(i); + std::vector outI; + std::vector outP; + nvcv::util::GetImageVectorFromTensor(*dstIData, i, outI); + nvcv::util::GetImageVectorFromTensor(*dstPData, i, outP); + EXPECT_EQ(InterleavedToPlanar(outI, w, h, channels), outP); + } + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +} // namespace + +TEST(OpConvertToPlanar, rgb8_to_rgbf32_matches_interleaved) +{ + RunConvertToPlanarParityCase(nvcv::FMT_RGB8, nvcv::FMT_RGB8p, nvcv::FMT_RGBf32, nvcv::FMT_RGBf32p, + 33, 17, 2.1, 2.0, 1); +} + +TEST(OpConvertToPlanar, rgba8_to_rgba8_matches_interleaved) +{ + RunConvertToPlanarParityCase(nvcv::FMT_RGBA8, nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8, nvcv::FMT_RGBA8p, + 16, 16, 1.0, 5.0, 1); +} + +TEST(OpConvertToPlanar, rgbf32_to_rgb8_matches_interleaved) +{ + RunConvertToPlanarParityCase(nvcv::FMT_RGBf32, nvcv::FMT_RGBf32p, nvcv::FMT_RGB8, nvcv::FMT_RGB8p, + 20, 12, 0.5, 1.0, 1); +} + +TEST(OpConvertToPlanar, rgb8_to_rgbf32_batched_matches_interleaved) +{ + RunConvertToPlanarParityCase(nvcv::FMT_RGB8, nvcv::FMT_RGB8p, nvcv::FMT_RGBf32, nvcv::FMT_RGBf32p, + 24, 24, 1.5, -3.0, 4); +} + +// Rounding mode: NEAREST (historical default) vs TRUNCATE (toward zero), affecting float-to-integer +// outputs only. Cases pin values whose two roundings differ, incl. a negative, and check the default. + +TEST(OpConvertTo_Round, f32_to_s32_positive) +{ + // 2.7 -> nearest 3, truncate 2. + testConvertTo(nvcv::FMT_F32, nvcv::FMT_S32, 1, 5, 5, 1.0, 0.0, 2.7f, 3); // default == NEAREST + testConvertTo(nvcv::FMT_F32, nvcv::FMT_S32, 1, 5, 5, 1.0, 0.0, 2.7f, 3, NVCV_ROUND_NEAREST); + testConvertTo(nvcv::FMT_F32, nvcv::FMT_S32, 1, 5, 5, 1.0, 0.0, 2.7f, 2, NVCV_ROUND_TRUNCATE); +} + +TEST(OpConvertTo_Round, f32_to_s32_negative) +{ + // -2.7 -> nearest -3, truncate (toward zero) -2. + testConvertTo(nvcv::FMT_F32, nvcv::FMT_S32, 1, 5, 5, 1.0, 0.0, -2.7f, -3, NVCV_ROUND_NEAREST); + testConvertTo(nvcv::FMT_F32, nvcv::FMT_S32, 1, 5, 5, 1.0, 0.0, -2.7f, -2, NVCV_ROUND_TRUNCATE); +} + +TEST(OpConvertTo_Round, float_output_unaffected) +{ + // Float output: the rounding mode is meaningless; TRUNCATE must not alter the fractional result. + testConvertTo(nvcv::FMT_F32, nvcv::FMT_F32, 1, 5, 5, 1.0, 0.0, 2.7f, 2.7f, NVCV_ROUND_TRUNCATE); +} + // clang-format off NVCV_TEST_SUITE_P(OpConvertTo_Negative, nvcv::test::ValueList{ - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8, 24, 24, 24, 24, 3, 3}, // data format is not kHWC/kNHWC + {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8, 24, 24, 24, 24, 3, 3}, // mismatched layout (planar in, interleaved out) {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_F16, nvcv::FMT_F32, 24, 24, 24, 24, 3, 3}, // invalid input data type {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_F32, nvcv::FMT_F16, 24, 24, 24, 24, 3, 3}, // invalid output data type {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_F32, nvcv::FMT_F32, 25, 24, 24, 24, 3, 3}, // width is different @@ -272,7 +433,8 @@ TEST_P(OpConvertTo_Negative, op) // run operator cvcuda::ConvertTo convertToOp; - EXPECT_EQ(expectedReturnCode, nvcv::ProtectCall([&] { convertToOp(stream, imgIn, imgOut, alpha, beta); })); + EXPECT_EQ(expectedReturnCode, nvcv::ProtectCall([&convertToOp, &stream, &imgIn, &imgOut, &alpha, &beta] + { convertToOp(stream, imgIn, imgOut, alpha, beta); })); EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); } diff --git a/tests/cvcuda/system/TestOpCopyMakeBorder.cpp b/tests/cvcuda/system/TestOpCopyMakeBorder.cpp index a594b370c..33c2b932d 100644 --- a/tests/cvcuda/system/TestOpCopyMakeBorder.cpp +++ b/tests/cvcuda/system/TestOpCopyMakeBorder.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,6 +16,7 @@ */ #include "Definitions.hpp" +#include "PlanarParityUtils.hpp" #include #include @@ -31,12 +32,104 @@ #include #include +#include //#define DEBUG_PRINT_IMAGE //#define DEBUG_PRINT_DIFF namespace test = nvcv::test; +inline bool IsInside(int2 coords, int2 size) +{ + return coords.x >= 0 && coords.x < size.x && coords.y >= 0 && coords.y < size.y; +} + +inline void ApplyBorderIndex(int2 &coords, int2 size, NVCVBorderType borderType) +{ + if (borderType == NVCV_BORDER_REPLICATE) + test::ReplicateBorderIndex(coords, size); + else if (borderType == NVCV_BORDER_WRAP) + test::WrapBorderIndex(coords, size); + else if (borderType == NVCV_BORDER_REFLECT) + test::ReflectBorderIndex(coords, size); + else if (borderType == NVCV_BORDER_REFLECT101) + test::Reflect101BorderIndex(coords, size); +} + +inline int ScaleDimension(int value, double scale) +{ + return static_cast(static_cast(value) * scale); +} + +inline float BorderComponent(const float4 &borderValue, int channel) +{ + switch (channel) + { + case 0: + return borderValue.x; + case 1: + return borderValue.y; + case 2: + return borderValue.z; + case 3: + return borderValue.w; + default: + return 0.0f; + } +} + +template +static int ElementCountFromBytes(Size numBytes) +{ + return static_cast(numBytes / sizeof(T)); +} + +template +static void FillRandom(std::vector &values, std::uniform_int_distribution &srcRand, + std::default_random_engine &randEng) +{ + for (T &value : values) + { + if constexpr (std::is_same_v) + { + value = static_cast(srcRand(randEng)) / 255.0f; + } + else + { + value = static_cast(srcRand(randEng)); + } + } +} + +template +static T CopyMakeBorderValue(const std::vector &hSrc, int2 coords, int2 size, int channel, NVCVBorderType borderType, + const float4 &borderValue, GetSrcOffset getSrcOffset) +{ + if (IsInside(coords, size)) + { + return hSrc[getSrcOffset(coords, channel)]; + } + + if (borderType == NVCV_BORDER_CONSTANT) + { + return static_cast(BorderComponent(borderValue, channel)); + } + + ApplyBorderIndex(coords, size, borderType); + return hSrc[getSrcOffset(coords, channel)]; +} + +template +static void CopyMakeBorderPixel(std::vector &hDst, int dstOffset, const std::vector &hSrc, int2 coords, int2 size, + int channels, NVCVBorderType borderType, const float4 &borderValue, + GetSrcOffset getSrcOffset) +{ + for (int dk = 0; dk < channels; dk++) + { + hDst[dstOffset + dk] = CopyMakeBorderValue(hSrc, coords, size, dk, borderType, borderValue, getSrcOffset); + } +} + template static void CopyMakeBorder(std::vector &hDst, const std::vector &hSrc, const nvcv::TensorDataAccessStridedImagePlanar &dDstData, const int srcWidth, @@ -44,51 +137,28 @@ static void CopyMakeBorder(std::vector &hDst, const std::vector &hSrc, const int top, const int left, const NVCVBorderType borderType, const float4 borderValue) { int dstPixPitch = dDstData.numChannels(); - int dstRowStride = dDstData.rowStride() / sizeof(T); - int dstImgPitch = dDstData.sampleStride() / sizeof(T); + int dstRowStride = ElementCountFromBytes(dDstData.rowStride()); + int dstImgPitch = ElementCountFromBytes(dDstData.sampleStride()); - int2 coords, size{srcWidth, srcHeight}; + int2 coords{}; + int2 size{srcWidth, srcHeight}; for (int db = 0; db < dDstData.numSamples(); db++) { for (int di = 0; di < dDstData.numRows(); di++) { - coords.y = di - top; + coords.y = di - top; + auto srcOffset = [db, srcImgPitch, srcRowStride, srcPixPitch](int2 srcCoords, int channel) + { + return db * srcImgPitch + srcCoords.y * srcRowStride + srcCoords.x * srcPixPitch + channel; + }; for (int dj = 0; dj < dDstData.numCols(); dj++) { coords.x = dj - left; - for (int dk = 0; dk < dDstData.numChannels(); dk++) - { - T out = 0; - - if (coords.x >= 0 && coords.x < srcWidth && coords.y >= 0 && coords.y < srcHeight) - { - out = hSrc[db * srcImgPitch + coords.y * srcRowStride + coords.x * srcPixPitch + dk]; - } - else - { - if (borderType == NVCV_BORDER_CONSTANT) - { - out = static_cast(reinterpret_cast(&borderValue)[dk]); - } - else - { - if (borderType == NVCV_BORDER_REPLICATE) - test::ReplicateBorderIndex(coords, size); - else if (borderType == NVCV_BORDER_WRAP) - test::WrapBorderIndex(coords, size); - else if (borderType == NVCV_BORDER_REFLECT) - test::ReflectBorderIndex(coords, size); - else if (borderType == NVCV_BORDER_REFLECT101) - test::Reflect101BorderIndex(coords, size); - - out = hSrc[db * srcImgPitch + coords.y * srcRowStride + coords.x * srcPixPitch + dk]; - } - } - - hDst[db * dstImgPitch + di * dstRowStride + dj * dstPixPitch + dk] = out; - } + int dstOffset = db * dstImgPitch + di * dstRowStride + dj * dstPixPitch; + CopyMakeBorderPixel(hDst, dstOffset, hSrc, coords, size, dstPixPitch, borderType, borderValue, + srcOffset); } } } @@ -106,17 +176,22 @@ static void CopyMakeBorder(std::vector> &hBatchDst, const std::ve auto &hDst = hBatchDst[db]; auto &dDst = dBatchDstData[db]; auto imgDstData = dDst.exportData(); - int dstRowStride = imgDstData->plane(0).rowStride / sizeof(T); + int dstRowStride = ElementCountFromBytes(imgDstData->plane(0).rowStride); int dstPixPitch = dDst.format().numChannels(); auto &hSrc = hBatchSrc[db]; auto &dSrc = dBatchSrcData[db]; auto imgSrcData = dSrc.exportData(); - int rowStride = imgSrcData->plane(0).rowStride / sizeof(T); + int rowStride = ElementCountFromBytes(imgSrcData->plane(0).rowStride); int pixPitch = imgSrcData->format().numChannels(); auto imgSize = dBatchSrcData[db].size(); int2 size{imgSize.w, imgSize.h}; + auto srcOffset = [rowStride, pixPitch](int2 srcCoords, int channel) + { + return srcCoords.y * rowStride + srcCoords.x * pixPitch + channel; + }; + for (int di = 0; di < imgDstData->plane(0).height; di++) //for rows { coords.y = di - top[db]; @@ -125,37 +200,9 @@ static void CopyMakeBorder(std::vector> &hBatchDst, const std::ve { coords.x = dj - left[db]; - for (int dk = 0; dk < dstPixPitch; dk++) - { - T out = 0; - - if (coords.x >= 0 && coords.x < size.x && coords.y >= 0 && coords.y < size.y) - { - out = hSrc[coords.y * rowStride + coords.x * pixPitch + dk]; - } - else - { - if (borderType == NVCV_BORDER_CONSTANT) - { - out = static_cast(reinterpret_cast(&borderValue)[dk]); - } - else - { - if (borderType == NVCV_BORDER_REPLICATE) - test::ReplicateBorderIndex(coords, size); - else if (borderType == NVCV_BORDER_WRAP) - test::WrapBorderIndex(coords, size); - else if (borderType == NVCV_BORDER_REFLECT) - test::ReflectBorderIndex(coords, size); - else if (borderType == NVCV_BORDER_REFLECT101) - test::Reflect101BorderIndex(coords, size); - - out = hSrc[coords.y * rowStride + coords.x * pixPitch + dk]; - } - } - - hDst[di * dstRowStride + dj * dstPixPitch + dk] = out; - } + int dstOffset = di * dstRowStride + dj * dstPixPitch; + CopyMakeBorderPixel(hDst, dstOffset, hSrc, coords, size, dstPixPitch, borderType, borderValue, + srcOffset); } } } @@ -168,19 +215,24 @@ static void CopyMakeBorder(std::vector &hDst, const std::vector &left, const NVCVBorderType borderType, const float4 borderValue) { int dstPixPitch = dDstData.numChannels(); - int dstRowStride = dDstData.rowStride() / sizeof(T); - int dstImgPitch = dDstData.sampleStride() / sizeof(T); + int dstRowStride = ElementCountFromBytes(dDstData.rowStride()); + int dstImgPitch = ElementCountFromBytes(dDstData.sampleStride()); int2 coords; for (int db = 0; db < dDstData.numSamples(); db++) { auto &hSrc = hBatchSrc[db]; auto imgSrcData = dBatchSrcData[db].exportData(); - int rowStride = imgSrcData->plane(0).rowStride / sizeof(T); + int rowStride = ElementCountFromBytes(imgSrcData->plane(0).rowStride); int pixPitch = imgSrcData->format().numChannels(); auto imgSize = dBatchSrcData[db].size(); int2 size{imgSize.w, imgSize.h}; + auto srcOffset = [rowStride, pixPitch](int2 srcCoords, int channel) + { + return srcCoords.y * rowStride + srcCoords.x * pixPitch + channel; + }; + for (int di = 0; di < dDstData.numRows(); di++) { coords.y = di - top[db]; @@ -189,37 +241,9 @@ static void CopyMakeBorder(std::vector &hDst, const std::vector= 0 && coords.x < size.x && coords.y >= 0 && coords.y < size.y) - { - out = hSrc[coords.y * rowStride + coords.x * pixPitch + dk]; - } - else - { - if (borderType == NVCV_BORDER_CONSTANT) - { - out = static_cast(reinterpret_cast(&borderValue)[dk]); - } - else - { - if (borderType == NVCV_BORDER_REPLICATE) - test::ReplicateBorderIndex(coords, size); - else if (borderType == NVCV_BORDER_WRAP) - test::WrapBorderIndex(coords, size); - else if (borderType == NVCV_BORDER_REFLECT) - test::ReflectBorderIndex(coords, size); - else if (borderType == NVCV_BORDER_REFLECT101) - test::Reflect101BorderIndex(coords, size); - - out = hSrc[coords.y * rowStride + coords.x * pixPitch + dk]; - } - } - - hDst[db * dstImgPitch + di * dstRowStride + dj * dstPixPitch + dk] = out; - } + int dstOffset = db * dstImgPitch + di * dstRowStride + dj * dstPixPitch; + CopyMakeBorderPixel(hDst, dstOffset, hSrc, coords, size, dstPixPitch, borderType, borderValue, + srcOffset); } } } @@ -240,6 +264,11 @@ NVCV_TEST_SUITE_P(OpCopyMakeBorder, test::ValueListsampleStride() / sizeof(T)) * srcAccess->numSamples(); + int srcBufSize = ElementCountFromBytes(srcAccess->sampleStride()) * static_cast(srcAccess->numSamples()); srcVec.resize(srcBufSize); std::default_random_engine randEng{0}; std::uniform_int_distribution srcRand{0u, 255u}; - if (std::is_same::value) - std::generate(srcVec.begin(), srcVec.end(), [&]() { return srcRand(randEng) / 255.0f; }); - else - std::generate(srcVec.begin(), srcVec.end(), [&]() { return srcRand(randEng); }); + FillRandom(srcVec, srcRand, randEng); // Copy each input image with random data to the GPU ASSERT_EQ(cudaSuccess, cudaMemcpyAsync(srcData->basePtr(), srcVec.data(), srcBufSize * sizeof(T), @@ -280,15 +306,15 @@ void StartTest(int srcWidth, int srcHeight, int numBatches, int topPad, int bott ASSERT_NE(nullptr, dstData); auto dstAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*dstData); ASSERT_TRUE(dstData); - int dstBufSize = (dstAccess->sampleStride() / sizeof(T)) * dstAccess->numSamples(); + int dstBufSize = ElementCountFromBytes(dstAccess->sampleStride()) * static_cast(dstAccess->numSamples()); ASSERT_EQ(cudaSuccess, cudaMemsetAsync(dstData->basePtr(), 0, dstBufSize * sizeof(T), stream)); std::vector testVec(dstBufSize); std::vector goldVec(dstBufSize); int srcPixPitch = srcAccess->numChannels(); - int srcRowStride = srcAccess->rowStride() / sizeof(T); - int srcImgPitch = srcAccess->sampleStride() / sizeof(T); + int srcRowStride = ElementCountFromBytes(srcAccess->rowStride()); + int srcImgPitch = ElementCountFromBytes(srcAccess->sampleStride()); // Generate gold result CopyMakeBorder(goldVec, srcVec, *dstAccess, srcWidth, srcHeight, srcRowStride, srcPixPitch, srcImgPitch, topPad, @@ -339,10 +365,10 @@ TEST_P(OpCopyMakeBorder, tensor_correct_output) nvcv::ImageFormat format = GetParamValue<12>(); - if (nvcv::FMT_RGB8 == format || nvcv::FMT_RGBA8 == format) + if (nvcv::FMT_U8 == format || nvcv::FMT_RGB8 == format || nvcv::FMT_RGBA8 == format) StartTest(srcWidth, srcHeight, numBatches, topPad, bottomPad, leftPad, rightPad, borderType, borderValue, format); - else if (nvcv::FMT_RGBf32 == format || nvcv::FMT_RGBAf32 == format) + else if (nvcv::FMT_F32 == format || nvcv::FMT_RGBf32 == format || nvcv::FMT_RGBAf32 == format) StartTest(srcWidth, srcHeight, numBatches, topPad, bottomPad, leftPad, rightPad, borderType, borderValue, format); } @@ -354,19 +380,22 @@ void StartTestVarShape(int srcWidthBase, int srcHeightBase, int numBatches, int cudaStream_t stream; ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); - std::default_random_engine randEng{0}; - std::uniform_int_distribution rndSrcWidth(srcWidthBase * 0.8, srcWidthBase * 1.2); - std::uniform_int_distribution rndSrcHeight(srcHeightBase * 0.8, srcHeightBase * 1.2); + std::default_random_engine randEng{0}; + std::uniform_int_distribution rndSrcWidth(ScaleDimension(srcWidthBase, 0.8), ScaleDimension(srcWidthBase, 1.2)); + std::uniform_int_distribution rndSrcHeight(ScaleDimension(srcHeightBase, 0.8), ScaleDimension(srcHeightBase, 1.2)); - std::uniform_int_distribution rndTop(topPad * 0.8, topPad * 1.2); - std::uniform_int_distribution rndBottom(bottomPad * 0.8, bottomPad * 1.2); + std::uniform_int_distribution rndTop(ScaleDimension(topPad, 0.8), ScaleDimension(topPad, 1.2)); + std::uniform_int_distribution rndBottom(ScaleDimension(bottomPad, 0.8), ScaleDimension(bottomPad, 1.2)); - std::uniform_int_distribution rndLeft(leftPad * 0.8, leftPad * 1.2); - std::uniform_int_distribution rndRight(rightPad * 0.8, rightPad * 1.2); + std::uniform_int_distribution rndLeft(ScaleDimension(leftPad, 0.8), ScaleDimension(leftPad, 1.2)); + std::uniform_int_distribution rndRight(ScaleDimension(rightPad, 0.8), ScaleDimension(rightPad, 1.2)); //Prepare input and output buffer - std::vector imgSrcVec, imgDstVec; - std::vector> hImgSrcVec, hImgDstVec, batchGoldVec; + std::vector imgSrcVec; + std::vector imgDstVec; + std::vector> hImgSrcVec; + std::vector> hImgDstVec; + std::vector> batchGoldVec; std::vector topVec(numBatches); std::vector leftVec(numBatches); for (int i = 0; i < numBatches; ++i) @@ -387,15 +416,12 @@ void StartTestVarShape(int srcWidthBase, int srcHeightBase, int numBatches, int auto imgSrcData = imgSrcVec.back().exportData(); int srcStride = imgSrcData->plane(0).rowStride; - int srcRowStride = srcStride / sizeof(T); + int srcRowStride = ElementCountFromBytes(srcStride); int srcBufSize = srcRowStride * imgSrcData->plane(0).height; std::vector srcVec(srcBufSize); std::uniform_int_distribution srcRand{0u, 255u}; - if (std::is_same::value) - std::generate(srcVec.begin(), srcVec.end(), [&]() { return srcRand(randEng) / 255.0f; }); - else - std::generate(srcVec.begin(), srcVec.end(), [&]() { return srcRand(randEng); }); + FillRandom(srcVec, srcRand, randEng); // Copy each input image with random data to the GPU ASSERT_EQ(cudaSuccess, cudaMemcpyAsync(imgSrcData->plane(0).basePtr, srcVec.data(), srcBufSize * sizeof(T), @@ -406,7 +432,7 @@ void StartTestVarShape(int srcWidthBase, int srcHeightBase, int numBatches, int imgDstVec.emplace_back(nvcv::Size2D{dstWidth, dstHeight}, format); auto imgDstData = imgDstVec.back().exportData(); int dstStride = imgDstData->plane(0).rowStride; - int dstRowStride = dstStride / sizeof(T); + int dstRowStride = ElementCountFromBytes(dstStride); int dstBufSize = dstRowStride * imgDstData->plane(0).height; std::vector dstVec(dstBufSize); @@ -498,10 +524,10 @@ TEST_P(OpCopyMakeBorder, varshape_correct_output) nvcv::ImageFormat format = GetParamValue<12>(); - if (nvcv::FMT_RGB8 == format || nvcv::FMT_RGBA8 == format) + if (nvcv::FMT_U8 == format || nvcv::FMT_RGB8 == format || nvcv::FMT_RGBA8 == format) StartTestVarShape(srcWidth, srcHeight, numBatches, topPad, bottomPad, leftPad, rightPad, borderType, borderValue, format); - else if (nvcv::FMT_RGBf32 == format || nvcv::FMT_RGBAf32 == format) + else if (nvcv::FMT_F32 == format || nvcv::FMT_RGBf32 == format || nvcv::FMT_RGBAf32 == format) StartTestVarShape(srcWidth, srcHeight, numBatches, topPad, bottomPad, leftPad, rightPad, borderType, borderValue, format); } @@ -514,18 +540,19 @@ void StartTestStack(int srcWidthBase, int srcHeightBase, int numBatches, int top ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); //make sure the random pad settings did not exceed the limit. - int dstWidth = (srcWidthBase + leftPad + rightPad) * 1.2; - int dstHeight = (srcHeightBase + topPad + bottomPad) * 1.2; + int dstWidth = ScaleDimension(srcWidthBase + leftPad + rightPad, 1.2); + int dstHeight = ScaleDimension(srcHeightBase + topPad + bottomPad, 1.2); - std::default_random_engine randEng{0}; - std::uniform_int_distribution rndSrcWidth(srcWidthBase * 0.8, srcWidthBase * 1.2); - std::uniform_int_distribution rndSrcHeight(srcHeightBase * 0.8, srcHeightBase * 1.2); + std::default_random_engine randEng{0}; + std::uniform_int_distribution rndSrcWidth(ScaleDimension(srcWidthBase, 0.8), ScaleDimension(srcWidthBase, 1.2)); + std::uniform_int_distribution rndSrcHeight(ScaleDimension(srcHeightBase, 0.8), ScaleDimension(srcHeightBase, 1.2)); - std::uniform_int_distribution rndTop(topPad * 0.8, topPad * 1.2); - std::uniform_int_distribution rndLeft(leftPad * 0.8, leftPad * 1.2); + std::uniform_int_distribution rndTop(ScaleDimension(topPad, 0.8), ScaleDimension(topPad, 1.2)); + std::uniform_int_distribution rndLeft(ScaleDimension(leftPad, 0.8), ScaleDimension(leftPad, 1.2)); //Prepare input and output buffer - std::vector imgSrcVec, imgDstVec; + std::vector imgSrcVec; + std::vector imgDstVec; std::vector> hImgSrcVec; std::vector topVec(numBatches); std::vector leftVec(numBatches); @@ -543,15 +570,12 @@ void StartTestStack(int srcWidthBase, int srcHeightBase, int numBatches, int top auto imgSrcData = imgSrcVec.back().exportData(); int srcStride = imgSrcData->plane(0).rowStride; - int srcRowStride = srcStride / sizeof(T); + int srcRowStride = ElementCountFromBytes(srcStride); int srcBufSize = srcRowStride * imgSrcData->plane(0).height; std::vector srcVec(srcBufSize); std::uniform_int_distribution srcRand{0u, 255u}; - if (std::is_same::value) - std::generate(srcVec.begin(), srcVec.end(), [&]() { return srcRand(randEng) / 255.0f; }); - else - std::generate(srcVec.begin(), srcVec.end(), [&]() { return srcRand(randEng); }); + FillRandom(srcVec, srcRand, randEng); // Copy each input image with random data to the GPU ASSERT_EQ(cudaSuccess, cudaMemcpyAsync(imgSrcData->plane(0).basePtr, srcVec.data(), srcBufSize * sizeof(T), @@ -567,7 +591,7 @@ void StartTestStack(int srcWidthBase, int srcHeightBase, int numBatches, int top ASSERT_NE(nullptr, dstData); auto dstAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*dstData); ASSERT_TRUE(dstData); - int dstBufSize = (dstAccess->sampleStride() / sizeof(T)) * dstAccess->numSamples(); + int dstBufSize = ElementCountFromBytes(dstAccess->sampleStride()) * static_cast(dstAccess->numSamples()); ASSERT_EQ(cudaSuccess, cudaMemsetAsync(dstData->basePtr(), 0, dstBufSize * sizeof(T), stream)); std::vector testVec(dstBufSize); @@ -642,29 +666,219 @@ TEST_P(OpCopyMakeBorder, stack_correct_output) nvcv::ImageFormat format = GetParamValue<12>(); - if (nvcv::FMT_RGB8 == format || nvcv::FMT_RGBA8 == format) + if (nvcv::FMT_U8 == format || nvcv::FMT_RGB8 == format || nvcv::FMT_RGBA8 == format) StartTestStack(srcWidth, srcHeight, numBatches, topPad, bottomPad, leftPad, rightPad, borderType, borderValue, format); - else if (nvcv::FMT_RGBf32 == format || nvcv::FMT_RGBAf32 == format) + else if (nvcv::FMT_F32 == format || nvcv::FMT_RGBf32 == format || nvcv::FMT_RGBAf32 == format) StartTestStack(srcWidth, srcHeight, numBatches, topPad, bottomPad, leftPad, rightPad, borderType, borderValue, format); } +// ============================================================================= +// Planar (NCHW/CHW) layout support +// +// CopyMakeBorder remaps each output pixel independently of its channel, including per-channel +// constant border values. A planar input must therefore match the equivalent interleaved result +// exactly once the planar output is re-interleaved. +// ============================================================================= + +namespace { + +void FillPadTensor(nvcv::Tensor &tensor, int value, int count) +{ + auto data = tensor.exportData(); + ASSERT_NE(data, nullptr); + + std::vector values(static_cast(count), value); + ASSERT_EQ(cudaSuccess, + cudaMemcpy(data->basePtr(), values.data(), values.size() * sizeof(int), cudaMemcpyHostToDevice)); +} + +void RunPlanarParityTensorCase(nvcv::ImageFormat planarFmt, nvcv::ImageFormat interleavedFmt, int srcW, int srcH, + int top, int bottom, int left, int right, NVCVBorderType borderType, + const float4 &borderValue, int numImages) +{ + test::planar::RunTensorParity(planarFmt, interleavedFmt, srcW, srcH, srcW + left + right, srcH + top + bottom, + numImages, + [top, left, borderType, borderValue](cudaStream_t stream, const nvcv::Tensor &src, + const nvcv::Tensor &dst, nvcv::ImageFormat) + { + cvcuda::CopyMakeBorder op; + EXPECT_NO_THROW(op(stream, src, dst, top, left, borderType, borderValue)); + }); +} + +void RunPlanarParityVarShapeCase(nvcv::ImageFormat planarFmt, nvcv::ImageFormat interleavedFmt, int srcW, int srcH, + int top, int bottom, int left, int right, NVCVBorderType borderType, + const float4 &borderValue, int numImages) +{ + nvcv::Tensor topTensor(1, {numImages, 1}, nvcv::FMT_S32); + nvcv::Tensor leftTensor(1, {numImages, 1}, nvcv::FMT_S32); + FillPadTensor(topTensor, top, numImages); + FillPadTensor(leftTensor, left, numImages); + + test::planar::RunVarShapeParity( + planarFmt, interleavedFmt, srcW, srcH, srcW + left + right, srcH + top + bottom, numImages, + [&topTensor, &leftTensor, borderType, borderValue](cudaStream_t stream, const nvcv::ImageBatchVarShape &src, + const nvcv::ImageBatchVarShape &dst, nvcv::ImageFormat) + { + cvcuda::CopyMakeBorder op; + EXPECT_NO_THROW(op(stream, src, dst, topTensor, leftTensor, borderType, borderValue)); + }); +} + +void RunPlanarParityStackCase(nvcv::ImageFormat planarFmt, nvcv::ImageFormat interleavedFmt, int srcW, int srcH, + int top, int bottom, int left, int right, NVCVBorderType borderType, + const float4 &borderValue, int numImages) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + const int channels = planarFmt.numChannels(); + const int elemSize = planarFmt.planePixelStrideBytes(0); + const int srcRowStr = srcW * channels * elemSize; + const int dstW = srcW + left + right; + const int dstH = srcH + top + bottom; + const int dstRowStr = dstW * channels * elemSize; + + std::vector srcI; + std::vector srcP; + for (int i = 0; i < numImages; ++i) + { + srcI.emplace_back(nvcv::Size2D{srcW, srcH}, interleavedFmt); + srcP.emplace_back(nvcv::Size2D{srcW, srcH}, planarFmt); + } + + nvcv::ImageBatchVarShape batchSrcI(numImages); + nvcv::ImageBatchVarShape batchSrcP(numImages); + batchSrcI.pushBack(srcI.begin(), srcI.end()); + batchSrcP.pushBack(srcP.begin(), srcP.end()); + + for (int i = 0; i < numImages; ++i) + { + std::vector hwc(srcH * srcRowStr); + test::planar::FillDeterministicValues(hwc, static_cast(i) * 101 + 17, planarFmt.planeDataType(0)); + + auto idata = srcI[i].exportData(); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(idata->plane(0).basePtr, idata->plane(0).rowStride, hwc.data(), srcRowStr, + srcRowStr, srcH, cudaMemcpyHostToDevice)); + + auto planes = test::planar::DeinterleaveToPlanes(hwc, srcW, srcH, channels, elemSize); + auto pdata = srcP[i].exportData(); + const int planeBytes = srcW * srcH * elemSize; + ASSERT_EQ(pdata->numPlanes(), channels); + for (int c = 0; c < channels; ++c) + { + ASSERT_EQ(cudaSuccess, + cudaMemcpy2D(pdata->plane(c).basePtr, pdata->plane(c).rowStride, planes.data() + c * planeBytes, + srcW * elemSize, srcW * elemSize, srcH, cudaMemcpyHostToDevice)); + } + } + + nvcv::Tensor dstI(numImages, {dstW, dstH}, interleavedFmt); + nvcv::Tensor dstP(numImages, {dstW, dstH}, planarFmt); + + nvcv::Tensor topTensor(1, {numImages, 1}, nvcv::FMT_S32); + nvcv::Tensor leftTensor(1, {numImages, 1}, nvcv::FMT_S32); + FillPadTensor(topTensor, top, numImages); + FillPadTensor(leftTensor, left, numImages); + + cvcuda::CopyMakeBorder op; + EXPECT_NO_THROW(op(stream, batchSrcI, dstI, topTensor, leftTensor, borderType, borderValue)); + EXPECT_NO_THROW(op(stream, batchSrcP, dstP, topTensor, leftTensor, borderType, borderValue)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + auto dstIData = dstI.exportData(); + auto dstPData = dstP.exportData(); + ASSERT_TRUE(dstIData && dstPData); + + auto dstIAcc = nvcv::TensorDataAccessStridedImagePlanar::Create(*dstIData); + auto dstPAcc = nvcv::TensorDataAccessStridedImagePlanar::Create(*dstPData); + ASSERT_TRUE(dstIAcc && dstPAcc); + + for (int i = 0; i < numImages; ++i) + { + SCOPED_TRACE(i); + auto gpuInter = test::planar::DownloadInterleavedSample(*dstIAcc, i, dstW, dstH, dstRowStr); + auto planesOut = test::planar::DownloadPlanarSample(*dstPAcc, i, dstW, dstH, channels, elemSize); + auto planarInter = test::planar::InterleaveFromPlanes(planesOut, dstW, dstH, channels, elemSize); + + EXPECT_EQ(gpuInter, planarInter); + } + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +float4 MakeBorderValue(float x, float y, float z, float w) +{ + return float4{x, y, z, w}; +} + +} // namespace + // clang-format off -NVCV_TEST_SUITE_P(OpCopyMakeBorder_Negative, test::ValueList{ - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_RGB8, nvcv::FMT_RGB8p, 0, 0, NVCV_BORDER_CONSTANT}, // data format is different - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_RGB8p, nvcv::FMT_RGB8p, 0, 0, NVCV_BORDER_CONSTANT}, // data format is not kNHWC/kHWC - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U16, 0, 0, NVCV_BORDER_CONSTANT}, // data type is different - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_F16, nvcv::FMT_F16, 0, 0, NVCV_BORDER_CONSTANT}, // invalid data type -#ifndef ENABLE_SANITIZER - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U8, 0, 0, static_cast(255)}, // invalid borderType -#endif - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U8, -1, 0, NVCV_BORDER_CONSTANT}, // invalid top - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U8, 0, -1, NVCV_BORDER_CONSTANT}, // invalid left +NVCV_TEST_SUITE_P(OpCopyMakeBorderPlanar, + test::ValueList{ + // srcW, srcH, top, bottom, left, right, border, b0, b1, b2, b3, images, planarFmt, interleavedFmt + { 24, 17, 3, 2, 4, 1, NVCV_BORDER_CONSTANT, 12.f, 100.f, 245.f, 0.f, 2, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + { 31, 19, 0, 5, 2, 3, NVCV_BORDER_REPLICATE, 0.f, 0.f, 0.f, 0.f, 2, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + { 28, 23, 4, 1, 0, 5, NVCV_BORDER_REFLECT, 0.f, 0.f, 0.f, 0.f, 1, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + { 26, 21, 5, 4, 3, 2, NVCV_BORDER_WRAP, 0.f, 0.f, 0.f, 0.f, 2, nvcv::FMT_RGBf32p, nvcv::FMT_RGBf32}, + { 25, 18, 2, 3, 5, 4, NVCV_BORDER_REFLECT101, 0.f, 0.f, 0.f, 0.f, 2, nvcv::FMT_RGBAf32p, nvcv::FMT_RGBAf32}, + { 18, 16, 1, 2, 2, 1, NVCV_BORDER_CONSTANT, 3.f, 7.f, 11.f, 13.f, 1, nvcv::FMT_RGBAf32p, nvcv::FMT_RGBAf32}, }); // clang-format on +TEST_P(OpCopyMakeBorderPlanar, tensor_matches_interleaved) +{ + RunPlanarParityTensorCase( + GetParamValue<12>(), GetParamValue<13>(), GetParamValue<0>(), GetParamValue<1>(), GetParamValue<2>(), + GetParamValue<3>(), GetParamValue<4>(), GetParamValue<5>(), GetParamValue<6>(), + MakeBorderValue(GetParamValue<7>(), GetParamValue<8>(), GetParamValue<9>(), GetParamValue<10>()), + GetParamValue<11>()); +} + +TEST_P(OpCopyMakeBorderPlanar, varshape_matches_interleaved) +{ + RunPlanarParityVarShapeCase( + GetParamValue<12>(), GetParamValue<13>(), GetParamValue<0>(), GetParamValue<1>(), GetParamValue<2>(), + GetParamValue<3>(), GetParamValue<4>(), GetParamValue<5>(), GetParamValue<6>(), + MakeBorderValue(GetParamValue<7>(), GetParamValue<8>(), GetParamValue<9>(), GetParamValue<10>()), + GetParamValue<11>()); +} + +TEST_P(OpCopyMakeBorderPlanar, stack_matches_interleaved) +{ + RunPlanarParityStackCase( + GetParamValue<12>(), GetParamValue<13>(), GetParamValue<0>(), GetParamValue<1>(), GetParamValue<2>(), + GetParamValue<3>(), GetParamValue<4>(), GetParamValue<5>(), GetParamValue<6>(), + MakeBorderValue(GetParamValue<7>(), GetParamValue<8>(), GetParamValue<9>(), GetParamValue<10>()), + GetParamValue<11>()); +} + +static auto OpCopyMakeBorderNegativeParams() +{ + test::ValueList params{ + {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_RGB8, nvcv::FMT_RGB8p, 0, 0, + NVCV_BORDER_CONSTANT }, // data format is different + {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_RGB8p, nvcv::FMT_RGB8, 0, 0, + NVCV_BORDER_CONSTANT }, // data format is different + {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U16, 0, 0, + NVCV_BORDER_CONSTANT }, // data type is different + {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_F16, nvcv::FMT_F16, 0, 0, NVCV_BORDER_CONSTANT}, // invalid data type + }; +#ifndef ENABLE_SANITIZER + params.emplace_back(NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U8, 0, 0, + static_cast(255)); +#endif + params.emplace_back(NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U8, -1, 0, NVCV_BORDER_CONSTANT); + params.emplace_back(NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U8, 0, -1, NVCV_BORDER_CONSTANT); + return params; +} + +NVCV_TEST_SUITE_P(OpCopyMakeBorder_Negative, OpCopyMakeBorderNegativeParams()); + TEST_P(OpCopyMakeBorder_Negative, op) { NVCVStatus expectedReturnCode = GetParamValue<0>(); @@ -695,7 +909,8 @@ TEST_P(OpCopyMakeBorder_Negative, op) cvcuda::CopyMakeBorder cpyMakeBorderOp; EXPECT_EQ( expectedReturnCode, - nvcv::ProtectCall([&] { cpyMakeBorderOp(stream, imgSrc, imgDst, topPad, leftPad, borderType, borderValue); })); + nvcv::ProtectCall([&cpyMakeBorderOp, &stream, &imgSrc, &imgDst, &topPad, &leftPad, &borderType, &borderValue] + { cpyMakeBorderOp(stream, imgSrc, imgDst, topPad, leftPad, borderType, borderValue); })); ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); @@ -712,11 +927,8 @@ TEST(OpCopyMakeBorder_Negative, invalid_out_size) { 0, -1} // invalid dst height }; - for (auto testCase : testSet) + for (const auto &[leftPadExtra, topPadExtra] : testSet) { - int leftPadExtra = std::get<0>(testCase); - int topPadExtra = std::get<1>(testCase); - int srcWidth = 24; int srcHeight = 24; int dstWidth = srcWidth + leftPad + leftPadExtra; @@ -736,30 +948,35 @@ TEST(OpCopyMakeBorder_Negative, invalid_out_size) // Run operator cvcuda::CopyMakeBorder cpyMakeBorderOp; - EXPECT_EQ( - NVCV_ERROR_INVALID_ARGUMENT, - nvcv::ProtectCall( - [&] { cpyMakeBorderOp(stream, imgSrc, imgDst, topPad, leftPad, NVCV_BORDER_CONSTANT, borderValue); })); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall( + [&cpyMakeBorderOp, &stream, &imgSrc, &imgDst, &topPad, &leftPad, &borderValue] { + cpyMakeBorderOp(stream, imgSrc, imgDst, topPad, leftPad, NVCV_BORDER_CONSTANT, borderValue); + })); ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); } } -// clang-format off -NVCV_TEST_SUITE_P(OpCopyMakeBorderVarshape_Negative, test::ValueList{ - {nvcv::FMT_RGB8, nvcv::FMT_RGB8p, nvcv::FMT_S32, nvcv::FMT_S32, NVCV_BORDER_CONSTANT}, - {nvcv::FMT_RGB8p, nvcv::FMT_RGB8p, nvcv::FMT_S32, nvcv::FMT_S32, NVCV_BORDER_CONSTANT}, - {nvcv::FMT_RGBf16, nvcv::FMT_RGBf16, nvcv::FMT_S32, nvcv::FMT_S32, NVCV_BORDER_CONSTANT}, - {nvcv::FMT_RGB8, nvcv::FMT_RGB8, nvcv::FMT_F32, nvcv::FMT_S32, NVCV_BORDER_CONSTANT}, - {nvcv::FMT_RGB8, nvcv::FMT_RGB8, nvcv::FMT_S32, nvcv::FMT_F32, NVCV_BORDER_CONSTANT}, - {nvcv::FMT_RGB8, nvcv::FMT_RGB8, nvcv::FMT_RGB8p, nvcv::FMT_S32, NVCV_BORDER_CONSTANT}, - {nvcv::FMT_RGB8, nvcv::FMT_RGB8, nvcv::FMT_S32, nvcv::FMT_RGB8p, NVCV_BORDER_CONSTANT}, +static auto OpCopyMakeBorderVarshapeNegativeParams() +{ + test::ValueList params{ + { nvcv::FMT_RGB8, nvcv::FMT_RGB8p, nvcv::FMT_S32, nvcv::FMT_S32, NVCV_BORDER_CONSTANT}, + { nvcv::FMT_RGB8p, nvcv::FMT_RGB8, nvcv::FMT_S32, nvcv::FMT_S32, NVCV_BORDER_CONSTANT}, + {nvcv::FMT_RGBf16, nvcv::FMT_RGBf16, nvcv::FMT_S32, nvcv::FMT_S32, NVCV_BORDER_CONSTANT}, + { nvcv::FMT_RGB8, nvcv::FMT_RGB8, nvcv::FMT_F32, nvcv::FMT_S32, NVCV_BORDER_CONSTANT}, + { nvcv::FMT_RGB8, nvcv::FMT_RGB8, nvcv::FMT_S32, nvcv::FMT_F32, NVCV_BORDER_CONSTANT}, + { nvcv::FMT_RGB8, nvcv::FMT_RGB8, nvcv::FMT_RGB8p, nvcv::FMT_S32, NVCV_BORDER_CONSTANT}, + { nvcv::FMT_RGB8, nvcv::FMT_RGB8, nvcv::FMT_S32, nvcv::FMT_RGB8p, NVCV_BORDER_CONSTANT}, + }; #ifndef ENABLE_SANITIZER - {nvcv::FMT_RGB8, nvcv::FMT_RGB8, nvcv::FMT_S32, nvcv::FMT_S32, static_cast(255)}, + params.emplace_back(nvcv::FMT_RGB8, nvcv::FMT_RGB8, nvcv::FMT_S32, nvcv::FMT_S32, static_cast(255)); #endif -}); -// clang-format on + return params; +} + +NVCV_TEST_SUITE_P(OpCopyMakeBorderVarshape_Negative, OpCopyMakeBorderVarshapeNegativeParams()); TEST_P(OpCopyMakeBorderVarshape_Negative, op) { @@ -784,12 +1001,13 @@ TEST_P(OpCopyMakeBorderVarshape_Negative, op) nvcv::ImageFormat leftPadDmt = GetParamValue<3>(); NVCVBorderType borderType = GetParamValue<4>(); - std::default_random_engine randEng{0}; - std::uniform_int_distribution rndSrcWidth(srcWidthBase * 0.8, srcWidthBase * 1.2); - std::uniform_int_distribution rndSrcHeight(srcHeightBase * 0.8, srcHeightBase * 1.2); + std::default_random_engine randEng{0}; + std::uniform_int_distribution rndSrcWidth(ScaleDimension(srcWidthBase, 0.8), ScaleDimension(srcWidthBase, 1.2)); + std::uniform_int_distribution rndSrcHeight(ScaleDimension(srcHeightBase, 0.8), ScaleDimension(srcHeightBase, 1.2)); //Prepare input and output buffer - std::vector imgSrcVec, imgDstVec; + std::vector imgSrcVec; + std::vector imgDstVec; for (int i = 0; i < numBatches; ++i) { int srcWidth = rndSrcWidth(randEng); @@ -815,7 +1033,8 @@ TEST_P(OpCopyMakeBorderVarshape_Negative, op) cvcuda::CopyMakeBorder cpyMakeBorderOp; EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall( - [&] { cpyMakeBorderOp(stream, imgBatchSrc, imgBatchDst, inTop, inLeft, borderType, borderValue); })); + [&cpyMakeBorderOp, &stream, &imgBatchSrc, &imgBatchDst, &inTop, &inLeft, &borderType, &borderValue] + { cpyMakeBorderOp(stream, imgBatchSrc, imgBatchDst, inTop, inLeft, borderType, borderValue); })); } TEST(OpCopyMakeBorder_Negative, create_null_handle) diff --git a/tests/cvcuda/system/TestOpCropFlipNormalizeReformat.cpp b/tests/cvcuda/system/TestOpCropFlipNormalizeReformat.cpp index 803555de9..545785e07 100644 --- a/tests/cvcuda/system/TestOpCropFlipNormalizeReformat.cpp +++ b/tests/cvcuda/system/TestOpCropFlipNormalizeReformat.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,6 +16,7 @@ */ #include "Definitions.hpp" +#include "PlanarParityUtils.hpp" #include #include @@ -31,6 +32,7 @@ #include #include +#include #include #include #include @@ -40,13 +42,151 @@ namespace test = nvcv::test; namespace cuda = nvcv::cuda; namespace ttype = nvcv::test::type; +static int ScaledSize(int value, double scale) +{ + return static_cast(value * scale); +} + +static int2 FlippedCropCoord(int j, int i, int flip_code, const NVCVRectI &cropRect) +{ + int2 coord{j, i}; + + if (flip_code == 1) + { + coord.x = cropRect.width - 1 - j; + } + else if (flip_code == 0) + { + coord.y = cropRect.height - 1 - i; + } + else if (flip_code == -1) + { + coord.x = cropRect.width - 1 - j; + coord.y = cropRect.height - 1 - i; + } + + coord.x += cropRect.x; + coord.y += cropRect.y; + + return coord; +} + +static float NormalizeScaleValue(const std::vector &hScale, int scaleRowStride, nvcv::ImageFormat scaleFormat, + int si, int sj, int sk, const float epsilon, const uint32_t flags) +{ + float scale = hScale.at(si * scaleRowStride + sj * scaleFormat.numChannels() + sk); + + if (flags & CVCUDA_NORMALIZE_SCALE_IS_STDDEV) + { + scale = float{1} / std::sqrt(scale * scale + epsilon); + } + + return scale; +} + +template +static void WriteCropDestination(std::vector &hDst, long4_16a outStrides, bool dst_planar, int j, int i, int k, + T_Dst value) +{ + if (dst_planar) + { + test::ValueAt(hDst, outStrides, int4{j, i, k, 0}) = value; + } + else + { + test::ValueAt(hDst, outStrides, int4{k, j, i, 0}) = value; + } +} + +template +static void ClearCropDestination(std::vector &hDst, long4_16a outStrides, bool dst_planar, int numChannels, + int j, int i) +{ + for (int k = 0; k < numChannels; k++) + { + WriteCropDestination(hDst, outStrides, dst_planar, j, i, k, 0); + } +} + +template +static T_Src ReadCropSource(const std::vector &hSrc, const long4_16a &inStrides, bool src_planar, + nvcv::Size2D src_size, const float borderValue, int2 coord, int k) +{ + int2 size{src_size.w, src_size.h}; + auto typedBorderValue = cuda::SaturateCast(borderValue); + + if (src_planar) + { + return test::ValueAt(hSrc, long4_16a{inStrides.x, inStrides.z, inStrides.w, inStrides.y}, size, + typedBorderValue, int4{k, coord.x, coord.y, 0}); + } + + return test::ValueAt(hSrc, long4_16a{inStrides.x, inStrides.y, inStrides.z, inStrides.w}, size, + typedBorderValue, int4{k, coord.x, coord.y, 0}); +} + +template +static void NormalizeCropPixel(std::vector &hDst, long4_16a outStrides, const std::vector &hSrc, + long4_16a inStrides, nvcv::Size2D src_size, bool src_planar, bool dst_planar, + nvcv::ImageFormat fmt, const float borderValue, int2 coord, + const std::vector &hBase, int baseRowStride, int bi, int bj, + nvcv::ImageFormat baseFormat, const std::vector &hScale, int scaleRowStride, + int si, int sj, nvcv::ImageFormat scaleFormat, const float globalScale, + const float globalShift, const float epsilon, const uint32_t flags, int j, int i) +{ + for (int k = 0; k < fmt.numChannels(); k++) + { + const int bk = (baseFormat.numChannels() == 1 ? 0 : k); + const int sk = (scaleFormat.numChannels() == 1 ? 0 : k); + float mul = NormalizeScaleValue(hScale, scaleRowStride, scaleFormat, si, sj, sk, epsilon, flags); + float base = hBase.at(bi * baseRowStride + bj * baseFormat.numChannels() + bk); + T_Src out = ReadCropSource(hSrc, inStrides, src_planar, src_size, borderValue, coord, k); + + WriteCropDestination( + hDst, outStrides, dst_planar, j, i, k, + cuda::SaturateCast((static_cast(out) - base) * mul * globalScale + globalShift)); + } +} + +template +static void CropFlipNormalizeReformatRow(std::vector &hDst, long4_16a outStrides, + const std::vector &hSrc, long4_16a inStrides, nvcv::Size2D src_size, + nvcv::Size2D dst_size, nvcv::ImageFormat fmt, bool src_planar, bool dst_planar, + const float borderValue, int flip_code, const NVCVRectI &cropRect, + const std::vector &hBase, int baseRowStride, nvcv::Size2D baseSize, + nvcv::ImageFormat baseFormat, const std::vector &hScale, + int scaleRowStride, nvcv::Size2D scaleSize, nvcv::ImageFormat scaleFormat, + const float globalScale, const float globalShift, const float epsilon, + const uint32_t flags, int i) +{ + const int bi = baseSize.h == 1 ? 0 : i; + const int si = scaleSize.h == 1 ? 0 : i; + + for (int j = 0; j < dst_size.w; j++) + { + if (i >= cropRect.height || j >= cropRect.width) + { + ClearCropDestination(hDst, outStrides, dst_planar, fmt.numChannels(), j, i); + continue; + } + + const int bj = baseSize.w == 1 ? 0 : j; + const int sj = scaleSize.w == 1 ? 0 : j; + int2 coord = FlippedCropCoord(j, i, flip_code, cropRect); + + NormalizeCropPixel(hDst, outStrides, hSrc, inStrides, src_size, src_planar, dst_planar, fmt, + borderValue, coord, hBase, baseRowStride, bi, bj, baseFormat, hScale, + scaleRowStride, si, sj, scaleFormat, globalScale, globalShift, epsilon, + flags, j, i); + } +} + template -static void CropFlipNormalizeReformat(std::vector &hDst, int dstRowStride, long4_16a outStrides, - std::vector &hSrc, int srcRowStride, long4_16a inStrides, - nvcv::Size2D src_size, nvcv::Size2D dst_size, nvcv::ImageFormat fmt, - nvcv::ImageFormat dst_fmt, const float borderValue, int flip_code, - const NVCVRectI &cropRect, const std::vector &hBase, int baseRowStride, - nvcv::Size2D baseSize, nvcv::ImageFormat baseFormat, +static void CropFlipNormalizeReformat(std::vector &hDst, int, long4_16a outStrides, std::vector &hSrc, + int, long4_16a inStrides, nvcv::Size2D src_size, nvcv::Size2D dst_size, + nvcv::ImageFormat fmt, nvcv::ImageFormat dst_fmt, const float borderValue, + int flip_code, const NVCVRectI &cropRect, const std::vector &hBase, + int baseRowStride, nvcv::Size2D baseSize, nvcv::ImageFormat baseFormat, const std::vector &hScale, int scaleRowStride, nvcv::Size2D scaleSize, nvcv::ImageFormat scaleFormat, const float globalScale, const float globalShift, const float epsilon, const uint32_t flags) @@ -56,95 +196,10 @@ static void CropFlipNormalizeReformat(std::vector &hDst, int dstRowStri for (int i = 0; i < dst_size.h; i++) { - const int bi = baseSize.h == 1 ? 0 : i; - const int si = scaleSize.h == 1 ? 0 : i; - - for (int j = 0; j < dst_size.w; j++) - { - const int bj = baseSize.w == 1 ? 0 : j; - const int sj = scaleSize.w == 1 ? 0 : j; - if (i >= cropRect.height || j >= cropRect.width) - { - for (int k = 0; k < fmt.numChannels(); k++) - { - if (dst_planar) - { - test::ValueAt(hDst, outStrides, int4{j, i, k, 0}) = 0; - } - else - { - test::ValueAt(hDst, outStrides, int4{k, j, i, 0}) = 0; - } - } - continue; - } - for (int k = 0; k < fmt.numChannels(); k++) - { - const int bk = (baseFormat.numChannels() == 1 ? 0 : k); - const int sk = (scaleFormat.numChannels() == 1 ? 0 : k); - - float mul; - - if (flags & CVCUDA_NORMALIZE_SCALE_IS_STDDEV) - { - float s = hScale.at(si * scaleRowStride + sj * scaleFormat.numChannels() + sk); - float x = s * s + epsilon; - mul = float{1} / std::sqrt(x); - } - else - { - mul = hScale.at(si * scaleRowStride + sj * scaleFormat.numChannels() + sk); - } - - float base = hBase.at(bi * baseRowStride + bj * baseFormat.numChannels() + bk); - - int2 coord{j, i}, size{src_size.w, src_size.h}; - - if (flip_code == 1) - { - coord.x = cropRect.width - 1 - j + cropRect.x; - coord.y = i + cropRect.y; - } - else if (flip_code == 0) - { - coord.x = j + cropRect.x; - coord.y = cropRect.height - 1 - i + cropRect.y; - } - else if (flip_code == -1) - { - coord.x = cropRect.width - 1 - j + cropRect.x; - coord.y = cropRect.height - 1 - i + cropRect.y; - } - else - { - coord.x = j + cropRect.x; - coord.y = i + cropRect.y; - } - - T_Src out = 0; - if (src_planar) - { - out = test::ValueAt(hSrc, long4_16a{inStrides.x, inStrides.z, inStrides.w, inStrides.y}, - size, borderValue, int4{k, coord.x, coord.y, 0}); - } - else - { - out = test::ValueAt(hSrc, long4_16a{inStrides.x, inStrides.y, inStrides.z, inStrides.w}, - size, borderValue, int4{k, coord.x, coord.y, 0}); - } - - if (dst_planar) - { - test::ValueAt(hDst, outStrides, int4{j, i, k, 0}) - = cuda::SaturateCast((out - base) * mul * globalScale + globalShift); - } - else - { - test::ValueAt(hDst, outStrides, int4{k, j, i, 0}) - = cuda::SaturateCast((out - base) * mul * globalScale + globalShift); - } - } - } + CropFlipNormalizeReformatRow( + hDst, outStrides, hSrc, inStrides, src_size, dst_size, fmt, src_planar, dst_planar, borderValue, flip_code, + cropRect, hBase, baseRowStride, baseSize, baseFormat, hScale, scaleRowStride, scaleSize, scaleFormat, + globalScale, globalShift, epsilon, flags, i); } } @@ -152,9 +207,9 @@ constexpr uint32_t normalScale = 0; constexpr uint32_t scaleIsStdDev = CVCUDA_NORMALIZE_SCALE_IS_STDDEV; template -const void testCropFlipNormalizeReformatPad(int width, int height, int numImages, bool scalarBase, bool scalarScale, - uint32_t flags, float globalScale, float globalShift, float epsilon, - nvcv::ImageFormat fmt, nvcv::ImageFormat dst_fmt, NVCVBorderType borderMode) +void testCropFlipNormalizeReformatPad(int width, int height, int numImages, bool scalarBase, bool scalarScale, + uint32_t flags, float globalScale, float globalShift, float epsilon, + nvcv::ImageFormat fmt, nvcv::ImageFormat dst_fmt, NVCVBorderType borderMode) { cudaStream_t stream; EXPECT_EQ(cudaSuccess, cudaStreamCreate(&stream)); @@ -167,8 +222,8 @@ const void testCropFlipNormalizeReformatPad(int width, int height, int numImages std::default_random_engine rng; // Create input varshape - std::uniform_int_distribution udistWidth(width * 0.8, width * 1.1); - std::uniform_int_distribution udistHeight(height * 0.8, height * 1.1); + std::uniform_int_distribution udistWidth(ScaledSize(width, 0.8), ScaledSize(width, 1.1)); + std::uniform_int_distribution udistHeight(ScaledSize(height, 0.8), ScaledSize(height, 1.1)); std::vector imgSrc; @@ -191,13 +246,13 @@ const void testCropFlipNormalizeReformatPad(int width, int height, int numImages int srcRowStride = imgSrc[i].size().w * fmt.planePixelStrideBytes(0); srcVecRowStride[i] = srcRowStride; - std::uniform_int_distribution udist(0, 255); + std::uniform_int_distribution pixelDist(0, 255); srcVec[i].resize(imgSrc[i].size().h * srcRowStride * src_planes); for (int j = 0; j < imgSrc[i].size().h * imgSrc[i].size().w * numChannels; ++j) { - reinterpret_cast(srcVec[i].data())[j] = static_cast(udist(rng)); + reinterpret_cast(srcVec[i].data())[j] = static_cast(pixelDist(rng)); } auto imgData = imgSrc[i].exportData(); @@ -215,10 +270,10 @@ const void testCropFlipNormalizeReformatPad(int width, int height, int numImages batchSrc.pushBack(imgSrc.begin(), imgSrc.end()); // Create flip code tensor - std::uniform_int_distribution udistflip(-1, 2); - std::vector flip_vec(numImages); - nvcv::Tensor flipCode({{numImages}, "N"}, nvcv::TYPE_S32); - auto dev = flipCode.exportData(); + std::uniform_int_distribution udistflip(-1, 2); + std::vector flip_vec(numImages); + nvcv::Tensor flipCode({{numImages}, "N"}, nvcv::TYPE_S32); + auto dev = flipCode.exportData(); ASSERT_NE(dev, nullptr); for (int i = 0; i < numImages; ++i) { @@ -242,12 +297,12 @@ const void testCropFlipNormalizeReformatPad(int width, int height, int numImages std::vector cropVec; for (int i = 0; i < numImages; i++) { - std::uniform_int_distribution x_dist(-3, std::min(10, imgSrc[i].size().w / 10) + 1); - std::uniform_int_distribution y_dist(-3, std::min(10, imgSrc[i].size().h / 10) + 1); - std::uniform_int_distribution w_dist(std::max((int)(imgSrc[i].size().w * 0.8), imgSrc[i].size().w - 10), - imgSrc[i].size().w - 1); - std::uniform_int_distribution h_dist(std::max((int)(imgSrc[i].size().h * 0.8), imgSrc[i].size().h - 10), - imgSrc[i].size().h - 1); + std::uniform_int_distribution x_dist(-3, std::min(10, imgSrc[i].size().w / 10) + 1); + std::uniform_int_distribution y_dist(-3, std::min(10, imgSrc[i].size().h / 10) + 1); + std::uniform_int_distribution w_dist(std::max((int)(imgSrc[i].size().w * 0.8), imgSrc[i].size().w - 10), + imgSrc[i].size().w - 1); + std::uniform_int_distribution h_dist(std::max((int)(imgSrc[i].size().h * 0.8), imgSrc[i].size().h - 10), + imgSrc[i].size().h - 1); std::vector cropVecTmp = {x_dist(rng), y_dist(rng), w_dist(rng), h_dist(rng)}; @@ -271,8 +326,8 @@ const void testCropFlipNormalizeReformatPad(int width, int height, int numImages auto baseAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*baseData); ASSERT_TRUE(baseAccess); - std::uniform_real_distribution udist(0, 255.f); - generate(baseVec.begin(), baseVec.end(), [&]() { return udist(rng); }); + std::uniform_real_distribution baseDist(0, 255.f); + std::ranges::generate(baseVec, [&baseDist, &rng]() { return baseDist(rng); }); ASSERT_EQ(cudaSuccess, cudaMemcpy2D(baseAccess->sampleData(0), baseAccess->rowStride(), baseVec.data(), baseVec.size() * sizeof(float), @@ -293,8 +348,8 @@ const void testCropFlipNormalizeReformatPad(int width, int height, int numImages auto scaleAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*scaleData); ASSERT_TRUE(scaleAccess); - std::uniform_real_distribution udist(0, 1.f); - generate(scaleVec.begin(), scaleVec.end(), [&]() { return udist(rng); }); + std::uniform_real_distribution scaleDist(0, 1.f); + std::ranges::generate(scaleVec, [&scaleDist, &rng]() { return scaleDist(rng); }); ASSERT_EQ(cudaSuccess, cudaMemcpy2D(scaleAccess->sampleData(0), scaleAccess->rowStride(), scaleVec.data(), scaleVec.size() * sizeof(float), @@ -349,7 +404,7 @@ const void testCropFlipNormalizeReformatPad(int width, int height, int numImages std::vector goldVec(dst_height * dstRowStride * dst_planes); - NVCVRectI cropRect = {cropVec[4 * i], cropVec[4 * i + 1], cropVec[4 * i + 2], cropVec[4 * i + 3]}; + NVCVRectI cropRectValue = {cropVec[4 * i], cropVec[4 * i + 1], cropVec[4 * i + 2], cropVec[4 * i + 3]}; long4_16a inStrides; long4_16a outStrides; @@ -390,17 +445,16 @@ const void testCropFlipNormalizeReformatPad(int width, int height, int numImages // Generate gold result CropFlipNormalizeReformat( goldVec, dstRowStride, outStrides, srcVec[i], srcVecRowStride[i], inStrides, {src_width, src_height}, - {dst_width, dst_height}, fmt, dst_fmt, borderValue, flip_vec[i], cropRect, baseVec, 0, {1, 1}, baseFormat, - scaleVec, 0, {1, 1}, scaleFormat, globalScale, globalShift, epsilon, flags); + {dst_width, dst_height}, fmt, dst_fmt, borderValue, flip_vec[i], cropRectValue, baseVec, 0, {1, 1}, + baseFormat, scaleVec, 0, {1, 1}, scaleFormat, globalScale, globalShift, epsilon, flags); // Compare test and gold with correct type std::vector testVecTyped(dst_height * dst_width * numChannels); std::vector goldVecTyped(dst_height * dst_width * numChannels); - for (size_t j = 0; j < testVecTyped.size(); ++j) - { - testVecTyped[j] = reinterpret_cast(testVec.data())[j]; - goldVecTyped[j] = reinterpret_cast(goldVec.data())[j]; - } + auto *testData = reinterpret_cast(testVec.data()); + auto *goldData = reinterpret_cast(goldVec.data()); + std::copy_n(testData, testVecTyped.size(), testVecTyped.begin()); + std::copy_n(goldData, goldVecTyped.size(), goldVecTyped.begin()); VEC_EXPECT_NEAR(goldVecTyped, testVecTyped, 1e-4); } @@ -476,6 +530,104 @@ TYPED_TEST(OpCropFlipNormalizeReformat, correct_output) dst_fmt, borderMode); } +TEST(OpCropFlipNormalizeReformatPlanar, varshape_matches_interleaved) +{ + constexpr int width = 19; + constexpr int height = 13; + constexpr int numImages = 2; + constexpr int channels = 3; + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + std::vector srcInterleaved; + std::vector srcPlanar; + for (int i = 0; i < numImages; ++i) + { + srcInterleaved.emplace_back(nvcv::Size2D{width, height}, nvcv::FMT_RGB8); + srcPlanar.emplace_back(nvcv::Size2D{width, height}, nvcv::FMT_RGB8p); + + std::vector hwc(width * height * channels); + nvcv::test::planar::FillDeterministicValues(hwc, i * 101 + 17, nvcv::TYPE_U8); + auto interleavedData = srcInterleaved.back().exportData(); + ASSERT_TRUE(interleavedData); + ASSERT_EQ(cudaSuccess, + cudaMemcpy2D(interleavedData->plane(0).basePtr, interleavedData->plane(0).rowStride, hwc.data(), + width * channels, width * channels, height, cudaMemcpyHostToDevice)); + + auto planes = nvcv::test::planar::DeinterleaveToPlanes(hwc, width, height, channels, 1); + auto planarData = srcPlanar.back().exportData(); + ASSERT_TRUE(planarData); + for (int c = 0; c < channels; ++c) + { + ASSERT_EQ(cudaSuccess, + cudaMemcpy2D(planarData->plane(c).basePtr, planarData->plane(c).rowStride, + planes.data() + c * width * height, width, width, height, cudaMemcpyHostToDevice)); + } + } + + nvcv::ImageBatchVarShape batchInterleaved(numImages); + nvcv::ImageBatchVarShape batchPlanar(numImages); + batchInterleaved.pushBack(srcInterleaved.begin(), srcInterleaved.end()); + batchPlanar.pushBack(srcPlanar.begin(), srcPlanar.end()); + + nvcv::Tensor dstInterleaved(numImages, {width, height}, nvcv::FMT_RGB8); + nvcv::Tensor dstPlanar(numImages, {width, height}, nvcv::FMT_RGB8p); + nvcv::Tensor cropRect( + { + {numImages, 1, 1, 4}, + nvcv::TENSOR_NHWC + }, + nvcv::TYPE_S32); + auto cropData = cropRect.exportData(); + auto cropAccess = nvcv::TensorDataAccessStridedImage::Create(*cropData); + ASSERT_TRUE(cropAccess); + const std::array fullCrop{0, 0, width, height}; + for (int i = 0; i < numImages; ++i) + { + ASSERT_EQ(cudaSuccess, + cudaMemcpy(cropAccess->sampleData(i), fullCrop.data(), sizeof(fullCrop), cudaMemcpyHostToDevice)); + } + + nvcv::Tensor flipCode = nvcv::test::planar::MakePerImageTensor(numImages, nvcv::TYPE_S32, 0); + nvcv::Tensor base( + { + {1, 1, 1, 1}, + nvcv::TENSOR_NHWC + }, + nvcv::TYPE_F32); + nvcv::Tensor scale( + { + {1, 1, 1, 1}, + nvcv::TENSOR_NHWC + }, + nvcv::TYPE_F32); + nvcv::test::planar::UploadTensorValues(base, std::vector{0.f}); + nvcv::test::planar::UploadTensorValues(scale, std::vector{1.f}); + + cvcuda::CropFlipNormalizeReformat op; + EXPECT_NO_THROW(op(stream, batchInterleaved, dstInterleaved, cropRect, NVCV_BORDER_REPLICATE, 0.f, flipCode, base, + scale, 1.f, 0.f, 0.f, 0)); + EXPECT_NO_THROW(op(stream, batchPlanar, dstPlanar, cropRect, NVCV_BORDER_REPLICATE, 0.f, flipCode, base, scale, 1.f, + 0.f, 0.f, 0)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + auto interleavedData = dstInterleaved.exportData(); + auto planarData = dstPlanar.exportData(); + auto interleavedAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*interleavedData); + auto planarAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*planarData); + ASSERT_TRUE(interleavedAccess && planarAccess); + for (int i = 0; i < numImages; ++i) + { + auto gpuInter + = nvcv::test::planar::DownloadInterleavedSample(*interleavedAccess, i, width, height, width * channels); + auto planesOut = nvcv::test::planar::DownloadPlanarSample(*planarAccess, i, width, height, channels, 1); + EXPECT_EQ(gpuInter, nvcv::test::planar::InterleaveFromPlanes(planesOut, width, height, channels, 1)); + } + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + TEST(OpCropFlipNormalizeReformat_Negative, create_with_null_handle) { EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, cvcudaCropFlipNormalizeReformatCreate(nullptr)); diff --git a/tests/cvcuda/system/TestOpCustomCrop.cpp b/tests/cvcuda/system/TestOpCustomCrop.cpp index d7dec9f4a..0b3d4743a 100644 --- a/tests/cvcuda/system/TestOpCustomCrop.cpp +++ b/tests/cvcuda/system/TestOpCustomCrop.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,6 +16,7 @@ */ #include "Definitions.hpp" +#include "PlanarParityUtils.hpp" #include #include @@ -24,14 +25,14 @@ #include #include +#include +#include #include #include namespace gt = ::testing; namespace test = nvcv::test; -//#define DBG_CROP_RECT - #ifdef DBG_CROP_RECT static void dbgImage(std::vector &in, int rowStride) { @@ -52,12 +53,12 @@ static void WriteData(const nvcv::TensorDataAccessStridedImagePlanar &data, uint EXPECT_LE(region.x + region.width, data.numCols()); EXPECT_LE(region.y + region.height, data.numRows()); - int bytesPerChan = data.dtype().bitsPerChannel()[0] / 8; - int bytesPerPixel = data.numChannels() * bytesPerChan; - uint8_t *impPtrTop = (uint8_t *)data.sampleData(0); - uint8_t *impPtr = nullptr; - int numImages = data.numSamples(); - int rowStride = data.rowStride(); + int bytesPerChan = data.dtype().bitsPerChannel()[0] / 8; + int bytesPerPixel = data.numChannels() * bytesPerChan; + auto *impPtrTop = reinterpret_cast(data.sampleData(0)); + std::byte *impPtr = nullptr; + auto numImages = static_cast(data.numSamples()); + auto rowStride = static_cast(data.rowStride()); EXPECT_NE(nullptr, impPtrTop); for (int img = 0; img < numImages; img++) @@ -74,8 +75,9 @@ static void setGoldBuffer(std::vector &vect, const nvcv::TensorDataAcce int bytesPerChan = data.dtype().bitsPerChannel()[0] / 8; int bytesPerPixel = data.numChannels() * bytesPerChan; - uint8_t *ptrTop = vect.data(); - for (int img = 0; img < data.numSamples(); img++) + uint8_t *ptrTop = vect.data(); + auto numImages = static_cast(data.numSamples()); + for (int img = 0; img < numImages; img++) { uint8_t *ptr = ptrTop + data.sampleStride() * img; for (int i = 0; i < region.height; i++) @@ -147,16 +149,17 @@ TEST_P(OpCustomCrop, CustomCrop_packed) auto outAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*outData); ASSERT_TRUE(outAccess); - int inSampleStride = inAccess->numRows() * inAccess->rowStride(); - int outSampleStride = outAccess->numRows() * outAccess->rowStride(); + auto inSampleStride = static_cast(inAccess->numRows() * inAccess->rowStride()); + auto outSampleStride = static_cast(outAccess->numRows() * outAccess->rowStride()); + auto numSamples = static_cast(inAccess->numSamples()); - int inBufSize = inSampleStride * inAccess->numSamples(); - int outBufSize = outSampleStride * outAccess->numSamples(); + size_t inBufSize = inSampleStride * numSamples; + size_t outBufSize = outSampleStride * numSamples; NVCVRectI crpRect = {cropX, cropY, cropWidth, cropHeight}; - EXPECT_EQ(cudaSuccess, cudaMemset(inData->basePtr(), 0x00, inSampleStride * inAccess->numSamples())); - EXPECT_EQ(cudaSuccess, cudaMemset(outData->basePtr(), 0x00, outSampleStride * outAccess->numSamples())); + EXPECT_EQ(cudaSuccess, cudaMemset(inData->basePtr(), 0x00, inBufSize)); + EXPECT_EQ(cudaSuccess, cudaMemset(outData->basePtr(), 0x00, outBufSize)); WriteData(*inAccess, cropVal, crpRect); // write data to be cropped std::vector gold(outBufSize); @@ -185,6 +188,209 @@ TEST_P(OpCustomCrop, CustomCrop_packed) EXPECT_EQ(gold, test); } +// Parameters: dtype, channels, input width, input height, crop width, crop height, crop x, crop y, batch +// clang-format off +NVCV_TEST_SUITE_P(OpCustomCropGold, + test::ValueList{ + // Exercise every byte-width/channel dispatch cell. Odd widths and unaligned origins protect + // vectorized implementations' body and scalar-tail paths. + { nvcv::TYPE_U8, 1, 19, 9, 1, 1, 0, 0, 1}, + { nvcv::TYPE_S8, 2, 23, 11, 7, 3, 1, 2, 2}, + { nvcv::TYPE_U8, 3, 41, 13, 17, 5, 3, 1, 3}, + { nvcv::TYPE_S8, 4, 59, 15, 33, 7, 5, 4, 2}, + + {nvcv::TYPE_U16, 1, 31, 10, 5, 4, 2, 3, 2}, + {nvcv::TYPE_S16, 2, 38, 12, 16, 6, 1, 1, 1}, + {nvcv::TYPE_F16, 3, 47, 14, 31, 5, 7, 6, 2}, + {nvcv::TYPE_U16, 4, 69, 16, 35, 8, 9, 3, 3}, + + {nvcv::TYPE_S32, 1, 29, 9, 3, 3, 4, 2, 2}, + {nvcv::TYPE_F32, 2, 43, 11, 15, 5, 2, 3, 1}, + {nvcv::TYPE_S32, 3, 61, 13, 32, 7, 11, 2, 3}, + {nvcv::TYPE_F32, 4, 71, 17, 37, 9, 13, 5, 2}, + + {nvcv::TYPE_F64, 1, 27, 8, 2, 2, 1, 1, 1}, + {nvcv::TYPE_F64, 2, 45, 12, 13, 4, 5, 4, 2}, + {nvcv::TYPE_F64, 3, 63, 14, 34, 6, 7, 5, 3}, + {nvcv::TYPE_F64, 4, 79, 18, 39, 10, 17, 3, 2}, +}); + +// clang-format on + +TEST_P(OpCustomCropGold, tensor_correct_output) +{ + const nvcv::DataType dtype = GetParamValue<0>(); + const int channels = GetParamValue<1>(); + const int inWidth = GetParamValue<2>(); + const int inHeight = GetParamValue<3>(); + const int cropWidth = GetParamValue<4>(); + const int cropHeight = GetParamValue<5>(); + const int cropX = GetParamValue<6>(); + const int cropY = GetParamValue<7>(); + const int numberOfImages = GetParamValue<8>(); + const NVCVRectI cropRect{cropX, cropY, cropWidth, cropHeight}; + + const nvcv::TensorShape inShape{ + {numberOfImages, inHeight, inWidth, channels}, + "NHWC" + }; + const nvcv::TensorShape outShape{ + {numberOfImages, cropHeight, cropWidth, channels}, + "NHWC" + }; + nvcv::Tensor imgIn(inShape, dtype); + nvcv::Tensor imgOut(outShape, dtype); + + auto inData = imgIn.exportData(); + auto outData = imgOut.exportData(); + ASSERT_NE(nullptr, inData); + ASSERT_NE(nullptr, outData); + + auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*inData); + auto outAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*outData); + ASSERT_TRUE(inAccess); + ASSERT_TRUE(outAccess); + + const auto bytesPerChannel = static_cast(dtype.bitsPerChannel()[0] / 8); + const size_t bytesPerPixel = static_cast(channels) * bytesPerChannel; + const auto inRowStride = static_cast(inAccess->rowStride()); + const auto outRowStride = static_cast(outAccess->rowStride()); + const auto inSampleStride = static_cast(inAccess->sampleStride()); + const auto outSampleStride = static_cast(outAccess->sampleStride()); + const size_t inBufSize = inSampleStride * (numberOfImages - 1) + inRowStride * inHeight; + const size_t outBufSize = outSampleStride * (numberOfImages - 1) + outRowStride * cropHeight; + + std::vector src(inBufSize, 0xC3); + for (int n = 0; n < numberOfImages; ++n) + { + for (int y = 0; y < inHeight; ++y) + { + for (size_t byte = 0; byte < static_cast(inWidth) * bytesPerPixel; ++byte) + { + const size_t x = byte / bytesPerPixel; + const size_t channelByte = byte % bytesPerPixel; + const size_t c = channelByte / bytesPerChannel; + const size_t b = channelByte % bytesPerChannel; + const size_t offset + = static_cast(n) * inSampleStride + static_cast(y) * inRowStride + byte; + src[offset] = static_cast((97 * n + 31 * y + 17 * x + 7 * c + 3 * b + 11) % 251); + } + } + } + + // Independent host oracle: copy the addressed byte rectangle row by row. This deliberately does + // not reuse the device kernel's wrappers, launch geometry, or dispatch table. + std::vector gold(outBufSize, 0xA5); + for (int n = 0; n < numberOfImages; ++n) + { + for (int y = 0; y < cropHeight; ++y) + { + const size_t srcOffset = static_cast(n) * inSampleStride + + static_cast(y + cropY) * inRowStride + + static_cast(cropX) * bytesPerPixel; + const size_t dstOffset = static_cast(n) * outSampleStride + static_cast(y) * outRowStride; + std::memcpy(gold.data() + dstOffset, src.data() + srcOffset, + static_cast(cropWidth) * bytesPerPixel); + } + } + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + ASSERT_EQ(cudaSuccess, cudaMemcpyAsync(inData->basePtr(), src.data(), inBufSize, cudaMemcpyHostToDevice, stream)); + ASSERT_EQ(cudaSuccess, cudaMemsetAsync(outData->basePtr(), 0xA5, outBufSize, stream)); + + cvcuda::CustomCrop cropOp; + EXPECT_NO_THROW(cropOp(stream, imgIn, imgOut, cropRect)); + + std::vector test(outBufSize); + ASSERT_EQ(cudaSuccess, + cudaMemcpyAsync(test.data(), outData->basePtr(), outBufSize, cudaMemcpyDeviceToHost, stream)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + EXPECT_EQ(gold, test); + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +// ============================================================================= +// Planar (NCHW/CHW) layout support +// +// CustomCrop copies a sub-region of each channel plane independently, so cropping a planar image is +// just copying N*C single-channel planes. These tests feed identical data through cvcuda::CustomCrop +// in interleaved and planar layout and require the (re-interleaved) planar output to match the +// interleaved output bit-for-bit, for every dtype and crop geometry. CustomCrop is tensor-only, so +// there is no var-shape parity case. +// ============================================================================= + +// Parameters: inWidth, inHeight, cropWidth, cropHeight, cropX, cropY, numImages, planarFmt, interleavedFmt +// clang-format off +NVCV_TEST_SUITE_P(OpCustomCropPlanar, + test::ValueList{ + {176, 113, 100, 64, 20, 10, 2, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, // RGB8, offset crop, batch + {123, 66, 50, 40, 0, 0, 1, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, // RGB8, top-left crop + { 64, 48, 32, 24, 16, 12, 2, nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8}, // RGBA8 (uchar4 planar tensor) + { 50, 40, 25, 20, 5, 5, 1, nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8}, // RGBA8, interior crop + { 64, 48, 40, 30, 8, 6, 2, nvcv::FMT_RGBAf32p, nvcv::FMT_RGBAf32}, // float planar +}); + +// clang-format on + +TEST_P(OpCustomCropPlanar, tensor_matches_interleaved) +{ + // Crop identical data in interleaved and planar tensor layout; outputs must match bit-for-bit. + // The shared upload/run/download/compare scaffolding lives in PlanarParityUtils.hpp; here we only + // bind the CustomCrop call and its ROI. + const NVCVRectI crpRect = {GetParamValue<4>(), GetParamValue<5>(), GetParamValue<2>(), GetParamValue<3>()}; + test::planar::RunTensorParity( + GetParamValue<7>(), GetParamValue<8>(), GetParamValue<0>(), GetParamValue<1>(), GetParamValue<2>(), + GetParamValue<3>(), GetParamValue<6>(), + [crpRect](cudaStream_t s, const nvcv::Tensor &src, const nvcv::Tensor &dst, nvcv::ImageFormat) + { + cvcuda::CustomCrop op; + EXPECT_NO_THROW(op(s, src, dst, crpRect)); + }); +} + +TEST_P(OpCustomCropPlanar, rejects_grid_z_overflow) +{ + const NVCVRectI crpRect = {0, 0, 1, 1}; + cvcuda::CustomCrop cropOp; + test::planar::ExpectPlanarTensorRejected( + {21846, 3, 1, 1}, {21846, 3, 1, 1}, + [&cropOp, &crpRect](cudaStream_t s, const nvcv::Tensor &in, const nvcv::Tensor &out) + { cropOp(s, in, out, crpRect); }); +} + +TEST_P(OpCustomCropPlanar, rejects_non_tightly_packed_batched_tensors) +{ + auto makePaddedNCHW = [](int numSamples, int numChannels, int height, int width) + { + nvcv::TensorDataStridedCuda::Buffer buf{}; + const int64_t channelStride = static_cast(height) * width; + buf.basePtr = reinterpret_cast(0xDEADBEEFULL); + buf.strides[0] = numChannels * channelStride + 1; + buf.strides[1] = channelStride; + buf.strides[2] = width; + buf.strides[3] = 1; + return nvcv::TensorWrapData(nvcv::TensorDataStridedCuda{ + nvcv::TensorShape{{numSamples, numChannels, height, width}, "NCHW"}, + nvcv::TYPE_U8, buf + }); + }; + + cudaStream_t stream; + EXPECT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + nvcv::Tensor imgIn = makePaddedNCHW(2, 3, 5, 5); + nvcv::Tensor imgOut = makePaddedNCHW(2, 3, 2, 2); + NVCVRectI crpRect = {0, 0, 2, 2}; + + cvcuda::CustomCrop cropOp; + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&cropOp, &stream, &imgIn, &imgOut, &crpRect] + { cropOp(stream, imgIn, imgOut, crpRect); })); + + EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + // clang-format off NVCV_TEST_SUITE_P(OpCustomCrop_Negative, test::ValueList{ {nvcv::FMT_RGB8p, nvcv::FMT_RGB8, 5, 5, 2, 2, 0, 0}, @@ -196,6 +402,14 @@ NVCV_TEST_SUITE_P(OpCustomCrop_Negative, test::ValueList{ + {1, 3, 1, 4}, + {1, 3, 2, 3}, +}); + +// clang-format on + +TEST_P(OpCustomCropInterleavedShapeNegative, rejects_mismatched_sample_or_channel_count) +{ + const int inSamples = GetParamValue<0>(); + const int inChannels = GetParamValue<1>(); + const int outSamples = GetParamValue<2>(); + const int outChannels = GetParamValue<3>(); + const NVCVRectI cropRect{0, 0, 2, 2}; + + nvcv::Tensor imgIn( + nvcv::TensorShape{ + {inSamples, 5, 5, inChannels}, + "NHWC" + }, + nvcv::TYPE_U8); + nvcv::Tensor imgOut( + nvcv::TensorShape{ + {outSamples, 2, 2, outChannels}, + "NHWC" + }, + nvcv::TYPE_U8); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + cvcuda::CustomCrop cropOp; + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&cropOp, stream, &imgIn, &imgOut, &cropRect] + { cropOp(stream, imgIn, imgOut, cropRect); })); + EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpCustomCrop_Negative, planar_rejects_mismatched_sample_channel_shape) +{ + // Flattening planar tensors as N*C single-channel planes would make these both look like six + // samples. CustomCrop must preserve the original sample/channel contract before flattening. + const NVCVRectI crpRect = {0, 0, 2, 2}; + cvcuda::CustomCrop cropOp; + test::planar::ExpectPlanarTensorRejected( + {2, 3, 5, 5}, {3, 2, 2, 2}, + [&cropOp, &crpRect](cudaStream_t s, const nvcv::Tensor &in, const nvcv::Tensor &out) + { cropOp(s, in, out, crpRect); }); +} diff --git a/tests/cvcuda/system/TestOpCvtColor.cpp b/tests/cvcuda/system/TestOpCvtColor.cpp index acc659c30..653b29418 100644 --- a/tests/cvcuda/system/TestOpCvtColor.cpp +++ b/tests/cvcuda/system/TestOpCvtColor.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "CvtColorUtils.hpp" #include "Definitions.hpp" +#include "PlanarParityUtils.hpp" #include "TestUtils.hpp" #include @@ -27,12 +28,20 @@ #include #include +#include +#include + namespace test = nvcv::test; namespace util = nvcv::util; namespace cuda = nvcv::cuda; using std::vector; +static int ScaledSize(int size, double scale) +{ + return static_cast(size * scale); +} + #define NVCV_IMAGE_FORMAT_RGBS8 NVCV_DETAIL_MAKE_COLOR_FMT1(RGB, UNDEFINED, PL, SIGNED, XYZ1, ASSOCIATED, X8_Y8_Z8) #define NVCV_IMAGE_FORMAT_BGRS8 NVCV_DETAIL_MAKE_COLOR_FMT1(RGB, UNDEFINED, PL, SIGNED, ZYX1, ASSOCIATED, X8_Y8_Z8) #define NVCV_IMAGE_FORMAT_RGBAS8 NVCV_DETAIL_MAKE_COLOR_FMT1(RGB, UNDEFINED, PL, SIGNED, XYZW, ASSOCIATED, X8_Y8_Z8_W8) @@ -83,320 +92,346 @@ using std::vector; // clang-format off //--------------------------------------------------------------------------------------------------------------------// -template -static void verifyOutput(nvcv::Tensor srcTensor, nvcv::ImageFormat srcFrmt, - nvcv::Tensor dstTensor, nvcv::ImageFormat dstFrmt, - NVCVColorConversionCode code, int wdth, int hght, int imgs, double maxDiff) +static bool IsInterleavedYuv422(nvcv::ImageFormat fmt) { - auto srcData = srcTensor.exportData(); - auto dstData = dstTensor.exportData(); - ASSERT_TRUE(srcData); - ASSERT_TRUE(dstData); - - auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcData); - auto dstAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*dstData); - ASSERT_TRUE(srcAccess); - ASSERT_TRUE(dstAccess); - - int srcChannels = srcAccess->numChannels(); - int dstChannels = dstAccess->numChannels(); - - ASSERT_LE(srcChannels, 4); - ASSERT_LE(dstChannels, 4); - - int srcWdth = wdth, - srcHght = hght; - int dstWdth = wdth, - dstHght = hght; - - if (srcFrmt == NVCV_IMAGE_FORMAT_UYVY || srcFrmt == NVCV_IMAGE_FORMAT_UYVY_ER || - srcFrmt == NVCV_IMAGE_FORMAT_YUYV || srcFrmt == NVCV_IMAGE_FORMAT_YUYV_ER) - srcWdth = srcWdth << 1; - if (srcFrmt == NVCV_IMAGE_FORMAT_NV12 || srcFrmt == NVCV_IMAGE_FORMAT_NV12_ER || - srcFrmt == NVCV_IMAGE_FORMAT_NV21 || srcFrmt == NVCV_IMAGE_FORMAT_NV21_ER) - srcHght = (srcHght * 3) >> 1; - ASSERT_EQ(srcWdth, srcAccess->numCols()); - ASSERT_EQ(srcHght, srcAccess->numRows()); - - if (dstFrmt == NVCV_IMAGE_FORMAT_UYVY || dstFrmt == NVCV_IMAGE_FORMAT_UYVY_ER || - dstFrmt == NVCV_IMAGE_FORMAT_YUYV || dstFrmt == NVCV_IMAGE_FORMAT_YUYV_ER) - dstWdth = dstWdth << 1; - if (dstFrmt == NVCV_IMAGE_FORMAT_NV12 || dstFrmt == NVCV_IMAGE_FORMAT_NV12_ER || - dstFrmt == NVCV_IMAGE_FORMAT_NV21 || dstFrmt == NVCV_IMAGE_FORMAT_NV21_ER) - dstHght = (dstHght * 3) >> 1; - ASSERT_EQ(dstWdth, dstAccess->numCols()); - ASSERT_EQ(dstHght, dstAccess->numRows()); + return fmt == NVCV_IMAGE_FORMAT_UYVY || fmt == NVCV_IMAGE_FORMAT_UYVY_ER || fmt == NVCV_IMAGE_FORMAT_YUYV + || fmt == NVCV_IMAGE_FORMAT_YUYV_ER; +} - int srcRowElems = srcChannels * srcWdth; - int dstRowElems = dstChannels * dstWdth; +static bool IsSemiPlanarYuv420(nvcv::ImageFormat fmt) +{ + return fmt == NVCV_IMAGE_FORMAT_NV12 || fmt == NVCV_IMAGE_FORMAT_NV12_ER || fmt == NVCV_IMAGE_FORMAT_NV21 + || fmt == NVCV_IMAGE_FORMAT_NV21_ER; +} - size_t numPixels = (size_t)imgs * (size_t)wdth * (size_t)hght; - size_t srcElems = (size_t)imgs * (size_t)srcWdth * (size_t)srcHght * (size_t)srcChannels; - size_t dstElems = (size_t)imgs * (size_t)dstWdth * (size_t)dstHght * (size_t)dstChannels; +static bool IsBGR(nvcv::Swizzle swizzle) +{ + return swizzle == nvcv::Swizzle::S_ZYXW || swizzle == nvcv::Swizzle::S_ZYX1 + || swizzle == nvcv::Swizzle::S_ZYX0; +} - size_t srcPitchCPU = srcRowElems * sizeof(T); - size_t dstPitchCPU = dstRowElems * sizeof(T); +static bool IsHsvToRgb(NVCVColorConversionCode code) +{ + return code == NVCV_COLOR_HSV2BGR || code == NVCV_COLOR_HSV2BGR_FULL || code == NVCV_COLOR_HSV2RGB + || code == NVCV_COLOR_HSV2RGB_FULL; +} - nvcv::Swizzle srcSwizzle = srcFrmt.swizzle(); - nvcv::Swizzle dstSwizzle = dstFrmt.swizzle(); +static bool IsFullHsvToRgb(NVCVColorConversionCode code) +{ + return code == NVCV_COLOR_HSV2BGR_FULL || code == NVCV_COLOR_HSV2RGB_FULL; +} - vector srcVec(srcElems); - vector refVec(dstElems); +template +static void GenerateHsvSource(vector &srcVec, int srcWdth, int srcHght, int imgs, size_t numPixels, RandEng &randEng) +{ + constexpr size_t minCntAllHSV = 90 * 256 * 256; // Minimum # of pixels to call generateAllHSV. + constexpr double minMultHSV = -0.5; // Set hue range multiplier to be outside normal range + constexpr double maxMultHSV = 1.5; // to test robustness to wrapped hue values. - bool srcBGR = (srcSwizzle == nvcv::Swizzle::S_ZYXW || - srcSwizzle == nvcv::Swizzle::S_ZYX1 || - srcSwizzle == nvcv::Swizzle::S_ZYX0); - bool dstBGR = (dstSwizzle == nvcv::Swizzle::S_ZYXW || - dstSwizzle == nvcv::Swizzle::S_ZYX1 || - dstSwizzle == nvcv::Swizzle::S_ZYX0); - bool srcRGBA = (srcChannels == 4), - dstRGBA = (dstChannels == 4); - bool success = true; + if (numPixels >= minCntAllHSV) + { + generateAllHSV(srcVec, srcWdth, srcHght, imgs); + return; + } - RandEng randEng(0); + generateRandHSV(srcVec, randEng, minMultHSV, maxMultHSV); +} +template +static void PopulateSource(vector &srcVec, int srcWdth, int srcHght, int imgs, int srcChannels, size_t numPixels, + bool srcRGBA, bool srcBGR, NVCVColorConversionCode code, RandEng &randEng) +{ constexpr size_t minCntAllRGB = 128 * 256 * 256; // Minimum # of pixels to call generateAllRGB. - constexpr size_t minCntAllHSV = 90 * 256 * 256; // Minimum # of pixels to call generateAllHSV. - constexpr double minMultHSV = -0.5; // Set hue range multiplier to be outside normal range - constexpr double maxMultHSV = 1.5; // to test robustness to wrapped hue values. - // Populate source tensor. - if (srcChannels > 2) + if (srcChannels <= 2) { - if (code == NVCV_COLOR_HSV2BGR || code == NVCV_COLOR_HSV2BGR_FULL || - code == NVCV_COLOR_HSV2RGB || code == NVCV_COLOR_HSV2RGB_FULL) - { - bool full = (code == NVCV_COLOR_HSV2BGR_FULL || code == NVCV_COLOR_HSV2RGB_FULL); + generateRandVec(srcVec, randEng); + return; + } - if (numPixels >= minCntAllHSV) - { - if (full) generateAllHSV(srcVec, srcWdth, srcHght, imgs); - else generateAllHSV(srcVec, srcWdth, srcHght, imgs); - } - else - { - if (full) generateRandHSV(srcVec, randEng, minMultHSV, maxMultHSV); - else generateRandHSV(srcVec, randEng, minMultHSV, maxMultHSV); - } + if (IsHsvToRgb(code)) + { + if (IsFullHsvToRgb(code)) + { + GenerateHsvSource(srcVec, srcWdth, srcHght, imgs, numPixels, randEng); } else { - if (numPixels >= minCntAllRGB) - generateAllRGB(srcVec, srcWdth, srcHght, imgs, srcRGBA, srcBGR); - else - generateRandTestRGB(srcVec, randEng, srcRGBA, srcBGR); + GenerateHsvSource(srcVec, srcWdth, srcHght, imgs, numPixels, randEng); } + return; } - else - generateRandVec(srcVec, randEng); - // Copy source from image vector to device tensor. - ASSERT_EQ(cudaSuccess, cudaMemcpy2D(srcData->basePtr(), srcAccess->rowStride(), srcVec.data(), srcPitchCPU, - srcPitchCPU, (size_t)imgs * (size_t)srcHght, cudaMemcpyHostToDevice)); + if (numPixels >= minCntAllRGB) + { + generateAllRGB(srcVec, srcWdth, srcHght, imgs, srcRGBA, srcBGR); + return; + } - switch (code) + generateRandTestRGB(srcVec, randEng, srcRGBA, srcBGR); +} + +template +static bool BuildBasicColorReference(vector &refVec, const vector &srcVec, NVCVColorConversionCode code, + size_t numPixels, bool srcRGBA, bool srcBGR, bool dstRGBA, bool dstBGR) +{ + switch (code) // NOSONAR: reference implementation covers each supported conversion code. { - // Add/remove alpha channel to RGB/BGR image. - case NVCV_COLOR_BGR2BGRA : // NVCV_COLOR_BGR2BGRA = 0 (NVCV_COLOR_RGB2RGBA) - case NVCV_COLOR_BGRA2BGR : // NVCV_COLOR_BGRA2BGR = 1 (NVCV_COLOR_RGBA2RGB) + case NVCV_COLOR_BGR2BGRA: + case NVCV_COLOR_BGRA2BGR: changeAlpha(refVec, srcVec, numPixels, srcRGBA, dstRGBA); - break; + return true; - // Convert between RGB and BGR (with or without alpha channel). - case NVCV_COLOR_BGR2RGBA : // NVCV_COLOR_BGR2RGBA = 2 (NVCV_COLOR_RGB2BGRA) - case NVCV_COLOR_RGBA2BGR : // NVCV_COLOR_RGBA2BGR = 3 (NVCV_COLOR_BGRA2RGB) - case NVCV_COLOR_BGR2RGB : // NVCV_COLOR_BGR2RGB = 4 (NVCV_COLOR_BGR2RGB) - case NVCV_COLOR_BGRA2RGBA : // NVCV_COLOR_BGRA2RGBA = 5 (NVCV_COLOR_RGBA2BGRA) + case NVCV_COLOR_BGR2RGBA: + case NVCV_COLOR_RGBA2BGR: + case NVCV_COLOR_BGR2RGB: + case NVCV_COLOR_BGRA2RGBA: convertRGBtoBGR(refVec, srcVec, numPixels, srcRGBA, dstRGBA); - break; + return true; - // Convert from RGB/BGR to grayscale. - case NVCV_COLOR_BGR2GRAY : // NVCV_COLOR_BGR2GRAY = 6 - case NVCV_COLOR_RGB2GRAY : // NVCV_COLOR_RGB2GRAY = 7 - case NVCV_COLOR_BGRA2GRAY : // NVCV_COLOR_BGRA2GRAY = 10 - case NVCV_COLOR_RGBA2GRAY : // NVCV_COLOR_RGBA2GRAY = 11 + case NVCV_COLOR_BGR2GRAY: + case NVCV_COLOR_RGB2GRAY: + case NVCV_COLOR_BGRA2GRAY: + case NVCV_COLOR_RGBA2GRAY: convertRGBtoGray(refVec, srcVec, numPixels, srcRGBA, srcBGR); - break; + return true; - // Convert from grayscale to RGB/BGR. - case NVCV_COLOR_GRAY2BGR : // NVCV_COLOR_GRAY2BGR = 8 (NVCV_COLOR_GRAY2RGB) - case NVCV_COLOR_GRAY2BGRA : // NVCV_COLOR_GRAY2BGRA = 9 (NVCV_COLOR_GRAY2RGBA) + case NVCV_COLOR_GRAY2BGR: + case NVCV_COLOR_GRAY2BGRA: convertGrayToRGB(refVec, srcVec, numPixels, dstRGBA); - break; + return true; - // Convert between RGB/BGR and BGR565 (16-bit images) --> Conversion codes 12-19 not implemented. - // Convert between grayscale and BGR565 (16-bit images) --> Conversion codes 20-21 not implemented. - // Convert between RGB/BGR and BGR555 (16-bit images) --> Conversion codes 22-29 not implemented. - // Convert between grayscale and BGR555 (16-bit images) --> Conversion codes 30-31 not implemented. - // Convert between RGB/BGR and CIE XYZ --> Conversion codes 32-35 not implemented. - // Convert between RGB/BGR and YCrCb (aka YCC) --> Conversion codes 36-39 not implemented. - - // Convert from RGB/BGR to HSV (hue, saturation, value). - case NVCV_COLOR_BGR2HSV : // NVCV_COLOR_BGR2HSV = 40 - case NVCV_COLOR_RGB2HSV : // NVCV_COLOR_RGB2HSV = 41 + case NVCV_COLOR_BGR2HSV: + case NVCV_COLOR_RGB2HSV: convertRGBtoHSV(refVec, srcVec, numPixels, srcRGBA, srcBGR); - break; + return true; - // Conversion codes 42 and 43 not specified. - // Convert from RGB/BGR to CIE Lab --> Conversion codes 44-45 not implemented. - // Bayer demosaicing to RGB/BGR --> Conversion codes 46-49 not implemented. - // Convert from RGB/BGR to CIE Luv --> Conversion codes 50-51 not implemented. - // Convert from RGB/BGR to HLS (hue, lightness, saturation) --> Conversion codes 52-53 not implemented. - - // Convert from HSV (hue, saturation, value) to RGB/BGR. - case NVCV_COLOR_HSV2BGR : // NVCV_COLOR_HSV2BGR = 54 - case NVCV_COLOR_HSV2RGB : // NVCV_COLOR_HSV2RGB = 55 + case NVCV_COLOR_HSV2BGR: + case NVCV_COLOR_HSV2RGB: convertHSVtoRGB(refVec, srcVec, numPixels, dstRGBA, dstBGR); - break; - - // Convert to RGB/BGR from CIE Lab --> Conversion codes 56-57 not implemented. - // Convert to RGB/BGR from CIE Luv --> Conversion codes 58-59 not implemented. - // Convert to RGB/BGR from HLS (hue, lightness, saturation) --> Conversion codes 60-61 not implemented. - // VNG (Variable Number of Gradients) demosaicing to RGB/BGR --> Conversion codes 62-65 not implemented. + return true; - // Convert from RGB/BGR to full-range HSV (hue, saturation, value). - case NVCV_COLOR_BGR2HSV_FULL : // NVCV_COLOR_BGR2HSV_FULL = 66 - case NVCV_COLOR_RGB2HSV_FULL : // NVCV_COLOR_RGB2HSV_FULL = 67 + case NVCV_COLOR_BGR2HSV_FULL: + case NVCV_COLOR_RGB2HSV_FULL: convertRGBtoHSV(refVec, srcVec, numPixels, srcRGBA, srcBGR); - break; - - // Convert from RGB/BGR to full-range HLS (hue, lightness, saturation) --> Conversion codes 68-69 not implemented. + return true; - // Convert from full-range HSV (hue, saturation, value) to RGB/BGR. - case NVCV_COLOR_HSV2BGR_FULL : // NVCV_COLOR_HSV2BGR_FULL = 70 - case NVCV_COLOR_HSV2RGB_FULL : // NVCV_COLOR_HSV2RGB_FULL = 71 + case NVCV_COLOR_HSV2BGR_FULL: + case NVCV_COLOR_HSV2RGB_FULL: convertHSVtoRGB(refVec, srcVec, numPixels, dstRGBA, dstBGR); - break; + return true; - // Convert from full-range HLS (hue, lightness, saturation) to RGB/BGR --> Conversion codes 72-73 not implemented. - // Convert from LRGB/LBGR (luminance, red, green, blue) to CIE Lab --> Conversion codes 74-75 not implemented. - // Convert from LRGB/LBGR (luminance, red, green, blue) to CIE Luv --> Conversion codes 76-77 not implemented. - // Convert to LRGB/LBGR (luminance, red, green, blue) from CIE Lab --> Conversion codes 78-79 not implemented. - // Convert to LRGB/LBGR (luminance, red, green, blue) from CIE Luv --> Conversion codes 80-81 not implemented. - - // Convert from RGB/BGR to YUV. - case NVCV_COLOR_BGR2YUV : // NVCV_COLOR_BGR2YUV = 82 - case NVCV_COLOR_RGB2YUV : // NVCV_COLOR_RGB2YUV = 83 + case NVCV_COLOR_BGR2YUV: + case NVCV_COLOR_RGB2YUV: convertRGBtoYUV_PAL(refVec, srcVec, numPixels, srcRGBA, srcBGR); - break; + return true; - // Convert from YUV to RGB/BGR. - case NVCV_COLOR_YUV2BGR : // NVCV_COLOR_YUV2BGR = 84 - case NVCV_COLOR_YUV2RGB : // NVCV_COLOR_YUV2RGB = 85 + case NVCV_COLOR_YUV2BGR: + case NVCV_COLOR_YUV2RGB: convertYUVtoRGB_PAL(refVec, srcVec, numPixels, dstRGBA, dstBGR); - break; + return true; - // Bayer demosaicing to grayscale --> Conversion codes 86-89 not implemented. + default: + return false; + } +} - // Convert from YUV 4:2:0 family to RGB/BGR. - case NVCV_COLOR_YUV2RGB_NV12 : // NVCV_COLOR_YUV2RGB_NV12 = 90 - case NVCV_COLOR_YUV2BGR_NV12 : // NVCV_COLOR_YUV2BGR_NV12 = 91 - case NVCV_COLOR_YUV2RGBA_NV12: // NVCV_COLOR_YUV2RGBA_NV12 = 94 - case NVCV_COLOR_YUV2BGRA_NV12: // NVCV_COLOR_YUV2BGRA_NV12 = 95 +template +static bool BuildYuvToColorReference(vector &refVec, const vector &srcVec, NVCVColorConversionCode code, int wdth, + int hght, int imgs, size_t numPixels, bool dstRGBA, bool dstBGR) +{ + switch (code) // NOSONAR: reference implementation covers each supported YUV conversion code. + { + case NVCV_COLOR_YUV2RGB_NV12: + case NVCV_COLOR_YUV2BGR_NV12: + case NVCV_COLOR_YUV2RGBA_NV12: + case NVCV_COLOR_YUV2BGRA_NV12: convertNV12toRGB(refVec, srcVec, wdth, hght, imgs, dstRGBA, dstBGR, false); - break; + return true; - case NVCV_COLOR_YUV2RGB_NV21 : // NVCV_COLOR_YUV2RGB_NV21 = 92 (NVCV_COLOR_YUV420sp2RGB) - case NVCV_COLOR_YUV2BGR_NV21 : // NVCV_COLOR_YUV2BGR_NV21 = 93 (NVCV_COLOR_YUV420sp2BGR) - case NVCV_COLOR_YUV2RGBA_NV21: // NVCV_COLOR_YUV2RGBA_NV21 = 96 (NVCV_COLOR_YUV420sp2RGBA) - case NVCV_COLOR_YUV2BGRA_NV21: // NVCV_COLOR_YUV2BGRA_NV21 = 97 (NVCV_COLOR_YUV420sp2BGRA) + case NVCV_COLOR_YUV2RGB_NV21: + case NVCV_COLOR_YUV2BGR_NV21: + case NVCV_COLOR_YUV2RGBA_NV21: + case NVCV_COLOR_YUV2BGRA_NV21: convertNV12toRGB(refVec, srcVec, wdth, hght, imgs, dstRGBA, dstBGR, true); - break; + return true; - case NVCV_COLOR_YUV2RGB_YV12 : // NVCV_COLOR_YUV2RGB_YV12 = 98 (NVCV_COLOR_YUV420p2RGB) - case NVCV_COLOR_YUV2BGR_YV12 : // NVCV_COLOR_YUV2BGR_YV12 = 99 (NVCV_COLOR_YUV420p2BGR) - case NVCV_COLOR_YUV2RGBA_YV12: // NVCV_COLOR_YUV2RGBA_YV12 = 102 (NVCV_COLOR_YUV420p2RGBA) - case NVCV_COLOR_YUV2BGRA_YV12: // NVCV_COLOR_YUV2BGRA_YV12 = 103 (NVCV_COLOR_YUV420p2BGRA) + case NVCV_COLOR_YUV2RGB_YV12: + case NVCV_COLOR_YUV2BGR_YV12: + case NVCV_COLOR_YUV2RGBA_YV12: + case NVCV_COLOR_YUV2BGRA_YV12: convertYUVtoRGB_420(refVec, srcVec, wdth, hght, imgs, dstRGBA, dstBGR, true); - break; + return true; - case NVCV_COLOR_YUV2RGB_IYUV : // NVCV_COLOR_YUV2RGB_IYUV = 100 (NVCV_COLOR_YUV2RGB_I420) - case NVCV_COLOR_YUV2BGR_IYUV : // NVCV_COLOR_YUV2BGR_IYUV = 101 (NVCV_COLOR_YUV2BGR_I420) - case NVCV_COLOR_YUV2RGBA_IYUV: // NVCV_COLOR_YUV2RGBA_IYUV = 104 (NVCV_COLOR_YUV2RGBA_I420) - case NVCV_COLOR_YUV2BGRA_IYUV: // NVCV_COLOR_YUV2BGRA_IYUV = 105 (NVCV_COLOR_YUV2BGRA_I420) + case NVCV_COLOR_YUV2RGB_IYUV: + case NVCV_COLOR_YUV2BGR_IYUV: + case NVCV_COLOR_YUV2RGBA_IYUV: + case NVCV_COLOR_YUV2BGRA_IYUV: convertYUVtoRGB_420(refVec, srcVec, wdth, hght, imgs, dstRGBA, dstBGR, false); - break; + return true; - // Convert from YUV 4:2:0 family to grayscale. - case NVCV_COLOR_YUV2GRAY_420 : // NVCV_COLOR_YUV2GRAY_420 = 106 (NVCV_COLOR_YUV2GRAY_NV21, NVCV_COLOR_YUV2GRAY_NV12, - // NVCV_COLOR_YUV2GRAY_YV12, NVCV_COLOR_YUV2GRAY_IYUV, - // NVCV_COLOR_YUV2GRAY_I420, NVCV_COLOR_YUV420sp2GRAY, - // NVCV_COLOR_YUV420p2GRAY) + case NVCV_COLOR_YUV2GRAY_420: convertYUVtoGray_420(refVec, srcVec, wdth, hght, imgs); - break; + return true; - // Convert from YUV 4:2:2 family to RGB/BGR. - case NVCV_COLOR_YUV2RGB_UYVY : // NVCV_COLOR_YUV2RGB_UYVY = 107 ( NVCV_COLOR_YUV2RGB_Y422, NVCV_COLOR_YUV2RGB_UYNV) - case NVCV_COLOR_YUV2BGR_UYVY : // NVCV_COLOR_YUV2BGR_UYVY = 108 ( NVCV_COLOR_YUV2RGB_Y422, NVCV_COLOR_YUV2RGB_UYNV) - // Conversion codes 109 (NVCV_COLOR_YUV2RGB_VYUY) and 110 (NVCV_COLOR_YUV2BGR_VYUY) not available. - case NVCV_COLOR_YUV2RGBA_UYVY: // NVCV_COLOR_YUV2RGBA_UYVY = 111 ( NVCV_COLOR_YUV2RGBA_Y422, NVCV_COLOR_YUV2RGBA_UYNV) - case NVCV_COLOR_YUV2BGRA_UYVY: // NVCV_COLOR_YUV2BGRA_UYVY = 112 ( NVCV_COLOR_YUV2BGRA_Y422, NVCV_COLOR_YUV2BGRA_UYNV) + case NVCV_COLOR_YUV2RGB_UYVY: + case NVCV_COLOR_YUV2BGR_UYVY: + case NVCV_COLOR_YUV2RGBA_UYVY: + case NVCV_COLOR_YUV2BGRA_UYVY: convertYUVtoRGB_422(refVec, srcVec, wdth, hght, imgs, dstRGBA, dstBGR, false); - break; + return true; - // Conversion codes 113 (NVCV_COLOR_YUV2RGBA_VYUY) and 114 (NVCV_COLOR_YUV2BGRA_VYUY) not available. - case NVCV_COLOR_YUV2RGB_YUY2 : // NVCV_COLOR_YUV2RGB_YUY2 = 115 (NVCV_COLOR_YUV2RGB_YUYV, NVCV_COLOR_YUV2RGB_YUNV) - case NVCV_COLOR_YUV2BGR_YUY2 : // NVCV_COLOR_YUV2BGR_YUY2 = 116 (NVCV_COLOR_YUV2BGR_YUYV, NVCV_COLOR_YUV2BGR_YUNV) - case NVCV_COLOR_YUV2RGBA_YUY2: // NVCV_COLOR_YUV2RGBA_YUY2 = 119 (NVCV_COLOR_YUV2RGBA_YUYV, NVCV_COLOR_YUV2RGBA_YUNV) - case NVCV_COLOR_YUV2BGRA_YUY2: // NVCV_COLOR_YUV2BGRA_YUY2 = 120 (NVCV_COLOR_YUV2BGRA_YUYV, NVCV_COLOR_YUV2BGRA_YUNV) + case NVCV_COLOR_YUV2RGB_YUY2: + case NVCV_COLOR_YUV2BGR_YUY2: + case NVCV_COLOR_YUV2RGBA_YUY2: + case NVCV_COLOR_YUV2BGRA_YUY2: convertYUVtoRGB_422(refVec, srcVec, wdth, hght, imgs, dstRGBA, dstBGR, false); - break; + return true; - case NVCV_COLOR_YUV2RGB_YVYU : // NVCV_COLOR_YUV2RGB_YVYU = 117 - case NVCV_COLOR_YUV2BGR_YVYU : // NVCV_COLOR_YUV2BGR_YVYU = 118 - case NVCV_COLOR_YUV2RGBA_YVYU: // NVCV_COLOR_YUV2RGBA_YVYU = 121 - case NVCV_COLOR_YUV2BGRA_YVYU: // NVCV_COLOR_YUV2BGRA_YVYU = 122 + case NVCV_COLOR_YUV2RGB_YVYU: + case NVCV_COLOR_YUV2BGR_YVYU: + case NVCV_COLOR_YUV2RGBA_YVYU: + case NVCV_COLOR_YUV2BGRA_YVYU: convertYUVtoRGB_422(refVec, srcVec, wdth, hght, imgs, dstRGBA, dstBGR, true); - break; + return true; - // Convert from YUV 4:2:2 family to grayscale. - case NVCV_COLOR_YUV2GRAY_UYVY: // NVCV_COLOR_YUV2GRAY_UYVY = 123 (NVCV_COLOR_YUV2GRAY_Y422, NVCV_COLOR_YUV2GRAY_UYNV) + case NVCV_COLOR_YUV2GRAY_UYVY: convertYUVtoGray_422(refVec, srcVec, numPixels); - break; + return true; - case NVCV_COLOR_YUV2GRAY_YUY2: // NVCV_COLOR_YUV2GRAY_YUY2 = 124 (NVCV_COLOR_YUV2GRAY_YVYU, NVCV_COLOR_YUV2GRAY_YUYV, - // NVCV_COLOR_YUV2GRAY_YUNV) + case NVCV_COLOR_YUV2GRAY_YUY2: convertYUVtoGray_422(refVec, srcVec, numPixels); - break; + return true; - // RGB/BGA alpha premultiplication --> Conversion codes 125-126 not implemented. + default: + return false; + } +} - // Convert from RGB/BGR to YUV 4:2:0 family. - case NVCV_COLOR_RGB2YUV_I420 : // NVCV_COLOR_RGB2YUV_I420 = 127 (NVCV_COLOR_RGB2YUV_IYUV) - case NVCV_COLOR_BGR2YUV_I420 : // NVCV_COLOR_BGR2YUV_I420 = 128 (NVCV_COLOR_BGR2YUV_IYUV) - case NVCV_COLOR_RGBA2YUV_I420: // NVCV_COLOR_RGBA2YUV_I420 = 129 (NVCV_COLOR_RGBA2YUV_IYUV) - case NVCV_COLOR_BGRA2YUV_I420: // NVCV_COLOR_BGRA2YUV_I420 = 130 (NVCV_COLOR_BGRA2YUV_IYUV) +template +static bool BuildColorToYuvReference(vector &refVec, const vector &srcVec, NVCVColorConversionCode code, int wdth, + int hght, int imgs, bool srcRGBA, bool srcBGR) +{ + switch (code) + { + case NVCV_COLOR_RGB2YUV_I420: + case NVCV_COLOR_BGR2YUV_I420: + case NVCV_COLOR_RGBA2YUV_I420: + case NVCV_COLOR_BGRA2YUV_I420: convertRGBtoYUV_420(refVec, srcVec, wdth, hght, imgs, srcRGBA, srcBGR, false); - break; + return true; - case NVCV_COLOR_RGB2YUV_YV12 : // NVCV_COLOR_RGB2YUV_YV12 = 131 - case NVCV_COLOR_BGR2YUV_YV12 : // NVCV_COLOR_BGR2YUV_YV12 = 132 - case NVCV_COLOR_RGBA2YUV_YV12: // NVCV_COLOR_RGBA2YUV_YV12 = 133 - case NVCV_COLOR_BGRA2YUV_YV12: // NVCV_COLOR_BGRA2YUV_YV12 = 134 + case NVCV_COLOR_RGB2YUV_YV12: + case NVCV_COLOR_BGR2YUV_YV12: + case NVCV_COLOR_RGBA2YUV_YV12: + case NVCV_COLOR_BGRA2YUV_YV12: convertRGBtoYUV_420(refVec, srcVec, wdth, hght, imgs, srcRGBA, srcBGR, true); - break; - - // Edge-aware demosaicing to RGB/BGR --> Conversion codes 135-138 not implemented. - // OpenCV COLORCVT_MAX --> Conversion code 139 not implemented. + return true; - // Convert RGB/BGR to YUV 4:2:0 family (two plane YUV; not in OpenCV). - case NVCV_COLOR_RGB2YUV_NV12 : // NVCV_COLOR_RGB2YUV_NV12 = 140 - case NVCV_COLOR_BGR2YUV_NV12 : // NVCV_COLOR_BGR2YUV_NV12 = 141 - case NVCV_COLOR_RGBA2YUV_NV12: // NVCV_COLOR_RGBA2YUV_NV12 = 144 - case NVCV_COLOR_BGRA2YUV_NV12: // NVCV_COLOR_BGRA2YUV_NV12 = 145 + case NVCV_COLOR_RGB2YUV_NV12: + case NVCV_COLOR_BGR2YUV_NV12: + case NVCV_COLOR_RGBA2YUV_NV12: + case NVCV_COLOR_BGRA2YUV_NV12: convertRGBtoNV12(refVec, srcVec, wdth, hght, imgs, srcRGBA, srcBGR, false); - break; + return true; - case NVCV_COLOR_RGB2YUV_NV21 : // NVCV_COLOR_RGB2YUV_NV21 = 142 (NVCV_COLOR_RGB2YUV420sp) - case NVCV_COLOR_BGR2YUV_NV21 : // NVCV_COLOR_BGR2YUV_NV21 = 143 (NVCV_COLOR_BGR2YUV420sp) - case NVCV_COLOR_RGBA2YUV_NV21: // NVCV_COLOR_RGBA2YUV_NV21 = 146 (NVCV_COLOR_RGBA2YUV420sp) - case NVCV_COLOR_BGRA2YUV_NV21: // NVCV_COLOR_BGRA2YUV_NV21 = 147 (NVCV_COLOR_BGRA2YUV420sp) + case NVCV_COLOR_RGB2YUV_NV21: + case NVCV_COLOR_BGR2YUV_NV21: + case NVCV_COLOR_RGBA2YUV_NV21: + case NVCV_COLOR_BGRA2YUV_NV21: convertRGBtoNV12(refVec, srcVec, wdth, hght, imgs, srcRGBA, srcBGR, true); - break; + return true; default: + return false; + } +} + +template +static bool BuildCvtColorReference(vector &refVec, const vector &srcVec, NVCVColorConversionCode code, + size_t numPixels, int wdth, int hght, int imgs, bool srcRGBA, bool srcBGR, + bool dstRGBA, bool dstBGR) +{ + return BuildBasicColorReference(refVec, srcVec, code, numPixels, srcRGBA, srcBGR, dstRGBA, dstBGR) + || BuildYuvToColorReference(refVec, srcVec, code, wdth, hght, imgs, numPixels, dstRGBA, dstBGR) + || BuildColorToYuvReference(refVec, srcVec, code, wdth, hght, imgs, srcRGBA, srcBGR); +} + +//--------------------------------------------------------------------------------------------------------------------// +template +static void verifyOutput(nvcv::Tensor srcTensor, nvcv::ImageFormat srcFrmt, + nvcv::Tensor dstTensor, nvcv::ImageFormat dstFrmt, + NVCVColorConversionCode code, int wdth, int hght, int imgs, double maxDiff) +{ + auto srcData = srcTensor.exportData(); + auto dstData = dstTensor.exportData(); + ASSERT_TRUE(srcData); + ASSERT_TRUE(dstData); + + auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcData); + auto dstAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*dstData); + ASSERT_TRUE(srcAccess); + ASSERT_TRUE(dstAccess); + + int srcChannels = srcAccess->numChannels(); + int dstChannels = dstAccess->numChannels(); + + ASSERT_LE(srcChannels, 4); + ASSERT_LE(dstChannels, 4); + + int srcWdth = wdth; + int srcHght = hght; + int dstWdth = wdth; + int dstHght = hght; + + if (IsInterleavedYuv422(srcFrmt)) + srcWdth = srcWdth << 1; + if (IsSemiPlanarYuv420(srcFrmt)) + srcHght = (srcHght * 3) >> 1; + ASSERT_EQ(srcWdth, srcAccess->numCols()); + ASSERT_EQ(srcHght, srcAccess->numRows()); + + if (IsInterleavedYuv422(dstFrmt)) + dstWdth = dstWdth << 1; + if (IsSemiPlanarYuv420(dstFrmt)) + dstHght = (dstHght * 3) >> 1; + ASSERT_EQ(dstWdth, dstAccess->numCols()); + ASSERT_EQ(dstHght, dstAccess->numRows()); + + int srcRowElems = srcChannels * srcWdth; + int dstRowElems = dstChannels * dstWdth; + + size_t numPixels = (size_t)imgs * (size_t)wdth * (size_t)hght; + size_t srcElems = (size_t)imgs * (size_t)srcWdth * (size_t)srcHght * (size_t)srcChannels; + size_t dstElems = (size_t)imgs * (size_t)dstWdth * (size_t)dstHght * (size_t)dstChannels; + + size_t srcPitchCPU = srcRowElems * sizeof(T); + size_t dstPitchCPU = dstRowElems * sizeof(T); + + nvcv::Swizzle srcSwizzle = srcFrmt.swizzle(); + nvcv::Swizzle dstSwizzle = dstFrmt.swizzle(); + + vector srcVec(srcElems); + vector refVec(dstElems); + + bool srcBGR = IsBGR(srcSwizzle); + bool dstBGR = IsBGR(dstSwizzle); + bool srcRGBA = (srcChannels == 4); + bool dstRGBA = (dstChannels == 4); + + RandEng randEng(0); + + PopulateSource(srcVec, srcWdth, srcHght, imgs, srcChannels, numPixels, srcRGBA, srcBGR, code, randEng); + + // Copy source from image vector to device tensor. + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(srcData->basePtr(), srcAccess->rowStride(), srcVec.data(), srcPitchCPU, + srcPitchCPU, (size_t)imgs * (size_t)srcHght, cudaMemcpyHostToDevice)); + + bool success = BuildCvtColorReference(refVec, srcVec, code, numPixels, wdth, hght, imgs, srcRGBA, srcBGR, dstRGBA, + dstBGR); + if (!success) + { std::cerr << "**** ERROR: Color conversion not implemented for conversion code " << code << ". ****\n\n"; - success = false; } if (success) @@ -424,8 +459,12 @@ static void verifyOutput(nvcv::Tensor srcTensor, nvcv::ImageFormat srcFrmt, // Compare "gold" reference to computed output. if (dstFrmt == NVCV_IMAGE_FORMAT_HSV8 || dstFrmt == NVCV_IMAGE_FORMAT_HSVf32) { - const bool full = (code == NVCV_COLOR_BGR2HSV_FULL || code == NVCV_COLOR_RGB2HSV_FULL); - const double range = (sizeof(T) > 1) ? 360.0 : (full ? 256.0 : 180.0); + const bool full = (code == NVCV_COLOR_BGR2HSV_FULL || code == NVCV_COLOR_RGB2HSV_FULL); + double range = 360.0; + if constexpr (sizeof(T) == 1) + { + range = full ? 256.0 : 180.0; + } EXPECT_NEAR_HSV_VEC_CNT(refVec, dstVec, range, maxDiff, maxErrCnt, success); } @@ -734,9 +773,9 @@ TEST_P(OpCvtColor, correct_output) nvcv::Tensor dstTensor = util::CreateTensor(imgs, wdth, hght, dstFrmt); NVCVDataType dataType; - ASSERT_EQ(nvcvImageFormatGetPlaneDataType(srcFrmt, 0, &dataType), NVCV_SUCCESS); + ASSERT_EQ(nvcvImageFormatGetPlaneDataType(static_cast(srcFrmt), 0, &dataType), NVCV_SUCCESS); - switch (dataType) + switch (dataType) // NOSONAR: typed test dispatch covers all supported source channel layouts. { case NVCV_DATA_TYPE_U8: case NVCV_DATA_TYPE_2U8: @@ -792,6 +831,363 @@ TEST_P(OpCvtColor, correct_output) //--------------------------------------------------------------------------------------------------------------------// +NVCV_TEST_SUITE_P(OpCvtColorVarShapeReference, + test::ValueList{ + // Input Format, Output Format, Conversion Code, Max Diff + { NVCV_IMAGE_FORMAT_RGB8, NVCV_IMAGE_FORMAT_BGR8, NVCV_COLOR_RGB2BGR, 0}, + { NVCV_IMAGE_FORMAT_RGB8, NVCV_IMAGE_FORMAT_RGBA8, NVCV_COLOR_RGB2RGBA, 0}, + {NVCV_IMAGE_FORMAT_RGBA8, NVCV_IMAGE_FORMAT_RGB8, NVCV_COLOR_RGBA2RGB, 0}, + { NVCV_IMAGE_FORMAT_RGB8, NVCV_IMAGE_FORMAT_Y8, NVCV_COLOR_RGB2GRAY, 1}, + { NVCV_IMAGE_FORMAT_HSV8, NVCV_IMAGE_FORMAT_RGB8, NVCV_COLOR_HSV2RGB, 1}, +}); + +TEST_P(OpCvtColorVarShapeReference, benchmark_kernels_match_independent_reference) +{ + nvcv::ImageFormat srcFormat{GetParamValue<0>()}; + nvcv::ImageFormat dstFormat{GetParamValue<1>()}; + NVCVColorConversionCode code{GetParamValue<2>()}; + int maxDiff = GetParamValue<3>(); + + // Odd, nonuniform dimensions cover scalar tails and per-image var-shape addressing. + const std::vector sizes{ + {31, 23}, + {37, 19}, + {65, 17} + }; + const int srcChannels = srcFormat.numChannels(); + const int dstChannels = dstFormat.numChannels(); + + std::vector srcImages; + std::vector dstImages; + std::vector> srcVectors; + std::vector> refVectors; + srcImages.reserve(sizes.size()); + dstImages.reserve(sizes.size()); + srcVectors.reserve(sizes.size()); + refVectors.reserve(sizes.size()); + + RandEng randEng(0); + for (const nvcv::Size2D &size : sizes) + { + const size_t numPixels = static_cast(size.w) * size.h; + srcVectors.emplace_back(numPixels * srcChannels); + refVectors.emplace_back(numPixels * dstChannels); + + const bool srcRGBA = srcChannels == 4; + const bool dstRGBA = dstChannels == 4; + const bool srcBGR = IsBGR(srcFormat.swizzle()); + const bool dstBGR = IsBGR(dstFormat.swizzle()); + PopulateSource(srcVectors.back(), size.w, size.h, 1, srcChannels, numPixels, srcRGBA, srcBGR, code, randEng); + ASSERT_TRUE(BuildCvtColorReference(refVectors.back(), srcVectors.back(), code, numPixels, size.w, size.h, 1, + srcRGBA, srcBGR, dstRGBA, dstBGR)); + + srcImages.emplace_back(size, srcFormat); + dstImages.emplace_back(size, dstFormat); + auto srcData = srcImages.back().exportData(); + ASSERT_TRUE(srcData); + const size_t srcRowBytes = static_cast(size.w) * srcFormat.planePixelStrideBytes(0); + ASSERT_EQ(cudaSuccess, + cudaMemcpy2D(srcData->plane(0).basePtr, srcData->plane(0).rowStride, srcVectors.back().data(), + srcRowBytes, srcRowBytes, size.h, cudaMemcpyHostToDevice)); + } + + nvcv::ImageBatchVarShape srcBatch(static_cast(sizes.size())); + nvcv::ImageBatchVarShape dstBatch(static_cast(sizes.size())); + srcBatch.pushBack(srcImages.begin(), srcImages.end()); + dstBatch.pushBack(dstImages.begin(), dstImages.end()); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + cvcuda::CvtColor op; + EXPECT_NO_THROW(op(stream, srcBatch, dstBatch, code)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + for (size_t i = 0; i < sizes.size(); ++i) + { + SCOPED_TRACE(i); + auto dstData = dstImages[i].exportData(); + ASSERT_TRUE(dstData); + const size_t dstRowBytes = static_cast(sizes[i].w) * dstFormat.planePixelStrideBytes(0); + std::vector dstVector(static_cast(sizes[i].h) * dstRowBytes); + ASSERT_EQ(cudaSuccess, + cudaMemcpy2D(dstVector.data(), dstRowBytes, dstData->plane(0).basePtr, dstData->plane(0).rowStride, + dstRowBytes, sizes[i].h, cudaMemcpyDeviceToHost)); + + if (maxDiff == 0) + { + EXPECT_EQ(refVectors[i], dstVector); + } + else + { + ASSERT_EQ(refVectors[i].size(), dstVector.size()); + for (size_t j = 0; j < refVectors[i].size(); ++j) + { + // This one-level tolerance covers fixed-point luma/HSV rounding against the scalar reference. + EXPECT_NEAR(refVectors[i][j], dstVector[j], maxDiff) << "At index " << j; + } + } + } + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +//--------------------------------------------------------------------------------------------------------------------// + +template +static std::vector GenerateCvtColorPlanarParitySource(int width, int height, int numImages, int srcChannels, + NVCVColorConversionCode code, bool srcRGBA, bool srcBGR) +{ + const size_t numPixels = static_cast(numImages) * height * width; + std::vector srcVec(numPixels * srcChannels); + RandEng randEng(0); + + PopulateSource(srcVec, width, height, numImages, srcChannels, numPixels, srcRGBA, srcBGR, code, randEng); + + std::vector srcBytes(srcVec.size() * sizeof(T)); + std::memcpy(srcBytes.data(), srcVec.data(), srcBytes.size()); + return srcBytes; +} + +static void RunCvtColorTensorPlanarParity(int width, int height, int numImages, int srcChannels, int dstChannels, + NVCVColorConversionCode code, bool srcRGBA, bool srcBGR) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + constexpr int elemSize = 1; + const int srcRowStride = width * srcChannels * elemSize; + const int dstRowStride = width * dstChannels * elemSize; + + nvcv::Tensor srcI( + { + {numImages, height, width, srcChannels}, + "NHWC" + }, + nvcv::TYPE_U8); + nvcv::Tensor dstI( + { + {numImages, height, width, dstChannels}, + "NHWC" + }, + nvcv::TYPE_U8); + nvcv::Tensor srcP( + { + {numImages, srcChannels, height, width}, + "NCHW" + }, + nvcv::TYPE_U8); + nvcv::Tensor dstP( + { + {numImages, dstChannels, height, width}, + "NCHW" + }, + nvcv::TYPE_U8); + + auto srcIData = srcI.exportData(); + auto dstIData = dstI.exportData(); + auto srcPData = srcP.exportData(); + auto dstPData = dstP.exportData(); + ASSERT_TRUE(srcIData && dstIData && srcPData && dstPData); + + auto srcIAcc = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcIData); + auto dstIAcc = nvcv::TensorDataAccessStridedImagePlanar::Create(*dstIData); + auto srcPAcc = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcPData); + auto dstPAcc = nvcv::TensorDataAccessStridedImagePlanar::Create(*dstPData); + ASSERT_TRUE(srcIAcc && dstIAcc && srcPAcc && dstPAcc); + + const auto srcHwcBytes + = GenerateCvtColorPlanarParitySource(width, height, numImages, srcChannels, code, srcRGBA, srcBGR); + const size_t sampleBytes = static_cast(height) * srcRowStride; + for (int i = 0; i < numImages; ++i) + { + const auto sampleStart = srcHwcBytes.begin() + static_cast(i * sampleBytes); + std::vector hwc(sampleStart, sampleStart + static_cast(sampleBytes)); + + test::planar::UploadInterleavedSample(*srcIAcc, i, hwc, width, height, srcRowStride); + test::planar::UploadPlanarSample(*srcPAcc, i, + test::planar::DeinterleaveToPlanes(hwc, width, height, srcChannels, elemSize), + width, height, srcChannels, elemSize); + } + + cvcuda::CvtColor op; + EXPECT_NO_THROW(op(stream, srcI, dstI, code)); + EXPECT_NO_THROW(op(stream, srcP, dstP, code)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + for (int i = 0; i < numImages; ++i) + { + SCOPED_TRACE(i); + auto gpuInter = test::planar::DownloadInterleavedSample(*dstIAcc, i, width, height, dstRowStride); + auto planesOut = test::planar::DownloadPlanarSample(*dstPAcc, i, width, height, dstChannels, elemSize); + auto planarInter = test::planar::InterleaveFromPlanes(planesOut, width, height, dstChannels, elemSize); + + EXPECT_EQ(gpuInter, planarInter); + } + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +template +static void RunCvtColorVarShapePlanarParity(int width, int height, int numImages, nvcv::ImageFormat planarSrcFmt, + nvcv::ImageFormat interleavedSrcFmt, nvcv::ImageFormat planarDstFmt, + nvcv::ImageFormat interleavedDstFmt, NVCVColorConversionCode code, + bool srcRGBA, bool srcBGR) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + const int srcChannels = planarSrcFmt.numChannels(); + const int dstChannels = planarDstFmt.numChannels(); + const int elemSize = sizeof(T); + const int srcRowStride = width * srcChannels * elemSize; + const int dstRowStride = width * dstChannels * elemSize; + + std::vector srcI; + std::vector dstI; + std::vector srcP; + std::vector dstP; + for (int i = 0; i < numImages; ++i) + { + srcI.emplace_back(nvcv::Size2D{width, height}, interleavedSrcFmt); + dstI.emplace_back(nvcv::Size2D{width, height}, interleavedDstFmt); + srcP.emplace_back(nvcv::Size2D{width, height}, planarSrcFmt); + dstP.emplace_back(nvcv::Size2D{width, height}, planarDstFmt); + } + + nvcv::ImageBatchVarShape batchSrcI(numImages); + nvcv::ImageBatchVarShape batchDstI(numImages); + nvcv::ImageBatchVarShape batchSrcP(numImages); + nvcv::ImageBatchVarShape batchDstP(numImages); + batchSrcI.pushBack(srcI.begin(), srcI.end()); + batchDstI.pushBack(dstI.begin(), dstI.end()); + batchSrcP.pushBack(srcP.begin(), srcP.end()); + batchDstP.pushBack(dstP.begin(), dstP.end()); + + const auto srcHwcBytes + = GenerateCvtColorPlanarParitySource(width, height, numImages, srcChannels, code, srcRGBA, srcBGR); + const size_t sampleBytes = static_cast(height) * srcRowStride; + for (int i = 0; i < numImages; ++i) + { + const auto sampleStart = srcHwcBytes.begin() + static_cast(i * sampleBytes); + std::vector hwc(sampleStart, sampleStart + static_cast(sampleBytes)); + + auto idata = srcI[i].exportData(); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(idata->plane(0).basePtr, idata->plane(0).rowStride, hwc.data(), + srcRowStride, srcRowStride, height, cudaMemcpyHostToDevice)); + + auto planes = test::planar::DeinterleaveToPlanes(hwc, width, height, srcChannels, elemSize); + auto pdata = srcP[i].exportData(); + const int planeBytes = width * height * elemSize; + ASSERT_EQ(pdata->numPlanes(), srcChannels); + for (int c = 0; c < srcChannels; ++c) + { + ASSERT_EQ(cudaSuccess, + cudaMemcpy2D(pdata->plane(c).basePtr, pdata->plane(c).rowStride, planes.data() + c * planeBytes, + width * elemSize, width * elemSize, height, cudaMemcpyHostToDevice)); + } + } + + cvcuda::CvtColor op; + EXPECT_NO_THROW(op(stream, batchSrcI, batchDstI, code)); + EXPECT_NO_THROW(op(stream, batchSrcP, batchDstP, code)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + const int dstPlaneBytes = width * height * elemSize; + for (int i = 0; i < numImages; ++i) + { + SCOPED_TRACE(i); + std::vector gpuInter(height * dstRowStride); + auto idata = dstI[i].exportData(); + EXPECT_EQ(cudaSuccess, cudaMemcpy2D(gpuInter.data(), dstRowStride, idata->plane(0).basePtr, + idata->plane(0).rowStride, dstRowStride, height, cudaMemcpyDeviceToHost)); + + std::vector planesOut(width * height * dstChannels * elemSize); + auto pdata = dstP[i].exportData(); + ASSERT_EQ(pdata->numPlanes(), dstChannels); + for (int c = 0; c < dstChannels; ++c) + { + EXPECT_EQ(cudaSuccess, + cudaMemcpy2D(planesOut.data() + c * dstPlaneBytes, width * elemSize, pdata->plane(c).basePtr, + pdata->plane(c).rowStride, width * elemSize, height, cudaMemcpyDeviceToHost)); + } + auto planarInter = test::planar::InterleaveFromPlanes(planesOut, width, height, dstChannels, elemSize); + + EXPECT_EQ(gpuInter, planarInter); + } + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +// clang-format off + +NVCV_TEST_SUITE_P(OpCvtColorPlanarTensor, +test::ValueList +{ + // W, H, N, SrcC, DstC, Conversion Code, SrcRGBA, SrcBGR + { 31, 23, 2, 3, 3, NVCV_COLOR_RGB2BGR, false, false}, + { 29, 17, 1, 3, 4, NVCV_COLOR_RGB2RGBA, false, false}, + { 33, 19, 2, 4, 3, NVCV_COLOR_RGBA2RGB, true, false}, + { 27, 25, 2, 3, 1, NVCV_COLOR_RGB2GRAY, false, false}, + { 35, 21, 1, 1, 3, NVCV_COLOR_GRAY2RGB, false, false}, + { 23, 31, 2, 3, 3, NVCV_COLOR_RGB2HSV, false, false}, + { 25, 29, 1, 3, 3, NVCV_COLOR_HSV2RGB, false, false}, + { 37, 27, 2, 3, 3, NVCV_COLOR_RGB2YUV, false, false}, + { 39, 23, 1, 3, 3, NVCV_COLOR_YUV2RGB, false, false}, +}); + +NVCV_TEST_SUITE_P(OpCvtColorPlanarVarShape, +test::ValueList +{ + // W, H, N, Planar Src, Interleaved Src, Planar Dst, Interleaved Dst, Conversion Code, SrcRGBA, SrcBGR + { 31, 23, 2, nvcv::FMT_RGB8p, nvcv::FMT_RGB8, nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8, NVCV_COLOR_RGB2RGBA, false, false}, + { 29, 17, 1, nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8, nvcv::FMT_RGB8p, nvcv::FMT_RGB8, NVCV_COLOR_RGBA2RGB, true, false}, + { 33, 21, 2, nvcv::FMT_RGB8p, nvcv::FMT_RGB8, nvcv::FMT_BGR8p, nvcv::FMT_BGR8, NVCV_COLOR_RGB2BGR, false, false}, + { 35, 19, 2, nvcv::FMT_RGB8p, nvcv::FMT_RGB8, nvcv::FMT_RGB8p, nvcv::FMT_RGB8, NVCV_COLOR_RGB2HSV, false, false}, + { 37, 23, 1, nvcv::FMT_RGB8p, nvcv::FMT_RGB8, nvcv::FMT_RGB8p, nvcv::FMT_RGB8, NVCV_COLOR_HSV2RGB, false, false}, + { 39, 25, 2, nvcv::FMT_BGR8p, nvcv::FMT_BGR8, nvcv::FMT_BGR8p, nvcv::FMT_BGR8, NVCV_COLOR_BGR2HSV_FULL, false, true}, + { 41, 27, 1, nvcv::FMT_BGR8p, nvcv::FMT_BGR8, nvcv::FMT_BGR8p, nvcv::FMT_BGR8, NVCV_COLOR_HSV2BGR_FULL, false, true}, + { 43, 29, 2, nvcv::FMT_RGB8p, nvcv::FMT_RGB8, nvcv::FMT_RGB8p, nvcv::FMT_RGB8, NVCV_COLOR_RGB2YUV, false, false}, + { 45, 31, 1, nvcv::FMT_RGB8p, nvcv::FMT_RGB8, nvcv::FMT_RGB8p, nvcv::FMT_RGB8, NVCV_COLOR_YUV2RGB, false, false}, + { 47, 33, 2, nvcv::FMT_BGR8p, nvcv::FMT_BGR8, nvcv::FMT_BGR8p, nvcv::FMT_BGR8, NVCV_COLOR_BGR2YUV, false, true}, + { 49, 35, 1, nvcv::FMT_BGR8p, nvcv::FMT_BGR8, nvcv::FMT_BGR8p, nvcv::FMT_BGR8, NVCV_COLOR_YUV2BGR, false, true}, +}); + +NVCV_TEST_SUITE_P(OpCvtColorPlanarVarShapeFloat, +test::ValueList +{ + // W, H, N, Planar Src, Interleaved Src, Planar Dst, Interleaved Dst, Conversion Code, SrcRGBA, SrcBGR + { 23, 17, 2, nvcv::FMT_RGBf32p, nvcv::FMT_RGBf32, nvcv::FMT_RGBf32p, nvcv::FMT_RGBf32, NVCV_COLOR_RGB2HSV, false, false}, + { 25, 19, 1, nvcv::FMT_RGBf32p, nvcv::FMT_RGBf32, nvcv::FMT_RGBf32p, nvcv::FMT_RGBf32, NVCV_COLOR_HSV2RGB, false, false}, + { 27, 21, 2, nvcv::FMT_BGRf32p, nvcv::FMT_BGRf32, nvcv::FMT_BGRf32p, nvcv::FMT_BGRf32, NVCV_COLOR_BGR2HSV_FULL, false, true }, + { 29, 23, 1, nvcv::FMT_BGRf32p, nvcv::FMT_BGRf32, nvcv::FMT_BGRf32p, nvcv::FMT_BGRf32, NVCV_COLOR_HSV2BGR_FULL, false, true }, + { 31, 25, 2, nvcv::FMT_RGBf32p, nvcv::FMT_RGBf32, nvcv::FMT_RGBf32p, nvcv::FMT_RGBf32, NVCV_COLOR_RGB2YUV, false, false}, + { 33, 27, 1, nvcv::FMT_RGBf32p, nvcv::FMT_RGBf32, nvcv::FMT_RGBf32p, nvcv::FMT_RGBf32, NVCV_COLOR_YUV2RGB, false, false}, +}); + +// clang-format on + +TEST_P(OpCvtColorPlanarTensor, tensor_matches_interleaved) +{ + RunCvtColorTensorPlanarParity(GetParamValue<0>(), GetParamValue<1>(), GetParamValue<2>(), GetParamValue<3>(), + GetParamValue<4>(), GetParamValue<5>(), GetParamValue<6>(), GetParamValue<7>()); +} + +TEST_P(OpCvtColorPlanarVarShape, varshape_matches_interleaved) +{ + RunCvtColorVarShapePlanarParity( + GetParamValue<0>(), GetParamValue<1>(), GetParamValue<2>(), GetParamValue<3>(), GetParamValue<4>(), + GetParamValue<5>(), GetParamValue<6>(), GetParamValue<7>(), GetParamValue<8>(), GetParamValue<9>()); +} + +TEST_P(OpCvtColorPlanarVarShapeFloat, varshape_matches_interleaved) +{ + RunCvtColorVarShapePlanarParity( + GetParamValue<0>(), GetParamValue<1>(), GetParamValue<2>(), GetParamValue<3>(), GetParamValue<4>(), + GetParamValue<5>(), GetParamValue<6>(), GetParamValue<7>(), GetParamValue<8>(), GetParamValue<9>()); +} + +//--------------------------------------------------------------------------------------------------------------------// + #define VEC_EXPECT_NEAR(vec1, vec2, delta, dtype) \ ASSERT_EQ(vec1.size(), vec2.size()); \ for (std::size_t idx = 0; idx < vec1.size() / sizeof(dtype); ++idx) \ @@ -917,7 +1313,7 @@ TEST_P(OpCvtColor_circular, varshape_correct_output) // clang-format on NVCVDataType nvcvDataType; - ASSERT_EQ(NVCV_SUCCESS, nvcvImageFormatGetPlaneDataType(srcFormat, 0, &nvcvDataType)); + ASSERT_EQ(NVCV_SUCCESS, nvcvImageFormatGetPlaneDataType(static_cast(srcFormat), 0, &nvcvDataType)); NVCVColorConversionCode src2dstCode{GetParamValue<5>()}; NVCVColorConversionCode dst2srcCode{GetParamValue<6>()}; @@ -925,9 +1321,9 @@ TEST_P(OpCvtColor_circular, varshape_correct_output) double maxDiff{GetParamValue<7>()}; // Create input varshape - std::default_random_engine rng; - std::uniform_int_distribution udistWidth(width * 0.8, width * 1.1); - std::uniform_int_distribution udistHeight(height * 0.8, height * 1.1); + std::default_random_engine rng; + std::uniform_int_distribution udistWidth(ScaledSize(width, 0.8), ScaledSize(width, 1.1)); + std::uniform_int_distribution udistHeight(ScaledSize(height, 0.8), ScaledSize(height, 1.1)); std::vector imgSrc; @@ -953,7 +1349,7 @@ TEST_P(OpCvtColor_circular, varshape_correct_output) generateRandVec(reinterpret_cast(srcVec[i].data()), srcVec[i].size() / sizeof(float), rng); break; default: - generateRandVec(reinterpret_cast(srcVec[i].data()), srcVec[i].size(), rng); + generateRandVec(srcVec[i].data(), srcVec[i].size(), rng); break; } @@ -1026,6 +1422,45 @@ TEST(OpCvtColor_negative, create_with_null_handle) EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, cvcudaCvtColorCreate(nullptr)); } +TEST(OpCvtColor_Negative, planar_tensor_layout_mismatch) +{ + nvcv::Tensor srcTensor( + { + {1, 3, 8, 8}, + "NCHW" + }, + nvcv::TYPE_U8); + nvcv::Tensor dstTensor( + { + {1, 8, 8, 3}, + "NHWC" + }, + nvcv::TYPE_U8); + + cvcuda::CvtColor cvtColorOp; + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&] { cvtColorOp(nullptr, srcTensor, dstTensor, NVCV_COLOR_RGB2BGR); })); +} + +TEST(OpCvtColor_Negative, planar_varshape_layout_mismatch) +{ + std::vector imgSrc; + imgSrc.emplace_back(nvcv::Size2D{8, 8}, nvcv::FMT_RGB8p); + + nvcv::ImageBatchVarShape batchSrc(1); + batchSrc.pushBack(imgSrc.begin(), imgSrc.end()); + + std::vector imgDst; + imgDst.emplace_back(nvcv::Size2D{8, 8}, nvcv::FMT_RGBA8); + + nvcv::ImageBatchVarShape batchDst(1); + batchDst.pushBack(imgDst.begin(), imgDst.end()); + + cvcuda::CvtColor cvtColorOp; + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&] { cvtColorOp(nullptr, batchSrc, batchDst, NVCV_COLOR_RGB2RGBA); })); +} + TEST(OpCvtColor_negative, mismatch_shape) { nvcv::Tensor tensorY8 = util::CreateTensor(2, 224, 224, nvcv::ImageFormat{NVCV_IMAGE_FORMAT_Y8}); @@ -1036,17 +1471,21 @@ TEST(OpCvtColor_negative, mismatch_shape) // run operator cvcuda::CvtColor cvtColorOp; EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, - nvcv::ProtectCall([&] { cvtColorOp(nullptr, tensorY8, tensorBGR8, NVCV_COLOR_GRAY2BGR); })); + nvcv::ProtectCall([&cvtColorOp, &tensorY8, &tensorBGR8] + { cvtColorOp(nullptr, tensorY8, tensorBGR8, NVCV_COLOR_GRAY2BGR); })); // reserved conversion invalid too EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, - nvcv::ProtectCall([&] { cvtColorOp(nullptr, tensorBGR8, tensorY8, NVCV_COLOR_BGR2GRAY); })); + nvcv::ProtectCall([&cvtColorOp, &tensorBGR8, &tensorY8] + { cvtColorOp(nullptr, tensorBGR8, tensorY8, NVCV_COLOR_BGR2GRAY); })); EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, - nvcv::ProtectCall([&] { cvtColorOp(nullptr, tensorHSV8, tensorBGR8, NVCV_COLOR_HSV2BGR); })); + nvcv::ProtectCall([&cvtColorOp, &tensorHSV8, &tensorBGR8] + { cvtColorOp(nullptr, tensorHSV8, tensorBGR8, NVCV_COLOR_HSV2BGR); })); EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, - nvcv::ProtectCall([&] { cvtColorOp(nullptr, tensorBGR8, tensorRGB8, NVCV_COLOR_BGR2RGB); })); + nvcv::ProtectCall([&cvtColorOp, &tensorBGR8, &tensorRGB8] + { cvtColorOp(nullptr, tensorBGR8, tensorRGB8, NVCV_COLOR_BGR2RGB); })); } TEST(OpCvtColor_negative, invalid_shape_BGR_to_YUV420xp) @@ -1056,7 +1495,7 @@ TEST(OpCvtColor_negative, invalid_shape_BGR_to_YUV420xp) util::CreateTensor(1, 8, 8, nvcv::ImageFormat{NVCV_IMAGE_FORMAT_BGRf16}), util::CreateTensor(1, 16, 16, nvcv::ImageFormat{NVCV_IMAGE_FORMAT_BGR8})}; - nvcv::Tensor dstTensor = nvcv::Tensor( + auto dstTensor = nvcv::Tensor( { {8, 8, 1}, "HWC" @@ -1065,10 +1504,11 @@ TEST(OpCvtColor_negative, invalid_shape_BGR_to_YUV420xp) // run operator cvcuda::CvtColor cvtColorOp; - for (auto &srcTensor : srcTensors) + for (const auto &srcTensor : srcTensors) { EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, - nvcv::ProtectCall([&] { cvtColorOp(nullptr, srcTensor, dstTensor, NVCV_COLOR_BGR2YUV_YV12); })); + nvcv::ProtectCall([&cvtColorOp, &srcTensor, &dstTensor] + { cvtColorOp(nullptr, srcTensor, dstTensor, NVCV_COLOR_BGR2YUV_YV12); })); } } @@ -1099,16 +1539,18 @@ TEST(OpCvtColor_negative, invalid_shape_YUV420xp_toBGR) // run operator cvcuda::CvtColor cvtColorOp; - for (auto &srcTensor : srcTensors) + for (const auto &srcTensor : srcTensors) { EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, - nvcv::ProtectCall([&] { cvtColorOp(nullptr, srcTensor, dstTensor, NVCV_COLOR_YUV2BGR_YV12); })); + nvcv::ProtectCall([&cvtColorOp, &srcTensor, &dstTensor] + { cvtColorOp(nullptr, srcTensor, dstTensor, NVCV_COLOR_YUV2BGR_YV12); })); } - EXPECT_EQ( - NVCV_ERROR_INVALID_ARGUMENT, - nvcv::ProtectCall( - [&] { cvtColorOp(nullptr, srcTensor_1, dstTensor_1, NVCV_COLOR_YUV2BGR_YV12); })); // incalid output channel + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall( + [&cvtColorOp, &srcTensor_1, &dstTensor_1] { + cvtColorOp(nullptr, srcTensor_1, dstTensor_1, + NVCV_COLOR_YUV2BGR_YV12); + })); // incalid output channel } TEST(OpCvtColor_negative, invalid_shape_YUV422_to_BGR) @@ -1125,7 +1567,7 @@ TEST(OpCvtColor_negative, invalid_shape_YUV422_to_BGR) .planeDataType(0).channelType(0)) }; - nvcv::Tensor dstTensor = nvcv::Tensor( + auto dstTensor = nvcv::Tensor( { {120, 40, 3}, "HWC" @@ -1134,16 +1576,17 @@ TEST(OpCvtColor_negative, invalid_shape_YUV422_to_BGR) // run operator cvcuda::CvtColor cvtColorOp; - for (auto &srcTensor : srcTensors) + for (const auto &srcTensor : srcTensors) { EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, - nvcv::ProtectCall([&] { cvtColorOp(nullptr, srcTensor, dstTensor, NVCV_COLOR_YUV2BGR_UYVY); })); + nvcv::ProtectCall([&cvtColorOp, &srcTensor, &dstTensor] + { cvtColorOp(nullptr, srcTensor, dstTensor, NVCV_COLOR_YUV2BGR_UYVY); })); } } TEST(OpCvtColor_negative, invalid_shape_YUV422_to_BGR_invalid_out) { - nvcv::Tensor srcTensor = nvcv::Tensor( + auto srcTensor = nvcv::Tensor( { {120, 20, 1}, "HWC" @@ -1161,13 +1604,45 @@ TEST(OpCvtColor_negative, invalid_shape_YUV422_to_BGR_invalid_out) // run operator cvcuda::CvtColor cvtColorOp; - for (auto &dstTensor : dstTensors) + for (const auto &dstTensor : dstTensors) { EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, - nvcv::ProtectCall([&] { cvtColorOp(nullptr, srcTensor, dstTensor, NVCV_COLOR_YUV2BGR_UYVY); })); + nvcv::ProtectCall([&cvtColorOp, &srcTensor, &dstTensor] + { cvtColorOp(nullptr, srcTensor, dstTensor, NVCV_COLOR_YUV2BGR_UYVY); })); } } +TEST(OpCvtColor_negative, invalid_conversion_code) +{ + nvcv::Tensor srcTensor = util::CreateTensor(1, 8, 8, nvcv::ImageFormat{NVCV_IMAGE_FORMAT_BGR8}); + nvcv::Tensor dstTensor = util::CreateTensor(1, 8, 8, nvcv::ImageFormat{NVCV_IMAGE_FORMAT_RGB8}); + + cvcuda::CvtColor cvtColorOp; + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall( + [&] { cvtColorOp(nullptr, srcTensor, dstTensor, static_cast(1000000)); })); +} + +TEST(OpCvtColor_negative, varshape_invalid_conversion_code) +{ + std::vector imgSrc; + imgSrc.emplace_back(nvcv::Size2D{8, 8}, nvcv::ImageFormat{NVCV_IMAGE_FORMAT_BGR8}); + + nvcv::ImageBatchVarShape batchSrc(1); + batchSrc.pushBack(imgSrc.begin(), imgSrc.end()); + + std::vector imgDst; + imgDst.emplace_back(nvcv::Size2D{8, 8}, nvcv::ImageFormat{NVCV_IMAGE_FORMAT_RGB8}); + + nvcv::ImageBatchVarShape batchDst(1); + batchDst.pushBack(imgDst.begin(), imgDst.end()); + + cvcuda::CvtColor cvtColorOp; + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall( + [&] { cvtColorOp(nullptr, batchSrc, batchDst, static_cast(1000000)); })); +} + // clang-format off NVCV_TEST_SUITE_P(OpCvtColor_negative, @@ -1177,7 +1652,6 @@ test::ValueList()}; // Create input varshape - std::default_random_engine rng; - std::uniform_int_distribution udistWidth(width * 0.8, width * 1.1); - std::uniform_int_distribution udistHeight(height * 0.8, height * 1.1); + std::default_random_engine rng; + std::uniform_int_distribution udistWidth(ScaledSize(width, 0.8), ScaledSize(width, 1.1)); + std::uniform_int_distribution udistHeight(ScaledSize(height, 0.8), ScaledSize(height, 1.1)); std::vector imgSrc; @@ -1309,10 +1783,6 @@ NVCV_TEST_SUITE_P(OpCvtColor_negative_diff_format, test::ValueList udistWidth(width * 0.8, width * 1.1); - std::uniform_int_distribution udistHeight(height * 0.8, height * 1.1); + std::default_random_engine rng; + std::uniform_int_distribution udistWidth(ScaledSize(width, 0.8), ScaledSize(width, 1.1)); + std::uniform_int_distribution udistHeight(ScaledSize(height, 0.8), ScaledSize(height, 1.1)); std::vector imgSrc; for (int i = 0; i < batches - 1; ++i) @@ -1357,7 +1827,8 @@ TEST_P(OpCvtColor_negative_diff_format, varshape_hasDifferentFormat) cvcuda::CvtColor cvtColorOp; EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, - nvcv::ProtectCall([&] { cvtColorOp(nullptr, batchSrc, batchDst, src2dstCode); })); + nvcv::ProtectCall([&cvtColorOp, &batchSrc, &batchDst, &src2dstCode] + { cvtColorOp(nullptr, batchSrc, batchDst, src2dstCode); })); } #undef VEC_EXPECT_NEAR diff --git a/tests/cvcuda/system/TestOpErase.cpp b/tests/cvcuda/system/TestOpErase.cpp index 40387f824..83fe008d9 100644 --- a/tests/cvcuda/system/TestOpErase.cpp +++ b/tests/cvcuda/system/TestOpErase.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,17 +16,306 @@ */ #include "Definitions.hpp" +#include "PlanarParityUtils.hpp" #include #include +#include #include #include #include #include #include +#include +#include #include #include +#include +#include +#include + +static int ScaledSize(int value, double scale) +{ + return static_cast(value * scale); +} + +static void ClearU8Image(const nvcv::Image &image) +{ + const auto data = image.exportData(); + assert(data->numPlanes() == 1); + + int width = data->plane(0).width; + int height = data->plane(0).height; + int rowStride = width * nvcv::FMT_U8.planePixelStrideBytes(0); + + EXPECT_EQ(cudaSuccess, cudaMemset2D(data->plane(0).basePtr, rowStride, 0, rowStride, height)); +} + +static void UploadU8Image(const nvcv::Image &image, const std::vector &host) +{ + const auto data = image.exportData(); + const int width = image.size().w; + const int height = image.size().h; + const int channels = image.format().numChannels(); + + ASSERT_NE(data, nullptr); + ASSERT_EQ(host.size(), static_cast(width * height * channels)); + + if (data->numPlanes() == 1) + { + const int rowBytes = width * channels; + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(data->plane(0).basePtr, data->plane(0).rowStride, host.data(), rowBytes, + rowBytes, height, cudaMemcpyHostToDevice)); + return; + } + + ASSERT_EQ(data->numPlanes(), channels); + const auto planes = nvcv::test::planar::DeinterleaveToPlanes(host, width, height, channels, 1); + const int planeBytes = width * height; + for (int c = 0; c < channels; ++c) + { + ASSERT_EQ(cudaSuccess, + cudaMemcpy2D(data->plane(c).basePtr, data->plane(c).rowStride, planes.data() + c * planeBytes, width, + width, height, cudaMemcpyHostToDevice)); + } +} + +static void DownloadU8Image(const nvcv::Image &image, std::vector &host) +{ + const auto data = image.exportData(); + const int width = image.size().w; + const int height = image.size().h; + const int channels = image.format().numChannels(); + + ASSERT_NE(data, nullptr); + + if (data->numPlanes() == 1) + { + const int rowBytes = width * channels; + host.resize(height * rowBytes); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(host.data(), rowBytes, data->plane(0).basePtr, data->plane(0).rowStride, + rowBytes, height, cudaMemcpyDeviceToHost)); + return; + } + + ASSERT_EQ(data->numPlanes(), channels); + std::vector planes(width * height * channels); + const int planeBytes = width * height; + for (int c = 0; c < channels; ++c) + { + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(planes.data() + c * planeBytes, width, data->plane(c).basePtr, + data->plane(c).rowStride, width, height, cudaMemcpyDeviceToHost)); + } + host = nvcv::test::planar::InterleaveFromPlanes(planes, width, height, channels, 1); +} + +static std::vector MakeNonzeroU8Image(int width, int height, int channels, int sample) +{ + std::vector image(width * height * channels); + for (size_t i = 0; i < image.size(); ++i) + { + image[i] = static_cast(1 + (i * 29 + static_cast(sample) * 47) % 251); + } + return image; +} + +struct EraseParamTensors +{ + const nvcv::Tensor &anchor; + const nvcv::Tensor &erasing; + const nvcv::Tensor &values; + const nvcv::Tensor &imgIdx; +}; + +struct EraseParamValues +{ + const std::vector &anchor; + const std::vector &erasing; + const std::vector &values; + const std::vector &imgIdx; +}; + +static void ApplyErasePixelGold(std::vector &image, size_t pixelOffset, int channels, int channelMask, + const float *values) +{ + for (int c = 0; c < channels; ++c) + { + if ((channelMask & (1 << c)) != 0) + { + image[pixelOffset + c] = static_cast(values[c]); + } + } +} + +static void ApplyEraseGold(std::vector> &images, const std::vector &sizes, + int channels, const EraseParamValues ¶ms) +{ + ASSERT_EQ(images.size(), sizes.size()); + ASSERT_EQ(params.anchor.size(), params.erasing.size()); + ASSERT_EQ(params.anchor.size(), params.imgIdx.size()); + ASSERT_EQ(params.anchor.size() * static_cast(channels), params.values.size()); + + for (size_t area = 0; area < params.anchor.size(); ++area) + { + const int imageIndex = params.imgIdx[area]; + ASSERT_GE(imageIndex, 0); + ASSERT_LT(static_cast(imageIndex), sizes.size()); + + const int width = sizes[imageIndex].w; + const int height = sizes[imageIndex].h; + const int2 anchor = params.anchor[area]; + const int3 erasing = params.erasing[area]; + + ASSERT_EQ(images[imageIndex].size(), static_cast(width * height * channels)); + ASSERT_GE(anchor.x, 0); + ASSERT_GE(anchor.y, 0); + + for (int y = 0; y < erasing.y && anchor.y + y < height; ++y) + { + for (int x = 0; x < erasing.x && anchor.x + x < width; ++x) + { + const auto pixelOffset = static_cast(((anchor.y + y) * width + anchor.x + x) * channels); + ApplyErasePixelGold(images[imageIndex], pixelOffset, channels, erasing.z, + ¶ms.values[area * channels]); + } + } + } +} + +static void CopyEraseParams(cudaStream_t stream, const EraseParamTensors &tensors, const EraseParamValues &values) +{ + auto anchorData = tensors.anchor.exportData(); + auto erasingData = tensors.erasing.exportData(); + auto valuesData = tensors.values.exportData(); + auto imgIdxData = tensors.imgIdx.exportData(); + + ASSERT_NE(nullptr, anchorData); + ASSERT_NE(nullptr, erasingData); + ASSERT_NE(nullptr, valuesData); + ASSERT_NE(nullptr, imgIdxData); + + ASSERT_EQ(cudaSuccess, cudaMemcpyAsync(anchorData->basePtr(), values.anchor.data(), + values.anchor.size() * sizeof(int2), cudaMemcpyHostToDevice, stream)); + ASSERT_EQ(cudaSuccess, cudaMemcpyAsync(erasingData->basePtr(), values.erasing.data(), + values.erasing.size() * sizeof(int3), cudaMemcpyHostToDevice, stream)); + ASSERT_EQ(cudaSuccess, cudaMemcpyAsync(valuesData->basePtr(), values.values.data(), + values.values.size() * sizeof(float), cudaMemcpyHostToDevice, stream)); + ASSERT_EQ(cudaSuccess, cudaMemcpyAsync(imgIdxData->basePtr(), values.imgIdx.data(), + values.imgIdx.size() * sizeof(int), cudaMemcpyHostToDevice, stream)); +} + +template +static void RunEraseWithParams(cudaStream_t stream, const Input &src, const Input &dst, int channels, + const EraseParamValues ¶ms) +{ + const auto numErasingArea = static_cast(params.anchor.size()); + nvcv::Tensor anchor({{numErasingArea}, "N"}, nvcv::TYPE_2S32); + nvcv::Tensor erasing({{numErasingArea}, "N"}, nvcv::TYPE_3S32); + nvcv::Tensor values({{numErasingArea * channels}, "N"}, nvcv::TYPE_F32); + nvcv::Tensor imgIdx({{numErasingArea}, "N"}, nvcv::TYPE_S32); + + ASSERT_NO_FATAL_FAILURE(CopyEraseParams(stream, {anchor, erasing, values, imgIdx}, params)); + + cvcuda::Erase op(numErasingArea); + EXPECT_NO_THROW(op(stream, src, dst, anchor, erasing, values, imgIdx, false, 0)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); +} + +static EraseParamValues MakePlanarParityEraseParams(int channels, int numImages, std::vector &anchor, + std::vector &erasing, std::vector &values, + std::vector &imgIdx) +{ + constexpr int numErasingArea = 2; + + anchor = { + { 1, 1}, + {12, 7} + }; + erasing = { + {5, 4, (1 << channels) - 1}, + {4, 3, channels == 4 ? 0xA : 0x5} + }; + imgIdx = {0, numImages > 1 ? 1 : 0}; + + values.resize(numErasingArea * channels); + for (int area = 0; area < numErasingArea; ++area) + { + for (int c = 0; c < channels; ++c) + { + values[area * channels + c] = static_cast(13 + area * 17 + c * 3); + } + } + + return {anchor, erasing, values, imgIdx}; +} + +struct PlanarParityEraseRunner +{ + static constexpr int kNumErasingArea = 2; + + nvcv::Tensor anchor{ + {{kNumErasingArea}, "N"}, + nvcv::TYPE_2S32 + }; + nvcv::Tensor erasing{ + {{kNumErasingArea}, "N"}, + nvcv::TYPE_3S32 + }; + nvcv::Tensor values; + nvcv::Tensor imgIdx{ + {{kNumErasingArea}, "N"}, + nvcv::TYPE_S32 + }; + + std::vector anchorVec; + std::vector erasingVec; + std::vector valuesVec; + std::vector imgIdxVec; + EraseParamValues params; + cvcuda::Erase op{kNumErasingArea}; + bool random; + + PlanarParityEraseRunner(int channels, int numImages, bool random = false) + : values({{kNumErasingArea * channels}, "N"}, nvcv::TYPE_F32) + , params(MakePlanarParityEraseParams(channels, numImages, anchorVec, erasingVec, valuesVec, imgIdxVec)) + , random(random) + { + } + + void operator()(cudaStream_t stream, const nvcv::Tensor &src, const nvcv::Tensor &dst, nvcv::ImageFormat) + { + submit(stream, src, dst); + } + + void operator()(cudaStream_t stream, const nvcv::ImageBatchVarShape &src, const nvcv::ImageBatchVarShape &dst, + nvcv::ImageFormat) + { + submit(stream, src, dst); + } + +private: + template + void submit(cudaStream_t stream, const Input &src, const Output &dst) + { + CopyEraseParams(stream, {anchor, erasing, values, imgIdx}, params); + op(stream, src, dst, anchor, erasing, values, imgIdx, random, 17); + } +}; + +static void RunPlanarParityTensorCase(nvcv::ImageFormat planarFmt, nvcv::ImageFormat interleavedFmt, int w, int h, + int numImages) +{ + PlanarParityEraseRunner runner(planarFmt.numChannels(), numImages); + nvcv::test::planar::RunTensorParity(planarFmt, interleavedFmt, w, h, w, h, numImages, runner); +} + +static void RunPlanarParityVarShapeCase(nvcv::ImageFormat planarFmt, nvcv::ImageFormat interleavedFmt, int w, int h, + int numImages) +{ + PlanarParityEraseRunner runner(planarFmt.numChannels(), numImages); + nvcv::test::planar::RunVarShapeParity(planarFmt, interleavedFmt, w, h, w, h, numImages, runner); +} NVCV_TEST_SUITE_P(OpErase, nvcv::test::ValueList{ // N, random, isInplace @@ -51,9 +340,9 @@ TEST_P(OpErase, correct_output) cudaStream_t stream; EXPECT_EQ(cudaSuccess, cudaStreamCreate(&stream)); - nvcv::Tensor imgIn = nvcv::util::CreateTensor(N, 640, 480, nvcv::FMT_U8); - nvcv::Tensor _imgOut = nvcv::util::CreateTensor(N, 640, 480, nvcv::FMT_U8); - nvcv::Tensor &imgOut = isInplace ? imgIn : _imgOut; + nvcv::Tensor imgIn = nvcv::util::CreateTensor(N, 640, 480, nvcv::FMT_U8); + nvcv::Tensor _imgOut = nvcv::util::CreateTensor(N, 640, 480, nvcv::FMT_U8); + const nvcv::Tensor &imgOut = isInplace ? imgIn : _imgOut; auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(imgIn.exportData()); ASSERT_TRUE(inAccess); @@ -160,7 +449,7 @@ TEST_P(OpErase, correct_output) EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); } -TEST(OpErase, OpErase_Varshape) +TEST(OpErase, varshape_correct_output) { cudaStream_t stream; EXPECT_EQ(cudaSuccess, cudaStreamCreate(&stream)); @@ -168,43 +457,24 @@ TEST(OpErase, OpErase_Varshape) std::deque isRandoms{true, false}; for (bool isInplace : isInplaces) { - for (bool random : isRandoms) + for (bool random : isRandoms) // NOSONAR { - std::vector imgSrc, imgDst; + std::vector imgSrc; + std::vector imgDst; imgSrc.emplace_back(nvcv::Size2D{640, 480}, nvcv::FMT_U8); imgDst.emplace_back(nvcv::Size2D{640, 480}, nvcv::FMT_U8); - nvcv::ImageBatchVarShape batchSrc(1); - nvcv::ImageBatchVarShape _batchDst(1); - nvcv::ImageBatchVarShape &batchDst = isInplace ? batchSrc : _batchDst; + nvcv::ImageBatchVarShape batchSrc(1); + nvcv::ImageBatchVarShape _batchDst(1); + const nvcv::ImageBatchVarShape &batchDst = isInplace ? batchSrc : _batchDst; batchSrc.pushBack(imgSrc.begin(), imgSrc.end()); _batchDst.pushBack(imgDst.begin(), imgDst.end()); - for (int i = 0; i < 1; ++i) - { - const auto srcData = imgSrc[i].exportData(); - assert(srcData->numPlanes() == 1); - - int srcWidth = srcData->plane(0).width; - int srcHeight = srcData->plane(0).height; - - int srcRowStride = srcWidth * nvcv::FMT_U8.planePixelStrideBytes(0); - - EXPECT_EQ(cudaSuccess, - cudaMemset2D(srcData->plane(0).basePtr, srcRowStride, 0, srcRowStride, srcHeight)); - } + ClearU8Image(imgSrc[0]); if (!isInplace) { - for (int i = 0; i < 1; ++i) - { - const auto dstData = imgSrc[i].exportData(); - int dstWidth = dstData->plane(0).width; - int dstHeight = dstData->plane(0).height; - int dstRowStride = dstWidth * nvcv::FMT_U8.planePixelStrideBytes(0); - EXPECT_EQ(cudaSuccess, - cudaMemset2D(dstData->plane(0).basePtr, dstRowStride, 0, dstRowStride, dstHeight)); - } + ClearU8Image(imgDst[0]); } //parameters @@ -296,13 +566,812 @@ TEST(OpErase, OpErase_Varshape) EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); } +NVCV_TEST_SUITE_P(OpEraseVarShapeCopyGold, nvcv::test::ValueList{ + // format, row bytes are a multiple of 16 + { nvcv::FMT_RGB8, true}, + { nvcv::FMT_RGB8, false}, + {nvcv::FMT_RGB8p, true}, + {nvcv::FMT_RGB8p, false}, +}); + +TEST_P(OpEraseVarShapeCopyGold, full_nonzero_output_matches_independent_gold) +{ + const nvcv::ImageFormat format = GetParamValue<0>(); + const bool vectorCopyRows = GetParamValue<1>(); + const int channels = format.numChannels(); + const std::vector sizes = vectorCopyRows ? std::vector{{16, 9}, {32, 7}} + : std::vector{{13, 9}, {19, 7}}; + + const int rowChannels = format.numPlanes() == 1 ? channels : 1; + for (const nvcv::Size2D &size : sizes) + { + EXPECT_EQ(vectorCopyRows, (size.w * rowChannels) % static_cast(sizeof(uint4)) == 0); + } + + std::vector srcImages; + std::vector dstImages; + std::vector> srcHost; + std::vector> gold; + for (size_t i = 0; i < sizes.size(); ++i) + { + srcImages.emplace_back(sizes[i], format); + dstImages.emplace_back(sizes[i], format); + srcHost.emplace_back(MakeNonzeroU8Image(sizes[i].w, sizes[i].h, channels, static_cast(i))); + UploadU8Image(srcImages.back(), srcHost.back()); + UploadU8Image(dstImages.back(), std::vector(srcHost.back().size(), 0xA5)); + } + gold = srcHost; + + nvcv::ImageBatchVarShape batchSrc(static_cast(sizes.size())); + nvcv::ImageBatchVarShape batchDst(static_cast(sizes.size())); + batchSrc.pushBack(srcImages.begin(), srcImages.end()); + batchDst.pushBack(dstImages.begin(), dstImages.end()); + + std::vector anchorVec{ + { 1, 1}, + {sizes[0].w - 2, sizes[0].h - 2}, + { 0, 0}, + {sizes[1].w - 3, 1}, + }; + std::vector erasingVec{ + {6, 4, 0x7}, + {5, 4, 0x5}, + {4, 3, 0x2}, + {6, 5, 0x7}, + }; + std::vector imgIdxVec{0, 0, 1, 1}; + std::vector valuesVec(anchorVec.size() * channels); + for (size_t area = 0; area < anchorVec.size(); ++area) + { + for (int c = 0; c < channels; ++c) + { + valuesVec[area * channels + c] = static_cast(17 + area * 37 + c * 9); + } + } + + const EraseParamValues params{anchorVec, erasingVec, valuesVec, imgIdxVec}; + ApplyEraseGold(gold, sizes, channels, params); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + ASSERT_NO_FATAL_FAILURE(RunEraseWithParams(stream, batchSrc, batchDst, channels, params)); + + for (size_t i = 0; i < dstImages.size(); ++i) + { + SCOPED_TRACE(i); + std::vector got; + DownloadU8Image(dstImages[i], got); + EXPECT_EQ(gold[i], got); + } + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpEraseVarShapeCopyGold, asymmetric_pitches_copy_visible_bytes_only) +{ + constexpr int width = 5; + constexpr int height = 3; + constexpr int channels = 3; + constexpr size_t rowBytes = width * channels; + constexpr size_t srcRowStride = 32; + constexpr size_t dstRowStride = 16; + constexpr size_t dstGuardBytes = 32; + constexpr size_t srcBufferBytes = srcRowStride * height; + constexpr size_t dstBufferBytes = dstRowStride * height + dstGuardBytes; + + NVCVByte *srcAllocation{}; + NVCVByte *dstAllocation{}; + ASSERT_EQ(cudaSuccess, cudaMalloc(reinterpret_cast(&srcAllocation), srcBufferBytes)); + ASSERT_EQ(cudaSuccess, cudaMalloc(reinterpret_cast(&dstAllocation), dstBufferBytes)); + + { + nvcv::ImageDataStridedCuda::Buffer srcBuffer{}; + srcBuffer.numPlanes = 1; + srcBuffer.planes[0].width = width; + srcBuffer.planes[0].height = height; + srcBuffer.planes[0].rowStride = srcRowStride; + srcBuffer.planes[0].basePtr = srcAllocation; + + auto dstBuffer = srcBuffer; + dstBuffer.planes[0].rowStride = dstRowStride; + dstBuffer.planes[0].basePtr = dstAllocation; + + nvcv::Image srcImage = nvcv::ImageWrapData(nvcv::ImageDataStridedCuda{nvcv::FMT_RGB8, srcBuffer}); + nvcv::Image dstImage = nvcv::ImageWrapData(nvcv::ImageDataStridedCuda{nvcv::FMT_RGB8, dstBuffer}); + + std::vector srcHost(rowBytes * height); + for (size_t i = 0; i < srcHost.size(); ++i) + { + srcHost[i] = static_cast(i + 1); + } + + ASSERT_EQ(cudaSuccess, cudaMemset(srcAllocation, 0xCC, srcBufferBytes)); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(srcAllocation, srcRowStride, srcHost.data(), rowBytes, rowBytes, height, + cudaMemcpyHostToDevice)); + ASSERT_EQ(cudaSuccess, cudaMemset(dstAllocation, 0xA5, dstBufferBytes)); + + nvcv::ImageBatchVarShape batchSrc(1); + nvcv::ImageBatchVarShape batchDst(1); + batchSrc.pushBack(srcImage); + batchDst.pushBack(dstImage); + + std::vector anchorVec{ + {0, 0} + }; + std::vector erasingVec{ + {0, 0, 0x7} + }; + std::vector valuesVec{0, 0, 0}; + std::vector imgIdxVec{0}; + + nvcv::Tensor anchor({{1}, "N"}, nvcv::TYPE_2S32); + nvcv::Tensor erasing({{1}, "N"}, nvcv::TYPE_3S32); + nvcv::Tensor values({{channels}, "N"}, nvcv::TYPE_F32); + nvcv::Tensor imgIdx({{1}, "N"}, nvcv::TYPE_S32); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + CopyEraseParams(stream, {anchor, erasing, values, imgIdx}, {anchorVec, erasingVec, valuesVec, imgIdxVec}); + + cvcuda::Erase op(1); + EXPECT_NO_THROW(op(stream, batchSrc, batchDst, anchor, erasing, values, imgIdx, false, 0)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + std::vector got(dstBufferBytes); + ASSERT_EQ(cudaSuccess, cudaMemcpy(got.data(), dstAllocation, dstBufferBytes, cudaMemcpyDeviceToHost)); + for (size_t y = 0; y < height; ++y) + { + EXPECT_TRUE(std::equal(srcHost.begin() + y * rowBytes, srcHost.begin() + (y + 1) * rowBytes, + got.begin() + y * dstRowStride)); + EXPECT_EQ(0xA5, got[y * dstRowStride + rowBytes]); + } + EXPECT_TRUE( + std::all_of(got.begin() + dstRowStride * height, got.end(), [](uint8_t value) { return value == 0xA5; })); + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + } + + ASSERT_EQ(cudaSuccess, cudaFree(srcAllocation)); + ASSERT_EQ(cudaSuccess, cudaFree(dstAllocation)); +} + +TEST(OpEraseTensorCopyGold, tight_and_padded_planar_output_match_independent_gold) +{ + constexpr int numImages = 2; + constexpr int channels = 3; + constexpr int width = 16; + constexpr int height = 9; + + auto runCase = [&](bool padded) + { + SCOPED_TRACE(padded ? "padded fallback" : "tight bulk copy"); + + const int64_t rowStride = width + (padded ? 1 : 0); + const int64_t channelStride = rowStride * height; + const int64_t sampleStride = channelStride * channels; + const size_t bufferBytes = sampleStride * numImages; + + NVCVByte *srcAllocation{}; + NVCVByte *dstAllocation{}; + ASSERT_EQ(cudaSuccess, cudaMalloc(reinterpret_cast(&srcAllocation), bufferBytes)); + cudaError_t status = cudaMalloc(reinterpret_cast(&dstAllocation), bufferBytes); + if (status != cudaSuccess) + { + cudaFree(srcAllocation); + } + ASSERT_EQ(cudaSuccess, status); + + nvcv::TensorDataStridedCuda::Buffer srcBuffer{}; + srcBuffer.basePtr = srcAllocation; + srcBuffer.strides[0] = sampleStride; + srcBuffer.strides[1] = channelStride; + srcBuffer.strides[2] = rowStride; + srcBuffer.strides[3] = 1; + auto dstBuffer = srcBuffer; + dstBuffer.basePtr = dstAllocation; + + nvcv::TensorShape shape{ + {numImages, channels, height, width}, + "NCHW" + }; + nvcv::Tensor src + = nvcv::TensorWrapData(nvcv::TensorDataStridedCuda{shape, nvcv::TYPE_U8, srcBuffer}, + nvcv::TensorDataCleanupCallback{[srcAllocation](const nvcv::TensorData &) + { + cudaFree(srcAllocation); + }}); + nvcv::Tensor dst + = nvcv::TensorWrapData(nvcv::TensorDataStridedCuda{shape, nvcv::TYPE_U8, dstBuffer}, + nvcv::TensorDataCleanupCallback{[dstAllocation](const nvcv::TensorData &) + { + cudaFree(dstAllocation); + }}); + + auto srcData = src.exportData(); + auto dstData = dst.exportData(); + auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcData); + auto dstAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*dstData); + ASSERT_TRUE(srcAccess && dstAccess); + ASSERT_EQ(rowStride, srcAccess->rowStride()); + ASSERT_EQ(channelStride, srcAccess->chStride()); + ASSERT_EQ(sampleStride, srcAccess->sampleStride()); + ASSERT_EQ(rowStride, dstAccess->rowStride()); + ASSERT_EQ(channelStride, dstAccess->chStride()); + ASSERT_EQ(sampleStride, dstAccess->sampleStride()); + EXPECT_EQ(padded, rowStride != width); + + std::vector> srcHost; + for (int image = 0; image < numImages; ++image) + { + srcHost.emplace_back(MakeNonzeroU8Image(width, height, channels, image)); + const auto planes = nvcv::test::planar::DeinterleaveToPlanes(srcHost.back(), width, height, channels, 1); + for (int c = 0; c < channels; ++c) + { + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(srcAccess->sampleData(image) + c * srcAccess->chStride(), + srcAccess->rowStride(), planes.data() + c * width * height, width, + width, height, cudaMemcpyHostToDevice)); + } + } + ASSERT_EQ(cudaSuccess, cudaMemset(dstAllocation, 0xA5, bufferBytes)); + + std::vector anchorVec{ + { 1, 1}, + {13, 7}, + }; + std::vector erasingVec{ + {6, 4, 0x7}, + {6, 5, 0x5}, + }; + std::vector valuesVec{ + 17, 29, 41, 53, 65, 77, + }; + std::vector imgIdxVec{0, 1}; + + std::vector> gold = srcHost; + const std::vector sizes(numImages, {width, height}); + const EraseParamValues params{anchorVec, erasingVec, valuesVec, imgIdxVec}; + ApplyEraseGold(gold, sizes, channels, params); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + ASSERT_NO_FATAL_FAILURE(RunEraseWithParams(stream, src, dst, channels, params)); + + for (int image = 0; image < numImages; ++image) + { + std::vector planes(width * height * channels); + for (int c = 0; c < channels; ++c) + { + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(planes.data() + c * width * height, width, + dstAccess->sampleData(image) + c * dstAccess->chStride(), + dstAccess->rowStride(), width, height, cudaMemcpyDeviceToHost)); + } + const auto got = nvcv::test::planar::InterleaveFromPlanes(planes, width, height, channels, 1); + EXPECT_EQ(gold[image], got); + } + + if (padded) + { + std::vector raw(bufferBytes); + ASSERT_EQ(cudaSuccess, cudaMemcpy(raw.data(), dstAllocation, bufferBytes, cudaMemcpyDeviceToHost)); + for (int plane = 0; plane < numImages * channels; ++plane) + { + for (int y = 0; y < height; ++y) + { + const size_t paddingOffset = plane * channelStride + y * rowStride + width; + EXPECT_EQ(0xA5, raw[paddingOffset]); + } + } + } + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + }; + + ASSERT_NO_FATAL_FAILURE(runCase(false)); + ASSERT_NO_FATAL_FAILURE(runCase(true)); +} + +NVCV_TEST_SUITE_P(OpErasePlanar, nvcv::test::ValueList{ + // W, H, N, planar format, interleaved format + {23, 17, 2, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + {29, 19, 1, nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8}, + {21, 15, 2, nvcv::FMT_RGBf32p, nvcv::FMT_RGBf32}, + {25, 13, 1, nvcv::FMT_RGBAf32p, nvcv::FMT_RGBAf32}, +}); + +TEST_P(OpErasePlanar, tensor_matches_interleaved) +{ + RunPlanarParityTensorCase(GetParamValue<3>(), GetParamValue<4>(), GetParamValue<0>(), GetParamValue<1>(), + GetParamValue<2>()); +} + +NVCV_TEST_SUITE_P(OpErasePlanarVarShape, nvcv::test::ValueList{ + // W, H, N, planar format, interleaved format + {23, 17, 2, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + {29, 19, 1, nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8}, + {21, 15, 2, nvcv::FMT_RGBf32p, nvcv::FMT_RGBf32}, + {25, 13, 1, nvcv::FMT_RGBAf32p, nvcv::FMT_RGBAf32}, +}); + +TEST_P(OpErasePlanarVarShape, varshape_matches_interleaved) +{ + RunPlanarParityVarShapeCase(GetParamValue<3>(), GetParamValue<4>(), GetParamValue<0>(), GetParamValue<1>(), + GetParamValue<2>()); +} + +TEST(OpErasePlanarRandom, varshape_matches_interleaved) +{ + PlanarParityEraseRunner runner(3, 2, true); + nvcv::test::planar::RunVarShapeParity(nvcv::FMT_RGB8p, nvcv::FMT_RGB8, 23, 17, 23, 17, 2, runner); +} + +namespace { + +struct EraseRegionTypeCase +{ + nvcv::DataType dtype; + int kind; +}; + +constexpr int kEraseRegionWidth = 5; +constexpr int kEraseRegionHeight = 4; +constexpr int kEraseRegionBatch = 2; + +nvcv::Tensor MakeEraseRegionImage(std::string_view layout, int channels, nvcv::DataType dtype) +{ + if (layout == "NHWC") + return nvcv::Tensor( + { + {kEraseRegionBatch, kEraseRegionHeight, kEraseRegionWidth, channels}, + "NHWC" + }, + dtype); + if (layout == "NCHW") + return nvcv::Tensor( + { + {kEraseRegionBatch, channels, kEraseRegionHeight, kEraseRegionWidth}, + "NCHW" + }, + dtype); + if (layout == "HWC") + return nvcv::Tensor( + { + {kEraseRegionHeight, kEraseRegionWidth, channels}, + "HWC" + }, + dtype); + return nvcv::Tensor( + { + {channels, kEraseRegionHeight, kEraseRegionWidth}, + "CHW" + }, + dtype); +} + +int EraseRegionStorageIndex(std::string_view layout, int channels, int c, int y, int x) +{ + if (layout == "NHWC" || layout == "HWC") + return (y * kEraseRegionWidth + x) * channels + c; + return (c * kEraseRegionHeight + y) * kEraseRegionWidth + x; +} + +uint16_t EraseRegionHalfBits(float value) +{ + return __half_raw(__float2half(value)).x; +} + +template +T EraseRegionHostValue(float value, bool isHalf) +{ + if constexpr (std::is_same_v) + return isHalf ? EraseRegionHalfBits(value) : static_cast(value); + else + return static_cast(value); +} + +template +std::vector MakeEraseRegionSource(int channels, int sample, bool isHalf) +{ + std::vector source(channels * kEraseRegionHeight * kEraseRegionWidth); + size_t index = 0; + for (T &element : source) + { + element = EraseRegionHostValue(static_cast((index + sample * 7) % 23), isHalf); + ++index; + } + return source; +} + +template +void FillEraseRegionExpected(std::vector &expected, std::string_view layout, int channels, bool isHalf) +{ + const T erasedValue = EraseRegionHostValue(7.0f, isHalf); + for (int c = 0; c < channels; ++c) + for (int y = 1; y < 3; ++y) + for (int x = 1; x < 4; ++x) expected[EraseRegionStorageIndex(layout, channels, c, y, x)] = erasedValue; +} + +template +void RunEraseRegionMatrixCase(cudaStream_t stream, cvcuda::Erase &op, nvcv::DataType dtype, bool isHalf, + std::string_view layout, int channels, bool inplace, bool floatValue) +{ + nvcv::Tensor input = MakeEraseRegionImage(layout, channels, dtype); + nvcv::Tensor output = MakeEraseRegionImage(layout, channels, dtype); + nvcv::Tensor values({{1}, "W"}, floatValue ? nvcv::TYPE_F32 : dtype); + + const int samples = layout.size() == 4 ? kEraseRegionBatch : 1; + for (int sample = 0; sample < samples; ++sample) + { + std::vector source = MakeEraseRegionSource(channels, sample, isHalf); + nvcv::util::SetImageTensorFromVector(input.exportData(), source, sample); + } + + if (floatValue) + { + std::vector hostValue{7.0f}; + ASSERT_EQ(cudaSuccess, cudaMemcpy(values.exportData()->basePtr(), hostValue.data(), + sizeof(float), cudaMemcpyHostToDevice)); + } + else + { + std::vector hostValue{EraseRegionHostValue(7.0f, isHalf)}; + ASSERT_EQ(cudaSuccess, cudaMemcpy(values.exportData()->basePtr(), hostValue.data(), + sizeof(T), cudaMemcpyHostToDevice)); + } + + const nvcv::Tensor &destination = inplace ? input : output; + op(stream, input, destination, 1, 1, 2, 3, values); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + for (int sample = 0; sample < samples; ++sample) + { + std::vector expected = MakeEraseRegionSource(channels, sample, isHalf); + FillEraseRegionExpected(expected, layout, channels, isHalf); + + std::vector actual; + nvcv::util::GetImageVectorFromTensor(destination.exportData(), sample, actual); + EXPECT_EQ(expected, actual); + } +} + +void DispatchEraseRegionMatrixCase(cudaStream_t stream, cvcuda::Erase &op, const EraseRegionTypeCase &typeCase, + std::string_view layout, int channels, bool inplace, bool floatValue) +{ + switch (typeCase.kind) + { + case 0: + RunEraseRegionMatrixCase(stream, op, typeCase.dtype, false, layout, channels, inplace, floatValue); + break; + case 1: + RunEraseRegionMatrixCase(stream, op, typeCase.dtype, false, layout, channels, inplace, floatValue); + break; + case 2: + RunEraseRegionMatrixCase(stream, op, typeCase.dtype, false, layout, channels, inplace, floatValue); + break; + case 3: + RunEraseRegionMatrixCase(stream, op, typeCase.dtype, false, layout, channels, inplace, floatValue); + break; + case 4: + RunEraseRegionMatrixCase(stream, op, typeCase.dtype, false, layout, channels, inplace, floatValue); + break; + case 5: + RunEraseRegionMatrixCase(stream, op, typeCase.dtype, false, layout, channels, inplace, floatValue); + break; + case 6: + RunEraseRegionMatrixCase(stream, op, typeCase.dtype, false, layout, channels, inplace, floatValue); + break; + case 7: + RunEraseRegionMatrixCase(stream, op, typeCase.dtype, false, layout, channels, inplace, floatValue); + break; + case 8: + RunEraseRegionMatrixCase(stream, op, typeCase.dtype, true, layout, channels, inplace, floatValue); + break; + case 9: + RunEraseRegionMatrixCase(stream, op, typeCase.dtype, false, layout, channels, inplace, floatValue); + break; + default: + RunEraseRegionMatrixCase(stream, op, typeCase.dtype, false, layout, channels, inplace, floatValue); + break; + } +} + +void RunEraseRegionValueModes(cudaStream_t stream, cvcuda::Erase &op, const EraseRegionTypeCase &typeCase, + std::string_view layout, int channels) +{ + for (bool inplace : {false, true}) + { + DispatchEraseRegionMatrixCase(stream, op, typeCase, layout, channels, inplace, false); + if (typeCase.dtype != nvcv::TYPE_F32) + DispatchEraseRegionMatrixCase(stream, op, typeCase, layout, channels, inplace, true); + } +} + +void FillEraseRegionBroadcastExpected(std::vector &expected, const std::vector &values, int channels) +{ + constexpr int start = 2; + constexpr int height = 2; + constexpr int width = 3; + + for (int c = 0; c < channels; ++c) + for (int y = 0; y < height; ++y) + for (int x = 0; x < width; ++x) + expected[(c * kEraseRegionHeight + y + start) * kEraseRegionWidth + x + start] + = values[(c * height + y) * width + x]; +} + +} // namespace + +TEST(OpEraseRegion, all_scalar_dtypes_layouts_channels_and_inplace) +{ + const std::array types{ + { + {nvcv::TYPE_U8, 0}, + {nvcv::TYPE_S8, 1}, + {nvcv::TYPE_U16, 2}, + {nvcv::TYPE_S16, 3}, + {nvcv::TYPE_U32, 4}, + {nvcv::TYPE_S32, 5}, + {nvcv::TYPE_U64, 6}, + {nvcv::TYPE_S64, 7}, + {nvcv::TYPE_F16, 8}, + {nvcv::TYPE_F32, 9}, + {nvcv::TYPE_F64, 10}, + } + }; + constexpr std::array layouts{ + {"NHWC", "HWC", "NCHW", "CHW"} + }; + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + cvcuda::Erase op(0); + + for (const auto &typeCase : types) + for (const auto &layout : layouts) + for (int channels = 1; channels <= 4; ++channels) + RunEraseRegionValueModes(stream, op, typeCase, layout, channels); + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpEraseRegion, broadcast_and_python_slice_semantics_are_bit_exact) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + constexpr int channels = 3; + nvcv::Tensor input( + { + {2, channels, 4, 5}, + "NCHW" + }, + nvcv::TYPE_F32); + nvcv::Tensor output( + { + {2, channels, 4, 5}, + "NCHW" + }, + nvcv::TYPE_F32); + nvcv::Tensor values( + { + {channels, 2, 3}, + "CHW" + }, + nvcv::TYPE_F32); + + std::vector source(channels * 4 * 5, -1.0f); + nvcv::util::SetImageTensorFromVector(input.exportData(), source, 0); + nvcv::util::SetImageTensorFromVector(input.exportData(), source, 1); + + std::vector hostValues(channels * 2 * 3); + for (size_t index = 0; index < hostValues.size(); ++index) hostValues[index] = static_cast(index) + 0.25f; + nvcv::util::SetImageTensorFromVector(values.exportData(), hostValues, 0); + + cvcuda::Erase op(0); + op(stream, input, output, -2, -3, 10, 10, values); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + for (int sample = 0; sample < 2; ++sample) + { + std::vector actual; + nvcv::util::GetImageVectorFromTensor(output.exportData(), sample, actual); + std::vector expected = source; + FillEraseRegionBroadcastExpected(expected, hostValues, channels); + EXPECT_EQ(expected, actual); + } + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpEraseRegion, strided_values_are_bit_exact) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + constexpr int channels = 3; + constexpr int valueHeight = 2; + constexpr int valueWidth = 3; + constexpr int valueRowPitch = 5; + constexpr int valueChPitch = valueHeight * valueRowPitch; + + NVCVByte *valueAllocation{}; + ASSERT_EQ(cudaSuccess, + cudaMalloc(reinterpret_cast(&valueAllocation), channels * valueChPitch * sizeof(float))); + + nvcv::TensorDataStridedCuda::Buffer valueBuffer{}; + valueBuffer.basePtr = valueAllocation; + valueBuffer.strides[0] = valueChPitch * sizeof(float); + valueBuffer.strides[1] = valueRowPitch * sizeof(float); + valueBuffer.strides[2] = sizeof(float); + nvcv::Tensor values = nvcv::TensorWrapData( + nvcv::TensorDataStridedCuda{ + nvcv::TensorShape{{channels, valueHeight, valueWidth}, "CHW"}, + nvcv::TYPE_F32, valueBuffer + }, + nvcv::TensorDataCleanupCallback{[valueAllocation](const nvcv::TensorData &) + { + cudaFree(valueAllocation); + }}); + + std::vector hostValues(channels * valueChPitch, -123.0f); + for (int c = 0; c < channels; ++c) + for (int y = 0; y < valueHeight; ++y) + for (int x = 0; x < valueWidth; ++x) + hostValues[c * valueChPitch + y * valueRowPitch + x] + = static_cast(c * 20 + y * valueWidth + x) + 0.25f; + ASSERT_EQ(cudaSuccess, cudaMemcpy(valueAllocation, hostValues.data(), hostValues.size() * sizeof(float), + cudaMemcpyHostToDevice)); + + nvcv::Tensor input( + { + {1, channels, 4, 5}, + "NCHW" + }, + nvcv::TYPE_F32); + nvcv::Tensor output( + { + {1, channels, 4, 5}, + "NCHW" + }, + nvcv::TYPE_F32); + std::vector source(channels * 4 * 5, -1.0f); + nvcv::util::SetImageTensorFromVector(input.exportData(), source, 0); + + cvcuda::Erase op(0); + op(stream, input, output, 1, 1, valueHeight, valueWidth, values); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + std::vector expected = source; + for (int c = 0; c < channels; ++c) + for (int y = 0; y < valueHeight; ++y) + for (int x = 0; x < valueWidth; ++x) + expected[(c * 4 + y + 1) * 5 + x + 1] = hostValues[c * valueChPitch + y * valueRowPitch + x]; + + std::vector actual; + nvcv::util::GetImageVectorFromTensor(output.exportData(), 0, actual); + EXPECT_EQ(expected, actual); + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpEraseRegion, float32_to_uint8_matches_torch_assignment_cast) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + nvcv::Tensor input( + { + {1, 1, 1, 6}, + "NCHW" + }, + nvcv::TYPE_U8); + nvcv::Tensor output( + { + {1, 1, 1, 6}, + "NCHW" + }, + nvcv::TYPE_U8); + nvcv::Tensor values({{6}, "W"}, nvcv::TYPE_F32); + ASSERT_EQ(cudaSuccess, cudaMemset(input.exportData()->basePtr(), 0, 6)); + std::vector hostValues{-1.9f, 1.9f, 255.9f, 256.1f, 4294967296.0f, 1.0e20f}; + ASSERT_EQ(cudaSuccess, cudaMemcpy(values.exportData()->basePtr(), hostValues.data(), + hostValues.size() * sizeof(float), cudaMemcpyHostToDevice)); + + cvcuda::Erase op(0); + op(stream, input, output, 0, 0, 1, 6, values); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + std::vector actual; + nvcv::util::GetImageVectorFromTensor(output.exportData(), 0, actual); + EXPECT_EQ((std::vector{255, 1, 255, 0, 0, 255}), actual); + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpEraseRegion_Negative, rejects_unsupported_matrix_and_broadcast_complement) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + cvcuda::Erase op(0); + + nvcv::Tensor validInput( + { + {1, 4, 5, 3}, + "NHWC" + }, + nvcv::TYPE_U8); + nvcv::Tensor validOutput( + { + {1, 4, 5, 3}, + "NHWC" + }, + nvcv::TYPE_U8); + nvcv::Tensor scalarValue({{1}, "W"}, nvcv::TYPE_U8); + + auto expectInvalid + = [&op, stream](const nvcv::Tensor &input, const nvcv::Tensor &output, const nvcv::Tensor &values) + { + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&op, stream, &input, &output, &values] + { op(stream, input, output, 1, 1, 2, 3, values); })); + }; + + nvcv::Tensor packedInput( + { + {1, 4, 5, 3}, + "NHWC" + }, + nvcv::TYPE_2U8); + nvcv::Tensor packedOutput( + { + {1, 4, 5, 3}, + "NHWC" + }, + nvcv::TYPE_2U8); + nvcv::Tensor packedValue({{1}, "W"}, nvcv::TYPE_2U8); + expectInvalid(packedInput, packedOutput, packedValue); + + nvcv::Tensor fiveChannelInput( + { + {1, 4, 5, 5}, + "NHWC" + }, + nvcv::TYPE_U8); + nvcv::Tensor fiveChannelOutput( + { + {1, 4, 5, 5}, + "NHWC" + }, + nvcv::TYPE_U8); + expectInvalid(fiveChannelInput, fiveChannelOutput, scalarValue); + + nvcv::Tensor wrongDtypeValue({{1}, "W"}, nvcv::TYPE_S16); + expectInvalid(validInput, validOutput, wrongDtypeValue); + + nvcv::Tensor wrongBroadcastValue( + { + {4, 2, 3}, + "CHW" + }, + nvcv::TYPE_U8); + expectInvalid(validInput, validOutput, wrongBroadcastValue); + + nvcv::Tensor wrongLayoutOutput( + { + {1, 3, 4, 5}, + "NCHW" + }, + nvcv::TYPE_U8); + expectInvalid(validInput, wrongLayoutOutput, scalarValue); + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + // clang-format off NVCV_TEST_SUITE_P(OpErase_Negative, nvcv::test::ValueList { // in_format, out_format, anchor_layout, anchor_datatype, erasingData_layout, erasingData_datatype, imgIdxData_layout, imgIdxData_datatype, valuesData_layout, valuesData_type, num_erasing_area - { nvcv::FMT_RGB8p, nvcv::FMT_RGB8, "N", nvcv::TYPE_2S32, "N", nvcv::TYPE_3S32, "N", nvcv::TYPE_S32, "N", nvcv::TYPE_F32, 2}, // invalid in layout + { nvcv::FMT_RGB8p, nvcv::FMT_RGB8, "N", nvcv::TYPE_2S32, "N", nvcv::TYPE_3S32, "N", nvcv::TYPE_S32, "N", nvcv::TYPE_F32, 2}, // layout mismatch { nvcv::FMT_RGBf16, nvcv::FMT_RGBf16, "N", nvcv::TYPE_2S32, "N", nvcv::TYPE_3S32, "N", nvcv::TYPE_S32, "N", nvcv::TYPE_F32, 2}, // invalid in layout - { nvcv::FMT_RGB8, nvcv::FMT_RGB8p, "N", nvcv::TYPE_2S32, "N", nvcv::TYPE_3S32, "N", nvcv::TYPE_S32, "N", nvcv::TYPE_F32, 2}, // invalid out layout + { nvcv::FMT_RGB8, nvcv::FMT_RGB8p, "N", nvcv::TYPE_2S32, "N", nvcv::TYPE_3S32, "N", nvcv::TYPE_S32, "N", nvcv::TYPE_F32, 2}, // layout mismatch { nvcv::FMT_RGB8, nvcv::FMT_RGBf32, "N", nvcv::TYPE_2S32, "N", nvcv::TYPE_3S32, "N", nvcv::TYPE_S32, "N", nvcv::TYPE_F32, 2}, // different datatype { nvcv::FMT_RGB8, nvcv::FMT_RGB8, "N", nvcv::TYPE_2F32, "N", nvcv::TYPE_3S32, "N", nvcv::TYPE_S32, "N", nvcv::TYPE_F32, 2}, // invalid anchor datatype { nvcv::FMT_RGB8, nvcv::FMT_RGB8, "NHW", nvcv::TYPE_2S32, "N", nvcv::TYPE_3S32, "N", nvcv::TYPE_S32, "N", nvcv::TYPE_F32, 2}, // invalid anchor dim @@ -328,6 +1397,139 @@ TEST(OpErase_Negative, create_negative_area) EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, cvcudaEraseCreate(&handle, -1)); } +TEST(OpErase_Negative, rejectsInvalidTensorAreaParameters) +{ + cudaStream_t stream; + EXPECT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + constexpr int numErasingArea = 1; + cvcuda::Erase eraseOp(numErasingArea); + + nvcv::Tensor imgIn = nvcv::util::CreateTensor(1, 8, 8, nvcv::FMT_RGB8); + nvcv::Tensor imgOut = nvcv::util::CreateTensor(1, 8, 8, nvcv::FMT_RGB8); + + auto expectInvalid = [&](int2 anchorValue, int imgIdxValue, int valuesLen) + { + nvcv::Tensor anchor({{numErasingArea}, "N"}, nvcv::TYPE_2S32); + nvcv::Tensor erasing({{numErasingArea}, "N"}, nvcv::TYPE_3S32); + nvcv::Tensor values({{valuesLen}, "N"}, nvcv::TYPE_F32); + nvcv::Tensor imgIdx({{numErasingArea}, "N"}, nvcv::TYPE_S32); + + std::vector anchorVec{anchorValue}; + std::vector erasingVec{ + {0, 0, 0x7} + }; + std::vector valuesVec(valuesLen, 1.f); + std::vector imgIdxVec{imgIdxValue}; + + CopyEraseParams(stream, {anchor, erasing, values, imgIdx}, {anchorVec, erasingVec, valuesVec, imgIdxVec}); + + EXPECT_EQ( + NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&] { eraseOp(stream, imgIn, imgOut, anchor, erasing, values, imgIdx, false, 0); })); + }; + + expectInvalid({-1, 0}, 0, 3); + expectInvalid({0, -1}, 0, 3); + expectInvalid({0, 0}, 1, 3); + expectInvalid({0, 0}, 0, 2); + + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpErase_Negative, rejectsInvalidVarShapeAreaParameters) +{ + cudaStream_t stream; + EXPECT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + constexpr int numErasingArea = 1; + cvcuda::Erase eraseOp(numErasingArea); + + nvcv::ImageBatchVarShape batchSrc(1); + nvcv::ImageBatchVarShape batchDst(1); + batchSrc.pushBack(nvcv::Image{ + nvcv::Size2D{8, 8}, + nvcv::FMT_RGB8 + }); + batchDst.pushBack(nvcv::Image{ + nvcv::Size2D{8, 8}, + nvcv::FMT_RGB8 + }); + + auto expectInvalid = [&](int2 anchorValue, int imgIdxValue, int valuesLen) + { + nvcv::Tensor anchor({{numErasingArea}, "N"}, nvcv::TYPE_2S32); + nvcv::Tensor erasing({{numErasingArea}, "N"}, nvcv::TYPE_3S32); + nvcv::Tensor values({{valuesLen}, "N"}, nvcv::TYPE_F32); + nvcv::Tensor imgIdx({{numErasingArea}, "N"}, nvcv::TYPE_S32); + + std::vector anchorVec{anchorValue}; + std::vector erasingVec{ + {0, 0, 0x7} + }; + std::vector valuesVec(valuesLen, 1.f); + std::vector imgIdxVec{imgIdxValue}; + + CopyEraseParams(stream, {anchor, erasing, values, imgIdx}, {anchorVec, erasingVec, valuesVec, imgIdxVec}); + + EXPECT_EQ( + NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&] { eraseOp(stream, batchSrc, batchDst, anchor, erasing, values, imgIdx, false, 0); })); + }; + + expectInvalid({-1, 0}, 0, 3); + expectInvalid({0, -1}, 0, 3); + expectInvalid({0, 0}, 1, 3); + expectInvalid({0, 0}, 0, 2); + + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpErase_Negative, varshape_batch_count_mismatch) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + constexpr int numErasingArea = 1; + cvcuda::Erase eraseOp(2); + + nvcv::ImageBatchVarShape batchSrc(2); + nvcv::ImageBatchVarShape batchDst(2); + batchSrc.pushBack(nvcv::Image{ + nvcv::Size2D{8, 8}, + nvcv::FMT_RGB8 + }); + batchDst.pushBack(nvcv::Image{ + nvcv::Size2D{8, 8}, + nvcv::FMT_RGB8 + }); + batchDst.pushBack(nvcv::Image{ + nvcv::Size2D{8, 8}, + nvcv::FMT_RGB8 + }); + + nvcv::Tensor anchor({{numErasingArea}, "N"}, nvcv::TYPE_2S32); + nvcv::Tensor erasing({{numErasingArea}, "N"}, nvcv::TYPE_3S32); + nvcv::Tensor values({{numErasingArea * 3}, "N"}, nvcv::TYPE_F32); + nvcv::Tensor imgIdx({{numErasingArea}, "N"}, nvcv::TYPE_S32); + + std::vector anchorVec{ + {0, 0} + }; + std::vector erasingVec{ + {1, 1, 0x7} + }; + std::vector valuesVec{1.f, 1.f, 1.f}; + std::vector imgIdxVec{0}; + CopyEraseParams(stream, {anchor, erasing, values, imgIdx}, {anchorVec, erasingVec, valuesVec, imgIdxVec}); + + EXPECT_EQ( + NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&] { eraseOp(stream, batchSrc, batchDst, anchor, erasing, values, imgIdx, false, 0); })); + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + TEST_P(OpErase_Negative, infer_negative_parameter) { nvcv::ImageFormat inputFmt = GetParamValue<0>(); @@ -360,9 +1562,9 @@ TEST_P(OpErase_Negative, infer_negative_parameter) // Call operator cvcuda::Erase eraseOp(max_num_erasing_area); - EXPECT_EQ( - NVCV_ERROR_INVALID_ARGUMENT, - nvcv::ProtectCall([&] { eraseOp(nullptr, imgIn, imgOut, anchor, erasing, values, imgIdx, false, seed); })); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&eraseOp, &imgIn, &imgOut, &anchor, &erasing, &values, &imgIdx, &seed] + { eraseOp(nullptr, imgIn, imgOut, anchor, erasing, values, imgIdx, false, seed); })); } TEST_P(OpErase_Negative, varshape_infer_negative_parameter) @@ -407,7 +1609,8 @@ TEST_P(OpErase_Negative, varshape_infer_negative_parameter) cvcuda::Erase eraseOp(max_num_erasing_area); EXPECT_EQ( NVCV_ERROR_INVALID_ARGUMENT, - nvcv::ProtectCall([&] { eraseOp(nullptr, batchSrc, batchDst, anchor, erasing, values, imgIdx, false, seed); })); + nvcv::ProtectCall([&eraseOp, &batchSrc, &batchDst, &anchor, &erasing, &values, &imgIdx, &seed] + { eraseOp(nullptr, batchSrc, batchDst, anchor, erasing, values, imgIdx, false, seed); })); } TEST(OpErase_Negative, varshape_hasDifferentFormat) @@ -426,15 +1629,12 @@ TEST(OpErase_Negative, varshape_hasDifferentFormat) {nvcv::FMT_RGBA8, fmt}, { fmt, nvcv::FMT_RGBA8} }; - for (auto testCase : testSet) + for (const auto &[inputFmtExtra, outputFmtExtra] : testSet) { - nvcv::ImageFormat inputFmtExtra = std::get<0>(testCase); - nvcv::ImageFormat outputFmtExtra = std::get<1>(testCase); - // Create input and output - std::default_random_engine randEng; - std::uniform_int_distribution rndSrcWidth(srcWidthBase * 0.8, srcWidthBase * 1.1); - std::uniform_int_distribution rndSrcHeight(srcHeightBase * 0.8, srcHeightBase * 1.1); + std::default_random_engine randEng; + std::uniform_int_distribution rndSrcWidth(ScaledSize(srcWidthBase, 0.8), ScaledSize(srcWidthBase, 1.1)); + std::uniform_int_distribution rndSrcHeight(ScaledSize(srcHeightBase, 0.8), ScaledSize(srcHeightBase, 1.1)); int num_erasing_area = 2; nvcv::Tensor anchor({{num_erasing_area}, "N"}, nvcv::TYPE_2S32); @@ -442,7 +1642,9 @@ TEST(OpErase_Negative, varshape_hasDifferentFormat) nvcv::Tensor values({{num_erasing_area}, "N"}, nvcv::TYPE_F32); nvcv::Tensor imgIdx({{num_erasing_area}, "N"}, nvcv::TYPE_S32); - std::vector imgSrc, imgDst; + std::vector imgSrc; + + std::vector imgDst; for (int i = 0; i < numberOfImages - 1; ++i) { @@ -463,9 +1665,10 @@ TEST(OpErase_Negative, varshape_hasDifferentFormat) // Generate test result cvcuda::Erase eraseOp(num_erasing_area); - EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, - nvcv::ProtectCall( - [&] { eraseOp(nullptr, batchSrc, batchDst, anchor, erasing, values, imgIdx, false, seed); })); + EXPECT_EQ( + NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&eraseOp, &batchSrc, &batchDst, &anchor, &erasing, &values, &imgIdx, &seed] + { eraseOp(nullptr, batchSrc, batchDst, anchor, erasing, values, imgIdx, false, seed); })); } // Get test data back diff --git a/tests/cvcuda/system/TestOpFindHomography.cpp b/tests/cvcuda/system/TestOpFindHomography.cpp index c726031d5..ed5c33dd4 100644 --- a/tests/cvcuda/system/TestOpFindHomography.cpp +++ b/tests/cvcuda/system/TestOpFindHomography.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -29,6 +29,7 @@ #include #include +#include #include #include #include @@ -42,24 +43,39 @@ namespace test = nvcv::test; namespace util = nvcv::util; namespace cuda = nvcv::cuda; -static std::default_random_engine g_rng(std::random_device{}()); - -static void calculateDst(float x, float y, float *X, float *Y, float *model) +static void calculateDst(float x, float y, float *X, float *Y, const float *model) { *X = model[0] * x + model[1] * y + model[2] * 1; *Y = model[3] * x + model[4] * y + model[5] * 1; } +static void calculateProjectiveDst(float x, float y, float *X, float *Y, const float *model) +{ + float w = model[6] * x + model[7] * y + model[8]; + *X = (model[0] * x + model[1] * y + model[2]) / w; + *Y = (model[3] * x + model[4] * y + model[5]) / w; +} + +static nvcv::Tensor createModelTensor(int numSamples) +{ + return nvcv::Tensor( + { + {numSamples, 3, 3}, + "NHW" + }, + nvcv::TYPE_F32); +} + static void calculateGoldModelMatrix(float *m, std::mt19937 &rng, std::uniform_int_distribution &dis) { // random rotation angle between 0 and pi - float theta = (M_PI / 2.0) * dis(rng) / 100; - float Tx = (float)dis(rng) / 100; - float Ty = (float)dis(rng) / 100; - float sx = (float)dis(rng) / 100; - float sy = (float)dis(rng) / 100; - float p1 = (float)dis(rng) / 100; - float p2 = (float)dis(rng) / 100 * 2; + float theta = static_cast(M_PI / 2.0) * static_cast(dis(rng)) / 100.0f; + float Tx = static_cast(dis(rng)) / 100.0f; + float Ty = static_cast(dis(rng)) / 100.0f; + float sx = static_cast(dis(rng)) / 100.0f; + float sy = static_cast(dis(rng)) / 100.0f; + float p1 = static_cast(dis(rng)) / 100.0f; + float p2 = static_cast(dis(rng)) / 100.0f * 2.0f; cuda::math::Matrix He; He[0] = {cos(theta), -sin(theta), Tx}; He[1] = {sin(theta), cos(theta), Ty}; @@ -80,7 +96,7 @@ static void calculateGoldModelMatrix(float *m, std::mt19937 &rng, std::uniform_i // clang-format off NVCV_TEST_SUITE_P(OpFindHomography, test::ValueList { - // numSamples, numPoints} + // Parameter order: sample count, point count. {8, 16}, {16, 20}, {25, 40} @@ -114,12 +130,15 @@ TEST_P(OpFindHomography, correct_output) std::vector estimatedModelsVec(numSamples * 9); std::vector computedDstVec(2 * numSamples * numPoints); - std::random_device rd; - std::mt19937 gen(rd()); // Mersenne Twister engine - std::uniform_int_distribution<> dis(0, 100); + // Fixed seed matches the sibling varshape_correct_output test below — the + // original random_device-seeded gen made input geometry non-deterministic + // and occasionally produced ill-conditioned point sets that exceeded the + // 1e-3 tolerance on rare-config CI (manylinux x86 gcc10 release). + std::mt19937 gen(12345); // Mersenne Twister engine + std::uniform_int_distribution dis(0, 100); - int numXPoints = static_cast(std::sqrt(numPoints)); - int numYPoints = numXPoints; + auto numXPoints = static_cast(std::sqrt(numPoints)); + int numYPoints = numXPoints; #ifdef WRITE_COORDINATES_TO_FILE std::string src_filename @@ -153,10 +172,11 @@ TEST_P(OpFindHomography, correct_output) for (int k = 0; k < numXPoints; k++) { int idx = j * numYPoints + k; - srcVec[i * numPoints * 2 + 2 * idx] = dis(gen); - srcVec[i * numPoints * 2 + 2 * idx + 1] = dis(gen); + srcVec[i * numPoints * 2 + 2 * idx] = static_cast(dis(gen)); + srcVec[i * numPoints * 2 + 2 * idx + 1] = static_cast(dis(gen)); - float dstx, dsty; + float dstx; + float dsty; calculateDst(srcVec[i * numPoints * 2 + 2 * idx], srcVec[i * numPoints * 2 + 2 * idx + 1], &dstx, &dsty, modelsVec.data() + i * 9); dstVec[i * numPoints * 2 + 2 * idx] = dstx; @@ -173,14 +193,21 @@ TEST_P(OpFindHomography, correct_output) outDstFile.close(); #endif - ASSERT_EQ(cudaSuccess, cudaMemcpy(srcData->basePtr(), srcVec.data(), sizeof(float) * 2 * numPoints * numSamples, - cudaMemcpyHostToDevice)); - ASSERT_EQ(cudaSuccess, cudaMemcpy(dstData->basePtr(), dstVec.data(), sizeof(float) * 2 * numPoints * numSamples, - cudaMemcpyHostToDevice)); - + // Create the test stream BEFORE the input uploads so the H2D copies are + // queued on the same stream the operator will use. Using cudaMemcpyAsync on + // a non-blocking stream guarantees the operator's reduction kernels see the + // populated dst data — synchronous cudaMemcpy on the default stream does + // NOT order against work on a non-blocking custom stream, which on certain + // driver versions (observed on 580.35) produced an all-zero dst tensor at + // kernel-read time and a silent zero-homography output (CVCUDA-####). cudaStream_t stream; ASSERT_EQ(cudaSuccess, cudaStreamCreateWithFlags(&stream, cudaStreamNonBlocking)); + ASSERT_EQ(cudaSuccess, cudaMemcpyAsync(srcData->basePtr(), srcVec.data(), + sizeof(float) * 2 * numPoints * numSamples, cudaMemcpyHostToDevice, stream)); + ASSERT_EQ(cudaSuccess, cudaMemcpyAsync(dstData->basePtr(), dstVec.data(), + sizeof(float) * 2 * numPoints * numSamples, cudaMemcpyHostToDevice, stream)); + cvcuda::FindHomography fh(numSamples, numPoints); #ifdef PERFORMANCE_RUN @@ -232,16 +259,19 @@ TEST_P(OpFindHomography, correct_output) for (int k = 0; k < numXPoints; k++) { int idx = j * numYPoints + k; - float dstx, dsty; + float dstx; + float dsty; calculateDst(srcVec[i * numPoints * 2 + 2 * idx], srcVec[i * numPoints * 2 + 2 * idx + 1], &dstx, &dsty, estimatedModelsVec.data() + i * 9); computedDstVec[i * numPoints * 2 + 2 * idx] = dstx; computedDstVec[i * numPoints * 2 + 2 * idx + 1] = dsty; float A = dstVec[i * numPoints * 2 + 2 * idx]; float B = computedDstVec[i * numPoints * 2 + 2 * idx]; + // The 1e-3 tolerance covers GPU reduction/FMA rounding against the independent CPU projection. EXPECT_NEAR(A, B, 1e-03); A = dstVec[i * numPoints * 2 + 2 * idx + 1]; B = computedDstVec[i * numPoints * 2 + 2 * idx + 1]; + // The 1e-3 tolerance covers GPU reduction/FMA rounding against the independent CPU projection. EXPECT_NEAR(A, B, 1e-03); } } @@ -249,6 +279,106 @@ TEST_P(OpFindHomography, correct_output) #endif } +TEST(OpFindHomography, nwc_correct_output) +{ + constexpr int numSamples = 2; + constexpr std::array goldModel = {1.05f, 0.08f, 0.15f, -0.04f, 0.97f, -0.10f, 0.015f, -0.020f, 1.0f}; + constexpr std::array numPointCases{4, 16}; + + auto runCase = [&](int numPoints) + { + SCOPED_TRACE(::testing::Message() << "numPoints=" << numPoints); + + nvcv::Tensor srcPoints( + { + {numSamples, numPoints, 2}, + "NWC" + }, + nvcv::TYPE_F32); + nvcv::Tensor dstPoints( + { + {numSamples, numPoints, 2}, + "NWC" + }, + nvcv::TYPE_F32); + nvcv::Tensor models = createModelTensor(numSamples); + + auto srcData = srcPoints.exportData(); + auto dstData = dstPoints.exportData(); + auto modelsData = models.exportData(); + + std::vector srcVec(2 * numSamples * numPoints); + std::vector dstVec(2 * numSamples * numPoints); + std::vector estimatedModelsVec(numSamples * 9); + + int gridSide = 1; + while (gridSide * gridSide < numPoints) + { + ++gridSide; + } + float gridScale = 2.0f / static_cast(gridSide - 1); + + for (int sample = 0; sample < numSamples; ++sample) + { + for (int point = 0; point < numPoints; ++point) + { + float x = -1.0f + static_cast(point % gridSide) * gridScale; + float y = -1.0f + static_cast(point / gridSide) * gridScale; + float dstx; + float dsty; + calculateProjectiveDst(x, y, &dstx, &dsty, goldModel.data()); + + int offset = 2 * (sample * numPoints + point); + srcVec[offset] = x; + srcVec[offset + 1] = y; + dstVec[offset] = dstx; + dstVec[offset + 1] = dsty; + } + } + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreateWithFlags(&stream, cudaStreamNonBlocking)); + ASSERT_EQ(cudaSuccess, cudaMemcpyAsync(srcData->basePtr(), srcVec.data(), srcVec.size() * sizeof(float), + cudaMemcpyHostToDevice, stream)); + ASSERT_EQ(cudaSuccess, cudaMemcpyAsync(dstData->basePtr(), dstVec.data(), dstVec.size() * sizeof(float), + cudaMemcpyHostToDevice, stream)); + + cvcuda::FindHomography fh(numSamples, numPoints); + EXPECT_NO_THROW(fh(stream, srcPoints, dstPoints, models)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + for (int sample = 0; sample < numSamples; ++sample) + { + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(estimatedModelsVec.data() + sample * 9, sizeof(float) * 3, + modelsData->basePtr() + sample * modelsData->stride(0), + modelsData->stride(1), sizeof(float) * 3, 3, cudaMemcpyDeviceToHost)); + } + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + + // GPU reductions and FMA contraction are not bit-exact with this independent CPU projection. + for (int sample = 0; sample < numSamples; ++sample) + { + for (int point = 0; point < numPoints; ++point) + { + int offset = 2 * (sample * numPoints + point); + float projectedX; + float projectedY; + calculateProjectiveDst(srcVec[offset], srcVec[offset + 1], &projectedX, &projectedY, + estimatedModelsVec.data() + sample * 9); + // The 1e-3 tolerance covers GPU reduction/FMA rounding against the independent CPU projection. + EXPECT_NEAR(dstVec[offset], projectedX, 1e-3f); + // The 1e-3 tolerance covers GPU reduction/FMA rounding against the independent CPU projection. + EXPECT_NEAR(dstVec[offset + 1], projectedY, 1e-3f); + } + } + }; + + for (int numPoints : numPointCases) + { + runCase(numPoints); + } +} + TEST_P(OpFindHomography, varshape_correct_output) { int numSamples = GetParamValue<0>(); @@ -256,15 +386,22 @@ TEST_P(OpFindHomography, varshape_correct_output) std::vector numPoints(numSamples); std::vector numXPoints(numSamples); - std::mt19937 rng(12345); - std::uniform_int_distribution dis(0, 100); - std::uniform_int_distribution dis_num_points(4, maxPoints); + std::mt19937 rng(12345); + std::uniform_int_distribution dis(0, 100); + std::uniform_int_distribution dis_num_points(4, maxPoints); auto reqs = nvcv::TensorBatch::CalcRequirements(numSamples); nvcv::TensorBatch srcTensorBatch(reqs); nvcv::TensorBatch dstTensorBatch(reqs); nvcv::TensorBatch modelsTensorBatch(reqs); + // Create the test stream up front so the per-batch H2D uploads below can + // use cudaMemcpyAsync on the same stream the operator will run on. See the + // matching comment in correct_output for why synchronous cudaMemcpy on the + // default stream is unsafe here. + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreateWithFlags(&stream, cudaStreamNonBlocking)); + std::vector> srcVec(numSamples); std::vector> dstVec(numSamples); std::vector modelsVec(numSamples * 9); @@ -285,11 +422,12 @@ TEST_P(OpFindHomography, varshape_correct_output) { int sx = dis(rng); int sy = dis(rng); - srcVec[i].push_back(sx); - srcVec[i].push_back(sy); + srcVec[i].push_back(static_cast(sx)); + srcVec[i].push_back(static_cast(sy)); - float dstx, dsty; - calculateDst(sx, sy, &dstx, &dsty, modelsVec.data() + i * 9); + float dstx; + float dsty; + calculateDst(static_cast(sx), static_cast(sy), &dstx, &dsty, modelsVec.data() + i * 9); dstVec[i].push_back(dstx); dstVec[i].push_back(dsty); } @@ -316,19 +454,16 @@ TEST_P(OpFindHomography, varshape_correct_output) auto srcData = srcPoints.exportData(); auto dstData = dstPoints.exportData(); - ASSERT_EQ(cudaSuccess, cudaMemcpy(srcData->basePtr(), srcVec[i].data(), sizeof(float) * srcVec[i].size(), - cudaMemcpyHostToDevice)); - ASSERT_EQ(cudaSuccess, cudaMemcpy(dstData->basePtr(), dstVec[i].data(), sizeof(float) * dstVec[i].size(), - cudaMemcpyHostToDevice)); + ASSERT_EQ(cudaSuccess, cudaMemcpyAsync(srcData->basePtr(), srcVec[i].data(), sizeof(float) * srcVec[i].size(), + cudaMemcpyHostToDevice, stream)); + ASSERT_EQ(cudaSuccess, cudaMemcpyAsync(dstData->basePtr(), dstVec[i].data(), sizeof(float) * dstVec[i].size(), + cudaMemcpyHostToDevice, stream)); srcTensorBatch.pushBack(srcPoints); dstTensorBatch.pushBack(dstPoints); modelsTensorBatch.pushBack(models); } - cudaStream_t stream; - ASSERT_EQ(cudaSuccess, cudaStreamCreateWithFlags(&stream, cudaStreamNonBlocking)); - cvcuda::FindHomography fh(numSamples, maxNumPoints); #ifdef PERFORMANCE_RUN @@ -375,8 +510,10 @@ TEST_P(OpFindHomography, varshape_correct_output) { for (int j = 0; j < numPoints[i]; j++) { - float dstx, dsty; - float sx, sy; + float dstx; + float dsty; + float sx; + float sy; sx = srcVec[i][2 * j + 0]; sy = srcVec[i][2 * j + 1]; calculateDst(sx, sy, &dstx, &dsty, estimatedModelsVec.data() + i * 9); @@ -384,9 +521,11 @@ TEST_P(OpFindHomography, varshape_correct_output) computedDstVec[i].push_back(dsty); float A = dstVec[i][2 * j + 0]; float B = computedDstVec[i][2 * j + 0]; + // The 1e-3 tolerance covers GPU reduction/FMA rounding against the independent CPU projection. EXPECT_NEAR(A, B, 1e-03); A = dstVec[i][2 * j + 1]; B = computedDstVec[i][2 * j + 1]; + // The 1e-3 tolerance covers GPU reduction/FMA rounding against the independent CPU projection. EXPECT_NEAR(A, B, 1e-03); } } @@ -410,12 +549,7 @@ TEST(OpFindHomography, degenerate_identical_source_points) "NW" }, nvcv::TYPE_2F32); - nvcv::Tensor models( - { - {numSamples, 3, 3}, - "NHW" - }, - nvcv::TYPE_F32); + nvcv::Tensor models = createModelTensor(numSamples); auto srcData = srcPoints.exportData(); auto dstData = dstPoints.exportData(); @@ -437,19 +571,19 @@ TEST(OpFindHomography, degenerate_identical_source_points) srcVec[i * numPoints * 2 + 2 * j + 1] = fixed_src_y; // Different destination points - dstVec[i * numPoints * 2 + 2 * j] = j * 10.0f; - dstVec[i * numPoints * 2 + 2 * j + 1] = j * 15.0f; + dstVec[i * numPoints * 2 + 2 * j] = static_cast(j) * 10.0f; + dstVec[i * numPoints * 2 + 2 * j + 1] = static_cast(j) * 15.0f; } } - ASSERT_EQ(cudaSuccess, cudaMemcpy(srcData->basePtr(), srcVec.data(), sizeof(float) * 2 * numPoints * numSamples, - cudaMemcpyHostToDevice)); - ASSERT_EQ(cudaSuccess, cudaMemcpy(dstData->basePtr(), dstVec.data(), sizeof(float) * 2 * numPoints * numSamples, - cudaMemcpyHostToDevice)); - cudaStream_t stream; ASSERT_EQ(cudaSuccess, cudaStreamCreateWithFlags(&stream, cudaStreamNonBlocking)); + ASSERT_EQ(cudaSuccess, cudaMemcpyAsync(srcData->basePtr(), srcVec.data(), + sizeof(float) * 2 * numPoints * numSamples, cudaMemcpyHostToDevice, stream)); + ASSERT_EQ(cudaSuccess, cudaMemcpyAsync(dstData->basePtr(), dstVec.data(), + sizeof(float) * 2 * numPoints * numSamples, cudaMemcpyHostToDevice, stream)); + cvcuda::FindHomography fh(numSamples, numPoints); EXPECT_NO_THROW(fh(stream, srcPoints, dstPoints, models)); EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); @@ -481,6 +615,17 @@ TEST(OpFindHomography_Negative, createWillNullHandle) EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, cvcudaFindHomographyCreate(nullptr, 8, 16)); } +TEST(OpFindHomography_Negative, createRejectsInvalidBatchOrPointCount) +{ + NVCVOperatorHandle handle = nullptr; + + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, cvcudaFindHomographyCreate(&handle, 0, 4)); + EXPECT_EQ(nullptr, handle); + + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, cvcudaFindHomographyCreate(&handle, 1, 3)); + EXPECT_EQ(nullptr, handle); +} + TEST(OpFindHomography_Negative, varshape_different_batch_size) { int numSamplesSrc = 4; @@ -538,7 +683,8 @@ TEST(OpFindHomography_Negative, varshape_different_batch_size) cvcuda::FindHomography fh(numSamples, maxNumPoints); EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, - nvcv::ProtectCall([&] { fh(stream, srcTensorBatch, dstTensorBatch, modelsTensorBatch); })); + nvcv::ProtectCall([&fh, &stream, &srcTensorBatch, &dstTensorBatch, &modelsTensorBatch] + { fh(stream, srcTensorBatch, dstTensorBatch, modelsTensorBatch); })); EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); @@ -588,9 +734,11 @@ TEST_P(OpFindHomography_Negative, invalid_parameters) cudaStream_t stream; ASSERT_EQ(cudaSuccess, cudaStreamCreateWithFlags(&stream, cudaStreamNonBlocking)); - cvcuda::FindHomography fh(std::max(numSamplesSrc, numSamplesDst), std::max(numPointsSrc, numPointsDst)); + cvcuda::FindHomography fh(std::max(numSamplesSrc, numSamplesDst), + std::max(4, std::max(numPointsSrc, numPointsDst))); - EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&] { fh(stream, srcPoints, dstPoints, models); })); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&fh, &stream, &srcPoints, &dstPoints, &models] + { fh(stream, srcPoints, dstPoints, models); })); EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); diff --git a/tests/cvcuda/system/TestOpFlip.cpp b/tests/cvcuda/system/TestOpFlip.cpp index 49c377c11..9eca7c621 100644 --- a/tests/cvcuda/system/TestOpFlip.cpp +++ b/tests/cvcuda/system/TestOpFlip.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "Definitions.hpp" #include "FlipUtils.hpp" +#include "PlanarParityUtils.hpp" #include #include @@ -28,10 +29,16 @@ #include #include +#include namespace test = nvcv::test; namespace cuda = nvcv::cuda; +static int ScaledSize(int size, double scale) +{ + return static_cast(size * scale); +} + // clang-format off NVCV_TEST_SUITE_P(OpFlip, test::ValueList @@ -42,7 +49,20 @@ NVCV_TEST_SUITE_P(OpFlip, test::ValueList { 123, 33, 3, NVCV_IMAGE_FORMAT_RGB8, -1}, { 42, 53, 4, NVCV_IMAGE_FORMAT_RGBA8, 1}, { 13, 42, 3, NVCV_IMAGE_FORMAT_RGB8, 0}, - { 62, 111, 4, NVCV_IMAGE_FORMAT_RGBA8, -1} + { 62, 111, 4, NVCV_IMAGE_FORMAT_RGBA8, -1}, + // Float3 is intentionally routed through conservative kernels. Cover every flip direction for + // both tensor and var-shape submissions against the independent CPU reference. + { 67, 45, 2, NVCV_IMAGE_FORMAT_RGBf32, 1}, + { 70, 43, 3, NVCV_IMAGE_FORMAT_RGBf32, 0}, + { 65, 41, 2, NVCV_IMAGE_FORMAT_RGBf32, -1}, + // Single-channel cases that exercise the wide (VEC=4) vectorized path and its in-register lane + // reversal: width divisible by 4 (vector body) for each flip code, plus a non-divisible width that + // falls back to the scalar single-channel tail. U8 and U16 cover both VEC lane widths. + { 256, 65, 2, NVCV_IMAGE_FORMAT_U8, 1}, // wide horizontal (lane reversal) + { 256, 64, 2, NVCV_IMAGE_FORMAT_U8, -1}, // wide both (lane reversal) + { 256, 48, 3, NVCV_IMAGE_FORMAT_U8, 0}, // wide vertical (direct copy) + { 128, 40, 2, NVCV_IMAGE_FORMAT_U16, -1}, // wide both, 16-bit lanes + { 255, 40, 2, NVCV_IMAGE_FORMAT_U8, -1} // scalar tail (width % 4 != 0) }); // clang-format on @@ -80,8 +100,8 @@ TEST_P(OpFlip, correct_output) long inSampleStride = inAccess->numRows() * inAccess->rowStride(); long outSampleStride = outAccess->numRows() * outAccess->rowStride(); - int inBufSize = inSampleStride * inAccess->numSamples(); - int outBufSize = outSampleStride * outAccess->numSamples(); + auto inBufSize = static_cast(inSampleStride * inAccess->numSamples()); + auto outBufSize = static_cast(outSampleStride * outAccess->numSamples()); long3 inStrides{inSampleStride, inAccess->rowStride(), inAccess->colStride()}; long3 outStrides{outSampleStride, outAccess->rowStride(), outAccess->colStride()}; @@ -91,7 +111,7 @@ TEST_P(OpFlip, correct_output) std::default_random_engine randEng(0); std::uniform_int_distribution rand(0u, 255u); - std::generate(inVec.begin(), inVec.end(), [&]() { return rand(randEng); }); + std::ranges::generate(inVec, [&rand, &randEng]() { return rand(randEng); }); std::vector goldVec(outBufSize); test::FlipCPU(goldVec, outStrides, inVec, inStrides, shape, format, flipCode); @@ -126,9 +146,9 @@ TEST_P(OpFlip, varshape_correct_output) int flipCode = GetParamValue<4>(); // Create input varshape - std::default_random_engine rng; - std::uniform_int_distribution udistWidth(width * 0.8, width * 1.1); - std::uniform_int_distribution udistHeight(height * 0.8, height * 1.1); + std::default_random_engine rng; + std::uniform_int_distribution udistWidth(ScaledSize(width, 0.8), ScaledSize(width, 1.1)); + std::uniform_int_distribution udistHeight(ScaledSize(height, 0.8), ScaledSize(height, 1.1)); std::vector imgSrc; @@ -145,7 +165,7 @@ TEST_P(OpFlip, varshape_correct_output) std::uniform_int_distribution udist(0, 255); srcVec[i].resize(imgSrc[i].size().h * srcRowStride); - std::generate(srcVec[i].begin(), srcVec[i].end(), [&]() { return udist(rng); }); + std::ranges::generate(srcVec[i], [&udist, &rng]() { return udist(rng); }); auto imgData = imgSrc[i].exportData(); ASSERT_NE(imgData, nvcv::NullOpt); @@ -219,11 +239,92 @@ TEST_P(OpFlip, varshape_correct_output) } } +// ============================================================================= +// Planar (NCHW/CHW) layout support +// +// Flip remaps each pixel independently of its channel, so a planar input is flipped plane-by-plane +// and must produce exactly the same pixels as the interleaved path. These tests feed identical data +// in both layouts through cvcuda::Flip and require the (re-interleaved) planar output to match the +// interleaved output bit-for-bit, for every dtype and flip code. +// ============================================================================= + +namespace { + +// Flip identical data in interleaved and planar tensor layout; outputs must match bit-for-bit. +// The shared scaffolding (upload/run/download/compare) lives in PlanarParityUtils.hpp; here we only +// bind the Flip call. +void RunPlanarParityTensorCase(nvcv::ImageFormat planarFmt, nvcv::ImageFormat interleavedFmt, int w, int h, + int flipCode, int numImages) +{ + test::planar::RunTensorParity( + planarFmt, interleavedFmt, w, h, w, h, numImages, + [numImages, flipCode](cudaStream_t s, const nvcv::Tensor &src, const nvcv::Tensor &dst, nvcv::ImageFormat) + { + cvcuda::Flip op(numImages); + EXPECT_NO_THROW(op(s, src, dst, flipCode)); + }); +} + +// Var-shape counterpart of RunPlanarParityTensorCase. +void RunPlanarParityVarShapeCase(nvcv::ImageFormat planarFmt, nvcv::ImageFormat interleavedFmt, int w, int h, + int flipCode, int numImages) +{ + // Var-shape Flip takes the flip code as a per-image tensor; upload it once (synchronously, so it + // is ready before the operator runs on the parity helper's stream). + nvcv::Tensor flip_code({{numImages}, "N"}, nvcv::TYPE_S32); + { + auto dev = flip_code.exportData(); + ASSERT_NE(dev, nullptr); + std::vector vec(numImages, flipCode); + ASSERT_EQ(cudaSuccess, + cudaMemcpy(dev->basePtr(), vec.data(), vec.size() * sizeof(int), cudaMemcpyHostToDevice)); + } + + test::planar::RunVarShapeParity(planarFmt, interleavedFmt, w, h, w, h, numImages, + [numImages, &flip_code](cudaStream_t s, const nvcv::ImageBatchVarShape &src, + const nvcv::ImageBatchVarShape &dst, nvcv::ImageFormat) + { + cvcuda::Flip op(numImages); + EXPECT_NO_THROW(op(s, src, dst, flip_code)); + }); +} + +} // namespace + +// Parameters: width, height, flipCode, numImages, planarFmt, interleavedFmt +// clang-format off +NVCV_TEST_SUITE_P(OpFlipPlanar, + test::ValueList{ + {176, 113, 1, 2, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, // RGB8, horizontal + {123, 66, 0, 2, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, // RGB8, vertical + { 64, 48, -1, 1, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, // RGB8, both + { 50, 40, 1, 2, nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8}, // RGBA8, horizontal + {100, 80, -1, 1, nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8}, // RGBA8, both + { 64, 48, 0, 2, nvcv::FMT_RGBAf32p, nvcv::FMT_RGBAf32}, // float planar + { 67, 45, 1, 2, nvcv::FMT_RGBf32p, nvcv::FMT_RGBf32}, // RGB float3, horizontal + { 70, 43, 0, 2, nvcv::FMT_RGBf32p, nvcv::FMT_RGBf32}, // RGB float3, vertical + { 65, 41, -1, 2, nvcv::FMT_RGBf32p, nvcv::FMT_RGBf32}, // RGB float3, both +}); + +// clang-format on + +TEST_P(OpFlipPlanar, tensor_matches_interleaved) +{ + RunPlanarParityTensorCase(GetParamValue<4>(), GetParamValue<5>(), GetParamValue<0>(), GetParamValue<1>(), + GetParamValue<2>(), GetParamValue<3>()); +} + +TEST_P(OpFlipPlanar, varshape_matches_interleaved) +{ + RunPlanarParityVarShapeCase(GetParamValue<4>(), GetParamValue<5>(), GetParamValue<0>(), GetParamValue<1>(), + GetParamValue<2>(), GetParamValue<3>()); +} + // clang-format off NVCV_TEST_SUITE_P(OpFlip_Negative, nvcv::test::ValueList{ {nvcv::FMT_RGB8, nvcv::FMT_RGBf32}, // data type is different - {nvcv::FMT_RGB8, nvcv::FMT_RGB8p}, // data format is different - {nvcv::FMT_RGB8p, nvcv::FMT_RGB8p}, // data format is not kNHWC/kHWC + {nvcv::FMT_RGB8, nvcv::FMT_RGB8p}, // data format is different (interleaved in, planar out) + {nvcv::FMT_2S16, nvcv::FMT_2S16}, // unsupported two-channel format {nvcv::FMT_F16, nvcv::FMT_F16}, // invalid data type, }); @@ -243,7 +344,8 @@ TEST_P(OpFlip_Negative, op) // run operator cvcuda::Flip flipOp; - EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&] { flipOp(stream, inTensor, outTensor, flipCode); })); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&flipOp, &stream, &inTensor, &outTensor, &flipCode] + { flipOp(stream, inTensor, outTensor, flipCode); })); ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); @@ -262,9 +364,9 @@ TEST_P(OpFlip_Negative, varshape_op) int batches = 3; int flipCode = 0; - std::default_random_engine rng; - std::uniform_int_distribution udistWidth(width * 0.8, width * 1.1); - std::uniform_int_distribution udistHeight(height * 0.8, height * 1.1); + std::default_random_engine rng; + std::uniform_int_distribution udistWidth(ScaledSize(width, 0.8), ScaledSize(width, 1.1)); + std::uniform_int_distribution udistHeight(ScaledSize(height, 0.8), ScaledSize(height, 1.1)); std::vector imgSrc; for (int i = 0; i < batches; ++i) @@ -298,7 +400,8 @@ TEST_P(OpFlip_Negative, varshape_op) // Run operator cvcuda::Flip flipOp(batches); - EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&] { flipOp(stream, batchSrc, batchDst, flip_code); })); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&flipOp, &stream, &batchSrc, &batchDst, &flip_code] + { flipOp(stream, batchSrc, batchDst, flip_code); })); } TEST(OpFlip_Negative, varshape_hasDifferentFormat) @@ -312,20 +415,17 @@ TEST(OpFlip_Negative, varshape_hasDifferentFormat) int height = 24; int batches = 3; - std::default_random_engine rng; - std::uniform_int_distribution udistWidth(width * 0.8, width * 1.1); - std::uniform_int_distribution udistHeight(height * 0.8, height * 1.1); + std::default_random_engine rng; + std::uniform_int_distribution udistWidth(ScaledSize(width, 0.8), ScaledSize(width, 1.1)); + std::uniform_int_distribution udistHeight(ScaledSize(height, 0.8), ScaledSize(height, 1.1)); std::vector> testSet{ {nvcv::FMT_U8, fmt}, { fmt, nvcv::FMT_U8} }; - for (auto testCase : testSet) + for (const auto &[inputFmtExtra, outputFmtExtra] : testSet) { - nvcv::ImageFormat inputFmtExtra = std::get<0>(testCase); - nvcv::ImageFormat outputFmtExtra = std::get<1>(testCase); - std::vector imgSrc; for (int i = 0; i < batches - 1; ++i) { @@ -360,8 +460,8 @@ TEST(OpFlip_Negative, varshape_hasDifferentFormat) // Run operator cvcuda::Flip flipOp(batches); - EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, - nvcv::ProtectCall([&] { flipOp(stream, batchSrc, batchDst, flip_code); })); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&flipOp, &stream, &batchSrc, &batchDst, &flip_code] + { flipOp(stream, batchSrc, batchDst, flip_code); })); } } diff --git a/tests/cvcuda/system/TestOpGammaContrast.cpp b/tests/cvcuda/system/TestOpGammaContrast.cpp index b9a408596..7de7dd52c 100644 --- a/tests/cvcuda/system/TestOpGammaContrast.cpp +++ b/tests/cvcuda/system/TestOpGammaContrast.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "ConvUtils.hpp" #include "Definitions.hpp" +#include "PlanarParityUtils.hpp" #include #include @@ -32,28 +33,47 @@ namespace test = nvcv::test; namespace cuda = nvcv::cuda; -#define DBG_GAMMA_CONTRAST 0 +constexpr bool DBG_GAMMA_CONTRAST = false; -static void printVec(std::vector &vec, int height, int rowPitch, int bytesPerPixel, std::string name) +static int ScaledSize(int size, double scale) { -#if DBG_GAMMA_CONTRAST - for (int i = 0; i < bytesPerPixel; i++) + return static_cast(size * scale); +} + +static void printChannelRow(const std::vector &vec, int row, int rowPitch, int bytesPerPixel, int channel) +{ + for (int col = 0; col < rowPitch / bytesPerPixel; col++) { - std::cout << "\nPrint " << name << " for channel: " << i << std::endl; + printf("%4d, ", static_cast(vec[row * rowPitch + col * bytesPerPixel + channel])); + } + std::cout << std::endl; +} - for (int k = 0; k < height; k++) +static void printChannel(const std::vector &vec, int height, int rowPitch, int bytesPerPixel, int channel, + const std::string &name) +{ + std::cout << "\nPrint " << name << " for channel: " << channel << std::endl; + + for (int row = 0; row < height; row++) + { + printChannelRow(vec, row, rowPitch, bytesPerPixel, channel); + } +} + +static void printVec(const std::vector &vec, int height, int rowPitch, int bytesPerPixel, + const std::string &name) +{ + if constexpr (DBG_GAMMA_CONTRAST) + { + for (int channel = 0; channel < bytesPerPixel; channel++) { - for (int j = 0; j < static_cast(rowPitch / bytesPerPixel); j++) - { - printf("%4d, ", static_cast(vec[k * rowPitch + j * bytesPerPixel + i])); - } - std::cout << std::endl; + printChannel(vec, height, rowPitch, bytesPerPixel, channel, name); } + std::cout << std::endl; } - std::cout << std::endl; -#endif } +// The tolerance accounts for the host std::pow/std::rint reference versus device __powf/SaturateCast rounding. #define VEC_EXPECT_NEAR(vec1, vec2, delta, dtype) \ ASSERT_EQ(vec1.size(), vec2.size()); \ for (std::size_t idx = 0; idx < vec1.size() / sizeof(dtype); ++idx) \ @@ -67,9 +87,8 @@ namespace { // uint8 cpu op template void GammaContrastVarShapeCpuOp(std::vector &hDst, int dstRowStride, nvcv::Size2D dstSize, - const std::vector &hSrc, int srcRowStride, nvcv::Size2D srcSize, - nvcv::ImageFormat fmt, const std::vector gamma, const int imageIndex, - bool perChannel) + const std::vector &hSrc, int, nvcv::Size2D, nvcv::ImageFormat fmt, + const std::vector &gamma, const int imageIndex, bool perChannel) { assert(fmt.numPlanes() == 1); @@ -87,7 +106,7 @@ void GammaContrastVarShapeCpuOp(std::vector &hDst, int dstRowStride, nvcv::Si int index = dst_y * dstRowStride + dst_x * elementsPerPixel + k; float gamma_tmp = perChannel ? gamma[imageIndex * elementsPerPixel + k] : gamma[imageIndex]; float tmp = (srcPtr[index] + 0.0f) / 255.0f; - T out = std::rint(pow(tmp, gamma_tmp) * 255.0f); + auto out = static_cast(std::rint(pow(tmp, gamma_tmp) * 255.0f)); dstPtr[index] = out; } } @@ -97,9 +116,8 @@ void GammaContrastVarShapeCpuOp(std::vector &hDst, int dstRowStride, nvcv::Si // float cpu op template<> void GammaContrastVarShapeCpuOp(std::vector &hDst, int dstRowStride, nvcv::Size2D dstSize, - const std::vector &hSrc, int srcRowStride, nvcv::Size2D srcSize, - nvcv::ImageFormat fmt, const std::vector gamma, const int imageIndex, - bool perChannel) + const std::vector &hSrc, int, nvcv::Size2D, nvcv::ImageFormat fmt, + const std::vector &gamma, const int imageIndex, bool perChannel) { assert(fmt.numPlanes() == 1); @@ -122,7 +140,7 @@ void GammaContrastVarShapeCpuOp(std::vector &hDst, int dstRowStride, nvcv void GammaContrastVarShapeCpuOpWrapper(std::vector &hDst, int dstRowStride, nvcv::Size2D dstSize, const std::vector &hSrc, int srcRowStride, nvcv::Size2D srcSize, - nvcv::ImageFormat fmt, const std::vector gamma, const int imageIndex, + nvcv::ImageFormat fmt, const std::vector &gamma, const int imageIndex, bool perChannel, NVCVDataType nvcvDataType) { if (nvcvDataType == NVCV_DATA_TYPE_F32 || nvcvDataType == NVCV_DATA_TYPE_2F32 || nvcvDataType == NVCV_DATA_TYPE_3F32 @@ -131,8 +149,7 @@ void GammaContrastVarShapeCpuOpWrapper(std::vector &hDst, int dstRowStr std::vector src_tmp(hSrc.size() / sizeof(float)); std::vector dst_tmp(hDst.size() / sizeof(float)); size_t copySize = hSrc.size(); - memcpy(static_cast(src_tmp.data()), const_cast(static_cast(hSrc.data())), - copySize); + memcpy(static_cast(src_tmp.data()), static_cast(hSrc.data()), copySize); memcpy(static_cast(dst_tmp.data()), static_cast(hDst.data()), copySize); GammaContrastVarShapeCpuOp(dst_tmp, dstRowStride / sizeof(float), dstSize, src_tmp, srcRowStride / sizeof(float), srcSize, fmt, gamma, imageIndex, perChannel); @@ -154,6 +171,7 @@ NVCV_TEST_SUITE_P(OpGammaContrast, test::ValueList()}; NVCVDataType nvcvDataType; - ASSERT_EQ(NVCV_SUCCESS, nvcvImageFormatGetPlaneDataType(format, 0, &nvcvDataType)); + ASSERT_EQ(NVCV_SUCCESS, nvcvImageFormatGetPlaneDataType(static_cast(format), 0, &nvcvDataType)); float gamma = GetParamValue<4>(); bool isFloatTest = false; @@ -203,9 +221,9 @@ TEST_P(OpGammaContrast, varshape_correct_output) // Create input varshape std::default_random_engine rng; - std::uniform_int_distribution udistWidth(width * 0.8, width * 1.1); - std::uniform_int_distribution udistHeight(height * 0.8, height * 1.1); - std::uniform_real_distribution udistGamma(gamma * 0.8, 1.0); + std::uniform_int_distribution udistWidth(ScaledSize(width, 0.8), ScaledSize(width, 1.1)); + std::uniform_int_distribution udistHeight(ScaledSize(height, 0.8), ScaledSize(height, 1.1)); + std::uniform_real_distribution udistGamma(gamma * 0.8f, 1.f); std::vector imgSrc; @@ -236,7 +254,7 @@ TEST_P(OpGammaContrast, varshape_correct_output) } break; default: - std::generate(srcVec[i].begin(), srcVec[i].end(), [&]() { return udist(rng); }); + std::ranges::generate(srcVec[i], [&udist, &rng]() { return udist(rng); }); break; } @@ -273,9 +291,9 @@ TEST_P(OpGammaContrast, varshape_correct_output) { gammaVec.resize(batches); } - std::generate(gammaVec.begin(), gammaVec.end(), [&]() { return udistGamma(rng); }); + std::ranges::generate(gammaVec, [&udistGamma, &rng]() { return udistGamma(rng); }); - int nElements = gammaVec.size(); + auto nElements = static_cast(gammaVec.size()); nvcv::Tensor gammaTensor({{nElements}, "N"}, nvcv::TYPE_F32); { auto dev = gammaTensor.exportData(); @@ -326,7 +344,7 @@ TEST_P(OpGammaContrast, varshape_correct_output) dstHeight, cudaMemcpyDeviceToHost)); std::vector goldVec(dstHeight * dstRowStride); - std::generate(goldVec.begin(), goldVec.end(), [&]() { return 0; }); + std::ranges::generate(goldVec, []() { return 0; }); // Generate gold result GammaContrastVarShapeCpuOpWrapper(goldVec, dstRowStride, {dstWidth, dstHeight}, srcVec[i], srcRowStride, @@ -349,20 +367,19 @@ TEST_P(OpGammaContrast, varshape_correct_output) // clang-format off -NVCV_TEST_SUITE_P(OpGammaContrastVarshape_negative, test::ValueList +NVCV_TEST_SUITE_P(OpGammaContrast_Negative, test::ValueList { // batches, inFmt, outFmt {6, nvcv::FMT_U8, nvcv::FMT_U8}, // larger than max batches {2, nvcv::FMT_RGBA8, nvcv::FMT_RGBA8}, // larger than max channels - {2, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, // different format - {2, nvcv::FMT_RGB8p, nvcv::FMT_RGB8p}, + {2, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, // input/output format mismatch (planar in, interleaved out) {2, nvcv::FMT_RGBf16, nvcv::FMT_RGBf16}, {2, nvcv::FMT_U8, nvcv::FMT_S8}, }); // clang-format on -TEST_P(OpGammaContrastVarshape_negative, op) +TEST_P(OpGammaContrast_Negative, op) { cudaStream_t stream; ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); @@ -377,10 +394,10 @@ TEST_P(OpGammaContrastVarshape_negative, op) int maxChannels = 3; // Create input varshape - std::default_random_engine rng; - std::uniform_int_distribution udistWidth(width * 0.8, width * 1.1); - std::uniform_int_distribution udistHeight(height * 0.8, height * 1.1); - std::vector imgSrc; + std::default_random_engine rng; + std::uniform_int_distribution udistWidth(ScaledSize(width, 0.8), ScaledSize(width, 1.1)); + std::uniform_int_distribution udistHeight(ScaledSize(height, 0.8), ScaledSize(height, 1.1)); + std::vector imgSrc; for (int i = 0; i < batches; ++i) { @@ -404,13 +421,14 @@ TEST_P(OpGammaContrastVarshape_negative, op) cvcuda::GammaContrast gammacontrastOp(maxBatches, maxChannels); EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, - nvcv::ProtectCall([&] { gammacontrastOp(stream, batchSrc, batchDst, gammaTensor); })); + nvcv::ProtectCall([&gammacontrastOp, &stream, &batchSrc, &batchDst, &gammaTensor] + { gammacontrastOp(stream, batchSrc, batchDst, gammaTensor); })); ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); } -TEST(OpGammaContrastVarshape_negative, varshape_hasDifferentFormat) +TEST(OpGammaContrast_Negative, varshape_hasDifferentFormat) { cudaStream_t stream; EXPECT_EQ(cudaSuccess, cudaStreamCreate(&stream)); @@ -428,19 +446,18 @@ TEST(OpGammaContrastVarshape_negative, varshape_hasDifferentFormat) { nvcv::FMT_RGB8, nvcv::FMT_RGBA8} }; - for (auto testCase : testSet) + for (const auto &[inputFmtExtra, outputFmtExtra] : testSet) { - nvcv::ImageFormat inputFmtExtra = std::get<0>(testCase); - nvcv::ImageFormat outputFmtExtra = std::get<1>(testCase); - // Create input and output - std::default_random_engine randEng; - std::uniform_int_distribution rndSrcWidth(srcWidthBase * 0.8, srcWidthBase * 1.1); - std::uniform_int_distribution rndSrcHeight(srcHeightBase * 0.8, srcHeightBase * 1.1); + std::default_random_engine randEng; + std::uniform_int_distribution rndSrcWidth(ScaledSize(srcWidthBase, 0.8), ScaledSize(srcWidthBase, 1.1)); + std::uniform_int_distribution rndSrcHeight(ScaledSize(srcHeightBase, 0.8), ScaledSize(srcHeightBase, 1.1)); nvcv::Tensor gammaTensor({{batches}, "N"}, nvcv::TYPE_F32); // not per channel - std::vector imgSrc, imgDst; + std::vector imgSrc; + + std::vector imgDst; for (int i = 0; i < batches - 1; ++i) { @@ -462,16 +479,738 @@ TEST(OpGammaContrastVarshape_negative, varshape_hasDifferentFormat) // Run operator cvcuda::GammaContrast gammacontrastOp(batches, maxChannels); EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, - nvcv::ProtectCall([&] { gammacontrastOp(stream, batchSrc, batchDst, gammaTensor); })); + nvcv::ProtectCall([&gammacontrastOp, &stream, &batchSrc, &batchDst, &gammaTensor] + { gammacontrastOp(stream, batchSrc, batchDst, gammaTensor); })); } EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); } -TEST(OpGammaContrastVarshape_negative, create_with_null_handle) +TEST(OpGammaContrast_Negative, gamma_length_must_match_batch_or_batch_channels) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + const int batches = 3; + const int channels = 3; + const int width = 8; + const int height = 8; + const int gammaLength = batches + 1; + + std::vector imgSrc; + std::vector imgDst; + + for (int i = 0; i < batches; ++i) + { + imgSrc.emplace_back(nvcv::Size2D{width, height}, nvcv::FMT_RGB8); + imgDst.emplace_back(nvcv::Size2D{width, height}, nvcv::FMT_RGB8); + } + + nvcv::ImageBatchVarShape batchSrc(batches); + batchSrc.pushBack(imgSrc.begin(), imgSrc.end()); + + nvcv::ImageBatchVarShape batchDst(batches); + batchDst.pushBack(imgDst.begin(), imgDst.end()); + + nvcv::Tensor gammaTensor({{gammaLength}, "N"}, nvcv::TYPE_F32); + cvcuda::GammaContrast gammacontrastOp(batches, channels); + + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&] { gammacontrastOp(stream, batchSrc, batchDst, gammaTensor); })); + + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpGammaContrast_Negative, input_output_data_types_and_channels_must_match) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + const int batches = 2; + const int width = 8; + const int height = 8; + + const std::array, 2> testCases{ + {{nvcv::FMT_U16, nvcv::FMT_F32, 1, "dtype"}, {nvcv::FMT_RGB8, nvcv::FMT_RGBA8, 4, "channels"}} + }; + + for (auto [inputFormat, outputFormat, maxChannels, name] : testCases) + { + SCOPED_TRACE(name); + + std::vector imgSrc; + std::vector imgDst; + + for (int i = 0; i < batches; ++i) + { + imgSrc.emplace_back(nvcv::Size2D{width, height}, inputFormat); + imgDst.emplace_back(nvcv::Size2D{width, height}, outputFormat); + } + + nvcv::ImageBatchVarShape batchSrc(batches); + batchSrc.pushBack(imgSrc.begin(), imgSrc.end()); + + nvcv::ImageBatchVarShape batchDst(batches); + batchDst.pushBack(imgDst.begin(), imgDst.end()); + + nvcv::Tensor gammaTensor({{batches}, "N"}, nvcv::TYPE_F32); + cvcuda::GammaContrast gammacontrastOp(batches, maxChannels); + + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&] { gammacontrastOp(stream, batchSrc, batchDst, gammaTensor); })); + } + + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +namespace { + +template +void TestTensorInputOutputShapeMismatches(Submit &&submit) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + constexpr int samples = 2; + constexpr int width = 8; + constexpr int height = 8; + + nvcv::Tensor input = nvcv::util::CreateTensor(samples, width, height, nvcv::FMT_RGB8); + + struct OutputShape + { + int samples; + int width; + int height; + const char *name; + }; + + const std::array outputShapes{ + {{samples - 1, width, height, "samples"}, + {samples, width - 1, height, "width"}, + {samples, width, height - 1, "height"}} + }; + + for (const auto &outputShape : outputShapes) + { + SCOPED_TRACE(outputShape.name); + nvcv::Tensor output + = nvcv::util::CreateTensor(outputShape.samples, outputShape.width, outputShape.height, nvcv::FMT_RGB8); + + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&] { submit(stream, input, output); })); + } + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +} // namespace + +TEST(OpGammaContrast_Negative, tensor_input_output_shapes_must_match) +{ + constexpr int samples = 2; + nvcv::Tensor gammaTensor({{samples}, "N"}, nvcv::TYPE_F32); + cvcuda::GammaContrast gammacontrastOp(samples, 3); + + TestTensorInputOutputShapeMismatches( + [&gammacontrastOp, &gammaTensor](cudaStream_t stream, const nvcv::Tensor &input, const nvcv::Tensor &output) + { gammacontrastOp(stream, input, output, gammaTensor); }); +} + +// The scalar (host-float) overload must reject sample/width/height mismatches too -- grid sizing is +// derived from the source access, so a smaller output would otherwise drive out-of-bounds writes. +TEST(OpGammaContrast_Negative, scalar_tensor_input_output_shapes_must_match) +{ + constexpr int samples = 2; + constexpr float gamma = 1.0f; + constexpr float gain = 1.0f; + cvcuda::GammaContrast gammacontrastOp(samples, 3); + + TestTensorInputOutputShapeMismatches( + [&gammacontrastOp](cudaStream_t stream, const nvcv::Tensor &input, const nvcv::Tensor &output) + { gammacontrastOp(stream, input, output, gamma, gain); }); +} + +TEST(OpGammaContrast_Negative, varshape_input_output_shapes_must_match) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + constexpr int samples = 2; + constexpr int width = 8; + constexpr int height = 8; + + nvcv::ImageBatchVarShape input(samples); + for (int i = 0; i < samples; ++i) + { + input.pushBack(nvcv::Image({width, height}, nvcv::FMT_RGB8)); + } + + nvcv::Tensor gammaTensor({{samples}, "N"}, nvcv::TYPE_F32); + cvcuda::GammaContrast gammacontrastOp(samples, 3); + + const std::array, const char *>, 3> outputShapes{ + {{{{width, height}}, "samples"}, + {{{width, height}, {width - 1, height}}, "width"}, + {{{width, height}, {width, height - 1}}, "height"}} + }; + + for (const auto &[sizes, name] : outputShapes) + { + SCOPED_TRACE(name); + nvcv::ImageBatchVarShape output(static_cast(sizes.size())); + for (nvcv::Size2D size : sizes) + { + output.pushBack(nvcv::Image(size, nvcv::FMT_RGB8)); + } + + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&] { gammacontrastOp(stream, input, output, gammaTensor); })); + } + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpGammaContrast_Negative, create_with_null_handle) { EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, cvcudaGammaContrastCreate(nullptr, 4, 4)); } +TEST(OpGammaContrast_Negative, create_rejects_non_positive_max_batch_or_channels) +{ + const std::vector> invalidLimits = { + { 0, 4}, + {-1, 4}, + { 4, 0}, + { 4, -1} + }; + + for (auto [maxBatchSize, maxChannelCount] : invalidLimits) + { + NVCVOperatorHandle handle = nullptr; + + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, cvcudaGammaContrastCreate(&handle, maxBatchSize, maxChannelCount)) + << "maxBatchSize=" << maxBatchSize << ", maxChannelCount=" << maxChannelCount; + EXPECT_EQ(nullptr, handle); + + if (handle != nullptr) + { + nvcvOperatorDestroy(handle); + } + } +} + #undef VEC_EXPECT_NEAR + +// --------------------------------------------------------------------------- +// Planar (NCHW/CHW) layout support +// +// Gamma contrast applies x**gamma (or 255*(x/255)**gamma) per pixel per channel, independently of the +// other channels, so a planar input is processed plane-by-plane and the per-channel gamma maps to the +// same value the interleaved kernel reads. These tests run identical data + the same gamma tensor in +// both layouts through cvcuda::GammaContrast and require the (re-interleaved) planar output to match +// the interleaved output bit-for-bit. Scaffolding lives in PlanarParityUtils.hpp. +namespace { + +float DeterministicGammaValue(size_t idx, float minValue, float maxValue, int salt) +{ + const int bucket = (static_cast(idx) * 37 + salt) % 101; + return minValue + (maxValue - minValue) * static_cast(bucket) / 100.f; +} + +uint8_t DeterministicByteValue(size_t idx, int sample) +{ + return static_cast((static_cast(idx) * 13 + sample * 29 + 17) % 256); +} + +float DeterministicUnitValue(size_t idx, int sample) +{ + const int bucket = (static_cast(idx) * 17 + sample * 31 + 11) % 100; + return static_cast(bucket) / 99.f; +} + +struct TensorTestData +{ + NVCVDataType dataType; + bool isFloat; + int rowStride; + size_t sampleBytes; + std::vector> hostSamples; +}; + +void UploadDeterministicTensorInput(nvcv::Tensor &tensor, int samples, int width, int height, nvcv::ImageFormat format, + TensorTestData &testData) +{ + auto tensorData = tensor.exportData(); + ASSERT_NE(tensorData, nullptr); + auto tensorAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*tensorData); + ASSERT_TRUE(tensorAccess); + + ASSERT_EQ(NVCV_SUCCESS, + nvcvImageFormatGetPlaneDataType(static_cast(format), 0, &testData.dataType)); + testData.isFloat = testData.dataType == NVCV_DATA_TYPE_F32 || testData.dataType == NVCV_DATA_TYPE_2F32 + || testData.dataType == NVCV_DATA_TYPE_3F32 || testData.dataType == NVCV_DATA_TYPE_4F32; + const int bytesPerElement = format.planePixelStrideBytes(0) / format.numChannels(); + testData.rowStride = width * format.numChannels() * bytesPerElement; + testData.sampleBytes = static_cast(testData.rowStride) * height; + testData.hostSamples.assign(samples, std::vector(testData.sampleBytes)); + + for (int sample = 0; sample < samples; ++sample) + { + auto &hostSample = testData.hostSamples[sample]; + if (testData.isFloat) + { + auto *values = reinterpret_cast(hostSample.data()); + for (size_t i = 0; i < testData.sampleBytes / sizeof(float); ++i) + { + values[i] = DeterministicUnitValue(i, sample); + } + } + else + { + for (size_t i = 0; i < hostSample.size(); ++i) + { + hostSample[i] = DeterministicByteValue(i, sample); + } + } + + ASSERT_EQ(cudaSuccess, + cudaMemcpy2D(tensorAccess->sampleData(sample), tensorAccess->rowStride(), hostSample.data(), + testData.rowStride, testData.rowStride, height, cudaMemcpyHostToDevice)); + } +} + +void DownloadTensorSample(const nvcv::Tensor &tensor, int sample, int rowStride, int height, + std::vector &hostSample) +{ + auto tensorData = tensor.exportData(); + ASSERT_NE(tensorData, nullptr); + auto tensorAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*tensorData); + ASSERT_TRUE(tensorAccess); + + hostSample.resize(static_cast(rowStride) * height); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(hostSample.data(), rowStride, tensorAccess->sampleData(sample), + tensorAccess->rowStride(), rowStride, height, cudaMemcpyDeviceToHost)); +} + +void UploadGammaTensor(nvcv::Tensor &tensor, const std::vector &values) +{ + auto tensorData = tensor.exportData(); + ASSERT_NE(tensorData, nullptr); + ASSERT_EQ(cudaSuccess, + cudaMemcpy(tensorData->basePtr(), values.data(), values.size() * sizeof(float), cudaMemcpyHostToDevice)); +} + +void ExpectTensorNear(const std::vector &got, const std::vector &gold, bool isFloat) +{ + ASSERT_EQ(got.size(), gold.size()); + if (isFloat) + { + const auto *actual = reinterpret_cast(got.data()); + const auto *expected = reinterpret_cast(gold.data()); + // Host std::pow and device __powf have different precision; this bounds the expected approximation error. + for (size_t i = 0; i < got.size() / sizeof(float); ++i) + { + EXPECT_NEAR(actual[i], expected[i], 1e-4f) << "at " << i; + } + } + else + { + // Integer results can differ by one when host and device power approximations reach the cast boundary. + for (size_t i = 0; i < got.size(); ++i) + { + EXPECT_NEAR(static_cast(got[i]), static_cast(gold[i]), 1) << "at " << i; + } + } +} + +void RunGammaContrastPlanarParityCase(nvcv::ImageFormat planarFmt, nvcv::ImageFormat interleavedFmt, int w, int h, + int numImages, bool perChannel) +{ + const int channels = interleavedFmt.numChannels(); + const int nElements = perChannel ? numImages * channels : numImages; + + // Build the gamma tensor once (same instance feeds both layouts). + std::vector gammaVec(nElements); + for (int i = 0; i < nElements; ++i) + { + gammaVec[i] = DeterministicGammaValue(i, 0.5f, 2.0f, 23); + } + + nvcv::Tensor gammaTensor({{nElements}, "N"}, nvcv::TYPE_F32); + ASSERT_NO_FATAL_FAILURE(UploadGammaTensor(gammaTensor, gammaVec)); + + test::planar::RunVarShapeParity( + planarFmt, interleavedFmt, w, h, w, h, numImages, + [numImages, channels, &gammaTensor](cudaStream_t s, const nvcv::ImageBatchVarShape &src, + const nvcv::ImageBatchVarShape &dst, nvcv::ImageFormat) + { + cvcuda::GammaContrast op(numImages, channels); + EXPECT_NO_THROW(op(s, src, dst, gammaTensor)); + }); +} + +} // namespace + +// Parameters: width, height, numImages, perChannelGamma, planarFmt, interleavedFmt +// clang-format off +NVCV_TEST_SUITE_P(OpGammaContrastPlanar, + test::ValueList{ + {176, 113, 2, false, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, // RGB8, per-image gamma + {123, 66, 2, true, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, // RGB8, per-channel gamma + { 64, 48, 1, true, nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8}, // RGBA8, per-channel gamma + { 50, 40, 2, false, nvcv::FMT_RGBf32p, nvcv::FMT_RGBf32}, // RGBf32, per-image gamma + { 72, 54, 2, true, nvcv::FMT_RGBAf32p, nvcv::FMT_RGBAf32}, // RGBAf32, per-channel gamma +}); + +// clang-format on + +TEST_P(OpGammaContrastPlanar, varshape_matches_interleaved) +{ + RunGammaContrastPlanarParityCase(GetParamValue<4>(), GetParamValue<5>(), GetParamValue<0>(), GetParamValue<1>(), + GetParamValue<2>(), GetParamValue<3>()); +} + +// --------------------------------------------------------------------------- +// Tensor variant (interleaved + planar) +namespace { + +// Tensor planar (NCHW/CHW) parity: planar tensor output must match interleaved tensor output bit-for- +// bit for the same data + gamma. Validates the planar tensor kernel against the interleaved one. +void RunGammaContrastTensorPlanarParityCase(nvcv::ImageFormat planarFmt, nvcv::ImageFormat interleavedFmt, int w, int h, + int numSamples, bool perChannel) +{ + const int channels = interleavedFmt.numChannels(); + const int nElements = perChannel ? numSamples * channels : numSamples; + + std::vector gammaVec(nElements); + for (int i = 0; i < nElements; ++i) + { + gammaVec[i] = DeterministicGammaValue(i, 0.5f, 2.0f, 47); + } + + nvcv::Tensor gammaTensor({{nElements}, "N"}, nvcv::TYPE_F32); + ASSERT_NO_FATAL_FAILURE(UploadGammaTensor(gammaTensor, gammaVec)); + + test::planar::RunTensorParity(planarFmt, interleavedFmt, w, h, w, h, numSamples, + [numSamples, channels, &gammaTensor](cudaStream_t s, const nvcv::Tensor &src, + const nvcv::Tensor &dst, nvcv::ImageFormat) + { + cvcuda::GammaContrast op(numSamples, channels); + EXPECT_NO_THROW(op(s, src, dst, gammaTensor)); + }); +} + +} // namespace + +// width, height, numSamples, perChannelGamma, planarFmt, interleavedFmt +// clang-format off +NVCV_TEST_SUITE_P(OpGammaContrastTensorPlanar, + test::ValueList{ + {176, 113, 2, false, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + {123, 66, 2, true, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + { 64, 48, 1, true, nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8}, + { 50, 40, 3, false, nvcv::FMT_RGBf32p, nvcv::FMT_RGBf32}, + { 72, 54, 2, true, nvcv::FMT_RGBAf32p, nvcv::FMT_RGBAf32}, +}); + +// clang-format on + +TEST_P(OpGammaContrastTensorPlanar, tensor_matches_interleaved) +{ + RunGammaContrastTensorPlanarParityCase(GetParamValue<4>(), GetParamValue<5>(), GetParamValue<0>(), + GetParamValue<1>(), GetParamValue<2>(), GetParamValue<3>()); +} + +// Tensor interleaved (NHWC) correctness against an independent CPU gold (the same gold the var-shape +// path is validated with), exercising the tensor kernel directly. +// width, height, numSamples, format, perChannelGamma +// clang-format off +NVCV_TEST_SUITE_P(OpGammaContrastTensor, + test::ValueList{ + { 9, 11, 2, NVCV_IMAGE_FORMAT_U8, true}, + { 12, 7, 3, NVCV_IMAGE_FORMAT_RGB8, true}, + { 11, 11, 2, NVCV_IMAGE_FORMAT_RGBA8, false}, + { 7, 9, 2, NVCV_IMAGE_FORMAT_RGBA8, true}, + { 8, 6, 2, NVCV_IMAGE_FORMAT_RGBf32, true}, + { 9, 7, 2, NVCV_IMAGE_FORMAT_RGBAf32, true}, +}); + +// clang-format on + +TEST_P(OpGammaContrastTensor, correct_output) +{ + const int width = GetParamValue<0>(); + const int height = GetParamValue<1>(); + const int samples = GetParamValue<2>(); + const nvcv::ImageFormat fmt{GetParamValue<3>()}; + const bool perCh = GetParamValue<4>(); + const int channels = fmt.numChannels(); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + nvcv::Tensor src = nvcv::util::CreateTensor(samples, width, height, fmt); + nvcv::Tensor dst = nvcv::util::CreateTensor(samples, width, height, fmt); + + TensorTestData testData; + ASSERT_NO_FATAL_FAILURE(UploadDeterministicTensorInput(src, samples, width, height, fmt, testData)); + + // Gamma tensor. + const int nElements = perCh ? samples * channels : samples; + std::vector gammaVec(nElements); + for (int i = 0; i < nElements; ++i) + { + gammaVec[i] = DeterministicGammaValue(i, 0.4f, 2.0f, 61); + } + nvcv::Tensor gammaTensor({{nElements}, "N"}, nvcv::TYPE_F32); + ASSERT_NO_FATAL_FAILURE(UploadGammaTensor(gammaTensor, gammaVec)); + + cvcuda::GammaContrast op(samples, channels); + EXPECT_NO_THROW(op(stream, src, dst, gammaTensor)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + for (int n = 0; n < samples; ++n) + { + std::vector got; + std::vector gold = testData.hostSamples[n]; + ASSERT_NO_FATAL_FAILURE(DownloadTensorSample(dst, n, testData.rowStride, height, got)); + GammaContrastVarShapeCpuOpWrapper(gold, testData.rowStride, {width, height}, testData.hostSamples[n], + testData.rowStride, {width, height}, fmt, gammaVec, n, perCh, + testData.dataType); + ExpectTensorNear(got, gold, testData.isFloat); + } + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +// --------------------------------------------------------------------------- +// Scalar (host-float) gamma/gain tensor path +// +// The scalar overload applies out = gain * in**gamma with a single gamma/gain baked into the kernel +// launch (no gamma tensor, no host->device copy). Three properties are checked below: +// * planar (NCHW/CHW) output matches interleaved output bit-for-bit (layout parity); +// * with gain != 1 it matches an independent CPU gold that includes the gain term; +// * with gain == 1 (an exact IEEE no-op) it is bit-exact with the device-tensor gamma path fed a +// gamma tensor filled with the same value -- i.e. the already-gold-validated kernel, across every +// supported dtype. +namespace { + +// Independent CPU gold for the scalar path (single gamma/gain for all samples/channels). +void GammaContrastScalarCpuGold(std::vector &hDst, const std::vector &hSrc, float gamma, float gain, + bool isFloat, NVCVRoundMode roundMode) +{ + if (isFloat) + { + const auto *src = reinterpret_cast(hSrc.data()); + auto *dst = reinterpret_cast(hDst.data()); + const size_t n = hSrc.size() / sizeof(float); + for (size_t i = 0; i < n; ++i) + { + dst[i] = nvcv::cuda::clamp(gain * std::pow(src[i], gamma), 0.f, 1.f); + } + } + else + { + for (size_t i = 0; i < hSrc.size(); ++i) + { + const float tmp = (hSrc[i] + 0.0f) / 255.0f; + float out = gain * std::pow(tmp, gamma) * 255.0f; + out = nvcv::cuda::clamp(out, 0.0f, 255.0f); + hDst[i] = static_cast(roundMode == NVCV_ROUND_TRUNCATE ? std::trunc(out) : std::rint(out)); + } + } +} + +void RunGammaContrastTensorScalarPlanarParityCase(nvcv::ImageFormat planarFmt, nvcv::ImageFormat interleavedFmt, int w, + int h, int numSamples, float gamma, float gain, + NVCVRoundMode roundMode) +{ + const int channels = interleavedFmt.numChannels(); + test::planar::RunTensorParity( + planarFmt, interleavedFmt, w, h, w, h, numSamples, + [numSamples, channels, gamma, gain, roundMode](cudaStream_t s, const nvcv::Tensor &src, const nvcv::Tensor &dst, + nvcv::ImageFormat) + { + cvcuda::GammaContrast op(numSamples, channels); + EXPECT_NO_THROW(op(s, src, dst, gamma, gain, roundMode)); + }); +} + +} // namespace + +// width, height, numSamples, gamma, gain, planarFmt, interleavedFmt, roundMode +// clang-format off +NVCV_TEST_SUITE_P(OpGammaContrastTensorScalarPlanar, + test::ValueList{ + {176, 113, 2, 0.75f, 1.0f, nvcv::FMT_RGB8p, nvcv::FMT_RGB8, NVCV_ROUND_NEAREST}, + {123, 66, 2, 1.50f, 0.9f, nvcv::FMT_RGB8p, nvcv::FMT_RGB8, NVCV_ROUND_TRUNCATE}, + { 64, 48, 1, 0.50f, 0.8f, nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8, NVCV_ROUND_NEAREST}, + { 50, 40, 3, 2.00f, 1.0f, nvcv::FMT_RGBf32p, nvcv::FMT_RGBf32, NVCV_ROUND_TRUNCATE}, + { 72, 54, 2, 0.70f, 1.2f, nvcv::FMT_RGBAf32p, nvcv::FMT_RGBAf32, NVCV_ROUND_NEAREST}, +}); + +// clang-format on + +TEST_P(OpGammaContrastTensorScalarPlanar, planar_matches_interleaved) +{ + RunGammaContrastTensorScalarPlanarParityCase(GetParamValue<5>(), GetParamValue<6>(), GetParamValue<0>(), + GetParamValue<1>(), GetParamValue<2>(), GetParamValue<3>(), + GetParamValue<4>(), GetParamValue<7>()); +} + +// Scalar path against an independent CPU gold that includes the gain term (interleaved). +// width, height, numSamples, format, gamma, gain, roundMode +// clang-format off +NVCV_TEST_SUITE_P(OpGammaContrastTensorScalar, + test::ValueList{ + { 9, 11, 2, NVCV_IMAGE_FORMAT_U8, 0.50f, 0.8f, NVCV_ROUND_NEAREST}, + { 12, 7, 3, NVCV_IMAGE_FORMAT_RGB8, 1.50f, 0.9f, NVCV_ROUND_TRUNCATE}, + { 11, 11, 2, NVCV_IMAGE_FORMAT_RGBA8, 0.80f, 1.0f, NVCV_ROUND_NEAREST}, + { 8, 6, 2, NVCV_IMAGE_FORMAT_RGBf32, 2.00f, 1.0f, NVCV_ROUND_TRUNCATE}, + { 10, 5, 2, NVCV_IMAGE_FORMAT_RGBAf32, 0.70f, 1.2f, NVCV_ROUND_NEAREST}, +}); + +// clang-format on + +TEST_P(OpGammaContrastTensorScalar, matches_cpu_gold) +{ + const int width = GetParamValue<0>(); + const int height = GetParamValue<1>(); + const int samples = GetParamValue<2>(); + const nvcv::ImageFormat fmt{GetParamValue<3>()}; + const float gamma = GetParamValue<4>(); + const float gain = GetParamValue<5>(); + const NVCVRoundMode roundMode = GetParamValue<6>(); + const int channels = fmt.numChannels(); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + nvcv::Tensor src = nvcv::util::CreateTensor(samples, width, height, fmt); + nvcv::Tensor dst = nvcv::util::CreateTensor(samples, width, height, fmt); + + TensorTestData testData; + ASSERT_NO_FATAL_FAILURE(UploadDeterministicTensorInput(src, samples, width, height, fmt, testData)); + + cvcuda::GammaContrast op(samples, channels); + EXPECT_NO_THROW(op(stream, src, dst, gamma, gain, roundMode)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + for (int n = 0; n < samples; ++n) + { + std::vector got; + std::vector gold(testData.sampleBytes); + ASSERT_NO_FATAL_FAILURE(DownloadTensorSample(dst, n, testData.rowStride, height, got)); + GammaContrastScalarCpuGold(gold, testData.hostSamples[n], gamma, gain, testData.isFloat, roundMode); + ExpectTensorNear(got, gold, testData.isFloat); + } + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpGammaContrastTensorScalar, round_mode_controls_integer_conversion) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + nvcv::Tensor src = nvcv::util::CreateTensor(1, 1, 1, nvcv::FMT_U8); + nvcv::Tensor nearest = nvcv::util::CreateTensor(1, 1, 1, nvcv::FMT_U8); + nvcv::Tensor truncate = nvcv::util::CreateTensor(1, 1, 1, nvcv::FMT_U8); + + auto srcData = src.exportData(); + auto nearestData = nearest.exportData(); + auto truncateData = truncate.exportData(); + ASSERT_TRUE(srcData && nearestData && truncateData); + + constexpr uint8_t input = 255; + ASSERT_EQ(cudaSuccess, cudaMemcpy(srcData->basePtr(), &input, sizeof(input), cudaMemcpyHostToDevice)); + + cvcuda::GammaContrast op(1, 1); + constexpr float gain = 1.5f / 255.0f; + EXPECT_NO_THROW(op(stream, src, nearest, 1.0f, gain, NVCV_ROUND_NEAREST)); + EXPECT_NO_THROW(op(stream, src, truncate, 1.0f, gain, NVCV_ROUND_TRUNCATE)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + uint8_t nearestValue = 0; + uint8_t truncateValue = 0; + ASSERT_EQ(cudaSuccess, + cudaMemcpy(&nearestValue, nearestData->basePtr(), sizeof(nearestValue), cudaMemcpyDeviceToHost)); + ASSERT_EQ(cudaSuccess, + cudaMemcpy(&truncateValue, truncateData->basePtr(), sizeof(truncateValue), cudaMemcpyDeviceToHost)); + EXPECT_EQ(nearestValue, 2); + EXPECT_EQ(truncateValue, 1); + + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&] { op(stream, src, nearest, 1.0f, gain, static_cast(-1)); })); + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +// Scalar path with gain == 1 must be bit-exact with the device-tensor gamma path fed a gamma tensor +// filled with the same value (that path is itself validated against the CPU gold above). Exercises the +// full supported dtype set through the scalar kernels. +// width, height, numSamples, format, gamma +// clang-format off +NVCV_TEST_SUITE_P(OpGammaContrastScalarVsTensor, + test::ValueList{ + { 9, 11, 2, NVCV_IMAGE_FORMAT_U8, 0.60f}, + { 12, 7, 3, NVCV_IMAGE_FORMAT_RGB8, 1.40f}, + { 11, 11, 2, NVCV_IMAGE_FORMAT_RGBA8, 0.80f}, + { 10, 9, 2, NVCV_IMAGE_FORMAT_U16, 1.20f}, + { 7, 8, 2, NVCV_IMAGE_FORMAT_S16, 0.90f}, + { 8, 6, 2, NVCV_IMAGE_FORMAT_S32, 1.10f}, + { 8, 6, 2, NVCV_IMAGE_FORMAT_RGBf32, 0.70f}, + { 10, 5, 2, NVCV_IMAGE_FORMAT_RGBAf32, 1.30f}, +}); + +// clang-format on + +TEST_P(OpGammaContrastScalarVsTensor, bit_exact_gain1) +{ + const int width = GetParamValue<0>(); + const int height = GetParamValue<1>(); + const int samples = GetParamValue<2>(); + const nvcv::ImageFormat fmt{GetParamValue<3>()}; + const float gamma = GetParamValue<4>(); + const int channels = fmt.numChannels(); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + nvcv::Tensor src = nvcv::util::CreateTensor(samples, width, height, fmt); + nvcv::Tensor dstScalar = nvcv::util::CreateTensor(samples, width, height, fmt); + nvcv::Tensor dstTensor = nvcv::util::CreateTensor(samples, width, height, fmt); + + TensorTestData testData; + ASSERT_NO_FATAL_FAILURE(UploadDeterministicTensorInput(src, samples, width, height, fmt, testData)); + + // Per-image gamma tensor filled with the same value -> every sample/channel uses `gamma`, matching + // the scalar broadcast; gain == 1 is an exact IEEE no-op, so the two outputs must be identical. + std::vector gammaVec(samples, gamma); + nvcv::Tensor gammaTensor({{samples}, "N"}, nvcv::TYPE_F32); + ASSERT_NO_FATAL_FAILURE(UploadGammaTensor(gammaTensor, gammaVec)); + + cvcuda::GammaContrast op(samples, channels); + EXPECT_NO_THROW(op(stream, src, dstScalar, gamma, 1.0f)); + EXPECT_NO_THROW(op(stream, src, dstTensor, gammaTensor)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + auto compareSample = [&dstScalar, &dstTensor, &testData, height](int n) + { + std::vector scalarOut; + std::vector tensorOut; + ASSERT_NO_FATAL_FAILURE(DownloadTensorSample(dstScalar, n, testData.rowStride, height, scalarOut)); + ASSERT_NO_FATAL_FAILURE(DownloadTensorSample(dstTensor, n, testData.rowStride, height, tensorOut)); + EXPECT_EQ(scalarOut, tensorOut) << "scalar (gain=1) != device-tensor gamma path at sample " << n; + }; + + for (int n = 0; n < samples; ++n) + { + ASSERT_NO_FATAL_FAILURE(compareSample(n)); + } + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} diff --git a/tests/cvcuda/system/TestOpGaussian.cpp b/tests/cvcuda/system/TestOpGaussian.cpp index ead1eb4b5..7ca7bf823 100644 --- a/tests/cvcuda/system/TestOpGaussian.cpp +++ b/tests/cvcuda/system/TestOpGaussian.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "ConvUtils.hpp" #include "Definitions.hpp" +#include "PlanarParityUtils.hpp" #include #include @@ -33,6 +34,29 @@ namespace test = nvcv::test; namespace cuda = nvcv::cuda; +namespace { + +// builds Gaussian and its per-image parameter tensors for a var-shape negative case +inline void InvokeGaussianVarShapeNegative(cudaStream_t stream, const nvcv::ImageBatchVarShape &src, + const nvcv::ImageBatchVarShape &dst, int maxBatches, + NVCVBorderType borderMode) +{ + const nvcv::Size2D kernelSize(3, 3); + const int numImages = src.numImages(); + auto kernelSizeTensor + = test::planar::MakePerImageTensor(numImages, nvcv::TYPE_2S32, int2{kernelSize.w, kernelSize.h}); + auto sigmaTensor = test::planar::MakePerImageTensor(numImages, nvcv::TYPE_2F64, double2{0.5, 0.5}); + cvcuda::Gaussian op(kernelSize, maxBatches); + op(stream, src, dst, kernelSizeTensor, sigmaTensor, borderMode); +} + +} // namespace + +static int ScaledSize(int size, double scale) +{ + return static_cast(size * scale); +} + // clang-format off NVCV_TEST_SUITE_P(OpGaussian, test::ValueList @@ -124,7 +148,7 @@ TEST_P(OpGaussian, correct_output) std::default_random_engine randEng(0); std::uniform_int_distribution rand(0u, 255u); - std::generate(inVec.begin(), inVec.end(), [&]() { return rand(randEng); }); + std::ranges::generate(inVec, [&rand, &randEng]() { return rand(randEng); }); // copy random input to device ASSERT_EQ(cudaSuccess, cudaMemcpy(inData->basePtr(), inVec.data(), inBufSize, cudaMemcpyHostToDevice)); @@ -192,9 +216,9 @@ TEST_P(OpGaussian, varshape_correct_output) nvcv::Size2D kernelSize(newKsizeX, newKsizeY); // Create input varshape - std::default_random_engine rng; - std::uniform_int_distribution udistWidth(width * 0.8, width * 1.1); - std::uniform_int_distribution udistHeight(height * 0.8, height * 1.1); + std::default_random_engine rng; + std::uniform_int_distribution udistWidth(ScaledSize(width, 0.8), ScaledSize(width, 1.1)); + std::uniform_int_distribution udistHeight(ScaledSize(height, 0.8), ScaledSize(height, 1.1)); std::vector imgSrc; @@ -211,7 +235,7 @@ TEST_P(OpGaussian, varshape_correct_output) std::uniform_int_distribution udist(0, 255); srcVec[i].resize(imgSrc[i].size().h * srcRowStride); - std::generate(srcVec[i].begin(), srcVec[i].end(), [&]() { return udist(rng); }); + std::ranges::generate(srcVec[i], [&udist, &rng]() { return udist(rng); }); auto imgData = imgSrc[i].exportData(); ASSERT_NE(imgData, nvcv::NullOpt); @@ -301,21 +325,107 @@ TEST_P(OpGaussian, varshape_correct_output) } } +// ============================================================================= +// Planar (NCHW/CHW) layout support +// +// Gaussian filters each channel independently, so a planar input is filtered plane-by-plane and +// must produce exactly the same pixels as the interleaved path. These tests feed identical uint8 +// data through cvcuda::Gaussian in both layouts and require the (re-interleaved) planar output to +// match the interleaved output bit-for-bit. +// ============================================================================= + +// Parameters: width, height, kernelWidth, kernelHeight, sigmaX, sigmaY, borderMode, numImages, planarFmt, interleavedFmt // clang-format off -NVCV_TEST_SUITE_P(OpGaussian_Negative, nvcv::test::ValueList{ - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U16, 3, 3, 0.5, 0.5, NVCV_BORDER_CONSTANT}, // data type is different - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_RGB8, nvcv::FMT_RGB8p, 3, 3, 0.5, 0.5, NVCV_BORDER_CONSTANT}, // data format is different - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_RGB8p, nvcv::FMT_RGB8p, 3, 3, 0.5, 0.5, NVCV_BORDER_CONSTANT}, // data format is not kNHWC/kHWC -#ifndef ENABLE_SANITIZER - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U8, 3, 3, 0.5, 0.5, static_cast(255)}, // invalid borderType -#endif - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_F16, nvcv::FMT_F16, 3, 3, 0.5, 0.5, NVCV_BORDER_CONSTANT}, // invalid data type - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U8, 4, 3, 0.5, 0.5, NVCV_BORDER_CONSTANT}, // invalid kernel size - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U8, 3, 4, 0.5, 0.5, NVCV_BORDER_CONSTANT}, // invalid kernel size +NVCV_TEST_SUITE_P(OpGaussianPlanar, + test::ValueList{ + { 64, 48, 3, 3, 0.5, 0.5, NVCV_BORDER_CONSTANT, 2, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + { 67, 51, 5, 3, 0.9, 0.7, NVCV_BORDER_REFLECT, 1, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + { 50, 40, 7, 7, 1.2, 1.2, NVCV_BORDER_REPLICATE, 2, nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8}, + { 64, 48, 9, 5, 1.4, 0.8, NVCV_BORDER_REFLECT101, 1, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + { 32, 28, 3, 5, 0.6, 1.1, NVCV_BORDER_WRAP, 1, nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8}, + { 33, 29, 67, 67, 14.0, 14.0, NVCV_BORDER_CONSTANT, 1, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + { 33, 29, 75, 75, 16.0, 16.0, NVCV_BORDER_REPLICATE, 1, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + { 33, 29, -1, -1, 15.0, 15.0, NVCV_BORDER_CONSTANT, 1, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + { 33, 29, 95, 95, 18.0, 18.0, NVCV_BORDER_REFLECT, 1, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + { 33, 29, -1, -1, 16.0, 16.0, NVCV_BORDER_REPLICATE, 1, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, }); // clang-format on +TEST_P(OpGaussianPlanar, tensor_matches_interleaved) +{ + nvcv::Size2D kernelSize{GetParamValue<2>(), GetParamValue<3>()}; + double2 sigma{GetParamValue<4>(), GetParamValue<5>()}; + nvcv::Size2D maxKernelSize = kernelSize; + if (maxKernelSize.w <= 0) + maxKernelSize.w = nvcv::cuda::round(sigma.x * 3 * 2 + 1) | 1; + if (maxKernelSize.h <= 0) + maxKernelSize.h = nvcv::cuda::round(sigma.y * 3 * 2 + 1) | 1; + NVCVBorderType borderMode = GetParamValue<6>(); + int numImages = GetParamValue<7>(); + + test::planar::RunTensorParity( + GetParamValue<8>(), GetParamValue<9>(), GetParamValue<0>(), GetParamValue<1>(), GetParamValue<0>(), + GetParamValue<1>(), numImages, + [kernelSize, maxKernelSize, sigma, borderMode, numImages](cudaStream_t s, const nvcv::Tensor &src, + const nvcv::Tensor &dst, nvcv::ImageFormat) + { + cvcuda::Gaussian op(maxKernelSize, numImages); + EXPECT_NO_THROW(op(s, src, dst, kernelSize, sigma, borderMode)); + }); +} + +TEST_P(OpGaussianPlanar, varshape_matches_interleaved) +{ + nvcv::Size2D kernelSize{GetParamValue<2>(), GetParamValue<3>()}; + double2 sigma{GetParamValue<4>(), GetParamValue<5>()}; + nvcv::Size2D maxKernelSize = kernelSize; + if (maxKernelSize.w <= 0) + maxKernelSize.w = nvcv::cuda::round(sigma.x * 3 * 2 + 1) | 1; + if (maxKernelSize.h <= 0) + maxKernelSize.h = nvcv::cuda::round(sigma.y * 3 * 2 + 1) | 1; + NVCVBorderType borderMode = GetParamValue<6>(); + int numImages = GetParamValue<7>(); + + auto kernelSizeTensor + = test::planar::MakePerImageTensor(numImages, nvcv::TYPE_2S32, int2{kernelSize.w, kernelSize.h}); + auto sigmaTensor = test::planar::MakePerImageTensor(numImages, nvcv::TYPE_2F64, sigma); + + test::planar::RunVarShapeParity( + GetParamValue<8>(), GetParamValue<9>(), GetParamValue<0>(), GetParamValue<1>(), GetParamValue<0>(), + GetParamValue<1>(), numImages, + [&kernelSizeTensor, &sigmaTensor, maxKernelSize, borderMode, numImages]( + cudaStream_t s, const nvcv::ImageBatchVarShape &src, const nvcv::ImageBatchVarShape &dst, nvcv::ImageFormat) + { + cvcuda::Gaussian op(maxKernelSize, numImages); + EXPECT_NO_THROW(op(s, src, dst, kernelSizeTensor, sigmaTensor, borderMode)); + }); +} + +static auto OpGaussianNegativeParams() +{ + nvcv::test::ValueList + params{ + {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U16, 3, 3, 0.5, 0.5, + NVCV_BORDER_CONSTANT}, // data type is different + {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_RGB8, nvcv::FMT_RGB8p, 3, 3, 0.5, 0.5, + NVCV_BORDER_CONSTANT}, // data format is different + {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_RGB8p, nvcv::FMT_RGB8, 3, 3, 0.5, 0.5, + NVCV_BORDER_CONSTANT}, // data format is different + }; +#ifndef ENABLE_SANITIZER + params.emplace_back(NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U8, 3, 3, 0.5, 0.5, + static_cast(255)); +#endif + params.emplace_back(NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_F16, nvcv::FMT_F16, 3, 3, 0.5, 0.5, + NVCV_BORDER_CONSTANT); + params.emplace_back(NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U8, 4, 3, 0.5, 0.5, NVCV_BORDER_CONSTANT); + params.emplace_back(NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U8, 3, 4, 0.5, 0.5, NVCV_BORDER_CONSTANT); + return params; +} + +NVCV_TEST_SUITE_P(OpGaussian_Negative, OpGaussianNegativeParams()); + TEST_P(OpGaussian_Negative, op) { cudaStream_t stream; @@ -344,134 +454,22 @@ TEST_P(OpGaussian_Negative, op) cvcuda::Gaussian gaussianOp({11, 11}, 1); EXPECT_EQ(expectedReturnCode, - nvcv::ProtectCall([&] { gaussianOp(stream, inTensor, outTensor, kernelSize, sigma, borderMode); })); + nvcv::ProtectCall([&gaussianOp, &stream, &inTensor, &outTensor, &kernelSize, &sigma, &borderMode] + { gaussianOp(stream, inTensor, outTensor, kernelSize, sigma, borderMode); })); } -// clang-format off -NVCV_TEST_SUITE_P(OpGaussianVarshape_Negative, test::ValueList{ - {nvcv::FMT_RGB8, nvcv::FMT_RGB8p, NVCV_BORDER_CONSTANT, 3, 3}, - {nvcv::FMT_RGB8p, nvcv::FMT_RGB8p, NVCV_BORDER_CONSTANT, 3, 3}, - {nvcv::FMT_RGBf16, nvcv::FMT_RGBf16, NVCV_BORDER_CONSTANT, 3, 3}, - {nvcv::FMT_RGB8, nvcv::FMT_RGB8, NVCV_BORDER_CONSTANT, 3, -1}, - {nvcv::FMT_RGB8, nvcv::FMT_RGB8, NVCV_BORDER_CONSTANT, 5, 3}, -#ifndef ENABLE_SANITIZER - {nvcv::FMT_RGB8, nvcv::FMT_RGB8, static_cast(255), 3, 3}, -#endif -}); -// clang-format on +NVCV_TEST_SUITE_P(OpGaussianVarshape_Negative, test::PlanarFilterVarShapeNegativeParams()); TEST_P(OpGaussianVarshape_Negative, op) { - cudaStream_t stream; - ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); - - int width = 32; - int height = 32; - nvcv::Size2D kernelSize(3, 3); - - nvcv::ImageFormat inputFmt = GetParamValue<0>(); - nvcv::ImageFormat outputFmt = GetParamValue<1>(); - NVCVBorderType borderMode = GetParamValue<2>(); - int batches = GetParamValue<3>(); - int maxBatches = GetParamValue<4>(); - - // Create input varshape - std::default_random_engine rng; - std::uniform_int_distribution udistWidth(width * 0.8, width * 1.1); - std::uniform_int_distribution udistHeight(height * 0.8, height * 1.1); - - std::vector imgSrc; - std::vector imgDst; - - for (int i = 0; i < batches; ++i) - { - imgSrc.emplace_back(nvcv::Size2D{udistWidth(rng), udistHeight(rng)}, inputFmt); - imgDst.emplace_back(imgSrc[i].size(), outputFmt); - } - - nvcv::ImageBatchVarShape batchSrc(batches); - batchSrc.pushBack(imgSrc.begin(), imgSrc.end()); - nvcv::ImageBatchVarShape batchDst(batches); - batchDst.pushBack(imgDst.begin(), imgDst.end()); - - // Create kernel size tensor - nvcv::Tensor kernelSizeTensor({{batches}, "N"}, nvcv::TYPE_2S32); - - // Create sigma tensor - nvcv::Tensor sigmaTensor({{batches}, "N"}, nvcv::TYPE_2F64); - - // Run operator - cvcuda::Gaussian gaussianOp(kernelSize, maxBatches); - - EXPECT_EQ( - NVCV_ERROR_INVALID_ARGUMENT, - nvcv::ProtectCall([&] { gaussianOp(stream, batchSrc, batchDst, kernelSizeTensor, sigmaTensor, borderMode); })); - - ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); - ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + test::planar::ExpectVarShapeUniformFormatRejected(GetParamValue<0>(), GetParamValue<1>(), GetParamValue<3>(), + GetParamValue<4>(), GetParamValue<2>(), + InvokeGaussianVarShapeNegative); } TEST(OpGaussianVarshape_Negative, varshape_hasDifferentFormat) { - cudaStream_t stream; - ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); - - nvcv::ImageFormat fmt = nvcv::FMT_RGB8; - - std::vector> testSet{ - {nvcv::FMT_U8, fmt}, - { fmt, nvcv::FMT_U8} - }; - - for (auto testCase : testSet) - { - nvcv::ImageFormat inputFmtExtra = std::get<0>(testCase); - nvcv::ImageFormat outputFmtExtra = std::get<1>(testCase); - - int width = 32; - int height = 32; - int batches = 3; - nvcv::Size2D kernelSize(3, 3); - NVCVBorderType borderMode = NVCV_BORDER_CONSTANT; - - // Create input varshape - std::default_random_engine rng; - std::uniform_int_distribution udistWidth(width * 0.8, width * 1.1); - std::uniform_int_distribution udistHeight(height * 0.8, height * 1.1); - - std::vector imgSrc; - std::vector imgDst; - - for (int i = 0; i < batches - 1; ++i) - { - imgSrc.emplace_back(nvcv::Size2D{udistWidth(rng), udistHeight(rng)}, fmt); - imgDst.emplace_back(imgSrc[i].size(), fmt); - } - imgSrc.emplace_back(nvcv::Size2D{udistWidth(rng), udistHeight(rng)}, inputFmtExtra); - imgDst.emplace_back(imgSrc.back().size(), outputFmtExtra); - - nvcv::ImageBatchVarShape batchSrc(batches); - batchSrc.pushBack(imgSrc.begin(), imgSrc.end()); - nvcv::ImageBatchVarShape batchDst(batches); - batchDst.pushBack(imgDst.begin(), imgDst.end()); - - // Create kernel size tensor - nvcv::Tensor kernelSizeTensor({{batches}, "N"}, nvcv::TYPE_2S32); - - // Create sigma tensor - nvcv::Tensor sigmaTensor({{batches}, "N"}, nvcv::TYPE_2F64); - - // Run operator - cvcuda::Gaussian gaussianOp(kernelSize, batches); - - EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, - nvcv::ProtectCall( - [&] { gaussianOp(stream, batchSrc, batchDst, kernelSizeTensor, sigmaTensor, borderMode); })); - - ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); - } - - ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + test::planar::ExpectVarShapeMixedFormatRejected(InvokeGaussianVarShapeNegative); } TEST(OpGaussian_Negative, create_null_handle) diff --git a/tests/cvcuda/system/TestOpGaussianNoise.cpp b/tests/cvcuda/system/TestOpGaussianNoise.cpp index 88682970c..3927c627e 100644 --- a/tests/cvcuda/system/TestOpGaussianNoise.cpp +++ b/tests/cvcuda/system/TestOpGaussianNoise.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,6 +16,7 @@ */ #include "Definitions.hpp" +#include "PlanarParityUtils.hpp" #include "GaussianNoiseUtils.cuh" @@ -31,78 +32,271 @@ #include #include +#include #include +#include #include +#include +#include + +#define NVCV_IMAGE_FORMAT_2U8 NVCV_DETAIL_MAKE_NONCOLOR_FMT1(PL, UNSIGNED, XY00, ASSOCIATED, X8_Y8) namespace { -inline uint8_t cast(float value) +template +void SubmitGaussianNoiseRepeatedly(int calls, Submit &&submit) { - int v = (int)(value + (value >= 0 ? 0.5 : -0.5)); - return (uint8_t)((unsigned)v <= 255 ? v : v > 0 ? 255 : 0); + for (int call = 0; call < calls; ++call) + { + submit(); + } +} + +inline int ScaledSize(int size, double scale) +{ + return static_cast(size * scale); } -//test for RGB8 template -void GaussianNoise(std::vector &src, std::vector &dst, float mu, float sigma, int batch, bool per_channel) +T cast(float value) { - int mem_size = src.size(); + static_assert(std::is_integral_v); + + double rounded = static_cast(value) + (value >= 0 ? 0.5 : -0.5); + if (rounded < static_cast(std::numeric_limits::lowest())) + { + return std::numeric_limits::lowest(); + } + if (rounded > static_cast(std::numeric_limits::max())) + { + return std::numeric_limits::max(); + } + return static_cast(rounded); +} + +template<> +float cast(float value) +{ + return nvcv::cuda::clamp(nvcv::cuda::StaticCast(value), 0.f, 1.f); +} + +template +void GaussianNoise(std::vector &src, std::vector &dst, float mu, float sigma, int batch, bool per_channel, + int channels, int call_index = 0) +{ + auto mem_size = static_cast(src.size()); if (!per_channel) - mem_size /= 3; - float *rand_h = (float *)malloc(sizeof(float) * mem_size); - get_random(rand_h, per_channel, batch, mem_size); + mem_size /= channels; + std::vector rand_h(mem_size); + get_random(rand_h.data(), per_channel, batch, mem_size, channels, call_index); + const float *rand = rand_h.data(); - int img_size = src.size() / 3; + auto img_size = static_cast(src.size() / channels); for (int i = 0; i < img_size; i++) { if (per_channel) { - for (int ch = 0; ch < 3; ch++) + for (int ch = 0; ch < channels; ch++) { - float delta = mu + rand_h[i * 3 + ch] * sigma; - dst[i * 3 + ch] = cast(src[i * 3 + ch] + delta); + float delta = mu + rand[i * channels + ch] * sigma; + dst[i * channels + ch] = cast(static_cast(src[i * channels + ch]) + delta); } } else { - float delta = mu + rand_h[i] * sigma; - dst[i * 3] = cast(src[i * 3] + delta); - dst[i * 3 + 1] = cast(src[i * 3 + 1] + delta); - dst[i * 3 + 2] = cast(src[i * 3 + 2] + delta); + float delta = mu + rand[i] * sigma; + for (int ch = 0; ch < channels; ++ch) + { + dst[i * channels + ch] = cast(static_cast(src[i * channels + ch]) + delta); + } } } - free(rand_h); } -// test for float -template<> -void GaussianNoise(std::vector &src, std::vector &dst, float mu, float sigma, int batch, bool per_channel) +nvcv::Tensor MakeGaussianNoiseParam(int batch, float value) { - int mem_size = src.size(); - if (!per_channel) - mem_size /= 3; - float *rand_h = (float *)malloc(sizeof(float) * mem_size); - get_random(rand_h, per_channel, batch, mem_size); + nvcv::Tensor tensor({{batch}, "N"}, nvcv::TYPE_F32); + nvcv::test::planar::UploadTensorValues(tensor, std::vector(batch, value)); + return tensor; +} - int img_size = src.size() / 3; - for (int i = 0; i < img_size; i++) +std::vector MakeGaussianNoiseInput(nvcv::ImageFormat fmt, int width, int height, int sample) +{ + const int channels = fmt.numChannels(); + const int elemSize = fmt.planePixelStrideBytes(0) / channels; + const size_t count = static_cast(width) * height * channels; + + std::vector bytes(count * elemSize); + if (fmt.dataKind() == nvcv::DataKind::FLOAT) { - if (per_channel) + if (elemSize != static_cast(sizeof(float))) { - for (int ch = 0; ch < 3; ch++) - { - float delta = mu + rand_h[i * 3 + ch] * sigma; - dst[i * 3 + ch] = nvcv::cuda::clamp(nvcv::cuda::StaticCast(src[i * 3 + ch] + delta), 0.f, 1.f); - } + throw std::invalid_argument("MakeGaussianNoiseInput supports only 32-bit float formats"); } - else + std::vector values(count); + for (size_t i = 0; i < values.size(); ++i) { - float delta = mu + rand_h[i] * sigma; - dst[i * 3] = nvcv::cuda::clamp(nvcv::cuda::StaticCast(src[i * 3] + delta), 0.f, 1.f); - dst[i * 3 + 1] = nvcv::cuda::clamp(nvcv::cuda::StaticCast(src[i * 3 + 1] + delta), 0.f, 1.f); - dst[i * 3 + 2] = nvcv::cuda::clamp(nvcv::cuda::StaticCast(src[i * 3 + 2] + delta), 0.f, 1.f); + values[i] = static_cast(((i + 1) * 37 + sample * 17) % 1024) / 1023.f; } + std::memcpy(bytes.data(), values.data(), values.size() * sizeof(float)); } - free(rand_h); + else + { + for (size_t i = 0; i < bytes.size(); ++i) + { + bytes[i] = static_cast(((i + 1) * 37 + sample * 17) % 256); + } + } + return bytes; +} + +void RunGaussianNoiseTensorPlanarParity(nvcv::ImageFormat planarFmt, nvcv::ImageFormat interleavedFmt, int width, + int height, int batch, float mu, float sigma, bool per_channel) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + const int channels = planarFmt.numChannels(); + const int elemSize = planarFmt.planePixelStrideBytes(0); + const int rowStride = width * channels * elemSize; + + nvcv::Tensor srcI = nvcv::util::CreateTensor(batch, width, height, interleavedFmt); + nvcv::Tensor dstI = nvcv::util::CreateTensor(batch, width, height, interleavedFmt); + nvcv::Tensor srcP = nvcv::util::CreateTensor(batch, width, height, planarFmt); + nvcv::Tensor dstP = nvcv::util::CreateTensor(batch, width, height, planarFmt); + + auto srcIData = srcI.exportData(); + auto dstIData = dstI.exportData(); + auto srcPData = srcP.exportData(); + auto dstPData = dstP.exportData(); + ASSERT_TRUE(srcIData && dstIData && srcPData && dstPData); + + auto srcIAcc = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcIData); + auto dstIAcc = nvcv::TensorDataAccessStridedImagePlanar::Create(*dstIData); + auto srcPAcc = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcPData); + auto dstPAcc = nvcv::TensorDataAccessStridedImagePlanar::Create(*dstPData); + ASSERT_TRUE(srcIAcc && dstIAcc && srcPAcc && dstPAcc); + + for (int i = 0; i < batch; ++i) + { + auto hwc = MakeGaussianNoiseInput(interleavedFmt, width, height, i + 17); + nvcv::test::planar::UploadInterleavedSample(*srcIAcc, i, hwc, width, height, rowStride); + nvcv::test::planar::UploadPlanarSample( + *srcPAcc, i, nvcv::test::planar::DeinterleaveToPlanes(hwc, width, height, channels, elemSize), width, + height, channels, elemSize); + } + + nvcv::Tensor muval = MakeGaussianNoiseParam(batch, mu); + nvcv::Tensor sigmaval = MakeGaussianNoiseParam(batch, sigma); + + unsigned long long seed = 12345; + cvcuda::GaussianNoise interleavedOp(batch); + cvcuda::GaussianNoise planarOp(batch); + EXPECT_NO_THROW(interleavedOp(stream, srcI, dstI, muval, sigmaval, per_channel, seed)); + EXPECT_NO_THROW(planarOp(stream, srcP, dstP, muval, sigmaval, per_channel, seed)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + for (int i = 0; i < batch; ++i) + { + SCOPED_TRACE(i); + auto gpuInter = nvcv::test::planar::DownloadInterleavedSample(*dstIAcc, i, width, height, rowStride); + auto planesOut = nvcv::test::planar::DownloadPlanarSample(*dstPAcc, i, width, height, channels, elemSize); + auto planarInter = nvcv::test::planar::InterleaveFromPlanes(planesOut, width, height, channels, elemSize); + EXPECT_EQ(gpuInter, planarInter); + } + + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +void RunGaussianNoiseVarShapePlanarParity(nvcv::ImageFormat planarFmt, nvcv::ImageFormat interleavedFmt, int width, + int height, int batch, float mu, float sigma, bool per_channel) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + const int channels = planarFmt.numChannels(); + const int elemSize = planarFmt.planePixelStrideBytes(0); + + std::vector srcI; + std::vector dstI; + std::vector srcP; + std::vector dstP; + std::vector widths; + std::vector heights; + for (int i = 0; i < batch; ++i) + { + widths.push_back(width + i % 3); + heights.push_back(height + i % 2); + srcI.emplace_back(nvcv::Size2D{widths.back(), heights.back()}, interleavedFmt); + dstI.emplace_back(nvcv::Size2D{widths.back(), heights.back()}, interleavedFmt); + srcP.emplace_back(nvcv::Size2D{widths.back(), heights.back()}, planarFmt); + dstP.emplace_back(nvcv::Size2D{widths.back(), heights.back()}, planarFmt); + } + + nvcv::ImageBatchVarShape batchSrcI(batch); + nvcv::ImageBatchVarShape batchDstI(batch); + nvcv::ImageBatchVarShape batchSrcP(batch); + nvcv::ImageBatchVarShape batchDstP(batch); + batchSrcI.pushBack(srcI.begin(), srcI.end()); + batchDstI.pushBack(dstI.begin(), dstI.end()); + batchSrcP.pushBack(srcP.begin(), srcP.end()); + batchDstP.pushBack(dstP.begin(), dstP.end()); + + for (int i = 0; i < batch; ++i) + { + const int rowStride = widths[i] * channels * elemSize; + auto hwc = MakeGaussianNoiseInput(interleavedFmt, widths[i], heights[i], i + 31); + + auto idata = srcI[i].exportData(); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(idata->plane(0).basePtr, idata->plane(0).rowStride, hwc.data(), rowStride, + rowStride, heights[i], cudaMemcpyHostToDevice)); + + auto planes = nvcv::test::planar::DeinterleaveToPlanes(hwc, widths[i], heights[i], channels, elemSize); + auto pdata = srcP[i].exportData(); + ASSERT_EQ(pdata->numPlanes(), channels); + const int planeBytes = widths[i] * heights[i] * elemSize; + for (int c = 0; c < channels; ++c) + { + ASSERT_EQ(cudaSuccess, + cudaMemcpy2D(pdata->plane(c).basePtr, pdata->plane(c).rowStride, planes.data() + c * planeBytes, + widths[i] * elemSize, widths[i] * elemSize, heights[i], cudaMemcpyHostToDevice)); + } + } + + nvcv::Tensor muval = MakeGaussianNoiseParam(batch, mu); + nvcv::Tensor sigmaval = MakeGaussianNoiseParam(batch, sigma); + + unsigned long long seed = 12345; + cvcuda::GaussianNoise interleavedOp(batch); + cvcuda::GaussianNoise planarOp(batch); + EXPECT_NO_THROW(interleavedOp(stream, batchSrcI, batchDstI, muval, sigmaval, per_channel, seed)); + EXPECT_NO_THROW(planarOp(stream, batchSrcP, batchDstP, muval, sigmaval, per_channel, seed)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + for (int i = 0; i < batch; ++i) + { + SCOPED_TRACE(i); + const int rowStride = widths[i] * channels * elemSize; + const int planeBytes = widths[i] * heights[i] * elemSize; + + std::vector gpuInter(heights[i] * rowStride); + auto idata = dstI[i].exportData(); + EXPECT_EQ(cudaSuccess, cudaMemcpy2D(gpuInter.data(), rowStride, idata->plane(0).basePtr, + idata->plane(0).rowStride, rowStride, heights[i], cudaMemcpyDeviceToHost)); + + std::vector planesOut(widths[i] * heights[i] * channels * elemSize); + auto pdata = dstP[i].exportData(); + for (int c = 0; c < channels; ++c) + { + EXPECT_EQ(cudaSuccess, cudaMemcpy2D(planesOut.data() + c * planeBytes, widths[i] * elemSize, + pdata->plane(c).basePtr, pdata->plane(c).rowStride, + widths[i] * elemSize, heights[i], cudaMemcpyDeviceToHost)); + } + auto planarInter + = nvcv::test::planar::InterleaveFromPlanes(planesOut, widths[i], heights[i], channels, elemSize); + EXPECT_EQ(gpuInter, planarInter); + } + + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); } } // namespace @@ -119,14 +313,14 @@ NVCV_TEST_SUITE_P(OpGaussianNoise, nvcv::test::ValueList -static void tensor_correct_output_test(int batch, int height, int width, float mu, float sigma, bool per_channel) +static void tensor_correct_output_test(int batch, int height, int width, float mu, float sigma, bool per_channel, + nvcv::ImageFormat fmt, int calls = 1) { cudaStream_t stream; EXPECT_EQ(cudaSuccess, cudaStreamCreate(&stream)); - nvcv::ImageFormat fmt = std::is_same::value ? nvcv::FMT_RGB8 : nvcv::FMT_RGBf32; - nvcv::Tensor imgIn = nvcv::util::CreateTensor(batch, width, height, fmt); - nvcv::Tensor imgOut = nvcv::util::CreateTensor(batch, width, height, fmt); + nvcv::Tensor imgIn = nvcv::util::CreateTensor(batch, width, height, fmt); + nvcv::Tensor imgOut = nvcv::util::CreateTensor(batch, width, height, fmt); auto inData = imgIn.exportData(); ASSERT_NE(nullptr, inData); @@ -178,17 +372,19 @@ static void tensor_correct_output_test(int batch, int height, int width, float m for (int i = 0; i < batch; i++) { - if constexpr (std::is_same::value) + if constexpr (std::is_integral_v) { - std::uniform_int_distribution rand(0, 255); + constexpr int64_t minValue = std::is_signed_v ? -1000 : 0; + constexpr int64_t maxValue = std::is_same_v ? 255 : 2000; + std::uniform_int_distribution rand(minValue, maxValue); srcVec[i].resize(height * rowStride / sizeof(datatype)); - std::generate(srcVec[i].begin(), srcVec[i].end(), [&]() { return rand(randEng); }); + std::ranges::generate(srcVec[i], [&rand, &randEng]() { return static_cast(rand(randEng)); }); } else { std::uniform_real_distribution rand(0.f, 1.f); srcVec[i].resize(height * rowStride / sizeof(datatype)); - std::generate(srcVec[i].begin(), srcVec[i].end(), [&]() { return rand(randEng); }); + std::ranges::generate(srcVec[i], [&rand, &randEng]() { return rand(randEng); }); } ASSERT_EQ(cudaSuccess, cudaMemcpy2D(inAccess->sampleData(i), inAccess->rowStride(), srcVec[i].data(), rowStride, rowStride, height, cudaMemcpyHostToDevice)); @@ -198,7 +394,9 @@ static void tensor_correct_output_test(int batch, int height, int width, float m int maxBatch = 4; unsigned long long seed = 12345; cvcuda::GaussianNoise GaussianNoiseOp(maxBatch); - EXPECT_NO_THROW(GaussianNoiseOp(stream, imgIn, imgOut, muval, sigmaval, per_channel, seed)); + SubmitGaussianNoiseRepeatedly( + calls, [&GaussianNoiseOp, &stream, &imgIn, &imgOut, &muval, &sigmaval, &per_channel, &seed] + { EXPECT_NO_THROW(GaussianNoiseOp(stream, imgIn, imgOut, muval, sigmaval, per_channel, seed)); }); EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); @@ -212,7 +410,7 @@ static void tensor_correct_output_test(int batch, int height, int width, float m rowStride, height, cudaMemcpyDeviceToHost)); std::vector goldVec(height * rowStride / sizeof(datatype)); - GaussianNoise(srcVec[i], goldVec, mu, sigma, i, per_channel); + GaussianNoise(srcVec[i], goldVec, mu, sigma, i, per_channel, fmt.numChannels(), calls - 1); EXPECT_EQ(goldVec, testVec); } @@ -227,7 +425,7 @@ TEST_P(OpGaussianNoise, tensor_correct_output) float mu = GetParamValue<3>(); float sigma = GetParamValue<4>(); bool per_channel = GetParamValue<5>(); - tensor_correct_output_test(batch, height, width, mu, sigma, per_channel); + tensor_correct_output_test(batch, height, width, mu, sigma, per_channel, nvcv::FMT_RGB8); } TEST_P(OpGaussianNoise, tensor_correct_output_float) @@ -238,23 +436,119 @@ TEST_P(OpGaussianNoise, tensor_correct_output_float) float mu = GetParamValue<3>(); float sigma = GetParamValue<4>(); bool per_channel = GetParamValue<5>(); - tensor_correct_output_test(batch, height, width, mu, sigma, per_channel); + tensor_correct_output_test(batch, height, width, mu, sigma, per_channel, nvcv::FMT_RGBf32); +} + +TEST(OpGaussianNoise, tensor_repeated_call_advances_rng_state) +{ + // Saturation makes the integer oracle depend on the random sign, not host/device rounding at half-integers. + tensor_correct_output_test(1, 480, 360, 0.f, 1e20f, true, nvcv::FMT_RGB8, 2); + tensor_correct_output_test(1, 480, 360, 0.f, 0.005f, false, nvcv::FMT_RGBf32, 2); +} + +TEST(OpGaussianNoise, tensor_variable_batch_preserves_rng_state_across_streams) +{ + constexpr int batch1 = 1; + constexpr int batch2 = 2; + constexpr int width = 263; + constexpr int height = 257; + constexpr int rowStride = width * 3; + + cudaStream_t stream1; + cudaStream_t stream2; + cudaEvent_t firstSubmitDone; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream1)); + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream2)); + ASSERT_EQ(cudaSuccess, cudaEventCreateWithFlags(&firstSubmitDone, cudaEventDisableTiming)); + + nvcv::Tensor src1 = nvcv::util::CreateTensor(batch1, width, height, nvcv::FMT_RGB8); + nvcv::Tensor dst1 = nvcv::util::CreateTensor(batch1, width, height, nvcv::FMT_RGB8); + nvcv::Tensor src2 = nvcv::util::CreateTensor(batch2, width, height, nvcv::FMT_RGB8); + nvcv::Tensor dst2 = nvcv::util::CreateTensor(batch2, width, height, nvcv::FMT_RGB8); + nvcv::Tensor ref0Dst = nvcv::util::CreateTensor(batch1, width, height, nvcv::FMT_RGB8); + nvcv::Tensor ref12Dst = nvcv::util::CreateTensor(batch2, width, height, nvcv::FMT_RGB8); + + auto src1Data = src1.exportData(); + auto src2Data = src2.exportData(); + auto dst2Data = dst2.exportData(); + auto ref0Data = ref0Dst.exportData(); + auto ref12Data = ref12Dst.exportData(); + ASSERT_TRUE(src1Data && src2Data && dst2Data && ref0Data && ref12Data); + auto src1Access = nvcv::TensorDataAccessStridedImagePlanar::Create(*src1Data); + auto src2Access = nvcv::TensorDataAccessStridedImagePlanar::Create(*src2Data); + auto dst2Access = nvcv::TensorDataAccessStridedImagePlanar::Create(*dst2Data); + auto ref0Access = nvcv::TensorDataAccessStridedImagePlanar::Create(*ref0Data); + auto ref12Access = nvcv::TensorDataAccessStridedImagePlanar::Create(*ref12Data); + ASSERT_TRUE(src1Access && src2Access && dst2Access && ref0Access && ref12Access); + + auto sample0 = MakeGaussianNoiseInput(nvcv::FMT_RGB8, width, height, 101); + auto sample1 = MakeGaussianNoiseInput(nvcv::FMT_RGB8, width, height, 202); + nvcv::test::planar::UploadInterleavedSample(*src1Access, 0, sample0, width, height, rowStride); + nvcv::test::planar::UploadInterleavedSample(*src2Access, 0, sample0, width, height, rowStride); + nvcv::test::planar::UploadInterleavedSample(*src2Access, 1, sample1, width, height, rowStride); + + nvcv::Tensor mu1 = MakeGaussianNoiseParam(batch1, 0.f); + nvcv::Tensor sigma1 = MakeGaussianNoiseParam(batch1, 1e20f); + nvcv::Tensor mu2 = MakeGaussianNoiseParam(batch2, 0.f); + nvcv::Tensor sigma2 = MakeGaussianNoiseParam(batch2, 1e20f); + constexpr auto seed = 12345ULL; + constexpr bool perCh = true; + + cvcuda::GaussianNoise op(2); + cvcuda::GaussianNoise ref0(1); + cvcuda::GaussianNoise ref12(2); + + EXPECT_NO_THROW(op(stream1, src1, dst1, mu1, sigma1, perCh, seed)); + ASSERT_EQ(cudaSuccess, cudaEventRecord(firstSubmitDone, stream1)); + ASSERT_EQ(cudaSuccess, cudaStreamWaitEvent(stream2, firstSubmitDone)); + EXPECT_NO_THROW(op(stream2, src2, dst2, mu2, sigma2, perCh, seed)); + + EXPECT_NO_THROW(ref0(stream2, src1, ref0Dst, mu1, sigma1, perCh, seed)); + EXPECT_NO_THROW(ref0(stream2, src1, ref0Dst, mu1, sigma1, perCh, seed)); + EXPECT_NO_THROW(ref12(stream2, src2, ref12Dst, mu2, sigma2, perCh, seed)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream2)); + + EXPECT_EQ(nvcv::test::planar::DownloadInterleavedSample(*dst2Access, 0, width, height, rowStride), + nvcv::test::planar::DownloadInterleavedSample(*ref0Access, 0, width, height, rowStride)); + EXPECT_EQ(nvcv::test::planar::DownloadInterleavedSample(*dst2Access, 1, width, height, rowStride), + nvcv::test::planar::DownloadInterleavedSample(*ref12Access, 1, width, height, rowStride)); + + EXPECT_EQ(cudaSuccess, cudaEventDestroy(firstSubmitDone)); + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream1)); + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream2)); +} + +TEST(OpGaussianNoise, tensor_correct_output_supported_integer_types_and_channels) +{ + const nvcv::ImageFormat fmt2U8{NVCV_IMAGE_FORMAT_2U8}; + + tensor_correct_output_test(2, 17, 19, 3.f, 25.f, false, nvcv::FMT_U8); + tensor_correct_output_test(2, 19, 17, 3.f, 25.f, false, fmt2U8); + tensor_correct_output_test(2, 17, 19, 3.f, 25.f, true, fmt2U8); + tensor_correct_output_test(2, 19, 17, 3.f, 25.f, false, nvcv::FMT_RGBA8); + tensor_correct_output_test(2, 17, 19, 3.f, 25.f, false, nvcv::FMT_U16); + tensor_correct_output_test(2, 19, 17, 3.f, 25.f, true, nvcv::FMT_U16); + tensor_correct_output_test(2, 17, 19, 3.f, 25.f, false, nvcv::FMT_S16); + tensor_correct_output_test(2, 19, 17, 3.f, 25.f, true, nvcv::FMT_S16); + tensor_correct_output_test(2, 17, 19, 3.f, 25.f, false, nvcv::FMT_S32); + tensor_correct_output_test(2, 19, 17, 3.f, 25.f, true, nvcv::FMT_S32); + tensor_correct_output_test(2, 19, 17, 0.f, 0.05f, true, nvcv::FMT_RGBAf32); } template -static void varshape_correct_output_test(int batch, int height, int width, float mu, float sigma, bool per_channel) +static void varshape_correct_output_test(int batch, int height, int width, float mu, float sigma, bool per_channel, + nvcv::ImageFormat fmt, int calls = 1) { cudaStream_t stream; EXPECT_EQ(cudaSuccess, cudaStreamCreate(&stream)); - nvcv::ImageFormat fmt = std::is_same::value ? nvcv::FMT_RGB8 : nvcv::FMT_RGBf32; - // Create input and output - std::default_random_engine randEng; - std::uniform_int_distribution rndWidth(width * 0.8, width * 1.1); - std::uniform_int_distribution rndHeight(height * 0.8, height * 1.1); + std::default_random_engine randEng; + std::uniform_int_distribution rndWidth(ScaledSize(width, 0.8), ScaledSize(width, 1.1)); + std::uniform_int_distribution rndHeight(ScaledSize(height, 0.8), ScaledSize(height, 1.1)); - std::vector imgSrc, imgDst; + std::vector imgSrc; + std::vector imgDst; for (int i = 0; i < batch; ++i) { int rw = rndWidth(randEng); @@ -301,17 +595,19 @@ static void varshape_correct_output_test(int batch, int height, int width, float int srcRowStride = srcWidth * fmt.planePixelStrideBytes(0); - if constexpr (std::is_same::value) + if constexpr (std::is_integral_v) { - std::uniform_int_distribution rand(0, 255); + constexpr int64_t minValue = std::is_signed_v ? -1000 : 0; + constexpr int64_t maxValue = std::is_same_v ? 255 : 2000; + std::uniform_int_distribution rand(minValue, maxValue); srcVec[i].resize(srcHeight * srcRowStride / sizeof(datatype)); - std::generate(srcVec[i].begin(), srcVec[i].end(), [&]() { return rand(randEng); }); + std::ranges::generate(srcVec[i], [&rand, &randEng]() { return static_cast(rand(randEng)); }); } else { std::uniform_real_distribution rand(0.f, 1.f); srcVec[i].resize(srcHeight * srcRowStride / sizeof(datatype)); - std::generate(srcVec[i].begin(), srcVec[i].end(), [&]() { return rand(randEng); }); + std::ranges::generate(srcVec[i], [&rand, &randEng]() { return rand(randEng); }); } // Copy input data to the GPU @@ -323,7 +619,9 @@ static void varshape_correct_output_test(int batch, int height, int width, float int maxBatch = 4; unsigned long long seed = 12345; cvcuda::GaussianNoise GaussianNoiseOp(maxBatch); - EXPECT_NO_THROW(GaussianNoiseOp(stream, batchSrc, batchDst, muval, sigmaval, per_channel, seed)); + SubmitGaussianNoiseRepeatedly( + calls, [&GaussianNoiseOp, &stream, &batchSrc, &batchDst, &muval, &sigmaval, &per_channel, &seed] + { EXPECT_NO_THROW(GaussianNoiseOp(stream, batchSrc, batchDst, muval, sigmaval, per_channel, seed)); }); EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); @@ -348,7 +646,7 @@ static void varshape_correct_output_test(int batch, int height, int width, float dstHeight, cudaMemcpyDeviceToHost)); std::vector goldVec(dstHeight * dstRowStride / sizeof(datatype)); - GaussianNoise(srcVec[i], goldVec, mu, sigma, i, per_channel); + GaussianNoise(srcVec[i], goldVec, mu, sigma, i, per_channel, fmt.numChannels(), calls - 1); EXPECT_EQ(goldVec, testVec); } @@ -364,7 +662,7 @@ TEST_P(OpGaussianNoise, varshape_correct_shape) float sigma = GetParamValue<4>(); bool per_channel = GetParamValue<5>(); - varshape_correct_output_test(batch, height, width, mu, sigma, per_channel); + varshape_correct_output_test(batch, height, width, mu, sigma, per_channel, nvcv::FMT_RGB8); } TEST_P(OpGaussianNoise, varshape_correct_shape_float) @@ -376,21 +674,298 @@ TEST_P(OpGaussianNoise, varshape_correct_shape_float) float sigma = GetParamValue<4>(); bool per_channel = GetParamValue<5>(); - varshape_correct_output_test(batch, height, width, mu, sigma, per_channel); + varshape_correct_output_test(batch, height, width, mu, sigma, per_channel, nvcv::FMT_RGBf32); +} + +TEST(OpGaussianNoise, varshape_repeated_call_advances_rng_state) +{ + // Saturation makes the integer oracle depend on the random sign, not host/device rounding at half-integers. + varshape_correct_output_test(2, 480, 360, 0.f, 1e20f, true, nvcv::FMT_RGB8, 2); + varshape_correct_output_test(2, 480, 360, 0.f, 0.005f, false, nvcv::FMT_RGBf32, 2); +} + +TEST(OpGaussianNoise, varshape_correct_output_supported_integer_types_and_channels) +{ + const nvcv::ImageFormat fmt2U8{NVCV_IMAGE_FORMAT_2U8}; + + varshape_correct_output_test(2, 17, 19, 3.f, 25.f, false, nvcv::FMT_U8); + varshape_correct_output_test(2, 19, 17, 3.f, 25.f, false, fmt2U8); + varshape_correct_output_test(2, 17, 19, 3.f, 25.f, true, fmt2U8); + varshape_correct_output_test(2, 19, 17, 3.f, 25.f, false, nvcv::FMT_RGBA8); + varshape_correct_output_test(2, 17, 19, 3.f, 25.f, false, nvcv::FMT_U16); + varshape_correct_output_test(2, 19, 17, 3.f, 25.f, true, nvcv::FMT_U16); + varshape_correct_output_test(2, 17, 19, 3.f, 25.f, false, nvcv::FMT_S16); + varshape_correct_output_test(2, 19, 17, 3.f, 25.f, true, nvcv::FMT_S16); + varshape_correct_output_test(2, 17, 19, 3.f, 25.f, false, nvcv::FMT_S32); + varshape_correct_output_test(2, 19, 17, 3.f, 25.f, true, nvcv::FMT_S32); + varshape_correct_output_test(2, 19, 17, 0.f, 0.05f, true, nvcv::FMT_RGBAf32); +} + +// clang-format off +NVCV_TEST_SUITE_P(OpGaussianNoisePlanar, nvcv::test::ValueList +{ + // batch, height, width, mu, sigma, per_channel, planar format, interleaved format + { 2, 48, 64, 0.f, 0.005f, false, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + { 1, 47, 65, 0.f, 0.008f, true, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + { 2, 29, 33, 0.f, 0.004f, false, nvcv::FMT_RGBAf32p, nvcv::FMT_RGBAf32}, + { 1, 27, 31, 0.f, 0.006f, true, nvcv::FMT_RGBAf32p, nvcv::FMT_RGBAf32}, + { 1, 257, 263, 3.f, 25.f, true, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + { 1, 263, 257, 0.f, 0.05f, false, nvcv::FMT_RGBAf32p, nvcv::FMT_RGBAf32}, +}); + +// clang-format on + +TEST_P(OpGaussianNoisePlanar, tensor_matches_interleaved) +{ + int batch = GetParamValue<0>(); + int height = GetParamValue<1>(); + int width = GetParamValue<2>(); + float mu = GetParamValue<3>(); + float sigma = GetParamValue<4>(); + bool per_channel = GetParamValue<5>(); + nvcv::ImageFormat planarFmt = GetParamValue<6>(); + nvcv::ImageFormat interleavedFmt = GetParamValue<7>(); + + RunGaussianNoiseTensorPlanarParity(planarFmt, interleavedFmt, width, height, batch, mu, sigma, per_channel); +} + +TEST_P(OpGaussianNoisePlanar, varshape_matches_interleaved) +{ + int batch = GetParamValue<0>(); + int height = GetParamValue<1>(); + int width = GetParamValue<2>(); + float mu = GetParamValue<3>(); + float sigma = GetParamValue<4>(); + bool per_channel = GetParamValue<5>(); + nvcv::ImageFormat planarFmt = GetParamValue<6>(); + nvcv::ImageFormat interleavedFmt = GetParamValue<7>(); + + RunGaussianNoiseVarShapePlanarParity(planarFmt, interleavedFmt, width, height, batch, mu, sigma, per_channel); +} + +TEST(OpGaussianNoisePlanar, tensor_rejects_two_channel) +{ + constexpr int batch = 1; + nvcv::Tensor muval = MakeGaussianNoiseParam(batch, 0.f); + nvcv::Tensor sigmaval = MakeGaussianNoiseParam(batch, 0.005f); + cvcuda::GaussianNoise op(batch); + + nvcv::test::planar::ExpectPlanarTensorRejected( + {batch, 2, 16, 16}, {batch, 2, 16, 16}, + [&op, &muval, &sigmaval](cudaStream_t stream, const nvcv::Tensor &src, const nvcv::Tensor &dst) + { op(stream, src, dst, muval, sigmaval, true, 12345); }); +} + +TEST(OpGaussianNoisePlanar, varshape_rejects_two_channel) +{ + constexpr int batch = 1; + nvcv::Tensor muval = MakeGaussianNoiseParam(batch, 0.f); + nvcv::Tensor sigmaval = MakeGaussianNoiseParam(batch, 0.005f); + cvcuda::GaussianNoise op(batch); + const nvcv::ImageFormat twoChannelPlanar{NVCV_DETAIL_MAKE_NONCOLOR_FMT2(PL, UNSIGNED, XY00, ASSOCIATED, X8, X8)}; + + nvcv::test::planar::ExpectVarShapeRejected( + {twoChannelPlanar}, {twoChannelPlanar}, + [&op, &muval, &sigmaval](cudaStream_t stream, const nvcv::ImageBatchVarShape &src, + const nvcv::ImageBatchVarShape &dst) + { op(stream, src, dst, muval, sigmaval, true, 12345); }); +} + +TEST(OpGaussianNoiseScalar, uint8_matches_torchvision_clip_and_wrap_semantics) +{ + constexpr int width = 2; + constexpr int height = 1; + constexpr int rowStride = width * 3; + nvcv::Tensor src = nvcv::util::CreateTensor(1, width, height, nvcv::FMT_RGB8); + nvcv::Tensor clipped = nvcv::util::CreateTensor(1, width, height, nvcv::FMT_RGB8); + nvcv::Tensor wrapped = nvcv::util::CreateTensor(1, width, height, nvcv::FMT_RGB8); + + auto srcData = src.exportData(); + auto clippedData = clipped.exportData(); + auto wrappedData = wrapped.exportData(); + ASSERT_TRUE(srcData && clippedData && wrappedData); + auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcData); + auto clippedAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*clippedData); + auto wrappedAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*wrappedData); + ASSERT_TRUE(srcAccess && clippedAccess && wrappedAccess); + + std::vector input{250, 1, 127, 0, 245, 255}; + nvcv::test::planar::UploadInterleavedSample(*srcAccess, 0, input, width, height, rowStride); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + cvcuda::GaussianNoise op(1); + op(stream, src, clipped, 10.75f, 0.f, true, 12345, true, true); + op(stream, src, wrapped, 10.75f, 0.f, true, 12345, true, false); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + EXPECT_EQ((std::vector{255, 11, 137, 10, 255, 255}), + nvcv::test::planar::DownloadInterleavedSample(*clippedAccess, 0, width, height, rowStride)); + EXPECT_EQ((std::vector{4, 11, 137, 10, 255, 9}), + nvcv::test::planar::DownloadInterleavedSample(*wrappedAccess, 0, width, height, rowStride)); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); } -TEST(OpGaussianNoise_negative, create_with_null_handle) +TEST(OpGaussianNoiseScalar, float_clip_is_optional) +{ + constexpr int width = 1; + constexpr int height = 1; + constexpr int rowStride = 3 * sizeof(float); + nvcv::Tensor src = nvcv::util::CreateTensor(1, width, height, nvcv::FMT_RGBf32); + nvcv::Tensor clipped = nvcv::util::CreateTensor(1, width, height, nvcv::FMT_RGBf32); + nvcv::Tensor raw = nvcv::util::CreateTensor(1, width, height, nvcv::FMT_RGBf32); + + auto srcData = src.exportData(); + auto clippedData = clipped.exportData(); + auto rawData = raw.exportData(); + ASSERT_TRUE(srcData && clippedData && rawData); + auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcData); + auto clippedAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*clippedData); + auto rawAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*rawData); + ASSERT_TRUE(srcAccess && clippedAccess && rawAccess); + + std::vector input{0.9f, -0.2f, 0.5f}; + std::vector inputBytes(input.size() * sizeof(float)); + std::memcpy(inputBytes.data(), input.data(), inputBytes.size()); + nvcv::test::planar::UploadInterleavedSample(*srcAccess, 0, inputBytes, width, height, rowStride); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + cvcuda::GaussianNoise op(1); + op(stream, src, clipped, 0.3f, 0.f, true, 12345, true, true); + op(stream, src, raw, 0.3f, 0.f, true, 12345, true, false); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + auto clippedBytes = nvcv::test::planar::DownloadInterleavedSample(*clippedAccess, 0, width, height, rowStride); + auto rawBytes = nvcv::test::planar::DownloadInterleavedSample(*rawAccess, 0, width, height, rowStride); + std::vector clippedValues(input.size()); + std::vector rawValues(input.size()); + std::memcpy(clippedValues.data(), clippedBytes.data(), clippedBytes.size()); + std::memcpy(rawValues.data(), rawBytes.data(), rawBytes.size()); + + EXPECT_FLOAT_EQ(1.f, clippedValues[0]); + EXPECT_FLOAT_EQ(0.1f, clippedValues[1]); + EXPECT_FLOAT_EQ(0.8f, clippedValues[2]); + EXPECT_FLOAT_EQ(1.2f, rawValues[0]); + EXPECT_FLOAT_EQ(0.1f, rawValues[1]); + EXPECT_FLOAT_EQ(0.8f, rawValues[2]); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpGaussianNoiseScalar, explicit_seed_reseeds_while_implicit_seed_advances) +{ + constexpr int width = 37; + constexpr int height = 29; + constexpr int rowStride = width * 3 * sizeof(float); + nvcv::Tensor src = nvcv::util::CreateTensor(1, width, height, nvcv::FMT_RGBf32); + nvcv::Tensor first = nvcv::util::CreateTensor(1, width, height, nvcv::FMT_RGBf32); + nvcv::Tensor repeated = nvcv::util::CreateTensor(1, width, height, nvcv::FMT_RGBf32); + nvcv::Tensor advanced = nvcv::util::CreateTensor(1, width, height, nvcv::FMT_RGBf32); + + auto srcData = src.exportData(); + auto firstData = first.exportData(); + auto repeatedData = repeated.exportData(); + auto advancedData = advanced.exportData(); + ASSERT_TRUE(srcData && firstData && repeatedData && advancedData); + auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcData); + auto firstAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*firstData); + auto repeatedAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*repeatedData); + auto advancedAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*advancedData); + ASSERT_TRUE(srcAccess && firstAccess && repeatedAccess && advancedAccess); + + auto input = MakeGaussianNoiseInput(nvcv::FMT_RGBf32, width, height, 7); + nvcv::test::planar::UploadInterleavedSample(*srcAccess, 0, input, width, height, rowStride); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + cvcuda::GaussianNoise op(1); + op(stream, src, first, 0.f, 0.05f, true, 98765, true, true); + op(stream, src, repeated, 0.f, 0.05f, true, 98765, true, true); + op(stream, src, advanced, 0.f, 0.05f, true, 98765, false, true); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + auto firstBytes = nvcv::test::planar::DownloadInterleavedSample(*firstAccess, 0, width, height, rowStride); + auto repeatedBytes = nvcv::test::planar::DownloadInterleavedSample(*repeatedAccess, 0, width, height, rowStride); + auto advancedBytes = nvcv::test::planar::DownloadInterleavedSample(*advancedAccess, 0, width, height, rowStride); + EXPECT_EQ(firstBytes, repeatedBytes); + EXPECT_NE(repeatedBytes, advancedBytes); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpGaussianNoiseScalar, implicit_seed_continues_after_explicit_seed) +{ + constexpr int width = 37; + constexpr int height = 29; + constexpr int rowStride = width * 3 * sizeof(float); + nvcv::Tensor src = nvcv::util::CreateTensor(1, width, height, nvcv::FMT_RGBf32); + nvcv::Tensor primedA = nvcv::util::CreateTensor(1, width, height, nvcv::FMT_RGBf32); + nvcv::Tensor primedB = nvcv::util::CreateTensor(1, width, height, nvcv::FMT_RGBf32); + nvcv::Tensor continuedA = nvcv::util::CreateTensor(1, width, height, nvcv::FMT_RGBf32); + nvcv::Tensor continuedB = nvcv::util::CreateTensor(1, width, height, nvcv::FMT_RGBf32); + + auto srcData = src.exportData(); + auto continuedAData = continuedA.exportData(); + auto continuedBData = continuedB.exportData(); + ASSERT_TRUE(srcData && continuedAData && continuedBData); + auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcData); + auto continuedAAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*continuedAData); + auto continuedBAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*continuedBData); + ASSERT_TRUE(srcAccess && continuedAAccess && continuedBAccess); + + auto input = MakeGaussianNoiseInput(nvcv::FMT_RGBf32, width, height, 7); + nvcv::test::planar::UploadInterleavedSample(*srcAccess, 0, input, width, height, rowStride); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + cvcuda::GaussianNoise implicitSeedOp(1); + cvcuda::GaussianNoise explicitSeedOp(1); + implicitSeedOp(stream, src, primedA, 0.f, 0.05f, true, 98765, true, true); + implicitSeedOp(stream, src, continuedA, 0.f, 0.05f, true, 0, false, true); + explicitSeedOp(stream, src, primedB, 0.f, 0.05f, true, 98765, true, true); + explicitSeedOp(stream, src, continuedB, 0.f, 0.05f, true, 98765, false, true); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + EXPECT_EQ(nvcv::test::planar::DownloadInterleavedSample(*continuedAAccess, 0, width, height, rowStride), + nvcv::test::planar::DownloadInterleavedSample(*continuedBAccess, 0, width, height, rowStride)); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpGaussianNoiseScalar, planar_matches_interleaved) +{ + cvcuda::GaussianNoise op(2); + nvcv::test::planar::RunTensorParity( + nvcv::FMT_RGB8p, nvcv::FMT_RGB8, 31, 27, 31, 27, 2, + [&op](cudaStream_t stream, const nvcv::Tensor &src, const nvcv::Tensor &dst, nvcv::ImageFormat) + { op(stream, src, dst, 3.f, 25.f, true, 12345, true, true); }); +} + +TEST(OpGaussianNoiseScalar_Negative, rejects_negative_sigma_and_unsupported_dtype) +{ + nvcv::Tensor srcU8 = nvcv::util::CreateTensor(1, 4, 4, nvcv::FMT_RGB8); + nvcv::Tensor dstU8 = nvcv::util::CreateTensor(1, 4, 4, nvcv::FMT_RGB8); + nvcv::Tensor srcF16 = nvcv::util::CreateTensor(1, 4, 4, nvcv::FMT_RGBf16); + nvcv::Tensor dstF16 = nvcv::util::CreateTensor(1, 4, 4, nvcv::FMT_RGBf16); + cvcuda::GaussianNoise op(1); + + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&op, &srcU8, &dstU8] { op(nullptr, srcU8, dstU8, 0.f, -1.f, true, 0, true, true); })); + EXPECT_EQ( + NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&op, &srcF16, &dstF16] { op(nullptr, srcF16, dstF16, 0.f, 1.f, true, 0, true, true); })); +} + +TEST(OpGaussianNoise_Negative, create_with_null_handle) { EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, cvcudaGaussianNoiseCreate(nullptr, 10)); } -TEST(OpGaussianNoise_negative, create_with_negative_batch) +TEST(OpGaussianNoise_Negative, create_with_negative_batch) { NVCVOperatorHandle opHandle; EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, cvcudaGaussianNoiseCreate(&opHandle, -1)); } -TEST(OpGaussianNoise_negative, invalid_mu_sigma_layout) +TEST(OpGaussianNoise_Negative, invalid_mu_sigma_layout) { nvcv::Tensor imgIn( { @@ -428,13 +1003,90 @@ TEST(OpGaussianNoise_negative, invalid_mu_sigma_layout) unsigned long long seed = 12345; cvcuda::GaussianNoise GaussianNoiseOp(maxBatch); EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, - nvcv::ProtectCall([&] { GaussianNoiseOp(NULL, imgIn, imgOut, invalidMuval, sigmaval, false, seed); })); + nvcv::ProtectCall([&GaussianNoiseOp, &imgIn, &imgOut, &invalidMuval, &sigmaval, &seed] + { GaussianNoiseOp(nullptr, imgIn, imgOut, invalidMuval, sigmaval, false, seed); })); EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, - nvcv::ProtectCall([&] { GaussianNoiseOp(NULL, imgIn, imgOut, muval, invalidSigmaval, false, seed); })); + nvcv::ProtectCall([&GaussianNoiseOp, &imgIn, &imgOut, &muval, &invalidSigmaval, &seed] + { GaussianNoiseOp(nullptr, imgIn, imgOut, muval, invalidSigmaval, false, seed); })); +} + +static void FillGaussianNoiseParams(const nvcv::Tensor &mu, const nvcv::Tensor &sigma, int batch, cudaStream_t stream) +{ + auto muData = mu.exportData(); + auto sigmaData = sigma.exportData(); + + ASSERT_NE(nullptr, muData); + ASSERT_NE(nullptr, sigmaData); + + std::vector muVec(batch, 0.f); + std::vector sigmaVec(batch, 0.005f); + + ASSERT_EQ(cudaSuccess, cudaMemcpyAsync(muData->basePtr(), muVec.data(), muVec.size() * sizeof(float), + cudaMemcpyHostToDevice, stream)); + ASSERT_EQ(cudaSuccess, cudaMemcpyAsync(sigmaData->basePtr(), sigmaVec.data(), sigmaVec.size() * sizeof(float), + cudaMemcpyHostToDevice, stream)); +} + +TEST(OpGaussianNoise_Negative, tensor_batch_exceeds_maxBatch) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + constexpr int batch = 2; + constexpr int maxBatch = 1; + + nvcv::Tensor imgIn = nvcv::util::CreateTensor(batch, 4, 4, nvcv::FMT_RGB8); + nvcv::Tensor imgOut = nvcv::util::CreateTensor(batch, 4, 4, nvcv::FMT_RGB8); + nvcv::Tensor muval({{batch}, "N"}, nvcv::TYPE_F32); + nvcv::Tensor sigmaval({{batch}, "N"}, nvcv::TYPE_F32); + + FillGaussianNoiseParams(muval, sigmaval, batch, stream); + + cvcuda::GaussianNoise gaussianNoiseOp(maxBatch); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&gaussianNoiseOp, &stream, &imgIn, &imgOut, &muval, &sigmaval] + { gaussianNoiseOp(stream, imgIn, imgOut, muval, sigmaval, false, 12345); })); + + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpGaussianNoiseVarShape_Negative, varshape_batch_exceeds_maxBatch) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + constexpr int batch = 2; + constexpr int maxBatch = 1; + + std::vector imgSrc; + std::vector imgDst; + for (int i = 0; i < batch; ++i) + { + imgSrc.emplace_back(nvcv::Size2D{4, 4}, nvcv::FMT_RGB8); + imgDst.emplace_back(nvcv::Size2D{4, 4}, nvcv::FMT_RGB8); + } + + nvcv::ImageBatchVarShape batchSrc(batch); + batchSrc.pushBack(imgSrc.begin(), imgSrc.end()); + + nvcv::ImageBatchVarShape batchDst(batch); + batchDst.pushBack(imgDst.begin(), imgDst.end()); + + nvcv::Tensor muval({{batch}, "N"}, nvcv::TYPE_F32); + nvcv::Tensor sigmaval({{batch}, "N"}, nvcv::TYPE_F32); + + FillGaussianNoiseParams(muval, sigmaval, batch, stream); + + cvcuda::GaussianNoise gaussianNoiseOp(maxBatch); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&gaussianNoiseOp, &stream, &batchSrc, &batchDst, &muval, &sigmaval] + { gaussianNoiseOp(stream, batchSrc, batchDst, muval, sigmaval, false, 12345); })); + + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); } // clang-format off -NVCV_TEST_SUITE_P(OpGaussianNoise_negative, nvcv::test::ValueList +NVCV_TEST_SUITE_P(OpGaussianNoise_Negative, nvcv::test::ValueList { // in_layout, in_data_type, out_layout, out_data_type, mu_layout, mu_data_type, sigma_layout, sigma_data_type, expected_return_status { "CHW", nvcv::TYPE_U8, "HWC", nvcv::TYPE_U8, "N", nvcv::TYPE_F32, "N", nvcv::TYPE_F32}, @@ -448,7 +1100,7 @@ NVCV_TEST_SUITE_P(OpGaussianNoise_negative, nvcv::test::ValueList +NVCV_TEST_SUITE_P(OpGaussianNoiseVarShape_Negative, nvcv::test::ValueList { // inFmt, outFmt, mu_layout, mu_data_type, sigma_layout, sigma_data_type {nvcv::FMT_RGB8p, nvcv::FMT_RGB8, "N", nvcv::TYPE_F32, "N", nvcv::TYPE_F32}, @@ -463,7 +1115,7 @@ NVCV_TEST_SUITE_P(OpGaussianNoiseVarshape_negative, nvcv::test::ValueList(); nvcv::DataType in_data_type = GetParamValue<1>(); @@ -496,10 +1148,11 @@ TEST_P(OpGaussianNoise_negative, op) unsigned long long seed = 12345; cvcuda::GaussianNoise GaussianNoiseOp(maxBatch); EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, - nvcv::ProtectCall([&] { GaussianNoiseOp(NULL, imgIn, imgOut, muval, sigmaval, false, seed); })); + nvcv::ProtectCall([&GaussianNoiseOp, &imgIn, &imgOut, &muval, &sigmaval, &seed] + { GaussianNoiseOp(nullptr, imgIn, imgOut, muval, sigmaval, false, seed); })); } -TEST_P(OpGaussianNoiseVarshape_negative, op) +TEST_P(OpGaussianNoiseVarShape_Negative, op) { nvcv::ImageFormat inFmt = GetParamValue<0>(); nvcv::ImageFormat outFmt = GetParamValue<1>(); @@ -516,11 +1169,12 @@ TEST_P(OpGaussianNoiseVarshape_negative, op) EXPECT_EQ(cudaSuccess, cudaStreamCreate(&stream)); // Create input and output - std::default_random_engine randEng; - std::uniform_int_distribution rndWidth(width * 0.8, width * 1.1); - std::uniform_int_distribution rndHeight(height * 0.8, height * 1.1); + std::default_random_engine randEng; + std::uniform_int_distribution rndWidth(ScaledSize(width, 0.8), ScaledSize(width, 1.1)); + std::uniform_int_distribution rndHeight(ScaledSize(height, 0.8), ScaledSize(height, 1.1)); - std::vector imgSrc, imgDst; + std::vector imgSrc; + std::vector imgDst; for (int i = 0; i < batch; ++i) { int rw = rndWidth(randEng); @@ -548,8 +1202,11 @@ TEST_P(OpGaussianNoiseVarshape_negative, op) unsigned long long seed = 12345; cvcuda::GaussianNoise GaussianNoiseOp(maxBatch); EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, - nvcv::ProtectCall([&] { GaussianNoiseOp(stream, batchSrc, batchDst, muVal, sigmaVal, false, seed); })); + nvcv::ProtectCall([&GaussianNoiseOp, &stream, &batchSrc, &batchDst, &muVal, &sigmaVal, &seed] + { GaussianNoiseOp(stream, batchSrc, batchDst, muVal, sigmaVal, false, seed); })); EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); } + +#undef NVCV_IMAGE_FORMAT_2U8 diff --git a/tests/cvcuda/system/TestOpHQResize.cpp b/tests/cvcuda/system/TestOpHQResize.cpp index 24ab2685b..0b8e512e3 100644 --- a/tests/cvcuda/system/TestOpHQResize.cpp +++ b/tests/cvcuda/system/TestOpHQResize.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -30,8 +30,11 @@ #include #include +#include +#include #include #include +#include #include #include @@ -126,12 +129,12 @@ struct CpuSample return m_shape; } - int numSamples() + int numSamples() const { return m_numSamples; } - int numChannels() + int numChannels() const { return m_numChannels; } @@ -154,6 +157,23 @@ struct CpuSample int m_numChannels; }; +template +double CompareTolerance() +{ + if constexpr (std::is_integral_v) + { + return std::is_same_v ? 1 : 10; + } + else if constexpr (!std::is_integral_v) + { + return 1e-4; + } + else + { + return std::is_same_v ? 0.1 : 6; + } +} + inline CpuSample GetIntermediate(int numSamples, int2 shape, int numChannels) { int64_t size = sizeof(float) * numSamples * shape.y * shape.x * numChannels; @@ -197,8 +217,9 @@ struct FilterCubic float operator[](int k) const { - float x = 4 * (k - (size() - 1) * 0.5f) / (size() - 1); - x = fabsf(x); + float x + = 4.f * (static_cast(k) - static_cast(size() - 1) * 0.5f) / static_cast(size() - 1); + x = fabsf(x); if (x >= 2) return 0; @@ -220,7 +241,8 @@ struct FilterGaussian float operator[](int k) const { - float x = 4 * (k - (size() - 1) * 0.5f) / (size() - 1); + float x + = 4.f * (static_cast(k) - static_cast(size() - 1) * 0.5f) / static_cast(size() - 1); return expf(-x * x); } }; @@ -237,30 +259,30 @@ struct FilterLanczos float operator[](int k) const { - float x = 2 * kLanczosA * (k - (size() - 1) * 0.5f) / (size() - 1); - if (fabsf(x) >= kLanczosA) + float x = 2.f * static_cast(kLanczosA) * (static_cast(k) - static_cast(size() - 1) * 0.5f) + / static_cast(size() - 1); + if (fabsf(x) >= static_cast(kLanczosA)) return 0.0f; - return nvcv::util::sinc(x) * nvcv::util::sinc(x / kLanczosA); + return nvcv::util::sinc(x) * nvcv::util::sinc(x / static_cast(kLanczosA)); } }; -template +template // NOSONAR: this small wrapper exposes a filter object with a call operator under test. struct Filter { - Filter(float support) - : m_filter{} - , m_support{support} + explicit Filter(float support) + : m_support{support} { } - float support() const + int support() const { - return std::ceil(m_support); + return static_cast(std::ceil(m_support)); } float scale() const { - return (m_filter.size() - 1) / m_support; + return static_cast(m_filter.size() - 1) / m_support; } float anchor() const @@ -272,19 +294,19 @@ struct Filter { if (!(x > -1)) return 0; - if (x >= m_filter.size()) + if (x >= static_cast(m_filter.size())) return 0; - int x0 = std::floor(x); + auto x0 = static_cast(std::floor(x)); int x1 = x0 + 1; - float d = x - x0; + float d = x - static_cast(x0); float f0 = x0 < 0 ? 0.0f : m_filter[x0]; float f1 = x1 >= m_filter.size() ? 0.0f : m_filter[x1]; - return f0 + d * (f1 - f0); + return f0 + d * (f1 - f0); // NOSONAR: std::lerp is C++20. } private: - FilterType m_filter; - float m_support; + [[no_unique_address]] FilterType m_filter{}; + float m_support; }; template @@ -297,16 +319,18 @@ void RunNN(int axis, CpuSample &outTensorCpu, CpuSample(inSize) / outSize; + const auto axisScale = static_cast(inSize) / static_cast(outSize); const float axisOrigin = 0.5f * axisScale; for (int sampleIdx = 0; sampleIdx < numSamples; sampleIdx++) { ForAllInRoi(roi, - [&](const cuda::MakeType outIdx) + [axis, axisScale, axisOrigin, inSize, numChannels, sampleIdx, &outTensorCpu, + &inTensorCpu](const cuda::MakeType outIdx) { auto inIdx = outIdx; - int inAxis = std::floor(cuda::GetElement(outIdx, axis) * axisScale + axisOrigin); - inAxis = inAxis < 0 ? 0 : (inAxis > inSize - 1 ? inSize - 1 : inAxis); + auto inAxis = static_cast( + std::floor(static_cast(cuda::GetElement(outIdx, axis)) * axisScale + axisOrigin)); + inAxis = std::clamp(inAxis, 0, inSize - 1); cuda::GetElement(inIdx, axis) = inAxis; for (int c = 0; c < numChannels; c++) { @@ -327,21 +351,23 @@ void RunLinear(int axis, CpuSample &outTensorCpu, CpuSample const auto outShape = outTensorCpu.shape(); const int inSize = cuda::GetElement(inShape, axis); const int outSize = cuda::GetElement(outShape, axis); - const float axisScale = static_cast(inSize) / outSize; + const auto axisScale = static_cast(inSize) / static_cast(outSize); const float axisOrigin = 0.5f * axisScale - 0.5f; for (int sampleIdx = 0; sampleIdx < numSamples; sampleIdx++) { ForAllInRoi(roi, - [&](const cuda::MakeType outIdx) + [axis, axisScale, axisOrigin, inSize, numChannels, sampleIdx, &outTensorCpu, + &inTensorCpu](const cuda::MakeType outIdx) { - const float inAxis0f = cuda::GetElement(outIdx, axis) * axisScale + axisOrigin; - int inAxis0 = std::floor(inAxis0f); - int inAxis1 = inAxis0 + 1; - const float q = inAxis0f - inAxis0; - inAxis0 = inAxis0 < 0 ? 0 : (inAxis0 > inSize - 1 ? inSize - 1 : inAxis0); - inAxis1 = inAxis1 < 0 ? 0 : (inAxis1 > inSize - 1 ? inSize - 1 : inAxis1); - auto inIdx0 = outIdx; - auto inIdx1 = outIdx; + const auto inAxis0f + = static_cast(cuda::GetElement(outIdx, axis)) * axisScale + axisOrigin; + auto inAxis0 = static_cast(std::floor(inAxis0f)); + int inAxis1 = inAxis0 + 1; + const float q = inAxis0f - static_cast(inAxis0); + inAxis0 = std::clamp(inAxis0, 0, inSize - 1); + inAxis1 = std::clamp(inAxis1, 0, inSize - 1); + auto inIdx0 = outIdx; + auto inIdx1 = outIdx; cuda::GetElement(inIdx0, axis) = inAxis0; cuda::GetElement(inIdx1, axis) = inAxis1; for (int c = 0; c < numChannels; c++) @@ -367,29 +393,31 @@ void RunFilter(int axis, CpuSample &outTensorCpu, CpuSample const int outSize = cuda::GetElement(outShape, axis); const int filterSupport = filter.support(); const float filterStep = filter.scale(); - const float axisScale = static_cast(inSize) / outSize; + const auto axisScale = static_cast(inSize) / static_cast(outSize); const float axisOrigin = 0.5f * axisScale - 0.5f - filter.anchor(); for (int sampleIdx = 0; sampleIdx < numSamples; sampleIdx++) { ForAllInRoi(roi, - [&](const cuda::MakeType outIdx) + [axis, axisScale, axisOrigin, filterStep, filterSupport, inSize, numChannels, sampleIdx, &filter, + &outTensorCpu, &inTensorCpu](const cuda::MakeType outIdx) { - const float inAxis0f = cuda::GetElement(outIdx, axis) * axisScale + axisOrigin; - int inAxis0 = std::ceil(inAxis0f); - const float fStart = (inAxis0 - inAxis0f) * filterStep; + const auto inAxis0f + = static_cast(cuda::GetElement(outIdx, axis)) * axisScale + axisOrigin; + auto inAxis0 = static_cast(std::ceil(inAxis0f)); + const float fStart = (static_cast(inAxis0) - inAxis0f) * filterStep; for (int c = 0; c < numChannels; c++) { float tmp = 0; float norm = 0; for (int k = 0; k < filterSupport; k++) { - int inAxis = inAxis0 + k; - inAxis = inAxis < 0 ? 0 : (inAxis > inSize - 1 ? inSize - 1 : inAxis); - auto inIdx = outIdx; + int inAxis = inAxis0 + k; + inAxis = std::clamp(inAxis, 0, inSize - 1); + auto inIdx = outIdx; cuda::GetElement(inIdx, axis) = inAxis; const InBT inVal = inTensorCpu.get(sampleIdx, inIdx, c); - float coeff = filter(fStart + k * filterStep); + float coeff = filter(fStart + static_cast(k) * filterStep); tmp = std::fmaf(inVal, coeff, tmp); norm += coeff; } @@ -403,37 +431,37 @@ template void RunFilter(int axis, CpuSample &outTensorCpu, CpuSample &inTensorCpu, const NVCVInterpolationType interpolation, bool antialias, Roi roi) { - const auto inShape = inTensorCpu.shape(); - const auto outShape = outTensorCpu.shape(); - const float inSize = cuda::GetElement(inShape, axis); - const float outSize = cuda::GetElement(outShape, axis); + const auto inShape = inTensorCpu.shape(); + const auto outShape = outTensorCpu.shape(); + const auto inSize = static_cast(cuda::GetElement(inShape, axis)); + const auto outSize = static_cast(cuda::GetElement(outShape, axis)); switch (interpolation) { case NVCV_INTERP_LINEAR: { - float radius = antialias ? inSize / outSize : 1; - float support = std::max(1.0f, 2 * radius); + float radius = antialias ? inSize / outSize : 1.f; + float support = std::max(1.0f, 2.f * radius); RunFilter(axis, outTensorCpu, inTensorCpu, Filter{support}, roi); } break; case NVCV_INTERP_CUBIC: { - float radius = antialias ? (2 * inSize / outSize) : 2; - float support = std::max(4.0f, 2 * radius); + float radius = antialias ? (2.f * inSize / outSize) : 2.f; + float support = std::max(4.0f, 2.f * radius); RunFilter(axis, outTensorCpu, inTensorCpu, Filter{support}, roi); } break; case NVCV_INTERP_GAUSSIAN: { - float radius = antialias ? inSize / outSize : 1; - float support = std::max(1.0f, 2 * radius); + float radius = antialias ? inSize / outSize : 1.f; + float support = std::max(1.0f, 2.f * radius); RunFilter(axis, outTensorCpu, inTensorCpu, Filter{support}, roi); } break; case NVCV_INTERP_LANCZOS: { - float radius = antialias ? (3 * inSize / outSize) : 3; - float support = std::max(6.0f, 2 * radius); + float radius = antialias ? (3.f * inSize / outSize) : 3.f; + float support = std::max(6.0f, 2.f * radius); RunFilter(axis, outTensorCpu, inTensorCpu, Filter{support}, roi); } break; @@ -490,7 +518,10 @@ void Resize(CpuSample &refTensorCpu, CpuSample &inTensorCpu, const int2 interShape = {outShape.x, inShape.y}; Roi<2> inRoi = inRoiArg.value_or(FullRoi<2>(inShape)); Roi<2> outRoi = outRoiArg.value_or(FullRoi<2>(outShape)); - auto interRoi = Roi<2>(int2(outRoi.origin.x, inRoi.origin.y), int2(outRoi.shape.x, inRoi.shape.y)); + auto interRoi = Roi<2>{ + int2{outRoi.origin.x, inRoi.origin.y}, + int2{ outRoi.shape.x, inRoi.shape.y} + }; auto intermediateTensor = GetIntermediate(numSamples, interShape, numChannels); RunPass(0, intermediateTensor, inTensorCpu, minInterpolation, magInterpolation, antialias, interRoi); @@ -514,8 +545,10 @@ void Resize(CpuSample &refTensorCpu, CpuSample &inTensorCpu, int3{outRoi.origin.x, inRoi.origin.y, inRoi.origin.z}, int3{ outRoi.shape.x, inRoi.shape.y, inRoi.shape.z} }; - auto interRoi1 = Roi<3>(int3{outRoi.origin.x, outRoi.origin.y, inRoi.origin.z}, - int3{outRoi.shape.x, outRoi.shape.y, inRoi.shape.z}); + auto interRoi1 = Roi<3>{ + int3{outRoi.origin.x, outRoi.origin.y, inRoi.origin.z}, + int3{ outRoi.shape.x, outRoi.shape.y, inRoi.shape.z} + }; auto intermediateTensor0 = GetIntermediate(numSamples, interShape0, numChannels); RunPass(0, intermediateTensor0, inTensorCpu, minInterpolation, magInterpolation, antialias, interRoi0); @@ -524,52 +557,64 @@ void Resize(CpuSample &refTensorCpu, CpuSample &inTensorCpu, RunPass(2, refTensorCpu, intermediateTensor1, minInterpolation, magInterpolation, antialias, outRoi); } -template -void Compare(CpuSample &tensor, CpuSample &refTensor, bool antialias, - std::optional> roi_arg = std::nullopt) +template +void CompareElementWise(CpuSample &tensor, CpuSample &refTensor, + std::optional> roi_arg, Cb &&cb) { - int numSamples = tensor.numSamples(); - int numChannels = tensor.numChannels(); + const int numSamples = tensor.numSamples(); + const int numChannels = tensor.numChannels(); const auto shape = tensor.shape(); const auto roi = roi_arg.value_or(FullRoi(shape)); ASSERT_EQ(numSamples, refTensor.numSamples()); ASSERT_EQ(numChannels, refTensor.numChannels()); ASSERT_EQ(shape, refTensor.shape()); - double err = 0; - int64_t vol = 0; + for (int sampleIdx = 0; sampleIdx < numSamples; sampleIdx++) { ForAllInRoi(roi, - [&](const cuda::MakeType idx) + [numChannels, sampleIdx, &cb](const cuda::MakeType idx) { for (int c = 0; c < numChannels; c++) { - const BT val = tensor.get(sampleIdx, idx, c); - const BT refVal = refTensor.get(sampleIdx, idx, c); - err += abs(val - refVal); - vol += 1; - - if (std::is_integral_v) // uchar -> uchar, short -> short, ushort -> ushort - { - ASSERT_NEAR(val, refVal, (std::is_same_v ? 1 : 10)); // uchar : short, ushort - } - else // output type is float - { - if (!std::is_integral_v) // float -> float - { - ASSERT_NEAR(val, refVal, 1e-4); - } - else // [uchar, short, ushort] -> float - { - ASSERT_NEAR(val, refVal, (std::is_same_v ? 0.1 : 6)); - } - } + cb(sampleIdx, idx, c); } }); } - double mean_err = err / vol; +} + +template +void Compare(CpuSample &tensor, CpuSample &refTensor, bool antialias, + std::optional> roi_arg = std::nullopt) +{ + double err = 0; + int64_t vol = 0; + CompareElementWise( + tensor, refTensor, roi_arg, + [&tensor, &refTensor, &err, &vol](int sampleIdx, const cuda::MakeType idx, int c) + { + const BT val = tensor.get(sampleIdx, idx, c); + const BT refVal = refTensor.get(sampleIdx, idx, c); + err += abs(val - refVal); + vol += 1; + + const double tolerance = CompareTolerance(); + ASSERT_NEAR(val, refVal, tolerance); + }); + double mean_err = err / static_cast(vol); ASSERT_LE(mean_err, antialias ? 0.1 : 0.4); } + +template +void CompareExact(CpuSample &tensor, CpuSample &refTensor, + std::optional> roi_arg = std::nullopt) +{ + CompareElementWise(tensor, refTensor, roi_arg, + [&tensor, &refTensor](int sampleIdx, const cuda::MakeType idx, int c) + { + ASSERT_EQ(tensor.get(sampleIdx, idx, c), refTensor.get(sampleIdx, idx, c)) + << "sampleIdx=" << sampleIdx << ", channel=" << c; + }); +} } // namespace baseline inline void GetMaxShape(HQResizeTensorShapeI &ret, const HQResizeTensorShapeI &other) @@ -582,7 +627,8 @@ inline void GetMaxShape(HQResizeTensorShapeI &ret, const HQResizeTensorShapeI &o } } -inline void GetMaxShape(HQResizeTensorShapeI &ret, const HQResizeTensorShapeI *shapes, int numSamples) +inline void GetMaxShape( // NOSONAR: std::span is C++20. + HQResizeTensorShapeI &ret, const HQResizeTensorShapeI *shapes, int numSamples) { if (numSamples > 0) { @@ -688,8 +734,49 @@ NVCV_TYPED_TEST_SUITE( NVCV_TEST_ROW(1, NVCV_SHAPE2D(8192, 8192), NVCV_SHAPE2D(32, 32), 1, uchar, uchar, NVCV_INTERP_LANCZOS)>); +inline long3 Tensor2DStrides(const nvcv::TensorDataStridedCuda &data, int2 shape, int numSamples, int numChannels) +{ + auto access = nvcv::TensorDataAccessStridedImagePlanar::Create(data); + EXPECT_TRUE(access); + if (!access) + { + return {}; + } + EXPECT_EQ(access->numSamples(), numSamples); + EXPECT_EQ(access->numChannels(), numChannels); + return {access->colStride(), access->rowStride(), + access->sampleStride() == 0 ? access->rowStride() * shape.y : access->sampleStride()}; +} + +template +struct Tensor2DTestSamples +{ + long3 inStrides; + long3 outStrides; + baseline::CpuSample inCpu; + baseline::CpuSample outCpu; + baseline::CpuSample refCpu; +}; + +template +Tensor2DTestSamples MakeTensor2DTestSamples(const nvcv::TensorDataStridedCuda &inData, + const nvcv::TensorDataStridedCuda &outData, int2 inShape, + int2 outShape, int numSamples, int numChannels) +{ + const long3 inStrides = Tensor2DStrides(inData, inShape, numSamples, numChannels); + const long3 outStrides = Tensor2DStrides(outData, outShape, numSamples, numChannels); + + return { + inStrides, + outStrides, + baseline::CpuSample{ inStrides.z * numSamples, inStrides, numSamples, inShape, numChannels}, + baseline::CpuSample{outStrides.z * numSamples, outStrides, numSamples, outShape, numChannels}, + baseline::CpuSample{outStrides.z * numSamples, outStrides, numSamples, outShape, numChannels}, + }; +} + template -void TestTensor(bool antialias) +void TestTensor(bool antialias, bool exactOutput = false) { const int numSamples = ttype::GetValue; const int2 inShape = ttype::GetValue; @@ -719,25 +806,7 @@ void TestTensor(bool antialias) auto inData = inTensor.exportData(); auto outData = outTensor.exportData(); ASSERT_TRUE(inData && outData); - - auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*inData); - auto outAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*outData); - ASSERT_TRUE(inAccess && outAccess); - long3 inStrides{inAccess->colStride(), inAccess->rowStride(), - inAccess->sampleStride() == 0 ? inAccess->rowStride() * inShape.y : inAccess->sampleStride()}; - long3 outStrides{outAccess->colStride(), outAccess->rowStride(), - outAccess->sampleStride() == 0 ? outAccess->rowStride() * outShape.y : outAccess->sampleStride()}; - - ASSERT_EQ(inAccess->numSamples(), numSamples); - ASSERT_EQ(inAccess->numChannels(), numChannels); - ASSERT_EQ(outAccess->numChannels(), numChannels); - ASSERT_EQ(outAccess->numSamples(), numSamples); - - baseline::CpuSample inTensorCpu(inStrides.z * numSamples, inStrides, numSamples, inShape, numChannels); - baseline::CpuSample outTensorCpu(outStrides.z * numSamples, outStrides, numSamples, outShape, - numChannels); - baseline::CpuSample refTensorCpu(outStrides.z * numSamples, outStrides, numSamples, outShape, - numChannels); + auto samples = MakeTensor2DTestSamples(*inData, *outData, inShape, outShape, numSamples, numChannels); uniform_distribution rand(InBT{0}, std::is_integral_v ? cuda::TypeTraits::max : InBT{1}); std::mt19937_64 rng(12345); @@ -745,11 +814,11 @@ void TestTensor(bool antialias) for (int sampleIdx = 0; sampleIdx < numSamples; sampleIdx++) { baseline::ForAllInRoi(inRoi, - [&](int2 idx) + [sampleIdx, &samples, &rand, &rng](int2 idx) { for (int c = 0; c < numChannels; c++) { - inTensorCpu.get(sampleIdx, idx, c) = rand(rng); + samples.inCpu.get(sampleIdx, idx, c) = rand(rng); } }); } @@ -772,14 +841,21 @@ void TestTensor(bool antialias) numSamples, inShapeDesc, outShapeDesc, interpolation, interpolation, antialias))); } ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); - ASSERT_EQ(cudaSuccess, cudaMemcpyAsync(inData->basePtr(), inTensorCpu.data(), inStrides.z * numSamples, + ASSERT_EQ(cudaSuccess, cudaMemcpyAsync(inData->basePtr(), samples.inCpu.data(), samples.inStrides.z * numSamples, cudaMemcpyHostToDevice, stream)); ASSERT_NO_THROW(op(stream, ws.get(), inTensor, outTensor, interpolation, interpolation, antialias)); - ASSERT_EQ(cudaSuccess, cudaMemcpyAsync(outTensorCpu.data(), outData->basePtr(), outStrides.z * numSamples, + ASSERT_EQ(cudaSuccess, cudaMemcpyAsync(samples.outCpu.data(), outData->basePtr(), samples.outStrides.z * numSamples, cudaMemcpyDeviceToHost, stream)); - baseline::Resize(refTensorCpu, inTensorCpu, interpolation, interpolation, antialias, {inRoi}, {outRoi}); + baseline::Resize(samples.refCpu, samples.inCpu, interpolation, interpolation, antialias, {inRoi}, {outRoi}); ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); - baseline::Compare(outTensorCpu, refTensorCpu, antialias, {outRoi}); + if (exactOutput) + { + baseline::CompareExact(samples.outCpu, samples.refCpu, {outRoi}); + } + else + { + baseline::Compare(samples.outCpu, samples.refCpu, antialias, {outRoi}); + } ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); } @@ -793,6 +869,476 @@ TYPED_TEST(OpHQResizeTensor2D, correct_output_with_antialias) TestTensor(true); } +// ============================================================================= +// Planar (NCHW/CHW) parity for the single-tensor path. +// +// HQResize resizes each channel independently, so a planar input is processed +// plane-by-plane and must produce exactly the same pixels as the interleaved +// path. These tests feed identical data through both layouts and require the +// (re-interleaved) planar output to match the interleaved output bit-for-bit, +// for every dtype, channel count, and interpolation mode. +// ============================================================================= +namespace planar_parity { + +// Copy channel `c` between a tightly-packed interleaved (HWC) buffer and a packed single plane. +// Flattening the (y, x) traversal into a single pixel index keeps callers shallow. +template +void ExtractPlane(std::vector &plane, const T *interleaved, int pixels, int channels, int c) +{ + for (int i = 0; i < pixels; ++i) plane[i] = interleaved[i * channels + c]; +} + +template +void StorePlane(T *interleaved, const std::vector &plane, int pixels, int channels, int c) +{ + for (int i = 0; i < pixels; ++i) interleaved[i * channels + c] = plane[i]; +} + +// Deterministically fill a buffer with well-mixed values so every element differs from its +// neighbors. A bit-exact parity test only needs varied, reproducible input shared by both layouts, +// so a fixed hash is preferable to a PRNG (and avoids a security-hotspot finding on std::mt19937). +template +void FillPattern(std::vector &buf, uint64_t seed) +{ + uint64_t i = seed; + for (T &v : buf) + { + uint64_t h = (++i) * 6364136223846793005ULL + 1442695040888963407ULL; + h ^= h >> 29; + if constexpr (std::is_integral_v) + v = static_cast(h); + else + v = static_cast((h >> 40) & 0xFFFF) / static_cast(65535); + } +} + +// Upload a per-sample interleaved (HWC) host buffer into an interleaved (NHWC) or planar (NCHW/CHW) +// tensor, deinterleaving into per-channel planes for the planar case. +template +void UploadTensor(const nvcv::Tensor &t, bool planar, int numSamples, int2 wh, int channels, + const std::vector &interleaved) +{ + auto data = t.exportData(); + ASSERT_TRUE(data); + auto acc = nvcv::TensorDataAccessStridedImagePlanar::Create(*data); + ASSERT_TRUE(acc); + const int W = wh.x; + const int H = wh.y; + const size_t sampleElems = static_cast(W) * H * channels; + for (int s = 0; s < numSamples; ++s) + { + if (!planar) + { + ASSERT_EQ(cudaSuccess, + cudaMemcpy2D(acc->sampleData(s), acc->rowStride(), interleaved.data() + s * sampleElems, + W * channels * sizeof(T), W * channels * sizeof(T), H, cudaMemcpyHostToDevice)); + continue; + } + std::vector plane(static_cast(W) * H); + for (int c = 0; c < channels; ++c) + { + ExtractPlane(plane, interleaved.data() + s * sampleElems, W * H, channels, c); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(acc->sampleData(s) + c * acc->chStride(), acc->rowStride(), + plane.data(), W * sizeof(T), W * sizeof(T), H, cudaMemcpyHostToDevice)); + } + } +} + +// Inverse of UploadTensor: download a tensor as a per-sample interleaved (HWC) host buffer. +template +std::vector DownloadTensor(const nvcv::Tensor &t, bool planar, int numSamples, int2 wh, int channels) +{ + auto data = t.exportData(); + EXPECT_TRUE(data); + auto acc = nvcv::TensorDataAccessStridedImagePlanar::Create(*data); + EXPECT_TRUE(acc); + const int W = wh.x; + const int H = wh.y; + const size_t sampleElems = static_cast(W) * H * channels; + std::vector interleaved(sampleElems * numSamples); + for (int s = 0; s < numSamples; ++s) + { + if (!planar) + { + EXPECT_EQ(cudaSuccess, + cudaMemcpy2D(interleaved.data() + s * sampleElems, W * channels * sizeof(T), acc->sampleData(s), + acc->rowStride(), W * channels * sizeof(T), H, cudaMemcpyDeviceToHost)); + continue; + } + std::vector plane(static_cast(W) * H); + for (int c = 0; c < channels; ++c) + { + EXPECT_EQ(cudaSuccess, cudaMemcpy2D(plane.data(), W * sizeof(T), acc->sampleData(s) + c * acc->chStride(), + acc->rowStride(), W * sizeof(T), H, cudaMemcpyDeviceToHost)); + StorePlane(interleaved.data() + s * sampleElems, plane, W * H, channels, c); + } + } + return interleaved; +} + +// Resize identical data in the given layout and return the (re-interleaved) output. The same +// workspace requirements (computed from the public C-channel shape) serve both layouts because the +// single-tensor workspace is volume-only and Volume*C*N == Volume*1*(N*C). +template +std::vector Run(bool planar, int numSamples, int2 inWH, int2 outWH, int channels, NVCVInterpolationType interp, + bool antialias, const std::vector &interleavedIn) +{ + const nvcv::DataType inDt = TypeAsFormat(); + const nvcv::DataType outDt = TypeAsFormat(); + nvcv::Tensor inT = planar ? CreateTensorHelper(inDt, "NCHW", numSamples, channels, inWH.y, inWH.x) + : CreateTensorHelper(inDt, "NHWC", numSamples, inWH.y, inWH.x, channels); + nvcv::Tensor outT = planar ? CreateTensorHelper(outDt, "NCHW", numSamples, channels, outWH.y, outWH.x) + : CreateTensorHelper(outDt, "NHWC", numSamples, outWH.y, outWH.x, channels); + UploadTensor(inT, planar, numSamples, inWH, channels, interleavedIn); + + cvcuda::HQResize op; + HQResizeTensorShapeI inDesc{ + {inWH.y, inWH.x}, + 2, + channels + }; + HQResizeTensorShapeI outDesc{ + {outWH.y, outWH.x}, + 2, + channels + }; + cvcuda::UniqueWorkspace ws = cvcuda::AllocateWorkspace( + op.getWorkspaceRequirements(numSamples, inDesc, outDesc, interp, interp, antialias)); + cudaStream_t stream; + EXPECT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + EXPECT_NO_THROW(op(stream, ws.get(), inT, outT, interp, interp, antialias)); + EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + return DownloadTensor(outT, planar, numSamples, outWH, channels); +} + +} // namespace planar_parity + +template +void TestTensorPlanarParity() +{ + const int numSamples = ttype::GetValue; + const int2 inShape = ttype::GetValue; + const int2 outShape = ttype::GetValue; + const int channels = ttype::GetValue; + using InBT = ttype::GetType; + using OutBT = ttype::GetType; + const NVCVInterpolationType interp = ttype::GetValue; + + ASSERT_LE(static_cast(inShape.x) * inShape.y, 1 << 22) + << "planar parity cases must stay small enough to run"; + + std::vector in(static_cast(inShape.x) * inShape.y * channels * numSamples); + planar_parity::FillPattern(in, 777); + + for (bool antialias : {false, true}) + { + const std::vector interleavedOut + = planar_parity::Run(false, numSamples, inShape, outShape, channels, interp, antialias, in); + const std::vector planarOut + = planar_parity::Run(true, numSamples, inShape, outShape, channels, interp, antialias, in); + ASSERT_EQ(interleavedOut.size(), planarOut.size()); + EXPECT_EQ(interleavedOut, planarOut) + << "planar != interleaved; channels=" << channels << " interp=" << interp << " antialias=" << antialias; + } +} + +NVCV_TYPED_TEST_SUITE( + OpHQResizeTensor2DPlanarParity, + ttype::Types); + +TYPED_TEST(OpHQResizeTensor2DPlanarParity, planar_matches_interleaved) +{ + TestTensorPlanarParity(); +} + +using HQResizeU8CubicExpandC1 + = NVCV_TEST_ROW(2, NVCV_SHAPE2D(37, 41), NVCV_SHAPE2D(74, 82), 1, uchar, uchar, NVCV_INTERP_CUBIC); +using HQResizeU8CubicContractC1 + = NVCV_TEST_ROW(2, NVCV_SHAPE2D(95, 129), NVCV_SHAPE2D(47, 65), 1, uchar, uchar, NVCV_INTERP_CUBIC); +using HQResizeU8LinearContractC1 + = NVCV_TEST_ROW(2, NVCV_SHAPE2D(95, 129), NVCV_SHAPE2D(47, 65), 1, uchar, uchar, NVCV_INTERP_LINEAR); +using HQResizeU8LinearExpandC1 + = NVCV_TEST_ROW(2, NVCV_SHAPE2D(37, 41), NVCV_SHAPE2D(74, 82), 1, uchar, uchar, NVCV_INTERP_LINEAR); +using HQResizeF32LinearContractC1 + = NVCV_TEST_ROW(2, NVCV_SHAPE2D(95, 129), NVCV_SHAPE2D(47, 65), 1, float, float, NVCV_INTERP_LINEAR); +using HQResizeF32LinearExpandC1 + = NVCV_TEST_ROW(2, NVCV_SHAPE2D(37, 41), NVCV_SHAPE2D(74, 82), 1, float, float, NVCV_INTERP_LINEAR); +using HQResizeU8LinearContractC3 + = NVCV_TEST_ROW(2, NVCV_SHAPE2D(95, 129), NVCV_SHAPE2D(47, 65), 3, uchar, uchar, NVCV_INTERP_LINEAR); +using HQResizeU8LinearExpandC3 + = NVCV_TEST_ROW(2, NVCV_SHAPE2D(37, 41), NVCV_SHAPE2D(74, 82), 3, uchar, uchar, NVCV_INTERP_LINEAR); +using HQResizeF32LinearContractC3 + = NVCV_TEST_ROW(2, NVCV_SHAPE2D(95, 129), NVCV_SHAPE2D(47, 65), 3, float, float, NVCV_INTERP_LINEAR); +using HQResizeF32LinearExpandC3 + = NVCV_TEST_ROW(2, NVCV_SHAPE2D(37, 41), NVCV_SHAPE2D(74, 82), 3, float, float, NVCV_INTERP_LINEAR); +using HQResizeU8CubicContractC3 + = NVCV_TEST_ROW(2, NVCV_SHAPE2D(96, 128), NVCV_SHAPE2D(48, 64), 3, uchar, uchar, NVCV_INTERP_CUBIC); +using HQResizeF32CubicContractC3 + = NVCV_TEST_ROW(2, NVCV_SHAPE2D(96, 128), NVCV_SHAPE2D(48, 64), 3, float, float, NVCV_INTERP_CUBIC); +using HQResizeU8CubicExpandC3 + = NVCV_TEST_ROW(2, NVCV_SHAPE2D(37, 41), NVCV_SHAPE2D(74, 82), 3, uchar, uchar, NVCV_INTERP_CUBIC); +using HQResizeF32CubicExpandC1 + = NVCV_TEST_ROW(2, NVCV_SHAPE2D(37, 41), NVCV_SHAPE2D(74, 82), 1, float, float, NVCV_INTERP_CUBIC); +using HQResizeF32CubicExpandC3 + = NVCV_TEST_ROW(2, NVCV_SHAPE2D(37, 41), NVCV_SHAPE2D(74, 82), 3, float, float, NVCV_INTERP_CUBIC); +using HQResizeF32CubicContractC1 + = NVCV_TEST_ROW(2, NVCV_SHAPE2D(95, 129), NVCV_SHAPE2D(47, 65), 1, float, float, NVCV_INTERP_CUBIC); +using HQResizeU8CubicContract2xC1 + = NVCV_TEST_ROW(2, NVCV_SHAPE2D(96, 128), NVCV_SHAPE2D(48, 64), 1, uchar, uchar, NVCV_INTERP_CUBIC); +using HQResizeF32CubicContract2xC1 + = NVCV_TEST_ROW(2, NVCV_SHAPE2D(96, 128), NVCV_SHAPE2D(48, 64), 1, float, float, NVCV_INTERP_CUBIC); +// Anisotropic (>=2x per axis, unequal factors) cubic magnification: 41->123 is 3x, 37->92 is ~2.49x. +using HQResizeU8CubicMagnifyAnisoC1 + = NVCV_TEST_ROW(2, NVCV_SHAPE2D(37, 41), NVCV_SHAPE2D(92, 123), 1, uchar, uchar, NVCV_INTERP_CUBIC); +using HQResizeU8CubicMagnifyAnisoC3 + = NVCV_TEST_ROW(2, NVCV_SHAPE2D(37, 41), NVCV_SHAPE2D(92, 123), 3, uchar, uchar, NVCV_INTERP_CUBIC); +using HQResizeF32CubicMagnifyAnisoC1 + = NVCV_TEST_ROW(2, NVCV_SHAPE2D(37, 41), NVCV_SHAPE2D(92, 123), 1, float, float, NVCV_INTERP_CUBIC); +using HQResizeF32CubicMagnifyAnisoC3 + = NVCV_TEST_ROW(2, NVCV_SHAPE2D(37, 41), NVCV_SHAPE2D(92, 123), 3, float, float, NVCV_INTERP_CUBIC); + +NVCV_TYPED_TEST_SUITE( + OpHQResizeTensor2DOptimizedReferencePaths, + ttype::Types< + HQResizeU8CubicExpandC1, HQResizeU8CubicContractC1, HQResizeF32CubicExpandC1, HQResizeU8LinearContractC1, + HQResizeU8LinearExpandC1, HQResizeF32LinearContractC1, HQResizeF32LinearExpandC1, HQResizeU8LinearContractC3, + HQResizeU8LinearExpandC3, HQResizeF32LinearContractC3, HQResizeF32LinearExpandC3, HQResizeU8CubicContractC3, + HQResizeF32CubicExpandC3, HQResizeF32CubicContractC3, HQResizeF32CubicContractC1, HQResizeU8CubicExpandC3, + HQResizeU8CubicContract2xC1, HQResizeF32CubicContract2xC1, HQResizeU8CubicMagnifyAnisoC1, + HQResizeU8CubicMagnifyAnisoC3, HQResizeF32CubicMagnifyAnisoC1, HQResizeF32CubicMagnifyAnisoC3>); + +TYPED_TEST(OpHQResizeTensor2DOptimizedReferencePaths, matches_reference) +{ + TestTensor(false); + TestTensor(true); +} + +TEST(OpHQResizeTensor2DOptimizedReferencePaths, direct_linear_u8_matches_reference_bit_exact) +{ + TestTensor(false, true); + TestTensor(false, true); +} + +template +void VerticalFlip(baseline::CpuSample &outTensorCpu, baseline::CpuSample &inTensorCpu) +{ + const int numSamples = inTensorCpu.numSamples(); + const int numChannels = inTensorCpu.numChannels(); + const int2 shape = inTensorCpu.shape(); + + for (int sampleIdx = 0; sampleIdx < numSamples; sampleIdx++) + { + baseline::ForAllInRoi(baseline::FullRoi<2>(shape), + [shape, numChannels, sampleIdx, &outTensorCpu, &inTensorCpu](int2 idx) + { + int2 srcIdx = idx; + srcIdx.y = shape.y - 1 - idx.y; + for (int c = 0; c < numChannels; c++) + { + outTensorCpu.get(sampleIdx, idx, c) = inTensorCpu.get(sampleIdx, srcIdx, c); + } + }); + } +} + +static void TestTensorFlippedRoiReference(NVCVInterpolationType interpolation, bool antialias) +{ + constexpr int numSamples = 2; + constexpr int numChannels = 1; + using InBT = uchar; + using OutBT = uchar; + const int2 inShape{129, 95}; + const int2 outShape{65, 47}; + const auto inDtype = TypeAsFormat(); + const auto outDtype = TypeAsFormat(); + + nvcv::Tensor inTensor = CreateTensorHelper(inDtype, "NHWC", numSamples, inShape.y, inShape.x, numChannels); + nvcv::Tensor outTensor = CreateTensorHelper(outDtype, "NHWC", numSamples, outShape.y, outShape.x, numChannels); + + auto inData = inTensor.exportData(); + auto outData = outTensor.exportData(); + ASSERT_TRUE(inData && outData); + auto samples = MakeTensor2DTestSamples(*inData, *outData, inShape, outShape, numSamples, numChannels); + baseline::CpuSample flippedInputCpu(samples.inStrides.z * numSamples, samples.inStrides, numSamples, + inShape, numChannels); + + for (int sampleIdx = 0; sampleIdx < numSamples; sampleIdx++) + { + baseline::ForAllInRoi(baseline::FullRoi<2>(inShape), + [sampleIdx, &samples](int2 idx) + { + uint64_t h = 0x9e3779b97f4a7c15ULL; + h ^= static_cast(sampleIdx + 1) * 0xbf58476d1ce4e5b9ULL; + h ^= static_cast(idx.x + 1) * 0x94d049bb133111ebULL; + h ^= static_cast(idx.y + 1) * 0xd2b74407b1ce6e93ULL; + h ^= h >> 31; + samples.inCpu.get(sampleIdx, idx, 0) = static_cast(h); + }); + } + + HQResizeRoiF roi{}; + roi.lo[0] = static_cast(inShape.y); + roi.hi[0] = 0.f; + roi.lo[1] = 0.f; + roi.hi[1] = static_cast(inShape.x); + + cvcuda::HQResize op; + cvcuda::UniqueWorkspace ws; + HQResizeTensorShapeI inShapeDesc{ + {inShape.y, inShape.x}, + 2, + numChannels + }; + HQResizeTensorShapeI outShapeDesc{ + {outShape.y, outShape.x}, + 2, + numChannels + }; + ASSERT_NO_THROW(ws = cvcuda::AllocateWorkspace(op.getWorkspaceRequirements( + numSamples, inShapeDesc, outShapeDesc, interpolation, interpolation, antialias, &roi))); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + ASSERT_EQ(cudaSuccess, cudaMemcpyAsync(inData->basePtr(), samples.inCpu.data(), samples.inStrides.z * numSamples, + cudaMemcpyHostToDevice, stream)); + ASSERT_NO_THROW(op(stream, ws.get(), inTensor, outTensor, interpolation, interpolation, antialias, &roi)); + ASSERT_EQ(cudaSuccess, cudaMemcpyAsync(samples.outCpu.data(), outData->basePtr(), samples.outStrides.z * numSamples, + cudaMemcpyDeviceToHost, stream)); + + VerticalFlip(flippedInputCpu, samples.inCpu); + baseline::Resize(samples.refCpu, flippedInputCpu, interpolation, interpolation, antialias); + + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + baseline::Compare(samples.outCpu, samples.refCpu, antialias); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpHQResizeTensor2DOptimizedReferencePaths, flipped_roi_contract_matches_reference) +{ + TestTensorFlippedRoiReference(NVCV_INTERP_LINEAR, false); + TestTensorFlippedRoiReference(NVCV_INTERP_LINEAR, true); + TestTensorFlippedRoiReference(NVCV_INTERP_CUBIC, false); + TestTensorFlippedRoiReference(NVCV_INTERP_CUBIC, true); +} + +// The fused u8 kernels read the source window as aligned-down 32-bit words; a sample +// whose base pointer is not word-aligned must produce output bit-identical to the +// aligned run without touching bytes below the first row. +static void TestTensorUnalignedBaseReference(NVCVInterpolationType interpolation, const int2 inShape, + const int2 outShape) +{ + constexpr int numChannels = 3; + using BT = uchar; + const auto dtype = TypeAsFormat(); + + nvcv::Tensor alignedIn = CreateTensorHelper(dtype, "HWC", 1, inShape.y, inShape.x, numChannels); + nvcv::Tensor alignedOut = CreateTensorHelper(dtype, "HWC", 1, outShape.y, outShape.x, numChannels); + auto alignedInData = alignedIn.exportData(); + auto alignedOutData = alignedOut.exportData(); + ASSERT_TRUE(alignedInData && alignedOutData); + auto samples = MakeTensor2DTestSamples(*alignedInData, *alignedOutData, inShape, outShape, 1, numChannels); + + baseline::ForAllInRoi(baseline::FullRoi<2>(inShape), + [&samples](int2 idx) + { + for (int c = 0; c < numChannels; c++) + { + uint64_t h = 0x9e3779b97f4a7c15ULL; + h ^= static_cast(idx.x + 1) * 0x94d049bb133111ebULL; + h ^= static_cast(idx.y + 1) * 0xd2b74407b1ce6e93ULL; + h ^= static_cast(c + 1) * 0xbf58476d1ce4e5b9ULL; + h ^= h >> 31; + samples.inCpu.get(0, idx, c) = static_cast(h); + } + }); + + const int64_t colStride = static_cast(numChannels) * sizeof(BT); + const int64_t inRowStride = inShape.x * colStride; + const int64_t outRowStride = outShape.x * colStride; + const int64_t inBytes = inShape.y * inRowStride; + const int64_t outBytes = outShape.y * outRowStride; + + NVCVByte *inAlloc = nullptr; + NVCVByte *outAlloc = nullptr; + ASSERT_EQ(cudaSuccess, cudaMalloc(reinterpret_cast(&inAlloc), inBytes + 4)); + ASSERT_EQ(cudaSuccess, cudaMalloc(reinterpret_cast(&outAlloc), outBytes + 4)); + + auto makeUnaligned = [dtype](NVCVByte *alloc, const int2 wh, const int64_t rowStrideArg, const int64_t colStrideArg) + { + nvcv::TensorDataStridedCuda::Buffer buf{}; + buf.strides[0] = rowStrideArg; + buf.strides[1] = colStrideArg; + buf.strides[2] = sizeof(BT); + buf.basePtr = alloc + 1; + return nvcv::TensorWrapData(nvcv::TensorDataStridedCuda{ + nvcv::TensorShape{{wh.y, wh.x, numChannels}, "HWC"}, + dtype, buf + }); + }; + nvcv::Tensor unalignedIn = makeUnaligned(inAlloc, inShape, inRowStride, colStride); + nvcv::Tensor unalignedOut = makeUnaligned(outAlloc, outShape, outRowStride, colStride); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + ASSERT_EQ(cudaSuccess, cudaMemcpyAsync(alignedInData->basePtr(), samples.inCpu.data(), samples.inStrides.z, + cudaMemcpyHostToDevice, stream)); + ASSERT_EQ(cudaSuccess, cudaMemcpy2DAsync(inAlloc + 1, inRowStride, samples.inCpu.data(), samples.inStrides.y, + inRowStride, inShape.y, cudaMemcpyHostToDevice, stream)); + + cvcuda::HQResize op; + HQResizeTensorShapeI inShapeDesc{ + {inShape.y, inShape.x}, + 2, + numChannels + }; + HQResizeTensorShapeI outShapeDesc{ + {outShape.y, outShape.x}, + 2, + numChannels + }; + cvcuda::UniqueWorkspace ws = cvcuda::AllocateWorkspace( + op.getWorkspaceRequirements(1, inShapeDesc, outShapeDesc, interpolation, interpolation, false)); + ASSERT_NO_THROW(op(stream, ws.get(), alignedIn, alignedOut, interpolation, interpolation, false)); + ASSERT_NO_THROW(op(stream, ws.get(), unalignedIn, unalignedOut, interpolation, interpolation, false)); + + const long3 outStrides{colStride, outRowStride, outBytes}; + baseline::CpuSample unalignedOutCpu(outBytes, outStrides, 1, outShape, numChannels); + ASSERT_EQ(cudaSuccess, cudaMemcpyAsync(samples.outCpu.data(), alignedOutData->basePtr(), samples.outStrides.z, + cudaMemcpyDeviceToHost, stream)); + ASSERT_EQ(cudaSuccess, cudaMemcpy2DAsync(unalignedOutCpu.data(), outRowStride, outAlloc + 1, outRowStride, + outRowStride, outShape.y, cudaMemcpyDeviceToHost, stream)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + baseline::Resize(samples.refCpu, samples.inCpu, interpolation, interpolation, false); + baseline::Compare(samples.outCpu, samples.refCpu, false); + baseline::CompareExact(unalignedOutCpu, samples.outCpu); + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + ASSERT_EQ(cudaSuccess, cudaFree(inAlloc)); + ASSERT_EQ(cudaSuccess, cudaFree(outAlloc)); +} + +TEST(OpHQResizeTensor2DOptimizedReferencePaths, unaligned_base_u8_matches_reference) +{ + TestTensorUnalignedBaseReference(NVCV_INTERP_CUBIC, int2{96, 128}, int2{48, 64}); + TestTensorUnalignedBaseReference(NVCV_INTERP_CUBIC, int2{129, 95}, int2{65, 47}); + TestTensorUnalignedBaseReference(NVCV_INTERP_LINEAR, int2{129, 95}, int2{65, 47}); +} + +NVCV_TYPED_TEST_SUITE(OpHQResizeTensor2DOptimizedPlanarPaths, + ttype::Types); + +TYPED_TEST(OpHQResizeTensor2DOptimizedPlanarPaths, planar_matches_interleaved) +{ + TestTensorPlanarParity(); +} + #define NVCV_SHAPE3D(d, h, w) (int3{w, h, d}) NVCV_TYPED_TEST_SUITE( OpHQResizeTensor3D, @@ -867,7 +1413,7 @@ TYPED_TEST(OpHQResizeTensor3D, correct_output_with_antialias) for (int sampleIdx = 0; sampleIdx < numSamples; sampleIdx++) { baseline::ForAllInRoi(inRoi, - [&](int3 idx) + [sampleIdx, &inTensorCpu, &rand, &rng](int3 idx) { for (int c = 0; c < numChannels; c++) { @@ -934,20 +1480,19 @@ TYPED_TEST(OpHQResizeBatch, tensor_batch_2d_correct_output) const NVCVInterpolationType magInterpolation = ttype::GetValue; const bool largeSample = ttype::GetValue; - constexpr int numSamples = 5; - const int varChannels[numSamples] = {4, 1, 7, 3, 5}; + constexpr int numSamples = 5; + constexpr std::array varChannels = {4, 1, 7, 3, 5}; - int inShape1[] = {1 << 14, 1 << 13}; + std::array inShape1 = {1 << 14, 1 << 13}; if (sizeof(InBT) == 1) { inShape1[0] *= 2; inShape1[1] *= 2; } - auto sample1 - = largeSample - ? HQResizeTensorShapeI({inShape1[0], inShape1[1]}, 2, numChannels > 0 ? numChannels : varChannels[0]) - : HQResizeTensorShapeI({728, 1024, 0}, 2, numChannels > 0 ? numChannels : varChannels[0]); + const int sample1Channels = numChannels > 0 ? numChannels : varChannels[0]; + auto sample1 = largeSample ? HQResizeTensorShapeI{{inShape1[0], inShape1[1]}, 2, sample1Channels} + : HQResizeTensorShapeI{{728, 1024, 0}, 2, sample1Channels}; std::vector inShapes = { sample1, @@ -1053,7 +1598,7 @@ TYPED_TEST(OpHQResizeBatch, tensor_batch_2d_correct_output) const auto &inRoi = inRois[sampleIdx]; auto &inTensorCpu = inBatchCpu[sampleIdx]; baseline::ForAllInRoi(inRoi, - [&](int2 idx) + [sampleIdx, &inShapes, &inTensorCpu, &rand, &rng](int2 idx) { for (int c = 0; c < inShapes[sampleIdx].numChannels; c++) { @@ -1107,8 +1652,8 @@ TYPED_TEST(OpHQResizeBatch, tensor_batch_3d_correct_output) const NVCVInterpolationType minInterpolation = ttype::GetValue; const NVCVInterpolationType magInterpolation = ttype::GetValue; - constexpr int numSamples = 5; - const int varChannels[numSamples] = {6, 2, 3, 4, 1}; + constexpr int numSamples = 5; + constexpr std::array varChannels = {6, 2, 3, 4, 1}; std::vector inShapes = { {{128, 128, 128}, 3, numChannels > 0 ? numChannels : varChannels[0]}, @@ -1191,19 +1736,14 @@ TYPED_TEST(OpHQResizeBatch, tensor_batch_3d_correct_output) baseline::CpuSample{outStrides.w, outStrides, 1, outShape, outAccess->numChannels()}); auto &inTensorCpu = inBatchCpu[sampleIdx]; - for (int z = 0; z < inShape.z; z++) - { - for (int y = 0; y < inShape.y; y++) - { - for (int x = 0; x < inShape.x; x++) - { - for (int c = 0; c < inShapes[sampleIdx].numChannels; c++) - { - inTensorCpu.get(0, int3{x, y, z}, c) = rand(rng); - } - } - } - } + baseline::ForAllInRoi(baseline::FullRoi<3>(inShape), + [&inShapes, &inTensorCpu, &rand, &rng, sampleIdx](const int3 idx) + { + for (int c = 0; c < inShapes[sampleIdx].numChannels; c++) + { + inTensorCpu.get(0, idx, c) = rand(rng); + } + }); ASSERT_EQ(cudaSuccess, cudaMemcpyAsync(inData->basePtr(), inTensorCpu.data(), inStrides.w, cudaMemcpyHostToDevice, stream)); } @@ -1322,16 +1862,14 @@ void TestImageBatch(int numSamples, std::vector &inShapes, }); auto &inTensorCpu = inBatchCpu[sampleIdx]; - for (int y = 0; y < inImgShape.h; y++) - { - for (int x = 0; x < inImgShape.w; x++) - { - for (int c = 0; c < numChannels; c++) - { - inTensorCpu.get(0, int2{x, y}, c) = rand(rng); - } - } - } + baseline::ForAllInRoi(baseline::FullRoi<2>(int2{inImgShape.w, inImgShape.h}), + [&inTensorCpu, &rand, &rng](const int2 idx) + { + for (int c = 0; c < numChannels; c++) + { + inTensorCpu.get(0, idx, c) = rand(rng); + } + }); ASSERT_EQ(cudaSuccess, cudaMemcpyAsync(inData->plane(0).basePtr, inTensorCpu.data(), inStrides.z, cudaMemcpyHostToDevice, stream)); } @@ -1379,6 +1917,366 @@ TYPED_TEST(OpHQResizeImageBatch, varbatch_2d_correct_output) TestImageBatch(numSamples, inShapes, outShapes, ws); } +// ============================================================================= +// Planar (RGB8p/RGBA8p/...) var-shape parity: planar output must be bit-identical to interleaved. +// Each plane is processed as an independent single-channel image, so the result matches the +// interleaved (RGB8/...) path for every dtype, channel count, and interpolation mode. +// ============================================================================= +namespace planar_parity_vs { + +// Upload a per-image interleaved (HWC) host buffer into an interleaved (1-plane) or planar +// (C-plane) image, deinterleaving into per-channel planes for the planar case. +template +void UploadImage(nvcv::Image &img, bool planar, int W, int H, int C, const std::vector &interleaved) +{ + auto d = img.exportData(); + ASSERT_TRUE(d); + if (!planar) + { + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(d->plane(0).basePtr, d->plane(0).rowStride, interleaved.data(), + W * C * sizeof(T), W * C * sizeof(T), H, cudaMemcpyHostToDevice)); + return; + } + std::vector plane(static_cast(W) * H); + for (int c = 0; c < C; ++c) + { + planar_parity::ExtractPlane(plane, interleaved.data(), W * H, C, c); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(d->plane(c).basePtr, d->plane(c).rowStride, plane.data(), W * sizeof(T), + W * sizeof(T), H, cudaMemcpyHostToDevice)); + } +} + +// Inverse of UploadImage. +template +std::vector DownloadImage(const nvcv::Image &img, bool planar, int W, int H, int C) +{ + auto d = img.exportData(); + EXPECT_TRUE(d); + std::vector interleaved(static_cast(W) * H * C); + if (!planar) + { + EXPECT_EQ(cudaSuccess, cudaMemcpy2D(interleaved.data(), W * C * sizeof(T), d->plane(0).basePtr, + d->plane(0).rowStride, W * C * sizeof(T), H, cudaMemcpyDeviceToHost)); + return interleaved; + } + std::vector plane(static_cast(W) * H); + for (int c = 0; c < C; ++c) + { + EXPECT_EQ(cudaSuccess, cudaMemcpy2D(plane.data(), W * sizeof(T), d->plane(c).basePtr, d->plane(c).rowStride, + W * sizeof(T), H, cudaMemcpyDeviceToHost)); + planar_parity::StorePlane(interleaved.data(), plane, W * H, C, c); + } + return interleaved; +} + +// Resize an N-image batch (varying sizes) in the given layout, returning each image's +// re-interleaved output. The planar workspace is sized from expanded (N*C single-channel) shapes; +// the interleaved one from the natural C-channel shapes. +template +std::vector> Run(bool planar, nvcv::ImageFormat inFmt, nvcv::ImageFormat outFmt, int C, + const std::vector &inWH, const std::vector &outWH, + NVCVInterpolationType interp, bool antialias, + const std::vector> &interleavedIn) +{ + const auto numSamples = static_cast(inWH.size()); + std::vector imgSrc; + std::vector imgDst; + for (int i = 0; i < numSamples; ++i) + { + imgSrc.emplace_back(nvcv::Size2D{inWH[i].x, inWH[i].y}, inFmt); + imgDst.emplace_back(nvcv::Size2D{outWH[i].x, outWH[i].y}, outFmt); + UploadImage(imgSrc[i], planar, inWH[i].x, inWH[i].y, C, interleavedIn[i]); + } + nvcv::ImageBatchVarShape batchSrc(numSamples); + nvcv::ImageBatchVarShape batchDst(numSamples); + batchSrc.pushBack(imgSrc.begin(), imgSrc.end()); + batchDst.pushBack(imgDst.begin(), imgDst.end()); + + // Workspace shapes: expanded single-channel (N*C) for planar, natural C-channel for interleaved. + std::vector inShapes; + std::vector outShapes; + const int wsChannels = planar ? 1 : C; + const int reps = planar ? C : 1; + for (int i = 0; i < numSamples; ++i) + for (int r = 0; r < reps; ++r) + { + inShapes.push_back(HQResizeTensorShapeI{ + {inWH[i].y, inWH[i].x}, + 2, + wsChannels + }); + outShapes.push_back(HQResizeTensorShapeI{ + {outWH[i].y, outWH[i].x}, + 2, + wsChannels + }); + } + const int wsSamples = numSamples * reps; + + cvcuda::HQResize op; + HQResizeTensorShapesI inDesc{inShapes.data(), wsSamples, 2, wsChannels}; + HQResizeTensorShapesI outDesc{outShapes.data(), wsSamples, 2, wsChannels}; + cvcuda::UniqueWorkspace ws + = cvcuda::AllocateWorkspace(op.getWorkspaceRequirements(wsSamples, inDesc, outDesc, interp, interp, antialias)); + cudaStream_t stream; + EXPECT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + EXPECT_NO_THROW(op(stream, ws.get(), batchSrc, batchDst, interp, interp, antialias)); + EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + + std::vector> out(numSamples); + for (int i = 0; i < numSamples; ++i) out[i] = DownloadImage(imgDst[i], planar, outWH[i].x, outWH[i].y, C); + return out; +} + +template +void Check(nvcv::ImageFormat inFmt, nvcv::ImageFormat inFmtP, nvcv::ImageFormat outFmt, nvcv::ImageFormat outFmtP, + int C, NVCVInterpolationType interp) +{ + const std::vector inWH{ + { 64, 48}, + { 31, 97}, + {128, 33} + }; + const std::vector outWH{ + { 40, 40}, + {120, 50}, + { 33, 128} + }; + std::vector> in(inWH.size()); + for (size_t i = 0; i < inWH.size(); ++i) + { + in[i].resize(static_cast(inWH[i].x) * inWH[i].y * C); + planar_parity::FillPattern(in[i], 909 + i); + } + for (bool antialias : {false, true}) + { + auto interleaved = Run(false, inFmt, outFmt, C, inWH, outWH, interp, antialias, in); + auto planar = Run(true, inFmtP, outFmtP, C, inWH, outWH, interp, antialias, in); + for (size_t i = 0; i < inWH.size(); ++i) + EXPECT_EQ(interleaved[i], planar[i]) + << "image " << i << " C=" << C << " interp=" << interp << " antialias=" << antialias; + } +} + +} // namespace planar_parity_vs + +TEST(OpHQResizeImageBatch, planar_matches_interleaved) +{ + using namespace planar_parity_vs; + for (auto interp : {NVCV_INTERP_NEAREST, NVCV_INTERP_LINEAR, NVCV_INTERP_CUBIC, NVCV_INTERP_LANCZOS}) + { + Check(nvcv::FMT_RGB8, nvcv::FMT_RGB8p, nvcv::FMT_RGB8, nvcv::FMT_RGB8p, 3, interp); + Check(nvcv::FMT_RGBA8, nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8, nvcv::FMT_RGBA8p, 4, interp); + Check(nvcv::FMT_RGBf32, nvcv::FMT_RGBf32p, nvcv::FMT_RGBf32, nvcv::FMT_RGBf32p, 3, interp); + } +} + +// ============================================================================= +// Planar (NCHW/CHW) tensor-batch parity: a planar tensor batch is expanded into single-channel +// plane views and must produce bit-identical output to the interleaved (HWC) tensor batch. +// ============================================================================= +namespace planar_parity_tb { + +template +std::vector> Run(bool planar, int C, const std::vector &inWH, const std::vector &outWH, + NVCVInterpolationType interp, bool antialias, + const std::vector> &in) +{ + const auto N = static_cast(inWH.size()); + const nvcv::DataType inDt = TypeAsFormat(); + const nvcv::DataType outDt = TypeAsFormat(); + std::vector srcTs; + std::vector dstTs; + for (int i = 0; i < N; ++i) + { + srcTs.push_back(planar ? CreateTensorHelper(inDt, "NCHW", 1, C, inWH[i].y, inWH[i].x) + : CreateTensorHelper(inDt, "HWC", 1, inWH[i].y, inWH[i].x, C)); + dstTs.push_back(planar ? CreateTensorHelper(outDt, "NCHW", 1, C, outWH[i].y, outWH[i].x) + : CreateTensorHelper(outDt, "HWC", 1, outWH[i].y, outWH[i].x, C)); + planar_parity::UploadTensor(srcTs[i], planar, 1, inWH[i], C, in[i]); + } + nvcv::TensorBatch sb(N); + nvcv::TensorBatch db(N); + sb.pushBack(srcTs.begin(), srcTs.end()); + db.pushBack(dstTs.begin(), dstTs.end()); + + // Planar workspace is sized from expanded (N*C single-channel) shapes; interleaved from C-channel. + std::vector inShapes; + std::vector outShapes; + const int wsCh = planar ? 1 : C; + const int reps = planar ? C : 1; + for (int i = 0; i < N; ++i) + for (int r = 0; r < reps; ++r) + { + inShapes.push_back(HQResizeTensorShapeI{ + {inWH[i].y, inWH[i].x}, + 2, + wsCh + }); + outShapes.push_back(HQResizeTensorShapeI{ + {outWH[i].y, outWH[i].x}, + 2, + wsCh + }); + } + const int wsN = N * reps; + cvcuda::HQResize op; + HQResizeTensorShapesI inDesc{inShapes.data(), wsN, 2, wsCh}; + HQResizeTensorShapesI outDesc{outShapes.data(), wsN, 2, wsCh}; + cvcuda::UniqueWorkspace ws + = cvcuda::AllocateWorkspace(op.getWorkspaceRequirements(wsN, inDesc, outDesc, interp, interp, antialias)); + cudaStream_t stream; + EXPECT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + EXPECT_NO_THROW(op(stream, ws.get(), sb, db, interp, interp, antialias)); + EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + + std::vector> out(N); + for (int i = 0; i < N; ++i) out[i] = planar_parity::DownloadTensor(dstTs[i], planar, 1, outWH[i], C); + return out; +} + +// Build random C-channel data, resize it as both an interleaved (HWC) and a planar (CHW) tensor +// batch, and assert the re-interleaved planar output matches the interleaved output bit-for-bit. +static void Check(int C, const std::vector &inWH, const std::vector &outWH, NVCVInterpolationType interp) +{ + std::vector> in(inWH.size()); + for (size_t i = 0; i < inWH.size(); ++i) + { + in[i].resize(static_cast(inWH[i].x) * inWH[i].y * C); + planar_parity::FillPattern(in[i], static_cast(404 + C) * 131 + i); + } + for (bool antialias : {false, true}) + { + auto interleaved = Run(false, C, inWH, outWH, interp, antialias, in); + auto planar = Run(true, C, inWH, outWH, interp, antialias, in); + for (size_t i = 0; i < inWH.size(); ++i) + EXPECT_EQ(interleaved[i], planar[i]) + << "tensorbatch image " << i << " C=" << C << " interp=" << interp << " antialias=" << antialias; + } +} + +} // namespace planar_parity_tb + +TEST(OpHQResizeTensorBatch, planar_matches_interleaved) +{ + using namespace planar_parity_tb; + const std::vector inWH{ + {64, 48}, + {31, 97}, + {50, 50} + }; + const std::vector outWH{ + { 40, 40}, + { 60, 33}, + {128, 20} + }; + for (auto interp : {NVCV_INTERP_NEAREST, NVCV_INTERP_LINEAR, NVCV_INTERP_CUBIC, NVCV_INTERP_LANCZOS}) + for (int C : {1, 3, 4}) Check(C, inWH, outWH, interp); +} + +// ============================================================================= +// Uniform-shape batches where one sample resizes through a flipped (lo > hi) ROI: +// the sample shapes match, but the mapping does not, so batch fast paths that share +// sample-0 phase state across samples must fall back, and every sample must match +// the CPU reference computed from its own (flipped) input. +// ============================================================================= +namespace batch_roi_mapping { + +static void Check(const int2 inWH, const int2 outWH, const NVCVInterpolationType interp) +{ + constexpr int numChannels = 3; + constexpr int numSamples = 2; + using BT = float; + const auto dtype = TypeAsFormat(); + + nvcv::TensorBatch inBatch(numSamples); + nvcv::TensorBatch outBatch(numSamples); + std::vector> inData(numSamples); + for (int s = 0; s < numSamples; s++) + { + inData[s].resize(static_cast(inWH.x) * inWH.y * numChannels); + planar_parity::FillPattern(inData[s], 777 + s); + nvcv::Tensor inT = CreateTensorHelper(dtype, "HWC", 1, inWH.y, inWH.x, numChannels); + nvcv::Tensor outT = CreateTensorHelper(dtype, "HWC", 1, outWH.y, outWH.x, numChannels); + planar_parity::UploadTensor(inT, false, 1, inWH, numChannels, inData[s]); + inBatch.pushBack(inT); + outBatch.pushBack(outT); + } + + std::array roiData{}; + for (int s = 0; s < numSamples; s++) + { + roiData[s].lo[0] = s == 0 ? 0.f : static_cast(inWH.y); + roiData[s].hi[0] = s == 0 ? static_cast(inWH.y) : 0.f; + roiData[s].hi[1] = static_cast(inWH.x); + } + HQResizeRoisF rois{numSamples, 2, roiData.data()}; + + std::vector inShapes(numSamples, HQResizeTensorShapeI{ + {inWH.y, inWH.x}, + 2, + numChannels + }); + std::vector outShapes(numSamples, HQResizeTensorShapeI{ + {outWH.y, outWH.x}, + 2, + numChannels + }); + HQResizeTensorShapesI inDesc{inShapes.data(), numSamples, 2, numChannels}; + HQResizeTensorShapesI outDesc{outShapes.data(), numSamples, 2, numChannels}; + + cvcuda::HQResize op; + cvcuda::UniqueWorkspace ws + = cvcuda::AllocateWorkspace(op.getWorkspaceRequirements(numSamples, inDesc, outDesc, interp, interp, false)); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + ASSERT_NO_THROW(op(stream, ws.get(), inBatch, outBatch, interp, interp, false, rois)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + + const int64_t colStride = static_cast(numChannels) * sizeof(BT); + const int64_t inRowStride = inWH.x * colStride; + const int64_t inBytes = inWH.y * inRowStride; + const int64_t outRowStride = outWH.x * colStride; + const int64_t outBytes = outWH.y * outRowStride; + for (int s = 0; s < numSamples; s++) + { + SCOPED_TRACE(s); + baseline::CpuSample inCpu(inBytes, long3{colStride, inRowStride, inBytes}, 1, inWH, numChannels); + baseline::ForAllInRoi(baseline::FullRoi<2>(inWH), + [s, inWH, &inCpu, &inData](int2 idx) + { + const int srcY = s == 0 ? idx.y : inWH.y - 1 - idx.y; + for (int c = 0; c < numChannels; c++) + { + inCpu.get(0, idx, c) + = inData[s][(static_cast(srcY) * inWH.x + idx.x) * numChannels + c]; + } + }); + + baseline::CpuSample refCpu(outBytes, long3{colStride, outRowStride, outBytes}, 1, outWH, numChannels); + baseline::Resize(refCpu, inCpu, interp, interp, false); + + baseline::CpuSample outCpu(outBytes, long3{colStride, outRowStride, outBytes}, 1, outWH, numChannels); + const auto out = planar_parity::DownloadTensor(outBatch[s], false, 1, outWH, numChannels); + std::memcpy(outCpu.data(), out.data(), outBytes); + + baseline::Compare(outCpu, refCpu, false); + } +} + +} // namespace batch_roi_mapping + +TEST(OpHQResizeTensorBatch, flipped_roi_uniform_shapes_matches_reference) +{ + using namespace batch_roi_mapping; + Check(int2{40, 36}, int2{100, 90}, NVCV_INTERP_CUBIC); + Check(int2{96, 120}, int2{40, 48}, NVCV_INTERP_CUBIC); +} + TEST(OpHQResizeImageBatch, test_multi_run_single_workspace) { using FirstRun = typename NVCV_TEST_ROW_IB(1, uchar, NVCV_IMAGE_FORMAT_U8, uchar, NVCV_IMAGE_FORMAT_U8, false, @@ -1429,7 +2327,8 @@ TEST(OpHQResizeNegative, getWorkspaceRequirementsWithNullReqOut) { NVCVOperatorHandle op; EXPECT_EQ(NVCV_SUCCESS, cvcudaHQResizeCreate(&op)); - HQResizeTensorShapeI shapeIn, shapeOut; + HQResizeTensorShapeI shapeIn; + HQResizeTensorShapeI shapeOut; shapeIn.extent[0] = 128; shapeIn.ndim = 1; shapeIn.numChannels = 1; @@ -1462,16 +2361,64 @@ TEST(OpHQResizeNegative, submitWithNullWorkspace) EXPECT_NO_THROW(nvcvOperatorDestroy(op)); } +TEST(OpHQResizeNegative, submitWithNonFiniteRoi) +{ + NVCVOperatorHandle op; + ASSERT_EQ(NVCV_SUCCESS, cvcudaHQResizeCreate(&op)); + + // NaN lo + HQResizeRoiF roiNanLo{}; + roiNanLo.lo[0] = std::numeric_limits::quiet_NaN(); + roiNanLo.hi[0] = 128.f; + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + cvcudaHQResizeSubmit(op, nullptr, nullptr, nullptr, nullptr, NVCV_INTERP_LINEAR, NVCV_INTERP_LINEAR, + false, &roiNanLo)); + + // Inf hi + HQResizeRoiF roiInfHi{}; + roiInfHi.lo[0] = 0.f; + roiInfHi.hi[0] = std::numeric_limits::infinity(); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + cvcudaHQResizeSubmit(op, nullptr, nullptr, nullptr, nullptr, NVCV_INTERP_LINEAR, NVCV_INTERP_LINEAR, + false, &roiInfHi)); + + EXPECT_NO_THROW(nvcvOperatorDestroy(op)); +} + +TEST(OpHQResizeNegative, submitWithNonFiniteRoiBatch) +{ + NVCVOperatorHandle op; + ASSERT_EQ(NVCV_SUCCESS, cvcudaHQResizeCreate(&op)); + + std::array roiData{}; + roiData[0].lo[0] = std::numeric_limits::quiet_NaN(); + roiData[0].hi[0] = 128.f; + + HQResizeRoisF rois; + rois.roi = roiData.data(); + rois.size = 1; + rois.ndim = 1; + + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + cvcudaHQResizeImageBatchSubmit(op, nullptr, nullptr, nullptr, nullptr, NVCV_INTERP_LINEAR, + NVCV_INTERP_LINEAR, false, rois)); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + cvcudaHQResizeTensorBatchSubmit(op, nullptr, nullptr, nullptr, nullptr, NVCV_INTERP_LINEAR, + NVCV_INTERP_LINEAR, false, rois)); + + EXPECT_NO_THROW(nvcvOperatorDestroy(op)); +} + TEST(OpHQResizeNegative, submitWithNullWorkspaceBatch) { NVCVOperatorHandle op; EXPECT_EQ(NVCV_SUCCESS, cvcudaHQResizeCreate(&op)); - HQResizeRoiF roi[1]; + std::array roi{}; roi[0].lo[0] = 0; roi[0].hi[0] = 128; HQResizeRoisF rois; - rois.roi = roi; + rois.roi = roi.data(); rois.size = 1; rois.ndim = 1; @@ -1488,7 +2435,8 @@ TEST(OpHQResizeNegative, getWorkspaceRequirementsWithNullReqOutBatch) { NVCVOperatorHandle op; EXPECT_EQ(NVCV_SUCCESS, cvcudaHQResizeCreate(&op)); - HQResizeTensorShapeI shapeIn[1], shapeOut[1]; + std::array shapeIn{}; + std::array shapeOut{}; shapeIn[0].extent[0] = 128; shapeIn[0].ndim = 1; shapeIn[0].numChannels = 1; @@ -1496,20 +2444,23 @@ TEST(OpHQResizeNegative, getWorkspaceRequirementsWithNullReqOutBatch) shapeOut[0].ndim = 1; shapeOut[0].numChannels = 1; - HQResizeTensorShapesI shapeInBatch, shapeOutBatch; - shapeInBatch.shape = shapeIn; - shapeInBatch.size = 1; - shapeInBatch.ndim = 1; - shapeInBatch.numChannels = 1; - shapeOutBatch.shape = shapeOut; - shapeOutBatch.size = 1; - shapeOutBatch.ndim = 1; + HQResizeTensorShapesI shapeInBatch; + + HQResizeTensorShapesI shapeOutBatch; + shapeInBatch.shape = shapeIn.data(); + shapeInBatch.size = 1; + shapeInBatch.ndim = 1; + shapeInBatch.numChannels = 1; + shapeOutBatch.shape = shapeOut.data(); + shapeOutBatch.size = 1; + shapeOutBatch.ndim = 1; + shapeOutBatch.numChannels = 1; - HQResizeRoiF roi[1]; + std::array roi{}; roi[0].lo[0] = 0; roi[0].hi[0] = 128; HQResizeRoisF rois; - rois.roi = roi; + rois.roi = roi.data(); rois.size = 1; rois.ndim = 1; diff --git a/tests/cvcuda/system/TestOpHistogram.cpp b/tests/cvcuda/system/TestOpHistogram.cpp index 5735c8523..77c4ccab9 100644 --- a/tests/cvcuda/system/TestOpHistogram.cpp +++ b/tests/cvcuda/system/TestOpHistogram.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -24,6 +24,7 @@ #include #include +#include #include #include @@ -31,7 +32,7 @@ namespace gt = ::testing; namespace test = nvcv::test; namespace util = nvcv::util; -static void computeHistogram(std::vector imageVec, std::vector &goldHistogram) +static void computeHistogram(const std::vector &imageVec, std::vector &goldHistogram) { // Assuming grayscale image, the histogram will be of size 256 std::vector histogram(256, 0); @@ -101,11 +102,11 @@ TEST_P(OpHistogram, Histogram) std::default_random_engine randEng(0); std::uniform_int_distribution rand(0u, 255u); - for (int i = 0; i < batches; ++i) + for (int i = 0; i < batches; ++i) // NOSONAR { // generate random input image std::vector imageVec(width * height); - std::generate(imageVec.begin(), imageVec.end(), [&]() { return rand(randEng); }); + std::ranges::generate(imageVec, [&rand, &randEng]() { return rand(randEng); }); // copy random input to device tensor EXPECT_NO_THROW(util::SetImageTensorFromVector(inTensor.exportData(), imageVec, i)); @@ -115,7 +116,7 @@ TEST_P(OpHistogram, Histogram) // run operator cvcuda::Histogram op; - EXPECT_NO_THROW(op(stream, inTensor, nvcv::NullOpt, histogram)); + EXPECT_NO_THROW(op(stream, inTensor, nvcv::OptionalTensorConstRef{nvcv::NullOpt}, histogram)); ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); @@ -146,14 +147,14 @@ TEST_P(OpHistogram, Histogram_mask) std::uniform_int_distribution rand(0u, 255u); std::uniform_int_distribution randMask(0u, 1u); // any value other than 0 is considered as 1 but want some 0s too - for (int i = 0; i < batches; ++i) + for (int i = 0; i < batches; ++i) // NOSONAR { // generate random input image std::vector imageVec(width * height); - std::generate(imageVec.begin(), imageVec.end(), [&]() { return rand(randEng); }); + std::ranges::generate(imageVec, [&rand, &randEng]() { return rand(randEng); }); //generate random mask std::vector maskVec(width * height); - std::generate(maskVec.begin(), maskVec.end(), [&]() { return randMask(randEng); }); + std::ranges::generate(maskVec, [&randMask, &randEng]() { return randMask(randEng); }); // copy random input to device tensor EXPECT_NO_THROW(util::SetImageTensorFromVector(inTensor.exportData(), imageVec, i)); @@ -166,7 +167,7 @@ TEST_P(OpHistogram, Histogram_mask) // run operator cvcuda::Histogram op; - EXPECT_NO_THROW(op(stream, inTensor, inMask, histogram)); + EXPECT_NO_THROW(op(stream, inTensor, nvcv::OptionalTensorConstRef{inMask}, histogram)); ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); @@ -178,6 +179,185 @@ TEST_P(OpHistogram, Histogram_mask) ASSERT_EQ(opHistogram, goldHistogram); } +static nvcv::Tensor CreateSingleChannelTensor(int batches, int width, int height, bool planar, bool batched) +{ + if (planar && batched) + { + nvcv::TensorShape shape{ + {batches, 1, height, width}, + "NCHW" + }; + return nvcv::Tensor(shape, nvcv::TYPE_U8); + } + if (planar) + { + nvcv::TensorShape shape{ + {1, height, width}, + "CHW" + }; + return nvcv::Tensor(shape, nvcv::TYPE_U8); + } + if (batched) + { + return nvcv::util::CreateTensor(batches, width, height, nvcv::FMT_U8); + } + + nvcv::TensorShape shape{ + {height, width, 1}, + "HWC" + }; + return nvcv::Tensor(shape, nvcv::TYPE_U8); +} + +template +static gt::AssertionResult SetImageTensor(nvcv::Tensor &tensor, std::vector &values, int sample) +{ + try + { + util::SetImageTensorFromVector(tensor.exportData(), values, sample); + } + catch (const util::TensorDataUtilsError &e) + { + return gt::AssertionFailure() << e.what(); + } + return gt::AssertionSuccess(); +} + +static std::vector MakeHistogramParityInput(int width, int height, int sample) +{ + // Use standard LCG constants and the top state byte for reproducible, well-spread intensities. + constexpr uint32_t kLcgMultiplier = 1664525u; + constexpr uint32_t kLcgIncrement = 1013904223u; + + std::vector imageVec(width * height); + for (size_t i = 0; i < imageVec.size(); ++i) + { + const uint32_t state = kLcgMultiplier * static_cast(i + sample * imageVec.size()) + kLcgIncrement; + imageVec[i] = static_cast(state >> 24); + } + return imageVec; +} + +static std::vector MakeHistogramParityMask(int width, int height, int sample) +{ + std::vector maskVec(width * height); + for (size_t i = 0; i < maskVec.size(); ++i) + { + maskVec[i] = ((i + static_cast(sample)) % 3) == 0 ? 0 : 1; + } + return maskVec; +} + +static gt::AssertionResult FillHistogramParityInputs(nvcv::Tensor &interleaved, nvcv::Tensor &planar, int width, + int height, int batches) +{ + for (int i = 0; i < batches; ++i) + { + std::vector imageVec = MakeHistogramParityInput(width, height, i); + + if (auto result = SetImageTensor(interleaved, imageVec, i); !result) + { + return result << " while filling interleaved sample " << i; + } + if (auto result = SetImageTensor(planar, imageVec, i); !result) + { + return result << " while filling planar sample " << i; + } + } + return gt::AssertionSuccess(); +} + +static gt::AssertionResult FillHistogramParityMasks(nvcv::Tensor &interleaved, nvcv::Tensor &planar, int width, + int height, int batches) +{ + for (int i = 0; i < batches; ++i) + { + std::vector maskVec = MakeHistogramParityMask(width, height, i); + + if (auto result = SetImageTensor(interleaved, maskVec, i); !result) + { + return result << " while filling interleaved mask sample " << i; + } + if (auto result = SetImageTensor(planar, maskVec, i); !result) + { + return result << " while filling planar mask sample " << i; + } + } + return gt::AssertionSuccess(); +} + +// clang-format off +NVCV_TEST_SUITE_P(OpHistogramPlanar, test::ValueList +{ + // width, height, batches, useMask, batched + { 17, 13, 3, false, true}, + { 17, 13, 3, true, true}, + { 19, 11, 1, false, false}, + { 19, 11, 1, true, false}, +}); + +// clang-format on + +TEST_P(OpHistogramPlanar, output_matches_interleaved) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + int width = GetParamValue<0>(); + int height = GetParamValue<1>(); + int batches = GetParamValue<2>(); + bool useMask = GetParamValue<3>(); + bool batched = GetParamValue<4>(); + + nvcv::Tensor srcI = CreateSingleChannelTensor(batches, width, height, false, batched); + nvcv::Tensor srcP = CreateSingleChannelTensor(batches, width, height, true, batched); + nvcv::Tensor histI = nvcv::util::CreateTensor(1, 256, batches, nvcv::ImageFormat(NVCV_IMAGE_FORMAT_S32)); + nvcv::Tensor histP = nvcv::util::CreateTensor(1, 256, batches, nvcv::ImageFormat(NVCV_IMAGE_FORMAT_S32)); + + ASSERT_TRUE(FillHistogramParityInputs(srcI, srcP, width, height, batches)); + + cvcuda::Histogram op; + if (useMask) + { + nvcv::Tensor maskI = CreateSingleChannelTensor(batches, width, height, false, batched); + nvcv::Tensor maskP = CreateSingleChannelTensor(batches, width, height, true, batched); + ASSERT_TRUE(FillHistogramParityMasks(maskI, maskP, width, height, batches)); + + EXPECT_NO_THROW(op(stream, srcI, nvcv::OptionalTensorConstRef{maskI}, histI)); + EXPECT_NO_THROW(op(stream, srcP, nvcv::OptionalTensorConstRef{maskP}, histP)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + } + else + { + EXPECT_NO_THROW(op(stream, srcI, nvcv::OptionalTensorConstRef{nvcv::NullOpt}, histI)); + EXPECT_NO_THROW(op(stream, srcP, nvcv::OptionalTensorConstRef{nvcv::NullOpt}, histP)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + } + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + + std::vector histIVec; + std::vector histPVec; + EXPECT_NO_THROW(util::GetImageVectorFromTensor(histI.exportData(), 0, histIVec)); + EXPECT_NO_THROW(util::GetImageVectorFromTensor(histP.exportData(), 0, histPVec)); + ASSERT_EQ(histIVec, histPVec); +} + +static void ExpectHistogramInvalidArgument(const nvcv::Tensor &histogram, nvcv::OptionalTensorConstRef mask) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + nvcv::Tensor inTensor = nvcv::util::CreateTensor(1, 2, 2, nvcv::FMT_U8); + std::vector imageVec(4, 0); + ASSERT_NO_THROW(util::SetImageTensorFromVector(inTensor.exportData(), imageVec, 0)); + + cvcuda::Histogram op; + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&] { op(stream, inTensor, mask, histogram); })); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + // clang-format off NVCV_TEST_SUITE_P(OpHistogram_Negative, test::ValueList{ // inFmt, histFmt, batches, histHeight @@ -209,12 +389,68 @@ TEST_P(OpHistogram_Negative, op) // run operator cvcuda::Histogram op; - EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&] { op(stream, inTensor, inMask, histogram); })); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&op, &stream, &inTensor, &inMask, &histogram] + { op(stream, inTensor, nvcv::OptionalTensorConstRef{inMask}, histogram); })); ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); } -// clang-format on +TEST(OpHistogram_Negative, rejects_invalid_histogram_dtype) +{ + nvcv::Tensor histogram = nvcv::util::CreateTensor(1, 256, 1, nvcv::FMT_U8); + ExpectHistogramInvalidArgument(histogram, nvcv::OptionalTensorConstRef{nvcv::NullOpt}); +} + +TEST(OpHistogram_Negative, rejects_histogram_width_less_than_256) +{ + nvcv::Tensor histogram = nvcv::util::CreateTensor(1, 255, 1, nvcv::FMT_S32); + ExpectHistogramInvalidArgument(histogram, nvcv::OptionalTensorConstRef{nvcv::NullOpt}); +} + +TEST(OpHistogram_Negative, rejects_histogram_with_multiple_channels) +{ + nvcv::Tensor histogram( + { + {1, 256, 2}, + "HWC" + }, + nvcv::TYPE_S32); + ExpectHistogramInvalidArgument(histogram, nvcv::OptionalTensorConstRef{nvcv::NullOpt}); +} + +TEST(OpHistogram_Negative, rejects_invalid_mask_dtype) +{ + nvcv::Tensor histogram = nvcv::util::CreateTensor(1, 256, 1, nvcv::FMT_S32); + nvcv::Tensor mask = nvcv::util::CreateTensor(1, 2, 2, nvcv::FMT_S32); + + std::vector maskVec(4, 1); + ASSERT_NO_THROW(util::SetImageTensorFromVector(mask.exportData(), maskVec, 0)); + + ExpectHistogramInvalidArgument(histogram, nvcv::OptionalTensorConstRef{mask}); +} + +TEST(OpHistogram_Negative, rejects_mask_layout_different_from_input) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + nvcv::Tensor inTensor{ + {{1, 2, 2, 1}, "NHWC"}, + nvcv::TYPE_U8 + }; + nvcv::Tensor mask{ + {{2, 2, 1}, "HWC"}, + nvcv::TYPE_U8 + }; + nvcv::Tensor histogram = nvcv::util::CreateTensor(1, 256, 1, nvcv::FMT_S32); + + cvcuda::Histogram op; + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&] { op(stream, inTensor, nvcv::OptionalTensorConstRef{mask}, histogram); })); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} TEST(OpHistogram_Negative, create_null_handle) { diff --git a/tests/cvcuda/system/TestOpHistogramEq.cpp b/tests/cvcuda/system/TestOpHistogramEq.cpp index bd79d5bae..dafbaa324 100644 --- a/tests/cvcuda/system/TestOpHistogramEq.cpp +++ b/tests/cvcuda/system/TestOpHistogramEq.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,6 +16,7 @@ */ #include "Definitions.hpp" +#include "PlanarParityUtils.hpp" #include #include @@ -32,6 +33,11 @@ namespace gt = ::testing; namespace test = nvcv::test; namespace util = nvcv::util; +static int ScaledSize(int size, double scale) +{ + return static_cast(size * scale); +} + static void histogramEqualization(const std::vector &inputImage, int width, int height, int numChannels, std::vector &outputImage) { @@ -54,9 +60,8 @@ static void histogramEqualization(const std::vector &inputImage, int wi { cumulativeHistogram[i] += cumulativeHistogram[i - 1]; } - auto smallestIt - = std::find_if(cumulativeHistogram.begin(), cumulativeHistogram.end(), [](int i) { return i != 0; }); - auto smallest = *smallestIt; + auto smallestIt = std::ranges::find_if(cumulativeHistogram, [](int i) { return i != 0; }); + auto smallest = *smallestIt; // Normalize the cumulative histogram to get the mapping values std::array equalizeMap = {}; @@ -64,7 +69,7 @@ static void histogramEqualization(const std::vector &inputImage, int wi { for (int i = 0; i < 256; ++i) { - equalizeMap[i] = smallestIt - cumulativeHistogram.begin(); + equalizeMap[i] = static_cast(smallestIt - cumulativeHistogram.begin()); } } else @@ -74,7 +79,7 @@ static void histogramEqualization(const std::vector &inputImage, int wi int tmpT = (cumulativeHistogram[i] - smallest); int tmpB = ((width * height) - smallest); float ratio = (float)(tmpT * 255) / (float)tmpB; - equalizeMap[i] = nvcv::cuda::SaturateCast(ratio); + equalizeMap[i] = nvcv::cuda::SaturateCast(ratio); } } @@ -124,29 +129,28 @@ TEST_P(OpHistogramEq, HistogramEq_correct_output) int height = GetParamValue<1>(); nvcv::ImageFormat format{GetParamValue<2>()}; int batches = GetParamValue<3>(); - for (bool AllPixelsHaveSameValue : std::vector{false, true}) + for (bool AllPixelsHaveSameValue : std::vector{false, true}) // NOSONAR { nvcv::Tensor inTensor = nvcv::util::CreateTensor(batches, width, height, format); nvcv::Tensor outTensor = nvcv::util::CreateTensor(batches, width, height, format); - std::vector goldImage; - std::default_random_engine randEng(0); - std::uniform_int_distribution rand(0u, 255u); - int colorChannels = inTensor.shape()[inTensor.shape().rank() - 1]; - size_t imageSizeBytes = width * height * colorChannels * sizeof(uint8_t); + std::default_random_engine randEng(0); + std::uniform_int_distribution rand(0, 255); + auto colorChannels = static_cast(inTensor.shape()[inTensor.shape().rank() - 1]); + size_t imageSizeBytes = width * height * colorChannels * sizeof(uint8_t); - for (int i = 0; i < batches; ++i) + for (int i = 0; i < batches; ++i) // NOSONAR { // generate random input image std::vector imageVec(imageSizeBytes); // all pixels have same value if (AllPixelsHaveSameValue) { - std::generate(imageVec.begin(), imageVec.end(), [&]() { return rand(randEng); }); + std::ranges::generate(imageVec, [&rand, &randEng]() { return rand(randEng); }); } else { - std::generate(imageVec.begin(), imageVec.end(), [&]() { return 5; }); + std::ranges::generate(imageVec, []() { return 5; }); } // copy random input to device tensor EXPECT_NO_THROW(util::SetImageTensorFromVector(inTensor.exportData(), imageVec, i)); @@ -157,16 +161,16 @@ TEST_P(OpHistogramEq, HistogramEq_correct_output) EXPECT_NO_THROW(op(stream, inTensor, outTensor)); ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); - for (int i = 0; i < batches; ++i) + for (int batchIndex = 0; batchIndex < batches; ++batchIndex) // NOSONAR { // get output image std::vector outImage; std::vector input; std::vector goldImage; - EXPECT_NO_THROW(util::GetImageVectorFromTensor(inTensor.exportData(), i, input)); + EXPECT_NO_THROW(util::GetImageVectorFromTensor(inTensor.exportData(), batchIndex, input)); histogramEqualization(input, width, height, colorChannels, goldImage); - EXPECT_NO_THROW(util::GetImageVectorFromTensor(outTensor.exportData(), i, outImage)); + EXPECT_NO_THROW(util::GetImageVectorFromTensor(outTensor.exportData(), batchIndex, outImage)); ASSERT_EQ(outImage, goldImage); } } @@ -174,6 +178,61 @@ TEST_P(OpHistogramEq, HistogramEq_correct_output) ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); } +TEST(OpHistogramEq, tensor_c1_nchw_widened_body_and_tail) +{ + constexpr int width = 16 * 8 + 3; + constexpr int height = 17; + constexpr int batches = 2; + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + nvcv::Tensor inTensor( + { + {batches, 1, height, width}, + "NCHW" + }, + nvcv::TYPE_U8); + nvcv::Tensor outTensor( + { + {batches, 1, height, width}, + "NCHW" + }, + nvcv::TYPE_U8); + + auto inData = inTensor.exportData(); + auto outData = outTensor.exportData(); + ASSERT_NE(inData, nullptr); + ASSERT_NE(outData, nullptr); + + auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*inData); + auto outAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*outData); + ASSERT_TRUE(inAccess); + ASSERT_TRUE(outAccess); + + std::vector> inputs(batches, std::vector(width * height)); + for (int sample = 0; sample < batches; ++sample) + { + test::planar::FillDeterministicValues(inputs[sample], sample * 101 + 13, nvcv::TYPE_U8); + test::planar::UploadPlanarSample(*inAccess, sample, inputs[sample], width, height, 1, 1); + } + + cvcuda::HistogramEq op(batches); + EXPECT_NO_THROW(op(stream, inTensor, outTensor)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + for (int sample = 0; sample < batches; ++sample) + { + SCOPED_TRACE(sample); + std::vector gold; + histogramEqualization(inputs[sample], width, height, 1, gold); + auto output = test::planar::DownloadPlanarSample(*outAccess, sample, width, height, 1, 1); + EXPECT_EQ(output, gold); + } + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + // clang-format off NVCV_TEST_SUITE_P(OpHistogramEqVarShape, test::ValueList { @@ -210,12 +269,12 @@ TEST_P(OpHistogramEqVarShape, varshape_correct_output) const int height = GetParamValue<1>(); const nvcv::ImageFormat format{GetParamValue<2>()}; const int batches = GetParamValue<3>(); - for (bool AllPixelsHaveSameValue : std::vector{false, true}) + for (bool AllPixelsHaveSameValue : std::vector{false, true}) // NOSONAR { // Create input varshape - std::default_random_engine rng; - std::uniform_int_distribution udistWidth(width * 0.8, width * 1.1); - std::uniform_int_distribution udistHeight(height * 0.8, height * 1.1); + std::default_random_engine rng; + std::uniform_int_distribution udistWidth(ScaledSize(width, 0.8), ScaledSize(width, 1.1)); + std::uniform_int_distribution udistHeight(ScaledSize(height, 0.8), ScaledSize(height, 1.1)); std::vector imgSrc; @@ -236,11 +295,11 @@ TEST_P(OpHistogramEqVarShape, varshape_correct_output) // all pixels have same value if (AllPixelsHaveSameValue) { - std::generate(srcVec[i].begin(), srcVec[i].end(), [&]() { return 5; }); + std::ranges::generate(srcVec[i], []() { return 5; }); } else { - std::generate(srcVec[i].begin(), srcVec[i].end(), [&]() { return udist(rng); }); + std::ranges::generate(srcVec[i], [&udist, &rng]() { return udist(rng); }); } auto imgData = imgSrc[i].exportData(); @@ -306,20 +365,56 @@ TEST_P(OpHistogramEqVarShape, varshape_correct_output) ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); } -#undef NVCV_IMAGE_FORMAT_2U8 +// Planar == interleaved parity --------------------------------------------------------------- +// clang-format off +NVCV_TEST_SUITE_P(OpHistogramEqPlanar, + test::ValueList{ + {37, 29, 2, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + {32, 25, 1, nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8}, +}); + +NVCV_TEST_SUITE_P(OpHistogramEqPlanarVarShape, + test::ValueList{ + {37, 29, 2, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, +}); + +// clang-format on + +TEST_P(OpHistogramEqPlanar, tensor_matches_interleaved) +{ + const int numImages = GetParamValue<2>(); + test::planar::RunTensorParity( + GetParamValue<3>(), GetParamValue<4>(), GetParamValue<0>(), GetParamValue<1>(), GetParamValue<0>(), + GetParamValue<1>(), numImages, + [numImages](cudaStream_t s, const nvcv::Tensor &src, const nvcv::Tensor &dst, nvcv::ImageFormat) + { + cvcuda::HistogramEq op(numImages); + EXPECT_NO_THROW(op(s, src, dst)); + }); +} + +TEST_P(OpHistogramEqPlanarVarShape, varshape_matches_interleaved) +{ + test::planar::RunVarShapeParity( + GetParamValue<3>(), GetParamValue<4>(), GetParamValue<0>(), GetParamValue<1>(), GetParamValue<0>(), + GetParamValue<1>(), GetParamValue<2>(), + [](cudaStream_t s, const nvcv::ImageBatchVarShape &src, const nvcv::ImageBatchVarShape &dst, nvcv::ImageFormat) + { + cvcuda::HistogramEq op(src.numImages()); + EXPECT_NO_THROW(op(s, src, dst)); + }); +} // clang-format off NVCV_TEST_SUITE_P(OpHistogramEq_Negative, test::ValueList{ {nvcv::FMT_RGB8, nvcv::FMT_RGBf16}, {nvcv::FMT_RGBf16, nvcv::FMT_RGBf16}, {nvcv::FMT_RGB8, nvcv::FMT_RGB8p}, - {nvcv::FMT_RGB8p, nvcv::FMT_RGB8p}, }); NVCV_TEST_SUITE_P(OpHistogramEqVarshape_Negative, test::ValueList{ {nvcv::FMT_RGB8p, nvcv::FMT_RGB8, 2, 2}, {nvcv::FMT_RGB8, nvcv::FMT_RGB8p, 2, 2}, - {nvcv::FMT_RGB8p, nvcv::FMT_RGB8p, 2, 2}, {nvcv::FMT_RGB8, nvcv::FMT_RGBf16, 2, 2}, {nvcv::FMT_RGBf16, nvcv::FMT_RGBf16, 2, 2}, {nvcv::FMT_RGB8, nvcv::FMT_RGB8, 4, 3}, @@ -340,12 +435,38 @@ TEST_P(OpHistogramEq_Negative, op) // run operator cvcuda::HistogramEq op(2); - EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&] { op(stream, inTensor, outTensor); })); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&op, &stream, &inTensor, &outTensor] { op(stream, inTensor, outTensor); })); ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); } +TEST(OpHistogramEq_Negative, rejects_two_channel_planar_tensor) +{ + test::planar::ExpectPlanarTensorRejected({2, 2, 24, 24}, {2, 2, 24, 24}, + [](cudaStream_t s, const nvcv::Tensor &src, const nvcv::Tensor &dst) + { + cvcuda::HistogramEq op(2); + op(s, src, dst); + }); +} + +TEST(OpHistogramEq_Negative, rejects_two_channel_planar_varshape) +{ + const nvcv::ImageFormat twoChannelPlanar{NVCV_DETAIL_MAKE_NONCOLOR_FMT2(PL, UNSIGNED, XY00, ASSOCIATED, X8, X8)}; + + test::planar::ExpectVarShapeRejected( + {twoChannelPlanar, twoChannelPlanar}, {twoChannelPlanar, twoChannelPlanar}, + [](cudaStream_t s, const nvcv::ImageBatchVarShape &src, const nvcv::ImageBatchVarShape &dst) + { + cvcuda::HistogramEq op(2); + op(s, src, dst); + }); +} + +#undef NVCV_IMAGE_FORMAT_2U8 + TEST_P(OpHistogramEqVarshape_Negative, op) { cudaStream_t stream; @@ -357,9 +478,9 @@ TEST_P(OpHistogramEqVarshape_Negative, op) int outputImages = GetParamValue<3>(); // Create input varshape - std::default_random_engine rng; - std::uniform_int_distribution udistWidth(24 * 0.8, 24 * 1.1); - std::uniform_int_distribution udistHeight(24 * 0.8, 24 * 1.1); + std::default_random_engine rng; + std::uniform_int_distribution udistWidth(ScaledSize(24, 0.8), ScaledSize(24, 1.1)); + std::uniform_int_distribution udistHeight(ScaledSize(24, 0.8), ScaledSize(24, 1.1)); std::vector imgSrc; //setup the input images @@ -383,7 +504,8 @@ TEST_P(OpHistogramEqVarshape_Negative, op) // Run operator set the max batches cvcuda::HistogramEq op(std::max(inputImages, outputImages)); - EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&] { op(stream, batchSrc, batchDst); })); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&op, &stream, &batchSrc, &batchDst] { op(stream, batchSrc, batchDst); })); ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); @@ -400,6 +522,46 @@ TEST(OpHistogramEq_Negative, create_invalid_maxBatch) EXPECT_EQ(cvcudaHistogramEqCreate(&handle, 0), NVCV_ERROR_INVALID_ARGUMENT); } +TEST(OpHistogramEq_Negative, tensor_batch_exceeds_maxBatch) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + nvcv::Tensor inTensor = nvcv::util::CreateTensor(2, 4, 4, nvcv::FMT_U8); + nvcv::Tensor outTensor = nvcv::util::CreateTensor(2, 4, 4, nvcv::FMT_U8); + + cvcuda::HistogramEq op(1); + ASSERT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&op, &stream, &inTensor, &outTensor] { op(stream, inTensor, outTensor); })); + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpHistogramEqVarshape_Negative, varshape_batch_exceeds_maxBatch) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + std::vector imgSrc; + std::vector imgDst; + for (int i = 0; i < 2; ++i) + { + imgSrc.emplace_back(nvcv::Size2D{4, 4}, nvcv::FMT_U8); + imgDst.emplace_back(nvcv::Size2D{4, 4}, nvcv::FMT_U8); + } + + nvcv::ImageBatchVarShape batchSrc(2); + batchSrc.pushBack(imgSrc.begin(), imgSrc.end()); + nvcv::ImageBatchVarShape batchDst(2); + batchDst.pushBack(imgDst.begin(), imgDst.end()); + + cvcuda::HistogramEq op(1); + ASSERT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&op, &stream, &batchSrc, &batchDst] { op(stream, batchSrc, batchDst); })); + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + TEST(OpHistogramEqVarshape_Negative, varshape_hasDifferentFormat) { cudaStream_t stream; @@ -412,17 +574,14 @@ TEST(OpHistogramEqVarshape_Negative, varshape_hasDifferentFormat) { fmt, nvcv::FMT_U8} }; - for (auto testCase : testSet) + for (const auto &[inputFmtExtra, outputFmtExtra] : testSet) { - nvcv::ImageFormat inputFmtExtra = std::get<0>(testCase); - nvcv::ImageFormat outputFmtExtra = std::get<1>(testCase); - int numImages = 3; // Create input varshape - std::default_random_engine rng; - std::uniform_int_distribution udistWidth(24 * 0.8, 24 * 1.1); - std::uniform_int_distribution udistHeight(24 * 0.8, 24 * 1.1); + std::default_random_engine rng; + std::uniform_int_distribution udistWidth(ScaledSize(24, 0.8), ScaledSize(24, 1.1)); + std::uniform_int_distribution udistHeight(ScaledSize(24, 0.8), ScaledSize(24, 1.1)); std::vector imgSrc; std::vector imgDst; @@ -442,7 +601,8 @@ TEST(OpHistogramEqVarshape_Negative, varshape_hasDifferentFormat) // Run operator set the max batches cvcuda::HistogramEq op(numImages); - EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&] { op(stream, batchSrc, batchDst); })); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&op, &stream, &batchSrc, &batchDst] { op(stream, batchSrc, batchDst); })); EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); } diff --git a/tests/cvcuda/system/TestOpInpaint.cpp b/tests/cvcuda/system/TestOpInpaint.cpp index 67c734482..7c0a5dd02 100644 --- a/tests/cvcuda/system/TestOpInpaint.cpp +++ b/tests/cvcuda/system/TestOpInpaint.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,29 +16,44 @@ */ #include "Definitions.hpp" +#include "PlanarParityUtils.hpp" #include #include #include +#include #include #include #include #include +#include +#include #include +#include #include #include +#include +#include +#include using namespace std; -#define KNOWN 0 //known outside narrow band -#define BAND 1 //narrow band (known) -#define INSIDE 2 //unknown -#define CHANGE 3 //servise +constexpr uint8_t KNOWN = 0; // known outside narrow band +constexpr uint8_t BAND = 1; // narrow band (known) +constexpr uint8_t INSIDE = 2; // unknown +constexpr uint8_t CHANGE = 3; // service + +namespace { + +const nvcv::ImageFormat kFmt2U8Planar{NVCV_DETAIL_MAKE_NONCOLOR_FMT2(PL, UNSIGNED, XY00, ASSOCIATED, X8, X8)}; + +} // namespace struct Point2f { - float x, y; + float x; + float y; }; inline static float VectorScalMult(const Point2f &v1, const Point2f &v2) @@ -58,45 +73,60 @@ inline float min4(float a, float b, float c, float d) return min(a, c); } -typedef struct HeapElem +struct HeapElem { - float T; - int i, j; - struct HeapElem *prev; - struct HeapElem *next; -} HeapElem; + float T; + int i; + int j; + HeapElem *prev; + HeapElem *next; +}; class PriorityQueueFloat { private: - PriorityQueueFloat(const PriorityQueueFloat &); // copy disabled - PriorityQueueFloat &operator=(const PriorityQueueFloat &); // assign disabled + PriorityQueueFloat(const PriorityQueueFloat &) = delete; + PriorityQueueFloat &operator=(const PriorityQueueFloat &) = delete; -protected: - HeapElem *mem, *empty, *head, *tail; - int num, in; + std::vector storage; + HeapElem *mem = nullptr; + HeapElem *empty = nullptr; + HeapElem *head = nullptr; + HeapElem *tail = nullptr; + int num = 0; + int in = 0; public: bool Init(const vector &f, int height, int width) { - int i, j; - for (i = num = 0; i < height; i++) + num = 0; + for (int i = 0; i < height; i++) { - for (j = 0; j < width; j++) num += (f[i * width + j] != 0); + for (int j = 0; j < width; j++) + { + if (f[i * width + j] != 0) + { + num++; + } + } } if (num <= 0) + { + storage.clear(); + mem = empty = head = tail = nullptr; return false; - mem = (HeapElem *)malloc((num + 2) * sizeof(HeapElem)); - if (mem == NULL) - return false; + } + storage.resize(num + 2); + mem = storage.data(); head = mem; head->i = head->j = -1; - head->prev = NULL; + head->prev = nullptr; head->next = mem + 1; head->T = -FLT_MAX; empty = mem + 1; - for (i = 1; i <= num; i++) + int i = 1; + for (; i <= num; i++) { mem[i].prev = mem + i - 1; mem[i].next = mem + i + 1; @@ -106,23 +136,21 @@ class PriorityQueueFloat tail = mem + i; tail->i = tail->j = -1; tail->prev = mem + i - 1; - tail->next = NULL; + tail->next = nullptr; tail->T = FLT_MAX; return true; } bool Add(const vector &f, int height, int width) { - int i, j; - for (i = 0; i < height; i++) + for (int i = 0; i < height; i++) { - for (j = 0; j < width; j++) + for (int j = 0; j < width; j++) { - if (f[i * width + j] != 0) - { - if (!Push(i, j, 0)) - return false; - } + if (f[i * width + j] == 0) + continue; + if (!Push(i, j, 0)) + return false; } } return true; @@ -130,7 +158,8 @@ class PriorityQueueFloat bool Push(int i, int j, float T) { - HeapElem *tmp = empty, *add = empty; + HeapElem *tmp = empty; + HeapElem *add = empty; if (empty == tail) return false; while (tmp->prev->T > T) tmp = tmp->prev; @@ -192,227 +221,402 @@ class PriorityQueueFloat return true; } - PriorityQueueFloat(void) + PriorityQueueFloat() = default; + + ~PriorityQueueFloat() = default; +}; + +struct IntPoint2D +{ + int y; + int x; +}; + +struct FastMarchingData +{ + const vector &f; + const vector &t; + const vector &out; + int height; + int width; +}; + +struct InpaintTarget +{ + int y; + int x; + int range; + Point2f gradT; +}; + +struct InpaintAccumulator +{ + float ia = 0.0f; + float jx = 0.0f; + float jy = 0.0f; + float s = 1.0e-20f; +}; + +static int GridIndex(int y, int x, int width) +{ + return y * width + x; +} + +static int OutputWidth(int width) +{ + return width - 2; +} + +static int OutputIndex(int y, int x, int width) +{ + return y * OutputWidth(width) + x; +} + +static int ScaleDimension(int value, double scale) +{ + return static_cast(static_cast(value) * scale); +} + +static float PixelCount(int height, int width) +{ + return static_cast(height) * static_cast(width); +} + +static int AbsDiff(uint8_t lhs, uint8_t rhs) +{ + return std::abs(static_cast(lhs) - static_cast(rhs)); +} + +static float PixelDiff(uint8_t lhs, uint8_t rhs) +{ + return static_cast(static_cast(lhs) - static_cast(rhs)); +} + +static float SelectDifference(float center, float next, float prev, bool hasNext, bool hasPrev, float centeredScale) +{ + if (hasNext && hasPrev) { - num = in = 0; - mem = empty = head = tail = NULL; + return (next - prev) * centeredScale; } - - ~PriorityQueueFloat(void) + if (hasNext) { - free(mem); + return next - center; } -}; + if (hasPrev) + { + return center - prev; + } + return 0.0f; +} static float FastMarching_solve(int i1, int j1, int i2, int j2, const vector &f, const vector &t, int width) { - double sol, a11, a22, m12; - a11 = t[i1 * width + j1]; - a22 = t[i2 * width + j2]; - m12 = min(a11, a22); - - if (f[i1 * width + j1] != INSIDE) - if (f[i2 * width + j2] != INSIDE) - if (fabs(a11 - a22) >= 1.0) - sol = 1 + m12; - else - sol = (a11 + a22 + sqrt((double)(2 - (a11 - a22) * (a11 - a22)))) * 0.5; - else - sol = 1 + a11; - else if (f[i2 * width + j2] != INSIDE) - sol = 1 + a22; - else - sol = 1 + m12; + double a11 = t[GridIndex(i1, j1, width)]; + double a22 = t[GridIndex(i2, j2, width)]; + double m12 = min(a11, a22); + + bool firstKnown = f[GridIndex(i1, j1, width)] != INSIDE; + bool secondKnown = f[GridIndex(i2, j2, width)] != INSIDE; + + if (firstKnown && secondKnown) + { + double diff = a11 - a22; + if (fabs(diff) >= 1.0) + { + return static_cast(1 + m12); + } + return static_cast((a11 + a22 + sqrt(2.0 - diff * diff)) * 0.5); + } + + if (firstKnown) + { + return static_cast(1 + a11); + } + if (secondKnown) + { + return static_cast(1 + a22); + } + return static_cast(1 + m12); +} + +static Point2f FastMarchingGradientAt(int y, int x, const vector &f, const vector &t, int width) +{ + int center = GridIndex(y, x, width); + + return {SelectDifference(t[center], t[center + 1], t[center - 1], f[center + 1] != INSIDE, f[center - 1] != INSIDE, + 0.5f), + SelectDifference(t[center], t[center + width], t[center - width], f[center + width] != INSIDE, + f[center - width] != INSIDE, 0.5f)}; +} + +static float HorizontalImageGradient(const FastMarchingData &data, int y, int x, int outY, int leftX, int rightX) +{ + bool hasRight = data.f[GridIndex(y, x + 1, data.width)] != INSIDE; + bool hasLeft = data.f[GridIndex(y, x - 1, data.width)] != INSIDE; + + if (hasRight && hasLeft) + { + return PixelDiff(data.out[OutputIndex(outY, rightX + 1, data.width)], + data.out[OutputIndex(outY, leftX - 1, data.width)]) + * 2.0f; + } + if (hasRight) + { + return PixelDiff(data.out[OutputIndex(outY, rightX + 1, data.width)], + data.out[OutputIndex(outY, leftX, data.width)]); + } + if (hasLeft) + { + return PixelDiff(data.out[OutputIndex(outY, rightX, data.width)], + data.out[OutputIndex(outY, leftX - 1, data.width)]); + } + return 0.0f; +} + +static float VerticalImageGradient(const FastMarchingData &data, int y, int x, int outX, int topY, int bottomY) +{ + bool hasBottom = data.f[GridIndex(y + 1, x, data.width)] != INSIDE; + bool hasTop = data.f[GridIndex(y - 1, x, data.width)] != INSIDE; + + if (hasBottom && hasTop) + { + return PixelDiff(data.out[OutputIndex(bottomY + 1, outX, data.width)], + data.out[OutputIndex(topY - 1, outX, data.width)]) + * 2.0f; + } + if (hasBottom) + { + return PixelDiff(data.out[OutputIndex(bottomY + 1, outX, data.width)], + data.out[OutputIndex(topY, outX, data.width)]); + } + if (hasTop) + { + return PixelDiff(data.out[OutputIndex(bottomY, outX, data.width)], + data.out[OutputIndex(topY - 1, outX, data.width)]); + } + return 0.0f; +} + +static bool IsInteriorPoint(int y, int x, int height, int width) +{ + return y > 0 && x > 0 && y < height - 1 && x < width - 1; +} + +static bool IsUsableSourcePixel(const FastMarchingData &data, const InpaintTarget &target, int y, int x) +{ + if (!IsInteriorPoint(y, x, data.height, data.width)) + { + return false; + } - return (float)sol; + int dy = y - target.y; + int dx = x - target.x; + return data.f[GridIndex(y, x, data.width)] != INSIDE && dx * dx + dy * dy <= target.range * target.range; +} + +static void AccumulateSourcePixel(const FastMarchingData &data, const InpaintTarget &target, int y, int x, + InpaintAccumulator &acc) +{ + if (!IsUsableSourcePixel(data, target, y, x)) + { + return; + } + + int outYBottom = y - 1 - (y == data.height - 2 ? 1 : 0); + int outYTop = y - 1 + (y == 1 ? 1 : 0); + int outXLeft = x - 1 + (x == 1 ? 1 : 0); + int outXRight = x - 1 - (x == data.width - 2 ? 1 : 0); + + Point2f r{static_cast(target.x - x), static_cast(target.y - y)}; + + float vectorLength = VectorLength(r); + auto dst = static_cast(1.0 / (vectorLength * sqrt(vectorLength))); + auto lev = static_cast( + 1.0 / (1 + fabs(data.t[GridIndex(y, x, data.width)] - data.t[GridIndex(target.y, target.x, data.width)]))); + float dir = VectorScalMult(r, target.gradT); + if (fabs(dir) <= 0.01) + { + dir = 0.000001f; + } + + auto weight = std::fabs(dst * lev * dir); + Point2f gradI{HorizontalImageGradient(data, y, x, outYTop, outXLeft, outXRight), + VerticalImageGradient(data, y, x, outXLeft, outYTop, outYBottom)}; + int outIndex = OutputIndex(outYTop, outXLeft, data.width); + + acc.ia += weight * static_cast(data.out[outIndex]); + acc.jx -= weight * gradI.x * r.x; + acc.jy -= weight * gradI.y * r.y; + acc.s += weight; +} + +static uint8_t SaturateToU8(float value) +{ + auto rounded = static_cast(std::round(value)); + if (rounded <= 0) + { + return 0; + } + if (rounded >= UCHAR_MAX) + { + return UCHAR_MAX; + } + return static_cast(rounded); +} + +static void InpaintTargetPixel(const FastMarchingData &data, const InpaintTarget &target, vector &out) +{ + InpaintAccumulator acc; + + for (int y = target.y - target.range; y <= target.y + target.range; y++) + { + for (int x = target.x - target.range; x <= target.x + target.range; x++) + { + AccumulateSourcePixel(data, target, y, x, acc); + } + } + + auto sat = acc.ia / acc.s + (acc.jx + acc.jy) / (sqrt(acc.jx * acc.jx + acc.jy * acc.jy) + 1.0e-20f) + 0.5f; + out[OutputIndex(target.y - 1, target.x - 1, data.width)] = SaturateToU8(sat); +} + +static float NeighborDistance(int y, int x, const vector &f, const vector &t, int width) +{ + return min4( + FastMarching_solve(y - 1, x, y, x - 1, f, t, width), FastMarching_solve(y + 1, x, y, x - 1, f, t, width), + FastMarching_solve(y - 1, x, y, x + 1, f, t, width), FastMarching_solve(y + 1, x, y, x + 1, f, t, width)); } static void InpaintFMM(vector &f, vector &t, vector &out, int range, shared_ptr Heap, int height, int width) { - int i = 0, j = 0, ii = 0, jj = 0, k, l, q, color = 0; - float dist; + constexpr array kNeighbors{ + {{-1, 0}, {0, -1}, {1, 0}, {0, 1}} + }; + int ii = 0; + int jj = 0; while (Heap->Pop(&ii, &jj)) { - f[ii * width + jj] = KNOWN; - for (q = 0; q < 4; q++) + f[GridIndex(ii, jj, width)] = KNOWN; + for (const IntPoint2D &offset : kNeighbors) { - if (q == 0) + int i = ii + offset.y; + int j = jj + offset.x; + if ((i <= 0) || (j <= 0) || (i > height - 1) || (j > width - 1)) { - i = ii - 1; - j = jj; + continue; } - else if (q == 1) + + if (f[GridIndex(i, j, width)] != INSIDE) { - i = ii; - j = jj - 1; + continue; } - else if (q == 2) + + float dist = NeighborDistance(i, j, f, t, width); + t[GridIndex(i, j, width)] = dist; + + FastMarchingData data{f, t, out, height, width}; + InpaintTarget target{i, j, range, FastMarchingGradientAt(i, j, f, t, width)}; + InpaintTargetPixel(data, target, out); + + f[GridIndex(i, j, width)] = BAND; + Heap->Push(i, j, dist); + } + } +} + +template +static void CopyMaskWithBorder(const vector &orgMask, vector &mask, int height, int width, int ecols) +{ + for (int i = 0; i < height; i++) + { + for (int j = 0; j < width; j++) + { + if (!orgMask[GridIndex(i, j, width)]) { - i = ii + 1; - j = jj; + continue; } - else if (q == 3) + mask[GridIndex(i + 1, j + 1, ecols)] = INSIDE; + } + } +} + +static void SetKnownBorder(vector &mask, int erows, int ecols) +{ + for (int i = 0; i < ecols; i++) + { + mask[i] = KNOWN; + mask[GridIndex(erows - 1, i, ecols)] = KNOWN; + } + for (int i = 0; i < erows; i++) + { + mask[GridIndex(i, 0, ecols)] = KNOWN; + mask[GridIndex(i, ecols - 1, ecols)] = KNOWN; + } +} + +static uint8_t DilatedMaskValue(const vector &mask, const array &kernel, int y, int x, int ecols) +{ + int kernelIndex = 0; + uint8_t result = 0; + for (int dy = 0; dy < 3; dy++) + { + for (int dx = 0; dx < 3; dx++) + { + if (kernel[kernelIndex] == 0) { - i = ii; - j = jj + 1; - } - if ((i <= 0) || (j <= 0) || (i > height - 1) || (j > width - 1)) + kernelIndex++; continue; + } + result = max(result, mask[GridIndex(y - 1 + dy, x - 1 + dx, ecols)]); + kernelIndex++; + } + } + return result; +} - if (f[i * width + j] == INSIDE) - { - dist = min4(FastMarching_solve(i - 1, j, i, j - 1, f, t, width), - FastMarching_solve(i + 1, j, i, j - 1, f, t, width), - FastMarching_solve(i - 1, j, i, j + 1, f, t, width), - FastMarching_solve(i + 1, j, i, j + 1, f, t, width)); - t[i * width + j] = dist; +static void DilateMask(const vector &mask, vector &band, int erows, int ecols) +{ + constexpr array kKernel{0, 1, 0, 1, 1, 1, 0, 1, 0}; - for (color = 0; color <= 0; color++) - { - Point2f gradI, gradT, r; - float Ia = 0, Jx = 0, Jy = 0, s = 1.0e-20f, w, dst, lev, dir, sat; - - if (f[i * width + j + 1] != INSIDE) - { - if (f[i * width + j - 1] != INSIDE) - { - gradT.x = (float)((t[i * width + j + 1] - t[i * width + j - 1])) * 0.5f; - } - else - { - gradT.x = (float)((t[i * width + j + 1] - t[i * width + j])); - } - } - else - { - if (f[i * width + j - 1] != INSIDE) - { - gradT.x = (float)((t[i * width + j] - t[i * width + j - 1])); - } - else - { - gradT.x = 0; - } - } - if (f[(i + 1) * width + j] != INSIDE) - { - if (f[(i - 1) * width + j] != INSIDE) - { - gradT.y = (float)((t[(i + 1) * width + j] - t[(i - 1) * width + j])) * 0.5f; - } - else - { - gradT.y = (float)((t[(i + 1) * width + j] - t[i * width + j])); - } - } - else - { - if (f[(i - 1) * width + j] != INSIDE) - { - gradT.y = (float)((t[i * width + j] - t[(i - 1) * width + j])); - } - else - { - gradT.y = 0; - } - } - for (k = i - range; k <= i + range; k++) - { - int km = k - 1 + (k == 1), kp = k - 1 - (k == height - 2); - for (l = j - range; l <= j + range; l++) - { - int lm = l - 1 + (l == 1), lp = l - 1 - (l == width - 2); - if (k > 0 && l > 0 && k < height - 1 && l < width - 1) - { - if ((f[k * width + l] != INSIDE) - && ((l - j) * (l - j) + (k - i) * (k - i) <= range * range)) - { - r.y = (float)(i - k); - r.x = (float)(j - l); - - dst = (float)(1. / (VectorLength(r) * sqrt(VectorLength(r)))); - lev = (float)(1. / (1 + fabs(t[k * width + l] - t[i * width + j]))); - - dir = VectorScalMult(r, gradT); - if (fabs(dir) <= 0.01) - dir = 0.000001f; - w = (float)fabs(dst * lev * dir); - - if (f[k * width + l + 1] != INSIDE) - { - if (f[k * width + l - 1] != INSIDE) - { - gradI.x = (float)((out[km * (width - 2) + lp + 1] - - out[km * (width - 2) + lm - 1])) - * 2.0f; - } - else - { - gradI.x = (float)((out[km * (width - 2) + lp + 1] - - out[km * (width - 2) + lm])); - } - } - else - { - if (f[k * width + l - 1] != INSIDE) - { - gradI.x = (float)((out[km * (width - 2) + lp] - - out[km * (width - 2) + lm - 1])); - } - else - { - gradI.x = 0; - } - } - if (f[(k + 1) * width + l] != INSIDE) - { - if (f[(k - 1) * width + l] != INSIDE) - { - gradI.y = (float)((out[(kp + 1) * (width - 2) + lm] - - out[(km - 1) * (width - 2) + lm])) - * 2.0f; - } - else - { - gradI.y = (float)((out[(kp + 1) * (width - 2) + lm] - - out[km * (width - 2) + lm])); - } - } - else - { - if (f[(k - 1) * width + l] != INSIDE) - { - gradI.y = (float)((out[kp * (width - 2) + lm] - - out[(km - 1) * (width - 2) + lm])); - } - else - { - gradI.y = 0; - } - } - Ia += (float)w * (float)(out[km * (width - 2) + lm]); - Jx -= (float)w * (float)(gradI.x * r.x); - Jy -= (float)w * (float)(gradI.y * r.y); - s += w; - } - } - } - } - sat = (float)((Ia / s + (Jx + Jy) / (sqrt(Jx * Jx + Jy * Jy) + 1.0e-20f) + 0.5f)); - { - int v = round(sat); - out[(i - 1) * (width - 2) + (j - 1)] = (uint8_t)((unsigned)v <= UCHAR_MAX ? v - : v > 0 ? UCHAR_MAX - : 0); - ; - } - } + for (int i = 1; i < erows - 1; i++) + { + for (int j = 1; j < ecols - 1; j++) + { + band[GridIndex(i, j, ecols)] = DilatedMaskValue(mask, kKernel, i, j, ecols); + } + } +} - f[i * width + j] = BAND; - Heap->Push(i, j, dist); +static void SubtractMaskFromBand(vector &band, const vector &mask, int erows, int ecols) +{ + for (int i = 1; i < erows - 1; i++) + { + for (int j = 1; j < ecols - 1; j++) + { + band[GridIndex(i, j, ecols)] -= mask[GridIndex(i, j, ecols)]; + } + } +} + +static void ApplyBandAndMask(vector &f, vector &t, const vector &band, + const vector &mask, int erows, int ecols) +{ + for (int i = 0; i < erows; i++) + { + for (int j = 0; j < ecols; j++) + { + int index = GridIndex(i, j, ecols); + if (band[index]) + { + f[index] = BAND; + t[index] = 0; + } + if (mask[index]) + { + f[index] = INSIDE; } } } @@ -422,77 +626,619 @@ static void InpaintFMM(vector &f, vector &t, vector &ou template void Inpaint(std::vector &src, std::vector &dst, std::vector &org_mask, double radius, int height, int width) { - int range = (int)std::round(radius); - range = std::max(range, 1); - range = std::min(range, 100); + auto range = static_cast(std::round(radius)); + range = std::max(range, 1); + range = std::min(range, 100); - int erows = height + 2, ecols = width + 2; + int erows = height + 2; + int ecols = width + 2; vector f(erows * ecols, KNOWN); vector t(erows * ecols, 1.0e6f); vector band(erows * ecols, KNOWN); vector mask(erows * ecols, KNOWN); - vector kernel = {0, 1, 0, 1, 1, 1, 0, 1, 0}; - // cvCopy(input_img, output_img); - dst.assign(src.begin(), src.end()); - // COPY_MASK_BORDER1_C1(inpaint_mask, mask, uchar); - for (int i = 0; i < height; i++) - for (int j = 0; j < width; j++) - if (org_mask[i * width + j]) - mask[(i + 1) * ecols + j + 1] = INSIDE; - // SET_BORDER1_C1(mask, uchar, 0); - for (int i = 0; i < ecols; i++) + dst.assign(src.begin(), src.end()); + CopyMaskWithBorder(org_mask, mask, height, width, ecols); + SetKnownBorder(mask, erows, ecols); + DilateMask(mask, band, erows, ecols); + + auto heap = make_shared(); + if (!heap->Init(band, erows, ecols)) + return; + + SubtractMaskFromBand(band, mask, erows, ecols); + if (!heap->Add(band, erows, ecols)) + return; + + ApplyBandAndMask(f, t, band, mask, erows, ecols); + + InpaintFMM(f, t, dst, range, heap, erows, ecols); +} + +namespace { + +enum class StrictInpaintRoute +{ + NHWC, + NCHW, + NCHW_FAKE, + HWC, + CHW, +}; + +const char *StrictRouteName(StrictInpaintRoute route) +{ + switch (route) + { + case StrictInpaintRoute::NHWC: + return "NHWC"; + case StrictInpaintRoute::NCHW: + return "NCHW"; + case StrictInpaintRoute::NCHW_FAKE: + return "NCHW_FAKE"; + case StrictInpaintRoute::HWC: + return "HWC"; + case StrictInpaintRoute::CHW: + return "CHW"; + } + return "unknown"; +} + +bool StrictRouteIsPlanar(StrictInpaintRoute route) +{ + return route == StrictInpaintRoute::NCHW || route == StrictInpaintRoute::NCHW_FAKE + || route == StrictInpaintRoute::CHW; +} + +bool StrictRouteIsRank3(StrictInpaintRoute route) +{ + return route == StrictInpaintRoute::HWC || route == StrictInpaintRoute::CHW; +} + +size_t StrictOutputIndex(int y, int x, int channel, int height, int width, int channels, bool planar) +{ + if (planar) + { + return (static_cast(channel) * height + y) * width + x; + } + return (static_cast(y) * width + x) * channels + channel; +} + +template +struct StrictFastMarchingData +{ + const vector &f; + const vector &t; + const vector &out; + int paddedHeight; + int paddedWidth; + int channels; + bool planar; +}; + +template +float StrictPixelDifference(T lhs, T rhs) +{ + return static_cast(lhs) - static_cast(rhs); +} + +template +float StrictHorizontalImageGradient(const StrictFastMarchingData &data, int y, int x, int outY, int leftX, + int rightX, int channel) +{ + const bool hasRight = data.f[GridIndex(y, x + 1, data.paddedWidth)] != INSIDE; + const bool hasLeft = data.f[GridIndex(y, x - 1, data.paddedWidth)] != INSIDE; + const int height = data.paddedHeight - 2; + const int width = data.paddedWidth - 2; + + if (hasRight && hasLeft) + { + return StrictPixelDifference( + data.out[StrictOutputIndex(outY, rightX + 1, channel, height, width, data.channels, data.planar)], + data.out[StrictOutputIndex(outY, leftX - 1, channel, height, width, data.channels, data.planar)]) + * 2.0f; + } + if (hasRight) + { + return StrictPixelDifference( + data.out[StrictOutputIndex(outY, rightX + 1, channel, height, width, data.channels, data.planar)], + data.out[StrictOutputIndex(outY, leftX, channel, height, width, data.channels, data.planar)]); + } + if (hasLeft) + { + return StrictPixelDifference( + data.out[StrictOutputIndex(outY, rightX, channel, height, width, data.channels, data.planar)], + data.out[StrictOutputIndex(outY, leftX - 1, channel, height, width, data.channels, data.planar)]); + } + return 0.0f; +} + +template +float StrictVerticalImageGradient(const StrictFastMarchingData &data, int y, int x, int outX, int topY, int bottomY, + int channel) +{ + const bool hasBottom = data.f[GridIndex(y + 1, x, data.paddedWidth)] != INSIDE; + const bool hasTop = data.f[GridIndex(y - 1, x, data.paddedWidth)] != INSIDE; + const int height = data.paddedHeight - 2; + const int width = data.paddedWidth - 2; + + if (hasBottom && hasTop) + { + return StrictPixelDifference( + data.out[StrictOutputIndex(bottomY + 1, outX, channel, height, width, data.channels, data.planar)], + data.out[StrictOutputIndex(topY - 1, outX, channel, height, width, data.channels, data.planar)]) + * 2.0f; + } + if (hasBottom) + { + return StrictPixelDifference( + data.out[StrictOutputIndex(bottomY + 1, outX, channel, height, width, data.channels, data.planar)], + data.out[StrictOutputIndex(topY, outX, channel, height, width, data.channels, data.planar)]); + } + if (hasTop) + { + return StrictPixelDifference( + data.out[StrictOutputIndex(bottomY, outX, channel, height, width, data.channels, data.planar)], + data.out[StrictOutputIndex(topY - 1, outX, channel, height, width, data.channels, data.planar)]); + } + return 0.0f; +} + +template +bool StrictIsUsableSourcePixel(const StrictFastMarchingData &data, const InpaintTarget &target, int y, int x) +{ + if (!IsInteriorPoint(y, x, data.paddedHeight, data.paddedWidth) || (y == target.y && x == target.x)) + { + return false; + } + + const int dy = y - target.y; + const int dx = x - target.x; + return data.f[GridIndex(y, x, data.paddedWidth)] != INSIDE && dx * dx + dy * dy <= target.range * target.range; +} + +template +void StrictAccumulateSourcePixel(const StrictFastMarchingData &data, const InpaintTarget &target, int y, int x, + int channel, InpaintAccumulator &acc) +{ + if (!StrictIsUsableSourcePixel(data, target, y, x)) + { + return; + } + + const int outYBottom = y - 1 - (y == data.paddedHeight - 2 ? 1 : 0); + const int outYTop = y - 1 + (y == 1 ? 1 : 0); + const int outXLeft = x - 1 + (x == 1 ? 1 : 0); + const int outXRight = x - 1 - (x == data.paddedWidth - 2 ? 1 : 0); + + const Point2f r{static_cast(target.x - x), static_cast(target.y - y)}; + + const float vectorLength = VectorLength(r); + const auto distanceWeight = static_cast(1.0 / (vectorLength * std::sqrt(static_cast(vectorLength)))); + const auto levelWeight = static_cast( + 1.0 + / (1.0 + + std::fabs(static_cast(data.t[GridIndex(y, x, data.paddedWidth)] + - data.t[GridIndex(target.y, target.x, data.paddedWidth)])))); + float direction = VectorScalMult(r, target.gradT); + if (std::fabs(direction) <= 0.01f) + { + direction = 0.000001f; + } + + const float weight = std::fabs(distanceWeight * levelWeight * direction); + const Point2f gradI{ + StrictHorizontalImageGradient(data, y, x, outYTop, outXLeft, outXRight, channel), + StrictVerticalImageGradient(data, y, x, outXLeft, outYTop, outYBottom, channel), + }; + const int height = data.paddedHeight - 2; + const int width = data.paddedWidth - 2; + const T source = data.out[StrictOutputIndex(outYTop, outXLeft, channel, height, width, data.channels, data.planar)]; + + acc.ia += weight * static_cast(source); + acc.jx -= weight * gradI.x * r.x; + acc.jy -= weight * gradI.y * r.y; + acc.s += weight; +} + +// CUDA's cvt.rni.sat.u8.f32 is round-to-nearest-even. Keep this host oracle independent from +// SaturateCast so a change to the production conversion cannot change the expected result too. +uint8_t StrictSaturateToU8(float value) +{ + if (std::isnan(value) || value <= 0.0f) + { + return 0; + } + if (value >= 255.0f) + { + return 255; + } + + float rounded = std::floor(value); + if (const float fraction = value - rounded; + fraction > 0.5f || (fraction == 0.5f && (static_cast(rounded) & 1) != 0)) + { + rounded += 1.0f; + } + return static_cast(rounded); +} + +template +void StrictInpaintTargetPixel(const StrictFastMarchingData &data, const InpaintTarget &target, vector &out) +{ + const int height = data.paddedHeight - 2; + const int width = data.paddedWidth - 2; + + for (int channel = 0; channel < data.channels; channel++) + { + InpaintAccumulator acc; + for (int y = target.y - target.range; y <= target.y + target.range; y++) + { + for (int x = target.x - target.range; x <= target.x + target.range; x++) + { + StrictAccumulateSourcePixel(data, target, y, x, channel, acc); + } + } + + const double correctionDenominator + = std::sqrt(static_cast(acc.jx * acc.jx + acc.jy * acc.jy)) + 1.0e-20; + const auto value = static_cast(acc.ia / acc.s + (acc.jx + acc.jy) / correctionDenominator + 0.5); + out[StrictOutputIndex(target.y - 1, target.x - 1, channel, height, width, data.channels, data.planar)] + = static_cast(StrictSaturateToU8(value)); + } +} + +template +void StrictInpaintFMM(vector &f, vector &t, vector &out, int range, + const shared_ptr &heap, int paddedHeight, int paddedWidth, int channels, + bool planar) +{ + constexpr array kNeighbors{ + {{-1, 0}, {0, -1}, {1, 0}, {0, 1}} + }; + + int y = 0; + int x = 0; + while (heap->Pop(&y, &x)) + { + f[GridIndex(y, x, paddedWidth)] = KNOWN; + for (const IntPoint2D &offset : kNeighbors) + { + const int targetY = y + offset.y; + const int targetX = x + offset.x; + if (targetY <= 0 || targetX <= 0 || targetY >= paddedHeight - 1 || targetX >= paddedWidth - 1 + || f[GridIndex(targetY, targetX, paddedWidth)] != INSIDE) + { + continue; + } + + const float distance = NeighborDistance(targetY, targetX, f, t, paddedWidth); + t[GridIndex(targetY, targetX, paddedWidth)] = distance; + + const StrictFastMarchingData data{f, t, out, paddedHeight, paddedWidth, channels, planar}; + const InpaintTarget target{targetY, targetX, range, + FastMarchingGradientAt(targetY, targetX, f, t, paddedWidth)}; + StrictInpaintTargetPixel(data, target, out); + + f[GridIndex(targetY, targetX, paddedWidth)] = BAND; + heap->Push(targetY, targetX, distance); + } + } +} + +template +vector StrictInpaintReference(const vector &src, const vector &originalMask, double radius, int height, + int width, int channels, bool planar) +{ + const int range = std::clamp(static_cast(std::round(radius)), 1, 100); + const int paddedHeight = height + 2; + const int paddedWidth = width + 2; + + vector f(paddedHeight * paddedWidth, KNOWN); + vector t(paddedHeight * paddedWidth, 1.0e6f); + vector band(paddedHeight * paddedWidth, KNOWN); + vector mask(paddedHeight * paddedWidth, KNOWN); + vector out = src; + + vector originalMaskCopy = originalMask; + CopyMaskWithBorder(originalMaskCopy, mask, height, width, paddedWidth); + SetKnownBorder(mask, paddedHeight, paddedWidth); + DilateMask(mask, band, paddedHeight, paddedWidth); + + auto heap = make_shared(); + if (!heap->Init(band, paddedHeight, paddedWidth)) + { + return out; + } + + SubtractMaskFromBand(band, mask, paddedHeight, paddedWidth); + if (!heap->Add(band, paddedHeight, paddedWidth)) + { + return out; + } + + ApplyBandAndMask(f, t, band, mask, paddedHeight, paddedWidth); + StrictInpaintFMM(f, t, out, range, heap, paddedHeight, paddedWidth, channels, planar); + return out; +} + +template +T StrictInputValue(int sample, int y, int x, int channel) +{ + const int base = (sample * 61 + y * 29 + x * 17 + channel * 43 + (x * y) % 19) % 241; + if constexpr (std::is_same_v) + { + return static_cast(base + 7); + } + else if constexpr (std::is_same_v) + { + return base - 73; + } + else + { + static_assert(std::is_same_v); + return static_cast(base - 61) + static_cast((x + 2 * y + 3 * channel + sample) % 8) * 0.125f; + } +} + +template +vector MakeStrictInput(int sample, int height, int width, int channels, bool planar) +{ + vector input(static_cast(height) * width * channels); + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + for (int channel = 0; channel < channels; channel++) + { + input[StrictOutputIndex(y, x, channel, height, width, channels, planar)] + = StrictInputValue(sample, y, x, channel); + } + } + } + return input; +} + +vector MakeStrictMask(int sample, int height, int width) +{ + vector mask(static_cast(height) * width, 0); + + // Isolated holes make the expected result independent of the order in which parallel band + // threads reach a target, while the second hole exercises the bottom/right launch tail. + const array holes = sample % 2 == 0 + ? array{IntPoint2D{4, 3}, IntPoint2D{height - 4, width - 4}} + : array{IntPoint2D{3, width - 5}, IntPoint2D{height - 4, 4}}; + mask[GridIndex(holes[0].y, holes[0].x, width)] = 1; + mask[GridIndex(holes[1].y, holes[1].x, width)] = 255; + return mask; +} + +nvcv::Tensor MakeStrictImageTensor(StrictInpaintRoute route, int batch, int height, int width, int channels, + nvcv::DataType dtype) +{ + if (route == StrictInpaintRoute::HWC) + { + return nvcv::Tensor( + { + {height, width, channels}, + "HWC" + }, + dtype); + } + if (route == StrictInpaintRoute::CHW) + { + return nvcv::Tensor( + { + {channels, height, width}, + "CHW" + }, + dtype); + } + if (StrictRouteIsPlanar(route)) + { + return nvcv::Tensor( + { + {batch, channels, height, width}, + "NCHW" + }, + dtype); + } + return nvcv::Tensor( + { + {batch, height, width, channels}, + "NHWC" + }, + dtype); +} + +nvcv::Tensor MakeStrictMaskTensor(StrictInpaintRoute route, int batch, int height, int width) +{ + // The public contract allows only HWC/NHWC masks, including when input/output are planar. + if (StrictRouteIsRank3(route)) + { + return nvcv::Tensor( + { + {height, width, 1}, + "HWC" + }, + nvcv::TYPE_U8); + } + return nvcv::Tensor( + { + {batch, height, width, 1}, + "NHWC" + }, + nvcv::TYPE_U8); +} + +template +void RunStrictInpaintTensorCase(nvcv::DataType dtype, int channels, StrictInpaintRoute route, int batch, double radius) +{ + constexpr int kHeight = 17; + constexpr int kWidth = 19; + + ASSERT_FALSE(StrictRouteIsRank3(route) && batch != 1); + const bool planar = StrictRouteIsPlanar(route); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + nvcv::Tensor src = MakeStrictImageTensor(route, batch, kHeight, kWidth, channels, dtype); + nvcv::Tensor dst = MakeStrictImageTensor(route, batch, kHeight, kWidth, channels, dtype); + nvcv::Tensor mask = MakeStrictMaskTensor(route, batch, kHeight, kWidth); + + auto srcData = src.exportData(); + auto dstData = dst.exportData(); + auto maskData = mask.exportData(); + ASSERT_TRUE(srcData && dstData && maskData); + + vector> source(batch); + vector> expected(batch); + vector> masks(batch); + for (int sample = 0; sample < batch; sample++) + { + source[sample] = MakeStrictInput(sample, kHeight, kWidth, channels, planar); + masks[sample] = MakeStrictMask(sample, kHeight, kWidth); + expected[sample] + = StrictInpaintReference(source[sample], masks[sample], radius, kHeight, kWidth, channels, planar); + nvcv::util::SetImageTensorFromVector(*srcData, source[sample], sample); + nvcv::util::SetImageTensorFromVector(*maskData, masks[sample], sample); + } + + cvcuda::Inpaint op(batch, nvcv::Size2D{kWidth, kHeight}); + if (route == StrictInpaintRoute::NCHW_FAKE) + { + nvcv::Tensor interSrc( + { + {batch, kHeight, kWidth, channels}, + "NHWC" + }, + dtype); + nvcv::Tensor interDst( + { + {batch, kHeight, kWidth, channels}, + "NHWC" + }, + dtype); + cvcuda::Reformat reformat; + EXPECT_NO_THROW(reformat(stream, src, interSrc)); + EXPECT_NO_THROW(op(stream, interSrc, mask, interDst, radius)); + EXPECT_NO_THROW(reformat(stream, interDst, dst)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + } + else + { + EXPECT_NO_THROW(op(stream, src, mask, dst, radius)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + } + + for (int sample = 0; sample < batch; sample++) + { + SCOPED_TRACE(::testing::Message() << "route=" << StrictRouteName(route) << " sample=" << sample); + vector actual; + nvcv::util::GetImageVectorFromTensor(*dstData, sample, actual); + EXPECT_EQ(expected[sample], actual); + } + + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +// clang-format off +NVCV_TEST_SUITE_P(OpInpaintStrictTensor, + nvcv::test::ValueList +{ + // dtype, channels, route, batch, radius + {nvcv::TYPE_U8, 1, StrictInpaintRoute::NHWC, 1, 2.0}, + {nvcv::TYPE_U8, 2, StrictInpaintRoute::NHWC, 1, 5.0}, + {nvcv::TYPE_U8, 3, StrictInpaintRoute::NHWC, 2, 2.0}, + {nvcv::TYPE_U8, 4, StrictInpaintRoute::NHWC, 1, 5.0}, + {nvcv::TYPE_S32, 1, StrictInpaintRoute::NHWC, 1, 5.0}, + {nvcv::TYPE_S32, 2, StrictInpaintRoute::NHWC, 1, 2.0}, + {nvcv::TYPE_S32, 3, StrictInpaintRoute::NHWC, 1, 5.0}, + {nvcv::TYPE_S32, 4, StrictInpaintRoute::NHWC, 1, 2.0}, + {nvcv::TYPE_F32, 1, StrictInpaintRoute::NHWC, 1, 2.0}, + {nvcv::TYPE_F32, 2, StrictInpaintRoute::NHWC, 1, 5.0}, + {nvcv::TYPE_F32, 3, StrictInpaintRoute::NHWC, 1, 2.0}, + {nvcv::TYPE_F32, 4, StrictInpaintRoute::NHWC, 1, 5.0}, + + {nvcv::TYPE_U8, 1, StrictInpaintRoute::NCHW, 1, 5.0}, + {nvcv::TYPE_U8, 3, StrictInpaintRoute::NCHW, 1, 2.0}, + {nvcv::TYPE_U8, 4, StrictInpaintRoute::NCHW, 1, 5.0}, + {nvcv::TYPE_S32, 1, StrictInpaintRoute::NCHW, 1, 2.0}, + {nvcv::TYPE_S32, 3, StrictInpaintRoute::NCHW, 1, 5.0}, + {nvcv::TYPE_S32, 4, StrictInpaintRoute::NCHW, 1, 2.0}, + {nvcv::TYPE_F32, 1, StrictInpaintRoute::NCHW, 1, 5.0}, + {nvcv::TYPE_F32, 3, StrictInpaintRoute::NCHW, 1, 2.0}, + {nvcv::TYPE_F32, 4, StrictInpaintRoute::NCHW, 2, 5.0}, + + {nvcv::TYPE_U8, 1, StrictInpaintRoute::NCHW_FAKE, 1, 2.0}, + {nvcv::TYPE_U8, 2, StrictInpaintRoute::NCHW_FAKE, 1, 5.0}, + {nvcv::TYPE_U8, 3, StrictInpaintRoute::NCHW_FAKE, 1, 2.0}, + {nvcv::TYPE_U8, 4, StrictInpaintRoute::NCHW_FAKE, 1, 5.0}, + {nvcv::TYPE_S32, 1, StrictInpaintRoute::NCHW_FAKE, 1, 5.0}, + {nvcv::TYPE_S32, 2, StrictInpaintRoute::NCHW_FAKE, 2, 2.0}, + {nvcv::TYPE_S32, 3, StrictInpaintRoute::NCHW_FAKE, 1, 5.0}, + {nvcv::TYPE_S32, 4, StrictInpaintRoute::NCHW_FAKE, 1, 2.0}, + {nvcv::TYPE_F32, 1, StrictInpaintRoute::NCHW_FAKE, 1, 2.0}, + {nvcv::TYPE_F32, 2, StrictInpaintRoute::NCHW_FAKE, 1, 5.0}, + {nvcv::TYPE_F32, 3, StrictInpaintRoute::NCHW_FAKE, 1, 2.0}, + {nvcv::TYPE_F32, 4, StrictInpaintRoute::NCHW_FAKE, 1, 5.0}, + + {nvcv::TYPE_U8, 3, StrictInpaintRoute::HWC, 1, 2.0}, + {nvcv::TYPE_F32, 4, StrictInpaintRoute::CHW, 1, 5.0}, +}); + +// clang-format on + +TEST_P(OpInpaintStrictTensor, matches_independent_cpu_gold) +{ + const nvcv::DataType dtype = GetParamValue<0>(); + const int channels = GetParamValue<1>(); + const StrictInpaintRoute route = GetParamValue<2>(); + const int batch = GetParamValue<3>(); + const double radius = GetParamValue<4>(); + + if (dtype == nvcv::TYPE_U8) { - mask[i] = KNOWN; - mask[(erows - 1) * ecols + i] = KNOWN; + RunStrictInpaintTensorCase(dtype, channels, route, batch, radius); } - for (int i = 0; i < erows; i++) + else if (dtype == nvcv::TYPE_S32) { - mask[i * ecols] = KNOWN; - mask[i * ecols + ecols - 1] = KNOWN; + RunStrictInpaintTensorCase(dtype, channels, route, batch, radius); } - // cvDilate(mask, band, el_cross, 1); - for (int i = 1; i < erows - 1; i++) - for (int j = 1; j < ecols - 1; j++) - { - int k = 0; - uint8_t res = 0; - for (int ii = 0; ii < 3; ii++) - for (int jj = 0; jj < 3; jj++) - if (kernel[k++]) - res = max(res, mask[(i - 1 + ii) * ecols + j - 1 + jj]); - band[i * ecols + j] = res; - } - // Heap = cv::makePtr(); - // if (!Heap->Init(band)) - // return; - shared_ptr heap = make_shared(); - if (!heap->Init(band, erows, ecols)) - return; - // cvSub(band, mask, band, NULL); - // SET_BORDER1_C1(band, uchar, 0); - for (int i = 1; i < erows - 1; i++) - for (int j = 1; j < ecols - 1; j++) band[i * ecols + j] -= mask[i * ecols + j]; - // if (!Heap->Add(band)) - // return; - if (!heap->Add(band, erows, ecols)) - return; - // cvSet(f, cvScalar(BAND, 0, 0, 0), band); - // cvSet(f, cvScalar(INSIDE, 0, 0, 0), mask); - // cvSet(t, cvScalar(0, 0, 0, 0), band); - for (int i = 0; i < erows; i++) - for (int j = 0; j < ecols; j++) + else if (dtype == nvcv::TYPE_F32) + { + RunStrictInpaintTensorCase(dtype, channels, route, batch, radius); + } + else + { + FAIL() << "Unexpected strict Inpaint dtype"; + } +} + +} // namespace + +static void ExpectInpaintOutputClose(const std::vector &testVec, const std::vector &goldVec, + int height, int width) +{ + int count = 0; + float diffsum = 0.0f; + for (size_t idx = 0; idx < testVec.size(); idx++) + { + int diff = AbsDiff(testVec[idx], goldVec[idx]); + if (diff > 1) { - if (band[i * ecols + j]) - { - f[i * ecols + j] = BAND; - t[i * ecols + j] = 0; - } - if (mask[i * ecols + j]) - f[i * ecols + j] = INSIDE; + count++; } + diffsum += static_cast(diff); + } - InpaintFMM(f, t, dst, range, heap, erows, ecols); + float pixels = PixelCount(height, width); + EXPECT_LE(static_cast(count) / pixels, 5e-2f); + + diffsum /= 255.0f; + diffsum /= pixels; + EXPECT_LE(diffsum, 5e-3f); } // clang-format off @@ -558,18 +1304,28 @@ TEST_P(OpInpaint, tensor_correct_output) for (int i = 0; i < batch; i++) { - srcVec[i].resize(height * rowStride / sizeof(uint8_t)); - fill(srcVec[i].begin(), srcVec[i].end(), 255); + srcVec[i].assign(height * rowStride / sizeof(uint8_t), 255); int h = height / 2; - int w1 = width * 0.2, w2 = width * 0.8; + int w1 = ScaleDimension(width, 0.2); + int w2 = ScaleDimension(width, 0.8); for (int hi = h - 10; hi < h + 10; hi++) - for (int wi = w1; wi <= w2; wi++) srcVec[i][hi * width + wi] = 0; + { + for (int wi = w1; wi <= w2; wi++) + { + srcVec[i][hi * width + wi] = 0; + } + } ASSERT_EQ(cudaSuccess, cudaMemcpy2D(inAccess->sampleData(i), inAccess->rowStride(), srcVec[i].data(), rowStride, rowStride, height, cudaMemcpyHostToDevice)); maskVec[i].assign(height * rowStride / sizeof(uint8_t), 0); for (int hi = h - 10; hi < h + 10; hi++) - for (int wi = w1; wi <= w2; wi++) maskVec[i][hi * width + wi] = 1; + { + for (int wi = w1; wi <= w2; wi++) + { + maskVec[i][hi * width + wi] = 1; + } + } ASSERT_EQ(cudaSuccess, cudaMemcpy2D(maskAccess->sampleData(i), maskAccess->rowStride(), maskVec[i].data(), rowStride, rowStride, height, cudaMemcpyHostToDevice)); } @@ -594,24 +1350,7 @@ TEST_P(OpInpaint, tensor_correct_output) std::vector goldVec(height * rowStride / sizeof(uint8_t)); Inpaint(srcVec[i], goldVec, maskVec[i], inpaintRadius, height, width); - //ratio = count(abs(diff) > 1) / size - //mean(abs(diff/255)) - int count = 0; - float diffsum = 0.f; - for (int x = 0; x < (int)testVec.size(); x++) - { - if (abs(testVec[x] - goldVec[x]) > 1) - { - count++; - } - diffsum += abs(testVec[x] - goldVec[x]); - } - float ratio = (float)count / (height * width); - EXPECT_LE(ratio, 5e-2); - - diffsum /= 255; - diffsum /= (height * width); - EXPECT_LE(diffsum, 5e-3); + ExpectInpaintOutputClose(testVec, goldVec, height, width); } EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); } @@ -629,11 +1368,13 @@ TEST_P(OpInpaint, varshape_correct_shape) nvcv::ImageFormat fmt = nvcv::FMT_U8; // Create input and output - std::default_random_engine randEng; - std::uniform_int_distribution rndWidth(width * 0.8, width * 1.1); - std::uniform_int_distribution rndHeight(height * 0.8, height * 1.1); + std::default_random_engine randEng; + std::uniform_int_distribution rndWidth(ScaleDimension(width, 0.8), ScaleDimension(width, 1.1)); + std::uniform_int_distribution rndHeight(ScaleDimension(height, 0.8), ScaleDimension(height, 1.1)); - std::vector imgSrc, imgDst, imgMask; + std::vector imgSrc; + std::vector imgDst; + std::vector imgMask; for (int i = 0; i < batch; ++i) { int rw = rndWidth(randEng); @@ -653,7 +1394,8 @@ TEST_P(OpInpaint, varshape_correct_shape) batchDst.pushBack(imgDst.begin(), imgDst.end()); //Generate input - std::vector> srcVec(batch), maskVec(batch); + std::vector> srcVec(batch); + std::vector> maskVec(batch); for (int i = 0; i < batch; i++) { @@ -667,12 +1409,17 @@ TEST_P(OpInpaint, varshape_correct_shape) int srcRowStride = srcWidth * fmt.planePixelStrideBytes(0); - srcVec[i].resize(srcHeight * srcRowStride / sizeof(uint8_t)); - fill(srcVec[i].begin(), srcVec[i].end(), 255); + srcVec[i].assign(srcHeight * srcRowStride / sizeof(uint8_t), 255); int h = srcHeight / 2; - int w1 = srcWidth * 0.2, w2 = srcWidth * 0.8; + int w1 = ScaleDimension(srcWidth, 0.2); + int w2 = ScaleDimension(srcWidth, 0.8); for (int hi = h - 10; hi < h + 10; hi++) - for (int wi = w1; wi <= w2; wi++) srcVec[i][hi * srcWidth + wi] = 0; + { + for (int wi = w1; wi <= w2; wi++) + { + srcVec[i][hi * srcWidth + wi] = 0; + } + } // Copy input data to the GPU ASSERT_EQ(cudaSuccess, cudaMemcpy2D(srcData->plane(0).basePtr, srcData->plane(0).rowStride, srcVec[i].data(), @@ -680,7 +1427,12 @@ TEST_P(OpInpaint, varshape_correct_shape) maskVec[i].assign(srcHeight * srcRowStride / sizeof(uint8_t), 0); for (int hi = h - 10; hi < h + 10; hi++) - for (int wi = w1; wi <= w2; wi++) maskVec[i][hi * srcWidth + wi] = 1; + { + for (int wi = w1; wi <= w2; wi++) + { + maskVec[i][hi * srcWidth + wi] = 1; + } + } ASSERT_EQ(cudaSuccess, cudaMemcpy2D(maskData->plane(0).basePtr, maskData->plane(0).rowStride, maskVec[i].data(), srcRowStride, srcRowStride, srcHeight, cudaMemcpyHostToDevice)); @@ -688,7 +1440,7 @@ TEST_P(OpInpaint, varshape_correct_shape) // Call operator int maxBatch = 4; - nvcv::Size2D maxsize{(int)(480 * 1.1), (int)(480 * 1.1)}; + nvcv::Size2D maxsize{ScaleDimension(480, 1.1), ScaleDimension(480, 1.1)}; cvcuda::Inpaint InpaintOp(maxBatch, maxsize); EXPECT_NO_THROW(InpaintOp(stream, batchSrc, batchMask, batchDst, inpaintRadius)); @@ -717,24 +1469,7 @@ TEST_P(OpInpaint, varshape_correct_shape) std::vector goldVec(dstHeight * dstRowStride / sizeof(uint8_t)); Inpaint(srcVec[i], goldVec, maskVec[i], inpaintRadius, dstHeight, dstWidth); - //ratio = count(abs(diff) > 1) / size - //mean(abs(diff/255)) - int count = 0; - float diffsum = 0.f; - for (int x = 0; x < (int)testVec.size(); x++) - { - if (abs(testVec[x] - goldVec[x]) > 1) - { - count++; - } - diffsum += abs(testVec[x] - goldVec[x]); - } - float ratio = (float)count / (dstHeight * dstWidth); - EXPECT_LE(ratio, 5e-2); - - diffsum /= 255; - diffsum /= (dstHeight * dstWidth); - EXPECT_LE(diffsum, 5e-3); + ExpectInpaintOutputClose(testVec, goldVec, dstHeight, dstWidth); } EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); @@ -784,7 +1519,7 @@ TEST(OpInpaint, test_grad_corner_condition) EXPECT_EQ(cudaSuccess, cudaStreamCreate(&stream)); // run tensor op - for (auto &maskVec : maskVecCases) + for (auto &maskVec : maskVecCases) // NOSONAR { nvcv::Tensor imgIn = nvcv::util::CreateTensor(batch, width, height, nvcv::FMT_U8); nvcv::Tensor imgMask = nvcv::util::CreateTensor(batch, width, height, nvcv::FMT_U8); @@ -822,32 +1557,17 @@ TEST(OpInpaint, test_grad_corner_condition) std::vector goldVec(height * rowStride / sizeof(uint8_t)); Inpaint(srcVec, goldVec, maskVec, inpaintRadius, height, width); - //ratio = count(abs(diff) > 1) / size - //mean(abs(diff/255)) - int count = 0; - float diffsum = 0.f; - for (int x = 0; x < (int)testVec.size(); x++) - { - if (abs(testVec[x] - goldVec[x]) > 1) - { - count++; - } - diffsum += abs(testVec[x] - goldVec[x]); - } - float ratio = (float)count / (height * width); - EXPECT_LE(ratio, 5e-2); - - diffsum /= 255; - diffsum /= (height * width); - EXPECT_LE(diffsum, 5e-3); + ExpectInpaintOutputClose(testVec, goldVec, height, width); } // run varshape op - for (auto &maskVec : maskVecCases) + for (auto &maskVec : maskVecCases) // NOSONAR { auto fmt = nvcv::FMT_U8; - std::vector imgSrc, imgDst, imgMask; + std::vector imgSrc; + std::vector imgDst; + std::vector imgMask; imgSrc.emplace_back(nvcv::Size2D{width, height}, fmt); imgMask.emplace_back(nvcv::Size2D{width, height}, fmt); imgDst.emplace_back(nvcv::Size2D{width, height}, fmt); @@ -877,7 +1597,7 @@ TEST(OpInpaint, test_grad_corner_condition) srcRowStride, srcRowStride, srcHeight, cudaMemcpyHostToDevice)); int maxBatch = 4; - nvcv::Size2D maxsize{(int)(480 * 1.1), (int)(480 * 1.1)}; + nvcv::Size2D maxsize{ScaleDimension(480, 1.1), ScaleDimension(480, 1.1)}; cvcuda::Inpaint InpaintOp(maxBatch, maxsize); EXPECT_NO_THROW(InpaintOp(stream, batchSrc, batchMask, batchDst, inpaintRadius)); @@ -902,29 +1622,311 @@ TEST(OpInpaint, test_grad_corner_condition) std::vector goldVec(dstHeight * dstRowStride / sizeof(uint8_t)); Inpaint(srcVec, goldVec, maskVec, inpaintRadius, dstHeight, dstWidth); - //ratio = count(abs(diff) > 1) / size - //mean(abs(diff/255)) - int count = 0; - float diffsum = 0.f; - for (int x = 0; x < (int)testVec.size(); x++) + ExpectInpaintOutputClose(testVec, goldVec, dstHeight, dstWidth); + } + + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpInpaint, tensor_uses_large_initial_fmm_distance) +{ + cudaStream_t stream; + EXPECT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + constexpr int batch = 1; + constexpr int height = 7; + constexpr int width = 7; + constexpr double inpaintRadius = 3.0; + + const std::array srcVec{ + 24, 65, 60, 53, 84, 117, 133, 56, 67, 95, 104, 136, 129, 165, 70, 97, 104, + 161, 136, 186, 180, 127, 155, 165, 153, 180, 184, 241, 128, 184, 179, 205, 227, 215, + 248, 182, 192, 228, 246, 236, 19, 31, 214, 236, 236, 240, 33, 15, 37, + }; + const std::array maskVec{ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, + 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + }; + + nvcv::Tensor imgIn = nvcv::util::CreateTensor(batch, width, height, nvcv::FMT_U8); + nvcv::Tensor imgMask = nvcv::util::CreateTensor(batch, width, height, nvcv::FMT_U8); + nvcv::Tensor imgOut = nvcv::util::CreateTensor(batch, width, height, nvcv::FMT_U8); + + auto inData = imgIn.exportData(); + auto maskData = imgMask.exportData(); + auto outData = imgOut.exportData(); + + auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*inData); + auto maskAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*maskData); + auto outAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*outData); + + int rowStride = width * nvcv::FMT_U8.planePixelStrideBytes(0); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(inAccess->sampleData(0), inAccess->rowStride(), srcVec.data(), rowStride, + rowStride, height, cudaMemcpyHostToDevice)); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(maskAccess->sampleData(0), maskAccess->rowStride(), maskVec.data(), rowStride, + rowStride, height, cudaMemcpyHostToDevice)); + + cvcuda::Inpaint op(batch, nvcv::Size2D{width, height}); + EXPECT_NO_THROW(op(stream, imgIn, imgMask, imgOut, inpaintRadius)); + EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + std::vector testVec(height * rowStride / sizeof(uint8_t)); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(testVec.data(), rowStride, outAccess->sampleData(0), outAccess->rowStride(), + rowStride, height, cudaMemcpyDeviceToHost)); + + // These pixels depend on FMM distances that keep the initial t value until a later expansion step. + EXPECT_EQ(195, testVec[3 * width + 3]); + EXPECT_EQ(201, testVec[4 * width + 3]); + EXPECT_EQ(209, testVec[4 * width + 4]); + + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +static void FillInpaintStripe(std::vector &src, std::vector &mask, int width, int height, + int channels, int elemSize) +{ + const int pixelStride = channels * elemSize; + + src.assign(static_cast(height) * width * pixelStride, 255); + mask.assign(static_cast(height) * width, 0); + + const int h = height / 2; + const int w1 = ScaleDimension(width, 0.2); + const int w2 = ScaleDimension(width, 0.8); + for (int wi = w1; wi <= w2; wi++) + { + auto pixel = src.begin() + static_cast((static_cast(h) * width + wi) * pixelStride); + std::fill(pixel, pixel + pixelStride, 0); + mask[static_cast(h) * width + wi] = 1; + } +} + +static void RunTensorInpaintPlanarParity(nvcv::ImageFormat planarFmt, nvcv::ImageFormat interleavedFmt, int width, + int height, int numImages, double inpaintRadius) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + const int channels = planarFmt.numChannels(); + const int elemSize = planarFmt.planePixelStrideBytes(0); + const int maskElemSize = nvcv::FMT_U8.planePixelStrideBytes(0); + const int rowStride = width * interleavedFmt.planePixelStrideBytes(0); + const int maskRowStride = width * maskElemSize; + + nvcv::Tensor srcI = nvcv::util::CreateTensor(numImages, width, height, interleavedFmt); + nvcv::Tensor dstI = nvcv::util::CreateTensor(numImages, width, height, interleavedFmt); + nvcv::Tensor srcP = nvcv::util::CreateTensor(numImages, width, height, planarFmt); + nvcv::Tensor dstP = nvcv::util::CreateTensor(numImages, width, height, planarFmt); + nvcv::Tensor maskI = nvcv::util::CreateTensor(numImages, width, height, nvcv::FMT_U8); + nvcv::Tensor maskP( { - if (abs(testVec[x] - goldVec[x]) > 1) - { - count++; - } - diffsum += abs(testVec[x] - goldVec[x]); + {numImages, 1, height, width}, + "NCHW" + }, + nvcv::TYPE_U8); + + auto srcIData = srcI.exportData(); + auto dstIData = dstI.exportData(); + auto srcPData = srcP.exportData(); + auto dstPData = dstP.exportData(); + auto maskIData = maskI.exportData(); + auto maskPData = maskP.exportData(); + ASSERT_TRUE(srcIData && dstIData && srcPData && dstPData && maskIData && maskPData); + + auto srcIAcc = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcIData); + auto dstIAcc = nvcv::TensorDataAccessStridedImagePlanar::Create(*dstIData); + auto srcPAcc = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcPData); + auto dstPAcc = nvcv::TensorDataAccessStridedImagePlanar::Create(*dstPData); + auto maskIAcc = nvcv::TensorDataAccessStridedImagePlanar::Create(*maskIData); + auto maskPAcc = nvcv::TensorDataAccessStridedImagePlanar::Create(*maskPData); + ASSERT_TRUE(srcIAcc && dstIAcc && srcPAcc && dstPAcc && maskIAcc && maskPAcc); + + for (int i = 0; i < numImages; i++) + { + std::vector src; + std::vector mask; + FillInpaintStripe(src, mask, width, height, channels, elemSize); + + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(srcIAcc->sampleData(i), srcIAcc->rowStride(), src.data(), rowStride, + rowStride, height, cudaMemcpyHostToDevice)); + nvcv::test::planar::UploadPlanarSample( + *srcPAcc, i, nvcv::test::planar::DeinterleaveToPlanes(src, width, height, channels, elemSize), width, + height, channels, elemSize); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(maskIAcc->sampleData(i), maskIAcc->rowStride(), mask.data(), maskRowStride, + maskRowStride, height, cudaMemcpyHostToDevice)); + nvcv::test::planar::UploadPlanarSample(*maskPAcc, i, mask, width, height, 1, maskElemSize); + } + + cvcuda::Inpaint op(numImages, nvcv::Size2D{width, height}); + EXPECT_NO_THROW(op(stream, srcI, maskI, dstI, inpaintRadius)); + EXPECT_NO_THROW(op(stream, srcP, maskP, dstP, inpaintRadius)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + for (int i = 0; i < numImages; i++) + { + SCOPED_TRACE(i); + + auto gpuInter = nvcv::test::planar::DownloadInterleavedSample(*dstIAcc, i, width, height, rowStride); + auto planesOut = nvcv::test::planar::DownloadPlanarSample(*dstPAcc, i, width, height, channels, elemSize); + auto planarInter = nvcv::test::planar::InterleaveFromPlanes(planesOut, width, height, channels, elemSize); + + if (gpuInter != planarInter) + { + const auto [interIt, planarIt] = std::mismatch(gpuInter.begin(), gpuInter.end(), planarInter.begin()); + ADD_FAILURE() << "sample=" << i << " offset=" << std::distance(gpuInter.begin(), interIt) + << " interleaved=" << static_cast(*interIt) << " planar=" << static_cast(*planarIt); + } + } + + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +static void RunVarShapeInpaintPlanarParity(nvcv::ImageFormat planarFmt, nvcv::ImageFormat interleavedFmt, int width, + int height, int numImages, double inpaintRadius) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + const int channels = planarFmt.numChannels(); + const int elemSize = planarFmt.planePixelStrideBytes(0); + const int maskElemSize = nvcv::FMT_U8.planePixelStrideBytes(0); + + std::vector srcI; + std::vector dstI; + std::vector maskI; + std::vector srcP; + std::vector dstP; + std::vector maskP; + for (int i = 0; i < numImages; i++) + { + nvcv::Size2D size{width + i * 3, height + i * 2}; + srcI.emplace_back(size, interleavedFmt); + dstI.emplace_back(size, interleavedFmt); + maskI.emplace_back(size, nvcv::FMT_U8); + srcP.emplace_back(size, planarFmt); + dstP.emplace_back(size, planarFmt); + maskP.emplace_back(size, nvcv::FMT_U8); + } + + nvcv::ImageBatchVarShape batchSrcI(numImages); + nvcv::ImageBatchVarShape batchDstI(numImages); + nvcv::ImageBatchVarShape batchMaskI(numImages); + nvcv::ImageBatchVarShape batchSrcP(numImages); + nvcv::ImageBatchVarShape batchDstP(numImages); + nvcv::ImageBatchVarShape batchMaskP(numImages); + batchSrcI.pushBack(srcI.begin(), srcI.end()); + batchDstI.pushBack(dstI.begin(), dstI.end()); + batchMaskI.pushBack(maskI.begin(), maskI.end()); + batchSrcP.pushBack(srcP.begin(), srcP.end()); + batchDstP.pushBack(dstP.begin(), dstP.end()); + batchMaskP.pushBack(maskP.begin(), maskP.end()); + + for (int i = 0; i < numImages; i++) + { + const int sampleW = srcI[i].size().w; + const int sampleH = srcI[i].size().h; + const int rowStride = sampleW * interleavedFmt.planePixelStrideBytes(0); + const int widthBytes = sampleW * elemSize; + const int maskRowStride = sampleW * maskElemSize; + + std::vector src; + std::vector mask; + FillInpaintStripe(src, mask, sampleW, sampleH, channels, elemSize); + + auto srcIData = srcI[i].exportData(); + auto srcPData = srcP[i].exportData(); + auto maskIData = maskI[i].exportData(); + auto maskPData = maskP[i].exportData(); + ASSERT_TRUE(srcIData && srcPData && maskIData && maskPData); + ASSERT_EQ(srcPData->numPlanes(), channels); + + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(srcIData->plane(0).basePtr, srcIData->plane(0).rowStride, src.data(), + rowStride, rowStride, sampleH, cudaMemcpyHostToDevice)); + auto planes = nvcv::test::planar::DeinterleaveToPlanes(src, sampleW, sampleH, channels, elemSize); + const int planeBytes = sampleW * sampleH * elemSize; + for (int c = 0; c < channels; c++) + { + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(srcPData->plane(c).basePtr, srcPData->plane(c).rowStride, + planes.data() + c * planeBytes, widthBytes, widthBytes, sampleH, + cudaMemcpyHostToDevice)); + } + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(maskIData->plane(0).basePtr, maskIData->plane(0).rowStride, mask.data(), + maskRowStride, maskRowStride, sampleH, cudaMemcpyHostToDevice)); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(maskPData->plane(0).basePtr, maskPData->plane(0).rowStride, mask.data(), + maskRowStride, maskRowStride, sampleH, cudaMemcpyHostToDevice)); + } + + cvcuda::Inpaint op(numImages, nvcv::Size2D{width + (numImages - 1) * 3, height + (numImages - 1) * 2}); + EXPECT_NO_THROW(op(stream, batchSrcI, batchMaskI, batchDstI, inpaintRadius)); + EXPECT_NO_THROW(op(stream, batchSrcP, batchMaskP, batchDstP, inpaintRadius)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + for (int i = 0; i < numImages; i++) + { + SCOPED_TRACE(i); + + const int sampleW = dstI[i].size().w; + const int sampleH = dstI[i].size().h; + const int rowStride = sampleW * interleavedFmt.planePixelStrideBytes(0); + const int widthBytes = sampleW * elemSize; + + std::vector gpuInter(static_cast(sampleH) * rowStride); + auto dstIData = dstI[i].exportData(); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(gpuInter.data(), rowStride, dstIData->plane(0).basePtr, + dstIData->plane(0).rowStride, rowStride, sampleH, cudaMemcpyDeviceToHost)); + + std::vector planesOut(static_cast(sampleW) * sampleH * channels * elemSize); + auto dstPData = dstP[i].exportData(); + const int planeBytes = sampleW * sampleH * elemSize; + for (int c = 0; c < channels; c++) + { + ASSERT_EQ(cudaSuccess, + cudaMemcpy2D(planesOut.data() + c * planeBytes, widthBytes, dstPData->plane(c).basePtr, + dstPData->plane(c).rowStride, widthBytes, sampleH, cudaMemcpyDeviceToHost)); } - float ratio = (float)count / (dstHeight * dstWidth); - EXPECT_LE(ratio, 5e-2); + auto planarInter = nvcv::test::planar::InterleaveFromPlanes(planesOut, sampleW, sampleH, channels, elemSize); - diffsum /= 255; - diffsum /= (dstHeight * dstWidth); - EXPECT_LE(diffsum, 5e-3); + if (gpuInter != planarInter) + { + const auto [interIt, planarIt] = std::mismatch(gpuInter.begin(), gpuInter.end(), planarInter.begin()); + ADD_FAILURE() << "sample=" << i << " offset=" << std::distance(gpuInter.begin(), interIt) + << " interleaved=" << static_cast(*interIt) << " planar=" << static_cast(*planarIt); + } } EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); } +// clang-format off +NVCV_TEST_SUITE_P(OpInpaintPlanar, + nvcv::test::ValueList +{ + // width, height, batch, radius, planarFmt, interleavedFmt + { 64, 48, 2, 5.0, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + { 80, 60, 1, 3.0, nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8}, +}); + +NVCV_TEST_SUITE_P(OpInpaintPlanarVarShape, + nvcv::test::ValueList +{ + // width, height, batch, radius, planarFmt, interleavedFmt + { 64, 48, 2, 5.0, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, +}); + +// clang-format on + +TEST_P(OpInpaintPlanar, tensor_matches_interleaved) +{ + RunTensorInpaintPlanarParity(GetParamValue<4>(), GetParamValue<5>(), GetParamValue<0>(), GetParamValue<1>(), + GetParamValue<2>(), GetParamValue<3>()); +} + +TEST_P(OpInpaintPlanarVarShape, varshape_matches_interleaved) +{ + RunVarShapeInpaintPlanarParity(GetParamValue<4>(), GetParamValue<5>(), GetParamValue<0>(), GetParamValue<1>(), + GetParamValue<2>(), GetParamValue<3>()); +} + // clang-format off NVCV_TEST_SUITE_P(OpInpaint_Negative, nvcv::test::ValueList { @@ -947,6 +1949,236 @@ TEST(OpInpaint_Negative, create_will_null_handle) EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, cvcudaInpaintCreate(nullptr, 1, 1, 1)); } +TEST(OpInpaint_Negative, create_rejects_non_positive_limits) +{ + const std::array, 6> invalidLimits{ + std::make_tuple(-1, 4, 4), std::make_tuple(0, 4, 4), std::make_tuple(1, -1, 4), + std::make_tuple(1, 0, 4), std::make_tuple(1, 4, -1), std::make_tuple(1, 4, 0), + }; + + for (auto [maxBatchSize, maxHeight, maxWidth] : invalidLimits) + { + NVCVOperatorHandle handle = nullptr; + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, cvcudaInpaintCreate(&handle, maxBatchSize, maxHeight, maxWidth)) + << "maxBatchSize=" << maxBatchSize << ", maxHeight=" << maxHeight << ", maxWidth=" << maxWidth; + EXPECT_EQ(nullptr, handle); + } +} + +TEST(OpInpaint_Negative, tensor_batch_exceeds_maxBatch) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + constexpr int maxBatch = 1; + constexpr int batch = 2; + constexpr int width = 4; + constexpr int height = 4; + constexpr double inpaintRadius = 1.0; + + nvcv::Tensor imgIn = nvcv::util::CreateTensor(batch, width, height, nvcv::FMT_U8); + nvcv::Tensor imgMask = nvcv::util::CreateTensor(batch, width, height, nvcv::FMT_U8); + nvcv::Tensor imgOut = nvcv::util::CreateTensor(batch, width, height, nvcv::FMT_U8); + + cvcuda::Inpaint op(maxBatch, nvcv::Size2D{width, height}); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&op, &stream, &imgIn, &imgMask, &imgOut, &inpaintRadius] + { op(stream, imgIn, imgMask, imgOut, inpaintRadius); })); + + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpInpaint_Negative, tensor_shape_exceeds_maxShape) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + constexpr int maxBatch = 1; + constexpr int maxWidth = 4; + constexpr int maxHeight = 4; + constexpr int width = maxWidth + 1; + constexpr int height = maxHeight + 1; + constexpr double inpaintRadius = 1.0; + + nvcv::Tensor imgIn = nvcv::util::CreateTensor(maxBatch, width, height, nvcv::FMT_U8); + nvcv::Tensor imgMask = nvcv::util::CreateTensor(maxBatch, width, height, nvcv::FMT_U8); + nvcv::Tensor imgOut = nvcv::util::CreateTensor(maxBatch, width, height, nvcv::FMT_U8); + + cvcuda::Inpaint op(maxBatch, nvcv::Size2D{maxWidth, maxHeight}); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&] { op(stream, imgIn, imgMask, imgOut, inpaintRadius); })); + + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpInpaint_Negative, varshape_batch_exceeds_maxBatch) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + constexpr int maxBatch = 1; + constexpr int batch = 2; + constexpr int width = 4; + constexpr int height = 4; + constexpr double inpaintRadius = 1.0; + + std::vector imgSrc; + std::vector imgMask; + std::vector imgDst; + for (int i = 0; i < batch; ++i) + { + imgSrc.emplace_back(nvcv::Size2D{width, height}, nvcv::FMT_U8); + imgMask.emplace_back(nvcv::Size2D{width, height}, nvcv::FMT_U8); + imgDst.emplace_back(nvcv::Size2D{width, height}, nvcv::FMT_U8); + } + + nvcv::ImageBatchVarShape batchSrc(batch); + batchSrc.pushBack(imgSrc.begin(), imgSrc.end()); + + nvcv::ImageBatchVarShape batchMask(batch); + batchMask.pushBack(imgMask.begin(), imgMask.end()); + + nvcv::ImageBatchVarShape batchDst(batch); + batchDst.pushBack(imgDst.begin(), imgDst.end()); + + cvcuda::Inpaint op(maxBatch, nvcv::Size2D{width, height}); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&op, &stream, &batchSrc, &batchMask, &batchDst, &inpaintRadius] + { op(stream, batchSrc, batchMask, batchDst, inpaintRadius); })); + + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpInpaint_Negative, varshape_shape_exceeds_maxShape) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + constexpr int maxBatch = 1; + constexpr int maxWidth = 4; + constexpr int maxHeight = 4; + constexpr int width = maxWidth + 1; + constexpr int height = maxHeight + 1; + constexpr double inpaintRadius = 1.0; + + std::vector imgSrc; + std::vector imgMask; + std::vector imgDst; + imgSrc.emplace_back(nvcv::Size2D{width, height}, nvcv::FMT_U8); + imgMask.emplace_back(nvcv::Size2D{width, height}, nvcv::FMT_U8); + imgDst.emplace_back(nvcv::Size2D{width, height}, nvcv::FMT_U8); + + nvcv::ImageBatchVarShape batchSrc(maxBatch); + batchSrc.pushBack(imgSrc.begin(), imgSrc.end()); + + nvcv::ImageBatchVarShape batchMask(maxBatch); + batchMask.pushBack(imgMask.begin(), imgMask.end()); + + nvcv::ImageBatchVarShape batchDst(maxBatch); + batchDst.pushBack(imgDst.begin(), imgDst.end()); + + cvcuda::Inpaint op(maxBatch, nvcv::Size2D{maxWidth, maxHeight}); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&] { op(stream, batchSrc, batchMask, batchDst, inpaintRadius); })); + + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpInpaint_Negative, varshape_batch_count_mismatch) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + constexpr int maxBatch = 3; + constexpr int inputBatch = 2; + constexpr int width = 4; + constexpr int height = 4; + constexpr double inpaintRadius = 1.0; + + std::vector imgSrc; + std::vector imgMask; + std::vector imgDst; + for (int i = 0; i < inputBatch; ++i) + { + imgSrc.emplace_back(nvcv::Size2D{width, height}, nvcv::FMT_RGB8); + imgMask.emplace_back(nvcv::Size2D{width, height}, nvcv::FMT_U8); + } + for (int i = 0; i < maxBatch; ++i) + { + imgDst.emplace_back(nvcv::Size2D{width, height}, nvcv::FMT_RGB8); + } + + nvcv::ImageBatchVarShape batchSrc(maxBatch); + batchSrc.pushBack(imgSrc.begin(), imgSrc.end()); + + nvcv::ImageBatchVarShape batchMask(maxBatch); + batchMask.pushBack(imgMask.begin(), imgMask.end()); + + nvcv::ImageBatchVarShape batchDst(maxBatch); + batchDst.pushBack(imgDst.begin(), imgDst.end()); + + cvcuda::Inpaint op(maxBatch, nvcv::Size2D{width, height}); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&] { op(stream, batchSrc, batchMask, batchDst, inpaintRadius); })); + + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpInpaint_Negative, tensor_planar_2channel_rejected) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + nvcv::Tensor imgIn( + { + {1, 2, 32, 32}, + "NCHW" + }, + nvcv::TYPE_U8); + nvcv::Tensor imgMask( + { + {1, 1, 32, 32}, + "NCHW" + }, + nvcv::TYPE_U8); + nvcv::Tensor imgOut( + { + {1, 2, 32, 32}, + "NCHW" + }, + nvcv::TYPE_U8); + + cvcuda::Inpaint op(1, nvcv::Size2D{32, 32}); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&] { op(stream, imgIn, imgMask, imgOut, 1.0); })); + + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpInpaint_Negative, varshape_planar_2channel_rejected) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + const nvcv::ImageFormat twoChannelPlanar = kFmt2U8Planar; + std::vector imgSrc; + std::vector imgMask; + std::vector imgDst; + imgSrc.emplace_back(nvcv::Size2D{32, 32}, twoChannelPlanar); + imgMask.emplace_back(nvcv::Size2D{32, 32}, nvcv::FMT_U8); + imgDst.emplace_back(nvcv::Size2D{32, 32}, twoChannelPlanar); + + nvcv::ImageBatchVarShape batchSrc(1); + batchSrc.pushBack(imgSrc.begin(), imgSrc.end()); + nvcv::ImageBatchVarShape batchMask(1); + batchMask.pushBack(imgMask.begin(), imgMask.end()); + nvcv::ImageBatchVarShape batchDst(1); + batchDst.pushBack(imgDst.begin(), imgDst.end()); + + cvcuda::Inpaint op(1, nvcv::Size2D{32, 32}); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&] { op(stream, batchSrc, batchMask, batchDst, 1.0); })); + + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + TEST_P(OpInpaint_Negative, invalid_parameters) { const int maxBatch = 4; @@ -972,7 +2204,8 @@ TEST_P(OpInpaint_Negative, invalid_parameters) cvcuda::Inpaint InpaintOp(maxBatch, maxsize); EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, - nvcv::ProtectCall([&] { InpaintOp(stream, imgIn, imgMask, imgOut, inpaintRadius); })); + nvcv::ProtectCall([&InpaintOp, &stream, &imgIn, &imgMask, &imgOut, &inpaintRadius] + { InpaintOp(stream, imgIn, imgMask, imgOut, inpaintRadius); })); EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); @@ -997,7 +2230,9 @@ TEST_P(OpInpaint_Negative, invalid_parameters_varshape) nvcv::ImageFormat fmtDst = GetParamValue<1>(); nvcv::ImageFormat fmtMask = GetParamValue<2>(); - std::vector imgSrc, imgDst, imgMask; + std::vector imgSrc; + std::vector imgDst; + std::vector imgMask; for (int i = 0; i < batch; i++) { imgSrc.emplace_back(nvcv::Size2D{width, height}, fmtSrc); @@ -1016,7 +2251,8 @@ TEST_P(OpInpaint_Negative, invalid_parameters_varshape) cvcuda::Inpaint InpaintOp(maxBatch, maxsize); EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, - nvcv::ProtectCall([&] { InpaintOp(stream, batchSrc, batchMask, batchDst, inpaintRadius); })); + nvcv::ProtectCall([&InpaintOp, &stream, &batchSrc, &batchMask, &batchDst, &inpaintRadius] + { InpaintOp(stream, batchSrc, batchMask, batchDst, inpaintRadius); })); EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); diff --git a/tests/cvcuda/system/TestOpInvert.cpp b/tests/cvcuda/system/TestOpInvert.cpp new file mode 100644 index 000000000..c2e101a13 --- /dev/null +++ b/tests/cvcuda/system/TestOpInvert.cpp @@ -0,0 +1,293 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "ElementwiseOpHarness.hpp" +#include "PlanarParityUtils.hpp" + +#include +#include + +#include + +namespace test = nvcv::test; +namespace ew = nvcv::test::elementwise; + +namespace { + +// Independent CPU gold reference: out = bound - in per element, where bound is the dtype maximum +// (255 / 65535) for unsigned integers and 1.0 for float. This mirrors the documented oracle +// (torchvision.transforms.v2.functional.invert / OpenCV cv::bitwise_not) and is intentionally +// computed independently of the kernel so the bit-exact EXPECT_EQ is a real regression check. +// +// goldFor(DT{}) yields the per-element reference for a given element type; one expression covers +// every declared dtype. +const auto goldFor = [](DT) +{ + return [](DT v) + { + return static_cast
(ew::Bound
() - v); + }; +}; + +// Run the operator. A single generic invoker serves Tensor, ImageBatchVarShape, and the parity +// harness (Invert takes no extra parameters). +const auto invoke = [](cudaStream_t s, const auto &in, auto &out) +{ + cvcuda::Invert op; + op(s, in, out); +}; + +} // namespace + +// Tensor correctness: bit-exact vs the CPU gold, over the declared dtype × channel matrix -------- +// clang-format off +NVCV_TEST_SUITE_P(OpInvert, test::ValueList +{ + // width, height, batch, format (dtype / channels) + { 66, 55, 1, nvcv::FMT_U8 }, // u8 / 1ch + { 123, 67, 3, nvcv::FMT_RGB8 }, // u8 / 3ch + { 42, 53, 4, nvcv::FMT_RGBA8 }, // u8 / 4ch + { 80, 40, 2, nvcv::FMT_U16 }, // u16 / 1ch + { 17, 19, 1, nvcv::FMT_F32 }, // f32 / 1ch + { 101, 33, 2, nvcv::FMT_RGBf32 }, // f32 / 3ch + { 64, 48, 3, nvcv::FMT_RGBAf32}, // f32 / 4ch +}); + +// clang-format on +TEST_P(OpInvert, tensor_correct_output) +{ + // Invert has no dtype-dependent parameter, so the invoke factory returns the same invoker for + // every element type. + ew::RunTensorCorrectDispatch(GetParamValue<0>(), GetParamValue<1>(), GetParamValue<2>(), + nvcv::ImageFormat{GetParamValue<3>()}, goldFor, [](auto) { return invoke; }); +} + +// VarShape correctness: bit-exact vs the CPU gold ------------------------------------------------ +TEST(OpInvert, varshape_correct_output) +{ + ew::RunVarShapeCorrect([](uint8_t v) { return static_cast(ew::Bound() - v); }, invoke); +} + +NVCV_TEST_SUITE_P(OpInvertVarShapeTyped, test::ValueList{nvcv::FMT_U16, nvcv::FMT_F32}); + +TEST_P(OpInvertVarShapeTyped, correct_output) +{ + nvcv::ImageFormat fmt = GetParam(); + if (ew::BaseKind(fmt) == 2) + { + ew::RunVarShapeCorrectTyped(fmt, goldFor(float{}), invoke); + } + else + { + ew::RunVarShapeCorrectTyped(fmt, goldFor(uint16_t{}), invoke); + } +} + +// Planar ≡ interleaved parity (fake-planar): native planar output must be byte-for-byte identical +// to the interleaved result on the same data. Uses the shared PlanarParityUtils scaffolding. ----- +// clang-format off +NVCV_TEST_SUITE_P(OpInvertPlanar, + test::ValueList{ + {177, 113, 2, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + { 65, 48, 1, nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8}, + {101, 80, 2, nvcv::FMT_RGBAf32p, nvcv::FMT_RGBAf32}, +}); + +NVCV_TEST_SUITE_P(OpInvertPlanarVarShape, + test::ValueList{ + {177, 113, 2, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + {101, 80, 2, nvcv::FMT_RGBAf32p, nvcv::FMT_RGBAf32}, +}); + +// clang-format on +TEST_P(OpInvertPlanar, tensor_matches_interleaved) +{ + test::planar::RunTensorParity(GetParamValue<3>(), GetParamValue<4>(), GetParamValue<0>(), GetParamValue<1>(), + GetParamValue<0>(), GetParamValue<1>(), GetParamValue<2>(), + [](cudaStream_t s, const nvcv::Tensor &src, const nvcv::Tensor &dst, + nvcv::ImageFormat) { EXPECT_NO_THROW(invoke(s, src, dst)); }); +} + +TEST_P(OpInvertPlanarVarShape, varshape_matches_interleaved) +{ + test::planar::RunVarShapeParity( + GetParamValue<3>(), GetParamValue<4>(), GetParamValue<0>(), GetParamValue<1>(), GetParamValue<0>(), + GetParamValue<1>(), GetParamValue<2>(), + [](cudaStream_t s, const nvcv::ImageBatchVarShape &src, const nvcv::ImageBatchVarShape &dst, nvcv::ImageFormat) + { EXPECT_NO_THROW(invoke(s, src, dst)); }); +} + +// Padded-stride tensors: the dense flat fast path must reject padded rows and fall back to the +// layout kernels, which must stay bit-exact vs the CPU gold and must not touch the padding. ------ +namespace { + +// Check every payload lane equals bound - src (the inverse) and every padding byte is still the +// 0xA5 sentinel, i.e. the fallback kernel wrote the payload correctly and never touched the padding. +template +void VerifyPaddedTensor(const std::vector &srcBytes, const std::vector &dstBytes, int batch, + int rowsPerSample, int64_t sampleStride, int64_t rowStride, int64_t rowBytes) +{ + for (int n = 0; n < batch; ++n) + { + for (int r = 0; r < rowsPerSample; ++r) + { + const size_t rowOff = n * sampleStride + r * rowStride; + for (size_t e = 0; e < rowBytes / sizeof(BT); ++e) + { + // memcpy the lanes out of the byte buffers: rowOff need not satisfy alignof(BT) + // and the payload is raw bytes, so a typed reinterpret_cast read would be UB. + BT in{}; + BT out{}; + std::memcpy(&in, srcBytes.data() + rowOff + e * sizeof(BT), sizeof(BT)); + std::memcpy(&out, dstBytes.data() + rowOff + e * sizeof(BT), sizeof(BT)); + ASSERT_EQ(static_cast(ew::Bound() - in), out) + << "payload mismatch at sample " << n << " row " << r << " elem " << e; + } + for (int64_t b = rowBytes; b < rowStride; ++b) + { + ASSERT_EQ(0xA5, dstBytes[rowOff + b]) << "padding overwritten at sample " << n << " row " << r; + } + } + } +} + +template +void RunPaddedTensor(nvcv::ImageFormat fmt, int width, int height, int batch, int padBytes) +{ + const bool planar = fmt.numPlanes() > 1; + const int channels = fmt.numChannels(); + const int64_t elemBytes = sizeof(BT); + // Packed row (per plane for planar formats) plus deliberate padding. + const int64_t rowBytes = width * elemBytes * (planar ? 1 : channels); + const int64_t rowStride = rowBytes + padBytes; + const int64_t planeStride = rowStride * height; + const int64_t sampleStride = planeStride * (planar ? channels : 1); + const size_t totalBytes = static_cast(sampleStride) * batch; + + NVCVByte *srcPtr = nullptr; + NVCVByte *dstPtr = nullptr; + ASSERT_EQ(cudaSuccess, cudaMalloc(&srcPtr, totalBytes)); + ASSERT_EQ(cudaSuccess, cudaMalloc(&dstPtr, totalBytes)); + + auto wrapTensor = [&](NVCVByte *ptr) + { + nvcv::TensorDataStridedCuda::Buffer buffer{}; + buffer.basePtr = ptr; + if (planar) + { + buffer.strides[0] = sampleStride; + buffer.strides[1] = planeStride; + buffer.strides[2] = rowStride; + buffer.strides[3] = elemBytes; + return nvcv::TensorWrapData(nvcv::TensorDataStridedCuda{ + nvcv::TensorShape{{batch, channels, height, width}, "NCHW"}, + nvcv::DataType{ fmt.planeDataType(0) }, + buffer + }); + } + buffer.strides[0] = sampleStride; + buffer.strides[1] = rowStride; + buffer.strides[2] = channels * elemBytes; + buffer.strides[3] = elemBytes; + return nvcv::TensorWrapData(nvcv::TensorDataStridedCuda{ + nvcv::TensorShape{ {batch, height, width, channels}, "NHWC"}, + nvcv::DataType{fmt.planeDataType(0).channelType(0) }, + buffer + }); + }; + + nvcv::Tensor src = wrapTensor(srcPtr); + nvcv::Tensor dst = wrapTensor(dstPtr); + + // Fill the whole src allocation (payload + padding) with a deterministic byte pattern and the + // dst allocation with a sentinel, so untouched padding can be verified after the run. + std::vector srcBytes(totalBytes); + for (size_t i = 0; i < totalBytes; ++i) + { + srcBytes[i] = static_cast((i * 31 + 7) % 251); + } + if constexpr (std::is_floating_point_v) + { + // Overwrite payload lanes with valid floats (deterministic in [0, 1]). memcpy each value in + // rather than a typed store so the write stays well-defined on the uint8_t backing storage. + for (size_t i = 0; i < totalBytes / sizeof(float); ++i) + { + const float v = static_cast((i * 37 + 11) % 1000) / 1000.0f; + std::memcpy(srcBytes.data() + i * sizeof(float), &v, sizeof(float)); + } + } + const std::vector dstSentinel(totalBytes, 0xA5); + ASSERT_EQ(cudaSuccess, cudaMemcpy(srcPtr, srcBytes.data(), totalBytes, cudaMemcpyHostToDevice)); + ASSERT_EQ(cudaSuccess, cudaMemcpy(dstPtr, dstSentinel.data(), totalBytes, cudaMemcpyHostToDevice)); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + EXPECT_NO_THROW(invoke(stream, src, dst)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + + std::vector dstBytes(totalBytes); + ASSERT_EQ(cudaSuccess, cudaMemcpy(dstBytes.data(), dstPtr, totalBytes, cudaMemcpyDeviceToHost)); + ASSERT_EQ(cudaSuccess, cudaFree(srcPtr)); + ASSERT_EQ(cudaSuccess, cudaFree(dstPtr)); + + const int rowsPerSample = height * (planar ? channels : 1); + VerifyPaddedTensor(srcBytes, dstBytes, batch, rowsPerSample, sampleStride, rowStride, rowBytes); +} + +} // namespace + +TEST(OpInvert, tensor_padded_strides_correct_output) +{ + // Interleaved fallbacks (scalar uchar3/uchar4/float3 kernels). + RunPaddedTensor(nvcv::FMT_RGB8, 41, 13, 2, 4); + RunPaddedTensor(nvcv::FMT_RGBA8, 22, 9, 2, 8); + RunPaddedTensor(nvcv::FMT_RGBf32, 21, 7, 1, 12); + // Single-channel interleaved: 8-byte-aligned pad keeps the ushort4 vector path (with padding), + // odd pad forces the scalar tensor kernel. + RunPaddedTensor(nvcv::FMT_U16, 30, 11, 2, 4); + RunPaddedTensor(nvcv::FMT_U16, 30, 11, 2, 6); + // Planar u8 (41-byte packed rows): pad 3 gives 4-byte-aligned strides and keeps the vectorized + // planar kernel, pad 2 gives odd strides and forces the scalar planar kernel. + RunPaddedTensor(nvcv::FMT_RGB8p, 41, 13, 2, 3); + RunPaddedTensor(nvcv::FMT_RGB8p, 41, 13, 2, 2); + // Planar f32 (84-byte packed rows): pad 12 gives 16-byte-aligned strides and keeps the + // vectorized planar kernel, pad 8 forces its scalar fallback. + RunPaddedTensor(nvcv::FMT_RGBf32p, 21, 7, 2, 12); + RunPaddedTensor(nvcv::FMT_RGBf32p, 21, 7, 2, 8); +} + +// Negative tests: the complement of the support matrix must be rejected ------------------------- +// clang-format off +NVCV_TEST_SUITE_P(OpInvert_Negative, test::ValueList{ + {nvcv::FMT_F16, nvcv::FMT_F16 }, // unsupported dtype (16-bit float) + {nvcv::FMT_S16, nvcv::FMT_S16 }, // unsupported dtype (signed 16-bit) + {nvcv::FMT_RGB8, nvcv::FMT_RGB8p}, // layout mismatch (interleaved in, planar out) + {nvcv::FMT_RGB8, nvcv::FMT_RGBf32}, // input/output data type mismatch +}); + +// clang-format on +TEST_P(OpInvert_Negative, rejects_unsupported) +{ + ew::ExpectRejected(GetParamValue<0>(), GetParamValue<1>(), invoke); +} + +// 2-channel input (outside the 1/3/4 matrix) is rejected. +TEST(OpInvert_Negative, rejects_two_channel) +{ + ew::ExpectRejected(nvcv::FMT_2F32, nvcv::FMT_2F32, invoke, 16, 16); +} diff --git a/tests/cvcuda/system/TestOpJointBilateralFilter.cpp b/tests/cvcuda/system/TestOpJointBilateralFilter.cpp index ba1dd6ee7..8546168cc 100644 --- a/tests/cvcuda/system/TestOpJointBilateralFilter.cpp +++ b/tests/cvcuda/system/TestOpJointBilateralFilter.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,6 +16,7 @@ */ #include "Definitions.hpp" +#include "PlanarParityUtils.hpp" #include #include @@ -34,53 +35,58 @@ namespace gt = ::testing; namespace test = nvcv::test; -static uint32_t saturate_cast(float n) +using TensorDim = nvcv::TensorShape::DimType; + +static uint8_t saturate_cast(float n) { - return static_cast(std::min(255.0f, std::round(n))); + return static_cast(std::min(255.0f, std::round(n))); } -static bool CompareImages(uint8_t *pTest, uint8_t *pGold, size_t columns, size_t rows, size_t rowStride, +static bool CompareImages(const uint8_t *pTest, const uint8_t *pGold, size_t columns, size_t rows, size_t rowStride, size_t channels, float delta) { + const size_t activeRowBytes = columns * channels; for (size_t j = 0; j < rows; j++) { - for (size_t k = 0; k < columns; k++) + const size_t rowOffset = j * rowStride; + for (size_t k = 0; k < activeRowBytes; k++) { - for (size_t c = 0; c < channels; ++c) + const size_t offset = rowOffset + k; + float diff = std::abs(static_cast(pTest[offset]) - static_cast(pGold[offset])); + if (diff > delta) { - size_t offset = j * rowStride + k * channels + c; - float diff = std::abs(static_cast(pTest[offset]) - static_cast(pGold[offset])); - if (diff > delta) - { - std::cout << " o = " << offset << " j = " << j << " k = " << k << " rowS = " << rowStride - << std::endl; - std::cout << " test = " << static_cast(pTest[offset]) - << " gold = " << static_cast(pGold[offset]) << std::endl; - - return false; - } + const size_t column = k / channels; + const size_t channel = k % channels; + std::cout << " o = " << offset << " j = " << j << " k = " << column << " c = " << channel + << " rowS = " << rowStride << std::endl; + std::cout << " test = " << static_cast(pTest[offset]) + << " gold = " << static_cast(pGold[offset]) << std::endl; + + return false; } } } return true; } -static bool CompareTensors(std::vector &vTest, std::vector &vGold, size_t columns, size_t rows, - size_t batch, size_t rowStride, size_t channels, size_t sampleStride, float delta) +static bool CompareTensors(const std::vector &vTest, const std::vector &vGold, size_t columns, + size_t rows, size_t batch, size_t rowStride, size_t channels, size_t sampleStride, + float delta) { for (size_t i = 0; i < batch; i++) { - uint8_t *pTest = vTest.data() + i * sampleStride; - uint8_t *pGold = vGold.data() + i * sampleStride; + const uint8_t *pTest = vTest.data() + i * sampleStride; + const uint8_t *pGold = vGold.data() + i * sampleStride; if (!CompareImages(pTest, pGold, columns, rows, rowStride, channels, delta)) return false; } return true; } -static bool CompareVarShapes(std::vector> &vTest, std::vector> &vGold, - std::vector &vColumns, std::vector &vRows, std::vector &vRowStride, - std::vector &vChannels, float delta) +static bool CompareVarShapes(const std::vector> &vTest, + const std::vector> &vGold, const std::vector &vColumns, + const std::vector &vRows, const std::vector &vRowStride, + const std::vector &vChannels, float delta) { for (size_t i = 0; i < vTest.size(); i++) { @@ -92,61 +98,91 @@ static bool CompareVarShapes(std::vector> &vTest, std::vect return true; } -static void CPUJointBilateralFilter(uint8_t *pIn, uint8_t *pInColor, uint8_t *pOut, int columns, int rows, - int rowStride, int channels, int radius, float colorCoefficient, - float spaceCoefficient) +static float ReadPixel(const uint8_t *pIn, TensorDim x, TensorDim y, int c, TensorDim columns, TensorDim rows, + int rowStride, int channels) +{ + return ((x >= 0) && (x < columns) && (y >= 0) && (y < rows)) + ? static_cast(pIn[y * rowStride + x * channels + c]) + : 0.0f; +} + +static std::vector ReadChannels(const uint8_t *pIn, TensorDim x, TensorDim y, TensorDim columns, TensorDim rows, + int rowStride, int channels) { - float radiusSquared = radius * radius; - for (int j = 0; j < rows; j++) + std::vector values(channels); + + for (int c = 0; c < channels; ++c) { - for (int k = 0; k < columns; k++) + values[c] = ReadPixel(pIn, x, y, c, columns, rows, rowStride, channels); + } + + return values; +} + +static void AccumulateJointBilateralSample(std::vector &numerators, float &denominator, + const std::vector ¢erColors, const uint8_t *pIn, + const uint8_t *pInColor, TensorDim x, TensorDim y, TensorDim columns, + TensorDim rows, int rowStride, int channels, float distanceSquared, + float colorCoefficient, float spaceCoefficient) +{ + std::vector pixels = ReadChannels(pIn, x, y, columns, rows, rowStride, channels); + std::vector pixelColors = ReadChannels(pInColor, x, y, columns, rows, rowStride, channels); + float eColor = 0.0f; + + for (int c = 0; c < channels; ++c) + { + eColor += std::abs(pixelColors[c] - centerColors[c]); + } + + float weight = std::exp(distanceSquared * spaceCoefficient + eColor * eColor * colorCoefficient); + denominator += weight; + + for (int c = 0; c < channels; ++c) + { + numerators[c] += weight * pixels[c]; + } +} + +static void AccumulateJointBilateralWindow(std::vector &numerators, float &denominator, + const std::vector ¢erColors, const uint8_t *pIn, + const uint8_t *pInColor, TensorDim column, TensorDim row, TensorDim columns, + TensorDim rows, int rowStride, int channels, int radius, float radiusSquared, + float colorCoefficient, float spaceCoefficient) +{ + for (TensorDim y = row - radius; y <= row + radius; y++) + { + for (TensorDim x = column - radius; x <= column + radius; x++) { - std::vector numerators(channels, 0.0f); - float denominator = 0; - std::vector centerColors{ - static_cast(pInColor[j * rowStride + k * channels]), - channels > 1 ? static_cast(pInColor[j * rowStride + k * channels + 1]) : 0, - channels > 2 ? static_cast(pInColor[j * rowStride + k * channels + 2]) : 0, - channels > 3 ? static_cast(pInColor[j * rowStride + k * channels + 3]) : 0}; - - for (int y = j - radius; y <= j + radius; y++) + auto distanceSquared = static_cast((column - x) * (column - x) + (row - y) * (row - y)); + + if (distanceSquared > radiusSquared) { - for (int x = k - radius; x <= k + radius; x++) - { - float distanceSquared = (k - x) * (k - x) + (j - y) * (j - y); - if (distanceSquared <= radiusSquared) - { - std::vector pixels; - std::vector pixelColors; - for (auto c = 0; c < channels; ++c) - { - float pixel = ((x >= 0) && (x < columns) && (y >= 0) && (y < rows)) - ? static_cast(pIn[y * rowStride + x * channels + c]) - : 0.0f; - float pixelColor = ((x >= 0) && (x < columns) && (y >= 0) && (y < rows)) - ? static_cast(pInColor[y * rowStride + x * channels + c]) - : 0.0f; - pixels.emplace_back(pixel); - pixelColors.emplace_back(pixelColor); - } - - float e_space = distanceSquared * spaceCoefficient; - float e_color = 0.0f; - for (auto c = 0; c < channels; ++c) - { - e_color += std::abs(pixelColors[c] - centerColors[c]); - } - e_color = e_color * e_color * colorCoefficient; - - float weight = std::exp(e_space + e_color); - denominator += weight; - for (auto c = 0; c < channels; ++c) - { - numerators[c] += weight * pixels[c]; - } - } - } + continue; } + + AccumulateJointBilateralSample(numerators, denominator, centerColors, pIn, pInColor, x, y, columns, rows, + rowStride, channels, distanceSquared, colorCoefficient, spaceCoefficient); + } + } +} + +static void CPUJointBilateralFilter(const uint8_t *pIn, const uint8_t *pInColor, uint8_t *pOut, TensorDim columns, + TensorDim rows, int rowStride, int channels, int radius, float colorCoefficient, + float spaceCoefficient) +{ + auto radiusSquared = static_cast(radius * radius); + for (TensorDim j = 0; j < rows; j++) + { + for (TensorDim k = 0; k < columns; k++) + { + std::vector numerators(channels, 0.0f); + float denominator = 0; + std::vector centerColors = ReadChannels(pInColor, k, j, columns, rows, rowStride, channels); + + AccumulateJointBilateralWindow(numerators, denominator, centerColors, pIn, pInColor, k, j, columns, rows, + rowStride, channels, radius, radiusSquared, colorCoefficient, + spaceCoefficient); + denominator = (denominator != 0) ? denominator : 1.0f; for (auto c = 0; c < channels; ++c) { @@ -157,9 +193,9 @@ static void CPUJointBilateralFilter(uint8_t *pIn, uint8_t *pInColor, uint8_t *pO } static void CPUJointBilateralFilterTensor(std::vector &vIn, std::vector &vInColor, - std::vector &vOut, int columns, int rows, int batch, int rowStride, - int channels, int sampleStride, int diameter, float sigmaColor, - float sigmaSpace) + std::vector &vOut, TensorDim columns, TensorDim rows, + TensorDim batch, int rowStride, int channels, int sampleStride, int diameter, + float sigmaColor, float sigmaSpace) { if (sigmaColor <= 0) { @@ -173,7 +209,7 @@ static void CPUJointBilateralFilterTensor(std::vector &vIn, std::vector int radius; if (diameter <= 0) { - radius = std::roundf(sigmaSpace * 1.5f); + radius = static_cast(std::roundf(sigmaSpace * 1.5f)); } else { @@ -184,13 +220,13 @@ static void CPUJointBilateralFilterTensor(std::vector &vIn, std::vector radius = 1; } - float spaceCoefficient = -1 / (2 * sigmaSpace * sigmaSpace); - float colorCoefficient = -1 / (2 * sigmaColor * sigmaColor); - for (int i = 0; i < batch; i++) + float spaceCoefficient = -1.f / (2.f * sigmaSpace * sigmaSpace); + float colorCoefficient = -1.f / (2.f * sigmaColor * sigmaColor); + for (TensorDim i = 0; i < batch; i++) { - uint8_t *pIn = vIn.data() + i * sampleStride; - uint8_t *pInColor = vInColor.data() + i * sampleStride; - uint8_t *pOut = vOut.data() + i * sampleStride; + const uint8_t *pIn = vIn.data() + i * sampleStride; + const uint8_t *pInColor = vInColor.data() + i * sampleStride; + uint8_t *pOut = vOut.data() + i * sampleStride; CPUJointBilateralFilter(pIn, pInColor, pOut, columns, rows, rowStride, channels, radius, colorCoefficient, spaceCoefficient); } @@ -221,7 +257,7 @@ static void CPUJointBilateralFilterVarShape(std::vector> &v int radius; if (diameter <= 0) { - radius = std::roundf(sigmaSpace * 1.5f); + radius = static_cast(std::roundf(sigmaSpace * 1.5f)); } else { @@ -232,8 +268,8 @@ static void CPUJointBilateralFilterVarShape(std::vector> &v radius = 1; } - float spaceCoefficient = -1 / (2 * sigmaSpace * sigmaSpace); - float colorCoefficient = -1 / (2 * sigmaColor * sigmaColor); + float spaceCoefficient = -1.f / (2.f * sigmaSpace * sigmaSpace); + float colorCoefficient = -1.f / (2.f * sigmaColor * sigmaColor); CPUJointBilateralFilter(vIn[i].data(), vInColor[i].data(), vOut[i].data(), vColumns[i], vRows[i], vRowStride[i], vChannels[i], radius, colorCoefficient, spaceCoefficient); } @@ -280,7 +316,7 @@ TEST_P(OpJointBilateralFilter, JointBilateralFilter_packed) std::vector fmts{nvcv::FMT_U8, nvcv::ImageFormat{NVCV_IMAGE_FORMAT_2U8}, nvcv::FMT_RGB8, nvcv::FMT_RGBA8}; - for (nvcv::ImageFormat fmt : fmts) + for (nvcv::ImageFormat fmt : fmts) // NOSONAR { nvcv::Tensor imgOut = nvcv::util::CreateTensor(numberOfImages, width, height, fmt); nvcv::Tensor imgIn = nvcv::util::CreateTensor(numberOfImages, width, height, fmt); @@ -304,13 +340,13 @@ TEST_P(OpJointBilateralFilter, JointBilateralFilter_packed) auto outAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*outData); ASSERT_TRUE(outAccess); - int inSampleStride = inAccess->numRows() * inAccess->rowStride(); - int inColorSampleStride = inColorAccess->numRows() * inColorAccess->rowStride(); - int outSampleStride = outAccess->numRows() * outAccess->rowStride(); + auto inSampleStride = static_cast(inAccess->numRows() * inAccess->rowStride()); + auto inColorSampleStride = static_cast(inColorAccess->numRows() * inColorAccess->rowStride()); + auto outSampleStride = static_cast(outAccess->numRows() * outAccess->rowStride()); - int inBufSize = inSampleStride * inAccess->numSamples(); - int inColorBufSize = inColorSampleStride * inColorAccess->numSamples(); - int outBufSize = outSampleStride * outAccess->numSamples(); + int inBufSize = inSampleStride * static_cast(inAccess->numSamples()); + int inColorBufSize = inColorSampleStride * static_cast(inColorAccess->numSamples()); + int outBufSize = outSampleStride * static_cast(outAccess->numSamples()); std::vector vIn(inBufSize); std::vector vInColor(inColorBufSize); @@ -325,9 +361,10 @@ TEST_P(OpJointBilateralFilter, JointBilateralFilter_packed) EXPECT_EQ(cudaSuccess, cudaMemcpy(inData->basePtr(), inGold.data(), inBufSize, cudaMemcpyHostToDevice)); EXPECT_EQ(cudaSuccess, cudaMemcpy(inColorData->basePtr(), inColorGold.data(), inColorBufSize, cudaMemcpyHostToDevice)); + const int rowStride{static_cast(inAccess->rowStride())}; CPUJointBilateralFilterTensor(inGold, inColorGold, outGold, inAccess->numCols(), inAccess->numRows(), - inAccess->numSamples(), inAccess->rowStride(), channels, inSampleStride, d, - sigmaColor, sigmaSpace); + inAccess->numSamples(), rowStride, channels, inSampleStride, d, sigmaColor, + sigmaSpace); // run operator cvcuda::JointBilateralFilter jointBilateralFilterOp; @@ -340,13 +377,15 @@ TEST_P(OpJointBilateralFilter, JointBilateralFilter_packed) EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); EXPECT_EQ(cudaSuccess, cudaMemcpy(outTest.data(), outData->basePtr(), outBufSize, cudaMemcpyDeviceToHost)); - ASSERT_TRUE(CompareTensors(outTest, outGold, inAccess->numCols(), inAccess->numRows(), inAccess->numSamples(), - inAccess->rowStride(), channels, inSampleStride, 0.9f)); + ASSERT_TRUE(CompareTensors( + outTest, outGold, static_cast(inAccess->numCols()), static_cast(inAccess->numRows()), + static_cast(inAccess->numSamples()), static_cast(inAccess->rowStride()), + static_cast(channels), static_cast(inSampleStride), 0.9f)); } EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); } -TEST_P(OpJointBilateralFilter, JointBilateralFilter_VarShape) +TEST_P(OpJointBilateralFilter, varshape_correct_output) { cudaStream_t stream; EXPECT_EQ(cudaSuccess, cudaStreamCreate(&stream)); @@ -359,12 +398,14 @@ TEST_P(OpJointBilateralFilter, JointBilateralFilter_VarShape) std::vector fmts{nvcv::FMT_U8, nvcv::ImageFormat{NVCV_IMAGE_FORMAT_2U8}, nvcv::FMT_RGB8, nvcv::FMT_RGBA8}; - for (nvcv::ImageFormat fmt : fmts) + for (nvcv::ImageFormat fmt : fmts) // NOSONAR { // Create input varshape - std::default_random_engine rng; - std::uniform_int_distribution udistWidth(width * 0.8, width * 1.1); - std::uniform_int_distribution udistHeight(height * 0.8, height * 1.1); + std::default_random_engine rng; + std::uniform_int_distribution udistWidth(static_cast(static_cast(width) * 0.8), + static_cast(static_cast(width) * 1.1)); + std::uniform_int_distribution udistHeight(static_cast(static_cast(height) * 0.8), + static_cast(static_cast(height) * 1.1)); std::vector imgSrc; std::vector imgSrcColor; @@ -395,10 +436,10 @@ TEST_P(OpJointBilateralFilter, JointBilateralFilter_VarShape) srcColorVec[i].resize(imgSrcColor[i].size().h * srcRowStride); goldVec[i].resize(imgSrc[i].size().h * srcRowStride); dstVec[i].resize(imgSrc[i].size().h * srcRowStride); - std::generate(srcVec[i].begin(), srcVec[i].end(), [&]() { return udist(rng); }); - std::generate(srcColorVec[i].begin(), srcColorVec[i].end(), [&]() { return udist(rng); }); - std::generate(goldVec[i].begin(), goldVec[i].end(), [&]() { return 0; }); - std::generate(dstVec[i].begin(), dstVec[i].end(), [&]() { return 0; }); + std::ranges::generate(srcVec[i], [&udist, &rng]() { return udist(rng); }); + std::ranges::generate(srcColorVec[i], [&udist, &rng]() { return udist(rng); }); + std::ranges::generate(goldVec[i], []() { return 0; }); + std::ranges::generate(dstVec[i], []() { return 0; }); auto imgData = imgSrc[i].exportData(); ASSERT_NE(imgData, nvcv::NullOpt); auto imgColorData = imgSrcColor[i].exportData(); @@ -489,39 +530,155 @@ TEST_P(OpJointBilateralFilter, JointBilateralFilter_VarShape) EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); } -#undef NVCV_IMAGE_FORMAT_2U8 +static void RunJointBilateralTensorPlanarParity(nvcv::ImageFormat interleavedFmt, nvcv::ImageFormat planarFmt, + NVCVBorderType border) +{ + cvcuda::JointBilateralFilter op; + nvcv::test::planar::RunTensorParity( + planarFmt, interleavedFmt, 33, 25, 33, 25, 2, + [&op, border](cudaStream_t stream, const nvcv::Tensor &src, const nvcv::Tensor &dst, nvcv::ImageFormat) + { op(stream, src, src, dst, 5, 15.f, 3.f, border); }); +} -// clang-format off -NVCV_TEST_SUITE_P(OpJointBilateralFilterVarshape_Negative, nvcv::test::ValueList{ - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U8, nvcv::FMT_U16, NVCV_BORDER_CONSTANT, nvcv::TYPE_S32, nvcv::TYPE_F32, nvcv::TYPE_F32, 5, 5}, // in/out image format not same - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U16, nvcv::FMT_U8, nvcv::FMT_U16, NVCV_BORDER_CONSTANT, nvcv::TYPE_S32, nvcv::TYPE_F32, nvcv::TYPE_F32, 5, 5}, // inColor/out image format not same - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_RGB8p, nvcv::FMT_RGB8, nvcv::FMT_RGB8, NVCV_BORDER_CONSTANT, nvcv::TYPE_S32, nvcv::TYPE_F32, nvcv::TYPE_F32, 5, 5}, // in/out data format not same - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_RGB8, nvcv::FMT_RGB8p, nvcv::FMT_RGB8, NVCV_BORDER_CONSTANT, nvcv::TYPE_S32, nvcv::TYPE_F32, nvcv::TYPE_F32, 5, 5}, // inColor/out data format not same - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_RGB8p, nvcv::FMT_RGB8p, nvcv::FMT_RGB8p, NVCV_BORDER_CONSTANT, nvcv::TYPE_S32, nvcv::TYPE_F32, nvcv::TYPE_F32, 5, 5}, // input not kHWC/kNHWC +static void RunJointBilateralVarShapePlanarParity(nvcv::ImageFormat interleavedFmt, nvcv::ImageFormat planarFmt, + NVCVBorderType border) +{ + cvcuda::JointBilateralFilter op; + nvcv::test::planar::RunVarShapeParity( + planarFmt, interleavedFmt, 31, 23, 31, 23, 2, + [&op, border](cudaStream_t stream, const nvcv::ImageBatchVarShape &src, const nvcv::ImageBatchVarShape &dst, + nvcv::ImageFormat) + { + const int numImages = src.numImages(); + + auto diameter = nvcv::test::planar::MakePerImageTensor(numImages, nvcv::TYPE_S32, 5); + auto sigmaColor = nvcv::test::planar::MakePerImageTensor(numImages, nvcv::TYPE_F32, 15.f); + auto sigmaSpace = nvcv::test::planar::MakePerImageTensor(numImages, nvcv::TYPE_F32, 3.f); + + op(stream, src, src, dst, diameter, sigmaColor, sigmaSpace, border); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + }); +} + +TEST(OpJointBilateralFilterPlanar, tensor_rgb8_matches_interleaved) +{ + RunJointBilateralTensorPlanarParity(nvcv::FMT_RGB8, nvcv::FMT_RGB8p, NVCV_BORDER_REFLECT); +} + +TEST(OpJointBilateralFilterPlanar, tensor_rgba8_matches_interleaved) +{ + RunJointBilateralTensorPlanarParity(nvcv::FMT_RGBA8, nvcv::FMT_RGBA8p, NVCV_BORDER_CONSTANT); +} + +TEST(OpJointBilateralFilterPlanar, varshape_rgb8_matches_interleaved) +{ + RunJointBilateralVarShapePlanarParity(nvcv::FMT_RGB8, nvcv::FMT_RGB8p, NVCV_BORDER_REFLECT); +} + +TEST(OpJointBilateralFilterPlanar, varshape_rgba8_matches_interleaved) +{ + RunJointBilateralVarShapePlanarParity(nvcv::FMT_RGBA8, nvcv::FMT_RGBA8p, NVCV_BORDER_CONSTANT); +} + +static auto OpJointBilateralFilterVarshapeNegativeParams() +{ + nvcv::test::ValueList + params{ + {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U8, nvcv::FMT_U16, NVCV_BORDER_CONSTANT, + nvcv::TYPE_S32, nvcv::TYPE_F32, nvcv::TYPE_F32, 5, 5}, // in/out image format not same + {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U16, nvcv::FMT_U8, nvcv::FMT_U16, NVCV_BORDER_CONSTANT, + nvcv::TYPE_S32, nvcv::TYPE_F32, nvcv::TYPE_F32, 5, 5}, // inColor/out image format not same + {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_RGB8p, nvcv::FMT_RGB8, nvcv::FMT_RGB8, NVCV_BORDER_CONSTANT, + nvcv::TYPE_S32, nvcv::TYPE_F32, nvcv::TYPE_F32, 5, 5}, // in/out data format not same + {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_RGB8, nvcv::FMT_RGB8p, nvcv::FMT_RGB8, NVCV_BORDER_CONSTANT, + nvcv::TYPE_S32, nvcv::TYPE_F32, nvcv::TYPE_F32, 5, 5}, // inColor/out data format not same + }; #ifndef ENABLE_SANITIZER - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U8, nvcv::FMT_U8, static_cast(255), nvcv::TYPE_S32, nvcv::TYPE_F32, nvcv::TYPE_F32, 5, 5}, // invalid border type + params.emplace_back(NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U8, nvcv::FMT_U8, + static_cast(255), nvcv::TYPE_S32, nvcv::TYPE_F32, nvcv::TYPE_F32, 5, 5); #endif - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_F16, nvcv::FMT_F16, nvcv::FMT_F16, NVCV_BORDER_CONSTANT, nvcv::TYPE_S32, nvcv::TYPE_F32, nvcv::TYPE_F32, 5, 5}, // invalid data type - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U8, nvcv::FMT_U8, NVCV_BORDER_CONSTANT, nvcv::TYPE_F32, nvcv::TYPE_F32, nvcv::TYPE_F32, 5, 5}, // invalid diameter data type - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U8, nvcv::FMT_U8, NVCV_BORDER_CONSTANT, nvcv::TYPE_S32, nvcv::TYPE_S32, nvcv::TYPE_F32, 5, 5}, // invalid sigmaColor data type - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U8, nvcv::FMT_U8, NVCV_BORDER_CONSTANT, nvcv::TYPE_S32, nvcv::TYPE_F32, nvcv::TYPE_S32, 5, 5}, // invalid sigmaSpace data type - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U8, nvcv::FMT_U8, NVCV_BORDER_CONSTANT, nvcv::TYPE_S32, nvcv::TYPE_F32, nvcv::TYPE_F32, 6, 5}, // in/out images number not equal -}); + params.emplace_back(NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_F16, nvcv::FMT_F16, nvcv::FMT_F16, NVCV_BORDER_CONSTANT, + nvcv::TYPE_S32, nvcv::TYPE_F32, nvcv::TYPE_F32, 5, 5); + params.emplace_back(NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U8, nvcv::FMT_U8, NVCV_BORDER_CONSTANT, + nvcv::TYPE_F32, nvcv::TYPE_F32, nvcv::TYPE_F32, 5, 5); + params.emplace_back(NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U8, nvcv::FMT_U8, NVCV_BORDER_CONSTANT, + nvcv::TYPE_S32, nvcv::TYPE_S32, nvcv::TYPE_F32, 5, 5); + params.emplace_back(NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U8, nvcv::FMT_U8, NVCV_BORDER_CONSTANT, + nvcv::TYPE_S32, nvcv::TYPE_F32, nvcv::TYPE_S32, 5, 5); + params.emplace_back(NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U8, nvcv::FMT_U8, NVCV_BORDER_CONSTANT, + nvcv::TYPE_S32, nvcv::TYPE_F32, nvcv::TYPE_F32, 6, 5); + return params; +} -NVCV_TEST_SUITE_P(OpJointBilateralFilter_Negative, nvcv::test::ValueList{ - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U8, nvcv::FMT_U16, NVCV_BORDER_CONSTANT}, // in/out image format not same - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U16, nvcv::FMT_U8, nvcv::FMT_U16, NVCV_BORDER_CONSTANT}, // inColor/out image format not same - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_RGB8p, nvcv::FMT_RGB8, nvcv::FMT_RGB8, NVCV_BORDER_CONSTANT}, // in/out data format not same - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_RGB8, nvcv::FMT_RGB8p, nvcv::FMT_RGB8, NVCV_BORDER_CONSTANT}, // inColor/out data format not same - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_RGB8p, nvcv::FMT_RGB8p, nvcv::FMT_RGB8p, NVCV_BORDER_CONSTANT}, // input not kHWC/kNHWC - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_RGB8, nvcv::FMT_RGB8, nvcv::FMT_RGB8p, NVCV_BORDER_CONSTANT}, // inColor not kHWC/kNHWC +NVCV_TEST_SUITE_P(OpJointBilateralFilterVarshape_Negative, OpJointBilateralFilterVarshapeNegativeParams()); + +static auto OpJointBilateralFilterNegativeParams() +{ + nvcv::test::ValueList params{ + {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U8, nvcv::FMT_U16, + NVCV_BORDER_CONSTANT}, // in/out image format not same + {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U16, nvcv::FMT_U8, nvcv::FMT_U16, + NVCV_BORDER_CONSTANT}, // inColor/out image format not same + {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_RGB8p, nvcv::FMT_RGB8, nvcv::FMT_RGB8, + NVCV_BORDER_CONSTANT}, // in/out data format not same + {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_RGB8, nvcv::FMT_RGB8p, nvcv::FMT_RGB8, + NVCV_BORDER_CONSTANT}, // inColor/out data format not same + {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_RGB8, nvcv::FMT_RGB8, nvcv::FMT_RGB8p, + NVCV_BORDER_CONSTANT}, // inColor not kHWC/kNHWC + }; #ifndef ENABLE_SANITIZER - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U8, nvcv::FMT_U8, static_cast(255)}, // invalid border type + params.emplace_back(NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U8, nvcv::FMT_U8, + static_cast(255)); #endif - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_F16, nvcv::FMT_F16, nvcv::FMT_F16, NVCV_BORDER_CONSTANT}, // invalid data type -}); + params.emplace_back(NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_F16, nvcv::FMT_F16, nvcv::FMT_F16, NVCV_BORDER_CONSTANT); + return params; +} -// clang-format on +NVCV_TEST_SUITE_P(OpJointBilateralFilter_Negative, OpJointBilateralFilterNegativeParams()); + +#undef NVCV_IMAGE_FORMAT_2U8 + +TEST(OpJointBilateralFilter_Negative, tensor_planar_2channel_rejected) +{ + cudaStream_t stream; + EXPECT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + nvcv::Tensor imgOut( + { + {5, 2, 24, 24}, + "NCHW" + }, + nvcv::TYPE_U8); + nvcv::Tensor imgIn( + { + {5, 2, 24, 24}, + "NCHW" + }, + nvcv::TYPE_U8); + nvcv::Tensor imgInColor( + { + {5, 2, 24, 24}, + "NCHW" + }, + nvcv::TYPE_U8); + + int diameter = 4; + float sigmaColor = 5; + float sigmaSpace = 3; + + cvcuda::JointBilateralFilter jointBilateralFilterOp; + + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall( + [&jointBilateralFilterOp, &stream, &imgIn, &imgInColor, &imgOut, &diameter, &sigmaColor, &sigmaSpace] + { + jointBilateralFilterOp(stream, imgIn, imgInColor, imgOut, diameter, sigmaColor, sigmaSpace, + NVCV_BORDER_CONSTANT); + })); + + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} TEST_P(OpJointBilateralFilter_Negative, op) { @@ -549,7 +706,8 @@ TEST_P(OpJointBilateralFilter_Negative, op) cvcuda::JointBilateralFilter jointBilateralFilterOp; EXPECT_EQ(expectedReturnCode, nvcv::ProtectCall( - [&] { + [&jointBilateralFilterOp, &stream, &imgIn, &imgInColor, &imgOut, &diameter, + &sigmaColor, &sigmaSpace, &borderType] { jointBilateralFilterOp(stream, imgIn, imgInColor, imgOut, diameter, sigmaColor, sigmaSpace, borderType); })); @@ -580,9 +738,9 @@ TEST_P(OpJointBilateralFilterVarshape_Negative, op) float sigmaSpace = 3; // Create input varshape - std::default_random_engine rng; - std::uniform_int_distribution udistWidth(width * 0.8, width * 1.1); - std::uniform_int_distribution udistHeight(height * 0.8, height * 1.1); + std::default_random_engine rng; + std::uniform_int_distribution udistWidth(width * 8 / 10, width * 11 / 10); + std::uniform_int_distribution udistHeight(height * 8 / 10, height * 11 / 10); std::vector imgSrc; std::vector imgSrcColor; @@ -646,7 +804,8 @@ TEST_P(OpJointBilateralFilterVarshape_Negative, op) // Run operator cvcuda::JointBilateralFilter jointBilateralFilterOp; EXPECT_EQ(expectedReturnCode, nvcv::ProtectCall( - [&] + [&jointBilateralFilterOp, &stream, &batchSrc, &batchSrcColor, &batchDst, + &diameterTensor, &sigmaColorTensor, &sigmaSpaceTensor, &borderType] { jointBilateralFilterOp(stream, batchSrc, batchSrcColor, batchDst, diameterTensor, sigmaColorTensor, sigmaSpaceTensor, @@ -667,12 +826,8 @@ TEST(OpJointBilateralFilter_Negative, varshape_hasDifferentFormat) { fmt, fmt, nvcv::FMT_U8} }; - for (auto testCase : testSet) + for (const auto &[inputFmtExtra, inputColorFmtExtra, outputFmtExtra] : testSet) { - nvcv::ImageFormat inputFmtExtra = std::get<0>(testCase); - nvcv::ImageFormat inputColorFmtExtra = std::get<1>(testCase); - nvcv::ImageFormat outputFmtExtra = std::get<2>(testCase); - cudaStream_t stream; EXPECT_EQ(cudaSuccess, cudaStreamCreate(&stream)); @@ -684,9 +839,11 @@ TEST(OpJointBilateralFilter_Negative, varshape_hasDifferentFormat) int numberOfImages = 5; // Create input varshape - std::default_random_engine rng; - std::uniform_int_distribution udistWidth(width * 0.8, width * 1.1); - std::uniform_int_distribution udistHeight(height * 0.8, height * 1.1); + std::default_random_engine rng; + std::uniform_int_distribution udistWidth(static_cast(static_cast(width) * 0.8), + static_cast(static_cast(width) * 1.1)); + std::uniform_int_distribution udistHeight(static_cast(static_cast(height) * 0.8), + static_cast(static_cast(height) * 1.1)); std::vector imgSrc; std::vector imgSrcColor; @@ -754,7 +911,8 @@ TEST(OpJointBilateralFilter_Negative, varshape_hasDifferentFormat) // Run operator cvcuda::JointBilateralFilter jointBilateralFilterOp; EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall( - [&] + [&jointBilateralFilterOp, &stream, &batchSrc, &batchSrcColor, + &batchDst, &diameterTensor, &sigmaColorTensor, &sigmaSpaceTensor] { jointBilateralFilterOp(stream, batchSrc, batchSrcColor, batchDst, diameterTensor, sigmaColorTensor, diff --git a/tests/cvcuda/system/TestOpJpegCompressionDistortion.cpp b/tests/cvcuda/system/TestOpJpegCompressionDistortion.cpp new file mode 100644 index 000000000..38776531b --- /dev/null +++ b/tests/cvcuda/system/TestOpJpegCompressionDistortion.cpp @@ -0,0 +1,756 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "ElementwiseOpHarness.hpp" // ExpectRejected, RunTensorCorrectBuffer +#include "PlanarParityUtils.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace test = nvcv::test; +namespace ew = nvcv::test::elementwise; + +namespace { + +// Independent CPU gold ----------------------------------------------------------------------- +// +// Reimplements the operator's documented pipeline (DALI's JpegCompressionDistortion fixed to +// 4:2:0): full-range JFIF YCbCr with uint8 intermediate storage, chroma from the 2x2 RGB box +// average, level shift, per-8x8-block forward DCT (rows then columns), Annex-K quantization +// scaled by the libjpeg quality mapping, inverse DCT (columns then rows), and nearest-neighbor +// chroma upsampling. Every multiply-add uses the same canonical fmaf order as the kernel; all +// other operations (add/sub, lone multiply, 1/x, roundf, round-to-nearest-even saturating cast) +// are correctly rounded, so the gold matches the GPU output bit-exactly. Constants are +// redeclared here rather than shared with the implementation header. + +constexpr float kA = 1.387039845322148f; // sqrt(2) * cos( pi / 16) +constexpr float kB = 1.306562964876377f; // sqrt(2) * cos( pi / 8) +constexpr float kC = 1.175875602419359f; // sqrt(2) * cos(3 * pi / 16) +constexpr float kD = 0.785694958387102f; // sqrt(2) * cos(5 * pi / 16) +constexpr float kE = 0.541196100146197f; // sqrt(2) * cos(3 * pi / 8) +constexpr float kF = 0.275899379282943f; // sqrt(2) * cos(7 * pi / 16) +constexpr float kNorm = 0.3535533905932737f; // 1 / sqrt(8) + +constexpr std::array kLumaQuantBase = { + 16, 11, 10, 16, 24, 40, 51, 61, // + 12, 12, 14, 19, 26, 58, 60, 55, // + 14, 13, 16, 24, 40, 57, 69, 56, // + 14, 17, 22, 29, 51, 87, 80, 62, // + 18, 22, 37, 56, 68, 109, 103, 77, // + 24, 35, 55, 64, 81, 104, 113, 92, // + 49, 64, 78, 87, 103, 121, 120, 101, // + 72, 92, 95, 98, 112, 100, 103, 99, // +}; + +constexpr std::array kChromaQuantBase = { + 17, 18, 24, 47, 99, 99, 99, 99, // + 18, 21, 26, 66, 99, 99, 99, 99, // + 24, 26, 56, 99, 99, 99, 99, 99, // + 47, 66, 99, 99, 99, 99, 99, 99, // + 99, 99, 99, 99, 99, 99, 99, 99, // + 99, 99, 99, 99, 99, 99, 99, 99, // + 99, 99, 99, 99, 99, 99, 99, 99, // + 99, 99, 99, 99, 99, 99, 99, 99, // +}; + +struct RgbU8 +{ + uint8_t r; + uint8_t g; + uint8_t b; +}; + +// Round-to-nearest-even saturating cast (the CPU equivalent of the device's cvt.rni.sat.u8.f32). +uint8_t GoldSatCastU8(float v) +{ + const float r = std::rint(v); // ties-to-even under the default rounding mode + return static_cast(std::min(255.0f, std::max(0.0f, r))); +} + +float GoldDot2(float c0, float v0, float c1, float v1) +{ + return std::fmaf(c0, v0, c1 * v1); +} + +float GoldDot3(float c0, float v0, float c1, float v1, float c2, float v2) +{ + return std::fmaf(c0, v0, GoldDot2(c1, v1, c2, v2)); +} + +float GoldDot4(float c0, float v0, float c1, float v1, float c2, float v2, float c3, float v3) +{ + return std::fmaf(c0, v0, GoldDot3(c1, v1, c2, v2, c3, v3)); +} + +float GoldDot3Bias(float c0, float v0, float c1, float v1, float c2, float v2, float bias) +{ + return std::fmaf(c0, v0, std::fmaf(c1, v1, std::fmaf(c2, v2, bias))); +} + +float GoldQuantScale(int quality) +{ + const int q = std::clamp(quality, 1, 100); + return q < 50 ? 50.0f / static_cast(q) : 2.0f - static_cast(2 * q) / 100.0f; +} + +// Half-away-from-zero rounding (DALI host-build / libjpeg semantics); ties matter at q=75. +float GoldQuantEntry(float scale, uint8_t base) +{ + const float entry = std::roundf(scale * static_cast(base)); + return std::clamp(entry, 1.0f, 255.0f); +} + +float GoldQuantize(float value, float q) +{ + return q * std::roundf(value * (1.0f / q)); +} + +uint8_t GoldRgbToY(RgbU8 p) +{ + return GoldSatCastU8(GoldDot3(0.299f, p.r, 0.587f, p.g, 0.114f, p.b)); +} + +uint8_t GoldRgbToCb(RgbU8 p) +{ + return GoldSatCastU8(GoldDot3Bias(-0.16873589f, p.r, -0.33126411f, p.g, 0.5f, p.b, 128.0f)); +} + +uint8_t GoldRgbToCr(RgbU8 p) +{ + return GoldSatCastU8(GoldDot3Bias(0.5f, p.r, -0.41868759f, p.g, -0.08131241f, p.b, 128.0f)); +} + +RgbU8 GoldYCbCrToRgb(uint8_t y, uint8_t cb, uint8_t cr) +{ + const auto ys = static_cast(y); + const float tb = static_cast(cb) - 128.0f; + const float tr = static_cast(cr) - 128.0f; + return RgbU8{GoldSatCastU8(std::fmaf(1.402f, tr, ys)), + GoldSatCastU8(std::fmaf(-0.714136285f, tr, std::fmaf(-0.344136285f, tb, ys))), + GoldSatCastU8(std::fmaf(1.772f, tb, ys))}; +} + +RgbU8 GoldAvg4(RgbU8 p00, RgbU8 p01, RgbU8 p10, RgbU8 p11) +{ + return RgbU8{GoldSatCastU8(static_cast(p00.r + p01.r + p10.r + p11.r) * 0.25f), + GoldSatCastU8(static_cast(p00.g + p01.g + p10.g + p11.g) * 0.25f), + GoldSatCastU8(static_cast(p00.b + p01.b + p10.b + p11.b) * 0.25f)}; +} + +void GoldFwdDct8(float *data, int stride) +{ + float x0 = data[0 * stride]; + float x1 = data[1 * stride]; + float x2 = data[2 * stride]; + float x3 = data[3 * stride]; + float x4 = data[4 * stride]; + float x5 = data[5 * stride]; + float x6 = data[6 * stride]; + float x7 = data[7 * stride]; + + const float tmp0 = x0 + x7; + const float tmp1 = x1 + x6; + const float tmp2 = x2 + x5; + const float tmp3 = x3 + x4; + + const float tmp4 = x0 - x7; + const float tmp5 = x6 - x1; + const float tmp6 = x2 - x5; + const float tmp7 = x4 - x3; + + const float tmp8 = tmp0 + tmp3; + const float tmp9 = tmp0 - tmp3; + const float tmp10 = tmp1 + tmp2; + const float tmp11 = tmp1 - tmp2; + + x0 = kNorm * (tmp8 + tmp10); + x2 = kNorm * GoldDot2(kB, tmp9, kE, tmp11); + x4 = kNorm * (tmp8 - tmp10); + x6 = kNorm * GoldDot2(kE, tmp9, -kB, tmp11); + + x1 = kNorm * GoldDot4(kA, tmp4, -kC, tmp5, kD, tmp6, -kF, tmp7); + x3 = kNorm * GoldDot4(kC, tmp4, kF, tmp5, -kA, tmp6, kD, tmp7); + x5 = kNorm * GoldDot4(kD, tmp4, kA, tmp5, kF, tmp6, -kC, tmp7); + x7 = kNorm * GoldDot4(kF, tmp4, kD, tmp5, kC, tmp6, kA, tmp7); + + data[0 * stride] = x0; + data[1 * stride] = x1; + data[2 * stride] = x2; + data[3 * stride] = x3; + data[4 * stride] = x4; + data[5 * stride] = x5; + data[6 * stride] = x6; + data[7 * stride] = x7; +} + +void GoldInvDct8(float *data, int stride) +{ + float x0 = data[0 * stride]; + float x1 = data[1 * stride]; + float x2 = data[2 * stride]; + float x3 = data[3 * stride]; + float x4 = data[4 * stride]; + float x5 = data[5 * stride]; + float x6 = data[6 * stride]; + float x7 = data[7 * stride]; + + const float tmp0 = x0 + x4; + const float tmp1 = GoldDot2(kB, x2, kE, x6); + + const float tmp2 = tmp0 + tmp1; + const float tmp3 = tmp0 - tmp1; + const float tmp4 = GoldDot4(kF, x7, kA, x1, kC, x3, kD, x5); + const float tmp5 = GoldDot4(kA, x7, -kF, x1, kD, x3, -kC, x5); + + const float tmp6 = x0 - x4; + const float tmp7 = GoldDot2(kE, x2, -kB, x6); + + const float tmp8 = tmp6 + tmp7; + const float tmp9 = tmp6 - tmp7; + const float tmp10 = GoldDot4(kC, x1, -kD, x7, -kF, x3, -kA, x5); + const float tmp11 = GoldDot4(kD, x1, kC, x7, -kA, x3, kF, x5); + + x0 = kNorm * (tmp2 + tmp4); + x7 = kNorm * (tmp2 - tmp4); + x4 = kNorm * (tmp3 + tmp5); + x3 = kNorm * (tmp3 - tmp5); + + x1 = kNorm * (tmp8 + tmp10); + x5 = kNorm * (tmp9 - tmp11); + x2 = kNorm * (tmp9 + tmp11); + x6 = kNorm * (tmp8 - tmp10); + + data[0 * stride] = x0; + data[1 * stride] = x1; + data[2 * stride] = x2; + data[3 * stride] = x3; + data[4 * stride] = x4; + data[5 * stride] = x5; + data[6 * stride] = x6; + data[7 * stride] = x7; +} + +// Forward DCT (rows then columns), quantization, inverse DCT (columns then rows) on one 8x8 +// block at blk within a plane of row pitch planeW. +void GoldDctQuantIdctBlock(float *blk, int planeW, const std::array &table) +{ + for (int r = 0; r < 8; ++r) + { + GoldFwdDct8(blk + static_cast(r) * planeW, 1); + } + for (int c = 0; c < 8; ++c) + { + GoldFwdDct8(blk + c, planeW); + } + for (int i = 0; i < 8; ++i) + { + for (int j = 0; j < 8; ++j) + { + float &v = blk[static_cast(i) * planeW + j]; + v = GoldQuantize(v, table[static_cast(i) * 8 + j]); + } + } + for (int c = 0; c < 8; ++c) + { + GoldInvDct8(blk + c, planeW); + } + for (int r = 0; r < 8; ++r) + { + GoldInvDct8(blk + static_cast(r) * planeW, 1); + } +} + +// Applies the block round trip over every 8x8 block of a padded plane whose dimensions are +// multiples of 8. +void GoldDctQuantIdctPlane(std::vector &plane, int planeW, int planeH, const std::array &table) +{ + for (int by = 0; by < planeH / 8; ++by) + { + for (int bx = 0; bx < planeW / 8; ++bx) + { + GoldDctQuantIdctBlock(&plane[static_cast(by) * 8 * planeW + static_cast(bx) * 8], planeW, + table); + } + } +} + +inline int PadTo8(int v) +{ + return (v + 7) / 8 * 8; +} + +// Full-pipeline gold on one interleaved HWC (or single-channel HW) image. +std::vector JpegDistortionGold(const std::vector &in, int w, int h, int channels, int quality) +{ + const float scale = GoldQuantScale(quality); + std::array lumaTable{}; + std::array chromaTable{}; + for (int i = 0; i < 64; ++i) + { + lumaTable[i] = GoldQuantEntry(scale, kLumaQuantBase[i]); + chromaTable[i] = GoldQuantEntry(scale, kChromaQuantBase[i]); + } + + std::vector out(in.size()); + + if (channels == 1) + { + const int planeW = PadTo8(w); + const int planeH = PadTo8(h); + std::vector plane(static_cast(planeW) * planeH); + for (int y = 0; y < planeH; ++y) + { + for (int x = 0; x < planeW; ++x) + { + const uint8_t v = in[static_cast(std::min(y, h - 1)) * w + std::min(x, w - 1)]; + plane[static_cast(y) * planeW + x] = static_cast(v) - 128.0f; + } + } + GoldDctQuantIdctPlane(plane, planeW, planeH, lumaTable); + for (int y = 0; y < h; ++y) + { + for (int x = 0; x < w; ++x) + { + out[static_cast(y) * w + x] + = GoldSatCastU8(plane[static_cast(y) * planeW + x] + 128.0f); + } + } + return out; + } + + // 4:2:0 color path: the luma plane covers twice the padded chroma grid, so partially-in-image + // luma blocks see the same edge-replicated content the kernel loads. + const int chromaW = PadTo8((w + 1) / 2); + const int chromaH = PadTo8((h + 1) / 2); + const int lumaW = 2 * chromaW; + const int lumaH = 2 * chromaH; + + const auto pixel = [&in, w, h](int y, int x) + { + const size_t ofs = (static_cast(std::min(y, h - 1)) * w + std::min(x, w - 1)) * 3; + return RgbU8{in[ofs], in[ofs + 1], in[ofs + 2]}; + }; + + std::vector luma(static_cast(lumaW) * lumaH); + for (int y = 0; y < lumaH; ++y) + { + for (int x = 0; x < lumaW; ++x) + { + luma[static_cast(y) * lumaW + x] = static_cast(GoldRgbToY(pixel(y, x))) - 128.0f; + } + } + + std::vector cb(static_cast(chromaW) * chromaH); + std::vector cr(cb.size()); + for (int cy = 0; cy < chromaH; ++cy) + { + for (int cx = 0; cx < chromaW; ++cx) + { + const RgbU8 avg = GoldAvg4(pixel(2 * cy, 2 * cx), pixel(2 * cy, 2 * cx + 1), pixel(2 * cy + 1, 2 * cx), + pixel(2 * cy + 1, 2 * cx + 1)); + cb[static_cast(cy) * chromaW + cx] = static_cast(GoldRgbToCb(avg)) - 128.0f; + cr[static_cast(cy) * chromaW + cx] = static_cast(GoldRgbToCr(avg)) - 128.0f; + } + } + + GoldDctQuantIdctPlane(luma, lumaW, lumaH, lumaTable); + GoldDctQuantIdctPlane(cb, chromaW, chromaH, chromaTable); + GoldDctQuantIdctPlane(cr, chromaW, chromaH, chromaTable); + + for (int y = 0; y < h; ++y) + { + for (int x = 0; x < w; ++x) + { + const size_t cofs = static_cast(y / 2) * chromaW + x / 2; + const uint8_t y8 = GoldSatCastU8(luma[static_cast(y) * lumaW + x] + 128.0f); + const uint8_t cb8 = GoldSatCastU8(cb[cofs] + 128.0f); + const uint8_t cr8 = GoldSatCastU8(cr[cofs] + 128.0f); + const RgbU8 rgb = GoldYCbCrToRgb(y8, cb8, cr8); + + const size_t ofs = (static_cast(y) * w + x) * 3; + out[ofs] = rgb.r; + out[ofs + 1] = rgb.g; + out[ofs + 2] = rgb.b; + } + } + return out; +} + +// Bind the operator with a fixed scalar quality for the harness / parity / negative helpers. +auto invokeQuality(int quality) +{ + return [quality](cudaStream_t s, const auto &in, auto &out) + { + cvcuda::JpegCompressionDistortion op; + op(s, in, out, quality); + }; +} + +} // namespace + +// Tensor correctness over the declared dtype x channel matrix + edge factors -------------------- +// clang-format off +NVCV_TEST_SUITE_P(OpJpegCompressionDistortion, test::ValueList +{ + // width, height, batch, format, quality + { 64, 32, 2, nvcv::FMT_RGB8, 50}, // block-aligned batch + { 123, 67, 2, nvcv::FMT_RGB8, 50}, // odd size (edge-replicated blocks) + { 123, 67, 1, nvcv::FMT_RGB8, 1}, // strongest distortion + { 123, 67, 1, nvcv::FMT_RGB8, 100}, // weakest distortion + { 17, 19, 1, nvcv::FMT_RGB8, 10}, // small odd size + { 9, 7, 1, nvcv::FMT_RGB8, 95}, // sub-block image + { 8, 8, 1, nvcv::FMT_RGB8, 50}, // single block + { 123, 67, 2, nvcv::FMT_RGB8, 75}, // exact-tie table scale (0.5, half-away) + { 64, 32, 2, nvcv::FMT_U8, 50}, // grayscale, block-aligned + { 64, 32, 1, nvcv::FMT_U8, 75}, // grayscale, exact-tie table scale + { 123, 67, 1, nvcv::FMT_U8, 10}, // grayscale, odd size + { 17, 19, 1, nvcv::FMT_U8, 100}, // grayscale, small odd size + { 9, 7, 1, nvcv::FMT_U8, 1}, // grayscale, sub-block image +}); + +// clang-format on +TEST_P(OpJpegCompressionDistortion, correct_output) +{ + const int w = GetParamValue<0>(); + const int h = GetParamValue<1>(); + const int b = GetParamValue<2>(); + const nvcv::ImageFormat fmt = GetParamValue<3>(); + const int quality = GetParamValue<4>(); + + ew::RunTensorCorrectBuffer( + w, h, b, fmt, + [w, h, quality](const std::vector &in, int channels) + { return JpegDistortionGold(in, w, h, channels, quality); }, + invokeQuality(quality)); +} + +// Per-image quality tensor: every sample must be distorted with its own quality value; values +// outside [1, 100] are clamped on the device (DALI semantics). +TEST(OpJpegCompressionDistortion, per_image_quality_tensor) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + constexpr int w = 40; + constexpr int h = 25; + + const std::vector qualities{5, 50, 75, 95, 0, 200}; // 0 / 200 exercise the device clamp + const std::vector goldQualities{5, 50, 75, 95, 1, 100}; + const auto batch = static_cast(qualities.size()); + + nvcv::Tensor inTensor = nvcv::util::CreateTensor(batch, w, h, nvcv::FMT_RGB8); + nvcv::Tensor outTensor = nvcv::util::CreateTensor(batch, w, h, nvcv::FMT_RGB8); + auto inData = inTensor.exportData(); + auto outData = outTensor.exportData(); + ASSERT_NE(inData, nullptr); + ASSERT_NE(outData, nullptr); + + for (int s = 0; s < batch; ++s) + { + std::vector in(static_cast(w) * h * 3); + ew::FillDeterministicValues(in, static_cast(s)); + nvcv::util::SetImageTensorFromVector(*inData, in, s); + } + + nvcv::Tensor qualityTensor({{batch}, "N"}, nvcv::TYPE_S32); + test::planar::UploadTensorValues(qualityTensor, qualities); + + cvcuda::JpegCompressionDistortion op; + ASSERT_NO_THROW(op(stream, inTensor, outTensor, qualityTensor)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + for (int s = 0; s < batch; ++s) + { + SCOPED_TRACE(s); + std::vector in; + std::vector got; + nvcv::util::GetImageVectorFromTensor(*inData, s, in); + nvcv::util::GetImageVectorFromTensor(*outData, s, got); + EXPECT_EQ(JpegDistortionGold(in, w, h, 3, goldQualities[s]), got); + } + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +// VarShape correctness: per-image sizes with both a scalar quality and a per-image tensor. +// clang-format off +NVCV_TEST_SUITE_P(OpJpegCompressionDistortionVarShape, test::ValueList +{ + {nvcv::FMT_RGB8, false}, // scalar quality + {nvcv::FMT_RGB8, true }, // per-image quality tensor + {nvcv::FMT_U8, false}, // grayscale, scalar quality + {nvcv::FMT_U8, true }, // grayscale, per-image quality tensor +}); + +// clang-format on +TEST_P(OpJpegCompressionDistortionVarShape, varshape_correct_output) +{ + const nvcv::ImageFormat fmt = GetParamValue<0>(); + const bool perImage = GetParamValue<1>(); + const int channels = fmt.numChannels(); + constexpr std::array widths = {23, 57, 89}; + constexpr std::array heights = {31, 71, 43}; + const auto numImages = static_cast(widths.size()); + const std::vector qualities{15, 75, 85}; + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + std::vector srcImgs; + std::vector dstImgs; + std::vector> inputs(numImages); + for (int i = 0; i < numImages; ++i) + { + srcImgs.emplace_back(nvcv::Size2D{widths[i], heights[i]}, fmt); + dstImgs.emplace_back(nvcv::Size2D{widths[i], heights[i]}, fmt); + + const size_t rowBytes = static_cast(widths[i]) * channels; + inputs[i].resize(rowBytes * heights[i]); + ew::FillDeterministicValues(inputs[i], static_cast(i)); + + auto idata = srcImgs[i].exportData(); + ASSERT_NE(idata, nullptr); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(idata->plane(0).basePtr, idata->plane(0).rowStride, inputs[i].data(), + rowBytes, rowBytes, heights[i], cudaMemcpyHostToDevice)); + } + + nvcv::ImageBatchVarShape src(numImages); + nvcv::ImageBatchVarShape dst(numImages); + src.pushBack(srcImgs.begin(), srcImgs.end()); + dst.pushBack(dstImgs.begin(), dstImgs.end()); + + cvcuda::JpegCompressionDistortion op; + if (perImage) + { + nvcv::Tensor qualityTensor({{numImages}, "N"}, nvcv::TYPE_S32); + test::planar::UploadTensorValues(qualityTensor, qualities); + ASSERT_NO_THROW(op(stream, src, dst, qualityTensor)); + } + else + { + ASSERT_NO_THROW(op(stream, src, dst, 50)); + } + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + for (int i = 0; i < numImages; ++i) + { + SCOPED_TRACE(i); + const size_t rowBytes = static_cast(widths[i]) * channels; + std::vector got(rowBytes * heights[i]); + auto odata = dstImgs[i].exportData(); + ASSERT_NE(odata, nullptr); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(got.data(), rowBytes, odata->plane(0).basePtr, odata->plane(0).rowStride, + rowBytes, heights[i], cudaMemcpyDeviceToHost)); + + const int quality = perImage ? qualities[i] : 50; + EXPECT_EQ(JpegDistortionGold(inputs[i], widths[i], heights[i], channels, quality), got); + } + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpJpegCompressionDistortion, zero_extent_tensors_are_noops) +{ + ew::ExpectZeroExtentTensorNoop( + nvcv::TensorShape{ + {0, 3, 5, 1}, + "NHWC" + }, + nvcv::TYPE_U8, invokeQuality(50)); + ew::ExpectZeroExtentTensorNoop( + nvcv::TensorShape{ + {1, 0, 5, 1}, + "NHWC" + }, + nvcv::TYPE_U8, invokeQuality(50)); + ew::ExpectZeroExtentTensorNoop( + nvcv::TensorShape{ + {1, 3, 0, 1}, + "NHWC" + }, + nvcv::TYPE_U8, invokeQuality(50)); +} + +TEST(OpJpegCompressionDistortion, empty_matching_varshape_is_noop) +{ + ew::ExpectEmptyVarShapeNoop(invokeQuality(50)); +} + +// Planar == interleaved parity ------------------------------------------------------------------ +// clang-format off +NVCV_TEST_SUITE_P(OpJpegCompressionDistortionPlanar, + test::ValueList{ + {176, 113, 2, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + { 48, 32, 2, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, +}); + +// clang-format on +TEST_P(OpJpegCompressionDistortionPlanar, tensor_matches_interleaved) +{ + test::planar::RunTensorParity(GetParamValue<3>(), GetParamValue<4>(), GetParamValue<0>(), GetParamValue<1>(), + GetParamValue<0>(), GetParamValue<1>(), GetParamValue<2>(), + [](cudaStream_t s, const nvcv::Tensor &src, const nvcv::Tensor &dst, + nvcv::ImageFormat) { EXPECT_NO_THROW(invokeQuality(50)(s, src, dst)); }); +} + +TEST_P(OpJpegCompressionDistortionPlanar, varshape_matches_interleaved) +{ + test::planar::RunVarShapeParity( + GetParamValue<3>(), GetParamValue<4>(), GetParamValue<0>(), GetParamValue<1>(), GetParamValue<0>(), + GetParamValue<1>(), GetParamValue<2>(), + [](cudaStream_t s, const nvcv::ImageBatchVarShape &src, const nvcv::ImageBatchVarShape &dst, nvcv::ImageFormat) + { EXPECT_NO_THROW(invokeQuality(50)(s, src, dst)); }); +} + +// Negative tests: the complement of the support matrix must be rejected ------------------------- +// clang-format off +NVCV_TEST_SUITE_P(OpJpegCompressionDistortion_Negative, test::ValueList{ + {nvcv::FMT_U16, nvcv::FMT_U16 }, // unsupported dtype (16-bit unsigned) + {nvcv::FMT_F32, nvcv::FMT_F32 }, // unsupported dtype (32-bit float) + {nvcv::FMT_S8, nvcv::FMT_S8 }, // unsupported dtype (8-bit signed) + {nvcv::FMT_RGBA8, nvcv::FMT_RGBA8 }, // unsupported channel count (4) + {nvcv::FMT_RGB8, nvcv::FMT_RGB8p }, // layout mismatch (interleaved in, planar out) + {nvcv::FMT_RGB8, nvcv::FMT_RGBf32}, // input/output data type mismatch +}); + +// clang-format on +TEST_P(OpJpegCompressionDistortion_Negative, rejects_unsupported) +{ + ew::ExpectRejected(GetParamValue<0>(), GetParamValue<1>(), invokeQuality(50)); +} + +TEST(OpJpegCompressionDistortion_Negative, rejects_two_channel) +{ + // 2-channel uint8 (no predefined FMT_2U8) so the rejection isolates the channel count, not the dtype. + constexpr nvcv::ImageFormat fmt2U8{NVCV_DETAIL_MAKE_NONCOLOR_FMT1(PL, UNSIGNED, XY00, ASSOCIATED, X8_Y8)}; + ew::ExpectRejected(fmt2U8, fmt2U8, invokeQuality(50), 16, 16); +} + +TEST(OpJpegCompressionDistortion_Negative, rejects_too_tall_image) +{ + // 1x1048576 grayscale: the luma grid needs DivUp(1048576, 16) = 65536 rows, past the CUDA grid.y limit. + ew::ExpectRejected(nvcv::FMT_U8, nvcv::FMT_U8, invokeQuality(50), 1, 1048576); +} + +TEST(OpJpegCompressionDistortion_Negative, rejects_non_rgb_varshape_formats) +{ + ew::ExpectVarShapeRejected(nvcv::FMT_YUV8, invokeQuality(50)); + ew::ExpectVarShapeRejected(nvcv::FMT_YUV8p, invokeQuality(50)); + ew::ExpectVarShapeRejected(nvcv::FMT_BGR8, invokeQuality(50)); + ew::ExpectVarShapeRejected(nvcv::FMT_NV12, invokeQuality(50)); +} + +TEST(OpJpegCompressionDistortion_Negative, rejects_vector_tensor_dtype) +{ + nvcv::Tensor src( + nvcv::TensorShape{ + {1, 2, 4, 1}, + "NHWC" + }, + nvcv::TYPE_3U8); + nvcv::Tensor dst( + nvcv::TensorShape{ + {1, 2, 4, 1}, + "NHWC" + }, + nvcv::TYPE_3U8); + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&] { cvcuda::JpegCompressionDistortion{}(stream, src, dst, 50); })); + EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpJpegCompressionDistortion_Negative, rejects_out_of_range_scalar_quality) +{ + ew::ExpectRejected(nvcv::FMT_RGB8, nvcv::FMT_RGB8, invokeQuality(0)); + ew::ExpectRejected(nvcv::FMT_RGB8, nvcv::FMT_RGB8, invokeQuality(101)); +} + +TEST(OpJpegCompressionDistortion_Negative, rejects_bad_quality_tensor) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + constexpr int batch = 2; + nvcv::Tensor inTensor = nvcv::util::CreateTensor(batch, 16, 16, nvcv::FMT_RGB8); + nvcv::Tensor outTensor = nvcv::util::CreateTensor(batch, 16, 16, nvcv::FMT_RGB8); + + cvcuda::JpegCompressionDistortion op; + + nvcv::Tensor wrongDtype({{batch}, "N"}, nvcv::TYPE_F32); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&] { op(stream, inTensor, outTensor, wrongDtype); })); + + nvcv::Tensor wrongRank( + { + {batch, 1}, + "NC" + }, + nvcv::TYPE_S32); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&] { op(stream, inTensor, outTensor, wrongRank); })); + + nvcv::Tensor wrongLength({{batch + 1}, "N"}, nvcv::TYPE_S32); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&] { op(stream, inTensor, outTensor, wrongLength); })); + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpJpegCompressionDistortion_Negative, rejects_mismatched_varshape_image_sizes) +{ + std::vector srcImages{ + nvcv::Image{{17, 19}, nvcv::FMT_RGB8}, + nvcv::Image{{31, 37}, nvcv::FMT_RGB8} + }; + std::vector dstImages{ + nvcv::Image{{17, 19}, nvcv::FMT_RGB8}, + nvcv::Image{{23, 29}, nvcv::FMT_RGB8} + }; + + nvcv::ImageBatchVarShape src(2); + nvcv::ImageBatchVarShape dst(2); + src.pushBack(srcImages.begin(), srcImages.end()); + dst.pushBack(dstImages.begin(), dstImages.end()); + + cvcuda::JpegCompressionDistortion op; + try + { + op(nullptr, src, dst, 50); + FAIL() << "Expected mismatched image sizes to be rejected"; + } + catch (const nvcv::Exception &e) + { + EXPECT_EQ(nvcv::Status::ERROR_INVALID_ARGUMENT, e.code()); + EXPECT_STREQ("Input and output image 1 sizes must match: input is 31x37, output is 23x29", e.msg()); + } +} + +TEST(OpJpegCompressionDistortion_Negative, create_null_handle) +{ + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, cvcudaJpegCompressionDistortionCreate(nullptr)); +} diff --git a/tests/cvcuda/system/TestOpLabel.cpp b/tests/cvcuda/system/TestOpLabel.cpp index f312d7cc0..0476fe2d9 100644 --- a/tests/cvcuda/system/TestOpLabel.cpp +++ b/tests/cvcuda/system/TestOpLabel.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,6 +19,7 @@ #include #include +#include #include #include #include @@ -27,6 +28,7 @@ #include #include +#include #include #include #include @@ -34,6 +36,7 @@ #include #include #include +#include #include // ----------------------- Basic utility definitions --------------------------- @@ -51,6 +54,52 @@ using RawBufferType = std::vector; namespace ref { +inline long NumElements(const long4_16a &shape) +{ + return shape.x * shape.y * shape.z * shape.w; +} + +inline long4_16a CoordFromIndex(long idx, const long4_16a &shape) +{ + long w = idx % shape.w; + idx /= shape.w; + long z = idx % shape.z; + idx /= shape.z; + long y = idx % shape.y; + idx /= shape.y; + return long4_16a{idx, y, z, w}; +} + +template +void ForEachCoord(const long4_16a &shape, const Func &func) +{ + for (long idx = 0; idx < NumElements(shape); ++idx) + { + func(CoordFromIndex(idx, shape)); + } +} + +template +void ForEachSampleCoord(const long4_16a &shape, long sample, const Func &func) +{ + long sampleElements = shape.y * shape.z * shape.w; + for (long idx = 0; idx < sampleElements; ++idx) + { + long linear = idx; + long w = linear % shape.w; + linear /= shape.w; + long z = linear % shape.z; + linear /= shape.z; + func(long4_16a{sample, linear, z, w}); + } +} + +template +DT PositionLabel(const long4_16a &coord, const long4_16a &strides) +{ + return static_cast
(coord.y * strides.y / sizeof(DT) + coord.z * strides.z / sizeof(DT) + coord.w); +} + // Pre-filter step is to binarize srcVec using threshold range [min, max] -> 1, zero otherwise template inline void Binarize(RawBufferType &srcVec, const RawBufferType &minVec, const RawBufferType &maxVec, @@ -60,39 +109,50 @@ inline void Binarize(RawBufferType &srcVec, const RawBufferType &minVec, const R bool hasMinThresh = minStrides.x > 0; bool hasMaxThresh = maxStrides.x > 0; - for (long x = 0; x < shape.x; ++x) - { - ST minThresh = hasMinThresh ? util::ValueAt(minVec, minStrides, long1{x}) : 0; - ST maxThresh = hasMaxThresh ? util::ValueAt(maxVec, maxStrides, long1{x}) : 0; + ForEachCoord(shape, + [&srcVec, &minVec, &maxVec, &srcStrides, &minStrides, &maxStrides, hasMinThresh, + hasMaxThresh](const long4_16a &curCoord) + { + ST minThresh = hasMinThresh ? util::ValueAt(minVec, minStrides, long1{curCoord.x}) : 0; + ST maxThresh = hasMaxThresh ? util::ValueAt(maxVec, maxStrides, long1{curCoord.x}) : 0; + ST value = util::ValueAt(srcVec, srcStrides, curCoord); + + if (hasMinThresh && hasMaxThresh) + { + value = (value < minThresh || value > maxThresh) ? 0 : 1; + } + else if (hasMinThresh) + { + value = (value < minThresh) ? 0 : 1; + } + else if (hasMaxThresh) + { + value = (value > maxThresh) ? 0 : 1; + } + + util::ValueAt(srcVec, srcStrides, curCoord) = value; + }); +} - for (long y = 0; y < shape.y; ++y) - { - for (long z = 0; z < shape.z; ++z) - { - for (long w = 0; w < shape.w; ++w) - { - long4_16a curCoord{x, y, z, w}; - - ST value = util::ValueAt(srcVec, srcStrides, curCoord); - - if (hasMinThresh && hasMaxThresh) - { - value = (value < minThresh || value > maxThresh) ? 0 : 1; - } - else if (hasMinThresh) - { - value = (value < minThresh) ? 0 : 1; - } - else if (hasMaxThresh) - { - value = (value > maxThresh) ? 0 : 1; - } - - util::ValueAt(srcVec, srcStrides, curCoord) = value; - } - } - } +template +inline void LabelComponent(RawBufferType &tmpVec, RawBufferType &dstVec, const RawBufferType &srcVec, + const long4_16a &tmpStrides, const long4_16a &dstStrides, const long4_16a &srcStrides, + const long4_16a &shape, const long4_16a &curCoord, ST value, DT label); + +template +void LabelPixel(RawBufferType &tmpVec, RawBufferType &dstVec, const RawBufferType &srcVec, const long4_16a &tmpStrides, + const long4_16a &dstStrides, const long4_16a &srcStrides, const long4_16a &shape, + const long4_16a &curCoord) +{ + if (util::ValueAt(tmpVec, tmpStrides, curCoord) == 1) + { + return; } + + ST value = util::ValueAt(srcVec, srcStrides, curCoord); + DT label = PositionLabel
(curCoord, dstStrides); + + LabelComponent(tmpVec, dstVec, srcVec, tmpStrides, dstStrides, srcStrides, shape, curCoord, value, label); } // Label each component with label in dstVec matching value in srcVec, marking labeled elements as 1 in tmpVec @@ -162,32 +222,9 @@ void Label(RawBufferType &dstVec, const RawBufferType &srcVec, const long4_16a & long4_16a tmpStrides{shape.y * shape.z * shape.w, shape.z * shape.w, shape.w, 1}; // For all elements in input tensor - for (long x = 0; x < shape.x; ++x) - { - for (long y = 0; y < shape.y; ++y) - { - for (long z = 0; z < shape.z; ++z) - { - for (long w = 0; w < shape.w; ++w) - { - long4_16a curCoord{x, y, z, w}; - - if (util::ValueAt(tmpVec, tmpStrides, curCoord) == 1) - { - continue; // The element was already labeled, skip it - } - - // Get current value from input tensor and set label as a 1D flattened (global) position - ST value = util::ValueAt(srcVec, srcStrides, curCoord); - DT label = y * dstStrides.y / sizeof(DT) + z * dstStrides.z / sizeof(DT) + w; - - // Recursively call to label component - LabelComponent(tmpVec, dstVec, srcVec, tmpStrides, dstStrides, srcStrides, shape, curCoord, value, - label); - } - } - } - } + ForEachCoord(shape, + [&tmpVec, &dstVec, &srcVec, &tmpStrides, &dstStrides, &srcStrides, &shape](const long4_16a &curCoord) + { LabelPixel(tmpVec, dstVec, srcVec, tmpStrides, dstStrides, srcStrides, shape, curCoord); }); } // Replace labels assigned to regions marked as background in source, and fix a potential region labeled with @@ -197,36 +234,25 @@ void ReplaceBgLabels(RawBufferType &dstVec, const RawBufferType &srcVec, const R const long4_16a &dstStrides, const long4_16a &srcStrides, const long1 &bglStrides, const long4_16a &shape) { - for (long x = 0; x < shape.x; ++x) - { - ST backgroundLabel = util::ValueAt(bglVec, bglStrides, long1{x}); - - for (long y = 0; y < shape.y; ++y) - { - for (long z = 0; z < shape.z; ++z) - { - for (long w = 0; w < shape.w; ++w) - { - long4_16a curCoord{x, y, z, w}; - - ST value = util::ValueAt(srcVec, srcStrides, curCoord); - DT label = util::ValueAt
(dstVec, dstStrides, curCoord); - - if (value == backgroundLabel) - { - // The current value is a background label, write it to output - util::ValueAt
(dstVec, dstStrides, curCoord) = (DT)backgroundLabel; - } - else if (label == (DT)backgroundLabel) - { - // If the label assigned happens to be the same as the background label, replace it by - // another label that is never assigned outside the possible offsets - util::ValueAt
(dstVec, dstStrides, curCoord) = dstStrides.x / sizeof(DT); - } - } - } - } - } + ForEachCoord(shape, + [&dstVec, &srcVec, &bglVec, &dstStrides, &srcStrides, &bglStrides](const long4_16a &curCoord) + { + ST backgroundLabel = util::ValueAt(bglVec, bglStrides, long1{curCoord.x}); + ST value = util::ValueAt(srcVec, srcStrides, curCoord); + DT label = util::ValueAt
(dstVec, dstStrides, curCoord); + + if (value == backgroundLabel) + { + // The current value is a background label, write it to output + util::ValueAt
(dstVec, dstStrides, curCoord) = static_cast
(backgroundLabel); + } + else if (label == (DT)backgroundLabel) + { + // If the label assigned happens to be the same as the background label, replace it by + // another label that is never assigned outside the possible offsets + util::ValueAt
(dstVec, dstStrides, curCoord) = static_cast
(dstStrides.x / sizeof(DT)); + } + }); } // Get the unique set of labels from output in dstVec, disregarding background labels @@ -236,28 +262,19 @@ void GetLabels(std::vector> &labels, const RawBufferType &dstVec, c { bool hasBgLabel = bglStrides.x > 0; - for (long x = 0; x < dstShape.x; ++x) - { - ST backgroundLabel = hasBgLabel ? util::ValueAt(bglVec, bglStrides, long1{x}) : 0; - - for (long y = 0; y < dstShape.y; ++y) - { - for (long z = 0; z < dstShape.z; ++z) - { - for (long w = 0; w < dstShape.w; ++w) - { - DT label = util::ValueAt
(dstVec, dstStrides, long4_16a{x, y, z, w}); + ForEachCoord(dstShape, + [&labels, &dstVec, &bglVec, &dstStrides, &bglStrides, hasBgLabel](const long4_16a &curCoord) + { + ST backgroundLabel = hasBgLabel ? util::ValueAt(bglVec, bglStrides, long1{curCoord.x}) : 0; + DT label = util::ValueAt
(dstVec, dstStrides, curCoord); - if (hasBgLabel && label == (DT)backgroundLabel) - { - continue; // ignore (do not get) background labels - } + if (hasBgLabel && label == static_cast
(backgroundLabel)) + { + return; // ignore (do not get) background labels + } - labels[x].insert(label); - } - } - } - } + labels[curCoord.x].insert(label); + }); } // Get the unique set of labels from statistics in staVec @@ -285,7 +302,7 @@ void CountLabels(RawBufferType &cntVec, const long1 &cntStrides, const std::vect { for (long x = 0; x < numSamples; ++x) { - util::ValueAt
(cntVec, cntStrides, long1{x}) = (DT)labels[x].size(); + util::ValueAt
(cntVec, cntStrides, long1{x}) = static_cast
(labels[x].size()); } } @@ -318,6 +335,84 @@ void SortStats(std::vector>> &stats, std::vector +bool FindLabelRegion(long ®ionIdx, DT &label, bool &usesPositionStats, const RawBufferType &dstVec, + const RawBufferType &bglVec, const long4_16a &dstStrides, const long1 &bglStrides, + const std::vector> &labels, const long4_16a &curCoord, DT endLabel, bool hasBgLabel) +{ + ST backgroundLabel = hasBgLabel ? util::ValueAt(bglVec, bglStrides, long1{curCoord.x}) : 0; + + label = util::ValueAt
(dstVec, dstStrides, curCoord); + auto fit = labels[curCoord.x].find(label); + if (fit == labels[curCoord.x].end()) + { + return false; + } + + DT posLabel = PositionLabel
(curCoord, dstStrides); + usesPositionStats + = (hasBgLabel && label == endLabel && posLabel == static_cast
(backgroundLabel)) || label == posLabel; + regionIdx = std::distance(labels[curCoord.x].cbegin(), fit); + + return true; +} + +template +bool IsInsideMask(const RawBufferType &mskVec, const long4_16a &mskStrides, long maskN, const long4_16a &curCoord) +{ + return util::ValueAt(mskVec, mskStrides, + long4_16a{maskN == 1 ? 0 : curCoord.x, curCoord.y, curCoord.z, curCoord.w}) + != 0; +} + +template +void InitializeRegionStats(std::vector
®ionStats, DT label, const long4_16a &curCoord, DT regionMark, + int numStats) +{ + regionStats.resize(numStats); + regionStats[0] = label; + regionStats[1] = static_cast
(curCoord.w); + regionStats[2] = static_cast
(curCoord.z); + + if (numStats == 7) + { + regionStats[3] = 1; + regionStats[4] = 1; + regionStats[5] = 1; + regionStats[6] = regionMark; + return; + } + + regionStats[3] = static_cast
(curCoord.y); + regionStats[4] = 1; + regionStats[5] = 1; + regionStats[6] = 1; + regionStats[7] = 1; + regionStats[8] = regionMark; +} + +template +void UpdateRegionStats(std::vector
®ionStats, const long4_16a &curCoord, int numStats) +{ + auto bboxAreaW = static_cast
(std::abs(static_cast(regionStats[1]) - curCoord.w) + 1); + auto bboxAreaH = static_cast
(std::abs(static_cast(regionStats[2]) - curCoord.z) + 1); + + if (numStats == 7) + { + regionStats[3] = std::max(regionStats[3], bboxAreaW); + regionStats[4] = std::max(regionStats[4], bboxAreaH); + regionStats[5] += 1; + return; + } + + auto bboxAreaD = static_cast
(std::abs(static_cast(regionStats[3]) - curCoord.y) + 1); + + regionStats[4] = std::max(regionStats[4], bboxAreaW); + regionStats[5] = std::max(regionStats[5], bboxAreaH); + regionStats[6] = std::max(regionStats[6], bboxAreaD); + regionStats[7] += 1; +} + // Compute statistics of labeled regions template void ComputeStats(std::vector>> &stats, const RawBufferType &dstVec, @@ -326,122 +421,74 @@ void ComputeStats(std::vector>> &stats, const RawBuf const long4_16a &shape, long maskN, int numStats) { // One-element-after-the-end label is a special label assigned to a region which got the background label - DT endLabel = dstStrides.x / sizeof(DT); + auto endLabel = static_cast
(dstStrides.x / sizeof(DT)); bool hasMask = mskStrides.x > 0; bool hasBgLabel = bglStrides.x > 0; for (long x = 0; x < shape.x; ++x) { - ST backgroundLabel = hasBgLabel ? util::ValueAt(bglVec, bglStrides, long1{x}) : 0; - stats[x].resize(labels[x].size()); + } - for (long y = 0; y < shape.y; ++y) + ForEachCoord(shape, + [&stats, &dstVec, &mskVec, &bglVec, &dstStrides, &mskStrides, &bglStrides, &labels, endLabel, hasMask, + hasBgLabel, maskN, numStats](const long4_16a &curCoord) + { + long regionIdx; + DT label; + bool usesPositionStats; + if (!FindLabelRegion(regionIdx, label, usesPositionStats, dstVec, bglVec, dstStrides, + bglStrides, labels, curCoord, endLabel, hasBgLabel)) + { + return; // this label is to be ignored + } + + if (!usesPositionStats) + { + return; + } + + DT regionMark = 0; // region has no marks + + // If has mask and the element is inside the mask + if (hasMask && IsInsideMask(mskVec, mskStrides, maskN, curCoord)) + { + regionMark = 2; // mark the region as inside the mask (= 2) + } + + InitializeRegionStats(stats[curCoord.x][regionIdx], label, curCoord, regionMark, numStats); + }); + + ForEachCoord( + shape, + [&stats, &dstVec, &mskVec, &bglVec, &dstStrides, &mskStrides, &bglStrides, &labels, endLabel, hasMask, + hasBgLabel, maskN, numStats](const long4_16a &curCoord) { - for (long z = 0; z < shape.z; ++z) + long regionIdx; + DT label; + bool usesPositionStats; + if (!FindLabelRegion(regionIdx, label, usesPositionStats, dstVec, bglVec, dstStrides, bglStrides, + labels, curCoord, endLabel, hasBgLabel)) { - for (long w = 0; w < shape.w; ++w) - { - DT label = util::ValueAt
(dstVec, dstStrides, long4_16a{x, y, z, w}); - auto fit = labels[x].find(label); // result of find iterator - if (fit == labels[x].end()) - { - continue; // this label is to be ignored - } - - DT posLabel = y * dstStrides.y / sizeof(DT) + z * dstStrides.z / sizeof(DT) + w; - - if ((hasBgLabel && label == endLabel && posLabel == (DT)backgroundLabel) || label == posLabel) - { - long regionIdx = std::distance(labels[x].cbegin(), fit); - DT regionMark = 0; // region has no marks - - // If has mask and the element is inside the mask - if (hasMask - && util::ValueAt(mskVec, mskStrides, long4_16a{maskN == 1 ? 0 : x, y, z, w}) != 0) - { - regionMark = 2; // mark the region as inside the mask (= 2) - } - - stats[x][regionIdx].resize(numStats); - stats[x][regionIdx][0] = label; - stats[x][regionIdx][1] = w; - stats[x][regionIdx][2] = z; - - if (numStats == 7) - { - stats[x][regionIdx][3] = 1; - stats[x][regionIdx][4] = 1; - stats[x][regionIdx][5] = 1; - stats[x][regionIdx][6] = regionMark; - } - else - { - stats[x][regionIdx][3] = y; - stats[x][regionIdx][4] = 1; - stats[x][regionIdx][5] = 1; - stats[x][regionIdx][6] = 1; - stats[x][regionIdx][7] = 1; - stats[x][regionIdx][8] = regionMark; - } - } - } + return; } - } - for (long y = 0; y < shape.y; ++y) - { - for (long z = 0; z < shape.z; ++z) + + if (usesPositionStats) { - for (long w = 0; w < shape.w; ++w) - { - DT label = util::ValueAt
(dstVec, dstStrides, long4_16a{x, y, z, w}); - auto fit = labels[x].find(label); - if (fit == labels[x].end()) - { - continue; - } - - DT posLabel = y * dstStrides.y / sizeof(DT) + z * dstStrides.z / sizeof(DT) + w; - - if ((hasBgLabel && label == endLabel && posLabel == (DT)backgroundLabel) || label == posLabel) - { - continue; // statistics for this element was already computed - } - - long regionIdx = std::distance(labels[x].cbegin(), fit); - DT bboxAreaW = std::abs(stats[x][regionIdx][1] - w) + 1; - DT bboxAreaH = std::abs(stats[x][regionIdx][2] - z) + 1; - - // If has mask and the region has no marks (it is no marked as inside mask) - if (hasMask && stats[x][regionIdx][numStats - 1] == 0) - { - // If element is inside mask - if (util::ValueAt(mskVec, mskStrides, long4_16a{maskN == 1 ? 0 : x, y, z, w}) != 0) - { - stats[x][regionIdx][numStats - 1] = 2; // mark the region as inside mask (= 2) - } - } - - if (numStats == 7) - { - stats[x][regionIdx][3] = std::max(stats[x][regionIdx][3], bboxAreaW); - stats[x][regionIdx][4] = std::max(stats[x][regionIdx][4], bboxAreaH); - stats[x][regionIdx][5] += 1; - } - else - { - DT bboxAreaD = std::abs(stats[x][regionIdx][3] - y) + 1; - - stats[x][regionIdx][4] = std::max(stats[x][regionIdx][4], bboxAreaW); - stats[x][regionIdx][5] = std::max(stats[x][regionIdx][5], bboxAreaH); - stats[x][regionIdx][6] = std::max(stats[x][regionIdx][6], bboxAreaD); - stats[x][regionIdx][7] += 1; - } - } + return; // statistics for this element was already computed } - } - } + + std::vector
®ionStats = stats[curCoord.x][regionIdx]; + + // If has mask and the region has no marks (it is no marked as inside mask) + if (hasMask && regionStats[numStats - 1] == 0 && IsInsideMask(mskVec, mskStrides, maskN, curCoord)) + { + regionStats[numStats - 1] = 2; // mark the region as inside mask (= 2) + } + + UpdateRegionStats(regionStats, curCoord, numStats); + }); } // Remove islands (regions with less than minimum size in mszVec) from dstVec based on statistics @@ -451,41 +498,31 @@ void RemoveIslands(std::vector> &labels, RawBufferType &dstVec, con const long1 &mszStrides, std::vector>> &stats, const long4_16a &shape, int numStats) { - for (long x = 0; x < shape.x; ++x) - { - ST backgroundLabel = util::ValueAt(bglVec, bglStrides, long1{x}); - DT minSize = util::ValueAt
(mszVec, mszStrides, long1{x}); - - for (long y = 0; y < shape.y; ++y) - { - for (long z = 0; z < shape.z; ++z) - { - for (long w = 0; w < shape.w; ++w) - { - long4_16a curCoord{x, y, z, w}; - - DT label = util::ValueAt
(dstVec, dstStrides, curCoord); - auto fit = labels[x].find(label); // result of find iterator - if (fit == labels[x].end()) - { - continue; // this label is to be ignored - } - - long regionIdx = std::distance(labels[x].cbegin(), fit); - DT regionSize = stats[x][regionIdx][numStats - 2]; - - // If region size is smaller than minimum size (it is an island) and the region is not marked - // as inside the mask (= 2), then remove the island and mark it as removed - if (regionSize < minSize && stats[x][regionIdx][numStats - 1] != 2) - { - util::ValueAt
(dstVec, dstStrides, curCoord) = backgroundLabel; - - stats[x][regionIdx][numStats - 1] = 1; - } - } - } - } - } + ForEachCoord(shape, + [&labels, &dstVec, &bglVec, &mszVec, &dstStrides, &bglStrides, &mszStrides, &stats, + numStats](const long4_16a &curCoord) + { + ST backgroundLabel = util::ValueAt(bglVec, bglStrides, long1{curCoord.x}); + DT minSize = util::ValueAt
(mszVec, mszStrides, long1{curCoord.x}); + DT label = util::ValueAt
(dstVec, dstStrides, curCoord); + auto fit = labels[curCoord.x].find(label); // result of find iterator + if (fit == labels[curCoord.x].end()) + { + return; // this label is to be ignored + } + + long regionIdx = std::distance(labels[curCoord.x].cbegin(), fit); + DT regionSize = stats[curCoord.x][regionIdx][numStats - 2]; + + // If region size is smaller than minimum size (it is an island) and the region is not marked + // as inside the mask (= 2), then remove the island and mark it as removed + if (regionSize < minSize && stats[curCoord.x][regionIdx][numStats - 1] != 2) + { + util::ValueAt
(dstVec, dstStrides, curCoord) = backgroundLabel; + + stats[curCoord.x][regionIdx][numStats - 1] = 1; + } + }); } // Relabel replaces index-based labels by consecutive region indices @@ -507,30 +544,25 @@ void Relabel(RawBufferType &dstVec, const RawBufferType &bglVec, const RawBuffer DT origLabel = util::ValueAt
(staVec, staStrides, long3{x, y, 0}); origLabelToRegionIdx.insert({origLabel, y}); } - for (long y = 0; y < shape.y; ++y) - { - for (long z = 0; z < shape.z; ++z) - { - for (long w = 0; w < shape.w; ++w) - { - DT label = util::ValueAt
(dstVec, dstStrides, long4_16a{x, y, z, w}); + ForEachSampleCoord(shape, x, + [&dstVec, &dstStrides, &origLabelToRegionIdx, backgroundLabel](const long4_16a &curCoord) + { + DT label = util::ValueAt
(dstVec, dstStrides, curCoord); - if (label == (DT)backgroundLabel) - { - continue; - } + if (label == (DT)backgroundLabel) + { + return; + } - DT regionIdx = origLabelToRegionIdx[label]; + DT regionIdx = origLabelToRegionIdx[label]; - if (regionIdx >= (DT)backgroundLabel) - { - regionIdx += 1; // increment region indices to skip background labels - } + if (regionIdx >= (DT)backgroundLabel) + { + regionIdx += 1; // increment region indices to skip background labels + } - util::ValueAt
(dstVec, dstStrides, long4_16a{x, y, z, w}) = regionIdx; - } - } - } + util::ValueAt
(dstVec, dstStrides, curCoord) = regionIdx; + }); } } @@ -560,6 +592,7 @@ NVCV_TYPED_TEST_SUITE(OpLabel, type::Types< NVCV_TEST_ROW(NVCV_SHAPE(40, 17, 1, 1), NVCV_DATA_TYPE_U8, uint8_t, true, true, true, 4, true), NVCV_TEST_ROW(NVCV_SHAPE(28, 73, 1, 3), NVCV_DATA_TYPE_U16, uint16_t, true, true, true, 3, true), NVCV_TEST_ROW(NVCV_SHAPE(19, 61, 1, 3), NVCV_DATA_TYPE_U16, uint16_t, true, true, true, 4, true), + NVCV_TEST_ROW(NVCV_SHAPE(35, 19, 1, 2), NVCV_DATA_TYPE_U32, uint32_t, false, false, false, 0, false), NVCV_TEST_ROW(NVCV_SHAPE(23, 21, 12, 1), NVCV_DATA_TYPE_U32, uint32_t, false, false, false, 0, false), NVCV_TEST_ROW(NVCV_SHAPE(33, 41, 22, 1), NVCV_DATA_TYPE_U32, uint32_t, false, false, false, 1, false), NVCV_TEST_ROW(NVCV_SHAPE(25, 38, 13, 2), NVCV_DATA_TYPE_S8, int8_t, true, false, false, 2, false), @@ -570,7 +603,12 @@ NVCV_TYPED_TEST_SUITE(OpLabel, type::Types< NVCV_TEST_ROW(NVCV_SHAPE(17, 29, 5, 2), NVCV_DATA_TYPE_U8, uint8_t, true, true, true, 2, false), NVCV_TEST_ROW(NVCV_SHAPE(16, 28, 4, 3), NVCV_DATA_TYPE_U8, uint8_t, true, true, true, 3, true), NVCV_TEST_ROW(NVCV_SHAPE(17, 27, 5, 2), NVCV_DATA_TYPE_U8, uint8_t, true, true, true, 4, true), - NVCV_TEST_ROW(NVCV_SHAPE(40, 17, 5, 2), NVCV_DATA_TYPE_U8, uint8_t, true, true, true, 4, true) + NVCV_TEST_ROW(NVCV_SHAPE(40, 17, 5, 2), NVCV_DATA_TYPE_U8, uint8_t, true, true, true, 4, true), + // Widths above one CUDA block exercise the X-reduction threshold paths for both 2D and 3D inputs. + NVCV_TEST_ROW(NVCV_SHAPE(48, 17, 1, 1), NVCV_DATA_TYPE_U8, uint8_t, false, true, false, 0, false), + NVCV_TEST_ROW(NVCV_SHAPE(49, 17, 1, 1), NVCV_DATA_TYPE_U8, uint8_t, false, false, true, 0, false), + NVCV_TEST_ROW(NVCV_SHAPE(48, 17, 5, 1), NVCV_DATA_TYPE_U8, uint8_t, false, true, false, 0, false), + NVCV_TEST_ROW(NVCV_SHAPE(49, 17, 5, 1), NVCV_DATA_TYPE_U8, uint8_t, false, false, true, 0, false) >); // clang-format on @@ -602,10 +640,25 @@ TYPED_TEST(OpLabel, correct_output) // labels (bgl), minimum threshold (min), maximum threshold (max), minimum size for islands removal (msz), // count of labeled regions (count) and statistics computed per labeled region (sta) - nvcv::Tensor srcTensor, dstTensor, bglTensor, minTensor, maxTensor, mszTensor, cntTensor, staTensor, mskTensor; + nvcv::Tensor srcTensor; + nvcv::Tensor dstTensor; + nvcv::Tensor bglTensor; + nvcv::Tensor minTensor; + nvcv::Tensor maxTensor; + nvcv::Tensor mszTensor; + nvcv::Tensor cntTensor; + nvcv::Tensor staTensor; + nvcv::Tensor mskTensor; - nvcv::Optional srcData, dstData, bglData, minData, maxData, mszData, cntData, staData, - mskData; + nvcv::Optional srcData; + nvcv::Optional dstData; + nvcv::Optional bglData; + nvcv::Optional minData; + nvcv::Optional maxData; + nvcv::Optional mszData; + nvcv::Optional cntData; + nvcv::Optional staData; + nvcv::Optional mskData; NVCVConnectivityType connectivity = (shape.z == 1) ? NVCV_CONNECTIVITY_4_2D : NVCV_CONNECTIVITY_6_3D; NVCVLabelType assignLabels = doRelabel ? NVCV_LABEL_SEQUENTIAL : NVCV_LABEL_FAST; @@ -711,12 +764,12 @@ TYPED_TEST(OpLabel, correct_output) long4_16a srcStrides{0, 0, srcData->stride(ids.z), srcData->stride(ids.w)}; long4_16a dstStrides{0, 0, dstData->stride(ids.z), dstData->stride(ids.w)}; - long1 bglStrides{(bglTensor) ? bglData->stride(0) : 0}; - long1 minStrides{(minTensor) ? minData->stride(0) : 0}; - long1 maxStrides{(maxTensor) ? maxData->stride(0) : 0}; - long1 mszStrides{(mszTensor) ? mszData->stride(0) : 0}; - long1 cntStrides{(cntTensor) ? cntData->stride(0) : 0}; - long3 staStrides = (staTensor) ? long3{staData->stride(0), staData->stride(1), staData->stride(2)} : long3{0, 0, 0}; + long1 bglStrides{bglTensor ? bglData->stride(0) : 0}; + long1 minStrides{minTensor ? minData->stride(0) : 0}; + long1 maxStrides{maxTensor ? maxData->stride(0) : 0}; + long1 mszStrides{mszTensor ? mszData->stride(0) : 0}; + long1 cntStrides{cntTensor ? cntData->stride(0) : 0}; + long3 staStrides = staTensor ? long3{staData->stride(0), staData->stride(1), staData->stride(2)} : long3{0, 0, 0}; long4_16a mskStrides{0, 0, 0, 0}; if (mskTensor) @@ -762,11 +815,11 @@ TYPED_TEST(OpLabel, correct_output) // clang-format off - for (long x = 0; x < srcShape.x; ++x) - for (long y = 0; y < srcShape.y; ++y) - for (long z = 0; z < srcShape.z; ++z) - for (long w = 0; w < srcShape.w; ++w) - util::ValueAt(srcVec, srcStrides, long4_16a{x, y, z, w}) = srcRandom(rng); + ref::ForEachCoord(srcShape, + [&srcVec, &srcStrides, &srcRandom, &rng](const long4_16a &curCoord) + { + util::ValueAt(srcVec, srcStrides, curCoord) = srcRandom(rng); + }); ASSERT_EQ(cudaSuccess, cudaMemcpy(srcData->basePtr(), srcVec.data(), srcBufSize, cudaMemcpyHostToDevice)); @@ -800,11 +853,11 @@ TYPED_TEST(OpLabel, correct_output) } if (mskTensor) { - for (long x = 0; x < mskShape.x; ++x) - for (long y = 0; y < mskShape.y; ++y) - for (long z = 0; z < mskShape.z; ++z) - for (long w = 0; w < mskShape.w; ++w) - util::ValueAt(mskVec, mskStrides, long4_16a{x, y, z, w}) = mskRandom(rng); + ref::ForEachCoord(mskShape, + [&mskVec, &mskStrides, &mskRandom, &rng](const long4_16a &curCoord) + { + util::ValueAt(mskVec, mskStrides, curCoord) = mskRandom(rng); + }); ASSERT_EQ(cudaSuccess, cudaMemcpy(mskData->basePtr(), mskVec.data(), mskBufSize, cudaMemcpyHostToDevice)); } @@ -877,7 +930,7 @@ TYPED_TEST(OpLabel, correct_output) ASSERT_EQ(cudaSuccess, cudaMemcpy(staTestVec.data(), staData->basePtr(), staBufSize, cudaMemcpyDeviceToHost)); ref::ComputeStats(goldStats, labGoldVec, mskVec, bglVec, dstStrides, mskStrides, bglStrides, - goldLabels, srcShape, maskN, staShape.z); + goldLabels, srcShape, maskN, static_cast(staShape.z)); ref::GetLabels(testLabels, cntTestVec, staTestVec, cntStrides, staStrides, srcShape.x); } @@ -891,7 +944,7 @@ TYPED_TEST(OpLabel, correct_output) if (mszTensor) { ref::RemoveIslands(goldLabels, labGoldVec, bglVec, mszVec, dstStrides, bglStrides, mszStrides, - goldStats, srcShape, staShape.z); + goldStats, srcShape, static_cast(staShape.z)); } if (doRelabel) @@ -910,9 +963,169 @@ TYPED_TEST(OpLabel, correct_output) EXPECT_EQ(labTestVec, labGoldVec); } -class OpLabel_Negative : public ::testing::Test +template +void UploadVectorTensor(const nvcv::Tensor &tensor, const std::vector &values) +{ + auto data = tensor.exportData(); + ASSERT_TRUE(data); + ASSERT_EQ(static_cast(values.size()), data->shape(0)); + ASSERT_EQ(cudaSuccess, + cudaMemcpy(data->basePtr(), values.data(), values.size() * sizeof(T), cudaMemcpyHostToDevice)); +} + +template +void DownloadVectorTensor(const nvcv::Tensor &tensor, std::vector &values) +{ + auto data = tensor.exportData(); + ASSERT_TRUE(data); + values.resize(data->shape(0)); + ASSERT_EQ(cudaSuccess, + cudaMemcpy(values.data(), data->basePtr(), values.size() * sizeof(T), cudaMemcpyDeviceToHost)); +} + +template +::testing::AssertionResult DownloadImageTensor(const nvcv::Tensor &tensor, int numSamples, std::vector &values) +{ + values.clear(); + int sample = 0; + try + { + for (; sample < numSamples; ++sample) + { + std::vector sampleValues; + util::GetImageVectorFromTensor(tensor.exportData(), sample, sampleValues); + values.insert(values.end(), sampleValues.begin(), sampleValues.end()); + } + } + catch (const util::TensorDataUtilsError &e) + { + return ::testing::AssertionFailure() << "sample " << sample << ": " << e.what(); + } + return ::testing::AssertionSuccess(); +} + +static std::vector MakeLabelParityInput(int width, int height) +{ + std::vector values(width * height); + for (size_t i = 0; i < values.size(); ++i) + { + values[i] = static_cast((i * 7 + 13) % 7); + } + return values; +} + +static ::testing::AssertionResult UploadLabelParityInputs(nvcv::Tensor &interleaved, nvcv::Tensor &planar, + std::vector &values, int numSamples) +{ + int sample = 0; + const char *tensorName = "interleaved"; + try + { + for (; sample < numSamples; ++sample) + { + tensorName = "interleaved"; + util::SetImageTensorFromVector(interleaved.exportData(), values, sample); + + tensorName = "planar"; + util::SetImageTensorFromVector(planar.exportData(), values, sample); + } + } + catch (const util::TensorDataUtilsError &e) + { + return ::testing::AssertionFailure() << tensorName << " sample " << sample << ": " << e.what(); + } + return ::testing::AssertionSuccess(); +} + +static nvcv::TensorShape MakeLabelTensorShape(int numSamples, int width, int height, const std::string &layout) +{ + if (layout == "HWC" || layout == "CHW") + { + return layout == "HWC" ? nvcv::TensorShape{{height, width, 1}, layout.c_str()} + : nvcv::TensorShape{{1, height, width}, layout.c_str()}; + } + + return layout == "NHWC" ? nvcv::TensorShape{{numSamples, height, width, 1}, layout.c_str()} + : nvcv::TensorShape{{numSamples, 1, height, width}, layout.c_str()}; +} + +static void RunLabelPlanarParityCase(int numSamples, int width, int height, const std::string &interleavedLayout, + const std::string &planarLayout) +{ + nvcv::TensorShape interleavedShape = MakeLabelTensorShape(numSamples, width, height, interleavedLayout); + nvcv::TensorShape planarShape = MakeLabelTensorShape(numSamples, width, height, planarLayout); + + nvcv::Tensor srcInterleaved(interleavedShape, nvcv::TYPE_U8); + nvcv::Tensor dstInterleaved(interleavedShape, nvcv::TYPE_U32); + nvcv::Tensor srcPlanar(planarShape, nvcv::TYPE_U8); + nvcv::Tensor dstPlanar(planarShape, nvcv::TYPE_U32); + + nvcv::Tensor bgInterleaved({{numSamples}, "N"}, nvcv::TYPE_U8); + nvcv::Tensor minInterleaved({{numSamples}, "N"}, nvcv::TYPE_U8); + nvcv::Tensor maxInterleaved({{numSamples}, "N"}, nvcv::TYPE_U8); + nvcv::Tensor countInterleaved({{numSamples}, "N"}, nvcv::TYPE_U32); + nvcv::Tensor bgPlanar({{numSamples}, "N"}, nvcv::TYPE_U8); + nvcv::Tensor minPlanar({{numSamples}, "N"}, nvcv::TYPE_U8); + nvcv::Tensor maxPlanar({{numSamples}, "N"}, nvcv::TYPE_U8); + nvcv::Tensor countPlanar({{numSamples}, "N"}, nvcv::TYPE_U32); + + auto srcValues = MakeLabelParityInput(width, height); + ASSERT_TRUE(UploadLabelParityInputs(srcInterleaved, srcPlanar, srcValues, numSamples)); + + std::vector bgValues(numSamples, 0); + std::vector minValues(numSamples, 1); + std::vector maxValues(numSamples, 5); + UploadVectorTensor(bgInterleaved, bgValues); + UploadVectorTensor(bgPlanar, bgValues); + UploadVectorTensor(minInterleaved, minValues); + UploadVectorTensor(minPlanar, minValues); + UploadVectorTensor(maxInterleaved, maxValues); + UploadVectorTensor(maxPlanar, maxValues); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + cvcuda::Label op; + ASSERT_NO_THROW(op(stream, srcInterleaved, dstInterleaved, bgInterleaved, minInterleaved, maxInterleaved, + nvcv::Tensor{nullptr}, countInterleaved, nvcv::Tensor{nullptr}, nvcv::Tensor{nullptr}, + NVCV_CONNECTIVITY_4_2D, NVCV_LABEL_FAST, NVCV_REMOVE_ISLANDS_OUTSIDE_MASK_ONLY)); + ASSERT_NO_THROW(op(stream, srcPlanar, dstPlanar, bgPlanar, minPlanar, maxPlanar, nvcv::Tensor{nullptr}, countPlanar, + nvcv::Tensor{nullptr}, nvcv::Tensor{nullptr}, NVCV_CONNECTIVITY_4_2D, NVCV_LABEL_FAST, + NVCV_REMOVE_ISLANDS_OUTSIDE_MASK_ONLY)); + + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + + std::vector dstInterleavedValues; + std::vector dstPlanarValues; + ASSERT_TRUE(DownloadImageTensor(dstInterleaved, numSamples, dstInterleavedValues)); + ASSERT_TRUE(DownloadImageTensor(dstPlanar, numSamples, dstPlanarValues)); + EXPECT_EQ(dstInterleavedValues, dstPlanarValues); + + std::vector countInterleavedValues; + std::vector countPlanarValues; + DownloadVectorTensor(countInterleaved, countInterleavedValues); + DownloadVectorTensor(countPlanar, countPlanarValues); + EXPECT_EQ(countInterleavedValues, countPlanarValues); +} + +// clang-format off +NVCV_TEST_SUITE_P(OpLabelPlanar, test::ValueList{ + // samples, width, height, interleavedLayout, planarLayout + {1, 19, 17, "HWC", "CHW"}, + {2, 23, 13, "NHWC", "NCHW"}, +}); + +// clang-format on + +TEST_P(OpLabelPlanar, tensor_matches_interleaved) +{ + RunLabelPlanarParityCase(GetParamValue<0>(), GetParamValue<1>(), GetParamValue<2>(), GetParamValue<3>(), + GetParamValue<4>()); +} + +struct OpLabel_Negative : public ::testing::Test // NOSONAR: negative tests keep shared invalid fixtures together. { -protected: void SetUp() override { shape = {33, 16, 1, 1}; @@ -951,22 +1164,25 @@ class OpLabel_Negative : public ::testing::Test ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); } - void runOpLabelNegativeTest(nvcv::Tensor srcTensor, nvcv::Tensor dstTensor, nvcv::Tensor bglTensor, - nvcv::Tensor minTensor, nvcv::Tensor maxTensor, nvcv::Tensor mszTensor, - nvcv::Tensor cntTensor, nvcv::Tensor staTensor, nvcv::Tensor mskTensor, - NVCVConnectivityType connectivity, NVCVLabelType assignLabels, - NVCVLabelMaskType maskType) + void runOpLabelNegativeTest(const nvcv::Tensor &testSrcTensor, const nvcv::Tensor &testDstTensor, + const nvcv::Tensor &testBglTensor, const nvcv::Tensor &testMinTensor, + const nvcv::Tensor &testMaxTensor, const nvcv::Tensor &testMszTensor, + const nvcv::Tensor &testCntTensor, const nvcv::Tensor &testStaTensor, + const nvcv::Tensor &testMskTensor, NVCVConnectivityType testConnectivity, + NVCVLabelType testAssignLabels, NVCVLabelMaskType testMaskType) { - EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall( - [&] - { - op(stream, srcTensor, dstTensor, bglTensor, minTensor, maxTensor, - mszTensor, cntTensor, staTensor, mskTensor, connectivity, - assignLabels, maskType); - })); - char msg[1024]; - nvcvGetLastErrorMessage(msg, sizeof(msg)); - std::cout << "\033[33m" << msg << "\033[0m" << std::endl; + EXPECT_EQ( + NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall( + [this, &testSrcTensor, &testDstTensor, &testBglTensor, &testMinTensor, &testMaxTensor, &testMszTensor, + &testCntTensor, &testStaTensor, &testMskTensor, &testConnectivity, &testAssignLabels, &testMaskType] + { + op(stream, testSrcTensor, testDstTensor, testBglTensor, testMinTensor, testMaxTensor, testMszTensor, + testCntTensor, testStaTensor, testMskTensor, testConnectivity, testAssignLabels, testMaskType); + })); + std::array msg; + nvcvGetLastErrorMessage(msg.data(), msg.size()); + std::cout << "\033[33m" << msg.data() << "\033[0m" << std::endl; } int4 shape; @@ -1001,7 +1217,7 @@ class OpLabel_Negative : public ::testing::Test // clang-format off TEST_F(OpLabel_Negative, InvalidSourceLayout) { - nvcv::Tensor srcTensorInvalidLayout({{1, shape.y, shape.x},"CHW"},srcDT); + nvcv::Tensor srcTensorInvalidLayout({{1, shape.y, shape.x},"WHC"},srcDT); runOpLabelNegativeTest(srcTensorInvalidLayout, dstTensor, bglTensor, minTensor, maxTensor, mszTensor, cntTensor, staTensor, mskTensor, connectivity, assignLabels, maskType); } @@ -1100,13 +1316,13 @@ TEST_F(OpLabel_Negative, InvalidCountDataType) TEST_F(OpLabel_Negative, StatusWithoutCount) { - runOpLabelNegativeTest(srcTensor, dstTensor, bglTensor, minTensor, maxTensor, mszTensor, nullptr, staTensor, + runOpLabelNegativeTest(srcTensor, dstTensor, bglTensor, minTensor, maxTensor, mszTensor, nvcv::Tensor{nullptr}, staTensor, mskTensor, connectivity, assignLabels, maskType); } TEST_F(OpLabel_Negative, reLabelWithoutStatus) { - runOpLabelNegativeTest(srcTensor, dstTensor, bglTensor, minTensor, maxTensor, mszTensor, cntTensor, nullptr, + runOpLabelNegativeTest(srcTensor, dstTensor, bglTensor, minTensor, maxTensor, mszTensor, cntTensor, nvcv::Tensor{nullptr}, mskTensor, connectivity, assignLabels, maskType); } @@ -1126,7 +1342,7 @@ TEST_F(OpLabel_Negative, InvalidStatsDataType) TEST_F(OpLabel_Negative, minSizeWithoutBgLabel) { - runOpLabelNegativeTest(srcTensor, dstTensor, nullptr, minTensor, maxTensor, mszTensor, cntTensor, staTensor, + runOpLabelNegativeTest(srcTensor, dstTensor, nvcv::Tensor{nullptr}, minTensor, maxTensor, mszTensor, cntTensor, staTensor, mskTensor, connectivity, assignLabels, maskType); } @@ -1146,7 +1362,7 @@ TEST_F(OpLabel_Negative, InvalidMinSizeDataType) TEST_F(OpLabel_Negative, MaskWithoutMinSize) { - runOpLabelNegativeTest(srcTensor, dstTensor, bglTensor, minTensor, maxTensor, nullptr, cntTensor, staTensor, + runOpLabelNegativeTest(srcTensor, dstTensor, bglTensor, minTensor, maxTensor, nvcv::Tensor{nullptr}, cntTensor, staTensor, mskTensor, connectivity, assignLabels, maskType); } diff --git a/tests/cvcuda/system/TestOpLaplacian.cpp b/tests/cvcuda/system/TestOpLaplacian.cpp index b1135afac..07062b04e 100644 --- a/tests/cvcuda/system/TestOpLaplacian.cpp +++ b/tests/cvcuda/system/TestOpLaplacian.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "ConvUtils.hpp" #include "Definitions.hpp" +#include "PlanarParityUtils.hpp" #include #include @@ -27,13 +28,36 @@ #include #include +#include #include namespace test = nvcv::test; namespace cuda = nvcv::cuda; -static const float kLaplacianKernel1[] = {0.0f, 1.0f, 0.0f, 1.0f, -4.0f, 1.0f, 0.0f, 1.0f, 0.0f}; -static const float kLaplacianKernel3[] = {2.0f, 0.0f, 2.0f, 0.0f, -8.0f, 0.0f, 2.0f, 0.0f, 2.0f}; +namespace { + +// Negative cases intentionally vary parameter-tensor lengths to exercise validation. +inline void InvokeLaplacianVarShapeNegative(cudaStream_t stream, const nvcv::ImageBatchVarShape &src, + const nvcv::ImageBatchVarShape &dst, int maxBatches, + NVCVBorderType borderMode) +{ + const int ksize = 3; + const int numParams = maxBatches < 0 ? 0 : maxBatches; + auto ksizeTensor = test::planar::MakePerImageTensor(numParams, nvcv::TYPE_S32, ksize); + auto scaleTensor = test::planar::MakePerImageTensor(numParams, nvcv::TYPE_F32, 1.0f); + cvcuda::Laplacian op; + op(stream, src, dst, ksizeTensor, scaleTensor, borderMode); +} + +} // namespace + +static int ScaledSize(int size, double scale) +{ + return static_cast(size * scale); +} + +static constexpr std::array kLaplacianKernel1 = {0.0f, 1.0f, 0.0f, 1.0f, -4.0f, 1.0f, 0.0f, 1.0f, 0.0f}; +static constexpr std::array kLaplacianKernel3 = {2.0f, 0.0f, 2.0f, 0.0f, -8.0f, 0.0f, 2.0f, 0.0f, 2.0f}; // clang-format off @@ -103,7 +127,7 @@ TEST_P(OpLaplacian, correct_output) std::default_random_engine randEng(0); std::uniform_int_distribution rand(0u, 255u); - std::generate(inVec.begin(), inVec.end(), [&]() { return rand(randEng); }); + std::ranges::generate(inVec, [&rand, &randEng]() { return rand(randEng); }); // copy random input to device ASSERT_EQ(cudaSuccess, cudaMemcpy(inData->basePtr(), inVec.data(), inBufSize, cudaMemcpyHostToDevice)); @@ -165,9 +189,9 @@ TEST_P(OpLaplacian, varshape_correct_output) float4 borderValue = cuda::SetAll(0); // Create input varshape - std::default_random_engine rng; - std::uniform_int_distribution udistWidth(width * 0.8, width * 1.1); - std::uniform_int_distribution udistHeight(height * 0.8, height * 1.1); + std::default_random_engine rng; + std::uniform_int_distribution udistWidth(ScaledSize(width, 0.8), ScaledSize(width, 1.1)); + std::uniform_int_distribution udistHeight(ScaledSize(height, 0.8), ScaledSize(height, 1.1)); std::vector imgSrc; @@ -184,7 +208,7 @@ TEST_P(OpLaplacian, varshape_correct_output) std::uniform_int_distribution udist(0, 255); srcVec[i].resize(imgSrc[i].size().h * srcRowStride); - std::generate(srcVec[i].begin(), srcVec[i].end(), [&]() { return udist(rng); }); + std::ranges::generate(srcVec[i], [&udist, &rng]() { return udist(rng); }); auto imgData = imgSrc[i].exportData(); ASSERT_NE(imgData, nvcv::NullOpt); @@ -267,15 +291,15 @@ TEST_P(OpLaplacian, varshape_correct_output) nvcv::Size2D kernelSize{3, 3}; int2 kernelAnchor{kernelSize.w / 2, kernelSize.h / 2}; - for (int i = 0; i < 9; ++i) + for (int kernelIndex = 0; kernelIndex < 9; ++kernelIndex) { if (ksize == 1) { - kernel[i] = kLaplacianKernel1[i] * scale; + kernel[kernelIndex] = kLaplacianKernel1[kernelIndex] * scale; } else if (ksize == 3) { - kernel[i] = kLaplacianKernel3[i] * scale; + kernel[kernelIndex] = kLaplacianKernel3[kernelIndex] * scale; } } @@ -288,20 +312,85 @@ TEST_P(OpLaplacian, varshape_correct_output) } } +// Laplacian filters each channel independently, so a planar input is filtered plane-by-plane and +// must produce exactly the same pixels as the interleaved path. These tests feed identical data +// through cvcuda::Laplacian in both layouts and require the re-interleaved planar output to match +// the interleaved output bit-for-bit. +// ============================================================================= + +// Parameters: width, height, ksize, scale, borderMode, numImages, planarFmt, interleavedFmt // clang-format off -NVCV_TEST_SUITE_P(OpLaplacian_Negative, nvcv::test::ValueList{ - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U8, 7, 0.5, NVCV_BORDER_CONSTANT}, // invalid kernel size - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U16, 3, 0.5, NVCV_BORDER_CONSTANT}, // data type is different - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_RGB8, nvcv::FMT_RGB8p, 3, 0.5, NVCV_BORDER_CONSTANT}, // data format is different - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_RGB8p, nvcv::FMT_RGB8p, 3, 0.5, NVCV_BORDER_CONSTANT}, // data format is not kNHWC/kHWC -#ifndef ENABLE_SANITIZER - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U8, 3, 0.5, static_cast(255)}, // invalid borderType -#endif - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_F16, nvcv::FMT_F16, 3, 0.5, NVCV_BORDER_CONSTANT}, // invalid data type +NVCV_TEST_SUITE_P(OpLaplacianPlanar, + test::ValueList{ + { 64, 48, 1, 1.0f, NVCV_BORDER_CONSTANT, 2, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + { 67, 51, 3, 2.0f, NVCV_BORDER_REFLECT, 1, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + { 50, 40, 1, 1.0f, NVCV_BORDER_REPLICATE, 2, nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8}, + { 64, 48, 3, 1.0f, NVCV_BORDER_REFLECT101, 1, nvcv::FMT_RGBf32p, nvcv::FMT_RGBf32}, + { 32, 28, 1, 3.0f, NVCV_BORDER_WRAP, 1, nvcv::FMT_RGBAf32p, nvcv::FMT_RGBAf32}, + { 40, 33, 3, 1.0f, NVCV_BORDER_CONSTANT, 2, nvcv::FMT_RGBf32p, nvcv::FMT_RGBf32}, }); // clang-format on +TEST_P(OpLaplacianPlanar, tensor_matches_interleaved) +{ + int ksize = GetParamValue<2>(); + float scale = GetParamValue<3>(); + NVCVBorderType borderMode = GetParamValue<4>(); + int numImages = GetParamValue<5>(); + + test::planar::RunTensorParity( + GetParamValue<6>(), GetParamValue<7>(), GetParamValue<0>(), GetParamValue<1>(), GetParamValue<0>(), + GetParamValue<1>(), numImages, + [ksize, scale, borderMode](cudaStream_t s, const nvcv::Tensor &src, const nvcv::Tensor &dst, nvcv::ImageFormat) + { + cvcuda::Laplacian op; + EXPECT_NO_THROW(op(s, src, dst, ksize, scale, borderMode)); + }); +} + +TEST_P(OpLaplacianPlanar, varshape_matches_interleaved) +{ + int ksize = GetParamValue<2>(); + float scale = GetParamValue<3>(); + NVCVBorderType borderMode = GetParamValue<4>(); + int numImages = GetParamValue<5>(); + + auto ksizeTensor = test::planar::MakePerImageTensor(numImages, nvcv::TYPE_S32, ksize); + auto scaleTensor = test::planar::MakePerImageTensor(numImages, nvcv::TYPE_F32, scale); + + test::planar::RunVarShapeParity( + GetParamValue<6>(), GetParamValue<7>(), GetParamValue<0>(), GetParamValue<1>(), GetParamValue<0>(), + GetParamValue<1>(), numImages, + [&ksizeTensor, &scaleTensor, borderMode](cudaStream_t s, const nvcv::ImageBatchVarShape &src, + const nvcv::ImageBatchVarShape &dst, nvcv::ImageFormat) + { + cvcuda::Laplacian op; + EXPECT_NO_THROW(op(s, src, dst, ksizeTensor, scaleTensor, borderMode)); + }); +} + +static auto OpLaplacianNegativeParams() +{ + nvcv::test::ValueList params{ + {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U8, 7, 0.5,NVCV_BORDER_CONSTANT }, // invalid kernel size + {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U16, 3, 0.5, + NVCV_BORDER_CONSTANT}, // data type is different + {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_RGB8, nvcv::FMT_RGB8p, 3, 0.5, + NVCV_BORDER_CONSTANT}, // interleaved in, planar out + {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_RGB8p, nvcv::FMT_RGB8, 3, 0.5, + NVCV_BORDER_CONSTANT}, // planar in, interleaved out + }; +#ifndef ENABLE_SANITIZER + params.emplace_back(NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_U8, 3, 0.5, + static_cast(255)); +#endif + params.emplace_back(NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_F16, nvcv::FMT_F16, 3, 0.5, NVCV_BORDER_CONSTANT); + return params; +} + +NVCV_TEST_SUITE_P(OpLaplacian_Negative, OpLaplacianNegativeParams()); + TEST_P(OpLaplacian_Negative, op) { cudaStream_t stream; @@ -324,133 +413,25 @@ TEST_P(OpLaplacian_Negative, op) // run operator cvcuda::Laplacian laplacianOp; EXPECT_EQ(expectedReturnCode, - nvcv::ProtectCall([&] { laplacianOp(stream, inTensor, outTensor, ksize, scale, borderMode); })); + nvcv::ProtectCall([&laplacianOp, &stream, &inTensor, &outTensor, &ksize, &scale, &borderMode] + { laplacianOp(stream, inTensor, outTensor, ksize, scale, borderMode); })); ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); } -// clang-format off -NVCV_TEST_SUITE_P(OpLaplacianVarshape_Negative, test::ValueList{ - {nvcv::FMT_RGB8, nvcv::FMT_RGB8p, NVCV_BORDER_CONSTANT}, - {nvcv::FMT_RGB8p, nvcv::FMT_RGB8p, NVCV_BORDER_CONSTANT}, - {nvcv::FMT_RGBf16, nvcv::FMT_RGBf16, NVCV_BORDER_CONSTANT}, -#ifndef ENABLE_SANITIZER - {nvcv::FMT_RGB8, nvcv::FMT_RGB8, static_cast(255)}, -#endif -}); -// clang-format on +NVCV_TEST_SUITE_P(OpLaplacianVarshape_Negative, test::PlanarFilterVarShapeNegativeParams()); TEST_P(OpLaplacianVarshape_Negative, op) { - cudaStream_t stream; - ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); - - int width = 32; - int height = 32; - int batches = 3; - - nvcv::ImageFormat inputFmt = GetParamValue<0>(); - nvcv::ImageFormat outputFmt = GetParamValue<1>(); - NVCVBorderType borderMode = GetParamValue<2>(); - - // Create input varshape - std::default_random_engine rng; - std::uniform_int_distribution udistWidth(width * 0.8, width * 1.1); - std::uniform_int_distribution udistHeight(height * 0.8, height * 1.1); - - std::vector imgSrc; - std::vector imgDst; - - for (int i = 0; i < batches; ++i) - { - imgSrc.emplace_back(nvcv::Size2D{udistWidth(rng), udistHeight(rng)}, inputFmt); - imgDst.emplace_back(imgSrc[i].size(), outputFmt); - } - - nvcv::ImageBatchVarShape batchSrc(batches); - batchSrc.pushBack(imgSrc.begin(), imgSrc.end()); - nvcv::ImageBatchVarShape batchDst(batches); - batchDst.pushBack(imgDst.begin(), imgDst.end()); - - // Create kernel aperture size tensor - nvcv::Tensor ksizeTensor({{batches}, "N"}, nvcv::TYPE_S32); - - // Create scale tensor - nvcv::Tensor scaleTensor({{batches}, "N"}, nvcv::TYPE_F32); - - // Run operator - cvcuda::Laplacian laplacianOp; - - EXPECT_EQ( - NVCV_ERROR_INVALID_ARGUMENT, - nvcv::ProtectCall([&] { laplacianOp(stream, batchSrc, batchDst, ksizeTensor, scaleTensor, borderMode); })); - - ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); - ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + test::planar::ExpectVarShapeUniformFormatRejected(GetParamValue<0>(), GetParamValue<1>(), GetParamValue<3>(), + GetParamValue<4>(), GetParamValue<2>(), + InvokeLaplacianVarShapeNegative); } TEST(OpLaplacianVarshape_Negative, varshape_hasDifferentFormat) { - cudaStream_t stream; - ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); - - nvcv::ImageFormat fmt = nvcv::FMT_RGB8; - - std::vector> testSet{ - {nvcv::FMT_U8, fmt}, - { fmt, nvcv::FMT_U8} - }; - - for (auto testCase : testSet) - { - nvcv::ImageFormat inputFmtExtra = std::get<0>(testCase); - nvcv::ImageFormat outputFmtExtra = std::get<1>(testCase); - - int width = 32; - int height = 32; - int batches = 3; - - NVCVBorderType borderMode = NVCV_BORDER_CONSTANT; - - // Create input varshape - std::default_random_engine rng; - std::uniform_int_distribution udistWidth(width * 0.8, width * 1.1); - std::uniform_int_distribution udistHeight(height * 0.8, height * 1.1); - - std::vector imgSrc; - std::vector imgDst; - - for (int i = 0; i < batches - 1; ++i) - { - imgSrc.emplace_back(nvcv::Size2D{udistWidth(rng), udistHeight(rng)}, fmt); - imgDst.emplace_back(imgSrc[i].size(), fmt); - } - imgSrc.emplace_back(nvcv::Size2D{udistWidth(rng), udistHeight(rng)}, inputFmtExtra); - imgDst.emplace_back(imgSrc.back().size(), outputFmtExtra); - - nvcv::ImageBatchVarShape batchSrc(batches); - batchSrc.pushBack(imgSrc.begin(), imgSrc.end()); - nvcv::ImageBatchVarShape batchDst(batches); - batchDst.pushBack(imgDst.begin(), imgDst.end()); - - // Create kernel aperture size tensor - nvcv::Tensor ksizeTensor({{batches}, "N"}, nvcv::TYPE_S32); - - // Create scale tensor - nvcv::Tensor scaleTensor({{batches}, "N"}, nvcv::TYPE_F32); - - // Run operator - cvcuda::Laplacian laplacianOp; - - EXPECT_EQ( - NVCV_ERROR_INVALID_ARGUMENT, - nvcv::ProtectCall([&] { laplacianOp(stream, batchSrc, batchDst, ksizeTensor, scaleTensor, borderMode); })); - - ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); - } - - ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + test::planar::ExpectVarShapeMixedFormatRejected(InvokeLaplacianVarShapeNegative); } TEST(OpLaplacian_Negative, create_null_handle) diff --git a/tests/cvcuda/system/TestOpMedianBlur.cpp b/tests/cvcuda/system/TestOpMedianBlur.cpp index d848dd3e2..8f1a5e646 100644 --- a/tests/cvcuda/system/TestOpMedianBlur.cpp +++ b/tests/cvcuda/system/TestOpMedianBlur.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,6 +16,7 @@ */ #include "Definitions.hpp" +#include "PlanarParityUtils.hpp" #include #include @@ -26,13 +27,21 @@ #include #include +#include namespace test = nvcv::test; namespace t = ::testing; // #define DBG_MEDIAN_BLUR 1 -static void printVec(std::vector &vec, int height, int rowStride, int bytesPerPixel, std::string name) +static int ScaledSize(int size, double scale) +{ + return static_cast(static_cast(size) * scale); +} + +static void printVec([[maybe_unused]] std::vector &vec, [[maybe_unused]] int height, + [[maybe_unused]] int rowStride, [[maybe_unused]] int bytesPerPixel, + [[maybe_unused]] std::string_view name) { #if DBG_MEDIAN_BLUR for (int i = 0; i < bytesPerPixel; i++) @@ -52,7 +61,7 @@ static void printVec(std::vector &vec, int height, int rowStride, int b #endif } -static uint8_t computeMedianInSubsetMatrix(const std::vector &hSrc, int srcRowStride, nvcv::Size2D srcSize, +static uint8_t computeMedianInSubsetMatrix(const std::vector &hSrc, int srcRowStride, nvcv::Size2D, nvcv::Size2D ksize, nvcv::ImageFormat fmt, int x, int y, int z) { assert(fmt.numPlanes() == 1); @@ -88,7 +97,7 @@ static uint8_t computeMedianInSubsetMatrix(const std::vector &hSrc, int std::cout << std::endl; #endif - sort(samples.begin(), samples.end()); + std::ranges::sort(samples); return samples[samples.size() / 2]; } @@ -121,6 +130,46 @@ static void GenerateMedianBlurGoldenOutput(std::vector &hDst, int dstRo } } +static int ReplicatedOffset(int dstCoord, int offset, int srcLimit) +{ + int srcCoord = dstCoord - offset; + if (srcCoord <= 0) + { + return 0; + } + + if (srcCoord >= srcLimit) + { + return srcLimit - 1; + } + + return srcCoord; +} + +struct ReplicateBorderData +{ + uint8_t *dstPtr; + const uint8_t *srcPtr; + int dstRowStride; + int srcRowStride; + int elementsPerPixel; + int offsetWidth; + int offsetHeight; + nvcv::Size2D srcSize; +}; + +static void GenerateReplicatedPixel(const ReplicateBorderData &ref, int dst_x, int dst_y) +{ + int srcX = ReplicatedOffset(dst_x, ref.offsetWidth, ref.srcSize.w); + int srcY = ReplicatedOffset(dst_y, ref.offsetHeight, ref.srcSize.h); + + for (int k = 0; k < ref.elementsPerPixel; k++) + { + ref.dstPtr[dst_y * ref.dstRowStride + dst_x * ref.elementsPerPixel + k] + = ref.srcPtr[srcY * ref.srcRowStride + srcX * ref.elementsPerPixel + k]; + } +} + static void GenerateInputWithBorderReplicate(std::vector &hDst, int dstRowStride, nvcv::Size2D dstSize, std::vector &hSrc, int srcRowStride, nvcv::Size2D srcSize, nvcv::ImageFormat fmt, nvcv::Size2D ksize) @@ -129,11 +178,8 @@ static void GenerateInputWithBorderReplicate(std::vector &hDst, int dst int elementsPerPixel = fmt.numChannels(); - uint8_t *srcPtr = hSrc.data(); - uint8_t *dstPtr = hDst.data(); - - int srcWidth = srcSize.w; - int srcHeight = srcSize.h; + const uint8_t *srcPtr = hSrc.data(); + uint8_t *dstPtr = hDst.data(); int dstWidth = dstSize.w; int dstHeight = dstSize.h; @@ -141,34 +187,14 @@ static void GenerateInputWithBorderReplicate(std::vector &hDst, int dst int offsetWidth = ksize.w / 2; int offsetHeight = ksize.h / 2; + ReplicateBorderData ref{dstPtr, srcPtr, dstRowStride, srcRowStride, + elementsPerPixel, offsetWidth, offsetHeight, srcSize}; + for (int dst_y = 0; dst_y < dstHeight; dst_y++) { for (int dst_x = 0; dst_x < dstWidth; dst_x++) { - for (int k = 0; k < elementsPerPixel; k++) - { - int reducedOffsetX = dst_x - offsetWidth; - int reducedOffsetY = dst_y - offsetHeight; - if (reducedOffsetX <= 0) - { - reducedOffsetX = 0; - } - else if (reducedOffsetX >= srcWidth) - { - reducedOffsetX = srcWidth - 1; - } - - if (reducedOffsetY <= 0) - { - reducedOffsetY = 0; - } - else if (reducedOffsetY >= srcHeight) - { - reducedOffsetY = srcHeight - 1; - } - dstPtr[dst_y * dstRowStride + dst_x * elementsPerPixel + k] - = srcPtr[reducedOffsetY * srcRowStride + reducedOffsetX * elementsPerPixel + k]; - } + GenerateReplicatedPixel(ref, dst_x, dst_y); } } } @@ -178,9 +204,15 @@ static void GenerateInputWithBorderReplicate(std::vector &hDst, int dst NVCV_TEST_SUITE_P(OpMedianBlur, test::ValueList { // width, height, kernel size, numberImages + { 9, 9, {3,3}, 1}, + { 9, 9, {3,3}, 4}, + { 9, 9, {5,5}, 1}, { 9, 9, {5,5}, 4}, + { 13, 13, {7,7}, 1}, + { 13, 13, {7,7}, 4}, + { 21, 21, {15,15}, 1}, { 21, 21, {15,15}, 4}, { 127, 127, {33,33}, 2}, // large filter to test global memory fetching @@ -228,7 +260,7 @@ TEST_P(OpMedianBlur, tensor_correct_output) std::uniform_int_distribution rand(0, 255); srcVec[i].resize(srcHeight * srcVecRowStride); - std::generate(srcVec[i].begin(), srcVec[i].end(), [&]() { return rand(randEng); }); + std::ranges::generate(srcVec[i], [&rand, &randEng]() { return rand(randEng); }); // Copy input data to the GPU ASSERT_EQ(cudaSuccess, @@ -316,13 +348,17 @@ TEST_P(OpMedianBlur, varshape_correct_output) ASSERT_TRUE(ksizeTensorDataAccess); // Create input and output - std::default_random_engine randEng; - std::uniform_int_distribution rndSrcWidth(srcWidthBase * 0.8, srcWidthBase * 1.1); - std::uniform_int_distribution rndSrcHeight(srcHeightBase * 0.8, srcHeightBase * 1.1); - std::uniform_int_distribution rndSrcKSizeWidth(ksize.w * 0.8, ksize.w * 1.1); - std::uniform_int_distribution rndSrcKSizeHeight(ksize.h * 0.8, ksize.h * 1.1); + std::default_random_engine randEng; + std::uniform_int_distribution rndSrcWidth(ScaledSize(srcWidthBase, 0.8), ScaledSize(srcWidthBase, 1.1)); + std::uniform_int_distribution rndSrcHeight(ScaledSize(srcHeightBase, 0.8), ScaledSize(srcHeightBase, 1.1)); + std::uniform_int_distribution rndSrcKSizeWidth(ScaledSize(ksize.w, 0.8), ScaledSize(ksize.w, 1.1)); + std::uniform_int_distribution rndSrcKSizeHeight(ScaledSize(ksize.h, 0.8), ScaledSize(ksize.h, 1.1)); + + std::vector imgSrc; - std::vector imgSrc, imgSrcBrdReplicate, imgDst; + std::vector imgSrcBrdReplicate; + + std::vector imgDst; std::vector ksizeVecs; for (int i = 0; i < numberOfImages; ++i) @@ -380,7 +416,7 @@ TEST_P(OpMedianBlur, varshape_correct_output) std::uniform_int_distribution rand(0, 255); srcVec[i].resize(srcHeight * srcVecRowStride[i]); - std::generate(srcVec[i].begin(), srcVec[i].end(), [&]() { return rand(randEng); }); + std::ranges::generate(srcVec[i], [&rand, &randEng]() { return rand(randEng); }); printVec(srcVec[i], srcHeight, srcVecRowStride[i], bytesPerPixel, "input"); @@ -435,8 +471,8 @@ TEST_P(OpMedianBlur, varshape_correct_output) int dstWidth = dstData->plane(0).width; int dstHeight = dstData->plane(0).height; - int dstRowStride = dstWidth * fmt.planePixelStrideBytes(0); - int srcBrdReplicateVecRowStride = srcBrdReplicateWidth * fmt.planePixelStrideBytes(0); + int dstRowStride = dstWidth * fmt.planePixelStrideBytes(0); + int srcBrdReplicateStep = srcBrdReplicateWidth * fmt.planePixelStrideBytes(0); std::vector testVec(dstHeight * dstRowStride); @@ -447,11 +483,11 @@ TEST_P(OpMedianBlur, varshape_correct_output) dstHeight, cudaMemcpyDeviceToHost)); std::vector goldVec(dstHeight * dstRowStride); - std::generate(goldVec.begin(), goldVec.end(), [&]() { return 0; }); + std::ranges::generate(goldVec, []() { return 0; }); // Generate gold result GenerateMedianBlurGoldenOutput(goldVec, dstRowStride, {dstWidth, dstHeight}, srcBrdReplicateVec[i], - srcBrdReplicateVecRowStride, {srcBrdReplicateWidth, srcBrdReplicateHeight}, fmt, + srcBrdReplicateStep, {srcBrdReplicateWidth, srcBrdReplicateHeight}, fmt, ksizeVecs[i]); printVec(goldVec, dstHeight, dstRowStride, bytesPerPixel, "golden output"); @@ -462,13 +498,76 @@ TEST_P(OpMedianBlur, varshape_correct_output) } } +// Planar (NCHW/CHW) layout support + +static void RunMedianBlurPlanarParityTensorCase(nvcv::ImageFormat planarFmt, nvcv::ImageFormat interleavedFmt, int w, + int h, nvcv::Size2D ksize, int numImages) +{ + cvcuda::MedianBlur medianBlurOp(0); + + test::planar::RunTensorParity( + planarFmt, interleavedFmt, w, h, w, h, numImages, + [&medianBlurOp, ksize](cudaStream_t stream, const nvcv::Tensor &src, const nvcv::Tensor &dst, nvcv::ImageFormat) + { medianBlurOp(stream, src, dst, ksize); }); +} + +static void RunMedianBlurPlanarParityVarShapeCase(nvcv::ImageFormat planarFmt, nvcv::ImageFormat interleavedFmt, int w, + int h, nvcv::Size2D ksize, int numImages) +{ + nvcv::Tensor ksizeTensor(nvcv::TensorShape({numImages}, "N"), nvcv::TYPE_2S32); + auto ksizeTensorData = ksizeTensor.exportData(); + ASSERT_NE(nullptr, ksizeTensorData); + + auto ksizeTensorDataAccess = nvcv::TensorDataAccessStrided::Create(*ksizeTensorData); + ASSERT_TRUE(ksizeTensorDataAccess); + + std::vector ksizeVec(numImages, ksize); + ASSERT_EQ(cudaSuccess, + cudaMemcpy2D(ksizeTensorDataAccess->sampleData(0), ksizeTensorDataAccess->sampleStride(), ksizeVec.data(), + sizeof(int2), sizeof(int2), numImages, cudaMemcpyHostToDevice)); + + cvcuda::MedianBlur medianBlurOp(numImages); + + test::planar::RunVarShapeParity( + planarFmt, interleavedFmt, w, h, w, h, numImages, + [&medianBlurOp, &ksizeTensor](cudaStream_t stream, const nvcv::ImageBatchVarShape &src, + const nvcv::ImageBatchVarShape &dst, nvcv::ImageFormat) + { medianBlurOp(stream, src, dst, ksizeTensor); }); +} + +// clang-format off +NVCV_TEST_SUITE_P(OpMedianBlurPlanar, + test::ValueList +{ + // width, height, ksize, numImages, planar format, interleaved format + { 31, 25, {3, 3}, 2, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + { 33, 29, {3, 5}, 2, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + { 29, 27, {5, 5}, 1, nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8}, + { 17, 13, {3, 3}, 2, nvcv::FMT_RGBf32p, nvcv::FMT_RGBf32}, + { 15, 11, {3, 3}, 1, nvcv::FMT_RGBAf32p, nvcv::FMT_RGBAf32}, +}); + +// clang-format on + +TEST_P(OpMedianBlurPlanar, tensor_matches_interleaved) +{ + RunMedianBlurPlanarParityTensorCase(GetParamValue<4>(), GetParamValue<5>(), GetParamValue<0>(), GetParamValue<1>(), + GetParamValue<2>(), GetParamValue<3>()); +} + +TEST_P(OpMedianBlurPlanar, varshape_matches_interleaved) +{ + RunMedianBlurPlanarParityVarShapeCase(GetParamValue<4>(), GetParamValue<5>(), GetParamValue<0>(), + GetParamValue<1>(), GetParamValue<2>(), GetParamValue<3>()); +} + // clang-format off NVCV_TEST_SUITE_P(OpMedianBlur_Negative, test::ValueList{ // inFmt, outFmt, kernelSize {nvcv::FMT_RGB8p, nvcv::FMT_RGB8, {3, 3}}, {nvcv::FMT_RGB8, nvcv::FMT_RGB8p, {3, 3}}, - {nvcv::FMT_RGB8p, nvcv::FMT_RGB8p, {3, 3}}, {nvcv::FMT_RGBf16, nvcv::FMT_RGBf16, {3, 3}}, + {nvcv::FMT_2F32, nvcv::FMT_2F32, {3, 3}}, {nvcv::FMT_RGB8, nvcv::FMT_RGB8, {4, 3}}, {nvcv::FMT_RGB8, nvcv::FMT_RGB8, {3, 4}}, }); @@ -476,9 +575,9 @@ NVCV_TEST_SUITE_P(OpMedianBlur_Negative, test::ValueList{ // inFmt, outFmt, kernelSize, maxBatchSize, numImages {nvcv::FMT_RGB8p, nvcv::FMT_RGB8, {3, 3}, 2, 2}, - {nvcv::FMT_RGB8p, nvcv::FMT_RGB8p, {3, 3}, 2, 2}, {nvcv::FMT_RGB8, nvcv::FMT_RGB8p, {3, 3}, 2, 2}, {nvcv::FMT_RGBf16, nvcv::FMT_RGBf16, {3, 3}, 2, 2}, + {nvcv::FMT_2F32, nvcv::FMT_2F32, {3, 3}, 2, 2}, {nvcv::FMT_RGB8, nvcv::FMT_RGB8, {4, 3}, 2, 2}, {nvcv::FMT_RGB8, nvcv::FMT_RGB8, {3, 4}, 2, 2}, {nvcv::FMT_RGB8, nvcv::FMT_RGB8, {3, 3}, 0, 2}, @@ -501,7 +600,8 @@ TEST_P(OpMedianBlur_Negative, op) nvcv::Tensor imgDst(2, {24, 24}, outFmt); cvcuda::MedianBlur medianBlurOp(0); - EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&] { medianBlurOp(stream, imgSrc, imgDst, ksize); })); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&medianBlurOp, &stream, &imgSrc, &imgDst, &ksize] + { medianBlurOp(stream, imgSrc, imgDst, ksize); })); EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); @@ -530,13 +630,17 @@ TEST_P(OpMedianBlurVarshape_Negative, op) ASSERT_TRUE(ksizeTensorDataAccess); // Create input and output - std::default_random_engine randEng; - std::uniform_int_distribution rndSrcWidth(srcWidthBase * 0.8, srcWidthBase * 1.1); - std::uniform_int_distribution rndSrcHeight(srcHeightBase * 0.8, srcHeightBase * 1.1); - std::uniform_int_distribution rndSrcKSizeWidth(ksize.w * 0.8, ksize.w * 1.1); - std::uniform_int_distribution rndSrcKSizeHeight(ksize.h * 0.8, ksize.h * 1.1); + std::default_random_engine randEng; + std::uniform_int_distribution rndSrcWidth(ScaledSize(srcWidthBase, 0.8), ScaledSize(srcWidthBase, 1.1)); + std::uniform_int_distribution rndSrcHeight(ScaledSize(srcHeightBase, 0.8), ScaledSize(srcHeightBase, 1.1)); + std::uniform_int_distribution rndSrcKSizeWidth(ScaledSize(ksize.w, 0.8), ScaledSize(ksize.w, 1.1)); + std::uniform_int_distribution rndSrcKSizeHeight(ScaledSize(ksize.h, 0.8), ScaledSize(ksize.h, 1.1)); + + std::vector imgSrc; + + std::vector imgSrcBrdReplicate; - std::vector imgSrc, imgSrcBrdReplicate, imgDst; + std::vector imgDst; std::vector ksizeVecs; for (int i = 0; i < numberOfImages; ++i) @@ -563,7 +667,8 @@ TEST_P(OpMedianBlurVarshape_Negative, op) cvcuda::MedianBlur medianBlurOp(maxBatchSize); EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, - nvcv::ProtectCall([&] { medianBlurOp(stream, batchSrc, batchDst, ksizeTensor); })); + nvcv::ProtectCall([&medianBlurOp, &stream, &batchSrc, &batchDst, &ksizeTensor] + { medianBlurOp(stream, batchSrc, batchDst, ksizeTensor); })); // Get test data back EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); diff --git a/tests/cvcuda/system/TestOpMinAreaRect.cpp b/tests/cvcuda/system/TestOpMinAreaRect.cpp index 72026bfa8..b7e9df09c 100644 --- a/tests/cvcuda/system/TestOpMinAreaRect.cpp +++ b/tests/cvcuda/system/TestOpMinAreaRect.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -24,6 +24,7 @@ #include #include +#include #include #include #include @@ -34,10 +35,25 @@ namespace t = ::testing; void formatPoints(std::vector> points, std::vector> &format_points); bool isNearOpenCvResults(std::vector opencvRes, std::vector cvcudaRes); +template +void SetRectanglePoints(nvcv::Tensor &tensor, size_t contourElements, int numPoints) +{ + std::vector contour(contourElements, 0); + constexpr std::array, 4> corners{ + {{10, 20}, {110, 20}, {110, 70}, {10, 70}} + }; + for (int i = 0; i < numPoints; ++i) + { + contour[2 * i] = corners[i % corners.size()].first; + contour[2 * i + 1] = corners[i % corners.size()].second; + } + nvcv::util::SetTensorFromVector(tensor.exportData(), contour, 0); +} + void formatPoints(std::vector> points, std::vector> &format_points) { - std::sort(points.begin(), points.end(), - [](std::pair &a, const std::pair &b) { return a.first < b.first; }); + std::ranges::sort( + points, [](const std::pair &a, const std::pair &b) { return a.first < b.first; }); if (points[0].second <= points[1].second) { @@ -122,9 +138,9 @@ TEST(OpMinAreaRect, MinAreaRect_sanity) for (int i = 0; i < batchsize; i++) { - inPointNumInContourValues[i] = contourPointsData[i].size() / 2; + inPointNumInContourValues[i] = static_cast(contourPointsData[i].size() / 2); } - int maxPointsNumInCountour = *std::max_element(inPointNumInContourValues.begin(), inPointNumInContourValues.end()); + int maxPointsNumInCountour = *std::ranges::max_element(inPointNumInContourValues); // inTensor auto tshapeIn = nvcv::TensorShape{ @@ -165,7 +181,7 @@ TEST(OpMinAreaRect, MinAreaRect_sanity) // copy output back to host for (size_t i = 0; i < testVec.size(); i++) { - nvcv::util::GetVectorFromTensor(outMinAreaRect.exportData(), i, testVec[i]); + nvcv::util::GetVectorFromTensor(outMinAreaRect.exportData(), static_cast(i), testVec[i]); ASSERT_PRED2(isNearOpenCvResults, openCV_minAreaRect_results[i], testVec[i]); } } @@ -193,9 +209,9 @@ TEST(OpMinAreaRect, MinAreaRect_multiple_contours_odd_stride) for (int i = 0; i < batchsize; i++) { - inPointNumInContourValues[i] = contourPointsData[i].size() / 2; + inPointNumInContourValues[i] = static_cast(contourPointsData[i].size() / 2); } - int maxPointsNumInCountour = *std::max_element(inPointNumInContourValues.begin(), inPointNumInContourValues.end()); + int maxPointsNumInCountour = *std::ranges::max_element(inPointNumInContourValues); // inTensor auto tshapeIn = nvcv::TensorShape{ @@ -232,8 +248,90 @@ TEST(OpMinAreaRect, MinAreaRect_multiple_contours_odd_stride) ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); } +NVCV_TEST_SUITE_P(OpMinAreaRectCorrectness, test::ValueList{ + {nvcv::TYPE_S16, 1024}, + {nvcv::TYPE_U16, 128}, + {nvcv::TYPE_U16, 512}, + {nvcv::TYPE_S32, 128}, + {nvcv::TYPE_S32, 512}, +}); + +TEST_P(OpMinAreaRectCorrectness, tensor_correct_output) +{ + nvcv::DataType dtype = GetParamValue<0>(); + int numOfPoints = GetParamValue<1>(); + + nvcv::Tensor inPointNumInContour{ + nvcv::TensorShape{{1, 1}, nvcv::TENSOR_NW}, + nvcv::TYPE_S32 + }; + auto pointCountAccess = nvcv::TensorDataAccessStrided::Create(inPointNumInContour.exportData()); + std::vector pointCounts(pointCountAccess->sampleStride() / sizeof(int), 0); + pointCounts[0] = numOfPoints; + nvcv::util::SetTensorFromVector(inPointNumInContour.exportData(), pointCounts, -1); + + nvcv::Tensor inContours{ + nvcv::TensorShape{{1, numOfPoints, 2}, nvcv::TENSOR_NWC}, + dtype + }; + auto contourAccess = nvcv::TensorDataAccessStrided::Create(inContours.exportData()); + auto contourElements = contourAccess->sampleStride() / dtype.strideBytes(); + + if (dtype == nvcv::TYPE_S16) + { + SetRectanglePoints(inContours, contourElements, numOfPoints); + } + else if (dtype == nvcv::TYPE_U16) + { + SetRectanglePoints(inContours, contourElements, numOfPoints); + } + else + { + ASSERT_EQ(dtype, nvcv::TYPE_S32); + SetRectanglePoints(inContours, contourElements, numOfPoints); + } + + nvcv::Tensor outMinAreaRect{ + nvcv::TensorShape{{1, 8}, nvcv::TENSOR_NW}, + nvcv::TYPE_F32 + }; + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + cvcuda::MinAreaRect minAreaRectOp(1); + EXPECT_NO_THROW(minAreaRectOp(stream, inContours, outMinAreaRect, inPointNumInContour, 1)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + + std::vector output(8); + nvcv::util::GetVectorFromTensor(outMinAreaRect.exportData(), 0, output); + + std::vector> expected{ + { 10, 20}, + {110, 20}, + {110, 70}, + { 10, 70} + }; + std::vector> actual{ + {output[0], output[1]}, + {output[2], output[3]}, + {output[4], output[5]}, + {output[6], output[7]} + }; + std::vector> expectedFormatted(4); + std::vector> actualFormatted(4); + formatPoints(expected, expectedFormatted); + formatPoints(actual, actualFormatted); + + for (size_t i = 0; i < expectedFormatted.size(); ++i) + { + EXPECT_EQ(expectedFormatted[i].first, actualFormatted[i].first); + EXPECT_EQ(expectedFormatted[i].second, actualFormatted[i].second); + } +} + // clang-format off -NVCV_TEST_SUITE_P(OpMinAreaRectNegative, test::ValueList +NVCV_TEST_SUITE_P(OpMinAreaRect_Negative, test::ValueList { // batchsize, inLayout, numPointsInContourLayout, outLayout, inDataType, numPointsInContourDataType, outDataType { 10, nvcv::TENSOR_NWC, nvcv::TENSOR_NW, nvcv::TENSOR_NW, nvcv::TYPE_S16, nvcv::TYPE_S32, nvcv::TYPE_F32}, @@ -247,7 +345,7 @@ NVCV_TEST_SUITE_P(OpMinAreaRectNegative, test::ValueList(); nvcv::TensorLayout inLayout = GetParamValue<1>(); @@ -286,7 +384,8 @@ TEST_P(OpMinAreaRectNegative, tensor_correct_output) cvcuda::MinAreaRect minAreaRectOp(maxContourNum); EXPECT_EQ( NVCV_ERROR_INVALID_ARGUMENT, - nvcv::ProtectCall([&] { minAreaRectOp(stream, inContours, outMinAreaRect, inPointNumInContour, batchsize); })); + nvcv::ProtectCall([&minAreaRectOp, &stream, &inContours, &outMinAreaRect, &inPointNumInContour, &batchsize] + { minAreaRectOp(stream, inContours, outMinAreaRect, inPointNumInContour, batchsize); })); ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); @@ -296,3 +395,50 @@ TEST(OpMinAreaRect, invalid_create) { EXPECT_EQ(cvcudaMinAreaRectCreate(nullptr, 1), NVCV_ERROR_INVALID_ARGUMENT); } + +TEST(OpMinAreaRect, numPointsInContour_exceeds_tensor_width) +{ + // Regression test for GPU heap overread: numPointsInContour > max_pts must not + // cause out-of-bounds GPU reads in calculateRotateArea. + int batchsize = 1; + int max_pts = 4; + int reported_pts = 100; // intentionally larger than max_pts + + auto tshapeIn = nvcv::TensorShape{ + {batchsize, max_pts, 2}, + nvcv::TENSOR_NWC + }; + nvcv::DataType dtypeIn = nvcv::TYPE_S16; + nvcv::Tensor inContours{tshapeIn, dtypeIn}; + auto inContoursAccess = nvcv::TensorDataAccessStrided::Create(inContours.exportData()); + auto numContoursElements = inContoursAccess->sampleStride() / (2 * dtypeIn.strideBytes()); + + // Use a simple axis-aligned rectangle. + std::vector pts = {0, 0, 100, 0, 100, 50, 0, 50}; + pts.resize(numContoursElements * 2, 0); + nvcv::util::SetTensorFromVector(inContours.exportData(), pts, 0); + + nvcv::Tensor inPointNumInContour{ + nvcv::TensorShape{{1, batchsize}, nvcv::TENSOR_NW}, + nvcv::TYPE_S32 + }; + auto inPointNumInContourAccess = nvcv::TensorDataAccessStrided::Create(inPointNumInContour.exportData()); + auto numPointNumInContourElements = inPointNumInContourAccess->sampleStride() / sizeof(int); + std::vector numPtsVec(numPointNumInContourElements, 0); + numPtsVec[0] = reported_pts; + nvcv::util::SetTensorFromVector(inPointNumInContour.exportData(), numPtsVec, -1); + + auto tshapeOut = nvcv::TensorShape{ + {batchsize, 8}, + nvcv::TENSOR_NW + }; + nvcv::Tensor outMinAreaRect{tshapeOut, nvcv::TYPE_F32}; + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + cvcuda::MinAreaRect minAreaRectOp(batchsize); + EXPECT_NO_THROW(minAreaRectOp(stream, inContours, outMinAreaRect, inPointNumInContour, batchsize)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} diff --git a/tests/cvcuda/system/TestOpMinMaxLoc.cpp b/tests/cvcuda/system/TestOpMinMaxLoc.cpp index 98fbbaa04..9c13f8936 100644 --- a/tests/cvcuda/system/TestOpMinMaxLoc.cpp +++ b/tests/cvcuda/system/TestOpMinMaxLoc.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -29,15 +30,32 @@ #include #include +#include +#include #include +#include #include #include namespace cuda = nvcv::cuda; +namespace gt = ::testing; namespace test = nvcv::test; namespace type = nvcv::test::type; +namespace util = nvcv::util; -static std::default_random_engine g_rng(std::random_device{}()); +static int ScaledSize(int size, double scale) +{ + return static_cast(size * scale); +} + +// Fixed seed: random_device made tests non-deterministic across CI runs and +// occasionally produced ill-conditioned numerical inputs that exceeded +// EXPECT_NEAR tolerances on rare-config CI. Use a known-good fixed seed. +static std::default_random_engine &Rng() +{ + static std::default_random_engine rng(12345); + return rng; +} template using uniform_distribution @@ -69,11 +87,38 @@ inline nvcv::DataType GetValDataType(nvcv::DataType inDataType) // Compute reference (gold) output of operator MinMaxLoc +template +inline cuda::BaseType InputValue(InContainerType &in, InStridesType &inStrides, int z, int x, int y) +{ + if constexpr (std::is_same_v>) + { + return test::ValueAt(in, inStrides, int3{x, y, z}).x; + } + else + { + return test::ValueAt(in[z], inStrides[z], int2{x, y}).x; + } +} + +inline void RecordLocation(std::vector> &locations, int z, int capacity, int &count, bool matches, + int2 loc) +{ + if (!matches) + { + return; + } + if (count < capacity) + { + locations[z].push_back(loc); + } + count++; +} + template inline void FindMinMax(InContainerType &in, InStridesType &inStrides, InShapeType &inShape, std::vector &minVal, std::vector &maxVal, long1 valStrides, std::vector> &minLoc, std::vector> &maxLoc, int capacity, - std::vector &numMin, std::vector &numMax, long1 &numStrides) + std::vector &numMin, std::vector &numMax, const long1 &numStrides) { constexpr bool InIsTensor = std::is_same_v>; @@ -84,17 +129,16 @@ inline void FindMinMax(InContainerType &in, InStridesType &inStrides, InShapeTyp } else { - numSamples = in.size(); + numSamples = static_cast(in.size()); } using InBT = cuda::BaseType; using OutBT = cuda::BaseType; - InBT val; - for (int z = 0; z < numSamples; ++z) { - OutBT min{cuda::TypeTraits::max}, max{cuda::Lowest}; + OutBT min{cuda::TypeTraits::max}; + OutBT max{cuda::Lowest}; int2 inSize; if constexpr (InIsTensor) @@ -106,59 +150,27 @@ inline void FindMinMax(InContainerType &in, InStridesType &inStrides, InShapeTyp inSize = inShape[z]; } - for (int y = 0; y < inSize.y; ++y) + const int numPixels = inSize.x * inSize.y; + for (int idx = 0; idx < numPixels; ++idx) { - for (int x = 0; x < inSize.x; ++x) - { - if constexpr (InIsTensor) - { - val = test::ValueAt(in, inStrides, int3{x, y, z}).x; - } - else - { - val = test::ValueAt(in[z], inStrides[z], int2{x, y}).x; - } - - min = std::min(min, static_cast(val)); - max = std::max(max, static_cast(val)); - } + InBT val = InputValue(in, inStrides, z, idx % inSize.x, idx / inSize.x); + min = std::min(min, static_cast(val)); + max = std::max(max, static_cast(val)); } test::ValueAt(minVal, valStrides, {z}).x = min; test::ValueAt(maxVal, valStrides, {z}).x = max; - int nMin{0}, nMax{0}; + int nMin{0}; + int nMax{0}; - for (int y = 0; y < inSize.y; ++y) + for (int idx = 0; idx < numPixels; ++idx) { - for (int x = 0; x < inSize.x; ++x) - { - if constexpr (InIsTensor) - { - val = test::ValueAt(in, inStrides, int3{x, y, z}).x; - } - else - { - val = test::ValueAt(in[z], inStrides[z], int2{x, y}).x; - } - - if (val == min) - { - if (nMin < capacity) - { - minLoc[z].push_back(int2{x, y}); - } - nMin++; - } - if (val == max) - { - if (nMax < capacity) - { - maxLoc[z].push_back(int2{x, y}); - } - nMax++; - } - } + const int x = idx % inSize.x; + const int y = idx / inSize.x; + InBT val = InputValue(in, inStrides, z, x, y); + RecordLocation(minLoc, z, capacity, nMin, val == min, int2{x, y}); + RecordLocation(maxLoc, z, capacity, nMax, val == max, int2{x, y}); } test::ValueAt(numMin, numStrides, {z}).x = nMin; @@ -169,8 +181,9 @@ inline void FindMinMax(InContainerType &in, InStridesType &inStrides, InShapeTyp // Sort min/max locations to be able to compare test vs. gold results inline void LocSort(std::vector> &minLocTest, std::vector> &maxLocTest, - int capacity, std::vector &minLocVec, std::vector &maxLocVec, long2 &locStrides, - std::vector &numMinVec, std::vector &numMaxVec, long1 &numStrides) + int capacity, std::vector &minLocVec, std::vector &maxLocVec, + const long2 &locStrides, std::vector &numMinVec, std::vector &numMaxVec, + const long1 &numStrides) { ASSERT_EQ(minLocTest.size(), maxLocTest.size()); @@ -179,7 +192,7 @@ inline void LocSort(std::vector> &minLocTest, std::vector(minLocTest.size()); z++) { int nMin = test::ValueAt(numMinVec, numStrides, {z}).x; int nMax = test::ValueAt(numMaxVec, numStrides, {z}).x; @@ -193,8 +206,8 @@ inline void LocSort(std::vector> &minLocTest, std::vector(maxLocVec, locStrides, {i, z})); } - std::sort(minLocTest[z].begin(), minLocTest[z].end(), locLower); - std::sort(maxLocTest[z].begin(), maxLocTest[z].end(), locLower); + std::ranges::sort(minLocTest[z], locLower); + std::ranges::sort(maxLocTest[z], locLower); } } @@ -202,13 +215,168 @@ inline void LocSort(std::vector> &minLocTest, std::vector minValTest, numMinTest, maxValTest, numMaxTest, minLocTemp, maxLocTemp; - std::vector minValGold, numMinGold, maxValGold, numMaxGold; + std::vector minValTest; + std::vector numMinTest; + std::vector maxValTest; + std::vector numMaxTest; + std::vector minLocTemp; + std::vector maxLocTemp; + std::vector minValGold; + std::vector numMinGold; + std::vector maxValGold; + std::vector numMaxGold; + + std::vector> minLocTest; + std::vector> maxLocTest; + std::vector> minLocGold; + std::vector> maxLocGold; +}; - std::vector> minLocTest, maxLocTest; - std::vector> minLocGold, maxLocGold; +struct MinMaxParityResults +{ + std::vector minVal; + std::vector numMin; + std::vector maxVal; + std::vector numMax; + std::vector> minLoc; + std::vector> maxLoc; }; +static nvcv::Tensor CreateMinMaxLocParityTensor(int batches, int width, int height, bool planar, bool batched) +{ + if (batched && planar) + { + return nvcv::Tensor( + { + {batches, 1, height, width}, + "NCHW" + }, + nvcv::TYPE_U8); + } + if (planar) + { + return nvcv::Tensor( + { + {1, height, width}, + "CHW" + }, + nvcv::TYPE_U8); + } + if (batched) + { + return nvcv::Tensor( + { + {batches, height, width, 1}, + "NHWC" + }, + nvcv::TYPE_U8); + } + return nvcv::Tensor( + { + {height, width, 1}, + "HWC" + }, + nvcv::TYPE_U8); +} + +static std::vector MakeMinMaxLocParityInput(int width, int height, int sample) +{ + std::vector values(width * height); + for (size_t i = 0; i < values.size(); ++i) + { + values[i] = static_cast((37 * i + 53 * sample + 17) & 0xff); + } + values.front() = 0; + values.back() = 255; + return values; +} + +static void FillMinMaxLocParityInput(nvcv::Tensor &interleaved, nvcv::Tensor &planar, int width, int height, int sample) +{ + std::vector values = MakeMinMaxLocParityInput(width, height, sample); + ASSERT_NO_THROW(util::SetImageTensorFromVector(interleaved.exportData(), values, sample)); + ASSERT_NO_THROW(util::SetImageTensorFromVector(planar.exportData(), values, sample)); +} + +static void FillMinMaxLocParityInputs(nvcv::Tensor &interleaved, nvcv::Tensor &planar, int width, int height, + int batches) +{ + for (int sample = 0; sample < batches; ++sample) + { + FillMinMaxLocParityInput(interleaved, planar, width, height, sample); + } +} + +static void DownloadMinMaxParityResults(const nvcv::Tensor &minVal, const nvcv::Tensor &minLoc, + const nvcv::Tensor &numMin, const nvcv::Tensor &maxVal, + const nvcv::Tensor &maxLoc, const nvcv::Tensor &numMax, + MinMaxParityResults &results) +{ + auto minValData = minVal.exportData(); + auto minLocData = minLoc.exportData(); + auto numMinData = numMin.exportData(); + auto maxValData = maxVal.exportData(); + auto maxLocData = maxLoc.exportData(); + auto numMaxData = numMax.exportData(); + ASSERT_TRUE(minValData && minLocData && numMinData && maxValData && maxLocData && numMaxData); + + const auto numSamples = static_cast(minValData->shape(0)); + const auto capacity = static_cast(minLocData->shape(1)); + const long1 valStrides{minValData->stride(0)}; + const long2 locStrides{minLocData->stride(0), minLocData->stride(1)}; + const long1 numStrides{numMinData->stride(0)}; + + std::vector minValVec(numSamples * valStrides.x); + std::vector maxValVec(numSamples * valStrides.x); + std::vector minLocVec(numSamples * locStrides.x); + std::vector maxLocVec(numSamples * locStrides.x); + std::vector numMinVec(numSamples * numStrides.x); + std::vector numMaxVec(numSamples * numStrides.x); + +#define NVCV_TEST_CUDA_COPY_PARITY(FROM, TO) \ + ASSERT_EQ(cudaSuccess, cudaMemcpy(TO.data(), FROM->basePtr(), TO.size(), cudaMemcpyDeviceToHost)) + + NVCV_TEST_CUDA_COPY_PARITY(minValData, minValVec); + NVCV_TEST_CUDA_COPY_PARITY(maxValData, maxValVec); + NVCV_TEST_CUDA_COPY_PARITY(minLocData, minLocVec); + NVCV_TEST_CUDA_COPY_PARITY(maxLocData, maxLocVec); + NVCV_TEST_CUDA_COPY_PARITY(numMinData, numMinVec); + NVCV_TEST_CUDA_COPY_PARITY(numMaxData, numMaxVec); + +#undef NVCV_TEST_CUDA_COPY_PARITY + + results.minVal.resize(numSamples); + results.maxVal.resize(numSamples); + results.numMin.resize(numSamples); + results.numMax.resize(numSamples); + results.minLoc.resize(numSamples); + results.maxLoc.resize(numSamples); + + auto locLower = [](int2 lhs, int2 rhs) + { + return lhs.y == rhs.y ? lhs.x < rhs.x : lhs.y < rhs.y; + }; + + for (int sample = 0; sample < numSamples; ++sample) + { + results.minVal[sample] = test::ValueAt(minValVec, valStrides, {sample}).x; + results.maxVal[sample] = test::ValueAt(maxValVec, valStrides, {sample}).x; + results.numMin[sample] = test::ValueAt(numMinVec, numStrides, {sample}).x; + results.numMax[sample] = test::ValueAt(numMaxVec, numStrides, {sample}).x; + + for (int i = 0; i < std::min(results.numMin[sample], capacity); ++i) + { + results.minLoc[sample].push_back(test::ValueAt(minLocVec, locStrides, {i, sample})); + } + for (int i = 0; i < std::min(results.numMax[sample], capacity); ++i) + { + results.maxLoc[sample].push_back(test::ValueAt(maxLocVec, locStrides, {i, sample})); + } + std::ranges::sort(results.minLoc[sample], locLower); + std::ranges::sort(results.maxLoc[sample], locLower); + } +} + template inline void GoldMinMaxLoc(const nvcv::Tensor &minVal, const nvcv::Tensor &minLoc, const nvcv::Tensor &numMin, const nvcv::Tensor &maxVal, const nvcv::Tensor &maxLoc, const nvcv::Tensor &numMax, @@ -223,8 +391,8 @@ inline void GoldMinMaxLoc(const nvcv::Tensor &minVal, const nvcv::Tensor &minLoc auto numMaxData = numMax.exportData(); ASSERT_TRUE(minValData && minLocData && numMinData && maxValData && maxLocData && numMaxData); - int capacity = minLocData->shape(1); - int numSamples = minValData->shape(0); + auto capacity = static_cast(minLocData->shape(1)); + auto numSamples = static_cast(minValData->shape(0)); long1 valStrides = {minValData->stride(0)}; long2 locStrides = {minLocData->stride(0), minLocData->stride(1)}; long1 numStrides = {numMinData->stride(0)}; @@ -270,7 +438,12 @@ inline void GoldMinMaxLoc(const nvcv::Tensor &minVal, const nvcv::Tensor &minLoc // clang-format off -typedef enum { MIN = 0b01, MAX = 0b10, MIN_MAX = 0b11 } RunChoice; +enum RunChoice +{ + MIN = 0b01, + MAX = 0b10, + MIN_MAX = 0b11 +}; #define NVCV_SHAPE(w, h, n) (int3{w, h, n}) @@ -330,7 +503,7 @@ TYPED_TEST(OpMinMaxLoc, tensor_correct_output) for (int z = 0; z < inShape.z; ++z) for (int y = 0; y < inShape.y; ++y) - for (int x = 0; x < inShape.x; ++x) test::ValueAt(inVec, inStrides, int3{x, y, z}).x = rg(g_rng); + for (int x = 0; x < inShape.x; ++x) test::ValueAt(inVec, inStrides, int3{x, y, z}).x = rg(Rng()); ASSERT_EQ(cudaSuccess, cudaMemcpy(inData->basePtr(), inVec.data(), inBufSize, cudaMemcpyHostToDevice)); @@ -353,11 +526,13 @@ TYPED_TEST(OpMinMaxLoc, tensor_correct_output) switch (run) { case RunChoice::MIN: - EXPECT_NO_THROW(op(stream, in, minVal, minLoc, numMin, nullptr, nullptr, nullptr)); + EXPECT_NO_THROW(op(stream, in, minVal, minLoc, numMin, nvcv::Tensor{nullptr}, nvcv::Tensor{nullptr}, + nvcv::Tensor{nullptr})); break; case RunChoice::MAX: - EXPECT_NO_THROW(op(stream, in, nullptr, nullptr, nullptr, maxVal, maxLoc, numMax)); + EXPECT_NO_THROW(op(stream, in, nvcv::Tensor{nullptr}, nvcv::Tensor{nullptr}, nvcv::Tensor{nullptr}, maxVal, + maxLoc, numMax)); break; case RunChoice::MIN_MAX: @@ -414,12 +589,12 @@ TYPED_TEST(OpMinMaxLoc, varshape_correct_output) uniform_distribution rg(std::is_integral_v ? cuda::TypeTraits::min : InBT{0}, std::is_integral_v ? cuda::TypeTraits::max : InBT{1}); - std::uniform_int_distribution rgW(inShape.x * 0.8, inShape.x * 1.2); - std::uniform_int_distribution rgH(inShape.y * 0.8, inShape.y * 1.2); + std::uniform_int_distribution rgW(ScaledSize(inShape.x, 0.8), ScaledSize(inShape.x, 1.2)); + std::uniform_int_distribution rgH(ScaledSize(inShape.y, 0.8), ScaledSize(inShape.y, 1.2)); for (int z = 0; z < inShape.z; ++z) { - inImg.emplace_back(nvcv::Size2D{rgW(g_rng), rgH(g_rng)}, inFormat); + inImg.emplace_back(nvcv::Size2D{rgW(Rng()), rgH(Rng())}, inFormat); auto inData = inImg[z].exportData(); ASSERT_TRUE(inData); @@ -431,7 +606,7 @@ TYPED_TEST(OpMinMaxLoc, varshape_correct_output) for (int y = 0; y < inShape2[z].y; ++y) for (int x = 0; x < inShape2[z].x; ++x) - test::ValueAt(inVec[z], inStrides[z], int2{x, y}).x = rg(g_rng); + test::ValueAt(inVec[z], inStrides[z], int2{x, y}).x = rg(Rng()); ASSERT_EQ(cudaSuccess, cudaMemcpy2D(inData->plane(0).basePtr, inStrides[z].x, inVec[z].data(), inStrides[z].x, inStrides[z].x, inImg[z].size().h, cudaMemcpyHostToDevice)); @@ -459,11 +634,13 @@ TYPED_TEST(OpMinMaxLoc, varshape_correct_output) switch (run) { case RunChoice::MIN: - EXPECT_NO_THROW(op(stream, in, minVal, minLoc, numMin, nullptr, nullptr, nullptr)); + EXPECT_NO_THROW(op(stream, in, minVal, minLoc, numMin, nvcv::Tensor{nullptr}, nvcv::Tensor{nullptr}, + nvcv::Tensor{nullptr})); break; case RunChoice::MAX: - EXPECT_NO_THROW(op(stream, in, nullptr, nullptr, nullptr, maxVal, maxLoc, numMax)); + EXPECT_NO_THROW(op(stream, in, nvcv::Tensor{nullptr}, nvcv::Tensor{nullptr}, nvcv::Tensor{nullptr}, maxVal, + maxLoc, numMax)); break; case RunChoice::MIN_MAX: @@ -492,6 +669,178 @@ TYPED_TEST(OpMinMaxLoc, varshape_correct_output) } } +// clang-format off +NVCV_TEST_SUITE_P(OpMinMaxLocPlanar, test::ValueList +{ + // width, height, batches, batched + { 17, 13, 3, true}, + { 19, 11, 1, false}, +}); + +// clang-format on + +TEST_P(OpMinMaxLocPlanar, tensor_matches_interleaved) +{ + const int width = GetParamValue<0>(); + const int height = GetParamValue<1>(); + const int batches = GetParamValue<2>(); + const bool batched = GetParamValue<3>(); + const int capacity = width * height; + + nvcv::Tensor srcInterleaved = CreateMinMaxLocParityTensor(batches, width, height, false, batched); + nvcv::Tensor srcPlanar = CreateMinMaxLocParityTensor(batches, width, height, true, batched); + FillMinMaxLocParityInputs(srcInterleaved, srcPlanar, width, height, batches); + + nvcv::Tensor minValInterleaved({{batches}, "N"}, nvcv::TYPE_U32); + nvcv::Tensor minLocInterleaved( + { + {batches, capacity}, + "NM" + }, + nvcv::TYPE_2S32); + nvcv::Tensor numMinInterleaved({{batches}, "N"}, nvcv::TYPE_S32); + nvcv::Tensor maxValInterleaved({{batches}, "N"}, nvcv::TYPE_U32); + nvcv::Tensor maxLocInterleaved( + { + {batches, capacity}, + "NM" + }, + nvcv::TYPE_2S32); + nvcv::Tensor numMaxInterleaved({{batches}, "N"}, nvcv::TYPE_S32); + + nvcv::Tensor minValPlanar({{batches}, "N"}, nvcv::TYPE_U32); + nvcv::Tensor minLocPlanar( + { + {batches, capacity}, + "NM" + }, + nvcv::TYPE_2S32); + nvcv::Tensor numMinPlanar({{batches}, "N"}, nvcv::TYPE_S32); + nvcv::Tensor maxValPlanar({{batches}, "N"}, nvcv::TYPE_U32); + nvcv::Tensor maxLocPlanar( + { + {batches, capacity}, + "NM" + }, + nvcv::TYPE_2S32); + nvcv::Tensor numMaxPlanar({{batches}, "N"}, nvcv::TYPE_S32); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + cvcuda::MinMaxLoc op; + EXPECT_NO_THROW(op(stream, srcInterleaved, minValInterleaved, minLocInterleaved, numMinInterleaved, + maxValInterleaved, maxLocInterleaved, numMaxInterleaved)); + EXPECT_NO_THROW( + op(stream, srcPlanar, minValPlanar, minLocPlanar, numMinPlanar, maxValPlanar, maxLocPlanar, numMaxPlanar)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + + MinMaxParityResults interleaved; + MinMaxParityResults planar; + DownloadMinMaxParityResults(minValInterleaved, minLocInterleaved, numMinInterleaved, maxValInterleaved, + maxLocInterleaved, numMaxInterleaved, interleaved); + DownloadMinMaxParityResults(minValPlanar, minLocPlanar, numMinPlanar, maxValPlanar, maxLocPlanar, numMaxPlanar, + planar); + + EXPECT_EQ(interleaved.minVal, planar.minVal); + EXPECT_EQ(interleaved.minLoc, planar.minLoc); + EXPECT_EQ(interleaved.numMin, planar.numMin); + EXPECT_EQ(interleaved.maxVal, planar.maxVal); + EXPECT_EQ(interleaved.maxLoc, planar.maxLoc); + EXPECT_EQ(interleaved.numMax, planar.numMax); +} + +// NaN must be ignored: min/max are taken over the finite values only. This guards the +// float path's ordered-int atomicMin/Max — a NaN reaching the atomic encodes to an integer +// extreme and would otherwise win min or max. The block-reduce (fminf/fmaxf) drops NaN +// before the atomic, so only finite values are encoded; this test pins that contract. +TEST(OpMinMaxLoc, ignores_nan_float) +{ + constexpr int W = 8; // 32 distinct finite values 0..31; min=0 @(0,0), max=31 @(7,3) + constexpr int H = 4; + constexpr int capacity = 8; + + nvcv::Tensor in = nvcv::util::CreateTensor(1, W, H, nvcv::FMT_F32); + + auto inData = in.exportData(); + ASSERT_TRUE(inData); + auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*inData); + ASSERT_TRUE(inAccess); + + long3 inStrides{inAccess->numRows() * inAccess->rowStride(), inAccess->rowStride(), inAccess->colStride()}; + size_t inBufSize = inStrides.x * inAccess->numSamples(); + + std::vector inVec(inBufSize, uint8_t{0}); + for (int y = 0; y < H; ++y) + for (int x = 0; x < W; ++x) test::ValueAt(inVec, inStrides, int3{x, y, 0}).x = float(y * W + x); + + // Inject a positive quiet NaN at two interior, non-extremal positions. A positive NaN + // encodes to the top of the ordered-int range, so if it were not dropped it would win max. + const float nan = std::numeric_limits::quiet_NaN(); + test::ValueAt(inVec, inStrides, int3{3, 1, 0}).x = nan; + test::ValueAt(inVec, inStrides, int3{5, 2, 0}).x = nan; + + ASSERT_EQ(cudaSuccess, cudaMemcpy(inData->basePtr(), inVec.data(), inBufSize, cudaMemcpyHostToDevice)); + + nvcv::Tensor minVal({{1}, "N"}, nvcv::TYPE_F32); + nvcv::Tensor minLoc( + { + {1, capacity}, + "NM" + }, + nvcv::TYPE_2S32); + nvcv::Tensor numMin({{1}, "N"}, nvcv::TYPE_S32); + nvcv::Tensor maxVal({{1}, "N"}, nvcv::TYPE_F32); + nvcv::Tensor maxLoc( + { + {1, capacity}, + "NM" + }, + nvcv::TYPE_2S32); + nvcv::Tensor numMax({{1}, "N"}, nvcv::TYPE_S32); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + cvcuda::MinMaxLoc op; + EXPECT_NO_THROW(op(stream, in, minVal, minLoc, numMin, maxVal, maxLoc, numMax)); + + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + + auto readScalar = [](const nvcv::Tensor &t, auto &dst) + { + auto data = t.exportData(); + ASSERT_TRUE(data); + ASSERT_EQ(cudaSuccess, cudaMemcpy(&dst, data->basePtr(), sizeof(dst), cudaMemcpyDeviceToHost)); + }; + + float minValHost = 0.f; + float maxValHost = 0.f; + int numMinHost = 0; + int numMaxHost = 0; + int2 minLocHost{-1, -1}; + int2 maxLocHost{-1, -1}; + readScalar(minVal, minValHost); + readScalar(maxVal, maxValHost); + readScalar(numMin, numMinHost); + readScalar(numMax, numMaxHost); + readScalar(minLoc, minLocHost); // first location (capacity-major), the only match + readScalar(maxLoc, maxLocHost); + + EXPECT_FALSE(std::isnan(minValHost)); + EXPECT_FALSE(std::isnan(maxValHost)); + EXPECT_EQ(minValHost, 0.f); + EXPECT_EQ(maxValHost, 31.f); + EXPECT_EQ(numMinHost, 1); + EXPECT_EQ(numMaxHost, 1); + EXPECT_EQ(minLocHost.x, 0); + EXPECT_EQ(minLocHost.y, 0); + EXPECT_EQ(maxLocHost.x, 7); + EXPECT_EQ(maxLocHost.y, 3); +} + TEST(OpMinMaxLoc_Negative, op) { int3 inShape{24, 24, 2}; @@ -530,48 +879,66 @@ TEST(OpMinMaxLoc_Negative, op) cvcuda::MinMaxLoc op; auto runOpMinMaxLocNegativeTest - = [&](nvcv::Tensor in, nvcv::Tensor minVal, nvcv::Tensor minLoc, nvcv::Tensor numMin, nvcv::Tensor maxVal, - nvcv::Tensor maxLoc, nvcv::Tensor numMax) + = [&op, &stream](nvcv::Tensor in, nvcv::Tensor minVal, nvcv::Tensor minLoc, nvcv::Tensor numMin, + nvcv::Tensor maxVal, nvcv::Tensor maxLoc, nvcv::Tensor numMax) { EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, - nvcv::ProtectCall([&] { op(stream, in, minVal, minLoc, numMin, maxVal, maxLoc, numMax); })); + nvcv::ProtectCall([&op, &stream, &in, &minVal, &minLoc, &numMin, &maxVal, &maxLoc, &numMax] + { op(stream, in, minVal, minLoc, numMin, maxVal, maxLoc, numMax); })); }; // cases runOpMinMaxLocNegativeTest(inInvalidSamples, minVal, minLoc, numMin, maxVal, maxLoc, numMax); runOpMinMaxLocNegativeTest(inInvalidChannels, minVal, minLoc, numMin, maxVal, maxLoc, numMax); - runOpMinMaxLocNegativeTest(in, nullptr, minLoc, numMin, maxVal, maxLoc, numMax); - runOpMinMaxLocNegativeTest(in, minVal, nullptr, numMin, maxVal, maxLoc, numMax); - runOpMinMaxLocNegativeTest(in, minVal, minLoc, nullptr, maxVal, maxLoc, numMax); - runOpMinMaxLocNegativeTest(in, minVal, minLoc, numMin, nullptr, maxLoc, numMax); - runOpMinMaxLocNegativeTest(in, minVal, minLoc, numMin, maxVal, nullptr, numMax); - runOpMinMaxLocNegativeTest(in, minVal, minLoc, numMin, maxVal, maxLoc, nullptr); - runOpMinMaxLocNegativeTest(in, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr); - - runOpMinMaxLocNegativeTest(in, valWrongDataType, minLoc, numMin, nullptr, nullptr, nullptr); - runOpMinMaxLocNegativeTest(in, nullptr, nullptr, nullptr, valWrongDataType, maxLoc, numMax); - - runOpMinMaxLocNegativeTest(in, valWrongNumSamples, minLoc, numMin, nullptr, nullptr, nullptr); - runOpMinMaxLocNegativeTest(in, nullptr, nullptr, nullptr, valWrongNumSamples, maxLoc, numMax); - - runOpMinMaxLocNegativeTest(in, valWrongNumChannels, minLoc, numMin, nullptr, nullptr, nullptr); - runOpMinMaxLocNegativeTest(in, nullptr, nullptr, nullptr, valWrongNumChannels, maxLoc, numMax); - - runOpMinMaxLocNegativeTest(in, minVal, locWrongNumSamples, numMin, nullptr, nullptr, nullptr); - runOpMinMaxLocNegativeTest(in, nullptr, nullptr, nullptr, maxVal, locWrongNumSamples, numMax); - - runOpMinMaxLocNegativeTest(in, minVal, locWrongDataType, numMin, nullptr, nullptr, nullptr); - runOpMinMaxLocNegativeTest(in, nullptr, nullptr, nullptr, maxVal, locWrongDataType, numMax); - - runOpMinMaxLocNegativeTest(in, minVal, minLoc, numWrongNumSamples, nullptr, nullptr, nullptr); - runOpMinMaxLocNegativeTest(in, nullptr, nullptr, nullptr, maxVal, maxLoc, numWrongNumSamples); - - runOpMinMaxLocNegativeTest(in, minVal, minLoc, numWrongNumChannels, nullptr, nullptr, nullptr); - runOpMinMaxLocNegativeTest(in, nullptr, nullptr, nullptr, maxVal, maxLoc, numWrongNumChannels); - - runOpMinMaxLocNegativeTest(in, minVal, minLoc, numWrongDataType, nullptr, nullptr, nullptr); - runOpMinMaxLocNegativeTest(in, nullptr, nullptr, nullptr, maxVal, maxLoc, numWrongDataType); + runOpMinMaxLocNegativeTest(in, nvcv::Tensor{nullptr}, minLoc, numMin, maxVal, maxLoc, numMax); + runOpMinMaxLocNegativeTest(in, minVal, nvcv::Tensor{nullptr}, numMin, maxVal, maxLoc, numMax); + runOpMinMaxLocNegativeTest(in, minVal, minLoc, nvcv::Tensor{nullptr}, maxVal, maxLoc, numMax); + runOpMinMaxLocNegativeTest(in, minVal, minLoc, numMin, nvcv::Tensor{nullptr}, maxLoc, numMax); + runOpMinMaxLocNegativeTest(in, minVal, minLoc, numMin, maxVal, nvcv::Tensor{nullptr}, numMax); + runOpMinMaxLocNegativeTest(in, minVal, minLoc, numMin, maxVal, maxLoc, nvcv::Tensor{nullptr}); + runOpMinMaxLocNegativeTest(in, nvcv::Tensor{nullptr}, nvcv::Tensor{nullptr}, nvcv::Tensor{nullptr}, + nvcv::Tensor{nullptr}, nvcv::Tensor{nullptr}, nvcv::Tensor{nullptr}); + + runOpMinMaxLocNegativeTest(in, valWrongDataType, minLoc, numMin, nvcv::Tensor{nullptr}, nvcv::Tensor{nullptr}, + nvcv::Tensor{nullptr}); + runOpMinMaxLocNegativeTest(in, nvcv::Tensor{nullptr}, nvcv::Tensor{nullptr}, nvcv::Tensor{nullptr}, + valWrongDataType, maxLoc, numMax); + + runOpMinMaxLocNegativeTest(in, valWrongNumSamples, minLoc, numMin, nvcv::Tensor{nullptr}, nvcv::Tensor{nullptr}, + nvcv::Tensor{nullptr}); + runOpMinMaxLocNegativeTest(in, nvcv::Tensor{nullptr}, nvcv::Tensor{nullptr}, nvcv::Tensor{nullptr}, + valWrongNumSamples, maxLoc, numMax); + + runOpMinMaxLocNegativeTest(in, valWrongNumChannels, minLoc, numMin, nvcv::Tensor{nullptr}, nvcv::Tensor{nullptr}, + nvcv::Tensor{nullptr}); + runOpMinMaxLocNegativeTest(in, nvcv::Tensor{nullptr}, nvcv::Tensor{nullptr}, nvcv::Tensor{nullptr}, + valWrongNumChannels, maxLoc, numMax); + + runOpMinMaxLocNegativeTest(in, minVal, locWrongNumSamples, numMin, nvcv::Tensor{nullptr}, nvcv::Tensor{nullptr}, + nvcv::Tensor{nullptr}); + runOpMinMaxLocNegativeTest(in, nvcv::Tensor{nullptr}, nvcv::Tensor{nullptr}, nvcv::Tensor{nullptr}, maxVal, + locWrongNumSamples, numMax); + + runOpMinMaxLocNegativeTest(in, minVal, locWrongDataType, numMin, nvcv::Tensor{nullptr}, nvcv::Tensor{nullptr}, + nvcv::Tensor{nullptr}); + runOpMinMaxLocNegativeTest(in, nvcv::Tensor{nullptr}, nvcv::Tensor{nullptr}, nvcv::Tensor{nullptr}, maxVal, + locWrongDataType, numMax); + + runOpMinMaxLocNegativeTest(in, minVal, minLoc, numWrongNumSamples, nvcv::Tensor{nullptr}, nvcv::Tensor{nullptr}, + nvcv::Tensor{nullptr}); + runOpMinMaxLocNegativeTest(in, nvcv::Tensor{nullptr}, nvcv::Tensor{nullptr}, nvcv::Tensor{nullptr}, maxVal, maxLoc, + numWrongNumSamples); + + runOpMinMaxLocNegativeTest(in, minVal, minLoc, numWrongNumChannels, nvcv::Tensor{nullptr}, nvcv::Tensor{nullptr}, + nvcv::Tensor{nullptr}); + runOpMinMaxLocNegativeTest(in, nvcv::Tensor{nullptr}, nvcv::Tensor{nullptr}, nvcv::Tensor{nullptr}, maxVal, maxLoc, + numWrongNumChannels); + + runOpMinMaxLocNegativeTest(in, minVal, minLoc, numWrongDataType, nvcv::Tensor{nullptr}, nvcv::Tensor{nullptr}, + nvcv::Tensor{nullptr}); + runOpMinMaxLocNegativeTest(in, nvcv::Tensor{nullptr}, nvcv::Tensor{nullptr}, nvcv::Tensor{nullptr}, maxVal, maxLoc, + numWrongDataType); ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); @@ -607,11 +974,14 @@ TEST(OpMinMaxLoc_Negative, varshape_invalid_plane) ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); cvcuda::MinMaxLoc op; - EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, - nvcv::ProtectCall([&] { op(stream, in, minVal, minLoc, numMin, nullptr, nullptr, nullptr); })); - char msg[1024]; - nvcvGetLastErrorMessage(msg, sizeof(msg)); - std::cout << "\033[33m" << msg << "\033[0m" << std::endl; + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall( + [&op, &stream, &in, &minVal, &minLoc, &numMin] { + op(stream, in, minVal, minLoc, numMin, nvcv::Tensor{nullptr}, + nvcv::Tensor{nullptr}, nvcv::Tensor{nullptr}); + })); + std::array msg; + nvcvGetLastErrorMessage(msg.data(), msg.size()); + std::cout << "\033[33m" << msg.data() << "\033[0m" << std::endl; ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); diff --git a/tests/cvcuda/system/TestOpMorphology.cpp b/tests/cvcuda/system/TestOpMorphology.cpp index 5504f31c9..686d099ab 100644 --- a/tests/cvcuda/system/TestOpMorphology.cpp +++ b/tests/cvcuda/system/TestOpMorphology.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "ConvUtils.hpp" #include "Definitions.hpp" +#include "PlanarParityUtils.hpp" #include #include @@ -27,13 +28,39 @@ #include #include +#include +#include #include +#include namespace test = nvcv::test; namespace cuda = nvcv::cuda; using uchar = unsigned char; +namespace { + +bool NeedsWorkspace(NVCVMorphologyType morphType, int iteration) +{ + return morphType == NVCVMorphologyType::NVCV_OPEN || morphType == NVCVMorphologyType::NVCV_CLOSE || iteration > 1; +} + +} // namespace + +class MorphologyTestError : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + +template +using TestImage = std::array, rows>; + +static int ScaledSize(int size, double scale) +{ + return static_cast(size * scale); +} + static void hostMorphDilateErode(std::vector &hDst, const long3 &dstStrides, const std::vector &hSrc, const long3 &srcStrides, const int3 &shape, const nvcv::ImageFormat &format, const nvcv::Size2D &kernelSize, int2 &kernelAnchor, int iterations, @@ -98,11 +125,26 @@ static void hostMorph(std::vector &hDst, const long3 &dstStrides, const } default: - throw std::runtime_error("Unsupported morph type"); + throw MorphologyTestError("Unsupported morph type"); break; } } +// checks pixels only in the logical image region. +template +static bool imagePixelValuesSame(nvcv::util::TensorImageData &a, nvcv::util::TensorImageData &b, int x, int y) +{ + for (int c = 0; c < a.numC(); ++c) + { + if (*a.item(x, y, c) != *b.item(x, y, c)) + { + return false; + } + } + + return true; +} + // checks pixels only in the logical image region. template static bool imageRegionValuesSame(nvcv::util::TensorImageData &a, nvcv::util::TensorImageData &b) @@ -114,45 +156,82 @@ static bool imageRegionValuesSame(nvcv::util::TensorImageData &a, nvcv::util::Te return false; for (int x = 0; x < minWidth; ++x) + { for (int y = 0; y < minHeight; ++y) - for (int c = 0; c < a.numC(); ++c) - if (*a.item(x, y, c) != *b.item(x, y, c)) - return false; + { + if (!imagePixelValuesSame(a, b, x, y)) + { + return false; + } + } + } return true; } template -void SetTensorToTestVector(const uchar inputVals[rows][cols], int width, int height, nvcv::Tensor &tensor, int sample) +void SetPixelToTestVector(const TestImage &inputVals, nvcv::util::TensorImageData &data, int x, int y) +{ + for (int c = 0; c < data.numC(); ++c) + { + *data.item(x, y, c) = (T)inputVals[y][x]; + } +} + +template +void SetTensorToTestVector(const TestImage &inputVals, int width, int height, const nvcv::Tensor &tensor, + int sample) { nvcv::util::TensorImageData data(tensor.exportData(), sample); for (int x = 0; x < width; ++x) + { for (int y = 0; y < height; ++y) - for (int c = 0; c < data.numC(); ++c) *data.item(x, y, c) = (T)inputVals[y][x]; + { + SetPixelToTestVector(inputVals, data, x, y); + } + } EXPECT_NO_THROW(nvcv::util::SetTensorFromVector(tensor.exportData(), data.getVector(), sample)); } template -bool MatchTensorToTestVector(const uchar checkVals[rows][cols], int width, int height, nvcv::Tensor &Tensor, int sample) +bool PixelMatchesTestVector(const TestImage &checkVals, nvcv::util::TensorImageData &data, int x, int y) +{ + for (int c = 0; c < data.numC(); ++c) + { + if (*data.item(x, y, c) != (T)checkVals[y][x]) + { + return false; + } + } + + return true; +} + +template +bool MatchTensorToTestVector(const TestImage &checkVals, int width, int height, const nvcv::Tensor &tensor, + int sample) { - nvcv::util::TensorImageData data(Tensor.exportData(), sample); + nvcv::util::TensorImageData data(tensor.exportData(), sample); for (int x = 0; x < width; ++x) + { for (int y = 0; y < height; ++y) - for (int c = 0; c < data.numC(); ++c) - if (*data.item(x, y, c) != (T)checkVals[y][x]) - { - return false; - } + { + if (!PixelMatchesTestVector(checkVals, data, x, y)) + { + return false; + } + } + } return true; } template void checkTestVectors(cudaStream_t &stream, nvcv::Tensor &inTensor, nvcv::Tensor &outTensor, - nvcv::OptionalTensorConstRef workspace, const uchar input[rows][cols], - const uchar output[rows][cols], int width, int height, const nvcv::Size2D &maskSize, + nvcv::OptionalTensorConstRef workspace, const TestImage &input, + const TestImage &output, int width, int height, const nvcv::Size2D &maskSize, const int2 &anchor, int iteration, NVCVMorphologyType type, NVCVBorderType borderMode, int batches) { @@ -165,13 +244,13 @@ void checkTestVectors(cudaStream_t &stream, nvcv::Tensor &inTensor, nvcv::Tensor morphOp(stream, inTensor, outTensor, workspace, type, maskSize, anchor, iteration, borderMode); if (cudaSuccess != cudaStreamSynchronize(stream)) - throw std::runtime_error("Cuda Sync failed"); + throw MorphologyTestError("Cuda Sync failed"); for (int i = 0; i < batches; ++i) { if (MatchTensorToTestVector(output, width, height, outTensor, i) != true) { - throw std::runtime_error("Op returned unexpected result"); + throw MorphologyTestError("Op returned unexpected result"); } } } @@ -191,7 +270,7 @@ TEST(OpMorphology, morph_check_dilate_kernel) nvcv::Tensor outTensor = nvcv::util::CreateTensor(batches, width, height, format); nvcv::Tensor workspaceTensor = nvcv::util::CreateTensor(batches, width, height, format); - int2 anchor(-1, -1); + int2 anchor{-1, -1}; nvcv::Size2D maskSize(3, 3); int iteration = 1; NVCVMorphologyType type = NVCVMorphologyType::NVCV_DILATE; @@ -199,75 +278,75 @@ TEST(OpMorphology, morph_check_dilate_kernel) { // clang-format off - uchar inImg[height][width] ={ + const TestImage inImg = {{ {0,0,0,0,0}, {0,0,0,0,0}, {0,0,1,0,0}, {0,0,0,0,0}, {0,0,0,0,0} - }; + }}; - uchar expImg[height][width] ={ + const TestImage expImg = {{ {0,0,0,0,0}, {0,1,1,1,0}, {0,1,1,1,0}, {0,1,1,1,0}, {0,0,0,0,0} - }; + }}; // clang-format on - EXPECT_NO_THROW( - (checkTestVectors(stream, inTensor, outTensor, nvcv::NullOpt, inImg, expImg, width, - height, maskSize, anchor, iteration, type, borderMode, batches))); + EXPECT_NO_THROW((checkTestVectors( + stream, inTensor, outTensor, nvcv::OptionalTensorConstRef{nvcv::NullOpt}, inImg, expImg, width, height, + maskSize, anchor, iteration, type, borderMode, batches))); } // iteration = 2 { // clang-format off iteration = 2; - uchar inImg[height][width] ={ + const TestImage inImg = {{ {0,0,0,0,0}, {0,0,0,0,0}, {0,0,1,0,0}, {0,0,0,0,0}, {0,0,0,0,0} - }; + }}; - uchar expImg[height][width] ={ + const TestImage expImg = {{ {1,1,1,1,1}, {1,1,1,1,1}, {1,1,1,1,1}, {1,1,1,1,1}, {1,1,1,1,1} - }; + }}; // clang-format on - EXPECT_NO_THROW( - (checkTestVectors(stream, inTensor, outTensor, workspaceTensor, inImg, expImg, width, - height, maskSize, anchor, iteration, type, borderMode, batches))); + EXPECT_NO_THROW((checkTestVectors( + stream, inTensor, outTensor, nvcv::OptionalTensorConstRef{workspaceTensor}, inImg, expImg, width, height, + maskSize, anchor, iteration, type, borderMode, batches))); } { // overlap iteration = 1; // clang-format off - uchar inImg[height][width] ={ + const TestImage inImg = {{ {1,0,0,0,2}, {0,0,0,0,0}, {0,0,5,0,0}, {0,0,0,0,0}, {4,0,0,0,3} - }; + }}; - uchar expImg[height][width] ={ + const TestImage expImg = {{ {1,1,0,2,2}, {1,5,5,5,2}, {0,5,5,5,0}, {4,5,5,5,3}, {4,4,0,3,3} - }; + }}; // clang-format on - EXPECT_NO_THROW( - (checkTestVectors(stream, inTensor, outTensor, nvcv::NullOpt, inImg, expImg, width, - height, maskSize, anchor, iteration, type, borderMode, batches))); + EXPECT_NO_THROW((checkTestVectors( + stream, inTensor, outTensor, nvcv::OptionalTensorConstRef{nvcv::NullOpt}, inImg, expImg, width, height, + maskSize, anchor, iteration, type, borderMode, batches))); } { @@ -276,25 +355,25 @@ TEST(OpMorphology, morph_check_dilate_kernel) // clang-format off maskSize.w = 1; maskSize.h = 2; - uchar inImg[height][width] ={ + const TestImage inImg = {{ {1,0,0,0,2}, {0,0,0,0,0}, {0,0,5,0,0}, {0,0,0,0,0}, {4,0,0,0,3} - }; + }}; - uchar expImg[height][width] ={ + const TestImage expImg = {{ {1,0,0,0,2}, {1,0,0,0,2}, {0,0,5,0,0}, {0,0,5,0,0}, {4,0,0,0,3} - }; + }}; // clang-format on - EXPECT_NO_THROW( - (checkTestVectors(stream, inTensor, outTensor, nvcv::NullOpt, inImg, expImg, width, - height, maskSize, anchor, iteration, type, borderMode, batches))); + EXPECT_NO_THROW((checkTestVectors( + stream, inTensor, outTensor, nvcv::OptionalTensorConstRef{nvcv::NullOpt}, inImg, expImg, width, height, + maskSize, anchor, iteration, type, borderMode, batches))); maskSize.w = 3; maskSize.h = 3; } @@ -306,25 +385,25 @@ TEST(OpMorphology, morph_check_dilate_kernel) anchor.x = 0; anchor.y = 0; - uchar inImg[height][width] ={ + const TestImage inImg = {{ {0,0,0,0,0}, {0,0,0,0,0}, {0,0,1,0,0}, {0,0,0,0,0}, {0,0,0,0,0} - }; + }}; - uchar expImg[height][width] ={ + const TestImage expImg = {{ {1,1,1,0,0}, {1,1,1,0,0}, {1,1,1,0,0}, {0,0,0,0,0}, {0,0,0,0,0} - }; + }}; // clang-format on - EXPECT_NO_THROW( - (checkTestVectors(stream, inTensor, outTensor, nvcv::NullOpt, inImg, expImg, width, - height, maskSize, anchor, iteration, type, borderMode, batches))); + EXPECT_NO_THROW((checkTestVectors( + stream, inTensor, outTensor, nvcv::OptionalTensorConstRef{nvcv::NullOpt}, inImg, expImg, width, height, + maskSize, anchor, iteration, type, borderMode, batches))); anchor.x = -1; anchor.y = -1; } @@ -346,7 +425,7 @@ TEST(OpMorphology, morph_check_erode_kernel) nvcv::Tensor inTensor = nvcv::util::CreateTensor(batches, width, height, format); nvcv::Tensor outTensor = nvcv::util::CreateTensor(batches, width, height, format); - int2 anchor(-1, -1); + int2 anchor{-1, -1}; nvcv::Size2D maskSize(3, 3); int iteration = 1; NVCVMorphologyType type = NVCVMorphologyType::NVCV_ERODE; @@ -354,25 +433,25 @@ TEST(OpMorphology, morph_check_erode_kernel) { // clang-format off - uchar inImg[height][width] ={ + const TestImage inImg = {{ {0,0,0,0,0}, {0,1,1,1,0}, {0,1,1,1,0}, {0,1,1,1,0}, {0,0,0,0,0} - }; + }}; - uchar expImg[height][width] ={ + const TestImage expImg = {{ {0,0,0,0,0}, {0,0,0,0,0}, {0,0,1,0,0}, {0,0,0,0,0}, {0,0,0,0,0} - }; + }}; // clang-format on - EXPECT_NO_THROW( - (checkTestVectors(stream, inTensor, outTensor, nvcv::NullOpt, inImg, expImg, width, - height, maskSize, anchor, iteration, type, borderMode, batches))); + EXPECT_NO_THROW((checkTestVectors( + stream, inTensor, outTensor, nvcv::OptionalTensorConstRef{nvcv::NullOpt}, inImg, expImg, width, height, + maskSize, anchor, iteration, type, borderMode, batches))); } ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); @@ -392,7 +471,7 @@ TEST(OpMorphology, morph_check_dilate_kernel_even) nvcv::Tensor inTensor = nvcv::util::CreateTensor(batches, width, height, format); nvcv::Tensor outTensor = nvcv::util::CreateTensor(batches, width, height, format); - int2 anchor(-1, -1); + int2 anchor{-1, -1}; nvcv::Size2D maskSize(3, 3); int iteration = 1; NVCVMorphologyType type = NVCVMorphologyType::NVCV_DILATE; @@ -400,25 +479,25 @@ TEST(OpMorphology, morph_check_dilate_kernel_even) { // clang-format off - uchar inImg[height][width] ={ + const TestImage inImg = {{ {1,0,0,0,0,2}, {0,0,0,0,0,0}, {0,0,5,0,0,0}, {0,0,0,0,0,0}, {0,0,0,0,0,0}, {4,0,0,0,0,3} - }; + }}; - uchar expImg[height][width] ={ + const TestImage expImg = {{ {1,1,0,0,2,2}, {1,5,5,5,2,2}, {0,5,5,5,0,0}, {0,5,5,5,0,0}, {4,4,0,0,3,3}, {4,4,0,0,3,3} - }; + }}; - EXPECT_NO_THROW((checkTestVectors(stream, inTensor, outTensor, nvcv::NullOpt, inImg, expImg, width, height, maskSize,anchor,iteration, type, borderMode, batches))); + EXPECT_NO_THROW((checkTestVectors(stream, inTensor, outTensor, nvcv::OptionalTensorConstRef{nvcv::NullOpt}, inImg, expImg, width, height, maskSize,anchor,iteration, type, borderMode, batches))); } ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); } @@ -490,13 +569,13 @@ TEST_P(OpMorphology, morph_noop) EXPECT_NO_THROW(nvcv::util::SetTensorTo(outTensor.exportData(), 0)); cvcuda::Morphology morphOp; - int2 anchor(0, 0); + int2 anchor{0, 0}; nvcv::Size2D maskSize(1, 1); int iteration = 0; - EXPECT_NO_THROW( - morphOp(stream, inTensor, outTensor, nvcv::NullOpt, morphType, maskSize, anchor, iteration, borderMode)); + EXPECT_NO_THROW(morphOp(stream, inTensor, outTensor, nvcv::OptionalTensorConstRef{nvcv::NullOpt}, morphType, + maskSize, anchor, iteration, borderMode)); ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); for (int i = 0; i < batches; ++i) @@ -563,17 +642,17 @@ TEST_P(OpMorphology, morph_random) std::default_random_engine randEng(0); std::uniform_int_distribution rand(0u, 255u); - std::generate(inVec.begin(), inVec.end(), [&]() { return rand(randEng); }); + std::ranges::generate(inVec, [&rand, &randEng]() { return rand(randEng); }); // copy random input to device ASSERT_EQ(cudaSuccess, cudaMemcpy(inData->basePtr(), inVec.data(), inBufSize, cudaMemcpyHostToDevice)); // run operator cvcuda::Morphology morphOp; - int2 anchor(-1, -1); + int2 anchor{-1, -1}; - EXPECT_NO_THROW( - morphOp(stream, inTensor, outTensor, workspaceTensor, morphType, maskSize, anchor, iteration, borderMode)); + EXPECT_NO_THROW(morphOp(stream, inTensor, outTensor, nvcv::OptionalTensorConstRef{workspaceTensor}, morphType, + maskSize, anchor, iteration, borderMode)); ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); @@ -597,6 +676,117 @@ TEST_P(OpMorphology, morph_random) EXPECT_EQ(testVec, goldVec); } +// Planar (NCHW/CHW) layout support + +// Parameters: width, height, maskWidth, maskHeight, borderMode, morphType, iteration, numImages, planarFmt, +// interleavedFmt +// clang-format off +NVCV_TEST_SUITE_P(OpMorphologyPlanar, + test::ValueList{ + {64, 48, 3, 3, NVCV_BORDER_CONSTANT, NVCV_ERODE, 1, 2, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + {35, 29, 3, 3, NVCV_BORDER_REPLICATE, NVCV_DILATE, 1, 1, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + {37, 31, 5, 3, NVCV_BORDER_REFLECT, NVCV_DILATE, 1, 1, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + {39, 33, 5, 3, NVCV_BORDER_REFLECT101, NVCV_ERODE, 2, 1, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + {61, 47, 5, 3, NVCV_BORDER_REFLECT, NVCV_DILATE, 2, 1, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + {34, 30, 3, 3, NVCV_BORDER_CONSTANT, NVCV_CLOSE, 1, 1, nvcv::FMT_RGBAf32p, nvcv::FMT_RGBAf32}, + {36, 32, 5, 3, NVCV_BORDER_REFLECT, NVCV_DILATE, 2, 1, nvcv::FMT_RGBf32p, nvcv::FMT_RGBf32}, + {38, 34, 5, 3, NVCV_BORDER_REPLICATE, NVCV_ERODE, 2, 1, nvcv::FMT_RGBf32p, nvcv::FMT_RGBf32}, + {32, 28, 3, 5, NVCV_BORDER_REFLECT101, NVCV_CLOSE, 1, 1, nvcv::FMT_RGBAf32p, nvcv::FMT_RGBAf32}, +}); + +// clang-format on + +TEST_P(OpMorphologyPlanar, tensor_matches_interleaved) +{ + const int width = GetParamValue<0>(); + const int height = GetParamValue<1>(); + const nvcv::Size2D maskSize{GetParamValue<2>(), GetParamValue<3>()}; + const NVCVBorderType borderMode = GetParamValue<4>(); + const NVCVMorphologyType morphType = GetParamValue<5>(); + const int iteration = GetParamValue<6>(); + const int numImages = GetParamValue<7>(); + const nvcv::ImageFormat planarFmt = GetParamValue<8>(); + const nvcv::ImageFormat interleavedFmt = GetParamValue<9>(); + const int2 anchor{-1, -1}; + + test::planar::RunTensorParity( + planarFmt, interleavedFmt, width, height, width, height, numImages, + [width, height, maskSize, borderMode, morphType, iteration, numImages, anchor]( + cudaStream_t stream, const nvcv::Tensor &src, const nvcv::Tensor &dst, nvcv::ImageFormat fmt) + { + nvcv::Tensor workspace{nullptr}; + nvcv::OptionalTensorConstRef workspaceRef{nvcv::NullOpt}; + if (NeedsWorkspace(morphType, iteration)) + { + workspace = nvcv::util::CreateTensor(numImages, width, height, fmt); + workspaceRef = nvcv::OptionalTensorConstRef{std::cref(workspace)}; + } + + cvcuda::Morphology op; + EXPECT_NO_THROW(op(stream, src, dst, workspaceRef, morphType, maskSize, anchor, iteration, borderMode)); + }); +} + +TEST_P(OpMorphologyPlanar, varshape_matches_interleaved) +{ + const int width = GetParamValue<0>(); + const int height = GetParamValue<1>(); + const int2 maskSize{GetParamValue<2>(), GetParamValue<3>()}; + const NVCVBorderType borderMode = GetParamValue<4>(); + const NVCVMorphologyType morphType = GetParamValue<5>(); + const int iteration = GetParamValue<6>(); + const int numImages = GetParamValue<7>(); + const nvcv::ImageFormat planarFmt = GetParamValue<8>(); + const nvcv::ImageFormat interleavedFmt = GetParamValue<9>(); + const int2 anchor{-1, -1}; + + test::planar::RunVarShapeParity( + planarFmt, interleavedFmt, width, height, width, height, numImages, + [maskSize, borderMode, morphType, iteration, numImages, anchor]( + cudaStream_t stream, const nvcv::ImageBatchVarShape &src, const nvcv::ImageBatchVarShape &dst, + nvcv::ImageFormat fmt) + { + auto masks = test::planar::MakePerImageTensor(numImages, nvcv::TYPE_2S32, maskSize); + auto anchors = test::planar::MakePerImageTensor(numImages, nvcv::TYPE_2S32, anchor); + + nvcv::ImageBatchVarShape workspace{nullptr}; + std::vector workspaceImages; + nvcv::OptionalImageBatchVarShapeConstRef workspaceRef{nvcv::NullOpt}; + if (NeedsWorkspace(morphType, iteration)) + { + workspace = nvcv::ImageBatchVarShape(numImages); + for (int i = 0; i < numImages; ++i) + { + workspaceImages.emplace_back(src[i].size(), fmt); + } + workspace.pushBack(workspaceImages.begin(), workspaceImages.end()); + workspaceRef = nvcv::OptionalImageBatchVarShapeConstRef{std::cref(workspace)}; + } + + cvcuda::Morphology op; + EXPECT_NO_THROW(op(stream, src, dst, workspaceRef, morphType, masks, anchors, iteration, borderMode)); + }); +} + +TEST(OpMorphologyPlanar, tensor_rejects_two_channel) +{ + const nvcv::Size2D maskSize{3, 3}; + const int2 anchor{-1, -1}; + const int iteration = 1; + const NVCVMorphologyType morphType = NVCVMorphologyType::NVCV_ERODE; + const NVCVBorderType border = NVCV_BORDER_CONSTANT; + + test::planar::ExpectPlanarTensorRejected( + {1, 2, 16, 16}, {1, 2, 16, 16}, + [maskSize, anchor](cudaStream_t stream, const nvcv::Tensor &src, const nvcv::Tensor &dst) + { + cvcuda::Morphology op; + op(stream, src, dst, nvcv::OptionalTensorConstRef{nvcv::NullOpt}, morphType, maskSize, anchor, iteration, + border); + }); +} + // clang-format off NVCV_TEST_SUITE_P(OpMorphologyVarShape, test::ValueList { @@ -659,9 +849,9 @@ TEST_P(OpMorphologyVarShape, varshape_correct_output) int iteration = GetParamValue<8>(); // Create input varshape - std::default_random_engine rng; - std::uniform_int_distribution udistWidth(width * 0.8, width * 1.1); - std::uniform_int_distribution udistHeight(height * 0.8, height * 1.1); + std::default_random_engine rng; + std::uniform_int_distribution udistWidth(ScaledSize(width, 0.8), ScaledSize(width, 1.1)); + std::uniform_int_distribution udistHeight(ScaledSize(height, 0.8), ScaledSize(height, 1.1)); std::vector imgSrc; @@ -679,7 +869,7 @@ TEST_P(OpMorphologyVarShape, varshape_correct_output) std::uniform_int_distribution udist(0, 255); srcVec[i].resize(imgSrc[i].size().h * srcRowStride); - std::generate(srcVec[i].begin(), srcVec[i].end(), [&]() { return udist(rng); }); + std::ranges::generate(srcVec[i], [&udist, &rng]() { return udist(rng); }); auto imgData = imgSrc[i].exportData(); ASSERT_NE(imgData, nvcv::NullOpt); @@ -734,8 +924,8 @@ TEST_P(OpMorphologyVarShape, varshape_correct_output) // Run operator set the max batches cvcuda::Morphology morphOp; - EXPECT_NO_THROW(morphOp(stream, batchSrc, batchDst, batchWorkspace, morphType, maskTensor, anchorTensor, iteration, - borderMode)); + EXPECT_NO_THROW(morphOp(stream, batchSrc, batchDst, nvcv::OptionalImageBatchVarShapeConstRef{batchWorkspace}, + morphType, maskTensor, anchorTensor, iteration, borderMode)); ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); @@ -807,9 +997,9 @@ TEST_P(OpMorphologyVarShape, varshape_noop) return; // Create input varshape - std::default_random_engine rng; - std::uniform_int_distribution udistWidth(width * 0.8, width * 1.1); - std::uniform_int_distribution udistHeight(height * 0.8, height * 1.1); + std::default_random_engine rng; + std::uniform_int_distribution udistWidth(ScaledSize(width, 0.8), ScaledSize(width, 1.1)); + std::uniform_int_distribution udistHeight(ScaledSize(height, 0.8), ScaledSize(height, 1.1)); std::vector imgSrc; @@ -827,7 +1017,7 @@ TEST_P(OpMorphologyVarShape, varshape_noop) std::uniform_int_distribution udist(0, 255); srcVec[i].resize(imgSrc[i].size().h * srcRowStride); - std::generate(srcVec[i].begin(), srcVec[i].end(), [&]() { return udist(rng); }); + std::ranges::generate(srcVec[i], [&udist, &rng]() { return udist(rng); }); auto imgData = imgSrc[i].exportData(); ASSERT_NE(imgData, nvcv::NullOpt); @@ -877,8 +1067,8 @@ TEST_P(OpMorphologyVarShape, varshape_noop) // Run operator set the max batches cvcuda::Morphology morphOp; - EXPECT_NO_THROW( - morphOp(stream, batchSrc, batchDst, nvcv::NullOpt, morphType, maskTensor, anchorTensor, iteration, borderMode)); + EXPECT_NO_THROW(morphOp(stream, batchSrc, batchDst, nvcv::OptionalImageBatchVarShapeConstRef{nvcv::NullOpt}, + morphType, maskTensor, anchorTensor, iteration, borderMode)); ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); @@ -929,27 +1119,30 @@ TEST(OpMorphology_Negative, operator_negative) nvcv::Tensor outTensor = nvcv::util::CreateTensor(1, 24, 24, format); cvcuda::Morphology morphOp; - int2 anchor(0, 0); + int2 anchor{0, 0}; nvcv::Size2D maskSize(1, 1); // testSet0: iteration < 0 - EXPECT_THROW(morphOp(nullptr, inTensor, outTensor, nvcv::NullOpt, NVCV_ERODE, maskSize, anchor, -1, borderMode), + EXPECT_THROW(morphOp(nullptr, inTensor, outTensor, nvcv::OptionalTensorConstRef{nvcv::NullOpt}, NVCV_ERODE, + maskSize, anchor, -1, borderMode), nvcv::Exception); // testSet1: NVCV_DILATE and NVCV_ERODE && iteration > 1 && null workspace std::vector testSet1{NVCV_DILATE, NVCV_ERODE}; - for (auto morphType : testSet1) + for (auto morphType : testSet1) // NOSONAR { - EXPECT_THROW(morphOp(nullptr, inTensor, outTensor, nvcv::NullOpt, morphType, maskSize, anchor, 2, borderMode), + EXPECT_THROW(morphOp(nullptr, inTensor, outTensor, nvcv::OptionalTensorConstRef{nvcv::NullOpt}, morphType, + maskSize, anchor, 2, borderMode), nvcv::Exception); } // testSet2: NVCV_CLOSE and NVCV_OPEN && null workspace std::vector testSet2{NVCV_CLOSE, NVCV_OPEN}; - for (auto morphType : testSet2) + for (auto morphType : testSet2) // NOSONAR { - EXPECT_THROW(morphOp(nullptr, inTensor, outTensor, nvcv::NullOpt, morphType, maskSize, anchor, 1, borderMode), + EXPECT_THROW(morphOp(nullptr, inTensor, outTensor, nvcv::OptionalTensorConstRef{nvcv::NullOpt}, morphType, + maskSize, anchor, 1, borderMode), nvcv::Exception); } @@ -959,44 +1152,37 @@ TEST(OpMorphology_Negative, operator_negative) = nvcv::util::CreateTensor(1, 24, 24, nvcv::ImageFormat{NVCV_IMAGE_FORMAT_RGBAf16}); nvcv::Tensor outTensorInvalid = nvcv::util::CreateTensor(1, 24, 24, nvcv::ImageFormat{NVCV_IMAGE_FORMAT_RGBAf16}); - EXPECT_THROW(morphOp(nullptr, inTensorInvalid, outTensorInvalid, nvcv::NullOpt, NVCV_ERODE, maskSize, anchor, 0, - borderMode), + EXPECT_THROW(morphOp(nullptr, inTensorInvalid, outTensorInvalid, nvcv::OptionalTensorConstRef{nvcv::NullOpt}, + NVCV_ERODE, maskSize, anchor, 0, borderMode), nvcv::Exception); } // testSet4: input format is not equal to output format { nvcv::Tensor outTensorInvalid = nvcv::util::CreateTensor(2, 24, 24, format); - EXPECT_THROW( - morphOp(nullptr, inTensor, outTensorInvalid, nvcv::NullOpt, NVCV_ERODE, maskSize, anchor, 0, borderMode), - nvcv::Exception); + EXPECT_THROW(morphOp(nullptr, inTensor, outTensorInvalid, nvcv::OptionalTensorConstRef{nvcv::NullOpt}, + NVCV_ERODE, maskSize, anchor, 0, borderMode), + nvcv::Exception); } #ifndef ENABLE_SANITIZER // testSet: invalid morph type - EXPECT_THROW(morphOp(nullptr, inTensor, outTensor, nvcv::NullOpt, static_cast(255), maskSize, - anchor, 0, borderMode), + EXPECT_THROW(morphOp(nullptr, inTensor, outTensor, nvcv::OptionalTensorConstRef{nvcv::NullOpt}, + static_cast(255), maskSize, anchor, 0, borderMode), nvcv::Exception); // testSet: invalid border mode - EXPECT_THROW(morphOp(nullptr, inTensor, outTensor, nvcv::NullOpt, NVCV_ERODE, maskSize, anchor, 0, - static_cast(255)), + EXPECT_THROW(morphOp(nullptr, inTensor, outTensor, nvcv::OptionalTensorConstRef{nvcv::NullOpt}, NVCV_ERODE, + maskSize, anchor, 0, static_cast(255)), nvcv::Exception); #endif - // testSet: invalid format - { - nvcv::Tensor inTensorInvalid = nvcv::util::CreateTensor(1, 24, 24, nvcv::FMT_RGB8p); - nvcv::Tensor outTensorInvalid = nvcv::util::CreateTensor(1, 24, 24, nvcv::FMT_RGB8p); - EXPECT_THROW(morphOp(nullptr, inTensorInvalid, outTensorInvalid, nvcv::NullOpt, NVCV_ERODE, maskSize, anchor, 0, - borderMode), - nvcv::Exception); - } + // testSet: unsupported 2-channel planar format { #define NVCV_IMAGE_FORMAT_2U8 NVCV_DETAIL_MAKE_NONCOLOR_FMT1(PL, UNSIGNED, XY00, ASSOCIATED, X8_Y8) nvcv::ImageFormat formatInvalid{NVCV_IMAGE_FORMAT_2U8}; nvcv::Tensor inTensorInvalid = nvcv::util::CreateTensor(1, 24, 24, formatInvalid); nvcv::Tensor outTensorInvalid = nvcv::util::CreateTensor(1, 24, 24, formatInvalid); - EXPECT_THROW(morphOp(nullptr, inTensorInvalid, outTensorInvalid, nvcv::NullOpt, NVCV_ERODE, maskSize, anchor, 0, - borderMode), + EXPECT_THROW(morphOp(nullptr, inTensorInvalid, outTensorInvalid, nvcv::OptionalTensorConstRef{nvcv::NullOpt}, + NVCV_ERODE, maskSize, anchor, 0, borderMode), nvcv::Exception); #undef NVCV_IMAGE_FORMAT_2U8 } @@ -1055,26 +1241,26 @@ TEST(OpMorphology_Negative, operator_varshape_negative) cvcuda::Morphology morphOp; // testSet0: iteration < 0 - EXPECT_THROW( - morphOp(nullptr, batchSrc, batchDst, batchWorkspace, NVCV_ERODE, maskTensor, anchorTensor, -1, borderMode), - nvcv::Exception); + EXPECT_THROW(morphOp(nullptr, batchSrc, batchDst, nvcv::OptionalImageBatchVarShapeConstRef{batchWorkspace}, + NVCV_ERODE, maskTensor, anchorTensor, -1, borderMode), + nvcv::Exception); // testSet1: NVCV_DILATE and NVCV_ERODE && iteration > 1 && null workspace std::vector testSet1{NVCV_DILATE, NVCV_ERODE}; - for (auto morphType : testSet1) + for (auto morphType : testSet1) // NOSONAR { - EXPECT_THROW( - morphOp(nullptr, batchSrc, batchDst, nvcv::NullOpt, morphType, maskTensor, anchorTensor, 2, borderMode), - nvcv::Exception); + EXPECT_THROW(morphOp(nullptr, batchSrc, batchDst, nvcv::OptionalImageBatchVarShapeConstRef{nvcv::NullOpt}, + morphType, maskTensor, anchorTensor, 2, borderMode), + nvcv::Exception); } // testSet2: NVCV_CLOSE and NVCV_OPEN && null workspace std::vector testSet2{NVCV_CLOSE, NVCV_OPEN}; - for (auto morphType : testSet2) + for (auto morphType : testSet2) // NOSONAR { - EXPECT_THROW( - morphOp(nullptr, batchSrc, batchDst, nvcv::NullOpt, morphType, maskTensor, anchorTensor, 1, borderMode), - nvcv::Exception); + EXPECT_THROW(morphOp(nullptr, batchSrc, batchDst, nvcv::OptionalImageBatchVarShapeConstRef{nvcv::NullOpt}, + morphType, maskTensor, anchorTensor, 1, borderMode), + nvcv::Exception); } // testSet3: invalid data type @@ -1100,7 +1286,8 @@ TEST(OpMorphology_Negative, operator_varshape_negative) batchDstInvalid.pushBack(imgDstInvalid.begin(), imgDstInvalid.end()); batchWorkspaceInvalid.pushBack(imgWorkspaceInvalid.begin(), imgWorkspaceInvalid.end()); - EXPECT_THROW(morphOp(nullptr, batchSrcInvalid, batchDstInvalid, batchWorkspaceInvalid, NVCV_ERODE, maskTensor, + EXPECT_THROW(morphOp(nullptr, batchSrcInvalid, batchDstInvalid, + nvcv::OptionalImageBatchVarShapeConstRef{batchWorkspaceInvalid}, NVCV_ERODE, maskTensor, anchorTensor, 1, borderMode), nvcv::Exception); } @@ -1116,49 +1303,24 @@ TEST(OpMorphology_Negative, operator_varshape_negative) batchDstInvalid.pushBack(imgDstInvalid.begin(), imgDstInvalid.end()); batchWorkspaceInvalid.pushBack(imgWorkspaceInvalid.begin(), imgWorkspaceInvalid.end()); - EXPECT_THROW(morphOp(nullptr, batchSrc, batchDstInvalid, batchWorkspaceInvalid, NVCV_ERODE, maskTensor, - anchorTensor, 1, borderMode), - nvcv::Exception); + EXPECT_THROW( + morphOp(nullptr, batchSrc, batchDstInvalid, nvcv::OptionalImageBatchVarShapeConstRef{batchWorkspaceInvalid}, + NVCV_ERODE, maskTensor, anchorTensor, 1, borderMode), + nvcv::Exception); } #ifndef ENABLE_SANITIZER // testSet5 : invalid morph type - EXPECT_THROW(morphOp(nullptr, batchSrc, batchDst, batchWorkspace, static_cast(255), maskTensor, - anchorTensor, 0, borderMode), + EXPECT_THROW(morphOp(nullptr, batchSrc, batchDst, nvcv::OptionalImageBatchVarShapeConstRef{batchWorkspace}, + static_cast(255), maskTensor, anchorTensor, 0, borderMode), nvcv::Exception); // invalid border mode - EXPECT_THROW(morphOp(nullptr, batchSrc, batchDst, batchWorkspace, NVCV_ERODE, maskTensor, anchorTensor, 0, - static_cast(255)), + EXPECT_THROW(morphOp(nullptr, batchSrc, batchDst, nvcv::OptionalImageBatchVarShapeConstRef{batchWorkspace}, + NVCV_ERODE, maskTensor, anchorTensor, 0, static_cast(255)), nvcv::Exception); #endif - // testSet6: invalid format - { - nvcv::ImageFormat formatInvalid = nvcv::FMT_RGB8p; - std::vector imgSrcInvalid; - nvcv::ImageBatchVarShape batchSrcInvalid(batches); - for (int i = 0; i < batches; ++i) - { - imgSrcInvalid.emplace_back(nvcv::Size2D{24, 24}, formatInvalid); - } - batchSrcInvalid.pushBack(imgSrcInvalid.begin(), imgSrcInvalid.end()); - - std::vector imgDstInvalid; - std::vector imgWorkspaceInvalid; - nvcv::ImageBatchVarShape batchDstInvalid(batches); - nvcv::ImageBatchVarShape batchWorkspaceInvalid(batches); - for (int i = 0; i < batches; ++i) - { - imgDstInvalid.emplace_back(imgSrcInvalid[i].size(), imgSrcInvalid[i].format()); - imgWorkspaceInvalid.emplace_back(imgSrcInvalid[i].size(), imgSrcInvalid[i].format()); - } - batchDstInvalid.pushBack(imgDstInvalid.begin(), imgDstInvalid.end()); - batchWorkspaceInvalid.pushBack(imgWorkspaceInvalid.begin(), imgWorkspaceInvalid.end()); - - EXPECT_THROW(morphOp(nullptr, batchSrcInvalid, batchDstInvalid, batchWorkspaceInvalid, NVCV_ERODE, maskTensor, - anchorTensor, 1, borderMode), - nvcv::Exception); - } + // testSet6: unsupported 2-channel planar format { #define NVCV_IMAGE_FORMAT_2U8 NVCV_DETAIL_MAKE_NONCOLOR_FMT1(PL, UNSIGNED, XY00, ASSOCIATED, X8_Y8) nvcv::ImageFormat formatInvalid{NVCV_IMAGE_FORMAT_2U8}; @@ -1182,7 +1344,8 @@ TEST(OpMorphology_Negative, operator_varshape_negative) batchDstInvalid.pushBack(imgDstInvalid.begin(), imgDstInvalid.end()); batchWorkspaceInvalid.pushBack(imgWorkspaceInvalid.begin(), imgWorkspaceInvalid.end()); - EXPECT_THROW(morphOp(nullptr, batchSrcInvalid, batchDstInvalid, batchWorkspaceInvalid, NVCV_ERODE, maskTensor, + EXPECT_THROW(morphOp(nullptr, batchSrcInvalid, batchDstInvalid, + nvcv::OptionalImageBatchVarShapeConstRef{batchWorkspaceInvalid}, NVCV_ERODE, maskTensor, anchorTensor, 1, borderMode), nvcv::Exception); } diff --git a/tests/cvcuda/system/TestOpNonMaximumSuppression.cpp b/tests/cvcuda/system/TestOpNonMaximumSuppression.cpp index 7138d6354..7bf1c7d56 100644 --- a/tests/cvcuda/system/TestOpNonMaximumSuppression.cpp +++ b/tests/cvcuda/system/TestOpNonMaximumSuppression.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -27,12 +27,20 @@ #include #include +#include #include namespace test = nvcv::test; namespace util = nvcv::util; -static std::default_random_engine g_rng(std::random_device{}()); +// Fixed seed: random_device made tests non-deterministic across CI runs and +// occasionally produced ill-conditioned numerical inputs that exceeded +// EXPECT_NEAR tolerances on rare-config CI. Use a known-good fixed seed. +static std::default_random_engine &Rng() +{ + static std::default_random_engine rng(12345); + return rng; +} template float GoldArea(const T &bbox) @@ -49,9 +57,9 @@ float GoldIoU(const T &box1, const T &box2) int yInterBottom = std::min(box1.y + box1.w, box2.y + box2.w); int widthInter = xInterRight - xInterLeft; int heightInter = yInterBottom - yInterTop; - float interArea = widthInter * heightInter; + auto interArea = static_cast(widthInter * heightInter); float iou = 0.f; - if (widthInter > 0.f && heightInter > 0.f) + if (widthInter > 0 && heightInter > 0) { float unionArea = GoldArea(box1) + GoldArea(box2) - interArea; if (unionArea > 0.f) @@ -62,6 +70,41 @@ float GoldIoU(const T &box1, const T &box2) return iou; } +struct GoldNmsContext +{ + const std::vector &srcBBVec; + const std::vector &srcScVec; + const long2 &srcBBStrides; + const long2 &srcScStrides; + int2 shape; + float iouThreshold; + + bool ShouldDiscard(int x, int y1, const short4 &src1, float score1) const + { + for (int y2 = 0; y2 < shape.y; ++y2) + { + if (y1 == y2) + { + continue; + } + + const short4 &src2 = util::ValueAt(srcBBVec, srcBBStrides, int2{x, y2}); + if (GoldIoU(src1, src2) <= iouThreshold) + { + continue; + } + + const float &score2 = util::ValueAt(srcScVec, srcScStrides, int2{x, y2}); + if (score1 < score2 || (score1 == score2 && GoldArea(src1) < GoldArea(src2))) + { + return true; + } + } + + return false; + } +}; + inline void GoldNMS(const std::vector &srcBBVec, std::vector &dstMkVec, const std::vector &srcScVec, const long2 &srcBBStrides, const long2 &dstMkStrides, const long2 &srcScStrides, const int2 &shape, float scoreThreshold, float iouThreshold) @@ -72,6 +115,7 @@ inline void GoldNMS(const std::vector &srcBBVec, std::vector & # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wdangling-reference" #endif + GoldNmsContext context{srcBBVec, srcScVec, srcBBStrides, srcScStrides, shape, iouThreshold}; for (int x = 0; x < shape.x; ++x) { for (int y1 = 0; y1 < shape.y; ++y1) @@ -86,27 +130,7 @@ inline void GoldNMS(const std::vector &srcBBVec, std::vector & } const short4 &src1 = util::ValueAt(srcBBVec, srcBBStrides, int2{x, y1}); - bool discard = false; - - for (int y2 = 0; y2 < shape.y; ++y2) - { - if (y1 == y2) - { - continue; - } - - const short4 &src2 = util::ValueAt(srcBBVec, srcBBStrides, int2{x, y2}); - - if (GoldIoU(src1, src2) > iouThreshold) - { - const float &score2 = util::ValueAt(srcScVec, srcScStrides, int2{x, y2}); - if (score1 < score2 || (score1 == score2 && GoldArea(src1) < GoldArea(src2))) - { - discard = true; - break; - } - } - } + bool discard = context.ShouldDiscard(x, y1, src1, score1); dst = discard ? 0 : 1; } @@ -160,7 +184,9 @@ TEST_P(OpNonMaximumSuppression, correct_output) int2 shape = nvcv::cuda::StaticCast(long2{srcBBData->shape(0), srcScData->shape(1)}); ASSERT_EQ(shape, inShape); - std::uniform_int_distribution randPos(0, 128), randSize(50, 100), randScore(0, 1024); + std::uniform_int_distribution randPos(0, 128); + std::uniform_int_distribution randSize(50, 100); + std::uniform_int_distribution randScore(0, 1024); long srcBBBufSize{srcBBStrides.x * shape.x}; long srcScBufSize{srcScStrides.x * shape.x}; @@ -169,7 +195,8 @@ TEST_P(OpNonMaximumSuppression, correct_output) std::vector srcScVec(srcScBufSize); short4 bbox; - int halfBBoxes = static_cast(std::ceil(shape.y / 2.f)); // repeat bboxes after pass half total + auto halfBBoxes + = static_cast(std::ceil(static_cast(shape.y) / 2.f)); // repeat bboxes after pass half total for (int x = 0; x < shape.x; ++x) { @@ -177,7 +204,7 @@ TEST_P(OpNonMaximumSuppression, correct_output) { if (y < halfBBoxes) { - bbox = short4{randPos(g_rng), randPos(g_rng), randSize(g_rng), randSize(g_rng)}; + bbox = short4{randPos(Rng()), randPos(Rng()), randSize(Rng()), randSize(Rng())}; } else { @@ -185,7 +212,7 @@ TEST_P(OpNonMaximumSuppression, correct_output) } util::ValueAt(srcBBVec, srcBBStrides, int2{x, y}) = bbox; - util::ValueAt(srcScVec, srcScStrides, int2{x, y}) = randScore(g_rng) / 1024.f; + util::ValueAt(srcScVec, srcScStrides, int2{x, y}) = randScore(Rng()) / 1024.f; } } @@ -221,6 +248,53 @@ TEST_P(OpNonMaximumSuppression, correct_output) EXPECT_EQ(dstMkVecTest, dstMkVecGold); } +TEST(OpNonMaximumSuppression, score_order_and_area_tiebreak) +{ + constexpr int numBoxes = 5; + constexpr float scoreThreshold = 0.5f; + constexpr float iouThreshold = 0.5f; + + // clang-format off + + nvcv::Tensor srcBB({{1, numBoxes}, "NW"}, nvcv::TYPE_4S16); + nvcv::Tensor dstMk({{1, numBoxes}, "NW"}, nvcv::TYPE_U8); + nvcv::Tensor srcSc({{1, numBoxes}, "NW"}, nvcv::TYPE_F32); + + // clang-format on + + const std::vector boxes{ + { 0, 0, 10, 10}, + { 0, 0, 10, 10}, + {100, 100, 10, 10}, + {100, 100, 12, 12}, + {200, 200, 10, 10} + }; + const std::vector scores{0.9f, 0.8f, 0.7f, 0.7f, 0.4f}; + const std::vector expected{1, 0, 0, 1, 0}; + + auto srcBBData = srcBB.exportData(); + auto srcScData = srcSc.exportData(); + auto dstMkData = dstMk.exportData(); + ASSERT_TRUE(srcBBData && srcScData && dstMkData); + + ASSERT_EQ(cudaSuccess, + cudaMemcpy(srcBBData->basePtr(), boxes.data(), boxes.size() * sizeof(short4), cudaMemcpyHostToDevice)); + ASSERT_EQ(cudaSuccess, + cudaMemcpy(srcScData->basePtr(), scores.data(), scores.size() * sizeof(float), cudaMemcpyHostToDevice)); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + cvcuda::NonMaximumSuppression nms; + EXPECT_NO_THROW(nms(stream, srcBB, dstMk, srcSc, scoreThreshold, iouThreshold)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + + std::vector actual(numBoxes); + ASSERT_EQ(cudaSuccess, + cudaMemcpy(actual.data(), dstMkData->basePtr(), actual.size() * sizeof(uint8_t), cudaMemcpyDeviceToHost)); + EXPECT_EQ(actual, expected); +} + // clang-format off NVCV_TEST_SUITE_P(OpNonMaximumSuppression_Negative, test::ValueList{ {"NWC", nvcv::TYPE_S16, "NW", nvcv::TYPE_U8, "NW", nvcv::TYPE_F32, 0.1f, 1, 3, 3, 3, 5, 5, 5}, // in: rank3 + S16 + last shape is not 4 @@ -274,7 +348,8 @@ TEST_P(OpNonMaximumSuppression_Negative, op) cvcuda::NonMaximumSuppression nms; EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, - nvcv::ProtectCall([&] { nms(stream, srcBB, dstMk, srcSc, scThresh, iouThresh); })); + nvcv::ProtectCall([&nms, &stream, &srcBB, &dstMk, &srcSc, &scThresh, &iouThresh] + { nms(stream, srcBB, dstMk, srcSc, scThresh, iouThresh); })); ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); diff --git a/tests/cvcuda/system/TestOpNormalize.cpp b/tests/cvcuda/system/TestOpNormalize.cpp index a5c5deb3b..07c316fbf 100644 --- a/tests/cvcuda/system/TestOpNormalize.cpp +++ b/tests/cvcuda/system/TestOpNormalize.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,70 +16,546 @@ */ #include "Definitions.hpp" +#include "PlanarParityUtils.hpp" #include #include #include +#include #include #include #include #include +#include +#include +#include #include +#include +#include #include +#include +#include namespace test = nvcv::test; namespace t = ::testing; -static void Normalize(std::vector &hDst, int dstRowStride, const std::vector &hSrc, int srcRowStride, - nvcv::Size2D size, nvcv::ImageFormat fmt, const std::vector &hBase, int baseRowStride, - nvcv::Size2D baseSize, nvcv::ImageFormat baseFormat, const std::vector &hScale, - int scaleRowStride, nvcv::Size2D scaleSize, nvcv::ImageFormat scaleFormat, - const float globalScale, const float globalShift, const float epsilon, const uint32_t flags) +static int ScaledSize(int size, double scale) +{ + return static_cast(size * scale); +} + +struct NormalizeRefData +{ + std::vector &hDst; + int dstRowStride; + const std::vector &hSrc; + int srcRowStride; + nvcv::Size2D size; + nvcv::ImageFormat fmt; + const std::vector &hBase; + int baseRowStride; + nvcv::Size2D baseSize; + nvcv::ImageFormat baseFormat; + const std::vector &hScale; + int scaleRowStride; + nvcv::Size2D scaleSize; + nvcv::ImageFormat scaleFormat; + float globalScale; + float globalShift; + float epsilon; + uint32_t flags; +}; + +static int BroadcastIndex(int index, int size) +{ + return size == 1 ? 0 : index; +} + +static float NormalizeMultiplier(const NormalizeRefData &ref, int y, int x, int channel) +{ + using FT = float; + + const int si = BroadcastIndex(y, ref.scaleSize.h); + const int sj = BroadcastIndex(x, ref.scaleSize.w); + const int sk = BroadcastIndex(channel, ref.scaleFormat.numChannels()); + + FT scale = ref.hScale.at(si * ref.scaleRowStride + sj * ref.scaleFormat.numChannels() + sk); + if (ref.flags & CVCUDA_NORMALIZE_SCALE_IS_STDDEV) + { + return FT{1} / std::sqrt(scale * scale + ref.epsilon); + } + + return scale; +} + +static uint8_t SaturateToU8(float value) +{ + if (value < 0) + { + return 0; + } + + if (value > 255) + { + return 255; + } + + return static_cast(value); +} + +static uint8_t NormalizeValue(const NormalizeRefData &ref, int y, int x, int channel) { using FT = float; - for (int i = 0; i < size.h; i++) + const int bi = BroadcastIndex(y, ref.baseSize.h); + const int bj = BroadcastIndex(x, ref.baseSize.w); + const int bk = BroadcastIndex(channel, ref.baseFormat.numChannels()); + + const FT src = ref.hSrc.at(y * ref.srcRowStride + x * ref.fmt.numChannels() + channel); + const FT base = ref.hBase.at(bi * ref.baseRowStride + bj * ref.baseFormat.numChannels() + bk); + const FT mul = NormalizeMultiplier(ref, y, x, channel); + + return SaturateToU8(std::rint((src - base) * mul * ref.globalScale + ref.globalShift)); +} + +static void Normalize(NormalizeRefData ref) +{ + for (int i = 0; i < ref.size.h; i++) + { + for (int j = 0; j < ref.size.w; j++) + { + for (int k = 0; k < ref.fmt.numChannels(); k++) + { + ref.hDst.at(i * ref.dstRowStride + j * ref.fmt.numChannels() + k) = NormalizeValue(ref, i, j, k); + } + } + } +} + +static constexpr uint32_t normalScale = 0; +static constexpr uint32_t scaleIsStdDev = CVCUDA_NORMALIZE_SCALE_IS_STDDEV; + +template +static T ReferenceNormalizeCast(float value) +{ + if constexpr (std::is_floating_point_v) + { + return static_cast(value); + } + else + { + float rounded = std::rint(value); + float clamped = std::min(std::max(rounded, 0.f), 255.f); + return static_cast(clamped); + } +} + +template +static T MakePlanarInputValue(int sample, int y, int x, int channel) +{ + if constexpr (std::is_floating_point_v) + { + return static_cast(10.f + static_cast(sample) * 0.25f + static_cast(y) * 0.5f + + static_cast(x) * 0.125f + static_cast(channel)); + } + else + { + return static_cast((sample * 17 + y * 5 + x * 3 + channel * 11) % 211); + } +} + +template +static std::vector MakePlanarHostImage(int sample, int width, int height, int channels) +{ + std::vector image(height * width * channels); + for (int y = 0; y < height; ++y) + { + for (int x = 0; x < width; ++x) + { + for (int c = 0; c < channels; ++c) + { + image[y * width * channels + x * channels + c] = MakePlanarInputValue(sample, y, x, c); + } + } + } + return image; +} + +static void FillPlanarParamTensor(nvcv::Tensor &tensor, const std::vector &values) +{ + auto data = tensor.exportData(); + ASSERT_NE(nullptr, data); + auto access = nvcv::TensorDataAccessStridedImagePlanar::Create(*data); + ASSERT_TRUE(access); + ASSERT_EQ(static_cast(values.size()), access->numChannels()); + + for (int c = 0; c < access->numChannels(); ++c) + { + nvcv::Byte *p = access->sampleData(0) + c * access->chStride(); + ASSERT_EQ(cudaSuccess, cudaMemcpy(p, &values[c], sizeof(float), cudaMemcpyHostToDevice)); + } +} + +// Deinterleave an HWC host image into per-channel planes and upload each plane to the device. +// planeDst(c) returns the {device pointer, row stride in bytes} for channel c's plane, so the same +// code serves both planar tensors (channel stride) and multi-plane images (per-plane base ptr). +template +static void UploadDeinterleavedPlanes(const std::vector &hostImage, int width, int height, int channels, + PlaneDstFn planeDst) +{ + std::vector plane(static_cast(height) * width); + for (int c = 0; c < channels; ++c) { - const int bi = baseSize.h == 1 ? 0 : i; - const int si = scaleSize.h == 1 ? 0 : i; + for (int y = 0; y < height; ++y) + { + for (int x = 0; x < width; ++x) + { + plane[y * width + x] = hostImage[y * width * channels + x * channels + c]; + } + } + + auto [ptr, rowStride] = planeDst(c); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(ptr, rowStride, plane.data(), width * sizeof(T), width * sizeof(T), height, + cudaMemcpyHostToDevice)); + } +} - for (int j = 0; j < size.w; j++) +// Inverse of UploadDeinterleavedPlanes: download per-channel planes and interleave into HWC. +template +static std::vector DownloadInterleavedPlanes(int width, int height, int channels, PlaneSrcFn planeSrc) +{ + std::vector image(static_cast(height) * width * channels); + std::vector plane(static_cast(height) * width); + for (int c = 0; c < channels; ++c) + { + auto [ptr, rowStride] = planeSrc(c); + EXPECT_EQ(cudaSuccess, cudaMemcpy2D(plane.data(), width * sizeof(T), ptr, rowStride, width * sizeof(T), height, + cudaMemcpyDeviceToHost)); + for (int y = 0; y < height; ++y) { - const int bj = baseSize.w == 1 ? 0 : j; - const int sj = scaleSize.w == 1 ? 0 : j; + for (int x = 0; x < width; ++x) + { + image[y * width * channels + x * channels + c] = plane[y * width + x]; + } + } + } + return image; +} + +template +static void UploadPlanarTensorSample(nvcv::TensorDataAccessStridedImagePlanar &access, int sample, + const std::vector &image, int width, int height, int channels) +{ + UploadDeinterleavedPlanes( + image, width, height, channels, + [&access, sample](int c) + { return std::make_pair(access.sampleData(sample) + c * access.chStride(), access.rowStride()); }); +} + +template +static void UploadPlanarImage(nvcv::Image &image, const std::vector &hostImage) +{ + auto imgData = image.exportData(); + ASSERT_NE(imgData, nvcv::NullOpt); + + UploadDeinterleavedPlanes(hostImage, image.size().w, image.size().h, image.format().numChannels(), + [&imgData](int c) + { return std::make_pair(imgData->plane(c).basePtr, imgData->plane(c).rowStride); }); +} + +template +static std::vector DownloadPlanarTensorSample(nvcv::TensorDataAccessStridedImagePlanar &access, int sample, + int width, int height, int channels) +{ + return DownloadInterleavedPlanes( + width, height, channels, + [&access, sample](int c) + { return std::make_pair(access.sampleData(sample) + c * access.chStride(), access.rowStride()); }); +} + +template +static std::vector DownloadPlanarImage(nvcv::Image &image) +{ + auto imgData = image.exportData(); + EXPECT_NE(imgData, nvcv::NullOpt); + + return DownloadInterleavedPlanes( + image.size().w, image.size().h, image.format().numChannels(), + [&imgData](int c) { return std::make_pair(imgData->plane(c).basePtr, imgData->plane(c).rowStride); }); +} - for (int k = 0; k < fmt.numChannels(); k++) +template +static std::vector MakeNormalizeGold(const std::vector &src, int width, int height, int channels, + const std::vector &base, const std::vector &scale, + float globalScale, float globalShift, uint32_t flags = 0, float epsilon = 0.f) +{ + std::vector gold(src.size()); + for (int y = 0; y < height; ++y) + { + for (int x = 0; x < width; ++x) + { + for (int c = 0; c < channels; ++c) { - const int bk = (baseFormat.numChannels() == 1 ? 0 : k); - const int sk = (scaleFormat.numChannels() == 1 ? 0 : k); - - FT mul; - - if (flags & CVCUDA_NORMALIZE_SCALE_IS_STDDEV) - { - FT s = hScale.at(si * scaleRowStride + sj * scaleFormat.numChannels() + sk); - FT x = s * s + epsilon; - mul = FT{1} / std::sqrt(x); - } - else - { - mul = hScale.at(si * scaleRowStride + sj * scaleFormat.numChannels() + sk); - } - - FT res = std::rint((hSrc.at(i * srcRowStride + j * fmt.numChannels() + k) - - hBase.at(bi * baseRowStride + bj * baseFormat.numChannels() + bk)) - * mul * globalScale - + globalShift); - - hDst.at(i * dstRowStride + j * fmt.numChannels() + k) = res < 0 ? 0 : (res > 255 ? 255 : res); + int idx = y * width * channels + x * channels + c; + const float mul = (flags & CVCUDA_NORMALIZE_SCALE_IS_STDDEV) + ? 1.f / std::sqrt(scale[c] * scale[c] + epsilon) + : scale[c]; + float value = (static_cast(src[idx]) - base[c]) * mul * globalScale + globalShift; + gold[idx] = ReferenceNormalizeCast(value); } } } + return gold; +} + +template +static void ExpectPlanarVectorsNear(const std::vector &gold, const std::vector &test) +{ + ASSERT_EQ(gold.size(), test.size()); + if constexpr (std::is_floating_point_v) + { + for (size_t i = 0; i < gold.size(); ++i) + { + EXPECT_NEAR(gold[i], test[i], 1e-4f) << "at flat index " << i; + } + } + else + { + EXPECT_EQ(gold, test); + } +} + +// Create a 1x1 planar param (base or scale) tensor of the given format, fill `channels` random +// floats in [0, hi], upload one float per channel via the tensor's channel stride, and return the +// host values for use in the reference computation. +static std::vector MakeAndUploadRandomParam(nvcv::Tensor ¶m, nvcv::ImageFormat fmt, int channels, float hi, + std::default_random_engine &rng) +{ + param = nvcv::util::CreateTensor(1, 1, 1, fmt); + + auto data = param.exportData(); + EXPECT_NE(nullptr, data); + auto access = nvcv::TensorDataAccessStridedImagePlanar::Create(*data); + EXPECT_TRUE(access); + + std::vector values(channels); + std::uniform_real_distribution udist(0.f, hi); + std::ranges::generate(values, [&]() { return udist(rng); }); + + for (int c = 0; c < channels; ++c) + { + nvcv::Byte *p = access->sampleData(0) + c * access->chStride(); + EXPECT_EQ(cudaSuccess, cudaMemcpy(p, &values[c], sizeof(float), cudaMemcpyHostToDevice)); + } + return values; +} + +static std::vector MakeAndUploadSpatialPlanarParam(nvcv::Tensor ¶m, int width, int height, int channels, + float hi) +{ + param = nvcv::Tensor( + { + {1, channels, height, width}, + "NCHW" + }, + nvcv::TYPE_F32); + + std::vector values(static_cast(width) * height * channels); + const float valueScale = hi / static_cast(values.size() + 1); + std::ranges::generate( + values, [index = size_t{0}, valueScale]() mutable { return static_cast(++index) * valueScale; }); + + auto data = param.exportData(); + EXPECT_NE(nullptr, data); + auto access = nvcv::TensorDataAccessStridedImagePlanar::Create(*data); + EXPECT_TRUE(access); + if (access) + UploadPlanarTensorSample(*access, 0, values, width, height, channels); + return values; +} + +// Compute the interleaved reference for one planar sample via the shared Normalize() gold and +// compare it against the operator's (re-interleaved) output. base/scale are 1x1 spatially; their +// channel layout is described by {base,scale}HelperFmt. +static void ExpectPlanarNormalizeMatchesGold(const std::vector &testVec, const std::vector &srcVec, + int rowStride, int width, int height, nvcv::ImageFormat fmt, + const std::vector &baseVec, int baseRowStride, + nvcv::Size2D baseSize, nvcv::ImageFormat baseHelperFmt, + const std::vector &scaleVec, int scaleRowStride, + nvcv::Size2D scaleSize, nvcv::ImageFormat scaleHelperFmt, + float globalScale, float globalShift, float epsilon, uint32_t flags) +{ + std::vector goldVec(static_cast(height) * rowStride); + Normalize({ + goldVec, + rowStride, + srcVec, + rowStride, + {width, height}, + fmt, + baseVec, + baseRowStride, + baseSize, + baseHelperFmt, + scaleVec, + scaleRowStride, + scaleSize, + scaleHelperFmt, + globalScale, + globalShift, + epsilon, + flags + }); + EXPECT_EQ(goldVec, testVec); +} + +// Deterministic per-channel base/scale (base = c+1, scale = 0.25*(c+1)) plus the global scale/shift +// used by the fixed-value planar correctness cases. +template +static void MakeSequentialPlanarParams(int channels, std::vector &base, std::vector &scale, + float &globalScale, float &globalShift) +{ + base.resize(channels); + scale.resize(channels); + for (int c = 0; c < channels; ++c) + { + base[c] = static_cast(c + 1); + scale[c] = 0.25f * static_cast(c + 1); + } + globalScale = 1.5f; + globalShift = std::is_floating_point_v ? -0.75f : 3.f; +} + +template +static void RunPlanarTensorNormalizeCase(nvcv::ImageFormat fmt, int width, int height, int numImages) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + int channels = fmt.numChannels(); + + std::vector base; + std::vector scale; + float globalScale; + float globalShift; + MakeSequentialPlanarParams(channels, base, scale, globalScale, globalShift); + + nvcv::Tensor imgSrc(numImages, {width, height}, fmt); + nvcv::Tensor imgDst(numImages, {width, height}, fmt); + nvcv::Tensor imgBase( + { + {1, channels, 1, 1}, + "NCHW" + }, + nvcv::TYPE_F32); + nvcv::Tensor imgScale( + { + {1, channels, 1, 1}, + "NCHW" + }, + nvcv::TYPE_F32); + + ASSERT_NO_FATAL_FAILURE(FillPlanarParamTensor(imgBase, base)); + ASSERT_NO_FATAL_FAILURE(FillPlanarParamTensor(imgScale, scale)); + + auto srcData = imgSrc.exportData(); + ASSERT_NE(nullptr, srcData); + auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcData); + ASSERT_TRUE(srcAccess); + + std::vector> srcVec(numImages); + for (int i = 0; i < numImages; ++i) + { + srcVec[i] = MakePlanarHostImage(i, width, height, channels); + ASSERT_NO_FATAL_FAILURE(UploadPlanarTensorSample(*srcAccess, i, srcVec[i], width, height, channels)); + } + + cvcuda::Normalize normalizeOp; + EXPECT_NO_THROW(normalizeOp(stream, imgSrc, imgBase, imgScale, imgDst, globalScale, globalShift, 0.f, normalScale)); + + EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + + auto dstData = imgDst.exportData(); + ASSERT_NE(nullptr, dstData); + auto dstAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*dstData); + ASSERT_TRUE(dstAccess); + + for (int i = 0; i < numImages; ++i) + { + SCOPED_TRACE(i); + std::vector test = DownloadPlanarTensorSample(*dstAccess, i, width, height, channels); + std::vector gold + = MakeNormalizeGold(srcVec[i], width, height, channels, base, scale, globalScale, globalShift); + ExpectPlanarVectorsNear(gold, test); + } } -static uint32_t normalScale = 0; -static uint32_t scaleIsStdDev = CVCUDA_NORMALIZE_SCALE_IS_STDDEV; +template +static void RunPlanarVarShapeNormalizeCase(nvcv::ImageFormat fmt, int width, int height, int numImages) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + int channels = fmt.numChannels(); + + std::vector base; + std::vector scale; + float globalScale; + float globalShift; + MakeSequentialPlanarParams(channels, base, scale, globalScale, globalShift); + + std::vector imgSrc; + std::vector imgDst; + std::vector> srcVec(numImages); + std::vector sizes(numImages); + + for (int i = 0; i < numImages; ++i) + { + sizes[i] = nvcv::Size2D{width + i * 3, height + i * 2}; + imgSrc.emplace_back(sizes[i], fmt); + imgDst.emplace_back(sizes[i], fmt); + srcVec[i] = MakePlanarHostImage(i, sizes[i].w, sizes[i].h, channels); + ASSERT_NO_FATAL_FAILURE(UploadPlanarImage(imgSrc[i], srcVec[i])); + } + + nvcv::ImageBatchVarShape batchSrc(numImages); + nvcv::ImageBatchVarShape batchDst(numImages); + batchSrc.pushBack(imgSrc.begin(), imgSrc.end()); + batchDst.pushBack(imgDst.begin(), imgDst.end()); + + nvcv::Tensor imgBase( + { + {1, channels, 1, 1}, + "NCHW" + }, + nvcv::TYPE_F32); + nvcv::Tensor imgScale( + { + {1, channels, 1, 1}, + "NCHW" + }, + nvcv::TYPE_F32); + ASSERT_NO_FATAL_FAILURE(FillPlanarParamTensor(imgBase, base)); + ASSERT_NO_FATAL_FAILURE(FillPlanarParamTensor(imgScale, scale)); + + cvcuda::Normalize normalizeOp; + EXPECT_NO_THROW( + normalizeOp(stream, batchSrc, imgBase, imgScale, batchDst, globalScale, globalShift, 0.f, normalScale)); + + EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + + for (int i = 0; i < numImages; ++i) + { + SCOPED_TRACE(i); + std::vector test = DownloadPlanarImage(imgDst[i]); + std::vector gold + = MakeNormalizeGold(srcVec[i], sizes[i].w, sizes[i].h, channels, base, scale, globalScale, globalShift); + ExpectPlanarVectorsNear(gold, test); + } +} // clang-format off @@ -145,7 +621,7 @@ TEST_P(OpNormalize, tensor_correct_output) std::uniform_int_distribution udist(0, 255); srcVec[i].resize(height * srcVecRowStride); - generate(srcVec[i].begin(), srcVec[i].end(), [&]() { return udist(rng); }); + std::ranges::generate(srcVec[i], [&udist, &rng]() { return udist(rng); }); // Copy input data to the GPU ASSERT_EQ(cudaSuccess, @@ -168,7 +644,7 @@ TEST_P(OpNormalize, tensor_correct_output) std::uniform_real_distribution udist(0, 255.f); baseVec[i].resize(baseHeight * baseVecRowStride); - generate(baseVec[i].begin(), baseVec[i].end(), [&]() { return udist(rng); }); + std::ranges::generate(baseVec[i], [&udist, &rng]() { return udist(rng); }); // Copy input data to the GPU ASSERT_EQ(cudaSuccess, cudaMemcpy2D(baseAccess->sampleData(i), baseAccess->rowStride(), baseVec[i].data(), @@ -189,224 +665,1891 @@ TEST_P(OpNormalize, tensor_correct_output) int scaleVecRowStride = scaleWidth * scaleFormat.numChannels(); for (int i = 0; i < scaleNumImages; ++i) { - std::uniform_real_distribution udist(0, 1.f); + std::uniform_real_distribution udist(0, 1.f); + + scaleVec[i].resize(scaleHeight * scaleVecRowStride); + std::ranges::generate(scaleVec[i], [&udist, &rng]() { return udist(rng); }); + + // Copy input data to the GPU + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(scaleAccess->sampleData(i), scaleAccess->rowStride(), scaleVec[i].data(), + scaleVecRowStride * sizeof(float), + scaleVecRowStride * sizeof(float), // vec has no padding + scaleHeight, cudaMemcpyHostToDevice)); + } + + // Create dest tensor + nvcv::Tensor imgDst = nvcv::util::CreateTensor(numImages, width, height, fmt); + + // Generate test result + cvcuda::Normalize normalizeOp; + EXPECT_NO_THROW(normalizeOp(stream, imgSrc, imgBase, imgScale, imgDst, globalScale, globalShift, epsilon, flags)); + + // Get test data back + EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + + // Check result + auto dstData = imgDst.exportData(); + ASSERT_NE(nullptr, dstData); + + auto dstAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*dstData); + ASSERT_TRUE(dstAccess); + + int dstVecRowStride = width * fmt.numChannels(); + for (int i = 0; i < numImages; ++i) + { + SCOPED_TRACE(i); + + std::vector testVec(height * dstVecRowStride); + + // Copy output data to Host + ASSERT_EQ(cudaSuccess, + cudaMemcpy2D(testVec.data(), dstVecRowStride, dstAccess->sampleData(i), dstAccess->rowStride(), + dstVecRowStride, // vec has no padding + height, cudaMemcpyDeviceToHost)); + + std::vector goldVec(height * dstVecRowStride); + + int bi = baseNumImages == 1 ? 0 : i; + int si = scaleNumImages == 1 ? 0 : i; + + // Generate gold result + Normalize({ + goldVec, + dstVecRowStride, + srcVec[i], + srcVecRowStride, + { width, height}, + fmt, + baseVec[bi], + baseVecRowStride, + { baseWidth, baseHeight}, + baseFormat, + scaleVec[si], + scaleVecRowStride, + {scaleWidth, scaleHeight}, + scaleFormat, + globalScale, + globalShift, + epsilon, + flags + }); + + EXPECT_EQ(goldVec, testVec); + } +} + +// Shared fixed parameters for the inverse-std-dev vectorized/hoisted coverage cases below. +static constexpr float kStdDevGlobalScale = 1.7f; +static constexpr float kStdDevGlobalShift = 3.5f; + +// Deterministic host fills shared by the inverse-std-dev tensor and var-shape helpers. The SAME +// host data feeds both the device upload and the gold computation, preserving bit-exactness; using +// fixed index-based formulas (instead of a PRNG) keeps the values reproducible without flagging the +// cpp:S2245 security hotspot. +// src bytes in [0,255]; base in [0,255); scale in [0.1, 2.0] (strictly positive). +static void FillDeterministicSrc(std::vector &dst) +{ + for (size_t k = 0; k < dst.size(); ++k) + { + dst[k] = static_cast((k * 37 + 11) & 0xFF); + } +} + +static void FillDeterministicBaseScale(std::vector &base, std::vector &scale) +{ + for (size_t k = 0; k < base.size(); ++k) + { + base[k] = static_cast((k * 53) % 256); + } + for (size_t k = 0; k < scale.size(); ++k) + { + scale[k] = 0.1f + static_cast((k * 7) % 20) * 0.1f; + } +} + +// Uploads a single-sample F32 param tensor (base or scale) from host data via a contiguous copy. +static void UploadParamTensor(const nvcv::Tensor &tensor, const std::vector &host) +{ + auto data = tensor.exportData(); + ASSERT_NE(nullptr, data); + auto access = nvcv::TensorDataAccessStridedImagePlanar::Create(*data); + ASSERT_TRUE(access); + ASSERT_EQ(cudaSuccess, + cudaMemcpy(access->sampleData(0), host.data(), host.size() * sizeof(float), cudaMemcpyHostToDevice)); +} + +// Builds the gold image for sample i against the shared reference Normalize() and EXPECT_EQ-compares +// it to the device result. Shared by the tensor and var-shape inverse-std-dev helpers. +static void ExpectGoldEquals(std::vector &testVec, const std::vector &srcVec, int srcRowStride, + nvcv::Size2D size, nvcv::ImageFormat fmt, const std::vector &baseVec, + const std::vector &scaleVec, int paramRowStride, nvcv::Size2D paramSize, + nvcv::ImageFormat paramFmt, float epsilon) +{ + std::vector goldVec(testVec.size()); + Normalize({goldVec, srcRowStride, srcVec, srcRowStride, size, fmt, baseVec, paramRowStride, paramSize, paramFmt, + scaleVec, paramRowStride, paramSize, paramFmt, kStdDevGlobalScale, kStdDevGlobalShift, epsilon, + CVCUDA_NORMALIZE_SCALE_IS_STDDEV}); + + EXPECT_EQ(goldVec, testVec); +} + +// Inverse-std-dev base/scale broadcast modes exercised by the consolidated tensor/var-shape helpers: +// Scalar -> 1 x 1 x 1 x 1 F32 param, broadcast over every sample/pixel/channel +// PerPixel -> N x H x W x 1 F32 param, one base/scale per src pixel (single-channel src) +// PerChannel -> 1 x 1 x 1 x C param (RGBf32/RGBAf32), broadcast over the spatial extent +enum class ParamMode +{ + Scalar, + PerPixel, + PerChannel +}; + +// The F32 param image-format that pairs with a given mode and channel count. +static nvcv::ImageFormat ParamFormatFor(ParamMode mode, int channels) +{ + if (mode == ParamMode::PerChannel) + { + return channels == 3 ? nvcv::FMT_RGBf32 : nvcv::FMT_RGBAf32; + } + return nvcv::FMT_F32; +} + +// Builds, fills (deterministically) and uploads the base/scale param tensors for a given mode. The +// per-sample host data is returned in baseVec/scaleVec so the gold can reuse the exact same values: +// Scalar/PerChannel -> one host vector (index 0); PerPixel -> one host vector per sample. +// paramW/paramH/paramRowStride report the per-sample param geometry the gold loop needs. +static void MakeStdDevParam(ParamMode mode, nvcv::ImageFormat paramFmt, int channels, int width, int height, + int numImages, nvcv::Tensor &imgBase, nvcv::Tensor &imgScale, + std::vector> &baseVec, std::vector> &scaleVec, + int ¶mW, int ¶mH, int ¶mRowStride) +{ + if (mode == ParamMode::PerChannel) + { + imgBase = nvcv::Tensor( + { + {1, 1, 1, channels}, + nvcv::TENSOR_NHWC + }, + paramFmt.planeDataType(0)); + imgScale = nvcv::Tensor( + { + {1, 1, 1, channels}, + nvcv::TENSOR_NHWC + }, + paramFmt.planeDataType(0)); + baseVec.assign(1, std::vector(channels)); + scaleVec.assign(1, std::vector(channels)); + FillDeterministicBaseScale(baseVec[0], scaleVec[0]); + UploadParamTensor(imgBase, baseVec[0]); + UploadParamTensor(imgScale, scaleVec[0]); + paramW = paramH = paramRowStride = 1; // unused by the PerChannel gold (paramRowStride==channels passed there) + return; + } + + const int paramN = (mode == ParamMode::PerPixel) ? numImages : 1; + paramW = (mode == ParamMode::PerPixel) ? width : 1; + paramH = (mode == ParamMode::PerPixel) ? height : 1; + paramRowStride = paramW; + + imgBase = nvcv::Tensor(paramN, {paramW, paramH}, paramFmt); + imgScale = nvcv::Tensor(paramN, {paramW, paramH}, paramFmt); + baseVec.assign(paramN, {}); + scaleVec.assign(paramN, {}); + + auto baseData = imgBase.exportData(); + auto scaleData = imgScale.exportData(); + ASSERT_NE(nullptr, baseData); + ASSERT_NE(nullptr, scaleData); + auto baseAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*baseData); + auto scaleAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*scaleData); + ASSERT_TRUE(baseAccess); + ASSERT_TRUE(scaleAccess); + for (int i = 0; i < paramN; ++i) + { + baseVec[i].resize(static_cast(paramH) * paramRowStride); + scaleVec[i].resize(static_cast(paramH) * paramRowStride); + FillDeterministicBaseScale(baseVec[i], scaleVec[i]); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(baseAccess->sampleData(i), baseAccess->rowStride(), baseVec[i].data(), + paramRowStride * sizeof(float), paramRowStride * sizeof(float), paramH, + cudaMemcpyHostToDevice)); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(scaleAccess->sampleData(i), scaleAccess->rowStride(), scaleVec[i].data(), + paramRowStride * sizeof(float), paramRowStride * sizeof(float), paramH, + cudaMemcpyHostToDevice)); + } +} + +// Consolidated inverse-std-dev tensor coverage (channels==1 U8 with Scalar/PerPixel param, or +// interleaved RGB8/RGBA8 with PerChannel param). The SAME deterministic host data feeds both the +// device upload and the shared gold, preserving bit-exactness. Exercises the vectorized +// single-channel uchar4 kernel and the hoisted NIX-pixels-per-thread interleaved kernel across the +// vector body (W aligned) and the scalar/per-pixel tail (W unaligned), plus every broadcast branch. +static void RunTensorStdDevCase(nvcv::ImageFormat fmt, int width, int height, int numImages, ParamMode mode) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + const float epsilon = (mode == ParamMode::PerChannel) ? 0.234f : 0.123f; + const int channels = fmt.numChannels(); + const nvcv::ImageFormat paramFmt = ParamFormatFor(mode, channels); + + nvcv::Tensor imgSrc = nvcv::util::CreateTensor(numImages, width, height, fmt); + auto srcData = imgSrc.exportData(); + ASSERT_NE(nullptr, srcData); + auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcData); + ASSERT_TRUE(srcAccess); + + std::vector> srcVec(numImages); + const int srcRowStride = width * channels; + for (int i = 0; i < numImages; ++i) + { + srcVec[i].resize(static_cast(height) * srcRowStride); + FillDeterministicSrc(srcVec[i]); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(srcAccess->sampleData(i), srcAccess->rowStride(), srcVec[i].data(), + srcRowStride, srcRowStride, height, cudaMemcpyHostToDevice)); + } + + nvcv::Tensor imgBase; + nvcv::Tensor imgScale; + std::vector> baseVec; + std::vector> scaleVec; + int paramW = 1; + int paramH = 1; + int paramRowStride = 1; + MakeStdDevParam(mode, paramFmt, channels, width, height, numImages, imgBase, imgScale, baseVec, scaleVec, paramW, + paramH, paramRowStride); + + nvcv::Tensor imgDst = nvcv::util::CreateTensor(numImages, width, height, fmt); + + cvcuda::Normalize op; + EXPECT_NO_THROW(op(stream, imgSrc, imgBase, imgScale, imgDst, kStdDevGlobalScale, kStdDevGlobalShift, epsilon, + CVCUDA_NORMALIZE_SCALE_IS_STDDEV)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + + auto dstData = imgDst.exportData(); + ASSERT_NE(nullptr, dstData); + auto dstAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*dstData); + ASSERT_TRUE(dstAccess); + + const int dstRowStride = srcRowStride; + for (int i = 0; i < numImages; ++i) + { + SCOPED_TRACE(i); + std::vector testVec(static_cast(height) * dstRowStride); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(testVec.data(), dstRowStride, dstAccess->sampleData(i), + dstAccess->rowStride(), dstRowStride, height, cudaMemcpyDeviceToHost)); + + if (mode == ParamMode::PerChannel) + { + ExpectGoldEquals(testVec, srcVec[i], dstRowStride, {width, height}, fmt, baseVec[0], scaleVec[0], channels, + {1, 1}, paramFmt, epsilon); + } + else + { + const auto bi = (mode == ParamMode::PerPixel) ? i : 0; + ExpectGoldEquals(testVec, srcVec[i], dstRowStride, {width, height}, fmt, baseVec[bi], scaleVec[bi], + paramRowStride, {paramW, paramH}, paramFmt, epsilon); + } + } +} + +TEST(OpNormalize, tensor_u8_single_channel_stddev_vectorized) +{ + RunTensorStdDevCase(nvcv::FMT_U8, 1920, 4, 2, ParamMode::Scalar); // W%4==0, vector body, scalar base/scale + RunTensorStdDevCase(nvcv::FMT_U8, 1920, 3, 1, ParamMode::PerPixel); // W%4==0, per-pixel base/scale + RunTensorStdDevCase(nvcv::FMT_U8, 23, 5, 2, ParamMode::Scalar); // W%4!=0, scalar-tail branch + RunTensorStdDevCase(nvcv::FMT_U8, 21, 7, 1, ParamMode::PerPixel); // W%4!=0 tail + per-pixel base/scale +} + +static void RunTensorU8SingleChannelStdDevVec4Stride(ParamMode mode) +{ + constexpr int width = 22; + constexpr int height = 3; + constexpr int rowStride = 24; // 4-byte aligned but deliberately not 16-byte aligned. + constexpr int sampleStride = rowStride * height; + constexpr float epsilon = 0.123f; + + NVCVByte *srcPtr = nullptr; + NVCVByte *dstPtr = nullptr; + ASSERT_EQ(cudaSuccess, cudaMalloc(&srcPtr, sampleStride)); + ASSERT_EQ(cudaSuccess, cudaMalloc(&dstPtr, sampleStride)); + + auto wrapTensor = [=](NVCVByte *ptr) + { + nvcv::TensorDataStridedCuda::Buffer buffer{}; + buffer.basePtr = ptr; + buffer.strides[0] = sampleStride; + buffer.strides[1] = rowStride; + buffer.strides[2] = 1; + buffer.strides[3] = 1; + return nvcv::TensorWrapData(nvcv::TensorDataStridedCuda{ + nvcv::TensorShape{{1, height, width, 1}, "NHWC"}, + nvcv::TYPE_U8, buffer + }); + }; + + nvcv::Tensor src = wrapTensor(srcPtr); + nvcv::Tensor dst = wrapTensor(dstPtr); + + std::vector srcVec(width * height); + FillDeterministicSrc(srcVec); + ASSERT_EQ(cudaSuccess, + cudaMemcpy2D(srcPtr, rowStride, srcVec.data(), width, width, height, cudaMemcpyHostToDevice)); + + nvcv::Tensor base; + nvcv::Tensor scale; + std::vector> baseVec; + std::vector> scaleVec; + int paramW; + int paramH; + int paramRowStride; + MakeStdDevParam(mode, nvcv::FMT_F32, 1, width, height, 1, base, scale, baseVec, scaleVec, paramW, paramH, + paramRowStride); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + cvcuda::Normalize op; + EXPECT_NO_THROW(op(stream, src, base, scale, dst, kStdDevGlobalScale, kStdDevGlobalShift, epsilon, + CVCUDA_NORMALIZE_SCALE_IS_STDDEV)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + std::vector testVec(width * height); + ASSERT_EQ(cudaSuccess, + cudaMemcpy2D(testVec.data(), width, dstPtr, rowStride, width, height, cudaMemcpyDeviceToHost)); + ExpectGoldEquals(testVec, srcVec, width, {width, height}, nvcv::FMT_U8, baseVec[0], scaleVec[0], paramRowStride, + {paramW, paramH}, nvcv::FMT_F32, epsilon); + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + ASSERT_EQ(cudaSuccess, cudaFree(srcPtr)); + ASSERT_EQ(cudaSuccess, cudaFree(dstPtr)); +} + +TEST(OpNormalize, tensor_u8_single_channel_stddev_vec4_stride) +{ + RunTensorU8SingleChannelStdDevVec4Stride(ParamMode::PerPixel); +} + +TEST(OpNormalize, tensor_u8_single_channel_stddev_vec4_scalar_broadcast) +{ + RunTensorU8SingleChannelStdDevVec4Stride(ParamMode::Scalar); +} + +// Bit-exact coverage for the vectorized single-channel F32 (NHWC) inverse-std-dev path. The reference +// is the device scalar kernel itself (run on the same data via the 3-channel F32 dispatch, which is +// untouched and uses the per-element normalizeInvStdDevKernel): the spec's bit-exact contract is that +// the vectorized path's output is byte-for-byte identical to the scalar kernel's, not to a host re- +// derivation (device FMA contraction differs from a naive host expression). Single-channel input is +// compared per element against one channel of a 3-channel run fed the identical per-pixel values, so +// any difference is purely the vectorization. Widths that are and are not a multiple of 16 (the float4 +// ILP block) cover both the vector body and the scalar tail. +static void RunTensorF32SingleChannelStdDevCase(int width, int height, int numImages) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + const float epsilon = 0.123f; + const float baseVal = 2.5f; + const float scaleVal = 0.8f; + + // Deterministic per-pixel source values, shared by the single-channel (vectorized) run and the + // 3-channel (scalar reference) run. + std::vector> srcVec(numImages); + for (int i = 0; i < numImages; ++i) + { + srcVec[i].resize(static_cast(height) * width); + for (int y = 0; y < height; ++y) + { + for (int x = 0; x < width; ++x) + { + srcVec[i][y * width + x] = 7.0f + static_cast(i) * 1.5f + static_cast(y) * 0.25f + + static_cast(x) * 0.5f; + } + } + } + + cvcuda::Normalize op; + + // Run 1: single-channel F32 -> vectorized float4 ILP path. + std::vector> vecOut(numImages); + { + nvcv::Tensor imgSrc = nvcv::util::CreateTensor(numImages, width, height, nvcv::FMT_F32); + nvcv::Tensor imgDst = nvcv::util::CreateTensor(numImages, width, height, nvcv::FMT_F32); + auto sAcc = nvcv::TensorDataAccessStridedImagePlanar::Create(*imgSrc.exportData()); + auto dAcc = nvcv::TensorDataAccessStridedImagePlanar::Create(*imgDst.exportData()); + ASSERT_TRUE(sAcc && dAcc); + for (int i = 0; i < numImages; ++i) + ASSERT_EQ(cudaSuccess, + cudaMemcpy2D(sAcc->sampleData(i), sAcc->rowStride(), srcVec[i].data(), width * sizeof(float), + width * sizeof(float), height, cudaMemcpyHostToDevice)); + + nvcv::Tensor imgBase(1, {1, 1}, nvcv::FMT_F32); + nvcv::Tensor imgScale(1, {1, 1}, nvcv::FMT_F32); + UploadParamTensor(imgBase, {baseVal}); + UploadParamTensor(imgScale, {scaleVal}); + + EXPECT_NO_THROW(op(stream, imgSrc, imgBase, imgScale, imgDst, kStdDevGlobalScale, kStdDevGlobalShift, epsilon, + CVCUDA_NORMALIZE_SCALE_IS_STDDEV)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + for (int i = 0; i < numImages; ++i) + { + vecOut[i].resize(static_cast(height) * width); + ASSERT_EQ(cudaSuccess, + cudaMemcpy2D(vecOut[i].data(), width * sizeof(float), dAcc->sampleData(i), dAcc->rowStride(), + width * sizeof(float), height, cudaMemcpyDeviceToHost)); + } + } + + // Run 2: 3-channel F32 with the same value replicated to every channel -> scalar reference kernel. + std::vector> refOut(numImages); + { + nvcv::Tensor imgSrc = nvcv::util::CreateTensor(numImages, width, height, nvcv::FMT_RGBf32); + nvcv::Tensor imgDst = nvcv::util::CreateTensor(numImages, width, height, nvcv::FMT_RGBf32); + auto sAcc = nvcv::TensorDataAccessStridedImagePlanar::Create(*imgSrc.exportData()); + auto dAcc = nvcv::TensorDataAccessStridedImagePlanar::Create(*imgDst.exportData()); + ASSERT_TRUE(sAcc && dAcc); + for (int i = 0; i < numImages; ++i) + { + std::vector interleaved(static_cast(height) * width * 3); + for (int p = 0; p < height * width; ++p) + interleaved[p * 3 + 0] = interleaved[p * 3 + 1] = interleaved[p * 3 + 2] = srcVec[i][p]; + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(sAcc->sampleData(i), sAcc->rowStride(), interleaved.data(), + width * 3 * sizeof(float), width * 3 * sizeof(float), height, + cudaMemcpyHostToDevice)); + } + + // Scalar (1x1x1x1) base/scale broadcasts over the channels too, so each channel sees identical + // base/scale -- matching the single-channel run's per-element inputs exactly. + nvcv::Tensor imgBase(1, {1, 1}, nvcv::FMT_F32); + nvcv::Tensor imgScale(1, {1, 1}, nvcv::FMT_F32); + UploadParamTensor(imgBase, {baseVal}); + UploadParamTensor(imgScale, {scaleVal}); + + EXPECT_NO_THROW(op(stream, imgSrc, imgBase, imgScale, imgDst, kStdDevGlobalScale, kStdDevGlobalShift, epsilon, + CVCUDA_NORMALIZE_SCALE_IS_STDDEV)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + for (int i = 0; i < numImages; ++i) + { + std::vector interleaved(static_cast(height) * width * 3); + ASSERT_EQ(cudaSuccess, + cudaMemcpy2D(interleaved.data(), width * 3 * sizeof(float), dAcc->sampleData(i), + dAcc->rowStride(), width * 3 * sizeof(float), height, cudaMemcpyDeviceToHost)); + refOut[i].resize(static_cast(height) * width); + for (int p = 0; p < height * width; ++p) refOut[i][p] = interleaved[p * 3 + 0]; + } + } + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + + for (int i = 0; i < numImages; ++i) + { + SCOPED_TRACE(i); + // Byte-for-byte identical to the scalar kernel's output. + EXPECT_EQ(0, std::memcmp(refOut[i].data(), vecOut[i].data(), refOut[i].size() * sizeof(float))); + } +} + +TEST(OpNormalize, tensor_f32_single_channel_stddev_vectorized) +{ + RunTensorF32SingleChannelStdDevCase(1920, 4, 2); // W%16==0, float4 vector body + RunTensorF32SingleChannelStdDevCase(67, 5, 2); // W%16!=0, vector body + scalar tail + RunTensorF32SingleChannelStdDevCase(9, 3, 1); // small width, mostly tail +} + +// Single-channel F32 var-shape inverse-std-dev: drives the vectorized float4 ILP var-shape path and +// byte-compares it against the untouched scalar var-shape kernel. The reference run uses a 3-channel +// F32 var-shape batch (replicating each value to all 3 channels) with a scalar (1x1) base/scale, which +// broadcasts identically across channels -- so each channel's per-element inputs match the +// single-channel run exactly, and the 3-channel batch never takes the single-channel vec path. +// Per-image widths cover a full float4-group body (W%4==0) and a sub-group tail (W%4!=0). +static void RunVarShapeF32SingleChannelStdDevCase(const std::vector &sizes) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + const float epsilon = 0.123f; + const float baseVal = 2.5f; + const float scaleVal = 0.8f; + const auto numImages = static_cast(sizes.size()); + + // Deterministic per-pixel source values, shared by the single-channel (vectorized) run and the + // 3-channel (scalar reference) run. + std::vector> srcVec(numImages); + for (int i = 0; i < numImages; ++i) + { + srcVec[i].resize(static_cast(sizes[i].h) * sizes[i].w); + for (int y = 0; y < sizes[i].h; ++y) + for (int x = 0; x < sizes[i].w; ++x) + srcVec[i][y * sizes[i].w + x] = 7.0f + static_cast(i) * 1.5f + static_cast(y) * 0.25f + + static_cast(x) * 0.5f; + } + + cvcuda::Normalize op; + + nvcv::Tensor imgBase(1, {1, 1}, nvcv::FMT_F32); + nvcv::Tensor imgScale(1, {1, 1}, nvcv::FMT_F32); + UploadParamTensor(imgBase, {baseVal}); + UploadParamTensor(imgScale, {scaleVal}); + + // Run 1: single-channel F32 var-shape -> vectorized float4 ILP path. + std::vector> vecOut(numImages); + { + std::vector imgSrc; + std::vector imgDst; + for (int i = 0; i < numImages; ++i) + { + imgSrc.emplace_back(sizes[i], nvcv::FMT_F32); + imgDst.emplace_back(sizes[i], nvcv::FMT_F32); + auto sData = imgSrc[i].exportData(); + ASSERT_NE(sData, nvcv::NullOpt); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(sData->plane(0).basePtr, sData->plane(0).rowStride, srcVec[i].data(), + sizes[i].w * sizeof(float), sizes[i].w * sizeof(float), sizes[i].h, + cudaMemcpyHostToDevice)); + } + nvcv::ImageBatchVarShape batchSrc(numImages); + nvcv::ImageBatchVarShape batchDst(numImages); + batchSrc.pushBack(imgSrc.begin(), imgSrc.end()); + batchDst.pushBack(imgDst.begin(), imgDst.end()); + + EXPECT_NO_THROW(op(stream, batchSrc, imgBase, imgScale, batchDst, kStdDevGlobalScale, kStdDevGlobalShift, + epsilon, CVCUDA_NORMALIZE_SCALE_IS_STDDEV)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + for (int i = 0; i < numImages; ++i) + { + auto dData = imgDst[i].exportData(); + ASSERT_NE(dData, nvcv::NullOpt); + vecOut[i].resize(static_cast(sizes[i].h) * sizes[i].w); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(vecOut[i].data(), sizes[i].w * sizeof(float), dData->plane(0).basePtr, + dData->plane(0).rowStride, sizes[i].w * sizeof(float), sizes[i].h, + cudaMemcpyDeviceToHost)); + } + } + + // Run 2: 3-channel F32 var-shape with the same value replicated to every channel -> scalar kernel. + std::vector> refOut(numImages); + { + std::vector imgSrc; + std::vector imgDst; + for (int i = 0; i < numImages; ++i) + { + imgSrc.emplace_back(sizes[i], nvcv::FMT_RGBf32); + imgDst.emplace_back(sizes[i], nvcv::FMT_RGBf32); + std::vector interleaved(static_cast(sizes[i].h) * sizes[i].w * 3); + for (int p = 0; p < sizes[i].h * sizes[i].w; ++p) + interleaved[p * 3 + 0] = interleaved[p * 3 + 1] = interleaved[p * 3 + 2] = srcVec[i][p]; + auto sData = imgSrc[i].exportData(); + ASSERT_NE(sData, nvcv::NullOpt); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(sData->plane(0).basePtr, sData->plane(0).rowStride, interleaved.data(), + sizes[i].w * 3 * sizeof(float), sizes[i].w * 3 * sizeof(float), + sizes[i].h, cudaMemcpyHostToDevice)); + } + nvcv::ImageBatchVarShape batchSrc(numImages); + nvcv::ImageBatchVarShape batchDst(numImages); + batchSrc.pushBack(imgSrc.begin(), imgSrc.end()); + batchDst.pushBack(imgDst.begin(), imgDst.end()); + + EXPECT_NO_THROW(op(stream, batchSrc, imgBase, imgScale, batchDst, kStdDevGlobalScale, kStdDevGlobalShift, + epsilon, CVCUDA_NORMALIZE_SCALE_IS_STDDEV)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + for (int i = 0; i < numImages; ++i) + { + auto dData = imgDst[i].exportData(); + ASSERT_NE(dData, nvcv::NullOpt); + std::vector interleaved(static_cast(sizes[i].h) * sizes[i].w * 3); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(interleaved.data(), sizes[i].w * 3 * sizeof(float), + dData->plane(0).basePtr, dData->plane(0).rowStride, + sizes[i].w * 3 * sizeof(float), sizes[i].h, cudaMemcpyDeviceToHost)); + refOut[i].resize(static_cast(sizes[i].h) * sizes[i].w); + for (int p = 0; p < sizes[i].h * sizes[i].w; ++p) refOut[i][p] = interleaved[p * 3 + 0]; + } + } + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + + for (int i = 0; i < numImages; ++i) + { + SCOPED_TRACE(i); + // Byte-for-byte identical to the scalar var-shape kernel's output. + EXPECT_EQ(0, std::memcmp(refOut[i].data(), vecOut[i].data(), refOut[i].size() * sizeof(float))); + } +} + +TEST(OpNormalize, varshape_f32_single_channel_stddev_vectorized) +{ + RunVarShapeF32SingleChannelStdDevCase({ + {1920, 4}, // W%4==0, full float4-group body + {1923, 3} // W%4!=0, body + scalar tail + }); + RunVarShapeF32SingleChannelStdDevCase({ + {9, 3}, // small width, mostly tail + {8, 5} // exactly two float4 groups + }); +} + +TEST(OpNormalize, tensor_interleaved_per_channel_stddev_hoist) +{ + RunTensorStdDevCase(nvcv::FMT_RGB8, 1920, 4, 2, ParamMode::PerChannel); // uchar3, NIX vector body + RunTensorStdDevCase(nvcv::FMT_RGB8, 257, 5, 1, ParamMode::PerChannel); // uchar3, NIX tail + RunTensorStdDevCase(nvcv::FMT_RGBA8, 1920, 4, 2, ParamMode::PerChannel); // uchar4, NIX vector body + RunTensorStdDevCase(nvcv::FMT_RGBA8, 259, 3, 1, ParamMode::PerChannel); // uchar4, NIX tail +} + +// Consolidated inverse-std-dev var-shape coverage: single-channel U8 with Scalar param, or +// interleaved RGB8/RGBA8 with PerChannel param. Per-image widths differ so both the vectorized +// uchar4 body / hoisted NIX body and the W-unaligned tail are covered. Each image is compared +// against the shared gold bit-for-bit. +static void RunVarShapeStdDevCase(nvcv::ImageFormat fmt, const std::vector &sizes, ParamMode mode) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + const float epsilon = (mode == ParamMode::PerChannel) ? 0.234f : 0.123f; + const int channels = fmt.numChannels(); + const nvcv::ImageFormat paramFmt = ParamFormatFor(mode, channels); + const auto numImages = static_cast(sizes.size()); + + std::vector imgSrc; + std::vector> srcVec(numImages); + std::vector srcRowStride(numImages); + for (int i = 0; i < numImages; ++i) + { + imgSrc.emplace_back(sizes[i], fmt); + srcRowStride[i] = sizes[i].w * channels; + + srcVec[i].resize(static_cast(sizes[i].h) * srcRowStride[i]); + FillDeterministicSrc(srcVec[i]); + + auto imgData = imgSrc[i].exportData(); + ASSERT_NE(imgData, nvcv::NullOpt); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(imgData->plane(0).basePtr, imgData->plane(0).rowStride, srcVec[i].data(), + srcRowStride[i], srcRowStride[i], sizes[i].h, cudaMemcpyHostToDevice)); + } + nvcv::ImageBatchVarShape batchSrc(numImages); + batchSrc.pushBack(imgSrc.begin(), imgSrc.end()); + + // Scalar param keeps a single host value (single-channel src); PerChannel broadcasts 1x1x1xC. + nvcv::Tensor imgBase; + nvcv::Tensor imgScale; + std::vector> baseVec; + std::vector> scaleVec; + if (mode == ParamMode::PerChannel) + { + int paramW = 1; + int paramH = 1; + int paramRowStride = 1; + MakeStdDevParam(mode, paramFmt, channels, 0, 0, numImages, imgBase, imgScale, baseVec, scaleVec, paramW, paramH, + paramRowStride); + } + else + { + imgBase = nvcv::Tensor(1, {1, 1}, paramFmt); + imgScale = nvcv::Tensor(1, {1, 1}, paramFmt); + baseVec.assign(1, std::vector(1)); + scaleVec.assign(1, std::vector(1)); + FillDeterministicBaseScale(baseVec[0], scaleVec[0]); + UploadParamTensor(imgBase, baseVec[0]); + UploadParamTensor(imgScale, scaleVec[0]); + } + + std::vector imgDst; + for (int i = 0; i < numImages; ++i) + { + imgDst.emplace_back(sizes[i], fmt); + } + nvcv::ImageBatchVarShape batchDst(numImages); + batchDst.pushBack(imgDst.begin(), imgDst.end()); + + cvcuda::Normalize op; + EXPECT_NO_THROW(op(stream, batchSrc, imgBase, imgScale, batchDst, kStdDevGlobalScale, kStdDevGlobalShift, epsilon, + CVCUDA_NORMALIZE_SCALE_IS_STDDEV)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + + const int paramChannels = (mode == ParamMode::PerChannel) ? channels : 0; + const nvcv::Size2D paramSz = {1, 1}; + for (int i = 0; i < numImages; ++i) + { + SCOPED_TRACE(i); + + auto dstData = imgDst[i].exportData(); + ASSERT_NE(dstData, nvcv::NullOpt); + + const int dstRowStride = srcRowStride[i]; + std::vector testVec(static_cast(sizes[i].h) * dstRowStride); + ASSERT_EQ(cudaSuccess, + cudaMemcpy2D(testVec.data(), dstRowStride, dstData->plane(0).basePtr, dstData->plane(0).rowStride, + dstRowStride, sizes[i].h, cudaMemcpyDeviceToHost)); + + ExpectGoldEquals(testVec, srcVec[i], dstRowStride, {sizes[i].w, sizes[i].h}, fmt, baseVec[0], scaleVec[0], + paramChannels, paramSz, paramFmt, epsilon); + } +} + +TEST(OpNormalize, varshape_u8_single_channel_stddev_vectorized) +{ + RunVarShapeStdDevCase(nvcv::FMT_U8, + { + {1920, 4}, + {1923, 3} + }, + ParamMode::Scalar); // vector body + W%4 tail across differing per-image widths + RunVarShapeStdDevCase(nvcv::FMT_U8, + { + {22, 5}, + {21, 7}, + {20, 3} + }, + ParamMode::Scalar); // small tail widths +} + +TEST(OpNormalize, varshape_interleaved_per_channel_stddev_hoist) +{ + RunVarShapeStdDevCase(nvcv::FMT_RGB8, + { + {1920, 4}, + {1923, 3} + }, + ParamMode::PerChannel); // uchar3, NIX body + tail + RunVarShapeStdDevCase(nvcv::FMT_RGBA8, + { + {1920, 4}, + {1925, 3} + }, + ParamMode::PerChannel); // uchar4, NIX body + tail +} + +// Distinct per-channel (or scalar-broadcast) base/scale for the float reference cases. PerChannel +// fills C distinct values; Scalar fills one value and replicates it across channels so the host gold +// (MakeNormalizeGold, indexed per channel) matches the operator's broadcast exactly. +static void MakeFloatRefParams(ParamMode mode, nvcv::ImageFormat paramFmt, int channels, nvcv::Tensor &imgBase, + nvcv::Tensor &imgScale, std::vector &goldBase, std::vector &goldScale) +{ + if (mode == ParamMode::PerChannel) + { + imgBase = nvcv::Tensor( + { + {1, 1, 1, channels}, + nvcv::TENSOR_NHWC + }, + paramFmt.planeDataType(0)); + imgScale = nvcv::Tensor( + { + {1, 1, 1, channels}, + nvcv::TENSOR_NHWC + }, + paramFmt.planeDataType(0)); + goldBase.resize(channels); + goldScale.resize(channels); + FillDeterministicBaseScale(goldBase, goldScale); + UploadParamTensor(imgBase, goldBase); + UploadParamTensor(imgScale, goldScale); + } + else + { + imgBase = nvcv::Tensor(1, {1, 1}, paramFmt); + imgScale = nvcv::Tensor(1, {1, 1}, paramFmt); + std::vector b(1); + std::vector s(1); + FillDeterministicBaseScale(b, s); + UploadParamTensor(imgBase, b); + UploadParamTensor(imgScale, s); + goldBase.assign(channels, b[0]); + goldScale.assign(channels, s[0]); + } +} + +// Independent math-correctness backstop for the FLOAT paths (single-channel F32 and interleaved +// float3/float4). The reference is the host C++ MakeNormalizeGold(), NOT the device scalar kernel, so +// EXPECT_NEAR(1e-4) is used: device FMA contraction differs from a naive host expression bit-for-bit. +// This is the only independent reference that exercises distinct per-channel base/scale on the +// interleaved float kernels; it complements the bit-identity memcmp-vs-scalar checks (which feed every +// channel the same value). Covers Scalar and PerChannel params, plain (normalScale) and inverse-std- +// dev (scaleIsStdDev) flags, and widths that do and do not fill the float4 vector body (scalar tail). +static void RunTensorFloatRefCase(nvcv::ImageFormat fmt, int width, int height, int numImages, ParamMode mode, + uint32_t flags) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + const float epsilon = 0.234f; + const int channels = fmt.numChannels(); + const nvcv::ImageFormat paramFmt = ParamFormatFor(mode, channels); + const int rowElems = width * channels; + + nvcv::Tensor imgSrc = nvcv::util::CreateTensor(numImages, width, height, fmt); + auto srcData = imgSrc.exportData(); + ASSERT_NE(nullptr, srcData); + auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcData); + ASSERT_TRUE(srcAccess); + + std::vector> srcVec(numImages); + for (int i = 0; i < numImages; ++i) + { + srcVec[i] = MakePlanarHostImage(i, width, height, channels); + ASSERT_EQ(cudaSuccess, + cudaMemcpy2D(srcAccess->sampleData(i), srcAccess->rowStride(), srcVec[i].data(), + rowElems * sizeof(float), rowElems * sizeof(float), height, cudaMemcpyHostToDevice)); + } + + nvcv::Tensor imgBase; + nvcv::Tensor imgScale; + std::vector goldBase; + std::vector goldScale; + MakeFloatRefParams(mode, paramFmt, channels, imgBase, imgScale, goldBase, goldScale); + + nvcv::Tensor imgDst = nvcv::util::CreateTensor(numImages, width, height, fmt); + + cvcuda::Normalize op; + EXPECT_NO_THROW( + op(stream, imgSrc, imgBase, imgScale, imgDst, kStdDevGlobalScale, kStdDevGlobalShift, epsilon, flags)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + + auto dstData = imgDst.exportData(); + ASSERT_NE(nullptr, dstData); + auto dstAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*dstData); + ASSERT_TRUE(dstAccess); + + for (int i = 0; i < numImages; ++i) + { + SCOPED_TRACE(i); + std::vector testVec(static_cast(height) * rowElems); + ASSERT_EQ(cudaSuccess, + cudaMemcpy2D(testVec.data(), rowElems * sizeof(float), dstAccess->sampleData(i), + dstAccess->rowStride(), rowElems * sizeof(float), height, cudaMemcpyDeviceToHost)); + std::vector gold = MakeNormalizeGold(srcVec[i], width, height, channels, goldBase, goldScale, + kStdDevGlobalScale, kStdDevGlobalShift, flags, epsilon); + ExpectPlanarVectorsNear(gold, testVec); + } +} + +// Var-shape twin of RunTensorFloatRefCase: per-image widths differ so both the float4 vector body and +// the W-unaligned tail are exercised; each image is compared against the independent host gold. +static void RunVarShapeFloatRefCase(nvcv::ImageFormat fmt, const std::vector &sizes, ParamMode mode, + uint32_t flags) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + const float epsilon = 0.234f; + const int channels = fmt.numChannels(); + const nvcv::ImageFormat paramFmt = ParamFormatFor(mode, channels); + const auto numImages = static_cast(sizes.size()); + + std::vector imgSrc; + std::vector> srcVec(numImages); + std::vector rowElems(numImages); + for (int i = 0; i < numImages; ++i) + { + imgSrc.emplace_back(sizes[i], fmt); + rowElems[i] = sizes[i].w * channels; + srcVec[i] = MakePlanarHostImage(i, sizes[i].w, sizes[i].h, channels); + + auto imgData = imgSrc[i].exportData(); + ASSERT_NE(imgData, nvcv::NullOpt); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(imgData->plane(0).basePtr, imgData->plane(0).rowStride, srcVec[i].data(), + rowElems[i] * sizeof(float), rowElems[i] * sizeof(float), sizes[i].h, + cudaMemcpyHostToDevice)); + } + nvcv::ImageBatchVarShape batchSrc(numImages); + batchSrc.pushBack(imgSrc.begin(), imgSrc.end()); + + nvcv::Tensor imgBase; + nvcv::Tensor imgScale; + std::vector goldBase; + std::vector goldScale; + MakeFloatRefParams(mode, paramFmt, channels, imgBase, imgScale, goldBase, goldScale); + + std::vector imgDst; + for (int i = 0; i < numImages; ++i) imgDst.emplace_back(sizes[i], fmt); + nvcv::ImageBatchVarShape batchDst(numImages); + batchDst.pushBack(imgDst.begin(), imgDst.end()); + + cvcuda::Normalize op; + EXPECT_NO_THROW( + op(stream, batchSrc, imgBase, imgScale, batchDst, kStdDevGlobalScale, kStdDevGlobalShift, epsilon, flags)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + + for (int i = 0; i < numImages; ++i) + { + SCOPED_TRACE(i); + auto dstData = imgDst[i].exportData(); + ASSERT_NE(dstData, nvcv::NullOpt); + std::vector testVec(static_cast(sizes[i].h) * rowElems[i]); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(testVec.data(), rowElems[i] * sizeof(float), dstData->plane(0).basePtr, + dstData->plane(0).rowStride, rowElems[i] * sizeof(float), sizes[i].h, + cudaMemcpyDeviceToHost)); + std::vector gold + = MakeNormalizeGold(srcVec[i], sizes[i].w, sizes[i].h, channels, goldBase, goldScale, + kStdDevGlobalScale, kStdDevGlobalShift, flags, epsilon); + ExpectPlanarVectorsNear(gold, testVec); + } +} + +TEST(OpNormalize, tensor_f32_single_channel_ref) +{ + for (uint32_t flags : {normalScale, scaleIsStdDev}) + { + RunTensorFloatRefCase(nvcv::FMT_F32, 1920, 4, 2, ParamMode::Scalar, flags); // W%16==0, float4 ILP body + RunTensorFloatRefCase(nvcv::FMT_F32, 21, 7, 1, ParamMode::Scalar, flags); // W%16!=0, scalar tail + } +} + +TEST(OpNormalize, tensor_interleaved_float_ref) +{ + for (uint32_t flags : {normalScale, scaleIsStdDev}) + for (nvcv::ImageFormat fmt : {nvcv::FMT_RGBf32, nvcv::FMT_RGBAf32}) + for (ParamMode mode : {ParamMode::Scalar, ParamMode::PerChannel}) + { + RunTensorFloatRefCase(fmt, 1920, 4, 2, mode, flags); // vector body + RunTensorFloatRefCase(fmt, 257, 5, 1, mode, flags); // unaligned tail + } +} + +TEST(OpNormalize, varshape_f32_single_channel_ref) +{ + for (uint32_t flags : {normalScale, scaleIsStdDev}) + RunVarShapeFloatRefCase(nvcv::FMT_F32, + { + {1920, 4}, + { 21, 7} + }, + ParamMode::Scalar, flags); +} + +TEST(OpNormalize, varshape_interleaved_float_ref) +{ + for (uint32_t flags : {normalScale, scaleIsStdDev}) + for (nvcv::ImageFormat fmt : {nvcv::FMT_RGBf32, nvcv::FMT_RGBAf32}) + for (ParamMode mode : {ParamMode::Scalar, ParamMode::PerChannel}) + RunVarShapeFloatRefCase(fmt, + { + {1920, 4}, + { 257, 3} + }, + mode, flags); +} + +TEST_P(OpNormalize, varshape_correct_output) +{ + cudaStream_t stream; + EXPECT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + int width = GetParamValue<0>(); + int height = GetParamValue<1>(); + int numImages = GetParamValue<2>(); + bool scalarBase = GetParamValue<3>(); + bool scalarScale = GetParamValue<4>(); + uint32_t flags = GetParamValue<5>(); + float globalScale = GetParamValue<6>(); + float globalShift = GetParamValue<7>(); + float epsilon = GetParamValue<8>(); + + nvcv::ImageFormat baseFormat = (scalarBase ? nvcv::FMT_F32 : nvcv::FMT_RGBAf32); + nvcv::ImageFormat scaleFormat = (scalarScale ? nvcv::FMT_F32 : nvcv::FMT_RGBAf32); + + nvcv::ImageFormat fmt = nvcv::FMT_RGBA8; + + std::default_random_engine rng; + + // Create input varshape + + std::uniform_int_distribution udistWidth(ScaledSize(width, 0.8), ScaledSize(width, 1.1)); + std::uniform_int_distribution udistHeight(ScaledSize(height, 0.8), ScaledSize(height, 1.1)); + + std::vector imgSrc; + + std::vector> srcVec(numImages); + std::vector srcVecRowStride(numImages); + + for (int i = 0; i < numImages; ++i) + { + imgSrc.emplace_back(nvcv::Size2D{udistWidth(rng), udistHeight(rng)}, fmt); + + int srcRowStride = imgSrc[i].size().w * fmt.numChannels(); + srcVecRowStride[i] = srcRowStride; + + std::uniform_int_distribution udist(0, 255); + + srcVec[i].resize(imgSrc[i].size().h * srcRowStride); + std::ranges::generate(srcVec[i], [&udist, &rng]() { return udist(rng); }); + + auto imgData = imgSrc[i].exportData(); + ASSERT_NE(imgData, nvcv::NullOpt); + + // Copy input data to the GPU + ASSERT_EQ(cudaSuccess, + cudaMemcpy2D(imgData->plane(0).basePtr, imgData->plane(0).rowStride, srcVec[i].data(), srcRowStride, + srcRowStride, // vec has no padding + imgSrc[i].size().h, cudaMemcpyHostToDevice)); + } + + nvcv::ImageBatchVarShape batchSrc(numImages); + batchSrc.pushBack(imgSrc.begin(), imgSrc.end()); + + // Create base tensor + nvcv::Tensor imgBase( + { + {1, 1, 1, baseFormat.numChannels()}, + nvcv::TENSOR_NHWC + }, + baseFormat.planeDataType(0)); + std::vector baseVec(baseFormat.numChannels()); + { + auto baseData = imgBase.exportData(); + ASSERT_NE(nullptr, baseData); + auto baseAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*baseData); + ASSERT_TRUE(baseAccess); + + std::uniform_real_distribution baseDist(0, 255.f); + std::ranges::generate(baseVec, [&baseDist, &rng]() { return baseDist(rng); }); + + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(baseAccess->sampleData(0), baseAccess->rowStride(), baseVec.data(), + baseVec.size() * sizeof(float), + baseVec.size() * sizeof(float), // vec has no padding + 1, cudaMemcpyHostToDevice)); + } + + // Create scale tensor + nvcv::Tensor imgScale( + { + {1, 1, 1, scaleFormat.numChannels()}, + nvcv::TENSOR_NHWC + }, + scaleFormat.planeDataType(0)); + std::vector scaleVec(scaleFormat.numChannels()); + { + auto scaleData = imgScale.exportData(); + ASSERT_NE(nullptr, scaleData); + auto scaleAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*scaleData); + ASSERT_TRUE(scaleAccess); + + std::uniform_real_distribution scaleDist(0, 1.f); + std::ranges::generate(scaleVec, [&scaleDist, &rng]() { return scaleDist(rng); }); + + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(scaleAccess->sampleData(0), scaleAccess->rowStride(), scaleVec.data(), + scaleVec.size() * sizeof(float), + scaleVec.size() * sizeof(float), // vec has no padding + 1, cudaMemcpyHostToDevice)); + } + + // Create output varshape + std::vector imgDst; + for (int i = 0; i < numImages; ++i) + { + imgDst.emplace_back(imgSrc[i].size(), imgSrc[i].format()); + } + nvcv::ImageBatchVarShape batchDst(numImages); + batchDst.pushBack(imgDst.begin(), imgDst.end()); + + // Generate test result + cvcuda::Normalize normalizeOp; + EXPECT_NO_THROW( + normalizeOp(stream, batchSrc, imgBase, imgScale, batchDst, globalScale, globalShift, epsilon, flags)); + + // Get test data back + EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + + // Check test data against gold + for (int i = 0; i < numImages; ++i) + { + SCOPED_TRACE(i); + + const auto srcData = imgSrc[i].exportData(); + assert(srcData->numPlanes() == 1); + int sampleWidth = srcData->plane(0).width; + int sampleHeight = srcData->plane(0).height; + + const auto dstData = imgDst[i].exportData(); + assert(dstData->numPlanes() == 1); + + int dstRowStride = srcVecRowStride[i]; + + std::vector testVec(sampleHeight * dstRowStride); + + // Copy output data to Host + ASSERT_EQ(cudaSuccess, + cudaMemcpy2D(testVec.data(), dstRowStride, dstData->plane(0).basePtr, dstData->plane(0).rowStride, + dstRowStride, // vec has no padding + sampleHeight, cudaMemcpyDeviceToHost)); + + std::vector goldVec(sampleHeight * dstRowStride); + + // Generate gold result + Normalize({ + goldVec, + dstRowStride, + srcVec[i], + srcVecRowStride[i], + {sampleWidth, sampleHeight}, + fmt, + baseVec, + 0, + { 1, 1}, + baseFormat, + scaleVec, + 0, + { 1, 1}, + scaleFormat, + globalScale, + globalShift, + epsilon, + flags + }); + + EXPECT_THAT(testVec, t::ElementsAreArray(goldVec)); + } +} + +// Shared body for the planar param-sweep correctness tests. Builds random interleaved input for +// numImages samples, runs Normalize with scalar-or-per-channel base/scale, and compares each +// re-interleaved output against the shared gold. varShape selects the image-batch path; otherwise a +// single NCHW tensor with numImages samples is used. +static void RunPlanarParamSweepCase(int width, int height, int numImages, bool scalarBase, bool scalarScale, + uint32_t flags, float globalScale, float globalShift, float epsilon, bool varShape, + bool spatialParams = false) +{ + cudaStream_t stream; + EXPECT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + const bool baseScalar = scalarBase && !spatialParams; + const bool scaleScalar = scalarScale && !spatialParams; + + nvcv::ImageFormat fmt = nvcv::FMT_RGBA8p; + nvcv::ImageFormat baseFormat = (baseScalar ? nvcv::FMT_F32 : nvcv::FMT_RGBAf32p); + nvcv::ImageFormat scaleFormat = (scaleScalar ? nvcv::FMT_F32 : nvcv::FMT_RGBAf32p); + nvcv::ImageFormat baseHelperFmt = (baseScalar ? nvcv::ImageFormat{NVCV_IMAGE_FORMAT_F32} : nvcv::FMT_RGBAf32); + nvcv::ImageFormat scaleHelperFmt = (scaleScalar ? nvcv::ImageFormat{NVCV_IMAGE_FORMAT_F32} : nvcv::FMT_RGBAf32); + const int numChannels = fmt.numChannels(); + const int baseChannels = baseFormat.numChannels(); + const int scaleChannels = scaleFormat.numChannels(); + + std::default_random_engine rng; + + std::vector> srcVec(numImages); + std::vector rowStride(numImages); + std::vector sampleW(numImages); + std::vector sampleH(numImages); + + // Source/destination containers; only the variant selected by varShape is populated. + nvcv::Tensor tensorSrc; + nvcv::Tensor tensorDst; + std::vector imgSrc; + std::vector imgDst; + nvcv::ImageBatchVarShape batchSrc(numImages); + nvcv::ImageBatchVarShape batchDst(numImages); + + auto fillSrc = [&](int i, int w, int h) + { + sampleW[i] = w; + sampleH[i] = h; + rowStride[i] = w * numChannels; + srcVec[i].resize(static_cast(h) * rowStride[i]); + std::uniform_int_distribution udist(0, 255); + std::ranges::generate(srcVec[i], [&]() { return udist(rng); }); + }; + + if (varShape) + { + std::uniform_int_distribution udistWidth(ScaledSize(width, 0.8), ScaledSize(width, 1.1)); + std::uniform_int_distribution udistHeight(ScaledSize(height, 0.8), ScaledSize(height, 1.1)); + for (int i = 0; i < numImages; ++i) + { + nvcv::Size2D size{udistWidth(rng), udistHeight(rng)}; + imgSrc.emplace_back(size, fmt); + imgDst.emplace_back(size, fmt); + fillSrc(i, size.w, size.h); + ASSERT_NO_FATAL_FAILURE(UploadPlanarImage(imgSrc[i], srcVec[i])); + } + batchSrc.pushBack(imgSrc.begin(), imgSrc.end()); + batchDst.pushBack(imgDst.begin(), imgDst.end()); + } + else + { + tensorSrc = nvcv::util::CreateTensor(numImages, width, height, fmt); + tensorDst = nvcv::util::CreateTensor(numImages, width, height, fmt); + + auto srcData = tensorSrc.exportData(); + ASSERT_NE(nullptr, srcData); + auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcData); + ASSERT_TRUE(srcAccess); + ASSERT_TRUE(srcData->layout() == nvcv::TENSOR_NCHW || srcData->layout() == nvcv::TENSOR_CHW); + + for (int i = 0; i < numImages; ++i) + { + fillSrc(i, width, height); + ASSERT_NO_FATAL_FAILURE(UploadPlanarTensorSample(*srcAccess, i, srcVec[i], width, height, numChannels)); + } + } + + // base/scale: scalar [1,1,1,1] or per-channel [1,C,1,1]; broadcasting is handled in the kernel. + nvcv::Tensor imgBase; + nvcv::Tensor imgScale; + std::vector baseVec; + std::vector scaleVec; + if (spatialParams) + { + ASSERT_FALSE(varShape); + baseVec = MakeAndUploadSpatialPlanarParam(imgBase, width, height, numChannels, 255.f); + scaleVec = MakeAndUploadSpatialPlanarParam(imgScale, width, height, numChannels, 1.f); + } + else + { + baseVec = MakeAndUploadRandomParam(imgBase, baseFormat, baseChannels, 255.f, rng); + scaleVec = MakeAndUploadRandomParam(imgScale, scaleFormat, scaleChannels, 1.f, rng); + } + + cvcuda::Normalize normalizeOp; + if (varShape) + { + EXPECT_NO_THROW( + normalizeOp(stream, batchSrc, imgBase, imgScale, batchDst, globalScale, globalShift, epsilon, flags)); + } + else + { + EXPECT_NO_THROW( + normalizeOp(stream, tensorSrc, imgBase, imgScale, tensorDst, globalScale, globalShift, epsilon, flags)); + } + + EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + + // For the tensor path, hold the output data/access alive for the compare loop. + nvcv::Optional dstData; + nvcv::Optional dstAccess; + if (!varShape) + { + dstData = tensorDst.exportData(); + ASSERT_TRUE(dstData); + dstAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*dstData); + ASSERT_TRUE(dstAccess); + } + + // Param geometry is identical for every sample, so compute it once outside the compare loop. + const nvcv::Size2D baseSize = spatialParams ? nvcv::Size2D{width, height} : nvcv::Size2D{1, 1}; + const nvcv::Size2D scaleSize = spatialParams ? nvcv::Size2D{width, height} : nvcv::Size2D{1, 1}; + const int baseRowStride = spatialParams ? width * numChannels : baseChannels; + const int scaleRowStride = spatialParams ? width * numChannels : scaleChannels; + + for (int i = 0; i < numImages; ++i) + { + SCOPED_TRACE(i); + std::vector testVec + = varShape ? DownloadPlanarImage(imgDst[i]) + : DownloadPlanarTensorSample(*dstAccess, i, sampleW[i], sampleH[i], numChannels); + ASSERT_NO_FATAL_FAILURE( + ExpectPlanarNormalizeMatchesGold(testVec, srcVec[i], rowStride[i], sampleW[i], sampleH[i], fmt, baseVec, + baseRowStride, baseSize, baseHelperFmt, scaleVec, scaleRowStride, + scaleSize, scaleHelperFmt, globalScale, globalShift, epsilon, flags)); + } +} + +TEST_P(OpNormalize, tensor_planar_correct_output) +{ + ASSERT_NO_FATAL_FAILURE(RunPlanarParamSweepCase(GetParamValue<0>(), GetParamValue<1>(), GetParamValue<2>(), + GetParamValue<3>(), GetParamValue<4>(), GetParamValue<5>(), + GetParamValue<6>(), GetParamValue<7>(), GetParamValue<8>(), + /*varShape=*/false)); +} + +TEST_P(OpNormalize, varshape_planar_correct_output) +{ + ASSERT_NO_FATAL_FAILURE(RunPlanarParamSweepCase(GetParamValue<0>(), GetParamValue<1>(), GetParamValue<2>(), + GetParamValue<3>(), GetParamValue<4>(), GetParamValue<5>(), + GetParamValue<6>(), GetParamValue<7>(), GetParamValue<8>(), + /*varShape=*/true)); +} + +TEST(OpNormalize, tensor_planar_spatial_params_vec4_correct_output) +{ + RunPlanarParamSweepCase(65, 3, 1, false, false, normalScale, 1.25f, 0.5f, 0.f, + /*varShape=*/false, /*spatialParams=*/true); +} + +TEST(OpNormalize, tensor_planar_rgb8_three_channel_correct_output) +{ + RunPlanarTensorNormalizeCase(nvcv::FMT_RGB8p, 13, 11, 2); +} + +TEST(OpNormalize, varshape_planar_rgb8_three_channel_correct_output) +{ + RunPlanarVarShapeNormalizeCase(nvcv::FMT_RGB8p, 13, 11, 2); +} + +TEST(OpNormalize, tensor_planar_f32_correct_output) +{ + RunPlanarTensorNormalizeCase(nvcv::FMT_RGBAf32p, 9, 7, 2); +} + +TEST(OpNormalize, varshape_planar_f32_correct_output) +{ + RunPlanarVarShapeNormalizeCase(nvcv::FMT_RGBAf32p, 9, 7, 2); +} + +TEST(OpNormalize, tensor_planar_s16_correct_output) +{ + const nvcv::ImageFormat fmt{nvcv::ColorModel::RGB, nvcv::CSPEC_UNDEFINED, nvcv::MemLayout::PITCH_LINEAR, + nvcv::DataKind::SIGNED, nvcv::Swizzle::S_XYZ0, nvcv::Packing::X16, + nvcv::Packing::X16, nvcv::Packing::X16}; + RunPlanarTensorNormalizeCase(fmt, 13, 11, 2); +} + +TEST(OpNormalize, tensor_planar_s8_preserves_signed_values) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + constexpr int width = 8; + constexpr int height = 1; + constexpr int channels = 3; + const nvcv::TensorShape shape({1, channels, height, width}, nvcv::TENSOR_NCHW); + nvcv::Tensor src(shape, nvcv::TYPE_S8); + nvcv::Tensor dst(shape, nvcv::TYPE_S8); + nvcv::Tensor base(nvcv::TensorShape({1, channels, 1, 1}, nvcv::TENSOR_NCHW), nvcv::TYPE_F32); + nvcv::Tensor scale(nvcv::TensorShape({1, channels, 1, 1}, nvcv::TENSOR_NCHW), nvcv::TYPE_F32); + + ASSERT_NO_FATAL_FAILURE(FillPlanarParamTensor(base, {0.f, 0.f, 0.f})); + ASSERT_NO_FATAL_FAILURE(FillPlanarParamTensor(scale, {1.f, 1.f, 1.f})); + + const std::vector input{-128, -127, -126, -96, -95, -94, -64, -63, -62, -1, 0, 1, + 0, 1, 2, 31, 32, 33, 63, 64, 65, 125, 126, 127}; + auto srcData = src.exportData(); + ASSERT_NE(nullptr, srcData); + auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcData); + ASSERT_TRUE(srcAccess); + ASSERT_EQ(0, srcAccess->rowStride() % 4); + ASSERT_EQ(0, srcAccess->chStride() % 4); + ASSERT_EQ(0, srcAccess->sampleStride() % 4); + ASSERT_NO_FATAL_FAILURE(UploadPlanarTensorSample(*srcAccess, 0, input, width, height, channels)); + + cvcuda::Normalize op; + EXPECT_NO_THROW(op(stream, src, base, scale, dst, 1.f, 0.f, 0.f, normalScale)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + auto dstData = dst.exportData(); + ASSERT_NE(nullptr, dstData); + auto dstAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*dstData); + ASSERT_TRUE(dstAccess); + EXPECT_EQ(input, DownloadPlanarTensorSample(*dstAccess, 0, width, height, channels)); + + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +// ------------------------------------------------------------------------------------------------- +// Tensor-free (by-value list / float4 base & scale) path. +// +// The by-value overload must produce output byte-identical to the tensor-based overload fed the same +// base/scale values -- the tensor path is the oracle (no external reference needed). The explicit cases +// below cover every dtype and each independent layout, channel, flag, parameter-mode, and global-profile +// value without multiplying unrelated axes. +// ------------------------------------------------------------------------------------------------- +static float4 MakeFloat4(const std::vector &vals) +{ + float4 v{0.f, 0.f, 0.f, 0.f}; + auto *p = reinterpret_cast(&v); + for (size_t i = 0; i < vals.size() && i < 4; ++i) + { + p[i] = vals[i]; + } + return v; +} + +static bool IsScalarPlanarLayout(nvcv::TensorLayout layout) +{ + return layout == nvcv::TENSOR_NCHW || layout == nvcv::TENSOR_CHW; +} + +static nvcv::TensorShape MakeScalarTensorShape(nvcv::TensorLayout layout, int numImages, int channels, int height, + int width) +{ + if (layout == nvcv::TENSOR_NHWC) + { + return nvcv::TensorShape({numImages, height, width, channels}, layout); + } + if (layout == nvcv::TENSOR_NCHW) + { + return nvcv::TensorShape({numImages, channels, height, width}, layout); + } + return nvcv::TensorShape({channels, height, width}, layout); +} + +static std::vector MakeScalarParamValues(int count, float initialValue, float increment) +{ + std::vector values(count); + for (int i = 0; i < count; ++i) + { + values[i] = initialValue + increment * static_cast(i); + } + return values; +} + +template +static void UploadScalarTensorSample(nvcv::TensorDataAccessStridedImagePlanar &access, int sample, + const std::vector &values, int width, int height, int channels, bool isPlanar) +{ + if (isPlanar) + { + ASSERT_NO_FATAL_FAILURE(UploadPlanarTensorSample(access, sample, values, width, height, channels)); + return; + } + + const size_t rowBytes = static_cast(width) * channels * sizeof(T); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(access.sampleData(sample), access.rowStride(), values.data(), rowBytes, + rowBytes, height, cudaMemcpyHostToDevice)); +} + +template +static std::vector DownloadScalarTensorSample(nvcv::TensorDataAccessStridedImagePlanar &access, int sample, + int width, int height, int channels, bool isPlanar) +{ + if (isPlanar) + { + return DownloadPlanarTensorSample(access, sample, width, height, channels); + } + + const size_t rowBytes = static_cast(width) * channels * sizeof(T); + std::vector values(static_cast(height) * width * channels); + EXPECT_EQ(cudaSuccess, cudaMemcpy2D(values.data(), rowBytes, access.sampleData(sample), access.rowStride(), + rowBytes, height, cudaMemcpyDeviceToHost)); + return values; +} + +static void UploadScalarParamTensor(nvcv::Tensor &tensor, const std::vector &values, bool isPlanar) +{ + if (isPlanar) + { + ASSERT_NO_FATAL_FAILURE(FillPlanarParamTensor(tensor, values)); + return; + } + ASSERT_NO_FATAL_FAILURE(UploadParamTensor(tensor, values)); +} + +template +static void RunScalarBitIdentityCase(nvcv::DataType dtype, nvcv::TensorLayout layout, int channels, uint32_t flags, + bool scalarBase, bool scalarScale, float gscale, float gshift, float eps) +{ + const bool isPlanar = IsScalarPlanarLayout(layout); + const int numImages = layout == nvcv::TENSOR_CHW ? 1 : 2; + const int width = 13; + const int height = 9; + SCOPED_TRACE(testing::Message() << "layout=" << layout << " C=" << channels << " flags=" << flags + << " scalarBase=" << scalarBase << " scalarScale=" << scalarScale + << " gscale=" << gscale << " gshift=" << gshift << " eps=" << eps); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + const auto shape = MakeScalarTensorShape(layout, numImages, channels, height, width); + nvcv::Tensor src(shape, dtype); + nvcv::Tensor dstRef(shape, dtype); + nvcv::Tensor dstNew(shape, dtype); + + auto srcData = src.exportData(); + ASSERT_NE(nullptr, srcData); + auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcData); + ASSERT_TRUE(srcAccess); + + std::vector> srcVec(numImages); + for (int i = 0; i < numImages; ++i) + { + srcVec[i] = MakePlanarHostImage(i, width, height, channels); + ASSERT_NO_FATAL_FAILURE( + UploadScalarTensorSample(*srcAccess, i, srcVec[i], width, height, channels, isPlanar)); + } - scaleVec[i].resize(scaleHeight * scaleVecRowStride); - generate(scaleVec[i].begin(), scaleVec[i].end(), [&]() { return udist(rng); }); + const int baseCount = scalarBase ? 1 : channels; + const int scaleCount = scalarScale ? 1 : channels; + auto baseVals = MakeScalarParamValues(baseCount, 0.40f, 0.05f); + auto scaleVals = MakeScalarParamValues(scaleCount, 0.20f, 0.03f); + + const auto paramLayout = isPlanar ? nvcv::TENSOR_NCHW : nvcv::TENSOR_NHWC; + const auto baseShape = isPlanar ? nvcv::TensorShape({1, baseCount, 1, 1}, paramLayout) + : nvcv::TensorShape({1, 1, 1, baseCount}, paramLayout); + const auto scaleShape = isPlanar ? nvcv::TensorShape({1, scaleCount, 1, 1}, paramLayout) + : nvcv::TensorShape({1, 1, 1, scaleCount}, paramLayout); + nvcv::Tensor baseT(baseShape, nvcv::TYPE_F32); + nvcv::Tensor scaleT(scaleShape, nvcv::TYPE_F32); + ASSERT_NO_FATAL_FAILURE(UploadScalarParamTensor(baseT, baseVals, isPlanar)); + ASSERT_NO_FATAL_FAILURE(UploadScalarParamTensor(scaleT, scaleVals, isPlanar)); + + cvcuda::Normalize op; + ASSERT_NO_THROW(op(stream, src, baseT, scaleT, dstRef, gscale, gshift, eps, flags)); + ASSERT_NO_THROW(op(stream, src, MakeFloat4(baseVals), MakeFloat4(scaleVals), baseCount, scaleCount, dstNew, gscale, + gshift, eps, flags)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + auto refData = dstRef.exportData(); + auto newData = dstNew.exportData(); + ASSERT_NE(nullptr, refData); + ASSERT_NE(nullptr, newData); + auto refAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*refData); + auto newAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*newData); + ASSERT_TRUE(refAccess); + ASSERT_TRUE(newAccess); - // Copy input data to the GPU - ASSERT_EQ(cudaSuccess, cudaMemcpy2D(scaleAccess->sampleData(i), scaleAccess->rowStride(), scaleVec[i].data(), - scaleVecRowStride * sizeof(float), - scaleVecRowStride * sizeof(float), // vec has no padding - scaleHeight, cudaMemcpyHostToDevice)); + for (int i = 0; i < numImages; ++i) + { + auto refVec = DownloadScalarTensorSample(*refAccess, i, width, height, channels, isPlanar); + auto newVec = DownloadScalarTensorSample(*newAccess, i, width, height, channels, isPlanar); + EXPECT_EQ(refVec, newVec) << "sample " << i; } - // Create dest tensor - nvcv::Tensor imgDst = nvcv::util::CreateTensor(numImages, width, height, fmt); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} - // Generate test result - cvcuda::Normalize normalizeOp; - EXPECT_NO_THROW(normalizeOp(stream, imgSrc, imgBase, imgScale, imgDst, globalScale, globalShift, epsilon, flags)); +using ScalarIdentityProfile = std::tuple; +using ScalarIdentityParams = std::tuple; - // Get test data back - EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); - EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +static const ScalarIdentityProfile identityProfile{1.f, 0.f, 0.f}; +static const ScalarIdentityProfile adjustedProfile{2.f, 5.f, 1e-4f}; - // Check result - auto dstData = imgDst.exportData(); - ASSERT_NE(nullptr, dstData); +class OpNormalizeScalarIdentity : public testing::TestWithParam +{ +}; - auto dstAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*dstData); - ASSERT_TRUE(dstAccess); +TEST_P(OpNormalizeScalarIdentity, matches_tensor_path) +{ + const auto &[layout, channels, flags, scalarBase, scalarScale, profile] = GetParam(); + const auto &[globalScale, globalShift, epsilon] = profile; + + RunScalarBitIdentityCase(nvcv::TYPE_U8, layout, channels, flags, scalarBase, scalarScale, globalScale, + globalShift, epsilon); + RunScalarBitIdentityCase(nvcv::TYPE_S8, layout, channels, flags, scalarBase, scalarScale, globalScale, + globalShift, epsilon); + RunScalarBitIdentityCase(nvcv::TYPE_U16, layout, channels, flags, scalarBase, scalarScale, globalScale, + globalShift, epsilon); + RunScalarBitIdentityCase(nvcv::TYPE_S16, layout, channels, flags, scalarBase, scalarScale, globalScale, + globalShift, epsilon); + RunScalarBitIdentityCase(nvcv::TYPE_S32, layout, channels, flags, scalarBase, scalarScale, globalScale, + globalShift, epsilon); + RunScalarBitIdentityCase(nvcv::TYPE_F32, layout, channels, flags, scalarBase, scalarScale, globalScale, + globalShift, epsilon); +} - int dstVecRowStride = width * fmt.numChannels(); +INSTANTIATE_TEST_SUITE_P( + AxisCovering, OpNormalizeScalarIdentity, + testing::Values(ScalarIdentityParams{nvcv::TENSOR_NHWC, 1, normalScale, false, false, identityProfile}, + ScalarIdentityParams{nvcv::TENSOR_NCHW, 3, scaleIsStdDev, true, true, adjustedProfile}, + ScalarIdentityParams{nvcv::TENSOR_CHW, 4, normalScale, true, false, adjustedProfile})); + +// Equivalent-layout parity (COV-PARITY / TST-7): the by-value path on native planar (NCHW) input must +// match the by-value path on interleaved (NHWC) input for the same logical pixels and per-channel +// base/scale -- i.e. no reformat is needed to get the planar result. +TEST(OpNormalizeScalar, planar_matches_interleaved) +{ + const int numImages = 2; + const int width = 13; + const int height = 9; + const int channels = 3; + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + nvcv::Tensor srcHWC(nvcv::TensorShape({numImages, height, width, channels}, nvcv::TENSOR_NHWC), nvcv::TYPE_F32); + nvcv::Tensor dstHWC(nvcv::TensorShape({numImages, height, width, channels}, nvcv::TENSOR_NHWC), nvcv::TYPE_F32); + nvcv::Tensor srcCHW(nvcv::TensorShape({numImages, channels, height, width}, nvcv::TENSOR_NCHW), nvcv::TYPE_F32); + nvcv::Tensor dstCHW(nvcv::TensorShape({numImages, channels, height, width}, nvcv::TENSOR_NCHW), nvcv::TYPE_F32); + + auto hwcData = srcHWC.exportData(); + auto chwData = srcCHW.exportData(); + ASSERT_NE(nullptr, hwcData); + ASSERT_NE(nullptr, chwData); + auto hwcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*hwcData); + auto chwAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*chwData); + ASSERT_TRUE(hwcAccess); + ASSERT_TRUE(chwAccess); + + // Same logical HWC-ordered images uploaded interleaved (NHWC) and deinterleaved (NCHW). + std::vector> srcVec(numImages); for (int i = 0; i < numImages; ++i) { - SCOPED_TRACE(i); + srcVec[i] = MakePlanarHostImage(i, width, height, channels); + ASSERT_NO_FATAL_FAILURE( + UploadScalarTensorSample(*hwcAccess, i, srcVec[i], width, height, channels, false)); + } + for (int i = 0; i < numImages; ++i) + { + ASSERT_NO_FATAL_FAILURE( + UploadScalarTensorSample(*chwAccess, i, srcVec[i], width, height, channels, true)); + } - std::vector testVec(height * dstVecRowStride); + const std::vector baseVals{0.40f, 0.45f, 0.50f}; + const std::vector scaleVals{0.20f, 0.23f, 0.26f}; + + cvcuda::Normalize op; + ASSERT_NO_THROW( + op(stream, srcHWC, MakeFloat4(baseVals), MakeFloat4(scaleVals), channels, channels, dstHWC, 2.f, 5.f, 0.f, 0)); + ASSERT_NO_THROW( + op(stream, srcCHW, MakeFloat4(baseVals), MakeFloat4(scaleVals), channels, channels, dstCHW, 2.f, 5.f, 0.f, 0)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + auto dHWCData = dstHWC.exportData(); + auto dCHWData = dstCHW.exportData(); + ASSERT_NE(nullptr, dHWCData); + ASSERT_NE(nullptr, dCHWData); + auto dHWCAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*dHWCData); + auto dCHWAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*dCHWData); + ASSERT_TRUE(dHWCAccess); + ASSERT_TRUE(dCHWAccess); - // Copy output data to Host - ASSERT_EQ(cudaSuccess, - cudaMemcpy2D(testVec.data(), dstVecRowStride, dstAccess->sampleData(i), dstAccess->rowStride(), - dstVecRowStride, // vec has no padding - height, cudaMemcpyDeviceToHost)); + for (int i = 0; i < numImages; ++i) + { + auto outHWC = DownloadScalarTensorSample(*dHWCAccess, i, width, height, channels, false); + auto outCHW = DownloadScalarTensorSample(*dCHWAccess, i, width, height, channels, true); + EXPECT_EQ(outHWC, outCHW) << "sample " << i; + } - std::vector goldVec(height * dstVecRowStride); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} - int bi = baseNumImages == 1 ? 0 : i; - int si = scaleNumImages == 1 ? 0 : i; +TEST(OpNormalizeScalar_Negative, rejects_invalid_param_count) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); - // Generate gold result - Normalize(goldVec, dstVecRowStride, srcVec[i], srcVecRowStride, {width, height}, fmt, baseVec[bi], - baseVecRowStride, {baseWidth, baseHeight}, baseFormat, scaleVec[si], scaleVecRowStride, - {scaleWidth, scaleHeight}, scaleFormat, globalScale, globalShift, epsilon, flags); + const nvcv::TensorShape shape({2, 9, 13, 3}, nvcv::TENSOR_NHWC); // 3-channel input + auto src = nvcv::Tensor(shape, nvcv::TYPE_F32); + auto dst = nvcv::Tensor(shape, nvcv::TYPE_F32); + cvcuda::Normalize op; + float4 v{0.5f, 0.5f, 0.5f, 0.f}; - EXPECT_EQ(goldVec, testVec); - } + // A count that is neither 1 (broadcast) nor the channel count (3) is rejected. + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&]() { op(stream, src, v, v, 2, 3, dst, 1.f, 0.f, 0.f, 0); })); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&]() { op(stream, src, v, v, 3, 4, dst, 1.f, 0.f, 0.f, 0); })); + + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); } -TEST_P(OpNormalize, varshape_correct_output) +TEST(OpNormalizeScalar_Negative, rejects_batch_exceeding_grid_z) { cudaStream_t stream; - EXPECT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); - int width = GetParamValue<0>(); - int height = GetParamValue<1>(); - int numImages = GetParamValue<2>(); - bool scalarBase = GetParamValue<3>(); - bool scalarScale = GetParamValue<4>(); - uint32_t flags = GetParamValue<5>(); - float globalScale = GetParamValue<6>(); - float globalShift = GetParamValue<7>(); - float epsilon = GetParamValue<8>(); + const nvcv::TensorShape shape({65536, 1, 1, 1}, nvcv::TENSOR_NHWC); + nvcv::Tensor src(shape, nvcv::TYPE_U8); + nvcv::Tensor dst(shape, nvcv::TYPE_U8); + cvcuda::Normalize op; + float4 base{0.f, 0.f, 0.f, 0.f}; + float4 scale{1.f, 0.f, 0.f, 0.f}; - nvcv::ImageFormat baseFormat = (scalarBase ? nvcv::FMT_F32 : nvcv::FMT_RGBAf32); - nvcv::ImageFormat scaleFormat = (scalarScale ? nvcv::FMT_F32 : nvcv::FMT_RGBAf32); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&]() { op(stream, src, base, scale, 1, 1, dst, 1.f, 0.f, 0.f, normalScale); })); - nvcv::ImageFormat fmt = nvcv::FMT_RGBA8; + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} - std::default_random_engine rng; +TEST(OpNormalizeScalar_Negative, rejects_unsupported_channels) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); - // Create input varshape + // 2-channel input is not a supported channel count. + const nvcv::TensorShape shape({2, 9, 13, 2}, nvcv::TENSOR_NHWC); + auto src = nvcv::Tensor(shape, nvcv::TYPE_F32); + auto dst = nvcv::Tensor(shape, nvcv::TYPE_F32); + cvcuda::Normalize op; + float4 v{0.5f, 0.5f, 0.f, 0.f}; - std::uniform_int_distribution udistWidth(width * 0.8, width * 1.1); - std::uniform_int_distribution udistHeight(height * 0.8, height * 1.1); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&]() { op(stream, src, v, v, 2, 2, dst, 1.f, 0.f, 0.f, 0); })); - std::vector imgSrc; + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} - std::vector> srcVec(numImages); - std::vector srcVecRowStride(numImages); +TEST(OpNormalizeScalar_Negative, rejects_mismatched_output_dtype) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); - for (int i = 0; i < numImages; ++i) - { - imgSrc.emplace_back(nvcv::Size2D{udistWidth(rng), udistHeight(rng)}, fmt); + // The header contract requires output dtype == input dtype; the kernels dispatch on the input + // dtype and would write the wrong element type through the output wrap. + const nvcv::TensorShape shape({2, 9, 13, 3}, nvcv::TENSOR_NHWC); + auto src = nvcv::Tensor(shape, nvcv::TYPE_F32); + auto dst = nvcv::Tensor(shape, nvcv::TYPE_S32); + cvcuda::Normalize op; + float4 v{0.5f, 0.5f, 0.5f, 0.f}; - int srcRowStride = imgSrc[i].size().w * fmt.numChannels(); - srcVecRowStride[i] = srcRowStride; + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&]() { op(stream, src, v, v, 3, 3, dst, 1.f, 0.f, 0.f, 0); })); - std::uniform_int_distribution udist(0, 255); + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} - srcVec[i].resize(imgSrc[i].size().h * srcRowStride); - generate(srcVec[i].begin(), srcVec[i].end(), [&]() { return udist(rng); }); +TEST(OpNormalizeScalar_Negative, rejects_mismatched_output_shape) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + // The header contract requires output extents == input extents; the kernels bounds-check + // against the input extents only, so a smaller output would be written out of bounds. + cvcuda::Normalize op; + float4 v{0.5f, 0.5f, 0.5f, 0.f}; + + nvcv::Tensor src(nvcv::TensorShape({2, 9, 13, 3}, nvcv::TENSOR_NHWC), nvcv::TYPE_F32); + nvcv::Tensor dstSmallH(nvcv::TensorShape({2, 4, 13, 3}, nvcv::TENSOR_NHWC), nvcv::TYPE_F32); + nvcv::Tensor dstSmallN(nvcv::TensorShape({1, 9, 13, 3}, nvcv::TENSOR_NHWC), nvcv::TYPE_F32); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&]() { op(stream, src, v, v, 3, 3, dstSmallH, 1.f, 0.f, 0.f, 0); })); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&]() { op(stream, src, v, v, 3, 3, dstSmallN, 1.f, 0.f, 0.f, 0); })); + + // Planar path validates the same contract. + nvcv::Tensor srcP(nvcv::TensorShape({2, 3, 9, 13}, nvcv::TENSOR_NCHW), nvcv::TYPE_F32); + nvcv::Tensor dstPSmallW(nvcv::TensorShape({2, 3, 9, 7}, nvcv::TENSOR_NCHW), nvcv::TYPE_F32); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&]() { op(stream, srcP, v, v, 3, 3, dstPSmallW, 1.f, 0.f, 0.f, 0); })); - auto imgData = imgSrc[i].exportData(); - ASSERT_NE(imgData, nvcv::NullOpt); + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} - // Copy input data to the GPU - ASSERT_EQ(cudaSuccess, - cudaMemcpy2D(imgData->plane(0).basePtr, imgData->plane(0).rowStride, srcVec[i].data(), srcRowStride, - srcRowStride, // vec has no padding - imgSrc[i].size().h, cudaMemcpyHostToDevice)); - } +TEST(OpNormalize_Negative, tensor_planar_invalid_param_shape) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); - nvcv::ImageBatchVarShape batchSrc(numImages); - batchSrc.pushBack(imgSrc.begin(), imgSrc.end()); + constexpr int width = 13; + constexpr int height = 11; + constexpr int numImages = 2; + constexpr int channels = 3; - // Create base tensor + nvcv::Tensor imgSrc(numImages, {width, height}, nvcv::FMT_RGB8p); + nvcv::Tensor imgDst(numImages, {width, height}, nvcv::FMT_RGB8p); nvcv::Tensor imgBase( { - {1, 1, 1, baseFormat.numChannels()}, - nvcv::TENSOR_NHWC + {numImages + 1, channels, 1, 1}, + "NCHW" }, - baseFormat.planeDataType(0)); - std::vector baseVec(baseFormat.numChannels()); - { - auto baseData = imgBase.exportData(); - ASSERT_NE(nullptr, baseData); - auto baseAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*baseData); - ASSERT_TRUE(baseAccess); - - std::uniform_real_distribution udist(0, 255.f); - generate(baseVec.begin(), baseVec.end(), [&]() { return udist(rng); }); - - ASSERT_EQ(cudaSuccess, cudaMemcpy2D(baseAccess->sampleData(0), baseAccess->rowStride(), baseVec.data(), - baseVec.size() * sizeof(float), - baseVec.size() * sizeof(float), // vec has no padding - 1, cudaMemcpyHostToDevice)); - } - - // Create scale tensor + nvcv::TYPE_F32); nvcv::Tensor imgScale( { - {1, 1, 1, scaleFormat.numChannels()}, - nvcv::TENSOR_NHWC + {1, channels, 1, 1}, + "NCHW" }, - scaleFormat.planeDataType(0)); - std::vector scaleVec(scaleFormat.numChannels()); - { - auto scaleData = imgScale.exportData(); - ASSERT_NE(nullptr, scaleData); - auto scaleAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*scaleData); - ASSERT_TRUE(scaleAccess); + nvcv::TYPE_F32); - std::uniform_real_distribution udist(0, 1.f); - generate(scaleVec.begin(), scaleVec.end(), [&]() { return udist(rng); }); + cvcuda::Normalize normalizeOp; + EXPECT_EQ( + NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&] { normalizeOp(stream, imgSrc, imgBase, imgScale, imgDst, 1.f, 0.f, 0.f, normalScale); })); - ASSERT_EQ(cudaSuccess, cudaMemcpy2D(scaleAccess->sampleData(0), scaleAccess->rowStride(), scaleVec.data(), - scaleVec.size() * sizeof(float), - scaleVec.size() * sizeof(float), // vec has no padding - 1, cudaMemcpyHostToDevice)); - } + EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} - // Create output varshape +TEST(OpNormalize_Negative, varshape_planar_invalid_param_shape) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + constexpr int width = 13; + constexpr int height = 11; + constexpr int numImages = 2; + constexpr int channels = 3; + + std::vector imgSrc; std::vector imgDst; for (int i = 0; i < numImages; ++i) { - imgDst.emplace_back(imgSrc[i].size(), imgSrc[i].format()); + imgSrc.emplace_back(nvcv::Size2D{width + i, height + i}, nvcv::FMT_RGB8p); + imgDst.emplace_back(imgSrc.back().size(), nvcv::FMT_RGB8p); } + + nvcv::ImageBatchVarShape batchSrc(numImages); nvcv::ImageBatchVarShape batchDst(numImages); + batchSrc.pushBack(imgSrc.begin(), imgSrc.end()); batchDst.pushBack(imgDst.begin(), imgDst.end()); - // Generate test result cvcuda::Normalize normalizeOp; - EXPECT_NO_THROW( - normalizeOp(stream, batchSrc, imgBase, imgScale, batchDst, globalScale, globalShift, epsilon, flags)); - - // Get test data back - EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); - EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); - // Check test data against gold - for (int i = 0; i < numImages; ++i) + auto expectInvalidArgument = [&](nvcv::Tensor &imgBase, nvcv::Tensor &imgScale) { - SCOPED_TRACE(i); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall( + [&] { normalizeOp(stream, batchSrc, imgBase, imgScale, batchDst, 1.f, 0.f, 0.f, normalScale); })); + }; - const auto srcData = imgSrc[i].exportData(); - assert(srcData->numPlanes() == 1); - int width = srcData->plane(0).width; - int height = srcData->plane(0).height; + { + nvcv::Tensor imgBase( + { + {numImages, channels, 1, 1}, + "NCHW" + }, + nvcv::TYPE_F32); + nvcv::Tensor imgScale( + { + {1, channels, 1, 1}, + "NCHW" + }, + nvcv::TYPE_F32); - const auto dstData = imgDst[i].exportData(); - assert(dstData->numPlanes() == 1); + expectInvalidArgument(imgBase, imgScale); + } - int dstRowStride = srcVecRowStride[i]; + { + nvcv::Tensor imgBase( + { + {1, channels, 1, 1}, + "NCHW" + }, + nvcv::TYPE_F32); + nvcv::Tensor imgScale( + { + {numImages, channels, 1, 1}, + "NCHW" + }, + nvcv::TYPE_F32); - std::vector testVec(height * dstRowStride); + expectInvalidArgument(imgBase, imgScale); + } - // Copy output data to Host - ASSERT_EQ(cudaSuccess, - cudaMemcpy2D(testVec.data(), dstRowStride, dstData->plane(0).basePtr, dstData->plane(0).rowStride, - dstRowStride, // vec has no padding - height, cudaMemcpyDeviceToHost)); + { + nvcv::Tensor imgBase( + { + {1, 1, 1, channels}, + "NHWC" + }, + nvcv::TYPE_F32); + nvcv::Tensor imgScale( + { + {1, channels, 1, 1}, + "NCHW" + }, + nvcv::TYPE_F32); - std::vector goldVec(height * dstRowStride); + expectInvalidArgument(imgBase, imgScale); + } - // Generate gold result - Normalize(goldVec, dstRowStride, srcVec[i], srcVecRowStride[i], {width, height}, fmt, baseVec, 0, {1, 1}, - baseFormat, scaleVec, 0, {1, 1}, scaleFormat, globalScale, globalShift, epsilon, flags); + { + nvcv::Tensor imgBase( + { + {1, channels, 1, 1}, + "NCHW" + }, + nvcv::TYPE_F32); + nvcv::Tensor imgScale( + { + {1, 1, 1, channels}, + "NHWC" + }, + nvcv::TYPE_F32); - EXPECT_THAT(testVec, t::ElementsAreArray(goldVec)); + expectInvalidArgument(imgBase, imgScale); } + + EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); } // clang-format off NVCV_TEST_SUITE_P(OpNormalize_Negative, test::ValueList{ // inFmt, outFmt, isVarShapeDifferentFormatTest - {nvcv::FMT_RGB8p, nvcv::FMT_RGB8, false}, {nvcv::FMT_RGB8, nvcv::FMT_RGB8p, false}, - {nvcv::FMT_RGB8p, nvcv::FMT_RGB8p, false}, {nvcv::FMT_RGB8, nvcv::FMT_RGB8, true}, + {nvcv::FMT_2F32, nvcv::FMT_2F32, false}, {nvcv::FMT_RGBf16, nvcv::FMT_RGBf16, false}, {nvcv::FMT_U16, nvcv::FMT_U16, false}, }); @@ -418,11 +2561,9 @@ TEST_P(OpNormalize_Negative, op) cudaStream_t stream; EXPECT_EQ(cudaSuccess, cudaStreamCreate(&stream)); - nvcv::ImageFormat inFmt = GetParamValue<0>(); - nvcv::ImageFormat outFmt = GetParamValue<1>(); - bool isVarShapeDifferentFormatTest = GetParamValue<2>(); - - if (isVarShapeDifferentFormatTest) + nvcv::ImageFormat inFmt = GetParamValue<0>(); + nvcv::ImageFormat outFmt = GetParamValue<1>(); + if (bool isVarShapeDifferentFormatTest = GetParamValue<2>(); isVarShapeDifferentFormatTest) { GTEST_SKIP() << "Skip varshape different format test for tensor test"; } @@ -461,7 +2602,8 @@ TEST_P(OpNormalize_Negative, op) EXPECT_EQ( NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall( - [&] { normalizeOp(stream, imgSrc, imgBase, imgScale, imgDst, globalScale, globalShift, epsilon, flags); })); + [&normalizeOp, &stream, &imgSrc, &imgBase, &imgScale, &imgDst, &globalScale, &globalShift, &epsilon, &flags] + { normalizeOp(stream, imgSrc, imgBase, imgScale, imgDst, globalScale, globalShift, epsilon, flags); })); // Get test data back EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); @@ -470,13 +2612,18 @@ TEST_P(OpNormalize_Negative, op) TEST_P(OpNormalize_Negative, varshape_op) { - cudaStream_t stream; - EXPECT_EQ(cudaSuccess, cudaStreamCreate(&stream)); - nvcv::ImageFormat inFmt = GetParamValue<0>(); nvcv::ImageFormat outFmt = GetParamValue<1>(); bool isVarShapeDifferentFormatTest = GetParamValue<2>(); + if (inFmt == nvcv::FMT_2F32 && outFmt == nvcv::FMT_2F32) + { + GTEST_SKIP() << "2-channel interleaved varshape normalize is supported"; + } + + cudaStream_t stream; + EXPECT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + std::vector> testSet{ {isVarShapeDifferentFormatTest ? nvcv::FMT_U16 : inFmt, outFmt}, { inFmt, isVarShapeDifferentFormatTest ? nvcv::FMT_U16 : outFmt} @@ -497,11 +2644,8 @@ TEST_P(OpNormalize_Negative, varshape_op) ASSERT_EQ(testSet[0].second, outFmt); } - for (auto testCase : testSet) + for (const auto &[inputFmtExtra, outputFmtExtra] : testSet) { - nvcv::ImageFormat inputFmtExtra = std::get<0>(testCase); - nvcv::ImageFormat outputFmtExtra = std::get<1>(testCase); - int width = 24; int height = 24; int numImages = 5; @@ -518,8 +2662,8 @@ TEST_P(OpNormalize_Negative, varshape_op) // Create input and output varshape - std::uniform_int_distribution udistWidth(width * 0.8, width * 1.1); - std::uniform_int_distribution udistHeight(height * 0.8, height * 1.1); + std::uniform_int_distribution udistWidth(ScaledSize(width, 0.8), ScaledSize(width, 1.1)); + std::uniform_int_distribution udistHeight(ScaledSize(height, 0.8), ScaledSize(height, 1.1)); std::vector imgSrc; std::vector imgDst; @@ -556,7 +2700,8 @@ TEST_P(OpNormalize_Negative, varshape_op) // Generate test result cvcuda::Normalize normalizeOp; EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall( - [&] { + [&normalizeOp, &stream, &batchSrc, &imgBase, &imgScale, &batchDst, + &globalScale, &globalShift, &epsilon, &flags] { normalizeOp(stream, batchSrc, imgBase, imgScale, batchDst, globalScale, globalShift, epsilon, flags); })); @@ -571,3 +2716,85 @@ TEST(OpNormalize_Negative, create_null_handle) { EXPECT_EQ(cvcudaNormalizeCreate(nullptr), NVCV_ERROR_INVALID_ARGUMENT); } + +// ============================================================================= +// Planar (NCHW/CHW) parity: Normalize treats channels independently, so the planar layout must +// produce exactly the same pixels as the interleaved path. These tests feed identical data in both +// layouts through cvcuda::Normalize and require the (re-interleaved) planar output to match the +// interleaved output bit-for-bit. Scalar (single-channel F32) base/scale are layout-agnostic, so any +// divergence is a real planar-vs-interleaved bug, not a parameter-layout artifact. +// ============================================================================= + +namespace { + +// 1x1x1 single-channel F32 scalar param (base or scale), broadcast to every channel by Normalize. +nvcv::Tensor MakeScalarNormalizeParam(float value) +{ + nvcv::Tensor t = nvcv::util::CreateTensor(1, 1, 1, nvcv::FMT_F32); + auto data = t.exportData(); + EXPECT_NE(nullptr, data); + auto access = nvcv::TensorDataAccessStridedImagePlanar::Create(*data); + EXPECT_TRUE(access); + EXPECT_EQ(cudaSuccess, cudaMemcpy(access->sampleData(0), &value, sizeof(float), cudaMemcpyHostToDevice)); + return t; +} + +// Normalize identical data in interleaved and planar tensor layout; outputs must match bit-for-bit. +// The shared scaffolding (upload/run/download/compare) lives in PlanarParityUtils.hpp; here we only +// bind the Normalize call with scalar base/scale (the same params for both layouts). +void RunNormalizePlanarParityTensorCase(nvcv::ImageFormat planarFmt, nvcv::ImageFormat interleavedFmt, int w, int h, + int numImages) +{ + nvcv::Tensor base = MakeScalarNormalizeParam(10.f); + nvcv::Tensor scale = MakeScalarNormalizeParam(2.f); + test::planar::RunTensorParity( + planarFmt, interleavedFmt, w, h, w, h, numImages, + [&base, &scale](cudaStream_t s, const nvcv::Tensor &src, const nvcv::Tensor &dst, nvcv::ImageFormat) + { + cvcuda::Normalize op; + EXPECT_NO_THROW(op(s, src, base, scale, dst, 1.5f, 3.f, 0.f, 0)); + }); +} + +// Var-shape counterpart of RunNormalizePlanarParityTensorCase. +void RunNormalizePlanarParityVarShapeCase(nvcv::ImageFormat planarFmt, nvcv::ImageFormat interleavedFmt, int w, int h, + int numImages) +{ + nvcv::Tensor base = MakeScalarNormalizeParam(10.f); + nvcv::Tensor scale = MakeScalarNormalizeParam(2.f); + test::planar::RunVarShapeParity(planarFmt, interleavedFmt, w, h, w, h, numImages, + [&base, &scale](cudaStream_t s, const nvcv::ImageBatchVarShape &src, + const nvcv::ImageBatchVarShape &dst, nvcv::ImageFormat) + { + cvcuda::Normalize op; + EXPECT_NO_THROW(op(s, src, base, scale, dst, 1.5f, 3.f, 0.f, 0)); + }); +} + +} // namespace + +// Parameters: width, height, numImages, planarFmt, interleavedFmt +NVCV_TEST_SUITE_P(OpNormalizePlanar, + test::ValueList{ + // RGB8 (3 channel uint8). + {64, 48, 2, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + {33, 17, 1, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + // RGBA8 (4 channel uint8). + {64, 48, 2, nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8}, + // Float planar (3 and 4 channel) -- exercises the float kernel path bit-exactly. + {64, 48, 2, nvcv::FMT_RGBf32p, nvcv::FMT_RGBf32}, + {50, 40, 1, nvcv::FMT_RGBf32p, nvcv::FMT_RGBf32}, + {64, 48, 2, nvcv::FMT_RGBAf32p, nvcv::FMT_RGBAf32}, +}); + +TEST_P(OpNormalizePlanar, tensor_matches_interleaved) +{ + RunNormalizePlanarParityTensorCase(GetParamValue<3>(), GetParamValue<4>(), GetParamValue<0>(), GetParamValue<1>(), + GetParamValue<2>()); +} + +TEST_P(OpNormalizePlanar, varshape_matches_interleaved) +{ + RunNormalizePlanarParityVarShapeCase(GetParamValue<3>(), GetParamValue<4>(), GetParamValue<0>(), GetParamValue<1>(), + GetParamValue<2>()); +} diff --git a/tests/cvcuda/system/TestOpOSD.cpp b/tests/cvcuda/system/TestOpOSD.cpp index 171ef0a28..92cb38221 100644 --- a/tests/cvcuda/system/TestOpOSD.cpp +++ b/tests/cvcuda/system/TestOpOSD.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,8 +16,7 @@ */ #include "Definitions.hpp" - -#include "OsdUtils.cuh" +#include "PlanarParityUtils.hpp" #include #include @@ -27,188 +26,57 @@ #include #include -#include +#include +#include +#include #include -#include #include namespace gt = ::testing; namespace test = nvcv::test; using namespace cvcuda::priv; -static int randl(int l, int h) +static std::mt19937 &Rng() { - int value = rand() % (h - l + 1); - return l + value; + static std::mt19937 rng; + return rng; } -#pragma GCC push_options -#pragma GCC optimize("O1") +static int randl(int l, int h) +{ + return std::uniform_int_distribution(l, h)(Rng()); +} -static void setGoldBuffer(std::vector &vect, nvcv::ImageFormat format, - const nvcv::TensorDataAccessStridedImagePlanar &data, nvcv::Byte *inBuf, - std::shared_ptr ctx, cudaStream_t stream) +static std::time_t CurrentTime() { - auto context = cuosd_context_create(); + return std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); +} - for (int n = 0; n < ctx->batch(); n++) +static void FillRandomSegment(std::vector &hSeg) +{ + for (auto &v : hSeg) { - test::osd::Image *image = test::osd::create_image( - data.numCols(), data.numRows(), - format == nvcv::FMT_RGBA8 ? test::osd::ImageFormat::RGBA : test::osd::ImageFormat::RGB); - int bufSize = data.numCols() * data.numRows() * data.numChannels(); - EXPECT_EQ(cudaSuccess, cudaMemcpy(image->data0, inBuf + n * bufSize, bufSize, cudaMemcpyDeviceToDevice)); - - auto numElements = ctx->numElementsAt(n); - - for (int i = 0; i < numElements; i++) - { - auto element = ctx->elementAt(n, i); - switch (element->type()) - { - case NVCVOSDType::NVCV_OSD_RECT: - { - auto bbox = *((NVCVBndBoxI *)element->ptr()); - - int left = std::max(std::min(bbox.box.x, data.numCols() - 1), 0); - int top = std::max(std::min(bbox.box.y, data.numRows() - 1), 0); - int right = std::max(std::min(left + bbox.box.width - 1, data.numCols() - 1), 0); - int bottom = std::max(std::min(top + bbox.box.height - 1, data.numRows() - 1), 0); - - if (left == right || top == bottom || bbox.box.width <= 0 || bbox.box.height <= 0) - { - continue; - } - - cuOSDColor borderColor - = {bbox.borderColor.r, bbox.borderColor.g, bbox.borderColor.b, bbox.borderColor.a}; - cuOSDColor fillColor = {bbox.fillColor.r, bbox.fillColor.g, bbox.fillColor.b, bbox.fillColor.a}; - cuosd_draw_rectangle(context, left, top, right, bottom, bbox.thickness, borderColor, fillColor); - break; - } - case NVCVOSDType::NVCV_OSD_TEXT: - { - auto text = *((NVCVText *)element->ptr()); - cuOSDColor fontColor = *(cuOSDColor *)(&text.fontColor); - cuOSDColor bgColor = *(cuOSDColor *)(&text.bgColor); - cuosd_draw_text(context, text.utf8Text, text.fontSize, text.fontName, text.tlPos.x, text.tlPos.y, - fontColor, bgColor); - break; - } - case NVCVOSDType::NVCV_OSD_SEGMENT: - { - auto segment = (NVCVSegment *)element->ptr(); - - int left = segment->box.x; - int top = segment->box.y; - int right = left + segment->box.width - 1; - int bottom = top + segment->box.height - 1; - - if (left == right || top == bottom || segment->box.width <= 0 || segment->box.height <= 0) - { - continue; - } - cuOSDColor borderColor = *(cuOSDColor *)(&segment->borderColor); - cuOSDColor segColor = *(cuOSDColor *)(&segment->segColor); - cuosd_draw_segmentmask(context, left, top, right, bottom, segment->thickness, segment->dSeg, - segment->segWidth, segment->segHeight, segment->segThreshold, borderColor, - segColor); - break; - } - case NVCVOSDType::NVCV_OSD_POINT: - { - auto point = *((NVCVPoint *)element->ptr()); - cuOSDColor color = *(cuOSDColor *)(&point.color); - cuosd_draw_point(context, point.centerPos.x, point.centerPos.y, point.radius, color); - break; - } - case NVCVOSDType::NVCV_OSD_LINE: - { - auto line = *((NVCVLine *)element->ptr()); - cuOSDColor color = *(cuOSDColor *)(&line.color); - cuosd_draw_line(context, line.pos0.x, line.pos0.y, line.pos1.x, line.pos1.y, line.thickness, color, - line.interpolation); - break; - } - case NVCVOSDType::NVCV_OSD_POLYLINE: - { - auto pl = (NVCVPolyLine *)element->ptr(); - cuOSDColor borderColor = *(cuOSDColor *)(&pl->borderColor); - cuOSDColor fill_color = *(cuOSDColor *)(&pl->fillColor); - cuosd_draw_polyline(context, pl->hPoints, pl->dPoints, pl->numPoints, pl->thickness, pl->isClosed, - borderColor, pl->interpolation, fill_color); - break; - } - case NVCVOSDType::NVCV_OSD_ROTATED_RECT: - { - auto rb = *((NVCVRotatedBox *)element->ptr()); - cuOSDColor borderColor = *(cuOSDColor *)(&rb.borderColor); - cuOSDColor bgColor = *(cuOSDColor *)(&rb.bgColor); - cuosd_draw_rotationbox(context, rb.centerPos.x, rb.centerPos.y, rb.width, rb.height, rb.yaw, - rb.thickness, borderColor, rb.interpolation, bgColor); - break; - } - case NVCVOSDType::NVCV_OSD_CIRCLE: - { - auto circle = *((NVCVCircle *)element->ptr()); - cuOSDColor borderColor = *(cuOSDColor *)(&circle.borderColor); - cuOSDColor bgColor = *(cuOSDColor *)(&circle.bgColor); - cuosd_draw_circle(context, circle.centerPos.x, circle.centerPos.y, circle.radius, circle.thickness, - borderColor, bgColor); - break; - } - case NVCVOSDType::NVCV_OSD_ARROW: - { - auto arrow = *((NVCVArrow *)element->ptr()); - cuOSDColor color = *(cuOSDColor *)(&arrow.color); - cuosd_draw_arrow(context, arrow.pos0.x, arrow.pos0.y, arrow.pos1.x, arrow.pos1.y, arrow.arrowSize, - arrow.thickness, color, arrow.interpolation); - break; - } - case NVCVOSDType::NVCV_OSD_CLOCK: - { - auto clock = *((NVCVClock *)element->ptr()); - cuOSDClockFormat clockFormat = (cuOSDClockFormat)(int)(clock.clockFormat); - cuOSDColor fontColor = *(cuOSDColor *)(&clock.fontColor); - cuOSDColor bgColor = *(cuOSDColor *)(&clock.bgColor); - cuosd_draw_clock(context, clockFormat, clock.time, clock.fontSize, clock.font, clock.tlPos.x, - clock.tlPos.y, fontColor, bgColor); - break; - } - default: - break; - } - } - - test::osd::cuosd_apply(context, image, stream); - EXPECT_EQ(cudaSuccess, cudaMemcpy(vect.data() + n * bufSize, image->data0, bufSize, cudaMemcpyDeviceToHost)); - test::osd::free_image(image); + v = static_cast(randl(0, 100)) / 100.0f; } - - cudaStreamSynchronize(stream); - cuosd_context_destroy(context); } +#pragma GCC push_options +#pragma GCC optimize("O1") + #pragma GCC pop_options -// run operator -static void runOp(cudaStream_t &stream, cvcuda::OSD &op, int &inN, int &inW, int &inH, int &num, int &sed, - nvcv::ImageFormat &format) +static void runOp(cudaStream_t &stream, const cvcuda::OSD &op, int inN, int inW, int inH, int num, int sed, + const nvcv::ImageFormat &format) { std::vector>> elementVec; - test::osd::Segment *test_segment = test::osd::create_segment(); - test::osd::Polyline *test_polyline = test::osd::create_polyline(); - - srand(sed); - // Note: borderColor.a must be >= 1 to avoid corner case where 3rd party cuOSD library - // (used for gold generation) has a bug with borderColor.a=0 preventing drawing operations + Rng().seed(sed); for (int n = 0; n < inN; n++) { std::vector> curVec; for (int i = 0; i < num; i++) { - NVCVOSDType type = (NVCVOSDType)randl(int(NVCV_OSD_NONE) + 1, int(NVCV_OSD_MAX) - 1); + auto type = static_cast(randl(int(NVCV_OSD_NONE) + 1, int(NVCV_OSD_MAX) - 1)); std::shared_ptr element; switch (type) { @@ -229,25 +97,28 @@ static void runOp(cudaStream_t &stream, cvcuda::OSD &op, int &inN, int &inW, int } case NVCVOSDType::NVCV_OSD_TEXT: { - NVCVText text = NVCVText("abcdefghijklmnopqrstuvwxyz", 5 * randl(1, 10), DEFAULT_OSD_FONT, - NVCVPointI({randl(0, inW - 1), randl(0, inH - 1)}), - NVCVColorRGBA({(unsigned char)randl(0, 255), (unsigned char)randl(0, 255), - (unsigned char)randl(0, 255), (unsigned char)randl(0, 255)}), - NVCVColorRGBA({(unsigned char)randl(0, 255), (unsigned char)randl(0, 255), - (unsigned char)randl(0, 255), (unsigned char)randl(0, 255)})); - element = std::make_shared(type, &text); + auto text = NVCVText("abcdefghijklmnopqrstuvwxyz", 5 * randl(1, 10), DEFAULT_OSD_FONT, + NVCVPointI({randl(0, inW - 1), randl(0, inH - 1)}), + NVCVColorRGBA({(unsigned char)randl(0, 255), (unsigned char)randl(0, 255), + (unsigned char)randl(0, 255), (unsigned char)randl(0, 255)}), + NVCVColorRGBA({(unsigned char)randl(0, 255), (unsigned char)randl(0, 255), + (unsigned char)randl(0, 255), (unsigned char)randl(0, 255)})); + element = std::make_shared(type, &text); break; } case NVCVOSDType::NVCV_OSD_SEGMENT: { - NVCVSegment segment = NVCVSegment( - NVCVBoxI({randl(0, inW - 1), randl(0, inH - 1), randl(1, inW), randl(1, inH)}), randl(-1, 5), - test_segment->data, test_segment->width, test_segment->height, 0.1 * randl(1, 5), - NVCVColorRGBA({(unsigned char)randl(0, 255), (unsigned char)randl(0, 255), - (unsigned char)randl(0, 255), (unsigned char)randl(0, 255)}), - NVCVColorRGBA({(unsigned char)randl(0, 255), (unsigned char)randl(0, 255), - (unsigned char)randl(0, 255), (unsigned char)randl(0, 255)})); - element = std::make_shared(type, &segment); + int32_t segW = randl(1, 8); + int32_t segH = randl(1, 8); + std::vector hSeg(segW * segH); + FillRandomSegment(hSeg); + NVCVSegment seg(NVCVBoxI{randl(0, inW - 1), randl(0, inH - 1), randl(1, inW), randl(1, inH)}, + randl(-1, 5), hSeg.data(), segW, segH, 0.5f, + NVCVColorRGBA({(unsigned char)randl(0, 255), (unsigned char)randl(0, 255), + (unsigned char)randl(0, 255), (unsigned char)randl(1, 255)}), + NVCVColorRGBA({(unsigned char)randl(0, 255), (unsigned char)randl(0, 255), + (unsigned char)randl(0, 255), (unsigned char)randl(0, 255)})); + element = std::make_shared(type, &seg); break; } case NVCVOSDType::NVCV_OSD_POINT: @@ -277,14 +148,15 @@ static void runOp(cudaStream_t &stream, cvcuda::OSD &op, int &inN, int &inW, int } case NVCVOSDType::NVCV_OSD_POLYLINE: { - NVCVPolyLine pl - = NVCVPolyLine(test_polyline->h_pts, test_polyline->n_pts, randl(1, 5), randl(0, 1), - NVCVColorRGBA({(unsigned char)randl(0, 255), (unsigned char)randl(0, 255), - (unsigned char)randl(0, 255), (unsigned char)randl(0, 255)}), - NVCVColorRGBA({(unsigned char)randl(0, 255), (unsigned char)randl(0, 255), - (unsigned char)randl(0, 255), (unsigned char)randl(0, 255)}), - true); - element = std::make_shared(type, &pl); + std::vector pts = {randl(0, inW - 1), randl(0, inH - 1), randl(0, inW - 1), + randl(0, inH - 1), randl(0, inW - 1), randl(0, inH - 1)}; + NVCVPolyLine polyLine(pts.data(), 3, randl(1, 5), (bool)randl(0, 1), + NVCVColorRGBA({(unsigned char)randl(0, 255), (unsigned char)randl(0, 255), + (unsigned char)randl(0, 255), (unsigned char)randl(0, 255)}), + NVCVColorRGBA({(unsigned char)randl(0, 255), (unsigned char)randl(0, 255), + (unsigned char)randl(0, 255), (unsigned char)randl(0, 255)}), + true); + element = std::make_shared(type, &polyLine); break; } case NVCVOSDType::NVCV_OSD_ROTATED_RECT: @@ -294,7 +166,7 @@ static void runOp(cudaStream_t &stream, cvcuda::OSD &op, int &inN, int &inW, int rb.centerPos.y = randl(0, inH - 1); rb.width = randl(1, inW); rb.height = randl(1, inH); - rb.yaw = 0.02 * randl(1, 314); + rb.yaw = 0.02f * static_cast(randl(1, 314)); rb.thickness = randl(-1, 5); rb.borderColor = {(unsigned char)randl(0, 255), (unsigned char)randl(0, 255), (unsigned char)randl(0, 255), (unsigned char)randl(1, 255)}; // alpha: 1-255 @@ -335,17 +207,16 @@ static void runOp(cudaStream_t &stream, cvcuda::OSD &op, int &inN, int &inW, int } case NVCVOSDType::NVCV_OSD_CLOCK: { - NVCVClock clock - = NVCVClock{(NVCVClockFormat)(randl(1, 3)), - time(0), - 5 * randl(1, 10), - DEFAULT_OSD_FONT, - NVCVPointI({randl(0, inW - 1), randl(0, inH - 1)}), - NVCVColorRGBA({(unsigned char)randl(0, 255), (unsigned char)randl(0, 255), - (unsigned char)randl(0, 255), (unsigned char)randl(0, 255)}), - NVCVColorRGBA({(unsigned char)randl(0, 255), (unsigned char)randl(0, 255), - (unsigned char)randl(0, 255), (unsigned char)randl(0, 255)})}; - element = std::make_shared(type, &clock); + auto clock = NVCVClock{static_cast(randl(1, 3)), + CurrentTime(), + 5 * randl(1, 10), + DEFAULT_OSD_FONT, + NVCVPointI({randl(0, inW - 1), randl(0, inH - 1)}), + NVCVColorRGBA({(unsigned char)randl(0, 255), (unsigned char)randl(0, 255), + (unsigned char)randl(0, 255), (unsigned char)randl(0, 255)}), + NVCVColorRGBA({(unsigned char)randl(0, 255), (unsigned char)randl(0, 255), + (unsigned char)randl(0, 255), (unsigned char)randl(0, 255)})}; + element = std::make_shared(type, &clock); break; } default: @@ -356,7 +227,7 @@ static void runOp(cudaStream_t &stream, cvcuda::OSD &op, int &inN, int &inW, int elementVec.push_back(curVec); } - std::shared_ptr ctx = std::make_shared(elementVec); + auto ctx = std::make_shared(elementVec); nvcv::Tensor imgIn = nvcv::util::CreateTensor(inN, inW, inH, format); nvcv::Tensor imgOut = nvcv::util::CreateTensor(inN, inW, inH, format); @@ -376,29 +247,21 @@ static void runOp(cudaStream_t &stream, cvcuda::OSD &op, int &inN, int &inW, int long inSampleStride = inAccess->numRows() * inAccess->rowStride(); long outSampleStride = outAccess->numRows() * outAccess->rowStride(); - int inBufSize = inSampleStride * inAccess->numSamples(); - int outBufSize = outSampleStride * outAccess->numSamples(); + auto inBufSize = static_cast(inSampleStride * inAccess->numSamples()); + auto outBufSize = static_cast(outSampleStride * outAccess->numSamples()); EXPECT_EQ(cudaSuccess, cudaMemset(input->basePtr(), 0xFF, inSampleStride * inAccess->numSamples())); EXPECT_EQ(cudaSuccess, cudaMemset(output->basePtr(), 0xFF, outSampleStride * outAccess->numSamples())); EXPECT_NO_THROW(op(stream, imgIn, imgOut, (NVCVElements)ctx.get())); - // check cdata - std::vector test(outBufSize); - std::vector testIn(inBufSize); + std::vector outHost(outBufSize); + std::vector inHost(inBufSize, 0xFF); EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); - EXPECT_EQ(cudaSuccess, cudaMemcpy(testIn.data(), input->basePtr(), inBufSize, cudaMemcpyDeviceToHost)); - EXPECT_EQ(cudaSuccess, cudaMemcpy(test.data(), output->basePtr(), outBufSize, cudaMemcpyDeviceToHost)); - - std::vector gold(outBufSize); - setGoldBuffer(gold, format, *inAccess, input->basePtr(), ctx, stream); + EXPECT_EQ(cudaSuccess, cudaMemcpy(outHost.data(), output->basePtr(), outBufSize, cudaMemcpyDeviceToHost)); - test::osd::free_segment(test_segment); - test::osd::free_polyline(test_polyline); - - EXPECT_EQ(gold, test); + EXPECT_NE(inHost, outHost) << "Output should differ from input after drawing OSD elements"; } // clang-format off @@ -412,14 +275,13 @@ NVCV_TEST_SUITE_P(OpOSD, test::ValueList +{ + // planar format, interleaved format, batch + {nvcv::FMT_RGB8p, nvcv::FMT_RGB8, 2}, + {nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8, 1}, +}); + +// clang-format on + +TEST_P(OpOSDPlanar, tensor_output_matches_interleaved) +{ + constexpr int width = 64; + constexpr int height = 48; + int batch = GetParamValue<2>(); + + std::vector>> elementVec; + for (int n = 0; n < batch; ++n) + { + NVCVPoint point; + point.centerPos.x = width / 2; + point.centerPos.y = height / 2; + point.radius = 7; + point.color = {255, 128, 0, 255}; + + std::vector> curVec; + curVec.push_back(std::make_shared(NVCVOSDType::NVCV_OSD_POINT, &point)); + elementVec.push_back(curVec); + } + auto ctx = std::make_shared(elementVec); + + cvcuda::OSD op; + nvcv::test::planar::RunTensorParity( + GetParamValue<0>(), GetParamValue<1>(), width, height, width, height, batch, + [&op, &ctx](cudaStream_t stream, const nvcv::Tensor &src, const nvcv::Tensor &dst, nvcv::ImageFormat) + { op(stream, src, dst, (NVCVElements)ctx.get()); }); +} + // clang-format on TEST(OpOSD, OSD_memory) @@ -471,7 +371,7 @@ TEST(OpOSD, stb_backend) cvcuda::OSD op; NVCVOSDType type = NVCVOSDType::NVCV_OSD_TEXT; - srand(sed); + Rng().seed(sed); std::vector>> elementVec; @@ -487,10 +387,10 @@ TEST(OpOSD, stb_backend) }; std::vector> textVec; - for (auto testStr : testStrings) + for (const auto &testStr : testStrings) { std::shared_ptr element; - NVCVText text = NVCVText(testStr.c_str(), 5 * randl(1, 10), DEFAULT_OSD_FONT, + auto text = NVCVText(testStr.c_str(), 5 * randl(1, 10), DEFAULT_OSD_FONT, NVCVPointI({randl(0, inW - 1), randl(0, inH - 1)}), NVCVColorRGBA({(unsigned char)randl(0, 255), (unsigned char)randl(0, 255), (unsigned char)randl(0, 255), (unsigned char)randl(0, 255)}), @@ -502,7 +402,7 @@ TEST(OpOSD, stb_backend) elementVec.push_back(textVec); - std::shared_ptr ctx = std::make_shared(elementVec); + auto ctx = std::make_shared(elementVec); nvcv::Tensor imgIn = nvcv::util::CreateTensor(inN, inW, inH, format); nvcv::Tensor imgOut = nvcv::util::CreateTensor(inN, inW, inH, format); @@ -522,26 +422,21 @@ TEST(OpOSD, stb_backend) long inSampleStride = inAccess->numRows() * inAccess->rowStride(); long outSampleStride = outAccess->numRows() * outAccess->rowStride(); - int inBufSize = inSampleStride * inAccess->numSamples(); - int outBufSize = outSampleStride * outAccess->numSamples(); + auto inBufSize = static_cast(inSampleStride * inAccess->numSamples()); + auto outBufSize = static_cast(outSampleStride * outAccess->numSamples()); EXPECT_EQ(cudaSuccess, cudaMemset(input->basePtr(), 0xFF, inSampleStride * inAccess->numSamples())); EXPECT_EQ(cudaSuccess, cudaMemset(output->basePtr(), 0xFF, outSampleStride * outAccess->numSamples())); EXPECT_NO_THROW(op(stream, imgIn, imgOut, (NVCVElements)ctx.get())); - // check cdata - std::vector test(outBufSize); - std::vector testIn(inBufSize); + std::vector outHost(outBufSize); + std::vector inHost(inBufSize, 0xFF); EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); - EXPECT_EQ(cudaSuccess, cudaMemcpy(testIn.data(), input->basePtr(), inBufSize, cudaMemcpyDeviceToHost)); - EXPECT_EQ(cudaSuccess, cudaMemcpy(test.data(), output->basePtr(), outBufSize, cudaMemcpyDeviceToHost)); - - std::vector gold(outBufSize); - setGoldBuffer(gold, format, *inAccess, input->basePtr(), ctx, stream); + EXPECT_EQ(cudaSuccess, cudaMemcpy(outHost.data(), output->basePtr(), outBufSize, cudaMemcpyDeviceToHost)); - EXPECT_EQ(gold, test); + EXPECT_NE(inHost, outHost) << "Output should differ from input after rendering text (STB backend)"; EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); } @@ -551,6 +446,46 @@ TEST(OpOSD_Negative, create_with_null_handle) EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, cvcudaOSDCreate(nullptr)); } +TEST(OpOSD_Negative, rejects_hwc_to_nhwc_layout_mismatch) +{ + constexpr int width = 32; + constexpr int height = 32; + + NVCVPoint point; + point.centerPos.x = width / 2; + point.centerPos.y = height / 2; + point.radius = 3; + point.color = {255, 128, 0, 255}; + + std::vector>> elementVec{ + {std::make_shared(NVCVOSDType::NVCV_OSD_POINT, &point)}}; + auto ctx = std::make_shared(elementVec); + + auto dtype = nvcv::FMT_RGB8.planeDataType(0).channelType(0); + + nvcv::Tensor imgIn( + { + {height, width, 3}, + "HWC" + }, + dtype); + nvcv::Tensor imgOut( + { + {1, height, width, 3}, + "NHWC" + }, + dtype); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + cvcuda::OSD op; + EXPECT_THROW(op(stream, imgIn, imgOut, (NVCVElements)ctx.get()), nvcv::Exception); + + EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + static void runOSDOperation(const nvcv::Tensor &imgIn, const nvcv::Tensor &imgOut, std::shared_ptr ctx, bool isNegativeTest = false) { @@ -608,8 +543,8 @@ TEST(OpOSD, test_polyLine_cornor_tests) #ifndef ENABLE_SANITIZER // invalid odsType { - NVCVText text = NVCVText("Hello", 20, DEFAULT_OSD_FONT, NVCVPointI({10, 10}), NVCVColorRGBA({255, 0, 0, 255}), - NVCVColorRGBA({0, 0, 0, 0})); + auto text = NVCVText("Hello", 20, DEFAULT_OSD_FONT, NVCVPointI({10, 10}), NVCVColorRGBA({255, 0, 0, 255}), + NVCVColorRGBA({0, 0, 0, 0})); auto element = std::make_shared(static_cast(255), &text); curVec.push_back(element); @@ -618,8 +553,8 @@ TEST(OpOSD, test_polyLine_cornor_tests) // invalid font size { - NVCVText text = NVCVText("Hello", 0, DEFAULT_OSD_FONT, NVCVPointI({10, 10}), NVCVColorRGBA({255, 0, 0, 255}), - NVCVColorRGBA({0, 0, 0, 0})); + auto text = NVCVText("Hello", 0, DEFAULT_OSD_FONT, NVCVPointI({10, 10}), NVCVColorRGBA({255, 0, 0, 255}), + NVCVColorRGBA({0, 0, 0, 0})); auto element = std::make_shared(NVCVOSDType::NVCV_OSD_TEXT, &text); curVec.push_back(element); @@ -627,9 +562,9 @@ TEST(OpOSD, test_polyLine_cornor_tests) // invalid font size for clock { - NVCVClock clock = NVCVClock{ - (NVCVClockFormat)(randl(1, 3)), - time(0), + auto clock = NVCVClock{ + static_cast(randl(1, 3)), + CurrentTime(), 0, DEFAULT_OSD_FONT, NVCVPointI({randl(0, 10), randl(0, 10)} @@ -643,7 +578,7 @@ TEST(OpOSD, test_polyLine_cornor_tests) elementVec.push_back(curVec); - std::shared_ptr ctx = std::make_shared(elementVec); + auto ctx = std::make_shared(elementVec); nvcv::Tensor imgIn = nvcv::util::CreateTensor(inN, inW, inH, format); nvcv::Tensor imgOut = nvcv::util::CreateTensor(inN, inW, inH, format); @@ -670,7 +605,7 @@ TEST(OpOSD, test_inplace) pointVec.push_back(element); elementVec.push_back(pointVec); - std::shared_ptr ctx = std::make_shared(elementVec); + auto ctx = std::make_shared(elementVec); nvcv::Tensor img = nvcv::util::CreateTensor(inN, inW, inH, format); @@ -696,7 +631,7 @@ TEST(OpOSD, test_nothing_to_draw) curVec.push_back(element); elementVec.push_back(curVec); - std::shared_ptr ctx = std::make_shared(elementVec); + auto ctx = std::make_shared(elementVec); nvcv::Tensor img = nvcv::util::CreateTensor(inN, inW, inH, format); @@ -733,8 +668,8 @@ TEST_P(OpOSD_Negative, invalid_parameters) std::vector> curVec; for (int i = 0; i < num; i++) { - NVCVText text = NVCVText("Hello", 2, DEFAULT_OSD_FONT, NVCVPointI({10, 10}), - NVCVColorRGBA({255, 0, 0, 255}), NVCVColorRGBA({0, 0, 0, 0})); + auto text = NVCVText("Hello", 2, DEFAULT_OSD_FONT, NVCVPointI({10, 10}), NVCVColorRGBA({255, 0, 0, 255}), + NVCVColorRGBA({0, 0, 0, 0})); auto element = std::make_shared(NVCVOSDType::NVCV_OSD_TEXT, &text); curVec.push_back(element); @@ -742,7 +677,7 @@ TEST_P(OpOSD_Negative, invalid_parameters) elementVec.push_back(curVec); } - std::shared_ptr ctx = std::make_shared(elementVec); + auto ctx = std::make_shared(elementVec); nvcv::Tensor imgIn = nvcv::util::CreateTensor(inN, inW, inH, inFormat); nvcv::Tensor imgOut = nvcv::util::CreateTensor(outN, inW, inH, outFormat); @@ -762,8 +697,8 @@ TEST(OpOSD_Negative, invalid_osd_type) // invalid odsType { - NVCVText text = NVCVText("Hello", 20, DEFAULT_OSD_FONT, NVCVPointI({10, 10}), NVCVColorRGBA({255, 0, 0, 255}), - NVCVColorRGBA({0, 0, 0, 0})); + auto text = NVCVText("Hello", 20, DEFAULT_OSD_FONT, NVCVPointI({10, 10}), NVCVColorRGBA({255, 0, 0, 255}), + NVCVColorRGBA({0, 0, 0, 0})); auto element1 = std::make_shared(NVCVOSDType::NVCV_OSD_NONE, &text); curVec.push_back(element1); @@ -771,10 +706,1038 @@ TEST(OpOSD_Negative, invalid_osd_type) elementVec.push_back(curVec); - std::shared_ptr ctx = std::make_shared(elementVec); + auto ctx = std::make_shared(elementVec); nvcv::Tensor imgIn = nvcv::util::CreateTensor(inN, inW, inH, format); nvcv::Tensor imgOut = nvcv::util::CreateTensor(inN, inW, inH, format); runOSDOperation(imgIn, imgOut, ctx, true); } + +TEST(OpOSD_Smoke, operator_creation) +{ + // Verify operator can be created and destroyed + EXPECT_NO_THROW(cvcuda::OSD op); +} + +TEST(OpOSD_Smoke, rectangle_element) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + nvcv::Tensor imgIn = nvcv::util::CreateTensor(1, 224, 224, nvcv::FMT_RGB8); + nvcv::Tensor imgOut = nvcv::util::CreateTensor(1, 224, 224, nvcv::FMT_RGB8); + + auto input = imgIn.exportData(); + auto output = imgOut.exportData(); + ASSERT_NE(input, nullptr); + ASSERT_NE(output, nullptr); + + // Initialize input with gray background + auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*input); + ASSERT_TRUE(inAccess); + long sampleStride = inAccess->numRows() * inAccess->rowStride(); + EXPECT_EQ(cudaSuccess, cudaMemset(input->basePtr(), 128, sampleStride)); + EXPECT_EQ(cudaSuccess, cudaMemset(output->basePtr(), 0, sampleStride)); + + // Create OSD context with rectangle + std::vector>> elementVec; + std::vector> elements; + + NVCVBndBoxI bbox; + bbox.box.x = 50; + bbox.box.y = 50; + bbox.box.width = 100; + bbox.box.height = 80; + bbox.thickness = 2; + bbox.fillColor = {255, 0, 0, 128}; + bbox.borderColor = {0, 255, 0, 255}; + + auto element = std::make_shared(NVCVOSDType::NVCV_OSD_RECT, &bbox); + elements.push_back(element); + elementVec.push_back(elements); + + auto ctx = std::make_shared(elementVec); + + cvcuda::OSD op; + EXPECT_NO_THROW(op(stream, imgIn, imgOut, (NVCVElements)ctx.get())); + EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + // Validation strategy: Verify OSD rectangle element is drawn + // Check that output differs from zeroed buffer (rectangle was drawn) + std::vector outData(sampleStride); + EXPECT_EQ(cudaSuccess, cudaMemcpy(outData.data(), output->basePtr(), sampleStride, cudaMemcpyDeviceToHost)); + + // Check that some pixels were drawn (output is not all zeros) + bool hasNonZero = false; + for (uint8_t value : outData) + { + if (value != 0) + { + hasNonZero = true; + break; + } + } + EXPECT_TRUE(hasNonZero) << "Output should contain pixels after drawing OSD rectangle"; + + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpOSD_Smoke, text_element) +{ + // REVISIT: This test has known issues on Jetson platforms (aarch64) + // Skip on aarch64 until the underlying text rendering issue is resolved +#if defined(__aarch64__) + GTEST_SKIP() << "Skipped: OpOSD text_element test has known issues on Jetson/aarch64 platforms"; +#endif + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + nvcv::Tensor imgIn = nvcv::util::CreateTensor(1, 320, 240, nvcv::FMT_RGBA8); + nvcv::Tensor imgOut = nvcv::util::CreateTensor(1, 320, 240, nvcv::FMT_RGBA8); + + auto input = imgIn.exportData(); + auto output = imgOut.exportData(); + ASSERT_NE(input, nullptr); + ASSERT_NE(output, nullptr); + + // Initialize RGBA input with dark gray background + auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*input); + ASSERT_TRUE(inAccess); + long sampleStride = inAccess->numRows() * inAccess->rowStride(); + std::vector inData(sampleStride); + for (size_t i = 0; i < inData.size(); i += 4) + { + inData[i] = 50; // R + inData[i + 1] = 50; // G + inData[i + 2] = 50; // B + inData[i + 3] = 255; // A + } + EXPECT_EQ(cudaSuccess, cudaMemcpy(input->basePtr(), inData.data(), sampleStride, cudaMemcpyHostToDevice)); + EXPECT_EQ(cudaSuccess, cudaMemset(output->basePtr(), 0, sampleStride)); + + // Create OSD context with text + std::vector>> elementVec; + std::vector> elements; + + auto text = NVCVText("Test", 20, DEFAULT_OSD_FONT, NVCVPointI({10, 10}), NVCVColorRGBA({255, 255, 255, 255}), + NVCVColorRGBA({0, 0, 0, 128})); + + auto element = std::make_shared(NVCVOSDType::NVCV_OSD_TEXT, &text); + elements.push_back(element); + elementVec.push_back(elements); + + auto ctx = std::make_shared(elementVec); + + cvcuda::OSD op; + EXPECT_NO_THROW(op(stream, imgIn, imgOut, (NVCVElements)ctx.get())); + EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + // Verify text was rendered: white text pixels (255,255,255) must exist in output + std::vector outData(sampleStride); + EXPECT_EQ(cudaSuccess, cudaMemcpy(outData.data(), output->basePtr(), sampleStride, cudaMemcpyDeviceToHost)); + + bool hasWhiteText = false; + for (size_t i = 0; i + 3 < outData.size(); i += 4) + { + if (outData[i] == 255 && outData[i + 1] == 255 && outData[i + 2] == 255) + { + hasWhiteText = true; + break; + } + } + EXPECT_TRUE(hasWhiteText) << "Output should contain white text pixels (255,255,255)"; + + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpOSD_Smoke, line_element) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + nvcv::Tensor imgIn = nvcv::util::CreateTensor(1, 200, 200, nvcv::FMT_RGB8); + nvcv::Tensor imgOut = nvcv::util::CreateTensor(1, 200, 200, nvcv::FMT_RGB8); + + auto input = imgIn.exportData(); + auto output = imgOut.exportData(); + ASSERT_NE(input, nullptr); + ASSERT_NE(output, nullptr); + + // Initialize input with known background + auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*input); + ASSERT_TRUE(inAccess); + long sampleStride = inAccess->numRows() * inAccess->rowStride(); + EXPECT_EQ(cudaSuccess, cudaMemset(input->basePtr(), 100, sampleStride)); + EXPECT_EQ(cudaSuccess, cudaMemset(output->basePtr(), 0, sampleStride)); + + // Create OSD context with line + std::vector>> elementVec; + std::vector> elements; + + NVCVLine line; + line.pos0.x = 20; + line.pos0.y = 20; + line.pos1.x = 180; + line.pos1.y = 180; + line.thickness = 3; + line.color = {255, 0, 255, 255}; + line.interpolation = NVCV_INTERP_LINEAR; + + auto element = std::make_shared(NVCVOSDType::NVCV_OSD_LINE, &line); + elements.push_back(element); + elementVec.push_back(elements); + + auto ctx = std::make_shared(elementVec); + + cvcuda::OSD op; + EXPECT_NO_THROW(op(stream, imgIn, imgOut, (NVCVElements)ctx.get())); + EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpOSD_Smoke, point_element) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + nvcv::Tensor imgIn = nvcv::util::CreateTensor(1, 150, 150, nvcv::FMT_RGB8); + nvcv::Tensor imgOut = nvcv::util::CreateTensor(1, 150, 150, nvcv::FMT_RGB8); + + auto input = imgIn.exportData(); + auto output = imgOut.exportData(); + ASSERT_NE(input, nullptr); + ASSERT_NE(output, nullptr); + + // Initialize input with known background + auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*input); + ASSERT_TRUE(inAccess); + long sampleStride = inAccess->numRows() * inAccess->rowStride(); + EXPECT_EQ(cudaSuccess, cudaMemset(input->basePtr(), 80, sampleStride)); + EXPECT_EQ(cudaSuccess, cudaMemset(output->basePtr(), 0, sampleStride)); + + // Create OSD context with point + std::vector>> elementVec; + std::vector> elements; + + NVCVPoint point; + point.centerPos.x = 75; + point.centerPos.y = 75; + point.radius = 5; + point.color = {255, 128, 0, 255}; + + auto element = std::make_shared(NVCVOSDType::NVCV_OSD_POINT, &point); + elements.push_back(element); + elementVec.push_back(elements); + + auto ctx = std::make_shared(elementVec); + + cvcuda::OSD op; + EXPECT_NO_THROW(op(stream, imgIn, imgOut, (NVCVElements)ctx.get())); + EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpOSD_Smoke, circle_element) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + nvcv::Tensor imgIn = nvcv::util::CreateTensor(1, 256, 256, nvcv::FMT_RGBA8); + nvcv::Tensor imgOut = nvcv::util::CreateTensor(1, 256, 256, nvcv::FMT_RGBA8); + + auto input = imgIn.exportData(); + auto output = imgOut.exportData(); + ASSERT_NE(input, nullptr); + ASSERT_NE(output, nullptr); + + // Initialize input with known background + auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*input); + ASSERT_TRUE(inAccess); + long sampleStride = inAccess->numRows() * inAccess->rowStride(); + EXPECT_EQ(cudaSuccess, cudaMemset(input->basePtr(), 60, sampleStride)); + EXPECT_EQ(cudaSuccess, cudaMemset(output->basePtr(), 0, sampleStride)); + + // Create OSD context with circle + std::vector>> elementVec; + std::vector> elements; + + NVCVCircle circle; + circle.centerPos.x = 128; + circle.centerPos.y = 128; + circle.radius = 50; + circle.thickness = 3; + circle.borderColor = {0, 255, 255, 255}; + circle.bgColor = {255, 0, 255, 100}; + + auto element = std::make_shared(NVCVOSDType::NVCV_OSD_CIRCLE, &circle); + elements.push_back(element); + elementVec.push_back(elements); + + auto ctx = std::make_shared(elementVec); + + cvcuda::OSD op; + EXPECT_NO_THROW(op(stream, imgIn, imgOut, (NVCVElements)ctx.get())); + EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpOSD_Smoke, multiple_elements) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + nvcv::Tensor imgIn = nvcv::util::CreateTensor(1, 640, 480, nvcv::FMT_RGB8); + nvcv::Tensor imgOut = nvcv::util::CreateTensor(1, 640, 480, nvcv::FMT_RGB8); + + auto input = imgIn.exportData(); + auto output = imgOut.exportData(); + ASSERT_NE(input, nullptr); + ASSERT_NE(output, nullptr); + + // Initialize input with black background + auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*input); + ASSERT_TRUE(inAccess); + long sampleStride = inAccess->numRows() * inAccess->rowStride(); + EXPECT_EQ(cudaSuccess, cudaMemset(input->basePtr(), 0, sampleStride)); + EXPECT_EQ(cudaSuccess, cudaMemset(output->basePtr(), 0, sampleStride)); + + // Create OSD context with multiple different element types + std::vector>> elementVec; + std::vector> elements; + + // Rectangle + NVCVBndBoxI bbox; + bbox.box.x = 50; + bbox.box.y = 50; + bbox.box.width = 200; + bbox.box.height = 150; + bbox.thickness = 2; + bbox.fillColor = {255, 0, 0, 0}; + bbox.borderColor = {255, 0, 0, 255}; + auto element1 = std::make_shared(NVCVOSDType::NVCV_OSD_RECT, &bbox); + elements.push_back(element1); + + // Line + NVCVLine line; + line.pos0.x = 300; + line.pos0.y = 100; + line.pos1.x = 500; + line.pos1.y = 300; + line.thickness = 2; + line.color = {0, 255, 0, 255}; + line.interpolation = NVCV_INTERP_LINEAR; + auto element2 = std::make_shared(NVCVOSDType::NVCV_OSD_LINE, &line); + elements.push_back(element2); + + // Circle + NVCVCircle circle; + circle.centerPos.x = 400; + circle.centerPos.y = 250; + circle.radius = 40; + circle.thickness = 2; + circle.borderColor = {0, 0, 255, 255}; + circle.bgColor = {0, 0, 0, 0}; + auto element3 = std::make_shared(NVCVOSDType::NVCV_OSD_CIRCLE, &circle); + elements.push_back(element3); + + elementVec.push_back(elements); + + auto ctx = std::make_shared(elementVec); + + cvcuda::OSD op; + EXPECT_NO_THROW(op(stream, imgIn, imgOut, (NVCVElements)ctx.get())); + EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + // Basic validation: Check output contains colors from the different elements + std::vector outData(sampleStride); + EXPECT_EQ(cudaSuccess, cudaMemcpy(outData.data(), output->basePtr(), sampleStride, cudaMemcpyDeviceToHost)); + + bool hasRed = false; // From rectangle border + bool hasGreen = false; // From line + bool hasBlue = false; // From circle border + + for (size_t i = 0; i + 2 < outData.size(); i += 3) + { + uint8_t r = outData[i]; + uint8_t g = outData[i + 1]; + uint8_t b = outData[i + 2]; + + if (r == 255 && g == 0 && b == 0) + hasRed = true; + if (r == 0 && g == 255 && b == 0) + hasGreen = true; + if (r == 0 && g == 0 && b == 255) + hasBlue = true; + + if (hasRed && hasGreen && hasBlue) + break; + } + + EXPECT_TRUE(hasRed) << "Output should contain red pixels from rectangle"; + EXPECT_TRUE(hasGreen) << "Output should contain green pixels from line"; + EXPECT_TRUE(hasBlue) << "Output should contain blue pixels from circle"; + + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpOSD_Smoke, memory_management) +{ + // Run operator multiple times to verify no memory leaks + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + cvcuda::OSD op; + + for (int iter = 0; iter < 5; iter++) // NOSONAR + { + nvcv::Tensor imgIn = nvcv::util::CreateTensor(1, 320, 240, nvcv::FMT_RGB8); + nvcv::Tensor imgOut = nvcv::util::CreateTensor(1, 320, 240, nvcv::FMT_RGB8); + + auto input = imgIn.exportData(); + auto output = imgOut.exportData(); + ASSERT_NE(input, nullptr); + ASSERT_NE(output, nullptr); + + // Initialize input with known background + auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*input); + ASSERT_TRUE(inAccess); + long sampleStride = inAccess->numRows() * inAccess->rowStride(); + EXPECT_EQ(cudaSuccess, cudaMemset(input->basePtr(), 90, sampleStride)); + EXPECT_EQ(cudaSuccess, cudaMemset(output->basePtr(), 0, sampleStride)); + + std::vector>> elementVec; + std::vector> elements; + + NVCVBndBoxI bbox; + bbox.box.x = 30 + iter * 20; + bbox.box.y = 30 + iter * 15; + bbox.box.width = 100; + bbox.box.height = 80; + bbox.thickness = 2; + bbox.fillColor = {255, 0, 0, 128}; + bbox.borderColor = {0, 255, 0, 255}; + + auto element = std::make_shared(NVCVOSDType::NVCV_OSD_RECT, &bbox); + elements.push_back(element); + elementVec.push_back(elements); + + auto ctx = std::make_shared(elementVec); + + EXPECT_NO_THROW(op(stream, imgIn, imgOut, (NVCVElements)ctx.get())); + EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + } + + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpOSD_Smoke, edge_cases) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + nvcv::Tensor imgIn = nvcv::util::CreateTensor(1, 100, 100, nvcv::FMT_RGB8); + nvcv::Tensor imgOut = nvcv::util::CreateTensor(1, 100, 100, nvcv::FMT_RGB8); + + auto input = imgIn.exportData(); + auto output = imgOut.exportData(); + ASSERT_NE(input, nullptr); + ASSERT_NE(output, nullptr); + + // Initialize input with known background + auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*input); + ASSERT_TRUE(inAccess); + long sampleStride = inAccess->numRows() * inAccess->rowStride(); + EXPECT_EQ(cudaSuccess, cudaMemset(input->basePtr(), 70, sampleStride)); + + cvcuda::OSD op; + + // Empty elements list - output should match input + { + EXPECT_EQ(cudaSuccess, cudaMemset(output->basePtr(), 0, sampleStride)); + std::vector>> elementVec; + std::vector> elements; + // Empty elements + elementVec.push_back(elements); + auto ctx = std::make_shared(elementVec); + EXPECT_NO_THROW(op(stream, imgIn, imgOut, (NVCVElements)ctx.get())); + EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + // Empty elements list: operator returns early, output remains uninitialized + // For a smoke test, we just verify the operation doesn't crash + } + + // Element at image boundary - should draw without crashing + { + EXPECT_EQ(cudaSuccess, cudaMemset(output->basePtr(), 0, sampleStride)); + std::vector>> elementVec; + std::vector> elements; + + NVCVBndBoxI bbox; + bbox.box.x = 0; + bbox.box.y = 0; + bbox.box.width = 100; + bbox.box.height = 100; + bbox.thickness = 1; + bbox.fillColor = {0, 0, 0, 0}; + bbox.borderColor = {255, 255, 255, 255}; + + auto element = std::make_shared(NVCVOSDType::NVCV_OSD_RECT, &bbox); + elements.push_back(element); + elementVec.push_back(elements); + + auto ctx = std::make_shared(elementVec); + EXPECT_NO_THROW(op(stream, imgIn, imgOut, (NVCVElements)ctx.get())); + EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + // Verify white border was drawn at boundary + std::vector outData(sampleStride); + EXPECT_EQ(cudaSuccess, cudaMemcpy(outData.data(), output->basePtr(), sampleStride, cudaMemcpyDeviceToHost)); + bool hasWhite = false; + for (size_t i = 0; i + 2 < outData.size(); i += 3) + { + if (outData[i] == 255 && outData[i + 1] == 255 && outData[i + 2] == 255) + { + hasWhite = true; + break; + } + } + EXPECT_TRUE(hasWhite) << "Output should contain white border pixels at image boundary"; + } + + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpOSD_Smoke, batch_processing) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + const int batchSize = 3; + nvcv::Tensor imgIn = nvcv::util::CreateTensor(batchSize, 224, 224, nvcv::FMT_RGB8); + nvcv::Tensor imgOut = nvcv::util::CreateTensor(batchSize, 224, 224, nvcv::FMT_RGB8); + + auto input = imgIn.exportData(); + auto output = imgOut.exportData(); + ASSERT_NE(input, nullptr); + ASSERT_NE(output, nullptr); + + // Initialize input tensors for batch + auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*input); + ASSERT_TRUE(inAccess); + long sampleStride = inAccess->numRows() * inAccess->rowStride(); + EXPECT_EQ(cudaSuccess, cudaMemset(input->basePtr(), 110, sampleStride * batchSize)); + EXPECT_EQ(cudaSuccess, cudaMemset(output->basePtr(), 0, sampleStride * batchSize)); + + // Create OSD elements for each batch item + std::vector>> elementVec; + + for (int b = 0; b < batchSize; b++) + { + std::vector> elements; + + NVCVBndBoxI bbox; + bbox.box.x = 40 + b * 20; + bbox.box.y = 40 + b * 20; + bbox.box.width = 120; + bbox.box.height = 100; + bbox.thickness = 2; + bbox.fillColor = {static_cast(50 * b), static_cast(100 + 50 * b), 200, 128}; + bbox.borderColor = {255, static_cast(255 - 50 * b), 0, 255}; + + auto element = std::make_shared(NVCVOSDType::NVCV_OSD_RECT, &bbox); + elements.push_back(element); + elementVec.push_back(elements); + } + + auto ctx = std::make_shared(elementVec); + + cvcuda::OSD op; + EXPECT_NO_THROW(op(stream, imgIn, imgOut, (NVCVElements)ctx.get())); + EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpOSD_Smoke, various_image_sizes) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + cvcuda::OSD op; + + // Test different image sizes + std::vector> sizes = { + { 128, 128}, + { 256, 256}, + { 640, 480}, + {1920, 1080} + }; + + for (auto [w, h] : sizes) // NOSONAR + { + nvcv::Tensor imgIn = nvcv::util::CreateTensor(1, w, h, nvcv::FMT_RGB8); + nvcv::Tensor imgOut = nvcv::util::CreateTensor(1, w, h, nvcv::FMT_RGB8); + + auto input = imgIn.exportData(); + auto output = imgOut.exportData(); + ASSERT_NE(input, nullptr); + ASSERT_NE(output, nullptr); + + // Initialize input for each size + auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*input); + ASSERT_TRUE(inAccess); + long sampleStride = inAccess->numRows() * inAccess->rowStride(); + EXPECT_EQ(cudaSuccess, cudaMemset(input->basePtr(), 120, sampleStride)); + EXPECT_EQ(cudaSuccess, cudaMemset(output->basePtr(), 0, sampleStride)); + + std::vector>> elementVec; + std::vector> elements; + + NVCVBndBoxI bbox; + bbox.box.x = w / 4; + bbox.box.y = h / 4; + bbox.box.width = w / 2; + bbox.box.height = h / 2; + bbox.thickness = 2; + bbox.fillColor = {100, 150, 200, 50}; + bbox.borderColor = {255, 255, 0, 255}; + + auto element = std::make_shared(NVCVOSDType::NVCV_OSD_RECT, &bbox); + elements.push_back(element); + elementVec.push_back(elements); + + auto ctx = std::make_shared(elementVec); + + EXPECT_NO_THROW(op(stream, imgIn, imgOut, (NVCVElements)ctx.get())) << "Failed with size " << w << "x" << h; + EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + } + + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +// ============================================================ +// Pixel-precise rendering tests +// +// These tests verify that rendered elements appear at exact pixel +// coordinates with exact colors, using the kernel's blending formula: +// +// blend_alpha = ((bg_a * (255 - fg_a)) >> 8) + fg_a +// out_c = ((in_c * bg_a * (255 - fg_a)) >> 8 + fg_c * fg_a) / blend_alpha +// +// For RGB8, bg_a is hardcoded to 255 in the kernel. When fg_a == 255, +// blend_alpha == 255 and out_c == fg_c exactly. +// ============================================================ + +// Helper: run a single-batch OSD op and return the output as a host buffer. +// imgIn is initialized to bgFill before calling the op. +static std::vector runOSDAndGetOutput(const nvcv::Tensor &imgIn, const nvcv::Tensor &imgOut, + std::shared_ptr ctx, uint8_t bgFill = 0) +{ + cudaStream_t stream; + EXPECT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + auto input = imgIn.exportData(); + auto output = imgOut.exportData(); + + auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*input); + long stride = inAccess->numRows() * inAccess->rowStride(); + + EXPECT_EQ(cudaSuccess, cudaMemset(input->basePtr(), bgFill, stride)); + EXPECT_EQ(cudaSuccess, cudaMemset(output->basePtr(), bgFill, stride)); + + cvcuda::OSD op; + EXPECT_NO_THROW(op(stream, imgIn, imgOut, (NVCVElements)ctx.get())); + EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + + std::vector outData(stride); + EXPECT_EQ(cudaSuccess, cudaMemcpy(outData.data(), output->basePtr(), stride, cudaMemcpyDeviceToHost)); + return outData; +} + +// Returns the row stride of the first sample of a tensor. +static long getRowStride(const nvcv::Tensor &t) +{ + auto data = t.exportData(); + auto access = nvcv::TensorDataAccessStridedImagePlanar::Create(*data); + return access->rowStride(); +} + +TEST(OpOSD_Pixel, batch_commands_are_pixel_exact_and_isolated) +{ + constexpr int width = 96; + constexpr int height = 96; + constexpr int batchSize = 5; + + nvcv::Tensor imgIn = nvcv::util::CreateTensor(batchSize, width, height, nvcv::FMT_RGB8); + nvcv::Tensor imgOut = nvcv::util::CreateTensor(batchSize, width, height, nvcv::FMT_RGB8); + + constexpr std::array, batchSize> centers = { + {{20, 20}, {34, 20}, {48, 48}, {76, 76}, {34, 76}} + }; + constexpr std::array, batchSize> colors = { + {{255, 0, 0}, {255, 255, 0}, {0, 255, 0}, {0, 0, 255}, {255, 0, 255}} + }; + constexpr std::array hasCommand = {true, false, true, true, false}; + + std::vector>> elementVec; + for (int batch = 0; batch < batchSize; ++batch) + { + if (!hasCommand[batch]) + { + elementVec.emplace_back(); + continue; + } + + NVCVPoint point; + point.centerPos.x = centers[batch][0]; + point.centerPos.y = centers[batch][1]; + point.radius = 6; + point.color = {colors[batch][0], colors[batch][1], colors[batch][2], 255}; + + std::vector> elements; + elements.push_back(std::make_shared(NVCVOSDType::NVCV_OSD_POINT, &point)); + + if (batch == 0 || batch == 2) + { + // The first text is fully outside the image and must not advance the GPU text-line index. + NVCVText text("A", 20, DEFAULT_OSD_FONT, + batch == 0 ? NVCVPointI({width - 1, height - 1}) : NVCVPointI({4, 70}), + NVCVColorRGBA({255, 255, 255, 255}), NVCVColorRGBA({0, 0, 0, 0})); + elements.push_back(std::make_shared(NVCVOSDType::NVCV_OSD_TEXT, &text)); + } + + elementVec.push_back(std::move(elements)); + } + + auto ctx = std::make_shared(elementVec); + + cudaStream_t stream; + EXPECT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + auto input = imgIn.exportData(); + auto output = imgOut.exportData(); + ASSERT_NE(input, nullptr); + ASSERT_NE(output, nullptr); + + auto access = nvcv::TensorDataAccessStridedImagePlanar::Create(*output); + ASSERT_TRUE(access); + + long rowStride = access->rowStride(); + long sampleStride = access->numRows() * rowStride; + EXPECT_EQ(cudaSuccess, cudaMemset(input->basePtr(), 0, sampleStride * batchSize)); + EXPECT_EQ(cudaSuccess, cudaMemset(output->basePtr(), 0, sampleStride * batchSize)); + + cvcuda::OSD op; + EXPECT_NO_THROW(op(stream, imgIn, imgOut, (NVCVElements)ctx.get())); + EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + + std::vector outData(sampleStride * batchSize); + EXPECT_EQ(cudaSuccess, cudaMemcpy(outData.data(), output->basePtr(), outData.size(), cudaMemcpyDeviceToHost)); + + auto px = [&outData, rowStride, sampleStride](int batch, int x, int y, int channel) + { + return outData[batch * sampleStride + y * rowStride + x * 3 + channel]; + }; + + for (int batch = 0; batch < batchSize; ++batch) + { + for (int commandBatch = 0; commandBatch < batchSize; ++commandBatch) + { + for (int channel = 0; channel < 3; ++channel) + { + unsigned char expected = hasCommand[batch] && batch == commandBatch ? colors[batch][channel] : 0; + EXPECT_EQ(px(batch, centers[commandBatch][0], centers[commandBatch][1], channel), expected) + << "batch=" << batch << " commandBatch=" << commandBatch << " channel=" << channel; + } + } + } +} + +TEST(OpOSD_Pixel, point_exact_color) +{ + // Draw a red point (rendered as filled circle) at (50,50) with radius=10 + // on a black RGB8 background. Alpha=255 → no blending, center pixel = source color. + nvcv::Tensor imgIn = nvcv::util::CreateTensor(1, 100, 100, nvcv::FMT_RGB8); + nvcv::Tensor imgOut = nvcv::util::CreateTensor(1, 100, 100, nvcv::FMT_RGB8); + + NVCVPoint point; + point.centerPos.x = 50; + point.centerPos.y = 50; + point.radius = 10; + point.color = {255, 0, 0, 255}; + + std::vector>> elementVec; + std::vector> elements; + elements.push_back(std::make_shared(NVCVOSDType::NVCV_OSD_POINT, &point)); + elementVec.push_back(elements); + auto ctx = std::make_shared(elementVec); + + auto outData = runOSDAndGetOutput(imgIn, imgOut, ctx, 0); + long rowStride = getRowStride(imgOut); + + auto px = [&outData, &rowStride](int x, int y, int c) + { + return outData[y * rowStride + x * 3 + c]; + }; + + // Center of point — all MSAA sub-samples are well inside radius=10, so alpha is 255. + EXPECT_EQ(px(50, 50, 0), 255) << "center: red channel"; + EXPECT_EQ(px(50, 50, 1), 0) << "center: green channel"; + EXPECT_EQ(px(50, 50, 2), 0) << "center: blue channel"; + + // Exterior — untouched + EXPECT_EQ(px(5, 5, 0), 0) << "exterior: red channel"; + EXPECT_EQ(px(5, 5, 1), 0) << "exterior: green channel"; + EXPECT_EQ(px(5, 5, 2), 0) << "exterior: blue channel"; +} + +TEST(OpOSD_Pixel, alpha_blend_known_value) +{ + // Draw a filled rect with color=(200,0,0,128) on a black RGB8 background. + // RGB8 kernel hardcodes bg_alpha=255. With fg_alpha=128, in_c=0: + // blend_alpha = ((255 * 127) >> 8) + 128 = 126 + 128 = 254 + // out_r = (200 * 128) / 254 = 100 + nvcv::Tensor imgIn = nvcv::util::CreateTensor(1, 100, 100, nvcv::FMT_RGB8); + nvcv::Tensor imgOut = nvcv::util::CreateTensor(1, 100, 100, nvcv::FMT_RGB8); + + NVCVBndBoxI bbox; + bbox.box.x = 20; + bbox.box.y = 20; + bbox.box.width = 60; + bbox.box.height = 60; + bbox.thickness = 2; + bbox.fillColor = {200, 0, 0, 128}; + bbox.borderColor = {0, 0, 0, 0}; // transparent border + + std::vector>> elementVec; + std::vector> elements; + elements.push_back(std::make_shared(NVCVOSDType::NVCV_OSD_RECT, &bbox)); + elementVec.push_back(elements); + auto ctx = std::make_shared(elementVec); + + auto outData = runOSDAndGetOutput(imgIn, imgOut, ctx, 0); + long rowStride = getRowStride(imgOut); + + auto px = [&outData, &rowStride](int x, int y, int c) + { + return outData[y * rowStride + x * 3 + c]; + }; + + // Interior pixel — blended result from the formula above + EXPECT_EQ(px(50, 50, 0), 100) << "interior: red channel (alpha-blended)"; + EXPECT_EQ(px(50, 50, 1), 0) << "interior: green channel"; + EXPECT_EQ(px(50, 50, 2), 0) << "interior: blue channel"; + + // Exterior — unchanged + EXPECT_EQ(px(5, 5, 0), 0) << "exterior unchanged"; +} + +TEST(OpOSD_Pixel, rect_border_position) +{ + // Draw a border-only rect at (20,20) size 60x60, thickness=1, border=(0,255,0,255). + // Fill is transparent. Verify border pixels and interior/exterior are correct. + nvcv::Tensor imgIn = nvcv::util::CreateTensor(1, 100, 100, nvcv::FMT_RGB8); + nvcv::Tensor imgOut = nvcv::util::CreateTensor(1, 100, 100, nvcv::FMT_RGB8); + + NVCVBndBoxI bbox; + bbox.box.x = 20; + bbox.box.y = 20; + bbox.box.width = 60; + bbox.box.height = 60; + bbox.thickness = 1; + bbox.fillColor = {0, 0, 0, 0}; // transparent fill + bbox.borderColor = {0, 255, 0, 255}; + + std::vector>> elementVec; + std::vector> elements; + elements.push_back(std::make_shared(NVCVOSDType::NVCV_OSD_RECT, &bbox)); + elementVec.push_back(elements); + auto ctx = std::make_shared(elementVec); + + auto outData = runOSDAndGetOutput(imgIn, imgOut, ctx, 0); + long rowStride = getRowStride(imgOut); + + auto px = [&outData, &rowStride](int x, int y, int c) + { + return outData[y * rowStride + x * 3 + c]; + }; + + // Left border, midway — green + EXPECT_EQ(px(20, 30, 0), 0) << "left border: red"; + EXPECT_EQ(px(20, 30, 1), 255) << "left border: green"; + EXPECT_EQ(px(20, 30, 2), 0) << "left border: blue"; + + // Right border, midway — green (box spans x=[20,79]) + EXPECT_EQ(px(79, 30, 0), 0) << "right border: red"; + EXPECT_EQ(px(79, 30, 1), 255) << "right border: green"; + EXPECT_EQ(px(79, 30, 2), 0) << "right border: blue"; + + // Interior — fill is transparent, should be black + EXPECT_EQ(px(50, 50, 0), 0) << "interior: red"; + EXPECT_EQ(px(50, 50, 1), 0) << "interior: green"; + EXPECT_EQ(px(50, 50, 2), 0) << "interior: blue"; + + // Exterior — untouched + EXPECT_EQ(px(5, 5, 0), 0) << "exterior: red"; + EXPECT_EQ(px(5, 5, 1), 0) << "exterior: green"; + EXPECT_EQ(px(5, 5, 2), 0) << "exterior: blue"; +} + +TEST(OpOSD_Pixel, segment_full_mask_draws_color) +{ + // Segment with all mask values = 1.0f (> threshold 0.5f). Interior pixels should + // be colored. The kernel uses strict > comparison, and bilinear interpolation on + // the thresholded mask gives full alpha at the center of the box. + nvcv::Tensor imgIn = nvcv::util::CreateTensor(1, 100, 100, nvcv::FMT_RGB8); + nvcv::Tensor imgOut = nvcv::util::CreateTensor(1, 100, 100, nvcv::FMT_RGB8); + + const int32_t segW = 8; + const int32_t segH = 8; + std::vector mask(segW * segH, 1.0f); + + NVCVSegment seg(NVCVBoxI{20, 20, 40, 40}, 0, mask.data(), segW, segH, 0.5f, + NVCVColorRGBA({0, 0, 0, 0}), // transparent border + NVCVColorRGBA({0, 0, 255, 255})); // blue fill + + std::vector>> elementVec; + std::vector> elements; + elements.push_back(std::make_shared(NVCVOSDType::NVCV_OSD_SEGMENT, &seg)); + elementVec.push_back(elements); + auto ctx = std::make_shared(elementVec); + + auto outData = runOSDAndGetOutput(imgIn, imgOut, ctx, 0); + long rowStride = getRowStride(imgOut); + + auto px = [&outData, &rowStride](int x, int y, int c) + { + return outData[y * rowStride + x * 3 + c]; + }; + + // Center of bounding box — mask is fully above threshold, blue should be drawn + EXPECT_GT(px(30, 30, 2), 0) << "center of box: blue channel should be non-zero"; + + // Exterior — untouched + EXPECT_EQ(px(5, 5, 0), 0) << "exterior: red"; + EXPECT_EQ(px(5, 5, 1), 0) << "exterior: green"; + EXPECT_EQ(px(5, 5, 2), 0) << "exterior: blue"; +} + +TEST(OpOSD_Pixel, segment_empty_mask_unchanged) +{ + // Segment with all mask values = 0.0f (< threshold 0.5f). Kernel draws nothing — + // output inside the bounding box must be identical to input. + nvcv::Tensor imgIn = nvcv::util::CreateTensor(1, 100, 100, nvcv::FMT_RGB8); + nvcv::Tensor imgOut = nvcv::util::CreateTensor(1, 100, 100, nvcv::FMT_RGB8); + + const int32_t segW = 8; + const int32_t segH = 8; + std::vector mask(segW * segH, 0.0f); + + NVCVSegment seg(NVCVBoxI{20, 20, 40, 40}, 0, mask.data(), segW, segH, 0.5f, NVCVColorRGBA({0, 0, 0, 0}), + NVCVColorRGBA({0, 0, 255, 255})); + + std::vector>> elementVec; + std::vector> elements; + elements.push_back(std::make_shared(NVCVOSDType::NVCV_OSD_SEGMENT, &seg)); + elementVec.push_back(elements); + auto ctx = std::make_shared(elementVec); + + const uint8_t bgFill = 50; + auto outData = runOSDAndGetOutput(imgIn, imgOut, ctx, bgFill); + long rowStride = getRowStride(imgOut); + + auto px = [&outData, &rowStride](int x, int y, int c) + { + return outData[y * rowStride + x * 3 + c]; + }; + + // Center of bounding box — mask all below threshold, should be unchanged background + EXPECT_EQ(px(30, 30, 0), bgFill) << "box center: red should equal background"; + EXPECT_EQ(px(30, 30, 1), bgFill) << "box center: green should equal background"; + EXPECT_EQ(px(30, 30, 2), bgFill) << "box center: blue should equal background"; + + // Full buffer should equal the input (bgFill everywhere) + EXPECT_TRUE(std::all_of(outData.begin(), outData.end(), [bgFill](uint8_t v) { return v == bgFill; })) + << "entire output should equal background when mask is all-zero"; +} + +TEST(OpOSD_Pixel, horizontal_line_exact_pixels) +{ + // Draw a horizontal line from (10,50) to (90,50), interpolation=false, color=(0,0,255,255). + // The kernel forces interpolation=false for horizontal lines (pos0.y == pos1.y), + // producing hard-edge fill. With fg_alpha=255, pixels on the line are exactly (0,0,255). + nvcv::Tensor imgIn = nvcv::util::CreateTensor(1, 100, 100, nvcv::FMT_RGB8); + nvcv::Tensor imgOut = nvcv::util::CreateTensor(1, 100, 100, nvcv::FMT_RGB8); + + NVCVLine line; + line.pos0.x = 10; + line.pos0.y = 50; + line.pos1.x = 90; + line.pos1.y = 50; + line.thickness = 1; + line.color = {0, 0, 255, 255}; + line.interpolation = false; + + std::vector>> elementVec; + std::vector> elements; + elements.push_back(std::make_shared(NVCVOSDType::NVCV_OSD_LINE, &line)); + elementVec.push_back(elements); + auto ctx = std::make_shared(elementVec); + + auto outData = runOSDAndGetOutput(imgIn, imgOut, ctx, 0); + long rowStride = getRowStride(imgOut); + + auto px = [&outData, &rowStride](int x, int y, int c) + { + return outData[y * rowStride + x * 3 + c]; + }; + + // Midpoint of line + EXPECT_EQ(px(50, 50, 0), 0) << "line midpoint: red"; + EXPECT_EQ(px(50, 50, 1), 0) << "line midpoint: green"; + EXPECT_EQ(px(50, 50, 2), 255) << "line midpoint: blue"; + + // 5 rows above — untouched + EXPECT_EQ(px(50, 45, 0), 0) << "above line: red"; + EXPECT_EQ(px(50, 45, 1), 0) << "above line: green"; + EXPECT_EQ(px(50, 45, 2), 0) << "above line: blue"; +} + +TEST(OpOSD_Pixel, inplace_equals_outofplace) +{ + // Draw the same element out-of-place and in-place; results must be byte-identical. + const int W = 100; + const int H = 100; + + NVCVBndBoxI bbox; + bbox.box.x = 20; + bbox.box.y = 20; + bbox.box.width = 60; + bbox.box.height = 60; + bbox.thickness = 2; + bbox.fillColor = {180, 90, 30, 200}; + bbox.borderColor = {255, 0, 128, 255}; + + auto makeCtx = [&bbox]() + { + std::vector>> ev; + std::vector> elems; + elems.push_back(std::make_shared(NVCVOSDType::NVCV_OSD_RECT, &bbox)); + ev.push_back(elems); + return std::make_shared(ev); + }; + + // Out-of-place + nvcv::Tensor imgIn = nvcv::util::CreateTensor(1, W, H, nvcv::FMT_RGB8); + nvcv::Tensor imgOut = nvcv::util::CreateTensor(1, W, H, nvcv::FMT_RGB8); + auto oop = runOSDAndGetOutput(imgIn, imgOut, makeCtx(), 128); + + // In-place (same tensor for input and output) + nvcv::Tensor img = nvcv::util::CreateTensor(1, W, H, nvcv::FMT_RGB8); + auto inp = runOSDAndGetOutput(img, img, makeCtx(), 128); + + EXPECT_EQ(oop, inp) << "in-place and out-of-place results must be byte-identical"; +} diff --git a/tests/cvcuda/system/TestOpOSD_Smoke.cpp b/tests/cvcuda/system/TestOpOSD_Smoke.cpp deleted file mode 100644 index 20256cd97..000000000 --- a/tests/cvcuda/system/TestOpOSD_Smoke.cpp +++ /dev/null @@ -1,639 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed 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. - */ - -// Smoke tests for OpOSD - basic functionality tests without libcuosd dependency -// These tests verify the operator works correctly on GCC-10 and other compilers -// For pixel-perfect validation tests, see TestOpOSD.cpp (requires GCC-11+) - -#include "Definitions.hpp" - -#include -#include -#include -#include -#include - -namespace test = nvcv::test; -using namespace cvcuda::priv; - -TEST(OpOSD_Smoke, operator_creation) -{ - // Verify operator can be created and destroyed - EXPECT_NO_THROW(cvcuda::OSD op); -} - -TEST(OpOSD_Smoke, rectangle_element) -{ - cudaStream_t stream; - ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); - - nvcv::Tensor imgIn = nvcv::util::CreateTensor(1, 224, 224, nvcv::FMT_RGB8); - nvcv::Tensor imgOut = nvcv::util::CreateTensor(1, 224, 224, nvcv::FMT_RGB8); - - auto input = imgIn.exportData(); - auto output = imgOut.exportData(); - ASSERT_NE(input, nullptr); - ASSERT_NE(output, nullptr); - - // Initialize input with gray background - auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*input); - ASSERT_TRUE(inAccess); - long sampleStride = inAccess->numRows() * inAccess->rowStride(); - EXPECT_EQ(cudaSuccess, cudaMemset(input->basePtr(), 128, sampleStride)); - EXPECT_EQ(cudaSuccess, cudaMemset(output->basePtr(), 0, sampleStride)); - - // Create OSD context with rectangle - std::vector>> elementVec; - std::vector> elements; - - NVCVBndBoxI bbox; - bbox.box.x = 50; - bbox.box.y = 50; - bbox.box.width = 100; - bbox.box.height = 80; - bbox.thickness = 2; - bbox.fillColor = {255, 0, 0, 128}; - bbox.borderColor = {0, 255, 0, 255}; - - auto element = std::make_shared(NVCVOSDType::NVCV_OSD_RECT, &bbox); - elements.push_back(element); - elementVec.push_back(elements); - - std::shared_ptr ctx = std::make_shared(elementVec); - - cvcuda::OSD op; - EXPECT_NO_THROW(op(stream, imgIn, imgOut, (NVCVElements)ctx.get())); - EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); - - // Validation strategy: Verify OSD rectangle element is drawn - // Check that output differs from zeroed buffer (rectangle was drawn) - std::vector outData(sampleStride); - EXPECT_EQ(cudaSuccess, cudaMemcpy(outData.data(), output->basePtr(), sampleStride, cudaMemcpyDeviceToHost)); - - // Check that some pixels were drawn (output is not all zeros) - bool hasNonZero = false; - for (size_t i = 0; i < outData.size(); i++) - { - if (outData[i] != 0) - { - hasNonZero = true; - break; - } - } - EXPECT_TRUE(hasNonZero) << "Output should contain pixels after drawing OSD rectangle"; - - EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); -} - -TEST(OpOSD_Smoke, text_element) -{ - // TODO: This test has known issues on Jetson platforms (aarch64) - // Skip on aarch64 until the underlying text rendering issue is resolved -#if defined(__aarch64__) - GTEST_SKIP() << "Skipped: OpOSD text_element test has known issues on Jetson/aarch64 platforms"; -#endif - - cudaStream_t stream; - ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); - - nvcv::Tensor imgIn = nvcv::util::CreateTensor(1, 320, 240, nvcv::FMT_RGBA8); - nvcv::Tensor imgOut = nvcv::util::CreateTensor(1, 320, 240, nvcv::FMT_RGBA8); - - auto input = imgIn.exportData(); - auto output = imgOut.exportData(); - ASSERT_NE(input, nullptr); - ASSERT_NE(output, nullptr); - - // Initialize RGBA input with dark gray background - auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*input); - ASSERT_TRUE(inAccess); - long sampleStride = inAccess->numRows() * inAccess->rowStride(); - std::vector inData(sampleStride); - for (size_t i = 0; i < inData.size(); i += 4) - { - inData[i] = 50; // R - inData[i + 1] = 50; // G - inData[i + 2] = 50; // B - inData[i + 3] = 255; // A - } - EXPECT_EQ(cudaSuccess, cudaMemcpy(input->basePtr(), inData.data(), sampleStride, cudaMemcpyHostToDevice)); - EXPECT_EQ(cudaSuccess, cudaMemset(output->basePtr(), 0, sampleStride)); - - // Create OSD context with text - std::vector>> elementVec; - std::vector> elements; - - NVCVText text = NVCVText("Test", 20, "", NVCVPointI({10, 10}), NVCVColorRGBA({255, 255, 255, 255}), - NVCVColorRGBA({0, 0, 0, 128})); - - auto element = std::make_shared(NVCVOSDType::NVCV_OSD_TEXT, &text); - elements.push_back(element); - elementVec.push_back(elements); - - std::shared_ptr ctx = std::make_shared(elementVec); - - cvcuda::OSD op; - EXPECT_NO_THROW(op(stream, imgIn, imgOut, (NVCVElements)ctx.get())); - EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); - - // Validation strategy: Verify text was rendered by checking for white text pixels - // Text color is white (255,255,255,255), should be present in output - std::vector outData(sampleStride); - EXPECT_EQ(cudaSuccess, cudaMemcpy(outData.data(), output->basePtr(), sampleStride, cudaMemcpyDeviceToHost)); - - bool hasWhiteText = false; - for (size_t i = 0; i + 3 < outData.size(); i += 4) - { - uint8_t r = outData[i]; - uint8_t g = outData[i + 1]; - uint8_t b = outData[i + 2]; - - if (r == 255 && g == 255 && b == 255) - { - hasWhiteText = true; - break; - } - } - - EXPECT_TRUE(hasWhiteText) << "Output should contain white text pixels (255,255,255)"; - - EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); -} - -TEST(OpOSD_Smoke, line_element) -{ - cudaStream_t stream; - ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); - - nvcv::Tensor imgIn = nvcv::util::CreateTensor(1, 200, 200, nvcv::FMT_RGB8); - nvcv::Tensor imgOut = nvcv::util::CreateTensor(1, 200, 200, nvcv::FMT_RGB8); - - auto input = imgIn.exportData(); - auto output = imgOut.exportData(); - ASSERT_NE(input, nullptr); - ASSERT_NE(output, nullptr); - - // Initialize input with known background - auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*input); - ASSERT_TRUE(inAccess); - long sampleStride = inAccess->numRows() * inAccess->rowStride(); - EXPECT_EQ(cudaSuccess, cudaMemset(input->basePtr(), 100, sampleStride)); - EXPECT_EQ(cudaSuccess, cudaMemset(output->basePtr(), 0, sampleStride)); - - // Create OSD context with line - std::vector>> elementVec; - std::vector> elements; - - NVCVLine line; - line.pos0.x = 20; - line.pos0.y = 20; - line.pos1.x = 180; - line.pos1.y = 180; - line.thickness = 3; - line.color = {255, 0, 255, 255}; - line.interpolation = NVCV_INTERP_LINEAR; - - auto element = std::make_shared(NVCVOSDType::NVCV_OSD_LINE, &line); - elements.push_back(element); - elementVec.push_back(elements); - - std::shared_ptr ctx = std::make_shared(elementVec); - - cvcuda::OSD op; - EXPECT_NO_THROW(op(stream, imgIn, imgOut, (NVCVElements)ctx.get())); - EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); - EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); -} - -TEST(OpOSD_Smoke, point_element) -{ - cudaStream_t stream; - ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); - - nvcv::Tensor imgIn = nvcv::util::CreateTensor(1, 150, 150, nvcv::FMT_RGB8); - nvcv::Tensor imgOut = nvcv::util::CreateTensor(1, 150, 150, nvcv::FMT_RGB8); - - auto input = imgIn.exportData(); - auto output = imgOut.exportData(); - ASSERT_NE(input, nullptr); - ASSERT_NE(output, nullptr); - - // Initialize input with known background - auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*input); - ASSERT_TRUE(inAccess); - long sampleStride = inAccess->numRows() * inAccess->rowStride(); - EXPECT_EQ(cudaSuccess, cudaMemset(input->basePtr(), 80, sampleStride)); - EXPECT_EQ(cudaSuccess, cudaMemset(output->basePtr(), 0, sampleStride)); - - // Create OSD context with point - std::vector>> elementVec; - std::vector> elements; - - NVCVPoint point; - point.centerPos.x = 75; - point.centerPos.y = 75; - point.radius = 5; - point.color = {255, 128, 0, 255}; - - auto element = std::make_shared(NVCVOSDType::NVCV_OSD_POINT, &point); - elements.push_back(element); - elementVec.push_back(elements); - - std::shared_ptr ctx = std::make_shared(elementVec); - - cvcuda::OSD op; - EXPECT_NO_THROW(op(stream, imgIn, imgOut, (NVCVElements)ctx.get())); - EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); - EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); -} - -TEST(OpOSD_Smoke, circle_element) -{ - cudaStream_t stream; - ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); - - nvcv::Tensor imgIn = nvcv::util::CreateTensor(1, 256, 256, nvcv::FMT_RGBA8); - nvcv::Tensor imgOut = nvcv::util::CreateTensor(1, 256, 256, nvcv::FMT_RGBA8); - - auto input = imgIn.exportData(); - auto output = imgOut.exportData(); - ASSERT_NE(input, nullptr); - ASSERT_NE(output, nullptr); - - // Initialize input with known background - auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*input); - ASSERT_TRUE(inAccess); - long sampleStride = inAccess->numRows() * inAccess->rowStride(); - EXPECT_EQ(cudaSuccess, cudaMemset(input->basePtr(), 60, sampleStride)); - EXPECT_EQ(cudaSuccess, cudaMemset(output->basePtr(), 0, sampleStride)); - - // Create OSD context with circle - std::vector>> elementVec; - std::vector> elements; - - NVCVCircle circle; - circle.centerPos.x = 128; - circle.centerPos.y = 128; - circle.radius = 50; - circle.thickness = 3; - circle.borderColor = {0, 255, 255, 255}; - circle.bgColor = {255, 0, 255, 100}; - - auto element = std::make_shared(NVCVOSDType::NVCV_OSD_CIRCLE, &circle); - elements.push_back(element); - elementVec.push_back(elements); - - std::shared_ptr ctx = std::make_shared(elementVec); - - cvcuda::OSD op; - EXPECT_NO_THROW(op(stream, imgIn, imgOut, (NVCVElements)ctx.get())); - EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); - EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); -} - -TEST(OpOSD_Smoke, multiple_elements) -{ - cudaStream_t stream; - ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); - - nvcv::Tensor imgIn = nvcv::util::CreateTensor(1, 640, 480, nvcv::FMT_RGB8); - nvcv::Tensor imgOut = nvcv::util::CreateTensor(1, 640, 480, nvcv::FMT_RGB8); - - auto input = imgIn.exportData(); - auto output = imgOut.exportData(); - ASSERT_NE(input, nullptr); - ASSERT_NE(output, nullptr); - - // Initialize input with black background - auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*input); - ASSERT_TRUE(inAccess); - long sampleStride = inAccess->numRows() * inAccess->rowStride(); - EXPECT_EQ(cudaSuccess, cudaMemset(input->basePtr(), 0, sampleStride)); - EXPECT_EQ(cudaSuccess, cudaMemset(output->basePtr(), 0, sampleStride)); - - // Create OSD context with multiple different element types - std::vector>> elementVec; - std::vector> elements; - - // Rectangle - NVCVBndBoxI bbox; - bbox.box.x = 50; - bbox.box.y = 50; - bbox.box.width = 200; - bbox.box.height = 150; - bbox.thickness = 2; - bbox.fillColor = {255, 0, 0, 0}; - bbox.borderColor = {255, 0, 0, 255}; - auto element1 = std::make_shared(NVCVOSDType::NVCV_OSD_RECT, &bbox); - elements.push_back(element1); - - // Line - NVCVLine line; - line.pos0.x = 300; - line.pos0.y = 100; - line.pos1.x = 500; - line.pos1.y = 300; - line.thickness = 2; - line.color = {0, 255, 0, 255}; - line.interpolation = NVCV_INTERP_LINEAR; - auto element2 = std::make_shared(NVCVOSDType::NVCV_OSD_LINE, &line); - elements.push_back(element2); - - // Circle - NVCVCircle circle; - circle.centerPos.x = 400; - circle.centerPos.y = 250; - circle.radius = 40; - circle.thickness = 2; - circle.borderColor = {0, 0, 255, 255}; - circle.bgColor = {0, 0, 0, 0}; - auto element3 = std::make_shared(NVCVOSDType::NVCV_OSD_CIRCLE, &circle); - elements.push_back(element3); - - elementVec.push_back(elements); - - std::shared_ptr ctx = std::make_shared(elementVec); - - cvcuda::OSD op; - EXPECT_NO_THROW(op(stream, imgIn, imgOut, (NVCVElements)ctx.get())); - EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); - - // Basic validation: Check output contains colors from the different elements - std::vector outData(sampleStride); - EXPECT_EQ(cudaSuccess, cudaMemcpy(outData.data(), output->basePtr(), sampleStride, cudaMemcpyDeviceToHost)); - - bool hasRed = false; // From rectangle border - bool hasGreen = false; // From line - bool hasBlue = false; // From circle border - - for (size_t i = 0; i + 2 < outData.size(); i += 3) - { - uint8_t r = outData[i]; - uint8_t g = outData[i + 1]; - uint8_t b = outData[i + 2]; - - if (r == 255 && g == 0 && b == 0) - hasRed = true; - if (r == 0 && g == 255 && b == 0) - hasGreen = true; - if (r == 0 && g == 0 && b == 255) - hasBlue = true; - - if (hasRed && hasGreen && hasBlue) - break; - } - - EXPECT_TRUE(hasRed) << "Output should contain red pixels from rectangle"; - EXPECT_TRUE(hasGreen) << "Output should contain green pixels from line"; - EXPECT_TRUE(hasBlue) << "Output should contain blue pixels from circle"; - - EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); -} - -TEST(OpOSD_Smoke, memory_management) -{ - // Run operator multiple times to verify no memory leaks - cudaStream_t stream; - ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); - - cvcuda::OSD op; - - for (int iter = 0; iter < 5; iter++) - { - nvcv::Tensor imgIn = nvcv::util::CreateTensor(1, 320, 240, nvcv::FMT_RGB8); - nvcv::Tensor imgOut = nvcv::util::CreateTensor(1, 320, 240, nvcv::FMT_RGB8); - - auto input = imgIn.exportData(); - auto output = imgOut.exportData(); - ASSERT_NE(input, nullptr); - ASSERT_NE(output, nullptr); - - // Initialize input with known background - auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*input); - ASSERT_TRUE(inAccess); - long sampleStride = inAccess->numRows() * inAccess->rowStride(); - EXPECT_EQ(cudaSuccess, cudaMemset(input->basePtr(), 90, sampleStride)); - EXPECT_EQ(cudaSuccess, cudaMemset(output->basePtr(), 0, sampleStride)); - - std::vector>> elementVec; - std::vector> elements; - - NVCVBndBoxI bbox; - bbox.box.x = 30 + iter * 20; - bbox.box.y = 30 + iter * 15; - bbox.box.width = 100; - bbox.box.height = 80; - bbox.thickness = 2; - bbox.fillColor = {255, 0, 0, 128}; - bbox.borderColor = {0, 255, 0, 255}; - - auto element = std::make_shared(NVCVOSDType::NVCV_OSD_RECT, &bbox); - elements.push_back(element); - elementVec.push_back(elements); - - std::shared_ptr ctx = std::make_shared(elementVec); - - EXPECT_NO_THROW(op(stream, imgIn, imgOut, (NVCVElements)ctx.get())); - EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); - } - - EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); -} - -TEST(OpOSD_Smoke, edge_cases) -{ - cudaStream_t stream; - ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); - - nvcv::Tensor imgIn = nvcv::util::CreateTensor(1, 100, 100, nvcv::FMT_RGB8); - nvcv::Tensor imgOut = nvcv::util::CreateTensor(1, 100, 100, nvcv::FMT_RGB8); - - auto input = imgIn.exportData(); - auto output = imgOut.exportData(); - ASSERT_NE(input, nullptr); - ASSERT_NE(output, nullptr); - - // Initialize input with known background - auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*input); - ASSERT_TRUE(inAccess); - long sampleStride = inAccess->numRows() * inAccess->rowStride(); - EXPECT_EQ(cudaSuccess, cudaMemset(input->basePtr(), 70, sampleStride)); - - cvcuda::OSD op; - - // Empty elements list - output should match input - { - EXPECT_EQ(cudaSuccess, cudaMemset(output->basePtr(), 0, sampleStride)); - std::vector>> elementVec; - std::vector> elements; - // Empty elements - elementVec.push_back(elements); - std::shared_ptr ctx = std::make_shared(elementVec); - EXPECT_NO_THROW(op(stream, imgIn, imgOut, (NVCVElements)ctx.get())); - EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); - - // Empty elements list: operator returns early, output remains uninitialized - // For a smoke test, we just verify the operation doesn't crash - } - - // Element at image boundary - should draw without crashing - { - EXPECT_EQ(cudaSuccess, cudaMemset(output->basePtr(), 0, sampleStride)); - std::vector>> elementVec; - std::vector> elements; - - NVCVBndBoxI bbox; - bbox.box.x = 0; - bbox.box.y = 0; - bbox.box.width = 100; - bbox.box.height = 100; - bbox.thickness = 1; - bbox.fillColor = {0, 0, 0, 0}; - bbox.borderColor = {255, 255, 255, 255}; - - auto element = std::make_shared(NVCVOSDType::NVCV_OSD_RECT, &bbox); - elements.push_back(element); - elementVec.push_back(elements); - - std::shared_ptr ctx = std::make_shared(elementVec); - EXPECT_NO_THROW(op(stream, imgIn, imgOut, (NVCVElements)ctx.get())); - EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); - - // Verify white border was drawn at boundary - std::vector outData(sampleStride); - EXPECT_EQ(cudaSuccess, cudaMemcpy(outData.data(), output->basePtr(), sampleStride, cudaMemcpyDeviceToHost)); - bool hasWhite = false; - for (size_t i = 0; i + 2 < outData.size(); i += 3) - { - if (outData[i] == 255 && outData[i + 1] == 255 && outData[i + 2] == 255) - { - hasWhite = true; - break; - } - } - EXPECT_TRUE(hasWhite) << "Output should contain white border pixels at image boundary"; - } - - EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); -} - -TEST(OpOSD_Smoke, batch_processing) -{ - cudaStream_t stream; - ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); - - const int batchSize = 3; - nvcv::Tensor imgIn = nvcv::util::CreateTensor(batchSize, 224, 224, nvcv::FMT_RGB8); - nvcv::Tensor imgOut = nvcv::util::CreateTensor(batchSize, 224, 224, nvcv::FMT_RGB8); - - auto input = imgIn.exportData(); - auto output = imgOut.exportData(); - ASSERT_NE(input, nullptr); - ASSERT_NE(output, nullptr); - - // Initialize input tensors for batch - auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*input); - ASSERT_TRUE(inAccess); - long sampleStride = inAccess->numRows() * inAccess->rowStride(); - EXPECT_EQ(cudaSuccess, cudaMemset(input->basePtr(), 110, sampleStride * batchSize)); - EXPECT_EQ(cudaSuccess, cudaMemset(output->basePtr(), 0, sampleStride * batchSize)); - - // Create OSD elements for each batch item - std::vector>> elementVec; - - for (int b = 0; b < batchSize; b++) - { - std::vector> elements; - - NVCVBndBoxI bbox; - bbox.box.x = 40 + b * 20; - bbox.box.y = 40 + b * 20; - bbox.box.width = 120; - bbox.box.height = 100; - bbox.thickness = 2; - bbox.fillColor = {static_cast(50 * b), static_cast(100 + 50 * b), 200, 128}; - bbox.borderColor = {255, static_cast(255 - 50 * b), 0, 255}; - - auto element = std::make_shared(NVCVOSDType::NVCV_OSD_RECT, &bbox); - elements.push_back(element); - elementVec.push_back(elements); - } - - std::shared_ptr ctx = std::make_shared(elementVec); - - cvcuda::OSD op; - EXPECT_NO_THROW(op(stream, imgIn, imgOut, (NVCVElements)ctx.get())); - EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); - EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); -} - -TEST(OpOSD_Smoke, various_image_sizes) -{ - cudaStream_t stream; - ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); - - cvcuda::OSD op; - - // Test different image sizes - std::vector> sizes = { - { 128, 128}, - { 256, 256}, - { 640, 480}, - {1920, 1080} - }; - - for (auto [w, h] : sizes) - { - nvcv::Tensor imgIn = nvcv::util::CreateTensor(1, w, h, nvcv::FMT_RGB8); - nvcv::Tensor imgOut = nvcv::util::CreateTensor(1, w, h, nvcv::FMT_RGB8); - - auto input = imgIn.exportData(); - auto output = imgOut.exportData(); - ASSERT_NE(input, nullptr); - ASSERT_NE(output, nullptr); - - // Initialize input for each size - auto inAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*input); - ASSERT_TRUE(inAccess); - long sampleStride = inAccess->numRows() * inAccess->rowStride(); - EXPECT_EQ(cudaSuccess, cudaMemset(input->basePtr(), 120, sampleStride)); - EXPECT_EQ(cudaSuccess, cudaMemset(output->basePtr(), 0, sampleStride)); - - std::vector>> elementVec; - std::vector> elements; - - NVCVBndBoxI bbox; - bbox.box.x = w / 4; - bbox.box.y = h / 4; - bbox.box.width = w / 2; - bbox.box.height = h / 2; - bbox.thickness = 2; - bbox.fillColor = {100, 150, 200, 50}; - bbox.borderColor = {255, 255, 0, 255}; - - auto element = std::make_shared(NVCVOSDType::NVCV_OSD_RECT, &bbox); - elements.push_back(element); - elementVec.push_back(elements); - - std::shared_ptr ctx = std::make_shared(elementVec); - - EXPECT_NO_THROW(op(stream, imgIn, imgOut, (NVCVElements)ctx.get())) << "Failed with size " << w << "x" << h; - EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); - } - - EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); -} diff --git a/tests/cvcuda/system/TestOpPadAndStack.cpp b/tests/cvcuda/system/TestOpPadAndStack.cpp index 544101ef9..3a61eb104 100644 --- a/tests/cvcuda/system/TestOpPadAndStack.cpp +++ b/tests/cvcuda/system/TestOpPadAndStack.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,6 +18,7 @@ #include "Definitions.hpp" #include +#include #include #include #include @@ -26,8 +27,75 @@ #include #include +#include namespace test = nvcv::test; +namespace util = nvcv::util; + +struct PadAndStackRefData +{ + std::vector &hDst; + const std::vector> &hBatchSrc; + int dstImgPitch; + int dstRowStride; + int dstPixPitch; + int srcRowStride; + int srcPixPitch; + int2 size; + const std::vector &topVec; + const std::vector &leftVec; + NVCVBorderType borderType; + float borderValue; +}; + +static void ApplyBorderIndex(int2 &coord, int2 size, NVCVBorderType borderType) +{ + if (borderType == NVCV_BORDER_REPLICATE) + { + test::ReplicateBorderIndex(coord, size); + } + else if (borderType == NVCV_BORDER_WRAP) + { + test::WrapBorderIndex(coord, size); + } + else if (borderType == NVCV_BORDER_REFLECT) + { + test::ReflectBorderIndex(coord, size); + } + else if (borderType == NVCV_BORDER_REFLECT101) + { + test::Reflect101BorderIndex(coord, size); + } +} + +static uint8_t PadAndStackValue(const PadAndStackRefData &ref, int db, int2 coord, int channel) +{ + const auto &hSrc = ref.hBatchSrc[db]; + + if (coord.x >= 0 && coord.x < ref.size.x && coord.y >= 0 && coord.y < ref.size.y) + { + return hSrc[coord.y * ref.srcRowStride + coord.x * ref.srcPixPitch + channel]; + } + + if (ref.borderType == NVCV_BORDER_CONSTANT) + { + return static_cast(ref.borderValue); + } + + ApplyBorderIndex(coord, ref.size, ref.borderType); + return hSrc[coord.y * ref.srcRowStride + coord.x * ref.srcPixPitch + channel]; +} + +static void WritePadAndStackPixel(const PadAndStackRefData &ref, int db, int di, int dj) +{ + int2 coord{dj - ref.leftVec[db], di - ref.topVec[db]}; + + for (int dk = 0; dk < ref.dstPixPitch; dk++) + { + ref.hDst[db * ref.dstImgPitch + di * ref.dstRowStride + dj * ref.dstPixPitch + dk] + = PadAndStackValue(ref, db, coord, dk); + } +} static void PadAndStack(std::vector &hDst, const std::vector> &hBatchSrc, const nvcv::TensorDataAccessStridedImagePlanar &dDstData, const int srcWidth, @@ -35,66 +103,143 @@ static void PadAndStack(std::vector &hDst, const std::vector &topVec, const std::vector &leftVec, const NVCVBorderType borderType, const float borderValue) { - int dstPixPitch = dDstData.numChannels(); - int dstRowStride = dDstData.rowStride() / sizeof(uint8_t); - int dstImgPitch = dDstData.sampleStride() / sizeof(uint8_t); + auto dstPixPitch = dDstData.numChannels(); + auto dstRowStride = static_cast(dDstData.rowStride() / sizeof(uint8_t)); + auto dstImgPitch = static_cast(dDstData.sampleStride() / sizeof(uint8_t)); - int2 coord, size{srcWidth, srcHeight}; + PadAndStackRefData ref{ + hDst, hBatchSrc, dstImgPitch, dstRowStride, dstPixPitch, srcRowStride, srcPixPitch, {srcWidth, srcHeight}, + topVec, leftVec, borderType, borderValue + }; for (int db = 0; db < dDstData.numSamples(); db++) { for (int di = 0; di < dDstData.numRows(); di++) { - coord.y = di - topVec[db]; - for (int dj = 0; dj < dDstData.numCols(); dj++) { - coord.x = dj - leftVec[db]; - - for (int dk = 0; dk < dDstData.numChannels(); dk++) - { - uint8_t out = 0; - - if (coord.x >= 0 && coord.x < size.x && coord.y >= 0 && coord.y < size.y) - { - out = hBatchSrc[db][coord.y * srcRowStride + coord.x * srcPixPitch + dk]; - } - else - { - if (borderType == NVCV_BORDER_CONSTANT) - { - out = static_cast(borderValue); - } - else - { - if (borderType == NVCV_BORDER_REPLICATE) - { - test::ReplicateBorderIndex(coord, size); - } - else if (borderType == NVCV_BORDER_WRAP) - { - test::WrapBorderIndex(coord, size); - } - else if (borderType == NVCV_BORDER_REFLECT) - { - test::ReflectBorderIndex(coord, size); - } - else if (borderType == NVCV_BORDER_REFLECT101) - { - test::Reflect101BorderIndex(coord, size); - } - - out = hBatchSrc[db][coord.y * srcRowStride + coord.x * srcPixPitch + dk]; - } - } - - hDst[db * dstImgPitch + di * dstRowStride + dj * dstPixPitch + dk] = out; - } + WritePadAndStackPixel(ref, db, di, dj); } } } } +template +static std::vector InterleavedToPlanar(const std::vector &hwc, int width, int height, int channels) +{ + std::vector chw(hwc.size()); + for (int y = 0; y < height; ++y) + { + for (int x = 0; x < width; ++x) + { + for (int c = 0; c < channels; ++c) + { + chw[(static_cast(c) * height + y) * width + x] + = hwc[(static_cast(y) * width + x) * channels + c]; + } + } + } + return chw; +} + +template +static std::vector MakePlanarParityInput(size_t count, int sample) +{ + std::vector values(count); + + size_t i = 0; + for (T &value : values) + { + value = static_cast((i * 7 + static_cast(sample) * 31 + 13) % 251); + ++i; + } + return values; +} + +template +static void UploadImage(const nvcv::Image &image, const std::vector &values, int width, int height, int channels, + cudaStream_t stream) +{ + auto data = image.exportData(); + ASSERT_NE(data, nvcv::NullOpt); + ASSERT_TRUE(data->numPlanes() == 1 || data->numPlanes() == channels); + + for (int plane = 0; plane < data->numPlanes(); ++plane) + { + const int planeChannels = data->numPlanes() == 1 ? channels : 1; + const size_t rowBytes = static_cast(width) * planeChannels * sizeof(T); + const auto *src = values.data() + (data->numPlanes() == 1 ? 0 : static_cast(plane) * width * height); + ASSERT_EQ(cudaSuccess, cudaMemcpy2DAsync(data->plane(plane).basePtr, data->plane(plane).rowStride, src, + rowBytes, rowBytes, height, cudaMemcpyHostToDevice, stream)); + } +} + +template +static void RunPadAndStackPlanarParity(int srcWidth, int srcHeight, int numBatches, int dstWidth, int dstHeight, + int topPad, int leftPad, NVCVBorderType borderType, float borderValue, + nvcv::ImageFormat interleavedFormat, nvcv::ImageFormat planarFormat) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + const int channels = interleavedFormat.numChannels(); + + nvcv::Tensor inTop(1, {numBatches, 1}, nvcv::FMT_S32); + nvcv::Tensor inLeft(1, {numBatches, 1}, nvcv::FMT_S32); + auto inTopData = inTop.exportData(); + auto inLeftData = inLeft.exportData(); + ASSERT_NE(nullptr, inTopData); + ASSERT_NE(nullptr, inLeftData); + + std::vector topVec(numBatches, topPad); + std::vector leftVec(numBatches, leftPad); + ASSERT_EQ(cudaSuccess, cudaMemcpyAsync(inTopData->basePtr(), topVec.data(), topVec.size() * sizeof(int), + cudaMemcpyHostToDevice, stream)); + ASSERT_EQ(cudaSuccess, cudaMemcpyAsync(inLeftData->basePtr(), leftVec.data(), leftVec.size() * sizeof(int), + cudaMemcpyHostToDevice, stream)); + + std::vector srcIImgs; + std::vector srcPImgs; + for (int b = 0; b < numBatches; ++b) + { + auto hwc = MakePlanarParityInput(static_cast(srcWidth) * srcHeight * channels, b); + srcIImgs.emplace_back(nvcv::Size2D{srcWidth, srcHeight}, interleavedFormat); + srcPImgs.emplace_back(nvcv::Size2D{srcWidth, srcHeight}, planarFormat); + UploadImage(srcIImgs.back(), hwc, srcWidth, srcHeight, channels, stream); + UploadImage(srcPImgs.back(), InterleavedToPlanar(hwc, srcWidth, srcHeight, channels), srcWidth, srcHeight, + channels, stream); + } + + nvcv::ImageBatchVarShape srcI(numBatches); + nvcv::ImageBatchVarShape srcP(numBatches); + srcI.pushBack(srcIImgs.begin(), srcIImgs.end()); + srcP.pushBack(srcPImgs.begin(), srcPImgs.end()); + + nvcv::Tensor dstI(numBatches, {dstWidth, dstHeight}, interleavedFormat); + nvcv::Tensor dstP(numBatches, {dstWidth, dstHeight}, planarFormat); + auto dstIData = dstI.exportData(); + auto dstPData = dstP.exportData(); + ASSERT_NE(nullptr, dstIData); + ASSERT_NE(nullptr, dstPData); + + cvcuda::PadAndStack op; + EXPECT_NO_THROW(op(stream, srcI, dstI, inTop, inLeft, borderType, borderValue)); + EXPECT_NO_THROW(op(stream, srcP, dstP, inTop, inLeft, borderType, borderValue)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + for (int sample = 0; sample < numBatches; ++sample) + { + SCOPED_TRACE(sample); + std::vector interleavedOut; + std::vector planarOut; + util::GetImageVectorFromTensor(*dstIData, sample, interleavedOut); + util::GetImageVectorFromTensor(*dstPData, sample, planarOut); + EXPECT_EQ(InterleavedToPlanar(interleavedOut, dstWidth, dstHeight, channels), planarOut); + } + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + // clang-format off NVCV_TEST_SUITE_P(OpPadAndStack, test::ValueList @@ -146,8 +291,8 @@ TEST_P(OpPadAndStack, correct_output) auto inLeftAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*inLeftData); ASSERT_TRUE(inLeftAccess); - int inTopBufSize = (inTopAccess->sampleStride() / sizeof(int)) * inTopAccess->numSamples(); - int inLeftBufSize = (inLeftAccess->sampleStride() / sizeof(int)) * inLeftAccess->numSamples(); + auto inTopBufSize = static_cast((inTopAccess->sampleStride() / sizeof(int)) * inTopAccess->numSamples()); + auto inLeftBufSize = static_cast((inLeftAccess->sampleStride() / sizeof(int)) * inLeftAccess->numSamples()); ASSERT_EQ(inTopBufSize, inLeftBufSize); @@ -172,7 +317,9 @@ TEST_P(OpPadAndStack, correct_output) std::default_random_engine randEng{0}; - int srcStride = 0, srcRowStride = 0, srcPixPitch = 0; + int srcStride = 0; + int srcRowStride = 0; + int srcPixPitch = 0; for (int b = 0; b < numBatches; ++b) { @@ -188,7 +335,7 @@ TEST_P(OpPadAndStack, correct_output) std::vector srcVec(srcBufSize); std::uniform_int_distribution srcRand{0u, 255u}; - std::generate(srcVec.begin(), srcVec.end(), [&]() { return srcRand(randEng); }); + std::ranges::generate(srcVec, [&srcRand, &randEng]() { return srcRand(randEng); }); // Copy each input image with random data to the GPU ASSERT_EQ(cudaSuccess, cudaMemcpyAsync(imgSrcData->plane(0).basePtr, srcVec.data(), @@ -210,7 +357,7 @@ TEST_P(OpPadAndStack, correct_output) auto dstAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*dstData); ASSERT_TRUE(dstData); - int dstBufSize = (dstAccess->sampleStride() / sizeof(uint8_t)) * dstAccess->numSamples(); + auto dstBufSize = static_cast((dstAccess->sampleStride() / sizeof(uint8_t)) * dstAccess->numSamples()); ASSERT_EQ(cudaSuccess, cudaMemsetAsync(dstData->basePtr(), 0, dstBufSize * sizeof(uint8_t), stream)); @@ -236,24 +383,73 @@ TEST_P(OpPadAndStack, correct_output) // clang-format off -NVCV_TEST_SUITE_P(OpPadAndStack_Negative, test::ValueList +NVCV_TEST_SUITE_P(OpPadAndStackPlanar, + test::ValueList { - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8, nvcv::FMT_S32, nvcv::FMT_S32, NVCV_BORDER_CONSTANT}, // input is not nhwc/hwc - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_RGBA8, nvcv::FMT_RGBA8p, nvcv::FMT_S32, nvcv::FMT_S32, NVCV_BORDER_CONSTANT}, // output is not nhwc/hwc -#ifndef ENABLE_SANITIZER - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_RGBA8, nvcv::FMT_RGBA8, nvcv::FMT_S32, nvcv::FMT_S32, static_cast(255)}, // invalid borderType -#endif - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_F16, nvcv::FMT_F16, nvcv::FMT_S32, nvcv::FMT_S32, NVCV_BORDER_CONSTANT}, // invalid datatype - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_F16, nvcv::FMT_S32, nvcv::FMT_S32, NVCV_BORDER_CONSTANT}, // invalid datatype - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_F16, nvcv::FMT_U8, nvcv::FMT_S32, nvcv::FMT_S32, NVCV_BORDER_CONSTANT}, // invalid datatype - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_RGBA8, nvcv::FMT_RGBA8, nvcv::FMT_F32, nvcv::FMT_S32, NVCV_BORDER_CONSTANT}, // invalid top datatype - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_RGBA8, nvcv::FMT_RGBA8, nvcv::FMT_S32, nvcv::FMT_F32, NVCV_BORDER_CONSTANT}, // invalid left datatype - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_RGBA8, nvcv::FMT_RGBA8, nvcv::FMT_RGBA8p, nvcv::FMT_S32, NVCV_BORDER_CONSTANT}, // invalid top datatype - {NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_RGBA8, nvcv::FMT_RGBA8, nvcv::FMT_S32, nvcv::FMT_RGBA8p, NVCV_BORDER_CONSTANT}, // invalid left datatype + // srcWidth, srcHeight, numBatches, dstWidth, dstHeight, topPad, leftPad, borderType, borderValue, interleavedFormat, planarFormat + { 17, 13, 2, 23, 21, 2, 3, NVCV_BORDER_CONSTANT, 11.f, nvcv::FMT_RGB8, nvcv::FMT_RGB8p}, + { 19, 11, 3, 27, 18, 1, 0, NVCV_BORDER_REFLECT101, 0.f, nvcv::FMT_RGBA8, nvcv::FMT_RGBA8p}, + { 15, 14, 2, 20, 19, 4, 2, NVCV_BORDER_REPLICATE, 0.f, nvcv::FMT_RGBf32, nvcv::FMT_RGBf32p}, }); // clang-format on +TEST_P(OpPadAndStackPlanar, varshape_matches_interleaved) +{ + int srcWidth = GetParamValue<0>(); + int srcHeight = GetParamValue<1>(); + int numBatches = GetParamValue<2>(); + int dstWidth = GetParamValue<3>(); + int dstHeight = GetParamValue<4>(); + int topPad = GetParamValue<5>(); + int leftPad = GetParamValue<6>(); + + NVCVBorderType borderType = GetParamValue<7>(); + float borderValue = GetParamValue<8>(); + nvcv::ImageFormat interleavedFormat = GetParamValue<9>(); + nvcv::ImageFormat planarFormat = GetParamValue<10>(); + + if (interleavedFormat == nvcv::FMT_RGBf32) + { + RunPadAndStackPlanarParity(srcWidth, srcHeight, numBatches, dstWidth, dstHeight, topPad, leftPad, + borderType, borderValue, interleavedFormat, planarFormat); + } + else + { + RunPadAndStackPlanarParity(srcWidth, srcHeight, numBatches, dstWidth, dstHeight, topPad, leftPad, + borderType, borderValue, interleavedFormat, planarFormat); + } +} + +static auto OpPadAndStackNegativeParams() +{ + test::ValueList + params{}; +#ifndef ENABLE_SANITIZER + params.emplace_back(NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_RGBA8, nvcv::FMT_RGBA8, nvcv::FMT_S32, nvcv::FMT_S32, + static_cast(255)); +#endif + params.emplace_back(NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_F16, nvcv::FMT_F16, nvcv::FMT_S32, nvcv::FMT_S32, + NVCV_BORDER_CONSTANT); + params.emplace_back(NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_U8, nvcv::FMT_F16, nvcv::FMT_S32, nvcv::FMT_S32, + NVCV_BORDER_CONSTANT); + params.emplace_back(NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_F16, nvcv::FMT_U8, nvcv::FMT_S32, nvcv::FMT_S32, + NVCV_BORDER_CONSTANT); + params.emplace_back(NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_RGBA8, nvcv::FMT_RGBA8, nvcv::FMT_F32, nvcv::FMT_S32, + NVCV_BORDER_CONSTANT); + params.emplace_back(NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_RGBA8, nvcv::FMT_RGBA8, nvcv::FMT_S32, nvcv::FMT_F32, + NVCV_BORDER_CONSTANT); + params.emplace_back(NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_RGBA8, nvcv::FMT_RGBA8, nvcv::FMT_RGBA8p, nvcv::FMT_S32, + NVCV_BORDER_CONSTANT); + params.emplace_back(NVCV_ERROR_INVALID_ARGUMENT, nvcv::FMT_RGBA8, nvcv::FMT_RGBA8, nvcv::FMT_S32, nvcv::FMT_RGBA8p, + NVCV_BORDER_CONSTANT); + return params; +} + +NVCV_TEST_SUITE_P(OpPadAndStack_Negative, OpPadAndStackNegativeParams()); + TEST_P(OpPadAndStack_Negative, op) { cudaStream_t stream; @@ -290,7 +486,8 @@ TEST_P(OpPadAndStack_Negative, op) EXPECT_EQ( expectedReturnCode, - nvcv::ProtectCall([&] { padAndStackOp(stream, imgBatchSrc, imgDst, inTop, inLeft, borderType, borderValue); })); + nvcv::ProtectCall([&padAndStackOp, &stream, &imgBatchSrc, &imgDst, &inTop, &inLeft, &borderType, &borderValue] + { padAndStackOp(stream, imgBatchSrc, imgDst, inTop, inLeft, borderType, borderValue); })); // Get test data back ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); @@ -325,10 +522,10 @@ TEST(OpPadAndStack_Negative, input_format_not_same) // Generate test result cvcuda::PadAndStack padAndStackOp; - EXPECT_EQ( - NVCV_ERROR_INVALID_ARGUMENT, - nvcv::ProtectCall( - [&] { padAndStackOp(stream, imgBatchSrc, imgDst, inTop, inLeft, NVCV_BORDER_CONSTANT, borderValue); })); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall( + [&padAndStackOp, &stream, &imgBatchSrc, &imgDst, &inTop, &inLeft, &borderValue] + { padAndStackOp(stream, imgBatchSrc, imgDst, inTop, inLeft, NVCV_BORDER_CONSTANT, borderValue); })); // Get test data back ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); diff --git a/tests/cvcuda/system/TestOpPairwiseMatcher.cpp b/tests/cvcuda/system/TestOpPairwiseMatcher.cpp index f5e8948c9..90bbc62d9 100644 --- a/tests/cvcuda/system/TestOpPairwiseMatcher.cpp +++ b/tests/cvcuda/system/TestOpPairwiseMatcher.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -83,16 +83,85 @@ void ComputeDistance(DT &dist, ST p1, ST p2, NVCVNormType normType) { if constexpr (!std::is_floating_point_v) { - dist += std::bitset(p1 ^ p2).count(); + dist = static_cast
(dist + static_cast
(std::bitset(p1 ^ p2).count())); } } else if (normType == NVCV_NORM_L1) { - dist += absdiff(p1, p2); + dist = static_cast
(dist + static_cast
(absdiff(p1, p2))); } else if (normType == NVCV_NORM_L2) { - dist += std::pow(absdiff(p1, p2), 2); + dist = static_cast
(dist + std::pow(absdiff(p1, p2), 2)); + } +} + +template +float DescriptorDistance(const RawBufferType &set1Vec, const RawBufferType &set2Vec, const long3 &set1Strides, + const long3 &set2Strides, int sampleIdx, int set1Idx, int set2Idx, int numDim, + NVCVNormType normType) +{ + float dist = 0.f; + + for (int coordIdx = 0; coordIdx < numDim; coordIdx++) + { + ST p1 = util::ValueAt(set1Vec, set1Strides, long3{sampleIdx, set1Idx, coordIdx}); + ST p2 = util::ValueAt(set2Vec, set2Strides, long3{sampleIdx, set2Idx, coordIdx}); + + ComputeDistance(dist, p1, p2, normType); + } + + return normType == NVCV_NORM_L2 ? std::sqrt(dist) : dist; +} + +template +void ComputeSortedDistances(std::vector> &distIdx, const RawBufferType &set1Vec, + const RawBufferType &set2Vec, const long3 &set1Strides, const long3 &set2Strides, + int sampleIdx, int set1Idx, int numDim, NVCVNormType normType) +{ + for (int set2Idx = 0; set2Idx < static_cast(distIdx.size()); set2Idx++) + { + distIdx[set2Idx] = std::make_tuple(DescriptorDistance(set1Vec, set2Vec, set1Strides, set2Strides, sampleIdx, + set1Idx, set2Idx, numDim, normType), + set2Idx); + } + + std::ranges::sort(distIdx); +} + +template +int FindCrossCheckMatch(std::vector> &cckDistIdx, const RawBufferType &set1Vec, + const RawBufferType &set2Vec, const long3 &set1Strides, const long3 &set2Strides, int sampleIdx, + int set2Idx, int numDim, NVCVNormType normType) +{ + for (int cck1Idx = 0; cck1Idx < static_cast(cckDistIdx.size()); cck1Idx++) + { + cckDistIdx[cck1Idx] = std::make_tuple(DescriptorDistance(set1Vec, set2Vec, set1Strides, set2Strides, + sampleIdx, cck1Idx, set2Idx, numDim, normType), + cck1Idx); + } + + std::ranges::sort(cckDistIdx); + return std::get<1>(cckDistIdx[0]); +} + +inline void StoreMatch(RawBufferType &mchVec, RawBufferType &nmVec, RawBufferType &dVec, const long3 &mchStrides, + const long1 &nmStrides, const long2 &dStrides, int sampleIdx, int &mchIdx, int set1Idx, + int set2Idx, float distance) +{ + util::ValueAt(mchVec, mchStrides, long3{sampleIdx, mchIdx, 0}) = set1Idx; + util::ValueAt(mchVec, mchStrides, long3{sampleIdx, mchIdx, 1}) = set2Idx; + + if (dStrides.x > 0) + { + util::ValueAt(dVec, dStrides, long2{sampleIdx, mchIdx}) = distance; + } + + mchIdx++; + + if (nmStrides.x > 0) + { + util::ValueAt(nmVec, nmStrides, long1{sampleIdx}) = mchIdx; } } @@ -106,91 +175,49 @@ void BruteForceMatcher(RawBufferType &mchVec, RawBufferType &nmVec, RawBufferTyp std::vector> distIdx(set2Size); std::vector> cckDistIdx(set1Size); - for (int sampleIdx = 0; sampleIdx < numSamples; sampleIdx++) + auto storeCrossCheckMatch + = [&cckDistIdx, &dStrides, &dVec, &mchStrides, &mchVec, &nmStrides, &nmVec, &set1Strides, &set1Vec, + &set2Strides, &set2Vec, numDim, normType](int sampleIdx, int &mchIdx, int set1Idx, + const std::vector> &sortedDistances) { - int mchIdx = 0; + int set2Idx = std::get<1>(sortedDistances[0]); - for (int set1Idx = 0; set1Idx < set1Size; set1Idx++) + if (FindCrossCheckMatch(cckDistIdx, set1Vec, set2Vec, set1Strides, set2Strides, sampleIdx, set2Idx, numDim, + normType) + == set1Idx) { - for (int set2Idx = 0; set2Idx < set2Size; set2Idx++) - { - float dist = 0.f; - - for (int coordIdx = 0; coordIdx < numDim; coordIdx++) - { - ST p1 = util::ValueAt(set1Vec, set1Strides, long3{sampleIdx, set1Idx, coordIdx}); - ST p2 = util::ValueAt(set2Vec, set2Strides, long3{sampleIdx, set2Idx, coordIdx}); + StoreMatch(mchVec, nmVec, dVec, mchStrides, nmStrides, dStrides, sampleIdx, mchIdx, set1Idx, set2Idx, + std::get<0>(sortedDistances[0])); + } + }; - ComputeDistance(dist, p1, p2, normType); - } - if (normType == NVCV_NORM_L2) - { - dist = std::sqrt(dist); - } + auto storeBestMatches + = [&dStrides, &dVec, &mchStrides, &mchVec, &nmStrides, &nmVec, matchesPerPoint]( + int sampleIdx, int &mchIdx, int set1Idx, const std::vector> &sortedDistances) + { + for (int m = 0; m < matchesPerPoint; m++) + { + StoreMatch(mchVec, nmVec, dVec, mchStrides, nmStrides, dStrides, sampleIdx, mchIdx, set1Idx, + std::get<1>(sortedDistances[m]), std::get<0>(sortedDistances[m])); + } + }; - distIdx[set2Idx] = std::tie(dist, set2Idx); - } + for (int sampleIdx = 0; sampleIdx < numSamples; sampleIdx++) + { + int mchIdx = 0; - std::sort(distIdx.begin(), distIdx.end()); + for (int set1Idx = 0; set1Idx < set1Size; set1Idx++) + { + ComputeSortedDistances(distIdx, set1Vec, set2Vec, set1Strides, set2Strides, sampleIdx, set1Idx, numDim, + normType); if (crossCheck) { - int set2Idx = std::get<1>(distIdx[0]); - - for (int cck1Idx = 0; cck1Idx < set1Size; cck1Idx++) - { - float dist = 0.f; - - for (int coordIdx = 0; coordIdx < numDim; coordIdx++) - { - ST p1 = util::ValueAt(set1Vec, set1Strides, long3{sampleIdx, cck1Idx, coordIdx}); - ST p2 = util::ValueAt(set2Vec, set2Strides, long3{sampleIdx, set2Idx, coordIdx}); - - ComputeDistance(dist, p1, p2, normType); - } - if (normType == NVCV_NORM_L2) - { - dist = std::sqrt(dist); - } - - cckDistIdx[cck1Idx] = std::tie(dist, cck1Idx); - } - - std::sort(cckDistIdx.begin(), cckDistIdx.end()); - - if (std::get<1>(cckDistIdx[0]) == set1Idx) - { - util::ValueAt(mchVec, mchStrides, long3{sampleIdx, mchIdx, 0}) = set1Idx; - util::ValueAt(mchVec, mchStrides, long3{sampleIdx, mchIdx, 1}) = std::get<1>(distIdx[0]); - if (dStrides.x > 0) - { - util::ValueAt(dVec, dStrides, long2{sampleIdx, mchIdx}) = std::get<0>(distIdx[0]); - } - - mchIdx++; - if (nmStrides.x > 0) - { - util::ValueAt(nmVec, nmStrides, long1{sampleIdx}) = mchIdx; - } - } + storeCrossCheckMatch(sampleIdx, mchIdx, set1Idx, distIdx); } else { - for (int m = 0; m < matchesPerPoint; m++) - { - util::ValueAt(mchVec, mchStrides, long3{sampleIdx, mchIdx, 0}) = set1Idx; - util::ValueAt(mchVec, mchStrides, long3{sampleIdx, mchIdx, 1}) = std::get<1>(distIdx[m]); - if (dStrides.x > 0) - { - util::ValueAt(dVec, dStrides, long2{sampleIdx, mchIdx}) = std::get<0>(distIdx[m]); - } - - mchIdx++; - if (nmStrides.x > 0) - { - util::ValueAt(nmVec, nmStrides, long1{sampleIdx}) = mchIdx; - } - } + storeBestMatches(sampleIdx, mchIdx, set1Idx, distIdx); } } } @@ -235,7 +262,7 @@ inline void SortOutput(std::vector> &outIdsDist } } - std::sort(outIdsDist.begin(), outIdsDist.end()); + std::ranges::sort(outIdsDist); } } // namespace ref @@ -253,6 +280,7 @@ inline void SortOutput(std::vector> &outIdsDist NVCV_TYPED_TEST_SUITE(OpPairwiseMatcher, type::Types< NVCV_TEST_ROW(1, 2, 2, 1, 1, false, false, NVCV_BRUTE_FORCE, NVCV_NORM_HAMMING, uint8_t), NVCV_TEST_ROW(2, 3, 4, 5, 1, false, true, NVCV_BRUTE_FORCE, NVCV_NORM_HAMMING, uint8_t), + NVCV_TEST_ROW(2, 13, 14, 32, 1, true, true, NVCV_BRUTE_FORCE, NVCV_NORM_HAMMING, uint8_t), NVCV_TEST_ROW(3, 4, 3, 32, 1, false, true, NVCV_BRUTE_FORCE, NVCV_NORM_HAMMING, uint32_t), NVCV_TEST_ROW(4, 11, 12, 128, 2, false, true, NVCV_BRUTE_FORCE, NVCV_NORM_HAMMING, uint8_t), NVCV_TEST_ROW(3, 17, 16, 128, 3, false, true, NVCV_BRUTE_FORCE, NVCV_NORM_HAMMING, uint8_t), @@ -347,8 +375,8 @@ TYPED_TEST(OpPairwiseMatcher, CorrectOutput) long1 ns1Strides{ns1Data->stride(0)}; long1 ns2Strides{ns2Data->stride(0)}; long3 mchStrides{mchData->stride(0), mchData->stride(1), mchData->stride(2)}; - long1 nmStrides = (numMatches) ? long1{nmData->stride(0)} : long1{0}; - long2 dStrides = (distances) ? long2{dData->stride(0), dData->stride(1)} : long2{0, 0}; + long1 nmStrides = numMatches ? long1{nmData->stride(0)} : long1{0}; + long2 dStrides = distances ? long2{dData->stride(0), dData->stride(1)} : long2{0, 0}; long set1BufSize = set1Strides.x * numSamples; long set2BufSize = set2Strides.x * numSamples; @@ -438,10 +466,10 @@ TYPED_TEST(OpPairwiseMatcher, CorrectOutput) EXPECT_EQ(testIdsDist, goldIdsDist); } -static void pairwiseMatcherNegative(nvcv::Tensor &set1, nvcv::Tensor &set2, nvcv::Tensor &numSet1, - nvcv::Tensor &numSet2, nvcv::Tensor &matches, nvcv::Tensor &numMatches, - nvcv::Tensor &distances, bool crossCheck, int matchesPerPoint, - NVCVNormType normType, NVCVPairwiseMatcherType algoChoice) +static void pairwiseMatcherNegative(const nvcv::Tensor &set1, const nvcv::Tensor &set2, const nvcv::Tensor &numSet1, + const nvcv::Tensor &numSet2, const nvcv::Tensor &matches, + const nvcv::Tensor &numMatches, const nvcv::Tensor &distances, bool crossCheck, + int matchesPerPoint, NVCVNormType normType, NVCVPairwiseMatcherType algoChoice) { cudaStream_t stream; ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); @@ -449,7 +477,8 @@ static void pairwiseMatcherNegative(nvcv::Tensor &set1, nvcv::Tensor &set2, nvcv cvcuda::PairwiseMatcher op(algoChoice); EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall( - [&] { + [&op, &stream, &set1, &set2, &numSet1, &numSet2, &matches, &numMatches, + &distances, &crossCheck, &matchesPerPoint, &normType] { op(stream, set1, set2, numSet1, numSet2, matches, numMatches, distances, crossCheck, matchesPerPoint, normType); })); diff --git a/tests/cvcuda/system/TestOpPillowResize.cpp b/tests/cvcuda/system/TestOpPillowResize.cpp index 58c14f556..4c1759580 100644 --- a/tests/cvcuda/system/TestOpPillowResize.cpp +++ b/tests/cvcuda/system/TestOpPillowResize.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,18 +16,30 @@ */ #include "Definitions.hpp" +#include "PlanarParityUtils.hpp" #include #include +#include #include #include #include #include #include +#include +#include #include +#include +#include #include +#include #include +#include +#include +#include +#include +#include namespace test = nvcv::test; namespace t = ::testing; @@ -35,11 +47,11 @@ namespace t = ::testing; using Vecf = std::vector; using uchar = unsigned char; -#include -#include -#include -#include -#include +class PillowResizeTestError : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; template class TestMat @@ -49,69 +61,71 @@ class TestMat : rows(rows_) , cols(cols_) , channels(channels_) + , data(static_cast(rows_) * static_cast(cols_) * static_cast(channels_)) , dkind(dkind_) { - data = std::vector(); - data.resize(rows * cols * channels); } - TestMat(int rows_, int cols_, int channels_, nvcv::DataKind dkind_, std::vector &data_) + TestMat(int rows_, int cols_, int channels_, nvcv::DataKind dkind_, const std::vector &data_) : rows(rows_) , cols(cols_) , channels(channels_) + , data(data_) , dkind(dkind_) { - data = std::vector(); - data = data_; } TestMat(const TestMat &test_mat, NVCVRectI roi) + : rows(roi.height) + , cols(roi.width) + , channels(test_mat.channels) + , dkind(test_mat.dkind) { - rows = roi.height; - cols = roi.width; - channels = test_mat.channels; - dkind = test_mat.dkind; if (roi.height == test_mat.rows && roi.width == test_mat.cols) { - data = std::vector(); data = test_mat.data; } else { - data = std::vector(); - data.resize(roi.width * roi.height * test_mat.channels); + data.resize(static_cast(roi.width) * static_cast(roi.height) + * static_cast(test_mat.channels)); + + auto copyPixel = [this, &test_mat, roi](int row, int col) + { + for (int c = 0; c < channels; c++) + { + data[row * cols * channels + col * channels + c] + = test_mat.data[(row + roi.y) * test_mat.cols * channels + (col + roi.x) * channels + c]; + } + }; + for (int i = 0; i < rows; i++) { for (int j = 0; j < cols; j++) { - for (int c = 0; c < channels; c++) - { - data[i * cols * channels + j * channels + c] - = test_mat.data[(i + roi.y) * test_mat.cols * channels + (j + roi.x) * channels + c]; - } + copyPixel(i, j); } } } } - TestMat(nvcv::DataKind dkind_) - : dkind(dkind_) + explicit TestMat(nvcv::DataKind dkind_) + : rows(0) + , cols(0) + , channels(0) + , data() + , dkind(dkind_) { - rows = 0; - cols = 0; - channels = 0; - data = std::vector(); } - bool empty() + bool empty() const { return data.empty(); } void create(int rows_, int cols_, int ch_) { - data = std::vector(); - data.resize(rows_ * cols_ * ch_); + data.assign(static_cast(rows_) * static_cast(cols_) * static_cast(ch_), T{}); rows = rows_; cols = cols_; channels = ch_; @@ -153,7 +167,7 @@ class TestMat std::cout << "i,j = " << i << "," << j; for (int c = 0; c < channels; c++) { - std::cout << " " << (int)get(i, j, c); + std::cout << " " << static_cast(get(i, j, c)); } std::cout << std::endl; } @@ -167,6 +181,23 @@ class TestMat nvcv::DataKind dkind; }; +static int ScaleDimension(int value, double scale) +{ + return static_cast(static_cast(value) * scale); +} + +template +static void FillRandomBytes(std::vector &values) +{ + std::default_random_engine randEng{0}; + std::uniform_int_distribution srcRand{0, 255}; + + for (T &value : values) + { + value = static_cast(srcRand(randEng)); + } +} + struct Rect2f { float x; //!< x coordinate of the top-left corner @@ -196,6 +227,8 @@ class PillowResizeCPU double _support; /** Support size (length of resampling filter). */ public: + virtual ~Filter() = default; + /** * \brief Construct a new Filter object. * @@ -231,6 +264,7 @@ class PillowResizeCPU public: BoxFilter() : Filter(box_filter_support){}; + ~BoxFilter() override = default; [[nodiscard]] double filter(double x) const override; }; @@ -241,6 +275,7 @@ class PillowResizeCPU public: BilinearFilter() : Filter(bilinear_filter_support){}; + ~BilinearFilter() override = default; [[nodiscard]] double filter(double x) const override; }; @@ -251,6 +286,7 @@ class PillowResizeCPU public: HammingFilter() : Filter(hamming_filter_support){}; + ~HammingFilter() override = default; [[nodiscard]] double filter(double x) const override; }; @@ -261,6 +297,7 @@ class PillowResizeCPU public: BicubicFilter() : Filter(bicubic_filter_support){}; + ~BicubicFilter() override = default; [[nodiscard]] double filter(double x) const override; }; @@ -274,6 +311,7 @@ class PillowResizeCPU public: LanczosFilter() : Filter(lanczos_filter_support){}; + ~LanczosFilter() override = default; [[nodiscard]] double filter(double x) const override; }; @@ -501,7 +539,7 @@ class PillowResizeCPU * * \return Resized matrix. * - * \throw std::runtime_error In case the box is invalid, the interpolation filter + * \throw PillowResizeTestError In case the box is invalid, the interpolation filter * or the input matrix type are not supported. */ template @@ -517,7 +555,7 @@ class PillowResizeCPU * * \return Resized matrix. * - * \throw std::runtime_error In case the box is invalid, the interpolation filter + * \throw PillowResizeTestError In case the box is invalid, the interpolation filter * or the input matrix type are not supported. */ template @@ -558,21 +596,28 @@ void PillowResizeCPU::_resampleHorizontal(TestMat &im_out, const TestMat & kk = preprocessCoefficients(kk); } + auto resampleChannel = [&im_in, &kk, offset, ksize, &bounds, init_buffer](int yy, int xx, int c) + { + int xmin = bounds[xx * 2 + 0]; + int xmax = bounds[xx * 2 + 1]; + const double *k = &kk[xx * ksize]; + double ss = init_buffer; + + for (int x = 0; x < xmax; ++x) + { + // NOLINTNEXTLINE + ss += (T)im_in.get(yy + offset, x + xmin, c) * k[x]; + } + return ss; + }; + for (int yy = 0; yy < im_out.rows; ++yy) { for (int xx = 0; xx < im_out.cols; ++xx) { - int xmin = bounds[xx * 2 + 0]; - int xmax = bounds[xx * 2 + 1]; - double *k = &kk[xx * ksize]; for (int c = 0; c < im_in.channels; ++c) { - double ss = init_buffer; - for (int x = 0; x < xmax; ++x) - { - // NOLINTNEXTLINE - ss += (T)im_in.get(yy + offset, x + xmin, c) * k[x]; - } + double ss = resampleChannel(yy, xx, c); // NOLINTNEXTLINE im_out.set(yy, xx, c, (T)(outMap == nullptr ? ss : outMap(ss))); } @@ -595,8 +640,7 @@ double PillowResizeCPU::BilinearFilter::filter(double x) const double PillowResizeCPU::BoxFilter::filter(double x) const { - const double half_pixel = 0.5; - if (x > -half_pixel && x <= half_pixel) + if (const double half_pixel = 0.5; x > -half_pixel && x <= half_pixel) { return 1.0; } @@ -651,8 +695,7 @@ double PillowResizeCPU::LanczosFilter::_sincFilter(double x) double PillowResizeCPU::LanczosFilter::filter(double x) const { - const double lanczos_a_param = 3.0; - if (-lanczos_a_param <= x && x < lanczos_a_param) + if (const double lanczos_a_param = 3.0; - lanczos_a_param <= x && x < lanczos_a_param) { return _sincFilter(x) * _sincFilter(x / lanczos_a_param); } @@ -666,7 +709,7 @@ int PillowResizeCPU::_precomputeCoeffs(int in_size, double in0, double in1, int // Prepare for horizontal stretch. double scale = 0; double filterscale = 0; - filterscale = scale = static_cast(in1 - in0) / out_size; + filterscale = scale = (in1 - in0) / out_size; if (filterscale < 1.0) { filterscale = 1.0; @@ -681,7 +724,7 @@ int PillowResizeCPU::_precomputeCoeffs(int in_size, double in0, double in1, int // Check for overflow if (out_size > INT_MAX / (k_size * static_cast(sizeof(double)))) { - throw std::runtime_error("Memory error"); + throw PillowResizeTestError("Memory error"); } // Coefficient buffer. @@ -764,7 +807,7 @@ std::vector PillowResizeCPU::_normalizeCoeffs8bpc(const std::vector TestMat PillowResizeCPU::resize(const TestMat &src, const nvcv::Size2D &out_size, int filter) { - Rect2f box(0.F, 0.F, static_cast(src.cols), static_cast(src.rows)); + Rect2f box{0.F, 0.F, static_cast(src.cols), static_cast(src.rows)}; return resize(src, out_size, filter, box); } @@ -777,29 +820,29 @@ TestMat PillowResizeCPU::resize(const TestMat &src, const nvcv::Size2D &ou int y_size = out_size.h; if (x_size < 1 || y_size < 1) { - throw std::runtime_error("Height and width must be > 0"); + throw PillowResizeTestError("Height and width must be > 0"); } if (rect[0] < 0.F || rect[1] < 0.F) { - throw std::runtime_error("Box offset can't be negative"); + throw PillowResizeTestError("Box offset can't be negative"); } if (static_cast(rect[2]) > src.cols || static_cast(rect[3]) > src.rows) { - throw std::runtime_error("Box can't exceed original image size"); + throw PillowResizeTestError("Box can't exceed original image size"); } if (box.width < 0 || box.height < 0) { - throw std::runtime_error("Box can't be empty"); + throw PillowResizeTestError("Box can't be empty"); } // If box's coordinates are int and box size matches requested size if (static_cast(box.width) == x_size && static_cast(box.height) == y_size) { - NVCVRectI roi(static_cast(box.x), static_cast(box.y), static_cast(box.width), - static_cast(box.height)); + NVCVRectI roi{static_cast(box.x), static_cast(box.y), static_cast(box.width), + static_cast(box.height)}; return TestMat(src, roi); } @@ -824,7 +867,7 @@ TestMat PillowResizeCPU::resize(const TestMat &src, const nvcv::Size2D &ou filter_p = std::make_shared(LanczosFilter()); break; default: - throw std::runtime_error("unsupported resampling filter"); + throw PillowResizeTestError("unsupported resampling filter"); } return PillowResizeCPU::_resample(src, x_size, y_size, filter_p, rect); @@ -921,7 +964,7 @@ void PillowResizeCPU::_resampleHorizontal(TestMat &im_out, const TestMat & case nvcv::DataKind::FLOAT: return _resampleHorizontal(im_out, im_in, offset, ksize, bounds, prekk); default: - throw std::runtime_error("Pixel kind not supported"); + throw PillowResizeTestError("Pixel kind not supported"); } } @@ -944,6 +987,7 @@ NVCV_TEST_SUITE_P(OpPillowResize, test::ValueList> srcVec(numberOfImages); int srcVecRowStride = srcWidth * fmt.planePixelStrideBytes(0); int num_channels = fmt.numChannels(); - nvcv::DataKind dkind = nvcv::DataKind::UNSIGNED; - std::default_random_engine randEng; + nvcv::DataKind dkind = std::is_same_v ? nvcv::DataKind::FLOAT : nvcv::DataKind::UNSIGNED; for (int i = 0; i < numberOfImages; ++i) { srcVec[i].resize(srcHeight * srcWidth * num_channels); - std::default_random_engine randEng{0}; - std::uniform_int_distribution srcRand{0u, 255u}; - if (std::is_same::value) - { - std::generate(srcVec[i].begin(), srcVec[i].end(), [&]() { return srcRand(randEng); }); - dkind = nvcv::DataKind::FLOAT; - } - else - { - std::generate(srcVec[i].begin(), srcVec[i].end(), [&]() { return srcRand(randEng); }); - dkind = nvcv::DataKind::UNSIGNED; - } + FillRandomBytes(srcVec[i]); // Copy input data to the GPU ASSERT_EQ(cudaSuccess, @@ -1050,10 +1082,10 @@ void StartTest(int srcWidth, int srcHeight, int dstWidth, int dstHeight, NVCVInt int maeThreshold = 2; int count = 0; std::vector mae(testVec.size()); - for (size_t i = 0; i < mae.size(); ++i) + for (size_t idx = 0; idx < mae.size(); ++idx) { - mae[i] = abs(static_cast((test_out.data)[i]) - static_cast(testVec[i])); - if (mae[i] > maeThreshold) + mae[idx] = abs(static_cast((test_out.data)[idx]) - static_cast(testVec[idx])); + if (mae[idx] > maeThreshold) count++; } @@ -1075,7 +1107,7 @@ TEST_P(OpPillowResize, tensor_correct_output) nvcv::ImageFormat fmt = GetParamValue<6>(); if (nvcv::FMT_RGB8 == fmt || nvcv::FMT_RGBA8 == fmt) StartTest(srcWidth, srcHeight, dstWidth, dstHeight, interpolation, numberOfImages, fmt); - else if (nvcv::FMT_RGBf32 == fmt || nvcv::FMT_RGBAf32 == fmt) + else if (nvcv::FMT_RGBf32 == fmt || nvcv::FMT_RGBAf32 == fmt || nvcv::FMT_2F32 == fmt) StartTest(srcWidth, srcHeight, dstWidth, dstHeight, interpolation, numberOfImages, fmt); else if (nvcv::FMT_S16 == fmt) StartTest(srcWidth, srcHeight, dstWidth, dstHeight, interpolation, numberOfImages, fmt); @@ -1091,15 +1123,17 @@ void StartVarShapeTest(int srcWidthBase, int srcHeightBase, int dstWidthBase, in EXPECT_EQ(cudaSuccess, cudaStreamCreate(&stream)); // Create input and output - std::default_random_engine randEng; - std::uniform_int_distribution rndSrcWidth(srcWidthBase * 0.8, srcWidthBase * 1.1); - std::uniform_int_distribution rndSrcHeight(srcHeightBase * 0.8, srcHeightBase * 1.1); + std::default_random_engine randEng; + std::uniform_int_distribution rndSrcWidth(ScaleDimension(srcWidthBase, 0.8), ScaleDimension(srcWidthBase, 1.1)); + std::uniform_int_distribution rndSrcHeight(ScaleDimension(srcHeightBase, 0.8), ScaleDimension(srcHeightBase, 1.1)); - std::uniform_int_distribution rndDstWidth(dstWidthBase * 0.8, dstWidthBase * 1.1); - std::uniform_int_distribution rndDstHeight(dstHeightBase * 0.8, dstHeightBase * 1.1); + std::uniform_int_distribution rndDstWidth(ScaleDimension(dstWidthBase, 0.8), ScaleDimension(dstWidthBase, 1.1)); + std::uniform_int_distribution rndDstHeight(ScaleDimension(dstHeightBase, 0.8), ScaleDimension(dstHeightBase, 1.1)); - std::vector imgSrc, imgDst; - std::vector srcSizes, dstSizes; + std::vector imgSrc; + std::vector imgDst; + std::vector srcSizes; + std::vector dstSizes; for (int i = 0; i < numberOfImages; ++i) { if (i == 0) @@ -1125,7 +1159,7 @@ void StartVarShapeTest(int srcWidthBase, int srcHeightBase, int dstWidthBase, in std::vector> srcVec(numberOfImages); std::vector srcVecRowStride(numberOfImages); int num_channels = fmt.numChannels(); - nvcv::DataKind dkind = nvcv::DataKind::UNSIGNED; + nvcv::DataKind dkind = std::is_same_v ? nvcv::DataKind::FLOAT : nvcv::DataKind::UNSIGNED; // Populate input for (int i = 0; i < numberOfImages; ++i) { @@ -1139,20 +1173,8 @@ void StartVarShapeTest(int srcWidthBase, int srcHeightBase, int dstWidthBase, in srcVecRowStride[i] = srcRowStride; - std::default_random_engine randEng{0}; - std::uniform_int_distribution srcRand{0u, 255u}; - srcVec[i].resize(srcHeight * srcWidth * num_channels); - if (std::is_same::value) - { - std::generate(srcVec[i].begin(), srcVec[i].end(), [&]() { return srcRand(randEng); }); - dkind = nvcv::DataKind::FLOAT; - } - else - { - std::generate(srcVec[i].begin(), srcVec[i].end(), [&]() { return srcRand(randEng); }); - dkind = nvcv::DataKind::UNSIGNED; - } + FillRandomBytes(srcVec[i]); // Copy input data to the GPU ASSERT_EQ(cudaSuccess, cudaMemcpy2D(srcData->plane(0).basePtr, srcData->plane(0).rowStride, srcVec[i].data(), srcRowStride, @@ -1205,10 +1227,10 @@ void StartVarShapeTest(int srcWidthBase, int srcHeightBase, int dstWidthBase, in int maeThreshold = 2; int count = 0; std::vector mae(testVec.size()); - for (size_t i = 0; i < mae.size(); ++i) + for (size_t idx = 0; idx < mae.size(); ++idx) { - mae[i] = abs(static_cast((test_out.data)[i]) - static_cast(testVec[i])); - if (mae[i] > maeThreshold) + mae[idx] = abs(static_cast((test_out.data)[idx]) - static_cast(testVec[idx])); + if (mae[idx] > maeThreshold) count++; } @@ -1230,7 +1252,7 @@ TEST_P(OpPillowResize, varshape_correct_output) nvcv::ImageFormat fmt = GetParamValue<6>(); if (nvcv::FMT_RGB8 == fmt || nvcv::FMT_RGBA8 == fmt) StartVarShapeTest(srcWidth, srcHeight, dstWidth, dstHeight, interpolation, numberOfImages, fmt); - else if (nvcv::FMT_RGBf32 == fmt || nvcv::FMT_RGBAf32 == fmt) + else if (nvcv::FMT_RGBf32 == fmt || nvcv::FMT_RGBAf32 == fmt || nvcv::FMT_2F32 == fmt) StartVarShapeTest(srcWidth, srcHeight, dstWidth, dstHeight, interpolation, numberOfImages, fmt); else if (nvcv::FMT_S16 == fmt) StartVarShapeTest(srcWidth, srcHeight, dstWidth, dstHeight, interpolation, numberOfImages, fmt); @@ -1238,17 +1260,106 @@ TEST_P(OpPillowResize, varshape_correct_output) StartVarShapeTest(srcWidth, srcHeight, dstWidth, dstHeight, interpolation, numberOfImages, fmt); } -// clang-format off -NVCV_TEST_SUITE_P(OpPillowResize_Negative, test::ValueList{ - {nvcv::FMT_RGB8p, nvcv::FMT_RGB8, NVCV_INTERP_LINEAR}, - {nvcv::FMT_RGB8p, nvcv::FMT_RGB8p, NVCV_INTERP_LINEAR}, - {nvcv::FMT_F64, nvcv::FMT_F64, NVCV_INTERP_LINEAR}, +// ============================================================================= +// Planar (NCHW/CHW) layout support +// +// PillowResize resizes each channel independently, so a planar input is resized plane-by-plane and +// must produce exactly the same pixels as the interleaved path. These tests feed identical data in +// both layouts through cvcuda::PillowResize and require the (re-interleaved) planar output to match +// the interleaved output bit-for-bit, for every supported interpolation mode and dtype. +// ============================================================================= + +namespace { + +const nvcv::ImageFormat FMT_RGBS16{nvcv::ColorModel::RGB, nvcv::CSPEC_UNDEFINED, nvcv::MemLayout::PITCH_LINEAR, + nvcv::DataKind::SIGNED, nvcv::Swizzle::S_XYZ1, nvcv::Packing::X16_Y16_Z16}; +const nvcv::ImageFormat FMT_RGBS16p{nvcv::ColorModel::RGB, nvcv::CSPEC_UNDEFINED, nvcv::MemLayout::PITCH_LINEAR, + nvcv::DataKind::SIGNED, nvcv::Swizzle::S_XYZ0, nvcv::Packing::X16, + nvcv::Packing::X16, nvcv::Packing::X16}; + +// Resize identical data in interleaved and planar tensor layout; outputs must match bit-for-bit. +// The shared scaffolding (upload/run/download/compare) lives in PlanarParityUtils.hpp; here we only +// bind the PillowResize call, which needs a per-format workspace. +void RunPlanarParityTensorCase(nvcv::ImageFormat planarFmt, nvcv::ImageFormat interleavedFmt, int srcW, int srcH, + int dstW, int dstH, NVCVInterpolationType interp, int numImages) +{ + test::planar::RunTensorParity( + planarFmt, interleavedFmt, srcW, srcH, dstW, dstH, numImages, + [numImages, srcW, srcH, dstW, dstH, interp](cudaStream_t s, const nvcv::Tensor &src, const nvcv::Tensor &dst, + nvcv::ImageFormat fmt) + { + cvcuda::PillowResize op; + cvcuda::UniqueWorkspace ws + = cvcuda::AllocateWorkspace(op.getWorkspaceRequirements(numImages, {srcW, srcH}, {dstW, dstH}, fmt)); + EXPECT_NO_THROW(op(s, ws.get(), src, dst, interp)); + }); +} + +// Var-shape counterpart of RunPlanarParityTensorCase. +void RunPlanarParityVarShapeCase(nvcv::ImageFormat planarFmt, nvcv::ImageFormat interleavedFmt, int srcW, int srcH, + int dstW, int dstH, NVCVInterpolationType interp, int numImages) +{ + std::vector srcSizes(numImages, {srcW, srcH}); + std::vector dstSizes(numImages, {dstW, dstH}); + test::planar::RunVarShapeParity( + planarFmt, interleavedFmt, srcW, srcH, dstW, dstH, numImages, + [numImages, &srcSizes, &dstSizes, interp](cudaStream_t s, const nvcv::ImageBatchVarShape &src, + const nvcv::ImageBatchVarShape &dst, nvcv::ImageFormat fmt) + { + cvcuda::PillowResize op; + cvcuda::UniqueWorkspace ws = cvcuda::AllocateWorkspace( + op.getWorkspaceRequirements(numImages, srcSizes.data(), dstSizes.data(), fmt)); + EXPECT_NO_THROW(op(s, ws.get(), src, dst, interp)); + }); +} + +} // namespace + +// Parameters: srcW, srcH, dstW, dstH, interpolation, numImages, planarFmt, interleavedFmt +NVCV_TEST_SUITE_P(OpPillowResizePlanar, + test::ValueList{ + // RGB8 (3 channel uint8): every supported interpolation, expand and contract. + { 64, 48, 128, 96, NVCV_INTERP_LINEAR, 2, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + {128, 96, 64, 48, NVCV_INTERP_CUBIC, 2, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + { 64, 48, 100, 72, NVCV_INTERP_BOX, 1, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + {100, 72, 40, 30, NVCV_INTERP_HAMMING, 1, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + { 50, 40, 100, 80, NVCV_INTERP_LANCZOS, 1, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + // RGBA8 (4 channel uint8). + { 50, 40, 100, 80, NVCV_INTERP_LINEAR, 2, nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8}, + {100, 80, 50, 40, NVCV_INTERP_CUBIC, 1, nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8}, + // Float planar (3 and 4 channel). + { 64, 48, 96, 72, NVCV_INTERP_LINEAR, 2, nvcv::FMT_RGBf32p, nvcv::FMT_RGBf32}, + { 96, 72, 48, 36, NVCV_INTERP_CUBIC, 1, nvcv::FMT_RGBAf32p, nvcv::FMT_RGBAf32}, + // Signed 16-bit exercises Pillow's round-to-nearest output path. + { 72, 54, 45, 35, NVCV_INTERP_LINEAR, 2, FMT_RGBS16p, FMT_RGBS16}, +}); + +TEST_P(OpPillowResizePlanar, tensor_matches_interleaved) +{ + RunPlanarParityTensorCase(GetParamValue<6>(), GetParamValue<7>(), GetParamValue<0>(), GetParamValue<1>(), + GetParamValue<2>(), GetParamValue<3>(), GetParamValue<4>(), GetParamValue<5>()); +} + +TEST_P(OpPillowResizePlanar, varshape_matches_interleaved) +{ + RunPlanarParityVarShapeCase(GetParamValue<6>(), GetParamValue<7>(), GetParamValue<0>(), GetParamValue<1>(), + GetParamValue<2>(), GetParamValue<3>(), GetParamValue<4>(), GetParamValue<5>()); +} + +static auto OpPillowResizeNegativeParams() +{ + test::ValueList params{ + {nvcv::FMT_RGB8p, nvcv::FMT_RGB8, NVCV_INTERP_LINEAR}, // planar in, interleaved out: layout mismatch + { nvcv::FMT_F64, nvcv::FMT_F64, NVCV_INTERP_LINEAR}, + { nvcv::FMT_RGB8, nvcv::FMT_RGB8, NVCV_INTERP_NEAREST}, + }; #ifndef ENABLE_SANITIZER - {nvcv::FMT_RGB8, nvcv::FMT_RGB8, static_cast(255)}, + params.emplace_back(nvcv::FMT_RGB8, nvcv::FMT_RGB8, static_cast(255)); #endif -}); + return params; +} -// clang-format on +NVCV_TEST_SUITE_P(OpPillowResize_Negative, OpPillowResizeNegativeParams()); TEST_P(OpPillowResize_Negative, op) { @@ -1271,7 +1382,8 @@ TEST_P(OpPillowResize_Negative, op) pillowResizeOp.getWorkspaceRequirements(numberOfImages, {24, 24}, {12, 12}, inputFmt)); EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, - nvcv::ProtectCall([&] { pillowResizeOp(stream, ws.get(), imgSrc, imgDst, interpolation); })); + nvcv::ProtectCall([&pillowResizeOp, &stream, &ws, &imgSrc, &imgDst, &interpolation] + { pillowResizeOp(stream, ws.get(), imgSrc, imgDst, interpolation); })); EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); @@ -1293,15 +1405,17 @@ TEST_P(OpPillowResize_Negative, varshape_op) int dstHeightBase = 8; // Create input and output - std::default_random_engine randEng; - std::uniform_int_distribution rndSrcWidth(srcWidthBase * 0.8, srcWidthBase * 1.1); - std::uniform_int_distribution rndSrcHeight(srcHeightBase * 0.8, srcHeightBase * 1.1); + std::default_random_engine randEng; + std::uniform_int_distribution rndSrcWidth(ScaleDimension(srcWidthBase, 0.8), ScaleDimension(srcWidthBase, 1.1)); + std::uniform_int_distribution rndSrcHeight(ScaleDimension(srcHeightBase, 0.8), ScaleDimension(srcHeightBase, 1.1)); - std::uniform_int_distribution rndDstWidth(dstWidthBase * 0.8, dstWidthBase * 1.1); - std::uniform_int_distribution rndDstHeight(dstHeightBase * 0.8, dstHeightBase * 1.1); + std::uniform_int_distribution rndDstWidth(ScaleDimension(dstWidthBase, 0.8), ScaleDimension(dstWidthBase, 1.1)); + std::uniform_int_distribution rndDstHeight(ScaleDimension(dstHeightBase, 0.8), ScaleDimension(dstHeightBase, 1.1)); - std::vector imgSrc, imgDst; - std::vector srcSizes, dstSizes; + std::vector imgSrc; + std::vector imgDst; + std::vector srcSizes; + std::vector dstSizes; for (int i = 0; i < numberOfImages; ++i) { imgSrc.emplace_back(nvcv::Size2D{rndSrcWidth(randEng), rndSrcHeight(randEng)}, inputFmt); @@ -1323,7 +1437,8 @@ TEST_P(OpPillowResize_Negative, varshape_op) pillowResizeOp.getWorkspaceRequirements(numberOfImages, srcSizes.data(), dstSizes.data(), inputFmt)); EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, - nvcv::ProtectCall([&] { pillowResizeOp(stream, ws.get(), batchSrc, batchDst, interpolation); })); + nvcv::ProtectCall([&pillowResizeOp, &stream, &ws, &batchSrc, &batchDst, &interpolation] + { pillowResizeOp(stream, ws.get(), batchSrc, batchDst, interpolation); })); // Get test data back EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); @@ -1349,20 +1464,21 @@ TEST(OpPillowResize_Negative, varshape_hasDifferentFormat) { fmt, nvcv::FMT_RGBA8} }; - for (auto testCase : testSet) + for (const auto &[inputFmtExtra, outputFmtExtra] : testSet) { - nvcv::ImageFormat inputFmtExtra = std::get<0>(testCase); - nvcv::ImageFormat outputFmtExtra = std::get<1>(testCase); - // Create input and output - std::default_random_engine randEng; - std::uniform_int_distribution rndSrcWidth(srcWidthBase * 0.8, srcWidthBase * 1.1); - std::uniform_int_distribution rndSrcHeight(srcHeightBase * 0.8, srcHeightBase * 1.1); - std::uniform_int_distribution rndDstWidth(dstWidthBase * 0.8, dstWidthBase * 1.1); - std::uniform_int_distribution rndDstHeight(dstHeightBase * 0.8, dstHeightBase * 1.1); - - std::vector imgSrc, imgDst; - std::vector srcSizes, dstSizes; + std::default_random_engine randEng; + std::uniform_int_distribution rndSrcWidth(ScaleDimension(srcWidthBase, 0.8), ScaleDimension(srcWidthBase, 1.1)); + std::uniform_int_distribution rndSrcHeight(ScaleDimension(srcHeightBase, 0.8), + ScaleDimension(srcHeightBase, 1.1)); + std::uniform_int_distribution rndDstWidth(ScaleDimension(dstWidthBase, 0.8), ScaleDimension(dstWidthBase, 1.1)); + std::uniform_int_distribution rndDstHeight(ScaleDimension(dstHeightBase, 0.8), + ScaleDimension(dstHeightBase, 1.1)); + + std::vector imgSrc; + std::vector imgDst; + std::vector srcSizes; + std::vector dstSizes; // Create n-1 images with standard format for (int i = 0; i < numberOfImages - 1; ++i) @@ -1397,26 +1513,68 @@ TEST(OpPillowResize_Negative, varshape_hasDifferentFormat) pillowResizeOp.getWorkspaceRequirements(numberOfImages, srcSizes.data(), dstSizes.data(), inputFmtExtra)); EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, - nvcv::ProtectCall([&] { pillowResizeOp(stream, ws.get(), batchSrc, batchDst, interpolation); })); + nvcv::ProtectCall([&pillowResizeOp, &stream, &ws, &batchSrc, &batchDst, &interpolation] + { pillowResizeOp(stream, ws.get(), batchSrc, batchDst, interpolation); })); } EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); } +// 2-channel planar (NCHW/CHW) is rejected: nvcv defines no 2-plane planar format, and PillowResize +// follows the Resize/Normalize convention of disallowing 2-channel planar (see .agents/guidance/PLANAR_GUIDELINES.md). +// The tensor is built by raw (N, C, H, W) shape because no 2-channel image format exists to construct +// it from. The var-shape 2-channel planar guard is unreachable from any constructable input (no +// 2-channel format), so only the tensor path is exercised here. +TEST(OpPillowResize_Negative, planar_two_channel_rejected) +{ + cudaStream_t stream; + EXPECT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + nvcv::Tensor src( + { + {1, 2, 24, 24}, + "NCHW" + }, + nvcv::TYPE_U8); + nvcv::Tensor dst( + { + {1, 2, 12, 12}, + "NCHW" + }, + nvcv::TYPE_U8); + + cvcuda::PillowResize pillowResizeOp; + // Workspace sizing only needs a valid format; the op rejects the 2-channel planar tensor before the + // workspace is touched. + cvcuda::UniqueWorkspace ws + = cvcuda::AllocateWorkspace(pillowResizeOp.getWorkspaceRequirements(1, {24, 24}, {12, 12}, nvcv::FMT_RGB8)); + + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&pillowResizeOp, &stream, &ws, &src, &dst] + { pillowResizeOp(stream, ws.get(), src, dst, NVCV_INTERP_LINEAR); })); + + EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + TEST(OpPillowResize_Negative, invalidGetWorkSpaceReq) { NVCVOperatorHandle pillowResizeHandle; ASSERT_EQ(NVCV_SUCCESS, cvcudaPillowResizeCreate(&pillowResizeHandle)); - NVCVSize2D inputSizesWH[1] = { - {224, 224} - }; - NVCVSize2D outputSizesWH[1] = { - {112, 112} - }; + std::array inputSizesWH{{{224, 224}}}; + std::array outputSizesWH{{{112, 112}}}; + NVCVWorkspaceRequirements req{}; + + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + cvcudaPillowResizeVarShapeGetWorkspaceRequirements(pillowResizeHandle, 1, inputSizesWH.data(), + outputSizesWH.data(), NVCV_IMAGE_FORMAT_U8, nullptr)); EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, - cvcudaPillowResizeVarShapeGetWorkspaceRequirements(pillowResizeHandle, 1, inputSizesWH, outputSizesWH, - NVCV_IMAGE_FORMAT_U8, nullptr)); + cvcudaPillowResizeVarShapeGetWorkspaceRequirements(pillowResizeHandle, 1, nullptr, outputSizesWH.data(), + NVCV_IMAGE_FORMAT_U8, &req)); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + cvcudaPillowResizeVarShapeGetWorkspaceRequirements(pillowResizeHandle, 1, inputSizesWH.data(), nullptr, + NVCV_IMAGE_FORMAT_U8, &req)); EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, cvcudaPillowResizeGetWorkspaceRequirements( pillowResizeHandle, 1, 24, 24, 24, 24, NVCV_IMAGE_FORMAT_U8, nullptr)); @@ -1428,3 +1586,169 @@ TEST(OpPillowResize_Negative, create_null_handle) { EXPECT_EQ(cvcudaPillowResizeCreate(nullptr), NVCV_ERROR_INVALID_ARGUMENT); } + +TEST(OpPillowResize_Negative, null_workspace) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + cvcuda::PillowResize op; + + nvcv::Tensor imgSrc(1, {4, 4}, nvcv::FMT_RGB8); + nvcv::Tensor imgDst(1, {4, 4}, nvcv::FMT_RGB8); + + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, cvcudaPillowResizeSubmit(op.handle(), stream, nullptr, imgSrc.handle(), + imgDst.handle(), NVCV_INTERP_LINEAR)); + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpPillowResize_Negative, null_workspace_varshape) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + cvcuda::PillowResize op; + + const int numImages = 2; + std::vector imgSrc; + std::vector imgDst; + for (int i = 0; i < numImages; ++i) + { + imgSrc.emplace_back(nvcv::Size2D{4, 4}, nvcv::FMT_RGB8); + imgDst.emplace_back(nvcv::Size2D{4, 4}, nvcv::FMT_RGB8); + } + + nvcv::ImageBatchVarShape batchSrc(numImages); + nvcv::ImageBatchVarShape batchDst(numImages); + batchSrc.pushBack(imgSrc.begin(), imgSrc.end()); + batchDst.pushBack(imgDst.begin(), imgDst.end()); + + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + cvcudaPillowResizeVarShapeSubmit(op.handle(), stream, nullptr, batchSrc.handle(), batchDst.handle(), + NVCV_INTERP_LINEAR)); + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpPillowResize_Negative, invalid_interpolation) +{ + NVCVOperatorHandle op; + ASSERT_EQ(NVCV_SUCCESS, cvcudaPillowResizeCreate(&op)); + + // NVCV_INTERP_NEAREST is valid for other ops but not supported by PillowResize + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + cvcudaPillowResizeSubmit(op, nullptr, nullptr, nullptr, nullptr, NVCV_INTERP_NEAREST)); + // Completely out-of-range enum value + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + cvcudaPillowResizeSubmit(op, nullptr, nullptr, nullptr, nullptr, static_cast(99))); + + EXPECT_NO_THROW(nvcvOperatorDestroy(op)); +} + +TEST(OpPillowResize_Negative, invalid_interpolation_varshape) +{ + NVCVOperatorHandle op; + ASSERT_EQ(NVCV_SUCCESS, cvcudaPillowResizeCreate(&op)); + + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + cvcudaPillowResizeVarShapeSubmit(op, nullptr, nullptr, nullptr, nullptr, NVCV_INTERP_NEAREST)); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, cvcudaPillowResizeVarShapeSubmit(op, nullptr, nullptr, nullptr, nullptr, + static_cast(99))); + + EXPECT_NO_THROW(nvcvOperatorDestroy(op)); +} + +// The legacy kernels compute per-sample offsets as 32-bit (sample * imgStride), so any tensor whose +// byte extent exceeds INT32_MAX overflows the addressing and corrupts memory. The operator must +// reject such tensors instead of launching. +TEST(OpPillowResize_Negative, oversized_tensor_rejected) +{ + size_t freeMem = 0; + size_t totalMem = 0; + ASSERT_EQ(cudaSuccess, cudaMemGetInfo(&freeMem, &totalMem)); + // src (1.2 GB) + dst (4.8 GB) + workspace intermediate (2.4 GB) plus slack. + if (freeMem < 10ULL << 30) + { + GTEST_SKIP() << "needs ~10 GB free device memory, have " << (freeMem >> 20) << " MiB"; + } + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + nvcv::ImageFormat fmt = nvcv::FMT_RGBf32; + // 48 x 2160 x 3840 x 3 floats = 4.6 GiB > INT32_MAX bytes: sample offsets overflow 32-bit. + nvcv::Tensor imgSrc(48, {1920, 1080}, fmt); + nvcv::Tensor imgDst(48, {3840, 2160}, fmt); + + cvcuda::PillowResize pillowResizeOp; + + cvcuda::UniqueWorkspace ws + = cvcuda::AllocateWorkspace(pillowResizeOp.getWorkspaceRequirements(48, {1920, 1080}, {3840, 2160}, fmt)); + + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&] { pillowResizeOp(stream, ws.get(), imgSrc, imgDst, NVCV_INTERP_LINEAR); })); + + EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +// Var-shape sibling of oversized_tensor_rejected: the horizontally-resized intermediate is one dense +// elem-typed image slot per batch entry (numImages x maxInH x maxOutW x C) addressed with 32-bit +// products, so batches whose intermediate exceeds INT32_MAX bytes must be rejected. +TEST(OpPillowResize_Negative, oversized_varshape_rejected) +{ + size_t freeMem = 0; + size_t totalMem = 0; + ASSERT_EQ(cudaSuccess, cudaMemGetInfo(&freeMem, &totalMem)); + if (freeMem < 10ULL << 30) + { + GTEST_SKIP() << "needs ~10 GB free device memory, have " << (freeMem >> 20) << " MiB"; + } + + try + { + nvcv::ImageFormat fmt = nvcv::FMT_RGB8; + int numberOfImages = 192; + nvcv::Size2D srcSize{1920, 1080}; + nvcv::Size2D dstSize{3840, 2160}; + + std::vector imgSrc; + std::vector imgDst; + std::vector srcSizes; + std::vector dstSizes; + for (int i = 0; i < numberOfImages; ++i) + { + imgSrc.emplace_back(srcSize, fmt); + imgDst.emplace_back(dstSize, fmt); + srcSizes.push_back(srcSize); + dstSizes.push_back(dstSize); + } + + nvcv::ImageBatchVarShape batchSrc(numberOfImages); + nvcv::ImageBatchVarShape batchDst(numberOfImages); + batchSrc.pushBack(imgSrc.begin(), imgSrc.end()); + batchDst.pushBack(imgDst.begin(), imgDst.end()); + + cvcuda::PillowResize pillowResizeOp; + cvcuda::UniqueWorkspace ws = cvcuda::AllocateWorkspace( + pillowResizeOp.getWorkspaceRequirements(numberOfImages, srcSizes.data(), dstSizes.data(), fmt)); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&] { pillowResizeOp(stream, ws.get(), batchSrc, batchDst, NVCV_INTERP_LINEAR); })); + + EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + } + catch (const nvcv::Exception &e) + { + if (e.code() == nvcv::Status::ERROR_OUT_OF_MEMORY) + { + GTEST_SKIP() << "insufficient device memory for oversized var-shape input: " << e.what(); + } + throw; + } +} diff --git a/tests/cvcuda/system/TestOpPosterize.cpp b/tests/cvcuda/system/TestOpPosterize.cpp new file mode 100644 index 000000000..1886692c5 --- /dev/null +++ b/tests/cvcuda/system/TestOpPosterize.cpp @@ -0,0 +1,165 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "ElementwiseOpHarness.hpp" +#include "PlanarParityUtils.hpp" + +#include +#include + +#include + +namespace test = nvcv::test; +namespace ew = nvcv::test::elementwise; + +namespace { + +// Independent CPU gold: keep the top `bits` bits of each channel value, zero the rest: +// out = in & ~((1 << (W - bits)) - 1), where W is the type bit width. Mirrors the documented +// oracle (torchvision/PIL posterize, generalized to 16-bit) and is computed independently of the +// kernel so the bit-exact EXPECT_EQ is a real regression check. +template +T PosterizeScalarGold(T v, int bits) +{ + constexpr auto W = static_cast(sizeof(T) * 8); + uint32_t mask; + if (bits <= 0) + { + mask = 0u; + } + else if (bits >= W) + { + mask = ~0u; + } + else + { + mask = ~((1u << (W - bits)) - 1u); + } + return static_cast(v & static_cast(mask)); +} + +// Generic invoker pinning a given bit count (Posterize is integer-only, so the same bits value is +// used for u8 and u16). +auto invokeBits(int bits) +{ + return [bits](cudaStream_t s, const auto &in, auto &out) + { + cvcuda::Posterize op; + op(s, in, out, bits); + }; +} + +} // namespace + +// Tensor correctness over the declared dtype × channel matrix (u8 / u16 only) ------------------- +// clang-format off +NVCV_TEST_SUITE_P(OpPosterize, test::ValueList +{ + // width, height, batch, format (dtype / channels) + { 66, 55, 1, nvcv::FMT_U8 }, // u8 / 1ch + { 123, 67, 3, nvcv::FMT_RGB8 }, // u8 / 3ch + { 42, 53, 4, nvcv::FMT_RGBA8 }, // u8 / 4ch + { 80, 40, 2, nvcv::FMT_U16 }, // u16 / 1ch +}); + +// clang-format on +TEST_P(OpPosterize, tensor_correct_output) +{ + const int width = GetParamValue<0>(); + const int height = GetParamValue<1>(); + const int batch = GetParamValue<2>(); + const nvcv::ImageFormat fmt{GetParamValue<3>()}; + const int bits = 4; + + // Posterize is integer-only, so dispatch directly over u8 / u16 (no f32 branch). + if (ew::BaseKind(fmt) == 1) + { + ew::RunTensorCorrect( + width, height, batch, fmt, [](uint16_t v) { return PosterizeScalarGold(v, 4); }, + invokeBits(bits)); + } + else + { + ew::RunTensorCorrect( + width, height, batch, fmt, [](uint8_t v) { return PosterizeScalarGold(v, 4); }, invokeBits(bits)); + } +} + +// VarShape correctness: bit-exact vs the CPU gold ------------------------------------------------ +TEST(OpPosterize, varshape_correct_output) +{ + ew::RunVarShapeCorrect([](uint8_t v) { return PosterizeScalarGold(v, 3); }, invokeBits(3)); +} + +// Planar ≡ interleaved parity (fake-planar) ----------------------------------------------------- +// clang-format off +NVCV_TEST_SUITE_P(OpPosterizePlanar, + test::ValueList{ + {176, 113, 2, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + { 64, 48, 1, nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8}, +}); + +NVCV_TEST_SUITE_P(OpPosterizePlanarVarShape, + test::ValueList{ + {176, 113, 2, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, +}); + +// clang-format on +TEST_P(OpPosterizePlanar, tensor_matches_interleaved) +{ + test::planar::RunTensorParity(GetParamValue<3>(), GetParamValue<4>(), GetParamValue<0>(), GetParamValue<1>(), + GetParamValue<0>(), GetParamValue<1>(), GetParamValue<2>(), + [](cudaStream_t s, const nvcv::Tensor &src, const nvcv::Tensor &dst, + nvcv::ImageFormat) { EXPECT_NO_THROW(invokeBits(3)(s, src, dst)); }); +} + +TEST_P(OpPosterizePlanarVarShape, varshape_matches_interleaved) +{ + test::planar::RunVarShapeParity( + GetParamValue<3>(), GetParamValue<4>(), GetParamValue<0>(), GetParamValue<1>(), GetParamValue<0>(), + GetParamValue<1>(), GetParamValue<2>(), + [](cudaStream_t s, const nvcv::ImageBatchVarShape &src, const nvcv::ImageBatchVarShape &dst, nvcv::ImageFormat) + { EXPECT_NO_THROW(invokeBits(3)(s, src, dst)); }); +} + +// Negative tests: the complement of the support matrix must be rejected ------------------------- +// clang-format off +NVCV_TEST_SUITE_P(OpPosterize_Negative, test::ValueList{ + {nvcv::FMT_F32, nvcv::FMT_F32 }, // unsupported dtype (32-bit float) + {nvcv::FMT_F16, nvcv::FMT_F16 }, // unsupported dtype (16-bit float) + {nvcv::FMT_S16, nvcv::FMT_S16 }, // unsupported dtype (signed 16-bit) + {nvcv::FMT_RGB8, nvcv::FMT_RGB8p}, // layout mismatch (interleaved in, planar out) + {nvcv::FMT_RGB8, nvcv::FMT_U8 }, // input/output channel mismatch +}); + +// clang-format on +TEST_P(OpPosterize_Negative, rejects_unsupported) +{ + ew::ExpectRejected(GetParamValue<0>(), GetParamValue<1>(), invokeBits(4)); +} + +TEST(OpPosterize_Negative, rejects_two_channel) +{ + ew::ExpectRejected(nvcv::FMT_2S16, nvcv::FMT_2S16, invokeBits(4), 16, 16); +} + +// bits outside [0, W] must be rejected (u8 has W=8, so 9 and -1 are out of range). +TEST(OpPosterize_Negative, rejects_bits_out_of_range) +{ + ew::ExpectRejected(nvcv::FMT_U8, nvcv::FMT_U8, invokeBits(9), 16, 16); + ew::ExpectRejected(nvcv::FMT_U8, nvcv::FMT_U8, invokeBits(-1), 16, 16); +} diff --git a/tests/cvcuda/system/TestOpRandomResizedCrop.cpp b/tests/cvcuda/system/TestOpRandomResizedCrop.cpp index c28209f2d..d81946baa 100644 --- a/tests/cvcuda/system/TestOpRandomResizedCrop.cpp +++ b/tests/cvcuda/system/TestOpRandomResizedCrop.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,6 +16,7 @@ */ #include "Definitions.hpp" +#include "PlanarParityUtils.hpp" #include "ResizeUtils.hpp" #include @@ -36,6 +37,11 @@ namespace cuda = nvcv::cuda; namespace test = nvcv::test; namespace t = ::testing; +static int ScaledSize(int size, double scale) +{ + return static_cast(static_cast(size) * scale); +} + static void GetCropParams(std::mt19937 &generator, double minScale, double maxScale, double minRatio, double maxRatio, int input_rows, int input_cols, int *top_indices, int *left_indices, int *crop_rows, int *crop_cols) @@ -46,23 +52,23 @@ static void GetCropParams(std::mt19937 &generator, double minScale, double maxSc const double log_min_ratio = std::log(minRatio); const double log_max_ratio = std::log(maxRatio); - std::uniform_real_distribution scale_dist(minScale, maxScale); - std::uniform_real_distribution ratio_dist(log_min_ratio, log_max_ratio); - bool got_params = false; + std::uniform_real_distribution scale_dist(minScale, maxScale); + std::uniform_real_distribution ratio_dist(log_min_ratio, log_max_ratio); + bool got_params = false; for (int i = 0; i < 10; ++i) { if (got_params) return; - int target_area = area * scale_dist(generator); + auto target_area = static_cast(area * scale_dist(generator)); double aspect_ratio = std::exp(ratio_dist(generator)); - *crop_cols = int(std::round(std::sqrt(target_area * aspect_ratio))); - *crop_rows = int(std::round(std::sqrt(target_area / aspect_ratio))); + *crop_cols = static_cast(std::round(std::sqrt(static_cast(target_area) * aspect_ratio))); + *crop_rows = static_cast(std::round(std::sqrt(static_cast(target_area) / aspect_ratio))); if (*crop_cols > 0 && *crop_cols <= cols && *crop_rows > 0 && *crop_rows <= rows) { - std::uniform_int_distribution row_uni(0, rows - *crop_rows); - std::uniform_int_distribution col_uni(0, cols - *crop_cols); + std::uniform_int_distribution row_uni(0, rows - *crop_rows); + std::uniform_int_distribution col_uni(0, cols - *crop_cols); *top_indices = row_uni(generator); *left_indices = col_uni(generator); got_params = true; @@ -71,16 +77,15 @@ static void GetCropParams(std::mt19937 &generator, double minScale, double maxSc // Fallback to central crop if (!got_params) { - double in_ratio = double(cols) / double(rows); - if (in_ratio < minRatio) + if (double in_ratio = double(cols) / double(rows); in_ratio < minRatio) { *crop_cols = cols; - *crop_rows = int(std::round(*crop_cols / minRatio)); + *crop_rows = static_cast(std::round(static_cast(*crop_cols) / minRatio)); } else if (in_ratio > maxRatio) { *crop_rows = rows; - *crop_cols = int(std::round(*crop_rows * maxRatio)); + *crop_cols = static_cast(std::round(static_cast(*crop_rows) * maxRatio)); } else // whole image { @@ -116,27 +121,17 @@ NVCV_TEST_SUITE_P(OpRandomResizedCrop, test::ValueList(); - int srcHeight = GetParamValue<1>(); - int dstWidth = GetParamValue<2>(); - int dstHeight = GetParamValue<3>(); - double minScale = 0.08; double maxScale = 1.0; double minRatio = 3.0 / 4; double maxRatio = 4.0 / 3; - NVCVInterpolationType interpolation = GetParamValue<4>(); - - int numberOfImages = GetParamValue<5>(); - - const nvcv::ImageFormat fmt = nvcv::FMT_RGBA8; - // Generate input nvcv::Tensor imgSrc = nvcv::util::CreateTensor(numberOfImages, srcWidth, srcHeight, fmt); @@ -157,7 +152,7 @@ TEST_P(OpRandomResizedCrop, tensor_correct_output) std::uniform_int_distribution rand(0, 255); srcVec[i].resize(srcHeight * srcVecRowStride); - std::generate(srcVec[i].begin(), srcVec[i].end(), [&]() { return rand(randEng); }); + std::ranges::generate(srcVec[i], [&rand, &randEng]() { return rand(randEng); }); // Copy input data to the GPU ASSERT_EQ(cudaSuccess, @@ -200,7 +195,10 @@ TEST_P(OpRandomResizedCrop, tensor_correct_output) dstVecRowStride, // vec has no padding dstHeight, cudaMemcpyDeviceToHost)); - int top, left, crop_rows, crop_cols; + int top; + int left; + int crop_rows; + int crop_cols; GetCropParams(generator, minScale, maxScale, minRatio, maxRatio, srcHeight, srcWidth, &top, &left, &crop_rows, &crop_cols); @@ -211,48 +209,62 @@ TEST_P(OpRandomResizedCrop, tensor_correct_output) {srcWidth, srcHeight}, top, left, crop_rows, crop_cols, fmt, interpolation); // maximum absolute error - std::vector mae(testVec.size()); - for (size_t i = 0; i < mae.size(); ++i) + std::vector absDiff(testVec.size()); + for (size_t idx = 0; idx < absDiff.size(); ++idx) { - mae[i] = abs(static_cast(goldVec[i]) - static_cast(testVec[i])); + absDiff[idx] = abs(static_cast(goldVec[idx]) - static_cast(testVec[idx])); } - int maeThreshold = 1; + int maxAbsDiff = 1; - EXPECT_THAT(mae, t::Each(t::Le(maeThreshold))); + EXPECT_THAT(absDiff, t::Each(t::Le(maxAbsDiff))); } } -TEST_P(OpRandomResizedCrop, varshape_correct_output) +TEST_P(OpRandomResizedCrop, tensor_correct_output) +{ + int srcWidth = GetParamValue<0>(); + int srcHeight = GetParamValue<1>(); + int dstWidth = GetParamValue<2>(); + int dstHeight = GetParamValue<3>(); + + NVCVInterpolationType interpolation = GetParamValue<4>(); + int numberImages = GetParamValue<5>(); + + RunTensorCorrectOutput(srcWidth, srcHeight, dstWidth, dstHeight, interpolation, numberImages, nvcv::FMT_RGBA8); + if (interpolation == NVCV_INTERP_LINEAR) + { + RunTensorCorrectOutput(srcWidth, srcHeight, dstWidth, dstHeight, interpolation, numberImages, nvcv::FMT_RGB8); + RunTensorCorrectOutput(srcWidth, srcHeight, dstWidth, dstHeight, interpolation, numberImages, nvcv::FMT_U8); + } + else if (interpolation == NVCV_INTERP_CUBIC) + { + RunTensorCorrectOutput(srcWidth, srcHeight, dstWidth, dstHeight, interpolation, numberImages, nvcv::FMT_U8); + } +} + +static void RunVarShapeCorrectOutput(int srcWidthBase, int srcHeightBase, int dstWidthBase, int dstHeightBase, + NVCVInterpolationType interpolation, int numberOfImages, nvcv::ImageFormat fmt) { cudaStream_t stream; EXPECT_EQ(cudaSuccess, cudaStreamCreate(&stream)); - int srcWidthBase = GetParamValue<0>(); - int srcHeightBase = GetParamValue<1>(); - int dstWidthBase = GetParamValue<2>(); - int dstHeightBase = GetParamValue<3>(); - double minScale = 0.08; double maxScale = 1.0; double minRatio = 3.0 / 4; double maxRatio = 4.0 / 3; - NVCVInterpolationType interpolation = GetParamValue<4>(); - - int numberOfImages = GetParamValue<5>(); - - const nvcv::ImageFormat fmt = nvcv::FMT_RGBA8; - // Create input and output - std::default_random_engine randEng; - std::uniform_int_distribution rndSrcWidth(srcWidthBase * 0.8, srcWidthBase * 1.1); - std::uniform_int_distribution rndSrcHeight(srcHeightBase * 0.8, srcHeightBase * 1.1); + std::default_random_engine randEng; + std::uniform_int_distribution rndSrcWidth(ScaledSize(srcWidthBase, 0.8), ScaledSize(srcWidthBase, 1.1)); + std::uniform_int_distribution rndSrcHeight(ScaledSize(srcHeightBase, 0.8), ScaledSize(srcHeightBase, 1.1)); + + std::uniform_int_distribution rndDstWidth(ScaledSize(dstWidthBase, 0.8), ScaledSize(dstWidthBase, 1.1)); + std::uniform_int_distribution rndDstHeight(ScaledSize(dstHeightBase, 0.8), ScaledSize(dstHeightBase, 1.1)); - std::uniform_int_distribution rndDstWidth(dstWidthBase * 0.8, dstWidthBase * 1.1); - std::uniform_int_distribution rndDstHeight(dstHeightBase * 0.8, dstHeightBase * 1.1); + std::vector imgSrc; - std::vector imgSrc, imgDst; + std::vector imgDst; for (int i = 0; i < numberOfImages; ++i) { imgSrc.emplace_back(nvcv::Size2D{rndSrcWidth(randEng), rndSrcHeight(randEng)}, fmt); @@ -284,7 +296,7 @@ TEST_P(OpRandomResizedCrop, varshape_correct_output) std::uniform_int_distribution rand(0, 255); srcVec[i].resize(srcHeight * srcRowStride); - std::generate(srcVec[i].begin(), srcVec[i].end(), [&]() { return rand(randEng); }); + std::ranges::generate(srcVec[i], [&rand, &randEng]() { return rand(randEng); }); // Copy input data to the GPU ASSERT_EQ(cudaSuccess, @@ -330,7 +342,10 @@ TEST_P(OpRandomResizedCrop, varshape_correct_output) dstRowStride, // vec has no padding dstHeight, cudaMemcpyDeviceToHost)); - int top, left, crop_rows, crop_cols; + int top; + int left; + int crop_rows; + int crop_cols; GetCropParams(generator, minScale, maxScale, minRatio, maxRatio, srcHeight, srcWidth, &top, &left, &crop_rows, &crop_cols); @@ -341,19 +356,123 @@ TEST_P(OpRandomResizedCrop, varshape_correct_output) {srcWidth, srcHeight}, top, left, crop_rows, crop_cols, fmt, interpolation); // maximum absolute error - std::vector mae(testVec.size()); - for (size_t i = 0; i < mae.size(); ++i) + std::vector absDiff(testVec.size()); + for (size_t idx = 0; idx < absDiff.size(); ++idx) { - mae[i] = abs(static_cast(goldVec[i]) - static_cast(testVec[i])); + absDiff[idx] = abs(static_cast(goldVec[idx]) - static_cast(testVec[idx])); } - int maeThreshold = 1; + int maxAbsDiff = 1; - EXPECT_THAT(mae, t::Each(t::Le(maeThreshold))); + EXPECT_THAT(absDiff, t::Each(t::Le(maxAbsDiff))); } } -TEST(OpRandomResizedCrop_negative, createWithNullHandle) +TEST_P(OpRandomResizedCrop, varshape_correct_output) +{ + int srcWidthBase = GetParamValue<0>(); + int srcHeightBase = GetParamValue<1>(); + int dstWidthBase = GetParamValue<2>(); + int dstHeightBase = GetParamValue<3>(); + + NVCVInterpolationType interpolation = GetParamValue<4>(); + int numberImages = GetParamValue<5>(); + + RunVarShapeCorrectOutput(srcWidthBase, srcHeightBase, dstWidthBase, dstHeightBase, interpolation, numberImages, + nvcv::FMT_RGBA8); + if (interpolation == NVCV_INTERP_LINEAR) + { + RunVarShapeCorrectOutput(srcWidthBase, srcHeightBase, dstWidthBase, dstHeightBase, interpolation, numberImages, + nvcv::FMT_RGB8); + RunVarShapeCorrectOutput(srcWidthBase, srcHeightBase, dstWidthBase, dstHeightBase, interpolation, numberImages, + nvcv::FMT_U8); + } + else if (interpolation == NVCV_INTERP_CUBIC) + { + RunVarShapeCorrectOutput(srcWidthBase, srcHeightBase, dstWidthBase, dstHeightBase, interpolation, numberImages, + nvcv::FMT_U8); + } +} + +// ============================================================================= +// Planar (NCHW/CHW) layout support +// +// RandomResizedCrop samples every channel from the same crop window, so a planar input is cropped +// and resized plane-by-plane and must produce the same pixels as the interleaved path. Each parity +// invocation constructs a fresh operator with the same seed so the interleaved and planar runs use +// identical crop parameters. +// ============================================================================= + +namespace { + +void RunPlanarParityTensorCase(nvcv::ImageFormat planarFmt, nvcv::ImageFormat interleavedFmt, int srcW, int srcH, + int dstW, int dstH, NVCVInterpolationType interp, int numImages) +{ + constexpr double minScale = 0.08; + constexpr double maxScale = 1.0; + constexpr double minRatio = 3.0 / 4.0; + constexpr double maxRatio = 4.0 / 3.0; + constexpr uint32_t seed = 11; + + test::planar::RunTensorParity( + planarFmt, interleavedFmt, srcW, srcH, dstW, dstH, numImages, + [interp, numImages](cudaStream_t s, const nvcv::Tensor &src, const nvcv::Tensor &dst, nvcv::ImageFormat) + { + cvcuda::RandomResizedCrop op(minScale, maxScale, minRatio, maxRatio, numImages, seed); + EXPECT_NO_THROW(op(s, src, dst, interp)); + }); +} + +void RunPlanarParityVarShapeCase(nvcv::ImageFormat planarFmt, nvcv::ImageFormat interleavedFmt, int srcW, int srcH, + int dstW, int dstH, NVCVInterpolationType interp, int numImages) +{ + constexpr double minScale = 0.08; + constexpr double maxScale = 1.0; + constexpr double minRatio = 3.0 / 4.0; + constexpr double maxRatio = 4.0 / 3.0; + constexpr uint32_t seed = 11; + + test::planar::RunVarShapeParity(planarFmt, interleavedFmt, srcW, srcH, dstW, dstH, numImages, + [interp, numImages](cudaStream_t s, const nvcv::ImageBatchVarShape &src, + const nvcv::ImageBatchVarShape &dst, nvcv::ImageFormat) + { + cvcuda::RandomResizedCrop op(minScale, maxScale, minRatio, maxRatio, numImages, + seed); + EXPECT_NO_THROW(op(s, src, dst, interp)); + }); +} + +} // namespace + +// Parameters: srcW, srcH, dstW, dstH, interpolation, numImages, planarFmt, interleavedFmt +// clang-format off +NVCV_TEST_SUITE_P(OpRandomResizedCropPlanar, + test::ValueList{ + { 64, 48, 128, 96, NVCV_INTERP_NEAREST, 2, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + {128, 96, 64, 48, NVCV_INTERP_LINEAR, 2, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + { 80, 60, 40, 30, NVCV_INTERP_CUBIC, 1, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + { 50, 40, 100, 80, NVCV_INTERP_NEAREST, 2, nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8}, + { 96, 64, 48, 32, NVCV_INTERP_LINEAR, 1, nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8}, + { 64, 48, 96, 72, NVCV_INTERP_NEAREST, 2, nvcv::FMT_RGBAf32p, nvcv::FMT_RGBAf32}, + { 72, 54, 45, 35, NVCV_INTERP_LINEAR, 2, nvcv::FMT_RGBf32p, nvcv::FMT_RGBf32}, + { 68, 52, 39, 31, NVCV_INTERP_CUBIC, 1, nvcv::FMT_RGBAf32p, nvcv::FMT_RGBAf32}, +}); + +// clang-format on + +TEST_P(OpRandomResizedCropPlanar, tensor_matches_interleaved) +{ + RunPlanarParityTensorCase(GetParamValue<6>(), GetParamValue<7>(), GetParamValue<0>(), GetParamValue<1>(), + GetParamValue<2>(), GetParamValue<3>(), GetParamValue<4>(), GetParamValue<5>()); +} + +TEST_P(OpRandomResizedCropPlanar, varshape_matches_interleaved) +{ + RunPlanarParityVarShapeCase(GetParamValue<6>(), GetParamValue<7>(), GetParamValue<0>(), GetParamValue<1>(), + GetParamValue<2>(), GetParamValue<3>(), GetParamValue<4>(), GetParamValue<5>()); +} + +TEST(OpRandomResizedCrop_Negative, createWithNullHandle) { EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, cvcudaRandomResizedCropCreate(nullptr, 0.2, 1.0, 0.8, 1.3, 2, 0)); } @@ -365,20 +484,35 @@ TEST(OpRandomResizedCrop, createWithZeroSeed) EXPECT_NO_THROW(nvcvOperatorDestroy(opHandle)); } -TEST(OpRandomResizedCrop_negative, createWithInvalidScale) +TEST(OpRandomResizedCrop_Negative, createWithInvalidScale) { NVCVOperatorHandle opHandle; EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, cvcudaRandomResizedCropCreate(&opHandle, 1.0, 0.2, 0.8, 1.3, 2, 0)); } -TEST(OpRandomResizedCrop_negative, createWithInvalidRatio) +TEST(OpRandomResizedCrop_Negative, createWithInvalidRatio) { NVCVOperatorHandle opHandle; EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, cvcudaRandomResizedCropCreate(&opHandle, 0.2, 1.0, 1.3, 0.8, 2, 0)); } +TEST(OpRandomResizedCrop_Negative, tensorBatchLargerThanMaxBatchSizeRejected) +{ + constexpr int maxBatchSize = 1; + constexpr int numberOfImages = 2; + + nvcv::Tensor imgSrc = nvcv::util::CreateTensor(numberOfImages, 24, 24, nvcv::FMT_RGBA8); + nvcv::Tensor imgDst = nvcv::util::CreateTensor(numberOfImages, 12, 12, nvcv::FMT_RGBA8); + + cvcuda::RandomResizedCrop randomResizedCropOp(0.08, 1.0, 3.0 / 4.0, 4.0 / 3.0, maxBatchSize, 1); + + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&randomResizedCropOp, &imgSrc, &imgDst] + { randomResizedCropOp(nullptr, imgSrc, imgDst, NVCV_INTERP_NEAREST); })); +} + // clang-format off -NVCV_TEST_SUITE_P(OpRandomResizedCrop_negative, nvcv::test::ValueList +NVCV_TEST_SUITE_P(OpRandomResizedCrop_Negative, nvcv::test::ValueList { // in_layout, in_data_type, out_layout, out_data_type, interpolation, channels { "CHW", nvcv::TYPE_U8, "HWC", nvcv::TYPE_U8, NVCV_INTERP_NEAREST, 2}, @@ -392,7 +526,7 @@ NVCV_TEST_SUITE_P(OpRandomResizedCrop_negative, nvcv::test::ValueList +NVCV_TEST_SUITE_P(OpRandomResizedCropVarshape_Negative, nvcv::test::ValueList { // exceed max batch size {10, nvcv::FMT_RGBA8, nvcv::FMT_RGBA8, NVCV_INTERP_NEAREST, nvcv::FMT_RGBA8, nvcv::FMT_RGBA8}, @@ -401,7 +535,6 @@ NVCV_TEST_SUITE_P(OpRandomResizedCropVarshape_negative, nvcv::test::ValueList(); nvcv::DataType in_data_type = GetParamValue<1>(); @@ -445,10 +578,11 @@ TEST_P(OpRandomResizedCrop_negative, infer_negative_parameter) cvcuda::RandomResizedCrop randomResizedCropOp(minScale, maxScale, minRatio, maxRatio, numberOfImages, seed); EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, - nvcv::ProtectCall([&] { randomResizedCropOp(NULL, imgSrc, imgDst, interpolation); })); + nvcv::ProtectCall([&randomResizedCropOp, &imgSrc, &imgDst, &interpolation] + { randomResizedCropOp(nullptr, imgSrc, imgDst, interpolation); })); } -TEST_P(OpRandomResizedCropVarshape_negative, infer_negative_parameter) +TEST_P(OpRandomResizedCropVarshape_Negative, infer_negative_parameter) { cudaStream_t stream; EXPECT_EQ(cudaSuccess, cudaStreamCreate(&stream)); @@ -472,14 +606,16 @@ TEST_P(OpRandomResizedCropVarshape_negative, infer_negative_parameter) double maxRatio = 4.0 / 3.0; // Create input and output - std::default_random_engine randEng; - std::uniform_int_distribution rndSrcWidth(srcWidthBase * 0.8, srcWidthBase * 1.1); - std::uniform_int_distribution rndSrcHeight(srcHeightBase * 0.8, srcHeightBase * 1.1); + std::default_random_engine randEng; + std::uniform_int_distribution rndSrcWidth(ScaledSize(srcWidthBase, 0.8), ScaledSize(srcWidthBase, 1.1)); + std::uniform_int_distribution rndSrcHeight(ScaledSize(srcHeightBase, 0.8), ScaledSize(srcHeightBase, 1.1)); + + std::uniform_int_distribution rndDstWidth(ScaledSize(dstWidthBase, 0.8), ScaledSize(dstWidthBase, 1.1)); + std::uniform_int_distribution rndDstHeight(ScaledSize(dstHeightBase, 0.8), ScaledSize(dstHeightBase, 1.1)); - std::uniform_int_distribution rndDstWidth(dstWidthBase * 0.8, dstWidthBase * 1.1); - std::uniform_int_distribution rndDstHeight(dstHeightBase * 0.8, dstHeightBase * 1.1); + std::vector imgSrc; - std::vector imgSrc, imgDst; + std::vector imgDst; for (int i = 0; i < numberOfImages - 1; ++i) { imgSrc.emplace_back(nvcv::Size2D{rndSrcWidth(randEng), rndSrcHeight(randEng)}, fmtIn); @@ -498,7 +634,8 @@ TEST_P(OpRandomResizedCropVarshape_negative, infer_negative_parameter) uint32_t seed = 1; cvcuda::RandomResizedCrop randomResizedCropOp(minScale, maxScale, minRatio, maxRatio, maxNumberOfImages, seed); EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, - nvcv::ProtectCall([&] { randomResizedCropOp(stream, batchSrc, batchDst, interpolation); })); + nvcv::ProtectCall([&randomResizedCropOp, &stream, &batchSrc, &batchDst, &interpolation] + { randomResizedCropOp(stream, batchSrc, batchDst, interpolation); })); EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); diff --git a/tests/cvcuda/system/TestOpReformat.cpp b/tests/cvcuda/system/TestOpReformat.cpp index 69717e971..d635f0a6e 100644 --- a/tests/cvcuda/system/TestOpReformat.cpp +++ b/tests/cvcuda/system/TestOpReformat.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -28,6 +28,7 @@ #include #include +#include namespace test = nvcv::test; namespace cuda = nvcv::cuda; @@ -36,7 +37,8 @@ namespace ttype = nvcv::test::type; using uchar = unsigned char; template -inline T &ValueAt(std::vector &vec, long4_16a pitches, int b, int y, int x, int c, nvcv::TensorLayout layout) +inline T &ValueAt(std::vector &vec, const long4_16a &pitches, int b, int y, int x, int c, + nvcv::TensorLayout layout) { if (layout == nvcv::TENSOR_NHWC || layout == nvcv::TENSOR_HWC) { @@ -49,22 +51,41 @@ inline T &ValueAt(std::vector &vec, long4_16a pitches, int b, int y, in return *reinterpret_cast(&vec[0]); } +struct ReformatRefData +{ + std::vector &hDst; + long4_16a dstStrides; + nvcv::TensorLayout dstLayout; + std::vector &hSrc; + long4_16a srcStrides; + nvcv::TensorLayout srcLayout; + int numChannels; +}; + template -inline void Reformat(std::vector &hDst, long4_16a dstStrides, nvcv::TensorLayout dstLayout, - std::vector &hSrc, long4_16a srcStrides, nvcv::TensorLayout srcLayout, int numBatches, - int numRows, int numCols, int numChannels) +inline void ReformatPixel(ReformatRefData &ref, int b, int y, int x) { + for (int c = 0; c < ref.numChannels; ++c) + { + ValueAt(ref.hDst, ref.dstStrides, b, y, x, c, ref.dstLayout) + = ValueAt(ref.hSrc, ref.srcStrides, b, y, x, c, ref.srcLayout); + } +} + +template +inline void Reformat(std::vector &hDst, const long4_16a &dstStrides, nvcv::TensorLayout dstLayout, + std::vector &hSrc, const long4_16a &srcStrides, nvcv::TensorLayout srcLayout, + int numBatches, int numRows, int numCols, int numChannels) +{ + ReformatRefData ref{hDst, dstStrides, dstLayout, hSrc, srcStrides, srcLayout, numChannels}; + for (int b = 0; b < numBatches; ++b) { for (int y = 0; y < numRows; ++y) { for (int x = 0; x < numCols; ++x) { - for (int c = 0; c < numChannels; ++c) - { - ValueAt(hDst, dstStrides, b, y, x, c, dstLayout) - = ValueAt(hSrc, srcStrides, b, y, x, c, srcLayout); - } + ReformatPixel(ref, b, y, x); } } } @@ -117,6 +138,8 @@ TYPED_TEST(OpReformat, correct_output) ASSERT_TRUE(outAccess); ASSERT_EQ(inAccess->numChannels(), outAccess->numChannels()); + ASSERT_EQ(inData->rank(), outData->rank()); + ASSERT_TRUE(inData->rank() == 3 || inData->rank() == 4); long4_16a inStrides; long4_16a outStrides; @@ -137,16 +160,16 @@ TYPED_TEST(OpReformat, correct_output) outStrides.z = outData->stride(1); outStrides.w = outData->stride(2); } - else if (inData->rank() == 4) + else { inStrides = long4_16a{inData->stride(0), inData->stride(1), inData->stride(2), inData->stride(3)}; outStrides = long4_16a{outData->stride(0), outData->stride(1), outData->stride(2), outData->stride(3)}; } - int numBatches = inAccess->numSamples(); - int numRows = inAccess->numRows(); - int numCols = inAccess->numCols(); - int numChannels = inAccess->numChannels(); + auto numBatches = static_cast(inAccess->numSamples()); + int numRows = inAccess->numRows(); + int numCols = inAccess->numCols(); + int numChannels = inAccess->numChannels(); long inBufSize = inStrides.x * inAccess->numSamples(); long outBufSize = outStrides.x * outAccess->numSamples(); @@ -156,7 +179,7 @@ TYPED_TEST(OpReformat, correct_output) std::default_random_engine randEng(0); std::uniform_int_distribution rand(0u, 255u); - std::generate(inVec.begin(), inVec.end(), [&]() { return rand(randEng); }); + std::ranges::generate(inVec, [&rand, &randEng]() { return rand(randEng); }); // copy random input to device ASSERT_EQ(cudaSuccess, cudaMemcpy(inData->basePtr(), inVec.data(), inBufSize, cudaMemcpyHostToDevice)); @@ -182,6 +205,348 @@ TYPED_TEST(OpReformat, correct_output) EXPECT_EQ(testVec, goldVec); } +static nvcv::Tensor WrapU8C1Tensor(NVCVByte *basePtr, nvcv::TensorLayout layout, int batches, int height, int width, + int rowStride, int sampleStride) +{ + nvcv::TensorDataStridedCuda::Buffer buffer{}; + buffer.basePtr = basePtr; + buffer.strides[0] = sampleStride; + + if (layout == nvcv::TENSOR_NHWC) + { + buffer.strides[1] = rowStride; + buffer.strides[2] = 1; + buffer.strides[3] = 1; + return nvcv::TensorWrapData(nvcv::TensorDataStridedCuda{ + nvcv::TensorShape{{batches, height, width, 1}, "NHWC"}, + nvcv::TYPE_U8, buffer + }); + } + + buffer.strides[1] = rowStride * height; + buffer.strides[2] = rowStride; + buffer.strides[3] = 1; + return nvcv::TensorWrapData(nvcv::TensorDataStridedCuda{ + nvcv::TensorShape{{batches, 1, height, width}, "NCHW"}, + nvcv::TYPE_U8, buffer + }); +} + +static void RunU8C1PaddedStrideCase(nvcv::TensorLayout srcLayout, nvcv::TensorLayout dstLayout, int batches, + int height = 5, int width = 37) +{ + const int srcRowStride = width + 11; + const int dstRowStride = width + 27; + const int srcSampleStride = srcRowStride * height + 16; + const int dstSampleStride = dstRowStride * height + 32; + constexpr uint8_t srcPadding = 0xA5; + constexpr uint8_t dstPadding = 0xD7; + + NVCVByte *srcPtr = nullptr; + NVCVByte *dstPtr = nullptr; + ASSERT_EQ(cudaSuccess, cudaMalloc(&srcPtr, batches * srcSampleStride)); + ASSERT_EQ(cudaSuccess, cudaMalloc(&dstPtr, batches * dstSampleStride)); + + std::vector src(batches * srcSampleStride, srcPadding); + std::vector dst(batches * dstSampleStride, dstPadding); + std::vector expected = dst; + for (int b = 0; b < batches; ++b) + { + for (int y = 0; y < height; ++y) + { + for (int x = 0; x < width; ++x) + { + const auto value = static_cast(b * 73 + y * 19 + x * 7 + 11); + src[b * srcSampleStride + y * srcRowStride + x] = value; + expected[b * dstSampleStride + y * dstRowStride + x] = value; + } + } + } + + ASSERT_EQ(cudaSuccess, cudaMemcpy(srcPtr, src.data(), src.size(), cudaMemcpyHostToDevice)); + ASSERT_EQ(cudaSuccess, cudaMemcpy(dstPtr, dst.data(), dst.size(), cudaMemcpyHostToDevice)); + + nvcv::Tensor srcTensor = WrapU8C1Tensor(srcPtr, srcLayout, batches, height, width, srcRowStride, srcSampleStride); + nvcv::Tensor dstTensor = WrapU8C1Tensor(dstPtr, dstLayout, batches, height, width, dstRowStride, dstSampleStride); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + cvcuda::Reformat op; + EXPECT_NO_THROW(op(stream, srcTensor, dstTensor)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + std::vector got(expected.size()); + ASSERT_EQ(cudaSuccess, cudaMemcpy(got.data(), dstPtr, got.size(), cudaMemcpyDeviceToHost)); + EXPECT_EQ(expected, got); + + std::vector gotSrc(src.size()); + ASSERT_EQ(cudaSuccess, cudaMemcpy(gotSrc.data(), srcPtr, gotSrc.size(), cudaMemcpyDeviceToHost)); + EXPECT_EQ(src, gotSrc); + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + ASSERT_EQ(cudaSuccess, cudaFree(srcPtr)); + ASSERT_EQ(cudaSuccess, cudaFree(dstPtr)); +} + +TEST(OpReformat, u8_c1_padded_strides_preserve_canaries) +{ + RunU8C1PaddedStrideCase(nvcv::TENSOR_NCHW, nvcv::TENSOR_NHWC, 1); + RunU8C1PaddedStrideCase(nvcv::TENSOR_NHWC, nvcv::TENSOR_NCHW, 1); + RunU8C1PaddedStrideCase(nvcv::TENSOR_NCHW, nvcv::TENSOR_NHWC, 3); + RunU8C1PaddedStrideCase(nvcv::TENSOR_NHWC, nvcv::TENSOR_NCHW, 3); +} + +TEST(OpReformat, u8_c1_large_image_pitched_copy_preserves_canaries) +{ + constexpr int batches = 8; + constexpr int height = 900; + constexpr int width = 1600; + + RunU8C1PaddedStrideCase(nvcv::TENSOR_NCHW, nvcv::TENSOR_NHWC, batches, height, width); + RunU8C1PaddedStrideCase(nvcv::TENSOR_NHWC, nvcv::TENSOR_NCHW, batches, height, width); +} + +struct U8ReformatSpec +{ + nvcv::TensorLayout layout; + int batches; + int channels; + int height; + int width; + int rowStride; + int sampleStride; +}; + +static bool IsInterleaved(nvcv::TensorLayout layout) +{ + return layout == nvcv::TENSOR_NHWC || layout == nvcv::TENSOR_HWC; +} + +static bool HasBatch(nvcv::TensorLayout layout) +{ + return layout == nvcv::TENSOR_NHWC || layout == nvcv::TENSOR_NCHW; +} + +static U8ReformatSpec MakeU8ReformatSpec(nvcv::TensorLayout layout, int batches, int channels, int height, int width, + int rowPadding = 0, int samplePadding = 0) +{ + const int rowStride = width * (IsInterleaved(layout) ? channels : 1) + rowPadding; + const int sampleStride = rowStride * height * (layout == nvcv::TENSOR_NCHW ? channels : 1) + samplePadding; + return {layout, batches, channels, height, width, rowStride, sampleStride}; +} + +static size_t U8StorageSize(const U8ReformatSpec &spec) +{ + if (HasBatch(spec.layout)) + { + return static_cast(spec.batches) * spec.sampleStride; + } + return static_cast(spec.rowStride) * spec.height * (spec.layout == nvcv::TENSOR_CHW ? spec.channels : 1); +} + +static size_t U8Offset(const U8ReformatSpec &spec, int b, int y, int x, int c) +{ + if (spec.layout == nvcv::TENSOR_NHWC) + { + return static_cast(b) * spec.sampleStride + static_cast(y) * spec.rowStride + + static_cast(x) * spec.channels + c; + } + if (spec.layout == nvcv::TENSOR_NCHW) + { + return static_cast(b) * spec.sampleStride + static_cast(c) * spec.rowStride * spec.height + + static_cast(y) * spec.rowStride + x; + } + if (spec.layout == nvcv::TENSOR_HWC) + { + return static_cast(y) * spec.rowStride + static_cast(x) * spec.channels + c; + } + return static_cast(c) * spec.rowStride * spec.height + static_cast(y) * spec.rowStride + x; +} + +static nvcv::Tensor WrapU8Tensor(NVCVByte *basePtr, const U8ReformatSpec &spec) +{ + nvcv::TensorDataStridedCuda::Buffer buffer{}; + buffer.basePtr = basePtr; + + if (spec.layout == nvcv::TENSOR_NHWC) + { + buffer.strides[0] = spec.sampleStride; + buffer.strides[1] = spec.rowStride; + buffer.strides[2] = spec.channels; + buffer.strides[3] = 1; + return nvcv::TensorWrapData(nvcv::TensorDataStridedCuda{ + nvcv::TensorShape{{spec.batches, spec.height, spec.width, spec.channels}, "NHWC"}, + nvcv::TYPE_U8, buffer + }); + } + if (spec.layout == nvcv::TENSOR_NCHW) + { + buffer.strides[0] = spec.sampleStride; + buffer.strides[1] = spec.rowStride * spec.height; + buffer.strides[2] = spec.rowStride; + buffer.strides[3] = 1; + return nvcv::TensorWrapData(nvcv::TensorDataStridedCuda{ + nvcv::TensorShape{{spec.batches, spec.channels, spec.height, spec.width}, "NCHW"}, + nvcv::TYPE_U8, buffer + }); + } + if (spec.layout == nvcv::TENSOR_HWC) + { + buffer.strides[0] = spec.rowStride; + buffer.strides[1] = spec.channels; + buffer.strides[2] = 1; + return nvcv::TensorWrapData(nvcv::TensorDataStridedCuda{ + nvcv::TensorShape{{spec.height, spec.width, spec.channels}, "HWC"}, + nvcv::TYPE_U8, buffer + }); + } + + buffer.strides[0] = spec.rowStride * spec.height; + buffer.strides[1] = spec.rowStride; + buffer.strides[2] = 1; + return nvcv::TensorWrapData(nvcv::TensorDataStridedCuda{ + nvcv::TensorShape{{spec.channels, spec.height, spec.width}, "CHW"}, + nvcv::TYPE_U8, buffer + }); +} + +static void SetU8ReformatPixel(std::vector &src, std::vector &expected, const U8ReformatSpec &srcSpec, + const U8ReformatSpec &dstSpec, int b, int y, int x) +{ + for (int c = 0; c < srcSpec.channels; ++c) + { + const auto value = static_cast(b * 73 + y * 19 + x * 7 + c * 41 + 11); + src[U8Offset(srcSpec, b, y, x, c)] = value; + expected[U8Offset(dstSpec, b, y, x, c)] = value; + } +} + +static void RunU8BitExactCase(nvcv::TensorLayout srcLayout, nvcv::TensorLayout dstLayout, int batches, int channels, + int height, int width) +{ + ASSERT_EQ(HasBatch(srcLayout), HasBatch(dstLayout)); + if (!HasBatch(srcLayout)) + { + ASSERT_EQ(batches, 1); + } + + const U8ReformatSpec srcSpec = MakeU8ReformatSpec(srcLayout, batches, channels, height, width); + const U8ReformatSpec dstSpec = MakeU8ReformatSpec(dstLayout, batches, channels, height, width); + + std::vector src(U8StorageSize(srcSpec), 0xA5); + std::vector dst(U8StorageSize(dstSpec), 0xD7); + std::vector expected = dst; + for (int b = 0; b < batches; ++b) + { + for (int y = 0; y < height; ++y) + { + for (int x = 0; x < width; ++x) + { + SetU8ReformatPixel(src, expected, srcSpec, dstSpec, b, y, x); + } + } + } + + NVCVByte *srcPtr = nullptr; + NVCVByte *dstPtr = nullptr; + ASSERT_EQ(cudaSuccess, cudaMalloc(&srcPtr, src.size())); + ASSERT_EQ(cudaSuccess, cudaMalloc(&dstPtr, dst.size())); + ASSERT_EQ(cudaSuccess, cudaMemcpy(srcPtr, src.data(), src.size(), cudaMemcpyHostToDevice)); + ASSERT_EQ(cudaSuccess, cudaMemcpy(dstPtr, dst.data(), dst.size(), cudaMemcpyHostToDevice)); + + nvcv::Tensor srcTensor = WrapU8Tensor(srcPtr, srcSpec); + nvcv::Tensor dstTensor = WrapU8Tensor(dstPtr, dstSpec); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + cvcuda::Reformat op; + EXPECT_NO_THROW(op(stream, srcTensor, dstTensor)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + std::vector got(expected.size()); + ASSERT_EQ(cudaSuccess, cudaMemcpy(got.data(), dstPtr, got.size(), cudaMemcpyDeviceToHost)); + EXPECT_EQ(expected, got); + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + ASSERT_EQ(cudaSuccess, cudaFree(srcPtr)); + ASSERT_EQ(cudaSuccess, cudaFree(dstPtr)); +} + +TEST(OpReformat, u8_c2_odd_tail_bit_exact) +{ + RunU8BitExactCase(nvcv::TENSOR_NCHW, nvcv::TENSOR_NHWC, 3, 2, 19, 131); + RunU8BitExactCase(nvcv::TENSOR_NHWC, nvcv::TENSOR_NCHW, 3, 2, 19, 131); +} + +TEST(OpReformat, u8_c1_contiguous_rank4_copy) +{ + RunU8BitExactCase(nvcv::TENSOR_NCHW, nvcv::TENSOR_NHWC, 3, 1, 5, 37); + RunU8BitExactCase(nvcv::TENSOR_NHWC, nvcv::TENSOR_NCHW, 3, 1, 5, 37); +} + +TEST(OpReformat, u8_c1_contiguous_width_256_bit_exact) +{ + RunU8BitExactCase(nvcv::TENSOR_NCHW, nvcv::TENSOR_NHWC, 1, 1, 5, 256); + RunU8BitExactCase(nvcv::TENSOR_NHWC, nvcv::TENSOR_NCHW, 1, 1, 5, 256); +} + +TEST(OpReformat, u8_c1_contiguous_rank3_copy) +{ + RunU8BitExactCase(nvcv::TENSOR_CHW, nvcv::TENSOR_HWC, 1, 1, 5, 37); + RunU8BitExactCase(nvcv::TENSOR_HWC, nvcv::TENSOR_CHW, 1, 1, 5, 37); +} + +TEST(OpReformat_Negative, mismatched_extents) +{ + struct ExtentCase + { + const char *name; + int inSamples; + int inChannels; + int inRows; + int inCols; + int outSamples; + int outChannels; + int outRows; + int outCols; + }; + + const std::vector testCases = { + { "samples", 1, 1, 5, 37, 2, 1, 5, 37}, + {"channels", 1, 4, 5, 37, 1, 3, 5, 37}, + { "rows", 1, 1, 4, 37, 1, 1, 5, 37}, + { "cols", 1, 1, 5, 36, 1, 1, 5, 37}, + }; + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + cvcuda::Reformat reformatOp; + for (const ExtentCase &testCase : testCases) + { + SCOPED_TRACE(testCase.name); + + nvcv::Tensor inTensor( + nvcv::TensorShape{ + {testCase.inSamples, testCase.inChannels, testCase.inRows, testCase.inCols}, + "NCHW" + }, + nvcv::TYPE_U8); + nvcv::Tensor outTensor( + nvcv::TensorShape{ + {testCase.outSamples, testCase.outRows, testCase.outCols, testCase.outChannels}, + "NHWC" + }, + nvcv::TYPE_U8); + + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&reformatOp, &stream, &inTensor, &outTensor] + { reformatOp(stream, inTensor, outTensor); })); + } + + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + // clang-format off NVCV_TEST_SUITE_P(OpReformat_Negative, test::ValueList{ // inFmt, outFmt, inputBatches, outputBatches @@ -210,7 +575,8 @@ TEST_P(OpReformat_Negative, op) // run operator cvcuda::Reformat reformatOp; - EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&] { reformatOp(stream, inTensor, outTensor); })); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&reformatOp, &stream, &inTensor, &outTensor] + { reformatOp(stream, inTensor, outTensor); })); ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); diff --git a/tests/cvcuda/system/TestOpRemap.cpp b/tests/cvcuda/system/TestOpRemap.cpp index c919d7880..b6865390e 100644 --- a/tests/cvcuda/system/TestOpRemap.cpp +++ b/tests/cvcuda/system/TestOpRemap.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -36,12 +36,39 @@ namespace cuda = nvcv::cuda; namespace test = nvcv::test; namespace ttype = nvcv::test::type; -static std::default_random_engine g_rng(std::random_device{}()); +// Fixed seed: random_device made tests non-deterministic across CI runs and +// occasionally produced ill-conditioned numerical inputs that exceeded +// EXPECT_NEAR tolerances on rare-config CI. Use a known-good fixed seed. +static std::default_random_engine &Rng() +{ + static std::default_random_engine rng(12345); + return rng; +} + +static int ScaledSize(int size, double scale) +{ + return static_cast(static_cast(size) * scale); +} template using uniform_distribution = std::conditional_t, std::uniform_int_distribution, std::uniform_real_distribution>; +template +static void FillRandomValue(std::vector &vec, const Strides &strides, Coord coord, Distribution &rand) +{ + for (int k = 0; k < cuda::NumElements; ++k) + { + cuda::GetElement(test::ValueAt(vec, strides, coord), k) = rand(Rng()); + } +} + +static void FillRandomMapValue(std::vector &mapVec, const long3 &mapStrides, int3 coord, + std::uniform_real_distribution &randf) +{ + test::ValueAt(mapVec, mapStrides, coord) = float2{randf(Rng()), randf(Rng())}; +} + template void Remap(const std::vector &src, std::vector &dst, const std::vector &map, const long3 &srcStrides, const long3 &dstStrides, const long3 &mapStrides, const int3 &srcShape, @@ -52,12 +79,19 @@ void Remap(const std::vector &src, std::vector &dst, const std constexpr NVCVBorderType MB = NVCV_BORDER_REPLICATE; - const int2 mapSize{mapShape.x, mapShape.y}, srcSize{srcShape.x, srcShape.y}, dstSize{dstShape.x, dstShape.y}; - const float2 mapAS{0.f, 0.f}, srcAS{0.f, 0.f}, mapBV{0.f, 0.f}; // map area scale and border values are not used + const int2 mapSize{mapShape.x, mapShape.y}; + const int2 srcSize{srcShape.x, srcShape.y}; + const int2 dstSize{dstShape.x, dstShape.y}; + const float2 mapAS{0.f, 0.f}; + const float2 srcAS{0.f, 0.f}; + const float2 mapBV{0.f, 0.f}; // map area scale and border values are not used const ValueType srcBV = cuda::DropCast>(cuda::StaticCast(borderValue)); - float2 srcScale, mapScale, valScale, srcOffset; + float2 srcScale; + float2 mapScale; + float2 valScale; + float2 srcOffset; float dstOffset; if (mapValueType == NVCV_REMAP_ABSOLUTE) @@ -213,16 +247,13 @@ TYPED_TEST(OpRemap, correct_output) for (int z = 0; z < srcShape.z; ++z) for (int y = 0; y < srcShape.y; ++y) - for (int x = 0; x < srcShape.x; ++x) - for (int k = 0; k < cuda::NumElements; ++k) - cuda::GetElement(test::ValueAt(srcVec, srcStrides, int3{x, y, z}), k) = rand(g_rng); + for (int x = 0; x < srcShape.x; ++x) FillRandomValue(srcVec, srcStrides, int3{x, y, z}, rand); std::uniform_real_distribution randf(-1.f, 1.f); for (int z = 0; z < mapShape.z; ++z) for (int y = 0; y < mapShape.y; ++y) - for (int x = 0; x < mapShape.x; ++x) - test::ValueAt(mapVec, mapStrides, int3{x, y, z}) = float2{randf(g_rng), randf(g_rng)}; + for (int x = 0; x < mapShape.x; ++x) FillRandomMapValue(mapVec, mapStrides, int3{x, y, z}, randf); ASSERT_EQ(cudaSuccess, cudaMemcpy(srcData->basePtr(), srcVec.data(), srcBufSize, cudaMemcpyHostToDevice)); ASSERT_EQ(cudaSuccess, cudaMemcpy(mapData->basePtr(), mapVec.data(), mapBufSize, cudaMemcpyHostToDevice)); @@ -274,8 +305,8 @@ TYPED_TEST(OpRemap, varshape_correct_output) std::vector> srcVec(srcShape.z); - std::uniform_int_distribution srcRandW(srcShape.x * 0.8, srcShape.x * 1.2); - std::uniform_int_distribution srcRandH(srcShape.y * 0.8, srcShape.y * 1.2); + std::uniform_int_distribution srcRandW(ScaledSize(srcShape.x, 0.8), ScaledSize(srcShape.x, 1.2)); + std::uniform_int_distribution srcRandH(ScaledSize(srcShape.y, 0.8), ScaledSize(srcShape.y, 1.2)); uniform_distribution rand(BT{0}, std::is_integral_v ? cuda::TypeTraits::max : BT{1}); @@ -283,27 +314,26 @@ TYPED_TEST(OpRemap, varshape_correct_output) for (int z = 0; z < srcShape.z; ++z) { - imgSrc.emplace_back(nvcv::Size2D{srcRandW(g_rng), srcRandH(g_rng)}, imgFormat); + imgSrc.emplace_back(nvcv::Size2D{srcRandW(Rng()), srcRandH(Rng())}, imgFormat); auto imgData = imgSrc[z].exportData(); ASSERT_NE(imgData, nvcv::NullOpt); - int srcRowStride = imgData->plane(0).rowStride; - long2 srcStrides = long2{srcRowStride, sizeof(ValueType)}; + int srcRowStride = imgData->plane(0).rowStride; + auto srcStrides = long2{srcRowStride, sizeof(ValueType)}; srcVec[z].resize(srcRowStride * imgSrc[z].size().h); for (int y = 0; y < imgSrc[z].size().h; ++y) for (int x = 0; x < imgSrc[z].size().w; ++x) - for (int k = 0; k < cuda::NumElements; ++k) - cuda::GetElement(test::ValueAt(srcVec[z], srcStrides, int2{x, y}), k) = rand(g_rng); + FillRandomValue(srcVec[z], srcStrides, int2{x, y}, rand); ASSERT_EQ(cudaSuccess, cudaMemcpy2D(imgData->plane(0).basePtr, srcRowStride, srcVec[z].data(), srcRowStride, srcRowStride, imgSrc[z].size().h, cudaMemcpyHostToDevice)); } - std::uniform_int_distribution dstRandW(dstShape.x * 0.8, dstShape.x * 1.2); - std::uniform_int_distribution dstRandH(dstShape.y * 0.8, dstShape.y * 1.2); + std::uniform_int_distribution dstRandW(ScaledSize(dstShape.x, 0.8), ScaledSize(dstShape.x, 1.2)); + std::uniform_int_distribution dstRandH(ScaledSize(dstShape.y, 0.8), ScaledSize(dstShape.y, 1.2)); nvcv::ImageBatchVarShape batchSrc(srcShape.z); batchSrc.pushBack(imgSrc.begin(), imgSrc.end()); @@ -311,7 +341,7 @@ TYPED_TEST(OpRemap, varshape_correct_output) std::vector imgDst; for (int z = 0; z < dstShape.z; ++z) { - imgDst.emplace_back(nvcv::Size2D{dstRandW(g_rng), dstRandH(g_rng)}, imgFormat); + imgDst.emplace_back(nvcv::Size2D{dstRandW(Rng()), dstRandH(Rng())}, imgFormat); } nvcv::ImageBatchVarShape batchDst(dstShape.z); batchDst.pushBack(imgDst.begin(), imgDst.end()); @@ -324,7 +354,7 @@ TYPED_TEST(OpRemap, varshape_correct_output) auto mapAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*mapData); ASSERT_TRUE(mapAccess); - long3 mapStrides = long3{mapAccess->sampleStride(), mapAccess->rowStride(), mapAccess->colStride()}; + auto mapStrides = long3{mapAccess->sampleStride(), mapAccess->rowStride(), mapAccess->colStride()}; mapStrides.x = (mapData->rank() == 3) ? mapAccess->numRows() * mapAccess->rowStride() : mapStrides.x; @@ -336,8 +366,7 @@ TYPED_TEST(OpRemap, varshape_correct_output) for (int z = 0; z < mapShape.z; ++z) for (int y = 0; y < mapShape.y; ++y) - for (int x = 0; x < mapShape.x; ++x) - test::ValueAt(mapVec, mapStrides, int3{x, y, z}) = float2{randf(g_rng), randf(g_rng)}; + for (int x = 0; x < mapShape.x; ++x) FillRandomMapValue(mapVec, mapStrides, int3{x, y, z}, randf); ASSERT_EQ(cudaSuccess, cudaMemcpy(mapData->basePtr(), mapVec.data(), mapBufSize, cudaMemcpyHostToDevice)); @@ -384,36 +413,295 @@ TYPED_TEST(OpRemap, varshape_correct_output) } } +// ============================================================================= +// Planar (NCHW/CHW) parity: remap each channel plane independently and require +// the (re-interleaved) planar output to match the interleaved output bit-for-bit. +// Uses a per-sample map (mapNumSamples == N) to exercise the planar map indexing, +// and a uniform/zero border value (the planar/interleaved constant-border results +// only agree for a uniform value, which the operator enforces). +// ============================================================================= + +namespace { + +template +std::vector
InterleavedToPlanar(const std::vector
&hwc, int w, int h, int channels) +{ + std::vector
chw(hwc.size()); + const int hw = w * h; + for (int p = 0; p < hw; ++p) + for (int c = 0; c < channels; ++c) chw[c * hw + p] = hwc[p * channels + c]; + return chw; +} + +template +std::vector
MakeDeterministicSrc(int count, int seed) +{ + std::vector
v(count); + for (int i = 0; i < count; ++i) v[i] = static_cast
((i * 7 + seed * 31 + 13) % 251); + return v; +} + +// Deterministic small relative-normalized displacements; the same map drives both layouts. +// FMT_2F32 is two interleaved F32 channels, so the host buffer is [x0,y0, x1,y1, ...]. +std::vector MakeDeterministicMap(int w, int h, int seed) +{ + std::vector m(static_cast(w) * h * 2); + for (int y = 0; y < h; ++y) + for (int x = 0; x < w; ++x) + { + m[(y * w + x) * 2 + 0] = ((x * 7 + seed) % 11) / 20.f - 0.25f; + m[(y * w + x) * 2 + 1] = ((y * 5 + seed) % 11) / 20.f - 0.25f; + } + return m; +} + +template +void RunRemapTensorParity(nvcv::ImageFormat interFmt, nvcv::ImageFormat planarFmt, int W, int H, int N, + NVCVInterpolationType srcInterp, NVCVInterpolationType mapInterp, NVCVBorderType border) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + const int channels = planarFmt.numChannels(); + + nvcv::Tensor srcI = nvcv::util::CreateTensor(N, W, H, interFmt); + nvcv::Tensor dstI = nvcv::util::CreateTensor(N, W, H, interFmt); + nvcv::Tensor srcP = nvcv::util::CreateTensor(N, W, H, planarFmt); + nvcv::Tensor dstP = nvcv::util::CreateTensor(N, W, H, planarFmt); + nvcv::Tensor mapT = nvcv::util::CreateTensor(N, W, H, nvcv::FMT_2F32); + + auto srcIData = srcI.exportData(); + auto dstIData = dstI.exportData(); + auto srcPData = srcP.exportData(); + auto dstPData = dstP.exportData(); + auto mapData = mapT.exportData(); + ASSERT_TRUE(srcIData && dstIData && srcPData && dstPData && mapData); + + for (int n = 0; n < N; ++n) + { + std::vector hwc = MakeDeterministicSrc(W * H * channels, n); + std::vector chw = InterleavedToPlanar(hwc, W, H, channels); + nvcv::util::SetImageTensorFromVector(*srcIData, hwc, n); + nvcv::util::SetImageTensorFromVector(*srcPData, chw, n); + + std::vector mp = MakeDeterministicMap(W, H, n); + nvcv::util::SetImageTensorFromVector(*mapData, mp, n); + } + + cvcuda::Remap op; + EXPECT_NO_THROW(op(stream, srcI, dstI, mapT, srcInterp, mapInterp, NVCV_REMAP_RELATIVE_NORMALIZED, false, border, + float4{0.f, 0.f, 0.f, 0.f})); + EXPECT_NO_THROW(op(stream, srcP, dstP, mapT, srcInterp, mapInterp, NVCV_REMAP_RELATIVE_NORMALIZED, false, border, + float4{0.f, 0.f, 0.f, 0.f})); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + for (int n = 0; n < N; ++n) + { + SCOPED_TRACE(n); + std::vector outI; + std::vector outP; + nvcv::util::GetImageVectorFromTensor(*dstIData, n, outI); + nvcv::util::GetImageVectorFromTensor(*dstPData, n, outP); + EXPECT_EQ(InterleavedToPlanar(outI, W, H, channels), outP); + } + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +} // namespace + +TEST(OpRemapPlanar, tensor_rgb8_nearest_matches_interleaved) +{ + RunRemapTensorParity(nvcv::FMT_RGB8, nvcv::FMT_RGB8p, 33, 17, 2, NVCV_INTERP_NEAREST, NVCV_INTERP_NEAREST, + NVCV_BORDER_REPLICATE); +} + +TEST(OpRemapPlanar, tensor_rgb8_cubic_constant_matches_interleaved) +{ + // CUBIC source interpolation + uniform (zero) constant border. + RunRemapTensorParity(nvcv::FMT_RGB8, nvcv::FMT_RGB8p, 24, 20, 2, NVCV_INTERP_CUBIC, NVCV_INTERP_LINEAR, + NVCV_BORDER_CONSTANT); +} + +TEST(OpRemapPlanar, tensor_rgba8_linear_matches_interleaved) +{ + RunRemapTensorParity(nvcv::FMT_RGBA8, nvcv::FMT_RGBA8p, 16, 16, 1, NVCV_INTERP_LINEAR, NVCV_INTERP_NEAREST, + NVCV_BORDER_REFLECT); +} + +namespace { + +template +void UploadImagePlanes(const nvcv::Image &img, const std::vector &planes, int w, int h, int channels) +{ + auto data = img.exportData(); + ASSERT_NE(data, nvcv::NullOpt); + for (int c = 0; c < channels; ++c) + { + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(data->plane(c).basePtr, data->plane(c).rowStride, + planes.data() + static_cast(c) * w * h, w * sizeof(T), + w * sizeof(T), h, cudaMemcpyHostToDevice)); + } +} + +// Interleaved image stores one plane of W*channels elements per row; treat it as a single +// "plane" of width W*channels to upload/download the HWC buffer in one shot. +template +void UploadImageInterleaved(const nvcv::Image &img, const std::vector &hwc, int w, int h, int channels) +{ + auto data = img.exportData(); + ASSERT_NE(data, nvcv::NullOpt); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(data->plane(0).basePtr, data->plane(0).rowStride, hwc.data(), + w * channels * sizeof(T), w * channels * sizeof(T), h, cudaMemcpyHostToDevice)); +} + +template +std::vector DownloadImagePlanes(const nvcv::Image &img, int w, int h, int channels) +{ + auto data = img.exportData(); + EXPECT_NE(data, nvcv::NullOpt); + std::vector planes(static_cast(w) * h * channels); + for (int c = 0; c < channels; ++c) + { + EXPECT_EQ(cudaSuccess, + cudaMemcpy2D(planes.data() + static_cast(c) * w * h, w * sizeof(T), data->plane(c).basePtr, + data->plane(c).rowStride, w * sizeof(T), h, cudaMemcpyDeviceToHost)); + } + return planes; +} + +template +std::vector DownloadImageInterleaved(const nvcv::Image &img, int w, int h, int channels) +{ + auto data = img.exportData(); + EXPECT_NE(data, nvcv::NullOpt); + std::vector hwc(static_cast(w) * h * channels); + EXPECT_EQ(cudaSuccess, cudaMemcpy2D(hwc.data(), w * channels * sizeof(T), data->plane(0).basePtr, + data->plane(0).rowStride, w * channels * sizeof(T), h, cudaMemcpyDeviceToHost)); + return hwc; +} + +template +void RunRemapVarShapeParity(nvcv::ImageFormat interFmt, nvcv::ImageFormat planarFmt, int W, int H, int N, + NVCVInterpolationType srcInterp, NVCVInterpolationType mapInterp, NVCVBorderType border) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + const int channels = planarFmt.numChannels(); + + std::vector srcIimgs; + std::vector dstIimgs; + std::vector srcPimgs; + std::vector dstPimgs; + for (int n = 0; n < N; ++n) + { + srcIimgs.emplace_back(nvcv::Size2D{W, H}, interFmt); + dstIimgs.emplace_back(nvcv::Size2D{W, H}, interFmt); + srcPimgs.emplace_back(nvcv::Size2D{W, H}, planarFmt); + dstPimgs.emplace_back(nvcv::Size2D{W, H}, planarFmt); + + std::vector hwc = MakeDeterministicSrc(W * H * channels, n); + UploadImageInterleaved(srcIimgs[n], hwc, W, H, channels); + UploadImagePlanes(srcPimgs[n], InterleavedToPlanar(hwc, W, H, channels), W, H, channels); + } + + auto makeBatch = [&](std::vector &imgs) + { + nvcv::ImageBatchVarShape b(N); + b.pushBack(imgs.begin(), imgs.end()); + return b; + }; + nvcv::ImageBatchVarShape bSrcI = makeBatch(srcIimgs); + nvcv::ImageBatchVarShape bDstI = makeBatch(dstIimgs); + nvcv::ImageBatchVarShape bSrcP = makeBatch(srcPimgs); + nvcv::ImageBatchVarShape bDstP = makeBatch(dstPimgs); + + nvcv::Tensor mapT = nvcv::util::CreateTensor(N, W, H, nvcv::FMT_2F32); + auto mapData = mapT.exportData(); + ASSERT_TRUE(mapData); + for (int n = 0; n < N; ++n) + { + std::vector mp = MakeDeterministicMap(W, H, n); + nvcv::util::SetImageTensorFromVector(*mapData, mp, n); + } + + cvcuda::Remap op; + EXPECT_NO_THROW(op(stream, bSrcI, bDstI, mapT, srcInterp, mapInterp, NVCV_REMAP_RELATIVE_NORMALIZED, false, border, + float4{0.f, 0.f, 0.f, 0.f})); + EXPECT_NO_THROW(op(stream, bSrcP, bDstP, mapT, srcInterp, mapInterp, NVCV_REMAP_RELATIVE_NORMALIZED, false, border, + float4{0.f, 0.f, 0.f, 0.f})); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + for (int n = 0; n < N; ++n) + { + SCOPED_TRACE(n); + std::vector outI = DownloadImageInterleaved(dstIimgs[n], W, H, channels); + std::vector outP = DownloadImagePlanes(dstPimgs[n], W, H, channels); + EXPECT_EQ(InterleavedToPlanar(outI, W, H, channels), outP); + } + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +} // namespace + +TEST(OpRemapPlanar, varshape_rgb8_nearest_matches_interleaved) +{ + RunRemapVarShapeParity(nvcv::FMT_RGB8, nvcv::FMT_RGB8p, 28, 19, 2, NVCV_INTERP_NEAREST, + NVCV_INTERP_NEAREST, NVCV_BORDER_REPLICATE); +} + +TEST(OpRemapPlanar, varshape_rgba8_linear_matches_interleaved) +{ + RunRemapVarShapeParity(nvcv::FMT_RGBA8, nvcv::FMT_RGBA8p, 20, 20, 2, NVCV_INTERP_LINEAR, + NVCV_INTERP_LINEAR, NVCV_BORDER_CONSTANT); +} + #define NVCV_IMAGE_FORMAT_INVALID_MAP NVCV_DETAIL_MAKE_NONCOLOR_FMT2(PL, FLOAT, XYZW, ASSOCIATED, X32_Y32, X32_Y32) -// clang-format off -NVCV_TEST_SUITE_P(OpRemap_Negative, test::ValueList{ - // inputShape, dstShape, mapShape, inputFormat, outputFormat, mapFormat, inputInterp, mapInterp, borderValue - {42, 42, 1, 42, 42, 1, 2, 2, 1, nvcv::FMT_RGB8, nvcv::FMT_U8, nvcv::FMT_2F32, NVCV_INTERP_NEAREST, NVCV_INTERP_NEAREST, NVCV_BORDER_CONSTANT}, - {42, 42, 2, 42, 42, 1, 2, 2, 1, nvcv::FMT_RGB8, nvcv::FMT_RGB8, nvcv::FMT_2F32, NVCV_INTERP_NEAREST, NVCV_INTERP_NEAREST, NVCV_BORDER_CONSTANT}, - {42, 42, 2, 42, 42, 2, 2, 2, 5, nvcv::FMT_RGB8, nvcv::FMT_RGB8, nvcv::FMT_2F32, NVCV_INTERP_NEAREST, NVCV_INTERP_NEAREST, NVCV_BORDER_CONSTANT}, - {42, 42, 1, 42, 42, 1, 2, 2, 1, nvcv::FMT_RGB8p, nvcv::FMT_RGB8, nvcv::FMT_2F32, NVCV_INTERP_NEAREST, NVCV_INTERP_NEAREST, NVCV_BORDER_CONSTANT}, - {42, 42, 1, 42, 42, 1, 2, 2, 1, nvcv::FMT_RGB8p, nvcv::FMT_RGB8p, nvcv::FMT_2F32, NVCV_INTERP_NEAREST, NVCV_INTERP_NEAREST, NVCV_BORDER_CONSTANT}, - {42, 42, 1, 42, 42, 1, 2, 2, 1, nvcv::FMT_RGB8, nvcv::FMT_RGBf32, nvcv::FMT_2F32, NVCV_INTERP_NEAREST, NVCV_INTERP_NEAREST, NVCV_BORDER_CONSTANT}, - {42, 42, 1, 42, 42, 1, 2, 2, 1, nvcv::FMT_RGB8, nvcv::FMT_RGB8, nvcv::FMT_RGBf32, NVCV_INTERP_NEAREST, NVCV_INTERP_NEAREST, NVCV_BORDER_CONSTANT}, - {42, 42, 1, 42, 42, 1, 2, 2, 1, nvcv::FMT_RGBf16, nvcv::FMT_RGBf16, nvcv::FMT_2F32, NVCV_INTERP_NEAREST, NVCV_INTERP_NEAREST, NVCV_BORDER_CONSTANT}, +static auto OpRemapNegativeParams() +{ + test::ValueList + params{ + // inputShape, dstShape, mapShape, inputFormat, outputFormat, mapFormat, inputInterp, mapInterp, borderValue + {42, 42, 1, 42, 42, 1, 2, 2, 1, nvcv::FMT_RGB8, nvcv::FMT_U8, nvcv::FMT_2F32, NVCV_INTERP_NEAREST, + NVCV_INTERP_NEAREST, NVCV_BORDER_CONSTANT}, + {42, 42, 2, 42, 42, 1, 2, 2, 1, nvcv::FMT_RGB8, nvcv::FMT_RGB8, nvcv::FMT_2F32, NVCV_INTERP_NEAREST, + NVCV_INTERP_NEAREST, NVCV_BORDER_CONSTANT}, + {42, 42, 2, 42, 42, 2, 2, 2, 5, nvcv::FMT_RGB8, nvcv::FMT_RGB8, nvcv::FMT_2F32, NVCV_INTERP_NEAREST, + NVCV_INTERP_NEAREST, NVCV_BORDER_CONSTANT}, + {42, 42, 1, 42, 42, 1, 2, 2, 1, nvcv::FMT_RGB8p, nvcv::FMT_RGB8, nvcv::FMT_2F32, NVCV_INTERP_NEAREST, + NVCV_INTERP_NEAREST, NVCV_BORDER_CONSTANT}, + {42, 42, 1, 42, 42, 1, 2, 2, 1, nvcv::FMT_RGB8, nvcv::FMT_RGBf32, nvcv::FMT_2F32, NVCV_INTERP_NEAREST, + NVCV_INTERP_NEAREST, NVCV_BORDER_CONSTANT}, + {42, 42, 1, 42, 42, 1, 2, 2, 1, nvcv::FMT_RGB8, nvcv::FMT_RGB8, nvcv::FMT_RGBf32, NVCV_INTERP_NEAREST, + NVCV_INTERP_NEAREST, NVCV_BORDER_CONSTANT}, + {42, 42, 1, 42, 42, 1, 2, 2, 1, nvcv::FMT_RGBf16, nvcv::FMT_RGBf16, nvcv::FMT_2F32, NVCV_INTERP_NEAREST, + NVCV_INTERP_NEAREST, NVCV_BORDER_CONSTANT}, + }; #ifndef ENABLE_SANITIZER - {42, 42, 1, 42, 42, 1, 2, 2, 1, nvcv::FMT_RGB8, nvcv::FMT_RGB8, nvcv::FMT_2F32, NVCV_INTERP_NEAREST, NVCV_INTERP_NEAREST, static_cast(255)}, - {42, 42, 1, 42, 42, 1, 2, 2, 1, nvcv::FMT_RGB8, nvcv::FMT_RGB8, nvcv::FMT_2F32, NVCV_INTERP_NEAREST, static_cast(255), NVCV_BORDER_CONSTANT}, - {42, 42, 1, 42, 42, 1, 2, 2, 1, nvcv::FMT_RGB8, nvcv::FMT_RGB8, nvcv::FMT_2F32, static_cast(255), NVCV_INTERP_NEAREST, NVCV_BORDER_CONSTANT}, + params.emplace_back(42, 42, 1, 42, 42, 1, 2, 2, 1, nvcv::FMT_RGB8, nvcv::FMT_RGB8, nvcv::FMT_2F32, + NVCV_INTERP_NEAREST, NVCV_INTERP_NEAREST, static_cast(255)); + params.emplace_back(42, 42, 1, 42, 42, 1, 2, 2, 1, nvcv::FMT_RGB8, nvcv::FMT_RGB8, nvcv::FMT_2F32, + NVCV_INTERP_NEAREST, static_cast(255), NVCV_BORDER_CONSTANT); + params.emplace_back(42, 42, 1, 42, 42, 1, 2, 2, 1, nvcv::FMT_RGB8, nvcv::FMT_RGB8, nvcv::FMT_2F32, + static_cast(255), NVCV_INTERP_NEAREST, NVCV_BORDER_CONSTANT); #endif -}); + return params; +} -NVCV_TEST_SUITE_P(OpRemapVarshape_Negative, test::ValueList{ - // inputNumImages, outputNumImages, mapNumSamples, inputFormat, outputFormat, mapFormat - {2, 1, 1, nvcv::FMT_RGB8, nvcv::FMT_RGB8, nvcv::FMT_2F32}, - {1, 1, 2, nvcv::FMT_RGB8, nvcv::FMT_RGB8, nvcv::FMT_2F32}, - {1, 1, 1, nvcv::FMT_RGB8, nvcv::FMT_RGB8, nvcv::FMT_RGBf32}, - {1, 1, 1, nvcv::FMT_RGB8p, nvcv::FMT_RGB8, nvcv::FMT_2F32}, - {1, 1, 1, nvcv::FMT_RGB8p, nvcv::FMT_RGB8p, nvcv::FMT_2F32}, -}); +NVCV_TEST_SUITE_P(OpRemap_Negative, OpRemapNegativeParams()); -// clang-format on +NVCV_TEST_SUITE_P(OpRemapVarshape_Negative, + test::ValueList{ + // inputNumImages, outputNumImages, mapNumSamples, inputFormat, outputFormat, mapFormat + {2, 1, 1, nvcv::FMT_RGB8, nvcv::FMT_RGB8, nvcv::FMT_2F32}, + {1, 1, 2, nvcv::FMT_RGB8, nvcv::FMT_RGB8, nvcv::FMT_2F32}, + {1, 1, 1, nvcv::FMT_RGB8, nvcv::FMT_RGB8, nvcv::FMT_RGBf32}, + {1, 1, 1, nvcv::FMT_RGB8p, nvcv::FMT_RGB8, nvcv::FMT_2F32}, +}); TEST_P(OpRemap_Negative, op) { @@ -443,7 +731,8 @@ TEST_P(OpRemap_Negative, op) cvcuda::Remap op; EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall( - [&] + [&op, &stream, &srcTensor, &dstTensor, &mapTensor, &kSrcInterp, + &kMapInterp, &kMapValueType, &kAlignCorners, &kBorderType, &borderValue] { op(stream, srcTensor, dstTensor, mapTensor, kSrcInterp, kMapInterp, kMapValueType, kAlignCorners, kBorderType, borderValue); @@ -481,16 +770,16 @@ TEST_P(OpRemapVarshape_Negative, op) std::vector imgSrc; - std::uniform_int_distribution srcRandW(srcShape.x * 0.8, srcShape.x * 1.2); - std::uniform_int_distribution srcRandH(srcShape.y * 0.8, srcShape.y * 1.2); + std::uniform_int_distribution srcRandW(ScaledSize(srcShape.x, 0.8), ScaledSize(srcShape.x, 1.2)); + std::uniform_int_distribution srcRandH(ScaledSize(srcShape.y, 0.8), ScaledSize(srcShape.y, 1.2)); for (int z = 0; z < srcShape.z; ++z) { - imgSrc.emplace_back(nvcv::Size2D{srcRandW(g_rng), srcRandH(g_rng)}, inputFmt); + imgSrc.emplace_back(nvcv::Size2D{srcRandW(Rng()), srcRandH(Rng())}, inputFmt); } - std::uniform_int_distribution dstRandW(dstShape.x * 0.8, dstShape.x * 1.2); - std::uniform_int_distribution dstRandH(dstShape.y * 0.8, dstShape.y * 1.2); + std::uniform_int_distribution dstRandW(ScaledSize(dstShape.x, 0.8), ScaledSize(dstShape.x, 1.2)); + std::uniform_int_distribution dstRandH(ScaledSize(dstShape.y, 0.8), ScaledSize(dstShape.y, 1.2)); nvcv::ImageBatchVarShape batchSrc(srcShape.z); batchSrc.pushBack(imgSrc.begin(), imgSrc.end()); @@ -498,7 +787,7 @@ TEST_P(OpRemapVarshape_Negative, op) std::vector imgDst; for (int z = 0; z < dstShape.z; ++z) { - imgDst.emplace_back(nvcv::Size2D{dstRandW(g_rng), dstRandH(g_rng)}, outputFmt); + imgDst.emplace_back(nvcv::Size2D{dstRandW(Rng()), dstRandH(Rng())}, outputFmt); } nvcv::ImageBatchVarShape batchDst(dstShape.z); batchDst.pushBack(imgDst.begin(), imgDst.end()); @@ -507,7 +796,8 @@ TEST_P(OpRemapVarshape_Negative, op) cvcuda::Remap op; EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall( - [&] + [&op, &stream, &batchSrc, &batchDst, &mapTensor, &kSrcInterp, + &kMapInterp, &kMapValueType, &kAlignCorners, &kBorderType, &borderValue] { op(stream, batchSrc, batchDst, mapTensor, kSrcInterp, kMapInterp, kMapValueType, kAlignCorners, kBorderType, borderValue); @@ -541,20 +831,20 @@ TEST(OpRemap_Negative, varshape_hasDifferentFormat) { fmt, nvcv::FMT_RGBA8} }; - for (auto testCase : testSet) + for (const auto &[inputFmtExtra, outputFmtExtra] : testSet) { - nvcv::ImageFormat inputFmtExtra = std::get<0>(testCase); - nvcv::ImageFormat outputFmtExtra = std::get<1>(testCase); - // Create input and output - std::default_random_engine randEng; - std::uniform_int_distribution rndSrcWidth(srcWidthBase * 0.8, srcWidthBase * 1.1); - std::uniform_int_distribution rndSrcHeight(srcHeightBase * 0.8, srcHeightBase * 1.1); - std::uniform_int_distribution rndDstWidth(dstWidthBase * 0.8, dstWidthBase * 1.1); - std::uniform_int_distribution rndDstHeight(dstHeightBase * 0.8, dstHeightBase * 1.1); + std::default_random_engine randEng; + std::uniform_int_distribution rndSrcWidth(ScaledSize(srcWidthBase, 0.8), ScaledSize(srcWidthBase, 1.1)); + std::uniform_int_distribution rndSrcHeight(ScaledSize(srcHeightBase, 0.8), ScaledSize(srcHeightBase, 1.1)); + std::uniform_int_distribution rndDstWidth(ScaledSize(dstWidthBase, 0.8), ScaledSize(dstWidthBase, 1.1)); + std::uniform_int_distribution rndDstHeight(ScaledSize(dstHeightBase, 0.8), ScaledSize(dstHeightBase, 1.1)); + + std::vector imgSrc; - std::vector imgSrc, imgDst; - std::vector srcSizes, dstSizes; + std::vector imgDst; + std::vector srcSizes; + std::vector dstSizes; // Create n-1 images with standard format for (int i = 0; i < numberOfImages - 1; ++i) @@ -589,7 +879,8 @@ TEST(OpRemap_Negative, varshape_hasDifferentFormat) cvcuda::Remap remapOp; EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall( - [&] + [&remapOp, &stream, &batchSrc, &batchDst, &mapTensor, &srcInterp, + &mapInterp, &mapValueType, &alignCorners, &borderType, &borderValue] { remapOp(stream, batchSrc, batchDst, mapTensor, srcInterp, mapInterp, mapValueType, alignCorners, borderType, diff --git a/tests/cvcuda/system/TestOpResize.cpp b/tests/cvcuda/system/TestOpResize.cpp index dfa64fc50..f7e2723f4 100644 --- a/tests/cvcuda/system/TestOpResize.cpp +++ b/tests/cvcuda/system/TestOpResize.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,6 +16,7 @@ */ #include "Definitions.hpp" +#include "PlanarParityUtils.hpp" #include "ResizeUtils.hpp" #include @@ -29,13 +30,20 @@ #include #include +#include #include +#include #include namespace cuda = nvcv::cuda; namespace test = nvcv::test; namespace t = ::testing; +static int ScaledSize(int size, double scale) +{ + return static_cast(static_cast(size) * scale); +} + // clang-format off #define NVCV_IMAGE_FORMAT_4U8 NVCV_DETAIL_MAKE_NONCOLOR_FMT1(PL, UNSIGNED, XYZW, ASSOCIATED, X8_Y8_Z8_W8) @@ -80,6 +88,25 @@ NVCV_TEST_SUITE_P(OpResize, test::ValueList rand(0, 255); srcVec[i].resize(srcHeight * srcVecRowStride); - std::generate(srcVec[i].begin(), srcVec[i].end(), [&]() { return rand(randEng); }); + std::ranges::generate(srcVec[i], [&rand, &randEng]() { return rand(randEng); }); // Copy input data to the GPU ASSERT_EQ(cudaSuccess, @@ -184,15 +211,15 @@ TEST_P(OpResize, tensor_correct_output) test::Resize(goldVec, dstVecRowStride, {dstWidth, dstHeight}, srcVec[i], srcVecRowStride, {srcWidth, srcHeight}, fmt, interpolation, false); - std::vector mae(testVec.size()); - for (size_t i = 0; i < mae.size(); ++i) + std::vector absDiff(testVec.size()); + for (size_t idx = 0; idx < absDiff.size(); ++idx) { - mae[i] = abs(static_cast(goldVec[i]) - static_cast(testVec[i])); + absDiff[idx] = abs(static_cast(goldVec[idx]) - static_cast(testVec[idx])); } - int maeThreshold = 1; + int maxAbsDiff = 1; - EXPECT_THAT(mae, t::Each(t::Le(maeThreshold))); + EXPECT_THAT(absDiff, t::Each(t::Le(maxAbsDiff))); } } @@ -214,16 +241,18 @@ TEST_P(OpResize, varshape_correct_output) // Create input and output std::default_random_engine randEng; - std::uniform_int_distribution rndSrcWidth(srcWidthBase * 0.8, srcWidthBase * 1.1); - std::uniform_int_distribution rndSrcHeight(srcHeightBase * 0.8, srcHeightBase * 1.1); + std::uniform_int_distribution rndSrcWidth(ScaledSize(srcWidthBase, 0.8), ScaledSize(srcWidthBase, 1.1)); + std::uniform_int_distribution rndSrcHeight(ScaledSize(srcHeightBase, 0.8), ScaledSize(srcHeightBase, 1.1)); - std::uniform_int_distribution rndDstWidth(dstWidthBase * 0.8, dstWidthBase * 1.1); - std::uniform_int_distribution rndDstHeight(dstHeightBase * 0.8, dstHeightBase * 1.1); + std::uniform_int_distribution rndDstWidth(ScaledSize(dstWidthBase, 0.8), ScaledSize(dstWidthBase, 1.1)); + std::uniform_int_distribution rndDstHeight(ScaledSize(dstHeightBase, 0.8), ScaledSize(dstHeightBase, 1.1)); - std::vector imgSrc, imgDst; + std::vector imgSrc; + + std::vector imgDst; // The size of the first image is fixed: to cover area fast code path imgSrc.emplace_back(nvcv::Size2D{srcWidthBase, srcHeightBase}, fmt); - imgDst.emplace_back(nvcv::Size2D{dstHeightBase, dstHeightBase}, fmt); + imgDst.emplace_back(nvcv::Size2D{dstWidthBase, dstHeightBase}, fmt); for (int i = 0; i < numberOfImages - 1; ++i) { imgSrc.emplace_back(nvcv::Size2D{rndSrcWidth(randEng), rndSrcHeight(randEng)}, fmt); @@ -255,7 +284,7 @@ TEST_P(OpResize, varshape_correct_output) std::uniform_int_distribution rand(0, 255); srcVec[i].resize(srcHeight * srcRowStride); - std::generate(srcVec[i].begin(), srcVec[i].end(), [&]() { return rand(randEng); }); + std::ranges::generate(srcVec[i], [&rand, &randEng]() { return rand(randEng); }); // Copy input data to the GPU ASSERT_EQ(cudaSuccess, @@ -305,16 +334,280 @@ TEST_P(OpResize, varshape_correct_output) fmt, interpolation, true); // maximum absolute error - std::vector mae(testVec.size()); - for (size_t i = 0; i < mae.size(); ++i) + std::vector absDiff(testVec.size()); + for (size_t idx = 0; idx < absDiff.size(); ++idx) + { + absDiff[idx] = abs(static_cast(goldVec[idx]) - static_cast(testVec[idx])); + } + + int maxAbsDiff = 1; + + EXPECT_THAT(absDiff, t::Each(t::Le(maxAbsDiff))); + } +} + +// ============================================================================= +// Cubic boundary interpolation tests +// +// These tests verify that bicubic resize correctly interpolates pixels near +// image boundaries using per-tap replicate-border clamping, instead of zeroing +// the fractional weight (which collapsed to nearest-neighbor at edges). +// ============================================================================= + +namespace { + +double CubicWeight(double A, double t) +{ + if (double at = std::abs(t); at <= 1.0) + return (A + 2.0) * at * at * at - (A + 3.0) * at * at + 1.0; + else if (at < 2.0) + return A * at * at * at - 5.0 * A * at * at + 8.0 * A * at - 4.0 * A; + return 0.0; +} + +double ReferenceCubicPixelReplicateBorder(const std::vector &src, int srcW, int srcH, int channels, int c, + int ix, int iy, const std::array &wx, + const std::array &wy) +{ + double val = 0.0; + for (int ky = 0; ky < 4; ky++) + { + int sy = std::clamp(iy + ky - 1, 0, srcH - 1); + for (int kx = 0; kx < 4; kx++) + { + int sx = std::clamp(ix + kx - 1, 0, srcW - 1); + val += src[sy * srcW * channels + sx * channels + c] * wx[kx] * wy[ky]; + } + } + return val; +} + +// Self-contained CPU reference for bicubic resize with replicate-border clamping. +// Does NOT depend on ResizeUtils (which had the same bug), so the test is +// independently correct even if both GPU kernel and ResizeUtils are broken. +void ReferenceCubicResizeReplicateBorder(std::vector &dst, int dstW, int dstH, + const std::vector &src, int srcW, int srcH, int channels) +{ + double scaleX = static_cast(srcW) / dstW; + double scaleY = static_cast(srcH) / dstH; + const double A = -0.75; + + dst.resize(dstH * dstW * channels); + + for (int dy = 0; dy < dstH; dy++) + { + for (int dx = 0; dx < dstW; dx++) + { + double srcY = (dy + 0.5) * scaleY - 0.5; + double srcX = (dx + 0.5) * scaleX - 0.5; + auto iy = static_cast(std::floor(srcY)); + auto ix = static_cast(std::floor(srcX)); + double fy = srcY - iy; + double fx = srcX - ix; + + std::array wy = {CubicWeight(A, fy + 1), CubicWeight(A, fy), CubicWeight(A, 1 - fy), + CubicWeight(A, 2 - fy)}; + std::array wx = {CubicWeight(A, fx + 1), CubicWeight(A, fx), CubicWeight(A, 1 - fx), + CubicWeight(A, 2 - fx)}; + + for (int c = 0; c < channels; c++) + { + double val = ReferenceCubicPixelReplicateBorder(src, srcW, srcH, channels, c, ix, iy, wx, wy); + // Clamp to [0, 255], no abs(). + val = std::rint(std::max(0.0, std::min(255.0, val))); + dst[dy * dstW * channels + dx * channels + c] = static_cast(val); + } + } + } +} + +// Create a gradient image where boundary pixels have distinct, non-uniform values. +// This makes boundary interpolation errors clearly visible: the old kernel would +// zero the fractional weight at edges, producing nearest-neighbor copies instead +// of proper cubic blends, yielding errors of 30-90+ on uint8 gradient images. +void FillGradientImage(std::vector &img, int w, int h, int channels) +{ + img.resize(h * w * channels); + for (int y = 0; y < h; y++) + { + for (int x = 0; x < w; x++) { - mae[i] = abs(static_cast(goldVec[i]) - static_cast(testVec[i])); + for (int c = 0; c < channels; c++) + { + int val = static_cast(255.0 * x / std::max(w - 1, 1)) + c * 30 + y * 10 / std::max(h - 1, 1); + img[y * w * channels + x * channels + c] = static_cast(std::clamp(val, 0, 255)); + } } + } +} + +} // anonymous namespace + +// clang-format off +NVCV_TEST_SUITE_P(OpResize_CubicBoundary, test::ValueList +{ + // srcW, srcH, dstW, dstH, channels + { 8, 8, 5, 5, 3}, // small: every output pixel near boundary + { 6, 6, 13, 13, 3}, // upscale: boundary taps dominate corners + { 10, 4, 7, 11, 3}, // asymmetric: left/right edges stressed + { 4, 10, 11, 7, 3}, // asymmetric: top/bottom edges stressed + { 8, 8, 5, 5, 1}, // single channel + { 8, 8, 5, 5, 4}, // RGBA +}); + +// clang-format on + +TEST_P(OpResize_CubicBoundary, tensor_cubic_boundary) +{ + int srcW = GetParamValue<0>(); + int srcH = GetParamValue<1>(); + int dstW = GetParamValue<2>(); + int dstH = GetParamValue<3>(); + int channels = GetParamValue<4>(); + + nvcv::ImageFormat fmt; + switch (channels) + { + case 1: + fmt = nvcv::FMT_U8; + break; + case 3: + fmt = nvcv::FMT_RGB8; + break; + case 4: + fmt = nvcv::FMT_RGBA8; + break; + default: + FAIL() << "Unsupported channel count"; + } + + // Create gradient source + std::vector srcVec; + FillGradientImage(srcVec, srcW, srcH, channels); + + // Compute expected with self-contained reference + std::vector goldVec; + ReferenceCubicResizeReplicateBorder(goldVec, dstW, dstH, srcVec, srcW, srcH, channels); + + // Upload source to GPU tensor + nvcv::Tensor imgSrc = nvcv::util::CreateTensor(1, srcW, srcH, fmt); + { + auto srcData = imgSrc.exportData(); + auto srcAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcData); + ASSERT_TRUE(srcAccess); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(srcAccess->sampleData(0), srcAccess->rowStride(), srcVec.data(), + srcW * channels, srcW * channels, srcH, cudaMemcpyHostToDevice)); + } + + // Run GPU resize + nvcv::Tensor imgDst = nvcv::util::CreateTensor(1, dstW, dstH, fmt); + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + cvcuda::Resize resizeOp; + EXPECT_NO_THROW(resizeOp(stream, imgSrc, imgDst, NVCV_INTERP_CUBIC)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + + // Read back result + std::vector testVec(dstH * dstW * channels); + { + auto dstData = imgDst.exportData(); + auto dstAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*dstData); + ASSERT_TRUE(dstAccess); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(testVec.data(), dstW * channels, dstAccess->sampleData(0), + dstAccess->rowStride(), dstW * channels, dstH, cudaMemcpyDeviceToHost)); + } + + // Compare: with the fix, max abs error should be <=1 (float rounding). + // Without the fix, boundary pixels would differ by 30-90+. + int maxDiff = 0; + for (size_t i = 0; i < testVec.size(); i++) + { + int diff = abs(static_cast(testVec[i]) - static_cast(goldVec[i])); + maxDiff = std::max(maxDiff, diff); + } + EXPECT_LE(maxDiff, 1) << "Tensor cubic resize boundary error: max pixel diff = " << maxDiff << " (src=" << srcW + << "x" << srcH << " -> dst=" << dstW << "x" << dstH << ")"; +} + +TEST_P(OpResize_CubicBoundary, varshape_cubic_boundary) +{ + int srcW = GetParamValue<0>(); + int srcH = GetParamValue<1>(); + int dstW = GetParamValue<2>(); + int dstH = GetParamValue<3>(); + int channels = GetParamValue<4>(); + + nvcv::ImageFormat fmt; + switch (channels) + { + case 1: + fmt = nvcv::FMT_U8; + break; + case 3: + fmt = nvcv::FMT_RGB8; + break; + case 4: + fmt = nvcv::FMT_RGBA8; + break; + default: + FAIL() << "Unsupported channel count"; + } + + // Create gradient source + std::vector srcVec; + FillGradientImage(srcVec, srcW, srcH, channels); - int maeThreshold = 1; + // Compute expected with self-contained reference + std::vector goldVec; + ReferenceCubicResizeReplicateBorder(goldVec, dstW, dstH, srcVec, srcW, srcH, channels); - EXPECT_THAT(mae, t::Each(t::Le(maeThreshold))); + // Upload to ImageBatchVarShape (batch of 1) + nvcv::Image imgSrc(nvcv::Size2D{srcW, srcH}, fmt); + { + auto srcData = imgSrc.exportData(); + ASSERT_TRUE(srcData); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(srcData->plane(0).basePtr, srcData->plane(0).rowStride, srcVec.data(), + srcW * channels, srcW * channels, srcH, cudaMemcpyHostToDevice)); } + + nvcv::Image imgDst(nvcv::Size2D{dstW, dstH}, fmt); + + nvcv::ImageBatchVarShape batchSrc(1); + batchSrc.pushBack(imgSrc); + nvcv::ImageBatchVarShape batchDst(1); + batchDst.pushBack(imgDst); + + // Run GPU var-shape resize + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + cvcuda::Resize resizeOp; + EXPECT_NO_THROW(resizeOp(stream, batchSrc, batchDst, NVCV_INTERP_CUBIC)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + + // Read back result + std::vector testVec(dstH * dstW * channels); + { + auto dstData = imgDst.exportData(); + ASSERT_TRUE(dstData); + ASSERT_EQ(cudaSuccess, + cudaMemcpy2D(testVec.data(), dstW * channels, dstData->plane(0).basePtr, dstData->plane(0).rowStride, + dstW * channels, dstH, cudaMemcpyDeviceToHost)); + } + + // Compare: with the fix, max abs error should be <=1 (float rounding). + // Without the fix, boundary pixels would differ by 30-90+. + int maxDiff = 0; + for (size_t i = 0; i < testVec.size(); i++) + { + int diff = abs(static_cast(testVec[i]) - static_cast(goldVec[i])); + maxDiff = std::max(maxDiff, diff); + } + EXPECT_LE(maxDiff, 1) << "VarShape cubic resize boundary error: max pixel diff = " << maxDiff << " (src=" << srcW + << "x" << srcH << " -> dst=" << dstW << "x" << dstH << ")"; } TEST(OpResize_Negative, createWithNullHandle) @@ -326,8 +619,7 @@ TEST(OpResize_Negative, createWithNullHandle) NVCV_TEST_SUITE_P(OpResize_Negative, test::ValueList{ {nvcv::FMT_U8, nvcv::FMT_U16, 1, 1, NVCV_INTERP_NEAREST}, // in/out image data type not same {nvcv::FMT_U8, nvcv::FMT_RGB8p, 1, 1, NVCV_INTERP_NEAREST}, // in/out image layout not same - {nvcv::FMT_RGB8p, nvcv::FMT_U8, 1, 1, NVCV_INTERP_NEAREST}, // in/out image layout not NHWC - {nvcv::FMT_RGB8p, nvcv::FMT_RGB8p, 1, 1, NVCV_INTERP_NEAREST}, // in/out image layout not NHWC + {nvcv::FMT_RGB8p, nvcv::FMT_U8, 1, 1, NVCV_INTERP_NEAREST}, // in/out image layout not same (planar in, interleaved out) {nvcv::FMT_RGB8, nvcv::FMT_RGB8, 1, 2, NVCV_INTERP_NEAREST}, // in/out image num are different {nvcv::FMT_U8, nvcv::FMT_RGB8, 1, 1, NVCV_INTERP_NEAREST}, // in/out image channels are different {nvcv::FMT_F16, nvcv::FMT_F16, 1, 1, NVCV_INTERP_NEAREST}, // invalid datatype @@ -341,7 +633,6 @@ NVCV_TEST_SUITE_P(OpResizeVarshape_Negative, test::ValueList(); // Create input and output - std::default_random_engine randEng; - std::uniform_int_distribution rndSrcWidth(srcWidthBase * 0.8, srcWidthBase * 1.1); - std::uniform_int_distribution rndSrcHeight(srcHeightBase * 0.8, srcHeightBase * 1.1); + std::default_random_engine randEng; + std::uniform_int_distribution rndSrcWidth(ScaledSize(srcWidthBase, 0.8), ScaledSize(srcWidthBase, 1.1)); + std::uniform_int_distribution rndSrcHeight(ScaledSize(srcHeightBase, 0.8), ScaledSize(srcHeightBase, 1.1)); - std::uniform_int_distribution rndDstWidth(dstWidthBase * 0.8, dstWidthBase * 1.1); - std::uniform_int_distribution rndDstHeight(dstHeightBase * 0.8, dstHeightBase * 1.1); + std::uniform_int_distribution rndDstWidth(ScaledSize(dstWidthBase, 0.8), ScaledSize(dstWidthBase, 1.1)); + std::uniform_int_distribution rndDstHeight(ScaledSize(dstHeightBase, 0.8), ScaledSize(dstHeightBase, 1.1)); - std::vector imgSrc, imgDst; + std::vector imgSrc; + + std::vector imgDst; for (int i = 0; i < numberOfImages - 1; ++i) { imgSrc.emplace_back(nvcv::Size2D{rndSrcWidth(randEng), rndSrcHeight(randEng)}, inFmt); @@ -419,9 +713,156 @@ TEST_P(OpResizeVarshape_Negative, op) batchDst.pushBack(imgDst.begin(), imgDst.end()); // run operator + cvcuda::Resize resizeOp; + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&resizeOp, &stream, &batchSrc, &batchDst, &interpolation] + { resizeOp(stream, batchSrc, batchDst, interpolation); })); + + EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpResize_Negative, linear_interp_requires_2x2_source) +{ + // Regression: LinearResize reads iSrcCoord+1 unconditionally; a 1x1 source causes GPU OOB. + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + nvcv::Tensor src1x1 = nvcv::util::CreateTensor(1, 1, 1, nvcv::FMT_U8); + nvcv::Tensor dst = nvcv::util::CreateTensor(1, 16, 16, nvcv::FMT_U8); + + cvcuda::Resize resizeOp; + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&resizeOp, &stream, &src1x1, &dst] + { resizeOp(stream, src1x1, dst, NVCV_INTERP_LINEAR); })); + + // Width==1 with height>1 is also rejected. + nvcv::Tensor src1xN = nvcv::util::CreateTensor(1, 1, 8, nvcv::FMT_U8); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&resizeOp, &stream, &src1xN, &dst] + { resizeOp(stream, src1xN, dst, NVCV_INTERP_LINEAR); })); + + EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpResizeVarshape_Negative, linear_interp_requires_2x2_source) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + nvcv::ImageBatchVarShape batchSrc(1); + + nvcv::ImageBatchVarShape batchDst(1); + batchSrc.pushBack(nvcv::Image({1, 1}, nvcv::FMT_U8)); + batchDst.pushBack(nvcv::Image({16, 16}, nvcv::FMT_U8)); + + cvcuda::Resize resizeOp; + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&resizeOp, &stream, &batchSrc, &batchDst] + { resizeOp(stream, batchSrc, batchDst, NVCV_INTERP_LINEAR); })); + + EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +// ============================================================================= +// Planar (NCHW/CHW) layout support +// +// Resize processes channels independently, so a planar input is resized plane-by-plane and must +// produce exactly the same pixels as the interleaved path. These tests feed identical data in both +// layouts through cvcuda::Resize and require the (re-interleaved) planar output to match the +// interleaved output bit-for-bit, which holds for every dtype and interpolation mode. +// ============================================================================= + +namespace { + +// Resize identical data in interleaved and planar tensor layout; outputs must match bit-for-bit. +// The shared scaffolding (upload/run/download/compare) lives in PlanarParityUtils.hpp; here we only +// bind the Resize call. +void RunPlanarParityTensorCase(nvcv::ImageFormat planarFmt, nvcv::ImageFormat interleavedFmt, int srcW, int srcH, + int dstW, int dstH, NVCVInterpolationType interp, int numImages) +{ + test::planar::RunTensorParity( + planarFmt, interleavedFmt, srcW, srcH, dstW, dstH, numImages, + [interp](cudaStream_t s, const nvcv::Tensor &src, const nvcv::Tensor &dst, nvcv::ImageFormat) + { + cvcuda::Resize op; + EXPECT_NO_THROW(op(s, src, dst, interp)); + }); +} + +// Var-shape counterpart of RunPlanarParityTensorCase. +void RunPlanarParityVarShapeCase(nvcv::ImageFormat planarFmt, nvcv::ImageFormat interleavedFmt, int srcW, int srcH, + int dstW, int dstH, NVCVInterpolationType interp, int numImages) +{ + test::planar::RunVarShapeParity(planarFmt, interleavedFmt, srcW, srcH, dstW, dstH, numImages, + [interp](cudaStream_t s, const nvcv::ImageBatchVarShape &src, + const nvcv::ImageBatchVarShape &dst, nvcv::ImageFormat) + { + cvcuda::Resize op; + EXPECT_NO_THROW(op(s, src, dst, interp)); + }); +} + +} // namespace + +// Parameters: srcW, srcH, dstW, dstH, interpolation, numImages, planarFmt, interleavedFmt +NVCV_TEST_SUITE_P(OpResizePlanar, + test::ValueList{ + // RGB8 (3 channel uint8), every interpolation, expand and contract. + { 64, 48, 128, 96, NVCV_INTERP_NEAREST, 2, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + {128, 96, 64, 48, NVCV_INTERP_LINEAR, 2, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + { 64, 48, 128, 96, NVCV_INTERP_LINEAR, 2, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + { 64, 48, 128, 96, NVCV_INTERP_CUBIC, 1, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + {128, 96, 64, 48, NVCV_INTERP_CUBIC, 2, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + { 90, 60, 45, 30, NVCV_INTERP_CUBIC, 1, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + {128, 96, 32, 24, NVCV_INTERP_AREA, 1, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + { 90, 60, 45, 30, NVCV_INTERP_AREA, 1, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + { 97, 73, 37, 29, NVCV_INTERP_AREA, 1, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + { 31, 23, 79, 61, NVCV_INTERP_AREA, 1, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + // RGBA8 (4 channel uint8). + { 50, 40, 100, 80, NVCV_INTERP_NEAREST, 2, nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8}, + {100, 80, 50, 40, NVCV_INTERP_LINEAR, 1, nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8}, + {120, 90, 30, 22, NVCV_INTERP_AREA, 1, nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8}, + // Float planar, every interpolation, expand and contract -- exercises the float kernel paths + // (vectorized LinearResize, gated CubicResize/AreaResizeVec) bit-exactly against interleaved. + { 64, 48, 96, 72, NVCV_INTERP_NEAREST, 2, nvcv::FMT_RGBAf32p, nvcv::FMT_RGBAf32}, + { 96, 72, 48, 36, NVCV_INTERP_LINEAR, 2, nvcv::FMT_RGBAf32p, nvcv::FMT_RGBAf32}, + { 48, 36, 96, 72, NVCV_INTERP_LINEAR, 1, nvcv::FMT_RGBAf32p, nvcv::FMT_RGBAf32}, + { 64, 48, 96, 72, NVCV_INTERP_CUBIC, 1, nvcv::FMT_RGBAf32p, nvcv::FMT_RGBAf32}, + { 64, 48, 128, 96, NVCV_INTERP_CUBIC, 1, nvcv::FMT_RGBAf32p, nvcv::FMT_RGBAf32}, + { 96, 72, 32, 24, NVCV_INTERP_AREA, 1, nvcv::FMT_RGBAf32p, nvcv::FMT_RGBAf32}, + { 32, 24, 96, 72, NVCV_INTERP_AREA, 1, nvcv::FMT_RGBAf32p, nvcv::FMT_RGBAf32}, + { 64, 48, 128, 96, NVCV_INTERP_LINEAR, 2, nvcv::FMT_RGBf32p, nvcv::FMT_RGBf32}, + {128, 96, 64, 48, NVCV_INTERP_CUBIC, 1, nvcv::FMT_RGBf32p, nvcv::FMT_RGBf32}, + { 64, 48, 128, 96, NVCV_INTERP_CUBIC, 1, nvcv::FMT_RGBf32p, nvcv::FMT_RGBf32}, + { 45, 30, 90, 60, NVCV_INTERP_CUBIC, 2, nvcv::FMT_RGBf32p, nvcv::FMT_RGBf32}, + {128, 96, 32, 24, NVCV_INTERP_AREA, 1, nvcv::FMT_RGBf32p, nvcv::FMT_RGBf32}, + { 90, 60, 45, 30, NVCV_INTERP_AREA, 1, nvcv::FMT_RGBf32p, nvcv::FMT_RGBf32}, +}); + +TEST_P(OpResizePlanar, tensor_matches_interleaved) +{ + RunPlanarParityTensorCase(GetParamValue<6>(), GetParamValue<7>(), GetParamValue<0>(), GetParamValue<1>(), + GetParamValue<2>(), GetParamValue<3>(), GetParamValue<4>(), GetParamValue<5>()); +} + +TEST_P(OpResizePlanar, varshape_matches_interleaved) +{ + RunPlanarParityVarShapeCase(GetParamValue<6>(), GetParamValue<7>(), GetParamValue<0>(), GetParamValue<1>(), + GetParamValue<2>(), GetParamValue<3>(), GetParamValue<4>(), GetParamValue<5>()); +} + +TEST(OpResize_Negative, planar_input_interleaved_output_layout_mismatch) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + // Planar input, interleaved output: layouts differ and must be rejected. + nvcv::Tensor src = nvcv::util::CreateTensor(1, 32, 24, nvcv::FMT_RGB8p); + nvcv::Tensor dst = nvcv::util::CreateTensor(1, 64, 48, nvcv::FMT_RGB8); + cvcuda::Resize resizeOp; EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, - nvcv::ProtectCall([&] { resizeOp(stream, batchSrc, batchDst, interpolation); })); + nvcv::ProtectCall([&resizeOp, &stream, &src, &dst] { resizeOp(stream, src, dst, NVCV_INTERP_NEAREST); })); EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); diff --git a/tests/cvcuda/system/TestOpResizeCropConvertReformat.cpp b/tests/cvcuda/system/TestOpResizeCropConvertReformat.cpp index ad536e078..307731cf1 100644 --- a/tests/cvcuda/system/TestOpResizeCropConvertReformat.cpp +++ b/tests/cvcuda/system/TestOpResizeCropConvertReformat.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,10 +16,12 @@ */ #include "Definitions.hpp" +#include "PlanarParityUtils.hpp" #include #include #include +#include #include #include #include @@ -31,6 +33,7 @@ #include #include +#include #include #include #include @@ -40,12 +43,107 @@ namespace test = nvcv::test; namespace cuda = nvcv::cuda; namespace ttype = test::type; -static std::default_random_engine randEng(std::random_device{}()); +// Fixed seed: random_device made tests non-deterministic across CI runs and +// occasionally produced ill-conditioned numerical inputs that exceeded +// EXPECT_NEAR tolerances on rare-config CI. Use a known-good fixed seed. +static std::default_random_engine &Rng() +{ + static std::default_random_engine rng(12345); + return rng; +} + +static int ScaledSize(int size, double scale) +{ + return static_cast(static_cast(size) * scale); +} template using uniform_dist = std::conditional_t, std::uniform_int_distribution, std::uniform_real_distribution>; +struct ResizeCropConvertCaseParams +{ + int3 srcShape; + int2 resize; + NVCVInterpolationType interp; + int2 cropDim; + int2 cropPos; + float scale; + float offset; + nvcv::ImageFormat srcFormat; + nvcv::ImageFormat dstFormat; + bool srcCast; +}; + +template +ResizeCropConvertCaseParams GetResizeCropConvertCaseParams() +{ + return {ttype::GetValue, + ttype::GetValue, + ttype::GetValue, + ttype::GetValue, + ttype::GetValue, + static_cast(ttype::GetValue), + static_cast(ttype::GetValue), + nvcv::ImageFormat{ttype::GetValue}, + nvcv::ImageFormat{ttype::GetValue}, + ttype::GetValue}; +} + +struct ResizeCropConvertCaseGeometry +{ + int srcW; + int srcH; + int dstW; + int dstH; + int numImages; + int srcChannels; + int dstChannels; + int srcPlanes; + int dstPlanes; + int srcPixElems; + int dstPixElems; + NVCVSize2D srcSize; + NVCVSize2D newSize; + NVCVSize2D dstSize; +}; + +static ResizeCropConvertCaseGeometry GetResizeCropConvertCaseGeometry(const ResizeCropConvertCaseParams ¶ms) +{ + const int srcW = params.srcShape.x; + const int srcH = params.srcShape.y; + const int dstW = params.cropDim.x; + const int dstH = params.cropDim.y; + const int numImages = params.srcShape.z; + const int srcChannels = params.srcFormat.numChannels(); + const int dstChannels = params.dstFormat.numChannels(); + const int srcPlanes = params.srcFormat.numPlanes(); + const int dstPlanes = params.dstFormat.numPlanes(); + + return { + srcW, + srcH, + dstW, + dstH, + numImages, + srcChannels, + dstChannels, + srcPlanes, + dstPlanes, + srcChannels / srcPlanes, + dstChannels / dstPlanes, + { srcW, srcH}, + {params.resize.x, params.resize.y}, + { dstW, dstH} + }; +} + +static void AssertResizeCropConvertCaseGeometry(const ResizeCropConvertCaseGeometry &geometry) +{ + ASSERT_LE(geometry.srcChannels, 4); + ASSERT_EQ(geometry.srcChannels, geometry.dstChannels); +} + inline NVCVChannelManip ChannelManip(nvcv::ImageFormat srcFormat, nvcv::ImageFormat dstFormat) { const int srcChannels = srcFormat.numChannels(); @@ -56,12 +154,13 @@ inline NVCVChannelManip ChannelManip(nvcv::ImageFormat srcFormat, nvcv::ImageFor if (srcChannels > 2 && srcSwizzle != dstSwizzle) { - int srcSwap = static_cast(srcSwizzle), dstSwap = static_cast(dstSwizzle); - bool srcRGB = (srcSwap == NVCV_SWIZZLE_XYZ0 || srcSwap == NVCV_SWIZZLE_XYZW || srcSwap == NVCV_SWIZZLE_XYZ1), - srcBGR = (srcSwap == NVCV_SWIZZLE_ZYX0 || srcSwap == NVCV_SWIZZLE_ZYXW || srcSwap == NVCV_SWIZZLE_ZYX1); - bool dstRGB = (dstSwap == NVCV_SWIZZLE_XYZ0 || dstSwap == NVCV_SWIZZLE_XYZW || dstSwap == NVCV_SWIZZLE_XYZ1), - dstBGR = (dstSwap == NVCV_SWIZZLE_ZYX0 || dstSwap == NVCV_SWIZZLE_ZYXW || dstSwap == NVCV_SWIZZLE_ZYX1); - bool swapRB = ((srcRGB && dstBGR) || (srcBGR && dstRGB)); + auto srcSwap = static_cast(srcSwizzle); + auto dstSwap = static_cast(dstSwizzle); + bool srcRGB = (srcSwap == NVCV_SWIZZLE_XYZ0 || srcSwap == NVCV_SWIZZLE_XYZW || srcSwap == NVCV_SWIZZLE_XYZ1); + bool srcBGR = (srcSwap == NVCV_SWIZZLE_ZYX0 || srcSwap == NVCV_SWIZZLE_ZYXW || srcSwap == NVCV_SWIZZLE_ZYX1); + bool dstRGB = (dstSwap == NVCV_SWIZZLE_XYZ0 || dstSwap == NVCV_SWIZZLE_XYZW || dstSwap == NVCV_SWIZZLE_XYZ1); + bool dstBGR = (dstSwap == NVCV_SWIZZLE_ZYX0 || dstSwap == NVCV_SWIZZLE_ZYXW || dstSwap == NVCV_SWIZZLE_ZYX1); + bool swapRB = ((srcRGB && dstBGR) || (srcBGR && dstRGB)); if (swapRB && srcChannels == 3) { @@ -73,6 +172,97 @@ inline NVCVChannelManip ChannelManip(nvcv::ImageFormat srcFormat, nvcv::ImageFor // clang-format off +struct ResizeCropConvertLayout +{ + int channels; + size_t srcIncrX; + size_t dstIncrC; + size_t srcIncrY; + size_t srcIncrC; + float scaleW; + float scaleH; + float scale; + float offset; + NVCVSize2D srcSize; + int2 crop; + std::array mapC; + bool srcCast; +}; + +template +void ResizeCropConvertNearest(DstT *dstPtr, const SrcT *srcBase, int dx, int dy, + const ResizeCropConvertLayout &layout) +{ + auto sx = static_cast( + std::floor(layout.scaleW * (static_cast(dx + layout.crop.x) + 0.5f))); + auto sy = static_cast( + std::floor(layout.scaleH * (static_cast(dy + layout.crop.y) + 0.5f))); + + const SrcT *src0 = srcBase + sy * layout.srcIncrY + sx * layout.srcIncrX; + + for (int c = 0; c < layout.channels; c++) + { + dstPtr[layout.mapC[c] * layout.dstIncrC] + = cuda::SaturateCast(layout.scale * src0[c * layout.srcIncrC] + layout.offset); + } +} + +template +void ResizeCropConvertLinear(DstT *dstPtr, const SrcT *srcBase, int dx, int dy, + const ResizeCropConvertLayout &layout) +{ + float fx = layout.scaleW * (static_cast(dx + layout.crop.x) + 0.5f) - 0.5f; + float fy = layout.scaleH * (static_cast(dy + layout.crop.y) + 0.5f) - 0.5f; + + auto sx0 = static_cast(std::floor(fx)); + auto sy0 = static_cast(std::floor(fy)); + int sx1 = std::min(sx0 + 1, layout.srcSize.w - 1); + int sy1 = std::min(sy0 + 1, layout.srcSize.h - 1); + + fx -= static_cast(sx0); + fy -= static_cast(sy0); + + sx0 = std::max(0, sx0); + sy0 = std::max(0, sy0); + + std::array wghtX = {1.f - fx, fx}; + std::array wghtY = {1.f - fy, fy}; + + const size_t x0 = sx0 * layout.srcIncrX; + const size_t x1 = sx1 * layout.srcIncrX; + + const SrcT *src0 = srcBase + sy0 * layout.srcIncrY; + const SrcT *src1 = srcBase + sy1 * layout.srcIncrY; + + for (int c = 0; c < layout.channels; c++) + { + const size_t xc = c * layout.srcIncrC; + + float val = src0[x0 + xc] * wghtY[0] * wghtX[0] + + src0[x1 + xc] * wghtY[0] * wghtX[1] + + src1[x0 + xc] * wghtY[1] * wghtX[0] + + src1[x1 + xc] * wghtY[1] * wghtX[1]; + + val = layout.scale * (layout.srcCast ? cuda::SaturateCast(val) : val) + layout.offset; + + dstPtr[layout.mapC[c] * layout.dstIncrC] = cuda::SaturateCast(val); + } +} + +template +void ResizeCropConvertPixel(DstT *dstPtr, const SrcT *srcBase, int dx, int dy, NVCVInterpolationType interp, + const ResizeCropConvertLayout &layout) +{ + if (interp == NVCV_INTERP_NEAREST) + { + ResizeCropConvertNearest(dstPtr, srcBase, dx, dy, layout); + } + else if (interp == NVCV_INTERP_LINEAR) + { + ResizeCropConvertLinear(dstPtr, srcBase, dx, dy, layout); + } +} + template void ResizeCropConvert( DstT *dst, NVCVSize2D dstSize, nvcv::ImageFormat dstFrmt, const SrcT *src, NVCVSize2D srcSize, nvcv::ImageFormat srcFrmt, @@ -92,15 +282,26 @@ void ResizeCropConvert( DstT *dst, NVCVSize2D dstSize, nvcv::ImageFormat ds size_t srcIncrN = srcSize.w * srcSize.h * channels; size_t dstIncrN = dstSize.w * dstSize.h * channels; - int mapC[4] = {0, 1, 2, 3}; + std::array mapC = {0, 1, 2, 3}; if (manip == NVCV_CHANNEL_REVERSE) { for (int c = 0; c < channels; ++c) mapC[c] = channels - c - 1; } - float scaleW = static_cast(srcSize.w) / newSize.w; - float scaleH = static_cast(srcSize.h) / newSize.h; + ResizeCropConvertLayout layout = {channels, + srcIncrX, + dstIncrC, + srcIncrY, + srcIncrC, + static_cast(srcSize.w) / newSize.w, + static_cast(srcSize.h) / newSize.h, + scale, + offset, + srcSize, + crop, + mapC, + srcCast}; for (int i = 0; i < numImages; i++) { @@ -115,57 +316,7 @@ void ResizeCropConvert( DstT *dst, NVCVSize2D dstSize, nvcv::ImageFormat ds { DstT *dstPtr = dstRow + dx * dstIncrX; - if (interp == NVCV_INTERP_NEAREST) - { - int sx = std::floor(scaleW * (dx + crop.x + 0.5f)); - int sy = std::floor(scaleH * (dy + crop.y + 0.5f)); - - const SrcT *src0 = srcBase + sy * srcIncrY + sx * srcIncrX; - - for (int c = 0; c < channels; c++) - { - dstPtr[mapC[c] * dstIncrC] = cuda::SaturateCast(scale * src0[c * srcIncrC] + offset); - } - } - else if (interp == NVCV_INTERP_LINEAR) - { - float fx = scaleW * (dx + crop.x + 0.5f) - 0.5f; - float fy = scaleH * (dy + crop.y + 0.5f) - 0.5f; - - int sx0 = std::floor(fx); - int sy0 = std::floor(fy); - int sx1 = std::min(sx0 + 1, srcSize.w - 1); - int sy1 = std::min(sy0 + 1, srcSize.h - 1); - - fx -= sx0; - fy -= sy0; - - sx0 = std::max(0, sx0); - sy0 = std::max(0, sy0); - - float wghtX[2] = {1 - fx, fx}; - float wghtY[2] = {1 - fy, fy}; - - const size_t x0 = sx0 * srcIncrX; - const size_t x1 = sx1 * srcIncrX; - - const SrcT *src0 = srcBase + sy0 * srcIncrY; - const SrcT *src1 = srcBase + sy1 * srcIncrY; - - for (int c = 0; c < channels; c++) - { - const size_t xc = c * srcIncrC; - - float val = src0[x0 + xc] * wghtY[0] * wghtX[0] - + src0[x1 + xc] * wghtY[0] * wghtX[1] - + src1[x0 + xc] * wghtY[1] * wghtX[0] - + src1[x1 + xc] * wghtY[1] * wghtX[1]; - - val = scale * (srcCast ? cuda::SaturateCast(val) : val) + offset; - - dstPtr[mapC[c] * dstIncrC] = cuda::SaturateCast(val); - } - } + ResizeCropConvertPixel(dstPtr, srcBase, dx, dy, interp, layout); } } } @@ -292,6 +443,11 @@ NVCV_TYPED_TEST_SUITE( _TEST_ROW(_SHAPE( 313, 212, 4), int2(412, 336), NVCV_INTERP_LINEAR, int2( 412, 336), int2( 0, 0), 1, 0, NVCV_IMAGE_FORMAT_RGB8, NVCV_IMAGE_FORMAT_RGB8, uchar3, uchar3 , false), // 38 _TEST_ROW(_SHAPE(1280, 960, 3), int2(300, 225), NVCV_INTERP_LINEAR, int2( 250, 200), int2( 15, 16), 1, 0, NVCV_IMAGE_FORMAT_RGB8, NVCV_IMAGE_FORMAT_RGB8, uchar3, uchar3 , false), // 39 + // Test cases: RGB (planar) -> BGR/RGB; linear interpolation; float and uchar output. + // source(w, h, n) , resize(w, h) , interpolation , dest.(w, h) , crop(x, y), scale, offst, source format , destination format , src type, dst type, src cast + _TEST_ROW(_SHAPE( 32, 24, 3), int2( 48, 36), NVCV_INTERP_LINEAR, int2( 24, 20), int2( 3, 2), 1, 0, NVCV_IMAGE_FORMAT_RGB8p, NVCV_IMAGE_FORMAT_BGR8, uint8_t, uchar3 , false), + _TEST_ROW(_SHAPE( 33, 25, 2), int2( 40, 32), NVCV_INTERP_LINEAR, int2( 21, 18), int2( 1, 4), 1, 0, NVCV_IMAGE_FORMAT_RGB8p, NVCV_IMAGE_FORMAT_RGBf32p, uint8_t, float , false), + // Test cases: RGB (interleaved) -> BGR (planar); nearest-neighbor interpolation; float and uchar output. // source(w, h, n) , resize(w, h) , interpolation , dest.(w, h) , crop(x, y), scale, offst, source format , destination format , src type, dst type, src cast _TEST_ROW(_SHAPE( 8, 8, 1), int2( 8, 8), NVCV_INTERP_NEAREST, int2( 6, 6), int2( 1, 1), 1, 0, NVCV_IMAGE_FORMAT_RGB8, NVCV_IMAGE_FORMAT_BGRf32p, uchar3, float , false), // 40 @@ -325,7 +481,15 @@ NVCV_TYPED_TEST_SUITE( _TEST_ROW(_SHAPE(1280, 960, 3), int2(300, 225), NVCV_INTERP_NEAREST, int2( 250, 200), int2( 15, 16), 2, -255, NVCV_IMAGE_FORMAT_BGR8, NVCV_IMAGE_FORMAT_RGBf32p, uchar3, float , true), // 59 _TEST_ROW(_SHAPE(1280, 960, 3), int2(300, 225), NVCV_INTERP_NEAREST, int2( 250, 200), int2( 15, 16), -1, 255, NVCV_IMAGE_FORMAT_BGR8, NVCV_IMAGE_FORMAT_RGB8, uchar3, uchar3 , true), // 60 _TEST_ROW(_SHAPE( 313, 212, 4), int2(412, 336), NVCV_INTERP_LINEAR, int2( 412, 336), int2( 0, 0), 1, 0, NVCV_IMAGE_FORMAT_BGR8, NVCV_IMAGE_FORMAT_RGB8p, uchar3, uint8_t, true), // 61 - _TEST_ROW(_SHAPE(1280, 960, 3), int2(300, 225), NVCV_INTERP_NEAREST, int2( 250, 200), int2( 15, 16), 1, 0, NVCV_IMAGE_FORMAT_BGR8, NVCV_IMAGE_FORMAT_RGB8p, uchar3, uint8_t, true) // 62 + _TEST_ROW(_SHAPE(1280, 960, 3), int2(300, 225), NVCV_INTERP_NEAREST, int2( 250, 200), int2( 15, 16), 1, 0, NVCV_IMAGE_FORMAT_BGR8, NVCV_IMAGE_FORMAT_RGB8p, uchar3, uint8_t, true), // 62 + + // Test cases: Y8 (1-channel) -> Y8 and F32; linear and nearest interpolation. + // source(w, h, n) , resize(w, h) , interpolation , dest.(w, h) , crop(x, y), scale, offst, source format , destination format , src type, dst type, src cast + _TEST_ROW(_SHAPE( 8, 8, 1), int2( 8, 8), NVCV_INTERP_LINEAR, int2( 6, 6), int2( 1, 1), 1, 0, NVCV_IMAGE_FORMAT_Y8, NVCV_IMAGE_FORMAT_Y8, uchar1, uint8_t, false), // 63 + _TEST_ROW(_SHAPE( 313, 212, 4), int2(412, 336), NVCV_INTERP_LINEAR, int2( 412, 336), int2( 0, 0), 1, 0, NVCV_IMAGE_FORMAT_Y8, NVCV_IMAGE_FORMAT_Y8, uchar1, uint8_t, false), // 64 + _TEST_ROW(_SHAPE( 313, 212, 4), int2(412, 336), NVCV_INTERP_NEAREST, int2( 412, 336), int2( 0, 0), 1, 0, NVCV_IMAGE_FORMAT_Y8, NVCV_IMAGE_FORMAT_Y8, uchar1, uint8_t, false), // 65 + _TEST_ROW(_SHAPE( 313, 212, 4), int2(412, 336), NVCV_INTERP_LINEAR, int2( 412, 336), int2( 0, 0), 1, 0, NVCV_IMAGE_FORMAT_Y8, NVCV_IMAGE_FORMAT_F32, uchar1, float , false), // 66 + _TEST_ROW(_SHAPE( 313, 212, 4), int2(412, 336), NVCV_INTERP_NEAREST, int2( 412, 336), int2( 0, 0), 1, 0, NVCV_IMAGE_FORMAT_Y8, NVCV_IMAGE_FORMAT_F32, uchar1, float , false) // 67 >); #undef _TEST_ROW @@ -333,48 +497,22 @@ NVCV_TYPED_TEST_SUITE( TYPED_TEST(OpResizeCropConvertReformat, tensor_correct_output) { - int3 srcShape = ttype::GetValue; - int2 resize = ttype::GetValue; - - NVCVInterpolationType interp = ttype::GetValue; - - int2 cropDim = ttype::GetValue; - int2 cropPos = ttype::GetValue; - - float scale = ttype::GetValue; - float offset = ttype::GetValue; - - nvcv::ImageFormat srcFormat{ttype::GetValue}; - nvcv::ImageFormat dstFormat{ttype::GetValue}; + const ResizeCropConvertCaseParams params = GetResizeCropConvertCaseParams(); + const ResizeCropConvertCaseGeometry geometry = GetResizeCropConvertCaseGeometry(params); + ASSERT_NO_FATAL_FAILURE(AssertResizeCropConvertCaseGeometry(geometry)); using SrcVT = typename ttype::GetType; using DstVT = typename ttype::GetType; using SrcBT = typename cuda::BaseType; using DstBT = typename cuda::BaseType; - bool srcCast = ttype::GetValue; - - int srcW = srcShape.x; - int srcH = srcShape.y; - int dstW = cropDim.x; - int dstH = cropDim.y; - - int numImages = srcShape.z; - int srcChannels = srcFormat.numChannels(); - int dstChannels = dstFormat.numChannels(); - int srcPlanes = srcFormat.numPlanes(); - int dstPlanes = dstFormat.numPlanes(); - int srcPixElems = srcChannels / srcPlanes; - int dstPixElems = dstChannels / dstPlanes; - - ASSERT_LE(srcChannels, 4); - ASSERT_EQ(srcChannels, dstChannels); - - NVCVChannelManip manip = ChannelManip(srcFormat, dstFormat); + NVCVChannelManip manip = ChannelManip(params.srcFormat, params.dstFormat); // Create input and output tensors. - nvcv::Tensor srcTensor = nvcv::util::CreateTensor(numImages, srcW, srcH, srcFormat); - nvcv::Tensor dstTensor = nvcv::util::CreateTensor(numImages, dstW, dstH, dstFormat); + nvcv::Tensor srcTensor + = nvcv::util::CreateTensor(geometry.numImages, geometry.srcW, geometry.srcH, params.srcFormat); + nvcv::Tensor dstTensor + = nvcv::util::CreateTensor(geometry.numImages, geometry.dstW, geometry.dstH, params.dstFormat); auto src = srcTensor.exportData(); auto dst = dstTensor.exportData(); @@ -388,50 +526,53 @@ TYPED_TEST(OpResizeCropConvertReformat, tensor_correct_output) auto dstAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*dst); ASSERT_TRUE(dstAccess); - int srcRowElems = srcPixElems * srcW; - int dstRowElems = dstPixElems * dstW; - - size_t srcElems = (size_t)srcRowElems * (size_t)srcH * (size_t)srcPlanes * (size_t)numImages; - size_t dstElems = (size_t)dstRowElems * (size_t)dstH * (size_t)dstPlanes * (size_t)numImages; + int srcRowElems = geometry.srcPixElems * geometry.srcW; + int dstRowElems = geometry.dstPixElems * geometry.dstW; - NVCVSize2D srcSize{srcW, srcH}; - NVCVSize2D newSize{resize.x, resize.y}; - NVCVSize2D dstSize{dstW, dstH}; + size_t srcElems + = (size_t)srcRowElems * (size_t)geometry.srcH * (size_t)geometry.srcPlanes * (size_t)geometry.numImages; + size_t dstElems + = (size_t)dstRowElems * (size_t)geometry.dstH * (size_t)geometry.dstPlanes * (size_t)geometry.numImages; - size_t srcPitch = srcW * sizeof(SrcVT); - size_t dstPitch = dstW * sizeof(DstVT); + size_t srcPitch = geometry.srcW * sizeof(SrcVT); + size_t dstPitch = geometry.dstW * sizeof(DstVT); std::vector srcVec(srcElems); std::vector refVec(dstElems); // Populate source tensor. - for (int n = 0; n < numImages; n++) + for (int n = 0; n < geometry.numImages; n++) { - fillVec(srcVec, srcSize, srcFormat, n * (size_t)srcRowElems * (size_t)srcH * (size_t)srcPlanes); + fillVec(srcVec, geometry.srcSize, params.srcFormat, + n * (size_t)srcRowElems * (size_t)geometry.srcH * (size_t)geometry.srcPlanes); } // Copy source tensor to device. - ASSERT_EQ(cudaSuccess, cudaMemcpy2D(src->basePtr(), srcAccess->rowStride(), srcVec.data(), srcPitch, srcPitch, - srcH * srcPlanes * numImages, cudaMemcpyHostToDevice)); + ASSERT_EQ(cudaSuccess, + cudaMemcpy2D(src->basePtr(), srcAccess->rowStride(), srcVec.data(), srcPitch, srcPitch, + geometry.srcH * geometry.srcPlanes * geometry.numImages, cudaMemcpyHostToDevice)); // Generate "gold" result for image and place in reference vector. - ResizeCropConvert(refVec, dstSize, dstFormat, srcVec, srcSize, srcFormat, numImages, newSize, cropPos, interp, - manip, scale, offset, srcCast); + ResizeCropConvert(refVec, geometry.dstSize, params.dstFormat, srcVec, geometry.srcSize, params.srcFormat, + geometry.numImages, geometry.newSize, params.cropPos, params.interp, manip, params.scale, + params.offset, params.srcCast); // Run fused ResizeCropConvertReformat operator. cudaStream_t stream; ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); cvcuda::ResizeCropConvertReformat resizeCrop; - EXPECT_NO_THROW(resizeCrop(stream, srcTensor, dstTensor, newSize, interp, cropPos, manip, scale, offset, srcCast)); + EXPECT_NO_THROW(resizeCrop(stream, srcTensor, dstTensor, geometry.newSize, params.interp, params.cropPos, manip, + params.scale, params.offset, params.srcCast)); ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); // Copy destination tensor back to host. std::vector dstVec(dstElems); - ASSERT_EQ(cudaSuccess, cudaMemcpy2D(dstVec.data(), dstPitch, dst->basePtr(), dstAccess->rowStride(), dstPitch, - dstH * dstPlanes * numImages, cudaMemcpyDeviceToHost)); + ASSERT_EQ(cudaSuccess, + cudaMemcpy2D(dstVec.data(), dstPitch, dst->basePtr(), dstAccess->rowStride(), dstPitch, + geometry.dstH * geometry.dstPlanes * geometry.numImages, cudaMemcpyDeviceToHost)); // Compare "gold" reference to computed output. VEC_EXPECT_NEAR(refVec, dstVec, 1); @@ -439,104 +580,87 @@ TYPED_TEST(OpResizeCropConvertReformat, tensor_correct_output) TYPED_TEST(OpResizeCropConvertReformat, varshape_correct_output) { - int3 srcShape = ttype::GetValue; - int2 resize = ttype::GetValue; - - NVCVInterpolationType interp = ttype::GetValue; - - int2 cropDim = ttype::GetValue; - int2 cropPos = ttype::GetValue; - - float scale = ttype::GetValue; - float offset = ttype::GetValue; - - nvcv::ImageFormat srcFormat{ttype::GetValue}; - nvcv::ImageFormat dstFormat{ttype::GetValue}; + const ResizeCropConvertCaseParams params = GetResizeCropConvertCaseParams(); + const ResizeCropConvertCaseGeometry geometry = GetResizeCropConvertCaseGeometry(params); + ASSERT_NO_FATAL_FAILURE(AssertResizeCropConvertCaseGeometry(geometry)); using SrcVT = typename ttype::GetType; using DstVT = typename ttype::GetType; using SrcBT = typename cuda::BaseType; using DstBT = typename cuda::BaseType; - bool srcCast = ttype::GetValue; - - int srcW = srcShape.x; - int srcH = srcShape.y; - int dstW = cropDim.x; - int dstH = cropDim.y; - - int numImages = srcShape.z; - int srcChannels = srcFormat.numChannels(); - int dstChannels = dstFormat.numChannels(); - int srcPlanes = srcFormat.numPlanes(); - int dstPlanes = dstFormat.numPlanes(); - int srcPixElems = srcChannels / srcPlanes; - int dstPixElems = dstChannels / dstPlanes; - - ASSERT_LE(srcChannels, 4); - ASSERT_EQ(srcChannels, dstChannels); - - NVCVChannelManip manip = ChannelManip(srcFormat, dstFormat); + NVCVChannelManip manip = ChannelManip(params.srcFormat, params.dstFormat); std::vector srcImg; uniform_dist randVal(std::is_integral_v ? cuda::TypeTraits::min : SrcBT{0}, std::is_integral_v ? cuda::TypeTraits::max : SrcBT{1}); - std::uniform_int_distribution randW(srcW * 0.8, srcW * 1.2); - std::uniform_int_distribution randH(srcH * 0.8, srcH * 1.2); - - int dstRowElems = dstPixElems * dstW; + std::uniform_int_distribution randW(ScaledSize(geometry.srcW, 0.8), ScaledSize(geometry.srcW, 1.2)); + std::uniform_int_distribution randH(ScaledSize(geometry.srcH, 0.8), ScaledSize(geometry.srcH, 1.2)); - size_t refIncr = (size_t)dstRowElems * (size_t)dstH * (size_t)dstPlanes; - size_t dstElems = refIncr * (size_t)numImages; + int dstRowElems = geometry.dstPixElems * geometry.dstW; - NVCVSize2D newSize{resize.x, resize.y}; - NVCVSize2D dstSize{dstW, dstH}; + size_t refIncr = (size_t)dstRowElems * (size_t)geometry.dstH * (size_t)geometry.dstPlanes; + size_t dstElems = refIncr * (size_t)geometry.numImages; std::vector refVec(dstElems); - size_t dstPitch = dstW * sizeof(DstVT); + size_t dstPitch = geometry.dstW * sizeof(DstVT); - for (int i = 0; i < numImages; ++i) + for (int i = 0; i < geometry.numImages; ++i) { - int imgW = (interp ? randW(randEng) : srcW); - int imgH = (interp ? randH(randEng) : srcH); + int imgW = (params.interp ? randW(Rng()) : geometry.srcW); + int imgH = (params.interp ? randH(Rng()) : geometry.srcH); - srcImg.emplace_back(NVCVSize2D{imgW, imgH}, srcFormat); + srcImg.emplace_back(nvcv::Size2D{imgW, imgH}, params.srcFormat); auto srcData = srcImg[i].exportData(); ASSERT_TRUE(srcData); - int imgRowElems = srcPixElems * imgW; + int imgRowElems = geometry.srcPixElems * imgW; size_t imgPitch = imgW * sizeof(SrcVT); - size_t imgElems = (size_t)imgRowElems * (size_t)imgH * (size_t)srcPlanes; + size_t imgElems = (size_t)imgRowElems * (size_t)imgH * (size_t)geometry.srcPlanes; NVCVSize2D imgSize{imgW, imgH}; std::vector imgVec(imgElems); // Populate image tensor . - fillVec(imgVec, imgSize, srcFormat); + fillVec(imgVec, imgSize, params.srcFormat); // Generate "gold" result for image and place in reference image plane. DstBT *refPlane = refVec.data() + i * refIncr; - ResizeCropConvert(refPlane, dstSize, dstFormat, imgVec, imgSize, srcFormat, 1, newSize, cropPos, interp, manip, - scale, offset, srcCast); + ResizeCropConvert(refPlane, geometry.dstSize, params.dstFormat, imgVec, imgSize, params.srcFormat, 1, + geometry.newSize, params.cropPos, params.interp, manip, params.scale, params.offset, + params.srcCast); // Copy source tensor to device. - ASSERT_EQ(cudaSuccess, cudaMemcpy2D(srcData->plane(0).basePtr, srcData->plane(0).rowStride, imgVec.data(), - imgPitch, imgPitch, imgH * srcPlanes, cudaMemcpyHostToDevice)); + if (geometry.srcPlanes > 1) + { + for (int p = 0; p < geometry.srcPlanes; ++p) + { + const SrcBT *srcPlane = imgVec.data() + p * (size_t)imgW * (size_t)imgH; + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(srcData->plane(p).basePtr, srcData->plane(p).rowStride, srcPlane, + imgPitch, imgPitch, imgH, cudaMemcpyHostToDevice)); + } + } + else + { + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(srcData->plane(0).basePtr, srcData->plane(0).rowStride, imgVec.data(), + imgPitch, imgPitch, imgH, cudaMemcpyHostToDevice)); + } } - nvcv::ImageBatchVarShape src(numImages); + nvcv::ImageBatchVarShape src(geometry.numImages); src.pushBack(srcImg.begin(), srcImg.end()); // Create output tensor. - nvcv::Tensor dstTensor = nvcv::util::CreateTensor(numImages, dstW, dstH, dstFormat); + nvcv::Tensor dstTensor + = nvcv::util::CreateTensor(geometry.numImages, geometry.dstW, geometry.dstH, params.dstFormat); auto dst = dstTensor.exportData(); @@ -550,20 +674,260 @@ TYPED_TEST(OpResizeCropConvertReformat, varshape_correct_output) ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); cvcuda::ResizeCropConvertReformat resizeCrop; - EXPECT_NO_THROW(resizeCrop(stream, src, dstTensor, newSize, interp, cropPos, manip, scale, offset, srcCast)); + EXPECT_NO_THROW(resizeCrop(stream, src, dstTensor, geometry.newSize, params.interp, params.cropPos, manip, + params.scale, params.offset, params.srcCast)); ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); // Copy destination tensor back to host. std::vector dstVec(dstElems); - ASSERT_EQ(cudaSuccess, cudaMemcpy2D(dstVec.data(), dstPitch, dst->basePtr(), dstAccess->rowStride(), dstPitch, - dstH * dstPlanes * numImages, cudaMemcpyDeviceToHost)); + ASSERT_EQ(cudaSuccess, + cudaMemcpy2D(dstVec.data(), dstPitch, dst->basePtr(), dstAccess->rowStride(), dstPitch, + geometry.dstH * geometry.dstPlanes * geometry.numImages, cudaMemcpyDeviceToHost)); // Compare "gold" reference to computed output. VEC_EXPECT_NEAR(refVec, dstVec, 1); } +namespace { + +#define ASSERT_PLANAR_TENSOR_ACCESS(Tensor, Data, Access) \ + auto Data = (Tensor).exportData(); \ + ASSERT_TRUE(Data); \ + auto Access = nvcv::TensorDataAccessStridedImagePlanar::Create(*Data); \ + ASSERT_TRUE(Access) + +void CompareTensorOutputParity(const nvcv::Tensor &interleaved, const nvcv::Tensor &planar, + nvcv::ImageFormat planarFormat, int width, int height, int numImages) +{ + ASSERT_PLANAR_TENSOR_ACCESS(interleaved, interleavedData, interleavedAccess); + ASSERT_PLANAR_TENSOR_ACCESS(planar, planarData, planarAccess); + + const int channels = planarFormat.numChannels(); + const int elemSize = planarFormat.planePixelStrideBytes(0); + const int rowBytes = width * channels * elemSize; + + for (int i = 0; i < numImages; ++i) + { + SCOPED_TRACE(i); + auto interleavedOutput + = test::planar::DownloadInterleavedSample(*interleavedAccess, i, width, height, rowBytes); + auto planarOutput = test::planar::DownloadPlanarSample(*planarAccess, i, width, height, channels, elemSize); + auto reinterleaved = test::planar::InterleaveFromPlanes(planarOutput, width, height, channels, elemSize); + + EXPECT_EQ(interleavedOutput, reinterleaved); + } +} + +void UploadTensorParityInputs(nvcv::Tensor &interleaved, nvcv::Tensor &planar, nvcv::ImageFormat planarFormat, + int width, int height, int numImages) +{ + ASSERT_PLANAR_TENSOR_ACCESS(interleaved, interleavedData, interleavedAccess); + ASSERT_PLANAR_TENSOR_ACCESS(planar, planarData, planarAccess); + + const int channels = planarFormat.numChannels(); + const int elemSize = planarFormat.planePixelStrideBytes(0); + const int rowBytes = width * channels * elemSize; + + for (int i = 0; i < numImages; ++i) + { + std::vector hwc(height * rowBytes); + test::planar::FillDeterministicValues(hwc, static_cast(i) * 131 + 17, planarFormat.planeDataType(0)); + + test::planar::UploadInterleavedSample(*interleavedAccess, i, hwc, width, height, rowBytes); + test::planar::UploadPlanarSample(*planarAccess, i, + test::planar::DeinterleaveToPlanes(hwc, width, height, channels, elemSize), + width, height, channels, elemSize); + } +} + +#undef ASSERT_PLANAR_TENSOR_ACCESS + +void AssertPlanarParityFormats(nvcv::ImageFormat srcPlanarFormat, nvcv::ImageFormat srcInterleavedFormat, + nvcv::ImageFormat dstPlanarFormat, nvcv::ImageFormat dstInterleavedFormat) +{ + ASSERT_EQ(srcPlanarFormat.numChannels(), srcInterleavedFormat.numChannels()); + ASSERT_EQ(dstPlanarFormat.numChannels(), dstInterleavedFormat.numChannels()); + ASSERT_EQ(srcPlanarFormat.numChannels(), dstPlanarFormat.numChannels()); +} + +template +void RunPlanarParityOperations(Source &srcInterleaved, nvcv::Tensor &dstInterleaved, Source &srcPlanar, + nvcv::Tensor &dstPlanar, nvcv::ImageFormat srcInterleavedFormat, + nvcv::ImageFormat dstInterleavedFormat, nvcv::ImageFormat dstPlanarFormat, int resizeW, + int resizeH, int dstW, int dstH, int cropX, int cropY, NVCVInterpolationType interp, + int numImages, double scale, double offset, bool srcCast) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + const NVCVSize2D resize{resizeW, resizeH}; + const int2 crop{cropX, cropY}; + const NVCVChannelManip manip = ChannelManip(srcInterleavedFormat, dstInterleavedFormat); + + cvcuda::ResizeCropConvertReformat op; + EXPECT_NO_THROW(op(stream, srcInterleaved, dstInterleaved, resize, interp, crop, manip, static_cast(scale), + static_cast(offset), srcCast)); + EXPECT_NO_THROW(op(stream, srcPlanar, dstPlanar, resize, interp, crop, manip, static_cast(scale), + static_cast(offset), srcCast)); + + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + + ASSERT_NO_FATAL_FAILURE( + CompareTensorOutputParity(dstInterleaved, dstPlanar, dstPlanarFormat, dstW, dstH, numImages)); +} + +void RunPlanarParityTensorCase(nvcv::ImageFormat srcPlanarFormat, nvcv::ImageFormat srcInterleavedFormat, + nvcv::ImageFormat dstPlanarFormat, nvcv::ImageFormat dstInterleavedFormat, int srcW, + int srcH, int resizeW, int resizeH, int dstW, int dstH, int cropX, int cropY, + NVCVInterpolationType interp, int numImages, double scale, double offset, bool srcCast) +{ + ASSERT_NO_FATAL_FAILURE( + AssertPlanarParityFormats(srcPlanarFormat, srcInterleavedFormat, dstPlanarFormat, dstInterleavedFormat)); + + nvcv::Tensor srcInterleaved = nvcv::util::CreateTensor(numImages, srcW, srcH, srcInterleavedFormat); + nvcv::Tensor dstInterleaved = nvcv::util::CreateTensor(numImages, dstW, dstH, dstInterleavedFormat); + nvcv::Tensor srcPlanar = nvcv::util::CreateTensor(numImages, srcW, srcH, srcPlanarFormat); + nvcv::Tensor dstPlanar = nvcv::util::CreateTensor(numImages, dstW, dstH, dstPlanarFormat); + + ASSERT_NO_FATAL_FAILURE( + UploadTensorParityInputs(srcInterleaved, srcPlanar, srcPlanarFormat, srcW, srcH, numImages)); + + ASSERT_NO_FATAL_FAILURE(RunPlanarParityOperations( + srcInterleaved, dstInterleaved, srcPlanar, dstPlanar, srcInterleavedFormat, dstInterleavedFormat, + dstPlanarFormat, resizeW, resizeH, dstW, dstH, cropX, cropY, interp, numImages, scale, offset, srcCast)); +} + +void UploadVarShapeParityInputs(std::vector &interleavedImages, std::vector &planarImages, + nvcv::ImageFormat planarFormat, int width, int height, int numImages) +{ + const int channels = planarFormat.numChannels(); + const int elemSize = planarFormat.planePixelStrideBytes(0); + const int rowBytes = width * channels * elemSize; + + for (int i = 0; i < numImages; ++i) + { + std::vector hwc(height * rowBytes); + test::planar::FillDeterministicValues(hwc, static_cast(i) * 131 + 29, planarFormat.planeDataType(0)); + + auto interleavedData = interleavedImages[i].exportData(); + ASSERT_TRUE(interleavedData); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(interleavedData->plane(0).basePtr, interleavedData->plane(0).rowStride, + hwc.data(), rowBytes, rowBytes, height, cudaMemcpyHostToDevice)); + + auto planarData = planarImages[i].exportData(); + auto planes = test::planar::DeinterleaveToPlanes(hwc, width, height, channels, elemSize); + const int planeBytes = width * height * elemSize; + ASSERT_TRUE(planarData); + ASSERT_EQ(planarData->numPlanes(), channels); + for (int c = 0; c < channels; ++c) + { + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(planarData->plane(c).basePtr, planarData->plane(c).rowStride, + planes.data() + c * planeBytes, width * elemSize, width * elemSize, + height, cudaMemcpyHostToDevice)); + } + } +} + +void RunPlanarParityVarShapeCase(nvcv::ImageFormat srcPlanarFormat, nvcv::ImageFormat srcInterleavedFormat, + nvcv::ImageFormat dstPlanarFormat, nvcv::ImageFormat dstInterleavedFormat, int srcW, + int srcH, int resizeW, int resizeH, int dstW, int dstH, int cropX, int cropY, + NVCVInterpolationType interp, int numImages, double scale, double offset, bool srcCast) +{ + ASSERT_NO_FATAL_FAILURE( + AssertPlanarParityFormats(srcPlanarFormat, srcInterleavedFormat, dstPlanarFormat, dstInterleavedFormat)); + + std::vector srcInterleavedImages; + std::vector srcPlanarImages; + for (int i = 0; i < numImages; ++i) + { + srcInterleavedImages.emplace_back(nvcv::Size2D{srcW, srcH}, srcInterleavedFormat); + srcPlanarImages.emplace_back(nvcv::Size2D{srcW, srcH}, srcPlanarFormat); + } + + ASSERT_NO_FATAL_FAILURE( + UploadVarShapeParityInputs(srcInterleavedImages, srcPlanarImages, srcPlanarFormat, srcW, srcH, numImages)); + + nvcv::ImageBatchVarShape srcInterleaved(numImages); + nvcv::ImageBatchVarShape srcPlanar(numImages); + srcInterleaved.pushBack(srcInterleavedImages.begin(), srcInterleavedImages.end()); + srcPlanar.pushBack(srcPlanarImages.begin(), srcPlanarImages.end()); + + nvcv::Tensor dstInterleaved = nvcv::util::CreateTensor(numImages, dstW, dstH, dstInterleavedFormat); + nvcv::Tensor dstPlanar = nvcv::util::CreateTensor(numImages, dstW, dstH, dstPlanarFormat); + + ASSERT_NO_FATAL_FAILURE(RunPlanarParityOperations( + srcInterleaved, dstInterleaved, srcPlanar, dstPlanar, srcInterleavedFormat, dstInterleavedFormat, + dstPlanarFormat, resizeW, resizeH, dstW, dstH, cropX, cropY, interp, numImages, scale, offset, srcCast)); +} + +struct InferNegativeCaseParams +{ + NVCVInterpolationType interp; + int inputBatchSize; + int outputBatchSize; + nvcv::ImageFormat srcFormat; + nvcv::ImageFormat dstFormat; + int2 cropDim; + int2 cropPos; + NVCVSize2D resizeDim; + NVCVStatus expectedReturnCode; + NVCVChannelManip manip; +}; + +template +InferNegativeCaseParams GetInferNegativeCaseParams() +{ + nvcv::ImageFormat srcFormat{ttype::GetValue}; + nvcv::ImageFormat dstFormat{ttype::GetValue}; + + return {ttype::GetValue, + ttype::GetValue, + ttype::GetValue, + srcFormat, + dstFormat, + ttype::GetValue, + ttype::GetValue, + ttype::GetValue, + ttype::GetValue, + ChannelManip(srcFormat, dstFormat)}; +} + +} // namespace + +// Parameters: srcW, srcH, resizeW, resizeH, dstW, dstH, cropX, cropY, interpolation, numImages, +// scale, offset, srcCast, srcPlanarFormat, srcInterleavedFormat, dstPlanarFormat, dstInterleavedFormat +// clang-format off +NVCV_TEST_SUITE_P(OpResizeCropConvertReformatPlanar, + test::ValueList{ + {31, 25, 42, 37, 29, 23, 3, 2, NVCV_INTERP_NEAREST, 2, 1, 0, false, nvcv::FMT_RGB8p, nvcv::FMT_RGB8, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + {64, 48, 32, 28, 25, 20, 2, 3, NVCV_INTERP_LINEAR, 2, 1 / 127.5, -1, false, nvcv::FMT_RGB8p, nvcv::FMT_RGB8, nvcv::FMT_RGBf32p, nvcv::FMT_RGBf32}, + {52, 39, 60, 45, 37, 31, 4, 5, NVCV_INTERP_LINEAR, 1, 1, 0, true, nvcv::FMT_BGR8p, nvcv::FMT_BGR8, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, +}); + +// clang-format on + +TEST_P(OpResizeCropConvertReformatPlanar, tensor_matches_interleaved) +{ + RunPlanarParityTensorCase(GetParamValue<13>(), GetParamValue<14>(), GetParamValue<15>(), GetParamValue<16>(), + GetParamValue<0>(), GetParamValue<1>(), GetParamValue<2>(), GetParamValue<3>(), + GetParamValue<4>(), GetParamValue<5>(), GetParamValue<6>(), GetParamValue<7>(), + GetParamValue<8>(), GetParamValue<9>(), GetParamValue<10>(), GetParamValue<11>(), + GetParamValue<12>()); +} + +TEST_P(OpResizeCropConvertReformatPlanar, varshape_matches_interleaved) +{ + RunPlanarParityVarShapeCase(GetParamValue<13>(), GetParamValue<14>(), GetParamValue<15>(), GetParamValue<16>(), + GetParamValue<0>(), GetParamValue<1>(), GetParamValue<2>(), GetParamValue<3>(), + GetParamValue<4>(), GetParamValue<5>(), GetParamValue<6>(), GetParamValue<7>(), + GetParamValue<8>(), GetParamValue<9>(), GetParamValue<10>(), GetParamValue<11>(), + GetParamValue<12>()); +} + #define _TEST_ROW(Interp, inputBatch, outputBatch, srcFmt, dstFmt, DstSize, CropPos, ResizeDim, SrcType, DstType, \ returnCode) \ ttype::Types, ttype::Value, ttype::Value, ttype::Value, \ @@ -584,7 +948,7 @@ ttype::Types< // different channels _TEST_ROW(NVCV_INTERP_LINEAR, 2, 2, NVCV_IMAGE_FORMAT_RGB8, NVCV_IMAGE_FORMAT_BGRA8, int2(4, 4), int2(0, 0), NVCVSize2D(16, 16), uchar3, uint8_t, NVCV_ERROR_NOT_COMPATIBLE), _TEST_ROW(NVCV_INTERP_LINEAR, 2, 2, NVCV_IMAGE_FORMAT_RGB8, NVCV_IMAGE_FORMAT_BGRA8, int2(4, 4), int2(0, 0), NVCVSize2D(16, 16), uchar3, uint8_t, NVCV_ERROR_NOT_COMPATIBLE), - // not equal to 3 channels + // unsupported channel count (4) _TEST_ROW(NVCV_INTERP_LINEAR, 2, 2, NVCV_IMAGE_FORMAT_RGBA8, NVCV_IMAGE_FORMAT_BGRA8, int2(4, 4), int2(0, 0), NVCVSize2D(16, 16), uchar3, uint8_t, NVCV_ERROR_NOT_COMPATIBLE), _TEST_ROW(NVCV_INTERP_LINEAR, 2, 2, NVCV_IMAGE_FORMAT_RGBA8, NVCV_IMAGE_FORMAT_BGRA8, int2(4, 4), int2(0, 0), NVCVSize2D(16, 16), uchar3, uint8_t, NVCV_ERROR_NOT_COMPATIBLE), // input is not uchar @@ -596,8 +960,6 @@ ttype::Types< _TEST_ROW(NVCV_INTERP_LINEAR, 2, 2, NVCV_IMAGE_FORMAT_RGB8, NVCV_IMAGE_FORMAT_BGR8, int2(4, 4), int2(0, -1), NVCVSize2D(16, 16), uchar3, uint8_t, NVCV_ERROR_INVALID_ARGUMENT), _TEST_ROW(NVCV_INTERP_LINEAR, 2, 2, NVCV_IMAGE_FORMAT_RGB8, NVCV_IMAGE_FORMAT_BGR8, int2(32, 4), int2(0, 0), NVCVSize2D(16, 16), uchar3, uint8_t, NVCV_ERROR_INVALID_ARGUMENT), _TEST_ROW(NVCV_INTERP_LINEAR, 2, 2, NVCV_IMAGE_FORMAT_RGB8, NVCV_IMAGE_FORMAT_BGR8, int2(4, 32), int2(0, 0), NVCVSize2D(16, 16), uchar3, uint8_t, NVCV_ERROR_INVALID_ARGUMENT), - // invalid input layout - _TEST_ROW(NVCV_INTERP_LINEAR, 2, 2, NVCV_IMAGE_FORMAT_RGB8p, NVCV_IMAGE_FORMAT_BGR8, int2(4, 4), int2(0, 0), NVCVSize2D(16, 16), uchar3, uint8_t, NVCV_ERROR_NOT_COMPATIBLE), // invalid resize dim _TEST_ROW(NVCV_INTERP_LINEAR, 2, 2, NVCV_IMAGE_FORMAT_RGB8, NVCV_IMAGE_FORMAT_BGR8, int2(4, 4), int2(0, 0), NVCVSize2D(0, 0), uchar3, uint8_t, NVCV_ERROR_INVALID_ARGUMENT) >); @@ -612,92 +974,57 @@ TEST(OpResizeCropConvertReformat_Negative, createWillNullPtr) TYPED_TEST(OpResizeCropConvertReformat_Negative, infer_negative_parameter) { - NVCVInterpolationType interp = ttype::GetValue; - - int inputBatchSize = ttype::GetValue; - int outputBatchSize = ttype::GetValue; - - nvcv::ImageFormat srcFormat{ttype::GetValue}; - nvcv::ImageFormat dstFormat{ttype::GetValue}; - - int2 cropDim = ttype::GetValue; - int2 cropPos = ttype::GetValue; - NVCVSize2D resizeDim = ttype::GetValue; - - using SrcVT = typename ttype::GetType; - using DstVT = typename ttype::GetType; - using SrcBT = typename cuda::BaseType; - using DstBT = typename cuda::BaseType; - - NVCVStatus expectedReturnCode = ttype::GetValue; + const InferNegativeCaseParams params = GetInferNegativeCaseParams(); // Resize to 16 * 16 then crop int srcW = 32; int srcH = 32; - int dstW = cropDim.x; - int dstH = cropDim.y; - - NVCVChannelManip manip = ChannelManip(srcFormat, dstFormat); + int dstW = params.cropDim.x; + int dstH = params.cropDim.y; // Create input and output tensors. - nvcv::Tensor srcTensor = nvcv::util::CreateTensor(inputBatchSize, srcW, srcH, srcFormat); - nvcv::Tensor dstTensor = nvcv::util::CreateTensor(outputBatchSize, dstW, dstH, dstFormat); + nvcv::Tensor srcTensor = nvcv::util::CreateTensor(params.inputBatchSize, srcW, srcH, params.srcFormat); + nvcv::Tensor dstTensor = nvcv::util::CreateTensor(params.outputBatchSize, dstW, dstH, params.dstFormat); cvcuda::ResizeCropConvertReformat resizeCrop; - EXPECT_EQ(expectedReturnCode, - nvcv::ProtectCall([&] { resizeCrop(nullptr, srcTensor, dstTensor, resizeDim, interp, cropPos, manip); })); + EXPECT_EQ(params.expectedReturnCode, nvcv::ProtectCall( + [&resizeCrop, &srcTensor, &dstTensor, ¶ms] { + resizeCrop(nullptr, srcTensor, dstTensor, params.resizeDim, + params.interp, params.cropPos, params.manip); + })); } TYPED_TEST(OpResizeCropConvertReformat_Negative, varshape_infer_negative_parameter) { - NVCVInterpolationType interp = ttype::GetValue; - - int inputBatchSize = ttype::GetValue; - int outputBatchSize = ttype::GetValue; - - nvcv::ImageFormat srcFormat{ttype::GetValue}; - nvcv::ImageFormat dstFormat{ttype::GetValue}; - - int2 cropDim = ttype::GetValue; - int2 cropPos = ttype::GetValue; - NVCVSize2D resizeDim = ttype::GetValue; - - using SrcVT = typename ttype::GetType; - using DstVT = typename ttype::GetType; - using SrcBT = typename cuda::BaseType; - using DstBT = typename cuda::BaseType; - - NVCVStatus expectedReturnCode = ttype::GetValue; + const InferNegativeCaseParams params = GetInferNegativeCaseParams(); std::vector srcImg; int srcW = 32; int srcH = 32; - int dstW = cropDim.x; - int dstH = cropDim.y; - - NVCVChannelManip manip = ChannelManip(srcFormat, dstFormat); - - uniform_dist randVal(std::is_integral_v ? cuda::TypeTraits::min : SrcBT{0}, - std::is_integral_v ? cuda::TypeTraits::max : SrcBT{1}); + int dstW = params.cropDim.x; + int dstH = params.cropDim.y; - std::uniform_int_distribution randW(srcW * 0.8, srcW * 1.2); - std::uniform_int_distribution randH(srcH * 0.8, srcH * 1.2); + std::uniform_int_distribution randW(ScaledSize(srcW, 0.8), ScaledSize(srcW, 1.2)); + std::uniform_int_distribution randH(ScaledSize(srcH, 0.8), ScaledSize(srcH, 1.2)); - for (int i = 0; i < inputBatchSize; ++i) + for (int i = 0; i < params.inputBatchSize; ++i) { - int imgW = (interp ? randW(randEng) : srcW); - int imgH = (interp ? randH(randEng) : srcH); + int imgW = (params.interp ? randW(Rng()) : srcW); + int imgH = (params.interp ? randH(Rng()) : srcH); - srcImg.emplace_back(nvcv::Size2D{imgW, imgH}, srcFormat); + srcImg.emplace_back(nvcv::Size2D{imgW, imgH}, params.srcFormat); } - nvcv::ImageBatchVarShape src(inputBatchSize); + nvcv::ImageBatchVarShape src(params.inputBatchSize); src.pushBack(srcImg.begin(), srcImg.end()); - nvcv::Tensor dstTensor = nvcv::util::CreateTensor(outputBatchSize, dstW, dstH, dstFormat); + nvcv::Tensor dstTensor = nvcv::util::CreateTensor(params.outputBatchSize, dstW, dstH, params.dstFormat); cvcuda::ResizeCropConvertReformat resizeCrop; - EXPECT_EQ(expectedReturnCode, - nvcv::ProtectCall([&] { resizeCrop(nullptr, src, dstTensor, resizeDim, interp, cropPos, manip); })); + EXPECT_EQ(params.expectedReturnCode, nvcv::ProtectCall( + [&resizeCrop, &src, &dstTensor, ¶ms] { + resizeCrop(nullptr, src, dstTensor, params.resizeDim, params.interp, + params.cropPos, params.manip); + })); } diff --git a/tests/cvcuda/system/TestOpRotate.cpp b/tests/cvcuda/system/TestOpRotate.cpp index e150f755e..632c37184 100644 --- a/tests/cvcuda/system/TestOpRotate.cpp +++ b/tests/cvcuda/system/TestOpRotate.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,6 +16,7 @@ */ #include "Definitions.hpp" +#include "PlanarParityUtils.hpp" #include #include @@ -26,6 +27,7 @@ #include #include +#include #include #include @@ -33,10 +35,15 @@ namespace t = ::testing; namespace test = nvcv::test; namespace cuda = nvcv::cuda; -#define PI 3.1415926535897932384626433832795 +constexpr double PI = 3.1415926535897932384626433832795; // NOSONAR: std::numbers::pi is C++20. // #define DBG_ROTATE 1 +static int ScaledSize(int size, double scale) +{ + return static_cast(static_cast(size) * scale); +} + static void compute_warpAffine(const double angle, const double xShift, const double yShift, double *aCoeffs) { aCoeffs[0] = cos(angle * PI / 180); @@ -50,21 +57,23 @@ static void compute_warpAffine(const double angle, const double xShift, const do static void compute_center_shift(const int center_x, const int center_y, const double angle, double &xShift, double &yShift) { - xShift = (1 - cos(angle * PI / 180)) * center_x - sin(angle * PI / 180) * center_y; - yShift = sin(angle * PI / 180) * center_x + (1 - cos(angle * PI / 180)) * center_y; + xShift = (1 - cos(angle * PI / 180)) * static_cast(center_x) + - sin(angle * PI / 180) * static_cast(center_y); + yShift = sin(angle * PI / 180) * static_cast(center_x) + + (1 - cos(angle * PI / 180)) * static_cast(center_y); } static void assignCustomValuesInSrc(std::vector &srcVec, int srcWidth, int srcHeight, int srcVecRowStride) { - int initialValue = 1; - int pixelBytes = static_cast(srcVecRowStride / srcWidth); + int initialValue = 1; + auto pixelBytes = srcVecRowStride / srcWidth; for (int i = 0; i < srcHeight; i++) { for (int j = 0; j < srcVecRowStride; j = j + pixelBytes) { for (int k = 0; k < pixelBytes; k++) { - srcVec[i * srcVecRowStride + j + k] = initialValue; + srcVec[i * srcVecRowStride + j + k] = static_cast(initialValue); } initialValue++; } @@ -84,10 +93,72 @@ static void assignCustomValuesInSrc(std::vector &srcVec, int srcWidth, #endif } +static bool IsInsideSource(float src_x, float src_y, int width, int height) +{ + return src_x > -0.5f && src_x < static_cast(width) && src_y > -0.5f && src_y < static_cast(height); +} + +template +static void StoreLinearPixel(T *dstPtr, int dstBase, const T *srcPtr, int srcRowStride, int elementsPerPixel, + float src_x, float src_y, int width, int height) +{ + if (!IsInsideSource(src_x, src_y, width, height)) + { + return; + } + + const int x1 = cuda::round(src_x); + const int y1 = cuda::round(src_y); + + const int x2 = x1 + 1; + const int y2 = y1 + 1; + const int x1_read = std::max(x1, 0); + const int y1_read = std::max(y1, 0); + const int x2_read = std::min(x2, width - 1); + const int y2_read = std::min(y2, height - 1); + + for (int k = 0; k < elementsPerPixel; k++) + { + float out = 0.; + + T src_reg = srcPtr[y1_read * srcRowStride + x1_read * elementsPerPixel + k]; + out = out + src_reg * ((static_cast(x2) - src_x) * (static_cast(y2) - src_y)); + + src_reg = srcPtr[y1_read * srcRowStride + x2_read * elementsPerPixel + k]; + out = out + src_reg * ((src_x - static_cast(x1)) * (static_cast(y2) - src_y)); + + src_reg = srcPtr[y2_read * srcRowStride + x1_read * elementsPerPixel + k]; + out = out + src_reg * ((static_cast(x2) - src_x) * (src_y - static_cast(y1))); + + src_reg = srcPtr[y2_read * srcRowStride + x2_read * elementsPerPixel + k]; + out = out + src_reg * ((src_x - static_cast(x1)) * (src_y - static_cast(y1))); + + dstPtr[dstBase + k] = cuda::SaturateCast(out); + } +} + +template +static void StoreNearestPixel(T *dstPtr, int dstBase, const T *srcPtr, int srcRowStride, int elementsPerPixel, + float src_x, float src_y, int width, int height) +{ + if (!IsInsideSource(src_x, src_y, width, height)) + { + return; + } + + const int x1 = std::min(cuda::round(src_x + .5f), width - 1); + const int y1 = std::min(cuda::round(src_y + .5f), height - 1); + + for (int k = 0; k < elementsPerPixel; k++) + { + dstPtr[dstBase + k] = srcPtr[y1 * srcRowStride + x1 * elementsPerPixel + k]; + } +} + template static void Rotate(std::vector &hDst, int dstRowStride, nvcv::Size2D dstSize, const std::vector &hSrc, - int srcRowStride, nvcv::Size2D srcSize, nvcv::ImageFormat fmt, const double angleDeg, - const double2 shift, NVCVInterpolationType interpolation) + int srcRowStride, nvcv::Size2D, nvcv::ImageFormat fmt, const double angleDeg, const double2 shift, + NVCVInterpolationType interpolation) { assert(fmt.numPlanes() == 1); @@ -97,8 +168,8 @@ static void Rotate(std::vector &hDst, int dstRowStride, nvcv::Size2D dstSize, const T *srcPtr = hSrc.data(); // calculate coefficients - double d_aCoeffs[6]; - compute_warpAffine(angleDeg, shift.x, shift.y, d_aCoeffs); + std::array d_aCoeffs; + compute_warpAffine(angleDeg, shift.x, shift.y, d_aCoeffs.data()); int width = dstSize.w; int height = dstSize.h; @@ -110,59 +181,21 @@ static void Rotate(std::vector &hDst, int dstRowStride, nvcv::Size2D dstSize, const double dst_x_shift = dst_x - d_aCoeffs[2]; const double dst_y_shift = dst_y - d_aCoeffs[5]; - float src_x = (float)(dst_x_shift * d_aCoeffs[0] + dst_y_shift * (-d_aCoeffs[1])); - float src_y = (float)(dst_x_shift * (-d_aCoeffs[3]) + dst_y_shift * d_aCoeffs[4]); + auto src_x = static_cast(dst_x_shift * d_aCoeffs[0] + dst_y_shift * (-d_aCoeffs[1])); + auto src_y = static_cast(dst_x_shift * (-d_aCoeffs[3]) + dst_y_shift * d_aCoeffs[4]); if (interpolation == NVCV_INTERP_LINEAR) { - if (src_x > -0.5 && src_x < width && src_y > -0.5 && src_y < height) - { - const int x1 = cuda::round(src_x); - const int y1 = cuda::round(src_y); - - const int x2 = x1 + 1; - const int y2 = y1 + 1; - const int x1_read = std::max(x1, 0); - const int y1_read = std::max(y1, 0); - const int x2_read = std::min(x2, width - 1); - const int y2_read = std::min(y2, height - 1); - - for (int k = 0; k < elementsPerPixel; k++) - { - float out = 0.; - - T src_reg = srcPtr[y1_read * srcRowStride + x1_read * elementsPerPixel + k]; - out = out + src_reg * ((x2 - src_x) * (y2 - src_y)); - - src_reg = srcPtr[y1_read * srcRowStride + x2_read * elementsPerPixel + k]; - out = out + src_reg * ((src_x - x1) * (y2 - src_y)); - - src_reg = srcPtr[y2_read * srcRowStride + x1_read * elementsPerPixel + k]; - out = out + src_reg * ((x2 - src_x) * (src_y - y1)); - - src_reg = srcPtr[y2_read * srcRowStride + x2_read * elementsPerPixel + k]; - out = out + src_reg * ((src_x - x1) * (src_y - y1)); - - dstPtr[dst_y * dstRowStride + dst_x * elementsPerPixel + k] = cuda::SaturateCast(out); - } - } + StoreLinearPixel(dstPtr, dst_y * dstRowStride + dst_x * elementsPerPixel, srcPtr, srcRowStride, + elementsPerPixel, src_x, src_y, width, height); } else if (interpolation == NVCV_INTERP_NEAREST || interpolation == NVCV_INTERP_CUBIC) { /* Use this for NVCV_INTERP_CUBIC interpolation only for angles - {90, 180} */ - if (src_x > -0.5 && src_x < width && src_y > -0.5 && src_y < height) - { - const int x1 = std::min(cuda::round(src_x + .5f), width - 1); - const int y1 = std::min(cuda::round(src_y + .5f), height - 1); - - for (int k = 0; k < elementsPerPixel; k++) - { - dstPtr[dst_y * dstRowStride + dst_x * elementsPerPixel + k] - = srcPtr[y1 * srcRowStride + x1 * elementsPerPixel + k]; - } - } + StoreNearestPixel(dstPtr, dst_y * dstRowStride + dst_x * elementsPerPixel, srcPtr, srcRowStride, + elementsPerPixel, src_x, src_y, width, height); } } } @@ -232,7 +265,7 @@ TEST_P(OpRotate, tensor_correct_output) for (int i = 0; i < numberOfImages; ++i) { srcVec[i].resize(srcHeight * srcVecRowStride); - std::generate(srcVec[i].begin(), srcVec[i].end(), [&]() { return 0; }); + std::ranges::generate(srcVec[i], []() { return 0; }); // Assign custom values in input vector assignCustomValuesInSrc(srcVec[i], srcWidth, srcHeight, srcVecRowStride); @@ -248,7 +281,8 @@ TEST_P(OpRotate, tensor_correct_output) nvcv::Tensor imgDst(numberOfImages, {dstWidth, dstHeight}, fmt); // Compute shiftX, shiftY using center - int center_x = (srcWidth - 1) / 2, center_y = (srcHeight - 1) / 2; + int center_x = (srcWidth - 1) / 2; + int center_y = (srcHeight - 1) / 2; compute_center_shift(center_x, center_y, angleDeg, shiftX, shiftY); cvcuda::Rotate RotateOp(0); @@ -279,7 +313,7 @@ TEST_P(OpRotate, tensor_correct_output) dstHeight, cudaMemcpyDeviceToHost)); std::vector goldVec(dstHeight * dstVecRowStride); - std::generate(goldVec.begin(), goldVec.end(), [&]() { return 0; }); + std::ranges::generate(goldVec, []() { return 0; }); // Generate gold result Rotate(goldVec, dstVecRowStride, {dstWidth, dstHeight}, srcVec[i], srcVecRowStride, @@ -330,10 +364,10 @@ TEST_P(OpRotate, varshape_correct_output) const nvcv::ImageFormat fmt = nvcv::FMT_RGB8; // Create input and output - std::default_random_engine randEng; - std::uniform_int_distribution rndSrcWidth(srcWidthBase * 0.8, srcWidthBase * 1.1); - std::uniform_int_distribution rndSrcHeight(srcHeightBase * 0.8, srcHeightBase * 1.1); - std::uniform_int_distribution rndAngle(0, 360); + std::default_random_engine randEng; + std::uniform_int_distribution rndSrcWidth(ScaledSize(srcWidthBase, 0.8), ScaledSize(srcWidthBase, 1.1)); + std::uniform_int_distribution rndSrcHeight(ScaledSize(srcHeightBase, 0.8), ScaledSize(srcHeightBase, 1.1)); + std::uniform_int_distribution rndAngle(0, 360); nvcv::Tensor angleDegTensor(nvcv::TensorShape({numberOfImages}, "N"), nvcv::TYPE_F64); auto angleDegTensorData = angleDegTensor.exportData(); @@ -346,7 +380,9 @@ TEST_P(OpRotate, varshape_correct_output) auto shiftTensorDataAccess = nvcv::TensorDataAccessStrided::Create(*shiftTensorData); ASSERT_TRUE(shiftTensorDataAccess); - std::vector imgSrc, imgDst; + std::vector imgSrc; + + std::vector imgDst; std::vector angleDegVecs; std::vector shiftVecs; @@ -371,7 +407,8 @@ TEST_P(OpRotate, varshape_correct_output) } // Compute shiftX, shiftY using center - int center_x = (tmpWidth - 1) / 2, center_y = (tmpHeight - 1) / 2; + int center_x = (tmpWidth - 1) / 2; + int center_y = (tmpHeight - 1) / 2; compute_center_shift(center_x, center_y, angleDeg, shift.x, shift.y); angleDegVecs.push_back(angleDeg); @@ -410,7 +447,7 @@ TEST_P(OpRotate, varshape_correct_output) std::uniform_int_distribution rand(0, 255); srcVec[i].resize(srcHeight * srcRowStride); - std::generate(srcVec[i].begin(), srcVec[i].end(), [&]() { return 0; }); + std::ranges::generate(srcVec[i], []() { return 0; }); // Assign custom values in input vector assignCustomValuesInSrc(srcVec[i], srcWidth, srcHeight, srcRowStride); @@ -458,7 +495,7 @@ TEST_P(OpRotate, varshape_correct_output) dstHeight, cudaMemcpyDeviceToHost)); std::vector goldVec(dstHeight * dstRowStride); - std::generate(goldVec.begin(), goldVec.end(), [&]() { return 0; }); + std::ranges::generate(goldVec, []() { return 0; }); // Generate gold result Rotate(goldVec, dstRowStride, {dstWidth, dstHeight}, srcVec[i], srcRowStride, {srcWidth, srcHeight}, @@ -468,11 +505,150 @@ TEST_P(OpRotate, varshape_correct_output) } } +// ============================================================================= +// Planar (NCHW/CHW) layout support +// +// Rotate maps each output pixel to a source pixel with the same per-image affine coefficients +// regardless of the channel, so a planar input is rotated plane-by-plane and must produce exactly the +// same pixels as the interleaved path. These tests feed identical data in both layouts through +// cvcuda::Rotate and require the (re-interleaved) planar output to match the interleaved output +// bit-for-bit, for every dtype and interpolation type. +// +// Unlike Resize/Flip, Rotate leaves out-of-bounds destination pixels unwritten (the source maps +// outside the image under a replicate border guard), so the destination is zero-filled before each +// run; a constant byte value is layout-invariant, keeping uncovered regions equal across layouts. +// ============================================================================= + +namespace { + +// Zero every plane of every sample on the stream. +void ZeroTensor(const nvcv::Tensor &t, cudaStream_t stream) +{ + auto data = t.exportData(); + ASSERT_NE(data, nvcv::NullOpt); + auto acc = nvcv::TensorDataAccessStridedImagePlanar::Create(*data); + ASSERT_TRUE(acc); + for (int n = 0; n < acc->numSamples(); ++n) + { + for (int p = 0; p < acc->numPlanes(); ++p) + { + ASSERT_EQ(cudaSuccess, cudaMemset2DAsync(acc->planeData(p, acc->sampleData(n)), acc->rowStride(), 0, + acc->rowStride(), acc->numRows(), stream)); + } + } +} + +// Zero every plane of every image in a var-shape batch on the stream. +void ZeroVarShapeBatch(const nvcv::ImageBatchVarShape &batch, cudaStream_t stream) +{ + for (int i = 0; i < batch.numImages(); ++i) + { + auto data = batch[i].exportData(); + ASSERT_NE(data, nvcv::NullOpt); + for (int p = 0; p < data->numPlanes(); ++p) + { + const auto &plane = data->plane(p); + ASSERT_EQ(cudaSuccess, + cudaMemsetAsync(plane.basePtr, 0, static_cast(plane.rowStride) * plane.height, stream)); + } + } +} + +// Rotate identical data in interleaved and planar tensor layout; outputs must match bit-for-bit. The +// shared scaffolding (upload/run/download/compare) lives in PlanarParityUtils.hpp; here we only bind +// the Rotate call (with a center-shift so the rotation pivots about the image center, like the other +// Rotate tests). +void RunPlanarParityTensorCase(nvcv::ImageFormat planarFmt, nvcv::ImageFormat interleavedFmt, int w, int h, + NVCVInterpolationType interp, double angleDeg, int numImages) +{ + double shiftX = 0; + double shiftY = 0; + compute_center_shift((w - 1) / 2, (h - 1) / 2, angleDeg, shiftX, shiftY); + const double2 shift = {shiftX, shiftY}; + + test::planar::RunTensorParity( + planarFmt, interleavedFmt, w, h, w, h, numImages, + [angleDeg, shift, interp](cudaStream_t s, const nvcv::Tensor &src, const nvcv::Tensor &dst, nvcv::ImageFormat) + { + ZeroTensor(dst, s); + cvcuda::Rotate op(0); + EXPECT_NO_THROW(op(s, src, dst, angleDeg, shift, interp)); + }); +} + +// Var-shape counterpart of RunPlanarParityTensorCase. Var-shape Rotate takes per-image angle/shift +// tensors; upload them once (synchronously, so they are ready before the op runs on the parity +// helper's stream). All images share the same transform here. +void RunPlanarParityVarShapeCase(nvcv::ImageFormat planarFmt, nvcv::ImageFormat interleavedFmt, int w, int h, + NVCVInterpolationType interp, double angleDeg, int numImages) +{ + double shiftX = 0; + double shiftY = 0; + compute_center_shift((w - 1) / 2, (h - 1) / 2, angleDeg, shiftX, shiftY); + + nvcv::Tensor angleDegTensor(nvcv::TensorShape({numImages}, "N"), nvcv::TYPE_F64); + nvcv::Tensor shiftTensor(nvcv::TensorShape({numImages, 2}, nvcv::TENSOR_NW), nvcv::TYPE_F64); + { + std::vector angles(numImages, angleDeg); + std::vector shifts(numImages, double2{shiftX, shiftY}); + + auto angleData = angleDegTensor.exportData(); + auto shiftData = shiftTensor.exportData(); + ASSERT_NE(angleData, nvcv::NullOpt); + ASSERT_NE(shiftData, nvcv::NullOpt); + auto shiftAcc = nvcv::TensorDataAccessStrided::Create(*shiftData); + ASSERT_TRUE(shiftAcc); + + ASSERT_EQ(cudaSuccess, cudaMemcpy(angleData->basePtr(), angles.data(), angles.size() * sizeof(double), + cudaMemcpyHostToDevice)); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(shiftAcc->sampleData(0), shiftAcc->sampleStride(), shifts.data(), + sizeof(double2), sizeof(double2), numImages, cudaMemcpyHostToDevice)); + } + + test::planar::RunVarShapeParity( + planarFmt, interleavedFmt, w, h, w, h, numImages, + [numImages, &angleDegTensor, &shiftTensor, interp](cudaStream_t s, const nvcv::ImageBatchVarShape &src, + const nvcv::ImageBatchVarShape &dst, nvcv::ImageFormat) + { + ZeroVarShapeBatch(dst, s); + cvcuda::Rotate op(numImages); + EXPECT_NO_THROW(op(s, src, dst, angleDegTensor, shiftTensor, interp)); + }); +} + +} // namespace + +// Parameters: width, height, interpolation, angle (deg), numImages, planarFmt, interleavedFmt +// clang-format off +NVCV_TEST_SUITE_P(OpRotatePlanar, + test::ValueList{ + {176, 113, NVCV_INTERP_NEAREST, 90, 2, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, // RGB8, nearest + {123, 66, NVCV_INTERP_LINEAR, 45, 2, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, // RGB8, linear, fractional + { 64, 48, NVCV_INTERP_CUBIC, 30, 1, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, // RGB8, cubic, fractional + { 50, 40, NVCV_INTERP_NEAREST, 90, 2, nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8}, // RGBA8, nearest + {100, 80, NVCV_INTERP_LINEAR, 60, 1, nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8}, // RGBA8, linear, fractional + { 64, 48, NVCV_INTERP_CUBIC, 45, 2, nvcv::FMT_RGBAf32p, nvcv::FMT_RGBAf32}, // float, cubic, fractional + { 72, 56, NVCV_INTERP_LINEAR, 120, 1, nvcv::FMT_RGBAf32p, nvcv::FMT_RGBAf32}, // float, linear +}); + +// clang-format on + +TEST_P(OpRotatePlanar, tensor_matches_interleaved) +{ + RunPlanarParityTensorCase(GetParamValue<5>(), GetParamValue<6>(), GetParamValue<0>(), GetParamValue<1>(), + GetParamValue<2>(), GetParamValue<3>(), GetParamValue<4>()); +} + +TEST_P(OpRotatePlanar, varshape_matches_interleaved) +{ + RunPlanarParityVarShapeCase(GetParamValue<5>(), GetParamValue<6>(), GetParamValue<0>(), GetParamValue<1>(), + GetParamValue<2>(), GetParamValue<3>(), GetParamValue<4>()); +} + // clang-format off NVCV_TEST_SUITE_P(OpRotate_Negative, test::ValueList{ {nvcv::FMT_RGB8, nvcv::FMT_RGB8, NVCV_INTERP_LANCZOS}, - {nvcv::FMT_RGB8, nvcv::FMT_RGB8p, NVCV_INTERP_NEAREST}, - {nvcv::FMT_RGB8p, nvcv::FMT_RGB8p, NVCV_INTERP_NEAREST}, + {nvcv::FMT_RGB8, nvcv::FMT_RGB8p, NVCV_INTERP_NEAREST}, // data format is different (interleaved in, planar out) {nvcv::FMT_RGBf16, nvcv::FMT_RGBf16, NVCV_INTERP_NEAREST}, }); @@ -480,8 +656,7 @@ NVCV_TEST_SUITE_P(OpRotateVarshape_Negative, test::ValueList rndSrcWidth(srcWidthBase * 0.8, srcWidthBase * 1.1); - std::uniform_int_distribution rndSrcHeight(srcHeightBase * 0.8, srcHeightBase * 1.1); + std::default_random_engine randEng; + std::uniform_int_distribution rndSrcWidth(ScaledSize(srcWidthBase, 0.8), ScaledSize(srcWidthBase, 1.1)); + std::uniform_int_distribution rndSrcHeight(ScaledSize(srcHeightBase, 0.8), ScaledSize(srcHeightBase, 1.1)); nvcv::Tensor angleDegTensor(nvcv::TensorShape({numberOfImages}, "N"), angleDataType); nvcv::Tensor shiftTensor(nvcv::TensorShape({numberOfImages, 2}, nvcv::TENSOR_NW), shiftDataType); - std::vector imgSrc, imgDst; + std::vector imgSrc; + + std::vector imgDst; for (int i = 0; i < numberOfImages; ++i) { @@ -558,7 +736,8 @@ TEST_P(OpRotateVarshape_Negative, op) cvcuda::Rotate rotateOp(maxVarShapeBatchSize); EXPECT_EQ( NVCV_ERROR_INVALID_ARGUMENT, - nvcv::ProtectCall([&] { rotateOp(stream, batchSrc, batchDst, angleDegTensor, shiftTensor, interpolation); })); + nvcv::ProtectCall([&rotateOp, &stream, &batchSrc, &batchDst, &angleDegTensor, &shiftTensor, &interpolation] + { rotateOp(stream, batchSrc, batchDst, angleDegTensor, shiftTensor, interpolation); })); // Get test data back EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); @@ -581,20 +760,19 @@ TEST(OpRotate_Negative, varshape_hasDifferentFormat) {nvcv::FMT_RGBA8, fmt}, { fmt, nvcv::FMT_RGBA8} }; - for (auto testCase : testSet) + for (const auto &[inputFmtExtra, outputFmtExtra] : testSet) { - nvcv::ImageFormat inputFmtExtra = std::get<0>(testCase); - nvcv::ImageFormat outputFmtExtra = std::get<1>(testCase); - // Create input and output - std::default_random_engine randEng; - std::uniform_int_distribution rndSrcWidth(srcWidthBase * 0.8, srcWidthBase * 1.1); - std::uniform_int_distribution rndSrcHeight(srcHeightBase * 0.8, srcHeightBase * 1.1); + std::default_random_engine randEng; + std::uniform_int_distribution rndSrcWidth(ScaledSize(srcWidthBase, 0.8), ScaledSize(srcWidthBase, 1.1)); + std::uniform_int_distribution rndSrcHeight(ScaledSize(srcHeightBase, 0.8), ScaledSize(srcHeightBase, 1.1)); nvcv::Tensor angleDegTensor(nvcv::TensorShape({numberOfImages}, "N"), nvcv::TYPE_F64); nvcv::Tensor shiftTensor(nvcv::TensorShape({numberOfImages, 2}, nvcv::TENSOR_NW), nvcv::TYPE_F64); - std::vector imgSrc, imgDst; + std::vector imgSrc; + + std::vector imgDst; for (int i = 0; i < numberOfImages - 1; ++i) { @@ -614,9 +792,10 @@ TEST(OpRotate_Negative, varshape_hasDifferentFormat) batchDst.pushBack(imgDst.begin(), imgDst.end()); cvcuda::Rotate rotateOp(numberOfImages); - EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, - nvcv::ProtectCall( - [&] { rotateOp(stream, batchSrc, batchDst, angleDegTensor, shiftTensor, interpolation); })); + EXPECT_EQ( + NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&rotateOp, &stream, &batchSrc, &batchDst, &angleDegTensor, &shiftTensor, &interpolation] + { rotateOp(stream, batchSrc, batchDst, angleDegTensor, shiftTensor, interpolation); })); } EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); diff --git a/tests/cvcuda/system/TestOpSIFT.cpp b/tests/cvcuda/system/TestOpSIFT.cpp index 032c0d1c2..1ad401b0d 100644 --- a/tests/cvcuda/system/TestOpSIFT.cpp +++ b/tests/cvcuda/system/TestOpSIFT.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ #include "ConvUtils.hpp" #include "Definitions.hpp" +#include "PlanarParityUtils.hpp" #include #include @@ -32,6 +33,9 @@ #include #include #include +#include +#include +#include #include #include @@ -57,7 +61,7 @@ struct DescriptorType int hammingDist = 0; for (int i = 0; i < static_cast(data.size()); i++) { - hammingDist += std::bitset<8>(data[i] ^ other.data[i]).count(); + hammingDist += std::bitset<8>(data[i] ^ other.data[i]).count(); // NOSONAR: SIFT descriptors are bytes. } return (hammingDist < static_cast((data.size() * sizeof(uint8_t) * 8) / 100)); } @@ -66,9 +70,10 @@ struct DescriptorType { std::ios_base::fmtflags f{out.flags()}; out << "0x"; - for (int i = 0; i < (int)desc.data.size(); i++) + for (uint8_t byte : desc.data) { - out << std::hex << std::setfill('0') << std::setw(2) << static_cast(desc.data[i]); + out << std::hex << std::setfill('0') << std::setw(2) // NOSONAR: std::format is C++20. + << static_cast(byte); } out.flags(f); return out; @@ -95,23 +100,76 @@ constexpr int kHistogramBins = 36; constexpr int kDescHistBins = 8; constexpr int kDescOriRadius = 3; constexpr int kDescWidth = 4; -constexpr float kDescWidthToRadius = M_SQRT2 * (kDescWidth + 1) * .5f; +constexpr float kDescWidthToRadius = static_cast(M_SQRT2) * static_cast(kDescWidth + 1) * .5f; constexpr int kDescMaxRadius = 51; constexpr float kDescWeightScale = -1.f / (kDescWidth * kDescWidth * .5f); constexpr float kDescHistPeakRatio = .2f; +constexpr int kDescHistSize = (kDescWidth + 2) * (kDescWidth + 2) * (kDescHistBins + 2); -static std::default_random_engine g_rng(0); // seed 0 to fix pseudo-randomness +using DescriptorHistogram = std::array; +using OrientationHistogram = std::array; + +static nvcv::Tensor CreateSIFTInputTensor(int3 inShape, std::string_view layout) +{ + if (layout == "HWC") + { + EXPECT_EQ(inShape.z, 1); + return nvcv::Tensor( + { + {inShape.y, inShape.x, 1}, + "HWC" + }, + nvcv::TYPE_U8); + } + if (layout == "CHW") + { + EXPECT_EQ(inShape.z, 1); + return nvcv::Tensor( + { + {1, inShape.y, inShape.x}, + "CHW" + }, + nvcv::TYPE_U8); + } + if (layout == "NCHW") + { + return nvcv::Tensor( + { + {inShape.z, 1, inShape.y, inShape.x}, + "NCHW" + }, + nvcv::TYPE_U8); + } + + if (layout == "NHWC") + { + return nvcv::Tensor( + { + {inShape.z, inShape.y, inShape.x, 1}, + "NHWC" + }, + nvcv::TYPE_U8); + } + + throw std::invalid_argument("Unsupported SIFT input layout: " + std::string(layout)); +} + +static std::default_random_engine &Rng() +{ + static std::default_random_engine rng(0); // seed 0 to fix pseudo-randomness + return rng; +} // --------------------- Gold (reference) definitions -------------------------- inline int GoldNumberOfOctaves(int width, int height) { - return std::floor(std::log2(std::min(width, height))) - 2; + return static_cast(std::floor(std::log2(std::min(width, height)))) - 2; } inline nvcv::Size2D GoldKernelSize(float sigma) { - int ksize = std::min((int)std::round(sigma * 8 + 1) | 1, kMaxKernelSize.x); + int ksize = std::min(static_cast(std::round(sigma * 8.f + 1.f)) | 1, kMaxKernelSize.x); return nvcv::Size2D{ksize, ksize}; } @@ -120,18 +178,48 @@ inline void GoldGaussianSigmas(std::vector &layerSigmas, float initSigma, { layerSigmas[0] = initSigma; - float k = std::pow(2.0, 1.0 / numOctaveLayers); - float prevSigma, totalSigma; + float k = std::pow(2.f, 1.f / static_cast(numOctaveLayers)); + float prevSigma; + float totalSigma; for (int i = 1; i < numOctaveLayers + 3; i++) { - prevSigma = std::pow(k, i - 1) * initSigma; + prevSigma = std::pow(k, static_cast(i - 1)) * initSigma; totalSigma = k * prevSigma; layerSigmas[i] = std::sqrt(totalSigma * totalSigma - prevSigma * prevSigma); } } +template +inline void GoldCopyPixel(RawBufferType &dstVec, const long3 &dstStrides, const RawBufferType &srcVec, + const long3 &srcStrides, const long3 &srcShape, const float2 &srcScale, long x, long y, + long z) +{ + float2 srcCoord; + + if (srcScale.x >= 1.f) + { + srcCoord.x = static_cast(z) * srcScale.x; + srcCoord.y = static_cast(y) * srcScale.y; + + util::ValueAt(dstVec, dstStrides, long3{x, y, z}) + = test::GoldInterp( + srcVec, srcStrides, int2{static_cast(srcShape.z), static_cast(srcShape.y)}, U{}, kScale, + srcCoord, static_cast(x)); + } + else + { + srcCoord.x = (static_cast(z) + .5f) * srcScale.x - .5f; + srcCoord.y = (static_cast(y) + .5f) * srcScale.y - .5f; + + util::ValueAt(dstVec, dstStrides, long3{x, y, z}) + = test::GoldInterp( + srcVec, srcStrides, int2{static_cast(srcShape.z), static_cast(srcShape.y)}, U{}, kScale, + srcCoord, static_cast(x)); + } +} + template inline void GoldCopy(RawBufferType &dstVec, const long3 &dstStrides, const long3 &dstShape, const RawBufferType &srcVec, const long3 &srcStrides, const long3 &srcShape) @@ -146,26 +234,7 @@ inline void GoldCopy(RawBufferType &dstVec, const long3 &dstStrides, const long3 { for (long z = 0; z < dstShape.z; z++) { - float2 srcCoord; - - if (srcScale.x >= 1.f) - { - srcCoord.x = z * srcScale.x; - srcCoord.y = y * srcScale.y; - - util::ValueAt(dstVec, dstStrides, long3{x, y, z}) - = test::GoldInterp( - srcVec, srcStrides, int2{(int)srcShape.z, (int)srcShape.y}, U{}, kScale, srcCoord, x); - } - else - { - srcCoord.x = (z + .5f) * srcScale.x - .5f; - srcCoord.y = (y + .5f) * srcScale.y - .5f; - - util::ValueAt(dstVec, dstStrides, long3{x, y, z}) - = test::GoldInterp( - srcVec, srcStrides, int2{(int)srcShape.z, (int)srcShape.y}, U{}, kScale, srcCoord, x); - } + GoldCopyPixel(dstVec, dstStrides, srcVec, srcStrides, srcShape, srcScale, x, y, z); } } } @@ -190,12 +259,14 @@ inline void GoldSubtract(RawBufferType &dstVec, const RawBufferType &aVec, const inline long3 GoldStrides(const long3 &shape) { - return long3(shape.y * shape.z * sizeof(WT), shape.z * sizeof(WT), sizeof(WT)); + const auto elemStride = static_cast(sizeof(WT)); + return long3{shape.y * shape.z * elemStride, shape.z * elemStride, elemStride}; } inline int3 ConvolveShape(const long3 &shape) { - return int3(shape.z, shape.y, shape.x); // test::Convolve expects shape as WHN int3 instead of NHW long3 + return int3{static_cast(shape.z), static_cast(shape.y), + static_cast(shape.x)}; // test::Convolve expects shape as WHN int3 instead of NHW long3 } inline void GoldGeneratePyramids(RawPyramidType &dstGaussianPyramid, RawPyramidType &dstDoGPyramid, long3 &baseStrides, @@ -205,7 +276,7 @@ inline void GoldGeneratePyramids(RawPyramidType &dstGaussianPyramid, RawPyramidT { baseShape = expandInput ? long3{srcShape.x, srcShape.y * 2, srcShape.z * 2} : srcShape; - numOctaves = GoldNumberOfOctaves(baseShape.z, baseShape.y); + numOctaves = GoldNumberOfOctaves(static_cast(baseShape.z), static_cast(baseShape.y)); baseStrides = GoldStrides(baseShape); @@ -222,7 +293,7 @@ inline void GoldGeneratePyramids(RawPyramidType &dstGaussianPyramid, RawPyramidT int srcScale = expandInput ? 4 : 1; float sigma = layerSigmas[0]; - sigma = std::sqrt(std::max(sigma * sigma - kPrevSigma * kPrevSigma * srcScale, kMinSigma)); + sigma = std::sqrt(std::max(sigma * sigma - kPrevSigma * kPrevSigma * static_cast(srcScale), kMinSigma)); double2 sigma2{sigma, sigma}; @@ -286,177 +357,164 @@ inline void GoldGeneratePyramids(RawPyramidType &dstGaussianPyramid, RawPyramidT } } -inline void GoldComputeDescriptor(DescriptorType &descriptor, float angle, float featRadius, - const RawPyramidType &srcGaussianPyramid, const long3 &currStrides, - const long3 &currShape, int octave, int layer, int currBatch, int r, int c) +inline int GoldWrapBin(int bin, int numBins) { - float cos_a = std::cos(static_cast(angle * M_PI / 180.f)); - float sin_a = std::sin(static_cast(angle * M_PI / 180.f)); - - float histWidth = kDescOriRadius * featRadius; - - int radius = cuda::round(histWidth * kDescWidthToRadius); - - if (radius > kDescMaxRadius) + if (bin >= numBins) { - radius = kDescMaxRadius; - histWidth = kDescMaxRadius / kDescWidthToRadius; + return bin - numBins; } - - cos_a /= histWidth; - sin_a /= histWidth; - - auto gaussVal = [&srcGaussianPyramid, &currStrides, &octave, &layer, &currBatch](int row, int col) + if (bin < 0) { - return util::ValueAt(srcGaussianPyramid[octave][layer], currStrides, long3{currBatch, row, col}); - }; - - float histogram[(kDescWidth + 2) * (kDescWidth + 2) * (kDescHistBins + 2)] = {0.f}; - - float magnitude; + return bin + numBins; + } + return bin; +} - for (int i = -radius; i <= radius; i++) +inline float GoldWrapAngle(float angle) +{ + if (angle < 0.f) { - if (r + i <= 0 || r + i >= currShape.y - 1) - { - continue; - } - - for (int j = -radius; j <= radius; j++) - { - if (c + j <= 0 || c + j >= currShape.z - 1) - { - continue; - } - - float r_rot = j * sin_a + i * cos_a; - float c_rot = j * cos_a - i * sin_a; - - float weight = r_rot * r_rot + c_rot * c_rot; - - r_rot += kDescWidth / 2 - .5f; - c_rot += kDescWidth / 2 - .5f; + return angle + 360.f; + } + if (angle >= 360.f) + { + return angle - 360.f; + } + return angle; +} - if (r_rot <= -1 || r_rot >= kDescWidth || c_rot <= -1 || c_rot >= kDescWidth) - { - continue; - } +inline int GoldDescriptorHistIndex(int row, int col, int bin) +{ + return ((row + 1) * (kDescWidth + 2) + (col + 1)) * (kDescHistBins + 2) + bin; +} - int iHist = std::floor(r_rot); - int jHist = std::floor(c_rot); +template +inline void GoldAccumulateDescriptorSample(DescriptorHistogram &histogram, const GaussValue &gaussVal, float angle, + float cos_a, float sin_a, const long3 &currShape, int r, int c, int i, int j) +{ + if (r + i <= 0 || r + i >= currShape.y - 1 || c + j <= 0 || c + j >= currShape.z - 1) + { + return; + } - r_rot -= iHist; - c_rot -= jHist; + auto r_rot = static_cast(j) * sin_a + static_cast(i) * cos_a; + auto c_rot = static_cast(j) * cos_a - static_cast(i) * sin_a; + float weight = r_rot * r_rot + c_rot * c_rot; - float dx = gaussVal(r + i + 0, c + j + 1) - gaussVal(r + i + 0, c + j - 1); - float dy = gaussVal(r + i - 1, c + j + 0) - gaussVal(r + i + 1, c + j + 0); + r_rot += kDescWidth / 2 - .5f; + c_rot += kDescWidth / 2 - .5f; - float o_rot = std::atan2(dy, dx) * 180.f / M_PI; - - if (o_rot < 0.f) - o_rot += 360.f; - if (o_rot >= 360.f) - o_rot -= 360.f; - - o_rot = (o_rot - angle) * kDescHistBins / 360.f; - - int bin = std::floor(o_rot); - - o_rot -= bin; - - if (bin < 0) - bin += kDescHistBins; - if (bin >= kDescHistBins) - bin -= kDescHistBins; - - weight = std::exp2f(weight * kDescWeightScale); - - magnitude = std::sqrt(dx * dx + dy * dy) * weight; - - float v_r1 = magnitude * r_rot; - float v_r0 = magnitude - v_r1; - float v_rc11 = v_r1 * c_rot; - float v_rc10 = v_r1 - v_rc11; - float v_rc01 = v_r0 * c_rot; - float v_rc00 = v_r0 - v_rc01; - float v_rco111 = v_rc11 * o_rot; - float v_rco110 = v_rc11 - v_rco111; - float v_rco101 = v_rc10 * o_rot; - float v_rco100 = v_rc10 - v_rco101; - float v_rco011 = v_rc01 * o_rot; - float v_rco010 = v_rc01 - v_rco011; - float v_rco001 = v_rc00 * o_rot; - float v_rco000 = v_rc00 - v_rco001; - - int idx = ((iHist + 1) * (kDescWidth + 2) + (jHist + 1)) * (kDescHistBins + 2) + bin; - - histogram[idx] += v_rco000; - histogram[idx + 1] += v_rco001; - histogram[idx + (kDescHistBins + 2)] += v_rco010; - histogram[idx + (kDescHistBins + 3)] += v_rco011; - histogram[idx + (kDescWidth + 2) * (kDescHistBins + 2)] += v_rco100; - histogram[idx + (kDescWidth + 2) * (kDescHistBins + 2) + 1] += v_rco101; - histogram[idx + (kDescWidth + 3) * (kDescHistBins + 2)] += v_rco110; - histogram[idx + (kDescWidth + 3) * (kDescHistBins + 2) + 1] += v_rco111; - } + if (r_rot <= -1 || r_rot >= kDescWidth || c_rot <= -1 || c_rot >= kDescWidth) + { + return; } + auto iHist = static_cast(std::floor(r_rot)); + auto jHist = static_cast(std::floor(c_rot)); + + r_rot -= static_cast(iHist); + c_rot -= static_cast(jHist); + + float dx = gaussVal(r + i + 0, c + j + 1) - gaussVal(r + i + 0, c + j - 1); + float dy = gaussVal(r + i - 1, c + j + 0) - gaussVal(r + i + 1, c + j + 0); + + float o_rot = (GoldWrapAngle(std::atan2(dy, dx) * 180.f / static_cast(M_PI)) - angle) + * static_cast(kDescHistBins) / 360.f; + auto bin = static_cast(std::floor(o_rot)); + + o_rot -= static_cast(bin); + bin = GoldWrapBin(bin, kDescHistBins); + + weight = std::exp2f(weight * kDescWeightScale); + + float magnitude = std::sqrt(dx * dx + dy * dy) * weight; + + float v_r1 = magnitude * r_rot; + float v_r0 = magnitude - v_r1; + float v_rc11 = v_r1 * c_rot; + float v_rc10 = v_r1 - v_rc11; + float v_rc01 = v_r0 * c_rot; + float v_rc00 = v_r0 - v_rc01; + float v_rco111 = v_rc11 * o_rot; + float v_rco110 = v_rc11 - v_rco111; + float v_rco101 = v_rc10 * o_rot; + float v_rco100 = v_rc10 - v_rco101; + float v_rco011 = v_rc01 * o_rot; + float v_rco010 = v_rc01 - v_rco011; + float v_rco001 = v_rc00 * o_rot; + float v_rco000 = v_rc00 - v_rco001; + + int idx = GoldDescriptorHistIndex(iHist, jHist, bin); + + histogram[idx] += v_rco000; + histogram[idx + 1] += v_rco001; + histogram[idx + (kDescHistBins + 2)] += v_rco010; + histogram[idx + (kDescHistBins + 3)] += v_rco011; + histogram[idx + (kDescWidth + 2) * (kDescHistBins + 2)] += v_rco100; + histogram[idx + (kDescWidth + 2) * (kDescHistBins + 2) + 1] += v_rco101; + histogram[idx + (kDescWidth + 3) * (kDescHistBins + 2)] += v_rco110; + histogram[idx + (kDescWidth + 3) * (kDescHistBins + 2) + 1] += v_rco111; +} + +inline float GoldDescriptorNorm(DescriptorHistogram &histogram) +{ float norm = 0.f; for (int i = 0; i < kDescWidth; i++) { for (int j = 0; j < kDescWidth; j++) { - int histIdx = ((i + 1) * (kDescWidth + 2) + (j + 1)) * (kDescHistBins + 2); + int histIdx = GoldDescriptorHistIndex(i, j, 0); histogram[histIdx] += histogram[histIdx + kDescHistBins]; histogram[histIdx + 1] += histogram[histIdx + kDescHistBins + 1]; for (int bin = 0; bin < kDescHistBins; bin++) { - magnitude = histogram[histIdx + bin]; - + float magnitude = histogram[histIdx + bin]; norm += magnitude * magnitude; } } } - float histMax = std::sqrt(norm) * kDescHistPeakRatio; + return norm; +} - norm = 0.f; +inline float GoldClampDescriptorHistogram(DescriptorHistogram &histogram, float histMax) +{ + float norm = 0.f; for (int i = 0; i < kDescWidth; i++) { for (int j = 0; j < kDescWidth; j++) { - int histIdx = ((i + 1) * (kDescWidth + 2) + (j + 1)) * (kDescHistBins + 2); + int histIdx = GoldDescriptorHistIndex(i, j, 0); for (int bin = 0; bin < kDescHistBins; bin++) { - magnitude = histogram[histIdx + bin]; - - magnitude = std::min(magnitude, histMax); + float magnitude = std::min(histogram[histIdx + bin], histMax); norm += magnitude * magnitude; - histogram[histIdx + bin] = magnitude; } } } - norm = 512 / std::max(std::sqrt(norm), 1e-5f); + return norm; +} +inline void GoldWriteDescriptor(DescriptorType &descriptor, DescriptorHistogram &histogram, float norm) +{ for (int i = 0; i < kDescWidth; i++) { for (int j = 0; j < kDescWidth; j++) { - int histIdx = ((i + 1) * (kDescWidth + 2) + (j + 1)) * (kDescHistBins + 2); + int histIdx = GoldDescriptorHistIndex(i, j, 0); for (int bin = 0; bin < kDescHistBins; bin++) { - magnitude = histogram[histIdx + bin]; - - int descIdx = (i * kDescWidth + j) * kDescHistBins + bin; + float magnitude = histogram[histIdx + bin]; + int descIdx = (i * kDescWidth + j) * kDescHistBins + bin; descriptor.data[descIdx] = cuda::SaturateCast(magnitude * norm); } @@ -464,13 +522,57 @@ inline void GoldComputeDescriptor(DescriptorType &descriptor, float angle, float } } -inline void GoldComputeHistogram(float (&histogram)[kHistogramBins], float featRadius, +inline void GoldComputeDescriptor(DescriptorType &descriptor, float angle, float featRadius, + const RawPyramidType &srcGaussianPyramid, const long3 &currStrides, + const long3 &currShape, int octave, int layer, int currBatch, int r, int c) +{ + float cos_a = std::cos(static_cast(angle * M_PI / 180.f)); + float sin_a = std::sin(static_cast(angle * M_PI / 180.f)); + + float histWidth = kDescOriRadius * featRadius; + + int radius = cuda::round(histWidth * kDescWidthToRadius); + + if (radius > kDescMaxRadius) + { + radius = kDescMaxRadius; + histWidth = kDescMaxRadius / kDescWidthToRadius; + } + + cos_a /= histWidth; + sin_a /= histWidth; + + auto gaussVal = [&srcGaussianPyramid, &currStrides, &octave, &layer, &currBatch](int row, int col) + { + return util::ValueAt(srcGaussianPyramid[octave][layer], currStrides, long3{currBatch, row, col}); + }; + + DescriptorHistogram histogram{}; + + for (int i = -radius; i <= radius; i++) + { + for (int j = -radius; j <= radius; j++) + { + GoldAccumulateDescriptorSample(histogram, gaussVal, angle, cos_a, sin_a, currShape, r, c, i, j); + } + } + + float norm = GoldDescriptorNorm(histogram); + float histMax = std::sqrt(norm) * kDescHistPeakRatio; + + norm = GoldClampDescriptorHistogram(histogram, histMax); + norm = 512 / std::max(std::sqrt(norm), 1e-5f); + + GoldWriteDescriptor(descriptor, histogram, norm); +} + +inline void GoldComputeHistogram(OrientationHistogram &histogram, float featRadius, const RawPyramidType &srcGaussianPyramid, const long3 &currStrides, const long3 &currShape, int octave, int layer, int currBatch, int r, int c) { std::vector tempHistogram(kHistogramBins + 4, 0.f); - int radius = std::round(featRadius * kOrientationRadius); + auto radius = static_cast(std::round(featRadius * kOrientationRadius)); float weightScale = -1.f / (2.f * (featRadius * kOrientationSigma) * (featRadius * kOrientationSigma)); @@ -496,13 +598,12 @@ inline void GoldComputeHistogram(float (&histogram)[kHistogramBins], float featR float dx = gaussVal(r + i + 0, c + j + 1) - gaussVal(r + i + 0, c + j - 1); float dy = gaussVal(r + i - 1, c + j + 0) - gaussVal(r + i + 1, c + j + 0); - float angle = std::atan2(dy, dx) * 180.f / M_PI; - float weight = std::exp2f((i * i + j * j) * weightScale); + float angle = std::atan2(dy, dx) * 180.f / static_cast(M_PI); + float weight = std::exp2f(static_cast(i * i + j * j) * weightScale); float magnitude = std::sqrt(dx * dx + dy * dy); - int bin = std::round(angle * kHistogramBins / 360.f); - - bin = (bin >= kHistogramBins ? bin - kHistogramBins : (bin < 0 ? bin + kHistogramBins : bin)); + int bin = GoldWrapBin(static_cast(std::round(angle * static_cast(kHistogramBins) / 360.f)), + kHistogramBins); tempHistogram[2 + bin] += weight * magnitude; } @@ -522,122 +623,204 @@ inline void GoldComputeHistogram(float (&histogram)[kHistogramBins], float featR } } -inline void GoldAddFeatures(RawBufferType &featCoords, const long2 &featCoordsStrides, RawBufferType &featMetadata, - const long2 &featMetadataStrides, RawBufferType &featDescriptors, - const long2 &featDescriptorsStrides, int maxCapacity, RawBufferType &numFeatures, - const long1 &numFeaturesStrides, const RawPyramidType &srcGaussianPyramid, - const RawPyramidType &srcDoGPyramid, const long3 &currStrides, const long3 &currShape, - int octave, int firstOctave, int numOctaveLayers, float contrastThreshold, - float edgeThreshold, float initSigma, int l, int currBatch, int r, int c) +inline void GoldAddFeatureOrientation(RawBufferType &featCoords, const long2 &featCoordsStrides, + RawBufferType &featMetadata, const long2 &featMetadataStrides, + RawBufferType &featDescriptors, const long2 &featDescriptorsStrides, + int maxCapacity, RawBufferType &numFeatures, const long1 &numFeaturesStrides, + const RawPyramidType &srcGaussianPyramid, const long3 &currStrides, + const long3 &currShape, int octave, int l, int currBatch, int r, int c, + const float4 &keypoint, float3 metadata, float featRadius, float descAngle) +{ + DescriptorType descriptor; + GoldComputeDescriptor(descriptor, descAngle, featRadius, srcGaussianPyramid, currStrides, currShape, octave, l, + currBatch, r, c); + + int &featIdx = util::ValueAt(numFeatures, numFeaturesStrides, long1{currBatch}); + + if (featIdx < maxCapacity) + { + util::ValueAt(featCoords, featCoordsStrides, long2{currBatch, featIdx}) = keypoint; + util::ValueAt(featMetadata, featMetadataStrides, long2{currBatch, featIdx}) = metadata; + util::ValueAt(featDescriptors, featDescriptorsStrides, long2{currBatch, featIdx}) = descriptor; + } + + featIdx += 1; +} + +struct GoldFeatureLocation +{ + int layer; + int row; + int col; + + WT value{}; + cuda::math::Vector derivative{}; + cuda::math::Vector offset{}; + cuda::math::Matrix hessian{}; +}; + +template +inline void GoldComputeFeatureSystem(GoldFeatureLocation &feature, const DogValue &dogVal) { constexpr float kImageScale = 1.f / cuda::TypeTraits::max; // source images data type scale constexpr float kDScale1 = kImageScale * .5f; // first derivative scale constexpr float kDScale2 = kImageScale; // second derivative scale constexpr float kDScaleC = kImageScale * .25f; // cross derivative scale - float cv; // central value - cuda::math::Vector dD, sol; // derivative distances and solver solution - cuda::math::Matrix H; // Hessian matrix + const int l = feature.layer; + const int r = feature.row; + const int c = feature.col; - auto dogVal = [&srcDoGPyramid, &currStrides, &octave, &currBatch](int layer, int row, int col) - { - return util::ValueAt(srcDoGPyramid[octave][layer], currStrides, long3{currBatch, row, col}); - }; + auto &dD = feature.derivative; + auto &H = feature.hessian; - bool converged = false; + // clang-format off + dD[0] = (dogVal(l + 0, r + 0, c + 1) - dogVal(l + 0, r + 0, c - 1)) * kDScale1; + dD[1] = (dogVal(l + 0, r + 1, c + 0) - dogVal(l + 0, r - 1, c + 0)) * kDScale1; + dD[2] = (dogVal(l + 1, r + 0, c + 0) - dogVal(l - 1, r + 0, c + 0)) * kDScale1; + + feature.value = dogVal(l, r, c); + + H[0][0] = (dogVal(l + 0, r + 0, c + 1) + dogVal(l + 0, r + 0, c - 1) - 2 * feature.value) * kDScale2; + H[1][1] = (dogVal(l + 0, r + 1, c + 0) + dogVal(l + 0, r - 1, c + 0) - 2 * feature.value) * kDScale2; + H[2][2] = (dogVal(l + 1, r + 0, c + 0) + dogVal(l - 1, r + 0, c + 0) - 2 * feature.value) * kDScale2; + + H[0][1] = H[1][0] = (dogVal(l + 0, r + 1, c + 1) - dogVal(l + 0, r + 1, c - 1) - + dogVal(l + 0, r - 1, c + 1) + dogVal(l + 0, r - 1, c - 1)) * kDScaleC; + H[0][2] = H[2][0] = (dogVal(l + 1, r + 0, c + 1) - dogVal(l + 1, r + 0, c - 1) - + dogVal(l - 1, r + 0, c + 1) + dogVal(l - 1, r + 0, c - 1)) * kDScaleC; + H[1][2] = H[2][1] = (dogVal(l + 1, r + 1, c + 0) - dogVal(l + 1, r - 1, c + 0) - + dogVal(l - 1, r + 1, c + 0) + dogVal(l - 1, r - 1, c + 0)) * kDScaleC; + // clang-format on +} - for (int i = 0; i < kMaxInterpolationSteps; i++) - { - // clang-format off - dD[0] = (dogVal(l + 0, r + 0, c + 1) - dogVal(l + 0, r + 0, c - 1)) * kDScale1; - dD[1] = (dogVal(l + 0, r + 1, c + 0) - dogVal(l + 0, r - 1, c + 0)) * kDScale1; - dD[2] = (dogVal(l + 1, r + 0, c + 0) - dogVal(l - 1, r + 0, c + 0)) * kDScale1; +inline bool GoldOffsetIsSmall(const cuda::math::Vector &offset) +{ + return std::abs(offset[2]) < 0.5f && std::abs(offset[1]) < 0.5f && std::abs(offset[0]) < 0.5f; +} - cv = dogVal(l, r, c); +inline bool GoldOffsetIsInRange(const cuda::math::Vector &offset) +{ + constexpr float kMaxStep = static_cast(std::numeric_limits::max()) / 3.f; + return std::abs(offset[2]) <= kMaxStep && std::abs(offset[1]) <= kMaxStep && std::abs(offset[0]) <= kMaxStep; +} - H[0][0] = (dogVal(l + 0, r + 0, c + 1) + dogVal(l + 0, r + 0, c - 1) - 2 * cv) * kDScale2; - H[1][1] = (dogVal(l + 0, r + 1, c + 0) + dogVal(l + 0, r - 1, c + 0) - 2 * cv) * kDScale2; - H[2][2] = (dogVal(l + 1, r + 0, c + 0) + dogVal(l - 1, r + 0, c + 0) - 2 * cv) * kDScale2; +inline void GoldApplyOffset(GoldFeatureLocation &feature) +{ + feature.col += static_cast(std::round(feature.offset[0])); + feature.row += static_cast(std::round(feature.offset[1])); + feature.layer += static_cast(std::round(feature.offset[2])); +} - H[0][1] = H[1][0] = (dogVal(l + 0, r + 1, c + 1) - dogVal(l + 0, r + 1, c - 1) - - dogVal(l + 0, r - 1, c + 1) + dogVal(l + 0, r - 1, c - 1)) * kDScaleC; - H[0][2] = H[2][0] = (dogVal(l + 1, r + 0, c + 1) - dogVal(l + 1, r + 0, c - 1) - - dogVal(l - 1, r + 0, c + 1) + dogVal(l - 1, r + 0, c - 1)) * kDScaleC; - H[1][2] = H[2][1] = (dogVal(l + 1, r + 1, c + 0) - dogVal(l + 1, r - 1, c + 0) - - dogVal(l - 1, r + 1, c + 0) + dogVal(l - 1, r - 1, c + 0)) * kDScaleC; - // clang-format on +inline bool GoldLocationIsInRange(const GoldFeatureLocation &feature, const long3 &currShape, int numOctaveLayers) +{ + return feature.layer >= 1 && feature.layer <= numOctaveLayers && feature.col >= kImageBorder + && feature.col < currShape.z - kImageBorder && feature.row >= kImageBorder + && feature.row < currShape.y - kImageBorder; +} - sol = dD; +template +inline bool GoldLocalizeFeature(GoldFeatureLocation &feature, const DogValue &dogVal, const long3 &currShape, + int numOctaveLayers) +{ + for (int i = 0; i < kMaxInterpolationSteps; i++) + { + GoldComputeFeatureSystem(feature, dogVal); - if (!cuda::math::solve_inplace(H, sol)) + feature.offset = feature.derivative; + if (!cuda::math::solve_inplace(feature.hessian, feature.offset)) { - return; + return false; } - sol = -sol; - - if (std::abs(sol[2]) < 0.5f && std::abs(sol[1]) < 0.5f && std::abs(sol[0]) < 0.5f) + feature.offset = -feature.offset; + if (GoldOffsetIsSmall(feature.offset)) { - converged = true; - break; + return true; } - if (std::abs(sol[2]) > std::numeric_limits::max() / 3.f - || std::abs(sol[1]) > std::numeric_limits::max() / 3.f - || std::abs(sol[0]) > std::numeric_limits::max() / 3.f) + if (!GoldOffsetIsInRange(feature.offset)) { - return; + return false; } - c += std::round(sol[0]); - r += std::round(sol[1]); - l += std::round(sol[2]); - - if (l < 1 || l > numOctaveLayers || c < kImageBorder || c >= currShape.z - kImageBorder || r < kImageBorder - || r >= currShape.y - kImageBorder) + GoldApplyOffset(feature); + if (!GoldLocationIsInRange(feature, currShape, numOctaveLayers)) { - return; + return false; } } - if (!converged) - { - return; - } + return false; +} - float3 metadata; +inline bool GoldFeaturePassesResponse(float3 &metadata, const GoldFeatureLocation &feature, int numOctaveLayers, + float contrastThreshold, float edgeThreshold) +{ + constexpr float kImageScale = 1.f / cuda::TypeTraits::max; // source images data type scale - metadata.y = std::abs(cv * kImageScale + cuda::math::dot(dD, sol) * .5f); + const auto &dD = feature.derivative; + const auto &H = feature.hessian; - if (metadata.y * numOctaveLayers < contrastThreshold) + metadata.y = std::abs(feature.value * kImageScale + cuda::math::dot(dD, feature.offset) * .5f); + if (metadata.y * static_cast(numOctaveLayers) < contrastThreshold) { - return; + return false; } float trace = H[0][0] + H[1][1]; float determinant = H[0][0] * H[1][1] - H[0][1] * H[1][0]; + return determinant > 0 + && trace * trace * edgeThreshold < (edgeThreshold + 1.f) * (edgeThreshold + 1.f) * determinant; +} + +inline float4 GoldMakeKeypoint(const GoldFeatureLocation &feature, int currOctave) +{ + float4 keypoint; + keypoint.x = (static_cast(feature.col) + feature.offset[0]) * std::pow(2.f, static_cast(currOctave)); + keypoint.y = (static_cast(feature.row) + feature.offset[1]) * std::pow(2.f, static_cast(currOctave)); + keypoint.w = static_cast(feature.layer) + feature.offset[2]; + keypoint.z = static_cast(currOctave); + return keypoint; +} + +inline void GoldAddFeatures(RawBufferType &featCoords, const long2 &featCoordsStrides, RawBufferType &featMetadata, + const long2 &featMetadataStrides, RawBufferType &featDescriptors, + const long2 &featDescriptorsStrides, int maxCapacity, RawBufferType &numFeatures, + const long1 &numFeaturesStrides, const RawPyramidType &srcGaussianPyramid, + const RawPyramidType &srcDoGPyramid, const long3 &currStrides, const long3 &currShape, + int octave, int firstOctave, int numOctaveLayers, float contrastThreshold, + float edgeThreshold, float initSigma, int l, int currBatch, int r, int c) +{ + auto dogVal = [&srcDoGPyramid, &currStrides, &octave, &currBatch](int layer, int row, int col) + { + return util::ValueAt(srcDoGPyramid[octave][layer], currStrides, long3{currBatch, row, col}); + }; - if (determinant <= 0 || trace * trace * edgeThreshold >= (edgeThreshold + 1) * (edgeThreshold + 1) * determinant) + GoldFeatureLocation feature{l, r, c}; + if (!GoldLocalizeFeature(feature, dogVal, currShape, numOctaveLayers)) { return; } - int currOctave = octave + firstOctave; + float3 metadata; - float4 keypoint; + if (!GoldFeaturePassesResponse(metadata, feature, numOctaveLayers, contrastThreshold, edgeThreshold)) + { + return; + } - keypoint.x = (c + sol[0]) * std::pow(2, currOctave); - keypoint.y = (r + sol[1]) * std::pow(2, currOctave); - keypoint.w = (l + sol[2]); - keypoint.z = currOctave; + int currOctave = octave + firstOctave; - float featRadius = initSigma * std::pow(2, keypoint.w / numOctaveLayers); + float4 keypoint = GoldMakeKeypoint(feature, currOctave); + float featRadius = initSigma * std::pow(2.f, keypoint.w / static_cast(numOctaveLayers)); - float hist[kHistogramBins]; + OrientationHistogram hist{}; - GoldComputeHistogram(hist, featRadius, srcGaussianPyramid, currStrides, currShape, octave, l, currBatch, r, c); + GoldComputeHistogram(hist, featRadius, srcGaussianPyramid, currStrides, currShape, octave, feature.layer, currBatch, + feature.row, feature.col); - metadata.z = featRadius * 2.f * std::pow(2, currOctave); + metadata.z = featRadius * 2.f * std::pow(2.f, static_cast(currOctave)); float histPeak = hist[0]; @@ -653,43 +836,66 @@ inline void GoldAddFeatures(RawBufferType &featCoords, const long2 &featCoordsSt int prologue = i > 0 ? i - 1 : kHistogramBins - 1; int epilogue = i < kHistogramBins - 1 ? i + 1 : 0; - if (hist[i] > hist[prologue] && hist[i] > hist[epilogue] && hist[i] >= histPeak) + if (hist[i] <= hist[prologue] || hist[i] <= hist[epilogue] || hist[i] < histPeak) { - float bin = i + .5f * (hist[prologue] - hist[epilogue]) / (hist[prologue] - 2 * hist[i] + hist[epilogue]); + continue; + } - bin = (bin < 0 ? kHistogramBins + bin : (bin >= kHistogramBins ? bin - kHistogramBins : bin)); + auto bin = static_cast(i) + + .5f * (hist[prologue] - hist[epilogue]) / (hist[prologue] - 2.f * hist[i] + hist[epilogue]); - metadata.x = 360.f - (360.f / kHistogramBins) * bin; + if (bin < 0) + { + bin += kHistogramBins; + } + else if (bin >= kHistogramBins) + { + bin -= kHistogramBins; + } - if (cuda::abs(metadata.x - 360.f) < 1e-5) - metadata.x = 0.f; + metadata.x = 360.f - (360.f / kHistogramBins) * bin; - ASSERT_TRUE(metadata.x >= 0.f && metadata.x <= 360.f); + if (cuda::abs(metadata.x - 360.f) < 1e-5) + metadata.x = 0.f; - float descAngle = 360.f - metadata.x; + ASSERT_TRUE(metadata.x >= 0.f && metadata.x <= 360.f); - if (cuda::abs(descAngle - 360.f) < 1e-5) - descAngle = 0.f; + float descAngle = 360.f - metadata.x; - ASSERT_TRUE(descAngle >= 0.f && descAngle <= 360.f); + if (cuda::abs(descAngle - 360.f) < 1e-5) + descAngle = 0.f; - DescriptorType descriptor; - GoldComputeDescriptor(descriptor, descAngle, featRadius, srcGaussianPyramid, currStrides, currShape, octave, - l, currBatch, r, c); + ASSERT_TRUE(descAngle >= 0.f && descAngle <= 360.f); - int &featIdx = util::ValueAt(numFeatures, numFeaturesStrides, long1{currBatch}); + GoldAddFeatureOrientation(featCoords, featCoordsStrides, featMetadata, featMetadataStrides, featDescriptors, + featDescriptorsStrides, maxCapacity, numFeatures, numFeaturesStrides, + srcGaussianPyramid, currStrides, currShape, octave, feature.layer, currBatch, + feature.row, feature.col, keypoint, metadata, featRadius, descAngle); + } +} - if (featIdx < maxCapacity) - { - util::ValueAt(featCoords, featCoordsStrides, long2{currBatch, featIdx}) = keypoint; - util::ValueAt(featMetadata, featMetadataStrides, long2{currBatch, featIdx}) = metadata; - util::ValueAt(featDescriptors, featDescriptorsStrides, long2{currBatch, featIdx}) - = descriptor; - } +template +inline bool GoldIsDoGExtremum(const DogValue &dogVal, int octave, int layer, long batch, long row, long col, WT val) +{ + for (int i = 0; i < 27; ++i) + { + int dl = i / 9 - 1; + int dr = (i / 3) % 3 - 1; + int dc = i % 3 - 1; - featIdx += 1; + if (dl == 0 && dr == 0 && dc == 0) + { + continue; + } + + WT neighbor = dogVal(octave, layer + dl, batch, row + dr, col + dc); + if ((val > 0 && val < neighbor) || (val < 0 && val > neighbor)) + { + return false; } } + + return true; } inline void GoldFindExtrema(RawBufferType &featCoords, const long2 &featCoordsStrides, const long2 &featCoordsShape, @@ -701,7 +907,8 @@ inline void GoldFindExtrema(RawBufferType &featCoords, const long2 &featCoordsSt int firstOctave, int numOctaves, int numOctaveLayers, float contrastThreshold, float edgeThreshold, float initSigma) { - int threshold = std::floor(.5f * contrastThreshold / numOctaveLayers * 255); + auto threshold + = static_cast(std::floor(.5f * contrastThreshold / static_cast(numOctaveLayers) * 255.f)); long3 currShape = baseShape; long3 currStrides = baseStrides; @@ -711,68 +918,48 @@ inline void GoldFindExtrema(RawBufferType &featCoords, const long2 &featCoordsSt return util::ValueAt(srcDoGPyramid[octave][layer], currStrides, long3{batch, row, col}); }; - long maxCapacity = featCoordsShape.y; + auto maxCapacity = static_cast(featCoordsShape.y); ASSERT_TRUE(featCoordsShape.x == currShape.x && featMetadataShape.x == currShape.x && featCoordsShape.y == featMetadataShape.y && numFeaturesShape.x == currShape.x); + auto addFeatureIfExtremum + = [&featCoords, &featCoordsStrides, &featDescriptors, &featDescriptorsStrides, &featMetadata, + &featMetadataStrides, &numFeatures, &numFeaturesStrides, &srcDoGPyramid, &srcGaussianPyramid, + contrastThreshold, edgeThreshold, firstOctave, initSigma, maxCapacity, numOctaveLayers, threshold, + &currShape, &currStrides, &dogVal](int o, int l, long b, int r, int c) + { + if (WT val = dogVal(o, l, b, r, c); + std::abs(val) <= static_cast(threshold) || !GoldIsDoGExtremum(dogVal, o, l, b, r, c, val)) + { + return; + } + + GoldAddFeatures(featCoords, featCoordsStrides, featMetadata, featMetadataStrides, featDescriptors, + featDescriptorsStrides, maxCapacity, numFeatures, numFeaturesStrides, srcGaussianPyramid, + srcDoGPyramid, currStrides, currShape, o, firstOctave, numOctaveLayers, contrastThreshold, + edgeThreshold, initSigma, l, static_cast(b), r, c); + }; + for (int o = 0; o < numOctaves; o++) { for (int l = 1; l <= numOctaveLayers; l++) { - for (long b = 0; b < currShape.x; b++) + const long width = currShape.z - 2 * kImageBorder; + const long height = currShape.y - 2 * kImageBorder; + const long numPositions = currShape.x * height * width; + + for (long p = 0; p < numPositions; ++p) { - for (long r = kImageBorder; r < currShape.y - kImageBorder; r++) - { - for (long c = kImageBorder; c < currShape.z - kImageBorder; c++) - { - WT val = dogVal(o, l, b, r, c); - - // clang-format off - if (std::abs(val) > threshold && - ((val > 0 && - val >= dogVal(o, l, b, r + 0, c - 1) && val >= dogVal(o, l, b, r + 0, c + 1) && - val >= dogVal(o, l, b, r - 1, c - 1) && val >= dogVal(o, l, b, r - 1, c + 0) && - val >= dogVal(o, l, b, r - 1, c + 1) && val >= dogVal(o, l, b, r + 1, c - 1) && - val >= dogVal(o, l, b, r + 1, c + 0) && val >= dogVal(o, l, b, r + 1, c + 1) && - val >= dogVal(o, l + 1, b, r + 0, c + 0) && - val >= dogVal(o, l + 1, b, r + 0, c - 1) && val >= dogVal(o, l + 1, b, r + 0, c + 1) && - val >= dogVal(o, l + 1, b, r - 1, c - 1) && val >= dogVal(o, l + 1, b, r - 1, c + 0) && - val >= dogVal(o, l + 1, b, r - 1, c + 1) && val >= dogVal(o, l + 1, b, r + 1, c - 1) && - val >= dogVal(o, l + 1, b, r + 1, c + 0) && val >= dogVal(o, l + 1, b, r + 1, c + 1) && - val >= dogVal(o, l - 1, b, r + 0, c + 0) && - val >= dogVal(o, l - 1, b, r + 0, c - 1) && val >= dogVal(o, l - 1, b, r + 0, c + 1) && - val >= dogVal(o, l - 1, b, r - 1, c - 1) && val >= dogVal(o, l - 1, b, r - 1, c + 0) && - val >= dogVal(o, l - 1, b, r - 1, c + 1) && val >= dogVal(o, l - 1, b, r + 1, c - 1) && - val >= dogVal(o, l - 1, b, r + 1, c + 0) && val >= dogVal(o, l - 1, b, r + 1, c + 1)) || - (val < 0 && - val <= dogVal(o, l, b, r + 0, c - 1) && val <= dogVal(o, l, b, r + 0, c + 1) && - val <= dogVal(o, l, b, r - 1, c - 1) && val <= dogVal(o, l, b, r - 1, c + 0) && - val <= dogVal(o, l, b, r - 1, c + 1) && val <= dogVal(o, l, b, r + 1, c - 1) && - val <= dogVal(o, l, b, r + 1, c + 0) && val <= dogVal(o, l, b, r + 1, c + 1) && - val <= dogVal(o, l + 1, b, r + 0, c + 0) && - val <= dogVal(o, l + 1, b, r + 0, c - 1) && val <= dogVal(o, l + 1, b, r + 0, c + 1) && - val <= dogVal(o, l + 1, b, r - 1, c - 1) && val <= dogVal(o, l + 1, b, r - 1, c + 0) && - val <= dogVal(o, l + 1, b, r - 1, c + 1) && val <= dogVal(o, l + 1, b, r + 1, c - 1) && - val <= dogVal(o, l + 1, b, r + 1, c + 0) && val <= dogVal(o, l + 1, b, r + 1, c + 1) && - val <= dogVal(o, l - 1, b, r + 0, c + 0) && - val <= dogVal(o, l - 1, b, r + 0, c - 1) && val <= dogVal(o, l - 1, b, r + 0, c + 1) && - val <= dogVal(o, l - 1, b, r - 1, c - 1) && val <= dogVal(o, l - 1, b, r - 1, c + 0) && - val <= dogVal(o, l - 1, b, r - 1, c + 1) && val <= dogVal(o, l - 1, b, r + 1, c - 1) && - val <= dogVal(o, l - 1, b, r + 1, c + 0) && val <= dogVal(o, l - 1, b, r + 1, c + 1)))) - { - // clang-format on - - GoldAddFeatures(featCoords, featCoordsStrides, featMetadata, featMetadataStrides, - featDescriptors, featDescriptorsStrides, maxCapacity, numFeatures, - numFeaturesStrides, srcGaussianPyramid, srcDoGPyramid, currStrides, - currShape, o, firstOctave, numOctaveLayers, contrastThreshold, - edgeThreshold, initSigma, l, b, r, c); - } - } // for each column - } // for each row - } // for each batch image - } // for each layer + long rest = p; + int c = kImageBorder + static_cast(rest % width); + rest /= width; + int r = kImageBorder + static_cast(rest % height); + rest /= height; + + addFeatureIfExtremum(o, l, rest, r, c); + } // for each batch image, row, and column + } // for each layer currShape.y /= 2; currShape.z /= 2; @@ -785,9 +972,11 @@ struct SIFTResults { using TupleType = std::tuple; // float4 coordinates, float3 metadata, descriptor - std::vector> testFeatures, goldFeatures; + std::vector> testFeatures; + std::vector> goldFeatures; - std::vector testNumFeatures, goldNumFeatures; + std::vector testNumFeatures; + std::vector goldNumFeatures; }; // Gold (CPU reference) computation of SIFT @@ -845,8 +1034,10 @@ inline void GoldSIFT(SIFTResults &outResults, const nvcv::Tensor &featCoords, co #undef NVCV_TEST_CUDA_COPY - RawPyramidType pyrGaussian, pyrDoG; - long3 baseShape, baseStrides; + RawPyramidType pyrGaussian; + RawPyramidType pyrDoG; + long3 baseShape; + long3 baseStrides; int numOctaves; @@ -909,8 +1100,8 @@ inline void GoldSIFT(SIFTResults &outResults, const nvcv::Tensor &featCoords, co // Need to sort both CPU and CUDA results due to extrema interpolation in add features - std::sort(outResults.testFeatures[x].begin(), outResults.testFeatures[x].end(), featureLower); - std::sort(outResults.goldFeatures[x].begin(), outResults.goldFeatures[x].end(), featureLower); + std::ranges::sort(outResults.testFeatures[x], featureLower); + std::ranges::sort(outResults.goldFeatures[x], featureLower); } } @@ -939,23 +1130,16 @@ NVCV_TYPED_TEST_SUITE(OpSIFT, type::Types< // clang-format on -TYPED_TEST(OpSIFT, correct_output) +static void RunSIFTCorrectOutput(int3 inShape, long capacity, int numOctaveLayers, float contrastThreshold, + float edgeThreshold, float initSigma, bool expandInput, std::string_view layout) { - int3 inShape = type::GetValue; - long capacity = type::GetValue; - int numOctaveLayers = type::GetValue; - float contrastThreshold = type::GetValue; - float edgeThreshold = type::GetValue; - float initSigma = type::GetValue; - bool expandInput = type::GetValue; - NVCVSIFTFlagType flags = expandInput ? NVCV_SIFT_USE_EXPANDED_INPUT : NVCV_SIFT_USE_ORIGINAL_INPUT; // Increasing inShape and numOctaveLayers to test bigger maxShape and maxOctaveLayers int3 maxShape = (inShape + 3) * (expandInput ? 2 : 1); int maxOctaveLayers = numOctaveLayers + 1; - nvcv::Tensor src = nvcv::util::CreateTensor(inShape.z, inShape.x, inShape.y, kInFormat); + nvcv::Tensor src = CreateSIFTInputTensor(inShape, layout); auto srcData = src.exportData(); ASSERT_TRUE(srcData); @@ -968,6 +1152,8 @@ TYPED_TEST(OpSIFT, correct_output) // While inShape is WHN, srcShape is NHW to match srcStrides ASSERT_TRUE(inShape.z == srcShape.x && inShape.y == srcShape.y && inShape.x == srcShape.z); + // CHW/HWC tensors have no explicit N dimension, so compute the + // per-sample stride from height and row stride before using the accessor. srcStrides.x = (srcData->rank() == 3) ? srcShape.y * srcStrides.y : srcStrides.x; long srcBufSize = srcStrides.x * srcShape.x; @@ -981,7 +1167,7 @@ TYPED_TEST(OpSIFT, correct_output) for (long x = 0; x < srcShape.x; ++x) for (long y = 0; y < srcShape.y; ++y) for (long z = 0; z < srcShape.z; ++z) - util::ValueAt(srcVec, srcStrides, long3{x, y, z}) = rg(g_rng); + util::ValueAt(srcVec, srcStrides, long3{x, y, z}) = rg(Rng()); nvcv::Tensor featCoords({{srcShape.x, capacity}, "NM"}, nvcv::TYPE_4F32); nvcv::Tensor featMetadata({{srcShape.x, capacity}, "NM"}, nvcv::TYPE_3F32); @@ -1012,6 +1198,152 @@ TYPED_TEST(OpSIFT, correct_output) EXPECT_EQ(results.testFeatures, results.goldFeatures); } +TYPED_TEST(OpSIFT, correct_output) +{ + int3 inShape = type::GetValue; + long capacity = type::GetValue; + int numOctaveLayers = type::GetValue; + float contrastThreshold = type::GetValue; + float edgeThreshold = type::GetValue; + float initSigma = type::GetValue; + bool expandInput = type::GetValue; + + RunSIFTCorrectOutput(inShape, capacity, numOctaveLayers, contrastThreshold, edgeThreshold, initSigma, expandInput, + "NHWC"); +} + +TEST(OpSIFT, planar_correct_output) +{ + RunSIFTCorrectOutput(int3{32, 32, 2}, 64, 3, 0.02f, 12.f, 1.0f, false, "NCHW"); + RunSIFTCorrectOutput(int3{32, 32, 1}, 64, 3, 0.02f, 12.f, 1.0f, true, "CHW"); +} + +static void RunSIFTPlanarParity(int3 inShape, std::string_view interleavedLayout, std::string_view planarLayout) +{ + constexpr long capacity = 256; + constexpr int numOctaveLayers = 3; + constexpr float contrastThreshold = 0.02f; + constexpr float edgeThreshold = 12.f; + constexpr float initSigma = 1.f; + + nvcv::Tensor srcInterleaved = CreateSIFTInputTensor(inShape, interleavedLayout); + nvcv::Tensor srcPlanar = CreateSIFTInputTensor(inShape, planarLayout); + + auto srcInterleavedData = srcInterleaved.exportData(); + auto srcPlanarData = srcPlanar.exportData(); + ASSERT_TRUE(srcInterleavedData && srcPlanarData); + + auto srcInterleavedAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcInterleavedData); + auto srcPlanarAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*srcPlanarData); + ASSERT_TRUE(srcInterleavedAccess && srcPlanarAccess); + + for (int sample = 0; sample < inShape.z; ++sample) + { + std::vector input(inShape.x * inShape.y); + test::planar::FillDeterministicValues(input, static_cast(sample) * 101 + 1, nvcv::TYPE_U8); + test::planar::UploadInterleavedSample(*srcInterleavedAccess, sample, input, inShape.x, inShape.y, inShape.x); + test::planar::UploadPlanarSample(*srcPlanarAccess, sample, input, inShape.x, inShape.y, 1, sizeof(uint8_t)); + } + + nvcv::Tensor featCoordsInterleaved( + { + {inShape.z, capacity}, + "NM" + }, + nvcv::TYPE_4F32); + nvcv::Tensor featMetadataInterleaved( + { + {inShape.z, capacity}, + "NM" + }, + nvcv::TYPE_3F32); + nvcv::Tensor featDescriptorsInterleaved( + { + {inShape.z, capacity, 128}, + "NMD" + }, + nvcv::TYPE_U8); + nvcv::Tensor numFeaturesInterleaved({{inShape.z}, "N"}, nvcv::TYPE_S32); + nvcv::Tensor featCoordsPlanar( + { + {inShape.z, capacity}, + "NM" + }, + nvcv::TYPE_4F32); + nvcv::Tensor featMetadataPlanar( + { + {inShape.z, capacity}, + "NM" + }, + nvcv::TYPE_3F32); + nvcv::Tensor featDescriptorsPlanar( + { + {inShape.z, capacity, 128}, + "NMD" + }, + nvcv::TYPE_U8); + nvcv::Tensor numFeaturesPlanar({{inShape.z}, "N"}, nvcv::TYPE_S32); + + std::array outputs{ + &featCoordsInterleaved, &featMetadataInterleaved, &featDescriptorsInterleaved, &numFeaturesInterleaved, + &featCoordsPlanar, &featMetadataPlanar, &featDescriptorsPlanar, &numFeaturesPlanar, + }; + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + for (const nvcv::Tensor *output : outputs) + { + auto data = output->exportData(); + ASSERT_TRUE(data); + ASSERT_EQ(cudaSuccess, cudaMemsetAsync(data->basePtr(), 0, data->shape(0) * data->stride(0), stream)); + } + + cvcuda::SIFT op(inShape + 3, numOctaveLayers + 1); + EXPECT_NO_THROW(op(stream, srcInterleaved, featCoordsInterleaved, featMetadataInterleaved, + featDescriptorsInterleaved, numFeaturesInterleaved, numOctaveLayers, contrastThreshold, + edgeThreshold, initSigma, NVCV_SIFT_USE_ORIGINAL_INPUT)); + EXPECT_NO_THROW(op(stream, srcPlanar, featCoordsPlanar, featMetadataPlanar, featDescriptorsPlanar, + numFeaturesPlanar, numOctaveLayers, contrastThreshold, edgeThreshold, initSigma, + NVCV_SIFT_USE_ORIGINAL_INPUT)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + auto expectExactTensor = [](const nvcv::Tensor &interleaved, const nvcv::Tensor &planar) + { + auto interleavedData = interleaved.exportData(); + auto planarData = planar.exportData(); + ASSERT_TRUE(interleavedData && planarData); + + const long numBytes = interleavedData->shape(0) * interleavedData->stride(0); + ASSERT_EQ(numBytes, planarData->shape(0) * planarData->stride(0)); + + RawBufferType interleavedBuffer(numBytes); + RawBufferType planarBuffer(numBytes); + ASSERT_EQ(cudaSuccess, + cudaMemcpy(interleavedBuffer.data(), interleavedData->basePtr(), numBytes, cudaMemcpyDeviceToHost)); + ASSERT_EQ(cudaSuccess, + cudaMemcpy(planarBuffer.data(), planarData->basePtr(), numBytes, cudaMemcpyDeviceToHost)); + EXPECT_EQ(interleavedBuffer, planarBuffer); + }; + + expectExactTensor(numFeaturesInterleaved, numFeaturesPlanar); + expectExactTensor(featCoordsInterleaved, featCoordsPlanar); + expectExactTensor(featMetadataInterleaved, featMetadataPlanar); + expectExactTensor(featDescriptorsInterleaved, featDescriptorsPlanar); + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpSIFTPlanar, tensor_nchw_matches_interleaved) +{ + RunSIFTPlanarParity(int3{67, 53, 2}, "NHWC", "NCHW"); +} + +TEST(OpSIFTPlanar, tensor_chw_matches_interleaved) +{ + RunSIFTPlanarParity(int3{43, 37, 1}, "HWC", "CHW"); +} + TEST(OpSIFT, no_linear_system_solution) { int3 inShape = {64, 64, 1}; @@ -1063,7 +1395,7 @@ TEST(OpSIFT, no_linear_system_solution) // clang-format off NVCV_TEST_SUITE_P(OpSIFT_Negative, test::ValueList{ - // inFmt ,inShape , initSigma, contrastThreshold, edgeThreshold, numOctaveLayers, {featCoords} , {featMetadata} , {featDescriptors} , {numFeatures} + // Negative cases vary image format, shape, SIFT thresholds, output tensors, and feature counts. // invalid input { nvcv::FMT_RGB8p , 32, 32, 8 , 0.5f , 0.01f , 20.f , 2 , 8, 55, nvcv::TYPE_4F32, 8, 55, nvcv::TYPE_3F32, 8, 55, 128, nvcv::TYPE_U8, 8, nvcv::TYPE_S32}, { nvcv::FMT_F32 , 32, 32, 8 , 0.5f , 0.01f , 20.f , 2 , 8, 55, nvcv::TYPE_4F32, 8, 55, nvcv::TYPE_3F32, 8, 55, 128, nvcv::TYPE_U8, 8, nvcv::TYPE_S32}, @@ -1147,13 +1479,14 @@ TEST_P(OpSIFT_Negative, invalid_parameters) cvcuda::SIFT op(maxShape, maxOctaveLayers); - EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall( - [&] - { - op(stream, src, featCoords, featMetadata, featDescriptors, - numFeatures, numOctaveLayers, contrastThreshold, edgeThreshold, - initSigma, flags); - })); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall( + [&op, &stream, &src, &featCoords, &featMetadata, &featDescriptors, &numFeatures, &numOctaveLayers, + &contrastThreshold, &edgeThreshold, &initSigma, &flags] + { + op(stream, src, featCoords, featMetadata, featDescriptors, numFeatures, numOctaveLayers, + contrastThreshold, edgeThreshold, initSigma, flags); + })); ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); diff --git a/tests/cvcuda/system/TestOpSolarize.cpp b/tests/cvcuda/system/TestOpSolarize.cpp new file mode 100644 index 000000000..830d04d18 --- /dev/null +++ b/tests/cvcuda/system/TestOpSolarize.cpp @@ -0,0 +1,161 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "ElementwiseOpHarness.hpp" +#include "PlanarParityUtils.hpp" + +#include +#include + +#include + +namespace test = nvcv::test; +namespace ew = nvcv::test::elementwise; + +namespace { + +// Independent CPU gold: out = (in >= threshold) ? (bound - in) : in, per element. Mirrors the +// documented oracle (torchvision solarize) and is computed independently of the kernel so the +// bit-exact EXPECT_EQ is a real regression check. +template +T SolarizeScalarGold(T v, double threshold) +{ + const T bound = ew::Bound(); + return (static_cast(v) >= threshold) ? static_cast(bound - v) : v; +} + +// Mid-range threshold per element type, so the random data exercises both the invert branch and +// the pass-through branch (u8 -> 127.5, u16 -> 32767.5, f32 -> 0.5). +template +double MidThreshold() +{ + return std::is_floating_point_v
? 0.5 : static_cast(ew::Bound
()) / 2.0; +} + +// Per-dtype gold + invoke factories: both pin the same mid-range threshold for a given element type +// so the kernel and the reference stay in lockstep across u8 / u16 / f32. +const auto goldFor = [](DT) +{ + const double thr = MidThreshold
(); + return [thr](DT v) + { + return SolarizeScalarGold
(v, thr); + }; +}; + +const auto invokeFor = [](DT) +{ + const double thr = MidThreshold
(); + return [thr](cudaStream_t s, const auto &in, auto &out) + { + cvcuda::Solarize op; + op(s, in, out, thr); + }; +}; + +// A fixed-threshold invoker for the var-shape / parity / negative cases (which use one dtype or +// don't reach the kernel). +auto invokeThr(double thr) +{ + return [thr](cudaStream_t s, const auto &in, auto &out) + { + cvcuda::Solarize op; + op(s, in, out, thr); + }; +} + +} // namespace + +// Tensor correctness over the declared dtype × channel matrix ----------------------------------- +// clang-format off +NVCV_TEST_SUITE_P(OpSolarize, test::ValueList +{ + // width, height, batch, format (dtype / channels) + { 66, 55, 1, nvcv::FMT_U8 }, // u8 / 1ch + { 123, 67, 3, nvcv::FMT_RGB8 }, // u8 / 3ch + { 42, 53, 4, nvcv::FMT_RGBA8 }, // u8 / 4ch + { 80, 40, 2, nvcv::FMT_U16 }, // u16 / 1ch + { 17, 19, 1, nvcv::FMT_F32 }, // f32 / 1ch + { 101, 33, 2, nvcv::FMT_RGBf32 }, // f32 / 3ch + { 64, 48, 3, nvcv::FMT_RGBAf32}, // f32 / 4ch +}); + +// clang-format on +TEST_P(OpSolarize, tensor_correct_output) +{ + ew::RunTensorCorrectDispatch(GetParamValue<0>(), GetParamValue<1>(), GetParamValue<2>(), + nvcv::ImageFormat{GetParamValue<3>()}, goldFor, invokeFor); +} + +// VarShape correctness: bit-exact vs the CPU gold (fixed uint8 threshold) ------------------------ +TEST(OpSolarize, varshape_correct_output) +{ + ew::RunVarShapeCorrect([](uint8_t v) { return SolarizeScalarGold(v, 100.0); }, invokeThr(100.0)); +} + +// Planar ≡ interleaved parity (fake-planar) ----------------------------------------------------- +// clang-format off +NVCV_TEST_SUITE_P(OpSolarizePlanar, + test::ValueList{ + {176, 113, 2, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + { 64, 48, 1, nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8}, + {100, 80, 2, nvcv::FMT_RGBAf32p, nvcv::FMT_RGBAf32}, +}); + +NVCV_TEST_SUITE_P(OpSolarizePlanarVarShape, + test::ValueList{ + {176, 113, 2, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + {100, 80, 2, nvcv::FMT_RGBAf32p, nvcv::FMT_RGBAf32}, +}); + +// clang-format on +TEST_P(OpSolarizePlanar, tensor_matches_interleaved) +{ + test::planar::RunTensorParity(GetParamValue<3>(), GetParamValue<4>(), GetParamValue<0>(), GetParamValue<1>(), + GetParamValue<0>(), GetParamValue<1>(), GetParamValue<2>(), + [](cudaStream_t s, const nvcv::Tensor &src, const nvcv::Tensor &dst, + nvcv::ImageFormat) { EXPECT_NO_THROW(invokeThr(100.0)(s, src, dst)); }); +} + +TEST_P(OpSolarizePlanarVarShape, varshape_matches_interleaved) +{ + test::planar::RunVarShapeParity( + GetParamValue<3>(), GetParamValue<4>(), GetParamValue<0>(), GetParamValue<1>(), GetParamValue<0>(), + GetParamValue<1>(), GetParamValue<2>(), + [](cudaStream_t s, const nvcv::ImageBatchVarShape &src, const nvcv::ImageBatchVarShape &dst, nvcv::ImageFormat) + { EXPECT_NO_THROW(invokeThr(100.0)(s, src, dst)); }); +} + +// Negative tests: the complement of the support matrix must be rejected ------------------------- +// clang-format off +NVCV_TEST_SUITE_P(OpSolarize_Negative, test::ValueList{ + {nvcv::FMT_F16, nvcv::FMT_F16 }, // unsupported dtype (16-bit float) + {nvcv::FMT_S16, nvcv::FMT_S16 }, // unsupported dtype (signed 16-bit) + {nvcv::FMT_RGB8, nvcv::FMT_RGB8p}, // layout mismatch (interleaved in, planar out) + {nvcv::FMT_RGB8, nvcv::FMT_RGBf32}, // input/output data type mismatch +}); + +// clang-format on +TEST_P(OpSolarize_Negative, rejects_unsupported) +{ + ew::ExpectRejected(GetParamValue<0>(), GetParamValue<1>(), invokeThr(128.0)); +} + +TEST(OpSolarize_Negative, rejects_two_channel) +{ + ew::ExpectRejected(nvcv::FMT_2F32, nvcv::FMT_2F32, invokeThr(0.5), 16, 16); +} diff --git a/tests/cvcuda/system/TestOpStack.cpp b/tests/cvcuda/system/TestOpStack.cpp index 0a3115b7d..0f7a4ded7 100644 --- a/tests/cvcuda/system/TestOpStack.cpp +++ b/tests/cvcuda/system/TestOpStack.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,6 +21,7 @@ #include #include #include +#include #include #include @@ -87,21 +88,21 @@ TEST_P(OpStack, test_NCHW_tensors) std::vector> inputVecs; // generate random input images - std::default_random_engine randEng(0); - std::uniform_int_distribution rand(0u, 255u); - std::uniform_int_distribution distribution(1, maxNumberInBatch); - int numberInBatch = distribution(randEng); + std::default_random_engine randEng(0); + std::uniform_int_distribution rand(0u, 255u); + std::uniform_int_distribution distribution(1, maxNumberInBatch); + int numberInBatch = distribution(randEng); for (int i = 0; i < numberOfTensors; ++i) { nvcv::Tensor inTensor(numberInBatch, {width, height}, format); totalNumberOfTensors += numberInBatch; // include individual tensors and tensors in N > 1 tensor(s) - for (int j = 0; j < numberInBatch; j++) + for (int j = 0; j < numberInBatch; j++) // NOSONAR { // generate random input image in bytes std::vector imageVec((width * height) * bytesPerPixel); - std::generate(imageVec.begin(), imageVec.end(), [&]() { return (nvcv::Byte)rand(randEng); }); + std::ranges::generate(imageVec, [&rand, &randEng]() { return (nvcv::Byte)rand(randEng); }); // copy random input to device tensor EXPECT_NO_THROW(util::SetImageTensorFromByteVector(inTensor.exportData(), imageVec, j)); // add tensor to batch and input vector @@ -119,7 +120,7 @@ TEST_P(OpStack, test_NCHW_tensors) ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); // go through each sample of the output tensor and compare vals. - for (int i = 0; i < totalNumberOfTensors; ++i) + for (int i = 0; i < totalNumberOfTensors; ++i) // NOSONAR { // generate random input image in bytes std::vector outSample; @@ -157,12 +158,12 @@ TEST_P(OpStack, test_CHW_tensors) std::uniform_int_distribution rand(0u, 255u); std::vector> inputVecs; - for (int i = 0; i < numberOfTensors; ++i) + for (int i = 0; i < numberOfTensors; ++i) // NOSONAR { nvcv::Tensor inTensor = nvcv::util::CreateTensor(1, width, height, format); //this will create a CHW/HWC tensor // generate random input image in bytes std::vector imageVec((width * height) * bytesPerPixel); - std::generate(imageVec.begin(), imageVec.end(), [&]() { return (nvcv::Byte)rand(randEng); }); + std::ranges::generate(imageVec, [&rand, &randEng]() { return (nvcv::Byte)rand(randEng); }); // copy random input to device tensor EXPECT_NO_THROW(util::SetImageTensorFromByteVector(inTensor.exportData(), imageVec)); // add tensor to batch and input vector @@ -179,7 +180,7 @@ TEST_P(OpStack, test_CHW_tensors) ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); // go through each sample of the output tensor and compare vals. - for (int i = 0; i < numberOfTensors; ++i) + for (int i = 0; i < numberOfTensors; ++i) // NOSONAR { // generate random input image in bytes std::vector outSample; @@ -235,11 +236,159 @@ TEST(OpStack_Negative, invalid_parameters) nvcv::Tensor misMatchOutTensor(numberOfTensors, {width - 1, height}, format); // run operator - EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&] { op(stream, inTensorBatch, invalidRankOutTensor); })); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&op, &stream, &inTensorBatch, &invalidRankOutTensor] + { op(stream, inTensorBatch, invalidRankOutTensor); })); + + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&op, &stream, &inTensorBatch, &smallOutTensor] + { op(stream, inTensorBatch, smallOutTensor); })); + + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&op, &stream, &inTensorBatch, &misMatchOutTensor] + { op(stream, inTensorBatch, misMatchOutTensor); })); + + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST_P(OpStack, varshape_correct_output) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + int width = GetParamValue<0>(); + int height = GetParamValue<1>(); + nvcv::ImageFormat format = GetParamValue<2>(); + int numberOfImages = GetParamValue<3>(); + + int numChannels = format.numChannels(); + int bytesPerPixel = 0; + + for (int32_t i = 0; i < numChannels; i++) + { + bytesPerPixel += format.bitsPerChannel()[i] / 8; + } + + // Create input varshape image batch + std::vector imgSrc; + for (int i = 0; i < numberOfImages; ++i) // NOSONAR + { + imgSrc.emplace_back(nvcv::Size2D{width, height}, format); + } + + nvcv::ImageBatchVarShape inBatch(numberOfImages); + inBatch.pushBack(imgSrc.begin(), imgSrc.end()); + + // generate random input images + std::default_random_engine randEng(0); + std::uniform_int_distribution rand(0u, 255u); + std::vector> inputVecs(numberOfImages); + + for (int i = 0; i < numberOfImages; ++i) // NOSONAR + { + // generate random input image in bytes + inputVecs[i].resize((width * height) * bytesPerPixel); + std::ranges::generate(inputVecs[i], [&rand, &randEng]() { return (nvcv::Byte)rand(randEng); }); + + // copy random input to device image + auto imgData = imgSrc[i].exportData(); + ASSERT_NE(imgData, nullptr); + + const NVCVImageBufferStrided &buf = imgData->cdata().buffer.strided; + for (int p = 0; p < buf.numPlanes; ++p) + { + const NVCVImagePlaneStrided &plane = buf.planes[p]; + int offset = (format.numPlanes() > 1) ? (p * width * height * (bytesPerPixel / numChannels)) : 0; + int rowBytes = plane.width * (bytesPerPixel / buf.numPlanes); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(plane.basePtr, plane.rowStride, inputVecs[i].data() + offset, rowBytes, + rowBytes, plane.height, cudaMemcpyHostToDevice)); + } + } + + nvcv::Tensor outTensor(numberOfImages, {width, height}, format); + + // run operator + cvcuda::Stack op; + EXPECT_NO_THROW(op(stream, inBatch, outTensor)); + + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + + // go through each sample of the output tensor and compare vals. + for (int i = 0; i < numberOfImages; ++i) // NOSONAR + { + std::vector outSample; + EXPECT_NO_THROW(util::GetImageByteVectorFromTensor(outTensor.exportData(), i, outSample)); + ASSERT_EQ(inputVecs[i], outSample); + } +} + +TEST(OpStack_Negative, varshape_invalid_parameters) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + int width = 24; + int height = 24; + nvcv::ImageFormat format = nvcv::FMT_U8; + int numberOfImages = 10; + + // Create input varshape image batch + std::vector imgSrc; + for (int i = 0; i < numberOfImages; ++i) + { + imgSrc.emplace_back(nvcv::Size2D{width, height}, format); + } - EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&] { op(stream, inTensorBatch, smallOutTensor); })); + nvcv::ImageBatchVarShape inBatch(numberOfImages); + inBatch.pushBack(imgSrc.begin(), imgSrc.end()); - EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&] { op(stream, inTensorBatch, misMatchOutTensor); })); + cvcuda::Stack op; + + // Invalid rank output tensor + nvcv::Tensor invalidRankOutTensor( + { + {height, width}, + "HW" + }, + nvcv::TYPE_U8); + + // Output tensor too small + nvcv::Tensor smallOutTensor(numberOfImages - 1, {width, height}, format); + + // Dimension mismatch + nvcv::Tensor misMatchOutTensor(numberOfImages, {width - 1, height}, format); + + // Test invalid cases + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&op, &stream, &inBatch, &invalidRankOutTensor] + { op(stream, inBatch, invalidRankOutTensor); })); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&op, &stream, &inBatch, &smallOutTensor] { op(stream, inBatch, smallOutTensor); })); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall([&op, &stream, &inBatch, &misMatchOutTensor] + { op(stream, inBatch, misMatchOutTensor); })); + + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpStack_Negative, varshape_plane_mismatch_does_not_access_missing_planes) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + constexpr int kWidth = 24; + constexpr int kHeight = 24; + + nvcv::Image image(nvcv::Size2D{kWidth, kHeight}, nvcv::FMT_RGB8); + nvcv::ImageBatchVarShape inBatch(1); + inBatch.pushBack(image); + nvcv::Tensor outTensor( + { + {1, 3, kHeight, kWidth}, + "NCHW" + }, + nvcv::TYPE_U8); + + cvcuda::Stack op; + EXPECT_NO_THROW(op(stream, inBatch, outTensor)); ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); diff --git a/tests/cvcuda/system/TestOpThreshold.cpp b/tests/cvcuda/system/TestOpThreshold.cpp index f09b54893..8574bd902 100644 --- a/tests/cvcuda/system/TestOpThreshold.cpp +++ b/tests/cvcuda/system/TestOpThreshold.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,6 +16,7 @@ */ #include "Definitions.hpp" +#include "PlanarParityUtils.hpp" #include #include @@ -25,9 +26,21 @@ #include #include +#include +#include #include +#include +#include #include #include +#include +#include +#include + +static int ScaledSize(int size, double scale) +{ + return static_cast(static_cast(size) * scale); +} static double getThreshVal_Otsu(std::vector &src) { @@ -35,22 +48,28 @@ static double getThreshVal_Otsu(std::vector &src) std::vector h(N, 0); int i; - int size = src.size(); + auto size = static_cast(src.size()); for (i = 0; i < size; i++) h[src[i]]++; - double mu = 0, scale = 1. / size; + double mu = 0; + double scale = 1. / size; for (i = 0; i < N; i++) { mu += i * (double)h[i]; } mu *= scale; - double mu1 = 0, q1 = 0; - double max_sigma = 0, max_val = 0; + double mu1 = 0; + double q1 = 0; + double max_sigma = 0; + double max_val = 0; for (i = 0; i < N; i++) { - double p_i, q2, mu2, sigma; + double p_i; + double q2; + double mu2; + double sigma; p_i = h[i] * scale; mu1 *= q1; @@ -76,12 +95,16 @@ static double getThreshVal_Triangle(std::vector &src) { int N = 256; std::vector h(N, 0); - int i, j; + int i; + int j; - int size = src.size(); + auto size = static_cast(src.size()); for (i = 0; i < size; i++) h[src[i]]++; - int left_bound = 0, right_bound = 0, max_ind = 0, max = 0; + int left_bound = 0; + int right_bound = 0; + int max_ind = 0; + int max = 0; int temp; bool isflipped = false; @@ -119,7 +142,8 @@ static double getThreshVal_Triangle(std::vector &src) if (max_ind - left_bound < right_bound - max_ind) { isflipped = true; - i = 0, j = N - 1; + i = 0; + j = N - 1; while (i < j) { temp = h[i]; @@ -133,7 +157,10 @@ static double getThreshVal_Triangle(std::vector &src) } double thresh = left_bound; - double a, b, dist = 0, tempdist; + double a; + double b; + double dist = 0; + double tempdist; a = max; b = left_bound - max_ind; @@ -157,62 +184,118 @@ static double getThreshVal_Triangle(std::vector &src) namespace { //test for uint8 template -void Threshold(std::vector &src, std::vector &dst, double thresh, double maxval, uint32_t type) +bool ResolveAutomaticThreshold(std::vector &src, double &thresh, int automatic_thresh) { - int automatic_thresh = (type & ~NVCV_THRESH_MASK); - type &= NVCV_THRESH_MASK; - if (automatic_thresh == (NVCV_THRESH_OTSU | NVCV_THRESH_TRIANGLE)) - return; + { + return false; + } + if (automatic_thresh == NVCV_THRESH_OTSU) + { thresh = getThreshVal_Otsu(src); + } else if (automatic_thresh == NVCV_THRESH_TRIANGLE) + { thresh = getThreshVal_Triangle(src); + } - int ithresh = floor(thresh); - thresh = ithresh; - int imaxval = round(maxval); - if (type == NVCV_THRESH_TRUNC) - imaxval = ithresh; - imaxval = (uint8_t)((unsigned)imaxval <= UCHAR_MAX ? imaxval : imaxval > 0 ? UCHAR_MAX : 0); + return true; +} - if (ithresh < 0 || ithresh >= 255) +uint8_t ClampMaxThreshold(int maxval) +{ + return static_cast(std::clamp(maxval, 0, UCHAR_MAX)); +} + +bool ShouldFillOutOfRangeThreshold(uint32_t type, int ithresh) +{ + return type == NVCV_THRESH_BINARY || type == NVCV_THRESH_BINARY_INV + || ((type == NVCV_THRESH_TRUNC || type == NVCV_THRESH_TOZERO_INV) && ithresh < 0) + || (type == NVCV_THRESH_TOZERO && ithresh >= 255); +} + +uint8_t OutOfRangeThresholdValue(uint32_t type, int ithresh, uint8_t maxval) +{ + if (type == NVCV_THRESH_BINARY) { - if (type == NVCV_THRESH_BINARY || type == NVCV_THRESH_BINARY_INV - || ((type == NVCV_THRESH_TRUNC || type == NVCV_THRESH_TOZERO_INV) && ithresh < 0) - || (type == NVCV_THRESH_TOZERO && ithresh >= 255)) - { - int v = type == NVCV_THRESH_BINARY ? (ithresh >= 255 ? 0 : imaxval) - : type == NVCV_THRESH_BINARY_INV ? (ithresh >= 255 ? imaxval : 0) - : 0; - std::fill(dst.begin(), dst.end(), v); - } - else - dst.assign(src.begin(), src.end()); - return; + return ithresh >= 255 ? 0 : maxval; + } + + if (type == NVCV_THRESH_BINARY_INV) + { + return ithresh >= 255 ? maxval : 0; } - thresh = ithresh; - maxval = imaxval; - int size = src.size(); + return 0; +} + +template +void ApplyOutOfRangeThreshold(const std::vector &src, std::vector &dst, uint32_t type, int ithresh, + uint8_t maxval) +{ + if (ShouldFillOutOfRangeThreshold(type, ithresh)) + { + std::ranges::fill(dst, static_cast(OutOfRangeThresholdValue(type, ithresh, maxval))); + } + else + { + dst.assign(src.begin(), src.end()); + } +} + +template +void ApplyThreshold(std::vector &src, std::vector &dst, uint32_t type, int ithresh, uint8_t maxval) +{ + auto size = static_cast(src.size()); switch (type) { case NVCV_THRESH_BINARY: - for (int i = 0; i < size; i++) dst[i] = src[i] > thresh ? maxval : 0; + for (int i = 0; i < size; i++) dst[i] = src[i] > ithresh ? maxval : 0; break; case NVCV_THRESH_BINARY_INV: - for (int i = 0; i < size; i++) dst[i] = src[i] <= thresh ? maxval : 0; + for (int i = 0; i < size; i++) dst[i] = src[i] <= ithresh ? maxval : 0; break; case NVCV_THRESH_TRUNC: - for (int i = 0; i < size; i++) dst[i] = std::min(src[i], (uint8_t)thresh); + for (int i = 0; i < size; i++) dst[i] = std::min(src[i], static_cast(ithresh)); break; case NVCV_THRESH_TOZERO: - for (int i = 0; i < size; i++) dst[i] = src[i] > thresh ? src[i] : 0; + for (int i = 0; i < size; i++) dst[i] = src[i] > ithresh ? src[i] : 0; break; case NVCV_THRESH_TOZERO_INV: - for (int i = 0; i < size; i++) dst[i] = src[i] <= thresh ? src[i] : 0; + for (int i = 0; i < size; i++) dst[i] = src[i] <= ithresh ? src[i] : 0; break; + default: + break; + } +} + +template +void Threshold(std::vector &src, std::vector &dst, double thresh, double maxval, uint32_t type) +{ + int automatic_thresh = (type & ~NVCV_THRESH_MASK); + type &= NVCV_THRESH_MASK; + + if (!ResolveAutomaticThreshold(src, thresh, automatic_thresh)) + { + return; + } + + auto ithresh = static_cast(floor(thresh)); + auto imaxval = static_cast(round(maxval)); + if (type == NVCV_THRESH_TRUNC) + { + imaxval = ithresh; + } + uint8_t clampedMaxVal = ClampMaxThreshold(imaxval); + + if (ithresh < 0 || ithresh >= 255) + { + ApplyOutOfRangeThreshold(src, dst, type, ithresh, clampedMaxVal); + return; } + + ApplyThreshold(src, dst, type, ithresh, clampedMaxVal); } // test for double @@ -227,7 +310,7 @@ void Threshold(std::vector &src, std::vector &dst, double thresh return; dst.assign(src.begin(), src.end()); - int size = src.size(); + auto size = static_cast(src.size()); switch (type) { case NVCV_THRESH_BINARY: @@ -237,7 +320,7 @@ void Threshold(std::vector &src, std::vector &dst, double thresh for (int i = 0; i < size; i++) dst[i] = src[i] <= thresh ? maxval : 0; break; case NVCV_THRESH_TRUNC: - for (int i = 0; i < size; i++) dst[i] = std::min(static_cast(src[i]), thresh); + for (int i = 0; i < size; i++) dst[i] = std::min(src[i], thresh); break; case NVCV_THRESH_TOZERO: for (int i = 0; i < size; i++) dst[i] = src[i] > thresh ? src[i] : 0; @@ -245,6 +328,8 @@ void Threshold(std::vector &src, std::vector &dst, double thresh case NVCV_THRESH_TOZERO_INV: for (int i = 0; i < size; i++) dst[i] = src[i] <= thresh ? src[i] : 0; break; + default: + break; } } @@ -268,26 +353,655 @@ void ThresholdWrapper(std::vector &src, std::vector &dst, doub } template -void myGenerate(T *src, std::size_t size, std::default_random_engine &randEng) +void myGenerate( // NOSONAR: std::span is C++20. + T *src, std::size_t size, std::default_random_engine &randEng) { std::uniform_int_distribution rand(0u, 255u); for (std::size_t idx = 0; idx < size; ++idx) { - src[idx] = rand(randEng); + src[idx] = static_cast(rand(randEng)); } } template<> -void myGenerate(double *src, std::size_t size, std::default_random_engine &randEng) +void myGenerate( // NOSONAR: std::span is C++20. + double *src, std::size_t size, std::default_random_engine &randEng) { - std::uniform_real_distribution rand(0., 1.); + std::uniform_real_distribution rand(0., 1.); for (std::size_t idx = 0; idx < size; ++idx) { src[idx] = rand(randEng); } } + +nvcv::Tensor MakeThresholdParam(int numImages, double value) +{ + return nvcv::test::planar::MakePerImageTensor(numImages, nvcv::TYPE_F64, value); +} + +template +T ThresholdGoldValue(T input, double thresh, double maxval, uint32_t type) +{ + T typedThresh; + T typedMaxval; + if constexpr (std::is_floating_point_v) + { + typedThresh = static_cast(thresh); + typedMaxval = static_cast(maxval); + } + else + { + typedThresh = static_cast(static_cast(std::floor(thresh))); + typedMaxval = static_cast(static_cast(std::round(maxval))); + } + + switch (type & NVCV_THRESH_MASK) + { + case NVCV_THRESH_BINARY: + return input > typedThresh ? typedMaxval : T{}; + case NVCV_THRESH_BINARY_INV: + return input > typedThresh ? T{} : typedMaxval; + case NVCV_THRESH_TRUNC: + return input > typedThresh ? typedThresh : input; + case NVCV_THRESH_TOZERO: + return input > typedThresh ? input : T{}; + default: + return input > typedThresh ? T{} : input; + } +} + +template +T ThresholdPackInput(int sample, int index) +{ + int value = (sample * 11 + index * 7) % 19; + if constexpr (std::is_signed_v || std::is_floating_point_v) + value -= 7; + return static_cast(value); +} + +template +void ThresholdGold(std::vector &src, std::vector &dst, double thresh, double maxval, uint32_t type) +{ + if constexpr (std::is_same_v) + { + Threshold(src, dst, thresh, maxval, type); + } + else + { + std::ranges::transform(src, dst.begin(), + [=](T input) { return ThresholdGoldValue(input, thresh, maxval, type); }); + } +} + +template +void RunTensorUnalignedPackCase(nvcv::DataType dtype, uint32_t type, int width) +{ + constexpr int numImages = 2; + constexpr int height = 2; + constexpr size_t guardBytes = 32; + constexpr double thresh = 3.5; + constexpr double maxval = 9.25; + const size_t offset = alignof(T); + const int rowBytes = width * sizeof(T); + const int rowStride = rowBytes + 16; + const int sampleStride = rowStride * height + 16; + const size_t bytes = offset + sampleStride * numImages + guardBytes; + const size_t packBytes = sizeof(T) == sizeof(double) ? 2 * sizeof(T) : 4 * sizeof(T); + + NVCVByte *srcAllocation{}; + NVCVByte *dstAllocation{}; + ASSERT_EQ(cudaSuccess, cudaMalloc(reinterpret_cast(&srcAllocation), bytes)); + ASSERT_EQ(cudaSuccess, cudaMalloc(reinterpret_cast(&dstAllocation), bytes)); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + { + auto wrapTensor = [&](NVCVByte *allocation) + { + nvcv::TensorDataStridedCuda::Buffer buffer{}; + buffer.basePtr = allocation + offset; + buffer.strides[0] = sampleStride; + buffer.strides[1] = rowStride; + buffer.strides[2] = sizeof(T); + buffer.strides[3] = sizeof(T); + return nvcv::TensorWrapData(nvcv::TensorDataStridedCuda{ + nvcv::TensorShape{{numImages, height, width, 1}, "NHWC"}, + dtype, buffer + }); + }; + + nvcv::Tensor src = wrapTensor(srcAllocation); + nvcv::Tensor dst = wrapTensor(dstAllocation); + EXPECT_EQ(0u, reinterpret_cast(srcAllocation + offset) % alignof(T)); + EXPECT_NE(0u, reinterpret_cast(srcAllocation + offset) % packBytes); + EXPECT_NE(0u, reinterpret_cast(dstAllocation + offset) % packBytes); + + std::vector expected(bytes, 0xD7); + ASSERT_EQ(cudaSuccess, cudaMemset(srcAllocation, 0xA5, bytes)); + ASSERT_EQ(cudaSuccess, cudaMemset(dstAllocation, 0xD7, bytes)); + + for (int b = 0; b < numImages; ++b) + { + std::vector srcVisible(width * height); + size_t index = 0; + std::ranges::generate(srcVisible, + [&] + { + const T value = ThresholdPackInput(b, static_cast(index)); + ++index; + return value; + }); + + std::vector dstVisible(srcVisible.size()); + ThresholdGold(srcVisible, dstVisible, thresh, maxval, type); + for (int y = 0; y < height; ++y) + { + std::memcpy(expected.data() + offset + b * sampleStride + y * rowStride, dstVisible.data() + y * width, + rowBytes); + } + + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(srcAllocation + offset + b * sampleStride, rowStride, srcVisible.data(), + rowBytes, rowBytes, height, cudaMemcpyHostToDevice)); + } + + nvcv::Tensor threshval = MakeThresholdParam(numImages, thresh); + nvcv::Tensor maxvalval = MakeThresholdParam(numImages, maxval); + cvcuda::Threshold thresholdOp(type, numImages); + EXPECT_NO_THROW(thresholdOp(stream, src, dst, threshval, maxvalval)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + std::vector got(bytes); + ASSERT_EQ(cudaSuccess, cudaMemcpy(got.data(), dstAllocation, bytes, cudaMemcpyDeviceToHost)); + EXPECT_EQ(expected, got); + } + + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); + EXPECT_EQ(cudaSuccess, cudaFree(srcAllocation)); + EXPECT_EQ(cudaSuccess, cudaFree(dstAllocation)); +} + +template +void RunVarShapeUnalignedPackCase(nvcv::ImageFormat fmt, uint32_t type) +{ + constexpr int numImages = 2; + constexpr int width = 9; + constexpr int height = 2; + constexpr size_t guardBytes = 32; + constexpr double thresh = 3.5; + constexpr double maxval = 9.25; + const size_t offset = alignof(T); + const int rowBytes = width * sizeof(T); + const int rowStride = rowBytes + 16; + const size_t bytes = offset + static_cast(rowStride) * height + guardBytes; + const size_t packBytes = sizeof(T) == sizeof(double) ? 2 * sizeof(T) : 4 * sizeof(T); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + std::array srcAllocations{}; + std::array dstAllocations{}; + std::array, numImages> expected; + { + std::vector srcImages; + std::vector dstImages; + for (int b = 0; b < numImages; ++b) + { + ASSERT_EQ(cudaSuccess, cudaMalloc(reinterpret_cast(&srcAllocations[b]), bytes)); + ASSERT_EQ(cudaSuccess, cudaMalloc(reinterpret_cast(&dstAllocations[b]), bytes)); + + nvcv::ImageDataStridedCuda::Buffer srcBuffer{}; + srcBuffer.numPlanes = 1; + srcBuffer.planes[0].width = width; + srcBuffer.planes[0].height = height; + srcBuffer.planes[0].rowStride = rowStride; + srcBuffer.planes[0].basePtr = srcAllocations[b] + offset; + auto dstBuffer = srcBuffer; + dstBuffer.planes[0].basePtr = dstAllocations[b] + offset; + EXPECT_EQ(0u, reinterpret_cast(srcBuffer.planes[0].basePtr) % alignof(T)); + EXPECT_NE(0u, reinterpret_cast(srcBuffer.planes[0].basePtr) % packBytes); + EXPECT_NE(0u, reinterpret_cast(dstBuffer.planes[0].basePtr) % packBytes); + + srcImages.emplace_back(nvcv::ImageWrapData(nvcv::ImageDataStridedCuda{fmt, srcBuffer})); + dstImages.emplace_back(nvcv::ImageWrapData(nvcv::ImageDataStridedCuda{fmt, dstBuffer})); + + std::vector srcVisible(width * height); + size_t index = 0; + std::ranges::generate(srcVisible, + [&] + { + const T value = ThresholdPackInput(b, static_cast(index)); + ++index; + return value; + }); + std::vector dstVisible(srcVisible.size()); + ThresholdGold(srcVisible, dstVisible, thresh, maxval, type); + + expected[b].assign(bytes, 0xD7); + for (int y = 0; y < height; ++y) + { + std::memcpy(expected[b].data() + offset + y * rowStride, dstVisible.data() + y * width, rowBytes); + } + ASSERT_EQ(cudaSuccess, cudaMemset(srcAllocations[b], 0xA5, bytes)); + ASSERT_EQ(cudaSuccess, cudaMemset(dstAllocations[b], 0xD7, bytes)); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(srcBuffer.planes[0].basePtr, rowStride, srcVisible.data(), rowBytes, + rowBytes, height, cudaMemcpyHostToDevice)); + } + + nvcv::ImageBatchVarShape srcBatch(numImages); + nvcv::ImageBatchVarShape dstBatch(numImages); + srcBatch.pushBack(srcImages.begin(), srcImages.end()); + dstBatch.pushBack(dstImages.begin(), dstImages.end()); + nvcv::Tensor threshval = MakeThresholdParam(numImages, thresh); + nvcv::Tensor maxvalval = MakeThresholdParam(numImages, maxval); + cvcuda::Threshold thresholdOp(type, numImages); + EXPECT_NO_THROW(thresholdOp(stream, srcBatch, dstBatch, threshval, maxvalval)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + for (int b = 0; b < numImages; ++b) + { + std::vector got(bytes); + ASSERT_EQ(cudaSuccess, cudaMemcpy(got.data(), dstAllocations[b], bytes, cudaMemcpyDeviceToHost)); + EXPECT_EQ(expected[b], got); + } + } + + for (int b = 0; b < numImages; ++b) + { + EXPECT_EQ(cudaSuccess, cudaFree(srcAllocations[b])); + EXPECT_EQ(cudaSuccess, cudaFree(dstAllocations[b])); + } + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +struct U8BinaryPackCase +{ + const char *name; + int width; + int height; + int rowStride; + size_t srcOffset; + size_t dstOffset; + double thresh; + double maxval; +}; + +struct U8BinaryPackStorage +{ + NVCVByte *srcAllocation{}; + NVCVByte *dstAllocation{}; + size_t srcBytes{}; + size_t dstBytes{}; + std::vector expected; +}; + +void RunU8PackCases(nvcv::ImageFormat fmt, uint32_t type, const std::vector &cases) +{ + constexpr size_t guardBytes = 32; + const int pixelBytes = fmt.planePixelStrideBytes(0); + const auto numImages = static_cast(cases.size()); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + std::vector storage(numImages); + { + std::vector srcImages; + std::vector dstImages; + srcImages.reserve(numImages); + dstImages.reserve(numImages); + + for (int b = 0; b < numImages; ++b) + { + const U8BinaryPackCase &testCase = cases[b]; + SCOPED_TRACE(testCase.name); + + const size_t rowBytes = static_cast(testCase.width) * pixelBytes; + ASSERT_GE(static_cast(testCase.rowStride), rowBytes); + + U8BinaryPackStorage &data = storage[b]; + data.srcBytes = testCase.srcOffset + static_cast(testCase.rowStride) * testCase.height + guardBytes; + data.dstBytes = testCase.dstOffset + static_cast(testCase.rowStride) * testCase.height + guardBytes; + ASSERT_EQ(cudaSuccess, cudaMalloc(reinterpret_cast(&data.srcAllocation), data.srcBytes)); + ASSERT_EQ(cudaSuccess, cudaMalloc(reinterpret_cast(&data.dstAllocation), data.dstBytes)); + + nvcv::ImageDataStridedCuda::Buffer srcBuffer{}; + srcBuffer.numPlanes = 1; + srcBuffer.planes[0].width = testCase.width; + srcBuffer.planes[0].height = testCase.height; + srcBuffer.planes[0].rowStride = testCase.rowStride; + srcBuffer.planes[0].basePtr = data.srcAllocation + testCase.srcOffset; + + auto dstBuffer = srcBuffer; + dstBuffer.planes[0].basePtr = data.dstAllocation + testCase.dstOffset; + + if (testCase.srcOffset == 0) + { + EXPECT_EQ(0u, reinterpret_cast(srcBuffer.planes[0].basePtr) & 15u); + EXPECT_EQ(0u, reinterpret_cast(dstBuffer.planes[0].basePtr) & 15u); + } + else + { + EXPECT_NE(0u, reinterpret_cast(srcBuffer.planes[0].basePtr) & 15u); + EXPECT_NE(0u, reinterpret_cast(dstBuffer.planes[0].basePtr) & 15u); + } + + srcImages.emplace_back(nvcv::ImageWrapData(nvcv::ImageDataStridedCuda{fmt, srcBuffer})); + dstImages.emplace_back(nvcv::ImageWrapData(nvcv::ImageDataStridedCuda{fmt, dstBuffer})); + + std::vector srcVisible(rowBytes * testCase.height); + data.expected.assign(data.dstBytes, 0xD7); + + for (int y = 0; y < testCase.height; ++y) + { + for (size_t x = 0; x < rowBytes; ++x) + { + const auto input = static_cast(b * 43 + y * 29 + x * 17 + 11); + srcVisible[static_cast(y) * rowBytes + x] = input; + } + } + + std::vector dstVisible(srcVisible.size()); + Threshold(srcVisible, dstVisible, testCase.thresh, testCase.maxval, type); + for (int y = 0; y < testCase.height; ++y) + { + std::copy_n(dstVisible.data() + static_cast(y) * rowBytes, rowBytes, + data.expected.data() + testCase.dstOffset + static_cast(y) * testCase.rowStride); + } + + ASSERT_EQ(cudaSuccess, cudaMemset(data.srcAllocation, 0xA5, data.srcBytes)); + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(srcBuffer.planes[0].basePtr, testCase.rowStride, srcVisible.data(), + rowBytes, rowBytes, testCase.height, cudaMemcpyHostToDevice)); + ASSERT_EQ(cudaSuccess, cudaMemset(data.dstAllocation, 0xD7, data.dstBytes)); + } + + nvcv::ImageBatchVarShape srcBatch(numImages); + nvcv::ImageBatchVarShape dstBatch(numImages); + srcBatch.pushBack(srcImages.begin(), srcImages.end()); + dstBatch.pushBack(dstImages.begin(), dstImages.end()); + + nvcv::Tensor threshval({{numImages}, "N"}, nvcv::TYPE_F64); + nvcv::Tensor maxvalval({{numImages}, "N"}, nvcv::TYPE_F64); + auto threshData = threshval.exportData(); + auto maxvalData = maxvalval.exportData(); + ASSERT_NE(nullptr, threshData); + ASSERT_NE(nullptr, maxvalData); + + std::vector threshVec; + std::vector maxvalVec; + threshVec.reserve(numImages); + maxvalVec.reserve(numImages); + for (const U8BinaryPackCase &testCase : cases) + { + threshVec.push_back(testCase.thresh); + maxvalVec.push_back(testCase.maxval); + } + + ASSERT_EQ(cudaSuccess, cudaMemcpyAsync(threshData->basePtr(), threshVec.data(), numImages * sizeof(double), + cudaMemcpyHostToDevice, stream)); + ASSERT_EQ(cudaSuccess, cudaMemcpyAsync(maxvalData->basePtr(), maxvalVec.data(), numImages * sizeof(double), + cudaMemcpyHostToDevice, stream)); + + cvcuda::Threshold thresholdOp(type, numImages); + EXPECT_NO_THROW(thresholdOp(stream, srcBatch, dstBatch, threshval, maxvalval)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + for (int b = 0; b < numImages; ++b) + { + SCOPED_TRACE(cases[b].name); + std::vector got(storage[b].dstBytes); + ASSERT_EQ(cudaSuccess, + cudaMemcpy(got.data(), storage[b].dstAllocation, storage[b].dstBytes, cudaMemcpyDeviceToHost)); + EXPECT_EQ(storage[b].expected, got); + } + } + + for (U8BinaryPackStorage &data : storage) + { + EXPECT_EQ(cudaSuccess, cudaFree(data.srcAllocation)); + EXPECT_EQ(cudaSuccess, cudaFree(data.dstAllocation)); + } + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} } // namespace +TEST(OpThreshold, varshape_u8_binary_aligned_pack_tail_broadcast_exact) +{ + RunU8PackCases(nvcv::FMT_U8, NVCV_THRESH_BINARY, + { + { "aligned_full_pack", 32, 2, 48, 0, 0, 100, 211}, + { "aligned_scalar_tail", 19, 2, 32, 0, 0, 100, 211}, + { "aligned_broadcast_max", 32, 2, 48, 0, 0, -1, 173}, + {"aligned_broadcast_zero", 19, 2, 32, 0, 0, 256, 173}, + }); + + RunU8PackCases(nvcv::FMT_RGB8, NVCV_THRESH_BINARY, + { + {"rgb_aligned_pack_tail", 7, 2, 32, 0, 0, 100, 211}, + }); +} + +TEST(OpThreshold, varshape_u8_binary_unaligned_pack_tail_broadcast_exact) +{ + RunU8PackCases(nvcv::FMT_U8, NVCV_THRESH_BINARY, + { + { "unaligned_full_pack", 32, 2, 48, 1, 1, 100, 211}, + { "unaligned_scalar_tail", 19, 2, 32, 1, 1, 100, 211}, + { "unaligned_broadcast_max", 32, 2, 48, 1, 1, -1, 173}, + {"unaligned_broadcast_zero_tail", 19, 2, 32, 1, 1, 256, 173}, + }); +} + +TEST(OpThreshold, varshape_rgb8_binary_unaligned_pack_tail_exact) +{ + RunU8PackCases(nvcv::FMT_RGB8, NVCV_THRESH_BINARY, + { + {"rgb_unaligned_pack_tail", 7, 2, 32, 1, 1, 100, 211}, + }); +} + +TEST(OpThreshold, varshape_u8_binary_inv_unaligned_pack_tail_broadcast_exact) +{ + RunU8PackCases(nvcv::FMT_U8, NVCV_THRESH_BINARY_INV, + { + {"binary_inv_in_range", 19, 2, 32, 1, 1, 100, 211}, + { "binary_inv_below", 19, 2, 32, 1, 1, -1, 173}, + { "binary_inv_above", 19, 2, 32, 1, 1, 256, 173}, + }); +} + +TEST(OpThreshold, varshape_u8_trunc_unaligned_pack_tail_broadcast_exact) +{ + RunU8PackCases(nvcv::FMT_U8, NVCV_THRESH_TRUNC, + { + {"trunc_in_range", 19, 2, 32, 1, 1, 100, 211}, + { "trunc_below", 19, 2, 32, 1, 1, -1, 173}, + { "trunc_above", 19, 2, 32, 1, 1, 256, 173}, + }); +} + +TEST(OpThreshold, varshape_u8_tozero_unaligned_pack_tail_broadcast_exact) +{ + RunU8PackCases(nvcv::FMT_U8, NVCV_THRESH_TOZERO, + { + {"tozero_in_range", 19, 2, 32, 1, 1, 100, 211}, + { "tozero_below", 19, 2, 32, 1, 1, -1, 173}, + { "tozero_above", 19, 2, 32, 1, 1, 256, 173}, + }); +} + +TEST(OpThreshold, varshape_u8_tozero_inv_unaligned_pack_tail_broadcast_exact) +{ + RunU8PackCases(nvcv::FMT_U8, NVCV_THRESH_TOZERO_INV, + { + {"tozero_inv_in_range", 19, 2, 32, 1, 1, 100, 211}, + { "tozero_inv_below", 19, 2, 32, 1, 1, -1, 173}, + { "tozero_inv_above", 19, 2, 32, 1, 1, 256, 173}, + }); +} + +namespace { +enum class UnalignedContainer +{ + Tensor, + VarShape, +}; + +enum class UnalignedDataType +{ + U16, + S16, + F32, + F64, +}; + +using UnalignedNonU8Param = std::tuple; + +const char *ThresholdModeName(uint32_t type) +{ + switch (type) + { + case NVCV_THRESH_BINARY: + return "Binary"; + case NVCV_THRESH_BINARY_INV: + return "BinaryInv"; + case NVCV_THRESH_TRUNC: + return "Trunc"; + case NVCV_THRESH_TOZERO: + return "ToZero"; + default: + return "ToZeroInv"; + } +} + +std::string UnalignedNonU8Name(const testing::TestParamInfo &info) +{ + const auto [container, dtype, type] = info.param; + const char *containerName = container == UnalignedContainer::Tensor ? "Tensor" : "VarShape"; + const char *dtypeName; + switch (dtype) + { + case UnalignedDataType::U16: + dtypeName = "U16"; + break; + case UnalignedDataType::S16: + dtypeName = "S16"; + break; + case UnalignedDataType::F32: + dtypeName = "F32"; + break; + default: + dtypeName = "F64"; + break; + } + return std::string(containerName) + dtypeName + ThresholdModeName(type); +} + +class OpThresholdUnalignedNonU8 : public testing::TestWithParam +{ +}; + +TEST_P(OpThresholdUnalignedNonU8, exact_gold) +{ + const auto [container, dtype, type] = GetParam(); + switch (dtype) + { + case UnalignedDataType::U16: + if (container == UnalignedContainer::Tensor) + RunTensorUnalignedPackCase(nvcv::TYPE_U16, type, 8); + else + RunVarShapeUnalignedPackCase(nvcv::FMT_U16, type); + break; + case UnalignedDataType::S16: + if (container == UnalignedContainer::Tensor) + RunTensorUnalignedPackCase(nvcv::TYPE_S16, type, 8); + else + RunVarShapeUnalignedPackCase(nvcv::FMT_S16, type); + break; + case UnalignedDataType::F32: + if (container == UnalignedContainer::Tensor) + RunTensorUnalignedPackCase(nvcv::TYPE_F32, type, 8); + else + RunVarShapeUnalignedPackCase(nvcv::FMT_F32, type); + break; + default: + if (container == UnalignedContainer::Tensor) + RunTensorUnalignedPackCase(nvcv::TYPE_F64, type, 8); + else + RunVarShapeUnalignedPackCase(nvcv::FMT_F64, type); + break; + } +} + +INSTANTIATE_TEST_SUITE_P( + All, OpThresholdUnalignedNonU8, + testing::Values(UnalignedNonU8Param{UnalignedContainer::Tensor, UnalignedDataType::U16, NVCV_THRESH_BINARY}, + UnalignedNonU8Param{UnalignedContainer::VarShape, UnalignedDataType::U16, NVCV_THRESH_BINARY_INV}, + UnalignedNonU8Param{UnalignedContainer::Tensor, UnalignedDataType::S16, NVCV_THRESH_TRUNC}, + UnalignedNonU8Param{UnalignedContainer::VarShape, UnalignedDataType::S16, NVCV_THRESH_TOZERO}, + UnalignedNonU8Param{UnalignedContainer::Tensor, UnalignedDataType::F32, NVCV_THRESH_TOZERO_INV}, + UnalignedNonU8Param{UnalignedContainer::VarShape, UnalignedDataType::F32, NVCV_THRESH_BINARY}, + UnalignedNonU8Param{UnalignedContainer::Tensor, UnalignedDataType::F64, NVCV_THRESH_BINARY_INV}, + UnalignedNonU8Param{UnalignedContainer::VarShape, UnalignedDataType::F64, NVCV_THRESH_TRUNC}), + UnalignedNonU8Name); + +enum class AutomaticMode +{ + Otsu, + Triangle, +}; + +using UnalignedAutomaticParam = std::tuple; + +std::string UnalignedAutomaticName(const testing::TestParamInfo &info) +{ + const auto [container, mode] = info.param; + return std::string(container == UnalignedContainer::Tensor ? "Tensor" : "VarShape") + + (mode == AutomaticMode::Otsu ? "Otsu" : "Triangle"); +} + +class OpThresholdUnalignedAutomatic : public testing::TestWithParam +{ +}; + +TEST_P(OpThresholdUnalignedAutomatic, exact_gold) +{ + const auto [container, mode] = GetParam(); + const uint32_t type = mode == AutomaticMode::Otsu ? NVCV_THRESH_OTSU | NVCV_THRESH_BINARY + : NVCV_THRESH_TRIANGLE | NVCV_THRESH_BINARY_INV; + if (container == UnalignedContainer::Tensor) + { + RunTensorUnalignedPackCase(nvcv::TYPE_U8, type, 32); + } + else if (mode == AutomaticMode::Otsu) + { + RunU8PackCases(nvcv::FMT_U8, type, + { + {"otsu_unaligned_full_pack", 32, 2, 48, 1, 1, 100, 211}, + { "otsu_unaligned_tail", 19, 2, 32, 1, 1, 100, 211}, + }); + } + else + { + RunU8PackCases(nvcv::FMT_U8, type, + { + {"triangle_unaligned_full_pack", 32, 2, 48, 1, 1, 100, 211}, + { "triangle_unaligned_tail", 19, 2, 32, 1, 1, 100, 211}, + }); + } +} + +INSTANTIATE_TEST_SUITE_P(All, OpThresholdUnalignedAutomatic, + testing::Combine(testing::Values(UnalignedContainer::Tensor, UnalignedContainer::VarShape), + testing::Values(AutomaticMode::Otsu, AutomaticMode::Triangle)), + UnalignedAutomaticName); +} // namespace + +TEST(OpThreshold, tensor_u8_binary_unaligned_pack_exact) +{ + RunTensorUnalignedPackCase(nvcv::TYPE_U8, NVCV_THRESH_BINARY, 32); +} + // clang-format off NVCV_TEST_SUITE_P(OpThreshold, nvcv::test::ValueList { @@ -339,7 +1053,7 @@ TEST_P(OpThreshold, tensor_correct_output) nvcv::ImageFormat fmt = GetParamValue<6>(); NVCVDataType nvcvDataType; - ASSERT_EQ(NVCV_SUCCESS, nvcvImageFormatGetPlaneDataType(fmt, 0, &nvcvDataType)); + ASSERT_EQ(NVCV_SUCCESS, nvcvImageFormatGetPlaneDataType(static_cast(fmt), 0, &nvcvDataType)); nvcv::Tensor imgIn = nvcv::util::CreateTensor(batch, width, height, fmt); nvcv::Tensor imgOut = nvcv::util::CreateTensor(batch, width, height, fmt); @@ -395,14 +1109,13 @@ TEST_P(OpThreshold, tensor_correct_output) for (int i = 0; i < batch; i++) { srcVec[i].resize(height * rowStride); - switch (nvcvDataType) + if (nvcvDataType == NVCV_DATA_TYPE_F64) { - case NVCV_DATA_TYPE_F64: myGenerate(reinterpret_cast(srcVec[i].data()), srcVec[i].size() / sizeof(double), randEng); - break; - default: - myGenerate(reinterpret_cast(srcVec[i].data()), srcVec[i].size(), randEng); - break; + } + else + { + myGenerate(srcVec[i].data(), srcVec[i].size(), randEng); } ASSERT_EQ(cudaSuccess, cudaMemcpy2D(inAccess->sampleData(i), inAccess->rowStride(), srcVec[i].data(), rowStride, rowStride, height, cudaMemcpyHostToDevice)); @@ -446,14 +1159,15 @@ TEST_P(OpThreshold, varshape_correct_shape) nvcv::ImageFormat fmt = GetParamValue<6>(); NVCVDataType nvcvDataType; - ASSERT_EQ(NVCV_SUCCESS, nvcvImageFormatGetPlaneDataType(fmt, 0, &nvcvDataType)); + ASSERT_EQ(NVCV_SUCCESS, nvcvImageFormatGetPlaneDataType(static_cast(fmt), 0, &nvcvDataType)); // Create input and output - std::default_random_engine randEng; - std::uniform_int_distribution rndWidth(width * 0.8, width * 1.1); - std::uniform_int_distribution rndHeight(height * 0.8, height * 1.1); + std::default_random_engine randEng; + std::uniform_int_distribution rndWidth(ScaledSize(width, 0.8), ScaledSize(width, 1.1)); + std::uniform_int_distribution rndHeight(ScaledSize(height, 0.8), ScaledSize(height, 1.1)); - std::vector imgSrc, imgDst; + std::vector imgSrc; + std::vector imgDst; for (int i = 0; i < batch; ++i) { int rw = rndWidth(randEng); @@ -501,14 +1215,13 @@ TEST_P(OpThreshold, varshape_correct_shape) int srcRowStride = srcWidth * fmt.planePixelStrideBytes(0); srcVec[i].resize(srcHeight * srcRowStride); - switch (nvcvDataType) + if (nvcvDataType == NVCV_DATA_TYPE_F64) { - case NVCV_DATA_TYPE_F64: myGenerate(reinterpret_cast(srcVec[i].data()), srcVec[i].size() / sizeof(double), randEng); - break; - default: - myGenerate(reinterpret_cast(srcVec[i].data()), srcVec[i].size(), randEng); - break; + } + else + { + myGenerate(srcVec[i].data(), srcVec[i].size(), randEng); } // Copy input data to the GPU @@ -551,6 +1264,88 @@ TEST_P(OpThreshold, varshape_correct_shape) EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); } +// clang-format off +NVCV_TEST_SUITE_P(OpThresholdPlanar, nvcv::test::ValueList +{ + // width, height, batch, type, thresh, maxval, planar format, interleaved format + { 64, 48, 2, NVCV_THRESH_BINARY, 100.0, 255.0, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + { 57, 41, 1, NVCV_THRESH_BINARY_INV, 96.0, 201.0, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + { 53, 39, 1, NVCV_THRESH_TRUNC, 100.0, 255.0, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + { 51, 37, 1, NVCV_THRESH_TOZERO, 100.0, 255.0, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + { 49, 35, 1, NVCV_THRESH_TOZERO_INV, 100.0, 255.0, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + { 47, 33, 1, NVCV_THRESH_BINARY_INV, -1.0, 201.0, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + { 45, 31, 1, NVCV_THRESH_TRUNC, 300.0, 255.0, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + { 43, 29, 1, NVCV_THRESH_TOZERO, -1.0, 255.0, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + { 41, 27, 1, NVCV_THRESH_TOZERO_INV, 300.0, 255.0, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + { 39, 25, 1, NVCV_THRESH_BINARY, 0.5, 1.0, nvcv::FMT_RGBf32p, nvcv::FMT_RGBf32}, + { 37, 23, 1, NVCV_THRESH_BINARY_INV, 0.5, 1.0, nvcv::FMT_RGBf32p, nvcv::FMT_RGBf32}, + { 35, 31, 2, NVCV_THRESH_TRUNC, 0.5, 1.0, nvcv::FMT_RGBf32p, nvcv::FMT_RGBf32}, + { 31, 21, 1, NVCV_THRESH_TOZERO, 0.5, 1.0, nvcv::FMT_RGBAf32p, nvcv::FMT_RGBAf32}, + { 33, 29, 1, NVCV_THRESH_TOZERO_INV, 0.5, 1.0, nvcv::FMT_RGBAf32p, nvcv::FMT_RGBAf32}, +}); + +// clang-format on + +TEST_P(OpThresholdPlanar, tensor_matches_interleaved) +{ + const int width = GetParamValue<0>(); + const int height = GetParamValue<1>(); + const int numImages = GetParamValue<2>(); + const uint32_t type = GetParamValue<3>(); + const double thresh = GetParamValue<4>(); + const double maxval = GetParamValue<5>(); + const nvcv::ImageFormat planarFmt = GetParamValue<6>(); + const nvcv::ImageFormat interleavedFmt = GetParamValue<7>(); + + nvcv::test::planar::RunTensorParity(planarFmt, interleavedFmt, width, height, width, height, numImages, + [numImages, type, thresh, maxval](cudaStream_t s, const nvcv::Tensor &src, + const nvcv::Tensor &dst, nvcv::ImageFormat) + { + auto threshval = MakeThresholdParam(numImages, thresh); + auto maxvalval = MakeThresholdParam(numImages, maxval); + + cvcuda::Threshold thresholdOp(type, numImages); + EXPECT_NO_THROW(thresholdOp(s, src, dst, threshval, maxvalval)); + }); +} + +TEST_P(OpThresholdPlanar, varshape_matches_interleaved) +{ + const int width = GetParamValue<0>(); + const int height = GetParamValue<1>(); + const int numImages = GetParamValue<2>(); + const uint32_t type = GetParamValue<3>(); + const double thresh = GetParamValue<4>(); + const double maxval = GetParamValue<5>(); + const nvcv::ImageFormat planarFmt = GetParamValue<6>(); + const nvcv::ImageFormat interleavedFmt = GetParamValue<7>(); + + nvcv::test::planar::RunVarShapeParity( + planarFmt, interleavedFmt, width, height, width, height, numImages, + [numImages, type, thresh, maxval](cudaStream_t s, const nvcv::ImageBatchVarShape &src, + const nvcv::ImageBatchVarShape &dst, nvcv::ImageFormat) + { + auto threshval = MakeThresholdParam(numImages, thresh); + auto maxvalval = MakeThresholdParam(numImages, maxval); + + cvcuda::Threshold thresholdOp(type, numImages); + EXPECT_NO_THROW(thresholdOp(s, src, dst, threshval, maxvalval)); + }); +} + +TEST(OpThresholdPlanar, tensor_rejects_two_channel) +{ + nvcv::test::planar::ExpectPlanarTensorRejected({1, 2, 16, 16}, {1, 2, 16, 16}, + [](cudaStream_t s, const nvcv::Tensor &src, const nvcv::Tensor &dst) + { + auto threshval = MakeThresholdParam(1, 100.0); + auto maxvalval = MakeThresholdParam(1, 255.0); + + cvcuda::Threshold thresholdOp(NVCV_THRESH_BINARY, 1); + thresholdOp(s, src, dst, threshval, maxvalval); + }); +} + // clang-format off NVCV_TEST_SUITE_P(OpThreshold_Negative, nvcv::test::ValueList { @@ -561,6 +1356,9 @@ NVCV_TEST_SUITE_P(OpThreshold_Negative, nvcv::test::ValueList(); + auto maxvalData = maxvalval.exportData(); + + ASSERT_NE(nullptr, threshData); + ASSERT_NE(nullptr, maxvalData); + + std::vector threshVec(batch, 100); + std::vector maxvalVec(batch, 255); + + ASSERT_EQ(cudaSuccess, cudaMemcpyAsync(threshData->basePtr(), threshVec.data(), threshVec.size() * sizeof(double), + cudaMemcpyHostToDevice, stream)); + ASSERT_EQ(cudaSuccess, cudaMemcpyAsync(maxvalData->basePtr(), maxvalVec.data(), maxvalVec.size() * sizeof(double), + cudaMemcpyHostToDevice, stream)); + + cvcuda::Threshold thresholdOp(type, maxBatch); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&thresholdOp, &stream, &imgIn, &imgOut, &threshval, &maxvalval] + { thresholdOp(stream, imgIn, imgOut, threshval, maxvalval); })); + + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +static void ExpectAutoThresholdVarShapeBatchExceedsMaxBatch(uint32_t type) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + constexpr int batch = 2; + constexpr int maxBatch = 1; + constexpr int width = 4; + constexpr int height = 4; + + std::vector imgSrc; + + std::vector imgDst; + for (int i = 0; i < batch; ++i) + { + imgSrc.emplace_back(nvcv::Size2D{width, height}, nvcv::FMT_U8); + imgDst.emplace_back(nvcv::Size2D{width, height}, nvcv::FMT_U8); + } + + nvcv::ImageBatchVarShape batchSrc(batch); + batchSrc.pushBack(imgSrc.begin(), imgSrc.end()); + + nvcv::ImageBatchVarShape batchDst(batch); + batchDst.pushBack(imgDst.begin(), imgDst.end()); + + nvcv::Tensor threshval({{batch}, "N"}, nvcv::TYPE_F64); + nvcv::Tensor maxvalval({{batch}, "N"}, nvcv::TYPE_F64); + + auto threshData = threshval.exportData(); + auto maxvalData = maxvalval.exportData(); + + ASSERT_NE(nullptr, threshData); + ASSERT_NE(nullptr, maxvalData); + + std::vector threshVec(batch, 100); + std::vector maxvalVec(batch, 255); + + ASSERT_EQ(cudaSuccess, cudaMemcpyAsync(threshData->basePtr(), threshVec.data(), threshVec.size() * sizeof(double), + cudaMemcpyHostToDevice, stream)); + ASSERT_EQ(cudaSuccess, cudaMemcpyAsync(maxvalData->basePtr(), maxvalVec.data(), maxvalVec.size() * sizeof(double), + cudaMemcpyHostToDevice, stream)); + + cvcuda::Threshold thresholdOp(type, maxBatch); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&thresholdOp, &stream, &batchSrc, &batchDst, &threshval, &maxvalval] + { thresholdOp(stream, batchSrc, batchDst, threshval, maxvalval); })); + + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpThreshold_Negative, tensor_otsu_batch_exceeds_maxBatch) +{ + ExpectAutoThresholdTensorBatchExceedsMaxBatch(NVCV_THRESH_OTSU | NVCV_THRESH_BINARY); +} + +TEST(OpThreshold_Negative, tensor_triangle_batch_exceeds_maxBatch) +{ + ExpectAutoThresholdTensorBatchExceedsMaxBatch(NVCV_THRESH_TRIANGLE | NVCV_THRESH_BINARY); +} + +TEST(OpThreshold_Negative, varshape_otsu_batch_exceeds_maxBatch) +{ + ExpectAutoThresholdVarShapeBatchExceedsMaxBatch(NVCV_THRESH_OTSU | NVCV_THRESH_BINARY); +} + +TEST(OpThreshold_Negative, varshape_triangle_batch_exceeds_maxBatch) +{ + ExpectAutoThresholdVarShapeBatchExceedsMaxBatch(NVCV_THRESH_TRIANGLE | NVCV_THRESH_BINARY); +} + TEST_P(OpThreshold_Negative, invalid_inputs) { cudaStream_t stream; @@ -654,11 +1560,13 @@ TEST_P(OpThreshold_Negative, varshape_invalid_inputs) nvcv::DataType maxvalDataType = GetParamValue<11>(); // Create input and output - std::default_random_engine randEng; - std::uniform_int_distribution rndWidth(width * 0.8, width * 1.1); - std::uniform_int_distribution rndHeight(height * 0.8, height * 1.1); + std::default_random_engine randEng; + std::uniform_int_distribution rndWidth(ScaledSize(width, 0.8), ScaledSize(width, 1.1)); + std::uniform_int_distribution rndHeight(ScaledSize(height, 0.8), ScaledSize(height, 1.1)); - std::vector imgSrc, imgDst; + std::vector imgSrc; + + std::vector imgDst; for (int i = 0; i < batch; ++i) { int rw = rndWidth(randEng); @@ -719,7 +1627,7 @@ TEST(OpThreshold, otsu_corner_cases) nvcv::ImageFormat fmt = nvcv::FMT_U8; NVCVDataType nvcvDataType; - ASSERT_EQ(NVCV_SUCCESS, nvcvImageFormatGetPlaneDataType(fmt, 0, &nvcvDataType)); + ASSERT_EQ(NVCV_SUCCESS, nvcvImageFormatGetPlaneDataType(static_cast(fmt), 0, &nvcvDataType)); nvcv::Tensor imgIn = nvcv::util::CreateTensor(batch, width, height, fmt); nvcv::Tensor imgOut = nvcv::util::CreateTensor(batch, width, height, fmt); @@ -783,11 +1691,11 @@ TEST(OpThreshold, otsu_corner_cases) } for (size_t j = 0; j < srcVec[1].size(); ++j) { - srcVec[1][j] = j % 256; + srcVec[1][j] = static_cast(j % 256); } for (size_t j = 0; j < srcVec[2].size(); ++j) { - srcVec[2][j] = (j % 4) * 64; + srcVec[2][j] = static_cast((j % 4) * 64); } for (int i = 0; i < batch; i++) @@ -835,14 +1743,16 @@ TEST(OpThreshold, otsu_corner_cases_varshape) nvcv::ImageFormat fmt = nvcv::FMT_U8; NVCVDataType nvcvDataType; - ASSERT_EQ(NVCV_SUCCESS, nvcvImageFormatGetPlaneDataType(fmt, 0, &nvcvDataType)); + ASSERT_EQ(NVCV_SUCCESS, nvcvImageFormatGetPlaneDataType(static_cast(fmt), 0, &nvcvDataType)); // Create input and output - std::default_random_engine randEng; - std::uniform_int_distribution rndWidth(width * 0.8, width * 1.1); - std::uniform_int_distribution rndHeight(height * 0.8, height * 1.1); + std::default_random_engine randEng; + std::uniform_int_distribution rndWidth(ScaledSize(width, 0.8), ScaledSize(width, 1.1)); + std::uniform_int_distribution rndHeight(ScaledSize(height, 0.8), ScaledSize(height, 1.1)); + + std::vector imgSrc; - std::vector imgSrc, imgDst; + std::vector imgDst; for (int i = 0; i < batch; ++i) { int rw = rndWidth(randEng); @@ -898,12 +1808,12 @@ TEST(OpThreshold, otsu_corner_cases_varshape) for (size_t j = 0; j < srcVec[1].size(); ++j) { - srcVec[1][j] = j % 256; + srcVec[1][j] = static_cast(j % 256); } for (size_t j = 0; j < srcVec[2].size(); ++j) { - srcVec[2][j] = (j % 4) * 64; + srcVec[2][j] = static_cast((j % 4) * 64); } for (int i = 0; i < batch; i++) @@ -967,7 +1877,7 @@ TEST(OpThreshold, triangle_corner_cases) nvcv::ImageFormat fmt = nvcv::FMT_U8; NVCVDataType nvcvDataType; - ASSERT_EQ(NVCV_SUCCESS, nvcvImageFormatGetPlaneDataType(fmt, 0, &nvcvDataType)); + ASSERT_EQ(NVCV_SUCCESS, nvcvImageFormatGetPlaneDataType(static_cast(fmt), 0, &nvcvDataType)); nvcv::Tensor imgIn = nvcv::util::CreateTensor(batch, width, height, fmt); nvcv::Tensor imgOut = nvcv::util::CreateTensor(batch, width, height, fmt); @@ -1027,12 +1937,12 @@ TEST(OpThreshold, triangle_corner_cases) for (size_t j = 0; j < srcVec[0].size(); j++) { - srcVec[0][j] = (j % 191) + 10; + srcVec[0][j] = static_cast((j % 191) + 10); } for (size_t j = 0; j < srcVec[1].size(); j++) { - srcVec[1][j] = j % 201; + srcVec[1][j] = static_cast(j % 201); } for (size_t j = 0; j < srcVec[2].size(); j++) diff --git a/tests/cvcuda/system/TestOpWarpAffine.cpp b/tests/cvcuda/system/TestOpWarpAffine.cpp index bceeb16c6..575bdef62 100644 --- a/tests/cvcuda/system/TestOpWarpAffine.cpp +++ b/tests/cvcuda/system/TestOpWarpAffine.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,6 +16,7 @@ */ #include "Definitions.hpp" +#include "PlanarParityUtils.hpp" #include #include @@ -26,6 +27,7 @@ #include #include +#include #include #include @@ -36,6 +38,12 @@ using namespace test; //#define DBG 1 +template +static float ToFloat(T value) +{ + return static_cast(value); +} + template static T getPixel(const T *srcPtr, const int y, const int x, int k, int width, int height, int srcStride, int elementsPerPixel, NVCVBorderType borderMode, const float4 borderVal) @@ -93,13 +101,110 @@ inline float calcBicubicCoeff(float x_) static void invertAffineTransform(const NVCVAffineTransform xform, NVCVAffineTransform inverseXform) { float den = xform[0] * xform[4] - xform[1] * xform[3]; - den = std::abs(den) > 1e-5 ? 1. / den : .0; - inverseXform[0] = (float)xform[4] * den; - inverseXform[1] = (float)-xform[1] * den; - inverseXform[2] = (float)(xform[1] * xform[5] - xform[4] * xform[2]) * den; - inverseXform[3] = (float)-xform[3] * den; - inverseXform[4] = (float)xform[0] * den; - inverseXform[5] = (float)(xform[3] * xform[2] - xform[0] * xform[5]) * den; + den = std::abs(den) > 1e-5f ? 1.0f / den : 0.0f; + inverseXform[0] = xform[4] * den; + inverseXform[1] = -xform[1] * den; + inverseXform[2] = (xform[1] * xform[5] - xform[4] * xform[2]) * den; + inverseXform[3] = -xform[3] * den; + inverseXform[4] = xform[0] * den; + inverseXform[5] = (xform[3] * xform[2] - xform[0] * xform[5]) * den; +} + +template +inline T clampU8(float value) +{ + value = std::rint(value); + if (value < 0.0f) + { + return 0; + } + if (value > 255.0f) + { + return 255; + } + return static_cast(value); +} + +template +static void StoreLinearPixel(T *dstPtr, int dstBase, const T *srcPtr, float src_x, float src_y, int srcWidth, + int srcHeight, int srcStride, int elementsPerPixel, NVCVBorderType borderMode, + const float4 borderVal) +{ + const auto x1 = static_cast(std::floor(src_x)); + const auto y1 = static_cast(std::floor(src_y)); + + const int x2 = x1 + 1; + const int y2 = y1 + 1; + + for (int k = 0; k < elementsPerPixel; k++) + { + float out = 0; + + T src_reg + = getPixel(srcPtr, y1, x1, k, srcWidth, srcHeight, srcStride, elementsPerPixel, borderMode, borderVal); + out += ToFloat(src_reg) * ((ToFloat(x2) - src_x) * (ToFloat(y2) - src_y)); + + src_reg + = getPixel(srcPtr, y1, x2, k, srcWidth, srcHeight, srcStride, elementsPerPixel, borderMode, borderVal); + out = out + ToFloat(src_reg) * ((src_x - ToFloat(x1)) * (ToFloat(y2) - src_y)); + + src_reg + = getPixel(srcPtr, y2, x1, k, srcWidth, srcHeight, srcStride, elementsPerPixel, borderMode, borderVal); + out = out + ToFloat(src_reg) * ((ToFloat(x2) - src_x) * (src_y - ToFloat(y1))); + + src_reg + = getPixel(srcPtr, y2, x2, k, srcWidth, srcHeight, srcStride, elementsPerPixel, borderMode, borderVal); + out = out + ToFloat(src_reg) * ((src_x - ToFloat(x1)) * (src_y - ToFloat(y1))); + + dstPtr[dstBase + k] = clampU8(out); + } +} + +template +static void StoreNearestPixel(T *dstPtr, int dstBase, const T *srcPtr, float src_x, float src_y, int srcWidth, + int srcHeight, int srcStride, int elementsPerPixel, NVCVBorderType borderMode, + const float4 borderVal) +{ + const auto x1 = static_cast(std::floor(src_x + .5f)); + const auto y1 = static_cast(std::floor(src_y + .5f)); + + for (int k = 0; k < elementsPerPixel; k++) + { + dstPtr[dstBase + k] + = getPixel(srcPtr, y1, x1, k, srcWidth, srcHeight, srcStride, elementsPerPixel, borderMode, borderVal); + } +} + +template +static void StoreCubicPixel(T *dstPtr, int dstBase, const T *srcPtr, float src_x, float src_y, int srcWidth, + int srcHeight, int srcStride, int elementsPerPixel, NVCVBorderType borderMode, + const float4 borderVal) +{ + const auto xmin = static_cast(std::ceil(src_x - 2.0f)); + const auto xmax = static_cast(std::floor(src_x + 2.0f)); + + const auto ymin = static_cast(std::ceil(src_y - 2.0f)); + const auto ymax = static_cast(std::floor(src_y + 2.0f)); + + for (int k = 0; k < elementsPerPixel; k++) + { + float sum = 0; + float wsum = 0; + + for (int cy = ymin; cy <= ymax; cy += 1) + { + for (int cx = xmin; cx <= xmax; cx += 1) + { + const float w = calcBicubicCoeff(src_x - ToFloat(cx)) * calcBicubicCoeff(src_y - ToFloat(cy)); + T src_reg = getPixel(srcPtr, cy, cx, k, srcWidth, srcHeight, srcStride, elementsPerPixel, borderMode, + borderVal); + sum += w * ToFloat(src_reg); + wsum += w; + } + } + + dstPtr[dstBase + k] = clampU8(wsum == 0.0f ? 0.0f : sum / wsum); + } } template @@ -138,81 +243,23 @@ static void WarpAffineGold(std::vector &hDst, int dstStride, nvcv::Size { for (int dst_x = 0; dst_x < dstSize.w; dst_x++) { - float src_x = (float)(dst_x * xform1[0] + dst_y * xform1[1] + xform1[2]); - float src_y = (float)(dst_x * xform1[3] + dst_y * xform1[4] + xform1[5]); + auto src_x = ToFloat(dst_x) * xform1[0] + ToFloat(dst_y) * xform1[1] + xform1[2]; + auto src_y = ToFloat(dst_x) * xform1[3] + ToFloat(dst_y) * xform1[4] + xform1[5]; if (interpolation == NVCV_INTERP_LINEAR) { - const int x1 = std::floor(src_x); - const int y1 = std::floor(src_y); - - const int x2 = x1 + 1; - const int y2 = y1 + 1; - - for (int k = 0; k < elementsPerPixel; k++) - { - float out = 0; - - T src_reg = getPixel(srcPtr, y1, x1, k, srcWidth, srcHeight, srcStride, elementsPerPixel, - borderMode, borderVal); - out += src_reg * ((x2 - src_x) * (y2 - src_y)); - - src_reg = getPixel(srcPtr, y1, x2, k, srcWidth, srcHeight, srcStride, elementsPerPixel, - borderMode, borderVal); - out = out + src_reg * ((src_x - x1) * (y2 - src_y)); - - src_reg = getPixel(srcPtr, y2, x1, k, srcWidth, srcHeight, srcStride, elementsPerPixel, - borderMode, borderVal); - out = out + src_reg * ((x2 - src_x) * (src_y - y1)); - - src_reg = getPixel(srcPtr, y2, x2, k, srcWidth, srcHeight, srcStride, elementsPerPixel, - borderMode, borderVal); - out = out + src_reg * ((src_x - x1) * (src_y - y1)); - - out = std::rint(out); - dstPtr[dst_y * dstStride + dst_x * elementsPerPixel + k] = out < 0 ? 0 : (out > 255 ? 255 : out); - } + StoreLinearPixel(dstPtr, dst_y * dstStride + dst_x * elementsPerPixel, srcPtr, src_x, src_y, srcWidth, + srcHeight, srcStride, elementsPerPixel, borderMode, borderVal); } else if (interpolation == NVCV_INTERP_NEAREST) { - const int x1 = std::floor(src_x + .5f); - const int y1 = std::floor(src_y + .5f); - for (int k = 0; k < elementsPerPixel; k++) - { - T src_reg = getPixel(srcPtr, y1, x1, k, srcWidth, srcHeight, srcStride, elementsPerPixel, - borderMode, borderVal); - dstPtr[dst_y * dstStride + dst_x * elementsPerPixel + k] = src_reg; - } + StoreNearestPixel(dstPtr, dst_y * dstStride + dst_x * elementsPerPixel, srcPtr, src_x, src_y, srcWidth, + srcHeight, srcStride, elementsPerPixel, borderMode, borderVal); } else if (interpolation == NVCV_INTERP_CUBIC) { - const int xmin = std::ceil(src_x - 2.0f); - const int xmax = std::floor(src_x + 2.0f); - - const int ymin = std::ceil(src_y - 2.0f); - const int ymax = std::floor(src_y + 2.0f); - - for (int k = 0; k < elementsPerPixel; k++) - { - float sum = 0; - float wsum = 0; - - for (int cy = ymin; cy <= ymax; cy += 1) - { - for (int cx = xmin; cx <= xmax; cx += 1) - { - const float w = calcBicubicCoeff(src_x - cx) * calcBicubicCoeff(src_y - cy); - T src_reg = getPixel(srcPtr, cy, cx, k, srcWidth, srcHeight, srcStride, elementsPerPixel, - borderMode, borderVal); - sum += w * src_reg; - wsum += w; - } - } - - float res = (!wsum) ? 0 : sum / wsum; - res = std::rint(res); - dstPtr[dst_y * dstStride + dst_x * elementsPerPixel + k] = res < 0 ? 0 : (res > 255 ? 255 : res); - } + StoreCubicPixel(dstPtr, dst_y * dstStride + dst_x * elementsPerPixel, srcPtr, src_x, src_y, srcWidth, + srcHeight, srcStride, elementsPerPixel, borderMode, borderVal); } else { @@ -222,10 +269,22 @@ static void WarpAffineGold(std::vector &hDst, int dstStride, nvcv::Size } } -static std::map, std::vector>> mapOfTransformationMatrix = { - {{5, 4, 5, 4}, {{1, 0, 0, 0, 1, 0}, {1, 0, 1, 0, 1, 2}, {1, 2, 1, 2, 1, 2}, {0.5, 2, 1, 0.75, 1, 2}}}, - {{5, 4, 6, 8}, {{1, 0, 0, 0, 1, 0}, {1, 0, 1, 0, 1, 2}, {1, 2, 1, 2, 1, 2}, {0.5, 2, 1, 0.75, 1, 2}}}, - {{7, 8, 4, 5}, {{1, 0, 0, 0, 1, 0}, {1, 0, 1, 0, 1, 2}, {1, 2, 1, 2, 1, 2}, {0.5, 2, 1, 0.75, 1, 2}}} +static const std::map, std::vector>> mapOfTransformationMatrix = { + {{5, 4, 5, 4}, + {{1.f, 0.f, 0.f, 0.f, 1.f, 0.f}, + {1.f, 0.f, 1.f, 0.f, 1.f, 2.f}, + {1.f, 2.f, 1.f, 2.f, 1.f, 2.f}, + {0.5f, 2.f, 1.f, 0.75f, 1.f, 2.f}}}, + {{5, 4, 6, 8}, + {{1.f, 0.f, 0.f, 0.f, 1.f, 0.f}, + {1.f, 0.f, 1.f, 0.f, 1.f, 2.f}, + {1.f, 2.f, 1.f, 2.f, 1.f, 2.f}, + {0.5f, 2.f, 1.f, 0.75f, 1.f, 2.f}}}, + {{7, 8, 4, 5}, + {{1.f, 0.f, 0.f, 0.f, 1.f, 0.f}, + {1.f, 0.f, 1.f, 0.f, 1.f, 2.f}, + {1.f, 2.f, 1.f, 2.f, 1.f, 2.f}, + {0.5f, 2.f, 1.f, 0.75f, 1.f, 2.f}}} }; // clang-format off @@ -364,10 +423,13 @@ TEST_P(OpWarpAffine, tensor_correct_output) for (int i = 0; i < numberOfImages; ++i) { - std::uniform_int_distribution rand(0, 255); + std::uniform_int_distribution rand(0, 255); srcVec[i].resize(srcHeight * srcVecStride); - std::generate(srcVec[i].begin(), srcVec[i].end(), [&]() { return rand(randEng); }); + for (uint8_t &value : srcVec[i]) + { + value = static_cast(rand(randEng)); + } // Copy input data to the GPU ASSERT_EQ(cudaSuccess, @@ -489,17 +551,18 @@ TEST_P(OpWarpAffine, varshape_correct_output) ASSERT_TRUE(transMatrixTensorDataAccess); // Create input and output - std::default_random_engine randEng; - std::uniform_int_distribution rndInputDimsIndex(0, mapOfTransformationMatrix.size() - 1); - std::uniform_int_distribution rndTransformationMatrixIndex(0, 3); + std::default_random_engine randEng; + std::uniform_int_distribution rndInputDimsIndex(0, static_cast(mapOfTransformationMatrix.size() - 1)); + std::uniform_int_distribution rndTransformationMatrixIndex(0, 3); - std::vector imgSrc, imgDst; + std::vector imgSrc; + std::vector imgDst; std::vector> transMatrixHostVec; transMatrixHostVec.resize(numberOfImages); // List the keys from the map for easy access std::vector> keysOfMapOfTransformationMatrix; - for (auto &[key, value] : mapOfTransformationMatrix) + for (const auto &[key, value] : mapOfTransformationMatrix) { keysOfMapOfTransformationMatrix.push_back(key); } @@ -518,7 +581,7 @@ TEST_P(OpWarpAffine, varshape_correct_output) int dictTransformationIndex = rndTransformationMatrixIndex(randEng); std::vector key = keysOfMapOfTransformationMatrix[dictInputIndex]; - std::vector chosenTransformationMatrix = mapOfTransformationMatrix[key][dictTransformationIndex]; + std::vector chosenTransformationMatrix = mapOfTransformationMatrix.at(key)[dictTransformationIndex]; // Legacy Reflect & Reflect101 has a bug. So, do special thing for them if (i > 0 && !(borderMode == NVCV_BORDER_REFLECT || borderMode == NVCV_BORDER_REFLECT101)) { @@ -558,23 +621,26 @@ TEST_P(OpWarpAffine, varshape_correct_output) const auto srcData = imgSrc[i].exportData(); assert(srcData->numPlanes() == 1); - int srcWidth = srcData->plane(0).width; - int srcHeight = srcData->plane(0).height; + int sampleSrcWidth = srcData->plane(0).width; + int sampleSrcHeight = srcData->plane(0).height; - int srcStride = srcWidth * fmt.planePixelStrideBytes(0); + int srcStride = sampleSrcWidth * fmt.planePixelStrideBytes(0); srcVecStride[i] = srcStride; - std::uniform_int_distribution rand(0, 255); + std::uniform_int_distribution rand(0, 255); - srcVec[i].resize(srcHeight * srcStride); - std::generate(srcVec[i].begin(), srcVec[i].end(), [&]() { return rand(randEng); }); + srcVec[i].resize(sampleSrcHeight * srcStride); + for (uint8_t &value : srcVec[i]) + { + value = static_cast(rand(randEng)); + } // Copy input data to the GPU ASSERT_EQ(cudaSuccess, cudaMemcpy2D(srcData->plane(0).basePtr, srcData->plane(0).rowStride, srcVec[i].data(), srcStride, srcStride, // vec has no padding - srcHeight, cudaMemcpyHostToDevice)); + sampleSrcHeight, cudaMemcpyHostToDevice)); } // Generate test result @@ -592,28 +658,27 @@ TEST_P(OpWarpAffine, varshape_correct_output) const auto srcData = imgSrc[i].exportData(); assert(srcData->numPlanes() == 1); - int srcWidth = srcData->plane(0).width; - int srcHeight = srcData->plane(0).height; + int sampleSrcWidth = srcData->plane(0).width; + int sampleSrcHeight = srcData->plane(0).height; const auto dstData = imgDst[i].exportData(); assert(dstData->numPlanes() == 1); - int dstWidth = dstData->plane(0).width; - int dstHeight = dstData->plane(0).height; + int sampleDstWidth = dstData->plane(0).width; + int sampleDstHeight = dstData->plane(0).height; - int srcStride = srcWidth * fmt.planePixelStrideBytes(0); - int dstStride = dstWidth * fmt.planePixelStrideBytes(0); + int srcStride = sampleSrcWidth * fmt.planePixelStrideBytes(0); + int dstStride = sampleDstWidth * fmt.planePixelStrideBytes(0); - std::vector testVec(dstHeight * dstStride); + std::vector testVec(sampleDstHeight * dstStride); // Copy output data to Host ASSERT_EQ(cudaSuccess, cudaMemcpy2D(testVec.data(), dstStride, dstData->plane(0).basePtr, dstData->plane(0).rowStride, dstStride, // vec has no padding - dstHeight, cudaMemcpyDeviceToHost)); + sampleDstHeight, cudaMemcpyDeviceToHost)); - std::vector goldVec(dstHeight * dstStride); - std::generate(goldVec.begin(), goldVec.end(), [&]() { return 0; }); + std::vector goldVec(sampleDstHeight * dstStride); NVCVAffineTransform transMatrixForGold; transMatrixForGold[0] = transMatrixHostVec[i][0]; @@ -624,8 +689,9 @@ TEST_P(OpWarpAffine, varshape_correct_output) transMatrixForGold[5] = transMatrixHostVec[i][5]; // Generate gold result - WarpAffineGold(goldVec, dstStride, {dstWidth, dstHeight}, srcVec[i], srcStride, {srcWidth, srcHeight}, - fmt, transMatrixForGold, flags, borderMode, borderValue); + WarpAffineGold(goldVec, dstStride, {sampleDstWidth, sampleDstHeight}, srcVec[i], srcStride, + {sampleSrcWidth, sampleSrcHeight}, fmt, transMatrixForGold, flags, borderMode, + borderValue); #if DBG std::cout << "\nPrint src vec " << std::endl; @@ -665,12 +731,113 @@ TEST_P(OpWarpAffine, varshape_correct_output) } } +// ============================================================================= +// Planar (NCHW/CHW) layout support +// +// Warp samples every channel at the same transformed coordinate, so a planar input is warped +// plane-by-plane and must produce exactly the same pixels as the interleaved path. These tests feed +// identical data and transform through cvcuda::WarpAffine in both layouts and require the +// (re-interleaved) planar output to match the interleaved output bit-for-bit, across interpolation +// and border modes. CONSTANT border uses a NON-UNIFORM borderValue to exercise the per-channel +// border path that planar must reproduce. Shared scaffolding lives in PlanarParityUtils.hpp. +// ============================================================================= + +namespace { + +// Non-trivial affine (scale + shear + translate). Any transform works since the parity check only +// requires interleaved and planar to match on identical inputs. +inline std::array PlanarAffine() +{ + return {1.1f, 0.05f, 3.0f, -0.03f, 0.95f, 2.0f}; +} + +void RunPlanarParityTensorCase(nvcv::ImageFormat planarFmt, nvcv::ImageFormat interleavedFmt, int srcW, int srcH, + int dstW, int dstH, NVCVInterpolationType interp, NVCVBorderType borderMode, + int numImages) +{ + const std::array xform = PlanarAffine(); + const float4 borderValue = {13.f, 57.f, 101.f, 211.f}; + const int32_t flags = interp; + test::planar::RunTensorParity( + planarFmt, interleavedFmt, srcW, srcH, dstW, dstH, numImages, + [xform, flags, borderMode, borderValue, numImages](cudaStream_t s, const nvcv::Tensor &src, + const nvcv::Tensor &dst, nvcv::ImageFormat) + { + cvcuda::WarpAffine op(numImages); + EXPECT_NO_THROW(op(s, src, dst, xform.data(), flags, borderMode, borderValue)); + }); +} + +void RunPlanarParityVarShapeCase(nvcv::ImageFormat planarFmt, nvcv::ImageFormat interleavedFmt, int srcW, int srcH, + int dstW, int dstH, NVCVInterpolationType interp, NVCVBorderType borderMode, + int numImages) +{ + const std::array xform = PlanarAffine(); + const float4 borderValue = {13.f, 57.f, 101.f, 211.f}; + const int32_t flags = interp; + + // Per-image transform tensor (same affine for every image); upload synchronously so it is ready + // before the operator runs on the parity helper's stream. + nvcv::Tensor transMatrix(nvcv::TensorShape({numImages, 6}, nvcv::TENSOR_NW), nvcv::TYPE_F32); + { + auto data = transMatrix.exportData(); + ASSERT_NE(data, nullptr); + auto acc = nvcv::TensorDataAccessStrided::Create(*data); + ASSERT_TRUE(acc); + for (int i = 0; i < numImages; ++i) + { + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(acc->sampleData(i), acc->sampleStride(), xform.data(), + sizeof(float) * 6, sizeof(float) * 6, 1, cudaMemcpyHostToDevice)); + } + } + + test::planar::RunVarShapeParity( + planarFmt, interleavedFmt, srcW, srcH, dstW, dstH, numImages, + [&transMatrix, flags, borderMode, borderValue, numImages]( + cudaStream_t s, const nvcv::ImageBatchVarShape &src, const nvcv::ImageBatchVarShape &dst, nvcv::ImageFormat) + { + cvcuda::WarpAffine op(numImages); + EXPECT_NO_THROW(op(s, src, dst, transMatrix, flags, borderMode, borderValue)); + }); +} + +} // namespace + +// Parameters: srcW, srcH, dstW, dstH, interpolation, borderMode, numImages, planarFmt, interleavedFmt +// clang-format off +NVCV_TEST_SUITE_P(OpWarpAffinePlanar, + test::ValueList{ + { 64, 48, 64, 48, NVCV_INTERP_NEAREST, NVCV_BORDER_CONSTANT, 2, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + { 64, 48, 96, 72, NVCV_INTERP_LINEAR, NVCV_BORDER_CONSTANT, 2, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + { 80, 60, 64, 48, NVCV_INTERP_CUBIC, NVCV_BORDER_REPLICATE, 1, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + { 64, 48, 64, 48, NVCV_INTERP_LINEAR, NVCV_BORDER_WRAP, 1, nvcv::FMT_RGB8p, nvcv::FMT_RGB8}, + { 50, 40, 60, 50, NVCV_INTERP_NEAREST, NVCV_BORDER_CONSTANT, 2, nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8}, + { 50, 40, 50, 40, NVCV_INTERP_LINEAR, NVCV_BORDER_REPLICATE, 1, nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8}, + { 64, 48, 96, 72, NVCV_INTERP_CUBIC, NVCV_BORDER_CONSTANT, 1, nvcv::FMT_RGBf32p, nvcv::FMT_RGBf32}, + { 64, 48, 64, 48, NVCV_INTERP_LINEAR, NVCV_BORDER_CONSTANT, 2, nvcv::FMT_RGBAf32p, nvcv::FMT_RGBAf32}, +}); + +// clang-format on + +TEST_P(OpWarpAffinePlanar, tensor_matches_interleaved) +{ + RunPlanarParityTensorCase(GetParamValue<7>(), GetParamValue<8>(), GetParamValue<0>(), GetParamValue<1>(), + GetParamValue<2>(), GetParamValue<3>(), GetParamValue<4>(), GetParamValue<5>(), + GetParamValue<6>()); +} + +TEST_P(OpWarpAffinePlanar, varshape_matches_interleaved) +{ + RunPlanarParityVarShapeCase(GetParamValue<7>(), GetParamValue<8>(), GetParamValue<0>(), GetParamValue<1>(), + GetParamValue<2>(), GetParamValue<3>(), GetParamValue<4>(), GetParamValue<5>(), + GetParamValue<6>()); +} + // clang-format off NVCV_TEST_SUITE_P(OpWarpAffine_Negative, test::ValueList{ // input format, output format, - {nvcv::FMT_RGBA8, nvcv::FMT_RGBA8p}, - {nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8}, - {nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8p}, + {nvcv::FMT_RGBA8, nvcv::FMT_RGBA8p}, // interleaved in, planar out: layout mismatch + {nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8}, // planar in, interleaved out: layout mismatch {nvcv::FMT_RGBAf16, nvcv::FMT_RGBAf16} }); @@ -678,7 +845,6 @@ NVCV_TEST_SUITE_P(OpWarpAffineVarshape_Negative, test::ValueList imgSrc, imgDst; + std::vector imgSrc; + std::vector imgDst; for (int i = 0; i < numImages; ++i) { @@ -750,10 +917,10 @@ TEST_P(OpWarpAffineVarshape_Negative, op) batchDst.pushBack(imgDst.begin(), imgDst.end()); cvcuda::WarpAffine warpAffineOp(maxBatchSize); - EXPECT_EQ( - NVCV_ERROR_INVALID_ARGUMENT, - nvcv::ProtectCall( - [&] { warpAffineOp(stream, batchSrc, batchDst, transMatrixTensor, flags, borderMode, borderValue); })); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall( + [&warpAffineOp, &stream, &batchSrc, &batchDst, &transMatrixTensor, &flags, &borderMode, &borderValue] + { warpAffineOp(stream, batchSrc, batchDst, transMatrixTensor, flags, borderMode, borderValue); })); EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); @@ -764,6 +931,125 @@ TEST(OpWarpAffine_Negative, create_null_handle) EXPECT_EQ(cvcudaWarpAffineCreate(nullptr, 2), NVCV_ERROR_INVALID_ARGUMENT); } +TEST(OpWarpAffine_Negative, invalid_border_mode) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + NVCVAffineTransform xform = {1, 0, 0, 0, 1, 0}; + const float4 borderValue = {0, 0, 0, 0}; + const int flags = NVCV_INTERP_NEAREST | NVCV_WARP_INVERSE_MAP; + + nvcv::Tensor imgSrc(1, {4, 4}, nvcv::FMT_U8); + nvcv::Tensor imgDst(1, {4, 4}, nvcv::FMT_U8); + + cvcuda::WarpAffine op(0); + + // 5 is one past the last valid NVCVBorderType value (NVCV_BORDER_REFLECT101 = 4) + auto invalidBorder = static_cast(5); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&op, &stream, &imgSrc, &imgDst, &xform, &flags, &invalidBorder, &borderValue] + { op(stream, imgSrc, imgDst, xform, flags, invalidBorder, borderValue); })); + + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpWarpAffine_Negative, invalid_interpolation) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + NVCVAffineTransform xform = {1, 0, 0, 0, 1, 0}; + const float4 borderValue = {0, 0, 0, 0}; + + nvcv::Tensor imgSrc(1, {4, 4}, nvcv::FMT_U8); + nvcv::Tensor imgDst(1, {4, 4}, nvcv::FMT_U8); + + cvcuda::WarpAffine op(0); + + // NVCV_INTERP_AREA (3) is not supported by the warp ops + const int flags = static_cast(NVCV_INTERP_AREA) | NVCV_WARP_INVERSE_MAP; + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&op, &stream, &imgSrc, &imgDst, &xform, &flags, &borderValue] + { op(stream, imgSrc, imgDst, xform, flags, NVCV_BORDER_CONSTANT, borderValue); })); + + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpWarpAffineVarshape_Negative, invalid_border_mode) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + const int numImages = 2; + const float4 borderValue = {0, 0, 0, 0}; + const int flags = NVCV_INTERP_NEAREST | NVCV_WARP_INVERSE_MAP; + + nvcv::Tensor transMatrixTensor(nvcv::TensorShape({numImages, 6}, nvcv::TENSOR_NW), nvcv::TYPE_F32); + + std::vector imgSrc; + std::vector imgDst; + for (int i = 0; i < numImages; ++i) + { + imgSrc.emplace_back(nvcv::Size2D{4, 4}, nvcv::FMT_U8); + imgDst.emplace_back(nvcv::Size2D{4, 4}, nvcv::FMT_U8); + } + + nvcv::ImageBatchVarShape batchSrc(numImages); + nvcv::ImageBatchVarShape batchDst(numImages); + batchSrc.pushBack(imgSrc.begin(), imgSrc.end()); + batchDst.pushBack(imgDst.begin(), imgDst.end()); + + cvcuda::WarpAffine op(numImages); + + auto invalidBorder = static_cast(5); + EXPECT_EQ( + NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&op, &stream, &batchSrc, &batchDst, &transMatrixTensor, &flags, &invalidBorder, &borderValue] + { op(stream, batchSrc, batchDst, transMatrixTensor, flags, invalidBorder, borderValue); })); + + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpWarpAffineVarshape_Negative, invalid_interpolation) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + const int numImages = 2; + const float4 borderValue = {0, 0, 0, 0}; + + nvcv::Tensor transMatrixTensor(nvcv::TensorShape({numImages, 6}, nvcv::TENSOR_NW), nvcv::TYPE_F32); + + std::vector imgSrc; + std::vector imgDst; + for (int i = 0; i < numImages; ++i) + { + imgSrc.emplace_back(nvcv::Size2D{4, 4}, nvcv::FMT_U8); + imgDst.emplace_back(nvcv::Size2D{4, 4}, nvcv::FMT_U8); + } + + nvcv::ImageBatchVarShape batchSrc(numImages); + nvcv::ImageBatchVarShape batchDst(numImages); + batchSrc.pushBack(imgSrc.begin(), imgSrc.end()); + batchDst.pushBack(imgDst.begin(), imgDst.end()); + + cvcuda::WarpAffine op(numImages); + + // NVCV_INTERP_AREA (3) is not supported by the warp ops + const int flags = static_cast(NVCV_INTERP_AREA) | NVCV_WARP_INVERSE_MAP; + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall( + [&op, &stream, &batchSrc, &batchDst, &transMatrixTensor, &flags, &borderValue] + { op(stream, batchSrc, batchDst, transMatrixTensor, flags, NVCV_BORDER_CONSTANT, borderValue); })); + + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + TEST(OpWarpAffineVarshape_Negative, different_format_varshape) { std::vector> extraFmts{ @@ -771,11 +1057,8 @@ TEST(OpWarpAffineVarshape_Negative, different_format_varshape) {nvcv::FMT_RGBA8, nvcv::FMT_RGB8} }; - for (const auto &testCase : extraFmts) + for (const auto &[extraFmtSrc, extraFmtDst] : extraFmts) { - auto extraFmtSrc = testCase.first; - auto extraFmtDst = testCase.second; - cudaStream_t stream; EXPECT_EQ(cudaSuccess, cudaStreamCreate(&stream)); @@ -792,8 +1075,8 @@ TEST(OpWarpAffineVarshape_Negative, different_format_varshape) nvcv::Tensor transMatrixTensor(nvcv::TensorShape({numImages, 6}, nvcv::TENSOR_NW), nvcv::TYPE_F32); // Create input and output - std::default_random_engine randEng; - std::vector imgSrc, imgDst; + std::vector imgSrc; + std::vector imgDst; for (int i = 0; i < numImages - 1; ++i) { @@ -814,7 +1097,8 @@ TEST(OpWarpAffineVarshape_Negative, different_format_varshape) EXPECT_EQ( NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall( - [&] { warpAffineOp(stream, batchSrc, batchDst, transMatrixTensor, flags, borderMode, borderValue); })); + [&warpAffineOp, &stream, &batchSrc, &batchDst, &transMatrixTensor, &flags, &borderMode, &borderValue] + { warpAffineOp(stream, batchSrc, batchDst, transMatrixTensor, flags, borderMode, borderValue); })); EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); diff --git a/tests/cvcuda/system/TestOpWarpPerspective.cpp b/tests/cvcuda/system/TestOpWarpPerspective.cpp index fa00839df..cc898db60 100644 --- a/tests/cvcuda/system/TestOpWarpPerspective.cpp +++ b/tests/cvcuda/system/TestOpWarpPerspective.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,6 +16,7 @@ */ #include "Definitions.hpp" +#include "PlanarParityUtils.hpp" #include #include @@ -27,16 +28,20 @@ #include #include +#include #include #include #include +#include namespace cuda = nvcv::cuda; namespace test = nvcv::test; // #define DBG_WARP_PERSPECTIVE 1 -static void printVec(std::vector &vec, int height, int rowStride, int bytesPerPixel, std::string name) +static void printVec([[maybe_unused]] const std::vector &vec, [[maybe_unused]] int height, + [[maybe_unused]] int rowStride, [[maybe_unused]] int bytesPerPixel, + [[maybe_unused]] std::string_view name) { #if DBG_WARP_PERSPECTIVE for (int i = 0; i < bytesPerPixel; i++) @@ -56,36 +61,59 @@ static void printVec(std::vector &vec, int height, int rowStride, int b #endif } -static uint8_t getPixelForPerspectiveTransform(const uint8_t *srcPtr, const int y, const int x, int k, int width, - int height, int srcRowStride, int elementsPerPixel, - NVCVBorderType borderMode, const float4 borderVal) +struct PerspectiveSource { - int2 coord = {x, y}; - int2 size = {width, height}; - if (borderMode == NVCV_BORDER_CONSTANT) + const uint8_t *ptr; + nvcv::Size2D size; + int rowStride; + int elementsPerPixel; + NVCVBorderType borderMode; + float4 borderVal; +}; + +struct WarpPerspectiveGoldParams +{ + int dstRowStride; + nvcv::Size2D dstSize; + int srcRowStride; + nvcv::Size2D srcSize; + nvcv::ImageFormat fmt; + const NVCVPerspectiveTransform &transMatrix; + int flags; + NVCVBorderType borderMode; + float4 borderVal; +}; + +static uint8_t getPixelForPerspectiveTransform(const PerspectiveSource &src, const int y, const int x, int k) +{ + const int width = src.size.w; + const int height = src.size.h; + int2 coord = {x, y}; + int2 size = {width, height}; + if (src.borderMode == NVCV_BORDER_CONSTANT) { - return (x >= 0 && x < width && y >= 0 && y < height) ? srcPtr[y * srcRowStride + x * elementsPerPixel + k] - : static_cast(cuda::GetElement(borderVal, k)); + return (x >= 0 && x < width && y >= 0 && y < height) ? src.ptr[y * src.rowStride + x * src.elementsPerPixel + k] + : static_cast(cuda::GetElement(src.borderVal, k)); } - else if (borderMode == NVCV_BORDER_REPLICATE) + else if (src.borderMode == NVCV_BORDER_REPLICATE) { test::ReplicateBorderIndex(coord, size); - return srcPtr[coord.y * srcRowStride + coord.x * elementsPerPixel + k]; + return src.ptr[coord.y * src.rowStride + coord.x * src.elementsPerPixel + k]; } - else if (borderMode == NVCV_BORDER_REFLECT) + else if (src.borderMode == NVCV_BORDER_REFLECT) { test::ReflectBorderIndex(coord, size); - return srcPtr[coord.y * srcRowStride + coord.x * elementsPerPixel + k]; + return src.ptr[coord.y * src.rowStride + coord.x * src.elementsPerPixel + k]; } - else if (borderMode == NVCV_BORDER_REFLECT101) + else if (src.borderMode == NVCV_BORDER_REFLECT101) { test::Reflect101BorderIndex(coord, size); - return srcPtr[coord.y * srcRowStride + coord.x * elementsPerPixel + k]; + return src.ptr[coord.y * src.rowStride + coord.x * src.elementsPerPixel + k]; } - else if (borderMode == NVCV_BORDER_WRAP) + else if (src.borderMode == NVCV_BORDER_WRAP) { test::WrapBorderIndex(coord, size); - return srcPtr[coord.y * srcRowStride + coord.x * elementsPerPixel + k]; + return src.ptr[coord.y * src.rowStride + coord.x * src.elementsPerPixel + k]; } else { @@ -110,38 +138,115 @@ inline float calcBicubicCoeff(float x_) } } -static void WarpPerspectiveGold(std::vector &hDst, const int dstRowStride, const nvcv::Size2D dstSize, - const std::vector &hSrc, const int srcRowStride, const nvcv::Size2D srcSize, - const nvcv::ImageFormat fmt, const NVCVPerspectiveTransform transMatrix, - const int flags, const NVCVBorderType borderMode, const float4 borderVal) +inline uint8_t clampU8(float value) +{ + const float rounded = std::rint(value); + if (rounded < 0.0f) + { + return static_cast(0); + } + if (rounded > 255.0f) + { + return static_cast(255); + } + return static_cast(rounded); +} + +static void StoreLinearPixel(uint8_t *dstPtr, int dstBase, const PerspectiveSource &src, float src_x, float src_y) +{ + const auto x1 = static_cast(std::floor(src_x)); + const auto y1 = static_cast(std::floor(src_y)); + + const int x2 = x1 + 1; + const int y2 = y1 + 1; + + for (int k = 0; k < src.elementsPerPixel; k++) + { + float out = 0; + + uint8_t srcReg = getPixelForPerspectiveTransform(src, y1, x1, k); + out += static_cast(srcReg) * ((static_cast(x2) - src_x) * (static_cast(y2) - src_y)); + + srcReg = getPixelForPerspectiveTransform(src, y1, x2, k); + out = out + static_cast(srcReg) * ((src_x - static_cast(x1)) * (static_cast(y2) - src_y)); + + srcReg = getPixelForPerspectiveTransform(src, y2, x1, k); + out = out + static_cast(srcReg) * ((static_cast(x2) - src_x) * (src_y - static_cast(y1))); + + srcReg = getPixelForPerspectiveTransform(src, y2, x2, k); + out = out + static_cast(srcReg) * ((src_x - static_cast(x1)) * (src_y - static_cast(y1))); + + dstPtr[dstBase + k] = clampU8(out); + } +} + +static void StoreNearestPixel(uint8_t *dstPtr, int dstBase, const PerspectiveSource &src, float src_x, float src_y) { - assert(fmt.numPlanes() == 1); + const auto x1 = static_cast(std::floor(src_x + .5f)); + const auto y1 = static_cast(std::floor(src_y + .5f)); - int elementsPerPixel = fmt.numChannels(); + for (int k = 0; k < src.elementsPerPixel; k++) + { + dstPtr[dstBase + k] = getPixelForPerspectiveTransform(src, y1, x1, k); + } +} + +static void StoreCubicPixel(uint8_t *dstPtr, int dstBase, const PerspectiveSource &src, float src_x, float src_y) +{ + const auto xmin = static_cast(std::ceil(src_x - 2.0f)); + const auto xmax = static_cast(std::floor(src_x + 2.0f)); - uint8_t *dstPtr = hDst.data(); - const uint8_t *srcPtr = hSrc.data(); + const auto ymin = static_cast(std::ceil(src_y - 2.0f)); + const auto ymax = static_cast(std::floor(src_y + 2.0f)); + + for (int k = 0; k < src.elementsPerPixel; k++) + { + float sum = 0; + float wsum = 0; + + for (int cy = ymin; cy <= ymax; cy += 1) + { + for (int cx = xmin; cx <= xmax; cx += 1) + { + const float w = calcBicubicCoeff(src_x - static_cast(cx)) + * calcBicubicCoeff(src_y - static_cast(cy)); + uint8_t srcReg = getPixelForPerspectiveTransform(src, cy, cx, k); + sum += w * static_cast(srcReg); + wsum += w; + } + } + + dstPtr[dstBase + k] = clampU8(wsum == 0.0f ? 0.0f : sum / wsum); + } +} + +static void WarpPerspectiveGold(std::vector &hDst, const std::vector &hSrc, + const WarpPerspectiveGoldParams ¶ms) +{ + assert(params.fmt.numPlanes() == 1); - int srcWidth = srcSize.w; - int srcHeight = srcSize.h; + PerspectiveSource src{hSrc.data(), params.srcSize, params.srcRowStride, params.fmt.numChannels(), + params.borderMode, params.borderVal}; - const int interpolation = flags & NVCV_INTERP_MAX; + uint8_t *dstPtr = hDst.data(); + const int elementsPerPixel = src.elementsPerPixel; + const int interpolation = params.flags & NVCV_INTERP_MAX; NVCVPerspectiveTransform finalTransformMatrix; - if (!(flags & NVCV_WARP_INVERSE_MAP)) + if (!(params.flags & NVCV_WARP_INVERSE_MAP)) { cuda::math::Matrix tempMatrixForInverse; - tempMatrixForInverse[0][0] = (float)(transMatrix[0]); - tempMatrixForInverse[0][1] = (float)(transMatrix[1]); - tempMatrixForInverse[0][2] = (float)(transMatrix[2]); - tempMatrixForInverse[1][0] = (float)(transMatrix[3]); - tempMatrixForInverse[1][1] = (float)(transMatrix[4]); - tempMatrixForInverse[1][2] = (float)(transMatrix[5]); - tempMatrixForInverse[2][0] = (float)(transMatrix[6]); - tempMatrixForInverse[2][1] = (float)(transMatrix[7]); - tempMatrixForInverse[2][2] = (float)(transMatrix[8]); + tempMatrixForInverse[0][0] = params.transMatrix[0]; + tempMatrixForInverse[0][1] = params.transMatrix[1]; + tempMatrixForInverse[0][2] = params.transMatrix[2]; + tempMatrixForInverse[1][0] = params.transMatrix[3]; + tempMatrixForInverse[1][1] = params.transMatrix[4]; + tempMatrixForInverse[1][2] = params.transMatrix[5]; + tempMatrixForInverse[2][0] = params.transMatrix[6]; + tempMatrixForInverse[2][1] = params.transMatrix[7]; + tempMatrixForInverse[2][2] = params.transMatrix[8]; cuda::math::inv_inplace(tempMatrixForInverse); @@ -159,97 +264,34 @@ static void WarpPerspectiveGold(std::vector &hDst, const int dstRowStri { for (int i = 0; i < 9; i++) { - finalTransformMatrix[i] = transMatrix[i]; + finalTransformMatrix[i] = params.transMatrix[i]; } } - for (int dst_y = 0; dst_y < dstSize.h; dst_y++) + for (int dst_y = 0; dst_y < params.dstSize.h; dst_y++) { - for (int dst_x = 0; dst_x < dstSize.w; dst_x++) + for (int dst_x = 0; dst_x < params.dstSize.w; dst_x++) { - float coeff - = 1.0f - / (float)(dst_x * finalTransformMatrix[6] + dst_y * finalTransformMatrix[7] + finalTransformMatrix[8]); + const auto dstX = static_cast(dst_x); + const auto dstY = static_cast(dst_y); + float coeff + = 1.0f / (dstX * finalTransformMatrix[6] + dstY * finalTransformMatrix[7] + finalTransformMatrix[8]); float src_x - = coeff - * (float)(dst_x * finalTransformMatrix[0] + dst_y * finalTransformMatrix[1] + finalTransformMatrix[2]); + = coeff * (dstX * finalTransformMatrix[0] + dstY * finalTransformMatrix[1] + finalTransformMatrix[2]); float src_y - = coeff - * (float)(dst_x * finalTransformMatrix[3] + dst_y * finalTransformMatrix[4] + finalTransformMatrix[5]); + = coeff * (dstX * finalTransformMatrix[3] + dstY * finalTransformMatrix[4] + finalTransformMatrix[5]); if (interpolation == NVCV_INTERP_LINEAR) { - const int x1 = std::floor(src_x); - const int y1 = std::floor(src_y); - - const int x2 = x1 + 1; - const int y2 = y1 + 1; - - for (int k = 0; k < elementsPerPixel; k++) - { - float out = 0; - - uint8_t src_reg = getPixelForPerspectiveTransform( - srcPtr, y1, x1, k, srcWidth, srcHeight, srcRowStride, elementsPerPixel, borderMode, borderVal); - out += src_reg * ((x2 - src_x) * (y2 - src_y)); - - src_reg = getPixelForPerspectiveTransform(srcPtr, y1, x2, k, srcWidth, srcHeight, srcRowStride, - elementsPerPixel, borderMode, borderVal); - out = out + src_reg * ((src_x - x1) * (y2 - src_y)); - - src_reg = getPixelForPerspectiveTransform(srcPtr, y2, x1, k, srcWidth, srcHeight, srcRowStride, - elementsPerPixel, borderMode, borderVal); - out = out + src_reg * ((x2 - src_x) * (src_y - y1)); - - src_reg = getPixelForPerspectiveTransform(srcPtr, y2, x2, k, srcWidth, srcHeight, srcRowStride, - elementsPerPixel, borderMode, borderVal); - out = out + src_reg * ((src_x - x1) * (src_y - y1)); - - out = std::rint(out); - dstPtr[dst_y * dstRowStride + dst_x * elementsPerPixel + k] = out < 0 ? 0 : (out > 255 ? 255 : out); - } + StoreLinearPixel(dstPtr, dst_y * params.dstRowStride + dst_x * elementsPerPixel, src, src_x, src_y); } else if (interpolation == NVCV_INTERP_NEAREST) { - const int x1 = std::floor(src_x + .5f); - const int y1 = std::floor(src_y + .5f); - for (int k = 0; k < elementsPerPixel; k++) - { - uint8_t src_reg = getPixelForPerspectiveTransform( - srcPtr, y1, x1, k, srcWidth, srcHeight, srcRowStride, elementsPerPixel, borderMode, borderVal); - dstPtr[dst_y * dstRowStride + dst_x * elementsPerPixel + k] = src_reg; - } + StoreNearestPixel(dstPtr, dst_y * params.dstRowStride + dst_x * elementsPerPixel, src, src_x, src_y); } else if (interpolation == NVCV_INTERP_CUBIC) { - const int xmin = std::ceil(src_x - 2.0f); - const int xmax = std::floor(src_x + 2.0f); - - const int ymin = std::ceil(src_y - 2.0f); - const int ymax = std::floor(src_y + 2.0f); - - for (int k = 0; k < elementsPerPixel; k++) - { - float sum = 0; - float wsum = 0; - - for (int cy = ymin; cy <= ymax; cy += 1) - { - for (int cx = xmin; cx <= xmax; cx += 1) - { - const float w = calcBicubicCoeff(src_x - cx) * calcBicubicCoeff(src_y - cy); - uint8_t src_reg - = getPixelForPerspectiveTransform(srcPtr, cy, cx, k, srcWidth, srcHeight, srcRowStride, - elementsPerPixel, borderMode, borderVal); - sum += w * src_reg; - wsum += w; - } - } - - float res = (!wsum) ? 0 : sum / wsum; - res = std::rint(res); - dstPtr[dst_y * dstRowStride + dst_x * elementsPerPixel + k] = res < 0 ? 0 : (res > 255 ? 255 : res); - } + StoreCubicPixel(dstPtr, dst_y * params.dstRowStride + dst_x * elementsPerPixel, src, src_x, src_y); } else { @@ -259,39 +301,28 @@ static void WarpPerspectiveGold(std::vector &hDst, const int dstRowStri } } -/* - The perspective transform matrix with non-trivial projection are calculated using the below formula: - - input_pts[0] = [0, 0]; - input_pts[1] = [cols - 1, 0]; - input_pts[2] = [0, rows - 1]; - input_pts[3] = [cols - 1, rows - 1]; +// Non-trivial projection matrices use the four input image corners and map them +// to scaled output-image corner positions. - output_pts[0] = [0, out_rows*0.13]; - output_pts[1] = [out_cols*0.9, 0]; - output_pts[2] = [out_cols*0.2, out_rows*0.7]; - output_pts[3] = [out_cols*0.8, out_rows]; -*/ - -std::map, std::vector>> mapOfTransformationMatrix = { +const std::map, std::vector>> mapOfTransformationMatrix = { {{5, 4, 5, 4}, - {{1, 0, 0, 0, 1, 0, 0, 0, 1}, - {1, 0, 1, 0, 1, 2, 0, 0, 1}, - {1, 2, 1, 2, 1, 2, 0, 0, 1}, - {0.5, 2, 1, 0.75, 1, 2, 0, 0, 1}, - {0.50, 0.47, 0.00, -0.13, 1.14, 0.52, -0.14, 0.14, 1.00}}}, + {{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f}, + {1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 2.0f, 0.0f, 0.0f, 1.0f}, + {1.0f, 2.0f, 1.0f, 2.0f, 1.0f, 2.0f, 0.0f, 0.0f, 1.0f}, + {0.5f, 2.0f, 1.0f, 0.75f, 1.0f, 2.0f, 0.0f, 0.0f, 1.0f}, + {0.50f, 0.47f, 0.00f, -0.13f, 1.14f, 0.52f, -0.14f, 0.14f, 1.00f}}}, {{5, 4, 6, 8}, - {{1, 0, 0, 0, 1, 0, 0, 0, 1}, - {1, 0, 1, 0, 1, 2, 0, 0, 1}, - {1, 2, 1, 2, 1, 2, 0, 0, 1}, - {0.5, 2, 1, 0.75, 1, 2, 0, 0, 1}, - {0.60, 0.56, 0.00, -0.26, 2.28, 1.04, -0.14, 0.14, 1.00}}}, + {{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f}, + {1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 2.0f, 0.0f, 0.0f, 1.0f}, + {1.0f, 2.0f, 1.0f, 2.0f, 1.0f, 2.0f, 0.0f, 0.0f, 1.0f}, + {0.5f, 2.0f, 1.0f, 0.75f, 1.0f, 2.0f, 0.0f, 0.0f, 1.0f}, + {0.60f, 0.56f, 0.00f, -0.26f, 2.28f, 1.04f, -0.14f, 0.14f, 1.00f}}}, {{7, 8, 4, 5}, - {{1, 0, 0, 0, 1, 0, 0, 0, 1}, - {1, 0, 1, 0, 1, 2, 0, 0, 1}, - {1, 2, 1, 2, 1, 2, 0, 0, 1}, - {0.5, 2, 1, 0.75, 1, 2, 0, 0, 1}, - {0.27, 0.16, 0.00, -0.11, 0.61, 0.65, -0.09, 0.06, 1.00}}} + {{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f}, + {1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 2.0f, 0.0f, 0.0f, 1.0f}, + {1.0f, 2.0f, 1.0f, 2.0f, 1.0f, 2.0f, 0.0f, 0.0f, 1.0f}, + {0.5f, 2.0f, 1.0f, 0.75f, 1.0f, 2.0f, 0.0f, 0.0f, 1.0f}, + {0.27f, 0.16f, 0.00f, -0.11f, 0.61f, 0.65f, -0.09f, 0.06f, 1.00f}}} }; // clang-format off @@ -515,7 +546,7 @@ TEST_P(OpWarpPerspective, tensor_correct_output) std::uniform_int_distribution rand(0, 255); srcVec[i].resize(srcHeight * srcVecRowStride); - std::generate(srcVec[i].begin(), srcVec[i].end(), [&]() { return rand(randEng); }); + std::ranges::generate(srcVec[i], [&rand, &randEng]() { return rand(randEng); }); // Copy input data to the GPU ASSERT_EQ(cudaSuccess, @@ -554,11 +585,21 @@ TEST_P(OpWarpPerspective, tensor_correct_output) dstHeight, cudaMemcpyDeviceToHost)); std::vector goldVec(dstHeight * dstVecRowStride); - std::generate(goldVec.begin(), goldVec.end(), [&]() { return 0; }); + std::ranges::generate(goldVec, []() { return 0; }); // Generate gold result - WarpPerspectiveGold(goldVec, dstVecRowStride, {dstWidth, dstHeight}, srcVec[i], srcVecRowStride, - {srcWidth, srcHeight}, fmt, transMatrix, flags, borderMode, borderValue); + const WarpPerspectiveGoldParams goldParams{ + dstVecRowStride, + {dstWidth, dstHeight}, + srcVecRowStride, + {srcWidth, srcHeight}, + fmt, + transMatrix, + flags, + borderMode, + borderValue + }; + WarpPerspectiveGold(goldVec, srcVec[i], goldParams); printVec(srcVec[i], srcHeight, srcVecRowStride, bytesPerPixel, "src vec"); printVec(goldVec, dstHeight, dstVecRowStride, bytesPerPixel, "golden output"); @@ -601,7 +642,7 @@ TEST_P(OpWarpPerspective, varshape_correct_output) bool inverseMap = GetParamValue<20>(); const nvcv::ImageFormat fmt = nvcv::FMT_RGBA8; - int bytesPerPixel = 4; + const int bytesPerPixel = 4; const int flags = interpolation | (inverseMap ? NVCV_WARP_INVERSE_MAP : 0); @@ -613,17 +654,18 @@ TEST_P(OpWarpPerspective, varshape_correct_output) ASSERT_TRUE(transMatrixTensorDataAccess); // Create input and output - std::default_random_engine randEng; - std::uniform_int_distribution rndInputDimsIndex(0, mapOfTransformationMatrix.size() - 1); - std::uniform_int_distribution rndTransformationMatrixIndex(0, 4); + std::default_random_engine randEng; + std::uniform_int_distribution rndInputDimsIndex(0, static_cast(mapOfTransformationMatrix.size()) - 1); + std::uniform_int_distribution rndTransformationMatrixIndex(0, 4); - std::vector imgSrc, imgDst; + std::vector imgSrc; + std::vector imgDst; std::vector> transMatrixHostVec; transMatrixHostVec.resize(numberOfImages); // List the keys from the map for easy access std::vector> keysOfMapOfTransformationMatrix; - for (auto &[key, value] : mapOfTransformationMatrix) + for (const auto &[key, transformationMatrices] : mapOfTransformationMatrix) { keysOfMapOfTransformationMatrix.push_back(key); } @@ -641,8 +683,9 @@ TEST_P(OpWarpPerspective, varshape_correct_output) int dictInputIndex = rndInputDimsIndex(randEng); int dictTransformationIndex = rndTransformationMatrixIndex(randEng); - std::vector key = keysOfMapOfTransformationMatrix[dictInputIndex]; - std::vector chosenTransformationMatrix = mapOfTransformationMatrix[key][dictTransformationIndex]; + const std::vector &key = keysOfMapOfTransformationMatrix[dictInputIndex]; + const std::vector &chosenTransformationMatrix + = mapOfTransformationMatrix.at(key)[dictTransformationIndex]; if (i > 0) { tmpSrcWidth = key[0]; @@ -681,23 +724,23 @@ TEST_P(OpWarpPerspective, varshape_correct_output) const auto srcData = imgSrc[i].exportData(); assert(srcData->numPlanes() == 1); - int srcWidth = srcData->plane(0).width; - int srcHeight = srcData->plane(0).height; + int currentSrcWidth = srcData->plane(0).width; + int currentSrcHeight = srcData->plane(0).height; - int srcRowStride = srcWidth * fmt.planePixelStrideBytes(0); + int srcRowStride = currentSrcWidth * fmt.planePixelStrideBytes(0); srcVecRowStride[i] = srcRowStride; std::uniform_int_distribution rand(0, 255); - srcVec[i].resize(srcHeight * srcRowStride); - std::generate(srcVec[i].begin(), srcVec[i].end(), [&]() { return rand(randEng); }); + srcVec[i].resize(currentSrcHeight * srcRowStride); + std::ranges::generate(srcVec[i], [&rand, &randEng]() { return rand(randEng); }); // Copy input data to the GPU ASSERT_EQ(cudaSuccess, cudaMemcpy2D(srcData->plane(0).basePtr, srcData->plane(0).rowStride, srcVec[i].data(), srcRowStride, srcRowStride, // vec has no padding - srcHeight, cudaMemcpyHostToDevice)); + currentSrcHeight, cudaMemcpyHostToDevice)); } // Generate test result @@ -715,28 +758,28 @@ TEST_P(OpWarpPerspective, varshape_correct_output) const auto srcData = imgSrc[i].exportData(); assert(srcData->numPlanes() == 1); - int srcWidth = srcData->plane(0).width; - int srcHeight = srcData->plane(0).height; + int currentSrcWidth = srcData->plane(0).width; + int currentSrcHeight = srcData->plane(0).height; const auto dstData = imgDst[i].exportData(); assert(dstData->numPlanes() == 1); - int dstWidth = dstData->plane(0).width; - int dstHeight = dstData->plane(0).height; + int currentDstWidth = dstData->plane(0).width; + int currentDstHeight = dstData->plane(0).height; - int srcRowStride = srcWidth * fmt.planePixelStrideBytes(0); - int dstRowStride = dstWidth * fmt.planePixelStrideBytes(0); + int srcRowStride = currentSrcWidth * fmt.planePixelStrideBytes(0); + int dstRowStride = currentDstWidth * fmt.planePixelStrideBytes(0); - std::vector testVec(dstHeight * dstRowStride); + std::vector testVec(currentDstHeight * dstRowStride); // Copy output data to Host ASSERT_EQ(cudaSuccess, cudaMemcpy2D(testVec.data(), dstRowStride, dstData->plane(0).basePtr, dstData->plane(0).rowStride, dstRowStride, // vec has no padding - dstHeight, cudaMemcpyDeviceToHost)); + currentDstHeight, cudaMemcpyDeviceToHost)); - std::vector goldVec(dstHeight * dstRowStride); - std::generate(goldVec.begin(), goldVec.end(), [&]() { return 0; }); + std::vector goldVec(currentDstHeight * dstRowStride); + std::ranges::generate(goldVec, []() { return 0; }); NVCVPerspectiveTransform transMatrixForGold; transMatrixForGold[0] = transMatrixHostVec[i][0]; @@ -750,23 +793,124 @@ TEST_P(OpWarpPerspective, varshape_correct_output) transMatrixForGold[8] = transMatrixHostVec[i][8]; // Generate gold result - WarpPerspectiveGold(goldVec, dstRowStride, {dstWidth, dstHeight}, srcVec[i], srcRowStride, - {srcWidth, srcHeight}, fmt, transMatrixForGold, flags, borderMode, borderValue); - - printVec(srcVec[i], srcHeight, srcRowStride, bytesPerPixel, "src vec"); - printVec(goldVec, dstHeight, dstRowStride, bytesPerPixel, "golden output"); - printVec(testVec, dstHeight, dstRowStride, bytesPerPixel, "warped output"); + const WarpPerspectiveGoldParams goldParams{ + dstRowStride, {currentDstWidth, currentDstHeight}, + srcRowStride, {currentSrcWidth, currentSrcHeight}, + fmt, transMatrixForGold, + flags, borderMode, + borderValue + }; + WarpPerspectiveGold(goldVec, srcVec[i], goldParams); + + printVec(srcVec[i], currentSrcHeight, srcRowStride, bytesPerPixel, "src vec"); + printVec(goldVec, currentDstHeight, dstRowStride, bytesPerPixel, "golden output"); + printVec(testVec, currentDstHeight, dstRowStride, bytesPerPixel, "warped output"); EXPECT_EQ(goldVec, testVec); } } +// ============================================================================= +// Planar (NCHW/CHW) layout support +// +// WarpPerspective samples every channel at the same transformed coordinate, so +// planar output must match the equivalent interleaved output bit-for-bit after +// re-interleaving. CONSTANT border uses a non-uniform border value to exercise +// per-channel border handling in the planar path. +// ============================================================================= + +namespace { + +inline std::array PlanarPerspective() +{ + return {1.05f, 0.03f, 2.0f, -0.02f, 0.98f, 1.0f, 0.0008f, -0.0004f, 1.0f}; +} + +void RunPlanarParityTensorCase(nvcv::ImageFormat planarFmt, nvcv::ImageFormat interleavedFmt, int srcW, int srcH, + int dstW, int dstH, NVCVInterpolationType interp, NVCVBorderType borderMode, + int numImages) +{ + const std::array xform = PlanarPerspective(); + const float4 borderValue = {13.f, 57.f, 101.f, 211.f}; + const int32_t flags = interp; + + test::planar::RunTensorParity(planarFmt, interleavedFmt, srcW, srcH, dstW, dstH, numImages, + [xform, flags, borderMode, borderValue](cudaStream_t s, const nvcv::Tensor &src, + const nvcv::Tensor &dst, nvcv::ImageFormat) + { + cvcuda::WarpPerspective op(0); + EXPECT_NO_THROW(op(s, src, dst, xform.data(), flags, borderMode, borderValue)); + }); +} + +void RunPlanarParityVarShapeCase(nvcv::ImageFormat planarFmt, nvcv::ImageFormat interleavedFmt, int srcW, int srcH, + int dstW, int dstH, NVCVInterpolationType interp, NVCVBorderType borderMode, + int numImages) +{ + const std::array xform = PlanarPerspective(); + const float4 borderValue = {13.f, 57.f, 101.f, 211.f}; + const int32_t flags = interp; + + nvcv::Tensor transMatrix(nvcv::TensorShape({numImages, 9}, nvcv::TENSOR_NW), nvcv::TYPE_F32); + { + auto data = transMatrix.exportData(); + ASSERT_NE(data, nullptr); + auto acc = nvcv::TensorDataAccessStrided::Create(*data); + ASSERT_TRUE(acc); + for (int i = 0; i < numImages; ++i) + { + ASSERT_EQ(cudaSuccess, cudaMemcpy2D(acc->sampleData(i), acc->sampleStride(), xform.data(), + sizeof(float) * 9, sizeof(float) * 9, 1, cudaMemcpyHostToDevice)); + } + } + + test::planar::RunVarShapeParity( + planarFmt, interleavedFmt, srcW, srcH, dstW, dstH, numImages, + [&transMatrix, flags, borderMode, borderValue, numImages]( + cudaStream_t s, const nvcv::ImageBatchVarShape &src, const nvcv::ImageBatchVarShape &dst, nvcv::ImageFormat) + { + cvcuda::WarpPerspective op(numImages); + EXPECT_NO_THROW(op(s, src, dst, transMatrix, flags, borderMode, borderValue)); + }); +} + +} // namespace + +// Parameters: planarFmt, interleavedFmt, interpolation, borderMode, numImages, srcW, srcH, dstW, dstH +// clang-format off +NVCV_TEST_SUITE_P(OpWarpPerspectivePlanar, + test::ValueList{ + { nvcv::FMT_RGB8p, nvcv::FMT_RGB8, NVCV_INTERP_NEAREST, NVCV_BORDER_CONSTANT, 2, 64, 48, 64, 48}, + { nvcv::FMT_RGB8p, nvcv::FMT_RGB8, NVCV_INTERP_LINEAR, NVCV_BORDER_CONSTANT, 2, 64, 48, 96, 72}, + { nvcv::FMT_RGB8p, nvcv::FMT_RGB8, NVCV_INTERP_CUBIC, NVCV_BORDER_REPLICATE, 1, 80, 60, 64, 48}, + { nvcv::FMT_RGB8p, nvcv::FMT_RGB8, NVCV_INTERP_LINEAR, NVCV_BORDER_WRAP, 1, 64, 48, 64, 48}, + { nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8, NVCV_INTERP_NEAREST, NVCV_BORDER_CONSTANT, 2, 50, 40, 60, 50}, + { nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8, NVCV_INTERP_LINEAR, NVCV_BORDER_REPLICATE, 1, 50, 40, 50, 40}, + { nvcv::FMT_RGBf32p, nvcv::FMT_RGBf32, NVCV_INTERP_CUBIC, NVCV_BORDER_CONSTANT, 1, 64, 48, 96, 72}, + {nvcv::FMT_RGBAf32p, nvcv::FMT_RGBAf32, NVCV_INTERP_LINEAR, NVCV_BORDER_CONSTANT, 2, 64, 48, 64, 48}, +}); + +// clang-format on + +TEST_P(OpWarpPerspectivePlanar, tensor_matches_interleaved) +{ + RunPlanarParityTensorCase(GetParamValue<0>(), GetParamValue<1>(), GetParamValue<5>(), GetParamValue<6>(), + GetParamValue<7>(), GetParamValue<8>(), GetParamValue<2>(), GetParamValue<3>(), + GetParamValue<4>()); +} + +TEST_P(OpWarpPerspectivePlanar, varshape_matches_interleaved) +{ + RunPlanarParityVarShapeCase(GetParamValue<0>(), GetParamValue<1>(), GetParamValue<5>(), GetParamValue<6>(), + GetParamValue<7>(), GetParamValue<8>(), GetParamValue<2>(), GetParamValue<3>(), + GetParamValue<4>()); +} + // clang-format off NVCV_TEST_SUITE_P(OpWarpPerspective_Negative, test::ValueList{ // input format, output format, {nvcv::FMT_RGBA8, nvcv::FMT_RGBA8p}, {nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8}, - {nvcv::FMT_RGBA8p, nvcv::FMT_RGBA8p}, {nvcv::FMT_RGBAf16, nvcv::FMT_RGBAf16} }); @@ -774,7 +918,6 @@ NVCV_TEST_SUITE_P(OpWarpPerspectiveVarshape_Negative, test::ValueList imgSrc, imgDst; + std::vector imgSrc; + std::vector imgDst; for (int i = 0; i < numImages; ++i) { @@ -850,7 +994,8 @@ TEST_P(OpWarpPerspectiveVarshape_Negative, op) EXPECT_EQ( NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall( - [&] { warpPerspectiveOp(stream, batchSrc, batchDst, transMatrixTensor, flags, borderMode, borderValue); })); + [&warpPerspectiveOp, &stream, &batchSrc, &batchDst, &transMatrixTensor, &flags, &borderMode, &borderValue] + { warpPerspectiveOp(stream, batchSrc, batchDst, transMatrixTensor, flags, borderMode, borderValue); })); EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); @@ -861,6 +1006,159 @@ TEST(OpWarpPerspective_Negative, create_null_handle) EXPECT_EQ(cvcudaWarpPerspectiveCreate(nullptr, 2), NVCV_ERROR_INVALID_ARGUMENT); } +// Regression test for CVCUDA issue #249: a projective matrix whose singular line +// falls inside the destination image produces source coordinates near +// +/-INT32_MAX. With BORDER_REPLICATE this used to dereference a wild index, +// triggering cudaErrorIllegalAddress. A plain stream sync is enough to surface +// the kernel crash; the output values are not checked because the test-side +// gold implementation has the same host-side float-to-int saturation hazard as +// the kernel and would need its own hardening to match the fix exactly. +TEST(OpWarpPerspective, extreme_projection_replicate_issue_249) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + const int srcWidth = 1928; + const int srcHeight = 1208; + const int dstWidth = 1928; + const int dstHeight = 1208; + const int batchSize = 1; + const nvcv::ImageFormat fmt = nvcv::FMT_RGB8; + + nvcv::Tensor imgSrc(batchSize, {srcWidth, srcHeight}, fmt); + nvcv::Tensor imgDst(batchSize, {dstWidth, dstHeight}, fmt); + + NVCVPerspectiveTransform transMatrix = { + 8.08776838e-02f, 2.36326631e+00f, -4.08795000e+02f, -1.28514739e-02f, 2.55201343e-01f, + -8.45896673e+01f, -2.68404432e-04f, -6.57235630e-04f, 1.00000000e+00f, + }; + + const int flags = NVCV_INTERP_LINEAR; + const float4 borderValue = {0, 0, 0, 0}; + + cvcuda::WarpPerspective warpPerspectiveOp(0); + EXPECT_NO_THROW(warpPerspectiveOp(stream, imgSrc, imgDst, transMatrix, flags, NVCV_BORDER_REPLICATE, borderValue)); + + EXPECT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpWarpPerspective_Negative, invalid_border_mode) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + NVCVPerspectiveTransform transMatrix = {1, 0, 0, 0, 1, 0, 0, 0, 1}; + const float4 borderValue = {0, 0, 0, 0}; + const int flags = NVCV_INTERP_NEAREST | NVCV_WARP_INVERSE_MAP; + + nvcv::Tensor imgSrc(1, {4, 4}, nvcv::FMT_U8); + nvcv::Tensor imgDst(1, {4, 4}, nvcv::FMT_U8); + + cvcuda::WarpPerspective op(0); + + // 5 is one past the last valid NVCVBorderType value (NVCV_BORDER_REFLECT101 = 4) + auto invalidBorder = static_cast(5); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&op, &stream, &imgSrc, &imgDst, &transMatrix, &flags, &invalidBorder, &borderValue] + { op(stream, imgSrc, imgDst, transMatrix, flags, invalidBorder, borderValue); })); + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpWarpPerspective_Negative, invalid_interpolation) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + NVCVPerspectiveTransform transMatrix = {1, 0, 0, 0, 1, 0, 0, 0, 1}; + const float4 borderValue = {0, 0, 0, 0}; + + nvcv::Tensor imgSrc(1, {4, 4}, nvcv::FMT_U8); + nvcv::Tensor imgDst(1, {4, 4}, nvcv::FMT_U8); + + cvcuda::WarpPerspective op(0); + + // NVCV_INTERP_AREA (3) is not supported by the warp ops + const int flags = static_cast(NVCV_INTERP_AREA) | NVCV_WARP_INVERSE_MAP; + EXPECT_EQ( + NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&op, &stream, &imgSrc, &imgDst, &transMatrix, &flags, &borderValue] + { op(stream, imgSrc, imgDst, transMatrix, flags, NVCV_BORDER_CONSTANT, borderValue); })); + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpWarpPerspectiveVarshape_Negative, invalid_border_mode) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + const int numImages = 2; + const float4 borderValue = {0, 0, 0, 0}; + const int flags = NVCV_INTERP_NEAREST | NVCV_WARP_INVERSE_MAP; + + nvcv::Tensor transMatrixTensor(nvcv::TensorShape({numImages, 9}, nvcv::TENSOR_NW), nvcv::TYPE_F32); + + std::vector imgSrc; + std::vector imgDst; + for (int i = 0; i < numImages; ++i) + { + imgSrc.emplace_back(nvcv::Size2D{4, 4}, nvcv::FMT_U8); + imgDst.emplace_back(nvcv::Size2D{4, 4}, nvcv::FMT_U8); + } + + nvcv::ImageBatchVarShape batchSrc(numImages); + nvcv::ImageBatchVarShape batchDst(numImages); + batchSrc.pushBack(imgSrc.begin(), imgSrc.end()); + batchDst.pushBack(imgDst.begin(), imgDst.end()); + + cvcuda::WarpPerspective op(numImages); + + auto invalidBorder = static_cast(5); + EXPECT_EQ( + NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall([&op, &stream, &batchSrc, &batchDst, &transMatrixTensor, &flags, &invalidBorder, &borderValue] + { op(stream, batchSrc, batchDst, transMatrixTensor, flags, invalidBorder, borderValue); })); + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + +TEST(OpWarpPerspectiveVarshape_Negative, invalid_interpolation) +{ + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + const int numImages = 2; + const float4 borderValue = {0, 0, 0, 0}; + + nvcv::Tensor transMatrixTensor(nvcv::TensorShape({numImages, 9}, nvcv::TENSOR_NW), nvcv::TYPE_F32); + + std::vector imgSrc; + std::vector imgDst; + for (int i = 0; i < numImages; ++i) + { + imgSrc.emplace_back(nvcv::Size2D{4, 4}, nvcv::FMT_U8); + imgDst.emplace_back(nvcv::Size2D{4, 4}, nvcv::FMT_U8); + } + + nvcv::ImageBatchVarShape batchSrc(numImages); + nvcv::ImageBatchVarShape batchDst(numImages); + batchSrc.pushBack(imgSrc.begin(), imgSrc.end()); + batchDst.pushBack(imgDst.begin(), imgDst.end()); + + cvcuda::WarpPerspective op(numImages); + + // NVCV_INTERP_AREA (3) is not supported by the warp ops + const int flags = static_cast(NVCV_INTERP_AREA) | NVCV_WARP_INVERSE_MAP; + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcv::ProtectCall( + [&op, &stream, &batchSrc, &batchDst, &transMatrixTensor, &flags, &borderValue] + { op(stream, batchSrc, batchDst, transMatrixTensor, flags, NVCV_BORDER_CONSTANT, borderValue); })); + + ASSERT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} + TEST(OpWarpPerspectiveVarshape_Negative, different_format_varshape) { std::vector> extraFmts{ @@ -868,15 +1166,12 @@ TEST(OpWarpPerspectiveVarshape_Negative, different_format_varshape) {nvcv::FMT_RGBA8, nvcv::FMT_RGB8} }; - for (const auto &testCase : extraFmts) + for (const auto &[extraFmtSrc, extraFmtDst] : extraFmts) { - auto extraFmtSrc = testCase.first; - auto extraFmtDst = testCase.second; - cudaStream_t stream; EXPECT_EQ(cudaSuccess, cudaStreamCreate(&stream)); - int numImages = 10; + const int numImages = 10; const nvcv::ImageFormat fmt = nvcv::FMT_RGB8; NVCVInterpolationType interpolation = NVCV_INTERP_NEAREST; @@ -889,8 +1184,8 @@ TEST(OpWarpPerspectiveVarshape_Negative, different_format_varshape) nvcv::Tensor transMatrixTensor(nvcv::TensorShape({numImages, 9}, nvcv::TENSOR_NW), nvcv::TYPE_F32); // Create input and output - std::default_random_engine randEng; - std::vector imgSrc, imgDst; + std::vector imgSrc; + std::vector imgDst; for (int i = 0; i < numImages - 1; ++i) { @@ -908,7 +1203,8 @@ TEST(OpWarpPerspectiveVarshape_Negative, different_format_varshape) cvcuda::WarpPerspective warpPerspectiveOp(numImages); EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcv::ProtectCall( - [&] { + [&warpPerspectiveOp, &stream, &batchSrc, &batchDst, + &transMatrixTensor, &flags, &borderMode, &borderValue] { warpPerspectiveOp(stream, batchSrc, batchDst, transMatrixTensor, flags, borderMode, borderValue); })); diff --git a/tests/cvcuda/system/TestOperatorWrapperCopy.cpp b/tests/cvcuda/system/TestOperatorWrapperCopy.cpp new file mode 100644 index 000000000..b68cbb7e1 --- /dev/null +++ b/tests/cvcuda/system/TestOperatorWrapperCopy.cpp @@ -0,0 +1,162 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "Definitions.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +template +static void ExpectWrapperIsMoveOnly() +{ + static_assert(!std::is_copy_constructible_v, "operator wrapper must not be copy constructible"); + static_assert(!std::is_copy_assignable_v, "operator wrapper must not be copy assignable"); + static_assert(std::is_move_constructible_v, "operator wrapper must be move constructible"); + static_assert(std::is_move_assignable_v, "operator wrapper must be move assignable"); + static_assert(std::is_nothrow_move_constructible_v, "operator wrapper move constructor must be noexcept"); + static_assert(std::is_nothrow_move_assignable_v, "operator wrapper move assignment must be noexcept"); +} + +TEST(PublicOperatorWrappers, allWrappersAreMoveOnly) +{ + ExpectWrapperIsMoveOnly(); + ExpectWrapperIsMoveOnly(); + ExpectWrapperIsMoveOnly(); + ExpectWrapperIsMoveOnly(); + ExpectWrapperIsMoveOnly(); + ExpectWrapperIsMoveOnly(); + ExpectWrapperIsMoveOnly(); + ExpectWrapperIsMoveOnly(); + ExpectWrapperIsMoveOnly(); + ExpectWrapperIsMoveOnly(); + ExpectWrapperIsMoveOnly(); + ExpectWrapperIsMoveOnly(); + ExpectWrapperIsMoveOnly(); + ExpectWrapperIsMoveOnly(); + ExpectWrapperIsMoveOnly(); + ExpectWrapperIsMoveOnly(); + ExpectWrapperIsMoveOnly(); + ExpectWrapperIsMoveOnly(); + ExpectWrapperIsMoveOnly(); + ExpectWrapperIsMoveOnly(); + ExpectWrapperIsMoveOnly(); + ExpectWrapperIsMoveOnly(); + ExpectWrapperIsMoveOnly(); + ExpectWrapperIsMoveOnly(); + ExpectWrapperIsMoveOnly(); + ExpectWrapperIsMoveOnly(); + ExpectWrapperIsMoveOnly(); + ExpectWrapperIsMoveOnly(); + ExpectWrapperIsMoveOnly(); + ExpectWrapperIsMoveOnly(); + ExpectWrapperIsMoveOnly(); + ExpectWrapperIsMoveOnly(); + ExpectWrapperIsMoveOnly(); + ExpectWrapperIsMoveOnly(); + ExpectWrapperIsMoveOnly(); + ExpectWrapperIsMoveOnly(); + ExpectWrapperIsMoveOnly(); + ExpectWrapperIsMoveOnly(); + ExpectWrapperIsMoveOnly(); + ExpectWrapperIsMoveOnly(); + ExpectWrapperIsMoveOnly(); + ExpectWrapperIsMoveOnly(); + ExpectWrapperIsMoveOnly(); + ExpectWrapperIsMoveOnly(); + ExpectWrapperIsMoveOnly(); + ExpectWrapperIsMoveOnly(); + ExpectWrapperIsMoveOnly(); + ExpectWrapperIsMoveOnly(); + ExpectWrapperIsMoveOnly(); + ExpectWrapperIsMoveOnly(); + ExpectWrapperIsMoveOnly(); + ExpectWrapperIsMoveOnly(); +} + +// Runtime evidence that the move-only contract closes the original double-destroy bug +// (CWE-415): construct -> move -> destroy must transfer the handle so that the +// destination wrapper owns the original handle and destruction does not double-free it. +TEST(PublicOperatorWrappers, moveTransfersHandle) +{ + cvcuda::Resize a; + const NVCVOperatorHandle original = a.handle(); + ASSERT_NE(nullptr, original); + + cvcuda::Resize b{std::move(a)}; + EXPECT_EQ(original, b.handle()); + + cvcuda::Resize c; + const NVCVOperatorHandle cOriginal = c.handle(); + ASSERT_NE(nullptr, cOriginal); + ASSERT_NE(original, cOriginal); + + c = std::move(b); + EXPECT_EQ(original, c.handle()); +} diff --git a/tests/cvcuda/system/TestUtils.cpp b/tests/cvcuda/system/TestUtils.cpp index 0c85de0d8..d9857f846 100644 --- a/tests/cvcuda/system/TestUtils.cpp +++ b/tests/cvcuda/system/TestUtils.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,14 +21,42 @@ #include +#include + namespace cuda = nvcv::cuda; using std::vector; +template +struct RgbStoreParams +{ + T alpha; + bool rgba; + bool bga; +}; + +template +void StoreRgb(T *&dst, T red, T grn, T blu, const RgbStoreParams ¶ms) +{ + if (params.bga) + { + std::swap(red, blu); + } + + *dst++ = red; + *dst++ = grn; + *dst++ = blu; + if (params.rgba) + { + *dst++ = params.alpha; + } +} + //-==================================================================================================================-// // Generate an random image image vector. template -void generateRandVec(T *dst, size_t size, RandEng &eng) +void generateRandVec( // NOSONAR: std::span is C++20. + T *dst, size_t size, RandEng &eng) { RandInt rand(0, cuda::TypeTraits::max); @@ -38,7 +66,8 @@ void generateRandVec(T *dst, size_t size, RandEng &eng) } template<> -void generateRandVec(float *dst, size_t size, RandEng &eng) +void generateRandVec( // NOSONAR: std::span is C++20. + float *dst, size_t size, RandEng &eng) { RandFlt rand(0.0f, 1.0f); @@ -48,9 +77,10 @@ void generateRandVec(float *dst, size_t size, RandEng &eng) } template<> -void generateRandVec(double *dst, size_t size, RandEng &eng) +void generateRandVec( // NOSONAR: std::span is C++20. + double *dst, size_t size, RandEng &eng) { - RandFlt rand(0.0, 1.0); + std::uniform_real_distribution rand(0.0, 1.0); // clang-format off for (size_t i = 0; i < size; i++) dst[i] = rand(eng); @@ -74,35 +104,32 @@ MAKE_RAND_VEC(double); template void generateRandTestRGB(T *dst, size_t size, RandEng &eng, bool rgba, bool bga) { - constexpr T max = std::is_floating_point_v ? 1 : cuda::TypeTraits::max; - constexpr T val[3] = {0, max / 2, max}; + constexpr T max = std::is_floating_point_v ? 1 : cuda::TypeTraits::max; + constexpr std::array val = {0, max / 2, max}; - const size_t minSize = 3 * 3 * 3 * (3 + rgba); + const size_t minSize = 3 * 3 * 3 * (rgba ? 4 : 3); generateRandVec(dst, size, eng); - if (size > minSize) + if (size <= minSize) { - size_t idx = 0; + return; + } + + RgbStoreParams params{max, rgba, bga}; + T *out = dst; - for (unsigned int r = 0; r < 3; r++) + for (unsigned int r = 0; r < 3; r++) + { + const T red = val[r]; + + for (unsigned int g = 0; g < 3; g++) { - const T red = val[r]; + const T grn = val[g]; - for (unsigned int g = 0; g < 3; g++) + for (unsigned int b = 0; b < 3; b++) { - const T grn = val[g]; - - for (unsigned int b = 0; b < 3; b++) - { - const T blu = val[b]; - - // clang-format off - if (bga) { dst[idx++] = blu; dst[idx++] = grn; dst[idx++] = red; } - else { dst[idx++] = red; dst[idx++] = grn; dst[idx++] = blu; } - if (rgba) dst[idx++] = max; - // clang-format on - } + StoreRgb(out, red, grn, val[b], params); } } } @@ -138,11 +165,13 @@ void generateAllRGB(T *dst, unsigned int wdth, unsigned int hght, unsigned int n constexpr double round = std::is_floating_point_v ? 0 : 0.5; constexpr double scale = (double)max / 255.0; - const size_t incrH = wdth * (3 + rgba); + const size_t incrH = wdth * (rgba ? 4 : 3); const size_t incrN = hght * incrH; unsigned int addB = 0; + RgbStoreParams params{max, rgba, bga}; + for (unsigned int i = 0; i < num; i++) { T *img = dst + i * incrN; @@ -151,20 +180,15 @@ void generateAllRGB(T *dst, unsigned int wdth, unsigned int hght, unsigned int n { T *row = img + y * incrH; - uint8_t grn = static_cast(y & 255); + auto grn = static_cast(y & 255); for (unsigned int x = 0; x < wdth; x++) { - uint8_t red = static_cast(x & 255); - uint8_t blu = static_cast(((x >> 8) + addB) & 255); - - // clang-format off - if (bga) std::swap(red, blu); - *row++ = static_cast(red * scale + round); - *row++ = static_cast(grn * scale + round); - *row++ = static_cast(blu * scale + round); - if (rgba) *row++ = max; - // clang-format on + auto red = static_cast(x & 255); + auto blu = static_cast(((x >> 8) + addB) & 255); + + StoreRgb(row, static_cast(red * scale + round), static_cast(grn * scale + round), + static_cast(blu * scale + round), params); } // clang-format off if (grn == 255) addB += ((wdth + 255) >> 8); @@ -198,7 +222,7 @@ void generateRandHSV(T *dst, size_t size, RandEng &eng, double minHueMult, doubl ASSERT_EQ(size % 3, 0); constexpr T max = std::is_floating_point_v ? 1 : cuda::TypeTraits::max; - constexpr unsigned int range = (sizeof(T) > 1) ? 360 : (FullRange ? 256 : 180); + constexpr unsigned int range = HsvHueRange(); constexpr double scale = (double)range / 360.0; constexpr double round = std::is_floating_point_v ? 0 : 0.5; @@ -210,8 +234,8 @@ void generateRandHSV(T *dst, size_t size, RandEng &eng, double minHueMult, doubl double minHue = minHueMult * range; double maxHue = maxHueMult * range; - RandFlt randHue(minHue, maxHue); - RandFlt randSV(0.0, 1.0); + std::uniform_real_distribution randHue(minHue, maxHue); + std::uniform_real_distribution randSV(0.0, 1.0); for (size_t i = 0; i < size; i += 3) { @@ -266,7 +290,7 @@ template void generateAllHSV(T *dst, unsigned int wdth, unsigned int hght, unsigned int num) { constexpr T max = std::is_floating_point_v ? 1 : cuda::TypeTraits::max; - constexpr unsigned int range = (sizeof(T) > 1) ? 360 : (FullRange ? 256 : 180); + constexpr unsigned int range = HsvHueRange(); constexpr double scale = (double)range / 360.0; constexpr double norm = (double)max / 255.0; constexpr double round = std::is_floating_point_v ? 0 : 0.5; @@ -287,13 +311,13 @@ void generateAllHSV(T *dst, unsigned int wdth, unsigned int hght, unsigned int n { T *row = img + y * incrH; - uint8_t S = static_cast(y & 255); + auto S = static_cast(y & 255); // clang-format off for (unsigned int x = 0; x < wdth; x++) { - uint8_t H = static_cast(x % range); - uint8_t V = static_cast((((unsigned int)(x / range) + addV) * stepV) & 255); + auto H = static_cast(x % range); + auto V = static_cast((((unsigned int)(x / range) + addV) * stepV) & 255); *row++ = static_cast(H * scale + round); *row++ = static_cast(S * norm + round); diff --git a/tests/cvcuda/system/TestUtils.hpp b/tests/cvcuda/system/TestUtils.hpp index fca01527b..388650471 100644 --- a/tests/cvcuda/system/TestUtils.hpp +++ b/tests/cvcuda/system/TestUtils.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -29,6 +29,23 @@ using RandInt = std::uniform_int_distribution; template using RandFlt = std::uniform_real_distribution; +template +constexpr unsigned int HsvHueRange() +{ + if constexpr (sizeof(T) > 1) + { + return 360U; + } + else if constexpr (FullRange) + { + return 256U; + } + else + { + return 180U; + } +} + //--------------------------------------------------------------------------------------------------------------------// template void generateRandVec(T *dst, size_t size, RandEng &eng); @@ -61,7 +78,7 @@ template inline void generateAllRGB(std::vector &dst, unsigned int wdth, unsigned int hght, unsigned int num, bool rgba = false, bool bga = false) { - ASSERT_GE(dst.size(), (size_t)num * (size_t)hght * (size_t)wdth * (size_t)(3 + rgba)); + ASSERT_GE(dst.size(), (size_t)num * (size_t)hght * (size_t)wdth * (size_t)(rgba ? 4 : 3)); generateAllRGB(dst.data(), wdth, hght, num, rgba, bga); } diff --git a/tests/cvcuda/unit/CMakeLists.txt b/tests/cvcuda/unit/CMakeLists.txt index db1a797ae..a2918d0f3 100644 --- a/tests/cvcuda/unit/CMakeLists.txt +++ b/tests/cvcuda/unit/CMakeLists.txt @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,13 +14,22 @@ # limitations under the License. add_executable(cvcuda_test_unit + TestCudaDeviceUtils.cpp + TestLegacyHelpers.cpp TestWorkspaceAllocator.cpp TestWorkspaceEstimator.cpp TestStreamId.cpp TestSimpleCache.cpp TestPerStreamCache.cpp + TestOpBoxBlur.cpp + TestOpHQResize.cpp + TestOpHQResizePolicy.cpp + TestOpInvertPolicy.cpp + TestTextBackend.cpp ) +get_target_property(CVCUDA_SOURCE_DIR cvcuda SOURCE_DIR) + target_compile_definitions(cvcuda_test_unit PRIVATE -DNVCV_UNIT_TEST=1 @@ -33,7 +42,13 @@ target_link_libraries(cvcuda_test_unit nvcv_test_common cvcuda_priv cvcuda_util + cvcuda_nvtx_config CUDA::cuda_driver ) +target_include_directories(cvcuda_test_unit + PRIVATE + ${CVCUDA_SOURCE_DIR}/.. +) + nvcv_add_test(cvcuda_test_unit cvcuda cpp) diff --git a/tests/cvcuda/unit/TestCudaDeviceUtils.cpp b/tests/cvcuda/unit/TestCudaDeviceUtils.cpp new file mode 100644 index 000000000..ed18bff55 --- /dev/null +++ b/tests/cvcuda/unit/TestCudaDeviceUtils.cpp @@ -0,0 +1,76 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "Definitions.hpp" + +#include + +namespace { + +class CudaDeviceRestorer +{ +public: + explicit CudaDeviceRestorer(int device) + : m_device(device) + { + } + + ~CudaDeviceRestorer() + { + (void)cudaSetDevice(m_device); + } + + CudaDeviceRestorer(const CudaDeviceRestorer &) = delete; + CudaDeviceRestorer &operator=(const CudaDeviceRestorer &) = delete; + +private: + int m_device; +}; + +TEST(CudaDeviceUtils, CurrentDeviceSMMatchesDeviceProperties) +{ + int deviceCount = 0; + cudaError_t status = cudaGetDeviceCount(&deviceCount); + if (status == cudaErrorNoDevice || status == cudaErrorInsufficientDriver) + GTEST_SKIP() << cudaGetErrorString(status); + ASSERT_EQ(cudaSuccess, status); + if (deviceCount == 0) + GTEST_SKIP() << "No CUDA devices available"; + + int originalDevice = 0; + ASSERT_EQ(cudaSuccess, cudaGetDevice(&originalDevice)); + CudaDeviceRestorer restoreDevice(originalDevice); + + for (int device = 0; device < deviceCount; ++device) + { + ASSERT_EQ(cudaSuccess, cudaSetDevice(device)); + + cudaDeviceProp properties{}; + ASSERT_EQ(cudaSuccess, cudaGetDeviceProperties(&properties, device)); + const int expectedSM = properties.major * 10 + properties.minor; + + int sm = 0; + EXPECT_EQ(cudaSuccess, cvcuda::priv::GetCurrentDeviceSM(sm)); + EXPECT_EQ(expectedSM, sm); + + sm = 0; + EXPECT_EQ(cudaSuccess, cvcuda::priv::GetCurrentDeviceSM(sm)); + EXPECT_EQ(expectedSM, sm); + } +} + +} // namespace diff --git a/tests/cvcuda/unit/TestLegacyHelpers.cpp b/tests/cvcuda/unit/TestLegacyHelpers.cpp new file mode 100644 index 000000000..78e07b9b5 --- /dev/null +++ b/tests/cvcuda/unit/TestLegacyHelpers.cpp @@ -0,0 +1,57 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "Definitions.hpp" + +#include +#include + +namespace { + +using nvcv::legacy::cuda_op::ErrorCode; +using nvcv::legacy::helpers::GetLegacyDataFormat; +using nvcv::legacy::helpers::GetLegacyDataType; + +TEST(LegacyHelpersTest, RejectsUnsupportedFormatsAndTypes) +{ + EXPECT_THROW(GetLegacyDataFormat(3, 2, 1), nvcv::Exception); + EXPECT_THROW(GetLegacyDataFormat(nvcv::TensorLayout("HW")), nvcv::Exception); + + EXPECT_THROW(GetLegacyDataType(24, nvcv::DataKind::FLOAT), nvcv::Exception); + EXPECT_THROW(GetLegacyDataType(24, nvcv::DataKind::SIGNED), nvcv::Exception); + EXPECT_THROW(GetLegacyDataType(8, nvcv::DataKind::COMPLEX), nvcv::Exception); + EXPECT_THROW(GetLegacyDataType(8, nvcv::DataKind::UNSPECIFIED), nvcv::Exception); +} + +TEST(LegacyHelpersTest, TranslatesSuccessAndUnknownErrors) +{ + EXPECT_EQ(nvcv::util::TranslateError(ErrorCode::SUCCESS), NVCV_SUCCESS); + EXPECT_EQ(nvcv::util::TranslateError(static_cast(99)), NVCV_ERROR_INTERNAL); +} + +TEST(LegacyHelpersTest, DescribesSuccessAndUnknownErrors) +{ + const char *description = nullptr; + + EXPECT_STREQ(nvcv::util::ToString(ErrorCode::SUCCESS, &description), "SUCCESS"); + EXPECT_STREQ(description, "Operation executed successfully"); + + EXPECT_STREQ(nvcv::util::ToString(static_cast(99), &description), "UNKNOWN"); + EXPECT_STREQ(description, "Unknown error"); +} + +} // namespace diff --git a/tests/cvcuda/unit/TestOpBoxBlur.cpp b/tests/cvcuda/unit/TestOpBoxBlur.cpp new file mode 100644 index 000000000..4206e3bf6 --- /dev/null +++ b/tests/cvcuda/unit/TestOpBoxBlur.cpp @@ -0,0 +1,114 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "Definitions.hpp" + +#include +#include +#include +#include + +#include +#include + +namespace legacy = nvcv::legacy::cuda_op; + +namespace { + +std::vector DownloadPixels(const nvcv::TensorDataAccessStridedImagePlanar &access) +{ + const auto samples = static_cast(access.numSamples()); + const auto rows = static_cast(access.numRows()); + const auto cols = static_cast(access.numCols()); + const auto channels = static_cast(access.numChannels()); + const auto rowBytes = cols * channels; + + std::vector pixels(samples * rows * rowBytes); + for (size_t n = 0; n < samples; ++n) + { + EXPECT_EQ( + cudaSuccess, + cudaMemcpy2D(pixels.data() + n * rows * rowBytes, rowBytes, access.sampleData(static_cast(n)), + static_cast(access.rowStride()), rowBytes, rows, cudaMemcpyDeviceToHost)); + } + return pixels; +} + +} // namespace + +TEST(OpBoxBlurPrivate, SkipCopyKeepsInputAsBlurSource) +{ + constexpr int numSamples = 1; + constexpr int width = 128; + constexpr int height = 96; + + nvcv::Tensor input(numSamples, {width, height}, nvcv::FMT_RGB8); + nvcv::Tensor normalOutput(numSamples, {width, height}, nvcv::FMT_RGB8); + nvcv::Tensor skipCopyOutput(numSamples, {width, height}, nvcv::FMT_RGB8); + + auto inputData = input.exportData(); + auto normalData = normalOutput.exportData(); + auto skipCopyData = skipCopyOutput.exportData(); + ASSERT_TRUE(inputData); + ASSERT_TRUE(normalData); + ASSERT_TRUE(skipCopyData); + + auto inputAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*inputData); + auto normalAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*normalData); + auto skipCopyAccess = nvcv::TensorDataAccessStridedImagePlanar::Create(*skipCopyData); + ASSERT_TRUE(inputAccess); + ASSERT_TRUE(normalAccess); + ASSERT_TRUE(skipCopyAccess); + + const auto bufferSize = static_cast(inputAccess->sampleStride() * inputAccess->numSamples()); + std::vector inputHost(bufferSize); + for (size_t i = 0; i < inputHost.size(); ++i) + { + inputHost[i] = static_cast((i * 31 + 7) & 0xFF); + } + + ASSERT_EQ(cudaSuccess, cudaMemcpy(inputData->basePtr(), inputHost.data(), bufferSize, cudaMemcpyHostToDevice)); + ASSERT_EQ(cudaSuccess, cudaMemcpy(skipCopyData->basePtr(), inputHost.data(), bufferSize, cudaMemcpyHostToDevice)); + + constexpr NVCVBlurBoxI box{ + {16, 12, 64, 64}, + 5 + }; + auto blurBoxes = std::make_shared(std::vector>{{box}}); + + // Corrupt the copied output inside the ROI. A skip-copy blur must still sample the immutable input tensor. + auto *corruptStart = skipCopyAccess->sampleData(0) + box.box.y * skipCopyAccess->rowStride() + + box.box.x * skipCopyAccess->colStride(); + ASSERT_EQ(cudaSuccess, cudaMemset2D(corruptStart, static_cast(skipCopyAccess->rowStride()), 0, + static_cast((box.box.width - 1) * skipCopyAccess->colStride()), + static_cast(box.box.height - 1))); + + cudaStream_t stream; + ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); + + legacy::DataShape maxInput; + legacy::DataShape maxOutput; + legacy::BoxBlur op(maxInput, maxOutput); + + EXPECT_EQ(legacy::ErrorCode::SUCCESS, op.infer(*inputData, *normalData, (NVCVBlurBoxesI)blurBoxes.get(), stream)); + EXPECT_EQ(legacy::ErrorCode::SUCCESS, + op.infer(*inputData, *skipCopyData, (NVCVBlurBoxesI)blurBoxes.get(), stream, true)); + ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); + + EXPECT_EQ(DownloadPixels(*normalAccess), DownloadPixels(*skipCopyAccess)); + EXPECT_EQ(cudaSuccess, cudaStreamDestroy(stream)); +} diff --git a/tests/cvcuda/unit/TestOpHQResize.cpp b/tests/cvcuda/unit/TestOpHQResize.cpp new file mode 100644 index 000000000..cc6895ddc --- /dev/null +++ b/tests/cvcuda/unit/TestOpHQResize.cpp @@ -0,0 +1,48 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "Definitions.hpp" + +#include + +namespace hqresize = cvcuda::priv::hq_resize; + +TEST(OpHQResizePolicy, UsesSeparableF32CubicContract2xOnSM75) +{ + using Path = hqresize::DirectTensorPath; + using Desc = hqresize::DirectTensorPathDesc; + + EXPECT_FALSE(hqresize::UseDirectTensorPathForSM(75, Path::CubicContract2x, true, 1)); + EXPECT_FALSE(hqresize::UseDirectTensorPathForSM(75, Path::CubicContract2x, true, 3)); + + const Desc cubicContract{false, true, false, 0.f, 0.f, 2.f, 2.f, 1920, 1080, 960, 540}; + EXPECT_FALSE(hqresize::ShouldUseDirectTensorPathForSM(75, cubicContract, true, 1)); + EXPECT_FALSE(hqresize::ShouldUseDirectTensorPathForSM(75, cubicContract, true, 3)); +} + +TEST(OpHQResizePolicy, KeepsDirectCubicContract2xForUnaffectedCases) +{ + using Path = hqresize::DirectTensorPath; + + EXPECT_TRUE(hqresize::UseDirectTensorPathForSM(75, Path::CubicContract2x, false, 1)); + EXPECT_TRUE(hqresize::UseDirectTensorPathForSM(75, Path::CubicContract2x, false, 3)); + EXPECT_TRUE(hqresize::UseDirectTensorPathForSM(75, Path::CubicContract2x, true, 2)); + EXPECT_TRUE(hqresize::UseDirectTensorPathForSM(75, Path::CubicContract2x, true, 4)); + EXPECT_TRUE(hqresize::UseDirectTensorPathForSM(75, Path::LinearExpand2x, true, 3)); + EXPECT_TRUE(hqresize::UseDirectTensorPathForSM(80, Path::CubicContract2x, true, 1)); + EXPECT_TRUE(hqresize::UseDirectTensorPathForSM(90, Path::CubicContract2x, true, 3)); +} diff --git a/tests/cvcuda/unit/TestOpHQResizePolicy.cpp b/tests/cvcuda/unit/TestOpHQResizePolicy.cpp new file mode 100644 index 000000000..7379b0474 --- /dev/null +++ b/tests/cvcuda/unit/TestOpHQResizePolicy.cpp @@ -0,0 +1,89 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "Definitions.hpp" + +#include + +namespace hqresize = cvcuda::priv::hq_resize; + +TEST(OpHQResizePolicy, ClassifiesOnlyExactDirectTensorPaths) +{ + using Path = hqresize::DirectTensorPath; + using Desc = hqresize::DirectTensorPathDesc; + + const Desc linearExpand{true, false, true, 0.f, 0.f, 0.5f, 0.5f, 1920, 1080, 3840, 2160}; + const Desc cubicContract{false, true, false, 0.f, 0.f, 2.f, 2.f, 1920, 1080, 960, 540}; + const Desc cubicExpand{false, true, true, 0.f, 0.f, 0.5f, 0.5f, 1920, 1080, 3840, 2160}; + + EXPECT_EQ(hqresize::ClassifyDirectTensorPath(linearExpand), Path::LinearExpand2x); + EXPECT_EQ(hqresize::ClassifyDirectTensorPath(cubicContract), Path::CubicContract2x); + EXPECT_EQ(hqresize::ClassifyDirectTensorPath(cubicExpand), Path::CubicExpand2x); + + Desc control = linearExpand; + control.scaleX = 0.6f; + EXPECT_EQ(hqresize::ClassifyDirectTensorPath(control), Path::Other); + control = cubicContract; + control.originX = 1.f; + EXPECT_EQ(hqresize::ClassifyDirectTensorPath(control), Path::Other); + control = cubicContract; + control.xFirst = true; + EXPECT_EQ(hqresize::ClassifyDirectTensorPath(control), Path::Other); + control = cubicExpand; + control.outHeight = 2159; + EXPECT_EQ(hqresize::ClassifyDirectTensorPath(control), Path::Other); + control = cubicExpand; + control.xFirst = false; + EXPECT_EQ(hqresize::ClassifyDirectTensorPath(control), Path::Other); + control = cubicExpand; + control.cubic = false; // Ineligible filter or nonzero ROI. + EXPECT_EQ(hqresize::ClassifyDirectTensorPath(control), Path::Other); +} + +TEST(OpHQResizePolicy, UsesSeparableForRegressedBlackwellTensorPaths) +{ + using Path = hqresize::DirectTensorPath; + using Desc = hqresize::DirectTensorPathDesc; + + EXPECT_FALSE(hqresize::UseDirectTensorPathForSM(100, Path::LinearExpand2x, true, 3)); + EXPECT_FALSE(hqresize::UseDirectTensorPathForSM(100, Path::CubicContract2x, true, 3)); + EXPECT_FALSE(hqresize::UseDirectTensorPathForSM(100, Path::CubicContract2x, false, 3)); + EXPECT_FALSE(hqresize::UseDirectTensorPathForSM(100, Path::CubicExpand2x, true, 1)); + EXPECT_FALSE(hqresize::UseDirectTensorPathForSM(103, Path::LinearExpand2x, true, 3)); + EXPECT_FALSE(hqresize::UseDirectTensorPathForSM(103, Path::CubicContract2x, true, 3)); + EXPECT_FALSE(hqresize::UseDirectTensorPathForSM(103, Path::CubicContract2x, false, 3)); + + const Desc linearExpand{true, false, true, 0.f, 0.f, 0.5f, 0.5f, 1920, 1080, 3840, 2160}; + EXPECT_FALSE(hqresize::ShouldUseDirectTensorPathForSM(100, linearExpand, true, 3)); + EXPECT_FALSE(hqresize::ShouldUseDirectTensorPathForSM(103, linearExpand, true, 3)); +} + +TEST(OpHQResizePolicy, KeepsDirectForUnaffectedTensorPaths) +{ + using Path = hqresize::DirectTensorPath; + + EXPECT_TRUE(hqresize::UseDirectTensorPathForSM(90, Path::LinearExpand2x, true, 3)); + EXPECT_TRUE(hqresize::UseDirectTensorPathForSM(103, Path::CubicExpand2x, true, 1)); + EXPECT_TRUE(hqresize::UseDirectTensorPathForSM(103, Path::LinearExpand2x, true, 1)); + EXPECT_TRUE(hqresize::UseDirectTensorPathForSM(103, Path::LinearExpand2x, false, 3)); + EXPECT_TRUE(hqresize::UseDirectTensorPathForSM(103, Path::CubicContract2x, true, 1)); + EXPECT_TRUE(hqresize::UseDirectTensorPathForSM(100, Path::LinearExpand2x, true, 1)); + EXPECT_TRUE(hqresize::UseDirectTensorPathForSM(100, Path::LinearExpand2x, false, 3)); + EXPECT_TRUE(hqresize::UseDirectTensorPathForSM(100, Path::CubicContract2x, true, 1)); + EXPECT_TRUE(hqresize::UseDirectTensorPathForSM(100, Path::CubicExpand2x, true, 3)); + EXPECT_TRUE(hqresize::UseDirectTensorPathForSM(100, Path::CubicExpand2x, false, 1)); +} diff --git a/tests/cvcuda/unit/TestOpInvertPolicy.cpp b/tests/cvcuda/unit/TestOpInvertPolicy.cpp new file mode 100644 index 000000000..29d93b2d5 --- /dev/null +++ b/tests/cvcuda/unit/TestOpInvertPolicy.cpp @@ -0,0 +1,36 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "Definitions.hpp" + +#include + +namespace priv = cvcuda::priv; + +TEST(OpInvertPolicy, UsesScalarU8C3VarShapeKernelOnL4) +{ + EXPECT_FALSE(priv::UsePackedU8C3VarShapeKernelForDevice(89, "NVIDIA L4")); +} + +TEST(OpInvertPolicy, KeepsPackedU8C3VarShapeKernelOnOtherDevices) +{ + EXPECT_TRUE(priv::UsePackedU8C3VarShapeKernelForDevice(89, "NVIDIA L40")); + EXPECT_TRUE(priv::UsePackedU8C3VarShapeKernelForDevice(89, "NVIDIA L40S")); + EXPECT_TRUE(priv::UsePackedU8C3VarShapeKernelForDevice(80, "NVIDIA A100-PCIE-40GB")); + EXPECT_TRUE(priv::UsePackedU8C3VarShapeKernelForDevice(90, "NVIDIA H100 PCIe")); + EXPECT_TRUE(priv::UsePackedU8C3VarShapeKernelForDevice(90, "NVIDIA L4")); +} diff --git a/tests/cvcuda/unit/TestPerStreamCache.cpp b/tests/cvcuda/unit/TestPerStreamCache.cpp index 17c4eebb3..a5ff3baef 100644 --- a/tests/cvcuda/unit/TestPerStreamCache.cpp +++ b/tests/cvcuda/unit/TestPerStreamCache.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -28,12 +28,17 @@ namespace { class Hog { public: - Hog(size_t size = 64 << 20) + explicit Hog(size_t size = 64 << 20) : m_size(size) { NVCV_CHECK_THROW(cudaMalloc(&m_buf, m_size)); } + Hog(const Hog &) = delete; + Hog(Hog &&) = delete; + Hog &operator=(const Hog &) = delete; + Hog &operator=(Hog &&) = delete; + ~Hog() { (void)cudaFree(m_buf); @@ -54,8 +59,9 @@ class Hog struct DummyPayload { - size_t size = 0, alignment = 1; - cudaEvent_t ready = nullptr; + size_t size = 0; + size_t alignment = 1; + cudaEvent_t ready = nullptr; }; using ItemAlloc = nvcv::util::detail::StreamCacheItemAllocator; @@ -78,10 +84,10 @@ TEST(StreamCacheItemAllocator, BasicTest) } else { - int n = items.size(); - std::uniform_int_distribution dist(0, n - 1); - int i = dist(rng); - std::swap(items[i], items.back()); + auto itemCount = static_cast(items.size()); + std::uniform_int_distribution dist(0, itemCount - 1); + int itemIndex = dist(rng); + std::swap(items[itemIndex], items.back()); alloc.deallocate(items.back()); items.pop_back(); } @@ -155,7 +161,7 @@ TEST(StreamOrderedCacheTest, FindNextReady) const int kMaxRetries = 10; int retries = kMaxRetries; - for (int split = 0; split < N; split += 20) + for (int split = 0; split < N; split += 20) // NOSONAR: split index drives cache readiness coverage. { events.clear(); @@ -182,10 +188,10 @@ TEST(StreamOrderedCacheTest, FindNextReady) if (split > 0) { - ASSERT_EQ(cudaSuccess, cudaEventSynchronize(events.events[split - 1])); + ASSERT_EQ(cudaSuccess, cudaEventSynchronize(events.events[split - 1].get())); } auto *item = cache.findNewestReady(); - if (cudaEventQuery(events.events[split]) == cudaSuccess) + if (cudaEventQuery(events.events[split].get()) == cudaSuccess) { if (--retries < 0) GTEST_SKIP() << "Unreliable test"; @@ -230,7 +236,7 @@ TEST(StreamOrderedCacheTest, RemoveAllReady) std::vector mask(N); - for (int split = 0; split < N; split += 20) + for (int split = 0; split < N; split += 20) // NOSONAR: split index drives cache readiness coverage. { events.clear(); for (int i = 0; i < N; i++) mask[i] = false; @@ -258,10 +264,10 @@ TEST(StreamOrderedCacheTest, RemoveAllReady) if (split > 0) { - ASSERT_EQ(cudaSuccess, cudaEventSynchronize(events.events[split - 1])); + ASSERT_EQ(cudaSuccess, cudaEventSynchronize(events.events[split - 1].get())); } - cache.removeAllReady([&](const DummyPayload &p) { mask[p.size] = true; }); - if (cudaEventQuery(events.events[split]) != cudaErrorNotReady) + cache.removeAllReady([&mask](const DummyPayload &p) { mask[p.size] = true; }); + if (cudaEventQuery(events.events[split].get()) != cudaErrorNotReady) { if (--retries < 0) GTEST_SKIP() << "Unreliable test"; @@ -271,9 +277,9 @@ TEST(StreamOrderedCacheTest, RemoveAllReady) continue; } retries = kMaxRetries; - for (int i = 0; i < N; i++) + for (int index = 0; index < N; index++) { - EXPECT_EQ(mask[i], (i < split)) << "@ i = " << i << " split = " << split; + EXPECT_EQ(mask[index], (index < split)) << "@ index = " << index << " split = " << split; } ASSERT_EQ(cudaSuccess, cudaDeviceSynchronize()); @@ -337,13 +343,13 @@ TEST(PerStreamCacheTest, TwoStream) cache.put(std::move(p3), s1.get()); cache.put(std::move(p4), s2.get()); auto e = std::chrono::high_resolution_clock::now(); - double insert_time = (e - s).count() / 4; + auto insert_time = static_cast((e - s).count()) / 4.0; double stream_get_time = 0; - s = std::chrono::high_resolution_clock::now(); - auto v0 = cache.get(1, 0, std::nullopt); - e = std::chrono::high_resolution_clock::now(); - double failed_get_time = (e - s).count(); + s = std::chrono::high_resolution_clock::now(); + auto v0 = cache.get(1, 0, std::nullopt); + e = std::chrono::high_resolution_clock::now(); + auto failed_get_time = static_cast((e - s).count()); if (v0.has_value()) { if (cudaSuccess == cudaEventQuery(p1.ready) || cudaSuccess == cudaEventQuery(p1.ready)) @@ -351,33 +357,33 @@ TEST(PerStreamCacheTest, TwoStream) EXPECT_FALSE(v0.has_value()) << "The resources are not ready - none should be returned for null stream."; } - s = std::chrono::high_resolution_clock::now(); - auto v1s1 = cache.get(1001, 0, s1); - e = std::chrono::high_resolution_clock::now(); - stream_get_time = (e - s).count(); + s = std::chrono::high_resolution_clock::now(); + auto v1s1 = cache.get(1001, 0, s1.get()); + e = std::chrono::high_resolution_clock::now(); + stream_get_time += static_cast((e - s).count()); ASSERT_TRUE(v1s1.has_value()); EXPECT_EQ(v1s1->size, 3000); - s = std::chrono::high_resolution_clock::now(); - auto v2s1 = cache.get(900, 0, s1); - e = std::chrono::high_resolution_clock::now(); - stream_get_time = (e - s).count(); + s = std::chrono::high_resolution_clock::now(); + auto v2s1 = cache.get(900, 0, s1.get()); + e = std::chrono::high_resolution_clock::now(); + stream_get_time += static_cast((e - s).count()); ASSERT_TRUE(v2s1.has_value()); EXPECT_EQ(v2s1->size, 1000); - s = std::chrono::high_resolution_clock::now(); - auto v1s2 = cache.get(900, 0, s2); - e = std::chrono::high_resolution_clock::now(); - stream_get_time = (e - s).count(); + s = std::chrono::high_resolution_clock::now(); + auto v1s2 = cache.get(900, 0, s2.get()); + e = std::chrono::high_resolution_clock::now(); + stream_get_time += static_cast((e - s).count()); stream_get_time /= 3; ASSERT_TRUE(v1s2.has_value()); EXPECT_EQ(v1s2->size, 2000); - ASSERT_EQ(cudaSuccess, cudaEventSynchronize(events.events[3])); - s = std::chrono::high_resolution_clock::now(); - auto v0ready = cache.get(1, 0, std::nullopt); - e = std::chrono::high_resolution_clock::now(); - double get_time = (e - s).count(); + ASSERT_EQ(cudaSuccess, cudaEventSynchronize(events.events[3].get())); + s = std::chrono::high_resolution_clock::now(); + auto v0ready = cache.get(1, 0, std::nullopt); + e = std::chrono::high_resolution_clock::now(); + auto get_time = static_cast((e - s).count()); ASSERT_TRUE(v0ready.has_value()); EXPECT_EQ(v0ready->size, 4000); diff --git a/tests/cvcuda/unit/TestSimpleCache.cpp b/tests/cvcuda/unit/TestSimpleCache.cpp index 387ed1227..00fd4e2c9 100644 --- a/tests/cvcuda/unit/TestSimpleCache.cpp +++ b/tests/cvcuda/unit/TestSimpleCache.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -37,12 +37,12 @@ struct Payload destroyed = true; } - Payload(Payload &&p) + Payload(Payload &&p) noexcept { *this = std::move(p); } - Payload &operator=(Payload &&p) + Payload &operator=(Payload &&p) noexcept { data = p.data; destroyed = p.destroyed; diff --git a/tests/cvcuda/unit/TestStreamId.cpp b/tests/cvcuda/unit/TestStreamId.cpp index f12f182dd..d87d74ec3 100644 --- a/tests/cvcuda/unit/TestStreamId.cpp +++ b/tests/cvcuda/unit/TestStreamId.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,11 +21,13 @@ #include #include +#include #include TEST(StreamIdTest, RegularAndDefault) { - cudaStream_t stream1 = 0, stream2 = 0; + cudaStream_t stream1 = nullptr; + cudaStream_t stream2 = nullptr; (void)cudaStreamCreateWithFlags(&stream1, cudaStreamNonBlocking); (void)cudaStreamCreateWithFlags(&stream2, cudaStreamNonBlocking); if (!stream1 || !stream2) @@ -39,7 +41,7 @@ TEST(StreamIdTest, RegularAndDefault) uint64_t id1 = nvcv::util::GetCudaStreamIdHint(stream1); uint64_t id2 = nvcv::util::GetCudaStreamIdHint(stream2); - uint64_t id3 = nvcv::util::GetCudaStreamIdHint(0); + uint64_t id3 = nvcv::util::GetCudaStreamIdHint(nullptr); EXPECT_NE(id1, id2); EXPECT_NE(id1, id3); EXPECT_NE(id2, id3); @@ -56,7 +58,7 @@ TEST(StreamIdTest, HandleReuse) struct CudaDeleter { - void operator()(void *p) + void operator()(std::byte *p) const { cudaFree(p); } @@ -66,11 +68,11 @@ TEST(StreamIdTest, HandleReuse) { void *ret = nullptr; cudaMalloc(&ret, size); - return ret; + return static_cast(ret); }; - size_t bufSize = 256 << 20; // 256MiB - std::unique_ptr mem(CudaAlloc(bufSize)); + size_t bufSize = 256 << 20; // 256MiB + std::unique_ptr mem(CudaAlloc(bufSize)); cudaEvent_t e; (void)cudaEventCreateWithFlags(&e, cudaEventDisableTiming); @@ -80,10 +82,11 @@ TEST(StreamIdTest, HandleReuse) for (int i = 0; i < maxAttempts; i++) { (void)cudaDeviceSynchronize(); - cudaStream_t stream1 = 0, stream2 = 0; + cudaStream_t stream1 = nullptr; + cudaStream_t stream2 = nullptr; (void)cudaStreamCreateWithFlags(&stream1, cudaStreamNonBlocking); uint64_t id1 = nvcv::util::GetCudaStreamIdHint(stream1); - for (int i = 0; i < 10; i++) cudaMemsetAsync(mem.get(), i, bufSize, stream1); + for (int fillValue = 0; fillValue < 10; fillValue++) cudaMemsetAsync(mem.get(), fillValue, bufSize, stream1); cudaEventRecord(e, stream1); if (stream1) (void)cudaStreamDestroy(stream1); @@ -110,14 +113,14 @@ TEST(StreamIdTest, HandleReuse) TEST(StreamIdTest, PerThreadDefault) { const int N = 4; - std::vector threads(N); + std::vector threads(N); // NOSONAR: std::jthread is C++20. std::vector ids(N); for (int i = 0; i < N; i++) { - threads[i] = std::thread( - [&, i]() + threads[i] = std::thread( // NOSONAR: std::jthread is C++20. + [&ids, i]() { - (void)cudaFree(0); // create/assign a context + (void)cudaFree(nullptr); // create/assign a context ids[i] = nvcv::util::GetCudaStreamIdHint(cudaStreamPerThread); }); } diff --git a/tests/cvcuda/unit/TestTextBackend.cpp b/tests/cvcuda/unit/TestTextBackend.cpp new file mode 100644 index 000000000..e294bfd88 --- /dev/null +++ b/tests/cvcuda/unit/TestTextBackend.cpp @@ -0,0 +1,209 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "Definitions.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +std::string get_ttf_path_from_family_name(const char *_font_family, const std::vector &search_paths); + +namespace { + +struct ScopedTempDir +{ + ScopedTempDir() + { + auto dirTemplate = std::to_array("/tmp/cvcuda-textbackend-XXXXXX"); + char *dir = ::mkdtemp(dirTemplate.data()); + if (dir != nullptr) + { + path = dir; + } + } + + ~ScopedTempDir() + { + std::error_code ec; + if (!path.empty()) + { + std::filesystem::remove_all(path, ec); + } + } + + ScopedTempDir(const ScopedTempDir &) = delete; + ScopedTempDir &operator=(const ScopedTempDir &) = delete; + ScopedTempDir(ScopedTempDir &&) = delete; + ScopedTempDir &operator=(ScopedTempDir &&) = delete; + + std::filesystem::path path; +}; + +void TouchFile(const std::filesystem::path &path) +{ + std::ofstream file(path); + file << "dummy"; +} + +} // namespace + +TEST(TextBackend, StbMeasureTextMatchesRenderedAdvance) +{ + auto backend = create_text_backend(TextBackendType::StbTrueType); + ASSERT_NE(backend, nullptr); + + constexpr int kFontSize = 60; + auto words = backend->split_utf8("Hello"); + ASSERT_FALSE(words.empty()); + + constexpr auto kDefaultFont = cvcuda::priv::DEFAULT_OSD_FONT; + + int measuredWidth = 0; + int measuredHeight = 0; + int measuredYOffset = 0; + std::tie(measuredWidth, measuredHeight, measuredYOffset) = backend->measure_text(words, kFontSize, kDefaultFont); + ASSERT_GT(measuredWidth, 0); + + backend->add_build_text(words, kFontSize, kDefaultFont); + backend->build_bitmap(); + + auto glyphMap = backend->query(kDefaultFont, kFontSize); + ASSERT_NE(glyphMap, nullptr); + + int renderedAdvance = 0; + for (auto word : words) + { + auto meta = glyphMap->query(word); + ASSERT_NE(meta, nullptr); + + if (meta->width() < 1 || meta->height() < 1) + renderedAdvance += meta->xadvance(kFontSize, true); + else + renderedAdvance += meta->xadvance(kFontSize); + } + + EXPECT_EQ(measuredWidth, renderedAdvance); +} + +TEST(TextBackend, StbSplitUtf8KeepsSupplementaryCodePoint) +{ + auto backend = create_text_backend(TextBackendType::StbTrueType); + ASSERT_NE(backend, nullptr); + + auto words = backend->split_utf8("\xF0\x9F\x98\x80"); + + ASSERT_EQ(words.size(), 1); + EXPECT_EQ(words[0], 0x1F600UL); +} + +TEST(TextBackend, StbSplitUtf8RejectsInvalidSequences) +{ + auto backend = create_text_backend(TextBackendType::StbTrueType); + ASSERT_NE(backend, nullptr); + + const std::array invalidUtf8 = { + "\x80", + "\xC0\x80", + "\xC2\x41", + "\xE0\x9F\x80", + "\xED\xA0\x80", + "\xE1\x41\x80", + "\xE1\x80\x41", + "\xF5\x80\x80\x80", + "\xF0\x80\x80\x80", + "\xF4\x90\x80\x80", + "\xF1\x41\x80\x80", + "\xF1\x80\x41\x80", + "\xF1\x80\x80\x41", + }; + + for (const char *text : invalidUtf8) + { + EXPECT_TRUE(backend->split_utf8(text).empty()); + } +} + +TEST(TextBackend, StbUninitializedAndUntrustedFontPathsFailSafely) +{ + auto backend = create_text_backend(TextBackendType::StbTrueType); + ASSERT_NE(backend, nullptr); + + EXPECT_EQ(backend->query("not-built", 12), nullptr); + EXPECT_EQ(backend->bitmap_device_pointer(), nullptr); + backend->build_bitmap(); + EXPECT_EQ(backend->bitmap_device_pointer(), nullptr); + + ScopedTempDir tempDir; + ASSERT_FALSE(tempDir.path.empty()); + auto untrustedFont = tempDir.path / "Untrusted.ttf"; + TouchFile(untrustedFont); + + auto words = backend->split_utf8("A"); + EXPECT_EQ(backend->measure_text(words, 12, untrustedFont.c_str()), std::make_tuple(-1, -1, -1)); +} + +TEST(TextBackend, StbFontFamilySearchContinuesAfterFirstNonEmptyDirectory) +{ + ScopedTempDir tempDir; + ASSERT_FALSE(tempDir.path.empty()); + + std::filesystem::path firstDir = tempDir.path / "first"; + std::filesystem::path secondDir = tempDir.path / "second"; + std::filesystem::create_directories(firstDir); + std::filesystem::create_directories(secondDir); + + TouchFile(firstDir / "OtherFont.ttf"); + TouchFile(secondDir / "TargetFont.ttf"); + + std::string path = get_ttf_path_from_family_name("TargetFont", {firstDir.string(), secondDir.string()}); + + EXPECT_EQ(path, (secondDir / "TargetFont.ttf").string()); +} + +TEST(TextBackend, StbFontFamilySearchHandlesEmptyAndFallbackCases) +{ + ScopedTempDir tempDir; + ASSERT_FALSE(tempDir.path.empty()); + + std::filesystem::path emptyDir = tempDir.path / "empty"; + std::filesystem::path fontDir = tempDir.path / "fonts"; + std::filesystem::path otherDir = tempDir.path / "other"; + std::filesystem::create_directories(emptyDir); + std::filesystem::create_directories(fontDir); + std::filesystem::create_directories(otherDir); + + EXPECT_TRUE(get_ttf_path_from_family_name("Missing", {emptyDir.string()}).empty()); + + TouchFile(fontDir / "x"); + TouchFile(fontDir / "DejaVuSansMono.ttf"); + + auto fallback = (fontDir / "DejaVuSansMono.ttf").string(); + EXPECT_EQ(get_ttf_path_from_family_name("", {fontDir.string()}), fallback); + EXPECT_EQ(get_ttf_path_from_family_name("Missing", {fontDir.string()}), fallback); + EXPECT_EQ(get_ttf_path_from_family_name("DejaVuSansMono.ttf", {fontDir.string()}), fallback); + + TouchFile(otherDir / "Other.ttf"); + EXPECT_EQ(get_ttf_path_from_family_name("Missing", {otherDir.string()}), (otherDir / "Other.ttf").string()); +} diff --git a/tests/cvcuda/unit/TestWorkspaceAllocator.cpp b/tests/cvcuda/unit/TestWorkspaceAllocator.cpp index 805752f07..f7b98ef22 100644 --- a/tests/cvcuda/unit/TestWorkspaceAllocator.cpp +++ b/tests/cvcuda/unit/TestWorkspaceAllocator.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,71 +19,74 @@ #include +#include +#include + #define EXPECT_PTR_EQ(a, b) EXPECT_EQ((const void *)(a), (const void *)(b)) TEST(WorkspaceMemAllocatorTest, Get) { - alignas(64) char base[64]; - cvcuda::WorkspaceMem wm{}; + alignas(64) std::array base; + cvcuda::WorkspaceMem wm{}; wm.req = {64, 64}; - wm.data = base; + wm.data = base.data(); cvcuda::WorkspaceMemAllocator wa(wm); - EXPECT_PTR_EQ(wa.get(3), base + 0); - EXPECT_PTR_EQ(wa.get(3), base + 4); - EXPECT_PTR_EQ(wa.get(), base + 16); - EXPECT_PTR_EQ(wa.get(1, 16), base + 32); - EXPECT_PTR_EQ(wa.get(4), base + 48); + EXPECT_PTR_EQ(wa.get(3), base.data() + 0); + EXPECT_PTR_EQ(wa.get(3), base.data() + 4); + EXPECT_PTR_EQ(wa.get(), base.data() + 16); + EXPECT_PTR_EQ(wa.get(1, 16), base.data() + 32); + EXPECT_PTR_EQ(wa.get(4), base.data() + 48); } TEST(WorkspaceMemAllocatorTest, ExceedWorkspaceSize) { - alignas(64) char base[64]; - cvcuda::WorkspaceMem wm{}; + alignas(64) std::array base; + cvcuda::WorkspaceMem wm{}; wm.req = {64, 64}; - wm.data = base; + wm.data = base.data(); cvcuda::WorkspaceMemAllocator wa(wm); - EXPECT_PTR_EQ(wa.get(4), base + 0); - EXPECT_PTR_EQ(wa.get(7), base + 32); + EXPECT_PTR_EQ(wa.get(4), base.data() + 0); + EXPECT_PTR_EQ(wa.get(7), base.data() + 32); EXPECT_PTR_EQ(wa.allocated(), 60); EXPECT_THROW(wa.get(2), nvcv::Exception); - EXPECT_PTR_EQ(wa.get(1), base + 60); + EXPECT_PTR_EQ(wa.get(1), base.data() + 60); EXPECT_THROW(wa.get(1), nvcv::Exception); } TEST(WorkspaceAllocatorTest, Get) { - alignas(64) char base[64]; - alignas(64) char pinnedBase[64]; - cvcuda::Workspace ws{}; + alignas(64) std::array base; + alignas(64) std::array pinnedBase; + cvcuda::Workspace ws{}; ws.hostMem.req = {64, 64}; - ws.hostMem.data = base; + ws.hostMem.data = base.data(); ws.pinnedMem.req = {64, 64}; - ws.pinnedMem.data = pinnedBase; + ws.pinnedMem.data = pinnedBase.data(); cvcuda::WorkspaceAllocator wa(ws); - EXPECT_PTR_EQ(wa.getHost(4), base + 0); - EXPECT_PTR_EQ(wa.getHost(7), base + 32); - EXPECT_PTR_EQ(wa.getPinned(4), pinnedBase + 0); + EXPECT_PTR_EQ(wa.getHost(4), base.data() + 0); + EXPECT_PTR_EQ(wa.getHost(7), base.data() + 32); + EXPECT_PTR_EQ(wa.getPinned(4), pinnedBase.data() + 0); EXPECT_EQ(wa.hostMem.allocated(), 60); EXPECT_EQ(wa.pinnedMem.allocated(), 32); EXPECT_THROW(wa.getHost(2), nvcv::Exception); - EXPECT_PTR_EQ(wa.getHost(1), base + 60); + EXPECT_PTR_EQ(wa.getHost(1), base.data() + 60); EXPECT_THROW(wa.getHost(1), nvcv::Exception); } TEST(WorkspaceMemAllocatorTest, AcquireRelease) { - alignas(64) char base[64]; - cvcuda::WorkspaceMem wm{}; + alignas(64) std::array base; + cvcuda::WorkspaceMem wm{}; wm.req = {64, 64}; - wm.data = base; + wm.data = base.data(); ASSERT_EQ(cudaEventCreateWithFlags(&wm.ready, cudaEventDisableTiming), cudaSuccess); EXPECT_NO_THROW({ cvcuda::WorkspaceMemAllocator wa(wm, cudaStream_t(0)); - EXPECT_PTR_EQ(wa.get(32), base); + EXPECT_PTR_EQ(wa.get(32), base.data()); }); EXPECT_NO_THROW({ cvcuda::WorkspaceMemAllocator wa(wm, cudaStream_t(0)); }); @@ -91,13 +94,13 @@ TEST(WorkspaceMemAllocatorTest, AcquireRelease) EXPECT_NO_THROW({ cvcuda::WorkspaceMemAllocator wa(wm, cudaStream_t(0)); wa.acquire(std::nullopt); - EXPECT_PTR_EQ(wa.get(32), base); + EXPECT_PTR_EQ(wa.get(32), base.data()); }); EXPECT_THROW( { cvcuda::WorkspaceMemAllocator wa(wm, std::nullopt, std::nullopt); - EXPECT_PTR_EQ(wa.get(32), base); + EXPECT_PTR_EQ(wa.get(32), base.data()); wa.acquire(std::nullopt); }, std::logic_error) @@ -116,7 +119,7 @@ TEST(WorkspaceMemAllocatorTest, AcquireRelease) { cvcuda::WorkspaceMemAllocator wa(wm, std::nullopt, std::nullopt); wa.release(std::nullopt); - EXPECT_PTR_EQ(wa.get(32), base); + EXPECT_PTR_EQ(wa.get(32), base.data()); }, std::logic_error) << "get after release should be an error"; @@ -144,19 +147,20 @@ TEST(WorkspaceMemAllocatorTest, AcquireRelease) TEST(WorkspaceMemAllocatorTest, Sync) { - void *_junk; - size_t junk_size = 100 << 20; - ASSERT_EQ(cudaMalloc(&_junk, junk_size), cudaSuccess); - std::unique_ptr junk(_junk, [](void *p) { EXPECT_EQ(cudaFree(p), cudaSuccess); }); - - alignas(64) char base[64]; - cvcuda::WorkspaceMem wm{}; + std::byte *junk_ptr = nullptr; + size_t junk_size = 100 << 20; + ASSERT_EQ(cudaMalloc(reinterpret_cast(&junk_ptr), junk_size), cudaSuccess); + std::unique_ptr junk(junk_ptr, + [](std::byte *p) { EXPECT_EQ(cudaFree(p), cudaSuccess); }); + + alignas(64) std::array base; + cvcuda::WorkspaceMem wm{}; wm.req = {64, 64}; - wm.data = base; + wm.data = base.data(); ASSERT_EQ(cudaEventCreateWithFlags(&wm.ready, cudaEventDisableTiming), cudaSuccess); // this is supposed to last long enough to be reliably "not ready" - auto hog = [&]() + auto hog = [&junk, &junk_size]() { for (int i = 0; i < 256; i++) { @@ -180,7 +184,7 @@ TEST(WorkspaceMemAllocatorTest, Sync) ASSERT_EQ(cudaEventRecord(wm.ready, 0), cudaSuccess); { cvcuda::WorkspaceMemAllocator wa(wm, cudaStream_t(0)); - EXPECT_PTR_EQ(wa.get(32), base); + EXPECT_PTR_EQ(wa.get(32), base.data()); hog(); } EXPECT_EQ(cudaEventQuery(wm.ready), cudaErrorNotReady); // device sync only @@ -194,7 +198,7 @@ TEST(WorkspaceMemAllocatorTest, Sync) { cvcuda::WorkspaceMemAllocator wa(wm, std::nullopt, std::nullopt); EXPECT_EQ(cudaEventQuery(wm.ready), cudaErrorNotReady); // no sync yet - EXPECT_PTR_EQ(wa.get(32), base); + EXPECT_PTR_EQ(wa.get(32), base.data()); EXPECT_EQ(cudaEventQuery(wm.ready), cudaSuccess); // sync in get } }) << "Acquire and release properly called, no exception should be raised"; diff --git a/tests/install_test_dependencies.sh b/tests/install_test_dependencies.sh new file mode 100755 index 000000000..eeb12c78b --- /dev/null +++ b/tests/install_test_dependencies.sh @@ -0,0 +1,143 @@ +#!/bin/bash -e + +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +# This script installs Python dependencies required to run CV-CUDA tests. +# When running inside a Docker devel image, all dependencies are pre-installed +# and this script is effectively a no-op. +# +# Usage: +# install_test_dependencies.sh [numpy1|numpy2] [cu12|cu13] +# +# Arguments: +# numpy1 - Install NumPy 1.x (Python 3.10-3.12 only) +# numpy2 - Install NumPy 2.x (Python 3.10-3.14, default) +# cu12 - Install CuPy/CUDA-Python for CUDA 12.x (compatible with CUDA 12.2+) +# cu13 - Install CuPy/CUDA-Python for CUDA 13.x (compatible with CUDA 13.3+) + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SEPARATOR_LINE="=======================================" + +NUMPY_MAJOR="${1:-numpy2}" +TORCH_CUDA="${2:-}" + +echo "$SEPARATOR_LINE" +echo "CV-CUDA Test Dependency Installer" +echo "$SEPARATOR_LINE" +echo "" + +# Validate numpy argument +case "$NUMPY_MAJOR" in + numpy1|numpy2) ;; + *) + echo "Error: Invalid first argument '$NUMPY_MAJOR'. Use 'numpy1' or 'numpy2'." >&2 + exit 1 + ;; +esac + +# Validate torch CUDA suffix if provided +if [[ -n "$TORCH_CUDA" ]]; then + case "$TORCH_CUDA" in + cu12|cu13) ;; + *) + echo "Error: Invalid second argument '$TORCH_CUDA'. Use 'cu12' (CUDA 12.2+) or 'cu13' (CUDA 13.x)." >&2 + exit 1 + ;; + esac +fi + +CUDA_REQUIREMENTS="" +if [[ -n "$TORCH_CUDA" ]]; then + if [[ "$NUMPY_MAJOR" == "numpy1" ]]; then + if [[ "$TORCH_CUDA" != "cu12" ]]; then + echo "Error: NumPy 1 test dependencies are supported only with cu12." >&2 + exit 1 + fi + CUDA_REQUIREMENTS="$SCRIPT_DIR/requirements.tests.cu12.numpy1.txt" + else + CUDA_REQUIREMENTS="$SCRIPT_DIR/requirements.tests.${TORCH_CUDA}.txt" + fi +fi + +# Check Python version +PYTHON_VERSION=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")') +echo "Detected Python version: $PYTHON_VERSION" + +# Check if pip is available +if ! python3 -m pip --version &> /dev/null; then + echo "Error: pip is not available. Please install python3-pip." >&2 + exit 1 +fi + +# Check if we're in a virtual environment (recommended) +if [[ -z "$VIRTUAL_ENV" ]]; then + echo "Note: Not running in a virtual environment." + echo "For isolated installations, consider using:" + echo " python3 -m venv venv_tests" + echo " source venv_tests/bin/activate" + echo "" +fi + +# Check that required requirements files have been generated. +REQ_FILES=( + "$SCRIPT_DIR/requirements.tests.common.txt" + "$SCRIPT_DIR/requirements.tests.${NUMPY_MAJOR}.txt" +) +[[ -n "$CUDA_REQUIREMENTS" ]] && REQ_FILES+=("$CUDA_REQUIREMENTS") + +MISSING=() +for f in "${REQ_FILES[@]}"; do + [[ -f "$f" ]] || MISSING+=("$f") +done +if [[ ${#MISSING[@]} -gt 0 ]]; then + echo "Error: the following requirements files are missing:" >&2 + for f in "${MISSING[@]}"; do echo " $f" >&2; done + echo "Generate them first by running from the repository root:" >&2 + echo " bash generate_requirements.sh" >&2 + exit 1 +fi + +echo "Installing test dependencies..." +python3 -m pip install -r "$SCRIPT_DIR/requirements.tests.common.txt" + +echo "" +echo "Installing NumPy ($NUMPY_MAJOR)..." +python3 -m pip install -r "$SCRIPT_DIR/requirements.tests.${NUMPY_MAJOR}.txt" + +if [[ -n "$CUDA_REQUIREMENTS" ]]; then + echo "" + echo "Installing CuPy/CUDA-Python ($TORCH_CUDA)..." + python3 -m pip install -r "$CUDA_REQUIREMENTS" +fi + +echo "" +echo "$SEPARATOR_LINE" +echo "Test dependency installation complete!" +echo "$SEPARATOR_LINE" +echo "" +if [[ -z "$TORCH_CUDA" ]]; then + echo "Note: CuPy/CUDA-Python was not installed. To install it, re-run with a CUDA suffix:" + echo " $0 $NUMPY_MAJOR cu12 # for CUDA 12.2+" + if [[ "$NUMPY_MAJOR" == "numpy2" ]]; then + echo " $0 $NUMPY_MAJOR cu13 # for CUDA 13.x (13.3+)" + fi + echo "" +fi +echo "To run tests, build the project with -DBUILD_TESTS=1 then:" +echo " build-rel/bin/run_tests.sh" +echo "" diff --git a/tests/nvcv_types/cudatools_system/DeviceTensorWrap.hpp b/tests/nvcv_types/cudatools_system/DeviceTensorWrap.hpp index 53672db04..d045e7efd 100644 --- a/tests/nvcv_types/cudatools_system/DeviceTensorWrap.hpp +++ b/tests/nvcv_types/cudatools_system/DeviceTensorWrap.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -41,64 +41,107 @@ template struct Array { // Array extends std::array to add extra properties using value_type = T; static constexpr int kNumDim = 1; - static constexpr int kStrides[kNumDim] = {sizeof(T)}; - static constexpr int kShapes[kNumDim] = {N}; + static constexpr int kStrides[kNumDim] = {sizeof(T)}; // NOSONAR: test data mirrors C array API. + static constexpr int kShapes[kNumDim] = {N}; // NOSONAR: test data mirrors C array API. static constexpr dim3 kBlocks = {N}; std::array m_data; T *data() { return m_data.data(); } const T *data() const { return m_data.data(); } const T& operator[](int i) const { return m_data[i]; } - bool operator==(const Array &that) const { return m_data == that.m_data; } + bool operator==(const Array &that) const // NOSONAR: defaulted comparisons are C++20. + { + return m_data == that.m_data; + } }; template struct PackedImage { // PackedImage extends std::array in two dimensions using value_type = T; static constexpr int kNumDim = 2; - static constexpr int kStrides[kNumDim] = {W * sizeof(T), sizeof(T)}; - static constexpr int kShapes[kNumDim] = {H, W}; + static constexpr int kStrides[kNumDim] = {W * sizeof(T), sizeof(T)}; // NOSONAR: test data mirrors C array API. + static constexpr int kShapes[kNumDim] = {H, W}; // NOSONAR: test data mirrors C array API. static constexpr dim3 kBlocks = {W, H}; std::array m_data; T *data() { return m_data.data(); } const T *data() const { return m_data.data(); } const T& operator[](int i) const { return m_data[i]; } - bool operator==(const PackedImage &that) const { return m_data == that.m_data; } + bool operator==(const PackedImage &that) const // NOSONAR: defaulted comparisons are C++20. + { + return m_data == that.m_data; + } }; template struct PackedTensor3D { // PackedTensor3D extends std::array in three dimensions using value_type = T; static constexpr int kNumDim = 3; - static constexpr int kStrides[kNumDim] = {H * W * sizeof(T), W * sizeof(T), sizeof(T)}; - static constexpr int kShapes[kNumDim] = {N, H, W}; + static constexpr int kStrides[kNumDim] = {H * W * sizeof(T), W * sizeof(T), sizeof(T)}; // NOSONAR + static constexpr int kShapes[kNumDim] = {N, H, W}; // NOSONAR: test data mirrors C array API. static constexpr dim3 kBlocks = {W, H, N}; std::array m_data; T *data() { return m_data.data(); } const T *data() const { return m_data.data(); } const T& operator[](int i) const { return m_data[i]; } - bool operator==(const PackedTensor3D &that) const { return m_data == that.m_data; } + bool operator==(const PackedTensor3D &that) const // NOSONAR: defaulted comparisons are C++20. + { + return m_data == that.m_data; + } }; template struct PackedTensor4D { // PackedTensor4D extends std::array in four dimensions using value_type = T; static constexpr int kNumDim = 4; - static constexpr int kStrides[kNumDim] = {H * W * C * sizeof(T), W * C * sizeof(T), C * sizeof(T), sizeof(T)}; - static constexpr int kShapes[kNumDim] = {N, H, W, C}; + static constexpr int kStrides[kNumDim] // NOSONAR: test data mirrors C array API. + = {H * W * C * sizeof(T), W * C * sizeof(T), C * sizeof(T), sizeof(T)}; + static constexpr int kShapes[kNumDim] = {N, H, W, C}; // NOSONAR: test data mirrors C array API. static constexpr dim3 kBlocks = {W, H, N}; std::array m_data; T *data() { return m_data.data(); } const T *data() const { return m_data.data(); } const T& operator[](int i) const { return m_data[i]; } - bool operator==(const PackedTensor4D &that) const { return m_data == that.m_data; } + bool operator==(const PackedTensor4D &that) const // NOSONAR: defaulted comparisons are C++20. + { + return m_data == that.m_data; + } }; // clang-format on +template +void ForEach4D(int n, int h, int w, int c, Func func) +{ + for (int i = 0, total = n * h * w * c; i < total; ++i) + { + int rest = i; + int k = rest % c; + rest /= c; + int x = rest % w; + rest /= w; + int y = rest % h; + int b = rest / h; + + func(b, y, x, k); + } +} + +template +int PackedOffset4D(int b, int y, int x, int k) +{ + return b * InputType::kShapes[1] * InputType::kShapes[2] * InputType::kShapes[3] + + y * InputType::kShapes[2] * InputType::kShapes[3] + x * InputType::kShapes[3] + k; +} + +template +auto StridedOffset4D(const TensorData &dev, int b, int y, int x, int k) +{ + return b * dev.stride(0) + y * dev.stride(1) + x * dev.stride(2) + k * dev.stride(3); +} + template void DeviceUseTensorWrap(const InputType &); diff --git a/tests/nvcv_types/cudatools_system/TestBorderVarShapeWrap.cpp b/tests/nvcv_types/cudatools_system/TestBorderVarShapeWrap.cpp index fd9f642f3..8b8e1e051 100644 --- a/tests/nvcv_types/cudatools_system/TestBorderVarShapeWrap.cpp +++ b/tests/nvcv_types/cudatools_system/TestBorderVarShapeWrap.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -84,7 +84,7 @@ TYPED_TEST(BorderVarShapeWrapTest, correct_fill) nvcv::ImageBatchVarShape dstImageBatch(samples); std::default_random_engine randEng{0}; - std::uniform_int_distribution randSize{-varSize, varSize}; + std::uniform_int_distribution randSize{-varSize, varSize}; std::uniform_int_distribution randValues{0, 255}; std::vector srcImageList; @@ -101,7 +101,7 @@ TYPED_TEST(BorderVarShapeWrapTest, correct_fill) int srcHeight = srcImageList[i].size().h; srcVec[i].resize(srcHeight * srcRowStride); - std::generate(srcVec[i].begin(), srcVec[i].end(), [&]() { return randValues(randEng); }); + std::ranges::generate(srcVec[i], [&randValues, &randEng]() { return randValues(randEng); }); ASSERT_EQ(cudaSuccess, cudaMemcpy2DAsync(srcData->plane(0).basePtr, srcRowStride, srcVec[i].data(), srcRowStride, @@ -203,6 +203,45 @@ NVCV_TYPED_TEST_SUITE(BorderVarShapeWrapNHWCTest, #undef NVCV_TEST_ROW +template +ValueType &GoldNHWCValueAt(std::vector &buffer, int rowStride, int numChannels, int x, int y, int c) +{ + return *reinterpret_cast( + &buffer[y * rowStride + x * sizeof(ValueType) * numChannels + c * sizeof(ValueType)]); +} + +template +void GoldFillNHWCPixel(std::vector &goldVec, int dstRowStride, int numChannels, + const std::vector &srcVec, int srcRowStride, nvcv::Size2D srcSize, int borderSize, + ValueType borderValue, int x, int y) +{ + int2 srcCoord{x - borderSize, y - borderSize}; + bool isInside = test::IsInside(srcCoord, {srcSize.w, srcSize.h}, BorderType); + + for (int c = 0; c < numChannels; ++c) + { + GoldNHWCValueAt(goldVec, dstRowStride, numChannels, x, y, c) + = isInside ? *reinterpret_cast( + &srcVec[srcCoord.y * srcRowStride + srcCoord.x * sizeof(ValueType) * numChannels + + c * sizeof(ValueType)]) + : borderValue; + } +} + +template +void GoldFillNHWC(std::vector &goldVec, int dstRowStride, int numChannels, const std::vector &srcVec, + int srcRowStride, nvcv::Size2D srcSize, nvcv::Size2D dstSize, int borderSize, ValueType borderValue) +{ + for (int y = 0; y < dstSize.h; ++y) + { + for (int x = 0; x < dstSize.w; ++x) + { + GoldFillNHWCPixel(goldVec, dstRowStride, numChannels, srcVec, srcRowStride, srcSize, + borderSize, borderValue, x, y); + } + } +} + TYPED_TEST(BorderVarShapeWrapNHWCTest, correct_fill) { cudaStream_t stream; @@ -226,7 +265,7 @@ TYPED_TEST(BorderVarShapeWrapNHWCTest, correct_fill) nvcv::ImageBatchVarShape dstImageBatch(samples); std::default_random_engine randEng{0}; - std::uniform_int_distribution randSize{-varSize, varSize}; + std::uniform_int_distribution randSize{-varSize, varSize}; std::uniform_int_distribution randValues{0, 255}; std::vector srcImageList; @@ -243,7 +282,7 @@ TYPED_TEST(BorderVarShapeWrapNHWCTest, correct_fill) int srcHeight = srcImageList[i].size().h; srcVec[i].resize(srcHeight * srcRowStride); - std::generate(srcVec[i].begin(), srcVec[i].end(), [&]() { return randValues(randEng); }); + std::ranges::generate(srcVec[i], [&randValues, &randEng]() { return randValues(randEng); }); ASSERT_EQ(cudaSuccess, cudaMemcpy2DAsync(srcData->plane(0).basePtr, srcRowStride, srcVec[i].data(), srcRowStride, @@ -305,28 +344,8 @@ TYPED_TEST(BorderVarShapeWrapNHWCTest, correct_fill) std::vector goldVec(dstSize.h * dstRowStride); // Run gold fill border - int2 srcCoord; - - for (int y = 0; y < dstSize.h; ++y) - { - srcCoord.y = y - borderSize; - - for (int x = 0; x < dstSize.w; ++x) - { - srcCoord.x = x - borderSize; - - bool isInside = test::IsInside(srcCoord, {srcSize.w, srcSize.h}, kBorderType); - for (int c = 0; c < numChannels; ++c) - { - *reinterpret_cast( - &goldVec[y * dstRowStride + x * sizeof(ValueType) * numChannels + c * sizeof(ValueType)]) - = isInside ? *reinterpret_cast( - &srcVec[i][srcCoord.y * srcRowStride + srcCoord.x * sizeof(ValueType) * numChannels - + c * sizeof(ValueType)]) - : borderValue; - } - } - } + GoldFillNHWC(goldVec, dstRowStride, numChannels, srcVec[i], srcRowStride, srcSize, + dstSize, borderSize, borderValue); EXPECT_EQ(testVec, goldVec); } diff --git a/tests/nvcv_types/cudatools_system/TestBorderWrap.cpp b/tests/nvcv_types/cudatools_system/TestBorderWrap.cpp index b4d53107a..89b0e205b 100644 --- a/tests/nvcv_types/cudatools_system/TestBorderWrap.cpp +++ b/tests/nvcv_types/cudatools_system/TestBorderWrap.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -90,8 +90,9 @@ TYPED_TEST(GetIndexWithBorderTests, correct_index) std::iota(test.begin(), test.end(), kBase); - std::transform(test.cbegin(), test.cend(), test.begin(), - [](const int &coord) { return cuda::GetIndexWithBorder(coord, kInputSize); }); + std::transform( // NOSONAR: std::ranges::transform is C++20. + test.cbegin(), test.cend(), test.begin(), + [](const int &coord) { return cuda::GetIndexWithBorder(coord, kInputSize); }); EXPECT_EQ(test, gold); } @@ -153,7 +154,8 @@ TYPED_TEST(BorderWrapNHWTest, correct_fill) ASSERT_TRUE(srcAccess); ASSERT_TRUE(dstAccess); - DimType srcSize, dstSize; + DimType srcSize; + DimType dstSize; srcSize.x = srcAccess->numCols(); dstSize.x = dstAccess->numCols(); @@ -161,8 +163,8 @@ TYPED_TEST(BorderWrapNHWTest, correct_fill) srcSize.y = srcAccess->numRows(); dstSize.y = dstAccess->numRows(); - srcSize.z = srcAccess->numSamples(); - dstSize.z = dstAccess->numSamples(); + srcSize.z = static_cast(srcAccess->numSamples()); + dstSize.z = static_cast(dstAccess->numSamples()); long3 srcStrides{srcAccess->sampleStride(), srcAccess->rowStride(), srcAccess->colStride()}; long3 dstStrides{dstAccess->sampleStride(), dstAccess->rowStride(), dstAccess->colStride()}; @@ -173,14 +175,14 @@ TYPED_TEST(BorderWrapNHWTest, correct_fill) dstStrides.x = dstAccess->numRows() * dstAccess->rowStride(); } - int srcSizeBytes = srcStrides.x * srcSize.z; - int dstSizeBytes = dstStrides.x * dstSize.z; + auto srcSizeBytes = static_cast(srcStrides.x * srcSize.z); + auto dstSizeBytes = static_cast(dstStrides.x * dstSize.z); std::vector srcVec(srcSizeBytes); std::default_random_engine randEng{0}; std::uniform_int_distribution srcRand{0u, 255u}; - std::generate(srcVec.begin(), srcVec.end(), [&]() { return srcRand(randEng); }); + std::ranges::generate(srcVec, [&srcRand, &randEng]() { return srcRand(randEng); }); ASSERT_EQ(cudaSuccess, cudaMemcpy(srcDev->basePtr(), srcVec.data(), srcVec.size(), cudaMemcpyHostToDevice)); diff --git a/tests/nvcv_types/cudatools_system/TestFullTensorWrap.cpp b/tests/nvcv_types/cudatools_system/TestFullTensorWrap.cpp index 354fb5a17..afc422c26 100644 --- a/tests/nvcv_types/cudatools_system/TestFullTensorWrap.cpp +++ b/tests/nvcv_types/cudatools_system/TestFullTensorWrap.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,6 +25,7 @@ #include // for Tensor, etc. #include // for TensorDataAccessStridedImagePlanar, etc. +#include #include #include @@ -154,9 +155,9 @@ TYPED_TEST(FullTensorWrap1DTensorTest, correct_with_tensor) EXPECT_EQ(wrap.strides()[0], dev->stride(0)); EXPECT_EQ(wrap.shapes()[0], dev->shape(0)); - const ValueType *ptr0 = reinterpret_cast(dev->basePtr()); - const ValueType *ptr1 - = reinterpret_cast(reinterpret_cast(dev->basePtr()) + dev->stride(0)); + auto ptr0 = reinterpret_cast(dev->basePtr()); + auto ptr1 + = reinterpret_cast(reinterpret_cast(dev->basePtr()) + dev->stride(0)); EXPECT_EQ(wrap.ptr(0), ptr0); EXPECT_EQ(wrap.ptr(1), ptr1); @@ -400,14 +401,14 @@ NVCV_TYPED_TEST_SUITE( TYPED_TEST(FullTensorWrap2DTensorTest, correct_with_tensor) { using ValueType = ttype::GetType; - auto imgFormat = ttype::GetValue; + nvcv::ImageFormat imgFormat{ttype::GetValue}; nvcv::Tensor tensor( nvcv::TensorShape{ {211, 213}, "HW" }, - nvcv::DataType{imgFormat}); + imgFormat.planeDataType(0)); auto dev = tensor.exportData(); ASSERT_NE(dev, nullptr); @@ -420,9 +421,9 @@ TYPED_TEST(FullTensorWrap2DTensorTest, correct_with_tensor) EXPECT_EQ(wrap.shapes()[i], dev->shape(i)); } - const ValueType *ptr0 = reinterpret_cast(dev->basePtr()); - const ValueType *ptr1 = reinterpret_cast(dev->basePtr() + dev->stride(0)); - const ValueType *ptr12 = reinterpret_cast(dev->basePtr() + dev->stride(0) + 2 * dev->stride(1)); + auto ptr0 = reinterpret_cast(dev->basePtr()); + auto ptr1 = reinterpret_cast(dev->basePtr() + dev->stride(0)); + auto ptr12 = reinterpret_cast(dev->basePtr() + dev->stride(0) + 2 * dev->stride(1)); EXPECT_EQ(wrap.ptr(0), ptr0); EXPECT_EQ(wrap.ptr(1), ptr1); @@ -432,14 +433,14 @@ TYPED_TEST(FullTensorWrap2DTensorTest, correct_with_tensor) TYPED_TEST(FullTensorWrap2DTensorTest, it_works_in_device) { using ValueType = std::remove_cv_t>; - auto imgFormat = ttype::GetValue; + nvcv::ImageFormat imgFormat{ttype::GetValue}; nvcv::Tensor tensor( nvcv::TensorShape{ {567, 234}, "HW" }, - nvcv::DataType{imgFormat}); + imgFormat.planeDataType(0)); cudaStream_t stream; ASSERT_EQ(cudaSuccess, cudaStreamCreate(&stream)); @@ -773,9 +774,9 @@ TYPED_TEST(FullTensorWrap3DTensorTest, correct_with_tensor) EXPECT_EQ(wrap.shapes()[i], dev->shape(i)); } - const ValueType *ptr0 = reinterpret_cast(dev->basePtr()); - const ValueType *ptr1 = reinterpret_cast(dev->basePtr() + dev->stride(0)); - const ValueType *ptr12 = reinterpret_cast(dev->basePtr() + dev->stride(0) + 2 * dev->stride(1)); + auto ptr0 = reinterpret_cast(dev->basePtr()); + auto ptr1 = reinterpret_cast(dev->basePtr() + dev->stride(0)); + auto ptr12 = reinterpret_cast(dev->basePtr() + dev->stride(0) + 2 * dev->stride(1)); EXPECT_EQ(wrap.ptr(0), ptr0); EXPECT_EQ(wrap.ptr(1), ptr1); @@ -872,53 +873,40 @@ TYPED_TEST(FullTensorWrap4DTest, correct_content_and_is_const) EXPECT_EQ(wrap.ptr(), input.data()); - for (int b = 0; b < InputType::kShapes[0]; ++b) - { - EXPECT_TRUE(std::is_pointer_v); - EXPECT_TRUE(std::is_const_v>); + ForEach4D( + InputType::kShapes[0], InputType::kShapes[1], InputType::kShapes[2], InputType::kShapes[3], + [&input, &wrap](int b, int y, int x, int k) + { + EXPECT_TRUE(std::is_pointer_v); + EXPECT_TRUE(std::is_const_v>); - EXPECT_EQ(wrap.ptr(b), &input[b * InputType::kShapes[1] * InputType::kShapes[2] * InputType::kShapes[3]]); + EXPECT_EQ(wrap.ptr(b), &input[b * InputType::kShapes[1] * InputType::kShapes[2] * InputType::kShapes[3]]); - for (int y = 0; y < InputType::kShapes[1]; ++y) - { EXPECT_TRUE(std::is_pointer_v); EXPECT_TRUE(std::is_const_v>); EXPECT_EQ(wrap.ptr(b, y), &input[b * InputType::kShapes[1] * InputType::kShapes[2] * InputType::kShapes[3] + y * InputType::kShapes[2] * InputType::kShapes[3]]); - for (int x = 0; x < InputType::kShapes[2]; ++x) - { - EXPECT_TRUE(std::is_pointer_v); - EXPECT_TRUE(std::is_const_v>); + EXPECT_TRUE(std::is_pointer_v); + EXPECT_TRUE(std::is_const_v>); - EXPECT_EQ(wrap.ptr(b, y, x), - &input[b * InputType::kShapes[1] * InputType::kShapes[2] * InputType::kShapes[3] - + y * InputType::kShapes[2] * InputType::kShapes[3] + x * InputType::kShapes[3]]); + EXPECT_EQ(wrap.ptr(b, y, x), + &input[b * InputType::kShapes[1] * InputType::kShapes[2] * InputType::kShapes[3] + + y * InputType::kShapes[2] * InputType::kShapes[3] + x * InputType::kShapes[3]]); - for (int k = 0; k < InputType::kShapes[3]; ++k) - { - EXPECT_TRUE(std::is_pointer_v); - EXPECT_TRUE(std::is_const_v>); + EXPECT_TRUE(std::is_pointer_v); + EXPECT_TRUE(std::is_const_v>); - EXPECT_EQ( - wrap.ptr(b, y, x, k), - &input[b * InputType::kShapes[1] * InputType::kShapes[2] * InputType::kShapes[3] - + y * InputType::kShapes[2] * InputType::kShapes[3] + x * InputType::kShapes[3] + k]); + EXPECT_EQ(wrap.ptr(b, y, x, k), &input[PackedOffset4D(b, y, x, k)]); - int4 c4{k, x, y, b}; + int4 c4{k, x, y, b}; - EXPECT_TRUE(std::is_reference_v); - EXPECT_TRUE(std::is_const_v>); + EXPECT_TRUE(std::is_reference_v); + EXPECT_TRUE(std::is_const_v>); - EXPECT_EQ( - wrap[c4], - input[b * InputType::kShapes[1] * InputType::kShapes[2] * InputType::kShapes[3] - + y * InputType::kShapes[2] * InputType::kShapes[3] + x * InputType::kShapes[3] + k]); - } - } - } - } + EXPECT_EQ(wrap[c4], input[PackedOffset4D(b, y, x, k)]); + }); } TYPED_TEST(FullTensorWrap4DTest, it_works_in_device) @@ -963,22 +951,13 @@ TYPED_TEST(FullTensorWrap4DCopyTest, can_change_content) ASSERT_EQ(InputType::kShapes[i], decltype(gold)::kShapes[i]); } - for (int b = 0; b < InputType::kShapes[0]; ++b) - { - for (int y = 0; y < InputType::kShapes[1]; ++y) - { - for (int x = 0; x < InputType::kShapes[2]; ++x) - { - for (int k = 0; k < InputType::kShapes[3]; ++k) - { - int4 c{k, x, y, b}; + ForEach4D(InputType::kShapes[0], InputType::kShapes[1], InputType::kShapes[2], InputType::kShapes[3], + [&gold, &wrap](int b, int y, int x, int k) + { + int4 c{k, x, y, b}; - wrap[c] = gold[b * InputType::kShapes[1] * InputType::kShapes[2] * InputType::kShapes[3] - + y * InputType::kShapes[2] * InputType::kShapes[3] + x * InputType::kShapes[3] + k]; - } - } - } - } + wrap[c] = gold[PackedOffset4D(b, y, x, k)]; + }); EXPECT_EQ(test, gold); } @@ -1040,23 +1019,14 @@ TYPED_TEST(FullTensorWrap4DTensorWrapTest, correct_with_tensor_wrap) EXPECT_EQ(wrap.shapes()[i], InputType::kShapes[i]); } - for (int b = 0; b < N; ++b) - { - for (int y = 0; y < H; ++y) - { - for (int x = 0; x < W; ++x) - { - for (int k = 0; k < C; ++k) - { - int4 c4{k, x, y, b}; - - EXPECT_EQ(wrap[c4], - *reinterpret_cast(dev->basePtr() + b * dev->stride(0) + y * dev->stride(1) - + x * dev->stride(2) + k * dev->stride(3))); - } - } - } - } + ForEach4D(N, H, W, C, + [&dev, &wrap](int b, int y, int x, int k) + { + int4 c4{k, x, y, b}; + + EXPECT_EQ(wrap[c4], + *reinterpret_cast(dev->basePtr() + StridedOffset4D(*dev, b, y, x, k))); + }); } // clang-format off @@ -1096,11 +1066,11 @@ TYPED_TEST(FullTensorWrap4DTensorTest, correct_with_tensor) EXPECT_EQ(wrap.shapes()[i], dev->shape(i)); } - const ValueType *ptr0 = reinterpret_cast(dev->basePtr()); - const ValueType *ptr1 = reinterpret_cast(dev->basePtr() + dev->stride(0)); - const ValueType *ptr12 = reinterpret_cast(dev->basePtr() + dev->stride(0) + 2 * dev->stride(1)); - const ValueType *ptr123 = reinterpret_cast(dev->basePtr() + dev->stride(0) + 2 * dev->stride(1) - + 3 * dev->stride(2)); + auto ptr0 = reinterpret_cast(dev->basePtr()); + auto ptr1 = reinterpret_cast(dev->basePtr() + dev->stride(0)); + auto ptr12 = reinterpret_cast(dev->basePtr() + dev->stride(0) + 2 * dev->stride(1)); + auto ptr123 = reinterpret_cast(dev->basePtr() + dev->stride(0) + 2 * dev->stride(1) + + 3 * dev->stride(2)); EXPECT_EQ(wrap.ptr(0), ptr0); EXPECT_EQ(wrap.ptr(1), ptr1); @@ -1141,21 +1111,11 @@ TYPED_TEST(FullTensorWrap4DTensorTest, it_works_in_device) std::vector test(sizeBytes); std::vector gold(sizeBytes); - for (int b = 0; b < dev->shape(0); b++) - { - for (int i = 0; i < dev->shape(1); i++) - { - for (int j = 0; j < dev->shape(2); j++) - { - for (int k = 0; k < dev->shape(3); k++) - { - *reinterpret_cast( - &gold[b * dev->stride(0) + i * dev->stride(1) + j * dev->stride(2) + k * dev->stride(3)]) - = cuda::SetAll(1); - } - } - } - } + ForEach4D(static_cast(dev->shape(0)), static_cast(dev->shape(1)), static_cast(dev->shape(2)), + static_cast(dev->shape(3)), + [&dev, &gold](int b, int i, int j, int k) { + *reinterpret_cast(&gold[StridedOffset4D(*dev, b, i, j, k)]) = cuda::SetAll(1); + }); // Get test data back ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); @@ -1222,7 +1182,8 @@ TYPED_TEST(BorderWrapFullTensorWrap3DTest, correct_fill) ASSERT_TRUE(srcAccess); ASSERT_TRUE(dstAccess); - DimType srcSize, dstSize; + DimType srcSize; + DimType dstSize; srcSize.x = srcAccess->numCols(); dstSize.x = dstAccess->numCols(); @@ -1230,17 +1191,17 @@ TYPED_TEST(BorderWrapFullTensorWrap3DTest, correct_fill) srcSize.y = srcAccess->numRows(); dstSize.y = dstAccess->numRows(); - srcSize.z = srcAccess->numSamples(); - dstSize.z = dstAccess->numSamples(); + srcSize.z = static_cast(srcAccess->numSamples()); + dstSize.z = static_cast(dstAccess->numSamples()); - int srcSizeBytes = srcDev->stride(0) * srcSize.z; - int dstSizeBytes = dstDev->stride(0) * dstSize.z; + auto srcSizeBytes = static_cast(srcDev->stride(0) * srcSize.z); + auto dstSizeBytes = static_cast(dstDev->stride(0) * dstSize.z); std::vector srcVec(srcSizeBytes); std::default_random_engine randEng{0}; std::uniform_int_distribution srcRand{0u, 255u}; - std::generate(srcVec.begin(), srcVec.end(), [&]() { return srcRand(randEng); }); + std::ranges::generate(srcVec, [&srcRand, &randEng]() { return srcRand(randEng); }); ASSERT_EQ(cudaSuccess, cudaMemcpy(srcDev->basePtr(), srcVec.data(), srcVec.size(), cudaMemcpyHostToDevice)); @@ -1339,7 +1300,8 @@ TYPED_TEST(BorderWrapFullTensorWrap4DTest, correct_fill) ASSERT_TRUE(srcAccess); ASSERT_TRUE(dstAccess); - DimType srcSize, dstSize; + DimType srcSize; + DimType dstSize; srcSize.x = srcAccess->numCols(); dstSize.x = dstAccess->numCols(); @@ -1347,20 +1309,20 @@ TYPED_TEST(BorderWrapFullTensorWrap4DTest, correct_fill) srcSize.y = srcAccess->numRows(); dstSize.y = dstAccess->numRows(); - srcSize.z = srcAccess->numSamples(); - dstSize.z = dstAccess->numSamples(); + srcSize.z = static_cast(srcAccess->numSamples()); + dstSize.z = static_cast(dstAccess->numSamples()); srcSize.w = srcAccess->numChannels(); dstSize.w = dstAccess->numChannels(); - int srcSizeBytes = srcDev->stride(0) * srcSize.z; - int dstSizeBytes = dstDev->stride(0) * dstSize.z; + auto srcSizeBytes = static_cast(srcDev->stride(0) * srcSize.z); + auto dstSizeBytes = static_cast(dstDev->stride(0) * dstSize.z); std::vector srcVec(srcSizeBytes); std::default_random_engine randEng{0}; std::uniform_int_distribution srcRand{0u, 255u}; - std::generate(srcVec.begin(), srcVec.end(), [&]() { return srcRand(randEng); }); + std::ranges::generate(srcVec, [&srcRand, &randEng]() { return srcRand(randEng); }); ASSERT_EQ(cudaSuccess, cudaMemcpy(srcDev->basePtr(), srcVec.data(), srcVec.size(), cudaMemcpyHostToDevice)); @@ -1387,32 +1349,21 @@ TYPED_TEST(BorderWrapFullTensorWrap4DTest, correct_fill) ASSERT_EQ(cudaSuccess, cudaMemcpy(test.data(), dstDev->basePtr(), test.size(), cudaMemcpyDeviceToHost)); // Run gold fill border - for (int z = 0; z < dstSize.z; ++z) - { - int2 srcCoord; - - for (int y = 0; y < dstSize.y; ++y) - { - srcCoord.y = y - borderSize; + ForEach4D(dstSize.z, dstSize.y, dstSize.x, dstSize.w, + [&chBorderValue, &dstDev, &gold, &srcDev, &srcVec, borderSize, height, width](int z, int y, int x, int w) + { + int2 srcCoord; - for (int x = 0; x < dstSize.x; ++x) - { - srcCoord.x = x - borderSize; + srcCoord.y = y - borderSize; + srcCoord.x = x - borderSize; - bool isInside = test::IsInside(srcCoord, {width, height}, kBorderType); + bool isInside = test::IsInside(srcCoord, {width, height}, kBorderType); - for (int w = 0; w < dstSize.w; ++w) - { - *reinterpret_cast(&gold[z * dstDev->stride(0) + y * dstDev->stride(1) - + x * dstDev->stride(2) + w * dstDev->stride(3)]) - = isInside ? *reinterpret_cast( - &srcVec[z * srcDev->stride(0) + srcCoord.y * srcDev->stride(1) - + srcCoord.x * srcDev->stride(2) + w * srcDev->stride(3)]) - : chBorderValue; - } - } - } - } + *reinterpret_cast(&gold[StridedOffset4D(*dstDev, z, y, x, w)]) + = isInside ? *reinterpret_cast( + &srcVec[StridedOffset4D(*srcDev, z, srcCoord.y, srcCoord.x, w)]) + : chBorderValue; + }); EXPECT_EQ(test, gold); } diff --git a/tests/nvcv_types/cudatools_system/TestImageBatchVarShapeWrap.cpp b/tests/nvcv_types/cudatools_system/TestImageBatchVarShapeWrap.cpp index 33876ac20..f85c8b510 100644 --- a/tests/nvcv_types/cudatools_system/TestImageBatchVarShapeWrap.cpp +++ b/tests/nvcv_types/cudatools_system/TestImageBatchVarShapeWrap.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -29,6 +29,20 @@ namespace cuda = nvcv::cuda; namespace ttype = nvcv::test::type; +template +void FillGoldImage(std::vector &gold, int width, int height, int rowStride, int numChannels) +{ + for (int i = 0, total = width * height * numChannels; i < total; ++i) + { + int k = i % numChannels; + int pixel = i / numChannels; + int x = pixel % width; + int y = pixel / width; + + *reinterpret_cast(&gold[y * rowStride + x * sizeof(T) * numChannels + k * sizeof(T)]) = cuda::SetAll(2); + } +} + // --------------------- Testing ImageBatchVarShapeWrap ------------------------ #define NVCV_TEST_ROW(WIDTH, HEIGHT, VARSIZE, SAMPLES, FORMAT, TYPE) \ @@ -71,8 +85,8 @@ NVCV_MIXTYPED_TEST(ImageBatchVarShapeWrapTest, correct_content) nvcv::ImageBatchVarShape imageBatch(samples); - std::default_random_engine randEng{0}; - std::uniform_int_distribution rand{-varSize, varSize}; + std::default_random_engine randEng{0}; + std::uniform_int_distribution rand{-varSize, varSize}; std::list imageList; @@ -105,26 +119,20 @@ NVCV_MIXTYPED_TEST(ImageBatchVarShapeWrapTest, correct_content) for (int s = 0; s < samples; s++) { - void *testBuffer = testPlanes[s].planes[0].basePtr; + const void *testBuffer = testPlanes[s].planes[0].basePtr; - int width = testPlanes[s].planes[0].width; - int height = testPlanes[s].planes[0].height; + int sampleWidth = testPlanes[s].planes[0].width; + int sampleHeight = testPlanes[s].planes[0].height; int rowStride = testPlanes[s].planes[0].rowStride; - int sizeBytes = rowStride * height; + int sizeBytes = rowStride * sampleHeight; std::vector test(sizeBytes); std::vector gold(sizeBytes); ASSERT_EQ(cudaSuccess, cudaMemcpy(test.data(), testBuffer, sizeBytes, cudaMemcpyDeviceToHost)); - for (int y = 0; y < height; y++) - { - for (int x = 0; x < width; x++) - { - *reinterpret_cast(&gold[y * rowStride + x * sizeof(T)]) = cuda::SetAll(2); - } - } + FillGoldImage(gold, sampleWidth, sampleHeight, rowStride, 1); EXPECT_EQ(test, gold); } @@ -164,8 +172,8 @@ NVCV_MIXTYPED_TEST(ImageBatchVarShapeWrapNHWCTest, correct_content) nvcv::ImageBatchVarShape imageBatch(samples); - std::default_random_engine randEng{0}; - std::uniform_int_distribution rand{-varSize, varSize}; + std::default_random_engine randEng{0}; + std::uniform_int_distribution rand{-varSize, varSize}; std::list imageList; @@ -200,30 +208,20 @@ NVCV_MIXTYPED_TEST(ImageBatchVarShapeWrapNHWCTest, correct_content) for (int s = 0; s < samples; s++) { - void *testBuffer = testPlanes[s].planes[0].basePtr; + const void *testBuffer = testPlanes[s].planes[0].basePtr; - int width = testPlanes[s].planes[0].width; - int height = testPlanes[s].planes[0].height; + int sampleWidth = testPlanes[s].planes[0].width; + int sampleHeight = testPlanes[s].planes[0].height; int rowStride = testPlanes[s].planes[0].rowStride; - int sizeBytes = rowStride * height; + int sizeBytes = rowStride * sampleHeight; std::vector test(sizeBytes); std::vector gold(sizeBytes); ASSERT_EQ(cudaSuccess, cudaMemcpy(test.data(), testBuffer, sizeBytes, cudaMemcpyDeviceToHost)); - for (int y = 0; y < height; y++) - { - for (int x = 0; x < width; x++) - { - for (int k = 0; k < numChannels; k++) - { - *reinterpret_cast(&gold[y * rowStride + x * sizeof(T) * numChannels + k * sizeof(T)]) - = cuda::SetAll(2); - } - } - } + FillGoldImage(gold, sampleWidth, sampleHeight, rowStride, numChannels); EXPECT_EQ(test, gold); } diff --git a/tests/nvcv_types/cudatools_system/TestInterpolationVarShapeWrap.cpp b/tests/nvcv_types/cudatools_system/TestInterpolationVarShapeWrap.cpp index 70e04dbe9..73edadaf2 100644 --- a/tests/nvcv_types/cudatools_system/TestInterpolationVarShapeWrap.cpp +++ b/tests/nvcv_types/cudatools_system/TestInterpolationVarShapeWrap.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -113,7 +113,7 @@ TYPED_TEST(InterpolationVarShapeWrapTest, correct_shift) nvcv::ImageBatchVarShape dstImageBatch(batches); std::default_random_engine randEng{0}; - std::uniform_int_distribution randSize{-varSize, varSize}; + std::uniform_int_distribution randSize{-varSize, varSize}; std::uniform_int_distribution randValues{0, 255}; std::vector srcImageList; @@ -130,7 +130,7 @@ TYPED_TEST(InterpolationVarShapeWrapTest, correct_shift) int srcHeight = srcImageList[i].size().h; srcVec[i].resize(srcHeight * srcRowStride); - std::generate(srcVec[i].begin(), srcVec[i].end(), [&]() { return randValues(randEng); }); + std::ranges::generate(srcVec[i], [&randValues, &randEng]() { return randValues(randEng); }); ASSERT_EQ(cudaSuccess, cudaMemcpy2DAsync(srcData->plane(0).basePtr, srcRowStride, srcVec[i].data(), srcRowStride, @@ -186,15 +186,15 @@ TYPED_TEST(InterpolationVarShapeWrapTest, correct_shift) const auto dstData = dstImageList[i].exportData(); ASSERT_EQ(dstData->numPlanes(), 1); - int2 srcSize = int2{srcImageList[i].size().w, srcImageList[i].size().h}; - int2 dstSize = int2{dstImageList[i].size().w, dstImageList[i].size().h}; + auto srcSize = int2{srcImageList[i].size().w, srcImageList[i].size().h}; + auto dstSize = int2{dstImageList[i].size().w, dstImageList[i].size().h}; ASSERT_EQ(srcSize, dstSize); int srcRowStride = srcData->plane(0).rowStride; int dstRowStride = dstData->plane(0).rowStride; - long2 srcStrides = long2{srcRowStride, sizeof(ValueType)}; - long2 dstStrides = long2{dstRowStride, sizeof(ValueType)}; + auto srcStrides = long2{srcRowStride, sizeof(ValueType)}; + auto dstStrides = long2{dstRowStride, sizeof(ValueType)}; std::vector testVec(dstSize.y * dstRowStride, 0); std::vector goldVec(dstSize.y * dstRowStride, 0); @@ -209,11 +209,11 @@ TYPED_TEST(InterpolationVarShapeWrapTest, correct_shift) for (dstCoord.y = 0; dstCoord.y < dstSize.y; ++dstCoord.y) { - srcCoord.y = dstCoord.y + shiftY; + srcCoord.y = static_cast(dstCoord.y) + shiftY; for (dstCoord.x = 0; dstCoord.x < dstSize.x; ++dstCoord.x) { - srcCoord.x = dstCoord.x + shiftX; + srcCoord.x = static_cast(dstCoord.x) + shiftX; test::ValueAt(goldVec, dstStrides, dstCoord) = test::GoldInterp( srcVec[i], srcStrides, srcSize, borderValue, scale, srcCoord); diff --git a/tests/nvcv_types/cudatools_system/TestInterpolationWrap.cpp b/tests/nvcv_types/cudatools_system/TestInterpolationWrap.cpp index 3756921ab..f5f3f1022 100644 --- a/tests/nvcv_types/cudatools_system/TestInterpolationWrap.cpp +++ b/tests/nvcv_types/cudatools_system/TestInterpolationWrap.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -44,6 +44,159 @@ constexpr auto RGBA8 = NVCV_IMAGE_FORMAT_RGBA8; constexpr auto RGBf32 = NVCV_IMAGE_FORMAT_RGBf32; constexpr auto RGBAf32 = NVCV_IMAGE_FORMAT_RGBAf32; +template +auto BorderValueAt(BorderWrap &borderWrap, int x, int y, int z = 0, int k = 0) +{ + return borderWrap[test::GetCoord(x, y, z, k)]; +} + +template +ValueType HostGoldInterpCubic(BorderWrap &borderWrap, float2 coord, int z = 0, int k = 0) +{ + int ix = cuda::round(coord.x); + int iy = cuda::round(coord.y); + + using FT = cuda::ConvertBaseTypeTo; + auto sum = cuda::SetAll(0); + + std::array wx; + test::GetBicubicCoeffs(coord.x - static_cast(ix), wx[0], wx[1], wx[2], wx[3]); + std::array wy; + test::GetBicubicCoeffs(coord.y - static_cast(iy), wy[0], wy[1], wy[2], wy[3]); + + for (int i = 0; i < 16; ++i) + { + int cx = i % 4 - 1; + int cy = i / 4 - 1; + sum += BorderValueAt(borderWrap, ix + cx, iy + cy, z, k) * (wx[cx + 1] * wy[cy + 1]); + } + + return cuda::SaturateCast(sum); +} + +template +ValueType HostGoldInterpArea(BorderWrap &borderWrap, float2 scale, float2 coord, int z = 0, int k = 0) +{ + int xmin = cuda::round(coord.x * scale.x); + int xmax = cuda::round((coord.x + 1) * scale.x); + int ymin = cuda::round(coord.y * scale.y); + int ymax = cuda::round((coord.y + 1) * scale.y); + + auto out = cuda::SetAll>(0); + + int width = std::max(0, xmax - xmin); + int height = std::max(0, ymax - ymin); + + for (int i = 0; i < width * height; ++i) + { + int cx = xmin + i % width; + int cy = ymin + i / width; + out += BorderValueAt(borderWrap, cx, cy, z, k) * (1.f / (scale.x * scale.y)); + } + + return cuda::SaturateCast(out); +} + +template +ValueType HostGoldInterp(BorderWrap &borderWrap, float2 scale, float2 coord, int z = 0, int k = 0) +{ + if constexpr (I == NVCV_INTERP_NEAREST) + { + int2 c = cuda::round(coord + .5f); + return BorderValueAt(borderWrap, c.x, c.y, z, k); + } + else if constexpr (I == NVCV_INTERP_LINEAR) + { + int2 c1 = cuda::round(coord); + int2 c2 = c1 + 1; + + auto out = cuda::SetAll>(0); + + const auto c1x = static_cast(c1.x); + const auto c1y = static_cast(c1.y); + const auto c2x = static_cast(c2.x); + const auto c2y = static_cast(c2.y); + + out += BorderValueAt(borderWrap, c1.x, c1.y, z, k) * (c2x - coord.x) * (c2y - coord.y); + out += BorderValueAt(borderWrap, c2.x, c1.y, z, k) * (coord.x - c1x) * (c2y - coord.y); + out += BorderValueAt(borderWrap, c1.x, c2.y, z, k) * (c2x - coord.x) * (coord.y - c1y); + out += BorderValueAt(borderWrap, c2.x, c2.y, z, k) * (coord.x - c1x) * (coord.y - c1y); + + return cuda::SaturateCast(out); + } + else if constexpr (I == NVCV_INTERP_CUBIC) + { + return HostGoldInterpCubic(borderWrap, coord, z, k); + } + else if constexpr (I == NVCV_INTERP_AREA) + { + return HostGoldInterpArea(borderWrap, scale, coord, z, k); + } +} + +template +void ExpectGridAligned3D(TensorWrap &tensorWrap, BorderWrap &borderWrap, InterpWrap &interpWrap, const InputType &input, + const ValueType &borderValue, int3 shapes, int x, int y, int z) +{ + int2 inCoord{x, y}; + int3 intCoord{x, y, z}; + float3 floatCoord = cuda::StaticCast(intCoord); + + ValueType gold = borderValue; + + if (test::IsInside(inCoord, int2{shapes.x, shapes.y}, B)) + { + intCoord.x = inCoord.x; + intCoord.y = inCoord.y; + + EXPECT_TRUE(std::is_reference_v); + + gold = input[intCoord.z * InputType::kShapes[2] * InputType::kShapes[1] + intCoord.y * InputType::kShapes[2] + + intCoord.x]; + + EXPECT_EQ(tensorWrap[intCoord], gold); + } + + EXPECT_TRUE(std::is_reference_v); + EXPECT_FALSE(std::is_reference_v); + + EXPECT_EQ(borderWrap[intCoord], gold); + EXPECT_EQ(interpWrap[floatCoord], gold); +} + +template +void ExpectGridAligned4D(TensorWrap &tensorWrap, BorderWrap &borderWrap, InterpWrap &interpWrap, const InputType &input, + const ValueType &borderValue, int4 shapes, int x, int y, int z, int c) +{ + int2 inCoord{x, y}; + int4 intCoord{c, x, y, z}; + float4 floatCoord = cuda::StaticCast(intCoord); + + ValueType gold = borderValue; + + if (test::IsInside(inCoord, int2{shapes.x, shapes.y}, B)) + { + intCoord.y = inCoord.x; + intCoord.z = inCoord.y; + + EXPECT_TRUE(std::is_reference_v); + + gold = input[intCoord.w * InputType::kShapes[1] * InputType::kShapes[2] * InputType::kShapes[3] + + intCoord.z * InputType::kShapes[2] * InputType::kShapes[3] + intCoord.y * InputType::kShapes[3] + + intCoord.x]; + + EXPECT_EQ(tensorWrap[intCoord], gold); + } + + EXPECT_TRUE(std::is_reference_v); + EXPECT_FALSE(std::is_reference_v); + + EXPECT_EQ(borderWrap[intCoord], gold); + EXPECT_EQ(interpWrap[floatCoord], gold); +} + // -------------------- Testing GetIndexForInterpolation ----------------------- #define NVCV_TEST_ROW(INTERP_TYPE, POSITION, INPUT, GOLD) \ @@ -68,7 +221,7 @@ TYPED_TEST(GetIndexForInterpolationTests, correct_index) const float in = ttype::GetValue; const int gold = ttype::GetValue; - int test = cuda::GetIndexForInterpolation(in); + auto test = static_cast(cuda::GetIndexForInterpolation(in)); EXPECT_EQ(test, gold); } @@ -132,7 +285,8 @@ TYPED_TEST(InterpolationWrap2DTest, correct_grid_aligned_values_in_host) EXPECT_EQ(InterpWrap::kCoordMap.id[0], 0); EXPECT_EQ(InterpWrap::kCoordMap.id[1], 1); - const float scaleX = 1.f, scaleY = 1.f; + const float scaleX = 1.f; + const float scaleY = 1.f; TensorWrap tensorWrap(input.data(), InputType::kStrides[0], InputType::kStrides[1]); BorderWrap borderWrap(tensorWrap, borderValue, InputType::kShapes[0], InputType::kShapes[1]); @@ -192,7 +346,8 @@ TYPED_TEST(InterpolationWrap2DTest, correct_grid_unaligned_values_in_host) using BorderWrap = cuda::BorderWrap; using InterpWrap = cuda::InterpolationWrap; - const float scaleX = 1.f, scaleY = 2.f; + const float scaleX = 1.f; + const float scaleY = 2.f; TensorWrap tensorWrap(input.data(), InputType::kStrides[0]); BorderWrap borderWrap(tensorWrap, borderValue, InputType::kShapes[0], InputType::kShapes[1]); @@ -200,79 +355,16 @@ TYPED_TEST(InterpolationWrap2DTest, correct_grid_unaligned_values_in_host) const int2 shapes{InputType::kShapes[1], InputType::kShapes[0]}; - ValueType gold; - std::default_random_engine randEng{0}; std::uniform_real_distribution randCoord{0.f, 1.f}; + const float2 scale{scaleX, scaleY}; - for (float y = -2; y < shapes.y + 2; ++y) + for (int y = -2; y < shapes.y + 2; ++y) { - for (float x = -2; x < shapes.x + 2; ++x) + for (int x = -2; x < shapes.x + 2; ++x) { - float2 floatCoord{x + randCoord(randEng), y + randCoord(randEng)}; - - if (kInterpType == NVCV_INTERP_NEAREST) - { - int2 c = cuda::round(floatCoord + .5f); - - gold = borderWrap[c]; - } - else if (kInterpType == NVCV_INTERP_LINEAR) - { - int2 c1 = cuda::round(floatCoord); - int2 c2 = c1 + 1; - - auto out = cuda::SetAll>(0); - - out += borderWrap[int2{c1.x, c1.y}] * (c2.x - floatCoord.x) * (c2.y - floatCoord.y); - out += borderWrap[int2{c2.x, c1.y}] * (floatCoord.x - c1.x) * (c2.y - floatCoord.y); - out += borderWrap[int2{c1.x, c2.y}] * (c2.x - floatCoord.x) * (floatCoord.y - c1.y); - out += borderWrap[int2{c2.x, c2.y}] * (floatCoord.x - c1.x) * (floatCoord.y - c1.y); - - gold = cuda::SaturateCast(out); - } - else if (kInterpType == NVCV_INTERP_CUBIC) - { - int ix = cuda::round(floatCoord.x); - int iy = cuda::round(floatCoord.y); - - using FT = cuda::ConvertBaseTypeTo; - auto sum = cuda::SetAll(0); - - float wx[4]; - test::GetBicubicCoeffs(floatCoord.x - ix, wx[0], wx[1], wx[2], wx[3]); - float wy[4]; - test::GetBicubicCoeffs(floatCoord.y - iy, wy[0], wy[1], wy[2], wy[3]); - - for (int cy = -1; cy <= 2; cy++) - { - for (int cx = -1; cx <= 2; cx++) - { - sum += borderWrap[int2{ix + cx, iy + cy}] * (wx[cx + 1] * wy[cy + 1]); - } - } - - gold = cuda::SaturateCast(sum); - } - else if (kInterpType == NVCV_INTERP_AREA) - { - int xmin = cuda::round(floatCoord.x * scaleX); - int xmax = cuda::round((floatCoord.x + 1) * scaleX); - int ymin = cuda::round(floatCoord.y * scaleY); - int ymax = cuda::round((floatCoord.y + 1) * scaleY); - - auto out = cuda::SetAll>(0); - - for (int cy = ymin; cy < ymax; ++cy) - { - for (int cx = xmin; cx < xmax; ++cx) - { - out += borderWrap[int2{cx, cy}] * (1.f / (scaleX * scaleY)); - } - } - - gold = cuda::SaturateCast(out); - } + float2 floatCoord{static_cast(x) + randCoord(randEng), static_cast(y) + randCoord(randEng)}; + ValueType gold = HostGoldInterp<2, kInterpType, ValueType>(borderWrap, scale, floatCoord); EXPECT_EQ(interpWrap[floatCoord], gold); } @@ -327,13 +419,13 @@ TYPED_TEST(InterpolationWrapHWTest, correct_shift_in_device) {height, width}, "HW" }, - nvcv::DataType{format}); + format.planeDataType(0)); nvcv::Tensor dstTensor( nvcv::TensorShape{ {height, width}, "HW" }, - nvcv::DataType{format}); + format.planeDataType(0)); auto srcDev = srcTensor.exportData(); auto dstDev = dstTensor.exportData(); @@ -360,7 +452,7 @@ TYPED_TEST(InterpolationWrapHWTest, correct_shift_in_device) std::default_random_engine randEng{0}; std::uniform_int_distribution srcRand{0u, 255u}; - std::generate(srcVec.begin(), srcVec.end(), [&]() { return srcRand(randEng); }); + std::ranges::generate(srcVec, [&srcRand, &randEng]() { return srcRand(randEng); }); ASSERT_EQ(cudaSuccess, cudaMemcpy(srcDev->basePtr(), srcVec.data(), srcVec.size(), cudaMemcpyHostToDevice)); @@ -391,11 +483,11 @@ TYPED_TEST(InterpolationWrapHWTest, correct_shift_in_device) for (int y = 0; y < dstSize.y; ++y) { - srcCoord.y = y + shiftY; + srcCoord.y = static_cast(y) + shiftY; for (int x = 0; x < dstSize.x; ++x) { - srcCoord.x = x + shiftX; + srcCoord.x = static_cast(x) + shiftX; test::ValueAt(gold, dstStrides, int2{x, y}) = test::GoldInterp( srcVec, srcStrides, srcSize2, borderValue, scale, srcCoord); @@ -470,7 +562,8 @@ TYPED_TEST(InterpolationWrap3DTest, correct_grid_aligned_values_in_host) EXPECT_EQ(InterpWrap::kCoordMap.id[1], 1); EXPECT_EQ(InterpWrap::kCoordMap.id[2], 2); - const float scaleX = 1.f, scaleY = 1.f; + const float scaleX = 1.f; + const float scaleY = 1.f; TensorWrap tensorWrap(input.data(), InputType::kStrides[0], InputType::kStrides[1], InputType::kStrides[2]); BorderWrap borderWrap(tensorWrap, borderValue, InputType::kShapes[1], InputType::kShapes[2]); @@ -482,42 +575,18 @@ TYPED_TEST(InterpolationWrap3DTest, correct_grid_aligned_values_in_host) const int3 shapes{InputType::kShapes[2], InputType::kShapes[1], InputType::kShapes[0]}; - ValueType gold; + const int xBegin = -2; + const int xCount = shapes.x + 4; + const int yBegin = -2; + const int yCount = shapes.y + 4; - for (int z = 0; z < shapes.z; ++z) + for (int i = 0; i < shapes.z * yCount * xCount; ++i) { - for (int y = -2; y < shapes.y + 2; ++y) - { - for (int x = -2; x < shapes.x + 2; ++x) - { - int2 inCoord{x, y}; - int3 intCoord{x, y, z}; - float3 floatCoord = cuda::StaticCast(intCoord); - - if (test::IsInside(inCoord, int2{shapes.x, shapes.y}, kBorderType)) - { - intCoord.x = inCoord.x; - intCoord.y = inCoord.y; - - EXPECT_TRUE(std::is_reference_v); - - gold = input[intCoord.z * InputType::kShapes[2] * InputType::kShapes[1] - + intCoord.y * InputType::kShapes[2] + intCoord.x]; + int x = xBegin + i % xCount; + int y = yBegin + (i / xCount) % yCount; + int z = i / (xCount * yCount); - EXPECT_EQ(tensorWrap[intCoord], gold); - } - else - { - gold = borderValue; - } - - EXPECT_TRUE(std::is_reference_v); - EXPECT_FALSE(std::is_reference_v); - - EXPECT_EQ(borderWrap[intCoord], gold); - EXPECT_EQ(interpWrap[floatCoord], gold); - } - } + ExpectGridAligned3D(tensorWrap, borderWrap, interpWrap, input, borderValue, shapes, x, y, z); } } @@ -538,7 +607,8 @@ TYPED_TEST(InterpolationWrap3DTest, correct_grid_unaligned_values_in_host) using BorderWrap = cuda::BorderWrap; using InterpWrap = cuda::InterpolationWrap; - const float scaleX = 2.f, scaleY = 1.f; + const float scaleX = 2.f; + const float scaleY = 1.f; TensorWrap tensorWrap(input.data(), InputType::kStrides[0], InputType::kStrides[1]); BorderWrap borderWrap(tensorWrap, borderValue, InputType::kShapes[1], InputType::kShapes[2]); @@ -546,81 +616,20 @@ TYPED_TEST(InterpolationWrap3DTest, correct_grid_unaligned_values_in_host) const int3 shapes{InputType::kShapes[2], InputType::kShapes[1], InputType::kShapes[0]}; - ValueType gold; - std::default_random_engine randEng{0}; std::uniform_real_distribution randCoord{0.f, 1.f}; + const float2 scale{scaleX, scaleY}; for (int z = 0; z < shapes.z; ++z) { - for (float y = -2; y < shapes.y + 2; ++y) + for (int y = -2; y < shapes.y + 2; ++y) { - for (float x = -2; x < shapes.x + 2; ++x) + for (int x = -2; x < shapes.x + 2; ++x) { - float3 floatCoord{x + randCoord(randEng), y + randCoord(randEng), static_cast(z)}; - - if (kInterpType == NVCV_INTERP_NEAREST) - { - int2 c = cuda::round(cuda::DropCast<2>(floatCoord + .5f)); - - gold = borderWrap[int3{c.x, c.y, z}]; - } - else if (kInterpType == NVCV_INTERP_LINEAR) - { - int2 c1 = cuda::round(cuda::DropCast<2>(floatCoord)); - int2 c2 = c1 + 1; - - auto out = cuda::SetAll>(0); - - out += borderWrap[int3{c1.x, c1.y, z}] * (c2.x - floatCoord.x) * (c2.y - floatCoord.y); - out += borderWrap[int3{c2.x, c1.y, z}] * (floatCoord.x - c1.x) * (c2.y - floatCoord.y); - out += borderWrap[int3{c1.x, c2.y, z}] * (c2.x - floatCoord.x) * (floatCoord.y - c1.y); - out += borderWrap[int3{c2.x, c2.y, z}] * (floatCoord.x - c1.x) * (floatCoord.y - c1.y); - - gold = cuda::SaturateCast(out); - } - else if (kInterpType == NVCV_INTERP_CUBIC) - { - int ix = cuda::round(floatCoord.x); - int iy = cuda::round(floatCoord.y); - - using FT = cuda::ConvertBaseTypeTo; - auto sum = cuda::SetAll(0); - - float wx[4]; - test::GetBicubicCoeffs(floatCoord.x - ix, wx[0], wx[1], wx[2], wx[3]); - float wy[4]; - test::GetBicubicCoeffs(floatCoord.y - iy, wy[0], wy[1], wy[2], wy[3]); - - for (int cy = -1; cy <= 2; cy++) - { - for (int cx = -1; cx <= 2; cx++) - { - sum += borderWrap[int3{ix + cx, iy + cy, z}] * (wx[cx + 1] * wy[cy + 1]); - } - } - - gold = cuda::SaturateCast(sum); - } - else if (kInterpType == NVCV_INTERP_AREA) - { - int xmin = cuda::round(floatCoord.x * scaleX); - int xmax = cuda::round((floatCoord.x + 1) * scaleX); - int ymin = cuda::round(floatCoord.y * scaleY); - int ymax = cuda::round((floatCoord.y + 1) * scaleY); - - auto out = cuda::SetAll>(0); - - for (int cy = ymin; cy < ymax; ++cy) - { - for (int cx = xmin; cx < xmax; ++cx) - { - out += borderWrap[int3{cx, cy, z}] * (1.f / (scaleX * scaleY)); - } - } - - gold = cuda::SaturateCast(out); - } + float3 floatCoord{static_cast(x) + randCoord(randEng), + static_cast(y) + randCoord(randEng), static_cast(z)}; + ValueType gold + = HostGoldInterp<3, kInterpType, ValueType>(borderWrap, scale, cuda::DropCast<2>(floatCoord), z); EXPECT_EQ(interpWrap[floatCoord], gold); } @@ -701,7 +710,7 @@ TYPED_TEST(InterpolationWrapNHWTest, correct_shift_in_device) std::default_random_engine randEng{0}; std::uniform_int_distribution srcRand{0u, 255u}; - std::generate(srcVec.begin(), srcVec.end(), [&]() { return srcRand(randEng); }); + std::ranges::generate(srcVec, [&srcRand, &randEng]() { return srcRand(randEng); }); ASSERT_EQ(cudaSuccess, cudaMemcpy(srcDev->basePtr(), srcVec.data(), srcVec.size(), cudaMemcpyHostToDevice)); @@ -731,11 +740,11 @@ TYPED_TEST(InterpolationWrapNHWTest, correct_shift_in_device) for (int y = 0; y < dstSize.y; ++y) { - srcCoord.y = y + shiftY; + srcCoord.y = static_cast(y) + shiftY; for (int x = 0; x < dstSize.x; ++x) { - srcCoord.x = x + shiftX; + srcCoord.x = static_cast(x) + shiftX; test::ValueAt(gold, dstStrides, int3{x, y, z}) = test::GoldInterp( srcVec, srcStrides, srcSize2, borderValue, scale, srcCoord, z); @@ -816,7 +825,8 @@ TYPED_TEST(InterpolationWrap4DTest, correct_grid_aligned_values_in_host) EXPECT_EQ(InterpWrap::kCoordMap.id[2], 3); EXPECT_EQ(InterpWrap::kCoordMap.id[3], 0); - const float scaleX = 1.f, scaleY = 1.f; + const float scaleX = 1.f; + const float scaleY = 1.f; TensorWrap tensorWrap(input.data(), InputType::kStrides[0], InputType::kStrides[1], InputType::kStrides[2], InputType::kStrides[3]); @@ -829,46 +839,19 @@ TYPED_TEST(InterpolationWrap4DTest, correct_grid_aligned_values_in_host) const int4 shapes{InputType::kShapes[2], InputType::kShapes[1], InputType::kShapes[0], InputType::kShapes[3]}; - ValueType gold; + const int xBegin = -2; + const int xCount = shapes.x + 4; + const int yBegin = -2; + const int yCount = shapes.y + 4; - for (int z = 0; z < shapes.z; ++z) + for (int i = 0; i < shapes.z * yCount * xCount * shapes.w; ++i) { - for (int y = -2; y < shapes.y + 2; ++y) - { - for (int x = -2; x < shapes.x + 2; ++x) - { - for (int c = 0; c < shapes.w; ++c) - { - int2 inCoord{x, y}; - int4 intCoord{c, x, y, z}; - float4 floatCoord = cuda::StaticCast(intCoord); - - if (test::IsInside(inCoord, int2{shapes.x, shapes.y}, kBorderType)) - { - intCoord.y = inCoord.x; - intCoord.z = inCoord.y; - - EXPECT_TRUE(std::is_reference_v); - - gold = input[intCoord.w * InputType::kShapes[1] * InputType::kShapes[2] * InputType::kShapes[3] - + intCoord.z * InputType::kShapes[2] * InputType::kShapes[3] - + intCoord.y * InputType::kShapes[3] + intCoord.x]; - - EXPECT_EQ(tensorWrap[intCoord], gold); - } - else - { - gold = borderValue; - } - - EXPECT_TRUE(std::is_reference_v); - EXPECT_FALSE(std::is_reference_v); - - EXPECT_EQ(borderWrap[intCoord], gold); - EXPECT_EQ(interpWrap[floatCoord], gold); - } - } - } + int c = i % shapes.w; + int x = xBegin + (i / shapes.w) % xCount; + int y = yBegin + (i / (shapes.w * xCount)) % yCount; + int z = i / (shapes.w * xCount * yCount); + + ExpectGridAligned4D(tensorWrap, borderWrap, interpWrap, input, borderValue, shapes, x, y, z, c); } } @@ -889,7 +872,8 @@ TYPED_TEST(InterpolationWrap4DTest, correct_grid_unaligned_values_in_host) using BorderWrap = cuda::BorderWrap; using InterpWrap = cuda::InterpolationWrap; - const float scaleX = 2.f, scaleY = 2.f; + const float scaleX = 2.f; + const float scaleY = 2.f; TensorWrap tensorWrap(input.data(), InputType::kStrides[0], InputType::kStrides[1], InputType::kStrides[2]); BorderWrap borderWrap(tensorWrap, borderValue, InputType::kShapes[1], InputType::kShapes[2]); @@ -897,90 +881,26 @@ TYPED_TEST(InterpolationWrap4DTest, correct_grid_unaligned_values_in_host) const int4 shapes{InputType::kShapes[2], InputType::kShapes[1], InputType::kShapes[0], InputType::kShapes[3]}; - ValueType gold; - std::default_random_engine randEng{0}; std::uniform_real_distribution randCoord{0.f, 1.f}; + const float2 scale{scaleX, scaleY}; - for (int z = 0; z < shapes.z; ++z) + const int xBegin = -2; + const int xCount = shapes.x + 4; + const int yBegin = -2; + const int yCount = shapes.y + 4; + + for (int i = 0; i < shapes.z * yCount * xCount * shapes.w; ++i) { - for (float y = -2; y < shapes.y + 2; ++y) - { - for (float x = -2; x < shapes.x + 2; ++x) - { - for (int k = 0; k < shapes.w; ++k) - { - float2 floatCoord{x + randCoord(randEng), y + randCoord(randEng)}; - - if (kInterpType == NVCV_INTERP_NEAREST) - { - int2 c = cuda::round(floatCoord + .5f); - - gold = borderWrap[int4{k, c.x, c.y, z}]; - } - else if (kInterpType == NVCV_INTERP_LINEAR) - { - int2 c1 = cuda::round(floatCoord); - int2 c2 = c1 + 1; - - auto out = cuda::SetAll>(0); - - out += borderWrap[int4{k, c1.x, c1.y, z}] * (c2.x - floatCoord.x) * (c2.y - floatCoord.y); - out += borderWrap[int4{k, c2.x, c1.y, z}] * (floatCoord.x - c1.x) * (c2.y - floatCoord.y); - out += borderWrap[int4{k, c1.x, c2.y, z}] * (c2.x - floatCoord.x) * (floatCoord.y - c1.y); - out += borderWrap[int4{k, c2.x, c2.y, z}] * (floatCoord.x - c1.x) * (floatCoord.y - c1.y); - - gold = cuda::SaturateCast(out); - } - else if (kInterpType == NVCV_INTERP_CUBIC) - { - int ix = cuda::round(floatCoord.x); - int iy = cuda::round(floatCoord.y); - - using FT = cuda::ConvertBaseTypeTo; - auto sum = cuda::SetAll(0); - - float wx[4]; - test::GetBicubicCoeffs(floatCoord.x - ix, wx[0], wx[1], wx[2], wx[3]); - float wy[4]; - test::GetBicubicCoeffs(floatCoord.y - iy, wy[0], wy[1], wy[2], wy[3]); - - for (int cy = -1; cy <= 2; cy++) - { - for (int cx = -1; cx <= 2; cx++) - { - sum += borderWrap[int4{k, ix + cx, iy + cy, z}] * (wx[cx + 1] * wy[cy + 1]); - } - } - - gold = cuda::SaturateCast(sum); - } - else if (kInterpType == NVCV_INTERP_AREA) - { - int xmin = cuda::round(floatCoord.x * scaleX); - int xmax = cuda::round((floatCoord.x + 1) * scaleX); - int ymin = cuda::round(floatCoord.y * scaleY); - int ymax = cuda::round((floatCoord.y + 1) * scaleY); - - auto out = cuda::SetAll>(0); - - for (int cy = ymin; cy < ymax; ++cy) - { - for (int cx = xmin; cx < xmax; ++cx) - { - out += borderWrap[int4{k, cx, cy, z}] * (1.f / (scaleX * scaleY)); - } - } - - gold = cuda::SaturateCast(out); - } - - float4 floatCoord4{static_cast(k), floatCoord.x, floatCoord.y, static_cast(z)}; - - EXPECT_EQ(interpWrap[floatCoord4], gold); - } - } - } + int k = i % shapes.w; + auto x = static_cast(xBegin + (i / shapes.w) % xCount); + auto y = static_cast(yBegin + (i / (shapes.w * xCount)) % yCount); + int z = i / (shapes.w * xCount * yCount); + float2 floatCoord{x + randCoord(randEng), y + randCoord(randEng)}; + float4 floatCoord4{static_cast(k), floatCoord.x, floatCoord.y, static_cast(z)}; + ValueType gold = HostGoldInterp<4, kInterpType, ValueType>(borderWrap, scale, floatCoord, z, k); + + EXPECT_EQ(interpWrap[floatCoord4], gold); } } @@ -1059,7 +979,7 @@ TYPED_TEST(InterpolationWrapNHWCTest, correct_shift_in_device) std::default_random_engine randEng{0}; std::uniform_int_distribution srcRand{0u, 255u}; - std::generate(srcVec.begin(), srcVec.end(), [&]() { return srcRand(randEng); }); + std::ranges::generate(srcVec, [&srcRand, &randEng]() { return srcRand(randEng); }); ASSERT_EQ(cudaSuccess, cudaMemcpy(srcDev->basePtr(), srcVec.data(), srcVec.size(), cudaMemcpyHostToDevice)); @@ -1082,27 +1002,16 @@ TYPED_TEST(InterpolationWrapNHWCTest, correct_shift_in_device) const int2 srcSize2{srcSize.x, srcSize.y}; - // Run gold interpolation shift - for (int z = 0; z < dstSize.z; ++z) + for (int i = 0; i < dstSize.z * dstSize.y * dstSize.x * dstSize.w; ++i) { - float2 srcCoord; - - for (int y = 0; y < dstSize.y; ++y) - { - srcCoord.y = y + shiftY; - - for (int x = 0; x < dstSize.x; ++x) - { - srcCoord.x = x + shiftX; - - for (int k = 0; k < dstSize.w; ++k) - { - test::ValueAt(gold, dstStrides, int4{k, x, y, z}) - = test::GoldInterp(srcVec, srcStrides, srcSize2, borderValue, scale, - srcCoord, z, k); - } - } - } + int k = i % dstSize.w; + int x = (i / dstSize.w) % dstSize.x; + int y = (i / (dstSize.w * dstSize.x)) % dstSize.y; + int z = i / (dstSize.w * dstSize.x * dstSize.y); + float2 srcCoord = {x + shiftX, y + shiftY}; + + test::ValueAt(gold, dstStrides, int4{k, x, y, z}) = test::GoldInterp( + srcVec, srcStrides, srcSize2, borderValue, scale, srcCoord, z, k); } VEC_EXPECT_NEAR(test, gold, 1); diff --git a/tests/nvcv_types/cudatools_system/TestLinAlg.cpp b/tests/nvcv_types/cudatools_system/TestLinAlg.cpp index b235c3d0f..6c0b43a71 100644 --- a/tests/nvcv_types/cudatools_system/TestLinAlg.cpp +++ b/tests/nvcv_types/cudatools_system/TestLinAlg.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,7 +22,7 @@ #include // for std::generate, etc. #include // for std::pow, etc. #include // for std::iota, etc. -#include // for std::random_device, etc. +#include // for std::mt19937, etc. #include // for std::stringstream, etc. #include // for std::remove_reference_t, etc. @@ -36,12 +36,25 @@ using TStr = typename test::StringLiteral; using schar = signed char; using uchar = unsigned char; -static std::random_device rd; -static std::mt19937 mt(rd()); // to generate random input +template +constexpr T ExplicitValue(U value) +{ + return static_cast(value); +} + +// Fixed seed: random_device made matrix tests non-deterministic across CI +// runs and occasionally produced ill-conditioned matrices that exceeded the +// MaxAbsErr tolerance on rare-config CI. Matrix conditioning is famously +// sensitive to input data, so a known-good fixed seed is the right default. +static std::mt19937 &Rng() +{ + static std::mt19937 rng(12345); // to generate random input + return rng; +} // Maximum absolute error expected given type T as either float or double template -constexpr T MaxAbsErr = std::is_same_v ? 1e-5 : 1e-8; +constexpr T MaxAbsErr = std::is_same_v ? ExplicitValue(1e-5) : ExplicitValue(1e-8); #define SCALAR(T, V) ttype::Value @@ -93,9 +106,9 @@ TYPED_TEST(LinAlgVectorTest, can_change_content) math::Vector vec; - for (int i = 0; i < vec.size(); ++i) + for (int i = 0; i < vec.size(); ++i) // NOSONAR: index-based access is the behavior under test. { - vec[i] = i; + vec[i] = ExplicitValue(i); EXPECT_EQ(vec[i], i); } @@ -108,7 +121,7 @@ TYPED_TEST(LinAlgVectorTest, pointer_works) math::Vector vec; - VectorType *begin = vec; + auto *begin = static_cast(vec); EXPECT_EQ(begin, vec.begin()); @@ -136,9 +149,9 @@ TYPED_TEST(LinAlgVectorTest, to_vector_works) math::Vector vec; - for (int i = 0; i < vec.size(); ++i) + for (int i = 0; i < vec.size(); ++i) // NOSONAR: index-based access is the behavior under test. { - vec[i] = i; + vec[i] = ExplicitValue(i); } std::vector test = vec.to_vector(); @@ -223,7 +236,7 @@ TYPED_TEST(LinAlgVectorTest, store_works) for (int i = 0; i < N; ++i) { - vec[i] = i; + vec[i] = ExplicitValue(i); } std::array test; @@ -325,11 +338,11 @@ TYPED_TEST(LinAlgMatrixTest, can_change_content) math::Matrix mat; - for (int i = 0; i < mat.rows(); ++i) + for (int i = 0; i < mat.rows(); ++i) // NOSONAR: matrix index access is under test. { - for (int j = 0; j < mat.cols(); ++j) + for (int j = 0; j < mat.cols(); ++j) // NOSONAR: matrix index access is under test. { - mat[i][j] = i * mat.rows() + j; + mat[i][j] = ExplicitValue(i * mat.rows() + j); int2 c{j, i}; @@ -348,14 +361,14 @@ TYPED_TEST(LinAlgMatrixTest, col_works) math::Vector gold; - for (int i = 0; i < mat.rows(); ++i) + for (int i = 0; i < mat.rows(); ++i) // NOSONAR: matrix index access is under test. { - for (int j = 0; j < mat.cols(); ++j) + for (int j = 0; j < mat.cols(); ++j) // NOSONAR: matrix index access is under test. { - mat[i][j] = i * mat.rows() + j; + mat[i][j] = ExplicitValue(i * mat.rows() + j); } - gold[i] = i * mat.rows(); + gold[i] = ExplicitValue(i * mat.rows()); } math::Vector test = mat.col(0); @@ -493,7 +506,7 @@ TYPED_TEST(LinAlgMatrixTest, store_works) { for (int j = 0; j < N; ++j) { - mat[i][j] = val++; + mat[i][j] = static_cast(val++); } } @@ -675,7 +688,7 @@ TYPED_TEST(LinAlgOutputStreamTest, correct_output) EXPECT_NO_THROW(oss << test); - EXPECT_STREQ(oss.str().c_str(), gold.value); + EXPECT_STREQ(oss.str().c_str(), gold.value.data()); } // -------------------- Testing LinAlg unary operator - ------------------------ @@ -1091,7 +1104,15 @@ TYPED_TEST(LinAlgSpecialVectorMatrixTest, correct_content_of_compan) { for (int j = 0; j < matCompan.cols(); ++j) { - Type value = ((j == matCompan.cols() - 1) ? -vec[i] : ((j == i - 1) ? 1 : 0)); + Type value = 0; + if (j == matCompan.cols() - 1) + { + value = -vec[i]; + } + else if (j == i - 1) + { + value = 1; + } EXPECT_EQ(matCompan[i][j], value); } @@ -1135,7 +1156,8 @@ TYPED_TEST(LinAlgDotAndReverseVectorTest, correct_content_of_dot) using Type = ttype::GetType; constexpr int M = ttype::GetValue; - math::Vector vec1, vec2; + math::Vector vec1; + math::Vector vec2; std::iota(vec1.begin(), vec1.end(), 1); std::iota(vec2.begin(), vec2.end(), 1); @@ -1160,7 +1182,7 @@ TYPED_TEST(LinAlgDotAndReverseVectorTest, correct_content_of_reverse) auto gold = vec; - std::reverse(gold.begin(), gold.end()); + std::reverse(gold.begin(), gold.end()); // NOSONAR: std::ranges::reverse is C++20. EXPECT_EQ(test, gold); } @@ -1198,9 +1220,9 @@ TYPED_TEST(LinAlgTransfTest, correct_content_of_transp) math::Matrix gold; - for (int i = 0; i < gold.rows(); ++i) + for (int i = 0; i < gold.rows(); ++i) // NOSONAR: matrix index access is under test. { - for (int j = 0; j < gold.cols(); ++j) + for (int j = 0; j < gold.cols(); ++j) // NOSONAR: matrix index access is under test. { gold[i][j] = mat[j][i]; } @@ -1233,9 +1255,9 @@ TYPED_TEST(LinAlgTransfTest, correct_content_of_transp_inplace) math::Matrix gold; - for (int i = 0; i < gold.rows(); ++i) + for (int i = 0; i < gold.rows(); ++i) // NOSONAR: matrix index access is under test. { - for (int j = 0; j < gold.cols(); ++j) + for (int j = 0; j < gold.cols(); ++j) // NOSONAR: matrix index access is under test. { gold[i][j] = mat[j][i]; } @@ -1262,9 +1284,9 @@ TYPED_TEST(LinAlgTransfTest, correct_content_of_transp_vector) math::Matrix gold; - for (int i = 0; i < gold.rows(); ++i) + for (int i = 0; i < gold.rows(); ++i) // NOSONAR: matrix index access is under test. { - for (int j = 0; j < gold.cols(); ++j) + for (int j = 0; j < gold.cols(); ++j) // NOSONAR: matrix index access is under test. { gold[i][j] = vec[i]; } @@ -1295,9 +1317,9 @@ TYPED_TEST(LinAlgTransfTest, correct_content_of_flip) math::Matrix gold; - for (int i = 0; i < gold.rows(); ++i) + for (int i = 0; i < gold.rows(); ++i) // NOSONAR: matrix index access is under test. { - for (int j = 0; j < gold.cols(); ++j) + for (int j = 0; j < gold.cols(); ++j) // NOSONAR: matrix index access is under test. { gold[i][j] = mat[gold.rows() - 1 - i][gold.cols() - 1 - j]; } @@ -1328,9 +1350,9 @@ TYPED_TEST(LinAlgTransfTest, correct_content_of_flip_rows) math::Matrix gold; - for (int i = 0; i < gold.rows(); ++i) + for (int i = 0; i < gold.rows(); ++i) // NOSONAR: matrix index access is under test. { - for (int j = 0; j < gold.cols(); ++j) + for (int j = 0; j < gold.cols(); ++j) // NOSONAR: matrix index access is under test. { gold[i][j] = mat[gold.rows() - 1 - i][j]; } @@ -1361,9 +1383,9 @@ TYPED_TEST(LinAlgTransfTest, correct_content_of_flip_cols) math::Matrix gold; - for (int i = 0; i < gold.rows(); ++i) + for (int i = 0; i < gold.rows(); ++i) // NOSONAR: matrix index access is under test. { - for (int j = 0; j < gold.cols(); ++j) + for (int j = 0; j < gold.cols(); ++j) // NOSONAR: matrix index access is under test. { gold[i][j] = mat[i][mat.cols() - 1 - j]; } @@ -1394,9 +1416,9 @@ TYPED_TEST(LinAlgTransfTest, correct_content_of_head) math::Matrix gold; - for (int i = 0; i < gold.rows(); ++i) + for (int i = 0; i < gold.rows(); ++i) // NOSONAR: matrix index access is under test. { - for (int j = 0; j < gold.cols(); ++j) + for (int j = 0; j < gold.cols(); ++j) // NOSONAR: matrix index access is under test. { gold[i][j] = mat[i][j]; } @@ -1427,9 +1449,9 @@ TYPED_TEST(LinAlgTransfTest, correct_content_of_tail) math::Matrix gold; - for (int i = 0; i < gold.rows(); ++i) + for (int i = 0; i < gold.rows(); ++i) // NOSONAR: matrix index access is under test. { - for (int j = 0; j < gold.cols(); ++j) + for (int j = 0; j < gold.cols(); ++j) // NOSONAR: matrix index access is under test. { gold[i][j] = mat[M - 1 - i][j]; } @@ -1463,14 +1485,14 @@ TYPED_TEST(LinAlgLTIFilterTest, correct_content_of_fwd) math::Matrix prologue; math::Matrix block; - std::generate(weights.begin(), weights.end(), [&]() { return d(mt); }); + std::ranges::generate(weights, [&d]() { return d(Rng()); }); for (int i = 0; i < R; ++i) { - std::generate(prologue[i].begin(), prologue[i].end(), [&]() { return d(mt); }); + std::ranges::generate(prologue[i], [&d]() { return d(Rng()); }); } for (int i = 0; i < M; ++i) { - std::generate(block[i].begin(), block[i].end(), [&]() { return d(mt); }); + std::ranges::generate(block[i], [&d]() { return d(Rng()); }); } auto test = math::fwd(prologue, block, weights); @@ -1521,14 +1543,14 @@ TYPED_TEST(LinAlgLTIFilterTest, correct_content_of_rev) math::Matrix epilogue; math::Matrix block; - std::generate(weights.begin(), weights.end(), [&]() { return d(mt); }); + std::ranges::generate(weights, [&d]() { return d(Rng()); }); for (int i = 0; i < R; ++i) { - std::generate(epilogue[i].begin(), epilogue[i].end(), [&]() { return d(mt); }); + std::ranges::generate(epilogue[i], [&d]() { return d(Rng()); }); } for (int i = 0; i < M; ++i) { - std::generate(block[i].begin(), block[i].end(), [&]() { return d(mt); }); + std::ranges::generate(block[i], [&d]() { return d(Rng()); }); } auto test = math::rev(block, epilogue, weights); @@ -1579,30 +1601,32 @@ void GetTestInput(math::Matrix &input) { if constexpr (M == 1) { - input.load({0.999998682}); + input.load({ExplicitValue(0.999998682)}); } else if constexpr (M == 2) { input.load( - {1.00034897, -0.000357094, - 0.000348814, 0.999643171}); + {ExplicitValue(1.00034897), ExplicitValue(-0.000357094), + ExplicitValue(0.000348814), ExplicitValue(0.999643171)}); } else if constexpr (M == 3) { input.load( - {1.01250394, -0.02495176, 0.01244351, - 0.01199532, 0.97607735, 0.01192297, - 0.01149353, -0.02290747, 1.01140953}); + {ExplicitValue(1.01250394), ExplicitValue(-0.02495176), ExplicitValue(0.01244351), + ExplicitValue(0.01199532), ExplicitValue(0.97607735), ExplicitValue(0.01192297), + ExplicitValue(0.01149353), ExplicitValue(-0.02290747), ExplicitValue(1.01140953)}); } else { static_assert(M == 4); input.load( - {1., 0.292789199, 0.384852709, 0.200596131, - 0., 0.941267619, 0.215589234, 0.344613902, - 0., -0.100899228, 0.808642026, 0.146461019, - 0., -0.042882204, -0.157265148, 0.779262512}); + {ExplicitValue(1.), ExplicitValue(0.292789199), ExplicitValue(0.384852709), + ExplicitValue(0.200596131), ExplicitValue(0.), ExplicitValue(0.941267619), + ExplicitValue(0.215589234), ExplicitValue(0.344613902), ExplicitValue(0.), + ExplicitValue(-0.100899228), ExplicitValue(0.808642026), ExplicitValue(0.146461019), + ExplicitValue(0.), ExplicitValue(-0.042882204), ExplicitValue(-0.157265148), + ExplicitValue(0.779262512)}); } } @@ -1626,7 +1650,7 @@ struct GoldDet template struct GoldDet { - T operator()(const math::Matrix &m) + T operator()(const math::Matrix &) { return T{1}; } @@ -1687,22 +1711,22 @@ void GetTestInputAndGoldOutput(math::Matrix &input, math::Matrix(1.000001318)}); } else if constexpr (M == 2) { output.load( - { 0.999651028, 0.000357097, - -0.000348817, 1.000356831}); + { ExplicitValue(0.999651028), ExplicitValue(0.000357097), + ExplicitValue(-0.000348817), ExplicitValue(1.000356831)}); } else { static_assert(M == 3); output.load( - { 0.98749612, 0.02495163, -0.01244344, - -0.01199525, 1.02392251, -0.0119229, - -0.01149346, 0.02290733, 0.98859054}); + { ExplicitValue(0.98749612), ExplicitValue(0.02495163), ExplicitValue(-0.01244344), + ExplicitValue(-0.01199525), ExplicitValue(1.02392251), ExplicitValue(-0.0119229), + ExplicitValue(-0.01149346), ExplicitValue(0.02290733), ExplicitValue(0.98859054)}); } } @@ -1713,7 +1737,9 @@ TYPED_TEST(LinAlgInvMatrixTest, correct_content_of_inv) using Type = ttype::GetType; constexpr int M = ttype::GetValue; - math::Matrix mat, gold, test; + math::Matrix mat; + math::Matrix gold; + math::Matrix test; GetTestInputAndGoldOutput(mat, gold); @@ -1733,7 +1759,8 @@ TYPED_TEST(LinAlgInvMatrixTest, correct_content_of_inv_inplace) using Type = ttype::GetType; constexpr int M = ttype::GetValue; - math::Matrix mat, gold; + math::Matrix mat; + math::Matrix gold; GetTestInputAndGoldOutput(mat, gold); @@ -1753,7 +1780,9 @@ TYPED_TEST(LinAlgInvMatrixTest, correct_content_of_inv_lu) using Type = ttype::GetType; constexpr int M = ttype::GetValue; - math::Matrix mat, gold, test; + math::Matrix mat; + math::Matrix gold; + math::Matrix test; GetTestInputAndGoldOutput(mat, gold); @@ -1773,7 +1802,8 @@ TYPED_TEST(LinAlgInvMatrixTest, correct_content_of_inv_lu_inplace) using Type = ttype::GetType; constexpr int M = ttype::GetValue; - math::Matrix mat, gold; + math::Matrix mat; + math::Matrix gold; GetTestInputAndGoldOutput(mat, gold); diff --git a/tests/nvcv_types/cudatools_system/TestTensorBatchWrap.cpp b/tests/nvcv_types/cudatools_system/TestTensorBatchWrap.cpp index b3079e7c3..b4b270339 100644 --- a/tests/nvcv_types/cudatools_system/TestTensorBatchWrap.cpp +++ b/tests/nvcv_types/cudatools_system/TestTensorBatchWrap.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -39,10 +39,10 @@ namespace ttype = nvcv::test::type; static constexpr int kMaxDim = 50; template -nvcv::Tensor GetRandomTensor(R &rg, nvcv::DataType dtype, cudaStream_t stream) +nvcv::Tensor GetRandomTensor(R &rg, nvcv::DataType dtype, cudaStream_t) { - std::uniform_int_distribution shape_dist(kMaxDim / 2, kMaxDim); - nvcv::TensorShape::ShapeType shapeData(NDIM); + std::uniform_int_distribution shape_dist(kMaxDim / 2, kMaxDim); + nvcv::TensorShape::ShapeType shapeData(NDIM); for (auto &d : shapeData) { d = shape_dist(rg); @@ -60,7 +60,7 @@ void VerifyTensorHelper(NVCVByte *data, const int64_t *shape, const int64_t *str { if constexpr (N == NDIM) { - auto gold = cuda::SetAll(startIndex % 255); + auto gold = cuda::SetAll(static_cast>(startIndex % 255)); auto value = *reinterpret_cast(data); ASSERT_EQ(value, gold); } diff --git a/tests/nvcv_types/cudatools_system/TestTensorWrap.cpp b/tests/nvcv_types/cudatools_system/TestTensorWrap.cpp index d57fc0db3..a91373fb1 100644 --- a/tests/nvcv_types/cudatools_system/TestTensorWrap.cpp +++ b/tests/nvcv_types/cudatools_system/TestTensorWrap.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,6 +25,7 @@ #include // for Tensor, etc. #include // for TensorDataAccessStridedImagePlanar, etc. +#include #include namespace t = ::testing; @@ -175,9 +176,9 @@ TYPED_TEST(Tensor1DWrapTensorTest, correct_with_tensor) cuda::Tensor1DWrap wrap(*dev); - const ValueType *ptr0 = reinterpret_cast(dev->basePtr()); - const ValueType *ptr1 - = reinterpret_cast(reinterpret_cast(dev->basePtr()) + dev->stride(0)); + auto ptr0 = reinterpret_cast(dev->basePtr()); + auto ptr1 + = reinterpret_cast(reinterpret_cast(dev->basePtr()) + dev->stride(0)); EXPECT_EQ(wrap.ptr(0), ptr0); EXPECT_EQ(wrap.ptr(1), ptr1); @@ -421,8 +422,8 @@ TYPED_TEST(Tensor2DWrapImageTest, correct_with_image) auto strides = wrap.strides(); EXPECT_EQ(strides[0], dev->plane(0).rowStride); - const ValueType *ptr0 = reinterpret_cast(dev->plane(0).basePtr); - const ValueType *ptr1 = reinterpret_cast(dev->plane(0).basePtr + dev->plane(0).rowStride); + auto ptr0 = reinterpret_cast(dev->plane(0).basePtr); + auto ptr1 = reinterpret_cast(dev->plane(0).basePtr + dev->plane(0).rowStride); EXPECT_EQ(wrap.ptr(0), ptr0); EXPECT_EQ(wrap.ptr(1), ptr1); @@ -738,9 +739,9 @@ TYPED_TEST(Tensor3DWrapTensorTest, correct_with_tensor) EXPECT_EQ(strides[i], dev->stride(i)); } - const ValueType *ptr0 = reinterpret_cast(dev->basePtr()); - const ValueType *ptr1 = reinterpret_cast(dev->basePtr() + dev->stride(0)); - const ValueType *ptr12 = reinterpret_cast(dev->basePtr() + dev->stride(0) + 2 * dev->stride(1)); + auto ptr0 = reinterpret_cast(dev->basePtr()); + auto ptr1 = reinterpret_cast(dev->basePtr() + dev->stride(0)); + auto ptr12 = reinterpret_cast(dev->basePtr() + dev->stride(0) + 2 * dev->stride(1)); EXPECT_EQ(wrap.ptr(0), ptr0); EXPECT_EQ(wrap.ptr(1), ptr1); @@ -840,53 +841,40 @@ TYPED_TEST(Tensor4DWrapTest, correct_content_and_is_const) EXPECT_EQ(wrap.ptr(), input.data()); - for (int b = 0; b < InputType::kShapes[0]; ++b) - { - EXPECT_TRUE(std::is_pointer_v); - EXPECT_TRUE(std::is_const_v>); + ForEach4D( + InputType::kShapes[0], InputType::kShapes[1], InputType::kShapes[2], InputType::kShapes[3], + [&input, &wrap](int b, int y, int x, int k) + { + EXPECT_TRUE(std::is_pointer_v); + EXPECT_TRUE(std::is_const_v>); - EXPECT_EQ(wrap.ptr(b), &input[b * InputType::kShapes[1] * InputType::kShapes[2] * InputType::kShapes[3]]); + EXPECT_EQ(wrap.ptr(b), &input[b * InputType::kShapes[1] * InputType::kShapes[2] * InputType::kShapes[3]]); - for (int y = 0; y < InputType::kShapes[1]; ++y) - { EXPECT_TRUE(std::is_pointer_v); EXPECT_TRUE(std::is_const_v>); EXPECT_EQ(wrap.ptr(b, y), &input[b * InputType::kShapes[1] * InputType::kShapes[2] * InputType::kShapes[3] + y * InputType::kShapes[2] * InputType::kShapes[3]]); - for (int x = 0; x < InputType::kShapes[2]; ++x) - { - EXPECT_TRUE(std::is_pointer_v); - EXPECT_TRUE(std::is_const_v>); + EXPECT_TRUE(std::is_pointer_v); + EXPECT_TRUE(std::is_const_v>); - EXPECT_EQ(wrap.ptr(b, y, x), - &input[b * InputType::kShapes[1] * InputType::kShapes[2] * InputType::kShapes[3] - + y * InputType::kShapes[2] * InputType::kShapes[3] + x * InputType::kShapes[3]]); + EXPECT_EQ(wrap.ptr(b, y, x), + &input[b * InputType::kShapes[1] * InputType::kShapes[2] * InputType::kShapes[3] + + y * InputType::kShapes[2] * InputType::kShapes[3] + x * InputType::kShapes[3]]); - for (int k = 0; k < InputType::kShapes[3]; ++k) - { - EXPECT_TRUE(std::is_pointer_v); - EXPECT_TRUE(std::is_const_v>); + EXPECT_TRUE(std::is_pointer_v); + EXPECT_TRUE(std::is_const_v>); - EXPECT_EQ( - wrap.ptr(b, y, x, k), - &input[b * InputType::kShapes[1] * InputType::kShapes[2] * InputType::kShapes[3] - + y * InputType::kShapes[2] * InputType::kShapes[3] + x * InputType::kShapes[3] + k]); + EXPECT_EQ(wrap.ptr(b, y, x, k), &input[PackedOffset4D(b, y, x, k)]); - int4 c4{k, x, y, b}; + int4 c4{k, x, y, b}; - EXPECT_TRUE(std::is_reference_v); - EXPECT_TRUE(std::is_const_v>); + EXPECT_TRUE(std::is_reference_v); + EXPECT_TRUE(std::is_const_v>); - EXPECT_EQ( - wrap[c4], - input[b * InputType::kShapes[1] * InputType::kShapes[2] * InputType::kShapes[3] - + y * InputType::kShapes[2] * InputType::kShapes[3] + x * InputType::kShapes[3] + k]); - } - } - } - } + EXPECT_EQ(wrap[c4], input[PackedOffset4D(b, y, x, k)]); + }); } TYPED_TEST(Tensor4DWrapTest, it_works_in_device) @@ -932,22 +920,13 @@ TYPED_TEST(Tensor4DWrapCopyTest, can_change_content) ASSERT_EQ(InputType::kShapes[i], decltype(gold)::kShapes[i]); } - for (int b = 0; b < InputType::kShapes[0]; ++b) - { - for (int y = 0; y < InputType::kShapes[1]; ++y) - { - for (int x = 0; x < InputType::kShapes[2]; ++x) - { - for (int k = 0; k < InputType::kShapes[3]; ++k) - { - int4 c{k, x, y, b}; + ForEach4D(InputType::kShapes[0], InputType::kShapes[1], InputType::kShapes[2], InputType::kShapes[3], + [&gold, &wrap](int b, int y, int x, int k) + { + int4 c{k, x, y, b}; - wrap[c] = gold[b * InputType::kShapes[1] * InputType::kShapes[2] * InputType::kShapes[3] - + y * InputType::kShapes[2] * InputType::kShapes[3] + x * InputType::kShapes[3] + k]; - } - } - } - } + wrap[c] = gold[PackedOffset4D(b, y, x, k)]; + }); EXPECT_EQ(test, gold); } @@ -1009,23 +988,14 @@ TYPED_TEST(Tensor4DWrapTensorWrapTest, correct_with_tensor_wrap) EXPECT_EQ(strides[i], InputType::kStrides[i]); } - for (int b = 0; b < N; ++b) - { - for (int y = 0; y < H; ++y) - { - for (int x = 0; x < W; ++x) - { - for (int k = 0; k < C; ++k) - { - int4 c4{k, x, y, b}; - - EXPECT_EQ(wrap[c4], - *reinterpret_cast(dev->basePtr() + b * dev->stride(0) + y * dev->stride(1) - + x * dev->stride(2) + k * dev->stride(3))); - } - } - } - } + ForEach4D(N, H, W, C, + [&dev, &wrap](int b, int y, int x, int k) + { + int4 c4{k, x, y, b}; + + EXPECT_EQ(wrap[c4], + *reinterpret_cast(dev->basePtr() + StridedOffset4D(*dev, b, y, x, k))); + }); } // clang-format off @@ -1065,11 +1035,11 @@ TYPED_TEST(Tensor4DWrapTensorTest, correct_with_tensor) EXPECT_EQ(strides[i], dev->stride(i)); } - const ValueType *ptr0 = reinterpret_cast(dev->basePtr()); - const ValueType *ptr1 = reinterpret_cast(dev->basePtr() + dev->stride(0)); - const ValueType *ptr12 = reinterpret_cast(dev->basePtr() + dev->stride(0) + 2 * dev->stride(1)); - const ValueType *ptr123 = reinterpret_cast(dev->basePtr() + dev->stride(0) + 2 * dev->stride(1) - + 3 * dev->stride(2)); + auto ptr0 = reinterpret_cast(dev->basePtr()); + auto ptr1 = reinterpret_cast(dev->basePtr() + dev->stride(0)); + auto ptr12 = reinterpret_cast(dev->basePtr() + dev->stride(0) + 2 * dev->stride(1)); + auto ptr123 = reinterpret_cast(dev->basePtr() + dev->stride(0) + 2 * dev->stride(1) + + 3 * dev->stride(2)); EXPECT_EQ(wrap.ptr(0), ptr0); EXPECT_EQ(wrap.ptr(1), ptr1); @@ -1113,21 +1083,11 @@ TYPED_TEST(Tensor4DWrapTensorTest, it_works_in_device) std::vector test(sizeBytes); std::vector gold(sizeBytes); - for (int b = 0; b < dev->shape(0); b++) - { - for (int i = 0; i < dev->shape(1); i++) - { - for (int j = 0; j < dev->shape(2); j++) - { - for (int k = 0; k < dev->shape(3); k++) - { - *reinterpret_cast( - &gold[b * dev->stride(0) + i * dev->stride(1) + j * dev->stride(2) + k * dev->stride(3)]) - = cuda::SetAll(1); - } - } - } - } + ForEach4D(static_cast(dev->shape(0)), static_cast(dev->shape(1)), static_cast(dev->shape(2)), + static_cast(dev->shape(3)), + [&dev, &gold](int b, int i, int j, int k) { + *reinterpret_cast(&gold[StridedOffset4D(*dev, b, i, j, k)]) = cuda::SetAll(1); + }); // Get test data back ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); @@ -1181,8 +1141,8 @@ NVCV_INSTANTIATE_TEST_SUITE_P(_, CreateTensorWrapNHWxTests, TEST_P(CreateTensorWrapNHWxTests, correct_properties_in_nhw) { - auto tensorShape = std::get<0>(GetParam()); - auto tensorDataType = std::get<1>(GetParam()); + const auto &tensorShape = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + const auto &tensorDataType = ::nvcv::test::ParamValue(std::get<1>(GetParam())); nvcv::Tensor tensor(tensorShape, tensorDataType); @@ -1208,8 +1168,8 @@ TEST_P(CreateTensorWrapNHWxTests, correct_properties_in_nhw) TEST_P(CreateTensorWrapNHWxTests, correct_properties_in_nhwc) { - auto tensorShape = std::get<0>(GetParam()); - auto tensorDataType = std::get<1>(GetParam()); + const auto &tensorShape = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + const auto &tensorDataType = ::nvcv::test::ParamValue(std::get<1>(GetParam())); nvcv::Tensor tensor(tensorShape, tensorDataType); diff --git a/tests/nvcv_types/cudatools_system/TestTypeTraits.cpp b/tests/nvcv_types/cudatools_system/TestTypeTraits.cpp index 96320483e..1d2f52ed4 100644 --- a/tests/nvcv_types/cudatools_system/TestTypeTraits.cpp +++ b/tests/nvcv_types/cudatools_system/TestTypeTraits.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -75,10 +75,14 @@ class HasTypeTraitsUnsupportedTest : public TypeTraitsBaseTest { }; -typedef struct _float5 +struct float5 { - float a, b, c, d, e; -} float5; + float a; + float b; + float c; + float d; + float e; +}; using UnsupportedBaseTypes = t::Types; @@ -422,11 +426,11 @@ NVCV_TYPED_TEST_SUITE_F( TYPED_TEST(TypeTraitsVectorTypePrintTest, correct_output_stream) { - EXPECT_STREQ(nvcv::cuda::GetTypeName(), this->GoldTypeName.value); + EXPECT_STREQ(nvcv::cuda::GetTypeName(), this->GoldTypeName.value.data()); std::ostringstream oss; EXPECT_NO_THROW(oss << this->val); - EXPECT_STREQ(oss.str().c_str(), this->GoldValueOutput.value); + EXPECT_STREQ(oss.str().c_str(), this->GoldValueOutput.value.data()); } diff --git a/tests/nvcv_types/cudatools_unit/TestLegacyHelpers.cpp b/tests/nvcv_types/cudatools_unit/TestLegacyHelpers.cpp index c4db8fa60..128d5177b 100644 --- a/tests/nvcv_types/cudatools_unit/TestLegacyHelpers.cpp +++ b/tests/nvcv_types/cudatools_unit/TestLegacyHelpers.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * NVIDIA CORPORATION, its affiliates and licensors retain all intellectual @@ -15,6 +15,8 @@ #include #include +#include + namespace gt = ::testing; namespace test = nvcv::test; namespace util = nvcv::util; @@ -236,9 +238,9 @@ TEST_P(CheckLegacyToString, check_error_to_string_conversion) std::string expectedErrorName = GetParamValue<1>(); std::string expectedDescr = GetParamValue<2>(); - char bufferDesc[256]; - const char *bufferDescPtr = bufferDesc; - const char *buffer = nvcv::util::ToString(err, &bufferDescPtr); + std::array bufferDesc; + const char *bufferDescPtr = bufferDesc.data(); + const char *buffer = nvcv::util::ToString(err, &bufferDescPtr); EXPECT_STREQ(bufferDescPtr, expectedDescr.c_str()); EXPECT_STREQ(buffer, expectedErrorName.c_str()); diff --git a/tests/nvcv_types/standalone/TestNVCVStandalone.cpp b/tests/nvcv_types/standalone/TestNVCVStandalone.cpp index ada80a34e..83ceaac40 100644 --- a/tests/nvcv_types/standalone/TestNVCVStandalone.cpp +++ b/tests/nvcv_types/standalone/TestNVCVStandalone.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,7 +21,9 @@ TEST(StandaloneTest, nvcv_can_be_used_standalone) { - constexpr nvcv::Size2D size12{1, 2}, size21{2, 1}, size22{2, 2}; + constexpr nvcv::Size2D size12{1, 2}; + constexpr nvcv::Size2D size21{2, 1}; + constexpr nvcv::Size2D size22{2, 2}; EXPECT_EQ(nvcv::MaxSize(size12, size21), size22); diff --git a/tests/nvcv_types/system/CMakeLists.txt b/tests/nvcv_types/system/CMakeLists.txt index 06173a761..adede9985 100644 --- a/tests/nvcv_types/system/CMakeLists.txt +++ b/tests/nvcv_types/system/CMakeLists.txt @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,6 +19,7 @@ set(CMAKE_FOLDER tests) # system core ------------------------------------------------- add_executable(nvcv_test_types_system TestVersion.cpp + TestVersionApiOverride.cpp TestStatus.cpp TestSize.cpp TestColorSpec.cpp @@ -42,6 +43,8 @@ add_executable(nvcv_test_types_system TestTensorBatch.cpp ) +set_source_files_properties(TestVersionApiOverride.cpp PROPERTIES COMPILE_DEFINITIONS NVCV_VERSION_API=160000) + target_link_libraries(nvcv_test_types_system PUBLIC nvcv_test_main diff --git a/tests/nvcv_types/system/TestAllocatorC.cpp b/tests/nvcv_types/system/TestAllocatorC.cpp index 754de3fd9..645ca0c41 100644 --- a/tests/nvcv_types/system/TestAllocatorC.cpp +++ b/tests/nvcv_types/system/TestAllocatorC.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,9 +20,11 @@ #include #include #include -#include #include +#include +#include +#include #include #include @@ -30,62 +32,96 @@ namespace t = ::testing; namespace test = nvcv::test; +namespace { + +NVCVMemoryBuffer AllocHost(int64_t size, int32_t align) +{ + return static_cast( + ::operator new (static_cast(size), std::align_val_t{static_cast(align)}, std::nothrow)); +} + +void FreeHost(NVCVMemoryBuffer ptr, int32_t align) noexcept +{ + ::operator delete (static_cast(ptr), std::align_val_t{static_cast(align)}); +} + +template +OpaquePointer OpaqueFromPointer(PointerType *ptr) noexcept +{ + return static_cast(static_cast(ptr)); +} + +template +PointerType *PointerFromOpaque(OpaquePointer ptr) noexcept +{ + return static_cast(static_cast(ptr)); +} + +} // namespace + TEST(AllocatorTest, CreateAndUseCustom) { - NVCVResourceAllocator allocators[2] = {}; + std::array allocators = {}; - int ctx0 = 100, ctx1 = 200; + int ctx0 = 100; + int ctx1 = 200; allocators[0].resType = NVCV_RESOURCE_MEM_HOST; - allocators[0].ctx = &ctx0; - allocators[0].res.mem.fnAlloc = [](void *ctx, int64_t size, int32_t align) + allocators[0].ctx = OpaqueFromPointer(&ctx0); + allocators[0].res.mem.fnAlloc = [](auto ctx, int64_t size, int32_t align) { - *(int *)ctx += 1; - return memalign(align, size); + *PointerFromOpaque(ctx) += 1; + return AllocHost(size, align); }; - allocators[0].res.mem.fnFree = [](void *ctx, void *ptr, int64_t size, int32_t align) + allocators[0].res.mem.fnFree = [](auto ctx, auto ptr, int64_t, int32_t align) { - *(int *)ctx += 10; - free(ptr); + *PointerFromOpaque(ctx) += 10; + FreeHost(ptr, align); }; - allocators[0].cleanup = [](void *ctx, NVCVResourceAllocator *alloc) + allocators[0].cleanup = [](auto ctx, auto *alloc) { EXPECT_EQ(ctx, alloc->ctx); - int *ctx_int = static_cast(ctx); + auto *ctx_int = PointerFromOpaque(ctx); EXPECT_EQ(*ctx_int, 111); *ctx_int = 0xDEAD; }; allocators[1].resType = NVCV_RESOURCE_MEM_CUDA; - allocators[1].ctx = &ctx1; - allocators[1].res.mem.fnAlloc = [](void *ctx, int64_t size, int32_t align) + allocators[1].ctx = OpaqueFromPointer(&ctx1); + allocators[1].res.mem.fnAlloc = [](auto ctx, int64_t size, int32_t) { - *(int *)ctx += 1; - void *mem; + *PointerFromOpaque(ctx) += 1; + void *mem = nullptr; EXPECT_EQ(cudaMalloc(&mem, size), cudaSuccess); - return mem; + return static_cast(mem); }; - allocators[1].res.mem.fnFree = [](void *ctx, void *ptr, int64_t size, int32_t align) + allocators[1].res.mem.fnFree = [](auto ctx, auto ptr, int64_t, int32_t) { - *(int *)ctx += 10; + *PointerFromOpaque(ctx) += 10; EXPECT_EQ(cudaFree(ptr), cudaSuccess); }; - allocators[1].cleanup = [](void *ctx, NVCVResourceAllocator *alloc) + allocators[1].cleanup = [](auto ctx, auto *alloc) { EXPECT_EQ(ctx, alloc->ctx); - int *ctx_int = static_cast(ctx); + auto *ctx_int = PointerFromOpaque(ctx); EXPECT_EQ(*ctx_int, 211); *ctx_int = 0xBAD; }; NVCVAllocatorHandle halloc = nullptr; - ASSERT_EQ(nvcvAllocatorConstructCustom(allocators, 2, &halloc), NVCV_SUCCESS); + ASSERT_EQ(nvcvAllocatorConstructCustom(allocators.data(), allocators.size(), &halloc), NVCV_SUCCESS); ASSERT_NE(halloc, nullptr); int refCount = 0; EXPECT_EQ(nvcvAllocatorRefCount(halloc, &refCount), NVCV_SUCCESS); EXPECT_EQ(refCount, 1); + int newRef = 0; + EXPECT_EQ(nvcvAllocatorIncRef(halloc, &newRef), NVCV_SUCCESS); + EXPECT_EQ(newRef, 2); + EXPECT_EQ(nvcvAllocatorDecRef(halloc, &newRef), NVCV_SUCCESS); + EXPECT_EQ(newRef, 1); + for (int i = 0; i < 2; i++) { NVCVResourceAllocator alloc = {}; @@ -108,7 +144,9 @@ TEST(AllocatorTest, CreateAndUseCustom) NVCVResourceAllocator pinnedAlloc{}; EXPECT_EQ(nvcvAllocatorGet(halloc, NVCV_RESOURCE_MEM_HOST_PINNED, &pinnedAlloc), NVCV_SUCCESS); - void *p0 = nullptr, *p1 = nullptr, *p2 = nullptr; + NVCVMemoryBuffer p0 = nullptr; + NVCVMemoryBuffer p1 = nullptr; + NVCVMemoryBuffer p2 = nullptr; EXPECT_EQ(nvcvAllocatorAllocHostMemory(halloc, &p0, (1 << 20), 256), NVCV_SUCCESS); EXPECT_NE(p0, nullptr); EXPECT_EQ(ctx0, 101) << "The custom alloc for host memory wasn't invoked"; @@ -126,7 +164,7 @@ TEST(AllocatorTest, CreateAndUseCustom) EXPECT_NE(p2, nullptr); EXPECT_EQ(nvcvAllocatorFreeHostPinnedMemory(halloc, p2, (1 << 20), 256), NVCV_SUCCESS); - int newRef = 1; + newRef = 1; EXPECT_EQ(nvcvAllocatorDecRef(halloc, &newRef), NVCV_SUCCESS); EXPECT_EQ(newRef, 0); EXPECT_EQ(ctx0, 0xDEAD); @@ -138,9 +176,9 @@ TEST(Allocator, smoke_test_default) { nvcv::CustomAllocator myalloc; - void *ptrDev = myalloc.cudaMem().alloc(768, 256); - void *ptrHost = myalloc.hostMem().alloc(160, 16); - void *ptrHostPinned = myalloc.hostPinnedMem().alloc(144, 16); + NVCVMemoryBuffer ptrDev = myalloc.cudaMem().alloc(768, 256); + NVCVMemoryBuffer ptrHost = myalloc.hostMem().alloc(160, 16); + NVCVMemoryBuffer ptrHostPinned = myalloc.hostPinnedMem().alloc(144, 16); myalloc.cudaMem().free(ptrDev, 768, 256); myalloc.hostMem().free(ptrHost, 160, 16); @@ -150,113 +188,118 @@ TEST(Allocator, smoke_test_default) // smoke: just to check if it compiles. TEST(Allocator, smoke_test_custom_functors) { - int devCounter = 1; - int hostCounter = 1; - int hostPinnedCounter = 1; + int devCounter = 1; + int hostCounter = 1; + int hostPinnedCounter = 1; + std::array devBuffers{}; + std::array hostBuffers{}; + std::array hostPinnedBuffers{}; // clang-format off nvcv::CustomAllocator myalloc1 { nvcv::CustomHostMemAllocator { - [&hostCounter](int64_t size, int32_t align) + [&hostCounter, &hostBuffers](int64_t size, int32_t) { - void *ptr = reinterpret_cast(hostCounter); + auto ptr = OpaqueFromPointer(&hostBuffers[hostCounter]); hostCounter += size; return ptr; }, - [&hostCounter](void *ptr, int64_t size, int32_t align) + [&hostCounter, &hostBuffers](const NVCVMemoryBufferRec *ptr, int64_t size, int32_t) { hostCounter -= size; - assert(hostCounter == reinterpret_cast(ptr)); + assert(ptr == OpaqueFromPointer(&hostBuffers[hostCounter])); } }, nvcv::CustomCudaMemAllocator { - [&devCounter](int64_t size, int32_t align) + [&devCounter, &devBuffers](int64_t size, int32_t) { - void *ptr = reinterpret_cast(devCounter); + auto ptr = OpaqueFromPointer(&devBuffers[devCounter]); devCounter += size; return ptr; }, - [&devCounter](void *ptr, int64_t size, int32_t align) + [&devCounter, &devBuffers](const NVCVMemoryBufferRec *ptr, int64_t size, int32_t) { devCounter -= size; - assert(devCounter == reinterpret_cast(ptr)); + assert(ptr == OpaqueFromPointer(&devBuffers[devCounter])); } }, nvcv::CustomHostPinnedMemAllocator { - [&hostPinnedCounter](int64_t size, int32_t align) + [&hostPinnedCounter, &hostPinnedBuffers](int64_t size, int32_t) { - void *ptr = reinterpret_cast(hostPinnedCounter); + auto ptr = OpaqueFromPointer(&hostPinnedBuffers[hostPinnedCounter]); hostPinnedCounter += size; return ptr; }, - [&hostPinnedCounter](void *ptr, int64_t size, int32_t align) + [&hostPinnedCounter, &hostPinnedBuffers](const NVCVMemoryBufferRec *ptr, int64_t size, int32_t) { hostPinnedCounter -= size; - assert(hostPinnedCounter == reinterpret_cast(ptr)); + assert(ptr == OpaqueFromPointer(&hostPinnedBuffers[hostPinnedCounter])); } }, }; // clang-format on - ASSERT_EQ((void *)1, myalloc1.hostMem().alloc(5)); + ASSERT_EQ(OpaqueFromPointer(&hostBuffers[1]), myalloc1.hostMem().alloc(5)); EXPECT_EQ(6, hostCounter); - ASSERT_EQ((void *)1, myalloc1.hostPinnedMem().alloc(10)); + ASSERT_EQ(OpaqueFromPointer(&hostPinnedBuffers[1]), myalloc1.hostPinnedMem().alloc(10)); EXPECT_EQ(11, hostPinnedCounter); - ASSERT_EQ((void *)1, myalloc1.cudaMem().alloc(7)); + ASSERT_EQ(OpaqueFromPointer(&devBuffers[1]), myalloc1.cudaMem().alloc(7)); EXPECT_EQ(8, devCounter); - ASSERT_EQ((void *)8, myalloc1.cudaMem().alloc(2)); + ASSERT_EQ(OpaqueFromPointer(&devBuffers[8]), myalloc1.cudaMem().alloc(2)); EXPECT_EQ(10, devCounter); - myalloc1.cudaMem().free((void *)8, 2); + myalloc1.cudaMem().free(OpaqueFromPointer(&devBuffers[8]), 2); EXPECT_EQ(8, devCounter); - myalloc1.cudaMem().free((void *)1, 7); + myalloc1.cudaMem().free(OpaqueFromPointer(&devBuffers[1]), 7); EXPECT_EQ(1, devCounter); } TEST(AllocatorTest, smoke_user_pointer) { - NVCVResourceAllocator allocators[1] = {}; + std::array allocators = {}; int ctx0 = 100; allocators[0].resType = NVCV_RESOURCE_MEM_HOST; - allocators[0].ctx = &ctx0; - allocators[0].res.mem.fnAlloc = [](void *ctx, int64_t size, int32_t align) + allocators[0].ctx = OpaqueFromPointer(&ctx0); + allocators[0].res.mem.fnAlloc = [](auto ctx, int64_t size, int32_t align) { - *(int *)ctx += 1; - return memalign(align, size); + *PointerFromOpaque(ctx) += 1; + return AllocHost(size, align); }; - allocators[0].res.mem.fnFree = [](void *ctx, void *ptr, int64_t size, int32_t align) + allocators[0].res.mem.fnFree = [](auto ctx, auto ptr, int64_t, int32_t align) { - *(int *)ctx += 10; - free(ptr); + *PointerFromOpaque(ctx) += 10; + FreeHost(ptr, align); }; - allocators[0].cleanup = [](void *ctx, NVCVResourceAllocator *alloc) + allocators[0].cleanup = [](auto ctx, auto *alloc) { EXPECT_EQ(ctx, alloc->ctx); - int *ctx_int = static_cast(ctx); - *ctx_int = 0xDEAD; + auto *ctx_int = PointerFromOpaque(ctx); + *ctx_int = 0xDEAD; }; NVCVAllocatorHandle halloc = nullptr; - ASSERT_EQ(nvcvAllocatorConstructCustom(allocators, 1, &halloc), NVCV_SUCCESS); + ASSERT_EQ(nvcvAllocatorConstructCustom(allocators.data(), allocators.size(), &halloc), NVCV_SUCCESS); ASSERT_NE(halloc, nullptr); - void *userPtr; + NVCVUserPointer userPtr; ASSERT_EQ(nvcvAllocatorGetUserPointer(halloc, &userPtr), NVCV_SUCCESS); EXPECT_EQ(nullptr, userPtr); - ASSERT_EQ(nvcvAllocatorSetUserPointer(halloc, (void *)0x123), NVCV_SUCCESS); + int userData = 0x123; + NVCVUserPointer expectedPtr = OpaqueFromPointer(&userData); + ASSERT_EQ(nvcvAllocatorSetUserPointer(halloc, expectedPtr), NVCV_SUCCESS); ASSERT_EQ(nvcvAllocatorGetUserPointer(halloc, &userPtr), NVCV_SUCCESS); - EXPECT_EQ((void *)0x123, userPtr); + EXPECT_EQ(expectedPtr, userPtr); ASSERT_EQ(nvcvAllocatorSetUserPointer(halloc, nullptr), NVCV_SUCCESS); ASSERT_EQ(nvcvAllocatorGetUserPointer(halloc, &userPtr), NVCV_SUCCESS); @@ -269,38 +312,32 @@ TEST(AllocatorTest, smoke_user_pointer) TEST(AllocatorTest, invalid_arguments_api_calls) { - NVCVResourceAllocator allocators[2] = {}; + std::array allocators = {}; allocators[0].resType = NVCV_RESOURCE_MEM_HOST; - allocators[0].res.mem.fnAlloc = [](void *ctx, int64_t size, int32_t align) + allocators[0].res.mem.fnAlloc = [](auto, int64_t size, int32_t align) { - return memalign(align, size); + return AllocHost(size, align); }; - allocators[0].res.mem.fnFree = [](void *ctx, void *ptr, int64_t size, int32_t align) + allocators[0].res.mem.fnFree = [](auto, auto ptr, int64_t, int32_t align) { - free(ptr); - }; - allocators[0].cleanup = [](void *ctx, NVCVResourceAllocator *alloc) { + FreeHost(ptr, align); }; - allocators[1].resType = NVCV_RESOURCE_MEM_CUDA; - allocators[1].res.mem.fnAlloc = [](void *ctx, int64_t size, int32_t align) + allocators[1].res.mem.fnAlloc = [](auto, int64_t size, int32_t) { - void *mem; + void *mem = nullptr; EXPECT_EQ(cudaMalloc(&mem, size), cudaSuccess); - return mem; + return static_cast(mem); }; - allocators[1].res.mem.fnFree = [](void *ctx, void *ptr, int64_t size, int32_t align) + allocators[1].res.mem.fnFree = [](auto, auto ptr, int64_t, int32_t) { EXPECT_EQ(cudaFree(ptr), cudaSuccess); }; - allocators[1].cleanup = [](void *ctx, NVCVResourceAllocator *alloc) { - }; - NVCVAllocatorHandle halloc = nullptr; // 1. Pointer to output handle must not be NULL - EXPECT_EQ(nvcvAllocatorConstructCustom(allocators, 2, nullptr), NVCV_ERROR_INVALID_ARGUMENT); - ASSERT_EQ(nvcvAllocatorConstructCustom(allocators, 2, &halloc), NVCV_SUCCESS); + EXPECT_EQ(nvcvAllocatorConstructCustom(allocators.data(), allocators.size(), nullptr), NVCV_ERROR_INVALID_ARGUMENT); + ASSERT_EQ(nvcvAllocatorConstructCustom(allocators.data(), allocators.size(), &halloc), NVCV_SUCCESS); ASSERT_NE(halloc, nullptr); // 2. Pointer to output user pointer cannot be NULL @@ -312,7 +349,7 @@ TEST(AllocatorTest, invalid_arguments_api_calls) EXPECT_EQ(nvcvAllocatorAllocCudaMemory(halloc, nullptr, (1 << 10), 256), NVCV_ERROR_INVALID_ARGUMENT); // 4. allocHostMem - void *p0 = nullptr; + NVCVMemoryBuffer p0 = nullptr; EXPECT_EQ(nvcvAllocatorAllocHostMemory(halloc, &p0, -1, 256), NVCV_ERROR_INVALID_ARGUMENT); EXPECT_EQ(nvcvAllocatorAllocHostMemory(halloc, &p0, (1 << 10), 3), NVCV_ERROR_INVALID_ARGUMENT); EXPECT_EQ(nvcvAllocatorAllocHostMemory(halloc, &p0, 128, 256), NVCV_ERROR_INVALID_ARGUMENT); @@ -334,62 +371,54 @@ TEST(AllocatorTest, invalid_arguments_api_calls) TEST(AllocatorTest, customAllocator_constructor_negative) { - NVCVResourceAllocator invalidFnAllocAllocator[1] = {}; - NVCVResourceAllocator invalidFnFreeAllocator[1] = {}; - NVCVResourceAllocator duplicatedResourceTypeAllocator[2] = {}; + std::array invalidFnAllocAllocator = {}; + std::array invalidFnFreeAllocator = {}; + std::array duplicatedResourceTypeAllocator = {}; // 1. allocation function must not be NULL invalidFnAllocAllocator[0].resType = NVCV_RESOURCE_MEM_HOST; - invalidFnAllocAllocator[0].res.mem.fnFree = [](void *ctx, void *ptr, int64_t size, int32_t align) + invalidFnAllocAllocator[0].res.mem.fnFree = [](auto, auto ptr, int64_t, int32_t align) { - free(ptr); - }; - invalidFnAllocAllocator[0].cleanup = [](void *ctx, NVCVResourceAllocator *alloc) { + FreeHost(ptr, align); }; - NVCVAllocatorHandle halloc = nullptr; - EXPECT_EQ(nvcvAllocatorConstructCustom(invalidFnAllocAllocator, 1, &halloc), NVCV_ERROR_INVALID_ARGUMENT); + EXPECT_EQ(nvcvAllocatorConstructCustom(invalidFnAllocAllocator.data(), invalidFnAllocAllocator.size(), &halloc), + NVCV_ERROR_INVALID_ARGUMENT); // 2. deallocation function must not be NULL invalidFnFreeAllocator[0].resType = NVCV_RESOURCE_MEM_CUDA; - invalidFnFreeAllocator[0].res.mem.fnAlloc = [](void *ctx, int64_t size, int32_t align) + invalidFnFreeAllocator[0].res.mem.fnAlloc = [](auto, int64_t size, int32_t) { - void *mem; + void *mem = nullptr; EXPECT_EQ(cudaMalloc(&mem, size), cudaSuccess); - return mem; - }; - invalidFnFreeAllocator[0].cleanup = [](void *ctx, NVCVResourceAllocator *alloc) { + return static_cast(mem); }; - - EXPECT_EQ(nvcvAllocatorConstructCustom(invalidFnFreeAllocator, 1, &halloc), NVCV_ERROR_INVALID_ARGUMENT); + EXPECT_EQ(nvcvAllocatorConstructCustom(invalidFnFreeAllocator.data(), invalidFnFreeAllocator.size(), &halloc), + NVCV_ERROR_INVALID_ARGUMENT); // 3. duplicated resource type duplicatedResourceTypeAllocator[0].resType = NVCV_RESOURCE_MEM_HOST; - duplicatedResourceTypeAllocator[0].res.mem.fnAlloc = [](void *ctx, int64_t size, int32_t align) + duplicatedResourceTypeAllocator[0].res.mem.fnAlloc = [](auto, int64_t size, int32_t align) { - return memalign(align, size); + return AllocHost(size, align); }; - duplicatedResourceTypeAllocator[0].res.mem.fnFree = [](void *ctx, void *ptr, int64_t size, int32_t align) + duplicatedResourceTypeAllocator[0].res.mem.fnFree = [](auto, auto ptr, int64_t, int32_t align) { - free(ptr); - }; - duplicatedResourceTypeAllocator[0].cleanup = [](void *ctx, NVCVResourceAllocator *alloc) { + FreeHost(ptr, align); }; - duplicatedResourceTypeAllocator[1].resType = NVCV_RESOURCE_MEM_HOST; - duplicatedResourceTypeAllocator[1].res.mem.fnAlloc = [](void *ctx, int64_t size, int32_t align) + duplicatedResourceTypeAllocator[1].res.mem.fnAlloc = [](auto, int64_t size, int32_t align) { - return memalign(align, size); + return AllocHost(size, align); }; - duplicatedResourceTypeAllocator[1].res.mem.fnFree = [](void *ctx, void *ptr, int64_t size, int32_t align) + duplicatedResourceTypeAllocator[1].res.mem.fnFree = [](auto, auto ptr, int64_t, int32_t align) { - free(ptr); - }; - duplicatedResourceTypeAllocator[1].cleanup = [](void *ctx, NVCVResourceAllocator *alloc) { + FreeHost(ptr, align); }; - - EXPECT_EQ(nvcvAllocatorConstructCustom(duplicatedResourceTypeAllocator, 2, &halloc), NVCV_ERROR_INVALID_ARGUMENT); + EXPECT_EQ(nvcvAllocatorConstructCustom(duplicatedResourceTypeAllocator.data(), + duplicatedResourceTypeAllocator.size(), &halloc), + NVCV_ERROR_INVALID_ARGUMENT); } TEST(AllocatorTest, get_name) diff --git a/tests/nvcv_types/system/TestAllocatorCpp.cpp b/tests/nvcv_types/system/TestAllocatorCpp.cpp index 9ed778f64..748cab466 100644 --- a/tests/nvcv_types/system/TestAllocatorCpp.cpp +++ b/tests/nvcv_types/system/TestAllocatorCpp.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,19 +17,39 @@ #include "Definitions.hpp" -#include #include #include +#include +#include +#include +#include namespace n = nvcv; +namespace { + +std::byte *AllocHost(int64_t size, int32_t align) +{ + return static_cast( + ::operator new (static_cast(size), std::align_val_t{static_cast(align)}, std::nothrow)); +} + +template +void FreeHost(PointerType *ptr, int32_t align) noexcept +{ + ::operator delete (ptr, std::align_val_t{static_cast(align)}); +} + +} // namespace + TEST(AllocatorTest, FromEmpty) { // Use thread-local variables because they don't need to be captured - thread_local bool alloc_called, free_called; - thread_local int64_t allocated_size; - thread_local void *allocated_ptr; + thread_local bool alloc_called; + thread_local bool free_called; + thread_local int64_t allocated_size; + thread_local NVCVMemoryBuffer allocated_ptr; alloc_called = false; free_called = false; @@ -41,23 +61,23 @@ TEST(AllocatorTest, FromEmpty) { alloc_called = true; allocated_size = size; - allocated_ptr = memalign(align, size); + allocated_ptr = static_cast(static_cast(AllocHost(size, align))); return allocated_ptr; }, - [](void *mem, int64_t size, int32_t align) + [](NVCVMemoryBuffer mem, int64_t, int32_t align) { free_called = true; EXPECT_EQ(allocated_ptr, mem); - free(mem); + FreeHost(mem, align); }); EXPECT_FALSE(alloc.needsCleanup()); - void *ctx = alloc.cdata().ctx; + NVCVResourceContext ctx = alloc.cdata().ctx; EXPECT_EQ(ctx, nullptr); auto &mem_alloc = alloc.cdata().res.mem; - void *ptr = mem_alloc.fnAlloc(ctx, 123, 16); + NVCVMemoryBuffer ptr = mem_alloc.fnAlloc(ctx, 123, 16); EXPECT_TRUE(alloc_called); EXPECT_EQ(allocated_size, 123); EXPECT_EQ(ptr, allocated_ptr); @@ -68,33 +88,35 @@ TEST(AllocatorTest, FromEmpty) TEST(AllocatorTest, FromSmall) { // Use thread-local variables because they don't need to be captured - thread_local bool alloc_called, free_called; + thread_local bool alloc_called; + thread_local bool free_called; alloc_called = false; free_called = false; - int16_t c1 = 123, c2 = 321; + int16_t c1 = 123; + int16_t c2 = 321; n::CustomMemAllocator alloc( [c1](int64_t size, int32_t align) { alloc_called = true; EXPECT_EQ(c1, 123); - return memalign(align, size); + return AllocHost(size, align); }, - [c2](void *mem, int64_t size, int32_t align) + [c2](NVCVMemoryBuffer mem, int64_t, int32_t align) { free_called = true; EXPECT_EQ(c2, 321); - free(mem); + FreeHost(mem, align); }); EXPECT_FALSE(alloc.needsCleanup()); - void *ctx = alloc.cdata().ctx; + NVCVResourceContext ctx = alloc.cdata().ctx; EXPECT_NE(ctx, nullptr); auto &mem_alloc = alloc.cdata().res.mem; - void *ptr = mem_alloc.fnAlloc(ctx, 123, 16); + NVCVMemoryBuffer ptr = mem_alloc.fnAlloc(ctx, 123, 16); EXPECT_TRUE(alloc_called); mem_alloc.fnFree(ctx, ptr, 123, 16); EXPECT_TRUE(free_called); @@ -102,43 +124,113 @@ TEST(AllocatorTest, FromSmall) TEST(AllocatorTest, FromDuplicate) { - // Use thread-local variables because they don't need to be captured - thread_local bool alloc_called, free_called; - alloc_called = false; - free_called = false; + struct Status + { + bool alloc_called = false; + bool free_called = false; + intptr_t value = 0x12345678; + std::byte *allocated = nullptr; + }; - intptr_t c = 0x12345678; + Status status; - n::CustomMemAllocator alloc( - [c](int64_t size, int32_t align) + struct DuplicateFunctor + { + Status *status; + + std::byte *operator()(int64_t size, int32_t align) const { - alloc_called = true; - EXPECT_EQ(c, 0x12345678); - return memalign(align, size); - }, - [c](void *mem, int64_t size, int32_t align) + status->alloc_called = true; + EXPECT_EQ(status->value, 0x12345678); + status->allocated = AllocHost(size, align); + return status->allocated; + } + + void operator()(NVCVMemoryBuffer mem, int64_t, int32_t align) const { - free_called = true; - EXPECT_EQ(c, 0x12345678); - free(mem); - }); + status->free_called = true; + EXPECT_EQ(status->value, 0x12345678); + EXPECT_EQ(status->allocated, reinterpret_cast(mem)); + FreeHost(mem, align); + } + }; + + n::CustomMemAllocator alloc(DuplicateFunctor{&status}, DuplicateFunctor{&status}); EXPECT_FALSE(alloc.needsCleanup()); - void *ctx = alloc.cdata().ctx; + NVCVResourceContext ctx = alloc.cdata().ctx; EXPECT_NE(ctx, nullptr); auto &mem_alloc = alloc.cdata().res.mem; - void *ptr = mem_alloc.fnAlloc(ctx, 123, 16); - EXPECT_TRUE(alloc_called); + NVCVMemoryBuffer ptr = mem_alloc.fnAlloc(ctx, 123, 16); + EXPECT_TRUE(status.alloc_called); mem_alloc.fnFree(ctx, ptr, 123, 16); - EXPECT_TRUE(free_called); + EXPECT_TRUE(status.free_called); +} + +TEST(AllocatorTest, FromDuplicateDifferentTypesMustNotShareStorage) +{ + struct Status + { + bool alloc_called = false; + bool free_called = false; + std::byte *allocated = nullptr; + }; + + Status status; + + struct AllocFunctor + { + Status *status; + + std::byte *operator()(int64_t size, int32_t align) const + { + status->alloc_called = true; + status->allocated = AllocHost(size, align); + return status->allocated; + } + }; + + struct FreeFunctor + { + Status *status; + + void operator()(NVCVMemoryBuffer mem, int64_t, int32_t align) const + { + status->free_called = true; + EXPECT_EQ(status->allocated, reinterpret_cast(mem)); + FreeHost(mem, align); + } + }; + + static_assert(sizeof(AllocFunctor) == sizeof(FreeFunctor), "Test requires equal-size functors"); + static_assert(std::is_trivially_copyable_v, "Test requires a trivial alloc functor"); + static_assert(std::is_trivially_copyable_v, "Test requires a trivial free functor"); + + AllocFunctor allocFn{&status}; + FreeFunctor freeFn{&status}; + ASSERT_EQ(0, std::memcmp(&allocFn, &freeFn, sizeof(allocFn))); + + n::CustomMemAllocator alloc(AllocFunctor{&status}, FreeFunctor{&status}); + + EXPECT_TRUE(alloc.needsCleanup()); + NVCVResourceContext ctx = alloc.cdata().ctx; + EXPECT_NE(ctx, nullptr); + + auto &mem_alloc = alloc.cdata().res.mem; + + NVCVMemoryBuffer ptr = mem_alloc.fnAlloc(ctx, 123, 16); + EXPECT_TRUE(status.alloc_called); + mem_alloc.fnFree(ctx, ptr, 123, 16); + EXPECT_TRUE(status.free_called); } TEST(AllocatorTest, FromComplexType) { // Use thread-local variables because they don't need to be captured - thread_local bool alloc_called, free_called; + thread_local bool alloc_called; + thread_local bool free_called; alloc_called = false; free_called = false; @@ -147,6 +239,13 @@ TEST(AllocatorTest, FromComplexType) struct Dummy { + Dummy() = default; + + Dummy(const Dummy &) = delete; + Dummy(Dummy &&) = delete; + Dummy &operator=(const Dummy &) = delete; + Dummy &operator=(Dummy &&) = delete; + ~Dummy() { val = -1; @@ -164,24 +263,24 @@ TEST(AllocatorTest, FromComplexType) { alloc_called = true; EXPECT_EQ(p->val, 0x12345678); - return memalign(align, size); + return AllocHost(size, align); }, - [p](void *mem, int64_t size, int32_t align) + [p](NVCVMemoryBuffer mem, int64_t, int32_t align) { free_called = true; EXPECT_EQ(p->val, 0x12345678); - free(mem); + FreeHost(mem, align); }); p.reset(); EXPECT_FALSE(destroyed); EXPECT_TRUE(alloc.needsCleanup()); - void *ctx = alloc.cdata().ctx; + NVCVResourceContext ctx = alloc.cdata().ctx; EXPECT_NE(ctx, nullptr); auto &mem_alloc = alloc.cdata().res.mem; - void *ptr = mem_alloc.fnAlloc(ctx, 123, 16); + NVCVMemoryBuffer ptr = mem_alloc.fnAlloc(ctx, 123, 16); EXPECT_TRUE(alloc_called); mem_alloc.fnFree(ctx, ptr, 123, 16); EXPECT_TRUE(free_called); @@ -206,22 +305,22 @@ TEST(AllocatorTest, ConstructCustom) [](int64_t size, int32_t align) { status.host_alloc_called = true; - return memalign(align, size); + return AllocHost(size, align); }, - [](void *mem, int64_t size, int32_t align) + [](NVCVMemoryBuffer mem, int64_t, int32_t align) { status.host_free_called = true; - return free(mem); + FreeHost(mem, align); }), n::CustomCudaMemAllocator( - [](int64_t size, int32_t align) + [](int64_t size, int32_t) { status.cuda_alloc_called = true; - void *mem; + void *mem = nullptr; EXPECT_EQ(cudaMalloc(&mem, size), cudaSuccess); - return mem; + return static_cast(mem); }, - [](void *mem, int64_t size, int32_t align) + [](NVCVMemoryBuffer mem, int64_t, int32_t) { status.cuda_free_called = true; EXPECT_EQ(cudaFree(mem), cudaSuccess); @@ -232,7 +331,7 @@ TEST(AllocatorTest, ConstructCustom) ASSERT_FALSE(status.host_alloc_called); ASSERT_FALSE(status.host_free_called); - void *cumem = ca.cudaMem().alloc(256); + NVCVMemoryBuffer cumem = ca.cudaMem().alloc(256); EXPECT_TRUE(status.cuda_alloc_called); ca.cudaMem().free(cumem, 256); EXPECT_TRUE(status.cuda_free_called); @@ -261,6 +360,13 @@ TEST(AllocatorTest, ConstructCustomWithDeleter) struct Dummy { + Dummy() = default; + + Dummy(const Dummy &) = delete; + Dummy(Dummy &&) = delete; + Dummy &operator=(const Dummy &) = delete; + Dummy &operator=(Dummy &&) = delete; + ~Dummy() { val = -1; @@ -276,22 +382,22 @@ TEST(AllocatorTest, ConstructCustomWithDeleter) [sh](int64_t size, int32_t align) { status.host_alloc_called = true; - return memalign(align, size); + return AllocHost(size, align); }, - [sh](void *mem, int64_t size, int32_t align) + [sh](NVCVMemoryBuffer mem, int64_t, int32_t align) { status.host_free_called = true; - return free(mem); + FreeHost(mem, align); }), n::CustomCudaMemAllocator( - [sh](int64_t size, int32_t align) + [sh](int64_t size, int32_t) { status.cuda_alloc_called = true; - void *mem; + void *mem = nullptr; EXPECT_EQ(cudaMalloc(&mem, size), cudaSuccess); - return mem; + return static_cast(mem); }, - [sh](void *mem, int64_t size, int32_t align) + [sh](NVCVMemoryBuffer mem, int64_t, int32_t) { status.cuda_free_called = true; EXPECT_EQ(cudaFree(mem), cudaSuccess); @@ -306,7 +412,7 @@ TEST(AllocatorTest, ConstructCustomWithDeleter) ASSERT_FALSE(status.host_alloc_called); ASSERT_FALSE(status.host_free_called); - void *cumem = ca.cudaMem().alloc(256); + NVCVMemoryBuffer cumem = ca.cudaMem().alloc(256); EXPECT_TRUE(status.cuda_alloc_called); ca.cudaMem().free(cumem, 256); EXPECT_TRUE(status.cuda_free_called); diff --git a/tests/nvcv_types/system/TestArray.cpp b/tests/nvcv_types/system/TestArray.cpp index adca3abf8..0f9165b3c 100644 --- a/tests/nvcv_types/system/TestArray.cpp +++ b/tests/nvcv_types/system/TestArray.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,6 +23,7 @@ #include #include +#include #include #include #include @@ -32,29 +33,13 @@ namespace t = ::testing; namespace test = nvcv::test; -// clang-format off -namespace std { +namespace { -template -std::ostream &operator<<(std::ostream &out, const std::vector &vec) -{ - out << '{'; - std::string sep = ""; - for (auto value: vec) - { - out << sep << value; - sep = ","; - } - return out << '}'; -} - -} // namespace std - -// clang-format on +const std::vector g_testCapacities = {1, 2, 1024, 1048576}; +const std::vector g_testDataTypes = {nvcv::TYPE_U8, nvcv::TYPE_F16, nvcv::TYPE_F32, nvcv::TYPE_4F64}; +const std::vector g_testResourceTypes = {NVCV_RESOURCE_MEM_CUDA, NVCV_RESOURCE_MEM_HOST}; -static std::vector g_testCapacities = {1, 2, 1024, 1048576}; -static std::vector g_testDataTypes = {nvcv::TYPE_U8, nvcv::TYPE_F32, nvcv::TYPE_4F64}; -static std::vector g_testResourceTypes = {NVCV_RESOURCE_MEM_CUDA, NVCV_RESOURCE_MEM_HOST}; +} // namespace class ArrayTests : public t::TestWithParam> { @@ -66,13 +51,10 @@ class ArrayTests : public t::TestWithParam(paramPack); - auto dtype = std::get<1>(paramPack); - auto target = std::get<2>(paramPack); + auto [capacity, dtype, target] = info.param; result += "mem" + std::string(nvcvResourceTypeGetName(target)).substr(18) + "__"; - result += "dtype" + std::string(nvcvDataTypeGetName(dtype)).substr(15) + "__"; + result += "dtype" + std::string(nvcvDataTypeGetName(static_cast(dtype))).substr(15) + "__"; result += "cap" + std::to_string(capacity); return result; @@ -82,15 +64,11 @@ class ArrayTests : public t::TestWithParam(paramPack); - auto dtype = std::get<1>(paramPack); - auto target = std::get<2>(paramPack); + const auto &[capacity, dtype, target] = GetParam(); NVCVArrayHandle handle; NVCVArrayRequirements reqs; - ASSERT_EQ(NVCV_SUCCESS, nvcvArrayCalcRequirements(capacity, dtype, 0, &reqs)); + ASSERT_EQ(NVCV_SUCCESS, nvcvArrayCalcRequirements(capacity, static_cast(dtype), 0, &reqs)); ASSERT_EQ(NVCV_SUCCESS, nvcvArrayConstructWithTarget(&reqs, nullptr, target, &handle)); int ref; @@ -111,28 +89,20 @@ TEST_P(ArrayTests, smoke_c_create) TEST_P(ArrayTests, smoke_cxx_create) { - auto paramPack = GetParam(); - - auto capacity = std::get<0>(paramPack); - auto dtype = std::get<1>(paramPack); - auto target = std::get<2>(paramPack); + const auto &[capacity, dtype, target] = GetParam(); - nvcv::Array *pArray = nullptr; + nvcv::Array array; - ASSERT_NO_THROW(pArray = new nvcv::Array(capacity, dtype, 0, target)); + ASSERT_NO_THROW(array = nvcv::Array(capacity, dtype, 0, target)); - EXPECT_EQ(pArray->target(), target); - EXPECT_EQ(pArray->dtype(), dtype); - EXPECT_EQ(pArray->capacity(), capacity); - EXPECT_EQ(pArray->length(), 0); + EXPECT_EQ(array.target(), target); + EXPECT_EQ(array.dtype(), dtype); + EXPECT_EQ(array.capacity(), capacity); + EXPECT_EQ(array.length(), 0); - auto data = pArray->exportData(); + auto data = array.exportData(); ASSERT_TRUE(data); EXPECT_NE(data->basePtr(), nullptr); - - EXPECT_NO_THROW(delete pArray); - - pArray = nullptr; } INSTANTIATE_TEST_SUITE_P(_, ArrayTests, @@ -146,11 +116,7 @@ class ArrayWrapTests : public ArrayTests TEST_P(ArrayWrapTests, smoke_create) { - auto paramPack = GetParam(); - - auto capacity = std::get<0>(paramPack); - auto dtype = std::get<1>(paramPack); - auto target = std::get<2>(paramPack); + const auto &[capacity, dtype, target] = GetParam(); nvcv::Array baseArray(capacity, dtype, dtype.alignment(), target); @@ -207,9 +173,9 @@ TEST(ArrayTests, smoke_create_allocator) void *ptr = nullptr; cudaMalloc(&ptr, size); - return ptr; + return static_cast(ptr); }, - [](void *ptr, int64_t bufLen, int32_t bufAlign) + [](auto ptr, int64_t, int32_t) { cudaFree(ptr); } @@ -290,7 +256,8 @@ TEST(ArrayTests, mismatch_construct_with_target) { NVCVArrayRequirements req; NVCVArrayHandle arrayHandle; - int64_t capacity = -1, length = -1; + int64_t capacity = -1; + int64_t length = -1; NVCVResourceType target; NVCVDataType dType; EXPECT_EQ(NVCV_SUCCESS, @@ -347,11 +314,16 @@ TEST(ArrayTests, invalid_handle_wrap_data_construct) void arrayDataCleanUpFunc(void *ctx, const NVCVArrayData *data); -void arrayDataCleanUpFunc(void *ctx, const NVCVArrayData *data) {} +void arrayDataCleanUpFunc(void *ctx, const NVCVArrayData *data) +{ + (void)ctx; + (void)data; +} TEST(ArrayTests, valid_handle_wrap_data_construct) { - NVCVArrayHandle arrayHandle, arrayHandle2; + NVCVArrayHandle arrayHandle; + NVCVArrayHandle arrayHandle2; NVCVArrayRequirements req; NVCVArrayData arrayData; EXPECT_EQ(NVCV_SUCCESS, nvcvArrayCalcRequirements(16, NVCV_DATA_TYPE_U8, 0, &req)); @@ -364,7 +336,8 @@ TEST(ArrayTests, valid_handle_wrap_data_construct) TEST(ArrayTests, valid_handle_wrap_data_construct_pinned) { - NVCVArrayHandle arrayHandle, arrayHandle2; + NVCVArrayHandle arrayHandle; + NVCVArrayHandle arrayHandle2; NVCVArrayRequirements req; NVCVArrayData arrayData; EXPECT_EQ(NVCV_SUCCESS, @@ -407,7 +380,7 @@ TEST(ArrayTests, smoke_user_pointer) { NVCVArrayHandle arrayHandle; NVCVArrayRequirements req; - void *userPtr; + NVCVUserPointer userPtr; EXPECT_EQ(NVCV_SUCCESS, nvcvArrayCalcRequirements(16, NVCV_DATA_TYPE_U8, 0, &req)); EXPECT_EQ(NVCV_SUCCESS, nvcvArrayConstruct(&req, nullptr, &arrayHandle)); @@ -415,9 +388,9 @@ TEST(ArrayTests, smoke_user_pointer) EXPECT_EQ(NVCV_SUCCESS, nvcvArrayGetUserPointer(arrayHandle, &userPtr)); EXPECT_EQ(nullptr, userPtr); - EXPECT_EQ(NVCV_SUCCESS, nvcvArraySetUserPointer(arrayHandle, reinterpret_cast(0x123ULL))); + EXPECT_EQ(NVCV_SUCCESS, nvcvArraySetUserPointer(arrayHandle, reinterpret_cast(0x123ULL))); EXPECT_EQ(NVCV_SUCCESS, nvcvArrayGetUserPointer(arrayHandle, &userPtr)); - EXPECT_EQ(reinterpret_cast(0x123ULL), userPtr); + EXPECT_EQ(reinterpret_cast(0x123ULL), userPtr); EXPECT_EQ(NVCV_SUCCESS, nvcvArraySetUserPointer(arrayHandle, nullptr)); EXPECT_EQ(NVCV_SUCCESS, nvcvArrayGetUserPointer(arrayHandle, &userPtr)); @@ -454,7 +427,7 @@ TEST(ArrayTests, valid_get_allocator) int tmp = 1; NVCVArrayHandle arrayHandle; NVCVArrayRequirements req; - NVCVAllocatorHandle alloc = reinterpret_cast(&tmp); + auto alloc = reinterpret_cast(&tmp); EXPECT_NE(alloc, nullptr); EXPECT_EQ(NVCV_SUCCESS, nvcvArrayCalcRequirements(16, NVCV_DATA_TYPE_U8, 0, &req)); @@ -556,7 +529,8 @@ TEST(ArrayTests, invalidResize) TEST(ArrayWrapTests, validResize) { - NVCVArrayHandle arrayHandle, arrayWrapHandle; + NVCVArrayHandle arrayHandle; + NVCVArrayHandle arrayWrapHandle; NVCVArrayData arrayData; NVCVArrayRequirements req; int64_t length = 0; @@ -576,7 +550,8 @@ TEST(ArrayWrapTests, validResize) TEST(ArrayWrapTests, invalidResize) { - NVCVArrayHandle arrayHandle, arrayWrapHandle; + NVCVArrayHandle arrayHandle; + NVCVArrayHandle arrayWrapHandle; NVCVArrayData arrayData; NVCVArrayRequirements req; @@ -594,10 +569,11 @@ TEST(ArrayWrapTests, invalidResize) TEST(ArrayWrapTests, valid_get_allocator) { int tmp = 1; - NVCVArrayHandle arrayHandle, arrayWrapHandle; + NVCVArrayHandle arrayHandle; + NVCVArrayHandle arrayWrapHandle; NVCVArrayData arrayData; NVCVArrayRequirements req; - NVCVAllocatorHandle alloc = reinterpret_cast(&tmp); + auto alloc = reinterpret_cast(&tmp); EXPECT_NE(alloc, nullptr); EXPECT_EQ(NVCV_SUCCESS, nvcvArrayCalcRequirements(16, NVCV_DATA_TYPE_U8, 0, &req)); diff --git a/tests/nvcv_types/system/TestColorSpec.cpp b/tests/nvcv_types/system/TestColorSpec.cpp index eff7fce84..aa396f82c 100644 --- a/tests/nvcv_types/system/TestColorSpec.cpp +++ b/tests/nvcv_types/system/TestColorSpec.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -51,12 +51,13 @@ NVCV_INSTANTIATE_TEST_SUITE_P(Predefined, ChromaSubsamplingTests, TEST_P(ChromaSubsamplingTests, predefined_has_correct_definition) { - NVCVChromaSubsampling css = std::get<0>(GetParam()); + NVCVChromaSubsampling css = ::nvcv::test::ParamValue(std::get<0>(GetParam())); - int goldSamplesHoriz = std::get<1>(GetParam()); - int goldSamplesVert = std::get<2>(GetParam()); + int goldSamplesHoriz = ::nvcv::test::ParamValue(std::get<1>(GetParam())); + int goldSamplesVert = ::nvcv::test::ParamValue(std::get<2>(GetParam())); - int samplesHoriz, samplesVert; + int samplesHoriz; + int samplesVert; ASSERT_EQ(NVCV_SUCCESS, nvcvChromaSubsamplingGetNumSamples(css, &samplesHoriz, &samplesVert)); EXPECT_EQ(goldSamplesHoriz, samplesHoriz); EXPECT_EQ(goldSamplesVert, samplesVert); @@ -64,10 +65,10 @@ TEST_P(ChromaSubsamplingTests, predefined_has_correct_definition) TEST_P(ChromaSubsamplingTests, make_chroma_subsampling_function_works) { - NVCVChromaSubsampling gold = std::get<0>(GetParam()); + NVCVChromaSubsampling gold = ::nvcv::test::ParamValue(std::get<0>(GetParam())); - int samplesHoriz = std::get<1>(GetParam()); - int samplesVert = std::get<2>(GetParam()); + int samplesHoriz = ::nvcv::test::ParamValue(std::get<1>(GetParam())); + int samplesVert = ::nvcv::test::ParamValue(std::get<2>(GetParam())); NVCVChromaSubsampling test; ASSERT_EQ(NVCV_SUCCESS, nvcvMakeChromaSubsampling(&test, samplesHoriz, samplesVert)); @@ -77,8 +78,8 @@ TEST_P(ChromaSubsamplingTests, make_chroma_subsampling_function_works) TEST_P(ChromaSubsamplingTests, get_name) { - NVCVChromaSubsampling css = std::get<0>(GetParam()); - const char *gold = std::get<3>(GetParam()); + NVCVChromaSubsampling css = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + const char *gold = ::nvcv::test::ParamValue(std::get<3>(GetParam())); EXPECT_STREQ(gold, nvcvChromaSubsamplingGetName(css)); } @@ -86,7 +87,8 @@ TEST_P(ChromaSubsamplingTests, get_name) TEST(ChromaSubsamplingTests, invalidChromaSubsamplingGetNumSamples) { #ifndef ENABLE_SANITIZER - int32_t outSamplesHoriz, outSamplesVert; + int32_t outSamplesHoriz; + int32_t outSamplesVert; EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcvChromaSubsamplingGetNumSamples(static_cast(255), &outSamplesHoriz, &outSamplesVert)); #endif @@ -190,8 +192,8 @@ NVCV_INSTANTIATE_TEST_SUITE_P(Limited, ColorSpecColorRangeTests, TEST_P(ColorSpecColorRangeTests, color_range_correct) { - const NVCVColorSpec cspec = std::get<0>(GetParam()); - const NVCVColorRange gold = std::get<1>(GetParam()); + const NVCVColorSpec cspec = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + const NVCVColorRange gold = ::nvcv::test::ParamValue(std::get<1>(GetParam())); NVCVColorRange test; ASSERT_EQ(NVCV_SUCCESS, nvcvColorSpecGetRange(cspec, &test)); @@ -236,8 +238,8 @@ NVCV_INSTANTIATE_TEST_SUITE_P(sYCC, ColorSpecColorTransferFunctionTests, TEST_P(ColorSpecColorTransferFunctionTests, color_mapping_correct) { - const NVCVColorSpec cspec = std::get<0>(GetParam()); - const NVCVColorTransferFunction gold = std::get<1>(GetParam()); + const NVCVColorSpec cspec = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + const NVCVColorTransferFunction gold = ::nvcv::test::ParamValue(std::get<1>(GetParam())); NVCVColorTransferFunction test; ASSERT_EQ(NVCV_SUCCESS, nvcvColorSpecGetColorTransferFunction(cspec, &test)); @@ -269,9 +271,9 @@ NVCV_INSTANTIATE_TEST_SUITE_P(Negative, ColorModelNeedsColorSpecTests, TEST_P(ColorModelNeedsColorSpecTests, run) { - const NVCVColorModel cmodel = std::get<0>(GetParam()); - const bool goldResult = std::get<1>(GetParam()); - const NVCVStatus goldStatus = std::get<2>(GetParam()); + const NVCVColorModel cmodel = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + const bool goldResult = ::nvcv::test::ParamValue(std::get<1>(GetParam())); + const NVCVStatus goldStatus = ::nvcv::test::ParamValue(std::get<2>(GetParam())); int8_t testResult = !goldResult; ASSERT_EQ(goldStatus, nvcvColorModelNeedsColorspec(cmodel, &testResult)); @@ -301,9 +303,9 @@ TEST(ColorSpecTests, set_color_space) { uint64_t mask = UINT64_MAX; - NVCVColorSpec type = NVCV_MAKE_COLOR_SPEC(cspace == 0 ? 1 : 0, mask, mask, mask, mask, mask); + auto type = NVCV_MAKE_COLOR_SPEC(cspace == 0 ? 1 : 0, mask, mask, mask, mask, mask); - NVCVColorSpec gold = NVCV_MAKE_COLOR_SPEC(cspace, mask, mask, mask, mask, mask); + auto gold = NVCV_MAKE_COLOR_SPEC(cspace, mask, mask, mask, mask, mask); ASSERT_EQ(NVCV_SUCCESS, nvcvColorSpecSetColorSpace(&type, (NVCVColorSpace)cspace)); EXPECT_EQ(gold, type); @@ -321,7 +323,7 @@ TEST(ColorSpecTests, get_color_space) { uint64_t mask = UINT64_MAX; - NVCVColorSpec type = NVCV_MAKE_COLOR_SPEC(cspace, mask, mask, mask, mask, mask); + auto type = NVCV_MAKE_COLOR_SPEC(cspace, mask, mask, mask, mask, mask); NVCVColorSpace test; ASSERT_EQ(NVCV_SUCCESS, nvcvColorSpecGetColorSpace(type, &test)); @@ -333,7 +335,7 @@ TEST(ColorSpecTests, invalid_get_color_space) { uint64_t mask = UINT64_MAX; - NVCVColorSpec type = NVCV_MAKE_COLOR_SPEC(mask, mask, mask, mask, mask, mask); + auto type = NVCV_MAKE_COLOR_SPEC(mask, mask, mask, mask, mask, mask); EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcvColorSpecGetColorSpace(type, nullptr)); } @@ -350,9 +352,9 @@ TEST(ColorSpecTests, set_encodings) uint64_t mask = UINT64_MAX; - NVCVColorSpec type = NVCV_MAKE_COLOR_SPEC(mask, enc == 0 ? 1 : 0, mask, mask, mask, mask); + auto type = NVCV_MAKE_COLOR_SPEC(mask, enc == 0 ? 1 : 0, mask, mask, mask, mask); - NVCVColorSpec gold = NVCV_MAKE_COLOR_SPEC(mask, enc, mask, mask, mask, mask); + auto gold = NVCV_MAKE_COLOR_SPEC(mask, enc, mask, mask, mask, mask); ASSERT_EQ(NVCV_SUCCESS, nvcvColorSpecSetYCbCrEncoding(&type, (NVCVYCbCrEncoding)enc)); ASSERT_EQ(gold, type); @@ -370,7 +372,7 @@ TEST(ColorSpecTests, get_encodings) { uint64_t mask = UINT64_MAX; - NVCVColorSpec type = NVCV_MAKE_COLOR_SPEC(mask, enc, mask, mask, mask, mask); + auto type = NVCV_MAKE_COLOR_SPEC(mask, enc, mask, mask, mask, mask); NVCVYCbCrEncoding test; ASSERT_EQ(NVCV_SUCCESS, nvcvColorSpecGetYCbCrEncoding(type, &test)); @@ -382,7 +384,7 @@ TEST(ColorSpecTests, invalid_get_encodings) { uint64_t mask = UINT64_MAX; - NVCVColorSpec type = NVCV_MAKE_COLOR_SPEC(mask, 0, mask, mask, mask, mask); + auto type = NVCV_MAKE_COLOR_SPEC(mask, 0, mask, mask, mask, mask); EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcvColorSpecGetYCbCrEncoding(type, nullptr)); } @@ -393,9 +395,9 @@ TEST(ColorSpecTests, set_xfer_func) { uint64_t mask = UINT64_MAX; - NVCVColorSpec type = NVCV_MAKE_COLOR_SPEC(mask, mask, xfer == 0 ? 1 : 0, mask, mask, mask); + auto type = NVCV_MAKE_COLOR_SPEC(mask, mask, xfer == 0 ? 1 : 0, mask, mask, mask); - NVCVColorSpec gold = NVCV_MAKE_COLOR_SPEC(mask, mask, xfer, mask, mask, mask); + auto gold = NVCV_MAKE_COLOR_SPEC(mask, mask, xfer, mask, mask, mask); ASSERT_EQ(NVCV_SUCCESS, nvcvColorSpecSetColorTransferFunction(&type, (NVCVColorTransferFunction)xfer)); ASSERT_EQ(gold, type); @@ -414,7 +416,7 @@ TEST(ColorSpecTests, get_xfer_func) { uint64_t mask = UINT64_MAX; - NVCVColorSpec type = NVCV_MAKE_COLOR_SPEC(mask, mask, xfer, mask, mask, mask); + auto type = NVCV_MAKE_COLOR_SPEC(mask, mask, xfer, mask, mask, mask); NVCVColorTransferFunction test; @@ -427,7 +429,7 @@ TEST(ColorSpecTests, invalid_get_xfer_func) { uint64_t mask = UINT64_MAX; - NVCVColorSpec type = NVCV_MAKE_COLOR_SPEC(mask, mask, 0, mask, mask, mask); + auto type = NVCV_MAKE_COLOR_SPEC(mask, mask, 0, mask, mask, mask); EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcvColorSpecGetColorTransferFunction(type, nullptr)); } @@ -438,9 +440,9 @@ TEST(ColorSpecTests, set_range) { uint64_t mask = UINT64_MAX; - NVCVColorSpec type = NVCV_MAKE_COLOR_SPEC(mask, mask, mask, range == 0 ? 1 : 0, mask, mask); + auto type = NVCV_MAKE_COLOR_SPEC(mask, mask, mask, range == 0 ? 1 : 0, mask, mask); - NVCVColorSpec gold = NVCV_MAKE_COLOR_SPEC(mask, mask, mask, range, mask, mask); + auto gold = NVCV_MAKE_COLOR_SPEC(mask, mask, mask, range, mask, mask); ASSERT_EQ(NVCV_SUCCESS, nvcvColorSpecSetRange(&type, (NVCVColorRange)range)); ASSERT_EQ(gold, type); @@ -458,7 +460,7 @@ TEST(ColorSpecTests, get_range) { uint64_t mask = UINT64_MAX; - NVCVColorSpec type = NVCV_MAKE_COLOR_SPEC(mask, mask, mask, range, mask, mask); + auto type = NVCV_MAKE_COLOR_SPEC(mask, mask, mask, range, mask, mask); NVCVColorRange test; ASSERT_EQ(NVCV_SUCCESS, nvcvColorSpecGetRange(type, &test)); @@ -470,7 +472,7 @@ TEST(ColorSpecTests, invalid_get_range) { uint64_t mask = UINT64_MAX; - NVCVColorSpec type = NVCV_MAKE_COLOR_SPEC(mask, mask, mask, mask, mask, mask); + auto type = NVCV_MAKE_COLOR_SPEC(mask, mask, mask, mask, mask, mask); EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcvColorSpecGetRange(type, nullptr)); } @@ -481,9 +483,9 @@ TEST(ColorSpecTests, set_chroma_loc_horiz) { uint64_t mask = UINT64_MAX; - NVCVColorSpec type = NVCV_MAKE_COLOR_SPEC(mask, mask, mask, mask, loc == 0 ? 1 : 0, mask); + auto type = NVCV_MAKE_COLOR_SPEC(mask, mask, mask, mask, loc == 0 ? 1 : 0, mask); - NVCVColorSpec gold = NVCV_MAKE_COLOR_SPEC(mask, mask, mask, mask, loc, mask); + auto gold = NVCV_MAKE_COLOR_SPEC(mask, mask, mask, mask, loc, mask); ASSERT_EQ(NVCV_SUCCESS, nvcvColorSpecSetChromaLoc(&type, (NVCVChromaLocation)loc, (NVCVChromaLocation)mask)); ASSERT_EQ(gold, type); @@ -504,7 +506,7 @@ TEST(ColorSpecTests, get_chroma_loc_horiz) { uint64_t mask = UINT64_MAX; - NVCVColorSpec type = NVCV_MAKE_COLOR_SPEC(mask, mask, mask, mask, loc, mask); + auto type = NVCV_MAKE_COLOR_SPEC(mask, mask, mask, mask, loc, mask); NVCVChromaLocation test; @@ -519,9 +521,9 @@ TEST(ColorSpecTests, set_chroma_loc_vert) { uint64_t mask = UINT64_MAX; - NVCVColorSpec type = NVCV_MAKE_COLOR_SPEC(mask, mask, mask, mask, mask, loc == 0 ? 1 : 0); + auto type = NVCV_MAKE_COLOR_SPEC(mask, mask, mask, mask, mask, loc == 0 ? 1 : 0); - NVCVColorSpec gold = NVCV_MAKE_COLOR_SPEC(mask, mask, mask, mask, mask, loc); + auto gold = NVCV_MAKE_COLOR_SPEC(mask, mask, mask, mask, mask, loc); ASSERT_EQ(NVCV_SUCCESS, nvcvColorSpecSetChromaLoc(&type, (NVCVChromaLocation)mask, (NVCVChromaLocation)loc)); ASSERT_EQ(gold, type); @@ -534,7 +536,7 @@ TEST(ColorSpecTests, get_chroma_loc_vert) { uint64_t mask = UINT64_MAX; - NVCVColorSpec type = NVCV_MAKE_COLOR_SPEC(mask, mask, mask, mask, mask, loc); + auto type = NVCV_MAKE_COLOR_SPEC(mask, mask, mask, mask, mask, loc); NVCVChromaLocation test; ASSERT_EQ(NVCV_SUCCESS, nvcvColorSpecGetChromaLoc(type, nullptr, &test)); @@ -548,12 +550,13 @@ TEST(ColorSpecTests, get_chroma_loc_both) { uint64_t mask = UINT64_MAX; - NVCVChromaLocation goldHoriz = static_cast(loc & 0b11); - NVCVChromaLocation goldVert = static_cast((~loc) & 0b11); + auto goldHoriz = static_cast(loc & 0b11); + auto goldVert = static_cast((~loc) & 0b11); - NVCVColorSpec type = NVCV_MAKE_COLOR_SPEC(mask, mask, mask, mask, goldHoriz, goldVert); + auto type = NVCV_MAKE_COLOR_SPEC(mask, mask, mask, mask, goldHoriz, goldVert); - NVCVChromaLocation testHoriz, testVert; + NVCVChromaLocation testHoriz; + NVCVChromaLocation testVert; ASSERT_EQ(NVCV_SUCCESS, nvcvColorSpecGetChromaLoc(type, &testHoriz, &testVert)); EXPECT_EQ(goldHoriz, testHoriz); EXPECT_EQ(goldVert, testVert); diff --git a/tests/nvcv_types/system/TestConfig.cpp b/tests/nvcv_types/system/TestConfig.cpp index 7527cc8f8..2a57bf853 100644 --- a/tests/nvcv_types/system/TestConfig.cpp +++ b/tests/nvcv_types/system/TestConfig.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -92,7 +92,7 @@ template class ConfigTests : public ::testing::Test { public: - ~ConfigTests() + ~ConfigTests() override { // Make sure we set the handle manager back to dynamic allocation. EXPECT_NO_THROW(SetMaxCount(-1)); @@ -107,7 +107,7 @@ TYPED_TEST(ConfigTests, set_max_obj_count_works) ASSERT_NO_THROW(SetMaxCount(5)); - for (int i = 0; i < 5; ++i) + for (int i = 0; i < 5; ++i) // NOSONAR { ASSERT_NO_THROW(objs.emplace_back(CreateObj())); } diff --git a/tests/nvcv_types/system/TestDataLayout.cpp b/tests/nvcv_types/system/TestDataLayout.cpp index e70f9535d..da52e0118 100644 --- a/tests/nvcv_types/system/TestDataLayout.cpp +++ b/tests/nvcv_types/system/TestDataLayout.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,6 +20,7 @@ #include #include +#include #include namespace t = ::testing; @@ -46,15 +47,6 @@ INSTANTIATE_TEST_SUITE_P( MAKE_SWIZZLE(X, 0, 0, Y), MAKE_SWIZZLE(Y, 0, 0, X), MAKE_SWIZZLE(X, 0, 0, 1), MAKE_SWIZZLE(X, Y, 0, 1), MAKE_SWIZZLE(X, Y, 0, 0), MAKE_SWIZZLE(0, X, Z, 0), MAKE_SWIZZLE(0, Z, X, 0), MAKE_SWIZZLE(0, Y, X, 1))); -// TEST_P(SwizzleTests, predefined_has_correct_definition) -// { -// NVCVSwizzle gold = std::get<0>(GetParam()); -// NVCVSwizzle test = NVCV_MAKE_SWIZZLE(std::get<2>(GetParam()), std::get<3>(GetParam()), std::get<4>(GetParam()), -// std::get<5>(GetParam())); - -// EXPECT_EQ(gold, test); -// } - TEST_P(SwizzleTests, make_sizzle_function_works) { NVCVSwizzle gold = std::get<0>(GetParam()); @@ -65,27 +57,12 @@ TEST_P(SwizzleTests, make_sizzle_function_works) EXPECT_EQ(gold, test); } -// TEST_P(SwizzleTests, make_sizzle_macro_works) -// { -// NVCVSwizzle gold = std::get<0>(GetParam()); -// NVCVSwizzle test = std::get<1>(GetParam()); - -// EXPECT_EQ(gold, test); -// } - -// TEST(SwizzleTests, make_swizzle_macro) -// { -// NVCVSwizzle swzl; -// EXPECT_EQ(NVCV_SUCCESS, nvcvMakeSwizzle(&swzl, NVCV_CHANNEL_X, NVCV_CHANNEL_W, NVCV_CHANNEL_Z, NVCV_CHANNEL_1)); -// EXPECT_EQ(swzl, NVCV_MAKE_SWIZZLE(NVCV_CHANNEL_X, NVCV_CHANNEL_W, NVCV_CHANNEL_Z, NVCV_CHANNEL_1)); -// } - TEST_P(SwizzleTests, get_channel_channels) { NVCVSwizzle swizzle = std::get<0>(GetParam()); - NVCVChannel channels[4]; - nvcvSwizzleGetChannels(swizzle, channels); + std::array channels; + nvcvSwizzleGetChannels(swizzle, channels.data()); EXPECT_EQ(std::get<1>(GetParam()), channels[0]); EXPECT_EQ(std::get<2>(GetParam()), channels[1]); @@ -95,11 +72,11 @@ TEST_P(SwizzleTests, get_channel_channels) TEST_P(SwizzleTests, get_channel_count) { - NVCVSwizzle swizzle = std::get<0>(GetParam()); - NVCVChannel channels[] + NVCVSwizzle swizzle = std::get<0>(GetParam()); + std::array channels = {std::get<1>(GetParam()), std::get<2>(GetParam()), std::get<3>(GetParam()), std::get<4>(GetParam())}; - int hist[4] = {}; + std::array hist = {}; int gold = 0; for (int i = 0; i < 4; ++i) @@ -133,7 +110,7 @@ struct PackingTestParams { NVCVPacking packing; - int bitsPerComponent[4]; + std::array bitsPerComponent; NVCVPackingParams params; @@ -171,16 +148,17 @@ struct PackingTestParams } }; -// from boost - -inline void hash_combine(std::size_t &seed) {} - -template -inline void hash_combine(std::size_t &seed, const T &v, Rest... rest) +template +inline void hash_combine_one(std::size_t &seed, const T &v) { std::hash hasher; seed ^= hasher(v) + 0x9e3779b9 + (seed << 6) + (seed >> 2); - hash_combine(seed, rest...); +} + +template +inline void hash_combine(std::size_t &seed, const Args &...args) +{ + (hash_combine_one(seed, args), ...); } struct HashPackingTestParams @@ -478,8 +456,8 @@ TEST_P(PackingTests, get_bits_per_component) { PackingTestParams p = GetParam(); - int bits[4]; - nvcvPackingGetBitsPerComponent(p.packing, bits); + std::array bits; + nvcvPackingGetBitsPerComponent(p.packing, bits.data()); EXPECT_EQ(p.bitsPerComponent[0], bits[0]) << p.packing; EXPECT_EQ(p.bitsPerComponent[1], bits[1]) << p.packing; @@ -520,9 +498,9 @@ TEST_P(PackingTests, check_bits_per_pixel) break; default: - for (int i = 0; i < 4; ++i) + for (int bits : p.params.bits) { - gold += p.params.bits[i]; + gold += bits; } break; } @@ -541,8 +519,80 @@ TEST(PackingTests, valid_values) PackingTestParams p; p.params.alignment = 0; // don't care - NVCVChannel swc[4]; - for (int bitsX = 0; bitsX <= 256; bitsX < 32 ? ++bitsX : (bitsX < 128 ? (bitsX += 8) : (bitsX += 32))) + std::array swc; + auto testPackingForBitsW = [&](int bitsX, int bitsW) + { + p.params.bits[3] = bitsW; + swc[3] = p.params.bits[3] != 0 ? NVCV_CHANNEL_W : NVCV_CHANNEL_0; + + NVCVStatus status = nvcvMakeSwizzle(&p.params.swizzle, swc[0], swc[1], swc[2], swc[3]); + if (p.params.swizzle != NVCV_SWIZZLE_UNSUPPORTED) + EXPECT_EQ(status, NVCV_SUCCESS); + else + return; + + p.params.byteOrder = NVCV_ORDER_MSB; + + auto it = packingList.find(p); + if (it != packingList.end()) + { + NVCVPacking packing; + ASSERT_EQ(NVCV_SUCCESS, nvcvMakePacking(&packing, &p.params)); + + EXPECT_EQ(it->packing, packing) << p; + packingList.erase(it); + } + // to save some time, let's do negative tests in only a subset of the parameter space + else if (bitsX == 8) + { + NVCVPacking packing = NVCV_PACKING_X8_Y8__X8_Z8; + ASSERT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcvMakePacking(&packing, &p.params)) << p; + EXPECT_EQ(packing, NVCV_PACKING_X8_Y8__X8_Z8) << "should not have modified output"; + } + + p.params.byteOrder = NVCV_ORDER_LSB; + + it = packingList.find(p); + if (it != packingList.end()) + { + NVCVPacking packing; + ASSERT_EQ(NVCV_SUCCESS, nvcvMakePacking(&packing, &p.params)); + + EXPECT_EQ(it->packing, packing) << p; + packingList.erase(it); + } + // to save some time, let's do negative tests in only a subset of the parameter space + else if (bitsX == 8) + { + NVCVPacking packing = NVCV_PACKING_X8_Y8__X8_Z8; + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcvMakePacking(&packing, &p.params)) << p; + EXPECT_EQ(packing, NVCV_PACKING_X8_Y8__X8_Z8) << "should not have modified output"; + } + }; + auto testPackingForBitsZ = [&](int bitsX) + { + for (int bitsW = 0; bitsW <= 128; bitsW < 32 ? ++bitsW : (bitsW += 8)) + { + ASSERT_NO_FATAL_FAILURE(testPackingForBitsW(bitsX, bitsW)); + } + }; + auto advanceBitsX = [](int &bitsX) + { + if (bitsX < 32) + { + ++bitsX; + } + else if (bitsX < 128) + { + bitsX += 8; + } + else + { + bitsX += 32; + } + }; + + for (int bitsX = 0; bitsX <= 256; advanceBitsX(bitsX)) { p.params.bits[0] = bitsX; swc[0] = p.params.bits[0] != 0 ? NVCV_CHANNEL_X : NVCV_CHANNEL_0; @@ -554,55 +604,7 @@ TEST(PackingTests, valid_values) { p.params.bits[2] = bitsZ; swc[2] = p.params.bits[2] != 0 ? NVCV_CHANNEL_Z : NVCV_CHANNEL_0; - for (int bitsW = 0; bitsW <= 128; bitsW < 32 ? ++bitsW : (bitsW += 8)) - { - p.params.bits[3] = bitsW; - swc[3] = p.params.bits[3] != 0 ? NVCV_CHANNEL_W : NVCV_CHANNEL_0; - - NVCVStatus status = nvcvMakeSwizzle(&p.params.swizzle, swc[0], swc[1], swc[2], swc[3]); - if (p.params.swizzle != NVCV_SWIZZLE_UNSUPPORTED) - EXPECT_EQ(status, NVCV_SUCCESS); - else - continue; - - p.params.byteOrder = NVCV_ORDER_MSB; - - auto it = packingList.find(p); - if (it != packingList.end()) - { - NVCVPacking packing; - ASSERT_EQ(NVCV_SUCCESS, nvcvMakePacking(&packing, &p.params)); - - EXPECT_EQ(it->packing, packing) << p; - packingList.erase(it); - } - // to save some time, let's do negative tests in only a subset of the parameter space - else if (bitsX == 8) - { - NVCVPacking packing = NVCV_PACKING_X8_Y8__X8_Z8; - ASSERT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcvMakePacking(&packing, &p.params)) << p; - EXPECT_EQ(packing, NVCV_PACKING_X8_Y8__X8_Z8) << "should not have modified output"; - } - - p.params.byteOrder = NVCV_ORDER_LSB; - - it = packingList.find(p); - if (it != packingList.end()) - { - NVCVPacking packing; - ASSERT_EQ(NVCV_SUCCESS, nvcvMakePacking(&packing, &p.params)); - - EXPECT_EQ(it->packing, packing) << p; - packingList.erase(it); - } - // to save some time, let's do negative tests in only a subset of the parameter space - else if (bitsX == 8) - { - NVCVPacking packing = NVCV_PACKING_X8_Y8__X8_Z8; - EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcvMakePacking(&packing, &p.params)) << p; - EXPECT_EQ(packing, NVCV_PACKING_X8_Y8__X8_Z8) << "should not have modified output"; - } - } + ASSERT_NO_FATAL_FAILURE(testPackingForBitsZ(bitsX)); ++counter; } @@ -664,9 +666,9 @@ TEST(PackingTests, valid_values) if (!packingList.empty()) { std::cerr << "Non-matched packings: " << std::endl; - for (auto &p : packingList) + for (const auto &packing : packingList) { - std::cerr << " " << p.packing << std::endl; + std::cerr << " " << packing.packing << std::endl; } } } @@ -693,11 +695,8 @@ INSTANTIATE_TEST_SUITE_P( TEST_P(PackingTests_Alignment, get_alignment) { - auto param = GetParam(); - - NVCVPacking packing = std::get<0>(param); - const int32_t expectedAlignment = std::get<1>(param); - int32_t outAlignment = -1; + const auto [packing, expectedAlignment] = GetParam(); + int32_t outAlignment = -1; ASSERT_EQ(NVCV_SUCCESS, nvcvPackingGetAlignment(packing, &outAlignment)); // 16 / 8 = 2 EXPECT_EQ(expectedAlignment, outAlignment); @@ -727,8 +726,6 @@ TEST(ByteOrderTests, get_name) TEST(SwizzleTests, get_name) { EXPECT_STREQ("XYZ1", nvcvSwizzleGetName(NVCV_SWIZZLE_XYZ1)); - // EXPECT_STREQ("110X", - // nvcvSwizzleGetName(NVCV_DETAIL_MAKE_SWIZZLE(NVCV_CHANNEL_1, NVCV_CHANNEL_1, NVCV_CHANNEL_0, NVCV_CHANNEL_X))); } TEST(SwizzleTests_Negative, Invalid_parameter) diff --git a/tests/nvcv_types/system/TestDataType.cpp b/tests/nvcv_types/system/TestDataType.cpp index aa7f2f8d6..758d49164 100644 --- a/tests/nvcv_types/system/TestDataType.cpp +++ b/tests/nvcv_types/system/TestDataType.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,6 +22,8 @@ #include #include +#include + namespace t = ::testing; namespace test = nvcv::test; @@ -62,6 +64,10 @@ INSTANTIATE_TEST_SUITE_P(ExplicitTypes, DataTypeTests, Params{NVCV_DATA_TYPE_2U8, FMT_DATA_PARAMS(UNSIGNED, X8_Y8), 2, 16}, Params{NVCV_DATA_TYPE_3U8, FMT_DATA_PARAMS(UNSIGNED, X8_Y8_Z8), 3, 24}, Params{NVCV_DATA_TYPE_4U8, FMT_DATA_PARAMS(UNSIGNED, X8_Y8_Z8_W8), 4, 32}, + Params{NVCV_DATA_TYPE_F16, FMT_DATA_PARAMS(FLOAT, X16), 1, 16}, + Params{NVCV_DATA_TYPE_2F16, FMT_DATA_PARAMS(FLOAT, X16_Y16), 2, 32}, + Params{NVCV_DATA_TYPE_3F16, FMT_DATA_PARAMS(FLOAT, X16_Y16_Z16), 3, 48}, + Params{NVCV_DATA_TYPE_4F16, FMT_DATA_PARAMS(FLOAT, X16_Y16_Z16_W16), 4, 64}, Params{NVCV_DATA_TYPE_F32, FMT_DATA_PARAMS(FLOAT, X32), 1, 32}, Params{NVCV_DATA_TYPE_F64, FMT_DATA_PARAMS(FLOAT, X64), 1, 64}, Params{NVCV_DATA_TYPE_C64, FMT_DATA_PARAMS(COMPLEX, X64), 1, 64}, @@ -95,14 +101,14 @@ TEST_P(DataTypeTests, get_bpc_works) { const Params &p = GetParam(); - int32_t bits[4]; - ASSERT_EQ(NVCV_SUCCESS, nvcvDataTypeGetBitsPerChannel(p.dtype, bits)); + std::array bits; + ASSERT_EQ(NVCV_SUCCESS, nvcvDataTypeGetBitsPerChannel(p.dtype, bits.data())); NVCVPacking packing; ASSERT_EQ(NVCV_SUCCESS, nvcvDataTypeGetPacking(p.dtype, &packing)); - int32_t goldbits[4]; - ASSERT_EQ(NVCV_SUCCESS, nvcvPackingGetBitsPerComponent(packing, goldbits)); + std::array goldbits; + ASSERT_EQ(NVCV_SUCCESS, nvcvPackingGetBitsPerComponent(packing, goldbits.data())); EXPECT_EQ(bits[0], goldbits[0]); EXPECT_EQ(bits[1], goldbits[1]); @@ -156,8 +162,8 @@ INSTANTIATE_TEST_SUITE_P(ExplicitTypes, ImageDataTypeTests, TEST_P(ImageDataTypeTests, pixel_type_matches_corresponding_image_type) { - NVCVImageFormat imgFormat = std::get<0>(GetParam()); - NVCVDataType dtype = std::get<1>(GetParam()); + auto imgFormat = static_cast(::nvcv::test::ParamValue(std::get<0>(GetParam()))); + auto dtype = static_cast(::nvcv::test::ParamValue(std::get<1>(GetParam()))); EXPECT_EQ((uint64_t)imgFormat, (uint64_t)dtype); } @@ -281,7 +287,6 @@ NVCV_INSTANTIATE_TEST_SUITE_P( test::ValueList { {NVCV_DATA_TYPE_U8, -1}, - // WAR {MAKE_DATA_TYPE_ABBREV(UNSIGNED, b2X14), 0}, {MAKE_DATA_TYPE_ABBREV(UNSIGNED, X5Y5b1Z5), 0}, {MAKE_DATA_TYPE_ABBREV(UNSIGNED, X3Y3Z2), 1} } @@ -291,10 +296,10 @@ NVCV_INSTANTIATE_TEST_SUITE_P( TEST_P(ChannelDataTypeTests, get_channel_type) { - NVCVDataType test = std::get<0>(GetParam()); - int channel = std::get<1>(GetParam()); - NVCVDataType gold = std::get<2>(GetParam()); - NVCVStatus goldStatus = std::get<3>(GetParam()); + NVCVDataType test = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + int channel = ::nvcv::test::ParamValue(std::get<1>(GetParam())); + NVCVDataType gold = ::nvcv::test::ParamValue(std::get<2>(GetParam())); + NVCVStatus goldStatus = ::nvcv::test::ParamValue(std::get<3>(GetParam())); NVCVDataType pix; ASSERT_EQ(goldStatus, nvcvDataTypeGetChannelType(test, channel, &pix)); @@ -330,8 +335,8 @@ NVCV_INSTANTIATE_TEST_SUITE_P(_,DataTypeStrideTests, TEST_P(DataTypeStrideTests, works) { - const NVCVDataType dtype = std::get<0>(GetParam()); - const int goldStride = std::get<1>(GetParam()); + const NVCVDataType dtype = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + const int goldStride = ::nvcv::test::ParamValue(std::get<1>(GetParam())); int32_t testStride; ASSERT_EQ(NVCV_SUCCESS, nvcvDataTypeGetStrideBytes(dtype, &testStride)); @@ -367,8 +372,8 @@ NVCV_TEST_SUITE_P(DataTypeAlignmentTests, TEST_P(DataTypeAlignmentTests, works) { - const NVCVDataType dtype = std::get<0>(GetParam()); - const int goldAlign = std::get<1>(GetParam()); + const NVCVDataType dtype = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + const int goldAlign = ::nvcv::test::ParamValue(std::get<1>(GetParam())); int32_t testAlign; ASSERT_EQ(NVCV_SUCCESS, nvcvDataTypeGetAlignment(dtype, &testAlign)); diff --git a/tests/nvcv_types/system/TestExceptions.cpp b/tests/nvcv_types/system/TestExceptions.cpp index ee1274ef4..08cbd77ec 100644 --- a/tests/nvcv_types/system/TestExceptions.cpp +++ b/tests/nvcv_types/system/TestExceptions.cpp @@ -14,7 +14,9 @@ #include -// TODO: once we have functions that generate errors, we should +#include + +// REVISIT: once we have functions that generate errors, we should // extend these tests to cover more scenarios namespace t = ::testing; @@ -25,13 +27,16 @@ TEST(ExceptionTest, exception_updates_internal_status) { throw nvcv::Exception(nvcv::Status::ERROR_DEVICE, "test error"); } - catch (...) + catch (const nvcv::Exception &e) { + // The thread-local status update is the behavior under test. + EXPECT_EQ(nvcv::Status::ERROR_DEVICE, e.code()); + EXPECT_STREQ("test error", e.msg()); } - char msg[NVCV_MAX_STATUS_MESSAGE_LENGTH]; - ASSERT_EQ(NVCV_ERROR_DEVICE, nvcvGetLastErrorMessage(msg, sizeof(msg))); - EXPECT_STREQ("test error", msg); + std::array msg; + ASSERT_EQ(NVCV_ERROR_DEVICE, nvcvGetLastErrorMessage(msg.data(), msg.size())); + EXPECT_STREQ("test error", msg.data()); } TEST(ExceptionTest, protect_call_nvcv_exception) @@ -47,9 +52,9 @@ TEST(ExceptionTest, protect_call_invalid_argument) EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, status); - char msg[NVCV_MAX_STATUS_MESSAGE_LENGTH]; - ASSERT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcvGetLastErrorMessage(msg, sizeof(msg))); - EXPECT_STREQ("test invalid argument", msg); + std::array msg; + ASSERT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcvGetLastErrorMessage(msg.data(), msg.size())); + EXPECT_STREQ("test invalid argument", msg.data()); } TEST(ExceptionTest, protect_call_bad_alloc) @@ -58,9 +63,19 @@ TEST(ExceptionTest, protect_call_bad_alloc) EXPECT_EQ(NVCV_ERROR_OUT_OF_MEMORY, status); - char msg[NVCV_MAX_STATUS_MESSAGE_LENGTH]; - ASSERT_EQ(NVCV_ERROR_OUT_OF_MEMORY, nvcvGetLastErrorMessage(msg, sizeof(msg))); - EXPECT_STREQ("Not enough space for resource allocation", msg); + std::array msg; + ASSERT_EQ(NVCV_ERROR_OUT_OF_MEMORY, nvcvGetLastErrorMessage(msg.data(), msg.size())); + EXPECT_STREQ("Not enough space for resource allocation", msg.data()); +} + +TEST(ExceptionTest, protect_call_standard_logic_and_runtime_errors) +{ + EXPECT_EQ(NVCV_ERROR_INTERNAL, nvcv::ProtectCall([] { throw std::domain_error("domain"); })); + EXPECT_EQ(NVCV_ERROR_INTERNAL, nvcv::ProtectCall([] { throw std::length_error("length"); })); + EXPECT_EQ(NVCV_ERROR_INTERNAL, nvcv::ProtectCall([] { throw std::out_of_range("out of range"); })); + EXPECT_EQ(NVCV_ERROR_INTERNAL, nvcv::ProtectCall([] { throw std::range_error("range"); })); + EXPECT_EQ(NVCV_ERROR_INTERNAL, nvcv::ProtectCall([] { throw std::overflow_error("overflow"); })); + EXPECT_EQ(NVCV_ERROR_INTERNAL, nvcv::ProtectCall([] { throw std::underflow_error("underflow"); })); } TEST(ExceptionTest, protect_call_unexpected) @@ -69,9 +84,9 @@ TEST(ExceptionTest, protect_call_unexpected) EXPECT_EQ(NVCV_ERROR_INTERNAL, status); - char msg[NVCV_MAX_STATUS_MESSAGE_LENGTH]; - ASSERT_EQ(NVCV_ERROR_INTERNAL, nvcvGetLastErrorMessage(msg, sizeof(msg))); - EXPECT_STREQ("Unexpected error", msg); + std::array msg; + ASSERT_EQ(NVCV_ERROR_INTERNAL, nvcvGetLastErrorMessage(msg.data(), msg.size())); + EXPECT_STREQ("Unexpected error", msg.data()); } TEST(ExceptionTest, exception_format_multiple_args) @@ -81,7 +96,15 @@ TEST(ExceptionTest, exception_format_multiple_args) EXPECT_STREQ("NVCV_ERROR_DEVICE: test error 123 rod l", e.what()); EXPECT_STREQ("test error 123 rod l", e.msg()); - char msg[NVCV_MAX_STATUS_MESSAGE_LENGTH]; - ASSERT_EQ(NVCV_ERROR_DEVICE, nvcvGetLastErrorMessage(msg, sizeof(msg))); - EXPECT_STREQ("test error 123 rod l", msg); + std::array msg; + ASSERT_EQ(NVCV_ERROR_DEVICE, nvcvGetLastErrorMessage(msg.data(), msg.size())); + EXPECT_STREQ("test error 123 rod l", msg.data()); +} + +TEST(ExceptionTest, exception_format_numeric_modifiers) +{ + nvcv::Exception e(nvcv::Status::ERROR_DEVICE, "test error %ld %.2f %x %%", 123L, 4.5, 255U); + + EXPECT_STREQ("NVCV_ERROR_DEVICE: test error 123 4.50 ff %", e.what()); + EXPECT_STREQ("test error 123 4.50 ff %", e.msg()); } diff --git a/tests/nvcv_types/system/TestImage.cpp b/tests/nvcv_types/system/TestImage.cpp index 396155195..548ce4e93 100644 --- a/tests/nvcv_types/system/TestImage.cpp +++ b/tests/nvcv_types/system/TestImage.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,8 +20,17 @@ #include #include +#include +#include + #include +class ImageTestError : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + TEST(Image, smoke_create) { nvcv::Image img({163, 117}, nvcv::FMT_RGBA8); @@ -77,15 +86,17 @@ TEST(Image, smoke_user_pointer) nvcv::Image img({163, 117}, nvcv::FMT_RGBA8); EXPECT_EQ(nullptr, img.userPointer()); - void *userPtr; + NVCVUserPointer userPtr; ASSERT_EQ(NVCV_SUCCESS, nvcvImageGetUserPointer(img.handle(), &userPtr)); EXPECT_EQ(nullptr, userPtr); - img.setUserPointer((void *)0x123); - EXPECT_EQ((void *)0x123, img.userPointer()); + int userValue = 0; + auto userValuePtr = static_cast(static_cast(&userValue)); + img.setUserPointer(userValuePtr); + EXPECT_EQ(userValuePtr, img.userPointer()); ASSERT_EQ(NVCV_SUCCESS, nvcvImageGetUserPointer(img.handle(), &userPtr)); - EXPECT_EQ((void *)0x123, userPtr); + EXPECT_EQ(static_cast(static_cast(&userValue)), userPtr); img.setUserPointer(nullptr); EXPECT_EQ(nullptr, img.userPointer()); @@ -96,8 +107,6 @@ TEST(Image, smoke_user_pointer) TEST(Image, smoke_create_managed) { - ; - int64_t setBufLen = 0; int32_t setBufAlign = 0; @@ -111,11 +120,11 @@ TEST(Image, smoke_create_managed) setBufLen = size; setBufAlign = bufAlign; - void *ptr = nullptr; + NVCVMemoryBuffer ptr = nullptr; cudaMallocManaged(&ptr, size); return ptr; }, - [](void *ptr, int64_t bufLen, int32_t bufAlign) + [](NVCVMemoryBuffer ptr, int64_t, int32_t) { cudaFree(ptr); } @@ -127,7 +136,7 @@ TEST(Image, smoke_create_managed) nvcv::MemAlignment{}.rowAddr(1).baseAddr(32)); // packed rows EXPECT_EQ(32, setBufAlign); - nvcv::Optional data = img.exportData(); + nvcv::Optional data{img.exportData()}; ASSERT_NE(nvcv::NullOpt, data); auto devdata = data->cast(); @@ -158,7 +167,8 @@ TEST(ImageWrapData, smoke_create) buf.planes[0].width = 173; buf.planes[0].height = 79; buf.planes[0].rowStride = 190; - buf.planes[0].basePtr = reinterpret_cast(678); + std::array buffer{}; + buf.planes[0].basePtr = buffer.data(); auto img = nvcv::ImageWrapData(nvcv::ImageDataStridedCuda{nvcv::FMT_U8, buf}); @@ -170,7 +180,7 @@ TEST(ImageWrapData, smoke_create) ASSERT_EQ(NVCV_SUCCESS, nvcvImageGetType(img.handle(), &type)); EXPECT_EQ(NVCV_TYPE_IMAGE_WRAPDATA, type); - nvcv::Optional data = img.exportData(); + nvcv::Optional data{img.exportData()}; ASSERT_NE(nvcv::NullOpt, data); auto devdata = data->cast(); @@ -198,8 +208,10 @@ TEST(ImageWrapData, smoke_user_pointer) EXPECT_EQ(nullptr, img.userPointer()); - img.setUserPointer((void *)0x123); - EXPECT_EQ((void *)0x123, img.userPointer()); + int userValue = 0; + auto userPtr = static_cast(static_cast(&userValue)); + img.setUserPointer(userPtr); + EXPECT_EQ(userPtr, img.userPointer()); img.setUserPointer(nullptr); EXPECT_EQ(nullptr, img.userPointer()); @@ -207,8 +219,6 @@ TEST(ImageWrapData, smoke_user_pointer) TEST(Image, smoke_operator) { - ; - nvcv::Image in{ {512, 256}, nvcv::FMT_RGBA8 @@ -223,15 +233,15 @@ TEST(Image, smoke_operator) if (!inData || !outData) { - throw std::runtime_error("Input and output images must have cuda-accessible pitch-linear memory"); + throw ImageTestError("Input and output images must have cuda-accessible pitch-linear memory"); } if (inData->format() != outData->format()) { - throw std::runtime_error("Input and output images must have same format"); + throw ImageTestError("Input and output images must have same format"); } if (inData->size() != outData->size()) { - throw std::runtime_error("Input and output images must have same size"); + throw ImageTestError("Input and output images must have same size"); } assert(inData->numPlanes() == outData->numPlanes()); @@ -252,7 +262,7 @@ TEST(Image, valid_get_allocator) int tmp = 1; NVCVImageHandle handle; NVCVImageRequirements reqs; - NVCVAllocatorHandle alloc = reinterpret_cast(&tmp); + auto alloc = reinterpret_cast(&tmp); EXPECT_NE(alloc, nullptr); EXPECT_EQ(NVCV_SUCCESS, nvcvImageCalcRequirements(224, 224, NVCV_IMAGE_FORMAT_RGBA8, 0, 0, &reqs)); @@ -309,7 +319,8 @@ TEST(Image, get_null_parameter) NVCVImageRequirements reqs; NVCVImageFormat fmt; NVCVTypeImage imageType; - int32_t width, height; + int32_t width; + int32_t height; EXPECT_EQ(NVCV_SUCCESS, nvcvImageCalcRequirements(224, 224, NVCV_IMAGE_FORMAT_U8, 0, 0, &reqs)); EXPECT_EQ(NVCV_SUCCESS, nvcvImageConstruct(&reqs, nullptr, &handle)); @@ -330,6 +341,19 @@ TEST(Image, get_null_parameter) EXPECT_EQ(NVCV_SUCCESS, nvcvImageDecRef(handle, nullptr)); } +TEST(Image, invalid_refcount_null_out_parameter) +{ + NVCVImageHandle handle; + NVCVImageRequirements reqs; + + ASSERT_EQ(NVCV_SUCCESS, nvcvImageCalcRequirements(224, 224, NVCV_IMAGE_FORMAT_U8, 0, 0, &reqs)); + ASSERT_EQ(NVCV_SUCCESS, nvcvImageConstruct(&reqs, nullptr, &handle)); + + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcvImageRefCount(handle, nullptr)); + + EXPECT_EQ(NVCV_SUCCESS, nvcvImageDecRef(handle, nullptr)); +} + TEST(ImageWrapData, smoke_cleanup) { nvcv::ImageDataStridedCuda::Buffer buf; @@ -340,13 +364,14 @@ TEST(ImageWrapData, smoke_cleanup) buf.planes[0].basePtr = reinterpret_cast(678); int cleanupCalled = 0; - auto cleanup = [&cleanupCalled](const nvcv::ImageData &data) + auto cleanup = [&cleanupCalled](const nvcv::ImageData &) { ++cleanupCalled; }; { - auto img = nvcv::ImageWrapData(nvcv::ImageDataStridedCuda{nvcv::FMT_U8, buf}, cleanup); + auto img = nvcv::ImageWrapData(nvcv::ImageDataStridedCuda{nvcv::FMT_U8, buf}, + nvcv::ImageDataCleanupCallback{cleanup}); EXPECT_EQ(0, cleanupCalled); } EXPECT_EQ(1, cleanupCalled) << "Cleanup must have been called when img got destroyed"; @@ -386,10 +411,11 @@ TEST(ImageWrapData, smoke_mem_reqs) TEST(ImageWrapData, valid_get_allocator) { int tmp = 1; - NVCVImageHandle handle, warpHandle; + NVCVImageHandle handle; + NVCVImageHandle warpHandle; NVCVImageData imageData; NVCVImageRequirements reqs; - NVCVAllocatorHandle alloc = reinterpret_cast(&tmp); + auto alloc = reinterpret_cast(&tmp); EXPECT_NE(alloc, nullptr); EXPECT_EQ(NVCV_SUCCESS, nvcvImageCalcRequirements(224, 224, NVCV_IMAGE_FORMAT_RGBA8, 0, 0, &reqs)); @@ -436,6 +462,85 @@ TEST(ImageWrapData, construct_invalid_buffer_numPlanes) callBackFunc.targetFunc(), callBackFunc.targetHandle(), &handle)); } +TEST(ImageWrapData, construct_invalid_buffer_numPlanes_too_large) +{ + // Reserve one extra plane after NVCVImageData so this testcase stays deterministic + // even before the implementation validates numPlanes upper bound. + struct ImageDataWithExtraPlane + { + NVCVImageData data{}; + NVCVImagePlaneStrided extraPlane{}; + }; + + ImageDataWithExtraPlane payload; + payload.data.format = static_cast(nvcv::FMT_U8); + payload.data.bufferType = NVCV_IMAGE_BUFFER_STRIDED_CUDA; + payload.data.buffer.strided.numPlanes = NVCV_MAX_PLANE_COUNT + 1; + payload.extraPlane.width = 173; + payload.extraPlane.height = 79; + payload.extraPlane.rowStride = 190; + payload.extraPlane.basePtr = reinterpret_cast(678); + + for (int p = 0; p < NVCV_MAX_PLANE_COUNT; ++p) + { + payload.data.buffer.strided.planes[p].width = 173; + payload.data.buffer.strided.planes[p].height = 79; + payload.data.buffer.strided.planes[p].rowStride = 190; + payload.data.buffer.strided.planes[p].basePtr = reinterpret_cast(678 + p); + } + + NVCVImageHandle handle = nullptr; + nvcv::ImageDataCleanupCallback callBackFunc{}; + + NVCVStatus st + = nvcvImageWrapDataConstruct(&payload.data, callBackFunc.targetFunc(), callBackFunc.targetHandle(), &handle); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, st); + if (st == NVCV_SUCCESS && handle != nullptr) + { + EXPECT_EQ(NVCV_SUCCESS, nvcvImageDecRef(handle, nullptr)); + } +} + +TEST(ImageWrapData, construct_invalid_buffer_rowStride_negative) +{ + nvcv::ImageDataStridedCuda::Buffer buf; + buf.numPlanes = 1; + buf.planes[0].width = 173; + buf.planes[0].height = 79; + buf.planes[0].rowStride = -190; + buf.planes[0].basePtr = reinterpret_cast(678); + NVCVImageHandle handle = nullptr; + nvcv::ImageDataCleanupCallback callBackFunc{}; + + NVCVStatus st = nvcvImageWrapDataConstruct(&(nvcv::ImageDataStridedCuda{nvcv::FMT_U8, buf}.cdata()), + callBackFunc.targetFunc(), callBackFunc.targetHandle(), &handle); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, st); + if (st == NVCV_SUCCESS && handle != nullptr) + { + EXPECT_EQ(NVCV_SUCCESS, nvcvImageDecRef(handle, nullptr)); + } +} + +TEST(ImageWrapData, construct_invalid_buffer_rowStride_too_small) +{ + nvcv::ImageDataStridedCuda::Buffer buf; + buf.numPlanes = 1; + buf.planes[0].width = 173; + buf.planes[0].height = 79; + buf.planes[0].rowStride = 172; + buf.planes[0].basePtr = reinterpret_cast(678); + NVCVImageHandle handle = nullptr; + nvcv::ImageDataCleanupCallback callBackFunc{}; + + NVCVStatus st = nvcvImageWrapDataConstruct(&(nvcv::ImageDataStridedCuda{nvcv::FMT_U8, buf}.cdata()), + callBackFunc.targetFunc(), callBackFunc.targetHandle(), &handle); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, st); + if (st == NVCV_SUCCESS && handle != nullptr) + { + EXPECT_EQ(NVCV_SUCCESS, nvcvImageDecRef(handle, nullptr)); + } +} + TEST(ImageWrapData, construct_null_parameters) { nvcv::ImageDataStridedCuda::Buffer buf; @@ -522,11 +627,11 @@ TEST(Image, smoke_image_managed_memory) { [](int64_t size, int32_t) { - void *ptr = nullptr; + NVCVMemoryBuffer ptr = nullptr; cudaMallocManaged(&ptr, size); return ptr; }, - [](void *ptr, int64_t, int32_t) + [](NVCVMemoryBuffer ptr, int64_t, int32_t) { cudaFree(ptr); } diff --git a/tests/nvcv_types/system/TestImageBatch.cpp b/tests/nvcv_types/system/TestImageBatch.cpp index 23d7ab6bc..852eca265 100644 --- a/tests/nvcv_types/system/TestImageBatch.cpp +++ b/tests/nvcv_types/system/TestImageBatch.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -28,7 +28,8 @@ namespace t = ::testing; namespace test = nvcv::test; -static bool operator==(const NVCVImagePlaneStrided &a, const NVCVImagePlaneStrided &b) +static bool operator==(const NVCVImagePlaneStrided &a, // NOSONAR: defaulted comparisons are C++20. + const NVCVImagePlaneStrided &b) { return a.width == b.width && a.height == b.height && a.rowStride == b.rowStride && a.basePtr == b.basePtr; } @@ -85,7 +86,7 @@ TEST(ImageBatchVarShape, smoke_create) // empty data { - nvcv::ImageBatchData data = batch.exportData(0); + nvcv::ImageBatchData data = batch.exportData(nullptr); auto devdata = data.cast(); ASSERT_NE(nvcv::NullOpt, devdata); @@ -119,10 +120,10 @@ TEST(ImageBatchVarShape, smoke_create) auto calcMaxSize = [&goldImages]() { nvcv::Size2D maxSize = {0, 0}; - for (size_t i = 0; i < goldImages.size(); ++i) + for (const auto &goldImage : goldImages) { - maxSize.w = std::max(maxSize.w, goldImages[i].planes[0].width); - maxSize.h = std::max(maxSize.h, goldImages[i].planes[0].height); + maxSize.w = std::max(maxSize.w, goldImage.planes[0].width); + maxSize.h = std::max(maxSize.h, goldImage.planes[0].height); } return maxSize; }; @@ -210,7 +211,7 @@ TEST(ImageBatchVarShape, smoke_create) // use callback batch.pushBack( - [&]() -> nvcv::Image + [&batch, &addToGold]() { int i = batch.numImages(); if (i < 5) @@ -221,7 +222,7 @@ TEST(ImageBatchVarShape, smoke_create) } else { - return {}; + return nvcv::Image{}; } }); @@ -229,32 +230,33 @@ TEST(ImageBatchVarShape, smoke_create) { auto data = batch.exportData(stream); - auto devdata = data.cast(); - ASSERT_NE(nvcv::NullOpt, devdata); + auto currentDevData = data.cast(); + ASSERT_NE(nvcv::NullOpt, currentDevData); - ASSERT_EQ(devdata->uniqueFormat(), batch.uniqueFormat()); + ASSERT_EQ(currentDevData->uniqueFormat(), batch.uniqueFormat()); - ASSERT_EQ(goldHandles.size(), devdata->numImages()); - EXPECT_NE(nullptr, devdata->imageList()); - EXPECT_NE(nullptr, devdata->formatList()); - EXPECT_NE(nullptr, devdata->hostFormatList()); + ASSERT_EQ(goldHandles.size(), currentDevData->numImages()); + EXPECT_NE(nullptr, currentDevData->imageList()); + EXPECT_NE(nullptr, currentDevData->formatList()); + EXPECT_NE(nullptr, currentDevData->hostFormatList()); - EXPECT_EQ(calcMaxSize(), devdata->maxSize()); - EXPECT_EQ(devdata->maxSize(), batch.maxSize()); + EXPECT_EQ(calcMaxSize(), currentDevData->maxSize()); + EXPECT_EQ(currentDevData->maxSize(), batch.maxSize()); - std::vector images(devdata->numImages()); - ASSERT_EQ(cudaSuccess, cudaMemcpyAsync(images.data(), devdata->imageList(), sizeof(images[0]) * images.size(), - cudaMemcpyDeviceToHost, stream)); + std::vector images(currentDevData->numImages()); + ASSERT_EQ(cudaSuccess, cudaMemcpyAsync(images.data(), currentDevData->imageList(), + sizeof(images[0]) * images.size(), cudaMemcpyDeviceToHost, stream)); - std::vector formats(devdata->numImages()); - ASSERT_EQ(cudaSuccess, cudaMemcpyAsync(formats.data(), devdata->formatList(), + std::vector formats(currentDevData->numImages()); + ASSERT_EQ(cudaSuccess, cudaMemcpyAsync(formats.data(), currentDevData->formatList(), sizeof(formats[0]) * formats.size(), cudaMemcpyDeviceToHost, stream)); ASSERT_EQ(cudaSuccess, cudaStreamSynchronize(stream)); EXPECT_THAT(images, t::ElementsAreArray(goldImages)); EXPECT_THAT(formats, t::ElementsAreArray(goldFormats)); - EXPECT_THAT(std::make_tuple(devdata->hostFormatList(), devdata->numImages()), t::ElementsAreArray(goldFormats)); + EXPECT_THAT(std::make_tuple(currentDevData->hostFormatList(), currentDevData->numImages()), + t::ElementsAreArray(goldFormats)); int cur = 0; for (auto it = batch.begin(); it != batch.end(); ++it, ++cur) @@ -271,7 +273,7 @@ TEST(ImageBatchVarShape, smoke_create) { nvcv::ImageBatchVarShapeWrapHandle wrap(batch.handle()); - const nvcv::ImageBatchVarShape &wrapref = wrap; + const nvcv::ImageBatchVarShape &wrapref = wrap.resource(); EXPECT_EQ(batch.capacity(), wrapref.capacity()); ASSERT_EQ(batch.numImages(), wrapref.numImages()); EXPECT_EQ(batch.handle(), wrapref.handle()); @@ -300,7 +302,7 @@ TEST(ImageBatchVarShape, smoke_sync) if (imgdata) { goldImages.push_back(imgdata->cdata().buffer.strided); - goldFormats.push_back(imgdata->format()); + goldFormats.push_back(static_cast(imgdata->format())); goldHandles.push_back(img.handle()); } }; @@ -386,7 +388,7 @@ TEST(ImageBatchVarShape, push_callback_exceed_capacity) nvcv::ImageBatchVarShape batch(32); std::vector vec1Handles; - auto cb = [&]() -> nvcv::Image + auto cb = [&batch, &vec1Handles]() { int i = batch.numImages(); if (i < batch.capacity() + 1) @@ -397,13 +399,13 @@ TEST(ImageBatchVarShape, push_callback_exceed_capacity) } else { - return {}; + return nvcv::Image{}; } }; auto *pcb = &cb; - auto ccb = [](void *ctx) -> NVCVImageHandle + auto ccb = [](auto ctx) { - return nvcv::detail::GetImageHandleForPushBack((*decltype(pcb)(ctx))()); + return nvcv::detail::GetImageHandleForPushBack((*static_cast(ctx))()); }; EXPECT_EQ(NVCV_ERROR_OVERFLOW, nvcvImageBatchVarShapePushImagesCallback(batch.handle(), ccb, pcb)); @@ -421,7 +423,7 @@ TEST(ImageBatchVarShape, push_callback_null_cbPushImage) { nvcv::ImageBatchVarShape batch(32); - auto cb = [&]() -> nvcv::Image + auto cb = [&batch]() { int i = batch.numImages(); if (i < batch.capacity()) @@ -431,7 +433,7 @@ TEST(ImageBatchVarShape, push_callback_null_cbPushImage) } else { - return {}; + return nvcv::Image{}; } }; auto *pcb = &cb; @@ -554,8 +556,8 @@ TEST(ImageBatch, smoke_user_pointer) nvcv::ImageBatchVarShape batch(3); EXPECT_EQ(nullptr, batch.userPointer()); - batch.setUserPointer((void *)0x123); - EXPECT_EQ((void *)0x123, batch.userPointer()); + batch.setUserPointer(reinterpret_cast(0x123)); + EXPECT_EQ(reinterpret_cast(0x123), batch.userPointer()); batch.setUserPointer(nullptr); EXPECT_EQ(nullptr, batch.userPointer()); @@ -590,26 +592,32 @@ TEST(ImageBatch, smoke_cast) class ImageBatchNullParamTest : public ::testing::Test { protected: - ImageBatchNullParamTest() {} + ImageBatchNullParamTest() = default; - ~ImageBatchNullParamTest() {} + ~ImageBatchNullParamTest() override = default; void SetUp() override { - ASSERT_EQ(NVCV_SUCCESS, nvcvImageBatchVarShapeCalcRequirements(5, &reqs)); - ASSERT_EQ(NVCV_SUCCESS, nvcvImageBatchVarShapeConstruct(&reqs, nullptr, &handle)); + ASSERT_EQ(NVCV_SUCCESS, nvcvImageBatchVarShapeCalcRequirements(5, &m_reqs)); + ASSERT_EQ(NVCV_SUCCESS, nvcvImageBatchVarShapeConstruct(&m_reqs, nullptr, &m_handle)); } void TearDown() override { int newRef = 1; - ASSERT_EQ(NVCV_SUCCESS, nvcvImageBatchDecRef(handle, &newRef)); + ASSERT_EQ(NVCV_SUCCESS, nvcvImageBatchDecRef(m_handle, &newRef)); ASSERT_EQ(newRef, 0); - handle = nullptr; + m_handle = nullptr; } - NVCVImageBatchHandle handle; - NVCVImageBatchVarShapeRequirements reqs; + NVCVImageBatchHandle handle() const + { + return m_handle; + } + +private: + NVCVImageBatchHandle m_handle; + NVCVImageBatchVarShapeRequirements m_reqs; }; TEST(ImageBatch, calc_req_invalid_parameters) @@ -633,7 +641,7 @@ TEST(ImageBatch, valid_get_allocator) int tmp = 1; NVCVImageBatchHandle handle; NVCVImageBatchVarShapeRequirements reqs; - NVCVAllocatorHandle alloc = reinterpret_cast(&tmp); + auto alloc = reinterpret_cast(&tmp); EXPECT_NE(alloc, nullptr); EXPECT_EQ(NVCV_SUCCESS, nvcvImageBatchVarShapeCalcRequirements(5, &reqs)); @@ -660,35 +668,35 @@ TEST(ImageBatch, invalid_out_get_allocator) TEST_F(ImageBatchNullParamTest, get_user_pointer_null_output) { - EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcvImageBatchGetUserPointer(handle, nullptr)); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcvImageBatchGetUserPointer(handle(), nullptr)); } TEST_F(ImageBatchNullParamTest, get_num_images_null_output) { - EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcvImageBatchGetNumImages(handle, nullptr)); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcvImageBatchGetNumImages(handle(), nullptr)); } TEST_F(ImageBatchNullParamTest, get_batch_capacity_null_output) { - EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcvImageBatchGetCapacity(handle, nullptr)); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcvImageBatchGetCapacity(handle(), nullptr)); } TEST_F(ImageBatchNullParamTest, get_unique_format_null_output) { - EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcvImageBatchVarShapeGetUniqueFormat(handle, nullptr)); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcvImageBatchVarShapeGetUniqueFormat(handle(), nullptr)); } TEST_F(ImageBatchNullParamTest, gbatch_get_type_null_output) { - EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcvImageBatchGetType(handle, nullptr)); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcvImageBatchGetType(handle(), nullptr)); } TEST_F(ImageBatchNullParamTest, export_data_null_output) { - EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcvImageBatchExportData(handle, 0, nullptr)); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcvImageBatchExportData(handle(), 0, nullptr)); } TEST_F(ImageBatchNullParamTest, get_max_size_null_output) { - EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcvImageBatchVarShapeGetMaxSize(handle, nullptr, nullptr)); + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcvImageBatchVarShapeGetMaxSize(handle(), nullptr, nullptr)); } diff --git a/tests/nvcv_types/system/TestImageFormat.cpp b/tests/nvcv_types/system/TestImageFormat.cpp index 27a5eead2..16f06fc55 100644 --- a/tests/nvcv_types/system/TestImageFormat.cpp +++ b/tests/nvcv_types/system/TestImageFormat.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -24,6 +24,8 @@ #include #include +#include +#include #include namespace t = ::testing; @@ -41,20 +43,57 @@ struct Params NVCVMemLayout memLayout; NVCVSwizzle swizzle; NVCVAlphaType alphaType; - NVCVPacking packing0, packing1, packing2, packing3; + NVCVPacking packing0; + NVCVPacking packing1; + NVCVPacking packing2; + NVCVPacking packing3; NVCVDataKind dataKind; - int samplesHoriz, samplesVert; - NVCVChromaLocation locHoriz, locVert; - int bitsPerChannel[4] = {}; + int samplesHoriz; + int samplesVert; + NVCVChromaLocation locHoriz; + NVCVChromaLocation locVert; + std::array bitsPerChannel = {}; int planeCount; - struct + struct PlaneInfo { int bpp = 0; int channels = 0; NVCVDataType pixFormat = NVCV_DATA_TYPE_NONE; NVCVSwizzle swizzle = NVCV_SWIZZLE_0000; - } planes[4]; + }; + + struct PlaneList + { + PlaneList() = default; + + PlaneList(std::initializer_list values) + { + auto it = data.begin(); + for (const PlaneInfo &value : values) + { + if (it == data.end()) + { + break; + } + *it++ = value; + } + } + + PlaneInfo &operator[](std::size_t idx) + { + return data[idx]; + } + + const PlaneInfo &operator[](std::size_t idx) const + { + return data[idx]; + } + + std::array data = {}; + }; + + PlaneList planes; NVCVExtraChannelInfo exChannelInfo; }; @@ -71,6 +110,142 @@ std::ostream &operator<<(std::ostream &out, const Params &p) << ", exChannelInfo.channelType = " << p.exChannelInfo.channelType << ", alphaType= " << p.alphaType; } +int GetTestPlaneChannelCount(int plane) +{ + switch (plane) + { + case 0: + return 4; + case 1: + case 2: + return 2; + case 3: + return 1; + default: + NVCV_ASSERT(!"Invalid plane"); + return 0; + } +} + +int GetMaxBitsPerPixel(int plane, int nchannels) +{ + if (nchannels == 0) + { + return 0; + } + if (plane == 0) + { + return 256; + } + if (plane <= 2) + { + return 128; + } + + NVCV_ASSERT(plane == 3); + // 4th plane can have at most 64 bits as it doesn't have channel count nor pack. + return 64; +} + +int GetTestPackingCount(int plane, int bpp) +{ + if (bpp <= 4) + { + return 1; + } + if (bpp <= 8) + { + return plane == 3 ? 1 : 3; + } + return plane == 3 ? 1 : 8; +} + +int NextBitsPerPixel(int bpp) +{ + if (bpp <= 8) + { + return bpp * 2; + } + if (bpp < 32) + { + return bpp + 8; + } + if (bpp < 64) + { + return bpp + 16; + } + if (bpp < 128) + { + return bpp + 32; + } + return bpp + 64; +} + +int NextTestPack(int pack) +{ + return pack == 0 ? pack + 1 : pack << 1; +} + +NVCVImageFormat MakePackingTestFormat(int planes, int plane, NVCVPacking packing) +{ + constexpr uint64_t mask = UINT64_MAX; + + std::array packings = {}; + for (int p = 0; p < planes; ++p) + { + packings[p] = mask; + } + packings[plane] = packing; + + return NVCV_MAKE_COLOR_IMAGE_FORMAT(mask, mask, mask, mask, mask, mask, 4, packings[0], packings[1], packings[2], + packings[3]); +} + +void TestPlanePackingAndBitsPerPixel(int planes, int plane, int nchannels, int bpp) +{ + int packCount = GetTestPackingCount(plane, bpp); + for (int pack = 0; pack < packCount; pack = NextTestPack(pack)) + { + auto packing = static_cast(NVCV_DETAIL_BPP_NCH(bpp, nchannels) + pack); + + auto fmt = MakePackingTestFormat(planes, plane, packing); + + NVCVPacking testPacking; + ASSERT_EQ(NVCV_SUCCESS, nvcvImageFormatGetPlanePacking(fmt, plane, &testPacking)); + EXPECT_EQ(packing, testPacking); + + int testBPP; + ASSERT_EQ(NVCV_SUCCESS, nvcvImageFormatGetPlaneBitsPerPixel(fmt, plane, &testBPP)); + EXPECT_EQ(bpp, testBPP); + + int expectedChannels = nchannels; + // these represent 4 channels, but comprise 2 pixels, 3 different channels, not 4. + if (packing == NVCV_PACKING_X8_Y8__X8_Z8 || packing == NVCV_PACKING_Y8_X8__Z8_X8) + { + --expectedChannels; + } + + int testNChannels; + ASSERT_EQ(NVCV_SUCCESS, nvcvImageFormatGetPlaneNumChannels(fmt, plane, &testNChannels)); + EXPECT_EQ(expectedChannels, testNChannels); + + int testNumPlanes; + ASSERT_EQ(NVCV_SUCCESS, nvcvImageFormatGetNumPlanes(fmt, &testNumPlanes)); + EXPECT_EQ(planes, testNumPlanes); + + for (int p = planes; p < 4; ++p) + { + ASSERT_EQ(NVCV_SUCCESS, nvcvImageFormatGetPlaneNumChannels(fmt, p, &testNChannels)); + EXPECT_EQ(0, testNChannels); + } + if (t::Test::HasFailure()) + { + FAIL() << "#planes=" << planes << ", plane=" << plane << ", #channels=" << nchannels << ", bpp=" << bpp + << ", pack=" << pack; + } + } +} + } // namespace class ImageFormatTests : public t::TestWithParam @@ -338,7 +513,7 @@ TEST_P(ImageFormatTests, make_image_format) TEST(ImageFormatTests, make_image_format_fourth_plane_128bpp_fails) { - NVCVImageFormat fmt = NVCV_IMAGE_FORMAT_NV12; + auto fmt = NVCV_IMAGE_FORMAT_NV12; ASSERT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcvMakeColorImageFormat(&fmt, NVCV_COLOR_MODEL_RGB, NVCV_COLOR_SPEC_BT601, NVCV_MEM_LAYOUT_PL, @@ -384,7 +559,7 @@ TEST(ImageFormatTests, get_data_type_of_image_format_none) TEST(ImageFormatTests, set_valid_data_type_of_image_format_none) { - NVCVImageFormat fmt = NVCV_IMAGE_FORMAT_NONE; + auto fmt = NVCV_IMAGE_FORMAT_NONE; EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcvImageFormatSetDataKind(&fmt, NVCV_DATA_KIND_SIGNED)); } @@ -427,7 +602,7 @@ TEST(ImageFormatTests, get_extra_channel_info_null_output) TEST(ImageFormatTests, set_extra_channel_info_image_format_none) { NVCVExtraChannelInfo exChannelInfo = {2, 8, NVCV_DATA_KIND_UNSIGNED, NVCV_EXTRA_CHANNEL_POS3D}; - NVCVImageFormat fmt = NVCV_IMAGE_FORMAT_NONE; + auto fmt = NVCV_IMAGE_FORMAT_NONE; ASSERT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcvImageFormatSetExtraChannelInfo(&fmt, &exChannelInfo)); } @@ -440,7 +615,7 @@ TEST(ImageFormatTests, set_extra_channel_info_null_input_ptr) TEST(ImageFormatTests, set_extra_channel_info_max_min_bounds) { NVCVExtraChannelInfo exChannelInfo = {8, 8, NVCV_DATA_KIND_UNSIGNED, NVCV_EXTRA_CHANNEL_POS3D}; - NVCVImageFormat fmt = NVCV_IMAGE_FORMAT_BGRf32; + auto fmt = NVCV_IMAGE_FORMAT_BGRf32; ASSERT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcvImageFormatSetExtraChannelInfo(&fmt, &exChannelInfo)); exChannelInfo.numChannels = -1; @@ -449,14 +624,14 @@ TEST(ImageFormatTests, set_extra_channel_info_max_min_bounds) TEST(ImageFormatTests, set_extra_channel_info_planar_image_format) { - NVCVImageFormat fmt = NVCV_IMAGE_FORMAT_BGRf32p; + auto fmt = NVCV_IMAGE_FORMAT_BGRf32p; NVCVExtraChannelInfo exChannelInfo = {2, 8, NVCV_DATA_KIND_UNSIGNED, NVCV_EXTRA_CHANNEL_POS3D}; ASSERT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcvImageFormatSetExtraChannelInfo(&fmt, &exChannelInfo)); } TEST(ImageFormatTests, set_extra_channel_info_256bpp_fails) { - NVCVImageFormat fmt = NVCV_IMAGE_FORMAT_BGRf32; + auto fmt = NVCV_IMAGE_FORMAT_BGRf32; NVCVExtraChannelInfo exChannelInfo = {2, 256, NVCV_DATA_KIND_UNSIGNED, NVCV_EXTRA_CHANNEL_POS3D}; ASSERT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcvImageFormatSetExtraChannelInfo(&fmt, &exChannelInfo)); } @@ -470,13 +645,13 @@ TEST(ImageFormatTests, get_mem_layout_of_image_format_none) TEST(ImageFormatTests, set_valid_mem_layout_of_image_format_none) { - NVCVImageFormat fmt = NVCV_IMAGE_FORMAT_NONE; + auto fmt = NVCV_IMAGE_FORMAT_NONE; ASSERT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcvImageFormatSetMemLayout(&fmt, NVCV_MEM_LAYOUT_BL)); } TEST(ImageFormatTests, set_valid_color_spec_of_image_format_none) { - NVCVImageFormat fmt = NVCV_IMAGE_FORMAT_NONE; + auto fmt = NVCV_IMAGE_FORMAT_NONE; ASSERT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcvImageFormatSetColorSpec(&fmt, NVCV_COLOR_SPEC_BT601)); } @@ -489,7 +664,7 @@ TEST(ImageFormatTests, get_color_spec_of_image_format_none) TEST(ImageFormatTests, set_valid_raw_pattern_of_image_format_none) { - NVCVImageFormat fmt = NVCV_IMAGE_FORMAT_NONE; + auto fmt = NVCV_IMAGE_FORMAT_NONE; EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcvImageFormatSetRawPattern(&fmt, NVCV_RAW_BAYER_BGGR)); } @@ -508,7 +683,7 @@ TEST(ImageFormatTests, get_chroma_subsampling_of_image_format_none) TEST(ImageFormatTests, set_valid_chroma_subsampling_of_image_format_none) { - NVCVImageFormat fmt = NVCV_IMAGE_FORMAT_NONE; + auto fmt = NVCV_IMAGE_FORMAT_NONE; EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcvImageFormatSetChromaSubsampling(&fmt, NVCV_CSS_420)); } @@ -582,8 +757,8 @@ TEST_P(ImageFormatTests, check_swizzle) TEST(ImageFormatTests, check_alpha_type) { - NVCVImageFormat fmt = NVCV_IMAGE_FORMAT_RGBA8_UNASSOCIATED_ALPHA; - NVCVAlphaType alphaType; + auto fmt = NVCV_IMAGE_FORMAT_RGBA8_UNASSOCIATED_ALPHA; + NVCVAlphaType alphaType; ASSERT_EQ(NVCV_SUCCESS, nvcvImageFormatGetAlphaType(fmt, &alphaType)); EXPECT_EQ(NVCV_ALPHA_UNASSOCIATED, alphaType); @@ -701,8 +876,8 @@ TEST_P(ImageFormatTests, get_bits_per_channel) { const Params &p = GetParam(); - int bits[4]; - ASSERT_EQ(NVCV_SUCCESS, nvcvImageFormatGetBitsPerChannel(p.imgFormat, bits)); + std::array bits; + ASSERT_EQ(NVCV_SUCCESS, nvcvImageFormatGetBitsPerChannel(p.imgFormat, bits.data())); EXPECT_EQ(p.bitsPerChannel[0], bits[0]); EXPECT_EQ(p.bitsPerChannel[1], bits[1]); @@ -731,9 +906,9 @@ TEST_P(ImageFormatTests, check_plane_pixel_type) TEST(ImageFormatTests, invalid_plane_swizzle) { // purposedly wrong fmt (more packing channels than swizzle channels) - NVCVImageFormat fmt = NVCV_MAKE_COLOR_IMAGE_FORMAT( - NVCV_COLOR_MODEL_RGB, NVCV_COLOR_SPEC_BT601, NVCV_MEM_LAYOUT_PL, NVCV_DATA_KIND_UNSIGNED, NVCV_SWIZZLE_XYZ1, - NVCV_ALPHA_ASSOCIATED, 3, NVCV_PACKING_X8, NVCV_PACKING_X8_Y8, NVCV_PACKING_X8); + auto fmt = NVCV_MAKE_COLOR_IMAGE_FORMAT(NVCV_COLOR_MODEL_RGB, NVCV_COLOR_SPEC_BT601, NVCV_MEM_LAYOUT_PL, + NVCV_DATA_KIND_UNSIGNED, NVCV_SWIZZLE_XYZ1, NVCV_ALPHA_ASSOCIATED, 3, + NVCV_PACKING_X8, NVCV_PACKING_X8_Y8, NVCV_PACKING_X8); NVCVSwizzle sw; ASSERT_EQ(NVCV_SUCCESS, nvcvImageFormatGetPlaneSwizzle(fmt, 0, &sw)); @@ -749,196 +924,26 @@ TEST(ImageFormatTests, invalid_plane_swizzle) TEST(ImageFormatTests, packing_and_bits_per_pixel) { + auto testBitsPerPixel = [](int planes, int plane, int nchannels, int begBPP, int maxBPP) + { + for (int bpp = begBPP; bpp <= maxBPP; bpp = NextBitsPerPixel(bpp)) + { + ASSERT_NO_FATAL_FAILURE(TestPlanePackingAndBitsPerPixel(planes, plane, nchannels, bpp)); + } + }; + for (int planes = 1; planes <= 4; ++planes) { for (int plane = 0; plane < planes; ++plane) { - int begChannels = plane == 0 ? 1 : 0; - int channelCount; - switch (plane) - { - case 0: - channelCount = 4; - break; - case 1: - channelCount = 2; - break; - case 2: - channelCount = 2; - break; - case 3: - channelCount = 1; - break; - default: - FAIL() << "Invalid plane"; - } + int begChannels = plane == 0 ? 1 : 0; + int channelCount = GetTestPlaneChannelCount(plane); for (int nchannels = begChannels; nchannels <= channelCount; ++nchannels) { int begBPP = plane == 0 ? 1 : 8; - int maxBPP; - if (nchannels == 0) - { - maxBPP = 0; - } - else if (plane == 0) - { - maxBPP = 256; - } - else if (plane <= 2) - { - maxBPP = 128; - } - else - { - assert(plane == 3); - // 4th plane can have at most 64 bits as it doesn't have channel count nor - // pack. - maxBPP = 64; - } - - for (int bpp = begBPP; bpp <= maxBPP; - bpp <= 8 - ? (bpp *= 2) - : (bpp < 32 ? (bpp += 8) : (bpp < 64 ? (bpp += 16) : (bpp < 128 ? (bpp += 32) : (bpp += 64))))) - { - int packCount; - if (bpp <= 4) - { - packCount = 1; - } - else if (bpp <= 8) - { - if (plane == 3) - { - // 4th plane doesn't have pack code... - packCount = 1; - } - else - { - packCount = 3; - } - } - else - { - switch (plane) - { - case 0: - case 1: - case 2: - packCount = 8; - break; - case 3: - packCount = 1; - break; - default: - FAIL() << "Invalid plane"; - } - } - - for (int pack = 0; pack < packCount; (pack == 0 ? ++pack : pack <<= 1)) - { - NVCVPacking packing = (NVCVPacking)(NVCV_DETAIL_BPP_NCH(bpp, nchannels) + pack); - - uint64_t mask = UINT64_MAX; - - std::optional fmt; - switch (plane) - { - case 0: - switch (planes) - { - case 1: - fmt = NVCV_MAKE_COLOR_IMAGE_FORMAT(mask, mask, mask, mask, mask, mask, 4, packing, 0, 0, - 0); - break; - case 2: - fmt = NVCV_MAKE_COLOR_IMAGE_FORMAT(mask, mask, mask, mask, mask, mask, 4, packing, mask, - 0, 0); - break; - case 3: - fmt = NVCV_MAKE_COLOR_IMAGE_FORMAT(mask, mask, mask, mask, mask, mask, 4, packing, mask, - mask, 0); - break; - case 4: - fmt = NVCV_MAKE_COLOR_IMAGE_FORMAT(mask, mask, mask, mask, mask, mask, 4, packing, mask, - mask, mask); - break; - } - break; - - case 1: - switch (planes) - { - case 2: - fmt = NVCV_MAKE_COLOR_IMAGE_FORMAT(mask, mask, mask, mask, mask, mask, 4, mask, packing, - 0, 0); - break; - case 3: - fmt = NVCV_MAKE_COLOR_IMAGE_FORMAT(mask, mask, mask, mask, mask, mask, 4, mask, packing, - mask, 0); - break; - case 4: - fmt = NVCV_MAKE_COLOR_IMAGE_FORMAT(mask, mask, mask, mask, mask, mask, 4, mask, packing, - mask, mask); - break; - } - break; - case 2: - switch (planes) - { - case 3: - fmt = NVCV_MAKE_COLOR_IMAGE_FORMAT(mask, mask, mask, mask, mask, mask, 4, mask, mask, - packing, 0); - break; - case 4: - fmt = NVCV_MAKE_COLOR_IMAGE_FORMAT(mask, mask, mask, mask, mask, mask, 4, mask, mask, - packing, mask); - break; - } - break; - case 3: - fmt = NVCV_MAKE_COLOR_IMAGE_FORMAT(mask, mask, mask, mask, mask, mask, 4, mask, mask, mask, - packing); - break; - } - - NVCV_ASSERT(fmt); - - NVCVPacking testPacking; - ASSERT_EQ(NVCV_SUCCESS, nvcvImageFormatGetPlanePacking(*fmt, plane, &testPacking)); - EXPECT_EQ(packing, testPacking); - - int testBPP; - ASSERT_EQ(NVCV_SUCCESS, nvcvImageFormatGetPlaneBitsPerPixel(*fmt, plane, &testBPP)); - EXPECT_EQ(bpp, testBPP); - - // these represent 4 channels, but comprise 2 pixels, 3 different channels, not 4. - if (packing == NVCV_PACKING_X8_Y8__X8_Z8 || packing == NVCV_PACKING_Y8_X8__Z8_X8) - { - nchannels -= 1; - } - - int testNChannels; - ASSERT_EQ(NVCV_SUCCESS, nvcvImageFormatGetPlaneNumChannels(*fmt, plane, &testNChannels)); - EXPECT_EQ(nchannels, testNChannels); - - int testNumPlanes; - ASSERT_EQ(NVCV_SUCCESS, nvcvImageFormatGetNumPlanes(*fmt, &testNumPlanes)); - EXPECT_EQ(planes, testNumPlanes); - - for (int p = planes; p < 4; ++p) - { - ASSERT_EQ(NVCV_SUCCESS, nvcvImageFormatGetPlaneNumChannels(*fmt, p, &testNChannels)); - EXPECT_EQ(0, testNChannels); - } - if (this->HasFailure()) - { - FAIL() << "#planes=" << planes << ", plane=" << plane << ", #channels=" << nchannels - << ", bpp=" << bpp << ", pack=" << pack; - } - } - } + int maxBPP = GetMaxBitsPerPixel(plane, nchannels); + ASSERT_NO_FATAL_FAILURE(testBitsPerPixel(planes, plane, nchannels, begBPP, maxBPP)); } } } @@ -956,8 +961,7 @@ TEST(ImageFormatTests, get_swizzle) uint64_t mask = UINT64_MAX; for (auto swizzle : swizzleList) { - NVCVImageFormat fmt - = NVCV_MAKE_COLOR_IMAGE_FORMAT(mask, mask, mask, mask, swizzle, mask, 4, mask, mask, mask, mask); + auto fmt = NVCV_MAKE_COLOR_IMAGE_FORMAT(mask, mask, mask, mask, swizzle, mask, 4, mask, mask, mask, mask); NVCVSwizzle testSwizzle; ASSERT_EQ(NVCV_SUCCESS, nvcvImageFormatGetSwizzle(fmt, &testSwizzle)); @@ -971,8 +975,7 @@ TEST(ImageFormatTests, get_data_type) { uint64_t mask = UINT64_MAX; - NVCVImageFormat fmt - = NVCV_MAKE_COLOR_IMAGE_FORMAT(mask, mask, mask, dataKind, mask, mask, 4, mask, mask, mask, mask); + auto fmt = NVCV_MAKE_COLOR_IMAGE_FORMAT(mask, mask, mask, dataKind, mask, mask, 4, mask, mask, mask, mask); NVCVDataKind testDataKind; ASSERT_EQ(NVCV_SUCCESS, nvcvImageFormatGetDataKind(fmt, &testDataKind)); @@ -986,8 +989,7 @@ TEST(ImageFormatTests, get_raw_pattern) { uint64_t mask = UINT64_MAX; - NVCVImageFormat fmt - = NVCV_MAKE_RAW_IMAGE_FORMAT(raw_pattern, mask, mask, mask, mask, 4, mask, mask, mask, mask); + auto fmt = NVCV_MAKE_RAW_IMAGE_FORMAT(raw_pattern, mask, mask, mask, mask, 4, mask, mask, mask, mask); NVCVRawPattern testRawPattern; ASSERT_EQ(NVCV_SUCCESS, nvcvImageFormatGetRawPattern(fmt, &testRawPattern)); @@ -1001,8 +1003,7 @@ TEST(ImageFormatTests, get_mem_layout) { uint64_t mask = UINT64_MAX; - NVCVImageFormat fmt - = NVCV_MAKE_COLOR_IMAGE_FORMAT(mask, mask, memLayout, mask, mask, mask, 4, mask, mask, mask, mask); + auto fmt = NVCV_MAKE_COLOR_IMAGE_FORMAT(mask, mask, memLayout, mask, mask, mask, 4, mask, mask, mask, mask); NVCVMemLayout testMemLayout; ASSERT_EQ(NVCV_SUCCESS, nvcvImageFormatGetMemLayout(fmt, &testMemLayout)); @@ -1016,8 +1017,7 @@ TEST(ImageFormatTests, get_color_model) { uint64_t mask = UINT64_MAX; - NVCVImageFormat fmt - = NVCV_MAKE_COLOR_IMAGE_FORMAT(model, mask, mask, mask, mask, mask, 4, mask, mask, mask, mask); + auto fmt = NVCV_MAKE_COLOR_IMAGE_FORMAT(model, mask, mask, mask, mask, mask, 4, mask, mask, mask, mask); NVCVColorModel testColorModel; ASSERT_EQ(NVCV_SUCCESS, nvcvImageFormatGetColorModel(fmt, &testColorModel)); @@ -1028,8 +1028,7 @@ TEST(ImageFormatTests, get_color_model) { uint64_t mask = UINT64_MAX; - NVCVImageFormat fmt - = NVCV_MAKE_COLOR_IMAGE_FORMAT(model + 7 + 2, mask, mask, mask, mask, mask, 4, mask, mask, mask, mask); + auto fmt = NVCV_MAKE_COLOR_IMAGE_FORMAT(model + 7 + 2, mask, mask, mask, mask, mask, 4, mask, mask, mask, mask); NVCVColorModel testColorModel; ASSERT_EQ(NVCV_SUCCESS, nvcvImageFormatGetColorModel(fmt, &testColorModel)); @@ -1040,7 +1039,7 @@ TEST(ImageFormatTests, get_color_model) TEST(ImageFormatTests, make_image_format_null_packing_returns_invalid) { - NVCVImageFormat imgFormat = NVCV_IMAGE_FORMAT_NV12; + auto imgFormat = NVCV_IMAGE_FORMAT_NV12; ASSERT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcvMakeColorImageFormat(&imgFormat, NVCV_COLOR_MODEL_RGB, NVCV_COLOR_SPEC_UNDEFINED, NVCV_MEM_LAYOUT_PL, @@ -1127,7 +1126,7 @@ TEST(ImageFormatTests, set_extra_channel_info) TEST(ImageFormatTests, set_alphatype_imageformat_none) { - NVCVImageFormat imgFormat = NVCV_IMAGE_FORMAT_NONE; + auto imgFormat = NVCV_IMAGE_FORMAT_NONE; EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcvImageFormatSetAlphaType(&imgFormat, NVCV_ALPHA_UNASSOCIATED)); EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcvImageFormatSetAlphaType(nullptr, NVCV_ALPHA_UNASSOCIATED)); } @@ -1330,7 +1329,10 @@ struct ParamsPlaneSwizzle } NVCVImageFormat imgFormat; - NVCVSwizzle planeSwizzle0, planeSwizzle1, planeSwizzle2, planeSwizzle3; + NVCVSwizzle planeSwizzle0; + NVCVSwizzle planeSwizzle1; + NVCVSwizzle planeSwizzle2; + NVCVSwizzle planeSwizzle3; }; std::ostream &operator<<(std::ostream &out, const ParamsPlaneSwizzle &p) @@ -1467,7 +1469,7 @@ TEST_P(ImageFormatPlaneSwizzleTests, make_imageformat_from_planes) { const ParamsPlaneSwizzle &p = GetParam(); - NVCVImageFormat planes[4]; + std::array planes; for (int i = 0; i < 4; ++i) { ASSERT_EQ(NVCV_SUCCESS, nvcvImageFormatGetPlaneFormat(p.imgFormat, i, &planes[i])); @@ -1539,8 +1541,8 @@ TEST(ImageFormatTests, invalid_make_imageformat_from_planes) struct SwizzlePacking { - NVCVSwizzle swizzle; - NVCVPacking packing[4] = {}; + NVCVSwizzle swizzle; + std::array packing = {}; friend std::ostream &operator<<(std::ostream &out, const SwizzlePacking &sp) { @@ -1559,7 +1561,7 @@ class ImageFormatNegativeSwizzlePackingTests : public t::TestWithParam g_InvalidSwizzlePacking = +static const std::vector g_InvalidSwizzlePacking = { { NVCV_SWIZZLE_XY00, {NVCV_PACKING_X8} }, { NVCV_SWIZZLE_X000, {NVCV_PACKING_X8, NVCV_PACKING_X8} }, @@ -1602,9 +1604,9 @@ TEST(ImageFormatTests, make_yuv422_packed_yuyv) NVCV_DATA_KIND_UNSIGNED, NVCV_SWIZZLE_XYZ1, NVCV_PACKING_X8_Y8__X8_Z8, NVCV_PACKING_0, NVCV_PACKING_0, NVCV_PACKING_0, NVCV_ALPHA_ASSOCIATED, 0)); - NVCVImageFormat gold = NVCV_MAKE_YCbCr_IMAGE_FORMAT( - NVCV_COLOR_SPEC_SMPTE240M, NVCV_CSS_422, NVCV_MEM_LAYOUT_BLOCK16_LINEAR, NVCV_DATA_KIND_UNSIGNED, - NVCV_SWIZZLE_XYZ1, NVCV_ALPHA_ASSOCIATED, 1, NVCV_PACKING_X8_Y8__X8_Z8); + auto gold = NVCV_MAKE_YCbCr_IMAGE_FORMAT(NVCV_COLOR_SPEC_SMPTE240M, NVCV_CSS_422, NVCV_MEM_LAYOUT_BLOCK16_LINEAR, + NVCV_DATA_KIND_UNSIGNED, NVCV_SWIZZLE_XYZ1, NVCV_ALPHA_ASSOCIATED, 1, + NVCV_PACKING_X8_Y8__X8_Z8); EXPECT_EQ(gold, fmt); } @@ -1618,7 +1620,7 @@ TEST(ImageFormatTests, make_yuv422_packed_yuyv_extra_channels) NVCV_DATA_KIND_UNSIGNED, NVCV_SWIZZLE_XYZ1, NVCV_PACKING_X8_Y8__X8_Z8, NVCV_PACKING_0, NVCV_PACKING_0, NVCV_PACKING_0, NVCV_ALPHA_ASSOCIATED, &exChannelInfo)); - NVCVImageFormat gold = NVCV_MAKE_YCbCr_IMAGE_EXTRA_CHANNELS_FORMAT( + auto gold = NVCV_MAKE_YCbCr_IMAGE_EXTRA_CHANNELS_FORMAT( NVCV_COLOR_SPEC_SMPTE240M, NVCV_CSS_422, NVCV_MEM_LAYOUT_BLOCK16_LINEAR, NVCV_DATA_KIND_UNSIGNED, NVCV_SWIZZLE_XYZ1, NVCV_ALPHA_ASSOCIATED, 3, 16, NVCV_DATA_KIND_SIGNED, NVCV_EXTRA_CHANNEL_U, 1, NVCV_PACKING_X8_Y8__X8_Z8); @@ -1634,9 +1636,9 @@ TEST(ImageFormatTests, make_yuv422_packed_yvyu) NVCV_DATA_KIND_UNSIGNED, NVCV_SWIZZLE_XZY1, NVCV_PACKING_X8_Y8__X8_Z8, NVCV_PACKING_0, NVCV_PACKING_0, NVCV_PACKING_0, NVCV_ALPHA_ASSOCIATED, 0)); - NVCVImageFormat gold = NVCV_MAKE_YCbCr_IMAGE_FORMAT( - NVCV_COLOR_SPEC_SMPTE240M, NVCV_CSS_422, NVCV_MEM_LAYOUT_BLOCK16_LINEAR, NVCV_DATA_KIND_UNSIGNED, - NVCV_SWIZZLE_XZY1, NVCV_ALPHA_ASSOCIATED, 1, NVCV_PACKING_X8_Y8__X8_Z8); + auto gold = NVCV_MAKE_YCbCr_IMAGE_FORMAT(NVCV_COLOR_SPEC_SMPTE240M, NVCV_CSS_422, NVCV_MEM_LAYOUT_BLOCK16_LINEAR, + NVCV_DATA_KIND_UNSIGNED, NVCV_SWIZZLE_XZY1, NVCV_ALPHA_ASSOCIATED, 1, + NVCV_PACKING_X8_Y8__X8_Z8); EXPECT_EQ(gold, fmt); } @@ -1650,7 +1652,7 @@ TEST(ImageFormatTests, make_yuv422_packed_yvyu_extra_channels) NVCV_DATA_KIND_UNSIGNED, NVCV_SWIZZLE_XZY1, NVCV_PACKING_X8_Y8__X8_Z8, NVCV_PACKING_0, NVCV_PACKING_0, NVCV_PACKING_0, NVCV_ALPHA_ASSOCIATED, &exChannelInfo)); - NVCVImageFormat gold = NVCV_MAKE_YCbCr_IMAGE_EXTRA_CHANNELS_FORMAT( + auto gold = NVCV_MAKE_YCbCr_IMAGE_EXTRA_CHANNELS_FORMAT( NVCV_COLOR_SPEC_SMPTE240M, NVCV_CSS_422, NVCV_MEM_LAYOUT_BLOCK16_LINEAR, NVCV_DATA_KIND_UNSIGNED, NVCV_SWIZZLE_XZY1, NVCV_ALPHA_ASSOCIATED, 3, 16, NVCV_DATA_KIND_SIGNED, NVCV_EXTRA_CHANNEL_U, 1, NVCV_PACKING_X8_Y8__X8_Z8); @@ -1666,9 +1668,9 @@ TEST(ImageFormatTests, make_yuv422_packed_uyvy) NVCV_DATA_KIND_UNSIGNED, NVCV_SWIZZLE_XYZ1, NVCV_PACKING_Y8_X8__Z8_X8, NVCV_PACKING_0, NVCV_PACKING_0, NVCV_PACKING_0, NVCV_ALPHA_ASSOCIATED, 0)); - NVCVImageFormat gold = NVCV_MAKE_YCbCr_IMAGE_FORMAT( - NVCV_COLOR_SPEC_SMPTE240M, NVCV_CSS_422, NVCV_MEM_LAYOUT_BLOCK16_LINEAR, NVCV_DATA_KIND_UNSIGNED, - NVCV_SWIZZLE_XYZ1, NVCV_ALPHA_ASSOCIATED, 1, NVCV_PACKING_Y8_X8__Z8_X8); + auto gold = NVCV_MAKE_YCbCr_IMAGE_FORMAT(NVCV_COLOR_SPEC_SMPTE240M, NVCV_CSS_422, NVCV_MEM_LAYOUT_BLOCK16_LINEAR, + NVCV_DATA_KIND_UNSIGNED, NVCV_SWIZZLE_XYZ1, NVCV_ALPHA_ASSOCIATED, 1, + NVCV_PACKING_Y8_X8__Z8_X8); EXPECT_EQ(gold, fmt); } @@ -1681,9 +1683,9 @@ TEST(ImageFormatTests, make_yuv422_packed_vyuy) NVCV_DATA_KIND_UNSIGNED, NVCV_SWIZZLE_XZY1, NVCV_PACKING_Y8_X8__Z8_X8, NVCV_PACKING_0, NVCV_PACKING_0, NVCV_PACKING_0, NVCV_ALPHA_ASSOCIATED, 0)); - NVCVImageFormat gold = NVCV_MAKE_YCbCr_IMAGE_FORMAT( - NVCV_COLOR_SPEC_SMPTE240M, NVCV_CSS_422, NVCV_MEM_LAYOUT_BLOCK16_LINEAR, NVCV_DATA_KIND_UNSIGNED, - NVCV_SWIZZLE_XZY1, NVCV_ALPHA_ASSOCIATED, 1, NVCV_PACKING_Y8_X8__Z8_X8); + auto gold = NVCV_MAKE_YCbCr_IMAGE_FORMAT(NVCV_COLOR_SPEC_SMPTE240M, NVCV_CSS_422, NVCV_MEM_LAYOUT_BLOCK16_LINEAR, + NVCV_DATA_KIND_UNSIGNED, NVCV_SWIZZLE_XZY1, NVCV_ALPHA_ASSOCIATED, 1, + NVCV_PACKING_Y8_X8__Z8_X8); EXPECT_EQ(gold, fmt); } @@ -1942,7 +1944,7 @@ TEST(ImageFormatTests, none_image_format_must_be_0) TEST(ImageFormatTests, set_colorspec_to_undefined_of_fmt_with_undefined_colorspec) { - NVCVImageFormat fmt = NVCV_IMAGE_FORMAT_U8; + auto fmt = NVCV_IMAGE_FORMAT_U8; ASSERT_EQ(NVCV_SUCCESS, nvcvImageFormatSetColorSpec(&fmt, NVCV_COLOR_SPEC_UNDEFINED)); EXPECT_EQ(NVCV_IMAGE_FORMAT_U8, fmt); } @@ -1986,17 +1988,18 @@ TEST(ImageFormatTests, set_raw_fmt_to_undefined_colorspec) TEST(ImageFormatTests, set_non_color_fmt_to_undefined_colorspec) { - NVCVImageFormat fmt = NVCV_IMAGE_FORMAT_U8; + auto fmt = NVCV_IMAGE_FORMAT_U8; ASSERT_EQ(NVCV_SUCCESS, nvcvImageFormatSetColorSpec(&fmt, NVCV_COLOR_SPEC_UNDEFINED)); EXPECT_EQ(NVCV_IMAGE_FORMAT_U8, fmt); } struct SetSwizzlePackingTestParams { - NVCVImageFormat input, output; - NVCVSwizzle swizzle; - NVCVPacking packing[4]; - const char *msg; + NVCVImageFormat input; + NVCVImageFormat output; + NVCVSwizzle swizzle; + std::array packing; + const char *msg; friend std::ostream &operator<<(std::ostream &out, const SetSwizzlePackingTestParams &p) { @@ -2030,7 +2033,7 @@ struct SetSwizzlePackingTestParams // clang-format off -static std::vector g_SSPParamsSuccess = { +static const std::vector g_SSPParamsSuccess = { {MAKE_COLOR_IMAGE_FORMAT_ABBREV3(RGB, BT601, PL, UNSIGNED, XYZ1, ASSOCIATED, X64, X16, X32), MAKE_COLOR_IMAGE_FORMAT_ABBREV3(RGB, BT601, PL, UNSIGNED, ZYX1, ASSOCIATED, X16, X32, X64), NVCV_SWIZZLE_ZYX1, @@ -2056,7 +2059,7 @@ static std::vector g_SSPParamsSuccess = { "Identity, no-op"}, }; -static std::vector g_SSPParamsFailure = { +static const std::vector g_SSPParamsFailure = { {MAKE_COLOR_IMAGE_FORMAT_ABBREV4(RGB, BT601, PL, UNSIGNED, XYZW, ASSOCIATED, X16, X32, X8, X16), NVCV_IMAGE_FORMAT_NONE, NVCV_SWIZZLE_XYZ1, @@ -2113,7 +2116,8 @@ TEST_P(ImageFormatSetSwizzlePackingTests, run) struct ImageFormatPair { - NVCVImageFormat a, b; + NVCVImageFormat a; + NVCVImageFormat b; friend std::ostream &operator<<(std::ostream &out, const ImageFormatPair &p) { @@ -2147,7 +2151,7 @@ class ImageFormatDataLayoutTests : public t::TestWithParam g_SameDataLayout = { +static const std::vector g_SameDataLayout = { { MAKE_COLOR_IMAGE_FORMAT_ABBREV3(RGB, UNDEFINED, BL, UNSIGNED, XYZ0, ASSOCIATED, X8, X8, X8), MAKE_COLOR_IMAGE_FORMAT_ABBREV3(XYZ, UNDEFINED, BL, UNSIGNED, XYZ0, ASSOCIATED, X8, X8, X8), @@ -2170,7 +2174,7 @@ static std::vector g_SameDataLayout = { }, }; -static std::vector g_DifferentDataLayout = { +static const std::vector g_DifferentDataLayout = { { MAKE_COLOR_IMAGE_FORMAT_ABBREV3(RGB, UNDEFINED, BL, UNSIGNED, XYZ0, ASSOCIATED, X8, X8, X8), MAKE_COLOR_IMAGE_FORMAT_ABBREV3(RGB, UNDEFINED, BLOCK4_LINEAR, UNSIGNED, XYZ0, ASSOCIATED, X8, X8, X8), @@ -2209,8 +2213,8 @@ INSTANTIATE_TEST_SUITE_P(Different, ImageFormatDataLayoutTests, TEST_P(ImageFormatDataLayoutTests, data_layout) { - int res = std::get<0>(GetParam()); - const ImageFormatPair &fmt = std::get<1>(GetParam()); + int res = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + const ImageFormatPair &fmt = ::nvcv::test::ParamValue(std::get<1>(GetParam())); int8_t has; ASSERT_EQ(NVCV_SUCCESS, nvcvImageFormatHasSameDataLayout(fmt.a, fmt.b, &has)); @@ -2340,7 +2344,7 @@ TEST(ImageFormatTests, get_valid_plane_size) { int32_t outPlaneWidth; int32_t outPlaneHeight; - auto reset_output = [&outPlaneWidth, &outPlaneHeight]() -> void + auto reset_output = [&outPlaneWidth, &outPlaneHeight]() { outPlaneWidth = -1; outPlaneHeight = -1; @@ -2447,7 +2451,7 @@ class FCC m_code[3] = d; } - operator uint32_t() const + explicit operator uint32_t() const { #if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ return static_cast(((int)m_code[3] << 24) | ((int)m_code[2] << 16) | ((int)m_code[1] << 8) @@ -2460,13 +2464,23 @@ class FCC #endif } + friend bool operator==(FCC lhs, FCC rhs) + { + return static_cast(lhs) == static_cast(rhs); + } + + friend bool operator<(FCC lhs, FCC rhs) + { + return static_cast(lhs) < static_cast(rhs); + } + friend std::ostream &operator<<(std::ostream &out, FCC fcc) { return out << fcc.m_code[0] << fcc.m_code[1] << fcc.m_code[2] << fcc.m_code[3]; } private: - char m_code[4]; + std::array m_code; }; static const test::ValueList g_FromFourCCParams = { @@ -2499,16 +2513,20 @@ static const test::ValueList g_ToFourCCParams = { class ImageFormatFromFourCCTests : public t::TestWithParam> { -public: - ImageFormatFromFourCCTests() - : m_fourcc(std::get<0>(GetParam())) - , m_fmt(std::get<1>(GetParam())) +protected: + FCC fourcc() const { + return m_fourcc; } -protected: - FCC m_fourcc; - NVCVImageFormat m_fmt; + NVCVImageFormat format() const + { + return m_fmt; + } + +private: + FCC m_fourcc = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + NVCVImageFormat m_fmt{::nvcv::test::ParamValue(std::get<1>(GetParam()))}; }; NVCV_INSTANTIATE_TEST_SUITE_P(_, ImageFormatFromFourCCTests, g_FromFourCCParams); @@ -2516,69 +2534,76 @@ NVCV_INSTANTIATE_TEST_SUITE_P(_, ImageFormatFromFourCCTests, g_FromFourCCParams) TEST_P(ImageFormatFromFourCCTests, conversion_works) { NVCVColorSpec cspec; - ASSERT_EQ(NVCV_SUCCESS, nvcvImageFormatGetColorSpec(m_fmt, &cspec)); + ASSERT_EQ(NVCV_SUCCESS, nvcvImageFormatGetColorSpec(format(), &cspec)); NVCVImageFormat test; - ASSERT_EQ(NVCV_SUCCESS, nvcvMakeImageFormatFromFourCC(&test, m_fourcc, cspec, NVCV_MEM_LAYOUT_PL)); - EXPECT_EQ(m_fmt, test); + ASSERT_EQ(NVCV_SUCCESS, + nvcvMakeImageFormatFromFourCC(&test, static_cast(fourcc()), cspec, NVCV_MEM_LAYOUT_PL)); + EXPECT_EQ(format(), test); } TEST_P(ImageFormatFromFourCCTests, conversion_with_undefined_colorspec_works) { - NVCVImageFormat gold = m_fmt; + NVCVImageFormat gold = format(); ASSERT_EQ(NVCV_SUCCESS, nvcvImageFormatSetColorSpec(&gold, NVCV_COLOR_SPEC_UNDEFINED)); NVCVImageFormat test; - ASSERT_EQ(NVCV_SUCCESS, - nvcvMakeImageFormatFromFourCC(&test, m_fourcc, NVCV_COLOR_SPEC_UNDEFINED, NVCV_MEM_LAYOUT_PL)); + ASSERT_EQ(NVCV_SUCCESS, nvcvMakeImageFormatFromFourCC(&test, static_cast(fourcc()), + NVCV_COLOR_SPEC_UNDEFINED, NVCV_MEM_LAYOUT_PL)); EXPECT_EQ(gold, test); } TEST_P(ImageFormatFromFourCCTests, conversion_works_while_forcing_mem_layout_works) { - NVCVImageFormat gold = m_fmt; + NVCVImageFormat gold = format(); ASSERT_EQ(NVCV_SUCCESS, nvcvImageFormatSetMemLayout(&gold, NVCV_MEM_LAYOUT_BL)); NVCVColorSpec cspec; - ASSERT_EQ(NVCV_SUCCESS, nvcvImageFormatGetColorSpec(m_fmt, &cspec)); + ASSERT_EQ(NVCV_SUCCESS, nvcvImageFormatGetColorSpec(format(), &cspec)); NVCVImageFormat test; - ASSERT_EQ(NVCV_SUCCESS, nvcvMakeImageFormatFromFourCC(&test, m_fourcc, cspec, NVCV_MEM_LAYOUT_BL)); + ASSERT_EQ(NVCV_SUCCESS, + nvcvMakeImageFormatFromFourCC(&test, static_cast(fourcc()), cspec, NVCV_MEM_LAYOUT_BL)); EXPECT_EQ(gold, test); } TEST_P(ImageFormatFromFourCCTests, conversion_works_while_forcing_colorspec_works) { - NVCVImageFormat gold = m_fmt; + NVCVImageFormat gold = format(); ASSERT_EQ(NVCV_SUCCESS, nvcvImageFormatSetColorSpec(&gold, NVCV_COLOR_SPEC_BT2020)); NVCVImageFormat test; - ASSERT_EQ(NVCV_SUCCESS, nvcvMakeImageFormatFromFourCC(&test, m_fourcc, NVCV_COLOR_SPEC_BT2020, NVCV_MEM_LAYOUT_PL)); + ASSERT_EQ(NVCV_SUCCESS, nvcvMakeImageFormatFromFourCC(&test, static_cast(fourcc()), + NVCV_COLOR_SPEC_BT2020, NVCV_MEM_LAYOUT_PL)); EXPECT_EQ(gold, test); } class ImageFormatToFourCCTests : public t::TestWithParam> { -public: - ImageFormatToFourCCTests() - : m_fmt(std::get<0>(GetParam())) - , m_fourcc(std::get<1>(GetParam())) +protected: + NVCVImageFormat format() const { + return m_fmt; } -protected: - NVCVImageFormat m_fmt; - FCC m_fourcc; + FCC fourcc() const + { + return m_fourcc; + } + +private: + NVCVImageFormat m_fmt{::nvcv::test::ParamValue(std::get<0>(GetParam()))}; + FCC m_fourcc = ::nvcv::test::ParamValue(std::get<1>(GetParam())); }; NVCV_INSTANTIATE_TEST_SUITE_P(_, ImageFormatToFourCCTests, g_ToFourCCParams); TEST_P(ImageFormatToFourCCTests, conversion_works) { - uint32_t fourcc; - ASSERT_EQ(NVCV_SUCCESS, nvcvImageFormatToFourCC(m_fmt, &fourcc)); + uint32_t rawFourcc; + ASSERT_EQ(NVCV_SUCCESS, nvcvImageFormatToFourCC(format(), &rawFourcc)); - EXPECT_EQ(m_fourcc, FCC{fourcc}); + EXPECT_EQ(fourcc(), FCC{rawFourcc}); } TEST(ImageFormatFourCCTests, image_doesnt_have_fourcc_return_0) @@ -2591,7 +2616,8 @@ TEST(ImageFormatFourCCTests, invalid_fourcc_returns_invalid_imageformat) { NVCVImageFormat fmt; EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, - nvcvMakeImageFormatFromFourCC(&fmt, FCC('R', 'O', 'D', 'S'), NVCV_COLOR_SPEC_BT601, NVCV_MEM_LAYOUT_PL)); + nvcvMakeImageFormatFromFourCC(&fmt, static_cast(FCC('R', 'O', 'D', 'S')), NVCV_COLOR_SPEC_BT601, + NVCV_MEM_LAYOUT_PL)); } class ImageFormatPlanePixelStrideBytesExecTests @@ -2619,9 +2645,9 @@ NVCV_INSTANTIATE_TEST_SUITE_P(_,ImageFormatPlanePixelStrideBytesExecTests, TEST_P(ImageFormatPlanePixelStrideBytesExecTests, works) { - const NVCVImageFormat dtype = std::get<0>(GetParam()); - const int plane = std::get<1>(GetParam()); - const int goldStride = std::get<2>(GetParam()); + const NVCVImageFormat dtype = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + const int plane = ::nvcv::test::ParamValue(std::get<1>(GetParam())); + const int goldStride = ::nvcv::test::ParamValue(std::get<2>(GetParam())); int32_t testStride; ASSERT_EQ(NVCV_SUCCESS, nvcvImageFormatGetPlanePixelStrideBytes(dtype, plane, &testStride)); diff --git a/tests/nvcv_types/system/TestRequirements.cpp b/tests/nvcv_types/system/TestRequirements.cpp index f1d5471e9..41da32450 100644 --- a/tests/nvcv_types/system/TestRequirements.cpp +++ b/tests/nvcv_types/system/TestRequirements.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,12 +21,14 @@ #include #include +#include +#include namespace util = nvcv::util; static void AssertEq(const NVCVRequirements &reqGold, const nvcv::Requirements &req) { - for (int i = 0; i < nvcv::Requirements::Memory::size(); ++i) + for (int i = 0; i < nvcv::Requirements::Memory::size(); ++i) // NOSONAR { SCOPED_TRACE("log2 BlockSize: " + std::to_string(i)); @@ -38,10 +40,12 @@ static void AssertEq(const NVCVRequirements &reqGold, const nvcv::Requirements & TEST(Requirements, init) { - std::byte buf[sizeof(nvcv::Requirements)]; - memset(&buf, 123, sizeof(buf)); + alignas(nvcv::Requirements) std::array buf; + memset(buf.data(), 123, buf.size()); - auto *reqs = new (buf) nvcv::Requirements; + std::allocator alloc; + auto *reqs = static_cast(static_cast(buf.data())); + std::allocator_traits::construct(alloc, reqs); NVCVRequirements reqGold = {}; @@ -51,7 +55,7 @@ TEST(Requirements, init) EXPECT_EQ(0, CalcTotalSizeBytes(reqs->hostMem())); EXPECT_EQ(0, CalcTotalSizeBytes(reqs->hostPinnedMem())); - reqs->~Requirements(); + std::allocator_traits::destroy(alloc, reqs); } TEST(MemRequirements, add_buffer) @@ -103,7 +107,6 @@ TEST(MemRequirements, sub_buffer) ASSERT_NO_THROW(reqs.cudaMem().addBuffer(bufSize2, bufAlign2)); ASSERT_NO_THROW(reqs.cudaMem().addBuffer(-bufSize2 * 5, bufAlign2)); - bufSize2 = 0; for (int i = 0; i < reqs.cudaMem().size(); ++i) { @@ -124,7 +127,7 @@ TEST(Requirements, add) NVCVRequirements reqGold = {}; nvcv::Requirements reqA; - for (int i = 0; i < nvcv::Requirements::Memory::size(); ++i) + for (int i = 0; i < nvcv::Requirements::Memory::size(); ++i) // NOSONAR { int64_t bufAlign = ((int64_t)1) << i; @@ -146,7 +149,7 @@ TEST(Requirements, add) ASSERT_NO_FATAL_FAILURE(AssertEq(reqGold, reqSum)); nvcv::Requirements reqB; - for (int i = 0; i < nvcv::Requirements::Memory::size(); ++i) + for (int i = 0; i < nvcv::Requirements::Memory::size(); ++i) // NOSONAR { int64_t bufAlign = ((int64_t)1) << i; diff --git a/tests/nvcv_types/system/TestSize.cpp b/tests/nvcv_types/system/TestSize.cpp index 73c30916c..92fac43fd 100644 --- a/tests/nvcv_types/system/TestSize.cpp +++ b/tests/nvcv_types/system/TestSize.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -26,8 +26,8 @@ namespace test = nvcv::test; TEST(Size2D, C_interop) { NVCVSize2D c_size{5, 7}; - nvcv::Size2D cxx_size = c_size; // construction - EXPECT_EQ(cxx_size, c_size); // comparison + nvcv::Size2D cxx_size{c_size}; // construction + EXPECT_EQ(cxx_size, c_size); // comparison cxx_size.w++; EXPECT_NE(cxx_size, c_size); cxx_size = c_size; // assignment @@ -61,9 +61,9 @@ INSTANTIATE_TEST_SUITE_P(Negative, Size2DEqualityTests, TEST_P(Size2DEqualityTests, are_equal) { - nvcv::Size2D a = std::get<0>(GetParam()); - nvcv::Size2D b = std::get<1>(GetParam()); - bool equal = std::get<2>(GetParam()); + nvcv::Size2D a = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + nvcv::Size2D b = ::nvcv::test::ParamValue(std::get<1>(GetParam())); + bool equal = ::nvcv::test::ParamValue(std::get<2>(GetParam())); EXPECT_EQ(equal, a == b); EXPECT_EQ(!equal, a != b); @@ -92,16 +92,16 @@ INSTANTIATE_TEST_SUITE_P(Negative, Size2DLessThanTests, TEST_P(Size2DLessThanTests, is_less_than) { - nvcv::Size2D a = std::get<0>(GetParam()); - nvcv::Size2D b = std::get<1>(GetParam()); - bool lessThan = std::get<2>(GetParam()); + nvcv::Size2D a = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + nvcv::Size2D b = ::nvcv::test::ParamValue(std::get<1>(GetParam())); + bool lessThan = ::nvcv::test::ParamValue(std::get<2>(GetParam())); EXPECT_EQ(lessThan, a < b); } // Size2D print -------------------------------------------- -static test::ValueList g_Size2DNames = { +static const test::ValueList g_Size2DNames = { { nvcv::Size2D{1, 5}, "1x5"}, { nvcv::Size2D{2, 5}, "2x5"}, { nvcv::Size2D{2, 8}, "2x8"}, diff --git a/tests/nvcv_types/system/TestStatus.cpp b/tests/nvcv_types/system/TestStatus.cpp index 1ff64a52f..62638b99c 100644 --- a/tests/nvcv_types/system/TestStatus.cpp +++ b/tests/nvcv_types/system/TestStatus.cpp @@ -15,7 +15,10 @@ #include #include -// TODO: once we have functions that generate errors, we should +#include +#include + +// REVISIT: once we have functions that generate errors, we should // extend these tests to cover more scenarios namespace t = ::testing; @@ -26,25 +29,31 @@ class StatusNameTest : public t::TestWithParam> params{ + MAKE_STATUS_NAME(NVCV_SUCCESS), + MAKE_STATUS_NAME(NVCV_ERROR_NOT_IMPLEMENTED), + MAKE_STATUS_NAME(NVCV_ERROR_INVALID_ARGUMENT), + MAKE_STATUS_NAME(NVCV_ERROR_INVALID_IMAGE_FORMAT), + MAKE_STATUS_NAME(NVCV_ERROR_INVALID_OPERATION), + MAKE_STATUS_NAME(NVCV_ERROR_DEVICE), + MAKE_STATUS_NAME(NVCV_ERROR_NOT_READY), + MAKE_STATUS_NAME(NVCV_ERROR_OUT_OF_MEMORY), + MAKE_STATUS_NAME(NVCV_ERROR_INTERNAL), + MAKE_STATUS_NAME(NVCV_ERROR_NOT_COMPATIBLE), + MAKE_STATUS_NAME(NVCV_ERROR_OVERFLOW), + MAKE_STATUS_NAME(NVCV_ERROR_UNDERFLOW), + }; #ifndef ENABLE_SANITIZER - ,std::make_tuple(static_cast(255), "Unknown error") + params.emplace_back(static_cast(255), "Unknown error"); #endif - )); + return params; +} + +INSTANTIATE_TEST_SUITE_P(AllStatuses, StatusNameTest, t::ValuesIn(StatusNameParams())); -// clang-format on +#undef MAKE_STATUS_NAME TEST_P(StatusNameTest, get_name) { @@ -62,9 +71,9 @@ TEST(StatusTest, main_thread_has_success_status_by_default) TEST(StatusTest, get_last_status_msg_success_has_correct_message) { - char msg[NVCV_MAX_STATUS_MESSAGE_LENGTH]; - ASSERT_EQ(NVCV_SUCCESS, nvcvGetLastErrorMessage(msg, sizeof(msg))); - EXPECT_STREQ("success", msg); + std::array msg; + ASSERT_EQ(NVCV_SUCCESS, nvcvGetLastErrorMessage(msg.data(), msg.size())); + EXPECT_STREQ("success", msg.data()); } TEST(StatusTest, get_last_status_resets_error_state) @@ -85,19 +94,19 @@ TEST(StatusTest, get_last_status_msg_error_has_correct_message) { nvcvSetThreadStatus(NVCV_ERROR_INTERNAL, "test message"); - char msg[NVCV_MAX_STATUS_MESSAGE_LENGTH]; - ASSERT_EQ(NVCV_ERROR_INTERNAL, nvcvGetLastErrorMessage(msg, sizeof(msg))); - EXPECT_STREQ("test message", msg); + std::array msg; + ASSERT_EQ(NVCV_ERROR_INTERNAL, nvcvGetLastErrorMessage(msg.data(), msg.size())); + EXPECT_STREQ("test message", msg.data()); - ASSERT_EQ(NVCV_SUCCESS, nvcvGetLastErrorMessage(msg, sizeof(msg))); - EXPECT_STREQ("success", msg); + ASSERT_EQ(NVCV_SUCCESS, nvcvGetLastErrorMessage(msg.data(), msg.size())); + EXPECT_STREQ("success", msg.data()); } TEST(StatusTest, peek_at_last_status_msg_success_has_correct_message) { - char msg[NVCV_MAX_STATUS_MESSAGE_LENGTH]; - ASSERT_EQ(NVCV_SUCCESS, nvcvPeekAtLastErrorMessage(msg, sizeof(msg))); - EXPECT_STREQ("success", msg); + std::array msg; + ASSERT_EQ(NVCV_SUCCESS, nvcvPeekAtLastErrorMessage(msg.data(), msg.size())); + EXPECT_STREQ("success", msg.data()); } TEST(StatusTest, function_success_doesnot_reset_status) @@ -114,22 +123,31 @@ TEST(StatusTest, peek_at_last_status_msg_error_has_correct_message) { nvcvSetThreadStatus(NVCV_ERROR_DEVICE, "test message"); - char msg[NVCV_MAX_STATUS_MESSAGE_LENGTH]; - ASSERT_EQ(NVCV_ERROR_DEVICE, nvcvPeekAtLastErrorMessage(msg, sizeof(msg))); - EXPECT_STREQ("test message", msg); + std::array msg; + ASSERT_EQ(NVCV_ERROR_DEVICE, nvcvPeekAtLastErrorMessage(msg.data(), msg.size())); + EXPECT_STREQ("test message", msg.data()); msg[0] = '\0'; - ASSERT_EQ(NVCV_ERROR_DEVICE, nvcvPeekAtLastErrorMessage(msg, sizeof(msg))); - EXPECT_STREQ("test message", msg); + ASSERT_EQ(NVCV_ERROR_DEVICE, nvcvPeekAtLastErrorMessage(msg.data(), msg.size())); + EXPECT_STREQ("test message", msg.data()); } TEST(StatusTest, set_thread_status_var_arg) { nvcvSetThreadStatus(NVCV_ERROR_DEVICE, "test message %d %c %s", 123, 'r', "lima"); - char msg[NVCV_MAX_STATUS_MESSAGE_LENGTH]; - ASSERT_EQ(NVCV_ERROR_DEVICE, nvcvGetLastErrorMessage(msg, sizeof(msg))); - EXPECT_STREQ("test message 123 r lima", msg); + std::array msg; + ASSERT_EQ(NVCV_ERROR_DEVICE, nvcvGetLastErrorMessage(msg.data(), msg.size())); + EXPECT_STREQ("test message 123 r lima", msg.data()); +} + +TEST(StatusTest, set_thread_status_var_arg_numeric_modifiers) +{ + nvcvSetThreadStatus(NVCV_ERROR_DEVICE, "test message %ld %.2f %x %%", 123L, 4.5, 255U); + + std::array msg; + ASSERT_EQ(NVCV_ERROR_DEVICE, nvcvGetLastErrorMessage(msg.data(), msg.size())); + EXPECT_STREQ("test message 123 4.50 ff %", msg.data()); } TEST(StatusTest, set_thread_status_var_arg_list) @@ -145,9 +163,9 @@ TEST(StatusTest, set_thread_status_var_arg_list) fn("test message %d %s %c", 321, "rod", 'l'); - char msg[NVCV_MAX_STATUS_MESSAGE_LENGTH]; - ASSERT_EQ(NVCV_ERROR_DEVICE, nvcvGetLastErrorMessage(msg, sizeof(msg))); - EXPECT_STREQ("test message 321 rod l", msg); + std::array msg; + ASSERT_EQ(NVCV_ERROR_DEVICE, nvcvGetLastErrorMessage(msg.data(), msg.size())); + EXPECT_STREQ("test message 321 rod l", msg.data()); } TEST(StatusTest, set_thread_status_var_arg_list_1) @@ -161,7 +179,7 @@ TEST(StatusTest, set_thread_status_null_message) { nvcvSetThreadStatus(NVCV_ERROR_DEVICE, nullptr); - char msg[NVCV_MAX_STATUS_MESSAGE_LENGTH]; - ASSERT_EQ(NVCV_ERROR_DEVICE, nvcvGetLastErrorMessage(msg, sizeof(msg))); - EXPECT_STREQ("", msg); + std::array msg; + ASSERT_EQ(NVCV_ERROR_DEVICE, nvcvGetLastErrorMessage(msg.data(), msg.size())); + EXPECT_STREQ("", msg.data()); } diff --git a/tests/nvcv_types/system/TestTensor.cpp b/tests/nvcv_types/system/TestTensor.cpp index 14d9bf068..197edf536 100644 --- a/tests/nvcv_types/system/TestTensor.cpp +++ b/tests/nvcv_types/system/TestTensor.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -24,6 +24,7 @@ #include #include +#include #include #include #include @@ -33,23 +34,6 @@ namespace t = ::testing; namespace test = nvcv::test; -namespace std { -template -std::ostream &operator<<(std::ostream &out, const std::vector &v) -{ - out << '{'; - for (size_t i = 0; i < v.size(); ++i) - { - if (i > 0) - { - out << ','; - } - out << v[i]; - } - return out << '}'; -} -} // namespace std - class TensorImageTests : public t::TestWithParam, test::Param<"width", int>, test::Param<"height", int>, test::Param<"format", nvcv::ImageFormat>, @@ -70,12 +54,12 @@ NVCV_INSTANTIATE_TEST_SUITE_P(_, TensorImageTests, TEST_P(TensorImageTests, smoke_create) { - const int PARAM_NUM_IMAGES = std::get<0>(GetParam()); - const int PARAM_WIDTH = std::get<1>(GetParam()); - const int PARAM_HEIGHT = std::get<2>(GetParam()); - const nvcv::ImageFormat PARAM_FORMAT = std::get<3>(GetParam()); - const nvcv::TensorShape GOLD_SHAPE = std::get<4>(GetParam()); - const nvcv::DataType GOLD_DTYPE = std::get<5>(GetParam()); + const int PARAM_NUM_IMAGES = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + const int PARAM_WIDTH = ::nvcv::test::ParamValue(std::get<1>(GetParam())); + const int PARAM_HEIGHT = ::nvcv::test::ParamValue(std::get<2>(GetParam())); + const nvcv::ImageFormat PARAM_FORMAT = ::nvcv::test::ParamValue(std::get<3>(GetParam())); + const nvcv::TensorShape GOLD_SHAPE = ::nvcv::test::ParamValue(std::get<4>(GetParam())); + const nvcv::DataType GOLD_DTYPE = ::nvcv::test::ParamValue(std::get<5>(GetParam())); const int GOLD_RANK = 4; nvcv::Tensor tensor(PARAM_NUM_IMAGES, {PARAM_WIDTH, PARAM_HEIGHT}, PARAM_FORMAT); @@ -125,7 +109,7 @@ TEST_P(TensorImageTests, smoke_create) nvcv::Byte *sampleBuffer = access->sampleData(i); for (int p = 1; p < access->numPlanes(); ++p) { - nvcv::Byte *planeBuffer = access->planeData(p, sampleBuffer); + const nvcv::Byte *planeBuffer = access->planeData(p, sampleBuffer); // enough for one plane std::vector buf(access->numCols() * access->colStride() * access->numRows()); @@ -157,6 +141,8 @@ NVCV_INSTANTIATE_TEST_SUITE_P(_, TensorTests, {nvcv::TensorShape{{53, 17, 16, 3},nvcv::TENSOR_NHWC}, nvcv::TYPE_U8, {17*64,64,3,1}}, {nvcv::TensorShape{{4, 16, 17},nvcv::TENSOR_CHW}, nvcv::TYPE_U8, {16*32,32,1}}, {nvcv::TensorShape{{17, 16, 3},nvcv::TENSOR_HWC}, nvcv::TYPE_U8, {64,3,1}}, + {nvcv::TensorShape{{32, 3, 16, 16},nvcv::TENSOR_NCHW}, nvcv::TYPE_F16, {3*16*32,16*32,32,2}}, + {nvcv::TensorShape{{16, 24, 24, 4},nvcv::TENSOR_NHWC}, nvcv::TYPE_F16, {24*24*4*2,24*4*2,4*2,2}} } ); @@ -164,9 +150,9 @@ NVCV_INSTANTIATE_TEST_SUITE_P(_, TensorTests, TEST_P(TensorTests, smoke_create) { - const nvcv::TensorShape PARAM_SHAPE = std::get<0>(GetParam()); - const nvcv::DataType PARAM_DTYPE = std::get<1>(GetParam()); - const std::vector GOLD_SHAPE = std::get<2>(GetParam()); + const nvcv::TensorShape PARAM_SHAPE = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + const nvcv::DataType PARAM_DTYPE = ::nvcv::test::ParamValue(std::get<1>(GetParam())); + const std::vector GOLD_SHAPE = ::nvcv::test::ParamValue(std::get<2>(GetParam())); nvcv::Tensor tensor(PARAM_SHAPE, PARAM_DTYPE); @@ -187,8 +173,6 @@ TEST_P(TensorTests, smoke_create) TEST(TensorTests, smoke_create_allocator) { - ; - int64_t setBufLen = 0; int32_t setBufAlign = 0; @@ -202,11 +186,11 @@ TEST(TensorTests, smoke_create_allocator) setBufLen = size; setBufAlign = bufAlign; - void *ptr = nullptr; + NVCVMemoryBuffer ptr = nullptr; cudaMalloc(&ptr, size); return ptr; }, - [](void *ptr, int64_t bufLen, int32_t bufAlign) + [](NVCVMemoryBuffer ptr, int64_t, int32_t) { cudaFree(ptr); } @@ -252,20 +236,34 @@ TEST(Tensor, smoke_cast) EXPECT_EQ(ref, 0); } +TEST(Tensor, dec_ref_returns_new_reference_count) +{ + NVCVTensorRequirements reqs; + NVCVTensorHandle handle; + ASSERT_EQ(NVCV_SUCCESS, nvcvTensorCalcRequirementsForImages(1, 8, 8, NVCV_IMAGE_FORMAT_U8, 0, 0, &reqs)); + ASSERT_EQ(NVCV_SUCCESS, nvcvTensorConstruct(&reqs, nullptr, &handle)); + ASSERT_EQ(NVCV_SUCCESS, nvcvTensorIncRef(handle, nullptr)); + + int newRefCount = 0; + EXPECT_EQ(NVCV_SUCCESS, nvcvTensorDecRef(handle, &newRefCount)); + EXPECT_EQ(1, newRefCount); + EXPECT_EQ(NVCV_SUCCESS, nvcvTensorDecRef(handle, nullptr)); +} + TEST(Tensor, smoke_user_pointer) { nvcv::Tensor tensor(3, {163, 117}, nvcv::FMT_RGBA8); EXPECT_EQ(nullptr, tensor.userPointer()); - void *userPtr; + NVCVUserPointer userPtr; ASSERT_EQ(NVCV_SUCCESS, nvcvTensorGetUserPointer(tensor.handle(), &userPtr)); ASSERT_EQ(nullptr, userPtr); - tensor.setUserPointer((void *)0x123); - EXPECT_EQ((void *)0x123, tensor.userPointer()); + tensor.setUserPointer(reinterpret_cast(0x123)); + EXPECT_EQ(reinterpret_cast(0x123), tensor.userPointer()); ASSERT_EQ(NVCV_SUCCESS, nvcvTensorGetUserPointer(tensor.handle(), &userPtr)); - ASSERT_EQ((void *)0x123, userPtr); + ASSERT_EQ(reinterpret_cast(0x123), userPtr); tensor.setUserPointer(nullptr); EXPECT_EQ(nullptr, tensor.userPointer()); @@ -279,7 +277,7 @@ TEST(Tensor, valid_get_allocator) int tmp = 1; NVCVTensorHandle tensorHandle; NVCVTensorRequirements reqs; - NVCVAllocatorHandle alloc = reinterpret_cast(&tmp); + auto alloc = reinterpret_cast(&tmp); EXPECT_NE(alloc, nullptr); ASSERT_EQ(NVCV_SUCCESS, nvcvTensorCalcRequirementsForImages(1, 224, 224, NVCV_IMAGE_FORMAT_RGBA8, 0, 0, &reqs)); @@ -301,10 +299,11 @@ TEST(Tensor, layout_ne_op) TEST(TensorWrapData, valid_get_allocator) { int tmp = 1; - NVCVTensorHandle tensorHandle, tensorWrapHandle; + NVCVTensorHandle tensorHandle; + NVCVTensorHandle tensorWrapHandle; NVCVTensorData tensorData; NVCVTensorRequirements reqs; - NVCVAllocatorHandle alloc = reinterpret_cast(&tmp); + auto alloc = reinterpret_cast(&tmp); EXPECT_NE(alloc, nullptr); ASSERT_EQ(NVCV_SUCCESS, nvcvTensorCalcRequirementsForImages(1, 224, 224, NVCV_IMAGE_FORMAT_RGBA8, 0, 0, &reqs)); @@ -321,9 +320,8 @@ TEST(TensorWrapData, valid_get_allocator) TEST(TensorWrapData, smoke_create) { - nvcv::ImageFormat fmt - = nvcv::ImageFormat(nvcv::ColorModel::RGB, nvcv::CSPEC_BT601_ER, nvcv::MemLayout::PL, nvcv::DataKind::FLOAT, - nvcv::Swizzle::S_XY00, nvcv::Packing::X16, nvcv::Packing::X16); + auto fmt = nvcv::ImageFormat(nvcv::ColorModel::RGB, nvcv::CSPEC_BT601_ER, nvcv::MemLayout::PL, + nvcv::DataKind::FLOAT, nvcv::Swizzle::S_XY00, nvcv::Packing::X16, nvcv::Packing::X16); nvcv::DataType GOLD_DTYPE = fmt.planeDataType(0); nvcv::Tensor origTensor(5, {173, 79}, fmt, nvcv::MemAlignment{}.rowAddr(1).baseAddr(32)); // packed rows @@ -400,6 +398,8 @@ NVCV_INSTANTIATE_TEST_SUITE_P(_, TensorWrapImageTests, {{61,23}, nvcv::FMT_RGB8p, nvcv::TensorShape{{1,3,23,61},nvcv::TENSOR_NCHW}, nvcv::TYPE_U8}, {{61,23}, nvcv::FMT_F32, nvcv::TensorShape{{1,1,23,61},nvcv::TENSOR_NCHW}, nvcv::TYPE_F32}, {{61,23}, nvcv::FMT_2F32, nvcv::TensorShape{{1,23,61,2},nvcv::TENSOR_NHWC}, nvcv::TYPE_F32}, + {{61,23}, nvcv::FMT_F16, nvcv::TensorShape{{1,1,23,61},nvcv::TENSOR_NCHW}, nvcv::TYPE_F16}, + {{61,23}, nvcv::FMT_2F16, nvcv::TensorShape{{1,23,61,2},nvcv::TENSOR_NHWC}, nvcv::TYPE_F16} } ); @@ -407,10 +407,10 @@ NVCV_INSTANTIATE_TEST_SUITE_P(_, TensorWrapImageTests, TEST_P(TensorWrapImageTests, smoke_create) { - const nvcv::Size2D PARAM_SIZE = std::get<0>(GetParam()); - const nvcv::ImageFormat PARAM_FORMAT = std::get<1>(GetParam()); - const nvcv::TensorShape GOLD_SHAPE = std::get<2>(GetParam()); - const nvcv::DataType GOLD_DTYPE = std::get<3>(GetParam()); + const nvcv::Size2D PARAM_SIZE = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + const nvcv::ImageFormat PARAM_FORMAT = ::nvcv::test::ParamValue(std::get<1>(GetParam())); + const nvcv::TensorShape GOLD_SHAPE = ::nvcv::test::ParamValue(std::get<2>(GetParam())); + const nvcv::DataType GOLD_DTYPE = ::nvcv::test::ParamValue(std::get<3>(GetParam())); nvcv::Image img(PARAM_SIZE, PARAM_FORMAT); @@ -455,6 +455,9 @@ NVCV_INSTANTIATE_TEST_SUITE_P(Positive, TensorWrapParamTests, {{ {3,1, 5}, "NHW"}, {5*4,1,4}, nvcv::TYPE_F32}, {{ {3,1, 1}, "NHW"}, {5*4,1,1}, nvcv::TYPE_F32}, { {{10, 5,3}, "HWC"}, {5*3*4, 3*4,4}, nvcv::TYPE_F32}, + {{ {3,1, 5}, "NHW"}, {5*2,1,2}, nvcv::TYPE_F16}, + {{ {3,1, 1}, "NHW"}, {5*2,1,1}, nvcv::TYPE_F16}, + { {{10, 5,3}, "HWC"}, {5*3*2, 3*2,2}, nvcv::TYPE_F16}, } * NVCV_SUCCESS); NVCV_INSTANTIATE_TEST_SUITE_P(Negative, TensorWrapParamTests, @@ -472,10 +475,10 @@ NVCV_INSTANTIATE_TEST_SUITE_P(Negative, TensorWrapParamTests, TEST_P(TensorWrapParamTests, smoke_create) { - const nvcv::TensorShape PARAM_TSHAPE = std::get<0>(GetParam()); - const std::vector PARAM_STRIDES = std::get<1>(GetParam()); - const nvcv::DataType PARAM_DTYPE = std::get<2>(GetParam()); - const NVCVStatus GOLD_STATUS = std::get<3>(GetParam()); + const nvcv::TensorShape PARAM_TSHAPE = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + const std::vector PARAM_STRIDES = ::nvcv::test::ParamValue(std::get<1>(GetParam())); + const nvcv::DataType PARAM_DTYPE = ::nvcv::test::ParamValue(std::get<2>(GetParam())); + const NVCVStatus GOLD_STATUS = ::nvcv::test::ParamValue(std::get<3>(GetParam())); NVCVTensorBufferStrided buf = {}; for (size_t i = 0; i < PARAM_STRIDES.size(); ++i) @@ -507,9 +510,9 @@ TEST_P(TensorWrapParamTests, smoke_create) class TensorTests_Negative : public ::testing::Test { public: - TensorTests_Negative() {} + TensorTests_Negative() = default; - ~TensorTests_Negative() {} + ~TensorTests_Negative() override = default; void SetUp() override { @@ -560,22 +563,25 @@ TEST_F(TensorTests_Negative, invalid_parameter_TensorCalcRequirementsForImages) TEST_F(TensorTests_Negative, invalid_parameter_TensorCalcRequirements) { - int64_t valid_wh[] = {224, 224}; + std::array valid_wh = {224, 224}; EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, - nvcvTensorCalcRequirements(2, valid_wh, NVCV_DATA_TYPE_NONE, NVCV_TENSOR_LAYOUT_MAKE("HW"), 0, 0, + nvcvTensorCalcRequirements(2, valid_wh.data(), NVCV_DATA_TYPE_NONE, NVCV_TENSOR_LAYOUT_MAKE("HW"), 0, 0, &reqs)); // invalid dtype EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, - nvcvTensorCalcRequirements(3, valid_wh, NVCV_DATA_TYPE_U8, NVCV_TENSOR_LAYOUT_MAKE("HW"), 0, 0, + nvcvTensorCalcRequirements(3, valid_wh.data(), NVCV_DATA_TYPE_U8, NVCV_TENSOR_LAYOUT_MAKE("HW"), 0, 0, &reqs)); // mismatch rank EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, - nvcvTensorCalcRequirements(-1, valid_wh, NVCV_DATA_TYPE_U8, NVCV_TENSOR_LAYOUT_MAKE(""), 0, 0, + nvcvTensorCalcRequirements(-1, valid_wh.data(), NVCV_DATA_TYPE_U8, NVCV_TENSOR_LAYOUT_MAKE(""), 0, 0, &reqs)); // invalid rank - EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcvTensorCalcRequirements(2, valid_wh, NVCV_DATA_TYPE_U8, NVCV_TENSOR_NONE, - 3, 0, &reqs)); // invalid baseAddrAlignment - EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcvTensorCalcRequirements(2, valid_wh, NVCV_DATA_TYPE_U8, NVCV_TENSOR_NONE, - 0, 3, &reqs)); // invalid rowAddrAlignment EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, - nvcvTensorCalcRequirements(2, valid_wh, NVCV_DATA_TYPE_U8, NVCV_TENSOR_NONE, 0, 0, nullptr)); // null reqs + nvcvTensorCalcRequirements(2, valid_wh.data(), NVCV_DATA_TYPE_U8, NVCV_TENSOR_NONE, 3, 0, + &reqs)); // invalid baseAddrAlignment + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcvTensorCalcRequirements(2, valid_wh.data(), NVCV_DATA_TYPE_U8, NVCV_TENSOR_NONE, 0, 3, + &reqs)); // invalid rowAddrAlignment + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcvTensorCalcRequirements(2, valid_wh.data(), NVCV_DATA_TYPE_U8, NVCV_TENSOR_NONE, 0, 0, + nullptr)); // null reqs } TEST_F(TensorTests_Negative, invalid_parameter_TensorConstruct) @@ -607,6 +613,10 @@ TEST_F(TensorTests_Negative, invalid_parameter_TensorWrapDataConstruct) nvcvTensorWrapDataConstruct(nullptr, nullptr, nullptr, &handle)); // null tensorData EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcvTensorWrapDataConstruct(&tensorData, nullptr, nullptr, nullptr)); // null handle + + tensorData.bufferType = NVCV_TENSOR_BUFFER_NONE; + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcvTensorWrapDataConstruct(&tensorData, nullptr, nullptr, &handle)); // unsupported buffer type } TEST_F(TensorTests_Negative, invalid_parameter_TensorGetLayout) @@ -627,17 +637,17 @@ TEST_F(TensorTests_Negative, invalid_parameter_TensorExportData) TEST_F(TensorTests_Negative, invalid_parameter_TensorGetShape) { - int32_t rank = NVCV_TENSOR_MAX_RANK; - int64_t shape[NVCV_TENSOR_MAX_RANK] = {0}; + int32_t rank = NVCV_TENSOR_MAX_RANK; + std::array shape = {}; - EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcvTensorGetShape(nullptr, &rank, shape)); // null handle - EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcvTensorGetShape(handle, nullptr, shape)); // null rank - EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcvTensorGetShape(handle, &rank, nullptr)); // null shape + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcvTensorGetShape(nullptr, &rank, shape.data())); // null handle + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcvTensorGetShape(handle, nullptr, shape.data())); // null rank + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcvTensorGetShape(handle, &rank, nullptr)); // null shape } TEST_F(TensorTests_Negative, invalid_parameter_TensorGetUserPointer) { - void *userPtr; + NVCVUserPointer userPtr; EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcvTensorGetUserPointer(nullptr, &userPtr)); // null handle EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcvTensorGetUserPointer(handle, nullptr)); // null rank @@ -645,18 +655,94 @@ TEST_F(TensorTests_Negative, invalid_parameter_TensorGetUserPointer) TEST_F(TensorTests_Negative, invalid_parameter_TensorReshape) { - int64_t new_shape[] = {4, 224, 224}; - NVCVTensorHandle outHandle; + std::array new_shape = {4, 224, 224}; + NVCVTensorHandle outHandle; EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, - nvcvTensorReshape(nullptr, 3, new_shape, NVCV_TENSOR_CHW, &outHandle)); // null handle + nvcvTensorReshape(nullptr, 3, new_shape.data(), NVCV_TENSOR_CHW, &outHandle)); // null handle EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, - nvcvTensorReshape(handle, 0, new_shape, NVCV_TENSOR_CHW, &outHandle)); // invalid rank - EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcvTensorReshape(handle, NVCV_TENSOR_MAX_RANK + 1, new_shape, + nvcvTensorReshape(handle, 0, new_shape.data(), NVCV_TENSOR_CHW, &outHandle)); // invalid rank + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, nvcvTensorReshape(handle, NVCV_TENSOR_MAX_RANK + 1, new_shape.data(), NVCV_TENSOR_CHW, &outHandle)); // invalid rank 2 EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, - nvcvTensorReshape(handle, 3, new_shape, NVCV_TENSOR_HW, &outHandle)); // mismatch layout + nvcvTensorReshape(handle, 3, new_shape.data(), NVCV_TENSOR_HW, &outHandle)); // mismatch layout + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcvTensorReshape(handle, 3, new_shape.data(), NVCV_TENSOR_CHW, nullptr)); // null out handle +} + +TEST(TensorTests, reshape_contiguous_tensor) +{ + nvcv::TensorShape shape{ + {2, 3, 4}, + nvcv::TENSOR_HWC + }; + nvcv::Tensor tensor(shape, nvcv::TYPE_U8, nvcv::MemAlignment{}.rowAddr(1).baseAddr(32)); + std::array newShape{6, 4}; + NVCVTensorHandle reshapedHandle{}; + + ASSERT_EQ(NVCV_SUCCESS, nvcvTensorReshape(tensor.handle(), 2, newShape.data(), NVCV_TENSOR_HW, &reshapedHandle)); + nvcv::Tensor reshaped(std::move(reshapedHandle)); + + EXPECT_EQ((nvcv::TensorShape{ + {6, 4}, + nvcv::TENSOR_HW + }), + reshaped.shape()); +} + +TEST(TensorTests, reshape_rejects_mismatched_volume) +{ + nvcv::TensorShape shape{ + {2, 3, 4}, + nvcv::TENSOR_HWC + }; + nvcv::Tensor tensor(shape, nvcv::TYPE_U8); + std::array newShape{5, 5}; + NVCVTensorHandle reshapedHandle{}; + + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, + nvcvTensorReshape(tensor.handle(), 2, newShape.data(), NVCV_TENSOR_HW, &reshapedHandle)); +} + +TEST(TensorTests, reshape_rejects_shape_crossing_padded_stride) +{ + nvcv::TensorShape shape{ + {2, 3}, + nvcv::TENSOR_HW + }; + nvcv::Tensor tensor(shape, nvcv::TYPE_U8); + std::array newShape{6}; + NVCVTensorHandle reshapedHandle{}; + EXPECT_EQ(NVCV_ERROR_INVALID_ARGUMENT, - nvcvTensorReshape(handle, 3, new_shape, NVCV_TENSOR_CHW, nullptr)); // null out handle + nvcvTensorReshape(tensor.handle(), 1, newShape.data(), NVCV_TENSOR_W, &reshapedHandle)); +} + +TEST(TensorTests, reshape_rank_one_tensor) +{ + NVCVTensorData tensorData{}; + tensorData.bufferType = NVCV_TENSOR_BUFFER_STRIDED_CUDA; + tensorData.rank = 1; + tensorData.shape[0] = 6; + tensorData.dtype = NVCV_DATA_TYPE_U8; + tensorData.layout = NVCV_TENSOR_NONE; + tensorData.buffer.strided.basePtr = reinterpret_cast(1); + tensorData.buffer.strided.strides[0] = 1; + + NVCVTensorHandle tensorHandle{}; + ASSERT_EQ(NVCV_SUCCESS, nvcvTensorWrapDataConstruct(&tensorData, nullptr, nullptr, &tensorHandle)); + nvcv::Tensor tensor(std::move(tensorHandle)); + + std::array newShape{6}; + NVCVTensorHandle reshapedHandle{}; + + ASSERT_EQ(NVCV_SUCCESS, nvcvTensorReshape(tensor.handle(), 1, newShape.data(), NVCV_TENSOR_NONE, &reshapedHandle)); + nvcv::Tensor reshaped(std::move(reshapedHandle)); + + auto reshapedData = reshaped.exportData(); + ASSERT_TRUE(reshapedData); + EXPECT_EQ(1, reshapedData->rank()); + EXPECT_EQ(6, reshapedData->shape(0)); + EXPECT_EQ(1, reshapedData->stride(0)); } TEST_F(TensorTests_Negative, invalid_parameter_TensorShapePermute) @@ -700,10 +786,10 @@ NVCV_INSTANTIATE_TEST_SUITE_P(_, TensorPermuteTests, TEST_P(TensorPermuteTests, smoke) { - NVCVTensorLayout srcLayout = std::get<0>(GetParam()); - std::vector srcShape = std::get<1>(GetParam()); - NVCVTensorLayout dstLayout = std::get<2>(GetParam()); - const std::vector goldShape = std::get<3>(GetParam()); + NVCVTensorLayout srcLayout = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + std::vector srcShape = ::nvcv::test::ParamValue(std::get<1>(GetParam())); + NVCVTensorLayout dstLayout = ::nvcv::test::ParamValue(std::get<2>(GetParam())); + const std::vector goldShape = ::nvcv::test::ParamValue(std::get<3>(GetParam())); std::vector outShape(goldShape.size()); ASSERT_EQ(NVCV_SUCCESS, nvcvTensorShapePermute(srcLayout, srcShape.data(), dstLayout, outShape.data())); diff --git a/tests/nvcv_types/system/TestTensorBatch.cpp b/tests/nvcv_types/system/TestTensorBatch.cpp index 4d0550631..ee3eb949a 100644 --- a/tests/nvcv_types/system/TestTensorBatch.cpp +++ b/tests/nvcv_types/system/TestTensorBatch.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -33,13 +33,14 @@ namespace test = nvcv::test; template nvcv::Tensor GetRandomTensor(R &rg, const nvcv::ImageFormat &format) { - std::uniform_int_distribution shape_dist(100, 400); - std::uniform_int_distribution images_num_dist(1, 16); + std::uniform_int_distribution shape_dist(int32_t{100}, int32_t{400}); + std::uniform_int_distribution images_num_dist(int32_t{1}, int32_t{16}); return nvcv::Tensor(images_num_dist(rg), {shape_dist(rg), shape_dist(rg)}, format); } template -void CheckTensorBatchData(const nvcv::TensorBatchData &tbdata, It tensors_begin, It tensors_end, CUstream stream) +void CheckTensorBatchData( // NOSONAR: test helper requires random-access iterators. + const nvcv::TensorBatchData &tbdata, It tensors_begin, It tensors_end, CUstream stream) { auto numTensors = tensors_end - tensors_begin; ASSERT_EQ(numTensors, tbdata.numTensors()); @@ -53,9 +54,9 @@ void CheckTensorBatchData(const nvcv::TensorBatchData &tbdata, It tensors_begin, int i = 0; for (auto it = tensors_begin; it != tensors_end; ++it) { - nvcv::Tensor &tensor = *it; - auto tdata = tensor.exportData().cast().value(); - auto &element = elements[i]; + const nvcv::Tensor &tensor = *it; + auto tdata = tensor.exportData().cast().value(); + auto &element = elements[i]; EXPECT_EQ(tdata.layout(), tbdata.layout()); EXPECT_EQ(tdata.dtype(), tbdata.dtype()); EXPECT_EQ(tdata.basePtr(), reinterpret_cast(element.data)); @@ -78,8 +79,10 @@ TEST(TensorBatch, create) nvcv::TensorBatch tb(reqs); EXPECT_EQ(tb.layout(), nvcv::TensorLayout("")); EXPECT_EQ(tb.dtype(), nvcv::DataType()); + EXPECT_EQ(-1, tb.rank()); tb.pushBack(tensors[0]); ASSERT_EQ(tb.numTensors(), 1); + EXPECT_EQ(4, tb.rank()); ASSERT_EQ(tensors[0].refCount(), 2); auto tbdata = tb.exportData(nullptr); CheckTensorBatchData(tbdata, tensors.begin(), tensors.end(), nullptr); @@ -105,6 +108,25 @@ TEST(TensorBatch, ref_counting) ASSERT_EQ(tensor.refCount(), 1); } +TEST(TensorBatch, c_api_reference_count_and_rank_outputs) +{ + NVCVTensorBatchRequirements reqs; + NVCVTensorBatchHandle handle; + ASSERT_EQ(NVCV_SUCCESS, nvcvTensorBatchCalcRequirements(1, &reqs)); + ASSERT_EQ(NVCV_SUCCESS, nvcvTensorBatchConstruct(&reqs, nullptr, &handle)); + + int32_t rank = 0; + EXPECT_EQ(NVCV_SUCCESS, nvcvTensorBatchGetRank(handle, &rank)); + EXPECT_EQ(-1, rank); + + int32_t refCount = 0; + EXPECT_EQ(NVCV_SUCCESS, nvcvTensorBatchIncRef(handle, &refCount)); + EXPECT_EQ(2, refCount); + EXPECT_EQ(NVCV_SUCCESS, nvcvTensorBatchDecRef(handle, &refCount)); + EXPECT_EQ(1, refCount); + EXPECT_EQ(NVCV_SUCCESS, nvcvTensorBatchDecRef(handle, nullptr)); +} + TEST(TensorBatch, properties) { int32_t capacity = 32; @@ -128,16 +150,18 @@ TEST(TensorBatch, user_pointer) { auto reqs = nvcv::TensorBatch::CalcRequirements(1); nvcv::TensorBatch tb(reqs); - int valueA = 0; - tb.setUserPointer(&valueA); - EXPECT_EQ(tb.getUserPointer(), &valueA); + int valueA = 0; + auto userPtrA = static_cast(static_cast(&valueA)); + tb.setUserPointer(userPtrA); + EXPECT_EQ(tb.getUserPointer(), userPtrA); auto tbCopy = tb; std::cout << tb.refCount() << std::endl; - EXPECT_EQ(tbCopy.getUserPointer(), &valueA); - int valueB = 0; - tb.setUserPointer(&valueB); - EXPECT_EQ(tb.getUserPointer(), &valueB); - EXPECT_EQ(tbCopy.getUserPointer(), &valueB); + EXPECT_EQ(tbCopy.getUserPointer(), userPtrA); + int valueB = 0; + auto userPtrB = static_cast(static_cast(&valueB)); + tb.setUserPointer(userPtrB); + EXPECT_EQ(tb.getUserPointer(), userPtrB); + EXPECT_EQ(tbCopy.getUserPointer(), userPtrB); } TEST(TensorBatch, consistency_validation) @@ -145,7 +169,7 @@ TEST(TensorBatch, consistency_validation) std::mt19937 rg{321}; auto base_tensor = GetRandomTensor(rg, nvcv::FMT_RGB8); - auto test_inconsistency = [&](int32_t rank, nvcv::DataType dtype, nvcv::TensorLayout layout) + auto test_inconsistency = [&base_tensor](int32_t rank, nvcv::DataType dtype, nvcv::TensorLayout layout) { auto reqs = nvcv::TensorBatch::CalcRequirements(2); nvcv::TensorBatch tb(reqs); @@ -157,6 +181,7 @@ TEST(TensorBatch, consistency_validation) test_inconsistency(4, nvcv::TYPE_U8, nvcv::TensorLayout("FHWC")); test_inconsistency(4, nvcv::TYPE_U32, nvcv::TensorLayout("NHWC")); test_inconsistency(3, nvcv::TYPE_U8, nvcv::TensorLayout("HWC")); + test_inconsistency(4, nvcv::TYPE_F16, nvcv::TensorLayout("NHWC")); } TEST(TensorBatch, push_in_parts) @@ -188,12 +213,12 @@ TEST(TensorBatch, push_in_parts) } tensors_begin += i; } - for (auto &t : tensors) + for (const auto &t : tensors) { ASSERT_EQ(t.refCount(), 2); } } - for (auto &t : tensors) + for (const auto &t : tensors) { ASSERT_EQ(t.refCount(), 1); } @@ -232,17 +257,18 @@ TEST(TensorBatch, clear) auto reqs = nvcv::TensorBatch::CalcRequirements(capacity); nvcv::TensorBatch tb(reqs); tb.pushBack(tensors.begin(), tensors.end()); - for (auto &t : tensors) + for (const auto &t : tensors) { EXPECT_EQ(t.refCount(), 2); } tb.clear(); - for (auto &t : tensors) + for (const auto &t : tensors) { EXPECT_EQ(t.refCount(), 1); } EXPECT_EQ(tb.layout(), nvcv::TensorLayout("")); EXPECT_EQ(tb.dtype(), nvcv::DataType()); + EXPECT_EQ(-1, tb.rank()); } TEST(TensorBatch, pop_tensors) @@ -340,6 +366,20 @@ TEST(TensorBatch, pop_tensors) NVCV_EXPECT_THROW_STATUS(NVCV_ERROR_INVALID_ARGUMENT, tb.popTensors(-1)); } +TEST(TensorBatch, pop_last_tensor_resets_metadata) +{ + nvcv::Tensor tensor(1, {300, 300}, nvcv::FMT_RGB8); + nvcv::TensorBatch batch(nvcv::TensorBatch::CalcRequirements(1)); + batch.pushBack(tensor); + + batch.popTensor(); + + EXPECT_EQ(0, batch.numTensors()); + EXPECT_EQ(-1, batch.rank()); + EXPECT_EQ(nvcv::DataType{}, batch.dtype()); + EXPECT_EQ(nvcv::TensorLayout{}, batch.layout()); +} + TEST(TensorBatch, iterator_arithm) { int32_t capacity = 4; @@ -471,7 +511,7 @@ TEST(TensorBatch, valid_get_allocator) int tmp = 1; NVCVTensorBatchHandle tensorBatchHandle; NVCVTensorBatchRequirements req; - NVCVAllocatorHandle alloc = reinterpret_cast(&tmp); + auto alloc = reinterpret_cast(&tmp); EXPECT_NE(alloc, nullptr); EXPECT_EQ(NVCV_SUCCESS, nvcvTensorBatchCalcRequirements(16, &req)); diff --git a/tests/nvcv_types/system/TestTensorDataAccess.cpp b/tests/nvcv_types/system/TestTensorDataAccess.cpp index 0ecaa3966..a59ce3110 100644 --- a/tests/nvcv_types/system/TestTensorDataAccess.cpp +++ b/tests/nvcv_types/system/TestTensorDataAccess.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,6 +21,8 @@ #include #include +#include + namespace test = nvcv::test; namespace t = ::testing; @@ -29,28 +31,34 @@ namespace { class MyTensorDataStrided : public nvcv::TensorDataStrided { public: - MyTensorDataStrided(nvcv::TensorShape tshape, nvcv::TensorShape::ShapeType strides, void *basePtr = nullptr) + MyTensorDataStrided(nvcv::TensorShape tshape, nvcv::TensorShape::ShapeType strides, nvcv::Byte *basePtr = nullptr) : m_tshape(std::move(tshape)) , m_basePtr(basePtr) , m_strides(std::move(strides)) { - assert((int)strides.size() == tshape.rank()); + assert((int)m_strides.size() == m_tshape.rank()); NVCVTensorData &data = this->data(); data.bufferType = NVCV_TENSOR_BUFFER_STRIDED_CUDA; - data.rank = tshape.size(); + data.rank = m_tshape.size(); data.dtype = NVCV_DATA_TYPE_U8; - data.layout = tshape.layout(); + data.layout = static_cast(m_tshape.layout()); - const nvcv::TensorShape::ShapeType &shape = tshape.shape(); - std::copy(shape.begin(), shape.end(), data.shape); + const nvcv::TensorShape::ShapeType &shape = m_tshape.shape(); + std::ranges::copy(shape, data.shape); NVCVTensorBufferStrided &buffer = data.buffer.strided; buffer.basePtr = reinterpret_cast(basePtr); - std::copy(strides.begin(), strides.end(), buffer.strides); + std::ranges::copy(m_strides, buffer.strides); } + MyTensorDataStrided(const MyTensorDataStrided &) = default; + MyTensorDataStrided(MyTensorDataStrided &&) noexcept = default; + + MyTensorDataStrided &operator=(const MyTensorDataStrided &) = default; + MyTensorDataStrided &operator=(MyTensorDataStrided &&) noexcept = default; + bool operator==(const MyTensorDataStrided &that) const { return std::tie(m_tshape, m_basePtr, m_strides) == std::tie(that.m_tshape, that.m_basePtr, that.m_strides); @@ -73,7 +81,7 @@ class MyTensorDataStrided : public nvcv::TensorDataStrided private: nvcv::TensorShape m_tshape; - void *m_basePtr; + nvcv::Byte *m_basePtr; nvcv::TensorShape::ShapeType m_strides; }; @@ -94,8 +102,8 @@ NVCV_TEST_SUITE_P(TensorDataAccessStrided_SampleStride_ExecTests, TEST_P(TensorDataAccessStrided_SampleStride_ExecTests, works) { - const MyTensorDataStrided &input = std::get<0>(GetParam()); - const int64_t &gold = std::get<1>(GetParam()); + const MyTensorDataStrided &input = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + const int64_t &gold = ::nvcv::test::ParamValue(std::get<1>(GetParam())); auto info = nvcv::TensorDataAccessStrided::Create(input); ASSERT_TRUE(info); @@ -104,26 +112,30 @@ TEST_P(TensorDataAccessStrided_SampleStride_ExecTests, works) // TensorDataAccessStrided::sampleData ======================== -static std::byte *TEST_BASE_ADDR = reinterpret_cast(0x123); +static nvcv::Byte *TestBaseAddr() +{ + static std::array storage{}; + return storage.data(); +} // clang-format off NVCV_TEST_SUITE_P(TensorDataAccessStrided_SampleData_ExecTests, test::ValueList, test::Param<"idx",int>, - test::Param<"gold",void *>> + test::Param<"gold",nvcv::Byte *>> { - {MyTensorDataStrided({{4,34,2},"NxC"},{160,4,2},TEST_BASE_ADDR),0,TEST_BASE_ADDR+0}, - {MyTensorDataStrided({{4,34,2},"NxC"},{160,4,2},TEST_BASE_ADDR),1,TEST_BASE_ADDR+160}, - {MyTensorDataStrided({{4,34,2},"NxC"},{160,4,2},TEST_BASE_ADDR),2,TEST_BASE_ADDR+2*160}, + {MyTensorDataStrided({{4,34,2},"NxC"},{160,4,2},TestBaseAddr()),0,TestBaseAddr()+0}, + {MyTensorDataStrided({{4,34,2},"NxC"},{160,4,2},TestBaseAddr()),1,TestBaseAddr()+160}, + {MyTensorDataStrided({{4,34,2},"NxC"},{160,4,2},TestBaseAddr()),2,TestBaseAddr()+2*160}, }); // clang-format on TEST_P(TensorDataAccessStrided_SampleData_ExecTests, works) { - const MyTensorDataStrided &input = std::get<0>(GetParam()); - const int &idx = std::get<1>(GetParam()); - void *gold = std::get<2>(GetParam()); + const MyTensorDataStrided &input = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + const int &idx = ::nvcv::test::ParamValue(std::get<1>(GetParam())); + nvcv::Byte *gold = ::nvcv::test::ParamValue(std::get<2>(GetParam())); auto info = nvcv::TensorDataAccessStrided::Create(input); ASSERT_TRUE(info); @@ -146,8 +158,8 @@ NVCV_TEST_SUITE_P(TensorDataAccessStridedImage_ChannelStride_ExecTests, TEST_P(TensorDataAccessStridedImage_ChannelStride_ExecTests, works) { - const MyTensorDataStrided &input = std::get<0>(GetParam()); - const int64_t &gold = std::get<1>(GetParam()); + const MyTensorDataStrided &input = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + const int64_t &gold = ::nvcv::test::ParamValue(std::get<1>(GetParam())); auto info = nvcv::TensorDataAccessStridedImage::Create(input); ASSERT_TRUE(info); @@ -160,20 +172,20 @@ TEST_P(TensorDataAccessStridedImage_ChannelStride_ExecTests, works) NVCV_TEST_SUITE_P(TensorDataAccessStrided_ChannelData_ExecTests, test::ValueList, test::Param<"idx",int>, - test::Param<"gold",void *>> + test::Param<"gold",nvcv::Byte *>> { - {MyTensorDataStrided({{4,34,3},"NWC"},{160,4,2}, TEST_BASE_ADDR),0, TEST_BASE_ADDR+0}, - {MyTensorDataStrided({{4,34,3},"NWC"},{160,4,2}, TEST_BASE_ADDR),1, TEST_BASE_ADDR+2}, - {MyTensorDataStrided({{4,34,3},"NWC"},{160,4,2}, TEST_BASE_ADDR),2, TEST_BASE_ADDR+4}, + {MyTensorDataStrided({{4,34,3},"NWC"},{160,4,2}, TestBaseAddr()),0, TestBaseAddr()+0}, + {MyTensorDataStrided({{4,34,3},"NWC"},{160,4,2}, TestBaseAddr()),1, TestBaseAddr()+2}, + {MyTensorDataStrided({{4,34,3},"NWC"},{160,4,2}, TestBaseAddr()),2, TestBaseAddr()+4}, }); // clang-format on TEST_P(TensorDataAccessStrided_ChannelData_ExecTests, works) { - const MyTensorDataStrided &input = std::get<0>(GetParam()); - const int &idx = std::get<1>(GetParam()); - void *gold = std::get<2>(GetParam()); + const MyTensorDataStrided &input = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + const int &idx = ::nvcv::test::ParamValue(std::get<1>(GetParam())); + nvcv::Byte *gold = ::nvcv::test::ParamValue(std::get<2>(GetParam())); auto info = nvcv::TensorDataAccessStridedImage::Create(input); ASSERT_TRUE(info); @@ -195,8 +207,8 @@ NVCV_TEST_SUITE_P(TensorDataAccessStridedImage_RowStride_ExecTests, TEST_P(TensorDataAccessStridedImage_RowStride_ExecTests, works) { - const MyTensorDataStrided &input = std::get<0>(GetParam()); - const int64_t &gold = std::get<1>(GetParam()); + const MyTensorDataStrided &input = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + const int64_t &gold = ::nvcv::test::ParamValue(std::get<1>(GetParam())); auto info = nvcv::TensorDataAccessStridedImage::Create(input); ASSERT_TRUE(info); @@ -209,20 +221,20 @@ TEST_P(TensorDataAccessStridedImage_RowStride_ExecTests, works) NVCV_TEST_SUITE_P(TensorDataAccessStrided_RowData_ExecTests, test::ValueList, test::Param<"idx",int>, - test::Param<"gold",void *>> + test::Param<"gold",nvcv::Byte *>> { - {MyTensorDataStrided({{4,6,34,2},"NHWC"},{160,32,4,2}, TEST_BASE_ADDR), 0, TEST_BASE_ADDR+0}, - {MyTensorDataStrided({{4,6,34,2},"NHWC"},{160,32,4,2}, TEST_BASE_ADDR), 1, TEST_BASE_ADDR+32}, - {MyTensorDataStrided({{4,6,34,2},"NHWC"},{160,32,4,2}, TEST_BASE_ADDR), 2, TEST_BASE_ADDR+64}, + {MyTensorDataStrided({{4,6,34,2},"NHWC"},{160,32,4,2}, TestBaseAddr()), 0, TestBaseAddr()+0}, + {MyTensorDataStrided({{4,6,34,2},"NHWC"},{160,32,4,2}, TestBaseAddr()), 1, TestBaseAddr()+32}, + {MyTensorDataStrided({{4,6,34,2},"NHWC"},{160,32,4,2}, TestBaseAddr()), 2, TestBaseAddr()+64}, }); // clang-format on TEST_P(TensorDataAccessStrided_RowData_ExecTests, works) { - const MyTensorDataStrided &input = std::get<0>(GetParam()); - const int &idx = std::get<1>(GetParam()); - void *gold = std::get<2>(GetParam()); + const MyTensorDataStrided &input = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + const int &idx = ::nvcv::test::ParamValue(std::get<1>(GetParam())); + nvcv::Byte *gold = ::nvcv::test::ParamValue(std::get<2>(GetParam())); auto info = nvcv::TensorDataAccessStridedImage::Create(input); ASSERT_TRUE(info); @@ -244,8 +256,8 @@ NVCV_TEST_SUITE_P(TensorDataAccessStridedImagePlanar_planeStride_ExecTests, TEST_P(TensorDataAccessStridedImagePlanar_planeStride_ExecTests, works) { - const MyTensorDataStrided &input = std::get<0>(GetParam()); - const int64_t &gold = std::get<1>(GetParam()); + const MyTensorDataStrided &input = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + const int64_t &gold = ::nvcv::test::ParamValue(std::get<1>(GetParam())); auto info = nvcv::TensorDataAccessStridedImagePlanar::Create(input); ASSERT_TRUE(info); @@ -258,20 +270,20 @@ TEST_P(TensorDataAccessStridedImagePlanar_planeStride_ExecTests, works) NVCV_TEST_SUITE_P(TensorDataAccessStridedImagePlanar_planeData_ExecTests, test::ValueList, test::Param<"idx",int>, - test::Param<"gold",void *>> + test::Param<"gold",nvcv::Byte *>> { - {MyTensorDataStrided({{4,6,2},"NCW"},{160,32,2}, TEST_BASE_ADDR),0, TEST_BASE_ADDR+0}, - {MyTensorDataStrided({{4,6,2},"NCW"},{160,32,2}, TEST_BASE_ADDR),1, TEST_BASE_ADDR+32}, - {MyTensorDataStrided({{4,6,2},"NCW"},{160,32,2}, TEST_BASE_ADDR),2, TEST_BASE_ADDR+64}, + {MyTensorDataStrided({{4,6,2},"NCW"},{160,32,2}, TestBaseAddr()),0, TestBaseAddr()+0}, + {MyTensorDataStrided({{4,6,2},"NCW"},{160,32,2}, TestBaseAddr()),1, TestBaseAddr()+32}, + {MyTensorDataStrided({{4,6,2},"NCW"},{160,32,2}, TestBaseAddr()),2, TestBaseAddr()+64}, }); // clang-format on TEST_P(TensorDataAccessStridedImagePlanar_planeData_ExecTests, works) { - const MyTensorDataStrided &input = std::get<0>(GetParam()); - const int &idx = std::get<1>(GetParam()); - void *gold = std::get<2>(GetParam()); + const MyTensorDataStrided &input = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + const int &idx = ::nvcv::test::ParamValue(std::get<1>(GetParam())); + nvcv::Byte *gold = ::nvcv::test::ParamValue(std::get<2>(GetParam())); auto info = nvcv::TensorDataAccessStridedImagePlanar::Create(input); ASSERT_TRUE(info); diff --git a/tests/nvcv_types/system/TestTensorDataUtils.cpp b/tests/nvcv_types/system/TestTensorDataUtils.cpp index 9b96f5453..fbed8ab65 100644 --- a/tests/nvcv_types/system/TestTensorDataUtils.cpp +++ b/tests/nvcv_types/system/TestTensorDataUtils.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -54,23 +54,24 @@ NVCV_TEST_SUITE_P(TensorDataUtils, test::ValueList -static void compareTensor(nvcv::Tensor &tensor, DT fillVal) +static void compareTensor(const nvcv::Tensor &tensor, DT fillVal) { auto ac = nvcv::TensorDataAccessStrided::Create(tensor.exportData()); - int elements = ac->sampleStride() / sizeof(DT); + auto elements = static_cast(ac->sampleStride() / sizeof(DT)); std::vector
goldVec(elements, static_cast
(fillVal)); for (int i = 0; i < ac->numSamples(); ++i) { std::vector
readVec(elements); if (cudaMemcpy(readVec.data(), ac->sampleData(i), ac->sampleStride(), cudaMemcpyDeviceToHost) != cudaSuccess) - throw std::runtime_error("CudaMemcpy failed"); + throw util::TensorDataUtilsError("CudaMemcpy failed"); if (goldVec != readVec) - throw std::runtime_error("Vectors not equal"); + throw util::TensorDataUtilsError("Vectors not equal"); } return; } @@ -81,17 +82,17 @@ template static void GetSetTensor(const nvcv::Tensor &tensor) { auto ac = nvcv::TensorDataAccessStrided::Create(tensor.exportData()); - int numElements = ac->sampleStride() / sizeof(DT); + auto numElements = static_cast(ac->sampleStride() / sizeof(DT)); std::vector
vec(numElements); std::default_random_engine rng; std::uniform_int_distribution rand; - generate(vec.begin(), vec.end(), [&rng, &rand] { return rand(rng); }); + std::ranges::generate(vec, [&rng, &rand] { return rand(rng); }); std::vector
vecOut(numElements, 0); util::SetTensorFromVector
(tensor.exportData(), vec); util::GetVectorFromTensor
(tensor.exportData(), 0, vecOut); if (vec != vecOut) - throw std::runtime_error("Vectors not equal"); + throw util::TensorDataUtilsError("Vectors not equal"); return; } @@ -103,11 +104,11 @@ static void setGetTensorImage(const nvcv::Tensor &tensor) { auto ac = nvcv::TensorDataAccessStridedImage::Create(tensor.exportData()); int numElements = ac->numCols() * ac->numRows() * ac->numChannels(); - int numElementsFull = ac->sampleStride() / sizeof(DT); + auto numElementsFull = static_cast(ac->sampleStride() / sizeof(DT)); std::vector
vec(numElements, 0); std::default_random_engine rng; std::uniform_int_distribution rand; - generate(vec.begin(), vec.end(), [&rng, &rand] { return rand(rng); }); + std::ranges::generate(vec, [&rng, &rand] { return rand(rng); }); std::vector
vecOutImage(numElements, 0); std::vector
vecImageOriginal(numElements, 0); @@ -124,18 +125,44 @@ static void setGetTensorImage(const nvcv::Tensor &tensor) util::GetImageVectorFromTensor
(tensor.exportData(), 0, vecOutImage); if (vec != vecOutImage) - throw std::runtime_error( + throw util::TensorDataUtilsError( "Vectors not equal vector out image does note contain the same data as the input vector"); util::SetImageTensorFromVector
(tensor.exportData(), vecImageOriginal); util::GetVectorFromTensor
(tensor.exportData(), 0, vecOutFull); if (vecOriginal != vecOutFull) - throw std::runtime_error("Vectors not equal, vector not restored to original state"); + throw util::TensorDataUtilsError("Vectors not equal, vector not restored to original state"); return; } +template +static void checkPixelRange(util::TensorImageData &img, int x, int y, DT lowBound, DT highBound) +{ + for (int c = 0; c < img.numC(); c++) + { + DT value = *img.item
(x, y, c); + //note floats are [a,b), while ints are (a,b) but this should be sufficient + if (value < lowBound || value > highBound) + { + throw util::TensorDataUtilsError("Value out of bounds"); + } + } +} + +template +static void checkImageRange(util::TensorImageData &img, DT lowBound, DT highBound) +{ + for (int x = 0; x < img.size().w; x++) + { + for (int y = 0; y < img.size().h; y++) + { + checkPixelRange(img, x, y, lowBound, highBound); + } + } +} + template static void checkRndRange(const nvcv::Tensor &tensor, DT lowBound, DT highBound) { @@ -145,17 +172,7 @@ static void checkRndRange(const nvcv::Tensor &tensor, DT lowBound, DT highBound) { util::TensorImageData img(tensor.exportData(), sample); - for (int x = 0; x < img.size().w; x++) - for (int y = 0; y < img.size().h; y++) - for (int c = 0; c < img.numC(); c++) - { - DT value = *img.item
(x, y, c); - //note floats are [a,b), while ints are (a,b) but this should be sufficient - if (value < lowBound || value > highBound) - { - throw std::runtime_error("Value out of bounds"); - } - } + checkImageRange(img, lowBound, highBound); } return; } @@ -238,10 +255,10 @@ TEST_P(TensorDataUtils, SetGetTensorToFromByteVector) std::uniform_int_distribution rand(0u, 255u); // Test the CHW/HWC tensors - for (int i = 0; i < number; ++i) + for (int i = 0; i < number; ++i) // NOSONAR { std::vector imageVec((width * height) * bytesPerPixel); - std::generate(imageVec.begin(), imageVec.end(), [&]() { return (nvcv::Byte)rand(randEng); }); + std::ranges::generate(imageVec, [&rand, &randEng]() { return (nvcv::Byte)rand(randEng); }); std::vector outVec((width * height) * bytesPerPixel); EXPECT_NO_THROW(util::SetImageTensorFromByteVector(tensor.exportData(), imageVec, i)); EXPECT_NO_THROW(util::GetImageByteVectorFromTensor(tensor.exportData(), i, outVec)); @@ -261,40 +278,40 @@ TEST_P(TensorDataUtils, SetGetTensorFromImageVector) nvcv::Tensor tensor(number, {width, height}, fmt); // Just put in some random data EXPECT_NO_THROW(util::SetTensorToRandomValue(tensor.exportData(), 0, 0xFF)); - switch (fmt) + switch (static_cast(fmt)) { - case nvcv::FMT_BGR8: - case nvcv::FMT_RGBA8: - case nvcv::FMT_BGRA8: - case nvcv::FMT_BGR8p: - case nvcv::FMT_RGB8p: - case nvcv::FMT_RGBA8p: - case nvcv::FMT_BGRA8p: + case static_cast(nvcv::FMT_BGR8): + case static_cast(nvcv::FMT_RGBA8): + case static_cast(nvcv::FMT_BGRA8): + case static_cast(nvcv::FMT_BGR8p): + case static_cast(nvcv::FMT_RGB8p): + case static_cast(nvcv::FMT_RGBA8p): + case static_cast(nvcv::FMT_BGRA8p): { EXPECT_NO_THROW(setGetTensorImage(tensor)); break; } - case nvcv::FMT_U32: + case static_cast(nvcv::FMT_U32): { EXPECT_NO_THROW(setGetTensorImage(tensor)); break; } - case nvcv::FMT_RGBf32: - case nvcv::FMT_BGRf32: - case nvcv::FMT_RGBAf32: - case nvcv::FMT_BGRAf32: - case nvcv::FMT_RGBf32p: - case nvcv::FMT_BGRf32p: - case nvcv::FMT_RGBAf32p: - case nvcv::FMT_BGRAf32p: + case static_cast(nvcv::FMT_RGBf32): + case static_cast(nvcv::FMT_BGRf32): + case static_cast(nvcv::FMT_RGBAf32): + case static_cast(nvcv::FMT_BGRAf32): + case static_cast(nvcv::FMT_RGBf32p): + case static_cast(nvcv::FMT_BGRf32p): + case static_cast(nvcv::FMT_RGBAf32p): + case static_cast(nvcv::FMT_BGRAf32p): { EXPECT_NO_THROW(setGetTensorImage(tensor)); break; } - case nvcv::FMT_F64: + case static_cast(nvcv::FMT_F64): { EXPECT_NO_THROW(setGetTensorImage(tensor)); @@ -355,7 +372,7 @@ TEST(TensorDataUtils, SetCvImageData) EXPECT_NO_THROW( util::SetCvDataTo(cvTensor, 0xFF, region, util::chflags::C0 | util::chflags::C2 | util::chflags::C3)); - uint8_t *dataPtr = cvTensor.getVector().data(); + const uint8_t *dataPtr = cvTensor.getVector().data(); //1st Col EXPECT_EQ(*dataPtr, 0xFF); dataPtr += sizeof(uint8_t); @@ -412,22 +429,24 @@ TEST(TensorDataUtils, SetCvImageDataP) EXPECT_NO_THROW( util::SetCvDataTo(cvTensorFp, .5f, region, util::chflags::C0 | util::chflags::C2 | util::chflags::C3)); - float *dataPtr = (float *)cvTensorFp.getVector().data(); + const auto *dataPtr = static_cast(static_cast(cvTensorFp.getVector().data())); EXPECT_EQ(*dataPtr, .5f); EXPECT_EQ(*(dataPtr + cvTensorFp.planeStride() / sizeof(float)), 1.0f); EXPECT_EQ(*(dataPtr + 2 * cvTensorFp.planeStride() / sizeof(float)), .5f); EXPECT_EQ(*(dataPtr + 3 * cvTensorFp.planeStride() / sizeof(float)), .5f); // last col should be 1.0 - float *lastCol = (float *)(cvTensorFp.getVector().data() + (cvTensorFp.size().w - 1) * sizeof(float)); + const auto *lastCol = static_cast( + static_cast(cvTensorFp.getVector().data() + (cvTensorFp.size().w - 1) * sizeof(float))); EXPECT_EQ(*lastCol, 1.0f); EXPECT_EQ(*(lastCol + cvTensorFp.planeStride() / sizeof(float)), 1.0f); EXPECT_EQ(*(lastCol + 2 * cvTensorFp.planeStride() / sizeof(float)), 1.0f); EXPECT_EQ(*(lastCol + 3 * cvTensorFp.planeStride() / sizeof(float)), 1.0f); // last row should be 1.0 - float *lastRow = (float *)(cvTensorFp.getVector().data() + (cvTensorFp.size().h - 1) * cvTensorFp.rowStride()); - EXPECT_EQ((float)*lastRow, 1.0f); + const auto *lastRow = static_cast( + static_cast(cvTensorFp.getVector().data() + (cvTensorFp.size().h - 1) * cvTensorFp.rowStride())); + EXPECT_EQ(*lastRow, 1.0f); EXPECT_EQ(*(lastRow + cvTensorFp.planeStride() / sizeof(float)), 1.0f); EXPECT_EQ(*(lastRow + 2 * cvTensorFp.planeStride() / sizeof(float)), 1.0f); EXPECT_EQ(*(lastRow + 3 * cvTensorFp.planeStride() / sizeof(float)), 1.0f); diff --git a/tests/nvcv_types/system/TestTensorLayout.cpp b/tests/nvcv_types/system/TestTensorLayout.cpp index a4c2006c6..533940516 100644 --- a/tests/nvcv_types/system/TestTensorLayout.cpp +++ b/tests/nvcv_types/system/TestTensorLayout.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -41,8 +41,8 @@ NVCV_TEST_SUITE_P(TensorLayoutMakeExecTests, TEST_P(TensorLayoutMakeExecTests, from_string) { - const char *input = std::get<0>(GetParam()); - const NVCVTensorLayout &gold = std::get<1>(GetParam()); + const char *input = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + const NVCVTensorLayout &gold = ::nvcv::test::ParamValue(std::get<1>(GetParam())); NVCVTensorLayout test; ASSERT_EQ(NVCV_SUCCESS, nvcvTensorLayoutMake(input, &test)); @@ -51,8 +51,8 @@ TEST_P(TensorLayoutMakeExecTests, from_string) TEST_P(TensorLayoutMakeExecTests, from_range) { - const char *input = std::get<0>(GetParam()); - const NVCVTensorLayout &gold = std::get<1>(GetParam()); + const char *input = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + const NVCVTensorLayout &gold = ::nvcv::test::ParamValue(std::get<1>(GetParam())); if (!input) { GTEST_SKIP(); @@ -86,9 +86,9 @@ NVCV_TEST_SUITE_P(TensorLayoutMakeFirstExecTests, TEST_P(TensorLayoutMakeFirstExecTests, works) { - const NVCVTensorLayout &input = std::get<0>(GetParam()); - const int &n = std::get<1>(GetParam()); - const NVCVTensorLayout &gold = std::get<2>(GetParam()); + const NVCVTensorLayout &input = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + const int &n = ::nvcv::test::ParamValue(std::get<1>(GetParam())); + const NVCVTensorLayout &gold = ::nvcv::test::ParamValue(std::get<2>(GetParam())); NVCVTensorLayout test; ASSERT_EQ(NVCV_SUCCESS, nvcvTensorLayoutMakeFirst(input, n, &test)); @@ -118,9 +118,9 @@ NVCV_TEST_SUITE_P(TensorLayoutMakeLastExecTests, TEST_P(TensorLayoutMakeLastExecTests, works) { - const NVCVTensorLayout &input = std::get<0>(GetParam()); - const int &n = std::get<1>(GetParam()); - const NVCVTensorLayout &gold = std::get<2>(GetParam()); + const NVCVTensorLayout &input = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + const int &n = ::nvcv::test::ParamValue(std::get<1>(GetParam())); + const NVCVTensorLayout &gold = ::nvcv::test::ParamValue(std::get<2>(GetParam())); NVCVTensorLayout test; ASSERT_EQ(NVCV_SUCCESS, nvcvTensorLayoutMakeLast(input, n, &test)); @@ -171,10 +171,10 @@ NVCV_TEST_SUITE_P(TensorLayoutMakeSubRangeExecTests, TEST_P(TensorLayoutMakeSubRangeExecTests, works) { - const NVCVTensorLayout &input = std::get<0>(GetParam()); - const int &beg = std::get<1>(GetParam()); - const int &end = std::get<2>(GetParam()); - const NVCVTensorLayout &gold = std::get<3>(GetParam()); + const NVCVTensorLayout &input = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + const int &beg = ::nvcv::test::ParamValue(std::get<1>(GetParam())); + const int &end = ::nvcv::test::ParamValue(std::get<2>(GetParam())); + const NVCVTensorLayout &gold = ::nvcv::test::ParamValue(std::get<3>(GetParam())); NVCVTensorLayout test; ASSERT_EQ(NVCV_SUCCESS, nvcvTensorLayoutMakeSubRange(input, beg, end, &test)); @@ -205,10 +205,10 @@ NVCV_TEST_SUITE_P(TensorLayoutFindDimIndexExecTests, TEST_P(TensorLayoutFindDimIndexExecTests, works) { - const NVCVTensorLayout &input = std::get<0>(GetParam()); - const int &label = std::get<1>(GetParam()); - const int &start = std::get<2>(GetParam()); - const int &gold = std::get<3>(GetParam()); + const NVCVTensorLayout &input = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + const int &label = ::nvcv::test::ParamValue(std::get<1>(GetParam())); + const int &start = ::nvcv::test::ParamValue(std::get<2>(GetParam())); + const int &gold = ::nvcv::test::ParamValue(std::get<3>(GetParam())); EXPECT_EQ(gold, nvcvTensorLayoutFindDimIndex(input, label, start)); } @@ -237,9 +237,9 @@ NVCV_TEST_SUITE_P(TensorLayoutGetLabelExecTests, TEST_P(TensorLayoutGetLabelExecTests, works) { - const NVCVTensorLayout &input = std::get<0>(GetParam()); - const int &idx = std::get<1>(GetParam()); - const char &gold = std::get<2>(GetParam()); + const NVCVTensorLayout &input = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + const int &idx = ::nvcv::test::ParamValue(std::get<1>(GetParam())); + const char &gold = ::nvcv::test::ParamValue(std::get<2>(GetParam())); EXPECT_EQ(gold, nvcvTensorLayoutGetLabel(input, idx)); } @@ -262,8 +262,8 @@ NVCV_TEST_SUITE_P(TensorLayoutGetNumDimExecTests, TEST_P(TensorLayoutGetNumDimExecTests, works) { - const NVCVTensorLayout &input = std::get<0>(GetParam()); - const int &gold = std::get<1>(GetParam()); + const NVCVTensorLayout &input = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + const int &gold = ::nvcv::test::ParamValue(std::get<1>(GetParam())); EXPECT_EQ(gold, nvcvTensorLayoutGetNumDim(input)); } @@ -292,9 +292,9 @@ NVCV_TEST_SUITE_P(TensorLayoutCompareExecTests, TEST_P(TensorLayoutCompareExecTests, works) { - const NVCVTensorLayout &a = std::get<0>(GetParam()); - const NVCVTensorLayout &b = std::get<1>(GetParam()); - const int &gold = std::get<2>(GetParam()); + const NVCVTensorLayout &a = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + const NVCVTensorLayout &b = ::nvcv::test::ParamValue(std::get<1>(GetParam())); + const int &gold = ::nvcv::test::ParamValue(std::get<2>(GetParam())); EXPECT_EQ(gold, nvcvTensorLayoutCompare(a, b)); } @@ -321,8 +321,8 @@ NVCV_TEST_SUITE_P(TensorLayoutGetNameExecTests, TEST_P(TensorLayoutGetNameExecTests, works) { - const NVCVTensorLayout &layout = std::get<0>(GetParam()); - const char *gold = std::get<1>(GetParam()); + const NVCVTensorLayout &layout = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + const char *gold = ::nvcv::test::ParamValue(std::get<1>(GetParam())); EXPECT_STREQ(gold, nvcvTensorLayoutGetName(&layout)); } @@ -353,9 +353,9 @@ NVCV_TEST_SUITE_P(TensorLayoutStartsWithExecTests, TEST_P(TensorLayoutStartsWithExecTests, works) { - const NVCVTensorLayout &a = std::get<0>(GetParam()); - const NVCVTensorLayout &b = std::get<1>(GetParam()); - const bool &gold = std::get<2>(GetParam()); + const NVCVTensorLayout &a = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + const NVCVTensorLayout &b = ::nvcv::test::ParamValue(std::get<1>(GetParam())); + const bool &gold = ::nvcv::test::ParamValue(std::get<2>(GetParam())); EXPECT_EQ(gold, (bool)nvcvTensorLayoutStartsWith(a, b)); } @@ -387,9 +387,9 @@ NVCV_TEST_SUITE_P(TensorLayoutEndsWithExecTests, TEST_P(TensorLayoutEndsWithExecTests, works) { - const NVCVTensorLayout &a = std::get<0>(GetParam()); - const NVCVTensorLayout &b = std::get<1>(GetParam()); - const bool &gold = std::get<2>(GetParam()); + const NVCVTensorLayout &a = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + const NVCVTensorLayout &b = ::nvcv::test::ParamValue(std::get<1>(GetParam())); + const bool &gold = ::nvcv::test::ParamValue(std::get<2>(GetParam())); EXPECT_EQ(gold, (bool)nvcvTensorLayoutEndsWith(a, b)); } @@ -416,8 +416,8 @@ NVCV_TEST_SUITE_P(TensorLayoutOStreamExecTests, TEST_P(TensorLayoutOStreamExecTests, works) { - const nvcv::TensorLayout &layout = std::get<0>(GetParam()); - const char *gold = std::get<1>(GetParam()); + const nvcv::TensorLayout &layout = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + const char *gold = ::nvcv::test::ParamValue(std::get<1>(GetParam())); std::ostringstream ss; ss << layout; diff --git a/tests/nvcv_types/system/TestTensorLayoutInfo.cpp b/tests/nvcv_types/system/TestTensorLayoutInfo.cpp index 543a734b6..a1b50a5f3 100644 --- a/tests/nvcv_types/system/TestTensorLayoutInfo.cpp +++ b/tests/nvcv_types/system/TestTensorLayoutInfo.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -41,8 +41,8 @@ NVCV_TEST_SUITE_P(TensorLayoutInfo_IsBatch_ExecTests, TEST_P(TensorLayoutInfo_IsBatch_ExecTests, works) { - const nvcv::TensorLayout input{std::get<0>(GetParam())}; - const bool &gold = std::get<1>(GetParam()); + const nvcv::TensorLayout input{::nvcv::test::ParamValue(std::get<0>(GetParam()))}; + const bool &gold = ::nvcv::test::ParamValue(std::get<1>(GetParam())); auto info = nvcv::TensorLayoutInfo::Create(input); ASSERT_TRUE(info); @@ -71,8 +71,8 @@ NVCV_TEST_SUITE_P(TensorLayoutInfo_IdxSample_ExecTests, TEST_P(TensorLayoutInfo_IdxSample_ExecTests, works) { - const nvcv::TensorLayout input{std::get<0>(GetParam())}; - const int &gold = std::get<1>(GetParam()); + const nvcv::TensorLayout input{::nvcv::test::ParamValue(std::get<0>(GetParam()))}; + const int &gold = ::nvcv::test::ParamValue(std::get<1>(GetParam())); auto info = nvcv::TensorLayoutInfo::Create(input); ASSERT_TRUE(info); @@ -97,8 +97,8 @@ NVCV_TEST_SUITE_P(TensorLayoutInfo_IsImage_ExecTests, TEST_P(TensorLayoutInfo_IsImage_ExecTests, works) { - const nvcv::TensorLayout input{std::get<0>(GetParam())}; - const bool &gold = std::get<1>(GetParam()); + const nvcv::TensorLayout input{::nvcv::test::ParamValue(std::get<0>(GetParam()))}; + const bool &gold = ::nvcv::test::ParamValue(std::get<1>(GetParam())); auto info = nvcv::TensorLayoutInfo::Create(input); ASSERT_TRUE(info); @@ -128,8 +128,8 @@ NVCV_TEST_SUITE_P(TensorLayoutInfoImage_NumSpatialDims_ExecTests, TEST_P(TensorLayoutInfoImage_NumSpatialDims_ExecTests, works) { - const nvcv::TensorLayout input{std::get<0>(GetParam())}; - const int &gold = std::get<1>(GetParam()); + const nvcv::TensorLayout input{::nvcv::test::ParamValue(std::get<0>(GetParam()))}; + const int &gold = ::nvcv::test::ParamValue(std::get<1>(GetParam())); auto info = nvcv::TensorLayoutInfoImage::Create(input); ASSERT_TRUE(info); @@ -172,8 +172,8 @@ NVCV_TEST_SUITE_P(TensorLayoutInfoImage_IsRowMajor_ExecTests, TEST_P(TensorLayoutInfoImage_IsRowMajor_ExecTests, works) { - const nvcv::TensorLayout input{std::get<0>(GetParam())}; - const bool &gold = std::get<1>(GetParam()); + const nvcv::TensorLayout input{::nvcv::test::ParamValue(std::get<0>(GetParam()))}; + const bool &gold = ::nvcv::test::ParamValue(std::get<1>(GetParam())); auto info = nvcv::TensorLayoutInfoImage::Create(input); ASSERT_TRUE(info); @@ -200,8 +200,8 @@ NVCV_TEST_SUITE_P(TensorLayoutInfoImage_IdxChannel_ExecTests, TEST_P(TensorLayoutInfoImage_IdxChannel_ExecTests, works) { - const nvcv::TensorLayout input{std::get<0>(GetParam())}; - const int &gold = std::get<1>(GetParam()); + const nvcv::TensorLayout input{::nvcv::test::ParamValue(std::get<0>(GetParam()))}; + const int &gold = ::nvcv::test::ParamValue(std::get<1>(GetParam())); auto info = nvcv::TensorLayoutInfoImage::Create(input); ASSERT_TRUE(info); @@ -228,8 +228,8 @@ NVCV_TEST_SUITE_P(TensorLayoutInfoImage_IdxWidth_ExecTests, TEST_P(TensorLayoutInfoImage_IdxWidth_ExecTests, works) { - const nvcv::TensorLayout input{std::get<0>(GetParam())}; - const int &gold = std::get<1>(GetParam()); + const nvcv::TensorLayout input{::nvcv::test::ParamValue(std::get<0>(GetParam()))}; + const int &gold = ::nvcv::test::ParamValue(std::get<1>(GetParam())); auto info = nvcv::TensorLayoutInfoImage::Create(input); ASSERT_TRUE(info); @@ -256,8 +256,8 @@ NVCV_TEST_SUITE_P(TensorLayoutInfoImage_IdxHeight_ExecTests, TEST_P(TensorLayoutInfoImage_IdxHeight_ExecTests, works) { - const nvcv::TensorLayout input{std::get<0>(GetParam())}; - const int &gold = std::get<1>(GetParam()); + const nvcv::TensorLayout input{::nvcv::test::ParamValue(std::get<0>(GetParam()))}; + const int &gold = ::nvcv::test::ParamValue(std::get<1>(GetParam())); auto info = nvcv::TensorLayoutInfoImage::Create(input); ASSERT_TRUE(info); @@ -284,8 +284,8 @@ NVCV_TEST_SUITE_P(TensorLayoutInfoImage_IdxDepth_ExecTests, TEST_P(TensorLayoutInfoImage_IdxDepth_ExecTests, works) { - const nvcv::TensorLayout input{std::get<0>(GetParam())}; - const int &gold = std::get<1>(GetParam()); + const nvcv::TensorLayout input{::nvcv::test::ParamValue(std::get<0>(GetParam()))}; + const int &gold = ::nvcv::test::ParamValue(std::get<1>(GetParam())); auto info = nvcv::TensorLayoutInfoImage::Create(input); ASSERT_TRUE(info); @@ -310,8 +310,8 @@ NVCV_TEST_SUITE_P(TensorLayoutInfoImage_IsChannelFirst_ExecTests, TEST_P(TensorLayoutInfoImage_IsChannelFirst_ExecTests, works) { - const nvcv::TensorLayout input{std::get<0>(GetParam())}; - const int &gold = std::get<1>(GetParam()); + const nvcv::TensorLayout input{::nvcv::test::ParamValue(std::get<0>(GetParam()))}; + const int &gold = ::nvcv::test::ParamValue(std::get<1>(GetParam())); auto info = nvcv::TensorLayoutInfoImage::Create(input); ASSERT_TRUE(info); @@ -339,8 +339,8 @@ NVCV_TEST_SUITE_P(TensorLayoutInfoImage_IsChannelLast_ExecTests, TEST_P(TensorLayoutInfoImage_IsChannelLast_ExecTests, works) { - const nvcv::TensorLayout input{std::get<0>(GetParam())}; - const int &gold = std::get<1>(GetParam()); + const nvcv::TensorLayout input{::nvcv::test::ParamValue(std::get<0>(GetParam()))}; + const int &gold = ::nvcv::test::ParamValue(std::get<1>(GetParam())); auto info = nvcv::TensorLayoutInfoImage::Create(input); ASSERT_TRUE(info); diff --git a/tests/nvcv_types/system/TestTensorShapeInfo.cpp b/tests/nvcv_types/system/TestTensorShapeInfo.cpp index 80172502a..488cb1ee6 100644 --- a/tests/nvcv_types/system/TestTensorShapeInfo.cpp +++ b/tests/nvcv_types/system/TestTensorShapeInfo.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -42,8 +42,8 @@ NVCV_TEST_SUITE_P(TensorShapeInfo_NumBatch_ExecTests, TEST_P(TensorShapeInfo_NumBatch_ExecTests, works) { - const nvcv::TensorShape input{std::get<0>(GetParam())}; - const int &gold = std::get<1>(GetParam()); + const nvcv::TensorShape input{::nvcv::test::ParamValue(std::get<0>(GetParam()))}; + const int &gold = ::nvcv::test::ParamValue(std::get<1>(GetParam())); auto info = nvcv::TensorShapeInfo::Create(input); ASSERT_TRUE(info); @@ -69,8 +69,8 @@ NVCV_TEST_SUITE_P(TensorShapeInfoImage_NumChannels_ExecTest, TEST_P(TensorShapeInfoImage_NumChannels_ExecTest, works) { - const nvcv::TensorShape input{std::get<0>(GetParam())}; - const int &gold = std::get<1>(GetParam()); + const nvcv::TensorShape input{::nvcv::test::ParamValue(std::get<0>(GetParam()))}; + const int &gold = ::nvcv::test::ParamValue(std::get<1>(GetParam())); auto info = nvcv::TensorShapeInfoImage::Create(input); ASSERT_TRUE(info); @@ -95,8 +95,8 @@ NVCV_TEST_SUITE_P(TensorShapeInfoImage_NumCols_ExecTest, TEST_P(TensorShapeInfoImage_NumCols_ExecTest, works) { - const nvcv::TensorShape input{std::get<0>(GetParam())}; - const int &gold = std::get<1>(GetParam()); + const nvcv::TensorShape input{::nvcv::test::ParamValue(std::get<0>(GetParam()))}; + const int &gold = ::nvcv::test::ParamValue(std::get<1>(GetParam())); auto info = nvcv::TensorShapeInfoImage::Create(input); ASSERT_TRUE(info); @@ -124,8 +124,8 @@ NVCV_TEST_SUITE_P(TensorShapeInfoImage_NumRows_ExecTest, TEST_P(TensorShapeInfoImage_NumRows_ExecTest, works) { - const nvcv::TensorShape input{std::get<0>(GetParam())}; - const int &gold = std::get<1>(GetParam()); + const nvcv::TensorShape input{::nvcv::test::ParamValue(std::get<0>(GetParam()))}; + const int &gold = ::nvcv::test::ParamValue(std::get<1>(GetParam())); auto info = nvcv::TensorShapeInfoImage::Create(input); ASSERT_TRUE(info); @@ -152,8 +152,8 @@ NVCV_TEST_SUITE_P(TensorShapeInfoImage_Size_ExecTest, TEST_P(TensorShapeInfoImage_Size_ExecTest, works) { - const nvcv::TensorShape input{std::get<0>(GetParam())}; - const nvcv::Size2D &gold = std::get<1>(GetParam()); + const nvcv::TensorShape input{::nvcv::test::ParamValue(std::get<0>(GetParam()))}; + const nvcv::Size2D &gold = ::nvcv::test::ParamValue(std::get<1>(GetParam())); auto info = nvcv::TensorShapeInfoImage::Create(input); ASSERT_TRUE(info); @@ -209,8 +209,8 @@ NVCV_TEST_SUITE_P(TensorShapeInfoImagePlanar_IsCompatible_ExecTest, TEST_P(TensorShapeInfoImagePlanar_IsCompatible_ExecTest, works) { - const nvcv::TensorShape input{std::get<0>(GetParam())}; - const bool &gold = std::get<1>(GetParam()); + const nvcv::TensorShape input{::nvcv::test::ParamValue(std::get<0>(GetParam()))}; + const bool &gold = ::nvcv::test::ParamValue(std::get<1>(GetParam())); EXPECT_EQ(gold, nvcv::TensorShapeInfoImagePlanar::IsCompatible(input)); } @@ -243,8 +243,8 @@ NVCV_TEST_SUITE_P(TensorShapeInfoImagePlanar_NumPlanes_ExecTest, TEST_P(TensorShapeInfoImagePlanar_NumPlanes_ExecTest, works) { - const nvcv::TensorShape input{std::get<0>(GetParam())}; - const int &gold = std::get<1>(GetParam()); + const nvcv::TensorShape input{::nvcv::test::ParamValue(std::get<0>(GetParam()))}; + const int &gold = ::nvcv::test::ParamValue(std::get<1>(GetParam())); auto info = nvcv::TensorShapeInfoImagePlanar::Create(input); ASSERT_TRUE(info); diff --git a/tests/nvcv_types/system/TestVersion.cpp b/tests/nvcv_types/system/TestVersion.cpp index 9762b4a89..f3465b2dd 100644 --- a/tests/nvcv_types/system/TestVersion.cpp +++ b/tests/nvcv_types/system/TestVersion.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -84,51 +84,3 @@ TEST(VersionTests, api_version_macro) { EXPECT_EQ(NVCV_MAKE_VERSION(NVCV_VERSION_MAJOR, NVCV_VERSION_MINOR), NVCV_VERSION_API); } - -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wredundant-decls" - -#undef NVCV_VERSION_API -#define NVCV_VERSION_API NVCV_MAKE_VERSION(1, 0) - -#pragma GCC diagnostic pop - -TEST(VersionTests, api_version_macro_redefinition) -{ - EXPECT_EQ(NVCV_MAKE_VERSION(1, 0), NVCV_VERSION_API); -} - -TEST(VersionTests, api_version_at_least) -{ - EXPECT_TRUE(NVCV_VERSION_API_AT_LEAST(1, 0)); - EXPECT_TRUE(NVCV_VERSION_API_AT_LEAST(0, 99)); - - EXPECT_FALSE(NVCV_VERSION_API_AT_LEAST(1, 2)); - EXPECT_FALSE(NVCV_VERSION_API_AT_LEAST(2, 0)); -} - -TEST(VersionTests, api_version_at_most) -{ - EXPECT_TRUE(NVCV_VERSION_API_AT_MOST(1, 0)); - EXPECT_FALSE(NVCV_VERSION_API_AT_MOST(0, 99)); - EXPECT_TRUE(NVCV_VERSION_API_AT_MOST(1, 1)); - EXPECT_TRUE(NVCV_VERSION_API_AT_MOST(2, 0)); -} - -TEST(VersionTests, api_version_in_range) -{ - EXPECT_TRUE(NVCV_VERSION_API_IN_RANGE(1, 0, 1, 0)); - EXPECT_TRUE(NVCV_VERSION_API_IN_RANGE(0, 99, 1, 0)); - EXPECT_TRUE(NVCV_VERSION_API_IN_RANGE(0, 99, 1, 1)); - EXPECT_TRUE(NVCV_VERSION_API_IN_RANGE(0, 99, 2, 0)); - - EXPECT_FALSE(NVCV_VERSION_API_IN_RANGE(0, 98, 0, 99)); - EXPECT_FALSE(NVCV_VERSION_API_IN_RANGE(1, 1, 1, 2)); -} - -TEST(VersionTests, api_version_is) -{ - EXPECT_TRUE(NVCV_VERSION_API_IS(1, 0)); - EXPECT_FALSE(NVCV_VERSION_API_IS(1, 1)); - EXPECT_FALSE(NVCV_VERSION_API_IS(0, 99)); -} diff --git a/tests/nvcv_types/system/TestVersionApiOverride.cpp b/tests/nvcv_types/system/TestVersionApiOverride.cpp new file mode 100644 index 000000000..c50de6cae --- /dev/null +++ b/tests/nvcv_types/system/TestVersionApiOverride.cpp @@ -0,0 +1,60 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "Definitions.hpp" + +#include + +TEST(VersionTests, api_version_macro_override) +{ + EXPECT_EQ(NVCV_MAKE_VERSION(0, 16), NVCV_VERSION_API); +} + +TEST(VersionTests, api_version_at_least) +{ + EXPECT_TRUE(NVCV_VERSION_API_AT_LEAST(0, 16)); + EXPECT_TRUE(NVCV_VERSION_API_AT_LEAST(0, 15)); + + EXPECT_FALSE(NVCV_VERSION_API_AT_LEAST(0, 17)); + EXPECT_FALSE(NVCV_VERSION_API_AT_LEAST(1, 0)); +} + +TEST(VersionTests, api_version_at_most) +{ + EXPECT_TRUE(NVCV_VERSION_API_AT_MOST(0, 16)); + EXPECT_FALSE(NVCV_VERSION_API_AT_MOST(0, 15)); + EXPECT_TRUE(NVCV_VERSION_API_AT_MOST(0, 17)); + EXPECT_TRUE(NVCV_VERSION_API_AT_MOST(1, 0)); +} + +TEST(VersionTests, api_version_in_range) +{ + EXPECT_TRUE(NVCV_VERSION_API_IN_RANGE(0, 16, 0, 16)); + EXPECT_TRUE(NVCV_VERSION_API_IN_RANGE(0, 15, 0, 16)); + EXPECT_TRUE(NVCV_VERSION_API_IN_RANGE(0, 15, 0, 17)); + EXPECT_TRUE(NVCV_VERSION_API_IN_RANGE(0, 15, 1, 0)); + + EXPECT_FALSE(NVCV_VERSION_API_IN_RANGE(0, 14, 0, 15)); + EXPECT_FALSE(NVCV_VERSION_API_IN_RANGE(0, 17, 1, 0)); +} + +TEST(VersionTests, api_version_is) +{ + EXPECT_TRUE(NVCV_VERSION_API_IS(0, 16)); + EXPECT_FALSE(NVCV_VERSION_API_IS(0, 17)); + EXPECT_FALSE(NVCV_VERSION_API_IS(0, 15)); +} diff --git a/tests/nvcv_types/unit/TestArray.cpp b/tests/nvcv_types/unit/TestArray.cpp index 6a30bc129..529bf3144 100644 --- a/tests/nvcv_types/unit/TestArray.cpp +++ b/tests/nvcv_types/unit/TestArray.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,10 +22,6 @@ #include #include -void arrayDataCleanUpFunc(void *ctx, const NVCVArrayData *data); - -void arrayDataCleanUpFunc(void *ctx, const NVCVArrayData *data) {} - TEST(ArrayTests, rank) { NVCVArrayRequirements req; @@ -47,6 +43,6 @@ TEST(ArrayTests, warp_rank) nvcv::priv::Array array(req, alloc, NVCV_RESOURCE_MEM_HOST); array.exportData(data); - nvcv::priv::ArrayWrapData arrayData(data, &arrayDataCleanUpFunc, nullptr); + nvcv::priv::ArrayWrapData arrayData(data, nullptr, nullptr); EXPECT_EQ(arrayData.rank(), 1); } diff --git a/tests/nvcv_types/unit/TestCallback.cpp b/tests/nvcv_types/unit/TestCallback.cpp index dd8055a9c..514b61ac7 100644 --- a/tests/nvcv_types/unit/TestCallback.cpp +++ b/tests/nvcv_types/unit/TestCallback.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,8 +18,10 @@ #include +#include #include #include +#include #include #include @@ -27,17 +29,17 @@ namespace { float foo(int x, float y) { - return x + y; + return static_cast(x) + y; } float bar(int x, float y) { - return x - y; + return static_cast(x) - y; } struct EmptyCallable { - const char *operator()(const char *x) + const char *operator()(const char *x) const { return x + 1; }; @@ -47,7 +49,7 @@ struct SimpleCallable { int offset = 2; - const char *operator()(const char *x) + const char *operator()(const char *x) const { return x + offset; }; @@ -66,7 +68,7 @@ struct InstanceCounter num_instances++; } - InstanceCounter(InstanceCounter &&) + InstanceCounter(InstanceCounter &&) noexcept { num_instances++; } @@ -84,14 +86,27 @@ std::atomic_int InstanceCounter::num_instances{0}; struct ComplexCallable : InstanceCounter { + ComplexCallable() = default; + ComplexCallable(const ComplexCallable &) = default; + ComplexCallable(ComplexCallable &&) noexcept = default; + + ComplexCallable &operator=(const ComplexCallable &) = default; + ComplexCallable &operator=(ComplexCallable &&) noexcept = default; + std::string prefix; - std::string operator()(const char *x) + std::string operator()(const char *x) const { return prefix + x; } }; +class CallbackTestError : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + } // namespace using nvcv::Callback; @@ -116,7 +131,7 @@ static_assert(Callback::requiresCleanup cb = foo; + Callback cb{foo}; EXPECT_EQ(cb(42, 1.5f), 43.5f); EXPECT_FALSE(cb.requiresCleanup()); EXPECT_EQ(cb.targetHandle(), reinterpret_cast(&foo)); @@ -134,7 +149,7 @@ TEST(CallbackTest, FromFunction) TEST(CallbackTest, FromEmptyCallable) { // test construction - Callback cb = EmptyCallable(); + Callback cb{EmptyCallable()}; const char *input = "test"; EXPECT_EQ(cb(input), input + 1); EXPECT_FALSE(cb.requiresCleanup()); @@ -148,7 +163,7 @@ TEST(CallbackTest, FromSimpleCallable) SimpleCallable sc; sc.offset = 3; - Callback cb = sc; + Callback cb{sc}; const char *input = "test"; EXPECT_EQ(cb(input), input + 3); @@ -165,7 +180,7 @@ TEST(CallbackTest, ComplexObjectLifecycle) cc.prefix = "pre"; EXPECT_EQ(cc.num_instances, 1); - Callback cb = cc; + Callback cb{cc}; EXPECT_EQ(cc.num_instances, 2); const char *input = "text"; EXPECT_EQ(cb(input), "pretext"); @@ -177,10 +192,10 @@ TEST(CallbackTest, ComplexObjectLifecycle) Callback cb2 = std::move(cb); // moved out - check it - EXPECT_FALSE(cb.requiresCleanup()); - EXPECT_EQ(cb.targetFunc(), nullptr); - EXPECT_EQ(cb.targetHandle(), nullptr); - EXPECT_EQ(cb.cleanupFunc(), nullptr); + EXPECT_FALSE(cb.requiresCleanup()); // NOSONAR: this test verifies moved-from state. + EXPECT_EQ(cb.targetFunc(), nullptr); // NOSONAR: this test verifies moved-from state. + EXPECT_EQ(cb.targetHandle(), nullptr); // NOSONAR: this test verifies moved-from state. + EXPECT_EQ(cb.cleanupFunc(), nullptr); // NOSONAR: this test verifies moved-from state. EXPECT_EQ(cc.num_instances, 2); EXPECT_TRUE(cb2.requiresCleanup()); @@ -204,7 +219,7 @@ TEST(CallbackTest, ComplexObjectLifecycle) TEST(CallbackTest, FromStdFunction) { // compatible, but not exact - the return type differs - Callback cb = std::function(&foo); + Callback cb{std::function(&foo)}; EXPECT_FALSE(cb.requiresCleanup()); EXPECT_EQ(cb(1, 2), 3); @@ -219,10 +234,10 @@ TEST(CallbackTest, FromStdFunction) TEST(CallbackTest, FromLambda) { - Callback cb = [](int a, int b) - { - return a + b; - }; + Callback cb{[](int a, int b) + { + return static_cast(a + b); + }}; // the lambda has no closure and therefore is empty - no cleanup required EXPECT_FALSE(cb.requiresCleanup()); @@ -230,17 +245,17 @@ TEST(CallbackTest, FromLambda) int z = 42; - cb = [=](float x, float y) + cb = [z](float x, float y) { - return x * y + z; + return x * y + static_cast(z); }; // The lambda capture fits inside one pointer - no cleanup required EXPECT_FALSE(cb.requiresCleanup()); EXPECT_EQ(cb(2, 3), 48); - int zzz[7] = {1, 2, 3, 4, 5, 6, 7}; + std::array zzz = {1, 2, 3, 4, 5, 6, 7}; - cb = [=](int x, int y) + cb = [zzz](int x, int y) { return x * y + zzz[6]; }; @@ -257,11 +272,11 @@ TEST(CallbackTest, FromLambda) namespace { -typedef struct SomeCStruct +struct CStruct { void *mem; int len; -} CStruct; +}; struct StructWrapper { @@ -281,14 +296,14 @@ using CppCleanup = void(const StructWrapper &wrapper); struct TranslateCleanup { template - int operator()(CppCallable &&fun, CStruct *data) + int operator()(CppCallable &&fun, CStruct *data) const { try { fun(StructWrapper(*data)); return 0; } - catch (...) + catch (...) // NOSONAR: callback wrapper converts any thrown value to a C status. { return -1; } @@ -306,9 +321,9 @@ struct CContainer } cleanup; }; -CContainer *CreateCContainer(const StructWrapper &wrapper, Callback &&cb) +CContainer *CreateCContainer(const StructWrapper &wrapper, const Callback &cb) { - auto c = std::make_unique(CContainer{}); + auto c = std::make_unique(); c->data = wrapper.m_data; if (cb.requiresCleanup()) throw std::invalid_argument("The cleanup function must not require extra cleanup."); @@ -321,38 +336,40 @@ void DestroyCContainer(CContainer *c) { if (!c) return; - if (c->cleanup.func) - c->cleanup.func(c->cleanup.ctx, &c->data); - delete c; + + std::unique_ptr container(c); + if (container->cleanup.func) + container->cleanup.func(container->cleanup.ctx, &container->data); } } // namespace TEST(CallbackTest, TestCCleanup) { - static const char payload[] = "test"; + static std::array payload = {'t', 'e', 's', 't', '\0'}; StructWrapper wrapper; - wrapper.m_data.mem = const_cast(payload); - wrapper.m_data.len = sizeof(payload); + wrapper.m_data.mem = payload.data(); + wrapper.m_data.len = payload.size(); bool cleanup_called = false; - auto good_cleanup = [&](const StructWrapper &w) + auto good_cleanup = [&cleanup_called](const StructWrapper &w) { - EXPECT_EQ(w.m_data.mem, payload); - EXPECT_EQ(w.m_data.len, sizeof(payload)); + EXPECT_EQ(w.m_data.mem, payload.data()); + EXPECT_EQ(w.m_data.len, payload.size()); cleanup_called = true; }; - CContainer *cc = CreateCContainer(wrapper, good_cleanup); + CContainer *cc = CreateCContainer(wrapper, Callback{good_cleanup}); DestroyCContainer(cc); EXPECT_TRUE(cleanup_called); std::function bad_cleanup = good_cleanup; - EXPECT_THROW(CreateCContainer(wrapper, bad_cleanup), std::invalid_argument); + EXPECT_THROW(CreateCContainer(wrapper, Callback{bad_cleanup}), + std::invalid_argument); } ////////////////////////////////////////////////////////////////////////////// @@ -364,7 +381,7 @@ TEST(CleanupCallbackTest, TestSelfCleanup) cc.prefix = "pre"; EXPECT_EQ(cc.num_instances, 1); - nvcv::CleanupCallback cb = cc; + nvcv::CleanupCallback cb{cc}; EXPECT_EQ(cc.num_instances, 2); const char *input = "text"; EXPECT_TRUE(cb.requiresCleanup()); @@ -384,13 +401,13 @@ TEST(CleanupCallbackTest, LambdaWithDestructorCalledFromC) ComplexCallable cc; cc.prefix = "pre"; - auto lambda = [=](const std::string &arg) mutable + auto lambda = [cc](const std::string &arg) mutable { return cc(arg.c_str()); }; EXPECT_EQ(cc.num_instances, 2); - nvcv::CleanupCallback cb = lambda; + nvcv::CleanupCallback cb{lambda}; EXPECT_EQ(cc.num_instances, 3); const char *input = "text"; EXPECT_TRUE(cb.requiresCleanup()); @@ -413,7 +430,7 @@ TEST(CleanupCallbackTest, LambdaWithDestructorNotCalled) EXPECT_EQ(ComplexCallable::num_instances, 1); - auto lambda = [=](const std::string &arg) mutable -> std::string + auto lambda = [cc](const std::string &arg) mutable { return cc.prefix + arg; }; @@ -421,7 +438,7 @@ TEST(CleanupCallbackTest, LambdaWithDestructorNotCalled) EXPECT_EQ(ComplexCallable::num_instances, 2); { - nvcv::CleanupCallback cb = std::move(lambda); + nvcv::CleanupCallback cb{std::move(lambda)}; EXPECT_EQ(ComplexCallable::num_instances, 3); } EXPECT_EQ(ComplexCallable::num_instances, 2) << "Callback context leaked"; @@ -433,13 +450,13 @@ TEST(CleanupCallbackTest, ThrowInC) ComplexCallable cc; cc.prefix = "defeat short string optimization"; - auto lambda = [=](const std::string &arg) mutable -> std::string + auto lambda = [cc](const std::string &) mutable -> std::string { - throw std::runtime_error(cc.prefix); + throw CallbackTestError(cc.prefix); }; EXPECT_EQ(cc.num_instances, 2); - nvcv::CleanupCallback cb = lambda; + nvcv::CleanupCallback cb{lambda}; EXPECT_EQ(cc.num_instances, 3); const char *input = "text"; EXPECT_TRUE(cb.requiresCleanup()); diff --git a/tests/nvcv_types/unit/TestCheckError.cpp b/tests/nvcv_types/unit/TestCheckError.cpp index 77f9c0357..e25f92116 100644 --- a/tests/nvcv_types/unit/TestCheckError.cpp +++ b/tests/nvcv_types/unit/TestCheckError.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -93,10 +93,9 @@ TEST_P(CheckStatusMacroTests, throw_return_something_else) TEST(CheckStatusMacroTests, throw_with_extra_string) { const cudaError_t cudaErrCode = cudaErrorTextureFetchFailed; - const char *fmt = " Extra String: %s"; const char *extraString = "abc\n\0"; - NVCV_EXPECT_STATUS(NVCV_ERROR_INTERNAL, NVCV_CHECK_THROW(cudaErrCode, fmt, extraString)); + NVCV_EXPECT_STATUS(NVCV_ERROR_INTERNAL, NVCV_CHECK_THROW(cudaErrCode, " Extra String: %s", extraString)); } TEST(CheckErrorTest, GetFunctionName) diff --git a/tests/nvcv_types/unit/TestColorSpec.cpp b/tests/nvcv_types/unit/TestColorSpec.cpp index ad12029d9..4cc0e3a7a 100644 --- a/tests/nvcv_types/unit/TestColorSpec.cpp +++ b/tests/nvcv_types/unit/TestColorSpec.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,6 +18,8 @@ #include "Definitions.hpp" #include "nvcv/src/priv/ColorSpec.hpp" +#include + TEST(ColorSpecTests, whitePoint) { EXPECT_EQ(nvcv::priv::ColorSpec{NVCV_COLOR_SPEC_BT601}.whitePoint(), NVCV_WHITE_POINT_D65); @@ -32,9 +34,18 @@ TEST(ColorSpecTests, GetName) EXPECT_STREQ("NVCVColorSpace(-1)", nvcv::priv::GetName(static_cast(-1))); } +TEST(ColorSpecTests, GetColorSpecName) +{ + EXPECT_STREQ("NVCV_COLOR_SPEC_BT601", nvcv::priv::GetName(NVCV_COLOR_SPEC_BT601)); + EXPECT_STREQ("NVCVColorSpec(SPACE_DCIP3,ENC_BT2020c,XFER_sYCC,RANGE_LIMITED,LOC_ODD,LOC_CENTER)", + nvcv::priv::GetName(NVCV_MAKE_COLOR_SPEC(NVCV_COLOR_SPACE_DCIP3, NVCV_YCbCr_ENC_BT2020c, + NVCV_COLOR_XFER_sYCC, NVCV_COLOR_RANGE_LIMITED, + NVCV_CHROMA_LOC_ODD, NVCV_CHROMA_LOC_CENTER))); +} + TEST(ColorSpecTests, operator_insertion_NVCVWhitePoint) { - auto testOperatorInsertion = [](std::string expectedStr, NVCVWhitePoint whitePoint) -> void + auto testOperatorInsertion = [](const std::string &expectedStr, NVCVWhitePoint whitePoint) { std::ostringstream ss; ss << whitePoint; @@ -49,7 +60,7 @@ TEST(ColorSpecTests, operator_insertion_NVCVWhitePoint) TEST(ColorSpecTests, operator_insertion_NVCVColorSpec) { - auto testOperatorInsertion = [](std::string expectedStr, NVCVColorSpec colorSpec) -> void + auto testOperatorInsertion = [](const std::string &expectedStr, NVCVColorSpec colorSpec) { std::ostringstream ss; ss << colorSpec; @@ -67,3 +78,21 @@ TEST(ColorSpecTests, StrNVCVColorSpec) EXPECT_EQ("NVCV_COLOR_SPEC_UNDEFINED", StrNVCVColorSpec(NVCV_COLOR_SPEC_UNDEFINED)); EXPECT_EQ("NVCV_COLOR_SPEC_BT601", StrNVCVColorSpec(NVCV_COLOR_SPEC_BT601)); } + +// Regression: operator<<(nvcv::ColorSpace) previously dispatched to +// nvcvColorSpecGetName with a cast through NVCVColorSpec (a wider packed +// type), producing garbage names for the four small ColorSpace values. +TEST(ColorSpecTests, operator_insertion_nvcv_ColorSpace) +{ + auto check = [](const std::string &expected, nvcv::ColorSpace cspace) + { + std::ostringstream ss; + ss << cspace; + EXPECT_EQ(expected, ss.str()); + }; + + check("NVCV_COLOR_SPACE_BT601", nvcv::ColorSpace::BT601); + check("NVCV_COLOR_SPACE_BT709", nvcv::ColorSpace::BT709); + check("NVCV_COLOR_SPACE_BT2020", nvcv::ColorSpace::BT2020); + check("NVCV_COLOR_SPACE_DCIP3", nvcv::ColorSpace::DCIP3); +} diff --git a/tests/nvcv_types/unit/TestCompat.cpp b/tests/nvcv_types/unit/TestCompat.cpp index 363b707a4..9a50afb24 100644 --- a/tests/nvcv_types/unit/TestCompat.cpp +++ b/tests/nvcv_types/unit/TestCompat.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,16 +20,23 @@ #include #if __has_include() # include -# define HAS_SYS_RANDOM_H 1 #endif #include +#include + namespace test = nvcv::test; namespace t = ::testing; -#if HAS_SYS_RANDOM_H +#if __has_include() + +static constexpr size_t kBufSize = 256; -static char g_buf[256] = {}; +static char *Buf() +{ + static std::array buf = {}; + return buf.data(); +} class CompatGetRandomParamTest : public t::TestWithParam< @@ -45,26 +52,26 @@ NVCV_INSTANTIATE_TEST_SUITE_P(Negative, CompatGetRandomParamTest, { NULL, 1, 0, -1, EFAULT}, { (void *)0x666, 1, 0, -1, EFAULT}, { (void *)0x666, -1, 0, -1, EFAULT}, - { g_buf, 1, 0x666, -1, EINVAL}, + { Buf(), 1, 0x666, -1, EINVAL}, }); NVCV_INSTANTIATE_TEST_SUITE_P(Positive, CompatGetRandomParamTest, { // buffer, length, flags, gold_retval, gold_errno - {(void *)&g_buf, sizeof(g_buf), 0, sizeof(g_buf), 0 /*ignored*/}, - {(void *)&g_buf, 1, 0, 1, 0 /*ignored*/}, - {(void *)&g_buf, 0, 0, 0, 0 /*ignored*/}, + { Buf(), kBufSize, 0, kBufSize, 0 /*ignored*/}, + { Buf(), 1, 0, 1, 0 /*ignored*/}, + { Buf(), 0, 0, 0, 0 /*ignored*/}, }); // clang-format on TEST_P(CompatGetRandomParamTest, test) { - void *buf = std::get<0>(GetParam()); - size_t length = std::get<1>(GetParam()); - unsigned int flags = std::get<2>(GetParam()); - int gold_retval = std::get<3>(GetParam()); - int gold_errno = std::get<4>(GetParam()); + void *buf = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + size_t length = ::nvcv::test::ParamValue(std::get<1>(GetParam())); + unsigned int flags = ::nvcv::test::ParamValue(std::get<2>(GetParam())); + auto gold_retval = ::nvcv::test::ParamValue(std::get<3>(GetParam())); + auto gold_errno = ::nvcv::test::ParamValue(std::get<4>(GetParam())); ssize_t ret = Compat_getrandom(buf, length, flags); EXPECT_EQ(gold_retval, ret); @@ -92,17 +99,17 @@ NVCV_INSTANTIATE_TEST_SUITE_P(Flags, CompatGetRandomExecTest, TEST_P(CompatGetRandomExecTest, works) { - unsigned int flags = GetParam(); + unsigned int flags = ::nvcv::test::ParamValue(GetParam()); // when using urandom, it's guaranteed that it'll return at least 256 bytes // so let's use 256. - char buf1[256 + 1] = {}; - ssize_t n = Compat_getrandom(buf1, 256, flags); + std::array buf1 = {}; + ssize_t n = Compat_getrandom(buf1.data(), 256, flags); ASSERT_EQ(256, n); ASSERT_EQ(0, buf1[256]); - char buf2[256 + 1] = {}; - n = Compat_getrandom(buf2, 256, flags); + std::array buf2 = {}; + n = Compat_getrandom(buf2.data(), 256, flags); ASSERT_EQ(256, n); ASSERT_EQ(0, buf2[256]); ASSERT_THAT(buf1, t::Not(t::ElementsAreArray(buf2))); @@ -120,25 +127,25 @@ NVCV_INSTANTIATE_TEST_SUITE_P(Negative, CompatGetEntropyParamTest, // buffer, length, gold_retval, gold_errno { NULL, 1, -1, EFAULT}, { (void *)0x666, 1, -1, EFAULT}, - { g_buf, 257, -1, EIO}, + { Buf(), kBufSize + 1, -1, EIO}, }); NVCV_INSTANTIATE_TEST_SUITE_P(Positive, CompatGetEntropyParamTest, { // buffer, length, gold_retval, gold_errno - {(void *)&g_buf, sizeof(g_buf), 0, 0 /*ignored*/}, - {(void *)&g_buf, 1, 0, 0 /*ignored*/}, - {(void *)&g_buf, 0, 0, 0 /*ignored*/}, + { Buf(), kBufSize, 0, 0 /*ignored*/}, + { Buf(), 1, 0, 0 /*ignored*/}, + { Buf(), 0, 0, 0 /*ignored*/}, }); // clang-format on TEST_P(CompatGetEntropyParamTest, test) { - void *buf = std::get<0>(GetParam()); - size_t length = std::get<1>(GetParam()); - int gold_retval = std::get<2>(GetParam()); - int gold_errno = std::get<3>(GetParam()); + void *buf = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + size_t length = ::nvcv::test::ParamValue(std::get<1>(GetParam())); + int gold_retval = ::nvcv::test::ParamValue(std::get<2>(GetParam())); + int gold_errno = ::nvcv::test::ParamValue(std::get<3>(GetParam())); ssize_t ret = Compat_getentropy(buf, length); EXPECT_EQ(gold_retval, ret); @@ -150,15 +157,15 @@ TEST_P(CompatGetEntropyParamTest, test) TEST(CompatGetEntropyExecTest, works) { - char buf1[256] = {}; - ASSERT_EQ(0, Compat_getentropy(buf1, sizeof(buf1) - 1)); - ASSERT_EQ(0, buf1[sizeof(buf1) - 1]); + std::array buf1 = {}; + ASSERT_EQ(0, Compat_getentropy(buf1.data(), buf1.size() - 1)); + ASSERT_EQ(0, buf1[buf1.size() - 1]); - char buf2[256] = {}; - ASSERT_EQ(0, Compat_getentropy(buf2, sizeof(buf2) - 1)); - ASSERT_EQ(0, buf1[sizeof(buf2) - 1]); + std::array buf2 = {}; + ASSERT_EQ(0, Compat_getentropy(buf2.data(), buf2.size() - 1)); + ASSERT_EQ(0, buf1[buf2.size() - 1]); ASSERT_THAT(buf1, t::Not(t::ElementsAreArray(buf2))); } -#endif +#endif // __has_include() diff --git a/tests/nvcv_types/unit/TestDataLayout.cpp b/tests/nvcv_types/unit/TestDataLayout.cpp index 3011f5037..29ea5a19b 100644 --- a/tests/nvcv_types/unit/TestDataLayout.cpp +++ b/tests/nvcv_types/unit/TestDataLayout.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -30,15 +30,16 @@ TEST(DataLayoutTest, GetBlockHeightLog2) TEST(DataLayoutTest, FlipByteOrder) { - auto makeSwizzle = [](NVCVChannel x, NVCVChannel y, NVCVChannel z, NVCVChannel w) -> NVCVSwizzle + auto makeSwizzle = [](NVCVChannel x, NVCVChannel y, NVCVChannel z, NVCVChannel w) { NVCVSwizzle res; EXPECT_EQ(NVCV_SUCCESS, nvcvMakeSwizzle(&res, x, y, z, w)); return res; }; - auto testFlipByteOrder = [&](NVCVChannel x, NVCVChannel y, NVCVChannel z, NVCVChannel w, int off, int len, - NVCVChannel goldX, NVCVChannel goldY, NVCVChannel goldZ, NVCVChannel goldW) -> void + auto testFlipByteOrder + = [&makeSwizzle](NVCVChannel x, NVCVChannel y, NVCVChannel z, NVCVChannel w, int off, int len, + NVCVChannel goldX, NVCVChannel goldY, NVCVChannel goldZ, NVCVChannel goldW) { NVCVSwizzle initialSwizzle = makeSwizzle(x, y, z, w); NVCVSwizzle res = nvcv::priv::FlipByteOrder(initialSwizzle, off, len); @@ -92,7 +93,7 @@ TEST(DataLayoutTest, IsSubWord) TEST(ByteOrderTests, get_name_operator) { - auto testOperatorInsertion = [](std::string expectedStr, NVCVByteOrder order) -> void + auto testOperatorInsertion = [](const std::string &expectedStr, NVCVByteOrder order) { std::ostringstream ss; ss << order; @@ -107,7 +108,7 @@ TEST(ByteOrderTests, get_name_operator) TEST(AlphaTypeTests, get_name_operator) { - auto testOperatorInsertion = [](std::string expectedStr, NVCVAlphaType alphaType) -> void + auto testOperatorInsertion = [](const std::string &expectedStr, NVCVAlphaType alphaType) { std::ostringstream ss; ss << alphaType; @@ -123,7 +124,7 @@ TEST(AlphaTypeTests, get_name_operator) TEST(ExtraChannelTests, get_name_operator) { - auto testOperatorInsertion = [](std::string expectedStr, NVCVExtraChannel extraChannel) -> void + auto testOperatorInsertion = [](const std::string &expectedStr, NVCVExtraChannel extraChannel) { std::ostringstream ss; ss << extraChannel; diff --git a/tests/nvcv_types/unit/TestExceptions.cpp b/tests/nvcv_types/unit/TestExceptions.cpp index 59a1c3fb4..90cab7453 100644 --- a/tests/nvcv_types/unit/TestExceptions.cpp +++ b/tests/nvcv_types/unit/TestExceptions.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,6 +20,18 @@ #include +#include +#include +#include + +namespace { + +class ProtectCallTestException : public std::exception +{ +}; + +} // namespace + #if !NVCV_DEBUG TEST(ExceptionTest, priv_protect_call_public_exception) { @@ -40,6 +52,44 @@ TEST(ExceptionTest, exception_what) } } +TEST(ExceptionTest, exception_format_numeric_modifiers) +{ + nvcv::priv::Exception e(NVCV_ERROR_DEVICE, "test error %ld %.2f %x %%", 123L, 4.5, 255U); + + EXPECT_STREQ("NVCV_ERROR_DEVICE: test error 123 4.50 ff %", e.what()); + EXPECT_STREQ("test error 123 4.50 ff %", e.msg()); +} + +TEST(ExceptionTest, exception_stream_append) +{ + auto e = nvcv::priv::Exception(NVCV_ERROR_DEVICE) << "streamed " << 7; + + EXPECT_STREQ("NVCV_ERROR_DEVICE: streamed 7", e.what()); + EXPECT_STREQ("streamed 7", e.msg()); +} + +TEST(ExceptionTest, exception_null_message) +{ + nvcv::priv::Exception e(NVCV_ERROR_DEVICE, static_cast(nullptr)); + + EXPECT_STREQ("NVCV_ERROR_DEVICE: ", e.what()); +} + +TEST(ExceptionTest, exception_copy_and_move_assignment) +{ + nvcv::priv::Exception source(NVCV_ERROR_DEVICE, "source"); + nvcv::priv::Exception copy(NVCV_ERROR_INTERNAL, "copy"); + nvcv::priv::Exception moved(NVCV_ERROR_INTERNAL, "moved"); + + copy = source; + EXPECT_EQ(NVCV_ERROR_DEVICE, copy.code()); + EXPECT_STREQ("source", copy.msg()); + + moved = std::move(source); + EXPECT_EQ(NVCV_ERROR_DEVICE, moved.code()); + EXPECT_STREQ("source", moved.msg()); +} + TEST(ExceptionTest, priv_protect_call_invalid_argument) { NVCVStatus status = nvcv::priv::ProtectCall([] { throw std::invalid_argument(""); }); @@ -52,9 +102,19 @@ TEST(ExceptionTest, priv_protect_call_bad_alloc) EXPECT_EQ(NVCV_ERROR_OUT_OF_MEMORY, status); } +TEST(ExceptionTest, priv_protect_call_standard_logic_and_runtime_errors) +{ + EXPECT_EQ(NVCV_ERROR_INTERNAL, nvcv::priv::ProtectCall([] { throw std::domain_error("domain"); })); + EXPECT_EQ(NVCV_ERROR_INTERNAL, nvcv::priv::ProtectCall([] { throw std::length_error("length"); })); + EXPECT_EQ(NVCV_ERROR_INTERNAL, nvcv::priv::ProtectCall([] { throw std::out_of_range("out of range"); })); + EXPECT_EQ(NVCV_ERROR_INTERNAL, nvcv::priv::ProtectCall([] { throw std::range_error("range"); })); + EXPECT_EQ(NVCV_ERROR_INTERNAL, nvcv::priv::ProtectCall([] { throw std::overflow_error("overflow"); })); + EXPECT_EQ(NVCV_ERROR_INTERNAL, nvcv::priv::ProtectCall([] { throw std::underflow_error("underflow"); })); +} + TEST(ExceptionTest, priv_protect_call_std_exception) { - NVCVStatus status = nvcv::priv::ProtectCall([] { throw std::exception(); }); + NVCVStatus status = nvcv::priv::ProtectCall([] { throw ProtectCallTestException(); }); EXPECT_EQ(NVCV_ERROR_INTERNAL, status); } diff --git a/tests/nvcv_types/unit/TestHandleManager.cpp b/tests/nvcv_types/unit/TestHandleManager.cpp index e38010577..e93fd537c 100644 --- a/tests/nvcv_types/unit/TestHandleManager.cpp +++ b/tests/nvcv_types/unit/TestHandleManager.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,6 +21,7 @@ #include #include +#include #include namespace priv = nvcv::priv; @@ -28,9 +29,25 @@ namespace priv = nvcv::priv; constexpr int FORCE_FAILURE = 0xDEADBEEF; namespace { +struct IObjectHandleRec +{ +}; + +using IObjectHandle = IObjectHandleRec *; + +class HandleManagerTestError : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + class alignas(priv::kResourceAlignment) IObject { public: + using HandleType = IObjectHandle; + + virtual ~IObject() = default; + virtual int value() const = 0; }; @@ -42,11 +59,11 @@ class Object : public IObject { if (val == FORCE_FAILURE) { - throw std::runtime_error("Forced failure"); + throw HandleManagerTestError("Forced failure"); } } - virtual int value() const override + int value() const override { return m_value; } @@ -70,30 +87,30 @@ TEST(HandleManager, smoke_handle_generation_wraps_around) mgr.setFixedSize(1); - std::unordered_set usedHandles; + std::unordered_set usedHandles; - void *h; - Object *obj; + IObjectHandle h; + Object *obj; std::tie(h, obj) = mgr.create(0); ASSERT_EQ(0, obj->value()); ASSERT_EQ(obj, mgr.validate(h)); usedHandles.insert(h); - void *origh = h; + IObjectHandle origh = h; // Maximum of 16 generations for (int i = 1; i < 16; ++i) { - IObject *obj = mgr.validate(h); - ASSERT_EQ(i - 1, obj->value()); + IObject *currentObj = mgr.validate(h); + ASSERT_EQ(i - 1, currentObj->value()); mgr.decRef(h); - void *newh = mgr.create(i).first; + IObjectHandle newh = mgr.create(i).first; ASSERT_FALSE(usedHandles.contains(newh)) << "Handle generation must be different"; usedHandles.insert(newh); IObject *newobj = mgr.validate(newh); - ASSERT_EQ(obj, newobj); + ASSERT_EQ(currentObj, newobj); ASSERT_EQ(i, newobj->value()); h = newh; @@ -114,7 +131,7 @@ TEST(HandleManager, smoke_destroy_already_destroyed) { priv::HandleManager mgr("Object"); - void *h = mgr.create(0).first; + IObjectHandle h = mgr.create(0).first; ASSERT_EQ(0, mgr.decRef(h)); ASSERT_THROW(mgr.decRef(h), nvcv::priv::Exception); } @@ -123,7 +140,7 @@ TEST(HandleManager, smoke_ref_unref) { priv::HandleManager mgr("Object"); - void *h = mgr.create(0).first; + IObjectHandle h = mgr.create(0).first; ASSERT_EQ(2, mgr.incRef(h)); ASSERT_EQ(1, mgr.decRef(h)); ASSERT_EQ(2, mgr.incRef(h)); @@ -138,8 +155,9 @@ TEST(HandleManager, smoke_dec_ref_invalid) { priv::HandleManager mgr("Object"); - void *h = mgr.create(0).first; - EXPECT_THROW(mgr.decRef((void *)0x666), nvcv::priv::Exception); + IObjectHandle h = mgr.create(0).first; + IObjectHandleRec invalidHandle; + EXPECT_THROW(mgr.decRef(&invalidHandle), nvcv::priv::Exception); EXPECT_EQ(0, mgr.decRef(h)); } @@ -147,7 +165,7 @@ TEST(HandleManager, smoke_validate_already_destroyed) { priv::HandleManager mgr("Object"); - void *h = mgr.create(0).first; + IObjectHandle h = mgr.create(0).first; ASSERT_NE(nullptr, mgr.validate(h)); ASSERT_EQ(0, mgr.decRef(h)); @@ -158,10 +176,11 @@ TEST(HandleManager, smoke_validate_invalid) { priv::HandleManager mgr("Object"); - void *h = mgr.create(0).first; + IObjectHandle h = mgr.create(0).first; ASSERT_NE(nullptr, mgr.validate(h)); // just to have something being managed already - ASSERT_EQ(nullptr, mgr.validate((void *)0x666)); + IObjectHandleRec invalidHandle; + ASSERT_EQ(nullptr, mgr.validate(&invalidHandle)); ASSERT_EQ(0, mgr.decRef(h)); } @@ -171,7 +190,7 @@ TEST(HandleManager, smoke_handle_count_overflow) priv::HandleManager mgr("Object"); mgr.setFixedSize(1); - void *h = nullptr; + IObjectHandle h = nullptr; ASSERT_NO_THROW(h = mgr.create(0).first); NVCV_ASSERT_STATUS(NVCV_ERROR_OUT_OF_MEMORY, mgr.create(1)); @@ -185,7 +204,7 @@ TEST(HandleManager, smoke_no_handle_leak_if_object_creation_throws) ASSERT_THROW(mgr.create(FORCE_FAILURE), std::runtime_error); - void *h = nullptr; + IObjectHandle h = nullptr; ASSERT_NO_THROW(h = mgr.create(1).first); mgr.decRef(h); } diff --git a/tests/nvcv_types/unit/TestHandleWrapper.cpp b/tests/nvcv_types/unit/TestHandleWrapper.cpp index 9d4695683..45b5d7235 100644 --- a/tests/nvcv_types/unit/TestHandleWrapper.cpp +++ b/tests/nvcv_types/unit/TestHandleWrapper.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,16 +21,28 @@ #include #include +#include + namespace { class alignas(nvcv::priv::kResourceAlignment) IObject { public: + using HandleType = struct NVCVDummy *; + + virtual ~IObject() = default; + virtual int value() const = 0; }; constexpr int kThrowAtConstruction = 0xBADF00D; +class HandleWrapperTestError : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + class DummyResource : public IObject { public: @@ -39,11 +51,11 @@ class DummyResource : public IObject { if (val == kThrowAtConstruction) { - throw std::runtime_error("Forced failure"); + throw HandleWrapperTestError("Forced failure"); } } - virtual int value() const override + int value() const override { return m_value; } @@ -64,7 +76,7 @@ struct ResourceStorage namespace { -typedef struct NVCVDummy *NVCVDummyHandle; +using NVCVDummyHandle = NVCVDummy *; auto &ManagerInst() { @@ -74,13 +86,13 @@ auto &ManagerInst() NVCVStatus nvcvDummyCreate(int k, NVCVDummyHandle *out) { - return nvcv::priv::ProtectCall([&]() { *out = (NVCVDummyHandle)ManagerInst().create(k).first; }); + return nvcv::priv::ProtectCall([out, k]() { *out = ManagerInst().create(k).first; }); } NVCVStatus nvcvDummyIncRef(NVCVDummyHandle handle, int *ref) { return nvcv::priv::ProtectCall( - [&]() + [handle, ref]() { int r = ManagerInst().incRef(handle); if (ref) @@ -91,7 +103,7 @@ NVCVStatus nvcvDummyIncRef(NVCVDummyHandle handle, int *ref) NVCVStatus nvcvDummyDecRef(NVCVDummyHandle handle, int *ref) { return nvcv::priv::ProtectCall( - [&]() + [handle, ref]() { int r = ManagerInst().decRef(handle); if (ref) @@ -107,7 +119,7 @@ NVCVStatus nvcvDummyDestroy(NVCVDummyHandle handle) NVCVStatus nvcvDummyRefCount(NVCVDummyHandle handle, int *ref) { return nvcv::ProtectCall( - [&]() + [handle, ref]() { int r = ManagerInst().refCount(handle); *ref = r; @@ -140,8 +152,7 @@ TEST(HandleWrapperTest, TestHandleOps) EXPECT_EQ(shared_ops.IncRef(handle), 3); EXPECT_EQ(shared_ops.RefCount(handle), 3); EXPECT_EQ(shared_ops.DecRef(handle), 2); - nvcv::detail::UniqueHandleOps unique_ops; - unique_ops.Destroy(handle); + nvcv::detail::UniqueHandleOps::Destroy(handle); EXPECT_EQ(shared_ops.RefCount(handle), 1); // destroy should call DecRef (that's how this Dummy works) EXPECT_EQ(shared_ops.DecRef(handle), 0); // object destroyed @@ -183,7 +194,8 @@ TEST(UniqueHandleTest, ResetRelease) TEST(UniqueHandleTest, Overwrite) { - NVCVDummyHandle h1, h2; + NVCVDummyHandle h1; + NVCVDummyHandle h2; ASSERT_EQ(NVCV_SUCCESS, nvcvDummyCreate(0, &h1)); ASSERT_EQ(NVCV_SUCCESS, nvcvDummyCreate(0, &h2)); @@ -198,7 +210,7 @@ TEST(UniqueHandleTest, Overwrite) EXPECT_NE(nullptr, ManagerInst().validate(backup2)); uh1 = std::move(uh2); - EXPECT_EQ(uh2.get(), nullptr); + EXPECT_EQ(uh2.get(), nullptr); // NOSONAR: this test verifies moved-from state. EXPECT_EQ(uh1.get(), backup2); EXPECT_NE(nullptr, ManagerInst().validate(backup2)); @@ -240,7 +252,8 @@ TEST(SharedHandleTest, ResetRelease) TEST(SharedHandleTest, CopyMove) { - NVCVDummyHandle h1, h2; + NVCVDummyHandle h1; + NVCVDummyHandle h2; ASSERT_EQ(NVCV_SUCCESS, nvcvDummyCreate(0, &h1)); ASSERT_EQ(NVCV_SUCCESS, nvcvDummyCreate(0, &h2)); diff --git a/tests/nvcv_types/unit/TestHashMD5.cpp b/tests/nvcv_types/unit/TestHashMD5.cpp index c2bf825ed..f511023fc 100644 --- a/tests/nvcv_types/unit/TestHashMD5.cpp +++ b/tests/nvcv_types/unit/TestHashMD5.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -59,7 +59,7 @@ const auto g_matchMd5String2 = t::ElementsAre 0x43, 0x08, 0xFA, 0x20 ); -auto g_matchFPzero = t::ElementsAre +const auto g_matchFPzero = t::ElementsAre ( 0x7D, 0xEA, 0x36, 0x2B, 0x3F, 0xAC, 0x8E, 0x00, @@ -132,7 +132,7 @@ TEST(HashMD5Tests, vector) test::HashMD5 hash; // clang-format off - const std::vector vec1 = {0.5, -0.2, 3.141}; + const std::vector vec1 = {0.5f, -0.2f, 3.141f}; const auto matcher1 = t::ElementsAre( 0xCC, 0xC2, 0xAB, 0x13, @@ -141,7 +141,7 @@ TEST(HashMD5Tests, vector) 0x63, 0xF8, 0xE1, 0x94 ); - const std::vector vec2 = {0.5, -0.2, 3.142}; + const std::vector vec2 = {0.5f, -0.2f, 3.142f}; const auto matcher2 = t::ElementsAre( 0xA8, 0xFA, 0x59, 0x16, @@ -226,7 +226,8 @@ TEST(HashMD5Tests, std_string_view_string) { test::HashMD5 hash; - Update(hash, std::string_view{std::string{g_str1}}); + std::string str{g_str1}; + Update(hash, std::string_view{str}); EXPECT_THAT(hash.getHashAndReset(), g_matchMd5String1); } diff --git a/tests/nvcv_types/unit/TestImageFormat.cpp b/tests/nvcv_types/unit/TestImageFormat.cpp index 75f7fe1f2..485eeb40a 100644 --- a/tests/nvcv_types/unit/TestImageFormat.cpp +++ b/tests/nvcv_types/unit/TestImageFormat.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -94,13 +94,16 @@ TEST(ImageFormatTests, UpdateColorSpec) nvcv::priv::ImageFormat{NVCV_IMAGE_FORMAT_NV12}.colorSpec()), nvcv::priv::Exception); - EXPECT_EQ(UpdateColorSpec(nvcv::priv::ImageFormat{NVCV_IMAGE_FORMAT_U8}, NVCV_COLOR_SPEC_BT601), - nvcv::priv::ImageFormat{NVCV_IMAGE_FORMAT_U8}); + EXPECT_EQ( + UpdateColorSpec(nvcv::priv::ImageFormat{NVCV_IMAGE_FORMAT_U8}, nvcv::priv::ColorSpec{NVCV_COLOR_SPEC_BT601}), + nvcv::priv::ImageFormat{NVCV_IMAGE_FORMAT_U8}); - EXPECT_EQ(UpdateColorSpec(nvcv::priv::ImageFormat{NVCV_IMAGE_FORMAT_RGB8}, NVCV_COLOR_SPEC_sRGB), - nvcv::priv::ImageFormat{NVCV_IMAGE_FORMAT_sRGB8}); + EXPECT_EQ( + UpdateColorSpec(nvcv::priv::ImageFormat{NVCV_IMAGE_FORMAT_RGB8}, nvcv::priv::ColorSpec{NVCV_COLOR_SPEC_sRGB}), + nvcv::priv::ImageFormat{NVCV_IMAGE_FORMAT_sRGB8}); - EXPECT_EQ(UpdateColorSpec(nvcv::priv::ImageFormat{NVCV_IMAGE_FORMAT_UYVY_UNDEFINED_SPEC}, NVCV_COLOR_SPEC_BT601), + EXPECT_EQ(UpdateColorSpec(nvcv::priv::ImageFormat{NVCV_IMAGE_FORMAT_UYVY_UNDEFINED_SPEC}, + nvcv::priv::ColorSpec{NVCV_COLOR_SPEC_BT601}), nvcv::priv::ImageFormat{NVCV_IMAGE_FORMAT_UYVY}); #undef NVCV_IMAGE_FORMAT_sRGB8 @@ -279,39 +282,40 @@ TEST(ImageFormatTests, constructor_0) TEST(ImageFormatTests, constructor_1) { - EXPECT_THROW(nvcv::priv::ImageFormat(NVCV_COLOR_MODEL_RAW, NVCV_COLOR_SPEC_BT601, NVCV_CSS_420, NVCV_MEM_LAYOUT_PL, - NVCV_DATA_KIND_UNSIGNED, NVCV_SWIZZLE_X000, NVCV_PACKING_X8, NVCV_PACKING_0, - NVCV_PACKING_0, NVCV_PACKING_0, NVCV_ALPHA_ASSOCIATED, nullptr), - nvcv::priv::Exception); - EXPECT_THROW( - nvcv::priv::ImageFormat(NVCV_COLOR_MODEL_UNDEFINED, NVCV_COLOR_SPEC_BT601, NVCV_CSS_NONE, NVCV_MEM_LAYOUT_PL, - NVCV_DATA_KIND_UNSIGNED, NVCV_SWIZZLE_X000, NVCV_PACKING_X8, NVCV_PACKING_0, - NVCV_PACKING_0, NVCV_PACKING_0, NVCV_ALPHA_ASSOCIATED, nullptr), + nvcv::priv::ImageFormat(NVCV_COLOR_MODEL_RAW, nvcv::priv::ColorSpec{NVCV_COLOR_SPEC_BT601}, NVCV_CSS_420, + NVCV_MEM_LAYOUT_PL, NVCV_DATA_KIND_UNSIGNED, NVCV_SWIZZLE_X000, NVCV_PACKING_X8, + NVCV_PACKING_0, NVCV_PACKING_0, NVCV_PACKING_0, NVCV_ALPHA_ASSOCIATED, nullptr), nvcv::priv::Exception); EXPECT_THROW( - nvcv::priv::ImageFormat(NVCV_COLOR_MODEL_UNDEFINED, NVCV_COLOR_SPEC_UNDEFINED, NVCV_CSS_420, NVCV_MEM_LAYOUT_PL, - NVCV_DATA_KIND_UNSIGNED, NVCV_SWIZZLE_X000, NVCV_PACKING_X8, NVCV_PACKING_0, - NVCV_PACKING_0, NVCV_PACKING_0, NVCV_ALPHA_ASSOCIATED, nullptr), + nvcv::priv::ImageFormat(NVCV_COLOR_MODEL_UNDEFINED, nvcv::priv::ColorSpec{NVCV_COLOR_SPEC_BT601}, NVCV_CSS_NONE, + NVCV_MEM_LAYOUT_PL, NVCV_DATA_KIND_UNSIGNED, NVCV_SWIZZLE_X000, NVCV_PACKING_X8, + NVCV_PACKING_0, NVCV_PACKING_0, NVCV_PACKING_0, NVCV_ALPHA_ASSOCIATED, nullptr), nvcv::priv::Exception); + EXPECT_THROW(nvcv::priv::ImageFormat(NVCV_COLOR_MODEL_UNDEFINED, nvcv::priv::ColorSpec{NVCV_COLOR_SPEC_UNDEFINED}, + NVCV_CSS_420, NVCV_MEM_LAYOUT_PL, NVCV_DATA_KIND_UNSIGNED, NVCV_SWIZZLE_X000, + NVCV_PACKING_X8, NVCV_PACKING_0, NVCV_PACKING_0, NVCV_PACKING_0, + NVCV_ALPHA_ASSOCIATED, nullptr), + nvcv::priv::Exception); + EXPECT_THROW( - nvcv::priv::ImageFormat(NVCV_COLOR_MODEL_HSV, NVCV_COLOR_SPEC_BT601, NVCV_CSS_NONE, NVCV_MEM_LAYOUT_PL, - NVCV_DATA_KIND_UNSIGNED, NVCV_SWIZZLE_XYZ0, NVCV_PACKING_X8_Y8_Z8, NVCV_PACKING_0, - NVCV_PACKING_0, NVCV_PACKING_0, NVCV_ALPHA_ASSOCIATED, nullptr), + nvcv::priv::ImageFormat(NVCV_COLOR_MODEL_HSV, nvcv::priv::ColorSpec{NVCV_COLOR_SPEC_BT601}, NVCV_CSS_NONE, + NVCV_MEM_LAYOUT_PL, NVCV_DATA_KIND_UNSIGNED, NVCV_SWIZZLE_XYZ0, NVCV_PACKING_X8_Y8_Z8, + NVCV_PACKING_0, NVCV_PACKING_0, NVCV_PACKING_0, NVCV_ALPHA_ASSOCIATED, nullptr), nvcv::priv::Exception); EXPECT_THROW( - nvcv::priv::ImageFormat(NVCV_COLOR_MODEL_HSV, NVCV_COLOR_SPEC_UNDEFINED, NVCV_CSS_420, NVCV_MEM_LAYOUT_PL, - NVCV_DATA_KIND_UNSIGNED, NVCV_SWIZZLE_XYZ0, NVCV_PACKING_X8_Y8_Z8, NVCV_PACKING_0, - NVCV_PACKING_0, NVCV_PACKING_0, NVCV_ALPHA_ASSOCIATED, nullptr), + nvcv::priv::ImageFormat(NVCV_COLOR_MODEL_HSV, nvcv::priv::ColorSpec{NVCV_COLOR_SPEC_UNDEFINED}, NVCV_CSS_420, + NVCV_MEM_LAYOUT_PL, NVCV_DATA_KIND_UNSIGNED, NVCV_SWIZZLE_XYZ0, NVCV_PACKING_X8_Y8_Z8, + NVCV_PACKING_0, NVCV_PACKING_0, NVCV_PACKING_0, NVCV_ALPHA_ASSOCIATED, nullptr), nvcv::priv::Exception); } TEST(ImageFormatTests, operator_insertion) { - auto testOperatorInsertion = [](std::string expectedStr, NVCVImageFormat fmt) -> void + auto testOperatorInsertion = [](const std::string &expectedStr, NVCVImageFormat fmt) { std::ostringstream ss; ss << nvcv::priv::ImageFormat{fmt}; diff --git a/tests/nvcv_types/unit/TestLockFreeStack.cpp b/tests/nvcv_types/unit/TestLockFreeStack.cpp index 298af4fc2..ea67f3518 100644 --- a/tests/nvcv_types/unit/TestLockFreeStack.cpp +++ b/tests/nvcv_types/unit/TestLockFreeStack.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,11 +19,13 @@ #include +#include + namespace priv = nvcv::priv; struct Node { - Node(int v = 0) + explicit Node(int v = 0) : value(v){}; int value; @@ -35,25 +37,25 @@ TEST(LockFreeStack, smoke_push) priv::LockFreeStack stack; ASSERT_TRUE(stack.empty()); - Node n[3]; + std::array n; for (int i = 0; i < 3; ++i) { n[i].value = i; - stack.push(n + i); + stack.push(n.data() + i); } - ASSERT_EQ(n + 2, stack.top()); - ASSERT_EQ(n + 1, n[2].next); - ASSERT_EQ(n + 0, n[1].next); + ASSERT_EQ(n.data() + 2, stack.top()); + ASSERT_EQ(n.data() + 1, n[2].next); + ASSERT_EQ(n.data() + 0, n[1].next); ASSERT_EQ(nullptr, n[0].next); - ASSERT_EQ(n + 2, stack.pop()); + ASSERT_EQ(n.data() + 2, stack.pop()); ASSERT_FALSE(stack.empty()); - ASSERT_EQ(n + 1, stack.pop()); + ASSERT_EQ(n.data() + 1, stack.pop()); ASSERT_FALSE(stack.empty()); - ASSERT_EQ(n + 0, stack.pop()); + ASSERT_EQ(n.data() + 0, stack.pop()); ASSERT_TRUE(stack.empty()); } @@ -65,28 +67,28 @@ TEST(LockFreeStack, smoke_push_stack) Node n(0); stack.push(&n); - Node nn[3]; + std::array nn; for (int i = 0; i < 3; ++i) { nn[i].value = i; nn[i].next = i + 1 < 3 ? &nn[i + 1] : nullptr; } - stack.pushStack(nn, nn + 2); + stack.pushStack(nn.data(), nn.data() + 2); - ASSERT_EQ(nn + 0, stack.top()); - ASSERT_EQ(nn + 1, nn[0].next); - ASSERT_EQ(nn + 2, nn[1].next); + ASSERT_EQ(nn.data() + 0, stack.top()); + ASSERT_EQ(nn.data() + 1, nn[0].next); + ASSERT_EQ(nn.data() + 2, nn[1].next); ASSERT_EQ(&n, nn[2].next); ASSERT_EQ(nullptr, n.next); - ASSERT_EQ(nn + 0, stack.pop()); + ASSERT_EQ(nn.data() + 0, stack.pop()); ASSERT_FALSE(stack.empty()); - ASSERT_EQ(nn + 1, stack.pop()); + ASSERT_EQ(nn.data() + 1, stack.pop()); ASSERT_FALSE(stack.empty()); - ASSERT_EQ(nn + 2, stack.pop()); + ASSERT_EQ(nn.data() + 2, stack.pop()); ASSERT_FALSE(stack.empty()); ASSERT_EQ(&n, stack.pop()); @@ -101,21 +103,21 @@ TEST(LockFreeStack, smoke_release) priv::LockFreeStack stack; ASSERT_TRUE(stack.empty()); - Node nn[3]; + std::array nn; for (int i = 0; i < 3; ++i) { nn[i].value = i; nn[i].next = i + 1 < 3 ? &nn[i + 1] : nullptr; } - stack.pushStack(nn, nn + 2); + stack.pushStack(nn.data(), nn.data() + 2); Node *h = stack.release(); EXPECT_EQ(nullptr, stack.top()); - EXPECT_EQ(nn + 0, h); - EXPECT_EQ(nn + 1, nn[0].next); - EXPECT_EQ(nn + 2, nn[1].next); + EXPECT_EQ(nn.data() + 0, h); + EXPECT_EQ(nn.data() + 1, nn[0].next); + EXPECT_EQ(nn.data() + 2, nn[1].next); EXPECT_EQ(nullptr, nn[2].next); } diff --git a/tests/nvcv_types/unit/TestMath.cpp b/tests/nvcv_types/unit/TestMath.cpp index 4ffc5f69e..aca59b609 100644 --- a/tests/nvcv_types/unit/TestMath.cpp +++ b/tests/nvcv_types/unit/TestMath.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -65,9 +65,9 @@ NVCV_INSTANTIATE_TEST_SUITE_P(_, MathRoundUpTests, TEST_P(MathRoundUpTests, works) { - const uint64_t input = std::get<0>(GetParam()); - const int next = std::get<1>(GetParam()); - const uint64_t gold = std::get<2>(GetParam()); + const uint64_t input = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + const int next = ::nvcv::test::ParamValue(std::get<1>(GetParam())); + const uint64_t gold = ::nvcv::test::ParamValue(std::get<2>(GetParam())); EXPECT_EQ(gold, util::RoundUp(input, next)); } @@ -105,8 +105,8 @@ NVCV_INSTANTIATE_TEST_SUITE_P(_, MathIsPowerOfTwoTests, TEST_P(MathIsPowerOfTwoTests, works) { - const uint64_t input = std::get<0>(GetParam()); - const bool gold = std::get<1>(GetParam()); + const uint64_t input = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + const bool gold = ::nvcv::test::ParamValue(std::get<1>(GetParam())); EXPECT_EQ(gold, util::IsPowerOfTwo(input)); } @@ -146,8 +146,8 @@ NVCV_INSTANTIATE_TEST_SUITE_P(_, MathRoundUpNextPowerOfTwoTests, TEST_P(MathRoundUpNextPowerOfTwoTests, works) { - const int64_t input = std::get<0>(GetParam()); - const int64_t gold = std::get<1>(GetParam()); + const int64_t input = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + const int64_t gold = ::nvcv::test::ParamValue(std::get<1>(GetParam())); if (input < 128) { @@ -191,9 +191,9 @@ NVCV_INSTANTIATE_TEST_SUITE_P(_, MathDivUpTests, TEST_P(MathDivUpTests, works) { - const int64_t num = std::get<0>(GetParam()); - const int64_t den = std::get<1>(GetParam()); - const int64_t gold = std::get<2>(GetParam()); + const int64_t num = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + const int64_t den = ::nvcv::test::ParamValue(std::get<1>(GetParam())); + const int64_t gold = ::nvcv::test::ParamValue(std::get<2>(GetParam())); EXPECT_EQ(gold, util::DivUp(num, den)); } @@ -222,9 +222,9 @@ NVCV_INSTANTIATE_TEST_SUITE_P(_, MathRoundUpPowerOfTwoTests, TEST_P(MathRoundUpPowerOfTwoTests, works) { - const int64_t value = std::get<0>(GetParam()); - const int64_t multiple = std::get<1>(GetParam()); - const int64_t gold = std::get<2>(GetParam()); + const int64_t value = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + const int64_t multiple = ::nvcv::test::ParamValue(std::get<1>(GetParam())); + const int64_t gold = ::nvcv::test::ParamValue(std::get<2>(GetParam())); EXPECT_EQ(gold, util::RoundUpPowerOfTwo(value, multiple)); } @@ -252,8 +252,8 @@ NVCV_INSTANTIATE_TEST_SUITE_P(_, MathILog2Tests, TEST_P(MathILog2Tests, works) { - const int64_t value = std::get<0>(GetParam()); - const int64_t gold = std::get<1>(GetParam()); + const int64_t value = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + const int64_t gold = ::nvcv::test::ParamValue(std::get<1>(GetParam())); EXPECT_EQ(gold, util::ILog2(value)); } @@ -284,9 +284,9 @@ NVCV_INSTANTIATE_TEST_SUITE_P(_, MathDivUpPowerOfTwoTests, TEST_P(MathDivUpPowerOfTwoTests, works) { - const int64_t num = std::get<0>(GetParam()); - const int64_t den = std::get<1>(GetParam()); - const int64_t gold = std::get<2>(GetParam()); + const int64_t num = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + const int64_t den = ::nvcv::test::ParamValue(std::get<1>(GetParam())); + const int64_t gold = ::nvcv::test::ParamValue(std::get<2>(GetParam())); EXPECT_EQ(gold, util::DivUpPowerOfTwo(num, den)); } @@ -309,8 +309,8 @@ NVCV_INSTANTIATE_TEST_SUITE_P(_, MathSincTests, TEST_P(MathSincTests, works) { - const float value = std::get<0>(GetParam()); - const float gold = std::get<1>(GetParam()); + const float value = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + const float gold = ::nvcv::test::ParamValue(std::get<1>(GetParam())); EXPECT_NEAR(gold, util::sinc(value), 1e-7f); } diff --git a/tests/nvcv_types/unit/TestOptional.cpp b/tests/nvcv_types/unit/TestOptional.cpp index 2efaa4dd6..47c300c94 100644 --- a/tests/nvcv_types/unit/TestOptional.cpp +++ b/tests/nvcv_types/unit/TestOptional.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,6 +19,7 @@ #include +#include #include TEST(Optional, default_no_value) @@ -50,7 +51,8 @@ TEST(Optional, assignment) ASSERT_TRUE(opt.hasValue()); ASSERT_EQ(*opt, test_value); - nvcv::Optional> o1, o2; + nvcv::Optional> o1; + nvcv::Optional> o2; o1 = opt; EXPECT_TRUE(o1.hasValue()); EXPECT_EQ(o1.value(), test_value); @@ -60,8 +62,8 @@ TEST(Optional, assignment) o2 = std::move(opt); EXPECT_TRUE(o1.hasValue()); EXPECT_EQ(o1.value(), test_value); - EXPECT_TRUE(opt.hasValue()) << "A moved-out optional still has a value."; - EXPECT_TRUE(opt.value().empty()) << "The value wasn't moved out properly"; + EXPECT_TRUE(opt.hasValue()); // NOSONAR: this test verifies moved-from state. + EXPECT_TRUE(opt.value().empty()); // NOSONAR: this test verifies moved-from state. opt = nvcv::NullOpt; EXPECT_FALSE(opt.hasValue()); @@ -70,7 +72,7 @@ TEST(Optional, assignment) EXPECT_TRUE(opt.hasValue()); EXPECT_EQ(opt.value(), test_value2); - nvcv::Optional c = 42; + nvcv::Optional c{42}; nvcv::Optional i; i = c; EXPECT_TRUE(i.hasValue()); @@ -103,6 +105,37 @@ TEST(Optional, equality) EXPECT_TRUE(nvcv::NullOpt != optA); } -// TODO need way more tests. +namespace { + +struct ThrowingCtorError : std::runtime_error +{ + ThrowingCtorError() + : std::runtime_error("construction failed") + { + } +}; + +struct ThrowingCtor +{ + explicit ThrowingCtor(bool shouldThrow) + { + if (shouldThrow) + { + throw ThrowingCtorError{}; + } + } +}; + +} // namespace + +TEST(Optional, emplace_leaves_empty_when_replacement_construction_throws) +{ + nvcv::Optional opt(nvcv::detail::InPlace, false); + + EXPECT_THROW(opt.emplace(true), std::runtime_error); + EXPECT_FALSE(opt.hasValue()); +} + +// REVISIT need way more tests. // We're not writing them now because if we can upgrade public API to c++17, // we won't need our Optional. diff --git a/tests/nvcv_types/unit/TestRange.cpp b/tests/nvcv_types/unit/TestRange.cpp index f2f57ccdb..67ff56771 100644 --- a/tests/nvcv_types/unit/TestRange.cpp +++ b/tests/nvcv_types/unit/TestRange.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,6 +20,7 @@ #include #include +#include #include #include #include @@ -34,7 +35,7 @@ constexpr int DEFAULT_RANGE_SIZE = 5; class RangeMember { public: - RangeMember(int len = DEFAULT_RANGE_SIZE) + explicit RangeMember(int len = DEFAULT_RANGE_SIZE) : m_length(len) { } @@ -56,54 +57,36 @@ class RangeMember class NotRangeBegin { public: - NotRangeBegin(int len = DEFAULT_RANGE_SIZE) - : m_length(len) - { - } + explicit NotRangeBegin(int = DEFAULT_RANGE_SIZE) {} int *begin() const { return nullptr; } - -private: - int m_length; }; class NotRangeEnd { public: - NotRangeEnd(int len = DEFAULT_RANGE_SIZE) - : m_length(len) - { - } + explicit NotRangeEnd(int = DEFAULT_RANGE_SIZE) {} int *end() const { return nullptr; } - -private: - int m_length; }; class NotRange { public: - NotRange(int len = DEFAULT_RANGE_SIZE) - : m_length(len) - { - } - -private: - int m_length; + explicit NotRange(int = DEFAULT_RANGE_SIZE) {} }; namespace range { class RangeGlobal { public: - RangeGlobal(int len = DEFAULT_RANGE_SIZE) + explicit RangeGlobal(int len = DEFAULT_RANGE_SIZE) : m_length(len) { } @@ -122,12 +105,12 @@ class RangeGlobal int m_length; }; -static int *begin(const RangeGlobal &r) +int *begin(const RangeGlobal &r) { return r.my_begin(); } -static int *end(const RangeGlobal &r) +int *end(const RangeGlobal &r) { return r.my_end(); } @@ -198,8 +181,7 @@ TYPED_TEST(RangeValueTest, works) ASSERT_TRUE((std::is_same_v, Value>)); } -NVCV_TYPED_TEST_SUITE(RangeRandomAccessPositiveTest, - ttest::Types, std::array,*/ int[10]>); +NVCV_TYPED_TEST_SUITE(RangeRandomAccessPositiveTest, ttest::Types, std::array, int[10]>); TYPED_TEST(RangeRandomAccessPositiveTest, works) { diff --git a/tests/nvcv_types/unit/TestSharedCoreObj.cpp b/tests/nvcv_types/unit/TestSharedCoreObj.cpp index 1fb2c4f71..f0f816f57 100644 --- a/tests/nvcv_types/unit/TestSharedCoreObj.cpp +++ b/tests/nvcv_types/unit/TestSharedCoreObj.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -31,7 +31,8 @@ inline NVCVImageHandle CreateImage() nvcv::Size2D size{640, 480}; nvcv::ImageFormat fmt = nvcv::FMT_RGBA8; NVCVImageRequirements reqs; - nvcv::detail::CheckThrow(nvcvImageCalcRequirements(size.w, size.h, fmt, 256, 16, &reqs)); + nvcv::detail::CheckThrow( + nvcvImageCalcRequirements(size.w, size.h, static_cast(fmt), 256, 16, &reqs)); auto &alloc = nvcv::priv::GetDefaultAllocator(); return nvcv::priv::CreateCoreObject(reqs, alloc); @@ -90,7 +91,7 @@ TEST(SharedCoreObjTest, Comparison) Ptr s2 = Ptr::FromHandle(h2, true); EXPECT_EQ(nvcv::priv::CoreObjectDecRef(h2), 1); - Ptr s3 = nullptr; + Ptr s3{nullptr}; EXPECT_TRUE(s1 == s1); EXPECT_FALSE(s1 != s1); @@ -137,7 +138,7 @@ TEST(SharedCoreObjTest, AssignCopyMove) Ptr s3 = std::move(s2); EXPECT_EQ(nvcv::priv::CoreObjectRefCount(h), 3) << "Ref count changed during move construction."; - EXPECT_EQ(s2, nullptr) << "Not moved out properly"; + EXPECT_EQ(s2, nullptr) << "Not moved out properly"; // NOSONAR: this test verifies moved-from state. s2 = s3; EXPECT_EQ(nvcv::priv::CoreObjectRefCount(h), 4) << "Ref count not raised after copy."; @@ -147,8 +148,8 @@ TEST(SharedCoreObjTest, AssignCopyMove) s3 = std::move(s2); EXPECT_EQ(nvcv::priv::CoreObjectRefCount(h), 3) << "Ref count changed during move into a null shared pointer."; EXPECT_EQ(s3, s1); - EXPECT_EQ(s2, nullptr) << "Not moved out properly"; - s3 = s2; + EXPECT_EQ(s2, nullptr) << "Not moved out properly"; // NOSONAR: this test verifies moved-from state. + s3 = s2; // NOSONAR: this test verifies copy-from-moved-out state. EXPECT_EQ(nvcv::priv::CoreObjectRefCount(h), 2) << "Ref count not dropped after copy-from-null assignment."; } EXPECT_EQ(nvcv::priv::CoreObjectRefCount(h), 1); diff --git a/tests/nvcv_types/unit/TestStaticVector.cpp b/tests/nvcv_types/unit/TestStaticVector.cpp index ac5235073..50806b173 100644 --- a/tests/nvcv_types/unit/TestStaticVector.cpp +++ b/tests/nvcv_types/unit/TestStaticVector.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,6 +19,12 @@ #include +#include +#include +#include +#include +#include + namespace util = nvcv::util; TEST(StaticVector, default_constructed_is_empty) @@ -29,7 +35,7 @@ TEST(StaticVector, default_constructed_is_empty) TEST(StaticVector, destructor_destroys_items) { - std::weak_ptr w[2]; + std::array, 2> w; { util::StaticVector, 2> v = {std::make_shared(2), std::make_shared(4)}; @@ -138,7 +144,7 @@ TEST(StaticVector, resize_to_bigger_doesnt_touch_existing_objects) { util::StaticVector, 3> v = {std::make_shared(0), std::make_shared(1)}; - std::weak_ptr w[] = {v[0], v[1]}; + std::array, 2> w = {v[0], v[1]}; ASSERT_NO_THROW(v.resize(3)); @@ -230,7 +236,7 @@ TEST(StaticVector, pop_back_destroys_removed_item) { util::StaticVector, 2> v = {std::make_shared(2), std::make_shared(3)}; - std::weak_ptr w[] = {v[0], v[1]}; + std::array, 2> w = {v[0], v[1]}; ASSERT_NO_THROW(v.pop_back()); @@ -263,7 +269,7 @@ TEST(StaticVector, move_ctor_src_isnt_emptied) [[maybe_unused]] util::StaticVector, 2> dst(std::move(src)); - EXPECT_EQ(1u, src.size()); + EXPECT_EQ(1u, src.size()); // NOSONAR: this test verifies moved-from state. } TEST(StaticVector, move_ctor_dst_has_same_size_as_src_before_move) @@ -291,7 +297,7 @@ TEST(StaticVector, move_assign_src_isnt_emptied) dst = std::move(src); - EXPECT_EQ(1u, src.size()); + EXPECT_EQ(1u, src.size()); // NOSONAR: this test verifies moved-from state. } TEST(StaticVector, move_assign_dst_has_same_size_as_src_before_move) @@ -326,6 +332,21 @@ TEST(StaticVector, copy_assign_not_available_when_elements_arent_copiable) EXPECT_FALSE((std::is_copy_assignable_v, 3>>)); } +struct CopyAssignableOnly +{ + CopyAssignableOnly() = default; + + CopyAssignableOnly(CopyAssignableOnly &) = default; + + CopyAssignableOnly &operator=(const CopyAssignableOnly &) = default; +}; + +TEST(StaticVector, copy_assign_not_available_when_elements_cannot_be_copy_constructed) +{ + EXPECT_TRUE(std::is_copy_assignable_v); + EXPECT_FALSE((std::is_copy_assignable_v>)); +} + TEST(StaticVector, copy_ctor_available_when_elements_are_copiable) { EXPECT_TRUE((std::is_copy_constructible_v, 3>>)); @@ -527,8 +548,8 @@ TEST(StaticVector, move_assignment_src_is_larger_than_src_items_lifetime_are_han util::StaticVector, 2> src{std::make_shared(0), std::make_shared(1)}; util::StaticVector, 2> dst{std::make_shared(3)}; - std::weak_ptr wsrc[2] = {src[0], src[1]}; - std::weak_ptr wdst = {dst[0]}; + std::array, 2> wsrc = {src[0], src[1]}; + std::weak_ptr wdst = {dst[0]}; dst = std::move(src); @@ -542,8 +563,8 @@ TEST(StaticVector, move_assignment_src_is_smaller_than_src_items_lifetime_are_ha util::StaticVector, 2> src{std::make_shared(0)}; util::StaticVector, 2> dst{std::make_shared(1), std::make_shared(3)}; - std::weak_ptr wsrc = {src[0]}; - std::weak_ptr wdst[2] = {dst[0], dst[1]}; + std::weak_ptr wsrc = {src[0]}; + std::array, 2> wdst = {dst[0], dst[1]}; dst = std::move(src); @@ -557,8 +578,8 @@ TEST(StaticVector, move_assignment_src_has_same_element_count_as_dst_items_lifet util::StaticVector, 2> src{std::make_shared(0), std::make_shared(2)}; util::StaticVector, 2> dst{std::make_shared(1), std::make_shared(3)}; - std::weak_ptr wsrc[2] = {src[0], src[1]}; - std::weak_ptr wdst[2] = {dst[0], dst[1]}; + std::array, 2> wsrc = {src[0], src[1]}; + std::array, 2> wdst = {dst[0], dst[1]}; dst = std::move(src); @@ -573,8 +594,8 @@ TEST(StaticVector, copy_assignment_src_is_larger_than_src_items_lifetime_are_han util::StaticVector, 2> src{std::make_shared(0), std::make_shared(1)}; util::StaticVector, 2> dst{std::make_shared(3)}; - std::weak_ptr wsrc[2] = {src[0], src[1]}; - std::weak_ptr wdst = {dst[0]}; + std::array, 2> wsrc = {src[0], src[1]}; + std::weak_ptr wdst = {dst[0]}; dst = src; @@ -588,8 +609,8 @@ TEST(StaticVector, copy_assignment_src_is_smaller_than_src_items_lifetime_are_ha util::StaticVector, 2> src{std::make_shared(0)}; util::StaticVector, 2> dst{std::make_shared(1), std::make_shared(3)}; - std::weak_ptr wsrc = {src[0]}; - std::weak_ptr wdst[2] = {dst[0], dst[1]}; + std::weak_ptr wsrc = {src[0]}; + std::array, 2> wdst = {dst[0], dst[1]}; dst = src; @@ -603,8 +624,8 @@ TEST(StaticVector, copy_assignment_src_has_same_element_count_as_src_items_lifet util::StaticVector, 2> src{std::make_shared(0), std::make_shared(2)}; util::StaticVector, 2> dst{std::make_shared(1), std::make_shared(3)}; - std::weak_ptr wsrc[2] = {src[0], src[1]}; - std::weak_ptr wdst[2] = {dst[0], dst[1]}; + std::array, 2> wsrc = {src[0], src[1]}; + std::array, 2> wdst = {dst[0], dst[1]}; dst = src; @@ -619,7 +640,7 @@ TEST(StaticVector, swap_non_trivial_type_a_is_larger_than_b_dont_destroy_objects util::StaticVector, 2> a{std::make_shared(0), std::make_shared(1)}; util::StaticVector, 2> b{std::make_shared(3)}; - std::weak_ptr w[] = {a[0], a[1], b[0]}; + std::array, 3> w = {a[0], a[1], b[0]}; swap(a, b); @@ -633,7 +654,7 @@ TEST(StaticVector, swap_non_trivial_type_a_is_smaller_than_b_dont_destroy_object util::StaticVector, 2> a{std::make_shared(3)}; util::StaticVector, 2> b{std::make_shared(0), std::make_shared(1)}; - std::weak_ptr w[] = {a[0], b[0], b[1]}; + std::array, 3> w = {a[0], b[0], b[1]}; swap(a, b); @@ -647,7 +668,7 @@ TEST(StaticVector, swap_non_trivial_type_a_has_same_element_count_as_b_dont_dest util::StaticVector, 2> a{std::make_shared(3), std::make_shared(4)}; util::StaticVector, 2> b{std::make_shared(0), std::make_shared(1)}; - std::weak_ptr w[] = {a[0], a[1], b[0], b[1]}; + std::array, 4> w = {a[0], a[1], b[0], b[1]}; swap(a, b); @@ -734,7 +755,7 @@ TEST(StaticVector, move_ctor_trivial_type_doesnt_reset_src) util::StaticVector src{1, 2}; util::StaticVector dst(std::move(src)); - EXPECT_EQ(2u, src.size()); + EXPECT_EQ(2u, src.size()); // NOSONAR: this test verifies moved-from state. EXPECT_EQ(2u, dst.size()); } @@ -745,7 +766,7 @@ TEST(StaticVector, move_assign_trivial_type_doesnt_reset_src) dst = std::move(src); - EXPECT_EQ(2u, src.size()); + EXPECT_EQ(2u, src.size()); // NOSONAR: this test verifies moved-from state. EXPECT_EQ(2u, dst.size()); } @@ -816,7 +837,7 @@ TEST(StaticVector, erase_single_element_at_beginning) util::StaticVector, 3> vec{std::make_shared(1), std::make_shared(2), std::make_shared(3)}; - std::weak_ptr items[] = {vec[0], vec[1], vec[2]}; + std::array, 3> items = {vec[0], vec[1], vec[2]}; vec.erase(vec.begin()); @@ -835,7 +856,7 @@ TEST(StaticVector, erase_single_element_in_the_middle) util::StaticVector, 3> vec{std::make_shared(1), std::make_shared(2), std::make_shared(3)}; - std::weak_ptr items[] = {vec[0], vec[1], vec[2]}; + std::array, 3> items = {vec[0], vec[1], vec[2]}; vec.erase(vec.begin() + 1); @@ -854,7 +875,7 @@ TEST(StaticVector, erase_single_element_at_the_end) util::StaticVector, 3> vec{std::make_shared(1), std::make_shared(2), std::make_shared(3)}; - std::weak_ptr items[] = {vec[0], vec[1], vec[2]}; + std::array, 3> items = {vec[0], vec[1], vec[2]}; vec.erase(vec.begin() + 2); @@ -873,7 +894,7 @@ TEST(StaticVector, erase_single_element_past_end_segfaults) util::StaticVector, 3> vec{std::make_shared(1), std::make_shared(2), std::make_shared(3)}; - std::weak_ptr items[] = {vec[0], vec[1], vec[2]}; + std::array, 3> items = {vec[0], vec[1], vec[2]}; ASSERT_DEATH(vec.erase(vec.end()), ".*"); } @@ -883,7 +904,7 @@ TEST(StaticVector, erase_single_element_before_beginning_segfaults) util::StaticVector, 3> vec{std::make_shared(1), std::make_shared(2), std::make_shared(3)}; - std::weak_ptr items[] = {vec[0], vec[1], vec[2]}; + std::array, 3> items = {vec[0], vec[1], vec[2]}; ASSERT_DEATH(vec.erase(vec.begin() - 1), ".*"); } @@ -893,7 +914,7 @@ TEST(StaticVector, erase_range_begin_before_start_segfaults) util::StaticVector, 3> vec{std::make_shared(1), std::make_shared(2), std::make_shared(3)}; - std::weak_ptr items[] = {vec[0], vec[1], vec[2]}; + std::array, 3> items = {vec[0], vec[1], vec[2]}; ASSERT_DEATH(vec.erase(vec.begin() - 1, vec.end()), ".*"); } @@ -903,7 +924,7 @@ TEST(StaticVector, erase_range_begin_after_end_segfaults) util::StaticVector, 3> vec{std::make_shared(1), std::make_shared(2), std::make_shared(3)}; - std::weak_ptr items[] = {vec[0], vec[1], vec[2]}; + std::array, 3> items = {vec[0], vec[1], vec[2]}; ASSERT_DEATH(vec.erase(vec.end() + 1, vec.end() + 1), ".*"); } @@ -913,7 +934,7 @@ TEST(StaticVector, erase_empty_range_noop) util::StaticVector, 3> vec{std::make_shared(1), std::make_shared(2), std::make_shared(3)}; - std::weak_ptr items[] = {vec[0], vec[1], vec[2]}; + std::array, 3> items = {vec[0], vec[1], vec[2]}; vec.erase(vec.begin(), vec.begin()); @@ -931,7 +952,7 @@ TEST(StaticVector, erase_empty_range_at_end_noop) util::StaticVector, 3> vec{std::make_shared(1), std::make_shared(2), std::make_shared(3)}; - std::weak_ptr items[] = {vec[0], vec[1], vec[2]}; + std::array, 3> items = {vec[0], vec[1], vec[2]}; vec.erase(vec.end(), vec.end()); @@ -949,7 +970,7 @@ TEST(StaticVector, erase_empty_range_at_end_returns_end) util::StaticVector, 3> vec{std::make_shared(1), std::make_shared(2), std::make_shared(3)}; - std::weak_ptr items[] = {vec[0], vec[1], vec[2]}; + std::array, 3> items = {vec[0], vec[1], vec[2]}; EXPECT_EQ(vec.end(), vec.erase(vec.end(), vec.end())); } @@ -959,7 +980,7 @@ TEST(StaticVector, erase_empty_range_returns_begin_range) util::StaticVector, 3> vec{std::make_shared(1), std::make_shared(2), std::make_shared(3)}; - std::weak_ptr items[] = {vec[0], vec[1], vec[2]}; + std::array, 3> items = {vec[0], vec[1], vec[2]}; EXPECT_EQ(vec.begin(), vec.erase(vec.begin(), vec.begin())); } @@ -969,7 +990,7 @@ TEST(StaticVector, erase_range_end_before_begin_segfaults) util::StaticVector, 3> vec{std::make_shared(1), std::make_shared(2), std::make_shared(3)}; - std::weak_ptr items[] = {vec[0], vec[1], vec[2]}; + std::array, 3> items = {vec[0], vec[1], vec[2]}; ASSERT_DEATH(vec.erase(vec.begin() + 2, vec.begin()), ".*"); } @@ -979,7 +1000,7 @@ TEST(StaticVector, erase_range_end_after_vector_end_segfaults) util::StaticVector, 3> vec{std::make_shared(1), std::make_shared(2), std::make_shared(3)}; - std::weak_ptr items[] = {vec[0], vec[1], vec[2]}; + std::array, 3> items = {vec[0], vec[1], vec[2]}; ASSERT_DEATH(vec.erase(vec.begin() + 2, vec.end() + 1), ".*"); } @@ -989,7 +1010,7 @@ TEST(StaticVector, erase_all_elements_empties_container) util::StaticVector, 3> vec{std::make_shared(1), std::make_shared(2), std::make_shared(3)}; - std::weak_ptr items[] = {vec[0], vec[1], vec[2]}; + std::array, 3> items = {vec[0], vec[1], vec[2]}; vec.erase(vec.begin(), vec.end()); @@ -1006,7 +1027,7 @@ TEST(StaticVector, erase_range_at_beginning_with_bigger_range_remaining) std::make_shared(3), std::make_shared(4), std::make_shared(5)}; - std::weak_ptr items[] = {vec[0], vec[1], vec[2], vec[3], vec[4]}; + std::array, 5> items = {vec[0], vec[1], vec[2], vec[3], vec[4]}; vec.erase(vec.begin(), vec.begin() + 2); @@ -1029,7 +1050,7 @@ TEST(StaticVector, erase_range_at_beginning_with_smaller_range_remaining) std::make_shared(3), std::make_shared(4), std::make_shared(5)}; - std::weak_ptr items[] = {vec[0], vec[1], vec[2], vec[3], vec[4]}; + std::array, 5> items = {vec[0], vec[1], vec[2], vec[3], vec[4]}; vec.erase(vec.begin(), vec.begin() + 3); @@ -1051,7 +1072,7 @@ TEST(StaticVector, erase_range_at_end) std::make_shared(3), std::make_shared(4), std::make_shared(5)}; - std::weak_ptr items[] = {vec[0], vec[1], vec[2], vec[3], vec[4]}; + std::array, 5> items = {vec[0], vec[1], vec[2], vec[3], vec[4]}; vec.erase(vec.end() - 2, vec.end()); @@ -1074,7 +1095,7 @@ TEST(StaticVector, erase_range_in_the_middle) std::make_shared(3), std::make_shared(4), std::make_shared(5)}; - std::weak_ptr items[] = {vec[0], vec[1], vec[2], vec[3], vec[4]}; + std::array, 5> items = {vec[0], vec[1], vec[2], vec[3], vec[4]}; vec.erase(vec.begin() + 1, vec.begin() + 3); @@ -1097,7 +1118,7 @@ TEST(StaticVector, erase_in_middle_return_iterator_to_next_element) Vector vec{std::make_shared(1), std::make_shared(2), std::make_shared(3)}; - std::weak_ptr items[] = {vec[0], vec[1], vec[2]}; + std::array, 3> items = {vec[0], vec[1], vec[2]}; Vector::iterator it = vec.erase(vec.begin(), vec.begin() + 2); @@ -1111,7 +1132,7 @@ TEST(StaticVector, erase_at_end_return_end_iterator) Vector vec{std::make_shared(1), std::make_shared(2), std::make_shared(3)}; - std::weak_ptr items[] = {vec[0], vec[1], vec[2]}; + std::array, 3> items = {vec[0], vec[1], vec[2]}; Vector::iterator it = vec.erase(vec.begin() + 2, vec.begin() + 3); @@ -1141,7 +1162,7 @@ TEST(StaticVector, reverse_iteration_works) using Vector = util::StaticVector; Vector v = {1, 2, 3, 4, 5}; - Vector::reverse_iterator it = v.rbegin(); + auto it = v.rbegin(); EXPECT_EQ(5, it[0]); EXPECT_EQ(4, it[1]); @@ -1182,7 +1203,7 @@ struct NonDefaultConstructible { NonDefaultConstructible() = delete; - NonDefaultConstructible(int d) + explicit NonDefaultConstructible(int d) : dummy(d) { } @@ -1200,7 +1221,7 @@ TEST(StaticVector, cant_increase_size_of_non_default_constructible_type_vector) TEST(StaticVector, can_decrease_size_of_non_default_constructible_type_vector) { util::StaticVector v; - ASSERT_NO_THROW(v.push_back(1)); + ASSERT_NO_THROW(v.push_back(NonDefaultConstructible{1})); ASSERT_EQ(1u, v.size()); EXPECT_NO_THROW(v.resize(0)); @@ -1210,7 +1231,7 @@ TEST(StaticVector, can_decrease_size_of_non_default_constructible_type_vector) TEST(StaticVector, can_add_non_default_constructible_type) { util::StaticVector v; - ASSERT_NO_THROW(v.push_back(1)); + ASSERT_NO_THROW(v.push_back(NonDefaultConstructible{1})); EXPECT_EQ(1u, v.size()); EXPECT_EQ(1, v[0].dummy); } @@ -1218,193 +1239,51 @@ TEST(StaticVector, can_add_non_default_constructible_type) TEST(StaticVector, can_erase_non_default_constructible_type) { util::StaticVector v; - ASSERT_NO_THROW(v.push_back(1)); + ASSERT_NO_THROW(v.push_back(NonDefaultConstructible{1})); EXPECT_NO_THROW(v.erase(v.begin())); EXPECT_EQ(0u, v.size()); } -struct NoThrowableMoveCtor -{ - // use the opposite exception spec to check whether it influences the result or not - NoThrowableMoveCtor(const NoThrowableMoveCtor &){}; - - NoThrowableMoveCtor &operator=(const NoThrowableMoveCtor &) - { - return *this; - } - - NoThrowableMoveCtor &operator=(NoThrowableMoveCtor &&) - { - return *this; - } - - NoThrowableMoveCtor(NoThrowableMoveCtor &&) noexcept {}; -}; - TEST(StaticVector, has_nothrow_move_ctor_when_type_has_it) { - EXPECT_TRUE((std::is_nothrow_move_constructible_v>)); + EXPECT_TRUE((std::is_nothrow_move_constructible_v, 5>>)); } -struct NoThrowableMoveAssign +TEST(StaticVector, has_nothrow_move_assign_when_element_move_ops_are_nothrow) { - // use the opposite exception spec to check whether it influences the result or not - NoThrowableMoveAssign(const NoThrowableMoveAssign &){}; - - NoThrowableMoveAssign &operator=(const NoThrowableMoveAssign &) - { - return *this; - } - - NoThrowableMoveAssign(NoThrowableMoveAssign &&){}; - - NoThrowableMoveAssign &operator=(NoThrowableMoveAssign &&) noexcept - { - return *this; - } -}; - -TEST(StaticVector, has_nothrow_move_assign_when_type_has_it) -{ - EXPECT_TRUE((std::is_nothrow_move_assignable_v>)); + EXPECT_TRUE((std::is_nothrow_move_assignable_v, 5>>)); } -struct ThrowableMoveCtor -{ - // use the opposite exception spec to check whether it influences the result or not - ThrowableMoveCtor(const ThrowableMoveCtor &) noexcept {}; - - ThrowableMoveCtor operator=(const ThrowableMoveCtor &) noexcept - { - return *this; - } - - ThrowableMoveCtor operator=(ThrowableMoveCtor &&) noexcept - { - return *this; - } - - ThrowableMoveCtor(ThrowableMoveCtor &&){}; -}; - -TEST(StaticVector, has_throwable_move_ctor_when_type_has_it) +TEST(StaticVector, has_nothrow_move_ctor_with_deque_elements) { - EXPECT_FALSE((std::is_nothrow_move_constructible_v>)); + EXPECT_TRUE((std::is_nothrow_move_constructible_v, 5>>)); } -struct ThrowableMoveAssign -{ - // use the opposite exception spec to check whether it influences the result or not - ThrowableMoveAssign(const ThrowableMoveAssign &) noexcept {}; - - ThrowableMoveAssign operator=(const ThrowableMoveAssign &) noexcept - { - return *this; - } - - ThrowableMoveAssign(ThrowableMoveAssign &&) noexcept {}; - - ThrowableMoveAssign operator=(ThrowableMoveAssign &&) - { - return *this; - } -}; - -TEST(StaticVector, has_throwable_move_assign_when_type_has_it) +TEST(StaticVector, has_nothrow_move_assign_with_throwable_element_move_ctor) { - EXPECT_FALSE((std::is_nothrow_move_assignable_v>)); + EXPECT_TRUE((std::is_nothrow_move_assignable_v, 5>>)); + EXPECT_TRUE((std::is_nothrow_move_assignable_v, 5>>)); } -struct NoThrowableCopyCtor -{ - // use the opposite exception spec to check whether it influences the result or not - NoThrowableCopyCtor(NoThrowableCopyCtor &&){}; - - NoThrowableCopyCtor &operator=(const NoThrowableCopyCtor &) - { - return *this; - } - - NoThrowableCopyCtor &operator=(NoThrowableCopyCtor &&) - { - return *this; - } - - NoThrowableCopyCtor(const NoThrowableCopyCtor &) noexcept {}; -}; - TEST(StaticVector, has_nothrow_copy_ctor_when_type_has_it) { - EXPECT_TRUE((std::is_nothrow_copy_constructible_v>)); + EXPECT_TRUE((std::is_nothrow_copy_constructible_v, 5>>)); } -struct NoThrowableCopyAssign -{ - // use the opposite exception spec to check whether it influences the result or not - NoThrowableCopyAssign(const NoThrowableCopyAssign &){}; - NoThrowableCopyAssign(NoThrowableCopyAssign &&){}; - - NoThrowableCopyAssign &operator=(NoThrowableCopyAssign &&) - { - return *this; - } - - NoThrowableCopyAssign &operator=(const NoThrowableCopyAssign &) noexcept - { - return *this; - } -}; - TEST(StaticVector, has_nothrow_copy_assign_when_type_has_it) { - EXPECT_TRUE((std::is_nothrow_copy_assignable_v>)); + EXPECT_TRUE((std::is_nothrow_copy_assignable_v, 5>>)); } -struct ThrowableCopyCtor -{ - // use the opposite exception spec to check whether it influences the result or not - ThrowableCopyCtor(ThrowableCopyCtor &&) noexcept {}; - - ThrowableCopyCtor operator=(const ThrowableCopyCtor &) noexcept - { - return *this; - } - - ThrowableCopyCtor operator=(ThrowableCopyCtor &&) noexcept - { - return *this; - } - - ThrowableCopyCtor(const ThrowableCopyCtor &){}; -}; - TEST(StaticVector, has_throwable_copy_ctor_when_type_has_it) { - EXPECT_FALSE((std::is_nothrow_copy_constructible_v>)); + EXPECT_FALSE((std::is_nothrow_copy_constructible_v>)); } -struct ThrowableCopyAssign -{ - // use the opposite exception spec to check whether it influences the result or not - ThrowableCopyAssign(const ThrowableCopyAssign &) noexcept {}; - - ThrowableCopyAssign operator=(ThrowableCopyAssign &) noexcept - { - return *this; - } - - ThrowableCopyAssign(ThrowableCopyAssign &&) noexcept {}; - - ThrowableCopyAssign operator=(const ThrowableCopyAssign &&) - { - return *this; - } -}; - TEST(StaticVector, has_throwable_copy_assign_when_type_has_it) { - EXPECT_FALSE((std::is_nothrow_copy_assignable_v>)); + EXPECT_FALSE((std::is_nothrow_copy_assignable_v>)); } TEST(StaticVector, construct_from_empty_range) @@ -1423,7 +1302,7 @@ TEST(StaticVector, construct_from_range) { util::StaticVector, 5> data{std::make_shared(1), std::make_shared(2)}; - std::weak_ptr wdata[2] = {data[0], data[1]}; + std::array, 2> wdata = {data[0], data[1]}; util::StaticVector, 2> v(data.begin(), data.end()); diff --git a/tests/nvcv_types/unit/TestString.cpp b/tests/nvcv_types/unit/TestString.cpp index b7f75cddb..4f27b89c7 100644 --- a/tests/nvcv_types/unit/TestString.cpp +++ b/tests/nvcv_types/unit/TestString.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,6 +21,10 @@ #include #include +#include +#include +#include + namespace util = nvcv::util; namespace test = nvcv::test; namespace t = ::testing; @@ -68,57 +72,97 @@ TEST_P(ReplaceAllInlineTests, test) const char *replace = GetParamValue<3>(); const char *gold = GetParamValue<4>(); - char buffer[256]; - // +1 for sentinel - NVCV_ASSERT(sizeof(buffer) + 1 >= strlen(input)); - NVCV_ASSERT(sizeof(buffer) + 1 >= strlen(gold)); + const std::string inputText{input}; + const std::string goldText{gold}; + + std::array buffer{}; + NVCV_ASSERT(buffer.size() > std::max(inputText.size(), goldText.size()) + 1); - strncpy(buffer, input, sizeof(buffer)); - char *sentinel = buffer + std::max(strlen(input), strlen(gold)) + 1; - *sentinel = '\xFF'; + std::ranges::copy(inputText, buffer.begin()); + buffer[inputText.size()] = '\0'; + char *sentinel = buffer.data() + std::max(inputText.size(), goldText.size()) + 1; + *sentinel = '\xFF'; - ASSERT_NO_THROW(util::ReplaceAllInline(buffer, bufSize, what, replace)); - EXPECT_STREQ(gold, buffer); + ASSERT_NO_THROW(util::ReplaceAllInline(buffer.data(), bufSize, what, replace)); + EXPECT_STREQ(gold, buffer.data()); EXPECT_EQ('\xFF', *sentinel) << "buffer overrun"; } TEST(BufferOStreamTests, is_zero_terminated_on_dtor) { - char buf[] = "rod"; + std::array buf = {"rod"}; { - util::BufferOStream str(buf, sizeof(buf)); + util::BufferOStream str(buf.data(), buf.size()); } EXPECT_EQ('\0', buf[0]); } TEST(BufferOStreamTests, is_flushed_on_dtor) { - char buf[] = "rod"; + std::array buf = {"rod"}; { - util::BufferOStream str(buf, sizeof(buf)); + util::BufferOStream str(buf.data(), buf.size()); str << 'x'; } - EXPECT_STREQ("x", buf); + EXPECT_STREQ("x", buf.data()); } TEST(BufferOStreamTests, data_is_written) { - char buf[] = "rod"; + std::array buf = {"rod"}; - util::BufferOStream str(buf, sizeof(buf)); + util::BufferOStream str(buf.data(), buf.size()); str << "123" << '\0' << std::flush; - EXPECT_STREQ("123", buf); + EXPECT_STREQ("123", buf.data()); } TEST(BufferOStreamTests, overflow) { - char buf[] = "rodlima"; + std::array buf = {"rodlima"}; - util::BufferOStream str(buf, sizeof(buf) - 1); + util::BufferOStream str(buf.data(), buf.size() - 1); str << "12345678\0" << std::flush; EXPECT_FALSE(str.good()); EXPECT_TRUE(str.fail()); - EXPECT_STREQ("1234567", buf); + EXPECT_STREQ("1234567", buf.data()); +} + +TEST(ReplaceAllInlineTests, unterminated_buffer_is_terminated) +{ + std::array buf = {'a', 'b', 'c', 'd'}; + + util::ReplaceAllInline(buf.data(), buf.size(), "missing", "replacement"); + + EXPECT_EQ((std::array{'a', 'b', 'c', '\0'}), buf); +} + +TEST(FixedBufferStreamBufTests, invalid_reset_and_seek) +{ + std::array buf{}; + util::FixedBufferStreamBuf streamBuf(nullptr, 0); + + EXPECT_EQ(std::streampos{std::streamoff{-1}}, streamBuf.pubseekpos(0, std::ios_base::out)); + + streamBuf.reset(buf.data(), buf.size()); + EXPECT_EQ(std::streampos{std::streamoff{-1}}, streamBuf.pubseekpos(0, std::ios_base::in)); + EXPECT_EQ(std::streampos{std::streamoff{-1}}, streamBuf.pubseekpos(-1, std::ios_base::out)); + EXPECT_EQ(std::streampos{std::streamoff{-1}}, streamBuf.pubseekpos(buf.size(), std::ios_base::out)); +} + +TEST(FixedBufferStreamBufTests, eof_overflow_is_not_an_error) +{ + class TestStreamBuf : public util::FixedBufferStreamBuf + { + public: + using FixedBufferStreamBuf::FixedBufferStreamBuf; + using FixedBufferStreamBuf::overflow; + }; + + std::array buf{}; + TestStreamBuf streamBuf(buf.data(), buf.size()); + + EXPECT_FALSE(TestStreamBuf::traits_type::eq_int_type(streamBuf.overflow(TestStreamBuf::traits_type::eof()), + TestStreamBuf::traits_type::eof())); } diff --git a/tests/nvcv_types/unit/TestTypeList.cpp b/tests/nvcv_types/unit/TestTypeList.cpp index 16b285bc9..68118c8c2 100644 --- a/tests/nvcv_types/unit/TestTypeList.cpp +++ b/tests/nvcv_types/unit/TestTypeList.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -37,8 +37,8 @@ TEST(TypeListTests, create_values) ASSERT_TRUE((std::is_same_v, test::Types<>>)); ASSERT_TRUE((std::is_same_v, test::Types>>)); ASSERT_TRUE((std::is_same_v, test::Types, test::Value<2>, test::Value<3>>>)); - ASSERT_TRUE((std::is_same_v, - test::Types, test::Value<2L>, test::Value<3ul>, test::Value<'b'>>>)); + ASSERT_TRUE((std::is_same_v, + test::Types, test::Value<2L>, test::Value<3UL>, test::Value<'b'>>>)); } TEST(TypeListTests, get_value) @@ -49,20 +49,20 @@ TEST(TypeListTests, get_value) ASSERT_TRUE((std::is_same_v, 1>), const int>)); ASSERT_EQ(2, (test::GetValue, 1>)); - ASSERT_TRUE((std::is_same_v, 0>), const int>)); - ASSERT_EQ(1, (test::GetValue, 0>)); + ASSERT_TRUE((std::is_same_v, 0>), const int>)); + ASSERT_EQ(1, (test::GetValue, 0>)); - ASSERT_TRUE((std::is_same_v, 1>), const char>)); - ASSERT_EQ('c', (test::GetValue, 1>)); + ASSERT_TRUE((std::is_same_v, 1>), const char>)); + ASSERT_EQ('c', (test::GetValue, 1>)); - // Using 5ul instead of 3ul to avoid gcc-7.0 bug below: + // Using 5UL instead of 3UL to avoid gcc-7.0 bug below: // Be aware that gcc-7.x has a bug where V's type will be // wrong if the value was already instantiated with another type. // gcc-8.0 fixes it. clang-6.0.0 doesn't have this bug. - // Ex: decltype(test::GetValue>) == 'const int' instead of 'const unsigned long' + // Ex: decltype(test::GetValue>) == 'const int' instead of 'const unsigned long' // Ref: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=79092 - ASSERT_TRUE((std::is_same_v, 2>), const unsigned long>)); - ASSERT_EQ(5ul, (test::GetValue, 2>)); + ASSERT_TRUE((std::is_same_v, 2>), const unsigned long>)); + ASSERT_EQ(5UL, (test::GetValue, 2>)); } TEST(TypeListTests, subset) @@ -309,7 +309,8 @@ TEST(TypeListTests, contains) { struct Foo { - int x, y; + int x; + int y; bool operator==(Foo that) const { @@ -399,10 +400,9 @@ TEST(TypeListTests, heterogeneous_but_implicitly_convertible_types_to_value_list b2 }; - using V = test::Values<4ul, 2, a1, b2, 'c'>; + using V = test::Values<4UL, 2, a1, b2, 'c'>; - test::ValueList gold - = {(unsigned long)4ul, (unsigned long)2, (unsigned long)a1, (unsigned long)b2, (unsigned long)'c'}; + test::ValueList gold = {4UL, 2, a1, b2, 'c'}; EXPECT_EQ(gold, test::ToValueList()); } diff --git a/tests/nvcv_types/unit/TestTypeTraits.cpp b/tests/nvcv_types/unit/TestTypeTraits.cpp index 5f4b781e6..eae3c973c 100644 --- a/tests/nvcv_types/unit/TestTypeTraits.cpp +++ b/tests/nvcv_types/unit/TestTypeTraits.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,8 +16,6 @@ */ #include -//#include "Definitions.hpp" - namespace d = nvcv::detail; static_assert(d::Conjunction<>::value, "Conjunction tail should evaluate to true"); diff --git a/tests/nvcv_types/unit/TestTypedTests.cpp b/tests/nvcv_types/unit/TestTypedTests.cpp index 7b92c631c..be1c120dd 100644 --- a/tests/nvcv_types/unit/TestTypedTests.cpp +++ b/tests/nvcv_types/unit/TestTypedTests.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -35,5 +35,5 @@ NVCV_TYPED_TEST_SUITE(TypedTest, test::type::Combine, test TYPED_TEST(TypedTest, test) { // For now we're concerned if typed tests will compile. - // TODO: How to test if the tests were correctly generated? + // REVISIT: How to test if the tests were correctly generated? } diff --git a/tests/nvcv_types/unit/TestValueList.cpp b/tests/nvcv_types/unit/TestValueList.cpp index f71082e71..8da7504bc 100644 --- a/tests/nvcv_types/unit/TestValueList.cpp +++ b/tests/nvcv_types/unit/TestValueList.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -33,29 +33,29 @@ TEST(JoinTupleTests, only_values) TEST(JoinTupleTests, all_tuples) { - std::tuple gold{1, 3.2f, 'a', 43l, 1, 3.2}; + std::tuple gold{1, 3.2f, 'a', 43L, 1, 3.2}; EXPECT_EQ(gold, - test::detail::JoinTuple(std::make_tuple(1, 3.2f), std::make_tuple('a'), std::make_tuple(43l, 1, 3.2))); + test::detail::JoinTuple(std::make_tuple(1, 3.2f), std::make_tuple('a'), std::make_tuple(43L, 1, 3.2))); } TEST(JoinTupleTests, tuples_and_values_value_first) { - std::tuple gold{1, 3.2f, 'a', 43l, 1, 3.2}; + std::tuple gold{1, 3.2f, 'a', 43L, 1, 3.2}; - EXPECT_EQ(gold, test::detail::JoinTuple(1, std::make_tuple(3.2f, 'a'), std::make_tuple(43l, 1, 3.2))); + EXPECT_EQ(gold, test::detail::JoinTuple(1, std::make_tuple(3.2f, 'a'), std::make_tuple(43L, 1, 3.2))); } TEST(JoinTupleTests, tuples_and_values_tuple_first) { - std::tuple gold{1, 3.2f, 'a', 43l, 1, 3.2}; + std::tuple gold{1, 3.2f, 'a', 43L, 1, 3.2}; - EXPECT_EQ(gold, test::detail::JoinTuple(std::make_tuple(1, 3.2f), 'a', std::make_tuple(43l, 1, 3.2))); + EXPECT_EQ(gold, test::detail::JoinTuple(std::make_tuple(1, 3.2f), 'a', std::make_tuple(43L, 1, 3.2))); } TEST(ExtractTupleTests, extract_first_element_of_tuple_with_many) { - std::tuple input{1, 3.2f, 'a', 43l, 1, 3.2}; + std::tuple input{1, 3.2f, 'a', 43L, 1, 3.2}; std::tuple gold{1}; EXPECT_EQ(gold, test::detail::ExtractTuple<0>(input)); @@ -63,7 +63,7 @@ TEST(ExtractTupleTests, extract_first_element_of_tuple_with_many) TEST(ExtractTupleTests, extract_middle_element_of_tuple_with_many) { - std::tuple input{1, 3.2f, 'a', 43l, 1, 3.2}; + std::tuple input{1, 3.2f, 'a', 43L, 1, 3.2}; std::tuple gold{'a'}; EXPECT_EQ(gold, test::detail::ExtractTuple<2>(input)); @@ -71,7 +71,7 @@ TEST(ExtractTupleTests, extract_middle_element_of_tuple_with_many) TEST(ExtractTupleTests, extract_last_element_of_tuple_with_many) { - std::tuple input{1, 3.2f, 'a', 43l, 1, 3.2}; + std::tuple input{1, 3.2f, 'a', 43L, 1, 3.2}; std::tuple gold{3.2}; EXPECT_EQ(gold, test::detail::ExtractTuple<5>(input)); @@ -79,7 +79,7 @@ TEST(ExtractTupleTests, extract_last_element_of_tuple_with_many) TEST(ExtractTupleTests, extract_first_few_elements_of_tuple_with_many) { - std::tuple input{1, 3.2f, 'a', 43l, 1, 3.2}; + std::tuple input{1, 3.2f, 'a', 43L, 1, 3.2}; std::tuple gold{1, 3.2f, 'a'}; EXPECT_EQ(gold, (test::detail::ExtractTuple<0, 1, 2>(input))); @@ -87,23 +87,23 @@ TEST(ExtractTupleTests, extract_first_few_elements_of_tuple_with_many) TEST(ExtractTupleTests, extract_middle_few_elements_of_tuple_with_many) { - std::tuple input{1, 3.2f, 'a', 43l, 1, 3.2}; - std::tuple gold{'a', 43l, 1}; + std::tuple input{1, 3.2f, 'a', 43L, 1, 3.2}; + std::tuple gold{'a', 43L, 1}; EXPECT_EQ(gold, (test::detail::ExtractTuple<2, 3, 4>(input))); } TEST(ExtractTupleTests, extract_last_few_elements_of_tuple_with_many) { - std::tuple input{1, 3.2f, 'a', 43l, 1, 3.2}; - std::tuple gold{43l, 1, 3.2}; + std::tuple input{1, 3.2f, 'a', 43L, 1, 3.2}; + std::tuple gold{43L, 1, 3.2}; EXPECT_EQ(gold, (test::detail::ExtractTuple<3, 4, 5>(input))); } TEST(ExtractTupleTests, extract_all_elements_of_tuple_with_many) { - std::tuple input{1, 3.2f, 'a', 43l, 1, 3.2}; + std::tuple input{1, 3.2f, 'a', 43L, 1, 3.2}; EXPECT_EQ(input, (test::detail::ExtractTuple<0, 1, 2, 3, 4, 5>(input))); } @@ -117,7 +117,7 @@ TEST(ExtractTupleTests, extract_element_of_tuple_with_one) TEST(ExtractTupleTests, extract_no_elements_of_tuple_with_many) { - std::tuple input{1, 3.2f, 'a', 43l, 1, 3.2}; + std::tuple input{1, 3.2f, 'a', 43L, 1, 3.2}; std::tuple<> gold; EXPECT_EQ(gold, test::detail::ExtractTuple<>(input)); @@ -141,7 +141,7 @@ TEST(ExtractTupleTests, extract_same_element_multiple_times_from_tuple_with_one) TEST(ExtractTupleTests, extract_same_element_multiple_times_from_tuple_with_many) { - std::tuple input{1, 3.2f, 'a', 43l, 1, 3.2}; + std::tuple input{1, 3.2f, 'a', 43L, 1, 3.2}; std::tuple gold{'a', 'a', 'a'}; EXPECT_EQ(gold, (test::detail::ExtractTuple<2, 2, 2>(input))); @@ -149,8 +149,8 @@ TEST(ExtractTupleTests, extract_same_element_multiple_times_from_tuple_with_many TEST(ExtractTupleTests, extract_multiple_elements_with_repetition_from_tuple_with_many) { - std::tuple input{1, 3.2f, 'a', 43l, 1, 3.2}; - std::tuple gold{'a', 43l, 'a'}; + std::tuple input{1, 3.2f, 'a', 43L, 1, 3.2}; + std::tuple gold{'a', 43L, 'a'}; EXPECT_EQ(gold, (test::detail::ExtractTuple<2, 3, 2>(input))); } @@ -356,11 +356,11 @@ TEST(ValueListTests, nested_parameters) for (int b1 : b) { - for (std::tuple a2 : aa) + for (const auto &[a20, a21] : aa) { - for (std::tuple b2 : bb) + for (const auto &[b20, b21] : bb) { - gold.emplace_back(b1, std::get<0>(a2), std::get<1>(a2), std::get<0>(b2), std::get<1>(b2)); + gold.emplace_back(b1, a20, a21, b20, b21); } } } @@ -1169,7 +1169,7 @@ TEST(ValueListTests, conversion_from_vector_multiple_parameters) TEST(ValueListTests, lists_are_not_sorted) { std::vector gold = {3, 1, 2, 7, 1}; - test::ValueList a = gold; + test::ValueList a(gold); EXPECT_THAT(a, t::ElementsAreArray(gold)); } @@ -1248,7 +1248,7 @@ TEST(ValueListTests, unique_sort_with_extractor) {7, 'c', 1.7} }; - auto extractor = [](int i, char c, float f) + auto extractor = [](int i, char, float f) { return std::make_tuple(i, f); }; @@ -1299,7 +1299,7 @@ TEST(ValueListTests, unique_sort_subset_complex_with_extractor_simple) {7, 'c', 1.7} }; - auto extractor = [](char c, float f) + auto extractor = [](char c, float) { return c; }; @@ -1429,9 +1429,9 @@ TEST(ValueListTests, extract_single_from_non_empty_homogeneous_multi_list) TEST(ValueListTests, extract_single_from_non_empty_heterogeneous_multi_list) { - test::ValueList list = {std::make_tuple(4, 2.1), std::make_tuple(4, 4.2), std::make_tuple(2, 4.3)}; + test::ValueList list = {std::make_tuple(4, 2.1f), std::make_tuple(4, 4.2f), std::make_tuple(2, 4.3f)}; test::ValueList a = {4, 4, 2}; - test::ValueList b = {2.1, 4.2, 4.3}; + test::ValueList b = {2.1f, 4.2f, 4.3f}; EXPECT_EQ(a, test::Extract<0>(list)); EXPECT_EQ(b, test::Extract<1>(list)); @@ -1627,7 +1627,7 @@ TEST(ValueListTests, make_struct) int a; float b; - bool operator==(const Foo &f) const + bool operator==(const Foo &f) const // NOSONAR: defaulted comparisons are C++20. { return a == f.a && b == f.b; }; @@ -1661,7 +1661,7 @@ TEST(ValueListTests, make_struct_implicit_ctor) int a; float b; - bool operator==(const Foo &f) const + bool operator==(const Foo &f) const // NOSONAR: defaulted comparisons are C++20. { return a == f.a && b == f.b; }; @@ -1674,9 +1674,9 @@ TEST(ValueListTests, make_struct_implicit_ctor) }; test::ValueList gold = { - Foo{1, 4.3}, - Foo{4, -2.4}, - Foo{5, 8} + Foo{1, 4.3f}, + Foo{4, -2.4f}, + Foo{5, 8} }; EXPECT_EQ(gold, test::Make(a)); @@ -1695,7 +1695,7 @@ TEST(ValueListTests, make_struct_explicit_ctor) int a; float b; - bool operator==(const Foo &f) const + bool operator==(const Foo &f) const // NOSONAR: defaulted comparisons are C++20. { return a == f.a && b == f.b; }; @@ -1729,7 +1729,7 @@ TEST(ValueListTests, make_optional_struct_implicit_ctor) int a; float b; - bool operator==(const Foo &f) const + bool operator==(const Foo &f) const // NOSONAR: defaulted comparisons are C++20. { return a == f.a && b == f.b; }; @@ -1763,7 +1763,7 @@ TEST(ValueListTests, make_optional_struct_explicit_ctor) int a; float b; - bool operator==(const Foo &f) const + bool operator==(const Foo &f) const // NOSONAR: defaulted comparisons are C++20. { return a == f.a && b == f.b; }; @@ -1791,7 +1791,7 @@ TEST(ValueListTests, make_optional_struct_no_ctor) int a; float b; - bool operator==(const Foo &f) const + bool operator==(const Foo &f) const // NOSONAR: defaulted comparisons are C++20. { return a == f.a && b == f.b; }; @@ -1834,11 +1834,11 @@ TEST(ValueListTests, or_works) EXPECT_FALSE(test::Or([](int a, int b) { return a == 4; }, [](int a, int b) { return b == 3; })(5, 6)); } -TEST(ValueListTests, implicit_conversion_different_types_multiple) +TEST(ValueListTests, conversion_different_types_multiple) { struct Foo { - Foo(int value_) + explicit Foo(int value_) : value(value_) { } @@ -1870,11 +1870,11 @@ TEST(ValueListTests, explicit_conversion_different_types_multiple) EXPECT_EQ('c', std::get<1>(*list.begin())); } -TEST(ValueListTests, implicit_conversion_different_types_single) +TEST(ValueListTests, conversion_different_types_single) { struct Foo { - Foo(int value_) + explicit Foo(int value_) : value(value_) { } @@ -1887,7 +1887,7 @@ TEST(ValueListTests, implicit_conversion_different_types_single) EXPECT_EQ(5, list.begin()->value); } -TEST(ValueListTests, default_ctor_implicit_conversion_different_types_single) +TEST(ValueListTests, default_ctor_conversion_different_types_single) { struct Foo { @@ -1896,7 +1896,7 @@ TEST(ValueListTests, default_ctor_implicit_conversion_different_types_single) { } - Foo(int value_) + explicit Foo(int value_) : value(value_) { } @@ -1952,12 +1952,9 @@ TEST(ValueListTests, create_with_default_parameters) { struct Foo { - Foo() - : value(123) - { - } + Foo() = default; - int value; + int value = 123; }; test::ValueList list{test::ValueDefault()}; @@ -1971,12 +1968,9 @@ TEST(ValueListTests, create_with_default_parameters_mixed) { struct Foo { - Foo() - : value(123) - { - } + Foo() = default; - int value; + int value = 123; }; test::ValueList list{5 * test::ValueDefault() * 'r'}; diff --git a/tests/nvcv_types/unit/TestValueTests.cpp b/tests/nvcv_types/unit/TestValueTests.cpp index 03f6b0c79..7cfcd34a1 100644 --- a/tests/nvcv_types/unit/TestValueTests.cpp +++ b/tests/nvcv_types/unit/TestValueTests.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -27,7 +27,7 @@ NVCV_TEST_SUITE_P(ValueTestsTests, test::ValueList{1, 2} * test::ValueList{'c', TEST_P(ValueTestsTests, test) { // For now we're concerned if typed tests will compile. - // TODO: How to test if the tests were correctly generated? + // REVISIT: How to test if the tests were correctly generated? int p1 = GetParamValue<0>(); char p2 = GetParamValue<1>(); @@ -38,7 +38,7 @@ TEST_P(ValueTestsTests, test) namespace { struct Foo { - Foo(int value_) + explicit Foo(int value_) : value(value_) { } @@ -50,7 +50,7 @@ struct Foo return value < that.value; } - bool operator==(const Foo &that) const + bool operator==(const Foo &that) const // NOSONAR: defaulted comparisons are C++20. { return value == that.value; } @@ -83,8 +83,8 @@ NVCV_INSTANTIATE_TEST_SUITE_P(_, ValueTestsNamedParameterTests, test::ValueList{ TEST_P(ValueTestsNamedParameterTests, test) { - int pi = std::get<0>(GetParam()); - char pc = std::get<1>(GetParam()); + int pi = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + char pc = ::nvcv::test::ParamValue(std::get<1>(GetParam())); EXPECT_THAT(pi, t::AnyOf(1, 2)); EXPECT_THAT(pc, t::AnyOf('a', 'b')); @@ -101,8 +101,8 @@ NVCV_INSTANTIATE_TEST_SUITE_P(_, ValueTestsNamedDefaultExplicitParameterTests, TEST_P(ValueTestsNamedDefaultExplicitParameterTests, test) { - int pi = std::get<0>(GetParam()); - char pc = std::get<1>(GetParam()); + int pi = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + char pc = ::nvcv::test::ParamValue(std::get<1>(GetParam())); EXPECT_THAT(pi, t::AnyOf(1, 2)); EXPECT_THAT(pc, 'c'); @@ -119,8 +119,8 @@ NVCV_INSTANTIATE_TEST_SUITE_P(_, ValueTestsNamedDefaultImplicitParameterTests, TEST_P(ValueTestsNamedDefaultImplicitParameterTests, test) { - int pi = std::get<0>(GetParam()); - char pc = std::get<1>(GetParam()); + int pi = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + char pc = ::nvcv::test::ParamValue(std::get<1>(GetParam())); EXPECT_THAT(pi, 0); EXPECT_THAT(pc, t::AnyOf('a', 'b')); @@ -131,12 +131,13 @@ class ValueTestsNamedNoDefaultParameterTests { }; -NVCV_INSTANTIATE_TEST_SUITE_P(_, ValueTestsNamedNoDefaultParameterTests, test::Value(123) * test::ValueList{'a', 'b'}); +NVCV_INSTANTIATE_TEST_SUITE_P(_, ValueTestsNamedNoDefaultParameterTests, + test::Value(Foo{123}) * test::ValueList{'a', 'b'}); TEST_P(ValueTestsNamedNoDefaultParameterTests, test) { - Foo pf = std::get<0>(GetParam()); - char pc = std::get<1>(GetParam()); + Foo pf = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + char pc = ::nvcv::test::ParamValue(std::get<1>(GetParam())); EXPECT_THAT(pf.value, 123); EXPECT_THAT(pc, t::AnyOf('a', 'b')); @@ -157,8 +158,8 @@ NVCV_INSTANTIATE_TEST_SUITE_P(_, ValueTestsInferParameterTypesTests, TEST_P(ValueTestsInferParameterTypesTests, test) { - Foo pf = std::get<0>(GetParam()); - char pc = std::get<1>(GetParam()); + Foo pf = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + char pc = ::nvcv::test::ParamValue(std::get<1>(GetParam())); test::ValueList params = { diff --git a/tests/nvcv_types/unit/TestVersion.cpp b/tests/nvcv_types/unit/TestVersion.cpp index 8b356a9e9..78086479b 100644 --- a/tests/nvcv_types/unit/TestVersion.cpp +++ b/tests/nvcv_types/unit/TestVersion.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -35,26 +35,50 @@ class VersionTests test::Param<"ctor_result", NVCVStatus>, // 5 test::Param<"values_result", NVCVStatus>>> // 6 { -public: - VersionTests() - : m_paramCode(std::get<0>(GetParam())) - , m_paramMajor(std::get<1>(GetParam())) - , m_paramMinor(std::get<2>(GetParam())) - , m_paramPatch(std::get<3>(GetParam())) - , m_paramTweak(std::get<4>(GetParam())) - , m_goldCtorResult(std::get<5>(GetParam())) - , m_goldValuesResult(std::get<6>(GetParam())) +protected: + uint32_t paramCode() const { + return m_paramCode; } -protected: - uint32_t m_paramCode; - int m_paramMajor; - int m_paramMinor; - int m_paramPatch; - int m_paramTweak; - NVCVStatus m_goldCtorResult; - NVCVStatus m_goldValuesResult; + int paramMajor() const + { + return m_paramMajor; + } + + int paramMinor() const + { + return m_paramMinor; + } + + int paramPatch() const + { + return m_paramPatch; + } + + int paramTweak() const + { + return m_paramTweak; + } + + NVCVStatus goldCtorResult() const + { + return m_goldCtorResult; + } + + NVCVStatus goldValuesResult() const + { + return m_goldValuesResult; + } + +private: + uint32_t m_paramCode = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + int m_paramMajor = ::nvcv::test::ParamValue(std::get<1>(GetParam())); + int m_paramMinor = ::nvcv::test::ParamValue(std::get<2>(GetParam())); + int m_paramPatch = ::nvcv::test::ParamValue(std::get<3>(GetParam())); + int m_paramTweak = ::nvcv::test::ParamValue(std::get<4>(GetParam())); + NVCVStatus m_goldCtorResult = ::nvcv::test::ParamValue(std::get<5>(GetParam())); + NVCVStatus m_goldValuesResult = ::nvcv::test::ParamValue(std::get<6>(GetParam())); }; // clang-format off @@ -104,35 +128,35 @@ TEST_P(VersionTests, code_to_version) // Code is always valid because any overflow will just make at most the major version // larger than expected. We can't check for that, it's still a valid version. - ASSERT_NO_THROW(ver = std::make_unique(m_paramCode)); + ASSERT_NO_THROW(ver = std::make_unique(paramCode())); - if (m_goldValuesResult == NVCV_SUCCESS) + if (goldValuesResult() == NVCV_SUCCESS) { ASSERT_NE(nullptr, ver); - EXPECT_EQ(ver->major(), m_paramMajor); - EXPECT_EQ(ver->minor(), m_paramMinor); - EXPECT_EQ(ver->patch(), m_paramPatch); - EXPECT_EQ(ver->tweak(), m_paramTweak); + EXPECT_EQ(ver->major(), paramMajor()); + EXPECT_EQ(ver->minor(), paramMinor()); + EXPECT_EQ(ver->patch(), paramPatch()); + EXPECT_EQ(ver->tweak(), paramTweak()); } - else if (ver && m_goldCtorResult == NVCV_SUCCESS) + else if (ver && goldCtorResult() == NVCV_SUCCESS) { - EXPECT_TRUE(ver->major() != m_paramMajor || ver->minor() != m_paramMinor || ver->patch() != m_paramPatch - || ver->tweak() != m_paramTweak); + EXPECT_TRUE(ver->major() != paramMajor() || ver->minor() != paramMinor() || ver->patch() != paramPatch() + || ver->tweak() != paramTweak()); } } TEST_P(VersionTests, version_to_code) { std::unique_ptr ver; - switch (m_goldValuesResult) + switch (goldValuesResult()) { case NVCV_SUCCESS: - ASSERT_NO_THROW(ver = std::make_unique(m_paramMajor, m_paramMinor, m_paramPatch, m_paramTweak)); + ASSERT_NO_THROW(ver = std::make_unique(paramMajor(), paramMinor(), paramPatch(), paramTweak())); break; case NVCV_ERROR_INVALID_ARGUMENT: - ASSERT_THROW(ver = std::make_unique(m_paramMajor, m_paramMinor, m_paramPatch, m_paramTweak), + ASSERT_THROW(ver = std::make_unique(paramMajor(), paramMinor(), paramPatch(), paramTweak()), std::invalid_argument); break; default: @@ -141,7 +165,7 @@ TEST_P(VersionTests, version_to_code) if (ver) { - EXPECT_EQ(m_paramCode, ver->code()); + EXPECT_EQ(paramCode(), ver->code()); } } @@ -152,22 +176,38 @@ class VersionStringTests test::Param<"tweak", int>, // 3 test::Param<"result", const char *>>> // 4 { -public: - VersionStringTests() - : m_paramMajor(std::get<0>(GetParam())) - , m_paramMinor(std::get<1>(GetParam())) - , m_paramPatch(std::get<2>(GetParam())) - , m_paramTweak(std::get<3>(GetParam())) - , m_goldResult(std::get<4>(GetParam())) +protected: + int paramMajor() const { + return m_paramMajor; } -protected: - int m_paramMajor; - int m_paramMinor; - int m_paramPatch; - int m_paramTweak; - const char *m_goldResult; + int paramMinor() const + { + return m_paramMinor; + } + + int paramPatch() const + { + return m_paramPatch; + } + + int paramTweak() const + { + return m_paramTweak; + } + + const char *goldResult() const + { + return m_goldResult; + } + +private: + int m_paramMajor = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + int m_paramMinor = ::nvcv::test::ParamValue(std::get<1>(GetParam())); + int m_paramPatch = ::nvcv::test::ParamValue(std::get<2>(GetParam())); + int m_paramTweak = ::nvcv::test::ParamValue(std::get<3>(GetParam())); + const char *m_goldResult = ::nvcv::test::ParamValue(std::get<4>(GetParam())); }; NVCV_INSTANTIATE_TEST_SUITE_P(Positive, VersionStringTests, @@ -181,12 +221,12 @@ NVCV_INSTANTIATE_TEST_SUITE_P(Positive, VersionStringTests, TEST_P(VersionStringTests, test) { - util::Version ver(m_paramMajor, m_paramMinor, m_paramPatch, m_paramTweak); + util::Version ver(paramMajor(), paramMinor(), paramPatch(), paramTweak()); std::ostringstream ss; ss << ver; - EXPECT_STREQ(m_goldResult, ss.str().c_str()); + EXPECT_STREQ(goldResult(), ss.str().c_str()); } class VersionComparisonTests @@ -194,17 +234,26 @@ class VersionComparisonTests test::Param<"rhs", util::Version, util::Version{0, 0, 0, 0}>, // 1 test::Param<"result", int>>> // 2 { -public: - VersionComparisonTests() - : m_paramLHS(std::get<0>(GetParam())) - , m_paramRHS(std::get<1>(GetParam())) - , m_goldResult(std::get<2>(GetParam())) +protected: + const util::Version ¶mLHS() const { + return m_paramLHS; } -protected: - util::Version m_paramLHS, m_paramRHS; - int m_goldResult; + const util::Version ¶mRHS() const + { + return m_paramRHS; + } + + int goldResult() const + { + return m_goldResult; + } + +private: + util::Version m_paramLHS = ::nvcv::test::ParamValue(std::get<0>(GetParam())); + util::Version m_paramRHS = ::nvcv::test::ParamValue(std::get<1>(GetParam())); + int m_goldResult = ::nvcv::test::ParamValue(std::get<2>(GetParam())); }; NVCV_INSTANTIATE_TEST_SUITE_P(Positive, VersionComparisonTests, @@ -216,30 +265,30 @@ NVCV_INSTANTIATE_TEST_SUITE_P(Positive, VersionComparisonTests, TEST_P(VersionComparisonTests, lower_than) { - EXPECT_EQ(m_goldResult < 0, m_paramLHS < m_paramRHS); + EXPECT_EQ(goldResult() < 0, paramLHS() < paramRHS()); } TEST_P(VersionComparisonTests, lower_equal_than) { - EXPECT_EQ(m_goldResult <= 0, m_paramLHS <= m_paramRHS); + EXPECT_EQ(goldResult() <= 0, paramLHS() <= paramRHS()); } TEST_P(VersionComparisonTests, equal_than) { - EXPECT_EQ(m_goldResult == 0, m_paramLHS == m_paramRHS); + EXPECT_EQ(goldResult() == 0, paramLHS() == paramRHS()); } TEST_P(VersionComparisonTests, not_equal_than) { - EXPECT_EQ(m_goldResult != 0, m_paramLHS != m_paramRHS); + EXPECT_EQ(goldResult() != 0, paramLHS() != paramRHS()); } TEST_P(VersionComparisonTests, greater_equal_than) { - EXPECT_EQ(m_goldResult >= 0, m_paramLHS >= m_paramRHS); + EXPECT_EQ(goldResult() >= 0, paramLHS() >= paramRHS()); } TEST_P(VersionComparisonTests, greater_than) { - EXPECT_EQ(m_goldResult > 0, m_paramLHS > m_paramRHS); + EXPECT_EQ(goldResult() > 0, paramLHS() > paramRHS()); } diff --git a/samples/run_interop.sh b/tests/requirements.tests.common.template old mode 100755 new mode 100644 similarity index 61% rename from samples/run_interop.sh rename to tests/requirements.tests.common.template index 068cd8f20..b11c4d86b --- a/samples/run_interop.sh +++ b/tests/requirements.tests.common.template @@ -1,6 +1,4 @@ -#!/bin/bash - -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,14 +13,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Get the directory where this script is located -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# Template for requirements.tests.common.txt — to regenerate, run from the repo root: +# bash generate_requirements.sh -# Source the shared functions from run_samples.sh -source "$SCRIPT_DIR/run_samples.sh" +# Python test dependencies for CV-CUDA +# - installed across Python versions (3.10-3.14) +# - numpy and torch are installed separately -# Run samples for each category -run_samples "Interoperability" "interoperability" -if [ $? -ne 0 ]; then - exit 1 -fi +pytest==${pytest} +typing-extensions>=${typing_extensions} diff --git a/bench/python/requirements_cu12.txt b/tests/requirements.tests.cu12.numpy1.template similarity index 59% rename from bench/python/requirements_cu12.txt rename to tests/requirements.tests.cu12.numpy1.template index c6533848a..639fa91e3 100644 --- a/bench/python/requirements_cu12.txt +++ b/tests/requirements.tests.cu12.numpy1.template @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,19 +13,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -# CUDA 12-specific Python packages for CV-CUDA benchmarking +# Template for requirements.tests.cu12.numpy1.txt — to regenerate, run from the repo root: +# bash generate_requirements.sh -# Include common requirements --r requirements_common.txt - -# PyTorch for CUDA 12 -torch==2.8.0 -torchvision==0.23.0 - -# CUDA 12-specific NVIDIA packages ---extra-index-url https://pypi.nvidia.com/ -nvidia-nvimgcodec-cu12==0.6.1.37 -nvidia-nvjpeg-cu12==12.4.0.76 - -# CV-CUDA for CUDA 12 -cvcuda-cu12 +# CUDA 12 test dependencies compatible with NumPy 1.x +cupy-cuda12x==${cupy_cu12_numpy1} +cuda-python==${cuda_python_cu12} diff --git a/.gitmodules b/tests/requirements.tests.cu12.template similarity index 56% rename from .gitmodules rename to tests/requirements.tests.cu12.template index 9de0bf303..3f1edf21d 100644 --- a/.gitmodules +++ b/tests/requirements.tests.cu12.template @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,16 +12,10 @@ # 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. -# -[submodule "python/pybind11"] - path = 3rdparty/pybind11 - url = https://github.com/pybind/pybind11.git -[submodule "3rdparty/googletest"] - path = 3rdparty/googletest - url = https://github.com/google/googletest.git -[submodule "3rdparty/dlpack"] - path = 3rdparty/dlpack - url = https://github.com/dmlc/dlpack.git -[submodule "3rdparty/nvbench"] - path = 3rdparty/nvbench - url = https://github.com/NVIDIA/nvbench.git + +# Template for requirements.tests.cu12.txt — to regenerate, run from the repo root: +# bash generate_requirements.sh + +# CUDA 12 test dependencies for NumPy 2.x +cupy-cuda12x==${cupy_cu12} +cuda-python==${cuda_python_cu12} diff --git a/tests/requirements.tests.cu13.template b/tests/requirements.tests.cu13.template new file mode 100644 index 000000000..181b305c3 --- /dev/null +++ b/tests/requirements.tests.cu13.template @@ -0,0 +1,21 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +# Template for requirements.tests.cu13.txt — to regenerate, run from the repo root: +# bash generate_requirements.sh + +# CUDA 13 test dependencies for NumPy 2.x +cupy-cuda13x==${cupy_cu13} +cuda-python==${cuda_python_cu13} diff --git a/docker/requirements.numpy1.txt b/tests/requirements.tests.numpy1.template similarity index 66% rename from docker/requirements.numpy1.txt rename to tests/requirements.tests.numpy1.template index 1d8b9c920..7128f3e62 100644 --- a/docker/requirements.numpy1.txt +++ b/tests/requirements.tests.numpy1.template @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,12 +13,14 @@ # See the License for the specific language governing permissions and # limitations under the License. +# Template for requirements.tests.numpy1.txt — to regenerate, run from the repo root: +# bash generate_requirements.sh # NumPy 1 Python packages for the Docker images, builder and devel -# - requirements are installed across Python versions (3.9-3.14) -# - using the same requirements for Manylinux builders, Ubuntu 22.04 and Ubuntu 24.04 +# - requirements are installed across Python versions (3.10-3.14) +# - using the same requirements for Manylinux builders, Ubuntu 22.04 and Ubuntu 26.04 # NumPy 1 with version constraints based on Python version and wheel availability -# - Python 3.9-3.12: NumPy 1.26.4 is the last stable 1.x release with precompiled wheels on x86_64/aarch64 +# - Python 3.10-3.12: NumPy ${numpy_v1} is the last stable 1.x release with precompiled wheels on x86_64/aarch64 # - Python 3.13+: NumPy 1.x does NOT support Python 3.13 or newer -numpy==1.26.4; python_version>="3.9" and python_version<"3.13" +numpy==${numpy_v1}; python_version>="3.10" and python_version<"3.13" diff --git a/docker/requirements.numpy2.txt b/tests/requirements.tests.numpy2.template similarity index 61% rename from docker/requirements.numpy2.txt rename to tests/requirements.tests.numpy2.template index 731ba18d1..a2729ff70 100644 --- a/docker/requirements.numpy2.txt +++ b/tests/requirements.tests.numpy2.template @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,15 +13,15 @@ # See the License for the specific language governing permissions and # limitations under the License. +# Template for requirements.tests.numpy2.txt — to regenerate, run from the repo root: +# bash generate_requirements.sh # NumPy 2 Python packages for the Docker images, builder and devel -# - requirements are installed across Python versions (3.9-3.14) -# - using the same requirements for Manylinux builders, Ubuntu 22.04 and Ubuntu 24.04 +# - requirements are installed across Python versions (3.10-3.14) +# - using the same requirements for Manylinux builders, Ubuntu 22.04 and Ubuntu 26.04 # NumPy 2 with version constraints based on Python version and wheel availability -# - Python 3.9: Limited to numpy 2.0.2 (last version with py3.9 precompiled wheels on x86_64/aarch64) -# - Python 3.10-3.13: Use numpy 2.2.6 (stable, has wheels for both architectures) -# - Python 3.14: Use latest numpy 2.3.3 -numpy==2.0.2; python_version=="3.9" -numpy==2.2.6; python_version>="3.10" and python_version<"3.14" -numpy==2.3.3; python_version=="3.14" +# - Python 3.10: Use numpy ${numpy_v2_py310} (stable, has wheels for both architectures) +# - Python 3.11+: Use latest numpy ${numpy_v2_py311plus} +numpy==${numpy_v2_py310}; python_version=="3.10" +numpy==${numpy_v2_py311plus}; python_version>="3.11" diff --git a/tests/run_tests.sh.in b/tests/run_tests.sh.in index f81e0e2d3..317ebd609 100755 --- a/tests/run_tests.sh.in +++ b/tests/run_tests.sh.in @@ -1,6 +1,6 @@ #!/bin/bash -e -# SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,6 +22,20 @@ test_set="all" curdir=$(dirname "$(readlink -f "$0")") failure_sets="" +# Some test binaries (notably nvcv_test_standalone, built via ExternalProject_Add +# and installed via install(PROGRAMS ...) which doesn't fix up RPATHs) carry a +# stale build-tree RPATH. When the artifact is consumed in a separate test pod, +# that path no longer exists. Pre-pending the install layout's parallel lib dir +# makes the dynamic linker find libnvcv_types.so / libcvcuda.so via the install +# layout instead. Harmless for binaries with proper INSTALL_RPATH. +# +# CMAKE_INSTALL_LIBDIR is "lib/${CMAKE_LIBRARY_ARCHITECTURE}" (e.g. +# lib/x86_64-linux-gnu, lib/aarch64-linux-gnu) — not plain lib. We add both the +# arch-specific dir (where libnvcv_types.so / libcvcuda.so actually live) and +# plain lib (defensive, for any builder that omits the arch suffix). +arch_triplet="$(uname -m)-linux-gnu" +export LD_LIBRARY_PATH="$curdir/../lib/$arch_triplet:$curdir/../lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" + if [[ $# -ge 1 ]]; then test_set=$1 fi diff --git a/tools/make_op.py b/tools/make_op.py new file mode 100755 index 000000000..f83511e2c --- /dev/null +++ b/tools/make_op.py @@ -0,0 +1,1425 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. +"""Deterministic checker for adding a NEW CV-CUDA operator (the /make-op skill). + +Implements the checklist in .agents/guidance/MAKE_OP_GUIDELINES.md. It does not author the +operator (that is the agent/human + tools/mkop/mkop.sh); it gates completeness, wiring, and +regression rigor against the operator's declared contract (its C-API header). Two phases: + + --phase scaffold : the wired skeleton — SPEC (approved contract) + SCF (files/wiring) + a + best-effort IMP stub scan. Run right after mkop.sh. + --phase done : the deterministic final regression checklist — composes review_op.py (all + four domains) + optimize_op.py preflight (RDY), then adds the make-op + teeth: IMP (no stubs), COV (declared-matrix mirror + gold + bit-exact + + parity + complement-negatives), DOC-REL (relnote), EXEC (--run tests pass). + +Composes, does not duplicate: review_op.py and optimize_op.py are imported / invoked. The static +checks are read-only and deterministic (no network, clocks, randomness); only --run executes. + +Usage: + python3 tools/make_op.py --phase scaffold|done + [--bare] [--format md|json] [--out PATH] [--run] +""" + +import argparse +import json +import re +import subprocess +import sys +from pathlib import Path + +REPO = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +import review_op # noqa: E402 (sibling module, same tools/ dir) + +MAKE_OP_GUIDE = ".agents/guidance/MAKE_OP_GUIDELINES.md" +PASS, GAP, NA, MANUAL, REC = ( + review_op.PASS, + review_op.GAP, + review_op.NA, + review_op.MANUAL, + review_op.REC, +) +Finding = review_op.Finding +read, rel, grep, first_evidence = ( + review_op.read, + review_op.rel, + review_op.grep, + review_op.first_evidence, +) + +# Phase-group ordering for rendering. +SCAFFOLD_DOMAINS = ("spec", "scaffold") +DONE_DOMAINS = ( + "spec", + "scaffold", + "implementation", + "coverage", + "support", + "test", + "bench", + "docs", + "execution", + "readiness", +) + +# canonical dtype -> regex detecting it in a C++ test (FMT_* / nvcv::TYPE_* / type-list tokens). +# Only run for *declared* dtypes, so unrelated tokens (e.g. a TYPE_S32 flip-code) never mislead. +DTYPE_TEST_RE = { + # The unsigned FMT_* patterns use a negative lookbehind so a signed token (FMT_S8 / FMT_S16) + # is NOT misread as unsigned coverage; the signed rows match the FMT_*S8 / FMT_*S16 forms too. + "u8": r"FMT_[A-Za-z]*(? (see make_operator.rst / OpCLAHE).", + ) + ) + else: + out.append( + Finding( + "IMP-3", + "implementation", + NA, + "No device allocation -> multi-GPU wrapper N-A", + "", + g, + ) + ) + + # NVTX-1 always-on NVTX submit marker present, per submit entry. The marker test + # (tests/cvcuda/python/test_nvtx_markers.py) has no registry to edit: it scans every Op*.h + # submit declaration for a CVCUDA_DEFINE_API definition whose body opens with the matching + # CVCUDA_NVTX_RANGE (and every Python binding for its NvtxTrace). Mirror the per-submit + # source check here so a GAP surfaces before that test fails. + htext_nvtx = read(P.header) or "" + capi_txt = blobs["capi"] or "" + declared_submits = re.findall( + rf"CVCUDA_PUBLIC\s+NVCVStatus\s+(cvcuda{re.escape(P.Op)}\w*Submit)\s*\(", + htext_nvtx, + ) + + def _submit_definition_marked(name): + # Definition-scoped, like the test: every CVCUDA_DEFINE_API definition of this + # submit must open its body with the matching range as the first statement. A + # marker elsewhere in the file (or a missing definition) does not count. + definitions = list( + re.finditer( + rf"CVCUDA_DEFINE_API\(\s*[^,]+,\s*[^,]+,\s*NVCVStatus\s*,\s*{re.escape(name)}\s*,", + capi_txt, + ) + ) + entry_range = re.compile( + rf'\s*CVCUDA_NVTX_RANGE\(\s*"{re.escape(name)}"\s*\)\s*;' + ) + if not definitions: + return False + for match in definitions: + body_start = capi_txt.find("{", match.end()) + if body_start < 0 or not entry_range.match(capi_txt, body_start + 1): + return False + return True + + unmarked_submits = [ + name for name in declared_submits if not _submit_definition_marked(name) + ] + marker_hits = grep( + rf'CVCUDA_NVTX_RANGE\("cvcuda{re.escape(P.Op)}\w*Submit"', blobs["capi"] + ) + nvtx_ok = bool(declared_submits) and not unmarked_submits + if nvtx_ok: + nvtx_msg = f"all {len(declared_submits)} submit entries carry their matching NVTX range" + nvtx_ev = first_evidence(X["capi_impl"], marker_hits) + elif not declared_submits: + nvtx_msg = f"no cvcuda{P.Op}...Submit declaration found in the public header" + nvtx_ev = rel(P.header) + else: + nvtx_msg = ( + "submit entries missing their matching CVCUDA_NVTX_RANGE: " + + ", ".join(unmarked_submits) + ) + nvtx_ev = rel(X["capi_impl"]) + out.append( + Finding( + "NVTX-1", + "implementation", + PASS if nvtx_ok else GAP, + nvtx_msg, + nvtx_ev, + g, + "" + if nvtx_ok + else ( + f'Open every cvcuda{P.Op}*Submit definition body with CVCUDA_NVTX_RANGE("") (the scaffold template emits it); ' + "tests/cvcuda/python/test_nvtx_markers.py enforces the same by scanning sources." + ), + ) + ) + return out + + +# ============================================================================ COVERAGE domain +def check_coverage(P, support_info): + out = [] + g = ".agents/guidance/MAKE_OP_GUIDELINES.md#coverage" + t = read(P.test_cpp) or "" + lim = support_info.get("limitations") or {} + declared_dtypes = lim.get("dtypes") or set() + declared_channels = lim.get("channels") or set() + has_vs = support_info.get("has_vs") + planar_declared = support_info.get("planar_declared") + planar_not_applicable = support_info.get("planar_not_applicable", False) + planar_reason = support_info.get("planar_reason", "") + + # COV-GOLD + gold = grep(r"Gold|gold|[Rr]eference|naive|CPU[Rr]ef|RefImpl", t) + out.append( + Finding( + "COV-GOLD", + "coverage", + PASS if gold else GAP, + "Independent CPU gold reference present" + if gold + else "No CPU gold reference found", + first_evidence(P.test_cpp, gold) or "no Gold/Reference/naive symbol", + g, + "" + if gold + else "Add an independent CPU gold reference implementing the cited oracle.", + ) + ) + + # COV-BITEXACT + near = grep(r"EXPECT_NEAR|ASSERT_NEAR", t) + eq = grep(r"EXPECT_EQ|ASSERT_EQ", t) + if near: + out.append( + Finding( + "COV-BITEXACT", + "coverage", + GAP, + f"{len(near)} EXPECT_NEAR/ASSERT_NEAR site(s) - bit-exact is the default", + first_evidence(P.test_cpp, near), + g, + "Make the gold bit-exact and use EXPECT_EQ/ASSERT_EQ; never loosen tolerance silently.", + ) + ) + else: + out.append( + Finding( + "COV-BITEXACT", + "coverage", + PASS if eq else GAP, + "Bit-exact comparison (EXPECT_EQ, no NEAR)" + if eq + else "No bit-exact comparison found", + first_evidence(P.test_cpp, eq) or "no EXPECT_EQ/ASSERT_EQ", + g, + "" if eq else "Compare gold vs result with EXPECT_EQ/ASSERT_EQ.", + ) + ) + + # COV-1 declared dtypes tested + if not declared_dtypes: + out.append( + Finding( + "COV-1", + "coverage", + MANUAL, + "No declared dtype matrix parsed -> verify dtype coverage", + rel(P.header), + g, + ) + ) + else: + missing = [ + d + for d in sorted(declared_dtypes) + if not re.search(DTYPE_TEST_RE.get(d, d), t) + ] + out.append( + Finding( + "COV-1", + "coverage", + PASS if not missing else GAP, + "Every declared dtype tested" + if not missing + else "Declared dtype(s) not tested: " + ", ".join(missing), + f"declared={sorted(declared_dtypes)}", + g, + "" + if not missing + else "Add a positive interleaved test case for each missing dtype.", + ) + ) + + # COV-CHAN declared channels tested + if not declared_channels: + out.append( + Finding( + "COV-CHAN", + "coverage", + MANUAL, + "No declared channel set parsed -> verify channel coverage", + rel(P.header), + g, + ) + ) + else: + tested_ch = fmt_channels(t) + missing_ch = sorted(c for c in declared_channels if c not in tested_ch) + out.append( + Finding( + "COV-CHAN", + "coverage", + PASS if not missing_ch else GAP, + "Every declared channel-count tested" + if not missing_ch + else "Declared channel(s) not tested: " + + ", ".join(map(str, missing_ch)), + f"declared={sorted(declared_channels)} tested={sorted(tested_ch)}", + g, + "" + if not missing_ch + else "Add a positive test case covering each missing channel count.", + ) + ) + + # COV-2 declared dtypes benched + cfg = review_op.load_bench_cfg(P.bench_cfg) or {} + benched = set() + for c in (cfg.get("configs") or {}).values(): + for d in c.get("dtypes", []): + canon = bench_dtype_canon(d) + if canon: + benched.add(canon) + if not declared_dtypes: + out.append( + Finding( + "COV-2", + "coverage", + MANUAL, + "No declared dtype matrix -> verify bench dtype coverage", + rel(P.bench_cfg), + g, + ) + ) + else: + missing_b = [d for d in sorted(declared_dtypes) if d not in benched] + out.append( + Finding( + "COV-2", + "coverage", + PASS if not missing_b else GAP, + "Every declared dtype benched" + if not missing_b + else "Declared dtype(s) not benched: " + ", ".join(missing_b), + f"benched={sorted(benched)}", + g, + "" + if not missing_b + else "Add bench config dtypes covering each missing dtype.", + ) + ) + + # BEN-DRV: the bench *drivers* must be implemented, not the mkop NHWC-only stub. The structural + # BEN-* checks (files/config/baselines) pass on a skip-everything stub, so this guards that the + # C++ and Python drivers actually exercise the declared layouts/containers (no TODO(make-op) / + # planar+varshape state.skip stubs). + stub_drivers = [] + for label, path in (("C++", P.bench_cpp), ("Python", P.bench_py)): + txt = read(path) + if txt is None: + continue # absence is already a GAP in BEN-1/2 + if re.search(r"TODO\(make-op\)", txt): + stub_drivers.append(label) + out.append( + Finding( + "BEN-DRV", + "coverage", + PASS if not stub_drivers else GAP, + "Bench drivers implemented (not the NHWC-only stub)" + if not stub_drivers + else "Bench driver is still a stub: " + ", ".join(stub_drivers), + "no TODO(make-op) markers in bench drivers" + if not stub_drivers + else f"TODO(make-op) stub in {', '.join(stub_drivers)} driver", + g, + "" + if not stub_drivers + else "Implement the bench driver(s) to exercise the declared layouts (NHWC/NCHW/" + "NCHW_FAKE) and containers (Tensor/VarShape); see BenchFlip.cpp / bench_flip.py.", + ) + ) + + # COV-3 containers tested + benched + tensor_test = bool(grep(rf"tensor_correct|TEST(_P)?\(\s*Op{re.escape(P.Op)}\b", t)) + benched_kinds = set() + for c in (cfg.get("configs") or {}).values(): + benched_kinds.update(c.get("string_axes", {}).get("inputKind", [])) + missing_c = [] + if not tensor_test: + missing_c.append("Tensor positive test") + if "Tensor" not in benched_kinds: + missing_c.append("Tensor bench") + if has_vs: + if not grep(r"[Vv]ar[Ss]hape.*correct|varshape_correct|ImageBatchVarShape", t): + missing_c.append("VarShape positive test") + if "VarShape" not in benched_kinds: + missing_c.append("VarShape bench") + out.append( + Finding( + "COV-3", + "coverage", + PASS if not missing_c else GAP, + "Declared containers tested + benched" + if not missing_c + else "Container coverage gaps: " + ", ".join(missing_c), + f"tensor_test={tensor_test} has_vs={has_vs} benched_kinds={sorted(benched_kinds)}", + g, + "" if not missing_c else "Add the missing container test/bench coverage.", + ) + ) + + # COV-PARITY + COV-5 (image layouts are complete by default) + parity = bool(grep(r"PlanarParityUtils|matches_interleaved", t)) or bool( + grep(r"[Rr]eformat", t) + and grep(r"NCHW|planar|Planar", t) + and grep(r"EXPECT_EQ|ASSERT_EQ", t) + ) + bench_layouts = set() + for c in (cfg.get("configs") or {}).values(): + bench_layouts.update(c.get("string_axes", {}).get("layout", [])) + if planar_not_applicable: + out.append( + Finding( + "COV-PARITY", + "coverage", + NA, + "Planar image layouts are not applicable -> parity N-A", + planar_reason, + MAKE_OP_GUIDE, + ) + ) + out.append( + Finding( + "COV-5", + "coverage", + NA, + "Planar image layouts are not applicable", + planar_reason, + MAKE_OP_GUIDE, + ) + ) + else: + out.append( + Finding( + "COV-PARITY", + "coverage", + PASS if parity else GAP, + "Equivalent image-layout parity test present" + if parity + else "Equivalent image-layout parity test MISSING", + first_evidence( + P.test_cpp, grep(r"PlanarParityUtils|matches_interleaved", t) + ) + or "no parity helper / reformat+EQ", + MAKE_OP_GUIDE, + "" + if parity + else "Add bit-exact planar == reformat->interleaved-op->reformat parity.", + ) + ) + miss5 = [] + if not planar_declared: + miss5.append("planar not declared in header") + if not parity: + miss5.append("no equivalent image-layout parity test") + if "NCHW" not in bench_layouts: + miss5.append("no NCHW bench") + if "NCHW_FAKE" not in bench_layouts: + miss5.append("no NCHW_FAKE bench") + out.append( + Finding( + "COV-5", + "coverage", + PASS if not miss5 else GAP, + "Image-layout coverage complete (declared+parity+bench)" + if not miss5 + else "Image-layout coverage incomplete: " + "; ".join(miss5), + f"declared={planar_declared} parity={parity} layouts={sorted(bench_layouts)}", + MAKE_OP_GUIDE, + "" + if not miss5 + else ( + "Add complete planar layout coverage or declare in the C-API header why " + "planar image layouts are not applicable." + ), + ) + ) + + # COV-NEG complement + neg_suite = bool(grep(rf"Op{re.escape(P.Op)}_Negative|_Negative", t)) + neg_err = bool(grep(r"ERROR_INVALID_ARGUMENT", t)) + neg_ok = neg_suite and neg_err + out.append( + Finding( + "COV-NEG", + "coverage", + PASS if neg_ok else GAP, + "Complement negative suite present (rejects unsupported inputs)" + if neg_ok + else "Negative/complement coverage MISSING", + f"negative_suite={neg_suite} error_invalid_argument={neg_err}", + g, + "" + if neg_ok + else "Add a _Negative suite asserting NVCV_ERROR_INVALID_ARGUMENT for " + "unsupported dtype/layout/channel + in/out mismatch.", + ) + ) + out.append( + Finding( + "COV-MATRIX", + "coverage", + MANUAL, + "Per-dtype x per-container exhaustiveness (each declared variant has its own bit-exact case)", + f"inspect {rel(P.test_cpp)}", + g, + ) + ) + + # DOC-REL relnote + out.append(check_doc_rel(P)) + return out + + +def check_doc_rel(P): + g = ".agents/guidance/MAKE_OP_GUIDELINES.md#docs (DOC-REL)" + relnote = latest_relnote() + if relnote is None: + return Finding( + "DOC-REL", + "docs", + MANUAL, + "Could not resolve the latest relnote from CMakeLists VERSION", + "", + g, + ) + txt = read(relnote) or "" + hit = re.search(rf"``{re.escape(P.Op)}``|\b{re.escape(P.Op)}\b", txt) + return Finding( + "DOC-REL", + "docs", + PASS if hit else GAP, + f"Operator in the latest relnote ({rel(relnote)})" + if hit + else f"Operator MISSING from the latest relnote ({rel(relnote)})", + rel(relnote), + g, + "" if hit else f"Add a New-Features bullet for {P.Op} to {rel(relnote)}.", + ) + + +# =========================================================================== EXECUTION domain +def find_build_dir(): + for name in ("build-rel", "build", "build-debug"): + d = REPO / name + if (d / "CMakeCache.txt").exists(): + return d + for d in sorted(REPO.glob("build*")): + if (d / "CMakeCache.txt").exists(): + return d + return None + + +def check_exec(P, do_run): + g = ".agents/guidance/MAKE_OP_GUIDELINES.md#execution" + if not do_run: + return [ + Finding( + "EXEC-1", + "execution", + MANUAL, + "C++ tests not executed (pass --run to build+run+assert pass)", + "", + g, + ), + Finding( + "EXEC-2", + "execution", + MANUAL, + "Python tests not executed (pass --run)", + "", + g, + ), + ] + out = [] + bd = find_build_dir() + if bd is None: + out.append( + Finding( + "EXEC-1", + "execution", + GAP, + "No configured build dir found (build-rel/build); cannot run tests", + "configure the project first", + g, + "Configure & build, then re-run --run; else defer to CI.", + ) + ) + out.append( + Finding( + "EXEC-2", + "execution", + GAP, + "No configured build dir found; cannot run Python tests", + "", + g, + "Build the Python module, then re-run --run; else defer to CI.", + ) + ) + return out + # C++: build the system test target and run this op's filter + try: + b = subprocess.run( + ["cmake", "--build", str(bd), "--target", "cvcuda_test_system"], + capture_output=True, + text=True, + cwd=str(REPO), + ) + if b.returncode != 0: + out.append( + Finding( + "EXEC-1", + "execution", + GAP, + "C++ system-test build failed", + (b.stderr or b.stdout)[-300:], + g, + "Fix the build.", + ) + ) + else: + binp = bd / "bin" / "cvcuda_test_system" + r = subprocess.run( + [str(binp), f"--gtest_filter=Op{P.Op}*:*{P.Op}*"], + capture_output=True, + text=True, + cwd=str(REPO), + ) + ok = r.returncode == 0 and "FAILED" not in r.stdout + out.append( + Finding( + "EXEC-1", + "execution", + PASS if ok else GAP, + "C++ tests pass" if ok else "C++ tests failed", + (r.stdout or "")[-300:], + g, + "" if ok else "Fix failing C++ tests.", + ) + ) + except (OSError, subprocess.SubprocessError) as e: + out.append( + Finding( + "EXEC-1", "execution", MANUAL, f"Could not run C++ tests ({e})", "", g + ) + ) + # Python + try: + r = subprocess.run( + [sys.executable, "-m", "pytest", str(P.test_py), "-q"], + capture_output=True, + text=True, + cwd=str(REPO), + ) + ok = r.returncode == 0 + out.append( + Finding( + "EXEC-2", + "execution", + PASS if ok else GAP, + "Python tests pass" if ok else "Python tests failed", + (r.stdout or "")[-300:], + g, + "" + if ok + else "Fix failing Python tests (needs the built module on PYTHONPATH).", + ) + ) + except (OSError, subprocess.SubprocessError) as e: + out.append( + Finding( + "EXEC-2", + "execution", + MANUAL, + f"Could not run Python tests ({e})", + "", + g, + ) + ) + return out + + +# =========================================================================== READINESS domain +def check_readiness(P, do_run): + g = ".agents/guidance/MAKE_OP_GUIDELINES.md#readiness / .agents/guidance/OPTIMIZATION_GUIDELINES.md" + opt = Path(__file__).resolve().parent / "optimize_op.py" + if not opt.exists(): + return [ + Finding( + "RDY-1", + "readiness", + MANUAL, + "optimize_op.py not found; run preflight manually", + "", + g, + ) + ] + try: + r = subprocess.run( + [ + sys.executable, + str(opt), + P.Op, + "--phase", + "preflight", + "--format", + "json", + ], + capture_output=True, + text=True, + cwd=str(REPO), + ) + except (OSError, subprocess.SubprocessError) as e: + return [ + Finding( + "RDY-1", + "readiness", + MANUAL, + f"Could not run optimize_op preflight ({e})", + "", + g, + ) + ] + gaps = None + try: + data = json.loads(r.stdout) + gaps = data.get("counts", {}).get(GAP, data.get("exit_gap")) + except (json.JSONDecodeError, AttributeError): + pass + ok = r.returncode == 0 + summary = ( + "Optimization-readiness (optimize_op preflight) green" + if ok + else "Optimization-readiness not met (optimize_op preflight)" + ) + evidence = f"preflight exit={r.returncode}" + ( + f", GAPs={gaps}" if gaps is not None else "" + ) + return [ + Finding( + "RDY-1", + "readiness", + PASS if ok else GAP, + summary, + evidence, + g, + "" + if ok + else "Resolve preflight GAPs (bench coverage, captured baseline [requires CI], profiling).", + ) + ] + + +# ================================================================================= driver +def check_bench_guidelines(P): + """BEN-GUIDE: run the static RGB-benchmark-guideline tests the CI burn-in legs run. + + These (bench/tests/test_bench_rgb_guidelines.py + test_run_bench_config_key.py) are + static (no GPU/build) and gate the burn-in *before* any benchmark runs, so a config + that is unclassified in operator_categories.json, mis-tiered (Cat-A scalar in basic), + or that drifts the tripwire row/key counts fails the baseline regen silently. Running + them here catches it locally instead of after a ~1 h CI fan-out. + """ + g = ".agents/guidance/MAKE_OP_GUIDELINES.md#coverage (BEN-GUIDE)" + tests = [ + REPO / "bench" / "tests" / "test_bench_rgb_guidelines.py", + REPO / "bench" / "tests" / "test_run_bench_config_key.py", + ] + present = [t for t in tests if t.exists()] + missing = [t for t in tests if not t.exists()] + if missing: + return [ + Finding( + "BEN-GUIDE", + "coverage", + GAP, + "Bench guideline tests missing", + ", ".join(rel(t) for t in missing), + g, + "Restore the missing bench guideline test file(s).", + ) + ] + try: + r = subprocess.run( + [sys.executable, "-m", "pytest", *[str(t) for t in present], "-q"], + capture_output=True, + text=True, + cwd=str(REPO / "bench"), + ) + except (OSError, subprocess.SubprocessError) as e: + return [ + Finding( + "BEN-GUIDE", + "coverage", + MANUAL, + f"Could not run bench guideline tests ({e})", + "", + g, + ) + ] + ok = r.returncode == 0 + # Keep evidence deterministic (no pytest timing line) so the static report is byte-identical + # on re-run: only the "FAILED - " lines, which carry no clocks. + failed = "; ".join( + ln.strip() for ln in (r.stdout or "").splitlines() if ln.startswith("FAILED ") + ) + return [ + Finding( + "BEN-GUIDE", + "coverage", + PASS if ok else GAP, + "Bench RGB-guideline tests pass" + if ok + else "Bench RGB-guideline tests fail (would fail the CI baseline burn-in)", + "" if ok else (failed or "see `pytest bench/tests/` output"), + g, + "" + if ok + else "Classify the op in bench/config/operator_categories.json (A/B/C), make a Cat-A " + "config RGB-only in basic with single-channel/RGBA in advanced (R2/R3/R4), and bump the " + "tripwire key/row counts in bench/tests/test_run_bench_config_key.py.", + ) + ] + + +def run_scaffold(P, X, bare): + # Scaffold phase is structural-only: it gates wiring (SCF) + the approved contract (SPEC). + # Implementation/coverage/bench-guideline checks belong to --phase done, so a freshly + # scaffolded (not-yet-implemented) op stays scaffold-green with IMP/COV/EXEC outstanding. + return check_spec(P, bare) + check_scaffold(P, X) + + +def run_done(P, X, bare, do_run): + findings = [] + curated = review_op.load_curated() + sup, support_info = review_op.check_support(P, curated) + # make-op-specific groups + findings += check_spec(P, bare) + findings += check_scaffold(P, X) + findings += check_impl(P, X) + findings += check_coverage(P, support_info) + findings += check_bench_guidelines(P) + # reuse review_op for the four standard domains (support already computed) + findings += sup + findings += review_op.check_test(P, support_info) + findings += review_op.check_bench(P, support_info, do_run) + findings += review_op.check_docs(P, support_info) + # execution + readiness + findings += check_exec(P, do_run) + findings += check_readiness(P, do_run) + return findings + + +def render_md(P, findings, domains, phase): + icon = {PASS: "✅", GAP: "❌", NA: "➖", MANUAL: "🔍", REC: "💡"} + lines = [f"# make-op ({phase}): {P.Op} (op={P.op}, py=cvcuda.{P.pyname})", ""] + counts = {} + seen = set() + for dom in domains: + df = [f for f in findings if f.domain == dom] + if not df: + continue + lines.append(f"## {dom}") + for f in df: + counts[f.status] = counts.get(f.status, 0) + 1 + seen.add(id(f)) + lines.append( + f"- {icon.get(f.status, '?')} **{f.status}** `{f.id}` — {f.summary}" + ) + if f.evidence: + lines.append(f" - evidence: {f.evidence}") + if f.status == GAP and f.fix: + lines.append(f" - fix: {f.fix}") + if f.guideline: + lines.append(f" - ref: {f.guideline}") + gaps = sum(1 for f in df if f.status == GAP) + man = sum(1 for f in df if f.status == MANUAL) + verdict = ( + "PASS" if gaps == 0 and man == 0 else ("GAPS" if gaps else "NEEDS-REVIEW") + ) + lines.append(f" → **{dom} verdict: {verdict}** ({gaps} GAP, {man} MANUAL)") + lines.append("") + total_gap = counts.get(GAP, 0) + total_man = counts.get(MANUAL, 0) + overall = ( + "PASS" + if total_gap == 0 and total_man == 0 + else ("GAPS" if total_gap else "NEEDS-REVIEW") + ) + lines.append("## verdict") + lines.append( + f"**{overall}** — " + ", ".join(f"{k}={v}" for k, v in sorted(counts.items())) + ) + if phase == "scaffold": + lines.append("") + lines.append( + "Scaffold-only: implementation outstanding (IMP/COV/EXEC). Run `--phase done` after implementing." + ) + else: + lines.append("") + lines.append( + "Done = a re-run (with --run + CI baselines) shows zero GAP and zero unresolved MANUAL." + ) + return "\n".join(lines) + + +def render_json(P, findings, domains, phase): + counts = {} + for f in findings: + counts[f.status] = counts.get(f.status, 0) + 1 + return json.dumps( + { + "operator": P.Op, + "op": P.op, + "pyname": P.pyname, + "phase": phase, + "findings": [vars(f) for f in findings], + "counts": counts, + "exit_gap": counts.get(GAP, 0), + }, + indent=2, + sort_keys=True, + ) + + +def main(argv=None): + ap = argparse.ArgumentParser( + description="Deterministic new-operator checker (CV-CUDA /make-op)." + ) + ap.add_argument("operator", help="Operator name (PascalCase, e.g. Invert)") + ap.add_argument("--phase", required=True, choices=["scaffold", "done"]) + ap.add_argument( + "--bare", + action="store_true", + help="scaffold-only with no authored spec (SPEC items -> MANUAL)", + ) + ap.add_argument("--format", default="md", choices=["md", "json"]) + ap.add_argument("--out", default=None) + ap.add_argument( + "--run", + action="store_true", + help="execute the operator's tests (EXEC group); needs a built GPU env", + ) + args = ap.parse_args(argv) + + P = review_op.resolve_op(args.operator) + X = extra_paths(P) + + if args.phase == "scaffold": + findings = run_scaffold(P, X, args.bare) + domains = SCAFFOLD_DOMAINS + else: + findings = run_done(P, X, args.bare, args.run) + domains = DONE_DOMAINS + + report = ( + render_md(P, findings, domains, args.phase) + if args.format == "md" + else render_json(P, findings, domains, args.phase) + ) + print(report) + if args.out: + Path(args.out).write_text(report + "\n", encoding="utf-8") + return 1 if any(f.status == GAP for f in findings) else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/mkop/Bench.cpp b/tools/mkop/Bench.cpp new file mode 100644 index 000000000..5f6dd3051 --- /dev/null +++ b/tools/mkop/Bench.cpp @@ -0,0 +1,83 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed 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. + */ + +#include "../CppBenchUtils.hpp" +#include "ops/generated/Bench__OPNAME__Config.hpp" + +#include + +#include + +// Benchmark for the __OPNAMESPACE__ operator. +// +// TODO(make-op): this stub benchmarks the interleaved NHWC Tensor path only. Extend it to the +// planar (NCHW) and fake-planar (NCHW_FAKE) layouts and the ImageBatchVarShape input kind so the +// benchmark coverage matches the operator's declared support matrix. See .agents/guidance/MAKE_OP_GUIDELINES.md +// (BEN-* / COV-2) and BenchFlip.cpp for a complete reference (planar + fake-planar + var-shape). +template +inline void __OPNAMELOW__(nvbench::state &state, nvbench::type_list) +try +{ + int3 shape = benchutils::GetShape<3, int3>(state.get_string("shape")); + auto layout = benchutils::GetStringParam(state, "layout", "NHWC"); + const benchutils::InputKind inputKind = benchutils::GetInputKind(state.get_string("inputKind")); + + if (inputKind != benchutils::InputKind::Tensor) + { + state.skip("TODO(make-op): implement the ImageBatchVarShape benchmark path"); + return; + } + if (layout != "NHWC") + { + state.skip("TODO(make-op): implement the planar (NCHW / NCHW_FAKE) benchmark path"); + return; + } + + using BT = typename nvcv::cuda::BaseType; + + int ch = nvcv::cuda::NumElements; + + const long bytes = static_cast(shape.x) * shape.y * shape.z * sizeof(T); + state.add_global_memory_reads(bytes); + state.add_global_memory_writes(bytes); + + cvcuda::__OPNAME__ op; + + nvcv::Tensor src( + { + {shape.x, shape.y, shape.z, ch}, + "NHWC" + }, + benchutils::GetDataType()); + nvcv::Tensor dst( + { + {shape.x, shape.y, shape.z, ch}, + "NHWC" + }, + benchutils::GetDataType()); + + benchutils::FillTensor(src, benchutils::CheckerboardValues()); + + benchutils::warmup_and_exec(state, BENCH___OPNAMEUPPER___WARMUP_ITERATIONS, + [&op, &src, &dst](cudaStream_t s) { op(s, src, dst); }); +} +CVCUDA_BENCH_SKIP_ERRORS(state) + +// Use auto-generated type list from bench_params.json + +NVBENCH_BENCH_TYPES(__OPNAMELOW__, NVBENCH_TYPE_AXES(BENCH___OPNAMEUPPER___TYPES)) +BENCH___OPNAMEUPPER___AXES; diff --git a/tools/mkop/BenchConfig.json b/tools/mkop/BenchConfig.json new file mode 100644 index 000000000..0017fef91 --- /dev/null +++ b/tools/mkop/BenchConfig.json @@ -0,0 +1,167 @@ +{ + "benchmark": "__OPNAMELOW__", + "configs": { + "__OPNAMELOW___uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "__OPNAMESPACE__ uchar3. TODO(make-op): calibrate this shape so the INTERLEAVED (NHWC) config lands in 1-2ms nvbench GPU; planar (NCHW) + fake-planar reuse the SAME shape and may run longer (apples-to-apples). Per-dtype shape (bytes/pixel differ); see .agents/guidance/MAKE_OP_GUIDELINES + flip.json." + }, + "baselines": {} + }, + "__OPNAMELOW___float3_basic": { + "tier": "basic", + "dtypes": [ + "float3" + ], + "string_axes": { + "shape": [ + "32x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "__OPNAMESPACE__ float3. TODO(make-op): calibrate this shape so the INTERLEAVED (NHWC) config lands in 1-2ms nvbench GPU; planar (NCHW) + fake-planar reuse the SAME shape and may run longer (apples-to-apples). Per-dtype shape (bytes/pixel differ); see .agents/guidance/MAKE_OP_GUIDELINES + flip.json." + }, + "baselines": {} + }, + "__OPNAMELOW___uint16_advanced": { + "tier": "advanced", + "dtypes": [ + "uint16" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "layout": [ + "NHWC" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "__OPNAMESPACE__ uint16. TODO(make-op): calibrate this shape so the INTERLEAVED (NHWC) config lands in 1-2ms nvbench GPU; planar (NCHW) + fake-planar reuse the SAME shape and may run longer (apples-to-apples). Per-dtype shape (bytes/pixel differ); see .agents/guidance/MAKE_OP_GUIDELINES + flip.json." + }, + "baselines": {} + }, + "__OPNAMELOW___fake_planar_uchar3_basic": { + "tier": "basic", + "dtypes": [ + "uchar3" + ], + "string_axes": { + "shape": [ + "64x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "__OPNAMESPACE__ fake-planar comparison (tensor-only); SAME input size as the interleaved/planar config of this dtype for an apples-to-apples ratio (will run longer than 1-2ms -- that's expected). TODO(make-op): keep this shape equal to the dtype's interleaved config." + }, + "baselines": {} + }, + "__OPNAMELOW___uchar4_advanced": { + "tier": "advanced", + "dtypes": [ + "uchar4" + ], + "string_axes": { + "shape": [ + "48x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "__OPNAMESPACE__ uchar4. TODO(make-op): calibrate this shape so the INTERLEAVED (NHWC) config lands in 1-2ms nvbench GPU; planar (NCHW) + fake-planar reuse the SAME shape and may run longer (apples-to-apples). Per-dtype shape (bytes/pixel differ); see .agents/guidance/MAKE_OP_GUIDELINES + flip.json." + }, + "baselines": {} + }, + "__OPNAMELOW___float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "24x1080x1920" + ], + "layout": [ + "NHWC", + "NCHW" + ], + "inputKind": [ + "Tensor", + "VarShape" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "__OPNAMESPACE__ float4. TODO(make-op): calibrate this shape so the INTERLEAVED (NHWC) config lands in 1-2ms nvbench GPU; planar (NCHW) + fake-planar reuse the SAME shape and may run longer (apples-to-apples). Per-dtype shape (bytes/pixel differ); see .agents/guidance/MAKE_OP_GUIDELINES + flip.json." + }, + "baselines": {} + }, + "__OPNAMELOW___fake_planar_float4_advanced": { + "tier": "advanced", + "dtypes": [ + "float4" + ], + "string_axes": { + "shape": [ + "24x1080x1920" + ], + "layout": [ + "NCHW_FAKE" + ], + "inputKind": [ + "Tensor" + ] + }, + "warmup_iterations": 200, + "metadata": { + "description": "__OPNAMESPACE__ fake-planar comparison (tensor-only); SAME input size as the interleaved/planar config of this dtype for an apples-to-apples ratio (will run longer than 1-2ms -- that's expected). TODO(make-op): keep this shape equal to the dtype's interleaved config." + }, + "baselines": {} + } + } +} diff --git a/tools/mkop/BenchPy.py b/tools/mkop/BenchPy.py new file mode 100755 index 000000000..007687628 --- /dev/null +++ b/tools/mkop/BenchPy.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +"""CV-CUDA __OPNAMESPACE__ operator benchmark - Python equivalent of Bench__OPNAME__.cpp + +TODO(make-op): this stub benchmarks the interleaved NHWC Tensor path only. Extend it to the +planar (NCHW) and fake-planar (NCHW_FAKE) layouts and the ImageBatchVarShape input kind so it +matches Bench__OPNAME__.cpp and the operator's declared support matrix. See bench_flip.py for a +complete reference. +""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import cvcuda # noqa: E402 +from python_bench_utils import ( # noqa: E402 + get_input_kind, + parse_shape, + get_dtype, + get_dtype_size, + get_num_channels, + create_tensor, + create_stream_cache, + run_benchmark, +) + + +def __OPNAMELOW__(state): + """__OPNAMESPACE__ operator benchmark matching C++ Bench__OPNAME__.cpp""" + + N, H, W = parse_shape(state.get_string("shape")) + dtype_str = state.get_string("InOutDataType") + dtype = get_dtype(dtype_str) + num_channels = get_num_channels(dtype_str) + try: + layout = state.get_string("layout") + except Exception: + layout = "NHWC" + input_kind = get_input_kind(state.get_string("inputKind")) + device_id = state.get_device() + + if input_kind != "Tensor": + state.skip("TODO(make-op): implement the ImageBatchVarShape benchmark path") + return None + if layout != "NHWC": + state.skip( + "TODO(make-op): implement the planar (NCHW / NCHW_FAKE) benchmark path" + ) + return None + + dtype_size = get_dtype_size(dtype_str) + bytes_ = N * H * W * dtype_size + state.add_global_memory_reads(bytes_) + state.add_global_memory_writes(bytes_) + + get_stream = create_stream_cache() + + shape = (N, H, W, num_channels) + src = create_tensor( + shape, dtype, device_id, layout="NHWC", fill_mode="checkerboard" + ) + dst = create_tensor(shape, dtype, device_id, layout="NHWC", fill_mode=0) + + def run(launch): + cvcuda.__OPNAMELOW___into(dst, src, stream=get_stream(launch)) + + return run + + +if __name__ == "__main__": + run_benchmark("__OPNAMELOW__", __OPNAMELOW__) diff --git a/tools/mkop/CImpl.cpp b/tools/mkop/CImpl.cpp index c9667a6af..b398c9e47 100644 --- a/tools/mkop/CImpl.cpp +++ b/tools/mkop/CImpl.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,19 +15,19 @@ * limitations under the License. */ +#include "priv/Nvtx.hpp" #include "priv/Op__OPNAME__.hpp" #include "priv/SymbolVersioning.hpp" #include #include -#include namespace priv = cvcuda::priv; CVCUDA_DEFINE_API(0, 0, NVCVStatus, cvcuda__OPNAME__Create, (NVCVOperatorHandle * handle)) { return nvcv::ProtectCall( - [&] + [handle] { if (handle == nullptr) { @@ -35,17 +35,21 @@ CVCUDA_DEFINE_API(0, 0, NVCVStatus, cvcuda__OPNAME__Create, (NVCVOperatorHandle "Pointer to NVCVOperator handle must not be NULL"); } - *handle = reinterpret_cast(new priv::__OPNAME__()); + // Ownership of the operator is transferred to the C handle and released by + // cvcudaOperatorDestroy; this matches every other operator's Create entry point. + *handle = reinterpret_cast(new priv::__OPNAME__()); // NOSONAR }); } CVCUDA_DEFINE_API(0, 0, NVCVStatus, cvcuda__OPNAME__Submit, (NVCVOperatorHandle handle, cudaStream_t stream, NVCVTensorHandle in, NVCVTensorHandle out)) { + CVCUDA_NVTX_RANGE("cvcuda__OPNAME__Submit"); return nvcv::ProtectCall( - [&] + [handle, stream, in, out] { - nvcv::TensorWrapHandle input(in), output(out); - priv::ToDynamicRef(handle)(stream, input, output); + nvcv::TensorWrapHandle input(in); + nvcv::TensorWrapHandle output(out); + priv::ToDynamicRef(handle)(stream, input.resource(), output.resource()); }); } diff --git a/tools/mkop/CppTest.cpp b/tools/mkop/CppTest.cpp index f4fb7c963..ba09b9e24 100644 --- a/tools/mkop/CppTest.cpp +++ b/tools/mkop/CppTest.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,6 +15,12 @@ * limitations under the License. */ +// NOTE (make-op): if __OPNAME__ is a unary element-wise operator that preserves shape/dtype/layout +// (like Invert / Solarize / Posterize), build the bit-exact test on the shared harness in +// "ElementwiseOpHarness.hpp" instead of the hand-rolled scaffold below — it centralizes the +// tensor/varshape/negative structure so each operator only supplies its gold + invoke callables +// (removing the copy-paste that otherwise trips SonarQube's duplication gate per op). See +// TestOpInvert.cpp for the pattern. Keep the generic scaffold below for non-element-wise operators. #include "Definitions.hpp" #include diff --git a/tools/mkop/PrivateImpl.cpp b/tools/mkop/PrivateImpl.cpp index 782e2b945..0e4d2ac5a 100644 --- a/tools/mkop/PrivateImpl.cpp +++ b/tools/mkop/PrivateImpl.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,6 +15,7 @@ * limitations under the License. */ +#include "Nvtx.hpp" #include "Op__OPNAME__.hpp" #include "legacy/CvCudaLegacy.h" #include "legacy/CvCudaLegacyHelpers.hpp" @@ -33,6 +34,7 @@ __OPNAME__::__OPNAME__() void __OPNAME__::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out) const { + CVCUDA_NVTX_RANGE("cvcuda::__OPNAME__::operator()[Tensor]"); auto inData = in.exportData(); if (inData == nullptr) { diff --git a/tools/mkop/Public.hpp b/tools/mkop/Public.hpp index 9c0a8b839..a23400b34 100644 --- a/tools/mkop/Public.hpp +++ b/tools/mkop/Public.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -41,36 +41,30 @@ class __OPNAME__ final : public IOperator public: explicit __OPNAME__(); - ~__OPNAME__(); + void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out) const; - void operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out); - - virtual NVCVOperatorHandle handle() const noexcept override; + NVCVOperatorHandle handle() const noexcept override; private: - NVCVOperatorHandle m_handle; + detail::OperatorHandle m_handle; }; inline __OPNAME__::__OPNAME__() { - nvcv::detail::CheckThrow(cvcuda__OPNAME__Create(&m_handle)); - assert(m_handle); -} - -inline __OPNAME__::~__OPNAME__() -{ - nvcvOperatorDestroy(m_handle); - m_handle = nullptr; + NVCVOperatorHandle h = nullptr; + nvcv::detail::CheckThrow(cvcuda__OPNAME__Create(&h)); + assert(h); + m_handle = detail::OperatorHandle{h}; } -inline void __OPNAME__::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out) +inline void __OPNAME__::operator()(cudaStream_t stream, const nvcv::Tensor &in, const nvcv::Tensor &out) const { - nvcv::detail::CheckThrow(cvcuda__OPNAME__Submit(m_handle, stream, in.handle(), out.handle())); + nvcv::detail::CheckThrow(cvcuda__OPNAME__Submit(m_handle.get(), stream, in.handle(), out.handle())); } inline NVCVOperatorHandle __OPNAME__::handle() const noexcept { - return m_handle; + return m_handle.get(); } } // namespace cvcuda diff --git a/tools/mkop/PythonWrap.cpp b/tools/mkop/PythonWrap.cpp index 0acec9038..40c48901d 100644 --- a/tools/mkop/PythonWrap.cpp +++ b/tools/mkop/PythonWrap.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,6 +15,16 @@ * limitations under the License. */ +// NOTE (make-op): if __OPNAME__ is a unary element-wise operator that preserves shape/dtype/layout +// (like Invert / Solarize / Posterize), prefer the shared binding helpers in +// "operators/UnaryElementwiseOp.hpp" over the hand-rolled wrappers below — they remove the +// create/ResourceGuard/submit copy-paste that otherwise trips SonarQube's duplication gate per op: +// #include "UnaryElementwiseOp.hpp" +// Tensor __OPNAME__Into(Tensor &o, Tensor &i, std::optional s) +// { return UnaryElementwiseInto(o, i, s /*, extra submit params */); } +// Tensor __OPNAME__(Tensor &i, std::optional s) +// { return UnaryElementwiseTensor(i, s /*, extra submit params */); } +// Keep the generic wrappers below for operators that change dtype/shape or are not element-wise. #include "Operators.hpp" #include @@ -60,7 +70,8 @@ void ExportOp__OPNAME__(py::module &m) { using namespace pybind11::literals; - m.def("__OPNAMELOW__", &__OPNAME__, "src"_a, "dtype"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( + m.def("__OPNAMELOW__", NvtxTrace("cvcuda.__OPNAMELOW__", &__OPNAME__), "src"_a, "dtype"_a, py::kw_only(), + "stream"_a = nullptr, R"pbdoc( Executes the __OPNAMESPACE__ operation on the given cuda stream. @@ -80,7 +91,8 @@ void ExportOp__OPNAME__(py::module &m) API references of the CV-CUDA operator. )pbdoc"); - m.def("__OPNAMELOW___into", &__OPNAME__Into, "dst"_a, "src"_a, py::kw_only(), "stream"_a = nullptr, R"pbdoc( + m.def("__OPNAMELOW___into", NvtxTrace("cvcuda.__OPNAMELOW___into", &__OPNAME__Into), "dst"_a, "src"_a, + py::kw_only(), "stream"_a = nullptr, R"pbdoc( Executes the __OPNAMESPACE__ operation on the given cuda stream. diff --git a/tools/mkop/mkop.sh b/tools/mkop/mkop.sh index 2157fa42a..8a77b7266 100755 --- a/tools/mkop/mkop.sh +++ b/tools/mkop/mkop.sh @@ -1,5 +1,5 @@ #!/bin/bash -# SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2023-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,24 +14,44 @@ # See the License for the specific language governing permissions and # limitations under the License. +# POSIX character-class names for `tr` case conversion (shared to avoid +# duplicating the literals; SonarQube shelldre:S1192). +readonly TR_LOWER='[:lower:]' +readonly TR_UPPER='[:upper:]' + +# Fail loudly + stop the scaffold when a required insertion anchor / version / file is missing, +# so mkop.sh never exits green with incomplete wiring (the helpers are called bare, without +# `set -e`, so a bare `return 1` would be swallowed — `exit` is what actually stops the run). +die() { # NOSONAR shelldre:S7682 — terminal helper: deliberately `exit`s, never returns + echo "mkop: $*" >&2 + exit 1 +} + modify_and_update_template() { local file="$1" local name="$2" local destination="$3" local year - local capName - local lowName + local cap_name + local low_name + local spaced_name + local upper_name year=$(date +%Y) - capName=$(echo "$name" | sed 's/\([A-Z]\)/_\L\1/g' | tr '[:lower:]' '[:upper:]') - lowName=$(echo "$name" | tr '[:upper:]' '[:lower:]') - spacedName=$(echo $name | sed 's/\([a-z]\)\([A-Z]\)/\1 \2/g') + cap_name=$(echo "$name" | sed 's/\([A-Z]\)/_\L\1/g' | tr "$TR_LOWER" "$TR_UPPER") + low_name=$(echo "$name" | tr "$TR_UPPER" "$TR_LOWER") + spaced_name=$(echo "$name" | sed 's/\([a-z]\)\([A-Z]\)/\1 \2/g') + # Uppercased lowercase name (no separators), matching the bench codegen's + # BENCH__* macro names (see bench/cpp/GenerateBenchConfig.cmake). + upper_name=$(echo "$low_name" | tr "$TR_LOWER" "$TR_UPPER") # Replace all occurrences of the "TAG" string with the provided name sed "s/__OPNAME__/$name/g" "$file" > "$destination" sed -i 's/\(.*(\s*c\s*)\).*\(NVIDIA.*\)/\1'" $year "'\2/' "$destination" - sed -i "s/__OPNAMECAP__/$capName/g" "$destination" - sed -i "s/__OPNAMELOW__/$lowName/g" "$destination" - sed -i "s/__OPNAMESPACE__/$spacedName/g" "$destination" + sed -i "s/__OPNAMECAP__/$cap_name/g" "$destination" + sed -i "s/__OPNAMELOW__/$low_name/g" "$destination" + sed -i "s/__OPNAMESPACE__/$spaced_name/g" "$destination" + sed -i "s/__OPNAMEUPPER__/$upper_name/g" "$destination" + return $? } add_to_cmake() { @@ -44,6 +64,7 @@ add_to_cmake() { line_number=$((line_number+1)) sed -i "$line_number i\ $name" "$file" fi + return $? } add_to_test_cmake() { @@ -56,6 +77,7 @@ add_to_test_cmake() { line_number=$((line_number+1)) sed -i "$line_number i\ $name" "$file" fi + return $? } add_to_cmake_python() { @@ -68,16 +90,25 @@ add_to_cmake_python() { line_number=$((line_number+2)) sed -i "$line_number i\ $name" "$file" fi + return $? } add_to_python_main() { local file="$1" local name="$2" + local line_number - if ! grep -q "$name" "$file"; then - sed "s/CV-CUDA Operators/&\n ExportOp$name(m);/g" -i "$file" + # "CV-CUDA Operators" appears twice — once in an include-section comment and once as the + # indented marker above the ExportOp* registration block. Anchor on the indented marker so + # the call lands among the other ExportOp* calls (not spliced into the include comment). + if ! grep -q "ExportOp$name(m);" "$file"; then + line_number=$(grep -nE '^[[:space:]]+// CV-CUDA Operators' "$file" | head -1 | cut -d: -f1) + [[ -z "$line_number" ]] && die "'// CV-CUDA Operators' anchor not found in $file" + line_number=$((line_number+1)) + sed -i "${line_number}i\\ ExportOp$name(m);" "$file" fi + return $? } add_to_python_operators() { @@ -92,6 +123,172 @@ add_to_python_operators() { sed -i "$line_number i\void ExportOp$name(py::module &m);" "$file" fi + return $? +} + +# Insert "$entry" on the line after the first line matching "$anchor", indented by "$indent". +# Used for the bench `set(bench_sources ...)` and `set(python_bench_scripts ...)` lists. +add_after_set_anchor() { + local file="$1" + local anchor="$2" + local indent="$3" + local entry="$4" + local line_number + + if ! grep -qF "$entry" "$file"; then + line_number=$(grep -n "$anchor" "$file" | head -n 1 | cut -d: -f1) + [[ -z "$line_number" ]] && die "anchor '$anchor' not found in $file" + line_number=$((line_number+1)) + sed -i "${line_number}i\\${indent}${entry}" "$file" + fi + return $? +} + +# Insert the operator's manifest entry into bench/config/bench_params.json (alphabetically), +# preserving the file's 4-space indentation. Idempotent. +add_to_bench_params() { + local file="$1" + local op="$2" + + python3 - "$file" "$op" <<'PYEOF' +import json +import sys + +path, op = sys.argv[1], sys.argv[2] +with open(path) as f: + data = json.load(f) +ops = data.setdefault("operators", {}) +if op not in ops: + ops[op] = { + "config": "operators/%s.json" % op, + "cpp": "bench_%s" % op, + "python": "bench_%s.py" % op, + } + data["operators"] = {k: ops[k] for k in sorted(ops)} + with open(path, "w") as f: + json.dump(data, f, indent=4) + f.write("\n") +PYEOF + return $? +} + +# Insert the operator into bench/config/operator_categories.json (the RGB-benchmark-guidelines +# manifest that bench/tests/test_bench_rgb_guidelines.py::test_r1_every_operator_classified +# requires every on-disk operator config to appear in). Defaults to category "A" (general +# per-pixel/per-channel image op — the common case); the agent must verify and switch to B +# (inherently single-channel) or C (intrinsic channel semantics) if that fits better. Idempotent. +add_to_operator_categories() { + local file="$1" + local op="$2" + + [[ -f "$file" ]] || die "operator category manifest not found: $file" + python3 - "$file" "$op" <<'PYEOF' +import json +import sys + +path, op = sys.argv[1], sys.argv[2] +with open(path) as f: + data = json.load(f) +ops = data.setdefault("operators", {}) +if op not in ops: + ops[op] = {"category": "A"} + data["operators"] = {k: ops[k] for k in sorted(ops)} + with open(path, "w") as f: + json.dump(data, f, indent=4) + f.write("\n") +PYEOF + return $? +} + +# Insert a two-line row into the docs/sphinx/operator_list.rst table, alphabetically by the +# displayed operator name. Idempotent (keyed on the py:func target). +add_to_oplist() { + local file="$1" + local disp="$2" + local pyfn="$3" + local desc="$4" + + grep -q "cvcuda\.$pyfn\`" "$file" && return 0 + # Only consider real operator rows (those with a :py:func: target); the list-table's + # header row ("Pre/Post-Processing Operators" | "Definition") has none and must be skipped. + awk -v disp="$disp" -v pyfn="$pyfn" -v desc="$desc" ' + BEGIN { ins = 0; key = tolower(disp) } + /^ \* - .*:py:func:/ { + d = $0; sub(/^ \* - /, "", d); sub(/ \(:py:func.*/, "", d) + if (ins == 0 && tolower(d) > key) { + printf " * - %s (:py:func:`cvcuda.%s`)\n", disp, pyfn + printf " - %s\n", desc + ins = 1 + } + } + { print } + END { + if (ins == 0) { + printf " * - %s (:py:func:`cvcuda.%s`)\n", disp, pyfn + printf " - %s\n", desc + } + } + ' "$file" > "$file.mkop.tmp" && mv "$file.mkop.tmp" "$file" + return $? +} + +# Insert the two cvcuda-autofunction directives (fn + fn_into) into +# docs/sphinx/modules/python/operators.rst, alphabetically. Idempotent. +add_to_autofunction() { + local file="$1" + local pyfn="$2" + + grep -qE "cvcuda-autofunction:: cvcuda\.$pyfn\$" "$file" && return 0 + awk -v n="$pyfn" ' + BEGIN { ins = 0 } + /^\.\. cvcuda-autofunction:: cvcuda\./ { + fn = $0; sub(/^.*cvcuda\./, "", fn); sub(/_into$/, "", fn) + if (ins == 0 && fn > n) { + printf ".. cvcuda-autofunction:: cvcuda.%s\n\n", n + printf ".. cvcuda-autofunction:: cvcuda.%s_into\n\n", n + ins = 1 + } + } + { print } + END { + if (ins == 0) { + printf "\n.. cvcuda-autofunction:: cvcuda.%s\n\n", n + printf ".. cvcuda-autofunction:: cvcuda.%s_into\n", n + } + } + ' "$file" > "$file.mkop.tmp" && mv "$file.mkop.tmp" "$file" + return $? +} + +# Insert a "New Features" bullet for the operator into the latest release notes. "Latest" is the +# relnote whose version matches CMakeLists.txt's VERSION. Idempotent. +add_to_relnote() { + local root="$1" + local name="$2" + local ver + local relfile + local line_number + + # The project VERSION is the indented `VERSION X.Y.Z` line inside project(); anchor on + # leading whitespace so we don't match `cmake_minimum_required(VERSION 3.20.1)`. + ver=$(grep -oE '^[[:space:]]+VERSION[[:space:]]+[0-9]+\.[0-9]+\.[0-9]+' "$root/CMakeLists.txt" | head -1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+') + [[ -z "$ver" ]] && die "project VERSION not found in $root/CMakeLists.txt" + relfile=$(ls "$root"/docs/sphinx/relnotes/v"$ver"-*.rst 2>/dev/null | head -1) + [[ -z "$relfile" ]] && relfile=$(ls "$root"/docs/sphinx/relnotes/v"$ver".rst 2>/dev/null | head -1) + [[ -z "$relfile" ]] && die "no relnote file for version $ver under $root/docs/sphinx/relnotes/" + local first_bullet + grep -q "\`\`$name\`\` operator" "$relfile" && return 0 + line_number=$(grep -n "New Features and Enhancements" "$relfile" | head -1 | cut -d: -f1) + [[ -z "$line_number" ]] && die "'New Features and Enhancements' anchor not found in $relfile" + # Insert before the first existing nested bullet so the blank line after the section + # header (required for RST nested-list rendering) is preserved; append a trailing blank. + first_bullet=$(awk -v h="$line_number" 'NR>h && /^[[:space:]]+\* / {print NR; exit}' "$relfile") + if [[ -n "$first_bullet" ]]; then + sed -i "${first_bullet}i\\ * Added the \`\`$name\`\` operator\n" "$relfile" + else + sed -i "$((line_number+1))i\\ * Added the \`\`$name\`\` operator" "$relfile" + fi + return $? } # Check if the correct number of arguments have been provided @@ -104,13 +301,13 @@ fi # if not provided assume script is in /cvcuda/tools/mkop root="../.." -if [ $# -eq 2 ]; then +if [[ $# -eq 2 ]]; then root="$2" fi # Store the name and destination arguments (first letter Cap) name=$(echo "$1" | awk '{print toupper(substr($0,1,1)) substr($0,2)}') -namelower=$(echo "$name" | tr '[:upper:]' '[:lower:]') +namelower=$(echo "$name" | tr "$TR_UPPER" "$TR_LOWER") #public API's for the operator modify_and_update_template "Public.h" "$name" "$root/src/cvcuda/include/cvcuda/Op$name.h" @@ -124,18 +321,32 @@ modify_and_update_template "PrivateImpl.hpp" "$name" "$root/src/cvcuda/priv/Op$n #C++ system tests modify_and_update_template "CppTest.cpp" "$name" "$root/tests/cvcuda/system/TestOp$name.cpp" -#add to makefiles -add_to_cmake "$root/src/cvcuda/priv/CMakeLists.txt" "Op$name.cpp" -add_to_cmake "$root/src/cvcuda/CMakeLists.txt" "Op$name.cpp" -add_to_test_cmake "$root/tests/cvcuda/system/CMakeLists.txt" "TestOp$name.cpp" +#add to makefiles (insert into the source-list set(...) blocks, not add_library/add_executable) +add_after_set_anchor "$root/src/cvcuda/priv/CMakeLists.txt" "set(CV_CUDA_PRIV_OP_FILES" " " "Op$name.cpp" +add_after_set_anchor "$root/src/cvcuda/CMakeLists.txt" "set(CV_CUDA_OP_FILES" " " "Op$name.cpp" +add_after_set_anchor "$root/tests/cvcuda/system/CMakeLists.txt" "set(CVCUDA_TEST_SOURCES" " " "TestOp$name.cpp" -#add python stub -modify_and_update_template "PythonWrap.cpp" "$name" "$root/python/mod_cvcuda/Op$name.cpp" +#add python stub (bindings live under operators/) +modify_and_update_template "PythonWrap.cpp" "$name" "$root/python/mod_cvcuda/operators/Op$name.cpp" add_to_python_main "$root/python/mod_cvcuda/Main.cpp" "$name" -add_to_python_operators "$root/python/mod_cvcuda/Operators.hpp" "$name" +add_to_python_operators "$root/python/mod_cvcuda/operators/Operators.hpp" "$name" #add python makefile -add_to_cmake_python "$root/python/mod_cvcuda/CMakeLists.txt" "Op$name.cpp" +add_to_cmake_python "$root/python/mod_cvcuda/CMakeLists.txt" "operators/Op$name.cpp" #add python test modify_and_update_template "PythonTest.py" "$name" "$root/tests/cvcuda/python/test_op$namelower.py" + +#add benchmarks (C++ driver + Python driver + shared config + manifest entry) +modify_and_update_template "Bench.cpp" "$name" "$root/bench/cpp/ops/Bench$name.cpp" +modify_and_update_template "BenchPy.py" "$name" "$root/bench/python/ops/bench_$namelower.py" +modify_and_update_template "BenchConfig.json" "$name" "$root/bench/config/operators/$namelower.json" +add_after_set_anchor "$root/bench/cpp/CMakeLists.txt" "set(bench_sources" " " "ops/Bench$name.cpp" +add_after_set_anchor "$root/bench/python/CMakeLists.txt" "set(python_bench_scripts" " " "ops/bench_$namelower.py" +add_to_bench_params "$root/bench/config/bench_params.json" "$namelower" +add_to_operator_categories "$root/bench/config/operator_categories.json" "$namelower" + +#add documentation (operator list + python autofunction directives + latest release notes) +add_to_oplist "$root/docs/sphinx/operator_list.rst" "$name" "$namelower" "TODO(make-op): one-line description of the $name operator." +add_to_autofunction "$root/docs/sphinx/modules/python/operators.rst" "$namelower" +add_to_relnote "$root" "$name" diff --git a/tools/mkstub/mkstub.sh b/tools/mkstub/mkstub.sh index 8e90bd6a2..4d89764c3 100755 --- a/tools/mkstub/mkstub.sh +++ b/tools/mkstub/mkstub.sh @@ -1,5 +1,5 @@ #!/bin/bash -e -# SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,12 +23,12 @@ fi dso=$1 stub_dso=$2 -if [ ! -r "$dso" ]; then +if [[ ! -r "$dso" ]]; then echo "$dso: file not found or is not readable" exit 1 fi -if [ -z "$stub_dso" ]; then +if [[ -z "$stub_dso" ]]; then tmp=$(basename "$dso") stub_dso="${tmp%.*}_stub.${tmp##*.}" fi @@ -37,7 +37,7 @@ CC=${CC:-gcc} target=$($CC -v 2>&1 | sed -n 's@Target: @@pg') #echo Targetting $target -if [ ${CC%-*} = $CC ]; then +if [[ ${CC%-*} == "$CC" ]]; then STRIP="strip" else STRIP="$target-strip" @@ -51,6 +51,7 @@ tmp_stub_symbols=$(mktemp) function cleanup() { rm -f $tmp_c $tmp_v $tmp_orig_symbols $tmp_stub_symbols + return $? } trap 'cleanup' EXIT @@ -135,6 +136,7 @@ function print_symbol() echo "Symbol type not understood: '$symtype'" exit 1; esac + return $? } symbol_filter='$7 !~ /(Ndx|UND|ABS|^$)/ && $5 !~ /(UNIQUE|LOCAL)/ && ( $5 ~ /WEAK/ || $4 !~ /NOTYPE/ )' @@ -147,6 +149,7 @@ function get_symbols() # IFUNC on x86_64-redhat-linux DSOs (Centos7) # aren't well understood x86_64-linux-gnu (Ubuntu, Gentoo...) readelf -W --dyn-syms "$dso" | sed 's/: 10/IFUNC/g' + return $? } first=1 @@ -155,7 +158,7 @@ versioned=0 declare -a weak_ifuncs while read ver vertype name symtype bind vis; do - if [ -z "$name" ]; then + if [[ -z "$name" ]]; then continue; fi @@ -163,7 +166,7 @@ while read ver vertype name symtype bind vis; do name=${name:1:-1} vertype=${vertype:1:-1} - if [ -n "$ver" ]; then + if [[ -n "$ver" ]]; then cver="$vertype$ver" cname=${name}_$cver cname="${cname//[.@-]/_}" @@ -180,7 +183,7 @@ while read ver vertype name symtype bind vis; do print_symbol "$cname" "$bind" "$vis" "$symtype" fi - if [ -z "$ver" ]; then + if [[ -z "$ver" ]]; then ver_symbols+=("$cname") continue; fi @@ -214,12 +217,12 @@ for syminfo in "${weak_ifuncs[@]}"; do print_symbol $syminfo done -if [ ! -f $tmp_c ]; then +if [[ ! -f $tmp_c ]]; then echo "No symbols found to be stubbed" exit 1 fi -if [ $versioned -eq 0 ]; then +if [[ $versioned -eq 0 ]]; then echo "{" >> $tmp_v fi @@ -236,13 +239,13 @@ args="-Wl,--version-script=$tmp_v" soname=$(readelf -d "$dso" | gawk '/SONAME/ { print $5 }') soname=${soname:1:-1} -if [ -n "$soname" ]; then +if [[ -n "$soname" ]]; then args="$args -Wl,-soname=$soname" fi # Some old versions of ld don't support these options. # Let's use only what's supported. -if [ ${CC%-*} = $CC ]; then +if [[ ${CC%-*} == "$CC" ]]; then LD=$(dirname $CC)/ld else LD="$(dirname $CC)/$target-ld" @@ -273,13 +276,14 @@ function list_contents() local parser='{ print $4,$5,$6,$8 }' local dso=$1 get_symbols $dso | gawk "$symbol_filter $parser" | sort + return $? } list_contents $dso > $tmp_orig_symbols list_contents $stub_dso > $tmp_stub_symbols if ! diff $tmp_orig_symbols $tmp_stub_symbols; then - echo "Error, resulting stub doesn't correspond to input" + echo "Error, resulting stub doesn't correspond to input" >&2 rm $stub_dso exit 1 fi diff --git a/tools/mkstub/test.sh b/tools/mkstub/test.sh index 57345bced..e81a24786 100755 --- a/tools/mkstub/test.sh +++ b/tools/mkstub/test.sh @@ -1,5 +1,5 @@ #!/bin/bash -e -# SPDX-FileCopyrightText: Copyright (c) 2022 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ function cleanup() { rm -f stub_symbols orig_symbols libtest.so libtest_stub.so + return $? } trap 'cleanup' EXIT @@ -30,7 +31,7 @@ function run_test() echo "test $name" local args='' - if [ -n "$ver" ]; then + if [[ -n "$ver" ]]; then args="-Wl,--version-script=$ver" fi @@ -40,13 +41,16 @@ function run_test() ./mkstub.sh libtest.so function list_contents() { - readelf --dyn-syms "$1" | awk '$7 !~ /(UND|^$|Ndx)/ { print $4,$5,$6,$8 }' | sort + local library="$1" + readelf --dyn-syms "$library" | awk '$7 !~ /(UND|^$|Ndx)/ { print $4,$5,$6,$8 }' | sort + return $? } list_contents libtest.so > orig_symbols list_contents libtest_stub.so > stub_symbols diff orig_symbols stub_symbols + return $? } run_test versioned "$(cat test_versioned.c)" test_versioned.v diff --git a/tools/optimize_op.py b/tools/optimize_op.py new file mode 100755 index 000000000..73932ecee --- /dev/null +++ b/tools/optimize_op.py @@ -0,0 +1,1832 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. +"""Deterministic definition-of-done checker for a single-operator optimization campaign. + +Gates the *edges* of an optimization campaign per .agents/guidance/OPTIMIZATION_GUIDELINES.md — it does not +choose or implement optimizations (that is the agent/GPU-driven loop). Three phases: + + --phase preflight : readiness BEFORE optimizing (correctness + bench coverage + baseline + + profiling availability). Reuses tools/review_op.py where present. + --phase evidence : definition-of-done AFTER optimizing (regression test + pixelwise + evidence per changed config, baselines updated, results-summary format, + leads exhausted, memory-footprint and review/refactor gate evidence, + API/ABI unchanged, perf: commit hygiene). + --phase summary : initialize or refresh the bounded v1 MR optimization summary while + preserving its human-authored assessment, evidence, and learnings. + +Read-only and deterministic (no network, no clocks, no randomness; the changed-set comes +from `git diff ..HEAD`). Emits PASS/GAP/N-A/MANUAL per item with evidence + a guideline +cite; the final evidence phase exits non-zero on any GAP or unresolved MANUAL. The agent +re-runs `--phase evidence` and uses the verdict to drive its optimize loop until the DoD holds. + +Usage: + python3 tools/optimize_op.py --phase preflight|evidence|summary + [--base ] [--results ] [--mr-iid ] + [--benchmark-base ] [--optimized-cases-file ] [--state provisional|final] + [--bottleneck Memory-bound|Compute-bound] [--profile-evidence ] + [--impact-metric cpp_time|python_overhead] [--secondary-operator ]... + [--format md|json] [--out PATH] +""" + +import argparse +import json +import math +import re +import shutil +import subprocess +import sys +from dataclasses import dataclass +from pathlib import Path + +REPO = Path(__file__).resolve().parent.parent +GUIDE = ".agents/guidance/OPTIMIZATION_GUIDELINES.md" + +AGENT_TOOLS = REPO / ".agents" / "tools" +if str(AGENT_TOOLS) not in sys.path: + sys.path.insert(0, str(AGENT_TOOLS)) +BENCH_TOOLS = REPO / "bench" +if str(BENCH_TOOLS) not in sys.path: + sys.path.insert(0, str(BENCH_TOOLS)) +CI_TOOLS = REPO / "ci" +if str(CI_TOOLS) not in sys.path: + sys.path.insert(0, str(CI_TOOLS)) + +from optimization_summary import ( # noqa: E402 + SummaryError, + SummaryMetadata, + generate_summary, + parse_summary, + refresh_summary, + validate_summary, +) + +try: + if not (CI_TOOLS / "optimization_secondary_scope_policy.py").is_file(): + raise ImportError("internal secondary-scope policy is unavailable") + from optimization_secondary_scope_policy import ( # noqa: E402 + is_reviewed_secondary_scope, + ) +except ImportError: + # The internal policy file is intentionally absent from the OSS mirror. + # Missing policy must deny every multi-operator exception. + def is_reviewed_secondary_scope(*_args): + return False + + +from binding_api import binding_api_snapshot as _binding_api_snapshot # noqa: E402 +from operator_source_map import SHARED_KERNEL_SOURCES # noqa: E402 +from _internal.quality import DEFAULT_BENCHMARK_QUALITY # noqa: E402 + +PASS, GAP, NA, MANUAL = "PASS", "GAP", "N-A", "MANUAL" +# 10 MB in decimal bytes. +MEMORY_AUTO_ACCEPT_BYTES = 10_000_000 + + +@dataclass +class Finding: + id: str + phase: str + status: str + summary: str + evidence: str = "" + guideline: str = "" + fix: str = "" + + +# --------------------------------------------------------------------------- io / git helpers +def read(p: Path): + try: + return p.read_text(encoding="utf-8", errors="replace") + except (OSError, AttributeError): + return None + + +def read_results(path): + """Read an MR description from a path or stdin (``-``).""" + if not path: + return None + if path == "-": + return sys.stdin.read() + return read(Path(path)) + + +def rel(p: Path) -> str: + try: + return str(p.relative_to(REPO)) + except ValueError: + return str(p) + + +def git(*args): + try: + r = subprocess.run( + ["git", *args], cwd=str(REPO), capture_output=True, text=True + ) + return r.stdout if r.returncode == 0 else "" + except OSError: + return "" + + +def git_success(*args): + """Return whether a read-only git command completed successfully.""" + try: + return ( + subprocess.run( + ["git", *args], cwd=str(REPO), capture_output=True, text=True + ).returncode + == 0 + ) + except OSError: + return False + + +def git_json(ref, path): + """Load a JSON file from a git revision without modifying the worktree.""" + text = git("show", f"{ref}:{path}") + if not text: + return None + try: + return json.loads(text) + except json.JSONDecodeError: + return None + + +def resolve_commit(ref): + return git("rev-parse", "--verify", f"{ref}^{{commit}}").strip() + + +def grep(pattern, text, flags=0): + if not text: + return [] + rx = re.compile(pattern, flags) + return [(i + 1, ln) for i, ln in enumerate(text.splitlines()) if rx.search(ln)] + + +# --------------------------------------------------------------------- operator resolution +@dataclass +class OpPaths: + op: str + Op: str + header: Path + hpp: Path + pybind: Path + bench_cfg: Path + + +def resolve_op(arg): + stem = arg.strip() + op = stem.lower() + hdr_dir = REPO / "src/cvcuda/include/cvcuda" + header, Op = None, stem + if hdr_dir.is_dir(): + for h in sorted(hdr_dir.glob("Op*.h")): + if h.stem[2:].lower() == op: + header, Op = h, h.stem[2:] + break + if header is None: + header = hdr_dir / f"Op{stem}.h" + return OpPaths( + op=op, + Op=Op, + header=header, + hpp=hdr_dir / f"Op{Op}.hpp", + pybind=REPO / f"python/mod_cvcuda/operators/Op{Op}.cpp", + bench_cfg=REPO / f"bench/config/operators/{op}.json", + ) + + +def review_op_findings(P, domain): + """Run tools/review_op.py for a domain and return its findings (or None if unavailable).""" + tool = REPO / "tools" / "review_op.py" + if not tool.exists(): + return None + try: + r = subprocess.run( + [sys.executable, str(tool), P.Op, "--domain", domain, "--format", "json"], + cwd=str(REPO), + capture_output=True, + text=True, + ) + return json.loads(r.stdout)["findings"] + except (OSError, ValueError, KeyError): + return None + + +def changed_paths(base): + out = git("diff", "--name-only", f"{base}...HEAD") + return [ln.strip() for ln in out.splitlines() if ln.strip()] + + +def all_operator_stems(): + """Operator stems from the bench manifest, for longest-prefix file attribution.""" + manifest = _load_json(REPO / "bench/config/bench_params.json") or {} + return sorted((manifest.get("operators") or {}).keys()) + + +def _is_op_kernel(path, P, all_ops): + """A changed priv file belongs to iff is the LONGEST operator-name prefix of + its filename body. Avoids both under-match (OpHQResizeKernel.cuh) and cross-op over-match + (OpResizeCropConvertReformat.cu must not count as 'resize').""" + pl = path.replace("\\", "/") + if not pl.startswith("src/cvcuda/priv/"): + return False + rel = pl.removeprefix("src/cvcuda/priv/") + if rel in SHARED_KERNEL_SOURCES.get(P.op, []): + return True + stem = Path(pl).stem.lower() + body = stem[2:] if (stem.startswith("op") and "/legacy/" not in pl) else stem + body = re.sub(r"[^a-z0-9]", "", body) + candidates = [o for o in all_ops if body.startswith(o)] or ( + [P.op] if body.startswith(P.op) else [] + ) + best = max(candidates, key=len, default=None) + return best == P.op + + +def _is_op_implementation(path, P, all_ops): + """Return whether a changed path implements this operator's timed execution path.""" + normalized = path.replace("\\", "/") + return normalized == rel(P.pybind).replace("\\", "/") or _is_op_kernel( + normalized, P, all_ops + ) + + +def _has_pixelwise_assertion(evidence): + """Recognize concrete C++/Python equality assertions, not descriptive prose.""" + evidence = evidence or "" + if re.search(r"\bEXPECT_(?:EQ|NEAR)\s*\(", evidence): + return True + if re.search( + r"\b(?:cupy|cp|numpy|np)\.testing\." + r"assert_(?:array_equal|allclose|equal)\s*\(", + evidence, + re.I, + ): + return True + return bool( + re.search( + r"\bassert\s+(?:" + r"bool\s*\(" + r"|(?:cupy|cp|numpy|np|torch)\.(?:all|array_equal|allclose|equal)\s*\(" + r"|[A-Za-z_]\w*(?:\.[A-Za-z_]\w*|\[[^\]]+\])*\s*" + r"(?:==|!=|<=|>=|<|>|\bis\b))", + evidence, + re.I, + ) + ) + + +# ============================================================================ PREFLIGHT +def check_preflight(P, base): + g = ".agents/guidance/OPTIMIZATION_GUIDELINES.md#establish-coverage-and-baseline" + out = [] + + # PRE-1/PRE-2: reuse review_op test + bench coverage + for pid, domain, key_ids, label in [ + ("PRE-1", "test", {"TST-1", "TST-2"}, "Correctness/regression coverage"), + ( + "PRE-2", + "bench", + {"BEN-4", "BEN-5", "BEN-6", "BEN-7"}, + "Benchmark coverage + baselines", + ), + ]: + f = review_op_findings(P, domain) + if f is None: + out.append( + Finding( + pid, + "preflight", + MANUAL, + f"{label}: run review_op.py (tools/review_op.py not present here)", + "reuse: python3 tools/review_op.py %s --domain %s" % (P.Op, domain), + g, + ) + ) + continue + gaps = [x for x in f if x["id"] in key_ids and x["status"] == "GAP"] + if gaps: + out.append( + Finding( + pid, + "preflight", + GAP, + f"{label} incomplete", + "; ".join(f"{x['id']}:{x['summary']}" for x in gaps), + g, + "Add the missing coverage before optimizing (separate, non-perf commit).", + ) + ) + else: + out.append( + Finding( + pid, + "preflight", + PASS, + f"{label} present (review_op {domain})", + "review_op %s domain: no blocking GAP in %s" + % (domain, sorted(key_ids)), + g, + ) + ) + + # PRE-3: baseline captured + cfg = read(P.bench_cfg) + has_baseline = bool(cfg and re.search(r'"baselines"\s*:\s*\{[^}]*[A-Za-z]', cfg)) + out.append( + Finding( + "PRE-3", + "preflight", + PASS if has_baseline else GAP, + ( + "Baseline captured for the surface" + if has_baseline + else "No baselines in the op config" + ), + rel(P.bench_cfg), + g, + ( + "" + if has_baseline + else "Capture/commit a baseline (CI on the bench-only SHA) before optimizing." + ), + ) + ) + + # PRE-4: profiling tools + have = [t for t in ("ncu", "nsys") if shutil.which(t)] + out.append( + Finding( + "PRE-4", + "preflight", + PASS if have else MANUAL, + ( + "Profiler(s) available: " + ", ".join(have) + if have + else "ncu/nsys not on PATH" + ), + "PATH probe", + ".agents/guidance/OPTIMIZATION_GUIDELINES.md#profiling", + ( + "" + if have + else "Install/enable ncu or nsys, or record the unavailability in the MR per the guidelines." + ), + ) + ) + + # PRE-5: API/ABI snapshot pointer (verified at evidence phase) + out.append( + Finding( + "PRE-5", + "preflight", + MANUAL, + "API/ABI must stay unchanged; verified at --phase evidence (ODO-5)", + "public headers: %s, %s; binding: %s" + % (rel(P.header), rel(P.hpp), rel(P.pybind)), + ".agents/guidance/OPTIMIZATION_GUIDELINES.md#survey-and-scope", + ) + ) + return out + + +# ============================================================================ EVIDENCE +def check_evidence(P, base, results_path, mr_iid=None): + g = ".agents/guidance/OPTIMIZATION_GUIDELINES.md" + out = [] + all_ops = all_operator_stems() + changed = changed_paths(base) + implementation_changed = [ + c for c in changed if _is_op_implementation(c, P, all_ops) + ] + + if not implementation_changed: + out.append( + Finding( + "ODO-0", + "evidence", + NA, + "No optimization detected: no %s implementation change vs %s" + % (P.Op, base), + "changed implementation files: none", + g, + ) + ) + # still run the format/hygiene checks below where meaningful + + results = read_results(results_path) + + # ODO-1: regression test + pixelwise-equality evidence per changed config + test_f = review_op_findings(P, "test") + tests_ok = test_f is not None and not [ + x for x in test_f if x["id"] in {"TST-1", "TST-2"} and x["status"] == "GAP" + ] + pixel_checked, pixel_evidence = _checklist_item( + results, "Pixelwise equality to reference" + ) + pixel_hard_evidence = bool( + pixel_checked + and _has_pixelwise_assertion(pixel_evidence) + and re.search(r"\b(?:reference|oracle|gold|host|cpu)\b", pixel_evidence, re.I) + and re.search(r"\b(?:pass|passed|green|\d+\s*/\s*\d+)\b", pixel_evidence, re.I) + ) + if test_f is None: + out.append( + Finding( + "ODO-1", + "evidence", + MANUAL, + "Regression coverage: verify via review_op; confirm checked pixelwise evidence", + "pixelwise checklist checked=%s" % pixel_checked, + g, + ) + ) + elif implementation_changed and (not tests_ok or not pixel_hard_evidence): + out.append( + Finding( + "ODO-1", + "evidence", + GAP, + "Changed implementation lacks regression test and/or hard pixelwise-equality evidence", + "regression tests ok=%s; checklist checked=%s; changed=%s" + % ( + tests_ok, + pixel_checked, + [Path(c).name for c in implementation_changed], + ), + g, + "Run the regression test against an independent reference, then check the " + "pixelwise item with the test, assertion, branches, and passing result.", + ) + ) + else: + out.append( + Finding( + "ODO-1", + "evidence", + PASS, + "Regression tests present + checked pixelwise-equality hard evidence", + pixel_evidence, + g, + ) + ) + + # ODO-2: baselines updated for changed configs + base_diff = git( + "diff", f"{base}...HEAD", "--", f"bench/config/operators/{P.op}.json" + ) + baselines_touched = bool( + re.search(r'gpu_time_us|gpu_gap_stddev_us|"baselines"|n_runs', base_diff) + ) + if not implementation_changed: + out.append( + Finding( + "ODO-2", + "evidence", + NA, + "No implementation change -> baseline update N-A", + "", + g, + ) + ) + elif baselines_touched: + out.append( + Finding( + "ODO-2", + "evidence", + PASS, + "Baselines updated for the operator", + "%s baselines changed vs %s" % (rel(P.bench_cfg), base), + g, + ) + ) + else: + out.append( + Finding( + "ODO-2", + "evidence", + GAP, + "Implementation changed but baselines not updated [requires CI regen]", + "no baseline delta in %s" % rel(P.bench_cfg), + g, + "Regenerate via the CI fan-out + bench/_internal/update_baseline.py " + "--from / --operator %s." % P.op, + ) + ) + + # ODO-4: leads exhausted + out.append(_leads_exhausted(results, g)) + + # ODO-5: API/ABI unchanged + out.append(_api_abi(P, base, g)) + + # ODO-6: perf: commit hygiene + out.append(_perf_hygiene(P, base, all_ops, g)) + + # ODO-7: regression-surface deltas and comparable committed-baseline guard. + out.append(_baseline_regression_gate(P, base, g)) + + # ODO-8: review/refactor lock-in gate + out.append(_review_refactor_gate(P, results, implementation_changed, g)) + + # ODO-9: memory-footprint growth gate + out.append(_memory_footprint_gate(results, implementation_changed, g)) + + touched_ops = _changed_perf_operators(changed, all_ops) + out.append(_operator_scope_gate(P, touched_ops, results, mr_iid, g)) + + # ODO-11: rerun the exact FakePlanar pairing contract at the final gate. + # Benchmark coverage can drift after preflight as configs and baselines are + # edited, so a preflight-only BEN-6 result is not durable evidence. + out.append(_fake_planar_coverage_gate(P, g)) + + # ODO-12: a binding-only campaign must prove the metric it changes. C++ + # timing can remain flat while Python overhead moves materially, so require + # every claimed target/reference-SKU reduction to clear combined standard + # error rather than accepting a mechanically flat C++ Impact table. + out.append(_binding_impact_gate(P, results, implementation_changed, g)) + + evidence_results = { + "Pixelwise equality to reference": _finding_status(out, "ODO-1"), + "Memory-footprint checks": _finding_status(out, "ODO-9"), + "Baselines updated": _finding_status(out, "ODO-2"), + "Baseline validation": _finding_status(out, "ODO-7"), + "Lead exhaustion": _finding_status(out, "ODO-4"), + "Review/refactor gate": _finding_status(out, "ODO-8"), + } + + # Insert ODO-3 in numeric order after the underlying evidence has been + # evaluated, so checked boxes can be correlated with hard gate results. + odo3 = _results_format(P, results, results_path, g, evidence_results) + insert_at = next((i for i, item in enumerate(out) if item.id == "ODO-4"), len(out)) + out.insert(insert_at, odo3) + return out + + +def _fake_planar_coverage_gate(P, g): + findings = review_op_findings(P, "bench") + if findings is None: + return Finding( + "ODO-11", + "evidence", + MANUAL, + "Final FakePlanar coverage could not be rerun", + f"python3 tools/review_op.py {P.Op} --domain bench", + g, + ) + ben6 = next((item for item in findings if item["id"] == "BEN-6"), None) + if ben6 is None: + return Finding( + "ODO-11", + "evidence", + MANUAL, + "Final FakePlanar coverage result is missing", + "review_op bench output has no BEN-6 finding", + g, + ) + status = ben6.get("status") + summary = ben6.get("summary", "") + if status == GAP: + return Finding( + "ODO-11", + "evidence", + GAP, + "Final FakePlanar coverage is incomplete", + f"BEN-6:{summary}", + g, + "Add the exact advanced same-tier native/FakePlanar pairs, then rerun evidence.", + ) + if status == NA: + return Finding( + "ODO-11", + "evidence", + NA, + "FakePlanar comparison is not applicable", + f"BEN-6:{summary}", + g, + ) + if status == PASS: + return Finding( + "ODO-11", + "evidence", + PASS, + "Final FakePlanar coverage is exact", + f"BEN-6:{summary}", + g, + ) + return Finding( + "ODO-11", + "evidence", + MANUAL, + "Final FakePlanar coverage status is unresolved", + f"BEN-6 status={status!r}: {summary}", + g, + ) + + +def _binding_impact_gate(P, results, implementation_changed, g): + gid = "ODO-12" + binding = rel(P.pybind).replace("\\", "/") + changed = {path.replace("\\", "/") for path in implementation_changed} + if changed != {binding}: + return Finding( + gid, + "evidence", + NA, + "Python-overhead significance gate applies to binding-only optimizations", + "changed implementation files=" + ", ".join(sorted(changed or {"none"})), + g, + ) + + try: + summary = parse_summary(results or "") + except SummaryError as exc: + return Finding( + gid, + "evidence", + GAP, + "Binding-only impact cannot be validated from the optimization summary", + str(exc), + g, + "Generate a valid optimization summary.", + ) + + metadata = summary.metadata + baseline = git_json(metadata.baseline_commit, rel(P.bench_cfg)) + candidate = _load_json(P.bench_cfg) + sku_map = _load_json(REPO / "bench/config/sku_map.json") + if not isinstance(baseline, dict) or not isinstance(candidate, dict): + return Finding( + gid, + "evidence", + GAP, + "Binding-only impact benchmark data is unavailable", + f"baseline={metadata.baseline_commit}; candidate={rel(P.bench_cfg)}", + g, + "Restore the declared baseline/candidate benchmark artifacts and rerun.", + ) + entries = sku_map.get("entries") if isinstance(sku_map, dict) else None + references = [ + item.get("stem") + for item in (entries or []) + if isinstance(item, dict) and isinstance(item.get("stem"), str) + ] + if not references: + return Finding( + gid, + "evidence", + GAP, + "Binding-only impact has no configured reference SKUs", + "bench/config/sku_map.json contains no valid stems", + g, + ) + + def payload(config, case_key, sku): + config_key = case_key.split("[", 1)[0] + try: + value = config["configs"][config_key]["baselines"][case_key][sku] + except (KeyError, TypeError): + return None + return value if isinstance(value, dict) else None + + def metrics(value): + fields = ( + "gpu_time_us_cpp", + "gpu_time_us_python", + "gpu_gap_stddev_us", + "n_runs", + ) + try: + result = tuple(float(value[field]) for field in fields) + except (KeyError, TypeError, ValueError): + return None + cpp, python, gap_stddev, runs = result + if ( + not all(math.isfinite(item) for item in result) + or cpp <= 0 + or python <= 0 + or gap_stddev < 0 + or runs < 2 + or not runs.is_integer() + ): + return None + return result + + evidence = [] + failures = [] + for case_key in metadata.optimized_cases: + short = case_key.split("[", 1)[0] + for sku in references: + before = metrics(payload(baseline, case_key, sku)) + after = metrics(payload(candidate, case_key, sku)) + if before is None or after is None: + failures.append( + f"{sku} {short}: missing timing/paired-gap-dispersion/run metrics" + ) + continue + b_cpp, b_python, b_gap_stddev, b_runs = before + a_cpp, a_python, a_gap_stddev, a_runs = after + before_gap = b_python - b_cpp + after_gap = a_python - a_cpp + reduction = before_gap - after_gap + standard_error = math.sqrt( + b_gap_stddev**2 / b_runs + a_gap_stddev**2 / a_runs + ) + reasons = [] + if reduction <= standard_error: + reasons.append("reduction does not clear combined standard error") + if DEFAULT_BENCHMARK_QUALITY.absolute_parity_exceeds_limit(after_gap): + reasons.append( + "candidate gap exceeds " + f"{DEFAULT_BENCHMARK_QUALITY.max_perf_diff_us:.0f} us" + ) + line = ( + f"{sku} {short}: gap {before_gap:.2f}->{after_gap:.2f} us; " + f"reduction {reduction:.2f} us; paired-gap combined SE " + f"{standard_error:.2f} us" + ) + evidence.append(line) + if reasons: + failures.append(line + " (" + "; ".join(reasons) + ")") + + if failures: + return Finding( + gid, + "evidence", + GAP, + "Binding-only Python-overhead improvement is not proven", + _clip_output("; ".join(failures)), + g, + "Collect repeated paired C++/Python reference/candidate artifacts, reimport " + "their baselines, or improve the binding until every target/reference-SKU " + "reduction clears paired-gap combined standard error and the candidate gap " + "stays within the parity limit.", + ) + return Finding( + gid, + "evidence", + PASS, + "Binding-only Python-overhead improvement clears paired-gap combined standard error", + _clip_output("; ".join(evidence)), + g, + ) + + +def _results_format(P, results, results_path, g, evidence_results): + gid = "ODO-3" + if not results: + return Finding( + gid, + "evidence", + GAP, + "Results summary not provided (pass --results or the MR description)", + "expected one bounded cvcuda-optimize-summary:v1 block", + g, + "Generate the v1 summary with --phase summary and put it in the MR description.", + ) + try: + summary = parse_summary(results) + except SummaryError as exc: + return Finding( + gid, + "evidence", + GAP, + "Results summary is missing or malformed", + str(exc), + g, + "Regenerate the bounded v1 block with --phase summary.", + ) + + metadata = summary.metadata + if metadata.baseline_commit == metadata.candidate_commit: + return Finding( + gid, + "evidence", + GAP, + "Summary baseline and candidate revisions are identical", + f"commit={metadata.candidate_commit}", + g, + "Choose the committed pre-optimization benchmark revision and refresh the summary.", + ) + if not git_success("merge-base", "--is-ancestor", metadata.baseline_commit, "HEAD"): + return Finding( + gid, + "evidence", + GAP, + "Summary baseline is not an ancestor of the candidate", + f"baseline={metadata.baseline_commit}; candidate={resolve_commit('HEAD')}", + g, + "Choose the committed pre-optimization benchmark revision and refresh the summary.", + ) + baseline = git_json(metadata.baseline_commit, rel(P.bench_cfg)) + if baseline is None: + return Finding( + gid, + "evidence", + GAP, + "Declared baseline benchmark config cannot be loaded", + f"baseline={metadata.baseline_commit}:{rel(P.bench_cfg)}", + g, + "Choose a baseline commit containing valid benchmark config JSON " + "for this operator, then refresh the summary.", + ) + candidate = _load_json(P.bench_cfg) + sku_map = _load_json(REPO / "bench/config/sku_map.json") + result = validate_summary( + summary, + baseline_config=baseline, + candidate_config=candidate, + sku_map=sku_map, + expected_operator=P.Op, + expected_candidate_commit=resolve_commit("HEAD"), + evidence_results=evidence_results, + ) + if not result.ok: + errors = [ + f"{getattr(item, 'code', 'summary')}: {getattr(item, 'message', item)}" + for item in result.errors + ] + return Finding( + gid, + "evidence", + GAP, + "Versioned optimization summary failed deterministic validation", + "; ".join(errors[:8]) + (" ..." if len(errors) > 8 else ""), + g, + "Refresh the summary, supply the named hard evidence, and rerun the gate.", + ) + warnings = [ + f"{getattr(item, 'code', 'summary')}: {getattr(item, 'message', item)}" + for item in result.warnings + ] + return Finding( + gid, + "evidence", + PASS, + "Versioned optimization summary is structurally and numerically valid", + "optimized cases=%d; full cases=%d%s" + % ( + len(metadata.optimized_cases), + _case_count(candidate), + "; warnings: " + "; ".join(warnings) if warnings else "", + ), + g, + ) + + +def _leads_exhausted(results, g): + gid = "ODO-4" + checked, evidence = _checklist_item(results, "Lead exhaustion") + measured = bool(re.search(r"\d+(?:\.\d+)?\s*(?:%|x\b|u?s\b|ms\b)", evidence, re.I)) + at_ridge = bool(re.search(r"\b(?:at[- ]?ridge|ridge)\b", evidence, re.I)) + struck = bool( + re.search( + r"\b(?:3|three)\b[^\n]{0,120}\b(?:strike|struck|failed leads?)\b", + evidence, + re.I, + ) + ) + if not checked or not measured or not (at_ridge or struck): + return Finding( + gid, + "evidence", + GAP, + "Lead exhaustion lacks checked, measurement-backed hard evidence", + evidence or "Lead exhaustion checklist item missing or unchecked", + g, + "Check the item only after citing measured at-ridge evidence or three " + "measured post-win strikes.", + ) + return Finding( + gid, + "evidence", + PASS, + "Lead exhaustion is checked with measurement-backed evidence", + evidence, + g, + ) + + +def _review_refactor_gate(P, results, implementation_changed, g): + gid = "ODO-8" + if not implementation_changed: + return Finding( + gid, + "evidence", + NA, + "No implementation change -> review/refactor lock-in gate N-A", + "", + g, + ) + if not results: + return Finding( + gid, + "evidence", + GAP, + "Review/refactor lock-in gate evidence missing", + "provide --results with a Review/refactor gate section", + g, + "Run a review pass and `python3 tools/refactor_op.py %s --phase assess`, " + "then record the disposition in the Results Summary." % P.Op, + ) + + checked, evidence = _checklist_item(results, "Review/refactor gate") + if not checked: + return Finding( + gid, + "evidence", + GAP, + "Review/refactor lock-in gate is missing or unchecked", + evidence or "Review/refactor checklist item missing", + g, + "Run the review and refactor assessment, record hard evidence, then check it.", + ) + + missing = [] + if not re.search(r"(?:tools/)?refactor_op\.py", evidence): + missing.append("refactor_op.py command") + if not re.search(r"--phase\s+assess|\bphase\s*[=:]\s*assess\b", evidence, re.I): + missing.append("--phase assess") + if not re.search( + r"\b(PASS|RECOMMENDATION|MANUAL|RED-\d+|recommendations?)\b", + evidence, + re.I, + ): + missing.append("recommendation disposition") + + applied = _refactor_applied(evidence) + if applied: + if not re.search(r"--phase\s+verify|\bphase\s*[=:]\s*verify\b", evidence, re.I): + missing.append("--phase verify") + if not re.search( + r"\b(cvcuda_test|run_tests|pytest|frozen[- ]?test|tests?\s+passed)\b", + evidence, + re.I, + ): + missing.append("frozen operator tests") + if not re.search( + r"\b(run_bench|benchmark|benchmarks?\s+passed|no regression|performance preserved)\b", + evidence, + re.I, + ): + missing.append("post-refactor benchmark proof") + + if missing: + return Finding( + gid, + "evidence", + GAP, + "Review/refactor lock-in gate evidence incomplete", + "missing: " + ", ".join(missing), + g, + "Record the review pass, `refactor_op.py --phase assess` result, and if a " + "refactor was applied, the verify/test/benchmark proof.", + ) + + return Finding( + gid, + "evidence", + PASS, + "Review/refactor lock-in gate evidence present", + "refactor applied=%s; %s" % ("yes" if applied else "no", evidence), + g, + ) + + +def _memory_footprint_sections(markdown): + """Return visible, canonical Memory footprint sections from Markdown.""" + markdown = re.sub(r"|$)", "", markdown or "", flags=re.S) + visible = [] + fence = None + for line in markdown.splitlines(): + marker = re.match(r"^\s*(`{3,}|~{3,})", line) + if fence is None and marker: + fence = (marker.group(1)[0], len(marker.group(1))) + visible.append("") + elif fence is not None: + closing = re.fullmatch(r"\s*(`{3,}|~{3,})\s*", line) + if ( + closing + and closing.group(1)[0] == fence[0] + and len(closing.group(1)) >= fence[1] + ): + fence = None + visible.append("") + else: + visible.append(line) + + headings = [ + index + for index, line in enumerate(visible) + if re.fullmatch(r"## Memory footprint\s*", line) + ] + + sections = [] + for start in headings: + end = len(visible) + for index in range(start + 1, len(visible)): + if re.match(r"^#{1,2}\s+", visible[index]): + end = index + break + sections.append("\n".join(visible[start:end])) + return sections + + +def _memory_footprint_gate(results, _implementation_changed, g): + gid = "ODO-9" + sections = _memory_footprint_sections(results) + if len(sections) != 1: + return Finding( + gid, + "evidence", + GAP, + "Memory footprint section missing or duplicated", + "expected exactly one visible `## Memory footprint` section; found %d" + % len(sections), + g, + "Add one canonical section outside comments/code fences and remove duplicates.", + ) + section = sections[0] + + fields = ( + ("Peak attributable increase", r"([0-9]{1,20}) B"), + ("New runtime CUDA allocations/frees", r"(no|yes)"), + ("Evidence", r"(.+?)"), + ) + occurrences = { + label: len(re.findall(r"^%s\s*:" % re.escape(label), section, re.M)) + for label, _ in fields + } + errors = [ + "%s: found %d occurrences" % (label, count) + for label, count in occurrences.items() + if count != 1 + ] + values = {} + if not errors: + for label, value_pattern in fields: + match = re.search( + r"^%s:[ \t]*%s[ \t]*$" % (re.escape(label), value_pattern), + section, + re.M, + ) + if not match: + errors.append("%s: malformed declaration" % label) + continue + values[label] = match.group(1) + + if errors: + return Finding( + gid, + "evidence", + GAP, + "Memory footprint declarations are missing, duplicated, or malformed", + "; ".join(errors), + g, + "Provide exactly one canonical aggregate peak-live increase, allocation/free " + "flag, and evidence line; do not net unrelated decreases.", + ) + + evidence = values["Evidence"].strip() + if not evidence or re.fullmatch( + r"(?:[-.]+|none|<[^>]+>|n/?a|tbd|todo|pending|placeholder|unknown|unmeasured|" + r"not\s+(?:measured|available|provided)|" + r"(?:measurement|evidence|review)(?:\s+is)?\s+" + r"(?:pending|unknown|unavailable|not\s+(?:available|provided)))\.?", + evidence, + re.I, + ): + return Finding( + gid, + "evidence", + GAP, + "Memory footprint evidence is a placeholder", + "Evidence: %s" % evidence, + g, + "Replace the placeholder with the measurement or source-inspection evidence.", + ) + + try: + increase = int(values["Peak attributable increase"]) + except ValueError: + return Finding( + gid, + "evidence", + GAP, + "Memory footprint declarations are missing, duplicated, or malformed", + "Peak attributable increase: malformed declaration", + g, + "Use a non-negative decimal integer followed by `B`.", + ) + new_allocations = values["New runtime CUDA allocations/frees"] == "yes" + if increase > MEMORY_AUTO_ACCEPT_BYTES or new_allocations: + reasons = [] + if increase > MEMORY_AUTO_ACCEPT_BYTES: + reasons.append("%d B exceeds the 10 MB limit" % increase) + if new_allocations: + reasons.append("a new runtime CUDA allocation/free path is declared") + return Finding( + gid, + "evidence", + MANUAL, + "Memory footprint change requires explicit human review", + "%s; Evidence: %s" % ("; ".join(reasons), evidence), + g, + "Obtain explicit human approval outside this offline checker.", + ) + + return Finding( + gid, + "evidence", + PASS, + "Memory footprint change is within the 10 MB limit", + "%d B increase; no new runtime CUDA allocations/frees; Evidence: %s" + % (increase, evidence), + g, + ) + + +def _checklist_item(results, label): + if not results: + return False, "" + match = re.search( + r"^- \[([ xX])\] \*\*%s\*\*\s+—\s+(.+?)\s*$" % re.escape(label), + results, + re.M, + ) + if not match: + return False, "" + return match.group(1).lower() == "x", match.group(2).strip() + + +def _finding_status(findings, finding_id): + return next((item.status for item in findings if item.id == finding_id), MANUAL) + + +def _case_count(config): + if not isinstance(config, dict): + return 0 + return sum( + len(item.get("baselines", {})) + for item in config.get("configs", {}).values() + if isinstance(item, dict) and isinstance(item.get("baselines", {}), dict) + ) + + +def _changed_perf_operators(paths, all_ops): + """Attribute deterministic per-operator config, binding, and priv deltas.""" + touched = set() + known = set(all_ops) + for path in paths: + match = re.fullmatch(r"bench/config/operators/([a-z0-9]+)\.json", path) + if match and match.group(1) in known: + touched.add(match.group(1)) + continue + normalized = path.replace("\\", "/") + match = re.fullmatch( + r"python/mod_cvcuda/operators/Op([A-Za-z0-9]+)\.cpp", normalized + ) + if match: + body = re.sub(r"[^a-z0-9]", "", match.group(1).lower()) + if body in known: + touched.add(body) + continue + if not normalized.startswith("src/cvcuda/priv/Op"): + continue + body = re.sub(r"[^a-z0-9]", "", Path(normalized).stem[2:].lower()) + candidates = [op for op in all_ops if body.startswith(op)] + if candidates: + touched.add(max(candidates, key=len)) + return touched + + +def _operator_scope_gate(P, touched_ops, results, mr_iid, guide): + """Enforce one operator unless one exact secondary scope was reviewed.""" + + primary = P.op.casefold() + actual = {operator.casefold() for operator in touched_ops} + declared = () + metadata_operator = P.Op + try: + metadata = parse_summary(results or "").metadata + metadata_operator = metadata.operator + declared = tuple(metadata.secondary_operators) + except SummaryError: + pass + + if not declared: + foreign = sorted(actual - {primary}) + return Finding( + "ODO-10", + "evidence", + GAP if foreign else PASS, + ( + "Optimization MR spans multiple operators" + if foreign + else "Optimization MR is scoped to one operator" + ), + "attributed operators=" + ", ".join(sorted(actual or {primary})), + guide, + ( + "Split the work into one perf MR and one v1 summary per operator." + if foreign + else "" + ), + ) + + normalized_declared = {operator.casefold() for operator in declared} + expected = {primary, *normalized_declared} + reviewed = metadata_operator.casefold() == primary and is_reviewed_secondary_scope( + mr_iid, metadata_operator, declared + ) + exact_changes = actual == expected + if reviewed and exact_changes: + return Finding( + "ODO-10", + "evidence", + PASS, + "Optimization MR uses a code-reviewed secondary baseline scope", + "MR !%s; attributed operators=%s" % (mr_iid, ", ".join(sorted(actual))), + guide, + ) + + reasons = [] + if not reviewed: + reasons.append("declaration is not allowlisted for this exact MR/primary pair") + if not exact_changes: + reasons.append( + "declared operators=%s but attributed operators=%s" + % ( + ", ".join(sorted(expected)), + ", ".join(sorted(actual)) or "none", + ) + ) + return Finding( + "ODO-10", + "evidence", + GAP, + "Optimization MR secondary operator scope is not authorized", + "; ".join(reasons), + guide, + "Use one operator, or obtain a code-reviewed exact MR/primary/secondary " + "policy entry and make the changed baseline configs match the declaration.", + ) + + +def _api_abi(P, base, g): + gid = "ODO-5" + paths = [ + f"src/cvcuda/include/cvcuda/Op{P.Op}.h", + f"src/cvcuda/include/cvcuda/Op{P.Op}.hpp", + f"python/mod_cvcuda/operators/Op{P.Op}.cpp", + ] + public_diff = git("diff", f"{base}...HEAD", "--", *paths[:2]) + binding_diff = git("diff", f"{base}...HEAD", "--", paths[2]) + sig = [ + ln + for ln in public_diff.splitlines() + if ln[:1] in "+-" + and ln[1:2] != ln[:1] + and re.search(r"CVCUDA_PUBLIC|operator\(\)|Submit\s*\(", ln) + ] + if not public_diff.strip() and not binding_diff.strip(): + return Finding( + gid, + "evidence", + PASS, + "API/ABI surface unchanged (no diff in public headers/binding)", + "no changes in " + ", ".join(Path(p).name for p in paths), + g, + ) + if sig: + return Finding( + gid, + "evidence", + GAP, + "Public API/ABI signature(s) changed — that is feature work, not optimization", + "; ".join(s.strip()[:80] for s in sig[:3]), + g, + "Revert the signature change or split it into a separate feature MR.", + ) + + if binding_diff.strip(): + merge_base = git("merge-base", base, "HEAD").strip() + baseline_source = git("show", f"{merge_base}:{paths[2]}") if merge_base else "" + candidate_source = git("show", f"HEAD:{paths[2]}") + baseline_surface = _binding_api_snapshot(baseline_source, P.Op) + candidate_surface = _binding_api_snapshot(candidate_source, P.Op) + if baseline_surface is None or candidate_surface is None: + return Finding( + gid, + "evidence", + MANUAL, + "Python binding surface could not be parsed safely", + "unable to compare ExportOp%s registrations and bound callable signatures" + % P.Op, + g, + ) + if baseline_surface != candidate_surface: + changed_parts = [] + if baseline_surface[0] != candidate_surface[0]: + changed_parts.append("m.def registration/arguments/defaults") + if baseline_surface[1] != candidate_surface[1]: + changed_parts.append("bound callable signature(s)") + if baseline_surface[2] != candidate_surface[2]: + changed_parts.append("bound callable type alias(es)") + return Finding( + gid, + "evidence", + GAP, + "Public Python API signature(s) changed — that is feature work, not optimization", + "; ".join(changed_parts), + g, + "Revert the signature change or split it into a separate feature MR.", + ) + + if not public_diff.strip() and binding_diff.strip(): + return Finding( + gid, + "evidence", + PASS, + "API/ABI surface unchanged (binding implementation only)", + "Op%s.cpp changed with identical m.def registrations and bound callable signatures" + % P.Op, + g, + ) + return Finding( + gid, + "evidence", + MANUAL, + "Public headers changed but no signature lines detected — verify API/ABI stability", + "diff in " + ", ".join(Path(p).name for p in paths[:2]), + g, + ) + + +def _baseline_regression_gate(P, base, g): + gid = "ODO-7" + tool = REPO / "bench" / "_internal" / "validate_baselines.py" + if not tool.exists(): + return Finding( + gid, + "evidence", + GAP, + "Committed-baseline regression gate could not run", + rel(tool) + " not found", + g, + "Restore bench/_internal/validate_baselines.py and rerun the evidence gate.", + ) + + cmd = [ + sys.executable, + str(tool), + "--config-dir", + str(REPO / "bench" / "config"), + "--reject-regressions-from", + base, + ] + try: + r = subprocess.run( + cmd, cwd=str(REPO), capture_output=True, text=True, timeout=120 + ) + except subprocess.TimeoutExpired as exc: + return Finding( + gid, + "evidence", + GAP, + "Committed-baseline regression gate timed out", + _clip_output(str(exc)), + g, + "Fix the baseline validation hang or run the validator separately, " + "then rerun the evidence gate.", + ) + except OSError as exc: + return Finding( + gid, + "evidence", + GAP, + "Committed-baseline regression gate could not run", + str(exc), + g, + "Fix the local Python/script environment and rerun the evidence gate.", + ) + + command = " ".join( + [ + "python3", + "bench/_internal/validate_baselines.py", + "--reject-regressions-from", + base, + ] + ) + evidence = _clip_output("\n".join(x for x in (r.stdout, r.stderr) if x.strip())) + if r.returncode == 0: + return Finding( + gid, + "evidence", + PASS, + "Comparable committed baselines do not regress vs %s" % base, + command + ("\n" + evidence if evidence else ""), + ".agents/guidance/OPTIMIZATION_GUIDELINES.md#golden-rules", + ) + + return Finding( + gid, + "evidence", + GAP, + "Comparable committed baseline regression(s) detected or validation failed", + command + f"\nexit={r.returncode}" + ("\n" + evidence if evidence else ""), + ".agents/guidance/OPTIMIZATION_GUIDELINES.md#golden-rules", + "Fix the performance regression or revert the regressed baseline row(s). " + "Only new/non-comparable baseline rows may bypass this check.", + ) + + +def _perf_hygiene(P, base, all_ops, g): + gid = "ODO-6" + merge_base = git("merge-base", base, "HEAD").strip() + if not merge_base: + return Finding( + gid, + "evidence", + MANUAL, + "Unable to resolve the changed commit range", + f"git merge-base {base} HEAD returned no commit", + g, + ) + log = git("log", f"{merge_base}..HEAD", "--no-merges", "--format=%H%x1f%s") + commits = [ln.split("\x1f") for ln in log.splitlines() if "\x1f" in ln] + if not commits: + return Finding(gid, "evidence", MANUAL, "No commits vs base to check", "", g) + bad = [] + regression_fixes = [] + for sha, subj in commits: + files = git("show", "--name-only", "--format=", sha).splitlines() + touches_implementation = any( + _is_op_implementation(f.strip(), P, all_ops) for f in files if f.strip() + ) + if not touches_implementation or re.match(r"^perf(?:\([^)]+\))?:\s+\S", subj): + continue + accepted, detail = _regression_backed_fix(P, all_ops, sha, subj) + if accepted: + regression_fixes.append("%s -> %s" % (detail[:8], sha[:8])) + else: + bad.append("%s %s (%s)" % (sha[:8], subj[:60], detail)) + if bad: + return Finding( + gid, + "evidence", + GAP, + "Implementation-changing commit(s) lack perf: or a scoped regression-backed fix", + "; ".join(bad[:3]), + g, + "Use perf: for kept optimization commits. A kernel bug fix must be a scoped " + "fix(): commit immediately after its scoped, operator-specific, " + "tests-only test(): regression commit.", + ) + detail = "%d commit(s) checked" % len(commits) + if regression_fixes: + detail += "; regression-backed fix pair(s): " + ", ".join(regression_fixes) + return Finding( + gid, + "evidence", + PASS, + "Implementation-changing commits use perf: or a scoped regression-backed fix pair", + detail, + g, + ) + + +def _regression_backed_fix(P, all_ops, sha, subject): + """Accept a kernel fix only when its immediate parent is its tests-only repro.""" + fix = re.match(r"^fix\(([^()]+)\):\s+\S", subject) + if not fix: + return False, "not a scoped fix(): subject" + scope = fix.group(1).casefold() + if scope != P.op.casefold(): + return False, "fix scope does not match %s" % P.op + + parents = git("show", "-s", "--format=%P", sha).split() + if len(parents) != 1: + return False, "fix must have exactly one immediate parent" + parent = parents[0] + parent_subject = git("show", "-s", "--format=%s", parent).strip() + test = re.match(r"^test\(([^()]+)\):\s+\S", parent_subject) + if not test: + return False, "immediate parent is not a scoped test(): commit" + test_scope = test.group(1).casefold() + if test_scope != scope: + return False, "test and fix scopes do not match" + + test_files = [ + f.strip() + for f in git("show", "--name-only", "--format=", parent).splitlines() + if f.strip() + ] + if not test_files or any( + not f.replace("\\", "/").startswith("tests/") for f in test_files + ): + return False, "immediate parent is not tests-only" + if not any(_is_op_test(f, P, all_ops) for f in test_files): + return False, "immediate parent has no operator-specific regression test" + return True, parent + + +def _is_op_test(path, P, all_ops): + """Return whether a tests/ filename belongs to the requested operator.""" + pl = path.replace("\\", "/") + if not pl.startswith("tests/"): + return False + body = re.sub(r"[^a-z0-9]", "", Path(pl).stem.lower()) + for prefix in ("testop", "test"): + if body.startswith(prefix): + body = body.removeprefix(prefix) + break + candidates = [op for op in all_ops if body.startswith(op)] or ( + [P.op] if body.startswith(P.op) else [] + ) + return max(candidates, key=len, default=None) == P.op + + +# ----------------------------------------------------------------------------- small utils +def _load_json(p): + t = read(p) + if not t: + return None + try: + return json.loads(t) + except json.JSONDecodeError: + return None + + +def _clip_output(text, limit=1200): + text = (text or "").strip() + if len(text) <= limit: + return text + return text[:limit].rstrip() + "\n... output truncated ..." + + +def _refactor_applied(section): + if re.search( + r"no refactors? applied|refactors? applied\s*[:|=]\s*(?:no|false|n/a)\b", + section, + re.I, + ): + return False + return bool( + re.search( + r"refactors? applied\s*[:|=]\s*(?:yes|true)\b|" + r"applied refactor|refactor commit|RED-\d+\s+resolved", + section, + re.I, + ) + ) + + +# ================================================================================ driver +def render_md(P, phase, findings): + icon = {PASS: "✅", GAP: "❌", NA: "➖", MANUAL: "🔍"} + lines = [f"# optimize-op: {P.Op} — phase={phase}", ""] + counts = {} + for f in findings: + counts[f.status] = counts.get(f.status, 0) + 1 + lines.append( + f"- {icon.get(f.status, '?')} **{f.status}** `{f.id}` — {f.summary}" + ) + if f.evidence: + lines.append(f" - evidence: {f.evidence}") + if f.status == GAP and f.fix: + lines.append(f" - fix: {f.fix}") + if f.guideline: + lines.append(f" - ref: {f.guideline}") + gaps, man = counts.get(GAP, 0), counts.get(MANUAL, 0) + verdict = "PASS" if gaps == 0 and man == 0 else ("GAPS" if gaps else "NEEDS-REVIEW") + lines += [ + "", + f"**{phase} verdict: {verdict}** — " + + ", ".join(f"{k}={v}" for k, v in sorted(counts.items())), + "", + "Done = a re-run shows zero GAP and zero unresolved MANUAL.", + ] + return "\n".join(lines) + + +def render_json(P, phase, findings): + counts = {} + for f in findings: + counts[f.status] = counts.get(f.status, 0) + 1 + return json.dumps( + { + "operator": P.Op, + "op": P.op, + "phase": phase, + "findings": [vars(f) for f in findings], + "counts": counts, + "exit_gap": counts.get(GAP, 0), + "exit_manual": counts.get(MANUAL, 0), + "exit_blocking": counts.get(GAP, 0) + + (counts.get(MANUAL, 0) if phase == "evidence" else 0), + }, + indent=2, + sort_keys=True, + ) + + +def build_summary(P, args): + """Initialize or refresh the canonical bounded MR summary.""" + candidate = _load_json(P.bench_cfg) + sku_map = _load_json(REPO / "bench/config/sku_map.json") + if candidate is None: + raise SummaryError( + f"cannot load candidate benchmark config: {rel(P.bench_cfg)}" + ) + if sku_map is None: + raise SummaryError("cannot load bench/config/sku_map.json") + + if args.results: + description = read_results(args.results) + if description is None: + raise SummaryError(f"cannot read MR description: {args.results}") + parsed = parse_summary(description) + baseline = git_json(parsed.metadata.baseline_commit, rel(P.bench_cfg)) + if baseline is None: + raise SummaryError( + "cannot load baseline benchmark config at " + f"{parsed.metadata.baseline_commit}:{rel(P.bench_cfg)}" + ) + return refresh_summary( + description, + baseline_config=baseline, + candidate_config=candidate, + sku_map=sku_map, + state=args.state, + candidate_commit=resolve_commit("HEAD"), + impact_metric=args.impact_metric, + secondary_operators=args.secondary_operator, + ) + + missing = [ + flag + for flag, value in ( + ("--benchmark-base", args.benchmark_base), + ("--optimized-cases-file", args.optimized_cases_file), + ("--state", args.state), + ("--bottleneck", args.bottleneck), + ("--profile-evidence", args.profile_evidence), + ) + if not value + ] + if missing: + raise SummaryError("initializing a summary requires " + ", ".join(missing)) + + baseline_commit = resolve_commit(args.benchmark_base) + candidate_commit = resolve_commit("HEAD") + if not baseline_commit: + raise SummaryError(f"cannot resolve benchmark base {args.benchmark_base!r}") + if not candidate_commit: + raise SummaryError("cannot resolve HEAD") + if baseline_commit == candidate_commit: + raise SummaryError("benchmark base must precede HEAD, not equal it") + if not git_success( + "merge-base", "--is-ancestor", baseline_commit, candidate_commit + ): + raise SummaryError("benchmark base must be an ancestor of HEAD") + baseline = git_json(baseline_commit, rel(P.bench_cfg)) + if baseline is None: + raise SummaryError( + f"cannot load baseline benchmark config at {baseline_commit}:{rel(P.bench_cfg)}" + ) + try: + optimized_cases = tuple( + line.strip() + for line in Path(args.optimized_cases_file) + .read_text(encoding="utf-8") + .splitlines() + if line.strip() + ) + except OSError as exc: + raise SummaryError( + f"cannot read optimized cases file {args.optimized_cases_file}: {exc}" + ) from exc + metadata = SummaryMetadata( + operator=P.Op, + state=args.state, + baseline_commit=baseline_commit, + candidate_commit=candidate_commit, + optimized_cases=optimized_cases, + impact_metric=args.impact_metric or "cpp_time", + secondary_operators=tuple(args.secondary_operator or ()), + ) + return generate_summary( + metadata, + baseline_config=baseline, + candidate_config=candidate, + sku_map=sku_map, + bottleneck=args.bottleneck, + profile_evidence=args.profile_evidence, + ) + + +def main(argv=None): + ap = argparse.ArgumentParser( + description="Deterministic DoD checker for an optimization campaign (CV-CUDA)." + ) + ap.add_argument("operator", help="Operator name (PascalCase, e.g. Resize)") + ap.add_argument( + "--phase", required=True, choices=["preflight", "evidence", "summary"] + ) + ap.add_argument( + "--base", + default="main", + help="git base ref for the changed-set diff (default: main)", + ) + ap.add_argument( + "--results", + default=None, + help="path to the MR description, or '-' for stdin", + ) + ap.add_argument( + "--benchmark-base", + default=None, + help="pre-optimization benchmark revision used to initialize a summary", + ) + ap.add_argument( + "--optimized-cases-file", + default=None, + help="newline-delimited exact expanded case keys used to initialize a summary", + ) + ap.add_argument("--state", choices=["provisional", "final"], default=None) + ap.add_argument( + "--impact-metric", + choices=["cpp_time", "python_overhead"], + default=None, + help="Impact metric for summary initialization or migration", + ) + ap.add_argument( + "--secondary-operator", + action="append", + default=None, + help=( + "declare a reviewed secondary baseline operator (repeatable; " + "does not grant authorization by itself)" + ), + ) + ap.add_argument( + "--mr-iid", + default=None, + help="GitLab MR IID used by the evidence-phase secondary-scope policy", + ) + ap.add_argument( + "--bottleneck", + choices=["Memory-bound", "Compute-bound"], + default=None, + ) + ap.add_argument("--profile-evidence", default=None) + ap.add_argument("--format", default="md", choices=["md", "json"]) + ap.add_argument("--out", default=None) + args = ap.parse_args(argv) + + P = resolve_op(args.operator) + if args.phase == "summary": + try: + report = build_summary(P, args) + except (OSError, SummaryError, ValueError) as exc: + print(f"optimize-op summary: {exc}", file=sys.stderr) + return 2 + if args.out: + Path(args.out).write_text(report.rstrip() + "\n", encoding="utf-8") + else: + print(report) + return 0 + if args.phase == "preflight": + findings = check_preflight(P, args.base) + else: + findings = check_evidence(P, args.base, args.results, args.mr_iid) + + report = ( + render_md(P, args.phase, findings) + if args.format == "md" + else render_json(P, args.phase, findings) + ) + print(report) + if args.out: + Path(args.out).write_text(report + "\n", encoding="utf-8") + blocking = {GAP, MANUAL} if args.phase == "evidence" else {GAP} + return 1 if any(f.status in blocking for f in findings) else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/refactor_op.py b/tools/refactor_op.py new file mode 100755 index 000000000..446b0eb83 --- /dev/null +++ b/tools/refactor_op.py @@ -0,0 +1,1258 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. +"""Deterministic per-operator refactoring / redundancy checker for CV-CUDA. + +Implements the catalog defined in .agents/guidance/REFACTOR_OP_GUIDELINES.md. Two phases: + + (default) assess : read-only candidate report of operator-scoped *semantic* redundancy + SonarQube CPD cannot express (near-duplicate Tensor/VarShape kernels, + reinvented shared utilities, dead code, ...). Findings are advisory + (RECOMMENDATION/MANUAL); the assess report is exit 0. + --phase verify : the strict, agent-independent PARITY gate for an applied refactor. + Deterministic, local, artifact-derived (git + parsed matrices): the + refactor must change nothing observable — frozen test surface, frozen + bench surface, public API/ABI, the declared feature matrix and the test + coverage matrix must all be identical base-vs-working. Bit-exactness is + proved by running the frozen tests locally when the MR scope includes a + refactor (flagged here as a manual proof). + +The checker is read-only, deterministic and idempotent: no clocks, no randomness, no network; +the changed-set comes from `git diff `. The same tree yields a byte-identical report. +Applying a refactor is the agent's job; this tool only assesses and verifies (see the skill). + +Usage: + python3 tools/refactor_op.py [--phase assess|verify] + [--domain impl|api|xcut|all] [--base ] [--format md|json] [--out PATH] [--apply] +""" + +import argparse +import hashlib +import json +import re +import subprocess +import sys +from dataclasses import dataclass +from pathlib import Path + +REPO = Path(__file__).resolve().parent.parent +GUIDE = ".agents/guidance/REFACTOR_OP_GUIDELINES.md" +AGENT_TOOLS = REPO / ".agents" / "tools" +if str(AGENT_TOOLS) not in sys.path: + sys.path.insert(0, str(AGENT_TOOLS)) + +from binding_api import binding_api_snapshot # noqa: E402 +from operator_source_map import ( # noqa: E402 + SHARED_KERNEL_SOURCES, + all_op_names, + legacy_belongs, +) + +PASS, GAP, NA, MANUAL, REC = "PASS", "GAP", "N-A", "MANUAL", "RECOMMENDATION" +DOMAINS = ("impl", "api", "xcut") + +# --- assess tunables (overridable from the guidelines doc's Curated data) ------------------- +SHINGLE_K = 5 # normalized-line k-gram window for the similarity fingerprint +SIM_THRESHOLD = 0.80 # Jaccard at/above which two blocks are "near-duplicate" +MIN_BLOCK_LINES = 6 # ignore blocks smaller than this (too small to be worth unifying) + +# The canonical "should-reuse" helpers. A locally-defined function whose name collides with one +# of these is a reinvented-wheel candidate (RED-10). Header paths are cited in the guidelines. +SHARED_UTILS = { + "SaturateCast", + "StaticCast", + "ConvertBaseTypeTo", + "TensorWrap", + "CreateSameShapeImageBatch", +} + +# Reusable Doxygen Limitations dtype row -> canonical token (feature-matrix parity, VER-4). +DTYPE_ROW = { + tuple(name.split()): token + for name, token in ( + ("8bit Unsigned", "u8"), + ("8bit Signed", "s8"), + ("16bit Unsigned", "u16"), + ("16bit Signed", "s16"), + ("32bit Unsigned", "u32"), + ("32bit Signed", "s32"), + ("16bit Float", "f16"), + ("32bit Float", "f32"), + ("64bit Float", "f64"), + ) +} + + +@dataclass +class Finding: + """One deterministic rule result in the emitted report.""" + + id: str + domain: str + status: str + summary: str + evidence: str = "" + guideline: str = "" + fix: str = "" + + +@dataclass +class OpPaths: + """Resolved file locations for one operator's refactor surface.""" + + op: str + Op: str + pyname: str + priv: list + header: Path + pybind: Path + hpp: Path + test_cpp: Path + bench_cpp: Path + test_py: Path + bench_py: Path + bench_cfg: Path + + +@dataclass +class Block: + """Normalized code block fingerprint used by the redundancy checks.""" + + name: str + start: int + end: int + norm: list # normalized non-empty body lines + shingles: list # stable shingle hashes of `norm` + + +# --------------------------------------------------------------------------- io / git helpers +def read(p: Path): + try: + return p.read_text(encoding="utf-8", errors="replace") + except (OSError, AttributeError): + return None + + +def rel(p: Path) -> str: + try: + return str(p.relative_to(REPO)) + except ValueError: + return str(p) + + +def git(*args): + try: + proc = subprocess.run( + ["git", *args], cwd=str(REPO), capture_output=True, text=True + ) + return proc.stdout if proc.returncode == 0 else "" + except OSError: + return "" + + +def git_show(ref: str, relpath: str): + """File content at :, or None if absent there.""" + try: + r = subprocess.run( + ["git", "show", f"{ref}:{relpath}"], + cwd=str(REPO), + capture_output=True, + text=True, + ) + return r.stdout if r.returncode == 0 else None + except OSError: + return None + + +def grep(pattern, text, flags=0): + if not text: + return [] + rx = re.compile(pattern, flags) + return [(i + 1, ln) for i, ln in enumerate(text.splitlines()) if rx.search(ln)] + + +def first_evidence(path: Path, hits): + if not hits: + return "" + ln, line = hits[0] + return f"{rel(path)}:{ln}: {line.strip()[:120]}" + + +# --------------------------------------------------------------- deterministic text analysis +def _stable_hash(s: str) -> int: + """Process-independent 64-bit hash. Uses hashlib (NOT Python's salted hash()) so the + report is byte-identical across runs/processes — the determinism guarantee.""" + return int.from_bytes( + hashlib.blake2b(s.encode("utf-8"), digest_size=8).digest(), "big" + ) + + +def _strip_code(text: str) -> str: + """Remove // and /* */ comments and the *contents* of string/char literals, preserving + newlines (so line numbers are stable) and brace structure. Deterministic, no state leak. + """ + out = [] + i, n = 0, len(text) + while i < n: + c = text[i] + nxt = text[i + 1] if i + 1 < n else "" + if c == "/" and nxt == "/": + while i < n and text[i] != "\n": + i += 1 + continue + if c == "/" and nxt == "*": + i += 2 + while i < n and not (text[i] == "*" and i + 1 < n and text[i + 1] == "/"): + if text[i] == "\n": + out.append("\n") + i += 1 + i += 2 + continue + if c in "\"'": + quote = c + out.append(quote) + i += 1 + while i < n and text[i] != quote: + if text[i] == "\\" and i + 1 < n: + i += 2 + continue + if text[i] == "\n": + out.append("\n") + i += 1 + out.append(quote) + i += 1 + continue + out.append(c) + i += 1 + return "".join(out) + + +def normalize_line(line: str) -> str: + """Whitespace-normalized code content of a line (comments/strings already stripped by + _strip_code upstream). Empty for blank/structure-only lines we don't fingerprint on. + """ + s = re.sub(r"\s+", " ", line.strip()) + return s + + +def shingle_hashes(norm_lines, k=SHINGLE_K): + if len(norm_lines) < k: + return [_stable_hash("\n".join(norm_lines))] if norm_lines else [] + return [ + _stable_hash("\n".join(norm_lines[i + offset] for offset in range(k))) + for i in range(len(norm_lines) - k + 1) + ] + + +def jaccard(a, b) -> float: + sa, sb = set(a), set(b) + if not sa and not sb: + return 0.0 + return len(sa & sb) / len(sa | sb) + + +_SIG_NAME = re.compile(r"([A-Za-z_]\w*)\s*$") + + +# `constexpr`: an `if constexpr (...)` head leaves `constexpr` as the token before the +# parens, which must not read as a function named "constexpr" (nested control flow would +# then pair with its enclosing function as a near-duplicate). +_CTRL_KW = { + "if", + "for", + "while", + "switch", + "catch", + "else", + "do", + "return", + "constexpr", +} + + +def extract_blocks(text, kind="cpp"): + """Brace/def-balanced function-or-kernel body extraction with stable (file) ordering, + at any nesting depth (functions live inside namespaces; pybind bodies are lambdas). + Returns Block(name, start_line, end_line, norm_lines, shingles). Tolerant: on any + structural oddity it yields fewer blocks rather than raising.""" + if not text: + return [] + if kind == "py": + return _extract_py_blocks(text) + stripped = _strip_code(text) + blocks = [] + stack = [] # one entry per open brace: (name_or_None, start_line, body_start_char) + paren = 0 + seg_start = 0 # char index just after the last statement boundary at paren depth 0 + line = 1 + i, n = 0, len(stripped) + while i < n: + c = stripped[i] + if c == "\n": + line += 1 + elif c == "(": + paren += 1 + elif c == ")": + paren = max(0, paren - 1) + elif paren == 0 and c == ";": + seg_start = i + 1 + elif c == "{": + name = _block_open(stripped[seg_start:i]) if paren == 0 else None + stack.append((name, line, i + 1)) + if paren == 0: + seg_start = i + 1 + elif c == "}": + name, start_line, body_start = stack.pop() if stack else (None, line, i) + if name: + norm = [ + normalize_line(ln) for ln in stripped[body_start:i].splitlines() + ] + norm = [ln for ln in norm if ln] + if len(norm) >= MIN_BLOCK_LINES: + blocks.append( + Block(name, start_line, line, norm, shingle_hashes(norm)) + ) + if paren == 0: + seg_start = i + 1 + i += 1 + return blocks + + +def _block_open(sig): + """If `sig` precedes a function/kernel/lambda body, return a name; else None (skips + namespace/class/struct/control-flow braces and brace-initializers).""" + paren = sig.rfind(")") + if paren < 0: + return None + open_paren = _match_open_paren(sig, paren) + if open_paren < 0: + return None + head = sig[:open_paren].rstrip() + if head.endswith("]"): # lambda capture list `[](...)` + return "lambda" + m = _SIG_NAME.search(head) + if not m: + return None + if m.group(1) in _CTRL_KW: + return None + return m.group(1) + + +def _match_open_paren(s, close_idx): + depth = 0 + i = close_idx + while i >= 0: + if s[i] == ")": + depth += 1 + elif s[i] == "(": + depth -= 1 + if depth == 0: + return i + i -= 1 + return -1 + + +def _extract_py_blocks(text): + lines = text.splitlines() + blocks = [] + i = 0 + while i < len(lines): + m = re.match(r"(\s*)def\s+(\w+)\s*\(", lines[i]) + if not m: + i += 1 + continue + indent, name, start = len(m.group(1)), m.group(2), i + 1 + body = [] + j = i + 1 + while j < len(lines): + ln = lines[j] + if ln.strip() and (len(ln) - len(ln.lstrip())) <= indent: + break + body.append(ln) + j += 1 + norm = [normalize_line(b) for b in body] + norm = [x for x in norm if x and not x.startswith("#")] + if len(norm) >= MIN_BLOCK_LINES: + blocks.append(Block(name, start, j, norm, shingle_hashes(norm))) + i = j + return blocks + + +def near_duplicate_pairs(blocks, threshold=SIM_THRESHOLD): + """All block pairs with jaccard >= threshold, sorted by (-ratio, a.start, b.start) + for byte-stable output.""" + pairs = [] + for x in range(len(blocks)): + for y in range(x + 1, len(blocks)): + r = jaccard(blocks[x].shingles, blocks[y].shingles) + if r >= threshold: + pairs.append((round(r, 4), blocks[x], blocks[y])) + pairs.sort(key=lambda t: (-t[0], t[1].start, t[2].start)) + return pairs + + +def duplicate_pair_findings(blocks, threshold, allow, make_finding): + """Build findings for near-duplicate block pairs while honoring the per-op allowlist.""" + findings = [] + for ratio, a, b in near_duplicate_pairs(blocks, threshold): + if a.name in allow or b.name in allow: + continue + findings.append(make_finding(ratio, a, b)) + return findings + + +# ----------------------------------------------------------------------- operator resolution +def resolve_op(arg: str): + stem = arg.strip() + op = stem.lower() + hdr_dir = REPO / "src/cvcuda/include/cvcuda" + header = next( + (h for h in sorted(hdr_dir.glob("Op*.h")) if h.stem[2:].lower() == op), + None, + ) + Op = header.stem[2:] if header else stem + header = header or hdr_dir / f"Op{stem}.h" + pybind = REPO / f"python/mod_cvcuda/operators/Op{Op}.cpp" + priv_dir = REPO / "src/cvcuda/priv" + priv = [ + cand + for cand in (priv_dir / f"Op{Op}.cu", priv_dir / f"Op{Op}.cpp") + if cand.exists() + ] + legacy = priv_dir / "legacy" + if legacy.is_dir(): + all_ops = all_op_names() + for g in sorted(legacy.glob("*.c*")): + if legacy_belongs(g.stem, op, all_ops): + priv.append(g) + # Shared/legacy kernel sources not matched by the op-name globs (e.g. filter.cu for + # Gaussian, the HQResize kernel headers) — see SHARED_KERNEL_SOURCES. + for extra in SHARED_KERNEL_SOURCES.get(op, []): + cand = priv_dir / extra + if cand.exists() and cand not in priv: + priv.append(cand) + paths = { + "header": header, + "hpp": hdr_dir / f"Op{Op}.hpp", + "pybind": pybind, + "test_cpp": REPO / f"tests/cvcuda/system/TestOp{Op}.cpp", + "test_py": REPO / f"tests/cvcuda/python/test_op{op}.py", + "bench_cpp": REPO / f"bench/cpp/ops/Bench{Op}.cpp", + "bench_py": REPO / f"bench/python/ops/bench_{op}.py", + "bench_cfg": REPO / f"bench/config/operators/{op}.json", + } + return OpPaths(op, Op, resolve_pyname(pybind, op), priv=priv, **paths) + + +def resolve_pyname(pybind: Path, op: str) -> str: + text = read(pybind) + if text: + names = re.findall(r'm\.def\(\s*"([a-z0-9_]+)"', text) + base = [n for n in names if not n.endswith("_into")] + if base: + return sorted(base, key=len)[0] + return op + + +def load_curated(): + """Override defaults (clone threshold, shared-util set, per-op duplicate allowlist) from + the guidelines doc. Falls back to module defaults when the doc is absent.""" + text = read(REPO / GUIDE) or "" + sim = SIM_THRESHOLD + utils = set(SHARED_UTILS) + allowlist = {} + m = re.search(r"similarity-threshold\s*[:=]\s*(0?\.\d+)", text) + if m: + sim = float(m.group(1)) + section, in_block = None, False + section_markers = { + "### Shared-util reference set": "utils", + "### Duplicate allowlist": "allow", + } + for line in text.splitlines(): + matched_section = section_markers.get(line) + if matched_section: + section, in_block = matched_section, False + continue + if line.startswith("###") or line.startswith("## "): + section = None + stripped = line.strip() + if section and stripped.startswith("```"): + in_block = not in_block + continue + if section and in_block: + if not stripped or stripped.startswith("#"): + continue + if section == "utils": + utils.add(stripped) + elif section == "allow" and ":" in stripped: + k, v = stripped.split(":", 1) + allowlist[k.strip().lower()] = [ + t.strip() for t in v.split(",") if t.strip() + ] + return {"sim": sim, "utils": utils, "allowlist": allowlist} + + +# ================================================================================ assess: impl +def check_impl(P: OpPaths, curated, reader=read): + out = [] + g = ".agents/guidance/REFACTOR_OP_GUIDELINES.md#impl" + priv_texts = [(p, reader(p)) for p in P.priv] + if not P.priv: + out.append( + Finding("RED-1", "impl", NA, "No priv implementation files located", "", g) + ) + return out + + allow = set(curated["allowlist"].get(P.op, [])) + + # RED-1: near-duplicate kernels/functions within the operator's priv (Tensor vs VarShape). + blocks = [] + for p, t in priv_texts: + for b in extract_blocks(t, "cpp"): + blocks.append((p, b)) + flat = [b for _, b in blocks] + owner = {id(b): p for p, b in blocks} + red1 = duplicate_pair_findings( + flat, + curated["sim"], + allow, + lambda ratio, a, b: ( + Finding( + "RED-1", + "impl", + REC, + f"Near-duplicate bodies '{a.name}' / '{b.name}' (jaccard {ratio:.2f}) — unify", + f"{rel(owner[id(a)])}:{a.start} vs {rel(owner[id(b)])}:{b.start}", + g, + "Unify behind a templated kernel; put the addressing difference in the accessor " + "(cf. OpBrightnessContrast.cu -> DoBrightnessContrast).", + ) + ), + ) + out += red1 + if not red1: + out.append( + Finding( + "RED-1", + "impl", + PASS, + "No near-duplicate kernel/function bodies in priv", + f"{len(flat)} block(s) compared at jaccard>={curated['sim']:.2f}", + g, + ) + ) + + # RED-4: re-implemented layout validation that should use nvcv TensorDataAccess helpers. + red4 = [] + for p, t in priv_texts: + red4 += [ + (p, h) + for h in grep( + r"TENSOR_NCHW\b.*\bTENSOR_NHWC\b|TENSOR_NHWC\b.*\bTENSOR_NCHW\b", t + ) + ] + if red4: + p, (ln, line) = red4[0] + out.append( + Finding( + "RED-4", + "impl", + MANUAL, + f"Manual layout validation in priv ({len(red4)} site[s]) — prefer shared helpers", + f"{rel(p)}:{ln}: {line.strip()[:100]}", + g, + "Replace hand-rolled layout checks with nvcv TensorDataAccess helpers (cf. OpStack.cpp).", + ) + ) + else: + out.append( + Finding( + "RED-4", + "impl", + PASS, + "No hand-rolled layout-validation chains found", + "", + g, + ) + ) + + # RED-5: manual index/stride arithmetic in a file that does not use a *Wrap accessor. + red5 = [] + for p, t in priv_texts: + if not t: + continue + uses_wrap = re.search(r"TensorWrap|ImageBatchVarShapeWrap|TensorDataAccess", t) + stride_hits = grep(r"\*\s*\w*[Ss]tride\w*\b", t) + if stride_hits and not uses_wrap: + red5.append((p, stride_hits[0])) + if red5: + p, (ln, line) = red5[0] + out.append( + Finding( + "RED-5", + "impl", + MANUAL, + f"Hand-rolled stride arithmetic without an accessor wrapper ({len(red5)} file[s])", + f"{rel(p)}:{ln}: {line.strip()[:100]}", + g, + "Use TensorWrap / TensorDataAccessStridedImagePlanar accessors instead of manual " + "stride math.", + ) + ) + else: + out.append( + Finding( + "RED-5", + "impl", + PASS, + "No accessor-free manual stride math detected", + "", + g, + ) + ) + return out + + +# ================================================================================= assess: api +def check_api(P: OpPaths, curated, reader=read): + out = [] + g = ".agents/guidance/REFACTOR_OP_GUIDELINES.md#api" + t = reader(P.pybind) + if t is None: + out.append( + Finding("RED-6", "api", NA, "Python binding not found", rel(P.pybind), g) + ) + return out + allow = set(curated["allowlist"].get(P.op, [])) + blocks = [b for b in extract_blocks(t, "cpp")] + red6 = duplicate_pair_findings( + blocks, + curated["sim"], + allow, + lambda ratio, a, b: ( + Finding( + "RED-6", + "api", + REC, + f"Duplicated binding bodies '{a.name}' / '{b.name}' (jaccard {ratio:.2f})", + f"{rel(P.pybind)}:{a.start} vs {rel(P.pybind)}:{b.start}", + g, + "Extract a shared submit helper for the Tensor/VarShape & allocating/_into paths; " + "reuse VarShapeUtils.hpp (CreateSameShapeImageBatch).", + ) + ), + ) + out += red6 + if not red6: + out.append( + Finding( + "RED-6", + "api", + PASS, + "No duplicated binding bodies above threshold", + f"{len(blocks)} binding block(s) compared", + g, + ) + ) + return out + + +# ================================================================================ assess: xcut +def check_xcut(P: OpPaths, curated, reader=read): + out = [] + g = ".agents/guidance/REFACTOR_OP_GUIDELINES.md#xcut" + surface = [(p, reader(p)) for p in (P.priv + [P.pybind])] + surface = [(p, t) for p, t in surface if t] + full_text = "\n".join(t for _, t in surface) + utils = curated["utils"] + + # RED-10: a locally-defined function shadowing a canonical shared util. + shadows = [] + for p, t in surface: + for u in sorted(utils): + for ln, line in grep(rf"\b{re.escape(u)}\b\s*\([^)]*\)\s*(\{{|$)", t): + # a *definition* (sig followed by a body), not a call site + if re.search(rf"\b{re.escape(u)}\s*\(", line) and re.search( + rf"(template|__device__|__host__|inline|static).*\b{re.escape(u)}\b", + line, + ): + shadows.append((u, p, ln, line)) + if shadows: + u, p, ln, line = shadows[0] + out.append( + Finding( + "RED-10", + "xcut", + MANUAL, + f"Local helper shadows shared util '{u}' ({len(shadows)} site[s])", + f"{rel(p)}:{ln}: {line.strip()[:100]}", + g, + "Replace the reinvented helper with cuda_tools/{SaturateCast,StaticCast,TypeTraits}.hpp.", + ) + ) + else: + out.append( + Finding( + "RED-10", + "xcut", + PASS, + "No local helper shadows a canonical shared util", + "", + g, + ) + ) + + # RED-11: dead code — a static / anonymous-namespace function referenced only by its + # definition (name occurs exactly once across the operator surface). + dead = [] + for p, t in surface: + for b in extract_blocks(t, "cpp"): + decl = grep(rf"\bstatic\b.*\b{re.escape(b.name)}\s*\(", t) + if not decl: + continue + occurrences = len(re.findall(rf"\b{re.escape(b.name)}\b", full_text)) + if occurrences <= 1: + dead.append((p, b)) + if dead: + p, b = dead[0] + out.append( + Finding( + "RED-11", + "xcut", + REC, + f"Dead code: static '{b.name}' referenced only at its definition ({len(dead)} fn[s])", + f"{rel(p)}:{b.start}", + g, + "Remove the unreferenced function.", + ) + ) + else: + out.append( + Finding( + "RED-11", + "xcut", + PASS, + "No obviously-dead static functions found", + "", + g, + ) + ) + return out + + +# =============================================================================== verify: parity +def parse_limitations(header_text): + """Declared feature matrix from the Doxygen Limitations table, split by Input/Output.""" + if not header_text: + return {} + m = re.search( + r"Limitations:(.*?)(?:\n\s*\*\s*Input/Output dependency|\*/)", header_text, re.S + ) + blob = m.group(1) if m else header_text + + def section(label): + found = re.search(rf"{label}:(.*?)(?:Input:|Output:|$)", blob, re.S) + region = found.group(1) if found else "" + layouts = set() + for lay in re.finditer(r"Data Layout:\s*\[([^\]]*)\]", region): + layouts.update(re.findall(r"k([A-Z]+)", lay.group(1))) + channels = set() + for ch in re.finditer(r"Channels:\s*\[([^\]]*)\]", region): + channels.update(int(x) for x in re.findall(r"\d+", ch.group(1))) + dtypes = set() + for bits, sign, allowed in re.findall( + r"(\d+bit)\s+(Unsigned|Signed|Float)\s*\|\s*(Yes|No)", region + ): + if allowed == "Yes" and (bits, sign) in DTYPE_ROW: + dtypes.add(DTYPE_ROW[(bits, sign)]) + return {"layouts": layouts, "channels": channels, "dtypes": dtypes} + + if "Input:" not in blob and "Output:" not in blob: + blob = f"Input:{blob}" + return {"input": section("Input"), "output": section("Output")} + + +def _feature_sig(header_text): + lim = parse_limitations(header_text) + return tuple( + ( + side, + tuple(sorted(lim.get(side, {}).get("layouts", set()))), + tuple(sorted(lim.get(side, {}).get("channels", set()))), + tuple(sorted(lim.get(side, {}).get("dtypes", set()))), + ) + for side in ("input", "output") + ) + + +_TEST_MACRO = re.compile( + r"\b(TEST|TEST_F|TEST_P|TYPED_TEST|TYPED_TEST_P|NVCV_TYPED_TEST_SUITE|" + r"NVCV_INSTANTIATE_TEST_SUITE_P|INSTANTIATE_TEST_SUITE_P|NVCV_TEST_SUITE_P)\s*\(\s*([A-Za-z_]\w*)" +) + + +def _coverage_sig(test_text): + """A stable signature of the test surface: the multiset of test/instantiation macros, plus a + count of parametrized value rows (test_case_t/Param/ValuesIn entries). Used for VER-5. + """ + if not test_text: + return () + macros = sorted( + f"{m.group(1)}:{m.group(2)}" for m in _TEST_MACRO.finditer(test_text) + ) + value_rows = len(re.findall(r"test::Param|test_case_t\{|\{\s*test::", test_text)) + return tuple(macros) + (f"value_rows={value_rows}",) + + +def check_verify(P: OpPaths, base): + g = ".agents/guidance/REFACTOR_OP_GUIDELINES.md#verify" + out = [] + + test_rels = [rel(P.test_cpp), rel(P.test_py)] + bench_rels = sorted({rel(P.bench_cpp), rel(P.bench_py), rel(P.bench_cfg)}) + + # VER-1: frozen test surface (no diff base->working under the op's test files). + test_diff = git("diff", "--name-only", base, "--", *test_rels) + changed_tests = [ln.strip() for ln in test_diff.splitlines() if ln.strip()] + + # VER-4: declared feature matrix identical base-vs-working. + base_hdr = git_show(base, rel(P.header)) + cur_hdr = read(P.header) + feat_base, feat_cur = _feature_sig(base_hdr), _feature_sig(cur_hdr) + if feat_base == feat_cur: + out.append( + Finding( + "VER-4", + "verify", + PASS, + "Declared feature matrix unchanged (layouts/channels/dtypes)", + f"layouts/ch/dtypes identical vs {base}", + g, + ) + ) + else: + out.append( + Finding( + "VER-4", + "verify", + GAP, + "Declared feature matrix changed — refactoring must not change supported features", + f"base={feat_base} now={feat_cur}", + g, + "Revert the Limitations/feature change, or move it to a separate feature MR.", + ) + ) + + # VER-5: test coverage matrix identical (only matters if the test files changed). + if not changed_tests: + out.append( + Finding( + "VER-1", + "verify", + PASS, + "Frozen test surface: no diff under the operator's test files", + "; ".join(test_rels), + g, + ) + ) + out.append( + Finding( + "VER-5", + "verify", + PASS, + "Test coverage matrix unchanged (test files untouched)", + "", + g, + ) + ) + else: + cov_base = _coverage_sig(git_show(base, rel(P.test_cpp))) + _coverage_sig( + git_show(base, rel(P.test_py)) + ) + cov_cur = _coverage_sig(read(P.test_cpp)) + _coverage_sig(read(P.test_py)) + same = cov_base == cov_cur + out.append( + Finding( + "VER-1", + "verify", + GAP, + "Test files changed; refactoring must not change frozen operator tests", + "changed: " + ", ".join(changed_tests), + g, + "Move shared helpers outside the operator test files or split the test change " + "into a separate MR.", + ) + ) + out.append( + Finding( + "VER-5", + "verify", + PASS if same else GAP, + "Test coverage matrix " + ("unchanged" if same else "changed"), + f"macros/value-rows {'identical' if same else 'differ'} vs {base}", + g, + ( + "" + if same + else "Restore the original test coverage; refactoring is feature-neutral." + ), + ) + ) + + # VER-2: frozen bench surface (no diff under the op's bench files incl. config/baselines). + bench_diff = git("diff", "--name-only", base, "--", *bench_rels) + changed_bench = sorted({ln.strip() for ln in bench_diff.splitlines() if ln.strip()}) + out.append( + Finding( + "VER-2", + "verify", + PASS if not changed_bench else GAP, + ( + "Frozen bench surface (sources/config/baselines untouched)" + if not changed_bench + else "Bench surface changed — benchmarks are the frozen measurement instrument" + ), + ( + "; ".join(bench_rels) + if not changed_bench + else "changed: " + ", ".join(changed_bench) + ), + g, + ( + "" + if not changed_bench + else "Revert all bench/baseline edits; a refactor must not touch the measurement." + ), + ) + ) + + # VER-3: public API/ABI unchanged (header + .hpp + binding signatures). + out.append(_api_abi(P, base, g)) + + # VER-6: bit-exactness — proved by the frozen tests passing on the refactored build. The + # build+run is a local proof for refactor-scoped work; flagged here so the gate is explicit. + out.append( + Finding( + "VER-6", + "verify", + MANUAL, + "Bit-exact: build the refactored tree and run the frozen Op%s tests (EXPECT_EQ goldens)" + % P.Op, + "run: build-rel/bin/cvcuda_test_system --gtest_filter='Op%s*' (must be green)" + % P.Op, + g, + ) + ) + + # VER-7: the applied redundancy is actually gone (re-run assess). + out.append( + Finding( + "VER-7", + "verify", + MANUAL, + "Confirm the applied redundancy is resolved: re-run assess and check the finding is gone", + "run: python3 tools/refactor_op.py %s" % P.Op, + g, + ) + ) + return out + + +def _api_abi(P, base, g): + header_paths = [rel(P.header), rel(P.hpp)] + header_diff = git("diff", base, "--", *header_paths) + sig = [] + signature_rx = re.compile(r"CVCUDA_PUBLIC|operator\(\)|Submit\s*\(") + for ln in header_diff.splitlines(): + is_changed_line = ln[:1] in "+-" and ln[1:2] != ln[:1] + if is_changed_line and signature_rx.search(ln): + sig.append(ln) + if sig: + return Finding( + "VER-3", + "verify", + GAP, + "Public API/ABI signature(s) changed — that is feature work, not a refactor", + "; ".join(s.strip()[:80] for s in sig[:3]), + g, + "Revert the signature change or split it into a separate feature MR.", + ) + if header_diff.strip(): + return Finding( + "VER-3", + "verify", + MANUAL, + "Public headers changed outside a recognized signature", + "diff in " + ", ".join(Path(p).name for p in header_paths), + g, + "Inspect the public-header diff and prove that API and ABI are unchanged.", + ) + + binding_path = rel(P.pybind) + binding_diff = git("diff", base, "--", binding_path) + if not binding_diff.strip(): + return Finding( + "VER-3", + "verify", + PASS, + "Public API/ABI surface unchanged", + "no changes in " + + ", ".join(Path(p).name for p in (*header_paths, binding_path)), + g, + ) + + baseline_source = git_show(base, binding_path) + candidate_source = read(P.pybind) + baseline_surface = binding_api_snapshot(baseline_source, P.Op) + candidate_surface = binding_api_snapshot(candidate_source, P.Op) + if baseline_surface is None or candidate_surface is None: + return Finding( + "VER-3", + "verify", + MANUAL, + "Binding API snapshot could not be resolved conservatively", + f"baseline parsed={baseline_surface is not None}; " + f"candidate parsed={candidate_surface is not None}", + g, + "Inspect registrations, callable signatures, and reachable type aliases manually.", + ) + if baseline_surface == candidate_surface: + return Finding( + "VER-3", + "verify", + PASS, + "Public API/ABI surface unchanged", + "public headers unchanged; binding registration/signatures/type aliases identical", + g, + ) + + changed_parts = [] + if baseline_surface[0] != candidate_surface[0]: + changed_parts.append("m.def registration/arguments/defaults") + if baseline_surface[1] != candidate_surface[1]: + changed_parts.append("bound callable signature(s)") + if baseline_surface[2] != candidate_surface[2]: + changed_parts.append("bound callable type alias(es)") + return Finding( + "VER-3", + "verify", + GAP, + "Python binding API changed — that is feature work, not a refactor", + "; ".join(changed_parts), + g, + "Revert the API change or split it into a separate feature MR.", + ) + + +# ================================================================================== driver +def run_assess(P, domains, curated, reader=read): + findings = [] + if "impl" in domains: + findings += check_impl(P, curated, reader) + if "api" in domains: + findings += check_api(P, curated, reader) + if "xcut" in domains: + findings += check_xcut(P, curated, reader) + return findings + + +def _open_red_ids(P, curated, reader): + """The set of RED-* ids with a non-PASS, non-N-A finding under `reader` — i.e. open + redundancy. Used to quantify before-vs-after when verifying an applied refactor.""" + return { + f.id + for f in run_assess(P, DOMAINS, curated, reader) + if f.status in (REC, MANUAL) + } + + +def refactor_summary(P, base, curated): + """Deterministic impact summary for an applied refactor: LOC delta on the impl/binding + files vs `base`, and which RED-* findings the change resolved (base -> working).""" + paths = [rel(p) for p in P.priv] + [rel(P.pybind)] + numstat = git("diff", "--numstat", base, "--", *paths) + ins = dels = 0 + for ln in numstat.splitlines(): + cols = ln.split("\t") + if len(cols) >= 2 and cols[0].isdigit() and cols[1].isdigit(): + ins += int(cols[0]) + dels += int(cols[1]) + base_open = _open_red_ids(P, curated, lambda p: git_show(base, rel(p))) + now_open = _open_red_ids(P, curated, read) + return { + "loc_insertions": ins, + "loc_deletions": dels, + "loc_net": ins - dels, + "redundancy_resolved": sorted(base_open - now_open), + "redundancy_introduced": sorted(now_open - base_open), + "redundancy_open_now": sorted(now_open), + } + + +def render_md(P, phase, findings, domains, summary=None): + icon = {PASS: "✅", GAP: "❌", NA: "➖", MANUAL: "🔍", REC: "💡"} + title = f"# refactor-op: {P.Op} (op={P.op}) phase={phase}" + lines = [title, ""] + counts = {} + groups = domains if phase == "assess" else ["verify"] + for grp in groups: + df = [f for f in findings if f.domain == grp] + if not df: + continue + lines.append(f"## {grp}") + for f in df: + counts[f.status] = counts.get(f.status, 0) + 1 + lines.append( + f"- {icon.get(f.status, '?')} **{f.status}** `{f.id}` — {f.summary}" + ) + if f.evidence: + lines.append(f" - evidence: {f.evidence}") + if f.status in (GAP, REC, MANUAL) and f.fix: + lines.append(f" - fix: {f.fix}") + if f.guideline: + lines.append(f" - ref: {f.guideline}") + lines.append("") + total_gap = counts.get(GAP, 0) + total_man = counts.get(MANUAL, 0) + if phase == "verify": + verdict = ( + "PARITY-OK" + if total_gap == 0 and total_man == 0 + else ("GATE-FAIL" if total_gap else "NEEDS-LOCAL-PROOF") + ) + tail = ( + "Gate passes when zero GAP and every MANUAL is resolved green " + "(bit-exact/redundancy proofs, plus any VER-3 header/binding inspection)." + ) + else: + verdict = "ADVISORY" + tail = "Assess is advisory (RECOMMENDATION/MANUAL). Apply via the skill, then run --phase verify." + lines.append("## verdict") + lines.append( + f"**{verdict}** — " + ", ".join(f"{k}={v}" for k, v in sorted(counts.items())) + ) + lines.append("") + lines.append(tail) + if summary is not None: + net = summary["loc_net"] + resolved = ", ".join(summary["redundancy_resolved"]) or "none" + intro = ", ".join(summary["redundancy_introduced"]) or "none" + lines += [ + "", + "## refactor summary", + f"- LOC delta (impl/binding): +{summary['loc_insertions']} / " + f"-{summary['loc_deletions']} (net {net:+d})", + f"- redundancy resolved: {resolved}", + f"- redundancy introduced: {intro}", + f"- redundancy still open: {', '.join(summary['redundancy_open_now']) or 'none'}", + ] + return "\n".join(lines) + + +def render_json(P, phase, findings, domains, summary=None): + counts = {} + for f in findings: + counts[f.status] = counts.get(f.status, 0) + 1 + payload = { + "operator": P.Op, + "op": P.op, + "phase": phase, + "domains": list(domains) if phase == "assess" else ["verify"], + "findings": [vars(f) for f in findings], + "counts": counts, + "exit_gap": counts.get(GAP, 0), + } + if summary is not None: + payload["summary"] = summary + return json.dumps(payload, indent=2, sort_keys=True) + + +def main(argv=None): + ap = argparse.ArgumentParser( + description="Deterministic per-operator refactoring/redundancy checker (CV-CUDA)." + ) + ap.add_argument( + "operator", help="Operator name (PascalCase, e.g. BrightnessContrast)" + ) + ap.add_argument("--phase", default="assess", choices=["assess", "verify"]) + ap.add_argument( + "--domain", default="all", help="impl|api|xcut|all (comma-separated; assess)" + ) + ap.add_argument( + "--base", + default="main", + help="git base ref for the verify diff (default: main)", + ) + ap.add_argument("--format", default="md", choices=["md", "json"]) + ap.add_argument("--out", default=None, help="also write the report to this path") + ap.add_argument( + "--apply", + action="store_true", + help="(wrapper-level) emphasize fixes; the checker stays read-only — the agent applies", + ) + args = ap.parse_args(argv) + + domains = ( + DOMAINS + if args.domain == "all" + else tuple(d.strip() for d in args.domain.split(",") if d.strip() in DOMAINS) + ) + if not domains: + ap.error("no valid --domain selected (choose from impl,api,xcut,all)") + + if args.apply: + print( + "note: refactor_op.py is read-only; --apply is performed by the /refactor-op " + "agent per .agents/guidance/REFACTOR_OP_GUIDELINES.md, then proven by `--phase verify`.", + file=sys.stderr, + ) + + P = resolve_op(args.operator) + resolved_surface = [ + P.header, + P.hpp, + P.pybind, + *P.priv, + P.test_cpp, + P.test_py, + P.bench_cpp, + P.bench_py, + P.bench_cfg, + ] + if not any(p.exists() for p in resolved_surface): + ap.error(f"operator '{args.operator}' could not be resolved to CV-CUDA files") + curated = load_curated() + summary = None + if args.phase == "verify": + findings = check_verify(P, args.base) + summary = refactor_summary(P, args.base, curated) + else: + findings = run_assess(P, domains, curated) + + report = ( + render_md(P, args.phase, findings, domains, summary) + if args.format == "md" + else render_json(P, args.phase, findings, domains, summary) + ) + print(report) + if args.out: + Path(args.out).write_text(report + "\n", encoding="utf-8") + + return 1 if any(f.status == GAP for f in findings) else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/review_op.py b/tools/review_op.py new file mode 100755 index 000000000..da40b74b9 --- /dev/null +++ b/tools/review_op.py @@ -0,0 +1,2255 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. +"""Deterministic per-operator review checker for CV-CUDA. + +Implements the checklist defined in .agents/guidance/REVIEW_OP_GUIDELINES.md across four domains +(support / test / bench / docs). Emits a structured report (markdown or json) with a +status + evidence + guideline id per item, and exits non-zero on any GAP. + +The checker is read-only, deterministic and idempotent: no network, no clocks, no +randomness, so the same tree yields a byte-identical report. Corrective actions are named +per finding (the `fix` field); applying them is the wrapper/agent's job (see the skills). + +Usage: + python3 tools/review_op.py [--domain support|test|bench|docs|all] + [--format md|json] [--out PATH] [--run] [--fix] +""" + +import argparse +import json +import re +import sys +from dataclasses import dataclass, field +from pathlib import Path + +REPO = Path(__file__).resolve().parent.parent +REVIEW_OP_GUIDE = ".agents/guidance/REVIEW_OP_GUIDELINES.md" +CURATED_DATA = ".agents/tools/review_op_data.json" +AGENT_TOOLS = REPO / ".agents" / "tools" +sys.path.insert(0, str(AGENT_TOOLS)) +sys.path.insert(0, str(REPO / "bench")) +from _internal.baselines import ( # noqa: E402 + BaselineError, + expected_case_keys_for_entry, + fake_planar_pairing_issues, +) +from operator_source_map import ( # noqa: E402 + SHARED_KERNEL_SOURCES, + all_op_names, + legacy_belongs, +) + +PASS, GAP, NA, MANUAL, REC = "PASS", "GAP", "N-A", "MANUAL", "RECOMMENDATION" +DOMAINS = ("support", "test", "bench", "docs") + +# dtype table rows in the Doxygen Limitations table -> canonical dtype tokens +DTYPE_ROW = { + ("8bit", "Unsigned"): "u8", + ("8bit", "Signed"): "s8", + ("16bit", "Unsigned"): "u16", + ("16bit", "Signed"): "s16", + ("32bit", "Unsigned"): "u32", + ("32bit", "Signed"): "s32", + ("16bit", "Float"): "f16", + ("32bit", "Float"): "f32", + ("64bit", "Float"): "f64", +} + + +@dataclass +class Finding: + id: str + domain: str + status: str + summary: str + evidence: str = "" + guideline: str = "" + fix: str = "" + + +@dataclass +class OpPaths: + op: str + Op: str + pyname: str + header: Path + hpp: Path + pybind: Path + test_cpp: Path + test_py: Path + bench_cpp: Path + bench_py: Path + bench_cfg: Path + priv: list = field(default_factory=list) + + +@dataclass(frozen=True) +class SubmitSignature: + """Container-relevant facts parsed from one public C Submit declaration.""" + + name: str + primary_container: str | None + line: int + declaration: str + + +@dataclass +class CAPIContainers: + """C-API input-container support derived from Submit signatures.""" + + tensor: list[SubmitSignature] = field(default_factory=list) + varshape: list[SubmitSignature] = field(default_factory=list) + generic_varshape: list[SubmitSignature] = field(default_factory=list) + + +# --------------------------------------------------------------------------- io helpers +def read(p: Path): + try: + return p.read_text(encoding="utf-8", errors="replace") + except (OSError, AttributeError): + return None + + +def rel(p: Path) -> str: + try: + return str(p.relative_to(REPO)) + except ValueError: + return str(p) + + +def grep(pattern: str, text, flags=0): + """Return list of (lineno, line) matching pattern. Empty if text is None.""" + if not text: + return [] + rx = re.compile(pattern, flags) + return [(i + 1, ln) for i, ln in enumerate(text.splitlines()) if rx.search(ln)] + + +def first_evidence(path: Path, hits): + if not hits: + return "" + ln, line = hits[0] + return f"{rel(path)}:{ln}: {line.strip()[:120]}" + + +def parse_submit_signatures(Op: str, header_text: str | None) -> list[SubmitSignature]: + """Parse this operator's public Submit declarations and their primary data input. + + The primary container is the first Tensor/TensorBatch/ImageBatch handle after the + operator and stream parameters. This deliberately ignores tensor outputs and auxiliary + tensors: legacy APIs such as CropFlipNormalizeReformat and PadAndStack have an image-batch + input, a tensor output, and no Tensor-input variant. + """ + if not header_text: + return [] + + pattern = re.compile( + rf"\bCVCUDA_PUBLIC\s+NVCVStatus\s+" + rf"(cvcuda{re.escape(Op)}(?:VarShape|ImageBatch|TensorBatch)?Submit)\s*\((.*?)\)\s*;", + re.I | re.S, + ) + handle_pattern = re.compile(r"\b(NVCV(?:ImageBatch|TensorBatch|Tensor)Handle)\b") + signatures = [] + for match in pattern.finditer(header_text): + handles = handle_pattern.findall(match.group(2)) + signatures.append( + SubmitSignature( + name=match.group(1), + primary_container=handles[0] if handles else None, + line=header_text.count("\n", 0, match.start(1)) + 1, + declaration=" ".join(match.group(0).split()), + ) + ) + return signatures + + +def detect_c_api_containers(Op: str, header_text: str | None) -> CAPIContainers: + """Classify Tensor vs VarShape support from public C Submit signatures. + + A generic ``Submit`` is classified by its primary input handle. A named + ``VarShapeSubmit`` and ``ImageBatchSubmit`` remain explicit VarShape forms, + including non-image TensorBatch APIs whose established public name carries + that distinction. + """ + result = CAPIContainers() + generic_name = f"cvcuda{Op}Submit".lower() + for signature in parse_submit_signatures(Op, header_text): + lower_name = signature.name.lower() + is_named_varshape = lower_name.endswith(("varshapesubmit", "imagebatchsubmit")) + if is_named_varshape or signature.primary_container == "NVCVImageBatchHandle": + result.varshape.append(signature) + if signature.name.lower() == generic_name: + result.generic_varshape.append(signature) + elif signature.primary_container in { + "NVCVTensorHandle", + "NVCVTensorBatchHandle", + }: + result.tensor.append(signature) + return result + + +def submit_evidence(path: Path, signature: SubmitSignature) -> str: + return f"{rel(path)}:{signature.line}: {signature.declaration[:180]}" + + +# --------------------------------------------------------------------- operator resolution +def resolve_op(arg: str): + """Resolve to op/Op/pyname + file paths (case-insensitive on the header).""" + stem = arg.strip() + op = stem.lower() + hdr_dir = REPO / "src/cvcuda/include/cvcuda" + header = None + Op = stem + if hdr_dir.is_dir(): + for h in sorted(hdr_dir.glob("Op*.h")): + if h.stem[2:].lower() == op: # "Op" -> + header = h + Op = h.stem[2:] + break + if header is None: + header = hdr_dir / f"Op{stem}.h" + Op = stem + + pybind = REPO / f"python/mod_cvcuda/operators/Op{Op}.cpp" + pyname = resolve_pyname(pybind, op) + + priv = [] + priv_dir = REPO / "src/cvcuda/priv" + for cand in [priv_dir / f"Op{Op}.cu", priv_dir / f"Op{Op}.cpp"]: + if cand.exists(): + priv.append(cand) + legacy = priv_dir / "legacy" + if legacy.is_dir(): + all_ops = all_op_names() + for g in sorted(legacy.glob("*.c*")): + if legacy_belongs(g.stem, op, all_ops): + priv.append(g) + # Shared/legacy kernel sources not matched by the op-name globs (e.g. warp.cu for the + # Warp* ops, the HQResize kernel headers) — see SHARED_KERNEL_SOURCES. + for extra in SHARED_KERNEL_SOURCES.get(op, []): + cand = priv_dir / extra + if cand.exists() and cand not in priv: + priv.append(cand) + + # Python tests are named after the flattened op name (mkop.sh scaffolds + # test_op$namelower.py); fall back to the pybind API name only for the few + # legacy files that use it (e.g. test_opnms.py, test_opmatch.py). + test_py = REPO / f"tests/cvcuda/python/test_op{op}.py" + if not test_py.exists(): + alt = REPO / f"tests/cvcuda/python/test_op{pyname}.py" + if alt.exists(): + test_py = alt + + return OpPaths( + op=op, + Op=Op, + pyname=pyname, + header=header, + hpp=hdr_dir / f"Op{Op}.hpp", + pybind=pybind, + test_cpp=REPO / f"tests/cvcuda/system/TestOp{Op}.cpp", + test_py=test_py, + bench_cpp=REPO / f"bench/cpp/ops/Bench{Op}.cpp", + bench_py=REPO / f"bench/python/ops/bench_{op}.py", + bench_cfg=REPO / f"bench/config/operators/{op}.json", + priv=priv, + ) + + +def resolve_pyname(pybind: Path, op: str) -> str: + text = read(pybind) + if text: + names = re.findall(r'm\.def\(\s*"([a-z0-9_]+)"', text) + base = [n for n in names if not n.endswith("_into")] + if base: + # the shortest base name is the operator's primary function + return sorted(base, key=len)[0] + return op + + +# --------------------------------------------------------------- deterministic curated data +_MISSING = object() + + +def _curated_schema_error(field: str, expected: str) -> None: + raise ValueError(f"{CURATED_DATA}: `{field}` must be {expected}") + + +def _curated_string_list(data: dict, field: str) -> list[str]: + value = data.get(field, _MISSING) + if value is _MISSING: + return [] + if not isinstance(value, list): + _curated_schema_error(field, "a list of strings") + for idx, item in enumerate(value): + if not isinstance(item, str): + _curated_schema_error(f"{field}[{idx}]", "a string") + return value + + +def _curated_basic_expected(data: dict) -> dict[str, list[str]]: + value = data.get("basic_expected", _MISSING) + if value is _MISSING: + return {} + if not isinstance(value, dict): + _curated_schema_error( + "basic_expected", "an object mapping operator names to string lists" + ) + normalized = {} + for op, expected in value.items(): + if not isinstance(op, str): + _curated_schema_error("basic_expected key", "a string") + if not isinstance(expected, list): + _curated_schema_error(f"basic_expected.{op}", "a list of strings") + for idx, item in enumerate(expected): + if not isinstance(item, str): + _curated_schema_error(f"basic_expected.{op}[{idx}]", "a string") + normalized[op.lower()] = expected + return normalized + + +def load_curated(): + """Load non-derivable review facts without putting operator names in guidance.""" + data = load_bench_cfg(REPO / CURATED_DATA) + if data is None: + data = {} + if not isinstance(data, dict): + _curated_schema_error("root", "an object") + tensor_only = {op.lower() for op in _curated_string_list(data, "tensor_only")} + varshape_only = {op.lower() for op in _curated_string_list(data, "varshape_only")} + bench_layout_na = { + op.lower() for op in _curated_string_list(data, "bench_layout_na") + } + bench_rgb_na = {op.lower() for op in _curated_string_list(data, "bench_rgb_na")} + basic_expected = _curated_basic_expected(data) + return tensor_only, varshape_only, basic_expected, bench_layout_na, bench_rgb_na + + +# --------------------------------------------------------------- Limitations table parsing +def parse_limitations(header_text): + """Parse the Input Limitations block -> {layouts:set, channels:set, dtypes:set} or None.""" + if not header_text: + return None + # isolate the Input: ... up to Output: (the first Limitations block) + m = re.search( + r"Limitations:(.*?)(?:\n\s*\*\s*Input/Output dependency|\*/)", header_text, re.S + ) + blob = m.group(1) if m else header_text + inp = re.search(r"Input:(.*?)(?:Output:|$)", blob, re.S) + region = inp.group(1) if inp else blob + + lay = re.search(r"Data Layout:\s*\[([^\]]*)\]", region) + layouts = set() + if lay: + for tok in re.findall(r"(?:k|NVCV_TENSOR_)?([A-Z]+)", lay.group(1)): + if tok in {"HW", "NHW", "HWC", "NHWC", "CHW", "NCHW"}: + layouts.add(tok) + # Planar layouts may be declared in prose/macro form (e.g. HQResize's + # "NVCV_TENSOR_[N]CHW (planar, 2D only)") that the [kNCHW,...] bracket parse above misses. + if re.search(r"NVCV_TENSOR_[\[\]A-Z]*CHW", region): + layouts.update({"NCHW", "CHW"}) + ch = re.search(r"Channels:\s*\[([^\]]*)\]", region) + channels = set() + if ch: + for tok in re.findall(r"\d+", ch.group(1)): + channels.add(int(tok)) + dtypes = set() + for bits, sign, allowed in re.findall( + r"(\d+bit)\s+(Unsigned|Signed|Float)\s*\|\s*(Yes|No)", region + ): + if allowed == "Yes": + key = DTYPE_ROW.get((bits, sign)) + if key: + dtypes.add(key) + if not layouts and not channels and not dtypes: + return None + return {"layouts": layouts, "channels": channels, "dtypes": dtypes} + + +def parse_planar_policy(header_text): + """Parse an operator-local planar inapplicability declaration from C-API Doxygen.""" + policy = { + "not_applicable": False, + "reason": "", + "error": "", + "line": None, + } + if not header_text: + return policy + + lines = header_text.splitlines() + declarations = [] + for idx, line in enumerate(lines): + clean = re.sub(r"^\s*(?:/\*+|\*+)?\s?", "", line).strip() + if clean.startswith("Planar image layouts:"): + declarations.append((idx, clean.split(":", 1)[1].strip())) + + if not declarations: + return policy + if len(declarations) != 1: + policy["error"] = "declare 'Planar image layouts' exactly once" + return policy + + idx, value = declarations[0] + policy["line"] = idx + 1 + if value != "Not applicable": + policy["error"] = ( + "the only supported declaration is " + "'Planar image layouts: Not applicable'" + ) + return policy + + for following_idx, following in enumerate(lines): + if following_idx <= idx: + continue + clean = re.sub(r"^\s*(?:/\*+|\*+)?\s?", "", following).strip() + if not clean: + continue + if clean.startswith("Reason:"): + policy["reason"] = clean.split(":", 1)[1].strip() + break + + if not policy["reason"]: + policy["error"] = "a planar inapplicability declaration requires a Reason" + return policy + + policy["not_applicable"] = True + return policy + + +def planar_policy_verdict(P: OpPaths, header_text, declared): + """Require channel-first image layouts unless the operator declares why they do not apply.""" + policy = parse_planar_policy(header_text) + location = rel(P.header) + if policy["line"]: + location += f":{policy['line']}" + + if policy["error"]: + return Finding( + "SUP-10", + "support", + GAP, + "Invalid planar layout policy declaration", + f"{location}: {policy['error']}", + REVIEW_OP_GUIDE, + "Use the exact operator-local declaration and provide a non-empty Reason.", + ) + if policy["not_applicable"] and declared: + return Finding( + "SUP-10", + "support", + GAP, + "Planar layouts are both declared and marked not applicable", + f"{location}: {policy['reason']}", + REVIEW_OP_GUIDE, + "Remove the inapplicability declaration or remove NCHW/CHW from the support contract.", + ) + if policy["not_applicable"]: + return Finding( + "SUP-10", + "support", + NA, + "Planar image layouts are not applicable", + f"{location}: {policy['reason']}", + REVIEW_OP_GUIDE, + ) + if declared: + return Finding( + "SUP-10", + "support", + PASS, + "Planar image layouts are declared", + "header Limitations include NCHW/CHW", + REVIEW_OP_GUIDE, + ) + return Finding( + "SUP-10", + "support", + GAP, + "Planar image layouts are required by default", + f"{location}: no NCHW/CHW layout or inapplicability declaration", + REVIEW_OP_GUIDE, + "Add NCHW/CHW support or declare 'Planar image layouts: Not applicable' with a Reason.", + ) + + +# --------------------------------------------------------------------- bench config helpers +def load_bench_cfg(path: Path): + text = read(path) + if not text: + return None + try: + return json.loads(text) + except json.JSONDecodeError: + return None + + +def rows_for_entry(entry: dict) -> int: + count = len(entry.get("dtypes", [])) or 1 + for grp in ("string_axes", "int64_axes", "float64_axes"): + for vals in entry.get(grp, {}).values(): + count *= max(len(vals), 1) + return count + + +# =========================================================================== SUPPORT domain +def check_support(P: OpPaths, curated): + tensor_only, varshape_only, _, bench_layout_na, bench_rgb_na = curated + out = [] + htext = read(P.header) + g = ".agents/guidance/REVIEW_OP_GUIDELINES.md#support / make_operator.rst" + + # Case-insensitive: C symbol casing can differ from the header stem (HQResize -> + # cvcudaHqResizeSubmit). Classify by the primary data-input handle rather than the symbol + # suffix because a few legacy generic Submit APIs take ImageBatch input and Tensor output. + capi = detect_c_api_containers(P.Op, htext) + has_tensor = bool(capi.tensor) + has_vs = bool(capi.varshape) + generic_varshape = bool(capi.generic_varshape) + tensor_absence_is_declared = generic_varshape or P.op in varshape_only + if has_tensor: + tensor_status = PASS + tensor_summary = "Tensor container declared (cvcuda%sSubmit)" % P.Op + tensor_evidence = first_evidence( + P.header, grep(rf"cvcuda{P.Op}Submit", htext, re.I) + ) + elif generic_varshape: + tensor_status = NA + tensor_summary = ( + "Tensor absent; generic Submit declares a VarShape primary input" + ) + tensor_evidence = submit_evidence(P.header, capi.generic_varshape[0]) + elif P.op in varshape_only: + tensor_status = NA + tensor_summary = ( + "Tensor absent; op is marked var-shape-only in deterministic review data" + ) + tensor_evidence = f"{rel(P.header)} (not found)" + else: + tensor_status = GAP + tensor_summary = "Tensor container missing" + tensor_evidence = f"{rel(P.header)} (not found)" + out.append( + Finding( + "SUP-1", + "support", + tensor_status, + tensor_summary, + tensor_evidence, + g, + "" + if has_tensor or tensor_absence_is_declared + else "Declare the Tensor Submit entry point.", + ) + ) + + if has_vs: + out.append( + Finding( + "SUP-2", + "support", + PASS, + "VarShape container declared", + submit_evidence(P.header, capi.varshape[0]), + g, + ) + ) + elif P.op in tensor_only: + out.append( + Finding( + "SUP-2", + "support", + NA, + "VarShape absent; op is marked tensor-only in deterministic review data", + "", + g, + ) + ) + else: + out.append( + Finding( + "SUP-2", + "support", + GAP, + "VarShape overload absent and op not marked tensor-only in deterministic review data", + f"{rel(P.header)}: no cvcuda{P.Op}VarShapeSubmit", + g, + "Add VarShape support, or record deterministic/operator-local evidence " + "that the op is tensor-only.", + ) + ) + + # SUP-3 C++ .hpp overloads + hpptext = read(P.hpp) + if hpptext is None: + out.append( + Finding( + "SUP-3", + "support", + GAP, + "C++ .hpp not found", + f"{rel(P.hpp)} (missing)", + g, + "Add the C++ operator header.", + ) + ) + else: + hpp_tensor = bool( + re.search( + r"operator\([^;{]*\bnvcv::Tensor\s*&\s*(?:in|src|input)[^;{]*" + r"\bnvcv::Tensor\s*&\s*(?:out|dst|output)", + hpptext, + re.S, + ) + ) + # C++ var-shape overloads take nvcv::ImageBatch& or nvcv::ImageBatchVarShape& (param, not the include) + hpp_vs = bool(grep(r"ImageBatch\w*\s*&", hpptext)) + ok = (hpp_tensor == has_tensor) and (hpp_vs == has_vs) + out.append( + Finding( + "SUP-3", + "support", + PASS if ok else MANUAL, + "C++ .hpp operator() overloads match C-API containers", + f"{rel(P.hpp)}: hpp(T={hpp_tensor},VS={hpp_vs}) capi(T={has_tensor},VS={has_vs})", + g, + "" if ok else "Reconcile C++ overloads with the C-API container set.", + ) + ) + + # SUP-4 Python allocating + _into + pytext = read(P.pybind) + if pytext is None: + out.append( + Finding( + "SUP-4", + "support", + GAP, + "Python binding not found", + f"{rel(P.pybind)} (missing)", + g, + "Add the Python binding.", + ) + ) + else: + defs = set(re.findall(r'm\.def\(\s*"([a-z0-9_]+)"', pytext)) + has_alloc = any(not d.endswith("_into") for d in defs) + has_into = any(d.endswith("_into") for d in defs) + ok = has_alloc and has_into + out.append( + Finding( + "SUP-4", + "support", + PASS if ok else GAP, + "Python binds allocating + _into variants", + f"{rel(P.pybind)}: defs={sorted(defs)}", + g, + "" if ok else "Bind the missing allocating/_into variant.", + ) + ) + + # SUP-5..8 Limitations table parse + lim = parse_limitations(htext) + if lim is None: + out.append( + Finding( + "SUP-5", + "support", + GAP if htext else GAP, + "Limitations tables not present/parseable", + f"{rel(P.header)}: no parseable Data Layout/Channels/Data Type table", + g, + "Add the Doxygen Limitations table (layout/channels/dtype, in+out).", + ) + ) + out.append( + Finding( + "SUP-6/7/8", + "support", + MANUAL, + "Declared layout/dtype/channel matrix not auto-parsed", + f"Inspect the Limitations table in {rel(P.header)}", + g, + ) + ) + lim = {"layouts": set(), "channels": set(), "dtypes": set()} + else: + out.append( + Finding( + "SUP-5", + "support", + PASS, + "Limitations tables present", + f"lay={sorted(lim['layouts'])} dt={sorted(lim['dtypes'])} ch={sorted(lim['channels'])}", + g, + ) + ) + out.append( + Finding( + "SUP-6/7/8", + "support", + PASS, + "Declared matrix parsed", + f"lay={sorted(lim['layouts'])} dt={sorted(lim['dtypes'])} ch={sorted(lim['channels'])}", + g, + ) + ) + + # SUP-9 enforcement (presence of input validation in priv) + enforce_hits = [] + for pv in P.priv: + enforce_hits += [ + (pv, h) + for h in grep( + r"ERROR_INVALID_ARGUMENT|Invalid (DataFormat|channel|data type|format)", + read(pv), + ) + ] + if enforce_hits: + pv, (ln, line) = enforce_hits[0] + out.append( + Finding( + "SUP-9", + "support", + MANUAL, + "Enforcement present; declared-vs-enforced match needs human read", + f"{rel(pv)}:{ln}: {line.strip()[:100]}", + g, + "Confirm the runtime guards match the declared Limitations matrix.", + ) + ) + else: + out.append( + Finding( + "SUP-9", + "support", + MANUAL, + "No obvious input-validation guard found in priv; verify manually", + f"priv files: {[rel(p) for p in P.priv] or 'none located'}", + g, + "Ensure unsupported layout/dtype/channel are rejected with ERROR_INVALID_ARGUMENT.", + ) + ) + + # Planar layouts are part of the default support contract. Only operators whose tensors do + # not represent images may opt out, and that decision lives beside the operator Limitations. + planar_declared = bool(lim["layouts"] & {"NCHW", "CHW"}) + planar_policy = parse_planar_policy(htext) + out.append(planar_policy_verdict(P, htext, planar_declared)) + + # SUP-11 cross-surface consistency (container sets) + note = [] + if hpptext is not None and pytext is not None: + note.append("containers checked across header/.hpp/python (see SUP-3/4)") + out.append( + Finding( + "SUP-11", + "support", + PASS, + "Cross-surface container consistency (see SUP-3/SUP-4)", + "; ".join(note), + g, + ) + ) + else: + out.append( + Finding( + "SUP-11", + "support", + MANUAL, + "Cross-surface consistency: a surface file is missing", + f".hpp={'ok' if hpptext else 'missing'} python={'ok' if pytext else 'missing'}", + g, + ) + ) + return out, { + "planar": planar_declared and not planar_policy["not_applicable"], + "planar_declared": planar_declared, + "planar_not_applicable": planar_policy["not_applicable"], + "planar_reason": planar_policy["reason"], + "limitations": lim, + "has_tensor": has_tensor, + "has_vs": has_vs, + "generic_varshape": generic_varshape, + "bench_layout_na": P.op in bench_layout_na, + "bench_rgb_na": P.op in bench_rgb_na, + } + + +# ============================================================================== TEST domain +def _tensor_positive_test_pattern(Op: str) -> str: + """Return the candidate Tensor-test macro pattern for one operator. + + Verdicts use :func:`_tensor_positive_test_hits`, which parses both the suite + and test name. This pattern remains the narrow candidate extractor for + callers that only need to locate relevant macros. + """ + test_macro = r"(?:TEST|TEST_P|TYPED_TEST)" + suite_prefix = rf"Op{re.escape(Op)}" + return ( + rf"{test_macro}\(\s*{suite_prefix}(?:\s*,|" + rf"Tensor(?![A-Za-z0-9_]*(?:[Nn]egative|[Ii]nvalid|[Rr]eject|[Ss]moke))" + rf"[A-Za-z0-9_]*\s*,)" + ) + + +_NONPOSITIVE_TENSOR_TEST_TOKENS = ( + "negative", + "incorrect", + "invalid", + "reject", + "smoke", + "unsupported", + "mismatch", + "null", + "error", + "failure", + "overflow", + "exceed", + "out_of_range", + "outofrange", +) + + +def _tensor_positive_test_hits(Op: str, text: str | None): + """Return parsed positive Tensor test macros with source-line evidence. + + The generic ``Op`` suite and Tensor-specialized suites are candidates, + but their test names still have to describe a non-negative execution case. + This prevents a smoke or rejection-only suite from satisfying TST-2 while + preserving established positive names such as ``CustomCrop_packed``. + """ + if not text: + return [] + + def blank_comment(match): + return re.sub(r"[^\n]", " ", match.group(0)) + + parsed_text = re.sub(r"//[^\n]*|/\*.*?\*/", blank_comment, text, flags=re.S) + macro = re.compile( + r"\b(?:TEST|TEST_P|TYPED_TEST)\s*\(\s*" + r"(?P[A-Za-z_]\w*)\s*,\s*(?P[A-Za-z_]\w*)\s*\)" + ) + prefix = f"Op{Op}" + lines = text.splitlines() + hits = [] + for match in macro.finditer(parsed_text): + suite = match.group("suite") + name = match.group("name") + if not suite.startswith(prefix): + continue + suffix = suite.removeprefix(prefix) + if suffix and not suffix.lower().startswith("tensor"): + continue + + suite_and_name = f"{suite}_{name}".lower() + if any(token in suite_and_name for token in _NONPOSITIVE_TENSOR_TEST_TOKENS): + continue + if "varshape" in suite_and_name or "imagebatch" in suite_and_name: + continue + if name.lower() in {"op", "operator_creation", "creation"}: + continue + + line_number = text.count("\n", 0, match.start()) + 1 + hits.append((line_number, lines[line_number - 1])) + return hits + + +def _varshape_positive_test_pattern(Op: str, generic_varshape: bool) -> str: + """Return the positive VarShape-test heuristic for one operator. + + A generic suite is accepted only when its test name is correctness-bearing. This keeps + unrelated smoke or negative tests from satisfying TST-3 for legacy VarShape-only APIs. + """ + correctness_token = r"(? bool: + """Return whether image-layout Python coverage is semantically inapplicable.""" + return bool( + support_info.get("bench_layout_na") and support_info.get("bench_rgb_na") + ) + + +def check_test(P: OpPaths, support_info): + out = [] + t = read(P.test_cpp) + g = ( + ".agents/guidance/REVIEW_OP_GUIDELINES.md#test / make_operator.rst / " + ".agents/guidance/OPTIMIZATION_GUIDELINES.md" + ) + planar = support_info.get("planar") + planar_not_applicable = support_info.get("planar_not_applicable", False) + has_tensor = support_info.get("has_tensor") + has_vs = support_info.get("has_vs") + generic_varshape = support_info.get("generic_varshape", False) + + if t is None: + out.append( + Finding( + "TST-1", + "test", + GAP, + "C++ system test file not found", + f"{rel(P.test_cpp)} (missing)", + g, + "Add the C++ system test.", + ) + ) + t = "" + + # TST-1 reference fn (heuristic: a Gold/Reference/Ref helper or a goldX) + ref_hits = grep(r"Gold|gold|[Rr]eference|setGold|MatchesGold", t) + out.append( + Finding( + "TST-1", + "test", + PASS if ref_hits else MANUAL, + "Independent CPU reference present" + if ref_hits + else "No obvious CPU reference; verify", + first_evidence(P.test_cpp, ref_hits) or "no Gold/Reference symbol found", + g, + "" if ref_hits else "Add/confirm an independent CPU reference.", + ) + ) + + # TST-2 / TST-3 — a positive test on the op's (non-Negative) suite, any naming convention + if generic_varshape and not has_tensor: + out.append( + Finding("TST-2", "test", NA, "VarShape-only op -> tensor test N-A", "", g) + ) + else: + tensor_hits = _tensor_positive_test_hits(P.Op, t) + out.append( + Finding( + "TST-2", + "test", + PASS if tensor_hits else GAP, + ( + "tensor positive/correctness test present" + if tensor_hits + else "tensor positive/correctness test MISSING" + ), + first_evidence(P.test_cpp, tensor_hits) + or f"{rel(P.test_cpp)}: no positive Tensor TEST macro found", + g, + "" if tensor_hits else "Add tensor positive/correctness test.", + ) + ) + if has_vs: + out.append( + _present( + P.test_cpp, + t, + "TST-3", + "test", + _varshape_positive_test_pattern(P.Op, generic_varshape), + "varshape positive/correctness test", + g, + ) + ) + else: + out.append( + Finding("TST-3", "test", NA, "Tensor-only op -> varshape test N-A", "", g) + ) + + # TST-4 parametrized or typed suite + out.append( + _present( + P.test_cpp, + t, + "TST-4", + "test", + r"NVCV_(?:TEST_SUITE_P|TYPED_TEST_SUITE)", + "parametrized or typed test suite", + g, + ) + ) + + # TST-5 matrix-mirror (axis coverage) — best-effort; MANUAL residual for modes + out.append(matrix_mirror(P, t, support_info, g)) + + # TST-6 negative suite + out.append( + _present( + P.test_cpp, + t, + "TST-6", + "test", + rf"Op{P.Op}.*_Negative|_Negative", + "negative test suite", + g, + fix="Add negative tests asserting NVCV_ERROR_INVALID_ARGUMENT.", + ) + ) + + # TST-7 equivalent image-layout parity + if planar: + fp = grep(r"PlanarParityUtils|matches_interleaved", t) + out.append( + Finding( + "TST-7", + "test", + PASS if fp else GAP, + "Equivalent image-layout parity test present" + if fp + else "Equivalent image-layout parity test MISSING", + first_evidence(P.test_cpp, fp) + or "no PlanarParityUtils/matches_interleaved", + REVIEW_OP_GUIDE, + "" if fp else "Add Op%sPlanar.*_matches_interleaved." % P.Op, + ) + ) + elif planar_not_applicable: + out.append( + Finding( + "TST-7", + "test", + NA, + "Image layouts are not applicable -> parity N-A", + support_info.get("planar_reason", ""), + g, + ) + ) + else: + out.append( + Finding( + "TST-7", + "test", + GAP, + "Layout policy is incomplete; parity cannot be verified", + "SUP-10 requires NCHW/CHW or an operator-local inapplicability declaration", + g, + "Complete the operator's image-layout support contract.", + ) + ) + + # TST-8 tolerance discipline + out.append(tolerance_discipline(P, t, g)) + + # TST-9 reference independence + edge adequacy + out.append( + Finding( + "TST-9", + "test", + MANUAL, + "Reference independence + edge-case adequacy need human read", + f"Inspect {rel(P.test_cpp)}", + g, + ) + ) + + # TST-10 deterministic inputs + rng = grep( + r"\brand\(\)|std::random_device|std::mt19937(?!.*\()|setSeed|RandomValues|\bsrand\b", + t, + ) + seeded = grep(r"mt19937[^;]*\(\s*\d|seed|Seed|deterministic|FillDeterministic", t) + if grep(r"random_device", t): + out.append( + Finding( + "TST-10", + "test", + MANUAL, + "Possible unseeded RNG (random_device) — verify determinism", + first_evidence(P.test_cpp, grep(r"random_device", t)), + g, + "Use a fixed seed / deterministic fill.", + ) + ) + else: + out.append( + Finding( + "TST-10", + "test", + PASS if (seeded or not rng) else MANUAL, + "Inputs appear deterministic" + if (seeded or not rng) + else "RNG without obvious seed — verify", + first_evidence(P.test_cpp, seeded or rng) or "no RNG detected", + g, + ) + ) + + # Python TST-11..14 + py = read(P.test_py) + if py is None: + for tid, desc in [ + ("TST-11", "Python Tensor NHWC/HWC"), + ("TST-12", "Python VarShape"), + ("TST-13", "Python allocating + _into"), + ("TST-14", "Python negative"), + ]: + if tid == "TST-11" and python_image_layout_na(support_info): + out.append( + Finding( + tid, + "test", + NA, + "Coordinate-list Tensor input -> Python image layouts N-A", + "", + g, + ) + ) + elif tid == "TST-12" and not has_vs: + out.append( + Finding( + tid, "test", NA, "Tensor-only -> Python varshape N-A", "", g + ) + ) + else: + out.append( + Finding( + tid, + "test", + GAP, + f"{desc}: python test file missing", + f"{rel(P.test_py)} (missing)", + g, + "Add the Python API test.", + ) + ) + elif grep(r"make_op_tests", py): + # The repo's standard generator covers layouts / _into / negative API-surface tests. + sl = re.search(r"supported_layouts\s*=\s*\{([^}]*)\}", py) + ev = ( + f"{rel(P.test_py)}: make_op_tests(" + + (f"supported_layouts={{{sl.group(1).strip()}}}" if sl else "...") + + ")" + ) + out.append( + Finding( + "TST-11", "test", PASS, "Python Tensor layouts via make_op_tests", ev, g + ) + ) + out.append( + Finding( + "TST-12", + "test", + PASS if has_vs else NA, + "Python VarShape via make_op_tests" + if has_vs + else "Tensor-only -> Python varshape N-A", + ev if has_vs else "", + g, + ) + ) + out.append( + Finding( + "TST-13", + "test", + PASS, + "Python allocating + _into via make_op_tests", + ev, + g, + ) + ) + out.append( + Finding("TST-14", "test", PASS, "Python negative via make_op_tests", ev, g) + ) + else: + if python_image_layout_na(support_info): + out.append( + Finding( + "TST-11", + "test", + NA, + "Coordinate-list Tensor input -> Python image layouts N-A", + "", + g, + ) + ) + else: + out.append( + _present( + P.test_py, + py, + "TST-11", + "test", + r"NHWC|HWC|supported_layouts", + "Python Tensor layouts exercised", + g, + ) + ) + if has_vs: + out.append( + _present( + P.test_py, + py, + "TST-12", + "test", + r"VarShape|ImageBatch|var_shape|varshape", + "Python VarShape exercised", + g, + ) + ) + else: + out.append( + Finding( + "TST-12", "test", NA, "Tensor-only -> Python varshape N-A", "", g + ) + ) + out.append( + _present( + P.test_py, + py, + "TST-13", + "test", + rf"{P.pyname}_into|_into", + "Python allocating + _into", + g, + ) + ) + out.append( + _present( + P.test_py, + py, + "TST-14", + "test", + r"pytest\.raises|raises\(", + "Python negative cases", + g, + ) + ) + return out + + +def _present(path, text, tid, domain, pattern, desc, guideline, fix=None): + hits = grep(pattern, text) + if hits: + return Finding( + tid, domain, PASS, desc + " present", first_evidence(path, hits), guideline + ) + return Finding( + tid, + domain, + GAP, + desc + " MISSING", + f"{rel(path)}: pattern /{pattern}/ not found", + guideline, + fix or f"Add {desc}.", + ) + + +def matrix_mirror(P, t, support_info, g): + """Best-effort axis-coverage: which declared dtypes/channels/layouts appear in the test.""" + lim = support_info.get("limitations") or {} + if not t or not lim.get("dtypes"): + return Finding( + "TST-5", + "test", + MANUAL, + "Matrix-mirror not auto-evaluated (no parsed matrix or empty test)", + f"Inspect test cases in {rel(P.test_cpp)}", + g, + ) + # crude format-token presence: FMT_RGB8 (u8/3ch), FMT_RGBA8 (u8/4ch), FMT_*f32 (f32) ... + fmt_tokens = set(re.findall(r"FMT_[A-Za-z0-9]+", t)) + covered_layouts = set() + if grep(r"FMT_[A-Za-z0-9]+p\b|NCHW|CHW", t): + covered_layouts.add("NCHW") + if fmt_tokens or grep(r"NHWC|HWC", t): + covered_layouts.add("NHWC") + missing_layout = sorted( + (lim.get("layouts") or set()) - covered_layouts - {"HWC", "CHW"} + ) + if missing_layout: + return Finding( + "TST-5", + "test", + MANUAL, + "Axis-coverage partial; verify modes/dtypes; layouts not clearly covered: " + + ",".join(missing_layout), + f"format tokens in test: {sorted(fmt_tokens)[:8]}", + g, + "Add positive cases for the uncovered axis values.", + ) + return Finding( + "TST-5", + "test", + MANUAL, + "Axis-coverage (layout/dtype) looks present; MODE coverage needs human confirmation", + f"format tokens: {sorted(fmt_tokens)[:8]}; layouts covered: {sorted(covered_layouts)}", + g, + ) + + +def tolerance_discipline(P, t, g): + near = grep(r"EXPECT_NEAR|ASSERT_NEAR", t) + if not near: + return Finding( + "TST-8", + "test", + PASS, + "Bit-exact comparisons (no EXPECT_NEAR)", + "no EXPECT_NEAR/ASSERT_NEAR found", + ".agents/guidance/OPTIMIZATION_GUIDELINES.md", + ) + # any EXPECT_NEAR without an adjacent rationale comment -> MANUAL/needs-human + lines = t.splitlines() + justified = 0 + for ln, _ in near: + start = max(0, ln - 3) + ctx = " ".join(lines[start:ln]) + if re.search( + r"//.*(toler|FMA|contract|precision|rationale|because)", ctx, re.I + ): + justified += 1 + n = len(near) + return Finding( + "TST-8", + "test", + MANUAL, + f"{n} EXPECT_NEAR site(s), {justified} with rationale; bit-exact is the default — review", + first_evidence(P.test_cpp, near), + ".agents/guidance/OPTIMIZATION_GUIDELINES.md (do not silently bump tolerances)", + "Justify each EXPECT_NEAR with a rationale comment or switch to EXPECT_EQ; never loosen silently.", + ) + + +# ============================================================================= BENCH domain +def check_bench(P: OpPaths, support_info, do_run): + out = [] + g = ".agents/guidance/REVIEW_OP_GUIDELINES.md#bench / bench/README.md" + planar = support_info.get("planar") + planar_not_applicable = support_info.get("planar_not_applicable", False) + + # BEN-1/2 drivers + registration + cml_cpp = read(REPO / "bench/cpp/CMakeLists.txt") or "" + cml_py = read(REPO / "bench/python/CMakeLists.txt") or "" + cpp_ok = P.bench_cpp.exists() + out.append( + Finding( + "BEN-1", + "bench", + PASS if cpp_ok else GAP, + "C++ bench present" + + ( + "" + if f"Bench{P.Op}" in cml_cpp or P.op in cml_cpp.lower() + else " (registration unverified)" + ), + rel(P.bench_cpp) + ("" if cpp_ok else " (missing)"), + g, + "" + if cpp_ok + else "Add the C++ benchmark + register in bench/cpp/CMakeLists.txt.", + ) + ) + py_ok = P.bench_py.exists() + py_reg = ( + "" + if (f"bench_{P.op}" in cml_py or P.op in cml_py.lower()) + else " (registration unverified)" + ) + out.append( + Finding( + "BEN-2", + "bench", + PASS if py_ok else GAP, + "Python bench present" + py_reg, + rel(P.bench_py) + ("" if py_ok else " (missing)"), + g, + "" + if py_ok + else "Add the Python benchmark + register in bench/python/CMakeLists.txt.", + ) + ) + + # BEN-3 manifest + manifest = load_bench_cfg(REPO / "bench/config/bench_params.json") or {} + in_manifest = P.op in (manifest.get("operators") or {}) + out.append( + Finding( + "BEN-3", + "bench", + PASS if in_manifest else GAP, + "Manifest entry in bench_params.json", + f"operators.{P.op} {'present' if in_manifest else 'MISSING'}", + g, + "" + if in_manifest + else "Add the operator entry to bench/config/bench_params.json.", + ) + ) + + # BEN-4 config + tiers + cfg = load_bench_cfg(P.bench_cfg) + if not cfg or "configs" not in cfg: + out.append( + Finding( + "BEN-4", + "bench", + GAP, + "Operator bench config missing/invalid", + rel(P.bench_cfg) + " (missing or no 'configs')", + g, + "Add bench/config/operators/.json.", + ) + ) + # without a config, the rest of bench can't be evaluated + for tid in ("BEN-5", "BEN-6", "BEN-7", "BEN-8", "BEN-14"): + out.append( + Finding( + tid, + "bench", + GAP, + "Not evaluable — bench config missing", + rel(P.bench_cfg), + g, + "Add the bench config first.", + ) + ) + return out + configs = cfg["configs"] + bad_tier = [ + k for k, c in configs.items() if c.get("tier") not in ("basic", "advanced") + ] + out.append( + Finding( + "BEN-4", + "bench", + PASS if not bad_tier else GAP, + "Config present; all entries have a valid tier" + if not bad_tier + else "Configs with bad/missing tier: " + ",".join(bad_tier[:5]), + f"{len(configs)} configs", + g, + "" if not bad_tier else "Set tier=basic|advanced on every config.", + ) + ) + + # BEN-5 layout axis on every config, except where the reviewed benchmark + # semantics have no image-layout dimension (point sets, reductions, or an + # operator whose purpose is to define/transform layout). + no_layout = [ + k for k, c in configs.items() if "layout" not in c.get("string_axes", {}) + ] + layout_na = support_info.get("bench_layout_na") + if layout_na: + unexpected_layout = [k for k in configs if k not in no_layout] + out.append( + Finding( + "BEN-5", + "bench", + NA if not unexpected_layout else GAP, + "layout axis not applicable to this benchmark's semantics" + if not unexpected_layout + else f"layout N-A but {len(unexpected_layout)} config(s) carry a dummy layout axis", + "curated layout-axis N-A classification" + if not unexpected_layout + else "unexpected: " + ", ".join(unexpected_layout[:6]), + g, + "" + if not unexpected_layout + else "Remove the misleading layout axis from these configs.", + ) + ) + else: + out.append( + Finding( + "BEN-5", + "bench", + PASS if not no_layout else GAP, + "layout axis on every config" + if not no_layout + else f"{len(no_layout)} config(s) missing the layout axis", + "all configs carry string_axes.layout" + if not no_layout + else "missing: " + ", ".join(no_layout[:6]), + g, + "" + if not no_layout + else 'Add a truthful "layout" axis to every config.', + ) + ) + + # collect benched axis values per tier + benched = collect_benched(configs) + layouts = benched["all"]["layout"] + has_fake = bool({"NCHW_FAKE", "CHW_FAKE"} & layouts) + pair_issues = () + if has_fake: + try: + pair_issues = fake_planar_pairing_issues(configs) + except BaselineError as exc: + pair_issues = (f"cannot expand FakePlanar cases: {exc}",) + + # BEN-6 native and layout-conversion reference configs. A benchmark whose + # semantics have no truthful layout-comparison axis must not grow dummy + # NCHW/NCHW_FAKE rows even if the underlying operator accepts those layouts. + # A declared FakePlanar row always owns its pairing contract, even when + # support/curated inference otherwise classifies that comparison as N-A. + if pair_issues: + out.append( + Finding( + "BEN-6", + "bench", + GAP, + f"{len(pair_issues)} unmatched/invalid FakePlanar case signature(s); " + "each requires exactly one same-tier native NCHW/CHW Tensor case", + " | ".join(pair_issues), + REVIEW_OP_GUIDE, + "Add one advanced Tensor native NCHW/CHW case with every axis except " + "layout identical to each FakePlanar case.", + ) + ) + elif support_info.get("bench_layout_na"): + out.append( + Finding( + "BEN-6", + "bench", + NA, + "layout comparison axis not applicable to this benchmark's semantics", + "deterministic benchmark layout-axis N-A classification", + g, + ) + ) + elif planar: + has_tensor = support_info.get("has_tensor") + fake_planar_applicable = bool( + has_tensor and not support_info.get("bench_rgb_na") + ) + has_native = bool({"NCHW", "CHW"} & layouts) + ok = has_native and (has_fake or not fake_planar_applicable) + summary = ( + "Image-layout bench configs (NCHW + NCHW_FAKE)" + if ok and fake_planar_applicable + else ( + "Image-layout bench configs " + "(NCHW; NCHW_FAKE N-A for scalar/RGB-N-A tensor benchmark)" + if ok and has_tensor + else ( + "Image-layout bench configs " + "(NCHW; NCHW_FAKE N-A for var-shape-only op)" + if ok + else "Image-layout bench configs incomplete" + ) + ) + ) + evidence = f"native NCHW/CHW={has_native} FakePlanar={has_fake}" + out.append( + Finding( + "BEN-6", + "bench", + PASS if ok else GAP, + summary, + evidence, + REVIEW_OP_GUIDE, + "" + if ok + else ( + "Add native NCHW and NCHW_FAKE configs (uchar4 tensor-only)." + if fake_planar_applicable + else ( + "Add native NCHW configs; NCHW_FAKE is N-A for this " + "scalar/RGB-N-A tensor benchmark." + if has_tensor + else "Add native NCHW configs for the var-shape-only planar path." + ) + ), + ) + ) + elif planar_not_applicable: + out.append( + Finding( + "BEN-6", + "bench", + NA, + "Image layouts are not applicable -> layout bench configs N-A", + support_info.get("planar_reason", ""), + g, + ) + ) + else: + out.append( + Finding( + "BEN-6", + "bench", + GAP, + "Layout policy is incomplete; required bench configs cannot be determined", + "SUP-10 requires NCHW/CHW or an operator-local inapplicability declaration", + g, + "Complete the operator's image-layout support contract.", + ) + ) + + # BEN-SIZE apples-to-apples: layouts of one dtype share an input size + out.append(bench_size_uniformity(configs, g)) + + # BEN-7 baseline completeness + out.append(baseline_completeness(P, configs, g)) + + # BEN-8 row-count consistency + out.append(rowcount_consistency(P, cfg, g)) + + # BEN-9 validate_baselines + out.append( + Finding( + "BEN-9", + "bench", + MANUAL, + "Run the internal baseline validator to confirm case-key/SKU validity", + f"python3 bench/_internal/validate_baselines.py --operator {P.op}", + g, + "Fix any case-key/SKU validation errors it reports.", + ) + ) + + # BEN-10 parity / BEN-11 run-dependent + out.append( + Finding( + "BEN-10", + "bench", + PASS, + "C++/Python config parity is structural (shared config); full parity needs a run", + "shared bench/config/operators/%s.json drives both languages" % P.op, + g, + ) + ) + out.append( + Finding( + "BEN-11", + "bench", + MANUAL, + "Noise/parity quality and baseline currency require a GPU run", + f"run: python3 run_bench.py --operator {P.op} (use --run to attempt)", + g, + ) + ) + + # BEN-14 basic-tier floor (HARD) + out.append(basic_floor(P, benched, support_info, g)) + + # BEN-12/13 coverage stats + BEN-15/16 recommendations (advisory) + out += coverage_analysis(P, benched, support_info, g) + return out + + +def bench_size_uniformity(configs, g): + """BEN-SIZE (advisory): the interleaved (NHWC), native-planar (NCHW) and fake-planar + (NCHW_FAKE) configs of the *same dtype* should share the SAME input `shape`, so the three + layouts are an apples-to-apples comparison. Calibrate the interleaved config to 1-2 ms; the + planar / fake-planar configs reuse that shape and may legitimately run longer. We group configs + by their `dtypes` tuple and flag any group whose configs span more than one distinct shape. + """ + target_layouts = {"NHWC", "NCHW", "NCHW_FAKE"} + groups = {} + for k, c in configs.items(): + dtypes = c.get("dtypes", []) + layouts = [ + lay + for lay in c.get("string_axes", {}).get("layout", []) + if lay in target_layouts + ] + if not dtypes or not layouts: + continue + shape = tuple(c.get("string_axes", {}).get("shape", [])) + # Group per *individual* dtype (not the dtype-tuple) so a config's dtype is compared + # against every other config carrying it, regardless of how dtypes are bundled. + for dt in dtypes: + groups.setdefault(dt, []).append((k, tuple(layouts), shape)) + offenders = [] + for dt, members in groups.items(): + # Only a genuine cross-layout comparison: the dtype must span >1 of NHWC/NCHW/NCHW_FAKE. + layouts_seen = {lay for _, lays, _ in members for lay in lays} + if len(layouts_seen) < 2: + continue + distinct = {shape for _, _, shape in members} + if len(distinct) > 1: + detail = "; ".join( + f"{name}[{','.join(lays)}]={'x'.join(shape) or '?'}" + for name, lays, shape in members + ) + offenders.append(f"dtype {dt}: {detail}") + if not offenders: + return Finding( + "BEN-SIZE", + "bench", + PASS, + "Each dtype's layouts (NHWC/NCHW/NCHW_FAKE) share one input size (apples-to-apples)", + f"{len(groups)} dtype group(s) size-consistent", + ".agents/guidance/MAKE_OP_GUIDELINES.md", + ) + return Finding( + "BEN-SIZE", + "bench", + REC, + "Layouts of a dtype use different input sizes -> not an apples-to-apples comparison", + " | ".join(offenders), + ".agents/guidance/MAKE_OP_GUIDELINES.md", + "Give each dtype's NHWC/NCHW/NCHW_FAKE configs the SAME shape (calibrate the interleaved " + "config to 1-2 ms; planar/fake-planar reuse that shape and may run longer).", + ) + + +def collect_benched(configs): + """Return benched axis values overall and per tier.""" + + def empty(): + return {"layout": set(), "inputKind": set(), "dtypes": set(), "channels": set()} + + res = {"all": empty(), "basic": empty(), "advanced": empty()} + for c in configs.values(): + tier = c.get("tier") + sa = c.get("string_axes", {}) + ia = c.get("int64_axes", {}) + dts = c.get("dtypes", []) + targets = [res["all"]] + if tier in ("basic", "advanced"): + targets.append(res[tier]) + for tgt in targets: + tgt["layout"].update(sa.get("layout", [])) + tgt["inputKind"].update(sa.get("inputKind", [])) + tgt["dtypes"].update(dts) + # Most configs encode channels in vector dtype names (uchar3, + # float4). HQResize and similar scalar-element benchmarks expose a + # separate numChannels axis; that is the authoritative image + # channel count when present. + if "numChannels" in ia: + tgt["channels"].update(int(v) for v in ia["numChannels"]) + else: + for d in dts: + m = re.search(r"(\d)$", d) + tgt["channels"].add(int(m.group(1)) if m else 1) + return res + + +def basic_floor(P, benched, support_info, g): + b = benched["basic"] + has_tensor = support_info.get("has_tensor") + has_vs = support_info.get("has_vs") + planar = support_info.get("planar") + supported_channels = (support_info.get("limitations") or {}).get( + "channels" + ) or set() + missing = [] + # RGB = a 3-channel dtype present in basic + if ( + not support_info.get("bench_rgb_na") + and (not supported_channels or 3 in supported_channels) + and 3 not in b["channels"] + ): + missing.append("RGB(3-channel dtype)") + if has_tensor and "Tensor" not in b["inputKind"]: + missing.append("Tensor") + if has_vs and "VarShape" not in b["inputKind"]: + missing.append("VarShape") + if not support_info.get("bench_layout_na"): + if "NHWC" not in b["layout"]: + missing.append("interleaved NHWC") + if planar and "NCHW" not in b["layout"]: + missing.append("planar NCHW") + ok = not missing + return Finding( + "BEN-14", + "bench", + PASS if ok else GAP, + "Basic-tier minimum floor satisfied" + if ok + else "Basic-tier floor MISSING: " + ", ".join(missing), + f"basic: dt={sorted(b['dtypes'])} ik={sorted(b['inputKind'])} lay={sorted(b['layout'])}", + g, + "" + if ok + else "Add basic configs covering: " + + ", ".join(missing) + + " (RGB×Tensor×VarShape-if-applicable×NHWC×NCHW-if-applicable).", + ) + + +def baseline_completeness(P, configs, g): + sku = load_bench_cfg(REPO / "bench/config/sku_map.json") or {} + skus = sorted(e["stem"] for e in sku.get("entries", []) if "stem" in e) + if not skus: + return Finding( + "BEN-7", "bench", MANUAL, "No SKUs in sku_map.json to check against", "", g + ) + missing = [] + invalid = [] + total_cases = 0 + for k, c in configs.items(): + bl = c.get("baselines", {}) + if not isinstance(bl, dict): + invalid.append(f"{k}: baselines must be an object keyed by case-key") + continue + try: + expected_case_keys = expected_case_keys_for_entry(k, c) + except BaselineError as exc: + invalid.append(f"{k}: {exc}") + continue + total_cases += len(expected_case_keys) + for case_key in expected_case_keys: + per_sku = bl.get(case_key) + if per_sku is None: + missing.append((case_key, "*")) + continue + if not isinstance(per_sku, dict): + invalid.append( + f"{case_key}: baseline case payload must be an object keyed by SKU" + ) + continue + for s in skus: + if s not in per_sku: + missing.append((case_key, s)) + if not missing and not invalid: + return Finding( + "BEN-7", + "bench", + PASS, + f"Baselines complete for all {total_cases} case-keys × {len(skus)} SKUs", + f"SKUs={skus}", + g, + ) + if invalid: + sample = "; ".join(invalid[:4]) + return Finding( + "BEN-7", + "bench", + GAP, + f"Baseline completeness could not expand {len(invalid)} config(s)", + f"e.g. {sample}", + g, + "Fix malformed benchmark axes before regenerating baselines.", + ) + sample = "; ".join(f"{ck}→{s}" for ck, s in missing[:4]) + return Finding( + "BEN-7", + "bench", + GAP, + f"Baselines incomplete: {len(missing)} missing case/SKU entries", + f"e.g. {sample}", + g, + "If a declared case is unsupported, remove it from the config axes; otherwise " + "regenerate via MR baseline-regen CI for operator '%s' + " + "bench/_internal/update_baseline.py — never fabricate." % P.op, + ) + + +def rowcount_consistency(P, cfg, g): + configs = cfg["configs"] + basic_rows = sum( + rows_for_entry(c) for c in configs.values() if c.get("tier") == "basic" + ) + adv_rows = sum( + rows_for_entry(c) for c in configs.values() if c.get("tier") == "advanced" + ) + test = read(REPO / "bench/tests/test_run_bench_config_key.py") or "" + # look for an explicit per-op expected count if present + m = re.search(rf'["\']{P.op}["\']\s*:\s*(\d+)', test) + if m: + exp = int(m.group(1)) + ok = exp in (basic_rows, basic_rows + adv_rows) + return Finding( + "BEN-8", + "bench", + PASS if ok else GAP, + "Row-count matches test_run_bench_config_key.py" + if ok + else f"Row-count mismatch: computed basic={basic_rows} (adv={adv_rows}) vs test {exp}", + f"computed basic_rows={basic_rows} advanced_rows={adv_rows}", + g, + "" + if ok + else "Recompute and update the per-op/global counts in test_run_bench_config_key.py.", + ) + return Finding( + "BEN-8", + "bench", + MANUAL, + f"No per-op count in test; computed basic={basic_rows} adv={adv_rows}; verify global asserts", + "bench/tests/test_run_bench_config_key.py", + g, + "Ensure the global basic/advanced key+row asserts include this operator's rows.", + ) + + +def coverage_analysis(P, benched, support_info, g): + out = [] + lim = support_info.get("limitations") or {} + input_kinds = set() + if support_info.get("has_tensor"): + input_kinds.add("Tensor") + if support_info.get("has_vs"): + input_kinds.add("VarShape") + if "TensorBatch" in benched["all"]["inputKind"]: + input_kinds.add("TensorBatch") + + # BEN-13 statistics (per-axis, per-tier) + def axis_stat(axis, supported, key): + b, a = benched["basic"][key], benched["advanced"][key] + lines = [] + for v in ( + sorted(map(str, supported)) + if supported + else sorted(map(str, benched["all"][key])) + ): + where = ( + "basic" + if v in map(str, b) + else ("advanced" if v in map(str, a) else "none") + ) + lines.append(f"{v}:{where}") + return f"{axis}: " + ", ".join(lines) if lines else f"{axis}: (none)" + + layout_stats = ( + "layout: N-A (non-image/layout-defining benchmark)" + if support_info.get("bench_layout_na") + else axis_stat("layout", lim.get("layouts"), "layout") + ) + stats = [ + layout_stats, + axis_stat( + "inputKind", + input_kinds, + "inputKind", + ), + # bench dtype names (uchar3/float3...) don't match canonical Limitations tokens + # (u8/f32...), so report the actually-benched dtype names by tier instead. + axis_stat("dtypes(benched)", None, "dtypes"), + axis_stat("channels", lim.get("channels"), "channels"), + ] + out.append( + Finding( + "BEN-13", + "bench", + REC, + "Coverage statistics (per-axis × tier)", + " | ".join(stats), + g, + ) + ) + # BEN-15/16 recommendations vs curated per-op basic-expected list + _, _, basic_expected, _, _ = load_curated() + rec = basic_expected.get(P.op) + if rec: + out.append( + Finding( + "BEN-15", + "bench", + REC, + "Tiering check vs curated basic-expected list", + f"expected in basic: {rec}", + g, + "Ensure listed combos are in basic; rest in advanced.", + ) + ) + else: + out.append( + Finding( + "BEN-15", + "bench", + REC, + "No curated basic-expected list for this op (floor BEN-14 applies)", + "advisory; add deterministic tool data or operator-local evidence " + "if a stronger expectation is needed", + g, + ) + ) + # advisory: any supported layout/dtype entirely unbenched + unbenched = [] + if not support_info.get("bench_layout_na"): + for v in lim.get("layouts") or set(): + if v not in benched["all"]["layout"] and v not in ("HWC", "CHW"): + unbenched.append("layout=" + v) + for v in lim.get("dtypes") or set(): + pass # dtype token mapping to bench dtype names is approximate; skip to avoid false recs + if unbenched: + out.append( + Finding( + "BEN-16", + "bench", + REC, + "Unbenched supported axis values (advisory)", + ", ".join(unbenched), + g, + "Consider adding to advanced (or basic if popular).", + ) + ) + return out + + +# ============================================================================== DOCS domain +def check_docs(P: OpPaths, support_info): + out = [] + g = ".agents/guidance/REVIEW_OP_GUIDELINES.md#docs / make_operator.rst / AGENTS.md" + + oplist = read(REPO / "docs/sphinx/operator_list.rst") or "" + row = grep(rf":py:func:`cvcuda\.{re.escape(P.pyname)}`", oplist) + out.append( + Finding( + "DOC-1", + "docs", + PASS if row else GAP, + "operator_list.rst row present" if row else "operator_list.rst row MISSING", + first_evidence(REPO / "docs/sphinx/operator_list.rst", row) + or f"no :py:func:`cvcuda.{P.pyname}` row", + g, + "" if row else f"Add a row for cvcuda.{P.pyname} to operator_list.rst.", + ) + ) + + ops = read(REPO / "docs/sphinx/modules/python/operators.rst") or "" + fn = grep(rf"cvcuda-autofunction::\s*cvcuda\.{re.escape(P.pyname)}\b", ops) + into = grep(rf"cvcuda-autofunction::\s*cvcuda\.{re.escape(P.pyname)}_into\b", ops) + ok = fn and into + out.append( + Finding( + "DOC-2", + "docs", + PASS if ok else GAP, + "Python autofunction directives (fn + _into)" + if ok + else "autofunction directive(s) missing", + f"fn={'yes' if fn else 'no'} _into={'yes' if into else 'no'}", + g, + "" + if ok + else f"Add cvcuda-autofunction:: cvcuda.{P.pyname}{{,_into}} to operators.rst.", + ) + ) + + out.append( + Finding( + "DOC-3", + "docs", + MANUAL, + "Confirm the operator appears in the C++ API reference", + "check docs/sphinx C++ API listing / doxygen", + g, + ) + ) + + # DOC-4 limitations-vs-code (cross-domain) — diff declared vs (enforced/tested) is partly in SUP-9/TST-5 + out.append( + Finding( + "DOC-4", + "docs", + MANUAL, + "Limitations table vs code/tests consistency (cross-domain diff)", + "compare SUP-6/7/8 declared vs SUP-9 enforced vs TST-5 tested", + g, + "Reconcile the Limitations table with the actual guards/tests.", + ) + ) + + pytext = read(P.pybind) or "" + doc = grep(r'R"pbdoc|pbdoc\(|"""|Args:|Returns:', pytext) + out.append( + Finding( + "DOC-5", + "docs", + PASS if doc else GAP, + "Python binding docstrings present" if doc else "Python docstrings missing", + first_evidence(P.pybind, doc) or rel(P.pybind), + g, + "" + if doc + else "Add pbdoc docstrings (args/returns/layouts) for the op + _into.", + ) + ) + + out.append( + Finding( + "DOC-6", + "docs", + MANUAL, + "Doxygen param docs explain 'why' + match code", + f"inspect {rel(P.header)}", + g, + ) + ) + + # DOC-7 SPDX on the op's files (skip .json — JSON cannot carry comment headers) + spdx_missing = [] + for f in [ + P.header, + P.hpp, + P.pybind, + P.test_cpp, + P.test_py, + P.bench_cpp, + P.bench_py, + ]: + if f.suffix == ".json": + continue + txt = read(f) + if txt is not None and "SPDX-License-Identifier" not in txt[:600]: + spdx_missing.append(rel(f)) + out.append( + Finding( + "DOC-7", + "docs", + PASS if not spdx_missing else GAP, + "SPDX headers present on the op's files" + if not spdx_missing + else "SPDX header missing in: " + ", ".join(spdx_missing[:4]), + "all checked files carry SPDX" + if not spdx_missing + else ", ".join(spdx_missing), + "AGENTS.md", + "" + if not spdx_missing + else "Add the SPDX 2026 header to the listed file(s).", + ) + ) + return out + + +# ================================================================================= driver +def run(P: OpPaths, domains, curated, do_run): + findings = [] + sup, support_info = check_support(P, curated) + if "support" in domains: + findings += sup + if "test" in domains: + findings += check_test(P, support_info) + if "bench" in domains: + findings += check_bench(P, support_info, do_run) + if "docs" in domains: + findings += check_docs(P, support_info) + return findings + + +def render_md(P, findings, domains): + icon = {PASS: "✅", GAP: "❌", NA: "➖", MANUAL: "🔍", REC: "💡"} + lines = [f"# review-op: {P.Op} (op={P.op}, py=cvcuda.{P.pyname})", ""] + counts = {} + for dom in domains: + df = [f for f in findings if f.domain == dom] + if not df: + continue + lines.append(f"## {dom}") + for f in df: + counts[f.status] = counts.get(f.status, 0) + 1 + lines.append( + f"- {icon.get(f.status, '?')} **{f.status}** `{f.id}` — {f.summary}" + ) + if f.evidence: + lines.append(f" - evidence: {f.evidence}") + if f.status in (GAP,) and f.fix: + lines.append(f" - fix: {f.fix}") + if f.guideline: + lines.append(f" - ref: {f.guideline}") + gaps = sum(1 for f in df if f.status == GAP) + man = sum(1 for f in df if f.status == MANUAL) + verdict = ( + "PASS" if gaps == 0 and man == 0 else ("GAPS" if gaps else "NEEDS-REVIEW") + ) + lines.append(f" → **{dom} verdict: {verdict}** ({gaps} GAP, {man} MANUAL)") + lines.append("") + total_gap = counts.get(GAP, 0) + total_man = counts.get(MANUAL, 0) + overall = ( + "PASS" + if total_gap == 0 and total_man == 0 + else ("GAPS" if total_gap else "NEEDS-REVIEW") + ) + lines.append("## verdict") + lines.append( + f"**{overall}** — " + ", ".join(f"{k}={v}" for k, v in sorted(counts.items())) + ) + lines.append("") + lines.append("Completion = a re-run shows zero GAP and zero unresolved MANUAL.") + return "\n".join(lines) + + +def render_json(P, findings, domains): + counts = {} + for f in findings: + counts[f.status] = counts.get(f.status, 0) + 1 + return json.dumps( + { + "operator": P.Op, + "op": P.op, + "pyname": P.pyname, + "domains": list(domains), + "findings": [vars(f) for f in findings], + "counts": counts, + "exit_gap": counts.get(GAP, 0), + }, + indent=2, + sort_keys=True, + ) + + +def main(argv=None): + ap = argparse.ArgumentParser( + description="Deterministic per-operator review checker (CV-CUDA)." + ) + ap.add_argument("operator", help="Operator name (PascalCase, e.g. CenterCrop)") + ap.add_argument( + "--domain", default="all", help="support|test|bench|docs|all (comma-separated)" + ) + ap.add_argument("--format", default="md", choices=["md", "json"]) + ap.add_argument( + "--out", default=None, help="write the report to this path as well as stdout" + ) + ap.add_argument( + "--run", + action="store_true", + help="attempt run-dependent bench checks (needs a built GPU env)", + ) + ap.add_argument( + "--fix", + action="store_true", + help="(wrapper-level) emphasize fix actions; the checker stays read-only", + ) + args = ap.parse_args(argv) + + domains = ( + DOMAINS + if args.domain == "all" + else tuple(d.strip() for d in args.domain.split(",") if d.strip() in DOMAINS) + ) + if not domains: + ap.error("no valid --domain selected (choose from support,test,bench,docs,all)") + + if args.fix: + print( + "note: review_op.py is read-only; --fix is applied by the /review-op " + "wrapper/agent per .agents/guidance/REVIEW_OP_GUIDELINES.md (see each GAP's 'fix' field).", + file=sys.stderr, + ) + + P = resolve_op(args.operator) + curated = load_curated() + findings = run(P, domains, curated, args.run) + + report = ( + render_md(P, findings, domains) + if args.format == "md" + else render_json(P, findings, domains) + ) + print(report) + if args.out: + Path(args.out).write_text(report + "\n", encoding="utf-8") + + return 1 if any(f.status == GAP for f in findings) else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/symcompat/symcompat.sh b/tools/symcompat/symcompat.sh index da97942ae..70a4c6673 100755 --- a/tools/symcompat/symcompat.sh +++ b/tools/symcompat/symcompat.sh @@ -1,6 +1,6 @@ #!/bin/bash -e -# SPDX-FileCopyrightText: Copyright (c) 2022 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,7 +20,7 @@ # It outputs a list of missing symbol in A that could and could not be found in B. -if [ $# -lt 1 ]; then +if [[ $# -lt 1 ]]; then echo "Invalid args. Usage: $(basename $0) [old lib,...]" exit 1 fi @@ -44,7 +44,7 @@ for lib in $oldlibs; do libsyms="$(readelf -sW $lib | awk '$4 ~ /FUNC/ && $5 ~ /(GLOBAL|WEAK)/ && $6 ~ /DEFAULT/ && $7 !~ /UND/ && $8 ~ /.*@@/ { print $8 }' | sort | uniq)" found="$(echo "$libsyms" | egrep "$misspattern" | sed 's/@@/@/g' || true)" echo "------ $lib" 1>&2 - if [ "$found" ]; then + if [[ "$found" ]]; then #if [ "$total_found" ]; then # total_found+="\n" #fi @@ -56,12 +56,12 @@ total_found="$(echo -e "$total_found" | egrep -v "^[[:space:]]*$" | sort | uniq) notfound="$(diff <( echo "$missingsyms" ) <( echo "$total_found" | awk -F '@' '{ print $1 }') || true)" -if [ "$notfound" ]; then +if [[ "$notfound" ]]; then echo -e "\nSymbols not found:" echo "$notfound" | awk '/^ byte-identical scaffold report.""" + a = run("Flip", "--phase", "scaffold", "--format", "json") + b = run("Flip", "--phase", "scaffold", "--format", "json") + assert a.stdout == b.stdout + + +def test_exit_code_matches_gap_contract(): + r = run("Flip", "--phase", "scaffold", "--format", "json") + findings = json.loads(r.stdout)["findings"] + assert all(f["status"] in VALID_STATUS for f in findings) + has_gap = any(f["status"] == "GAP" for f in findings) + assert r.returncode in (0, 1) + assert (r.returncode == 1) == has_gap + + +def test_findings_carry_evidence_and_guideline(): + for f in json.loads(run("Flip", "--phase", "done", "--format", "json").stdout)[ + "findings" + ]: + assert f["id"] and f["domain"] and f["status"] + if f["status"] != "N-A": + assert f.get("evidence") or f.get("guideline") + + +def test_unknown_operator_degrades_gracefully(): + """A bogus operator must not crash; scaffold must GAP on the missing public header.""" + r = run("NotARealMakeOp", "--phase", "scaffold", "--format", "json") + scf1 = [f for f in json.loads(r.stdout)["findings"] if f["id"] == "SCF-1"] + assert scf1 and scf1[0]["status"] == "GAP" + + +def test_bare_mode_delegates_spec_as_manual(): + """--bare: the SPEC items must be MANUAL (spec delegated), never GAP.""" + r = run("NotARealMakeOp", "--phase", "scaffold", "--bare", "--format", "json") + spec = [ + f + for f in json.loads(r.stdout)["findings"] + if f["id"].startswith("SPEC-") and f["id"] != "SPEC-CORRECT" + ] + assert spec, "expected SPEC findings" + assert all(f["status"] == "MANUAL" for f in spec) + + +def test_spec_gated_without_bare(): + """Without --bare, an unauthored stub header GAPs SPEC-BRIEF/ORACLE.""" + r = run("NotARealMakeOp", "--phase", "scaffold", "--format", "json") + ids = {f["id"]: f["status"] for f in json.loads(r.stdout)["findings"]} + assert ids.get("SPEC-ORACLE") == "GAP" + + +# --------------------------------------------------------------------------- COV unit tests +def test_bench_dtype_canon(): + cases = { + "uchar3": "u8", + "uchar4": "u8", + "uint8": "u8", + "float3": "f32", + "float4": "f32", + "float32": "f32", + "float16": "f16", + "half": "f16", + "ushort": "u16", + "uint16": "u16", + "short3": "s16", + "double": "f64", + "float64": "f64", + } + for name, canon in cases.items(): + assert make_op.bench_dtype_canon(name) == canon, name + + +def test_fmt_channels_inference(): + assert make_op.fmt_channels("FMT_RGB8 FMT_RGBA8 FMT_U8") == {1, 3, 4} + assert make_op.fmt_channels("nvcv::TYPE_F32") == {1} + assert 4 in make_op.fmt_channels("FMT_RGBAf32") + assert 3 in make_op.fmt_channels("FMT_BGRf32") + + +def test_dtype_token_detection(): + import re + + txt = "ValueList { FMT_RGB8, FMT_RGBAf32, FMT_F16 }" + assert re.search(make_op.DTYPE_TEST_RE["u8"], txt) + assert re.search(make_op.DTYPE_TEST_RE["f32"], txt) + assert re.search(make_op.DTYPE_TEST_RE["f16"], txt) + # a dtype with no token present is (correctly) not detected + assert not re.search(make_op.DTYPE_TEST_RE["f64"], txt) diff --git a/tools/tests/test_optimize_op.py b/tools/tests/test_optimize_op.py new file mode 100644 index 000000000..55ef6a1fe --- /dev/null +++ b/tools/tests/test_optimize_op.py @@ -0,0 +1,1482 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. +"""Contract/smoke tests for the optimization DoD checker (tools/optimize_op.py). + +Guards the harness's promises — determinism, the GAP/MANUAL exit contract, correct +changed-implementation attribution (incl. the cross-op CamelCase-prefix trap), and +results-summary format enforcement — without coupling to live per-op state. Stdlib-only; +runs in the bench Python unit-test CI step alongside tools/tests/test_review_op.py. +""" +import importlib.util +import json +import math +import subprocess +import sys +from pathlib import Path +from types import SimpleNamespace + +REPO = Path(__file__).resolve().parents[2] +TOOL = REPO / "tools" / "optimize_op.py" +VALID = {"PASS", "GAP", "N-A", "MANUAL"} + + +def run(*args): + return subprocess.run( + [sys.executable, str(TOOL), *args], + cwd=str(REPO), + capture_output=True, + text=True, + timeout=60, + ) + + +def _module(): + spec = importlib.util.spec_from_file_location("optimize_op", TOOL) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod + + +def _synthetic_reviewed_scope(mr_iid, primary, secondary): + return ( + str(mr_iid), + str(primary).casefold(), + tuple(sorted(operator.casefold() for operator in secondary)), + ) == ("123", "osd", ("bndbox",)) + + +# ---- CLI contract ----------------------------------------------------------------------- +def test_preflight_runs_and_reports(): + r = run("Resize", "--phase", "preflight", "--format", "json") + data = json.loads(r.stdout) + assert data["phase"] == "preflight" and data["findings"] + assert all(f["status"] in VALID for f in data["findings"]) + + +def test_phases_are_deterministic(): + for phase in ("preflight", "evidence"): + a = run("Resize", "--phase", phase, "--format", "json") + b = run("Resize", "--phase", phase, "--format", "json") + assert a.stdout == b.stdout, f"{phase} not deterministic" + + +def test_exit_code_matches_blocking_contract(): + r = run("Resize", "--phase", "evidence", "--format", "json") + findings = json.loads(r.stdout)["findings"] + assert all(f["status"] in VALID for f in findings) + has_blocker = any(f["status"] in {"GAP", "MANUAL"} for f in findings) + assert r.returncode in (0, 1) + assert (r.returncode == 1) == has_blocker + + +def test_evidence_exit_blocks_unresolved_manual(monkeypatch): + m = _module() + monkeypatch.setattr( + m, + "check_evidence", + lambda *_args: [m.Finding("ODO-X", "evidence", m.MANUAL, "needs proof")], + ) + assert m.main(["Resize", "--phase", "evidence"]) == 1 + + +def test_phase_is_required_and_validated(): + assert run("Resize").returncode == 2 # missing --phase + assert run("Resize", "--phase", "bogus").returncode == 2 # invalid choice + + +def test_summary_initialization_requires_explicit_artifact_inputs(): + r = run("Resize", "--phase", "summary") + assert r.returncode == 2 + assert "--benchmark-base" in r.stderr + assert "--optimized-cases-file" in r.stderr + + +def test_preflight_ben6_gap_blocks_pre2(monkeypatch): + m = _module() + paths = m.resolve_op("Invert") + + def findings(_paths, domain): + if domain == "bench": + return [ + { + "id": "BEN-6", + "status": "GAP", + "summary": "1 unmatched FakePlanar case", + } + ] + return [] + + monkeypatch.setattr(m, "review_op_findings", findings) + monkeypatch.setattr(m, "read", lambda _path: '{"baselines":{"case":{}}}') + + pre2 = next( + item for item in m.check_preflight(paths, "origin/main") if item.id == "PRE-2" + ) + + assert pre2.status == "GAP" + assert "BEN-6" in pre2.evidence + + +def test_final_evidence_reruns_ben6_and_blocks_orphan_fake_planar(monkeypatch): + m = _module() + paths = m.resolve_op("Invert") + + monkeypatch.setattr(m, "all_operator_stems", lambda: {"invert"}) + monkeypatch.setattr(m, "changed_paths", lambda _base: []) + monkeypatch.setattr(m, "read_results", lambda _path: "") + monkeypatch.setattr( + m, + "review_op_findings", + lambda _paths, domain: ( + [ + { + "id": "BEN-6", + "status": "GAP", + "summary": "1 unmatched FakePlanar case", + } + ] + if domain == "bench" + else [] + ), + ) + monkeypatch.setattr( + m, + "_leads_exhausted", + lambda *_args: m.Finding("ODO-4", "evidence", m.PASS, "ok"), + ) + monkeypatch.setattr( + m, + "_api_abi", + lambda *_args: m.Finding("ODO-5", "evidence", m.PASS, "ok"), + ) + monkeypatch.setattr( + m, + "_perf_hygiene", + lambda *_args: m.Finding("ODO-6", "evidence", m.PASS, "ok"), + ) + monkeypatch.setattr( + m, + "_baseline_regression_gate", + lambda *_args: m.Finding("ODO-7", "evidence", m.PASS, "ok"), + ) + monkeypatch.setattr( + m, + "_review_refactor_gate", + lambda *_args: m.Finding("ODO-8", "evidence", m.PASS, "ok"), + ) + monkeypatch.setattr( + m, + "_memory_footprint_gate", + lambda *_args: m.Finding("ODO-9", "evidence", m.PASS, "ok"), + ) + monkeypatch.setattr(m, "_changed_perf_operators", lambda *_args: {"invert"}) + monkeypatch.setattr( + m, + "_results_format", + lambda *_args: m.Finding("ODO-3", "evidence", m.PASS, "ok"), + ) + + evidence = m.check_evidence(paths, "origin/main", None) + ben6 = next(item for item in evidence if item.id == "ODO-11") + + assert ben6.status == "GAP" + assert "BEN-6" in ben6.evidence + + +def test_final_fake_planar_gate_does_not_autopass_unresolved_status(monkeypatch): + m = _module() + paths = m.resolve_op("Invert") + + for status in (m.MANUAL, "UNEXPECTED"): + monkeypatch.setattr( + m, + "review_op_findings", + lambda *_args, status=status: [ + { + "id": "BEN-6", + "status": status, + "summary": "coverage result needs review", + } + ], + ) + + finding = m._fake_planar_coverage_gate(paths, "guide") + + assert finding.status == m.MANUAL + assert repr(status) in finding.evidence + assert "coverage result needs review" in finding.evidence + + +# ---- changed-implementation attribution (deterministic, no git) ------------------------- +def test_kernel_attribution_longest_prefix(): + m = _module() + ops = m.all_operator_stems() + resize = m.resolve_op("Resize") + hq = m.resolve_op("HQResize") + assert m._is_op_kernel("src/cvcuda/priv/OpResize.cu", resize, ops) is True + # cross-op trap: a longer operator name must not be attributed to the shorter prefix + assert ( + m._is_op_kernel("src/cvcuda/priv/OpResizeCropConvertReformat.cu", resize, ops) + is False + ) + assert m._is_op_kernel("src/cvcuda/priv/OpHQResizeKernel.cuh", resize, ops) is False + assert m._is_op_kernel("src/cvcuda/priv/OpHQResizeKernel.cuh", hq, ops) is True + # non-priv paths never count + assert m._is_op_kernel("bench/config/operators/resize.json", resize, ops) is False + + +def test_odo10_accepts_synthetic_reviewed_secondary_scope(monkeypatch): + m = _module() + osd = m.resolve_op("OSD") + monkeypatch.setattr(m, "is_reviewed_secondary_scope", _synthetic_reviewed_scope) + monkeypatch.setattr( + m, + "parse_summary", + lambda _results: SimpleNamespace( + metadata=SimpleNamespace(operator="OSD", secondary_operators=("BndBox",)) + ), + ) + + finding = m._operator_scope_gate(osd, {"osd", "bndbox"}, "summary", "123", "guide") + + assert finding.status == m.PASS + assert "code-reviewed secondary" in finding.summary + + +def test_odo10_secondary_scope_fails_closed(monkeypatch): + m = _module() + osd = m.resolve_op("OSD") + monkeypatch.setattr(m, "is_reviewed_secondary_scope", _synthetic_reviewed_scope) + + cases = ( + ("122", "OSD", ("BndBox",), {"osd", "bndbox"}), + ("123", "BndBox", ("OSD",), {"osd", "bndbox"}), + ("123", "OSD", ("BndBox",), {"osd"}), + ("123", "OSD", ("BndBox",), {"osd", "bndbox", "warp"}), + ("123", "OSD", ("Warp",), {"osd", "warp"}), + ) + for mr_iid, metadata_operator, secondary, touched in cases: + monkeypatch.setattr( + m, + "parse_summary", + lambda _results, metadata_operator=metadata_operator, secondary=secondary: ( + SimpleNamespace( + metadata=SimpleNamespace( + operator=metadata_operator, + secondary_operators=secondary, + ) + ) + ), + ) + + finding = m._operator_scope_gate(osd, touched, "summary", mr_iid, "guide") + + assert finding.status == m.GAP, (mr_iid, metadata_operator, secondary, touched) + + +def test_odo10_default_scope_remains_strict(monkeypatch): + m = _module() + osd = m.resolve_op("OSD") + monkeypatch.setattr( + m, + "parse_summary", + lambda _results: SimpleNamespace( + metadata=SimpleNamespace(operator="OSD", secondary_operators=()) + ), + ) + + assert ( + m._operator_scope_gate(osd, {"osd"}, "summary", None, "guide").status == m.PASS + ) + assert ( + m._operator_scope_gate(osd, {"osd", "bndbox"}, "summary", None, "guide").status + == m.GAP + ) + + +def test_implementation_attribution_includes_only_the_matching_binding(): + m = _module() + ops = m.all_operator_stems() + resize = m.resolve_op("Resize") + hq = m.resolve_op("HQResize") + + assert m._is_op_implementation( + "python/mod_cvcuda/operators/OpHQResize.cpp", hq, ops + ) + assert not m._is_op_implementation( + "python/mod_cvcuda/operators/OpHQResize.cpp", resize, ops + ) + assert m._is_op_implementation("src/cvcuda/priv/OpHQResizeKernel.cuh", hq, ops) + + +def test_kernel_attribution_explicit_shared_legacy_sources(): + m = _module() + ops = m.all_operator_stems() + histogram = m.resolve_op("Histogram") + histogram_eq = m.resolve_op("HistogramEq") + warp_affine = m.resolve_op("WarpAffine") + warp_perspective = m.resolve_op("WarpPerspective") + + calc_hist = "src/cvcuda/priv/legacy/calc_hist.cu" + assert m._is_op_kernel(calc_hist, histogram, ops) is True + assert m._is_op_kernel(calc_hist, histogram_eq, ops) is False + + shared_warp = "src/cvcuda/priv/legacy/warp.cu" + assert m._is_op_kernel(shared_warp, warp_affine, ops) is True + assert m._is_op_kernel(shared_warp, warp_perspective, ops) is True + + +def test_multi_operator_perf_attribution_uses_configs_and_longest_priv_prefix(): + m = _module() + ops = m.all_operator_stems() + touched = m._changed_perf_operators( + [ + "bench/config/operators/resize.json", + "bench/config/operators/warpaffine.json", + "src/cvcuda/priv/OpHQResizeKernel.cuh", + "python/mod_cvcuda/operators/OpPillowResize.cpp", + "docs/sphinx/api/python.rst", + ], + ops, + ) + assert touched == {"resize", "warpaffine", "hqresize", "pillowresize"} + + +def test_pixelwise_assertion_requires_a_concrete_assertion_name(): + m = _module() + + for evidence in ( + "EXPECT_EQ(output, reference)", + "EXPECT_NEAR(output, reference, 1e-6)", + "assert bool(cupy.all(output == reference))", + "assert output == reference", + "cupy.testing.assert_array_equal(output, reference)", + ): + assert m._has_pixelwise_assertion(evidence), evidence + + for evidence in ( + "We assert every pixel matches the independent reference.", + "EXPECT_EQ compares every output pixel", + "cupy.testing.assert_array_equal verifies the output", + "The Python test asserts exact output", + "The equality assertion passed", + "Pixel equality was checked", + ): + assert not m._has_pixelwise_assertion(evidence), evidence + + +def test_binding_only_change_triggers_implementation_evidence_gates(monkeypatch): + m = _module() + hq = m.resolve_op("HQResize") + binding = "python/mod_cvcuda/operators/OpHQResize.cpp" + reviewed = [] + + monkeypatch.setattr(m, "all_operator_stems", lambda: {"hqresize"}) + monkeypatch.setattr(m, "changed_paths", lambda _base: [binding]) + monkeypatch.setattr( + m, + "read_results", + lambda _path: ( + "- [x] **Pixelwise equality to reference** — `assert " + "bool(cupy.all(output == expected))` checks every pixel against a " + "constant-value oracle; 6 passed.\n" + ), + ) + monkeypatch.setattr(m, "review_op_findings", lambda *_args: []) + monkeypatch.setattr( + m, + "git", + lambda *args: '+ "gpu_gap_stddev_us": 4.25' + if args[0] == "diff" + else "", + ) + monkeypatch.setattr( + m, + "_leads_exhausted", + lambda *_args: m.Finding("ODO-4", "evidence", m.PASS, "ok"), + ) + monkeypatch.setattr( + m, + "_api_abi", + lambda *_args: m.Finding("ODO-5", "evidence", m.PASS, "ok"), + ) + monkeypatch.setattr( + m, + "_perf_hygiene", + lambda *_args: m.Finding("ODO-6", "evidence", m.PASS, "ok"), + ) + monkeypatch.setattr( + m, + "_baseline_regression_gate", + lambda *_args: m.Finding("ODO-7", "evidence", m.PASS, "ok"), + ) + + def review_gate(_paths, _results, changed, _guide): + reviewed.extend(changed) + return m.Finding("ODO-8", "evidence", m.PASS, "ok") + + monkeypatch.setattr(m, "_review_refactor_gate", review_gate) + monkeypatch.setattr(m, "_changed_perf_operators", lambda *_args: {"hqresize"}) + monkeypatch.setattr( + m, + "_results_format", + lambda *_args: m.Finding("ODO-3", "evidence", m.PASS, "ok"), + ) + monkeypatch.setattr( + m, + "_fake_planar_coverage_gate", + lambda *_args: m.Finding("ODO-11", "evidence", m.PASS, "ok"), + ) + + findings = m.check_evidence(hq, "origin/main", None) + + assert not any(item.id == "ODO-0" for item in findings) + assert next(item for item in findings if item.id == "ODO-1").status == "PASS" + assert next(item for item in findings if item.id == "ODO-2").status == "PASS" + assert next(item for item in findings if item.id == "ODO-9").status == "GAP" + assert reviewed == [binding] + + +def test_perf_hygiene_treats_binding_changes_as_implementation(monkeypatch): + m = _module() + hq = m.resolve_op("HQResize") + binding = "python/mod_cvcuda/operators/OpHQResize.cpp" + sha = "1" * 40 + merge_base = "2" * 40 + + for subject, expected in ( + ("fix(hqresize): cache requirements", "GAP"), + ("perf(hqresize): cache requirements", "PASS"), + ): + + def fake_git(*args, subject=subject): + if args[0] == "merge-base": + return merge_base + "\n" + if args[0] == "log": + assert args[1] == f"{merge_base}..HEAD" + assert "--no-merges" in args + return f"{sha}\x1f{subject}\n" + if args[0] == "show": + return binding + "\n" + return "" + + monkeypatch.setattr(m, "git", fake_git) + result = m._perf_hygiene(hq, "origin/main", {"hqresize"}, "guide") + assert result.status == expected, subject + + +def test_perf_hygiene_excludes_target_only_commits_from_diverged_base(monkeypatch): + m = _module() + hq = m.resolve_op("HQResize") + branch_sha = "1" * 40 + target_sha = "2" * 40 + merge_base = "3" * 40 + binding = "python/mod_cvcuda/operators/OpHQResize.cpp" + + def fake_git(*args): + if args[0] == "merge-base": + return merge_base + "\n" + if args[0] == "log": + assert args[1] == f"{merge_base}..HEAD" + assert "--no-merges" in args + return f"{branch_sha}\x1fperf(hqresize): cache requirements\n" + if args[0] == "show": + assert args[-1] != target_sha + return binding + "\n" + return "" + + monkeypatch.setattr(m, "git", fake_git) + + result = m._perf_hygiene(hq, target_sha, {"hqresize"}, "guide") + + assert result.status == "PASS" + + +def _binding_impact_config( + case_keys, + *, + gap_us, + gap_stddev_us=2.0, + noise_us=2.0, + n_runs=5, + include_gap_stddev=True, +): + configs = {} + for case_key in case_keys: + config_key = case_key.split("[", 1)[0] + baselines = {} + for sku in ("A100", "H100"): + metrics = { + "n_runs": n_runs, + "gpu_time_us_cpp": 1000.0, + "gpu_time_us_python": 1000.0 + gap_us, + "gpu_noise_us_cpp": noise_us, + "gpu_noise_us_python": noise_us, + } + if include_gap_stddev: + metrics["gpu_gap_stddev_us"] = gap_stddev_us + baselines[sku] = metrics + configs[config_key] = { + "tier": "advanced", + "baselines": { + case_key: baselines, + }, + } + return {"benchmark": "hqresize", "configs": configs} + + +def _binding_impact_inputs( + monkeypatch, + module, + *, + reduction_us, + gap_stddev_us=2.0, + noise_us=2.0, + n_runs=5, + include_gap_stddev=True, +): + cases = ( + "target_nhwc[layout=NHWC][inputKind=TensorBatch]", + "target_nchw[layout=NCHW][inputKind=TensorBatch]", + ) + metadata = SimpleNamespace( + impact_metric="python_overhead", + baseline_commit="a" * 40, + optimized_cases=cases, + ) + baseline = _binding_impact_config( + cases, + gap_us=40.0, + gap_stddev_us=gap_stddev_us, + noise_us=noise_us, + n_runs=n_runs, + include_gap_stddev=include_gap_stddev, + ) + candidate = _binding_impact_config( + cases, + gap_us=40.0 - reduction_us, + gap_stddev_us=gap_stddev_us, + noise_us=noise_us, + n_runs=n_runs, + include_gap_stddev=include_gap_stddev, + ) + sku_map = {"entries": [{"stem": "A100"}, {"stem": "H100"}]} + + monkeypatch.setattr( + module, "parse_summary", lambda _results: SimpleNamespace(metadata=metadata) + ) + monkeypatch.setattr(module, "git_json", lambda *_args: baseline) + monkeypatch.setattr( + module, + "_load_json", + lambda path: sku_map if str(path).endswith("sku_map.json") else candidate, + ) + return cases + + +def test_binding_impact_gate_requires_every_target_to_clear_combined_error(monkeypatch): + m = _module() + hq = m.resolve_op("HQResize") + cases = _binding_impact_inputs(monkeypatch, m, reduction_us=10.0, noise_us=100.0) + + finding = m._binding_impact_gate( + hq, + "summary", + ["python/mod_cvcuda/operators/OpHQResize.cpp"], + "guide", + ) + + expected_error = math.sqrt(2 * (2.0**2) / 5) + assert finding.status == "PASS" + assert f"{expected_error:.2f} us" in finding.evidence + assert "paired-gap combined SE" in finding.evidence + assert all(case.split("[", 1)[0] in finding.evidence for case in cases) + + +def test_binding_impact_gate_rejects_claim_within_paired_gap_error(monkeypatch): + m = _module() + hq = m.resolve_op("HQResize") + _binding_impact_inputs(monkeypatch, m, reduction_us=1.0) + + finding = m._binding_impact_gate( + hq, + "summary", + ["python/mod_cvcuda/operators/OpHQResize.cpp"], + "guide", + ) + + assert finding.status == "GAP" + assert "does not clear" in finding.evidence + + +def test_binding_impact_gate_requires_paired_gap_dispersion(monkeypatch): + m = _module() + hq = m.resolve_op("HQResize") + _binding_impact_inputs( + monkeypatch, + m, + reduction_us=10.0, + include_gap_stddev=False, + ) + + finding = m._binding_impact_gate( + hq, + "summary", + ["python/mod_cvcuda/operators/OpHQResize.cpp"], + "guide", + ) + + assert finding.status == "GAP" + assert "paired-gap-dispersion" in finding.evidence + + +def test_binding_impact_gate_requires_repeated_artifacts(monkeypatch): + m = _module() + hq = m.resolve_op("HQResize") + _binding_impact_inputs(monkeypatch, m, reduction_us=10.0, n_runs=1) + + finding = m._binding_impact_gate( + hq, + "summary", + ["python/mod_cvcuda/operators/OpHQResize.cpp"], + "guide", + ) + + assert finding.status == "GAP" + assert "paired-gap-dispersion" in finding.evidence + + +def test_binding_impact_gate_accepts_legacy_summary_metric(monkeypatch): + m = _module() + hq = m.resolve_op("HQResize") + cases = _binding_impact_inputs(monkeypatch, m, reduction_us=10.0) + monkeypatch.setattr( + m, + "parse_summary", + lambda _results: SimpleNamespace( + metadata=SimpleNamespace( + impact_metric="cpp_time", + baseline_commit="a" * 40, + optimized_cases=cases, + ) + ), + ) + + finding = m._binding_impact_gate( + hq, + "summary", + ["python/mod_cvcuda/operators/OpHQResize.cpp"], + "guide", + ) + + assert finding.status == "PASS" + assert "paired-gap combined standard error" in finding.summary + + +def _binding_source(signature=None, registration=None, body=None, aliases=""): + if signature is None: + signature = "Tensor TensorHQResize(Tensor &src, std::optional stream)" + if registration is None: + registration = """ m.def("hq_resize", &TensorHQResize, "src"_a, py::kw_only(), + "stream"_a = nullptr);""" + if body is None: + body = " return src;" + return f"""namespace cvcudapy {{ +namespace {{ + +{aliases} +{signature} +{{ +{body} +}} + +}} // namespace + +void ExportOpHQResize(py::module &m) +{{ +{registration} +}} + +}} // namespace cvcudapy +""" + + +def test_binding_api_snapshot_recognizes_direct_and_nvtx_wrapped_callables(): + m = _module() + signature = "TensorBatch TensorBatchHQResize(TensorBatch &src)" + + for callable_arg in ( + "&TensorBatchHQResize", + 'NvtxTrace("cvcuda.hq_resize", &TensorBatchHQResize)', + ): + source = _binding_source( + signature=signature, + registration=f' m.def("hq_resize", {callable_arg}, "src"_a);', + ) + + snapshot = m._binding_api_snapshot(source, "HQResize") + + assert snapshot is not None, callable_arg + assert snapshot[1] == (("TensorBatchHQResize", signature),) + + +def test_api_abi_referenced_transitive_using_alias_change_is_a_gap(monkeypatch): + m = _module() + hq = m.resolve_op("HQResize") + signature = "Tensor TensorHQResize(Tensor &src, Rois rois)" + baseline = _binding_source( + signature=signature, + aliases="using Roi = pybind11::tuple;\nusing Rois = std::vector;", + ) + candidate = baseline.replace( + "using Roi = pybind11::tuple;", "using Roi = pybind11::list;" + ) + _mock_api_abi_sources( + monkeypatch, + m, + baseline, + candidate, + "-using Roi = pybind11::tuple;\n+using Roi = pybind11::list;", + ) + + result = m._api_abi(hq, "origin/main", "guide") + + assert result.status == "GAP" + assert "type alias" in result.evidence + + +def test_api_abi_referenced_typedef_change_is_a_gap(monkeypatch): + m = _module() + hq = m.resolve_op("HQResize") + signature = "Tensor TensorHQResize(Tensor &src, Roi roi)" + baseline = _binding_source( + signature=signature, aliases="typedef pybind11::tuple Roi;" + ) + candidate = baseline.replace("pybind11::tuple", "pybind11::list") + _mock_api_abi_sources( + monkeypatch, + m, + baseline, + candidate, + "-typedef pybind11::tuple Roi;\n+typedef pybind11::list Roi;", + ) + + result = m._api_abi(hq, "origin/main", "guide") + + assert result.status == "GAP" + assert "type alias" in result.evidence + + +def test_api_abi_unused_private_alias_change_is_implementation_only(monkeypatch): + m = _module() + hq = m.resolve_op("HQResize") + baseline = _binding_source(aliases="using Scratch = pybind11::tuple;") + candidate = baseline.replace("pybind11::tuple", "pybind11::list") + _mock_api_abi_sources( + monkeypatch, + m, + baseline, + candidate, + "-using Scratch = pybind11::tuple;\n+using Scratch = pybind11::list;", + ) + + result = m._api_abi(hq, "origin/main", "guide") + + assert result.status == "PASS" + assert "binding implementation only" in result.summary + + +def test_api_abi_ambiguous_referenced_alias_requires_manual_review(monkeypatch): + m = _module() + hq = m.resolve_op("HQResize") + aliases = "using Roi = pybind11::tuple;\nusing Roi = pybind11::list;" + baseline = _binding_source( + signature="Tensor TensorHQResize(Tensor &src, Roi roi)", aliases=aliases + ) + candidate = baseline.replace("return src;", "return Tensor(src);") + _mock_api_abi_sources( + monkeypatch, + m, + baseline, + candidate, + "+ return Tensor(src);", + ) + + result = m._api_abi(hq, "origin/main", "guide") + + assert result.status == "MANUAL" + assert "could not be parsed safely" in result.summary + + +def test_api_abi_unparseable_referenced_typedef_requires_manual_review(monkeypatch): + m = _module() + hq = m.resolve_op("HQResize") + baseline = _binding_source( + signature="Tensor TensorHQResize(Tensor &src, Roi roi)", + aliases="typedef void (*Roi)(int);", + ) + candidate = baseline.replace("return src;", "return Tensor(src);") + _mock_api_abi_sources( + monkeypatch, + m, + baseline, + candidate, + "+ return Tensor(src);", + ) + + result = m._api_abi(hq, "origin/main", "guide") + + assert result.status == "MANUAL" + assert "could not be parsed safely" in result.summary + + +def test_api_abi_unsupported_referenced_typedefs_require_manual_review(monkeypatch): + m = _module() + hq = m.resolve_op("HQResize") + + for typedef in ( + "typedef pybind11::tuple Roi[2];", + "typedef pybind11::tuple Roi(int);", + "typedef pybind11::tuple (Owner::*Roi)(int);", + "typedef pybind11::tuple Roi[2], Other;", + "typedef struct { int value; } Roi;", + ): + baseline = _binding_source( + signature="Tensor TensorHQResize(Tensor &src, Roi roi)", + aliases=typedef, + ) + candidate = baseline.replace("return src;", "return Tensor(src);") + _mock_api_abi_sources( + monkeypatch, + m, + baseline, + candidate, + "+ return Tensor(src);", + ) + + result = m._api_abi(hq, "origin/main", "guide") + + assert result.status == "MANUAL", typedef + assert "could not be parsed safely" in result.summary + + +def test_api_abi_unsupported_private_typedef_respects_name_reachability(monkeypatch): + m = _module() + hq = m.resolve_op("HQResize") + + for typedef, status, summary in ( + ( + "typedef pybind11::tuple Scratch[2];", + "PASS", + "binding implementation only", + ), + ( + "typedef struct { int value; } Scratch;", + "MANUAL", + "could not be parsed safely", + ), + ): + baseline = _binding_source(aliases=typedef) + candidate = baseline.replace("return src;", "return Tensor(src);") + _mock_api_abi_sources( + monkeypatch, + m, + baseline, + candidate, + "+ return Tensor(src);", + ) + + result = m._api_abi(hq, "origin/main", "guide") + + assert result.status == status, typedef + assert summary in result.summary + + +def _mock_api_abi_sources(monkeypatch, module, baseline, candidate, diff_line): + merge_base = "a" * 40 + + def fake_git(*args): + if args[0] == "diff": + return diff_line if args[-1].endswith("OpHQResize.cpp") else "" + if args[0] == "merge-base": + return merge_base + "\n" + if args[0] == "show" and args[1].startswith(merge_base + ":"): + return baseline + if args[0] == "show" and args[1].startswith("HEAD:"): + return candidate + return "" + + monkeypatch.setattr(module, "git", fake_git) + + +def test_api_abi_binding_implementation_only_is_automatic_pass(monkeypatch): + m = _module() + hq = m.resolve_op("HQResize") + baseline = _binding_source() + candidate = _binding_source(body=" RequirementsCache cache;\n return src;") + _mock_api_abi_sources( + monkeypatch, + m, + baseline, + candidate, + "+ RequirementsCache cache;", + ) + + result = m._api_abi(hq, "origin/main", "guide") + + assert result.status == "PASS" + assert "binding implementation only" in result.summary + + +def test_api_abi_binding_export_change_remains_gap(monkeypatch): + m = _module() + hq = m.resolve_op("HQResize") + baseline = _binding_source() + candidate = _binding_source().replace('m.def("hq_resize"', 'm.def("hq_resize_new"') + _mock_api_abi_sources( + monkeypatch, + m, + baseline, + candidate, + '- m.def("hq_resize", &TensorHQResize);\n' + '+ m.def("hq_resize_new", &TensorHQResize);', + ) + + result = m._api_abi(hq, "origin/main", "guide") + + assert result.status == "GAP" + assert "m.def registration" in result.evidence + + +def test_api_abi_binding_multiline_argument_change_remains_gap(monkeypatch): + m = _module() + hq = m.resolve_op("HQResize") + baseline = _binding_source() + candidate = baseline.replace('"stream"_a = nullptr', '"priority"_a = 0') + _mock_api_abi_sources( + monkeypatch, + m, + baseline, + candidate, + '- "stream"_a = nullptr);\n+ "priority"_a = 0);', + ) + + result = m._api_abi(hq, "origin/main", "guide") + + assert result.status == "GAP" + assert "m.def registration" in result.evidence + + +def test_api_abi_bound_callable_signature_change_remains_gap(monkeypatch): + m = _module() + hq = m.resolve_op("HQResize") + baseline = _binding_source() + candidate = _binding_source( + signature="Tensor TensorHQResize(const Tensor &src, std::optional stream)" + ) + _mock_api_abi_sources( + monkeypatch, + m, + baseline, + candidate, + "-Tensor TensorHQResize(Tensor &src, std::optional stream)\n" + "+Tensor TensorHQResize(const Tensor &src, std::optional stream)", + ) + + result = m._api_abi(hq, "origin/main", "guide") + + assert result.status == "GAP" + assert "bound callable signature" in result.evidence + + +def test_api_abi_unparseable_binding_surface_requires_manual_review(monkeypatch): + m = _module() + hq = m.resolve_op("HQResize") + baseline = _binding_source() + candidate = baseline.replace("ExportOpHQResize", "RegisterHQResize") + _mock_api_abi_sources( + monkeypatch, + m, + baseline, + candidate, + "-void ExportOpHQResize(py::module &m)\n+void RegisterHQResize(py::module &m)", + ) + + result = m._api_abi(hq, "origin/main", "guide") + + assert result.status == "MANUAL" + assert "could not be parsed safely" in result.summary + + +# ---- ODO-6 commit hygiene -------------------------------------------------------------- +def _init_git_history(repo): + repo.mkdir() + subprocess.run(["git", "init", "-q"], cwd=repo, check=True) + subprocess.run( + ["git", "config", "user.email", "optimize-op-test@nvidia.com"], + cwd=repo, + check=True, + ) + subprocess.run( + ["git", "config", "user.name", "Optimize Op Test"], + cwd=repo, + check=True, + ) + _commit(repo, "chore: establish test base", {"README.md": "base\n"}) + return subprocess.run( + ["git", "rev-parse", "HEAD"], + cwd=repo, + check=True, + capture_output=True, + text=True, + ).stdout.strip() + + +def _commit(repo, subject, files): + for name, contents in files.items(): + path = repo / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(contents) + subprocess.run(["git", "add", "--all"], cwd=repo, check=True) + subprocess.run(["git", "commit", "-q", "-m", subject], cwd=repo, check=True) + + +def test_perf_hygiene_accepts_scoped_regression_backed_fix(monkeypatch, tmp_path): + m = _module() + boxblur = m.resolve_op("BoxBlur") + repo = tmp_path / "paired" + base = _init_git_history(repo) + _commit( + repo, + "test(boxblur): cover signed negative pixels", + {"tests/cvcuda/system/TestOpBoxBlur.cpp": "regression\n"}, + ) + _commit( + repo, + "fix(boxblur): honor signed 8-bit pixels", + {"src/cvcuda/priv/legacy/box_blur.cu": "fix\n"}, + ) + monkeypatch.setattr(m, "REPO", repo) + + result = m._perf_hygiene(boxblur, base, ["boxblur"], "guide") + + assert result.status == "PASS" + assert "regression-backed fix" in result.evidence + + +def test_perf_hygiene_rejects_unpaired_or_disguised_fix(monkeypatch, tmp_path): + cases = ( + ( + "blanket", + "test(boxblur): cover signed negative pixels", + {"tests/cvcuda/system/TestOpBoxBlur.cpp": "regression\n"}, + "fix: honor signed 8-bit pixels", + ), + ( + "unpaired", + None, + {}, + "fix(boxblur): honor signed 8-bit pixels", + ), + ( + "mismatched-scope", + "test(resize): cover signed negative pixels", + {"tests/cvcuda/system/TestOpResize.cpp": "regression\n"}, + "fix(boxblur): honor signed 8-bit pixels", + ), + ( + "wrong-operator-test", + "test(boxblur): cover signed negative pixels", + {"tests/cvcuda/system/TestOpResize.cpp": "regression\n"}, + "fix(boxblur): honor signed 8-bit pixels", + ), + ( + "not-test-only", + "test(boxblur): cover signed negative pixels", + { + "tests/cvcuda/system/TestOpBoxBlur.cpp": "regression\n", + "bench/config/operators/boxblur.json": "{}\n", + }, + "fix(boxblur): optimize staging", + ), + ) + for name, test_subject, test_files, fix_subject in cases: + m = _module() + boxblur = m.resolve_op("BoxBlur") + repo = tmp_path / name + base = _init_git_history(repo) + if test_subject: + _commit(repo, test_subject, test_files) + _commit( + repo, + fix_subject, + {"src/cvcuda/priv/legacy/box_blur.cu": "implementation\n"}, + ) + monkeypatch.setattr(m, "REPO", repo) + + result = m._perf_hygiene(boxblur, base, ["boxblur", "resize"], "guide") + + assert result.status == "GAP", name + assert "fix" in result.evidence, name + + +# ---- results-summary format enforcement (base-independent) ------------------------------ +def test_results_missing_sections_is_gap(tmp_path): + rf = tmp_path / "res.md" + rf.write_text("## Overall table\nspeedup\n## Pixelwise-equality\nEXPECT_EQ\n") + r = run("Resize", "--phase", "evidence", "--results", str(rf), "--format", "json") + odo3 = [f for f in json.loads(r.stdout)["findings"] if f["id"] == "ODO-3"][0] + assert odo3["status"] == "GAP" + + +def test_results_absent_is_gap(): + r = run("Resize", "--phase", "evidence", "--format", "json") + odo3 = [f for f in json.loads(r.stdout)["findings"] if f["id"] == "ODO-3"][0] + assert odo3["status"] == "GAP" + + +def test_results_declared_baseline_config_unavailable_is_precise_gap(monkeypatch): + m = _module() + resize = m.resolve_op("Resize") + baseline_commit = "1" * 40 + candidate_commit = "2" * 40 + summary = SimpleNamespace( + metadata=SimpleNamespace( + baseline_commit=baseline_commit, + candidate_commit=candidate_commit, + ) + ) + monkeypatch.setattr(m, "parse_summary", lambda _results: summary) + monkeypatch.setattr(m, "git_success", lambda *_args: True) + monkeypatch.setattr(m, "git_json", lambda *_args: None) + + finding = m._results_format( + resize, + "v1 summary", + Path("description.md"), + "guideline", + {}, + ) + + assert finding.id == "ODO-3" + assert finding.status == "GAP" + assert finding.summary == "Declared baseline benchmark config cannot be loaded" + assert baseline_commit in finding.evidence + assert "bench/config/operators/resize.json" in finding.evidence + + +# ---- ODO-4 lead-exhaustion hard evidence ------------------------------------------------ +def test_lead_exhaustion_requires_checked_measured_evidence(): + m = _module() + unchecked = "- [ ] **Lead exhaustion** — ncu showed 93% Memory SOL at ridge.\n" + unmeasured = ( + "- [x] **Lead exhaustion** — profiling says the implementation is at ridge.\n" + ) + assert m._leads_exhausted(unchecked, "guide").status == "GAP" + assert m._leads_exhausted(unmeasured, "guide").status == "GAP" + + +def test_lead_exhaustion_accepts_ridge_or_three_measured_strikes(): + m = _module() + ridge = "- [x] **Lead exhaustion** — ncu showed 93% Memory SOL at ridge; PASS.\n" + strikes = ( + "- [x] **Lead exhaustion** — three failed leads measured at -2.1%, " + "-0.8%, and 0.1%; PASS.\n" + ) + assert m._leads_exhausted(ridge, "guide").status == "PASS" + assert m._leads_exhausted(strikes, "guide").status == "PASS" + + +# ---- ODO-8 review/refactor lock-in gate evidence ---------------------------------------- +def test_review_refactor_gate_requires_assess_evidence(): + m = _module() + resize = m.resolve_op("Resize") + result = m._review_refactor_gate( + resize, + "- [x] **Review/refactor gate** — reviewed changed implementation; PASS.\n", + ["src/cvcuda/priv/OpResize.cu"], + "guide", + ) + assert result.status == "GAP" + assert "refactor_op.py" in result.fix + + +def test_review_refactor_gate_passes_with_clean_assess_result(): + m = _module() + resize = m.resolve_op("Resize") + results = ( + "- [x] **Review/refactor gate** — reviewed changed implementation; " + "tools/refactor_op.py Resize --phase assess PASS with 0 recommendations; " + "no refactor applied.\n" + ) + result = m._review_refactor_gate( + resize, results, ["src/cvcuda/priv/OpResize.cu"], "guide" + ) + assert result.status == "PASS" + + +def test_review_refactor_gate_requires_verify_tests_and_bench_for_applied_refactor(): + m = _module() + resize = m.resolve_op("Resize") + missing = ( + "- [x] **Review/refactor gate** — reviewed changed implementation; " + "tools/refactor_op.py Resize --phase assess RECOMMENDATION RED-1; " + "Refactor applied: yes.\n" + ) + result = m._review_refactor_gate( + resize, missing, ["src/cvcuda/priv/OpResize.cu"], "guide" + ) + assert result.status == "GAP" + assert "--phase verify" in result.evidence + assert "frozen operator tests" in result.evidence + assert "post-refactor benchmark proof" in result.evidence + + complete = ( + "- [x] **Review/refactor gate** — reviewed changed implementation and fixed RED-1; " + "tools/refactor_op.py Resize --phase assess RECOMMENDATION RED-1; " + "Refactor applied: yes; tools/refactor_op.py Resize --phase verify PASS; " + "cvcuda_test_system tests passed; run_bench.py benchmark passed with no regression.\n" + ) + result = m._review_refactor_gate( + resize, complete, ["src/cvcuda/priv/OpResize.cu"], "guide" + ) + assert result.status == "PASS" + + +# ---- ODO-9 memory-footprint growth gate ----------------------------------------------- +def _memory_results(increase=0, allocations="no", evidence=None): + if evidence is None: + evidence = ( + "Measured aggregate peak-live added bytes across changed memory paths." + ) + return f"""## Memory footprint +Peak attributable increase: {increase} B +New runtime CUDA allocations/frees: {allocations} +Evidence: {evidence} +""" + + +def test_memory_footprint_gate_accepts_zero_and_ten_mb_boundaries(): + m = _module() + for increase in (0, 9_999_999, 10_000_000): + result = m._memory_footprint_gate( + _memory_results(increase), ["src/cvcuda/priv/OpResize.cu"], "guide" + ) + assert result.status == "PASS", increase + + +def test_memory_footprint_gate_requires_review_above_ten_mb(): + m = _module() + result = m._memory_footprint_gate(_memory_results(10_000_001), [], "guide") + assert result.status == "MANUAL" + assert "10000001 B exceeds" in result.evidence + + +def test_memory_footprint_gate_requires_review_for_new_cuda_allocation_path(): + m = _module() + for increase in (0, 10_000_000): + result = m._memory_footprint_gate( + _memory_results(increase, allocations="yes"), [], "guide" + ) + assert result.status == "MANUAL", increase + assert "allocation/free path" in result.evidence + + +def test_memory_footprint_gate_rejects_missing_fields(): + m = _module() + lines = _memory_results().splitlines() + for label in ( + "Peak attributable increase:", + "New runtime CUDA allocations/frees:", + "Evidence:", + ): + results = "\n".join(line for line in lines if not line.startswith(label)) + result = m._memory_footprint_gate(results, [], "guide") + assert result.status == "GAP", label + assert "missing, duplicated, or malformed" in result.summary + + +def test_memory_footprint_gate_rejects_duplicate_fields(): + m = _module() + duplicates = ( + "Peak attributable increase: 0 B", + "New runtime CUDA allocations/frees: no", + "Evidence: Independently inspected the changed source.", + ) + for duplicate in duplicates: + result = m._memory_footprint_gate( + _memory_results() + duplicate + "\n", [], "guide" + ) + assert result.status == "GAP", duplicate + assert "found 2 occurrences" in result.evidence + + +def test_memory_footprint_gate_rejects_malformed_increase(): + m = _module() + for value in ("-1 B", "1.5 B", "1 KiB", "1", "9" * 5000 + " B"): + results = _memory_results().replace( + "Peak attributable increase: 0 B", + "Peak attributable increase: " + value, + ) + result = m._memory_footprint_gate(results, [], "guide") + assert result.status == "GAP", value + assert "malformed declaration" in result.evidence + + +def test_memory_footprint_gate_rejects_malformed_allocation_flag(): + m = _module() + for value in ("No", "maybe", "false"): + result = m._memory_footprint_gate( + _memory_results(allocations=value), [], "guide" + ) + assert result.status == "GAP", value + + +def test_memory_footprint_gate_rejects_placeholder_evidence_before_manual(): + m = _module() + for evidence in ( + "", + "TBD", + "pending", + "measurement pending", + "n/a", + "", + "\nSupporting prose must not fill a blank declaration.", + ): + result = m._memory_footprint_gate( + _memory_results(10_000_001, allocations="yes", evidence=evidence), + [], + "guide", + ) + assert result.status == "GAP", evidence + + +def test_memory_footprint_gate_allows_substantive_evidence_with_placeholder_words(): + m = _module() + result = m._memory_footprint_gate( + _memory_results( + evidence="Code inspection found no pending CUDA allocation/free paths." + ), + [], + "guide", + ) + assert result.status == "PASS" + + +def test_memory_footprint_gate_hidden_fields_do_not_satisfy_declarations(): + m = _module() + fields = _memory_results().removeprefix("## Memory footprint\n") + for hidden in ( + "", + "\n" + result = m._memory_footprint_gate(results, [], "guide") + assert result.status == "PASS" + + +def test_memory_footprint_gate_ignores_hidden_or_fenced_headings(): + m = _module() + for results in ( + "", + "